Model fine tuning

Author

차상진

Published

April 15, 2025

1. Install

아래 코드를 실행하고 런타임 재시작

# pip install datasets==2.20.0 transformers==4.41.2 peft==0.10.0 evaluate==0.4.2 scikit-learn==1.4.2 accelerate -U

이전 chapter에서는 전체적으로 미세조정을 하지 않고 모델과 각 태스크의 대략적인 구조만 학습했다.

아무래도 학습을 하지 않고 바로 predict를 하니 결과가 만족스럽지 않았다.

이번 chapter에서는 미세조정을 해보는 코드를 배워볼 것이다.

2. Encoder Sequence Classification

2-1 Model

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)
/root/anaconda3/envs/asdf/lib/python3.9/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
  warnings.warn(
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at klue/bert-base and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.

2-2. Dataset

from datasets import load_dataset

dataset = load_dataset("klue", "sts")

def process_data(batch):
  result = tokenizer(
      batch["sentence1"],
      text_pair=batch["sentence2"],
      max_length=128,
      padding="max_length",
      truncation=True,
      return_tensors="np",
  )
  result["labels"] = [x["binary-label"] for x in batch["labels"]]
  return result

tokenized_dataset = dataset.map(
    process_data,
    batched=True,
    remove_columns=dataset["train"].column_names,
)
tokenized_dataset["train"].column_names
['labels', 'input_ids', 'token_type_ids', 'attention_mask']

2-3 Train

from transformers import (
    Trainer, # 모델 학습을 위한 훈련 도구
    TrainingArguments, # 학습을 위한 하이퍼파라미터 및 설정을 정의하는 클래스
    default_data_collator, # 콜레이터
    EarlyStoppingCallback # early stop 함수
)
import evaluate 


def custom_metrics(pred): # micro f1 score 평가 지표를 로드하는 함수
  f1 = evaluate.load("f1")
  labels = pred.label_ids
  preds = pred.predictions.argmax(-1)

  return f1.compute(predictions=preds, references=labels, average="micro")

training_args = TrainingArguments( # 학습 argument 설정
    per_device_train_batch_size=64, # 학습할 때 배치 크기
    per_device_eval_batch_size=64, # 평가할 때 배치 크기
    learning_rate=5e-6,
    max_grad_norm=1, # 그래디언트 클리핑 (그래디언트 폭발 방지)
    num_train_epochs=10, 
    evaluation_strategy="steps", # 일정 step마다 검증 실행
    logging_strategy="steps", # 일정 스텝마다 로그 저장
    logging_steps=100, # 100 step마다 로그 출력
    logging_dir="data/logs", # 로그 저장 경로 (현재 실행중인 폴더를 기준으로 저장되므로 현재 폴더를 잘 확인해야함)
    save_strategy="steps", # 일정 step마다 체크포인트 저장
    save_steps=100, # 100 step마다 체크포인트 저장
    output_dir="data/ckpt", # 로그 저장 경로 (현재 실행중인 폴더를 기준으로 저장되므로 현재 폴더를 잘 확인해야함)
    load_best_model_at_end = True, # 학습 종료 후 가장 좋은 모델 로드
    report_to='tensorboard', # tensorboard에 학습 로그 기록
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset["train"],
    eval_dataset=tokenized_dataset["validation"],
    tokenizer=tokenizer,
    data_collator=default_data_collator,
    compute_metrics=custom_metrics,
    callbacks = [EarlyStoppingCallback(early_stopping_patience=2)] # 2번 연속으로 검증 성능이 향상되지 않으면 학습 중단
)

trainer.train()
/root/anaconda3/envs/asdf/lib/python3.9/site-packages/transformers/training_args.py:1474: FutureWarning: `evaluation_strategy` is deprecated and will be removed in version 4.46 of 🤗 Transformers. Use `eval_strategy` instead
  warnings.warn(
huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...
To disable this warning, you can either:
    - Avoid using `tokenizers` before the fork if possible
    - Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)
Using the latest cached version of the module from /root/.cache/huggingface/modules/evaluate_modules/metrics/evaluate-metric--f1/0ca73f6cf92ef5a268320c697f7b940d1030f8471714bffdb6856c641b818974 (last modified on Tue Apr  1 08:33:18 2025) since it couldn't be found locally at evaluate-metric--f1, or remotely on the Hugging Face Hub.
Using the latest cached version of the module from /root/.cache/huggingface/modules/evaluate_modules/metrics/evaluate-metric--f1/0ca73f6cf92ef5a268320c697f7b940d1030f8471714bffdb6856c641b818974 (last modified on Tue Apr  1 08:33:18 2025) since it couldn't be found locally at evaluate-metric--f1, or remotely on the Hugging Face Hub.
Using the latest cached version of the module from /root/.cache/huggingface/modules/evaluate_modules/metrics/evaluate-metric--f1/0ca73f6cf92ef5a268320c697f7b940d1030f8471714bffdb6856c641b818974 (last modified on Tue Apr  1 08:33:18 2025) since it couldn't be found locally at evaluate-metric--f1, or remotely on the Hugging Face Hub.
Using the latest cached version of the module from /root/.cache/huggingface/modules/evaluate_modules/metrics/evaluate-metric--f1/0ca73f6cf92ef5a268320c697f7b940d1030f8471714bffdb6856c641b818974 (last modified on Tue Apr  1 08:33:18 2025) since it couldn't be found locally at evaluate-metric--f1, or remotely on the Hugging Face Hub.
[ 400/1830 04:27 < 16:01, 1.49 it/s, Epoch 2/10]
Step Training Loss Validation Loss F1
100 0.045700 0.930365 0.786127
200 0.083700 0.718717 0.805395
300 0.071400 0.800480 0.786127
400 0.060000 0.828407 0.799615

TrainOutput(global_step=400, training_loss=0.06518504738807679, metrics={'train_runtime': 268.0555, 'train_samples_per_second': 435.283, 'train_steps_per_second': 6.827, 'total_flos': 1678122311086080.0, 'train_loss': 0.06518504738807679, 'epoch': 2.185792349726776})

2-4. Predict

미세조정으로 400 step에서 체크포인트로 저장한 경로에서 모델과 토크나이저를 불러와 검증 게이터 중 10개 샘플을 입력해 간단한 추론을 진행한다.

import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    BertForSequenceClassification,
    DataCollatorWithPadding
)

