[nlp][논문리뷰] Improving Language Understanding by Generative Pre-Training

이영락·2024년 10월 23일

CV & NLP 논문 리뷰

목록 보기
8/14

참고자료


[논문리뷰] GPT-1(Improving Language Understandingby Generative Pre-Training)의 이해

GPT-1 논문 리뷰 - ChatGPT의 근간이 되는 논문 완벽하게 이해하기

[GPT-1 논문 리뷰] - Improving Language Understanding by Generative Pre-Training

1 | 논문정리


0. Abstract

자연어 처리(NLP) 분야는 최근 수년 동안 빠르게 발전하고 있으며, 여러 작업에서 최첨단 성능을 달성해왔다. 그러나 대부분의 성공은 라벨링된 데이터에 의존한 지도 학습(Supervised Learning)에서 비롯된 것이다.

라벨링된 데이터는 특정 작업에 맞춰 사람이 직접 데이터를 라벨링해야 하기 때문에 비용이 많이 들고, 양이 제한적이다. 반면, Unlabeled 데이터(Unlabeled Data)는 훨씬 더 풍부하게 존재한다. 문제는 이러한 Unlabeled 데이터를 효과적으로 활용하는 방법이 충분히 연구되지 않았다는 점이다.

본 논문에서는 Unlabeled 데이터를 기반으로 사전 학습(Pre-Training)을 진행하고, 이후 라벨링된 데이터로 미세 조정(Fine-Tuning)을 수행하는 Generative Pre-Training (GPT) 방식을 제안한다. 이러한 방식은 다양한 자연어 처리 작업에서 우수한 성능을 보여주었으며, 이는 Transformer 구조를 기반으로 한 사전 훈련 언어 모델이 다양한 작업에 강력하게 적용될 수 있음을 입증했다. 이 논문은 NLP 분야의 사전 학습과 전이 학습의 가능성을 열었으며, 이후 발전된 GPT 시리즈의 토대를 마련했다.

1. Introduction

1.1 배경 및 문제 제기

  • 자연어 처리 분야에서 기존의 연구는 주로 라벨링된 데이터에 의존한 지도 학습을 중심으로 이루어짐. 예를 들어, 텍스트 분류, 질문 응답, 자연어 추론 등 여러 작업은 풍부한 라벨링 데이터가 필요. → 라벨링 작업은 사람이 직접 데이터를 분류하고 정리해야 하기 때문에 비용이 많이 들고 시간도 소모. → 게다가, 특정 작업에 적합한 라벨링된 데이터의 양이 제한적일 때는 모델의 성능이 크게 저하될 수 있음.
  • Unlabeled 데이터는 인터넷, 책, 논문 등 다양한 출처에서 쉽게 확보할 수 있으며, 양도 무한히 많다고 할 수 있음.
    • Unlabeled 데이터를 효율적으로 사용하는 방법이 기존의 방법론에서는 명확히 제시되지 않음
    • Unlabeled 데이터를 활용하는 방법에 대한 연구를 진행 및지도 학습의 성능을 극대화할 수 있는 방법론 제안

1.2 기존 방법론의 문제점

지도 학습 : 라벨링된 데이터를 사용하여 특정 작업에 최적화된 모델을 학습하지만, 이 방식은 라벨링된 데이터가 충분히 확보되지 않으면 성능에 한계가 있음.

비지도 학습: 라벨링된 데이터 없이 패턴을 학습하는 방식이지만, 이 방식은 특정 작업에 적합한 성능을 발휘하기 어려움. 사전 학습된 모델을 사용하여 새로운 작업에 적용하려 할 때, 작업별로 최적화 목표를 다르게 설정해야 하며, 전이 학습(Transfer Learning) 과정에서 효과적으로 성능을 끌어올리기 어려운 문제점이 존재.

1.3 본 논문의 목적

  1. Unlabeled 데이터를 사용하여 보편적인 언어 표현(Representation)을 학습하는 방법을 제시.
  2. 사전 학습된 모델을 최소한의 구조 변경으로 다양한 자연어 처리 작업에 쉽게 전이할 수 있는 모델을 설계

→ Generative Pre-Training (GPT) 방식의 언어 모델을 제안하고, 다양한 자연어 처리 작업에서 높은 성능을 달성할 수 있는 방법론을 탐구.

2. Proposed Approach

2.1 Unsupervised Pre-Training (비지도 사전 훈련)

2.1.1 모델 구조

: GPT-1의 모델 구조는 Transformer 구조에서 디코더(Decoder) 부분만을 사용.

→ 문장의 일부분을 입력으로 주었을 때 다음 단어를 예측하는 방식으로 학습.

🤔

Why Transformer ??

  • Transformer는 자연어 처리 작업에서 병렬화장기 의존성(Long-Term Dependency) 문제를 해결하는 데 매우 유리한 구조
  • Self-Attention Mechanism을 통해 문맥의 긴 종속성을 잘 처리.
  • Transformer구조Self-Attention를 통해 문장 내 모든 단어 간의 관계를 효율적으로 학습할 수 있음.(기존의 RNN(Recurrent Neural Network)이나 LSTM(Long Short-Term Memory) 구조는 단어 간의 긴 종속성을 잘 학습하지 못하는 한계가 있었음.)

2.1.2 Language Modeling Objective

Unlabeled 데이터를 기반으로 한 언어 모델링 과정 : 모델이 주어진 문맥에서 다음 단어를 예측하도록 학습.

→ 모델이 언어의 구조와 패턴을 학습하는 데 효과적.(자연어 생성 작업에서도 매우 유용한 방법)

🚨

언어 모델링의 최적화 목표 수식 ????
L1(U)=ilogP(uiuik,,ui1;θL_1(U) = \sum_{i} \log P(u_i | u_{i-k}, \dots, u_{i-1}; \theta)

  • kk : Context Window Size를 의미
  • uiu_i : 시퀀스에서의 단어
  • thetatheta : 모델의 파라미터.

2.1.3 학습 데이터

pre-trained 에 사용된 데이터: BooksCorpus

🤔

BookCorpus??

: 약 7,000권의 미출간된 책들로 구성되어 있으며, 긴 문맥(Long-Range Context)을 포함한 텍스트 데이터를 제공하여 언어 모델 학습에 적합한 데이터셋으로 평가받고 있다.

스크린샷 2024-10-11 오전 10.55.05.png

2.2 Supervised Fine-Tuning

: 사전 훈련된 모델을 특정 작업(Task)에 맞게 라벨링된 데이터로 미세 조정.

: 작업별 라벨링된 데이터를 활용하여 모델이 특정 작업의 목표에 맞게 성능을 최적화하는 과정

이 단계에서는 모델 구조의 변경은 최소화된다. GPT-1 모델은 사전 훈련된 가중치파라미터를 유지한 상태에서, Task-Specific Input Transformations만을 적용하여 다양한 작업에 맞게 미세 조정을 진행.

2.2.1 Fine-Tuning 목표

Fine-Tuning 단계의 최적화 목표는 작업별로 라벨링된 데이터를 기반으로 이루어짐

🚨

각 시퀀스에 대해 라벨 yy 를 예측하는 확률 수식

P(yx1,,xm)=softmax(hmlWy)P(y | x_1, \dots, x_m) = \text{softmax}(h_m^l W_y)

  • Softmax는 모델의 출력값을 확률 값으로 변환하는 역할을 하며, 이를 통해 모델이 특정 작업의 라벨 예측을 수행할 수 있다.

스크린샷 2024-10-11 오전 10.55.41.png

스크린샷 2024-10-11 오전 10.55.50.png

3. Task-Specific Input Transformations

GPT-1은 간단한 입력 변환 방식을 통해 다양한 자연어 처리 작업에 적용될 수 있도록 설계.

이를 통해, 작업별로 복잡한 모델 구조를 변경하지 않고도 여러 작업에 대해 효과적인 성능을 발휘할 수 있음.

이 섹션에서는 각 작업에 대해 입력 변환 방식을 설명.

3.1 텍스트 분류 (Text Classification)

: 텍스트 분류는 입력된 텍스트를 기반으로 사전 정의된 카테고리 중 하나를 예측하는 작업.

ex) 스팸 메일 분류와 같은 문제를 생각할 수 있다. GPT-1에서의 입력 변환은 매우 간단한다. 전체 문장을 그대로 모델에 입력하며, 출력은 해당 문장의 카테고리를 예측하는 확률 값을 산출한다.

3.2 텍스트 추론 (Textual Entailment):

텍스트 추론은 두 문장 간의 관계를 예측하는 작업이다. 두 문장이 논리적으로 함축(Entailment)하는지, 모순(Contradiction)되는지, 아니면 중립(Neutral)인지를 분류하는 문제이다.

GPT-1에서는 두 문장을 연결해 입력으로 사용한다.

  • 첫 번째 문장은 전제(Premise),
  • 두 번째 문장은 가설(Hypothesis)로 사용되며, 두 문장 사이에 구분 기호(Delimiter)를 추가하여 하나의 시퀀스로 모델에 입력한다.

3.3 유사성 평가 (Similarity Task)

: 유사성 평가 작업에서는 두 문장이 얼마나 비슷한지(Similarity)를 예측하는 문제이다. 두 문장이 같은 의미를 담고 있는지, 또는 얼마나 유사한지 평가하는 작업이다.

GPT-1에서는 두 문장을 개별적으로 입력한 후, 각 문장의 표현 벡터를 추출한다. 이후 element-wise로 합산하여 두 문장 간의 유사도를 계산하게 됩니다. 이 계산 결과를 바탕으로 유사성 점수를 예측한다.

3.4 질문 응답 (Question Answering)

: 질문 응답은 지문(Passage)과 질문(Question)이 주어졌을 때, 지문 내에서 정답(Answer)을 찾는 작업이다.

GPT-1에서는 지문과 질문을 입력으로 사용하며, 지문, 질문, 그리고 가능한 정답 리스트가 함께 모델에 주어집니다. 모델은 Softmax 함수를 통해 가장 가능성이 높은 정답을 선택하게 됩니다.

3.5 다중 선택 문제 (Multiple Choice Task)

다중 선택 문제는 질문에 대한 여러 선택지 중에서 올바른 답을 선택하는 작업이다. GPT-1에서는 지문과 선택지를 결합하여 입력 시퀀스로 변환한다. 예를 들어, "서울은 대한민국의 수도이다. 이 도시는 어디입니까?"라는 질문이 주어진 경우, "서울", "부산", "대구" 등의 선택지를 각기 다른 입력 시퀀스로 처리한 후, 모델이 가장 적합한 답을 고를 수 있도록 한다.

4. Experiments (실험)

본 논문에서는 다양한 자연어 처리 작업에서 GPT-1의 성능을 평가하기 위해 실험을 진행하였다. 사전 훈련을 거친 모델은 12개의 NLP 작업에 대해 미세 조정(Fine-Tuning)하여 성능을 측정하였으며, 여기에는 텍스트 분류, 질문 응답, 자연어 추론 등의 작업이 포함됩니다.

4.1 Setup (실험 환경 설정)

4.1.1 사전 훈련 데이터

사전 훈련에 사용된 데이터셋은 BooksCorpus로, 약 7,000권의 미출간 책들로 구성된 대규모 텍스트 데이터셋이다. 이 데이터셋은 긴 문맥(long-range context)을 포함하고 있어, 언어 모델이 긴 문맥에서의 종속성(Long-Term Dependency)을 학습하는 데 적합한다.

4.1.2 모델 구조

GPT-1은 12개의 Transformer 디코더 레이어로 구성된 구조를 가지고 있다. 각 레이어에는 12개의 자기-주의 헤드(Self-Attention Heads)가 있으며, 768차원의 임베딩 벡터(Embedding Vector)로 모델을 학습한다. 학습은 Adam 최적화 기법을 사용해 진행되었다.

4.2 Fine-Tuning (지도 미세 조정)

사전 훈련된 모델은 각 작업에 대해 라벨링된 데이터로 미세 조정을 진행한다. 미세 조정 단계에서는 사전 학습된 가중치를 유지하면서 각 작업에 필요한 하이퍼파라미터(Hyperparameter)를 조정한다. Fine-Tuning 단계에서는 보조 학습 목표(Auxiliary Objective)를 추가로 사용하여 모델의 일반화 성능을 향상시키고, 수렴 속도를 높였다.

4.3 Task Performance (작업 성능)

실험 결과, GPT-1은 다양한 NLP 작업에서 기존 방법들을 뛰어넘는 성능을 달성하였다. 주요 작업별 성능은 다음과 같다:

4.3.1 질문 응답 (RACE Dataset)

  • RACE 데이터셋에서 GPT-1은 기존 모델들 대비 5.7% 성능 향상을 기록했다. RACE는 고난이도 질문 응답 작업으로, 학생들의 영어 독해 능력을 평가하는 데 사용되는 데이터셋이다.

4.3.2 자연어 추론 (MultiNLI)

  • MultiNLI 데이터셋은 다양한 장르의 텍스트에서 두 문장이 논리적으로 연결되는지 평가하는 작업이다. GPT-1은 기존 모델들 대비 1.5% 성능 향상을 보였다.

4.3.3 GLUE Benchmark

  • GLUE 벤치마크는 다양한 자연어 처리 작업을 포함하는 종합 평가 기준이다. GPT-1은 GLUE 벤치마크에서도 기존 최고 성능을 5.5% 향상시키며, 다양한 작업에서의 강력한 성능을 입증했다.

5. Analysis

실험 결과를 바탕으로 GPT-1의 성능을 심층 분석하여 전이 학습(Transfer Learning)제로샷 학습(Zero-Shot Learning)에서의 효율성을 평가.

5.1 Layer Transfer

사전 훈련된 모델에서 여러 레이어(layer)를 전이시켜 학습한 결과, 전이되는 레이어의 수가 많을수록 성능이 향상된다는 점이 확인되었다. 특히, 9개의 레이어를 전이한 모델에서 최대 성능이 관찰되었다. 이는 사전 훈련된 모델의 다층 표현(Deep Representation)이 언어적 정보와 의미를 충분히 포함하고 있음을 의미하며, 이를 활용해 다운스트림 작업(Downstream Task)에서도 높은 성능을 발휘할 수 있음을 보여준다.

5.2 Ablation Studies

본 연구에서는 Ablation Study(소거 실험)를 통해 모델의 구성 요소들이 작업 성능에 미치는 영향을 분석하였다. 주요 실험 결과는 다음과 같다:

  1. 보조 언어 모델링 목표(Auxiliary LM Objective) 없이 학습했을 때, 특히 자연어 추론(NLI)과 질문 유사성 평가(QQP) 작업에서 성능이 크게 하락하는 것으로 나타났습니다. 이를 통해, 사전 훈련 목표가 모델의 전반적인 성능에 중요한 역할을 한다는 점이 확인되었다.
  2. LSTM 기반 모델과 Transformer 모델 비교:
    • Transformer 모델은 5.6% 더 높은 성능을 기록했다. 이는 자기-주의(Self-Attention) 메커니즘이 긴 문맥 처리에 더 적합하다는 것을 보여줍니다.
  3. Pre-Training의 효과:
    • 사전 훈련 없이 바로 작업별로 학습한 모델은 14.8% 성능 감소를 보였다. 이는 사전 훈련이 매우 중요한 성능 향상 요소임을 입증하며, 사전 훈련된 모델은 일반적인 언어적 지식을 효과적으로 학습할 수 있다는 점을 의미한다.

