NLP/구현

[Naver Sentiment Movie Corpus] 영화 리뷰 학습을 통한 감정 예측 구현

DongJin Jeong 2021. 1. 3. 23:34

0. 사용할 말뭉치(Corpus)

이번 구현에 사용할 말뭉치는 Naver Sentiment Movie Corpus v1.0(이하 NSMC)이다. 네이버 영화 리뷰에서 스크랩한 데이터이며, 모두 140자 미만의 길이고, 0(Negative)과 1(Positive)로 라벨링 되어있다. 자세한 정보는 아래 링크를 통해 확인할 수 있다.

# Naver sentiment movie corpus v1.0

1. 데이터 전처리

데이터를 학습시키기 이전에 데이터 전처리를 할 것이다. 우선 NSMC 데이터를 불러온다.

def load_data(filename):
    with open(filename, 'r', encoding='UTF8') as f:
        id, document, label = [list() for _ in range(3)]
        for line in f.read().split('\n'):
            try:
                id_, document_, label_ = line.split('\t')  # 데이터가 Tab으로 구분되어 있음.
            except:
                print(f"Err line: '{line}'")

            id.append(id_)
            document.append(document_)
            label.append(label_)

    return id[1:], document[1:], label[1:] # 열 이름 제거

train_id, train_document, train_label = load_data('data/ratings_train.txt')
test_id, test_document, test_label = load_data('data/ratings_test.txt')
train_label = list(map(int, train_label))
test_label = list(map(int, test_label))

1-1. 단어 토큰화(Word Tokenization) 및 품사 태깅(Part-of-Speech Tagging)

기계가 문장을 이해하기 쉽도록 단어 토큰화를 수행한다. Konlpy 라이브러리를 사용하여 한국어 문장을 토큰화할 수 있다. 결과는 (단어, 품사) 꼴의 튜플 형식으로 나오지만, 과정을 거쳐 "단어/품사" 형태로 변환되도록 하여 저장하였다.

# Open Korean Text, OKT
# [('이', 'Determiner'), ('것', 'Noun'), ('도', 'Josa'), ('되나욬', 'Noun'), ('ㅋㅋ', 'KoreanParticle')]
# >> > print(okt.pos(u'이것도 되나욬ㅋㅋ', norm=True))
# [('이', 'Determiner'), ('것', 'Noun'), ('도', 'Josa'), ('되나요', 'Verb'), ('ㅋㅋ', 'KoreanParticle')]
# >> > print(okt.pos(u'이것도 되나욬ㅋㅋ', norm=True, stem=True))
# [('이', 'Determiner'), ('것', 'Noun'), ('도', 'Josa'), ('되다', 'Verb'), ('ㅋㅋ', 'KoreanParticle')]

from konlpy.tag import Okt
okt = Okt()

tokenized_document_for_train = [[token+"/"+POS for token, POS in okt.pos(doc_, norm=True, stem=True)] for doc_ in train_document]
tokenized_document_for_test = [[token+"/"+POS for token, POS in okt.pos(doc_, norm=True, stem=True)] for doc_ in test_document]

1-2. 불용어 제거(Removing Stopwords)

이전 단계를 거치고 나니, 가장 많이 나온 토큰은 이해에 큰 도움을 주지 않는 구두점(Punctuation)과 조사(은, 는, 이, 가 등)가 많은 부분을 차지하였다. 따라서 미리 제거함으로써 학습 시 더욱 수월하게 진행될 수 있도록한다.

text = nltk.Text([token for token in all_tokens if not token.split("/")[-1] in ["Punctuation", "Josa"]], name='NSMC')

1-3. Count Vectorization

신경망 학습에 들어가기 전에, 토큰화 되었지만 그래도 아직 문자열 상태인 문장들을 숫자로 바꿔주는 작업이 필요하다. 이 때 쓰는 방법 중 하나가 Count Vectorization이다. Count Vectorization이란 사용자의 임의로 벡터의 길이를 정하고, 벡터의 각 원소마다 해당되는 단어를 결정한다. 그리고 그 단어의 출현빈도를 각 원소의 값으로 지니게 만드는 방법이다.

예시

["i", "love", "you", "do", "you", "love", "me"]
원소에 해당하는 단어 i love you do me
인덱스 0 1 2 3 4
1 2 2 1 1

