자연어처리(NLP) 16일차 (ELMo)

정민수
13 min readJun 19, 2019

--

2019.06.19

출처 : https://wikidocs.net/book/2155

핵심키워드

  • ELMo ; Embeddings from Language Model
  • biLM
  • 양방향 RNN

엘모 (Embeddings from Language Model, ELMo)

ELMo(Embeddings from Language Model)는 2018년에 제안된 새로운 워드 임베딩 방법론이다 ELMo라는 이름은 세서미 스트리트라는 미국 인형극의 캐릭터 이름이기도 한데, 뒤에서 배우게 되는 BERT나 최근 마이크로소프트가 사용한 Big Bird라는 NLP 모델 또한 ELMo에 이어 세서미 스트리트의 캐릭터의 이름을 사용했다. ELMo는 Embeddings from Language Model의 약자이다. 해석하면 ‘언어 모델로 하는 임베딩’이다. ELMo의 가장 큰 특징은 사전 훈련된 언어 모델(Pre-trained language model)을 사용한다는 점이다. 이는 ELMo의 이름에 LM이 들어간 이유이다.

1. ELMo(Embeddings from Language Model)

Bank라는 단어를 생각해보자. Bank Account(은행 계좌)와 River Bank(강둑)에서의 Bank는 전혀 다른 의미를 가지는데, Word2Vec이나 GloVe 등으로 표현된 임베딩 벡터들은 이를 제대로 반영하지 못한다는 단점이 있다. 예를 들어서 Word2Vec이나 GloVe 등의 임베딩 방법론으로 Bank란 단어를 [0.2 0.8 -1.2]라는 임베딩 벡터로 임베딩하였다고 하면, 이 단어는 Bank Account(은행 계좌)와 River Bank(강둑)에서의 Bank는 전혀 다른 의미임에도 불구하고 두 가지 상황 모두에서 [0.2 0.8 -1.2]의 벡터가 사용된다.

그렇다면 같은 표기의 단어라도 문맥에 따라서 다르게 워드 임베딩을 할 수 있으면 자연어 처리의 성능이 더 올라가지 않을까? 단어를 임베딩하기 전에 전체 문장을 고려해서 임베딩을 하겠다는 것이다. 그래서 탄생한 것이 문맥을 반영한 워드 임베딩(Contextualized Word Embedding)이다.

2. biLM(Bidirectual Language Model)의 사전 훈련

우선 다음 단어를 예측하는 작업인 언어 모델링을 상기해보자. 아래의 그림은 은닉층이 2개인 일반적인 단방향 RNN 언어 모델의 언어 모델링을 보여준다.

RNN 언어 모델은 문장으로부터 단어 단위로 입력을 받는데, RNN 내부의 은닉 상태 hₜ의 값이 문장의 문맥 정보를 점차적으로 반영한다고 말할 수 있다. 지금 설명하는 내용은 새로운 개념이 아니라 RNN의 기본 개념이다. 그런데 ELMo는 위의 그림의 순방향 RNN뿐만 아니라, 위의 그림과는 반대 방향으로 문장을 스캔하는 역방향 RNN 또한 활용한다. ELMo는 양쪽 방향의 언어 모델을 둘 다 활용한다고하여 이 언어 모델을 biLM(Bidirectional Language Model)이라고 한다.

ELMo에서 말하는 biLM은 기본적으로 다층 구조(Multi-layer)를 전제로 한다. 은닉층이 최소 2개 이상이라는 의미이다. 아래의 그림은 은닉층이 2개인 순방향 언어 모델과 역방향 언어 모델의 모습을 보여준다.

이 때 biLM의 입력이 되는 워드 임베딩 방법으로는 책에서 다루지 않은 char CNN이라는 방법을 사용한다. 이 임베딩 방법은 글자(character) 단위로 계산되는데, 이렇게 하면 마치 서브단어(subword)의 정보를 참고하는 것처럼 문맥과 상관없이 dog란 단어와 doggy란 단어의 연관성을 찾아낼 수 있다. 또한 이 방법은 OOV에도 견고하다는 장점이 있다.

주의할 점은 앞서 RNN 챕터에서 설명한 양방향 RNN과 ELMo에서의 biLM은 다소 다르다. 양방향 RNN은 순방향 RNN의 은닉 상태와 역방향 RNN의 은닉 상태를 다음 층의 입력으로 보내기 전에 연결(concatenate)시킨다.