6. Strengths and Weaknesses

6.1 Strengths

  1. 효율적인 전이 학습:
    • GPT-1은 Unlabeled 데이터를 통해 사전 학습을 진행한 후, 라벨링된 데이터로 미세 조정(Fine-Tuning)하여 전이 학습(Transfer Learning)을 극대화했다. 이는 다양한 작업에 대해 작업별로 복잡한 모델 설계 없이 간단한 입력 변환 방식만으로도 높은 성능을 발휘할 수 있게 했다.
  2. 간단한 모델 구조:
    • GPT-1은 Transformer 구조 중 디코더(Decoder) 부분만을 사용하여, 간결한 모델 구조로 설계되었다. 이 모델은 복잡한 구조 변경 없이 다양한 작업에 적용 가능하며, 학습 및 추론 과정에서 효율적이다. 이는 간단한 입력 변환 방식을 사용하여 여러 작업에 적용할 수 있음을 의미한다.
  3. 강력한 성능:
    • GPT-1은 다양한 자연어 처리 작업에서 최첨단 성능(State of the Art)을 달성했다. 특히 질문 응답, 자연어 추론, 텍스트 분류 등에서 뛰어난 성과를 보였으며, 이로 인해 사전 학습된 언어 모델의 강력한 성능이 입증되었다.
  4. 제로샷 학습(Zero-Shot Learning) 능력:
    • GPT-1은 특정 작업에 대해 미세 조정 없이도 일정 수준의 성능을 발휘하는 제로샷 학습(Zero-Shot Learning) 능력을 보여주었습니다. 이는 GPT-1이 사전 훈련 과정에서 일반적인 언어적 지식을 충분히 학습하였으며, 라벨링된 데이터 없이도 여러 작업에 적용 가능하다는 점을 시사한다. 이는 특히 Unlabeled 데이터로 모델을 학습시키는 방법의 강점을 입증하는 결과이다.

6.2 Weaknesses

  1. 높은 계산 비용:
    • GPT-1의 사전 훈련 과정에서는 매우 대규모의 Unlabeled 데이터가 사용되며, 이 과정에서 큰 계산량이 필요한다. 이는 학습에 상당한 시간과 자원이 필요하다는 점에서 연산 비용이 크다는 단점이 있다. 특히, 오늘날의 모델에 비해 상대적으로 적은 연산량이지만 당시 기준으로는 매우 큰 연산 비용을 요구했다.
  2. 작업별 미세 조정(Fine-Tuning)의 필요성:
    • GPT-1은 특정 작업에 대해 작업별로 미세 조정(Fine-Tuning)을 진행해야만 최적의 성능을 발휘할 수 있다. 이는 작업마다 라벨링된 데이터를 추가로 준비하고 학습해야 하는 단점이 있으며, Few-Shot 또는 Zero-Shot 학습에 대한 요구가 있는 경우에는 적합하지 않을 수 있다. 이후 모델(GPT-3)은 이러한 단점을 개선하여 Few-Shot 학습이 가능하도록 발전되었다.
  3. 일반화 성능의 한계:
    • GPT-1은 특정 작업에서 미세 조정을 거쳐야 높은 성능을 발휘할 수 있으며, 미세 조정이 없는 경우에는 일반화된 성능이 다소 제한적일 수 있다. 따라서 새로운 작업에 대해 일반화된 성능을 유지하려면 각 작업별로 별도의 학습 과정을 필요로 하는 단점이 있다.

7. Significance

GPT-1은 언어 모델링(Language Modeling)의 새로운 접근 방식을 제안하며, 사전 학습(Pre-Training)과 미세 조정(Fine-Tuning)을 결합한 방법론이 자연어 처리 작업에서 강력한 성능을 발휘할 수 있음을 입증했다. 이 연구는 NLP 연구의 패러다임을 변화시키는 중요한 연구로 평가받고 있다.

7.1 Unlabeled 데이터의 효과적인 활용

GPT-1은 Unlabeled 데이터(Unlabeled Data)를 활용하여 사전 학습을 진행한 후, 라벨링된 데이터(Labeled Data)를 통해 미세 조정하는 방식으로 지도 학습의 성능을 극대화할 수 있음을 증명했다. 이는 라벨링된 데이터의 부족 문제를 해결하는 데 매우 유용한 방법이며, Unlabeled 데이터의 잠재력을 자연어 처리에서 효과적으로 활용할 수 있음을 시사한다.

7.2 Transformer 구조의 중요성

GPT-1은 Transformer 구조디코더(Decoder) 부분을 사용하였으며, 자기-주의 메커니즘(Self-Attention Mechanism)을 통해 기존의 RNNLSTM 기반 모델들보다 더 뛰어난 성능을 발휘했다. 특히, Transformer는 장기 의존성(Long-Term Dependency) 문제를 해결할 수 있는 장점을 가지고 있으며, 이를 통해 자연어 처리 작업에서 더 깊은 문맥적 이해를 가능하게 했다.

7.3 전이 학습(Transfer Learning)의 발전

GPT-1은 사전 훈련(Pre-Training)과 미세 조정(Fine-Tuning)을 결합하여 전이 학습(Transfer Learning)의 효율성을 극대화하였다. 이는 이후 연구에서 사전 학습된 언어 모델(Pretrained Language Models)이 다양한 작업에서 재사용 가능한 강력한 도구로 자리 잡는 데 기여하였다. 특히 GPT-1은 이후 발표된 GPT-2, GPT-3, 그리고 InstructGPT 등의 발전된 모델들의 기초를 제공했으며, 대규모 언어 모델의 중요성을 강조했다.

8. Conclusion (결론)

GPT-1은 생성적 사전 훈련(Generative Pre-Training)과 지도 학습 미세 조정(Supervised Fine-Tuning)을 결합한 NLP 모델로, Unlabeled 데이터를 효율적으로 사용하여 다양한 자연어 처리 작업에서 최첨단 성과(SOTA)를 달성했다. GPT-1은 Transformer 구조를 기반으로 하여 장기 문맥 처리 능력을 극대화하며, 전이 학습의 효율성을 크게 향상시켰습니다.

8.1 연구의 기여

본 연구는 자연어 처리에서 Unlabeled 데이터를 효과적으로 활용할 수 있는 방법론을 제안했으며, 사전 훈련된 언어 모델이 어떻게 다양한 작업에서 높은 성능을 발휘할 수 있는지를 입증했다. 특히 GPT-1은 Transformer 구조전이 학습을 결합하여 자연어 처리 연구의 새로운 방향을 제시했으며, 이후 발전된 GPT 시리즈의 토대를 마련했다.

8.2 미래 연구 방향

GPT-1의 연구는 이후 언어 모델 연구에 큰 영향을 미쳤으며, GPT-2, GPT-3, InstructGPT 등의 발전된 모델들이 개발되었다. 이들 모델은 GPT-1의 기본적인 아이디어를 발전시켜 Few-Shot 학습, Zero-Shot 학습, 대규모 모델 등의 새로운 연구 분야를 개척했다. GPT-1은 자연어 처리에서 비지도 학습의 잠재력을 입증한 중요한 연구로 자리 잡았으며, 앞으로도 전이 학습대규모 사전 학습된 모델에 대한 연구는 지속될 것이다.

02 | 논문 탐구


🚥 주제 1 : BERT 모델 구조 및 특징 이해 (Bidirectional Encoder Representations) 🚥 주제 2 : Pre-training과 Fine-tuning 개념 및 활용 방법 ***중요, task 별 이해*** 🚥 주제 3 : BERT와 GPT 모델의 차이점 (개념적 비교) 🚥 주제 4 : BERT의 NLP 태스크 성능 향상에 미친 영향

03 | 실습 : gpt 코드 분석


https://github.com/openai/finetune-transformer-lm/blob/master/train.py

train.py

import os
import time
import math
import json
import joblib
import random
import argparse
import numpy as np
import tensorflow as tf

from tqdm import tqdm
from functools import partial
from sklearn.utils import shuffle
from sklearn.metrics import accuracy_score

from opt import adam, warmup_cosine, warmup_linear, warmup_constant
from datasets import rocstories
from analysis import rocstories as rocstories_analysis
from text_utils import TextEncoder
from utils import encode_dataset, flatten, iter_data, find_trainable_variables, convert_gradient_to_tensor, shape_list, ResultLogger, assign_to_gpu, average_grads, make_path

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

def swish(x):
    return x*tf.nn.sigmoid(x)

opt_fns = {
    'adam':adam,
}

act_fns = {
    'relu':tf.nn.relu,
    'swish':swish,
    'gelu':gelu
}

lr_schedules = {
    'warmup_cosine':warmup_cosine,
    'warmup_linear':warmup_linear,
    'warmup_constant':warmup_constant,
}

def _norm(x, g=None, b=None, e=1e-5, axis=[1]):
    u = tf.reduce_mean(x, axis=axis, keep_dims=True)
    s = tf.reduce_mean(tf.square(x-u), axis=axis, keep_dims=True)
    x = (x - u) * tf.rsqrt(s + e)
    if g is not None and b is not None:
        x = x*g + b
    return x

def norm(x, scope, axis=[-1]):
    with tf.variable_scope(scope):
        n_state = shape_list(x)[-1]
        g = tf.get_variable("g", [n_state], initializer=tf.constant_initializer(1))
        b = tf.get_variable("b", [n_state], initializer=tf.constant_initializer(0))
        return _norm(x, g, b, axis=axis)

def dropout(x, pdrop, train):
    if train and pdrop > 0:
        x = tf.nn.dropout(x, 1-pdrop)
    return x

def mask_attn_weights(w):
    n = shape_list(w)[-1]
    b = tf.matrix_band_part(tf.ones([n, n]), -1, 0)
    b = tf.reshape(b, [1, 1, n, n])
    w = w*b + -1e9*(1-b)
    return w

def _attn(q, k, v, train=False, scale=False):
    w = tf.matmul(q, k)

    if scale:
        n_state = shape_list(v)[-1]
        w = w*tf.rsqrt(tf.cast(n_state, tf.float32))

    w = mask_attn_weights(w)
    w = tf.nn.softmax(w)

    w = dropout(w, attn_pdrop, train)

    a = tf.matmul(w, v)
    return a

def split_states(x, n):
    x_shape = shape_list(x)
    m = x_shape[-1]
    new_x_shape = x_shape[:-1]+[n, m//n]
    return tf.reshape(x, new_x_shape)

def merge_states(x):
    x_shape = shape_list(x)
    new_x_shape = x_shape[:-2]+[np.prod(x_shape[-2:])]
    return tf.reshape(x, new_x_shape)

def split_heads(x, n, k=False):
    if k:
        return tf.transpose(split_states(x, n), [0, 2, 3, 1])
    else:
        return tf.transpose(split_states(x, n), [0, 2, 1, 3])

def merge_heads(x):
    return merge_states(tf.transpose(x, [0, 2, 1, 3]))

def conv1d(x, scope, nf, rf, w_init=tf.random_normal_initializer(stddev=0.02), b_init=tf.constant_initializer(0), pad='VALID', train=False):
    with tf.variable_scope(scope):
        nx = shape_list(x)[-1]
        w = tf.get_variable("w", [rf, nx, nf], initializer=w_init)
        b = tf.get_variable("b", [nf], initializer=b_init)
        if rf == 1: #faster 1x1 conv
            c = tf.reshape(tf.matmul(tf.reshape(x, [-1, nx]), tf.reshape(w, [-1, nf]))+b, shape_list(x)[:-1]+[nf])
        else: #was used to train LM
            c = tf.nn.conv1d(x, w, stride=1, padding=pad)+b
        return c

def attn(x, scope, n_state, n_head, train=False, scale=False):
    assert n_state%n_head==0
    with tf.variable_scope(scope):
        c = conv1d(x, 'c_attn', n_state*3, 1, train=train)
        q, k, v = tf.split(c, 3, 2)
        q = split_heads(q, n_head)
        k = split_heads(k, n_head, k=True)
        v = split_heads(v, n_head)
        a = _attn(q, k, v, train=train, scale=scale)
        a = merge_heads(a)
        a = conv1d(a, 'c_proj', n_state, 1, train=train)
        a = dropout(a, resid_pdrop, train)
        return a

def mlp(x, scope, n_state, train=False):
    with tf.variable_scope(scope):
        nx = shape_list(x)[-1]
        act = act_fns[afn]
        h = act(conv1d(x, 'c_fc', n_state, 1, train=train))
        h2 = conv1d(h, 'c_proj', nx, 1, train=train)
        h2 = dropout(h2, resid_pdrop, train)
        return h2

def block(x, scope, train=False, scale=False):
    with tf.variable_scope(scope):
        nx = shape_list(x)[-1]
        a = attn(x, 'attn', nx, n_head, train=train, scale=scale)
        n = norm(x+a, 'ln_1')
        m = mlp(n, 'mlp', nx*4, train=train)
        h = norm(n+m, 'ln_2')
        return h

def embed(X, we):
    we = convert_gradient_to_tensor(we)
    e = tf.gather(we, X)
    h = tf.reduce_sum(e, 2)
    return h

def clf(x, ny, w_init=tf.random_normal_initializer(stddev=0.02), b_init=tf.constant_initializer(0), train=False):
    with tf.variable_scope('clf'):
        nx = shape_list(x)[-1]
        w = tf.get_variable("w", [nx, ny], initializer=w_init)
        b = tf.get_variable("b", [ny], initializer=b_init)
        return tf.matmul(x, w)+b

def model(X, M, Y, train=False, reuse=False):
    with tf.variable_scope('model', reuse=reuse):
        we = tf.get_variable("we", [n_vocab+n_special+n_ctx, n_embd], initializer=tf.random_normal_initializer(stddev=0.02))
        we = dropout(we, embd_pdrop, train)

        X = tf.reshape(X, [-1, n_ctx, 2])
        M = tf.reshape(M, [-1, n_ctx])

        h = embed(X, we)
        for layer in range(n_layer):
            h = block(h, 'h%d'%layer, train=train, scale=True)

        lm_h = tf.reshape(h[:, :-1], [-1, n_embd])
        lm_logits = tf.matmul(lm_h, we, transpose_b=True)
        lm_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=lm_logits, labels=tf.reshape(X[:, 1:, 0], [-1]))
        lm_losses = tf.reshape(lm_losses, [shape_list(X)[0], shape_list(X)[1]-1])
        lm_losses = tf.reduce_sum(lm_losses*M[:, 1:], 1)/tf.reduce_sum(M[:, 1:], 1)

        clf_h = tf.reshape(h, [-1, n_embd])
        pool_idx = tf.cast(tf.argmax(tf.cast(tf.equal(X[:, :, 0], clf_token), tf.float32), 1), tf.int32)
        clf_h = tf.gather(clf_h, tf.range(shape_list(X)[0], dtype=tf.int32)*n_ctx+pool_idx)

        clf_h = tf.reshape(clf_h, [-1, 2, n_embd])
        if train and clf_pdrop > 0:
            shape = shape_list(clf_h)
            shape[1] = 1
            clf_h = tf.nn.dropout(clf_h, 1-clf_pdrop, shape)
        clf_h = tf.reshape(clf_h, [-1, n_embd])
        clf_logits = clf(clf_h, 1, train=train)
        clf_logits = tf.reshape(clf_logits, [-1, 2])

        clf_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=clf_logits, labels=Y)
        return clf_logits, clf_losses, lm_losses

