목록으로

프로그래밍 · Python

Python 완전정복 시리즈 41: RewardTrainer 완벽 이해하기

BeanCon
Python에서 Hugging Face TRL RewardTrainer로 AI 답변 품질을 점수로 평가하는 방법을 설명하는 대표 이미지

Python 완전정복 시리즈 41편입니다. Hugging Face TRL의 RewardTrainer로 AI 답변 품질을 점수로 평가하는 Reward Model을 만드는 방법을 정리했습니다. Reward Model과 RLHF의 관계, Sequence Classification Head, Chosen·Rejected Pair, Pairwise Preference Learning, Bradley-Terry 모델, Reward Loss, Reward Margin, Pairwise Accuracy, Centered Reward, Reward Dataset 형식, Chat Template, RewardConfig, LoRA·QLoRA Reward Model, score Head 저장, 학습 로그 해석, 임의 답변 점수 계산, 여러 답변 Ranking, Best-of-N Sampling, Gradio AI 답변 심사위원과 오류 해결까지 다룹니다.

목차

메타 설명
AI가 생성한 여러 답변 중 어떤 답변이 더 좋은지 자동으로 평가할 수 있을까요? Hugging Face TRL의 RewardTrainer를 이용해 Reward Model을 만들고, Preference Dataset, Chosen·Rejected Pair, Bradley-Terry Loss, Reward Margin, Pairwise Accuracy, LoRA, QLoRA, 여러 답변 Ranking, Gradio 채점 화면까지 실습합니다.

지난 시간에는 `DPOTrainer`를 이용해 같은 질문에 대한 두 답변을 비교했습니다.

질문

├─ Chosen
│  더 좋은 답변
│
└─ Rejected
   덜 좋은 답변

그리고 모델에게 이렇게 가르쳤습니다.

“앞으로는 Chosen 같은 답변을
Rejected보다 더 선호하세요.”

그런데 이번에는 역할을 바꿔보겠습니다.

답변을 생성하는 AI가 아니라 답변을 평가하는 AI를 만드는 것입니다.

예를 들어 다음 세 답변이 있다고 하겠습니다.

질문:
Python의 리스트와 튜플 차이를 설명해 주세요.

답변 A:

리스트는 mutable이고
튜플은 immutable입니다.

답변 B:

리스트와 튜플은 여러 값을 순서대로 저장합니다.

리스트는 생성 후 값을 변경할 수 있지만
튜플은 일반적으로 값을 변경할 수 없습니다.

변경이 필요한 데이터에는 리스트,
고정된 데이터에는 튜플을 사용할 수 있습니다.

답변 C:

리스트와 튜플은 완전히 같은 자료형입니다.

사람이라면 대략 이렇게 평가할 수 있습니다.

답변 B
★★★★★

답변 A
★★★☆☆

답변 C
★☆☆☆☆

AI도 비슷하게 평가하도록 만들 수 있습니다.

답변 A → 0.8

답변 B → 3.4

답변 C → -2.1

이처럼 Prompt와 Response를 입력받아 하나의 품질 점수를 출력하는 모델을 Reward Model이라고 합니다.

Prompt

+

Response

↓

Reward Model

↓

Reward Score

오늘의 주인공은 Hugging Face TRL의 `RewardTrainer`입니다.

현재 TRL의 RewardTrainer는 선호·비선호 응답 Pair를 이용해 Outcome-supervised Reward Model, ORM을 학습하도록 제공됩니다. 모델은 선호되는 응답에 더 높은 점수를 주도록 훈련됩니다.

생성형 AI:
“답변을 만들겠습니다.”


Reward Model:
“잠깐만요.
제가 먼저 채점하겠습니다.”

AI 훈련소에 드디어 채점관이 등장했습니다. 🧑‍⚖️📋🤖

1. Reward Model이란?

Reward Model은 입력된 응답의 품질을 하나의 Scalar Score로 변환하는 모델입니다.

Prompt

+

Response

↓

Transformer

↓

Reward Head

↓

3.27

일반적인 생성형 언어 모델은 다음 Token을 예측합니다.

안녕하세요

→ 저는

→ Python

→ 교관입니다

Reward Model은 다음 Token을 생성하지 않습니다.

대신 전체 입력을 읽고 하나의 숫자를 출력합니다.

“이 응답을 얼마나 선호하는가?”

RewardTrainer는 Preference Pair를 이용해 선호되는 응답의 점수가 비선호 응답보다 높아지도록 Reward 함수를 학습합니다.

2. Reward는 정확히 무엇인가?

Reward라는 표현 때문에 다음처럼 생각하기 쉽습니다.

10점 만점 중

8.7점

하지만 Reward Model의 점수는 일반적으로 사람이 정한 절대 평가 점수와 다릅니다.

예:

응답 A:
-1.2

응답 B:
0.4

응답 C:
3.1

여기서 중요한 것은 보통 다음입니다.

3.1 > 0.4 > -1.2

즉,

C를 B보다 선호

B를 A보다 선호

Reward Model의 핵심 역할은 절대 평점 산정이 아니라 상대적인 Ranking입니다.

3. Reward Model이 필요한 이유

생성형 모델은 같은 질문에도 다양한 답변을 생성할 수 있습니다.

Prompt

↓

답변 A

답변 B

답변 C

답변 D

이 가운데 가장 좋은 답변을 고르고 싶다면 평가 기준이 필요합니다.

Reward Model은 다음 용도로 활용할 수 있습니다.

여러 후보 답변 Ranking

Best-of-N 선택

RLHF Reward Signal

PPO 학습

온라인 Preference Optimization

모델 품질 비교

자동 데이터 필터링

TRL의 PPOTrainer에서도 Reward Model은 생성된 응답에 대한 Reward를 계산하는 구성 요소로 사용됩니다.

4. SFT·DPO·Reward Model 차이

SFT

질문

+

모범 답안

→ 모범 답안을 생성하도록 학습

DPO

질문

+

Chosen

+

Rejected

→ Chosen을 더 선호하도록
  생성 모델 자체를 학습

Reward Model

질문

+

Chosen

+

Rejected

→ 어떤 답변이 더 좋은지
  점수화하는 모델 학습

정리하면:

기술하는 일
SFT좋은 답변을 따라 쓰기
DPO좋은 답변을 더 선호하기
Reward Model답변 품질을 점수로 평가하기

비유하면:

SFT
→ 학생

DPO
→ 취향을 배우는 학생

Reward Model
→ 시험 채점관

5. Reward Model과 RLHF의 관계

전통적인 RLHF 흐름을 단순화하면 다음과 같습니다.

Pre-trained Model

↓

SFT

↓

Preference Dataset

↓

Reward Model 학습

↓

PPO 등의 강화학습

↓

정렬된 모델

Reward Model은 사람이 매번 직접 평가하지 않아도 반복적인 학습 과정에서 응답의 품질을 대신 점수화하는 역할을 합니다.

사람:

Preference Pair 작성


Reward Model:

사람의 선호 패턴 학습


강화학습:

Reward 점수가 높은 방향으로
Policy 개선

6. RewardTrainer란?

`RewardTrainer`는 Hugging Face TRL이 제공하는 Reward Model 학습용 Trainer입니다.

가장 단순한 공식 예제는 다음 구조입니다.

from datasets import load_dataset
from trl import RewardTrainer


dataset = load_dataset(
    "trl-lib/ultrafeedback_binarized",
    split="train"
)


trainer = RewardTrainer(
    model="Qwen/Qwen2.5-0.5B-Instruct",
    train_dataset=dataset
)


trainer.train()

RewardTrainer는 Transformers의 Trainer를 감싼 형태이며 Preference Dataset 전처리, Tokenization, Pairwise Reward Loss 계산과 평가 지표 기록을 담당합니다.

7. Reward Model의 구조

Reward Model의 기본 구조는 생각보다 단순합니다.

Prompt + Response

↓

Transformer Backbone

↓

Hidden Representation

↓

Score Head

↓

1개의 숫자

예를 들어 Qwen 모델을 Reward Model로 사용하면:

Qwen Transformer

+

Score Head

↓

Reward

