저번 글에 이어 <4> 자연어 생성부터 진행하고자 한다.

 

<4> 자연어 생성

언어 생성이란 무엇일까? 사람이 언어를 생성하는 과정을 생각해보자. 일반적으로 글을 쓰거나 말을 할 때 어떠한 주제에 대한 목적 의식을 가지고 언어에 맞는 문법과 올바른 단어를 사용해 문장을 생성한다. 신문 기사가 될 수도 있고, 상대방과의 대화, 문장 요약 등 언어를 활용해 우리는 서로 "소통(Communication)"하면서 살아간다. 그렇다면 컴퓨터가 상대방의 대화를 이해하고, 글도 쓴다면 어떨까?

 

실제로 미국 캐리포니아 LA Times에서는 "Quakebot"이 인공지능으로 지진이 난 지 3분만에 기사를 작성했다. 

 

원문 링크: https://slate.com/technology/2014/03/quakebot-los-angeles-times-robot-journalist-writes-article-on-la-earthquake.html

 

The First News Report on the L.A. Earthquake Was Written by a Robot

Ken Schwencke, a journalist and programmer for the Los Angeles Times, was jolted awake at 6:25 a.m. on Monday by an earthquake. He rolled out of bed...

slate.com

 

하지만 이런 사실 기반의 기사를 제외하고 일반적으로 감정 및 논리력이 들어가는 기사를 작성하는 데는 아직까지 한계가 있어서 보조 도구로 쓰이고 있는 상태이며, 관련 영역에 해당하는 많은 데이터가 필요하다.

 

언어 생성은 사람의 대화를 최대한 많이 수집해서 대화를 배우게 하고 지속적으로 평가하는 과정을 반복해서 특정 목적에 맞는 텍스트를 생성하는 것이 주 목적이다. 이 분야는 아직 많은 연구와 개척이 필요한 분야다. 현재 언어 생성 기술을 활용해서 성공적으로 활용하는 분야 중 하나는 기계번역 분야인데, 대표적으로 네이버의 파파고와 구글의 구글 번역기가 있다.

 

<5> 기계 이해

 

기계 이해(Machine Comprehension)는 기계가 어떤 텍스트에 대한 정보를 학습하고 사용자가 질의(Question)을 던졌을 때 그에 대해 응답하는 문제다. 다시 말하자면 기계가 텍스트를 이해하고 논리적 추론을 할 수 있는지 데이터 학습을 통해 보는 것이다. 다음 예시를 보자.

 

Text:

자연어 처리 또는 자연 언어 처리는 인간의 언어 현상을 컴퓨터와 같은 기계를 이용해서 모사 할 수 있도록 연구하고 이를 구현하는 인공지능의 주요 분야 중 하나다. 자연 언어 처리는 연구 대상이 언어이기 때문에 당연하게도 언어 자체를 연구하는 언어학과 언어 현상의 내적 기재를 탐구하는 언어 인지 과학과 연관이 깊다. 구현을 위해 수학적 통계적 도구를 많이 활용하며 특히 기계학습 도구를 많이 사용하는 대표적인 분야이다. 정보검색, QA 시스템, 문서 자동 분류, 신문기사 클러스터링, 대화형 Agent 등 다양한 응용이 이루어지고 있다.

 

Question:

자연어 처리는 어느 분야 중 하나인가?

 

예를 들어, 기계한테 위와 같은 텍스트에 대한 내용을 학습시켰다고 하자. 그리고 "자연어 처리는 어느 분야 중 하나인가?" 라는 텍스트와 관련이 있는 질문을 기계에게 한다. 그러면 기계는 위 텍스트의 내용을 토대로 추론해서 이에 대한 응답을 텍스트 내에서 하거나 정답 선택지를 선택하는 방식으로 답하게 된다. 

 

기계 이해 분야는 자연어 처리 기술에 대한 개념이 총망라된 학습 태스크여서 앞에서 알아본 다른 자연어 처리 태스크와 비교하면 어렵고, 더욱 복잡한 모델링을 필요로 한다. 기계 이해는 아직 연구 단계에 있고 QA(Question Answering) 태스크와 관련된 여러 대회를 통해 많은 모델들이 제시되고 있다. 이러한 대회 중 대표적으로 SQuAD(Stanford Question Answering Dataset)를 사례로 들 수 있다.

 

데이터셋

 

기계 이해 태스크에서는 대체로 자연 언어를 이해하는 과제에서 기계가 텍스트 내용에 대해 추론을 잘 하는지 파악하는 목적에서 학습하게 된다. 그렇기 때문에 이 태스크를 QA 태스크라고 부르기도 하며 보통 Question Answering(QA) 형태의 데이터셋을 활용해 기계에게 학습하게 한다. 이러한 데이터셋은 위키피디아나 뉴스 기사를 가지고 데이터를 구성하며 대체로 텍스트와 지문, 정답 형태로 구성돼 있다. 

 

그 중 하나의 예시로는 bAbI 데이터셋이 있는데, 페이스북 AI 연구팀에서 기계가 데이터를 통해 학습해서 텍스트를 이해하고 추론하는 목적에서 만들어진 데이터셋이다. 총 20가지 부류의 질문 내용으로 구성돼 있으며 질문 데이터셋 구성은 다음과 같다.

 

그림 1. bAbI 데이터셋 예시

 

 

위 그림에서 보여지는 것 처럼 bAbI 데이터셋은 시간 순서대로 나열된 텍스트 문장 정보와 그에 대한 질문으로 구성되어 텍스트 정보에 대해 질문을 던지고 응답하는 형태다. 

 

bAbI의 경우 기계 모델이 사람이 문제를 풀었을 때보다 더 좋은 점수를 내면서 이미 해결된 부분으로 알려져 있다.

 

<6> 데이터 이해하기

 

4장부터는 본격적으로 캐글 도전 및 모델링을 진행하게 된다. 처음 캐글 문제를 풀기 시작할 때 많은 사람들은 모델을 만들고 훈련 후에 성능을 평가하고, 생각보다 성능이 안 나온다면 모델에 문제가 있다고 판단하고 다른 모델을 사용한다. 이처럼 모델에 문제가 있는 경우도 있지만 우선적으로 해당 문제를 잘 해결하기 위해서는 데이터 이해가 선행되어야 한다. 이러한 과정을 탐색적 데이터 분석(EDA: Exploratory Data Analysis)라 한다. 이러한 과정에서 생각하지 못한 데이터의 여러 패턴이나 잠재적인 문제점 등을 발견할 수 있다.

 

그리고 문제를 해결하기 위한 모델에 문제가 없더라도 데이터마다 적합한 모델이 있는데 해당 모델과 데이터가 잘 맞지 않으면 좋은 결과를 얻을 수 있다. 즉, 아무리 좋은 모델이더라도 데이터와 궁합이 맞지 않는 모델이라면 여러 가지 문제에 직면하게 될 것이다.

 

그렇다면 EDA는 어떻게 진행되는가? 간단하게 얘기하면 정해진 틀 없이 데이터에 대해 최대한 많은 정보를 뽑아내면 된다. 데이터에 대한 정보란 데이터의 평균값, 중앙값, 최솟값, 최대값, 범위, 분포, 이상치(Outlier) 등이 있다. 이러한 값들을 확인하고 히스토그램, 그래프 등의 다양한 방법으로 시각화하면서 데이터에 대한 직관을 얻어야 한다.

 

