1. 개요
문장 생성: 컨텍스트가 주어졌을 때, 다음 단어로 적합한 것을 분류하는 것.
모델의 입력: 컨텍스트, 출력: 다음 토큰의 등장 확률
예를 들어 컨텍스트가 “안녕” 인 경우,
- 입력 : 컨텍스트를 토큰화한 결과(토큰 시퀸스)
- 출력 : 다음 토큰에 대한 확률분포
- 확률분포의 길이 = 어휘 집합의 길이
이 과정을 문장이 끝날때까지 반복한다.
데이터
- 네이버 영화 리뷰 말뭉치인 NSMC 데이터 활용
- SKT가 공개한 KoGPT2 모델을 파인튜닝하는 실습
모델 구조
- 컨텍스트가 주어질 때 다음 단어를 맞추는 방식의 모델
- 다만, GPT를 사용한다는 점에서 이전 모델들과 다르다.
- 프리트레인과 파인튜닝 태스크가 ‘다음 단어 맞추기’로 같다.
- 프리트레인 구조를 그대로 파인튜닝에 붙일 수 있다.
2. 모델 파인튜닝하기
각종 설정
!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)
말뭉치 내려받기
from Korpora import Korpora
Korpora.fetch(
corpus_name=args.downstream_corpus_name,
root_dir=args.downstream_corpus_root_dir,
force_download=args.force_download,
)
토크나이저 준비하기
from transformers import PreTrainedTokenizerFast
tokenizer = PreTrainedTokenizerFast.from_pretrained(
args.pretrained_model_name,
eos_token="</s>",
)
데이터 전처리하기
현재 사용되는 모델은 GPT 모델
- 텍스트 왼쪽 → 오른쪽으로 순차적으로 읽으면서 학습하는 GPT 모델
- 파인튜닝 시 문장 맨 앞에 극성 정보를 부여; 즉 입력시 반드시 긍정인지 부정인지 적어야 함.
- 조건부 문장 생성 능력이 있는지 검증
학습 데이터를 만들어준다.
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,
)
테스트 데이터 구축
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,
)
모델 초기화
from transformers import GPT2LMHeadModel
model = GPT2LMHeadModel.from_pretrained(
args.pretrained_model_name
)
task와 trainer 준비하기
# task
from ratsnlp.nlpbook.generation import GenerationTask
task = GenerationTask(model, args)
# trainer
trainer = nlpbook.get_trainer(args)
학습에 들어간다.
trainer.fit(
task,
train_dataloaders=train_dataloader,
val_dataloaders=val_dataloader,
)
3. 프리트레인 마친 모델로 문장 생성
각종 설정
# 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")
Greedy Search
다음 단어 확률분포에서 최대 확률을 내는 단어들을 리턴한다.
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]]))
그리디 서치란? 매 순간 최선인 애들만 선택한다는 뜻.
그리디 알고리즘과 비슷하다고 생각하면 될 듯.
Beam Search
다음 단어 확률분포에서 num_beans만큼의 경우의 수를 남겨가면서 문장 생성.
Greedy Search보다는 계산량이 많지만, 좀 더 확률값이 높아진다.
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와 정확히 같다.
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 사이즈를 지정하기
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인 건 패널티가 없다는 뜻이기 때문)
# 패널티가 없는 경우 : 범위 = 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]]))
# 패널티가 없는 경우 : 범위 = 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개 확률값에 속한 단어라면, 확률값이 낮더라도 다음 단어로 추출될 수 있다.
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해진다.
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이면 모델 출력 분포를 그대로 사용한다. 단, 샘플링 방식을 쓰기에 다른 문장에 매번 나온다.
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하게 분포가 변한다.
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 이하인 단어라면 확률값이 낮아도 다음 단어로 추출될 수 있다
따라서, 매 시행마다 생성 결과가 달라진다.
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
이를 종합적으로 모은 코드는 다음과 같다.
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. 파인튜닝 마친 모델로 문장 생성하기
설정하기
!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",
)
모델 로딩
# 이미 만든 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>",
)
인퍼런스 함수 선언
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에 들어가서 인증 토큰을 따오고, 이를 기반으로 실행한다.
!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()