[논문 리뷰] Bert : Pre-training of Deep Bidirectional Transformers for Language Understanding

이영락·2024년 9월 16일

CV & NLP 논문 리뷰

목록 보기
6/14

2주차

Author: Jacob Devlin Ming-Wei Chang Kenton Lee Kristina Toutanova
Published Date: 2019년 3월 1일
keyword: Vit, attention
status: not yet
스터디 주제: 주제: BERT 모델의 기본 개념
논문: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
내용:
• BERT 모델 구조 및 특징 이해 (Bidirectional Encoder Representations)
• Pre-training과 Fine-tuning 개념 및 활용 방법 중요, task 별 이해
• BERT와 GPT 모델의 차이점 (개념적 비교)
• BERT의 NLP 태스크 성능 향상에 미친 영향
실습: BERT를 활용한 간단한 문장 분류 모델 구현

주차: 2주차

참고자료


[최대한 자세하게 설명한 논문리뷰] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding (1)

[NLP | 논문리뷰] BERT : Pre-training of Deep Bidirectional Transformers for Language Understanding 상편

[딥러닝 자연어처리] BERT 이해하기

1 | 논문정리


1. Introduction

BERT는 "Bidirectional Encoder Representations from Transformers"의 약자로, 구글 AI 연구팀에서 개발한 자연어 처리(NLP) 모델입니다. BERT는 텍스트의 좌우 문맥을 동시에 학습하는 양방향 트랜스포머(Transformer) 구조를 기반으로 합니다. 기존의 언어 모델들이 주로 단방향(좌에서 우 혹은 우에서 좌)으로 문장을 처리한 것과 달리, BERT는 마스크 언어 모델(Masked Language Model, MLM)을 통해 문장 전체의 양방향 문맥을 동시에 학습할 수 있는 것이 가장 큰 특징입니다.

BERT는 두 가지 주요 과제인 Masked Language Model(MLM)Next Sentence Prediction(NSP)을 통해 사전 학습을 진행하며, 이후 다양한 NLP 과제에 미세 조정(fine-tuning)을 통해 적용됩니다. 이를 통해 BERT는 다양한 자연어 처리 과제에서 기존의 모델들을 능가하는 성능을 보여줍니다.

BERT의 기본 구조: Pre-training과 Fine-Tuning

Pre-training 방법과 Fine-Tuning의 배경

BERT가 등장하기 이전에도 언어 모델(LM)의 pre-training 방법은 활발히 연구되었으며, 이미 좋은 성능을 내고 있었다. 특히 문장 단위의 작업에서 뛰어난 성능을 보였고, 이러한 연구들은 두 문장 사이의 관계를 분석하여 예측하는 것을 목표로 했다. 또한, 문장뿐만 아니라 토큰 단위의 작업(예: 개체명 인식, 질문응답 시스템)에서도 성능이 우수했다. 토큰 단위 작업에서는 fine-grained output을 생성해야 하는데, 이는 하나의 출력값을 도출하기 위해 더 작은 단위의 출력 프로세스로 나눠 처리하는 것이다.

fine-grained output이란?

fine-grained output이란 하나의 output을 내기 위해 작은 단위의 output 프로세스로 나눈 뒤 수행하는 것을 의미한다.

Pre-trained 언어 표현을 down stream task에 적용하는 방법에는 크게 두 가지가 있다.

  1. Feature-based Approach
    • 대표적으로 ELMo가 해당되며, pre-trained representations을 down stream 작업에 추가적인 feature로 사용하는 방법이다.
    • ELMo는 bidirectional language model에서 얻은 표현을 embedding vector와 단순히 concat(병합)하여 사용한다.
  2. Fine-Tuning Approach
    • 대표적인 예는 OpenAI의 GPT(Generative Pre-trained Transformer)가 있다. 이 접근 방식에서는 task-specific한(fine-tuned) 파라미터 수를 최소화하면서, pre-trained된 모든 파라미터를 조금씩 수정해 down stream 작업을 학습한다.

ELMo, GPT, BERT 비교

공통점과 차이점

Feature-based 접근법과 Fine-tuning 접근법 모두 pre-training 과정에서 동일한 목적함수(objective function)을 사용한다. 일반적으로 언어 표현을 학습하기 위해서는 unidirectional(단방향) language model을 사용하는데, 여기서 중요한 차이점들이 발생한다.

  • ELMo는 순방향 언어 모델과 역방향 언어 모델을 각각 사용하기 때문에 bidirectional language model처럼 보일 수 있다. 그러나 실제로는 각각의 단방향 언어 모델의 출력을 concat하여 사용하는 방식이므로, 엄밀히 말해 모델 자체는 단방향 모델이다.
  • BERT는 이에 반해 진정한 deep bidirectional 모델로서, 문장의 양방향 문맥을 모두 활용하는 것이 핵심이다. 이는 기존의 단방향 언어 모델인 GPT와 차별화되는 중요한 특징이다.

