공부/AIFFEL

Exploration 15 : 트랜스포머로 만드는 대화형 챗봇

dong_dong_2 2021. 3. 5. 21:41

1. 트랜스포머와 인코더 디코더
    - 번역기를 만드는 데 사용한 대표적인 모델인 인코더와 디코더 구조를 되짚어 보겠다.
    - 번역기는 인코더와 디코더 두 가지 아키텍처로 구성되어 있다.
       1) 인코더 : 입력문장이 들어가는 곳
       2) 디코더 : 인코더에 들어간 문장에 상응하는 출력 문장을 생성하는 곳
    - 번역기를 훈련한다는 것은 결국 입력 문장과 출력 문장 두 가지 병렬 구조로 구성된 데이터셋을 훈련한다는 의미이다.
    - 이런 병렬적으로 구성된 데이터셋을 인코더와 디코더로 학습하는 경우는 번역기에만 한정되어 있지 않다. 질문에 대한 대답을 하도록 구성된 데이터셋을 인코더와 디코더로 학습한다면, 챗봇을 만들 수 있을 것이다.
    - 트랜스포머의 인코더와 디코더
       1) 트랜스포머라는 것도 변역기와 마찬가지로 인코더와 디코더 구성을 가지고 있다. 입력 문장을 넣으면 출력 문장을 출력한다.
       2) 다음은 트랜스포머의 구조이다.


       3) 위의 블랙박스로 가려진 트랜스포머의 내부구조를 자세히 보면 아래의 그림과 같다.


       4) 위의 그림에서 초록색 도형을 인코더 층(Encoder layer), 핑크색 도형을 디코더(Decoder layer)라고 했을 때, 입력 문장은 누적되어 쌓아 올린 인코더의 층을 통해서 정보를 뽑아내고, 디코더는 누적되어 쌓아 올린 디코더의 층을 통해서 출력 문장의 단어를 하나씩 만들어가는 구조를 갖고 있다.
       5) 그리고 그 내부를 더 자세히 보면 아래의 그림과 같다. 다음으로는 아래의 그림의 모듈을 하나씩 살펴보겠다.


2. 트랜스포머의 입력 이해하기
    - 많은 자연어 처리 모델들은 텍스트 문장을 입력으로 받기 위해 단어를 임베딩 벡터로 변환하는 벡터화 과정을 거친다. 트랜스포머도 다르지 않다.
    - 하지만 트랜스포머 모델의 입력데이터 처리는 RNN 계열의 모델들과 한 가지 다른점이 있는데, 바로 임베딩 벡터에 어떤 값을 더해준 뒤에 입력으로 사용한다는 점이다. 그 값은 포지셔널 인코딩(positional Encoding)이라는 부분에 해당된다.
    - 이러한 것을 해주는 이유는 트랜스포머는 입력을 받을 때, 문장에 있는 단어들을 1개씩 순차적으로 받는 것이 아닌, 문장에 있는 모든 단어를 한꺼번에 입력으로 받기 때문이다. 트랜스포머가 RNN과 결정적으로 다른 점이 이 부분이다.
    - RNN에는 문장을 구성하는 단어들이 어순대로 모델에 입력되므로, 모델에게 따로 어순 정보를 알려줄 필요가 없다.
    - 하지만 문장에 있는 모든 단어를 한꺼번에 문장 단위로 입력받는 트랜스포머는 어순 정보가 필요하다. 같은 단어라도 그 단어가 문장의 몇 번째 어순으로 입력되는지를 모델에 추가로 알려주기 위해, 단어의 임베딩 벡터에다가 위치 정보를 가진 벡터(Positional Encoding) 값을 더해서 모델의 입력으로 삼는다.
    - 포지셔널 인코딩의 벡터값은 사인함수와 코사인함수를 이용하여 정한다. 사인 함수와 코사인 함수의 그래프를 상기해보면 요동치는 값의 형태를 생각해 볼 수 있다.
    - 트랜스포머는 사인 함수와 코사인 함수의 값을 임베딩 벡터에 더해줌으로써 단어의 순서 정보를 더해 준다.


    - 포지셔널 수식을 찾아보면 위와 같은 수식을 발견할 수 있을 것이다.
       1) d_model은 임베딩 벡터의 차원을 뜻한다.
       2) pos는 입력 문장의서의 임베딩 벡터의 위치를 나타낸다.
       3) i는 임베딩 벡터 내의 차원의 인덱스를 의미한다.
