📌 목차

1. preview
2. Duality를 활용한 Supervised Learning

3. Duality를 활용한 Unsupervised Learning
4. Back-Translation 재해석

😚 글을 마치며...

 

 

 


1. Preview

1.1 Duality란?
우린 보통 기계학습을 통해 특정 도메인의 data X를 받아
다른 도메인의 data Y로 mapping하는 함수를 근사하는 법을 학습한다.
따라서 대부분의 기계학습에 사용되는 dataset은 두 도메인 사이 data로 구성된다.
Duality: 두 도메인 사이의 관계, 대부분의 기계학습문제는 이처럼 Duality속성을 갖는다.
특히나 NMT는 각 도메인의 data사이에 정보량의 차이가 거의 없다는 점이 큰 특징이자 장점인데, Duality를 적극적으로 활용한다면 기계번역을 고도화할 수 있다.

 

1.2 CycleGAN
번역 외 분야에서도 Duality를 활용할 수 있는데,
∙ Vision분야에서 소개한 [CycleGAN; 2017]에 대해 소개해보고 한다.


CycleGAN이란 아래그림처럼 짝지어지지않은 두 도메인의 이미지가 있을때,
X도메인의 image를 Y도메인의 image로 변환하는 방법이다.
사진의 전반적 구조는 유지하되, 모네의 화풍으로 바꾸거나 말을 얼룩말이나 민무늬로 바꿔주기도 한다.
XYYX 모두 각각 생성자 G, F와 판별자 Dx, Dy를 갖고있기에 min/max 게임을 수행한다.
G는 x를 입력으로 받아 ŷ으로 변환한다.
F는 y를 입력으로 받아 x̂ 으로 변환한다.
Dx는 x̂ 이나 x를 입력으로 받아 합성 여부를 판단한다.
CycleGAN의 동작개요
이 방식의 핵심은 x̂ 나 ŷ를 합성할 때, 기존의 도메인 X, Y에 실제로 속하는 이미지처럼 만들어 내야 한다는 것이다.
그리고 각각의 x̂ 와 ŷ로부터 원래 data로 돌아올 수 있어야 한다는 것이다.
 

Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks

Image-to-image translation is a class of vision and graphics problems where the goal is to learn the mapping between an input image and an output image using a training set of aligned image pairs. However, for many tasks, paired training data will not be a

arxiv.org

 

 

 

 

 

 

 

 

 

 

 

 

 

 


2. Duality를 활용한 Supervised Learning

2.1 DSL (Dual Supervised Learning)
이번에 소개할 논문은 Duality를 활용한 지도학습[DSL;2017]이다.
이 방법은 기존의 Teacher-Forcing의 어려움 해결 시,
RL방법대신 Duality의 Regularization Term을 도출해 해결한다.


Bayes 정리에 따르면 아래수식은 항상 성립.

그렇다면, 이 수식대로 dataset을 통해 훈련한 모델들은 아래 수식을 만족해야한다.
위와 같은 전제로 번역훈련을 위한 목표에 적용한다면, 아래와 같다.
위의 수식을 해석해보자.

∙[목표. 1]: Bayes정리에따른 제약조건을 만족 + ℓ1을 최소화
1은 번역함수 f에 입력 xi를 넣어 나온 반환값과 yi사이의 손실을 의미

∙[목표. 2]: ℓ2도 번역함수 g에대해 동일작업을 수행, 최소화

따라서 위와 같은 MSE손실함수를 만들 수 있다.
이 함수는 Bayes정리에따른 제약조건의 양 변의 값의 차이를 최소화한다.


∙ 위의 수식에서 우리가 동시에 훈련시키는 신경망파라미터로 logP(y | x; θx→y)와 logP(x | y; θy→x)를 구하고 
∙ monolingual corpus를 통해 별도로 이미 훈련시켜 놓은 LM으로 logp̂(x)과 logp̂(y)를 근사시킬 수 있다.
이 부가적 제약조건의 손실함수를 기존의 손실함수에 추가해 동시해 최소화하면, 아래와 같이 표현가능하다.
여기서 λ를 통해 손실함수내 비율조정이 가능하다. (만약 λ가 너무 크다면, 최적화과정에서 regularization term최소화 시 지나치게 집중).
 

Dual Supervised Learning

Many supervised learning tasks are emerged in dual forms, e.g., English-to-French translation vs. French-to-English translation, speech recognition vs. text to speech, and image classification vs. image generation. Two dual tasks have intrinsic connections

arxiv.org

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


3.  Duality를 활용한 Unsupervised Learning

3.1 Dual-learning Machine Translation
공교롭게도 CycleGAN출시시기에 맞춰 나온 논문, [Dual learning for machine translation; 2016]이 있다.

NLP의 특성상 CycleGAN처럼 직접적 미분값전 전달은 불가능하다.
하지만 기본적으로 아주 비슷한 개념을 활용하는데,
위의 논문은 "parallel corpus를 활용해 훈련된 기본성능의 NMT를 monolingual corpus를 이용해 그 성능을 극대화"하고자 했다.

NLP에서 GAN같은 방식으론 Gradient를 전달이 불가능하기에 강화학습을 활용해 판별자의 값을 전달해줘야한다.
(∵GD사이의 명확한 목표나 보상 신호가 부족하거나 모호한 경우가 많기 때문.)


∙mono-lingual corpus로부터 받은 문장 s에대해 번역을 하고
∙번역된 문장 smid를 사용해 반대방향 번역을 통해 번역 시, 
∙복원된 문장 ŝ이 기존 처음문장과의 차이 Δ(ŝ, s)가 최소화되도록 훈련한다.
이때, 번역된 문장 smid가 얼마나 자연스럽게 해당언어문장이 되는지 여부가 중요한 지표가 된다.
위의 알고리즘을 살펴보자.
두 도메인(A언어, B언어)의 문장들이 주어지고
생성자 GA→B의 parameter θAB
반대방향 생성자 FB→A의 parameter θBA가 등장한다.
이 GA→B와 FB→A는 모두 parallel corpus를 활용해 pretrain된 상태이다.


앞에서 배운 Policy Gradient를 활용해 parameter를 update하면, 14번째 식이 도출된다.
Ê[r]을 각각의 Parameter에 대해 미분한 값을 더해주는 것을 볼 수 있다.

이제, 이 보상의 기댓값을 구하면 아래와 같다.

이제 k개의 sampling된 문장에 대해 각 방향에 대한 보상을 각각 구하고 이를 선형결합한다. (이때, smid: sampling된 문장을 의미)
LMB를 사용해 해당 문장이 B언어의 집합에 속하는지 보상값을 계산한다.
LMB는 기존 언어B단일코퍼스로 사전훈련되어있기에 자연스러운 문장이 생성될수록 해당 LM에서 높은 확률을 가질 것이다.

이때, 람다와 알파는 동일.
이렇게 얻은 𝔼[r]을 각 parameter에 대해 미분하면 위와같은 수식을 얻을 수 있고, 
앞에서 서술한 parameter update수식에 대입하면 된다.



비슷한 방식으로 반대방향의 번역 B→A를 구할 수 있다.
최종적인 성능에 대한 설명 및 결과는 위와 같다.
Dual-Learning이 문장길이에 상관없이 항상 높은 성능을 보임을 알 수 있다.
 

Dual Learning for Machine Translation

While neural machine translation (NMT) is making good progress in the past two years, tens of millions of bilingual sentence pairs are needed for its training. However, human labeling is very costly. To tackle this training data bottleneck, we develop a du

arxiv.org

 

 

 

3.2 DUL (Dual Unsupervised Learning)
앞서 설명한 듀얼지도학습(DSL)은 Bayes정리에 따른 수식을 제약조건으로 사용했다.
지금 소개하는 논문[DUL ; 2018]의 방법은 주변분포(marginal distribution)의 성질을 이용해 제약조건을 생성한다.

주변 분포의 속성을 통해 위의 수식은 항상 참이다.
이는 조건부확률로 나타낼 수 있고, 좀 더 나아가면 기대값표현으로도 바꿀 수 있다.
그 후 K번 sampling 해 Monte Carlo Sampling으로 근사해 표현할 수 있다.

위의 수식을 NMT에 적용해보자.
∙src문장 x, tgt문장 y로 이뤄진 양방향병렬코퍼스 𝑩 와
∙S개의 tgt문장 y로만 이뤄진 단일언어코퍼스 ℳ이 있다 가정하자.


이때, 목적함수를 최대화하는 동시에 주변분포에 따른 제약조건 또한 만족시켜야한다.
[Objectiv; 목표]

위의 수식을 DSL과 마찬가지로 λ와 함께 S(θ)와 같이 표현해 기존 손실함수에 추가하면 아래와 같다.

이제 DSL과 유사하게 p̂(x)와 p̂(y)가 등장한다.
∙p̂(x): 단일언어코퍼스로 만든 LM으로 계산한 각 문장들의 확률값


위의 수식에 따르면 p̂(x)를 통해 src문장 x를 sampling해 신경망 θ를 통과시켜 P(y | x; θ)를 구해야 될 것처럼 보인다.
하지만, 아래처럼 조금 더 다른 방법으로 접근한다.
이처럼 중요도표집법(importance sampling)을 통해
tgt언어의 문장 y를 반대방향번역기 (y→x)에 넣어
K개의 src문장 x를 sampling해 P(y)를 구한다.


이 과정을 하나의 손실함수로 표현하면 아래와 같다.
∙1항: 문장 xn이 주어질 때, yn의 확률을 최대로하는 θ를 찾는다.

∙2항: 단일언어코퍼스에서 주어진 문장 ys LM에서의 확률값 log p̂(ys)과의 차이를 줄여야한다.
그 값은 반대방향번역기(y→x)를 통해 K번 sampling한 문장 xi의 LM확률값 p̂(xi)와 xi가 주어질 때, ys의 확률값을 곱하고, 문장 ys가 주어졌을 때 sampling한 문장 xi의 확률값으로 나눠준 값이 된다.


[성능 및 결과]
이 표에서 볼 수 있듯, DUL과 다른 기존 단일언어코퍼스를 활용한 알고리즘들과 비교한 결과는 위와 같은데, 이 방법은 앞서 소개한 기존의 단일언어코퍼스를 활용한 방식들과 비교했을 때, 훨씬 더 나은 성능의 개선을 보여준다.

더 나아가, 앞서 소개한 Dual-Learning보다 더 나은 성능을 보여준다.
마찬가지로, 비효율적 RL을 사용하지 않고도 더 나은 성능을 보여주는 것은 주목할만한 성과라 할 수 있다.
 

Dual Transfer Learning for Neural Machine Translation with Marginal Distribution Regularization | Proceedings of the AAAI

 

ojs.aaai.org

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


4. 쉬어가기) Back-Translation 재해석

 

[Gain Study_NLP]08. NMT 심화 (Zero-Shot, Transformer)

📌 목차 1. Multi-Lingual. with. Zero-shot Learning 2. Mono-Lingual Corpus 3. Transformer 😚 글을 마치며... 1. Mutli-Lingual. with. Zero-shot. Learning 이제, NMT performance를 끌어올리기 위한 고급기법들을 설명해보려 한다.

chan4im.tistory.com

4.1 Back-Translation
앞서 Back-Translation을 추상적 관점에서 왜 잘 동작하는지 알고 넘어갔다면, 
오늘의 논문은 Back-Translation을 수치적 관점으로 그 이유를 파악해보고자 한다.

∙ N개의 src문장 x, tgt문장 y로 이뤄진 양방향병렬코퍼스 𝛣
∙ S개의 tgt문장 y로만 이뤄진 단일언어코퍼스 ℳ이 있다고 가정하자.


앞서 다룬 DUL(Dual Unsupervised Learning)처럼 최종적으로 최소화하려는 손실함수는 아래와 같이 표현가능하다.

여기서, DUL과 마찬가지로 P(y)는 주변분포의 속성을 활용해 표현가능하다.
다만, 여기서 좌변이 P(y)가 아닌 logP(y)임을 주목하자.

cf) Jensen's 부등식정리는 항상 log P(y)보다 작거나 같은 수식으로 정리할 수 있다.
[Jensen's 부등식 정리]
로그함수곡선이 아래와 같을 때, 두점 x1, x2에 대한 평균을 xm이라 하자.
log xm ≥ (1/2)×(logx1 + logx2)는 항상 성립하는 것을 알 수 있다.


위의 부등식 결과물에서 음의부호를 붙여주면 아래와 같고,
결과적으로 목적은 -logP(y)를 최소화 하는 것임을 알 수 있다.
이를 조금 전, 최소화하려는 손실함수에 이 수식을 대입해보면 아래와 같다.
결국 L~을 최소화하는 것은 L을 최소화하는 효과를 만들 수 있다.
따라서 L~최소화를 위해 optimizer를 이용할 수 있다.
KL Divergence의 경우, θ에 대해 미분하면 상수이기에 생략되므로
GD를 이용한 예시일 경우, 아래와 같이 미분된다.


결과적으로 얻게된 손실함수의 각 부분의 의미를 살펴보자.
첫번째 항: xn이 주어졌을 때, yn의 확률을 최대로 하는 θ를 찾는 것
두번째 항: sampling된 문장 xi이 주어질 때, 단일언어코퍼스의 문장 ys가 나올 평균확률을 최대로 하는 θ를 찾는 것
Back-Translation이란 L~(θ)를 최소화하는것임을 알 수 있다.
 

Joint Training for Neural Machine Translation Models with Monolingual Data

Monolingual data have been demonstrated to be helpful in improving translation quality of both statistical machine translation (SMT) systems and neural machine translation (NMT) systems, especially in resource-poor or domain adaptation tasks where parallel

arxiv.org

 

 

 

 

 


마치며...

이번시간에는 기존 RL을 넘어 Duality라는 속성을 활용해 NMT의 성능을 극대화하는 방법을 살펴봤다.
이전 RL을 활용한 NLP 챕터에서 언급했듯, "Policy Gradient방식"
∙ 장점) 미분불가능한 보상함수도 사용할 수 있으나 
∙ 단점) Sampling기반 동작으로 훨씬 더 비효율적 학습을 진행한다.
    ∵ High Variance //  Exploration vs. Exploitation Trade-off 

하지만 Duality를 활용한 방식에서는 기존의 MLE 및 Teacher-Forcing방법 하에서 teacher-forcing의 단점을 보완하는, "Regularization Term"을 추가함으로써 모델의 성능을 극대화하였다.


추가적으로 기존의 Back-Translation등의 mono-lingual corpus 활용방법에 대한 재해석을 제공한다.
추가적으로 통계기반해석이 훨씬 수월하기에 현재 딥러닝학계의 연구방향과 상당부분 일치한다.

 

📌 목차

1. preview
2. Reinforcement Learning 기초

3. Policy based RL
4. NLG에 RL 적용
5. RL을 활용한 Supervised Learning
6. RL을 활용한 Unsupervised Learning

😚 글을 마치며...

Reinforcement Learning의 경우, 매우 방대한 분야이기에 그 방대한 영역의 일부분인 Policy Gradient를 활용해 자연어생성(NLG; Natural Language Generation)의 성능을 끌어올리는 방법을 다뤄볼 것이다.

먼저, Reinforcement Learning이 무엇인지, NLG에 왜 필요한지 차근차근 다뤄볼 것이다.

 

 

 

 

 

 


1. Preview

1.1 GAN (Generative Adversarial Network)
2016년부터 주목받으며 2017년 가장 큰 화제가 되었던 분야이고, 현재 Stable Diffusion 등으로 인해 Vision에서 가장 큰 화제의 분야는 단연코 생성적대신경망(GAN)이다. 이는 변분오토인코더(VAE)와 함께 생성모델학습을 대표하는 방법 중 하나이다.
이렇게 생성된 이미지는 실생활에 중요하지만 trainset을 얻기 힘든 문제들의 해결에 큰 도움을 줄 것이라 기대되고 있다.
위의 그림처럼 GAN은 생성자(Generator)G와 판별자(Discriminator) D라는 2개의 모델을 각기다른 목표를 갖고 "동시에 훈련"시킨다.
두 모델이 균형을 이루면 min/max 게임을 펼치게 되면 최종적으로 G는 훌륭한 이미지를 생성할 수 있게 된다.

 

1.2 GAN을 자연어생성에 적용
한번 GAN을 NLG에 적용해보자.
예를들어, CE를 사용해 바로 학습하기보단

 - 실제 corpus에서 나온 문장인지
 - seq2seq에서 나온 문장인지
위의 두 경우에 대한 판별자 D를 두어 seq2seq에서 나온 문장이 진짜 문장과 같아지도록 훈련하는 등을 예시로 들 수 있다.



다만, 아쉽게도 이 좋아보이는 아이디어는 바로 적용할 수 없는데,
seq2seq의 결과는 이산확률분포이기 때문이다.
따라서 여기서 sampling이나 argmax로 얻어지는 결과물은 discrete한 값이기에 one-hot벡터로 표현되어야 할 것이다.
하지만 이 과정은 확률적인 과정(stochastic process)으로 기울기를 역전파할 수 없거나 미분 결과값이 0이나 불연속적인 경우가 되기에 D가 맞춘 여부를 역전파를 통해 seq2seq G로 전달될 수 없고, 결과적으로 학습이 불가능하다.

 

1.3 GAN과 자연어생성
1.1과 1.2에서 말했듯, GAN은 Vision에서는 대성공을 이뤘지만 NLG에서는 적용이 어려운데, 이는 자연어 그 자체의 특징때문이다.
∙ 이미지는 어떤 "연속적"인 값들로 채워진 행렬
언어는 불연속적인 값들의 순차적 배열이기에 우린 LM을 통해 latent space에 연속적 변수로 그 값들을 치환해 다룬다.
결국, 외부적으로 언어를 표현하려면 이산확률분포와 변수로 나타내야하고,
분포가 아닌 어떤 sample로 표현하려면 이산확률분포에서 sampling하는 과정이 필요하다.
이런 이유로 D의 loss를 G에 전달할 수 없고, 따라서 적대적신경망방법을 NLG에 적용할 수 없다는 인식이 주를 이루게 되었다.
But!! 강화학습을 통해 적대적 학습방식을 우회적으로 사용할 수 있게 되었다.

 

1.4 강화학습 사용이유
어떤 task해결을 위해 CE를 쓸 수 있는 classification이나 continuous변수를 다루는 MSE등으로는 정의할 수 없는 복잡한 목적함수가 많기 때문이다.

즉, 우리는 CE나 MSE로 문제를 해결했지만, 이는 문제를 단순화해 접근했다는 것을 알 수 있다.
이런 문제들을 강화학습을 통해 해결하고, 성능을 극대화할 수 있다.

이를 위해 잘 설계된 보상(reward)을 사용더 복잡하고 정교한 문제를 해결할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


2. Reinforcement Learning 기초

Reinforcement Learning은 이미 오래전 Machine Learning의 한 종류로 나왔던 방대하고도 유서깊은 학문이기에 이번에 하나의 글로는 다루기 무리가 있다.

 

따라서 이번시간에 다룰, Policy Gradient에 대한 이해를 위해 필요한 정도만 이해하고 넘어가보려 한다.

추가적인 Reinforcement Learning에 대한 기타 추천은 아래 링크 참고.

⌈Reinforcement Learning: An Introduction ; (MIT Press, 2018)⌋

2.1 Universe
먼저 강화학습이 동작하는 과정부터 알아보자.

Q: 강화학습이란??
A: 어떤 객체가 주어진 환경에서, 상황에 따라 어떻게 행동해야할 지 학습하는 방법
강화학습 동작과정
처음 상태인 St(t=0)을 받고, 
Agent는 자신의 policy에 따라 action At를 선택한다.
EnvironmentAgent로부터 선택된 At를 받아
보상 Rt+1새롭게 바뀐 상태 St+1을 반환한다.

이 과정을 특정조건이 만족될 때까지 반복하며, 환경은 이 시퀀스를 종료한다.
이를 하나의 episode라 한다.

목표: 반복되는 episode에서 agent가 RL을 통해 적절한 행동(보상이 최대가 되도록)을 하도록 훈련시키는 것

 

2.2 MDP (Markov Decision Process)
여기에 더해 Markov결정과정(MDP)라는 개념을 도입한다.
우린 온 세상의 현재 T=t라는 순간을 하나의 상태(state)로 정의할 수 있다.

가정) 현재상태(present state)가 주어졌을 때, 미래(T>t)는 과거(T<t)로부터 "독립적".
이 가정 하에, 온 세상은 Markov과정 위에서 동작한다 할 수 있다.
이때, 현재상황에서 미래상황으로 바뀔 확률P(S' | S)로 표현가능하다.

여기에 MDP는 결정을 내리는 과정(= 행동을 선택하는 과정)이 추가된 것이다.
즉, 현재상황에서 어떤행동을 선택 시, 미래상황으로 바뀔 확률P(S' | S, A)이다.


쉽게 설명하자면, 아래 가위바위보게임을 예시로 들 수 있다.
예를들어 사람마다 가위바위보를 내는 패턴이 다르기에 가위바위보 상대방에 따라 전략이 바뀌어야 하므로
∙ 상대방이 첫 상태 S0를 결정한다.
∙ Agent는 S0에 따라 행동 A0를 선택한다.
∙ 상대방은 상대방의 정책에 따라 가위/바위/보 중 하나를 낸다.
∙ 승패가 결정, 환경으로부터 방금 선택한 행동에 대한 보상 R1을 받는다.
∙ update된 상태 S1을 얻는다.

 

2.3 Reward
앞서 Agent가 어떤 행동을 했을 때, 환경으로부터 "보상"을 받는다.
이때, 우린 Gt어떤 시점으로부터 받는 보상의 누적합이라 정의하자.
따라서 Gt는 아래와 같이 정의된다.

이때 감소율(discount factor) γ(0~1값)를 도입해 수식을 조금 변형할 수 있다.
γ의 도입으로 먼 미래의 보상보다 가까운 미래의 보상을 더 중시해 다룰 수 있게 된다.

 

2.4 Policy
Agent는 주어진 상태에서 앞으로 받을 보상의 누적합을 최대로 하도록 행동해야한다.
즉, 눈앞의 작은 작은 손해보다 먼미래까지 포함한 보상의 총합이 최대가 되는것이 중요하다.
(😂 마치 우리가 시험기간에 놀지못하고 공부하는 것처럼...?)

정책(policy)Agent가 상황에 따라 어떻게 행동을 해야할 지, "확률적으로 나타낸 기준"이다.
즉, 같은 상황이 주어졌을 때, 어떤 행동을 선택할지에 대한 확률함수이기에 따라서 우리가 행동하는 과정은 확률적인 프로세스라 볼 수 있다. 이때, 함수를 통해 주어진 행동을 취할 확률값을 아래 함수를 통해 구할 수 있다.

 

2.5 Value Function (가치함수)
가치함수란 주어진 policy 𝛑 내에 특정 상태 s에서부터 앞으로 얻을 수 있는 보상의 누적총합의 기댓값을 의미한다.
아래 수식과 같이 나타낼 수 있는데, 앞으로 얻을 수 있는 보상의 총합의 기댓값은 기대누적보상(Expected Cumulative Reward)라고도 한다.
행동가치함수 (Q-function ; Q 함수)

행동가치함수(activation-value function ; Q-function)는 주어진 policy 𝛑 아래 상황 s에서 action a를 선택했을 때, 앞으로 얻을 수 있는 보상의 누적합의 기댓값(기대누적보상)을 표현한다.

가치함수어떤 s에서 어떤 a를 선택할지와 관계없이 얻을 수 있는 누적보상의 기댓값이라 한다면
Q함수어떤 a를 선택하는가에 대한 개념이 추가된 것이다.
즉, 상태와 행동에 따른 기대누적보상을 나타내는 Q함수의 식은 아래와 같다.

 

2.6 Bellman 방정식
가치함수와 행동가치함수의 정의에 따라 이상적인 가치함수와 이상적인 Q함수를 정의해보고자 한다면...?
→ Bellman Equation(벨만 방정식)을 다음과 같이 나타낼 수 있다.
좌) Back-Tracking / 우) Dynamic Programming.&nbsp; //. Back-Tracking의 경우, 모든 경우의 수를 전부 탐색해야 한다.

DP: 문제를 겹치는 하위 문제(sub-problems)로 분해하고 최적 부분 구조(optimal substructure)를 따르는 방법으로 문제를 해결하는 기술로 큰 문제를 해결하는 것은 작은 하위 문제를 해결하는 것으로 나누어질 수 있습니다. 이 작은 하위 문제들을 해결한 후에는 그 결과를 조합하여 원래 문제를 해결합니다. 중요한 점은 동일한 하위 문제가 여러 번 계산되는 대신, 한 번 계산된 결과를 저장하고 재사용하여 계산 비용을 줄이는 것입니다. 이것이 DP의 핵심 아이디어이며, 겹치는 하위 문제가 있다면 DP를 효과적으로 활용할 수 있다.

Bellman방정식은 동적프로그래밍(DP; Dynamic Programming)알고리즘 문제로 접근가능하다.
즉, 단순히 최단경로를 찾는 문제와 비슷한데, 아래 그림처럼 모든 경우에 대해 탐색을 수행하는 Back-Tracking보다 훨씬 효율적이고 빠르게 문제풀이에 접근할 수 있다.

 

2.7 Monte Carlo Method
하지만 그렇게 쉽게 문제가 풀리면 강화학습이 필요하진 않았을 것이다.
Prob) 대부분의 경우, 위의 2.6의 수식에서 가장 중요한 P(s', r | s, a)부분을 모른다는 점이다.
즉, 어떤 상태→어떤행동→어떤확률로 다른상태 s'과 보상 r'을 받게되는지, "직접 해봐야" 안다는 점이다.
∴ DP가 적용될 수 없는 경우가 대부분이다.


따라서 RL처럼 simulation등의 경험을 통해 Agent를 학습해야한다.
이런 이유로 Monte Carlo Method처럼 sampling을 통해 Bellman의 Expectation Equation을 해결할 수 있다.
Prob) 긴 episode에 대해 sampling으로 학습하는 경우이다.
실제 episode가 끝나야 Gt를 구할 수 있기에 episode의 끝까지 기다려야 한다.
다만, 그간 익히 보아온 AlphaGo처럼 굉장히 긴 episode의 경우, 매우 많은 시간과 긴 시간이 필요하게 된다.

 

2.8 TD 학습 (Temporal Difference Learning)
이때, 시간차학습(TD)방법이 유용하다.
TD학습법은 다음수식처럼 episode보상의 누적합 없이도 바로 가치함수를 update할 수 있다.
Q-Learning
만약 올바른 Q함수를 알고있다면, 어떤상황이더라도 항상 기대누적보상을 최대화하는 매우 좋은 선택이 가능하다.
이때, Q함수를 잘 학습하는 것을 Q-Learning이라 한다.

아래수식처럼 target과 현재가치함수(current)의 차이를 줄이면 올바른 Q함수를 학습할 것이다.
DQN (Deep Q-Learning)
Q함수를 학습할 때, state공간의 크기와 action공간의 크기가 너무 커 상황과 행동이 희소할 경우, 문제가 생긴다.
훈련과정에서 희소성으로 인해 잘 볼 수 없기 때문이다.
이처럼 상황과 행동이 불연속적인 별개의 값이더라도, Q함수를 근사하면 문제가 발생할 수 있다.

DeepMind는 신경망을 사용해 근사한 Q-Learning을 통해 Atari게임을 훌륭히 플레이하는 강화학습방법을 제시했는데, 이를 DQN(Deep Q-Learning)이라 한다.
신경망을 활용한 Q-Learning 개요
아래 수식처럼 Q함수 부분을 신경망으로 근사해 희소성 문제를 해결했고, 심지어 Atari게임을 사람보다 더 잘 플레이하는 Agent를 학습하기도 했다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


3.  Policy based Reinforcement Learning

3.1 Policy Gradient
Policy Gradient는 Policy based Reinforcement Learning방식에 속한다.
cf) DQN은 가치기반(Value based Reinforcement Learning)방식에 속한다.-ex) DeepMind가 사용한 AlphaGo.

정책기반과 가치기반 강화학습, 두 방식의 차이는 다음과 같다.
∙ 가치기반학습: ANN을 사용해 어떤 행동을 선택 시, 얻을 수 있는 보상을 예측하도록 훈련
∙ 정책기반학습: ANN의 행동에 대한 보상을 역전파알고리즘을 통해 전달해 학습
∴ DQN의 경우, 행동의 선택이 확률적(stochastic)으로 나오지 않지만
Policy Gradient는 행동 선택 시, 확률적인 과정을 거친다.


Policy Gradient에 대한 수식은 아래와 같다.

이 수식에서 앞의 𝜋는 policy를 의미한다.
즉, 신경망 가중치 θ는 현재상태 s가 주어졌을때, 어떤행동 a를 선택해야하는지에 관한 확률을 반환한다.


우리의 목표는 최초상태(initial state)에서의 기대누적보상을 최대로 하는 정책 θ를 찾는것이고
최소화해야하는 손실과 달리 보상은 최대화해야하므로
기존의 경사하강법대신, 경사상승법(Gradient Ascent)를 사용한다.


이런 경사상승법에 따라 θJ(θ)를 구해 θ를 update해야한다.
이때, d(s)는 Markov Chain의 정적분포(stationary distribution)로써 시작점에 상관없이 전체경로에서 s에 머무르는 시간비율을 의미한다.

이때, 로그미분의 성질을 이용해 아래 수식처럼 θJ(θ)를 구할 수 있다.
이 수식을 해석하자면 다음과 같다.
∙ 매 time-step별 상황 s가 주어질 때, a를 선택할 로그확률의 기울기와 그에따른 보상을 곱한 값의 기댓값이 된다.

Policy Gradient Theorem에 따르면
여기서 해당 time-step에 대한 즉각적 보상 r대신, episode 종료까지의 기대누적보상을 사용할 수 있다.
즉, Q함수를 사용할 수 있다는 것인데, 이때 Policy Gradient의 진가가 드러난다.
우린 Policy Gradient 신경망에 대해 미분계산이 필요하지만,
"Q함수에 대해서는 미분할 필요가 없다!!"
즉, 미분가능여부를 떠나, 임의의 어떤 함수라도 보상함수로 사용할 수 있는 것이다!!


이렇게 어떤 함수든 보상함수로 사용할 수 있게되면서, 기존 Cross-Entropy나 MSE같은 손실함수로 fitting하는 대신, 좀 더 실무에 부합하는 함수(ex. 번역의 경우, BLEU)를 사용해 θ를 훈련시킬 수 있게되었다.

추가적으로 위의 수식에서 기댓값 수식을 Monte Carlo Sampling으로 대체하면 아래처럼 신경망 파라미터 θ를 update할 수 있다.
이 수식을 더 풀어서 설명해보자.
log𝜋θ(at | st) : st가 주어졌을 때, 정책파라미터 θ상의 확률분포에서 sampling되어 선택된 행동이 at일 확률값이다.
해당확률값을 θ에 대해 미분한 값이  θlog𝜋θ(at | st)이다.
따라서 해당 기울기를 통한 경사상승법은 log𝜋θ(at | st)를 최대화함을 의미한다.
∙ 즉, at의 확률을 높이도록 하여 앞으로 동일상태에서 해당행동이 더 자주 선택되게 한다.


