임베딩과 Word2Vec을 이해해 보기
『한 권으로 끝내는 실전 LLM 파인튜닝』을 읽다가 다음과 같은 설명을 만났다.
어휘 크기가 1,000이고 임베딩 차원을 300으로 설정하면,
nn.Embedding레이어는 1,000개의 단어 각각에 대해 300차원의 벡터를 만든다.
문장 자체는 어렵지 않았다. 그런데 이해한 내용을 직접 설명해 보려고 하니 막혔다.
단어 하나마다 300차원의 벡터가 존재한다는 것은 단어 하나가 300개의 숫자로 표현된다는 뜻일까? 그렇다면 그 숫자들은 각각 무엇을 의미할까? 차원이 많아질수록 단어의 의미를 더 정확하게 표현할 수 있는 걸까?
그동안 임베딩을 ‘문장을 숫자로 변환하는 과정’ 정도로만 이해하고 있었다. 이번에는 컴퓨터가 언어를 처리하기 위해 왜 임베딩이 필요한지, 단어와 벡터가 어떻게 연결되는지를 처음부터 다시 정리해 보기로 했다.
컴퓨터가 단어를 그대로 이해할 수는 없다
사람은 ‘고양이’라는 단어를 보면 작은 동물의 모습이나 울음소리, 성격과 같은 여러 정보를 떠올릴 수 있다. 하지만 컴퓨터는 ‘고양이’라는 문자열 자체에서 이런 의미를 바로 읽어 내지 못한다.
신경망은 덧셈과 곱셈 같은 수치 연산을 통해 작동한다. 따라서 자연어를 모델에 입력하려면 문자나 단어를 계산 가능한 숫자로 바꾸는 과정이 필요하다.
가장 단순하게 생각할 수 있는 방법은 어휘에 포함된 각 단어에 번호를 붙이는 것이다.
고양이 → 0
강아지 → 1
자동차 → 2
보험 → 3
하지만 번호를 그대로 모델에 입력하면 문제가 생긴다. 숫자만 보면 ‘보험’이 ‘고양이’보다 세 배 크다거나, ‘강아지’와 ‘자동차’가 ‘고양이’와 ‘보험’보다 가깝다는 식의 관계가 생겨 버린다. 이 숫자에는 언어적인 의미가 없는데도 모델은 숫자의 크기와 간격을 계산할 수 있기 때문이다.
단어에 붙인 번호는 계산할 값이라기보다, 각 단어를 구별하기 위한 인덱스로 사용해야 한다. 그리고 모델은 그 인덱스에 해당하는 별도의 숫자 표현을 찾아서 사용한다. 이 숫자 표현이 임베딩 벡터다.
원-핫 인코딩은 단어를 어떻게 구분할까
임베딩 이전에 단어를 표현하는 대표적인 방법으로 원-핫 인코딩(one-hot Encoding)이 있다.
어휘에 네 개의 단어가 있다면 각각을 다음과 같이 표현할 수 있다.
고양이 → [1, 0, 0, 0]
강아지 → [0, 1, 0, 0]
자동차 → [0, 0, 1, 0]
보험 → [0, 0, 0, 1]
각 벡터에서 자신의 위치만 1이고 나머지는 모두 0이다. 이 방법을 사용하면 단어를 서로 다른 숫자 벡터로 명확하게 구분할 수 있다.
하지만 원-핫 인코딩에는 중요한 한계가 있다.
우선 어휘가 커질수록 벡터도 지나치게 커진다. 10만 개의 토큰을 구분하려면 각 토큰을 10만 차원의 벡터로 표현해야 한다. 벡터 대부분이 0이라서 공간도 비효율적으로 사용된다.
더 중요한 문제는 단어 사이의 관계가 표현되지 않는다는 것이다. 원-핫 벡터에서 ‘고양이’와 ‘강아지’는 ‘고양이’와 ‘보험’만큼이나 서로 다르다. 사람에게는 고양이와 강아지가 모두 동물이라는 공통점이 있다고 인식되지만, 원-핫 인코딩만 보면 모든 단어가 서로 동일한 정도로 분리되어 있다.
컴퓨터가 단어의 의미적 관계를 활용하려면, 단어를 단순히 구별하는 것을 넘어 비슷한 단어가 비슷한 숫자 표현을 갖도록 만들 필요가 있다.
임베딩은 단어마다 학습 가능한 좌표를 부여한다
임베딩은 각각의 단어나 토큰을 비교적 작은 크기의 밀집 벡터로 표현하는 방법이다.
예를 들어 다음과 같은 코드를 생각할 수 있다.
embedding = torch.nn.Embedding(
num_embeddings=1000,
embedding_dim=300
)
PyTorch의 nn.Embedding은 정수 인덱스를 받아 해당 인덱스의 임베딩 벡터를 반환하는 일종의 조회 테이블이다. num_embeddings는 테이블에 저장할 항목 수이고, embedding_dim은 각 항목을 몇 개의 숫자로 표현할지를 뜻한다. 따라서 위 레이어의 학습 가능한 가중치 행렬 크기는 1000 × 300이 된다.
이를 표처럼 단순화하면 다음과 같다.
토큰 0 → [ 0.12, -0.31, 0.07, ..., 0.44] # 300개
토큰 1 → [-0.28, 0.15, 0.91, ..., -0.06] # 300개
토큰 2 → [ 0.43, 0.02, -0.17, ..., 0.38] # 300개
...
토큰 999 → [0.09, -0.52, 0.21, ..., 0.10] # 300개
즉, 하나의 단어나 토큰에 하나의 300차원 벡터가 대응한다.
문장에 토큰이 다섯 개 들어 있다면 각 토큰에서 300차원 벡터 하나씩을 가져오므로, 입력은 개념적으로 5 × 300 형태가 된다.
문장: 보험금을 청구하려면 어떻게 해야 하나요?
토큰 1 ─→ 300차원 벡터
토큰 2 ─→ 300차원 벡터
토큰 3 ─→ 300차원 벡터
토큰 4 ─→ 300차원 벡터
토큰 5 ─→ 300차원 벡터
정확히 말하면 현대 언어 모델은 문장을 언제나 완전한 단어 단위로 나누지는 않는다. 단어의 일부인 서브워드나 문자, 기호가 하나의 토큰이 될 수도 있다. Qwen 계열 역시 텍스트를 토큰으로 나눌 때 BPE 계열의 서브워드 토큰화 방식을 사용한다. 따라서 ‘단어마다 벡터가 하나 있다’는 설명은 입문 단계에서는 편리하지만, 실제 LLM에서는 토큰마다 벡터가 하나 있다고 표현하는 편이 더 정확하다.
300개의 숫자는 각각 무엇을 뜻할까
처음에는 300개의 숫자가 각각 단어의 속성을 하나씩 담당한다고 생각했다.
첫 번째 차원 → 사람인가?
두 번째 차원 → 동물인가?
세 번째 차원 → 긍정적인가?
네 번째 차원 → 생명체인가?
하지만 실제 임베딩은 보통 이렇게 명확하게 나뉘지 않는다.
각 차원에 사람이 이해할 수 있는 하나의 이름이 붙어 있는 것이 아니라, 여러 차원의 값이 함께 만드는 패턴 속에 의미가 분산되어 표현된다. 이를 분산 표현이라고 한다.
예를 들어 ‘고양이’의 의미가 특정 숫자 하나에 들어 있는 것이 아니라, 300개 숫자의 조합과 다른 토큰 벡터와의 상대적인 관계를 통해 표현되는 것이다.
이를 지도 위의 위치에 비유할 수 있다. 서울의 특성이 위도 하나에만 들어 있는 것은 아니다. 위도와 경도를 함께 봐야 위치를 알 수 있고, 다른 도시와의 거리와 방향을 비교해야 지리적 관계를 파악할 수 있다.
임베딩도 마찬가지다. 하나의 차원을 떼어 보는 것보다 전체 벡터가 임베딩 공간의 어디에 놓여 있고 다른 벡터와 어떤 관계를 형성하는지가 중요하다.
다만 지도는 2차원이지만 임베딩 공간은 수백 또는 수천 차원일 수 있다. 사람은 이 공간을 직접 볼 수 없기 때문에, 필요할 때는 차원 축소 기법을 사용해 2차원이나 3차원으로 투영한 뒤 대략적인 분포를 확인한다.
임베딩하면 바로 의미를 알 수 있을까
nn.Embedding 레이어를 처음 만들었다고 해서 각 토큰의 벡터에 곧바로 의미가 담기는 것은 아니다. 일반적으로 임베딩 행렬은 무작위에 가까운 값으로 시작하고, 모델의 학습 과정에서 다른 가중치와 함께 조정된다.
모델이 어떤 문맥에서 어떤 토큰이 등장하는지 반복해서 학습하면, 비슷한 역할을 하는 토큰들은 유사한 방향이나 구조를 갖는 표현으로 조정될 수 있다.
예를 들어 다음과 같은 문장이 학습 데이터에 반복해서 등장한다고 생각해 보자.
고양이는 사료를 먹는다.
강아지는 사료를 먹는다.
고양이는 동물이다.
강아지는 동물이다.
‘고양이’와 ‘강아지’는 비슷한 주변 단어와 함께 등장한다. 모델은 학습 과정에서 이러한 문맥을 활용해 두 토큰이 비슷한 관계를 갖도록 표현할 수 있다.
이를 흔히 다음과 같은 생각으로 요약한다.
비슷한 문맥에서 사용되는 단어는 비슷한 의미를 가질 가능성이 크다.
임베딩은 사람이 직접 각 단어의 의미를 숫자로 입력해서 만드는 것이 아니다. 모델이 학습 목표를 더 잘 달성하도록 벡터 값을 계속 수정하는 과정에서, 결과적으로 언어적 관계를 활용할 수 있는 표현이 형성된다.
Word2Vec은 왜 혁신적이었을까
Word2Vec은 2013년 토마시 미콜로프 연구진이 발표한 단어 표현 학습 방법이다. 연구진은 대규모 데이터에서 연속적인 단어 벡터를 효율적으로 학습하는 두 가지 구조를 제안했고, 기존 방법보다 낮은 계산 비용으로 의미적·문법적 유사성을 잘 포착할 수 있음을 보였다.
Word2Vec에는 대표적으로 두 가지 학습 방식이 있다.
CBOW
주변 단어를 보고 가운데 단어를 예측한다.
나는 _____ 을 청구했다.
주변에 ‘나는’, ‘청구했다’ 같은 단어가 있다면 빈칸에 들어갈 단어를 예측하도록 학습한다.
Skip-gram
가운데 단어를 보고 주변에 등장할 단어를 예측한다.
중심 단어: 보험금
예측할 주변 단어:
청구, 지급, 보상, 계약
이러한 예측을 잘하려면 비슷한 환경에서 사용되는 단어들이 서로 관련된 표현을 가져야 한다. 모델은 예측 오류를 줄이는 과정에서 단어 벡터를 조정한다.
Word2Vec의 중요한 점은 사람이 단어 사이의 관계를 하나씩 정의하지 않아도, 대규모 말뭉치에서 단어가 사용되는 패턴을 바탕으로 의미적·문법적 관계를 담은 벡터를 비교적 효율적으로 학습할 수 있었다는 데 있다. 후속 연구에서는 Skip-gram의 학습 속도와 표현 품질을 개선하기 위해 네거티브 샘플링과 빈출 단어 서브샘플링 같은 방법도 제안되었다.
Word2Vec의 벡터와 LLM의 임베딩은 같을까
Word2Vec과 현대 LLM의 임베딩은 모두 언어를 벡터로 표현하지만 완전히 같은 것은 아니다.
Word2Vec에서 한 단어는 일반적으로 하나의 고정된 벡터를 갖는다. 따라서 ‘배’라는 단어가 과일을 뜻하든, 사람의 신체를 뜻하든, 선박을 뜻하든 기본적으로 같은 벡터가 사용된다.
배를 먹었다.
배가 아프다.
배를 타고 섬에 갔다.
Word2Vec 자체만으로는 세 문장의 ‘배’를 문맥에 따라 서로 다른 벡터로 바꾸기 어렵다. 단어 순서와 관용 표현을 충분히 반영하기 어렵다는 한계도 지적되었다.
반면 트랜스포머 기반 언어 모델에서는 입력 단계에서 토큰 임베딩을 가져온 뒤, 셀프 어텐션과 여러 층의 연산을 거치며 주변 문맥을 반영한다. 따라서 같은 토큰이라도 최종적으로는 문장에 따라 서로 다른 표현을 형성할 수 있다.
구분하면 다음과 같다.
토큰 임베딩
→ 각 토큰에 부여된 기본적인 벡터
문맥적 표현
→ 문장 전체의 관계를 반영한 뒤 만들어진 벡터
Word2Vec이 단어를 고정된 벡터 공간에 배치하는 방법을 널리 알렸다면, 현대 언어 모델은 그 벡터를 문맥에 따라 계속 변화시키며 사용한다고 이해할 수 있다.
차원이 많으면 무조건 좋을까
임베딩 차원이 크면 하나의 토큰을 더 많은 숫자로 표현할 수 있다. 따라서 복잡한 관계를 표현할 수 있는 여지가 커진다.
그렇다고 차원을 무조건 늘리는 것이 좋은 것은 아니다.
임베딩의 어휘 크기가 (V), 임베딩 차원이 (D)라면 임베딩 행렬의 파라미터 수는 다음과 같다.
파라미터 수 = V × D
어휘 크기가 100,000이고 임베딩 차원이 300이라면 임베딩 행렬에만 3천만 개의 파라미터가 필요하다.
100,000 × 300 = 30,000,000
차원을 1,000으로 늘리면 파라미터는 1억 개가 된다.
100,000 × 1,000 = 100,000,000
차원이 커질수록 표현할 수 있는 정보는 많아질 수 있지만 다음과 같은 비용도 증가한다.
- 저장해야 할 파라미터가 많아진다.
- 학습과 추론에 필요한 연산량이 늘어난다.
- 충분한 데이터가 없다면 늘어난 표현 공간을 효과적으로 학습하기 어렵다.
- 모델 전체 구조와 균형이 맞지 않으면 차원을 늘려도 성능 향상이 제한적일 수 있다.
반대로 차원이 지나치게 작으면 서로 다른 언어적 관계를 충분히 구별하지 못할 수 있다.
결국 적절한 임베딩 차원은 하나의 정답으로 정해지는 것이 아니다. 어휘 크기, 데이터 규모, 모델 구조, 계산 자원, 수행하려는 작업을 함께 고려해야 한다.
임베딩 공간에서 가깝다는 것은 무엇일까
두 벡터가 얼마나 비슷한지 확인할 때는 코사인 유사도를 자주 사용한다.
코사인 유사도는 두 벡터의 절대적인 크기보다 방향이 얼마나 비슷한지를 측정한다.
방향이 매우 비슷함 → 1에 가까움
관련이 적음 → 0에 가까움
반대 방향 → -1에 가까움
예를 들어 잘 학습된 임베딩 공간에서는 다음과 같은 단어들이 비교적 유사한 방향에 놓일 수 있다.
자동차 ↔ 차량
의사 ↔ 간호사
보험금 ↔ 보상금
고양이 ↔ 강아지
하지만 ‘가깝다’는 표현을 조심해서 사용해야 한다.
임베딩 공간에서 가까운 두 토큰이 사람이 생각하는 모든 면에서 같은 의미라는 뜻은 아니다. 모델이 학습한 데이터와 학습 목표에 따라 비슷한 문맥에 자주 등장하거나, 문법적으로 비슷한 역할을 하는 단어가 가까워질 수도 있다.
예를 들어 반대말도 비슷한 문장에서 사용되는 경우가 많다.
기온이 높다.
기온이 낮다.
‘높다’와 ‘낮다’는 의미상 반대지만 사용되는 문맥은 비슷하다. 이 때문에 분포 기반 임베딩에서는 반의어도 가까운 위치에 나타날 수 있다.
임베딩은 사람이 만든 완벽한 의미 지도가 아니라, 모델이 학습 데이터에서 발견한 사용 패턴을 압축한 공간에 가깝다.
보험 챗봇에서 갑자기 한자가 튀어나왔던 이유
이 책을 읽으면서 과거에 진행했던 보험 도메인 프로젝트가 떠올랐다.
당시에는 보험 관련 질문에 답하는 챗봇을 구현했다. RAG를 사용해 보험 자료에서 답변의 근거를 검색하고, 언어 모델이 검색된 내용을 바탕으로 한국어 문장을 생성하도록 했다.
정확한 버전은 기억나지 않지만, API 비용이 비교적 저렴한 Qwen 계열 모델을 사용했던 것으로 기억한다.
문제는 답변의 내용이 대체로 한국어로 생성되다가 문장 중간에 갑자기 한자나 러시아어처럼 보이는 문자가 섞여 나온다는 것이었다. 검색된 근거는 한국어였기 때문에, 당시에는 프롬프트가 모델을 충분히 통제하지 못한 것이 원인이라고 생각했다.
그래서 프롬프트에 다음과 같은 지시를 계속 추가했다.
한국어로만 답변하세요.
중국어를 사용하지 마세요.
한자를 사용하지 마세요.
다른 언어를 섞지 마세요.
일부 경우에는 이런 지시가 도움이 되었지만, 문제의 원인을 정확히 파악한 뒤 수정한 것은 아니었다. 결과에 이상한 문자가 나타나면 우선 프롬프트부터 바꾸는 방식이었다.
책에서는 양자화된 Qwen 모델에서 한글 대신 한자가 출력되는 사례를 임베딩 공간과 양자화 오차를 통해 설명했다. 양자화는 모델의 가중치와 활성값을 더 낮은 정밀도의 숫자로 표현해 메모리와 계산 비용을 줄이는 기술이다. 낮은 비트 수로 값을 표현하는 과정에서는 반올림에 가까운 근사 오차가 발생할 수 있다.
당시 사용한 정확한 모델 버전과 API 내부 설정, 양자화 여부를 확인하지 못했고, Qwen은 애초에 중국어와 한국어, 러시아어를 포함한 여러 언어로 학습된 다국어 모델이어서 여러 언어의 토큰이 같은 어휘와 모델 안에 존재하기 때문에 생성 확률이 조금만 달라져도 다른 언어의 토큰이 선택될 가능성이 있으므로 내가 경험한 현상의 원인을 양자화나 임베딩 거리로 단정할 수는 없다.
당시 현상에는 다음과 같은 원인이 하나 이상 영향을 주었을 수 있다고 추측만 할 수 있다.
- 다국어 사전 학습 데이터의 영향
- 토크나이저의 서브워드 분할 방식
- temperature나 top-p 같은 생성 설정
- 검색된 RAG 문서에 포함된 특수문자나 숨은 문자
- 모델 크기와 한국어 생성 능력
- API 제공자가 사용한 양자화 방식
- 이전 대화 문맥에 포함된 다른 언어
- 단순한 확률적 생성 오류
특히 “한글과 한자가 임베딩 공간에서 가까워서 잘못 출력되었다”는 설명만으로 생성 과정을 모두 설명하기는 어렵다. 최종 토큰은 입력 임베딩만 보고 선택되는 것이 아니라, 트랜스포머의 여러 층을 통과해 만들어진 문맥적 표현과 출력 확률을 바탕으로 선택되기 때문이다.
이 경험을 지금 다시 마주한다면 프롬프트만 반복해서 수정하기보다 다음과 같이 문제를 나누어 확인할 수 있을 것 같다.
- 사용한 모델과 정확한 버전을 기록한다.
- 양자화된 모델인지 확인한다.
- 같은 입력을 여러 번 실행해 재현 빈도를 측정한다.
- temperature를 낮췄을 때도 발생하는지 비교한다.
- RAG로 검색된 원문에 다른 문자나 인코딩 문제가 없는지 확인한다.
- RAG 없이 같은 질문을 입력해 모델 자체의 문제인지 비교한다.
- 다른 한국어 지원 모델에서도 동일한 현상이 발생하는지 확인한다.
- 생성 후 언어 감지나 허용 문자 검사 같은 후처리를 적용한다.
프롬프트에 ‘한국어만 사용하라’고 지시하는 것은 여전히 필요한 대응일 수 있다. 하지만 프롬프트는 여러 해결책 중 하나일 뿐, 모델과 데이터 파이프라인에서 발생한 모든 문제를 해결해 주지는 않는다.
임베딩을 이해하고 나니 달라진 점
임베딩을 공부하기 전에는 모델이 단어의 뜻을 사전처럼 저장하고 있다가 답변할 때 꺼내 쓰는 모습을 막연하게 상상했다.
하지만 임베딩은 단어의 정의를 문장으로 저장하는 방식이 아니다. 토큰을 여러 숫자의 조합으로 표현하고, 학습 데이터에서 발견한 관계가 벡터 공간의 구조에 반영되도록 만드는 방식이다.
여기서 내가 정리한 핵심은 다음과 같다.
nn.Embedding(1000, 300)은 1,000개의 토큰마다 하나의 300차원 벡터를 부여한다는 뜻이다.- 하나의 토큰은 지정된 개수의 숫자로 표현된다.
- 각 숫자에 사람이 이해할 수 있는 의미가 하나씩 대응하는 것은 아니다.
- 의미는 여러 차원의 조합과 다른 토큰과의 상대적인 관계에 분산되어 표현된다.
- 임베딩 벡터는 학습 과정에서 모델의 목적에 맞게 조정된다.
- Word2Vec은 주변 문맥을 통해 의미적·문법적 관계가 담긴 단어 벡터를 효율적으로 학습할 수 있음을 보여주었다.
- 현대 LLM은 고정된 토큰 임베딩에서 출발하지만, 트랜스포머를 거치며 문맥에 따른 표현을 새로 만든다.
- 임베딩 차원이 크다고 항상 좋은 것은 아니며 데이터, 모델 크기, 연산 비용과 함께 결정해야 한다.
보험 챗봇에서 다른 언어가 섞여 나왔던 경험 역시 이제는 단순히 “프롬프트가 부족했다”는 문제로만 보이지 않는다.
언어 모델의 출력은 프롬프트뿐 아니라 토크나이저, 다국어 학습 데이터, 임베딩과 내부 표현, 디코딩 설정, 양자화, RAG로 전달된 문맥이 함께 만든 결과다. 출력에서 문제가 발생했을 때 어느 한 부분만 무작정 수정하기보다, 생성 파이프라인을 단계별로 나누어 원인을 확인해야 한다.
아직 남은 질문
임베딩의 기본 개념을 정리했지만 여전히 궁금한 점이 남아 있다.
- 토큰 임베딩은 트랜스포머의 각 층을 지나며 구체적으로 어떻게 변할까?
- 하나의 토큰이 문맥에 따라 서로 다른 의미를 갖는 과정은 어떻게 확인할 수 있을까?
- 임베딩 차원을 결정하는 명확한 기준이 있을까?
- 한국어와 한자처럼 여러 언어의 토큰은 다국어 모델의 공간에서 어떤 관계를 형성할까?
- 입력 임베딩과 다음 토큰을 선택하는 출력층의 가중치는 어떤 관계가 있을까?
- 양자화 전후의 임베딩과 출력 확률은 실제로 얼마나 달라질까?
다음에는 이 질문과 이어지는 셀프 어텐션을 정리해 보려고 한다. 임베딩이 각 토큰의 출발점이라면, 셀프 어텐션은 이 토큰들이 문장 안에서 서로의 정보를 주고받으며 문맥을 형성하는 과정에 해당하기 때문이다.
참고 자료
- Tomas Mikolov et al., “Efficient Estimation of Word Representations in Vector Space,” 2013.
- Tomas Mikolov et al., “Distributed Representations of Words and Phrases and their Compositionality,” 2013.
- PyTorch,
torch.nn.Embedding공식 문서. - Qwen Documentation, “Key Concepts.”
- Hugging Face Transformers, “Quantization” 및 “Quantization Concepts.”