# tokenizer, model
model_name = "data/ckpt/checkpoint-400"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(model_name)

collator = DataCollatorWithPadding(tokenizer) # 콜레이터 설정
batch = collator([tokenized_dataset["validation"][i] for i in range(10)])

# inference
with torch.no_grad():
  logits = model(**batch).logits
logits
tensor([[-3.8785,  3.4420],
        [ 3.2443, -2.9451],
        [ 1.9444, -1.9935],
        [-3.5267,  3.0033],
        [ 0.2386, -0.7375],
        [ 3.3531, -2.7962],
        [-3.2954,  2.9353],
        [ 3.8899, -3.2844],
        [ 4.2035, -3.6925],
        [ 4.1651, -3.5504]])

2-5. Evaluate

import evaluate

pred_labels = logits.argmax(dim=1).cpu().numpy()
true_labels = batch["labels"].numpy()

f1 = evaluate.load("f1")
f1.compute(predictions=pred_labels, references=true_labels, average="micro")
Using the latest cached version of the module from /root/.cache/huggingface/modules/evaluate_modules/metrics/evaluate-metric--f1/0ca73f6cf92ef5a268320c697f7b940d1030f8471714bffdb6856c641b818974 (last modified on Tue Apr  1 08:33:18 2025) since it couldn't be found locally at evaluate-metric--f1, or remotely on the Hugging Face Hub.
{'f1': 1.0}

