AI 에이전트 구축Building AI Agents
Chapter 01 · Text Representation

텍스트를 숫자로 — 딥러닝의 언어 표현

기계는 단어를 이해하지 못한다. 기계가 보는 것은 벡터다. 원-핫 인코딩에서 word2vec까지, 우리는 의미를 좌표로 옮기는 여정을 따라간다.

기초 Word Embedding RNN · LSTM · GRU 감정 분석

Section 01표현의 문제

인간이 가진 능력 중 가장 놀라운 것은 언어다. 그러나 자연 상태의 언어는 기계가 이해할 수 없다. 이 책의 모든 이야기는 바로 이 간극에서 시작된다.

텍스트를 컴퓨터가 다룰 수 있는 형태로 바꾸는 일은 이미지나 표(table)를 다루는 것보다 훨씬 어렵다. 단어의 의미(signified)와 그 의미를 가리키는 기호(signifier) 사이에는 어떤 고유한 관계도 없기 때문이다. "restaurant"라는 글자 배열 자체에는 음식점이라는 개념이 들어있지 않다. 게다가 단어의 의미는 문맥과 화자의 의도에 따라 끊임없이 변한다.

따라서 첫 단계는 텍스트를 기본 단위로 쪼개는 일이다. 이 일련의 처리를 통틀어 텍스트 정규화(text normalization)라 부른다. 문장을 공백 기준으로 단어로 나누고(text segmentation), 구두점도 하나의 단위로 취급하며("He"와 "he"를 같은 인스턴스로 보기 위해) 소문자로 통일하고, 필요하면 표제어 추출(lemmatization)이나 어간 추출(stemming)을 수행한다. 텍스트를 기본 단위로 변환하는 이 작업이 바로 토큰화(tokenization)다.

원-핫 인코딩 — 가장 단순한 시작

전통적 NLP는 텍스트를 이산 기호(discrete symbols)로 표현한다. 가장 단순한 예가 원-핫 인코딩이다. 코퍼스 전체의 서로 다른 단어 집합을 어휘(vocabulary)라 하고, 각 단어를 어휘 크기만큼 긴 벡터로 나타낸다. 해당 단어의 인덱스에만 1, 나머지는 0이다.

one_hot_encoding — 문장을 희소 행렬로
def one_hot_encoding(sentence):
    words      = sentence.lower().split()
    vocabulary = sorted(set(words))
    word_to_index = {w: i for i, w in enumerate(vocabulary)}
    matrix = np.zeros((len(words), len(vocabulary)), dtype=int)
    for i, w in enumerate(words):
        matrix[i, word_to_index[w]] = 1   # 단어 위치에 1
    return matrix, vocabulary
함정 — 원-핫의 세 가지 약점 첫째, 단어의 존재/부재만 포착하므로 의미론적 관계가 전부 사라진다. 둘째, 평균적 언어는 약 200,000개 단어를 가지므로 벡터가 극도로 고차원·희소해진다. 셋째, "restaurant"와 "pizzeria"처럼 비슷한 단어조차 유사성이 0으로 표현된다.

백 오브 워즈와 TF-IDF

백 오브 워즈(Bag-of-Words, BoW)는 단어의 위치를 무시하고 빈도만 보존한다. "백(bag)"이라는 이름은 순서 정보를 버린다는 사실에서 나왔다. 문서 하나가 하나의 벡터가 되고, 각 차원은 어휘 단어의 등장 횟수다.

그런데 원시 빈도는 편향되어 있다. "good"이나 "bad"는 어디서나 자주 쓰여 변별력이 낮고, 드물게 나타나는 단어가 오히려 정보가 많다. TF-IDF는 이 직관을 수식으로 옮긴다. 단어 빈도(TF)에 역문서빈도(IDF)의 로그를 곱해, 특정 문서에만 나타나는 단어에 더 큰 가중치를 준다.

TF-IDF Weighting
w(t, d) = TF(t, d) × log( N / DF(t) )
TF(t,d) 문서 d에서 단어 t의 (정규화된) 빈도 · N 코퍼스 전체 문서 수 · DF(t) 단어 t를 포함하는 문서 수. 드물수록 가중치가 커진다.
핵심 원-핫 → BoW → TF-IDF는 모두 희소(sparse)·고차원·이산 벡터를 만든다. 메모리·계산 비용이 크고, 무엇보다 단어 간 의미적 거리를 표현하지 못한다. 이 한계가 다음 절의 임베딩을 불러온다.

Section 02희소에서 밀집으로

희소 벡터의 모든 문제는 한 가지 원인에서 비롯된다. 의미를 국소적으로(local) 한 자리에만 저장한다는 것. 우리가 원하는 것은 의미를 분산 표현(distributed representation)으로, 크기가 작고 실수로 채워진 밀집 벡터(dense vector)에 담는 것이다.

