실습 코드를 바탕으로 RNN 구현을 다시 한 번 이해해보고자 한다.
실습은 IMDB 영화 리뷰를 바탕으로 진행한다.
실습 코드:
https://wikidocs.net/60691
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchtext import data, datasets, legacy
import random
SEED = 5
random.seed(SEED)
torch.manual_seed(SEED)
# 하이퍼파라미터
BATCH_SIZE = 64
lr = 0.001
EPOCHS = 10
시드와 배치 크기, 학습률, 몇 에폭을 돌지 미리 결정해둔다.
이제 text와 label을 받을 데이터 필드를 저장해둔다. text의 경우는 순서가 존재하므로 sequential하게 받아야 할 것이다.
TEXT = legacy.data.Field(sequential=True, batch_first=True, lower=True)
LABEL = legacy.data.Field(sequential=False, batch_first=True)
# 전체 데이터를 훈련 데이터와 테스트 데이터를 8:2 비율로 나누기
trainset, testset = legacy.datasets.IMDB.splits(TEXT, LABEL)
# 이를 바탕으로 trainset, test set에는 리뷰와 label의 쌍이 들어가게 된다.
# 단어 집합의 생성
# min_freq를 통해서 5번도 안나온 단어는 <unk>가 될 것이다.
TEXT.build_vocab(trainset, min_freq=5) # 단어 집합 생성
LABEL.build_vocab(trainset)
trainset, valset = trainset.split(split_ratio=0.8)
잠깐>
1. batch_first의 역할 : 신경망에 입력되는 텐서의 첫번째 차원 값이 batch_size가 된다.
즉 여기서는 위에서 지정한 64가 들어가게 될 것이다.
2. text의 lower의 역할 : 텍스트 데이터 속 모든 영문을 소문자로 처리한다.
이를 하지 않을 경우 Apple과 apple을 다른 단어로 인식하게 되는 불상사(?!)가 발생한다.
여기서 한 것은 데이터셋의 구성, 및 TVT split, 그리고 단어의 집합을 만든 것이다.
train_iter, val_iter, test_iter = legacy.data.BucketIterator.splits(
(trainset, valset, testset), batch_size=BATCH_SIZE,
shuffle=True, repeat=False)
지난번에 우리는 dataloader라는 것에 대해 배웠다. torchtext에서도 비슷한 기능을 제공하는데 , 그것이 BucketIterator이다.
이제, 이를 바탕으로 신경망을 먼저 구성해보자.
class GRU(nn.Module):
def __init__(self, n_layers, hidden_dim, n_vocab, embed_dim, n_classes, dropout_p=0.2):
super(GRU, self).__init__()
self.n_layers = n_layers
self.hidden_dim = hidden_dim
self.embed = nn.Embedding(n_vocab, embed_dim)
self.dropout = nn.Dropout(dropout_p)
# 앞에서 batch_first를 true로 했으므로, 여기서도 batch_first = True로 통일.
self.gru = nn.GRU(embed_dim, self.hidden_dim,
num_layers=self.n_layers,
batch_first=True)
# 최종 은닉층에서, 답을 내기 위한 과정이다.
self.out = nn.Linear(self.hidden_dim, n_classes)
def forward(self, x):
x = self.embed(x)
# 중요! 최초의 hidden_state를 정의하는 과정을 넣어줘야 한다.
# 첫번째 히든 스테이트를 0벡터로 초기화
h_0 = self._init_state(batch_size=x.size(0))
# GRU의 리턴값은 (배치 크기, 시퀀스 길이, 은닉 상태의 크기)
x, _ = self.gru(x, h_0)
# (배치 크기, 은닉 상태의 크기)의 텐서로 크기가 변경됨. 즉, 마지막 time-step의 은닉 상태만 가져온다.
h_t = x[:,-1,:]
self.dropout(h_t)
# (배치 크기, 은닉 상태의 크기) -> (배치 크기, 출력층의 크기)
logit = self.out(h_t)
return logit
def _init_state(self, batch_size=1):
weight = next(self.parameters()).data
return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_()
지금까지 나는, rnn을 그냥 정의만 했지, hidden state를 초기화하는 작업을 하지 않았었다.!
또한, 마지막 hidden_state를 가져와서 이를 통과해야 하는데, 다른 값을 가져오고 있었다...
그러니 지금까지 실습 중 학습이 올바르게 안되고 있었던 것이다.....
왜, 마지막 hidden_state를 가져와야 하는가?
RNN의 공식을 다시 확인해보면, hidden state가 갱신되다가, 원하는 시점에서 최종적으로 원하는 y_t를 가져온다.
이를 위해 최종 hidden state를 가져오고, out에서 선형회귀로 처리하게 된다!
vocab_size = len(TEXT.vocab)
n_classes = 2
model = GRU(1, 256, vocab_size, 128, n_classes, 0.5)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
훈련/검증 함수를 살펴보자.
def train(model, optimizer, train_iter):
model.train()
for b, batch in enumerate(train_iter):
# 배치에서 text와 label을 가져온다.
x, y = batch.text, batch.label
y.data.sub_(1) # 레이블 값을 0과 1로 변환
optimizer.zero_grad()
# 학습 과정을 통해 0, 1 결정, binary이므로cross_entropy
logit = model(x)
loss = F.cross_entropy(logit, y)
# back-propagation
loss.backward()
optimizer.step()
def evaluate(model, val_iter):
"""evaluate model"""
model.eval()
corrects, total_loss = 0, 0
for batch in val_iter:
x, y = batch.text, batch.label
y.data.sub_(1) # 레이블 값을 0과 1로 변환
logit = model(x)
loss = F.cross_entropy(logit, y, reduction='sum')
# error를 더하고 data개수로 나눠서, 평균 error를 평가한다.
total_loss += loss.item()
corrects += (logit.max(1)[1].view(y.size()).data == y.data).sum()
size = len(val_iter.dataset)
avg_loss = total_loss / size
avg_accuracy = 100.0 * corrects / size
return avg_loss, avg_accuracy
최종 학습/검증을 진행하면 다음과 같다.
best_val_loss = None
for e in range(1, EPOCHS+1):
train(model, optimizer, train_iter)
val_loss, val_accuracy = evaluate(model, val_iter)
print("[Epoch: %d] val loss : %5.2f | val accuracy : %5.2f" % (e, val_loss, val_accuracy))
# 검증 오차가 가장 적은 최적의 모델을 저장
if not best_val_loss or val_loss < best_val_loss:
if not os.path.isdir("snapshot"):
os.makedirs("snapshot")
torch.save(model.state_dict(), './snapshot/txtclassification.pt')
best_val_loss = val_loss
* Hidden state를 초기화하고 이의 마지막을 선형회귀에 통과시켜야 한다는 점 및, label값을 어떻게 0, 1로 초기화하는지 등등을 알게 되었다.
다음번에는, nn.Embedding에 대해서 알아보고자 한다.
'ML & DL' 카테고리의 다른 글
[DL] 전이학습 개념 간단하게. (0) | 2022.03.30 |
---|---|
(극복Proj-1)[Pytorch] DataLoader란? (0) | 2021.12.23 |
[DL] 순환 신경망 RNN과 장단기메모리 LSTM 개요 (0) | 2021.08.21 |
[DL] 컨볼루션 신경망, CNN 이해하기 (0) | 2021.06.24 |
[DL] Pytorch로 Layer 만들기 (0) | 2021.06.23 |