카테고리 없음
파이토치 RNN(영화리뷰 감정분석 & 기계분석)
3pie
2022. 2. 16. 15:17
기록 - 22/02/15
프로젝트 1. 영화 리뷰 감정 분석
RNN 을 이용해 IMDB 데이터를 가지고 텍스트 감정분석을 해 봅시다.
이번 책에서 처음으로 접하는 텍스트 형태의 데이터셋인 IMDB 데이터셋은 50,000건의 영화 리뷰로 이루어져 있습니다. 각 리뷰는 다수의 영어 문장들로 이루어져 있으며, 평점이 7점 이상의 긍정적인 영화 리뷰는 2로, 평점이 4점 이하인 부정적인 영화 리뷰는 1로 레이블링 되어 있습니다. 영화 리뷰 텍스트를 RNN 에 입력시켜 영화평의 전체 내용을 압축하고, 이렇게 압축된 리뷰가 긍정적인지 부정적인지 판단해주는 간단한 분류 모델을 만드는 것이 이번 프로젝트의 목표입니다.
In [1]:
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchtext import data, datasets # torchtext 자연어 데이터셋
In [2]:
# 하이퍼파라미터
BATCH_SIZE = 64
lr = 0.001
EPOCHS = 10
USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device("cuda" if USE_CUDA else "cpu")
print("다음 기기로 학습합니다:", DEVICE)
다음 기기로 학습합니다: cuda
In [3]:
# 데이터 로딩하고 텐서로 변환
print("데이터 로딩중...")
#텍스트형태의 영화리뷰들과 그에 해당하는 레이블을 텐서로 바꿔주는 설정을 정합니다
#torchtext이용하면 설정정보를 담은 TEXT, LABEL이라는 객체를쉽게 생성할 수 있다
#sequential파라미터를 이용해 데이터셋이 순차적인 데이터셋인지 명시해줌 , 레이블값은 단순히 클래스를 나타내는 숫자이므로 순차적인 데이터가 아님
#batch_first 파라미터로 신경망에 입력되는 텐서의 첫번째 차원값이 batch_size가 되도록 정해주기
#lower변수를 이용해 텍스트 데이터 속 모든 영문알파벳이 소문자로 만들어주기
TEXT = data.Field(sequential=True, batch_first=True, lower=True)
LABEL = data.Field(sequential=False, batch_first=True)
#데이터셋 만들어주기
trainset, testset = datasets.IMDB.splits(TEXT, LABEL)
#워드임베딩에 필요한 단어사전 만들기
#min_freq = 최소 다섯번이상 등장한 단어만을 사전에 담겠다는 의미(이때 5번 미만출현 단어는 unknown토큰으로 대체 )
TEXT.build_vocab(trainset, min_freq=5)
LABEL.build_vocab(trainset)
# 학습용 데이터를 학습셋 80% 검증셋 20% 로 나누기
trainset, valset = trainset.split(split_ratio=0.8)
#배치단위로 쪼개서 학습 진행
train_iter, val_iter, test_iter = data.BucketIterator.splits(
(trainset, valset, testset), batch_size=BATCH_SIZE,
shuffle=True, repeat=False)
#사전 속 단어들의 개수와 레이블의 수를 정해주는 변수를 만들기
vocab_size = len(TEXT.vocab)
n_classes = 2
데이터 로딩중...
In [4]:
print("[학습셋]: %d [검증셋]: %d [테스트셋]: %d [단어수]: %d [클래스] %d"
% (len(trainset),len(valset), len(testset), vocab_size, n_classes))
[학습셋]: 20000 [검증셋]: 5000 [테스트셋]: 25000 [단어수]: 46159 [클래스] 2
In [5]:
#신경망
class BasicGRU(nn.Module):
#__init__에서 가장먼저 정의하는 변수는 은닉벡터들의 '층' 이라고 할수 있는 n_layer입니다. 아주 복잡한 모델 아닌이상 n_layers는 2 이하로 정의
def __init__(self, n_layers, hidden_dim, n_vocab, embed_dim, n_classes, dropout_p=0.2):
super(BasicGRU, self).__init__()
print("Building Basic GRU model...")
self.n_layers = n_layers
self.embed = nn.Embedding(n_vocab, embed_dim)
#nn.Embedding()함수 n_vocab: 사전에 등재된 단어수 / embed_dim: 임베딩된 단어 텐서가 지나는 차원값
# < 은닉벡터의 차원값과 드롭아웃 정의 >
self.hidden_dim = hidden_dim
self.dropout = nn.Dropout(dropout_p)
#rnn model 정의 (rnn이 아니고nn.GRU인 이유 : rnn문제점 극복)
self.gru = nn.GRU(embed_dim, self.hidden_dim,
num_layers=self.n_layers,
batch_first=True)
#gru도 rnn과 마찬가지로 시계열데이터를 하나의 텐서로 압축을 하는데 이 텐서는 문장 전체에 대한 맥락을 담고 있다.
#이 정보를 토대로 모델이 영화리뷰에 대한 분류를 하기 위해선 압축된 텐서를 다음과 같이 신경망에 통과시켜 클래스에 대한 예측을 출력하도록 한다
self.out = nn.Linear(self.hidden_dim, n_classes)
def forward(self, x):
x = self.embed(x) #압력데이터
h_0 = self._init_state(batch_size=x.size(0)) #첫번째 은닉벡터H0을 정의해 x와함께 입력해줘야 한다._init_state 함수를 구현하고 은닉벡터 정의
x, _ = self.gru(x, h_0) # [i, b, h] (배치사이즈, 입력x의 길이, hidden_dim의 모양을 가진 3d 텐서가 된다 )
h_t = x[:,-1,:] # 인덱싱 하면 배치내 모든 시계열 은닉벡터들의 마지막 토큰들을 내포한 (배치사이즈, 1, hidden_dim)모양의 텐서를 추출할수 있다
#결론 h_t가 곧 영화리뷰 배열들을 압축한 은닉벡터가 되는것
self.dropout(h_t)
logit = self.out(h_t) # [b, h] -> [b, o] 신경망에 입력해서 결과를 출력
return logit
def _init_state(self, batch_size=1):
weight = next(self.parameters()).data #nn.GRU모듈의 첫번째 가중치 텐서를 추출한다(이 텐서는 모델의 가중치 텐서와 같은 데이터 타입이다)
return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_()
#new()함수를 호출해 모델의 가중치와 같은 모양인 self.n_layers, batch_size, self.hidden_dim 모양을 갖춘 텐서로 변환한후 zero_() 함수를 호출해 텐서 내 모든값을 0으로 초기화 합니다
#이처럼 첫번째 은닉벡터 H0(코드상에 h_0은 보통 모든 특성값이 0인 벡터로 설정이 된다)
In [6]:
#학습
def train(model, optimizer, train_iter):
model.train()
#enumerate(train_iter) = 반복마다 배치데이터를 반환 한다
for b, batch in enumerate(train_iter):
x, y = batch.text.to(DEVICE), batch.label.to(DEVICE)#배치내의 영화평 데이터와, 그에 상응하는 레이블은 batch.text.to.로 접근
#batch.label.to은 1이나 2의 값을 가지고 있어서 0과1로 변환
y.data.sub_(1) # 레이블 값을 0과 1로 변환 data.sub_(1) 함수는 y의 모든값에서 1씩 빼서 1은0으로 2는 1로 만들어 준다
optimizer.zero_grad()#기울기를 0으로 초기화 시키고, 학습데이터x를 모델에 입력해 예측값이 logit을 계산한다
logit = model(x)
loss = F.cross_entropy(logit, y)
loss.backward()
optimizer.step()
In [7]:
#검증
def evaluate(model, val_iter):
"""evaluate model"""
model.eval()
corrects, total_loss = 0, 0
for batch in val_iter:
x, y = batch.text.to(DEVICE), batch.label.to(DEVICE)
y.data.sub_(1) # 레이블 값을 0과 1로 변환
logit = model(x)
loss = F.cross_entropy(logit, y, reduction='sum') #reduction='sum' : 오차합 구하기
total_loss += loss.item() #오차합을 total_loss에 더해주기
corrects += (logit.max(1)[1].view(y.size()).data == y.data).sum() #corrects 모델이 맞힌수 담기
size = len(val_iter.dataset)
avg_loss = total_loss / size #오차평균
avg_accuracy = 100.0 * corrects / size #정확도 평균
return avg_loss, avg_accuracy
In [8]:
#모델 객체 정의
#모델내 은닉벡터의 차원값은 256으로, 그리고 임베딩된 토큰의 차원값은 128로 임의로 설정한다.
model = BasicGRU(1, 256, vocab_size, 128, n_classes, 0.5).to(DEVICE)
#최적화 알고리즘 정의
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
Building Basic GRU model...
BasicGRU(
(embed): Embedding(46159, 128)
(dropout): Dropout(p=0.5)
(gru): GRU(128, 256, batch_first=True)
(out): Linear(in_features=256, out_features=2, bias=True)
)
In [9]:
#학습 실행
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("[이폭: %d] 검증 오차:%5.2f | 검증 정확도:%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
[이폭: 1] 검증 오차: 0.69 | 검증 정확도:49.00
[이폭: 2] 검증 오차: 0.69 | 검증 정확도:50.00
[이폭: 3] 검증 오차: 0.69 | 검증 정확도:54.00
[이폭: 4] 검증 오차: 0.70 | 검증 정확도:50.00
[이폭: 5] 검증 오차: 0.40 | 검증 정확도:81.00
[이폭: 6] 검증 오차: 0.33 | 검증 정확도:86.00
[이폭: 7] 검증 오차: 0.35 | 검증 정확도:85.00
[이폭: 8] 검증 오차: 0.36 | 검증 정확도:86.00
[이폭: 9] 검증 오차: 0.38 | 검증 정확도:86.00
[이폭: 10] 검증 오차: 0.41 | 검증 정확도:86.00
In [10]:
model.load_state_dict(torch.load('./snapshot/txtclassification.pt'))
test_loss, test_acc = evaluate(model, test_iter)
print('테스트 오차: %5.2f | 테스트 정확도: %5.2f' % (test_loss, test_acc))
테스트 오차: 0.31 | 테스트 정확도: 86.00
Seq2Seq 기계 번역
이번 프로젝트에선 임의로 Seq2Seq 모델을 아주 간단화 시켰습니다. 한 언어로 된 문장을 다른 언어로 된 문장으로 번역하는 덩치가 큰 모델이 아닌 영어 알파벳 문자열("hello")을 스페인어 알파벳 문자열("hola")로 번역하는 Mini Seq2Seq 모델을 같이 구현해 보겠습니다.
In [115]:
import torch
import torch.nn as nn
import torch.nn.functional as F
import random
import matplotlib.pyplot as plt
In [116]:
vocab_size = 256 # 총 아스키 코드(영문을 숫자로 표현하는 방식인 아스키코드로 임베댕을 대신함 )개수 /(데이터속에 총 몇종류의 토큰이 있는지 정의해주는 변수)
#seq2seq 모델에 입력된 원문과 번역문을 아스키 코드의 배열로 정의하고 파이토치 텐서로 바꿔준다
x_ = list(map(ord, "hello")) # 아스키 코드 리스트로 변환
y_ = list(map(ord, "hola")) # 아스키 코드 리스트로 변환
print("hello -> ", x_)
print("hola -> ", y_)
hello -> [104, 101, 108, 108, 111]
hola -> [104, 111, 108, 97]
In [117]:
#파이토치 텐서로 바꿔주기
x = torch.LongTensor(x_)
y = torch.LongTensor(y_)
In [118]:
#seq2seq모델 클래스를 정의한다
class Seq2Seq(nn.Module):
def __init__(self, vocab_size, hidden_size):
super(Seq2Seq, self).__init__()
self.n_layers = 1 #아무리 복잡한 모델이여도 2를 초과하진 않음
self.hidden_size = hidden_size
#임베딩 함수 정의
self.embedding = nn.Embedding(vocab_size, hidden_size)#별도의 임베딩 차원값을 정의하지 않고 이번에는 hidden_size를 임베딩된 토큰의 차원값으로 정의
self.encoder = nn.GRU(hidden_size, hidden_size)
self.decoder = nn.GRU(hidden_size, hidden_size)
self.project = nn.Linear(hidden_size, vocab_size)#디코더가 번역문의 다음 토큰을 예상해내는 작은 신경망을 추가
def forward(self, inputs, targets):
# 인코더에 들어갈 입력벡터 정의하고 인코더에 입력되는 원문('hello')를 구성하는 모든 문자를 임베딩 시킴
initial_state = self._init_state()
embedding = self.embedding(inputs).unsqueeze(1)
# embedding = [seq_len, batch_size, embedding_size]
# 인코더 (Encoder)
encoder_output, encoder_state = self.encoder(embedding, initial_state)
# encoder_output = [seq_len, batch_size, hidden_size]
# encoder_state = [n_layers, seq_len, hidden_size]
# 디코더에 들어갈 입력
decoder_state = encoder_state
decoder_input = torch.LongTensor([0])
# 디코더 (Decoder)
outputs = []
for i in range(targets.size()[0]):
decoder_input = self.embedding(decoder_input).unsqueeze(1)
decoder_output, decoder_state = self.decoder(decoder_input, decoder_state)
projection = self.project(decoder_output)
outputs.append(projection)
#티처 포싱(Teacher Forcing) 사용 (티처포싱 : 디코더 학습시 실제 번역문의 토큰을 디코더의 전 출력값 대신 입력으로 사용해 학습을 가속하는 방법 )
decoder_input = torch.LongTensor([targets[i]])
outputs = torch.stack(outputs).squeeze()
return outputs
def _init_state(self, batch_size=1):
weight = next(self.parameters()).data
return weight.new(self.n_layers, batch_size, self.hidden_size).zero_()
In [119]:
seq2seq = Seq2Seq(vocab_size, 16)
Seq2Seq(
(embedding): Embedding(256, 16)
(encoder): GRU(16, 16)
(decoder): GRU(16, 16)
(project): Linear(in_features=16, out_features=256, bias=True)
)
In [120]:
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(seq2seq.parameters(), lr=1e-3)
In [121]:
log = []
for i in range(1000):
prediction = seq2seq(x, y)
loss = criterion(prediction, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_val = loss.data
log.append(loss_val)
if i % 100 == 0:
print("\n 반복:%d 오차: %s" % (i, loss_val.item()))
_, top1 = prediction.data.topk(1, 1)
print([chr(c) for c in top1.squeeze().numpy().tolist()])
반복:0 오차: 5.596976280212402
['9', 'L', '\x98', 'L']
반복:100 오차: 2.069061756134033
['h', 'o', 'l', 'a']
반복:200 오차: 0.4633035957813263
['h', 'o', 'l', 'a']
반복:300 오차: 0.19558477401733398
['h', 'o', 'l', 'a']
반복:400 오차: 0.11498594284057617
['h', 'o', 'l', 'a']
반복:500 오차: 0.07863441854715347
['h', 'o', 'l', 'a']
반복:600 오차: 0.058321841061115265
['h', 'o', 'l', 'a']
반복:700 오차: 0.04549476131796837
['h', 'o', 'l', 'a']
반복:800 오차: 0.03673341125249863
['h', 'o', 'l', 'a']
반복:900 오차: 0.030412672087550163
['h', 'o', 'l', 'a']
In [122]:
plt.plot(log)
plt.ylabel('cross entropy loss')
plt.show()

▶ reference - 3분딥러닝 파이토치맛