Fine-Tuning Approach의 한계

논문에서는 기존 방법(특히 fine-tuning 방식)이 pre-training된 표현의 성능을 저하시킬 수 있다고 지적하고 있다. 예를 들어 GPT와 같은 단방향 언어 모델의 경우 모든 토큰이 이전 토큰과의 attention만을 계산하기 때문에 문장 수준 작업에서는 최적의 방법이 아니며, 단방향 언어 모델로 인한 성능 제한이 발생할 수 있다. 이에 비해 BERT는 양방향 문맥을 활용하여 이 문제를 해결하고 더 나은 성능을 발휘한다.


BERT의 Deep Bidirectional 특징

양방향성의 중요성

BERT가 특히 강조하는 점은 deep bidirectional context의 중요성이다. 이를 통해 문장의 양방향 문맥을 모두 포함하여 더 풍부한 언어 표현을 학습할 수 있다. 이를 위해 BERT는 Masked Language Model(MLM)을 pre-training 목적으로 도입하여 기존의 단방향 언어 모델의 제약을 완화시켰다.

Masked Language Model(MLM)의 작동 방식

  • MLM은 입력 문장의 일부 토큰을 임의로 마스킹하고, 그 마스킹된 토큰을 문맥에 기반하여 정확히 예측하는 것을 목표로 한다. 이 과정에서 양방향 문맥을 모두 활용할 수 있게 되어, deep bidirectional transformer의 구현이 가능해진다.
  • 또한, MLM으로 text-pair representations을 사전 학습하면, BERT는 Next Sentence Prediction 작업도 수행할 수 있게 되어 문장 간 관계를 학습하는 데 더 효과적인 모델이 된다.

🧑‍💻 자연어 처리(NLP)에서 사전 학습된 언어 표현을 사용하는 다양한 접근 방식들이 소개

1) 비지도 학습 특징 기반 접근법(Unsupervised Feature-based Approaches)

2) 비지도 학습 미세 조정 접근법(Unsupervised Fine-tuning Approaches)

3) 지도 학습 데이터에서 전이 학습(Transfer Learning from Supervised Data)

2.1 비지도 학습 특징 기반 접근법 (Unsupervised Feature-based Approaches)

  • 단어의 일반적인 표현을 학습하는 것은 오랜 연구 주제이며, 초기에는 비신경망 방법론(Brown et al., 1992; Ando and Zhang, 2005)과 신경망 기반 방법론(Mikolov et al., 2013; Pennington et al., 2014)이 사용되었습니다.
  • 미리 학습된 단어 임베딩은 현대 NLP 시스템에서 중요한 부분을 차지하며, 처음부터 학습된 임베딩보다 더 나은 성능을 보여줍니다(Turian et al., 2010).
  • 전통적으로 왼쪽에서 오른쪽으로 텍스트를 처리하는 언어 모델링 기법(Mnih and Hinton, 2009)이나, 양쪽 문맥에서 올바른 단어와 잘못된 단어를 구별하는 방식(Mikolov et al., 2013)이 사용되었습니다.
  • 이후 문장 임베딩(Kiros et al., 2015; Logeswaran and Lee, 2018)이나 문단 임베딩(Le and Mikolov, 2014)과 같은 더 큰 단위로 일반화되었습니다.
  • ELMo(Peters et al., 2017, 2018a)는 기존의 단어 임베딩 연구를 확장하여 좌->우와 우->좌 언어 모델에서 각각 문맥에 따라 달라지는 특징을 추출합니다. 이 방법은 여러 NLP 벤치마크에서 뛰어난 성과를 보여주었으며, 주요 과제로는 질문 응답(Rajpurkar et al., 2016), 감정 분석(Socher et al., 2013), 개체명 인식(Tjong Kim Sang and De Meulder, 2003) 등이 있습니다.
  • *Melamud et al. (2016)**은 양방향 문맥에서 단어를 예측하는 작업을 통해 문맥 표현을 학습하는 방법을 제안하였습니다.

2.2 비지도 학습 미세 조정 접근법 (Unsupervised Fine-tuning Approaches)

  • 이 접근 방식은 처음에는 비지도 학습된 단어 임베딩 파라미터만을 사용했지만(예: Collobert and Weston, 2008), 최근에는 문장이나 문서 인코더가 비지도 학습된 후, 지도 학습 과제에 맞게 미세 조정됩니다(Dai and Le, 2015; Howard and Ruder, 2018; Radford et al., 2018).
  • 이 방식의 주요 장점은 미세 조정 시 학습해야 할 파라미터가 적다는 것입니다.
  • OpenAI GPT(Radford et al., 2018)는 사전 학습된 파라미터를 미세 조정하여 다양한 문장 수준의 작업에서 매우 높은 성능을 보여주었으며, 대표적인 성과로 GLUE 벤치마크에서의 뛰어난 성적을 들 수 있습니다.

