brunch

You can make anything
by writing

C.S.Lewis

by Qscar Dec 01. 2023

Transformer: 한영 번역기 만들기

Paper Review 2 : 모델링, 학습, 튜닝, 시각화

|Intro

들어가기에 앞서 본 포스팅은 Transformer 논문에서 제시한 구조와 기능들을 이용해 한국어를 영어로 번역하는 번역기를 구현하는 일체의 과정을 담고 있습니다. 포스팅 내에서 코드를 제시하지만 보다 원활한 이해를 돕기 위해선 저자의 github를 참조하며 진행하는 것을 추천드립니다.


이번 포스팅에서는 이전 포스팅에 이어서 Transformer를 살펴볼 것입니다. 이전 포스팅에서 Transformer란 무엇인지, 어떤 것들로 구성돼있고, 공식은 어떻게 되는지를 중심으로 세세하게 살펴봤다면, 이번 포스팅에서는 이렇게 구현한 Transformer를 이용해 한국어를 영어로 번역하는 Task를 수행해볼 것입니다. 혹시 이전 포스팅을 보고 싶거나 Transformer에 대해 잘 알지 못한다면 이전 포스팅을 먼저 보고 오시는 것을 추천드립니다.


Transformer 알고리즘은 현존하는 거의 모든 Task에 적용가능합니다. 그 시작은 자연어 처리로 시작됐지만, 그 이후 컴퓨터 비전, 이상탐지, 시계열 등 여러 Task에 적용되었습니다. 기껏해야 Transformer를 적절히 훈련시킬만큼 충분한 데이터를 수집하지 못했거나, 모으기 힘든 환경에서 운영되는 서비스만이 Transformer가 아닌 전통적 방식의 기계적 학습 방법론을 적용할 뿐입니다.


Transformer를 단순히 적용하는 방법은 간단합니다. Tensorflow나 Pytorch, hugginface와 같은 DL 라이브러리의 API를 활용하는 것입니다. 이를 통해 우리는 그저 Transformer의 하이퍼 파라미터 정도만 맞춰주면 손쉽게 사용가능합니다.

pytorch, tensorflow transformer API

혹은 BERT, GPT와 같이 구현된 모델 자체를 하나의 API로 활용할 수도 있습니다.

이렇게 구현된 모델을 사용할 경우에는 pytorch나 tensorflow보다 pytorch lightening이나 huggingface의 transformers 라이브러리가 더 효율적입니다. (이전에 모델 최적화를 위한 포스팅에서 사용했던 방식도 huggingface의 DistilBERT를 가져와 튜닝하는 식이었습니다)


이러한 API활용은 결국 해당 모델 구조에 대한 어느정도의 이해가 뒷받침되어야 더욱 효과적으로 운용할 수 있는 것도 사실입니다. 하지만 transformer에서 파생된 모델들을 일일히 익히고, 숙달하는 것은 불가능에 가까우니 이번 포스팅을 통해 Transformer 구조를 직접 구현하고, 본 논문에서 제안된 번역 Task를 진행해보며 Transformer에 대한 이해를 해보도록 하겠습니다.(본 논문에서는 영어-독어, 영어-불어 간 번역 task였지만 여기서는 한국어-영어 번역 task를 진행하겠습니다)


본격적인 내용을 진행하기 전에 이전 포스팅에서 진행했던 사항을 간략히 복습하자면 Transformer는 Attention Mechanism이 구현된 알고리즘을 지칭하며, 일반적으로 MHA(Multi-head attention)과 PE(Positional Encoding)이 포함됩니다. 이 과정에서 학습되는 것은 MHA 내부의 Query, Key, Value에 대한 가중치 행렬이며, 이들은 Multi Head로 쪼개지는 과정에서 Transformer의 비선형성을 강화시키는 역할을 하게 됩니다. (실제로 이들을 제외하면 최종적인 FC Layer만이 유일하게 가중치 업데이트가 이뤄지는 부분입니다)


Transformer Structure

위 그림과 같이 MHA와 Skip Connection, Feed Forward Network 등이 결합된 결과물을 하나의 Block이라고 지칭하며, 위의 오른쪽 그림에서 강조된 부분(Nx, N번만큼 반복한다는 뜻)과 같이 일반적으로 이러한 Block을 여러 번 반복해서 거치게 됩니다. (때문에 Attention Block의 input shape과 output shape은 동일해야 합니다)


복수의 Attention Block이 적용된 구조는 크게 Encoder와 Decoder로 나뉘게 됩니다. 다만 Encoder의 경우, 입력 데이터를 '중간 표현'이라 칭하는 형태로 변환하는 과정이고, 이는 입력 데이터의 중요한 특징을 담고 있습니다. Decoder의 경우엔 입력 데이터를 기반으로 새로운 출력을 만들어내는 것으로, 이 과정에서 Encoder의 중간 표현 결과를 고려할 수 있습니다. 자연어 분야의 대표적인 Encoder 모델로는 BERT가 있으며, 일반적으로 문장의 의도나 감성, 긍부정 분석 등을 수행하는 모델입니다. 자연어 분야의 대표적인 Decoder 모델로는 GPT가 있으며, 이를 통해 챗봇과 같은 질의응답 과정을 수행할 수 있습니다.



|Paper Summary

모델의 실질적 구현을 위해선 크게 세 가지가 병행되어야 합니다.

첫 번째로 수식의 구현입니다. 본 논문에서는 Scaled Dot-Product Attention 수식이 핵심이며, 수식 자체로는 그리 어렵거나 복잡하거나 새롭지 않습니다. 그 외에는 Positonal Encoding을 위한 Sin, Cos 함수 적용 방식이나 Adam Optimizer, Regularization, Warm up 등의 Hyper Parameter 값들뿐입니다.


두 번째로는 이러한 수식들이 기반이 된 각 레이어들의 조합입니다. 일반적으로 그림을 통해 전체적인 구조를 파악한 뒤, 수식과 하이퍼 파라미터가 적용된 레이어를 쌓는 방식으로 진행할 수 있습니다. 다만 레이어 간 추가적인 처리 코드나 레이어가 추가되거나 필요한 경우가 종종 있습니다.(혹은 그 외에 논문에 작성해둔 수치 몇몇이 틀린 경우도 종종 있습니다)


세 번째로는 각 모델 사이즈 별 세부 레이어 파라미터입니다. 논문에 따라 자세하게 제시되지 않는 경우도 종종 있습니다. 본 논문에서는 6.2 Model Variations 부분에 제시된 그래프이며, 아래와 같습니다.

모델 사이즈별 구조도와 성능지표

일반적으로 모델이 클 경우, 성능이 높습니다. 하지만 종종 그렇지 않은 경우가 있는데요, 그 대표적인 경우가 바로 데이터가 적을 경우입니다. 데이터의 양 자체가 충분하지 않은데, 복잡한 모델을 사용할 경우엔 모델의 학습에 더 많은 컴퓨팅 리소스를 소모하면서도 더 낮은 성능의 무거운 모델을 만들게 되는 결과를 야기할 수 있습니다. 특히 이번 포스팅에서는 AI Hub에서 다운받은 한국어-영어 번역 데이터셋을 사용할 것이며, 이 데이터셋의 사이즈가 10만건 정도로 작기 때문에 더욱 유의해야 합니다.


