목록으로

프로그래밍 · Python

Python 완전정복 시리즈 39: TRL과 SFTTrainer 완벽 이해하기

BeanCon
Python에서 Hugging Face TRL과 SFTTrainer로 대화형 AI 모델을 Fine-tuning하는 방법을 설명하는 대표 이미지

Python 완전정복 시리즈 39편입니다. Hugging Face TRL과 SFTTrainer로 대화형 AI 모델을 Instruction Dataset으로 Fine-tuning하는 방법을 정리했습니다. SFT와 Instruction Tuning, Base Model과 Instruct Model, Standard Format, Prompt-Completion Dataset, Conversational Dataset, System·User·Assistant 역할, Chat Template, apply_chat_template, EOS Token, Completion Only Loss, Assistant Only Loss, Loss Masking, Packing, SFTConfig, LoRA·QLoRA 연결, Adapter 저장·병합, Hub 업로드, TRL CLI, Gradio 챗봇 연결과 오류 해결까지 다룹니다.

목차

메타 설명
대화형 AI 모델은 어떻게 사용자의 지시를 이해하고 답변하는 방법을 배울까요? Hugging Face TRL과 SFTTrainer를 이용해 Instruction Dataset, Conversational Dataset, Chat Template, Completion Only Loss, Assistant Only Loss, Packing, LoRA·QLoRA, Checkpoint, 모델 평가, 추론과 Gradio 연결 방법까지 실습합니다.

지난 시간에는 PEFT와 LoRA를 이용해 거대한 모델 전체를 학습하지 않고 작은 Adapter만 학습하는 방법을 알아보았습니다.

기본 모델

→ 가중치 동결

→ LoRA Adapter 추가

→ Adapter만 학습

→ 작은 파일로 저장

덕분에 GPU 메모리와 저장 공간을 크게 줄일 수 있었습니다.

하지만 아직 중요한 질문이 남아 있습니다.

LoRA를 이용해

무엇을 가르칠 것인가?

모델에게 질문을 이해하고 답변하는 방법을 가르치고 싶다면 다음과 같은 데이터가 필요합니다.

사용자:
Python의 리스트와 튜플 차이를 설명해 주세요.

AI:
리스트는 값을 변경할 수 있지만
튜플은 생성 후 값을 변경할 수 없습니다.

또는 다음과 같은 지시 데이터가 필요합니다.

지시:
입력된 문장을 세 줄로 요약하세요.

입력:
Python은 다양한 분야에서 사용되는 프로그래밍 언어입니다...

정답:
Python은 읽기 쉬운 범용 프로그래밍 언어입니다.
데이터 분석과 웹 개발 등에 활용됩니다.
풍부한 라이브러리 생태계를 제공합니다.

이런 데이터를 모델에 반복적으로 보여주면 모델은 다음 패턴을 학습합니다.

사용자 지시를 확인한다.

→ 필요한 작업을 파악한다.

→ 적절한 답변 형식을 선택한다.

→ 모범 답안과 비슷한 응답을 생성한다.

이 과정을 Supervised Fine-Tuning, 줄여서 SFT라고 부릅니다.

이번 시간의 주인공은 Hugging Face TRL의 `SFTTrainer`입니다.

Transformers Trainer

→ 범용 모델 학습 교관


TRL SFTTrainer

→ 생성형 AI와 대화형 모델을 위한
  전문 교육 교관

AI 훈련소에 질문지와 모범 답안이 도착했습니다.

이제 모델에게 “말은 할 줄 알지만, 대답하는 법은 아직 서툰 상태”에서 벗어나는 방법을 가르쳐 보겠습니다. 🎓🤖

1. TRL이란?

TRL은 Hugging Face가 제공하는 Foundation Model Post-training 라이브러리입니다.

이름은 다음 표현에서 출발했습니다.

Transformers Reinforcement Learning

초기에는 강화학습 중심 도구였지만 현재는 SFT, DPO, GRPO, Reward Modeling 등 Foundation Model의 다양한 후처리 학습 방법을 제공하는 종합 Post-training 라이브러리로 확장되었습니다. Transformers, Accelerate, PEFT와 통합되어 단일 GPU부터 분산 학습 환경까지 연결할 수 있습니다.

대표 Trainer는 다음과 같습니다.

SFTTrainer
→ 모범 답안을 이용한 지도 학습

DPOTrainer
→ 선호 답변과 비선호 답변 비교 학습

GRPOTrainer
→ 보상 함수 기반 강화학습

RewardTrainer
→ 답변 품질을 평가하는 보상 모델 학습

KTOTrainer
→ 선호·비선호 단일 피드백 기반 학습

이번 편에서는 Post-training의 출발점인 `SFTTrainer`를 사용합니다.

2. SFT란?

SFT는 다음 표현의 약자입니다.

Supervised Fine-Tuning

한국어로는 다음처럼 표현할 수 있습니다.

지도 미세조정

또는

지도 학습 기반 Fine-tuning

SFT에서는 모델에게 입력과 정답 출력을 함께 제공합니다.

입력:
REST API란 무엇인가요?

정답:
REST API는 HTTP를 이용해
자원을 주고받는 인터페이스 설계 방식입니다.

모델은 정답 문장의 다음 Token을 예측하도록 학습합니다.

REST

→ REST API

→ REST API는

→ REST API는 HTTP를

→ REST API는 HTTP를 이용해

SFTTrainer의 기본 학습 목표는 이전 Token들을 바탕으로 다음 정답 Token의 확률을 높이는 Token 단위 음의 로그 우도 손실입니다. Padding이나 학습에서 제외한 Token에는 `-100`과 같은 Ignore Index가 적용되어 Loss 계산에서 빠집니다.

3. Pre-training과 SFT의 차이

Pre-training

방대한 일반 문서를 이용해 언어 패턴을 학습합니다.

책

웹 문서

소스코드

뉴스

백과사전

논문

학습 목표는 대체로 다음 Token을 예측하는 것입니다.

Python은 배우기

→ 쉬운

→ 프로그래밍

→ 언어입니다.

Pre-training을 마친 모델을 Base Model이라고 부릅니다.

Base Model은 언어를 이어 쓰는 능력은 있지만 사용자의 지시를 안정적으로 따르지 못할 수 있습니다.

SFT

질문과 모범 답안을 이용해 원하는 행동을 추가로 가르칩니다.

질문에 답하기

문서 요약하기

코드 설명하기

표 형식으로 출력하기

특정 말투 유지하기

도구 호출 형식 생성하기

전체 관계는 다음과 같습니다.

Pre-training

→ 언어와 지식의 기본 패턴 학습

→ Base Model


SFT

→ 지시와 대화 방식 학습

→ Instruct·Chat Model

4. Instruction Tuning이란?

Instruction Tuning은 다양한 자연어 지시와 정답을 이용해 모델이 새로운 지시도 이해하고 수행하도록 학습하는 과정입니다.

지시:
이 문장을 영어로 번역하세요.

지시:
다음 내용을 세 줄로 요약하세요.

지시:
오류가 있는 Python 코드를 수정하세요.

지시:
초보자가 이해하도록 설명하세요.

단순한 업무 하나만 학습하는 것이 아니라 다양한 지시 형식을 함께 학습하면 모델은 새로운 요청에 대응하는 일반화 능력을 얻을 수 있습니다.

모델:
“이 문장을 학습한 적은 없지만
요청의 형태는 이해했습니다.”

다만 데이터가 지나치게 작거나 지시 유형이 편중되면 모델은 특정 문장 패턴만 외울 수 있습니다.

5. Chat Model이 학습되는 과정

대화형 모델도 내부에서는 Token Sequence를 이어 쓰는 Causal Language Model입니다.

개발자가 보는 대화는 다음과 같습니다.

messages = [
    {
        "role": "system",
        "content": "당신은 친절한 Python 교관입니다."
    },
    {
        "role": "user",
        "content": "리스트란 무엇인가요?"
    },
    {
        "role": "assistant",
        "content": "리스트는 여러 값을 순서대로 저장하는 자료형입니다."
    }
]

하지만 모델이 보는 것은 역할과 특수 Token이 포함된 하나의 Token Sequence입니다.

<system>
당신은 친절한 Python 교관입니다.
</system>

<user>
리스트란 무엇인가요?
</user>

<assistant>
리스트는 여러 값을 순서대로 저장하는 자료형입니다.
</assistant>

모델마다 역할을 표시하는 Control Token과 형식이 다릅니다. Chat Template은 `role`과 `content`로 구성된 메시지 목록을 모델이 학습한 Token 형식으로 변환합니다. 같은 Base Model에서 출발한 Chat Model도 서로 다른 Control Token을 사용할 수 있어 올바른 Template 적용이 중요합니다.

6. SFTTrainer란?

`SFTTrainer`는 생성형 언어 모델의 지도 Fine-tuning을 쉽게 구성하도록 만든 TRL의 Trainer입니다.

기본 사용법은 매우 간단합니다.

from datasets import load_dataset
from trl import SFTTrainer


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


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


