HuggingFace TRL을 활용한 GLM-4 명령어 미세 조정
본 가이드는 대규모 언어 모델(LLM)의 명령어 준수(Instruction Following) 미세 조정을 다룹니다. 구현의 편의성과 코드 간결성을 위해 HuggingFace의 TRL 프레임워크를 사용합니다. TRL은 SFT(Supervised Fine-Tuning) 외에도 DPO, PPO, GRPO 등 다양한 강화 학습 기반 미세 조정 알고리즘을 지원합니다.
기반 모델로는 GLM-4를 선택했습니다. ChatGLM 모델군은 상대적으로 크기가 커, 실제 실행에는 최소 16GB 이상의 GPU 메모리(VRAM)가 필요합니다.
참고: SwanLab이 HuggingFace Transformers에 공식 통합되었습니다. 로컬 환경에 SwanLab이 설치되어 있으면 자동으로 활성화되며,
report_to="swanlab"인자를 통해 훈련 추적을 시작할 수 있습니다.
핵심 참고 자료:
- 智谱AI 공식 웹사이트: https://www.zhipuai.cn/
- ChatGLM-9B 기본 모델: https://huggingface.co/THUDM/glm-4-9b-hf
- ChatGLM-9B-Chat 모델: https://huggingface.co/THUDM/glm-4-9b-chat-hf
- Alpaca 데이터셋 (중국어): https://huggingface.co/datasets/llamafactory/alpaca_gpt4_zh
- 본 가이드의 오픈소스 프로젝트: https://github.com/SwanHubX/glm4-finetune
- SwanLab 훈련 로그 확인: https://swanlab.cn/@ShaohonChen/chatglm-finetune
TRL 패키지 소개 및 환경 설정
이 튜토리얼은 HuggingFace TRL 프레임워크를 사용하여 미세 조정 코드를 구현합니다. TRL은 강력하고 사용하기 쉬운 프레임워크로, SFT 지원 외에도 DPO, PPO, GRPO와 같은 강화 학습 알고리즘을 손쉽게 호출할 수 있으며 Transformers 아키텍처와 완벽하게 호환됩니다.
필요한 패키지를 설치합니다.
pip install transformers trl datasets peft swanlab
transformers,trl,peft: 모델 로딩 및 훈련datasets: 데이터셋 임포트swanlab: 훈련 과정 시각화 및 추적
다음은 TRL 프레임워크 사용법을 보여주는 간단한 예제 코드입니다.
from datasets import load_dataset
from trl import SFTConfig, SFTTrainer
# 미세 조정 데이터셋 로드 (IMDB 영화 리뷰 분류 데이터 사용)
dataset = load_dataset("stanfordnlp/imdb", split="train")
# 미세 조정 파라미터 설정
training_args = SFTConfig(
max_length=512,
output_dir="/tmp",
)
# 모델 및 트레이너 설정 (예시: 작은 모델 사용)
trainer = SFTTrainer(
"facebook/opt-350m",
train_dataset=dataset,
args=training_args,
)
# 훈련 시작
trainer.train()
TRL 패키지 사용법은 기본적으로 Transformers와 유사하지만, SFTConfig와 SFTTrainer라는 두 가지 추가 모듈을 사용합니다. SFTConfig는 Transformers의 TrainingArguments를 기반으로 SFT에 특화된 파라미터와 LoRA 지원 파라미터를 추가합니다. SFTTrainer는 SFT 코드 구현체이며, PEFT의 LoRA 지원 및 데이터셋 형식 변환 기능을 포함합니다.
GLM-4 모델 소개 및 준비
GLM-4-9B는 智谱AI(Zhipu AI)의 최신 GLM-4 시리즈 중 오픈소스 모델입니다. 기본 모델인 GLM-4-9B와 미세 조정된 대화 모델인 GLM-4-9B-Chat이 있으며, 후자는 웹 브라우징, 코드 실행, 함수 호출(Function Call), 최대 128K 컨텍스트의 장문 추론 등을 지원합니다.
이 튜토리얼은 명령어 준수 기능 미세 조정을 위해 GLM-4-9B 모델을 사용하고, 결과 추적을 위해 SwanLab을 사용합니다.
참고: ChatGLM은 HuggingFace Transformers 업데이트에 맞춰
THUDM/glm-4-9b와THUDM/glm-4-9b-hf두 가지 버전의 가중치를 공개했습니다. 후자는 최신 Transformers 버전에 해당하므로, 본 튜토리얼에서는THUDM/glm-4-9b-hf가중치를 사용합니다.
제공된 스크립트를 사용하여 모델을 다운로드합니다.
huggingface-cli download --local-dir ./weights/glm-4-9b-hf THUDM/glm-4-9b-hf
모델은 프로젝트 디렉토리 내 ./weights/glm-4-9b-hf에 저장됩니다.
Transformers를 사용하여 GLM-4 기본 모델을 로드하고 추론하는 예제입니다.
from transformers import AutoTokenizer, AutoModelForCausalLM
device = "cuda"
tokenizer = AutoTokenizer.from_pretrained("THUDM/glm-4-9b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("THUDM/glm-4-9b-chat-hf").eval().to(device)
inputs = tokenizer.encode("나는 ChatGLM입니다. 저는", return_tensors="pt").to(device)
outputs = model.generate(inputs)
print(tokenizer.decode(outputs[0]))
기본 모델은 간단한 텍스트 완성만 수행하므로, 위 코드는 "나는 ChatGLM입니다. 저는" 다음에 오는 내용을 확률적으로 생성합니다.
대화 기능을 사용하려면 미세 조정된 대화 모델을 로드해야 합니다.
from transformers import pipeline
messages = [
{"role": "user", "content": "당신은 누구인가요?"},
]
pipe = pipeline("text-generation", model="THUDM/glm-4-9b-chat-hf")
print(pipe(messages))
print(model) 명령으로 출력되는 모델 구조는 다음과 같습니다.
GlmForCausalLM(
(model): GlmModel(
(embed_tokens): Embedding(151552, 4096, padding_idx=151329)
(layers): ModuleList(
(0-39): 40 x GlmDecoderLayer(
(self_attn): GlmAttention(
(q_proj): Linear(in_features=4096, out_features=4096, bias=True)
(k_proj): Linear(in_features=4096, out_features=256, bias=True)
(v_proj): Linear(in_features=4096, out_features=256, bias=True)
(o_proj): Linear(in_features=4096, out_features=4096, bias=False)
)
(mlp): GlmMLP(
(gate_up_proj): Linear(in_features=4096, out_features=27392, bias=False)
(down_proj): Linear(in_features=13696, out_features=4096, bias=False)
(activation_fn): SiLU()
)
(input_layernorm): GlmRMSNorm((4096,), eps=1.5625e-07)
(post_attention_layernorm): GlmRMSNorm((4096,), eps=1.5625e-07)
)
)
(norm): GlmRMSNorm((4096,), eps=1.5625e-07)
(rotary_emb): GlmRotaryEmbedding()
)
(lm_head): Linear(in_features=4096, out_features=151552, bias=False)
)
GLM-4는 40개의 디코더 레이어로 구성되어 있어, LoRA 미세 조정 시 다른 모델에 비해 학습 가능한 파라미터 수가 다소 많습니다.
데이터셋 준비
데이터셋은 GitHub 프로젝트에 포함되어 있습니다. 다음 명령어로 전체 실험 코드를 다운로드할 수 있습니다.
git clone https://github.com/SwanHubX/glm4-finetune.git
데이터셋만 다운로드하려면 아래 명령어를 사용합니다.
wget https://github.com/SwanHubX/glm4-finetune/blob/main/data/alpaca_gpt4_data_zh.json
HuggingFace에서도 다운로드 가능합니다: llamafactory/alpaca_gpt4_zh
코드 설명 및 하이퍼파라미터 조정
전체 미세 조정 코드는 GitHub에 공개되어 있습니다.
git clone https://github.com/SwanHubX/glm4-finetune.git
주요 코드 모듈을 설명합니다.
모델 하이퍼파라미터 설정
LoRA 파라미터 설정에 주목하십시오. 이 튜토리얼의 LoRA 파라미터는 ChatGLM 공식 미세 조정 코드를 참고했습니다. 학습률은 5e-4이며, 전체 파라미터 미세 조정 시에는 이보다 한 단계 낮은 값을 사용해야 합니다.
################# Model kwargs
################
@dataclass
class ChatGLM4ModelConfig(ModelConfig):
model_name_or_path: Optional[str] = field(
default="./weights/glm-4-9b-hf",
metadata={
"help": "Model checkpoint for weights initialization. default used glm4"
},
)
torch_dtype: Optional[str] = field(
default="bfloat16",
metadata={
"help": "Override the default `torch.dtype` and load the model under this dtype.",
"choices": ["auto", "bfloat16", "float16", "float32"],
},
)
use_peft: bool = field(
default=True,
metadata={"help": "Whether to use PEFT for training. Default true"},
)
lora_r: int = field(
default=8,
metadata={"help": "LoRA R value."},
)
lora_alpha: int = field(
default=32,
metadata={"help": "LoRA alpha."},
)
lora_dropout: float = field(
default=0.1,
metadata={"help": "LoRA dropout."},
)
lora_target_modules: Optional[list[str]] = field(
default_factory=lambda: ["q_proj", "k_proj", "v_proj"],
metadata={"help": "LoRA target modules."},
)
데이터셋 하이퍼파라미터 설정
간단히 로컬 데이터셋 파일 경로를 지정합니다.
################# Datasets kwargs
################
@dataclass
class DataTrainingArguments:
data_files: Optional[str] = field(
default="./data/alpaca_gpt4_data_zh.json",
metadata={"help": "The name of the dataset to use (via the datasets library)."},
)
데이터셋의 구조를 확인하는 스크립트입니다.
import datasets
raw_dataset = datasets.load_dataset("json", data_files="data/alpaca_gpt4_data_zh.json")
print(raw_dataset)
# DatasetDict({
# train: Dataset({
# features: ['instruction', 'input', 'output'],
# num_rows: 42677
# })
# })
첫 번째 데이터 항목을 출력합니다.
print(raw_dataset["train"][0])
# {
# "instruction": "건강을 유지하기 위한 세 가지 팁.",
# "input": "",
# "output": "건강을 유지하기 위한 세 가지 팁은 다음과 같습니다:\n\n1. 신체 활동 유지...\n2. 균형 잡힌 식단...\n3. 충분한 수면..."
# }
Alpaca 데이터셋은 instruction과 input 필드를 분리합니다. 이는 초기 명령어 준수 연구에서 비롯된 관례로, 모델을 번역, 계산 등 일반적인 태스크를 처리할 수 있도록 설계하기 위함이었습니다. 그러나 현재는 ChatGPT와 같은 AI 어시스턴트의 등장으로 이러한 분리가 필수적이지 않게 되었습니다. 데이터 형식만 다를 뿐 모델의 핵심 동작(이전 텍스트를 기반으로 다음 텍스트 생성)에는 영향을 미치지 않습니다. 본 튜토리얼에서는 이 데이터를 GLM-4의 권장 Chat 형식에 맞게 변환하여 처리합니다.
GLM-4가 권장하는 입력 미세 조정 데이터 구조는 다음과 같습니다.
{
"messages": [
{
"role": "user",
"content": "유형#바지*소재#청바지*스타일#섹시"
},
{
"role": "assistant",
"content": "3x1의 이 청바지는..."
}
]
}
GLM-4의 네이티브 chat_template을 사용하여 데이터를 변환합니다. 이렇게 하면 GLM-4의 다양한 도구와 호환성을 유지하고 특수 토큰(special token)을 최대한 활용할 수 있습니다.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("THUDM/glm-4-9b-chat-hf")
print(tokenizer.chat_template)
데이터 변환 함수입니다.
def formatting_func(example):
"""
데이터 형식 처리
"""
prompt = example["instruction"]
if len(example["input"]) != 0:
prompt += "\n\n" + example["input"]
conversations = [
{"role": "user", "content": prompt},
{"role": "assistant", "content": example["output"]},
]
output_text = tokenizer.apply_chat_template(
conversation=conversations, tokenize=False
)
return output_text
변환된 데이터 샘플입니다.
[gMASK]<sop><|user|>건강을 유지하기 위한 세 가지 팁.<|assistant|>건강을 유지하기 위한 세 가지 팁은 다음과 같습니다:
1. 신체 활동 유지...
2. 균형 잡힌 식단...
3. 충분한 수면...
훈련 하이퍼파라미터 및 프로세스
데이터 규모가 작으므로 600 스텝 동안 훈련하며, GPU당 실제 배치 크기는 1 * 4입니다.
################# Train kwargs
################
@dataclass
class MySFTConfig(SFTConfig):
output_dir: Optional[str] = field(
default="./output/lora-glm4-9b-alpaca",
metadata={
"help": "The output directory where the model predictions and checkpoints will be written. Defaults to 'lora-glm4-9b-toolcall' if not provided."
},
)
num_train_epochs: float = field(
default=3.0, metadata={"help": "Total number of training epochs to perform."}
)
per_device_train_batch_size: int = field(
default=2,
metadata={"help": "Batch size per GPU/TPU/MPS/NPU core/CPU for training."},
)
per_device_eval_batch_size: int = field(
default=4,
metadata={"help": "Batch size per GPU/TPU/MPS/NPU core/CPU for evaluation."},
)
gradient_accumulation_steps: int = field(
default=1,
metadata={
"help": "Number of updates steps to accumulate before performing a backward/update pass."
},
)
learning_rate: float = field(
default=5e-4, metadata={"help": "The initial learning rate for AdamW."}
)
bf16: bool = field(
default=True,
metadata={
"help": "Whether to use bf16 (mixed) precision instead of 32-bit. Requires Ampere or higher NVIDIA architecture or using CPU (use_cpu) or Ascend NPU."
},
)
bf16_full_eval: bool = field(
default=True,
metadata={
"help": "Whether to use full bfloat16 evaluation instead of 32-bit."
},
)
max_seq_length: Optional[int] = field(
default=512,
metadata={
"help": "Maximum length of the tokenized sequence."
},
)
eval_strategy: Union[str] = field(
default="steps",
metadata={"help": "The evaluation strategy to use."},
)
eval_steps: Optional[float] = field(
default=0.1,
metadata={
"help": "Run an evaluation every X steps."
},
)
logging_steps: float = field(
default=10,
metadata={
"help": "Log every X updates steps."
},
)
save_steps: float = field(
default=0.1,
metadata={
"help": "Save checkpoint every X updates steps."
},
)
TRL을 사용한 훈련 프로세스는 매우 간결합니다.
################# Training
################
trainer = SFTTrainer(
model=model_args.model_name_or_path,
args=training_args,
data_collator=None,
train_dataset=raw_datasets["train"],
eval_dataset=(
raw_datasets["test"] if training_args.eval_strategy != "no" else None
),
processing_class=tokenizer,
peft_config=get_peft_config(model_args),
formatting_func=formatting_func,
callbacks=[SavePredictCallback()],
)
trainer.train()
훈련 시작 및 성능 평가
훈련 시 기본적으로 SwanLab이 활성화됩니다. report_to="swanlab" 인자로 훈련 추적을 시작하거나, 로컬에 SwanLab이 설치된 경우 자동으로 시작됩니다.
훈련 명령어는 다음과 같습니다.
python instruct_train.py
SwanLab 로그인이 필요할 수 있습니다. swanlab login 명령어로 로그인한 후 출력된 링크를 통해 대시보드에서 훈련 로그를 확인할 수 있습니다.
콜백(Callback)을 통해 모델의 예측 출력을 자동으로 기록할 수 있습니다.
################# Print prediction text callback
################
class SavePredictCallback(TrainerCallback):
def on_save(self, args, state, control, model, processing_class, **kwargs):
if state.is_world_process_zero:
tokenizer = processing_class
batch_test_message = [
[{"role": "user", "content": "안녕하세요, 당신의 이름을 알려주세요."}],
[{"role": "user", "content": "1+2는 얼마인가요?"}],
]
batch_inputs_text = tokenizer.apply_chat_template(
batch_test_message,
return_tensors="pt",
return_dict=True,
padding=True,
padding_side="left",
add_generation_prompt=True,
).to(model.device)
outputs = model.generate(**batch_inputs_text, max_new_tokens=512)
batch_reponse = tokenizer.batch_decode(
outputs, skip_special_tokens=False
)
log_text_list = [swanlab.Text(response) for response in batch_reponse]
swanlab.log({"Prediction": log_text_list}, step=state.global_step)
멀티 GPU 훈련
멀티 GPU를 사용하면 훈련 속도를 크게 향상시킬 수 있습니다. 먼저 accelerate와 deepspeed를 설치합니다.
pip install accelerate deepspeed
다음 명령어로 멀티 GPU 훈련을 시작합니다. (num_processes를 실제 GPU 개수로 변경)
accelerate launch --num_processes 8 --config_file configs/zero2.yaml instruct_train.py
Zero2에 대한 자세한 설정은 configs/zero2.yaml 파일에 있습니다. 모델 가중치는 output/lora-glm4-9b-alpaca에 저장되며, LoRA 가중치만 저장되므로 추론 시 원본 모델을 함께 로드해야 합니다.
추론 및 성능 비교
다음 명령어로 명령행 기반 채팅을 실행할 수 있습니다.
bash chat_cli.py