딥러닝 감성분석(텍스트 분류)

본 예제는 감성분석 혹은 텍스트 분류라고 할 수 있습니다. 감성분석이란 쉽게 말해서 어떤 글이 찬성/반대, 좋음/보통/싫음, 긍정/중립/부정 등 어떠한 polarity를 나타내는지에 대한 상태를 분석하는 것입니다.

소비자의 감성과 관련된 텍스트 정보를 자동으로 추출하는 텍스트 마이닝(Text Mining) 기술의 한 영역. 문서를 작성한 사람의 감정을 추출해 내는 기술로 문서의 주제보다 어떠한 감정을 가지고 있는가를 판단하여 분석한다. 주로 온라인 쇼핑몰에서 사용자의 상품평에 대한 분석이 대표적 사례로 하나의 상품에 대해 사용자의 좋고 나쁨에 대한 감정을 표현한 결과이다.
[네이버 지식백과] 감성분석
[Sentimental Analysis , 感性 分析]

import torch
import torch.nn as nn
import torch.optim as optim

import random
import numpy as np

감성분석에 사용한 데이터는 네이버에서 공개한 영화 평점 정보입니다. 해당 데이터는 아래 링크에서 받을 수 있습니다.
https://github.com/e9t/nsmc

본 예제는 평점 데이터의 전체를 사용하지 않고 RNN에서 many-to-one 형태의 감성분석 모델의 개념을 위해 일부 데이터만 사용했습니다. 또 그중에서 문장의 길이가 30 미만인 데이터만 사용했습니다.

sentence = []
file = open("./data/ratings_test.txt", "r")
for i in range(1000):
    line = file.readline()
    arr = line.split('\t')
    if len(arr[1]) < 30:
        sentence.append(arr[1]+'|'+arr[2].replace('\n',''))

file.close()

sentences = sentence[1:]
len(sentences) #560

단어셋 생성을 위해서 Vocab 클래스를 생성합니다. <unk>는 데이터의 Sequence Length를 맞춰주기 위해서 빈 데이터를 채우기 위해 생성한 코드입니다.

데이터의 형식은 아래와 같습니다. 분석에 필요한 데이터는 텍스트 부분과 뒤 이어 나오는 0,1의 데이터입니다. 0은 부정적인 평가이며 1은 긍정적인 평가입니다.

2541728	아찔한 사랑 줄다리기???	0
9648521	재미있었다! 또봐야징ㅎ	1
9911421	중간에 화면이 좀 끊기는것 빼곤 넘 좋았어요~	1
3608055	이 영화를 말하는데 긴 단어는 필요없다. 재수없는 졸작 이거면 충분하다.	0
class Vocab:
    def __init__(self):
        self.vocab2index = {'<unk>':0}
        self.index2vocab = {0:'<unk>'}
        self.vocab_count = {}
        self.n_vocab = len(self.vocab2index)

    def add_vocab(self, sentence):
        for word in sentence:
            if word not in self.vocab2index:
                self.vocab2index[word] = self.n_vocab
                self.vocab_count[word] = 1
                self.index2vocab[self.n_vocab] = word
                self.n_vocab += 1
            else:
                self.vocab_count[word] += 1

단어는 형태소 분석을 사용합니다. 형태소 분석기로 konlpy를 사용합니다.

vo = Vocab()

from konlpy.tag import Okt
okt = Okt()

for sentence in sentences:
    vo.add_vocab(okt.morphs(sentence.split('|')[0]))

Model

input_size = vo.n_vocab
hidden_size = 2

