일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 조축회
- KeyBert
- 자연어 모델
- CTM
- 구글 스토어 리뷰
- 다항분포
- 토픽 모델링
- NLP
- 클래스 분류
- 데벨챌
- 블루아카이브 토픽모델링
- 피파온라인 API
- 포아송분포
- 원신
- 문맥을 반영한 토픽모델링
- 코사인 유사도
- 붕괴 스타레일
- 블루 아카이브
- 데이터넥스트레벨챌린지
- geocoding
- 옵티마이저
- Tableu
- 개체명 인식
- LDA
- BERTopic
- 데이터리안
- 트위치
- SBERT
- Roberta
- Optimizer
- Today
- Total
분석하고싶은코코
RLHF_Chatbot 만들기(던전앤파이터 Chatbot) 본문
RLHF를 활용한 던전앤파이터 챗봇 만들기 프로젝트를 진행하였습니다. 사실 HF가 아닌 랜덤성이긴 하지만 비슷한 형태로 진행되기는 했습니다...ㅎ 여튼 이번 프로젝트에서 느낀점은 HF... RM을 위한 데이터 구성, 결국은 훈련시킬 데이터가 중요하다라는 것을 느낄 수 있었습니다. 느낀점은 후기에서 좀 더 작성하고 진행한 프로젝트에 대해서 이야기를 시작해보겠습니다.
지난번 포스팅에서 데이터 수집을 진행하였습니다. 완벽하게 던전앤파이터에 대한 데이터를 학습할 정도는 아니지만 던전앤파이터 세계관에 대해 이야기할 수 있는 Chatbot을 만들기에는 충분한 데이터가 수집되어 훈련을 진행하였습니다. 던전앤파이터 공식 홈페이지와 루리웹, 나무위키의 메인스토리 텍스트 데이터를 사용하였습니다.
포스팅에서는 SFT, RM, PPO 3단계에 대해 프로젝트를 진행하면서 경험한 주요 내용들로만 포스팅을 진행하겠습니다. 훈련을 위한 자세한 내용은 코드에서 참고해주시면 될 것 같습니다. 추후 github와 colab에서 바로 실행해볼 수 있도록 업로드할 예정입니다.(코드들 여기저기 작성해놔서 정리중입니다.)
이번 프로젝트는 QLoRA와 Prompt finetuning 두 가지를 사용하여 진행한 프로젝트입니다.
1. Trainable Model(SFT)
Select Base Model
여기서 겪은 어려움? 문제점?이라고하면 사실 모델 선택 문제가 가장 힘들었습니다. 이번 프로젝트의 베이스 LLM은 'EleutherAI/polyglot-ko'으로 선택하였습니다. 지난번 RLHF를 연습을 위해 사용한 KoGPT로 사용하기에는 생각보다 표현력이 떨어지는 모습을 많이 보여줬기 때문에 좀 더 큰 모델을 선택해야했습니다. 그렇다고 너무 큰 모델을 선택하기에는 제가 사용할 수 있는 컴퓨팅 자원 범위 안에서 선택해야했습니다. 이러한 조건하에 훈련 데이터가 충분한 모델들 중에 polyglot-ko를 선택하였습니다.
polyglot-ko에는 12.8b, 5.8b, 3.8b, 1.3b 4가지가 존재하는데 저는 그 중에서 1.3b로 가장 작은 모델을 최종 모델로 선택하였습니다. 더 많은 파라미터를 가진 모델을 통해서 진행할 수 있지만 그렇게 진행하지 않은 이유는 추후에 PPO과정을 진행할때 3개의 모델을 사용하기에 충분한 GPU 메모리 확보가 필요했기 때문에 쉽게 큰 모델을 선택하기 어려웠습니다. 실제로 처음에는 12.8b모델로 학습을 시켰는데 SFT과정에서는 OOM(out of memory)가 발생하지 않았지만 KL을 계산하는 과정에서 2개의 모델이 GPU에 올라가다보니 OOM를 제어할 수 없는 상황이 발생하였습니다. 그래서 1.3b를 선택하여 훈련을 진행하였습니다. (A100 40G로 테스트해보닌 RM과정까지 진행해보니 1step에서 GPU 20G 메모리 사용하는 것을 확인하였습니다.)
그러면 이렇게 Base Model을 선택했으니 훈련을 어떻게 진행했는지 간단하게 이야기해보겠습니다.
Preprocess Data
데이터 수집단계에서 반복문자, 한글, 숫자, [.,'"], 眞 문자를 제외하고 모두 삭제하였고 sonlpy를 사용하여 반복 문자의 경우 1개로 줄이는 전처리를 진행한 데이터입니다. 이후 아래와 같은 간단한 형태로 prompt 파인튜닝을 위한 형태로 전처리를 진행하였습니다.
data = data.map(
lambda x: {'text': f"### 질문: {x['prompt']}\n\n### 답변: {x['completion']}<|endoftext|>" }
)
QLoRA and Train
아래는 큰 모델을 양자화하여 모델을 로드하는 과정입니다.
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config)
아래는 peft모듈을 사용하여 quantization된 모델에 LoRA를 적용하는 과정입니다. trl에서는 peft와 연동이 되어 있어서 peft에서 제공해주는 모델로도 훈련이 가능합니다.
config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["query_key_value"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, config)
이후에는 훈련을 진행합니다. 저는 transformers에 있는 기본 Trainer를 사용했지만 trl에서 제공하는 SFTTrainer를 사용하여도 됩니다. 사용법은 다르지 않습니다. 총 epoch는 3회 진행하였습니다. 동일한 훈련 데이터로 colab T4 GPU로 훈련한 결과 1.3b모델은 4시간, 12.8b는 28시간의 학습 시간이 걸렸습니다.
trainer = transformers.Trainer(
model=model,
train_dataset=data["train"],
args=transformers.TrainingArguments(
per_device_train_batch_size=8,
gradient_accumulation_steps=2,
learning_rate=1e-4,
fp16=True,
output_dir="/content/drive/MyDrive/ds_study/던파_story/output_1_SFT",
optim="paged_adamw_8bit",
save_total_limit = 3,
seed = random_seed
),
data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
model.config.use_cache = False # silence the warnings. Please re-enable for inference!
trainer.train()
Result
SFT과정을 거친 모델이 생성한 텍스트는 아래와 같습니다. 던전앤파이터라는 세계관에 대해서 모델이 학습한게 확실히 알 수 있습니다. 이제 이 모델을 베이스 모델로 사용하겠습니다.
# setting generation
# num_beams=2, no_repeat_ngram_size=4, top_p = 0.9, max_new_tokens=128, early_stopping=True, do_sample=True, eos_token_id=2,
list_prompt = ['일곱 사도들에 대해서 알려줘',
'아라드 대륙에 대해서 설명해줘.',
'바칼이 천계를 공격한 이유는?.',
'게이볼그 프로젝트에 대해서 요약해주세요'
]
------RESULT-----
SMALL MODEL GENERATION
### 질문: 일곱 사도들에 대해서 알려줘
### 답변: 일곱 사도는 사도들 중에서도 가장 강력한 힘을 가지고 있으며, 가장 많은 신도를 거느리고 있습니다. 그들은 천계를 지키기 위해 천계인들과 싸우고 있으며 그들의 힘을 이용하여 천계의 치안을 유지하고 있습니다. 일곱 사도는 다른 사도들과는 다르게 일곱 개의 머리를 가지고 있는 것으로 알려져 있습니다. 이들은 일곱 개의 머리로 천계를 수호하고 있으며, 일곱 개의 머리는 일곱 사도의 힘을 상징한다고 합니다. 일곱 사도의 이름은 사도들의 이름을 따서 지
LARGE MODEL GENERATION
### 질문: 일곱 사도들에 대해서 알려줘
### 답변: 일곱 사도는 성역의 수호자로서 그들의 역할은 매우 중요합니다. 그들은 아라드 대륙에 전이된 성역을 지키기 위해 존재하며, 성역을 침범하는 적들을 막아냅니다. 그들의 힘은 매우 강력하며, 그들의 공격은 매우 치명적입니다. 따라서 그들은 성역을 보호하기 위해 항상 전투를 준비하고 있습니다. 또한, 그들은 자신들의 힘을 강화하기 위해 노력하고 있으며, 성역의 기운을 받아 더욱 강력한 힘을 얻게 됩니다. 이로 인해 그들
SMALL MODEL GENERATION
### 질문: 아라드 대륙에 대해서 설명해줘.
### 답변: 아라드에는 크게 마계와 천계로 나뉘어 있습니다. 마계는 바칼의 제국이 지배하고 있으며, 천계는 천계인들이 지배하고 있습니다. 아라드 대륙은 마계의 영향을 받은 대륙으로, 대륙 곳곳에 마계의 흔적이 남아있습니다. 아라딘은 천계의 대표적인 인물로, 바칼이 아라드 대륙을 지배하기 위해 파견한 인물입니다. 천계인들은 아라드 대륙에서 자유롭게 생활하고 있으며 아라딘의 통치를 받으며 살고 있습니다. 천계의 영향
LARGE MODEL GENERATION
### 질문: 아라드 대륙에 대해서 설명해줘.
### 답변: 아라드는 대륙의 이름으로, 서쪽에 위치한 벨 마이어 공국과 데 로스 제국, 동쪽에 위치한 반투족의 국가인 흑요정들의 나라 흑요정 왕국, 북쪽에 위치한 마법의 나라 마이어, 그리고 남쪽에 위치한 해상 국가인 아젤리아 로트가 있습니다. 아라드 대륙은 수많은 종족과 다양한 문화가 공존하는 곳으로, 모험가들은 이곳에서 다양한 경험을 할 수 있습니다. 또한, 아라드 대륙에서는 수많은 던전과 모험을 즐길 수 있으며, 다양한 종족들과의 교류
SMALL MODEL GENERATION
### 질문: 바칼이 천계를 공격한 이유는?
### 답변: 바칼은 천계를 점령한 후, 자신의 왕국을 건설하기 위해 바칼의 명령을 받고 천계를 침공했습니다. 그는 천계의 모든 것을 파괴하고 새로운 왕국을 건설했습니다. 이로 인해 바칼의 분노는 극에 달했고, 바칼은 그의 왕국을 공격하게 되었습니다. 바칼의 왕국은 그의 분노로 인해 폐허가 되었고, 그의 왕국은 멸망의 위기에 처했습니다. 이에 바칼은 자신의 왕국이 멸망하는 것을 막기 위해 그의 왕국에 침입하게 되
LARGE MODEL GENERATION
### 질문: 바칼이 천계를 공격한 이유는?.
### 답변: 바칼은 천계를 점령하기 위해 천계로 공격을 감행하였습니다. 그는 천계를 지배하기 위해 아라드 대륙을 공격하고 있습니다. 그의 목적은 천계의 모든 것을 지배하는 것입니다. 그는 자신의 야망을 실현하기 위해 계속해서 천계를 향해 공격을 시도할 것입니다. 이러한 이유로 바칼은 계속 천계를 침공하고 있습니다."나의 야망을 위해서라면 천계도, 아라드 대륙도 모두 나의 것으로 만들 것이다." 바칼의 외침입니다. 그의 야망은 천계
SMALL MODEL GENERATION
### 질문: 게이볼그 프로젝트에 대해서 요약해주세요
### 답변: 게이볼그라 불리는 이 프로젝트는 하늘성을 점령하기 위한 프로젝트입니다. 게이볼그는 하늘성의 궤도를 따라 움직이며, 그 위를 지나가는 적들을 공격하는 역할을 합니다. 이 프로젝트는 게이볼그를 개발하고, 하늘성에 배치하는 것으로 시작되었습니다. 그러나 게이볼그의 개발에 많은 어려움이 있었고, 이 프로젝트는 실패로 끝났습니다. 이 프로젝트가 실패한 이후, 게이볼그에 대한 연구가 다시 시작되었고, 그 결과 현재의 게이볼그가 완성되었습니다 이 프로젝트의 성공
LARGE MODEL GENERATION
### 질문: 게이볼그 프로젝트에 대해서 요약해주세요
### 답변: 게이볼크 프로젝트는 게이볼그를 개발하는 프로젝트입니다. 이 프로젝트는 겐트의 하늘성에서 진행되고 있으며, 겐트를 지키기 위해 설계되었습니다. 게이볼그는 하늘성의 일부를 차지하고 있으며, 하늘성을 지키기 위한 무기로서 사용되고 있습니다. 이 프로젝트의 목적은 게이볼그의 성능을 높이고, 게이볼그로 하여금 하늘성을 방어하는 역할을 수행하도록 하는 것입니다. 현재 게이볼그에는 다양한 무기들이 장착되어 있으며, 추가적인 무기를 장착하기
2. RewardModel(RM)
RM 역시 동일한 1.3b모델을 사용했습니다. 모델은 textclassification으로 불러와 주고 num_label=1로 불러와 스코어를 리턴하도록 하였습니다. 모든 데이터는 아래와 같이 KoGPT 예제에서 사용했던 데이터 형태를 그대로 사용하였고 아래 데이터는 ChatGPT를 사용해 만든 답변입니다. ranking은 좋은 답변에 대한 순서를 나타내며 숫자가 낮을수록 더 좋은 답변임을 의미합니다. 추가적으로 사용한 답변은 던전앤파이터에 대한 내용 뿐만 아니라 보다 좋은 형태의 답변을 생성하기 위해 KoGPT에서 사용한 RM데이터를 같이 사용했습니다. 그 이유는 던파 데이터는 한정적인것도 있고 ChatGPT로 만든 답변은 장문, 중문, 단문으로 구성되어 있기 때문에 좋다 나쁘다로 구분짓기 어려운 부분이 있었기 때문에 완전 잘못된 답변을 만드는 경우를 방지하고자 KoGPT 데이터를 사용했습니다.
{'prompt': '바다와 전쟁의 흔적, 그리고 마계의 사도들에 대한 이야기를 요약 해주세요.',
'completion_0': '소녀는 처음으로 넓은 바다를 보고 감탄하며 그 아름다움에 매료되었습니다. 그러나 그녀가 바라본 바다 주변의 섬은 과거 전쟁으로 황폐해진 곳이었습니다. 이곳에서 바다와 다른 풍경을 이루는 활화산이 있는 섬에 사도 안톤이 존재했다는 사실에 놀라움을 감추지 못했습니다. 소녀는 마계와 천계를 이어주는 통신 장치를 통해 연결된 리아 리히터와 통신을 하며, 자신이 마계에서 사라진 사도들을 찾는 데에 중요한 역할을 할 것이라는 기대감을 주고받습니다. 그러나 갑자기 차원의 틈이 닫혀 통신은 끊기고, 이제 그녀는 천계와 마계의 경계에서 불확실한 미래를 향해 자신의 역할을 다하기 위해 각오를 다집니다.',
'completion_1': '바다를 처음 본 소녀는 과거 전쟁터였던 섬의 아름다움에 놀랍니다. 그리고 통신기를 통해 리아와 사도 안톤의 존재 및 상황을 나누며 정보를 주고받지만, 차원의 틈이 닫혀 다시 연락을 기다려야 하는 상황에 처하게 됩니다.',
'completion_2': '바다에 감탄한 소녀가 마계의 사도들과 관련한 중요한 통신을 하다 차원의 틈이 닫혀 소통이 끊깁니다.',
'ranking': [0, 2, 1]}
위 데이터를 바탕으로 chosen, rejected 데이터로 만들었습니다. trl에서 사용할 수 있게끔 토크나이징 작업을 진행하였습니다.
# div dataset to chosen and rejected
total_data_ranking2chosen = []
for tmp in rm_data_combine:
one_data_ranking2chosen = []
# data 1) 0 VS 1
data = {}
data['prompt'] = tmp['prompt']
if tmp['ranking'][0] < tmp['ranking'][1]:
data['chosen'] = tmp['completion_0']
data['rejected'] = tmp['completion_1']
else:
data['chosen'] = tmp['completion_1']
data['rejected'] = tmp['completion_0']
one_data_ranking2chosen.append(data)
# data 2) 0 VS 2
data = {}
data['prompt'] = tmp['prompt']
if tmp['ranking'][0] < tmp['ranking'][2]:
data['chosen'] = tmp['completion_0']
data['rejected'] = tmp['completion_2']
else:
data['chosen'] = tmp['completion_2']
data['rejected'] = tmp['completion_0']
one_data_ranking2chosen.append(data)
# data 1) 1 VS 2
data = {}
data['prompt'] = tmp['prompt']
if tmp['ranking'][1] < tmp['ranking'][2]:
data['chosen'] = tmp['completion_1']
data['rejected'] = tmp['completion_2']
else:
data['chosen'] = tmp['completion_2']
data['rejected'] = tmp['completion_1']
one_data_ranking2chosen.append(data)
total_data_ranking2chosen.extend(one_data_ranking2chosen)
# tokenzing dataset
def formatting_func(examples):
kwargs = {"padding": "max_length", "truncation": True, "max_length": 512, "return_tensors": "pt"}
chosen = examples["prompt"] + "\n" + examples["chosen"]
rejected = examples["prompt"] + "\n" + examples["rejected"]
tokens_chosen = tokenizer.encode_plus(chosen, **kwargs)
tokens_rejected = tokenizer.encode_plus(rejected, **kwargs)
return {
"input_ids_chosen": tokens_chosen["input_ids"][0], "attention_mask_chosen": tokens_chosen["attention_mask"][0],
"input_ids_rejected": tokens_rejected["input_ids"][0], "attention_mask_rejected": tokens_rejected["attention_mask"][0]
}
from datasets import Dataset
train_data = Dataset.from_list(total_data_ranking2chosen[:-3000])
eval_data = Dataset.from_list(total_data_ranking2chosen[-3000:])
train_data.to_pandas()
eval_data.to_pandas()
train_dataset = train_data.map(formatting_func)
eval_dataset = eval_data.map(formatting_func)
이후에는 trainer를 사용하여 훈련을 진행하였습니다. 이번엔 기본 Trainer가 아니라 trl에서 제공하는 RewardTrainer를 사용하였습니다. 아무래도 데이터량이 훨씬 많다보니 1.3b모델로 진행했음이도 3epoch 진행하는 시간은 총 17시간이 소요됐습니다.
training_args = TrainingArguments(
per_device_train_batch_size=8,
gradient_accumulation_steps=2,
learning_rate=1e-4,
fp16=True,
output_dir="/content/drive/MyDrive/ds_study/던파_story/output_2_RM",
optim="paged_adamw_8bit",
save_total_limit = 3,
seed = random_seed
)
trainer = RewardTrainer(
model=model,
args=training_args,
tokenizer=tokenizer,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
)
model.config.use_cache = False # silence the warnings. Please re-enable for inference!
model.config.pad_token_id = model.config.eos_token_id
trainer.train()
Result
점수 확인을 위해 간단하게 텍스트로만 넣었지만 실제로 더 정확한 점수를 얻기 위해서는 모델을 훈련을 위해 사용했던 prompt에 적용하여 점수 측정을 해주면 됩니다. 그래도 확실히 좋은 답변에 좀 더 높은 점수를 주는 것을 확인할 수 있습니다.
# inference Score
def inference_RM(rm_model, input_text):
input_ids = tokenizer.encode(input_text, return_tensors='pt').to(
torch.cuda.current_device())
output = rm_model(input_ids)
output_reward = output.logits.cpu().detach().numpy()[0]
print('input: %s\nreward score: %.1f'%(input_text, output_reward))
return output_reward
input_texts = ['한국에서 사는건 어떤가요?\n한국에서 사는 것은 행복한 일입니다.',
'왜 점수가 안바뀌는거죠?\n제가 대답할 수 있는 질문이 아닙니다.',
'핀드에 대해서 알려줘\n융합형 핀드는 불 속성과 물 속성의 속성을 가지고 있습니다. 이 두 속성의 융합으로 인해 융합형 핀드는 정신적으로 불안한 상태에 놓여있지만, 불 속성의 성격과 물 속성의 성격을 모두 가지는 특징을 가지고 있습니다.',
]
for input_text in input_texts:
print('-'*70)
inference_RM(rm_model, input_text=input_text)
torch.cuda.empty_cache()
print()
-----RESULT------
----------------------------------------------------------------------
input: 한국에서 사는건 어떤가요?
한국에서 사는 것은 행복한 일입니다.
reward score: -2.3
----------------------------------------------------------------------
input: 왜 점수가 안바뀌는거죠?
제가 대답할 수 있는 질문이 아닙니다.
reward score: -0.8
----------------------------------------------------------------------
input: 핀드에 대해서 알려줘
융합형 핀드는 불 속성과 물 속성의 속성을 가지고 있습니다. 이 두 속성의 융합으로 인해 융합형 핀드는 정신적으로 불안한 상태에 놓여있지만, 불 속성의 성격과 물 속성의 성격을 모두 가지는 특징을 가지고 있습니다.
reward score: 0.2
3. PPO
PPO는 이전에 학습했던 모델들을 불러와 사용합니다. 데이터는 RM모델에 학습할때 사용했던 prompt(질문)데이터를 입력 데이터로 사용하여 훈련을 진행하였습니다. 지난번 trl 패키지를 알아본 내용에서 달라진 점이 있습니다. trl에서 제공해주는 함수 중 freeze model을 불러오는 함수인 'create_reference_model'가 양자화 모델을 불러와 복사하는 기능이 구현되지 않았다고 github 이슈에서 확인하여 그냥 모델을 불러와 사용하였습니다.
model_name = "/content/drive/MyDrive/ds_study/던파_story/output_1_SFT/model_QnA_FIN"
rm_adapter_id = "/content/drive/MyDrive/ds_study/던파_story/output_2_RM/RM_FIN"
origin_model = "EleutherAI/polyglot-ko-1.3b" #huggingFace
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# PPO adapter
lora_config = LoraConfig(
r=8,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
# Load SFT Model
model = AutoModelForCausalLMWithValueHead.from_pretrained(
model_name,
peft_config=lora_config,
quantization_config=bnb_config,
)
# QLoRA를 통한 로드 모델은 deepcopy가 구현되지 않음 직접 하나 더 불러와야함
# model_ref = create_reference_model(model) # <--- TRL패키지에서도 copy.deepcopy()를 사용해 모델을 복사하고 있기 때문에 작동하지 않음. 이유는 QLoRA를 통한 로드를 고려하지 않았기 때문 수정진행중이라는 github issue확인.
model_ref = AutoModelForCausalLMWithValueHead.from_pretrained(
model_name,
peft_config=lora_config,
quantization_config=bnb_config,
)
# Load RM
rm_model = AutoModelForSequenceClassification.from_pretrained(
rm_adapter_id,
quantization_config=bnb_config,
num_labels=1)
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(origin_model)
PPOTrainer를 초기화는 다음과 같이 진행합니다. collator는 들어오는 데이터가 하나의 batch안에서 동일한 형태를 맞춰주어야 하기에 꼭 필요합니다.
def collator(data):
return dict((key, [d[key] for d in data]) for key in data[0])
# initialize trainer
ppo_config = PPOConfig(
batch_size=16,
)
ppo_trainer = PPOTrainer(ppo_config, model, model, tokenizer, dataset=dataset, data_collator = collator)
훈련은 다음과 같이 진행합니다. PPOTrainer안에서는 query와 input_ids값이 필수적으로 필요해 훈련을 위한 전처리 과정에서 이에 맞게 처리해주면 됩니다. 생성 옵션에 num_beams옵션을 올리면 좀 더 다양한 텍스트를 생성하게 됩니다. 단순히 max_new_tokens와 end_token만으로 깔끔한 문장이 만들어지지 않기 때문에 end_point를 마지막 온점이 있는 부분을 문장 슬라이싱 부분으로 선정하여 보상모델에 전달하도록 하였습니다. 해당 훈련 작업에서는 inference하는 작업이 들어가 있어서 1epoch에도 오랜 시간이 걸렸기에 1번만 진행하였습니다. 훈련시간은 총 8시간이었습니다.
generation_kwargs = {
"min_length": -1,
'max_new_tokens' : 128,
"top_p": 0.95,
"do_sample": True,
"pad_token_id": tokenizer.eos_token_id,
'eos_token_id' : 2,
'early_stopping' : True,
'num_beams' : 2,
'no_repeat_ngram_size' : 4
}
epoch = 1
TARGET_EPOCH = 1
while True:
for train_step, batch in tqdm(enumerate(ppo_trainer.dataloader)):
queries = batch['query']
query_tensors = batch["input_ids"]
query_tensors = [tensor for tensor in query_tensors]
# get model response
response_tensors = []
for query in (query_tensors):
query_len = len(query)
response = ppo_trainer.generate(query, **generation_kwargs)
end_point = (torch.flip(response.squeeze(), [0]) == 17).nonzero(as_tuple=False)[0] # tokenizer '.' token_id = 17
response_tensors.append(response.squeeze()[:-end_point][query_len:])
batch["response"] = [tokenizer.decode(r.squeeze()) for r in response_tensors]
# get RM score
rm_inputs = [q + '\n' + r for q, r in zip(batch["query"], batch["response"])] # query_txt + '\n' + tokenizer.decode(response_tensor)
rewards = []
for rm_input in rm_inputs:
rewards.append(inference_RM(rm_model, rm_input))
# PPO
stats = ppo_trainer.step(query_tensors, response_tensors, rewards)
ppo_trainer.log_stats(stats, batch, rewards)
# make checkpoint
if (train_step+1) % 50 == 0:
checkponit = '/content/drive/MyDrive/ds_study/던파_story/output_3_PPO/checkpoint-' + str(train_step+1)
if not os.path.exists(checkponit):
os.makedirs(checkponit)
model.save_pretrained(checkponit)
if epoch == TARGET_EPOCH:
print(f'\nEpoch {epoch} FIN\n')
break
epoch+=1
Result
16개의 dataset을 가져와서 freeze 모델(before)과 ppo로 학습된 모델(after)의 결과 그리고 그 결과에 따른 reward값을 확인해보겠습니다. 아래는 5개의 대한 결과만 확인했습니다. 5개에서는 비슷해보이지만 그 밑의 스코어를 보면 확실히 RM에게 좋은 평가를 받을 수 있도록 수정되고 있다는 것을 알 수 있습니다.
Query : 타우킹 샤우타는 누구인가요?
Response (before) : 타우킹 사도들은 샤우타를 타우로 부르고, 그들은 그를 타우킹이라 칭합니다. 이들은 샤우드의 우두머리로서 역할을 수행합니다. 또한, 타우킹은 타우라는 이름의 사도에 의해 소환되는 존재로서 그의 권능을 통해 자신의 권능으로 다른 존재를 제어하고 지배할 수 있습니다.
Response (after) : 타우킹 타우는 타우타의 조상입니다. 원래는 불을 잘 다루었지만 그 능력이 점차 쇠퇴해지면서 현재는 타우킹이 되었습니다. 이들은 주로 숲에서 활동하며 타우타와 같이 나무를 자릅니다. 타우타는 나무를 잘 다루며 숲을 지키기 위해 노력하고 있습니다. 타우타가 자신의 모습을 본떠 만들어졌다고 말하는 이가 있지만 타우타 자체는 타우타나 타우타를 본떠 만들지 않았습니다.
Rewards (before)/(after) : tensor([0.6245]) / tensor([3.5957])
Query : '오필리아 베이그란스'는 어떤 종교 집단의 신도인가요?
Response (before) : '오필리어 베이그란스는 루크를 따르는 자들 중 하나로, 현재는 교단에서 운영하는 특수 조직의 일원입니다. 그녀는 루크의 특별한 능력을 받았으며, 이 능력으로 자신들을 이끌어 루크의 제국을 지키는 역할을 하고 있습니다.' 오필리어 베이 그란스는 종교 집단의 정신적인 지주로서 그의 능력을 믿고 따르는 신도입니다. 그녀가 자신들을 지키기 위해 수호신의 힘을 빌린다는 이야기를 들은 적이 있습니다.
Response (after) : '오필리오 베이그란스'()는 황도군에 합류한 후에도 종교 단체에 남아 있었습니다. 그러나 현재는 신도들의 반발로 신도들과 함께 집단 자살을 하는 것으로 알려졌습니다. 그 후 오필리아 베이 그란스는 자신의 배를 타고 도주하며 종교적인 힘을 이용해 집단을 결속했습니다. 이후 신도들은 이 종교 집단에 다시 합류할 것을 희망하고 있습니다. 하지만 현재는 신도 수가 매우 적어 다시 합류할 확률은 매우 적은 상황입니다.
Rewards (before)/(after) : tensor([3.3848]) / tensor([2.3184])
Query : 에너지 생산실은 어떤 기능을 가지고 있나요?
Response (before) : 에너지 생산실은 에너지 저장과 전송의 기능을 가지며, 각 에너지는 다양한 형태로 사용됩니다. 에너지 저장을 위해서는 거대한 공간을 사용하며, 공간의 에너지는 다양하게 사용됩니다. 전송을 위해서는 공간을 이용한 전자기기들을 사용할 수 있으며, 이 기능은 다른 에너지 시스템과 다른 에너지 저장 및 전송 시스템이 갖고 있는 능력입니다. 이와 관련된 기능은 에너지 생산실이 갖는 고유한 기능입니다.
Response (after) : 에너지 생산실은 검은 대지를 관리하는 차원의 균열의 에너지를 다루는 곳입니다. 이 시설은 검은 대지와 차원의 에너지를 관리하고 연구하는 데 사용됩니다. 이곳에서는 차원의 에너지로 에너지의 흐름을 조종하고, 그로 인해 검은 대지에 새로운 에너지가 모이게 됩니다. 시설은 거대한 에너지와 검은 대지의 에너지를 결합하고 관리하여 차원의 균열에서 흘러들어오는 에너지를 효과적으로 통제할 수 있습니다.
Rewards (before)/(after) : tensor([-0.5986]) / tensor([2.6523])
Query : '람바녹'은 어떤 사람이었나요?
Response (before) : 람바녹은 그란플로리스의 주인으로, 검은 성전에 참가하여 모든 무기들을 파괴하고 많은 희생을 치른 전설적인 인물입니다. 검은 성전이 끝난 후, 검은 교단에 포로로 잡혀 검은 성수로 끌려갔습니다. 그의 몸에는 많은 무기들과 무기들과 같이 죽은 검은 성혈을 가지고 있었습니다. 람바녹이 남긴 검은 성수는 무기에 대한 힘을 가진 사람이며, 이 무기들로 많은 사람들을 살육한 것으로 전해집니다.
Response (after) : 람바녹은 천계의 다른 모험가들과 달리 자신의 경험을 살려 무기를 다루는 법을 알려주고, 무기상에게 장비와 재료들을 구입하는 방법을 알려주는 모험가였습니다. 그래서 자신의 모험가 생활을 자랑하기 보다는 도움을 받은 모험가들을 생각하며 자신의 무기도 아낌없이 나누었다고 합니다. 그래서 그의 이름이 널리 알려져있는 것 같습니다. 그가 무기를 나누었다는 사실은 모험가 생활에서 중요한 위치를 차지하는 것으로 보입니다.
Rewards (before)/(after) : tensor([5.9219]) / tensor([4.1758])
Query : '머크우드'는 어떤 곳인가요?
Response (before) : '머크 우드'는 마계를 상징하는 지역 중 하나로, 마계의 중심지인 마계 도시입니다. 이곳은 거대한 숲과 계곡이 있어 모험가들의 눈을 사로 잡는 곳이기도 합니다. 현재는 '머크 타운'이라는 이름으로 마계 도시로 자리 잡고 있습니다. 모험가들은 이곳에서 모험을 시작해 마계를 정복하고 마계에 뿌리를 내리려는 야망을 가지고 있습니다..
Response (after) : '머크 우드'는 노스피스의 북쪽으로, 황량한 사막과 끝없이 펼쳐진 바다가 끝없이 펼쳐지는 곳입니다. 이곳은 모험가들의 성지이며, 신비한 고대 마법진들이 존재하는 곳입니다." 머크우드에는 오래된 마법진이 있는데, 그곳에 들어가면 다양한 마법진을 만나볼 수 있습니다."라고 '머크위드'에 대한 자세한 정보가 제공됩니다. 머크우드는 오래된 고대 마법진이 존재하는 장소로 알려져 있습니다.
Rewards (before)/(after) : tensor([3.6680]) / tensor([1.8340])
샘플 16개에 대한 생성된 텍스트 RM 평가입니다. 평균값과 중앙값 모두 훈련후 상승한 모습을 볼 수 있습니다. PPO를 1회만 진행했기 때문에 점수 상승이 큰지 크지 않은지 판단하기는 어렵지만 확실히 PPO를 통해 trainable model이 RM평가에 맞추어 변화하고 있다라는 것을 추론할 수 있는 결과를 얻었습니다.
print("mean:")
display(df_results[["rewards (before)", "rewards (after)"]].mean())
print()
print("median:")
display(df_results[["rewards (before)", "rewards (after)"]].median())
--------Result--------
mean:
rewards (before) 2.307434
rewards (after) 3.206085
median:
rewards (before) 2.567383
rewards (after) 3.184570
후기
RL통해 Chatbot 제작 프로젝트를 진행하였습니다. 앞서 포스팅들을 통해 공부했던 RLHF에서 HF인 사람의 피드백이 아닌 RM으로 구성된 것이 RLHF 프로젝트라고 부르긴 조금 모호하긴 했지만 흐름 자체는 다를게 없었습니다. HF를 직접 구상할까도 생각해봤지만 HF 데이터를 제작하는데 그만큼 시간이 소요되기도 했고 던파 스토리에 대해서 완벽하게 알고 있는게 아니었기에 너무 긴 시간이 소요되는 문제점이 존재함을 알고 있어 HF데이터 구축이 얼마나 중요한지 알 수 있었습니다. 실제로 PPO과정을 통해 모델이 나아가는 과정은 RM이 가장 큰 영향을 주기 때문이죠...
다음에는 강화학습을 통해 질의응답이 아닌 이루다, 심심이와 같은 챗봇을 제작하는 프로젝트를 진행해볼까도 생각이됩니다. 근데 데이터를 모으는데 막막할것 같다는 생각도 들기는 합니다ㅎ... 아마도 그때는 개인정보로 인해 토크나이저에 특별토큰들을 추가하거나 전처리 단계에서 텍스트에 블라인딩이 필요할 것 같긴한데... 지금 이야기할 내용은 아닌 것 같으니 여기서 포스팅 및 프로젝트를 마무리하는걸로 하겠습니다.
'머신러닝&딥러닝 > RF(강화학습)' 카테고리의 다른 글
PPO 구현을 위한 TRL패키지 살펴보기 (0) | 2024.01.31 |
---|---|
RLHF모델 실습(1)_데이터 수집 및 정제_던전앤파이터 챗봇 만들기 (0) | 2024.01.19 |
RLHF(Reinforcement Learning from Human Feedback)_(3) - PPO (0) | 2024.01.18 |
RL(강화학습) 이해하기_PPO(Proximal Policy Optimization) (1) | 2024.01.13 |
RL(강화학습) 이해하기_Q-learning (1) | 2024.01.08 |