이 발상의 토대는 언어학의 분포 가설(Distributional Hypothesis)이다. "유사한 의미를 가진 단어는 비슷한 문맥에서 자주 함께 나타난다." 같은 맥락에 등장하는 단어는 같은 의미장(意味場)을 공유한다.

Concept 워드 임베딩 (Word Embedding)

단어의 의미를 인코딩하는, 크기가 제한된(보통 100~1024차원) 실수 밀집 벡터. 어휘가 커져도 벡터 크기는 늘지 않는다.

희소 벡터와 달리 임베딩 벡터는 기하학적 성질을 갖는다. 두 벡터의 거리가 곧 두 단어의 의미적 유사성이며, 따라서 클러스터링·유추(analogy)·검색 같은 연산이 가능하다.

유사성을 어떻게 잴까

벡터를 얻었으면 둘 사이의 유사성을 계산할 방법이 필요하다. 가장 단순한 후보는 내적(dot product)이다. 두 벡터가 같은 차원에서 큰 값을 가질 때 내적이 커진다. 그러나 내적은 긴 벡터, 높은 값(흔히 쓸모없는 고빈도 단어)을 선호하고, 값의 범위에 제한이 없다.

그래서 정규화된 내적, 즉 두 벡터가 이루는 각도의 코사인을 쓴다. 이것이 코사인 유사성(Cosine Similarity)이다.

Cosine Similarity
cos θ = ( a · b ) / ( ‖a‖ · ‖b‖ )
a · b 두 벡터의 내적 · ‖a‖, ‖b‖ 각 벡터의 크기(L2 norm). 결과는 -1 ~ +1 범위 — 완벽히 유사하면 1, 직교하면 0, 정반대면 -1.
직관 — 왜 코사인인가 코사인 유사성은 척도 불변(scale-invariant)이다. 벡터의 크기가 아니라 방향만 본다. 단어 빈도에 덜 민감해 이상치에 강하고, 정해진 범위 안에 있어 직관적으로 해석된다. 고차원에서도 안정적으로 작동한다.

Section 03word2vec과 의미 공간

2013년 Mikolov가 발표한 word2vec의 직관은 놀랍도록 간단하다. 문맥으로 단어를 예측하라. 그 부산물로 임베딩이 태어난다.

혁신적 아이디어는 이렇다. 타깃 단어 w 근처에 어떤 단어 c가 나타날지 예측하도록 신경망을 학습시키면, 학습된 신경망의 가중치 자체가 임베딩 벡터가 된다. 이것은 라벨을 사람이 달지 않는 자기지도학습(self-supervised)이다 — 라벨은 텍스트 안에 암묵적으로 들어있다.

word2vec은 이 문제를 두 가지로 단순화한다. 작업을 이진 분류로 바꾸고("c는 w의 문맥에 있는가? 예/아니오"), 로지스틱 회귀를 쓴다. 문맥 윈도우를 슬라이드하며 주변 단어를 양성 예시로, 무작위로 뽑은 단어를 음성 예시로 삼는다.

word2vec — 문맥 확률
P(+ | w, c) = σ( c · w ) = 1 / ( 1 + e−c·w )
c · w 문맥 단어와 타깃 단어 임베딩의 내적 · σ 시그모이드 함수 — 내적을 0~1 확률로 변환. 두 임베딩이 유사할수록 함께 나타날 확률이 높다.
Motion · word2vec 문맥 윈도우 STEP 01 / 5
space 재생 · → 다음 · R 리셋

임베딩이 품은 성질

학습된 임베딩은 놀라운 성질을 보인다. 가장 유명한 것이 유추(analogy)다. 평행사변형 모델은 king : queen :: man : ? 같은 관계를 벡터 연산 a − a* + b로 풀어낸다. 또한 임베딩 벡터는 한 단어의 여러 의미를 가중 합(선형 중첩)으로 담는다 — "apple"은 과일이면서 회사다.

함정 word2vec의 유사성은 "같은 문맥에 나타남"으로 정의된다. 그래서 모델은 동의어와 반의어를 구별하지 못한다 — "good"과 "bad"는 비슷한 문맥에 쓰이므로 가깝게 배치된다. 또한 드물게 나타나는 엔티티에는 성능이 크게 떨어진다.

Section 04순서를 기억하는 신경망

표현을 얻었으니 이제 텍스트의 순차적 성질을 처리할 모델이 필요하다. 고전적 피드포워드 신경망의 치명적 약점은 메모리가 없다는 것이다. 시간 t의 단어는 t-1의 단어에 의존하는데, 피드포워드 망은 입력이 독립적이라고 가정한다.

RNN(Recurrent Neural Network)은 이전 입력의 정보를 저장하는 숨겨진 상태(hidden state)를 유지한다. 시퀀스의 모든 요소에 같은 연산을 반복(recurrent)하며, 그 연산의 기억을 다음 단계로 넘긴다.