위 그림은 모델의 파라미터나 특정 방법론을 적용함에 따른 스코어(성능차이)를 가시화해 놓은 것입니다. 제일 윗줄의 base가 base model에 해당하는 파라미터이며, (A)에서는 head의 개수를 바꿔감에 따라서 어떻게 성능지 바뀌는지, (B)에서는 attention의 Key Size를 줄였을 경우 어떤 문제가 발생하는지, (C)에서는 모델의 사이즈를 키우면 어떻게 달라지는지, (D)에서는 dropout과 label smoothing을 적용할 경우 성능이 어떻게 바뀌는지, 마지막으로 (E)에서는 positional encoding을 embedding으로 해 학습가능하도록 바꿀 경우 어떻게 성능이 바뀌는지를 나타내고 있습니다.


이러한 지표는 transformer를 이용한 후속 논문에서 다른 결과를 만들어내기도 합니다만, 일반적으로 모델의 사이즈는 키울수록 성능이 좋아지며, head를 키울수록 성능이 좋아지다가 떨어지는 지점이 있고, 과적합 방지를 위한 dropout은 범용적으로 좋지만, label smoothing과 positional encoding을 학습가능하도록 둘 것인가의 여부는 지표나 task에 따라 적용유무가 바뀔 수 있다는 것입니다.


아래 모델 구현 단계의 마지막에서 위 그림의 지표를 고려한 모델을 구현할 것입니다. 다만 단순히 AI모델이라는 것이 단순히 모델 구조를 구현한다고 끝이 아니기에, 본 논문에서 함께 제시된 다른 요소들도 하나씩 구현해 보도록 하겠습니다.



|Model Implementation


|Data Preview

데이터는 한국어-영어 번역 말뭉치로 왼쪽 링크를 클릭하면 이동하는 AI HUB 페이지에서 다운받을 수 있습니다. 본 논문에서는 450만에 해당하는 영어-독어 번역 말뭉치를 사용했다고 하지만, 여기선 다소의 응용과 컴퓨팅 리소스, 직관적인 이해를 위해 간단한 한국어-영어 말뭉치를 사용하겠습니다.

데이터 요약

데이터를 간략이 살펴보면 위와 같습니다. 크게 해당 문장이 사용되는 분야에 대한 분류와 상황, 발화자 등에 대한 정보가 있습니다. 이를 통해 해당 문장의 번역뿐 아니라 다양한 상황에 대한 분류나 챗봇 제작에도 도움이 될만한 데이터셋입니다.


|Tokenizer

데이터를 로드했다면 두 번째로 진행할 것은 Tokenizer를 로드하는 것입니다. Tokenizer는 자연어로 구성된 단어를 컴퓨터가 이해할 수 있는 형태로 임베딩하는 기능을 가지고 있습니다. 일반적으로 사전을 구축한다고 하기도 하는데요, 자연어에서는 대체어, 불용어, 형태소 사전 등 여러 형태의 사전이 목적에 따라 추가적으로 활용되기도 합니다. 이러한 Tokenizer도 별도의 알고리즘으로 구성된 AI라고 할 수 있으며, 다양한 방법과 형태가 존재하고 있습니다. 


여기서는 BPE(Byte Pair Encoding) 방식이 적용된 Tokenizer를 로드해 사용하겠습니다. 일반적으로 BPE 방식의 토크나이저는 띄어쓰기를 기준으로 단어를 분리하며, 학습한 데이터셋에서 자주 등장한 문자열을 토큰으로 인식하며, 재귀적으로 바이그램 분석을 수행하며 지정한 사이즈의 사전이 완성될 때까지 반복해 자주 사용되는 사전을 구성하는 방식입니다. 만약 토크나이저에 대해 좀 더 자세히 알고 싶다면 여기를 참고하세요.


본 논문에서는 BPE 방식의 37,000 사이즈의 사전 크기를 가진 토크나이저를 채택했습니다. 사전의 사이즈가 크다는 것은 더욱 많은 단어를 이해하고, 고려할 수 있다는 의미를 지니지만 그만큼 많은 컴퓨팅 리소스를 요하게 됩니다. 본 논문 구현 과정에서는 현실적인 한계(컴퓨팅 리소스)를 고려해 세 가지 부분에서 원 논문과 차이가 있는데, 그 중 첫 번째가 이 토크나이저의 사이즈입니다. (참고로 두 번째는 배치 사이즈이며, 세 번째는 학습 epoch입니다)

tokenizer code

위 tokenizer가 로드돼 수행된 것을 보면, 띄어쓰기는 '_'로 구분되며, 우리의 생각보다 더 많이 쪼개진 형태로 구분돼있는 것을 알 수 있습니다. 이렇게 token으로 구분된 문장은 각기 다른 값으로 embedding되게 됩니다. 좀 더 자세히 살펴보겠습니다.


tokenizer encoder test

tokenzier는 크게 세 가지 기능이 있습니다. 하나는 긴 문장을 여러 개의 토큰으로 분할하는 것이고, 다른 하나는 토큰을 지정된 인덱스로 변환하는 것, 마지막 하나는 지정된 인덱스를 토큰으로 변환하는 것입니다. 일반적으로 우리는 뒤의 두 가지 기능, 인코더 기능과 디코더 기능만을 사용하게 됩니다.


자연어 처리를 이해하기 위해서는 </s>와 같은 special token에 대해서 알아야 하는데요, 특별한 토큰이라고 작성돼있지만 일반적으로 자연어 처리를 위해 범용적으로 사용되는 토큰입니다. 크게 문장의 시작과 끝을 지정하는 토큰과 지정된 최대 길이보다 작은 문장이 입력됐을 때, 남은 부분을 채워주는 패딩 토큰이 있습니다. (번외로 만약 데이터셋이 충분히 크다면, 이러한 토큰을 무시하고 통째로 밀어넣거나 모델 자체적으로 문장의 시작과 끝을 찾아내도록 함으로써 성능을 향상시키는 방법론들도 존재합니다)


위 코드에서 tokenizer의 encode 기능을 통해 speical token을 True로 지정할 경우, 문장의 마지막을 의미하는 토큰이 마지막에 추가로 들어가게 됩니다. 


tokenizer decoder test

tokenizer decode의 테스트 결과는 위와 같습니다. 이전의 encode에서 speical token=True를 통해 추가한 토큰의 인덱스인 0을 넣으면 문장의 마지막을 의미하는 eos토큰인 </s>가 추가되고, '_문장'이 인코드됐던 13774를 넣으면 다시 '_문장'이 반환되며, 인코딩된 값으로 구성된 리스트를 넘겨주니 띄어쓰기가 '_'로 치환된 값이 반환됐습니다.


이런 식으로 tokenizer는 핵심적인 어휘를 찾아내 자체적인 사전을 구축한 후, 이 사전을 기초로 자연어를 기계어로 변환하고, 다시 기계어를 자연어로 변환하는 역할을 하게 됩니다.


|Optimizer

일반적으로 Deep Learning은 모델 구조, 손실함수, 옵티마이저의 결합으로 완성됩니다. 이들 중 하나만 달라도 다른 결과를 만들어낼 수 있으며, 상황이나 데이터, task에 따라 각기 다른 옵션을 선택하기도 합니다. Optimizer는 손실함수가 최소화할 수 있도록 학습률을 조정하는 등의 최적화와 관련이 있습니다. 

