import urllib.request
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
from copy import deepcopy
from torch.utils.data import Dataset, DataLoader
from tqdm.auto import tqdm
urllib.request.urlretrieve('https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt', filename='ratings_train.txt' )
urllib.request.urlretrieve('https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt', filename='ratings_test.txt' )
('ratings_test.txt', <http.client.HTTPMessage at 0x7d500c667d30>)
train_dataset = pd.read_table('ratings_train.txt')
train_dataset
id | document | label | |
---|---|---|---|
0 | 9976970 | 아 더빙.. 진짜 짜증나네요 목소리 | 0 |
1 | 3819312 | 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 | 1 |
2 | 10265843 | 너무재밓었다그래서보는것을추천한다 | 0 |
3 | 9045019 | 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 | 0 |
4 | 6483659 | 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... | 1 |
... | ... | ... | ... |
149995 | 6222902 | 인간이 문제지.. 소는 뭔죄인가.. | 0 |
149996 | 8549745 | 평점이 너무 낮아서... | 1 |
149997 | 9311800 | 이게 뭐요? 한국인은 거들먹거리고 필리핀 혼혈은 착하다? | 0 |
149998 | 2376369 | 청춘 영화의 최고봉.방황과 우울했던 날들의 자화상 | 1 |
149999 | 9619869 | 한국 영화 최초로 수간하는 내용이 담긴 영화 | 0 |
150000 rows × 3 columns
# pos, neg 비율
train_dataset['label'].value_counts()
label 0 75173 1 74827 Name: count, dtype: int64
sum(train_dataset['document'].isnull())
5
~train_dataset['document'].isnull()
0 True 1 True 2 True 3 True 4 True ... 149995 True 149996 True 149997 True 149998 True 149999 True Name: document, Length: 150000, dtype: bool
train_dataset = train_dataset[~train_dataset['document'].isnull()]
sum(train_dataset['document'].isnull())
0
train_dataset
id | document | label | |
---|---|---|---|
0 | 9976970 | 아 더빙.. 진짜 짜증나네요 목소리 | 0 |
1 | 3819312 | 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 | 1 |
2 | 10265843 | 너무재밓었다그래서보는것을추천한다 | 0 |
3 | 9045019 | 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 | 0 |
4 | 6483659 | 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... | 1 |
... | ... | ... | ... |
149995 | 6222902 | 인간이 문제지.. 소는 뭔죄인가.. | 0 |
149996 | 8549745 | 평점이 너무 낮아서... | 1 |
149997 | 9311800 | 이게 뭐요? 한국인은 거들먹거리고 필리핀 혼혈은 착하다? | 0 |
149998 | 2376369 | 청춘 영화의 최고봉.방황과 우울했던 날들의 자화상 | 1 |
149999 | 9619869 | 한국 영화 최초로 수간하는 내용이 담긴 영화 | 0 |
149995 rows × 3 columns
Tokenization¶
- 자연어를 모델이 이해하기 위해서는 자연어를 숫자의 형시으로 변형 시켜야 함
train_dataset['document'].iloc[0].split()
['아', '더빙..', '진짜', '짜증나네요', '목소리']
vocab = set()
for doc in train_dataset['document']:
for token in doc.split():
vocab.add(token)
len(vocab)
357862
# 단어의 빈도수 구하기
'''
[('아', 1024),
('더빙', 2),
('진짜', 5929),
...]
'''
vocab_cnt_dict = {}
for doc in train_dataset['document']:
for token in doc.split():
if token not in vocab_cnt_dict:
vocab_cnt_dict[token] = 0
vocab_cnt_dict[token] += 1
vocab_cnt_dict
{'아': 1204, '더빙..': 2, '진짜': 5929, '짜증나네요': 10, '목소리': 99, '흠...포스터보고': 1, '초딩영화줄....오버연기조차': 1, '가볍지': 17, '않구나': 2, '너무재밓었다그래서보는것을추천한다': 1, ...}
vocab_cnt_list = [(token, cnt) for token, cnt in vocab_cnt_dict.items()]
vocab_cnt_list[:10]
[('아', 1204), ('더빙..', 2), ('진짜', 5929), ('짜증나네요', 10), ('목소리', 99), ('흠...포스터보고', 1), ('초딩영화줄....오버연기조차', 1), ('가볍지', 17), ('않구나', 2), ('너무재밓었다그래서보는것을추천한다', 1)]
top_vocabs = sorted(vocab_cnt_list, key=lambda tup: tup[1], reverse=True)
top_vocabs[:10]
[('영화', 10825), ('너무', 8239), ('정말', 7791), ('진짜', 5929), ('이', 5059), ('영화.', 3598), ('왜', 3285), ('더', 3260), ('이런', 3249), ('그냥', 3237)]
cnts = [cnt for _, cnt in top_vocabs]
cnts
[10825, 8239, 7791, 5929, 5059, 3598, 3285, 3260, 3249, 3237, 2945, 2759, ...]
np.mean(cnts)
3.1792590439890236
cnts[:10]
[10825, 8239, 7791, 5929, 5059, 3598, 3285, 3260, 3249, 3237]
sum(np.array(cnts) > 2)
42635
n_vocab = sum(np.array(cnts) > 2)
n_vocab
42635
top_vocabs_truncated = top_vocabs[:n_vocab]
top_vocabs_truncated[:5]
[('영화', 10825), ('너무', 8239), ('정말', 7791), ('진짜', 5929), ('이', 5059)]
vocabs = [token for token, _ in top_vocabs_truncated]
vocabs[:5]
['영화', '너무', '정말', '진짜', '이']
special token¶
- [UNK]: Unknown token
- [PAD]: Padding token
unk_token = '[UNK]'
unk_token in vocabs
False
pad_token = '[PAD]'
pad_token in vocabs
False
vocabs.insert(0, unk_token)
vocabs.insert(0, pad_token)
vocabs[:5]
['[PAD]', '[UNK]', '영화', '너무', '정말']
idx_to_token = vocabs
token_to_idx = {token: i for i, token in enumerate(idx_to_token)}
class Tokenizer:
def __init__(self, vocabs, use_padding=True, max_padding=64, pad_token='[PAD]', unk_token='[UNK]'):
self.idx_to_token = vocabs
self.token_to_idx = {token: i for i, token in enumerate(self.idx_to_token)}
self.use_padding = use_padding
self.max_padding = max_padding
self.pad_token = pad_token
self.unk_token = unk_token
self.unk_token_idx = self.token_to_idx[self.unk_token]
self.pad_token_idx = self.token_to_idx[self.pad_token]
def __call__(self, x:str):
token_ids = []
token_list = x.split()
for token in token_list:
if token in self.token_to_idx:
token_idx = self.token_to_idx[token]
else:
token_idx = self.unk_token_idx
token_ids.append(token_idx)
if self.use_padding:
token_ids = token_ids[:self.max_padding]
n_pads = self.max_padding - len(token_ids)
token_ids = token_ids + [self.pad_token_idx] * n_pads
return token_ids
tokenizer = Tokenizer(vocabs, use_padding=False)
sample = train_dataset['document'].iloc[0]
print(sample)
아 더빙.. 진짜 짜증나네요 목소리
tokenizer(sample) # [51, 1, 5, 10485, 1064]
[51, 1, 5, 10485, 1064]
token_length_list = []
for sample in train_dataset['document']:
token_length_list.append(len(tokenizer(sample)))
plt.hist(token_length_list)
plt.xlabel('token length')
plt.ylabel('count')
Text(0, 0.5, 'count')
max(token_length_list)
41
tokenizer = Tokenizer(vocabs, use_padding=True, max_padding=50)
print(tokenizer(sample))
[201, 2, 3635, 1, 121, 1946, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
train_valid_dataset = pd.read_table('ratings_train.txt')
test_dataset = pd.read_table('ratings_test.txt')
print(f'train, valid samples: {len(train_valid_dataset)}')
print(f'test samples: {len(test_dataset)}')
train, valid samples: 150000 test samples: 50000
train_valid_dataset.head()
id | document | label | |
---|---|---|---|
0 | 9976970 | 아 더빙.. 진짜 짜증나네요 목소리 | 0 |
1 | 3819312 | 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 | 1 |
2 | 10265843 | 너무재밓었다그래서보는것을추천한다 | 0 |
3 | 9045019 | 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 | 0 |
4 | 6483659 | 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... | 1 |
train_valid_dataset = train_valid_dataset.sample(frac=1)
train_valid_dataset.head()
id | document | label | |
---|---|---|---|
65864 | 3927262 | 강연 짱재미있었어요~!! 다음에도 강연해주실꺼 죠?? | 1 |
44642 | 10063080 | 악역 매력이 전작에 비해 너무 떨어짐... | 1 |
96991 | 8499620 | 진심 잼엄슴 별그대가 더재밋음ㅋ | 0 |
84276 | 9211116 | 난 평론가 별점이 5점이면 박평식 개XX밖에 안떠오르더라 | 1 |
41489 | 60456 | 그다지 완성감을 주는영화는 아니지만 욕망에 충실한 삶을 살으란건 동의함 | 0 |
train_tatio = 0.8
n_train = int(len(train_valid_dataset) * train_tatio)
train_df = train_valid_dataset[:n_train]
valid_df = train_valid_dataset[n_train:]
test_df = test_dataset
print(f'train, valid samples: {len(valid_df)}')
print(f'train, valid samples: {len(train_df)}')
print(f'test samples: {len(test_df)}')
train, valid samples: 30000 train, valid samples: 120000 test samples: 50000
# 1/10으로 샘플링
train_df = train_df.sample(frac=0.1)
valid_df = valid_df.sample(frac=0.1)
test_df = test_df.sample(frac=0.1)
print(f'train, valid samples: {len(valid_df)}')
print(f'train, valid samples: {len(train_df)}')
print(f'test samples: {len(test_df)}')
train, valid samples: 3000 train, valid samples: 12000 test samples: 5000
class NSMCDataset(Dataset):
def __init__(self, data_df, tokenizer=None):
self.data_df = data_df
self.tokenizer = tokenizer
def __len__(self):
return len(self.data_df)
def __getitem__(self, idx):
sample_raw = self.data_df.iloc[idx]
sample = {}
sample['doc'] = str(sample_raw['document'])
sample['label'] = int(sample_raw['label'])
if self.tokenizer is not None:
sample['doc_ids'] = self.tokenizer(sample['doc'])
return sample
train_dataset = NSMCDataset(data_df=train_df, tokenizer=tokenizer)
valid_dataset = NSMCDataset(data_df=valid_df, tokenizer=tokenizer)
test_dataset = NSMCDataset(data_df=test_df, tokenizer=tokenizer)
print(train_dataset[0])
{'doc': '킬링타임용으로도 부족하군', 'label': 0, 'doc_ids': [2553, 13384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
def collate_fn(batch):
keys = [key for key in batch[0].keys()]
data = {key: [] for key in keys}
for item in batch:
for key in keys:
data[key].append(item[key])
return data
train_dataloader = DataLoader(
train_dataset,
batch_size=128,
collate_fn=collate_fn,
shuffle=True,
)
valid_dataloader = DataLoader(
valid_dataset,
batch_size=128,
collate_fn=collate_fn,
shuffle=False,
)
test_dataloader = DataLoader(
test_dataset,
batch_size=128,
collate_fn=collate_fn,
shuffle=False,
)
sample = next(iter(train_dataloader))
sample.keys() # dict_keys(['doc', 'label', 'doc_ids'])
dict_keys(['doc', 'label', 'doc_ids'])
sample['doc'][2] # 정말 재미지게 오랫동안 보게되는 드라마
'정말 공감되는게 니가 내 여친이다라는 생각이 들지 않는 상태에서 먼저 ㅅㅅ부터 하게되면 갈수록 이게 무슨관계인가 하는 생각이 들게됨,, ㅅㅅ하고 나면 사귀는 사이가 되겠지라고 생각하지만 막상 이후에도 긴가민가'
print(sample['doc_ids'][2]) # [4, 17366, 2223, 2798, 52, 0, 0, ... 0]
[4, 1, 2622, 37, 1, 199, 2961, 135, 6946, 626, 1, 1, 231, 34, 1, 49, 199, 1, 1, 2658, 1, 15053, 1, 14539, 3165, 18440, 38901, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
CBOW(Continuous Bag of Words)¶
- 자연어 처리에서 단어의 의미를 벡터로 표현하는 Word2Vec 모델 중 하나
- 문맥(context) 단어들을 사용해서 타겟(target) 단어를 예측하는 것
- 모델의 원리
- 문장은 단어의 연속으로 구성
- 예) The cat sat on the mat
- 타겟단어: 'cat', 주변 단어: ('The', 'sat')
- 문맥 단어의 수를 결정하는 윈도우 크기를 설정
- 예) 윈도우 크기: 2, 타겟 단어의 왼쪽과 오른쪽에서 각각 2개의 단어를 문맥 단어로 사용
- 모든 단어를 고유한 인덱스로 매핑하고 원 핫 인코딩으로 변환
- 입력으로 주어진 문맥 단어들을 이용해 타겟 단어를 예측하는 신경망을 학습
- 예) 일반적으로 입력층, 은닉층, 출력층 3개의 층으로 구성
- 입력층: 문맥 단어들의 원 핫 인코딩 벡터를 받음
- 은닉층: 입력 벡터들의 평균을 계산하여 은닉층의 벡터를 만듦
- 출력층: 은닉층 벡터를 사용해 타겟 단어를 예측
- 문장은 단어의 연속으로 구성
class CBOW(nn.Module):
def __init__(self, vocab_size, embed_dim):
super().__init__()
self.output_dim = embed_dim
self.embeddings = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
def forward(self, x):
# (batch_size, sequence) -> (batch_size, sequence, embed_dim)
x_embeded = self.embeddings(x)
stnc_repr = torch.mean(x_embeded, dim=1) # batch_size * embed_dim
return stnc_repr
class Classifier(nn.Module):
def __init__(self, sr_model, output_dim, vocab_size, embed_dim, **kwargs):
super().__init__()
self.sr_model = sr_model(vocab_size=vocab_size, embed_dim=embed_dim, **kwargs)
self.input_dim = self.sr_model.output_dim
self.output_dim = output_dim
self.fc = nn.Linear(self.input_dim, self.output_dim)
def forward(self, x):
return self.fc(self.sr_model(x))
model = Classifier(sr_model=CBOW, output_dim=2, vocab_size=len(vocabs), embed_dim=16)
model.sr_model.embeddings.weight[1]
tensor([-0.2740, -0.0174, 1.3547, -0.8324, 0.9788, 0.5641, -0.7510, 0.6817, 0.3176, -0.3878, -0.7905, -0.1354, -0.9579, -2.1958, -0.4043, -0.3356], grad_fn=<SelectBackward0>)
use_cuda = torch.cuda.is_available()
if use_cuda:
model.cuda()
optimizer = optim.Adam(params=model.parameters(), lr=0.01)
calc_loss = nn.CrossEntropyLoss()
n_epoch = 10
global_i = 0
valid_loss_history = []
train_loss_history = []
best_model = None
best_epoch_i = None
min_valid_loss = 9e+9
for epoch_i in range(n_epoch):
model.train()
for batch in train_dataloader:
optimizer.zero_grad()
X = torch.tensor(batch['doc_ids'])
y = torch.tensor(batch['label'])
if use_cuda:
X = X.cuda()
y = y.cuda()
y_pred = model(X)
loss = calc_loss(y_pred, y)
if global_i % 1000 == 0:
print(f'i: {global_i}, epoch: {epoch_i}, loss: {loss.item()}')
train_loss_history.append((global_i, loss.item()))
loss.backward()
optimizer.step()
global_i += 1
model.eval()
valid_loss_list = []
for batch in valid_dataloader:
X = torch.tensor(batch['doc_ids'])
y = torch.tensor(batch['label'])
if use_cuda:
X = X.cuda()
y = y.cuda()
y_pred = model(X)
loss = calc_loss(y_pred, y)
valid_loss_list.append(loss.item())
valid_loss_mean = np.mean(valid_loss_list)
valid_loss_history.append((global_i, valid_loss_mean.item()))
if valid_loss_mean < min_valid_loss:
min_valid_loss = valid_loss_mean
best_epoch_i = epoch_i
best_model = deepcopy(model)
if epoch_i % 2 == 0:
print("*"*30)
print(f'valid_loss_mean: {valid_loss_mean}')
print("*"*30)
print(f'best_epoch: {best_epoch_i}')
i: 0, epoch: 0, loss: 0.6995193958282471 ****************************** valid_loss_mean: 0.6590393508474032 ****************************** ****************************** valid_loss_mean: 0.5344555626312891 ****************************** ****************************** valid_loss_mean: 0.5585109815001488 ****************************** ****************************** valid_loss_mean: 0.6116074522336324 ****************************** ****************************** valid_loss_mean: 0.673076349000136 ****************************** best_epoch: 2
def calc_moving_average(arr, win_size=100):
new_arr = []
win = []
for i, val in enumerate(arr):
win.append(val)
if len(win) > win_size:
win.pop(0)
new_arr.append(np.mean(win))
return np.array(new_arr)
valid_loss_history = np.array(valid_loss_history)
train_loss_history = np.array(train_loss_history)
plt.figure(figsize=(12,8))
plt.plot(train_loss_history[:,0],
calc_moving_average(train_loss_history[:,1]), color='blue')
plt.plot(valid_loss_history[:,0],
valid_loss_history[:,1], color='red')
plt.xlabel("step")
plt.ylabel("loss")
Text(0, 0.5, 'loss')
Evaluation¶
model = best_model
model.eval()
total = 0
correct = 0
for batch in tqdm(test_dataloader,
total=len(test_dataloader.dataset)//test_dataloader.batch_size):
X = torch.tensor(batch['doc_ids'])
y = torch.tensor(batch['label'])
if use_cuda:
X = X.cuda()
y = y.cuda()
y_pred = model(X)
curr_correct = y_pred.argmax(dim=1) == y
total += len(curr_correct)
correct += sum(curr_correct)
print(f'test accuracy: {correct/total}')
0%| | 0/39 [00:00<?, ?it/s]
test accuracy: 0.7218000292778015