Gradient ∇θlog𝜋θ(at | st)에 보상을 곱해주었기에 만약 sampling된 해당 행동들이 큰 보상을 받았다면,
학습률 γ에 추가적인 곱셈을 통해 더 큰 step으로 경사상승을 수행할 수 있다.

하지만 음의 보상값을 받게된다면, 경사의 반대방향으로 step을 갖게 값이 곱해질 것이므로 경사하강법을 수행하는 것과 같은 효과가 발생할 것이다.
따라서 해당 sampling된 a들이 앞으로는 잘 나오지 않게 신경망 파라미터 θ가 update될 것이다.
따라서 실제 보상을 최대화하는 행동의 확률을 최대로하는 파라미터 θ를 찾도록 할것이다.

다만, 기존의 경사도는 방향과 크기를 나타낼 수 있었지만,
Policy Gradient 기존 경사도의 방향에 스칼라 크기값을 곱해주므로
실제 보상을 최대화하는 직접적인 방향을 지정할 수 없기에 사실상 훈련이 어렵고 비효율적이라는 단점이 존재한다.

 

3.2 MLE  v.s  Policy Gradient
다음 예시로 최대가능도추정(MLE)과의 비교를 통해 Policy Gradient를 더 이해해보자.

∙ n개의 sequence로 이뤄진 data를 입력받아
∙ m개의 sequence로 이뤄진 data를 출력하는 함수를 근사시키는 것이 목표

그렇다면 sequence x1:n과 y1:mB라는 dataset에 존재한다.


목표) 실제함수 f: x→y를 근사하는 신경망 parameter θ를 찾는것이므로


이제 해당 함수를 근사하기 위해 parameter θ를 학습해야한다.
θ는 아래와 같이 MLE로 얻을 수 있다.

Dataset B의 관계를 잘 설명하는 θ를 얻기위해, 목적함수를 아래와 같이 정의한다.
아래 수식은 Cross-Entropy 를 목적함수로 정의한 것이다.
목표) 손실함수의 값을 최소화 하는 것.

앞에서 정의한 목적함수를 최소화 해야하므로 Optimizer를 통해 근사할 수 있다.
(아래 수식은 Optimizer로 Gradient Descent를 사용하였다.)
해당 수식에서 학습률 γ를 통해 update의 크기를 조절한다.

다음 수식은 Policy Gradient에 기반해 누적기대보상을 최대로하는 경사상승법 수식이다.


이 수식에서도 이전 MLE의 경사하강법 수식처럼 γ의 추가로 Q𝛑θ(st, at)가 기울기 앞에 붙어 학습률역할을 하는 것을 볼 수 있다.
따라서 보상의 크기에 따라 해당 행동을 더욱 강화하거나 반대 방향으로 부정할 수 있게 되는 것이다.
즉, 결과에 따라 동적으로 학습률을 알맞게 조절해준다고 이해할 수 있다.

Sampling 확률을 최대화하는 방향으로 경사도를 구하는 Policy Gradient

 

3.3 Baseline을 고려한 Reinforce 알고리즘
앞에서 설명한 Policy Gradient를 수행할 때, 보상이 양수인 경우 어떻게 동작할까?

ex) 시험이 0~100점사이 분포할 때, 정규분포에 의해 대부분 평균근처에 점수가 분포할 것이다.
따라서 대부분의 학생들은 양의 보상을 받는다.
그렇게되면, 앞의 기존 policy gradient는 항상 양의 보상을 받아 Agent(학생)에게 해당 policy를 더욱 독려할 것이다.
But! 평균점수가 50점일 때, 10점은 상대적으로 매우 나쁜점수이므로 기존 정책의 반대방향으로 학습하게된다.

즉, 주어진 상황에 마땅한 보상이 있기에 우린 이를 바탕으로 현재 policy가 얼마나 훌륭한지 평가를 할 수 있다.
이를 아래 수식처럼 policy gradient수식으로 표현할 수 있다.
이처럼 Reinforce 알고리즘은 baseline을 고려해 좀 더 안정적 강화학습수행이 가능하다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 


4. Natural Language Generation에 Reinforcement Learning 적용

강화학습은 마르코프 결정 과정(MDP) 상에서 정의되고 동작한다.
여러 Decision Action → 여러 상황을 이동(transition)하며 episode가 구성 → 선택된 행동과 상태에 따라 보상이 주어진다.
이것이 누적되고 에피소드가 종료되면 누적보상을 얻을 수 있습니다.

이런 과정은 NLP에서 text classification보다는 sequential data를 예측해야 하는 자연어 생성(NLG)에 적용된다.
 
∙ 이제까지 생성된 word sequence = current state(현재상황)
이제까지 생성된 단어를 기반 → 새롭게 선택하는 단어가 행동이 될 것
하나의 문장을 생성하는 과정(= (BOS)~(EOS)까지 선택하는 과정)하나의 Episode가 된다.
 
 episode를 반복해 문장생성경험을 축적 → 실제 정답과의 비교 → 기대누적보상을 최대화하도록 θ를 훈련

 
NMT에 RL을 적용한다면...?
NMT에 RL을 구체적으로 대입해보자.
∙ 현재 상태 = 주어진 src문장과 이전까지 생성(번역)된 단어들의 시퀀스
 행동을 선택하는 것 = 현재 상태에 기반하여 새로운 단어를 선택하는 것.
 현재 time-step의 행동을 선택 시 → 다음 time-step의 상태는 소스 문장과 이전까지 생성된 단어들의 시퀀스에 현재 time-step에 선택된 단어가 추가되어 정해진다.
❗️중요한 점
행동을 선택한 후, 환경으로부터 즉각적인 보상을 받지는 않으며,
모든 단어의 선택이 끝나고 최종적으로 EOS를 선택해 디코딩이 종료되어 에피소드가 끝나면,
비로소 BLEU 점수를 계산하여 누적 보상을 받을 수 있다는 것
즉, 종료 시 받는 보상값 = 에피소드 누적보상(cumulative reward) 값
강화학습을 통해 모델을 훈련 시, 훈련의 도입부부터 강화학습만 적용하기에는 그 훈련방식이 비효율적이고 어려움이 크므로,
보통 기존의 MLE를 통해 어느 정도 학습이 된 신경망 θ에 강화학습을 적용한다.
즉, 강화학습은 탐험(exploration)을 통해 더 나은 정책의 가능성을 찾아내고 착취(exploitation)를 통해 그 정책을 발전시켜 나갑니다.

 

4.1 NLG에서 강화학습의 특징
 앞에서 RL의 Policy based Learning인 Policy Gradient방식에 대해 간단히 알아봤다.
Policy Gradient의 경우, 위에서 설명한 내용 이외에도 발전된 방법들이 많다.
ex) Actor Critic, A3C, ...
∙ Actor Critic: 정책망 θ이외에도 가중치망 W를 따로 두어 episode종료까지 기다리지 않고 TD학습법으로 학습한다.
∙ A3C(Asunchronous Advantage Actor Critic): Actor Critic에서 더욱 발전 및 기존 단점을 보완

다만, NLP의 RL은 이런 다양한 방법들을 굳이 사용할 필요없이 간단한 Reinforce알고리즘을 사용해도 큰 문제가 없는데, 이는 NLP분야의 특징 덕분으로 강화학습을 자연어처리에 적용 시, 아래와 같은 특징이 존재한다.
1. 선택 가능한 매우 많은 행동(action) at가 존재
∙ 보통 다음 단어를 선택하는 것 = 행동을 선택하는 것
선택 가능한 행동의 집합의 크기 = 어휘 사전의 크기
∴  그 집합의 크기는 보통 몇 만 개가 되기 마련.



2. 매우 많은 상태 (state)가 존재
 단어를 선택하는 것이 행동이었다면,
이제까지 선택된 단어들의 시퀀스 = 상태
여러 time-step을 거쳐 수많은 행동(단어)이 선택되었다면 가능한 상태의 경우의 수는 매우 커질 것.



3. 따라서 매우 많은 행동을 선택하고, 매우 많은 상태를 훈련 과정에서 모두 겪는 것은 거의 불가능하다고 볼 수 있다.
결국 추론 과정에서 unseen sample을 만나는 것은 매우 당연할 것이다.
이런 희소성 문제는 큰 골칫거리가 될 수 있지만 DNN으로 이 문제를 해결 할 수 있다.



4. 강화학습을 자연어 처리에 적용할 때 쉬운 점도 있다.
대부분 하나의 문장 생성 = 하나의 에피소드 (이때, 문장의 길이는 보통 100 단어 미만)
→ 다른 분야의 강화학습보다 훨씬 쉽다는 이점을 갖는다.

ex) DeepMind의 AlphaGo, Starcraft의 경우, 하나의 에피소드가 끝나기까지 매우 긴 시간이 든다.
→ 에피소드 내에서 선택된 행동들이 정책을 업데이트하려면 매우 긴 에피소드가 끝나기를 기다려야 한다.
→ 뿐만 아니라, 10분 전에 선택했던 행동이 해당 게임의 승패에 얼마나 큰 영향을 미쳤는지 알아내기란 매우 어려운 일이 될 것이다.
❗️이때 자연어 생성 분야가 다른 분야에 비해 에피소드가 짧다는 것은 매우 큰 이점으로 작용하여 정책망을 훨씬 더 쉽게 훈련 시킬 수 있다.



5. 대신, 문장 단위의 에피소드를 가지는 강화학습에서는 보통 에피소드 중간에 보상을 얻기 어렵다.
ex) 번역의 경우, 각 time-step마다 단어를 선택할 때 즉각적인 보상을 얻지 못하고, 번역이 모두 끝난 이후 완성된 문장과 정답 문장을 비교하여 BLEU 점수를 누적 보상으로 사용한다.
마찬가지로 에피소드가 매우 길다면 이것은 매우 큰 문제가 되었겠지만, 다행히도 문장 단위의 에피소드에서는 큰 문제가 되지 않습니다

 

4.2 RL 적용의 장점
Teacher Forcing을 이용한 문제해결
seq2seq같은 AR속성의 모델훈련 시, teacher forcing 방법을 사용한다.
이 방법은 train과 inference방식의 차이가 발생, 실제 추론방식과 다르게 문제를 훈련해야한다.

❗️하지만, RL을 통해 실제 추론형태와 같이 sampling으로 모델을 학습
→ "train과 inference"의 차이가 없어졌다.
더 정확한 목적함수의 사용
BLEU는 PPL에 비해 더 나은 번역품질을 반영한다. [Gain_NLP_07]
다만, 이런 metric들은 미분을 통해 훈현하기 불가능한 경우가 대부분이어서 신경망 훈련에 사용이 어렵다.

❗️하지만, RL의 Policy Gradient를 응용보상함수에 대해 미분을 계산할 필요가 없어지면서 정확한 Metric 사용이 가능했다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


5. RL을 활용한 Supervised Learning

BLEU를 훈련과정의 목적함수로 사용한다면 더 좋은 결과를 얻을 수 있을텐데...

마찬가지로 다른 NLG문제에 대해서도 비슷한 접근을 할 수 있으면 좋을텐데...

 

5.1 MRT (Minimum Risk Training)
위의 바람에서 출발하여 위험최소화 훈련[MRT; Minimum Risk Training]논문이 제안되었다.
당시 저자는 Policy Gradient를 직접적으로 사용하지 않았으나 유사한 수식이 유도되었다는 점에서 매우 인상적이다.



기존의 최대가능도추정(MLE)방식은 위와같은 손실함수를 사용해 |S|개의 입출력에 대한 손실값을 구하고, 이를 최소화하는 θ를 찾는 것이 목표였다.
하지만, 이 논문에서는 risk를 아래와 같이 정의하고, 이를 최소화하는 학습방식인 MRT를 제안했다.

위의 수식에서 y(x(s))는 search scape(탐색공간)의 전체 집합이다.
이는 S번째 x(s)가 주어졌을 때 가능한 정답집합을 의미한다.

또한, Δ(y, y(s))는 입력 파라미터 θ가 주어졌을 때, sampling한 y와 실제 정답 y(s)의 차이(= error)값을 의미한다.

즉, 이 수식에 따르면 risk R은 주어진 입력과 현재 파라미터 상에서 얻은 y를 통해 현재 모델함수를 구하고, 동시에 이를 사용해 Risk의 기대값을 구한다 볼 수 있다.

이렇게 정의된 Risk를 최소화하는 것이 목표이다.
반대로 risk대신 보상으로 생각하면, 보상을 최대화하는 것이 목표이다.
결국, risk를 최소화할 때 경사하강법, 보상을 최대화할 때는 경사상승법을 사용하기에
수식을 분해하면 결국 온전히 동일한 내용임을 알 수 있다.
따라서 실제 구현 시,  Δ(y, y(s))사용을 위한 보상함수 BLEU에 -1을 곱해 Risk함수로 만든다.

다만 주어진 입력에 대해 가능한 정답에 관한 전체공간을 탐색할 수 없기에
전체탐색공간에 sampling한 sub-space에서 sampling하는 것을 택한다.



그 후 위의 수식에서 θ에 대해 미분을 수행한다.
이제, 미분을 통해 얻은 MRT의 최종 수식을 해석해보자.


최종적으로는 수식에서 기대값부분을 Monte Carlo Sampling을 통해 제거할 수 있다.
아래수식은 Policy Gradient의 Reinforce알고리즘 수식으로 앞의 MRT수식과 비교하여 참고해보자.


[MRT의 Reinforce알고리즘 수식]


MRT는 강화학습으로써의 접근을 전혀하지 않고, 수식적으로 Policy Gradient의 Reinforce알고리즘 수식을 도출해내어 성능을 끌어올린다는 점에서 매우 인상깊은 방법임을 알 수 있다.



[Policy Gradient의 Reinforce알고리즘 수식]

 

Pytorch NMT_with_MRT 예제코드

구현과정

1. 주어진 입력문장에대해 정책 𝜃를 이용해 번역문장 sampling
2. sampling문장과 정답문장사이 BLEU를 계산, -1을 곱해 Risk로 변환
3. log확률분포전체에 Risk를 곱함
4. 각 sample과 time-step별 구해진 NLL값의 합에 -1을 곱해줌(=PLL)
5. 로그확률값의 합에 𝜃로 미분을 수행, BP로 신경망 𝜃전체 기울기가 구해짐
6. 이미 Risk를 확률분포에 곱했기에, 바로 이 기울기로 BP를 수행, 최적화
from nltk.translate.gleu_score import sentence_gleu
from nltk.translate.bleu_score import sentence_bleu
from nltk.translate.bleu_score import SmoothingFunction

import numpy as np

import torch
from torch import optim
from torch.nn import functional as F
import torch.nn.utils as torch_utils

from ignite.engine import Engine
from ignite.engine import Events
from ignite.metrics import RunningAverage
from ignite.contrib.handlers.tqdm_logger import ProgressBar

import simple_nmt.data_loader as data_loader
from simple_nmt.trainer import MaximumLikelihoodEstimationEngine
from simple_nmt.utils import get_grad_norm, get_parameter_norm

VERBOSE_SILENT = 0
VERBOSE_EPOCH_WISE = 1
VERBOSE_BATCH_WISE = 2


class MinimumRiskTrainingEngine(MaximumLikelihoodEstimationEngine):

    @staticmethod
    def _get_reward(y_hat, y, n_gram=6, method='gleu'):
        # This method gets the reward based on the sampling result and reference sentence.
        # For now, we uses GLEU in NLTK, but you can used your own well-defined reward function.
        # In addition, GLEU is variation of BLEU, and it is more fit to reinforcement learning.
        sf = SmoothingFunction()
        score_func = {
            'gleu':  lambda ref, hyp: sentence_gleu([ref], hyp, max_len=n_gram),
            'bleu1': lambda ref, hyp: sentence_bleu([ref], hyp,
                                                    weights=[1./n_gram] * n_gram,
                                                    smoothing_function=sf.method1),
            'bleu2': lambda ref, hyp: sentence_bleu([ref], hyp,
                                                    weights=[1./n_gram] * n_gram,
                                                    smoothing_function=sf.method2),
            'bleu4': lambda ref, hyp: sentence_bleu([ref], hyp,
                                                    weights=[1./n_gram] * n_gram,
                                                    smoothing_function=sf.method4),
        }[method]

        # Since we don't calculate reward score exactly as same as multi-bleu.perl,
        # (especialy we do have different tokenization,) I recommend to set n_gram to 6.

        # |y| = (batch_size, length1)
        # |y_hat| = (batch_size, length2)

        with torch.no_grad():
            scores = []

            for b in range(y.size(0)):
                ref, hyp = [], []
                for t in range(y.size(-1)):
                    ref += [str(int(y[b, t]))]
                    if y[b, t] == data_loader.EOS:
                        break

                for t in range(y_hat.size(-1)):
                    hyp += [str(int(y_hat[b, t]))]
                    if y_hat[b, t] == data_loader.EOS:
                        break
                # Below lines are slower than naive for loops in above.
                # ref = y[b].masked_select(y[b] != data_loader.PAD).tolist()
                # hyp = y_hat[b].masked_select(y_hat[b] != data_loader.PAD).tolist()

                scores += [score_func(ref, hyp) * 100.]
            scores = torch.FloatTensor(scores).to(y.device)
            # |scores| = (batch_size)

            return scores


    @staticmethod
    def _get_loss(y_hat, indice, reward=1):
        # |indice| = (batch_size, length)
        # |y_hat| = (batch_size, length, output_size)
        # |reward| = (batch_size,)
        batch_size = indice.size(0)
        output_size = y_hat.size(-1)

        '''
        # Memory inefficient but more readable version
        mask = indice == data_loader.PAD
        # |mask| = (batch_size, length)
        indice = F.one_hot(indice, num_classes=output_size).float()
        # |indice| = (batch_size, length, output_size)
        log_prob = (y_hat * indice).sum(dim=-1)
        # |log_prob| = (batch_size, length)
        log_prob.masked_fill_(mask, 0)
        log_prob = log_prob.sum(dim=-1)
        # |log_prob| = (batch_size, )
        '''

        # Memory efficient version
        log_prob = -F.nll_loss(
            y_hat.view(-1, output_size),
            indice.view(-1),
            ignore_index=data_loader.PAD,
            reduction='none'
        ).view(batch_size, -1).sum(dim=-1)

        loss = (log_prob * -reward).sum()
        # Following two equations are eventually same.
        # \theta = \theta - risk * \nabla_\theta \log{P}
        # \theta = \theta - -reward * \nabla_\theta \log{P}
        # where risk = -reward.

        return loss

    @staticmethod
    def train(engine, mini_batch):
        # You have to reset the gradients of all model parameters
        # before to take another step in gradient descent.
        engine.model.train()
        if engine.state.iteration % engine.config.iteration_per_update == 1 or \
            engine.config.iteration_per_update == 1:
            if engine.state.iteration > 1:
                engine.optimizer.zero_grad()

        device = next(engine.model.parameters()).device
        mini_batch.src = (mini_batch.src[0].to(device), mini_batch.src[1])
        mini_batch.tgt = (mini_batch.tgt[0].to(device), mini_batch.tgt[1])

        # Raw target variable has both BOS and EOS token.
        # The output of sequence-to-sequence does not have BOS token.
        # Thus, remove BOS token for reference.
        x, y = mini_batch.src, mini_batch.tgt[0][:, 1:]
        # |x| = (batch_size, length)
        # |y| = (batch_size, length)

        # Take sampling process because set False for is_greedy.
        y_hat, indice = engine.model.search(
            x,
            is_greedy=False,
            max_length=engine.config.max_length
        )

        with torch.no_grad():
            # Based on the result of sampling, get reward.
            actor_reward = MinimumRiskTrainingEngine._get_reward(
                indice,
                y,
                n_gram=engine.config.rl_n_gram,
                method=engine.config.rl_reward,
            )
            # |y_hat| = (batch_size, length, output_size)
            # |indice| = (batch_size, length)
            # |actor_reward| = (batch_size)

            # Take samples as many as n_samples, and get average rewards for them.
            # I figured out that n_samples = 1 would be enough.
            baseline = []

            for _ in range(engine.config.rl_n_samples):
                _, sampled_indice = engine.model.search(
                    x,
                    is_greedy=False,
                    max_length=engine.config.max_length,
                )
                baseline += [
                    MinimumRiskTrainingEngine._get_reward(
                        sampled_indice,
                        y,
                        n_gram=engine.config.rl_n_gram,
                        method=engine.config.rl_reward,
                    )
                ]

            baseline = torch.stack(baseline).mean(dim=0)
            # |baseline| = (n_samples, batch_size) --> (batch_size)

            # Now, we have relatively expected cumulative reward.
            # Which score can be drawn from actor_reward subtracted by baseline.
            reward = actor_reward - baseline
            # |reward| = (batch_size)

        # calculate gradients with back-propagation
        loss = MinimumRiskTrainingEngine._get_loss(
            y_hat,
            indice,
            reward=reward
        )
        backward_target = loss.div(y.size(0)).div(engine.config.iteration_per_update)
        backward_target.backward()

        p_norm = float(get_parameter_norm(engine.model.parameters()))
        g_norm = float(get_grad_norm(engine.model.parameters()))

        if engine.state.iteration % engine.config.iteration_per_update == 0 and \
            engine.state.iteration > 0:
            # In orther to avoid gradient exploding, we apply gradient clipping.
            torch_utils.clip_grad_norm_(
                engine.model.parameters(),
                engine.config.max_grad_norm,
            )
            # Take a step of gradient descent.
            engine.optimizer.step()

        return {
            'actor': float(actor_reward.mean()),
            'baseline': float(baseline.mean()),
            'reward': float(reward.mean()),
            '|param|': p_norm if not np.isnan(p_norm) and not np.isinf(p_norm) else 0.,
            '|g_param|': g_norm if not np.isnan(g_norm) and not np.isinf(g_norm) else 0.,
        }

    @staticmethod
    def validate(engine, mini_batch):
        engine.model.eval()

        with torch.no_grad():
            device = next(engine.model.parameters()).device
            mini_batch.src = (mini_batch.src[0].to(device), mini_batch.src[1])
            mini_batch.tgt = (mini_batch.tgt[0].to(device), mini_batch.tgt[1])

            x, y = mini_batch.src, mini_batch.tgt[0][:, 1:]
            # |x| = (batch_size, length)
            # |y| = (batch_size, length)

            # feed-forward
            y_hat, indice = engine.model.search(
                x,
                is_greedy=True,
                max_length=engine.config.max_length,
            )
            # |y_hat| = (batch_size, length, output_size)
            # |indice| = (batch_size, length)
            reward = MinimumRiskTrainingEngine._get_reward(
                indice,
                y,
                n_gram=engine.config.rl_n_gram,
                method=engine.config.rl_reward,
            )

        return {
            'BLEU': float(reward.mean()),
        }

    @staticmethod
    def attach(
        train_engine,
        validation_engine,
        training_metric_names = ['actor', 'baseline', 'reward', '|param|', '|g_param|'],
        validation_metric_names = ['BLEU', ],
        verbose=VERBOSE_BATCH_WISE
    ):
        # Attaching would be repaeted for serveral metrics.
        # Thus, we can reduce the repeated codes by using this function.
        def attach_running_average(engine, metric_name):
            RunningAverage(output_transform=lambda x: x[metric_name]).attach(
                engine,
                metric_name,
            )

        for metric_name in training_metric_names:
            attach_running_average(train_engine, metric_name)

        if verbose >= VERBOSE_BATCH_WISE:
            pbar = ProgressBar(bar_format=None, ncols=120)
            pbar.attach(train_engine, training_metric_names)

        if verbose >= VERBOSE_EPOCH_WISE:
            @train_engine.on(Events.EPOCH_COMPLETED)
            def print_train_logs(engine):
                avg_p_norm = engine.state.metrics['|param|']
                avg_g_norm = engine.state.metrics['|g_param|']
                avg_reward = engine.state.metrics['actor']

                print('Epoch {} - |param|={:.2e} |g_param|={:.2e} BLEU={:.2f}'.format(
                    engine.state.epoch,
                    avg_p_norm,
                    avg_g_norm,
                    avg_reward,
                ))

        for metric_name in validation_metric_names:
            attach_running_average(validation_engine, metric_name)

        if verbose >= VERBOSE_BATCH_WISE:
            pbar = ProgressBar(bar_format=None, ncols=120)
            pbar.attach(validation_engine, validation_metric_names)

        if verbose >= VERBOSE_EPOCH_WISE:
            @validation_engine.on(Events.EPOCH_COMPLETED)
            def print_valid_logs(engine):
                avg_bleu = engine.state.metrics['BLEU']
                print('Validation - BLEU={:.2f} best_BLEU={:.2f}'.format(
                    avg_bleu,
                    -engine.best_loss,
                ))

    @staticmethod
    def resume_training(engine, resume_epoch):
        resume_epoch = max(1, resume_epoch - engine.config.n_epochs)
        engine.state.iteration = (resume_epoch - 1) * len(engine.state.dataloader)
        engine.state.epoch = (resume_epoch - 1)

    @staticmethod
    def check_best(engine):
        loss = -float(engine.state.metrics['BLEU'])
        if loss <= engine.best_loss:
            engine.best_loss = loss

    @staticmethod
    def save_model(engine, train_engine, config, src_vocab, tgt_vocab):
        avg_train_bleu = train_engine.state.metrics['actor']
        avg_valid_bleu = engine.state.metrics['BLEU']

        # Set a filename for model of last epoch.
        # We need to put every information to filename, as much as possible.
        model_fn = config.model_fn.split('.')

        model_fn = model_fn[:-1] + ['mrt',
                                    '%02d' % train_engine.state.epoch,
                                    '%.2f-%.2f' % (avg_train_bleu,
                                                   avg_valid_bleu),
                                    ] + [model_fn[-1]]

        model_fn = '.'.join(model_fn)

        # Unlike other tasks, we need to save current model, not best model.
        torch.save(
            {
                'model': engine.model.state_dict(),
                'opt': train_engine.optimizer.state_dict(),
                'config': config,
                'src_vocab': src_vocab,
                'tgt_vocab': tgt_vocab,
            }, model_fn
        )

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


6. RL을 활용한 Unsupervised Learning

지도학습방식은 높은 정확도를 자랑한다. 다만, labeled data가 필요해 data확보나 cost가 높다.

비지도학습방식은 data확보에 대한 cost가 낮기에 더 좋은 대안이 될 수 있다.

(물론, 지도학습에 비해 성능이나 효율이 떨어질 가능성은 높음)

 

이런 점에서 parallel corpus에 비해 monolinugal corpus를 확보하기 쉽다는 NLP의 특성상, 좋은 대안이 될 수 있다.

소량의 parallel corpus와 다량의 monolingual corpus를 결합 더 나은 성능을 확보할 수 있을 것이다.

 

 

6.1 Unsupervised를 통한 NMT
이번에 소개할 논문은 오직 monolingual corpus만을 사용해 번역기를 제작하는 방법을 제안했다. [Guillaume Lampl;2018]
따라서 진정한 비지도학습을 통한 NMT라 볼 수 있다.

[핵심 idea]
∙ 언어에 상관없이 같은 의미의 문장일 경우, Encoder가 같은 값으로 embedding할 수 있도록 훈련하는 것.
이를 위해 GAN이 도입되었다!! →❓어? 분명 GAN을 NLP에서 못쓴다고 앞서 얘기했던 것 같은데...??
❗️encoder의 출력값이 연속적 값이기에 GAN을 적용할 수 있었다.
[Encoder]
언어에 상관없이 동일한 내용의 문장에 대해 같은 값의 벡터로 encoding하도록 훈련
∙ 이를 위해 판별자 D(encoding된 문장의 언어를 맞추는 역할)가 필요하고
D를 속이도록 Encoder는 학습된다.

[Decoder]
encoder의 출력값을 갖고 Decoder를 통해 기존 문장으로 잘 돌아오도록 함

즉, Encoder와 Decoder를 언어에 따라 다르게 사용하지 않고 언어에 상관없이 1개씩의 Encoder, Decoder를 사용한다.

결과적으로 손실함수는 아래 3가지 부분으로 구성된다.

 

손실함수의 3가지 구성

De-noising Auto-Encoder
seq2seq도 일종의 Auto-Encoder의 일종이라 볼 수 있다.
AE는 굉장히 쉬운 문제에 속한다.

따라서 AE에서 단순히 복사작업을 지시하는대신,
noise를 섞어준 src문장에서 De-noising을 하면서 입력값을 출력에서 복원(reconstruction)하도록 훈련해야하는데, 이를 "De-noising AutoEncoder"라 부르며 아래와 같이 표현된다.
이 수식에서  x_hat은 입력문장 x를 noise_model C를 통해 noise를 더하고 같은 언어 ℓ로 encoding과 decoding을 수행하는 것을 의미한다.
Δ(x_hat, x)는 MRT에서와 같이 원문과 복원된 문장과의 차이를 의미한다.
noise_model C(x)는 임의로 문장 내 단어들을 드롭하거나 순서를 섞어주는 일을 한다.
Cross Domain training
Cross Domain훈련이란
사전번역을 통해 사전훈련한 저성능의 번역모델 M에서 언어 ℓ2의 noise가 추가되어
번역된 문장 y를 다시 언어 ℓ1 src 문장으로 원상복구하는 작업을 학습하는 것
이다.

Adversarial Learning
Encoder가 언어와 상관없이 항상 같은 분포로 latent space에 문장벡터를 embedding하는지 감시하는 판별자 D가 추가되어 적대적 학습을 진행한다.
D는 latent variable z의 기존 언어를 예측하도록 훈련된다.
 xi , ℓi는 같은 언어(language pair)를 의미한다.

따라서 GAN처럼 Encoder는 판별자 D를 속일 수 있도록 훈련되어야 한다.
이때,  j = - (i - 1) 값을 갖는다.

 

최종 목적함수
위의 3가지 목적함수를 결합하면 최종 목적함수를 얻을 수 있다.
λ를 통해 손실함수 상에서 비율을 조절, 최적의 parameter θ를 찾는다.


논문에서는 오직 monolingual corpus만 존재할 때, NMT를 만드는 방식에 대해 다룬다.
parallel corpus가 없는 상황에서도 NMT를 만들 수 있따는 점에서 매우 고무적이다.

다만, 이 방법 자체만으론 실제필드에서 활용하기 어려운데, 실무에서는 번역기구축 시 parallel corpus가 없는 경우는 드물고, 없다 하더라도 monolingual corpus만으로 번역기를 구축해 낮은 성능의 번역기를 확보하기 보단, 비용을 들여 parallel corpus를 직접 구축하고, parallel corpus와 다수의 monolingual corpus를 합쳐 NMT를 구축하는 방향으로 진행하는 것이 낫기 때문이다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