본 논문의 학습 정보

본 논문에서는 베이스 모델의 경우, 8대의 NVIDIA P100 GPU를 활용해 12시간 동안 100,000 STEP 학습했다고 언급돼 있습니다. 배치당 25,000개의 source-target 토큰 쌍으로 이뤄져있고, 전체 데이터셋의 규모가 약 450만이니, 한 번의 에포크당 180회의 STEP이 진행된 셈입니다. 


|Scheduler

본 논문의 학습률 공식

대략 556 EPOCH가 진행되었으며, 전체의 4% 부분인 4,000 STEP까지 Warm-up Step을 진행하였습니다. 이러한 Warm-Up Step은 일반적으로 모델이 데이터의 특징을 파악하기 위해 최초에 진행되며, 이를 통해 데이터의 분포와 같은 특징에 대해 익힌 후 본격적인 학습이 진행되게 됩니다. 이는 일반적으로 학습 초기에 높은 학습률을 가지는 기존의 방식을 채택할 경우, 전체 신경망을 불안정하게 만드는 문제를 해소하고자 진행합니다. 정확히는 초기에 오히려 낮은 학습률로 데이터의 특징을 파악하는 시간을 가지며, 점차 학습률을 올렸다가, 모델이 학습됨에 따라 다시 학습률을 점차 낮추는 방식으로 진행하는 것입니다.


이러한 Warm-Up은 일반적으로 Scheduler라는 기능을 통해 구현할 수 있으며, Scheduler는 Optimizer와 함께 구현돼 적용됩니다. 위 이미지의 공식을 살펴보면 둘 중 더 작은 값(min)을 모델의 차원수(d_model)의 루트로 나누도록 되어있습니다. 하나는 전체 스탭 수 제곱근의 역수이며, 다른 하나는 전체 스탭 수에 웜업 스탭수를 -1.5승한 값입니다. 이해하기 쉽도록 작성하면 아래와 같습니다.


직관적으로 이해할 수 있도록 이를 적용한 코드를 작성해보겠습니다.

NoamScheduler를 구현한 코드

위 스케줄러는 본 논문의 저자 중 하나인 Noam이 제시한 스케줄러로, Noam Scheduler라는 명칭이 붙어있습니다. 위 코드는 스탭이 하나씩 늘어남에 따라 지정된 웜업 스탭수까지는 낮은 학습률에서 서서히 높여나가다가, 웜업 스탭수를 기준으로 다시 점차 낮춰나가는 방식으로 작동하게 됩니다.


이에 대한 성능을 직관적으로 이해하기 위해 그래프를 그리는 코드를 작성해보겠습니다.

scheduler 공식을 적용한 그래프


위 코드의 실행 결과는 아래와 같습니다.

스케줄러가 적용된 학습률을 표현한 그래프

총 100,000 STEP이 진행된다고 할 때(본 논문의 base model), 처음에는 아주 낮은 learning rate로 시작했다가 우리가 warm-up step으로 지정한 4,000 step부터는 조금씩 줄어드는 형태입니다. 이러한 scheduler는 모든 DL 과정에서 필수적이진 않지만 최초의 학습율을 마지막까지 그대로 유지할 경우, 성능 최적화가 적절히 일어나지 않는 경우가 많습니다. 때문에 여러 다양한 방법을 적용해 학습률을 줄이는 방식이 채택됩니다.


|Regularization

본 논문에서 제안한 세 가지(?) 종류의 규제화

본 논문에서는 크게 세 가지 종류의 규제(Regularization)들을 적용했다고 하지만, 큰 묶음으로 제시하는 것은 두 가지입니다. 바로 Residual Dropout과 Label Smoothing인데요, 굳이 하나를 더 찾자면 Residual Dropout 내부에서 언급된 Normalization 정도까지를 포함해 세 가지라고 한 게 아닌가 싶습니다.


일반적으로 이러한 규제는 모델의 과적합을 방지하고, 일반화 성능을 높이기 위해 사용합니다. 이러한 규제는 모델링의 내부에서 주로 구현되지만, 옵티마이저나 손실함수에서도 구현될 수 있습니다.


|Residual Dropout

Residual Dropout이나 Normalization은 모델링의 과정에서 구현되며 그 과정에서 다시 설명할 것입니다. 간단히 개념적으로만 설명하자면 아래 그림에서 붉은색으로 체크된 레이어, 정확히는 인코더와 디코더의 서브 레이어(MSA, FF Network)들의 출력값에 대해 Dropout(0.1)을 적용하고, 이후에 Normalization을 적용하는 방식입니다.

Residual Dropout이 진행되는 레이어

일반적으로 Dropout은 네트워크의 일부 뉴런(노드)를 무작위로 비활성화하며, 이 과정에서 과적합을 방지할 수 있습니다. 이렇게 Dropout된 네트워크로부터 전달된 결과를 Normalization시키는데, 이러한 순서에 대한 성능 차이는 (경험에 비춰보면) task마다 다른 것 같습니다.


residual dropout에 대한 설명

본 논문에서 제시된 Residual Dropout의 방식은 위 그림의 하이라이트 부분을 참고해 적용했습니다. 이전의 입력값(잔차)과 이전 서브레이어를 통과한 값을 합치는 Residual Connection을 진행한 뒤, 레이어 정규화를 진행하는 것인데요. Dropout을 정규화 이전에 적용해야 한다고 했으니, Sublayer를 통과한 값에 대해 드랍아웃을 적용하면 될 것 같습니다.


|Label Smoothing

이전에 모델 최적화와 관련된 포스팅에서도 다뤘던 주제입니다. 당시 사용했던 Google Brain의 논문의 그림을 살펴보면 아래와 같습니다. 만일 이 부분에 대해서 좀 더 자세한 설명을 원한다면 여기를 클릭하셔서 일독해보시는 것을 권장합니다. Label Smoothing에 대해서 간단히 설명하면 일반적인 분류 문제에 대해 정답 클래스만을 1로, 나머지를 0으로 두는 일반적인 방식 대신, 그와 유사한 다른 카테고리에 대한 확률도 함께 고려하는 방식입니다.


개와 고양이, 새와 악어를 구분하는 문제를 푼다고 했을 때, 고양이와 개는 다른 새나 악어보다 서로 유사합니다. 네 발 짐승이고, 꼬리가 있다는 점을 포함하면 악어도 비슷하다고 할 수 있을 것이고, 부드러운 털로 뒤덮여있다는 점을 고려하면 새와도 비슷하다고 판단할 수 있습니다. 이처럼 분류 문제라는 것은 완벽하게 배타적으로 나눠떨어지는 문제가 아닙니다. 오히려 이러한 부분을 고려함으로써 모델의 성능을 높일 수 있다는 것이 'soft target들에 대한 아주 작은 확률'이라는 개념입니다.


이런 Label Smoothing을 적용할 때에도 위에서 한 것처럼 실제 데이터 간의 유사성 등을 면밀하게 분석하고 판단해 적용할 수 있으면 좋겠지만, 최소 수십 만에 이르는 데이터에 대해 일일히 레이블 값을 조정하는 것은 현실적으로 어렵기에 우리는 일반적으로 아래와 같은 공식을 통해 Label Smoothing을 적용합니다.

