개념
하나의 뉴런, 하나의 결정
퍼셉트론은 n개의 입력을 받아 각 입력에 가중치를 곱하고, 모두 더한 뒤, 편향을 더하고, 그 결과를 활성화 함수(activation function)에 통과시킵니다.
graph LR
x1["x1"] -- "w1" --> sum["Σ(wi*xi) + b"]
x2["x2"] -- "w2" --> sum
x3["x3"] -- "w3" --> sum
bias["bias"] --> sum
sum --> step["step(z)"]
step --> out["output (0 또는 1)"]
계단 함수(step function)는 매우 단순합니다. 가중합(weighted sum)에 편향을 더한 값이 0 이상이면 1을 출력하고, 그렇지 않으면 0을 출력합니다.
step(z) = 1 if z >= 0
0 if z < 0
이것은 선형 분류기(linear classifier)입니다. 가중치와 편향은 입력 공간을 두 영역으로 나누는 직선, 더 높은 차원에서는 초평면(hyperplane)을 정의합니다.
결정 경계
입력이 두 개라면 퍼셉트론은 2차원 공간에 직선을 그립니다.
x2
┤
│ Class 1 /
│ (0) /
│ /
│ / w1·x1 + w2·x2 + b = 0
│ /
│ / Class 2
│ / (1)
┼───────────/──────────── x1
직선의 한쪽은 0을 출력하고, 다른 쪽은 1을 출력합니다. 학습은 이 직선을 옮기면서 클래스(class)를 올바르게 나누도록 만드는 과정입니다.
학습 규칙
퍼셉트론 학습 규칙(perceptron learning rule)은 단순합니다.
각 학습 예제 (x, y_true)에 대해:
y_pred = predict(x)
error = y_true - y_pred
각 가중치에 대해:
w_i = w_i + learning_rate * error * x_i
bias = bias + learning_rate * error
예측이 맞으면 error는 0이고 아무것도 바뀌지 않습니다. 0이라고 예측했지만 실제로는 1이어야 하면 가중치가 증가합니다. 1이라고 예측했지만 실제로는 0이어야 하면 가중치가 감소합니다. 학습률(learning rate)은 각 조정의 크기를 제어합니다.
XOR 문제
여기서 한계가 드러납니다. 아래 논리 게이트(logic gate)를 봅니다.
AND gate: OR gate: XOR gate:
x1 x2 out x1 x2 out x1 x2 out
0 0 0 0 0 0 0 0 0
0 1 0 0 1 1 0 1 1
1 0 0 1 0 1 1 0 1
1 1 1 1 1 1 1 1 0
AND와 OR은 선형 분리 가능합니다. 0과 1을 나누는 단일 직선을 그을 수 있습니다. XOR은 그렇지 않습니다. [0, 1], [1, 0]을 [0, 0], [1, 1]과 나누는 단일 직선은 없습니다.
AND (분리 가능): XOR (분리 불가능):
x2 x2
1 ┤ 0 1 1 ┤ 1 0
│ / │
0 ┤ 0 / 0 0 ┤ 0 1
┼──/──────── x1 ┼──────────── x1
직선 하나로 가능 직선 하나로 불가능
이것은 근본적인 한계입니다. 단일 퍼셉트론은 선형 분리 가능한 문제만 풀 수 있습니다. 민스키와 페퍼트(Minsky and Papert)는 1969년에 이 한계를 증명했고, 이 결과는 한동안 신경망 연구를 크게 위축시켰습니다.
해결 방법은 퍼셉트론을 층으로 쌓는 것입니다. 다층 퍼셉트론은 두 개 이상의 선형 결정을 조합해 비선형 결정(nonlinear decision)을 만들 수 있습니다.
만들어 보기
Step 1: Perceptron 클래스
class Perceptron:
def __init__(self, n_inputs, learning_rate=0.1):
self.weights = [0.0] * n_inputs
self.bias = 0.0
self.lr = learning_rate
def predict(self, inputs):
total = sum(w * x for w, x in zip(self.weights, inputs))
total += self.bias
return 1 if total >= 0 else 0
def train(self, training_data, epochs=100):
for epoch in range(epochs):
errors = 0
for inputs, target in training_data:
prediction = self.predict(inputs)
error = target - prediction
if error != 0:
errors += 1
for i in range(len(self.weights)):
self.weights[i] += self.lr * error * inputs[i]
self.bias += self.lr * error
if errors == 0:
print(f"{epoch + 1}번째 epoch에서 수렴했습니다.")
return
print(f"{epochs}번의 epoch 이후에도 수렴하지 않았습니다.")
Step 2: 논리 게이트 학습
and_data = [
([0, 0], 0),
([0, 1], 0),
([1, 0], 0),
([1, 1], 1),
]
or_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 1),
]
not_data = [
([0], 1),
([1], 0),
]
print("=== AND Gate ===")
p_and = Perceptron(2)
p_and.train(and_data)
for inputs, _ in and_data:
print(f" {inputs} -> {p_and.predict(inputs)}")
print("\n=== OR Gate ===")
p_or = Perceptron(2)
p_or.train(or_data)
for inputs, _ in or_data:
print(f" {inputs} -> {p_or.predict(inputs)}")
print("\n=== NOT Gate ===")
p_not = Perceptron(1)
p_not.train(not_data)
for inputs, _ in not_data:
print(f" {inputs} -> {p_not.predict(inputs)}")
선형 분리 가능한 게이트는 퍼셉트론이 수렴합니다.
Step 3: XOR 실패 확인
xor_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 0),
]
print("\n=== XOR Gate (single perceptron) ===")
p_xor = Perceptron(2)
p_xor.train(xor_data, epochs=1000)
for inputs, expected in xor_data:
result = p_xor.predict(inputs)
status = "정답" if result == expected else "오답"
print(f" {inputs} -> {result} (기댓값 {expected}) {status}")
이 모델은 수렴하지 않습니다. 단일 퍼셉트론이 XOR을 학습할 수 없다는 것을 직접 보여 주는 예입니다.
Step 4: 두 개의 층으로 XOR 풀기
핵심은 XOR = (x1 OR x2) AND NOT (x1 AND x2)입니다. 세 개의 퍼셉트론을 조합합니다.
graph LR
x1["x1"] --> OR["OR neuron"]
x1 --> NAND["NAND neuron"]
x2["x2"] --> OR
x2 --> NAND
OR --> AND["AND neuron"]
NAND --> AND
AND --> out["output"]
def xor_network(x1, x2):
or_neuron = Perceptron(2)
or_neuron.weights = [1.0, 1.0]
or_neuron.bias = -0.5
nand_neuron = Perceptron(2)
nand_neuron.weights = [-1.0, -1.0]
nand_neuron.bias = 1.5
and_neuron = Perceptron(2)
and_neuron.weights = [1.0, 1.0]
and_neuron.bias = -1.5
hidden1 = or_neuron.predict([x1, x2])
hidden2 = nand_neuron.predict([x1, x2])
output = and_neuron.predict([hidden1, hidden2])
return output
print("\n=== XOR Gate (multi-layer network) ===")
for inputs, expected in xor_data:
result = xor_network(inputs[0], inputs[1])
print(f" {inputs} -> {result} (기댓값 {expected})")
네 가지 경우가 모두 맞습니다. 퍼셉트론을 층으로 쌓으면 단일 퍼셉트론으로 만들 수 없는 결정 경계를 만들 수 있습니다.
Step 5: 2층 네트워크 학습
Step 4는 가중치를 손으로 정했습니다. XOR에서는 가능하지만, 실제 문제에서는 올바른 가중치를 미리 알 수 없습니다. 계단 함수 대신 시그모이드(sigmoid)를 사용하고, 역전파(backpropagation)로 가중치를 자동 학습합니다.
class TwoLayerNetwork:
def __init__(self, learning_rate=0.5):
import random
random.seed(0)
self.w_hidden = [[random.uniform(-1, 1), random.uniform(-1, 1)] for _ in range(2)]
self.b_hidden = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.w_output = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.b_output = random.uniform(-1, 1)
self.lr = learning_rate
def sigmoid(self, x):
import math
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def forward(self, inputs):
self.inputs = inputs
self.hidden_outputs = []
for i in range(2):
z = sum(w * x for w, x in zip(self.w_hidden[i], inputs)) + self.b_hidden[i]
self.hidden_outputs.append(self.sigmoid(z))
z_out = sum(w * h for w, h in zip(self.w_output, self.hidden_outputs)) + self.b_output
self.output = self.sigmoid(z_out)
return self.output
def train(self, training_data, epochs=10000):
for epoch in range(epochs):
total_error = 0
for inputs, target in training_data:
output = self.forward(inputs)
error = target - output
total_error += error ** 2
d_output = error * output * (1 - output)
saved_w_output = self.w_output[:]
hidden_deltas = []
for i in range(2):
h = self.hidden_outputs[i]
hd = d_output * saved_w_output[i] * h * (1 - h)
hidden_deltas.append(hd)
for i in range(2):
self.w_output[i] += self.lr * d_output * self.hidden_outputs[i]
self.b_output += self.lr * d_output
for i in range(2):
for j in range(len(inputs)):
self.w_hidden[i][j] += self.lr * hidden_deltas[i] * inputs[j]
self.b_hidden[i] += self.lr * hidden_deltas[i]
net = TwoLayerNetwork(learning_rate=2.0)
net.train(xor_data, epochs=10000)
for inputs, expected in xor_data:
result = net.forward(inputs)
predicted = 1 if result >= 0.5 else 0
print(f" {inputs} -> {result:.4f} (반올림: {predicted}, 기댓값 {expected})")
Step 4와 중요한 차이가 두 가지 있습니다. 첫째, 계단 함수 대신 매끄러운 시그모이드를 사용하므로 기울기(gradient)가 존재합니다. 둘째, train 메서드(method)는 출력층(output layer)의 오차를 은닉층(hidden layer)으로 거꾸로 전파해 각 가중치가 오차에 기여한 정도에 비례해 조정합니다. 이것이 20줄짜리 역전파입니다.
이 내용은 Lesson 03으로 이어집니다. d_output과 hidden_deltas 뒤에 있는 수학은 네트워크 그래프에 연쇄 법칙(chain rule)을 적용한 것입니다. 그곳에서 제대로 유도합니다.
사용하기
방금 직접 만든 것은 실제로는 한 줄 import로 사용할 수 있습니다.
from sklearn.linear_model import Perceptron as SkPerceptron
import numpy as np
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 0, 0, 1])
clf = SkPerceptron(max_iter=100, tol=1e-3)
clf.fit(X, y)
print([clf.predict([x])[0] for x in X])
다섯 줄입니다. 여러분의 30줄짜리 Perceptron 클래스와 핵심은 같습니다. sklearn 버전은 수렴 검사, 여러 손실 함수(loss function), 희소 입력(sparse input) 지원을 더하지만 핵심 루프(loop)는 같습니다. 가중합, 계단 함수, 오차가 있을 때 가중치 업데이트입니다.
생산 환경의 네트워크에서 달라지는 점은 다음과 같습니다.
- 계단 함수는 시그모이드, ReLU, 또는 다른 매끄러운 활성화 함수로 바뀝니다.
- 가중치는 역전파로 자동 학습됩니다.
- 층은 더 깊어집니다. 3층, 10층, 100층 이상도 가능합니다.
- 원리는 같습니다. 각 층은 이전 층의 출력에서 새로운 특성(feature)을 만듭니다.
단일 퍼셉트론은 직선만 그릴 수 있습니다. 퍼셉트론을 쌓으면 어떤 모양의 경계도 그릴 수 있습니다.
산출물 만들기
이 lesson의 산출물은 다음입니다.
outputs/skill-perceptron.md: 단일층과 다층 구조가 각각 언제 필요한지 판단하는 스킬(skill)
연습문제
- (쉬움) NAND 게이트에서 퍼셉트론을 학습합니다. NAND는 보편 게이트(universal gate)이므로 모든 논리 회로를 만들 수 있습니다. 학습된 가중치와 편향이 유효한 결정 경계를 이루는지 확인합니다.
- (중간)
Perceptron 클래스를 수정해 각 에포크(epoch)마다 결정 경계 w1*x1 + w2*x2 + b = 0을 추적합니다. AND 게이트 학습 중 직선이 어떻게 이동하는지 출력합니다.
- (어려움) 세 입력 중 최소 두 개가 1일 때만 1을 출력하는 3입력 퍼셉트론, 즉 다수결 함수(majority vote function)를 만듭니다. 이 문제는 선형 분리 가능한가요? 이유는 무엇인가요?
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 퍼셉트론(Perceptron) | "가짜 뉴런" | 입력과 가중치의 내적에 편향을 더하고 계단 함수를 통과시키는 선형 분류기 |
| 가중치(Weight) | "입력이 얼마나 중요한지" | 각 입력이 결정에 기여하는 크기를 조절하는 배율(multiplier) |
| 편향(Bias) | "문턱값" | 결정 경계를 이동시키는 상수항 |
| 활성화 함수(Activation function) | "값을 눌러 주는 함수" | 가중합 뒤에 적용되는 함수. 퍼셉트론에서는 계단 함수, 현대 네트워크에서는 시그모이드(sigmoid)/ReLU 등을 사용 |
| 선형 분리 가능(Linearly separable) | "선을 그어 나눌 수 있음" | 단일 초평면으로 클래스를 완벽히 나눌 수 있는 데이터 |
| XOR 문제(XOR problem) | "퍼셉트론이 못 푸는 문제" | 단일층 네트워크가 비선형 분리 문제를 학습할 수 없음을 보여 주는 사례 |
| 결정 경계(Decision boundary) | "분류기가 바뀌는 위치" | 입력 공간을 두 클래스로 나누는 w*x + b = 0 초평면 |
| 다층 퍼셉트론(Multi-layer perceptron) | "진짜 신경망" | 퍼셉트론을 여러 층으로 쌓아 이전 층의 출력을 다음 층 입력으로 사용하는 구조 |
더 읽을거리