단순하지만 강력한 Smooth Inverse Frequency 문장 임베딩 기법

Posted by 적분 ∫2tdt=t²+c
2019.04.24 21:18 그냥 공부

자연언어처리 분야에서 임베딩 기법은 자연언어를 수치의 형태로 효과적으로 표현한다는 강점 때문에 최근 널리 사용되고 있습니다. 대표적인 것이 단어 임베딩 기법인데, Word2Vec, GloVe, FastText 등이 있지요. 이들은 단어의 의미를 벡터 공간 상의 점으로 표현하는데, 그 점이 단어의 실제 의미를 반영한다는 점에서 의미가 크지요. 그러나 많은 텍스트 처리 기법들은 단어 이상의 단위를 처리할 것을 요구받습니다. 문장이나 문단, 혹은 문헌처럼 말이지요. 따라서 당연히 문장 전체나 문단 등 그보다 큰 단위에 대해 임베딩을 실시하려는 시도가 있었습니다.

본 포스팅에서는 유명한 문장 임베딩 기법들과 함께, 간단하지만 강력한 문장 임베딩 기법인 SIF(Smooth Inverse Frequency)[각주:1]에 대해 살펴보고자 합니다.


문장 임베딩 기법

문장 임베딩(Sentence Embedding 혹은 Phrase나 Paragraph Embedding이라고도 함)은 문장 전체를 벡터 공간 상의 점으로 표현하는 기법을 말합니다. 특히 벡터 공간 상에 점으로 문장을 옮겼을때, 의미적으로 유사한 문장끼리는 유사한 지점에 모이게 되는데, 이를 통해 분류나 클러스터링과 같은 기법 뿐만 아니라, 자동 질의 응답과 같은 더 복잡한 작업도 수행할 수 있습니다. 근데 문제는 "어떻게 문장을 벡터 공간으로 옮길 것인가" 입니다. 일단 단어 임베딩 기법이 주어졌으므로 이를 이용하는 방법을 생각해볼 수 있겠구요, 최근 유행하는 RNN과 같은 신경망을 이용할 수도 있겠습니다. 몇 가지 대표적인 방법을 살펴보시지요.


단어 임베딩 기법을 활용하기

제일 쉬운 것은 문장 내의 각 단어의 임베딩을 다 합하고 평균을 구하는 것입니다.

I like an apple, not a pear.

예를 들어 위와 같은 문장이 있다면 I, like, an, apple, not, a, pear의 임베딩을 얻고 이들의 평균을 구하자는 것이지요. 짧은 문장의 경우, Word2vec을 개발한 Mikolov에 따르면[각주:2], 이 방법으로 구한 문장 임베딩도 효과적으로 쓰일 수 있다고 합니다.

근데 이 방법의 한계는 자명하게도, 문장이 길어질수록 다양한 중요하지 않은 단어들이 포함되고, 이들이 평균계산에 다 포함되기 때문에 문장 임베딩이 가리키는 의미가 희석될 수 밖에 없다는 것입니다. 위의 예시에서도 문장에서 제일 중요한 건 like, apple, pear이지, an, a 같은게 아니지요. 문장이 길어질수록 이런 불용어의 비중도 증가하기 때문에 이는 골치 아픈 문제라고 할 수 있습니다. 


이를 개선하는 것도 가능하겠죠. 입력 문장의 전처리를 잘 해서 불용어를 사전에 제거하는 것도 좋은 접근법입니다. 혹은 조금 더 정교하게, 각 단어의 가중치를 다르게 부여하는 방법도 있겠습니다. tf-idf 등의 가중치를 사용할 수도 있겠네요.


RNN 모형을 활용하기

RNN은 자신의 출력을 다시 입력으로 받는 신경망 셀을 여러 개를 겹쳐 다양한 길이의 입력을 다룰 수 있도록 확장한 신경망 모형입니다. 이를 이용하면 가변적인 길이의 입력을 고정된 크기의 숫자로 압축할 수도 있고, 고정된 크기의 숫자를 가변적 길이의 출력으로 풀어낼 수도 있습니다. 이 둘을 결합하면 seq2seq과 같이 가변 길이의 입력을 받아서 가변 길이의 출력을 생성하는 모형을 만들수도 있습니다. 이 모형은 기계번역이나 자동 질의 응답과 같은 작업에 널리 쓰이고 있는데요, 이를 이용해 문장을 임베딩하는 것도 가능합니다.