2.3 지도 학습 데이터에서 전이 학습 (Transfer Learning from Supervised Data)

  • 대규모 데이터셋을 사용한 지도 학습에서의 전이 학습도 효과적임이 입증되었습니다. 예를 들어, 자연어 추론(Conneau et al., 2017)과 기계 번역(McCann et al., 2017) 분야에서의 성과가 대표적입니다.
  • 컴퓨터 비전 분야에서도 대규모 사전 학습된 모델에서 전이 학습을 적용하는 것이 중요하다는 연구가 있었으며, 특히 ImageNet으로 미리 학습된 모델을 미세 조정하는 것이 효과적인 방식으로 입증되었습니다(Deng et al., 2009; Yosinski et al., 2014).

Chat GPT VS BERT

3. Bart


BERT(Bidirectional Encoder Representations from Transformers)는 두 가지 주요 단계로 구성

  1. 사전 학습(Pre-training) : 레이블이 없는 데이터를 사용하여 다양한 학습 과제를 수행하며 모델을 학습
  2. 미세 조정(Fine-tuning) : 사전 학습된 파라미터로 모델을 초기화한 후, 각 다운스트림 과제의 레이블이 있는 데이터를 사용하여 모델 전체를 미세 조정

항상 동일한 Pre-trained model의 파라미터서로 다른 downstream tasks(QA, 번역 등) 초기 값으로 사

BERT의 통합 아키텍처

  • 다양한 과제에서 동일한 통합 아키텍처를 사용함.
  • 사전 학습된 아키텍처와 다운스트림 과제의 최종 아키텍처 간의 차이는 최소화.

3.1 Model Architecture

: multi-layer bidirectional Transformer encoder(양방향 Transformer encoder를 여러 층 쌓은 것)

BERT의 아키텍처는 다층의 양방향 트랜스포머(Transformer) 인코더로 구성

BERT의 모델은 트랜스포머 블록의 수(L), 히든 크기(H), 그리고 셀프 어텐션 헤드 수(A)로 정의. BERT에는 두 가지 주요 모델 크기가 있습니다:

  • BERT-BASE: 트랜스포머 블록 수 L=12, 히든 크기 H=768, 셀프 어텐션 헤드 수 A=12, 총 파라미터 1억 1천만 개
  • BERT-LARGE: 트랜스포머 블록 수 L=24, 히든 크기 H=1024, 셀프 어텐션 헤드 수 A=16, 총 파라미터 3억 4천만 개

BERT-BASE는 OpenAI GPT와 비교할 수 있도록 같은 크기로 설계되었지만, GPT는 단방향(좌->우)으로 문맥을 처리하는 반면, BERT는 양방향 문맥을 모두 처리하는 차별점을 가지고 있습니다.

3.2 입력/출력 표현 (Input/Output Representations)

BERT는 다양한 다운스트림 과제를 처리할 수 있도록 입력 표현 방식을 설계.

BERT의 입력은 단일 문장이나 문장 쌍을 명확히 표현해야함!! 여기서 '문장'은 실제 언어적 문장이 아니라 연속된 텍스트의 임의의 부분을 의미하며, '시퀀스'는 BERT에 입력되는 토큰 시퀀스를 의미합니다.

→ 총 3가지 Embedding vector(Token Embeddings, Segment Embeddings, Position Embeddings)를 합쳐 input으로 활용

BERT는 WordPiece 임베딩(Wu et al., 2016)을 사용하며, 총 30,000개의 토큰 어휘를 갖고 있습니다. 모든 시퀀스의 첫 번째 토큰은 항상 [CLS]라는 특수 분류 토큰으로 시작하며, 이 토큰의 최종 히든 상태는 분류 작업에서 전체 시퀀스를 나타내는 데 사용됩니다. 문장 쌍은 하나의 시퀀스로 결합되며, 각 문장은 [SEP] 토큰으로 구분됩니다. 또한, 각 토큰에는 해당 토큰이 문장 A에 속하는지 문장 B에 속하는지를 나타내는 학습된 임베딩이 추가됩니다.

3.3 사전 학습 (Pre-training BERT)

BERT는 전통적인 좌->우 또는 우->좌 언어 모델을 사용하지 않고, 두 가지 비지도 학습 과제를 통해 사전 학습을 진행합니다.

과제 #1: Masked Language Model (MLM)

: Masked LM(MLM)이란, input tokens의 일정 비율을 마스킹하고, 마스킹 된 토큰을 예측하는 과정.

