퍼셉트론(The Perceptron)

퍼셉트론(Perceptron)은 인공신경망(Neural Network)의 원자 같은 단위입니다. 그 안을 열어 보면 가중치(weights), 편향(bias), 그리고 하나의 결정(decision)이 있습니다.

유형: Build

언어: Python

선수 지식: Phase 1 선형대수 직관(Linear Algebra Intuition)

소요 시간: 약 60분

학습 목표

  • Python으로 퍼셉트론(Perceptron)을 처음부터 구현하고, 가중치 업데이트 규칙(weight update rule)과 계단 활성화 함수(step activation function)를 설명합니다.
  • 단일 퍼셉트론이 선형 분리 가능(linearly separable)한 문제만 풀 수 있는 이유를 설명하고 XOR 실패 사례를 직접 확인합니다.
  • OR, NAND, AND 게이트를 조합해 XOR을 푸는 다층 퍼셉트론(Multi-Layer Perceptron)을 구성합니다.
  • 시그모이드 활성화 함수(sigmoid activation)와 역전파(backpropagation)를 사용하는 2층 네트워크를 학습시켜 XOR을 자동으로 학습합니다.

문제

여러분은 벡터(vector)와 내적(dot product)을 알고 있습니다. 행렬(matrix)이 입력을 출력으로 변환한다는 것도 알고 있습니다. 그렇다면 기계는 어떤 변환을 써야 하는지 어떻게 학습할까요?

퍼셉트론은 이 질문에 답합니다. 퍼셉트론은 가능한 가장 단순한 학습 기계입니다. 입력 몇 개를 받고, 각 입력에 가중치를 곱하고, 편향을 더한 뒤, 이진 결정(binary decision)을 내립니다. 그리고 틀리면 조정합니다. 이 아이디어를 층(layer)으로 쌓은 것이 지금까지 만들어진 모든 인공신경망의 출발점입니다.

퍼셉트론을 이해한다는 것은 코드에서 "학습"이 실제로 무엇을 의미하는지 이해한다는 뜻입니다. 출력이 현실과 맞을 때까지 숫자를 조정하는 것입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.퍼셉트론은 활성화 함수를 적용하기 전에 입력에 어떤 수학 연산을 수행하나요?

2.분류 문제에서 '선형 분리 가능(linearly separable)'하다는 것은 무엇을 의미하나요?

0/2 답변 완료

개념

하나의 뉴런, 하나의 결정

퍼셉트론은 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_outputhidden_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)

연습문제

  1. (쉬움) NAND 게이트에서 퍼셉트론을 학습합니다. NAND는 보편 게이트(universal gate)이므로 모든 논리 회로를 만들 수 있습니다. 학습된 가중치와 편향이 유효한 결정 경계를 이루는지 확인합니다.
  2. (중간) Perceptron 클래스를 수정해 각 에포크(epoch)마다 결정 경계 w1*x1 + w2*x2 + b = 0을 추적합니다. AND 게이트 학습 중 직선이 어떻게 이동하는지 출력합니다.
  3. (어려움) 세 입력 중 최소 두 개가 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)"진짜 신경망"퍼셉트론을 여러 층으로 쌓아 이전 층의 출력을 다음 층 입력으로 사용하는 구조

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

perceptron
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

skill-perceptron

Understand the perceptron pattern and when to use single-layer vs multi-layer architectures

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.단일 퍼셉트론이 XOR 함수를 학습하지 못하는 이유는 무엇인가요?

2.퍼셉트론 학습 규칙에서 예측이 목표값(target)과 일치하면 어떤 일이 일어나나요?

3.여러 퍼셉트론을 사용하면 XOR을 어떻게 풀 수 있나요?

0/3 답변 완료