Ssul's Blog
Chat_template 구조 파인튜닝하기(feat. EXAONE-3.5-7B) 본문
1. Instruction Fine-Tuning
우선 LLM을 튜닝할때는 사전학습이 된 LLM의 지식을 활용하는 것이 핵심이다.
그러기 위해서는 기존의 ML(머신러닝)방식의 입력값과 라벨(정답) 데이터만 무수히 많이 가지고 모델을 만드는 것이 아닌,
엄청난 양의 사전 학습된 언어 지식을 활용하는 것이 Instruction FT라고 할수 있다.
스팸분류기 모델을 만든다고 했을때, 기존 ML방식으로 데이터 셋을 구성한다면,
문자1내용, 스팸문자
문자2내용, 스팸문자
문자3내용, 정상문자
이렇게 데이터 셋(입력값, 라벨)을 구성하고 신경망에 넣어서 스팸과 정상을 구분하는 모델을 만드는 것이다.
instruction FT는 사전학습된 모델이 언어능력을 가지고 있기 때문에
원래 내가 가지고 있던 데이터셋
문자1내용, 스팸문자
문자2내용, 스팸문자
문자3내용, 정상문자
를, 아래의 템플릿에 넣어서 변환시킨다.
instruct_template = {
"prompt_input": "스팸인지 아닌지 구분하세요:\n\n### sms:\n{문자1내용}\n\n### Input:\n{input}\n\n### label:\n",
"prompt_no_input": "스팸인지 아닌지 구분하세요:\n\n### sms:\n{문자1내용}\n\n### Input:\n{input}\n\n### label:\n",
"response_split": "### label:\n"
}
ML에서는 "문자1내용"을 입력한다면,
LLM Instruction FT에서는 "스팸인지 아닌지 구분하세요:\n\n### sms:\n{문자1내용}\n\n### Input:\n{input}\n\n### label:\n"를 입력하는 것이다.
위 내용을 보면, 사전 학습된 LLM은 인간의 언어를 이해하니까, 맥락상 해당 문자를 보고 label을 예측하는 것이, 문자1내용만 입력하는 것보다 모델에게 더 친절한 것이다.
여기까지는 다 아는 Instruction FT이야기
2. Chat_template형태의 Fine-Tuning 어떻게 하는가?
보통 LLM 오픈소스 파인튜닝을 입문하게 되면, 소스코드에서 가장 많이 보는 Template는 위에서 본 알파카 템플릿이다.
입력값 앞에 적절한 맥락을 알려주고, 입력값을 넣고, 라벨을 예측하는 형태.
근데 요즘 가장 한국어 성능이 괜찮다는 LG의 EXAONE을 사용하려고 hf의 문서를 보니,
# Choose your prompt
prompt = "Explain how wonderful you are" # English example
prompt = "스스로를 자랑해 봐" # Korean example
messages = [
{"role": "system",
"content": "You are EXAONE model from LG AI Research, a helpful assistant."},
{"role": "user", "content": prompt}
]
input_ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt"
)
이렇게 openai와 동일한 chat_template를 사용하고 있는 것이다.
이것을 보면서 의문점이 생겼다.
"role": "system"도 임베딩이 되는 것인가?
저렇게 입력되면
{"role": "assistant", "content": "스팸문자"}이렇게 출력되는 것인가? 아니면 "스팸문자"만 출력이 되는 것인가?
알파카 템플릿으로만 파인튜닝을 해오던 나에게는 의문점 투성이었다. 그래서 테스트를 완료하고, 확인한 내용을 공유하고자 한다.
3. Chat_template의 데이터 토크나이징
3-1. 맥락있는 LLM용 chat_template데이터 구성
우선 chat_template로 학습데이터를 구성하는 첫번째 코드 살펴보면, 우리가 잘 알고 있는 것처럼, LLM이 잘 문제를 해결할 수 있게 맥락있는 데이터를 구성해. 근데 기존의 알파카 템플릿이 아닌 챗 템플릿으로..
SYSTEM_PROMPT="스팸인지 아닌지 구분하세요"
instruction="문자1내용"
label="스팸문자"
def __init__(self, verbose: bool = False):
self.system_prompt = SYSTEM_PROMPT
def generate_chat_prompt(
self,
instruction: str,
label: Union[None, str] = None,
) -> list:
"""EXAONE 모델의 chat_template 형식에 맞게 프롬프트 생성"""
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": instruction}, # 내담자 발화를 직접 전달
]
if label:
messages.append({"role": "assistant", "content": label})
return messages
messages = [
{"role": "system", "content": "스팸인지 아닌지 구분하세요"},
{"role": "user", "content": "문자1내용"},
{"role": "assistant", "content": "스팸문자"},
]
3-2. 토크나이징
transformers라이브러리는 친절하게 토크나이저안에 apply_chat_template를 넣어놔 주었다. 이게 어떻게 작동하는지만 파악하면 되겠다.
from transformers import AutoTokenizer
# EXAONE이나 LLaMA2 같은 모델에 맞는 tokenizer를 불러와야 함
tokenizer = AutoTokenizer.from_pretrained("LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct")
# 2. 챗템플릿 토크나이질 ['<s>', '[SYSTEM]', '당신은'...</s>,'[USER]',..] >> [1, 32000, 2456, 9321, 2, 32001,...]
tokenized_chat = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors=None,
padding=False,
truncation=True,
max_length=CUTOFF_LEN,
)
tokenizer.apply_chat_template을 거치면서, 아래와 같이 토크나이징 된다
messages = [
{"role": "system", "content": "스팸인지 아닌지 구분하세요"},
{"role": "user", "content": "문자1내용"},
{"role": "assistant", "content": "스팸문자"},
]
EXAONE의 고유한 포맷(학습할때 보니, 터미널에 약간 다르게 출력되는것 같음....
<s>[SYSTEM]스팸인지 아닌지 구분하세요</s>
<s>[USER]문자1내용</s>
<s>[ASSISTANT]스팸문자</s>
위 내용이 토크나이징 되어서
[1, 32000, 2456, 9321, 2, 32001, 8723, 9832, 1234, 2, 32002, 4321, 5678, 8765, 2,.....]
이걸 해석해보면 <s>는 EXAONE사전에서 1번, [SYSTEM]은 32000번 이런 형태로 맵핑이 되는 것이다.
막상 까보니 별게 없다. 그리고 어렵지 않다.
이제 다 이해 되었으니, 마저 이해해보자
3-3. 라벨 마스킹
def generate_and_tokenize_chat_prompt(data_point, prompter, tokenizer):
"""EXAONE 모델의 chat_template 형식에 맞게 프롬프트 생성 및 토큰화"""
# 데이터는 이미 유효한 카테고리로만 구성되어 있음
label = data_point["스팸여부"]
# 1. 채팅 템플릿으로 messages 생성
messages = prompter.generate_chat_prompt(data_point["내담자"], label=label)
# 2. 챗템플릿 토크나이질 ['<s>', '[SYSTEM]', '당신은'...</s>,'[USER]',..] >> [1, 32000, 2456, 9321, 2, 32001,...]
tokenized_chat = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors=None,
padding=False,
truncation=True,
max_length=CUTOFF_LEN,
)
# 사용자 입력부분도 학습안하면 -100으로 마스킹
if not TRAIN_ON_INPUTS:
# 사용자 입력 부분만 있는 메시지 생성
user_messages = prompter.generate_chat_prompt(data_point["문자내용"])
user_tokenized = tokenizer.apply_chat_template(
user_messages,
tokenize=True,
add_generation_prompt=True,
return_tensors=None,
padding=False,
truncation=True,
max_length=CUTOFF_LEN,
)
user_len = len(user_tokenized)
# 사용자 입력 부분은 -100으로 마스킹
labels = [-100] * user_len + tokenized_chat[user_len:]
return {
"input_ids": tokenized_chat,
"labels": labels,
"attention_mask": [1] * len(tokenized_chat),
}
return {
"input_ids": tokenized_chat,
"labels": tokenized_chat.copy(),
"attention_mask": [1] * len(tokenized_chat),
}
여기서 TRAIN_ON_INPUTS = False 인경우, 모델이 사용자 입력 부분을 학습하지 않고, 어시스턴트의 응답만 학습하도록 -100으로 마스킹
보통은 False로 하고 학습을 시키는데, 풍문으로 들었을때 True로 하여서 사용자 입력부분까지 학습하는 것이 더 성능이 좋다고 한다.
하지만, GPU자원이 부족한 우리같은 연구자들은 False로 하자!
labels = [-100] * len(user_tokenized) + tokenized_chat[len(user_tokenized):]
위코드를 통해서
[-100, -100, -100, -100, -100, -100, -100, -100, 8765, 4321, 2]
[|system|] 스팸인지 아닌지 구분하세요
[|user|] 문자1내용
[|assistant|]
까지는 -100으로 마스킹이 된다.
그리고 8767, 4321, 2 = 스팸문자 만 남는다.
3-4. 모델 학습
input_text="문자1내용"
# Generate prompt and get model inputs
prompt = self.prompter.generate_prompt(input_text)
model_inputs = self.tokenizer(
prompt, return_tensors="pt", truncation=True, max_length=512
).to(self.model.device)
# Get model prediction
self.model.eval()
with torch.no_grad():
outputs = self.model.generate(
input_ids=model_inputs["input_ids"],
attention_mask=model_inputs["attention_mask"],
max_new_tokens=20,
eos_token_id=self.tokenizer.eos_token_id,
pad_token_id=self.tokenizer.pad_token_id,
)
self.model.train()
# Decode prediction
generated_text = outputs[0][model_inputs["input_ids"].size(1):]
pred_text = self.tokenizer.decode(generated_text, skip_special_tokens=True)
위코드를 통해서, 문자1내용만 넣은 입력데이터를 완성한다.
messages = [
{"role": "system", "content": "스팸인지 아닌지 구분하세요"},
{"role": "user", "content": "문자1내용"},
]
라벨이 없는 입력데이터만 입력하고, 모델은 출력을 한다.
모델의 출력값은
system, user, assistant값이 모두 포함되어 출력된다.
그래서 outputs[0]에서 입력된 값의 길이만큼 앞에를 짤라내고, 남은 값을 decode하면
모델이 예측한 "스팸문자" 또는 "정상문자" 또는 "쏼라쏼라"가 나온다.
이 pred_text가 라벨을 예측하도록 파라미터를 조정하며 파인튜닝이 진행된다.
4. 인스트럭션 모델, 챗모델, 나아가 추론모델의 데이터셋 이해
챗모델이 크게 대단한 것이 아닌 것을 확인하였다.
instruction template가 모델에 입력하는 값:
"스팸인지 아닌지 구분하세요:\n\n### sms:\n문자1내용\n\n### label:\n스팸문자"
chat template가 모델에 입력하는 값:
[|system|] 스팸인지 아닌지 구분하세요
[|user|] 문자1내용
[|assistant|] 스팸문자
이라면
추론모델의 reasoning template가 모델에 입력하는 값:
[|system|] 스팸인지 아닌지 구분하세요
[|user|] 문자1내용
[|assistant|] <think>이 메시지는 특정 키워드(광고, 혜택, 당첨)가 포함되어 있으며, 의심스러운 URL이 포함될 가능성이 높습니다.</think> 스팸문자
이렇게 이해할수 있다. 이 역시 <think>는 특정토큰이 될것이다.
'AI & ML > 학습하기' 카테고리의 다른 글
Gemma3 finetuning(파인튜닝)하기 (0) | 2025.03.25 |
---|---|
DeepSeek-R1 정리(공부하기) + Open r1 (0) | 2025.02.04 |
패캠(패스트캠퍼스) "LLM 모델 파인튜닝을 위한 GPU 최적화" 후기 (2) | 2024.12.02 |
파인튜닝을 통한 감정단어 분류기(NER) (3) | 2024.09.12 |
AI Product 개발전략과 개발기 (0) | 2024.06.20 |