데이터 한 그릇

텍스트 유사도 본문

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

텍스트 유사도

장사이언스 2022. 1. 28. 15:20

텍스트 유사도

 

  1. 쿼라 데이터 사용(질의 응답 데이터)
  2. 비슷한 질문끼리 분류할 수 있다면, 미리 잘 작성된 답변을 중복 사용할 수 있게 된다.
  3. 질문들이 서로 유사한지 유사하지 않은지 유사도를 판별해야 한다.

 


데이터 EDA

데이터 불러오기

!kaggle competitions download -c quora-question-pairs

import numpy as np
import pandas as pd
import os
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
%matplotlib inline

DATA_IN_PATH = './data_in/'
train_data = pd.read_csv(DATA_IN_PATH + 'train.csv')
train_data.head()

 

캐글 API 이용해서 데이터 불러오기

다른 데이터 유형과 달리 평가 데이터가 훈련 데이터보다 더 크다.

이는 평가 데이터에 컴퓨터가 임의로 만든 데이터를 집어 넣었기 떄문이다.

 

데이터 전체 길이 및 중복 데이터 개수

 

train_set = pd.Series(train_data['question1'].tolist() + train_data['question2'].tolist()).astype(str)
train_set.head()

 

데이터는 두 질문이 유사한지 판별하기 위해서 질문에 대한 데이터를 question1, question2 칼럼으로 보관하고 있다.

이 두 데이터를 합쳐서 전체 데이터의 길이 및 중복된 데이터가 몇 개 있는지 살펴보자.

 

print('교육 데이터의 총 질문 수 : {}'.format(len(np.unique(train_set))))
print('반복해서 나타나는 질문의 수 : {}'.format(np.sum(train_set.value_counts() > 1)))

 

unique 를 통해서 개별적인 질문들이 몇 개 있는지 파악하고,

value_counts를 사용해서 1개 이상이면 중복된걸로 간주하고 그 개수를 np.sum으로 합쳐서 중복된 개수를 가져온다.

 

교육 데이터의 총 질문 수 : 537361
반복해서 나타나는 질문의 수 : 111873

 

plt.figure(figsize= (12,5))
plt.hist(train_set.value_counts(), bins = 50, alpha = 0.5, color = 'r', label = 'word')
plt.yscale('log', nonposy = 'clip')
plt.title('Log-Histogram of question appearance counts')
plt.xlabel('Numbeer of occurrences of question')
plt.ylabel('Number of questions')

 

중복 질문의 빈도수를 히스토그램으로 그려서 확인하기

 

print('중복 최대 개수 : {}'.format(np.max(train_set.value_counts())))
print('중복 최소 개수 : {}'.format(np.min(train_set.value_counts())))
print('중복 평균 개수 : {:.2f}'.format(np.mean(train_set.value_counts())))
print('중복 표준편차 : {:.2f}'.format(np.std(train_set.value_counts())))
print('중복 중간길이 : {}'.format(np.median(train_set.value_counts())))
print('제1사분위 중복 : {}'.format(np.percentile(train_set.value_counts(),25)))
print('제 3사분위 중복 : {}'.format(np.percentile(train_set.value_counts(),75)))

 

 

기초 통계량 얻기

 

plt.figure(figsize  = (12,5))
plt.boxplot([train_set.value_counts()],
           labels = ['counts'],
           showmeans = True)

 

박스플롯을 그려서 기초 통계량 시각화.

이상치가 많이 등장한다.

 

데이터에 많이 등장한 단어

 

from wordcloud import WordCloud
cloud = WordCloud(width = 800, height = 600).generate(" ".join(train_set.astype(str)))
plt.figure(figsize = (15,10))
plt.imshow(cloud)
plt.axis('off')

 

wordcloud 를 이용해서 확인한다.

 

fig, axs = plt.subplots(ncols = 1
                       )
fig.set_size_inches(6,3)
sns.countplot(train_data['is_duplicate'])

train_data['is_duplicate'].value_counts()

 

 

is_duplicate 칼럼은 question1 과 question2 문장이 서로 중복된 문장인지 아닌지에 대한 정답 레이블이다.

