Chapter 04

토큰과 임베딩

신경망은 숫자만 먹는다. 그런데 우리가 모델에게 주는 것은 "고양이가 귀엽다" 같은 글자다. 이 장은 글자를 숫자로 바꾸는 두 단계, 즉 텍스트를 조각내는 토큰화와 조각마다 벡터를 붙이는 임베딩을 다룬다. 그리고 이 책에서 가장 손이 많이 가는 실험실을 연다. 임베딩 공간을 3D로 돌려 보고, 벡터를 더하고 빼서 유추를 풀고, 단어를 손가락으로 끌어 옮겨 그 단어의 "의미"를 직접 바꿔 본다. 마지막에는 브라우저 안에서 word2vec을 처음부터 학습시켜, 아무 의미도 없던 점들이 스스로 뭉치는 과정을 지켜본다.

텍스트를 숫자로: 무엇을 한 단위로 볼까

2장의 신경망은 입력으로 실수 벡터를 받는다. 이미지라면 픽셀 밝기가 이미 숫자이니 문제가 없다. 하지만 텍스트는 기호의 나열이다. "고양이"라는 글자 세 개에는 크기도, 방향도, 거리도 없다. 그래서 언어 모델의 맨 앞에는 항상 두 단계가 붙는다.

  1. 토큰화(tokenization) — 문자열을 미리 정해 둔 어휘(vocabulary)의 조각, 즉 토큰들로 자르고 각 토큰을 정수 ID로 바꾼다.
  2. 임베딩(embedding) — 정수 ID마다 학습 가능한 \(d\)차원 실수 벡터를 하나씩 붙인다. 이 벡터들이 신경망이 실제로 보는 입력이다.
① 텍스트 ② 토큰 ③ 정수 ID ④ 임베딩 벡터 (d차원) ⑤ 모델 고양이가 귀엽다 자르기 ▁고양이 가 ▁귀엽 다 9012 118 20577 342 … … … … 트랜스포머 5~6장 어휘(V개) 중 하나씩 0 … V−1 테이블 E의 해당 행
그림 4-1. 언어 모델 입력 파이프라인. 텍스트는 토큰으로 잘리고(②), 각 토큰은 어휘 안의 번호(③)가 된 뒤, 임베딩 테이블에서 그 번호의 행(④)을 꺼내 모델에 들어간다. 토큰 분할과 ID 값은 설명을 위한 예시다. ▁는 "단어의 시작(앞에 공백이 있었음)"을 표시하는 관례 기호다.

첫 번째 질문은 "무엇을 한 단위로 볼 것인가"다. 가장 단순한 선택지는 세 가지다.

단위"고양이가 귀엽다"어휘 크기장점단점
문자고양이가 귀엽다작다 (영어 ~100, 한글 완성형 11,172)모르는 단어가 없다시퀀스가 길다. 한 토큰이 담는 의미가 적어 모델이 조합을 처음부터 배워야 한다
단어고양이가귀엽다매우 크다 (수십만~수백만)토큰 하나가 의미 단위"고양이가/고양이를/고양이는"이 모두 별개. 처음 보는 단어는 [UNK]. 한국어처럼 활용이 많은 언어에서 폭발
서브워드▁고양이가▁귀엽다적당 (3만~26만)자주 쓰는 단어는 통째로, 드문 단어는 조각으로분할이 언어학적 형태소와 꼭 일치하지는 않는다

어휘 크기 \(V\)는 두 비용 사이의 줄다리기다. \(V\)를 키우면 같은 문장이 더 적은 토큰으로 표현되어 시퀀스가 짧아진다. 5장에서 볼 어텐션의 비용은 시퀀스 길이 \(n\)의 제곱에 비례하므로, 그리고 8장의 생성은 토큰 하나씩 진행되므로 짧은 시퀀스는 곧 빠르고 싼 추론이다. 반대로 \(V\)가 커지면 임베딩 테이블과 출력층이 \(V\)에 비례해 커지고, 드문 토큰은 학습 데이터에서 몇 번 나오지 않아 벡터가 제대로 학습되지 않는다. 현대 LLM은 대부분 서브워드를 택하고, \(V\)는 3만(Llama 2)에서 12.8만(Llama 3), 25만 이상(다국어 모델)까지 다양하다.

압축률이라는 관점

토크나이저는 일종의 압축기다. 같은 텍스트를 몇 개의 토큰으로 표현하느냐(토큰당 평균 글자 수, 또는 바이트 수)가 그 토크나이저의 "효율"이다. 영어 텍스트에서 GPT 계열 토크나이저는 대략 토큰 하나에 영어 4글자, 단어 0.75개 정도를 담는다. 이 비율은 언어마다 크게 다르고, 이것이 3절에서 볼 "한국어 세금"의 정체다.

토큰화와 BPE: 가장 흔한 쌍부터 붙인다

서브워드 어휘를 어떻게 정할까? 가장 널리 쓰이는 방법은 BPE(Byte Pair Encoding)다. 원래 1994년의 데이터 압축 기법이었고, 2016년 Sennrich 등이 신경망 기계 번역에 도입한 뒤 GPT-2, GPT-4, Llama 3 같은 모델의 기본 토크나이저가 되었다. 아이디어는 놀랄 만큼 단순하다.

  1. 코퍼스의 모든 단어를 문자 단위로 쪼갠다. 단어 시작에는 경계 표시 ▁를 붙인다. 처음 어휘 = 등장한 모든 문자.
  2. 코퍼스 전체에서 인접한 두 토큰 쌍의 빈도를 센다(단어 빈도로 가중).
  3. 가장 빈번한 쌍 (a, b)를 새 토큰 ab로 병합하고, 코퍼스의 모든 a b를 ab로 바꾼다. 이 병합 규칙을 기록한다.
  4. 원하는 어휘 크기(= 기본 문자 수 + 병합 횟수)가 될 때까지 2~3을 반복한다.

새 텍스트를 토큰화할 때는 같은 문자 분해에서 출발해 학습할 때 기록한 순서대로 병합 규칙을 적용한다. 그래서 학습 코퍼스에 자주 나온 문자열일수록 큰 토큰 하나로 묶이고, 드문 문자열은 작은 조각 여러 개로 남는다. 아래 시뮬레이터는 이 알고리즘을 그대로 구현했다. 코퍼스를 고쳐 쓰면 처음부터 다시 학습한다.

SIMULATOR

BPE 단계별 학습