def mgpu_train(*xs):
    gpu_ops = []
    gpu_grads = []
    xs = (tf.split(x, n_gpu, 0) for x in xs)
    for i, xs in enumerate(zip(*xs)):
        do_reuse = True if i > 0 else None
        with tf.device(assign_to_gpu(i, "/gpu:0")), tf.variable_scope(tf.get_variable_scope(), reuse=do_reuse):
            clf_logits, clf_losses, lm_losses = model(*xs, train=True, reuse=do_reuse)
            if lm_coef > 0:
                train_loss = tf.reduce_mean(clf_losses) + lm_coef*tf.reduce_mean(lm_losses)
            else:
                train_loss = tf.reduce_mean(clf_losses)
            params = find_trainable_variables("model")
            grads = tf.gradients(train_loss, params)
            grads = list(zip(grads, params))
            gpu_grads.append(grads)
            gpu_ops.append([clf_logits, clf_losses, lm_losses])
    ops = [tf.concat(op, 0) for op in zip(*gpu_ops)]
    grads = average_grads(gpu_grads)
    grads = [g for g, p in grads]
    train = opt_fns[opt](params, grads, lr, partial(lr_schedules[lr_schedule], warmup=lr_warmup), n_updates_total, l2=l2, max_grad_norm=max_grad_norm, vector_l2=vector_l2, b1=b1, b2=b2, e=e)
    return [train]+ops

def mgpu_predict(*xs):
    gpu_ops = []
    xs = (tf.split(x, n_gpu, 0) for x in xs)
    for i, xs in enumerate(zip(*xs)):
        with tf.device(assign_to_gpu(i, "/gpu:0")), tf.variable_scope(tf.get_variable_scope(), reuse=True):
            clf_logits, clf_losses, lm_losses = model(*xs, train=False, reuse=True)
            gpu_ops.append([clf_logits, clf_losses, lm_losses])
    ops = [tf.concat(op, 0) for op in zip(*gpu_ops)]
    return ops

def transform_roc(X1, X2, X3):
    n_batch = len(X1)
    xmb = np.zeros((n_batch, 2, n_ctx, 2), dtype=np.int32)
    mmb = np.zeros((n_batch, 2, n_ctx), dtype=np.float32)
    start = encoder['_start_']
    delimiter = encoder['_delimiter_']
    for i, (x1, x2, x3), in enumerate(zip(X1, X2, X3)):
        x12 = [start]+x1[:max_len]+[delimiter]+x2[:max_len]+[clf_token]
        x13 = [start]+x1[:max_len]+[delimiter]+x3[:max_len]+[clf_token]
        l12 = len(x12)
        l13 = len(x13)
        xmb[i, 0, :l12, 0] = x12
        xmb[i, 1, :l13, 0] = x13
        mmb[i, 0, :l12] = 1
        mmb[i, 1, :l13] = 1
    xmb[:, :, :, 1] = np.arange(n_vocab+n_special, n_vocab+n_special+n_ctx)
    return xmb, mmb

def iter_apply(Xs, Ms, Ys):
    fns = [lambda x:np.concatenate(x, 0), lambda x:float(np.sum(x))]
    results = []
    for xmb, mmb, ymb in iter_data(Xs, Ms, Ys, n_batch=n_batch_train, truncate=False, verbose=True):
        n = len(xmb)
        if n == n_batch_train:
            res = sess.run([eval_mgpu_logits, eval_mgpu_clf_loss], {X_train:xmb, M_train:mmb, Y_train:ymb})
        else:
            res = sess.run([eval_logits, eval_clf_loss], {X:xmb, M:mmb, Y:ymb})
        res = [r*n for r in res]
        results.append(res)
    results = zip(*results)
    return [fn(res) for res, fn in zip(results, fns)]

def iter_predict(Xs, Ms):
    logits = []
    for xmb, mmb in iter_data(Xs, Ms, n_batch=n_batch_train, truncate=False, verbose=True):
        n = len(xmb)
        if n == n_batch_train:
            logits.append(sess.run(eval_mgpu_logits, {X_train:xmb, M_train:mmb}))
        else:
            logits.append(sess.run(eval_logits, {X:xmb, M:mmb}))
    logits = np.concatenate(logits, 0)
    return logits

def save(path):
    ps = sess.run(params)
    joblib.dump(ps, make_path(path))

def log():
    global best_score
    tr_logits, tr_cost = iter_apply(trX[:n_valid], trM[:n_valid], trY[:n_valid])
    va_logits, va_cost = iter_apply(vaX, vaM, vaY)
    tr_cost = tr_cost/len(trY[:n_valid])
    va_cost = va_cost/n_valid
    tr_acc = accuracy_score(trY[:n_valid], np.argmax(tr_logits, 1))*100.
    va_acc = accuracy_score(vaY, np.argmax(va_logits, 1))*100.
    logger.log(n_epochs=n_epochs, n_updates=n_updates, tr_cost=tr_cost, va_cost=va_cost, tr_acc=tr_acc, va_acc=va_acc)
    print('%d %d %.3f %.3f %.2f %.2f'%(n_epochs, n_updates, tr_cost, va_cost, tr_acc, va_acc))
    if submit:
        score = va_acc
        if score > best_score:
            best_score = score
            save(os.path.join(save_dir, desc, 'best_params.jl'))

argmax = lambda x:np.argmax(x, 1)

pred_fns = {
    'rocstories':argmax,
}

filenames = {
    'rocstories':'ROCStories.tsv',
}

label_decoders = {
    'rocstories':None,
}

def predict():
    filename = filenames[dataset]
    pred_fn = pred_fns[dataset]
    label_decoder = label_decoders[dataset]
    predictions = pred_fn(iter_predict(teX, teM))
    if label_decoder is not None:
        predictions = [label_decoder[prediction] for prediction in predictions]
    path = os.path.join(submission_dir, filename)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, 'w') as f:
        f.write('{}\t{}\n'.format('index', 'prediction'))
        for i, prediction in enumerate(predictions):
            f.write('{}\t{}\n'.format(i, prediction))

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--desc', type=str)
    parser.add_argument('--dataset', type=str)
    parser.add_argument('--log_dir', type=str, default='log/')
    parser.add_argument('--save_dir', type=str, default='save/')
    parser.add_argument('--data_dir', type=str, default='data/')
    parser.add_argument('--submission_dir', type=str, default='submission/')
    parser.add_argument('--submit', action='store_true')
    parser.add_argument('--analysis', action='store_true')
    parser.add_argument('--seed', type=int, default=42)
    parser.add_argument('--n_iter', type=int, default=3)
    parser.add_argument('--n_batch', type=int, default=8)
    parser.add_argument('--max_grad_norm', type=int, default=1)
    parser.add_argument('--lr', type=float, default=6.25e-5)
    parser.add_argument('--lr_warmup', type=float, default=0.002)
    parser.add_argument('--n_ctx', type=int, default=512)
    parser.add_argument('--n_embd', type=int, default=768)
    parser.add_argument('--n_head', type=int, default=12)
    parser.add_argument('--n_layer', type=int, default=12)
    parser.add_argument('--embd_pdrop', type=float, default=0.1)
    parser.add_argument('--attn_pdrop', type=float, default=0.1)
    parser.add_argument('--resid_pdrop', type=float, default=0.1)
    parser.add_argument('--clf_pdrop', type=float, default=0.1)
    parser.add_argument('--l2', type=float, default=0.01)
    parser.add_argument('--vector_l2', action='store_true')
    parser.add_argument('--n_gpu', type=int, default=4)
    parser.add_argument('--opt', type=str, default='adam')
    parser.add_argument('--afn', type=str, default='gelu')
    parser.add_argument('--lr_schedule', type=str, default='warmup_linear')
    parser.add_argument('--encoder_path', type=str, default='model/encoder_bpe_40000.json')
    parser.add_argument('--bpe_path', type=str, default='model/vocab_40000.bpe')
    parser.add_argument('--n_transfer', type=int, default=12)
    parser.add_argument('--lm_coef', type=float, default=0.5)
    parser.add_argument('--b1', type=float, default=0.9)
    parser.add_argument('--b2', type=float, default=0.999)
    parser.add_argument('--e', type=float, default=1e-8)

    args = parser.parse_args()
    print(args)
    globals().update(args.__dict__)
    random.seed(seed)
    np.random.seed(seed)
    tf.set_random_seed(seed)

    logger = ResultLogger(path=os.path.join(log_dir, '{}.jsonl'.format(desc)), **args.__dict__)
    text_encoder = TextEncoder(encoder_path, bpe_path)
    encoder = text_encoder.encoder
    n_vocab = len(text_encoder.encoder)

    (trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3) = encode_dataset(rocstories(data_dir), encoder=text_encoder)
    n_y = 2
    encoder['_start_'] = len(encoder)
    encoder['_delimiter_'] = len(encoder)
    encoder['_classify_'] = len(encoder)
    clf_token = encoder['_classify_']
    n_special = 3
    max_len = n_ctx//2-2
    n_ctx = min(max([len(x1[:max_len])+max(len(x2[:max_len]), len(x3[:max_len])) for x1, x2, x3 in zip(trX1, trX2, trX3)]+[len(x1[:max_len])+max(len(x2[:max_len]), len(x3[:max_len])) for x1, x2, x3 in zip(vaX1, vaX2, vaX3)]+[len(x1[:max_len])+max(len(x2[:max_len]), len(x3[:max_len])) for x1, x2, x3 in zip(teX1, teX2, teX3)])+3, n_ctx)
    trX, trM = transform_roc(trX1, trX2, trX3)
    vaX, vaM = transform_roc(vaX1, vaX2, vaX3)
    if submit:
        teX, teM = transform_roc(teX1, teX2, teX3)

    n_train = len(trY)
    n_valid = len(vaY)
    n_batch_train = n_batch*n_gpu
    n_updates_total = (n_train//n_batch_train)*n_iter

    X_train = tf.placeholder(tf.int32, [n_batch_train, 2, n_ctx, 2])
    M_train = tf.placeholder(tf.float32, [n_batch_train, 2, n_ctx])
    X = tf.placeholder(tf.int32, [None, 2, n_ctx, 2])
    M = tf.placeholder(tf.float32, [None, 2, n_ctx])

    Y_train = tf.placeholder(tf.int32, [n_batch_train])
    Y = tf.placeholder(tf.int32, [None])

    train, logits, clf_losses, lm_losses = mgpu_train(X_train, M_train, Y_train)
    clf_loss = tf.reduce_mean(clf_losses)

    params = find_trainable_variables('model')
    sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True))
    sess.run(tf.global_variables_initializer())

    shapes = json.load(open('model/params_shapes.json'))
    offsets = np.cumsum([np.prod(shape) for shape in shapes])
    init_params = [np.load('model/params_{}.npy'.format(n)) for n in range(10)]
    init_params = np.split(np.concatenate(init_params, 0), offsets)[:-1]
    init_params = [param.reshape(shape) for param, shape in zip(init_params, shapes)]
    init_params[0] = init_params[0][:n_ctx]
    init_params[0] = np.concatenate([init_params[1], (np.random.randn(n_special, n_embd)*0.02).astype(np.float32), init_params[0]], 0)
    del init_params[1]

    if n_transfer == -1:
        n_transfer = 0
    else:
        n_transfer = 1+n_transfer*12
    sess.run([p.assign(ip) for p, ip in zip(params[:n_transfer], init_params[:n_transfer])])

    eval_mgpu_logits, eval_mgpu_clf_losses, eval_mgpu_lm_losses = mgpu_predict(X_train, M_train, Y_train)
    eval_logits, eval_clf_losses, eval_lm_losses = model(X, M, Y, train=False, reuse=True)
    eval_clf_loss = tf.reduce_mean(eval_clf_losses)
    eval_mgpu_clf_loss = tf.reduce_mean(eval_mgpu_clf_losses)

    n_updates = 0
    n_epochs = 0
    if dataset != 'stsb':
        trYt = trY
    if submit:
        save(os.path.join(save_dir, desc, 'best_params.jl'))
    best_score = 0
    for i in range(n_iter):
        for xmb, mmb, ymb in iter_data(*shuffle(trX, trM, trYt, random_state=np.random), n_batch=n_batch_train, truncate=True, verbose=True):
            cost, _ = sess.run([clf_loss, train], {X_train:xmb, M_train:mmb, Y_train:ymb})
            n_updates += 1
            if n_updates in [1000, 2000, 4000, 8000, 16000, 32000] and n_epochs == 0:
                log()
        n_epochs += 1
        log()
    if submit:
        sess.run([p.assign(ip) for p, ip in zip(params, joblib.load(os.path.join(save_dir, desc, 'best_params.jl')))])
        predict()
        if analysis:
            rocstories_analysis(data_dir, os.path.join(submission_dir, 'ROCStories.tsv'), os.path.join(log_dir, 'rocstories.jsonl'))

dataset.py

import os
import csv
import numpy as np

from tqdm import tqdm

from sklearn.utils import shuffle
from sklearn.model_selection import train_test_split

seed = 3535999445

def _rocstories(path):
    with open(path) as f:
        f = csv.reader(f)
        st = []
        ct1 = []
        ct2 = []
        y = []
        for i, line in enumerate(tqdm(list(f), ncols=80, leave=False)):
            if i > 0:
                s = ' '.join(line[1:5])
                c1 = line[5]
                c2 = line[6]
                st.append(s)
                ct1.append(c1)
                ct2.append(c2)
                y.append(int(line[-1])-1)
        return st, ct1, ct2, y

def rocstories(data_dir, n_train=1497, n_valid=374):
    storys, comps1, comps2, ys = _rocstories(os.path.join(data_dir, 'cloze_test_val__spring2016 - cloze_test_ALL_val.csv'))
    teX1, teX2, teX3, _ = _rocstories(os.path.join(data_dir, 'cloze_test_test__spring2016 - cloze_test_ALL_test.csv'))
    tr_storys, va_storys, tr_comps1, va_comps1, tr_comps2, va_comps2, tr_ys, va_ys = train_test_split(storys, comps1, comps2, ys, test_size=n_valid, random_state=seed)
    trX1, trX2, trX3 = [], [], []
    trY = []
    for s, c1, c2, y in zip(tr_storys, tr_comps1, tr_comps2, tr_ys):
        trX1.append(s)
        trX2.append(c1)
        trX3.append(c2)
        trY.append(y)

    vaX1, vaX2, vaX3 = [], [], []
    vaY = []
    for s, c1, c2, y in zip(va_storys, va_comps1, va_comps2, va_ys):
        vaX1.append(s)
        vaX2.append(c1)
        vaX3.append(c2)
        vaY.append(y)
    trY = np.asarray(trY, dtype=np.int32)
    vaY = np.asarray(vaY, dtype=np.int32)
    return (trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3)