counplot 을 통해서 레이블의 숫자를 시각화한다.

 

중복이 아닌 데이터가 255027 개

중복인 데이터가 149263 개이다.

레이블 불균형의 문제를 가지고 있는데, 최대한 라벨의 개수를 맞추는 게 모델의 성능에 좋다.

 

1. 데이터를 줄여서 라벨의 개수를 맞출 수 있고

2. 데이터를 늘려서 라벨의 개수를 맞출 수 있다.

 

 

텍스트 데이터의 길이

 

train_length = train_set.apply(len)
train_length

각 행마다의 텍스트 길이를 train_length 함수에 저장한다.

 

plt.figure(figsize = (15,10))
plt.hist(train_length, bins = 200, range = [0,200], facecolor = 'r',label = 'train')
plt.title('Normalised histogram of character count in questions', fontsize = 15)
plt.legend()
plt.xlabel('Number of charecters', fontsize = 15)
plt.ylabel('Probability', fontsize = 15)

 

이를 히스토그램으로 시각화한다.

 

plt.figure(figsize = (12,5))
plt.boxplot(train_length,
           labels = ['char counts'])
showmeans = True

 

박스플롯.

 

다음으로 단어의 개수를 확인해보자.

 

train_words_counts =  train_set.apply(lambda x : len(x.split(' ')))

 

split함수로 각 행마다 단어별로 구분하고 그 개수를 세어줘서 train_words_counts 변수에 저장해준다.

 

plt.figure(figsize = (15,10))
plt.hist(train_words_counts, bins = 50, range = [0,50], facecolor = 'r', label = 'train')
plt.title('Normalised histogram of word count in questions', fontsize = 15)
plt.legend()
plt.xlabel('Number of words', fontsize = 15)
plt.ylabel('Probability', fontsize = 15)

 

train_words_counts 를 히스토그램으로 시각화한다.

 

plt.figure(figsize = (12,5))
plt.boxplot(train_words_counts,
           labels = ['word counts'],
           showmeans = True)

 

박스플롯.

 

특수문자 중 구두점, 물음표, 마침표가 사용된 비율과 수학 기호가 사용된 비율, 대/소문자 비율 확인

 

qmarks = np.mean(train_set.apply(lambda x : '?' in x)) #물음표가 구두점으로 쓰임
math = np.mean(train_set.apply(lambda x : '[math]' in x)) #[]
fullstop = np.mean(train_set.apply(lambda x : '.' in x)) #마침표
capital_first = np.mean(train_set.apply(lambda x: x[0].isupper())) #첫 대문자
capitals = np.mean(train_set.apply(lambda x :max([y.isupper() for y in x]))) #대문자가 몇 개?

print('물음표가 있는 질문 : {:.2f}%'.format(qmarks * 100))
print('수학수식이 있는 질문 : {:.2f}%'.format(math * 100))
print('마침표가 있는 질문 : {:.2f}%'.format(fullstop * 100))
print('첫 대문자인 질문 : {:.2f}%'.format(capital_first * 100))
print('대문자의 개수 : {:.2f}%'.format(capitals * 100))

 

train_set 각각에 apply lambda 함수를 사용해서 구하고 싶은 조건에 부합한지 부합하지 않은지를 판별해서 평균들을 구한다.

 

데이터 전처리

 

import pandas as npd
import numpy as np
import re
import json

DATA_IN_PATH = './data_in/'
train_data  = pd.read_csv(DATA_IN_PATH + 'train.csv', encoding = 'utf-8')

 

데이터를 불러온다.

 

클래스 불균형 해소

 

1. 중복인 데이터와 중복이 아닌 데이터로 나누기

2. 중복이 아닌 개수가 비슷하도록 데이터의 일부를 다시 뽑는다.

 

train_pos_data = train_data.loc[train_data['is_duplicate'] == 1] #중복인 데이터
train_neg_data = train_data.loc[train_data['is_duplicate'] == 0] #중복이 아닌 데이터

