Section 01표현의 문제
인간이 가진 능력 중 가장 놀라운 것은 언어다. 그러나 자연 상태의 언어는 기계가 이해할 수 없다. 이 책의 모든 이야기는 바로 이 간극에서 시작된다.
텍스트를 컴퓨터가 다룰 수 있는 형태로 바꾸는 일은 이미지나 표(table)를 다루는 것보다 훨씬 어렵다. 단어의 의미(signified)와 그 의미를 가리키는 기호(signifier) 사이에는 어떤 고유한 관계도 없기 때문이다. "restaurant"라는 글자 배열 자체에는 음식점이라는 개념이 들어있지 않다. 게다가 단어의 의미는 문맥과 화자의 의도에 따라 끊임없이 변한다.
따라서 첫 단계는 텍스트를 기본 단위로 쪼개는 일이다. 이 일련의 처리를 통틀어 텍스트 정규화(text normalization)라 부른다. 문장을 공백 기준으로 단어로 나누고(text segmentation), 구두점도 하나의 단위로 취급하며("He"와 "he"를 같은 인스턴스로 보기 위해) 소문자로 통일하고, 필요하면 표제어 추출(lemmatization)이나 어간 추출(stemming)을 수행한다. 텍스트를 기본 단위로 변환하는 이 작업이 바로 토큰화(tokenization)다.
원-핫 인코딩 — 가장 단순한 시작
전통적 NLP는 텍스트를 이산 기호(discrete symbols)로 표현한다. 가장 단순한 예가 원-핫 인코딩이다. 코퍼스 전체의 서로 다른 단어 집합을 어휘(vocabulary)라 하고, 각 단어를 어휘 크기만큼 긴 벡터로 나타낸다. 해당 단어의 인덱스에만 1, 나머지는 0이다.
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
백 오브 워즈와 TF-IDF
백 오브 워즈(Bag-of-Words, BoW)는 단어의 위치를 무시하고 빈도만 보존한다. "백(bag)"이라는 이름은 순서 정보를 버린다는 사실에서 나왔다. 문서 하나가 하나의 벡터가 되고, 각 차원은 어휘 단어의 등장 횟수다.
그런데 원시 빈도는 편향되어 있다. "good"이나 "bad"는 어디서나 자주 쓰여 변별력이 낮고, 드물게 나타나는 단어가 오히려 정보가 많다. TF-IDF는 이 직관을 수식으로 옮긴다. 단어 빈도(TF)에 역문서빈도(IDF)의 로그를 곱해, 특정 문서에만 나타나는 단어에 더 큰 가중치를 준다.
Section 02희소에서 밀집으로
희소 벡터의 모든 문제는 한 가지 원인에서 비롯된다. 의미를 국소적으로(local) 한 자리에만 저장한다는 것. 우리가 원하는 것은 의미를 분산 표현(distributed representation)으로, 크기가 작고 실수로 채워진 밀집 벡터(dense vector)에 담는 것이다.
이 발상의 토대는 언어학의 분포 가설(Distributional Hypothesis)이다. "유사한 의미를 가진 단어는 비슷한 문맥에서 자주 함께 나타난다." 같은 맥락에 등장하는 단어는 같은 의미장(意味場)을 공유한다.
단어의 의미를 인코딩하는, 크기가 제한된(보통 100~1024차원) 실수 밀집 벡터. 어휘가 커져도 벡터 크기는 늘지 않는다.
희소 벡터와 달리 임베딩 벡터는 기하학적 성질을 갖는다. 두 벡터의 거리가 곧 두 단어의 의미적 유사성이며, 따라서 클러스터링·유추(analogy)·검색 같은 연산이 가능하다.
유사성을 어떻게 잴까
벡터를 얻었으면 둘 사이의 유사성을 계산할 방법이 필요하다. 가장 단순한 후보는 내적(dot product)이다. 두 벡터가 같은 차원에서 큰 값을 가질 때 내적이 커진다. 그러나 내적은 긴 벡터, 높은 값(흔히 쓸모없는 고빈도 단어)을 선호하고, 값의 범위에 제한이 없다.
그래서 정규화된 내적, 즉 두 벡터가 이루는 각도의 코사인을 쓴다. 이것이 코사인 유사성(Cosine Similarity)이다.
Section 03word2vec과 의미 공간
2013년 Mikolov가 발표한 word2vec의 직관은 놀랍도록 간단하다. 문맥으로 단어를 예측하라. 그 부산물로 임베딩이 태어난다.
혁신적 아이디어는 이렇다. 타깃 단어 w 근처에 어떤 단어 c가 나타날지 예측하도록 신경망을 학습시키면, 학습된 신경망의 가중치 자체가 임베딩 벡터가 된다. 이것은 라벨을 사람이 달지 않는 자기지도학습(self-supervised)이다 — 라벨은 텍스트 안에 암묵적으로 들어있다.
word2vec은 이 문제를 두 가지로 단순화한다. 작업을 이진 분류로 바꾸고("c는 w의 문맥에 있는가? 예/아니오"), 로지스틱 회귀를 쓴다. 문맥 윈도우를 슬라이드하며 주변 단어를 양성 예시로, 무작위로 뽑은 단어를 음성 예시로 삼는다.
임베딩이 품은 성질
학습된 임베딩은 놀라운 성질을 보인다. 가장 유명한 것이 유추(analogy)다. 평행사변형 모델은 king : queen :: man : ? 같은 관계를 벡터 연산 a − a* + b로 풀어낸다. 또한 임베딩 벡터는 한 단어의 여러 의미를 가중 합(선형 중첩)으로 담는다 — "apple"은 과일이면서 회사다.
Section 04순서를 기억하는 신경망
표현을 얻었으니 이제 텍스트의 순차적 성질을 처리할 모델이 필요하다. 고전적 피드포워드 신경망의 치명적 약점은 메모리가 없다는 것이다. 시간 t의 단어는 t-1의 단어에 의존하는데, 피드포워드 망은 입력이 독립적이라고 가정한다.
RNN(Recurrent Neural Network)은 이전 입력의 정보를 저장하는 숨겨진 상태(hidden state)를 유지한다. 시퀀스의 모든 요소에 같은 연산을 반복(recurrent)하며, 그 연산의 기억을 다음 단계로 넘긴다.
# 이전 숨겨진 상태 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) ) # 비선형 변환
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 시간 단계 | 가장 단순, 소실 그래디언트에 취약 |
| LSTM | 3개 (잊음·입력·출력) | ~100 시간 단계 | 맥락 셀 분리, 장기 의존성에 강함 |
| GRU | 2개 (업데이트·리셋) | 중간~장기 | 매개변수 적음, 빠른 수렴, 과적합에 취약 |
| 1D CNN | 해당 없음 | 윈도우 크기 제한 | 국소 패턴 추출, 매우 빠름, 병렬화 용이 |
텍스트용 CNN도 빼놓을 수 없다. 합성곱 신경망은 원래 이미지의 국소 패턴을 잡으려 설계됐지만, 1차원 필터를 시퀀스 위로 슬라이드하면 텍스트에도 쓸 수 있다. 3~7개 단어 크기의 커널이 단어들 사이의 패턴을 학습하고, 최대 풀링으로 가장 중요한 특징을 추출한다. 매우 빠르고 병렬화하기 쉽다.
Section 05종단간 감정 분석
이제 모든 조각을 모은다. 텍스트를 수치 표현으로 바꾸는 법(임베딩)과 그 표현을 처리할 모델(RNN 계열)을 가졌으니, 둘을 결합해 종단간(end-to-end) 시스템을 만들 수 있다.
예제는 영화 리뷰 감정 분석이다. 50,000개의 긍정·부정 리뷰(평균 230단어)를 받아 리뷰가 긍정인지 부정인지 분류한다.
- 전처리 — 특수 문자·구두점·과도한 공백 제거.
- 토큰화 — 불용어(stopwords)와 한 글자 단어 제거, 상위 1,000개 단어만 유지.
- 벡터화 — 어휘에 따라 단어를 인덱스로 변환.
- 패딩 — 길이가 다른 리뷰를 고정 길이로 맞춤.
# 하이퍼파라미터 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()