BERT는 기존의 언어 모델의 단방향 문맥 학습을 극복하기 위해 입력 토큰의 일부를 무작위로 마스킹한 후, 이 마스킹된 토큰을 예측하는 Masked Language Model(MLM) 방법을 사용합니다. Bert는 오직 [MASK] token만을 예측. BERT는 시퀀스의 모든 WordPiece 토큰 중 15%를 무작위로 마스킹하여 이 과제를 수행합니다.

그러나 사전 학습 중에 [MASK] 토큰을 사용하면, 실제 다운스트림 과제에서는 이 토큰이 나타나지 않기 때문에 사전 학습과 미세 조정 간에 차이가 발생할 수 있습니다. 이를 해결하기 위해 BERT는 마스킹된 토큰을 [MASK]로 대체하는 확률을 80%로 설정하고, 나머지 10%는 무작위 토큰, 또 다른 10%는 원래 토큰을 유지하는 방식으로 처리합니다.

  • 80%의 경우 : token을 [MASK] token으로 바꾼다. ex) my dog is hairy -> my dog is [MASK]
  • 10%의 경우 : token을 random word로 바꾼다. ex) my dog is hairy -> my dog is apple
  • 10%의 경우 : token을 원래 단어 그대로 놔둔다. ex) my dog is hairy -> my dog is hairy

과제 #2: Next Sentence Prediction (NSP)

다양한 다운스트림 과제(예: 질문 응답, 자연어 추론 등)는 두 문장 간의 관계를 이해하는 것이 중요. 이를 학습하기 위해, BERT는 다음 문장이 실제로 앞 문장의 다음 문장인지를 예측하는 Next Sentence Prediction (NSP) 과제를 사용. 각 사전 학습 예제에서 문장 A와 문장 B는 50%의 확률로 실제로 이어진 문장이며, 나머지 50%의 확률로는 임의의 다른 문장이 주어집니다.

즉, pre-training example로 문장A와 B를 선택할때, 50퍼센트는 실제 A의 다음 문장인 B(IsNext), 나머지 50퍼센트는 랜덤 문장 B(NotNext) 고른다는 것이다.

🧑‍💻

💡예를 들자면, 아래와 같은 예시가 50:50의 비율로 등장한다는 것이다.

Input = [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP] Label = IsNext