class_difference = len(train_neg_data) - len(train_pos_data) #두 변수의 길이의 차
sample_frac = 1 - (class_difference / len(train_neg_data)) #적은 데이터의 개수와 많은 데이터의 개수의 비율을 구한다.

#적은 데이터의 개수가 많은 데이터에 대한 비율을 계산.

train_neg_data = train_neg_data.sample(frac = sample_frac) #더 많은 데이터에서 더 적은 데이터 비율만큼만 뽑기


print('중복 질문 개수 : ',len(train_pos_data))
print('중복이 아닌 질문 개수 : ',len(train_neg_data))

 

 

앞에서 is_duplicate 칼럼이 중복인지 중복이 아닌지에 관한 칼럼임을 살펴봤다.

중복인 데이터와 중복이 아닌 데이터들을 따로 구분해서 train_pos_data와 train_neg_data 에 따로 저장해둔다.

 

 

그리고 적은 데이터의 개수와 많은 데이터의 개수의 비율을 구하고 sample_frac 변수에 저장한다.

이 비율을 이용해서 더 개수가 많은 데이터에서 개수가 적은 데이터의 비율만큼 샘플을 뽑는다.

그리고 중복인 데이터의 개수와 중복이 아닌 데이터의 개수를 살펴본다.

 

 

특수문자 제거 및 소문자 변환

# change_filter = re.compile(FILTERS)

question1 = [str(s) for s in train_data['question1']]
question2 = [str(s) for s in train_data['question2']]

filtered_question1 = list()
filtered_question2 = list()

for q in question1:
    filtered_question1.append(re.sub('[^a-zA-Z]'," ",q).lower())
    
for q in question2:
    filtered_question2.append(re.sub('[^a-zA-Z]'," ",q).lower())

 

정규화 식을 이용해서 question1 과 question2 데이터에서 특수문자들을 제거한다.

 

단어 token 화

 

from tensorflow.keras.preprocessing.text import Tokenizer

tokenizer = Tokenizer()
tokenizer.fit_on_texts(filtered_question1 + filtered_question2) #전체 질문의 단어들에 대한 단어id 사전 만들기

question1_sequence = tokenizer.texts_to_sequences(filtered_question1) #문장을 단어id 시퀀스 데이터로 만듬
question2_sequence = tokenizer.texts_to_sequences(filtered_question2)

 

tensorflow Tokenizer 를 이용해서 텍스트 단어들에 id를 붙이고 단어 id 시퀀스들을 만든다.

 

입력 데이터 길이 통일

 

from tensorflow.keras.preprocessing.sequence import pad_sequences

MAX_SEQUENCE_LENGTH = 31

q1_data = pad_sequences(question1_sequence, maxlen = MAX_SEQUENCE_LENGTH, padding = 'post')
q2_data = pad_sequences(question2_sequence, maxlen = MAX_SEQUENCE_LENGTH, padding = 'post')

 

입력 데이터가 모델에 들어가려면 크기가 통일되어야 하기 때문에 pad_sequences 로 길이를 맞춘다.

 

단어 id 사전

word_vocab = {}
word_vocab = tokenizer.word_index
word_vocab["<PAD>"] = 0

labels = np.array(train_data['is_duplicate'], dtype = int)

print('Shape of question1 data : {}'.format(q1_data.shape))
print('Shape of question2 data : {}'.format(q2_data.shape))
print('Shape of label : {}'.format(labels.shape))
print('Words in index : {}'.format(len(word_vocab)))

tokenizer의 word_index 함수를 이용해서 단어 id 사전을 저장해둔다.

 

전처리된 데이터들 저장(label,  단어 id사전 포함)

 

TRAIN_01_DATA = 'q1_train.npy'
TRAIN_02_DATA = 'q2_train.npy'
TRAIN_LABEL_DATA = 'label_train.npy'
DATA_CONFIGS = 'data_configs.npy'