일반 생성 모델의 마지막 Layer는 Vocabulary 크기의 Logit을 출력합니다.

Vocabulary:
150,000개

→ 다음 Token 후보 150,000개

Reward Model은 다음처럼 출력합니다.

Reward Head:

1개 값

현재 RewardTrainer에 모델 ID 문자열을 전달하면 내부적으로 `AutoModelForSequenceClassification.from_pretrained()`를 사용하며 `num_labels`는 자동으로 1로 설정됩니다.

8. Sequence Classification Head

일반적인 분류 모델은 다음처럼 여러 Class의 Score를 출력합니다.

긍정

부정

중립

즉:

num_labels=3

Reward Model은 하나의 Scalar가 필요합니다.

num_labels=1

개념적으로는 다음과 같습니다.

model = (
    AutoModelForSequenceClassification
    .from_pretrained(
        MODEL_ID,
        num_labels=1
    )
)

RewardTrainer에서는 모델을 문자열로 전달할 경우 `num_labels=1`을 자동 구성하므로 `model_init_kwargs`에서는 별도로 지정할 필요가 없습니다.

9. Chosen과 Rejected

Reward Model 역시 DPO에서 사용했던 Preference Pair를 이용합니다.

Prompt

├─ Chosen
│
└─ Rejected

학습 목표:

Reward(Chosen)

>

Reward(Rejected)

예:

Chosen Reward:
2.8


Rejected Reward:
-0.7

모델은 사람이 선호한 응답에 더 높은 Reward를 부여하도록 학습합니다.

10. Pairwise Preference Learning

일반적인 회귀 문제와는 다릅니다.

Reward Model에게 다음 정답을 직접 제공하지 않습니다.

답변 A:
8.7점

답변 B:
3.2점

대신 다음 정보만 제공합니다.

A가 B보다 좋다.

즉:

A > B

이런 방식을 Pairwise Preference Learning이라고 볼 수 있습니다.

사람:

“몇 점인지는 모르겠지만
A가 B보다 낫습니다.”

이 정보만으로도 Ranking Model을 학습할 수 있습니다.

11. Bradley-Terry 모델

RewardTrainer의 기본적인 Pairwise 학습은 Bradley-Terry 형태의 선호 확률을 사용합니다.

개념적으로는:

Chosen Reward가 Rejected보다 높을수록

Chosen이 선택될 확률이 증가

공식 TRL 문서에서는 선호 확률을 다음 형태로 설명합니다.

P(Chosen > Rejected)

=

sigmoid(
    Reward(Chosen)
    -
    Reward(Rejected)
)

즉 Reward 차이가 클수록 Chosen이 선택될 확률도 높아집니다.

12. Reward Loss 이해하기

RewardTrainer의 기본 Loss를 직관적으로 표현하면 다음과 같습니다.

Chosen Reward

-

Rejected Reward

→ 큰 양수로 만들기

예를 들어:

Chosen:
3.0

Rejected:
-1.0

Margin:
4.0

좋은 방향입니다.

반대로:

Chosen:
-1.0

Rejected:
2.0

Margin:
-3.0

사람의 Preference와 반대입니다.

따라서 Loss는 다음을 장려합니다.

Reward(Chosen)

>

Reward(Rejected)

공식 RewardTrainer는 `-log(sigmoid(chosen_reward - rejected_reward))` 형태의 Pairwise Preference Loss를 사용합니다.

13. Reward Margin이란?

Reward Margin은 매우 중요한 지표입니다.

Margin

=

Chosen Reward

-

Rejected Reward

예:

Chosen:
2.3

Rejected:
0.7

Margin:
1.6

Margin이 양수입니다.

→ 올바른 Ranking

반대로:

Chosen:
-0.5

Rejected:
1.2

Margin:
-1.7
→ Rejected를 더 높게 평가

RewardTrainer는 학습과 평가 중 평균 Reward Margin을 로그로 기록합니다.

14. Pairwise Accuracy란?

RewardTrainer의 `accuracy`는 일반적인 분류 Accuracy와 약간 다르게 해석합니다.

정답 조건:

Reward(Chosen)

>

Reward(Rejected)

100개의 Preference Pair 중 82개를 올바르게 Ranking했다면:

Accuracy:

82 / 100

= 82%

현재 RewardTrainer의 Accuracy는 Chosen에 Rejected보다 높은 Reward를 준 Pair의 비율로 정의됩니다.

15. Reward 점수의 절대값을 믿으면 안 되는 이유

Reward Model에서 중요한 함정입니다.

다음 모델 A:

좋은 답변:
2

나쁜 답변:
0

모델 B:

좋은 답변:
102

나쁜 답변:
100

두 모델의 Preference Probability 관점에서는 Reward 차이가 동일합니다.

Margin:

2

즉 모든 Reward에 같은 상수를 더해도 Ranking은 변하지 않습니다.

2, 0

→

102, 100

이런 특성 때문에 Reward의 절대적인 0이나 100 자체에 특별한 의미를 부여하면 안 됩니다.

공식 TRL 문서도 Bradley-Terry 모델은 모든 Reward에 동일한 상수를 더해도 선호 확률이 변하지 않는 underdetermined 특성이 있다고 설명합니다.

16. Centered Reward

Reward Score가 불필요하게 계속 큰 방향이나 작은 방향으로 이동하는 것을 줄이기 위해 TRL은 Reward Centering 옵션을 제공합니다.

center_rewards_coefficient=0.01

이는 Chosen과 Rejected Reward가 전체적으로 0 근처를 유지하도록 보조 Loss를 추가합니다.

공식 TRL 문서는 `center_rewards_coefficient=1e-2`를 권장값으로 소개합니다.

Centering 없음:

130
127

→ Margin 3


Centering 적용:

1.5
-1.5

→ Margin 3

Ranking 정보는 유지하면서 점수 범위를 더 해석하기 편하게 만들 수 있습니다.

17. 현재 TRL 버전

2026년 8월 10일 기준 PyPI의 최신 TRL 안정 버전은 1.9.2이며, 2026년 7월 28일 공개되었습니다. Python 3.10 이상을 요구합니다.

확인:

python -m pip show trl

Python:

import trl


print(
    trl.__version__
)

TRL은 Post-training 기능이 빠르게 발전하고 있으므로 과거 0.x 예제와 현재 1.x API를 섞지 않는 것이 중요합니다.

18. 환경 설치

Windows

python -m venv venv
venv\Scripts\activate

python -m pip install --upgrade pip
python -m pip install "transformers[torch]" datasets accelerate peft trl

macOS·Linux

python3 -m venv venv
source venv/bin/activate

python -m pip install --upgrade pip
python -m pip install "transformers[torch]" datasets accelerate peft trl

QLoRA

python -m pip install bitsandbytes

현재 TRL은 PEFT와 Quantization 통합을 제공하며 RewardTrainer에서도 `peft_config`와 `quantization_config`를 함께 사용할 수 있습니다.

19. Reward Dataset 형식

RewardTrainer가 요구하는 기본 데이터 유형은 Preference Dataset입니다.

현재 다음 네 가지 형태를 지원합니다.

Standard + Implicit Prompt

Conversational + Implicit Prompt

Standard + Explicit Prompt

Conversational + Explicit Prompt

Conversational Dataset이 입력되면 Chat Template이 자동 적용됩니다.

20. Standard Preference Format

example = {
    "prompt": (
        "Python 리스트란 무엇인가요?"
    ),

    "chosen": (
        "리스트는 여러 값을 순서대로 "
        "저장하는 변경 가능한 자료형입니다."
    ),

    "rejected": (
        "리스트는 숫자 하나만 저장합니다."
    )
}

구조가 단순해 데이터 검사와 변환이 쉽습니다.

21. Conversational Preference Format

example = {
    "prompt": [
        {
            "role": "system",
            "content": (
                "당신은 Python 답변을 "
                "평가하는 AI 교관입니다."
            )
        },
        {
            "role": "user",
            "content": (
                "리스트란 무엇인가요?"
            )
        }
    ],

    "chosen": [
        {
            "role": "assistant",
            "content": (
                "리스트는 여러 값을 "
                "순서대로 저장하는 "
                "변경 가능한 자료형입니다."
            )
        }
    ],

    "rejected": [
        {
            "role": "assistant",
            "content": (
                "리스트는 숫자 하나입니다."
            )
        }
    ]
}