biLM의 순방향 언어모델과 역방향 언어모델이 각각의 은닉 상태만을 다음 은닉층으로 보내며 훈련시킨 후에 ELMo 표현으로 사용하기 위해서 은닉 상태를 연결(concatenate) 시키는 것과는 다르다. biLM의 두 언어 모델이 공유하는 층은 오직 입력을 받는 임베딩 층과 소프트맥스 함수를 통해 다음 단어를 예측하는 출력층이다.

3. biLM의 활용

biLM이 훈련되었다면, 이제 ELMo가 사전 훈련된 biLM을 통해 입력 문장으로부터 단어를 임베딩하기 위한 과정을 보자.

이 예제서는 play란 단어가 임베딩이 되고 있다는 가정 하에 ELMo를 설명한다. play란 단어를 임베딩하기 위해서 ELMo는 위의 점선의 사각형 내부의 각 층의 결과값을 재료로 사용한다. 다시 말해 해당 시점(time-step)의 biLM의 각 층의 출력값을 가져온다. 그리고 순방향 언어 모델과 역방향 언어 모델의 각 층의 출력값을 연결(concatenate)하고 추가 작업을 진행한다.

여기서 각 층의 출력값이란 첫번째는 임베딩 층을 말하며, 나머지 층은 각 층의 은닉 상태를 말한다. ELMo의 직관적인 아이디어는 각 층의 출력값이 가진 정보는 저눕 서로 다른 종류의 정보를 갖고 있을 것이므로, 이들을 모두 활용한다는 점에 있다. 아래는 ELMo가 임베딩 벡터를 얻는 과정을 보여준다.

1) 각 층의 출력값을 연결(concatenate)한다.

2) 각 층의 출력 값 별로 가중치를 준다.

이 가중치를 여기서는 s₁, s₂, s₃라고 하자.

3) 각 층의 출력값을 모두 더한다.

2)번과 3)번의 단계를 요약하여 가중합(Weighted Sum)을 한다고 할 수 있다.

4) 벡터의 크기를 결정하는 스칼라 매개변수를 곱한다.

이 스칼라 매개변수를 여기서는 γ라고 하자.

이렇게 완성된 벡터를 ELMo 표현(representation)이라고 한다. 지금까지는 ELMo 표현을 얻기 위한 과정이고 이제 ELMo를 입력으로 사용하고 수행하고 싶은 텍스트 분류, 질의 응답 시스템 등의 자연어 처리 작업이 있을 것이다. 예를 들어 텍스트 분류 작업을 하고 싶다고 가정하자. 그렇다면 ELMo 표현을 어떻게 텍스트 분류 작업에 사용할 수 있을까?

ELMo 표현은 그 자체만으로 사용한다기 보다는 기존의 임베딩 벡터와 함께 사용할 수 있다. 우선 텍스트 분류 작업을 위해서 GloVe와 같은 기존의 방법론을 사용한 임베딩 벡터를 준비했다고 하자. 이 때, GloVe를 사용한 임베딩 벡터만 텍스트 분류 작업에 사용하는 것이 아니라 이렇게 준비된 ELMo 표현을 GloVe 임베딩 벡터와 연결(concatenate)해서 입력으로 사용할 수 있다. 그리고 이 때, ELMo 표현을 만드는데 사용되는 사전 훈련된 언어 모델의 가중치는 고정시킨다. 그리고 대신 위에서 사용한 s₁, s₂, s₃와 γ는 훈련 과정에서 학습된다.

위의 그림은 ELMo 표현이 기존의 GloVe 등과 같은 임베딩 벡터와 함께 NLP 태스크의 입력이 되는 것을 보여준다.

4. ELMo 표현을 사용해서 스팸 메일 분류하기

텐서플로우 허브로부터 다양한 사전 훈련된 모델(Pre-Trained Model)들을 사용할 수 있다. 여기서는 사전 훈련된 모델로부터 ELMo 표현을 사용해보는 정도로 예제를 진행해본다. 시작 전에 텐서플로우 허브를 인스톨 해야한다. 아나콘다 프롬프트를 이용해서 설치했다.

pip install tensorflow-hub

설치가 끝났다면 이제 텐서플로우 허브를 임포트할 수 있다. ELMo를 사용해서 스팸 메일을 분류해보자.