np.save(open(DATA_IN_PATH + TRAIN_01_DATA, 'wb'), q1_data) #q1데이터 저장
np.save(open(DATA_IN_PATH + TRAIN_02_DATA,'wb'),q2_data) #q2 데이터 저장 
np.save(open(DATA_IN_PATH + TRAIN_LABEL_DATA,'wb'), labels) #라벨 데이터 저장
json.dump(data_configs, open(DATA_IN_PATH + DATA_CONFIGS,'w')) #단어 id 사전 저장

 

test 데이터 전처리

 

test_data = pd.read_csv(DATA_IN_PATH + 'test.csv', encoding = 'utf-8')
valid_ids = [type(x) == int for x in test_data.test_id]
test_data = test_data[valid_ids].drop_duplicates()

test_question1 = [str(s) for s in test_data['question1']]
test_question2 = [str(s) for s in test_data['question2']]

filtered_test_question1 = list()
filtered_test_question2 = list()

for q in test_question1:
    filtered_test_question1.append(re.sub("[^a-zA-Z]"," ", q).lower())
    
for q in test_question2:
    filtered_test_question2.append(re.sub("[^a-zA-Z]"," ",q).lower())


#train 에서 만들었던 단어 인덱스를 그대로 사용해야 한다. (tokenizer 객체 그대로 사용)

test_question1_sequence = tokenizer.texts_to_sequences(filtered_test_question1)
test_question2_sequence = tokenizer.texts_to_sequences(filtered_test_question2)

test_q1_data = pad_sequences(test_question1_sequence, maxlen = MAX_SEQUENCE_LENGTH,
                            padding = "post")
test_q2_data = pad_sequences(test_question2_sequence, maxlen = MAX_SEQUENCE_LENGTH,
                            padding = "post")

 

train 데이터와 마찬가지로 전처리 진행

 

test 데이터 저장

 

TEST_Q1_DATA = 'test_q1.npy'
TEST_Q2_DATA = 'test_q2.npy'
TEST_ID_DATA = 'test_id.npy'

np.save(open(DATA_IN_PATH + TEST_Q1_DATA,'wb'), test_q1_data)
np.save(open(DATA_IN_PATH + TEST_Q2_DATA, 'wb'), test_q2_data)
np.save(open(DATA_IN_PATH + TEST_ID_DATA,'wb'), test_id)

 

모델링

 

XG부스트 모델

대략적으로 앙상블의 정의와 종류에 대해서 살펴보자.

앙상블은 여러 모델들을 합친 모델을 의미한다.

 

  1. 배깅 : 같은 알고리즘의 모델들 여러 개를 합치고 각각의 결과들을 취합하여 최종 결론을 내는 유형(하드 보팅, 소프트 보팅)
  2. 부스팅 : 배깅과 똑같이 여러 알고리즘의 결과를 취합하지만 순차적으로 취합하면서, 이전 모델이 잘못 예측한 부분에 대해서 가중치를 줘서 다시 모델로 가서 학습하는 방식.
  3. XG부스팅 : 부스팅 기법 중, 트리 기반 부스팅 기법. 여러 개의 트리 알고리즘을 사용하지만 단순히 결과를 평균 내는 게 아니라, 결과를 보고 오답에 대해서 가충치를 부여하는 방식으로 학습. 즉, xg부스팅은 트리 부스팅 방식에 경사 하강법을 통해서 최적화하는 방식

 

 

전처리된 데이터 불러오기

 

import numpy as np

DATA_IN_PATH = './data_in/'

TRAIN_Q1_DATA_FILE = 'q1_train.npy'
TRAIN_Q2_DATA_FILE = 'q2_train.npy'
TRAIN_LABEL_DATA_FILE = 'label_train.npy'

#훈련 데이터를 가져온다.

train_q1_data = np.load(open(DATA_IN_PATH + TRAIN_Q1_DATA_FILE, 'rb'))
train_q2_data = np.load(open(DATA_IN_PATH + TRAIN_Q2_DATA_FILE, 'rb'))
train_labels = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA_FILE, 'rb'))

train_input = np.stack([train_q1_data, train_q2_data], axis = 1)
train_input.shape

 

데이터 분리

 

from sklearn.model_selection import train_test_split