‘klue/bert-base’ 모델을 불러와서 AutoModelForSequenceClassification 헤드를 붙혔다.

그 후 학습을 진행하였고 학습된 모델을 불러와서 예측을 하였으므로 미세조정(fine tuning)이다.

3. Decoder Causal LM

3-1. model

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM


model_name = "skt/kogpt2-base-v2"
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
    bos_token="</s>",
    eos_token="</s>",
    unk_token="<unk>",
    pad_token="<pad>",
    mask_token="<mask>"
)
model = AutoModelForCausalLM.from_pretrained(model_name)

3-2. Dataset

from datasets import load_dataset

# 데이터셋 구조화
split_dict = {
    "train": "train[:8000]",
    "test": "train[8000:10000]",
    "unused": "train[10000:]",
}
dataset = load_dataset("heegyu/kowikitext", split=split_dict)
del dataset["unused"] # 사용하지 않는 unused 데이터를 삭제

# 토큰화
tokenized_dataset = dataset.map(
    lambda batch: tokenizer([f"{ti}\n{te}" for ti, te in zip(batch["title"], batch["text"])]),
    batched=True, # 배치로 묶어서 처리
    num_proc=2, # 2개 프로세스 병렬 실행
    remove_columns=dataset["train"].column_names, # 기존의 'title','text' 컬럼을 삭제 후 토큰화 결과만 남김
)

# 최대 길이로 그룹화
max_length = 512 
def group_texts(batched_sample):
    sample = {k: v[0] for k, v in batched_sample.items()} # 데이터셋에서 각 key의 첫 번째 값만 가져오기.

    if sample["input_ids"][-1] != tokenizer.eos_token_id: # 마지막 토큰이 <eos>가 아니라면 <eos> 추가.
        for k in sample.keys():
            sample[k].append(
                tokenizer.eos_token_id if k == "input_ids" else sample[k][-1]
                # sample['input_ids']라면 <eos> 토큰을 추가. / sample['input_ids']가 아니라면 기존 값 유지.(sample[k][-1])
            )

    result = {
        k: [v[i: i + max_length] for i in range(0, len(v), max_length)] # 문장이 길면 여러 개의 샘플로 분할
        for k, v in sample.items()
    }
    return result

grouped_dataset = tokenized_dataset.map(
    group_texts,
    batched=True,
    batch_size=1,
    num_proc=2,
)
grouped_dataset["train"].column_names
['input_ids', 'attention_mask']

3-3. Fine tuning

from transformers import Trainer, TrainingArguments, DataCollatorForLanguageModeling

training_args = TrainingArguments(
    per_device_train_batch_size=4,
    per_device_eval_batch_size=4,
    learning_rate=5e-6,
    max_grad_norm=1,
    num_train_epochs=3,
    evaluation_strategy="steps",
    logging_strategy="steps",
    logging_steps=500,
    logging_dir="data/logs",
    output_dir="data/ckpt",
    report_to="tensorboard",
)

collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=grouped_dataset["train"],
    eval_dataset=grouped_dataset["test"],
    tokenizer=tokenizer,
    data_collator=collator,
)

