import torch
import torchvision
import torch.nn.functional as F
from torch import nn, optim
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import numpy as np
import pandas as pd
import urllib.request
위 github 링크에서 네이버 영화 리뷰 데이터셋을 다운받을 수 있다. urllib.request를 이용해서 폴더 내로 영화 리뷰 모음을 txt파일로 저장한다. 파일 이름은 훈련 데이터, 테스트 데이터 각각 "ratings_train.txt"와 "ratings_test.txt"로 저장했다.
train_data = pd.read_table('ratings_train.txt')
test_data = pd.read_table('ratings_test.txt')
print('number of training data :', len(train_data))
train_data[:5]
다운받은 데이터셋을 pandas 데이터프레임으로 만든다. 앞으로 진행할 데이터 전처리에서 pandas에 내장되어 있는 유용한 함수를 계속하여 사용할 예정이다.
현재 총 존재하는 data는 number of training data : 150000으로 확인할 수 있다.
밑 사진에서 데이터프레임의 상위 5개 데이터를 확인할 수 있다. train_data 데이터프레임의 'id', 'document', 'label'로 이루어져있다. label은 '부정'과 '긍정'의 0과 1만 존재한다.
2) Data Preprocessing
train_data['document'].nunique(), train_data['label'].nunique()
train_data.drop_duplicates(subset=['document'], inplace=True)
print('total number of sample :', len(train_data))
.nunique()를 이용해서, 'document' 열과 'label' 에서 중복된 데이터가 있는지 확인한다.
(146182, 2) 15만 개의 데이터 중에서 'document' 열 중복된 데이터를 제거한 샘플의 개수가 146182개가 됨을 뜻한다. 'label'의 열은 0 혹은 1의 값만을 가지므로, 2가 출력되는 것이다.
.drop_duplicates(subset=['document'], inplace=True)를 이용해서, 'document' 열에서 중복되는 값을 삭제한다. (inplace=True는 원본 데이터를 변경함을 의미한다)
그리고, 총 샘플의 개수를 확인하면, total number of sample : 146183으로 확인할 수 있다.
다음은 'document' 열에서의 Null 값의 유무를 알아보자. 밑 사진과 같이, 'document' 열에서 Null 값이 하나 존재함을 알 수 있다.
.dropna(how = 'any')를 이용하여 Null 값을 가진 샘플을 제거한다. (.dropna는 Null 값이 있는 을 삭제하는 함수다. 그리고, how = 'any'는 한 행에서 Null이 한 개라도 있으면 그 행을 삭제함을 의미한다. how = 'all'로 하면 모든 값이 Null이 되야 한다)
그리고, print(train_data.isnull().values.any())로 남아있는 Null 값을 확인하면 False를 출력함으로써, Null 값을 전부 제거 했음을 확인한다.
print('total number of sample :', len( train_data))
print(train_data.groupby('label').size().reset_index(name = 'count'))
중복값과 Null 값을 전부 제거한 데이터의 개수는 146182개다.
그리고, label 0과 1의 개수를 세면, '긍정'을 뜻하는 리뷰가 근소하게 더 많음을 알 수 있다.
import re
text = 'Hi !!! are you .... interested studying ??!!@@ ,, AI ?..'
re.sub(r'[^a-z A-Z]', '', text)
네이버 영화 리뷰 데이터셋을 보면, 오직 한국어뿐만 아니라 수많은 문장부호와 기호들과 함께 이루어져있다. re 라이브러리를 import 하여 데이터프레임에서 한국어와 공백을 제외한 나머지를 제거해주도록 하자.
re.sub(r'[^a-z A-z]', ' ', text)에서 알파벳 소문자와 대문자, 공백을 제외한 나머지 text에서 날려준다. (^를 빼면, 알파벳과 공백을 모두 제거한 나머지를 보여준다. 그리고, r'[^a-z A-Z]'에서 r과 ' 사이를 띄어쓰기 하면 syntax error가 발생한다)
그렇다면, 한국어 같은 경우에는 범위를 어떻게 설정해야하는가. 이는 unicode를 참고하면 된다. 한글의 글자는 ㄱ~ㅎ, ㅏ~ㅣ로 이루어져있고, 한글의 완성형은 가~힣 까지 설정한다면, 모든 글자를 포함시킬 수 있게된다.
위 사진과 같이 띄어쓰기는 유지되었지만, 문장부호는 모두 제거되었음을 확인할 수 있다. 그렇다면, 영어나 특수문자를 이용한 리뷰는 현재 empty 값이 되었을 것이다.
일단 str.replace를 이용하여 white space 공백으로만 이루어진 데이터를 empty value로 만들어준다. 그리고, empty value를 모두 Null로 바꾸어준 후에 앞서 했던 과정을 반복해준다.
밑 사진과 같이 새로운 Null 값이 789개나 생성된 것을 볼 수 있다.
이와 같이 복잡한 과정을 감수하면서 KoNLPy를 설치한 이유는 불용어를 제거하기 위해서다. 불용어 혹은 Stop word는 분석에 있어서 큰 의미가 없는 것을 말한다. 위와 같은 경우에는 여러 조사들이 이 불용어에 해당될 수 있다.
okt = Okt()
okt.morphs('와 이 영화를 영화관에 내보냈네 차라리 뮤비를 만드는게 나을 것 같은데', stem=True)
KoNLPy에서 제공하는 형태소 분석기인 Okt를 이용해서 위 문장을 토큰화 해보자. 이 과정은 영어처럼 띄어쓰기 기준이 아닌 형태소를 기준으로 토큰화한다. (stem = True는 정규화를 의미한다. 위 예시 같은 경우에는 '이런'이 '이렇다'로 변환되었고, '만드는'이 '만들다'로 변환되었다)
tokenizer = Okt()
X_train = []
for sentence in train_data['document']:
temp_X = okt.morphs(sentence, stem=True)
temp_X = [word for word in temp_X if not word in stopwords]
X_train.append(temp_X)
print(X_train[:5])
위와 같이 Okt를 사용해서, 훈련용 데이터를 모두 토큰화 시켜준다. 이 과정은 꽤 시간이 소요된다. 그 이후에 상위 5개의 샘플을 출력해보자.
X_test = []
for sentence in test_data['document']:
temp_X = tokenizer.morphs(sentence, stem=True)
temp_X = [word for word in temp_X if not word in stopwords]
X_test.append(temp_X)
테스트용 데이터도 똑같이 토큰화 해준다.
4) Integer Encoding
vocab = FreqDist(np.hstack(X_train))
print('Size of Vocabulary : {}'.format(len(vocab)))
print('Frequency of 영화 : ', vocab['영화'])
print('Frequency of 재밌 : ', vocab['재밌'])
NLTK에서 지원하는 FreqDist()를 이용하여 단어 집합(Vocabulary)를 생성한다. 단어를 key, 그리고 단어에 대한 빈도수가 값(value)로 저장된다. vocab에 단어를 입력한 그 빈도수를 반환해준다.
총 단어 집합의 개수는 46503개이다. 그 중 '영화'라는 말은 44383번, '재밌'이라는 말은 2번 쓰였다.
threshold = 3
total_cnt = len(vocab) # 총 단어
rare_cnt = 0 # 등장 빈도수가 낮은 단어 개수
total_freq = 0 # 전체 단어 빈도수 총합
rare_freq = 0 # 등장 빈도수가 낮은 단어의 빈도수 총합
for key, value in vocab.items():
total_freq = total_freq + value
#단어의 등장 빈도수가 threshold보다 작으면
if(value < threshold):
rare_cnt = rare_cnt + 1
rare_freq = rare_freq + value
print('Size of Vocab :', total_cnt)
print('Rare Vocab which used under 3 times : ', rare_cnt)
print('Ratio of Rare Vocab among whole Vocab : ', (rare_cnt/total_cnt)*100)
print('Ratio of Rare Vocab Frequency among whole Vocab Frequency : ', (rare_freq/total_freq)*100)
vocab.items()에서 key와 value를 각각 불러와서 등장 빈도수가 3회 미만인 단어들이 데이터에서 어느정도 차지하는지 확인해보자.
등장 빈도가 2회 이하인 단어들은 단어 집합에서 약 54.8%를 차지하고 있다. 하지만, 실제로 등장 빈도로 차지하는 비중은 약 2% 밖에 되지 않는다. 그리하여, 이러한 단어들은 배제하고자 해도 자연어 처리에 있어서 무관할 것 같다.
전체 단어 집합의 개수에서 등장 빈도가 2회 이하인 단어들의 개수를 뺀 수를 새로운 단어 집합의 개수로 한다. 그리고, 등장 빈도 상위 단어들로 정렬하여 새로운 단어 집합을 만든다.
그러면, 현재 총 단어 집합의 개수는 21029개임을 확인할 수 있다.
이제, 본격적으로 정수 인코딩(Integer Encoding)을 진행하고자 한다.
컴퓨터는 인간처럼 단어 자체의 의미를 받아 들일 수 없다. 즉, 이러한 단어 집합을 다루기 위해서는 각 단어에 고유한 정수를 부여해서, 숫자의 형태로 변환해주어야한다.
word_to_index = {word[0]: index + 2 for index, word in enumerate(vocab)}
word_to_index['pad'] = 1
word_to_index['unk'] = 0
enumerate()는 list, tuple, dictionary, string 등을 입력으로 받아 인덱스를 순차적으로 함께 반환한다는 특징이 있다. 인덱스 0과 1은 다른 용도로 남겨두고 나머지 단어들은 2부터 순차적으로 인덱스를 부여한다.
encoded = []
for line in X_train:
temp = []
for w in line:
try:
temp.append(word_to_index[w])
except KeyError:
temp.append(word_to_index['unk'])
encoded.append(temp)
print(encoded[:10])
그럼 이제 단어 집합에서 만들어진 단어와 인덱스를 토대로, 훈련용 데이터에 존재하는 각 단어를 고유한 정수로 변환해주는 작업을 거치려고 한다. 단어 집합에 존재하지 않는 경우, 'unk'로 대체되며, 'unk'의 인덱스를 부여하도록 만들었다. 그럼 밑에 있는 사진과 같은 결과를 확인할 수 있다.
5) Padding
패딩은 길이가 서로 다른 리뷰들을 모두 동일한 길이로 바꾸어주는 작업이다. 앞서 word_to_index['pad'] = 1은 패딩을 위해 추가한 토큰이었다. 패딩은 정해진 길이만큼으로 모든 샘플들의 길이를 맞춰주는 것이다. 하지만, 정해진 길이보다 길이가 짧은 샘플은 'pad' 토큰을 추가하면서 길이를 맞춘다.
max_len = max(len(n) for n in encoded)
print('max length of Review : ', max_len)
print('min length of Review : ', min(len(n) for n in encoded))
print('average length of Review : ', (sum(map(len, encoded))/len(encoded)))
max length of Review : 68
min length of Review : 0
average length of Review : 10.95426877497541
for line in encoded:
if len(line) < max_len:
line += [word_to_index['pad']] * (max_len - len(line))
가장 길이가 긴 리뷰의 길이인 68로 모든 리뷰의 길이를 통일시켜준다.
max length of Review : 68
min length of Review : 68
average length of Review : 68.0