train_input, eval_input, train_label, eval_label = train_test_split(train_input, train_labels, test_size = 0.2, random_state = 4242)

 

학습 데이터와 검증 데이터 DMatrix 화 하기

 

import xgboost as xgb

#xgboost 를 사용하려면 입력값을 DMatrix 형태로 변환해줘야 한다.
#학습 데이터와 검증 데이터 모두 적용한다.
#sum 을 해주는 이유는 각 데이터에 대해 두 질문의 값을 하나의 값으로 만들어 주기 위함임 (array 두 개가 31 차원을 공유하고 있다.)

train_data = xgb.DMatrix(train_input.sum(axis = 1), label = train_label)
eval_data = xgb.DMatrix(eval_input.sum(axis = 1), label = eval_label)

data_list = [(train_data, 'train'),(eval_data,'valid')]

 

xgboost 를 사용하려면 입력값을 DMatrix 형태로 변환해줘야 한다.
학습 데이터와 검증 데이터 모두 적용한다.
sum 을 해주는 이유는 각 데이터에 대해 두 질문의 값을 하나의 값으로 만들어 주기 위함임 (array 두 개가 31 차원을 공유하고 있다.)

 

 

모델을 만들고 파라미터 값 설정하기

 

params = {}
params['objective'] = 'binary:logistic' #모델의 목적 함수. 결과는 내기 위한 함수.
params['eval_metric'] = 'rmse' #경사하강법을 사용한다니까 그런듯. 평가 지표.


#num boost round 는 데이터를 반복 학습하는 횟수
#early stopping rounds 는 성능이 그대로이거나 나빠진게 10번이상일떄 학습을 조기 멈추게 해준다.
#epochs = 1000 은 1000번 이걸 반복한다는 의미.

bst = xgb.train(params, train_data, num_boost_round = 1000, evals = data_list,
               early_stopping_rounds = 10)

 

목적 함수를 binary logistic 함수 사용.

평가 지표를 rmse 를 사용.

 

Test 데이터 불러오기

 

TEST_Q1_DATA_FILE = 'test_q1.npy'
TEST_Q2_DATA_FILE = 'test_q2.npy'
TEST_ID_DATA_FILE = 'test_id.npy'

test_q1_data = np.load(open(DATA_IN_PATH + TEST_Q1_DATA_FILE,'rb'))
test_q2_data = np.load(open(DATA_IN_PATH + TEST_Q2_DATA_FILE,'rb'))
test_id_data = np.load(open(DATA_IN_PATH + TEST_ID_DATA_FILE,'rb'), allow_pickle = True)

 

Test 데이터 DMatrix 변환 후 예측 결과값 내기

 

test_input = np.stack((test_q1_data, test_q2_data), axis = 1)
test_data = xgb.DMatrix(test_input.sum(axis = 1))
test_predict = bst.predict(test_data)

 

 

CNN 모델

 

감성 분석(텍스트 분류)에서의 CNN과 유사하지만, 이번 데이터는 각 텍스트 문서가 두 개로 되어 있기 때문에 두 데이터를 병렬적인 구조를 가진 모델을 만든다.

 

기준 문장 : 기준이 되는 문장

대상 문장 : 대상이 되는 문장

예) I love deep NLP 를 기준문장으로 두고 그것과 비교할 대상을 Deep NLP is awesome 으로 두고 유사한지 유사하지 않은지 비교한다.

 

데이터 불러오기

 

DATA_IN_PATH = './data_in/'
DATA_OUT_PATH = './data_out/'
TRAIN_Q1_DATA_FILE = 'q1_train.npy'
TRAIN_Q2_DATA_FILE = 'q2_train.npy'
TRAIN_LABEL_DATA_FILE = 'label_train.npy'
DATA_CONFIGS = 'data_configs.npy'

q1_data = np.load(open(DATA_IN_PATH + TRAIN_Q1_DATA_FILE, 'rb'))
q2_data = np.load(open(DATA_IN_PATH + TRAIN_Q2_DATA_FILE, 'rb'))
labels = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA_FILE, 'rb'))
prepro_configs = json.load(open(DATA_IN_PATH + DATA_CONFIGS , 'r'))

 