이번 프로젝트에서는 이 형식을 사용합니다.

22. Explicit Prompt Format

Prompt가 별도 열에 존재합니다.

prompt

chosen

rejected

RewardTrainer는 내부적으로 다음 Sequence를 구성합니다.

Prompt + Chosen

Prompt + Rejected

Explicit Prompt 형태에서는 RewardTrainer가 Prompt와 각 Completion을 합친 후 Tokenization합니다.

23. Implicit Prompt Format

Chosen과 Rejected 안에 질문까지 포함합니다.

example = {
    "chosen": [
        {
            "role": "user",
            "content": "리스트란 무엇인가요?"
        },
        {
            "role": "assistant",
            "content": "여러 값을 저장하는 자료형입니다."
        }
    ],

    "rejected": [
        {
            "role": "user",
            "content": "리스트란 무엇인가요?"
        },
        {
            "role": "assistant",
            "content": "숫자 하나입니다."
        }
    ]
}

가능하지만 직접 Dataset을 구축한다면 Explicit Prompt가 관리하기 편한 경우가 많습니다.

24. Reward Dataset 만들기

````pythonSYSTEM_MESSAGE = ("당신은 Python 입문자를 위한 ""친절하고 정확한 교관입니다.")

PREFERENCE_DATA = [{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 변수란 무엇인가요?")}],

"chosen": [{"role": "assistant","content": ("변수는 값을 저장하기 위해 ""사용하는 이름입니다.\n\n""```python\n""age = 20\n""print(age)\n""```")}],

"rejected": [{"role": "assistant","content": ("변수는 Python을 설치하는 기능입니다.")}]}]````

25. Preference Pair 품질 관리

Reward Model의 성능은 Preference Pair의 품질에 크게 영향을 받습니다.

좋은 Pair

Chosen:

정확함


Rejected:

실제 모델에서 나올 법한 오류

좋지 않은 Pair

Chosen:

정확함


Rejected:

!@#$%^&

이런 데이터만 학습하면 모델은 답변 품질 대신 다음 기준을 배울 수 있습니다.

문장처럼 보이는가?

좋은 Dataset에는 다양한 실패 유형이 필요합니다.

사실 오류

설명 부족

질문 회피

코드 오류

형식 위반

과도한 장황함

근거 없는 확신

26. Train·Validation·Test 분리

from datasets import Dataset


dataset = Dataset.from_list(
    PREFERENCE_DATA
)


split_dataset = (
    dataset.train_test_split(
        test_size=0.2,
        seed=2026,
        shuffle=True
    )
)


train_dataset = (
    split_dataset["train"]
)


validation_dataset = (
    split_dataset["test"]
)

실제 운영 프로젝트에서는 별도의 Test Set을 추가하는 것이 좋습니다.

Train

→ Reward 학습


Validation

→ Hyperparameter 선택


Test

→ 최종 Ranking 평가

27. Tokenizer와 Chat Template

from transformers import (
    AutoTokenizer
)


MODEL_ID = (
    "Qwen/"
    "Qwen2.5-0.5B-Instruct"
)


tokenizer = (
    AutoTokenizer
    .from_pretrained(
        MODEL_ID
    )
)

Padding:

if tokenizer.pad_token is None:
    tokenizer.pad_token = (
        tokenizer.eos_token
    )

Chat Template 확인:

formatted_prompt = (
    tokenizer.apply_chat_template(
        PREFERENCE_DATA[0][
            "prompt"
        ],
        tokenize=False,
        add_generation_prompt=True
    )
)


print(
    formatted_prompt
)

Conversational Dataset을 RewardTrainer에 넘기면 Trainer가 Chat Template 적용을 자동 처리합니다.

28. Reward 모델 불러오기

직접 생성한다면:

from transformers import (
    AutoModelForSequenceClassification
)


reward_model = (
    AutoModelForSequenceClassification
    .from_pretrained(
        MODEL_ID,
        num_labels=1
    )
)

하지만 RewardTrainer에는 모델 ID를 직접 전달할 수 있습니다.

trainer = RewardTrainer(
    model=MODEL_ID,
    train_dataset=train_dataset
)

이 경우 RewardTrainer가 Sequence Classification 모델을 생성하고 `num_labels=1`로 설정합니다.

29. RewardConfig 이해하기

from trl import (
    RewardConfig
)


training_args = RewardConfig(
    output_dir=(
        "outputs/"
        "python-reward-model"
    ),

    learning_rate=1e-4,

    per_device_train_batch_size=2,
    per_device_eval_batch_size=2,

    gradient_accumulation_steps=4,

    num_train_epochs=3,

    max_length=512,

    eval_strategy="epoch",
    save_strategy="epoch",

    logging_steps=5,

    center_rewards_coefficient=0.01,

    report_to="none"
)

현재 RewardConfig의 주요 Reward 전용 옵션은 다음과 같습니다.

max_length

center_rewards_coefficient

chat_template_path

disable_dropout

dataset_num_proc

eos_token

pad_token

activation_offloading

기본 `max_length`는 1024이고, 기본 Learning Rate는 `1e-4`, 기본적으로 Gradient Checkpointing이 활성화되어 있습니다.

30. Max Length 설정

max_length=512

Reward Model은 다음 전체 Sequence를 읽습니다.

Prompt

+

Response

따라서 너무 짧게 설정하면 Response의 중요한 부분이 잘릴 수 있습니다.

Prompt:
200 Token

Response:
400 Token

max_length:
512


결과:

Response 뒤쪽 일부 손실

실제 데이터의 Token 길이를 먼저 분석하는 것이 좋습니다.

31. Learning Rate 설정

Full Fine-tuning:

learning_rate=1e-4

RewardConfig의 현재 기본값도 `1e-4`입니다.

하지만 LoRA Adapter만 학습한다면 더 큰 Learning Rate를 사용할 수 있습니다.

공식 RewardTrainer 문서는 Adapter 학습에서 약 `1e-3` 수준을 일반적인 출발점으로 안내합니다.

learning_rate=1e-3

실제 프로젝트에서는 다음 후보를 비교할 수 있습니다.

1e-4

3e-4

5e-4

1e-3

32. LoRA Reward Model

전체 모델 대신 LoRA Adapter만 학습해 보겠습니다.

from peft import (
    LoraConfig,
    TaskType
)


lora_config = LoraConfig(
    task_type=TaskType.SEQ_CLS,

    r=16,
    lora_alpha=32,
    lora_dropout=0.05,

    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj"
    ],

    modules_to_save=[
        "score"
    ],

    bias="none"
)

RewardTrainer는 `peft_config`를 직접 받아 PEFT Adapter를 구성할 수 있습니다.

33. score Head를 저장해야 하는 이유

LoRA Target Module만 학습하면 다음 Layer가 Adapter에 포함됩니다.

q_proj

k_proj

v_proj

o_proj

하지만 Reward Model에서 정말 중요한 마지막 출력 Layer가 있습니다.

score
Transformer

↓

score Head

↓

Reward

이 `score` Head가 학습·저장되지 않으면 Reward Model이 제대로 복원되지 않을 수 있습니다.

따라서:

modules_to_save=[
    "score"
]

현재 RewardTrainer 공식 PEFT 예제도 기본 모델이 Sequence Classification 모델이 아닌 구조에서 Reward Head를 보존하려면 `modules_to_save=["score"]`를 지정하는 것이 중요하다고 안내합니다.

LoRA만 저장하고

채점표를 두고 온 심사위원

↓

평가 불가

34. RewardTrainer 생성

from trl import (
    RewardTrainer
)


trainer = RewardTrainer(
    model=MODEL_ID,

    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config
)

RewardTrainer는 다음 작업을 자동으로 처리합니다.

Chat Template 적용

Prompt + Chosen 결합

Prompt + Rejected 결합

Tokenization

Dynamic Padding

Chosen Reward 계산

Rejected Reward 계산

Pairwise Loss 계산

Accuracy 계산