Input = [CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight ##less birds [SEP] Label = NotNext

사전 학습 데이터

BERT의 사전 학습 과정은 기존의 언어 모델 사전 학습 문헌을 따름. BERT는 BooksCorpus(8억 단어)와 영어 Wikipedia(25억 단어)를 사용하여 사전 학습됩니다. Wikipedia에서는 텍스트 부분만을 사용하고, 목록, 표, 헤더 등은 제외합니다. 이는 문장 수준의 코퍼스 대신 문서 수준의 코퍼스를 사용하여 더 긴 연속적인 시퀀스를 학습하는 데 도움을 줍니다.

3.4 미세 조정 (Fine-tuning BERT)

BERT의 미세 조정 과정은 매우 간단합니다. 트랜스포머의 셀프 어텐션 메커니즘 덕분에, BERT는 단일 텍스트나 텍스트 쌍을 다루는 다양한 다운스트림 과제를 처리할 수 있습니다. 예를 들어, 질문-문단 쌍 또는 문장-가설 쌍과 같은 텍스트 쌍을 입력으로 받는 작업을 처리할 수 있습니다.

BERT에서는 모든 다운스트림 과제에서 동일한 사전 학습된 모델을 사용하며, 과제별로 적합한 입력과 출력을 추가한 후 전체 파라미터를 미세 조정합니다. 예를 들어, 질문 응답(QA) 과제에서는 질문과 문단 쌍이 입력으로 들어가고, [CLS] 토큰의 최종 히든 상태는 분류 과제에서 사용됩니다. 또한, 시퀀스 태깅 또는 질문 응답과 같은 토큰 수준의 작업에서는 각 토큰의 히든 상태가 출력 레이어로 연결됩니다.

수행하고자하는 downstream task

  1. Sentence pairs in paraphrasing
  2. Hypothesis-Premise pairs in entailment
  3. Question-Passage pairs in question answering
  4. Degenerate-None pair in text classification or sequence tagging

Output역시 downstream task에 따라 달라진다.

  1. token representation in sequence tagging or question answering
  2. [CLS] representation in classification(entailment or sentiment analysis)

미세 조정은 사전 학습에 비해 상대적으로 비용이 적게 듭니다. 모든 실험은 단일 Cloud TPU에서 1시간 이내에 재현할 수 있으며, GPU 환경에서는 몇 시간 정도 소요됩니다.


02 | 논문 탐구


🚥 주제 1 : BERT 모델 구조 및 특징 이해 (Bidirectional Encoder Representations)
  • Bidirectional Encoder Representations:
    • 양방향성: BERT는 Transformer의 Encoder를 사용하여 문맥의 좌우 정보를 모두 활용합니다. 이는 단어의 의미를 이해하는 데 있어서 앞뒤 문맥을 모두 고려할 수 있게 합니다.
    • Transformer 구조: Self-Attention 메커니즘을 통해 문장 내 모든 단어 간의 관계를 학습합니다.
🚥 주제 2 : Pre-training과 Fine-tuning 개념 및 활용 방법 ***중요, task 별 이해***
  • Pre-training(사전 학습):
    • Masked Language Modeling (MLM): 입력 문장에서 전체 토큰의 15%를 무작위로 마스킹하고, 해당 마스킹된 단어를 예측하도록 모델을 학습시킵니다.
      • 마스킹된 위치의 단어는 80%는 [MASK] 토큰으로, 10%는 무작위 단어로, 10%는 원래 단어 그대로 둡니다.
    • Next Sentence Prediction (NSP): 두 문장이 주어졌을 때, 두 번째 문장이 첫 번째 문장의 다음 문장인지 여부를 예측합니다.
  • Fine-tuning(미세 조정):
    • 사전 학습된 BERT 모델에 태스크별 출력층을 추가하고, 다운스트림 태스크의 데이터로 전체 모델을 재학습합니다.
    • 예: 감정 분석, 질문 응답, 개체명 인식 등 다양한 NLP 태스크에 적용됩니다.
🚥 주제 3 : BERT와 GPT 모델의 차이점 (개념적 비교)
  • BERT:
    • 양방향성: 입력 텍스트의 양방향 정보를 모두 활용합니다.
    • 구조: Transformer의 Encoder 사용.
    • 주요 태스크: 텍스트 이해 및 분류.
  • GPT:
    • 단방향성: 입력 텍스트의 좌측 맥락만을 사용하여 다음 단어를 예측합니다.
    • 구조: Transformer의 Decoder 사용.
    • 주요 태스크: 텍스트 생성.

BERT-BASE는 OpenAI GPT와 비교할 수 있도록 같은 크기로 설계되었지만, GPT는 단방향(좌->우)으로 문맥을 처리하는 반면, BERT는 양방향 문맥을 모두 처리하는 차별점을 가지고 있습니다.

🚥 주제 4 : BERT의 NLP 태스크 성능 향상에 미친 영향
  • State-of-the-Art 달성: 다양한 NLP 벤치마크에서 기존 모델을 능가하는 성능을 보여주었습니다.
  • 전이 학습의 효율성: 사전 학습과 미세 조정의 조합으로 소량의 데이터로도 높은 성능을 얻을 수 있습니다.
  • 범용성: 단일 모델로 여러 태스크에 적용 가능하여 모델 개발의 효율성을 높였습니다.

03 | 실습 : Bert 코드 구현


pytorch로 BERT 구현하기 - 이론

BERT - pytorch 구현

BERT - (2) Transformer 이해하기, 코드 구현

1. 환경 설정

pip install transformers
pip install torch

2. 코드 구현

import torch
from transformers import BertTokenizer, BertForSequenceClassification

# 토크나이저와 모델 불러오기
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained('bert-base-uncased')

# 입력 데이터 준비
texts = ["I love this movie!", "I didn't like this film."]
labels = [1, 0]  # 긍정:1, 부정:0

# 토크나이징
inputs = tokenizer(texts, return_tensors='pt', padding=True, truncation=True)

# 레이블 텐서화
labels = torch.tensor(labels)

# 모델에 입력 및 출력 얻기
outputs = model(**inputs, labels=labels)

# 손실 및 로짓 확인
loss = outputs.loss
logits = outputs.logits

print(f"Loss: {loss}")
print(f"Logits: {logits}")

3. 코드 설명

  • 토크나이저: 문장을 토큰화하고 모델 입력 형식에 맞게 변환합니다.
  • BertForSequenceClassification: 문장 분류를 위한 BERT 모델로, 출력층에 분류기가 추가되어 있습니다.
  • 손실(loss): 모델의 예측과 실제 값 간의 차이를 나타내며, 모델 학습 시 최소화됩니다.
  • 로짓(logits): 분류 결과 이전의 원시 출력 값으로, 소프트맥스 함수를 통해 확률로 변환할 수 있습니다.

4. 추가 학습

  • 옵티마이저 설정 및 학습 루프
from torch.optim import AdamW

optimizer = AdamW(model.parameters(), lr=1e-5)

# 학습 루프
for epoch in range(3):
    model.train()
    outputs = model(**inputs, labels=labels)
    loss = outputs.loss
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    print(f"Epoch {epoch+1}, Loss: {loss.item()}")
  • 설명: 옵티마이저를 설정하고, 에포크마다 모델을 학습시킵니다. loss.backward()를 통해 역전파를 수행하고 가중치를 업데이트합니다.

5. 평가

  • 학습된 모델을 사용하여 새로운 문장에 대한 감정 분류를 수행할 수 있습니다.
# 새로운 문장 예측
test_texts = ["What a fantastic experience!", "I wouldn't recommend this to anyone."]
test_inputs = tokenizer(test_texts, return_tensors='pt', padding=True, truncation=True)
test_outputs = model(**test_inputs)
predictions = torch.argmax(test_outputs.logits, dim=1)
print(f"Predictions: {predictions}")

1. Attention


single.py

import torch
import torch.nn.functional as F
import torch
import math

# Scaled Dot Product Attention

class Attention(nn.Module) :
    
    def forward(self, query, key, value, mask = None, dropout = None) :
        scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(query.size(-1))
        
        if mask is not None :
            scores = scores.masked_fill(mask == 0, -1e9)
            
        p_attn = F.softmax(scores, dim = 1)
        
        if dropout is not None :
            p_attn = dropout(p_attn)
            
        return torch.matmul(p_attn, value), p_attn

multi_head.py

import torch.nn as nn
from .single import Attention

class MultiHeadAttention(nn.Module) :
    
    def __init__(self, h, d_model, dropout = 0.1) :
        super().__init()
        assert d_model % h == 0
        
        self.d_k = d_model // h
        self.h = h
        
        self.linear_layers = nn.ModuleList([nn.Linear(d_model, d_model) for _ in range(3)])
        self.output_linear = nn.Linear(d_model, d_model)
        self.attention = Attention()
        self.dropout = nn.Dropout(p = dropout)
        
    def forward(self, query, key, value, mask = None) :
        batch_size = query.size()
        query, key, value = [l(x).view(batch_size, -1, self.h, self.d_k).transpose(1,2) for l, x in zip(self.linear_layers, (query, key, value))]
        
        x, attn = self.attention(query, key, value, mask = mask, dropout = self.dropout)
        
        x = x.transpose(1,2).contiguous().view(batch_size, -1, self.h * self.d_k)
        
        return self.output_linear(x)

2. UTILS


feed forward.py

import torch.nn as nn
from .gelu import GELU

class PositionwiseFeedForward(nn.Module):
    "Implements FFN equation."

    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        self.w_2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)
        self.activation = GELU()

    def forward(self, x):
        return self.w_2(self.dropout(self.activation(self.w_1(x))))

