05.6 Affine/Softmax 계층 구현하기

5.6.1 Affine 계층

신경망의 순전파에서는 가중치 신호의 총합을 계산하기 때문에 행렬의 내적(넘파이에서는 np.dot())을 사용했습니다(“3.3 다차원 배열의 계산” 참고). 예를 들어 파이썬으로 다음과 같이 구현한 것을 기억하시나요?

>>> X = np.random.rand(2)   # 입력
>>> W = np.random.rand(2,3) # 가중치
>>> B = np.random.rand(3)   # 편향
>>>
>>> X.shape # (2,)
>>> W.shape # (2, 3)
>>> B.shape # (3,)
>>>
>>> Y = np.dot(X, W) + B

여기에서 $X$, $W$, $B$는 각각 형상이 $(2,)$, $(2, 3)$, $(3,)$인 다차원 배열입니다. 그러면 뉴런의 가중치 합은 $Y = \text{np.dot}(X, W) + B$처럼 계산합니다. 그리고 이 $Y$를 활성화 함수로 변환해 다음 층으로 전파하는 것이 신경망 순전파의 흐름이었습니다. 복습을 조금 더 해보면, 행렬의 내적 계산은 대응하는 차원의 원소 수를 일치시키는 게 핵심입니다. 예를 들어 $X$와 $W$의 내적은 [그림 5-23]처럼 대응하는 차원의 원소 수를 일치시켜야 합니다. 이 책에서는 행렬의 형상을 $(2, 3)$처럼 괄호로 표기하는 이유는 넘파이의 shape 함수의 출력과 형태를 통일하기 위해서입니다.

그림 5-23 행렬의 내적에서는 대응하는 차원의 원소 수를 일치시킨다.

그림 5-23

NOTE_ 신경망의 순전파 때 수행하는 행렬의 내적은 기하학에서는 어파인 변환${}^{\text{affine transformation}}$이라고 합니다. 그래서 이 책에서는 어파인 변환을 수행하는 처리를 ‘Affine 계층’이라는 이름으로 구현합니다.

그럼 앞에서 수행한 계산(행렬의 내적과 편향의 합)을 계산 그래프로 그려보죠. 내적을 계산하는 노드를 ‘$\text{dot}$’이라 하면, $\text{np.dot}(X, W) + B$ 계산은 [그림 5-24]처럼 그려집니다. 또한, 각 변수의 이름 위에 그 변수의 형상도 표기합니다. 예를 들어 [그림 5-24]에서는 $X$의 형상은 $(2,)$, $X \cdot W$의 형상은 $(3,)$임을 표기했습니다.

그림 5-24 Affine 계층의 계산 그래프 : 변수가 행렬임에 주의. 각 변수의 형상을 변수명 위에 표기했다.

그림 5-24

[그림 5-24]는 비교적 단순한 계산 그래프입니다. 단, $\mathbf{X}, \mathbf{W}, \mathbf{B}$가 행렬(다차원 배열)이라는 점에 주의하세요. 지금까지의 계산 그래프는 노드 사이에 ‘스칼라값’이 흘렀는 데 반해, 이 예에서는 ‘행렬’이 흐르고 있는 것이죠.

지니의 행렬 형상 매칭 (어파인 계층)

  • 지니: “도로시, 신경망에서 다차원 배열(행렬)이 흐르는 구간을 어파인(Affine) 계층이라고 불러. 행렬곱 연산 $\mathbf{X} \cdot \mathbf{W} + \mathbf{B}$을 할 때는 반드시 앞 행렬의 열 수와 뒤 행렬의 행 수가 맞닿는 자석 블록처럼 똑같이 맞아야 해! 이걸 형상 매칭(Shape Matching)이라고 부르지!”
  • 도로시: “아! 순방향뿐만 아니라 역방향으로 기울기 행렬 $\frac{\partial L}{\partial \mathbf{X}} = \frac{\partial L}{\partial \mathbf{Y}} \cdot \mathbf{W}^{\mathrm{T}}$를 전치행렬과 곱해 계산할 때도 형상의 크기가 한 치의 오차도 없이 맞물려 돌아가는구나! 형상의 규칙성만 잘 따라가면 복잡한 행렬 미분도 쉽게 유도되네!”
  • 토토: “왈왈! (전치행렬 자석 자리를 바꾸며) 전치는 가로세로 뒤집기다 멍!”