trainer.train()
trainer.save_model("data/model")
/root/anaconda3/envs/asdf/lib/python3.9/site-packages/transformers/training_args.py:1474: FutureWarning: `evaluation_strategy` is deprecated and will be removed in version 4.46 of 🤗 Transformers. Use `eval_strategy` instead
  warnings.warn(
[13776/13776 1:59:37, Epoch 3/3]
Step Training Loss Validation Loss
500 4.284300 4.471501
1000 4.219000 3.416660
1500 4.206900 2.995688
2000 4.185800 2.903164
2500 4.168300 2.876508
3000 4.151000 2.805254
3500 4.151500 2.801232
4000 4.160500 2.774476
4500 4.133300 2.774085
5000 4.085200 2.745091
5500 4.062300 2.747127
6000 4.066100 2.767898
6500 4.037000 2.726495
7000 4.048100 2.735216
7500 4.060500 2.741515
8000 4.031300 2.703305
8500 4.050100 2.725095
9000 4.062700 2.712202
9500 4.020300 2.688988
10000 3.996800 2.699451
10500 4.008100 2.701477
11000 3.989600 2.698209
11500 3.984000 2.686813
12000 4.007600 2.688273
12500 3.992500 2.678961
13000 3.982600 2.687267
13500 4.009200 2.686797

3-4. Predict by before fine tuning model

import torch
from transformers import AutoTokenizer, GPT2LMHeadModel

# 미세조정 이전
origin_name = "skt/kogpt2-base-v2"
origin_tokenizer = AutoTokenizer.from_pretrained(
    origin_name,
    bos_token="</s>",
    eos_token="</s>",
    unk_token="<unk>",
    pad_token="<pad>",
    mask_token="<mask>"
)
origin_model = GPT2LMHeadModel.from_pretrained(origin_name)

inputs1 = origin_tokenizer(
    "우리는 누구나 희망을 가지고",
    return_tensors="pt"
).to(origin_model.device)
outputs1 = origin_model.generate(inputs1.input_ids, max_length=128, repetition_penalty=2.0)
result1 = origin_tokenizer.batch_decode(outputs1, skip_special_tokens=True)
print(result1[0])
/root/anaconda3/envs/asdf/lib/python3.9/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
  warnings.warn(
우리는 누구나 희망을 가지고 살아갈 수 있는 사회를 만들어야 한다"고 강조했다.
이날 행사에는 박근혜 대통령, 황우여 새누리당 대표 등 여권 지도부와 김무성 전 대표가 참석해 축사를 했다.
김영삼 정부 시절인 지난 2007년 대선 당시 이명박 후보의 당선을 위해 '국민통합21'을 이끌었던 이 후보는 "우리나라에서 가장 큰 문제는 경제"라며 "이명박은 경제를 살리고 서민을 위한 정치를 하겠다고 약속했지만 현실은 그렇지 못했다"며 이같이 말했다.
그는 이어 "나는 지금 대한민국을 걱정하고 있다, 경제가 어렵다면 우리 모두 힘을 모아 위기를 극복해야 한다고 생각한다

3-5. Predict by after fine tuning model

# 미세조정 이후
finetuned_name = "data/model"
finetuned_tokenizer = AutoTokenizer.from_pretrained(finetuned_name)
finetuned_model = GPT2LMHeadModel.from_pretrained(finetuned_name)

inputs2 = finetuned_tokenizer(
    "우리는 누구나 희망을 가지고",
    return_tensors="pt").to(finetuned_model.device)
outputs2 = finetuned_model.generate(
    inputs2.input_ids,
    max_length=128,
    repetition_penalty=2.0
)
result2 = finetuned_tokenizer.batch_decode(outputs2, skip_special_tokens=True)
print(result2[0])
우리는 누구나 희망을 가지고 살아갈 수 있는 사회를 만들자는 것이었다. 그러나 그 희망은 결국 좌절되고 말았다. 이 절망적인 상황 속에서, 사람들은 자신들의 삶을 포기하고 다른 사람들의 삶으로 돌아가고자 하였다. 이러한 상황에서 그들은 자신의 삶에 대한 책임을 회피하고 자기 자신을 희생하는 선택을 하게 되었다. 그리하여 많은 사람들이 자살을 선택하게 되었고, 이는 곧 자살로 이어지게 되었다.
이러한 상황에 대해서, 현대 사회는 개인의 존엄성을 존중하지 않는 사회라고 비판하였다. 또한 개인들이 스스로 목숨을 끊는 것을 막기 위해 노력하기도 했다. 하지만 그러한 극단적인 행동들은 오히려 사람들을 죽음으로 내몰게 만드는 결과를 가져왔다. 예를 들어, 한 개인이 사망할 경우 가족들의 동의 없이 강제로 죽음을

4. Encoder-Decoder Conditional Generation

어떤 문장이 주어졌을 때, 입력한 문장을 기반으로 새로운 문장을 작성하는 조건부 생성 태스크로 미세조정한다.

번역 Task

4-1. model

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

model_name = 'hyunwoongko/kobart'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
You passed along `num_labels=3` with an incompatible id to label map: {'0': 'NEGATIVE', '1': 'POSITIVE'}. The number of labels wil be overwritten to 2.

4-2. Dataset

번역 모델에서는 text_target이라는 인자를 사용하고 한국어를 입력했을 때 영어를 출력으로 생성하는 목적으로 text_target = batch['english']를 사용한다

from datasets import load_dataset

dataset = load_dataset('msarmi9/korean-english-multitarget-ted-talks-task')
tokenized_dataset = dataset.map(
    lambda batch: (tokenizer(batch['korean'], text_target = batch['english'], max_length=128, truncation = True)
                  ),batched = True, batch_size = 1000, num_proc = 2, remove_columns = dataset['train'].column_names)

tokenized_dataset['train'].column_names
['input_ids', 'attention_mask', 'labels']

input_ids : 입력 문장이 토큰화 되어서 사전에 매핑된 숫자로 변환된다.

attention mask: 진짜 입력과 패딩을 구별하도록 도와준다. input_ids의 길이가 모두 같아야 모델이 한 꺼번에 처리할 수 있기에 문장이 짧은 경우엔 뒤에 0 또는 [PAD]를 넣어서 길이를 맞춘다. 이때, 어떤 부분이 실제 문장이고 어떤 부분이 패딩인지 알려주는 게 바로 attention mask이다.

labels: 정답 토큰 시퀀스이다. Seq2Seq 모델에서 모델이 예측해야 할 목표 문장을 의미한다. labels도 길이를 맞추기 위해 padding을 넣을 수 있는데 padding 부분은 Loss 계산에서 무시해야하므로 보통 -100으로 채운다.

labels가 있다는 건 정답이 있다는 의미!

번역 Task에서 정답은 번역하고자 하는 언어로 쓰여진 문장이므로 여기서는 영어 문장이다.

영어 문장은 Dataset에 이미 포함되어있고 학습을 위해 Loss 계산을 할 때 사용된다.

4-3. fine tuning

from transformers import Trainer, TrainingArguments, DataCollatorForSeq2Seq

training_args = TrainingArguments(
    per_device_train_batch_size=32,
    per_device_eval_batch_size=32,
    learning_rate=2e-5,
    max_grad_norm=1,
    num_train_epochs=2,
    evaluation_strategy="steps",
    logging_strategy="steps",
    logging_steps=500,
    logging_dir="data/logs",
    save_strategy="steps",
    save_steps=1000,
    output_dir="data/ckpt",
    report_to="tensorboard",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset["train"],
    eval_dataset=tokenized_dataset["test"],
    tokenizer=tokenizer,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, model=model),
)


trainer.train()
trainer.save_model("data/model")

- GPU 사용할 때 학습 시간은 2시간이 조금 넘기에 학습은 생략했습니다.

4-4. predict

import torch
from transformers import (
    AutoTokenizer,
    AutoModelForSeq2SeqLM,
    DataCollatorForSeq2Seq,
    GenerationConfig
)

model_name = "data/model" # 미세조정한 model
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)

collator = DataCollatorForSeq2Seq(
    tokenizer=tokenizer,
    model=model,
    padding="max_length",
    max_length=512,
)
batch = collator([tokenized_dataset["test"][i] for i in range(2)])

outputs = model.generate(batch["input_ids"], max_length=128, do_sample=False)
result = tokenizer.batch_decode(outputs, skip_special_tokens=True)
origin = tokenizer.batch_decode(batch["input_ids"], skip_special_tokens=True)
print(f"원본 : {origin[0]} -> 영어 : {result[0]}")
print(f"원본 : {origin[1]} -> 영어 : {result[1]}")