데이터를 분석할 때는 분석가의 선입견을 철저하게 배제하고 데이터가 보여주는 수치만으로 분석을 진행해야하며, 이러한 데이터 분석 과정은 모델링 과정과 서로 상호 작용하면서 결과적으로 성능에 영향을 주기 때문에 매우 중요한 작업이다.

 

다음 그림은 EDA의 전체적인 흐름도이며, 서로의 결과에 직접적으로 영향을 줄 수 있다는 것을 확인할 수 있다.

 

그림 2. 탐색적 데이터 분석

 

 

간단한 실습을 통해 데이터 분석에 대해서 자세히 알아보자. 실습에 사용한 데이터는 영화 리뷰 데이터로, 리뷰와 그 리뷰에 해당하는 감정(긍정, 부정) 값을 가지고 있다. 

 

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 데이터의 경우, 판다스로 가져올 수 없는 상황이다.

 

압축이 풀린 데이터를 확인하면 다음과 같다.

 

그림 3. 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를 적용하면 다음과 같다.

 

그림 5. 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()

 

그림 6. 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는 범례를 의미하며, 이를 출력하면 그래프를 보기가 더 수월하기 때문에 추가하였다.

 

그림 7. 히스토그램 

범례는 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)))

그림 8. 위 코드 결과창

 

문장 최대 길이, 최소 길이, 평균 길이, 길이의 표준편차, 중간 길이, 1 / 3사분위 길이 등을 확인할 수 있는 코드이다.

 

사분위수의 경우 0 ~ 100 스케일로 되어 있다.

 

 

 

다음으로는 박스 플롯을 이용하여 데이터를 시각화해본다. 박스 플롯은 직관적인 시각화를 제공하는 특징이 있다.

 

plt.figure(figsize = (12, 5))
plt.boxplot([review_len_by_token], labels = ['token'], showmeans = True)

plt.boxplot은 박스 플롯을 만들어내는 함수이며, showmeans = True로 지정 시 평균값을 표현해주게 된다.

 

그림 9. 박스플롯

그려진 박스플롯에 대한 상세한 설명은 다음과 같다.

 

그림 10. 박스플롯 설명

박스플롯을 확인해 보면 전체적인 데이터 분포를 확인할 수 있으며, 특히 이상치가 심한 데이터를 확인할 수 있다는 장점이 있다.

 

이상치가 심하게 되면 데이터의 범위가 너무 넓어 학습이 효율적으로 이루어지지 않기 때문에 이를 확인하는 것이 중요하다.

 

다음으로는 워드 클라우드를 이용하여 데이터를 시각해보자.

 

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"로 지정하여 축을 따로 나타내지 않는것으로 보인다.

 

실제 코드를 실행한 결과 다음과 같은 워드 클라우드를 볼 수 있다.

 

 

그림 11. 워드클라우드 결과

 

워드클라우드에서 특히 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 일때만 사용가능하다는점에 유의하여야 한다.

 

위 코드를 실행시키면 다음과 같은 그림을 얻을 수 있다.

 

그림 12. 긍정 부정의 분포 결과

sentiment가 0인 것과 1인 것이 몇개씩 있는지를 나타내주는 그래프를 얻을 수 있었다.

 

둘다 개수는 12500개 이며, 긍정 부정 데이터의 균형이 아주 좋은 경우라고 볼 수 있다.

 

 

 

지금까지의 내용이 Chapter 3의 마지막 부분까지 다룬 내용이 된다.

 

이론적인 부분들은 어느정도 잘라내어 생략하였고, 코드가 있는 부분은 최대한 한줄 한줄 풀어서 설명하였다.

 

책에서는 한줄 한줄 세세하게 설명해주지 않다보니, 나 또한 코드에 사용된 함수들을 하나하나 찾아보면서 공부할 수 있는 기회가 되었고

 

혹시나 이 책을 공부하시다가 모르는 코드가 있어서 검색을 해 나의 블로그에 누군가가 들어와서 확인해주신다면 이 또한 글을 쓴 보람이 생길 것 같다. 

 

다음 글에서는 본격적으로 Kaggle 데이터를 통해 텍스트 분류를 진행해보려고 한다.

 

마지막으로 체크해보아야 할 것들을 정리해보자.

 

 

첫번째: 자연어 생성이란 무엇이고, 자연어 생성 기술을 활용하여 성공한 분야는 무엇인가?

 

두번째: 기계 이해란 무엇이고, 보통 어떤 형태의 데이터셋을 이용하여 학습하는가?

 

 

 

앞으로도 코드에 관련된 부분은 뭐 외우거나 하기는 어려울듯하여 따로 문제로 정리하지는 않을 예정이다.

 

한줄 한줄 최대한 이해해가면서 코드를 보는 것 그 자체가 큰 도움이 되는듯하여.... (물론 외우는게 어느정도 필요할 것 같지만 아직 나도 초보라서 뭘 외워야하는지 모름...)

 

 

 

 

 

 

 

 

 

 

저번 글에서는 단어 표현이 무엇인지에 대한 내용을 다루었고, 또 CBOW가 무엇인지에 대해서 조금 더 상세하게 설명을 했다.

 

이번 글에서는 그 뒤를 이어 CBOW의 정반대 메커니즘을 가지고 있는 Skip-Gram에 대해서 얘기해보고 그 뒤 내용들을 다루어보도록 하겠다.

 

Skip-Gram은 다음과 같은 과정을 통해 학습이 진행된다.

 

1. 하나의 단어를 원-핫 벡터로 만들어서 입력값으로 사용한다.

2. 가중치 행렬을 원-핫 벡터에 곱해서 N-차원 벡터를 만든다.

3. N-차원 벡터에 다시 가중치 행렬을 곱해서 원-핫 벡터와 같은 차원의 벡터로 만든다.

4. 만들어진 벡터를 실제 예측하려는 주변 단어들 각각의 원-핫 벡터와 비교해서 학습한다.

 

물론 CBOW와 거의 대부분이 똑같기 때문에, 전체적인 구조를 설명하는 그림만 추가해도 충분히 이해할 수 있을 듯하다.

(혹시나 CBOW에 대한 설명을 아직 안 보셨다면, 이전 글을 보고 오시면 충분히 이해가 되실 것 같습니다.)

(예시도 CBOW에서 사용했던 그대로를 사용합니다. 즉 예시 문장은 지금 너무 추워서 패딩 꼭 입어야 해 입니다.)

 

그림 1. Skip-Gram의 전체적인 구조도

Skip-Gram의 전체적인 구조도를 그린 그림이다. CBOW와 마찬가지로 가중치 행렬곱을 M과 M'으로 총 두 번 작업하게 된다. 다른 점이라고 한다면 CBOW의 경우 입력값이 여러 개였으므로, 입력값을 받고 나서 평균을 취해줘서 평균 N-차원 은닉층을 만든 반면, Skip-Gram의 경우는 입력값이 한 개이므로 평균을 취해주지 않아도 된다.

 

 

이러한 Word2Vec의 두 모델은 여러 가지 장점이 있는데,

 

 

