열렬히.뛰기

8장 : 문장생성 모델

머신러닝 & 딥러닝 > 자연어처리 (1) : BERT와 GPT > 8장 : 문장생성 모델

1. 개요

문장 생성: 컨텍스트가 주어졌을 때, 다음 단어로 적합한 것을 분류하는 것.

모델의 입력: 컨텍스트, 출력: 다음 토큰의 등장 확률

예를 들어 컨텍스트가 “안녕” 인 경우,

  • 입력 : 컨텍스트를 토큰화한 결과(토큰 시퀸스)
  • 출력 : 다음 토큰에 대한 확률분포
  • 확률분포의 길이 = 어휘 집합의 길이
\text{안녕 → 하세요 → !}\\[5pt] p(w)\\[5pt] p(w|\text{안녕})\\[5pt] p(w|\text{안녕, 하세요})\\[5pt]

이 과정을 문장이 끝날때까지 반복한다.

데이터

  • 네이버 영화 리뷰 말뭉치인 NSMC 데이터 활용
  • SKT가 공개한 KoGPT2 모델을 파인튜닝하는 실습

모델 구조

  • 컨텍스트가 주어질 때 다음 단어를 맞추는 방식의 모델
  • 다만, GPT를 사용한다는 점에서 이전 모델들과 다르다.
    • 프리트레인과 파인튜닝 태스크가 ‘다음 단어 맞추기’로 같다.
    • 프리트레인 구조를 그대로 파인튜닝에 붙일 수 있다.

2. 모델 파인튜닝하기

각종 설정

python
!pip install ratsnlp

# 구글 드라이브
from google.colab import drive
drive.mount('/gdrive', force_remount=True)

# pyTorch 설정
import torch
from ratsnlp.nlpbook.generation import GenerationTrainArguments
args = GenerationTrainArguments(
    pretrained_model_name="skt/kogpt2-base-v2",
    downstream_corpus_name="nsmc",
    downstream_model_dir="/gdrive/My Drive/nlpbook/checkpoint-generation",
    max_seq_length=32,
    batch_size=32 if torch.cuda.is_available() else 4,
    learning_rate=5e-5,
    epochs=3,
    tpu_cores=0 if torch.cuda.is_available() else 8,
    seed=7,
)

# 랜덤시드 고정
from ratsnlp import nlpbook
nlpbook.set_seed(args)

# 로거 설정
nlpbook.set_logger(args)

말뭉치 내려받기

python
from Korpora import Korpora
Korpora.fetch(
    corpus_name=args.downstream_corpus_name,
    root_dir=args.downstream_corpus_root_dir,
    force_download=args.force_download,
)

토크나이저 준비하기

python
from transformers import PreTrainedTokenizerFast
tokenizer = PreTrainedTokenizerFast.from_pretrained(
    args.pretrained_model_name,
    eos_token="</s>",
)

데이터 전처리하기

현재 사용되는 모델은 GPT 모델

  • 텍스트 왼쪽 → 오른쪽으로 순차적으로 읽으면서 학습하는 GPT 모델
  • 파인튜닝 시 문장 맨 앞에 극성 정보를 부여; 즉 입력시 반드시 긍정인지 부정인지 적어야 함.
  • 조건부 문장 생성 능력이 있는지 검증

학습 데이터를 만들어준다.

python
from ratsnlp.nlpbook.generation import NsmcCorpus, GenerationDataset
from torch.utils.data import DataLoader, SequentialSampler, RandomSampler
corpus = NsmcCorpus()
train_dataset = GenerationDataset(
    args=args,
    corpus=corpus,
    tokenizer=tokenizer,
    mode="train",
)
train_dataloader = DataLoader(
    train_dataset,
    batch_size=args.batch_size,
    sampler=RandomSampler(train_dataset, replacement=False),
    collate_fn=nlpbook.data_collator,
    drop_last=False,
    num_workers=args.cpu_workers,
)

테스트 데이터 구축

python
val_dataset = GenerationDataset(
    args=args,
    corpus=corpus,
    tokenizer=tokenizer,
    mode="test",
)
val_dataloader = DataLoader(
    val_dataset,
    batch_size=args.batch_size,
    sampler=SequentialSampler(val_dataset),
    collate_fn=nlpbook.data_collator,
    drop_last=False,
    num_workers=args.cpu_workers,
)