대표적인 것으로 Skip-thought vector[각주:3]가 있습니다. 이는 여러 문장을 포함한 문헌을 입력으로 받는데요, seq2seq 모형이 문장을 입력 받으면, 그 바로 앞의 문장과 바로 뒤의 문장을 출력하도록 학습을 진행합니다. 대게 붙어 있는 문장은 의미적으로 연결되는 경우가 많기 때문에, 이를 이용해 앞 뒤의 문장을 예측하도록 한 것이죠. 따라서 한 문장이 입력되면, 이 문장은 벡터 공간의 점으로 변환되고, 이 벡터 공간 상의 점은 그 문장의 바로 앞, 뒤 문장을 예측할 수 있어야 하므로, 그 의미를 반영하게 됩니다.


이와 비슷하게 seq2seq을 이용해 Autoencoder를 구현하는 방법(이전 포스팅에서 소개한 바 있음)도 있습니다. 이는 입력 문장을 고정 길이의 숫자로 변환하고 이를 다시 복원하여 원래 문장이 나오도록 seq2seq 모형을 학습함으로써, 가변 길이 문장에 대한 고정 길이 표현을 발견합니다. 이 경우는 Skip-thought vector와는 다르게 입력 문장을 그대로 복원하는 것을 목표로 하므로, 문장에 담긴 내재적인 의미보다도 문장의 표현 그 자체에 좀 더 집중한다는 차이가 있겠습니다.


간단하지만 강력한 SIF 기법

저자들이 아예 논문 제목에 'Simple but Tough-To-Beat'라는 표현을 썼기에, 저자들의 의견을 존중해 '간단하지만 강력하다'는 수식어를 붙였습니다. 본 논문에 제안된 SIF 기법은 정말 매우 간단한데요, 단어 임베딩 기법을 활용하되 가중치를 조금 다르게 주고, 이를 다듬어주는 것이 전체 기법의 전부입니다. 구체적으로 적으면 다음과 같습니다.

  1. 먼저 단어 임베딩 기법을 통해 각 단어의 임베딩을 얻어낸다.
  2. 문장 내의 모든 단어에 대해, 그 단어 임베딩을 합하되 가중치를 a / (a + P(w))로 둔다
  3. 2번의 방법으로 전체 문장들의 문장 임베딩을 구한뒤, 이를 합쳐 행렬을 구성한다. 이 행렬에 대해 SVD를 진행하여 각 문장 임베딩에서 공통 부분을 구하고, 전체 문장 임베딩에서 이 공통 부분을 제거해준다.

2번에서 a는 SIF 기법의 파라미터로 논문에 따르면 약 0.0001 ~ 0.001의 값이 적절하다고 합니다. P(w)는 해당 단어가 출현할 확률로, 이 값이 클수록 전체 가중치는 작아집니다. 즉 일반적으로 자주 등장하는 단어에 대해서는 낮은 가중치를 부여하고, 드물게 등장하는 단어에 대해서는 높은 가중치를 부여하는 것이죠.

3번에서는 문장 임베딩에서 공통부분을 제거합니다. 전체 문장 임베딩을 모아서 SVD를 실시하면 각 문장 임베딩에 대해 n개의 특잇값(Singular Value)이 나오는데, 이 중 가장 큰 첫번째 특잇값을 문장 임베딩에서 제거해줍니다. 즉 모든 문장 임베딩이 공유하는 요소를 제거해줌으로써 문장 임베딩 간의 공통점을 줄이고 차이점을 키우는 과정이라고 볼 수 있겠습니다.


2번은 단어 임베딩 기법에 IDF 가중치를 적용한 것과 매우 유사합니다. 다만 그 가중치 값이 IDF가 아니라 a / (a + P(w))라는게 차이점이구요. 3번은 SIF에서 처음 제안된 방법인데 의외로 높은 성능을 내는 것을 확인할 수가 있습니다. 이 방법이 우연히 얻어진 것은 아니구요, 사실 다음과 같은 수식으로부터 유도된 것입니다.

복잡해 보이는 식입니다만 풀어서 보면 별거 없습니다. 여기서 c가 문장 임베딩이고, w는 그 문장에 포함되는 단어 중 하나입니다. Pr(w|c)는 문장 임베딩이 c일때 w라는 단어가 문장에서 출현할 확률을 계산합니다. 확률 식은 총 2가지 항으로 계산되는데, 전자는 단어 w가 일반적으로 출현할 확률 P(w)이고, 후자는 해당 단어 w의 벡터 u와 공통 부분을 포함한 임베딩 벡터 ^c을 내적한 값입니다. Z는 모든 종류의 단어에 대해 exp(u_w * ^c)의 합을 가리킵니다. 현재 단어 w에 대한 exp값을 전체 exp 합으로 나눠 정규화함으로써 저 분수부분은 0~1사이의 확률 값을 가지게 되지요.


