본문 바로가기

Computer Science/AI Exploration

[E-08] Text Summarization

728x90

Intro

이번 시간에는 긴 문장을 짧게 요약해주는 텍스트 요약기를 만들어보자.

학습목표

  • Extractive/Abstractive summarization 이해하기
  • 단어장 크기를 줄이는 다양한 text normalization 적용해보기
  • seq2seq의 성능을 Up시키는 Attention Mechanism 적용하기

What is Text Summarization?

Text Summarization: the process of shortening a set of data computationally, to create a subset that represents the most important or relevant information within the original content.

이때 중요한 것은 요약 전후에 정보 손실 발생이 최소화되어야 한다는 점이다. 이렇게 요약 문장을 만들어내기 위해서는 크게 두 가지 접근으로 나누어볼 수 있는데, 이는 Extractive Summarization과 Abstractive Summarization이다.

1) 추출적 요약(Extractive Summarization)

단어 그대로 원문에서 문장들을 추출해서 요약하는 방식이다. 꺼내온 문장이 원문에서 중요한 문장일 수 있지만, 문장 간 연결이 자연스럽지 않을 수 있다. 딥러닝보다는 주로 텍스트 랭크와 같은 전통적인 머신러닝 알고리즘을 사용해 이 방법을 적용한다.

ex. 네이버 뉴스 서비스에 있는 요약봇

2) 추상적 요약(Abstractive Summarization)

원문으로부터 내용이 요약된 새로운 문장을 생성한다. 자연어 처리 분야 중 자연어 생성(NLG) 영역인 셈이다. 반면, 추출적 요약은 원문을 구성하는 문장 중 어느 것이 요약문에 들어갈 핵심문장인지를 판별한다는 점에서 문장 분류(Text Classification) 문제로 볼 수 있다.

❓ RNN은 학습 데이터의 길이가 길어질수록 먼 과거의 정보를 현재에 전달하기 어렵다는 문제가 있다. 이를 해결하기 위해 LSTM, GRU가 등장했고, 그 이후로는 Attention 메커니즘이 등장했다. 이 문제의 이름은 뭘까?
❗ 장기 의존성(long term dependencies) 문제

구글의 텍스트 요약 모델 관련 기사
❓ 텍스트마이닝 분야의 '역문서빈도' 같은 지표를 활용해 문서 내에서 중요해 보이는 부분을 추출해 요약문에 담는 방식을 쓸 때의 문제점을 무엇인가?
❗ 원문에서 발췌하는 방식(Extractive summarization)의 요약 기법은 어색하거나 문법적으로 이상한 결과물을 만드는 문제가 있음

❓ 구글이 짧은 문장, 요약문을 생성하는 모델을 딥러닝을 통해 end-to-end로 설계했다. 구글이 메일서비스에 적용한 자동 회신 기능을 만든 것과 비슷한 딥러닝 기법이며, 인코더와 디코더의 구조로 구성된 딥러닝 아키텍쳐의 이름은 무엇일까?
❗ seq2seq(sequence-to-sequence)

인공 신경망으로 텍스트 요약 훈련시키기

seq2seq 모델을 통해 Abstractive summarization 방식의 텍스트 요약기를 만들어보자.

1) seq2seq 개요

https://medium.com/dl-for-product-and-service/abstractive-text-summary-with-reinforcement-learning-ab2458ab29d5

원문을 첫 번째 RNN인 인코더로 입력하면, 인코더는 하나의 고정된 벡터로 변환한다. 이 벡터를 문맥 정보를 가진 벡터라고 하여 context vector라고 한다. 두 번째 RNN인 디코더는 context vector를 전달받아 한 단어씩 생성해서 요약 문장을 완성한다.

2) LSTM과 context vector

seq2seq를 구현할 때 인코더/디코더로 LSTM을 사용할 것이다.

RNN과 LSTM

LSTM이 바닐라 RNN과 다른 점은 다음 time step의 셀에 hidden state뿐만 아니라, cell state도 함께 전달한다는 점이다. 즉, 인코더가 디코더에 전달하는 컨텍스트 벡터 또한 hidden state h와 cell state c 두 개의 값 모두 존재해야 한다는 의미다.

3) start/end token

시작 토큰 SOS와 종료 토큰 EOS는 각각 start of a sequence와 end of a sequence를 나타낸다)

seq2seq 구조에서 디코더는 시작 토큰 SOS가 입력되면, 각 시점마다 단어를 생성하고 이 과정을 종료 토큰 EOS를 예측하는 순간까지 멈추지 않는다. 즉, 훈련 데이터의 예측 대상 시퀀스의 앞뒤에 토큰을 넣는 전처리를 통해 멈출 지점을 알려줘야 한다.

4) 어텐션 메커니즘을 통한 새로운 컨텍스트 벡터 사용하기

기존의 컨텍스트 벡터보다 인코더의 정보를 적극적으로 활용하여 성능을 끌어올리는 어텐션 메커니즘

기존에 배운 seq2seq를 수정하고, 새로운 모듈을 붙여 모델의 성능을 높여보자. 기존 seq2seq는 인코더의 마지막 time step의 hidden state를 컨텍스트 벡터로 사용한다. 하지만 RNN 계열의 신경망의 한계로 인해 이미 컨텍스트 정보에는 입력 스퀀스의 많은 정보가 손실이 난 상태가 된다.

하지만 Attention Mechanism은 인코더의 모든 step의 hidden state의 정보가 컨텍스트 벡터에 전부 반영되게 한다. 디코더의 현재 time step의 예측에 인코더의 각 step이 얼마나 영향을 미치는지에 따른 가중합으로 계산된다.

✔ 주의: 컨텍스트 벡터를 구성하기 위한 인코더 hidden state의 가중치 값은 디코더의 현재 스텝의 위치에 따라 계속 변한다. 즉, 디코더의 문장 생성 부위가 주어부/줄어부/목적어인지 등에 따라 인코더가 입력데이터를 해석한 컨텍스트 벡터가 다른 값이 된다는 것이다.

5) 정리

  1. seq2seq를 사용합니다.
  2. RNN 계열 중 LSTM을 사용하므로 hidden state뿐만 아니라 cell state도 사용해야 합니다.
  3. 디코더의 예측 시퀀스에는 시작 토큰 SOS와 예측 토큰 EOS를 시퀀스의 앞, 뒤로 붙입니다.
  4. seq2seq를 구동시키면 디코더는 시작 토큰을 입력받아 예측을 시작합니다.
  5. seq2seq 기본 모델과 달리, 어텐션 메커니즘을 이용해 인코더의 hidden state의 중요도를 취합한 컨텍스트 벡터를 디코더 스텝별로 계산합니다.
  6. 계산된 컨텍스트 벡터를 이용해서 디코더는 다음 등장할 단어를 예측합니다.

데이터 준비하기

터미널을 열어 작업환경을 구성한다.

오늘 사용할 데이터셋은 아마존 리뷰 데이터셋이다.

Reviews.csv.zip