마치며...

이번시간에는 Reinforcement Learning에 대해 알아보고, 이를 이용해 자연어 생성문제(NLG)를 해결하는 방법을 다루었다.
다양한 RL 알고리즘을 사용해 NLG문제의 성능을 높일 수 있는데, 특히 Policy Gradient를 사용해 NLG에 적용하는 방법을 설명했다.



Policy Gradient방법을 NLG에 적용해 얻는 이점은 크게 2가지인다.
teacher-forcing(AR속성으로 인해 실제추론방식과 다르게 훈련)방법에서 탈피
실제 추론방식과 같은 sampling을 통해 문장생성능력 향상

② 더 정확한 목적함수를 훈련이 가능하다.
 - 기존 PPL: 번역품질, NLG품질을 정확히 반영X
 - 따라서 BLEU 등의 metric을 사용
 - 하지만 BLEU 등의 metric은 미분을 할 수 없음.
  ∴ PPL과 동일한 Cross-Entropy를 활용해 신경망을 훈련해야만 했다.

다만, Policy Gradient 또한 단점이 존재한다.
sampling기반 훈련이기에 많은 iteration이 필요.
따라서 Cost가 높아 더 비효율적 학습이 진행된다.

② 보상함수는 방향이 없는 스칼라값을 반환한다.
따라서 보상함수를 최대화하는 방향을 정확히 알 수 없다.

이는 기존 MLE방식에서 손실함수를 신경망 파라미터 θ
에 대해
미분해 얻은 기울기로 손실함수자체를 최소화하는 방향으로 update하는 것과 차이가 존재한다.
결국, 이 또한 기존 MLE방식보다 훨씬 비효율적 학습으로 이어지게 되는 것이다.

📌 목차

1. Transfer Learning이란?
2. 기존 pre-train 방식

3. ELMo
4. BERT.  &.  Metric
5. GPT-2

😚 글을 마치며...

 

 

 


1. Transfer Learning

1.1 전이학습(Transfer Learning)
∙전이학습: 신경망의 일부or전체를 MLE를 통해 train하기 전, 다른 dataset이나 목적함수로 미리 훈련한 후, 본격적인 학습에서는 가중치를 더 쉽게 최적화하는 것이다.

이미 예전부터 Vision분야에서는 ImageNet같은 weight값을 다른 문제에 전이학습을 적용하는 접근방식이 흔하게 사용되었다.
하지만 NLP에서 여러 전이학습 방법들이 제시되면서 점차 주류로 잡아가고 있다.

더 넓은 범위의 목적(objective)을 갖는 Unsupervised Learning문제의 Global-minima는
더 작은 범위의 목적을 갖는 Supervised Learning의 Global-minima를 포함할 가능성이 높다.
따라서 비지도학습을 통해 local-minima를 찾으면,
지도학습에서 더 낮은 지점의 local-minima에 도달 할 가능성이 높다.

즉, random 초기값에서 시작하는 것은 dataset의 noise로 어려울 수 있지만
pretrained_weight에서 최적화를 시작하는 것은 앞선 문제를 다소 해결할 수 있고, 이는 좀 더 높은 성능을 기대할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 


2. 기존 Pre-train 방식

2.1 pretrain된 가중치로 사용.
word2vec의 등장 이후, 꾸준히 pretrained word embedding을 사용해 DNN개선을 하려 했지만, 이는 생각보다 큰 효과를 거둘 수 없었기에 pretrained word embedding를 활용한 방법을 transfer learning에 적용하는 방법을 먼저 알아보자.

아래 그림처럼 RNN을 사용한 간단한 text classification신경망을 가정해보자.
앞서 text classification관련 내용에서 다뤘듯이 신경망은 Softmax층과 RNN층들, embedding층으로 이뤄져있다.
One-Hot encoding vector들을 문장 내 단어 대신 입력 x로 넣고, 신경망 fθ를 통과시켜 클래스를 예측하는 분포 y_hat을 반환하도록 할 것이다.
이전에 다루었듯, One-Hot vector의 특징으로인해 1 위치의 값만 살아남을 것이다.
∴ embedding층의 가중치파라미터 W의 각 행(row) = 해당 단어의 embedding vector

 

신경망 가중치 초기값으로 사용
∙ 각 word embedding vector → pretrained word embedding vector로 치환해 사용할 수 있다.
이후 해당 embedding층의 가중치값과 기타 신경망의 random초기화 가중치값에 대해 BP와 최적화 등을 통해 학습을 수행한다.


 이때, 각 단어에 해당하는 word embedding vector는 위의 그림에서처럼 embedding층의 가중치의 해당 행(row)을 대체하여 초기값으로 설정할 수 있다.

신경망 가중치 초기값으로 고정
이 방법은 embedding층의 가중치는 최적화대상에서 제외된다.
embedding층을 제외한 신경망의 다른 가중치들만 학습하게 한다.

따라서 아래와 같이 optimizer를 설정할 수 있다.
# 기존
optimizer = optim.Adam(model.parameters())

# embedding층 제외 나머지 param 학습
optimizer = optim.Adam(model.softmax_layer.parameters() + model.rnn.parameters())


마찬가지로 0번 dataset을 통해 pretrained word embedding vector를 embedding층에 적용했으나, 이후 훈련에서 embedding층을 제외한 빨간점선안쪽부분의 신경망만 MLE를 통해 update가 된다.
신경망 가중치 초기값으로 사용 .&. Slow training
마지막으로 embedding층의 가중치를 최적화에서 제외하는 대신, 천천히 학습시키는 방법이다.
다만 이때, 또다른 Learning Rate가 hyper-parameter로 추가된다.
optimizer = optim.Adam(model.softmax_layer.parameters() + model.rnn.parameters())
optimizer_emb = optim.Adam(model.emb_layer.parameters(), lr=1e-6)


이전 방법들처럼 embedding층을 초기화했으나, 이번에는 2개의 다른 학습률을 사용, embedding층은 빨간 점선부분과 달리 천천히 update되게 한다.

 

 

2.2 pretrain된 word embedding vector를 사용하지 않는 이유
다만 이전에 말했듯, 이런 방식의 전이학습은 그다지 큰 효과를 거둘 수 없다.
[문맥반영못하는 기존 word embedding 알고리즘]
word2vec의 Skip-Gram이나 GloVe는 성공적으로 단어를 latent space에 embedding했지만, 이 알고리즘들은 문장에 함께 출현한 단어들(co-occurence words)을 예측하는데 기반한다. 따라서 embedding 정보(feature)가 매우 한정적이다.

우리가 실제수행하려는 문제해결을 위한 목적함수와 위의 알고리즘의 목적함수는 상당히 상이할 것이고, 우리에게 필요한 정보를 반영하기는 어려울 것이다.

추가적으로 기존 word embedding 알고리즘들의 결과는 context를 고려한 단어의 의미를 embedding하기엔 지나치게 단순한데, 같은 단어라 할지라도 문장의 문맥에 따라 그 의미가 확연히 달라진다.
또한, 문장 내 단어의 위치에 따라서 쓰임새 및 의미도 달라진다.
이런 정보들이 embedding층의 상위층에서 제대로 추출 및 반영되더라도 word embedding 시 고려되어 입력으로 주어지는 것에 비해 불리하다.
[신경망 입력층의 가중치에만 적용]
위의 적용방법의 설명에서 보았듯, 대부분의 기존 적용방법들은 embedding층에 국한된다.
embedding층의 가중치만 한정해 본다면 Global minima에 더 가까울지라도, 신경망 전체 가중치를 고려한다면 최적화에 유리할지는 알 수 없다.

따라서 "신경망 전체에 대해 사전훈련하는 방법"사용하는 편이 더 낫다.

 

2.3 NLP에서 pretrain의 효과
Vision에서 ImageNet을 이용해 pretrain하는 것처럼 NLP에서도 수많은 문장을 수집해 학습한 신경망으로 다른 문제에 성공적으로 적용하고 성능을 개선하는 방법이 마련되었다.

[NLP분야에서의 Transfer-Learning 분류]
NLP에서의 pretrain은 다른 분야의 pretrain보다 유리한 점을 몇가지 갖는다.
① 일반 문장들에 대해 단순 LM훈련하는 것만으로도 매우 큰 전이학습으로 인한 성능향상이 가능
② 특히, 수집에 비용이 들어가는 parallel corpus, labeled text와 달리 일반적 문장들의 수집은 매우 쉽고 값싸다.
즉, 기존 dataset수집에 비해 일반 corpus수집은 거의 비용이 들지 않기에 매우 큰 성과를 위험부담없이 얻을 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 


3.  ELMo 

3.1 ELMo (Embedding from Language Model)
ELMo논문에서는 앞서 제기한 기존 pretrain훈련방식의 문제점을 해결하는 방법을 제시했다.

∙ 입력으로 주어진 문장을 단방향 LSTM(Uni-Directional LSTM)에 넣고, 정방향과 역방향 LM을 각각 훈련한다.
이때, LM은 AR(자기회귀)모델이다.(= 양방향 LSTM으로 훈련불가)
이때, LSTM은 여러 층으로 구성이 가능하며, 각 층이 주어진 문장 내의 token수만큼 정방향과 역방향에 대해 훈련된다.
각 time-step의 입력과 출력에 따른 내부 LSTM의 정방향 및 역방향 hidden_state

❗️embedding층과 softmax층은 정방향과 역방향에 대해 공유되며, 이전층의 각 방향별 출력값이 같은 방향의 다음 층으로 전달된다.

이때, 정방향과 역방향 LSTM은 따로 구성되지만 LSTM의 입력을 위한 embedding층과 출력을 받아 단어를 예측하는 softmax층의 경우, weight_parameter를 공유한다.

이를 통해 word_embedding_vector는 물론, LSTM의 층별 hidden_state를 time-step별로 얻을 수 있고
이들에 대한 가중합을 구해 해당 단어나 토큰에 대한 문장 내에서의 embedding representation인 ELMoktask를 구할 수 있다.
ELMoktask는 각 time-step별 L개의 LSTM_hidden state들을 가중치 sjtask에 따라 가중합을 구한다.
이때, j=0일 때, 단어의 embedding vector를 가리키며
j > 0일 때, 정방향과 역방향의 LSTM의 hidden_state인 hj,kLM ; hk,jLM가 연계되어 구성된다.
이 ELMoktask(= representation)들은 풀고자하는 문제에 따라 달라지기에 task라 표현한다.

이렇게 얻어진 ELMo표현은 실제 문제수행을 위한 신경망의 입력벡터에 연계되어 사용된다.
이때, ELMo표현을 위한 신경망가중치파라미터는 update되지 않는다.


ELMo는 이런 방법들로 전이학습을 수행함으로써 SQuAD(Stanford Question Answering Dataset)와 같은 고난도 종합독해(Reading Comprehension)문제에서 SRL(Semantic Role Labeling), NER(Named Entity Recognition)등의 NLP처리문제까지 큰 성능개선을 이룩했다.

 

 

 

 

 

 

 

 

 

 

 

 

 


4. BERT (Bidirectional Encoder Representations from Transformer)

ELMo에 이어 해당년도인 2018년 말에 출시된 BERT논문은 매우 뛰어나 현재도 많이 사용 및 응용이 가장 많이되는 모델이다.

 

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

We introduce a new language representation model called BERT, which stands for Bidirectional Encoder Representations from Transformers. Unlike recent language representation models, BERT is designed to pre-train deep bidirectional representations from unla

arxiv.org

기존의 ELMo와 달리 BERT는 Fine-Tuning을 통해 접근한다. 자세히 설명해보자면,

∙ELMo: pretrained LM의 층별 hidden_state들을 가중합해 해당 time-step의 embedding vector로 사용

  - 즉, ELMo + task전용 신경망이 필요했었음

 

∙BERT: 훈련된 LM에 단순히 아주 약간의 층을 추가해 학습, 특정 task에 맞는 신경망을 생성

  - Fine-Tuning으로 task해결을 위한 또다른 신경망 구현등의 추가적 노력요소가 적음

  - 또한, MLM을 통해 기존의 AR속성으로 인한 단방향의 제약을 뛰어 넘어 양방향시퀀셜모델링을 성공적으로 이룩했다.

  - 마지막으로, 한개의 문장만을 모델링하는 대신, 2개의 문장을 동시에 학습하도록 하여 질의응답같은 2개이상의 문장이 주어지는 문제에도 성공적 예측을 가능케한다.

 

4.1 MLM (Masked Language Model)
LM또한 AR모델이기에 양방향 RNN으로 훈련할 수 없는게 일반적이었다.

하지만, BERT는 Masked LM기법으로 LM훈련을 단순히 다음 단어를 훈련하는 기존방식에서 벗어나 "입력으로 들어가는 단어 token 일부를 masking, 모델이 문맥을 통해 원래 단어를 예측해 빈칸을 채우는 형태의 문제로 바꿈"으로써 Bi-Directional Encoding을 적용할 수 있게 하였다.

[MASK] token을 임의의 단어와 치환(보통 masking시, 15% 단어만 masking.)한 후, 신경망이 기존 단어를 맞추게 학습하는데 이는 마치 Denoising Auto-Encoder와 비슷한 맥락으로 원리를 이해할 수 잇따.

다만 MLM은 학습수렴속도가 늦어지는 문제가 발생하지만, 훨씬 더 큰 성능의 개선을 이룩하기에 현재도 유용히 사용된다.

 

4.2 NSP (Next Sentence Prediction)
사실 질의응답과 같은 문제는 입력으로 여러개의 문장이 주어진다.
이 여러 문장사이의 관계가 매우 중요하기에 단순 LM으로는 문장간의 관계를 모델링하기 어렵다.
이때, BERT는 문장의 쌍을 입력으로 삼아 사전훈련을 수행하여 사후 fine-tuning단계에서 여러 문장이 입력으로 주어지는 문제에 효과적 대처가 가능하다.

NSP는 "2개의 문장을 주고 이 문장들이 이어진 문장인지 맞추도록 학습, 문장마다 문장끝에 [SEP]라는 token을 붙여 문장을 구분하는 방법"이다.


아래 그림과 같이 문장순서를 나타내는 정보를 추가로 embedding,
기존의 word embedding 및 position embedding의 합에 더해준다.

[CLS]라는 특별 token을 추가로 도입해 현재 분류작업이 진행중임을 알리고,
[SEP]라는 특별 token을 추가로 도입해 문장사이 경계를 신경망에 알려준다.

 corpus에서 실제 앞뒤 두 문장을 추출해 학습하기도하고
임의의 관계없는 두 문장을 추출해 학습하기도 한다.
예를들어 아래와 같다.
[CLS] the man went to [MASK] stor [SEP] he bought a gallon [MASK] milk [SEP]
[CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight less birds [SEP]

 

4.3 문장 쌍 분류문제
대표적인 NLP Metric GLUE(General Langauge Understanding)는 다양한 자연어 이해문제에 대한 성능평가를 제공한다.
예를들어, QNLI test(Question Natural Language Inference)는 SQuAD에서 추출되어 변형된 문제들로 입력으로 주어진 질문과 이어지는 문장이 올바른 Q-A관계인지 맞추는 이진분류문제이다.


이런 분류문제에 적용하기 위해 transformer encoder의 가장 첫 time-step 입력은 [CLS]라는 특별 token을 입력으로 받는다.
해당 time-step의 출력벡터에 softmax층을 추가해 Fine-tuning을 함으로써 주어진 문제에 대한 분류신경망을 얻을 수 있다.
[CLS]에 해당하는 transformer encoder의 출력벡터를 C ∈ ℝH라 하자. (이때, H는 vector의 차원이다.)
이때, softmax층의 가중치파라미터 W ∈ ℝK×H를 추가한다. (이때, K는 후보클래스의 개수이다.)
이제, 각 클래스에 속할 확률분포 P ∈ ℝK를 구해보자.


[특징기반 전이학습방법인 ELMo와 가장 큰 차이점]

해당 문제에 대한 train dataset에 MLE를 수행하면서, 새롭게 추가된 W뿐만아니라 기존 BERT의 가중치까지 한꺼번에 학습시키는 것.

 

4.4 단일문장 분류문제
Text Classification이 가장 대표적인 이 유형에 속하는 문제이다.
마찬가지로 [CLS]토큰의 transformer출력에 대해 softmax층을 추가해 Fine-tuning으로 신경망을 훈련한다.
인터넷 등으로 수집한 corpus에 대해 LM을 훈련한 결과를 통해 task에 대한 dataset만으로 얻어낼 수 있는 성능을 훨씬 상회하는 성능을 발휘할 수 있게 되었다.

것처럼 분절이 완료된 파일을 데이터로 사용한다.
이후

 

4.5 질의응답 문제
BERT는 마찬가지로 종합독해문제에도 매우 강력한 성능을 발휘했다.
SQuAD에서도 훌륭한 결과를 얻었고, 한국어 dataset인 KorSQuAD에서도 매우 뛰어난 성능을 거뒀다.

SQuAD와 같은 문제는 질문과 문장이 주어졌을 때, 문장 내에서 질문에 해당하는 답을 예측하도록 학습한다.
따라서 정답단어나 구절이 위치한 time-step의 시작과 끝을 출력학 반환한다.
즉, 여럭 time-step의 입력을 받아 여러 time-step의 출력값을 내도록 한다.
SQuAD문제는 BERT를 이용한 주류가 대부분을 차지하였으며(KorSQuAD), 이미 해당 task에서 사람의 능력치를 뛰어넘었다.

 

4.6 단일문장 Tagging문제
이외에도 BERT는 NER, SRL같은 문장 내 정보 tagging문제 등에도 적용되어 뛰어난 성능을 발휘한다.
하나의 고정된 architecture에서 단순히 수많은 corpus를 훈련한 결과로 NLP전반에 걸쳐 뛰어난 성능을 거두었는데,
이 경우, 입력으로 들어온 각 단어나 토큰에 대한 tagging정보 등을 출력으로 반환하도록 추가적 훈련을 진행한다.

 

 

 

 

 

 

 

 

 


5. GPT-2

5.1 Architecture
GPT-2논문에서도 BERT와 같이 Transformer Decoder구조를 활용, LM을 비지도학습으로 사전학습 후 추가적인 지도학습으로 성능향상을 얻는다.
GPT-2이전, GPT-1논문이 발표되었었는데, 사실 GPT-1과의 차이점은 Decoder를 더 크게 사용하고, 몇가지 수정사항만 제외하면 거의 없다.

GPT-2는 기존의 Transformer에서 활용된 MHA블록을 굉장히 넓고 깊게 쌓아 parameter개수를 크게 늘려 수용력을 극대화 했는데, 이는 역전파의 전달을 용이하게 하는 skip-connection과 같은 방법으로 인해 가능하다.

다만, 논문에 따르면 이렇게 큰 모델구조를 활용했음에도 여전히 LM-비지도 사전학습에 사용된 dataset에 대해 underfitting이 될 만큼 큰 dataset을 사용했음을 알 수 있다.

 

5.2 pretrained Dataset 예시
∙ 기존의 다양하지 않은 도메인들
  ex) 뉴스기사, Wikipedia, 소설 등의 corpus

∙ WebText: 직접 crawling을 통해 최대한 많은 corpus를 모은 dataset

 

5.3 전처리 및 LM-Unsupervised pretraining
이 논문에서는 수집된 웹페이지로부터 텍스트들을 추출하기 위해 'Dragnet'과 'Newspaper'라는 라이브러리를 활용했다.
결과적으로 총 800만개의 문서로 구성된 40GB에 이르는 dataset을 수집하는 결과를 얻었고 이를 활용해 LM을 pretrain시켜 최고성능 LM을 얻을 수 있었다.

이후 subword과정에서 BPE만을 활용하며, 이때 BPE train과정에서 merge가 같은 wordset 내에서만 일어나게 제약을 둠으로써 BPE 분절의 성능을 단순한 방법으로 극대화 하였다.
이때, subword의 효율을 높이기 위해 같은 그룹의 character끼리만 merge과정을 수행한다.

ex) "입니다."와 같은 마침표(.)가 알파벳이나 한글과 붙어 나타난다면, 마침표와 앞서 나타난 문자들은 다른 그룹에 속하기 때문에 빈도가 아무리 높더라도 BPE 훈련 및 적용과정에서 결합이 수행되지 않는다. (= 여전히 떨어져 있는 상태가 될 것)

따라서 BPE를 통해 GPT-2는 추가적인 큰 노력없이 준수한 성능의 전처리 결과를 얻을 수 있었다.

 

5.4 실험 결과
GPT-2는 매우 방대한 양의 crawling data를 구축하고, transformer모델의 구조를 매우 깊고 넓게 가져갔다.
이를 통해 GPT-2는 매우 그럴싸한 느낌이 들 정도로 수준높은 LM임을 자랑한다.
parameter # LAMBADA WikiText-2 PTB WikiText-103 1B Words
previous SOTA 99.8 39.14 46.54 18.3 21.8
117M 35.13 29.41 65.85 37.50 75.20
345M 15.60 22.76 47.33 26.37 55.72
762M 10.87 19.93 40.31 22.05 44.58
1542M 8.63 18.34 35.76 17.48 42.16

위의 표는 GPT-2의 각 dataset에 대한 PPL(perplexity)성능으로
표에서 알 수 있듯, 대부분의 datsaset에서 S.O.T.A를 보여줬다.
특히나 LAMBADA datatset의 경우, parameter수가 늘어날수록 매우 큰 폭의 PPL의 감소를 확인할 수 있다.

 

Conclusion
GPT-2는 매우 방대한 양의 corpus를 수집해 training과정에 사용한 것이 큰 특징이다.
덕분에 매우 깊고 넓은 모델구조를 가지며 훌륭한 LM을 갖는다.
즉, 비지도학습에서 좀 더 Global-minima에 접근가능하다.
→ 추가적 지도학습으로 다른 문제들에서도 큰 성능향상을 가질 수 있게 되었다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


마치며...

이번시간에는 전이학습(transfer learning)을 활용해 NLP성능을 끌어올리는 방법을 알아보았다.
기존의 word2vec과 같은 사전훈련법과 달리 이번에 소개한
∙ELMo처럼 "문맥을 반영한 word embedding vector"를 구하거나
∙BERT처럼 "신경망 전체에 대해 사전학습된 가중치"를 제공한다.


특히 기존 Vision분야에서는 이미 ImageNet을 pretrain_weight_parameter로 두고 다른 dataset의 training에 사용을 진행해 왔었다.
이 효과를 NLP에서도 얻을 수 있게 되었기에 BERT는 매우 효과적이다. (by MLM. &. NSP)
BERT는 손쉽게 수집가능한 일반적인 문장들을 바탕으로 양방향 언어모델을 학습한 후, 이를 다른 문제해결에 사용한다.

기계번역과 같이 parallel corpus가 필요하거나
text classification과 같이 labeling된 corpus가 필요한 경우
제한적으로 dataset을 수집해야한다.

하지만 전이학습을 통해 훨씬 더 많은 양의 corpus로부터 문장의 특징을 추출하는 방법을 신경망이 배울 수 있게 되는 것이다.
BERT의 활용은 질의응답문제에 대해 사람보다 뛰어난 해결능력을 갖는 신경망을 손쉽게 얻을 수 있었다.

📌 목차

1. Multi-Lingual. with. Zero-shot Learning
2. Mono-Lingual  Corpus

3. Transformer

😚 글을 마치며...

 

 

 


1. Mutli-Lingual. with. Zero-shot. Learning

이제, NMT performance를 끌어올리기 위한 고급기법들을 설명해보려 한다.
하나의 end2end모델에서 여러 언어쌍의 번역을 동시에 제공하는 mulilingual NMT에 대해 알아보자.

 

1.1 Zero-Shot  Learning
NMT에서 Zero-Shot Learning이라는 흥미로운 논문이 제안되었다.[Enabling Zero-shot translation; 2017]
이 방식의 특징은 여러 언어쌍의 parallel corpus를 하나의 모델에 훈련하면 부가적 학습에 참여한 corpus에 존재하지 않는 언어쌍도 번역이 가능하다는 점이다.
즉, 한번도 NMT에 data를 보여주지 않아도 해당 언어쌍 번역을 처리할 수 있다.
(쉽게 말하자면, 모델이 train data에 직접 노출되지 않은 클래스를 인식하고 분류할 수 있는 능력을 의미)

[구현방법]
∙ 기존 병렬코퍼스의 맨 앞에 특수 token을 삽입, 훈련하면 된다.
∙ 이때, 삽입된 token에 따라 target언어가 결정된다.
  src언어 target언어
기존 Hello, how are you? Hola, ¿ cómo estás?
Zero-Shot <2es> Hello, how are you? Hola, ¿ cómo estás?
위의 목표는 단순히 다국어 NMT end2end모델구현이 아닌, 
서로 다른 언어쌍의 corpus를 활용해 NMT의 모든 언어쌍에 대해 전체적인 성능을 올릴 수 있는지 확인하려는 관점이다.
이에 대해 아래 4개의 관점으로 실험이 진행될 것이다.

 

Many-to-One
다수의 언어를 encoder에 넣고 train

이 방법은 실제문제로 주어진 언어 dataset외에도, 동시에 훈련된 다른언어의 dataset을 통해
해당 언어의 번역성능을 높이는 정보를 추가로 얻을 수 있다.
One-to-Many
∙ 다수의 언어를 decoder에 넣고 train

이 방법은 위의 방법과 달리, 성능향상이 있다보긴 어렵다.
게다가 양이 충분한(ex. ENG-FRA) corpus의 경우, oversampling을 하게되면 더 큰 손해를 보게 된다.
Many-to-Many
∙ 다수의 언어를 encoder, decoder 모두에 넣고 train

이 방법은 대부분의 실험결과가 하락세이다.
(다만 다양한 언어쌍을 하나의 모델에 넣고 훈련한 것 치고는 BLEU Score는 괜츈한편)

 

Zero-Shot.NMT test
∙ 위의 방법으로 train된 모델에서 train corpus에 없는 언어쌍의 번역성능을 평가
  Method Zero-Shot 유무 BLEU
(a) PBMT. Bridge X 28.99
(b) NMT. Bridge X 30.91
(c) NMT. POR→SPA X 31.50
(d) 모델1] POR→ENG,ENG→SPA O 21.62
(e) 모델2] ENG↔POR,SPA O 24.75
(f) 모델2 + 점진학습 X 31.77
(a), (b)
Bridge방법은 중간언어를 영어로 하여 2단계에 걸쳐 번역한다.
구문기반기계번역(PBMT: Phrase-Based Machine Translation)방식은 통계기반기계번역(SMT)의 일종이다.

(c)
NMT '포르투갈어→스페인어'는 단순병렬코퍼스를 활용해 기존 방법대로 훈련한 baseline이다.
물론, zero-shot 훈련방식으로는 넘을 수 없는 수치이다.

(d), (e)
모델 1은 POR→ENG,ENG→SPA을 단일모델에 훈련한 방법이고
모델 2는 ENG↔POR, ENG↔SPA를 단일모델에 훈련한 방법이다.

(f)
모델2 + 점진(incremental)학습방식은 (c)보다 적은 양의 corpus로 훈련한 기존 모델에 추가로 모델 2방식으로 훈련한 모델이다.

비록 모델1과 모델2는 훈련중 한번도 POR→SPA parallel corpus를 보지 못했지만, 20이 넘는 BLEU를 보여준다.
하지만, 물론 (a), (b)보다는 성능이 떨어진다.
다행히도 (f)의 경우, (c)보다 큰 차이는 아니나 성능이 뛰어남을 확인할 수 있다.

parallel corpus의 양이 얼마되지않는 언어쌍의 번역기 훈련 시, 이 방법으로 성능을 끌어올릴 수 있다.
(특히 한국어-일어, 스페인어-포르투갈어 와 같이 매우 비슷한 언어쌍을 같은 src, tgt언어로 사용 시 그 효과가 증폭된다.)

 

 

 

 

 

 

 

 

 

 

 

 


2. Mono-Lingual Corpus

NMT훈련을 위해 다량의 parallel corpus가 필요하다.
보통 완벽하지는 않지만 나름 사용할만한 번역기가 나오려면 최소 300만 문장 쌍이상이 필요하다.

하지만, 인터넷에서 monolingual corpus는 많지만 multilingual corpus를 대량으로 얻기란 매우 힘든 일이다.
또한, 단일 언어 corpus가 양이 더욱 많기에 실제 우리가 사용하는 언어의 확률분포에 가까울 수 있고
따라서 더 나은 LM을 학습하기에 monolingual corpus가 훨씬 유리하다.

이번 Section에는 저렴한 monolingual corpus를 활용해 NMT성능을 쥐어짜보는 방법을 알아보자.

 

2.1 LM Ensemble
위의 방법은 딥러닝의 거두, Yoshua Bengio교수님께서 제안하신 방법이다.
여기서 LM을 명시적으로 앙상블하여 Decoder성능을 올리고자 했다.

[Shallow Fusion 방법]
∙ 2개의 서로 다른 모델을 사용하는 방법

[Deep Fusion 방법]
∙ LM을 seq2seq에 포함시켜 end2end학습을 통해 하나의 모델로 만드는 방법

위의 두 방식에서 Deep Fusion방법이 더 나은 성능을 나타냈다.
두 방식 모두 monolingual corpus를 활용해 LM을 학습,
실제 번역기 훈련 시 신경망의 파라미터값을 고정한 채로 seq2seq모델을 학습시킨다.


아래 표는 '터키어→영어' NMT 성능을 각 방법을 적용해 실험한 결과이다.

뒤에 나올 back-translation이나 copied-translation들 보다 성능적 측면에서 이득이 적다.
하지만 수집한 단일언어코퍼스를 전부 활용할 수 있다는 장점이 존재한다.
 

On Using Monolingual Corpora in Neural Machine Translation

Recent work on end-to-end neural network-based architectures for machine translation has shown promising results for En-Fr and En-De translation. Arguably, one of the major factors behind this success has been the availability of high quality parallel corp

arxiv.org

 