따라서 전자는 문장 임베딩과 상관없이 단어 w가 출현할 확률, 후자는 문장 임베딩과 현재 단어 w가 잘 어울리는 정도를 바탕으로 계산한 w가 출현할 확률입니다. 이 둘을 적절한 정도 α로 조합하여 전체 확률을 계산한게 최종적으로 단어 w가 문장 임베딩 c에서 출현할 확률이지요. w가 출현할 확률을 두 가지 항을 합하여 계산하는 것에는 나름대로 합리적인 이유가 있습니다. 문장 임베딩 c가 문장 내에서 등장하는 단어들과 밀접한 연관을 가져야하는 것은 당연합니다. 다만 모든 단어가 문장 임베딩과 관련이 있지는 않을 겁니다. 문장 내에는 문장의 핵심 의미와는 관련 없는 불용어들도 많이 등장하기 때문이죠. 따라서 문장에 어떤 단어가 등장하는 것에는 크게 두 가지 이유가 있겠죠. 

  1. 원래 여기저기 자주 등장하는 단어라서 해당 문장에 등장하거나

  2. 문장의 핵심 의미와 관련이 있어서 해당 문장에 등장하거나

위의 Pr(w|c)는 이를 잘 반영하는 확률 계산법이라고 볼 수 있지요.


한 가지 미심쩍은 부분은 ^c입니다. ^c는 문장 임베딩에 공통 부분 임베딩인 c0를 섞어서 만든 임베딩입니다. 이게 왜 필요한지는 이론적으로 좀더 고민해볼 여지가 있지만, 주어진 학습 데이터의 편향을 반영하기 위한게 아닐까 싶습니다.


c값을 구하기 위해서는 문장 내에 등장하는 모든 단어 w_i들에 대해서 Pr(w_i|c)가 최대가 되는 c를 찾아야합니다. 이를 위해 양변에 로그를 취하고 미분을 하여 전체 우도가 최대가 되는 c를 찾으면 앞서 설명한 SIF 계산식이 나오게 됩니다. 꽤 치밀하지요?


성능은?

논문을 보면 단순 단어 임베딩 평균이나 tf-idf 가중치를 적용한 단어 임베딩 평균뿐만 아니라 Skip-thought와 같이 체계적인 RNN 모형보다도 문장 유사도(similarity), 추론(entailment), 분류(classification) 등의 과제에서 SIF 기법이 5~10% 정도 더 높은 성능을 보인다는 것을 확인할 수 있습니다. 기반이 되는 단어 임베딩 기법으로는 Word2Vec보다는 GloVe가 더 높은 성능을 보였구요, Paragram-SL999이라는 반지도 학습 단어 임베딩 기법이 가장 높은 성능을 보였습니다.

SIF 기법은 문장 임베딩을 계산할 때 전혀 어순을 고려하지 않기에, 어순을 고려하는 RNN 모형에 비하면 여러 모로 한계가 있을 수 밖에 없습니다. 그런데 SIF가 RNN 기반 모형보다 높은 성능을 보였다는 점은 흥미로울 수 밖에 없죠. 두 가지 가능성이 있을 겁니다. 1) 제시된 자연언어처리 과제에서 높은 성능을 내는 데에 어순이 필요없는 것이거나 2) RNN 모형이 어순을 고려하는데에 생각보다 한계가 있다던가 말이죠.


SIF 기법은 그 자체로는 단어 출현 확률을 제외하면, 학습 데이터도 전혀 필요없고, 비지도 방법이라는 장점이 있습니다. 따라서 다양한 과제에 쉽게 적용될 수 있다는 것인데요, SIF 기법을 보면서 때론 간단한 기법들이 간단하기 때문에 더 강력할 수 있는게 아닌가 생각해보게 됩니다.


코드

사실 워낙 간단한 모형이기 때문에 직접 구현해도 크게 복잡하지 않습니다만, 저자들이 친히 코드 또한 공개해주었습니다. https://github.com/PrincetonML/SIF 코드 역시 어렵지 않으므로 필요한 곳에 쉽게 적용하여 사용 가능할 듯합니다.

  1. Arora, S., Liang, Y., & Ma, T. (2016). A simple but tough-to-beat baseline for sentence embeddings. [본문으로]
  2. Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., & Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems (pp. 3111-3119). [본문으로]
  3. Kiros, R., Zhu, Y., Salakhutdinov, R. R., Zemel, R., Urtasun, R., Torralba, A., & Fidler, S. (2015). Skip-thought vectors. In Advances in neural information processing systems (pp. 3294-3302). [본문으로]
이 댓글을 비밀 댓글로