gelu.py

import torch.nn as nn
import torch
import math

class GELU(nn.Module):

    def forward(self, x):
        return 0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * torch.pow(x, 3))))

layer_norm

import torch.nn as nn
import torch

class LayerNorm(nn.Module):

    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

sublayer.py

import torch.nn as nn
from .layer_norm import LayerNorm

class SublayerConnection(nn.Module):
    """
    A residual connection followed by a layer norm.
    """

    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        "Apply residual connection to any sublayer with the same size."
        return x + self.dropout(sublayer(self.norm(x)))

3. Transformer

import torch.nn as nn

from .attention import MultiHeadedAttention
from .utils import SublayerConnection, PositionwiseFeedForward

class TransformerBlock(nn.Module):
    """
    Bidirectional Encoder = Transformer (self-attention)
    Transformer = MultiHead_Attention + Feed_Forward with sublayer connection
    """

    def __init__(self, hidden, attn_heads, feed_forward_hidden, dropout):
        """
        :param hidden: hidden size of transformer
        :param attn_heads: head sizes of multi-head attention
        :param feed_forward_hidden: feed_forward_hidden, usually 4*hidden_size
        :param dropout: dropout rate
        """

        super().__init__()
        self.attention = MultiHeadedAttention(h=attn_heads, d_model=hidden)
        self.feed_forward = PositionwiseFeedForward(d_model=hidden, d_ff=feed_forward_hidden, dropout=dropout)
        self.input_sublayer = SublayerConnection(size=hidden, dropout=dropout)
        self.output_sublayer = SublayerConnection(size=hidden, dropout=dropout)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x, mask):
        x = self.input_sublayer(x, lambda _x: self.attention.forward(_x, _x, _x, mask=mask))
        x = self.output_sublayer(x, self.feed_forward)
        return self.dropout(x)

4. Three Embeddings

position.py

import torch.nn as nn
import torch
import math

class PositionalEmbedding(nn.Module) :
    
    def __init__(self, d_model, max_len = 512) :
        super().__init__()
        
        # compute positional encoding in log space
        pe = torch.zeros(max_len, d_model).float()
        pe.required_grad = False
        
        position = torch.arange(0, max_len).float().unsqueeze(1)
        div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp()
        
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
        
    def forward(self, x) :
        return self.pe[:, :x.size(1)]

segment.py

import torch.nn as nn