2.2 Dummy-Sentence 활용
위의 명시적으로 LM을 앙상블하는 대신 Decoder로 하여금 단일언어코퍼스를 학습하는 방법의 논문을 제시했다.[https://arxiv.org/abs/1508.07909, https://arxiv.org/abs/1511.06709]
이 방법의 핵심은 src문장인 X를 빈 입력으로 넣어줌으로써, 그리고 attention등을 모두 Dropout으로 끊어줌으로써 Encoder로부터 전달되는 정보들을 없애는 것이다.
이 방법을 사용하면 Decoder가 단일언어코퍼스를 활용해 LM학습하는 것과 같다.

 

2.3 Back-Translation
한편, 위의 논문들에서 좀 더 발전된 다른 방법도 함께 제시되었다.
기존의 훈련된 역방향번역기(Back-Translation)를 사용해 mono-lingual corpus를 기계번역 후 합성병렬코퍼스(synthetic parallel corpus)를 만든 후, 이를 기존 양방향병렬코퍼스에 추가해 훈련에 사용하는 방식이다.
❗️중요한 점은 NMT로 만들어진 합성병렬코퍼스 사용 시, Back-Translation의 훈련에 사용한다는 점이다.

사실, 번역기를 만들면 하나의 parallel corpus로 2개의 NMT모델을 만들 수 있다.
따라서 이렇게 동시에 얻어지는 2개의 모델을 활용해 서로 보완을 통해 성능을 높이는 방법이다.
즉, Back Translation반대방향의 번역기를 통해 얻어지는 합성병렬코퍼스를 target신경망에 넣는다.
예를들어, KOR단일코퍼스가 있을 때, 아래 과정을 따른다.
∙ 기존에 훈련된 한→영 번역기에 기계번역을 시킴
∙ 한영 합성 병렬코퍼스를 생성
∙ 이를 기존에 수집한 한영 병렬코퍼스와 합친다.
∙ 이를 영→한 번역기를 훈련시키는 데 사용한다.
즉, 일종의 Augmentation 효과를 얻을 수 있다.
다만, 지나치게 많은 합성 병렬 코퍼스의 생성을 사용하면,
주객전도 현상이 될 수 있어 그 양을 제한해 훈련에 사용해야한다.

cf) 추가적인 설명(https://chan4im.tistory.com/205)

 

2.4 Copied Translation
이 방식은 Sennrich교수님이 제안한 방법으로 앞서 설명한 기존의 Dummy문장을 활용한 방식에서 좀 더 발전한 방식이다.
기존의 Dummy방법대신, src와 tgt쪽에 같은 data를 넣어 훈련시킨다.

∙기존의 Dummy문장을 Encoder에 넣는 방식은 Encoder에서 Decoder로 가는 경로를 훈련 시, DropOut이 필요
∙이 방식은 그럴 필요가 없지만, src언어의 vocabulary에 tgt언어의 어휘가 포함되는 불필요함을 감수하긴 해야한다.
따라서 보통 Back-Translation방식과 함께 사용된다.

Method TUR→ENG ENG→TUR
Back-Translation 19.7 14.7
Back Translation + Copied 19.7 15.6

 

Conclusion
위와 같이 여러 방법들이 제안되었다.
다만, 소개된 여러 방법들 중, 구현의 용이성 및 효율성으로 인해 아래 2가지가 많이 사용된다.
∙ Back Translation
∙ Copied Translation
위의 두 방법들은 매우 직관적이고 간단하면서 효과적인 성능향상을 얻을 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


3. Transformer (Attention is all you need)

제목에서부터 알 수 있듯, 기존의 attention연산만을 활용해 seq2seq를 구현, 성능과 속도 둘 다 모두 성공적으로 잡아냈다.

같이보면 좋을 내용(https://chan4im.tistory.com/162)

 

[논문 preview] - ViT : Vision Transformer(2020). part 2. Transformer

[Transformer] Attention is all you need[Vaswani2017; https://arxiv.org/abs/1706.03762] Attention Is All You Need The dominant sequence transduction models are based on complex recurrent or convolutional neural networks in an encoder-decoder configuration.

chan4im.tistory.com

 

3.1 Architecture
 Transformer는 오직 attention만을 사용해 Encoding과 Decoding을 전부 수행한다.
이때, Skip-Connection을 활용해 신경망을 깊게 쌓도록 도와준다.
Transformer의 Encoder와 Decoder를 이루는 Sub-Module은 크게 다음 3가지로 나뉜다.
∙ Self-Attention: 이전 층의 출력에 대해 attention연산을 수행
∙ Attention: 기존 seq2seq와 같이 encoder의 결과에 대해 attention연산을 수행
∙ Feed-Forward: attention층을 거쳐 얻은 결과물을 최종적으로 정리

 

Position Embedding
기존 RNN은 순차적으로 받아 자동으로 순서에 대한 정보를 기록한다.

하지만 Transformer는 RNN을 사용하지 않는다.
따라서, 순서정보를 단어와 함께 제공해야 된다. (같은 단어라도 위치에 따라 쓰임새, 역할이 달라질 수 있기 때문.)
결과적으로 위치정보를 Positional Embedding이라는 방법으로 나타낸다.

[Positional Embedding 일반식]
Positional Embedding 결과값의 차원 = word embedding vector 차원
∴ Positional Embedding행렬 + 문장임베딩행렬 → Encoder, Decoder의 입력.

cf) 문장 임베딩행렬 = 단어임베딩 벡터를 합친 행렬

 

Attention
Transformer의 Attention구성
[MHA]
Transformer는 여러개의 Attention으로 구성된 Multi-Head Attention(MHA)방식을 제안한다.
마치 CNN에서 여러 kernel이 다양한 feature를 추출하는 것과 같은 원리라 볼 수 있다.
이전시간, Q를 생성하기 위한 linear transformation을 배우는 과정이라 소개했다.

이때, 다양한 Q를 생성해 다양한 정보들을 추출한다면, 더욱 유용할 것이다.
따라서 Multi-Head로 여러 attention을 동시에 수행한다.


Q,K,V를 입력으로 받는 기본적인 Attention 수식은 다음과 같다.
앞의 Attention함수를 활용한 MHA 수식은 다음과 같다.
∙ Self-Attention의 경우, Q,K,V 모두 같은값으로써 이전층의 결과를 받아온다.
그리고 일반 Attention의 경우, Q는 이전층의 결과이고
K, V는 encoder의 마지막 층 결과가 된다.

이때, Q,K,V의 tensor_size는 다음과 같다. (이때, n = src_length, m = tgt_length)
∙ |Q| = (batch_size, n, hidden_size)
∙ |K| = |V| = (batch_size, m, hidden_size)

또한, MHA의 신경망 가중치 WiQ, WiK, WiV, WO의 크기는 아래와 같다.
∙ |WiQ|=|WiK|=|WiV| = (hidden_size, head_size)
∙ |WO| = (hidden_size×h , hidden_size)
이때, hidden_size=head_size×h이며 보통 512값을 갖는다.


transformer에서는 tgt_sentence의 모든 time-step을 encoder나 대상 tensor의 모든 time-step에 대해 한번에 attention을 수행한다.
이전장의 attention결과, tensor의 크기는 (batch_size, 1, hidden_size)였지만
MHA의 결과 tensor의 크기는 (batch_size, m, hidden_size)가 된다.
Self-Attention도 K,V가 Q와 같은 tensor일 뿐, 원리는 같기에 m=n이다.

 

Decoder의  Self-attention
Decoder에서 Self-Attention은 Encoder와 결이 살짝 달리하는데, 이전 층의 출력값으로 Q,K,V를 구성하는 것 같지만 약간의 제약이 가미된다. 왜냐하면 inference time에서 다음 time-step의 입력값을 당연히 알 수 없기 때문이다.

따라서 train에서도 이전 층의 결과값을 K와 V로 활용하는 self-attention을 수행하더라도 미래의 time-step에 접근할 수 없도록 똑같이 구현해줄 필요가 있다. 이를 위해 attention연산 시, masking을 추가해줘야한다.
이를 통해 미래의 time-step에 대해 attention_weight를 가질 수 없게 한다.

[Attention을 위한 Mask생성 방법]
mini-batch내부의 문장들은 길이가 서로 다를 수 있는데, masking을 통해 선택적으로 attention수행이 가능하다.
mini-batch의 크기는 mini-batch 내부의 가장 긴 문장의 길이(max_length)에 의해 결정된다.
길이가 짧은 문장들은 문장의 종료 후에 padding으로 채워진다.
문장 길이에 따른 mini-batch 형태

따라서 해당 mini-batch가 encoder를 통과하고 decoder에서 attention연산을 수행 시 문제가 발생한다.
padding이 존재하는 time-step에도 attention가중치가 넘어가 Decoder에 쓸데없는 정보를 넘겨줄 수 있다.
따라서 해당 time-step에 attention가중치를 추가적으로 다시 0으로 만들어줘야 한다.
mask 적용 시 attention

 

Feed Forward. &. Skip-Connection
∙ FFN은 attention결과를 정리하며
∙ attention블록의 출력값에 attention블록의 입력값을 더해 skip-connection을 구현해준다.

 

3.2 Pytorch의 MHA클래스
Attention을 직접 구현할 필요없이 pytorch에서 제공하는 attention클래스를 직접사용할 수 있다.
(https://pytorch.org/docs/stable/nn.html#multiheadattention)

다만 이 attention클래스는 이전(https://chan4im.tistory.com/201#n4)에 소개한 기본attention이 아닌, transformer에 사용된 attention이라는 점만 주의하자.
 

torch.nn — PyTorch 2.0 documentation

Shortcuts

pytorch.org

 

3.3 평가
Models BLEU(ENG→GER) FLOPs(훈련비용) BLEU(ENG→FRA) FLOPs(훈련비용)
GNMT + RL 24.6 2.3 × 1019 39.92 1.4 × 1020
ConvS2S 25.16 9.6 × 1018 40.46 1.5 × 1020
Transformer 27.3 3.3 × 1018 38.1 3.3 × 1018
Transformer(Big) 28.4 2.3 × 1018 41.8 2.3 × 1019
Google은 transformer가 기존 여타 알고리즘들보다 훨씬 좋은 성능을 달성했음을 밝혔는데, 기존 RNN 및 meta(facebook)의 ConvS2S(Convolutional Sequence to Sequence)보다 훨씬 빠른 속도로 훈련했음을 밝혔다(FLOPs).

이런 속도의 개선의 원인 중 하나로 transformer구조와 함께 input feeding의 제거 2가지 요인이 기인했다 보는 시각이 많은데, 기존 RNN 기반 seq2seq방식input feeding이 도입되면서 decoder훈련 시 모든 time-step을 한번에 할 수 없게 되었다.
따라서 FLOPs 대부분의 bottleneck문제가 decoder에서 발생하게 된다.
하지만 transformer의 경우, input feeding이 없기에 훈련 시 한번에 모든 time-step에 대한 병렬처리가 가능하다.

 

Conclusion
transformer의 혁신적인 구조적 차이는 seq2seq를 활용한 NMT 및 자연어 생성에도 사용되었다.
또한, BERT와 같은 자연어이해의 범주로까지 폭넓게 사용되고 있다.
 

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

We introduce a new language representation model called BERT, which stands for Bidirectional Encoder Representations from Transformers. Unlike recent language representation models, BERT is designed to pre-train deep bidirectional representations from unla

arxiv.org

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


마치며...

이번시간에는 NMT의 성능을 더 향상시키는 방법들을 다루었다.
신경망은 data가 많아질수록 그 성능이 향상되는데, 번역과 같은 seq2seq를 활용한 문제를 해결할 때, train data인 parallel corpus가 많을수록 좋다.
다만, parallel corpus는 매우 수집이 어렵고 제한적이다. (보통 완벽하지는 않지만 나름 사용할만한 번역기가 나오려면 최소 300만 이상의 문장 쌍이 필요)

따라서 zero-shot learning을 사용한다.
zero-shot learning모델이 train data에 직접 노출되지 않은 클래스를 인식하고 분류할 수 있는 능력으로 parallel corpus의 양이 얼마되지않는 언어쌍의 번역기 훈련 시, 이 방법으로 성능을 끌어올릴 수 있다.
(특히 한국어-일어, 스페인어-포르투갈어 와 같이 매우 비슷한 언어쌍을 같은 src, tgt언어로 사용 시 그 효과가 증폭된다.)


이런 제한적인 상황에 대해 단일언어(monolingual corpus)를 활용해 NMT 성능을 향상하는 방법 또한 다뤄보았다.
뒤에서 단일언어코퍼스를 활용한 성능향상에 대해서도 중점적으로 다룰 것이다.
또한 transformer라는 모델구조로 인해 seq2seq가 더욱 다양하게 구현될 수 있었다.
특히 transformer는 attention만으로 seq2seq를 구현하였기에 속도와 성능 둘 다 잡아냈다.

 

📌 목차

1. Neural Machine Translation (NMT)
2. seq2seq

3. Attention
4. Input Feeding
5. AutoRegressive. &. Teacher Forcing
6. Searching Algorithm(Inference)  &  Beam Search
7. Performance Metric [PPL / BLEU / METEOR / ROUGE]

😚 글을 마치며...

 

 


1. Neural Machine Translation (NMT)

1.1 번역의 목표
NMT는 end-to-end학습으로써, 규칙기반기계번역(RBMT)과 통계기반기계번역(SMT)의 명목을 이어받아서 가장 큰 성취를 이룩해냈다.
cf) end-to-end학습: 입력 데이터에서부터 원하는 출력을 직접 예측하고 학습하는 방식, 중간 단계나 특징 추출 단계 없이 입력과 출력 간의 관계를 모델링하려는 것을 의미


번역의 궁극적인 목표: 어떤 언어 f의 문장이 주어질 때, 가능한 e 언어의 번역문장 중, 최대확률을 갖는 값을 찾는것

 

1.2 기계번역의 역사
∙ 규칙기반 기계번역[RBMT]
주어진 문장의 구조를 분석, 그 분석에 따라 규칙을 세우고, 분류를 나눠 정해진 규칙에 따라 번역
이 과정을 사람이 모두 개입해야하기에 비용적 측면에서 매우 불리하다.

∙ 통계기반 기계번역[SMT]
대량의 Bi-Direction corpus에서 통계를 얻어 번역시스템을 구성하는 것으로
알고리즘이나 시스템으로 인해 언어쌍을 확장할 때, RBMT에 비해 훨씬 유리하다.

∙ 신경망 기계번역[NMT]
 - DNN 이전: Encoder-Decoder형태의 구조
 - DNN 이후: end-to-end 모델, NNLM기반, 훌륭한 문장임베딩 등의 장점으로 매우 powerful해졌다.

 

 

 


2. seq2seq

2.1 Architecture
seq2seq는 사후확률 P(Y | X;θ)를 최대로하는 모델의 파라미터를 찾아야 하며, 이 사후확률을 최대로 하는 Y를 찾아야 하기에 크게 3가지의 서브모듈[ Encoder / Decoder ]로 구성된다.

seq2seq : Sequence to Sequence[Sutskever2014; https://arxiv.org/abs/1409.3215]의 혁신성은 "가변길이의 문장을 가변길이의 문장으로 변환"할 수 있다는 것이다.
ex) 한국어→영어로 번역 시, 둘의 문장길이가 달라 seq2seq model이 필요

- 학습 시 Decoder의 input부분과 output부분이 모두 동작한다. 
즉, 정답에 해당하는 출력을 알려주는 교사강요(teacher forcing)방법을 사용.
teacher forcing에 대해서는 아래 5번항목에서 설명하겠다.

- 예측 시 정답을 모르기 때문에 회색표시한 input 부분을 제외하고 자기회귀(auto-regressive) 방식으로 동작한다.
자기회귀에서 <SOS>가 입력되면 첫 단어 'That'을 출력하고 'That'을 보고 그 다음 둘째 단어 'can't'를 출력한다.
즉, 이전에 출력된 단어를 보고 현재단어를 출력하는 일을 반복하며, 문장끝을 나타내는 <EOS>가 발생하면 멈춘다.


- 한계 : 가장 큰 문제는 encoder의 마지막 hidden state만 decoder에 전달한다는 점이다.
아래 그림에서 보면 h5만 decoder로 전달된다.
따라서 encoder는 마지막 hidden state에 모든 정보를 압축해야하는 부담이 존재한다.

 

 

Encoder
주어진 문장인 여러 개의 벡터를 입력으로 받아 문장을 함축하는 문장임베딩벡터로 만든다.
즉, P(z | X)를 모델링 후, 주어진 문장을 manifold를 따라 차원축소, 해당 도메인의 latent space의 어떤 하나의 점에 투영하는 작업이다.

다만 기존의 text classification에서는 모든 정보(feature)가 필요하지 않다.
따라서 벡터생성 시 많은 정보를 가질 필요가 없다.
하지만 NMT를 위한 sentence embedding vector 생성 시, 최대한 많은 정보를 가져야한다.

추가적으로 seq2seq모델에서 hidden layer간에 concatenate 작업으로 전체 time-step을 한번에 병렬로 처리한다.

 

Decoder
일종의 조건부 신경망 언어모델[CNNLM]에 조건부 확률변수 부분에 X가 추가된 형태라 할 수 있다.
즉, encoder의 결과인 sentence embedding vector와 이전 time-step까지 번역해 생성한 단어들에 기반해 현재 time-step의 단어를 생성한다.

특이한 점은 Decoder의 입력의 초기값으로써 BOS token을 입력으로 준다는 점이다.
❗️BOS (Beginning of Sentence): BOS는 문장의 시작을 나타내는 특별한 토큰 또는 심볼로 주로 Seq2Seq모델과 같은 모델에서 입력 시퀀스의 시작을 표시하는 데 사용된다.

 

Generator
Decoder에서 각 time-step별로 결과벡터 h를 받아 softmax를 계산해 각 target언어의 단어어휘별 확률값을 반환한다.
생성자의 결과값은 각 단어가 나타난 확률인 이산확률분포가 된다.

이때, 주의할 점은 문장의 길이가 |Y|=m이라면, 마지막 반환단어 ymEOS token이 된다는 점이다.
이 EOS로 Decoder 계산의 종료를 나타낸다.
❗️EOS (End of Sentence): EOS는 문장의 끝을 나타내는 특별한 토큰 또는 심볼로 주로 Seq2Seq 모델과 같은 모델에서 출력 시퀀스의 끝을 나타내는 데 사용됩니다.

 

2.2 seq2seq 활용분야
기계번역(MT)
ChatBot
Summarization
Speech Recognition
Image Captioning

 

2.3 한계 [Bottleneck Problem]
가장 큰 문제는 encoder의 마지막 hidden state만 decoder에 전달한다는 점이다.
이를 Bottleneck Problem이라 하는데, Encoder는 마지막 hidden state에 하나의 고정된 크기의 single vector로 모든 정보를 압축해야하는 부담이 존재하게 된다.
즉, 정보손실이 발생 및 기울기 소실이 되어버린다.

이런 한계를 해결하는 것이 바로 Attention 메커니즘을 이용하는 방법이다.
attention 메커니즘은 관련있는 단어와의 attention을 높여 기존처럼 뒤에 집중되는 현상을 방지한다.
즉, "특정부분에 집중"하기 위해 Decoder의 각 단계에서 encoder와 직접적인 연결을 하게 한다.

 


Pytorch 예제

Encoder 클래스
Encoder는 RNN을 사용한 text classification과 거의 유사하다.
따라서 Bi-Directional LSTM을 사용한다.
class Encoder(nn.Module):

    def __init__(self, word_vec_size, hidden_size, n_layers=4, dropout_p=.2):
        super(Encoder, self).__init__()

        # Be aware of value of 'batch_first' parameter.
        # Also, its hidden_size is half of original hidden_size,
        # because it is bidirectional.
        self.rnn = nn.LSTM(
            word_vec_size,
            int(hidden_size / 2),
            num_layers=n_layers,
            dropout=dropout_p,
            bidirectional=True,
            batch_first=True,
        )

    def forward(self, emb):
        # |emb| = (batch_size, length, word_vec_size)

        if isinstance(emb, tuple):
            x, lengths = emb
            x = pack(x, lengths.tolist(), batch_first=True)

            # Below is how pack_padded_sequence works.
            # As you can see,
            # PackedSequence object has information about mini-batch-wise information,
            # not time-step-wise information.
            # 
            # a = [torch.tensor([1,2,3]), torch.tensor([3,4])]
            # b = torch.nn.utils.rnn.pad_sequence(a, batch_first=True)
            # >>>>
            # tensor([[ 1,  2,  3],
            #     [ 3,  4,  0]])
            # torch.nn.utils.rnn.pack_padded_sequence(b, batch_first=True, lengths=[3,2]
            # >>>>PackedSequence(data=tensor([ 1,  3,  2,  4,  3]), batch_sizes=tensor([ 2,  2,  1]))
        else:
            x = emb

        y, h = self.rnn(x)
        # |y| = (batch_size, length, hidden_size)
        # |h[0]| = (num_layers * 2, batch_size, hidden_size / 2)

        if isinstance(emb, tuple):
            y, _ = unpack(y, batch_first=True)

        return y, h
Decoder 클래스
이후 나올 Attention개념을 추가하면, 아래와 같다.

class Decoder(nn.Module):

    def __init__(self, word_vec_size, hidden_size, n_layers=4, dropout_p=.2):
        super(Decoder, self).__init__()

        # Be aware of value of 'batch_first' parameter and 'bidirectional' parameter.
        self.rnn = nn.LSTM(
            word_vec_size + hidden_size,
            hidden_size,
            num_layers=n_layers,
            dropout=dropout_p,
            bidirectional=False,
            batch_first=True,
        )

    def forward(self, emb_t, h_t_1_tilde, h_t_1):
        # |emb_t| = (batch_size, 1, word_vec_size)
        # |h_t_1_tilde| = (batch_size, 1, hidden_size)
        # |h_t_1[0]| = (n_layers, batch_size, hidden_size)
        batch_size = emb_t.size(0)
        hidden_size = h_t_1[0].size(-1)

        if h_t_1_tilde is None:
            # If this is the first time-step,
            h_t_1_tilde = emb_t.new(batch_size, 1, hidden_size).zero_()

        # Input feeding trick.
        x = torch.cat([emb_t, h_t_1_tilde], dim=-1)

        # Unlike encoder, decoder must take an input for sequentially.
        y, h = self.rnn(x, h_t_1)

        return y, h
Generator 클래스
class Generator(nn.Module):

    def __init__(self, hidden_size, output_size):
        super(Generator, self).__init__()

        self.output = nn.Linear(hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, x):
        # |x| = (batch_size, length, hidden_size)

        y = self.softmax(self.output(x))
        # |y| = (batch_size, length, output_size)

        # Return log-probability instead of just probability.
        return y
Loss Function
'softmax+CE'보다는 'logsoftmax + NLL(음의 로그가능도)'를 사용한다.
# Default weight for loss equals to 1
# But, we don't need to get loss for PAD Token
# Thus, set a weight for PAD to 0

loss_weight = torch.ones(output_size)
loss_weight[data_loader.PAD] = 0.

# Instead of using Cross-Entropy,
# We can use NLL(Negative-Log-Likelihood) loss with log-probability
crit = nn.NLLLoss(weight=loss_weight, reduction='sum', )


따라서 softmax사용대신 logsoftmax함수로 로그확률을 구한다.

def _get_loss(self, y_hat, y, crit=None):
	# |y_hat| = (batch_size, length, output_size)
    # |y| = (batch_size, length)
    
    crit = self.crit if crit is None else crit
    loss = crit(y_hat.contiguous().view(-1, y_hat.size(-1)), 
    			y_contiguous().view(-1)
                )
    return loss

 

 

 

 

 

 

 

 

 

 

 

 


3. Attention

3.1 Attention의 목표
Query와 비슷한 값을 갖는 Key를 찾아 그 값을 얻는 과정. (이때, 그 값을 Value라 한다.)

 

3.2 Key-Value 함수
∙ Python의 Dictionary: Key-Value의 쌍으로 이루어진 자료형
Dic = {'A.I':9 , 'computer':5, 'NLP':4}


이와 같이 Key와 Value에 해당하는 값들을 넣고, Key를 통해 Value값에 접근가능하다.
즉, Query가 주어질 때, Key값에따라 Value값에 접근할 수 있다.
def KV(Q):
	weights = []
    
    for K in dic.keys():
    	weights += [is_same(K, Q)]
    
    weight_sum = sum(weights)
    
    for i, w in enumerate(weights):
    	weights[i] = weights[i] / weight_sum
        
    ans = 0
    
    for weight, V in zip(weights, dic.values()):
    	ans += weight*V 
    
    return ans
def is same(K, Q):
	if K == Q:
    	return 1.
    else:
    	reutnr .0

 

3.3 연속적인 Key-Value 벡터 함수
만약 Dic의 Value에 100차원 벡터가 들어가있다면??
혹은 Query와 Key값 모두 벡터를 다뤄야 한다면??
❓ 즉, Q, K가 word embedding vector라면??
 또는, Dic의 K, V값이 서로 같다면??

def KV(Q):
	weights = []
    
    for K in dic.keys():
    	weights += [how_similar(K, Q)]	# cosine similarity값을 채운다.
        
    weights = softmax(weights)	# 모든 가중치를 구한 후 softmax를 계산(모든 w합크기를 1로 고정)
    ans = 0
    
    for w, V in zip(weights, dic.values()):
    	ans += w*V
        
    return ans

 

위의 코드에서 ans에는 어떠한 벡터값이 들어간다.
ans내부의 벡터들의 코사인 유사도에 따라 벡터값이 정해진다.

즉, 위의 함수는 Q와 비슷한 K값을 찾아 유사도에 따라 Weight를 정하고, 각 K와 V값을 W값만큼 가져와 모두 더하는 것으로
이것이 바로 Attention Mechanism의 핵심 아이디어이다.

 

3.4 NMT에서의 Attention
그렇다면, MT에서 Attention Mechanism은 어떻게 작동될까?
  ∙ K,V: Encoder의 각 time-step별 출력
  ∙ Q: 현재 time-step의 Decoder출력

Seq2Seq with Attention
원하는 정보를 Attention을 통해 Encoder에서 얻고,
해당 정보를 Decoder의 출력과 이어붙여 tanh를 취한 후
softmax계산을 통해 다음 time-step의 입력이 되는 y_hat을 구한다.

 

Linear Transformation
신경망 내부의 각 차원들은 latent feature값이기에 정확히 정리할 수 없다.
하지만, 확실한 점은 source언어와 대상언어가 애초에 다르다는 것이다.
따라서, 단순히 벡터내적을 하기보단 소스와 대상간에 연결고리가 필요하다.

따라서, 두 언어가 각각 임베딩된 latent space이 선형관계에 있다 가정하고,
내적연산수행을 위해 선형변환을 해준다. (선형변환을 위한 W값은 가중치로 FF, BP로 학습된다.)


❓왜 Attention이 필요할까?에 대한 질문에서, 이 선형변환을 배우는 것 자체가 Attention이라 표현하는 것은 과하지 않다.
(∵ 선형변환과정으로 Decoder의 현재상태에 필요한 Q를 생성, Encoder의 K값들과 비교, 가중합을 하는 것이기 때문)
즉, Attention을 통해 Decoder는 Encoder에 Q를 전달하며, 이때 좋은 값을 전달하는 것은 좋은 결과로 이어지기 때문에 현재 Decoder상태에 필요한 정보가 무엇인지 스스로 판단해 선형변환을 통해 Q를 만드는 것이 매우 중요한 것이다.
또한 선형변환을 위한 가중치 자체도 한계가 있기에 Decoder의 상태 자체가 선형변환이 되어 Q가 좋은 형태가 되도록 RNN이 동작할 것이다.

수식 등의 자세한 예시 및 과정들 참고: https://chan4im.tistory.com/161
 

[논문 preview] - ViT : Vision Transformer(2020). part 1. Attention

[Attention의 배경] 🧐 고전적인 Attention Algorithm 1. Feature Selection - feature selection은 유용한 특징을 남기고 나머지는 제거하는 방법이다. 여기서 유용한 특징이란 예를 들어 분별력이 강한 특징 등을

chan4im.tistory.com

 

 

Pytorch 예제

∙ Attention 클래스: 선형변환을 위한 가중치파라미터를 bias가 없는 선형층으로 대체하였다.
조금 더 자세한 설명은 다음 Section에서 계속 진행한다.
class Attention(nn.Module):
	def __init__(self, hidden_size):
    	super(Attention, self).__init__()
        self.linear = nn.Linear(hidden_size, hidden_size, bias=False)
        self.softmax = nn.Softmax(dim = -1)
        
    def forward(self, h_src, h_t_target, mask=None):
    	# |h_src| = (batch_size, length, hidden_size)
        # |h_t_target| = (batch_size, 1, hidden_size)
        # |mask| = (batch_size, length)
        
        Q = self.linear(h_t_target.squeeze(1)).unsqueeze(-1)
        # |Q| = (batch_size, hidden_size, 1)
        
        weight = torch.bmm(h_src, Q).squeeze(-1)
        # |weight| = (batch_size, length)
        
        if mask is not None:
        	# Set each weight as -inf, if the mask value equals to 1.
            # Since the softmax operation makes -inf to 0,
            # masked weights would be set to 0 after softmax operation.
            # Thus, if the sample is shorter than other samples in mini-batch,
            # the weight for empty time-step would be set to 0.
            weight.masked_fill_(mask.unsqueeze(1), -float('inf'))
        
        weight = self.softmax(weight)

        context_vector = torch.bmm(weight, h_src)
        # |context_vector| = (batch_size, 1, hidden_size)

        return context_vector


cf) bmm (batch matrix multiplication; 배치 행렬곱)
torch.bmm(x, y)에 대해 설명해보자.

∙ |x| = (batch_size, m, k)
∙ |y| = (batch_size, h, m)
| torch.bmm(x, y) | = (batch_size, n, m)

 



 

 

 

 

 

 

 


4. Input Feeding

각 time-step의 decoder출력값과 attention결과값을 이어붙인 후 Generator Module에서 softmax를 취해 확률분포를 구한다.
이후 해당 확률분포에서 argmax를 수행해 y_hat을 sampling한다.
다만, 분포에서 sampling하는 과정에서보다 더 많은 정보가 손실된다.

따라서 softmax이전값도 같이 넣어주는 편이 정보손실없이 더 좋은 효과를 얻는다.,
y와 달리 concat층의 출력은 y가 embedding층에서 dense벡터로 변환되고 난 후 임베딩 벡터와 이어붙여 Decoder RNN에 입력으로 주어지는 과정input feeding이라 한다.

 

4.1 단점
이 방식은 train속도저하의 단점이 존재하는데, input feeding이전방식에서는 훈련 시 decoder 또한 encoder처럼 모든 time-step을 한번에 계산한다.
하지만 input feeding으로인해 decoder RNN입력으로 이전 time-step의 결과가 필요하게 되어 순차적으로 time-step별로 계산해야한다.

다만, 이 단점은 추론단계에서 어차피 decoder는 input feeding이 아니더라도 time-step별 병렬처리가 아닌 순차적 계산이 필요하기에 추론 시 input feeding으로 인한 속도 저하는 거의 없다; 따라서 이 단점이 크게 부각되지는 않는다.

 

Pytorch 예제
https://github.com/V2LLAIN/NLP/blob/main/5.%20RNN__Seq2Seq/seq2seq(with_attention).py

 

 

 

 

 

 

 

 

 

 

 


5. Auto Regressive .&.  Teacher Forcing

여기서 의문점...?

train 시 Decoder의 입력으로 time-step의 출력이 들어가는걸까??

사실, seq2seq의 기본적 훈련방식은 추론할 때의 방식과 상이하다.

 

5.1 AR(Auto Regressive) 속성
seq2seq의 train과 inference의 근본적인 차이는 AR속성으로 발생한다.

자기회귀(AR): 과거 자신의 값을 참조해 현재의 값을 추론하는 특징
이를 아래 수식에서도 확인할 수 있다.
다만, 과거의 결과값에따라 문장이나 시퀀스의 구성이 바뀔뿐만아니라 예측문장시퀀스의 길이도 바뀔 수 있고,
과거에 잘못된 예측을 했을 때, 더 큰 잘못된 예측을 할 가능성을 야기하기도 한다.

학습과정에서는 이미 정답을 알고있고 현재모델의 예측값과 정답과의 차이를 통해 학습하기에 자기회귀(AR)속성을 유지한 채 훈련할 수는 없다.

따라서 Teacher Forcing이라 불리는 방법을 통해 훈련을 진행한다.

 

5.2 Teacher Forcing 훈련방법
Teacher Forcing은 훈련 시 decoder의 입력으로 이전 time-step의 decoder 출력값이 아닌, 정답 Y가 들어간다는 점이다.
하지만 추론 시, 정답 Y를 모르기에 이전 time-step에서 계산되어 나온 y_hat값을 decoder의 입력으로 사용한다.
이렇게 입력을 넣어주는 훈련방법을 teacher forcing이라 한다.

이점:
초기 훈련 단계에서 안정적인 학습을 돕고, 모델이 초기에 어떤 것을 생성해야 하는지에 대한 강력한 신호를 제공한다.
모델이 정답 레이블을 보고 학습하므로 기울기소실 문제를 완화하여 그래디언트가 더 잘 흐를 수 있습니다.

 

cf) LM의 Teacher Forcing
[훈련 과정]
모델을 훈련할 때, 각 시점에서 이전 시점의 모델 출력이 아닌 실제 정답 레이블을 입력으로 제공한다.
즉, 이전 시점의 출력이 아닌 "선생님(teacher)" 역할의 정답 데이터를 사용한다.

[테스트(추론) 과정]
모델이 훈련을 마친 후에는 이전 출력을 입력으로 사용하여 시퀀스를 생성한다.
이때 이전 시점의 출력을 "자기 회귀적으로(autoregressively)" 사용한다.

 

 

 

 

 

 

 

 

 

 

 


6. Searching Algorithm(Inference). &. Beam Search

X가 주어졌을 때, Y_hat을 추론하는 방법에 대해 이야기 해보자.

이런 과정을 추론 또는 탐색(search)이라 부르는데, 탐색알고리즘에 기반하기 때문이다.

즉, 우리가 원하는 단어들 사이 최고의 확률을 갖는 경로(path)를 찾는 과정이다.

 

6.1 sampling
가장 정확한 방법은 time-step별 y_hat을 고를 때, 마지막 softmax층에서의 확률분포대로 sampling하는 것이다.
그 후 time-step에서 그 선택(y_hat)을 기반으로 그 다음 y_hat을 또 다시 sampling해 최종적으로 EOS가 나올때 까지 sampling하는 것이다.

다만, 이런 방식은 같은 입력에 대해 매번 다른 출력결과물이 나올 수 있어 지양하는편이다.

6.2 Greedy Search Algorithm 활용
DFS, BFS, DP 등 수많은 탐색기법이 존재하지만 Greedy Search Algorithm을 기반으로 탐색을 구현해보자.
즉, 모든 time-step에 대한 softmax확률값들 중 가장 확률값이 큰 인덱스를 뽑아 그 time-step의 y_hat을 사용하는 것이다. 

즉, 각 출력 예측 시 각 step에서 가장 가능성 높은 단어를 선택해 매우 빠른 탐색이 가능하다.

단점 1.) Decision을 되돌릴 수 없게 될 수 있고
단점 2.) 최종출력이 최적화된 결과에서 멀어질 수 있다.
             (∵ <END>token 생성전까지 decoding을 진행하기 때문)