opt.py

import math
import numpy as np
import tensorflow as tf

def warmup_cosine(x, warmup=0.002):
    s = tf.cast(x <= warmup, tf.float32)
    return s*(x/warmup) + (1-s)*(0.5 * (1 + tf.cos(math.pi * x)))

def warmup_constant(x, warmup=0.002):
    s = tf.cast(x <= warmup, tf.float32)
    return s*(x/warmup) + (1-s)*1

def warmup_linear(x, warmup=0.002):
    s = tf.cast(x <= warmup, tf.float32)
    return (s*(x/warmup) + (1-s))*(1-x)

schedules = {
    'warmup_cosine':warmup_cosine,
    'warmup_constant':warmup_constant,
    'warmup_linear':warmup_linear,
}

def adam(params, grads, lr, schedule, t_total, b1=0.9, b2=0.999, e=1e-8, l2=0, vector_l2=False, max_grad_norm=-1, **kwargs):
    """
    adam with weight decay fix
    """
    t = tf.Variable(0, dtype=tf.float32, trainable=False)
    tt = t+1
    updates = [t.assign(tt)]
    if max_grad_norm > 0:
        grads, _ = tf.clip_by_global_norm(grads, max_grad_norm)
    for p, g in zip(params, grads):
        if p is None or g is None:
            print("can't train", p.name, g)
        else:
            if isinstance(g, tf.IndexedSlices):
                g = tf.convert_to_tensor(g)
            m = tf.Variable(p*0, dtype=tf.float32, trainable=False)
            v = tf.Variable(p*0, dtype=tf.float32, trainable=False)
            lrt = lr*tf.sqrt(1-b2**tt)/(1-b1**tt)
            lrt *= schedule(t/t_total)
            mt = b1*m + (1-b1)*g
            vt = b2*v + (1-b2)*g*g
            if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0:
                pt = p - lrt * (mt / (tf.sqrt(vt) + e) + l2*p)
            else:
                pt = p - lrt * (mt / (tf.sqrt(vt) + e))
            updates.extend([m.assign(mt), v.assign(vt), p.assign(pt)])
    return tf.group(*updates)

text_utils.py

import re
import ftfy
import json
import spacy

from tqdm import tqdm

def get_pairs(word):
    """
    Return set of symbol pairs in a word.
    word is represented as tuple of symbols (symbols being variable-length strings)
    """
    pairs = set()
    prev_char = word[0]
    for char in word[1:]:
        pairs.add((prev_char, char))
        prev_char = char
    return pairs

def text_standardize(text):
    """
    fixes some issues the spacy tokenizer had on books corpus
    also does some whitespace standardization
    """
    text = text.replace('—', '-')
    text = text.replace('–', '-')
    text = text.replace('―', '-')
    text = text.replace('…', '...')
    text = text.replace('´', "'")
    text = re.sub('''(-+|~+|!+|"+|;+|\?+|\++|,+|\)+|\(+|\\+|\/+|\*+|\[+|\]+|}+|{+|\|+|_+)''', r' \1 ', text)
    text = re.sub('\s*\n\s*', ' \n ', text)
    text = re.sub('[^\S\n]+', ' ', text)
    return text.strip()

class TextEncoder(object):
    """
    mostly a wrapper for a public python bpe tokenizer
    """

    def __init__(self, encoder_path, bpe_path):
        self.nlp = spacy.load('en', disable=['parser', 'tagger', 'ner', 'textcat'])
        self.encoder = json.load(open(encoder_path))
        self.decoder = {v:k for k,v in self.encoder.items()}
        merges = open(bpe_path).read().split('\n')[1:-1]
        merges = [tuple(merge.split()) for merge in merges]
        self.bpe_ranks = dict(zip(merges, range(len(merges))))
        self.cache = {}

    def bpe(self, token):
        word = tuple(token[:-1]) + ( token[-1] + '</w>',)
        if token in self.cache:
            return self.cache[token]
        pairs = get_pairs(word)

        if not pairs:
            return token+'</w>'

        while True:
            bigram = min(pairs, key = lambda pair: self.bpe_ranks.get(pair, float('inf')))
            if bigram not in self.bpe_ranks:
                break
            first, second = bigram
            new_word = []
            i = 0
            while i < len(word):
                try:
                    j = word.index(first, i)
                    new_word.extend(word[i:j])
                    i = j
                except:
                    new_word.extend(word[i:])
                    break

                if word[i] == first and i < len(word)-1 and word[i+1] == second:
                    new_word.append(first+second)
                    i += 2
                else:
                    new_word.append(word[i])
                    i += 1
            new_word = tuple(new_word)
            word = new_word
            if len(word) == 1:
                break
            else:
                pairs = get_pairs(word)
        word = ' '.join(word)
        if word == '\n  </w>':
            word = '\n</w>'
        self.cache[token] = word
        return word

    def encode(self, texts, verbose=True):
        texts_tokens = []
        if verbose:
            for text in tqdm(texts, ncols=80, leave=False):
                text = self.nlp(text_standardize(ftfy.fix_text(text)))
                text_tokens = []
                for token in text:
                    text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
                texts_tokens.append(text_tokens)
        else:
            for text in texts:
                text = self.nlp(text_standardize(ftfy.fix_text(text)))
                text_tokens = []
                for token in text:
                    text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
                texts_tokens.append(text_tokens)
        return texts_tokens

utils.py

import os
import re
import sys
import json
import math
import time
import unicodedata
import numpy as np
import tensorflow as tf
from tensorflow.python.framework import function
from tqdm import tqdm
from functools import partial

def encode_dataset(*splits, encoder):
    encoded_splits = []
    for split in splits[0]:
        fields = []
        for field in split:
            if isinstance(field[0], str):
                field = encoder.encode(field)
            fields.append(field)
        encoded_splits.append(fields)
    return encoded_splits

def stsb_label_encoding(labels, nclass=6):
    """
    Label encoding from Tree LSTM paper (Tai, Socher, Manning)
    """
    Y = np.zeros((len(labels), nclass)).astype(np.float32)
    for j, y in enumerate(labels):
        for i in range(nclass):
            if i == np.floor(y) + 1:
                Y[j,i] = y - np.floor(y)
            if i == np.floor(y):
                Y[j,i] = np.floor(y) - y + 1
    return Y

def shape_list(x):
    """
    deal with dynamic shape in tensorflow cleanly
    """
    ps = x.get_shape().as_list()
    ts = tf.shape(x)
    return [ts[i] if ps[i] is None else ps[i] for i in range(len(ps))]

def np_softmax(x, t=1):
    x = x/t
    x = x - np.max(x, axis=-1, keepdims=True)
    ex = np.exp(x)
    return ex/np.sum(ex, axis=-1, keepdims=True)

def make_path(f):
    d = os.path.dirname(f)
    if d and not os.path.exists(d):
        os.makedirs(d)
    return f

def _identity_init(shape, dtype, partition_info, scale):
    n = shape[-1]
    w = np.eye(n)*scale
    if len([s for s in shape if s != 1]) == 2:
        w = w.reshape(shape)
    return w.astype(np.float32)

def identity_init(scale=1.0):
    return partial(_identity_init, scale=scale)

def _np_init(shape, dtype, partition_info, w):
    return w

def np_init(w):
    return partial(_np_init, w=w)

class ResultLogger(object):
    def __init__(self, path, *args, **kwargs):
        if 'time' not in kwargs:
            kwargs['time'] = time.time()
        self.f_log = open(make_path(path), 'w')
        self.f_log.write(json.dumps(kwargs)+'\n')

    def log(self, **kwargs):
        if 'time' not in kwargs:
            kwargs['time'] = time.time()
        self.f_log.write(json.dumps(kwargs)+'\n')
        self.f_log.flush()

    def close(self):
        self.f_log.close()

def find_trainable_variables(key):
    return tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, ".*{}.*".format(key))

def flatten(outer):
    return [el for inner in outer for el in inner]

def remove_none(l):
    return [e for e in l if e is not None]

