어텐션 구현을 열면 거의 언제나 이 세 줄이 먼저 나옵니다.
self.q_proj = nn.Linear(768, 64, bias=False)
self.k_proj = nn.Linear(768, 64, bias=False)
self.v_proj = nn.Linear(768, 64, bias=False)
이것을 "질의·키·값으로 투영한다"고 부릅니다. 그런데 q_proj.weight 를 찍어 보면 그냥 64×768짜리 숫자 덩어리이고, 그 49,152개의 숫자를 아무리 들여다봐도 무슨 일이 일어나는지는 보이지 않습니다.
행렬을 숫자표로 읽으면 답이 안 나옵니다. 행렬은 표가 아니라 함수이고, 그것도 아주 특별한 종류의 함수입니다. 이 글은 그 함수의 정체를 정의부터 세우고, 행렬의 열에 무엇이 적혀 있는지를 밝히고, 그 함수가 무엇을 남기고 무엇을 버리는지 센 뒤, 두 층을 겹치는 일이 왜 곱셈이 되는지까지 갑니다. 마지막에 그 눈으로 첫머리의 세 줄로 돌아옵니다.
선형사상의 두 조건
가법성과 동차성
정의. 함수 이 모든 와 모든 실수 에 대해
를 만족하면 선형사상이라고 한다.
앞의 식을 가법성, 뒤의 식을 동차성이라고 부릅니다. 말로 옮기면 가법성은 "더한 뒤 보내나 보낸 뒤 더하나 같다"이고, 동차성은 "늘린 뒤 보내나 보낸 뒤 늘리나 같다"입니다. 두 조건을 합치면 한 문장이 됩니다.
선형사상은 선형결합을 그대로 통과시킵니다. 지난 글에서 선형결합이 이 커리큘럼에서 가장 자주 나오는 조작이라고 했는데, 선형사상은 정확히 그 조작과 사이좋게 지내는 함수만 골라낸 것입니다. 이 글에서 앞으로 나오는 결론은 전부 이 한 줄에서 나옵니다.
원점 대칭
동차성에 특별한 수 둘을 넣으면 조건이 눈에 보이는 모양으로 바뀝니다. 먼저 을 넣으면 입니다. 선형사상은 원점을 반드시 원점으로 보냅니다.
다음으로 을 넣으면 입니다. 입력을 원점 반대편으로 뒤집으면 출력도 원점 반대편으로 뒤집혀야 한다는 뜻이고, 이런 함수를 원점 대칭이라고 합니다. 그래프로 치면 원점을 중심으로 반 바퀴 돌려도 겹치는 모양입니다.
이 둘은 판정 도구로 아주 쓸모가 있습니다. 두 조건을 모든 입력에 대해 확인하기는 어렵지만, 반례는 하나만 찾으면 되기 때문입니다. 활성화 함수 ReLU는 음수 성분을 0으로 바꾸는 함수인데, 를 넣으면
로 원점 대칭이 깨집니다. ReLU는 원점을 원점으로 보내므로 검사는 통과하는데, 에서 걸립니다. 원점을 지키는 것만으로는 선형이 아니라는 좋은 예입니다.
층의 조각 판정
같은 방식으로 신경망 한 층에 들어 있는 조각들을 하나씩 걸어 봅니다. 가장 흔한 조각이 편향인데, 곱한 결과에 입력과 상관없이 늘 같은 벡터 를 더하는 부분입니다.
| 조각 | 선형사상인가 | 걸리는 조건 |
|---|---|---|
| 그렇다 | — | |
| 아니다 | ||
| 아니다 | ||
| 아니다 | 합이 늘 1이라 | |
| LayerNorm | 아니다 | 나누는 값이 입력에 따라 달라진다 |
편향은 가장 약한 조건인 원점 검사에서 바로 떨어지고, 가법성도 깨집니다. 에는 가 두 번 들어가는데 에는 한 번만 들어가기 때문입니다. softmax는 출력의 합을 1로 묶어 두므로 입력을 두 배 해도 출력의 합이 2가 될 수 없고, LayerNorm은 입력의 표준편차로 나누므로 입력을 키우면 나누는 수도 같이 커져 출력이 그대로 남습니다.
이 표에서 딥러닝의 구조적인 사실 하나가 곧바로 나옵니다. 비선형이 하나도 없으면 층을 아무리 쌓아도 한 층입니다. 는 뒤의 합성 절에서 보듯 하나로 접히고, 그러면 100층짜리 표현력이 1층과 같습니다. 활성화 함수가 층 사이에 반드시 끼어야 하는 까닭이 이것입니다.
아핀사상과 동차좌표
편향을 더한 것처럼 선형사상에 평행이동 하나를 붙인 함수를 아핀사상이라고 부릅니다. 그림으로 보면 차이가 분명합니다. 선형사상은 격자를 휘거나 늘여도 원점을 붙잡아 두는데, 아핀사상은 그렇게 만든 격자를 통째로 만큼 밀어 버립니다. 모양도 넓이도 그대로이고 자리만 옮겨 갑니다.
그런데 좌표를 한 칸 늘리면 아핀사상도 선형사상처럼 적을 수 있습니다. 점 를 끝에 1을 붙여 로 쓰는 표기를 동차좌표라고 하는데, 이렇게 쓰면 평행이동이 행렬 안의 한 열로 들어갑니다.
그림의 , 이면 3×3 행렬의 마지막 열이 이 되고, 을 넣으면 이 나옵니다. 끝의 1을 떼면 과 같습니다. 컴퓨터 그래픽스가 이동·회전·확대를 한 줄의 곱으로 묶는 방법이 이것입니다. nn.Linear 의 기본값은 아핀이고, 어텐션의 투영에서 bias=False 를 주는 구현이 많은 것은 그 자리를 순수한 선형사상으로 두려는 선택입니다.
기저의 상과 행렬
열에 적는 장부
여기가 이 글에서 가장 중요한 장면입니다. 위의 함수를 안다는 것은 원래 무한히 많은 입력에 대한 출력을 안다는 뜻입니다. 그런데 선형사상은 그렇지 않습니다.
를 표준기저로 적으면 이고, 선형결합을 통과시키는 성질을 그대로 쓰면
입니다. 함수가 보낸 결과 를 의 상이라고 부르면, 오른쪽에 남은 미지의 값은 기저 벡터의 상 뿐입니다. 기저 벡터 개가 어디로 가는지만 정해 주면 나머지 입력에 대한 답은 계산으로 따라옵니다. 무한히 많던 정보가 개의 벡터로 줄었습니다.
그 개를 어디에 적어 둘까요. 세로로 세워 나란히 놓으면 그것이 행렬입니다.
행렬의 정체. 행렬은 숫자표가 아니라 기저가 어디로 갔는지를 열에 적어 둔 장부다.
그리고 위 식은 그대로 의 계산법이 됩니다.
는 열들의 선형결합이고, 각 열에 곱해지는 수는 의 성분입니다. 행마다 내적을 하는 익숙한 계산법과 값은 같지만, 무슨 일이 일어나는지는 이 관점에서만 보입니다. 같은 곱을 몇 가지 시선으로 갈아 끼우는 연습은 다음 글이 맡습니다.
손으로 한 번 해 봅니다. 이면 , 입니다. 그러면 은
으로 갑니다. 행으로 내적해도 으로 같습니다. 행렬을 보고 "1열이 (2,0)이니 축 방향이 두 배로 늘어나는군" 하고 읽는 습관이 이 글의 목표입니다.
상과 랭크
입력 전부를 보냈을 때 나오는 출력들의 모임을 그 사상의 상이라고도 부릅니다. 벡터 하나의 상과 같은 말을 쓰는 것은 "보낸 결과"라는 뜻이 같아서입니다. 모든 출력이 열의 선형결합이므로, 사상의 상은 곧 열들의 span입니다.
그 span의 차원을 행렬의 랭크라고 합니다. 지난 글의 말로 하면 열 가운데 일차독립인 것이 몇 개인가이고, 기하로 하면 출력이 도착 공간을 몇 차원만큼 채우는가입니다. 위의 는 두 열 , 이 나란하지 않으므로 랭크가 2이고, 출력이 평면 전체를 덮습니다.
랭크가 모자란 예를 하나 봅니다. 는 2열 가 1열 의 두 배입니다. 무엇을 넣든 출력은 방향 직선 위에 떨어지므로 상은 그 직선 하나이고 랭크는 1입니다. 평면에서 들어와 평면으로 나가는데, 나가는 쪽은 한 차원만 씁니다.
랭크에는 위아래로 뻔한 한계가 있습니다. 열이 개이니 을 넘을 수 없고, 열 하나하나가 차원 벡터이니 도 넘을 수 없습니다. 그래서 행렬의 랭크는 이하입니다. 인 q_proj.weight 는 열이 768개나 있어도 랭크가 64를 넘지 못합니다.
사상의 핵
반대쪽에서 볼 것은 무엇이 사라지는가입니다. 사상이 영벡터로 보내는 입력 전부의 모임을 핵(kernel)이라고 합니다. GPU에서 말하는 커널과는 상관없는 말입니다. 핵은 0을 품고, 핵의 두 벡터를 더하거나 늘려도 여전히 0으로 가므로 원점을 지나는 부분공간입니다.
의 핵을 구해 봅니다. 이고 두 성분이 모두 의 배수이므로, 인 입력이 전부 0으로 갑니다. 그런 입력은 의 배수이니 핵은 방향 직선이고 1차원입니다.
핵이 0이 아닌 벡터를 하나라도 품으면 서로 다른 입력이 같은 출력으로 갑니다. 인데 도 입니다. 두 입력의 차 이 정확히 핵에 들어 있기 때문입니다. 일반적으로 에 핵의 벡터 를 더해도 이라 출력은 꿈쩍하지 않습니다. 출력만 보고는 만큼의 차이를 되살릴 방법이 없으니, 핵이 0 하나뿐일 때만 사상을 되돌릴 수 있습니다.
차원 정리
그림으로 보면 핵과 상의 관계가 한 장에 들어옵니다. 입력 평면은 핵과, 핵을 옮겨 놓은 평행선들로 빈틈없이 나뉩니다. 평행선 하나가 통째로 출력 한 점으로 접히고, 평행선마다 다른 점으로 가므로 평행선들의 가짓수가 곧 상의 크기입니다.
이것을 차원으로 세면 차원 정리가 됩니다.
에서는 1 + 1 = 2입니다. 입력이 가진 차원은 둘 중 한 곳으로 갑니다. 상에 살아남아 출력을 벌리거나, 핵으로 들어가 사라지거나입니다. 버린 만큼만 남는다고 읽으면 됩니다.
이제 q_proj 에 대입합니다. 입력이 768차원이고 랭크가 많아야 64이니 핵의 차원은 이상입니다. 무작위로 초기화한 가중치는 거의 언제나 랭크가 꽉 찬 64라서 핵은 정확히 704차원입니다. 768차원 토큰 벡터가 가진 방향 가운데 704차원어치는 질의를 만들 때 통째로 무시됩니다.
단위 정사각형 읽기
넓이 배율
과 가 만드는 단위 정사각형이 어디로 가는지를 그리면 그 행렬의 성격이 한눈에 보입니다. 선형사상은 격자를 격자로 보내므로, 정사각형 하나의 운명이 평면 전체의 운명입니다.
세 가지가 그림에서 바로 읽힙니다.
- 원점은 어느 경우에도 움직이지 않습니다. 첫 절의 이 그림으로 나타난 것입니다.
- 평행선은 평행선으로 남습니다. 정사각형이 평행사변형이 되지 곡선이 되지는 않습니다.
- 넓이가 몇 배가 되는지가 행렬식입니다. 초급 44번에서 로 정의한 그 값이고, 여기서는 읽기만 합니다. 첫 칸은 , 전단과 회전은 1입니다.
행렬식이 0이라는 것은 넓이가 0이 된다는 뜻이고, 정사각형이 선분으로 납작해졌다는 말입니다. 두 열이 나란해서 span이 직선으로 주저앉은 경우이고, 앞 절의 말로 하면 랭크가 모자라 핵이 0이 아닌 벡터를 품은 경우입니다. 앞의 가 그렇습니다. 이고, 핵이 있으니 되돌릴 수 없습니다.
행렬식의 부호
넓이는 음수가 될 수 없는데 행렬식은 음수가 됩니다. 그 부호가 방향을 말합니다. 에서 로 가는 회전이 반시계 방향인데, 보낸 뒤의 두 열에서도 1열에서 2열로 반시계 방향이면 행렬식이 양수이고, 시계 방향으로 뒤집혔으면 음수입니다.
90도 회전 은 입니다. 정사각형을 통째로 돌리기만 하니 방향이 그대로입니다. 반면 축에 대한 거울 대칭 은 입니다. 넓이는 그대로인데 종이를 뒤집은 것처럼 앞뒷면이 바뀌었기 때문입니다. 두 좌표를 맞바꾸는 도 대각선에 대한 거울이라 입니다. 회전을 아무리 여러 번 해도 거울 하나를 흉내 낼 수 없다는 것이 이 부호에 적혀 있습니다.
부피와 손잡이
3차원에서는 단위 정육면체가 평행육면체로 가고, 행렬식은 부피 배율이 됩니다. 처럼 축마다 따로 늘이는 행렬이면 부피가 배가 됩니다.
부호의 뜻도 한 차원 올라갑니다. 3차원 좌표계는 오른손 엄지·검지·중지로 축을 가리키는 오른손 좌표계와, 왼손으로 가리키는 왼손 좌표계로 갈리고, 어떻게 돌려도 서로 겹치지 않습니다. 축만 뒤집는 의 행렬식은 이고, 오른손 좌표계를 왼손 좌표계로 바꿉니다. 행렬식이 음수인 사상은 장갑을 뒤집어 끼우는 것과 같습니다.
0에 가까운 행렬식
행렬식이 정확히 0이면 되돌릴 수 없습니다. 0에 아주 가까우면 되돌리기는 되지만 조심해야 합니다. 의 행렬식은 0.001입니다. 두 열이 거의 나란해서 단위 정사각형이 거의 납작해진 상태입니다.
을 풀면 입니다. 그런데 오른쪽을 로 0.001만 바꾸면 답이 로 뛰어갑니다. 출력에서 0.001이던 차이가 입력에서는 천 배 넘게 불어난 것입니다. 거의 납작해진 평행사변형에서는 출력이 조금만 움직여도 그것을 만든 입력이 멀리 떨어져 있기 때문입니다. 부동소수점 반올림 오차가 딱 이런 작은 차이라, 이런 행렬의 역을 구하면 오차가 커진 결과가 나옵니다.
다만 행렬식 하나만으로 이 위험을 재지는 못합니다. 는 행렬식이 이지만 모든 방향을 똑같이 줄일 뿐이라 되돌리는 데 아무 문제가 없습니다. 방향마다 얼마나 고르지 않게 줄이는가를 재는 정확한 척도는 중급 17번 · 행렬 노름에서 다룹니다.
합성사상과 행렬곱
합성의 행렬
층을 두 개 쌓는 일은 함수를 두 번 먹이는 일입니다. 한 함수의 출력을 다음 함수의 입력으로 넣어 만든 새 함수를 합성사상이라고 합니다.
합성사상도 선형사상입니다. 선형결합을 통과시키는 함수를 두 번 통과시켜도 여전히 통과하기 때문입니다. 그렇다면 앞 절의 결론에 따라 합성에도 자기 행렬이 있어야 하고, 그 행렬의 열은 기저의 상입니다.
를 의 열마다 먹여 나온 것을 다시 열로 세우면 그것이 합성의 행렬이고, 우리는 그것을 라고 씁니다. 행렬곱의 그 복잡한 규칙은 임의로 정한 약속이 아니라 "두 번 보내는 것을 한 번에 하려면 어떻게 적어야 하는가"의 답입니다.
손으로 따라갑니다. 뒤에 90도 회전 을 붙입니다. 는 를 로 보내므로 의 열마다 적용하면
이 둘을 열로 세우면
검산합니다. 에 대해 두 번 보내면 이고 입니다. 한 번에 보내면 으로 같습니다. 행렬식도 으로, 의 6과 의 1을 곱한 값입니다. 넓이를 6배 한 뒤 1배 했으니 당연합니다.
순서와 결합법칙
순서가 왜 중요한지도 이 관점에서 당연합니다. 는 " 를 먼저"이고 는 " 를 먼저"입니다. 전단한 뒤 회전한 그림과 회전한 뒤 전단한 그림이 다르니 두 행렬이 같을 이유가 없습니다. 실제로
수식에서 를 보면 오른쪽이 먼저 적용된다는 것을 이 순서에서 기억하면 됩니다.
반면 괄호를 어디에 치는지는 결과를 바꾸지 않습니다. 이고, 이 결합법칙은 " 로 보내고, 로 보내고, 로 보낸다"는 세 번 보내기를 어떻게 묶어 부르든 같은 일이라는 말입니다. 성분을 펼쳐 증명하면 길지만 함수로 보면 따로 증명할 것이 없습니다.
그런데 결과가 같아도 계산 비용은 묶는 방식에 따라 크게 다릅니다. 행렬 뒤에 행렬 를 붙여 벡터 하나를 보낸다고 합시다. 로 묶으면 를 만드는 데만 곱셈이 번 들고, 로 묶으면 번이면 됩니다. 60배 가까운 차이입니다. 벡터를 수만 개 보낼 거라면 곱한 행렬을 한 번 만들어 두는 쪽이 이기기도 하니, 어느 쪽이 싼지는 그때그때 세어 봐야 합니다.
항등사상과 역사상
아무것도 바꾸지 않는 함수 를 항등사상이라고 합니다. 기저가 제자리에 있으니 열이 그대로이고, 그 행렬이 대각선만 1인 단위행렬 입니다. 어떤 사상 앞뒤에 붙여도 결과가 같으니 입니다.
어떤 사상이 한 일을 정확히 되돌리는 사상을 역사상이라고 합니다. 둘을 합성하면 항등사상이 되어야 하므로 행렬로는 이고, 역사상의 행렬이 역행렬입니다. 앞의 는 행렬식이 6이라 역행렬이 있고
입니다. 이 되어 가 을 보낸 자리에서 정확히 되돌아옵니다. 역사상이 있으려면 핵이 0 하나뿐이어야 하고 출력이 도착 공간을 다 채워야 하니, 정사각행렬이면서 행렬식이 0이 아닐 때입니다.
합성의 역은 순서가 뒤집힙니다. 입니다. 양말을 신고 신발을 신었으면 벗을 때는 신발부터 벗는 것과 같습니다. 방금 구한 로 계산해 봐도 두 쪽이 같습니다.
전치의 자리
행렬의 행과 열을 맞바꾼 전치 도 사상으로 읽고 싶어집니다. 그런데 전치는 기저의 상만으로는 뜻이 드러나지 않습니다. 의 열은 의 행인데, 의 행은 "기저가 어디로 갔는가"의 장부가 아니기 때문입니다.
전치의 뜻은 내적을 지나야 보입니다. 내적을 다룬 글의 내적으로 쓰면
가 모든 에서 성립합니다. 출력 쪽에서 재던 내적을 입력 쪽으로 옮겨 오는 사상이 라는 뜻입니다. 이 식이 이 글의 마지막 절에서 점수 표를 만듭니다.
코드로 확인하기
열과 합성
행렬의 열이 정말 기저의 상인지부터 봅니다. 표준기저를 넣으면 행렬의 열이 그대로 나오고, 둘을 더한 을 넣으면 두 열의 합이 나옵니다.
import numpy as np
A = np.array([[2., 1], [0, 3]])
e1, e2 = np.array([1., 0]), np.array([0., 1])
print(A @ e1, A @ e2) # [2. 0.] [1. 3.] = A의 1열, 2열
print(A @ np.array([1., 1])) # [3. 3.] = 두 열의 합
합성이 곱과 같은지도 한 줄입니다. 괄호를 어떻게 쳐도 같은 벡터가 나오고, 곱의 순서를 바꾼 두 행렬만 다릅니다.
B = np.array([[0., -1], [1, 0]])
x = np.array([1., 1])
print(B @ (A @ x)) # [-3. 3.]
print((B @ A) @ x) # [-3. 3.]
print(B @ A) # [[ 0. -3.] [ 2. 1.]]
print(A @ B) # [[ 1. -2.] [ 3. 0.]] 다르다
랭크와 핵
이번에는 q_proj.weight 와 같은 모양인 64×768 무작위 행렬로 앞 절의 계산을 확인합니다. 열이 768개 있어도 일차독립인 것은 64개뿐이라는 것, 그래서 핵이 704차원이라는 것을 matrix_rank 가 셉니다.
rng = np.random.default_rng(0)
W = rng.standard_normal((64, 768))
r = np.linalg.matrix_rank(W)
print(r, 768 - r) # 64 704
_, _, Vt = np.linalg.svd(W)
z = Vt[-1] # 핵에 든 벡터 하나
x = rng.standard_normal(768)
print(np.allclose(W @ x, W @ (x + 5 * z))) # True
Vt[-1] 은 특잇값 분해가 돌려주는 방향 가운데 하나로, 이 행렬이 0으로 보내는 방향입니다. 왜 그 줄이 핵에 드는지는 중급 15번 · 특잇값 분해에서 보고, 여기서는 결과만 씁니다. 그 방향을 다섯 배 해서 입력에 더해도 출력이 반올림 오차 안에서 똑같습니다. 서로 다른 두 768차원 벡터가 같은 64차원 벡터로 간 것이고, 평면에서 과 이 같은 점으로 간 일이 768차원에서 똑같이 일어난 것입니다.
다각형의 넓이
넓이 배율도 재어 봅니다. 꼭짓점 일곱 개짜리 무작위 다각형을 만들어 로 보내고, 넓이를 보내기 전후로 신발끈 공식으로 재어 나눕니다. 신발끈 공식은 꼭짓점 좌표를 엇갈려 곱해 다각형 넓이를 구하는 방법입니다.
def area(P): # 신발끈 공식
x, y = P[:, 0], P[:, 1]
return abs(x @ np.roll(y, -1) - y @ np.roll(x, -1)) / 2
t = np.sort(rng.uniform(0, 2 * np.pi, 7))
P = np.c_[np.cos(t), np.sin(t)] * rng.uniform(1, 2, (7, 1))
print(area(P @ A.T) / area(P), np.linalg.det(A)) # 6.0 6.0
다각형 모양이 매번 달라도 비율은 늘 6입니다. 행렬식이 단위 정사각형만의 성질이 아니라는 뜻입니다. 어떤 도형이든 아주 작은 정사각형들로 잘게 채울 수 있고, 그 정사각형 하나하나가 똑같이 6배가 되니 합도 6배가 됩니다. 점들을 행으로 쌓은 P 에 A.T 를 오른쪽에서 곱한 것은 아래 PyTorch 절에서 보는 저장 순서와 같은 이유입니다.
nn.Linear의 저장 순서
PyTorch의 nn.Linear 를 열 관점으로 읽을 때 걸리는 자리가 하나 있습니다.
import torch, torch.nn as nn
proj = nn.Linear(768, 64, bias=False)
print(proj.weight.shape) # torch.Size([64, 768])
x = torch.randn(768)
print(torch.allclose(proj(x), proj.weight @ x)) # True
가중치가 (출력, 입력) 모양으로 저장됩니다. 즉 proj.weight 가 곧 우리가 말한 이고, 그 열이 768개라서 입력 기저 768개의 상이 하나씩 들어 있습니다. 생성자에는 nn.Linear(768, 64) 처럼 입력을 먼저 적는데 저장은 출력이 먼저라 헷갈리기 쉽습니다. 배치를 넣을 때 x @ W.T 꼴이 되는 것도 이 저장 순서 때문이며, 축이 어디로 가는지는 다음 글에서 정리합니다.
W_Q가 하는 일
열 768개의 목적지
이제 첫머리의 세 줄을 읽을 수 있습니다.
nn.Linear(768, 64, bias=False) 는 인 선형사상 하나이고, 그 행렬의 열 768개에는 입력 기저가 각각 64차원 어디로 가는지가 적혀 있습니다. 학습이 하는 일은 이 768개의 목적지를 조정하는 것입니다.
세 가지가 이 관점에서 따라옵니다.
- 되돌릴 수 없습니다. 차원 정리에 따라 핵이 적어도 704차원이라 서로 다른 가 같은 로 가는 일이 반드시 생깁니다. 어텐션이 질의·키를 만들 때 정보를 일부러 버린다는 말이 이것이고, 무엇을 버릴지 고르는 것이 곧 학습입니다.
- "투영"이라는 이름은 느슨한 표현입니다. 직교와 사영의 사영 행렬은 를 만족하는 특별한 것이었지만 는 입력과 출력의 차원부터 달라 두 번 곱할 수도 없습니다. 차원을 줄인다는 뜻으로 쓰는 관용어일 뿐입니다.
- 와 가 다른 이유가 기하로 보입니다. 같은 토큰 벡터를 서로 다른 두 함수로 보내 놓고 그 둘을 내적하는 것이 이므로, 질의와 키는 같은 벡터를 두 가지 방식으로 읽은 결과입니다. 두 사상이 같다면 모든 토큰이 자기 자신과 가장 잘 맞게 되어 버립니다.
접힌 점수 표
토큰 가 토큰 에 주는 점수는 질의 와 키 의 내적입니다. 앞 절의 전치 식을 쓰면 두 사상이 하나로 접힙니다.
가운데의 는 행렬 하나입니다. 점수는 결국 토큰 두 개를 이 표 하나에 끼워 읽은 값이고, 질의·키라는 두 단계는 이 표를 만드는 방법이었던 셈입니다.
그런데 이 표는 768×768이라는 크기에 비해 속이 비어 있습니다. 의 모든 출력은 먼저 를 지나 64차원으로 좁아졌다가 로 다시 넓어진 것이라, 상이 64차원을 넘을 수 없습니다. 랭크가 많아야 64이고, 핵은 적어도 차원입니다. 무작위 64×768 행렬 두 장으로 을 만들어 matrix_rank 를 찍어 봐도 64가 나옵니다.
저랭크와 여러 헤드
크기에 비해 랭크가 작은 행렬을 저랭크 행렬이라고 부릅니다. 저랭크라는 것은 이 헤드 하나가 토큰 벡터의 704차원어치 차이를 점수에서 전혀 못 본다는 뜻입니다. 두 키가 핵 방향으로만 다르면 어떤 질의에게서든 같은 점수를 받습니다.
헤드를 여럿 두는 이유가 여기서 이어집니다. 768차원 모델에서는 흔히 64차원 헤드 12개를 두는데, 입니다. 헤드마다 자기 가 있어 보는 64차원 부분공간이 서로 다르므로, 한 헤드가 버린 방향을 다른 헤드가 볼 수 있습니다. 헤드마다 softmax를 따로 걸기 때문에 열두 표가 하나의 큰 표로 합쳐지지는 않고, 서로 다른 기준의 점수 표 열두 장이 나란히 섭니다. 표 한 장의 한계와 여러 장의 이점은 중급 16번 · 저랭크 근사와 LoRA에서 다시 만납니다.
파라미터 수의 대비
그렇다면 두 장 대신 접힌 표 하나를 직접 학습하면 어떨까요. 할 수는 있습니다. 점수는 만으로 계산되니 수학적으로 막는 것이 없고, 랭크 제한도 사라집니다.
대신 값이 큽니다. 은 개의 수이고, 두 장은 개입니다. 헤드 하나에 6배이고, 헤드 12개면 7,077,888개 대 1,179,648개입니다. 계산도 무거워집니다. 토큰마다 64차원 질의·키를 만들어 두면 점수 하나가 64번의 곱이지만, 을 쓰면 가 768차원이라 점수 하나가 768번의 곱입니다. 둘로 쪼개 적은 것은 저랭크라는 제약을 받아들이고 파라미터와 계산을 아낀 선택이고, 그 제약을 헤드 여럿으로 메우는 것이 멀티헤드 어텐션의 설계입니다.
정리
- 선형사상은 가법성과 동차성, 곧 선형결합을 통과시키는 함수이고, 그래서 원점을 원점으로 보내고 원점 대칭입니다. 편향·ReLU·softmax·LayerNorm은 선형사상이 아니며, 편향은 동차좌표로 한 차원을 늘리면 행렬 안에 들어갑니다.
- 기저의 상만 알면 함수 전체가 결정되고, 그 상을 열에 적어 둔 것이 행렬입니다. 는 열의 선형결합입니다.
- 상의 차원이 랭크이고, 0으로 가는 입력의 모임이 핵입니다. 둘을 더하면 입력의 차원이라, 768→64 사상은 적어도 704차원을 버립니다.
- 행렬식은 넓이·부피 배율이고 부호는 방향이 뒤집혔는지를 말합니다. 0이면 되돌릴 수 없고, 0에 가까우면 되돌려도 오차가 커질 수 있습니다.
- 행렬곱은 합성사상의 행렬입니다. 그래서 순서가 바뀌면 달라지고, 괄호는 바꿔도 결과가 같지만 비용은 다릅니다. 역행렬은 역사상, 전치는 내적을 반대쪽으로 옮기는 사상입니다.
- 는 열이 768개인 사상 하나이고, 질의와 키를 합친 점수 표는 랭크가 64 이하인 768×768 행렬입니다.
다음 글은 이 곱을 실제로 계산합니다 — 같은 행렬곱을 원소·행·열·외적의 네 시선으로 갈아 끼우고, 그 눈으로 텐서의 축이 어디서 축약되는지를 손으로 따라갑니다.
읽어주셔서 감사합니다. 😊