3. 어텐션? 어텐션!
    - 어텐션 함수는 주어진 쿼리(Query)에 대해서 모든 키(Key)와의 유사도를 각각 구한다. 그리고 구해낸 유사도를 키(Key)와 맵핑되어있는 각각의 값(Value)에 반영해준다. 그리고 유사도가 반영된 값(Value)을 모두 더해서 뭉쳐주면 최종 결과인 어텐션 값(Attention Value)가 나온다.
    - 트랜스포머는 총 세 가지의 어텐션을 사용한다. 이 중 2가지가 셀프 어텐션이다.
       1) 인코더 셀프 어텐션 : 인코더의 입력으로 들어간 문장 내 단어들이 서로 유사도를 구한다.
       2) 디코더 셀프 어텐션 : 단어를 1개씩 생성하는 디코더가 이미 생성된 앞 단어들과의 유사도를 구한다.
       3) 인코더-디코더 어텐션 : 디코더가 잘 예측하기 위해서 인코더에 입력된 단어들과 유사도를 구한다.
    - 트랜스포머의 어텐션 함수에 사용되는 쿼리(Query), 키(Key), 밸류(Value)는 기본적으로 단어(정보를 함축한)벡터이다.
    - 여기서 단어 벡터란 초기 입력으로 사용되었던 임베딩 벡터가 아니라, 트랜스포머의 여러 연산을 거친 후의 단어 벡터를 말한다.
    - 셀프 어텐션(Self Attention)
       1) 셀프 어텐션이란 유사도를 구하는 대상이 다른 문장의 단어가 아니라 현재 문장 내의 단어들이 서로 유사도를 구하는 경우를 말한다.
       2) 위에서 언급한 인코더-디코더 어텐션은 서로 다른 단어 목록(인코더 내 단어와 디코더 내 단어) 사이에서 유사도를 구하기에 셀프 어텐션이 아니다.
4. 스케일드 닷 프로덕트 어텐션
    - 어텐션이 단어들 간의 유사도를 구하는 메커니즘이라 배웠다. 그렇다면 유사도를 구하는 방법이 있을 것이다.


    - 위에 나온 수식이 트랜스포머에서 어텐션 값을 구하는 방법이다. Q, K, V는 각각 쿼리(Query), 키(Key), 값(Value)를 나타낸다.
    - 수식을 그림으로 정리할 수 있게 3가지를 기억하자.
       1) Q, K, V는 단어 벡터를 행으로 하는 문장 행렬이다.
       2) 벡터의 내적(dot product)은 벡터의 유사도를 의미한다.
       3) 특정 값을 분모로 사용하는 것은 값의 크기를 조절하는 스케일링(Scaling)을 위함이다.
    - 위의 수식은 내적(dot product)를 통해 단어 벡터 간 유사도를 구한 후에, 특정 값을분모로 나눠주는 방식으로 Q와 K의 유사도를 구하였다고 하여 스케일드 닷 프로덕트 어텐션(Scaled Dot Product Attention)이라 한다.
    - 만약에 분모에 특정 값을 나눠주는 부분을 사용하지 않았다면 이름이 어떻게 됬을까? -> 닷 프로덕트 어텐션(dot product attention)이라 부른다.
5. 머리가 여러 개인 어텐션
    - 만약 시험을 여러 사람이 같이 풀게 되면 더 좋은 점수를 받을 수 있을 것이다. 그럼 기계가 여러 개면 더 쉽게 학습 할 수 있지 않을까?
    - 트랜스포머에서 num_heads라는 변수는 기계가 몇 개의 똑똑한 머리를 사용할지, 즉 병렬적으로 몇 개의 어텐션 연산을 수행할지를 결정하는 하이퍼파라미터이다. (d_model = d_v * num_heads)
    - 위에서 포지셔널 인코딩에서 d_model은 임베딩 벡터의 차원이라고 했다. 결국 트랜스포머의 초기 입력인 문장 행렬의 크기는 문장의 길이를 행으로, d_model을 열의 크기로 가진다.
    - 트랜스포머는 입력된 문장 행렬을 num_heads의 수만큼 쪼개서 어텐션을 수행하고, 이렇게 얻은 num_heads의 개수만큼의 어텐션 값 행렬을 다시 하나로 concatenate한다.
    - 멀티-헤드 어텐션
       1) num_heads의 값이 8일 때, 병렬로 수행되는 어텐션이 서로 다른 셀프 어텐션 결과를 얻을 수 있을 것이다.
       2) 다르게 말하자면, 8개의 머리는 각각 다른 관점에서 어텐션을 수행하므로 한 번의 어텐션만 수행했다면 놓쳤을지 모를 정보를 캐치할 수 있다.
       3) 이와 같이 어텐션을 병렬로 수행하는 것을 멀티 헤드 어텐션이라고 한다.
       4) 이 멀티 헤드 어텐션을 구현할 때 내부적으로 스케일드 닷 프로덕트 어텐션 함수를 호출하게 된다.