어파인 계층과 행렬 형상 매칭 지니가 대형 칠판에 어파인 계층 식 $\mathbf{Y} = \mathbf{X}\mathbf{W} + \mathbf{b}$과 형상 매칭 규칙을 판서하고 있고, 도로시가 손에 전치행렬 자석 블록을 쥐고 퍼즐을 맞추며 행렬 내적의 차원 변환 원리를 이해하고 있어요.

이제 [그림 5-24]의 역전파에 대해 생각해볼까요? 행렬을 사용한 역전파도 행렬의 원소마다 전개해보면 스칼라값을 사용한 지금까지의 계산 그래프와 같은 순서로 생각할 수 있습니다. 실제로 전개해보면 다음 식이 이루어집니다.

\[\begin{aligned} \frac{\partial L}{\partial \mathbf{X}} &= \frac{\partial L}{\partial \mathbf{Y}} \cdot \mathbf{W}^{\mathrm{T}} \\ \frac{\partial L}{\partial \mathbf{W}} &= \mathbf{X}^{\mathrm{T}} \cdot \frac{\partial L}{\partial \mathbf{Y}} \end{aligned} \tag{식 5.13}\]

[식 5.13]에서 $\mathbf{W}^{\mathrm{T}}$의 $\mathrm{T}$는 전치행렬을 뜻합니다. 전치행렬은 $\mathbf{W}$의 $(i, j)$ 위치의 원소를 $(j, i)$ 위치로 바꾼 것을 말합니다. 수식으로는 다음과 같이 쓸 수 있습니다.