RNN — 한 시간 단계의 연산
# 이전 숨겨진 상태 h(t-1)를 현재 입력에 결합
a(t) = b + U·h(t-1) + W·x(t)
h(t) = tanh( a(t) )            # 새 숨겨진 상태
o(t) = c + V·h(t)              # 출력 계산
y(t) = σ( o(t) )               # 비선형 변환
함정 — 소실 그래디언트 RNN은 이론상 무한 시퀀스를 학습할 수 있지만, 실제로는 몇 시간 단계만 지나면 초기 입력을 잊는다. 학습 중 그래디언트가 최종 셀에서 초기 셀로 전파될 때 지수적으로 축소되어 0에 가까워지는 소실 그래디언트(vanishing gradient) 문제 때문이다. 반대로 폭발하는 그래디언트도 발생한다.

LSTM과 GRU — 게이트로 기억을 제어하다

LSTM(Long Short-Term Memory)은 "중요하지 않은 정보는 잊고 중요한 맥락만 유지하자"는 발상이다. 두 개의 상태를 둔다 — 단기 메모리 h와 장기 메모리(맥락) c. 그리고 정보 흐름을 제어하는 게이트(gate)를 둔다. 게이트는 시그모이드로 0~1 값을 만들고 하다마드 곱으로 작동하는 이진 밸브다.

  • 잊음 게이트(forget gate) — 더 이상 필요 없는 맥락 정보를 버린다.
  • 입력 게이트(input gate) — 새 정보를 얼마나 맥락에 추가할지 결정한다.
  • 출력 게이트(output gate) — 최종 출력과 다음 숨겨진 상태를 만든다.

GRU(Gated Recurrent Unit)는 LSTM과 매우 비슷하지만 더 간단하고 가볍다. 잊음 게이트는 업데이트 게이트로, 입력 게이트는 리셋 게이트로 통합된다. 매개변수가 적어 더 빠르게 수렴하면서 비슷한 성능을 낸다.

순환 신경망 계열 비교
모델게이트장기 기억특징
RNN없음~7 시간 단계가장 단순, 소실 그래디언트에 취약
LSTM3개 (잊음·입력·출력)~100 시간 단계맥락 셀 분리, 장기 의존성에 강함
GRU2개 (업데이트·리셋)중간~장기매개변수 적음, 빠른 수렴, 과적합에 취약
1D CNN해당 없음윈도우 크기 제한국소 패턴 추출, 매우 빠름, 병렬화 용이

텍스트용 CNN도 빼놓을 수 없다. 합성곱 신경망은 원래 이미지의 국소 패턴을 잡으려 설계됐지만, 1차원 필터를 시퀀스 위로 슬라이드하면 텍스트에도 쓸 수 있다. 3~7개 단어 크기의 커널이 단어들 사이의 패턴을 학습하고, 최대 풀링으로 가장 중요한 특징을 추출한다. 매우 빠르고 병렬화하기 쉽다.

Section 05종단간 감정 분석

이제 모든 조각을 모은다. 텍스트를 수치 표현으로 바꾸는 법(임베딩)과 그 표현을 처리할 모델(RNN 계열)을 가졌으니, 둘을 결합해 종단간(end-to-end) 시스템을 만들 수 있다.

예제는 영화 리뷰 감정 분석이다. 50,000개의 긍정·부정 리뷰(평균 230단어)를 받아 리뷰가 긍정인지 부정인지 분류한다.

  1. 전처리 — 특수 문자·구두점·과도한 공백 제거.
  2. 토큰화 — 불용어(stopwords)와 한 글자 단어 제거, 상위 1,000개 단어만 유지.
  3. 벡터화 — 어휘에 따라 단어를 인덱스로 변환.
  4. 패딩 — 길이가 다른 리뷰를 고정 길이로 맞춤.
감정 분류 — 임베딩 + GRU 학습 루프
# 하이퍼파라미터
embedding_dim = 300;  hidden_dim = 256;  no_layers = 3
model     = SentimentRNN(no_layers, vocab_size, hidden_dim,
                         embedding_dim, drop_prob=0.5)
criterion = nn.BCELoss()                  # 이진 교차 엔트로피
optimizer = optim.Adam(model.parameters(), lr=0.001)

for epoch in range(epochs):
    for inputs, labels in train_loader:
        model.zero_grad()
        output, h = model(inputs, h)
        loss = criterion(output.squeeze(), labels.float())
        loss.backward()                   # 역전파
        optimizer.step()
Motion · RNN의 시퀀스 처리 STEP 01 / 6
space 재생 · → 다음 · R 리셋
핵심 이 장이 알려준 것 — 텍스트는 점점 정교한 벡터로 변환된다(원-핫 → BoW → TF-IDF → 임베딩). 임베딩은 단어 간 의미를 보존하고, RNN·LSTM·GRU·CNN은 시퀀스를 분석한다. 이 둘을 잇는 종단간 학습이 곧 딥러닝의 힘이다. 다음 장에서는 RNN의 한계를 정면으로 무너뜨린 트랜스포머를 만난다.