class SentimentModel(nn.Module):
    
    # (batch_size, n, ) torch already know, you don't need to let torch know
    def __init__(self,input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        
        self.embedding = nn.Embedding(self.input_size, 250)
        
        self.rnn = nn.LSTM(
            input_size = 250, 
            hidden_size = 100, 
            num_layers = 4, 
            batch_first = True,
            bidirectional = False
        )
        
        self.layers = nn.Sequential(
            nn.ReLU(),
            nn.Linear(100,50),
            nn.Linear(50,25),
            nn.Linear(25,self.hidden_size),
            #nn.Sigmoid()
        )
        
        self.softmax = nn.LogSoftmax(dim=0)
         
        
    def forward(self, x):
        x = self.embedding(x) 
        y,_ = self.rnn(x)
        y = self.layers(y)
        return self.softmax(y[:,-1,:])
    
model = SentimentModel(input_size, hidden_size)

생성한 모델 정보를 출력해보면 아래와 같습니다.
간략히 살펴보면 입력 데이터 1751을 250 차원으로 Embedding 합니다. 그리고 Embedding의 마지막 값을 LSTM의 입력값으로 사용합니다. LSTM은 250을 입력 받아서 100개의 정보를 출력하는 4개층의 레이어 구조로 되어 있습니다.

입력 받은 데이터는 Linear 모델을 통과하며 차원 정보를 낮춰주고 마지막에는 이 문장의 값이 “긍정” 혹은 “부정”을 나타내는 2개의 값을 최종적으로 출력합니다.

최종 output 데이터는 모두 사용하지 않고 각 배치 사이즈의 마지막 Sequence(or Time-Step) 데이터만 사용합니다. 해당 정보는 (batch_size * hidden_vector)로 표시할 수 있습니다. 이렇게 만들어진 정보를 LogSoftmax를 통과 시키고 예측값을 구해냅니다.

이렇게 구해진 예측값과 정답의 차이 즉, Loss를 계산하고 이 값을 줄이는 과정을 수행하는 학습을 수행합니다.

SentimentModel(
  (embedding): Embedding(1751, 250)
  (rnn): LSTM(250, 100, num_layers=4, batch_first=True)
  (layers): Sequential(
    (0): ReLU()
    (1): Linear(in_features=100, out_features=50, bias=True)
    (2): Linear(in_features=50, out_features=25, bias=True)
    (3): Linear(in_features=25, out_features=2, bias=True)
  )
  (softmax): LogSoftmax()
)

입력 문장의 최대 길이는 30으로 정하고 길이가 30이 안되는 문장에는 <unk> 값을 채워줍니다.

def tensorize(vocab, sentence):
    idx = [vocab.vocab2index[word] for word in okt.morphs(sentence)]
    #return torch.Tensor(idx).long().item()
    return idx

ten = []
y_data = []
for sentence in sentences:
    tmp = tensorize(vo,sentence.split('|')[0])
    tmp_zero = np.zeros(30)
    
    for i,val in enumerate(tmp):
        tmp_zero[i] = val
    
    ten.append(tmp_zero)
    y_data.append(float(sentence.split('|')[1]))
    
x_data = torch.Tensor(ten).long()
y_data = torch.Tensor(y_data).long()

Training

# loss & optimizer setting
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr=0.01)

hist = []

# start training
for epoch in range(201):
    model.train()
    outputs = model(x_data)
    
    loss = criterion(outputs, y_data)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    hist.append(loss.item())
    
    if epoch%20 == 0:
        print(epoch, loss.item())
        #result = outputs.data.numpy().argmax(axis=2)
        #result_str = ''.join([char_set[c] for c in np.squeeze(result)])
        #print(i, "loss: ", loss.item(), "\nprediction: ", result, "\ntrue Y: ", y_data, "\nprediction str: ", result_str,"\n")
    
    

Evaluate

with torch.no_grad():

    prediction = model(x_data)
    correct_prediction = torch.argmax(prediction, 1) == y_data
    
    accuracy = correct_prediction.float().mean()
    print('Accuracy:', accuracy.item()) # 정확도 표시

학습의 Loss 값을 표시해보면 아래와 같습니다.

import matplotlib.pyplot as plt

plt.plot(hist)
plt.show()

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다