[Text Representation-2] NNLM 코드 구현
본 포스팅은 Taehyoung Kim님의 블로그 내용을 기반으로 작성한 것임을 미리 밝힙니다.
NNLM
Import Library
기본 라이브러리들을 import 하는데, 먼저 pytorch의 neural network와 weight update를 위한 optimizer를 가져온다.
import torch
import torch.nn as nn
import torch.optim as optim
# fig 한글 설치
# !sudo apt-get install -y fonts-nanum
# !sudo fc-cache -fv
# !rm ~/.cache/matplotlib -rf
우선 데이터를 준비해야 한다. 이 때, 데이터가 정확히 어떤 형식으로 준비되어야 하는지 알지 못한다면 모델이나 함수를 구현할 때 헷갈리는 경우가 많다.
직관적인 이해를 위해 오픈 데이터셋을 사용하지 않고, 간단한 몇 개의 문장으로 데이터를 준비해본다.
우선, NNLM은 n-gram을 기반으로 하기 때문에 n을 정해보자. 대부분 n-gram에서 n=3으로 하는 tri-gram을 많이 사용하기 때문에, 3개의 어절로 구성된 문장들을 준비한다.
corpus = ['비정형데이터 수업 시작이다', '나는 오늘도 이뻐', '코딩은 너무 어렵잖아', 'NLP 이러지마 제발', '우리 열심히 해봐요']
Tokenization (토큰화)
다음으로, 문장으로 구성된 데이터 셋을 단어 단위로 쪼개는 Tokenization(토큰화) 과정이 필요하다.
이번에 준비한 corpus에선 띄어쓰기를 기준으로 아주 간단하게 토큰화를 진행할 수 있지만, 실제 대용량 데이터를 다룰 땐 중복단어, 특수문자, 띄어쓰기, 불필요한 단어, 오타 등을 처리하기 위해 다양한 전처리 과정들을 반드시 진행해야 한다.
전처리까지 다루기엔 너무 복잡하니 띄어쓰기 기준으로 간단히 단어를 나누고, 중복되는 단어만 제거하는 토큰화를 진행해보자.
tokens = " ".join(corpus).split() # " "(띄어쓰기)를 기준으로 corpus 안의 데이터를 split(분리)해줍니다.
tokens = list(set(tokens))
print(tokens[:5])
['어렵잖아', '오늘도', '나는', '제발', '코딩은']
위와 같이 띄어쓰기를 기준으로 단어들을 구분한 뒤, 단어들을 모두 모아보았다. corpus가 준비되었으니, 각 단어의 인덱스를 만들어주자. 인덱싱은 dictionary를 활용해 단어를 알 때 인덱스를 찾거나, 인덱스를 알 때 단어를 찾을 수 있도록 두 가지를 만들어 준다.
word_dict = {w: i for i, w in enumerate(tokens)} # word -> index
index_dict = {i: w for i, w in enumerate(tokens)} # index -> word
word_dict
{'어렵잖아': 0,
'오늘도': 1,
'나는': 2,
'제발': 3,
'코딩은': 4,
'해봐요': 5,
'우리': 6,
'NLP': 7,
'너무': 8,
'이러지마': 9,
'이뻐': 10,
'시작이다': 11,
'수업': 12,
'열심히': 13,
'비정형데이터': 14}
index_dict
{0: '어렵잖아',
1: '오늘도',
2: '나는',
3: '제발',
4: '코딩은',
5: '해봐요',
6: '우리',
7: 'NLP',
8: '너무',
9: '이러지마',
10: '이뻐',
11: '시작이다',
12: '수업',
13: '열심히',
14: '비정형데이터'}
Input / Output 나누기
다음으로 input과 target을 나눠주자. 우리의 목적은 단어 2개를 참고해 그 다음에 올 단어를 예측하는 것이다.(tri-gram) 각 단어들을 추출한 후 추후 Embedding을 진행하기 위해 미리 word_dict를 이용하여 인덱스로 변환해주도록 하자. 마지막으로 모델에 들어가는 input형태는 tensor 타입으로 들어가기 때문에 최종 출력을 tensor 타입으로 변화해준다.
def make_input_target():
input_batch = []
target_batch = []
for sentence in corpus:
word = sentence.split() # 띄어쓰기 기준으로 단어 구분
input = [word_dict[n] for n in word[:-1]] # 1 ~ n-1 번째 단어를 input으로 사용. n=3이므로 첫 두 단어 사용
target = word_dict[word[-1]] # n번째 단어를 target으로 사용
input_batch.append(input)
target_batch.append(target)
return torch.LongTensor(input_batch), torch.LongTensor(target_batch)
input과 target이 인덱스를 기준으로 잘 정리 되었는지 확인해보자.
input_batch, target_batch = make_input_target()
print('Input : ', input_batch)
print('Target : ', target_batch)
Input : tensor([[14, 12],
[ 2, 1],
[ 4, 8],
[ 7, 9],
[ 6, 13]])
Target : tensor([11, 10, 0, 3, 5])
Model
targe을 예측하기 위한 output의 수식: $Y_{w_{t}} = b + Wx + U( tanh(Hx_{t}+d))$
V : 총 토큰(단어) 개수
m : 임베딩 벡터 차원 (단어를 몇 차원의 벡터로 임베딩할건지 결정. 예를 들어 m=2이면 2차원의 벡터가 생성됨)
n_hidden : hidden layer의 unit 수
C (Embedding matrix) : 각 인덱스에 있는 단어가 embedding되어 vector 형태로 들어 있음. 차원 = V x m
H : Embedding matrix와 hidden layer 사이의 weight. 차원 = ( (n-1) * m ) x n_hidden
d : hidden layer의 bias. 차원 = n_hidden
U : hidden layer가 activation function (tanh)까지 거친 후와 output 사이의 weight. 차원 = n_hidden x V
W : Embedding layer에서 output까지 직접 연결하기 위한 weight. 차원 = ( (n-1) * m ) x V \
b : 가장 마지막에(out 직전에) 더해주는 bias.
$w_t$: 문장에서 t번째 단어
$x_t$: 문장에서 t번째 단어가 C (Embedding matrix)를 거쳐 임베딩 된 형태. C($w_t$)와 동일
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.C = nn.Embedding(V, m)
self.H = nn.Linear((n-1)*m, n_hidden, bias=False)
self.d = nn.Parameter(torch.ones(n_hidden))
self.U = nn.Linear(n_hidden, V, bias=False)
self.W = nn.Linear((n-1)*m, V, bias=False)
self.b = nn.Parameter(torch.ones(V))
def forward(self, x):
x = self.C(x) # 인덱스 -> embedding vector
x = x.view(-1, (n-1)*m) # (n-1)개의 embedding vector를 unroll 하여 [batch_size x (n-1)*m] 으로 만들어줌
tanh = torch.tanh(self.d + self.H(x)) # [batch_size, n_hidden]
output = self.b + self.W(x) + self.U(tanh)
return output
모델 구축을 완료했으니, 이제 모든 요소들을 포함한 메인 루프를 완성해보자!
Main
if __name__ == "__main__": # #코드를 직접 실행했을때만 작동하고, import 될때는 작동하지 않게 만들어줌
n = 3
n_hidden = 2
m = 2
V = len(tokens)
model = NNLM()
criterion = nn.CrossEntropyLoss() # loss function으로 cross entropy 사용
optimizer = optim.Adam(model.parameters(), lr=0.001) # optimizer로 adam 사용
input_batch, target_batch = make_input_target()
# Training
for epoch in range(10000):
optimizer.zero_grad() # optimizer 초기화
output = model(input_batch) # output 계산
loss = criterion(output, target_batch) # loss 계산
if (epoch+1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch+1), 'cost =', '{:.6f}'.format(loss)) #1000번째 epoch마다 loss 출력
loss.backward() # backpropagation
optimizer.step() # weight update
Epoch: 1000 cost = 0.087228
Epoch: 2000 cost = 0.014579
Epoch: 3000 cost = 0.005196
Epoch: 4000 cost = 0.002333
Epoch: 5000 cost = 0.001162
Epoch: 6000 cost = 0.000612
Epoch: 7000 cost = 0.000333
Epoch: 8000 cost = 0.000184
Epoch: 9000 cost = 0.000103
Epoch: 10000 cost = 0.000059
학습이 잘 되었다!
이제 예측을 살펴보자
# Prediction
predict = model(input_batch).data.max(1, keepdim=True)[1]
for i, j in zip(corpus, predict.view(-1).tolist()):
print(i.split()[:n-1], ' -> ', str(index_dict[j]))
['비정형데이터', '수업'] -> 시작이다
['나는', '오늘도'] -> 이뻐
['코딩은', '너무'] -> 어렵잖아
['NLP', '이러지마'] -> 제발
['우리', '열심히'] -> 해봐요
앞의 두 단어를 이용해 뒤의 단어를 잘 예측한것을 확인할 수 있다.
Embedded word scatter plot
다음은 학습된 Embedding Vector들을 2차원 평면상에 뿌려보았을 때, 서로 관련있는 단어들끼리 뭉치는지 확인해보자.
from matplotlib import pyplot as plt
plt.rc('font', family='NanumBarunGothic') # 한글 출력을 가능하게 만들기
fit, ax = plt.subplots()
ax.scatter(model.C.weight[:, 0].tolist(), model.C.weight[:, 1].tolist())
for i, txt in enumerate(tokens):
ax.annotate(txt, (model.C.weight[i, 0].tolist(), model.C.weight[i, 1].tolist()))
평면상에 표상은 되었지만 큰 연관성은 보이지 않아 보인다. 데이터를 더 학습시킨다면, 조금 더 단어의 유의성을 잘 표상할 수 있지 않을까 싶다.
전체 코드
전체 코드 정리본으로 마무리한다.
### Import Library ###
import torch
import torch.nn as nn
import torch.optim as optim
### Dataset ###
corpus = ['비정형데이터 수업 시작이다', '나는 오늘도 이뻐', '코딩은 너무 어렵잖아', 'NLP 이러지마 제발', '우리 열심히 해봐요']
### preprocessing ###
tokens = " ".join(corpus).split() # " "(띄어쓰기)를 기준으로 corpus 안의 데이터를 split(분리)해줍니다.
tokens = list(set(tokens))
word_dict = {w: i for i, w in enumerate(tokens)} # word -> index
index_dict = {i: w for i, w in enumerate(tokens)} # index -> word
# Making input dataset
input_batch = []
target_batch = []
for sentence in corpus:
word = sentence.split() # 띄어쓰기 기준으로 단어 구분
input = [word_dict[n] for n in word[:-1]] # 1 ~ n-1 번째 단어를 input으로 사용. n=3이므로 첫 두 단어 사용
target = word_dict[word[-1]] # n번째 단어를 target으로 사용
input_batch.append(input)
target_batch.append(target)
input_batch = torch.LongTensor(input_batch)
target_batch = torch.LongTensor(target_batch)
### Hyper-params ###
n = 3
n_hidden = 2
m = 2
V = len(tokens)
### modeling ###
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.C = nn.Embedding(V, m)
self.H = nn.Linear((n-1)*m, n_hidden, bias=False)
self.d = nn.Parameter(torch.ones(n_hidden))
self.U = nn.Linear(n_hidden, V, bias=False)
self.W = nn.Linear((n-1)*m, V, bias=False)
self.b = nn.Parameter(torch.ones(V))
def forward(self, x):
x = self.C(x)
x = x.view(-1, (n-1)*m)
tanh = torch.tanh(self.d + self.H(x))
output = self.b + self.W(x) + self.U(tanh)
return output
### Loss function and Optimizer ###
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
### Training ###
for epoch in range(10000):
optimizer.zero_grad()
output = model(input_batch)
loss = criterion(output, target_batch)
if (epoch+1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch+1), 'cost =', '{:.6f}'.format(loss)) #1000번째 epoch마다 loss 출력
loss.backward()
optimizer.step()
### Prediction ###
predict = model(input_batch).data.max(1, keepdim=True)[1]
for i, j in zip(corpus, predict.view(-1).tolist()):
print(i.split()[:n-1], ' -> ', str(index_dict[j]))
### Plotting ###
plt.rc('font', family='NanumBarunGothic') # 한글 출력을 가능하게 만들기
fit, ax = plt.subplots()
ax.scatter(model.C.weight[:, 0].tolist(), model.C.weight[:, 1].tolist())
for i, txt in enumerate(tokens):
ax.annotate(txt, (model.C.weight[i, 0].tolist(), model.C.weight[i, 1].tolist()))
Epoch: 1000 cost = 0.000000
Epoch: 2000 cost = 0.000000
Epoch: 3000 cost = 0.000000
Epoch: 4000 cost = 0.000000
Epoch: 5000 cost = 0.000000
Epoch: 6000 cost = 0.000000
Epoch: 7000 cost = 0.000000
Epoch: 8000 cost = 0.000000
Epoch: 9000 cost = 0.000000
Epoch: 10000 cost = 0.000000
['비정형데이터', '수업'] -> 시작이다
['나는', '오늘도'] -> 이뻐
['코딩은', '너무'] -> 어렵잖아
['NLP', '이러지마'] -> 제발
['우리', '열심히'] -> 해봐요
댓글남기기