첫 번째로는 기존의 카운트 기반 방법으로 만든 단어 벡터보다 단어 간의 유사도를 잘 측정한다는 점이다.

 

 

두 번째로는 단어들의 복잡한 특징까지도 잘 잡아낸다는 점이고

 

 

세 번째로는 Word2Vec을 이용해서 만들어낸 단어 벡터는 서로에게 유의미한 관계를 측정할 수 있다는 점이다. 예를 들어, 4개의 단어(엄마, 아빠, 남자, 여자)를 word2vec 방식을 사용해 단어 벡터로 만들었다고 한다면, '엄마'와 '아빠'라는 단어 벡터 사이의 cosine 거리와 '여자'와 '남자'라는 단어 벡터 사이의 cosine 거리가 같게 나온다.

 

 

Word2Vec의 CBOW와 Skip-Gram 모델 중에서는 보통 Skip-Gram이 성능이 더 좋아 일반적인 경우 Skip-Gram을 사용하는 것으로 알려져 있으나, 절대적으로 항상 좋은 것은 아니므로 두 가지 모두 고려할 만하다.

 

 

이처럼 단어 벡터를 표현하는데 카운트 기반 방법과 예측 기반 방법을 사용하는데, 보통의 경우 예측 기반 방법의 성능이 더 좋아 주로 예측 기반 방법을 사용한다. 그리고 두 가지 방법을 모두 포함하는 "Glove"라는 단어 표현 방법 또한 사용된다. (Glove도 대략 찾아보았는데 생각보다 너무 난해해 일단은 포기하고 나중을 기약한다...)

 

 

단어 표현은 자연어 처리 문제를 해결하는 기반이 되는 가장 근본적인 내용이므로 정확히 이해하는 것이 중요하며, 항상 가장 좋은 성능을 내는 유일한 방법이 있는 것이 아니라서 각 방법 간에 어떤 차이점이 있는지 항상 염두에 두고 상황에 맞게 사용하는 것이 중요하다.

 

<2> 텍스트 분류

 

텍스트 분류(Text Classification)는 자연어 처리 문제 중 가장 대표적이고 많이 접하는 문제다. 자연어 처리 기술을 활용해 특정 텍스트를 사람들이 정한 몇 가지 범주(Class) 중 어느 범주에 속하는지 분류하는 문제다. 분류해야 하는 범주의 수에 따라 문제를 구분하기도 하는데, 보통 2가지 범주에 대해 구분하는 문제는 이진 분류(Binary Classification) 문제라고 하고, 3개 이상의 범주에 대해 분류하는 문제는 다중 범주 분류(Multi Class Classification) 문제라고 한다. 다음 예시를 이용해 텍스트 분류에 대해 좀 더 알아보자.

 

텍스트 분류의 예시

 

- 스팸 분류

 

메일 중에서 일반 메일과 스팸 메일을 분류하는 문제가 스팸 분류 문제이다. 현재 이메일 서비스를 하고 있는 포털들은 이 기술들이 잘 적용되고 있어서, 스팸 메일은 알아서 잘 걸러내고 있다. 이 문제에서 분류해야 할 범주(Class)는 스팸 메일과 일반 메일로 두 가지이다.

 

 

- 감정 분류

 

감정 분류란 주어진 글에 대해서 이 글이 긍정적인지 부정적인지 판단하는 문제이다. 이 경우 범주는 긍정 혹은 부정이 된다. 경우에 따라서 범주는 긍정 혹은 부정에 한정되는 것이 아닌 중립의 범주가 추가될 수 있으며, 긍정 혹은 부정의 경우에도 정도에 따라 범주를 세분화하기도 한다. 즉 문제에서 분류하려는 의도에 따라 범주가 정해지는 것이다. 가장 대표적인 감정 분류 문제는 영화 리뷰에 대해 각 리뷰가 긍정적인지 부정적인지 판단하는 문제인데, 4장에서 자세히 설명할 예정이다.

 

 

- 뉴스 기사 분류

 

매일매일 뉴스가 계속해서 쏟아지는데, 이를 분류하지 않는다면 내가 원하는 뉴스를 선택해서 보는 것이 어려울 것이다. 따라서 뉴스 업체들은 사용자들이 원하는 뉴스를 선택해서 볼 수 있게끔 범주를 잘 구분 지어 분류해둬야 한다. 예를 들면 스포츠, 연예, 문화, 경제, 사회 등등이 있을 것이다. 여기에 자연어 처리 기술을 사용한다면 뉴스 기사를 더욱 효율적으로 분류할 수 있다.

 

 

 

텍스트 분류의 예시에 3가지 정도를 알아보았는데, 이외에도 다양한 문제들이 존재한다. 분류하는 단위를 글 전체로 하지 않고 각 단어를 한 단위로 분류하는 문제도 있다. 예를 들면 품사 태깅(POS tagging) 문제는 각 단어를 기준으로 어떤 품사를 가지는 것인지 분류해야 하는 문제이다.

 

 

그렇다면 텍스트 분류 문제는 어떻게 해결해야 하는가? 큰 기준으로 본다면 지도 학습 혹은 비지도 학습을 통해 해결할 수 있을 것이다.

 

지도 학습을 통한 텍스트 분류

 

지도학습을 통해 문장 분류를 하는 전체적인 구조는 다음 그림과 같다.

 

그림 2. 지도학습을 통한 문장 분류 과정

지도 학습의 경우 데이터에 대한 라벨이 주어져 있으므로, 데이터를 통해 학습 후 원하는 데이터에 대한 라벨을 구하면 된다.

 

 

지도 학습을 통한 문장 분류 모델에는 다양한 종류가 있고, 그중에 대표적으로 사용되는 지도 학습의 예는 다음과 같다.

 

- 나이브 베이즈 분류(Naive Bayes Classifier)

- 서포트 벡터 머신(Support Vector Machine)

- 신경망(Neural Network)

- 선형 분류(Linear Classifier)

- 로지스틱 분류(Logistic Classifier)

- 랜덤 포레스트(Random Forest)

 

 

비지도 학습을 통한 텍스트 분류

 

비지도 학습의 경우는 라벨이 없기 때문에, 특성을 찾아내서 적당한 범주를 만들어서 각 데이터를 나누면 된다.

 

어떤 특정한 분류가 있는 것이 아니라, 데이터의 특성에 따라 비슷한 데이터끼리 묶어주는 개념이다.

 

비지도 학습을 통한 텍스트 분류는 텍스트 군집화라고도 불리며, 대표적인 비지도 학습 모델은 다음과 같다.

 

- K-평균 군집화(K-means Clustering)

- 계층적 군집화(Hierarchical Clustering)

 

지도 학습과 비지도학습 중 어떤 방법을 사용할지 결정하는데 기여하는 가장 큰 기준은 데이터에 정답 라벨이 있느냐 없느냐이다. 정답 라벨이 있는 경우 지도학습 방법으로 문제를 해결하면 되고, 정답 라벨이 없는 경우 비지도 학습을 사용해서 문제를 해결하면 된다. 그리고 일반적인 분류의 경우에는 지도 학습을 사용하지만, 정확한 범주가 없고 단순히 군집화만 할 경우에는 비지도 학습을 통해 데이터를 군집화하면 된다.

 