Distilation softmax with T

T를 이용해서 기존의 softmax와 같은 손실함수를 변형시키는 방식인데요, 이를 적용해 학습한 결과를 살펴보면 아래 그림과 같습니다.


T값에 따른 Gumbel softmax의 결과 및 학습 후 예측

위 그림에서 a)는 모델이 학습한 후 추론한 결과이고, b)는 T값에 따른 샘플입니다. 위 그림을 통해 우리는 T를 높일수록 전체 분포가 완만해지고, 단순히 하나의 클래스만을 정확히 맞추는 것이 아니라 그 외의 가능성까지도 고려한다는 것을 알 수 있습니다. 본 논문에서는 0.1을 적용했다고 합니다. 이를 이용한 손실함수를 간단히 작성하면 아래와 같습니다.

label smoothing이 적용된 cross entropy loss

첫 번째 함수 smooth_label은 기존의 일반적인 categorical label 변환에 label smoothing을 적용한 것입니다. 이를 통해 간단히 노이즈가 섞인 categorical label을 생성할 수 있습니다. 이에 대한 결과를 살펴보면 아래와 같습니다.

label smoothing의 효과

위 코드는 임의의 예측값과 레이블을 이용해 label smoothing의 효과를 보기 위한 코드입니다. 아래의 출력으로 보여지는 결과는 각각 임의로 생성한 예측값, categorical 형태로 변환한 label, 이 label에 label_smoothing을 적용한 결과이며, 그 아래로는 원본 label과 smoothing이 적용된 label에 대한 loss입니다. 이러한 결과를 통해 알 수 있듯 label_smoothing은 심플하게 본래 정답 레이블은 1, 그 외에는 0으로 표현됐던 것을 정답 레이블은 0.9, 그 외에는 아주 낮은 값으로 균등하게 들어가는 식으로 처리된 것을 알 수 있습니다. 이런 식의 처리 방법은 모델의 강한 확신을 막아 과적합을 방지하는데 효과적이지만, 일부 지표에서는 불리하게 측정될 수 있습니다.


실제 모델 학습을 위해서는 일반적으로 CrossEntropyLoss 구현체를 사용할 수 있습니다. Label Smoothing도 마찬가지로 아래와 같이 기존 CrossEntropyLoss 함수를 활용할 수 있습니다.

label smoothing 적용 여부에 따른 손실함수 정의

위 코드를 살펴보면 CrossEntropyLoss 내부의 label_smoothing을 활용하는 것을 확인할 수 있습니다. 0에 가까울수록 일반적인 CE Loss처럼 작동하게 되는 식입니다. 이런 식으로 기존의 구현체를 사용하는 것 외에도 자체적으로 손실함수를 정의할 수도 있습니다. 

label smoothing이 적용된 custom loss function

이렇게 자체적으로 구현한 로스함수는 정상적으로 손실값을 출력하는지, 그것이 우리의 의도에 맞는지 등을 검증하는 과정이 필수적으로 동반되어야 합니다. 또한 자체적으로 정의한 손실함수로 인한 비효율이 발생할 수도 있습니다.


향후 학습 과정에서 위 손실함수와 일반적으로 사용되는 CrossEntropyLoss 함수를 사용했을 때의 성능차이를 확인해보도록 하겠습니다. 일반적으로 위 함수는 log를 취해서 연산하기 때문에 출력되는 loss 값 자체는 CrossEntropyLoss보다 작게 나타나지만 살펴볼 부분은 train loss와 val loss가 잘 수렴하며 떨어지는지, 어느 정도 선까지 성능 향상이 가능한지 입니다. 이를 위해 학습된 모델을 별도의 지표를 이용해 평가해볼 것입니다.


|Modeling

모델링은 본 논문에서 제시한 형태와 파라미터를 적용해 구현할 것입니다. 모든 부분에 사용되는 MHA와 FF 네트워크를 먼저 구현한 뒤, 이를 이용해 Encoder Layer와 Encoder, Decoder Layer와 Decoder 클래스를 구축하고, 이들을 한데 묶어 Transformer를 구축할 것입니다. 이때 번역기를 구현할 것이기 때문에 정답 레이블에 대한 Mask가 함께 구현되게 됩니다.


|MHA(Multi-Head Attention) Network

처음으로 구현할 것은 본 논문의 핵심인 Multi-Head Attention입니다. 이후에 나온 논문에서는 이러한 Multi-Head Attention 구조만으로도 대부분의 문제를 풀어내고, SOTA 성능을 달성한 적이 있을 정도로 최근 AI의 가장 중요한 요소라고 할 수 있습니다. 실질적으로 Attention 구조만 적용이 될 경우, Linear 외에는 가중치 행렬 부분만이 거의 유일한 비선형성을 지니게 되는데요. 이를 복수의 head로 나눠 진행함으로써 대부분의 문제를 풀어낼 수 있는 비선형성을 충분히 갖출 수 있습니다.

MHA Code

위 코드에 대한 자세한 설명은 이전 포스팅을 참조하시기 바랍니다. 일반적인 MHA 구현 코드입니다.


|FF(Feed Forward) Network

다음으로는 피드포워드 네트워크라 하여, 흔히 FF Network로 칭하는 레이어입니다. 일반적으로 하나의 어텐션 블록의 마지막에 통과하게 되며, 구현한 코드는 아래와 같습니다.

FF Network

위 코드에 보면 7번째 줄에 Dropout을 추가하였고, 그 옆으로 #을 두 개 작성한 주석을 적었습니다. 본래 논문에서는 해당 구간에 Dropout에 관한 언급이 없지만 선형층 두 개가 연속되는 구조로 연결되면 일반적으로 과적합이 발생하기 쉬워 방지하기 위해 추가적으로 작성한 부분입니다. 이처럼 논문과 다른 형태로 구현한 부분에 대해서는 ##로 주석을 작성하였습니다.


|Encoder Layer

Encoder Layer

MHA과 FF Network를 구현했다면 다음은 이들을 이용한 Encoder Layer를 구축합니다. 여기서 Encoder Layer란 Encoder의 내부요소들만을 포함하는 클래스로 본 논문에서 위 그림에 해당하는 부분을 구현한 것입니다.

EncoderLayer Class

위 코드에서도 본 논문과 조금 달라지는 부분이 있는데요, 바로 26번째 줄의 Normalization입니다. 사전에 코드를 구현해 학습하고 테스트해본 결과, 아무래도 학습 데이터와 학습 시간이 적어 제대로 학습이 되지 않는 문제가 발생해 이를 보완하고자 사전 정규화(Pre Normalization)라는 것을 추가하였습니다. 위 코드를 살펴보면 그 외에는 본 논문과 동일하게 구현하였습니다. 


[row26~31] 어텐션을 진행한 후에, 서브레이어의 출력에 대한 드랍아웃(Residual Dropout)을 진행해 합친 뒤 정규화를 진행하였으며, [row32~35] 피드 포워드 네트워크를 통과시킨 뒤, 그 출력 결과에 대한 Dropout과 잔차연결을 진행한 뒤, 정규화를 진행해 그 결과물을 최종적으로 출력합니다. (참고로 함께 출력되는 atten_enc는 추후에 어텐션 맵을 보기 위해 추가해놓은 것입니다. hook이라는 방식을 통해 Attention Map을 구현할 수도 있지만 여기선 좀 더 직관적인 방법을 사용합니다)