6. 마스킹
    - 마스킹(Masking) : 특정 값들을 가려서 실제 연산에 방해가 되지 않도록 하는 기법
    - 트랜스포머에서는 어텐션을 위해 크게 두 가지 마스킹을 사용한다.
    - 패딩 마스킹(Padding Masking)
       1) 자연어 처리에서 패딩(Padding)이란 문장의 길이가 서로 다를 때, 모든 문장의 길이를 동일하게 해주는 과정에서 정해준 길이보다 짧은 문장의 경우에 숫자 0을 채워서 문장의 길이를 맞춰주는 자연어 처리의 전처리 방법이다.
       2) 그런데 이렇게 주어진 숫자 0은 실제 의미가 있는 단어가 아니므로 실제 어텐션 등과 같은 연산에서는 제외할 필요가 있다.
       3) 패딩 마스킹은 이를 위해 숫자 0의 위치를 체크한다.
       4) 패딩 마스킹 함수는 정수 시퀀스를 입력으로 하면 숫자가 0인 부분을 체크한 벡터를 리턴한다.
       5) 정수 시퀀스에 대해서 결과가 출력되는데, 오직 숫자가 0인 위치에서만 숫자 1이 나오고, 숫자 0이 아닌 위치에서는 숫자 0인 벡터를 출력한다.
       6) 어텐션 연산 시 패딩마스킹을 참고하면 불필요하게 숫자 0을 참고하지 않게 할 수 있다.
    - 룩 어헤드 마스킹(Look-ahead masking, 다음 단어 가리기)
       1) 순환 신경망, RNN과 트랜스포머는 문장을 입력받을 때 방법이 다르다.
       2) RNN은 step이라는 개념이 있어서 각 step마다 단어가 순서대로 입력으로 들어가는 구조이지만, 트랜스포머의 경우에는 문장 행렬을 만들어 한 번에 행렬 형태로 입력이 들어간다는 특징이 있다.
       3) 이 특징 때문에 추가적인 마스킹(Masking)을 필요하게 된다.
       4) 트랜스포머가 입력 문장을 전체 문장 행렬로 들어가기 때문에 위치와 상관없이 모든 단어를 참고해서 다음 단어를 예측할 수 있다.
       5) 하지만 우리가 원하는 것은 이전 단어로부터 다음 단어를 예측하는 훈련을 제대로 하게 하는 것이기에 자신보다 다음에 나올 단어를 참고하지 않도록 가리는 기법이 필요한데 이것이 룩 어헤드 마스킹 기법이다.
7. 인코더
    - 하나의 인코더 층은 크게 총 2개의 서브 층(sublayer)으로 나눠진다.
    - 바로 셀프 어텐션과 피드 포워드 신경망이다. 셀프 어텐션은 멀티 헤드 어텐션으로 병렬적으로 이루어진다. 실습을 통해 구현하였다.
    - 구현한 인코더 층을 임베팅 층(Embedding layer)와 포지셔널 인코딩(Positional Encoding)을 연결하고, 사용자가 원하는 만큼 인코더 층을 쌓음으로 트랜스포머의 인코더가 완성된다.
    - 인코더와 디코더 내부에는 각 서브층 이후에 훈련을 돕는 Layer Normalization이라는 테크닉이 사용되었다.
    - 트랜스포머는 하이퍼파라미터인 num_layers 개수의 인코더 층을 쌓는다. 논문에서는 6개의 인코더 층을 쌓았지만, 여기서는 시간을 고려해서 적은 개수를 사용했다.