Sentence Embedding layer 구현

 

import tensorflow as tf

class SentenceEmbedding(tf.keras.layers.Layer):
    def __init__(self, **kargs):
        super(SentenceEmbedding, self).__init__()
        
        self.conv = tf.keras.layers.Conv1D(kargs['conv_num_filters'], kargs['conv_window_size'],
                                          activation = tf.keras.activations.relu,
                                          padding = 'same')
        
        self.max_pool = tf.keras.layers.MaxPool1D(kargs['max_pool_seq_len'],1)
        self.dense = tf.keras.layers.Dense(kargs['sent_embedding_dimension'],
                                 activation = tf.keras.activations.relu)
        
        
    def call(self, x):
        x = self.conv(x)
        x = self.max_pool(x)
        x = self.dense(x)
        
        return tf.squeeze(x, 1)

 

문장을 하나의 벡터로.

필터의 개수와 윈도의 크기를 파라미터로 지정.

각 필터마다 벡터가 생성이 됨. 만일 필터가 3개면 3개의 벡터가 생성이 됨.

맥스 풀링 해서 생긴 벡터를 합쳐서 dense 계층에 전달.

 

Sentence Similarity layer 구현

 

class SentenceSimilarityModel(tf.keras.Model):
    def __init__(self, **kargs):
        super(SentenceSimilarityModel,self).__init__(name =kargs['model_name'])
        
        self.word_embedding = tf.keras.layers.Embedding(kargs['vocab_size'],
                                                       kargs['word_embedding_dimension'])
        
        self.base_encoder = SentenceEmbedding(**kargs) #기존 문장
        self.hypo_encoder = SentenceEmbedding(**kargs) #비교할 대상 문장
        self.dense = tf.keras.layers.Dense(kargs['hidden_dimension'],
                                          activation = tf.keras.activations.relu)
        
        self.logit = tf.keras.layers.Dense(1, activation= tf.keras.activations.sigmoid)
        self.dropout = tf.keras.layers.Dropout(kargs['dropout_rate'])
        
    def call(self, x):
        x1, x2 = x
        b_x = self.word_embedding(x1)
        h_x = self.word_embedding(x2)
        b_x = self.dropout(b_x)
        h_x = self.dropout(h_x)
        
        b_x = self.base_encoder(b_x)
        h_x = self.hypo_encoder(h_x)
        
        e_x = tf.concat([b_x,h_x],-1)
        
        e_x = self.dense(e_x)
        e_x = self.dropout(e_x)
        
        return self.logit(e_x)

 

기준 문장과 비교 대상 문장을 각각 Sentence Embedding 해준다.

concat 으로 합쳐주고, 그걸 dense 계층에 전달한다.

 

모델 하이퍼파라미터 정의

 

model_name = 'cnn_similarity'
BATCH_SIZE = 1024
NUM_EPOCHS = 100
VALID_SPLIT = 0.1
MAX_LEN = 31

kargs = {'model_name' : model_name,
        'vocab_size' : prepro_configs['vocab_size'],
        'word_embedding_dimension' : 100,
        'conv_num_filters' : 300,
        'conv_window_size' : 3,
        'max_pool_seq_len' : MAX_LEN,
        'sent_embedding_dimension' : 128,
        'dropout_rate' : 0.2,
        'hidden_dimension' : 200,
        'output_dimension' : 1}

 

하이퍼 파라미터를 kargs 에 저장해둔다.

 

model = SentenceSimilarityModel(**kargs)

model.compile(optimizer = tf.keras.optimizers.Adam(1e-3),
             loss = tf.keras.losses.BinaryCrossentropy(),
             metrics = [tf.keras.metrics.BinaryAccuracy(name = 'accuracy')])

 

모델을 생성하고 학습을 정의내린다.

 

모델 학습

 

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

earlystop_callback = EarlyStopping(monitor = 'val_accuracy', min_delta = 0.0001, patience = 1)