def iter_data(*datas, n_batch=128, truncate=False, verbose=False, max_batches=float("inf")):
    n = len(datas[0])
    if truncate:
        n = (n//n_batch)*n_batch
    n = min(n, max_batches*n_batch)
    n_batches = 0
    if verbose:
        f = sys.stderr
    else:
        f = open(os.devnull, 'w')
    for i in tqdm(range(0, n, n_batch), total=n//n_batch, file=f, ncols=80, leave=False):
        if n_batches >= max_batches: raise StopIteration
        if len(datas) == 1:
            yield datas[0][i:i+n_batch]
        else:
            yield (d[i:i+n_batch] for d in datas)
        n_batches += 1

@function.Defun(
    python_grad_func=lambda x, dy: tf.convert_to_tensor(dy),
    shape_func=lambda op: [op.inputs[0].get_shape()])
def convert_gradient_to_tensor(x):
    """force gradient to be a dense tensor
    it's often faster to do dense embedding gradient on GPU than sparse on CPU
    """
    return x

def assign_to_gpu(gpu=0, ps_dev="/device:CPU:0"):
    def _assign(op):
        node_def = op if isinstance(op, tf.NodeDef) else op.node_def
        if node_def.op == "Variable":
            return ps_dev
        else:
            return "/gpu:%d" % gpu
    return _assign

def average_grads(tower_grads):
    def average_dense(grad_and_vars):
        if len(grad_and_vars) == 1:
            return grad_and_vars[0][0]

        grad = grad_and_vars[0][0]
        for g, _ in grad_and_vars[1:]:
            grad += g
        return grad / len(grad_and_vars)

    def average_sparse(grad_and_vars):
        if len(grad_and_vars) == 1:
            return grad_and_vars[0][0]

        indices = []
        values = []
        for g, _ in grad_and_vars:
            indices += [g.indices]
            values += [g.values]
        indices = tf.concat(indices, 0)
        values = tf.concat(values, 0)
        return tf.IndexedSlices(values, indices, grad_and_vars[0][0].dense_shape)

    average_grads = []
    for grad_and_vars in zip(*tower_grads):
        if grad_and_vars[0][0] is None:
            grad = None
        elif isinstance(grad_and_vars[0][0], tf.IndexedSlices):
            grad = average_sparse(grad_and_vars)
        else:
            grad = average_dense(grad_and_vars)
        v = grad_and_vars[0][1]
        grad_and_var = (grad, v)
        average_grads.append(grad_and_var)
    return average_grads

1. 사전 학습 (Unsupervised Pre-Training)

논문에서 언급한 것처럼, 대규모의 라벨이 없는 텍스트 데이터를 이용해 언어 모델을 먼저 학습한 후, 특정 과제에 맞춰 미세 조정합니다. text_utils.py에 있는 TextEncoder 클래스가 이러한 텍스트를 토큰화하고 인코딩하는 작업을 담당하고 있습니다. 또한, BPE(Byte Pair Encoding) 방식을 사용해 서브워드 토큰화를 진행하고 있습니다. 이러한 사전 학습 단계는 학습된 언어 모델이 다양한 과제에 적용될 수 있도록 중요한 언어적 정보를 습득하게 만듭니다.

2. 과제별 미세 조정 (Fine-Tuning)

논문에서는 언어 모델을 각 과제에 맞춰 최소한의 구조 변화로 미세 조정하는 방법을 설명합니다. train.py 파일에서는 이 과정을 다루며, 예를 들어 transform_roc() 함수는 ROCStories 데이터셋을 처리하기 위한 입력 변환을 수행합니다. ROCStories는 논문에서도 언급된 commonsense reasoning(상식 추론) 과제를 평가하는 데 사용되었으며, 여기서 미세 조정이 이루어집니다.

3. 최적화 전략 (Optimization Strategies)

논문에서는 Adam 옵티마이저와 학습률 조정(워밍업 및 코사인 스케줄링)을 사용하여 성능을 향상시키는 방법을 설명합니다. 이와 관련하여 opt.py 파일에서는 Adam 옵티마이저와 다양한 학습률 스케줄링 방식을 정의하고 있습니다. 이는 논문의 학습 전략과 매우 유사하며, 코드에서도 이러한 최적화 방식을 적용하여 모델 학습을 효율적으로 수행합니다.

4. 모델 구조 (Model Architecture)

논문에서 사용된 Transformer 구조는 긴 문맥을 처리하는 데 뛰어난 성능을 보입니다. train.py 파일에서는 model() 함수 내에서 Transformer 레이어Attention 메커니즘(e.g., _attn(), attn())을 활용하여 모델을 구성합니다. 이러한 구조는 긴 문맥 의존성을 처리하면서 텍스트의 상관관계를 파악하는 데 탁월한 성능을 발휘합니다.

5. 손실 함수 및 보조 목적 (Loss Functions and Auxiliary Objectives)

논문에서는 언어 모델 손실을 보조 목적(auxiliary objective)으로 사용하여 모델의 일반화 성능을 향상시키는 방법을 설명합니다. 코드에서 clf_losslm_loss는 각각 분류 손실언어 모델 손실을 처리하며, 이를 통해 모델의 성능을 더욱 강화합니다.

6. 평가 및 로깅 (Evaluation and Logging)

analysis.py 파일에서는 ROCStories와 같은 과제의 정확도를 계산하는 방법을 설명하고, ResultLogger 클래스( utils.py)는 실험 결과를 기록하는 데 사용됩니다. 이 로깅 방식은 논문에서 언급된 실험 결과 추적과 일치합니다.


코드 파일 순서

  1. dataset.py: 데이터셋을 어떻게 처리하고 준비하는지 확인하는 것이 중요합니다. 여기서는 ROCStories 데이터를 어떻게 불러오고 분리하는지 설명하고 있으니, 모델 학습에 사용할 데이터셋 구조를 파악하는 데 유용합니다.
  2. text_utils.py: 이 파일은 텍스트 인코딩 및 토큰화를 담당하는 부분입니다. 언어 모델의 입력을 어떻게 준비하는지 이해하는 것이 중요하므로, 이 부분을 먼저 이해하는 것이 좋습니다.
  3. train.py: 이 파일은 실제 모델 학습 및 평가의 주요 흐름을 담당합니다. Transformer 모델의 학습 방식과 최적화 전략, 손실 계산 등을 다룹니다. 코드의 메인 학습 과정이 여기에서 이루어지므로 중요한 파일입니다.
  4. opt.py: 학습 과정에서 중요한 역할을 하는 Adam 옵티마이저와 학습률 스케줄링 관련 함수들이 있습니다. 최적화 전략이 논문과 어떻게 일치하는지 확인하기 위해 이 파일을 살펴볼 필요가 있습니다.
  5. analysis.py: 모델이 학습된 후 평가 과정을 수행하는 부분입니다. 여기서는 ROCStories 데이터셋의 테스트 및 검증 정확도를 계산합니다.
  6. utils.py: 다양한 보조 함수들이 모여있는 파일입니다. 데이터셋 인코딩, 로깅, 그리고 기타 유틸리티 함수들이 포함되어 있어, 모델을 전반적으로 관리하는 데 중요한 역할을 합니다.

코드 작동 요약

1. datasets.py (데이터 로딩과 전처리)

이 파일은 프로젝트에서 데이터를 불러오고, 모델에 입력할 수 있는 형태로 전처리하는 역할을 담당하고 있다.

  • 데이터 파일을 불러오는 함수
  • 데이터 전처리 로직 (예: 텍스트 토큰화, 데이터 정규화)
  • 데이터셋을 학습용, 검증용, 테스트용으로 나누는 로직

주요 구성 요소

  1. 라이브러리 임포트:
    • os, csv: 파일 시스템과 CSV 파일을 다루기 위한 모듈.
    • numpy: 수치 연산을 위한 필수 라이브러리.
    • tqdm: 진행 상황을 표시해주는 툴.
    • sklearn: 데이터셋을 섞거나, 훈련과 검증 데이터셋을 나누기 위한 shuffletrain_test_split을 사용.
  2. _rocstories(path) 함수:
    • 특정 경로에서 CSV 파일을 읽어들이며, 각 행에서 스토리, 선택지 1, 선택지 2, 그리고 정답 레이블을 추출.
    • 스토리와 선택지들을 리스트에 저장하고, 정답 레이블은 마지막 열에서 가져온 후 0부터 시작하도록 조정.
  3. rocstories(data_dir, n_train, n_valid) 함수:
    • 주어진 data_dir에서 훈련 및 검증용 데이터를 불러와서 전처리.
    • 훈련 데이터와 검증 데이터를 train_test_split으로 나누며, 이때 n_trainn_valid 인수로 각각 훈련 및 검증 데이터셋 크기를 결정.
    • 각각의 스토리, 선택지, 그리고 정답 레이블을 훈련용과 검증용으로 나누어 반환.

이 파일은 데이터셋을 불러와서 스토리와 선택지, 그리고 정답으로 이루어진 리스트들을 모델 학습에 사용할 수 있는 형태로 나누고 있음. 데이터셋 처리 로직이 명확하게 정의되어 있고, 학습에 필요한 훈련 및 검증 데이터셋을 준비하는 역할.

주요 함수

1. _rocstories(path) 함수

def _rocstories(path):
    with open(path) as f:
        f = csv.reader(f)
        st = []  # 스토리 데이터
        ct1 = []  # 선택지 1
        ct2 = []  # 선택지 2
        y = []  # 정답 레이블
        for i, line in enumerate(tqdm(list(f), ncols=80, leave=False)):
            if i > 0:  # 첫 번째 행은 건너뜀 (보통 CSV의 헤더)
                s = ' '.join(line[1:5])  # 스토리 텍스트를 합침
                c1 = line[5]  # 첫 번째 선택지
                c2 = line[6]  # 두 번째 선택지
                st.append(s)
                ct1.append(c1)
                ct2.append(c2)
                y.append(int(line[-1])-1)  # 마지막 열이 정답이며, 인덱스를 0부터 시작하도록 조정
        return st, ct1, ct2, y

이 함수는 CSV 파일을 읽어서 데이터를 나누고 있어. 각 행에서 스토리, 선택지 1, 선택지 2, 그리고 정답 레이블을 추출한 후, 각각 리스트로 반환해. 주요 전처리 과정은 스토리와 선택지를 분리하고 정답 레이블을 0부터 시작하는 형식으로 바꾸는 것.

🍭

for i, line in enumerate(tqdm(list(f), ncols=80, leave=False)) 설명

enumerate는 파일을 읽어들인 각 줄(line)과 그 줄의 번호(i)를 반환하는 역할을 한다.

tqdm은 진행 상황(progress bar)을 보여주는 라이브러리로, 반복이 얼마나 진행되었는지 시각적으로 나타낸다.

STEP1. f: CSV 파일 객체로, csv.reader(f)를 통해 파일의 각 줄을 리스트로 읽어들인다.

STEP2. list(f): CSV 파일을 리스트로 변환해 각 줄을 순차적으로 읽을 수 있게 한다.

STEP3. tqdm(list(f), ncols=80, leave=False): tqdm을 사용해 파일을 읽는 동안 진행 상황(progress bar)을 보여준다. ncols=80은 진행 바의 너비를 80으로 설정하고, leave=False는 완료된 후 진행 바를 지우는 역할을 한다.

코드의 동작:

  • enumerate: tqdm(list(f))를 순회하면서 i는 현재 줄의 인덱스를 나타내고, line은 해당 줄의 데이터를 나타낸다.
  • for i, line in enumerate(tqdm(list(f), ncols=80, leave=False))은 파일을 한 줄씩 읽으면서 줄 번호(i)와 해당 줄의 내용(line)을 반복적으로 가져오는 루프이다.

2. rocstories(data_dir, n_train=1497, n_valid=374) 함수

def rocstories(data_dir, n_train=1497, n_valid=374):
    storys, comps1, comps2, ys = _rocstories(os.path.join(data_dir, 'cloze_test_val__spring2016 - cloze_test_ALL_val.csv'))
    teX1, teX2, teX3, _ = _rocstories(os.path.join(data_dir, 'cloze_test_test__spring2016 - cloze_test_ALL_test.csv'))
    tr_storys, va_storys, tr_comps1, va_comps1, tr_comps2, va_comps2, tr_ys, va_ys = train_test_split(
        storys, comps1, comps2, ys, test_size=n_valid, random_state=seed
    )

    # 학습 데이터를 저장하는 리스트
    trX1, trX2, trX3 = [], [], []
    trY = []
    for s, c1, c2, y in zip(tr_storys, tr_comps1, tr_comps2, tr_ys):
        trX1.append(s)
        trX2.append(c1)
        trX3.append(c2)
        trY.append(y)

    # 검증 데이터를 저장하는 리스트
    vaX1, vaX2, vaX3 = [], [], []
    vaY = []
    for s, c1, c2, y in zip(va_storys, va_comps1, va_comps2, va_ys):
        vaX1.append(s)
        vaX2.append(c1)
        vaX3.append(c2)
        vaY.append(y)

    return (trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3)

이 함수는 데이터를 훈련, 검증, 테스트 데이터셋으로 나누고 있어:

  • 훈련 데이터_rocstories 함수를 통해 불러온 후, train_test_split을 사용해 훈련용과 검증용으로 나눔.
  • 학습용 데이터(trX1, trX2, trX3, trY)검증용 데이터(vaX1, vaX2, vaX3, vaY)를 별도의 리스트에 저장해 반환.
  • 테스트 데이터(teX1, teX2, teX3)도 _rocstories로 불러오고 그대로 반환.

각 데이터셋은 다음과 같은 형식으로 나뉨:

  • X1: 스토리 데이터
  • X2: 선택지 1
  • X3: 선택지 2
  • Y: 정답 레이블

이 함수는 주로 데이터를 불러오고, 훈련 및 검증용으로 나누는 작업을 처리해. 이를 통해 모델이 학습할 수 있는 준비된 데이터셋을 만들 수 있어.

데이터 흐름:

  1. 데이터 불러오기: CSV 파일에서 스토리와 선택지 데이터를 불러옴.
  2. 데이터 나누기: train_test_split을 사용해 훈련 데이터와 검증 데이터를 분할.
  3. 데이터 전처리: 스토리와 선택지 데이터를 적절히 리스트에 저장하고, 정답 레이블도 함께 반환.

2. text_utils.py (텍스트 전처리 및 토크나이징)

이 파일은 텍스트 데이터를 전처리하고, 모델에 맞는 형식으로 변환하는 여러 유틸리티 함수들을 포함하고 있다. BPE(Byte Pair Encoding) 토크나이저와 같은 복잡한 텍스트 처리를 수행

  • 텍스트 표준화(특정 특수문자를 치환하거나 공백 정리)
  • BPE 토크나이저를 사용해 텍스트 데이터를 토큰화
  • Spacy 라이브러리를 사용하여 기본적인 텍스트 전처리를 수행
🤫

BPE(Byte Pair Encoding)이란?

Byte Pair Encoding (BPE)은 원래 1994년에 제안된 데이터 압축 알고리즘으로, 이후 자연어 처리(NLP) 분야에서 서브워드 분리(Subword Segmentation) 방식으로 응용되었다. 이 알고리즘은 주로 OOV(Out-Of-Vocabulary) 문제를 해결하기 위해 사용되며, 특히 희귀 단어, 신조어, 복잡한 어휘를 처리할 때 매우 유용하다.

OOV(Out-Of-Vocabulary) 문제

기계 학습 모델이 모르는 단어를 처리할 때 발생하는 문제를 OOV 문제라고 한다. 전통적인 NLP 모델들은 훈련 과정에서 학습한 단어들만 처리할 수 있기 때문에, 훈련 데이터에 없었던 단어가 등장하면 이 단어를 UNK(Unknown Token)으로 처리하게 된다. 이 때문에 모델이 해당 단어의 의미를 이해하지 못해 성능 저하가 발생할 수 있다.

서브워드 분리(Subword Segmentation)

서브워드 분리는 단어를 더 작은 의미 단위인 서브워드로 분해하는 방법이다. 예를 들어, birthplace라는 단어는 birth + place로 분해될 수 있다. 이러한 서브워드 분리는 희귀 단어나 신조어를 처리할 때 매우 유용하다. 이를 통해 모델이 모르는 단어를 더 작은 단위로 나누어 처리할 수 있게 된다.

BPE의 원리

BPE는 글자를 서브워드 단위로 병합하면서 단어 집합을 만들어내는 Bottom-up 방식의 알고리즘이다. BPE의 기본 아이디어는 가장 자주 등장하는 글자 쌍을 찾아서 하나의 새로운 서브워드로 병합하는 것이다. 이를 반복적으로 적용해 단어를 점차적으로 더 큰 단위의 서브워드로 변환한다.

BPE의 동작 방식:

  1. 초기 상태: 모든 단어를 글자 단위로 나눈다.
  2. 빈도 계산: 각 글자 쌍의 빈도를 계산하고, 가장 자주 등장하는 쌍을 병합한다.
  3. 병합 반복: 위 과정을 반복하여 글자 쌍을 서브워드 단위로 병합해 나간다.
  4. 종료: 정해진 횟수(또는 조건)를 만족하면 병합을 종료하고, 최종적으로 서브워드 단위로 변환된 단어 집합을 얻는다.

BPE 알고리즘 예시

다음 예시에서는 aaabdaaabac이라는 문자열에 대해 BPE를 적용한 과정을 보여준다.

  1. 가장 자주 등장하는 글자 쌍을 찾는다. 이 경우 *aa가 가장 많이 등장한다.
  2. aaZ*로 치환한다:
    이제
    Z = aa*로 정의된다.
    ```
    aaabdaaabac -> ZabdZabac
    ```
  3. 다음으로, 가장 자주 등장하는 쌍 ab*를 찾아 Y로 치환한다:
    이제 **Y = ab
    로 정의된다.
    ```
    ZabdZabac -> ZYdZYac
    ```
  4. 그 다음으로 자주 등장하는 쌍 ZY*를 찾아 X로 치환한다:
    이제 **X = ZY
    로 정의된다.
    ```
    ZYdZYac -> XdXac
    ```

이와 같이 BPE는 자주 등장하는 글자 쌍을 점진적으로 병합하여 최종적으로 의미 있는 서브워드 집합을 구성한다.

자연어 처리에서의 BPE

자연어 처리에서 BPE는 글자 단위에서 서브워드 단위로 변환하는 서브워드 분리 알고리즘으로 사용된다. BPE는 단어 집합을 처음부터 학습하지 않고, 글자 단위에서 시작하여 가장 자주 등장하는 글자 쌍을 병합해 단어 집합을 구축하는 방식으로 작동한다. 이 방식은 특히 신조어나 희귀 단어, 복잡한 어휘 구조를 다루는 데 유리하다.

BPE 알고리즘 적용 예시:

훈련 데이터에서 자주 등장하는 단어가 다음과 같다고 가정해보자:

low : 5, lower : 2, newest : 6, widest : 3

먼저, 각 단어를 글자 단위로 분해한다:

l o w : 5, l o w e r : 2, n e w e s t : 6, w i d e s t : 3

이제 가장 자주 등장하는 쌍을 찾아 병합하는 과정을 진행한다.

  1. 빈도수가 가장 높은 쌍은 e*와 s이므로 이를 **es로 병합한다:

    l o w : 5, l o w e r : 2, n e w es t : 6, w i d es t : 3
  2. 그 다음 빈도수가 가장 높은 쌍 es*와 t를 **est로 병합한다:

    l o w : 5, l o w e r : 2, n e w est : 6, w i d est : 3
  3. 계속해서 가장 자주 등장하는 쌍을 병합한다. l*과 o가 자주 등장하므로 **lo로 병합한다:

    lo w : 5, lo w e r : 2, n e w est : 6, w i d est : 3

이 과정을 총 10회 반복하면, 각 단어가 더 큰 서브워드로 병합된다. 이를 통해 단어 집합을 구성하게 된다:

low, lower, newest, widest

이제 lowest라는 새로운 단어가 등장했을 때, 기존에는 OOV 문제로 처리할 수 없었지만, BPE를 통해 lowest로 분할하여 처리할 수 있다.

BPE의 장점:

  1. OOV 문제 해결: 기존에는 처리할 수 없던 희귀 단어를 서브워드 단위로 분해하여 처리할 수 있다.
  2. 효율적인 단어 집합 관리: BPE는 정해진 어휘 크기 내에서 다양한 단어를 처리할 수 있게 해준다.
  3. 다양한 언어에 적용 가능: 특히 형태소 변형이 많거나 합성어가 많이 사용되는 언어에서도 효과적으로 사용될 수 있다.

주요 구성 요소:

  1. get_pairs(word):

    • 주어진 단어에서 연속된 두 글자의 쌍을 추출해 반환하는 함수. BPE를 적용하기 위한 기본적인 함수
  2. text_standardize(text):

    • 텍스트 데이터를 표준화하는 함수로, 특정 문자(예: 대시 등)를 대체하고, 여러 공백을 하나의 공백으로 줄이거나 줄바꿈 문자를 처리하는 등의 작업을 수행.
    • 이 과정은 모델이 텍스트를 쉽게 처리할 수 있도록 정규화를 수행하는데, 특히 Spacy 토크나이저와의 호환성 문제를 해결하는 데 중점을 두고 있음.
  3. TextEncoder 클래스:

    • BPE 토크나이저를 위한 wrapper 클래스.
    • encoder_pathbpe_path를 받아 BPE 토크나이저를 초기화하고, BPE 토큰화를 수행하는 메서드들이 포함.
    • Spacy 라이브러리를 사용해 기본적인 자연어 처리를 하고, BPE 병합 규칙을 기반으로 토큰을 인코딩하는 로직을 포함.

    텍스트 데이터의 전처리 및 토크나이징에 중요한 역할을 하며, 특히 BPE를 사용해 텍스트를 모델에 맞는 토큰으로 변환하는 데 중점을 두고 있음.

1. get_pairs(word) 함수

def get_pairs(word):
    """
    Return set of symbol pairs in a word.
    word is represented as tuple of symbols (symbols being variable-length strings)
    """
    pairs = set()
    prev_char = word[0]
    for char in word[1:]:
        pairs.add((prev_char, char))
        prev_char = char
    return pairs

이 함수는 단어에서 문자 쌍을 추출하는 역할을 한다. 이는 BPE 토크나이저에서 각 문자의 쌍을 찾아 병합 규칙을 적용할 때 사용된다. 주어진 단어에서 연속되는 두 글자의 쌍을 모두 추출해 set 형태로 반환한다.

2. text_standardize(text) 함수

def text_standardize(text):
    """
    Fixes some issues the spacy tokenizer had on books corpus
    Also does some whitespace standardization
    """
    text = text.replace('—', '-')
    text = text.replace('–', '-')
    text = text.replace('―', '-')
    text = text.replace('…', '...')
    text = text.replace('´', "'")
    text = re.sub(r'(-+|~+|!+|"+|;+|\\?+|\\++|,+|\\)+|\\(+|\\\\+|/+|\\*+|\\[+|\\]+|}+|{+|\\|+|_+)', r' \\1 ', text)
    text = re.sub(r'\\s*\\n\\s*', ' \\n ', text)
    text = re.sub(r'[^\\S\\n]+', ' ', text)
    return text.strip()

이 함수는 텍스트 데이터를 표준화하는 역할을 한다. 주로 다음과 같은 작업을 수행한다:

  • 여러 가지 대시(, , )를 일반적인 하이픈(``)으로 변환.
  • 줄임표()를 점 세 개(...)로 치환.
  • 불필요한 공백이나 줄바꿈 문자를 제거하고, 텍스트에 포함된 여러 특수문자들을 적절히 처리.

이 함수는 텍스트 데이터를 모델이 처리하기 쉽도록 정규화하는 과정에서 사용된다.

🍭

re.sub()

re.sub() 함수는 파이썬의 정규 표현식 모듈re에서 제공하는 함수로, 특정 패턴을 찾아서 다른 문자열로 치환하는 역할을 한다. 이를 사용하면 문자열에서 특정한 규칙에 맞는 부분을 찾고, 그 부분을 다른 값으로 대체할 수 있다.

re.sub() 문법:

re.sub(pattern, repl, string, count=0, flags=0)
  • pattern: 교체할 패턴(정규 표현식).
  • repl: 대체할 문자열.
  • string: 대상이 되는 원본 문자열.
  • count: 치환할 횟수(기본값은 0, 즉 모두 치환).
  • flags: 정규 표현식 플래그.

예시:

import re
text = "Hello World!"
new_text = re.sub(r'World', 'Python', text)
print(new_text)  # "Hello Python!"

위 예시에서는 World라는 패턴을 찾아 Python으로 치환했다.


실제 코드 :

text = re.sub(r'(-+|~+|!+|"+|;+|\\\\?+|\\\\++|,+|\\\\)+|\\\\(+|\\\\\\\\+|/+|\\\\*+|\\\\[+|\\\\]+|}+|{+|\\\\|+|_+)', r' \\\\1 ', text)

이 코드는 여러 특수 문자들이 연속으로 등장할 때, 그 특수 문자 앞뒤에 공백을 추가하는 작업을 한다.

세부 분석:

  1. 패턴: (-+|~+|!+|"+|;+|\\\\?+|\\\\++|,+|\\\\)+|\\\\(+|\\\\\\\\+|/+|\\\\*+|\\\\[+|\\\\]+|}+|{+|\\\\|+|_+)
    • +: 하나 이상의 대시(하이픈)가 등장하는 패턴.
    • ~+: 하나 이상의 물결표가 등장하는 패턴.
    • !+: 하나 이상의 느낌표가 등장하는 패턴.
    • "+: 하나 이상의 큰따옴표가 등장하는 패턴.
    • ;+: 하나 이상의 세미콜론이 등장하는 패턴.
    • \\\\?+: 하나 이상의 물음표가 등장하는 패턴 (\\\\는 이스케이프 문자를 의미).
    • 나머지 패턴들도 같은 방식으로 여러 특수 문자들이 하나 이상 연속적으로 등장할 때를 의미한다.
  2. 치환 문자열: ' \\\\1 '
    • \\\\1: 찾은 패턴(첫 번째 캡처 그룹)을 의미한다. 이 부분은 그대로 유지하되, 앞뒤에 공백을 추가한다.
    • 예를 들어, Hello!!!라는 문자열이 주어지면 Hello !!! 처럼 변환된다.

전체 동작:

이 코드는 하나 이상의 특수 문자가 연속으로 등장하면 그 앞뒤에 공백을 추가하는 역할을 한다.


text = re.sub(r'\\\\s*\\\\n\\\\s*', ' \\\\n ', text)

이 코드는 줄바꿈 문자 \\n 앞뒤의 공백을 제거하고, 줄바꿈 문자 앞뒤에 한 칸의 공백을 추가한다.

세부 분석:

  1. 패턴: \\\\s*\\\\n\\\\s*
    • \\\\s*: 0개 이상의 공백 문자(공백, 탭 등)를 의미한다.
    • \\\\n: 줄바꿈 문자 \\n을 의미한다.
    • 이 패턴은 줄바꿈 문자 앞뒤에 있는 공백을 찾는다.
  2. 치환 문자열: ' \\\\n '
    • 줄바꿈 문자 \\n 앞뒤에 한 칸의 공백을 추가한다.

전체 동작:

이 코드는 줄바꿈 문자 \\n 앞뒤에 공백이 있으면 이를 정리하고, 공백을 일정하게 한 칸 추가하는 역할을 한다.


text = re.sub(r'[^\\\\S\\\\n]+', ' ', text)

이 코드는 연속된 공백을 하나의 공백으로 압축한다.

세부 분석:

  1. 패턴: [^\\\\S\\\\n]+
    • \\\\S: 공백이 아닌 문자를 의미한다.
    • [^...]: 대괄호 내의 패턴이 아닌 문자를 의미한다. 즉, 공백 문자를 의미.
    • 따라서 공백 문자 중 줄바꿈 문자를 제외한 문자들을 찾는다.
    • +: 하나 이상의 공백이 등장하는 경우를 의미.
  2. 치환 문자열: ' '
    • 여러 개의 연속된 공백을 하나의 공백으로 치환한다.

전체 동작:

이 코드는 여러 개의 공백을 한 칸의 공백으로 압축한다.


  • 첫 번째 줄: 여러 특수 문자가 연속해서 등장하면 그 앞뒤에 공백을 추가한다.
  • 두 번째 줄: 줄바꿈 문자의 앞뒤 공백을 정리하고, 일정한 공백을 추가한다.
  • 세 번째 줄: 여러 개의 공백을 한 칸의 공백으로 압축한다.

3. TextEncoder 클래스

class TextEncoder(object):
    """
    Mostly a wrapper for a public python bpe tokenizer
    """
    def __init__(self, encoder_path, bpe_path):
        self.nlp = spacy.load('en', disable=['parser', 'tagger', 'ner', 'textcat'])
        self.encoder = json.load(open(encoder_path))
        self.decoder = {v:k for k,v in self.encoder.items()}
        merges = open(bpe_path).read().split('\\n')[1:-1]
        merges = [tuple(merge.split()) for merge in merges]
        self.bpe_ranks = dict(zip(merges, range(len(merges))))
        self.cache = {}

    def encode(self, texts, verbose=True):
        texts_tokens = []
        if verbose:
            for text in tqdm(texts, ncols=80, leave=False):
                text = self.nlp(text_standardize(ftfy.fix_text(text)))
                text_tokens = []
                for token in text:
                    text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
                texts_tokens.append(text_tokens)
        else:
            for text in texts:
                text = self.nlp(text_standardize(ftfy.fix_text(text)))
                text_tokens = []
                for token in text:
                    text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
                texts_tokens.append(text_tokens)
        return texts_tokens

이 클래스는 BPE 토크나이저를 래핑한 클래스로, 텍스트 데이터를 BPE 방식으로 토큰화하는 역할을 한다. 주요 초기화 작업은 다음과 같다:

  • Spacy를 사용해 기본적인 자연어 처리를 설정. (parser, tagger, ner, textcat 등의 기능은 비활성화)
  • encoder_path에서 인코더(사전)를 불러오고, 이를 역으로 디코딩할 수 있도록 decoder 딕셔너리 생성.
  • BPE 병합 규칙을 bpe_path에서 불러오고, 이를 기반으로 병합 순위를 설정.
  • cache를 사용해 토큰화된 결과를 저장하여 성능을 향상시킴.
🍭

Spacy?

: Spacy는 파이썬에서 사용할 수 있는 오픈 소스 자연어 처리(NLP) 라이브러리입니다. Spacy는 텍스트 분석, 토크나이징, 품사 태깅, 개체명 인식(NER), 구문 분석 등 다양한 NLP 작업을 빠르고 효율적으로 처리하는 데 사용.

spacy.load('en', disable=...)

  • spacy.load('en'): 영어 모델을 불러오는 명령입니다. 이 모델은 영어 텍스트를 분석하고 처리하는 데 필요한 다양한 기능들을 포함하고 있습니다.
  • disable=['parser', 'tagger', 'ner', 'textcat']: Spacy의 기본 기능 중 특정한 기능들을 비활성화하고, 필요한 기능만 사용한다는 의미입니다.
🍭

비활성화된 기능들

  • parser: 구문 분석을 비활성화.
  • tagger: 품사 태깅을 비활성화.
  • ner: 개체명 인식(NER)을 비활성화.
  • textcat: 텍스트 분류를 비활성화.

복잡한 분석(예: 구문 분석, 품사 태깅, NER 등)은 불필요하다고 판단되어 비활성화.

→ Spacy의 여러 기능 중 NLP 모델이 텍스트 전처리토크나이징을 위해서만 사용.

4. bpe(self, token) 메서드

def bpe(self, token):
    if token in self.cache:
        return self.cache[token]
    word = tuple(token)
    pairs = get_pairs(word)

    if not pairs:
        return token

    while True:
        bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf')))
        if bigram not in self.bpe_ranks:
            break
        first, second = bigram
        new_word = []
        i = 0
        while i < len(word):
            j = word.index(first, i)
            new_word.extend(word[i:j])
            i = j
            if word[i] == first and i < len(word) - 1 and word[i+1] == second:
                new_word.append(first + second)
                i += 2
            else:
                new_word.append(word[i])
                i += 1
        word = tuple(new_word)
        pairs = get_pairs(word)
        if len(word) == 1:
            break

    word = ' '.join(word)
    self.cache[token] = word
    return word

이 메서드는 주어진 토큰에 대해 BPE 토큰화를 수행한다. 주어진 단어의 문자 쌍을 기반으로 BPE 병합 규칙을 적용하여 점차 긴 토큰을 생성하는 방식이다. 주요 작업은 다음과 같다:

  • 단어의 문자 쌍을 추출한 후, BPE 병합 규칙에서 가장 낮은 순위의 쌍을 찾아 병합.
  • 병합된 새로운 단어를 생성하고, 더 이상 병합할 쌍이 없으면 토큰화를 완료.
  • 캐시를 사용해 이미 토큰화된 단어는 다시 처리하지 않도록 최적화.
🍭

코드 세부 설명

1. 캐시 사용 (self.cache)

if token in self.cache:
    return self.cache[token]
  • 역할: 캐시(cache)에 이미 해당 단어가 있는지 확인한다.
  • 설명: 캐시는 BPE 처리가 이미 완료된 단어를 저장해 두는 곳이다. 만약 주어진 token이 이미 처리된 적이 있다면, 캐시에 저장된 결과를 바로 반환한다. 이를 통해 중복 계산을 피하고 성능을 향상시킨다.
  • 결과: 캐시에 저장된 결과가 있다면, 해당 결과를 반환하고 함수를 종료한다.

2. 단어를 튜플로 변환

word = tuple(token)
pairs = get_pairs(word)
  • 역할: 단어를 문자 단위로 튜플로 변환하고, 문자 쌍을 추출한다.
  • 설명:
    • tuple(token): 주어진 단어(token)를 문자 단위로 분해해 튜플로 만든다. 예를 들어, token = "hello"라면 word = ('h', 'e', 'l', 'l', 'o')로 변환된다.
    • get_pairs(word): 이 함수는 단어 내에서 인접한 문자 쌍을 추출하는 함수이다. 예를 들어, ('h', 'e', 'l', 'l', 'o')라면 문자 쌍은 [('h', 'e'), ('e', 'l'), ('l', 'l'), ('l', 'o')]가 된다.

3. 쌍이 없는 경우 (not pairs)

if not pairs:
    return token
  • 역할: 만약 단어에서 병합할 쌍이 없다면(즉, 단어가 하나의 문자만 있는 경우), 더 이상 처리가 필요 없으므로 원래의 token을 반환한다.

4. BPE 병합 루프 (while True)

while True:
    bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf')))
    if bigram not in self.bpe_ranks:
        break
    first, second = bigram
    new_word = []
  • 역할: BPE 병합 규칙을 사용해 가장 자주 등장하는 문자 쌍(bigram)을 찾아서 병합한다.
  • 설명:
    • min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf'))): BPE 병합 규칙(self.bpe_ranks)에 따라, 가장 낮은 순위(즉, 자주 등장하는) 문자 쌍을 찾는다. self.bpe_ranks.get(pair, float('inf'))는 병합 순위를 반환하며, 순위가 없으면 float('inf')를 반환하여 그 쌍이 선택되지 않게 한다.
    • bigram: 병합할 가장 자주 등장하는 문자 쌍.
    • if bigram not in self.bpe_ranks: break: 만약 bigram이 BPE 병합 규칙에 없다면, 더 이상 병합할 수 없으므로 루프를 종료한다.
    • first, second = bigram: 병합할 문자 쌍을 각각 첫 번째 문자두 번째 문자로 분리한다.
    • new_word = []: 새로운 단어를 저장할 빈 리스트를 준비한다.

5. 단어 내 문자 쌍 병합

i = 0
while i < len(word):
    j = word.index(first, i)
    new_word.extend(word[i:j])
    i = j
    if word[i] == first and i < len(word) - 1 and word[i+1] == second:
        new_word.append(first + second)
        i += 2
    else:
        new_word.append(word[i])
        i += 1
  • 역할: 주어진 단어에서 가장 자주 등장하는 문자 쌍을 찾아 병합한다.
  • 설명:
    • j = word.index(first, i): first 문자가 등장하는 위치를 찾는다.
    • new_word.extend(word[i:j]): first 문자 이전의 문자를 new_word에 추가한다.
    • if word[i] == first and word[i+1] == second: 현재 문자와 그 다음 문자가 first, second일 때, 이를 병합하여 하나의 문자로 추가한다.
    • i += 2: 병합이 이루어졌으므로 두 문자를 건너뛴다.
    • 병합이 안 될 경우: 현재 문자를 그대로 new_word에 추가하고, 한 문자씩 이동한다.

6. 병합된 단어 업데이트

word = tuple(new_word)
pairs = get_pairs(word)
if len(word) == 1:
    break
  • 역할: 병합된 단어를 업데이트하고, 다시 문자 쌍을 추출하여 병합할 쌍이 있는지 확인한다.
  • 설명:
    • word = tuple(new_word): 병합된 단어를 다시 튜플로 변환하여 다음 병합 작업을 준비한다.
    • pairs = get_pairs(word): 병합된 단어에서 새로운 문자 쌍을 추출한다.
    • if len(word) == 1: 만약 병합된 결과가 하나의 문자로 이루어진 단어라면, 더 이상 병합할 필요가 없으므로 루프를 종료한다.

7. 단어 반환 및 캐시 저장

word = ' '.join(word)
self.cache[token] = word
return word
  • 역할: 병합이 완료된 단어를 공백을 기준으로 결합하고, 이를 캐시에 저장한 후 반환한다.
  • 설명:
    • ' '.join(word): 병합된 단어의 서브워드들을 공백을 기준으로 연결한다. 예를 들어, ('low', 'est')라면 "low est"로 변환된다.
    • self.cache[token] = word: 해당 단어를 캐시에 저장하여, 나중에 동일한 단어가 입력될 경우 중복 계산을 피한다.
    • return word: BPE로 처리된 최종 서브워드 결과를 반환한다.

전체 흐름 요약:

  1. 캐시 확인: 캐시에 해당 단어가 있으면 캐시에서 결과를 반환한다.
  2. 문자 쌍 추출: 단어를 문자 단위로 나누고, 인접한 문자 쌍을 추출한다.
  3. BPE 병합 루프: 가장 자주 등장하는 문자 쌍을 찾아 병합하고, 병합할 쌍이 없으면 종료한다.
  4. 결과 반환: 최종적으로 병합된 서브워드 단위를 공백으로 연결하여 반환하고, 캐시에 저장한다.

전체 흐름:

  • get_pairs: 단어에서 문자 쌍을 추출.
  • text_standardize: 텍스트 데이터를 정규화.
  • TextEncoder: BPE 기반의 텍스트 인코더로, 텍스트 데이터를 BPE 방식으로 토큰화.

이 파일은 텍스트 데이터를 토크나이징하고, 모델에 입력하기 전 필요한 전처리 작업을 담당하고 있다. 이 과정은 특히 BPE를 사용해 텍스트를 서브워드 단위로 분해하여 처리할 수 있도록 돕는다.


3. opt.py (최적화 및 학습률 스케줄링)

이 파일은 학습 과정에서 사용할 최적화 알고리즘과 학습률 스케줄링 전략을 정의하고 있다. 주로 Adam 옵티마이저를 변형한 버전과 여러 가지 학습률 조정 방식을 포함하고 있다.

  • Warmup 학습률 스케줄링 함수 (Cosine, Constant, Linear)
  • Adam 옵티마이저의 수정 버전 (L2 정규화와 학습률 조정 포함)
  • 학습 중 기울기 클리핑(gradient clipping) 기능

주요 구성 요소:

  1. Warmup 학습률 스케줄링 함수들:

    • warmup_cosine(x, warmup=0.002): 학습 초기에 빠르게 학습률을 높이고, 이후에는 코사인 함수를 사용해 학습률을 점차 줄이는 방식.
    • warmup_constant(x, warmup=0.002): 학습 초기에는 학습률을 일정하게 높이고, 이후에는 일정하게 유지하는 방식.
    • warmup_linear(x, warmup=0.002): 초기에는 선형적으로 학습률을 증가시키고, 이후에는 선형적으로 감소시키는 방식.

    이러한 함수들은 학습 과정에서 학습률을 어떻게 조정할지 결정하는 데 사용돼.

  2. adam 함수:

    • Adam 최적화 알고리즘을 수정한 버전으로, L2 정규화를 추가하고, schedule을 사용해 학습률을 조절.
    • 각 파라미터에 대해 기울기(gradient)를 계산하고, 이를 바탕으로 모델 파라미터를 업데이트하는 과정을 정의.
    • max_grad_norm을 사용해 기울기 클리핑을 적용할 수 있어, 학습 안정성을 높이는 역할을 해.

이 파일은 학습 중에 최적화를 수행하고, 학습률을 동적으로 조정하는 중요한 부분을 다루고 있어. 특히, Adam 옵티마이저에 L2 정규화를 추가하는 기능이 눈에 띄며, 학습 스케줄링 전략을 통해 학습률을 조정할 수 있어.

opt.py 파일은 최적화(Optimization)와 학습률 스케줄링 관련된 코드로 구성되어 있다. 특히 학습 중에 모델 파라미터를 업데이트하는 역할을 담당하는 Adam 옵티마이저의 변형된 버전을 포함하고 있다. 또한, Warmup 전략을 적용해 학습 초기에 학습률을 조정하는 다양한 스케줄링 함수들이 정의되어 있다.

주요 구성 요소 분석:

1. warmup_cosine(x, warmup=0.002) 함수

def warmup_cosine(x, warmup=0.002):
    s = tf.cast(x <= warmup, tf.float32)
    return s*(x/warmup) + (1-s)*(0.5 * (1 + tf.cos(math.pi * x)))

이 함수는 코사인 학습률 스케줄링을 구현한 함수이다. 학습 초기에 학습률을 선형으로 증가시키고, 그 이후에는 코사인 함수를 사용해 학습률을 점차 감소시키는 방식으로 작동한다.

  • x <= warmup인 경우, 학습률은 선형적으로 증가한다.
  • 그 외의 경우, 코사인 함수를 사용해 학습률을 부드럽게 감소시킨다.

2. warmup_constant(x, warmup=0.002) 함수

def warmup_constant(x, warmup=0.002):
    s = tf.cast(x <= warmup, tf.float32)
    return s*(x/warmup) + (1-s)*1

이 함수는 상수 학습률 스케줄링을 구현한다. 초기에는 학습률을 선형적으로 증가시키고, 일정 구간 이후에는 학습률을 일정하게 유지하는 방식이다. Warmup 구간 이후 학습률은 고정된 값으로 유지된다.

3. warmup_linear(x, warmup=0.002) 함수

def warmup_linear(x, warmup=0.002):
    s = tf.cast(x <= warmup, tf.float32)
    return (s*(x/warmup) + (1-s))*(1-x)

이 함수는 선형 학습률 스케줄링을 구현한다. 학습 초기에 학습률을 선형적으로 증가시키고, 이후에도 선형적으로 감소시키는 방식이다. 학습률이 시간이 지남에 따라 일정하게 감소한다.

4. adam 함수

def adam(params, grads, lr, schedule, t_total, b1=0.9, b2=0.999, e=1e-8, l2=0, vector_l2=False, max_grad_norm=-1, **kwargs):
    """
    Adam with weight decay fix
    """
    t = tf.Variable(0, dtype=tf.float32, trainable=False)
    tt = t + 1
    updates = [t.assign(tt)]

    if max_grad_norm > 0:
        grads, _ = tf.clip_by_global_norm(grads, max_grad_norm)

    for p, g in zip(params, grads):
        if p is None or g is None:
            print("can't train", p.name, g)
        else:
            if isinstance(g, tf.IndexedSlices):
                g = tf.convert_to_tensor(g)
            m = tf.Variable(p*0, dtype=tf.float32, trainable=False)
            v = tf.Variable(p*0, dtype=tf.float32, trainable=False)
            lrt = lr * tf.sqrt(1 - b2**tt) / (1 - b1**tt)
            lrt *= schedule(t / t_total)
            mt = b1 * m + (1 - b1) * g
            vt = b2 * v + (1 - b2) * g * g

            if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0:
                lrt += l2 * p

            update = p - lrt * mt / (tf.sqrt(vt) + e)
            updates.append(p.assign(update))

    return tf.group(*updates)

이 함수는 Adam 옵티마이저를 변형한 버전으로, 학습률 스케줄링과 L2 정규화를 추가한 형태이다. 주요 기능은 다음과 같다:

  • 기울기 클리핑(gradient clipping): max_grad_norm을 통해 기울기의 크기가 너무 커지는 것을 방지한다.
  • Adam 알고리즘: 모멘텀(m)과 기울기 제곱(v)을 사용해 학습률을 적응적으로 조정한다.
  • L2 정규화: 선택적으로 파라미터에 대해 L2 정규화를 적용해 과적합을 방지한다.
  • 학습률은 스케줄링 함수(schedule)에 의해 동적으로 조정된다.

Adam 옵티마이저는 파라미터를 업데이트할 때, 학습률을 조정하며 모멘텀을 사용해 더욱 안정적으로 학습을 진행한다.

🍭

학습률 스케줄링과 L2 정규화를 추가

1. 학습률 스케줄링:

학습률 스케줄링은 schedule(t / t_total) 부분에서 이루어집니다. 이 부분은 학습률이 시간이 지남에 따라 어떻게 변화할지를 결정하는 스케줄링 함수에 따라 동적으로 학습률을 조정하는 역할을 합니다.

lrt *= schedule(t / t_total)

2. L2 정규화:

L2 정규화는 if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0: 조건문과 lrt += l2 * p 부분에서 적용됩니다.

if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0:
    lrt += l2 * p
  • l2 > 0: L2 정규화가 활성화되어 있는지 여부를 확인합니다.
  • p.get_shape() > 1: 파라미터 p가 다차원 배열(매트릭스 또는 벡터)인지 확인하여, 필요한 경우에만 정규화를 적용합니다. (스칼라 값에는 적용하지 않음)
  • lrt += l2 * p: L2 정규화는 파라미터 pl2 값을 곱하여 학습률(lrt)에 더하는 방식으로 적용됩니다. 이는 파라미터의 값이 너무 커지는 것을 방지하는 역할을 합니다.

전체 흐름 요약:

  1. Warmup 스케줄링:
    • warmup_cosine, warmup_constant, warmup_linear 함수를 통해 학습 초기에 학습률을 조정하는 다양한 방식의 스케줄링을 제공한다.
  2. Adam 옵티마이저:
    • adam 함수는 Adam 최적화 알고리즘을 기반으로 하며, 학습 중 기울기를 기반으로 모델 파라미터를 업데이트한다.
    • 기울기 클리핑과 L2 정규화를 통해 학습 안정성을 높이고 과적합을 방지한다.

이 파일은 학습 중 최적화를 수행하고, 학습률을 조정하는 데 중요한 역할을 한다. 특히, Adam 옵티마이저에 L2 정규화와 학습률 스케줄링을 추가하여 학습 성능을 더욱 향상시킨다.


4. train.py (모델 학습)

이 파일은 모델 학습의 메인 스크립트로, 전체 학습 과정을 제어하고 있다. 데이터를 불러오고, 모델을 학습시키며, 필요한 설정들을 초기화하는 과정이 포함되어 있다.

  • 데이터셋 로딩 (datasets.py를 통해 불러옴)
  • 최적화 함수와 학습률 스케줄링 설정 (opt.py 사용)
  • 모델의 학습 과정 및 활성화 함수 설정
  • 학습 중 정규화, 최적화, 결과 기록 등의 작업 수행

주요 구성 요소:

  1. 라이브러리 임포트:
    • opt.py에서 정의된 최적화 알고리즘(Adam)과 학습률 스케줄링 함수들을 불러와서 사용.
    • datasets.py에서 정의된 데이터셋 로딩 함수(rocstories)를 사용해 데이터를 불러옴.
    • 그 외 다양한 유틸리티 함수들이 utils.py, text_utils.py, analysis.py에서 임포트됨.
  2. Activation 및 최적화 함수 등록:
    • gelu, swish, relu 등의 활성화 함수들이 정의되고, 이를 학습에 사용할 수 있도록 등록.
    • opt_fns에 Adam 옵티마이저가, lr_schedules에는 학습률 스케줄링 함수들이 등록되어 , 다양한 설정으로 학습을 조정할 수 있음.
  3. _norm, norm 함수:
    • 입력 데이터에 대해 정규화(Normalization)를 수행하는 함수로, 학습 중에 데이터의 스케일을 조정하는 데 사용.
    • gb라는 가중치를 통해 데이터에 맞는 정규화를 적용할 수 있게 설계됨.
  4. 기타 학습 설정 및 유틸리티:
    • 이 파일은 데이터셋 로딩, 모델 학습, 최적화, 정규화, 그리고 결과 저장 등의 전 과정을 포함하며, 이를 통해 모델이 학습될 수 있도록 모든 단계를 처리하고 있음.

train.py 파일은 프로젝트의 모델 학습을 제어하는 메인 스크립트이다. 데이터 로딩, 모델 설정, 최적화, 학습률 스케줄링, 그리고 모델 학습과 관련된 전체 과정이 여기에 담겨 있다. 이 파일을 통해 전체적인 학습 과정이 관리된다. 일종의 main.py

주요 구성 요소:

1. 필요한 라이브러리 임포트

import os
import time
import math
import json
import joblib
import random
import argparse
import numpy as np
import tensorflow as tf

from tqdm import tqdm
from functools import partial
from sklearn.utils import shuffle
from sklearn.metrics import accuracy_score

from opt import adam, warmup_cosine, warmup_linear, warmup_constant
from datasets import rocstories
from analysis import rocstories as rocstories_analysis
from text_utils import TextEncoder
from utils import encode_dataset, flatten, iter_data, find_trainable_variables, convert_gradient_to_tensor, shape_list, ResultLogger, assign_to_gpu, average_grads, make_path

다양한 외부 라이브러리 및 내부 모듈들이 임포트된다:

  • opt.py: 최적화 알고리즘(Adam)과 학습률 스케줄링 함수들이 임포트된다.
  • datasets.py: 데이터셋 로딩 및 전처리 함수들이 임포트된다.
  • text_utils.py: 텍스트 인코더(BPE 기반 토크나이저)가 임포트된다.
  • utils.py: 여러 유틸리티 함수들이 임포트되어 모델 학습과 데이터 처리에 도움을 준다.

2. 활성화 함수 및 최적화 함수 설정

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

def swish(x):
    return x * tf.nn.sigmoid(x)

opt_fns = {
    'adam': adam,
}

act_fns = {
    'relu': tf.nn.relu,
    'swish': swish,
    'gelu': gelu
}

lr_schedules = {
    'warmup_cosine': warmup_cosine,
    'warmup_linear': warmup_linear,
    'warmup_constant': warmup_constant,
}
  • 활성화 함수: relu, swish, gelu와 같은 활성화 함수들이 정의되어 있으며, 모델의 학습 중에 사용된다.
  • 최적화 함수: Adam 옵티마이저가 선택되어 학습 중 사용될 최적화 알고리즘으로 설정된다.
  • 학습률 스케줄링: warmup_cosine, warmup_linear, warmup_constant 함수들을 통해 학습률이 동적으로 조정될 수 있다.

3. 정규화 함수

def _norm(x, g=None, b=None, e=1e-5, axis=[1]):
    u = tf.reduce_mean(x, axis=axis, keepdims=True)
    s = tf.reduce_mean(tf.square(x - u), axis=axis, keepdims=True)
    x = (x - u) * tf.rsqrt(s + e)
    if g is not None and b is not None:
        x = x * g + b
    return x

def norm(x, scope, axis=[-1]):
    with tf.variable_scope(scope):
        n_state = shape_list(x)[-1]
        g = tf.get_variable("g", [n_state], initializer=tf.constant_initializer(1))
        b = tf.get_variable("b", [n_state], initializer=tf.constant_initializer(0))
        return _norm(x, g, b, axis=axis)
  • 정규화 함수: 입력 데이터에 대해 평균을 빼고 분산으로 나누어 정규화하는 과정이다. 이를 통해 학습 중에 데이터가 더 안정적으로 처리될 수 있게 돕는다.

4. 모델 학습 루프 및 학습 흐름

def train_model(args):
    # 텍스트 인코더 설정
    encoder = TextEncoder(args.encoder_path, args.bpe_path)

    # 데이터셋 로드
    (trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3) = rocstories(args.data_dir)

    # 텐서플로우 그래프 설정 및 세션 생성
    with tf.Session(graph=tf.Graph()) as sess:
        # 모델 및 최적화 설정
        opt_fn = opt_fns[args.opt]
        lr_schedule = lr_schedules[args.lr_schedule]

        # 모델 빌딩 및 학습 관련 설정들
        # 학습 준비

        for epoch in range(args.epochs):
            # 데이터 섞기 및 미니 배치로 학습 진행
            for step, (batchX1, batchX2, batchX3, batchY) in enumerate(iter_data(trX1, trX2, trX3, trY, size=args.batch_size)):
                # 기울기 계산 및 파라미터 업데이트
                _, loss_val = sess.run([train_op, loss], feed_dict={
                    # 데이터 전달
                })

            # 검증 데이터로 성능 평가
            val_acc = accuracy_score(sess.run([val_pred], feed_dict={...}))

        # 학습 완료 후 모델 저장
        joblib.dump(sess.run([params]), os.path.join(args.save_path, 'model_params.joblib'))

이 부분은 모델 학습의 전체 흐름을 보여준다:

  1. 텍스트 인코더 설정: TextEncoder를 사용해 데이터를 BPE 토크나이징하여 모델에 입력할 준비를 한다.
  2. 데이터셋 로드: rocstories 함수로 데이터셋을 불러와 훈련, 검증, 테스트 데이터를 준비한다.
  3. 텐서플로우 세션 설정: TensorFlow 세션과 그래프를 설정하여 학습 준비를 한다.
  4. 학습 루프: 여러 에포크(epoch) 동안 학습을 진행하며, 미니 배치 단위로 데이터를 처리하고, 기울기를 계산해 모델의 파라미터를 업데이트한다.
  5. 검증 루프: 학습 중 검증 데이터로 성능을 평가하고, 결과를 기록한다.
  6. 모델 저장: 학습이 끝나면 모델 파라미터를 저장한다.

전체 흐름 요약:

  1. 데이터 로딩 및 인코딩: 데이터를 불러와 텍스트 인코더를 사용해 토큰화하고 학습에 사용할 수 있는 형식으로 변환한다.
  2. 모델 설정: 최적화 함수(Adam)와 학습률 스케줄링 방법을 선택하고, 활성화 함수를 정의해 모델 학습의 준비를 마친다.
  3. 훈련 루프: 훈련 데이터를 미니 배치 단위로 처리하며, 각 배치에서 손실 값을 계산하고 기울기를 업데이트한다.
  4. 검증 및 평가: 각 에포크 후 검증 데이터를 사용해 모델 성능을 평가하고, 정확도를 기록한다.
  5. 모델 저장: 학습이 완료된 후, 모델 파라미터를 저장해 나중에 사용할 수 있도록 준비한다.

train.py는 학습의 메인 스크립트로, 전체 모델 학습 과정을 책임진다. 데이터를 준비하고, 모델을 학습시키며, 검증을 통해 성능을 평가하는 전 과정을 관리한다.

추가적으로 더 알고 싶은 내용이 있으면 말해줘!


5. analysis.py (모델 성능 분석)

이 파일은 학습이 끝난 후 모델의 성능을 분석하거나, 결과를 시각화하고 평가하는 기능을 담당하고 있다. 훈련이 완료된 모델을 평가하거나 결과를 해석하는 데 사용될 수 있다.

analysis.py 파일은 주로 학습이 완료된 모델의 성능을 평가하거나 결과를 분석하는 데 사용되는 코드가 포함되어 있을 것으로 보인다. 이를 통해 모델이 실제로 얼마나 잘 학습되었는지, 정확도나 손실 같은 지표들을 계산하고 시각화할 수 있다.

주요 구성 요소 분석:

1. 라이브러리 임포트

import numpy as np
from sklearn.metrics import accuracy_score
from sklearn.utils import shuffle

이 파일에서는 numpy, sklearn.metrics, sklearn.utils가 임포트된다. 이 라이브러리들은 주로 성능 평가, 정확도 계산, 데이터 셔플링 등의 기능을 수행한다.

2. 모델 성능 평가 함수

def rocstories_analysis(preds, ys):
    """
    preds: 모델이 예측한 결과
    ys: 실제 정답 레이블
    두 개의 리스트를 입력으로 받아 정확도를 계산한다.
    """
    accuracy = accuracy_score(ys, preds)
    print(f"Accuracy: {accuracy * 100:.2f}%")
    return accuracy

이 함수는 모델의 예측값과 실제 정답 레이블을 비교하여 정확도를 계산하는 함수이다. 주요 작업은 다음과 같다:

  • accuracy_score(ys, preds): Scikit-learn의 accuracy_score 함수를 사용하여 정확도를 계산한다.
  • 결과를 출력한 후, 정확도 값을 반환한다.

이 함수는 학습 후 검증 데이터나 테스트 데이터에 대해 모델의 성능을 간단하게 평가할 때 사용된다.

3. 데이터 셔플링 함수

def shuffle_data(X1, X2, X3, Y):
    """
    X1, X2, X3: 입력 데이터 (스토리, 선택지 1, 선택지 2)
    Y: 정답 레이블
    주어진 데이터를 셔플링하여 반환한다.
    """
    X1, X2, X3, Y = shuffle(X1, X2, X3, Y)
    return X1, X2, X3, Y

이 함수는 데이터 셔플링을 수행한다. 주로 모델이 특정한 순서에 치우치지 않고 다양한 데이터로 학습할 수 있도록 데이터를 섞어준다. Scikit-learn의 shuffle 함수를 사용하여 스토리, 선택지, 정답 데이터를 함께 셔플링한 후, 이를 반환한다.

4. 추가적인 유틸리티 및 분석 함수

이 파일에는 다른 분석 함수나 추가적인 유틸리티 함수들이 포함되어 있을 수 있다. 하지만 주된 역할은 모델의 성능을 평가하거나, 데이터를 셔플링해 학습에 사용할 준비를 돕는 것이다.

전체 흐름 요약:

  1. 모델 성능 평가 (rocstories_analysis):
    • 모델의 예측값과 실제 정답값을 비교하여 정확도를 계산하고, 이를 출력한다. 학습이 끝난 후 성능을 평가하는데 사용된다.
  2. 데이터 셔플링 (shuffle_data):
    • 스토리와 선택지, 정답 데이터를 셔플링하여 모델이 다양한 데이터를 고르게 학습할 수 있도록 한다.

이 파일은 학습 후 모델의 성능을 평가하거나, 데이터를 전처리하는 데 유용한 함수들이 포함되어 있다. 간단하게 정확도를 계산하고, 데이터를 섞어주는 역할을 하며, 이를 통해 모델의 성능을 분석하고 검증할 수 있다.


6. utils.py (유틸리티 함수)

이 파일은 여러 작업을 보조하는 유틸리티 함수들을 포함하고 있다. 데이터 인코딩, 파일 경로 생성, 텐서 크기 처리 등 학습 과정에서 필요한 다양한 보조 작업을 수행한다.

  • 데이터셋 인코딩 함수
  • 레이블 인코딩 함수
  • 텐서 모양 처리 함수
  • 소프트맥스 함수
  • 경로 생성 및 파일 저장 관련 함수

주요 구성 요소:

  1. encode_dataset 함수:
    • 여러 데이터셋 스플릿(훈련, 검증, 테스트 데이터셋)을 받아서, 각 필드를 인코더를 사용해 토크나이징하는 함수.
    • 데이터셋을 모델에 입력할 수 있도록 변환하는 과정에서 중요한 역할을 해.
  2. stsb_label_encoding 함수:
    • Tree LSTM 논문에서 사용한 레이블 인코딩 방식을 적용한 함수로, 학습에 사용될 레이블을 적절한 형식으로 변환.
    • 레이블을 다차원 배열로 변환하여 모델이 다중 클래스를 예측할 수 있도록 함.
  3. shape_list 함수:
    • TensorFlow의 동적 텐서 크기를 처리하는 함수로, 텐서의 모양을 명확히 가져오는 데 도움을 줌.
  4. np_softmax 함수:
    • 넘파이 기반 소프트맥스 함수로, 주어진 입력에 대해 확률 분포를 계산해 반환.
  5. make_path 함수:
    • 파일 경로를 생성하는 함수로, 필요한 디렉토리가 없을 경우 이를 생성하는 역할을 함.

이 파일은 학습 과정에서 필요한 데이터 인코딩, 텐서의 모양 처리, 소프트맥스 계산, 파일 경로 생성 등 여러 보조 작업을 수행하는 데 사용돼. 다양한 유틸리티 함수들이 모델 학습의 여러 단계에서 호출될 수 있어.

utils.py 파일은 프로젝트에서 다양한 보조 작업을 수행하는 유틸리티 함수들이 정의된 파일이다. 이 파일의 함수들은 모델 학습과 데이터 처리의 여러 단계에서 활용되며, 특히 데이터 인코딩, 파일 경로 생성, 텐서 크기 처리, 그리고 소프트맥스 계산 등을 수행하는 데 사용된다.

주요 구성 요소 분석:

1. encode_dataset 함수

def encode_dataset(*splits, encoder):
    encoded_splits = []
    for split in splits[0]:
        fields = []
        for field in split:
            if isinstance(field[0], str):
                field = encoder.encode(field)
            fields.append(field)
        encoded_splits.append(fields)
    return encoded_splits

이 함수는 여러 데이터셋(훈련, 검증, 테스트)을 인코더를 사용해 토크나이징하는 함수이다. encoder.encode()를 호출하여 텍스트 데이터를 토큰화하고, 각 필드를 인코딩된 데이터로 변환한다.

  • 여러 개의 데이터셋을 입력받아 인코더로 각 데이터셋의 텍스트 필드를 인코딩하여 반환한다.

2. stsb_label_encoding 함수

def stsb_label_encoding(labels, nclass=6):
    """
    Label encoding from Tree LSTM paper (Tai, Socher, Manning)
    """
    Y = np.zeros((len(labels), nclass)).astype(np.float32)
    for j, y in enumerate(labels):
        for i in range(nclass):
            if i == np.floor(y) + 1:
                Y[j,i] = y - np.floor(y)
            if i == np.floor(y):
                Y[j,i] = np.floor(y) - y + 1
    return Y

이 함수는 STSB(Tree LSTM) 레이블 인코딩을 수행한다. 레이블을 다차원 배열로 변환하여 모델이 다중 클래스 분류 문제에서 사용할 수 있도록 한다.

  • 각 레이블을 주어진 클래스 개수(nclass)에 맞춰 적절한 형식으로 변환하여 반환한다.

3. shape_list 함수

def shape_list(x):
    """
    Deal with dynamic shape in tensorflow cleanly
    """
    ps = x.get_shape().as_list()
    ts = tf.shape(x)
    return [ts[i] if ps[i] is None else ps[i] for i in range(len(ps))]

이 함수는 TensorFlow 텐서의 동적 모양(dynamic shape)을 처리하는 함수이다. 텐서의 크기를 안정적으로 처리하기 위해 사용되며, TensorFlow 그래프 내에서 동적으로 변화하는 텐서의 크기를 다룰 수 있게 한다.

  • 텐서의 실제 크기를 가져와 리스트 형태로 반환한다.

4. np_softmax 함수

def np_softmax(x, t=1):
    x = x / t
    x = x - np.max(x, axis=-1, keepdims=True)
    ex = np.exp(x)
    return ex / np.sum(ex, axis=-1, keepdims=True)

이 함수는 넘파이 기반의 소프트맥스 함수이다. 주어진 입력에 대해 소프트맥스 계산을 수행하여, 확률 분포를 반환한다.

  • 입력 값을 소프트맥스 방식으로 변환해 각 클래스의 확률을 계산한다.

5. make_path 함수

def make_path(f):
    d = os.path.dirname(f)
    if d and not os.path.exists(d):
        os.makedirs(d)
    return f

이 함수는 파일 경로가 존재하지 않을 경우, 경로를 생성하는 역할을 한다. 파일이나 디렉토리 경로가 주어지면, 필요한 디렉토리가 없을 때 이를 생성해준다.

  • 파일 경로가 존재하지 않으면 새로 생성하고, 해당 경로를 반환한다.

6. 기타 유틸리티 함수들

파일에는 다양한 유틸리티 함수들이 포함되어 있을 수 있으며, 주로 데이터셋 처리, 학습 중 필요한 데이터 전처리, 모델 저장 및 경로 관리 등을 위한 도우미 함수들이 많이 정의되어 있다.

전체 흐름 요약:

  1. encode_dataset: 여러 데이터셋을 인코더를 사용해 토크나이징하고, 이를 모델이 학습할 수 있는 형식으로 변환한다.
  2. stsb_label_encoding: 다중 클래스 레이블을 적절한 형식으로 변환하는 함수로, 모델의 출력과 레이블이 일치할 수 있도록 변환한다.
  3. shape_list: 텐서의 동적 크기를 처리하고, 이를 리스트 형태로 반환하는 함수이다.
  4. np_softmax: 넘파이 기반의 소프트맥스 함수로, 입력 값을 확률로 변환하여 반환한다.
  5. make_path: 파일 경로가 없을 경우 디렉토리를 생성해주는 함수로, 파일 저장을 위한 경로를 설정하는 데 사용된다.

이 파일은 학습 과정에서 자주 사용하는 여러 도우미 함수들을 제공하며, 데이터를 인코딩하거나 텐서의 크기를 처리하는 등의 작업에 도움을 준다.


profile
AI Engineer / 의료인공지능

0개의 댓글