모델 초기화

python
from transformers import GPT2LMHeadModel
model = GPT2LMHeadModel.from_pretrained(
    args.pretrained_model_name
)

tasktrainer 준비하기

python
# task
from ratsnlp.nlpbook.generation import GenerationTask
task = GenerationTask(model, args)

# trainer
trainer = nlpbook.get_trainer(args)

학습에 들어간다.

python
trainer.fit(
    task,
    train_dataloaders=train_dataloader,
    val_dataloaders=val_dataloader,
)

3. 프리트레인 마친 모델로 문장 생성

각종 설정

python
# ratsnlp 깔기
!pip install ratsnlp

# 프리트레인한 GPT2 모델과 토크나이저 읽기
from transformers import GPT2LMHeadModel
model = GPT2LMHeadModel.from_pretrained(
    "skt/kogpt2-base-v2",
)
model.eval()

from transformers import PreTrainedTokenizerFast
tokenizer = PreTrainedTokenizerFast.from_pretrained(
    "skt/kogpt2-base-v2",
    eos_token="</s>",
)

# 프롬프트 준비
input_ids = tokenizer.encode("안녕하세요", return_tensors="pt")

다음 단어 확률분포에서 최대 확률을 내는 단어들을 리턴한다.

python
import torch
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=False,
        min_length=10,
        max_length=50,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

그리디 서치란? 매 순간 최선인 애들만 선택한다는 뜻.

그리디 알고리즘과 비슷하다고 생각하면 될 듯.

다음 단어 확률분포에서 num_beans만큼의 경우의 수를 남겨가면서 문장 생성.

Greedy Search보다는 계산량이 많지만, 좀 더 확률값이 높아진다.

python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=False,
        min_length=10,
        max_length=50,
        num_beams=3,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

num_beams = 1 이면 Greedy search와 정확히 같다.

python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=False,
        min_length=10,
        max_length=50,
        num_beams=1,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

그리디 서치의 단점: 순간순간의 최선이 항상 최선이 될 순 없음.

따라서 각 정점별 노드 중 고려대상의 크기(빔 크기)를 정하고 비교해 가며 서치.

반복 줄이기

반복되는 n-gram 사이즈를 지정하기

python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=False,
        min_length=10,
        max_length=50,
        no_repeat_ngram_size=3,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

repetition penalty

repetition penalty로도 반복을 통제할 수 있다. 범위는 반드시 1 이상!

(범위가 1인 건 패널티가 없다는 뜻이기 때문)

python
# 패널티가 없는 경우 : 범위 = 1
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=False,
        min_length=10,
        max_length=50,
        repetition_penalty=1.0,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))
python
# 패널티가 없는 경우 : 범위 = 1
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=False,
        min_length=10,
        max_length=50,
        repetition_penalty=1.1,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

top-k 샘플링

다음 단어를 뽑을 때 확률값 기준 가장 큰 k개 중 하나를 선택하는 기법.

k개 확률값에 속한 단어라면, 확률값이 낮더라도 다음 단어로 추출될 수 있다.

python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=True,
        min_length=10,
        max_length=50,
        top_k=50,       # k = 50개로 설정했다.
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

만약 k = 1 이면, Greedy Search와 동일하다.

top-k 샘플링 + temperature scaling

두개를 동시에 적용했을 때.

  • t가 0에 가까워질 수록 토큰 분포가 sharp해진다.
python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=True,
        min_length=10,
        max_length=50,
        top_k=50,
        temperature=0.01,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))
  • t=1이면 모델 출력 분포를 그대로 사용한다. 단, 샘플링 방식을 쓰기에 다른 문장에 매번 나온다.
python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=True,
        min_length=10,
        max_length=50,
        top_k=50,
        temperature=1.0,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))
  • t가 커질 수록 uniform하게 분포가 변한다.
python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=True,
        min_length=10,
        max_length=50,
        top_k=50,
        temperature=100000000.0,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

top-p 샘플링

다음 단어로 확률값이 p 이하인 단어들을 선택하는 기법.