import tensorflow_hub as hub
import tensorflow as tf
from keras import backend as K
sess = tf.Session()
K.set_session(sess)
# 세션 초기화. 이는 텐서플로우 개념.
elmo = hub.Module("https://tfhub.dev/google/elmo/1", trainable=True)
# ELMo 다운로드
sess.run(tf.global_variables_initializer())
sess.run(tf.tables_initializer())

기본적으로 필요한 것들을 임포트했다. 이제 데이터를 불러오고, 5개만 출력해보자.

파일 다운로드 링크 : https://www.kaggle.com/uciml/sms-spam-collection-dataset

import pandas as pd
data = pd.read_csv('spam.csv',encoding='latin-1')
data[:5]

여기서 필요한 건 v2열과 v1열이다. v1열은 숫자 레이블로 바꿔야 할 필요가 있다. 이를 각각 x_data와 y_data로 저장한다.

data['v1'] = data['v1'].replace(['ham','spam'],[0,1])
y_data = list(data['v1'])
x_data = list(data['v2'])

v2열을 x_data에 저장한다. v1열에 있는 ham과 spam 레이블을 각각 숫자 0과 1로 바꾸고 y_data에 저장한다. 정상적으로 저장되었는 지 이를 각각 5개만 출력해보도록 한다.

x_data[:5]
['Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...',
'Ok lar... Joking wif u oni...',
"Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's",
'U dun say so early hor... U c already then say...',
"Nah I don't think he goes to usf, he lives around here though"]
y_data[:5]
[0, 0, 1, 0, 0]

훈련 데이터와 테스트 데이터를 8:2 비율로 분할해보겠다. 그런데 그 전에 이를 위해 전체 데이터 개수의 80%와 20%는 각각 몇 개인지 확인하자.

print(len(x_data))
n_of_train = int(len(x_data)*0.8)
n_of_test = int(len(x_data) - n_of_train)
print(n_of_train)
print(n_of_test)
5572
4457
1115

전체 데이터는 5,572개이며 8:2 비율로 분할하면 각각 4,457과 1,115개가 된다. 이를 각각 훈련 데이터와 테스트 데이터의 양으로 정하여 데이터를 분할하자.

import numpy as np
X_train = np.asarray(x_data[:n_of_train])
# x_train 데이터 중에서 앞의 4457개의 데이터만 저장
y_train = np.asarray(y_data[:n_of_train])
# y_train 데이터 중에서 앞의 4457개의 데이터만 저장
X_test = np.asarray(x_data[n_of_train:])
y_test = np.asarray(y_data[n_of_train:])

이제 훈련을 위한 데이터 준비는 끝났다. 이제 ELMo와 설계한 모델을 연결하는 작업들을 진행해보자. ELMo는 텐서플로우 허브로부터 가져온 것이기 때문에 케라스에서 사용하기 위해서는 케라스에서 사용할 수 있도록 변환해주는 작업이 필요하다.

def ELMoEmbedding(x):
return elmo(tf.squeeze(tf.cast(x, tf.string)), as_dict=True,
signature='default')['default']
# 데이터의 이동이 케라스 -> 텐서플로우 -> 케라스가 되도록 하는 함수

이제 모델을 설계한다.

from keras.models import Model
from keras.layers import Dense, Lambda, Input
input_text = Input(shape=(1,), dtype=tf.string)
embedding_layer = Lambda(ELMoEmbedding, output_shape=(1024,))(input_text)
hidden_layer=Dense(256, activation='relu')(embedding_layer)
output_layer=Dense(1, activation='sigmoid')(hidden_layer)
model = Model(inputs=[input_text], output=output_layer)
model.compile(loss='binary_crossentropy',optimizer='adam',
metrics=['accuracy'])

모델은 ELMo를 이용한 임베딩 층을 거쳐서 256개의 뉴런이 있는 은닉층을 거친 후 마지막 1개의 뉴런을 통해 이진 분류를 수행한다. 이진 분류를 위한 마지막 뉴런의 활성화 함수는 시그모이드 함수이며, 모델의 손실 함수는 binary_crossentropy 이다.

history = model.fit(X_train, y_train, epochs=1, batch_size=60)Epoch 1/1
4457/4457 [==============================] - 597s 134ms/step - loss: 0.1394 - acc: 0.9479
print("\n 테스트 정확도: %.4f" % (model.evaluate(X_test, y_test)[1]))
1115/1115 [==============================] - 157s 141ms/step

테스트 정확도: 0.9767

1번의 에포크에서 98%의 정확도를 얻어냈다.

--

--

정민수
정민수

No responses yet