Pytorch 예제
def search(self, src, is_greedy=True, max_length=255):
        if isinstance(src, tuple):
            x, x_length = src
            mask = self.generate_mask(x, x_length)
        else:
            x, x_length = src, None
            mask = None
        batch_size = x.size(0)

        # Same procedure as teacher forcing.
        emb_src = self.emb_src(x)
        h_src, h_0_tgt = self.encoder((emb_src, x_length))
        decoder_hidden = self.fast_merge_encoder_hiddens(h_0_tgt)

        # Fill a vector, which has 'batch_size' dimension, with BOS value.
        y = x.new(batch_size, 1).zero_() + data_loader.BOS

        is_decoding = x.new_ones(batch_size, 1).bool()
        h_t_tilde, y_hats, indice = None, [], []
        
        # Repeat a loop while sum of 'is_decoding' flag is bigger than 0,
        # or current time-step is smaller than maximum length.
        while is_decoding.sum() > 0 and len(indice) < max_length:
            # Unlike training procedure,
            # take the last time-step's output during the inference.
            emb_t = self.emb_dec(y)
            # |emb_t| = (batch_size, 1, word_vec_size)

            decoder_output, decoder_hidden = self.decoder(emb_t,
                                                          h_t_tilde,
                                                          decoder_hidden)
            context_vector = self.attn(h_src, decoder_output, mask)
            h_t_tilde = self.tanh(self.concat(torch.cat([decoder_output,
                                                         context_vector
                                                         ], dim=-1)))
            y_hat = self.generator(h_t_tilde)
            # |y_hat| = (batch_size, 1, output_size)
            y_hats += [y_hat]

            if is_greedy:
                y = y_hat.argmax(dim=-1)
                # |y| = (batch_size, 1)
            else:
                # Take a random sampling based on the multinoulli distribution.
                y = torch.multinomial(y_hat.exp().view(batch_size, -1), 1)
                # |y| = (batch_size, 1)

            # Put PAD if the sample is done.
            y = y.masked_fill_(~is_decoding, data_loader.PAD)
            # Update is_decoding if there is EOS token.
            is_decoding = is_decoding * torch.ne(y, data_loader.EOS)
            # |is_decoding| = (batch_size, 1)
            indice += [y]

        y_hats = torch.cat(y_hats, dim=1)
        indice = torch.cat(indice, dim=1)
        # |y_hat| = (batch_size, length, output_size)
        # |indice| = (batch_size, length)

        return y_hats, indice​

6.3 Beam Search
Greedy Algorithm은 매우 쉽고 간단하지만, 최적(optimal)해는 보장하지 않는다.
따라서 약간의 trick을 가미하는데, k개의 후보를 더 추적하는 것이다.
이때, k를 beam_size라 한다. 

Beam_size k에 대해 step이 진행되면서 k개의 가짓수에 대해 k를 유지, 최종 후보군에서 확률이 가장 높은 것을 선택한다.
다른 time-step에서 <END>token 생성이 가능하며
하나의 가설(hypothesis)에서 <END>가 나오면 종료하고, 다른 가설분기를 계속 탐색한다.
즉, 단어가 순차적 생성되어 동시사건확률 고려 및 생성할 때마다 log값이 더해져 더해지는 음수값이 많아져 작은값이 되는, 일종의 Normalize하는 과정을 한번 더 거칠 수 있게 된다.

∙ small k
  - greedy와 거의 비슷하다.(= ungrammatic, unnatural, nonsensical, incorrect)

∙ Large k
  - k가 커질수록 greedy문제는 줄지만 계산비용이 커진다.
  - BLEU_Score가 떨어지는 문제가 발생한다. (∵ too-short translation)

따라서 보통 Beam_size를 10 이하로 사용한다.



Pytorch 예제
#@profile
    def batch_beam_search(
        self,
        src,
        beam_size=5,
        max_length=255,
        n_best=1,
        length_penalty=.2
    ):
        mask, x_length = None, None

        if isinstance(src, tuple):
            x, x_length = src
            mask = self.generate_mask(x, x_length)
            # |mask| = (batch_size, length)
        else:
            x = src
        batch_size = x.size(0)

        emb_src = self.emb_src(x)
        h_src, h_0_tgt = self.encoder((emb_src, x_length))
        # |h_src| = (batch_size, length, hidden_size)
        h_0_tgt = self.fast_merge_encoder_hiddens(h_0_tgt)

        # initialize 'SingleBeamSearchBoard' as many as batch_size
        boards = [SingleBeamSearchBoard(
            h_src.device,
            {
                'hidden_state': {
                    'init_status': h_0_tgt[0][:, i, :].unsqueeze(1),
                    'batch_dim_index': 1,
                }, # |hidden_state| = (n_layers, batch_size, hidden_size)
                'cell_state': {
                    'init_status': h_0_tgt[1][:, i, :].unsqueeze(1),
                    'batch_dim_index': 1,
                }, # |cell_state| = (n_layers, batch_size, hidden_size)
                'h_t_1_tilde': {
                    'init_status': None,
                    'batch_dim_index': 0,
                }, # |h_t_1_tilde| = (batch_size, 1, hidden_size)
            },
            beam_size=beam_size,
            max_length=max_length,
        ) for i in range(batch_size)]
        is_done = [board.is_done() for board in boards]

        length = 0
        # Run loop while sum of 'is_done' is smaller than batch_size, 
        # or length is still smaller than max_length.
        while sum(is_done) < batch_size and length <= max_length:
            # current_batch_size = sum(is_done) * beam_size

            # Initialize fabricated variables.
            # As far as batch-beam-search is running, 
            # temporary batch-size for fabricated mini-batch is 
            # 'beam_size'-times bigger than original batch_size.
            fab_input, fab_hidden, fab_cell, fab_h_t_tilde = [], [], [], []
            fab_h_src, fab_mask = [], []
            
            # Build fabricated mini-batch in non-parallel way.
            # This may cause a bottle-neck.
            for i, board in enumerate(boards):
                # Batchify if the inference for the sample is still not finished.
                if board.is_done() == 0:
                    y_hat_i, prev_status = board.get_batch()
                    hidden_i    = prev_status['hidden_state']
                    cell_i      = prev_status['cell_state']
                    h_t_tilde_i = prev_status['h_t_1_tilde']

                    fab_input  += [y_hat_i]
                    fab_hidden += [hidden_i]
                    fab_cell   += [cell_i]
                    fab_h_src  += [h_src[i, :, :]] * beam_size
                    fab_mask   += [mask[i, :]] * beam_size
                    if h_t_tilde_i is not None:
                        fab_h_t_tilde += [h_t_tilde_i]
                    else:
                        fab_h_t_tilde = None

            # Now, concatenate list of tensors.
            fab_input  = torch.cat(fab_input,  dim=0)
            fab_hidden = torch.cat(fab_hidden, dim=1)
            fab_cell   = torch.cat(fab_cell,   dim=1)
            fab_h_src  = torch.stack(fab_h_src)
            fab_mask   = torch.stack(fab_mask)
            if fab_h_t_tilde is not None:
                fab_h_t_tilde = torch.cat(fab_h_t_tilde, dim=0)
            # |fab_input|     = (current_batch_size, 1)
            # |fab_hidden|    = (n_layers, current_batch_size, hidden_size)
            # |fab_cell|      = (n_layers, current_batch_size, hidden_size)
            # |fab_h_src|     = (current_batch_size, length, hidden_size)
            # |fab_mask|      = (current_batch_size, length)
            # |fab_h_t_tilde| = (current_batch_size, 1, hidden_size)

            emb_t = self.emb_dec(fab_input)
            # |emb_t| = (current_batch_size, 1, word_vec_size)

            fab_decoder_output, (fab_hidden, fab_cell) = self.decoder(emb_t,
                                                                      fab_h_t_tilde,
                                                                      (fab_hidden, fab_cell))
            # |fab_decoder_output| = (current_batch_size, 1, hidden_size)
            context_vector = self.attn(fab_h_src, fab_decoder_output, fab_mask)
            # |context_vector| = (current_batch_size, 1, hidden_size)
            fab_h_t_tilde = self.tanh(self.concat(torch.cat([fab_decoder_output,
                                                             context_vector
                                                             ], dim=-1)))
            # |fab_h_t_tilde| = (current_batch_size, 1, hidden_size)
            y_hat = self.generator(fab_h_t_tilde)
            # |y_hat| = (current_batch_size, 1, output_size)

            # separate the result for each sample.
            # fab_hidden[:, begin:end, :] = (n_layers, beam_size, hidden_size)
            # fab_cell[:, begin:end, :]   = (n_layers, beam_size, hidden_size)
            # fab_h_t_tilde[begin:end]    = (beam_size, 1, hidden_size)
            cnt = 0
            for board in boards:
                if board.is_done() == 0:
                    # Decide a range of each sample.
                    begin = cnt * beam_size
                    end = begin + beam_size

                    # pick k-best results for each sample.
                    board.collect_result(
                        y_hat[begin:end],
                        {
                            'hidden_state': fab_hidden[:, begin:end, :],
                            'cell_state'  : fab_cell[:, begin:end, :],
                            'h_t_1_tilde' : fab_h_t_tilde[begin:end],
                        },
                    )
                    cnt += 1

            is_done = [board.is_done() for board in boards]
            length += 1

        # pick n-best hypothesis.
        batch_sentences, batch_probs = [], []

        # Collect the results.
        for i, board in enumerate(boards):
            sentences, probs = board.get_n_best(n_best, length_penalty=length_penalty)

            batch_sentences += [sentences]
            batch_probs     += [probs]

        return batch_sentences, batch_probs​

 

 

 

 

 

 

 

 

 

 

 

 

 

 


7. Performance Metric [PPL / BLEU / METEOR / ROUGE]

7.1 정성적 평가 (Intrinsic Evaluation)
보통 사람이 번역된 문장을 채점하는 형태
이때, 사람의 선입견이 반영될 수 있기에 보통 blind test와 같은 방식을 고수한다.
가장 정확할 수는 있지만 자원과 시간이 많이 든다는 단점이 존재한다.

이후 구글의 번역시스템을 알아볼 때, 구글에서 정성적 평가를 통해 얻은 점수에 대해 알아볼 것이다.

 

7.2 정량적 평가 (Extrinsic Evaluation)
위에서 언급한 정성적 평가의 단점으로 인해 보통 자동화된 정량평가를 주로 사용한다.
이때, 최대한 비슷한 일관성을 갖는 평가를 해야하며, 언어적 특징이 반영된 평가방법이라면 더더욱 좋을 것이다.

PPL (Perplexity)
NMT도 기본적으로 매 time-step마다 최고확률을 갖는 단어를 선택(classification)하는 작업이기에 Cross Entropy를 기본적인 Loss Function으로 사용한다.
NMT또한 조건부 언어모델이기에 PPL를 통한 성능측정이 가능하다.

결과적으로 Cross-Entropy에 exp를 취한 PPL을 평가지표로 활용가능하다.
(PPL이 낮을수록, N-gram의 N이 클수록 좋은 모델 ; https://chan4im.tistory.com/200#n3)
CE와 직결되어 간편함이 있다는 장점이 있지만, 실제 번역기 성능과 완벽한 비례관계에 있다 할 수는 없다.

각 time-step별 실제 정답에 해당하는 단어의 확률만 채점하기 때문이다.
하지만 언어는 같은 의미의 문장들이라도 어순이 바뀔수도 있고,
비슷한 의미의 단어로 치환될 수도 있기에 완전히 잘못된 번역이더라도 Loss값이 낮을수도 있다.

따라서 실제 번역문의 품질과 CE사이에는 괴리가 존재한다.
(특히나 Teacher Forcing방식이 더해지기에 더더욱 괴리가 존재한다.)
 

[Gain Study_NLP]06. Language modeling (N-gram, Metric, SRILM, NNLM, OCR)

📌 목차 1. preview 2. N-gram 3. Language Model Metric 4. SRILM 활용한 N-gram 실습 5. NNLM (BOS, EOS) 6. Language Model의 활용 (Speech Recognition / 기계번역 / OCR 등) 😚 글을 마치며... 1. Preview 2.1 LM (Language Model) 언어모델

chan4im.tistory.com


BLEU (Bi-Lingual Evaluation Understudy)
위에서 말한 PPL에서 CE의 괴리를 줄이기 위해 여러 방법들이 제시되었는데, 가장 대표적인 BLEU에 대해 알아보고자 한다.

BLEU Score는 정답과 예측문장간에 일치하는 N-gram 개수의 비율의 기하평균에 따라 점수를 매긴다.
즉, 각 N-gram별 precision의 평균을 백분율로 나타내는 것이고
짧은문장에대한 페널티(brevity_penalty)는 예측된 번역문이 정답문장보다 짧을경우, 점수가 좋아지는 것을 방지하기 위한 것이다.
BLEU Score가 높을수록 좋은 모델임을 의미한다.

또한, 실제 성능측정 시, BLEU를 직접구현하기보단 SMT Framework인 MOSES에 포함된 multi-bleu.perl을 주로 사용한다.

 


METEOR (Metric for Evaluation of Translation with Explicit ORdering)
METEOR는 NMT 및 NLP생성 작업에서 사용되는 자동 평가지표 중 하나이다.
METEOR는 번역 품질을 측정하고 참조(reference) 번역과 생성된 번역 간의 유사성을 판단한다.
METEOR는 BLEU와 유사한 목적을 가지고 있지만 몇 가지 중요한 차이점이 있다.

Main 주안점)
BLEU로부터 파생된 방법으로 BLEU의 불완전함을 보완하고 더 나은 지표를 설계하고자
BLEU와의 가장 큰 차이점으로 precision만 고려하는 BLEU와는 달리
 - recall도 함께 고려했다는 것이다.
 - 추가적으로 다른 가중치를 적용한 이 둘의 조화평균을 성능 계산에 활용하고,
 - 오답에 대해 별도의 penalty를 부과하는 방식을 채택하거나,
 - 여러 단어나 구 등을 정답으로 처리하는 등 BLEU를 보완하고자 했다.


METEOR의 주요 특징과 작동 방식은 다음과 같다:

∙ 항목 정확도 (Precision): METEOR는 단어나 구절 수준의 일치를 측정한다.
번역 후보와 참조 번역 간의 공통된 토큰 (단어 또는 구절) 수를 계산한다.
이것은 "항목 정확도" 또는 "Precision"으로 알려져 있습니다. 
수식은 아래와 같다.


∙ 기하 평균 F1 점수: METEOR는 정확도와 리콜(recall) 간의 균형을 측정하기 위해 정확도와 리콜의 조화 평균인 F1 점수를 계산합니다. 이것은 번역 후보의 정확성과 참조 번역과의 유사성을 모두 고려합니다.
수식은 아래와 같다.
penalty의 경우, 아래와 같이 계산되며
최종적인 METEOR Score는 아래와 같다.


∙ 번역 후보와 참조 번역 사이의 어휘와 구조적 변화: METEOR은 단어의 순서와 구조적 변경을 포함한 다양한 어휘와 문법적인 변화를 고려합니다. 이것은 단어 순서를 보다 강조하는 특징이 있으며, 번역 후보와 참조 번역 간의 공통 어휘 및 구조를 비교합니다.

∙ 어휘, 구절 정렬 및 동의어 처리: METEOR은 어휘의 동의어 처리를 수행하며, 구절 정렬과 유사성을 계산하는 데 사용됩니다.

METEOR의 결과는 0과 1 사이의 점수로 나타납니다. 높은 METEOR 점수는 번역이 참조 번역과 유사하다는 것을 나타내며, 높은 번역 품질을 의미합니다. METEOR는 주로 번역 품질 평가를 위해 사용되며, 다양한 자연어 처리 작업에서도 적용될 수 있습니다.

ROUGE (Recall-Oriented Understudy for Gisting Evaluation)
ROUGE는 NLP에서 사용되는 자동 평가지표 중 하나로, 텍스트 생성 및 자동 요약 작업에서 많이 사용된다.
ROUGE는 생성된 텍스트 또는 요약과 기준(reference) 텍스트 간의 유사성을 측정하고 평가하는 데 사용된다.
ROUGE는 5개의 평가 지표가 있다.
- ROUGE-N
- ROUGE-L
- ROUGE-W 
- ROUGE-S
- ROUGE-SU


∙ ROUGE-N (ROUGE-Ngram)
ROUGE-N 메트릭은 N-gram (연속된 n개의 단어 또는 문자) 일치를 측정한다.
일반적으로 ROUGE-1, ROUGE-2, ROUGE-3 등과 같이 지정된 n-gram 길이를 나타낸다.


즉, ROUGE-N은 예측한 요약문과 실제 요약문간의 N-gram의 Recall값으로 쉽게 나타내면 아래와 같다.
 ‣ ROUGE-1: 1-gram (unigram) = 단어 단위의 일치를 계산
 ‣ ROUGE-2: 2-gram (bigram) 단위의 일치를 계산
 ‣ ROUGE-3: 3-gram (trigram) 단위의 일치를 계산
ROUGE-N은 유사성을 측정하고 단어 순서를 고려하지 않는다.
ex) ROUGE-1
∙실제 요약문 uni-gram: 'Korea', 'won', 'the', 'world', 'cup'
∙예측 요약문 uni-gram: 'Korea', 'won', 'the', 'soccer', 'world', 'cup', 'final'
예측 요약문과 실제 요약문 사이에 겹치는 uni-gram 수는 5이고,
실제 요약문의 uni-gram수도 5이므로
∙ROUGE-1 = 5/5 = 1

ex) ROUGE-2
∙실제 요약문 bi-gram: 'Korea won', 'won the', 'the world', 'world cup'
∙예측 요약문 bi-gram: 'Korea won', 'won the', 'the soccer', 'soccer world', 'world cup', 'cup final'
예측 요약문과 실제 요약문 사이에 겹치는 bi-gram 수는 3이고,
실제 요약문의 bi-gram수는 4이므로
∙ROUGE-2 = 3/4 = 0.75

📌 목차

1. preview
2. N-gram

3. Language Model Metric
4. SRILM 활용한 N-gram 실습
5. NNLM (BOS, EOS)
6. Language Model의 활용 (Speech Recognition / 기계번역 / OCR 등)

😚 글을 마치며...

 

 

 


1. Preview

1.1 LM (Language Model)
언어모델(LM; Language Model)은 문장의 확률을 나타내는 모델이다.
확률값을 통해 문장 자체의 출현확률, 이전단어들에 대해 다음단어를 예측할 수 있다.
결과적으로 주어진 문장이 얼마나 자연스럽고 유창한지(fluent) 계산할 수 있다.

 

1.2 Hell 난이도, 한국어
∙ 한국어: 대표적인 교착어
∙ 영어: 고립어(+ 굴절어)
∙ 중국어: 고립어

교착어의 특징상 단어의 의미나 역할은 어순보다는 단어에 부착되는 어미같은 접사나 조사에 의해 결정된다.
즉, 단어의 어순이 중요하지않고 생략 또한 가능하기에 단어간에 확률계산 시 불리하다.

영어나 기타 라틴어 기반 언어들은 어순이 더 규칙적이기에 한국어에 비해 헷갈릴 가능성이 낮다.
추가적으로 한국어는 교착어의 특징상 접사 및 조사로 단어의 의미∙역할이 결정되기에 아래와 같이 여러 조사가 붙어 수많은 단어로 파생될 수 있다.
ex) 학교, 학교에서, 학교에서도, 학교, 학교, 학교, 학교조차도, . . .

따라서 어미를 분리해주지 않으면 어휘의 수가 기하급수적으로 늘기에 희소성이 높아져 문제해결이 더 어려워질 수 있다.

 

1.3 문장의 확률표현
문장의 확률은 Bayes 정리에 따라 조건부 확률을 표현할 수 있다.
(참고: https://chan4im.tistory.com/199#n2)
 

[Gain Study_NLP]05. Text Classification (Naïve Bayes(MLE/MAP), RNN, CNN, Multi-Label 분류)

📌 목차 1. preview 2. Naïve Bayes 활용하기 3. 흔한 오해 2 4. RNN 활용하기 5. CNN 활용하기 6. 쉬어가기) Multi-Label Classification 😚 글을 마치며... 1. Preview Text Classification이란, 텍스트∙문장∙문서를 입력

chan4im.tistory.com

 

 

 

 

 

 

 

 


2. N-gram

전체 단어를 조합하는 대신, 바로 앞의 일부 조합만 출현빈도를 계산해 확률을 추정하는 방법 (이때, N = k+1)

2.1 Sparse Data Problem
LM은 문장의 확률을 수식으로 나타내고, 해당확률을 근사하기 위해 수집한 corpus에서 각 word sequence 빈도를 계산하면 된다.

하지만, 확률추정치를 제대로 구하기란 어려운 문제이다.
수많은 단어를 crawling하더라도, 애초에 출현가능한 단어의 조합의 경우의 수는 훨씬 더 크기 때문이다.

단어의 조합이 조금만 길어지더라도,
corpus에서 출현빈도를 구할 수 없기에 분자가 0이되어 확률이 0이되거나,
심지어 분모가 0이되어 정의불가능이라 할 수도 있다.

물론 이에대해, 차원의 저주와 희소성에 대해 미리 다루긴 했다.
∙ 단순성 & 모호성(https://chan4im.tistory.com/196)
∙ word embedding (https://chan4im.tistory.com/197#n2)
 

[Gain Study_NLP]02. Similarity. &. Ambiguity (one-hot encoding, feature, TF-IDF)

📌 목차 1. word sense 2. one-hot encoding 3. thesaurus(어휘분류사전) 4. Feature 5. Feature Extraction. &. Text Mining (TF-IDF) 6. Feature vector 생성 7. Vector Similarity (with Norm) 8. 단어 중의성 해소 (WSD) 9. Selection Preference 😚

chan4im.tistory.com

 

[Gain Study_NLP]03. Word Embedding (word2vec, GloVe)

📌 목차 1. preview 2. Dimension Reduction 3. 흔한 오해 1 4. word2vec [2013] 5. GloVe (Global Vectors for word representation) 6. word2vec 예제 (FastText 오픈소스) 😚 글을 마치며... 1. Preview [Gain Study_NLP]02. Similarity. &. Ambiguit

chan4im.tistory.com

 

 

 

2.2 Markov Assumption
corpus에서 word sequence에 대한 확률을 효과적으로 추정하려면 희소성문제를 해결해야한다.
이때, Markov 가정(Markov Assumption)을 도입한다.

Markov 가정이란??
특정시점의 상태확률은 단지 그 직전 상태에만 의존한다는 논리.
즉, 앞서 출현한 모든 단어를 살펴볼 필요없이,
앞의 k개의 단어(= 바로 직전 상태)만 보고 다음 단어의 출현확률을 구하는 것
.


식으로 나타내면 위와 같은데, 이렇게 조건을 간소화해 실제로 구하고자하는 확률을 근사한다.
보통 k는 0~3의 값을 갖는다.(k=2라면, 앞 2개단어를 참조해 다음 단어 xi의 확률을 근사해 나타낸다.)

여기에 Chain Rule을 적용하고 로그확률로 표현하면 아래와 같다.

이렇게 전체 단어를 조합하는 대신, 바로 앞의 일부 조합만 출현빈도를 계산해 확률을 추정하는 방법을 N-gram이라 부른다. (이때, N = k+1)

corpus의 양과 N의 수치는 보통 비례하는데,
N이 커질수록 우리가 가진 train corpus에 존재하지 않을 가능성이 높기에 정확한 추정이 어려워진다.



k (N=k+1) N-gram 명칭
0 1-gram uni-gram
1 2-gram bi-gram
2 3-gram tri-gram
따라서 보통 3-gram을 가장 많이 사용하며, train data가 매우 충분하다면, 4-gram을 사용하기도 한다.(사실 그렇게 큰 효율성은 없음)
∵ 4-gram을 사용하면 모델의 성능은 크게 오르지 않지만 단어 조합의 경우의 수는 지수적으로 증가하기 때문


ex) 3-gram
∙ 3-gram의 가정에 따라, 다음과 같이 3개 단어의 출현빈도와 앞 2개의 출현빈도만 구하면 xi의 확률을 근사할 수 있다.
즉, 문장전체의 확률에 비해 Markov가정을 도입하면, 문장의 확률을 근사할 수 있다.
이렇게 되면, train corpus에서 보지못한 문장에 대해서도 확률을 추정할 수 있다.

 

2.3 Generalization
train data에 없는 unseen sample의 예측능력 (= 일반화 능력)에 성능이 좌우된다.
N-gram역시 Markov가정의 도입으로 희소성에 대처하는 일반화능력을 어느정도 갖는다.

더욱 일반화 능력을 향상시킬 수 있는 방법들을 살펴보도록 하자.
Smoothing  &  Discounting
출현 횟수를 단순히 확률값으로 추정한다면...?
train corpus에 출현하지 않는 단어 corpus에 대처능력이 저하된다.
즉, unseen word sequence라고해서 확률을 0으로 추정해버리게 된다.
∴ 출현빈도값(word frequency)이나 확률값을 더욱 다듬어(smoothing)줘야 한다.

가장 간단한 방법은 모든 word sequence의 출현빈도에 1을 더하는 것이다.
이를 수식으로 나타내면 아래와 같다.
이 방법은 매우 간단하고 직관적이지만, LM처럼 희소성 문제가 클 경우 사용은 비적절하다.
이와 관련해 Naïve Bayes 등을 활용하는 내용을 전에 다뤘다.(https://chan4im.tistory.com/199#n2)

Kneser-Ney Discounting
Smoothing의 희소성문제 해결을 위해 KN(Kneser-Ney) Discounting을 제안한다.

❗️핵심 아이디어
∙ 단어 w가 다른 단어 v의 뒤에 출현 시, 얼마나 다양한 단어 뒤에서 출현하는지(= 즉, v가 얼마나 다양한 지)를 알아내는 것
다양한 단어 뒤에 나타나는 단어일수록 unseen word sequence로 나타날 가능성이 높다는 것이다.


KN Discounting은 Scorecontinuation을 다음과 같이 모델링하는데, 
즉, w와 함께 나타난 v들의 집합인 {v:Count(v,w)>0}의 크기가 클수록 Scorecontinuation은 클 것이라 가정한다.
수식은 아래와 같다.
위의 수식을 아래와 같은 과정으로 진행해보자.

w와 함께 나타난 v들의 집합 {v : Count(v:w)>0}의 크기를
전체 단어 집합으로부터 sampling한 w'∈W일때 v, w'가 함께 나타난 집합{v:Count(v,w')>0}의 크기합으로 나눈다.
수식은 아래와 같다.

이렇게 우린 bi-gram을 위한 PKN을 아래 수식처럼 정의할 수 있다.
이때, d는 상수로 보통 0.75의 값을 갖는다.
이처럼 KN Discounting은 간단한 직관에서 출발해 복잡한 수식을 갖는다.
여기서 약간의 수정을 가미한, Modified-KN Discounting이 보편적인 방법이다.
cf) 언어모델툴킷(SRILM)에 구현되어있는 기능을 통해 쉽게 사용할 수 있다.
Interpolation
다수의 LM사이의 선형결합(interpolation)을 통해 LM을 일반화해보자.
LM의 interpolation이란, 2개의 서로다른 LM을 선형적으로 일정비율(λ)로 섞어주는 것이다.

특정 영역에 특화된 LM구축 시, interpolation은 굉장히 유용한데, 특정영역의 작은 corpus로 만든 LM과 섞음으로써 특정영역에 특화된 LM을 강화할 수 있다.

예를 들어 의료분야음성인식이나 기계번역시스템 구축을 가정해보자.
기존의 일반 영역 corpus를 통해 생성한 LM이라면 의료용어표현이 낯설 수 있다.
반대로 특화영역의 corpus만 사용해 LM을 생성한다면, generalization능력이 지나치게 떨어질 수 있다.

∙ 일반 영역
 - P(진정제 | 준비,된) = 0.00001
 - P(사나이 | 준비,된) = 0.01

∙ 특화 영역
 - P(진정제 | 준비,된) = 0.09
 - P(약 | 준비,된) = 0.04

∙ interpolation 결과
 - P(진정제 | 준비,된) = 0.5*0.09  +  (1-0.5)*0.00001 = 0.045005


결국 일반적인 의미와는 다른 뜻의 단어가 나올수도 있고, 일반적인 대화에서는 희소한 word sequence가 훨씬 자주 등장할 수도 있다.
또한, 특화영역의 corpus에는 일반적인 word sequence가 매우 부족할 것이다.
이런 문제점들을 해결하기 위해 각 영역의 corpus로 생성한 LM을 섞어주어 해당영역에 특화할 수 있다.
Back-Off
너무 길거나 복잡한 word sequence는실제 train corpus에서 굉장히 희소하다.
따라서 Markov 가정을 통해 일반화가 가능하며, Back-Off방식은 한단계 더 나아간 방식이다.

아래 수식을 보면, 특정 N-gram의 확률을 N보다 더 작은 sequence에 대해 확률을 구해 interpolation을 진행한다.
예를 들어 3-gram의 확률에 대해 2-gram, 1-gram의 확률을 interpolation을 할 때, 이를 수식으로 나타내면 다음과 같이 N보다 더 작은 sequence의 확률을 활용함으로써 더 높은 smoothing&generalization 효과를 얻을 수 있다.

 

2.4 Conclusion
N-gram방식은 출현빈도를 통해 확률을 근사하기에 매우 쉽고 간편하다.

Prob)
다만 단점 또한 명확한데, train corpus에 등장하지 않는 단어 corpus의 경우, 확률을 정확히 알 수 없다.

Sol)
따라서 Markov 가정을 통해 단어 조합에 필요한 조건을 간소화할 수 있고
더 나아가 Smoothing, Back-Off 등으로 단점을 보완하였다.

하지만 여전히 근본적 해결책은 아니며, 현재 DNN의 도입은 음성인식, 기계번역에 사용되는 LM에 큰 빛을 가져다 주었다.
DNN시대에서도 여전히 N-gram방식은 강력하게 사용될 수 있는데, 문장을 생성하는 것이 아닌, 주어진 문장의 유창성(fluency)을 채점하는 문제라면, 굳이 복잡한 DNN이 아니더라도 N-gram방식이 여전히 좋은 성능을 낼 수 있다.
(DNN이 얻는 사소한 이득은 매우 귀찮고 어려운 일이 될 것.)

 

 

 

 

 

 

 

 

 

 


3.  LM - Metrics

3.1 PPL (Perplexity)
LM의 평가척도인 perplexity(PPL)을 측정하는 정량평가(extrinsic evaluation) 방법이다.
PPL은 문장의 길이를 반영, 확률값을 정규화한 값이다.

문장의 확률값이 분모에 있기에 확률값이 높을수록 PPL은 작아진다.
따라서 PPL값이 작을수록, N-gram의 N이 클수록좋은 모델이다.

 

3.2 PPL의 해석
출현확률이 n개라면, 매 time-step으로 가능한 경우의 수가 n인 PPL로
PPL은 일종의 n개의 branch의 수(뻗어나가는 수)를 의미하기도 한다.

ex) 20,000개의 vocabulary라면, PPL은 20,000이다. (단, 단어출현확률이 모두 같을때)

