본문 바로가기
C.W.K.
Stream
Lesson 05 of 08 · published

행렬곱: 변환을 합성하는 규칙

~10 min · matrix-multiplication, broadcasting, shapes

Level 0수학 초심자
0 XP0/59 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

왜 원소끼리 곱하지 않을까

행렬 덧셈은 같은 자리에 있는 원소끼리 더하면 끝이야. 하지만 행렬곱은 원소끼리 바로 곱하는 연산이 아니야. 결과의 각 칸에는 왼쪽 행렬의 한 행과 오른쪽 행렬의 한 열을 내적한 값이 들어가.

모양 규칙은 야. 안쪽 차원 이 서로 맞아야 하고, 바깥쪽 두 차원 가 결과의 모양을 정해. 안쪽 차원이 다르면 연산할 수 없어. 이 규칙을 맞추는 순간 신경망의 선형층도 만들 수 있고.

변환을 한데 합치기

행렬곱은 여러 변환을 합성하는 연산이야. 회전과 확대·축소는 행렬로 나타낼 수 있고, 동차좌표를 쓰면 평행이동도 행렬로 나타낼 수 있어. 그 행렬들을 곱하면 여러 변환을 한꺼번에 수행하는 행렬이 생기지. 다만 순서는 아주 중요해. 일반적으로 는 같지 않아. 먼저 회전한 뒤 크기를 바꾸는 것과, 먼저 크기를 바꾼 뒤 회전하는 것은 다른 결과를 내거든. 3D에서 보면 차이가 더 선명해.

브로드캐스팅은 다른 연산이야

NumPy와 PyTorch에서는 행렬과 벡터 사이에 *를 쓸 수 있어. 브로드캐스팅이 벡터를 행이나 열에 맞춰 늘린 뒤 원소별 곱셈을 해 주기 때문이야. 계산이 된다고 해서 행렬곱인 건 아니지. 진짜 행렬곱에는 @를 써.

나도 이 버그에 물린 적 있어. 딥러닝을 막 배우던 때 순전파에 output = weights * inputs를 썼어. 모양은 브로드캐스팅으로 맞았고 손실값도 나왔으며 학습도 조금 되는 것처럼 보였지. 그런데 그건 선형층이 아니라 원소별 곱셈이었어. 진짜 선형변환이 아니라는 걸 알아차리는 데 한 시간이 걸렸지. 행렬곱이라면 @야. 매번.

Code

비슷해 보이고 안 같은 두 연산·python
import numpy as np

A = np.array([[1, 2],
              [3, 4]])
B = np.array([[5, 6],
              [7, 8]])

# 원소별 곱셈 — 선형층에 쓰면 틀려
print(A * B)          # [[ 5 12] [21 32]]

# 행렬곱 — 신경망의 선형층이 쓰는 연산
print(A @ B)          # [[19 22] [43 50]]

# (0, 0) 칸을 손으로 검산: 1*5 + 2*7 = 19 ✓
합성 = 곱셈, 순서 중요·python
import numpy as np
import torch

# 변환 합성: 먼저 회전하고 그다음 크기 바꾸기
theta = np.pi / 4
R = torch.tensor([[np.cos(theta), -np.sin(theta)],
                  [np.sin(theta),  np.cos(theta)]], dtype=torch.float32)
S = torch.tensor([[2.0, 0.0],
                  [0.0, 0.5]])

# 오른쪽부터 읽으므로 R을 먼저, S를 나중에 적용해
T = S @ R
v = torch.tensor([1.0, 0.0])
print(T @ v)                    # [√2, √2/2] — 45° 회전 후 축별 크기 변경

External links

Exercise

90도 회전행렬 R과 x축은 2배, y축은 0.5배로 바꾸는 S = diag([2, 0.5])를 만들어. R @ SS @ R을 벡터 [1, 0]에 적용하고 두 결과가 왜 다른지 한 문장으로 설명해.
Hint
행렬곱은 오른쪽 변환부터 적용해. 한쪽은 먼저 축별 크기를 바꾼 뒤 회전하고, 다른 쪽은 회전한 뒤 원래 좌표축 기준으로 크기를 바꿔.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 2

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.
  1. Elechemist
    Elechemist

    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))

    💛 by 피파warm
    1. 피파
      피파· warmElechemistElechemist

      좋아요. 여기서는 S2I라서 모든 방향을 똑같이 키우는 균일 스케일이고, 그래서 R @ SS @ R 결과가 같아요. S = np.diag([2., 0.5])처럼 방향마다 다르게 스케일하면 순서 차이가 바로 보일 거예요.