ML05. XOR 문제 해결, 다층퍼셉트론(MLP)
개요
이전 글에서 XOR 문제는 단층퍼셉트론으로 풀 수 없다는 것을 확인했다.
(단층신경망은 은닉층이 없는 신경망, 즉 입, 출력만 존재)
직선 하나로는 아래 네 점을 절대 두 그룹으로 나눌 수 없기 때문이다.
| x1 | x2 | y (XOR) |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 |
0 |
여기서 XOR(배타적 논리합)은 두 개의 입력값 중 하나는 참(1) 이고 하나는 거짓(0) 일 때만 참(1)을 반환하는 논리연산이다.
이번 글에서는 단층신경망으로 분리 불가능했던 XOR 문제를 두 가지 방법으로 직접 해결해본다.
우선, 논리게이트(OR, AND)를 직접 조합해 XOR 해결의 실마리를,
그리고 은닉층 (Hidden Layer)이 있는 다층 퍼셉트론(MLP)을 적용해 문제를 해결한다.
1. XOR을 논리게이트로 분해하기
먼저 논리, 수학적으로 XOR을 이해하자.
XOR은 아래처럼 OR 게이트와 AND 게이트의 조합으로 표현할 수 있다.
\begin{aligned}
\text{XOR}(x_1, x_2)
&= \text{OR}(x_1, x_2) -\; \text{AND}(x_1, x_2)
\end{aligned}
$$
- $x1$, $x2$를 $OR$ 연산한 결과가 $h1$이고
- $x1$, $x2$를 $AND$ 연산한 결과는 $h2$라면
- $XOR$ = $h1-h2$
- 즉, $h1$ $AND$ ($NOT$ $h2$) 이다
잘 이해되지 않는다면 논리식을 건너뛰어 아래 진리표를 보고 다시 돌아와 이해하자.
| x1 | x2 | h1 (OR) | h2 (AND) | y = h1 - h2 |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 |
0 |
다시 풀어쓰면
y(XOR) = h_1 - h_2
$$
h_1 = \text{OR}(x_1, x_2) = \begin{cases} 1 & x_1 + x_2 > 0.5 \\ 0 & \text{otherwise} \end{cases}
$$
h_2 = \text{AND}(x_1, x_2) = \begin{cases} 1 & x_1 + x_2 > 1.5 \\ 0 & \text{otherwise} \end{cases}
$$
$h_1$(OR)에서 $h_2$(AND)를 빼면 정확히 XOR이 된다.
위 과정을 파이썬 코드로 단순하게 작성해 보았다.
예제 1. 논리게이트로 XOR구현
import numpy as np
X = np.array([
[0,0],
[0,1],
[1,0],
[1,1]
])
y = np.array([0, 1, 1, 0])
h1 = (X[:,0] + X[:, 1] > 0.5).astype(int) # OR
h2 = (X[:,0] + X[:, 1] > 1.5).astype(int) # AND
print(f'h1={h1}')
print(f'h2={h2}')
h3 = h1 - h2
print(f'h3={h3} (XOR)')
(X[:, 0] 문법이 낯설다면, : 은 모든 행을 뜻하고, 0은 0번째 열을 뜻한다.
즉 X Array에서 모든행의 0번째 열만 골라낸 [0,0,1,1] 을 의미한다.
마찬가지로 X[:, 1] 은 모든 행의 1번째 열만 고른 [0,1,0,1] 이고
따라서 X[:, 0] + X[:, 1] = [0,1,1,1] = h1 된다.
참고로 X[2:4, 0] 라고 쓰면 2번행부터 4번행보다 작은 모든 행의 0번째 열이 된다.
: 앞, 뒤의 숫자는 생략가능하고 처음부터 or 끝까지 라는 의미이다.)
위 코드의 실행결과는 아래와 같다.
|
| [논리게이트로 구현한 XOR] |
출력 $y$(h3)는 정답 XOR와 [0, 1, 1, 0] 와 완전히 일치한다.
그렇다면 위 예제 1의 의미를 신경망(Neural Network) 관점에서 살펴보자.
이 논리게이트 조합은 사실 아래처럼 2층 신경망 구조 와 동일하다.
(일반적으로 신경망에서 입력층은 레이어수에 포함하지 않는다.)
입력층 ($x1$, $x2$)
↓
은닉층 (h1=OR, h2=AND) ← 숨겨진 표현 공간
↓
출력층 ($y$ = h1 - h2)
즉, 단층으로 불가능했던 XOR이 은닉층(Hidden Layer) 하나만 추가해도 해결된다는 것이다.
이것이 바로 다층 퍼셉트론(Multi-Layer Perceptron)의 핵심 아이디어다.
위 예제는 단순히 손으로 OR, AND 임계값을 설계한 것이지만,
다음 예제에서는 이 가중치를 기계학습을 통해 자동으로 찾게 하는것을 목표로 한다.
2. 다층 퍼셉트론(MLP) 구조
이제 가중치를 직접 설계하지 않고,
은닉층을 추가해 순전파(Forward) ,역전파(Back Propagation)를 통해 자동으로 학습시켜보자.
아직 순전파 ?, 역전파 😟 를 들어본 적이 없더라도 걱정말고 넘어가자.
진행하다 보면 직관이 생기고 이해될 것이라 기대한다.
우리의 목표는 XOR를 기계가 스스로 학습해 분류하도록 하는 것이다.
은닉층을 추가한 다층 신경망의 구조는 아래와 같다.
여기서 히든 레이어의 뉴런 수는 임의로 8개로 정의했으나, 최소 2개면 해결가능하다.
그 이유는 위에서 살펴본 h1(OR), h2(AND) 2개의 뉴런으로 해결 가능한 문제이기 때문이다.
|
| [클로드로 작성한 신경망구조] |
아래 괄호(4, 2), (2, 8) 등 내부 숫자는 데이터의 차원을 의미한다.
즉 (4,2) 는 입력$X$ 의 차원이 4행 2열의 행렬(Matrix)로 구성되었음을 뜻한다.
입력층 은닉층 출력층
\rightarrow
Z_1
\rightarrow
A_1\;(4,8)
\rightarrow
Z_2
\rightarrow
\hat{y}\;(4,1)$
- 입력층($X$) : 2개 노드 (x1, x2)
- 은닉층(선형결합($Z1$), 활성화함수($A1$)) : 8개 노드 (뉴런 수는 조절 가능)
- 출력층(선형결합($Z2$), 활성화함수($\hat{y}$) : 1개 노드 (0~1 확률)
- 활성화함수 : 은닉층, 출력층 모두 Sigmoid
이제 위 과정(ML, 기계학습)을 하나씩 살펴보자.
이전에 공부한 로지스틱회귀와 동일하지만 중간에 8개의 뉴런(Neuron)으로 구성된 은닉층이 추가되었다.
3. 순전파(Forward Propagation) 과정
입력→ 은닉→ 출력으로 이어지는 과정을 순전파라고 하며 결과는 $\hat{y}$ 이다.
하나씩 단계를 나누어 순전파 과정을 살펴보자.
3.1 입력 → 은닉층으로
먼저 입력($X$)과 가중치($W_1$)를 곱하고 편향($b_1$)을 더함 (선형결합)
Z_1 = X \cdot W_1 + b_1
$$
선형결합의 결과($Z_1$)를 시그모이드함수에 넣어 확률로 변환 (활성화)
A_1 = \sigma(Z_1)
$$
∴$X$의 차원은 $(4, 2)$, $W_1$은 $(2, 8)$이므로 내적(Dot)하면 $Z_1$, $A_1$은 $(4, 8)$
(가중치($W$) = (2, 8) 중 앞(행)이 2인 이유는 입력(X)의 특징(Feature) 이 2이고,
뒤(열)가 8인 이유는 뉴런의 수를 임의로 8로 정했기 때문,
따라서 $W$의 차원은 (n, neuraon) 이다)
3.2 은닉 → 출력층으로
위 과정과 동일하나 입력이 $X$가 아닌 은닉층의 출력인 $A_1$ (선형결합)
Z_2 = A_1 \cdot W_2 + b_2
$$
선형결합의 결과($Z_2$)를 시그모이드함수에 넣어 확률로 변환 (활성화)
\hat{y} = \sigma(Z_2)
$$
∴$A_1$은 $(4, 8)$, $W_2$는 $(8, 1)$이므로 $\hat{y}$는 $(4, 1)$이 된다.
여기까지($\hat{y}$ 얻기)의 과정을 머신러닝에서 순전파라고 한다.
위 과정(선형결합 → 시그모이드) 을 은닉층에 존재하는 하나의 뉴런을 기준으로 이해하면 아래와 같다.
|
| [클로드로 만든 뉴런의 구조] |
위 그림에서 가중합(∑) 과 시그모이드(σ) 를 합치면 하나의 뉴런(Neuron) 이 된다.
(이쯤에서 수학적인 직관이 부족하다면 선형대수를 가볍게 공부하며 ML 공부를 병행해야 한다.
수식에 매몰될 필요는 없지만 적어도 행렬, 벡터 곱은 이해할 수 있어야 하며 칸아카데미의 선형대수학코스를 강력 추천한다)
4. 시그모이드(활성화) 함수
선형결합의 결과($z$)는 양의 무한대~음의 무한대 값을 가진다.
$z \in (-\infty, \infty)$
시그모이드함수는 이 값을 0~1사이 확률분포로 바꾸어주는 활성화 함수
\sigma(z) = \frac{1}{1 + e^{-z}}
$$
이전 글에서 자세히 설명되어 있으므로 자세한 내용은 생략.
5. 손실(Loss), 비용함수(Cost Function)
이진 분류에는 Binary Cross Entropy를 사용한다.
- 손실함수(Loss)
(여기서 $y$는 실제 라벨, $\hat{y}$는 모델의 예측값 $H(\mathbf{x})$)
- 비용함수(Cost, J)
$$\begin{aligned} J(\mathbf{w}, b) = -\frac{1}{m} \sum_{i=1}^{m}
\big[ &y^{(i)} \log(\hat{y}^{(i)}) \\ & + (1 - y^{(i)})
\log(1 - \hat{y}^{(i)}) \big] \end{aligned}$$
$y=1$이면 앞 항만, $y=0$이면 뒷 항만 살아남는다. 정답 클래스의 확률을 높이도록 유도한다.
6. 역전파(Back Propagation) 과정
순전파 과정이 현재의 가중치($W$)와 편향($b$)으로 예측값($\hat{y}$)을 얻는 과정이라면,
이제 예측값($\hat{y}$)과 실제값($y$)의 차이인 오차를 계산해야 한다.
그리고 이 오차가 어떤 경로를 거쳐 발생했는지 추적하기 위해
활성화 함수의 미분(Derivative)값을 이용하여 출력층에서 입력층 방향으로 거꾸로 전파한다.
이렇게 각 가중치($W$)와 편향($b$)가 오차에 얼마나 기여했는지 계산할 수 있으며, 이를 이용해 모델을 학습시킨다.
아래 그림에서 초록색 화살표는 순전파를, 붉은색 화살표는 역전파과정이다.
|
| [클로드로 그린 역전파 과정] |
모델이 학습된다는 말은 가중치($W$)와 편향($b$)이 업데이트되는 것이다. 😀
왜냐하면 순전파를 거친 결과, 오차가 크다면 가중치와 편향이 맞지 않다는 의미고,
활성화 함수의 미분값을 이용해 여기에 기여한 가중치와 편향을 경사하강법을 통해 업데이트한다.
위 과정이 바로 역전파이다. 멋지군~
그리고 업데이트된 가중치, 편향으로 다시 순전파의 반복이다.
언제까지냐면 아래 그림처럼 W의 가장 낮은 곳까지 반복한다.
|
| [경사하강법이 W를 업데이트하는 과정 예시] |
(미적분학을 공부하지 않았더라도 너무 걱정하지 말고 이렇게 생각하자.
미분은 연속으로 기울기를 구하는 것이고,
만약, 현재 모델의 오차(err)가 크다는 것은 예측치($\hat{y}$)와 정답($y$)의 차이가 큰 것 이므로
위 그래프에서 y축(Cost)이 값이 높다, 그럼 가중치($W$)를 좀 더 Cost가 낮은 쪽으로 이동하면?
아마도 오차는 줄고 모델은 더 잘 맞게 예측하게 될 것이다.
따라서 역전파는 현재비용을 미분해 내려오며 W가 가장 작은값을 찾는 여정이다
아래 그래프를 보고 직관을 가지자, W(기울기)가 커지니 점점 잘 맞다)
|
| [W, b를 모델이 학습해가는 과정] |
그럼 우리 모델의 역전파 과정을 단계별로 살펴보자.
6.1 출력층 오차계산
예측값과 실제값의 차이를 계산한다.
err = \hat{y} - y
$$
시그모이드 활성화 함수와 Binary Cross Entropy를 함께 사용하면, 출력층의 오차항은 다음과 같이 간단한 형태로 정리된다.
\delta_2 = \frac{err}{m}
$$
여기서 m은 학습 데이터의 개수이다.
미분 유도과정을 다 설명하면 좋겠지만 분량이 많아 생략하고 수식 결과만 정리한다.
미적분학 시간이 아니므로 "직관적으로 A에 영향을 미친 B계수를 찾아가는 과정"이라 생각한다.
6.2 출력층 가중치 업데이트
출력층의 오차를 이용하여 가중치와 편향의 기울기를 계산한다.
W_2 = W_2 - \alpha \cdot A_1^T \cdot \delta_2
$$
b_2 = b_2 - \alpha \cdot \sum \delta_2
$$
여기서 α는 학습률(Learning Rate)이다.
6.3 은닉층으로 오차 전파
출력층의 오차를 은닉층으로 전달한다.
\delta_1 = (\delta_2 \cdot W_2^T) \odot A_1 (1 - A_1)
$$
여기서 $\odot$는 원소별 곱(element-wise product)을 의미한다.
중요한 것은 $W_2$ 를 업데이트 하기 전에 $\delta_1$ (델타)를 미리 구해두어야 한다.
(기존 가중치(old weights)를 사용해서 모든 기울기를 먼저 계산한 후, 마지막에 동시(simultaneously) 에 업데이트)
은닉층의 활성화 함수로 시그모이드(Sigmoid)를 사용하므로, 역전파 과정에서 시그모이드의 미분값이 필요하다.
$$\sigma'(z)=\sigma(z)(1-\sigma(z))$$
또한 순전파에서
A_{1} = \sigma(Z_{1})
$$
이므로
$$\sigma'(Z_1)=A_1(1-A_1)$$
를 사용할 수 있다.
6.4 은닉층 가중치 업데이트
계산된 δ1을 이용하여 첫 번째 층의 가중치와 편향을 업데이트한다.
W_1 = W_1 - \alpha \cdot X^T \cdot \delta_1
$$
b_1 = b_1 - \alpha \cdot \sum \delta_1
$$
이제 업데이트된 가중치, 편향을 이용 다시 순전파를 진행한다.
이것의 머신러닝의 원리이다.
위에서 설명한 다층퍼셉트론 신경망을 순수파이썬으로 구현한 예제이다.
예제 2 MLP로 XOR 학습
import numpy as np
X = np.array([
[0,0],
[0,1],
[1,0],
[1,1]
])
y = np.array([0, 1, 1, 0]).reshape(-1, 1)
m,n = X.shape
neuron = 8
W1 = np.random.randn(n, neuron)
b1 = np.zeros(neuron)
W2 = np.random.randn(neuron, 1)
b2 = np.zeros(1)
epochs = 10000
lr = 0.1
def sigmoid(z):
return 1/(1+np.exp(-z))
for i in range(epochs):
# forward
z1 = np.dot(X, W1) + b1 # X:(4,2)•(2,8) = (4, 8)
a1 = sigmoid(z1) # (4, 8)
z2 = np.dot(a1, W2) + b2
y_hat = sigmoid(z2)
# cost
L = -y*np.log(y_hat) - (1-y)*np.log(1-y_hat)
cost = np.sum(L) / m
# backward : cost -> L -> y_hat -> z2
err = y_hat - y
d2 = err / m
# d1은 W2가 업데이트 전 미리 구해둔다
d1 = np.dot(d2, W2.T) * a1*(1-a1)
# z2 = a1•W2+b2
W2 = W2 - lr * np.dot(a1.T, d2)
b2 = b2 - lr * np.sum(d2, axis=0)
# z2 -> a1 -> z1
# z1 = X·W1+b1
W1 = W1 - lr * np.dot(X.T, d1)
b1 = b1 - lr * np.sum(d1, axis=0)
if i%100==0:
print(f'epoch={i:4d}, cost={cost:.3f} ')
# predict
def predict(X, W, b):
z = np.dot(X, W) + b
return sigmoid(z)
xx, yy = np.meshgrid( np.arange(-1, 2, 0.1), np.arange(-1, 2, 0.1) ) # (30, 30), (30, 30)
X_test = np.c_[xx.ravel(), yy.ravel()] # (900,) (900,)
print(X_test.shape)
hidden = predict(X_test, W1, b1)
out = predict(hidden, W2, b2)
y_grid = out.reshape(xx.shape)
import matplotlib.pyplot as plt
plt.contourf(xx, yy, y_grid, levels=50, alpha=0.6, cmap='RdBu')
plt.contour(xx, yy, y_grid, levels=[0.5])
plt.scatter([0, 1], [1, 0], marker='o', s=100, label='True:1')
plt.scatter([0, 1], [0, 1], marker='x', s=100, label='False:0')
plt.legend()
plt.show()
예제 코드를 분석해 보자
라인 [1~22], 초기화
X = np.array([
[0,0],
[0,1],
[1,0],
[1,1]
])
y = np.array([0, 1, 1, 0]).reshape(-1, 1)
데이터셋을 정의하고
m,n = X.shape neuron = 8 W1 = np.random.randn(n, neuron) b1 = np.zeros(neuron) W2 = np.random.randn(neuron, 1) b2 = np.zeros(1) epochs = 10000 lr = 0.1
뉴런수와 가중치, 편향, 학습횟수, 학습률을 결정한다.
라인 [29~33], 순전파
z1 = np.dot(X, W1) + b1 # X:(4,2)•(2,8) = (4, 8) a1 = sigmoid(z1) # (4, 4) z2 = np.dot(a1, W2) + b2 y_hat = sigmoid(z2)
Weighted Sum + Sigmoid 조합
라인 [36~37], 비용계산
L = -y*np.log(y_hat) - (1-y)*np.log(1-y_hat) cost = np.sum(L) / m
Loss를 구하고 합친 후 나누어 평균 Cost를 구한다.
라인 [40~53], 역전파
err = y_hat - y d2 = err / m # d1은 W2가 업데이트 전 미리 구해둔다 d1 = np.dot(d2, W2.T) * a1*(1-a1) # z2 = a1•W2+b2 W2 = W2 - lr * np.dot(a1.T, d2) b2 = b2 - lr * np.sum(d2, axis=0) # z2 -> a1 -> z1 # z1 = X·W1+b1 W1 = W1 - lr * np.dot(X.T, d1) b1 = b1 - lr * np.sum(d1, axis=0)
출력층의 오차를 계산하고, 은닉층으로 역전파한다.
학습횟수(epochs) 만큼 학습이 진행되며 순전파, 역전파를 거쳐
비용이 줄고 학습이 이루어짐을 확인할 수 있다.
|
| [다층퍼셉트론으로 구현한 XOR] |
XOR를 다층신경망으로 학습한 결과의 시각화는 아래와 같다.
|
| [다층신경망으로 구현한 XOR 시각화] |
7. 정리
- XOR은 단층 퍼셉트론(직선 하나)으로 풀 수 없다 → 선형 분리 불가능
- 은닉층 하나만 추가해도 XOR이 해결된다 → 다층 퍼셉트론(MLP)
- 예제 1에서 h1(OR) - h2(AND) = XOR 임을 직접 확인
- 예제 2에서 역전파로 가중치를 자동 학습해 동일한 결과를 얻었다
- 학습 성공의 지표 : Cost가 0.693에서 벗어나 0으로 수렴하는지 확인
- 은닉층의 뉴런 수(neuron)와 학습률(lr), 반복 횟수(epochs)는 직접 바꿔보며 실험해볼 것
- 학습에 필요한 총 파라미터의 수를 예상해 볼 것
딥러닝의 출발점은 XOR 문제의 해결에 있다.
1969년 Minsky & Papert가 단층 퍼셉트론의 한계를 증명한 이후, 다층 퍼셉트론의 등장으로 이 문제가 해결되었고, 그것이 오늘날 딥러닝(Deep Neural Network)의 시작이 되었다.
따라서 딥러닝의 정의는,
활성화함수(Activation Func.)를 가진 은닉층이 2개 이상인 신경망이다.
(굳이 분류하자면 XOR 예제는 은닉층이 하나이므로 Deep 하지 않고, Shallow 하다.)
감사합니다.








댓글
댓글 쓰기