trainer.train()

SFTTrainer는 Transformers의 `Trainer`를 기반으로 하며 데이터 형식 변환, Chat Template 적용, Tokenization, Packing, Completion Masking, PEFT와 Quantization 연결 등 생성형 모델 학습에 필요한 기능을 추가로 제공합니다.

7. Trainer와 SFTTrainer의 차이

구분TrainerSFTTrainer
주요 목적범용 Transformers 학습생성형 AI SFT
원본 대화 데이터직접 전처리 필요자동 처리 가능
Chat Template직접 적용Conversational Dataset에 자동 적용
Prompt·Completion직접 결합·Masking기본 지원
Completion Only Loss직접 구성 필요설정 지원
Assistant Only Loss직접 Mask 생성 필요설정 지원
Packing별도 구현내장 지원
PEFT연결 가능`peft_config` 직접 지원
QLoRA직접 모델 구성 가능Quantization과 PEFT 동시 지원
일반 분류 모델적합주로 Causal LM SFT 중심

Trainer가 종합 운동장이라면 SFTTrainer는 대화형 AI 전용 훈련관입니다.

8. 현재 TRL 버전

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

설치 버전 확인:

python -m pip show trl

Python에서 확인:

import trl


print(
    trl.__version__
)

TRL 1.x는 과거 0.x 버전과 API가 크게 달라질 수 있습니다.

오래된 블로그:

trl 0.7

현재 환경:

trl 1.9


개발자:

“같은 SFTTrainer인데
왜 인수가 다르죠?”

코드를 사용할 때 작성 시점과 설치 버전을 함께 확인해야 합니다.

9. 개발 환경 설치

Windows

python -m venv venv
venv\Scripts\activate

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

Windows 명령 프롬프트에서는 한 줄로 입력합니다.

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을 통합해 LoRA·QLoRA 학습을 구성할 수 있습니다. 현재 SFTTrainer는 모델 ID, `peft_config`, `quantization_config`를 함께 받아 모델 로딩과 Adapter 구성을 처리할 수 있습니다.

10. 전체 SFT 과정

이번 프로젝트의 학습 흐름입니다.

1. 대화형 학습 데이터 준비

2. Prompt와 Completion 분리

3. Train·Validation 분리

4. Tokenizer와 Chat Template 확인

5. LoRA 설정

6. SFTConfig 작성

7. SFTTrainer 생성

8. Chat Template 자동 적용

9. Tokenization

10. Completion Token만 Loss 계산

11. LoRA Adapter 학습

12. Validation Loss 확인

13. Adapter 저장

14. 대화형 추론

11. Base Model과 Instruct Model

생성형 모델은 크게 두 종류로 구분할 수 있습니다.

Base Model

일반적인 다음 Token 예측을 학습한 모델입니다.

Qwen Base

Llama Base

Mistral Base

Base Model은 자유로운 텍스트 생성을 할 수 있지만 지시를 따르는 능력은 약할 수 있습니다.

Base Model을 사용해 Instruction Model을 만들려면 대화 데이터와 Chat Template을 함께 학습해야 합니다.

Instruct Model

이미 Instruction Tuning을 거친 모델입니다.

Qwen Instruct

Llama Instruct

Mistral Instruct

기본적인 지시 수행 능력이 있으며, 특정 조직·업무·말투에 맞게 추가 SFT를 진행할 수 있습니다.

이번 프로젝트에서는 실행 부담이 비교적 작은 다음 모델을 사용합니다.

Qwen/Qwen2.5-0.5B-Instruct

해당 모델은 Transformers에서 `apply_chat_template()`를 이용한 대화형 생성 예제를 제공하는 Instruct Model입니다.

이미 대화할 줄 아는 신입사원

+ 우리 조직 업무 교육

= 업무 특화 AI

12. SFT 데이터 형식

SFTTrainer가 지원하는 주요 데이터 유형은 다음 두 가지입니다.

Language Modeling

Prompt-Completion

각 유형은 다시 두 형식으로 작성할 수 있습니다.

Standard Format

Conversational Format

SFTTrainer는 네 가지 조합을 지원합니다.

Standard Language Modeling

Conversational Language Modeling

Standard Prompt-Completion

Conversational Prompt-Completion

Conversational Dataset이 전달되면 SFTTrainer가 Chat Template을 자동으로 적용합니다.

13. Standard Format

Standard Format은 일반 문자열로 데이터를 작성합니다.

Language Modeling

example = {
    "text": (
        "Python의 리스트는 여러 값을 "
        "순서대로 저장하는 자료형입니다."
    )
}

Prompt-Completion

example = {
    "prompt": (
        "Python 리스트란 무엇인가요?"
    ),
    "completion": (
        "리스트는 여러 값을 순서대로 "
        "저장하는 변경 가능한 자료형입니다."
    )
}

Standard Format은 단순하지만 역할 정보를 직접 표현하기 어렵습니다.

14. Conversational Format

Conversational Format은 각 메시지를 `role`과 `content`로 작성합니다.

example = {
    "messages": [
        {
            "role": "system",
            "content": (
                "당신은 친절한 "
                "Python 교관입니다."
            )
        },
        {
            "role": "user",
            "content": (
                "리스트란 무엇인가요?"
            )
        },
        {
            "role": "assistant",
            "content": (
                "리스트는 여러 값을 "
                "순서대로 저장하는 자료형입니다."
            )
        }
    ]
}

Conversational Format에서 일반적으로 사용하는 역할은 다음과 같습니다.

system
→ 모델의 행동과 역할 정의

user
→ 사용자의 질문이나 지시

assistant
→ 모델의 정답 응답

tool
→ 도구 실행 결과

TRL의 Conversational Dataset은 각 메시지가 `role`과 `content`를 갖는 메시지 목록으로 구성됩니다.

15. Language Modeling Dataset

대화 전체를 `messages` 열에 저장할 수 있습니다.

example = {
    "messages": [
        {
            "role": "user",
            "content": (
                "Python의 딕셔너리는 "
                "무엇인가요?"
            )
        },
        {
            "role": "assistant",
            "content": (
                "딕셔너리는 Key와 Value를 "
                "한 쌍으로 저장하는 자료형입니다."
            )
        }
    ]
}

이 형식에서는 설정에 따라 전체 대화 Token이 Loss 계산에 포함될 수 있습니다.

사용자 질문 Token

+

AI 응답 Token

→ 모두 학습

모델이 대화 형식 전체를 학습하도록 만들 때 사용할 수 있습니다.

16. Prompt-Completion Dataset

Prompt와 Completion을 명확하게 분리합니다.

example = {
    "prompt": [
        {
            "role": "system",
            "content": (
                "당신은 초보자를 위한 "
                "Python 교관입니다."
            )
        },
        {
            "role": "user",
            "content": (
                "for 반복문을 설명해 주세요."
            )
        }
    ],
    "completion": [
        {
            "role": "assistant",
            "content": (
                "for 반복문은 여러 데이터를 "
                "하나씩 꺼내 같은 작업을 "
                "반복할 때 사용합니다."
            )
        }
    ]
}

이 구조는 다음을 명확하게 구분합니다.

Prompt

→ 모델에게 제공할 입력


Completion

→ 모델이 학습해야 할 정답

이번 프로젝트에서는 Conversational Prompt-Completion Format을 사용합니다.

17. System·User·Assistant 역할

System

모델의 기본 행동을 정합니다.

{
    "role": "system",
    "content": (
        "당신은 친절하고 정확한 "
        "Python 입문 교관입니다. "
        "답변에는 짧은 예제를 포함하세요."
    )
}

User

사용자의 요청입니다.

{
    "role": "user",
    "content": (
        "while 반복문은 언제 사용하나요?"
    )
}

Assistant

학습할 모범 답안입니다.

{
    "role": "assistant",
    "content": (
        "while 반복문은 특정 조건이 "
        "참인 동안 작업을 반복할 때 사용합니다."
    )
}

System Message가 일관되지 않으면 모델이 여러 정체성을 동시에 학습할 수 있습니다.

데이터 1:

당신은 간결한 교관입니다.


데이터 2:

당신은 매우 장황한 소설가입니다.


데이터 3:

답변하지 말고 질문만 하세요.

AI 신입사원이 첫날부터 세 부서의 업무 지시를 동시에 받은 상황입니다.

18. Chat Template이란?

Chat Template은 메시지 목록을 모델이 이해하는 하나의 문자열 또는 Token Sequence로 변환하는 규칙입니다.

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

Template 적용 결과는 모델에 따라 다음처럼 달라질 수 있습니다.

<|im_start|>user
리스트란 무엇인가요?
<|im_end|>
<|im_start|>assistant
여러 값을 저장하는 자료형입니다.
<|im_end|>

또는:

[INST]
리스트란 무엇인가요?
[/INST]

여러 값을 저장하는 자료형입니다.

Chat Template은 Tokenizer에 저장되며 `apply_chat_template()`로 사용할 수 있습니다.

19. apply_chat_template 사용하기