8. 디코더
    - 디코더는 인코더와 비슷하지만, 인코더보다 조금 복잡하다. 인코더는 두 개의 서브 층으로 구성되지만, 디코더는 세 개의 서브 층으로 구성된다는 것이 다르다.
    - 첫 번째는 셀프 어텐션, 두 번째는 인코더-디코더 어텐션, 세 번째는 피드 포워드 신경망으로 구성된다.
    - 인코더-디코더 어텐션은 셀프 어텐션과는 달리, Query가 디코더의 벡터인 반면에 Key와 Value가 인코더의 벡터라는 특징이 있다. 이 부분이 인코더가 입력 문장으로부터 정보를 디코더에 전달하는 과정이다.
    - 인코더의 셀프 어텐션과 마찬가지로 디코더의 셀프 어텐션, 인코더-디코더 어텐션 두 개의 어텐션 모두 스케일드 닷 프로덕트 어텐션을 멀티 헤드 어텐션으로 병렬적으로 수행한다. 디코더도 코드로 구현했다.
    - 구현한 디코더의 층은 임베팅 층(Embedding layer)과 포지셔널 인코딩(Positional Encoding)을 연결하고, 사용자가 원하는 만큼 디코더 층을 쌓아 트랜스포머의 디코더가 완성된다.
    - 인코더와 마찬가지로 num_layers 개수의 디코더 층을 쌓습니다. 논문에서는 총 6개의 디코더 층을 사용했지만, 여기서는 시간을 고려해서 더 적게 쌓았다.
9. 챗봇의 병렬 데이터 받아오기
    - 데이터를 읽어오고, 정규표현식을 이용해서 데이터를 전처리했다.
10. 병렬 데이터 전처리
    - 위에서 데이터에서 질문과 답변의 셋을 각각 questions와 answers에 저장했다. 여기서는 본격적으로 전처리를 진행해 보겠다. 이번에 할 전체적인 과정을 요약하면 아래와 같다.
       1) TensorFlow Datasets SubwordTextEncoder를 토크나이저로 사용한다. 단어보다 더 작은 단위인 Subword를 기준으로 토크나이징하고, 각 토큰을 고유한 정수로 인코딩한다.
       2) 각 문장을 토큰화하고 각 문장의 시작과 끝을 나타내는 START_TOKEN 및 END_TOKEN을 추가한다.
       3) 최대 길이 MAX_LENGTH인 40을 넘는 문장들은 필터링한다.
       4) MAX_LENGTH보다 길이가 짧은 문장들은 40에 맞도록 패딩한다.
    - 단어장(Vocabulary) 만들기 : TensorFlow Datasets SubwordTextEncoder를 이용하여 만들었다.
    - 각 단어를 고유한 정수로 인코딩(Integer encoding) & 패딩(Padding)
    - 교사 강요(Teacher Forcing) 사용하기
11. 모델 정의 및 학습하기
    - 앞에서 사용한 인코더 층 함수와 디코더 층 함수를 사용하여 트랜스포머 함수를 정의한다. 실습으로 코드 구현했다.
       1) 모델 생성
       2) 손실함수 정의
       3) 커스텀된 학습률 정의 : 딥러닝 모델학습 시 초기에는 학습률을 급격히 높였다가, 이후 train step이 진행됨에 따라 서서히 낮추어 가면서 안정적으로 수렴하게 하는 고급 기법을 널리 사용한다.
       4) 모델 컴파일
       5) 훈련하기
12. 챗봇 테스트하기
    - 예측(inference) 단계는 기본적으로 다음과 같은 과정을 거친다.
       1) 새로운 입력 문장에 대해서 훈련 때와 동일한 전처리를 한다.
       2) 입력 문장을 토크나이징하고, START_TOKEN과 END_TOKEN을 추가한다.
       3) 패딩 마스킹과 룩 어헤드 마스킹을 계산한다.
       4) 디코더는 입력 시퀀스로부터 다음 단어를 예측한다.
       5) 디코더는 예측된 다음 단어를 기존의 입력 시퀀스에 추가하여 새로운 입력으로 사용한다.
       6) END_TOKEN이 예측되거나 문장의 최대 길이에 도달하면 디코더는 동작을 멈춘다.
    - 위에 과정을 담은 함수를 만들어서 챗봇을 테스트했다.
    - 지금까지의 과정을 영어 문장 데이터로 했다.
13. 프로젝트 : 한국어 데이터로 챗봇 만들기 : 앞에서 배운 과정을 응용하여 한국어 챗봇을 만들었다.