|Encoder

Encoder Class에서 하는 역할(highlight part)

Encoder Layer가 Encoder 내부의 구성요소들을 하나의 블록으로 구현한 것이라면, Encoder는 Encoder Layer를 N번 반복시키는 것, 그리고 Positional Encoding과 같이 Encoder 블록 외부에 있는 것들을 포함해 하나의 Encoder를 완성시킵니다.

Encoder Class

[row 34] Encoder 코드를 살펴보면 벡터화된 입력값에 대해 scale을 적용한 것을 볼 수 있습니다. 이는 이전 포스팅에서는 구체적으로 언급하지 않은 부분이며, 본 논문에서 언급된 사항을 구현한 것입니다.


입력 임베딩에 대한 스케일을 적용한다는 부분

본 논문에서도 이에 대해선 한 줄 정도로 표현하고 있는데요, 이러한 스케일 또한 근본적으로 Scaled Dot-Product를 수행할 때와 동일한 이유로 사용됩니다. 임베딩 벡터에 대한 내적을 진행할 때, 차원의 수(d_model)이 커질수록 내적값이 매우 커져 softmax 함수를 거치며 기울기가 소실되는 문제가 발생할 수 있어, 스케일링을 통해 이 값을 줄여 softmax 함수가 의도에 맞게 작동하도록 돕습니다. 또한 이 과정에서 포지셔널 인코딩 값이 더해지게 되는데, 본래의 의도와 다르게 이 값에 대한 영향력이 너무 커지게 되는 등의 문제도 방지해줍니다. 이러한 스케일링의 효과를 살펴보면 아래 그림과 같습니다.

Input Embedding에 대한 Scaling Factor의 효과

위의 왼쪽 그림 두 개는 Scaling factor를 통해 데이터의 범위(x축)가 늘어난 것을 확인하기 위한 것이고, 오른쪽 그림 두 개는 그렇게 늘어난 결과 Attention Score의 분포가 어떻게 변하는지를 보여주는 것입니다. 위 그림처럼 Scaling을 적용함에 따라 데이터 분포의 범위가 늘어나고 이에 따라 더욱 면밀한 Attention Score 연산이 가능해진다는 것을 알 수 있습니다. 이러한 부분은 Transformer 구조가 깊어질수록 중요해지게 됩니다.(반대로 Transformer가 아주 간단하고, 얕은 구조로 구현되었다면 큰 의미가 없을 수 있습니다)


[row 18] 참고로 여기선 이전의 포스팅에서 언급한 positional encoding 대신 positional embedding을 사용하는데요, 그 이유는 크게 두 가지입니다. 첫 번째로는 위치 정보를 학습가능한 레이어로 구축한다고 해서 본 논문에선 큰 성능의 향상은 없다고 했지만 후속 논문을 통해선 그렇지 않을 수 있다는 것을 보여줬다는 것과 두 번째로 cos, sin 함수를 이용해 구현했더니 전체 코드가 가독성이 떨어지고 포스팅용 이미지로 사용하기에 다소 길어졌기 때문에 이를 좀 줄이고 싶었습니다. (추가적으로 이전 포스팅에서 이미 보여준 방식이기에 다른 방식을 제시하고 싶었습니다. 모델의 복잡성이 높아지는 것에 관련해서도 테스트해봤으나 에포크당 처리되는 속도나 성능의 차이가 거의 없었습니다)


[row 23] 또한 인코더 클래스에선 이전의 인코더 레이어로 구축한 블록을 여러 번 반복합니다. 이를 위해 입출력을 위한 데이터의 형상은 같아야 합니다.


|Decoder Layer

Decoder Layer 구조

Decoder Layer의 구조는 위와 같습니다. Decoder Layer는 Encoder Layer보다 조금 복잡합니다. 그 이유는 크게 두 가지가 있는데, 첫 번째로 내부의 서브레이어가 세 개로 두 개인 인코더 레이어보다 길고, 두 번째로 cross attention과 self attention이 공존하기 때문입니다. 이를 이해하기 쉽도록 조금 더 세분화해서 구현한 코드는 아래와 같습니다.

Decoder Layer Class

복잡해보일 수 있지만 하나씩 살펴보면 이미 그 전에 진행했던 것들과 동일한 것입니다. Encoder Layer와 동일하게 MHA와 FF Network를 이용한 서브 레이어를 구축하며, 레이어 정규화와 드롭아웃이 존재합니다. 이를 이용해 process_sublayer 함수를 정의해 이 안에서 self attention과 encoder-decoder attention(cross attention), ff network 서브레이어를 구축합니다. 


[row 41~44] 여기서 주의해야할 점은 encoder-decoder attention을 구현할 땐, query는 decoder의 출력(x_norm)을 사용하지만, key와 value는 encoder에서 넘어온 값을 이용하도록 구현해야 한다는 것입니다. 


|Decoder

Decoder Class

Decoder 클래스에 대한 코드는 Encoder와 그리 다르지 않습니다. Decoder를 Encoder와 다르게 하는 부분은 Decoder Layer 혹은 Transformer를 통해 구현되기 때문입니다. Encoder 클래스와 마찬가지로 여기에서도 임베딩 레이어에 대한 스케일을 적용하고, 내부 구성요소인 디코더 레이어를 n회 반복하며 어텐션 맵을 저장하고, 최종 출력으로 전달하는 형태입니다.


|Transformer

이전의 단계까지를 통해 우리는 MHA와 FF Network를 통해 Encoder Layer와 Decoder Layer를 구축했고, 다시 이들을 이용해 Encoder와 Decoder를 구축했습니다. 이제는 Encoder와 Decoder를 연결하고, 적절한 마스크를 생성해 넘겨주고, 모델 학습을 위해 가중치 초기화를 시켜주는 정도의 일만이 남았을 뿐입니다. 코드로 살펴보면 아래와 같습니다.

transformer class

코드가 조금 긴데요, 하지만 살펴보면 크게 세 개의 마스크 생성 코드로 인한 것임을 알 수 있습니다. 이러한 마스크가 하는 공통적인 역할은 바로 패딩 토큰을 무시하도록 해주는 것입니다. 패딩 토큰은 입력 시퀀스의 길이가 지정한 시퀀스보다 작을 경우, 지정한 시퀀스 사이즈로 만들기 위해 의미없이 들어가는 토큰이기 때문에 실질적인 모델의 학습 과정에서는 다시 배제하는 과정을 거쳐야 하기 때문입니다. 


마스크의 종류는 총 세 가지인데요, 하나는 인코더 마스크로 패딩에 해당하는 값만 가려줍니다. 다른 하나는 디코더 마스크로 패딩에 해당하는 값뿐 아니라 미래 위치에 대해서도 마스크를 생성하고, 마지막으로 인코더 디코더 마스크에서는 인코더의 패딩 토큰을 가려줌으로써 디코더에서 인코더의 정보를 참조해 번역 등의 task를 진행할 때, 패딩 토큰을 무시하도록 합니다.


