본문 바로가기
목차
ML

Hugging Face Trainer Callback과 JSONL 기반 Curve Logger

by ds31x 2026. 6. 4.
728x90
반응형

Hugging Face의 Trainer

  • model 학습,
  • evaluation,
  • checkpoint 저장,
  • logging 등을
  • 하나의 학습 loop 안에서 처리해주는 high-level API임.

기본 Logging 기능

일반적인 경우에는 Trainer가 제공하는 기본 logging 기능만으로도 학습 상태를 확인할 수 있음.

예를 들어 TrainingArguments에서 다음과 같은 방식으로 logging 관련 옵션을 설정 하면
Trainer가 일정 step마다 자동으로 학습 로그를 출력함.

from transformers import TrainingArguments


training_args = TrainingArguments(
    output_dir="./outputs",

    # 몇 step마다 log를 출력할지 설정.
    logging_strategy="steps",
    logging_steps=50,

    # tqdm progress bar 표시 여부.
    disable_tqdm=False,
)

이후 Trainer를 생성하고 학습을 시작하면 다음처럼 console에 log가 출력됨.

from transformers import Trainer


trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
)

trainer.train()

실행 중에는 다음과 같은 형태의 log를 볼 수 있음.

{'loss': 0.6123, 'learning_rate': 1.8e-05, 'epoch': 0.25}
{'loss': 0.4812, 'learning_rate': 1.6e-05, 'epoch': 0.50}
{'loss': 0.3921, 'learning_rate': 1.4e-05, 'epoch': 0.75}
  • loss는 최근 training step들의 평균 loss
  • learning_rate는 현재 optimizer learning rate
  • epoch는 현재 epoch 진행 위치
    • epoch가 소수점인 이유는
    • Trainer가 epoch 단위가 아니라 step 단위로 logging하며,
    • 해당 step이 전체 dataset의 몇 epoch 지점에 해당하는지를 비율로 표시하기 때문임.

evaluation을 함께 설정하면 validation metric도 자동 출력됨.

training_args = TrainingArguments(
    output_dir="./outputs",

    logging_strategy="steps",
    logging_steps=50,

    # evaluation 수행 방식.
    # 최신 버전에서는 eval_strategy 사용.
    evaluation_strategy="steps",
    eval_steps=100,
)

 

이 경우 학습 중 eval_steps마다 evaluation이 수행되고 다음과 같은 결과가 출력될 수 있음.

{'eval_loss': 0.433, 'eval_accuracy': 0.812, 'epoch': 0.60}
  • 기본적으로는 eval_loss가 출력되고,
  • compute_metrics를 Trainer에 넘겨준 경우에는 eval_accuracy, eval_f1 같은 custom metric도 함께 출력될 수 있음.
    • eval_accuracy는 자동으로 항상 나오는 값이 아님.
    • compute_metrics에서 accuracy를 계산해 반환해야 출력됨.

evaluation_strategy는 예전 이름이고,
최신 transformers에서는 eval_strategy 사용을 권장.

 

또한 Trainer는 이러한 log를 내부적으로 trainer.state.log_history에도 저장하며,
JSONL logger는 이 정보를 학습 중 즉시 파일에 append 가능한 형태로 외부 저장하는 방식이라고 볼 수 있음.

사용자가
trainer.train()의 반환값을 직접 사용해야만
log를 얻는 구조는 아님.

 

예를 들어 다음처럼 학습을 수행할 경우:

train_output = trainer.train()
  • train_output에는 최종 training summary 정보가 들어 있음.
  • 반환되는 객체의 실제 class는 transformers.trainer_utils.TrainOutput임.
  • 즉, 단순 dictionary가 아니라 Hugging Face에서 정의한 dataclass 형태의 결과 객체임.

반환된 객체에는 대표적으로 다음과 같은 정보를 포함함:

  • global_step
    • 학습 종료 시점의 optimizer update step 수임.
    • load_best_model_at_end=True를 사용하더라도, 여기의 값은 best model이 저장된 step이 아니라 전체 training loop가 실제로 진행된 최종 step을 의미함.
  • training_loss
    • 전체 학습 과정에서의 평균 training loss
  • metrics
    • 학습 종료 시점에 계산된 metric dictionary

실제로 출력해 보면 다음과 비슷한 형태가 나옴.

print(train_output)

 

예시:

TrainOutput(
    global_step=1500,
    training_loss=0.2841,
    metrics={
        'train_runtime': 523.18,
        'train_samples_per_second': 42.7,
        'train_steps_per_second': 2.86,
        'total_flos': 1.23e+15,
        'train_loss': 0.2841,
        'epoch': 3.0,
    }
)

 

