이 포스팅 시리즈는 딥러닝에 대해 깊이 있게 이해하고자 하는 여정의 일환으로 작성되었습니다. "밑바닥부터 시작하는 딥러닝 1"이라는 책을 베이스로 공부했으며, 학습 중에 생기는 추가적인 궁금증은 chatgpt, 구글링, 유튜브 등 등 모든 방면으로 해소하고자 노력했습니다. 만약 내용 중 잘못된 부분을 발견하시면, 댓글로 알려주시면 매우 감사하겠습니다.
퍼셉트론의 정의
퍼셉트론(Perceptron)은 1957년에 Frank Rosenblatt에 의해 처음 발명되었으며, 다수의 신호를 입력으로 받아 하나의 신호를 출력하는 신경망의 매우 단순한 형태이다.
[그림 1] 여기 2개의 입력신호를 받는 퍼셉트론의 예시가 있다. $x_1$과 $x_2$는 입력 신호이며, 각각의 신호는 가중치 $w_1$과 $w_2$에 의해 가중된다. 이 가중된 신호들의 총합을 $z$라고 할 때, 활성화함수($\sigma$)는 $z$를 입력으로 받아 최종 출력값을 도출한다. [그림 1]의 활성화 함수는 계단함수이다. 계단함수는 실제 뉴런이 작동하는 방식을 모방하여, 입력신호가 임계값($\theta$)을 넘어섰을 때만 활성화되고(1) 그렇지 않을 경우 활성화되지 않는다(0).
퍼셉트론의 한계
[그림 1]과 같은 형태의 퍼셉트론을 이용하여 우리는 논리게이트를 구현할 수 있다. 다만 이제부터는 임계값을 좌변으로 이항하고 이를 편향($b$)이라는 이름으로 부를 것이다. 형태만 바뀌었을 분 그 의미는 같다.
그럼 이제부터 AND, OR, NAND, XOR 게이트 구현을 시작해 보자.
<hide/>
import numpy as np
def AND(x1, x2):
x = np.array([x1, x2])
w = np.array([0.5, 0.5])
b = -0.7
tmp = np.sum(w*x) + b
if tmp > 0:
return 1
else:
return 0
def NAND(x1, x2):
x = np.array([x1, x2])
w = np.array([-0.5, -0.5])
b = 0.7
tmp = np.sum(w*x) + b
if tmp > 0:
return 1
else:
return 0
def OR(x1, x2):
x = np.array([x1, x2])
w = np.array([0.5, 0.5])
b = -0.2
tmp = np.sum(w*x) + b
if tmp > 0:
return 1
else:
return 0
퍼셉트론은 기본적으로 선형 분류기로서, 1 선형결정 경계를 이용해 데이터를 분류한다. 이 결정 경계는 일차방정식($wx+b=0$) 의 해, 즉 초평면인데, 2차원에서는 직선, 3차원에서는 평면의 형태이다.
[그림 2] XOR 게이트의 경우에는 다른 논리 게이트들과 달리 선형결정경계를 통한 분류가 불가능함을 확인할 수 있다. 이는 XOR게이트와 같은 비선형문제는 퍼셉트론으로 구현할 수 없음을 의미한다. 정확하게 말하자면 '단층'퍼셉트론으로는 구현이 불가능하다.
다층 퍼셉트론(multi-layer perceptron)
퍼셉트론을 층으로 쌓아 다층퍼셉트론으로 만들 수 있다. 그리고 우리는 다층퍼셉트론을 사용하여 XOR게이트를 표현할 수 있다.
왜 다층퍼셉트론으로는 XOR게이트를 표현할 수 있을까? [그림 3] 다층 퍼셉트론의 추론과정을 따라가 보며 알아보자. 2
<hide/>
import matplotlib.pyplot as plt
import numpy as np
def activation_function(input):
return (input > 0).astype(int)
transformation_matrix = np.array([[-0.5, 0.5], [-0.5, 0.5]])
bias = np.array([0.7, -0.2])
# 격자 데이터
x = np.linspace(-10, 10, 21)
y = np.linspace(-10, 10, 21)
X, Y = np.meshgrid(x, y) # [x1, x2, ...] [y1, y2, ...]
plt.figure(figsize=(12, 12))
# 1
plt.subplot(2, 2, 1)
# 원래 격자 그리기
plt.plot(X, Y, color='grey', linewidth=0.2)
plt.plot(X.T, Y.T, color='grey', linewidth=0.2)
plt.plot([-10, 10], [0, 0], color='black', linewidth=0.6)
plt.plot([0, 0], [-10, 10], color='black', linewidth=0.6)
# 데이터 그리기
plt.plot(1, 1, 'ro')
plt.plot(0, 0, 'ro')
plt.plot(1, 0, 'go')
plt.plot(0, 1, 'go')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xticks([])
plt.yticks([])
# 2
plt.subplot(2, 2, 2)
# 원래 격자 그리기
plt.plot(X, Y, color='grey', linewidth=0.2)
plt.plot(X.T, Y.T, color='grey', linewidth=0.2)
plt.plot([-10, 10], [0, 0], color='black', linewidth=0.6)
plt.plot([0, 0], [-10, 10], color='black', linewidth=0.6)
# 격자에 선형 변환 적용 wx+b
X_transformed, Y_transformed = np.empty_like(X), np.empty_like(Y)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
[X_transformed[i, j], Y_transformed[i, j]] = [X[i, j], Y[i, j]] @ transformation_matrix + bias
transformed_x_axis = (np.array([[-10, 0], [10, 0]]) @ transformation_matrix + bias).T
transformed_y_axis = (np.array([[0, -10], [0, 10]]) @ transformation_matrix + bias).T
# 선형변환된 격자 그리기
plt.plot(X_transformed, Y_transformed, color='grey', linewidth=0.5)
plt.plot(X_transformed.T, Y_transformed.T, color='grey', linewidth=0.5)
plt.plot(transformed_x_axis[0], transformed_x_axis[1], color='black', linewidth=1.5)
plt.plot(transformed_y_axis[0], transformed_y_axis[1], color='black', linewidth=1.5)
# 데이터에 선형 변환 적용
transformed_point1 = [1, 1] @ transformation_matrix + bias
transformed_point2 = [0, 0] @ transformation_matrix + bias
transformed_point3 = [1, 0] @ transformation_matrix + bias
transformed_point4 = [0, 1] @ transformation_matrix + bias
# 데이터 그리기
plt.plot(transformed_point1[0], transformed_point1[1], 'ro')
plt.plot(transformed_point2[0], transformed_point2[1], 'ro')
plt.plot(transformed_point3[0], transformed_point3[1], 'go')
plt.plot(transformed_point4[0], transformed_point4[1], 'go')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xticks([])
plt.yticks([])
# 3
plt.subplot(2, 2, 3)
# 격자 그리기
plt.plot(X, Y, color='grey', linewidth=0.2)
plt.plot(X.T, Y.T, color='grey', linewidth=0.2)
plt.plot([-10, 10], [0, 0], color='black', linewidth=0.6)
plt.plot([0, 0], [-10, 10], color='black', linewidth=0.6)
# 데이터의 선형변환 + 비선형변환
transformed_point1 = activation_function([1, 1] @ transformation_matrix + bias)
transformed_point2 = activation_function([0, 0] @ transformation_matrix + bias)
transformed_point3 = activation_function([1, 0] @ transformation_matrix + bias)
transformed_point4 = activation_function([0, 1] @ transformation_matrix + bias)
# 데이터 그리기
plt.plot(transformed_point1[0], transformed_point1[1], 'ro')
plt.plot(transformed_point2[0], transformed_point2[1], 'ro')
plt.plot(transformed_point3[0], transformed_point3[1], 'go')
plt.plot(transformed_point4[0], transformed_point4[1], 'go')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xticks([]) # x축 눈금 레이블 제거
plt.yticks([]) # y축 눈금 레이블 제거
# 4
plt.subplot(2, 2, 4)
plt.plot(X, Y, color='grey', linewidth=0.2)
plt.plot(X.T, Y.T, color='grey', linewidth=0.2)
plt.plot([-10, 10], [0, 0], color='black', linewidth=0.6)
plt.plot([0, 0], [-10, 10], color='black', linewidth=0.6)
transformed_point1 = activation_function([1, 1] @ transformation_matrix + bias)
transformed_point2 = activation_function([0, 0] @ transformation_matrix + bias)
transformed_point3 = activation_function([1, 0] @ transformation_matrix + bias)
transformed_point4 = activation_function([0, 1] @ transformation_matrix + bias)
plt.plot(transformed_point1[0], transformed_point1[1], 'ro')
plt.plot(transformed_point2[0], transformed_point2[1], 'ro')
plt.plot(transformed_point3[0], transformed_point3[1], 'go')
plt.plot(transformed_point4[0], transformed_point4[1], 'go')
# 선형결정경계 그리기
x = np.linspace(-5, 5, 2)
y =-x + 1.4
plt.plot(x, y, color='blue', linestyle='--')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xticks([])
plt.yticks([])
plt.show()
[그림 4] 모든 입력벡터를 2차원 벡터 공간에 표시해 본다.
[그림 5] 입력벡터에 가중치와 편향을 적용한다. 이과정은 다음과 같이 수식으로 표현할 수 있다.
$$\begin {equation}
\left [ \begin {array}{c}
x_1 \\
x_2
\end {array} \right]
\cdot
\left [ \begin {array}{cc}
w_1 & w_2 \\
w_3 & w_4
\end {array} \right]
+
\left [ \begin {array}{c}
b_1 \\
b_2
\end {array} \right]
\end {equation}$$
수식을 보면, 입력벡터에 가중치와 편향을 적용하는 과정은 하나의 선형변환과정이라는 것을 확인할 수 있다. 선형변환(linear transformation)의 직관적인 이해를 위해 시각화 함에 있어, 입력벡터뿐만 아니라 벡터공간 전체를 선형변환하였다. 다만 선형변환 후 기저벡터(basis vector)가 서로 선형종속적(linearly dependent) 임으로 모든 벡터가 하나의 직선 위에 위치하게 되었다. 또한 선형변환 후에도 여전히 선형결정경계를 통한 분류가 불가능한 것을 확인할 수 있다.
[그림 6] 선형변환된 벡터의 각 요소에 활성화 함수를 적용한다. 적용결과 기존 전체 벡터공간은 (1,1), (1,0), (0,1), (0, 0) 4개의 점으로 쪼그라든다. 활성화함수를 통해 비선형변환된 백터들은 직관적으로 선형결정경계를 통한 분류가 가능한 상태라는 것을 알 수 있다.
[그림 7] 마지막으로 2차원 입력벡터를 1차원벡터로 선형변환 후, 활성화함수를 통해 이진분류한다. 이과정을 선형변환 전 2차원 벡터공간에 시각화하면 직선형태의 결정경계를 통한 분류로 표현할 수 있다. 이때, 직선을 기준으로 어느 쪽에 위치하느냐에 따라 클래스가 결정된다.
다층퍼셉트론의 추론과정을 단계별로 분석해 본 결과, 활성화함수를 통한 데이터의 비선형변환이 다층퍼셉트론의 핵심이라는 사실을 알 수 있다. 3따라서, 활성화 함수가 실제 뉴런의 행동방식을 모방하는데 중점을 두었던 신경망 발전 초기와 달리, 현재는 활성화 함수가 가진 비선형성에 중점을 두어, 신경망에서 사용되는 비선형 함수로 정의되고 있다.
- 딥러닝에서 언급되는 '선형성'은 실제 수학적인 정의와 약간 다르게 해석되는 경우가 많다. 수학에서 선형성을 가지고 있다는 뜻은 어떤 변수들 간의 관계가 선형 함수로 표현될 수 있음을 의미하며, 선형 함수는 가산성과 동차성을 만족해야 한다. 반면, 딥러닝에서는 변수들 간의 관계를 일차 함수로 나타낼 수 있다면 선형성을 가진다고 보는 경향이 있다. [본문으로]
- 위에서 구현했던 AND, NAND, OR게이트의 가중치와 편향을 사용했습니다 [본문으로]
- 물론 단층퍼셉트론도 활성화 함수를 가지고 있지만, 이미 선형분리가능한 데이터에 대해 결정경계를 형성하는 데 사용되어, 데이터에 실질적인 비선형함수로서의 역할을 한다고 보기는 어렵다. [본문으로]
'공부 정리 > 딥러닝' 카테고리의 다른 글
[딥러닝2]신경망의 순전파 (0) | 2024.01.08 |
---|---|
Pooling 레이어의 이해 (0) | 2023.02.21 |
패딩(padding) 사용하는 이유 (0) | 2023.02.21 |
합성곱 연산(Convolution operation) 방법 (0) | 2023.02.21 |
합성곱 신경망의 필요성 (0) | 2023.02.21 |
댓글