[row 21~24] 또한 위 코드에서는 가중치 초기화라는 과정이 진행되는데요, 일반적으로 아무런 초기화 코드를 적용하지 않을 경우 무작위로 초기화되게 됩니다. 일반적으로 Xavier(Glorot) 초기화 혹은 He(Kaiming) 초기화가 있습니다. Xavier 초기화는 입력 노드의 입출력을 모두 고려해 입출력의 분산을 균일하게 유지하려고 하며, He는 입력에 대한 분산만을 고려하게 됩니다. 일반적으로 Sigmoid나 Tanh 활성화함수를 사용하는 네트워크에서는 Xavier를, ReLU 활성화함수를 사용하는 네트워크에는 He를 선택하는게 좋다고 합니다. 다만 Transformer 구조에선 어텐션 매커니즘이 구현되는 과정에서 안정성이 요구되어 Xavier초기화가 주로 선택되는데, 일반적으로 He초기화는 Xavier보다 더 큰 분산을 설정하게 되며, 이로 인해 네트워크 폭주 문제에 취약하기 때문입니다.


추가적으로 본 논문에서는 기계번역을 진행하면서 추가적으로 두 가지를 고려했습니다. 

기계번역 task를 위해 고려한 점

하나는 각 언어의 특성에 맞게 max_len을 조정하는 것인데요, 본 논문에서는 영어를 독어나 불어로 바꾸면 좀 더 긴 토큰으로 구성된 번역문이 나오는 경우를 고려해 출력토큰의 최대길이를 50 더 길게했고, 추가적인 연구에서는 300 더 길게 진행하기도 했습니다. 본 포스팅에서는 일반적으로 한국어를 영어로 번역할 경우, 토큰이 더 길어질 수 있고 본 논문의 레퍼런스를 그대로 따르는 컨셉을 유지하기 위해 동일하게 50 더 길게 구현하였습니다. (하지만 사전에 구축된 데이터셋의 길이 확인 및 늘리지 않고 학습시켜본 결과 굳이 늘리지 않아도 상관없긴 했습니다)


다른 하나는 Beam Search라는 기술입니다. 간단히 말해 번역이나 생성과 같은 Transformer의 Decoder를 이용한 테스크를 수행할 때, 몇 개의 가능성 높은 후보를 살펴보고 가능성을 넓히는 방법론을 의미합니다. 하지만 본 포스팅의 주제에서는 벗어나는 내용이고, 본 논문에서도 핵심으로 다루고 있는 것은 아니라 따로 설명하지 않습니다.



|Train & Validation

위 과정까지 진행했다면 이제 남은 것은 모델을 학습하고, 그 성능을 측정하는 것입니다. 바로 학습 코드 작성으로 넘어가겠습니다. 


|Hyper Parameters

논문과 동일한 학습 모델 구현을 위해 hyper parameter를 동일하게 맞춰보겠습니다. 

Hyper Parameters

이전에 설명한 바와 같이 현실적 한계를 고려해 수정변경한 부분이 있습니다. ##로 표시한 주석 부분인데요, 컴퓨팅 리소스의 한계 및 데이터셋 규모의 한계로 인해 다소 조정한 부분입니다. 제대로 학습을 하려면 모델의 hyper parameter도 더 작게 수정해야 합니다. 지금은 데이터셋의 사이즈에 비해 모델이 너무 크기 때문입니다.


모델의 파라미터 수

이렇게 구축한 본 모델은 positional embedding 등의 어느 정도 차이가 있어 base 모델보다는 크고, big 모델보다는 작은 약 1억 개의 파라미터를 가지고 있습니다. 이를 약간 조정해 본 논문에서 제시한 base model의 파라미터수와 얼추 비슷하게 맞춰보겠습니다.

본 논문의 base model params인 65mil에 맞춘 결과

위 코드를 통해 실행하면 gpu 성능에 따라 다를 수 있지만 제 경우엔 한 epoch 당 대략 3분 정도가 소요됩니다. 만약 코드를 돌려보고 싶은데, gpu 메모리 부족 등이 문제라면 배치 사이즈를 줄여서 진행하는 것을 추천드립니다.


[23.12.01 추가] 학습 코드는 jupyter notebook이 아닌 02.translator_train.py를 통해 실행하는 것을 추천합니다. click 함수를 통해 파라미터를 바꿀 수 있으며, 파라미터는 main 함수를 참조해 수정변경하시면 됩니다.


|Training

실제 학습은 위에서 언급한 다소 큰 사이즈의 하이퍼 파라미터(base model과 맞춘)를 사용하였습니다. 데이터 사이즈가 작아 10시간 동안 약 150에포크 학습하였고, 약 11만 스탭 동안 학습 진행하였습니다. 참고로 이 경우엔 위에서 제시한 하이퍼 파라미터에서 웜업 스탭을 기존의 논문과 동일하게 4,000스탭으로 맞춰놓고 진행하였습니다.


손실함수는 일반적인 Cross Entropy Loss와 위에서 정의한 Label Smoothing이 적용된 Cross Entropy Loss를 사용한 버전 두 가지로 나눠 학습을 진행하였습니다. 이를 적용하는 코드는 아래와 같은 식입니다.

손실함수 적용 예시

이를 적용해 학습코드를 간단히 작성해보았습니다. 일반적인 학습코드와 거의 유사하지만 두 가지 차이점이 있는데요. 하나는 손실함수 외에 Scheduler를 통한 Learning Rate의 변화를 관측하기 위해 이에 대한 추이를 저장하였다는 것이며, 다른 하나는 이러한 LR와 Loss를 기록하는 그래프를 저장하도록 했다는 것입니다. 학습 코드는 아래와 같습니다.


학습코드

로그 출력 부분이 길어서 작게 보이는데, 잘 확인이 안되신다면 제 github의 원본 코드를 참조하셔도 좋습니다. 위 코드를 요약하면 일반적인 구조에 맞게 학습모드로 학습하고 평가모드로 평가를 진행해 val loss가 이전에 기록된 최소 loss보다 작으면 모델을 저장하고 최소 loss를 최신화합니다. 이 과정에서 학습시간을 측정하기 위한 코드, loss 측정을 위한 별도의 함수, loss와 lr의 시각화를 위한 별도의 함수가 추가되었으며, 이는 본 포스팅의 주제가 아니기에 코드는 살펴보지 않고 결과만 확인해보겠습니다. (이 부분이 만약 궁금하시면 github를 참조하시기 바랍니다)


우선 Loss 추이입니다.

ce loss만 적용했을 경우(좌), ce+ls가 적용된 경우(우)의 loss 그래프

위의 왼쪽 그래프가 Cross Entropy Loss만을 적용했을 경우이고, 오른쪽 그래프가 Label Smoothing(0.1)을 적용했을 경우입니다. 왼쪽 그래프에 비해 label smoothing을 적용했을 때, 비교적 과적합이 천천히/적게 발생하고 있는 것처럼 보입니다. 


다만 이러한 label smoothing을 적용하기 위해선 이번에 사용한 데이터셋보다 최소 몇 배는 많은 데이터셋이 구축되어야 합니다. label smoothing은 다양한 가능성을 함께 고려하게 만듦으로써 모델의 최적화 속도를 늦춰 천천히 학습하며, 섣부르게 최적화되는 것을 제한하는 것 대신에 우리가 생각하는 정답에 이르는 과정에 요구되는 리소스도 더욱 많이 필요하기 때문입니다.

