GLM-4 명령어 미세 조정 실전 가이드: 데이터 전처리부터 학습까지

HuggingFace TRL을 활용한 GLM-4 명령어 미세 조정

본 가이드는 대규모 언어 모델(LLM)의 명령어 준수(Instruction Following) 미세 조정을 다룹니다. 구현의 편의성과 코드 간결성을 위해 HuggingFaceTRL 프레임워크를 사용합니다. TRL은 SFT(Supervised Fine-Tuning) 외에도 DPO, PPO, GRPO 등 다양한 강화 학습 기반 미세 조정 알고리즘을 지원합니다.

기반 모델로는 GLM-4를 선택했습니다. ChatGLM 모델군은 상대적으로 크기가 커, 실제 실행에는 최소 16GB 이상의 GPU 메모리(VRAM)가 필요합니다.

참고: SwanLab이 HuggingFace Transformers에 공식 통합되었습니다. 로컬 환경에 SwanLab이 설치되어 있으면 자동으로 활성화되며, report_to="swanlab" 인자를 통해 훈련 추적을 시작할 수 있습니다.

핵심 참고 자료:

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와 유사하지만, SFTConfigSFTTrainer라는 두 가지 추가 모듈을 사용합니다. 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-9bTHUDM/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 데이터셋은 instructioninput 필드를 분리합니다. 이는 초기 명령어 준수 연구에서 비롯된 관례로, 모델을 번역, 계산 등 일반적인 태스크를 처리할 수 있도록 설계하기 위함이었습니다. 그러나 현재는 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를 사용하면 훈련 속도를 크게 향상시킬 수 있습니다. 먼저 acceleratedeepspeed를 설치합니다.

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

태그: GLM-4 TRL HuggingFace LoRA SFT

6월 24일 22:12에 게시됨