$ mkdir -p ~/aiffel/news_summarization/data
$ ln -s ~/data/*.csv ~/aiffel/news_summarization/data #클라우드 데이터 실행

이번 실습에서는 NLTK의 stopword를 사용할 것이다. NLTK를 설치하고 NTLK의 데이터셋을 다운로드하자.

NLTK(Natural Language ToolKit)은 영어 기호, 통계, 자연어 처리를 위한 라이브러리이다. 의미를 분석하고 요약하는 데는 거의 의미가 없는 100여개의 불용어가 미리 정리되어 있다. NLTK 패키지에서 stopword dictionary를 다운로드하고, 데이터 전처리를 위한 나머지 패키지도 불러오자.

import nltk
nltk.download('stopwords')

import numpy as np
import pandas as pd
import os
import re
import matplotlib.pyplot as plt
import warnings

warnings.filterwarnings(action='ignore')

from nltk.corpus import stopwords
from bs4 import BeautifulSoup 
from tensorflow.keras.preprocessing.text import Tokenizer 
from tensorflow.keras.preprocessing.sequence import pad_sequences
import urllib.request
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module='bs4')

print('=3')
=3


[nltk_data] Downloading package stopwords to /aiffel/nltk_data...
[nltk_data]   Package stopwords is already up-to-date!
# 시간상 10만개의 샘플만 사용하자

data = pd.read_csv(os.getenv("HOME")+"/aiffel/news_summarization/data/Reviews.csv", nrows=100000)
print('전체 샘플수 :', (len(data)))
전체 샘플수 : 100000
# 전체 데이터 중 Summary / text 열만 훈련에 사용하기 때문에, 두 열만 별도로 저장

data = data[['Text','Summary']]
data.head()

#랜덤한 15개 샘플 출력
data.sample(15)

Text Summary
42650 Had high hopes for these, but alas, these bulb... Unnatural, fluorescent quality of light
90510 For about a week now I've checked all the loca... Thank you, Amazon.com
97802 Teavana's Blueberry Kona Pop (w/rock sugar) ca... Donna B
55745 Jerky-type treat, too big for my Yorkshire Ter... Great size, appeal, and food value for SMALL dogs
85905 First and foremost I will say that I was sorel... Couscous, not Tabouli
67772 I LOVE these chips! It is a little weird to r... OooH Yummy!
44314 I never drink this tea alone. I always steep i... Basically a laxative...
56956 I recently bought an espresso machine that had... Lots of Tasty Fun
95914 I can't believe I found one food that works fo... Three Happy Cats
71529 I have tried everything to help my milk chocol... The perfect snack.
85552 Love this tea - Quality seems really good for... Great tasting tea!!!
535 Got these Kettle Chips Sea Salt & Vinegar (15 ... Kettle Chips
54163 We were thrilled to find DeLallo's Organic Who... whole wheat pasta haters take notice
32470 I found Dogswell products 7 years ago when my ... LOVE LOVE LOVE Dogswell products!
9158 I expected more flavor from W. Puck. Though th... Good, not Great

데이터 전처리하기 (1) 데이터 정리하기

전처리를 진행하자. null 데이터, 중복 데이터를 제거하자.

1) 중복 샘플과 NULL 값이 존재하는 샘플 제거

print('Text 열에서 중복을 배제한 유일한 샘플의 수 :', data['Text'].nunique())
print('Summary 열에서 중복을 배제한 유일한 샘플의 수 :', data['Summary'].nunique())
Text 열에서 중복을 배제한 유일한 샘플의 수 : 88426
Summary 열에서 중복을 배제한 유일한 샘플의 수 : 72348
# 데이터프레임의 drop_duplicates()를 사용하면, 손쉽게 중복 샘플을 제거할 수 있다

# inplace=True 를 설정하면 DataFrame 타입 값을 return 하지 않고 data 내부를 직접적으로 바꿉니다
data.drop_duplicates(subset = ['Text'], inplace=True)
print('전체 샘플수 :', (len(data)))
전체 샘플수 : 88426

중복이 제거되었다. 하지만 데이터 Null 값을 가지는 샘플이 있다면, drop_duplicates()가 null 값 한 개를 제외하고 지워줄 것이다.

이를 확인하기 위해 .isnull().sum()을 사용하자.

print(data.isnull().sum())
Text       0
Summary    1
dtype: int64
# dropna 함수로 Null을 제거
data.dropna(axis=0, inplace=True)
print('전체 샘플수 :', (len(data)))
전체 샘플수 : 88425

2) 텍스트 정규화와 불용어 제거

텍스트 정규화 과정을 진행하여 기계의 연산량을 줄여주자.

정규화 참고 사전

contractions = {"ain't": "is not", "aren't": "are not","can't": "cannot", "'cause": "because", "could've": "could have", "couldn't": "could not",
                           "didn't": "did not",  "doesn't": "does not", "don't": "do not", "hadn't": "had not", "hasn't": "has not", "haven't": "have not",
                           "he'd": "he would","he'll": "he will", "he's": "he is", "how'd": "how did", "how'd'y": "how do you", "how'll": "how will", "how's": "how is",
                           "I'd": "I would", "I'd've": "I would have", "I'll": "I will", "I'll've": "I will have","I'm": "I am", "I've": "I have", "i'd": "i would",
                           "i'd've": "i would have", "i'll": "i will",  "i'll've": "i will have","i'm": "i am", "i've": "i have", "isn't": "is not", "it'd": "it would",
                           "it'd've": "it would have", "it'll": "it will", "it'll've": "it will have","it's": "it is", "let's": "let us", "ma'am": "madam",
                           "mayn't": "may not", "might've": "might have","mightn't": "might not","mightn't've": "might not have", "must've": "must have",
                           "mustn't": "must not", "mustn't've": "must not have", "needn't": "need not", "needn't've": "need not have","o'clock": "of the clock",
                           "oughtn't": "ought not", "oughtn't've": "ought not have", "shan't": "shall not", "sha'n't": "shall not", "shan't've": "shall not have",
                           "she'd": "she would", "she'd've": "she would have", "she'll": "she will", "she'll've": "she will have", "she's": "she is",
                           "should've": "should have", "shouldn't": "should not", "shouldn't've": "should not have", "so've": "so have","so's": "so as",
                           "this's": "this is","that'd": "that would", "that'd've": "that would have", "that's": "that is", "there'd": "there would",
                           "there'd've": "there would have", "there's": "there is", "here's": "here is","they'd": "they would", "they'd've": "they would have",
                           "they'll": "they will", "they'll've": "they will have", "they're": "they are", "they've": "they have", "to've": "to have",
                           "wasn't": "was not", "we'd": "we would", "we'd've": "we would have", "we'll": "we will", "we'll've": "we will have", "we're": "we are",
                           "we've": "we have", "weren't": "were not", "what'll": "what will", "what'll've": "what will have", "what're": "what are",
                           "what's": "what is", "what've": "what have", "when's": "when is", "when've": "when have", "where'd": "where did", "where's": "where is",
                           "where've": "where have", "who'll": "who will", "who'll've": "who will have", "who's": "who is", "who've": "who have",
                           "why's": "why is", "why've": "why have", "will've": "will have", "won't": "will not", "won't've": "will not have",
                           "would've": "would have", "wouldn't": "would not", "wouldn't've": "would not have", "y'all": "you all",
                           "y'all'd": "you all would","y'all'd've": "you all would have","y'all're": "you all are","y'all've": "you all have",
                           "you'd": "you would", "you'd've": "you would have", "you'll": "you will", "you'll've": "you will have",
                           "you're": "you are", "you've": "you have"}

print("정규화 사전의 수: ", len(contractions))
정규화 사전의 수:  120
# NLTK에서 제공하는 불용어 리스트를 참조해, 샘플에서 불용어를 제거하자.

print('불용어 개수 :', len(stopwords.words('english') ))
print(stopwords.words('english'))

# 데이터 전처리 함수
## Text 전처리에서만 호출하고, Summary 전처리에서는 호출하지 않는다.
def preprocess_sentence(sentence, remove_stopwords=True):
    sentence = sentence.lower() # 텍스트 소문자화
    sentence = BeautifulSoup(sentence, "lxml").text # <br />, <a href = ...> 등의 html 태그 제거
    sentence = re.sub(r'\([^)]*\)', '', sentence) # 괄호로 닫힌 문자열 (...) 제거 Ex) my husband (and myself!) for => my husband for
    sentence = re.sub('"','', sentence) # 쌍따옴표 " 제거
    sentence = ' '.join([contractions[t] if t in contractions else t for t in sentence.split(" ")]) # 약어 정규화
    sentence = re.sub(r"'s\b","", sentence) # 소유격 제거. Ex) roland's -> roland
    sentence = re.sub("[^a-zA-Z]", " ", sentence) # 영어 외 문자(숫자, 특수문자 등) 공백으로 변환
    sentence = re.sub('[m]{2,}', 'mm', sentence) # m이 3개 이상이면 2개로 변경. Ex) ummmmmmm yeah -> umm yeah

    # 불용어 제거 (Text)
    if remove_stopwords:
        tokens = ' '.join(word for word in sentence.split() if not word in stopwords.words('english') if len(word) > 1)
    # 불용어 미제거 (Summary)
    else:
        tokens = ' '.join(word for word in sentence.split() if len(word) > 1)
    return tokens
print('=3')
불용어 개수 : 179
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', "she's", 'her', 'hers', 'herself', 'it', "it's", 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', "that'll", 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', "don't", 'should', "should've", 'now', 'd', 'll', 'm', 'o', 're', 've', 'y', 'ain', 'aren', "aren't", 'couldn', "couldn't", 'didn', "didn't", 'doesn', "doesn't", 'hadn', "hadn't", 'hasn', "hasn't", 'haven', "haven't", 'isn', "isn't", 'ma', 'mightn', "mightn't", 'mustn', "mustn't", 'needn', "needn't", 'shan', "shan't", 'shouldn', "shouldn't", 'wasn', "wasn't", 'weren', "weren't", 'won', "won't", 'wouldn', "wouldn't"]
=3
# Text를 전처리하고, 결과를 확인하기 위해 상위 5개 출력

clean_text = []
# 전체 Text 데이터에 대한 전처리 : 10분 이상 시간이 걸릴 수 있습니다. 
for s in data['Text']:
    clean_text.append(preprocess_sentence(s))

# 전처리 후 출력
print("Text 전처리 후 결과: ", clean_text[:5])
Text 전처리 후 결과:  ['bought several vitality canned dog food products found good quality product looks like stew processed meat smells better labrador finicky appreciates product better', 'product arrived labeled jumbo salted peanuts peanuts actually small sized unsalted sure error vendor intended represent product jumbo', 'confection around centuries light pillowy citrus gelatin nuts case filberts cut tiny squares liberally coated powdered sugar tiny mouthful heaven chewy flavorful highly recommend yummy treat familiar story lewis lion witch wardrobe treat seduces edmund selling brother sisters witch', 'looking secret ingredient robitussin believe found got addition root beer extract ordered made cherry soda flavor medicinal', 'great taffy great price wide assortment yummy taffy delivery quick taffy lover deal']
# Summary 전처리 시에는 불용어 제거를 'False'로 지정

clean_summary = []
# 전체 Summary 데이터에 대한 전처리 : 5분 이상 시간이 걸릴 수 있습니다. 
for s in data['Summary']:
    clean_summary.append(preprocess_sentence(s, False))

print("Summary 전처리 후 결과: ", clean_summary[:5])
Summary 전처리 후 결과:  ['good quality dog food', 'not as advertised', 'delight says it all', 'cough medicine', 'great taffy']
# 이후, 다시 한 번 empty sample이 생겼는지 확인해보기.(정제 과정에서 모든 단어가 사라지는 경우도 있음)

data['Text'] = clean_text
data['Summary'] = clean_summary

# 빈 값을 Null 값으로 변환
data.replace('', np.nan, inplace=True)
print('=3')
=3
# 빈 샘플을 확인후 제거하기

print(data.isnull().sum())

data.dropna(axis=0, inplace=True)
print('전체 샘플수 :', (len(data)))
Text        0
Summary    70
dtype: int64
전체 샘플수 : 88355

데이터 전처리하기 (2) 훈련데이터와 테스트데이터 나누기

학습 진행을 위해 훈련데이터의 크기를 결정하고, 문장의 시작과 끝을 표시해줘야 한다.

1) 샘플의 최대 길이 정하기

필요없는 단어를 모두 솎아낸 데이터를 가지게 되었으니, 훈련에 사용한 샘플의 최대 길이를 정해주자.

Text,Summary의 최소,최대,평균 길이를 구하고 길이 분포를 시각화해서 보자.

# 길이 분포 출력
import matplotlib.pyplot as plt

text_len = [len(s.split()) for s in data['Text']]
summary_len = [len(s.split()) for s in data['Summary']]

print('텍스트의 최소 길이 : {}'.format(np.min(text_len)))
print('텍스트의 최대 길이 : {}'.format(np.max(text_len)))
print('텍스트의 평균 길이 : {}'.format(np.mean(text_len)))
print('요약의 최소 길이 : {}'.format(np.min(summary_len)))
print('요약의 최대 길이 : {}'.format(np.max(summary_len)))
print('요약의 평균 길이 : {}'.format(np.mean(summary_len)))

plt.subplot(1,2,1)
plt.boxplot(text_len)
plt.title('Text')
plt.subplot(1,2,2)
plt.boxplot(summary_len)
plt.title('Summary')
plt.tight_layout()
plt.show()

plt.title('Text')
plt.hist(text_len, bins = 40)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

plt.title('Summary')
plt.hist(summary_len, bins = 40)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()
텍스트의 최소 길이 : 2
텍스트의 최대 길이 : 50
텍스트의 평균 길이 : 24.113905010787324
요약의 최소 길이 : 1
요약의 최대 길이 : 8
요약의 평균 길이 : 3.4102221276854356


위 그래프처럼, 많은 양의 데이터를 다룰 때는 데이터를 시각화하는 게 도움이 된다. 이 시각화한 데이터로부터 Text 최대 길이와 Summary의 적절한 최대 길이를 임의로 정하자.

text_max_len = 50
summary_max_len = 8
print('=3')
=3
# 50, 8로 지정했을 때, 객관적으로 데이터의 몇 %가 이에 해당하는지 계산해서 판단하자.

def below_threshold_len(max_len, nested_list):
  cnt = 0
  for s in nested_list:
    if(len(s.split()) <= max_len):
        cnt = cnt + 1
  print('전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s'%(max_len, (cnt / len(nested_list))))
print('=3')

below_threshold_len(text_max_len, data['Text'])
below_threshold_len(summary_max_len,  data['Summary'])
=3
전체 샘플 중 길이가 50 이하인 샘플의 비율: 1.0
전체 샘플 중 길이가 8 이하인 샘플의 비율: 1.0
# 정해진 길이보다 길면 제외하기

data = data[data['Text'].apply(lambda x: len(x.split()) <= text_max_len)]
data = data[data['Summary'].apply(lambda x: len(x.split()) <= summary_max_len)]
print('전체 샘플수 :', (len(data)))
전체 샘플수 : 65818

2) 시작 토큰과 종료 토큰 추가하기

seq2seq훈련을 위해 디코더의 입력과 레이블에 시작 토근과 종료 토큰을 추가해야 한다.(sostoken, eostoken)

  • decoder_input : 디코더 입력에 해당하며, 시작 토큰이 맨 앞에 있는 문장의 이름
  • decoder_target: 디코더 출력에 해당하며, 종료 토큰이 맨 뒤에 붙는 문장의 이름
# 요약 데이터에는 시작 토큰과 종료 토큰을 추가한다.
data['decoder_input'] = data['Summary'].apply(lambda x : 'sostoken '+ x)
data['decoder_target'] = data['Summary'].apply(lambda x : x + ' eostoken')
data.head()

Text Summary decoder_input decoder_target
0 bought several vitality canned dog food produc... good quality dog food sostoken good quality dog food good quality dog food eostoken
1 product arrived labeled jumbo salted peanuts p... not as advertised sostoken not as advertised not as advertised eostoken
2 confection around centuries light pillowy citr... delight says it all sostoken delight says it all delight says it all eostoken
3 looking secret ingredient robitussin believe f... cough medicine sostoken cough medicine cough medicine eostoken
4 great taffy great price wide assortment yummy ... great taffy sostoken great taffy great taffy eostoken
# 인코더의 입력, 디코더의 입력 & 레이블을 다시 Numpy 타입으로 저장하기

encoder_input = np.array(data['Text']) # 인코더의 입력
decoder_input = np.array(data['decoder_input']) # 디코더의 입력
decoder_target = np.array(data['decoder_target']) # 디코더의 레이블
print('=3')
=3
# 훈련/테스트 데이터 분리

## encoder_input과 크기/형태가 같은 순서가 섞인 정수 시퀀스 생성
indices = np.arange(encoder_input.shape[0])
np.random.shuffle(indices)
print(indices)

## 만든 정수 시퀀스를 이용해 데이터의 샘플 순서를 정의
encoder_input = encoder_input[indices]
decoder_input = decoder_input[indices]
decoder_target = decoder_target[indices]
print('=3')

## 데이터를 8:2 비율로 분리. 전체 데이터 크기에서 0.2를 곱해서 테스트 데이터의 크기를 정한다.
n_of_val = int(len(encoder_input)*0.2)
print('테스트 데이터의 수 :', n_of_val)

## 정의한 테스트 데이터 개수를 이용해 전체 데이터를 split.
## : 표시의 위치에 주의!!

encoder_input_train = encoder_input[:-n_of_val]
decoder_input_train = decoder_input[:-n_of_val]
decoder_target_train = decoder_target[:-n_of_val]

encoder_input_test = encoder_input[-n_of_val:]
decoder_input_test = decoder_input[-n_of_val:]
decoder_target_test = decoder_target[-n_of_val:]

print('훈련 데이터의 개수 :', len(encoder_input_train))
print('훈련 레이블의 개수 :', len(decoder_input_train))
print('테스트 데이터의 개수 :', len(encoder_input_test))
print('테스트 레이블의 개수 :', len(decoder_input_test))
[47900 61241   778 ... 25858 24553 43623]
=3
테스트 데이터의 수 : 13163
훈련 데이터의 개수 : 52655
훈련 레이블의 개수 : 52655
테스트 데이터의 개수 : 13163
테스트 레이블의 개수 : 13163

데이터 전처리하기 (3) 정수 인코딩

1) 단어 집합(vocabulary) 만들기 및 정수 인코딩

기계가 텍스트를 숫자로 처리하도록 데이터의 단어들을 모두 정수로 바꿔야한다.(고유한 정수를 맵핑) 이 과정을 단어집합(vocabulary)을 만든다고 한다.

Keras의 토크나이저를 사용하면, 입력된 훈련 데이터로부터 단어 집합을 만들 수 있다.

src_tokenizer = Tokenizer() # 토크나이저 정의
src_tokenizer.fit_on_texts(encoder_input_train) # 입력된 데이터로부터 단어 집합 생성
print('=3')
=3

단어 집합이 생성되고, 고유한 정수가 부여되었다. 생성된 단어 집합은 src_tokenizer.word_index에 저장되어 있다. 이 중 빈도수가 낮은 단어는 제외하고 진행하자.

src_tokenizer.word_counts.items()에는 단어와 각 단어의 등장 빈도수가 저장되어 있는데, 이를 통해 통계적인 정보를 얻을 수 있다.

threshold = 7
total_cnt = len(src_tokenizer.word_index) # 단어의 수
rare_cnt = 0 # 등장 빈도수가 threshold보다 작은 단어의 개수를 카운트
total_freq = 0 # 훈련 데이터의 전체 단어 빈도수 총 합
rare_freq = 0 # 등장 빈도수가 threshold보다 작은 단어의 등장 빈도수의 총 합

# 단어와 빈도수의 쌍(pair)을 key와 value로 받는다.
for key, value in src_tokenizer.word_counts.items():
    total_freq = total_freq + value

    # 단어의 등장 빈도수가 threshold보다 작으면
    if(value < threshold):
        rare_cnt = rare_cnt + 1
        rare_freq = rare_freq + value

print('단어 집합(vocabulary)의 크기 :', total_cnt)
print('등장 빈도가 %s번 이하인 희귀 단어의 수: %s'%(threshold - 1, rare_cnt))
print('단어 집합에서 희귀 단어를 제외시킬 경우의 단어 집합의 크기 %s'%(total_cnt - rare_cnt))
print("단어 집합에서 희귀 단어의 비율:", (rare_cnt / total_cnt)*100)
print("전체 등장 빈도에서 희귀 단어 등장 빈도 비율:", (rare_freq / total_freq)*100)
단어 집합(vocabulary)의 크기 : 32039
등장 빈도가 6번 이하인 희귀 단어의 수: 23811
단어 집합에서 희귀 단어를 제외시킬 경우의 단어 집합의 크기 8228
단어 집합에서 희귀 단어의 비율: 74.31879896376292
전체 등장 빈도에서 희귀 단어 등장 빈도 비율: 3.4051462933937278

등장 빈도가 threshold 값인 7회 미만, 즉 6회 이하인 단어들은 단어 집합에서 무려 70% 이상을 차지한다. 하지만 실제로 훈련 데이터에서 등장 빈도로 차지하는 비중은 상대적으로 적은 수치인 3.39%밖에 되지 않는다.

그래서 등장 빈도가 6회 이하인 단어들은 정수 인코딩 과정에서 빼고, 훈련 데이터에서 제거하고자 한다. 위에서 이를 제외한 단어 집합의 크기를 8천여 개로 계산했는데, 이와 비슷한 값으로 어림잡아 단어 집합의 크기를 8,000으로 제한해보자. 토크나이저를 정의할 때 num_words의 값을 정해주면, 단어 집합의 크기를 제한할 수 있다.

src_vocab = 8000
src_tokenizer = Tokenizer(num_words=src_vocab) # 단어 집합의 크기를 8,000으로 제한
src_tokenizer.fit_on_texts(encoder_input_train) # 단어 집합 재생성
print('=3')
=3
# texts_to_sequences()는 생성된 단어 집합에 기반하여 입력으로 주어진 단어들을 모두 정수로 변환한다(정수인코딩).
# 단어 집합의 크기를 8000으로 제한했으니, 8000이 넘는 숫자들은 정수 인코딩 후에는 존재하지 않는다.

# 텍스트 시퀀스를 정수 시퀀스로 변환
encoder_input_train = src_tokenizer.texts_to_sequences(encoder_input_train) 
encoder_input_test = src_tokenizer.texts_to_sequences(encoder_input_test)

# 잘 진행되었는지 샘플 출력
print(encoder_input_train[:3])
[[1537, 1090, 1783, 989, 1869, 643, 1771, 1463, 58, 2790, 102], [137, 89, 1993, 253, 3, 9, 3399, 1153, 121, 1898, 1402, 7201, 578, 3598, 3991, 1495], [212, 14, 123, 1110, 348, 177, 49, 162, 39, 405, 318, 2681, 49, 6, 538, 1143, 3292, 26, 6, 1136, 56, 421, 244]]
=3
# Summary 데이터에 대해서도 동일한 작업 수행
tar_tokenizer = Tokenizer()
tar_tokenizer.fit_on_texts(decoder_input_train)
print('=3')


# tar_tokenizer.word_counts.items()에는 단어와 각 단어의 등장 빈도수가 저장돼 있다. 
# 이를 통해 등장 빈도수가 6회 미만인 단어들이 이 데이터에서 얼만큼의 비중을 차지하는지 확인해보자.
threshold = 6
total_cnt = len(tar_tokenizer.word_index) # 단어의 수
rare_cnt = 0 # 등장 빈도수가 threshold보다 작은 단어의 개수를 카운트
total_freq = 0 # 훈련 데이터의 전체 단어 빈도수 총 합
rare_freq = 0 # 등장 빈도수가 threshold보다 작은 단어의 등장 빈도수의 총 합

# 단어와 빈도수의 쌍(pair)을 key와 value로 받는다.
for key, value in tar_tokenizer.word_counts.items():
    total_freq = total_freq + value

    # 단어의 등장 빈도수가 threshold보다 작으면
    if(value < threshold):
        rare_cnt = rare_cnt + 1
        rare_freq = rare_freq + value

print('단어 집합(vocabulary)의 크기 :', total_cnt)
print('등장 빈도가 %s번 이하인 희귀 단어의 수: %s'%(threshold - 1, rare_cnt))
print('단어 집합에서 희귀 단어를 제외시킬 경우의 단어 집합의 크기 %s'%(total_cnt - rare_cnt))
print("단어 집합에서 희귀 단어의 비율:", (rare_cnt / total_cnt)*100)
print("전체 등장 빈도에서 희귀 단어 등장 빈도 비율:", (rare_freq / total_freq)*100)
단어 집합(vocabulary)의 크기 : 10491
등장 빈도가 5번 이하인 희귀 단어의 수: 8121
단어 집합에서 희귀 단어를 제외시킬 경우의 단어 집합의 크기 2370
단어 집합에서 희귀 단어의 비율: 77.40920789247927
전체 등장 빈도에서 희귀 단어 등장 빈도 비율: 5.914596600915437
# 이전과 동일하게, 등장 빈도가 5회 이하인 단어들을 제거한다.
# 어림잡아 2000을 단어 집합의 크기로 제한한다.

tar_vocab = 2000
tar_tokenizer = Tokenizer(num_words=tar_vocab) 
tar_tokenizer.fit_on_texts(decoder_input_train)
tar_tokenizer.fit_on_texts(decoder_target_train)

# 텍스트 시퀀스를 정수 시퀀스로 변환
decoder_input_train = tar_tokenizer.texts_to_sequences(decoder_input_train) 
decoder_target_train = tar_tokenizer.texts_to_sequences(decoder_target_train)
decoder_input_test = tar_tokenizer.texts_to_sequences(decoder_input_test)
decoder_target_test = tar_tokenizer.texts_to_sequences(decoder_target_test)

# 잘 변환되었는지 확인
print('input')
print('input ',decoder_input_train[:5])
print('target')
print('decoder ',decoder_target_train[:5])
input
input  [[1, 42, 9, 57], [1, 268, 1156], [1, 8, 113, 349], [1, 3, 7, 1548], [1, 29]]
target
decoder  [[42, 9, 57, 2], [268, 1156, 2], [8, 113, 349, 2], [3, 7, 1548, 2], [29, 2]]

정수 인코딩 작업이 끝났다. 하지만, 패딩으로 넘어가기 전에 점검해야 할 것이 있다. 바로 요약문에서 길이가 0이 된 샘플들을 삭제하는 것이다! decoder_input에는 sostoken, decoder_token에는 eostoken이 추가된 상태이고, 그렇기에 길이가 0이된 요약문의 실제 길이는 1로 나올 것이다. 이 점을 주의하여 코드를 수행하자.

drop_train = [index for index, sentence in enumerate(decoder_input_train) if len(sentence) == 1]
drop_test = [index for index, sentence in enumerate(decoder_input_test) if len(sentence) == 1]

print('삭제할 훈련 데이터의 개수 :', len(drop_train))
print('삭제할 테스트 데이터의 개수 :', len(drop_test))

encoder_input_train = [sentence for index, sentence in enumerate(encoder_input_train) if index not in drop_train]
decoder_input_train = [sentence for index, sentence in enumerate(decoder_input_train) if index not in drop_train]
decoder_target_train = [sentence for index, sentence in enumerate(decoder_target_train) if index not in drop_train]

encoder_input_test = [sentence for index, sentence in enumerate(encoder_input_test) if index not in drop_test]
decoder_input_test = [sentence for index, sentence in enumerate(decoder_input_test) if index not in drop_test]
decoder_target_test = [sentence for index, sentence in enumerate(decoder_target_test) if index not in drop_test]

print('훈련 데이터의 개수 :', len(encoder_input_train))
print('훈련 레이블의 개수 :', len(decoder_input_train))
print('테스트 데이터의 개수 :', len(encoder_input_test))
print('테스트 레이블의 개수 :', len(decoder_input_test))
삭제할 훈련 데이터의 개수 : 1257
삭제할 테스트 데이터의 개수 : 343
훈련 데이터의 개수 : 51398
훈련 레이블의 개수 : 51398
테스트 데이터의 개수 : 12820
테스트 레이블의 개수 : 12820

2) 패딩하기

정수 시퀀스로 변환이 되었다면, 서로 다른 길이의 샘플들을 병렬 처리하기 위해 같은 길이로 맞춰주어야 한다. 아까 정해둔 최대 길이로 패딩을 하자. 최대 길이보다 짧은 데이터들은 뒤의 공간에 숫자 0을 넣어 최대 길이로 맞춰준다.

encoder_input_train = pad_sequences(encoder_input_train, maxlen=text_max_len, padding='post')
encoder_input_test = pad_sequences(encoder_input_test, maxlen=text_max_len, padding='post')
decoder_input_train = pad_sequences(decoder_input_train, maxlen=summary_max_len, padding='post')
decoder_target_train = pad_sequences(decoder_target_train, maxlen=summary_max_len, padding='post')
decoder_input_test = pad_sequences(decoder_input_test, maxlen=summary_max_len, padding='post')
decoder_target_test = pad_sequences(decoder_target_test, maxlen=summary_max_len, padding='post')
print('=3')
=3

모델 설계하기

1) 인코더 설계

함수형 API를 이용해서 인코더를 설계하자.

from tensorflow.keras.layers import Input, LSTM, Embedding, Dense, Concatenate, TimeDistributed
from tensorflow.keras.models import Model
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint


# 인코더 설계 시작
embedding_dim = 128
hidden_size = 256

# 인코더
encoder_inputs = Input(shape=(text_max_len,))

# 인코더의 임베딩 층
enc_emb = Embedding(src_vocab, embedding_dim)(encoder_inputs)

# 인코더의 LSTM 1
encoder_lstm1 = LSTM(hidden_size, return_sequences=True, return_state=True ,dropout = 0.4, recurrent_dropout = 0.4)
encoder_output1, state_h1, state_c1 = encoder_lstm1(enc_emb)

# 인코더의 LSTM 2
encoder_lstm2 = LSTM(hidden_size, return_sequences=True, return_state=True, dropout=0.4, recurrent_dropout=0.4)
encoder_output2, state_h2, state_c2 = encoder_lstm2(encoder_output1)

# 인코더의 LSTM 3
encoder_lstm3 = LSTM(hidden_size, return_state=True, return_sequences=True, dropout=0.4, recurrent_dropout=0.4)
encoder_outputs, state_h, state_c= encoder_lstm3(encoder_output2)
WARNING:tensorflow:Layer lstm will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.
WARNING:tensorflow:Layer lstm_1 will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.
WARNING:tensorflow:Layer lstm_2 will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.

hidden state는 LSTM에서 얼만큼의 수용력(capacity)를 가질지를 정하는 파라미터이다.

인코더의 LSTM은 총 3개의 층으로 구성해서 모델의 복잡도를 높였다. hidden state의 크기를 늘리는 것이 LSTM 층 1개의 용량을 늘린다면, 3개의 층을 사용하는 것은 모델의 용량을 늘린다고 볼 수 있다. 3개 층을 지나서 인코더로부터 나온 출력 벡터는 디코더로 보내져야 한다.

또한 LSTM은 dropout 뿐 아니라 recurrent dropout까지 사용한다. 일반적인 dropout은 레이어의 weight를 랜덤으로 생략하여 모델의 과적합을 해결해준다. 반면 recurrent dropout은 dropout을 레이어가 아닌 time step마다 해주는 방식이다. 즉, time step의 입력을 랜덤으로 생략해준다. recurrent dropout은 일반적인 dropout과 같이 regularization을 해주는 효과가 있고, 과적합을 방지할 수 있다.

dropout(왼쪽)과 dropout + recurrent dropout(오른쪽)

위 그림은 일반적인 dropout과 recurrent dropout을 동시에 사용한 것을 시각적으로 표현한 것이다. 색이 있는 화살표는 dropout을 나타낸 것이다. (참고: dropout과 recurrent dropout을 모두 사용한 것을 Variational Dropout이라고도 한다.)

참고로 recurrent dropout을 사용하면 아래와 같은 경고문이 뜬다.

WARNING:tensorflow:Layer lstm_15 will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU
cuDNN을 사용할 수 없어서 recurrent dropout을 사용하지 않을 때보다 학습 시간이 오래 걸린다.

Recurrent Dropout에 대한 설명:Recurrent Dropout without Memory Loss

2) 이제 디코더를 설계해보자!

# 디코더 설계
decoder_inputs = Input(shape=(None,))

# 디코더의 임베딩 층
dec_emb_layer = Embedding(tar_vocab, embedding_dim)
dec_emb = dec_emb_layer(decoder_inputs)

# 디코더의 LSTM
decoder_lstm = LSTM(hidden_size, return_sequences=True, return_state=True, dropout=0.4, recurrent_dropout=0.2)
decoder_outputs, _, _ = decoder_lstm(dec_emb, initial_state=[state_h, state_c])
WARNING:tensorflow:Layer lstm_3 will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.

디코더 임베딩 층과 LSTM을 설계하는 것은 거의 동일하나, 입력을 정의할 때, initial_state의 인자값으로 인코더의 hidden state와 cell state의 값을 넣어줘야 한다.

3) 이제 디코더의 출력층을 설계해보자.

# 디코더의 출력층
decoder_softmax_layer = Dense(tar_vocab, activation='softmax')
decoder_softmax_outputs = decoder_softmax_layer(decoder_outputs) 

# 모델 정의
model = Model([encoder_inputs, decoder_inputs], decoder_softmax_outputs)
model.summary()
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, 50)]         0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, 50, 128)      1024000     input_1[0][0]                    
__________________________________________________________________________________________________
lstm (LSTM)                     [(None, 50, 256), (N 394240      embedding[0][0]                  
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
lstm_1 (LSTM)                   [(None, 50, 256), (N 525312      lstm[0][0]                       
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, None, 128)    256000      input_2[0][0]                    
__________________________________________________________________________________________________
lstm_2 (LSTM)                   [(None, 50, 256), (N 525312      lstm_1[0][0]                     
__________________________________________________________________________________________________
lstm_3 (LSTM)                   [(None, None, 256),  394240      embedding_1[0][0]                
                                                                 lstm_2[0][1]                     
                                                                 lstm_2[0][2]                     
__________________________________________________________________________________________________
dense (Dense)                   (None, None, 2000)   514000      lstm_3[0][0]                     
==================================================================================================
Total params: 3,633,104
Trainable params: 3,633,104
Non-trainable params: 0
__________________________________________________________________________________________________

4) 어텐션 메커니즘

Tensorflow에서 구현해둔 어텐션 함수를 가져와서 디코더의 출력층에 어떤 방식으로 결합하는지 배워보자. 여기서의 어텐션 함수는 Bahdanau style의 어텐션이다.

자세한 설명: 텐서플로우 홈페이지

아래와 같이 어텐션 층을 만들고, 위에서 설계한 디코더의 출력층을 수정해보자

from tensorflow.keras.layers import AdditiveAttention

# 어텐션 층(어텐션 함수)
attn_layer = AdditiveAttention(name='attention_layer')

# 인코더와 디코더의 모든 time step의 hidden state를 어텐션 층에 전달하고 결과를 리턴
attn_out = attn_layer([decoder_outputs, encoder_outputs])


# 어텐션의 결과와 디코더의 hidden state들을 연결
decoder_concat_input = Concatenate(axis=-1, name='concat_layer')([decoder_outputs, attn_out])

# 디코더의 출력층
decoder_softmax_layer = Dense(tar_vocab, activation='softmax')
decoder_softmax_outputs = decoder_softmax_layer(decoder_concat_input)

# 모델 정의
model = Model([encoder_inputs, decoder_inputs], decoder_softmax_outputs)
model.summary()
Model: "model_2"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, 50)]         0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, 50, 128)      1024000     input_1[0][0]                    
__________________________________________________________________________________________________
lstm (LSTM)                     [(None, 50, 256), (N 394240      embedding[0][0]                  
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
lstm_1 (LSTM)                   [(None, 50, 256), (N 525312      lstm[0][0]                       
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, None, 128)    256000      input_2[0][0]                    
__________________________________________________________________________________________________
lstm_2 (LSTM)                   [(None, 50, 256), (N 525312      lstm_1[0][0]                     
__________________________________________________________________________________________________
lstm_3 (LSTM)                   [(None, None, 256),  394240      embedding_1[0][0]                
                                                                 lstm_2[0][1]                     
                                                                 lstm_2[0][2]                     
__________________________________________________________________________________________________
attention_layer (AdditiveAttent (None, None, 256)    256         lstm_3[0][0]                     
                                                                 lstm_2[0][0]                     
__________________________________________________________________________________________________
concat_layer (Concatenate)      (None, None, 512)    0           lstm_3[0][0]                     
                                                                 attention_layer[0][0]            
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, None, 2000)   1026000     concat_layer[0][0]               
==================================================================================================
Total params: 4,145,360
Trainable params: 4,145,360
Non-trainable params: 0
__________________________________________________________________________________________________

위 코드는 인코더의 hidden state들과 디코더의 hidden state들을 어텐션 함수의 입력으로 사용하고, 어텐션 함수가 리턴한 값을 예측 시에 디코더의 hidden state와 함께 활용하는 형태로 작동하고 있다.

모델 훈련하기

1) 훈련 진행하기

model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy')
es = EarlyStopping(monitor='val_loss', patience=2, verbose=1)
history = model.fit(x=[encoder_input_train, decoder_input_train], y=decoder_target_train, \
          validation_data=([encoder_input_test, decoder_input_test], decoder_target_test), \
          batch_size=256, callbacks=[es], epochs=50)
Epoch 1/50
201/201 [==============================] - 142s 666ms/step - loss: 2.3673 - val_loss: 2.2697
Epoch 2/50
201/201 [==============================] - 132s 659ms/step - loss: 2.2251 - val_loss: 2.1659
Epoch 3/50
201/201 [==============================] - 131s 653ms/step - loss: 2.1138 - val_loss: 2.0695
Epoch 4/50
201/201 [==============================] - 130s 647ms/step - loss: 2.0296 - val_loss: 2.0070
Epoch 5/50
201/201 [==============================] - 130s 646ms/step - loss: 1.9653 - val_loss: 1.9682
Epoch 6/50
201/201 [==============================] - 130s 646ms/step - loss: 1.9113 - val_loss: 1.9338
Epoch 7/50
201/201 [==============================] - 130s 648ms/step - loss: 1.8656 - val_loss: 1.9101
Epoch 8/50
201/201 [==============================] - 130s 648ms/step - loss: 1.8253 - val_loss: 1.8975
Epoch 9/50
201/201 [==============================] - 130s 647ms/step - loss: 1.7902 - val_loss: 1.8743
Epoch 10/50
201/201 [==============================] - 130s 648ms/step - loss: 1.7567 - val_loss: 1.8629
Epoch 11/50
201/201 [==============================] - 130s 646ms/step - loss: 1.7259 - val_loss: 1.8582
Epoch 12/50
201/201 [==============================] - 129s 644ms/step - loss: 1.6979 - val_loss: 1.8518
Epoch 13/50
201/201 [==============================] - 130s 645ms/step - loss: 1.6719 - val_loss: 1.8434
Epoch 14/50
201/201 [==============================] - 130s 649ms/step - loss: 1.6472 - val_loss: 1.8419
Epoch 15/50
201/201 [==============================] - 129s 644ms/step - loss: 1.6231 - val_loss: 1.8398
Epoch 16/50
201/201 [==============================] - 129s 643ms/step - loss: 1.6009 - val_loss: 1.8337
Epoch 17/50
201/201 [==============================] - 129s 642ms/step - loss: 1.5779 - val_loss: 1.8363
Epoch 18/50
201/201 [==============================] - 129s 642ms/step - loss: 1.5578 - val_loss: 1.8372
Epoch 00018: early stopping

EarlyStopping은 특정 조건이 충족되면 훈련을 멈춘다.

es = EarlyStopping(monitor='val_loss', patience=2, verbose=1)

위 코드에서는 val_loss을 관찰하다가, 검층 데이터의 손실이 증가하는 현상이 2회(patience=2) 관측되면 학습을 멈추게 설계되어 있다!

EarlyStopping 내용

2) 훈련 데이터의 손실과 검증 데이터의 손실이 줄어드는 과정을 시각화

plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
plt.show()

인퍼런스 모델 구현하기

테스트 단계에서는 정수 인덱스 행렬로 존재하던 텍스트 데이터를 실제 데이터로 복원해야 하므로, 필요한 3개의 사전을 아래와 같이 미리 준비해두자.

src_index_to_word = src_tokenizer.index_word # 원문 단어 집합에서 정수 -> 단어를 얻음
tar_word_to_index = tar_tokenizer.word_index # 요약 단어 집합에서 단어 -> 정수를 얻음
tar_index_to_word = tar_tokenizer.index_word # 요약 단어 집합에서 정수 -> 단어를 얻음

print('=3')
=3

seq2seq는 훈련 때와 실제 동작(인퍼런스 단계)의 방식이 달라서, 그에 맞게 모델 설계를 별도로 진행해야 한다!

  • 훈련 단계에서는 디코더의 입력부에 정답이 되는 문장 전체를 한번에 넣고 디코더의 출력과 한 번에 비교할 수 있으므로, 인코더와 디코더를 엮은 통짜 모델 하나만 준비했다.

  • 하지만, 정답 문장이 없는 인퍼런스 단계에서는 만들어야 할 문장 길이만큼 디코더가 *반복 구조로 동작해야 하기 때문에, 부득이하게 인퍼런스를 위한 모델 설계를 별도로 해주어야 한다. *

1) 인코더 설계

# 인코더 설계
encoder_model = Model(inputs=encoder_inputs, outputs=[encoder_outputs, state_h, state_c])

# 이전 시점의 상태들을 저장하는 텐서
decoder_state_input_h = Input(shape=(hidden_size,))
decoder_state_input_c = Input(shape=(hidden_size,))

dec_emb2 = dec_emb_layer(decoder_inputs)

# 문장의 다음 단어를 예측하기 위해서 초기 상태(initial_state)를 이전 시점의 상태로 사용. 이는 뒤의 함수 decode_sequence()에 구현
# 훈련 과정에서와 달리 LSTM의 리턴하는 은닉 상태와 셀 상태인 state_h와 state_c를 버리지 않음.
decoder_outputs2, state_h2, state_c2 = decoder_lstm(dec_emb2, initial_state=[decoder_state_input_h, decoder_state_input_c])

print('=3')
=3

2) 어텐션 메커니즘을 사용하는 출력층 설계

# 어텐션 함수
decoder_hidden_state_input = Input(shape=(text_max_len, hidden_size))
attn_out_inf = attn_layer([decoder_outputs2, decoder_hidden_state_input])
decoder_inf_concat = Concatenate(axis=-1, name='concat')([decoder_outputs2, attn_out_inf])

# 디코더의 출력층
decoder_outputs2 = decoder_softmax_layer(decoder_inf_concat) 

# 최종 디코더 모델
decoder_model = Model(
    [decoder_inputs] + [decoder_hidden_state_input,decoder_state_input_h, decoder_state_input_c],
    [decoder_outputs2] + [state_h2, state_c2])

print('=3')
=3

3) 인퍼런스 단계에서 단어 시퀀스를 완성하는 함수 만들기|

def decode_sequence(input_seq):
    # 입력으로부터 인코더의 상태를 얻음
    e_out, e_h, e_c = encoder_model.predict(input_seq)

     # <SOS>에 해당하는 토큰 생성
    target_seq = np.zeros((1,1))
    target_seq[0, 0] = tar_word_to_index['sostoken']

    stop_condition = False
    decoded_sentence = ''
    while not stop_condition: # stop_condition이 True가 될 때까지 루프 반복

        output_tokens, h, c = decoder_model.predict([target_seq] + [e_out, e_h, e_c])
        sampled_token_index = np.argmax(output_tokens[0, -1, :])
        sampled_token = tar_index_to_word[sampled_token_index]

        if (sampled_token!='eostoken'):
            decoded_sentence += ' '+sampled_token

        #  <eos>에 도달하거나 최대 길이를 넘으면 중단.
        if (sampled_token == 'eostoken'  or len(decoded_sentence.split()) >= (summary_max_len-1)):
            stop_condition = True

        # 길이가 1인 타겟 시퀀스를 업데이트
        target_seq = np.zeros((1,1))
        target_seq[0, 0] = sampled_token_index

        # 상태를 업데이트 합니다.
        e_h, e_c = h, c

    return decoded_sentence
print('=3')
=3

모델 테스트하기

테스트 단계에서는 정수 시퀀스를 텍스트 시퀀스로 변환하여 결과를 확인하는 것이 편할 것이다. 주어진 정수 시퀀스를 텍스트 시퀀스로 변환하는 함수를 만들어보자. 함수를 만들 때,

  • Text의 정수 시퀀스에서는 패딩을 위해 사용되는 숫자 0을 제외하고
  • Summary의 정수 시퀀스에서는 숫자 0, 시작 토큰의 인덱스, 종료 토큰의 인덱스를 출력에서 제외하도록 만들자.
# 원문의 정수 시퀀스를 텍스트 시퀀스로 변환
def seq2text(input_seq):
    temp=''
    for i in input_seq:
        if (i!=0):
            temp = temp + src_index_to_word[i]+' '
    return temp

# 요약문의 정수 시퀀스를 텍스트 시퀀스로 변환
def seq2summary(input_seq):
    temp=''
    for i in input_seq:
        if ((i!=0 and i!=tar_word_to_index['sostoken']) and i!=tar_word_to_index['eostoken']):
            temp = temp + tar_index_to_word[i] + ' '
    return temp

print('=3')
=3
# 테스트 데이터 50개 샘플에 대한 실제 요약과 예측 요약 비교

for i in range(50, 100):
    print("원문 :", seq2text(encoder_input_test[i]))
    print("실제 요약 :", seq2summary(decoder_input_test[i]))
    print("예측 요약 :", decode_sequence(encoder_input_test[i].reshape(1, text_max_len)))
    print("\n")
원문 : makes tasty coffee drink every day smooth yet body among favorite keurig cup coffees 
실제 요약 : tasty 
예측 요약 :  great coffee


원문 : brought us mere taste cereals truly blessed lucky charms cinnamon toast crunch would throw man made attempt creating god given cereal 
실제 요약 : god eats this 
예측 요약 :  not as good as the same


원문 : taken drinking one black red rain morning one red red rain afternoon caffiene intake day replacing much expensive original hour energy subscription program think tastes better reason stopped addiction diet coke good advertised 
실제 요약 : great substitute for hour energy 
예측 요약 :  great for espresso


원문 : excited saw cracker jack box read review arrived one buy grocery store unfortunately cannot return also unhappy really wanted box cracker barrel sells genuine box know 
실제 요약 : am not happy 
예측 요약 :  not as described


원문 : tried alot asparagus far best enjoyed garlic spices lightly pickled variety excellant bloody mary vendor job done right also highly recommend sprouts even think like sprouts stars winner 
실제 요약 : wow these are fantastic 
예측 요약 :  best seasoning ever


원문 : cannot say ever ate piece chocolate like chocolate bar best nice rich sweet flavor disappeared pretty quick house left table share like support newman products donate tax profits charity plus chocolate bar also organic like fattening perfect food 
실제 요약 : delicious taste and go to 
예측 요약 :  chocolate heaven


원문 : currently dairy free egg free soy free peanut free diet cookie makes okay soft chocolatey continue eat even go back regular food 
실제 요약 : great product 
예측 요약 :  great cookie


원문 : hot hot hot hot hot omg got make salsa others brother tried pod rolling floor funny watching reaction would dare eat one straight though salsa made mouth fire love order 
실제 요약 : perfect 
예측 요약 :  hot hot hot


원문 : get crackers locally price lot higher absolutely love crackers deep rich cheese flavor continue purchase crackers amazon complement holiday 
실제 요약 : great taste 
예측 요약 :  great crackers


원문 : candy bars really delicious chocolate seems richer normal kit kat bars 
실제 요약 : delicious 
예측 요약 :  great bar


원문 : nice strong brew fooled shape coffee container price awesome really happy tried 
실제 요약 : great coffee 
예측 요약 :  good coffee


원문 : insert picky month old anti veggies frustrated mom willing try anything really life saver comes time making sure daughter getting type veggie eats use great eater toddler stage came went hill sure pricey know always gobble offered eat everyday times week amazon best price 
실제 요약 : only way can get some veggies in 
예측 요약 :  my dog loves it


원문 : someone suffers occasionally digestive crispy thins among things eat times always keep crackers around add soups however also times thing properly digest great item around digestive associated flu stress work better bananas rice applesauce toast diet used ibs thin easy chew crackers really great product 
실제 요약 : helps 
예측 요약 :  great for toddlers


원문 : say wish done largest jars feel medium apples jar dried apples many years hmm maybe apples go higher one still feel apples get larger jar good tasting wonderful cut used kids 
실제 요약 : good dried apples 
예측 요약 :  love these


원문 : seriously thing fear taste fear nasty drinking notice whole lot energy really enjoy get much boost fan energy drinks like like 
실제 요약 : bad taste not much energy 
예측 요약 :  not bad


원문 : two little italian greyhounds love greenies buy wrong size greenies time usually buy teenie greenies package price must hit wrong size product big deal break product arrived quickly certain order future thought sure order correct size next time 
실제 요약 : greenies 
예측 요약 :  greenies


원문 : one favorite flavored coffees although flavors personal preference gloria jean good product would definitely recommend anyone considering flavored coffee 
실제 요약 : good flavored coffee 
예측 요약 :  good coffee


원문 : cheese mind ever since trying restaurant sf one things though im bored want burn money shipping would order worth yes cheese fan goes great wine much curious 
실제 요약 : worth the shipping 
예측 요약 :  good but not great


원문 : decided get item new shelter dog went dog toys like mad dog chews would swallowed whole later trying find product would get excited chewing would destroyed within minutes boy fit bill well month even made way yet smell broke pieces recommend product friends dogs buying another one time comes 
실제 요약 : amazing product 
예측 요약 :  not bad but not great


원문 : wasabi almonds love cannot stop eating glad come packs six 
실제 요약 : addicting is right 
예측 요약 :  great snack


원문 : really enjoyed product delicious even candy floss came good condition suprise glue everything place bit annoying trying take item breaks sticks everything least keeps everything place shipping overall excellent product 
실제 요약 : nice selection and good quality 
예측 요약 :  great product


원문 : used blender make great cereal oatmeal chemical taste good price hard find item 
실제 요약 : good stuff 
예측 요약 :  good cereal


원문 : cups smaller hold less coffee competitors also airtight package opened unless used quickly even used quickly weak watery actually like come warning label would wanted know weak watery design 
실제 요약 : rip off version 
예측 요약 :  not bad


원문 : longest time would ship know maybe afraid yummy super bubble gum would fall hands get flavor one piece works chew gum every day gym grind teeth helps alot 
실제 요약 : finally 
예측 요약 :  gum


원문 : great product actually used great veggies well cooking meals highly recommends 
실제 요약 : loved it 
예측 요약 :  great product


원문 : good whole bean coffee better well worth price brew couple scoops normal get rich flavor still worth price 
실제 요약 : good coffee 
예측 요약 :  great coffee


원문 : love good earth original caffeine free tea use hot iced tea made strong weak depending preference wonderful spicy flavor aroma need sweetened naturally sweet leftover tea put refrigerator either drink cold microwave flavor holds well 
실제 요약 : good earth original is great 
예측 요약 :  great tea


원문 : product older would purchase knew date individual boxes purchase gives one month eat outdated 
실제 요약 : cracker jack 
예측 요약 :  not


원문 : cats love flavor temptations treat time sit look husband get crumb left 
실제 요약 : cat treats 
예측 요약 :  cats love it


원문 : pretty good healthy granola product fact healthy healthier others although could used little chocolate taste also smaller granola bars good product hard recommend better 
실제 요약 : pretty good not the best 
예측 요약 :  good stuff


원문 : gotten flavor larabar grocery store loved fact reason purchased amazon cost per unit much lower peanut butter cookie bar grocery store moist delicious bars received amazon taste stale dry crumbly expiration date understand would way reordering larabars amazon 
실제 요약 : amazon larabars taste stale and dry 
예측 요약 :  stale


원문 : looking something spice kids like one mine never eat spicy food liked easy prepare follow box stir every minutes bottom get stuck burnt pound chicken sounded like alot needed balance seasoning tried less chicken good add chicken water butter chicken enough fat 
실제 요약 : kids liked it too 
예측 요약 :  great for


원문 : new favorite tea absolutely delicious even sat gotten cold good fan milk tea drink without sugar problem naturally creamy flavor fantastic tea 
실제 요약 : love this tea 
예측 요약 :  great tea


원문 : yes also bought local drugstore discovered tastes nothing like ranch thought might put wrong crackers ranch labeled container tastes like curry cumin spices read back says made india assuming ranch flavoring taste like 
실제 요약 : wrong flavor 
예측 요약 :  not bad


원문 : item delicious great way protein diet good ordered 
실제 요약 : fantastic taste 
예측 요약 :  delicious


원문 : best price buying organic maple syrup years best price line master cleanse using syrup instead sugar outstanding deal 
실제 요약 : organic syrup healthy and delicious 
예측 요약 :  great syrup


원문 : tired paying tea drink coffee shop adventure find home product tasted good coffee shop really delicious disappointed 
실제 요약 : yummy 
예측 요약 :  green tea


원문 : favorite cereal love cinnamon cereal abundance tastes much like cinnamon toast crunch cereal less sugar 
실제 요약 : perfect 
예측 요약 :  great cereal


원문 : afraid image satisfying salty crunch generally need stay past bedtime dont want eat something serious late night color orange carrot potato sage spinach delicate flavor veggie sooo good 
실제 요약 : fabulous 
예측 요약 :  good but not great


원문 : truffles excellent package arrived record speed standard shipping chosen pleased truffles experience company ordering 
실제 요약 : excellent 
예측 요약 :  great product


원문 : used dry rub barbeque rib recipe disappointed flavor neither sweet smokey spicy rather easily make home made rub far superior using recipes online 
실제 요약 : disappointing flavor 
예측 요약 :  good stuff


원문 : put sprouts sandwich becomes gourmet meal last long time fridge customer life wish made bigger quantity buy 
실제 요약 : great tasty and healthy food 
예측 요약 :  great for quick meal


원문 : bread good makes feel like eating like normal person quick easy make great taste nice firm texture 
실제 요약 : best ever bread 
예측 요약 :  great bread


원문 : price right organic tea great price far goes tried far lack character 
실제 요약 : so so 
예측 요약 :  great tea


원문 : great variety much coffee maker admit enjoyed coffee much ordering 
실제 요약 : coffee 
예측 요약 :  great coffee


원문 : ordered decorate husbands birthday cake awesome wanted darker color fondant needed color another layer problem color light blue ink starting run 
실제 요약 : awesome 
예측 요약 :  great product


원문 : really wish money grew trees tree got money would get lol kidding aside bought money tree amazon arrived baby plant tall plant tree repotted cannot wait see tall really gets would say grown least inches looks healthy unique 
실제 요약 : wish money on 
예측 요약 :  do not buy


원문 : complaint product three boxes mystery combination whole lot variety however lucky enough get happy camper 
실제 요약 : who would not love variety 
예측 요약 :  good product


원문 : learned today product previous bag bought recalled due happened across article sent household stomach flu also receipt offer coupon replacement weeks would want another bag diamond lost customer amazon seller 
실제 요약 : this company does not care about their 
예측 요약 :  great product


원문 : never problem anything amazon ordered cases earth best baby food tasty nutritious variety offer grams protein iron subscribe save cannot get enough eats everyday 
실제 요약 : my son loves these 
예측 요약 :  great for baby

기존 요약과는 다른 요약을 출력하면서 원문 내용을 담고 있는 요약들이 종종 보인다.

추가적인 성능을 개선하기 위해서는

  1. seq2seq와 어텐션 자체의 조합을 좀 더 좋게 수정하기
  2. 빔 서치(beam search)
  3. 사전 훈련된 워드 임베딩(pre-trained word embedding)
  4. 인코더-디코더 자체의 구조를 새로이 변경하는 트랜스포머(Transformer)

와 같은 방법들이 있다.

추출적 요약 해보기

앞서 seq2seq2를 통해 추상적 요약을 진행했다.

이번엔 패키지 Summa에서 제공하는 모듈인 summarize를 통해 추출적 요약을 해보자. 영화 매트릭스 시놉시스를 요약해보자!

1) 패키지 설치

$ pip list | grep summa

2) 데이터 다운로드

import requests
from summa.summarizer import summarize
# 매트릭스 시놉시스 다운로드
text = requests.get('http://rare-technologies.com/the_matrix_synopsis.txt').text
# 일부만 출력해보고, 저장이 잘 되었는지 check
print(text[:1500])
The screen is filled with green, cascading code which gives way to the title, The Matrix.

A phone rings and text appears on the screen: "Call trans opt: received. 2-19-98 13:24:18 REC: Log>" As a conversation takes place between Trinity (Carrie-Anne Moss) and Cypher (Joe Pantoliano), two free humans, a table of random green numbers are being scanned and individual numbers selected, creating a series of digits not unlike an ordinary phone number, as if a code is being deciphered or a call is being traced.

Trinity discusses some unknown person. Cypher taunts Trinity, suggesting she enjoys watching him. Trinity counters that "Morpheus (Laurence Fishburne) says he may be 'the One'," just as the sound of a number being selected alerts Trinity that someone may be tracing their call. She ends the call.

Armed policemen move down a darkened, decrepit hallway in the Heart O' the City Hotel, their flashlight beam bouncing just ahead of them. They come to room 303, kick down the door and find a woman dressed in black, facing away from them. It's Trinity. She brings her hands up from the laptop she's working on at their command.

Outside the hotel a car drives up and three agents appear in neatly pressed black suits. They are Agent Smith (Hugo Weaving), Agent Brown (Paul Goddard), and Agent Jones (Robert Taylor). Agent Smith and the presiding police lieutenant argue. Agent Smith admonishes the policeman that they were given specific orders to contact the agents first, for their

3) summarize 사용하기

  • text (str) : 요약할 테스트.
  • ratio (float, optional) – 요약문에서 원본에서 선택되는 문장 비율. 0~1 사이값
  • words (int or None, optional) – 출력에 포함할 단어 수.
  • 만약, ratio와 함께 두 파라미터가 모두 제공되는 경우 ratio는 무시한다.
  • split (bool, optional) – True면 문장 list / False는 조인(join)된 문자열을 반환

summarize는 문장 토큰화를 별도로 하지 않아도 내부적으로 문장 토큰화를 수행한다. 그렇기 때문에 문장 구분이 되어있지 않은 원문을 바로 입력으로 넣을 수 있다. 비율을 적게 주어서 요약문으로 선택되는 문장의 개수를 줄여보자.

# 원문의 0.005%만을 출력

print('Summary:')
print(summarize(text, ratio=0.005)) 
Summary:
Morpheus, Trinity, Neo, Apoc, Switch, Mouse and Cypher are jacked into the Matrix.
Trinity brings the helicopter down to the floor that Morpheus is on and Neo opens fire on the three Agents.
# 리스트로 출력 결과를 받고 싶다면 split 인자의 값을 True로 하면 된다.
print('Summary:')
print(summarize(text, ratio=0.005, split=True))
Summary:
['Morpheus, Trinity, Neo, Apoc, Switch, Mouse and Cypher are jacked into the Matrix.', 'Trinity brings the helicopter down to the floor that Morpheus is on and Neo opens fire on the three Agents.']
# 단어의 수로 요약문의 크기를 조절할 수도 있다. 
# 단어를 50개만 선택하도록 설정

print('Summary:')
print(summarize(text, words=50))
Summary:
Trinity takes Neo to Morpheus.
Morpheus, Trinity, Neo, Apoc, Switch, Mouse and Cypher are jacked into the Matrix.
Trinity brings the helicopter down to the floor that Morpheus is on and Neo opens fire on the three Agents.
728x90

'Computer Science > AI Exploration' 카테고리의 다른 글

[E-09] Pneumonia  (0) 2022.02.22
[E-08] Project  (0) 2022.02.22
[E-07] Image Segmentation  (0) 2022.02.22
[E-06]project  (0) 2022.02.22
[E-06]Sentiment analysis  (0) 2022.02.22