학습이 진행됨에 따른 learning rate의 추이

위 그래프들은 마찬가지로 ce와 ls-ce가 각각 적용된 모델의 학습률 곡선을 나타냅니다. 이는 손실함수와는 관련이 없기 때문에 동일한 양상을 나타내는 것을 알 수 있습니다. 우리가 지정한 4,000 스탭(전체의 약 1/28 지점)까지 학습률이 올라갔다가 점차 하락하는 모양새입니다. 다음으로 모델을 실제로 불러와 평가해보는 과정을 거치겠습니다.


|Validation

각 모델의 info

여기서도 마찬가지로 ls-ce가 적용된 모델과 ce만 적용된 모델별로 나눠서 성능을 비교해보겠습니다. 우선 모델의 저장시점은 ce only 모델은 26epoch에서, ce+ls(0.1) 모델은 28epoch에서 가장 좋은 성능을 보였습니다. 참고로 ce+ls(0.2) 모델은 30epoch로 label smoothing이 늘어날수록 모델이 최적화되는 시기는 조금씩 뒤로 밀리는 경향을 보였습니다.


이를 아래의 코드를 이용해 직접 성능을 확인해볼 수 있습니다.

번역 모델을 활용하기 위한 코드

위 코드에선 실제 논문에서 언급된 바와 같이 입력 데이터의 토큰보다 훨씬 긴 문장으로 번역되도 상관없도록 50개의 추가토큰을 허용하는 방식으로 구축하였습니다(row 9). 그  외에는 Transformer 모델의 특징인 attention score를 시각화한 attention map을 출력하도록 함으로써 이를 가시적으로 확인하고자 하였습니다.


ce모델의 번역성능
ce+ls(0.1) 모델의 성능
ce+ls(0.2) 모델의 성능

위 코드는 test_DS로 분류해놓았던(학습 데이터셋에 포함되지 않은) 데이터를 활용해 모델의 성능을 테스트해본 것입니다. 세 모델 다 '저기'를 Excuse me로 번역했네요. 다른 문장을 더 살펴보겠습니다.


다른 문장으로  한 번 더 해보겠습니다.

ce모델의 번역성능 2
ce+ls(0.1) 모델의 번역성능 2
ce+ls(0.2) 모델의 번역성능 2

조금 잘되는 것들만을 고른 것이긴 한데요, 일반적으로 발생하는 문제로는 비슷한 단어의 반복, 유사어의 헷갈림, 상황 판단 부족 등이 있습니다. 이러한 이유로 번역기나 생성기와 같은 모델을 구축할 때에는 이러한 반복 등에 대한 규제나 가중치 등을 추가해 더 효과적인 모델을 만드는 기법이 사용됩니다.


|Translator Test

위와 같은 방식 말고도 이미 작성한 번역 함수를 이용해 본인이 작성한 문장을 번역하는 것도 가능합니다.

직접 작성한 문장으로 번역기 테스트

나중에 여유가 되면 최근에 추가로 찾은 AI HUB의 100만 개 이상의 번역 말뭉치에 대해서 학습해 추가적으로 업데이트해보겠습니다. 보다 많은 데이터셋을 사용하면 번역기의 성능이 높아지지 않을까 기대해봅니다.

참고로 이러한 번역 과정에서 주목할 부분은 우리는 문법 등을 따로 알려준 적이 없음에도 번역 결과가 틀릴지언정 기본적인 문법을 잘 지키고 있다는 것입니다. 이러한 사항은 Transformer 이전까지 여러 언어학자들에 의해 기계번역이 불가능한 가장 큰 이유 중 하나였습니다.


|Attention Map

Transformer가 적용된 모델의 특징 중 하나를 꼽으라면 바로 설명가능성입니다. 해석가능한 AI, 설명가능한 AI와 같은 분야는 지속적으로 중요하게 다뤄지고 있는데요, Transformer의 경우, Attention Map을 통해서 각 처리과정에서 어느 부분에 중점을 두고 있는지 판단할 수 있습니다. 바로 본 내용으로 넘어가기 보다는 이해를 돕기 위해 Computer Vision Taks에 사용된 Transformer 모델의 Attention Map을 살펴보겠습니다.

ViT 모델의 Attention Map[4]

위 그림은 해당 사진을 해석하는 ViT 모델이 어느 부분에 집중하고 있는지를 살펴보는 것입니다. 왼쪽 그림이 원본이고, 오른쪽 그림이 n번째 Attention Block의 Head에서 주목(Attention)하고 있는 부분을 시각화해놓은 것입니다. 위 그림은 첫 번째 어텐션 블록과 마지막 열두 번째 어텐션 블록을 시각화해놓은 것인데요, 일반적으로 최종 출력 레이어에 가까워질수록 올바른 대상을 잘 살펴보게 됩니다. 위 출력결과 및 코드에 대해 추가적인 사항이 궁금하시다면 주석으로 달아놓은 reference를 참고하시기 바랍니다.


다시 본론으로 돌아와 우리가 구축한 번역기의 Attention Map을 살펴보겠습니다. 여기서 살펴볼 Attention Map은 크게 세 개인데요, 각기 Encoder Layer, Decoder Layer, Encoder-Decoder Layer의 Attention Map입니다. Encoder Layer의 Attention Map은 우리가 입력한 언어, 즉 한국어끼리의 관계성을 파악하며, Decoder Layer의 Attention Map은 번역할 언어, 즉 영어끼리의 관계성을 파악하고, Encoder-Decoder Layer의 Attention Map에서는 한국어와 영어 간의 관계성을 파악합니다.


먼저 Encoder Layer의 Attention Map입니다. 본래라면 Head별, Layer별로 어디를 주목하는지를 상세하게 살펴볼 수도 있지만, 여기서는 간략하게 모든 Layer의 마지막 3개 Layer의 모든 Head가 가진 Attention Matrix를 평균해 가시화하였습니다. 이에 대한 코드는 아래와 같습니다.

Attention Map을 시각화하기 위한 코드

위 코드로 구현한 Encoder의 Attention Map은 아래와 같습니다.

Encoder의 Attention Map

색이 진할수록 Attention Score가 높은 것을 의미합니다. 위 Attention Map을 살펴보면 기본적으로 자기자신과는 어느정도의 상관관계를 지님과 동시에 그 주변의 다른 단어, 때로는 멀리있는 단어와도 어느정도의 관계성을 지니는 것을 확인할 수 있습니다.

Decoder의 Attention Map

위 그림은 Decoder의 Attention Map입니다. Decoder의 경우, 학습 과정에서 이후에 생성될 단어에 대해서 마스크를 씌움으로써 정답이 유출되지 않도록, 순서대로 예측이 진행되도록 하였기 때문에 위 그림처럼 자기보다 이후의 단어에 대해서는 일관적으로 0의 Attention Score를 출력한다는 특징이 있습니다.


Encoder-Decoder Attention Map

위는 Encoder-Decoder Attention(Cross Attention) 블록에서의 Attention Map입니다. 여기선 각기 Encoder와 이전 단계까지의 Decoder에서 전달된 토큰 간 Attention Score를 가시화합니다. 이처럼 언어간 번역 등의 과정에서는 어순 및 문법의 차이가 발생할 수 있기 때문에 Transformer를 활용한 구조가 필수적이 되었습니다. (그 이전의 seq2seq 방식 혹은 LSTM 등의 방식은 어순의 영향을 많이 받았습니다)