학습 코퍼스 (띄어쓰기로 단어 구분, 직접 고쳐 써도 된다)
이번 단계의 후보 쌍 — 맨 앞(분홍 테두리)이 최빈 쌍. 다른 쌍을 눌러 직접 병합할 수도 있다
코퍼스의 현재 분할 (×빈도)
어휘 (회색 = 기본 문자, 보라 = 병합으로 생긴 토큰, 분홍 = 방금 생긴 토큰)
병합 기록
    어휘 크기—
    병합 횟수—
    코퍼스 토큰 수—
    테스트 토큰 수—
    해볼 것: ① "병합 1단계"를 몇 번 눌러 ▁학 → ▁학교, ▁공 → ▁공부처럼 자주 나오는 음절 묶음이 먼저 토큰이 되는 것을 보자. 매 단계 코퍼스 토큰 수가 그 쌍의 빈도만큼 줄어든다. ② "자동"으로 끝까지 돌린 뒤 슬라이더를 0에서 오른쪽으로 끌면, 테스트 문장이 문자 단위에서 점점 큰 조각으로 합쳐진다. ③ 테스트 문장에 코퍼스에 없는 글자(예: "고양이")를 넣으면 노란 [UNK]가 생긴다. 이것이 문자 단위 BPE의 약점이고, 아래 콜아웃의 바이트 수준 BPE가 이를 해결한다. ④ 후보 쌍 중 최빈이 아닌 것을 눌러 병합하면 같은 어휘 크기에서 토큰 수가 더 많아진다 — 탐욕적 최빈 병합이 압축에 유리한 이유다.
    바이트 수준 BPE와 한국어

    위 시뮬레이터는 이해를 돕기 위해 음절(유니코드 문자) 단위에서 출발했다. 그러면 처음 보는 글자가 나올 때 [UNK]를 피할 수 없다. 한글 완성형 음절만 11,172개, 유니코드 전체는 15만 자가 넘는다. GPT-2 이후 대부분의 LLM은 텍스트를 UTF-8 바이트로 바꾼 뒤 BPE를 돌린다. 기본 어휘가 256개 바이트이므로 어떤 문자열이든 표현할 수 있고 [UNK]가 원천적으로 사라진다. GPT-2의 어휘 50,257 = 바이트 256 + 병합 50,000 + 특수 토큰 1이다. 대가도 있다. 한글 음절 하나는 UTF-8로 3바이트(예: "고" = EA B3 A0)이므로, 그 음절 조합이 병합 규칙으로 학습되지 않았다면 한 글자가 토큰 2~3개로 쪼개진다. SentencePiece(Llama 2 등)는 문자 단위 BPE를 쓰되 모르는 문자만 바이트로 떨어뜨리는 byte fallback으로 같은 문제를 푼다.

    BPE만 있는 것은 아니다. BERT는 빈도 대신 "병합했을 때 코퍼스 우도가 얼마나 오르나"로 쌍을 고르는 WordPiece(어휘 30,522)를, T5와 많은 다국어 모델은 큰 후보 어휘에서 출발해 덜 중요한 토큰을 깎아 내는 Unigram LM(SentencePiece)을 쓴다. 원리는 다르지만 "자주 나오는 문자열을 한 토큰으로"라는 결과는 비슷하다.

    실제 토크나이저의 디테일

    교과서용 BPE와 실제 LLM 토크나이저 사이에는 몇 가지 중요한 공학적 결정이 더 있다. 이 결정들이 모델의 이상한 행동(철자 세기 실패, 산수 실수 등)의 원인이 되기도 한다.

    공백, 대소문자, 숫자

    한국어가 토큰을 더 많이 쓰는 이유

    영어 "cat" 문자 3개 → UTF-8 3바이트 63 61 74 cat 토큰 1개 (" cat"은 흔해서 병합됨) 한국어 "고양이" 음절 3개 → UTF-8 9바이트 EA B3 A0 EC 96 91 EC 9D B4 고양이 학습 데이터에 한국어가 적으면: 음절마다 1~3토큰 → 합계 3~9토큰
    그림 4-2. 바이트 수준 BPE에서 영어와 한국어의 출발점 차이. 영어 알파벳은 1바이트, 한글 음절은 3바이트다. 병합 규칙은 학습 코퍼스에서 자주 나온 바이트 열로만 만들어지므로, 토크나이저 학습 데이터에서 한국어 비중이 작으면 한국어 텍스트는 잘게 쪼개진다(토큰 수는 토크나이저마다 다르다).

    원인은 두 가지가 겹친다. 첫째, 출발점이 다르다. 그림 4-2처럼 같은 "세 글자"라도 한국어는 바이트가 세 배다. 둘째, 병합 예산의 배분이 다르다. BPE는 코퍼스에서 빈도가 높은 쌍에 병합을 쓰는데, 토크나이저 학습 코퍼스의 대부분이 영어와 코드라면 한국어 바이트 열에 돌아갈 병합이 적다. 결과적으로 같은 내용을 표현하는 데 한국어가 영어보다 토큰을 대략 1.5~3배 쓰는 일이 흔하다(토크나이저와 문체에 따라 크게 다르다). 토큰 수는 곧 API 비용, 컨텍스트 창 소모, 생성 시간이다. 어휘를 128K로 늘리고 다국어 데이터를 더 넣은 Llama 3 토크나이저가 Llama 2(32K)보다 한국어를 더 짧게 표현하는 것도, 국내 기업들이 한국어 비중을 높인 자체 토크나이저를 만드는 것도 이 때문이다.

    어휘 크기와 특수 토큰

    모델방식어휘 크기비고
    BERT-baseWordPiece30,522[CLS] [SEP] [MASK] [PAD] [UNK]
    GPT-2바이트 수준 BPE50,257256 바이트 + 50,000 병합 + <|endoftext|>
    Llama 2SentencePiece BPE + byte fallback32,000숫자 한 자리씩
    GPT-4 (cl100k)바이트 수준 BPE~100,000숫자 최대 3자리 묶음
    Llama 3바이트 수준 BPE (tiktoken 계열)128,256일반 128,000 + 특수 256

    특수 토큰(special tokens)은 텍스트에서 나오지 않고 사람이 정해 넣는 토큰이다. 문서의 시작과 끝(<|begin_of_text|>, <|endoftext|>), 대화의 역할 구분(<|start_header_id|>user<|end_header_id|>, <|eot_id|>), 패딩, 마스크 등이 여기에 속한다. 7장의 채팅 템플릿과 8장의 "생성 멈춤" 조건이 모두 이 특수 토큰으로 구현된다. 사용자 입력에 특수 토큰 문자열이 그대로 들어오면 역할 구분이 깨질 수 있어서, 실제 서비스는 일반 텍스트 안의 특수 토큰 문자열을 따로 이스케이프한다.

    토큰이 만드는 착시

    "strawberry에 r이 몇 개인가?" 같은 질문에 LLM이 자주 틀리는 이유 중 하나는 모델이 글자를 보지 못하기 때문이다. 모델 입력은 "str", "aw", "berry" 같은 토큰 ID일 뿐이고, 각 토큰 안에 어떤 글자가 들어 있는지는 학습으로 간접적으로만 알 수 있다. 숫자 계산, 운율, 철자 뒤집기도 같은 이유로 어렵다.

    원-핫의 한계와 임베딩 룩업 테이블

    토큰 ID는 정수지만, 정수 그대로 신경망에 넣으면 곤란하다. ID 118("가")과 ID 119가 수치상 가깝다고 의미가 가까운 것은 아니기 때문이다. 순서 없는 범주를 숫자로 바꾸는 고전적 방법은 원-핫 벡터(one-hot)다. 어휘 크기 \(V\)짜리 벡터에서 자기 자리만 1, 나머지는 0.

    $$\mathbf{x}_{\text{고양이}} = (0, 0, 0, 1, 0, \dots, 0) \in \{0,1\}^{V}, \qquad \mathbf{x}_i \cdot \mathbf{x}_j = \begin{cases}1 & i=j\\ 0 & i\ne j\end{cases}$$
    서로 다른 두 단어의 내적은 언제나 0, 거리는 언제나 \(\sqrt{2}\)다.

    원-핫에는 두 가지 치명적 한계가 있다. 첫째, 모든 단어가 서로 똑같이 멀다. "고양이"와 "강아지"의 거리가 "고양이"와 "민주주의"의 거리와 같다. 둘째, 너무 크고 희소하다. \(V = 128{,}256\)이면 벡터 하나가 12만 차원인데 그중 1이 하나뿐이다. 해결책은 각 토큰에 작은 \(d\)차원 밀집 벡터를 하나씩 배정하고, 그 값을 학습으로 정하는 것이다. 이 벡터들을 쌓은 행렬이 임베딩 테이블 \(E\)다.

    $$E \in \mathbb{R}^{V \times d}, \qquad \mathbf{h}_i = \mathbf{x}_i^{\top} E = E_{i,:} \quad (\text{i번째 행})$$
    원-핫 행벡터에 \(E\)를 곱하면 \(i\)번째 행 하나만 살아남는다. 그래서 실제 구현은 곱셈 없이 E[i]로 행을 꺼내는 룩업(lookup)이다. PyTorch의 nn.Embedding이 바로 이것이다.
    원-핫 x (1×V) 000 1 0000 i = 3 × 임베딩 테이블 E (V×d) 012 34567 .21−.84.33.07 = E의 3번째 행 (1×d) .21−.84.33.07 곱셈 V·d번 중 0이 아닌 항은 d개뿐 → 실제로는 E[3]으로 바로 꺼낸다
    그림 4-3. 원-핫 벡터 × 임베딩 테이블 = 행 선택. 수학적으로는 행렬곱이지만 계산적으로는 배열 인덱싱이다. 행렬곱으로 보는 관점은 "임베딩도 다른 층과 똑같이 역전파로 학습되는 가중치"임을 알려 준다. 선택된 행에만 기울기가 흐른다.
    SIMULATOR

    임베딩 룩업: 원-핫 × E = 행 선택

    입력 토큰 (클릭해서 하나 고르기)
    원-핫 x (1×8)
    임베딩 테이블 E (8×4) — 칸을 위아래로 끌어 값을 바꿀 수 있다
    x · E (1×4) — 행렬곱을 실제로 계산한 결과
    실제 모델 규모
    저장 형식
    선택 토큰 → ID—
    입력 임베딩 V×d—
    입력+출력층 합계—
    메모리—
    모델 전체 대비—
    해볼 것: ① 토큰을 바꿔 누르면 원-핫의 1이 움직이고, x·E의 결과가 늘 E의 그 행과 정확히 같다(아래 식에서 0이 곱해진 항이 모두 사라지는 것을 확인). ② 선택된 행의 칸을 위아래로 끌면 출력이 바로 바뀌지만, 다른 행을 바꾸면 출력은 그대로다 — 학습 때 기울기도 선택된 행에만 흐른다. ③ GPT-2 small(50,257 × 768 ≈ 38.6M, 전체 124M의 약 31%)과 Llama 3 8B(128,256 × 4,096 ≈ 525M)를 비교해 보자. Llama 3는 가중치 공유를 하지 않아 출력층까지 합치면 약 1.05B, 전체 8.03B의 13%다. 작은 모델일수록 임베딩 비중이 커서 가중치 공유의 이득이 크다.

    시뮬레이터 오른쪽의 가중치 공유(weight tying)는 모델 맨 끝의 출력층과 관련이 있다. 7장에서 보겠지만 LLM의 마지막 층은 \(d\)차원 은닉 벡터를 \(V\)개 토큰의 점수(로짓)로 바꾸는 \(d\times V\) 행렬이다. 이것은 임베딩 테이블의 전치와 모양이 같다. 그래서 GPT-2처럼 두 행렬을 같은 파라미터로 쓰면 \(V\times d\)만큼 절약된다. "입력에서 토큰을 벡터로 바꾸는 사전"과 "출력에서 벡터를 토큰 점수로 바꾸는 사전"을 하나로 쓰는 셈이다. 모델이 커지면 임베딩 비중이 작아지므로 Llama 3 8B처럼 공유하지 않는 경우도 많다(작은 Llama 3.2 1B·3B는 공유한다).

    임베딩 차원 d는 얼마나 클까

    LLM에서는 임베딩 차원이 곧 모델 전체의 은닉 차원 \(d_{\text{model}}\)이다. GPT-2 small 768, GPT-2 XL 1,600, Llama 3 8B 4,096, Llama 3 70B 8,192. 어휘가 12만 개인데 4,096차원이면 충분할까? 고차원에서는 서로 "거의 직교"인 방향을 차원 수보다 훨씬 많이 만들 수 있다(1장의 고차원 직관). 그래서 수만 개 토큰이 수천 차원 안에 겹치지 않고 들어간다.

    분포 가설: 단어는 이웃으로 알 수 있다

    임베딩 테이블 \(E\)의 값은 처음엔 무작위다. 그렇다면 무엇을 기준으로 "고양이"와 "강아지"의 벡터를 가깝게 만들어야 할까? 사람이 의미를 일일이 적어 줄 수는 없다. 1950년대 언어학자들이 답을 남겨 두었다.

    분포 가설 (Distributional Hypothesis)

    "You shall know a word by the company it keeps." — J. R. Firth (1957). 비슷한 문맥에 나타나는 단어는 비슷한 의미를 가진다(Z. Harris, 1954). "차가운 ___ 를 마신다"의 빈칸에 들어갈 수 있는 단어(우유, 주스, 물)는 서로 비슷하고, "___ 를 키운다"에 들어갈 단어(고양이, 강아지)는 또 서로 비슷하다.

    이 가설을 숫자로 옮기는 가장 직접적인 방법은 동시출현 행렬(co-occurrence matrix)이다. 코퍼스를 훑으며 단어 \(w\)의 앞뒤 \(k\)칸(윈도우) 안에 단어 \(c\)가 몇 번 나왔는지 \(\#(w,c)\)를 센다. 그러면 각 단어의 행이 곧 그 단어의 "문맥 프로필" 벡터가 된다. 그런데 원시 횟수에는 문제가 있다. "를", "에" 같은 조사는 어떤 단어 옆에나 나오므로 모든 행에서 큰 값을 차지해, 의미와 상관없이 모든 단어를 비슷하게 만든다. 그래서 "우연히 기대되는 것보다 얼마나 자주 함께 나오나"를 재는 PMI(Pointwise Mutual Information)를 쓴다.

    $$\mathrm{PMI}(w,c) = \log_2 \frac{P(w,c)}{P(w)\,P(c)}, \qquad \mathrm{PPMI}(w,c) = \max\bigl(0,\ \mathrm{PMI}(w,c)\bigr)$$
    \(P(w,c) = \#(w,c)/N\), \(P(w)\), \(P(c)\)는 주변 확률. 독립이면 PMI = 0이다. 음수 PMI는 작은 코퍼스에서 잡음이 크므로 0으로 자른 것이 PPMI다.

    PPMI 행렬의 행은 \(V\)차원으로 여전히 크고 희소하다. 이것을 특이값 분해(SVD)로 상위 몇 개 방향만 남기면 짧고 밀집된 단어 벡터가 된다. 1990년대의 LSA(잠재 의미 분석)가 이 방법이고, 놀랍게도 9절의 word2vec이 학습하는 것도 본질적으로는 (이동된) PMI 행렬의 분해다. 아래 시뮬레이터는 교육용으로 만든 24문장짜리 코퍼스로 이 과정을 실제로 계산한다. 각 단어의 PPMI 행을 길이 1로 정규화하고, 중심화한 뒤 SVD로 상위 2개 방향(= PCA 평면)에 투영했다.

    SIMULATOR

    동시출현 행렬 → PPMI → 2D 단어 벡터

    히트맵에 표시할 값
    2D 좌표에 쓸 행렬
    히트맵 칸이나 산점도의 단어를 눌러 보자.
    어휘 / 목표 단어—
    같은 그룹 평균 cos—
    다른 그룹 평균 cos—
    2D 설명 분산—
    위 히트맵의 행은 목표 단어 12개(동물·음료·탈것 각 4개), 열은 나머지 문맥 단어다. 해볼 것: ① 히트맵 값을 "횟수"로 바꾸면 "를", "에" 열이 모든 행에서 밝다. PPMI로 바꾸면 그 열이 어두워지고 "키운다", "마신다", "탄다" 같은 의미 있는 문맥이 살아난다. ② 2D 좌표를 "횟수"로 만들면 그룹이 덜 깔끔하게 갈리고 그룹 내·외 cos 차이가 줄어든다. ③ 윈도우를 1로 줄이면 바로 옆 단어만 보게 되어 그룹 구분이 흐려지고, 2~3이면 세 무리가 뚜렷하다. ④ 코퍼스에 "귀여운 버스 를 키운다"를 몇 줄 추가하면 버스가 동물 쪽으로 끌려간다 — 의미는 오로지 문맥에서 온다.

    여기까지가 세는 방법(count-based)이다. 행렬을 만들고 분해한다. 2013년의 word2vec은 같은 목표를 예측하는 방법(prediction-based)으로 바꿨다. 행렬을 통째로 만들지 않고, 문장을 하나씩 읽으며 "이 단어 주변에 저 단어가 나올까?"를 맞히도록 벡터를 조금씩 고친다. 수십억 단어 코퍼스에서도 메모리 걱정 없이 학습할 수 있다는 것이 결정적 장점이었다. 이 방법은 9절에서 직접 돌려 보고, 먼저 "잘 학습된 임베딩 공간"이 어떻게 생겼는지 탐험해 보자.

    임베딩 공간 탐험: 의미의 지도를 3D로 돌려 보기

    실제 word2vec이나 LLM의 임베딩은 수백~수천 차원이고, 각 축은 사람이 해석할 수 있는 의미를 갖지 않는다. 그래서 여기서는 원리를 눈으로 보기 위해 교육용으로 손수 설계한 작은 임베딩을 쓴다. 단어 76개(사람, 동물, 음식, 나라·도시, 동작, 색, 수)에 대해 12개의 해석 가능한 축 — 왕족, 성별, 나이, 생물, 크기, 음식, 장소, 동서(지역), 동작, 과거, 색, 수 — 의 값을 직접 정하고, 표준편차 0.05의 작은 잡음을 더했다. 예를 들어 "왕"은 왕족 1, 성별 +1, 나이 0.6, 생물 1이고 나머지는 거의 0이다. 실제 모델의 공간도 이런 "의미 방향"을 어느 정도 품고 있지만, 그 방향들이 좌표축과 나란하지 않고 서로 비스듬히 섞여 있다는 점이 다르다.

    두 단어가 얼마나 비슷한지는 보통 코사인 유사도로 잰다. 벡터의 길이는 빈도 같은 부수적 요인의 영향을 많이 받으므로 방향만 비교한다.

    $$\cos(\mathbf{u},\mathbf{v}) = \frac{\mathbf{u}\cdot\mathbf{v}}{\lVert\mathbf{u}\rVert\,\lVert\mathbf{v}\rVert} \in [-1, 1]$$

    12차원은 볼 수 없으므로 3차원으로 줄여야 한다. 가장 정보를 많이 남기는 선형 투영은 주성분 분석(PCA)이다. 데이터의 공분산 행렬을 고유분해해서 분산이 가장 큰 방향 세 개를 고른다.

    $$C = \frac{1}{n-1}\sum_{i=1}^{n} (\mathbf{x}_i-\boldsymbol{\mu})(\mathbf{x}_i-\boldsymbol{\mu})^{\top}, \qquad C\,\mathbf{u}_k = \lambda_k \mathbf{u}_k, \qquad \mathbf{z}_i = \bigl[(\mathbf{x}_i-\boldsymbol{\mu})\cdot\mathbf{u}_1,\ \dots,\ (\mathbf{x}_i-\boldsymbol{\mu})\cdot\mathbf{u}_3\bigr]$$
    \(\lambda_1 \ge \lambda_2 \ge \lambda_3\)는 각 방향이 설명하는 분산. 3D가 보존하는 비율은 \((\lambda_1+\lambda_2+\lambda_3)/\sum_k \lambda_k\)다. 아래 시뮬레이터는 12×12 공분산 행렬을 Jacobi 회전으로 실제 고유분해한다.
    3D

    임베딩 공간 탐색기

    드래그 회전 · 휠/핀치 확대 · 탭하면 선택
    단어 찾기
    투영 축
    라벨
    범주 (눌러서 숨기기)
    최근접 이웃 (12차원 코사인)
    선택 단어—
    1위 이웃 (cos)—
    3D가 보존한 분산—
    12D top-5 중 3D top-5와 겹침—
    해볼 것: ① 공간을 돌려 범주별 무리를 찾아보자. PCA는 범주 정보를 주지 않았는데도 사람·동물·음식·장소가 따로 모인다. ② "왕"을 눌러 이웃을 보면 아버지·왕자·남자·할아버지가 나온다. 각각 성별·왕족·나이 중 일부를 공유한다. ③ "의미 축 3개"로 바꾸고 x=성별, y=왕족, z=나이를 고르면 왕/여왕/왕자/공주가 정육면체의 꼭짓점처럼 정렬된다. x=동서, y=크기, z=장소로 두면 나라와 수도가 두 줄로 나란히 선다. ④ 오른쪽 아래 "겹침" 수치를 보자. 3D에서 가까워 보여도 12차원에서는 아닐 수 있다 — 투영은 항상 정보를 버린다. 3D 보존 분산이 60%도 안 된다는 점을 기억하자.

    무리 짓기보다 더 흥미로운 것은 무리 사이의 방향이다. 시뮬레이터에서 남자 → 여자로 가는 화살표와 왕 → 여왕, 소년 → 소녀로 가는 화살표가 거의 같은 방향, 같은 길이라는 점에 주목하자. 임베딩 공간에서 "성별"은 하나의 방향이다. 이 관찰이 다음 절의 벡터 산술로 이어진다.

    2D·3D 그림을 너무 믿지 말 것

    논문과 블로그의 임베딩 그림은 대부분 PCA나 t-SNE, UMAP으로 수백 차원을 2차원으로 누른 것이다. PCA는 전체 구조(큰 분산)를 보존하는 대신 국소 이웃을 뭉개고, t-SNE·UMAP은 국소 이웃을 보존하는 대신 무리 사이의 거리와 크기를 왜곡한다. 실제 판단(최근접 이웃, 검색, 유추)은 언제나 원래 차원에서 계산해야 한다. 위 시뮬레이터의 이웃 목록이 3D 좌표가 아니라 12차원 코사인으로 계산되는 이유다.

    벡터 산술과 유추: 왕 − 남자 + 여자 = ?

    2013년 Mikolov 등이 word2vec을 발표하며 세상을 놀라게 한 장면이 이것이다. 학습된 벡터로 \(\mathbf{v}_{\text{king}} - \mathbf{v}_{\text{man}} + \mathbf{v}_{\text{woman}}\)을 계산하고 가장 가까운 단어를 찾으면 queen이 나온다. 아무도 "성별"이나 "왕족"을 가르쳐 주지 않았는데 말이다. 원리는 그림 4-4와 같다. "남자 → 왕"의 차이 벡터가 "여자 → 여왕"의 차이 벡터와 같다면, 네 점은 평행사변형을 이룬다.

    성별 · 왕족 ← 여성 · 성별 · 남성 → 왕족 → 남자 여자 왕 여왕 + 왕족 여자 − 남자 왕 − 남자 + 여자 → 여왕 나라 · 수도 동서(지역) → 규모 → 한국 일본 서울 도쿄 수도 = 나라 + (서울−한국)
    그림 4-4. 유추의 평행사변형. 왼쪽: "남자→왕"과 "여자→여왕"이 같은 왕족 방향(보라)이고, "남자→여자"와 "왕→여왕"이 같은 성별 방향(분홍)이다. 오른쪽: "나라 → 그 나라의 수도"도 공통 방향이다. 점선이 산술로 찾는 마지막 변이다.

    정식으로 쓰면 "a가 b에 대한 관계는 ?가 c에 대한 관계와 같다"(b : a = c : ?)를 다음처럼 푼다. 입력 단어 세 개를 후보에서 빼는 것이 중요하다. 빼지 않으면 대개 a 자신(예: 왕)이 1등이 된다.

    $$d^{*} = \underset{w \notin \{a,b,c\}}{\arg\max}\ \cos\bigl(\mathbf{v}_w,\ \mathbf{v}_a - \mathbf{v}_b + \mathbf{v}_c\bigr)$$
    예: a = 왕, b = 남자, c = 여자 → \(\mathbf{v}_{\text{왕}} - \mathbf{v}_{\text{남자}} + \mathbf{v}_{\text{여자}}\)에 가장 가까운 단어.
    SIMULATOR

    벡터 산술 & 유추

    빈 점을 탭하면 그 단어가 선택한 자리에 들어간다
    프리셋
    a − b + c
    −+
    캔버스에서 탭하면 바꿀 자리
    보기 평면
    결과 top-5 (12차원 코사인)
    결과 1위—
    기대 답의 순위—
    제외 안 할 때 1위—
    프리셋 8개 정답률—
    "유추 평면"은 b를 원점으로 a − b와 c − b가 펼치는 평면이다. 결과 벡터 a − b + c(★)는 정확히 이 평면 위에 있고, 다른 단어들은 평면에서 멀수록 흐리게 그렸다. 해볼 것: ① 프리셋을 차례로 눌러 평행사변형을 확인하자. 서울 − 한국 + 일본, 김치 − 한국 + 일본도 된다. ② "입력 단어 제외"를 끄면 김치 − 한국 + 일본의 답이 김치 자신으로 바뀐다. 실제 word2vec에서도 흔한 일이다. ③ 잡음 σ를 0.15 → 0.3으로 올려 보자. 8개 프리셋 정답률이 급격히 떨어진다. 차이 벡터(예: 여자 − 남자)는 두 벡터의 잡음을 모두 품으므로 잡음에 특히 약하다. ④ c 자리에 아무 단어나 탭해서 넣어 보자. 왕 − 남자 + 고양이 같은 엉뚱한 조합에도 산술은 언제나 "무언가"를 돌려준다 — 결과를 믿을지는 cos 값과 상식으로 판단해야 한다.
    정직한 주석: 실제 임베딩에서 유추는 생각만큼 깔끔하지 않다

    위 시뮬레이터는 의미 방향이 깨끗하도록 설계한 벡터라서 잘 된다. 실제 word2vec(Google News, 300차원)에서 "king − man + woman = queen"은 정말로 성립하지만, 유추 벤치마크 전체의 정답률은 70% 안팎이고 관계 종류에 따라 편차가 크다. 문법적 관계(시제, 복수형)는 잘 되고 백과사전적 관계는 덜 된다. 또 입력 단어를 제외하는 규칙이 결과를 상당 부분 "만들어 낸다"는 지적이 있다(제외하지 않으면 많은 문제에서 b나 a가 1위다). 더 나아가 현대 LLM의 입력 임베딩은 여러 층을 거치며 문맥과 섞이기 전의 출발점일 뿐이어서, 단어 산술이 LLM의 추론 방식을 설명하는 것도 아니다. 유추는 "임베딩 공간에 선형 구조가 생긴다"는 사실을 보여 주는 멋진 시연이지, 만능의 추론 도구가 아니다.

    임베딩을 손으로 바꾸기: 의미는 상대적 위치다

    지금까지는 주어진 벡터를 관찰만 했다. 이번에는 직접 바꿔 보자. 임베딩은 결국 숫자 목록이고, 학습이란 이 숫자들을 조금씩 옮기는 일이다. 아래 평면에서 단어를 끌어 옮기면 그 단어의 12차원 벡터가 실제로 수정된다. 평면이 PCA 평면(선택한 8개 단어의 주성분)이면 두 주성분 방향 \(\mathbf{u}_1, \mathbf{u}_2\)로만, 의미 축 평면이면 선택한 두 축으로만 바뀐다.

    $$\mathbf{v}_w \leftarrow \mathbf{v}_w + \Delta x\,\mathbf{u}_1 + \Delta y\,\mathbf{u}_2$$
    \(\Delta x, \Delta y\)는 화면에서 끈 거리(평면 좌표). 평면에 수직인 나머지 10개 성분은 그대로다. 아래 막대에서는 12개 성분을 하나씩 직접 고칠 수도 있다.
    SIMULATOR

    임베딩을 손으로 바꾸기

    라벨 붙은 점을 끌어 옮기기
    단어 묶음 (8개)
    편집 평면
    실시간 유추 확인
    선택 단어의 최근접 이웃
    선택 단어의 벡터 (막대를 좌우로 끌어 편집)
    8개 단어의 코사인 유사도 행렬
    선택 단어—
    최근접 1위—
    유추 결과—
    원래 벡터에서 ‖Δ‖—
    흐린 점은 나머지 68개 단어(이웃 계산에 포함), 점선 고리는 원래 위치다. 해볼 것: ① "왕족·가족"에서 여왕을 끌어 남자 쪽으로 옮기자. 유사도 행렬의 여왕 행이 바뀌고, 오른쪽 "왕 − 남자 + 여자" 결과가 여왕에서 다른 단어로 넘어가는 순간이 온다. ② "의미 축 2개"에서 x=성별, y=나이로 두고 소년을 위로 끌어 올리면 이웃이 아들 → 남자 → 아버지 → 할아버지로 바뀐다. 나이 축 값이 막대에서도 함께 움직인다. ③ "동물·음식"에서 돼지를 음식 쪽으로 끌면, 최근접 이웃이 동물에서 음식으로 넘어간다. 막대에서 "음식" 성분을 직접 올려도 같다. ④ 모든 단어를 같은 방향으로 똑같이 옮기면(원점 회전·이동) 이웃 관계는 변하지 않는다는 점을 생각해 보자 — 의미는 좌표값이 아니라 단어들 사이의 상대적 위치에 있다.

    이 실험에서 체감해야 할 핵심은 두 가지다. 첫째, 단어 하나의 "의미"는 그 벡터 자체보다 다른 벡터들과의 관계로 정해진다. 여왕을 남자 옆으로 옮기면, 숫자 12개가 바뀌었을 뿐인데 모델 입장에서 여왕은 "남자 같은 것"이 된다. 둘째, 공간 전체를 회전시키면 모든 내적과 코사인이 그대로이므로 모델의 행동도 그대로다. 그래서 실제 학습된 임베딩의 좌표축 하나하나에는 의미가 없다. 의미 있는 것은 방향과 거리, 그리고 그것들이 이루는 기하 구조다. 학습은 손가락 대신 기울기가 이 끌어 옮기기를 수십억 번 하는 과정이다. 정확히 어떤 힘으로 끄는지 다음 절에서 보자.

    회전 불변성과 해석 가능성

    임의의 직교 행렬 \(Q\)에 대해 \(E' = EQ\)로 모든 임베딩을 돌리고 다음 층 가중치를 \(Q^{\top}\)로 보정하면 모델 출력은 완전히 같다. 이 대칭성 때문에 학습된 임베딩의 개별 차원은 해석이 어렵다. 그럼에도 성별·시제·국가 같은 개념은 공간 안의 특정 방향으로 나타나는 경향이 있는데, 이를 "선형 표현 가설"이라 부른다. 희소 오토인코더(SAE) 같은 해석 가능성 연구는 LLM 내부에서 이런 방향을 대규모로 찾아내는 작업이다.

    word2vec: 주변 단어를 맞히며 배우는 임베딩

    word2vec의 skip-gram 모델은 단순한 예측 문제를 푼다. 문장의 각 위치에서 중심 단어 \(c\)를 보고, 윈도우 안의 주변 단어 \(o\)가 무엇인지 맞힌다. 단어마다 벡터가 두 개 있다. 중심 단어로 쓰일 때의 \(\mathbf{v}_c\)(입력 임베딩)와 주변 단어로 쓰일 때의 \(\mathbf{u}_o\)(출력 임베딩)다. 원래 정의는 \(V\)개 단어 전체에 소프트맥스를 하는 것이지만 \(V\)가 크면 너무 비싸다. 그래서 실제로는 음성 샘플링(negative sampling, SGNS)으로 바꾼다. "진짜 주변 단어 쌍인가?"를 맞히는 이진 분류다.

    중심 단어 "고양이", 윈도우 k = 2 귀여운 고양이 를 키운다 (문장 끝) 윈도우: 중심에서 앞뒤 2칸 양성 쌍 (label 1) (고양이, 귀여운) (고양이, 를) (고양이, 키운다) 음성 쌍 (label 0, 무작위 k개) (고양이, 기차) (고양이, 커피) (고양이, 역) 점수 = σ(uo · vc) 양성: 점수 → 1, 두 벡터를 가깝게 음성: 점수 → 0, 두 벡터를 멀게 음성 분포 ∝ 빈도^0.75
    그림 4-5. Skip-gram + negative sampling. 중심 단어와 윈도우 안의 단어는 양성 쌍, 무작위로 뽑은 단어는 음성 쌍이 된다. 모델은 양성의 내적을 키우고 음성의 내적을 줄이도록 두 임베딩을 동시에 고친다. 같은 문맥("를 키운다")을 공유하는 고양이와 강아지는 같은 \(\mathbf{u}\)들 쪽으로 끌려가 결국 서로 가까워진다.
    $$\mathcal{L}(c,o) = -\log \sigma(\mathbf{u}_o\cdot\mathbf{v}_c) \;-\; \sum_{j=1}^{k} \log \sigma(-\mathbf{u}_{n_j}\cdot\mathbf{v}_c), \qquad n_j \sim P_n(w) \propto f(w)^{3/4}$$
    \(\sigma\)는 시그모이드, \(k\)는 음성 표본 수(작은 코퍼스 5~20, 큰 코퍼스 2~5), \(f(w)\)는 단어 빈도. 3/4 제곱은 흔한 단어를 덜, 드문 단어를 더 뽑게 만든다.

    기울기도 간단하다. \(s = \sigma(\mathbf{u}\cdot\mathbf{v}_c)\), 정답 레이블 \(y \in \{0,1\}\)이라 하면 \(\partial\mathcal{L}/\partial\mathbf{v}_c = \sum (s-y)\,\mathbf{u}\), \(\partial\mathcal{L}/\partial\mathbf{u} = (s-y)\,\mathbf{v}_c\)이다. 3장의 로지스틱 회귀와 똑같은 형태로, 예측이 틀린 만큼 상대 벡터 방향으로 끌거나 민다. 8절에서 손으로 하던 일을 이 기울기가 대신하는 것이다. 아래 시뮬레이터는 5절의 코퍼스로 이 SGD를 브라우저에서 그대로 돌린다(연산은 모두 실제 계산이고, 난수 시드를 고정했다).

    SIMULATOR

    브라우저 word2vec (SGNS)

    임베딩 차원 d
    에폭0
    평균 손실—
    같은 그룹 평균 cos—
    다른 그룹 평균 cos—
    점은 입력 임베딩 \(\mathbf{v}\), 색은 단어 그룹(색은 우리가 붙인 정답일 뿐 학습에는 쓰이지 않는다). 해볼 것: ① "학습 시작"을 누르고 처음 20~30 에폭을 지켜보자. 무작위로 흩어진 점들이 동물·음료·탈것 세 무리로 갈라진다. 손실 곡선과 "같은 그룹 cos − 다른 그룹 cos" 차이가 함께 커진다. ② 학습률을 0.3으로 올리면 점이 요동치며 손실이 덜 매끄럽게 줄고, 0.005로 내리면 거의 움직이지 않는다(3장의 학습률 실험과 같다). ③ d = 2는 공간이 좁아 무리가 겹치기 쉽다. d = 8로 바꾸면 손실이 더 낮아지고 PCA 2D에서도 무리가 더 잘 갈린다 — 차원이 표현력이다. ④ "다른 시드"를 여러 번 눌러 보자. 무리의 위치와 방향은 매번 다르지만 무리 구조는 같다. 회전 불변성(8절)을 학습이 직접 보여 준다.
    word2vec은 사실 행렬 분해다

    Levy와 Goldberg(2014)는 SGNS가 수렴하면 \(\mathbf{u}_c\cdot\mathbf{v}_w \approx \mathrm{PMI}(w,c) - \log k\)가 됨을 보였다. 즉 word2vec은 5절의 PMI 행렬을 \(\log k\)만큼 이동시킨 것을 암묵적으로 분해한다. "세는 방법"과 "예측하는 방법"이 같은 대상을 다른 경로로 근사하는 셈이다. 같은 해(2014)의 GloVe는 아예 동시출현 횟수의 로그를 직접 맞히는 목적 함수로 두 관점을 합쳤다. 실제 word2vec 구현에는 이 밖에도 흔한 단어를 확률적으로 건너뛰는 subsampling, 학습률 선형 감소 등의 요령이 들어간다. 원 논문은 Google News 약 1천억 단어로 300차원 벡터 300만 개를 학습했다.

    문맥 임베딩과 문장 임베딩

    word2vec 벡터는 정적(static)이다. 단어 하나에 벡터 하나. 그런데 "배"는 "배가 아프다"에서는 신체, "배를 타고"에서는 탈것, "배를 깎아 먹었다"에서는 과일이다. 정적 임베딩은 세 의미의 평균 어딘가, 어느 무리에도 속하지 않는 애매한 곳에 놓일 수밖에 없다.

    정적 임베딩 (word2vec, 입력 테이블) 문맥 임베딩 (트랜스포머 층 통과 후) 신체 머리손 탈것 기차버스 과일 사과포도 배 세 의미의 평균 → 어디에도 안 속함 신체 탈것 과일 배(가 아프다) 배(를 타고) 배(를 깎아) 출발점은 같은 행 E[배]
    그림 4-6. 정적 임베딩과 문맥 임베딩. 왼쪽: 테이블에서 꺼낸 "배"의 벡터는 문맥과 무관하게 하나다. 오른쪽: 트랜스포머는 같은 출발점에서 시작해, 어텐션으로 주변 토큰의 정보를 섞어 문맥에 맞는 위치로 벡터를 옮긴다.

    현대 LLM의 입력 임베딩 테이블도 사실 정적이다. 차이는 그 뒤에 있다. 테이블에서 꺼낸 벡터가 5장의 어텐션을 거치며 같은 문장의 다른 토큰 벡터들과 가중합으로 섞이고, 6장의 트랜스포머 블록 수십 개를 지나며 점점 문맥을 반영한 문맥 임베딩(contextual embedding)이 된다. ELMo(2018)와 BERT(2018)가 이 방식의 위력을 보여 준 뒤로 "단어 하나에 벡터 하나"라는 생각은 출발점으로만 남았다.

    문장 임베딩과 의미 검색

    단어가 아니라 문장이나 문서 전체를 벡터 하나로 만들면, 코사인 유사도로 "의미가 비슷한 문서"를 찾을 수 있다. 가장 단순한 방법은 토큰 벡터들의 평균 풀링(mean pooling)이다.

    $$\mathbf{s} = \frac{1}{n}\sum_{i=1}^{n}\mathbf{h}_i, \qquad \text{score}(q, \text{doc}) = \cos(\mathbf{s}_q, \mathbf{s}_{\text{doc}})$$
    \(\mathbf{h}_i\)는 각 토큰의 벡터. 실제 문장 임베딩 모델(Sentence-BERT 계열 등)은 문맥 임베딩 \(\mathbf{h}_i\)를 평균하고, 비슷한 문장 쌍이 가까워지도록 대조 학습으로 추가 학습한다.
    SIMULATOR

    평균 풀링 문장 임베딩과 코사인 검색

    질의 (토큰을 누르면 풀링에서 뺐다 넣었다 할 수 있다 · 회색 = 어휘에 없는 토큰)
    문서 순위 (코사인 유사도)
    질의 예시
    풀링

    단어 벡터는 6~8절과 같은 교육용 12차원 벡터(76단어)다. 조사처럼 어휘에 없는 토큰은 무시한다.

    풀링에 쓴 토큰—
    1위 문서 cos—
    cos("고양이가 쥐를 봤다", "쥐가 고양이를 봤다")—
    해볼 것: ① 기본 질의 "왕자 가 런던 에 갔다"의 1위가 "왕과 여왕이 파리에 갔다"인지 보자. 겹치는 단어가 "갔다" 하나뿐인데도 왕족·도시 방향이 비슷해서 이긴다. 단어 일치가 아닌 의미 검색이다. ② 질의 토큰 "갔다"를 눌러 빼면 순위가 어떻게 바뀌는지 보자. ③ 오른쪽 아래 수치: 평균 풀링은 어순을 무시하므로 "고양이가 쥐를 봤다"와 "쥐가 고양이를 봤다"가 cos = 1이다. 누가 누구를 봤는지 구분하려면 어순을 아는 문맥 임베딩이 필요하다. ④ 질의를 "빨강 사과 를 먹었다"로 고쳐 쓰면 1위가 어떻게 되는지 확인하자.

    이 검색 방식이 10장 RAG의 핵심 부품이다. 문서들을 미리 임베딩해 벡터 데이터베이스에 넣어 두고, 질문이 오면 질문을 임베딩해 코사인이 높은 문서를 찾아 LLM에게 함께 준다. 실제 임베딩 모델의 차원은 다음과 같다(2024년 기준 대표 예).

    모델차원비고
    all-MiniLM-L6-v2 (Sentence-Transformers)384가볍고 빠른 영어 문장 임베딩
    BERT-base / multilingual-e5-base768다국어 지원 e5 계열
    bge-m3, multilingual-e5-large1,024한국어 포함 다국어 검색에 널리 쓰임
    OpenAI text-embedding-3-small / -large1,536 / 3,072차원 축소 옵션 제공
    LLM 기반 임베딩 (e5-mistral 등)4,0967B급 LLM의 은닉 차원 그대로
    임베딩은 데이터의 편향도 배운다

    분포 가설은 양날의 검이다. 임베딩은 코퍼스에 나타난 그대로의 연관을 배우므로 사회적 편향도 함께 배운다. Bolukbasi 등(2016)은 Google News word2vec에서 "man : computer programmer = woman : ?"의 답으로 homemaker가 나온다는 것을 보였다. 방법은 이 장의 도구 그대로다. 성별 방향 \(\mathbf{g} = \mathbf{v}_{he} - \mathbf{v}_{she}\)(여러 쌍의 평균)를 구하고 직업 단어를 \(\mathbf{g}\)에 투영하면, 성별과 무관해야 할 직업들이 한쪽으로 치우쳐 있다. 이 방향을 투영해서 빼 버리는 "편향 제거"도 제안되었지만, 편향이 다른 방향에 남아 여전히 복원된다는 반론(Gonen & Goldberg, 2019)도 있다. 8절에서 손으로 했던 "벡터 고치기"가 실제로는 얼마나 어려운 문제인지 보여 주는 사례다.

    핵심 정리

    1. 언어 모델의 입력은 토큰화(텍스트 → 토큰 ID)와 임베딩(ID → \(d\)차원 벡터) 두 단계를 거친다. 현대 LLM은 서브워드 단위를 쓰고, 어휘 크기 \(V\)는 시퀀스 길이와 임베딩 크기의 트레이드오프다.
    2. BPE는 문자(또는 바이트)에서 출발해 가장 빈번한 인접 쌍을 반복 병합한다. 바이트 수준 BPE는 [UNK]가 없지만, 한글 음절은 3바이트라 학습 데이터 비중이 작으면 한국어가 토큰을 더 많이 쓴다.
    3. 원-핫 × \(E\) = \(E\)의 한 행. 임베딩은 곱셈 없는 룩업이지만 역전파로 학습되는 가중치다. GPT-2 small 50,257 × 768 ≈ 38.6M, Llama 3 8B 128,256 × 4,096 ≈ 525M. 가중치 공유는 출력층과 테이블을 같이 쓴다.
    4. 분포 가설: 비슷한 문맥의 단어는 비슷한 의미다. 동시출현 → PPMI(흔한 단어 보정) → SVD가 세는 방법이고, word2vec(SGNS)은 같은 대상(이동된 PMI)을 예측으로 근사한다.
    5. 임베딩 공간에서 유사도는 코사인, 시각화는 PCA 같은 투영으로 한다. 투영은 정보를 버리므로 판단은 원래 차원에서 해야 한다.
    6. 의미 관계는 공간의 방향으로 나타나 a − b + c 유추가 가능하다. 하지만 입력 제외 규칙에 기대고, 실제 모델에서는 관계에 따라 잘 안 되는 경우가 많다.
    7. 임베딩은 숫자 목록이고 의미는 상대적 위치다. 공간 전체 회전에 불변이라 개별 축은 의미가 없고, 학습은 기울기가 벡터를 끌고 미는 과정이다.
    8. 정적 임베딩은 다의어를 구분하지 못한다. 트랜스포머는 어텐션으로 문맥 임베딩을 만들고, 풀링한 문장 임베딩은 코사인 검색(RAG)의 기반이 된다. 임베딩은 데이터의 편향도 함께 배운다.

    확인 퀴즈

    Q1. BPE 학습의 각 단계에서 새 토큰으로 병합하는 것은?

    BPE는 순수하게 빈도 기반이다. 매 단계 모든 인접 쌍의 빈도를 세어 최빈 쌍을 병합하고 그 규칙을 순서대로 기록한다. 새 텍스트는 같은 규칙을 같은 순서로 적용해 토큰화한다. 언어학적 형태소와 일치한다는 보장은 없다.

    Q2. 바이트 수준 BPE 토크나이저에서 같은 내용의 한국어 문장이 영어보다 토큰을 더 많이 쓰는 주된 이유로 가장 알맞은 것은?

    출발점(음절당 3바이트)과 병합 예산의 배분(코퍼스 빈도에 비례) 두 요인이 겹친다. 어휘를 키우고 다국어 데이터 비중을 높이면 격차가 줄어든다.

    Q3. 어휘 128,256, 은닉 차원 4,096인 Llama 3 8B의 입력 임베딩 테이블 파라미터 수와, 가중치 공유를 하지 않을 때 출력층까지 합한 값은?

    128,256 × 4,096 = 525,336,576 ≈ 525M. 출력층(4,096 × 128,256)도 같은 크기라 공유하지 않으면 합계 약 1.05B, 전체 8.03B의 13% 정도다. BF16이면 테이블 하나에 약 1.05 GB다.

    Q4. 동시출현 행렬에서 원시 횟수 대신 PPMI를 쓰는 이유는?

    PMI는 \(P(w,c)\)를 독립일 때의 기대값 \(P(w)P(c)\)와 비교한다. 흔한 단어는 \(P(c)\)가 커서 PMI가 작아진다. 작은 코퍼스에서 잡음이 큰 음수 PMI는 0으로 잘라 PPMI를 쓴다. 5절 시뮬레이터에서 횟수 ↔ PPMI를 바꿔 보면 차이가 보인다.

    Q5. 유추 a − b + c에서 결과 후보에서 a, b, c를 제외하는 관례에 대해 옳은 설명은?

    \(\mathbf{v}_a - \mathbf{v}_b + \mathbf{v}_c\)는 보통 a와 c에 가깝다. 차이 벡터가 작기 때문이다. 7절 시뮬레이터에서 "입력 단어 제외"를 끄면 김치 − 한국 + 일본의 답이 김치로 바뀐다. 거리 척도를 바꿔도 이 경향은 남는다.

    Q6. "배가 아프다"와 "배를 타고"의 "배"에 대해 옳은 것은?

    입력 임베딩 테이블은 토큰 ID만 보므로 문맥과 무관하다. 문맥 구분은 그 뒤의 어텐션(5장)과 트랜스포머 층(6장)이 한다. 정적 단어 벡터의 평균 풀링은 어순과 문맥을 무시하므로 다의어를 구분하지 못한다.