<3> 텍스트 유사도

 

인공지능 스피커에게 다음과 같은 질문을 한다고 생각해보자.

 

  • 이 노래 누가 만들었어?
  • 지금 나오는 노래의 작곡가가 누구야?

 

위 두 문장은 똑같은 의미이지만 인공지능 스피커에게는 다른 문장으로 인식될 것이다. 따라서 각기 다른 대답을 만들어야 하는데, 좀 더 효율성을 위해 비슷한 의미를 가진 문장에 대해서는 같은 대답을 준비할 수 있을 것이다. 이때 문장이 유사한지 측정해야 하며, 텍스트 유사도(Text Similarity) 측정 방법을 사용하면 된다.

 

 

텍스트 유사도란, 말 그대로 텍스트가 얼마나 유사한지를 표현하는 방식 중 하나다. 앞에서 예로 든 두 문장의 경우 다른 구조의 문장이지만 의미는 비슷하므로 두 문장의 유사도는 높다고 판단할 수 있다. 물론, 유사도를 판단하는 척도는 매우 주관적이므로 데이터를 구성하기가 쉽지가 않으며 정량화하는데 한계가 있다.

 

 

일반적으로 유사도를 측정하기 위해 정량화하는 방법에는 여러 가지가 있다. 단순히 같은 단어의 개수를 사용해서 유사도를 판단하는 방법, 형태소를 나누어 형태소를 비교하는 방법, 자소 단위로 나누어 단어를 비교하는 방법 등 다양한 방법이 있다.

 

 

이 책에서는 그중에서도 딥러닝을 기반으로 텍스트의 유사도를 측정하는 방식을 주로 다룰 것이다. 단어, 형태소, 유사도의 종류에 상관없이, 텍스트를 벡터화한 후 벡터화된 각 문장 간의 유사도를 측정하는 방식이다. 그리고 자주 쓰이는 4개의 유사도 측정 방법에 대해 알아볼 것이다. 

 

 

우선 유사도를 측정해보기 전에 다음의 두 가지 예시 문장을 보자.

 

  • 휴일인 오늘도 서쪽을 중심으로 폭염이 이어졌는데요, 내일은 반가운 비 소식이 있습니다.
  • 폭염을 피해서 휴일에 놀러 왔다가 갑작스런 비로 인해 망연자실하고 있습니다.

 

이제 이 두 문장에 대해 각 유사도 측정 방법으로 유사도를 측정해 보겠다. 우선은 유사도 측정을 하기 전에 단어를 벡터화한다. 여기서는 TF-IDF를 통해 벡터화한다.

 

 

- 복습 -

TF-IDF를 이용하기 전에, 이전에 다루었던 내용을 잠시 복습하고 가고자 한다.

 

sklearn에서 제공하는 특징 추출 모듈을 사용할 것인데, 자연어 처리에서 특징 추출이란 텍스트 데이터에서 단어나 문장들을 어떤 특징 값으로 바꿔주는 것을 의미한다. 기존에 문자로 구성되어 있던 데이터를 모델에 적용할 수 있도록 특징을 뽑아 어떤 값으로 바꿔서 수치화하는 것이다. 

 

그중에서 TF-IDF이라는 값을 사용해 특징 추출을 하는 TfidfVectorizer를 사용한다.

 

TF는 Term Frequency의 약자로, 특정 단어가 하나의 데이터 안에서 등장하는 횟수를 의미한다.

 

DF는 Document Frequency의 약자로, 특정 단어가 여러 데이터에 자주 등장하는지를 알려주는 지표이다.

 

IDF는 Inverse Document Frequency의 약자로, DF의 역수를 취해 구할 수 있다.

 

마지막으로 TF-IDF는 TF 값과 IDF 값을 곱해서 구하게 된다. 특정 단어가 하나의 데이터 안에서는 자주 등장하고, 여러 데이터에서는 자주 등장하지 않을수록 TF-IDF의 값은 높아지게 된다.

 

 

from sklearn.feature_extraction.text import TfidfVectorizer

sent = ("휴일 인 오늘 도 서쪽 을 중심 으로 폭염 이 이어졌는데요, 내일 은 반가운 비 소식 이 있습니다.", 
        "폭염 을 피해서 휴일 에 놀러왔다가 갑작스런 비 로 인해 망연자실 하고 있습니다.")
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(sent) # 문장 벡터화 진행

print(tfidf_matrix)

print(tfidf_vectorizer.vocabulary_)

idf = tfidf_vectorizer.idf_
print(dict(zip(tfidf_vectorizer.get_feature_names(), idf)))

결과창

먼저 처음 출력한 tfidf_matrix는 각각의 단어를 TF-IDF 값을 이용하여 특징 추출한 matrix를 나타낸다.

 

 

(1, 0) 0.34287125... 이런 식으로 출력되는데, 여기서 (1, 0) 중에 앞부분인 1은 문장의 index를 의미하고(즉 index가 1인, 두 번째 문장이라는 의미), 뒷부분인 0은 단어 중에서 0에 해당하는 단어라는 말이다.(즉 이 경우 (1, 0)에 해당하는 것은 '갑작스런' 임.)

 

 

※ tfidf_matrix에 대한 자세한 설명이 책에는 없었기 때문에, https://stackoverflow.com/questions/42002859/creating-a-tf-idf-matrix-python-3-6 stackoverflow를 참고하였다.

 

 

해당 숫자에 어떤 단어가 매칭 되고 있는지를 알아보기 위해서는 tfidf_vectorizer.vocabulary_를 출력하면 확인할 수 있다. 이를 출력하면 단어와 그에 해당하는 숫자가 dictionary 형태로 출력된다.

 

 

마지막으로 출력된 내용은 각 단어들의 idf 값을 출력한 것이다. (책에서 있어서 출력한 것이지만, 사실 이게 왜 갑자기 필요한지는 모르겠음.)

 

 

 

1. 자카드 유사도

 

자카드 유사도(Jaccard Similarity), 또는 자카드 지수는 두 문장을 각각 단어의 집합으로 만든 뒤 두 집합을 통해 유사도를 측정하는 방식 중 하나다. 유사도를 측정하는 방법은 두 집합의 교집합인 공통된 단어의 개수를 두 집합의 합집합, 즉 전체 단어의 수로 나누면 된다. 결괏값은 공통의 원소의 개수에 따라 0과 1 사이의 값이 나올 것이고, 1에 가까울수록 유사도가 높다는 의미이다.

 

자카드 유사도

 

앞에서 제시한 두 예시 문장을 통해 자카드 유사도를 측정해보자. 유사도를 측정할 때 단어에서 조사는 따로 구분해서 사용하므로 두 문장 A, B는 다음과 같이 정의될 것이다.

 