class SegmentEmbedding(nn.Embedding) :
    
    def __init__(self, embed_size = 512) :
        super().__init__(3, embed_size, padding_idx = 0)

token.py

import torch.nn as nn

class TokenEmbedding(nn.Embedding) :
    def __init__(self, vocab_size, embed_size = 512) :
        super().__init__(vocab_size, embed_size, padding_idx = 0)

bert.py

: 세가지 임베딩을 최종적으로 모델에 입력할 수 있는 형태로 전달하는 class

import torch.nn as nn
from .token import TokenEmbedding
from .position import PositionalEmbedding
from .segment import SegmentEmbedding

class BERTEmbedding(nn.Module):

    def __init__(self, vocab_size, embed_size, dropout=0.1):
   
        super().__init__()
        self.token = TokenEmbedding(vocab_size=vocab_size, embed_size=embed_size)
        self.position = PositionalEmbedding(d_model=self.token.embedding_dim)
        self.segment = SegmentEmbedding(embed_size=self.token.embedding_dim)
        self.dropout = nn.Dropout(p=dropout)
        self.embed_size = embed_size

    def forward(self, sequence, segment_label):
        x = self.token(sequence) + self.position(sequence) + self.segment(segment_label)
        return self.dropout(x)

5. Bert model

Bert model

import torch.nn as nn

from .transformer import TransformerBlock
from .embedding import BERTEmbedding

class BERT(nn.Module) :
    
    def __init__(self, vocab_size, hidden = 768, n_layers = 12, attn_heads = 12, dropout = 0.1) :
        super().__init__()
        self.hidden = hidden
        self.n_layer = n_layers
        self.attn_heads = attn_heads
        
        # paper : use 4*hidden_size for ff network hidden size
        self.feed_forward_hidden = hidden * 4
        
        # embedding for BERT = token + segment + position
        self.embedding = BERTEmbedding(vocab_size = vocab_size, embed_size = hidden)
        
        # transformer block
        self.transformer_blocks = nn.ModuleList(
            [TransformerBlock(hidden, attn_heads, hidden * 4, dropout) for _ in range(n_layers)]
        )
        
    def forward(self, x, segment_info) :
        # attention masking
        mask = (x > 0).unsqueeze(1).repeat(1, x.size(1), 1).unsqueeze(1)
        
        # embedding the indexed sequence to sequence of vectors
        x = self.embedding(x, segment_info)
        
        # run multiple transformer block
        for transformer in self.transformer_blocks :
            x = transformer.forward(x, mask)
        
        return x

MLM(masked language model)

class MaskedLanguageModel(nn.Module) :
    def __init__(self, hidden, vocab_size) :
        super().__init__()
        self.linear = nn.Linear(hidden, vocab_size)
        self.softmax = nn.LogSoftmax(dim = -1)
        
    def forward(self, x) :
        return self.softmax(self.linear(x))

NSP(next sentence prediction)

class NextSentencePrediction(nn.Module) :
    
    def __init__(self, hidden) :
        super().__init__()
        self.linear = nn.Linear(hidden, 2)
        self.softmaz = nn.LogSoftmax(dim = -1)
        
    def forward(self, x) :
        return self.softmax(self.linear(x[:, 0]))

languagae_model.py (MLM + NSP 학습)

import torch.nn as nn
from .bert import BERT

class BERTLTM(nn.Module) :
    
    def __init__(self, bert : BERT, vocab_size) :
        super().__init__()
        self.bert = bert
        self.next_sentence = NextSentencePrediction(self.bert.hidden)
        self.mask_lm = MaskedLanguageModel(self.bert.hidden, vocab_size)
        
    def forward(self, x, segment_label) :
        x = self.bert(x, segment_label)
        return self.next_sentence(x), self.mask_lm
    
class NextSentencePrediction(nn.Module) :
    
    def __init__(self, hidden) :
        super().__init__()
        self.linear = nn.Linear(hidden, 2)
        self.softmaz = nn.LogSoftmax(dim = -1)
        
    def forward(self, x) :
        return self.softmax(self.linear(x[:, 0]))
    
class MaskedLanguageModel(nn.Module) :
    def __init__(self, hidden, vocab_size) :
        super().__init__()
        self.linear = nn.Linear(hidden, vocab_size)
        self.softmax = nn.LogSoftmax(dim = -1)
        
    def forward(self, x) :
        return self.softmax(self.linear(x))

6. Training

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader

from ..model import BERTLM, BERT
from .optim_schedule import ScheduledOptim

import tqdm