checkpoint_path = DATA_IN_PATH + model_name + '/weights.h5'
checkpoint_dir = os.path.dirname(checkpoint_path)

if os.path.exists(checkpoint_dir):
    print('{} -- Folder already exists \n'.format(checkpoint_dir))
    
else:
    os.makedirs(checkpoint_dir, exist_ok = True)
    print('{} -- Folder create complete \n'.format(checkpoint_dir))
    
    
    
cp_callback = ModelCheckpoint(checkpoint_path,
                             monitor = 'val_accuracy',
                             verbose = 1,
                             save_best_only = True,
                             save_weights_only = True)
                             
                             
history = model.fit((q1_data, q2_data), labels, batch_size = BATCH_SIZE, epochs = NUM_EPOCHS,
                   validation_split = VALID_SPLIT, callbacks = [earlystop_callback,
                                                               cp_callback])

 

오버피팅을 막을 callback 들을 설정한다.

그리고 모델을 fit함수를 사용해서 학습시킨다.

 

LSTM(MaLSTM) 모델

 

유사도를 측정하기 위해서 코사인 유사도를 사용하는 대신에 LSTM 모델 중에서 맨허튼 거리를 사용한 LSTM 모델인 MaLSTM 을 살펴보도록 하자.

 

모델 구현

 

class Model(tf.keras.Model):
    
    def __init__(self, **kargs):
        super(Model,self).__init__(name=model_name)
        self.embedding = tf.keras.layers.Embedding(input_dim = kargs['vocab_size'],
                                                  output_dim = kargs['embedding_dimension'])
        
        self.lstm = tf.keras.layers.LSTM(units = kargs['lstm_dimension'])
        
    def call(self,x):
        x1, x2 = x
        x1 = self.embedding(x1)
        x2 = self.embedding(x2)
        x1 = self.lstm(x1)
        x2 = self.lstm(x2)
        x = tf.exp(-tf.reduce_sum(tf.abs(x1-x2), axis = 1)) #맨하튼 거리 계산
        #x1-x2 한 값을 절댓값 씌우고 두 벡터 간의 원소 간의 차이에 대한 절댓값 합을 계산하다.
        
        return x

 

기준 문장과 대상 문장에 대해서 각각 embedding 하고 lstm 모델에 넘긴다.

그리고 각각의 값들을 맨하튼 거리 계산 식에 집어넣는다.

 

 

모델 하이퍼파라미터 정의 및 학습 정의

 

model_name = 'malstm_similarity'
BATCH_SIZE = 128
NUM_EPOCHS = 5
VALID_SPLIT = 0.1

kargs = {
    'vocab_size' : prepro_configs['vocab_size'],
    'embedding_dimension' : 100,
    'lstm_dimension' : 150
}

model = Model(**kargs)

model.compile(optimizer = tf.keras.optimizers.Adam(1e-3),
             loss = tf.keras.losses.BinaryCrossentropy(),
             metrics = [tf.keras.metrics.BinaryAccuracy(name = 'accuracy')])

 

call back 설정 및 학습

 

earlystop_callback = EarlyStopping(monitor = 'val_accuracy', min_delta = 0.0001, patience = 1)
checkpoint_path = DATA_OUT_PATH + model_name + '/weights.h5'
checkpoint_dir = os.path.dirname(checkpoint_path)

if os.path.exists(checkpoint_dir):
    print("{} -- Folder already exists \n".format(checkpoint_dif))
else:
    os.makedirs(checkpoint_dir, exist_ok = True)
    print("{} -- Folder create complete \n".format(checkpoint_dir))
    
cp_callback = ModelCheckpoint(
checkpoint_path,monitor = 'val_accuracy',
verbose = 1,
save_best_only = True,
save_weights_only = True)

history = model.fit((q1_data, q2_data), labels, batch_size = BATCH_SIZE, epochs = NUM_EPOCHS,
                   validation_split = VALID_SPLIT, callbacks = [earlystop_callback,
                                                               cp_callback])

 

이상으로 두 벡터의 유사도를 확인하는 모델들을 만들어봤다.

XgBoost, CNN, MaLSTM

Comments