Margin 계산

35. Reward Model 학습

train_result = (
    trainer.train()
)

학습 중:

Chosen Sequence

→ Reward Model

→ Reward_chosen


Rejected Sequence

→ Reward Model

→ Reward_rejected


Reward_chosen
-
Reward_rejected

↓

Pairwise Loss

↓

모델 업데이트

결과 저장:

trainer.log_metrics(
    "train",
    train_result.metrics
)


trainer.save_metrics(
    "train",
    train_result.metrics
)


trainer.save_state()

36. 학습 로그 읽기

현재 RewardTrainer는 다음 지표를 기록합니다.

global_step

epoch

num_tokens

loss

accuracy

min_reward

mean_reward

max_reward

margin

learning_rate

grad_norm

예:

loss:
0.42

accuracy:
0.81

margin:
1.37

mean_reward:
0.04

37. Accuracy 해석하기

accuracy = 0.81

의미:

Preference Pair의 81%에서

Chosen Reward

>

Rejected Reward

Train Accuracy만 보면 안 됩니다.

Train:
0.99

Validation:
0.68

이 경우 Preference Pair를 외웠을 가능성이 있습니다.

38. Reward Margin 해석하기

초기:

Chosen:
0.1

Rejected:
0.0

Margin:
0.1

학습 후:

Chosen:
1.7

Rejected:
-1.2

Margin:
2.9

모델이 두 응답을 더 명확하게 구분하게 되었습니다.

그러나 Margin이 무조건 클수록 좋다고 단정할 수는 없습니다.

Validation Ranking 성능

독립 Test 성능

새로운 Prompt 성능

을 함께 확인해야 합니다.

39. Min·Mean·Max Reward

RewardTrainer는 다음 값도 기록합니다.

min_reward

mean_reward

max_reward

예:

min:
-4.2

mean:
0.1

max:
5.8

Centered Reward를 사용하면 평균 Reward가 지나치게 한쪽으로 이동하는 것을 억제할 수 있습니다.

하지만 다음은 기억해야 합니다.

Reward가 5라고

5점 만점이라는 뜻이 아니다.

40. Validation 평가

validation_metrics = (
    trainer.evaluate()
)


print(
    validation_metrics
)

저장:

trainer.log_metrics(
    "validation",
    validation_metrics
)


trainer.save_metrics(
    "validation",
    validation_metrics
)

확인:

eval_loss

eval_accuracy

eval_margin

eval_min_reward

eval_mean_reward

eval_max_reward

41. Reward Adapter 저장

from pathlib import Path


REWARD_MODEL_DIR = Path(
    "models/"
    "python-reward-model-lora"
)


trainer.save_model(
    str(
        REWARD_MODEL_DIR
    )
)


tokenizer.save_pretrained(
    REWARD_MODEL_DIR
)

PEFT를 사용했다면 폴더에는 다음 파일이 포함될 수 있습니다.

adapter_config.json

adapter_model.safetensors

tokenizer.json

tokenizer_config.json

42. 저장 모델 불러오기

PEFT는 Sequence Classification용 AutoPeftModel도 제공합니다.

from peft import (
    AutoPeftModelForSequenceClassification
)


reward_model = (
    AutoPeftModelForSequenceClassification
    .from_pretrained(
        REWARD_MODEL_DIR
    )
)


reward_model.eval()

장치 선택:

import torch


if torch.cuda.is_available():
    device = torch.device(
        "cuda"
    )

elif torch.backends.mps.is_available():
    device = torch.device(
        "mps"
    )

else:
    device = torch.device(
        "cpu"
    )


reward_model = reward_model.to(
    device
)

43. 임의의 답변 점수 계산

Reward Model에 새로운 Prompt와 Response를 넣어보겠습니다.

def score_response(
    model,
    tokenizer,
    device,
    question: str,
    response: str
) -> float:
    messages = [
        {
            "role": "system",
            "content": SYSTEM_MESSAGE
        },
        {
            "role": "user",
            "content": question
        },
        {
            "role": "assistant",
            "content": response
        }
    ]

    encoded = (
        tokenizer.apply_chat_template(
            messages,
            tokenize=True,
            add_generation_prompt=False,
            return_tensors="pt"
        )
        .to(device)
    )

    with torch.inference_mode():
        outputs = model(
            input_ids=encoded
        )

    score = (
        outputs.logits
        .squeeze()
        .float()
        .item()
    )

    return score

사용:

question = (
    "Python 변수란 무엇인가요?"
)


response = (
    "변수는 값을 저장하기 위해 "
    "사용하는 이름입니다."
)


score = score_response(
    reward_model,
    tokenizer,
    device,
    question,
    response
)


print(
    f"Reward: {score:.4f}"
)

44. 두 답변 비교하기

answer_a = (
    "변수는 값을 저장하기 위해 "
    "사용하는 이름입니다."
)


answer_b = (
    "변수는 Python을 삭제하는 명령입니다."
)

점수:

score_a = score_response(
    reward_model,
    tokenizer,
    device,
    question,
    answer_a
)


score_b = score_response(
    reward_model,
    tokenizer,
    device,
    question,
    answer_b
)

출력:

print(
    f"A: {score_a:.4f}"
)

print(
    f"B: {score_b:.4f}"
)

판정:

winner = (
    "A"
    if score_a > score_b
    else "B"
)


print(
    f"Reward Model 선택: {winner}"
)

45. 여러 답변 Ranking

Reward Model의 진짜 재미는 여기서 시작됩니다.

candidate_answers = [
    (
        "리스트는 여러 값을 "
        "순서대로 저장하는 자료형입니다."
    ),

    (
        "리스트는 숫자 하나만 "
        "저장할 수 있습니다."
    ),

    (
        "리스트는 여러 값을 순서대로 저장하며 "
        "생성 후 값을 추가하거나 수정할 수 있습니다."
    ),

    (
        "잘 모르겠습니다."
    )
]

점수 계산:

scored_answers = []


for answer in candidate_answers:
    score = score_response(
        reward_model,
        tokenizer,
        device,
        question,
        answer
    )

    scored_answers.append(
        {
            "answer": answer,
            "score": score
        }
    )

정렬:

ranked_answers = sorted(
    scored_answers,
    key=lambda item: item["score"],
    reverse=True
)

출력:

for rank, item in enumerate(
    ranked_answers,
    start=1
):
    print(
        f"{rank}위"
    )

    print(
        f"Reward: "
        f"{item['score']:.4f}"
    )

    print(
        item["answer"]
    )

    print(
        "-" * 60
    )

46. Best-of-N Sampling 개념

생성 모델에게 하나의 답변만 생성시키는 대신 여러 답변을 생성할 수 있습니다.

Prompt

↓

Generator

├─ Candidate 1
├─ Candidate 2
├─ Candidate 3
├─ Candidate 4
└─ Candidate 5

↓

Reward Model

↓

가장 높은 Score 선택

이를 단순화하면 Best-of-N 방식이라고 볼 수 있습니다.

Generator:

“답변 후보 5개를 만들었습니다.”


Reward Model:

“3번이 가장 좋습니다.”


사용자:

3번 답변 수신

정책 모델을 다시 학습하지 않고도 생성 품질을 개선하는 방법으로 활용할 수 있습니다.

47. DPO와 Reward Model 비교

지난 편과 연결해 보겠습니다.

DPO

Preference Dataset

↓

생성 모델 자체 개선

Reward Model

Preference Dataset

↓

별도 채점 모델 생성
항목DPOReward Model
결과생성 모델평가 모델
출력텍스트Score
Reference 필요아니오
Pairwise 데이터필요필요
활용모델 정렬Ranking·RLHF
답변 생성가능목적 아님

48. PPO와 Reward Model 관계

Reward Model은 강화학습 파이프라인에서도 활용할 수 있습니다.

Prompt

↓

Policy Model

↓

Response

↓

Reward Model

↓

Reward

↓

Policy 업데이트

TRL의 PPOTrainer API도 Reward Model을 입력받아 생성된 Response를 평가하는 구조를 제공합니다.

즉:

Reward Model

=

강화학습의 시험 감독관

49. Reward Hacking이란?