이렇게 단어의 출현빈도만 신경쓰는 방법을 Bag of Words 기법이라고 한다. 이런 방법들은 단어들의 어순이 의미를 잃는다는 단점이 있다. 인덱스에 매칭시킬 단어를 정하는 방법은 일반적으로 가장 높은 출현빈도수를 지니는 단어부터 내림차순으로 선택하는 것이 일반적인 것 같다.

def count_vectorization(doc_, BOW):
    # doc_ is a sentence, not corpus!
    return [doc_.count(word) for word in BOW]

NUM_OF_BOW = 10000 # num of 'Bag of Words'

bag_of_words = [word for word, _ in text.vocab().most_common(NUM_OF_BOW)]
count_vectorized_train_document = [count_vectorization(doc_, BOW=bag_of_words) for doc_ in tokenized_document_for_train]
count_vectorized_test_document = [count_vectorization(doc_, BOW=bag_of_words) for doc_ in tokenized_document_for_test]

# Convert to float
# BackPropagation을 적용하기 위해서는 실수여야 함.
count_vectorized_train_document = torch.FloatTensor(count_vectorized_train_document)
count_vectorized_test_document = torch.FloatTensor(count_vectorized_test_document)
train_label = torch.FloatTensor(train_label)
test_label = torch.FloatTensor(test_label)

2. 신경망 학습(Multi-Layer Perceptron)

신경망 학습은 4개층의 Fully-Connected Layer로 구성되어 있다. 각 층의 활성화함수는 Relu를 사용하였으며, 마지막에는 시그모이드 함수를 사용하여 [0, 1] 사이의 값을 지니도록 하였다. (0 - Negative, 1 - Positive) 또한, 분류 문제임과 동시에 이진분류인 만큼 손실 함수는 교차 엔트로피 함수인 torch.nn.BCELoss()를 사용하였다. 옵티마이저는 RMSprop을 사용하였다. 구현에 사용한 라이브러리는 Pytorch이다.

신경망

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

class sentiment_classifier(nn.Module):
    def __init__(self):
        super(sentiment_classifier, self).__init__()
        self.fc1 = nn.Linear(NUM_OF_BOW, 2**10)
        self.fc2 = nn.Linear(2**10, 2**6)
        self.fc3 = nn.Linear(2**6, 2**2)
        # binary Classification
        self.fc4 = nn.Linear(2**2, 1)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        return torch.sigmoid(self.fc4(x))

학습

# Hyperparameter for Deep Learning
TRAIN_EPOCH = 10
BATCH_SIZE = 100
BATCH_SHUFFLE = True
LEARNING_RATE = 0.001
MOMENTUM = 0.9

# Create Iterator(DataLoader)
from torch.utils.data import TensorDataset, DataLoader
train_dataset = TensorDataset(count_vectorized_train_document, train_label)
train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=BATCH_SHUFFLE)

# Neural Network Training
net = sentiment_classifier().cuda()
loss_function = nn.BCELoss().cuda()
optimizer = optim.RMSprop(net.parameters(), lr=LEARNING_RATE, momentum=MOMENTUM)

if not os.path.isfile(WEIGHT_FILE):
    # Training
    for epoch in range(TRAIN_EPOCH):
        for batch_idx, train_data in enumerate(train_dataloader):
            x_train, y_train = train_data
            # H(x) 계산
            prediction = net(x_train.cuda())
            # prediction Size = (100, 1), y_train Size = (100)
            y_train = torch.unsqueeze(y_train, 1).cuda()

            # loss 계산
            loss = loss_function(prediction, y_train)

            #오차역전파
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            print(f"Epoch [{epoch + 1}/{TRAIN_EPOCH}] Batch [{batch_idx + 1}/{len(train_dataloader)}] loss: {loss.item()}")

3. 결과

torch.no_grad()
net.eval()
test_prediction = net(count_vectorized_test_document.cuda())
test_label = torch.unsqueeze(test_label, 1).cuda()

#Calculate Loss
test_loss = loss_function(test_prediction, test_label)

#Calculate Accuracy
accuracy = 0.0
for predict_, target in zip(test_prediction, test_label):
    if round(float(predict_[0])) == float(target[0]):
        accuracy += 1
accuracy /= test_label.size()[0]

print(f"Accuracy : {accuracy * 100}%")
print(f"loss : {test_loss.item()}")

결과는 아래와 같았다.

Accuracy : 84.27231455370892%
loss : 1.6345622539520264

학습 결과 약 84.3%의 확률로 리뷰의 호오(好惡)를 정확히 파악할 수 있었다.