각 값은 다음 의미를 가짐.

  • train_runtime : 전체 학습 시간(초)
  • train_samples_per_second : 초당 처리한 sample 수
  • train_steps_per_second : 초당 수행한 optimizer step 수
  • total_flos : 전체 floating point operation 추정치
  • train_loss : 최종 평균 training loss
  • epoch : 학습 종료 시점의 epoch 위치

 

개별 값은 attribute 형태로 접근 가능함.

print(train_output.global_step)
print(train_output.training_loss)
print(train_output.metrics)

 

또는 metrics dictionary 내부 값만 따로 사용할 수도 있음.

print(train_output.metrics["train_runtime"])
print(train_output.metrics["train_loss"])

trainer의 train()메서드의 반환 객체는
학습 결과를 programmatically 기록하거나,
실험 로그를 저장하거나, 이후 분석 코드와 연결할 때 자주 사용됨.

 

하지만 step 단위의 logging 기록은 보통 반환값보다 trainer.state.log_history에 누적되는 방식으로 관리됨.

print(trainer.state.log_history[:3])

 

예를 들면 다음과 같은 형태임.

[
    {"loss": 0.612, "learning_rate": 1.8e-5, "epoch": 0.25, "step": 50},
    {"loss": 0.481, "learning_rate": 1.6e-5, "epoch": 0.50, "step": 100},
]

 

여기서 epoch가 소수점인 이유는 다음과 같음.

  • step: optimizer update가 몇 번 수행되었는지를 의미함.
  • epoch: 전체 train dataset을 몇 바퀴 돌았는지를 의미함.
  • Hugging Face Trainer는 logging을 step 단위로 수행함.
  • 따라서 epoch가 끝나기 전 중간 step에서도 log가 기록될 수 있음.
  • 예를 들어 전체 epoch의 25% 지점이면 epoch=0.25, 절반 지점이면 epoch=0.5처럼 표시됨.

참고로, callback의 on_log() 와 같은 hook에서 전달받는 logs 역시

  • Trainer가 내부적으로 관리하는 logging 흐름의 일부이며,
    이러한 값들이 trainer.state.log_history에도 함께 저장된다고 이해하면 됨.
history = trainer.state.log_history

print(history[:3])

 

예시:

[
    {
        'loss': 0.6123,
        'learning_rate': 1.8e-05,
        'epoch': 0.25,
        'step': 50,
    },
    {
        'loss': 0.4812,
        'learning_rate': 1.6e-05,
        'epoch': 0.50,
        'step': 100,
    },
]

 

이 값은 이후 pandas.DataFrame으로 변환하여 간단한 learning curve를 그리는 데 사용할 수 있음.

import pandas as pd


df = pd.DataFrame(trainer.state.log_history)
print(df.head())

 

단순히 현재 학습 상태를 모니터링하는 목적이라면 Hugging Face의 기본 logging 기능만으로도 충분한 경우가 많음.

 

하지만 학습을 중간에 중단했다가 checkpoint에서 다시 시작하는 경우,
이전 학습 로그와 재개된 학습 로그를 하나의 learning curve로 이어서 처리하는 경우는 별도의 처리가 필요함.

이때 사용할 수 있는 방식이
TrainerCallback을 이용한 custom logging임.

 

TrainerCallback

  • Trainer의 학습 loop 자체를 새로 작성하지 않고,
  • 학습 중 특정 시점에 사용자가 원하는 동작을 끼워 넣을 수 있게 해주는 hook 구조임.

예를 들어 다음과 같은 시점에 callback method가 자동 호출될 수 있음.

on_train_begin()      # 학습 시작 시점
on_step_begin()       # 하나의 step 시작 시점
on_step_end()         # 하나의 step 종료 시점
on_log()              # logging 발생 시점
on_evaluate()         # evaluation 종료 시점
on_save()             # checkpoint 저장 시점
on_epoch_begin()      # epoch 시작 시점
on_epoch_end()        # epoch 종료 시점
on_train_end()        # 학습 종료 시점

 

중요한 점은 사용자가 이 method들을 직접 호출하지 않는다는 것임.

2023.07.13 - [Python] - [Python] Callback function

 

[Python] Callback function