|Validation Score

마지막으로 학습시킨 모델을 객관적인 스코어 지표로 평가해보겠습니다. 일반적으로 자연어 영역에서 사용되는 지표는 크게 두 가지로, 각각 BLEU, PPL 스코어가 있습니다. 이에 대해서 간략히 소개해보겠습니다.


BLEU Score는 Bilingual Evaluation Understudy Score의 준말로, 일반적으로 기계번역의 품질을 평가하기 위해 사용됩니다. 핵심 원리는 n-gram으로 연속된 단어의 집합이 얼마나 일치하는지를 살펴보는 방식입니다. 이는 실제 정답과 번역된 결과에서 등장하는 단어가 동일할수록 높은 점수를 부여한다는 것을 의미하며, 문장의 유창성 혹은 문맥을 고려하지 못한다는 한계를 지니고 있습니다. 일반적으로 0~1 사이의 값을 가지며 높을수록 성능이 높음을 의미합니다. 여기서는 100을 곱해 표현할 것입니다.


PPL Score는 Perplexity Score의 준말로, 모델의 예측결과에 대한 분포가 얼마나 정답과 유사한지를 측정하는 방식입니다. 이로 인해 Label Smoothing이 적용된 모델들은 일반적으로 비교적 높은 PPL Score를 보이곤 합니다. 참고로 PPL Score는 1에 가까울수록 좋은 성능임을 의미하며, 높은 PPL Score는 PPL 관점에서 해당 모델의 성능이 낮다는 것을 의미합니다. PPL Score도 BLEU Score와 마찬가지로 문장의 유창성이나 문맥을 고려하지 못한다는 한계를 가지고 있습니다.

본 논문의 모델별 성능지표

본 논문에서 제시된 성능지표를 보면 base model의 경우 PPL은 4.92, BLEU는 25.8이었지만, Label Smoothing이 0.0일 때(ls를 전혀 하지 않았을 때)는 PPL은 4.67로 좋아졌고, BLEU는 25.3으로 나빠졌습니다. 반대로 Label Smoothing이 0.2일 때는 PPL은 5.47로 떨어졌고, BLEU는 25.7로 소폭 나빠졌습니다. 이는 일반적으로 Label Smoothing 자체는 BLEU Score에 긍정적이고, PPL Score에는 부정적이나 너무 많은 Label Smoothing이 이뤄지면 모델 성능 자체가 떨어질 수 있어 적절한 수준으로 진행해야 한다는 의미로 해석됩니다.


기존의 번역함수를 이용해 간단히 BLEU Score를 측정해보겠습니다. BLEU Score는 Hold Out을 통해 학습에 사용되지 않은 Test Dataset으로 평가되며, 그 코드는 아래와 같습니다.

BLEU 스코어 측정을 위한 코드

위 코드를 통해 측정된 스코어는 아래와 같습니다.

측정된 BLEU Score

이러한 스코어는 지표 외에도 어떤 데이터셋에 대해서 진행했는지도 중요합니다. 우리가 구현한 번역기가 본 논문의 지표보다 높게 나왔지만 데이터셋의 규모가 수십 배 적으니, 더욱 수월하게 점수를 얻을 수 있는 것도 사실이기 때문입니다. 이번에는 PPL Score도 측정해보겠습니다. PPL Score는 모델의 출력에 대한 확률분포가 필요하기 때문에 Translation 함수를 아래와 같이 수정해야 합니다.

PPL 측정을 위한 translation 함수

또한 PPL Score와 BLEU Score를 측정하기 위한 함수도 새로 정의합니다.

ppl-bleu score 측정을 위한 함수

위 코드를 통해 여러 개의 모델의 성능 지표를 Pandas Frame으로 출력할 수 있습니다.

위 코드를 통해 제가 학습시킨 Cross Entropy만을 사용한 모델, CE에 Label Smoothing 0.1을 적용한 모델과 Label Smoothing 0.2를 적용한 모델 총 세 개의 모델의 평가지표를 출력해보겠습니다.

모델 로드 및 스코어 출력

위 그림의 하단의 Pandas DataFrame으로 출력된 성능지표 결과를 확인할 수 있습니다. 그 결과를 살펴보면 본 논문과 유사하게 Label Smoothing이 진행될수록 PPL Score가 올라가 성능이 하락하고 있고, BLEU Score는 Label Smoothing이 0.1일 때 제일 높았습니다. 이러한 경향은 다른 데이터셋(Train, Valid)에서도 동일하게 나타났습니다.



|마치며

이번 포스팅의 내용은 굉장히 긴 편에 속합니다. 


그 이유는 몇 가지가 있는데, 첫 번째로 이후에 진행할 논문 리뷰에 Transformer가 빠지기가 힘들 것으로 예상돼, 해당 포스팅에서는 Transformer에 대한 설명을 최소화하고 싶어 최대한 자세히 설명했습니다.

두 번째로는 본 논문에 작성된 지표로 모델링을 하는데, 성능이 생각보다 안나와서 전체 구조 변경을 최소화하는 선에서 몇 가지 기법을 추가하느라 시간이 오래 걸렸습니다.

마지막으로 Label Smoothing이 적용된 Loss를 수정변경해보며 작업하다가 생각보다 시간을 많이 소모했습니다. 그 출력결과나 그래프, Attention Map을 같이 출력하며 비교분석하니 근거있는 분석이 돼 좋았지만, 시간을 꽤 잡아먹었네요.


일단 하나의 포스팅을 별다른 일이 없다면 2주 정도 간격으로 두고 작성하고자 하는데, 이번 정도의 분량이라면 지속하지 못할 것 같아 다음 포스팅부터는 본 포스팅까지의 내용을 적용해 조금 더 간략히 작성할 예정입니다.


|Reference

[1] Ashish Vaswani, Noam Shazeer, Niki Parmer, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, and Illia Polosukhin. Attention Is All You Need. https://arxiv.org/pdf/1706.03762.pdf

[2] Google, A Transformer Chatbot Tutorial with TensorFlow 2.0https://blog.tensorflow.org/2019/05/transformer-chatbot-tutorial-with-tensorflow-2.html

[3] AI Hub, 한국어-영어 번역(병렬) 말뭉치, https://aihub.or.kr/aihubdata/data/view.do?currMenu=115&topMenu=100&aihubDataSe=realm&dataSetSn=126

[4] ViT-pytorch, visualize_attention_map, https://github.com/jeonsworld/ViT-pytorch/blob/main/visualize_attention_map.ipynb


|Log

2023.11.27 | 초고 작성 및 Data Research, Data Loader, Tokenizer, Activation 일부 작성

2023.11.28 | Activation 작성 완료, Loss 정의, label smoothing loss 결과 돌려보기

2023.11.29 | Modeling, Train, Validation Code 작성 및 테스트/학습 돌려놓기

2023.11.30 | Label Smoothing이 적용 전후의 성능 비교(수치, 번역 결과)

2023.12.01 | Attention Map, Score 코드 작성 및 포스팅 마무리

2023.12.02 | Transformer 함수의 docstring 추가, github 주소 추가

2023.12.03 | 검토, github 주소 변경


                    

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari