nn.Linear 한 층의 역전파를 손으로 짜 보려고 논문에 적힌 그래디언트 식을 그대로 코드로 옮겼는데 shape 오류가 납니다. 전치를 하나 붙이니 통과합니다. 그러면 논문이 틀린 걸까요, 아니면 전치를 붙인 내 코드가 우연히 돌아가는 걸까요.
둘 다 아닙니다. 행렬 미분에는 배치 규약이 둘 있고, 논문마다 다른 것을 쓰기 때문에 같은 사실이 서로 전치된 모습으로 적힙니다. 지난 글까지 「무엇을 계산하는가」를 닫았으니, 이 글은 그것을 종이 위에 어떻게 적는가를 정리합니다.
목표는 셋입니다 — 규약이 왜 둘인지 알아 남의 식을 읽을 수 있는 것, 자주 쓰는 공식을 성분에서 직접 유도해 보는 것, 그리고 외운 공식이 기억나지 않을 때 shape만으로 복원하는 것입니다. 마지막 것이 실무에서 가장 자주 쓰입니다. 그 뒤로 배치 축, 원소별 함수, 브로드캐스트처럼 실제 코드에서 전치 말고도 어긋나는 자리들을 차례로 짚고, 유한차분으로 검산하는 법으로 끝냅니다.
두 가지 배치 규약
분자 배치와 분모 배치
이 의 함수일 때, 에는 편미분 가 개 들어갑니다. 이것을 담을 표의 모양이 두 가지이고, 어느 쪽을 행에 둘 것인가만 다릅니다.
분자 배치(numerator layout)는 분자의 크기를 행에 둔다 — 모양이 이다. 앞에서 다룬 야코비안이 이 규약이다. 분모 배치(denominator layout)는 분모의 크기를 행에 둔다 — 모양이 이고, 분자 배치를 전치한 것이다.
두 표에 든 수는 완전히 같습니다. 분자 배치의 칸에 있던 가 분모 배치에서는 칸으로 옮겨 앉을 뿐이라, 정보가 늘지도 줄지도 않습니다. 그래서 둘 중 무엇을 쓰느냐는 옳고 그름의 문제가 아니라 표기의 선택이고, 선택이 갈리면 같은 식이 전치된 모양으로 나타납니다.
스칼라 손실의 그래디언트
손실처럼 분자가 스칼라()인 경우가 실무에서 가장 흔한데, 여기서 차이가 눈에 띕니다.
| 의 모양 | 부르는 이름 | |
|---|---|---|
| 분자 배치 | · 행벡터 | 야코비안 |
| 분모 배치 | · 열벡터 | 그래디언트 |
스칼라 하나를 벡터 로 미분한 결과를 와 같은 모양의 열로 세운 것이 그래디언트이고, 분모 배치로 적은 야코비안이 곧 그래디언트입니다. 지난 글에서 VJP의 결과를 짜리 행으로 적은 것은 분자 배치를 따랐기 때문이고, p.grad가 p와 같은 모양인 것은 프레임워크가 분모 배치 쪽에 서 있기 때문입니다.
규약을 밝히지 않은 글을 읽을 때는 이 표를 거꾸로 씁니다. 글 안에서 스칼라 손실을 벡터로 미분한 식을 하나 찾아 그 결과가 행인지 열인지 봅니다. 처럼 행벡터가 앞에서 곱해진 꼴이면 행, 즉 분자 배치이고, 처럼 열이 나오거나 결과를 파라미터에서 바로 빼는 갱신식이 있으면 열, 즉 분모 배치입니다. 갱신식을 적은 논문이라면 이 한 줄로 판정이 끝납니다.
연쇄법칙의 곱 순서
규약이 가장 크게 드러나는 곳은 연쇄법칙입니다. , 를 이어 붙이면 분자 배치에서는
처럼 출력에 가까운 것이 왼쪽에 옵니다. 모양으로 보면 (z의 크기 × y의 크기)와 (y의 크기 × x의 크기)가 가운데 y에서 맞물립니다. 분모 배치에서는 두 인자를 각각 전치해야 하고, 곱의 전치는 순서를 뒤집으므로
로 입력에 가까운 것이 왼쪽에 옵니다. 같은 연쇄가 한쪽에서는 왼쪽으로, 다른 쪽에서는 오른쪽으로 자랍니다. 분자 배치의 야코비안 와 로 스칼라 손실에 대해 이 순서를 적으면 분모 배치의 가 나오는데, 이것이 지난 글의 VJP를 열로 세운 모습입니다.
이 글의 규약
이 글은 아래 규약을 씁니다.
- 손실에 대한 미분은 대상과 같은 모양으로 적는다. 는 와 같은 모양이고, 는 와 같은 모양이다. (분모 배치)
- 벡터함수의 미분은 (출력 × 입력)으로 적는다. 지난 글의 야코비안 그대로다. (분자 배치)
두 규약을 섞은 것처럼 보이지만, 이것이 실제 코드와 가장 잘 맞습니다. 그래디언트는 파라미터에서 빼야 하니 파라미터와 모양이 같아야 하고, 야코비안은 곱해서 넘겨야 하니 (출력 × 입력)이 편합니다.
남의 식을 이 규약으로 옮길 때 전치를 어디에 넣을지도 이 두 줄이 정합니다. 손실 미분이 행으로 적혀 있으면 통째로 전치해 대상과 같은 모양으로 세우고, 야코비안이 (입력 × 출력)으로 적혀 있으면 곱하는 쪽에서 전치를 붙입니다. 한 식 안에서 규약이 섞인 논문도 있는데, 그때도 식 전체를 뜯어고치지 말고 모양 하나를 기준으로 삼아 결과만 맞추는 편이 빠릅니다. 기준으로 삼을 모양이 바로 「파라미터와 같은 모양」입니다.
성분에서 공식 만들기
행렬 미분 공식은 전부 성분 하나를 골라 보통 미분을 하고, 그 결과를 다시 행렬로 묶는 것입니다. 세 개를 이 절차로 만들어 보고, 첨자가 많아질 때 쓰는 더 짧은 길을 하나 덧붙입니다.
선형 사상의 야코비안
의 성분은 입니다. 로 미분하면 그 항 하나만 남으므로
입니다. 야코비안의 성분이 이니 야코비안이 그 자체입니다. 지난 글의 표에 적어 둔 첫 줄이 이 두 줄짜리 유도였습니다.
가중치의 그래디언트
이번에는 로 미분합니다. 뒤에서 흘러온 값을 이라 두겠습니다.
는 하나에만 들어갑니다. 다른 출력 ()의 식에는 가 들어가지 가 아니기 때문입니다. 그래서 연쇄법칙의 합이 한 항으로 줄어듭니다.
이제 이 값들을 와 같은 모양으로 다시 묶습니다. 자리에 가 오는 행렬은
입니다. 과 의 곱이라 결과가 이고, 이런 열벡터 곱하기 행벡터를 외적(outer product)이라고 부릅니다.
수로 확인합니다. , 이면 입니다. 손실을 로 두고 이라 하면 이고
입니다. 가운데 열이 통째로 0인 것을 눈여겨봅니다. 이라 그 입력에 붙은 가중치는 이번 표본에서 아무 일도 하지 않았고, 그래서 갱신도 받지 않습니다.
같은 방식으로 입력 쪽 미분도 나옵니다. 는 모든 에 들어가므로 이번에는 합이 남습니다.
우리 수로는 입니다. 가중치 쪽은 합이 사라져 외적이 되고 입력 쪽은 합이 남아 행렬-벡터 곱이 되는 이 대비가, 한 변수가 식에 몇 군데 들어가느냐에서 바로 나옵니다.
이차형식의 미분
를 이차형식(quadratic form)이라고 하고, 성분으로 풀면 입니다. 여기서 가 들어가는 자리가 둘입니다 — 앞의 로 한 번(), 뒤의 로 한 번()입니다.
이므로 묶으면 입니다. 가 대칭이면 로 줄어들고, 그래서 이차형식을 다룰 때 를 대칭으로 잡는 관례가 생겼습니다.
, 로 확인합니다. 이므로 답은 입니다. 직접 전개해도 이고 편미분이 로 맞습니다.
미분 형식
성분 유도는 확실하지만 첨자가 셋 넘게 얽히면 합 기호를 쫓다 길을 잃기 쉽습니다. 그럴 때 쓰는 짧은 길이 미분 형식입니다. 를 아주 조금 만큼 흔들었을 때 손실이 변하는 양 을 적는 것을 미분 형식(differential form)이라고 하며, 이 스칼라이면 그 변화는 언제나
꼴로 쓸 수 있습니다. 여기서 은 정사각행렬의 대각 성분을 더한 대각합(trace)이고, 의 대각을 더하면 두 행렬의 같은 칸끼리 곱해 더한 값이 됩니다. 규칙은 하나입니다 — 을 이 꼴로 정리했을 때 앞에 전치되어 붙은 가 곧 이고, 모양은 저절로 와 같습니다.
앞의 를 이 방법으로 다시 얻어 봅니다. 에서 이고, 스칼라 손실의 변화는 이므로
입니다. 두 번째 등호는 스칼라를 행렬로 보고 대각합을 씌운 것이고, 세 번째는 대각합이 곱의 순서를 돌려도 변하지 않는 순환 성질 를 쓴 것입니다. 앞에 가 섰으니 이고, 성분으로 한 결과와 같습니다.
이차형식도 두 줄입니다. 인데, 앞 항은 스칼라라 전치해도 같으므로 로 바뀌고, 둘을 합치면 입니다. 합 기호가 한 번도 안 나왔습니다. 층이 여럿 겹치거나 행렬 셋이 곱해진 식에서는 이 길이 성분보다 훨씬 짧고, 순서를 돌리는 수고는 순환 성질이 다 맡습니다.
shape로 복원하기
실무에서 진짜로 쓰는 기술은 이쪽입니다. 공식이 기억나지 않아도 모양만 적어 두면 답이 거의 정해집니다.
세 걸음 절차
절차는 셋입니다.
- 재료의 모양을 적는다. 손에 있는 것이 무엇인지 — 는 , 는 , 는 .
- 답의 모양을 적는다. 규약대로 「미분 대상과 같은 모양」이다 — 라면 .
- 그 모양이 나오는 조합을 찾는다. 대개 하나뿐이다.
로 해 봅니다. 재료가 와 이고 답이 이니, 곱할 수 있는 방식은 둘뿐입니다.
하나만 살아남습니다. 도 같습니다 — 재료가 와 , 답이 이니 뿐이고 는 모양이 안 맞습니다.
이 절차가 통하는 까닭은 앞 절의 두 유도가 보여 줍니다. 선형인 식의 그래디언트는 언제나 재료 둘을 한 번씩 써서 곱한 꼴이고, 곱의 모양은 어느 축을 맞물려 없애느냐로만 갈립니다. 가능한 곱의 가짓수가 적으니 모양이라는 조건 하나로 대부분 걸러집니다.
자주 쓰는 공식
자주 쓰는 것들을 모양과 함께 적어 둡니다.
| 식 | 미분 | 재료의 모양 | 답의 모양 |
|---|---|---|---|
마지막 줄은 다섯째 줄에서 를 단위행렬로 둔 경우라 로 곧바로 따라 나옵니다. 표를 외울 필요가 없는 이유가 여기 있습니다 — 줄끼리 서로 이어져 있어서 하나를 알면 옆 줄이 나옵니다.
후보가 둘 남는 경우
shape가 맞는다고 답이 맞는 것은 아닙니다. 두 조합이 같은 모양을 낼 때가 있고, 그때는 이 기술이 갈라 주지 못합니다. 그런 자리는 미리 알아볼 수 있는데, 경우가 둘입니다.
- 정사각행렬이 낄 때. 와 는 둘 다 이라 모양으로는 구별되지 않습니다. 위 표의 다섯째 줄이 정확히 그 경우이고, 그래서 그 공식만은 성분으로 유도해야 했습니다. 도 도 도 전부 입니다.
- 두 재료의 모양이 같을 때. 인 층에서는 와 가 둘 다 이 되어 첫 걸음에서 살아남던 유일한 후보가 둘로 늘어납니다. 은닉 크기가 같은 층을 쌓는 트랜스포머에서는 이런 정사각 가중치가 흔합니다.
모양은 후보를 좁히는 도구이지 증명이 아닙니다. 후보가 여럿 남으면 앞 절의 성분이나 미분 형식으로 돌아가고, 하나로 좁혀졌더라도 미덥지 않으면 유한차분으로 한 번 확인하면 됩니다. 검산할 때 층의 크기를 일부러 으로 잡는 것도 요령입니다. 정사각이 아니면 틀린 전치가 곧바로 shape 오류로 드러나므로, 실제 모델이 정사각이더라도 검산용 작은 층은 직사각으로 만듭니다.
배치 차원
실제 코드는 표본 하나가 아니라 배치를 한꺼번에 굴립니다. 여기서 전치가 한 번 더 헷갈리므로 따로 짚어 둡니다.
배치 행렬곱
nn.Linear의 계산은 이고 모양이 이렇습니다.
같은 shape 기술을 씁니다. 는 이어야 하는데, 재료는 와 입니다. 를 가운데 두고 없애야 이 나오므로
이고, 입력 쪽은 로 입니다.
가 곱의 가운데에서 사라지는 것이 핵심입니다. 행렬곱의 정의가 그 자리에서 합을 하므로, 는 표본마다의 외적 를 전부 더한 값입니다.
여기서 과 은 와 의 번째 행을 열로 세운 것입니다. 표본 하나일 때의 가 배치에서도 그대로 살아 있고, 행렬곱이 그 합을 한 번에 해 줄 뿐입니다.
합과 평균
손실을 표본마다의 손실 의 합으로 정의했는지 평균으로 정의했는지는 식에 하나로 나타납니다. 이면 의 각 행이 에 을 곱한 값이 되고, 그래서 입니다.
이 차이가 모양에는 전혀 안 드러난다는 점이 함정입니다. 합으로 정의하면 배치를 두 배로 키울 때 그래디언트도 대략 두 배가 되어 같은 학습률이 사실상 두 배로 커지고, 평균으로 정의하면 배치 크기와 무관하게 그래디언트의 크기가 유지됩니다. 프레임워크의 손실 함수가 기본으로 평균을 내는 것도 그 때문입니다. 손으로 쓴 역전파를 프레임워크와 비교했는데 모든 값이 정확히 배 어긋난다면, 전치가 아니라 이 정의부터 의심합니다.
시퀀스 축과 마스크
언어 모델에서는 입력이 처럼 축이 셋입니다. 는 시퀀스 길이이고, 같은 가 모든 표본의 모든 위치에 똑같이 쓰입니다. 이때는 앞의 두 축을 하나로 접어 짜리 행렬로 보면 식이 전혀 바뀌지 않습니다 — 에서 가운데 축이 대신 가 되어, 표본과 위치 전부에 걸친 외적의 합이 됩니다.
길이가 다른 문장을 한 배치에 담으려고 뒤를 빈칸으로 채웠다면 그 빈칸 위치가 그래디언트에 섞이면 안 됩니다. 0과 1로 된 마스크 을 만들어 빈칸 자리를 0으로 두고, 역전파에서 의 각 행에 곱합니다. 그러면 그 행이 0이 되어 외적의 합에서 통째로 빠집니다. 평균을 낼 때는 가 아니라 마스크의 합, 곧 실제 토큰 수로 나누어야 빈칸이 많은 배치의 그래디언트가 부당하게 작아지지 않습니다.
원소별 함수와 브로드캐스트
선형층 사이에는 활성화 함수가 끼고, 편향처럼 작은 텐서가 큰 텐서에 더해집니다. 둘 다 전치 문제는 없지만 다른 방식으로 어긋나기 쉬운 자리입니다.
대각 야코비안
처럼 함수를 벡터의 칸마다 따로 적용하는 것을 원소별 함수라고 합니다. 는 하나에만 의존하므로 는 이면 0이고, 야코비안은 대각에만 가 선 대각행렬 입니다.
대각행렬을 곱하는 것은 칸마다 곱하는 것과 같으므로 역전파는
한 줄로 줄어듭니다. 여기서 은 같은 모양의 두 벡터를 칸끼리 곱하는 원소곱입니다. 행렬을 만들어 곱하는 대신 길이 짜리 곱 한 번이면 되니, 프레임워크가 활성화 함수의 야코비안을 실제로 만들지 않는 이유가 이것입니다.
수로 봅니다. ReLU는 이면 기울기가 1, 아니면 0입니다. 에서 뒤로 이 왔다면 입니다. 음수였던 첫 칸은 그래디언트를 통째로 막습니다. 원소별이면 제곱이든 나눗셈이든 같은 규칙입니다 — 이면 , 상수 로 나눈 이면 입니다. 원소별 함수의 역전파에서는 모양이 절대 바뀌지 않으므로, 여기서 모양이 달라졌다면 무언가 원소별이 아닌 것이 끼어 있다는 신호입니다.
브로드캐스트와 합
에서 는 인데 는 입니다. 크기가 다른 두 텐서를 더할 수 있는 것은 작은 쪽을 없는 축 방향으로 복제해 모양을 맞추기 때문이고, 이 자동 복제를 브로드캐스트라고 합니다. 복제된 는 개의 행 전부에 더해지니, 를 조금 흔들면 개의 출력이 한꺼번에 흔들립니다. 그래서 연쇄법칙의 합이 그 개를 모두 모읍니다.
규칙으로 적으면 이렇습니다 — 순전파에서 브로드캐스트로 늘린 축은 역전파에서 그 축을 따라 더해 원래 모양으로 되돌린다. 복제와 합이 서로 짝이고, 이 짝은 지난 글의 VJP로도 설명됩니다. 복제는 선형 사상이고 그 전치가 합이기 때문입니다.
아래 코드의 수로 따라갑니다. 가 두 표본에 똑같이 복제되어 더해지고, 역전파에서 돌아온 의 두 행이 와 이면 입니다. 모양이 에서 로 줄어 와 같아졌고, 규약도 지켜졌습니다. 코드에서 delta.sum(axis=0)으로 적히는 그 줄입니다.
조용한 브로드캐스트
브로드캐스트는 편하지만 shape 검산을 무력하게 만들기도 합니다. 모양이 안 맞는 두 텐서를 오류 없이 합쳐 버리기 때문입니다. 가장 흔한 사고는 예측이 이고 정답이 일 때 일어납니다. 둘을 빼면 오류 대신 짜리 행렬이 나오고, 그 제곱을 평균 내면 손실은 멀쩡한 스칼라입니다. 모양은 맞고 값만 틀립니다 — 모든 예측이 모든 정답과 비교되고 있기 때문입니다.
앞의 「후보가 둘 남는 경우」와 같은 종류의 함정이지만 원인이 다릅니다. 그때는 수학적으로 후보가 둘이었고, 이번에는 라이브러리가 없던 후보를 만들어 냅니다. 막는 법은 중간 텐서의 모양을 한 번씩 찍어 보거나 단언문으로 못 박아 두는 것이고, 무엇보다 손실처럼 스칼라로 끝나는 값은 모양 검사로 아무것도 알려 주지 않는다는 것을 기억하는 것입니다.
코드로 확인하기
손 유도와 유한차분
import copy
mm = lambda A, B: [[sum(A[i][k] * B[k][j] for k in range(len(B)))
for j in range(len(B[0]))] for i in range(len(A))]
tr = lambda A: [list(r) for r in zip(*A)]
W = [[1., 2., 3.], [4., 5., 6.]] # (m, n) = (2, 3)
b = [0.5, -0.5] # (m,)
X = [[1., 0., -1.], [2., 1., 0.]] # (B, n) = (2, 3)
T = [[-2., -3.], [4., 13.]] # 정답 (B, m)
def forward(W, b, X):
return [[sum(X[r][j] * W[i][j] for j in range(3)) + b[i] for i in range(2)]
for r in range(len(X))]
def loss(W, b, X):
Y = forward(W, b, X)
return 0.5 * sum((Y[r][i] - T[r][i]) ** 2 for r in range(len(X)) for i in range(2))
Y = forward(W, b, X)
print(Y, round(loss(W, b, X), 6)) # [[-1.5, -2.5], [4.5, 12.5]] 0.5
# ① 손으로 유도한 그래디언트 — shape 로 복원한 조합 그대로
D = [[Y[r][i] - T[r][i] for i in range(2)] for r in range(2)] # (B, m)
gW = mm(tr(D), X) # (m,B)(B,n) = (m,n)
gb = [sum(D[r][i] for r in range(2)) for i in range(2)] # 배치 축 합
gX = mm(D, W) # (B,m)(m,n) = (B,n)
print(gW) # [[1.5, 0.5, -0.5], [-0.5, -0.5, -0.5]]
print(gb) # [1.0, 0.0]
print(gX) # [[2.5, 3.5, 4.5], [-1.5, -1.5, -1.5]]
# ② 유한차분으로 검산
eps = 1e-6
num = [[0.] * 3 for _ in range(2)]
for i in range(2):
for j in range(3):
p, m_ = copy.deepcopy(W), copy.deepcopy(W)
p[i][j] += eps; m_[i][j] -= eps
num[i][j] = round((loss(p, b, X) - loss(m_, b, X)) / (2 * eps), 6)
print(num) # [[1.5, 0.5, -0.5], [-0.5, -0.5, -0.5]]
# ③ 전치를 뒤집으면 shape 부터 어긋난다
print(len(gW), len(gW[0])) # 2 3 ← W 와 같다
wrong = mm(tr(X), D) # (n,B)(B,m) = (n,m)
print(len(wrong), len(wrong[0])) # 3 2 ← W 와 다르다
# ④ 이차형식은 shape 로 갈리지 않으므로 성분으로 확인한다
A = [[1., 2.], [3., 4.]]
x = [1., 2.]
q = lambda x: sum(x[i] * A[i][j] * x[j] for i in range(2) for j in range(2))
print(q(x)) # 27.0
S = [[A[i][j] + A[j][i] for j in range(2)] for i in range(2)] # A + Aᵀ
print([sum(S[i][j] * x[j] for j in range(2)) for i in range(2)]) # [12.0, 21.0]
eps = 1e-6
print([round((q([x[0] + eps * (k == 0), x[1] + eps * (k == 1)])
- q([x[0] - eps * (k == 0), x[1] - eps * (k == 1)])) / (2 * eps), 4)
for k in range(2)]) # [12.0, 21.0]
# A 만 써도, Aᵀ 만 써도 모양은 (2,) 로 똑같이 맞는다 — 값이 틀릴 뿐이다
print([sum(A[i][j] * x[j] for j in range(2)) for i in range(2)]) # [5.0, 11.0]
②와 ④가 이 글이 권하는 두 가지 확인입니다. ②는 shape로 복원한 조합이 옳았음을, ④는 shape만으로는 못 잡는 자리가 실제로 있다는 것을 보여 줍니다 — 마지막 줄의 도 모양은 완벽히 맞지만 답이 아닙니다. 그리고 ①의 gb가 앞 절에서 손으로 더한 그대로입니다.
중심차분의 오차
②에서 쓴 것은 으로, 양쪽으로 같은 거리만큼 흔들어 기울기를 재는 중심차분입니다. 한쪽으로만 흔드는 보다 정확한 이유는 테일러 전개에서 나옵니다. 를 빼면 짝수 차수 항이 서로 지워지므로
이고 오차가 에 비례합니다. 한쪽 차분은 항이 남아 오차가 에 비례합니다. 을 에서 로 재면 한쪽 차분은 , 중심차분은 입니다. 참값 3과의 차이가 각각 약 과 이고, 뒤쪽은 공식대로 입니다.
그렇다면 을 한없이 작게 잡으면 될 것 같지만 그렇지 않습니다. 테일러 전개를 끊어서 생기는 절단 오차는 이 작을수록 줄지만, 컴퓨터가 수를 유한한 자릿수로 저장해서 생기는 반올림 오차는 거의 같은 두 수를 빼고 작은 으로 나누는 과정에서 오히려 커집니다. float64의 상대 정밀도를 이라 하면 반올림 쪽이 대략 , 절단 쪽이 이고, 둘의 합은 , 곧 에서 사이에서 가장 작습니다. 코드가 을 쓴 것이 그 언저리이고, 그때 기대할 수 있는 정확도는 소수 열 자리 안팎입니다. float32로 같은 검산을 하면 가 약 이라 최적 도 언저리로 커지고 정확도는 크게 떨어지므로, 검산은 float64로 합니다.
상대오차 판정
손 유도 값 와 유한차분 값 을 비교할 때 절대오차 만 보면 기준을 정할 수 없습니다. 그래디언트가 인 칸의 차이는 완벽한 일치이지만, 그래디언트가 인 칸에서는 전부 틀린 것입니다. 그래서 크기로 나눈 상대오차
로 판정합니다. float64와 중심차분이면 아래는 맞은 것, 위는 거의 확실히 틀린 것으로 봅니다.
다만 두 값이 모두 0에 가까우면 분모가 0에 가까워져 상대오차가 의미를 잃습니다. 앞의 한 표본 예에서 이라 통째로 0이던 가운데 열이 그런 칸입니다. 거기서는 반올림 잡음만으로 상대오차가 1까지 튈 수 있으니, 분모에 작은 하한을 두거나 그 칸만 따로 절대오차로 봅니다. ReLU처럼 꺾인 점이 있는 함수는 가 0 근처인 칸에서 유한차분이 꺾인 점을 가로질러 엉뚱한 값을 내기도 하므로, 그런 칸이 어긋나면 먼저 입력을 조금 옮겨 다시 재 봅니다.
정리
요점
- 행렬 미분에는 분자 배치와 분모 배치 두 규약이 있고 서로 전치다. 논문마다 전치가 달라 보이는 이유가 이것이다.
- 어느 쪽이 옳은 것이 아니라 섞지 않는 것이 규칙이다. 남의 식은 스칼라 손실의 그래디언트 모양을 보면 어느 규약인지 알 수 있다 — 행이면 분자, 열이면 분모다. 연쇄법칙의 곱이 왼쪽으로 자라는지 오른쪽으로 자라는지도 같은 신호다.
- 이 글의 규약은 손실 미분은 대상과 같은 모양, 야코비안은 (출력 × 입력)이다. 코드와 가장 잘 맞는다.
- 모든 공식은 성분 하나를 골라 미분하고 다시 묶어 만든다. 첨자가 많으면 꼴로 정리해 를 읽는 미분 형식이 더 짧다.
- 는 외적이라 자연히 와 같은 모양이다. 입력이 0인 자리의 열은 통째로 0이 되어 갱신을 받지 않는다.
- 공식이 기억나지 않으면 재료의 모양과 답의 모양을 적고 그것이 나오는 조합을 찾는다. 그러나 정사각행렬이 끼거나 두 재료의 모양이 같으면 후보가 둘 남고, 그때는 성분으로 돌아가야 한다.
- 배치가 끼면 이고 는 곱의 가운데에서 합쳐진다. 평균 손실이면 이 붙고, 축이 셋이면 앞의 둘을 접고, 빈칸은 마스크로 뺀다.
- 원소별 함수의 역전파는 원소곱 한 줄이고, 브로드캐스트로 늘린 축은 역전파에서 더한다. 편향이 배치 축 합인 것이 그 한 예다.
- 검산은 float64 중심차분에 , 판정은 상대오차로 하고 0에 가까운 칸은 따로 본다.
처음의 두 식
처음의 물음으로 돌아갑니다. nn.Linear의 역전파를 짜다 만난 논문 A의 와 논문 B의 는 같은 사실을 다른 규약으로 적은 것이고, 전치를 붙여 통과한 내 코드도 우연이 아니라 규약을 맞춘 것이었습니다. 코드로 옮길 때 봐야 할 것은 어느 쪽이 「옳은가」가 아니라 내 텐서의 모양에 맞는 쪽이 어느 것인가이고, 그 판단은 모양 셋을 적는 것으로 끝납니다. 다만 그 층이 정사각이었다면 틀린 전치도 통과했을 테니, 마지막 확인은 유한차분 한 번이 맡습니다.
이제 도구가 다 모였습니다. 연쇄법칙, 야코비안, VJP, 그리고 종이 위에 적는 규약까지 — 이 넷으로 실제 층 하나의 그래디언트를 끝까지 유도할 수 있습니다. 다음 글에서 모든 분류 모델의 마지막 층을 그렇게 유도하면, 복잡해 보이던 행렬 하나가 깨끗하게 소거되어 놀랄 만큼 짧은 식이 남습니다.
읽어주셔서 감사합니다. 😊

