17장 딥러닝을 이용한 자연어 처리¶
1. 텍스트의 토큰화¶
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,Flatten,Embedding
from tensorflow.keras.utils import to_categorical
from numpy import array
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
# 케라스의 텍스트 전처리와 관련한 함수 중 text_to_word_sequence 함수를 불러옵니다.
from tensorflow.keras.preprocessing.text import text_to_word_sequence
# 전처리할 텍스트를 정합니다.
text = '해보지 않으면 해낼 수 없다'
# 해당 텍스트를 토큰화합니다.
result = text_to_word_sequence(text)
print("\n원문:\n", text)
print("\n토큰화:\n", result)
원문: 해보지 않으면 해낼 수 없다 토큰화: ['해보지', '않으면', '해낼', '수', '없다']
# 단어 빈도수 세기
# 전처리하려는 세 개의 문장을 정합니다.
docs = ['먼저 텍스트의 각 단어를 나누어 토큰화합니다.',
'텍스트의 단어로 토큰화해야 딥러닝에서 인식됩니다.',
'토큰화한 결과는 딥러닝에서 사용할 수 있습니다.',
]
# 토큰화 함수를 이용해 전처리 하는 과정입니다.
token = Tokenizer() # 토큰화 함수 지정
token.fit_on_texts(docs) # 토큰화 함수에 문장 적용
# 단어의 빈도수를 계산한 결과를 각 옵션에 맞추어 출력합니다.
# Tokenizer()의 word_counts 함수는 순서를 기억하는 OrderedDict 클래스를 사용합니다.
print("\n단어 카운트:\n", token.word_counts)
# 출력되는 순서는 랜덤입니다.
print("\n문장 카운트: ", token.document_count)
print("\n각 단어가 몇 개의 문장에 포함되어 있는가:\n", token.word_docs)
print("\n각 단어에 매겨진 인덱스 값:\n", token.word_index)
단어 카운트:
OrderedDict({'먼저': 1, '텍스트의': 2, '각': 1, '단어를': 1, '나누어': 1, '토큰화합니다': 1, '단어로': 1, '토큰화해야': 1, '딥러닝에서': 2, '인식됩니다': 1, '토큰화한': 1, '결과는': 1, '사용할': 1, '수': 1, '있습니다': 1})
문장 카운트: 3
각 단어가 몇 개의 문장에 포함되어 있는가:
defaultdict(<class 'int'>, {'토큰화합니다': 1, '단어를': 1, '텍스트의': 2, '각': 1, '나누어': 1, '먼저': 1, '토큰화해야': 1, '딥러닝에서': 2, '단어로': 1, '인식됩니다': 1, '있습니다': 1, '사용할': 1, '수': 1, '결과는': 1, '토큰화한': 1})
각 단어에 매겨진 인덱스 값:
{'텍스트의': 1, '딥러닝에서': 2, '먼저': 3, '각': 4, '단어를': 5, '나누어': 6, '토큰화합니다': 7, '단어로': 8, '토큰화해야': 9, '인식됩니다': 10, '토큰화한': 11, '결과는': 12, '사용할': 13, '수': 14, '있습니다': 15}
2. 단어의 원-핫 인코딩¶
text="오랫동안 꿈꾸는 이는 그 꿈을 닮아간다"
token = Tokenizer()
token.fit_on_texts([text])
print(token.word_index)
{'오랫동안': 1, '꿈꾸는': 2, '이는': 3, '그': 4, '꿈을': 5, '닮아간다': 6}
x=token.texts_to_sequences([text])
print(x)
[[1, 2, 3, 4, 5, 6]]
# 인덱스 수에 하나를 추가해서 원-핫 인코딩 배열 만들기
word_size = len(token.word_index) + 1
x = to_categorical(x, num_classes=word_size)
print(x)
[[[0. 1. 0. 0. 0. 0. 0.] [0. 0. 1. 0. 0. 0. 0.] [0. 0. 0. 1. 0. 0. 0.] [0. 0. 0. 0. 1. 0. 0.] [0. 0. 0. 0. 0. 1. 0.] [0. 0. 0. 0. 0. 0. 1.]]]
4.텍스트를 읽고 긍정, 부정 예측하기¶
# 텍스트 리뷰 자료를 지정합니다.
docs = ["너무 재밌네요","최고예요","참 잘 만든 영화예요","추천하고 싶은 영화입니다","한번 더 보고싶네요","글쎄요","별로예요","생각보다 지루하네요","연기가 어색해요","재미없어요"]
# 긍정 리뷰는 1, 부정 리뷰는 0으로 클래스를 지정합니다.
classes = array([1,1,1,1,1,0,0,0,0,0])
# 토큰화
token = Tokenizer()
token.fit_on_texts(docs)
print(token.word_index)
{'너무': 1, '재밌네요': 2, '최고예요': 3, '참': 4, '잘': 5, '만든': 6, '영화예요': 7, '추천하고': 8, '싶은': 9, '영화입니다': 10, '한번': 11, '더': 12, '보고싶네요': 13, '글쎄요': 14, '별로예요': 15, '생각보다': 16, '지루하네요': 17, '연기가': 18, '어색해요': 19, '재미없어요': 20}
x = token.texts_to_sequences(docs)
print("\n리뷰 텍스트, 토큰화 결과:\n", x)
리뷰 텍스트, 토큰화 결과: [[1, 2], [3], [4, 5, 6, 7], [8, 9, 10], [11, 12, 13], [14], [15], [16, 17], [18, 19], [20]]
# 패딩, 서로 다른 길이의 데이터를 4로 맞추어 줍니다.
padded_x = pad_sequences(x, 4)
print("\n패딩 결과:\n", padded_x)
패딩 결과: [[ 0 0 1 2] [ 0 0 0 3] [ 4 5 6 7] [ 0 8 9 10] [ 0 11 12 13] [ 0 0 0 14] [ 0 0 0 15] [ 0 0 16 17] [ 0 0 18 19] [ 0 0 0 20]]
# 임베딩에 입력될 단어의 수를 지정합니다.
word_size = len(token.word_index) +1
# 단어 임베딩을 포함하여 딥러닝 모델을 만들고 결과를 출력합니다.
model = Sequential()
model.add(Embedding(word_size, 8, input_length=4, name="word_embedding"))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.summary()
/usr/local/lib/python3.12/dist-packages/keras/src/layers/core/embedding.py:97: UserWarning: Argument `input_length` is deprecated. Just remove it. warnings.warn(
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ word_embedding (Embedding) │ ? │ 0 (unbuilt) │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ flatten (Flatten) │ ? │ 0 (unbuilt) │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (Dense) │ ? │ 0 (unbuilt) │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 0 (0.00 B)
Trainable params: 0 (0.00 B)
Non-trainable params: 0 (0.00 B)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(padded_x, classes, epochs=20)
print("\n Accuracy: %.4f" % (model.evaluate(padded_x, classes)[1]))
Epoch 1/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 1s 1s/step - accuracy: 0.4000 - loss: 0.6999 Epoch 2/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 112ms/step - accuracy: 0.4000 - loss: 0.6978 Epoch 3/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 73ms/step - accuracy: 0.5000 - loss: 0.6957 Epoch 4/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 142ms/step - accuracy: 0.5000 - loss: 0.6937 Epoch 5/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 80ms/step - accuracy: 0.5000 - loss: 0.6917 Epoch 6/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 135ms/step - accuracy: 0.6000 - loss: 0.6896 Epoch 7/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 137ms/step - accuracy: 0.6000 - loss: 0.6876 Epoch 8/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 138ms/step - accuracy: 0.7000 - loss: 0.6856 Epoch 9/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 146ms/step - accuracy: 0.7000 - loss: 0.6836 Epoch 10/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 82ms/step - accuracy: 0.7000 - loss: 0.6816 Epoch 11/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 80ms/step - accuracy: 0.7000 - loss: 0.6796 Epoch 12/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 98ms/step - accuracy: 1.0000 - loss: 0.6776 Epoch 13/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 68ms/step - accuracy: 1.0000 - loss: 0.6756 Epoch 14/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 146ms/step - accuracy: 1.0000 - loss: 0.6736 Epoch 15/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 74ms/step - accuracy: 1.0000 - loss: 0.6716 Epoch 16/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 80ms/step - accuracy: 1.0000 - loss: 0.6696 Epoch 17/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 75ms/step - accuracy: 1.0000 - loss: 0.6676 Epoch 18/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 76ms/step - accuracy: 1.0000 - loss: 0.6656 Epoch 19/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 65ms/step - accuracy: 1.0000 - loss: 0.6636 Epoch 20/20 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 48ms/step - accuracy: 1.0000 - loss: 0.6616 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 184ms/step - accuracy: 1.0000 - loss: 0.6596 Accuracy: 1.0000
임베딩이란?¶
🧠 1. 단어 임베딩이란?
단어를 ‘숫자 벡터’로 표현하는 방법입니다.
컴퓨터는 문자(글자) 를 직접 이해하지 못하고, 숫자만 계산할 수 있죠. 그래서 문장을 신경망에 넣으려면, 단어 → 숫자 벡터로 변환해야 합니다.
예를 들어, "강아지", "고양이", "자동차" 이런 단어들을 신경망이 이해하려면 각 단어를 숫자 형태로 표현해야 하는데, 단순히 "강아지"=1, "고양이"=2, "자동차"=3 처럼 정수 인덱스로만 표현하면 단어 간의 의미 관계를 전혀 표현할 수 없습니다.
📉 2. 원-핫 인코딩의 한계 초기에는 원-핫 인코딩(one-hot encoding) 을 많이 썼습니다.
| 단어 | 벡터 (단어 사전 크기=5) |
|---|---|
| 강아지 | [1,0,0,0,0] |
| 고양이 | [0,1,0,0,0] |
| 자동차 | [0,0,1,0,0] |
하지만 문제점이 있죠 👇
- 단어 간 유사도 없음 → “강아지”와 “고양이”는 둘 다 동물이지만, 벡터 상으로 완전히 달라요. (내적 = 0, 즉 전혀 관련 없다고 나옴)
- 차원 폭발(Dimension Explosion) → 단어가 10,000개면, 각 벡터 길이도 10,000차원이 되어 메모리 낭비가 심함.
🚀 3. 임베딩(Embedding)의 핵심 아이디어 그래서 단어를 더 작은 실수 벡터 공간에 “압축”하여 표현합니다.
예를 들어 8차원 공간으로 임베딩하면:
| 단어 | 8차원 임베딩 벡터(예시) |
|---|---|
| 강아지 | [0.7, 0.2, -0.1, 0.3, …] |
| 고양이 | [0.6, 0.3, -0.2, 0.4, …] |
| 자동차 | [-0.1, 0.8, 0.7, -0.5, …] |
이 벡터들은 훈련을 통해 자동으로 학습됩니다. 즉, 단어의 의미적 관계를 반영하도록 조정되죠.
👉 예를 들어 학습이 잘 되면 이런 관계를 보입니다:
벡터(“왕”) - 벡터(“남자”) + 벡터(“여자”) ≈ 벡터(“여왕”)
이건 실제로 Word2Vec 실험에서 발견된 유명한 예시입니다.
⚙️ 4. 코드에서 임베딩이 하는 일
model = Sequential()
model.add(Embedding(word_size, 8, input_length=4))
이 한 줄이 바로 단어 임베딩층(Embedding Layer) 입니다.
| 매개변수 | 의미 |
|---|---|
| word_size | 단어 사전 크기 (예: 단어 개수 + 1) |
| 8 | 임베딩 차원 (단어 하나를 8차원 벡터로 표현하겠다는 뜻) |
| input_length=4 | 입력 문장의 최대 길이 (패딩된 길이) |
즉,
• 입력: [2, 3, 5, 1] (토큰화된 문장)
• 출력: 4개의 단어 각각을 8차원 벡터로 바꿔서 (4, 8) 크기의 행렬로 반환
이 벡터는 그 다음 Flatten()이나 LSTM, CNN 등에 입력됩니다.
📊 5. 학습 방식 Embedding 층은 가중치 행렬 W(크기: [단어 수, 임베딩 차원])를 학습합니다.
학습 과정에서 • 문장에서 단어가 함께 등장하는 패턴(문맥)을 학습하며 • 비슷한 문맥에 자주 등장하는 단어들이 비슷한 벡터를 갖게 됩니다.
즉, 모델이 “단어의 의미”를 벡터로 학습하는 셈이죠.
💬 6. 쉽게 말하면
|표현 방식|비유| |원-핫 인코딩|모든 단어를 “서로 완전히 다른 상자”에 넣는 방식| |임베딩|단어들을 “의미의 위치가 비슷한 좌표 공간”에 배치하는 방식|
예를 들어, 2차원으로 시각화하면 이렇게 됩니다:
고양이 ●
강아지 ●
자동차 ●
비행기 ●
비슷한 의미의 단어는 가까운 위치에 모여 있죠.
🧩 7. 다른 임베딩 방법들
| 방법 | 설명 |
|---|---|
| Word2Vec | 주변 단어를 예측하는 방식(Skip-gram / CBOW)으로 임베딩 학습 |
| GloVe | 단어-단어 동시 등장 행렬을 이용해 통계적으로 학습 |
| FastText | 단어 내부의 형태소(문자 n-gram)까지 고려 |
| BERT 임베딩 | 문맥에 따라 단어의 의미가 달라지는 “문맥적 임베딩” (최신 방식) |
임베딩이 어떻게 실제로 학습되는가¶
🔹 1. 임베딩 층 내부 구조
임베딩 층을 예로 들어봅시다 👇
embedding = Embedding(input_dim=1000, output_dim=8, input_length=4)
이때 내부에는 다음과 같은 가중치 행렬(lookup table) 이 있습니다.
W = \begin{bmatrix} \text{— 벡터(단어 0) —} \\ \text{— 벡터(단어 1) —} \\ \text{— 벡터(단어 2) —} \\ \vdots \\ \text{— 벡터(단어 999) —} \end{bmatrix}
즉, W의 크기는 [1000, 8] (단어 개수 × 임베딩 차원) • 각 행(row) 은 하나의 단어를 나타내는 8차원 벡터입니다. • 모델이 학습을 반복하면서, 이 행들(단어 벡터)을 미세하게 조정해 나갑니다.
🔹 2. 입력과 출력의 관계
예를 들어, 입력 문장이 토큰화되어 [4, 15, 82, 6] 이라고 합시다. (input_length=4)
임베딩 층은 이 인덱스들을 W 행렬에서 그대로 뽑습니다.
= [ W[4], W[15], W[82], W[6] ]
즉, 단어 인덱스를 “행 번호”로 쓰는 lookup operation이에요. 이 단계에서는 곱셈이나 덧셈이 없습니다 — 단순히 W 행렬에서 해당 행을 가져오는 것뿐입니다.
🔹 3. 그럼 “학습”은 언제 일어나나?
핵심은 역전파(backpropagation) 입니다. 임베딩 층은 다른 층들과 함께 오차(손실) 를 전달받고, 그 오차로부터 임베딩 행렬의 해당 단어 행(row) 들만 업데이트합니다.
예시로 풀어볼게요 🔍
1. 순전파(forward)
• 문장: "좋아요" → [2]
• Embedding에서 W[2]을 꺼냄 → [-0.1, 0.4, 0.7, …]
• 이 벡터가 Dense 층 등으로 흘러가 예측 수행.
2. 역전파(backpropagation)
• 모델이 예측을 잘못함 → 손실(loss) 계산.
• 그 손실로부터 “W[2]의 어떤 방향으로 움직여야 하는지”의 gradient (∂L/∂W[2]) 가 계산됨.
• 옵티마이저(Adam 등)가 W[2]를 조금 수정
• 즉, “좋아요”라는 단어의 의미 벡터가 조금씩 갱신되는 거예요.
3. 반복 학습으로 벡터 의미가 형성됨
• "좋아요"가 “긍정” 문장에서 자주 등장한다면,
"최고예요", "추천합니다" 등과 비슷한 문맥의 gradient 방향을 공유하게 됩니다.
• 결과적으로 이 단어들의 임베딩 벡터가 서로 가까워지게 됩니다.
임베딩 행렬 W (단어 × 벡터차원)
┌───────────────────────────────┐
│ 단어0 : [ 0.2, 0.1, -0.3 ] │
│ 단어1 : [-0.1, 0.7, 0.5 ] │
│ 단어2 : [ 0.6, -0.4, 0.9 ] │ ← “좋아요”
│ 단어3 : [ 0.0, 0.2, -0.2 ] │
│ 단어4 : [ ... ] │
└───────────────────────────────┘
↑
입력 인덱스 2 → forward
↓
손실 역전파 → 해당 행만 업데이트
임베딩에서 손실은 어떻게 구해지느냐?
🔹 1. 임베딩 자체는 “손실을 직접 계산하지 않음”
Embedding 층은 단지 입력(단어 인덱스)을 벡터로 바꾸는 역할만 합니다. 즉, 손실을 계산하는 건 임베딩이 아니라 그 뒤에 있는 모델 전체입니다.
예를 들어, 이런 모델을 보세요 👇
model = Sequential()
model.add(Embedding(word_size, 8, input_length=4))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy')
여기서 손실 함수는 binary_crossentropy 즉, 이진 분류 손실이에요.
🔹 2. 임베딩이 손실과 연결되는 방식
손실은 모델 출력단(Dense) 에서 계산되지만, 역전파(backpropagation)로 인해 그 손실의 기울기가 임베딩 층까지 전파됩니다.
즉:
손실 L
↑
Dense 가중치
↑
Flatten
↑
Embedding 행렬 (단어 벡터)
손실의 미분값이 임베딩 행렬까지 전해져서 “이 단어 벡터를 어떤 방향으로 바꿔야 손실이 줄어드는가”를 알려줍니다.
그래서 임베딩 행렬의 특정 단어 행(row)이 업데이트되는 거예요.
🔹 3. 손실 함수의 종류는 모델 목적에 따라 다름
| 목적 | 출력층 | 손실 함수 | 설명 |
|---|---|---|---|
| 이진 분류 (긍정/부정) | sigmoid | binary_crossentropy | 두 클래스 구분 |
| 다중 분류 (감정 5종류 등) | softmax | categorical_crossentropy | 여러 클래스 중 하나 |
| 언어 모델링 (다음 단어 예측) | softmax | sparse_categorical_crossentropy | 다음 단어 확률 예측 |
| Word2Vec (skip-gram) | 없음(자체 구조) | negative sampling loss | 단어 간 유사도 학습 |
| 번역 등 seq2seq | softmax | categorical_crossentropy | 전체 시퀀스 예측 |
요약하자면 👇
| 구분 | 설명 |
|---|---|
| 손실 계산 위치 | 모델 출력층 (Dense) |
| 손실 함수 | binary_crossentropy |
| 임베딩 역할 | 입력 단어를 벡터로 바꿔 오차 역전파 받을 준비 |
| 학습 방식 | 손실이 역전파되어 임베딩 행렬의 해당 단어 벡터만 업데이트 |
| 결과 | 비슷한 역할을 하는 단어들이 비슷한 방향의 벡터로 수렴 |