카테고리 없음

파이토치 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)  # 레이블 값을 01로 변환 data.sub_(1) 함수는 y의 모든값에서 1씩 빼서 10으로 21로 만들어 준다 
        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분딥러닝 파이토치맛