본문 바로가기
공부 정리/딥러닝

[딥러닝1]퍼셉트론

by 블로그별명 2023. 12. 26.

 

 

이 포스팅 시리즈는 딥러닝에 대해 깊이 있게 이해하고자 하는 여정의 일환으로 작성되었습니다. "밑바닥부터 시작하는 딥러닝 1"이라는 책을 베이스로 공부했으며, 학습 중에 생기는 추가적인 궁금증은 chatgpt, 구글링, 유튜브 등 등 모든 방면으로 해소하고자 노력했습니다. 만약 내용 중 잘못된 부분을 발견하시면, 댓글로 알려주시면 매우 감사하겠습니다.


퍼셉트론의 정의

퍼셉트론(Perceptron)은 1957년에 Frank Rosenblatt에 의해 처음 발명되었으며, 다수의 신호를 입력으로 받아 하나의 신호를 출력하는 신경망의 매우 단순한 형태이다.

[그림1]입력이 2개인 퍼셉트론을 그래프로 표현(좌) 수식으로 표현(우)

[그림 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]

[그림 2] XOR 게이트의 경우에는 다른 논리 게이트들과 달리 선형결정경계를 통한 분류가 불가능함을 확인할 수 있다. 이는 XOR게이트와 같은 비선형문제는 퍼셉트론으로 구현할 수 없음을 의미한다. 정확하게 말하자면 '단층'퍼셉트론으로는 구현이 불가능하다.

다층 퍼셉트론(multi-layer perceptron)

퍼셉트론을 층으로  쌓아 다층퍼셉트론으로 만들 수 있다. 그리고 우리는 다층퍼셉트론을 사용하여 XOR게이트를 표현할 수 있다.

[그림3] 논리게이트의 조합으로 표현한 XOR게이트(좌) 다층퍼셉트론으로 표현한 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]

[그림 4] 모든 입력벡터를 2차원 벡터 공간에 표시해 본다.

 

 

[그림5]

[그림 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]

[그림 6] 선형변환된 벡터의 각 요소에 활성화 함수를 적용한다. 적용결과 기존 전체 벡터공간은 (1,1), (1,0), (0,1), (0, 0) 4개의 점으로 쪼그라든다. 활성화함수를 통해 비선형변환된 백터들은 직관적으로 선형결정경계를 통한 분류가 가능한 상태라는 것을 알 수 있다.

 

 

[그림7]

[그림 7] 마지막으로 2차원 입력벡터를 1차원벡터로 선형변환 후, 활성화함수를 통해 이진분류한다. 이과정을 선형변환 전 2차원 벡터공간에 시각화하면 직선형태의 결정경계를 통한 분류로 표현할 수 있다. 이때, 직선을 기준으로 어느 쪽에 위치하느냐에 따라 클래스가 결정된다.

 

 

다층퍼셉트론의 추론과정을 단계별로 분석해 본 결과, 활성화함수를 통한 데이터의 비선형변환이 다층퍼셉트론의 핵심이라는 사실을 알 수 있다. [각주:3] 따라서, 활성화 함수가 실제 뉴런의 행동방식을 모방하는데 중점을 두었던 신경망 발전 초기와 달리, 현재는 활성화 함수가 가진 비선형성에 중점을 두어, 신경망에서 사용되는 비선형 함수로 정의되고 있다.


  1.  딥러닝에서 언급되는 '선형성'은 실제 수학적인 정의와 약간 다르게 해석되는 경우가 많다. 수학에서 선형성을 가지고 있다는 뜻은 어떤 변수들 간의 관계가 선형 함수로 표현될 수 있음을 의미하며, 선형 함수는 가산성과 동차성을 만족해야 한다. 반면, 딥러닝에서는 변수들 간의 관계를 일차 함수로 나타낼 수 있다면 선형성을 가진다고 보는 경향이 있다.  [본문으로]
  2. 위에서 구현했던 AND, NAND, OR게이트의 가중치와 편향을 사용했습니다  [본문으로]
  3. 물론 단층퍼셉트론도 활성화 함수를 가지고 있지만, 이미 선형분리가능한 데이터에 대해 결정경계를 형성하는 데 사용되어, 데이터에 실질적인 비선형함수로서의 역할을 한다고 보기는 어렵다. [본문으로]

댓글