from transformers import (
    AutoTokenizer
)


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


tokenizer = (
    AutoTokenizer
    .from_pretrained(
        MODEL_ID
    )
)


messages = [
    {
        "role": "system",
        "content": (
            "당신은 친절한 "
            "Python 교관입니다."
        )
    },
    {
        "role": "user",
        "content": (
            "변수란 무엇인가요?"
        )
    }
]


formatted_text = (
    tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True
    )
)


print(
    formatted_text
)

tokenize=False

결과를 문자열로 반환합니다.

tokenize=True

결과를 Token ID로 반환합니다.

add_generation_prompt=True

모델이 다음 Assistant Message를 생성하도록 Assistant 시작 Token을 추가합니다.

20. Chat Template이 중요한 이유

두 Chat Model이 같은 Base Model에서 출발했더라도 서로 다른 역할 Token을 사용했을 수 있습니다.

모델 A:

[INST] 질문 [/INST]


모델 B:

<|user|>질문</|user|>
<|assistant|>

잘못된 Template을 적용하면 모델은 역할 경계를 정확히 인식하지 못할 수 있습니다.

사용자 질문이 어디서 끝나는가?

Assistant 답변은 어디서 시작하는가?

한 대화 Turn은 어디서 끝나는가?

공식 Transformers 문서는 잘못된 Control Token을 사용하면 모델 성능이 크게 저하될 수 있다고 설명합니다.

대본은 맞는데

배역 이름표가 모두 바뀐 연극

모델은 대사를 가지고 있어도 누가 말해야 하는지 혼란스러워질 수 있습니다.

21. 학습용과 추론용 Template 차이

학습

이미 Assistant 정답까지 포함되어 있습니다.

tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=False
)

추론

Assistant 답변이 아직 없습니다.

tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True
)

차이:

학습:

System
→ User
→ Assistant 정답
→ 대화 종료


추론:

System
→ User
→ Assistant 시작
→ 모델이 이어서 생성

SFTTrainer는 Conversational Dataset을 받으면 학습용 Chat Template 적용을 자동으로 처리합니다.

22. EOS Token의 역할

EOS는 다음 표현의 약자입니다.

End Of Sequence

모델에게 문장이나 대화 Turn이 끝났음을 알려주는 Token입니다.

답변 생성

→ EOS Token 출력

→ 생성 종료

EOS Token이 Chat Template과 맞지 않으면 다음 문제가 생길 수 있습니다.

답변이 끝없이 이어짐

다음 User 역할까지 모델이 생성

불필요한 반복

최대 Token까지 멈추지 않음

Qwen2.5 계열의 Chat Template에서는 다음 Token을 대화 종료에 사용합니다.

<|im_end|>

TRL 공식 SFTTrainer 문서도 Qwen2.5 계열처럼 기존 Chat Template이 있는 모델에서는 Template의 종료 Token과 SFTConfig의 `eos_token`을 맞추도록 안내합니다.

eos_token="<|im_end|>"

23. Completion Only Loss

Prompt-Completion Dataset에서는 Prompt는 모델 입력이고 Completion은 정답입니다.

Prompt:

Python 함수란 무엇인가요?


Completion:

함수는 특정 작업을 묶어
재사용할 수 있게 만든 코드 블록입니다.

Completion Only Loss는 Prompt Token을 Loss 계산에서 제외하고 Completion Token만 학습합니다.

Prompt Token:

Loss 제외


Completion Token:

Loss 계산

현재 SFTTrainer는 Prompt-Completion Dataset에서 기본적으로 Completion Token에만 Loss를 계산합니다. 전체 Sequence를 학습하려면 `completion_only_loss=False`로 변경할 수 있습니다.

명시적으로 설정:

completion_only_loss=True

24. Assistant Only Loss

Conversational Language Modeling Dataset에는 여러 역할의 메시지가 함께 들어 있습니다.

System

User

Assistant

User

Assistant

`assistant_only_loss=True`를 사용하면 Assistant 답변 Token에만 Loss를 계산합니다.

assistant_only_loss=True

이 기능을 사용하려면 Chat Template이 Assistant 생성 영역을 표시하는 `{% generation %}`와 `{% endgeneration %}` 구문을 지원해야 합니다. 일부 알려진 모델 계열은 TRL이 Template을 보완하지만 모든 사용자 정의 Template에서 자동으로 작동하는 것은 아닙니다.

따라서 이번 실습에서는 더 명확한 다음 구조를 사용합니다.

Conversational Prompt-Completion

+

completion_only_loss=True

25. Loss Masking 원리

학습할 Token과 제외할 Token은 Label 값으로 구분할 수 있습니다.

예:

입력 Token:

[101, 2054, 2003, 1037, 3231, 102]

Label:

[-100, -100, -100, 1037, 3231, 102]

`-100`인 위치는 Cross Entropy Loss 계산에서 제외됩니다.

Prompt Token

→ labels = -100

→ 학습 제외


Completion Token

→ labels = 실제 Token ID

→ 학습 대상

현재 SFTTrainer는 Pre-tokenized Dataset의 `labels`, `assistant_masks`, `completion_mask`를 인식해 Loss Mask를 구성할 수 있습니다.

26. Packing이란?

대화 데이터는 길이가 짧은 경우가 많습니다.

Example 1:
80 Token

Example 2:
120 Token

Example 3:
60 Token

`max_length=512`로 Batch를 만들면 나머지가 Padding으로 채워질 수 있습니다.

실제 데이터:

80 Token


Padding:

432 Token

Packing은 여러 짧은 Example을 하나의 고정 길이 Sequence에 채웁니다.

512 Token 공간

├─ Example 1: 80
├─ Example 2: 120
├─ Example 3: 60
├─ Example 4: 170
└─ 나머지 공간

활성화:

packing=True

SFTTrainer는 여러 Example을 고정 길이 블록에 배치해 Padding 낭비를 줄이는 Packing을 기본 기능으로 제공합니다.

27. Packing의 장점과 주의사항

장점

Padding 감소

GPU 연산 활용률 향상

짧은 데이터 처리량 증가

학습 시간 감소 가능

주의사항

Example 경계가 복잡해짐

디버깅이 어려워질 수 있음

아주 긴 Example은 잘릴 수 있음

평가 데이터 Packing 여부 검토 필요

현재 SFTConfig의 주요 Packing 전략은 다음과 같습니다.

bfd
→ Best-Fit Decreasing

bfd_split
→ 긴 Sequence 분할 허용

wrapped
→ Sequence 중간 분할을 적극 허용

`packing=True`일 때 `max_length`가 Packing Sequence의 크기를 결정합니다. 현재 기본 전략은 `bfd`입니다.

첫 실습에서는 데이터 구조 확인을 쉽게 하기 위해 다음처럼 설정하겠습니다.

packing=False

기능이 정상 작동한 뒤 Packing을 활성화해 성능을 비교합니다.

28. SFTConfig 이해하기

`SFTConfig`는 SFTTrainer의 학습과 데이터 처리 설정을 관리합니다.

from trl import SFTConfig