Reward Model이 완벽하지 않으면 Policy Model이 실제 품질이 아니라 Reward Model의 약점을 공략할 수 있습니다.

이를 Reward Hacking 문제로 볼 수 있습니다.

예를 들어 Reward Model이 다음 패턴을 잘못 학습했다고 가정합니다.

긴 답변

→ 높은 Reward

그러면 Policy Model은 다음 행동을 할 수 있습니다.

질문:

2 + 2는?


답변:

2 + 2는 매우 흥미로운 수학적 질문으로
고대 수학의 발전 과정부터 살펴보면...

내용보다 길이를 늘려 Reward를 획득합니다.

Policy:

“정답을 잘해야 하나요?”


Reward Model:

“길면 점수를 많이 줍니다.”


Policy:

“말을 많이 하겠습니다.”

Reward Model을 공격적으로 최적화할수록 이런 오류가 커질 수 있으므로 독립적인 평가가 필요합니다.

50. QLoRA Reward Model

큰 Reward Model을 학습한다면 4비트 양자화와 LoRA를 결합할 수 있습니다.

import torch

from transformers import (
    BitsAndBytesConfig
)


compute_dtype = (
    torch.bfloat16
    if (
        torch.cuda.is_available()
        and torch.cuda.is_bf16_supported()
    )
    else torch.float16
)


quantization_config = (
    BitsAndBytesConfig(
        load_in_4bit=True,

        bnb_4bit_quant_type="nf4",

        bnb_4bit_use_double_quant=True,

        bnb_4bit_compute_dtype=(
            compute_dtype
        )
    )
)

RewardTrainer:

trainer = RewardTrainer(
    model=MODEL_ID,

    args=training_args,

    train_dataset=train_dataset,

    processing_class=tokenizer,

    peft_config=lora_config,

    quantization_config=(
        quantization_config
    )
)

현재 RewardTrainer의 `quantization_config`는 모델 ID에서 모델을 불러올 때 적용되며 `peft_config`와 함께 QLoRA 형태로 사용할 수 있습니다. 이미 모델 객체를 직접 생성해 전달했다면 이 인수는 무시됩니다.

51. Checkpoint 재개

가장 최근 Checkpoint:

trainer.train(
    resume_from_checkpoint=True
)

특정 Checkpoint:

trainer.train(
    resume_from_checkpoint=(
        "outputs/"
        "python-reward-model/"
        "checkpoint-100"
    )
)

RewardTrainer는 Trainer의 Checkpoint 재개 기능을 상속하며 모델, Optimizer와 Scheduler 상태를 복원해 학습을 이어갈 수 있습니다.

52. Hugging Face Hub 업로드

로그인:

hf auth login

RewardConfig:

training_args = RewardConfig(
    push_to_hub=True,

    hub_model_id=(
        "사용자명/"
        "python-reward-model"
    ),

    hub_private_repo=True,

    hub_strategy="end"
)

업로드:

trainer.push_to_hub(
    commit_message=(
        "Train Python "
        "answer reward model"
    )
)

RewardTrainer의 `push_to_hub()`은 모델과 Processing Class를 설정된 Hub Repository에 업로드합니다.

53. Gradio AI 답변 심사위원

Reward Model을 웹 화면으로 만들어 보겠습니다.

import gradio as gr


def judge_answers(
    question: str,
    answer_a: str,
    answer_b: str
):
    question = question.strip()
    answer_a = answer_a.strip()
    answer_b = answer_b.strip()

    if not question:
        raise gr.Error(
            "질문을 입력해 주세요."
        )

    if not answer_a:
        raise gr.Error(
            "답변 A를 입력해 주세요."
        )

    if not answer_b:
        raise gr.Error(
            "답변 B를 입력해 주세요."
        )

    score_a = score_response(
        reward_model,
        tokenizer,
        device,
        question,
        answer_a
    )

    score_b = score_response(
        reward_model,
        tokenizer,
        device,
        question,
        answer_b
    )

    margin = (
        score_a
        - score_b
    )

    if score_a > score_b:
        winner = "🏆 답변 A"

    elif score_b > score_a:
        winner = "🏆 답변 B"

    else:
        winner = "🤝 동점"

    return (
        f"{score_a:.4f}",
        f"{score_b:.4f}",
        f"{margin:.4f}",
        winner
    )


with gr.Blocks() as demo:
    gr.Markdown(
        """
        # ⚖️ AI 답변 Reward 심사위원

        같은 질문에 대한 두 답변을 입력하면
        Reward Model이 점수를 비교합니다.
        """
    )

    question_input = gr.Textbox(
        label="질문",
        lines=3
    )

    with gr.Row():
        answer_a_input = gr.Textbox(
            label="답변 A",
            lines=10
        )

        answer_b_input = gr.Textbox(
            label="답변 B",
            lines=10
        )

    judge_button = gr.Button(
        "⚖️ 채점하기",
        variant="primary"
    )

    with gr.Row():
        score_a_output = gr.Textbox(
            label="A Reward"
        )

        score_b_output = gr.Textbox(
            label="B Reward"
        )

        margin_output = gr.Textbox(
            label="A - B Margin"
        )

    winner_output = gr.Textbox(
        label="판정"
    )

    judge_button.click(
        fn=judge_answers,

        inputs=[
            question_input,
            answer_a_input,
            answer_b_input
        ],

        outputs=[
            score_a_output,
            score_b_output,
            margin_output,
            winner_output
        ]
    )


demo.launch()

54. 실전 프로젝트 전체 코드

다음 내용을 `train_reward.py`로 저장합니다.

````pythonfrom pathlib import Pathimport jsonimport time

import torch

from datasets import Dataset

from peft import (AutoPeftModelForSequenceClassification,LoraConfig,TaskType)

from transformers import (AutoTokenizer,set_seed)

from trl import (RewardConfig,RewardTrainer)

MODEL_ID = ("Qwen/""Qwen2.5-0.5B-Instruct")

OUTPUT_DIR = Path("outputs/""python-reward-model")

REWARD_MODEL_DIR = Path("models/""python-reward-model-lora")

SEED = 2026

SYSTEM_MESSAGE = ("당신은 Python 입문자를 위한 ""친절하고 정확한 교관입니다. ""핵심부터 설명하고 필요한 경우 ""짧은 코드 예제를 제공합니다.")