누적 확률값이 p 이하인 단어라면 확률값이 낮아도 다음 단어로 추출될 수 있다

따라서, 매 시행마다 생성 결과가 달라진다.

python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=True,
        min_length=10,
        max_length=50,
        top_p=0.92,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

p가 0에 가까워 질수록 Greedy Search 와 비슷해진다.

통합적용

모든 허깅페이스 라이브러리는 다음과 같다.

  • _get_logits_processor
    • RepetitionPenalty
    • NoRepeatNGramLogits
    • MinLengthLogits
  • _get_logits_warper
    • TemperatureLogits
    • TopKLogits
    • TopPLogits

이를 종합적으로 모은 코드는 다음과 같다.

python
with torch.no_grad():
    generated_ids = model.generate(
        input_ids,
        do_sample=True,
        min_length=10,
        max_length=50,
        repetition_penalty=1.5,
        no_repeat_ngram_size=3,
        temperature=0.9,
        top_k=50,
        top_p=0.92,
    )
    print(tokenizer.decode([el.item() for el in generated_ids[0]]))

4. 파인튜닝 마친 모델로 문장 생성하기

설정하기

python
!pip install ratsnlp

# 구글 드라이브 연동
from google.colab import drive
drive.mount('/gdrive', force_remount=True)

# 설정 정보
from ratsnlp.nlpbook.generation import GenerationDeployArguments
args = GenerationDeployArguments(
    pretrained_model_name="skt/kogpt2-base-v2",
    downstream_model_dir="/gdrive/My Drive/nlpbook/checkpoint-generation",
)

모델 로딩

python
# 이미 만든 GPT2
import torch
from transformers import GPT2Config, GPT2LMHeadModel
pretrained_model_config = GPT2Config.from_pretrained(
    args.pretrained_model_name,
)
model = GPT2LMHeadModel(pretrained_model_config)
fine_tuned_model_ckpt = torch.load(
    args.downstream_model_checkpoint_fpath,
    map_location=torch.device("cpu"),
)
model.load_state_dict({k.replace("model.", ""): v for k, v in fine_tuned_model_ckpt['state_dict'].items()})
model.eval()

# 이미 만든 토크나이저
from transformers import PreTrainedTokenizerFast
tokenizer = PreTrainedTokenizerFast.from_pretrained(
    args.pretrained_model_name,
    eos_token="</s>",
)

인퍼런스 함수 선언

python
def inference_fn(
        prompt,
        min_length=10,
        max_length=20,
        top_p=1.0,
        top_k=50,
        repetition_penalty=1.0,
        no_repeat_ngram_size=0,
        temperature=1.0,
):
    try:
        input_ids = tokenizer.encode(prompt, return_tensors="pt")
        with torch.no_grad():
            generated_ids = model.generate(
                input_ids,
                do_sample=True,
                top_p=float(top_p),
                top_k=int(top_k),
                min_length=int(min_length),
                max_length=int(max_length),
                repetition_penalty=float(repetition_penalty),
                no_repeat_ngram_size=int(no_repeat_ngram_size),
                temperature=float(temperature),
           )
        generated_sentence = tokenizer.decode([el.item() for el in generated_ids[0]])
    except:
        generated_sentence = """처리 중 오류가 발생했습니다. <br>
            변수의 입력 범위를 확인하세요. <br><br> 
            min_length: 1 이상의 정수 <br>
            max_length: 1 이상의 정수 <br>
            top-p: 0 이상 1 이하의 실수 <br>
            top-k: 1 이상의 정수 <br>
            repetition_penalty: 1 이상의 실수 <br>
            no_repeat_ngram_size: 1 이상의 정수 <br>
            temperature: 0 이상의 실수
            """
    return {
        'result': generated_sentence,
    }

웹서비스

역시 ngrok에 들어가서 인증 토큰을 따오고, 이를 기반으로 실행한다.

python
!mkdir /root/.ngrok2 && echo "authtoken: {이곳에 확인된 인증 토큰을 입력하세요}" > /root/.ngrok2/ngrok.yml

from ratsnlp.nlpbook.generation import get_web_service_app
app = get_web_service_app(inference_fn)
app.run()