training_args = SFTConfig(
    output_dir="outputs/python-tutor",

    learning_rate=1e-4,
    num_train_epochs=3,

    per_device_train_batch_size=2,
    per_device_eval_batch_size=2,

    gradient_accumulation_steps=8,

    max_length=512,
    packing=False,

    completion_only_loss=True,

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

SFTConfig는 Transformers TrainingArguments의 설정에 더해 Chat Template, Dataset 열, EOS, Max Length, Packing, Completion Only Loss, Assistant Only Loss 같은 SFT 전용 설정을 제공합니다.

29. 학습 길이와 max_length

max_length=512

`max_length`는 학습 Sequence의 최대 Token 수입니다.

현재 SFTConfig의 기본값은 1,024입니다. 길이를 초과한 Sequence는 설정된 Truncation 방식에 따라 잘릴 수 있습니다. Packing을 사용할 때는 Packing Block 크기로도 사용됩니다.

너무 작게 설정

System Message

+ 긴 User 질문

+ Assistant 답변

→ 뒷부분 잘림

정답 답변이 잘리면 모델은 문장을 끝맺는 방법을 충분히 학습하지 못할 수 있습니다.

너무 크게 설정

GPU 메모리 증가

연산량 증가

Padding 낭비 증가 가능

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

30. 학습 데이터 만들기

이번 프로젝트에서는 Python 입문 교관용 대화 데이터를 직접 만듭니다.

````pythonTRAINING_EXAMPLES = [{"prompt": [{"role": "system","content": ("당신은 Python 입문자를 위한 ""친절한 교관입니다. ""핵심 개념을 설명하고 ""짧은 코드 예제를 제공합니다.")},{"role": "user","content": ("Python 변수란 무엇인가요?")}],"completion": [{"role": "assistant","content": ("변수는 프로그램에서 값을 ""저장하기 위해 붙이는 이름입니다.\n\n""예를 들어 다음 코드는 age라는 ""변수에 20을 저장합니다.\n\n""```python\n""age = 20\n""print(age)\n""```")}]},{"prompt": [{"role": "system","content": ("당신은 Python 입문자를 위한 ""친절한 교관입니다. ""핵심 개념을 설명하고 ""짧은 코드 예제를 제공합니다.")},{"role": "user","content": ("리스트와 튜플의 차이를 ""설명해 주세요.")}],"completion": [{"role": "assistant","content": ("리스트와 튜플은 여러 값을 ""순서대로 저장합니다.\n\n""리스트는 생성 후 값을 변경할 수 있고, ""튜플은 일반적으로 변경할 수 없습니다.\n\n""```python\n""numbers = [1, 2, 3]\n""point = (10, 20)\n""```")}]}]````

실제 프로젝트에서는 수십 개가 아니라 업무 범위를 충분히 대표하는 고품질 데이터가 필요합니다.

두 개의 예제:

코드 실행 테스트용


수천 개의 검증된 예제:

실제 학습 검토용

31. 데이터 품질 검증

SFT 데이터에는 최소한 다음 검사가 필요합니다.

Prompt가 비어 있지 않은가?

Completion이 비어 있지 않은가?

역할 순서가 올바른가?

Assistant 정답이 정확한가?

답변 스타일이 일관적인가?

개인정보가 포함되어 있지 않은가?

서로 모순되는 답변이 없는가?

검증 함수:

def validate_example(
    example: dict
) -> None:
    prompt = example.get(
        "prompt"
    )

    completion = example.get(
        "completion"
    )

    if not prompt:
        raise ValueError(
            "prompt가 비어 있습니다."
        )

    if not completion:
        raise ValueError(
            "completion이 비어 있습니다."
        )

    if completion[-1].get(
        "role"
    ) != "assistant":
        raise ValueError(
            "completion의 마지막 역할은 "
            "assistant여야 합니다."
        )

    for message in (
        prompt + completion
    ):
        if message.get(
            "role"
        ) not in {
            "system",
            "user",
            "assistant"
        }:
            raise ValueError(
                "지원하지 않는 역할입니다."
            )

        if not str(
            message.get(
                "content",
                ""
            )
        ).strip():
            raise ValueError(
                "메시지 내용이 비어 있습니다."
            )

32. Train·Validation 분리

from datasets import Dataset


dataset = Dataset.from_list(
    TRAINING_EXAMPLES
)


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


train_dataset = dataset[
    "train"
]


validation_dataset = dataset[
    "test"
]

실제 프로젝트에서는 Test Dataset을 별도로 유지하는 것이 좋습니다.

Train

→ Adapter 학습


Validation

→ 학습 설정과 Checkpoint 선택


Test

→ 최종 품질 평가

33. LoRA 설정

Qwen2.5의 Attention Projection Layer에 LoRA를 적용합니다.

from peft import (
    LoraConfig,
    TaskType
)


lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    inference_mode=False,

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

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

    bias="none"
)

구성:

r=16
→ LoRA 내부 Rank

lora_alpha=32
→ Adapter Scaling

lora_dropout=0.05
→ 과적합 완화

target_modules
→ Attention Projection에 Adapter 추가

34. SFTTrainer 생성

from trl import (
    SFTConfig,
    SFTTrainer
)


training_args = SFTConfig(
    output_dir=(
        "outputs/"
        "python-tutor-sft"
    ),

    learning_rate=1e-4,

    per_device_train_batch_size=2,
    per_device_eval_batch_size=2,

    gradient_accumulation_steps=8,

    num_train_epochs=3,

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

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

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

    save_total_limit=2,

    max_length=512,
    packing=False,

    completion_only_loss=True,

    eos_token="<|im_end|>",

    report_to="none",

    seed=2026,
    data_seed=2026
)


trainer = SFTTrainer(
    model=MODEL_ID,
    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config
)

SFTTrainer에 `peft_config`를 전달하면 모델을 직접 `get_peft_model()`로 감싸지 않아도 Trainer가 PEFT 모델로 구성합니다. Adapter 학습에서는 새 파라미터만 학습하므로 Full Fine-tuning보다 높은 `1e-4` 안팎의 Learning Rate가 자주 사용됩니다.

35. SFT Fine-tuning 시작

train_result = trainer.train()

SFTTrainer 내부에서는 다음 작업이 수행됩니다.

대화 데이터 읽기

→ Chat Template 적용

→ Prompt와 Completion 결합

→ Tokenization

→ Prompt Token Masking

→ Completion Token Loss 계산

→ LoRA Gradient 계산

→ Adapter 업데이트

→ Validation Loss 측정

→ Checkpoint 저장

학습 결과:

print(
    train_result.metrics
)

저장:

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


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


trainer.save_state()

36. 학습 로그 해석

SFTTrainer는 학습 중 다음과 같은 지표를 기록합니다.

global_step

epoch

num_tokens

loss

entropy

mean_token_accuracy

learning_rate

grad_norm

현재 SFTTrainer는 기본적으로 Mask되지 않은 Token에 대한 Loss와 평균 Token Accuracy, 예측 분포의 Entropy, Gradient Norm 등을 기록합니다.

로그 예:

{
    "loss": 2.31,
    "entropy": 4.82,
    "mean_token_accuracy": 0.41,
    "learning_rate": 0.00008,
    "epoch": 0.5
}

37. Token Accuracy와 Loss

Loss

정답 Token에 높은 확률을 부여할수록 감소합니다.

Loss 감소

→ 학습 데이터의 답변 Token을
  더 잘 예측하고 있음

Mean Token Accuracy

각 학습 위치에서 확률이 가장 높은 Token이 정답 Token과 일치한 비율입니다.

정답:

리스트는 여러 값을 저장합니다.


예측:

리스트는 여러 데이터를 저장합니다.

문장 의미는 비슷하지만 `값을` 대신 `데이터를` 예측하면 해당 Token은 오답으로 계산될 수 있습니다.

따라서 Token Accuracy만으로 실제 답변 품질을 판단해서는 안 됩니다.

낮은 Loss

≠

정확한 사실


높은 Token Accuracy

≠

좋은 대화 품질

생성 결과를 실제 질문으로 평가해야 합니다.

38. Adapter 저장

from pathlib import Path


ADAPTER_DIR = Path(
    "models/"
    "python-tutor-sft-lora"
)


trainer.save_model(
    str(
        ADAPTER_DIR
    )
)


tokenizer.save_pretrained(
    ADAPTER_DIR
)

PEFT를 사용한 SFTTrainer에서 저장되는 주요 모델 파일은 전체 Base Model이 아니라 LoRA Adapter입니다.

adapter_config.json

adapter_model.safetensors

tokenizer.json

tokenizer_config.json

39. 저장된 모델 불러오기

from peft import (
    AutoPeftModelForCausalLM
)


model = (
    AutoPeftModelForCausalLM
    .from_pretrained(
        ADAPTER_DIR
    )
)


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"
    )


model = model.to(
    device
)

40. Chat Template 기반 추론

messages = [
    {
        "role": "system",
        "content": (
            "당신은 Python 입문자를 위한 "
            "친절한 교관입니다. "
            "핵심 개념과 짧은 예제를 제공합니다."
        )
    },
    {
        "role": "user",
        "content": (
            "Python 함수의 return은 "
            "무슨 역할을 하나요?"
        )
    }
]

Token 변환:

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

생성:

with torch.inference_mode():
    output_ids = model.generate(
        input_ids=input_ids,

        max_new_tokens=256,

        do_sample=True,
        temperature=0.7,
        top_p=0.9,

        eos_token_id=(
            tokenizer.eos_token_id
        ),

        pad_token_id=(
            tokenizer.pad_token_id
        )
    )

새로 생성된 부분만 추출:

generated_ids = output_ids[
    0,
    input_ids.shape[-1]:
]

문자열 변환:

response = tokenizer.decode(
    generated_ids,
    skip_special_tokens=True
)


print(
    response
)

41. Base Model과 SFT 결과 비교

같은 질문을 Base Model과 SFT Adapter Model에 각각 입력합니다.

질문:

Python 함수의 return은
무슨 역할을 하나요?

비교 항목:

질문을 정확히 이해하는가?

한국어로 답변하는가?

설명이 초보자에게 적합한가?

코드 예제가 포함되는가?

불필요한 내용을 생성하지 않는가?

System Message를 따르는가?

비교표:

항목Base ModelSFT Model
지시 준수측정측정
답변 형식측정측정
정확성측정측정
코드 예제측정측정
장황함측정측정
일관성측정측정

하나의 예제만 보고 성능을 판단하지 않습니다.

42. 여러 질문 일괄 평가

EVALUATION_QUESTIONS = [
    "Python 변수란 무엇인가요?",
    "리스트와 튜플의 차이는 무엇인가요?",
    "for 반복문을 예제로 설명해 주세요.",
    "함수의 매개변수란 무엇인가요?",
    "try-except는 언제 사용하나요?"
]

반복 평가:

def generate_answer(
    question: str
) -> str:
    messages = [
        {
            "role": "system",
            "content": (
                "당신은 Python 입문자를 위한 "
                "친절하고 정확한 교관입니다."
            )
        },
        {
            "role": "user",
            "content": question
        }
    ]

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

    with torch.inference_mode():
        output_ids = model.generate(
            input_ids=input_ids,
            max_new_tokens=200,
            do_sample=False,
            eos_token_id=(
                tokenizer.eos_token_id
            ),
            pad_token_id=(
                tokenizer.pad_token_id
            )
        )

    generated_ids = output_ids[
        0,
        input_ids.shape[-1]:
    ]

    return tokenizer.decode(
        generated_ids,
        skip_special_tokens=True
    )


for question in (
    EVALUATION_QUESTIONS
):
    answer = generate_answer(
        question
    )

    print(
        f"[질문]\n{question}"
    )

    print(
        f"\n[답변]\n{answer}"
    )

    print(
        "\n"
        + "=" * 60
        + "\n"
    )

43. 답변 품질 평가 방법

생성형 AI는 단순 Accuracy만으로 평가하기 어렵습니다.

사실 정확성

설명에 기술적 오류가 없는가?

지시 준수

사용자가 요구한 형식을 따르는가?

관련성

질문과 직접 관련된 내용을 답하는가?

완전성

필요한 핵심 정보를 빠뜨리지 않았는가?

간결성

불필요한 반복이 없는가?

안전성

위험하거나 부적절한 내용을 생성하지 않는가?

형식 정확성

JSON 요청 시 유효한 JSON인가?

표 요청 시 표로 출력하는가?

코드 요청 시 실행 가능한가?

평가표 예:

질문정확성지시 준수관련성형식종합
--------:----:--:-:--:
변수 설명55544.8
반복문 예제45554.8
예외 처리44544.3

자동 지표와 사람 평가를 함께 사용하는 것이 좋습니다.

44. QLoRA와 SFTTrainer

더 큰 모델을 SFT하려면 QLoRA를 사용할 수 있습니다.

Base Model:

4비트 양자화

+

LoRA Adapter:

BF16·FP16 학습

필요 패키지:

python -m pip install bitsandbytes

양자화 설정:

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
        )
    )
)

45. SFTTrainer의 Quantization 통합

현재 SFTTrainer는 `quantization_config`와 `peft_config`를 함께 받을 수 있습니다.

qlora_trainer = SFTTrainer(
    model=MODEL_ID,

    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config,

    quantization_config=(
        quantization_config
    )
)

SFTTrainer에 모델 ID를 문자열로 전달하면 Quantization 설정으로 모델을 불러오고 PEFT 설정으로 Adapter를 연결할 수 있습니다. 이미 생성된 모델 객체를 전달한 경우 `quantization_config` 인수는 사용되지 않습니다.

QLoRA는 주로 CUDA GPU 환경에서 사용하며 `bitsandbytes`의 운영체제와 Hardware Backend 지원 상태를 먼저 확인해야 합니다.

46. Checkpoint와 학습 재개

SFTTrainer도 Transformers Trainer의 Checkpoint 기능을 상속합니다.

가장 최근 Checkpoint에서 재개:

trainer.train(
    resume_from_checkpoint=True
)

특정 Checkpoint:

trainer.train(
    resume_from_checkpoint=(
        "outputs/"
        "python-tutor-sft/"
        "checkpoint-100"
    )
)

`resume_from_checkpoint=True`를 사용하면 Output Directory의 마지막 Checkpoint에서 모델, Optimizer, Scheduler 상태를 복원합니다.

Checkpoint와 최종 Adapter 폴더는 역할이 다릅니다.

Checkpoint:

학습 재개용


최종 Adapter:

추론·배포용

47. Adapter 병합

학습된 Adapter를 Base Model에 병합할 수 있습니다.

merged_model = (
    model.merge_and_unload(
        safe_merge=True
    )
)

저장:

MERGED_DIR = Path(
    "models/"
    "python-tutor-sft-merged"
)


merged_model.save_pretrained(
    MERGED_DIR
)


tokenizer.save_pretrained(
    MERGED_DIR
)

병합 전

Base Model

+

LoRA Adapter

병합 후

독립된 전체 모델

병합 모델은 Adapter보다 훨씬 크지만 PEFT 없이 일반 모델처럼 배포하기 편리합니다.

48. Hugging Face Hub 업로드

로그인:

hf auth login

SFTConfig:

training_args = SFTConfig(
    push_to_hub=True,

    hub_model_id=(
        "사용자명/"
        "python-tutor-sft-lora"
    ),

    hub_private_repo=True,

    hub_strategy="end"
)

학습 후:

trainer.push_to_hub(
    commit_message=(
        "Fine-tune Python tutor "
        "with TRL SFTTrainer"
    )
)

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

업로드 전 확인:

Base Model 라이선스

학습 데이터 라이선스

개인정보 포함 여부

회사 내부 정보

모델의 알려진 한계

상업 이용 조건

49. TRL CLI 사용하기

TRL은 Python 코드를 직접 작성하지 않고 설정 파일을 이용해 SFT를 실행할 수 있는 CLI를 제공합니다.

실행:

trl sft \
    --config sft_config.yaml

설정 파일 예:

model_name_or_path: Qwen/Qwen2.5-0.5B-Instruct

dataset_name: trl-lib/Capybara

output_dir: outputs/qwen-sft

learning_rate: 0.0001

num_train_epochs: 3

per_device_train_batch_size: 2

gradient_accumulation_steps: 8

max_length: 512

packing: true

use_peft: true

lora_r: 16

lora_alpha: 32

lora_dropout: 0.05

report_to: none

CLI는 여러 실험 설정을 YAML 파일로 관리할 때 편리합니다.

실험 A:

config_a.yaml


실험 B:

config_b.yaml


실험 C:

config_c.yaml

50. Gradio 챗봇 연결

import gradio as gr


def chat(
    message: str,
    history: list[dict]
):
    messages = [
        {
            "role": "system",
            "content": (
                "당신은 Python 입문자를 위한 "
                "친절하고 정확한 교관입니다."
            )
        }
    ]

    for item in history:
        role = item.get(
            "role"
        )

        content = item.get(
            "content"
        )

        if role in {
            "user",
            "assistant"
        }:
            messages.append({
                "role": role,
                "content": content
            })

    messages.append({
        "role": "user",
        "content": message
    })

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

    with torch.inference_mode():
        output_ids = model.generate(
            input_ids=input_ids,

            max_new_tokens=256,

            do_sample=True,
            temperature=0.7,
            top_p=0.9,

            eos_token_id=(
                tokenizer.eos_token_id
            ),

            pad_token_id=(
                tokenizer.pad_token_id
            )
        )

    generated_ids = output_ids[
        0,
        input_ids.shape[-1]:
    ]

    yield tokenizer.decode(
        generated_ids,
        skip_special_tokens=True
    )


demo = gr.ChatInterface(
    fn=chat,
    title="🐍 Python AI 교관",
    description=(
        "TRL과 LoRA로 SFT한 "
        "Python 입문 교관입니다."
    )
)


demo.queue()
demo.launch()

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

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

````pythonfrom pathlib import Pathimport jsonimport time

import torch

from datasets import Dataset

from peft import (AutoPeftModelForCausalLM,LoraConfig,TaskType)

from transformers import (AutoTokenizer,set_seed)

from trl import (SFTConfig,SFTTrainer)

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

OUTPUT_DIR = Path("outputs/""python-tutor-sft")

ADAPTER_DIR = Path("models/""python-tutor-sft-lora")

SEED = 2026

SYSTEM_MESSAGE = ("당신은 Python 입문자를 위한 ""친절하고 정확한 교관입니다. ""먼저 핵심 개념을 설명하고 ""필요하면 짧은 코드 예제를 제공합니다. ""확실하지 않은 내용은 추측하지 않습니다.")

