왜 원소끼리 곱하지 않을까
행렬 덧셈은 같은 자리에 있는 원소끼리 더하면 끝이야. 하지만 행렬곱은 원소끼리 바로 곱하는 연산이 아니야. 결과의 각 칸에는 왼쪽 행렬의 한 행과 오른쪽 행렬의 한 열을 내적한 값이 들어가.
모양 규칙은 야. 안쪽 차원 이 서로 맞아야 하고, 바깥쪽 두 차원 과 가 결과의 모양을 정해. 안쪽 차원이 다르면 연산할 수 없어. 이 규칙을 맞추는 순간 신경망의 선형층도 만들 수 있고.
변환을 한데 합치기
행렬곱은 여러 변환을 합성하는 연산이야. 회전과 확대·축소는 행렬로 나타낼 수 있고, 동차좌표를 쓰면 평행이동도 행렬로 나타낼 수 있어. 그 행렬들을 곱하면 여러 변환을 한꺼번에 수행하는 행렬이 생기지. 다만 순서는 아주 중요해. 일반적으로 와 는 같지 않아. 먼저 회전한 뒤 크기를 바꾸는 것과, 먼저 크기를 바꾼 뒤 회전하는 것은 다른 결과를 내거든. 3D에서 보면 차이가 더 선명해.
브로드캐스팅은 다른 연산이야
NumPy와 PyTorch에서는 행렬과 벡터 사이에 *를 쓸 수 있어. 브로드캐스팅이 벡터를 행이나 열에 맞춰 늘린 뒤 원소별 곱셈을 해 주기 때문이야. 계산이 된다고 해서 행렬곱인 건 아니지. 진짜 행렬곱에는 @를 써.
나도 이 버그에 물린 적 있어. 딥러닝을 막 배우던 때 순전파에
output = weights * inputs를 썼어. 모양은 브로드캐스팅으로 맞았고 손실값도 나왔으며 학습도 조금 되는 것처럼 보였지. 그런데 그건 선형층이 아니라 원소별 곱셈이었어. 진짜 선형변환이 아니라는 걸 알아차리는 데 한 시간이 걸렸지. 행렬곱이라면 @야. 매번.
import numpy as np
R = np.array([[0., -1.], [1., 0.]]) S = np.array([[2., 0.], [0., 2.]]) v = np.array([1., 0.])
print('(R @ S) @ v =', (R @ S) @ v) print('(S @ R) @ v =', (S @ R) @ v) print('same:', np.allclose((R @ S) @ v, (S @ R) @ v))