2장. Transformer 입력 표현
임베딩: 번호를 성격표로
학교 출석부를 떠올려 봅니다. 출석 번호 7번과 8번은 번호가 붙어 있을 뿐, 두 학생의 성격이 비슷하다는 뜻은 아닙니다. 학생을 제대로 알려면 번호로 생활기록부를 찾아 "활발함 0.8, 꼼꼼함 0.3, 수학 좋아함 0.9" 같은 성격표를 봐야 합니다. 토큰 ID 도 그냥 번호라서 5913번과 5914번이 비슷한 뜻이라는 보장이 없습니다. 그래서 번호마다 실수 여러 개짜리 성격표(벡터)를 붙이는데, 이것이 임베딩입니다.
그림 왼쪽의 정수 ID 1079(Qwen 에서 " am")가 가운데의 큰 표, 임베딩 층으로 들어갑니다. 표의 크기는 (어휘 수 × d_model)이고, 1079번 행을 그대로 꺼내 오른쪽의 (1 × d_model) 벡터가 됩니다. 곱셈 없이 번호로 바로 찾아오는 조회(lookup)라서 빠릅니다. 데이터 분석으로 치면 행이 15만 개, 열이 896개인 DataFrame 에서 df.iloc[1079] 로 한 행을 꺼내는 일입니다.
임베딩 층의 성질
| 성질 | 설명 | 출석부로 치면 |
|---|---|---|
| 모양 | (어휘 수, d_model) 표 | 학생 수 × 성격 항목 수 |
| 꺼내는 법 | 토큰 ID 번째 행을 그대로 꺼낸다(lookup) | 번호로 생활기록부 펼치기 |
| 처음 값 | 무작위 숫자 | 아무렇게나 적은 첫 기록 |
| 학습 뒤 | 비슷한 뜻의 토큰끼리 가까운 벡터가 된다 | 지내 보며 고쳐 쓴 기록 |
| 같은 ID | 언제나 같은 행을 꺼낸다 | 같은 번호면 같은 기록부 |
짧은 예시
import numpy as np
from transformers import AutoConfig
# 손으로 만든 작은 임베딩 표: 사전 4개 × d_model 3
# 행 번호가 토큰 ID 다(0: 나, 1: 는, 2: 학생, 3: 입니다 라고 해 두자)
table = np.array([
[0.1, 0.5, 0.2],
[0.0, 0.3, 0.9],
[0.8, 0.1, 0.4],
[0.2, 0.7, 0.6],
])
# 토큰 ID 리스트로 행을 한꺼번에 꺼낸다(NumPy 의 팬시 인덱싱 = lookup)
ids = [2, 0, 2]
x = table[ids]
print(x.shape) # 결과: (3, 3) 토큰 3개 × d_model 3
print(x[0], x[2]) # 결과: [0.8 0.1 0.4] [0.8 0.1 0.4] 같은 ID 2 는 같은 행
# 실제 Qwen 의 표 크기는 설정 파일(config)만 읽어 확인한다. 모델 무게는 내려받지 않는다
# AutoConfig = 모델 이름만 주면 그 모델의 설정표를 불러오는 도구
config = AutoConfig.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
print(config.vocab_size, config.hidden_size) # 결과: 151936 896
rows_x_cols = config.vocab_size * config.hidden_size
print(f"{rows_x_cols:,}") # 결과: 136,134,656
print(f"BF16(2바이트) 메모리: {rows_x_cols * 2 / 1e6:.1f} MB") # 결과: BF16(2바이트) 메모리: 272.3 MB이 코드가 뜻하는 것
출석 번호 [2, 0, 2] 로 생활기록부를 세 번 펼친 코드입니다. 2번은 두 번 나왔으므로 같은 기록이 두 번 꺼내집니다. 아래 절반은 실제 Qwen 의 출석부 크기입니다. 손으로 곱하면 151,936 × 896 = 136,134,656 칸이고, 한 칸에 2바이트(BF16)면 272,269,312바이트, 약 272 MB 입니다. 0.5B(5억 개) 모델 파라미터의 4분의 1 남짓이 이 표 하나에 들어 있습니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
table[ids] | ID 번째 행들을 꺼내 쌓는다 | 번호로 기록부 펼치기 |
x.shape | (토큰 수, d_model) | 행은 토큰, 열은 성격 항목 |
AutoConfig.from_pretrained(...) | 설정표만 읽는다 | 출석부의 표지만 보기 |
vocab_size * hidden_size | 표의 칸 수 | 파라미터 수 |
주의할 점
- Qwen 의
config.vocab_size는 151,936 이고, 실습 1에서 찍을len(qwen_tokenizer)는 151,665 입니다. 계산을 빠르게 하려고 표의 행을 넉넉히 만들어 두었기 때문에 표가 실제 사전보다 조금 큽니다. 남는 행은 쓰이지 않습니다. - 표에서 꺼낸 벡터는 같은 ID 면 늘 같습니다. 하지만 3장의 Self-Attention 을 지나면 주변 토큰의 정보가 섞여, 같은 토큰이라도 문장마다 다른 벡터가 됩니다. "배"가 과일인지 타는 배인지는 이 단계에서 가려집니다.
- 꺼낸 행렬의 모양은 (토큰 수, d_model)입니다. 열 수는 사전 크기(4)가 아니라 d_model(3)입니다. shape 을 헷갈리면 뒤의 행렬곱에서 오류가 납니다.