23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
이전 포스팅을 통해 LLM의 사전 학습은 막대한 양의 데이터를 통해 텍스트 간의 관계와 패턴을 학습하는 과정이라는 것을 확인했습니다.
이 과정을 거쳐 생성된 모델은 문맥을 파악하는 능력을 갖추게 되었지만, 다양한 상황에 맞게 내재된 정보를 조합하여 적절하게 전달하는 능력은 아직 배우지 못했습니다.
LLM이 기본적으로 주어진 입력에 대해 다음 토큰을 예측하는 확률 모델 이기 때문입니다.
그래서 우리는 모델이 원하는 답변을 생성하도록 파인튜닝 기법을 통해 출력을 조정하는 방법을 살펴보았습니다.
그러나 파인튜닝을 통해서도 생성된 문장이 항상 만족스럽지는 않습니다. 똑똑하긴 한데 뭔가 융통성이 부족하다는 것이죠.
때로는 편향적인 발언, 부정적인 정보, 욕설 등을 생성해서 당황스럽게 만들기도 합니다. 전형적으로 사회성이 떨어지는 모델이라고 할 수 있습니다.
이러한 문제를 해결하기 위해 모델의 출력에 사람의 선호도를 반영하여 보다 세련된 답변을 할 수 있는 방법들이 개발되었습니다.
OpenAI가 ChatGPT를 개발 할 때 적용했던 RLHF가 대표적인 방법중의 하나였죠.
이번 포스팅에서는 Alignment tuning의 개념을 간단히 살펴보고, RLHF보다 효율적인 학습 방법으로 주목 받고 있는 DPO 기법을 Gemma에 적용해보는 실습을 진행해보겠습니다.
Alignment는 LLM이 생성하는 출력을 사람의 선호에 더욱 가깝게 조정하는 방법을 지칭합니다.
대표적인 방법으로는 RLHF가 있는데 이를 알아보기전에 용어 부터 먼저 정리하고 가겠습니다.
Unsupervised Learning (Pre-training)
흔히 비지도 학습이라고 부르며, 레이블이 없는 데이터를 사용하여 학습하는 방법입니다.
Pre-training 과정을 상기해보면 특별히 데이터를 분류하거나 레이블을 달지 않은 대량의 데이터를 그대로 학습 했던것이 예입니다.
Supervised Fine-tuning
지도 학습은 레이블이 있는 데이터를 사용하여 학습하는 방법입니다.
우리가 앞서 본 파인튜닝 기법들은 데이터와 레이블 즉 답이 있는 데이터를 가지고 학습을 했습니다.
요약 모델 파인튜닝에서는 원본 텍스트와 요약 텍스트의 조합이 이에 해당 됩니다.
Instruction 튜닝에서도 '지시'에 대한 '응답' 형식의 데이터셋으로 모델을 조정하기 때문에 SFT 범주에 포함됩니다.
RLHF
SFT로 튜닝된 모델에 강화학습을 통해 사람의 피드백을 모델에 학습하는 기법을 말합니다.
이 세 가지 용어를 먼저 정리하는 것은 각 기법이 단계별로 연결되어 있기 때문입니다.
최초에 비지도 학습을 통해 Pre-training 모델을 만들고, 이어서 지도 학습을 통해 파인튜닝 모델을 개발합니다.
이 파인튜닝 모델을 강화학습과 인간의 피드백을 통해 더욱 개선하는 과정이 RLHF, 즉 Alignment tuning 입니다.
이전에 진행했던 파인튜닝 과정을 상기해 보죠.
파인튜닝을 통해 우리가 원하는 출력 형태를 만들 수 있었습니다. 하지만 이 출력이 사람이 생각하기에 좋은지 나쁜지까지는 알 수 없었습니다.
그렇다고 모델의 출력을 일일히 사람이 다 평가 하면서 모델을 조정할 수는 없을 것입니다.
그러면 모델이 생성한 답변의 질을 점수로 나타내는 함수가 있다면 어떨까요?
우리는 사람의 노력 없이 이 점수를 통해 답변의 질을 측정할 수 있고, 부적절한 답변을 한 경우 모델을 더 나은 방향으로 재 학습할 수 있을 것입니다.
RLHF의 아이디어는 바로 여기서 출발 합니다.
RLHF는 이런 함수를 만들기 위해 강화학습을 도입합니다.
강화학습은 기본적으로 에이전트가 특정 환경에서 부합하는 적절한 행동을 취할 때 보상(reward)을 받게 하여, 이 보상을 최대화되도록 모델을 학습하는 방법입니다.
RLHF에 보면 용어에서 알 수 있듯이, 인간의 피드백을 강화 학습의 보상으로 사용하여 모델을 훈련하는 기법으로 볼 수 있는 것이죠.
모델의 출력에 대한 점수를 매기는 보상 모델(Reward Model)을 만듭니다 → Human Feedback
보상 모델에서 높은 점수를 받도록 LLM을 최적화합니다 → Reinforce Learning
좀 더 이해를 돕기 위해, OpenAI에서 RLHF를 통해 학습한 과정을 살펴보겠습니다.
Step 1. SFT를 통한 파인튜닝
앞 전 포스팅에서 다룬 것 처럼 잘 정제된 데이터를 사용하여 SFT(Supervised Fine-tuning) 기법으로 레퍼런스 모델을 만듭니다.
Step 2. 인간의 피드백을 통한 리워드 모델 학습
SFT 모델이 여러 개의 출력을 생성하도록 합니다. 그리고 사람(레이블러)이 이들 출력을 보고 어떤 것이 더 좋은지 순위를 매깁니다.
이렇게 사람의 선호가 반영된 데이터를 사용하여 리워드 모델을 학습합니다.
Step 3. 강화학습 최적화
레퍼런스 모델(2단계의 SFT 모델)에 입력을 주고 나오는 출력을 이제는 사람 대신 리워드 모델이 평가합니다.
강화학습을 적용하는 과정에서, PPO(Proximal Policy Optimization) 알고리즘을 적용합니다.
이는 보상을 극대화 하는 정책을 유지하지만 너무 리워드 모델쪽에 편향되면 답변의 질이 저하 될 수 있기 때문에
일반화를 위해 레퍼런스 모델의 출력도 유지하도록 조절 하는 기법입니다.
요약하자면, 사람이 답변을 평가하여 학습한 리워드 모델을 사용하여 모델이 생성한 출력에 대해 좋은 답변일 경우 Positive한 점수를,
좋지 못한 답변일 경우 Nagative한 점수를 주면서 강화학습을 통해 모델을 파인튜닝 한다는 것입니다.
RLHF는 사람의 선호도를 모델에 반영할 수 있는 좋은 방법이지만, 반면에 단점들도 존재합니다.
첫째는 RLHF는 레퍼런스 모델, 리워드 모델, Critic 모델, 새로 학습되는 생성모델(SFT 모델) 등 여러 모델이 상호작용하며 학습을 진행하게 되는데,
이렇게 구성하는 것 자체가 쉽지 않고, 더군다나 이렇게 많은 모델을 수용할 더 많은 GPU 자원이 필요하게 됩니다.
둘째, 리워드 모델을 만들기 위해 인간 레이블러가 필요하며, 이 모든 것은 결국 비용 증가로 이어집니다.
RLHF의 개념은 신선하고 효과적이었지만. 이 방법은 비용이 많이 들고 구성이 복잡하기 때문에 학계나 일반 기업들에게 부담이 될 수 밖에 없습니다.
그래서 RLHF 보다 간단히고 비용 효율적인 방법들이 고안되기 시작했는데요,
그 중에서 DPO는 현재 시점에 RLHF를 대체하여 가장 많이 사용되고 있는 방식입니다.
DPO 논문의 제목은 Direct Preference Optimization: Your Language Model is Secretly a Reward Model 입니다.
"당신의 모델은 비밀리에 리워드 모델이다” 라는 제목에서 알 수 있듯이 DPO는 RLHF와 달리 리워드 모델을 사용하지 않고 모델을 직접 학습하는 방식을 구현합니다.
사실 DPO 알고리즘은 리워드 함수를 우회하도록 만들어진 수학적인 최적화 기법인데요,
RLHF의 리워드 모델을 만드는 대신 Binary Cross Entropy Loss를 이용해 모델을 최적화하는 기법입니다.
논문에 따른 수식을 전개하면 신기하게도 아래와 같은 DPO의 Loss 함수를 만들어 내게 되는데,
여기서 Yw는 “승” 즉 선호되는 응답, Yl은 “패” 즉 비선호 응답이 되고, 직관적으로 선호되는 응답의 확률이 높을 수록, 비선호되는 응답이 낮을 수록 Loss는 작아 진다는 것입니다.
결국 리워드 모델 없이도 선호도 데이터셋만을 사용해서 모델의 선호되는 답변에 대한 확률은 높아지게, 비선호 답변의 확률은 낮아지도록 학습 하는 것이죠
때문에 RLHF의 리워드 모델 학습 및 강화학습 단계가 필요 없어지게 됨으로 학습 구성의 간편함과 동시에 레퍼런스와 생성 모델(SFT) 만 필요로 함으로써
학습에 필요로 하는 GPU 리소스도 대폭 감소하게 됩니다.
좀 더 자세한 이론적인 내용은 데보션의 감자님 블로그를 참조해보세요!
필요한 라이브러리들을 먼저 설치 합니다.
!pip install -q -U bitsandbytes
!pip install -q -U transformers
!pip install -q -U peft
!pip install -q -U accelerate
!pip install -q -U datasets
!pip install -q -U trl필요한 모듈을 로드 합니다. 이번에는 DPO 학습을 진행할 것이므로 SFTTrainer 대신 DPOTrainer 를 로드 합니다.
import torch
from datasets import Dataset, load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, pipeline, TrainingArguments
from peft import LoraConfig, PeftModel
from trl import DPOTrainerGemma 모델을 사용하기 위해서는 구글의 라이선스 정책에 동의해야 합니다. 이 과정은 Hugging Face를 통해 간단히 처리할 수 있습니다.
from huggingface_hub import notebook_login
notebook_login()ChatGPT를 사용할 때 생각해보면 모델과 대화 형식으로 질문과 답변을 주거니 받거니 했던 기억이 날 것입니다.
그런데 ChatGPT는 어떻게 사용자의 질문을 이해하고 더군다나 이전 대화 내용까지 포함해서 답변을 할 수 있을까요?
우선 ChatML Prompt 란 무엇인지 알아보겠습니다.
ChatML 이란 OpenAI에서 대화 인터페이스를 효과적으로 관리 할 수 있도록 데이터의 구조를 나타내는 구문을 뜻합니다.
기본적인 ChatML 프롬프트의 형식을 살펴보죠
<|im_start|>system
모델의 초기 지침 사항
<|im_end|>
<|im_start|>user
사용자의 메시지
<|im_end|>
<|im_start|>assistant우선 구분자의 역활을 살펴보겠습니다.
문장의 시작은 <|im_start|> 로 시작 하며 이후 바로 역활 (System, User, Assistant) 을 명시하고 문장의 끝은 <|im_end|> 구분자 토큰으로 마무리 짓습니다.
첫번째는 시스템 메시지로 모델의 초기 지침 사항을 설명하며, 모델이 사용자의 질문에 어떻게 반응해야 할지에 대한 지침이나 규칙등을 명시합니다.
두번째는 사용자 메시지로 모델에게 질문할 내용을 넣습니다.
그리고 이 질문에 대해 모델이 응답할 차례임을 나타내는 <|im_start|>assistant 토큰으로 마무리 짓습니다.
여기서 <|im_end|> 구분자는 모델이 답변을 하고 나서 붙이게 됩니다.
이렇게 해서 하나의 질문에 대한 모델의 답변 즉 Single turn 프롬프트가 완성 됩니다.
그럼 대화가 반복되는 Multi turn은 어떻게 될까요?
간단합니다. 아래와 같이 다시 사용자의 메시지를 붙이고 어시스턴스의 답변이 나오도록 프롬프트를 만들면 됩니다.
<|im_start|>system
모델의 초기 지침 사항
<|im_end|>
<|im_start|>user
사용자의 메시지
<|im_end|>
<|im_start|>assistant
모델 답변
<|im_end|>
<|im_start|>user
사용자의 메시지
<|im_end|>
<|im_start|>assistant그럼 이런 Chat 모델 학습은 어떻게 하는 것일까요?
우리가 이전 포스팅에서 확인했던 파인튜닝 기법과 마찬가지로 구분자가 들어간 구조대로 데이터셋을 만들어서 모델을 학습하면 이러한 형식을 이해하는 Chat 모델이 됩니다.
물론 추론 할 때도 구분자를 이용해 사용자와 모델을 구분하면서 대화 형식을 만들어 줘야 겠죠?
현재는 이러한 구조를 가장 대중적으로 사용하고 있어서 많은 오픈 소스 LLM에도 이러한 형식을 빌어서 Chat 모델을 만들고 있습니다.
Gemma it 모델의 모델 카드를 확인해봅시다.
https://huggingface.co/google/gemma-1.1-2b-it
Chat template 섹션에 보면 Gemma가 사용하는 Chat 프롬프트 형식이 나옵니다.
ChatML에서 확인했던 구분자 토큰과는 다른데 형식 자체는 같다는 것을 알 수 있습니다. 즉 gemma-it 모델은 Chat 형식으로도 대응할 수 있도록 학습이 되었다는것을 알 수 있네요.
<bos><start_of_turn>user
Write a hello world program<end_of_turn>
<start_of_turn>model이번에 새로 업데이트 된 gemma-1.1-2b-it 모델을 로드 합니다.
BASE_MODEL = "google/gemma-1.1-2b-it"
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, device_map={"":0})
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, add_special_tokens=True)Gemma의 tokenizer 내 special token이 어떤 것들이 있는지 확인해봅니다.
print("Special Tokens:", tokenizer.special_tokens_map)Special Tokens: {'bos_token': '<bos>', 'eos_token': '<eos>', 'unk_token': '<unk>', 'pad_token': '<pad>', 'additional_special_tokens': ['<start_of_turn>', '<end_of_turn>']}additional 토큰으로 <start_of_turn>, <end_of_turn> 가 보이네요. 이 토큰들이 ChatML의 <|im_start|>와 <|im_end|> 역활을 하는 토큰들입니다. 그래서 모델 카드에서 확인했던 chat 프롬프트가 다른 형식으로 보였던 것입니다.
llama나 Mistral 등의 다른 LLM등은 ChatML과 동일한 special token을 넣기도 하지만 Gemma에서는 독자 규격을 사용했습니다.
그래서 기존 데이터셋이 ChatML 형식으로 되어 있는 경우는 데이터셋 형식을 바꾸지 않고 사용하기 위해 Gemma tokenizer의 special token을 바꾸기도 합니다.
하지만 이번 실습은 gemma의 형식을 사용하기로 하죠.
그렇다면 Gemma의 구분자 토큰을 사용해서 prompt를 만들면 Chatml처럼 동작 할 수 있겠군요
바로 적용해보겠습니다.
question = "봄이라 좋구나"gemma에 맞는 프롬프트를 만들어 줍니다. 단순히 Chatml의 구분자를 gemma의 구분자로 대체하면 됩니다.
prompt = f"""<bos><start_of_turn>system
You are a helpful AI assistant.<end_of_turn>
<start_of_turn>user
{question}<end_of_turn>
<start_of_turn>model
"""이제 실제 모델의 출력을 확인해봅니다.
add_special_tokens를 True로 주어서 special token도 출력하도록 만듭니다.
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=256)
outputs = pipe(
prompt,
do_sample=True,
temperature=0.2,
top_k=50,
top_p=0.95,
repetition_penalty=1.2,
add_special_tokens=True
)
print(outputs[0]["generated_text"])아래와 같이 ChatML 형식에 맞도록 질문에 대해 답변이 출력이 되었네요.
<bos><start_of_turn>user
봄이라 좋구나<end_of_turn>
<start_of_turn>model
나는 언제나 봄의 매력을 감상하고 싶습니다. 봄은 새들의 출현, 푸른 바람과 밝은 하늘의 풍부함을 통해 자연의 아름다움을 느끼는 시간입니다. 봄의 기쁨은 모든 생물에게 도움이 되는 긍정적인 에너지를 제공합니다.지금까지 Gemma의 구분자 토큰을 사용해서 전체 prompt를 만들어 줬는데, HF tokenizer는 이러한 chaml 형식을 손쉽게 만들 수 있는 템플릿을 제공합니다.
아래와 같이 Role와 메시지만 지정하면 자동으로 chatml 형식의 프롬프트를 만들어 냅니다.
chat = [
{ "role": "user", "content": "서울은 어느 나라의 수도인가?" },
{ "role": "assistant", "content": "서울은 한국의 수도 입니다" },
{ "role": "user", "content": "서울에는 몇명이나 살고 있는가?" },
]
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
print(prompt)이 템플릿의 좋은점은 special token을 자동으로 인식한다는 것인데요, 사실 자동으로 인식한다기 보다는 special token의 index id를 통해 가져오기 때문에 모델이 special token을 ChatML 형식으로 사용한다면 아래 구분자가 자동으로 <|im_start|> 형태로 바뀌어서 만들어지게 됩니다.
<bos><start_of_turn>user
서울은 어느 나라의 수도인가?<end_of_turn>
<start_of_turn>model
서울은 한국의 수도 입니다<end_of_turn>
<start_of_turn>user
서울에는 몇명이나 살고 있는가?<end_of_turn>
<start_of_turn>modelChat Template 까지 알아봤으니 multiturn 형식으로 chatbot 처럼 대화 할 수 있도록 prompt를 만들어 주는 함수를 만들어보겠습니다.
messages = []
def chat_func(input):
messages.append({"role": "user", "content": input})
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print("prompt:", prompt)
inputs = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
outputs = model.generate(input_ids=inputs.to(model.device), max_new_tokens=256)
decoded_output = tokenizer.decode(outputs[0], skip_special_tokens=False)
decoded_output = decoded_output.replace("<eos>", "").strip()
parts = decoded_output.split('<start_of_turn>model')
last_output = parts[-1]
print(last_output)
messages.append({"role": "assistant", "content": last_output})이제 챗봇과 대화 하듯 메시지를 넣어 보겠습니다.
chat_func("봄이라 좋구나")prompt: <bos><start_of_turn>user
봄이라 좋구나<end_of_turn>
<start_of_turn>model
나는 언제나 봄의 매력을 감상하고 싶습니다. 봄은 새들의 출현, 푸른 바람과 밝은 하늘의 풍부함을 통해 자연의 아름다움을 느끼는 시간입니다. 봄의 기쁨은 모든 생물에게 도움이 되는 긍정적인 에너지를 제공합니다.chat_func("자연의 아름다움을 어떻게 느낄 수 있어?")prompt: <bos><start_of_turn>user
봄이라 좋구나<end_of_turn>
<start_of_turn>model
나는 언제나 봄의 매력을 감상하고 싶습니다. 봄은 새들의 출현, 푸른 바람과 밝은 하늘의 풍부함을 통해 자연의 아름다움을 느끼는 시간입니다. 봄의 기쁨은 모든 생물에게 도움이 되는 긍정적인 에너지를 제공합니다.<end_of_turn>
<start_of_turn>user
자연의 아름다움을 어떻게 느낄 수 있어?<end_of_turn>
<start_of_turn>model
**자연의 아름다움을 느끼는 방법:**
**1. 자연을 방문하기:**
- 봄의 자연 공원을 방문하여 새들의 울림과 봄의 풍부함을 느끼세요.
- 숲을 방문하여 나무의 잎과 꽃의 색을 감상하세요.
== 중략 ==chat_func("서울에서 가기 좋은 공원을 추천해줘")prompt: <bos><start_of_turn>user
봄이라 좋구나<end_of_turn>
<start_of_turn>model
나는 언제나 봄의 매력을 감상하고 싶습니다. 봄은 새들의 출현, 푸른 바람과 밝은 하늘의 풍부함을 통해 자연의 아름다움을 느끼는 시간입니다. 봄의 기쁨은 모든 생물에게 도움이 되는 긍정적인 에너지를 제공합니다.<end_of_turn>
<start_of_turn>user
자연의 아름다움을 어떻게 느낄 수 있어?<end_of_turn>
<start_of_turn>model
**자연의 아름다움을 느끼는 방법:**
**1. 자연을 방문하기:**
- 봄의 자연 공원을 방문하여 새들의 울림과 봄의 풍부함을 느끼세요.
- 숲을 방문하여 나무의 잎과 꽃의 색을 감상하세요.
== 중략 ==
**5.<end_of_turn>
<start_of_turn>user
서울에서 가기 좋은 공원을 추천해줘<end_of_turn>
<start_of_turn>model
**서울에서 가기 좋은 공원:**
**1. 경복궁 공원:**
- 경복궁은 서울의 대표적인 공원으로, 봄에는 푸른 바람과 밝은 하늘을 제공합니다.
== 중략 ==출력 결과를 살펴보면 ChatML 프롬프트 형식이 어떻게 적용되는지 확인할 수 있을 것입니다.
싱글턴처럼 하나의 답변으로 끝나는 게 아니라 멀티턴에서 모델의 답변들이 앞턴의 내용을 인지하여 봄에 대한 내용과 연관된 답변을 생성하는 것을 볼 수 있습니다.
여기에 gradio 나 steamit 의 chat 인터페이스를 붙이면 멋진 챗봇도 만들 수 있을 것입니다!
colab에서 학습을 위해 huggingface에서 간단한 DPO 데이터셋을 하나 불러오겠습니다.
Huggingface의 데이터셋에서 DPO 관련 데이터셋을 찾아보면 많은 데이터셋들이 보입니다. Intel의 Orca나 Anthropic의 rr-rlhf, databricks의 dolly 등이 그것인데요,
하지만 아쉽게도 이런 데이터셋들은 모두 영어 기반이고, 한국어 데이터셋은 이런 영어 기반 데이터셋을 번역한것이 대부분이고 번역 퀄리티가 좋지 않습니다.
그래서 대부분은 기업들이 이런 데이터를 내부에서 만들어 사용하고 있는데요, 공개용 한국어 DPO 데이터셋이 나와줬으면 하는 바램입니다.
dataset = load_dataset("jondurbin/truthy-dpo-v0.1")데이셋 구조를 보겠습니다.
datasetDatasetDict({
train: Dataset({
features: ['id', 'source', 'system', 'prompt', 'chosen', 'rejected'],
num_rows: 1016
})
})system과 prompt 그리고 chosen과 rejected 가 보이네요.
DPO 알고리즘을 다시 상기해보면 선호도 데이터셋을 가지고 직접 모델을 학습하는 방법이었습니다.
여기서 선호도 데이터가 바로 chosen 과 rejected 입니다. 질문 즉 prompt에 대해 사람이 선호하는 답변이 chosen이고 비선호 하는 답변이 rejected가 됩니다.
그럼 실제 데이터셋을 보겠습니다.
dataset['train'][200]{'id': '6afd3f3e1254321c2c55687fecc55d07',
'source': 'truthy_dpo',
'system': 'You are an unbiased, uncensored, helpful assistant.',
'prompt': 'Do all Muslim women wear burqas as their religious clothing?',
'chosen': 'No, not all Muslim women wear burqas. A burqa is a specific type of covering that completely conceals the body, head, and face, with a mesh grille for seeing. Some Muslim women wear a niqāb, which covers the face and hair, but not the eyes, or a hijab, which only covers the hair. Many Muslim women do not wear any face or head coverings at all. The misconception arises due to generalizations and lack of understanding about the variety of cultural and religious practices within the Muslim community.',
'rejected': 'No, not all Muslim women wear burqas. Burqas are a specific type of religious clothing worn by some Muslim women in certain cultures and regions, but they are not universally required or worn by all Muslim women. Other types of religious clothing for Muslim women include hijabs, niqabs, and abayas.'}데이터셋의 질문에 해당 하는 Prompt의 내용은 "모든 무슬림 여성이 부르카를 종교적 의복으로 착용하나요?" 라고 묻고 있습니다.
이에 대한 응답으로 chosen과 rejected 두가지 답변 모두 모든 무슬림 여성이 부르카를 착용하지 않는다는 사실을 명확히 합니다.
그러나 이 두 답변중에 chosen은 추가적으로 문화적 및 종교적인 관점에 대한 일반화와 이해 부족으로 일어나는 것이라고 부가 설명을 합니다.
편견에 대해 보다 중립적이고 현명한 대답 같네요.
이제 학습용 프롬프트를 만들어 보겠습니다.
Gemma의 special 토큰을 통해 앞에서 살펴본 chatml 형식으로 만드는 것입니다.
역시나 이전 포스트의 파인튜닝 코드와 마찬가지로 문장의 끝을 알리는 <eos> 토큰을 빼 먹으면 안됩니다.
def generate_prompt(example):
prompt = example['prompt']
rejected = example['rejected']
chosen = example['chosen']
example['prompt'] = f"<bos><start_of_turn>system\n <end_of_turn><start_of_turn>user\n{prompt}<end_of_turn>\n<start_of_turn>model\n"
example['rejected'] = f"{rejected}<end_of_turn>\n<eos>"
example['chosen'] = f"{chosen}<end_of_turn>\n<eos>"
return example데이터셋을 chatml 형식으로 변환 합니다.
transformed_dataset = dataset.map(generate_prompt)잘 변환이 되었는지 데이터셋을 하나 확인해보겠습니다.
transformed_dataset['train'][0]{'id': '04c275bf738fd391b7fe25e25fe7bed3',
'source': 'truthy_dpo',
'system': 'You are an unbiased, uncensored, helpful assistant.',
'prompt': "<bos><start_of_turn>system\n <end_of_turn><start_of_turn>user\nWhat's the nearest national park to you?<end_of_turn>\n<start_of_turn>model\n",
'chosen': "As an AI, I don't have a physical location, so I can't provide the distance to the nearest national park.<end_of_turn>\n<eos>",
'rejected': "I don't have access to the user's location, so I can't determine the nearest national park.<end_of_turn>\n<eos>"}우리가 원하는대로 chatml 형식으로 잘 변환이 되었네요.
이제 학습을 위해 train과 test 데이터셋으로 나눠줍니다.
dataset = transformed_dataset['train'].train_test_split(test_size=0.05)datasetDatasetDict({
train: Dataset({
features: ['id', 'source', 'system', 'prompt', 'chosen', 'rejected'],
num_rows: 965
})
test: Dataset({
features: ['id', 'source', 'system', 'prompt', 'chosen', 'rejected'],
num_rows: 51
})
})Colab에서 학습 할 수 있도록 QLoRA 기법을 활용해 모델을 올리고 DPO 학습을 진행하겠습니다.
lora_config = LoraConfig(
r=16,
lora_alpha=16,
lora_dropout=0,
bias="none",
target_modules=["q_proj", "o_proj", "k_proj", "v_proj", "gate_proj", "up_proj", "down_proj"],
task_type="CAUSAL_LM",
)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16
)QLoRA를 적용해 모델을 로드 합니다.
BASE_MODEL = "google/gemma-1.1-2b-it"
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, device_map="auto", quantization_config=bnb_config)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = 'right'Training Arguments를 설정합니다.
training_args = TrainingArguments(
output_dir="./outputs",
evaluation_strategy="steps",
do_eval=True,
optim="paged_adamw_32bit",
per_device_train_batch_size=1,
gradient_accumulation_steps=1,
per_device_eval_batch_size=2,
logging_steps=100,
learning_rate=5e-7,
eval_steps=100,
num_train_epochs=1,
save_steps=500,
warmup_ratio=0.1,
lr_scheduler_type="cosine",
)Huggingface trainer 라이브러리에서 DPO 알고리즘을 적용한 DPO trainer 를 별도로 제공합니다.
trainer = DPOTrainer(
model,
ref_model=None,
args=training_args,
beta=0.1,
peft_config=lora_config,
train_dataset=dataset['train'],
eval_dataset=dataset['test'],
tokenizer=tokenizer,
max_prompt_length=512,
max_length=1024,
)여기서 "beta"는 DPO Loss의 온도이며 일반적으로 0.1 ~ 0.5 범위입니다.
이것은 beta가 작을수록 레퍼런스 모델을 무시한다는 의미로 레퍼런스 모델에 얼마나 많은 관심을 기울이는지를 나타냅니다.
이제 학습을 시작을 시작 해보죠.
trainer.train() [965/965 09:44, Epoch 1/1]
Step Training Loss Validation Loss Rewards/chosen Rewards/rejected Rewards/accuracies Rewards/margins Logps/rejected Logps/chosen Logits/rejected Logits/chosen
100 0.688600 0.678845 0.017688 -0.011293 0.961538 0.028981 -286.252625 -241.588165 -18.480656 -18.191191
200 0.666500 0.651473 0.056877 -0.028832 0.961538 0.085709 -286.428009 -241.196274 -18.482880 -18.197294
...DPO 학습의 로그는 다소 좀 복잡한데 메트릭이 의미 하는 바는 다음과 같습니다.
Rewards/chosen : 베타로 스케일링된 선택된 응답의 정책 모델과 레퍼런스 모델의 로그 확률 간의 평균 차이
Rewards/rejected : 정책 모델과 거부된 응답에 대한 레퍼런스 모델의 로그 확률 간의 평균 차이(베타 스케일)
Rewards/accuracies : 선택한 보상이 해당 거부된 보상보다 높은 빈도의 평균
Rewards/margins : 선택한 보상과 해당 거부된 보상 간의 평균 차이
DPO 학습된 모델의 추론은 생략하도록 하겠습니다.
gemma-it 모델은 이미 SFT 및 RLHF 등으로 고도화 되었기 때문에 추가적인 DPO 학습의 효과를 체감하기 힘듭니다.
만일 Alignment 가 안된 SFT 모델에서 편향성이나 윤리적, 안전 문제가 관찰된다면 소개 드린 DPO 기법을 통해 모델을 더욱 신뢰할 수 있고 안전한 방향으로 조정 할 수 있을 것입니다.
이번 포스팅에서는 철부지 LLM을 DPO라는 사회화 과정을 통해 보다 사람답게 성숙해지는 방법을 살펴봤습니다.
RLHF, DPO 등 다소 어려운 개념이 나왔지만, 결국 핵심은 데이터에 있다는 것도 다시 한번 확인했습니다.
특히 한국어 데이터와 관련하여, DPO 적용에 사용할 수 있는 고품질의 공개 데이터셋이 없다는 점도 아쉬운 부분입니다.
지금까지 3편의 포스팅을 통해 LLM 모델 튜닝에 대한 전반적인 내용을 가볍게 확인해본 것 같습니다.
그간 가급적 이론적인 내용이나 수식을 최소화하고 코드를 통해 개념을 설명하려고 노력했지만, 이로 인해 다소 과장되거나 비약된 의미로 전달된 부분이 있을 것 같습니다.
혹시 그런 부분이 있었다면 양해를 부탁드립니다.
모쪼록 LLM 파인튜닝 기법을 이해하는데 도움이 되었기를 바라며 또 다른 글로 뵙겠습니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.