TRAINING_EXAMPLES = [{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 변수란 무엇인가요?")}],"completion": [{"role": "assistant","content": ("변수는 프로그램에서 값을 ""저장하기 위해 붙이는 이름입니다.\n\n""예를 들어 다음 코드는 age라는 ""변수에 숫자 20을 저장합니다.\n\n""```python\n""age = 20\n""print(age)\n""```\n\n""변수를 사용하면 같은 값을 ""여러 곳에서 쉽게 재사용할 수 있습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("리스트와 튜플의 차이를 ""설명해 주세요.")}],"completion": [{"role": "assistant","content": ("리스트와 튜플은 여러 값을 ""순서대로 저장합니다.\n\n""리스트는 생성 후 값을 변경할 수 있지만 ""튜플은 일반적으로 변경할 수 없습니다.\n\n""```python\n""numbers = [1, 2, 3]\n""numbers[0] = 10\n\n""point = (10, 20)\n""```\n\n""값을 변경해야 한다면 리스트, ""고정된 값을 보관한다면 튜플을 ""고려할 수 있습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("for 반복문은 언제 사용하나요?")}],"completion": [{"role": "assistant","content": ("for 반복문은 여러 데이터를 ""하나씩 꺼내 같은 작업을 ""반복할 때 사용합니다.\n\n""```python\n""fruits = ['사과', '바나나', '포도']\n\n""for fruit in fruits:\n"" print(fruit)\n""```\n\n""이 코드는 리스트의 과일 이름을 ""차례대로 출력합니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("while 반복문과 for 반복문의 ""차이는 무엇인가요?")}],"completion": [{"role": "assistant","content": ("for 반복문은 반복할 데이터나 횟수가 ""비교적 명확할 때 사용합니다.\n\n""while 반복문은 특정 조건이 참인 동안 ""계속 반복해야 할 때 사용합니다.\n\n""```python\n""count = 0\n\n""while count < 3:\n"" print(count)\n"" count += 1\n""```\n\n""while문에서는 조건이 언젠가 ""거짓이 되도록 값을 변경해야 ""무한 반복을 방지할 수 있습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 함수는 왜 사용하나요?")}],"completion": [{"role": "assistant","content": ("함수는 특정 작업을 하나의 이름으로 ""묶어 재사용하기 위해 사용합니다.\n\n""```python\n""def greet(name):\n"" return f'{name}님, 안녕하세요.'\n\n""message = greet('파이썬')\n""print(message)\n""```\n\n""함수를 사용하면 중복 코드를 줄이고 ""프로그램의 구조를 이해하기 쉬워집니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("함수의 return은 ""무슨 역할을 하나요?")}],"completion": [{"role": "assistant","content": ("return은 함수가 만든 결과를 ""함수 바깥으로 전달하고 ""함수 실행을 종료합니다.\n\n""```python\n""def add(a, b):\n"" return a + b\n\n""result = add(3, 5)\n""print(result)\n""```\n\n""이 예제에서 add 함수는 8을 반환하고 ""그 값이 result 변수에 저장됩니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("딕셔너리는 어떤 자료형인가요?")}],"completion": [{"role": "assistant","content": ("딕셔너리는 Key와 Value를 ""한 쌍으로 저장하는 자료형입니다.\n\n""```python\n""user = {\n"" 'name': '홍길동',\n"" 'age': 20\n""}\n\n""print(user['name'])\n""```\n\n""순번 대신 의미 있는 Key로 ""값에 접근할 수 있다는 장점이 있습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("try-except는 언제 사용하나요?")}],"completion": [{"role": "assistant","content": ("try-except는 프로그램 실행 중 ""발생할 수 있는 예외를 처리할 때 ""사용합니다.\n\n""```python\n""try:\n"" number = int(input('숫자: '))\n""except ValueError:\n"" print('올바른 숫자를 입력하세요.')\n""```\n\n""예외 처리를 사용하면 오류가 발생해도 ""프로그램이 갑자기 종료되는 상황을 ""줄일 수 있습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("클래스와 객체의 관계를 ""쉽게 설명해 주세요.")}],"completion": [{"role": "assistant","content": ("클래스는 객체를 만들기 위한 설계도이고, ""객체는 그 설계도로 만든 실제 결과물입니다.\n\n""```python\n""class Dog:\n"" def __init__(self, name):\n"" self.name = name\n\n""dog = Dog('초코')\n""print(dog.name)\n""```\n\n""Dog는 클래스이고 dog는 ""Dog 클래스로 만든 객체입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("모듈을 사용하는 이유는 무엇인가요?")}],"completion": [{"role": "assistant","content": ("모듈은 관련된 함수와 클래스, 변수를 ""하나의 Python 파일에 정리한 것입니다.\n\n""```python\n""import math\n\n""print(math.sqrt(16))\n""```\n\n""모듈을 사용하면 코드를 기능별로 나누고 ""다른 프로그램에서 재사용할 수 있습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("리스트 컴프리헨션을 ""초보자에게 설명해 주세요.")}],"completion": [{"role": "assistant","content": ("리스트 컴프리헨션은 반복문을 이용한 ""리스트 생성을 짧게 표현하는 문법입니다.\n\n""```python\n""squares = [number ** 2 ""for number in range(5)]\n""print(squares)\n""```\n\n""복잡한 조건이 많아 읽기 어려워진다면 ""일반 for 반복문을 사용하는 편이 좋습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("가상환경은 왜 필요한가요?")}],"completion": [{"role": "assistant","content": ("가상환경은 프로젝트마다 Python 패키지와 ""버전을 분리하기 위해 사용합니다.\n\n""```bash\n""python -m venv venv\n""```\n\n""프로젝트 A와 프로젝트 B가 서로 다른 ""라이브러리 버전을 사용하더라도 ""충돌하지 않도록 도와줍니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("패키지 설치에 pip를 ""어떻게 사용하나요?")}],"completion": [{"role": "assistant","content": ("pip는 Python 패키지를 설치하고 ""관리하는 명령줄 도구입니다.\n\n""```bash\n""python -m pip install requests\n""```\n\n""설치된 패키지를 확인하려면 ""다음 명령을 사용할 수 있습니다.\n\n""```bash\n""python -m pip list\n""```")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("None은 무엇을 의미하나요?")}],"completion": [{"role": "assistant","content": ("None은 값이 없거나 아직 값이 ""정해지지 않았음을 나타내는 ""Python의 특별한 객체입니다.\n\n""```python\n""result = None\n\n""if result is None:\n"" print('결과가 없습니다.')\n""```\n\n""None을 비교할 때는 일반적으로 ""==보다 is를 사용합니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Boolean 자료형을 ""예제로 설명해 주세요.")}],"completion": [{"role": "assistant","content": ("Boolean은 참과 거짓을 나타내는 ""자료형이며 값은 True와 False입니다.\n\n""```python\n""is_logged_in = True\n\n""if is_logged_in:\n"" print('로그인 상태입니다.')\n""```\n\n""조건문에서 프로그램의 실행 방향을 ""결정할 때 자주 사용합니다.")}]}]

def validate_example(example: dict,index: int) -> None:"""대화 학습 데이터 구조를 검사합니다."""prompt = example.get("prompt")

completion = example.get("completion")

if not isinstance(prompt,list) or not prompt:raise ValueError(f"{index}번 데이터의 "f"prompt가 올바르지 않습니다.")

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

allowed_roles = {"system","user","assistant"}

for message in (prompt + completion):role = message.get("role")

content = str(message.get("content","")).strip()

if role not in allowed_roles:raise ValueError(f"{index}번 데이터에 "f"지원하지 않는 역할이 있습니다: "f"{role}")

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

if completion[-1].get("role") != "assistant":raise ValueError(f"{index}번 데이터의 "f"completion은 assistant "f"역할이어야 합니다.")

def select_precision() -> tuple[bool,bool,torch.dtype]:"""실행 환경에 맞는 학습 정밀도를 선택합니다."""if not torch.cuda.is_available():return (False,False,torch.float32)

use_bf16 = (torch.cuda.is_bf16_supported())

if use_bf16:return (True,False,torch.bfloat16)

return (False,True,torch.float16)

def select_device() -> torch.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 generate_answer(model,tokenizer,device: torch.device,question: str) -> str:"""학습된 모델로 Python 질문에 답합니다."""messages = [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": question}]

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

with torch.inference_mode():output_ids = model.generate(input_ids=input_ids,

max_new_tokens=256,

do_sample=False,

eos_token_id=(tokenizer.eos_token_id),

pad_token_id=(tokenizer.pad_token_id))

generated_ids = output_ids[0,input_ids.shape[-1]:]

return tokenizer.decode(generated_ids,skip_special_tokens=True).strip()

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

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

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

print("[1/10] 학습 데이터 검증")

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

print(f"검증된 데이터: "f"{len(TRAINING_EXAMPLES)}개")

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

dataset = Dataset.from_list(TRAINING_EXAMPLES)

split_dataset = (dataset.train_test_split(test_size=0.2,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/10] Tokenizer 로딩")

tokenizer = (AutoTokenizer.from_pretrained(MODEL_ID))

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

print("[4/10] Chat Template 확인")

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

print(sample_text)

print("[5/10] LoRA 설정")

lora_config = LoraConfig(task_type=TaskType.CAUSAL_LM,inference_mode=False,

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

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

bias="none")

(use_bf16,use_fp16,model_dtype) = select_precision()

print(f"CUDA: "f"{torch.cuda.is_available()}")

print(f"BF16: {use_bf16}")

print(f"FP16: {use_fp16}")

print("[6/10] SFTConfig 생성")

training_args = SFTConfig(output_dir=str(OUTPUT_DIR),

model_init_kwargs={"dtype": model_dtype},

learning_rate=1e-4,lr_scheduler_type="linear",warmup_ratio=0.1,

per_device_train_batch_size=2,per_device_eval_batch_size=2,

gradient_accumulation_steps=8,

num_train_epochs=3,

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

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

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

save_total_limit=2,

max_length=512,packing=False,

completion_only_loss=True,

eos_token="<|im_end|>",

gradient_checkpointing=True,

bf16=use_bf16,fp16=use_fp16,

report_to="none",

seed=SEED,data_seed=SEED,

run_name=("python-tutor-sft-lora"))

print("[7/10] SFTTrainer 생성")

trainer = SFTTrainer(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("[8/10] SFT 학습 시작")

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("[9/10] Validation 평가")

validation_metrics = (trainer.evaluate())

trainer.log_metrics("validation",validation_metrics)

trainer.save_metrics("validation",validation_metrics)

print("[10/10] Adapter 저장과 추론")

trainer.save_model(str(ADAPTER_DIR))

tokenizer.save_pretrained(ADAPTER_DIR)

training_summary = {"model_id": MODEL_ID,"train_examples": len(train_dataset),"validation_examples": 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(training_summary,ensure_ascii=False,indent=2),encoding="utf-8")

print(f"Adapter 저장 위치: "f"{ADAPTER_DIR.resolve()}")

del trainer

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

inference_model = (AutoPeftModelForCausalLM.from_pretrained(ADAPTER_DIR))

device = select_device()

inference_model = (inference_model.to(device))

inference_model.eval()

questions = [("Python 함수의 return은 ""무슨 역할을 하나요?"),("set과 list의 차이는 무엇인가요?"),("파일을 안전하게 닫으려면 ""어떤 문법을 사용해야 하나요?")]

print("\n[생성 결과]")

for question in questions:answer = generate_answer(inference_model,tokenizer,device,question)

print(f"\n질문:\n{question}")

print(f"\n답변:\n{answer}")

print("\n"+ "=" * 70)

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

52. 프로젝트 실행 방법

1단계: 폴더 생성

mkdir trl_sft_project
cd trl_sft_project

2단계: 가상환경 생성

Windows

python -m venv venv
venv\Scripts\activate

macOS·Linux

python3 -m venv venv
source venv/bin/activate

3단계: 패키지 설치

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

4단계: 코드 저장

train_sft.py

5단계: 실행

python train_sft.py

주의사항

예제 데이터는 학습 파이프라인을 확인하기 위한 소규모 데이터입니다.

15개 데이터

→ 코드와 구조 검증


실제 AI 교관 개발

→ 충분한 업무 데이터
→ 전문가 검수
→ 독립 Test Dataset
→ 안전성 평가

소량 데이터만으로 모델의 지식이나 정확도가 크게 향상되지는 않습니다.

53. 결과 파일 구조

trl_sft_project/
├─ train_sft.py
│
├─ outputs/
│  └─ python-tutor-sft/
│     ├─ checkpoint-...
│     ├─ train_results.json
│     ├─ validation_results.json
│     ├─ trainer_state.json
│     └─ training_summary.json
│
└─ models/
   └─ python-tutor-sft-lora/
      ├─ adapter_config.json
      ├─ adapter_model.safetensors
      ├─ tokenizer.json
      ├─ tokenizer_config.json
      └─ special_tokens_map.json

Adapter 폴더는 추론과 배포에 사용합니다.

Checkpoint 폴더는 학습 재개에 사용합니다.

54. 데이터 보안과 라이선스

Instruction Dataset은 다음 정보를 포함할 수 있습니다.

회사 업무 절차

고객 상담 기록

내부 매뉴얼

보안 정책

소스코드

장애 보고서

개인정보

SFT 데이터 업로드 전 확인:

학습에 사용할 권리가 있는가?

개인정보를 제거했는가?

외부 Hub에 업로드해도 되는가?

모델이 답변으로 기밀을 재현할 수 있는가?

데이터 보관 기간은 적절한가?

접근 권한이 제한되어 있는가?

모델에 데이터를 학습했다고 원본 정보가 안전하게 삭제되는 것은 아닙니다.

원본 파일 삭제

≠

모델이 정보를 기억하지 않음

민감정보가 포함된 데이터는 조직 보안 정책과 개인정보 보호 절차를 따라야 합니다.

55. SFT의 한계

SFT는 모범 답안을 따라 하는 방법을 가르칩니다.

하지만 다음 문제를 자동으로 해결하지는 않습니다.

사실 정확성

학습 데이터가 틀리면 모델도 틀린 답변을 배웁니다.

최신 정보

학습 후 새로 발생한 정보는 알 수 없습니다.

선호도 정렬

여러 개의 가능한 답변 중 사용자가 더 좋아하는 답변을 선택하는 능력은 별도의 Preference 학습이 필요할 수 있습니다.

안전성

위험한 요청을 거절하는 능력도 적절한 안전 데이터와 평가가 필요합니다.

환각

SFT 후에도 모델은 존재하지 않는 정보를 생성할 수 있습니다.

지식 주입의 한계

소량 SFT 데이터로 대량의 정확한 업무 지식을 안정적으로 저장하기는 어렵습니다.

업무 행동과 말투

→ SFT


실시간·정확한 문서 지식

→ RAG·검색·도구 연결 검토

56. 자주 발생하는 오류

오류 1. trl을 찾을 수 없음

ModuleNotFoundError:
No module named 'trl'

설치:

python -m pip install --upgrade trl

확인:

python -m pip show trl

오류 2. Python 버전이 낮음

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

확인:

python --version

오류 3. SFTConfig 인수를 인식하지 못함

unexpected keyword argument
'completion_only_loss'

TRL 버전을 확인합니다.

python -m pip show trl

오래된 TRL 예제와 현재 1.x API를 혼합하지 않았는지 확인합니다.

오류 4. dataset_text_field 오류

Standard Language Modeling Dataset의 문자열 열 이름이 `text`가 아니라면 지정합니다.

training_args = SFTConfig(
    dataset_text_field=(
        "instruction_text"
    )
)

현재 기본 열 이름은 `text`입니다.

오류 5. Chat Template이 없음

tokenizer.chat_template is not set

해결 방법:

Chat Template이 있는 Instruct Model 사용

또는

chat_template_path 지정
training_args = SFTConfig(
    chat_template_path=(
        "Chat Template이 있는 "
        "Tokenizer ID"
    )
)

SFTConfig의 `chat_template_path`에는 Hub Tokenizer, 로컬 Tokenizer 폴더 또는 Jinja Template 파일을 지정할 수 있습니다.

오류 6. EOS Token이 맞지 않음

증상:

답변이 끝나지 않음

User 역할까지 생성

같은 문장 반복

Chat Template이 사용하는 종료 Token을 확인합니다.

print(
    tokenizer.eos_token
)

Qwen2.5 예:

eos_token="<|im_end|>"

오류 7. Padding Token이 없음

Asking to pad but the tokenizer
does not have a padding token

설정:

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

SFTTrainer는 Processing Class에 Padding Token이 없으면 EOS Token을 기본 Padding Token으로 사용할 수 있습니다.

오류 8. Completion Only Loss를 사용할 수 없음

`completion_only_loss=True`는 Prompt-Completion Dataset에서 사용합니다.

잘못된 데이터:

{
    "messages": [...]
}

권장 구조:

{
    "prompt": [...],
    "completion": [...]
}

오류 9. Assistant Only Loss가 작동하지 않음

Chat Template에 Assistant 생성 영역을 표시하는 구문이 없을 수 있습니다.

{% generation %}

{% endgeneration %}

Prompt-Completion 구조와 `completion_only_loss=True`를 사용하는 방법도 검토합니다.

오류 10. Target Module을 찾지 못함

Target modules not found

모델의 Linear Module 이름을 확인합니다.

import torch

from transformers import (
    AutoModelForCausalLM
)


model = (
    AutoModelForCausalLM
    .from_pretrained(
        MODEL_ID
    )
)


for name, module in (
    model.named_modules()
):
    if isinstance(
        module,
        torch.nn.Linear
    ):
        print(name)

오류 11. 학습 가능한 파라미터가 0개

trainer.model.print_trainable_parameters()

확인:

peft_config가 전달되었는가?

Target Module이 존재하는가?

inference_mode=False인가?

Adapter가 활성화되어 있는가?

오류 12. CUDA 메모리 부족

CUDA out of memory

대응:

Batch Size 감소

Gradient Accumulation 증가

max_length 감소

Rank 감소

Target Module 감소

Gradient Checkpointing

FP16·BF16

QLoRA

오류 13. Loss가 NaN이 됨

확인:

Learning Rate가 너무 높지 않은가?

FP16이 불안정하지 않은가?

빈 Completion이 있는가?

데이터가 모두 Mask되지 않았는가?

Gradient가 폭발하지 않는가?

Learning Rate 감소:

learning_rate=5e-5

오류 14. Training Loss가 0임

Completion Token이 모두 Mask되었거나 데이터 형식이 올바르지 않을 수 있습니다.

Prompt 존재

Completion 비어 있음

→ 학습할 Token 없음

실제 Template 적용 결과와 Label Mask를 확인합니다.

오류 15. Packing 활성화 후 오류 발생

먼저 다음처럼 비활성화해 데이터 문제를 확인합니다.

packing=False

정상 학습을 확인한 뒤 다시 활성화합니다.

packing=True

오류 16. Validation Loss가 계속 증가함

과적합 가능성이 있습니다.

Epoch 감소

LoRA Dropout 증가

Rank 감소

데이터 추가

중복 데이터 제거

Early Stopping 적용

오류 17. 학습 후 답변이 그대로임

가능한 원인:

데이터가 너무 적음

Learning Rate가 너무 낮음

LoRA Target이 부족함

학습 Epoch가 부족함

질문이 학습 도메인 밖임

Adapter가 로드되지 않음

오류 18. 모델이 학습 답변을 그대로 반복함

데이터를 외운 과적합 상태일 수 있습니다.

유사한 질문만 반복

동일한 문장 구조

데이터 수 부족

Epoch 과다

질문 표현과 난이도를 다양화해야 합니다.

오류 19. 모델이 System Message를 무시함

확인:

System Message가 데이터마다 일관적인가?

사용 모델이 system 역할을 지원하는가?

올바른 Chat Template을 적용했는가?

학습 데이터에 상충하는 지시가 없는가?

오류 20. Gradio Chat History 형식 오류

현재 Gradio ChatInterface의 대화 기록 구조와 모델의 Chat Template 입력 구조가 다를 수 있습니다.

History를 다음 형태로 변환해야 합니다.

{
    "role": "user",
    "content": "질문"
}
{
    "role": "assistant",
    "content": "답변"
}

57. 연습 문제

문제 1

Pre-training과 SFT의 차이를 설명하세요.

문제 2

다음 내용을 Conversational Language Modeling 형식으로 작성하세요.

System:
친절한 SQL 교관

User:
SELECT문이란 무엇인가요?

Assistant:
데이터베이스에서 데이터를 조회하는 명령입니다.

문제 3

같은 데이터를 Conversational Prompt-Completion 형식으로 변환하세요.

문제 4

Tokenizer의 Chat Template을 문자열로 출력하세요.

문제 5

다음 두 설정의 결과 차이를 설명하세요.

add_generation_prompt=False
add_generation_prompt=True

문제 6

Prompt Token에는 Loss를 계산하지 않도록 설정하세요.

completion_only_loss=True

문제 7

Assistant Message에만 Loss를 계산하도록 설정하세요.

assistant_only_loss=True

필요한 Chat Template 조건도 확인하세요.

문제 8

Packing을 활성화하고 다음 설정을 적용하세요.

max_length:
512

packing_strategy:
bfd

문제 9

다음 LoRA 설정을 작성하세요.

Rank:
8

Alpha:
16

Dropout:
0.05

Target:
q_proj, v_proj

문제 10

SFTTrainer에서 LoRA Adapter만 학습되는지 확인하세요.

trainer.model.print_trainable_parameters()

문제 11

Learning Rate를 다음 세 값으로 비교하세요.

5e-5

1e-4

2e-4

문제 12

Packing 사용 전후의 학습 시간을 비교하세요.

문제 13

Completion Only Loss 사용 전후의 생성 결과를 비교하세요.

문제 14

학습된 Adapter를 저장하고 다시 불러오세요.

문제 15

다섯 개 질문에 대해 Base Model과 SFT Model 결과를 비교하세요.

문제 16

4비트 QLoRA 설정을 SFTTrainer에 연결하세요.

문제 17

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

문제 18

학습된 Adapter를 Base Model에 병합하세요.

문제 19

Gradio ChatInterface에 학습된 모델을 연결하세요.

문제 20

실전 프로젝트에 다음 기능을 추가하세요.

CSV·JSONL 학습 데이터 불러오기

데이터 중복 제거

Token 길이 통계

Packing 전후 속도 비교

Base·SFT 답변 비교표

사람 평가 결과 CSV 저장

Adapter Hub 비공개 업로드

Gradio 스트리밍 응답

58. 핵심 요약

설치

python -m pip install \
    trl \
    transformers \
    datasets \
    accelerate \
    peft

Conversational Dataset

example = {
    "messages": [
        {
            "role": "user",
            "content": "질문"
        },
        {
            "role": "assistant",
            "content": "정답"
        }
    ]
}

Prompt-Completion Dataset

example = {
    "prompt": [
        {
            "role": "user",
            "content": "질문"
        }
    ],
    "completion": [
        {
            "role": "assistant",
            "content": "정답"
        }
    ]
}

Chat Template

tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True
)

LoRA 설정

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,

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

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

SFTConfig

training_args = SFTConfig(
    output_dir="outputs/sft",

    learning_rate=1e-4,

    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,

    num_train_epochs=3,

    max_length=512,
    packing=False,

    completion_only_loss=True,

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

SFTTrainer

trainer = SFTTrainer(
    model=MODEL_ID,
    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config
)

학습

trainer.train()

Adapter 저장

trainer.save_model(
    "saved_adapter"
)

모델 불러오기

model = (
    AutoPeftModelForCausalLM
    .from_pretrained(
        "saved_adapter"
    )
)

추론

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

QLoRA

trainer = SFTTrainer(
    model=MODEL_ID,

    peft_config=lora_config,

    quantization_config=(
        quantization_config
    )
)

59. 마무리

이번 시간에는 Hugging Face TRL과 SFTTrainer를 이용해 대화형 AI 모델을 Instruction Dataset으로 Fine-tuning하는 방법을 알아보았습니다.

전체 흐름을 다시 정리해 보겠습니다.

질문과 모범 답안 준비

→ Conversational Dataset 생성

→ Prompt와 Completion 분리

→ Chat Template 적용

→ Tokenization

→ Prompt Token Masking

→ Completion Token Loss 계산

→ LoRA Adapter 학습

→ Validation 평가

→ Adapter 저장

→ 대화형 추론

SFT의 핵심은 단순히 많은 문장을 모델에게 보여주는 것이 아닙니다.

어떤 질문을 받았을 때

어떤 방식으로 생각하고

어떤 형식과 말투로

어떤 답변을 생성해야 하는가

이 행동 패턴을 모범 답안으로 가르치는 과정입니다.

Chat Template은 대화의 역할과 경계를 알려줍니다.

누가 지시하는가?

누가 답변하는가?

답변은 어디서 시작하는가?

한 대화는 어디서 끝나는가?

Completion Only Loss는 모델이 사용자의 질문을 따라 쓰는 대신 정답 응답을 생성하는 데 집중하게 합니다.

Packing은 짧은 대화 여러 개를 하나의 Sequence에 담아 Padding으로 낭비되는 공간을 줄입니다.

LoRA와 QLoRA는 거대한 모델 전체를 학습하지 않고 작은 Adapter만 업데이트할 수 있도록 도와줍니다.

Chat Template

→ 대화 형식


SFTTrainer

→ 지도 학습


Completion Only Loss

→ 정답에 집중


Packing

→ 연산 효율


LoRA·QLoRA

→ 메모리 효율

모든 부품이 연결되면 Base Model은 질문을 단순히 이어 쓰는 모델에서 사용자의 지시를 이해하고 정해진 방식으로 답하는 Assistant로 성장할 수 있습니다.

하지만 SFT 데이터가 곧 모델의 행동 교과서라는 사실을 잊어서는 안 됩니다.

무례한 답변 데이터

→ 무례한 모델


장황한 답변 데이터

→ 장황한 모델


틀린 코드 데이터

→ 자신감 있게 틀린 코드 생성


일관된 고품질 데이터

→ 일관된 답변 가능성 증가

AI는 모범 답안의 품질을 판단하고 골라서 배우지 않습니다.

주어진 교재를 매우 성실하게 따라 합니다.

개발자:
“왜 모델이 항상 다섯 문단으로 답하죠?”

데이터:
“모든 정답이 다섯 문단이었습니다.”

모델:
“학습한 대로 했습니다.”

SFT에서 가장 비싼 자원은 GPU만이 아닙니다.

정확하고 일관되며 안전한 고품질 모범 답안입니다. 🎓🤖

다음 편 예고

[Python 완전정복 시리즈 #40] DPOTrainer 완벽 이해하기 | 좋은 답변과 나쁜 답변을 비교해 AI의 선호도를 학습하는 방법

다음 시간에는 SFT를 마친 모델에 `chosen`과 `rejected` 답변을 제공해 더 유용하고 정확한 응답을 선호하도록 학습하는 DPO를 알아봅니다.

Preference Dataset, Reference Model, Beta, DPO Loss, Conversational Preference Format, LoRA·QLoRA 연결, Reward Margin, 평가, Adapter 저장과 Gradio 비교 화면까지 실습합니다.

#Python #파이썬 #Python강좌 #파이썬기초 #HuggingFace #TRL #SFTTrainer #SFT #SupervisedFineTuning #InstructionTuning #대화형AI #ChatModel #ChatTemplate #InstructionDataset #ConversationalDataset #PromptCompletion #CompletionOnlyLoss #AssistantOnlyLoss #Packing #LoRA #QLoRA #PEFT #Transformers #생성형AI #LLM #파인튜닝 #AI교관 #Gradio #PyTorch #코딩공부 #프로그래밍

댓글

0

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