class BERTTrainer :
    
    def __init__(self, bert : BERT, vocab_size : int, train_dataloader : DataLoader, test_dataloader : DataLoader = None,
                 lr : float = 1e-4, betas = (0.9, 0.999), weight_decay : float = 0.01, warmup_steps = 10000,
                 with_cuda : bool = True, cuda_devices = None, log_freq : int = 10) :
        
        """
        :param bert: BERT model which you want to train
        :param vocab_size: total word vocab size
        :param train_dataloader: train dataset data loader
        :param test_dataloader: test dataset data loader [can be None]
        :param lr: learning rate of optimizer
        :param betas: Adam optimizer betas
        :param weight_decay: Adam optimizer weight decay param
        :param with_cuda: traning with cuda
        :param log_freq: logging frequency of the batch iteration
        """
        
                # Setup cuda device for BERT training, argument -c, --cuda should be true
        cuda_condition = torch.cuda.is_available() and with_cuda
        self.device = torch.device("cuda:0" if cuda_condition else "cpu")

        # This BERT model will be saved every epoch
        self.bert = bert
        # Initialize the BERT Language Model, with BERT model
        self.model = BERTLM(bert, vocab_size).to(self.device)

        # Distributed GPU training if CUDA can detect more than 1 GPU
        if with_cuda and torch.cuda.device_count() > 1:
            print("Using %d GPUS for BERT" % torch.cuda.device_count())
            self.model = nn.DataParallel(self.model, device_ids=cuda_devices)

        # Setting the train and test data loader
        self.train_data = train_dataloader
        self.test_data = test_dataloader

        # Setting the Adam optimizer with hyper-param
        self.optim = Adam(self.model.parameters(), lr=lr, betas=betas, weight_decay=weight_decay)
        self.optim_schedule = ScheduledOptim(self.optim, self.bert.hidden, n_warmup_steps=warmup_steps)

        # Using Negative Log Likelihood Loss function for predicting the masked_token
        self.criterion = nn.NLLLoss(ignore_index=0)

        self.log_freq = log_freq

        print("Total Parameters:", sum([p.nelement() for p in self.model.parameters()]))

    def train(self, epoch):
        self.iteration(epoch, self.train_data)

    def test(self, epoch):
        self.iteration(epoch, self.test_data, train=False)

    def iteration(self, epoch, data_loader, train=True):
        """
        loop over the data_loader for training or testing
        if on train status, backward operation is activated
        and also auto save the model every peoch
        :param epoch: current epoch index
        :param data_loader: torch.utils.data.DataLoader for iteration
        :param train: boolean value of is train or test
        :return: None
        """
        str_code = "train" if train else "test"

        # Setting the tqdm progress bar
        data_iter = tqdm.tqdm(enumerate(data_loader),
                              desc="EP_%s:%d" % (str_code, epoch),
                              total=len(data_loader),
                              bar_format="{l_bar}{r_bar}")

        avg_loss = 0.0
        total_correct = 0
        total_element = 0

        for i, data in data_iter:
            # 0. batch_data will be sent into the device(GPU or cpu)
            data = {key: value.to(self.device) for key, value in data.items()}

            # 1. forward the next_sentence_prediction and masked_lm model
            next_sent_output, mask_lm_output = self.model.forward(data["bert_input"], data["segment_label"])

            # 2-1. NLL(negative log likelihood) loss of is_next classification result
            next_loss = self.criterion(next_sent_output, data["is_next"])

            # 2-2. NLLLoss of predicting masked token word
            mask_loss = self.criterion(mask_lm_output.transpose(1, 2), data["bert_label"])

            # 2-3. Adding next_loss and mask_loss : 3.4 Pre-training Procedure
            loss = next_loss + mask_loss

            # 3. backward and optimization only in train
            if train:
                self.optim_schedule.zero_grad()
                loss.backward()
                self.optim_schedule.step_and_update_lr()

            # next sentence prediction accuracy
            correct = next_sent_output.argmax(dim=-1).eq(data["is_next"]).sum().item()
            avg_loss += loss.item()
            total_correct += correct
            total_element += data["is_next"].nelement()

            post_fix = {
                "epoch": epoch,
                "iter": i,
                "avg_loss": avg_loss / (i + 1),
                "avg_acc": total_correct / total_element * 100,
                "loss": loss.item()
            }

            if i % self.log_freq == 0:
                data_iter.write(str(post_fix))

        print("EP%d_%s, avg_loss=" % (epoch, str_code), avg_loss / len(data_iter), "total_acc=",
              total_correct * 100.0 / total_element)

    def save(self, epoch, file_path="output/bert_trained.model"):
        """
        Saving the current BERT model on file_path
        :param epoch: current epoch number
        :param file_path: model output path which gonna be file_path+"ep%d" % epoch
        :return: final_output_path
        """
        output_path = file_path + ".ep%d" % epoch
        torch.save(self.bert.cpu(), output_path)
        self.bert.to(self.device)
        print("EP:%d Model Saved on:" % epoch, output_path)
        return output_path
profile
AI Engineer / 의료인공지능

0개의 댓글