\[\begin{aligned} \mathbf{W} &= \begin{pmatrix} w_{11} & w_{21} & w_{31} \\ w_{12} & w_{22} & w_{32} \end{pmatrix} \\ \mathbf{W}^{\mathrm{T}} &= \begin{pmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \\ w_{31} & w_{32} \end{pmatrix} \end{aligned} \tag{식 5.14}\]

[식 5.14]와 같이 $\mathbf{W}$의 형상이 $(2, 3)$이었다면 전치행렬 $\mathbf{W}^{\mathrm{T}}$의 형상은 $(3, 2)$가 됩니다.

[식 5.13]을 바탕으로 계산 그래프의 역전파를 구해봅시다. 결과는 [그림 5-25]처럼 됩니다.

그림 5-25 Affine 계층의 역전파 : 변수가 다차원 배열임에 주의. 역전파에서의 변수 형상은 해당 변수명 아래에 표기했다.

그림 5-25

[그림 5-25]의 계산 그래프에서는 각 변수의 형상에 주의해서 살펴봅시다. 특히 $\mathbf{X}$와 $\frac{\partial L}{\partial \mathbf{X}}$은 같은 형상이고, $\mathbf{W}$와 $\frac{\partial L}{\partial \mathbf{W}}$도 같은 형상이라는 것을 기억하세요. $\mathbf{X}$와 $\frac{\partial L}{\partial \mathbf{X}}$의 형상이 같다는 것은 다음 식을 보면 명확해지죠.

\[\begin{aligned} \mathbf{X} &= (x_0, x_1, \dots, x_n) \\ \frac{\partial L}{\partial \mathbf{X}} &= \left( \frac{\partial L}{\partial x_0}, \frac{\partial L}{\partial x_1}, \dots, \frac{\partial L}{\partial x_n} \right) \end{aligned} \tag{식 5.15}\]

왜 행렬의 형상에 주의해야 할까요? 행렬의 내적에서는 대응하는 차원의 원소 수를 일치시켜야 하는데, 이를 위해서는 [식 5.13]을 동원해야 할 수도 있기 때문입니다. 예를 들어 $\frac{\partial L}{\partial \mathbf{Y}}$의 형상이 $(3,)$이고 $\mathbf{W}$의 형상이 $(2, 3)$일 때, $\frac{\partial L}{\partial \mathbf{X}}$의 형상이 $(2,)$가 되도록 하는 $\frac{\partial L}{\partial \mathbf{Y}}$와 $\mathbf{W}$의 내적을 생각해보세요(그림 5-26). 그러면 자연히 [식 5.13]이 유도될 것입니다.

그림 5-26 행렬 내적(‘dot’ 노드)의 역전파는 행렬의 대응하는 차원의 원소 수가 일치하도록 내적을 조립하여 구할 수 있다.

그림 5-26

5.6.2 배치용 Affine 계층

지금까지 설명한 Affine 계층은 입력 데이터로 $\mathbf{X}$ 하나만을 고려한 것이었습니다. 이번 절에서는 데이터 $N$개를 묶어 순전파하는 경우, 즉 배치용 Affine 계층을 생각해보겠습니다(묶은 데이터를 ‘배치’라고 부릅니다).

그럼 당장 배치용 Affine 계층을 계산 그래프로 그려보죠(그림 5-27).

그림 5-27 배치용 Affine 계층의 계산 그래프

그림 5-27

기존과 다른 부분은 입력인 $\mathbf{X}$의 형상이 $(N, 2)$가 된 것뿐입니다. 그 뒤로는 지금까지와 같이 계산 그래프의 순서를 따라 순순히 행렬 계산을 하게 됩니다. 또, 역전파 때는 행렬의 형상에 주의하면 $\frac{\partial L}{\partial \mathbf{X}}$과 $\frac{\partial L}{\partial \mathbf{W}}$은 이전과 같이 도출할 수 있습니다.

배치용 Affine 계층 병렬 처리 비유 여러 개의 데이터를 한데 묶은 배치 입력 $\mathbf{X}$ (형상 $(N, 2)$)가 넓은 컨베이어 벨트를 타고 효율적으로 일괄 처리(순전파) 및 역전파되는 배치용 Affine 계층 연산 방식을 지니가 설명하고 있어요.

지니의 배치 처리 가이드 (배치용 Affine)

  • 지니: “도로시! 현실적인 인공지능 학습에서는 이미지 데이터를 하나씩 처리하는 것보다, $N$개를 묶어서 하나의 묶음인 배치(Batch) 형태로 한 번에 처리하는 게 병렬 연산 효율이 엄청나게 좋아져. 이때 입력 $\mathbf{X}$의 형상은 $(N, 2)$로 넓어지게 된단다!”
  • 도로시: “아! 데이터를 조각조각 개별로 가공하기보다 넓은 벨트 레일 위에 한꺼번에 올려서 한 방에 행렬곱 $\mathbf{X} \cdot \mathbf{W}$을 연산하니까 공정이 비약적으로 효율적이네! 편향 $B$도 모든 데이터 행에 똑같이 복사되듯 더해지는 원리(브로드캐스트)가 정말 명확하게 보여!”

편향을 더할 때도 주의해야 합니다. 순전파 때의 편향 덧셈은 $\mathbf{X} \cdot \mathbf{W}$에 대한 편향이 각 데이터에 더해집니다. 예를 들어 $N = 2$(데이터가 2개)로 한 경우, 편향은 그 두 데이터 각각에(각각의 계산 결과에) 더해집니다. 구체적인 예를 살펴보죠.

>>> X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
>>> B = np.array([1, 2, 3])
>>>
>>> X_dot_W
array([[ 0,  0,  0],
       [10, 10, 10]])
>>> X_dot_W + B
array([[ 1,  2,  3],
       [11, 12, 13]])

순전파의 편향 덧셈은 각각의 데이터(1번째 데이터, 2번째 데이터, …)에 더해집니다. 그래서 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 합니다. 코드로는 다음과 같습니다.

>>> dY = np.array([[1, 2, 3], [4, 5, 6]])
>>> dY
array([[1, 2, 3],
       [4, 5, 6]])
>>>
>>> dB = np.sum(dY, axis=0)
>>> dB
array([5, 7, 9])

이 예에서는 데이터가 2개($N = 2$)라고 가정합니다. 편향의 역전파는 그 두 데이터에 대한 미분을 데이터마다 더해서 구합니다. 그래서 np.sum()에서 0번째 축(데이터를 단위로 한 축)에 대해서 (axis=0)의 총합을 구하는 것입니다.

이상의 Affine 구현은 다음과 같습니다.

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None
        self.dW = None
        self.db = None
        
    def forward(self, x):
        self.x = x
        out = np.dot(x, self.W) + self.b
        
        return out
        
    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        return dx

5.6.3 Softmax-with-Loss 계층

마지막으로 출력층에서 사용하는 소프트맥스 함수에 관해 설명하겠습니다. 앞에서 말했듯 이 소프트맥스 함수는 입력 값을 정규화하여 출력합니다. 예를 들어 손글씨 숫자 인식에서의 Softmax 계층의 출력은 [그림 5-28]처럼 됩니다.

그림 5-28 입력 이미지가 Affine 계층과 ReLU 계층을 통과하며 변환되고, 마지막 Softmax 계층에 의해서 10개의 입력이 정규화된다. 이 그림에서는 숫자 ‘0’의 점수는 5.3이며, 이것이 Softmax 계층에 의해서 0.008(0.8%)로 변환된다. 또, ‘2’의 점수는 10.1에서 0.991(99.1%)로 변환된다.

그림 5-28

[그림 5-28]과 같이 Softmax 계층은 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력합니다. 또한, 손글씨 숫자는 가짓수가 10개(10클래스 분류)이므로 Softmax 계층의 입력은 10개가 됩니다.

NOTE_ 신경망에서 수행하는 작업은 학습추론 두 가지가 있습니다. 추론할 때는 일반적으로 Softmax 계층을 사용하지 않습니다. 예컨대 [그림 5-28]의 신경망은 추론할 때는 마지막 Affine 계층의 출력을 인식 결과로 이용합니다. 또한, 신경망에서 정규화하지 않는 출력 결과([그림 5-28]에서는 Softmax 앞의 Affine 계층의 출력)를 점수${}^{\text{score}}$라 합니다. 즉, 신경망 추론에서 답을 하나만 내는 경우에는 가장 높은 점수만 알면 되니 Softmax 계층은 필요 없다는 것이죠. 반면, 신경망을 학습할 때는 Softmax 계층이 필요합니다.

이제 소프트맥스 계층을 구현할 텐데, 손실 함수인 교차 엔트로피 오차도 포함하여 ‘Softmax-with-Loss 계층’이라는 이름으로 구현합니다. 먼저 Softmax-with-Loss 계층의 계산 그래프를 살펴보죠(그림 5-29).

그림 5-29 Softmax-with-Loss 계층의 계산 그래프

그림 5-29

[그림 5-29]의 계산 그래프는 [그림 5-30]처럼 간소화할 수 있습니다.

그림 5-30 ‘간소화한’ Softmax-with-Loss 계층의 계산 그래프

그림 5-30

[그림 5-30]의 계산 그래프에서 소프트맥스 함수는 ‘$\text{Softmax}$’ 계층으로, 교차 엔트로피 오차는 ‘$\text{Cross Entropy Error}$’ 계층으로 표기했습니다. 여기에서는 3클래스 분류를 가정하고 이전 계층에서 3개의 입력(점수)을 받습니다. 그림과 같이 $\text{Softmax}$ 계층은 입력 $(a_1, a_2, a_3)$을 정규화하여 $(y_1, y_2, y_3)$를 출력합니다. $\text{Cross Entropy Error}$ 계층은 $\text{Softmax}$의 출력 $(y_1, y_2, y_3)$와 정답 레이블 $(t_1, t_2, t_3)$를 받고, 이들 데이터로부터 손실 $L$을 출력합니다.

[그림 5-30]에서 주목할 것은 역전파의 결과입니다. $\text{Softmax}$ 계층의 역전파는 $(y_1 - t_1, y_2 - t_2, y_3 - t_3)$라는 ‘말끔한’ 결과를 내놓고 있습니다. $(y_1, y_2, y_3)$는 $\text{Softmax}$ 계층의 출력이고 $(t_1, t_2, t_3)$는 정답 레이블이므로 $(y_1 - t_1, y_2 - t_2, y_3 - t_3)$는 $\text{Softmax}$ 계층의 출력과 정답 레이블의 차분인 것이죠. 신경망의 역전파에서는 이 차이인 오차가 앞 계층에 전해지는 것입니다. 이는 신경망 학습의 중요한 성질입니다.

지니의 아름다운 오차 저울 (Softmax-with-Loss)

  • 지니: “도로시! 소프트맥스 함수와 교차 엔트로피 손실 함수가 결합한 Softmax-with-Loss 계층은 정말 놀라운 마법을 부려. 각각의 미분은 아주 지저분하지만, 둘이 합쳐지면 역전파 기울기가 단지 예측값과 정답의 차이인 $\mathbf{y} - \mathbf{t}$로 아주 말끔하게 정리된단다!”
  • 도로시: “와! 천칭 저울에 내 예측 $y$와 정답 $t$를 올려놓았을 때 기우는 무게 차이($y-t$)만큼만 피드백을 앞으로 보내서 가중치를 조율하는 거구나! 수학이 이렇게 완벽하고 직관적으로 연결되다니 소름이 돋아!”
  • 토토: “왈왈! (저울대를 흔들며) 예측과 정답의 차이만큼만 훈련해라 멍!”

소프트맥스 손실 계층과 오차 저울 지니가 천칭 저울 양쪽에 구슬 $y$와 $t$를 얹어 오차 저울대의 평형 차이($y-t$)를 검출하는 비유를 설명하고 있고, 도로시와 토토가 그 직관적인 수학적 대칭 미에 탄성을 지르고 있어요.

그런데 신경망 학습의 목적은 신경망의 출력($\text{Softmax}$의 출력)이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것이었습니다. 그래서 신경망의 출력과 정답 레이블의 오차를 효율적으로 앞 계층에 전달해야 합니다. 앞의 $(y_1 - t_1, y_2 - t_2, y_3 - t_3)$라는 결과는 바로 $\text{Softmax}$ 계층의 출력과 정답 레이블의 차이로, 신경망의 현재 출력과 정답 레이블의 오차를 있는 그대로 드러내는 것입니다.

NOTE_ ‘소프트맥스 함수’의 손실 함수로 ‘교차 엔트로피 오차’를 사용하니 역전파가 $(y_1 - t_1, y_2 - t_2, y_3 - t_3)$로 말끔히 떨어집니다. 사실 이런 말끔한 결과는 우연이 아니라 교차 엔트로피 오차라는 함수가 그렇게 설계되었기 때문입니다. 또, 회귀의 출력층에서 사용하는 ‘항등 함수’의 손실 함수로 ‘평균 제곱 오차’를 이용(“3.5 출력층의 설계” 참고)하는 이유도 이와 같습니다. 즉, ‘항등 함수’의 손실 함수로 ‘평균 제곱 오차’를 사용하면 역전파의 결과가 $(y_1 - t_1, y_2 - t_2, y_3 - t_3)$로 말끔히 떨어집니다.

이쯤에서 구체적인 예를 하나 보는 게 좋겠네요. 가령 정답 레이블이 $(0, 1, 0)$일 때 $\text{Softmax}$ 계층이 $(0.3, 0.2, 0.5)$를 출력했다고 해볼까요? 정답 레이블을 보면 정답의 인덱스는 1입니다. 그런데 출력에서는 이때의 확률이 겨우 0.2(20%)라서, 이 시점의 신경망은 올바른 인식을 못 하고 있습니다. 이 경우 $\text{Softmax}$ 계층의 역전파는 $(0.3, -0.8, 0.5)$라는 커다란 오차를 전파하게 됩니다. 결과적으로 $\text{Softmax}$ 계층의 앞 계층들은 그 큰 오차로부터 큰 깨달음을 얻게 됩니다.

이번에 살펴볼 예는 정답 레이블이 똑같이 $(0, 1, 0)$일 때 $\text{Softmax}$ 계층이 $(0.01, 0.99, 0)$을 출력한 경우입니다(이 신경망은 꽤 정확히 인식하고 있군요). 이 경우 $\text{Softmax}$ 계층의 역전파가 보내는 오차는 비교적 작은 $(0.01, -0.01, 0)$이 됩니다. 이번에는 앞 계층으로 전달된 오차가 작으므로 학습하는 정도도 작아집니다.

소프트맥스 + 손실 차분 오차 그래프 도로시가 출력 확률 $y$ 분포 그래프와 정답 $t$ 원-핫 분포를 비교하며, 그 오차 차분($y-t$)이 가중치 갱신의 가속 동력으로 피드백되는 과정을 그리고 있어요.

도로시의 오차 크기에 따른 학습 폭 깨달음

  • 도로시: “와! 정답 확률이 낮을 때($y_2=0.2$, 정답 $t_2=1$)는 역전파로 피드백되는 오차 값이 $-0.8$로 엄청나게 커서 신경망 가중치들이 ‘큰 충격’을 받고 급속도로 훈련을 시작해! 반대로 정답을 이미 잘 맞출 때($y_2=0.99$, 정답 $t_2=1$)는 오차가 $-0.01$로 매우 미미해서 훈련 가중치가 거의 잔잔하게 유지되는구나!”
  • 지니: “맞아! 오차의 크기 자체가 학습의 보폭(기울기 크기)이 되어 스스로 제어되는 이 수학적 밸런스가 바로 인공지능이 자율적으로 공부하는 비결이란다!”

그럼 $\text{Softmax-with-Loss}$ 계층을 구현한 코드를 보겠습니다.

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 손실
        self.y = None    # softmax의 출력
        self.t = None    # 정답 레이블(원-핫 벡터)
        
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        return self.loss
        
    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        
        return dx

이 구현에서는 “3.5.2 소프트맥스 함수 구현 시 주의점”과 “4.2.2 교차 엔트로피 오차”를 구현한 함수인 softmax()cross_entropy_error()를 이용했습니다. 그 덕분에 이번 구현은 매우 간단합니다. 또, 역전파 때는 전파하는 값을 배치의 수 (batch_size)로 나눠서 데이터 1개당 오차를 앞 계층으로 전파하는 점에 주의하세요.

서브목차