A = {휴일, 인, 오늘, 도, 서쪽, 을, 중심,으로, 폭염, 이, 이어졌는데요, 내일, 은, 반가운, 비, 소식, 있습니다.

B = {폭염, 을, 피해서, 휴일, 에, 놀러 왔다가, 갑작스런, 비, 로, 인해, 망연자실, 하고, 있습니다.}

 

두 집합을 벤 다이어그램으로 그리면 다음과 같다.

 

벤 다이어그램

자카드 공식에 위의 예제를 대입해 본다면, 두 문장의 교집합은 {휴일, 폭염, 비, 을, 있습니다}로 5개, A-B는 12개, B-A는 8개로 두 문장의 합집합은 25개가 되어 자카드 유사도는 5/25 = 0.2이다.

 

2. 코사인 유사도

 

코사인 유사도는 두 개의 벡터 값에서 코사인 각도를 구하는 방법이다. 코사인 유사도 값은 -1과 1 사이의 값을 가지고 1에 가까울수록 유사하다는 것을 의미한다. 코사인 유사도는 유사도를 계산할 때 가장 널리 쓰이는 방법 중 하나이다. 다른 유사도 접근법에 비해 일반적으로 성능이 좋은데, 이는 단순히 좌표 상의 거리를 구하는 다른 유사도 측정 방법에 비해 코사인 유사도는 말 그대로 두 벡터 간의 각도를 구하는 것이기 때문에 방향성의 개념이 더해지기 때문이다. 두 문장이 유사하다면 같은 방향을 가리킬 것이고, 유사하지 않을수록 직교로 표현될 것이다.

 

 

코사인 유사도

앞서 TF-IDF로 벡터화한 문장을 사용해 코사인 유사도를 구해볼 수 있다. 코사인 유사도의 경우 직접 함수를 구현할 필요 없이 sklearn에서 유사도 측정을 위한 함수를 제공한다.

 

from sklearn.metrics.pairwise import cosine_similarity

cosine_similarity(tfidf_matrix[0], tfidf_matrix[1]) # 첫 번째와 두 번째 문장 비교

코사인 유사도에서 문장 A와 문장 B의 유사도는 대략 18% 정도로 산출된다.

 

3. 유클리디언 유사도

 

유클리디언 유사도는 가장 기본적인 거리를 측정하는 유사도 공식이며, 공식은 다음과 같다.

 

유클리디언 유사도

여기서 구하는 거리는 유클리디언 거리(Euclidean Distance) 혹은 L2거리(L2-Distance)라고 불리며, n차원 공간에서 두 점 사이의 최단 거리를 구하는 접근법이다. 

 

python 코드를 이용해서 유클리디언 유사도를 구해보자. sklearn에서 제공하는 유클리디언 거리 측정 함수를 사용하면 된다.

 

from sklearn.metrics.pairwise import euclidean_distances

euclidean_distances(tfidf_matrix[0], tfidf_matrix[1])

유클리디언 거리는 단순히 두 점 사이의 거리를 뜻하기 때문에, 이전에 소개했던 유사도 방식들이 0과 1 사이의 값을 가지는 것과 달리 유클리디언 거리는 1 이상의 값을 가질 수 있다. 

 

이러한 제한이 없는 유사도 값은 사용하기가 어려우므로 값을 제한하는 방법을 취한다. 다른 유사도 측정 방식과 동일하게 0과 1 사이의 값을 갖도록 만들어줄 것인데, 이는 벡터화된 각 문장들을 일반화(Normalize) 한 후 다시 유클리디언 유사도를 측정하면 0과 1 사이의 값을 갖게 된다. 여기서는 L1 정규화 방법(L1-Normalization)을 사용하도록 한다. L1 정규화 방법에 대해서 간단히 설명하자면 각 벡터 안의 요소 값을 모두 더한 것이 크기가 1이 되도록 벡터들의 크기를 조절하는 방법이다.

 

import numpy as np

def l1_normalize(v):
    norm = np.sum(v)
    return v / norm

tfidf_norm_l1 = l1_normalize(tfidf_matrix)
euclidean_distances(tfidf_norm_l1[0], tfidf_norm_l1[1])

정규화를 거친 후 유클리디언 유사도를 측정한 결과는 0.205 정도가 나온다. 유클리디언 유사도를 측정할 때는 편의를 위해 정규화한 후 측정하는 방법이 있다는 점을 기억하도록 하자.

 

4. 맨하탄 유사도

 

맨하탄 유사도(Manhattan Similarity)는 맨하탄 거리를 통해 유사도를 측정하는 방법이다. 맨하탄 거리란, 사각형 격자로 이뤄진 지도에서 출발점에서 도착점까지를 가로지르지 않고 갈 수 있는 최단거리를 구하는 공식이다. 유클리디언 거리를 L2 거리로 부르는 반면, 맨하탄 거리는 L1 거리로 부른다.

 

맨하탄 거리 공식

 

맨하탄 유사도 또한 유클리디언 유사도와 마찬가지로 거리를 통해 유사도를 측정하는 방법이라 값이 계속해서 커질 수 있다. 따라서 0과 1 사이의 값을 갖도록 L1 정규화 방법을 사용해 벡터 안의 요소 값을 정규화한 뒤 유사도를 측정한다.

 

from sklearn.metrics.pairwise import manhattan_distances

manhattan_distances(tfidf_norm_l1[0], tfidf_norm_l1[1])

위에서 사용한 네 가지 유사도 방법 중, 맨해튼 유사도로 측정했을 때가 가장 높게 나왔다. 측정 방법에 따라 같은 것을 재더라도 유사도가 크게 달라질 수 있으므로 의도하고자 하는 방향에 맞는 유사도 측정 방법을 고르는 것이 매우 중요하다.

 

 

 

 

이번에도 생각보다 글이 많이 길어진듯 하다.

 

여기서 글을 마무리 하고, 그 다음으로 이어가려고 한다.

 

이번 글에서도 역시 많은 것들을 다루었기 때문에, 이번 글에서 다룬 내용들을 바탕으로 정리할 수 있는 문제들을 준비했다.

 

- 점검할 내용 -

 

첫번째, Skip-Gram의 전체적인 구조에 대해서 설명하라.

 

 

두번째, Word2Vec이 가진 장점 세 가지는 무엇인가?

 

 

세번째, TF-IDF란 무엇이며, TF-IDF가 높을수록 어떤 의미를 갖고 있는가?

 

 

네번째, 텍스트 유사도를 나타내는데 사용되는 네 가지의 유사도는 무엇이며, 각각을 설명하라.

 

(티스토리 임시저장 기능 믿고 임시저장 했다가 글이 전부 날라가서 전부 다시 다 쓰고 있습니다.... 혹시나 티스토리를 이용하려는 분이 있다면 반드시 비공개로라도 글을 작성해두시는걸 추천합니다...)

 

이번 Chapter 3에서는 자연어 처리에 대한 전반적인 내용에 대해 알아본다.

 

이번 장에서는 총 4개의 문제에 대해서 알아볼탠데, 4개의 문제란 (1) 텍스트 분류, (2) 텍스트 유사도, (3) 텍스트 생성, (4) 기계이해로 자연어 처리의 핵심 문제에 해당한다. 여기서는 자연어 처리를 통해 어떤 문제를 해결할 수 있고, 어떤 방식으로 해결하는지 알아보려고 한다.

 

앞서 소개한 4가지 문제에 대해서 알아보기 전에, 단어 표현 이라는 분야에 대해서 먼저 알아본다. 단어 표현은 모든 자연어 처리 문제의 기본 바탕이 되는 개념으로, 자연어를 어떻게 표현할지 정하는 것이다.

 

<1> 단어 표현

 

자연어 처리는 컴퓨터가 인간의 언어를 이해하고 분석 가능한 모든 분야를 말한다. 따라서 자연어 처리의 가장 기본적인 문제는 '어떻게 자연어를 컴퓨터에게 인식시킬 수 있을까?' 이다.

 

기본적으로 컴퓨터가 텍스트를 인식하는 방법은 텍스트를 '유니코드' 혹은 '아스키 코드' 라는 방법을 이용해서 인식한다.

 

예를 들면 언 이라는 단어가 있다면, 이를 이진화된 값(예를 들어 11010101010) 으로 인식하는 것이다.

 

하지만 자연어 처리에 이 방식을 그대로 사용하기에는 언어적인 특성을 반영하지 않는다는 점에서 부적절하다.

 

그럼 어떤 방식으로 텍스트를 표현해야 자연어 처리 모델에 적용할 수 있는가?

 

이러한 질문의 답을 찾는 것이 단어 표현(Word Representation) 분야 이다. 텍스트를 자연어 처리를 위한 모델에 적용할 수 있게 언어적인 특성을 반영해서 단어를 수치화하는 방법을 찾는 것이다. 그리고 이렇게 단어를 수치화할 때는 단어를 주로 벡터로 표현한다. 따라서 단어 표현을 단어 임베딩(Word Embedding) 혹은 단어 벡터(Word Vector)라고 표현하기도 한다. 단어 표현에는 다양한 방법이 있고 계속해서 연구되고 있는 분야이기 때문에 하나의 정답이 있는 것은 아니지만 이 책에서는 많이 사용하는 방법 위주로 하나씩 알아본다.

 

단어를 표현하는 가장 기본적인 방법은 원-핫 인코딩(one-hot encoding) 방식이다. 단어를 하나의 벡터로 표현하는 방법인데, 각 값은 0과 1만 갖는다. 예를 들어서 4개의 단어(사과, 딸기, 개, 고양이)가 있다면 사과는 [1, 0, 0, 0], 딸기는 [0, 1 , 0, 0]과 같은 방식으로 표현하는 것이다. 방법 자체가 매우 간단하고 이해하기가 쉬운 장점이 존재한다.

 

하지만, 원-핫 인코딩 방식이 가지고 있는 결정적인 두 가지 문제점이 있는데,

 

첫번째, 실제 자연어 처리 문제를 해결할 때에는 수십만, 수백만 개가 넘는 단어를 표현해야 하므로 단어 벡터의 크기가 너무 커져 공간을 많이 사용하게 되고, 큰 공간에 비해 실제로 사용하는 값은 1이 되는 값 하나뿐이므로 매우 비효율적이다.

 

두번째, 원-핫 인코딩 방식은 단순히 단어가 무엇인지만 알려줄 수 있고, 벡터값 자체에는 단어의 의미나 특성 같은 것들이 전혀 표현되지 않는다는 점이다.

 

위에서 언급된 두가지 원-핫 인코딩 방식의 문제점을 해결하기 위해서 다른 인코딩 방법들이 제안되었다. 즉, 벡터의 크기가 작으면서도 벡터가 단어의 의미를 표현할 수 있는 방법들인데, 이러한 방법들은 분포 가설(Distributed hypothesis)을 기반으로 한다. 분포 가설이란, "같은 문맥의 단어, 즉 비슷한 위치에 나오는 단어는 비슷한 의미를 가진다"라는 개념이다.

 

 

따라서 어떤 글에서 비슷한 위치에 존재하는 단어는 단어 간의 유사도가 높다고 판단하는 방법인데, 크게 두 가지 방법으로 나뉜다.

 

첫번째, 특정 문맥 안에서 단어들이 동시에 등장하는 횟수를 직접 세는 방법인 카운트 기반(count-base)

 

카운트 기반 방법으로 단어를 표현한다는 것은 어떤 글의 문맥 안에 단어가 동시에 등장하는 횟수를 세는 방법이다. 여기서 동시에 등장하는 횟수를 동시 출현 또는 공기라고 부르고 영어로는 Co-occurrence라고 한다. 카운트 기반 방법에서는 기본적으로 동시 등장 횟수를 하나의 행렬로 나타낸 뒤, 그 행렬을 수치화해서 단어 벡터로 만드는 방법을 사용하는 방식인데 다음과 같은 방법들이 있다.

 

- 특이값 분해(Singular Value Decomposition, SVD)

- 잠재의미분석(Latent Semantic Analysis, LSA)

- Hyperspace Analogue to Langauge(HAL)

- Hellinger PCA(Principal Component Analysis)

 

위의 네 가지 방법은 모두 동시 출현 행렬(Co-occureence matrix)를 만들고 그 행렬들을 변형하는 방식인데, 이 책에서는 동시 출현 행렬까지만 만들어 보고 행렬을 통해 다시 단어 벡터로 만드는 방법에 대해서는 다루지 않는다. (따로 찾아봐야 한다는 얘기인듯하다.)

 

우선 다음 예시를 가지고 동시 출현 행렬을 만들어 보자.

 

- 성진과 창욱은 야구장에 갔다.

- 성진과 태균은 도서관에 갔다.

- 성진과 창욱은 공부를 좋아한다.

 

위의 3개의 문장을 가지고 동시 출현 행렬을 만들어본다면 다음과 같이 만들 수 있다.

 

  성진과 창욱은 태균은 야구장에 도서관에 공부를 갔다. 좋아한다.
성진과 0 2 1 0 0 0 0 0
창욱은 2 0 0 1 0 1 0 0
태균은 1 0 0 0 1 0 0 0
야구장에 0 1 0 0 0 0 1 0
도서관에 0 0 1 0 0 0 1 0
공부를 0 1 0 0 0 0 0 1
갔다. 0 0 0 1 1 0 0 0
좋아한다. 0 0 0 0 0 1 0 0

 

이렇게 만들어진 동시 출현 행렬을 토대로 4가지 방법등을 사용하여 단어 벡터를 만들면 된다.

 

카운트 기반 방법의 장점은 빠르다는 점이고, 예측 방법에 비해 좀 더 이전에 만들어진 방법이지만 데이터가 많을 경우에는 단어가 잘 표현되고 효율적이어서 아직까지도 많이 사용하는 방법이다.

 

두번째, 신경망 구조 혹은 어떠한 모델을 사용해 특정 문맥에서 어떤 단어가 나올지 예측하는 예측 방법(Predictive)

 

예측 방법에는 다음과 같은 것들이 존재한다.

 

- Word2Vec

- NNLM(Neural Network Language Model)

- RNNLM(Recurrent Neural Network Language Model)

 

여러 예측 기반 방법 중에서 단어 표현 방법으로 가장 많이 사용되는 Word2Vec에 대해 자세히 알아본다.

 

Word2Vec은 CBOW(Continuous Bag of Words)와 Skip-Gram이라는 두 가지 모델로 나뉜다. 두 모델은 각각 서로 반대되는 개념으로 생각하면 되는데, CBOW의 경우 "문맥 안의 주변 단어들을 통해 어떤 단어를 예측하는 방법"이고, 반대로 Skip-Gram의 경우 "어떤 단어를 가지고 특정 문맥 안의 주변 단어들을 예측하는 방법"이다.

 

예시를 들어서 CBOW와 Skip-Gram을 이해해보자.

 

- 민수는 냉장고에서 음식을 꺼내서 먹었다.

 

CBOW은 주변 단어를 통해 하나의 단어를 예측하는 모델인데, 이를 이용한다면 다음 문장의 빈칸을 채울 수 있다.

 

- 민수는 냉장고에서 _______ 꺼내서 먹었다.

 

반대로, Skip-Gram은 하나의 단어를 가지고 주변에 올 단어를 예측하는 모델인데, 이를 이용한다면 다음 문장의 빈칸을 채울 수 있다.

 

- _____ _____________ 음식을 ______  ________

 

 

그렇다면 CBOW와 Skip-Gram이 어떤 순서로 학습되는지 알아보도록 하자.

 

 

CBOW의 경우는 다음과 같은 순서로 학습한다.

 

1. 각 주변 단어들을 원-핫 벡터로 만들어 입력값으로 사용한다.

2. 가중치 행렬(weight matrix)을 각 원-핫 벡터에 곱해서 N-차원 벡터를 만든다.

3. 만들어진 N-차원 벡터를 모두 더한 후 개수로 나눠 평균 N-차원 벡터를 만든다.

4. N-차원 벡터에 다시 가중치 행렬을 곱해서 원-핫 벡터와 같은 차원의 벡터로 만든다.

5. 만들어진 벡터를 실제 예측하려고 하는 단어의 원-핫 벡터와 비교해서 학습한다.

 

책에는 이렇게 나와있었지만, 사실 CBOW를 5줄로 해서 이해하기란 어려울 것이다. 나 또한 이것을 읽고 그저 너무 겉핥기식으로 설명했다고 생각했다.

 

그래서 나는 CBOW와 관련된 글들을 추가적으로 읽고, 이것을 좀 더 상세히 풀어서 설명해보면 좋겠다는 생각을 했다.

 

따라서 다음에 나올 내용은 CBOW를 위에 언급된 5가지 순서를 지키되, 상세하게 어떤식으로 작동하게 되는지를 설명해보고자 한다.

 

 

 

이해를 돕기 위해 특정 예시를 들어서 설명을 하고자 하는데, 다음과 같은 예시를 사용한다.

 

예시 문장: 지금 너무 추워서 꼭 패딩 입어야 해(7글자)

 

CBOW는 주변 단어를 통해 하나의 단어를 예측하는 모델이라고 위에서 설명하였는데, 책에서는 언급이 되지 않았지만 꼭 고려해야 할 사항은 바로 주변 단어를 몇개로 잡을 것이냐의 문제이다. 즉, 주변에 있는 모든 단어를 사용하는 것이 아닌, 특정 몇 개의 단어를 이용해서 하나의 단어를 예측한다는 것이다. 주변 단어를 몇개로 잡을 것인지를 윈도우(window) 라는 단어로 표현하는데, 예시 문장에 윈도우를 2로 설정하였을 때 어떤 식으로 예측하게 되는지를 설명해보도록 하겠다. 파란색은 입력값으로 들어가는 단어, 빨간색은 예측을 하는 단어이다.

 

1. 지금 너무 추워서 꼭 패딩 입어야 해

 

2. 지금 너무 추워서 꼭 패딩 입어야 해

 

3. 지금 너무 추워서 꼭 패딩 입어야 해

 

4. 지금 너무 추워서 패딩 입어야

 

5. 지금 너무 추워서 꼭 패딩 입어야 해

 

6. 지금 너무 추워서 꼭 패딩 입어야

 

7. 지금 너무 추워서 꼭 패딩 입어야

 

예측 하려고 하는 단어(빨간색)의 양 옆으로 2개의 단어를 사용하여 예측하는 것을 볼 수 있을 것이다.

 

이런식으로 CBOW는 진행이 된다고 생각하면 된다.

 

 

 

가장 먼저 해볼 것은 CBOW의 전체적인 구조를 설명해볼 것이다. 상세한 내용은 차차 설명할 것이고, 전체적인 조망을 하기 위해 전체적인 구조를 먼저 설명하고자 한다.

 

그림 1. CBOW의 전체적인 구조

 

먼저 첫번째 순서(1. 각 주변 단어들을 원-핫 벡터로 만들어 입력값으로 사용한다.)를 적용하면, 예시 문장을 각각 원-핫 벡터로 만들어준다. 즉 지금 = [1, 0, 0, 0, 0, 0, 0]으로, 너무 = [0, 1, 0, 0, 0, 0, 0], 꼭 = [0, 0, 0, 1, 0, 0, 0], 패딩 = [0, 0, 0, 0, 1, 0, 0]으로 만들어준다. 그리고 나서 이 입력값에 가중치 행렬을 곱해서 N-차원의 은닉층을 만들어낸다. 현재 예시에서는 4개를 입력값으로 넣었으므로, 4개의 N-차원 은닉층이 생기게 될 것이다. 이 4개의 N-차원 은닉층을 평균을 취해 평균 N-차원 은닉층을 만든 뒤, 여기에 새로운 가중치 행렬을 곱해서 출력층을 만들고, 여기에 Sigmoid를 적용해 모든 값들을 0 ~ 1 사이의 값으로 만든 뒤에 가장 큰 값을 가지는 요소를 정답으로 출력하게 된다.

 

 

이제부터 전체적인 구조를 조목조목 나누어 하나하나 세세하게 볼 것이다.

 

 

그 다음으로는 두번째 순서(2. 가중치 행렬(weight matrix)을 각 원-핫 벡터에 곱해서 N-차원 벡터를 만든다.)를 적용해 보아야 할탠데, 가중치 행렬은 크기가 어떻게 되고, 어떻게 곱해지는 것인지를 설명해보겠다.

 

현재 예시에서는 입력값이 1 x 7 크기의 행렬이므로, 가중치 행렬과 곱하려면 7 x N 형태의 벡터가 필요하다.

N 으로 하면 잘 와닿지 않을 것이므로, 이 글에서는 N을 4로 두고, 4차원 은닉층을 만드는 것으로 생각해보자.

그렇다면 가중치 행렬은 7 x 4 형태가 될 것이며, 이는 다음과 같이 곱해질 것이다.

 

그림 2. 가중치 행렬 곱(1)

 

그림 3. 가중치 행렬 곱(2)

 

가중치 행렬이 곱해지는 것을 보여주기 위해서, 7 x 4 형태의 matrix를 구성하고 값은 무작위의 정수로 채워넣었다.

실제로 CBOW가 학습되는 과정에서는 이 가중치 행렬은 정답을 가장 잘 맞출 수 있게끔 값이 학습될 것이다.

 

가중치 행렬을 곱하면 결과는 4차원의 은닉층이 될 것이고, 사이즈는 1 x 4가 되는 것을 볼 수 있다.

 

 

다음으로 세번째 순서(3. 만들어진 N-차원 벡터를 모두 더한 후 개수로 나눠 평균 N-차원 벡터를 만든다.)를 실제로 적용해보도록 하겠다.

 

그림 4. 평균 은닉층 만들기

 

그림 2와 그림 3에서 만들어낸 4개의 은닉층을 모두 더한 뒤, 이를 입력층의 크기인 4로 나누어 평균 은닉층을 구한 모습이다. 

 

그 다음으로는 네번째 순서(4. N-차원 벡터에 다시 가중치 행렬을 곱해서 원-핫 벡터와 같은 차원의 벡터로 만든다.)를 실제로 적용해보도록 하겠다.

 

그림 5. 평균 은닉층과 가중치 행렬의 곱

 

그림 4에서 구한 평균 은닉층에다가 가중치 행렬 4 x 7 사이즈를 곱해서 기존의 입력값인 1 x 7 형태의 출력층을 만들어낸다. 왜 출력층이 다시 1 x 7 형태로 되게끔 만들어야 하나? 라고 한다면 결국 우리가 원하는 것은 우리의 모델이 추워서 = [0, 0, 1, 0, 0, 0, 0]를 정답으로 내야 하기 때문에, 우리가 원래 사용했던 벡터의 크기와 동일한 크기의 벡터가 출력으로 나와야 하는 것이다.

 

그림 5에서 구한 출력층의 값들은 경우에 따라서 매우 클 수도 있고, 작을 수도 있는데 우리가 원하는 것은 가장 큰 값을 가지는 값을 찾아야 하는 것이기 때문에, 여기에 Sigmoid 함수를 적용시켜 모든 벡터의 값들을 0 ~ 1 의 값으로 바꾼다.

 

그렇게 했을때 가장 큰 값이 정답이 될 것이다. (그림 5에서는 가장 마지막 벡터가 Sigmoid를 적용했을때 가장 큰 값일 것이므로 정답으로 나오게 될 것이다. 물론 이번 예시에서는 임의의 가중치 행렬을 사용했기 때문에 틀린 정답이 나오게 되었다.)

 

여기서 사용된 두번째 가중치 행렬 또한 학습 과정에서 적절한 값으로 학습이 되는 것으로 생각하면 된다.

 

 

마지막 순서(5. 만들어진 벡터를 실제 예측하려고 하는 단어의 원-핫 벡터와 비교해서 학습한다.)는 손실 함수로 Cross-Entropy 오차를 사용한다. 

 

그림 6. 실제 정답과의 비교

0 ~ 1 출력층은 Sigmoid가 적용되어 모든 벡터들이 0과 1사이의 값이 되고, 이는 예측값이므로 y hat으로 표기하였다. 또 실제 정답은 추워서 [0, 0, 1, 0, 0, 0, 0] 이므로 이는 y로 표현하였다.

 

그림 7. Cross-entropy 

 Cross-entropy가 왜 이런 식으로 구성되어 있는지에 대해서는 다른 글을 참고해보심이 좋을듯 하고, 여기서는 CBOW의 학습 과정에서 Loss function으로 Cross-entropy를 이용하여 학습한다는 점만 짚고 넘어가면 될듯 하다!

 

여기까지가 CBOW에 관련된 세부적인 설명이 되었다.

 

중간중간에도 썼지만, 결국 CBOW가 학습을 통해서 값을 찾아나가는 것은 바로 두개의 가중치 행렬이다. 즉 가장 정답을 잘 맞출 수 있는 가중치 값들로 점차적으로 학습해나간다는 말이다.

 

 

 

글이 너무 길어진 것 같아서, 여기서 한번 끊어주고 다음 글에서는 Skip-Gram에 대한 설명과 그 이후 내용들을 다뤄보도록 하겠다.

 

글을 마치기 전에, 이 내용을 읽었다면 다시 한번 정리해볼만한 내용들을 적어보고 이것들에 어느정도 답할 수 있다면 잘 기억하고 있다고 생각할 수 있을듯 하다.

 

 

 

첫번째 - 단어 임베딩 혹은 단어 표현 이라고 하는 분야는 왜 필요한가?

 

두번째 - 원-핫 인코딩 방식의 단어 표현이 가진 두 가지 문제점은 무엇인가?

 

세번째 -  분포 가설(Distributed hypothesis)란 무엇인가?

 

네번째 - 분포 가설을 기반으로 한 방법 두 가지는 각각 무엇인가?

 

다섯번째 - Word2Vec은 두 가지 모델로 나뉘는데, 각각 모델의 이름과 모델이 작동하는 방법은 무엇인가?

 

여섯번째 - CBOW의 전반적인 구조를 설명하라.

 

 

https://book.naver.com/bookdb/book_detail.nhn?bid=14488487

 

텐서플로와 머신러닝으로 시작하는 자연어 처리

이 책은 기존 자연어 처리 서적과는 다른 세 가지 특징을 가지고 있다.첫째, 자연어 처리에 활용되는 개념적인 설명에서 끝나는 것이 아니라 모델 구현에 집중한다. 그뿐만 아니라 상용 서비스를 지원하는 텐서플로를 기반으로 모델을 개발한다.둘째, 실무에서 자연어 처리 문제를 해결하는 데 조금이나마 도움이 되고자 캐글 대회의 문제를 활용했으며, 감정분석부터 유사도 처리, 챗봇에 이르기까지 다양한 문제를 다룬다.셋째, 딥러닝 기반 자연어 처리를 다룬 서적은 대부분

book.naver.com

 

자연어 처리 분야로 공부를 하려고 찾아보던 중, 꽤 인기있는 도서인듯해서 책을 구해서 보고 있다.

 

 

특히 번역이 되어서 나온 다른 자연어 처리 도서들과 달리, 한국 저자들이 만든 것이라서 한국어 자연어 처리와 관련된 내용들도 수록되어 있었기에, 읽을 가치가 충분히 있다고 판단하였다.

 

 

사실 이 책을 읽기 전에 다른 자연어 처리 책을 읽었었는데, 일단 내 스타일에 맞지 않았고 번역서여서 영어 자연어 처리에 대한 부분만 다룬다는 점이 아쉽다고 느꼈다.

 

 

혼자 공부하고 말 생각이였다가, 공부한 내용을 글로 남겨두면 머리속에 더 많이 남을 것이라고 생각했고

 

 

또 책에서 다루지 못하고 있는 좀 더 세부적인 내용이나, 코드의 오류 등을 찾아서 올려두면 비슷한 애로사항이 있으신 분들이 찾아오셔서 도움을 받으실 수 있지 않을까 하는 생각에 글을 작성하기로 하였다.

 

 

열심히 보고 있긴 하지만 코드가 들어있는 책이고 하나하나 코드를 세심히 봐야하다보니 생각보다는 진도가 빠르게 나가지는 못하고 있다.

 

 

최대한 열심히 봐서 12월 중으로 책의 모든 내용을 다루어 보는것이 목표이다.

 

 

챕터 1은 개요 부분이라서 글로 다루지 않을 예정이고, 챕터 2는 자연어 처리와 관련한 도구들을 설명하는 부분이나 

 

 

지금 이 글을 쓰고 있는 시점은 이미 챕터 2를 다 읽고 챕터 3부터 정리하고 있는 상황이라 

 

 

아마도 챕터 2는 따로 글을 남기지 않거나 추후에 작성할 듯 하다.

 

 

그래서 시작은 챕터 3부터 시작하게 될 것 같다.

 

 

열심히 봐서 꼭 완독을 해보고 싶다!

+ Recent posts