PREFERENCE_DATA = [{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 변수란 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("변수는 프로그램에서 값을 저장하기 위해 ""사용하는 이름입니다.\n\n""```python\n""age = 20\n""print(age)\n""```\n\n""이 코드에서 age는 숫자 20을 ""가리키는 변수입니다.")}],"rejected": [{"role": "assistant","content": ("변수는 Python을 설치하는 기능입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("리스트와 튜플의 차이를 ""설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("리스트와 튜플은 여러 값을 ""순서대로 저장합니다.\n\n""리스트는 생성 후 값을 변경할 수 있지만 ""튜플은 일반적으로 변경할 수 없습니다.\n\n""변경이 필요한 데이터에는 리스트, ""고정된 데이터에는 튜플을 고려할 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("리스트와 튜플은 완전히 같습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("for 반복문을 예제로 ""설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("for 반복문은 여러 데이터를 하나씩 꺼내 ""같은 작업을 반복할 때 사용합니다.\n\n""```python\n""fruits = ['사과', '바나나', '포도']\n\n""for fruit in fruits:\n"" print(fruit)\n""```")}],"rejected": [{"role": "assistant","content": ("for는 반복입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("while 반복문에서 ""주의할 점은 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("while 반복문에서는 조건이 언젠가 ""거짓이 되도록 상태를 변경해야 합니다.\n\n""```python\n""count = 0\n\n""while count < 3:\n"" print(count)\n"" count += 1\n""```\n\n""상태가 변하지 않으면 무한 반복이 ""발생할 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("while 반복문은 한번 시작하면 ""절대로 종료할 수 없습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("함수의 return은 어떤 역할을 하나요?")}],"chosen": [{"role": "assistant","content": ("return은 함수가 만든 결과를 호출한 곳으로 ""전달하고 함수 실행을 종료합니다.\n\n""```python\n""def add(a, b):\n"" return a + b\n\n""result = add(3, 5)\n""```")}],"rejected": [{"role": "assistant","content": ("return은 화면에 문자를 ""출력하는 명령입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("딕셔너리는 어떤 자료형인가요?")}],"chosen": [{"role": "assistant","content": ("딕셔너리는 Key와 Value를 한 쌍으로 ""저장하는 자료형입니다.\n\n""```python\n""user = {\n"" 'name': '홍길동',\n"" 'age': 20\n""}\n""```")}],"rejected": [{"role": "assistant","content": ("딕셔너리는 영어 단어를 ""번역하는 기능입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("try-except는 언제 사용하나요?")}],"chosen": [{"role": "assistant","content": ("try-except는 실행 중 발생할 수 있는 ""예외를 처리할 때 사용합니다.\n\n""```python\n""try:\n"" number = int(input('숫자: '))\n""except ValueError:\n"" print('숫자를 입력하세요.')\n""```")}],"rejected": [{"role": "assistant","content": ("모든 코드를 try-except로 감싸면 ""모든 버그가 자동으로 수정됩니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("None을 어떻게 비교하나요?")}],"chosen": [{"role": "assistant","content": ("None은 일반적으로 is 또는 ""is not으로 비교합니다.\n\n""```python\n""result = None\n\n""if result is None:\n"" print('결과 없음')\n""```")}],"rejected": [{"role": "assistant","content": ("None은 항상 숫자 0과 같습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("가상환경은 왜 사용하나요?")}],"chosen": [{"role": "assistant","content": ("가상환경은 프로젝트마다 Python 패키지와 ""버전을 독립적으로 관리하기 위해 사용합니다.\n\n""```bash\n""python -m venv venv\n""```")}],"rejected": [{"role": "assistant","content": ("가상환경을 사용하면 CPU 속도가 ""자동으로 두 배가 됩니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("파일을 안전하게 열고 ""닫는 방법을 알려 주세요.")}],"chosen": [{"role": "assistant","content": ("with 문을 사용하면 작업이 끝난 뒤 ""파일을 자동으로 닫을 수 있습니다.\n\n""```python\n""with open('data.txt', 'r', encoding='utf-8') as file:\n"" content = file.read()\n""```")}],"rejected": [{"role": "assistant","content": ("파일은 절대로 닫을 필요가 없습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("set 자료형의 특징은 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("set은 중복되지 않는 값을 저장하는 ""집합 자료형입니다.\n\n""```python\n""numbers = {1, 2, 2, 3}\n""print(numbers)\n""```\n\n""중복 제거와 집합 연산에 유용합니다.")}],"rejected": [{"role": "assistant","content": ("set은 리스트와 동일하며 ""중복 값도 그대로 저장합니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("클래스와 객체의 관계를 ""쉽게 설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("클래스는 객체를 만들기 위한 설계도이고 ""객체는 그 설계도로 만든 실제 결과물입니다.\n\n""```python\n""class Dog:\n"" pass\n\n""dog = Dog()\n""```")}],"rejected": [{"role": "assistant","content": ("클래스와 객체는 이름만 다르고 ""완전히 같은 것입니다.")}]}]

def validate_example(example: dict,index: int) -> None:required_keys = {"prompt","chosen","rejected"}

missing = (required_keys- set(example.keys()))

if missing:raise ValueError(f"{index}번 데이터 누락: "f"{sorted(missing)}")

for column_name in ("prompt","chosen","rejected"):messages = (example[column_name])

if not isinstance(messages,list) or not messages:raise ValueError(f"{index}번 "f"{column_name}이 ""올바르지 않습니다.")

for message in messages:content = str(message.get("content","")).strip()

if not content:raise ValueError(f"{index}번 데이터에 ""빈 메시지가 있습니다.")

chosen_text = (example["chosen"][-1]["content"].strip())

rejected_text = (example["rejected"][-1]["content"].strip())

if chosen_text == rejected_text:raise ValueError(f"{index}번 Chosen과 ""Rejected가 같습니다.")

def select_precision():if not torch.cuda.is_available():return (False,False,torch.float32)

if torch.cuda.is_bf16_supported():return (True,False,torch.bfloat16)

return (False,True,torch.float16)

def select_device():if torch.cuda.is_available():return torch.device("cuda")

if torch.backends.mps.is_available():return torch.device("mps")

return torch.device("cpu")

def score_response(model,tokenizer,device,question: str,response: str) -> float:messages = [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": question},{"role": "assistant","content": response}]

input_ids = (tokenizer.apply_chat_template(messages,tokenize=True,add_generation_prompt=False,return_tensors="pt").to(device))

with torch.inference_mode():outputs = model(input_ids=input_ids)

return (outputs.logits.squeeze().float().item())

def main() -> None:set_seed(SEED)

OUTPUT_DIR.mkdir(parents=True,exist_ok=True)

REWARD_MODEL_DIR.mkdir(parents=True,exist_ok=True)

print("[1/9] Preference 데이터 검증")

for index, example in enumerate(PREFERENCE_DATA):validate_example(example,index)

print(f"Preference Pair: "f"{len(PREFERENCE_DATA)}개")

print("[2/9] Dataset 생성")

dataset = Dataset.from_list(PREFERENCE_DATA)

split_dataset = (dataset.train_test_split(test_size=0.25,seed=SEED,shuffle=True))

train_dataset = (split_dataset["train"])

validation_dataset = (split_dataset["test"])

print(f"Train: "f"{len(train_dataset)}")

print(f"Validation: "f"{len(validation_dataset)}")

print("[3/9] Tokenizer 로딩")

tokenizer = (AutoTokenizer.from_pretrained(MODEL_ID))

if tokenizer.pad_token is None:tokenizer.pad_token = (tokenizer.eos_token)

print("[4/9] LoRA 설정")

lora_config = LoraConfig(task_type=TaskType.SEQ_CLS,

r=16,lora_alpha=32,lora_dropout=0.05,

target_modules=["q_proj","k_proj","v_proj","o_proj"],

modules_to_save=["score"],

bias="none")

(use_bf16,use_fp16,model_dtype) = select_precision()

print("[5/9] RewardConfig 생성")

training_args = RewardConfig(output_dir=str(OUTPUT_DIR),

model_init_kwargs={"dtype": model_dtype},

learning_rate=5e-4,

lr_scheduler_type="linear",warmup_ratio=0.1,

per_device_train_batch_size=2,per_device_eval_batch_size=2,

gradient_accumulation_steps=4,

num_train_epochs=3,

max_length=512,

eval_strategy="epoch",save_strategy="epoch",

logging_strategy="steps",logging_steps=2,logging_first_step=True,

load_best_model_at_end=True,metric_for_best_model=("eval_loss"),greater_is_better=False,

save_total_limit=2,

center_rewards_coefficient=0.01,

bf16=use_bf16,fp16=use_fp16,

report_to="none",

seed=SEED,data_seed=SEED,

run_name=("python-reward-model"))

print("[6/9] RewardTrainer 생성")

trainer = RewardTrainer(model=MODEL_ID,

args=training_args,

train_dataset=train_dataset,eval_dataset=(validation_dataset),

processing_class=tokenizer,

peft_config=lora_config)

print("\n[학습 가능한 파라미터]")

trainer.model.print_trainable_parameters()

print("[7/9] Reward Model 학습")

start_time = (time.perf_counter())

train_result = (trainer.train())

elapsed_seconds = (time.perf_counter()- start_time)

train_result.metrics["elapsed_seconds"] = elapsed_seconds

trainer.log_metrics("train",train_result.metrics)

trainer.save_metrics("train",train_result.metrics)

trainer.save_state()

print("[8/9] Validation 평가")

validation_metrics = (trainer.evaluate())

trainer.log_metrics("validation",validation_metrics)

trainer.save_metrics("validation",validation_metrics)

print("[9/9] Reward Model 저장")

trainer.save_model(str(REWARD_MODEL_DIR))

tokenizer.save_pretrained(REWARD_MODEL_DIR)

summary = {"model_id": MODEL_ID,"train_pairs": len(train_dataset),"validation_pairs": len(validation_dataset),"elapsed_seconds": (elapsed_seconds),"best_checkpoint": (trainer.state.best_model_checkpoint),"best_metric": (trainer.state.best_metric)}

(OUTPUT_DIR/ "training_summary.json").write_text(json.dumps(summary,ensure_ascii=False,indent=2),encoding="utf-8")

del trainer

if torch.cuda.is_available():torch.cuda.empty_cache()

print("\n[Reward Model 재로딩]")

reward_model = (AutoPeftModelForSequenceClassification.from_pretrained(REWARD_MODEL_DIR))

device = select_device()

reward_model = reward_model.to(device)

reward_model.eval()

question = ("Python의 리스트와 튜플 ""차이를 설명해 주세요.")

candidate_answers = [("리스트는 값을 변경할 수 있고 ""튜플은 일반적으로 값을 변경할 수 없습니다."),

("리스트와 튜플은 완전히 같습니다."),

("리스트와 튜플은 여러 값을 순서대로 저장합니다. ""리스트는 생성 후 값을 변경할 수 있지만 ""튜플은 일반적으로 변경할 수 없습니다. ""변경 가능한 데이터에는 리스트, ""고정된 데이터에는 튜플을 고려할 수 있습니다.")]

scored_answers = []

for answer in candidate_answers:score = score_response(reward_model,tokenizer,device,question,answer)

scored_answers.append({"answer": answer,"score": score})

ranked_answers = sorted(scored_answers,key=lambda item: (item["score"]),reverse=True)

print("\n[답변 Ranking]")

for rank, item in enumerate(ranked_answers,start=1):print(f"\n{rank}위")

print(f"Reward: "f"{item['score']:.4f}")

print(item["answer"])

if __name__ == "__main__":main()````

55. 결과 파일 구조

reward_model_project/
├─ train_reward.py
│
├─ outputs/
│  └─ python-reward-model/
│     ├─ checkpoint-...
│     ├─ train_results.json
│     ├─ validation_results.json
│     ├─ trainer_state.json
│     └─ training_summary.json
│
└─ models/
   └─ python-reward-model-lora/
      ├─ adapter_config.json
      ├─ adapter_model.safetensors
      ├─ tokenizer.json
      └─ tokenizer_config.json

56. Reward Model 평가 방법

Reward Model은 단순 Training Loss만 평가하면 부족합니다.

Pairwise Accuracy

Chosen을 Rejected보다
높게 평가하는 비율

Reward Margin

Chosen Reward
-
Rejected Reward

새로운 Domain 평가

학습에 없던 질문으로 평가합니다.

Hard Negative 평가

Rejected가 완전히 틀린 답변이 아니라 꽤 그럴듯한 답변인 Pair를 사용합니다.

Chosen:

정확하고 충분한 답변


Rejected:

90%는 맞지만 중요한 오류 하나 포함

Ranking Correlation

사람이 정한 여러 답변 순위와 Reward Model 순위를 비교할 수 있습니다.

Blind Human Evaluation

Reward Model 결과를 숨긴 상태에서 사람이 직접 Ranking합니다.

57. 데이터 편향과 보안

Reward Model은 사람이 만든 Preference를 학습합니다.

따라서 사람의 편향도 배울 수 있습니다.

항상 긴 답변을 Chosen

→ 장문 선호


전문용어가 많은 답변을 Chosen

→ 어려운 답변 선호


특정 문체만 Chosen

→ 문체 편향


특정 관점만 Chosen

→ 관점 편향

평가 기준을 명확하게 문서화해야 합니다.

예:

1. 사실 정확성이 가장 중요하다.

2. 질문에 직접 답해야 한다.

3. 필요한 설명은 충분히 제공한다.

4. 불필요한 반복은 피한다.

5. 코드는 실제로 실행 가능해야 한다.

6. 모르는 정보는 추측하지 않는다.

7. 답변 길이 자체는 평가 기준으로 사용하지 않는다.

또한 Preference Dataset에 고객 정보, 내부 문서, 소스코드, 개인정보 등이 포함되어 있다면 외부 Hub 업로드 전에 반드시 조직의 보안·개인정보 정책을 확인해야 합니다.

58. 자주 발생하는 오류

오류 1. RewardTrainer를 찾을 수 없음

ImportError:
cannot import name 'RewardTrainer'

TRL 업데이트:

python -m pip install --upgrade trl

오류 2. Python 버전이 낮음

현재 TRL 1.9.2는 Python 3.10 이상을 요구합니다.

python --version

오류 3. Dataset 열이 없음

필요한 열:

chosen

rejected

Explicit Prompt에서는:

prompt

chosen

rejected

확인:

print(
    dataset.column_names
)

오류 4. Chat Template이 없음

Conversational Dataset에서는 Tokenizer의 Chat Template이 필요합니다.

print(
    tokenizer.chat_template
)

필요하면 RewardConfig의 `chat_template_path`를 설정할 수 있습니다. 현재 RewardConfig는 Hub Tokenizer 경로나 로컬 Jinja Template을 지정하는 옵션을 제공합니다.

오류 5. Padding Token 없음

if tokenizer.pad_token is None:
    tokenizer.pad_token = (
        tokenizer.eos_token
    )

오류 6. Reward 출력이 2개 이상임

Reward Model은 일반적으로:

num_labels=1

을 사용합니다.

모델을 직접 생성했다면 확인합니다.

print(
    model.config.num_labels
)

오류 7. score Head가 저장되지 않음

LoRA 설정:

modules_to_save=[
    "score"
]

Reward Head를 함께 학습·저장해야 합니다.

오류 8. Target Module을 찾지 못함

Target modules not found

모델 구조를 확인합니다.

for name, module in (
    model.named_modules()
):
    print(name)

모델마다 다음 이름이 다를 수 있습니다.

q_proj

q_lin

query

c_attn

오류 9. CUDA Out of Memory

대응:

Batch Size 감소

Gradient Accumulation 증가

max_length 감소

LoRA 사용

Rank 감소

QLoRA 적용

Gradient Checkpointing

오류 10. Accuracy가 0.5 부근에서 멈춤

가능한 원인:

Preference Pair가 애매함

Chosen·Rejected가 잘못됨

데이터가 너무 적음

Learning Rate 부족

Target Module 부족

학습 Epoch 부족

오류 11. Train Accuracy는 높은데 Validation이 낮음

과적합 가능성:

데이터 추가

중복 제거

Epoch 감소

LoRA Rank 감소

더 어려운 Preference Pair 추가

오류 12. Reward가 계속 커짐

Reward 절대값 자체가 중요한 것은 아닙니다.

Centering 사용을 검토합니다.

center_rewards_coefficient=0.01

오류 13. 좋은 답변도 음수 Reward

문제가 아닐 수 있습니다.

Chosen:
-0.5

Rejected:
-3.2

중요한 것은:

-0.5 > -3.2

입니다.

오류 14. 모든 답변의 점수가 거의 같음

0.12
0.13
0.11
0.12

가능한 원인:

모델 학습 부족

Preference Signal 부족

Reward Head 미학습

LoRA Adapter 미적용

데이터 품질 문제

오류 15. 긴 답변만 높은 점수

Length Bias 가능성이 있습니다.

Training Dataset의 Chosen과 Rejected Token 길이를 분석합니다.

chosen_lengths = []
rejected_lengths = []

길이 자체가 Preference Label과 강하게 연결되어 있지 않은지 확인합니다.

오류 16. 코드가 틀렸는데 Reward가 높음

Reward Model은 Python Interpreter가 아닙니다.

문장이 자연스럽다

≠

코드가 실행된다

코드 평가에는 별도의 실행 기반 검증을 함께 사용해야 합니다.

오류 17. 새로운 주제에서는 점수가 이상함

Training Domain 밖의 문제일 수 있습니다.

Python 데이터로 학습

↓

의료 상담 평가

↓

신뢰하기 어려움

Reward Model도 일반화 한계를 가집니다.

오류 18. Reward Model을 생성 모델처럼 사용함

잘못된 목적:

model.generate(...)

Reward Model은 일반적으로 텍스트 생성이 목적이 아닙니다.

outputs = model(
    input_ids=input_ids
)


score = (
    outputs.logits
    .squeeze()
)

59. 연습 문제

문제 1

Reward Model과 일반 생성 모델의 차이를 설명하세요.

문제 2

다음 Pair를 Reward Dataset으로 작성하세요.

질문:
Python 함수란 무엇인가요?

Chosen:
특정 작업을 묶어 재사용하는 코드 블록입니다.

Rejected:
함수는 Python 파일을 삭제하는 명령입니다.

문제 3

같은 데이터를 Conversational Preference Format으로 변경하세요.

문제 4

Chosen과 Rejected가 같은 데이터를 찾아 제거하세요.

문제 5

RewardConfig에 다음 설정을 적용하세요.

Learning Rate:
5e-4

Epoch:
3

Max Length:
512

Center Reward:
0.01

문제 6

LoRA에 다음 Target Module을 사용하세요.

q_proj

k_proj

v_proj

o_proj

문제 7

`score` Head를 Adapter에 함께 저장하세요.

문제 8

학습 후 다음 값을 비교하세요.

Train Accuracy

Validation Accuracy

Train Margin

Validation Margin

문제 9

두 답변에 Reward Score를 출력하세요.

문제 10

10개의 Candidate Response를 Reward Score 순서로 정렬하세요.

문제 11

Chosen의 평균 Reward와 Rejected의 평균 Reward를 계산하세요.

문제 12

Test Dataset에서 Pairwise Accuracy를 직접 계산하세요.

문제 13

Chosen·Rejected의 평균 Token 길이를 비교하세요.

문제 14

Reward와 답변 길이의 상관관계를 조사하세요.

문제 15

LoRA와 Full Fine-tuning Reward Model의 성능과 GPU 메모리를 비교하세요.

문제 16

QLoRA를 사용한 RewardTrainer를 구성하세요.

문제 17

Checkpoint에서 학습을 재개하세요.

문제 18

Reward Adapter를 Hub의 Private Repository에 업로드하세요.

문제 19

Gradio에서 답변 두 개를 입력받아 Reward를 비교하세요.

문제 20

다음 Best-of-N 시스템을 구현하세요.

Prompt

→ 생성 모델이 5개 답변 생성

→ Reward Model이 모두 채점

→ 최고 점수 답변 선택

→ 사용자에게 반환

60. 핵심 요약

Reward Dataset

example = {
    "prompt": "질문",
    "chosen": "좋은 답변",
    "rejected": "나쁜 답변"
}

RewardConfig

training_args = RewardConfig(
    output_dir="outputs/reward",

    learning_rate=5e-4,

    per_device_train_batch_size=2,

    num_train_epochs=3,

    max_length=512,

    center_rewards_coefficient=0.01,

    eval_strategy="epoch",
    save_strategy="epoch"
)

LoRA

lora_config = LoraConfig(
    task_type=TaskType.SEQ_CLS,

    r=16,
    lora_alpha=32,

    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj"
    ],

    modules_to_save=[
        "score"
    ]
)

RewardTrainer

trainer = RewardTrainer(
    model=MODEL_ID,

    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config
)

학습

trainer.train()

평가

metrics = (
    trainer.evaluate()
)

저장

trainer.save_model(
    "reward_adapter"
)

Reward Model 로딩

model = (
    AutoPeftModelForSequenceClassification
    .from_pretrained(
        "reward_adapter"
    )
)

Reward 계산

outputs = model(
    input_ids=input_ids
)


reward = (
    outputs.logits
    .squeeze()
    .item()
)

Ranking

ranked = sorted(
    answers,
    key=lambda item: item["score"],
    reverse=True
)

61. 마무리

이번 시간에는 Hugging Face TRL의 `RewardTrainer`를 이용해 AI 답변의 품질을 숫자로 평가하는 Reward Model을 만들어 보았습니다.

전체 과정을 다시 정리하겠습니다.

Preference Dataset 준비

→ Prompt 작성

→ Chosen·Rejected 구성

→ Chat Template 적용

→ Reward Model 생성

→ Chosen Reward 계산

→ Rejected Reward 계산

→ Reward Margin 계산

→ Pairwise Loss

→ Reward Model 학습

→ 새로운 답변 채점

→ 여러 답변 Ranking

Reward Model의 가장 중요한 목표는 다음과 같습니다.

정확한 절대 점수 예측

이 아니라

좋은 답변을
나쁜 답변보다 높게 평가하기

즉 다음 숫자 자체는 크게 중요하지 않습니다.

Chosen:
37.2

Rejected:
35.1

다음 관계가 중요합니다.

37.2 > 35.1

그리고 더 직접적으로 확인할 수 있는 것이 Reward Margin입니다.

Margin

=

Reward(Chosen)

-

Reward(Rejected)

RewardTrainer는 이 Preference 구조를 이용해 사람의 선호 패턴을 하나의 점수 함수로 압축합니다.

사람:

“이 답변이 저 답변보다 좋아요.”


Preference Dataset:

Chosen > Rejected


RewardTrainer:

“그 차이를 학습하겠습니다.”


Reward Model:

“새 답변도 제가 채점해 보겠습니다.”

하지만 Reward Model은 진실 판독기가 아닙니다.

높은 Reward

≠

절대적으로 정확함

Reward Model 역시 자신이 학습한 평가 기준의 범위 안에서 판단합니다.

Training Dataset이 이런 구조라면:

긴 답변

→ 항상 Chosen

모델은 다음 규칙을 배울 수도 있습니다.

길면 좋다.

Training Dataset이 이런 구조라면:

전문용어가 많은 답변

→ 항상 Chosen

다음 기준을 배울 수도 있습니다.

어려운 말이 많으면 좋다.

따라서 Reward Model을 만들 때 가장 중요한 것은 RewardTrainer의 코드보다 평가 기준을 얼마나 명확하게 정의했는가입니다.

정확성

관련성

완전성

간결성

형식 준수

실행 가능성

안전성

이 기준을 데이터에 일관되게 반영해야 합니다.

잘 만든 Reward Model은 여러 후보를 빠르게 평가할 수 있습니다.

Generator:

“답변 후보 10개를 만들었습니다.”


Reward Model:

“채점 완료했습니다.”


1위:
3.81

2위:
2.96

3위:
1.77

그리고 여기서 한 단계 더 나아가면 Reward Model의 점수를 이용해 생성 모델 자체를 강화학습으로 개선할 수 있습니다.

Reward Model은 선수가 아닙니다.

하지만 어떤 선수가 잘하고 있는지를 판단하는 심판입니다.

SFTTrainer

→ 기본기를 가르치는 코치


RewardTrainer

→ 경기 점수를 매기는 심판


PPOTrainer

→ 점수를 보고 선수를 다시 훈련하는 감독

이제 AI 훈련장에 학생, 심판, 채점표가 모두 준비되었습니다. 🏟️🤖📋

다음 편 예고

[Python 완전정복 시리즈 #42] PPOTrainer 완벽 이해하기 | Reward Model의 점수를 이용해 AI를 강화학습시키는 방법

다음 시간에는 지금 만든 Reward Model을 실제 강화학습 루프에 연결합니다.

Prompt 생성, Policy Model, Reference Model, Reward Model, Value Model, KL Penalty, Advantage, PPO Update, Reward 추적, LoRA·QLoRA, Checkpoint, 학습 전후 답변 비교까지 하나의 RLHF 파이프라인으로 연결해 보겠습니다.

SFT

↓

Reward Model

↓

PPO

↓

RLHF

드디어 생성형 AI가 점수를 보고 스스로 답변 전략을 수정하는 단계로 진입합니다.

#Python #파이썬 #Python강좌 #HuggingFace #TRL #RewardTrainer #RewardModel #RewardModeling #RLHF #PreferenceLearning #PreferenceDataset #Chosen #Rejected #RewardScore #RewardMargin #PairwiseLearning #BradleyTerry #LoRA #QLoRA #PEFT #Transformers #AI정렬 #생성형AI #LLM #PPO #BestOfN #AI평가 #Gradio #PyTorch #FineTuning #코딩공부 #프로그래밍

댓글

0

댓글을 불러오는 중입니다.