Callback Functioncallback function란 다음 두가지에 해당하는 function을 의미한다.다른 function의 argument로 전달되어 특정 event가 발생시 호출이 이루어지는 function을 가르킨다 (사용자가 명시적으로 호출하

ds31x.tistory.com

 

Custom TrainerCallback 클래스를 만들려면,

  • 사용자는 TrainerCallback을 상속한 class를 만들고,
  • 필요한 hook method만 override한 뒤, 이를 Trainercallbacks 인자에 넘겨줌.
  • 그러면 Trainer가 학습을 수행하면서 적절한 시점에 해당 callback method를 자동으로 호출하게 됨.

TrainerCallback의 주요 Hook Method

TrainerCallback은 학습 loop의 특정 시점마다 자동 호출되는 여러 hook method를 제공함.

2026.02.23 - [Python] - Hook 이란?

 

Hook 이란?

hook: 특정 S/W나 framework의 실행 중간 과정에 사용자가 만든 코드를 끼워 넣어 실행하게 하는 디자인 패턴임. 참고로, 음악에서의 hook는 노래에서 청취자의 귀를 사로잡아 기억에 남도록 만드는 짧

ds31x.tistory.com

 

사용자는 필요한 method만 override하여 원하는 동작을 추가할 수 있음.

 

대표적인 hook method는 다음과 같음.

def on_init_end(self, args, state, control, **kwargs):
    """Trainer 초기화 완료 시점"""

def on_train_begin(self, args, state, control, **kwargs):
    """전체 학습 시작 시점"""

def on_train_end(self, args, state, control, **kwargs):
    """전체 학습 종료 시점"""

def on_epoch_begin(self, args, state, control, **kwargs):
    """epoch 시작 시점"""

def on_epoch_end(self, args, state, control, **kwargs):
    """epoch 종료 시점"""

def on_step_begin(self, args, state, control, **kwargs):
    """training step 시작 시점"""

def on_step_end(self, args, state, control, **kwargs):
    """training step 종료 시점"""

def on_substep_end(self, args, state, control, **kwargs):
    """gradient accumulation substep 종료 시점"""

def on_log(self, args, state, control, logs=None, **kwargs):
    """logging 발생 시점"""

def on_evaluate(self, args, state, control, metrics=None, **kwargs):
    """evaluation 종료 시점"""

def on_predict(self, args, state, control, metrics=None, **kwargs):
    """prediction 종료 시점"""

def on_save(self, args, state, control, **kwargs):
    """checkpoint 저장 시점"""

def on_prediction_step(self, args, state, control, **kwargs):
    """prediction step 수행 시점"""

 

각 hook은 Trainer 내부 학습 loop의 특정 이벤트와 연결되어 있음.

 

예를 들어 다음과 같은 작업을 수행할 수 있음.

  • on_log()
    • training loss 저장
    • learning rate 기록
    • custom logger 연동
  • on_evaluate()
    • evaluation metric 저장
    • validation 결과 분석
    • early stopping 조건 확인
  • on_save()
    • checkpoint 저장 후 추가 파일 기록
    • 외부 storage 업로드
  • on_train_end()
    • 최종 metric 정리
    • 결과 report 생성

Hook method들은 보통 다음과 같은 형태를 가짐.

def on_log(self, args, state, control, logs=None, **kwargs):
    ...

 

여기서 주요 인자의 의미는 다음과 같음:

  • args
    • TrainingArguments 객체임.
    • batch size, logging strategy, output directory 같은 학습 설정을 담고 있음.
  • state
    • TrainerState 객체임.
    • 현재 global_step, epoch, best_metric 등의 상태 정보를 담고 있음.
  • control
    • TrainerControl 객체임.
    • 저장, 평가, 학습 중단 여부 같은 loop 제어에 사용됨.
  • logs
    • on_log()에서 전달되는 training log 정보임.
    • 예를 들어 loss, learning_rate, grad_norm 등이 포함될 수 있음.
    • 여기서 learning_rate는 일반적으로 scheduler가 현재 optimizer에 적용한 대표 learning rate 값임.
      • AdamW처럼 parameter group마다 서로 다른 learning rate를 사용하는 경우에도, Hugging Face 기본 logging에서는 보통 첫 번째 parameter group 기준의 learning rate 하나만 기록됨.
      • parameter group별 learning rate를 모두 기록하려면 custom callback 내부에서 optimizer.param_groups를 직접 읽어 추가로 저장해야 함.
  • metrics
    • on_evaluate()에서 전달되는 evaluation 결과임.
    • 예를 들어 eval_loss, eval_accuracy, eval_f1 등이 포함될 수 있음.
  • **kwargs
    • Hugging Face 내부 구현에서 추가로 전달하는 값들을 받기 위한 인자임.
    • callback 호환성을 위해 일반적으로 유지함.

 


Hugging Face에서 제공하는 Built-in Callback들

사실 모든 callback을 직접 클래스로 구현해야 하는 것은 아님.

Hugging Face transformers
여러 built-in callback class를 기본 제공함.

 

따라서 단순 logging이나 experiment tracking 정도라면 custom callback 없이 기존 callback을 그대로 사용할 수 있음.

대표적인 built-in callback들은 다음과 같음:

EarlyStoppingCallback

evaluation metric이 더 이상 개선되지 않을 때 학습을 조기에 종료하는 callback임.

예를 들어 validation loss가 일정 횟수 이상 개선되지 않으면 training을 중단할 수 있음.

사용 예시는 다음과 같음.

from transformers import EarlyStoppingCallback

trainer = Trainer(
    model=model,
    args=training_args,
    callbacks=[
        EarlyStoppingCallback(
            early_stopping_patience=3
        )
    ],
)

이 경우 metric 개선이 3번 연속 발생하지 않으면 학습이 중단될 수 있음.

TensorBoardCallback

TensorBoard와 연동하기 위한 callback임.

학습 중 발생하는 loss와 metric을 TensorBoard event file로 저장함.

이후 다음 명령으로 시각화할 수 있음.

tensorboard --logdir output_dir

WandbCallback

Weights & Biases(W&B)와 연동하기 위한 callback임.

학습 metric, learning curve, system usage 등을 자동으로 업로드하고 dashboard에서 시각화할 수 있음.

보통 다음처럼 사용함.

pip install wandb
training_args = TrainingArguments(
    output_dir="output",
    report_to="wandb",
)

그러면 내부적으로 WandbCallback이 활성화됨.

MLflowCallback

MLflow experiment tracking과 연동하기 위한 callback임.

experiment parameter, metric, artifact 등을 MLflow server에 기록할 수 있음.

CometCallback

Comet ML과 연동하기 위한 callback임.

학습 metric과 experiment 정보를 Comet dashboard에 자동 업로드함.

Built-in Callback과 Custom Callback의 차이

built-in callback은 이미 구현된 일반적인 기능을 바로 사용할 수 있다는 장점이 있음.

예를 들어 다음과 같은 경우에는 built-in callback만으로 충분할 수 있음.

  • TensorBoard logging
  • W&B tracking
  • MLflow tracking
  • progress bar 출력
  • early stopping
  • terminal logging

반면 다음과 같은 요구사항은 custom callback이 더 적합함.

  • 특정 형식(JSONL)으로 저장
  • 여러 training stage를 하나의 curve로 연결
  • 내부 metric을 원하는 구조로 후처리
  • custom monitoring 로직 추가
  • 특정 시점에 사용자 정의 동작 수행

즉, 일반적인 experiment tracking은 built-in callback으로 해결하고,
프로젝트 특화 로직은 custom callback으로 구현하는 방식이 많이 사용됨.

참고로, 현재 Trainer에 어떤 callback들이 등록되어 있는지는 trainer.callback_handler.callbacks를 확인하면 됨.

for callback in trainer.callback_handler.callbacks:
    print(type(callback).__name__)

예를 들어 다음과 같은 출력이 나올 수 있음.

DefaultFlowCallback
ProgressCallback
CurveLoggerCallback
TensorBoardCallback

여기서 CurveLoggerCallback은 사용자가 직접 추가한 custom callback이고, 나머지는 Hugging Face가 기본적으로 등록한 callback들임.

특정 callback이 등록되어 있는지 확인하고 싶다면 다음처럼 검사할 수도 있음.

from transformers.integrations import TensorBoardCallback

has_tensorboard = any(
    isinstance(callback, TensorBoardCallback)
    for callback in trainer.callback_handler.callbacks
)

print(has_tensorboard)

Callback은 학습 loop를 바꾸는 기능이 아님

TrainerCallback은 학습 loop의 특정 시점에 추가 동작을 수행하는 구조임.

따라서 다음과 같은 작업에 적합함.

  • 학습 로그 저장
  • evaluation metric 저장
  • experiment tracker 연동
  • 특정 조건에서 학습 중단 요청
  • checkpoint 저장 시점에 추가 작업 수행
  • epoch 또는 step 단위의 monitoring

callback은 model의 forward pass나 loss 계산 방식 자체를 바꾸는 용도는 아님.

즉, 다음과 같은 작업은 callback보다 Trainer를 subclassing하는 방식이 더 적절함.

  • compute_loss() 변경
  • prediction step 변경
  • training step 변경
  • custom forward pass 제어

Callback에 대한 이해를 위한 예제로서 on_log()on_evaluate() 시점에 로그를 JSONL 파일로 남기는 것을 작성해볼 예정임.

JSONL 기반 Curve Logger의 목적

학습을 여러 번에 나누어 진행하거나 checkpoint에서 resume하는 경우, 다음과 같은 문제가 생길 수 있음.

stage1: 처음 학습
stage2: checkpoint에서 resume
stage3: learning rate를 바꾸고 추가 학습
stage4: fine-tuning 전략을 바꾸고 추가 학습

이때 각 stage의 학습 로그가 따로 흩어지면 전체 learning curve를 그리기 불편함.

이를 해결하기 위해 학습 중 발생하는 log와 evaluation metric을 하나의 JSONL 파일에 계속 append하는 callback을 만들 수 있음.

JSONL은 JSON Lines의 약자임. 한 줄에 하나의 JSON object를 저장하는 형식 임.

{"step": 50, "loss": 0.612}
{"step": 100, "loss": 0.481}
{"step": 120, "eval_loss": 0.433, "eval_f1": 0.805}

이 방식은 학습 중 log가 발생할 때마다 파일 끝에 한 줄만 추가하면 됨.

따라서 학습이 중간에 중단되어도 기존 log가 유지되고, resume 이후에도 같은 파일에 이어서 기록할 수 있음.

참고: JSONL

일반적인 JSON 파일은 전체가 하나의 list나 dict 구조를 가져야 함.

[
    {"step": 50, "loss": 0.612},
    {"step": 100, "loss": 0.481},
    {"step": 120, "eval_loss": 0.433}
]

이 방식은 전체 data를 하나의 JSON 구조로 관리하기 때문에, 새로운 record를 추가하려면 기존 파일을 읽고, list에 record를 추가한 뒤, 다시 전체 파일을 저장해야 함.

반면 JSONL은 다음처럼 한 줄이 하나의 독립된 JSON object임.

{"step": 50, "loss": 0.612}
{"step": 100, "loss": 0.481}
{"step": 120, "eval_loss": 0.433}

따라서 log가 발생할 때마다 파일 끝에 한 줄만 append하면 됨.

이 때문에 JSONL은 다음과 같은 경우에 적합함.

  • training log처럼 계속 누적되는 data
  • 중간에 process가 중단될 수 있는 long-running job
  • line 단위로 순차 처리할 수 있는 대용량 data
  • streaming 방식으로 저장하거나 읽어야 하는 data
  • pandas, Python json module, command-line tool로 쉽게 후처리할 data

JSONL 기반 CurveLoggerCallback 구현

다음은 TrainerCallback을 상속하여 구현한 CurveLoggerCallback임.

주석은 PEP 8의 기본 관례를 따르도록 작성함.

  • Hugging Face callback hook의 signature는 유지함.
import json
import os

from transformers import TrainerCallback


class CurveLoggerCallback(TrainerCallback):
    """Save Trainer logs and evaluation metrics as JSONL records.

    This callback appends training logs and evaluation metrics to a JSONL
    file. It is useful when training is resumed from a checkpoint and the
    learning curve should be continued from the previous log.
    """

    def __init__(
        self,
        out_dir,
        filename="learning_curve.jsonl",
        stage="stage1",
    ):
        """Initialize the curve logger callback.

        Args:
            out_dir: Directory where the JSONL log file is saved.
            filename: Name of the JSONL log file.
            stage: Name of the current training stage.
        """
        # Full path of the JSONL file.
        self.path = os.path.join(out_dir, filename)

        # Stage name used to separate different training phases.
        self.stage = stage

        # Create the output directory if it does not exist.
        os.makedirs(out_dir, exist_ok=True)

    def _append(self, record):
        """Append one record to the JSONL file."""
        with open(self.path, "a", encoding="utf-8") as file:
            file.write(json.dumps(record, ensure_ascii=False) + "\n")

    def _base(self, state):
        """Create common fields shared by all log records."""
        return {
            # Name of the current training stage.
            "stage": self.stage,

            # Global optimizer update step.
            "step": int(state.global_step),

            # Current epoch position. It can be a float value.
            "epoch": float(state.epoch) if state.epoch else None,
        }

    def on_log(self, args, state, control, logs=None, **kwargs):
        """Run when Trainer emits a training log.

        Args:
            args: TrainingArguments used by Trainer.
            state: Current TrainerState.
            control: TrainerControl object for training flow control.
            logs: Training log values emitted by Trainer.
            **kwargs: Additional values passed by Trainer.
        """
        if not logs:
            return

        # Start with common fields such as stage, step, and epoch.
        record = self._base(state)

        # Add training log values such as loss and learning_rate.
        record.update(logs)

        # Append the final record to the JSONL file.
        self._append(record)

    def on_evaluate(self, args, state, control, metrics=None, **kwargs):
        """Run after Trainer finishes evaluation.

        Args:
            args: TrainingArguments used by Trainer.
            state: Current TrainerState.
            control: TrainerControl object for training flow control.
            metrics: Evaluation metrics returned by Trainer.
            **kwargs: Additional values passed by Trainer.
        """
        if not metrics:
            return

        # Start with common fields such as stage, step, and epoch.
        record = self._base(state)

        # Add evaluation metrics such as eval_loss and eval_f1.
        record.update(metrics)

        # Append the final record to the JSONL file.
        self._append(record)

코드 설명

import 구문

import json
import os

from transformers import TrainerCallback

jsonos는 Python standard library임.

json은 dictionary를 JSON 문자열로 변환하기 위해 사용함.
os는 directory 생성과 file path 조합에 사용함.

TrainerCallback은 Hugging Face transformers에서 제공하는 callback base class임. Custom callback을 만들려면 이를 상속하면 됨.

class 정의

class CurveLoggerCallback(TrainerCallback):

CurveLoggerCallbackTrainerCallback을 상속함.

따라서 Trainer가 인식할 수 있는 callback 객체가 됨. 이 class 안에서 on_log()on_evaluate()를 override하면, Trainer가 logging과 evaluation 시점에 해당 method를 자동으로 호출함.

생성자

def __init__(
    self,
    out_dir,
    filename="learning_curve.jsonl",
    stage="stage1",
):

생성자는 callback이 사용할 기본 설정을 저장함.

out_dir은 JSONL 파일이 저장될 directory임.
filename은 저장할 JSONL 파일 이름임.
stage는 현재 학습 구간을 구분하기 위한 이름임.

예를 들어 처음 학습이면 "stage1", checkpoint에서 resume한 학습이면 "stage2"처럼 지정할 수 있음.

self.path = os.path.join(out_dir, filename)
self.stage = stage
os.makedirs(out_dir, exist_ok=True)

self.path에는 최종 JSONL 파일 경로가 저장됨.

os.makedirs(out_dir, exist_ok=True)는 저장 directory가 없으면 생성함. 이미 존재하는 경우에도 error가 발생하지 않음.

_append method

def _append(self, record):
    """Append one record to the JSONL file."""
    with open(self.path, "a", encoding="utf-8") as file:
        file.write(json.dumps(record, ensure_ascii=False) + "\n")

_append()는 하나의 dictionary를 JSONL 파일에 한 줄로 추가하는 내부 helper method임.

여기서 "a" mode를 사용하므로 기존 파일 내용을 삭제하지 않고 뒤에 추가함.

ensure_ascii=False는 한글이 \uXXXX 형태로 escape되지 않고 그대로 저장되도록 함.

마지막에 "\n"을 붙이는 이유는 JSONL에서 한 줄이 하나의 JSON record이기 때문임.

_base method

def _base(self, state):
    """Create common fields shared by all log records."""
    return {
        "stage": self.stage,
        "step": int(state.global_step),
        "epoch": float(state.epoch) if state.epoch else None,
    }

_base()는 모든 log record에 공통으로 들어갈 값을 만듦.

stage는 현재 학습 구간임.
step은 현재 global step임.
epoch는 현재 epoch 위치임.

state.global_step은 optimizer update가 몇 번 수행되었는지를 나타냄. Learning curve의 x-axis로 자주 사용됨.

state.epoch는 반드시 정수는 아님. 예를 들어 epoch 중간에서는 1.25, 2.7 같은 float 값이 될 수 있음.

on_log method ***

def on_log(self, args, state, control, logs=None, **kwargs):

on_log()Trainer가 logging을 수행할 때 호출됨.

예를 들어 TrainingArguments에서 다음처럼 설정할 수 있음.

training_args = TrainingArguments(
    output_dir=OUT_DIR,
    logging_strategy="steps",
    logging_steps=50,
)

이 경우 50 step마다 logging이 발생하고, 그 시점에 on_log()가 호출됨.

logs에는 다음과 같은 값들이 들어올 수 있음.

{
    "loss": 0.4213,
    "learning_rate": 1.2e-5,
    "grad_norm": 0.87,
}

on_log() 내부에서는 먼저 logs가 비어 있는지 확인함.

if not logs:
    return

값이 없으면 저장할 것이 없으므로 바로 반환함.

그 다음 공통 정보를 만들고, 여기에 logs를 합침.

record = self._base(state)
record.update(logs)
self._append(record)

결과적으로 JSONL 파일에는 다음과 같은 record가 추가됨.

{"stage": "stage1", "step": 50, "epoch": 0.25, "loss": 0.612, "learning_rate": 1.8e-05}

on_evaluate method ***

def on_evaluate(self, args, state, control, metrics=None, **kwargs):

on_evaluate()는 evaluation이 끝난 직후 호출됨.

 

evaluation은 다음과 같은 경우에 수행될 수 있음.

  • trainer.evaluate()를 직접 호출한 경우
  • TrainingArguments에서 evaluation strategy를 지정한 경우
  • 학습 중 특정 step 또는 epoch마다 evaluation이 설정된 경우

metrics에는 다음과 같은 값들이 들어올 수 있음.

{
    "eval_loss": 0.3521,
    "eval_accuracy": 0.884,
    "eval_f1": 0.871,
    "eval_runtime": 12.3,
}

 

on_evaluate()on_log()와 같은 방식으로 동작함.

if not metrics:
    return

record = self._base(state)
record.update(metrics)
self._append(record)

 

결과적으로 JSONL 파일에는 다음과 같은 record가 추가됨.

{"stage": "stage1", "step": 120, "epoch": 0.6, "eval_loss": 0.433, "eval_accuracy": 0.812, "eval_f1": 0.805}

Trainer에 Callback 등록

작성한 callback은 Trainer를 생성할 때 callbacks 인자에 list 형태로 넘겨줌.

callbacks는 이름 그대로 여러 callback 객체를 담는 list임.

  • 따라서 하나의 callback만 등록할 수도 있고,
  • 여러 callback을 동시에 등록할 수도 있음.

예를 들어 custom curve logger와 함께 Hugging Face에서 제공하는 EarlyStoppingCallback을 같이 사용할 수 있음.

from transformers import EarlyStoppingCallback

curve_logger = CurveLoggerCallback(
    OUT_DIR,
    stage="stage1",
)

# Callback은 반드시 직접 구현해야 하는 것은 아님.
# Hugging Face가 제공하는 built-in callback을 그대로 사용할 수도 있음.
#
# EarlyStoppingCallback은 validation metric이 일정 횟수 이상
# 개선되지 않으면 학습을 자동으로 중단하는 callback임.
#
# 여기서는 patience를 3으로 설정했으므로,
# evaluation metric이 3번 연속 개선되지 않으면 training이 종료됨.
early_stopping = EarlyStoppingCallback(
    early_stopping_patience=3,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["test"],
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
    callbacks=[
        curve_logger,
        early_stopping,
    ],
)

위 코드에서는 다음 두 callback이 동시에 동작함.

  • curve_logger
    • training log와 evaluation metric을 JSONL 파일에 저장함.
  • early_stopping
    • evaluation metric이 개선되지 않으면 학습을 조기에 종료함.

Trainer는 학습 loop 중 특정 event가 발생할 때 등록된 callback들을 순서대로 호출함.

예를 들어 logging 시점에는 다음과 같은 흐름으로 동작함.

on_log()
 ├─ curve_logger.on_log(...)
 └─ early_stopping.on_log(...)

evaluation 시점에도 동일하게 각 callback의 on_evaluate()가 순서대로 호출됨.

 

따라서 callback을 여러 개 등록하면

logging, monitoring, early stopping, experiment tracking 등을 서로 독립적으로 조합할 수 있음.

curve_logger = CurveLoggerCallback(
    OUT_DIR,
    stage="stage1",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["test"],
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
    callbacks=[curve_logger],
)

 

핵심은 다음 부분임.

callbacks=[curve_logger]

 

이렇게 등록하면 Trainer가 학습을 수행하면서 curve_logger의 callback hook을 자동으로 호출함.

 

계속 강조하지만, 사용자가 다음과 같이 직접 호출하지 않음.

curve_logger.on_log(...)
curve_logger.on_evaluate(...)

이런 호출은 Trainer 내부에서 수행됨.


JSONL 파일에 저장되는 결과

학습이 진행되면 learning_curve.jsonl 파일에는 다음과 같은 내용이 누적됨.

{"stage": "stage1", "step": 50, "epoch": 0.25, "loss": 0.612, "learning_rate": 1.8e-05}
{"stage": "stage1", "step": 100, "epoch": 0.50, "loss": 0.481, "learning_rate": 1.6e-05}
{"stage": "stage1", "step": 120, "epoch": 0.60, "eval_loss": 0.433, "eval_accuracy": 0.812, "eval_f1": 0.805}

여기서 training log와 evaluation metric이
같은 파일에 저장된다는 점에 주의해야 함.

 

즉, 어떤 row에는 loss가 있고 eval_loss가 없을 수 있음.

반대로 어떤 row에는 eval_loss가 있고 loss가 없을 수 있음.

이는 on_log()on_evaluate()는 같은 global_step에서 발생할 수는 있지만, 서로 다른 callback hook으로 호출되며, 전달받는 값의 종류도 다르기 때문임.

 

즉, 위의 예제에서 다음의 단점이 있음:

  • on_evaluate()에서 저장된 evaluation metric이 다시 logging event로 전달되어 on_log()에서도 저장되면 evaluation metric이 중복 저장될 수 있음.
  • 이 경우에는 on_log()에서는 training log만 저장하도록 eval_로 시작하는 key를 제외하는 방식을 사용하면 됨.

저장된 로그 읽기

저장된 JSONL 파일은 pandas.read_json()으로 읽을 수 있음.

import os

import pandas as pd


curve_path = os.path.join(OUT_DIR, "learning_curve.jsonl")

df = pd.read_json(curve_path, lines=True)
df.head()

 

training loss만 보고 싶으면 loss가 존재하는 row만 선택하면 됨.

train_df = df[df["loss"].notna()]

 

evaluation 결과만 보고 싶으면 eval_loss가 존재하는 row만 선택하면 됨.

eval_df = df[df["eval_loss"].notna()]

 

이후 step 또는 epoch를 x-axis로 사용하여 learning curve를 그릴 수 있음.


Resume 학습에서의 사용

처음 학습에서는 다음처럼 callback을 생성할 수 있음.

curve_logger = CurveLoggerCallback(
    OUT_DIR,
    stage="stage1",
)

 

checkpoint에서 resume하여 추가 학습을 수행할 때는 stage만 바꾸어 같은 JSONL 파일에 이어서 저장할 수 있음.

curve_logger = CurveLoggerCallback(
    OUT_DIR,
    stage="stage2",
)

 

그러면 같은 learning_curve.jsonl 파일에 다음과 같이 stage가 구분되어 저장됨.

{"stage": "stage1", "step": 1000, "epoch": 1.8, "loss": 0.312}
{"stage": "stage2", "step": 1500, "epoch": 2.3, "loss": 0.231}
{"stage": "stage2", "step": 1600, "epoch": 2.5, "eval_loss": 0.289, "eval_f1": 0.901}

이 방식의 장점은 전체 learning curve를 하나로 이어서 볼 수 있으면서도,

어떤 구간이 어떤 stage에 해당하는지 구분할 수 있다는 점임.


정리

TrainerCallback

Hugging Face Trainer의 학습 loop 중

특정 시점에 사용자 정의 동작을 추가하기 위한 hook 구조임.

 

이 예제의 CurveLoggerCallback은 그 중 on_log()on_evaluate()만 사용함.

  • on_log()에서는 training loss, learning rate 같은 학습 중간 log를 저장함.
  • on_evaluate()에서는 eval loss, accuracy, F1 score 같은 evaluation metric을 저장함.

저장 형식으로 JSONL을 사용하면 log가 발생할 때마다 파일 끝에 한 줄씩 추가할 수 있음.

  • 따라서 학습이 중단되어도 기존 log가 유지되고, checkpoint에서 resume한 뒤에도 같은 파일에 이어서 기록할 수 있음.

같이 보면 좋은 자료들

2025.04.10 - [Python] - [DL] PyTorch-Hook

 

[DL] PyTorch-Hook

PyTorch의 hook은Neural Network 내부의 계산 과정을 관찰하거나,특정 시점에서 개입할 수 있도록 해주는 기능 (사실은 function 또는 instance method임).을 제공함. 이를 통해 forward 중간 출력 및 backward 에서

ds31x.tistory.com

https://ds31x.github.io/wiki/hf/hf_trainer/

 

HF-Trainer

PyTorch 학습 루프를 추상화한 Hugging Face Trainer의 A to Z. TrainingArguments, compute_metrics를 사용한 기본 설정부터, Colab에서 Google Drive를 활용해 학습을 재개하고, AdamW와 스케줄러의 동작 원리를 이해하며

ds31x.github.io

 

728x90

'ML' 카테고리의 다른 글

torch_xla 간단 사용법: TPU로 PyTorch 사용하기  (0) 2026.06.23
2026 정리  (0) 2026.06.10
[ML] linear_model.SGDRegressor  (0) 2026.04.28
Multi-Head "Masked/Cross" Attention - Transformer Decoder  (1) 2026.04.18
Ex: Linear Regression 연습  (0) 2026.04.11