하지만 만약 3-gram기반 LM으로 측정한 PPL이 30이 나왔다면
평균적으로 30개의 후보단어 중에 헷갈리고 있다는 것으로
다음 단어 예측 시, 30개의 후보군중 고르는 경우로 알 수 있다.

 

3.3 PPL과 Entropy의 관계
앞서 언급했듯, 정보량의 평균을 의미하는 Entropy의 경우,
∙ 정보량이 낮으면 확률분포는 sharp한 모양이고
∙ 정보량이 높으면 확률분포는 flat한 모양이 된다.


먼저 실제 LM의 분포 P(x)나 출현가능문장들의 집합 W에서 길이 n의 문장 w1:n을 sampling 시, 우리의 LM분포 Pθ(x)의 entropy를 나타낸 수식은 아래와 같다.

여기서 몬테카를로(Monte Carlo) sampling을 통해 위의 수식을 근사시킬 수 있다.



앞서와 같이 entropy H식을 근사시킬 수 있지만, 사실 문장은 sequential data이기에
entropy rate라는 개념을 사용하면 아래처럼 단어당 평균 entropy로 나타낼 수 있다.
마찬가지로 Monte Carlo sampling을 적용할 수 있다.


이 수식을 조금만 더 바꾸면, 아래와 같다.

여기에 PPL 수식을 생각해보면 앞서 Cross-Entropy로부터 도출한 수식과 비슷한 형태임을 알 수 있다.



최종적으로 PPL과 CE의 관계는 아래와 같다.


∴ MLE를 통해 parameter θ 학습 시, CE로 얻는 손실값에 exp를 취함으로써 PPL을 얻을 수 있다.

 

 

 

 

 

 

 

 

 

 

 


4. SRILM을 활용한 N-gram 실습

SRILM은 음성인식∙segmentation∙MT(기계번역) 등에 사용되는 n-gram 모델을 쉽게 구축하고 적용하능한 Tool-kit이다.

 

4.1 SRILM 설치하기
(http://www.speech.sri.com/projects/srilm/download.html)에서 간단한 정보를 기입, SRILM을 내려받을 수 있다.

이후 아래와 같이 디렉토리를 생성, 그 안에 압축해제 진행.
$ mkdir srilm
$ cd ./srilm
$ tar -xzvf ./srilm-1.7.2.tar.gz​

 


디렉토리 내부에 Makefile을 열어 7번째 라인의 SRILM의 경로 지정 후 주석을 해제한다.
이후 make명령으로 SRILM을 빌드한다.
$ vi ./Makefile

7번째 라인의 SRILM의 경로 지정 후 주석을 해제

$ make​


build가 정상적으로 완료되면 PATH에 SRILM/bin 내부에 새롭게 생성된 디렉토리를 등록 후 export
PATH = {SRILM_PATH}/bin/{MACHINE}:$PATH
#PATH = /home/IHC/Workspace/nlp/srilm/bin/i686-m64:$PATH
export PATH​


아래와 같이 ngram-count와 ngram이 정상적으로 동작하는 것을 확인한다.
$ source ~/.profile
$ ngram-count -help
$ ngram -help​

 

4.2 Dataset 준비
이전 전처리 장에서 다뤘던 것처럼 분절이 완료된 파일을 데이터로 사용한다.
이후 파일을 train data와 test data로 나눈다.

 

4.3 기본 사용법
SRILM에서 사용되는 프로그램들의 주요인자 설명
∙ ngram-count : LM 훈련
∙ vocab : lexicon file_name
∙ text : training corpus file_name
∙ order : n-gram count
∙ write : output count file_name
∙ unk : mark OOV as
∙ kndiscountn : Use Kneser -Ney discounting for N-grams of oerder n
∙ ngram : LM 활용
∙ ppl : calculate perplexity for test file name
∙ order : n-gram count
∙ lm : LM file_name​
LM 만들기
ex) kndiscount를 사용한 상태에서 3-gram을 훈련,
LM과 LM을 구성하는 vocabulary를 출력하는 과정
$ time ngram-count -order 3 -kndiscount -text <text_fn> -lm <output_lm_fn> -write_vocab <output_vocab_fn> -debug 2​
문장 생성하기
N-gram모듈을 사용해 만들어진 LM을 활용해 문장을 생성해보자.
문장 생성 이후 전처리(https://chan4im.tistory.com/195)에서 설명했듯, 분절을 복원해줘야 한다.
이때, 아래 예시는 Linux의 pipeline을 연계해 sed를 통한 정규표현식을 사용해 분적을 복원한다.
$ ngram -lm <input_lm_fn> -gen <n_sentence_to_generate> | sed "s/ // g" | sed "s/__//g" | sed "s/_//g" | sed "s/^\s//g"​

만약, 항상 sed와 정규표현식이 귀찮다면 파이썬으로도 가능하다.

평가
성능 평가의 경우, 아래 명령을 통해 수행될 수 있다.
$ ngram -ppl <test_fn> -lm <input_lm_fn> -order 3 -debug 2​

위의 명령을 실행하면 OoVs(Out of Vacabularies)와 해당 test문장들에 대한 PPL이 출력으로 나온다.
주로 문장수에 대해서 평균을 계산한(pp1 아님) ppl을 참고하면 된다.
interpolation
SRILM을 통해 단순 Smoothing(= discounting)뿐만 아니라 Interpolation(보간) 또한 수행할 수 있는데,
이 경우에는 완성된 2개의 다른 LM이 필요하고, 이를 섞기위한 hyper-parameter λ가 필요하다.

아래와 같은 명령어를 입력하면 interpolation 수행이 가능하다.
$ ngram -lm <input_lm_fn> -mix-lm <mix_lm_fn> -lambda <mix_ratio_between_0_and_1> -write-lm <output_lm_fn> -debug 2​


interpolation 이후 성능평가 시, 경우에 따라 성능향상을 경험할 수 있으며 λ를 튜닝함으로써 성능향상 폭을 더 높일 수 있다.

 

 

 

 

 

 

 

 

 

 


5. NNLM

5.1 희소성 해결하기
N-gram기반 LM은 간편하지만 기존 corpus train data에 해당 N-gram이 없거나 존재하지 않는 단어의 조합에는 출현 빈도를 계산할 수 없어서 확률을 구할 수 없고 확률간 비교를 할 수 없는 등 상당히 generalization에 취약하다는 것을 알 수 있다.

N-gram 기반 LM의 약점을 보완하기 위해 NNLM이 나오게 되었는데, NNLM(Neural Network Language Model)은 word embedding을 사용해 단어차원축소를 통해 corpus와 유사한 dense vector를 학습하고, 더 높은 유사도를 갖게 하여 generalization 성능을 높임으로써 희소성해소(WSD)가 가능하다.

NNLM은 다양한 형태를 갖지만 가장 효율적이고 흔한형태인 RNN계열의 LSTM을 활용한 RNNLM방식에 대해 알아보자.

 

5.2 RNNLM (RNN Language Model)

기존의 LM은 각각의 단어를 불연속적 데이터로 취급해서 word sequence(단어조합)의 길이가 길어지면 희소성문제로 어려움을 겪었다. 따라서 Markov가정을 통해 n-1이전까지의 단어만 주로 조건부로 사용해 확률을 근사시켰다.

하지만 RNNLM은 word embedding을 통해 dense vector를 생성하고
이를 통해 희소성문제를 해소하여 문장의 첫 단어부터 해당 단어 직전의 단어까지 모두 조건부에 넣어 확률을 근사시킬 수 있다.
여기에 로그를 넣을 수도 있는데, π로 바꾸고 양변에 로그를 취하면 된다.

 

5.3 구현 수식 및 설명



이때, 입력문장의 시작과 끝에는 x0와 xn+1이 추가되어 BOS와 EOS를 나타낸다.
수식을 과정별로 설명해보면, 아래와 같다.

∙ 먼저 문장 x1:n[:-1]를 입력으로 받는다.
∙ 이후 각 time-step별 토큰 xi로 임베딩 계층 emb에 넣는다.
∙ 그 후 정해진 차원의 word embedding vector를 얻는다.
❗️주의점) EOS를 떼고 embedding layer에 input으로 넣어야한다.

❗️BOS (Beginning of Sentence): BOS는 문장의 시작을 나타내는 특별한 토큰 또는 심볼로 주로 Seq2Seq모델과 같은 모델에서 입력 시퀀스의 시작을 표시하는 데 사용된다.
ex) 기계 번역 모델에서 번역할 문장의 시작을 BOS 토큰으로 표시하여 모델에게 문장을 시작하라고 알려줄 수 있습니다.

❗️EOS (End of Sentence): EOS는 문장의 끝을 나타내는 특별한 토큰 또는 심볼로 주로 Seq2Seq 모델과 같은 모델에서 출력 시퀀스의 끝을 나타내는 데 사용됩니다.
ex) 기계 번역 모델이 번역을 마쳤을 때 EOS 토큰을 생성하여 출력 시퀀스가 끝났음을 나타냅니다.




RNN은 해당 word_embedding_vector를 입력으로 받고
RNN의 hidden_state_size인 hidden_size의 vector를 반환한다.
이때, pytorch를 통해 문장의 모든 time-step을 한번에 병렬로 계산할 수 있다.



여기 tensor에 linear layer와 softmax를 적용해 각 단어에 대한 확률분포인 (x_hat)_i+1를 구한다.

여기서 LSTM을 사용해 RNN을 대체할 수 있다.

test dataset에 대해 PPL을 최소화하는 것이 목표이므로 Cross Entropy Loss를 사용해 optimizing을 진행한다.
이때, 주의할 점은 입력과 반대로 BOS를 제거한 정답인 x1:n[1:]와 비교한다는 것이다.
Pytorch 구현예제
import torch
import torch.nn as nn

import data_loader


class LanguageModel(nn.Module):

    def __init__(self, 
                 vocab_size,
                 word_vec_dim=512,
                 hidden_size=512,
                 n_layers=4,
                 dropout_p=.2,
                 max_length=255
                 ):
        self.vocab_size = vocab_size
        self.word_vec_dim = word_vec_dim
        self.hidden_size = hidden_size
        self.n_layers = n_layers
        self.dropout_p = dropout_p
        self.max_length = max_length

        super(LanguageModel, self).__init__()

        self.emb = nn.Embedding(vocab_size, 
                                word_vec_dim,
                                padding_idx=data_loader.PAD
                                )
        self.rnn = nn.LSTM(word_vec_dim,
                           hidden_size,
                           n_layers,
                           batch_first=True,
                           dropout=dropout_p
                           )
        self.out = nn.Linear(hidden_size, vocab_size, bias=True)
        self.log_softmax = nn.LogSoftmax(dim=2)

    def forward(self, x):
        # |x| = (batch_size, length)
        x = self.emb(x) 
        # |x| = (batch_size, length, word_vec_dim)
        x, (h, c) = self.rnn(x) 
        # |x| = (batch_size, length, hidden_size)
        x = self.out(x) 
        # |x| = (batch_size, length, vocab_size)
        y_hat = self.log_softmax(x)

        return y_hat

    def search(self, batch_size=64, max_length=255):
        x = torch.LongTensor(batch_size, 1).to(next(self.parameters()).device).zero_() + data_loader.BOS
        # |x| = (batch_size, 1)
        is_undone = x.new_ones(batch_size, 1).float()

        y_hats, indice = [], []
        h, c = None, None
        while is_undone.sum() > 0 and len(indice) < max_length:
            x = self.emb(x)
            # |emb_t| = (batch_size, 1, word_vec_dim)

            x, (h, c) = self.rnn(x, (h, c)) if h is not None and c is not None else self.rnn(x)
            # |x| = (batch_size, 1, hidden_size)
            y_hat = self.log_softmax(x)
            # |y_hat| = (batch_size, 1, output_size)
            y_hats += [y_hat]

            # y = torch.topk(y_hat, 1, dim = -1)[1].squeeze(-1)
            y = torch.multinomial(y_hat.exp().view(batch_size, -1), 1)
            y = y.masked_fill_((1. - is_undone).byte(), data_loader.PAD)
            is_undone = is_undone * torch.ne(y, data_loader.EOS).float()            
            # |y| = (batch_size, 1)
            # |is_undone| = (batch_size, 1)
            indice += [y]

            x = y

        y_hats = torch.cat(y_hats, dim=1)
        indice = torch.cat(indice, dim=-1)
        # |y_hat| = (batch_size, length, output_size)
        # |indice| = (batch_size, length)

        return y_hats, indice

 

5.4 Conclusion
NNLM은 word_embedding_vector를 사용해 희소성문제해결에 큰 효과를 본다.
결과적으로 train dataset에 없는 단어조합에도 훌륭한 대처가 가능하다.

다만, N-gram에 비해 더 많은 cost가 필요하다.

 

 

 

 

 

 

 

 

 

 


6. Language Model의 활용

Language Model을 단독으로 사용하는 경우는 매우 드물다.

다만, NLP에서 가장 기본이라 할 수 있는 LM은 매우 중요하며, 현재 DNN을 활용해 더욱 발전하고 있다.

LM은 자연어생성의 가장 기본이되는 모델이므로 활용도는 떨어질 지언정 중요성과 역할이 미치는 영향은 부인할 수 없을 것이다.

대표적 활용분야는 아래와 같다.

 

6.1 Speech Recognition
컴퓨터의 경우, 음소별 분류의 성능은 이미 사람보다 뛰어나다.
하지만 사람과 달리 주변 문맥정보를 활용하는 능력(= 일명 '눈치')이 없기에
주제가 전환되는 등의 상황에서 음성 인식률이 떨어지는 경우가 상당히 있다.
이때, 좋은 LM을 학습해 사용하면 음성인식의 정확도를 높일 수 있다.

아래 수식은 음성인식의 수식을 대략적으로 나타낸 것으로
음성신호 X가 주어졌을 때, 확률을 최대로 하는 문장 Y_hat을 구하는 것이 목표이다.


여기에 Bayes 정리로 수식을 전개하면, 밑변 P(X)를 날려버릴 수 있다.
∙P(X|Y) : Speech Model (= 해당 음향 signal이 나타날 확률)
∙P(Y) : Language Model (= 문장의 확률)

 

6.2 Machine Translation
기계번역의 경우, 언어모델이 번역시스템을 구성할 때, 중요한 역할을 한다.
기존의 통계기반 기계번역(SMT)에서는 음성인식과 유사하게
LM이 번역모델과 결합해 자연스러운 문장을 만들도록 동작한다. 

신경망 기계번역(NMT; https://chan4im.tistory.com/201)이 주로 사용되는데, 
NMT에서도 LM이 매우 중요한 역할을 한다. (자세한 내용은 아래 링크 참고)

 

6.3 OCR (광학 문자 인식)
광학문자인식(OCR)를 만들 때도 LM이 사용된다.
사진에서 추출해 글자를 인식할 때, 각 글자간 확률을 정의하면 더 높은 성능을 낼 수 있다.

따라서 OCR에서도 언어모델의 도움을 받아  글자나 글씨를 인식한다.

 

6.4 기타 Generative Model
음성인식, MT, OCR 역시 주어진 정보를 바탕으로 문장을 생성해내는 일종의 자연어 생성이라 볼 수 있다.
기계학습의 결과물로써 문장을 만들어내는 작업은 모두 자연어 생성문제의 카테고리라 볼 수 있다.

 

 

 

 

 

 

 

 

 

 

 


마치며...

이번시간에는 주어진 문장을 확률적(stochastic)으로 모델링하는 방법(LM)을 알아보았다.
NLP에서 문장예측의 필요성은 DNN이전부터 있어왔기에, N-gram 등의 방법으로 많은 곳에 활용되었다.

다만, N-gram과 같은 방식들은 여전히 단어를 불연속적인 존재로 취급하기에
희소성문제를 해결하지 못해 generalization에서 많은 어려움을 겪었다.

이를 위해 Markov가정, Smoothing, Didcounting으로 N-gram의 단점을 보완하고자 했지만 N-gram은 근본적으로 출현빈도에 기반하기에 완벽한 해결책이 될 수는 없었다.


하지만 DNN의 도입으로 LM을 시도하면 Generalization이 가능하다.
DNN은 비선형적 차원축소에 매우 뛰어난 성능을 갖기에, 희소단어조합에도 효과적 차원축소를 통해 뛰어난 성능을 낼 수 있다.
따라서 inference time에서 처음보는 sequence data가 주어지더라도 기존에 비해 기존 학습을 기반으로 훌륭한 예측이 가능하다.




지금까지 LM이 정말 많은 분야(음성인식, TM, OCR)에서 초석으로 다양하게 활용됨을 알 수 있었다.
이제, 신경망을 통해 개선된 LM으로 뒷 내용들에서는 자연어생성(특히 TM;번역)에 대해 다뤄볼 것이다.
https://chan4im.tistory.com/201 , https://chan4im.tistory.com/202

📌 목차

1. preview
2. Naïve Bayes 활용하기

3. 흔한 오해 2
4. RNN 활용하기
5. CNN 활용하기
6. 쉬어가기) Multi-Label  Classification

😚 글을 마치며...

 

 

 


1. Preview

Text Classification이란, 텍스트∙문장∙문서를 입력으로 받아 사전에 정의된 클래스중 어디에 속하는지 분류(classification)하는 과정으로 아래와 같이 응용분야가 다양하다.

문제 클래스 예시
감성분석 (Sentiment Analysis) 긍정 / 중립 / 부정
스팸메일 탐지 (Spam Detection) 정상 / 스팸
사용자 의도 분류 (Intent Classification) 명령 / 질문 / 잡담 등
주제 분류 (Topic Classification) 각 주제
카테고리 분류 (Category Classification) 각 카테코리

딥러닝 이전에는 Naïve Bayes Classification, Support Vector Machine 등 다양한 방법으로 Text Classification을 진행하였다.
이번시간에는 딥러닝 이전의 가장 간단한 분류방식인 Naïve Bayes방식을 비롯, 여러 딥러닝 방식을 알아보자.

 

 

 

 

 

 


2. Naïve Bayes 활용하기

Naïve Bayes는 아주 강력한("각 feature는 independent하다!"라는 강력한 가정을 가짐) 분류방식으로

성능은 준수하지만 단어라는 불연속적인 symbol을 다루는 NLP에서는 아쉬운 면이 존재한다.

2.1 MAP (Maximum A Posterior)

❗️Bayes Theorem

이때, 대부분의 문제에서 evidence, P(D)는 구하기 어렵기에
P(c | D) ∝ P(D | c)∙P(c) 식으로 접근하기도 한다.
앞의 성질을 이용하면, 주어진 data D에 대해 확률을 최대로 하는 클래스 c를 구할 수 있는데,

❗️MAP
이처럼 사후확률을 최대화하는 클래스 c를 구하는 것을 MAP(사후확률최대화)라 한다.
❗️MLE
이와 마찬가지로 가능도를 최대화하는 클래스 c를 구하는 것을 MLE(최대가능도추정)이라 한다.
MLE는 주어진 data D와 label C에 대해 확률분포를 근사하기 위한 
parameter θ를 훈련하기위한 방법으로 사용된다.
MLE. vs. MAP
MAP가 경우에 따라 MLE보다 더 정확할 수 있다. (∵ 사전확률이 포함되어있어서)

 

2.2 Naïve Bayes
 Naïve Bayes는 MAP를 기반으로 작동한다.
가정: 각 feature는 independent하다! 라는 강력한 가정을 바탕으로 진행된다.
대부분의 경우, 사후확률을 구하기 어렵기에 가능도와 사전확률의 곱으로 클래스를 예측한다.

만약 다양한 특징으로 이루어진 data의 경우, feature가 희박하기에 가능도를 구하기 또한 어렵다.
이때, Naïve Bayes가 매우 강력한 힘을 발휘하는데, 각 특징이 독립적이라는 가정을 통해 사전확률이 실제 data corpus에서 출현한 빈도를 통해 추정이 가능해지는 것이다.

이처럼 간단한 가정으로 데이터의 희소성문제를 해결하는 쉽고 강력한 방법으로 MAP의 정답클래스라벨예측이 가능해지는 것이다.

상세예시 및 식은 아래 2.3을 참고

 

2.3 Sentiment Analysis 예제
위와 같이 class와 data가 긍정/부정과 document로 주어질 때, 
'I am happy to see this movie' 라는 문장이 주어진다면, 이 문장이 긍정인지 부정인지 판단해보자!

 Naïve Bayes를 활용해 단어의 조합에 대한 확률을 각각 분해할 수 있다.
즉, 각 단어의 출현확률을 독립적이라 가정 후, 결합가능도확률을 모두 각각의 가능도확률로 분해한다.
그렇게 되면 데이터 D에서의 출현빈도를 구할 수 있다.

이처럼 corpus에서 단순히 각 단어의 class당 출현빈도를 계산하는 것만으로도 간단한 sentiment analysis가 가능하다.

 

2.4 Add-One Smoothing
Naïve Bayes가정을 통해 corpus에서 출현확률을 독립으로 만들어 출현횟수를 적극적으로 활용할 수 있게 되었다.
여기서 문제점이 발생하는데, 만약 Count(happy, neg)=0이라면? P(happy | neg)=0이 되어버린다.
 
아무리 data corpus에 존재하지 않더라도 그런 이유로 해당 sample의 출현확률을 0으로 추정해버리는 것은 매우 위험한 일이 되어버리기에 아래처럼 분자(출현횟수)에 1을 더해주면 쉽게 문제해결이 가능하다.(물론 완벽한 해결법은 아님)

 

2.5 장점 및 한계
장점: 단순히 출현빈도를 세는 것처럼 쉽고 간단하지만 강력!!
딥러닝을 활용하기에 label랑 문장 수가 매우 적은 경우, 오히려 복잡한 딥러닝방식보다 더 나은 대안이 될 수 있다.

한계: 'I am not happy'와 'I am happy'에서 not의 추가로 문장은 정반대뜻이 된다.
수식으로는 P(not, happy) ≠ P(not)∙P(happy)가 된다.
단어간 순서로 인해 생기는 정보도 무시할 수 없는데, "각 특징은 서로 독립적이다."라는 Naïve Bayes의 기본가정은 언어의 이런 특징을 단순화해 접근해 한계가 존재한다.

 

 

 

 

 

 

 

 

 


3.  흔한 오해 2

 

표제어추출(lemmatization), 어간추출(stemming)을 수행해 접사등을 제거한 이후 Text Classification을 진행해야하는가??
예를들어, "나는 학교에 가요"라는 원문이 있다면, [나 학교 가] 처럼 어간추출이 진행된다.

이는 적은 corpus에서 효과를 발휘하여 희소성문제에서 어느정도의 타협점이 존재할 수 있게된다.
특히, DNN이전 전통적 기계학습방법에서 불연속적 존재인 자연어에 좋은 돌파구를 마련해주었다.

