문장 임베딩 모델을 하나 불러오면 벡터가 768차원으로 나옵니다. 그런데 임베딩 공간을 실제로 들여다본 보고들은 한결같이 같은 말을 합니다 — 그 벡터들이 공간 전체에 퍼져 있지 않고 좁은 원뿔 안에 몰려 있다는 것입니다. 서로 다른 문장 두 개를 아무렇게나 뽑아 코사인 유사도를 재면 0 근처가 아니라 0.6, 0.7 같은 값이 나옵니다.
여기서 "768개를 준다고 768개를 쓰는 것은 아니다"라는 말이 나오고, 이것을 표현 붕괴라고 부릅니다. 그런데 이 문장은 아직 정확하지 않습니다. "몇 차원을 쓴다"는 것이 무엇을 세는 말인지를 먼저 정해야 합니다.
이 글이 그 도구를 만듭니다. 지난 글에서 "부분공간"과 "기저"라는 말을 정의 없이 계속 썼는데, 그 빚도 여기서 갚습니다.
생성 — 그 벡터들로 만들 수 있는 것 전부
벡터 몇 개를 손에 쥐었을 때 "이것들로 무엇을 만들 수 있는가"를 묻습니다. 만들 수 있는 것은 선형결합, 즉 각자에 수를 곱해 더한 것 전부입니다.
정의. 벡터 의 생성(span)은 그것들의 선형결합 전체가 이루는 집합이다.
계수 는 아무 실수나 되므로 span은 원점을 지나는 직선, 평면, 그 이상이 됩니다. 이렇게 덧셈과 스칼라배에 닫힌 집합을 부분공간이라고 부릅니다 — 지난 글에서 "평면 위로 사영한다"고 할 때의 그 평면이 정확히 이것입니다.
그림의 세 번째 칸이 중요합니다. 벡터를 하나 더 준다고 span이 반드시 커지지는 않습니다. 이면 라서 결국 의 배수만 나옵니다. 이미 만들 수 있던 것을 다시 만들 뿐입니다.
span이 커지는 것은 새 방향을 보탤 때뿐이고, "새 방향인가"를 판정하는 말이 다음 절의 일차독립입니다.
일차독립 — 낭비가 없다는 조건
" 가 나머지로 만들어지는가"를 벡터마다 따로 확인하는 것은 번거롭습니다. 한 줄로 묶은 조건이 있습니다.
정의. 의 해가 하나뿐이면 이 벡터들이 일차독립이라고 한다. 그렇지 않으면 일차종속이다.
계수를 전부 0으로 두면 합이 인 것은 언제나 참이라 이 해는 늘 있습니다. 그래서 이것을 자명한 해라고 부르고, 질문은 "그 말고 다른 해가 있는가"입니다.
왜 이 조건이 '낭비가 없다'와 같은 말인지는 한 줄로 보입니다. 자명하지 않은 해가 있어서 예컨대 이라면 양변을 로 나눠
즉 이 나머지의 선형결합이 됩니다. 그 벡터는 span에 아무것도 보태지 않으므로 빼도 됩니다. 거꾸로 하나라도 나머지로 만들어지면 그 식을 한쪽으로 옮겨 자명하지 않은 해를 얻습니다. 두 진술은 정확히 같습니다.
손으로 한 번. 와 는 이므로 계수가 인 자명하지 않은 해가 있습니다 — 종속입니다. 와 은 어떨까요.
첫 식에서 를 둘째에 넣으면 이라 , 따라서 입니다. 자명한 해뿐이므로 독립입니다.
셋을 주면 어떻게 될까요. , , 에서 로 두고 풀면
확인하면 입니다. 종속입니다. 이것은 우연이 아닙니다 — 에서 벡터를 개 이상 모으면 반드시 종속입니다. 미지수 개에 식이 개인 제차연립방정식이고, 식보다 미지수가 많으면 자유변수가 남아 자명하지 않은 해가 생기기 때문입니다.
기저와 차원
이제 두 성질을 한 대상에 겹쳐 놓습니다.
정의. 부분공간 에 대해 일차독립이면서 를 생성하는 벡터들의 모음을 의 기저라고 한다.
생성한다는 것은 "빠짐없이 만든다"이고 독립이라는 것은 "군더더기가 없다"입니다. 둘을 함께 요구하면 좋은 성질이 하나 따라옵니다.
기저로 적는 방법은 하나뿐입니다. 를 두 가지로 적었다고 해 봅시다.
빼면 이고, 기저는 독립이므로 계수가 전부 0이어야 합니다. 즉 입니다. 표현이 유일하다는 것이 독립을 요구한 대가로 얻는 것이고, 다음 절의 좌표는 이 유일성 위에 서 있습니다.
한 부분공간의 기저는 여럿입니다. 는 로도 로도 생성됩니다. 그러나 어떤 기저를 잡아도 벡터의 개수는 같다는 것이 알려져 있고, 그 개수를 의 차원이라고 부릅니다. 차원이 잘 정의된 값이라는 사실이 "몇 차원을 쓰는가"라는 질문을 비로소 말이 되게 합니다.
을 표준기저라고 합니다. 배열에 적힌 숫자를 그대로 좌표로 읽을 때 우리가 말없이 쓰고 있던 기저가 이것입니다.
좌표 — 벡터가 아니라 기저에 붙는 값
여기가 이 글의 핵심입니다. [3, 1] 이라는 배열은 벡터 자체가 아니라 어떤 기저로 읽었을 때의 계수입니다.
정의. 기저 에 대해 일 때 을 의 B-좌표라고 한다.
기저 벡터를 열로 세운 행렬을 라고 쓰면 정의가 곧바로 행렬 한 줄이 됩니다.
의 열이 기저이므로 독립이고, 그래서 는 가역입니다. 는 좌표를 받아 벡터를 돌려주고, 은 벡터를 받아 좌표를 돌려줍니다. 이 두 행렬을 좌표변환 행렬이라고 부릅니다.
손으로 한 번. , 로 을 읽습니다.
역행렬은 초급 44번에서 세운 2×2 공식을 그대로 쓴 것입니다. 그러면
되돌려 확인하면 입니다. 같은 점인데 배열이 [3, 1] 에서 [2, 1] 로 바뀌었습니다 — 점이 움직인 것이 아니라 눈금이 바뀐 것입니다.
기저가 정규직교이면 역행렬이 필요 없다
지난 글의 결과가 여기서 바로 쓰입니다. 기저가 정규직교이면 라 이고, 좌표는 내적 한 번으로 나옵니다.
위의 는 서로 직교하지만 노름이 라 정규직교는 아닙니다. 로 나눠 , 로 만들면
이고 로 맞습니다. 역행렬 하나가 전치 한 번으로 줄어드는 것이 정규직교기저를 선호하는 이유이고, 실제 구현이 직교기저를 고집하는 자리들이 전부 이 계산 때문입니다.
축을 바꾸는 조작은 전부 같은 것
기저를 바꾼다는 말이 낯설어 보이지만, AI 코드에서 이미 여러 이름으로 하고 있는 일입니다.
| 이름 | 새 기저로 무엇을 잡는가 | 무엇이 편해지는가 |
|---|---|---|
| 주성분 축 | 데이터의 분산이 큰 방향부터 | 앞쪽 몇 축만 남겨도 정보가 대부분 남는다 |
| 화이트닝 | 축마다 분산이 1이 되도록 잡은 축 | 모든 방향이 같은 규모가 된다 |
| 회전 위치 인코딩 | 2차원씩 각도만큼 돌린 축 | 상대 위치가 각도 차이로 읽힌다 |
| 대각화 | 행렬이 늘이기만 하는 방향 | 행렬 곱이 수 몇 개의 곱이 된다 |
네 줄이 전부 한 줄이고, 다른 것은 를 무엇으로 고르느냐뿐입니다. 표의 첫 두 줄이 필요로 하는 "분산이 큰 방향"과 넷째 줄의 "늘이기만 하는 방향"은 아직 정의하지 않았습니다 — 「중급 12번 · 고윳값과 고유벡터」가 그 방향을 정의하고, 「중급 14번 · 대칭 행렬」이 그것이 왜 늘 직교하는지를 답합니다. 셋째 줄은 「중급 11번 · 회전·전단·스케일」이 세우는 회전행렬을 씁니다.
행렬을 새 기저에서 다시 쓰는 규칙도 같은 자리에서 나옵니다. 원래 좌표에서 인 조작을 B-좌표로 옮기면 "좌표를 벡터로 되돌리고 → 를 먹이고 → 다시 좌표로" 이므로
라는 낯익은 꼴이 이 세 단계를 그대로 적은 것입니다. 논문에서 이 표현을 만나면 "같은 조작을 다른 축에서 본 것"이라고 읽으면 됩니다.
코드로 확인하기
import numpy as np
B = np.array([[1., 1], [1, -1]]) # 열이 기저 벡터
x = np.array([3., 1])
coords = np.linalg.solve(B, x) # B c = x 를 푼다
print(coords) # [2. 1.]
print(B @ coords) # [3. 1.] 되돌아온다
정규직교로 바꾸면 풀 것이 없어집니다.
Q = B / np.sqrt(2) # 열의 노름을 1로
print(Q.T @ Q) # [[1 0] [0 1]]
print(Q.T @ x) # [2.828 1.414] = (2√2, √2)
독립인지 세는 것은 matrix_rank 한 줄입니다.
V = np.array([[1., 2, 4],
[2., 1, 5]]) # 열이 벡터 셋
print(np.linalg.matrix_rank(V)) # 2 — 셋이 만드는 것은 평면
print(np.linalg.matrix_rank(V[:, :2]))# 2 — 앞의 둘만으로 이미 평면
셋째 벡터가 span을 넓히지 않았다는 것이 숫자로 보입니다. 다만 이 한 줄을 그대로 믿기 전에 짚을 것이 있습니다.
다시 그 768차원으로
이제 첫머리의 질문을 정확히 쓸 수 있습니다. 문장 개를 임베딩해 얻은 벡터 에 대해
모델이 실제로 쓰는 차원 수
이고, 이 값이 768보다 훨씬 작으면 그것이 표현 붕괴입니다.
그런데 이 정의를 컴퓨터로 그대로 확인할 수는 없습니다. 일차독립은 " 가 정확히 0인가"를 묻는데, 부동소수점에서 정확히 0인 값은 거의 나오지 않습니다. 실제 임베딩 행렬을 matrix_rank 에 넣으면 거의 언제나 768이 나옵니다 — 어느 방향으로도 만큼은 퍼져 있기 때문입니다. 그러나 그 방향이 "쓰이고 있다"고 말하기는 어렵습니다.
그래서 실제로 필요한 것은 예/아니오가 아니라 방향마다 얼마나 쓰이는지를 재는 자이고, 그 자를 만드는 것이 「중급 15번 · 특잇값 분해와 랭크」입니다. 이 글은 무엇을 세야 하는지까지를 맡고, 어떻게 세는지는 그 글이 이어받습니다.
붕괴가 왜 문제인지도 이제 한 줄로 말할 수 있습니다. 벡터들이 낮은 차원 부분공간에 몰려 있으면 서로 다른 문장이 가리키는 방향의 폭이 좁아지고, 그러면 지난 글들에서 본 코사인 유사도가 전부 비슷한 값으로 밀려 순위가 뭉갭니다. 검색이 "가까운 것"을 못 고르는 것은 거리 지표의 문제가 아니라 span의 문제입니다.
정리
- span은 선형결합 전체이고, 덧셈과 스칼라배에 닫혀 있어 부분공간이 됩니다. 벡터를 더 준다고 span이 반드시 커지지는 않습니다.
- 일차독립은 의 해가 자명한 것뿐이라는 조건이고, "하나도 나머지로 만들어지지 않는다"와 같은 말입니다. 에서 개는 항상 종속입니다.
- 기저는 독립이면서 생성하는 모음이고, 그래서 표현이 유일합니다. 기저의 개수는 어떤 기저를 잡아도 같고 그것이 차원입니다.
- 좌표는 기저에 붙는 값입니다. 이므로 이고, 기저가 정규직교이면 로 줄어듭니다.
- 주성분·화이트닝·회전 위치 인코딩은 전부 를 다르게 고른 같은 조작이며, 같은 사상을 새 축에서 보면 입니다.
- "몇 차원을 쓰는가"는 span의 차원을 묻는 질문이지만, 부동소수점에서는 예/아니오로 판정되지 않아 방향마다 크기를 재는 자가 따로 필요합니다.
다음 글은 여기서 "조작"이라고 뭉뚱그린 를 제대로 봅니다 — 행렬은 숫자표가 아니라 함수이고, 그 열에는 기저가 어디로 갔는지가 적혀 있습니다.
읽어주셔서 감사합니다. 😊

