저번 글에 이어 <4> 자연어 생성부터 진행하고자 한다.
<4> 자연어 생성
언어 생성이란 무엇일까? 사람이 언어를 생성하는 과정을 생각해보자. 일반적으로 글을 쓰거나 말을 할 때 어떠한 주제에 대한 목적 의식을 가지고 언어에 맞는 문법과 올바른 단어를 사용해 문장을 생성한다. 신문 기사가 될 수도 있고, 상대방과의 대화, 문장 요약 등 언어를 활용해 우리는 서로 "소통(Communication)"하면서 살아간다. 그렇다면 컴퓨터가 상대방의 대화를 이해하고, 글도 쓴다면 어떨까?
실제로 미국 캐리포니아 LA Times에서는 "Quakebot"이 인공지능으로 지진이 난 지 3분만에 기사를 작성했다.
하지만 이런 사실 기반의 기사를 제외하고 일반적으로 감정 및 논리력이 들어가는 기사를 작성하는 데는 아직까지 한계가 있어서 보조 도구로 쓰이고 있는 상태이며, 관련 영역에 해당하는 많은 데이터가 필요하다.
언어 생성은 사람의 대화를 최대한 많이 수집해서 대화를 배우게 하고 지속적으로 평가하는 과정을 반복해서 특정 목적에 맞는 텍스트를 생성하는 것이 주 목적이다. 이 분야는 아직 많은 연구와 개척이 필요한 분야다. 현재 언어 생성 기술을 활용해서 성공적으로 활용하는 분야 중 하나는 기계번역 분야인데, 대표적으로 네이버의 파파고와 구글의 구글 번역기가 있다.
<5> 기계 이해
기계 이해(Machine Comprehension)는 기계가 어떤 텍스트에 대한 정보를 학습하고 사용자가 질의(Question)을 던졌을 때 그에 대해 응답하는 문제다. 다시 말하자면 기계가 텍스트를 이해하고 논리적 추론을 할 수 있는지 데이터 학습을 통해 보는 것이다. 다음 예시를 보자.
Text:
자연어 처리 또는 자연 언어 처리는 인간의 언어 현상을 컴퓨터와 같은 기계를 이용해서 모사 할 수 있도록 연구하고 이를 구현하는 인공지능의 주요 분야 중 하나다. 자연 언어 처리는 연구 대상이 언어이기 때문에 당연하게도 언어 자체를 연구하는 언어학과 언어 현상의 내적 기재를 탐구하는 언어 인지 과학과 연관이 깊다. 구현을 위해 수학적 통계적 도구를 많이 활용하며 특히 기계학습 도구를 많이 사용하는 대표적인 분야이다. 정보검색, QA 시스템, 문서 자동 분류, 신문기사 클러스터링, 대화형 Agent 등 다양한 응용이 이루어지고 있다.
Question:
자연어 처리는 어느 분야 중 하나인가?
예를 들어, 기계한테 위와 같은 텍스트에 대한 내용을 학습시켰다고 하자. 그리고 "자연어 처리는 어느 분야 중 하나인가?" 라는 텍스트와 관련이 있는 질문을 기계에게 한다. 그러면 기계는 위 텍스트의 내용을 토대로 추론해서 이에 대한 응답을 텍스트 내에서 하거나 정답 선택지를 선택하는 방식으로 답하게 된다.
기계 이해 분야는 자연어 처리 기술에 대한 개념이 총망라된 학습 태스크여서 앞에서 알아본 다른 자연어 처리 태스크와 비교하면 어렵고, 더욱 복잡한 모델링을 필요로 한다. 기계 이해는 아직 연구 단계에 있고 QA(Question Answering) 태스크와 관련된 여러 대회를 통해 많은 모델들이 제시되고 있다. 이러한 대회 중 대표적으로 SQuAD(Stanford Question Answering Dataset)를 사례로 들 수 있다.
데이터셋
기계 이해 태스크에서는 대체로 자연 언어를 이해하는 과제에서 기계가 텍스트 내용에 대해 추론을 잘 하는지 파악하는 목적에서 학습하게 된다. 그렇기 때문에 이 태스크를 QA 태스크라고 부르기도 하며 보통 Question Answering(QA) 형태의 데이터셋을 활용해 기계에게 학습하게 한다. 이러한 데이터셋은 위키피디아나 뉴스 기사를 가지고 데이터를 구성하며 대체로 텍스트와 지문, 정답 형태로 구성돼 있다.
그 중 하나의 예시로는 bAbI 데이터셋이 있는데, 페이스북 AI 연구팀에서 기계가 데이터를 통해 학습해서 텍스트를 이해하고 추론하는 목적에서 만들어진 데이터셋이다. 총 20가지 부류의 질문 내용으로 구성돼 있으며 질문 데이터셋 구성은 다음과 같다.
위 그림에서 보여지는 것 처럼 bAbI 데이터셋은 시간 순서대로 나열된 텍스트 문장 정보와 그에 대한 질문으로 구성되어 텍스트 정보에 대해 질문을 던지고 응답하는 형태다.
bAbI의 경우 기계 모델이 사람이 문제를 풀었을 때보다 더 좋은 점수를 내면서 이미 해결된 부분으로 알려져 있다.
<6> 데이터 이해하기
4장부터는 본격적으로 캐글 도전 및 모델링을 진행하게 된다. 처음 캐글 문제를 풀기 시작할 때 많은 사람들은 모델을 만들고 훈련 후에 성능을 평가하고, 생각보다 성능이 안 나온다면 모델에 문제가 있다고 판단하고 다른 모델을 사용한다. 이처럼 모델에 문제가 있는 경우도 있지만 우선적으로 해당 문제를 잘 해결하기 위해서는 데이터 이해가 선행되어야 한다. 이러한 과정을 탐색적 데이터 분석(EDA: Exploratory Data Analysis)라 한다. 이러한 과정에서 생각하지 못한 데이터의 여러 패턴이나 잠재적인 문제점 등을 발견할 수 있다.
그리고 문제를 해결하기 위한 모델에 문제가 없더라도 데이터마다 적합한 모델이 있는데 해당 모델과 데이터가 잘 맞지 않으면 좋은 결과를 얻을 수 있다. 즉, 아무리 좋은 모델이더라도 데이터와 궁합이 맞지 않는 모델이라면 여러 가지 문제에 직면하게 될 것이다.
그렇다면 EDA는 어떻게 진행되는가? 간단하게 얘기하면 정해진 틀 없이 데이터에 대해 최대한 많은 정보를 뽑아내면 된다. 데이터에 대한 정보란 데이터의 평균값, 중앙값, 최솟값, 최대값, 범위, 분포, 이상치(Outlier) 등이 있다. 이러한 값들을 확인하고 히스토그램, 그래프 등의 다양한 방법으로 시각화하면서 데이터에 대한 직관을 얻어야 한다.
데이터를 분석할 때는 분석가의 선입견을 철저하게 배제하고 데이터가 보여주는 수치만으로 분석을 진행해야하며, 이러한 데이터 분석 과정은 모델링 과정과 서로 상호 작용하면서 결과적으로 성능에 영향을 주기 때문에 매우 중요한 작업이다.
다음 그림은 EDA의 전체적인 흐름도이며, 서로의 결과에 직접적으로 영향을 줄 수 있다는 것을 확인할 수 있다.
간단한 실습을 통해 데이터 분석에 대해서 자세히 알아보자. 실습에 사용한 데이터는 영화 리뷰 데이터로, 리뷰와 그 리뷰에 해당하는 감정(긍정, 부정) 값을 가지고 있다.
import os
import re
import pandas as pd
import tensorflow as tf
from tensorflow.keras import utils
data_set = tf.keras.utils.get_file(
fname='imdb.tar.gz',
origin='http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz',
extract=True)
텐서플로우의 케라스 모듈의 get_file 함수를 통해 IMDB 데이터를 가져온다. 첫 번째 인자인 fname은 다운로드한 파일의 이름을 재지정하는 부분이고, 두 번째 인자인 origin은 데이터가 위치해 있는 URL을 입력하면 해당 URL에서 데이터를 다운로드하게 된다. URL의 마지막 부분은 확장자인데 확장자가 tar.gz라는 것은 압축 파일임을 의미한다. 세 번째 인자인 extract를 통해 다운로드한 압축 파일의 압축 해제 여부를 지정할 수 있다. 현재 코드에서는 True로 되어있어 압축 해제를 시행한다.
2장의 앞부분에서 했던 것 처럼, 데이터 분석 과정을 쉽게 하기 위해서는 Pandas로 데이터를 불러오는 것이 좋은데, 현재 IMDB 데이터의 경우, 판다스로 가져올 수 없는 상황이다.
압축이 풀린 데이터를 확인하면 다음과 같다.
등 모든 데이터가 디렉터리 안에 txt 파일 형태로 있어서 판다스의 DataFrame을 만들기 위해서는 변환 작업을 진행해야 한다. 변환 작업에는 두 가지 함수가 필요한데, 하나는 각 파일에서 리뷰 텍스트를 불러오는 함수이며, 다른 하나는 각 리뷰에 해당하는 라벨값을 가져오는 함수다. 우선 첫 번째 함수부터 정의해보자.
def directory_data(directory):
data = {}
data['review'] = []
for file_path in os.listdir(directory): # directory의 모든 파일 리스트에서 하나하나 꺼냄
with open(os.path.join(directory, file_path), 'r', encoding = 'UTF-8') as file: # 디렉토리/파일 형식으로 만들어줌
data["review"].append(file.read()) # 파일을 읽어서 data['review']에 하나씩 집어 넣어줌.
return pd.DataFrame.from_dict(data) # data를 DataFrame으로 만들어줌.
먼저, data 라는 이름으로 딕셔너리 객체를 만들어주고, data에 'review' 키를 생성하고 거기에 값으로 빈 리스트를 생성한다. (딕셔너리는 key : value 형식이다.)
os.listdir(directory) 에서 사용된 os.listdir은 해당 directory에 포함된 파일 리스트를 가져오는 함수이다.
예를 들어, fruit 라는 폴더에 apple.txt, pineapple.txt, mango.txt가 들어있다면 이 파일 리스트를 가져오는 것이다.
for file_path in os.listdir(directory)는 이러한 파일 리스트에서 파일 이름 하나하나를 꺼내는 코드로 생각하면 된다.
다음줄의 os.path.join(directory, file_path)는 디렉토리와 파일이름을 이어주는 함수이다.
예를 들어, os.path.join("C:\Temp", "pos") 를 작동시키면 C:\Temp\pos가 되는 것이다. 따라서 디렉토리 안에 있는 파일의 전체 경로를 표현할 수 있게 된다.
with open(... , "r") as file: 코드는 ...에 해당하는 경로에 있는 파일을 읽어서 이를 file 이라는 이름의 객체로 저장하는 코드가 된다. with를 사용하면 따로 file.close()를 하지 않아도 되는 장점이 있다. 'r'은 read이며, 파일에 쓰는 경우는 'w'(write)를 사용한다.
그 다음줄에 data['review'].append(file.read())는 data['review'] 에 읽어들인 file을 append(추가) 하는것이다.
마지막 pd.DataFrame.from_dict(data)은 data가 딕셔너리 형태였는데 이를 이용해서 DataFrame으로 만드는 것이다.
전체 코드를 작동하게되면 대략 이런 형식으로 DataFrame이 나오게 될 것이다.
그림 4. directory_data 함수 사용 예시
data라는 이름의 딕셔너리 객체에서 review라는 키를 지정했었는데, 이것이 DataFrame 형태로 변환되었을때는 Column의 이름이 된다.
DataFrame 특성상 모든 자료 앞에는 index가 붙게되므로 위 그림과 같은 형태가 된다.
(리뷰는 임의로 넣었습니다.)
다음 함수를 살펴보자.
def data(directory):
pos_df = directory_data(os.path.join(directory, "pos"))
neg_df = directory_data(os.path.join(directory, "neg"))
pos_df["sentiment"] = 1
neg_df["sentiment"] = 0
return pd.concat([pos_df, neg_df])
pos_df 부분의 경우, 앞에서 언급된 os.path.join을 이용하여 directory\pos에 해당하는 데이터들을 DataFrame 형태로 저장하게된다.
데이터가 들어있는 폴더를 살펴보면, pos와 neg로 나뉘어져있는데, 이는 label이 긍정인 것과 부정인 것을 나누어 폴더를 구성한 것이다.
neg_df도 마찬가지.
pos_df["sentiment"] = 1은 pos_df인 데이터에 sentiment라는 column을 추가하고 거기에 1 값을 집어넣는다. (즉 긍정 자료는 감정이 1)
neg_df["sentiment"] = 0은 neg_df인 데이터에 sentiment에 0 값을 집어넣는다.
마지막의 pd.concat([pos_df, neg_df])은 pos_df 데이터와 neg_df를 위 아래로 합치는 함수이다. (R을 다뤄보신분이 있다면 R에서 rbind()에 해당한다고 보시면 될듯하다.)
위에서 정의한 data를 적용하면 다음과 같다.
위와 같이 위에는 긍정자료, 아래는 부정자료로 구성된 데이터셋이 완성되게 된다.
앞에 설명했던 두 함수를 호출해서 우리가 처음에 받았던 데이터셋을 Pandas의 DataFrame으로 반환받아보자.
train_df = data(os.path.join(os.path.dirname(data_set), 'aclImdb', 'train'))
test_df = data(os.path.join(os.path.dirname(data_set), 'aclImdb', 'test'))
만들어낸 DataFrame의 결과를 확인해보자.
train_df.head()
위에서 directory_data와 data를 정의할때 대략 설명했던것과 같은 출력이 예상대로 나왔다.
이제 이 DataFrame으로부터 리뷰 문장 리스트를 가져와보자.
reviews = list(train_df['review'])
reviews에는 각 문장을 리스트 형태로 담고 있다. 이 문장들을 단어 기준으로 토크나이징하고, 문장마다 토크나이징된 단어의 수를 저장하고 그 단어들을 붙여서 알파벳의 전체 개수를 저장해보자.
# 공백을 기준으로 쪼개서 토크나이징함.
tokenized_reviews = [r.split() for r in reviews]
# 토크나이징된 리스트에 대한 각 길이를 저장
review_len_by_token = [len(t) for t in tokenized_reviews]
# 토크나이징된 것을 다시 붙여서 음절 길이 저장
review_len_by_eumjeol = [len(s.replace(' ','')) for s in reviews]
첫번째인 tokenized_reviews는 reviews를 .split()으로 쪼개는데 이는 .split(' ')와 동일하다. 즉 공백을 기준으로 쪼개서 단어 하나하나를 리스트의 요소로 만들어낸다.
예를 들면, "고양이가 지붕 위를 올라간다." 라는 문장에 .split()를 적용하면 ['고양이가', '지붕', '위를', '올라간다.']로 만들어지게 된다.
두번째인 review_len_by_token은 위에서 토크나이징한 리스트의 요소들(즉 리뷰에 사용된 각각의 단어들)의 길이를 저장한다.
예를 들어서 "고양이가 지붕 위를 올라간다." 를 기준으로 적용하면 [4, 2, 2, 4]가 될 것이다.
세번째인 review_len_by_eumjeol은 reviews에 있는 공백을 ''로 대체한 후, 이것의 길이를 측정하게 되는데, 예를 들면 "고양이가 지붕 위를 올라간다."에 s.replace(' ','')를 적용하면 "고양이가지붕위를올라간다."가 되고 이것의 길이(len)를 측정해서 리스트에 저장하는 것이다.
위와 같이 만드는 이유는 문장에 포함된 단어와 알파벳의 개수에 대한 데이터 분석을 수월하게 만들기 위해서다.
데이터 분석을 위한 사전 준비 작업이 완료됐으니 실제로 데이터 분석을 진행해본다.
import matplotlib.pyplot as plt
plt.figure(figsize = (12, 5))
plt.hist(review_len_by_token, bins = 50, alpha = 0.5, color = 'r', label = 'word')
plt.hist(review_len_by_eumjeol, bins = 50, alpha = 0.5, color = 'b', label = 'alphabet')
plt.yscale('log', nonposy= 'clip') # nonposy = 'clip' => non-positive 값을 아주 작은 값으로 자름.
plt.title("Review Length Histogram")
plt.xlabel("Review Length")
plt.ylabel("Number of Reviews")
plt.legend()
figsize는 (가로, 세로) 형태의 튜플로 입력하며, 입력한 사이즈로 그림을 만들게 된다.
plt.hist는 히스토그램을 그리게 되며, bins는 출력할때 히스토그램의 칸 개수를 말한다. alpha는 그래프 색상 투명도이며, color는 색을 의미한다. 'r'은 red, 'b'는 blue다. label은 각각 그래프에 라벨을 다는 것이다.
plt.yscale은 y 좌표의 스케일을 지정하는것으로, log 스케일로 지정되어 있으며, nonposy = 'clip'으로 되어 있는 경우 non-positive 값을 아주 작은 값으로 자른다.
plt.legend()는 책에는 없었으나 그림 자체를 더 잘 표현하기 위해 내가 추가하였다. legend는 범례를 의미하며, 이를 출력하면 그래프를 보기가 더 수월하기 때문에 추가하였다.
범례는 1시 방향에 띄워져 각각의 그래프가 어떤 의미를 띄는지를 보여준다.
빨간색 히스토그램인 word는 리뷰 하나당 단어가 몇개 있는지를 보여주는 것이고
파란색 히스토그램인 alphabet은 리뷰 하나당 알파벳 개수가 몇개 있는지를 보여주는 히스토그램이다.
다음은 데이터 분포에 대한 통계치를 알아본다.
import numpy as np
print("문장 최대 길이: {}".format(np.max(review_len_by_token)))
print("문장 최소 길이: {}".format(np.min(review_len_by_token)))
print("문장 평균 길이: {:.2f}".format(np.mean(review_len_by_token)))
print("문장 길이 표준편차: {:.2f}".format(np.std(review_len_by_token)))
print("문장 중간 길이: {}".format(np.median(review_len_by_token)))
print("제1사분위 길이: {}".format(np.percentile(review_len_by_token, 25)))
print("제3사분위 길이: {}".format(np.percentile(review_len_by_token, 75)))
문장 최대 길이, 최소 길이, 평균 길이, 길이의 표준편차, 중간 길이, 1 / 3사분위 길이 등을 확인할 수 있는 코드이다.
사분위수의 경우 0 ~ 100 스케일로 되어 있다.
다음으로는 박스 플롯을 이용하여 데이터를 시각화해본다. 박스 플롯은 직관적인 시각화를 제공하는 특징이 있다.
plt.figure(figsize = (12, 5))
plt.boxplot([review_len_by_token], labels = ['token'], showmeans = True)
plt.boxplot은 박스 플롯을 만들어내는 함수이며, showmeans = True로 지정 시 평균값을 표현해주게 된다.
그려진 박스플롯에 대한 상세한 설명은 다음과 같다.
박스플롯을 확인해 보면 전체적인 데이터 분포를 확인할 수 있으며, 특히 이상치가 심한 데이터를 확인할 수 있다는 장점이 있다.
이상치가 심하게 되면 데이터의 범위가 너무 넓어 학습이 효율적으로 이루어지지 않기 때문에 이를 확인하는 것이 중요하다.
다음으로는 워드 클라우드를 이용하여 데이터를 시각해보자.
from wordcloud import WordCloud, STOPWORDS
import matplotlib.pyplot as plt
%matplotlib inline
wordcloud = WordCloud(stopwords = STOPWORDS, background_color = 'black', width = 800,
height = 600).generate(' '.join(train_df['review']))
plt.figure(figsize = (15, 10))
plt.imshow(wordcloud) # 그림 그리기
plt.axis("off") # 축 끄기
plt.show()
stopword 는 한국어로 불용어 라고 말하며, 자주 등장하지만 분석을 하는 것에 있어서는 큰 도움이 되지 않는 단어들을 말합니다. 예를 들면, I, my, me, over, 조사, 접미사 같은 단어들은 문장에서는 자주 등장하지만 실제 의미 분석을 하는데는 거의 기여하는 바가 없습니다.
wordcloud 안에 STOPWORDS로 이미 불용어들이 저장되어 있으며, 이를 이용하여 불용어를 제외한 나머지 단어들만 워드 클라우드에 표현하게 된다.
' '.join(train_df['review'])는 train_df에 저장되어 있는 리뷰들을 ' '만큼 띄워서 연결하는 코드라고 생각하면 될거같다.
예를 들어, ' '.join('apple', 'bear') 를 적용하면 'apple bear'가 되는 것이다.
plt.imshow()는 그림을 그리는 함수이며, plt.show()는 실제로 그림을 나타내는 함수라고 보면 된다.
plt.axis()는 축을 설정하는 함수인데, 이 경우는 "off"로 지정하여 축을 따로 나타내지 않는것으로 보인다.
실제 코드를 실행한 결과 다음과 같은 워드 클라우드를 볼 수 있다.
워드클라우드에서 특히 br이 좀 많은데, 이는 HTML 태그를 아직 전처리 하지 않았기 때문에 그런 것으로 보인다.
마지막으로는 긍정, 부정의 분포를 확인해본다.
import seaborn as sns
import matplotlib.pyplot as plt
sentiment = train_df['sentiment'].value_counts()
fig, axe = plt.subplots(ncols = 1)
fig.set_size_inches(6, 3)
sns.countplot(train_df['sentiment'])
train_df['sentiment'].value_counts()를 적용하면 train_df['sentiment']에 들어있는 값들을 종류별로 정리해 표로 나타내준다.
예를 들어서, 자료에 긍정이 100개, 부정이 200개 있으면 긍정 100 부정 200 이런식으로 표에 나타나는 것이다.
다음줄에 사용된 plt.subplots()의 경우 한번에 여러 그래프를 그릴때 설정하기가 쉬워서 사용되는 코드이다.
이때 등장하는 fig는 전체 plot을 의미하며, subplot이 몇개가 그려지던지 상관없이 그것을 담는 전체 그림을 의미한다.
axe는 전체 그림 중에 낱개 plot 하나하나를 의미한다.
여기에는 nrows와 ncols 라는 인자들을 설정할 수 있는데, 이를 설정해 subplot을 몇개나 그릴지 지정할 수 있게 된다.
마지막줄의 sns.countplot(train_df['sentiment])의 경우, 각 category 별로 data가 얼마나 있는지 표시할 수 있는 plot을 그리는 코드가 된다. 특히 이 코드는 자료가 DataFrame 일때만 사용가능하다는점에 유의하여야 한다.
위 코드를 실행시키면 다음과 같은 그림을 얻을 수 있다.
sentiment가 0인 것과 1인 것이 몇개씩 있는지를 나타내주는 그래프를 얻을 수 있었다.
둘다 개수는 12500개 이며, 긍정 부정 데이터의 균형이 아주 좋은 경우라고 볼 수 있다.
지금까지의 내용이 Chapter 3의 마지막 부분까지 다룬 내용이 된다.
이론적인 부분들은 어느정도 잘라내어 생략하였고, 코드가 있는 부분은 최대한 한줄 한줄 풀어서 설명하였다.
책에서는 한줄 한줄 세세하게 설명해주지 않다보니, 나 또한 코드에 사용된 함수들을 하나하나 찾아보면서 공부할 수 있는 기회가 되었고
혹시나 이 책을 공부하시다가 모르는 코드가 있어서 검색을 해 나의 블로그에 누군가가 들어와서 확인해주신다면 이 또한 글을 쓴 보람이 생길 것 같다.
다음 글에서는 본격적으로 Kaggle 데이터를 통해 텍스트 분류를 진행해보려고 한다.
마지막으로 체크해보아야 할 것들을 정리해보자.
첫번째: 자연어 생성이란 무엇이고, 자연어 생성 기술을 활용하여 성공한 분야는 무엇인가?
두번째: 기계 이해란 무엇이고, 보통 어떤 형태의 데이터셋을 이용하여 학습하는가?
앞으로도 코드에 관련된 부분은 뭐 외우거나 하기는 어려울듯하여 따로 문제로 정리하지는 않을 예정이다.
한줄 한줄 최대한 이해해가면서 코드를 보는 것 그 자체가 큰 도움이 되는듯하여.... (물론 외우는게 어느정도 필요할 것 같지만 아직 나도 초보라서 뭘 외워야하는지 모름...)
'딥러닝 & 머신러닝 > 자연어 처리' 카테고리의 다른 글
텐서플로와 머신러닝으로 시작하는 자연어 처리 - Chapter 3. 자연어 처리 개요(2) (0) | 2019.11.22 |
---|---|
텐서플로와 머신러닝으로 시작하는 자연어 처리 - Chapter 3. 자연어 처리 개요(1) (0) | 2019.11.22 |
텐서플로와 머신러닝으로 시작하는 자연어 처리 - 소개 및 다짐 (0) | 2019.11.22 |