하지만, DNN시대에서는 성공적으로 차원축소(https://chan4im.tistory.com/197#n2)를 수행할 수 있게 되면서 희소성문제는 더이상 큰 장애물이 되지는 않기에 lemmazation, stemming등은 반드시 정석이라 하긴 어렵다.
 

[Gain Study_NLP]03. Word Embedding (word2vec, GloVe)

📌 목차 1. preview 2. Dimension Reduction 3. 흔한 오해 1 4. word2vec [2013] 5. GloVe (Global Vectors for word representation) 6. word2vec 예제 (FastText 오픈소스) 😚 글을 마치며... 1. Preview [Gain Study_NLP]02. Similarity. &. Ambiguit

chan4im.tistory.com

또한, "나는 학교에 가요" / "나만 학교에 가요" 라는 두 문장은 서로 긍정 / 부정이라는 다른 class를 갖기에 lemmazation이나 stemming을 한 후 Text Classification에 접근하는 것은 바람직하지 못한 방법일 수도 있다.
따라서 이후 설명될 신경망모델을 사용해 text classification을 시도하는것이 훨씬 바람직하다.

만약, 성능향상을 위해 tuning 및 여러 시도에서 corpus의 부족이 성능저하의 원인이라 생각될 때, 추가적인 실험으로는 괜찮은 시도가 될 수 있다.

 

 

 

 

 

 

 

 


4. RNN 활용하기

이제 DNN을 통한 text classification문제를 살펴보자.
가장 간단한 방법은 RNN을 활용하는 것으로 sequential data라는 문장의 특징을 가장 잘 활용가능한 신경망 구조이다.

n개의 단어로 이루어진 문장 x에 대해 RNN이 순전파 시, n개의 hidden_state를 얻는다.
이때, 가장 마지막 은닉층으로 text classification이 가능하며 RNN은 입력으로 주어진 문장을 분류문제에 맞게 encoding할 수 있다.
즉, RNN의 출력값은 문장임베딩벡터(sentence embedding vector)라 할 수 있다.

 

4.1 Architecture
알다시피, text에서 단어는 불연속적 값이기에 이들이 모인 문장 또한, 불연속적값이다.
즉, 이산확률분포에서 문장을 sampling한 것이므로 입력으로는 one-hot벡터들이 여러 time-step으로 주어진다.

mini-batch까지 고려한다면, 입력은 3차원 tensor (n×m×|V|)가 될 것이다.
 ∙  n : mini_batch size (= 한번에 처리할 문서의 개수)
 ∙ m : sentence length (= feature vector의 차원수 = 텍스트 문서의 단어의 개수)
 ∙|V| : Vocabulary size (= Dataset내의 고유한 단어/토큰의 총 수)


하지만 원핫벡터는 주어진 |V| 차원에 단 하나의 1과 |V|-1개의 0으로 이루어진다.
효율적 저장을 위해 굳이 원핫벡터 전체를 가지고 있을 필요는 없기에 
원핫벡터를 0 ~ |V|-1 사이 정수로 나타낼 수 있게 된다면,  2차원 matrix (n×m)으로 충분히 나타낼 수 있다.

이렇게 원핫인코딩된 (n×m) tensor를 embedding층에 통과시키면,
word embedding tensor를 얻을 수 있다.

이후 word_embedding tensor를 RNN에 통과시키면 된다.
이때, 우린 RNN에 대해 각 time-step별, 계층별로 구분해 word_embedding tensor나 hidden_state를 넣어줄 필요가 없다.

최종적으로 제일 마지막 time-step만 선택해 softmax층을 통과시켜 이산확률분포 P(y | x;θ)로 나타낼 수 있다.
이때 제일 마지막 time-step은 H[:, -1]과 같은 방식으로 index slicing을 통해 도출할 수 있다.

모델구조로 보면 아래와 같다.
마지막으로 원핫벡터 y이기에 인덱스의 로그확률값만 최대화하면 되므로
CE Loss 수식은 NLL(음의 로그가능도)를 최소화하는 것과 동치이다.

 

Pytorch 구현예제
앞의 수식을 pytorch로 구현한 예제코드로 여러계층으로 이뤄진 LSTM을 사용했다.
∙ LSTM에는 각 층마다 Dropout이 사용되며
∙ NLL(음의 로그가능도)손실함수로 최적화하기 위해 logsoftmax로 로그확률을 반환한다.
import torch.nn as nn


class RNNClassifier(nn.Module):

    def __init__(
        self,
        input_size,
        word_vec_size,
        hidden_size,
        n_classes,
        n_layers=4,
        dropout_p=.3,
    ):
        self.input_size = input_size  # vocabulary_size
        self.word_vec_size = word_vec_size
        self.hidden_size = hidden_size
        self.n_classes = n_classes
        self.n_layers = n_layers
        self.dropout_p = dropout_p

        super().__init__()

        self.emb = nn.Embedding(input_size, word_vec_size)
        self.rnn = nn.LSTM(
            input_size=word_vec_size,
            hidden_size=hidden_size,
            num_layers=n_layers,
            dropout=dropout_p,
            batch_first=True,
            bidirectional=True,
        )
        self.generator = nn.Linear(hidden_size * 2, n_classes)
        # We use LogSoftmax + NLLLoss instead of Softmax + CrossEntropy
        self.activation = nn.LogSoftmax(dim=-1)

    def forward(self, x):
        # |x| = (batch_size, length)
        x = self.emb(x)
        # |x| = (batch_size, length, word_vec_size)
        x, _ = self.rnn(x)
        # |x| = (batch_size, length, hidden_size * 2)
        y = self.activation(self.generator(x[:, -1]))
        # |y| = (batch_size, n_classes)

        return y​

 

 

 

 

 

 

 

 

 


5. CNN 활용하기

5.1 Convolution Operation
5.2 Convolution Layer

자세한 설명은 아래 링크 참고 (https://chan4im.tistory.com/133)

 

self.DL.(03). CNN (Convolution Neural Network)

🧐 CNN (Convolution Neural Network) 🤫 CNN, 합성곱 신경망이란? 여러 분야, 특히나 image classification에서 좋은 성능을 보여주는 방법이다. 이런 합성곱 신경망에서 합성곱의 연산은 정의 자체에 가중치를

chan4im.tistory.com

 

 

5.3 Text Classification with CNN
CNN은 RNN과 달리 순차적 정보보다는 패턴인식 및 파악에 중점을 두는 구조를 갖는다.
CNN은 classification에 중요한 단어들의 조합에 대한 패턴을 감지하기도 하는데,
해당 클래스를 나타내는 단어조합에 대한 pattern의 유무를 가장 중시한다.

예를들어, 'good'이라는 단어는 긍정/부정 분류에 핵심이 되는 중요한 signal로 작동한다.
그렇다면, 'good'에 해당하는 embedding vector의 pattern을 감지하는 filter를 모델이 학습한다면?
'better', 'best', 'great'등의 단어들도 'good'과 비슷한 벡터값을 갖게 될 것이다.
→ 더 나아가 단어들의 조합 패턴(word sequence pattern)을 감지하는 filter도 학습이 가능할 것이다.

모델 구조에 대해 간단히 설명하자면, 아래와 같다.
먼저 one-hot벡터를 표현하는 인덱스값을 단어임베딩벡터(1차원)로 변환한다.
그 후 문장 내 모든 time-step의 단어임베딩벡터를 합치면 2차원 행렬이 된다.
그 후 Convolution Operation을 수행하면 CNN이 효과를 발휘한다.



 

Pytorch 구현예제
RNN의 text classification처럼 NLL(음의 로그가능도)손실함수로 최적화하기 위해 logsoftmax로 로그확률을 반환한다.
import torch
import torch.nn as nn


class CNNClassifier(nn.Module):

    def __init__(
        self,
        input_size,
        word_vec_size,
        n_classes,
        use_batch_norm=False,
        dropout_p=.5,
        window_sizes=[3, 4, 5],
        n_filters=[100, 100, 100],
    ):
        self.input_size = input_size  # vocabulary size
        self.word_vec_size = word_vec_size
        self.n_classes = n_classes
        self.use_batch_norm = use_batch_norm
        self.dropout_p = dropout_p
        # window_size means that how many words a pattern covers.
        self.window_sizes = window_sizes
        # n_filters means that how many patterns to cover.
        self.n_filters = n_filters

        super().__init__()

        self.emb = nn.Embedding(input_size, word_vec_size)
        # Use nn.ModuleList to register each sub-modules.
        self.feature_extractors = nn.ModuleList()
        for window_size, n_filter in zip(window_sizes, n_filters):
            self.feature_extractors.append(
                nn.Sequential(
                    nn.Conv2d(
                        in_channels=1, # We only use one embedding layer.
                        out_channels=n_filter,
                        kernel_size=(window_size, word_vec_size),
                    ),
                    nn.ReLU(),
                    nn.BatchNorm2d(n_filter) if use_batch_norm else nn.Dropout(dropout_p),
                )
            )

        # An input of generator layer is max values from each filter.
        self.generator = nn.Linear(sum(n_filters), n_classes)
        # We use LogSoftmax + NLLLoss instead of Softmax + CrossEntropy
        self.activation = nn.LogSoftmax(dim=-1)

    def forward(self, x):
        # |x| = (batch_size, length)
        x = self.emb(x)
        # |x| = (batch_size, length, word_vec_size)
        min_length = max(self.window_sizes)
        if min_length > x.size(1):
            # Because some input does not long enough for maximum length of window size,
            # we add zero tensor for padding.
            pad = x.new(x.size(0), min_length - x.size(1), self.word_vec_size).zero_()
            # |pad| = (batch_size, min_length - length, word_vec_size)
            x = torch.cat([x, pad], dim=1)
            # |x| = (batch_size, min_length, word_vec_size)

        # In ordinary case of vision task, you may have 3 channels on tensor,
        # but in this case, you would have just 1 channel,
        # which is added by 'unsqueeze' method in below:
        x = x.unsqueeze(1)
        # |x| = (batch_size, 1, length, word_vec_size)

        cnn_outs = []
        for block in self.feature_extractors:
            cnn_out = block(x)
            # |cnn_out| = (batch_size, n_filter, length - window_size + 1, 1)

            # In case of max pooling, we does not know the pooling size,
            # because it depends on the length of the sentence.
            # Therefore, we use instant function using 'nn.functional' package.
            # This is the beauty of PyTorch. :)
            cnn_out = nn.functional.max_pool1d(
                input=cnn_out.squeeze(-1),
                kernel_size=cnn_out.size(-2)
            ).squeeze(-1)
            # |cnn_out| = (batch_size, n_filter)
            cnn_outs += [cnn_out]
        # Merge output tensors from each convolution layer.
        cnn_outs = torch.cat(cnn_outs, dim=-1)
        # |cnn_outs| = (batch_size, sum(n_filters))
        y = self.activation(self.generator(cnn_outs))
        # |y| = (batch_size, n_classes)

        return y​

 

 

 

 

 

 

 

 

 


6. 쉬어가기) Multi-Label  Classification

Mutli-Label Classification: 기존 softmax 분류와 달리 여러 클래스가 동시에 정답이 될 수 있는것

6.1 Binary-Classification
sigmoid. &. BCELoss를 사용한다. (이진분류상황은 Bernoulli Distribution이기 때문)

수식은 아래와 같은데, BCE Loss는 이진분류에 특화된 기존 CE Loss의 한 종류이다.

이 수식에서 y는 0또는 1을 갖는 불연속적인 값이고
y_hat은 sigmoid를 통과한 0~1사이의 연속적인 출력값이다.

 

6.2 Multi-Binary Classification
그렇다면, Multi-Label문제에서 Binary Classification를 어떻게 적용할까?

n개의 항목을 갖는 분류에 대해 신경망의 마지막 계층에 n개의 노드를 주고, 모두 sigmoid함수를 적용한다.
즉, 하나의 모델로 여러 이진분류작업이 가능하다.
그렇다면 최종 손실함수는? 다음과 같다.

 

6.3 ETC
이진분류가 아닐 때는, sigmoid가 아닌 softmax를 사용하고, Loss도 Cross-Entropy로 바꾸면 된다.

 

 

 

 

 

 


마치며...

이번시간에는 text classification에 대해 다루었다.
text classification은 모델의 구조의 복잡도나 코드작성난도에 비해 활용도가 매우 높은 분야이다.
다만, 신경망사용이전, 불연속적값에 대한 희소성문제해결을 하지 못한 채,  
Naïve Bayes방식과 같이 매우 간단하고 직관적인 방법을 사용했다.
다만, Naïve Bayes방식은 "각 feature는 independent하다!"라는 강력한 가정으로인해 classification의 정확도가 떨어질 수 밖에 없었다.


하지만 딥러닝의 도입으로 매우 효율적이고 정확하게 text classification이 가능해졌는데,
RNN은 단어들을 순차적으로 받아 가장 마지막 time-step에서 classification을 예측하고
CNN은 classification에 중요한 단어들의 조합에 대한 패턴을 감지하기도 한다.

∙RNN의 경우, 문장전체의 맥락과 의미에 더 집중해 classification을 수행하며
∙CNN의 경우, 해당 클래스를 나타내는 단어조합에 대한 pattern의 유무를 가장 중시한다.

따라서 RNN과 CNN을 조합해 Ensemble Model로 구현한다면 더 좋은 결과를 얻을수도 있다.
이를 기반으로 다른 모델들을 참고한다면, 긴문장이나 어려운 텍스트에서도 더 높은 성능을 낼 수 있을 것이다.

📌 목차

1. preview
2. RNN

3. LSTM
4. GRU
5. Gradient  Clipping

😚 글을 마치며...

 

 

 


1. Preview

자연어 처리분야는 문장 내 단어들이 앞뒤 위치에 따라 서로 영향을 주고받는다.
따라서 단순히 y = f(x)같은 순서의 개념 없이 입력을 넣으면 출력이 나오는 함수의 형태가 아닌, 
순차적(sequential)입력으로 입력에 따른 모델의 hidden state가 순차적으로 변하며, 
상태에 따라 출력결과가 순차적으로 반환되는 함수가 필요하다.

이런 시간개념이나 순서정보를 사용해 입력을 학습하는 것을 sequential modeling이라 한다.
신경망으로는 RNN 등으로, 신경망 뿐만 아니라 HMM, CRFs(Hidden Markov Model이나 Conditional Random Fields)등의 다양한 방법으로 위의 문제에 접근할 수 있다.

 

 

 

 

 


2. RNN

2.1 Feed Forward

기본적인 RNN을 활용한 순전파 계산흐름을 알아보자.
다음 그림은 각 time-step별로 입력 xt와 이전 time-step ht가 RNN으로 들어가 출력 o를 반환한다.
이렇게 도출한 o들을 y_hat으로 삼아서 정답인 y와 비교 후, 손실 L을 계산한다.





이를 수식으로 표현하면 다음과 같다.

 

2.2 BPTT (Back Propagation Through Time)
순전파 이후 time-step의 RNN에 사용된 parameter θ는 모든 시간에 공유되어 사용된다.
따라서 앞서 구한 손실 L에 미분을 통해 역전파를 수행하면, 각 time-step별로 뒤로부터  θ의 기울기가 구해지고, 이전 time-step(t-1)θ의 기울기에 더해진다.
즉, t가 0에 가까워질수록 RNN의 parameter θ의 기울기는 각 time-step별 기울기가 더해져 점점 커진다.

아래 그림에서 좌측으로 갈수록 기울기가 더해져 점점커지는 속성을 갖는데,
이 속성을 '시간 축에 대해 수행되는 역전파 방법'이라는 뜻으로 BPTT라 한다.
이런 RNN 역전파의 속성으로 인해, RNN은 마치 time-step 수만큼 layer가 존재하는 것과 같은 상태가 되므로 time-step이 길어질수록 Deep RNN과 유사하게 동작한다.

 

2.3 Gradient Vanishing
앞서 설명했듯, BPTT로인해 RNN은 역전파 시 마치 time-step만큼의 계층이 있는것과 비슷한 속성을 갖는다.
하지만 앞의 RNN 수식을 보면 활성화함수로 tanh함수가 사용된다.
tanh의 양 끝 기울기가 -1과 1로 수렴(기울기는 0에 근접)한다.
따라서 tanh 양 끝의 값을 반환하는 층의 경우, 기울기가 0에 가까워진다.
이렇게되면 그 다음으로 미분값을 전달받은 층은 제대로된 미분값(기울기)을 전달받을 수 없게 된다.
y>0: sigmoid함수, -1~1: tanh함수

추가적으로 tanh와 sigmoid의 도함수는 모두 기울기 값이 1보다 작거나 같으므로
❗️층을 거칠수록 기울기의 크기는 작아질 수 밖에 없다!! → gradient vanishing


tanh'(0)=1 , sigmoid'(0)=0.5 값을 갖는다.
따라서 RNN같이 time-step이 많거나 여러층을 갖는 MLP의 경우, 이런 기울기소실문제가 쉽게 발생한다.
다만 MLP에서 ReLU와 Skip-Connection의 등장으로 너무 큰 문제가 되는 것은 아니다.

 

2.4 Deep RNN
기본적으로 time-step별 RNN이 동작한다.
다만, 아래처럼 하나의 time-step내에서 여러층의 RNN을 쌓을 수도 있다.
당연히 층별로 parameter θ를 공유하지 않고 따로 갖는다.

하나의 층만 갖는 기존 RNN의 경우, hidden state와 출력값이 같은 값이었다.
여러 층이 쌓여 이뤄진 RNN의 경우, 각 time-step의 RNN 전체 출력값은 맨 위층 hidden state가 된다.

출력텐서의 크기의 경우 다음과 같다.
 ∙ 단일 RNN: |h1:n| = (batch_size, n, hidden_size)
 ∙ Deep RNN: |ht| = (#layers, batch_size, hidden_size)

 

2.5 Bidirectional RNN
이제 RNN의 방향에 관해 이야기해보자.
양방양 RNN을 사용하면 기존 정방향에 역방향까지 추가되어
마지막 time-step에서부터 거꾸로 역방향(reverse direction)으로 입력받아 진행한다.
당연히 정방향과 역방향의 parameter θ는 공유되지 않는다.

결과적으로, output은 과거와 미래 모두에 의존할 수 있게 되는 것이다.


출력텐서의 크기의 경우 다음과 같다.
 ∙ |ht| = (#direction × #layers, batch_size, hidden_size)

 

2.6 NLP 적용사례

 

2.7 정리
NLP에서 거의 대부분의 입출력형태는 모두 불연속적인 값을 갖는다.
즉, regression보다는 classification에 가깝다.
따라서 Cross-Entropy Loss function을 사용해 신경망을 train한다.

이처럼 RNN은 가변길이의 입력을 받아 출력으로 가변길이를 반환하는 모델이다.
하지만 가장 기본적인 Vanilla-RNN은 time-step이 길어질수록 앞의 data기억이 어렵다.

 

 

 

 

 


3.  LSTM (Long Short Term Memory)

3.1 LSTM
 ∙ RNN은 가변길이의 sequential data형태 입력에 잘 작동하지만 그 길이가 길어질수록 앞서입력된 data를 까먹는 치명적인 단점이 존재한다. 이를 보완하고자 LSTM이 도입되었다. (여전히 긴 길이의 data에 대해 기억하지는 못함, 보완만 함)

LSTM은 기존 RNN의 은닉상태 이외에 별도의 cell state를 갖게하여 기억력을 증강한다.
추가적으로 여러 gate를 둬 forget, output 등을 효과적으로 제어한다.
그 결과, 긴 길이의 data에 대해서도 효율적으로 대처할 수 있게 되었다.
다만, 구조적으로 더욱 복잡해져서 더 많아진 parameter 학습을 위해 더 많은 data를 이용해 훈련해야한다.
아래는 LSTM의 수식이다.
각 gate의 sigmoid(σ)가 붙어 0~1값으로 gate를 얼마나 열고 닫을지를 결정한다.

 

3.2 LSTM의 역전파

 

 

 

 

 

 


4. GRU (Gated Recurrent Unit)

4.1 GRU
GRU는 LSTM의 간소화 버전으로 기존에 비해 더 간단하지만 성능이 비슷한 것이 특징이다.

σ로 구성된 rt(reset gate)와 zt(update gate)가 존재한다.
σ로 여전히 data의 흐름을 열고 닫아 제어할 수 있으며,
기존 LSTM대비 gate의 숫자는 줄고 따라서 gate에 딸려있는 parameter 수도 그만큼 줄어든다.
GRU의 수식은 아래와 같다.

GRU는 LSTM보다 몸집이 작긴하지만 LSTM이 현저히 사용빈도가 더 높은데, 
성능차이보다는 LSTM과 GRU의 학습률, hidden_size등의 hyper-parameter가 다르기에
사용모델에따라 parameter setting을 다시 찾아내야 한다.

 

 

 

 

 

 

 

 


5. Gradient Clipping

5.1 Gradient Clipping
RNN은 BPTT(Back Propagation Through Time)을 통해 시간역행으로 기울기를 구한다.
매 time step마다 RNN의 parameter에 기울기가 더해지므로 출력의 길이에따라 기울기크기가 달라진다.
즉, 길이가 길수록 자칫 기울기의 크기인 norm이 너무 커지는, gradient exploding문제가 야기될 수 있다.

❗️기울기의 크기가 너무 커질 때, 가장 쉬운 대처법: 학습률을 아주 작게 설정
다만, 훈련속도가 매우 느려질 수 있다는 단점이 존재하며 local optima에 빠질 수 있음
즉, 길이가 가변이기에 학습률을 매번 알맞게 최적의 값을 찾는 것은 무척 어렵기에
이때, Gradient Clipping이 큰 위력을 발휘한다.




Gradient Clipping은 parameter θ의 norm(보통 L2 norm)를 구하고
이 norm의 크기를 제한하는 방법이다.
즉, gradient vector는 유지, 크기를 학습에 영향주지 않는 만큼 줄이는 것이다.

수식을 보면, 기울기 norm이 정해진 최대값(threshold)보다 크다면 최대값보다 큰 만큼의 비율로 나눠준다.
결과적으로 항상 기울기는 threshold보다 작아지게 되며, 이는 학습의 발산을 방지하고 기울기의 방향자체를 바꾸지 않고 유지시켜 parameter θ가 학습해야하는 방향성을 잃지 않게 해준다.

즉, 손실함수를 최소화하기 위한 기울기의 방향은 유지하고, 크기만 조절하기에 학습률을 1과 같이 큰 값으로도 학습에 사용가능하다.
다만, Adam과 같은 동적 학습률을 갖는 optimizer는 사용할 필요성이 없고 SGD와 같은 경우 적용하는 편이 좋다.

아래와 같이 pytorch에서 Gradient Clipping기능을 사용할 수 있다.
import torch.optim as optim
import torch.nn.utils as torch_utils

learning_rate = 1.
max_norm = 5

optimizier = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=0.01)

# 기울기폭발을 피하기 위해 gradient clipping을 도입
torch_utils.clip_grad_norm_(model.parameters(), max_grad_norm)

optimizer.step()

 

 

 

 

 

 

 


마치며...

이번시간에는 RNN을 활용한 순서정보를 가진 순차데이터, 시계열데이터를 학습하는 방법을 익혔다.
기존의 신경망과 달리 RNN은 이전 time step의 자기자신을 참조해 현재 자신의 state를 결정한다.
따라서 time step마다 RNN의 신경망가중치는 공유되지만 기울기소실, 장기기억 등의 문제로 긴 순차데이터처리에 어려움을 겪는다.


LSTM과 GRU의 경우, 이런 RNN의 단점을 보완해 여러 gate를 열고 닫아(by sigmoid)
정보의 흐름을 조절함으로써 장기기억력에 더 나은 성능을 보여준다.


RNN의 역전파알고리즘인 BPTT는 시간에 대해서도 이뤄지는데,
time-step이 많은 data일수록 time-step별 기울기가 더해져 최종기울기가 커진다.
기울기가 클 때, 너무 큰 학습률을 사용하면 해당 학습은 발산할 가능성이 높다.

따라서 기울기가 정해진 임계치보다 커지지 않도록하는 Gradient Clipping을 통해 방향은 유지, 크기만 감소시켜 
학습률 1과 같은 매우 큰 값도 학습에 사용할 수 있게 해준다.

 

📌 목차

1. preview
2. Dimension Reduction

3. 흔한 오해 1
4. word2vec [2013]
5. GloVe (Global Vectors for word representation)
6. word2vec 예제 (FastText 오픈소스)

😚 글을 마치며...

 

 

 


1. Preview

 

[Gain Study_NLP]02. Similarity. &. Ambiguity (one-hot encoding, feature, TF-IDF)

📌 목차 1. word sense 2. one-hot encoding 3. thesaurus(어휘분류사전) 4. Feature 5. Feature Extraction. &. Text Mining (TF-IDF) 6. Feature vector 생성 7. Vector Similarity (with Norm) 8. 단어 중의성 해소 (WSD) 9. Selection Preference 😚

chan4im.tistory.com

앞서 단어의 의미와 모호성을 살펴보았는데, 사람의 언어는 불연속적형태인 단어로 이뤄지며, 각 단어가 갖는 의미는 서로 연관성이 있을때도 있지만 겉으로는 파악하기 어렵다.

이런 NLP의 특징때문에 NLP에서 단어∙문장∙문서를 vector로 나타내는 것은 매우 큰 숙명이었다.
(컴퓨터가 이해하고 처리하기는 어렵기 때문)
따라서 컴퓨터가 이해하는 벡터로 변환이 가능한 함수나 mapping table을 만드는 과정이 중요하다.

이전시간에는 단어의 의미를 다루며 corpus로부터 단어의 feature를 추출해 벡터로 만드는 과정을 살펴보았지만
이번시간에는 단어를 컴퓨터가 이해하고 처리하기 쉬운 형태로 변환하는 과정인, "word embedding"을 다뤄볼 것이다.

 

 

 

 

 

 


2. Dimension Reduction

이전 시간(https://chan4im.tistory.com/196)에서 높은차원에서 data를 표현하는 과정에서 희소성 문제가 많이 나타남을 확인했다.

따라서 같은정보 표현 시, 더 낮은차원을 사용하는 것이 중요 하다.

2.1 PCA
가장 대표적 차원축소방법인 주성분 분석(PCA)은 고차원데이터를 더 낮은차원으로 표현할 수 있다.
주로 특이값분해(SVD)를 통해 주성분분석이 가능하다.
위의 사진에서 다음과 같은 조건을 만족해야 한다.
파란점에서 빨간점으로 투영된 거리가 최소가 되어야 하고
빨간점들의 분산이 최대가 되어야 한다.

다만, 저차원으로 투영하게되면 정보의 손실이 불가피하게 발생하며, 이는 복구불가능하다.
따라서 고차원에 표현된 정보를 지나치게 낮은 차원으로 축소해 표현하기 어렵다.
특히, data가 비선형적으로 구성될수록 더욱 어려워진다.

 

2.2 Manifold Hypothesis
위의 가설로 차원축소에 더 효율적으로 접근해볼 수 있다.

매니폴드 가설: 고차원의 데이터의 경우, 해당 데이터들을 아우르는 저차원의 다양체(manifold)역시 존재 한다.
3차원 공간상의 2차원 manifold를 2차원 공간에 표현 시, 각 점사이 최단경로, 공간에 따라 최단경로가 바뀐다.
위와 같이 3차원 공간에 분포하는 data를 아우르는 2차원 manifold가 존재할 수도 있으며 이런 manifold를 찾아 2차원 평면에 data point들을 mapping할 수 있다.
따라서 manifold를 찾을 수만 있다면, PCA처럼 고차원평면에 data를 선형적으로 투사하며 생기는 손실을 최소화할 수 있다.

또다른 흥미로운 특징 하나는 위의 그림처럼 고차원상에서 가까운 거리의 point라 할 지라도
manifold보다 저차원으로 mapping하면 오히려 거리가 멀어질 수 있다.

또한, 저차원의 공간상에서 가까운 점끼리는 실제로도 가까운 특징을 갖는다는 것이다.

따라서 각 공간의 차원축은 고차원에서 비선형적으로 표현될 것이며, 데이터의 특징을 각각 표현할 것이다.

 

2.3 딥러닝이 잘 작동하는 이유
대부분, 딥러닝이 문제를 풀기 위해 차원축소를 수행하는 과정이
바로 데이터가 존재하는 고차원상에서 manifold를 찾는 과정이다. (다만 가설임.)

 

2.4 Auto Encoder
 ∙ 고차원의 sample_data를 입력으로 받음
 ∙ 입력을 토대로 manifold를 찾고, 저차원으로 축소하는 encoder를 거침
 ∙ encoder를 거친 후 bottel-neck구간에서의 hidden 벡터로 표현한다.
 ∙ decoder는 자차원벡터를 받아 고차원 데이터로 복원하는 작업을 수행한다.
    복원된 데이터는 고차원상 manifold위에 위치하게 된다.

이때, 고차원의 data를 저차원으로 압축 후 복원하는 과정에서 AE의 bottle-neck차원은 매우 낮기에
복원에 필요한 정보만 남기고 필요없는 정보부터 버려진다.

이전시간(https://chan4im.tistory.com/196)에서의 TF-IDF등을 활용, 계산한 희소단어특징벡터를 입력으로 넣고
같은 출력값을 갖도록 훈련 시, AE의 bottle-neck층 결과값을 dense word embedding vector로 사용할 수 있다.

 

 

 

 

 

 

 


3.  흔한 오해 1.

Skip-Gram, GloVe를 사용해 훈련한 단어 임베딩 벡터가 딥러닝 모델입력으로 사용된다?

No! 이 임베딩 벡터는 pre-train된 임베딩 벡터로 word2vec을 통해 얻은 단어임베딩벡터가 훌륭하게 단어의 특징을 잘 반영은 하지만 모델훈련을 통한 문제해결에 대한 최적의 벡터임베딩은 아니다.

따라서 다른 목적함수를 통해 훈련한 임베딩 벡터는 기존목적에 맞지않을 가능성이 높기에 문제의 특징을 고려하지 않은 단어임베딩벡터는 그다지 좋은방법이 될 수 없다.

 

 

 

 

 

 


4. word2vec [2013]

4.1 word2vec
word2vec은 단어를 임베딩하는 2가지 알고리즘 CBOW와 Skip-Gram을 제시한다.
두 방법 모두 함께 등장하는 단어가 비슷할수록 비슷한 벡터값을 가질 것이라는 공통된 가정을 전제로 진행한다.

앞에서처럼 두 방법 모두 window_size가 주어지면 특정 단어를 기준으로 window 내의 주변 단어들을 사용, word embedding을 학습한다.
다만, window내에서의 위치는 고려하지 않지만 그렇다고 위치정보를 무시하지는 않는다.
이는 window 자체에 단어의 위치정보를 내포하기에 가능한 일이다.

 

4.2 CBOW .&. Skip-Gram 
📌 CBOW (Continuous Bag Of Words)
  - 신경망 주변에 나타나는 단어들을 one-hot encoding된 벡터로 입력받음
  - 입력을 토대로 해당 단어를 예측

📌 Skip-Gram 
  - 대상 단어를 one-hot encoding벡터로 입력을 받음
  - 입력을 토대로 주변단어들을 예측하는 신경망을 구성, word embedding vector를 학습
  - 보통 CBOW보다 성능이 뛰어나 더 널리 사용됨

 

4.3 상세 (Skip-Gram) 훈련방식
① MLE를 통해 아래 수식의 argmax내의 수식을 최대화하는 파라미터 θ를 찾음.
② 이를 통해 wt가 주어지면, 앞뒤 n개의 단어(wt-(\frac{n}{2}), ... ,wt+(\frac{n}{2}))를 예측하도록 훈련. (window_size=n)
③ one-hot vector를 dense vector인 word embedding vector로 변환


∙ 위의 수식을 그림으로 표현하면 아래와 같다.
수식에서 볼 수 있듯, 매우 간단한 구조로 softmax계층이 출력층이다.

∙ 흥미로운 점은 신경망 내부에 sigmoid같은 비선형함수가 없다는 것이다.

 

 

 


5. GloVe (Global Vectors for word representation)

5.1 알고리즘
 ∙ word2vec과 같은 단어임베딩방식 중 하나인 GloVe는 "대상 단어에 대해 corpus에 함께 나타난 단어별 출현빈도를 예측"한다.
 ∙ GloVe 알고리즘의 parameter를 구하는 수식은 아래와 같다.

Skip-Gram을 위한 신경망과 거의 유사한 형태인데,다만 이는 classification문제가 아닌
"출현빈도를 근사하는 regression"문제이기에 MSE를 사용한다.


∙마찬가지로 원핫벡터 x를 입력으로 1개의 은닉층 W를 거쳐 출력층 W'을 통해 출력벡터를 반환하는데, 
이 출력벡터는 단어 x와 함께 corpus에 출현한 모든 단어의 동시출현빈도들을 나타낸 벡터 Cx를 근사해야한다.
따라서 이 둘의 차이값인 손실을 최소화하도록 역전파 및 SGD를 통해 학습할 수 있다.

∙ 이때, 단어 x 자체의 출현빈도(= prior probability; 사전확률)에 따라 MSE Loss값이 매우 달라진다.
❗️ Cx값 자체가 클수록 손실값이 매우 커질것이기 때문!!
 ∴ f(x)는 단어의 빈도에 따라 아래와 같이 손실함수에 가중치를 부여한다.

이와 관련해 GloVe논문[https://aclanthology.org/D14-1162.pdf]에서는 threshold=100, a=3/4일 때, 최적의 결과라 명시

 

5.2 Skip-Gram. vs. GloVe
∙ GloVe는 처음에 corpus를 통해 단어별 동시출현빈도를 조사,
그에대한 출현빈도행렬을 만들고 이후 해당 행렬을 통해 동시출현빈도를 근사한다.
따라서 corpus 전체를 훑으며 대상단어와 주변 단어를 가져와 학습하는 과정을 반복하는 skip-gram보다 훨씬 학습이 빠르다.

또한, skip-gram은 corpus를 훑으며(window특성) 학습하기에
사전확률이 낮은(= 출현빈도자체가 적은) 단어에 대해서는 학습기회가 부실하다.
결과적으로 skip-gram은 비교적 부정확한 단어임베딩벡터를 학습하게 된다.

하지만 GloVe의 경우, skip-gram에 비해 이런 단점이 어느정도 보완된다.

 

5.3 정리
GloVe가 가장 뛰어난 단어임베딩방식임을 주장했지만,
skip-gram의 window_size, learning_rate, epoch수 등 hyper-parameter tuning에 따라 성능적으로 큰 차이가 없기에
적절한 방식을 시스템 구성의 제약에 따라 선택하는 것이 좋다.

 

 

 

 

 

 

 

 


6. word2vec 예제 (FastText 오픈소스)

6.1 FastText를 활용한 word embedding 학습

 

6.2 시각화
위의 방법으로 얻은 벡터들을 시각화 툴로 시각화하면 훈련결과에 대해 쉽게 살펴볼 수 있다.
아쉽게도 pytorch는 자체시각화툴을 제공하지 않기에 tensorflow의 tensorboard를 활용하면 좋은 시각화가 가능하다.

pytorch 1.1 version부터 tensorboard를 공식적으로 지원하기에 사용가능!!

 

 

 

 

 


마치며...

이전시간에는 특징벡터를 TF-IDF방식으로 구성해 사용했지만, "희소성"문제등으로
이번시간, 차원축소에 대해 살펴보았다.

기존의 선형적 차원축소방법에 비해 Neural Network는 비선형적 차원축소를 통한 특징추출이 효과적이기에 딥러닝이 기존 머신러닝보다 월등한 성과를 내는 것을 알 수 있었다.


하지만 이런 딥러닝의 비선형적 차원축소는 Cost가 높고 최적화가 어렵다.
이런 점에서 word2vec은 비선형적방법을 사용하지 않고도 매우 좋은 단어임베딩구현이 가능하며
GloVe는 더 빠르고 정확한 단어임베딩방법을 제안하였다.

위의 방법들을 사용하여 단어간 유사도를 data기반으로 효과적이면서 정확하게 계산할 수 있게 되었다.


하지만 앞으로 소개할 text classification, Language generation과 같은 기법에서는 
오늘 설명한 word embedding algorithm보다 단순히 embedding 계층을 사용하는 편이 더 정확하고 효율적이다.

이제, 문장단위로 자연어를 처리하는 방법들에 대해 본격적으로 알아보자!!

 

📌 목차

1. word sense
2. one-hot encoding

3. thesaurus(어휘분류사전)
4. Feature
5. Feature Extraction. &. Text Mining (TF-IDF)
6. Feature vector 생성
7. Vector Similarity (with Norm)
8. 단어 중의성 해소 (WSD)
9. Selection Preference

😚 글을 마치며...

 

 

 


1. Word Sense

1.1 단어와 의미의 관계성
NLP에서 가장 기초가 되는 근간이자 가장 어려운 문제인 단어의 의미(word sense)와 의미의 유사성, 모호성에 대해 알아보자.
단어는 겉으로 보이는 형태인 표제어(lemma)안에 여러 의미를 담고있어서 상황에 따라 다른 의미로 사용된다.
이때, 주변정보(context)에 따라 숨겨진 의미를 파악하고 이해해야하지만
context가 부족하면 ambiguity가 증가할 수 있다. ( ∵ 과거기억 등의 이유)

즉, 한가지 형태의 단어에 여러 의미가 포함되어 생기는 '중의성' 문제는 NLP에서 매우 중요한 위치를 갖는데, 특히 기계번역에서는 단어의 의미에 따라 해당 번역 단어의 형태가 완전히 바뀌기에 매우 중요하다.
즉, lemma(표제어)를 매개체 삼아 내부 latent space의 'word sense'로 변환해 사용해야한다.

 

1.2
동형어∙다의어

 동형어: 형태는 같으나 뜻이 서로 다른 단어 (ex. 차 - tea / car)
 다의어: 동형어개념 + 그 의미들이 서로 관련이 있는 뜻 (ex. 다리 - leg / desk leg)

이때, 한 형태내 여러 의미를 갖는 동형어∙다의어의 경우, 단어 중의성해소(WSD)라는 방법을 통해 단어의 의미를 더 명확히 하는 과정이 필요하다.
단어의 의미를 더 명확히 하기 위해 주변문맥을 통해 원래단어의미를 파악해야하는데, 이때 end-to-end 방법이 DNN에서 선호된다.
이로인해 단어 중의성 해소에 대한 필요도가 낮아졌지만 아직 ambiguity는 문제해결이 어려운 경우가 많다.
동의어
 동의어: 다른 형태의 단어간에 의미가 같은 단어 (ex. home, place)

물론, 의미가 완전히 딱 떨어지지는 않고 똑같지 않을 수도 있지만 일종의 동의(consensus)가 존재하며, 이 동의어가 여러개 있을 때, 이를 동의어 집합(synset)이라 한다.
상위어∙하위어
단어는 하나의 추상적 개념을 가지며 이때, 그 개념들을 포함하는 상∙하위 개념이 존재하며 이에 해당하는 단어들을 상위어(hypemym), 하위어(hyponym)이라 한다. (ex. 동물-포유류, 포유류-코끼리)

이런 단어들의 어휘분류(taxonomy)에 따라 단어간 관계구조를 계층화 할 수 있다.

 

1.3 모호성 해소 (WSD)
컴퓨터는 오직 text만 가지므로 text가 내포한 진짜 의미를 파악하는 과정이 필요하다.
즉, 단어의 겉 형태인 text만으로는 모호성(ambiguity)이 높기에 모호성을 제거하는 과정인, WSD(단어 중의성 해소)으로 NLP의 성능을 높일 수 있다.

 

 

 

 

 


2. One-Hot Encoding

2.1 One-Hot Encoding
단어를 컴퓨터가 인지할 수 있는 수치로 바꾸는 가장 간단한 방법은 벡터로 표현하는 것으로 가장 기본적인 방법 중 하나는 one-hot encoding이라는 방식이다.
말 그대로 단 하나의 1과 나머지 수많은 0들로 표현된 encoding방식으로 one-hot encoding vector의 차원은 보통 전체 vocabulary의 개수가 된다. (보통 그 숫자는 매우 큰 수가 됨; 보통 30,000~100,000)

단어는 연속적인 심볼로써 이산확률변수로 나타낸다.
위와 같이 사전(dictionary)내의 각 단어를 one-hot encoding 방식을 통해 vector로 나타낼 수 있는데, 이 표현방식은 여러 문제점이 존재한다.
 Prob 1. vector space가 너무 커졌다. (하나만 1, 나머지는 0;  이때 0으로 채워진 vector를 sparse vector라 한다.)
 Prob 2. sparse vector의 가장 큰 문제점은 vector간 연산 시 결과값이 0이 되는, orthogonal하는 경우가 많아진다.
  - 즉, 다시 말하면 '강아지', '개'라는 단어는 상호유사하지만 이 둘의 연산 시 둘의 유사도가 0이 되어버릴 것이고, 일반화에 어려움을 겪을 수 있다.


  - 이는 Curse of Dimensionality와 연관되는데, 차원이 높아질수록 정보를 표현하는 각 점(vector)가 매우 낮은 밀도로 sparse하게 퍼져있게 된다. 따라서 차원의 저주로부터 벗어나고자 차원의 축소하여 단어를 표현할 필요성이 대두된다.

 

 

 

 

 

 

 


3. Thesaurus (어휘분류사전)

3.1 WordNet
Thesaurus(어휘분류사전)는 계층적 구조를 갖는 단어의미를 잘 분석∙분류해 구축된 데이터베이스로 WordNet은 가장 대표적인 시소러스의 일종이다.


WordNet은 동의어집합(Synset), 상위어, 하위어 정보가 특히 유향비순환그래프(DAG)로 잘 구축되어있다는 장점이 있다.
(트리구조가 아님: 하나의 노드가 여러 상위노드를 가질 수 있기 때문.)

WordNet은 워드넷 웹사이트에서 바로 이용할 수도 있다. (http://wordnetweb.princeton.edu/perl/webwn)
아래 사진을 보면 명사일 때 의미 10개, 동사일 때 의미 8개를 정의하였으며 명사 bank#2의 경우, 여러 다른표현(depository financial institution#1, banking concern#1)들도 같이 게시되어있다. (이것들이 바로 동의어 집합)
이처럼 wordnet은 단어별 여러 가능한 의미를 미리 정의하고 번호를 매기고, 동의어를 링크해 동의어 집합을 제공한다.
이는 WSD에 매우 좋은 label data가 되며 wordnet이 제공하는 이 data들을 바탕으로 supervised learning을 진행하면 단어중의성해소(WSD)문제를 풀 수 있다.

cf) 한국어 wordnet도 존재
 ∙ KorLex(http://korlex.pusan.ac.kr/)
 ∙ KWN(http://wordnet.kaist.ac.kr/)

 

3.2 WordNet을 활용한 단어간 유사도 비교
추가적으로 NLTK에 wrapping되어 포함되므로 import하여 사용가능하다.
from nltk.corpus import wordnet as wn

def hypernyms(word):
    current_node = wn.synsets(word)[0]
    yield current_node
    
    while True:
        try:
            current_node = current_node.hypernyms()[0]
            yield current_node
        except IndexError:
            break​

 

위의 코드는 wordnet에서 특정 단어의 최상위 부모노드까지의 경로를 구할 수 있고,
추가적으로 단어마다 나오는 숫자로 각 노드 간 거리를 알 수 있다. 
예를 들어, 위의 경우, 'student'와 'fireman' 사이의 거리는 5 임을 알 수 있다.

이를 통해 각 최하단 노드간의 최단거리를 알 수 있고, 이를 유사도로 치환해 활용할 수 있다.
이를 이용해 공식을 적용해보면 아래와 같다.

한계점: 사전을 구축하는데 비용과 시간이 많이 소요되고 상하위어가 잘 반영된 사전이어야만 해서 
사전에 기반한 유사도 구하기 방식은 정확성은 높으나 한계가 뚜렷하다는 단점이 존재.

 



 


4. Feature

4.1 Feature
지금까지는 one-hot vector를 통해 단어를 표현했을 때, 많은 문제가 발생한다고 설명하였다.
이는 one-hot vector의 표현방식이 효과적이지 않기 때문이다.

효과적인 정보 추출 및 학습을 위해서는 대상의 특징(feature)을 잘 표현해야한다.
이런 특징은 수치로 표현되며, 최대한 많은 samples를 설명할 수 있어야하기에 samples는 수치가 서로 다른 공통된 특징을 갖고 다양하게 표현되는것이 좋다.
즉, 각 sample의 feature마다 수치를 갖게하여 이를 모아 벡터로 표현한 것feature vector라 한다.
4.2 단어의 feature vector 구하기 위한 가정
① 의미가 비슷한 단어 → 쓰임새가 비슷할 것
② 쓰임새가 비슷 → 비슷한 문장안에서 비슷한 역할로 사용될 것
③ ∴ 함께 나타나는 단어들이 유사할 것

 

 

 


5. Feature Extraction. &. Text Mining (TF-IDF)

특징벡터를 만들기 앞서 text mining에서 중요하게 사용되는 TF-IDF를 사용해 특징을 추출하는 방법을 알아보자.

 

5.1 TF-IDF (Term Frequency-Inverse Document Frequency)

TF-IDF: 출현빈도를 사용어떤단어 w가 문서 d 내에서 얼마나 중요한지 나타내는 수치이다.
즉, TF-IDF의 값이 높을수록 w는 d를 대표하는 성질을 가진다.
TF: 단어의 문서내에 출현한 횟수
IDF: 그 단어가 출현한 문서의 숫자(DF)의 역수

예를 들어, 'the'의 경우, TF값이 매우 클 것이다.
하지만, 'the'가 중요한 경우는 거의 없을 것이기에 이를 위해 IDF가 필요하다.
(IDF를 사용함으로써 'the'와 같은 단어들에 penalty를 준다.)

∴ 최종적으로 얻게되는 숫자는 "다른 문서들에서는 잘 나타나지 않지만 특정 문서에서만 잘 나타나는 경우 큰 값을 가지며, 특정 문서에서 얼마나 중요한 역할을 차지하는지 나타내는 수치가 될 수 있다."

 

5.2 TF-IDF 구현예제
 ∙ 3개의 논문스크립트가 담긴 문서들이 doc1, doc2, doc3 변수에 들어있다 가정하자.

📌 TF 함수
def get_term_frequency(document, word_dict=None):
    if word_dict is None:
        word_dict = {}
    words = document.split()

    for w in words:
        word_dict[w] = 1 + (0 if word_dict.get(w) is None else word_dict[w])

    return pd.Series(word_dict).sort_values(ascending=False)​



📌 DF 함수
def get_document_frequency(documents):
    dicts = []
    vocab = set([])
    df = {}

    for d in documents:
        tf = get_term_frequency(d)
        dicts += [tf]
        vocab = vocab | set(tf.keys())
    
    for v in list(vocab):
        df[v] = 0
        for dict_d in dicts:
            if dict_d.get(v) is not None:
                df[v] += 1

    return pd.Series(df).sort_values(ascending=False)​



📌 TF-IDF 함수
def get_tfidf(docs):
    vocab = {}
    tfs = []
    for d in docs:
        vocab = get_term_frequency(d, vocab)
        tfs += [get_term_frequency(d)]
    df = get_document_frequency(docs)

    from operator import itemgetter
    import numpy as np

    stats = []
    for word, freq in vocab.items():
        tfidfs = []
        for idx in range(len(docs)):
            if tfs[idx].get(word) is not None:
                tfidfs += [tfs[idx][word] * np.log(len(docs) / df[word])]
            else:
                tfidfs += [0]

        stats.append((word, freq, *tfidfs, max(tfidfs)))

    return pd.DataFrame(stats, columns=('word',
                                        'frequency',
                                        'doc1',
                                        'doc2',
                                        'doc3',
                                        'max')).sort_values('max', ascending=False)

get_tfidf([doc1, doc2, doc3])​

<출력>

 

 

 

 

 


6. Feature Vector 생성

6.1 TF 행렬 만들기
TF 또한 좋은 특징이 되는데, 출현한 횟수가 차원별로 구성되면 하나의 특징벡터를 이룰 수 있다.
(물론 문서별 TF-IDF자체를 사용하는것도 좋음.)

def get_tf(docs):
    vocab = {}
    tfs = []
    for d in docs:
        vocab = get_term_frequency(d, vocab)
        tfs += [get_term_frequency(d)]

    from operator import itemgetter
    import numpy as np

    stats = []
    for word, freq in vocab.items():
        tf_v = []
        for idx in range(len(docs)):
            if tfs[idx].get(word) is not None:
                tf_v += [tfs[idx][word]]
            else:
                tf_v += [0]
        stats.append((word, freq, *tf_v))
    
    return pd.DataFrame(stats, columns=('word',
                                        'frequency',
                                        'doc1',
                                        'doc2',
                                        'doc3')).sort_values('frequency', ascending=False)

get_tf([doc1, doc2, doc3])​

<출력>
이때, 각 열들은 아래와 같다.
frequency: TF(w)
doc1: TF(w, d1)
doc2: TF(w, d2)
doc3: TF(w, d3)


TF(w, d1), TF(w, d2), TF(w, d3)는 문서에 대한 단어별 출현횟수를 활용한 특징벡터가 될 것이다.
예를들어 '여러분'은 [5, 6, 1]이라는 특징벡터를 갖는다.
문서가 많다면 지금보다 더 나은 특징벡터를 구할 수 있다.

다만, 문서가 지나치게 많으면 벡터의 차원 역시 지나치게 커질 수 있다.
문제가 되는 이유: 지나치게 커진 차원의 벡터 대부분의 값이 0으로 채워진다는 것.

위의 표에서 3개의 문서 중 일부만 출현해 TF가 0인 경우가 상당히 존재한다.
이렇게 벡터의 극히 일부분만 의미있는 값으로 채워진 벡터를 희소벡터라 한다.
희소벡터의 각 차원은 사실 대부분의 경우 0이기에 유의미한 특정통계를 얻기에 방지턱이 될 수 있다.
즉, 단순히 문서출현횟수로만 특징벡터를 구성하게되기에 많은 정보가 소실되었다.
( ∵ 매우 단순화되었기에 아주 정확한 특징벡터를 구성했다기엔 여전히 무리가 있다.)

 

6.2 context window 함께 출현한 단어들의 정보 활용하기
앞선 TF로 특징벡터를 구성한 방식보다 더 정확하다.

context window: 함께 나타나는 단어들을 조사하기 위해 windowing을 실행하여 그 안에 있는 unit들의 정보를 취합하는 방법에서 사용되는 window를 context window라 한다.
context window는 window_size라는 하나의 hyper-parameter가 추가되기에 사용자가 그 값을 지정해줄 수 있다.
 ∙ window_size가 지나치게 클 때: 현재 단어와 관계없는 단어들까지 TF를 count
 ∙ window_size가 지나치게 작을 때: 관계가 있는 단어들의 TF를 count


[문장들을 입력으로 받아 주어진 window_size내에 함께 출현한 단어들의 빈도를 세는 함수]
from collections import defaultdict

import pandas as pd

def get_context_counts(lines, w_size=2):
    co_dict = defaultdict(int)
    
    for line in lines:
        words = line.split()
        
        for i, w in enumerate(words):
            for c in words[i - w_size:i + w_size]:
                if w != c:
                    co_dict[(w, c)] += 1
            
    return pd.Series(co_dict)​

 

[TF-IDF를 위해 작성한 get_term_frequency()함수를 활용해 동시발생정보를 통해 벡터를 만드는 코드]
def co_occurrence(co_dict, vocab):
    data = []
    
    for word1 in vocab:
        row = []
        
        for word2 in vocab:
            try:
                count = co_dict[(word1, word2)]
            except KeyError:
                count = 0
            row.append(count)
            
        data.append(row)
    
    return pd.DataFrame(data, index=vocab, columns=vocab)​




<출력>

앞쪽의 출현빈도가 높은 단어들은 대부분의 값이 잘 채워져 있는 것을 알 수 있다.
뒤쪽의 출현빈도가 낮은 단어들은 대부분의 값이 0으로 채워져 있음을 알 수 있다. 

 

 

 

 

 

 


7. Vector Similarity (with Norm)

앞서 구한 특징벡터를 어떻게 사용할 수 있을까? 특징벡터는 단어간의 유사도를 구할 때 아주 유용하다.

가장 먼저 언급한 WordNet의 그래프구조에서 단어사이의 거리를 측정, 이를 바탕으로 단어사이의 유사도를 구하는 방법에 대해 알아보다.

 

이번에는 벡터간의 유사도 or 거리를 구하는 방법들을 다뤄볼 것이다.

 

 

 

7.1  L1 Distance (Manhattan Distance)
L1 Distance는 L1 norm을 사용하는 것으로 Manhattan Distance라고도 한다.
이 방법은 두 벡터의 각 차원별 값의 차이의 절대값을 모두 합한 값이다.

코드로 나타내면 아래와 같다.
def L1_distance(x1, x2):
    return ((x1-x2).abs()).sum()​

초록색선(L2 Distance)을 제외하고 나머지는 L1 Distance이다.

7.2  L2 Distance (Euclidean Distance)
L2 Distance란 L2 norm을 사용하는 것으로 Euclidean Distance라고도 한다.
이 방법은 두 벡터의 각 차원별 값 차이의 제곱의 합에 루트를 취한 값이다.



코드로 나타내면 아래와 같다.
def L2_distance(x1, x2):
    return ((x1 - x2)**2).sum()**.5​

 

7.3 Infinity Norm
Lnorm을 사용한 infinity distance는 차원별 값의 차이중 가장 큰 값을 나타낸다.

코드로 나타내면 아래와 같다.
def infinity_distance(x1, x2):
    return ((x1 - x2).abs()).max()​​

 

 

 

 

 

7.4 Cosine Similarity
코사인 유사도함수는 두 벡터사이의 방향과 크기를 모두 고려하는 방법이다.
특히, 자연어처리에서 가장 널리 쓰이는 유사도측정방법 이다.

∙ 수식에서 분자는 두 벡터간의 요소곱(element-wise)을 사용한다.(= 내적곱)
  - 코사인 유사도의 결과가 1에 가까울수록 방향은 일치하고
  - 코사인 유사도의 결과가 0에 가까울수록 직교이며
  - 코사인 유사도의 결과가 -1에 가까울수록 방향은 반대이다.


코드로 나타내면 아래와 같다.
def cosine_similarity(x1, x2):
    return (x1 * x2).sum() / ((x1**2).sum()**.5 * (x2**2).sum()**.5)​


[유의할 점]
다만, 분자의 벡터곱연산이나 분자의 L2 norm 연산은 cost가 높아 벡터차원이 커질수록 연산량이 커진다.
이는 희소벡터일 경우, 가장 큰 문제가 발생하는데, 해당차원이 직교하면 곱의 값이 0이되므로 정확한 유사도 및 거리를 반영하지 못한다.

 

7.5 Jarccard Similarity
jarccard similarity는 두 집합간의 유사도를 구하는 방법이다.


∙ 수식에서 분자는 두 집합의 교집합크기이고, 분모는 두 집합의 합집합니다.
  - 이때, 특징벡터의 각 차원이 집합의 요소(element)이다.
  - 다만, 각 차원에서의 값이 0 or 0이 아닌 값이 아닌, 수치 자체에 대해 jarccard similarity를 구하고자 할 때는, 2번째 줄의 수식처럼 두 벡터의 각 차원의 숫자에 대해 min, max연산으로 계산할 수 있다.

코드로 나타내면 아래와 같다.
def get_jaccard_similarity(x1, x2):
    return torch.stack([x1, x2]).min(dim=0)[0].sum() / torch.stack([x1, x2]).max(dim=0)[0].sum()​

 

7.6 문서 간 유사도 구하기
문서 = 문장의 집합 ; 문장 = 단어들의 집합
앞서 설명한 7절의 내용들은 단어에 대한 특징을 수집, 유사도를 구하는 방법이다.

따라서 문서에 대한 특징을 추출하여 문서간의 유사도를 구할 수 있는데, 
예를들어, 문서 내의 단어들에 대한 TF나 TF-IDF를 구해 벡터를 구성하고, 이를 활용해 벡터간의 유사도를 구할 수 있다.

물론, 현재 딥러닝에서는 훨씬 더 정확한 방법을 통해 문서간 또는 문장간 유사도를 구할 수 있다.

 

 

 

 

 

 


8. 단어 중의성 해소 (WSD)

8.1 Lesk Algorithm (Thesaurus 기반 WSD)
Lesk 알고리즘은 가장 간단한 사전 기반 중의성해소방법이다.
주어진 문장에서 특정단어의 의미를 명확히 할 때 사용할 수 있다.

∙ Lesk 알고리즘의 가정: 문장내 같이 등장하는 단어들은 공통 토픽을 공유
Lesk Algorithm

∙ 중의성을 해소하고자하는 단어에 대해 사전(wordnet)의 의미별 설명을 구함 

∙ 주어진 문장 내 등장단어의 사전에서 의미별 설명 사이 유사도를 구한다.
  주로 유사도를 구할 때, 단어의 개수를 구한다.
  
∙ 문장 내 단어들의 의미별 설명과 가장 유사도가 높은 의미를 선택한다.​



코드로 나타내면 아래와 같다.

def lesk(sentence, word):
    from nltk.wsd import lesk

    best_synset = lesk(sentence.split(), word)
    print(best_synset, best_synset.definition())





<예제>

In [26] 전까지는 잘 작동하지만, In [26]은 전혀 다른 의미로 예측하는 것을 볼 수 있다.
Lesk Algorithm은 명확한 장단점을 갖는다.
 - 장점) WordNet과 같이 잘 분류된 사전이 있다면, 쉽고 빠르게 WSD(단어 중의성 해소)문제를 해결할 수 있다.
 - 단점) 사전의 단어 및 의미에 관한 설명에 크게 의존하게되고, 설명이 부실하거나 주어진 문장에 큰 특징이 없다면 WSD능력이 크게 떨어진다.

 

 

 

 

 

 


9. Selection Preference

선택선호도(Selection Preference): 문장은 여러 단어의 시퀀스로 이뤄지는데, 문장 내 주변 단어들에 따라 의미가 정해지며 이를 더 수치화해 나타내는 방법이다. 이를 통해 WSD문제를 해결할 수 있다.

ex) '마시다'라는 동사에 대한 목적어로는 '음료'클래스에 속하는 단어가 올 가능성이 높기에 '차'라는 단어가 '음료'일지 '자가용'일지 어디에 속할 지 쉽게 알 수 있다.

 

9.1 Selection Preference Strength
선택선호도는 단어간 관계가 좀 더 특별한 경우를 수치화해 나타내는데
단어간 분포의 차이가 클수록 더 강력한 선택선호도를 갖는다
(= 선택 선호도 강도(strength)가 강하다).

이를 KLD(KL-Divergence)를 사용해 정의하였다.
선택선호도 강도 SR(w)은 w가 주어졌을 때, 목표클래스 C의 분포 P(C|w)와 그냥 해당 클래스들의 사전분포 P(C)와의 KLD로 정의되어 있음을 알 수 있다. (즉, 특정 클래스를 얼마나 선택적으로 선호하는지를 알 수 있다.)

 

9.2 Selectional Association
술어와 특정클래스사이 선택관련도를 살펴보자.
위의 수식에 따르면, 선택선호도강도가 낮은 술어에 대해 분자의 값이 클 경우, 술어와 클래스사이 더 큰 선택관련도를 갖는다는 것을 의미한다.
즉, 선택선호도강도가 낮아 해당 술어는 클래스에 대한 선택적선호강도가 낮지만,
특정클래스만 유독 술어에 영향을 받아 분자가 커져 선택관련도의 수치가 커질 수 있음을 내포한다.

 

9.3 Selection Preference. &. WSD
이런 선택선호도의 특징을 이용하면 WSD에 활용할 수 있다.
ex)  '마시다'라는 동사에 '치'라는 목적어가 함께 있을 때, '음료'클래스인지 '자가용'클래스인지만 알아내면 된다.

우리가 아는 corpus들은 단어로 표현되어 있을 뿐, 클래스로 표현되어 있지 않기에 
이를 위해 사전에 정의된 지식이나 dataset이 필요하다.
9.4 WordNet기반 Selection Preference
여기서 WordNet이 큰 위력을 발휘한다.
WordNet을 이용하면 '차(car)'의 상위어를 알 수 있고 이를 클래스로하여 필요정보를 얻을 수 있다.
술어와 클래스사이 확률분포를 정의하는 출현빈도의 계산수식을 아래와 같이 정의할 수 있다.

클래스 c에 속하는 표제어(headword), 즉 h는 실제 corpus가 나타난 단어로 술어(predicate) w와 함께 출현한 h의 빈도를 세고, h가 속하는 클래스들의 집합의 크기인 |Classes(h)|로 나누어준다.
그리고 이를 클래스 c에 속하는 모든 표제어에 대해 수행한 후 이를 합한 값을 CountR(w,c)를 근사한다.

이를 통해 술어 w와 표제어 h가 주어졌을 때 h의 클래스 c를 추정한 c_hat을 구할 수 있다.

 

9.5 pseudo word를 통한 Selection Preference평가
유사어휘(pseudo word)가 바로 정교한 testset설계를 위한 하나의 해답이 될 수 있다.
유사어휘는 2개의 단어가 인위적으로 합성되어 만들어진 단어이다.

 

9.6 Similarity Based Method[2007]
WordNet은 모든 언어에 존재하지 않고, 새롭게 생겨나는 신조어들도 반영되지 않을 가능성이 높다.
따라서 wordnet과 thesaurus에 의존하지 않고 선택선호도를 구할 수 있다면??

이를 위해 thesaurus나 thesaurus기반 Lesk알고리즘에 의존하지 않고 data를 기반으로 간단하게 선택선호도를 구하는 방법을 알아보자.
술어 w, 표제어 h, 두 단어 사이의 관계 R이 tuple로 주어질 때, 선택관련도를 아래와 같이 정의할 수 있다.

이때, 가중치 ØR(w,h)는 동일하게 1이나 아래처럼 IDF를 사용해 정의할 수도 있다.

또한 sim함수는 일전의 코사인유사도나 jarccard유사도를 포함, 다양한 유사도함수를 사용할 수 있다.
유사도비교를 위해 SeenR(w)함수로 대상단어를 선정하기에 corpus에 따라 유사도를 구할 수 있는 대상이 달라지며 이를 통해 thesaurus없이도 쉽게 선택선호도를 계산할 수 있게 된다.

 

유사도기반 선택선호도 예제

from konlpy.tag import Kkma

with open('ted.aligned.ko.refined.tok.random-10k.txt') as f:
    lines = [l.strip() for l in f.read().splitlines() if l.strip()]

def count_seen_headwords(lines, predicate='VV', headword='NNG'):
    tagger = Kkma()
    seen_dict = {}
    
    for line in lines:
        pos_result = tagger.pos(line)
        
        word_h = None
        word_p = None
        for word, pos in pos_result:
            if pos == predicate or pos[:3] == predicate + '+':
                word_p = word
                break
            if pos == headword:
                word_h = word
        
        if word_h is not None and word_p is not None:
            seen_dict[word_p] = [word_h] + ([] if seen_dict.get(word_p) is None else seen_dict[word_p])
            
    return seen_dict
def get_selectional_association(predicate, headword, lines, dataframe, metric):
    v1 = torch.FloatTensor(dataframe.loc[headword].values)
    seens = seen_headwords[predicate]
    
    total = 0
    for seen in seens:
        try:
            v2 = torch.FloatTensor(dataframe.loc[seen].values)
            total += metric(v1, v2)
        except:
            pass
        
    return total
def wsd(predicate, headwords):
    selectional_associations = []
    for h in headwords:
        selectional_associations += [get_selectional_association(predicate, h, lines, co, get_cosine_similarity)]

    print(selectional_associations)

'가'라는 조사에 가장 잘 맞는 단어가 '학교'임을 잘 예측하는 것을 알 수 있따.

 

 

 


마치며...

단어는 보기에는 불연속적인 형태이지만 내부적으로는 연속적인 '의미(sense)'를 갖는다.
따라서 우린 단어의 겉모양이 다르더라도 의미가 유사할 수 있음을 알고 있으며,
단어의 유사도를 걔산할 수 있더라면 corpus로부터 분포나 특징을 훈련 시, 더 정확한 훈련이 가능하다.


WordNet이라는 사전의 등장으로 단어사이 유사도(거리)를 계산할 수 있게 되었지만
WordNet과 같은 Thesaurus 구축은 엄청난 일이기에 사전없이 유사도를 구하면 더 좋을 것이다.

사전없이 corpus만으로 특징을 추출해 단어를 벡터로 만든다면, WordNet보다 정교하지는 않지만 더 간단한 작업이 될 것이다.
추가적으로 corpus의 크기가 커질수록 추출할 특징들은 점차 정확해질 것이고, 특징벡터도 더 정확해질 것이다.

특징벡터가 추출된 후 cosine유사도, L2 Distance 등의 Metric을 통해 유사도를 계산할 수 있다.


하지만 앞서 서술했듯, 단어사전 내 단어 수가 많은만큼 특징벡터의 차원도 단어사전크기와 같기에 
"단어대신 문서"를 특징으로 사용하더라도 주어진 문서의 숫자만큼 벡터의 차원이 생성될 것이다.

다만 더 큰 문제는 그 차원의 대부분 값이 0으로 채워지는 "희소벡터"로 
무엇인가 학습하고자 할 때, cosine유사도의 경우 직교하여 유사도값을 0으로 만들 수 있다.
즉, 정확한 유사도를 구하기 어려워 질 수 있다.

이런 희소성문제는 NLP의 두드러진 특징으로 단어가 불연속적 심볼로 이뤄지기 때문이다.
전통적인 NLP는 이런 희소성문제에 큰 어려움을 겪었다.


하지만 최신 딥러닝에서는 단어의 특징벡터를 이런 희소벡터로 만들기보다는
"word embedding"기법으로 0이 잘 존재하지 않는 dense 벡터로 만들어 사용한다.
ex) word2vec, GloVe, ... (https://chan4im.tistory.com/197)

 

+ Recent posts