이미지 로딩 중...
AI Generated
2025. 11. 12. · 5 Views
바닥부터 만드는 ChatGPT 11편 SmolTalk 데이터셋 통합
SmolTalk 데이터셋을 활용하여 대화형 AI 모델을 학습시키는 방법을 다룹니다. 데이터셋 로딩부터 전처리, 토크나이징, 그리고 실제 모델 학습까지 전체 파이프라인을 단계별로 구현합니다.
목차
- SmolTalk 데이터셋 소개
- 데이터셋 로딩과 구조 파악
- 대화 포맷 변환
- 토크나이저 설정
- 데이터 전처리 파이프라인
- 어텐션 마스크 생성
- 데이터 콜레이터 구현
- 학습 데이터셋 준비
- 모델 초기화와 설정
- 학습 루프 구현
1. SmolTalk 데이터셋 소개
시작하며
여러분이 ChatGPT 같은 대화형 AI를 만들고 싶을 때, 가장 먼저 부딪히는 문제는 무엇일까요? 바로 "어떤 데이터로 학습시킬 것인가?"입니다.
일반적인 텍스트 데이터와 달리, 대화형 AI는 자연스러운 대화 흐름, 맥락 이해, 적절한 응답 생성 능력이 필요합니다. 많은 초보 개발자들이 단순히 질문-답변 쌍을 모아서 학습시키려고 하지만, 이는 실제 대화의 복잡성을 담아내지 못합니다.
실제 대화는 여러 턴으로 이어지고, 이전 맥락을 기억해야 하며, 다양한 주제를 다룹니다. 바로 이럴 때 필요한 것이 SmolTalk 데이터셋입니다.
이 데이터셋은 고품질의 다중 턴 대화 데이터를 제공하여, 여러분의 모델이 실제 사람처럼 대화할 수 있도록 도와줍니다.
개요
간단히 말해서, SmolTalk은 대화형 AI 학습을 위해 특별히 설계된 고품질 대화 데이터셋입니다. HuggingFaceTB에서 제공하며, 다양한 주제와 스타일의 대화를 포함하고 있습니다.
왜 SmolTalk이 필요한지 실무 관점에서 설명하자면, 일반적인 텍스트 데이터셋으로는 대화의 흐름과 맥락을 학습하기 어렵습니다. SmolTalk은 실제 대화 패턴을 학습할 수 있도록 구조화되어 있어, 모델이 자연스러운 응답을 생성할 수 있습니다.
예를 들어, 사용자가 이전 대화 내용을 참조하는 경우에도 적절하게 대응할 수 있는 능력을 키울 수 있습니다. 기존에는 수동으로 대화 데이터를 수집하고 정제해야 했다면, 이제는 SmolTalk을 활용하여 즉시 고품질 데이터로 학습을 시작할 수 있습니다.
SmolTalk의 핵심 특징은 다음과 같습니다: 첫째, 다중 턴 대화 구조로 되어 있어 맥락 학습이 가능합니다. 둘째, 다양한 주제와 톤을 포함하여 일반화 성능이 뛰어납니다.
셋째, Hugging Face 플랫폼을 통해 쉽게 접근할 수 있습니다. 이러한 특징들이 대화형 AI 개발 시간을 크게 단축시켜줍니다.
코드 예제
from datasets import load_dataset
# SmolTalk 데이터셋 로딩
# Hugging Face에서 직접 가져오기
dataset = load_dataset("HuggingFaceTB/smoltalk", "all")
# 데이터셋 구조 확인
print(f"데이터셋 크기: {len(dataset['train'])}")
# 첫 번째 대화 샘플 확인
sample = dataset['train'][0]
print(f"대화 ID: {sample['conversation_id']}")
print(f"대화 내용: {sample['messages']}")
# 대화 턴 수 확인
print(f"대화 턴 수: {len(sample['messages'])}")
설명
이것이 하는 일: SmolTalk 데이터셋을 Hugging Face의 datasets 라이브러리를 사용하여 로딩하고, 데이터 구조를 파악하는 작업입니다. 첫 번째로, load_dataset("HuggingFaceTB/smoltalk", "all") 부분은 Hugging Face Hub에서 SmolTalk 데이터셋을 다운로드하고 메모리에 로딩합니다.
"all" 파라미터는 모든 대화 데이터를 가져오라는 의미입니다. 이렇게 하면 네트워크를 통해 데이터를 가져오지만, 캐싱되어 다음 실행 시에는 빠르게 로딩됩니다.
그 다음으로, 데이터셋의 크기와 구조를 확인하는 코드가 실행됩니다. dataset['train']은 학습용 데이터를 의미하며, 각 샘플은 하나의 대화를 나타냅니다.
conversation_id는 각 대화의 고유 식별자이고, messages는 실제 대화 내용을 담고 있는 리스트입니다. 마지막으로, len(sample['messages'])를 통해 해당 대화가 몇 개의 턴으로 구성되어 있는지 확인합니다.
이는 모델이 얼마나 긴 대화 맥락을 학습해야 하는지 파악하는 데 중요합니다. 여러분이 이 코드를 사용하면 즉시 고품질 대화 데이터에 접근할 수 있고, 데이터 구조를 이해하여 다음 전처리 단계를 계획할 수 있습니다.
또한 데이터셋의 크기를 파악하여 학습 시간과 리소스를 미리 예측할 수 있습니다.
실전 팁
💡 처음 데이터셋을 로딩할 때는 인터넷 연결이 필요하지만, 자동으로 캐시되므로 이후에는 오프라인에서도 사용 가능합니다
💡 데이터셋이 크다면 streaming=True 옵션을 사용하여 메모리 부담을 줄일 수 있습니다
💡 실제 학습 전에 여러 샘플을 확인하여 데이터 품질과 형식을 검증하는 것이 중요합니다
💡 데이터셋의 split 정보를 확인하여 train/validation/test가 어떻게 나뉘어 있는지 파악하세요
💡 대화 턴 수의 분포를 확인하면 모델의 최대 시퀀스 길이를 설정하는 데 도움이 됩니다
2. 데이터셋 로딩과 구조 파악
시작하며
여러분이 새로운 데이터셋을 받았을 때, 바로 모델 학습에 뛰어드는 것은 위험합니다. 데이터의 구조를 이해하지 못하면, 전처리 과정에서 오류가 발생하거나, 잘못된 형식으로 학습하여 모델 성능이 떨어질 수 있습니다.
실제로 많은 초보 개발자들이 데이터 구조를 제대로 파악하지 않고 학습을 시작했다가, 나중에 모든 코드를 다시 작성해야 하는 상황에 직면합니다. 특히 대화 데이터는 일반 텍스트보다 복잡한 중첩 구조를 가지고 있어, 더욱 신중한 분석이 필요합니다.
바로 이럴 때 필요한 것이 체계적인 데이터 탐색입니다. 데이터의 키 구조, 값의 타입, 실제 내용을 꼼꼼히 확인하면 이후 모든 과정이 훨씬 수월해집니다.
개요
간단히 말해서, 데이터 구조 파악은 본격적인 작업을 시작하기 전에 데이터의 형태와 내용을 이해하는 필수 과정입니다. 이는 마치 건물을 짓기 전에 설계도를 보는 것과 같습니다.
왜 이 과정이 필요한지 실무 관점에서 설명하자면, 데이터 구조를 모르면 어떤 전처리가 필요한지, 어떤 필드를 사용해야 하는지 알 수 없습니다. SmolTalk의 경우 각 대화가 어떤 키를 가지고 있는지, 메시지가 어떤 형식인지 파악해야 합니다.
예를 들어, role과 content 필드가 어떻게 구성되어 있는지 알아야 적절한 프롬프트 형식으로 변환할 수 있습니다. 기존에는 데이터를 하나하나 수동으로 확인하며 구조를 파악했다면, 이제는 파이썬 코드를 사용하여 체계적이고 빠르게 데이터를 분석할 수 있습니다.
데이터 탐색의 핵심 요소는 다음과 같습니다: 첫째, 데이터의 키와 타입을 확인하여 어떤 정보가 있는지 파악합니다. 둘째, 실제 값을 출력하여 내용과 형식을 이해합니다.
셋째, 데이터의 통계를 계산하여 분포와 특성을 파악합니다. 이러한 과정을 거치면 데이터를 완벽하게 이해할 수 있습니다.
코드 예제
import json
from collections import Counter
# 데이터셋 샘플 상세 분석
sample = dataset['train'][0]
# 전체 키 확인
print("데이터 키:", sample.keys())
# 메시지 구조 확인
for i, message in enumerate(sample['messages']):
print(f"\n턴 {i+1}:")
print(f" 역할: {message['role']}")
print(f" 내용 미리보기: {message['content'][:100]}...")
# 역할 분포 확인
roles = [msg['role'] for conv in dataset['train'] for msg in conv['messages']]
role_counts = Counter(roles)
print(f"\n역할 분포: {dict(role_counts)}")
설명
이것이 하는 일: 데이터셋의 구조를 다각도로 분석하여, 각 대화가 어떻게 구성되어 있는지, 어떤 역할(user, assistant 등)이 있는지, 그리고 실제 내용이 어떤 형식인지 파악하는 작업입니다. 첫 번째로, sample.keys()를 통해 각 대화 샘플이 어떤 필드를 가지고 있는지 확인합니다.
일반적으로 conversation_id, messages, 그리고 메타데이터 필드들이 포함되어 있습니다. 이를 통해 어떤 정보를 활용할 수 있는지 파악할 수 있습니다.
그 다음으로, 각 메시지를 순회하면서 role과 content를 출력합니다. role은 보통 'user', 'assistant', 'system' 중 하나이며, 이는 ChatML 형식의 기본 구조입니다.
content는 실제 대화 내용을 담고 있으며, 길이가 길 수 있으므로 처음 100자만 미리보기로 출력합니다. 이렇게 하면 메모리를 절약하면서도 내용을 확인할 수 있습니다.
마지막으로, Counter를 사용하여 전체 데이터셋에서 각 역할이 얼마나 자주 등장하는지 통계를 계산합니다. 이는 데이터의 균형을 확인하는 데 중요합니다.
예를 들어, user와 assistant의 비율이 거의 1:1이어야 정상적인 대화 데이터입니다. 여러분이 이 코드를 사용하면 데이터의 전체 구조를 명확히 이해할 수 있고, 이를 바탕으로 전처리 파이프라인을 설계할 수 있습니다.
또한 데이터의 품질을 검증하여, 잘못된 형식이나 누락된 정보가 있는지 미리 파악할 수 있습니다.
실전 팁
💡 데이터를 처음 확인할 때는 최소 10개 이상의 샘플을 살펴보며 일관성을 검증하세요
💡 role 필드에 예상치 못한 값이 있는지 확인하여 데이터 정제가 필요한지 판단하세요
💡 content의 최대 길이를 확인하면 토크나이저의 max_length 설정에 도움이 됩니다
💡 json.dumps()를 사용하여 샘플을 예쁘게 출력하면 구조를 더 잘 이해할 수 있습니다
💡 빈 content나 None 값이 있는지 확인하여 에러를 미리 방지하세요
3. 대화 포맷 변환
시작하며
여러분이 대화 데이터를 모델에 입력하려고 할 때, 단순히 텍스트를 이어붙이면 될까요? 아닙니다.
모델은 누가 말했는지, 어디서 대화가 시작되고 끝나는지 명확하게 알아야 합니다. 이를 위해 특별한 포맷이 필요합니다.
많은 개발자들이 대화를 그냥 하나의 긴 문자열로 합쳐서 학습시키려다가, 모델이 user와 assistant를 구분하지 못하는 문제를 겪습니다. 이는 모델이 자신이 언제 말해야 하고 언제 들어야 하는지 혼란스러워하게 만듭니다.
바로 이럴 때 필요한 것이 ChatML 포맷입니다. 이는 각 메시지에 명확한 역할 태그를 붙여서, 모델이 대화의 구조를 정확히 이해할 수 있도록 해줍니다.
개요
간단히 말해서, ChatML은 대화를 구조화된 형식으로 변환하는 표준 포맷입니다. 각 메시지 앞에 특수 토큰을 붙여 역할을 표시하고, 메시지의 시작과 끝을 명확히 구분합니다.
왜 포맷 변환이 필요한지 실무 관점에서 설명하자면, 모델은 텍스트의 패턴을 학습하므로 일관된 형식이 매우 중요합니다. ChatML 형식을 사용하면 모델이 "지금은 사용자가 말하는 구간이구나", "이제 내가 답변할 차례구나"를 명확히 알 수 있습니다.
예를 들어, 고객 서비스 챗봇을 만들 때, 모델이 고객과 상담원의 역할을 정확히 구분해야 적절한 응답을 생성할 수 있습니다. 기존에는 각 프로젝트마다 다른 포맷을 사용했다면, 이제는 ChatML이라는 표준 형식을 사용하여 일관성을 유지하고 다른 모델과 호환성을 높일 수 있습니다.
ChatML의 핵심 요소는 다음과 같습니다: 첫째, 각 메시지는 "<|im_start|>role\ncontent<|im_end|>" 형식으로 래핑됩니다. 둘째, 특수 토큰들이 명확한 경계를 만들어 파싱을 쉽게 합니다.
셋째, 모든 대화형 모델에서 널리 사용되는 표준 형식입니다. 이러한 일관성이 모델의 학습 효율을 크게 높여줍니다.
코드 예제
def format_chat_messages(messages):
"""대화 메시지를 ChatML 형식으로 변환"""
formatted = ""
for message in messages:
role = message['role']
content = message['content']
# ChatML 형식: <|im_start|>role\ncontent<|im_end|>
formatted += f"<|im_start|>{role}\n{content}<|im_end|>\n"
return formatted.strip()
# 샘플 대화 변환
sample_messages = dataset['train'][0]['messages']
formatted_chat = format_chat_messages(sample_messages)
print(formatted_chat[:500]) # 처음 500자만 출력
설명
이것이 하는 일: 원본 대화 데이터를 ChatML 표준 형식으로 변환하여, 모델이 학습할 수 있는 구조화된 텍스트를 만드는 작업입니다. 첫 번째로, format_chat_messages 함수는 메시지 리스트를 받아서 각 메시지를 순회합니다.
각 메시지에서 role과 content를 추출하여, 이를 특수 토큰으로 감싸는 작업을 수행합니다. 이 특수 토큰들(<|im_start|>와 <|im_end|>)은 나중에 토크나이저에 추가되어 모델이 인식할 수 있게 됩니다.
그 다음으로, 각 메시지를 "시작토큰+역할+줄바꿈+내용+끝토큰+줄바꿈" 형식으로 조합합니다. 줄바꿈 문자(\n)는 역할과 내용을 구분하고, 각 메시지를 명확히 분리하는 역할을 합니다.
이렇게 하면 모델이 텍스트를 파싱할 때 경계를 정확히 인식할 수 있습니다. 마지막으로, 모든 메시지를 연결한 후 strip()을 사용하여 앞뒤 공백을 제거합니다.
이렇게 만들어진 포맷된 텍스트는 모델의 입력으로 바로 사용될 수 있습니다. 여러분이 이 코드를 사용하면 복잡한 대화 구조를 모델이 이해할 수 있는 형식으로 쉽게 변환할 수 있습니다.
또한 이 형식은 GPT, LLaMA 등 대부분의 대화형 모델에서 표준으로 사용되므로, 다른 프로젝트에서도 재사용할 수 있습니다. 특히 멀티턴 대화에서 맥락을 유지하는 데 매우 효과적입니다.
실전 팁
💡 특수 토큰은 반드시 토크나이저에 등록해야 하므로, 나중에 add_special_tokens()를 호출하는 것을 잊지 마세요
💡 system 메시지가 있다면 대화의 맨 처음에 배치하여 모델에게 역할 지시를 명확히 전달하세요
💡 formatted 문자열을 만들 때 리스트를 사용한 후 join()하면 메모리 효율이 더 좋습니다
💡 실제 학습 시에는 assistant의 응답 부분만 loss를 계산하도록 마스킹해야 합니다
💡 긴 대화는 토큰 길이 제한을 초과할 수 있으므로, 최근 N개 턴만 사용하는 전략을 고려하세요
4. 토크나이저 설정
시작하며
여러분이 텍스트를 모델에 입력하려고 할 때, 모델은 텍스트를 직접 이해할 수 없습니다. 모델은 숫자만 처리할 수 있으므로, 텍스트를 토큰(작은 단위)으로 나누고 각 토큰을 숫자로 변환해야 합니다.
이 과정을 토크나이징이라고 합니다. 많은 초보자들이 일반 토크나이저를 그대로 사용하다가, ChatML 특수 토큰을 인식하지 못해 이상한 결과를 얻는 경우가 많습니다.
특수 토큰이 여러 개의 일반 토큰으로 쪼개지면, 모델은 그것이 특별한 의미를 가진 토큰인지 알 수 없게 됩니다. 바로 이럴 때 필요한 것이 토크나이저에 특수 토큰을 추가하는 작업입니다.
이를 통해 ChatML 형식의 경계 표시를 모델이 정확히 인식할 수 있게 됩니다.
개요
간단히 말해서, 토크나이저 설정은 기존 토크나이저에 ChatML 특수 토큰을 추가하여, 대화 형식을 올바르게 토크나이징할 수 있도록 만드는 과정입니다. 왜 토크나이저 설정이 필요한지 실무 관점에서 설명하자면, 특수 토큰이 제대로 인식되지 않으면 모델이 대화의 경계를 구분할 수 없습니다.
예를 들어, <|im_start|>가 5개의 일반 토큰으로 쪼개지면, 모델은 이것이 단순한 텍스트인지 특수 마커인지 알 수 없습니다. 특수 토큰으로 등록하면 하나의 고유한 토큰 ID를 받게 되어, 모델이 명확히 인식할 수 있습니다.
기존에는 토크나이저 소스 코드를 수정하거나 새로운 토크나이저를 처음부터 학습해야 했다면, 이제는 add_special_tokens() 메서드를 사용하여 간단히 특수 토큰을 추가할 수 있습니다. 토크나이저 설정의 핵심 단계는 다음과 같습니다: 첫째, 필요한 특수 토큰 리스트를 정의합니다.
둘째, add_special_tokens()로 토크나이저 어휘에 추가합니다. 셋째, 모델의 임베딩 레이어를 새 어휘 크기에 맞게 조정합니다.
이 세 단계를 거치면 모델이 특수 토큰을 완벽하게 처리할 수 있습니다.
코드 예제
from transformers import AutoTokenizer
# 기본 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained("gpt2")
# ChatML 특수 토큰 정의
special_tokens = {
"additional_special_tokens": [
"<|im_start|>",
"<|im_end|>"
],
"pad_token": "<|pad|>"
}
# 특수 토큰 추가
num_added = tokenizer.add_special_tokens(special_tokens)
print(f"추가된 토큰 수: {num_added}")
# 토큰 ID 확인
print(f"im_start ID: {tokenizer.convert_tokens_to_ids('<|im_start|>')}")
print(f"im_end ID: {tokenizer.convert_tokens_to_ids('<|im_end|>')}")
print(f"어휘 크기: {len(tokenizer)}")
설명
이것이 하는 일: GPT-2 토크나이저를 로드한 후, ChatML 형식에 필요한 특수 토큰들을 어휘에 추가하여, 모델이 대화 구조를 정확히 토크나이징할 수 있도록 설정하는 작업입니다. 첫 번째로, AutoTokenizer.from_pretrained("gpt2")는 사전 학습된 GPT-2 토크나이저를 로드합니다.
GPT-2는 Byte Pair Encoding(BPE) 방식을 사용하며, 약 50,000개의 어휘를 가지고 있습니다. 이 토크나이저를 기반으로 우리의 대화 모델을 학습할 것입니다.
그 다음으로, 특수 토큰 딕셔너리를 정의합니다. additional_special_tokens는 ChatML 경계 표시용 토큰들이고, pad_token은 배치 처리 시 짧은 시퀀스를 같은 길이로 맞추는 데 사용됩니다.
GPT-2는 원래 pad_token이 없으므로 추가해야 합니다. add_special_tokens() 메서드는 이 토큰들을 어휘의 끝에 추가하고, 추가된 토큰 개수를 반환합니다.
마지막으로, 각 특수 토큰의 ID를 확인합니다. convert_tokens_to_ids()는 토큰 문자열을 해당하는 정수 ID로 변환합니다.
이 ID들은 모델이 실제로 처리하는 값입니다. 새 어휘 크기를 출력하여, 토큰이 제대로 추가되었는지 확인합니다.
여러분이 이 코드를 사용하면 기존 토크나이저를 대화형 AI에 맞게 확장할 수 있습니다. 중요한 점은 토크나이저가 수정되면 모델의 임베딩 레이어도 model.resize_token_embeddings(len(tokenizer))로 크기를 조정해야 한다는 것입니다.
이를 통해 모델과 토크나이저가 완벽하게 동기화됩니다.
실전 팁
💡 특수 토큰을 추가한 후에는 반드시 model.resize_token_embeddings()를 호출하여 모델의 어휘 크기를 업데이트하세요
💡 pad_token이 없는 토크나이저를 사용할 때는 eos_token을 pad_token으로 설정하거나 새로운 pad_token을 추가하세요
💡 토크나이저를 저장할 때는 save_pretrained()를 사용하면 특수 토큰 설정도 함께 저장됩니다
💡 특수 토큰이 제대로 작동하는지 테스트하려면 간단한 ChatML 형식 텍스트를 토크나이징해보고 decode()로 복원해보세요
💡 배치 처리를 위해 padding_side='left'로 설정하면 생성 작업에 더 적합합니다
5. 데이터 전처리 파이프라인
시작하며
여러분이 수천 개의 대화 데이터를 하나씩 처리한다고 상상해보세요. 각 대화를 ChatML로 변환하고, 토크나이징하고, 텐서로 변환하는 작업을 반복하는 것은 매우 비효율적이고 시간이 오래 걸립니다.
많은 개발자들이 for 루프를 사용하여 데이터를 하나씩 처리하다가, 수만 개의 샘플을 처리하는 데 몇 시간씩 걸리는 경험을 합니다. 특히 대화 데이터는 각 샘플의 길이가 다르고 복잡한 구조를 가지고 있어, 효율적인 배치 처리가 더욱 중요합니다.
바로 이럴 때 필요한 것이 Hugging Face datasets의 map() 함수입니다. 이는 전체 데이터셋에 대한 병렬 처리를 지원하여, 전처리 시간을 획기적으로 단축시켜줍니다.
개요
간단히 말해서, 데이터 전처리 파이프라인은 원본 대화 데이터를 모델이 학습할 수 있는 형태로 자동 변환하는 시스템입니다. 포맷 변환, 토크나이징, 텐서 변환을 한 번에 처리합니다.
왜 파이프라인이 필요한지 실무 관점에서 설명하자면, 데이터 전처리는 학습 과정에서 가장 시간이 많이 걸리는 부분 중 하나입니다. 효율적인 파이프라인을 구축하면 전처리를 한 번만 수행하고 결과를 캐싱하여, 이후 실험에서 시간을 절약할 수 있습니다.
예를 들어, 하이퍼파라미터를 바꿔가며 여러 번 학습할 때, 전처리는 한 번만 하고 재사용할 수 있습니다. 기존에는 데이터를 수동으로 반복 처리하고 결과를 파일에 저장했다면, 이제는 datasets 라이브러리의 map() 함수로 병렬 처리와 자동 캐싱을 동시에 활용할 수 있습니다.
전처리 파이프라인의 핵심 요소는 다음과 같습니다: 첫째, 전처리 함수를 정의하여 각 샘플에 적용할 변환을 명시합니다. 둘째, map() 함수로 전체 데이터셋에 병렬 적용합니다.
셋째, batched=True 옵션으로 배치 단위 처리를 활성화하여 속도를 높입니다. 넷째, num_proc 파라미터로 멀티프로세싱을 활용합니다.
이러한 기법들이 전처리 속도를 10배 이상 향상시킬 수 있습니다.
코드 예제
def preprocess_function(examples):
"""배치 단위로 대화 데이터 전처리"""
formatted_texts = []
# 각 대화를 ChatML 형식으로 변환
for messages in examples['messages']:
formatted = format_chat_messages(messages)
formatted_texts.append(formatted)
# 토크나이징 (배치 처리)
tokenized = tokenizer(
formatted_texts,
truncation=True,
max_length=2048,
padding=False # 동적 패딩은 나중에 collator에서 수행
)
return tokenized
# 전체 데이터셋에 전처리 적용 (병렬 처리)
processed_dataset = dataset['train'].map(
preprocess_function,
batched=True,
batch_size=1000,
num_proc=4, # 4개의 프로세스 사용
remove_columns=dataset['train'].column_names # 원본 컬럼 제거
)
print(f"전처리 완료: {len(processed_dataset)} 샘플")
설명
이것이 하는 일: 전체 대화 데이터셋을 받아서 각 대화를 ChatML 형식으로 변환하고, 토크나이징까지 완료하여 모델이 바로 학습할 수 있는 형태로 만드는 통합 파이프라인입니다. 첫 번째로, preprocess_function은 배치 단위로 여러 샘플을 동시에 처리합니다.
examples['messages']는 여러 대화의 리스트이며, 각각을 순회하면서 앞서 정의한 format_chat_messages 함수로 ChatML 형식으로 변환합니다. 리스트 컴프리헨션 대신 for 루프를 사용한 것은 가독성을 위해서지만, 실제로는 리스트 컴프리헨션이 약간 더 빠릅니다.
그 다음으로, 변환된 텍스트들을 토크나이저에 한 번에 전달합니다. truncation=True는 최대 길이를 초과하는 시퀀스를 자르고, max_length=2048은 각 샘플의 최대 토큰 수를 제한합니다.
padding=False로 설정한 이유는 지금 당장 패딩하지 않고, 나중에 DataCollator에서 배치별로 동적 패딩을 수행하는 것이 메모리 효율적이기 때문입니다. 마지막으로, dataset['train'].map()을 호출하여 전처리 함수를 전체 데이터셋에 적용합니다.
batched=True는 배치 단위 처리를 활성화하고, batch_size=1000은 한 번에 1000개씩 처리합니다. num_proc=4는 4개의 CPU 코어를 사용하여 병렬 처리를 수행하며, remove_columns는 원본 컬럼들을 제거하여 메모리를 절약합니다.
처리 결과는 자동으로 캐시되어 다음 실행 시 빠르게 로드됩니다. 여러분이 이 코드를 사용하면 수만 개의 대화 데이터를 몇 분 안에 전처리할 수 있습니다.
단일 프로세스로 처리할 때보다 4배 이상 빠르며, 결과가 캐시되어 반복 실험 시 즉시 사용할 수 있습니다. 또한 메모리 사용량도 최적화되어 대용량 데이터셋도 처리할 수 있습니다.
실전 팁
💡 num_proc을 CPU 코어 수에 맞게 설정하면 최대 성능을 얻을 수 있지만, 너무 많이 설정하면 오버헤드가 발생할 수 있습니다
💡 캐시를 무효화하려면 load_from_cache_file=False 옵션을 사용하세요
💡 전처리 중 오류가 발생하면 작은 batch_size로 시작하여 디버깅하세요
💡 메모리가 부족하면 batched=False로 하나씩 처리하거나, 데이터를 청크로 나누어 처리하세요
💡 전처리 결과를 save_to_disk()로 저장하면 다음 세션에서 즉시 로드할 수 있습니다
6. 어텐션 마스크 생성
시작하며
여러분이 모델을 학습시킬 때, 모든 토큰에 대해 똑같이 손실을 계산하면 어떤 문제가 생길까요? 패딩 토큰까지 학습하게 되어 모델이 의미 없는 패딩을 생성하는 법을 배우게 됩니다.
또한 user의 질문 부분까지 학습하면, 모델이 질문을 생성하는 법을 배우게 되어 원하지 않는 동작을 할 수 있습니다. 많은 초보자들이 전체 시퀀스에 대해 loss를 계산하다가, 모델이 이상한 출력을 생성하거나 패딩 토큰을 반복하는 문제를 겪습니다.
특히 대화 모델에서는 assistant의 응답 부분만 학습해야 하는데, 이를 구분하지 않으면 모델이 역할을 혼동하게 됩니다. 바로 이럴 때 필요한 것이 어텐션 마스크와 레이블 마스킹입니다.
이를 통해 모델이 어느 부분에 집중하고, 어느 부분을 학습해야 하는지 명확히 지정할 수 있습니다.
개요
간단히 말해서, 어텐션 마스크는 모델이 어느 토큰에 주목해야 하는지 알려주는 이진 배열이고, 레이블 마스킹은 어느 토큰에 대해 손실을 계산할지 결정하는 기법입니다. 왜 마스킹이 필요한지 실무 관점에서 설명하자면, 효율적이고 정확한 학습을 위해서입니다.
패딩 토큰은 의미가 없으므로 어텐션에서 제외해야 하고, user의 입력은 이미 주어진 것이므로 손실 계산에서 제외해야 합니다. 예를 들어, 고객이 "환불하고 싶어요"라고 하면, 모델은 이 문장을 생성하는 법을 배울 필요가 없고, "알겠습니다.
환불 절차를 안내해드리겠습니다"라는 응답을 생성하는 법만 배우면 됩니다. 기존에는 마스킹을 수동으로 계산하거나 복잡한 논리를 작성해야 했다면, 이제는 토크나이저의 기능과 간단한 텐서 연산으로 자동화할 수 있습니다.
마스킹의 핵심 요소는 다음과 같습니다: 첫째, 어텐션 마스크는 실제 토큰에 1, 패딩에 0을 할당하여 모델이 패딩을 무시하게 합니다. 둘째, 레이블 마스킹은 학습하지 않을 토큰에 -100을 할당하여 PyTorch의 CrossEntropyLoss가 자동으로 무시하게 합니다.
셋째, user와 system 메시지는 -100으로, assistant 메시지는 실제 토큰 ID로 설정합니다. 이러한 전략이 모델이 올바른 부분만 학습하도록 보장합니다.
코드 예제
def create_labels_with_masking(input_ids, tokenizer):
"""assistant 응답 부분만 학습하도록 레이블 생성"""
labels = input_ids.clone()
# 특수 토큰 ID
im_start_id = tokenizer.convert_tokens_to_ids("<|im_start|>")
im_end_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
# user와 system 메시지를 -100으로 마스킹
in_assistant = False
for i in range(len(labels)):
if input_ids[i] == im_start_id:
# 다음 토큰이 'assistant'인지 확인
if i+1 < len(input_ids):
next_token = tokenizer.decode([input_ids[i+1]])
in_assistant = 'assistant' in next_token
# assistant가 아닌 부분은 -100으로 마스킹
if not in_assistant:
labels[i] = -100
if input_ids[i] == im_end_id:
labels[i] = -100 # 끝 토큰도 마스킹
return labels
설명
이것이 하는 일: 입력 토큰 ID 시퀀스를 받아서, assistant의 응답 부분만 학습하도록 레이블을 생성하고, 나머지 부분은 -100으로 마스킹하여 손실 계산에서 제외하는 작업입니다. 첫 번째로, labels = input_ids.clone()으로 입력 시퀀스의 복사본을 만듭니다.
레이블은 기본적으로 다음 토큰을 예측하는 것이므로, 입력과 동일하게 시작합니다. 그런 다음 특수 토큰들의 ID를 가져와서 경계를 인식할 수 있도록 준비합니다.
그 다음으로, 토큰 시퀀스를 순회하면서 현재 어느 역할의 메시지인지 추적합니다. <|im_start|> 토큰을 만나면 다음 토큰을 디코드하여 'assistant'인지 확인합니다.
in_assistant 플래그를 사용하여 현재 assistant 응답 구간인지 추적하고, 아니라면 해당 토큰을 -100으로 설정합니다. PyTorch의 CrossEntropyLoss는 -100 값을 자동으로 무시하므로, 이 부분에 대해 손실이 계산되지 않습니다.
마지막으로, <|im_end|> 토큰도 -100으로 마스킹합니다. 끝 토큰 자체는 학습할 필요가 없기 때문입니다.
이렇게 만들어진 레이블 배열은 assistant의 실제 응답 토큰만 원래 ID를 유지하고, 나머지는 모두 -100으로 설정되어 있습니다. 여러분이 이 코드를 사용하면 모델이 정확히 원하는 부분만 학습하게 할 수 있습니다.
이는 모델의 성능을 크게 향상시키며, 불필요한 패턴을 학습하는 것을 방지합니다. 또한 계산 효율도 높아지는데, 실제로 학습할 토큰만 손실을 계산하므로 불필요한 연산이 줄어듭니다.
실전 팁
💡 -100은 PyTorch CrossEntropyLoss의 ignore_index 기본값이므로, 다른 값을 사용하려면 손실 함수를 수정해야 합니다
💡 전체 대화가 아닌 마지막 assistant 응답만 학습하고 싶다면, 마지막 assistant 구간만 마스킹하지 않도록 로직을 수정하세요
💡 system 프롬프트를 학습에 포함하고 싶다면 in_assistant 조건에 'system'도 추가하세요
💡 레이블 마스킹이 제대로 되었는지 확인하려면 labels != -100인 토큰들만 디코드해보세요
💡 효율을 위해 벡터화된 연산을 사용할 수도 있지만, 가독성과 디버깅을 위해 루프 방식도 충분히 좋습니다
7. 데이터 콜레이터 구현
시작하며
여러분이 배치 학습을 할 때, 각 샘플의 길이가 다르면 어떻게 해야 할까요? 텐서는 고정된 크기를 가져야 하므로, 길이가 다른 시퀀스들을 하나의 배치로 묶을 수 없습니다.
그렇다고 모든 샘플을 최대 길이로 패딩하면 메모리가 엄청나게 낭비됩니다. 많은 개발자들이 전체 데이터셋을 가장 긴 샘플에 맞춰 패딩하다가, GPU 메모리 부족 오류를 겪습니다.
예를 들어, 대부분의 대화가 512 토큰인데 한 개가 2048 토큰이라면, 모든 샘플을 2048로 패딩하는 것은 매우 비효율적입니다. 바로 이럴 때 필요한 것이 DataCollator입니다.
이는 배치별로 동적 패딩을 수행하여, 각 배치 내에서 가장 긴 시퀀스에만 맞춰 패딩합니다.
개요
간단히 말해서, DataCollator는 배치 생성 시 호출되는 함수로, 여러 샘플을 하나의 텐서 배치로 결합하고, 동적 패딩과 레이블 마스킹을 수행합니다. 왜 DataCollator가 필요한지 실무 관점에서 설명하자면, 메모리 효율과 학습 속도를 극대화하기 위해서입니다.
동적 패딩을 사용하면 각 배치마다 필요한 만큼만 패딩하므로, 평균적으로 50% 이상의 메모리를 절약할 수 있습니다. 예를 들어, 첫 번째 배치의 최대 길이가 600이고 두 번째 배치의 최대 길이가 1200이라면, 각각에 맞게 패딩하여 불필요한 계산을 줄입니다.
기존에는 전처리 단계에서 모든 샘플을 고정 길이로 패딩했다면, 이제는 DataCollator를 사용하여 학습 중 실시간으로 최적화된 패딩을 수행할 수 있습니다. DataCollator의 핵심 기능은 다음과 같습니다: 첫째, 배치 내 최대 길이를 찾아 그에 맞춰 패딩합니다.
둘째, 패딩 토큰을 추가하고 어텐션 마스크를 업데이트합니다. 셋째, 레이블도 동일하게 패딩하되 -100으로 채웁니다.
넷째, 모든 샘플을 텐서로 변환하여 GPU에 바로 전달할 수 있게 합니다. 이러한 기능들이 학습 파이프라인을 매끄럽게 만들어줍니다.
코드 예제
from dataclasses import dataclass
from transformers import DataCollatorWithPadding
@dataclass
class DataCollatorForChatML:
"""ChatML 형식의 대화 데이터를 위한 커스텀 콜레이터"""
tokenizer: AutoTokenizer
def __call__(self, features):
# 배치 내 최대 길이 찾기
max_length = max(len(f['input_ids']) for f in features)
batch = {
'input_ids': [],
'attention_mask': [],
'labels': []
}
for feature in features:
# 패딩 길이 계산
padding_length = max_length - len(feature['input_ids'])
# input_ids 패딩
batch['input_ids'].append(
feature['input_ids'] + [self.tokenizer.pad_token_id] * padding_length
)
# attention_mask 패딩 (패딩 부분은 0)
batch['attention_mask'].append(
feature['attention_mask'] + [0] * padding_length
)
# labels 패딩 (패딩 부분은 -100)
labels = create_labels_with_masking(feature['input_ids'], self.tokenizer)
batch['labels'].append(
labels.tolist() + [-100] * padding_length
)
# 리스트를 텐서로 변환
import torch
return {k: torch.tensor(v) for k, v in batch.items()}
# 콜레이터 인스턴스 생성
data_collator = DataCollatorForChatML(tokenizer=tokenizer)
설명
이것이 하는 일: 여러 개의 전처리된 샘플을 받아서 배치로 결합하고, 각 배치 내에서 가장 긴 시퀀스에 맞춰 동적으로 패딩을 적용하며, 모든 데이터를 텐서로 변환하는 작업입니다. 첫 번째로, @dataclass 데코레이터를 사용하여 간결하게 클래스를 정의합니다.
__call__ 메서드를 구현하면 이 객체를 함수처럼 호출할 수 있습니다. DataLoader는 각 배치를 생성할 때 이 메서드를 자동으로 호출합니다.
features 파라미터는 배치에 포함될 샘플들의 리스트입니다. 그 다음으로, 배치 내 모든 샘플의 길이를 확인하여 최대 길이를 찾습니다.
이것이 동적 패딩의 핵심입니다. 전체 데이터셋의 최대 길이가 아닌, 현재 배치의 최대 길이만 사용하므로 메모리가 절약됩니다.
그런 다음 각 샘플을 순회하면서 필요한 패딩 길이를 계산하고, input_ids에는 pad_token_id를, attention_mask에는 0을, labels에는 -100을 추가합니다. 마지막으로, 모든 리스트를 PyTorch 텐서로 변환합니다.
torch.tensor(v)는 중첩 리스트를 2D 텐서로 변환하며, 이는 GPU에 바로 전달될 수 있는 형태입니다. 반환된 딕셔너리는 모델의 forward() 메서드에 **kwargs로 풀어서 전달됩니다.
여러분이 이 코드를 사용하면 배치 학습의 효율을 극대화할 수 있습니다. 메모리 사용량이 줄어들어 더 큰 배치 크기를 사용할 수 있고, 이는 학습 속도와 안정성을 향상시킵니다.
또한 코드가 자동으로 처리하므로 수동으로 패딩을 관리할 필요가 없습니다.
실전 팁
💡 return_tensors="pt" 옵션을 토크나이저에 사용하면 자동으로 텐서 변환이 되지만, 커스텀 로직이 필요하면 수동 변환이 더 유연합니다
💡 배치 크기가 작으면 동적 패딩의 이점이 적으므로, 최소 8 이상의 배치 크기를 사용하세요
💡 DataCollator에서 데이터 증강(augmentation)을 수행할 수도 있습니다
💡 디버깅 시에는 콜레이터가 반환하는 텐서의 shape를 출력하여 크기를 확인하세요
💡 Hugging Face의 DataCollatorForLanguageModeling을 상속받아 확장할 수도 있습니다
8. 학습 데이터셋 준비
시작하며
여러분이 모델을 학습시킬 때, 전체 데이터를 모두 학습에만 사용하면 어떤 문제가 생길까요? 모델이 학습 데이터에 과적합(overfitting)되어, 새로운 데이터에 대한 성능이 떨어질 수 있습니다.
또한 학습 중 모델의 실제 성능을 평가할 방법이 없습니다. 많은 초보자들이 validation set을 따로 분리하지 않고 학습하다가, 학습 손실은 계속 감소하지만 실제 사용 시 모델이 제대로 작동하지 않는 경험을 합니다.
특히 대화 모델은 과적합이 쉽게 발생하므로, 검증 데이터를 통한 모니터링이 매우 중요합니다. 바로 이럴 때 필요한 것이 train/validation split입니다.
데이터를 학습용과 검증용으로 나누어, 모델의 일반화 성능을 지속적으로 모니터링하면서 학습할 수 있습니다.
개요
간단히 말해서, 데이터셋 분할은 전체 데이터를 학습용(train)과 검증용(validation)으로 나누는 과정이며, 이를 통해 모델의 실제 성능을 평가하고 과적합을 방지할 수 있습니다. 왜 데이터셋 분할이 필요한지 실무 관점에서 설명하자면, 학습 중 모델의 진짜 성능을 알아야 하기 때문입니다.
학습 데이터에 대한 손실은 계속 감소할 수 있지만, 이것이 실제 성능 향상을 의미하지는 않습니다. 검증 데이터는 모델이 한 번도 본 적 없는 새로운 데이터이므로, 여기서의 성능이 실제 배포 후 성능을 더 잘 나타냅니다.
예를 들어, 고객 문의 답변 봇을 만들 때, 학습 데이터의 특정 표현만 암기하는 것이 아니라 새로운 질문에도 답할 수 있어야 합니다. 기존에는 수동으로 인덱스를 계산하여 데이터를 나누거나 파일을 분리했다면, 이제는 datasets 라이브러리의 train_test_split() 메서드로 간단히 분할할 수 있습니다.
데이터셋 분할의 핵심 원칙은 다음과 같습니다: 첫째, 일반적으로 80-90%를 학습용, 10-20%를 검증용으로 사용합니다. 둘째, 무작위 분할을 통해 두 세트가 비슷한 분포를 가지도록 합니다.
셋째, seed를 고정하여 재현 가능한 분할을 만듭니다. 넷째, 검증 데이터는 절대 학습에 사용하지 않습니다.
이러한 원칙들이 신뢰할 수 있는 성능 평가를 가능하게 합니다.
코드 예제
# 전처리된 데이터셋을 train/validation으로 분할
split_dataset = processed_dataset.train_test_split(
test_size=0.1, # 10%를 검증용으로
seed=42, # 재현성을 위한 시드 고정
shuffle=True # 무작위 섞기
)
train_dataset = split_dataset['train']
eval_dataset = split_dataset['test'] # 'test'라는 이름이지만 실제로는 validation
print(f"학습 샘플 수: {len(train_dataset)}")
print(f"검증 샘플 수: {len(eval_dataset)}")
# 샘플 확인
sample = train_dataset[0]
print(f"\n샘플 키: {sample.keys()}")
print(f"Input IDs 길이: {len(sample['input_ids'])}")
print(f"Attention Mask 길이: {len(sample['attention_mask'])}")
# DataLoader 생성
from torch.utils.data import DataLoader
train_dataloader = DataLoader(
train_dataset,
batch_size=4,
shuffle=True,
collate_fn=data_collator
)
eval_dataloader = DataLoader(
eval_dataset,
batch_size=4,
shuffle=False, # 검증 시에는 섞지 않음
collate_fn=data_collator
)
설명
이것이 하는 일: 전처리가 완료된 데이터셋을 학습용과 검증용으로 분할하고, 각각에 대한 DataLoader를 생성하여 모델 학습에 바로 사용할 수 있도록 준비하는 작업입니다. 첫 번째로, train_test_split() 메서드는 데이터셋을 무작위로 섞은 후 지정된 비율로 분할합니다.
test_size=0.1은 10%를 테스트(검증)용으로 분리한다는 의미이고, seed=42는 매번 같은 방식으로 분할되도록 무작위성을 고정합니다. 이는 실험의 재현성을 위해 매우 중요합니다.
shuffle=True는 분할 전에 데이터를 섞어서, 순서에 따른 편향을 방지합니다. 그 다음으로, 분할된 데이터셋에서 train과 test(실제로는 validation) 세트를 추출합니다.
datasets 라이브러리는 'test'라는 이름을 사용하지만, 이는 검증용입니다. 진짜 테스트 세트는 별도로 관리해야 합니다.
각 세트의 크기를 출력하여 분할이 제대로 되었는지 확인하고, 샘플을 하나 가져와서 구조를 점검합니다. 마지막으로, PyTorch의 DataLoader를 생성합니다.
DataLoader는 데이터셋을 배치 단위로 순회하는 이터레이터를 제공합니다. batch_size=4는 한 번에 4개의 샘플을 묶어서 처리하고, shuffle=True는 매 에폭마다 데이터 순서를 섞습니다.
중요한 것은 collate_fn=data_collator인데, 이는 앞서 만든 커스텀 콜레이터를 사용하여 배치를 생성한다는 의미입니다. 검증용 DataLoader는 shuffle=False로 설정하여, 일관된 평가를 보장합니다.
여러분이 이 코드를 사용하면 완전히 준비된 학습 파이프라인을 얻게 됩니다. 이제 단순히 DataLoader를 순회하면서 배치를 가져와 모델에 전달하기만 하면 됩니다.
모든 전처리, 패딩, 마스킹이 자동으로 처리되므로, 학습 로직에만 집중할 수 있습니다.
실전 팁
💡 검증 세트는 최소 1000개 이상의 샘플을 유지하여 신뢰할 수 있는 평가를 하세요
💡 데이터가 시간 순서대로 정렬되어 있다면, shuffle=True가 더욱 중요합니다
💡 배치 크기는 GPU 메모리에 따라 조정하되, 너무 작으면 학습이 불안정해질 수 있습니다
💡 num_workers > 0으로 설정하면 데이터 로딩이 병렬화되어 학습 속도가 향상됩니다
💡 pin_memory=True 옵션을 사용하면 GPU 전송 속도가 빨라집니다
9. 모델 초기화와 설정
시작하며
여러분이 데이터 준비를 마쳤다면, 이제 실제로 학습할 모델이 필요합니다. 처음부터 모델을 만들 수도 있지만, 사전 학습된 모델을 활용하면 훨씬 적은 데이터와 시간으로 좋은 성능을 얻을 수 있습니다.
많은 개발자들이 모델을 로드한 후 토크나이저와 동기화하지 않거나, 학습률 등의 하이퍼파라미터를 제대로 설정하지 않아 학습이 실패하는 경험을 합니다. 특히 토크나이저에 특수 토큰을 추가했다면, 모델의 임베딩 레이어도 반드시 조정해야 합니다.
바로 이럴 때 필요한 것이 체계적인 모델 초기화입니다. 사전 학습된 가중치를 로드하고, 새로운 어휘에 맞게 조정하며, 학습 파라미터를 설정하는 과정을 거쳐야 합니다.
개요
간단히 말해서, 모델 초기화는 사전 학습된 모델을 로드하고, 현재 작업에 맞게 설정을 조정하며, 학습 준비를 완료하는 과정입니다. 왜 모델 초기화가 중요한지 실무 관점에서 설명하자면, 잘못된 설정은 학습 실패나 성능 저하로 이어집니다.
예를 들어, 토크나이저에 3개의 특수 토큰을 추가했는데 모델의 임베딩 크기를 조정하지 않으면, 모델은 새 토큰의 임베딩을 찾지 못해 에러가 발생합니다. 또한 학습률이 너무 높으면 학습이 발산하고, 너무 낮으면 학습이 너무 느려집니다.
기존에는 모델 설정을 수동으로 하나씩 조정했다면, 이제는 Hugging Face의 Trainer API를 사용하여 대부분의 설정을 자동화하고 모범 사례를 적용할 수 있습니다. 모델 초기화의 핵심 단계는 다음과 같습니다: 첫째, 사전 학습된 모델을 로드하여 전이 학습을 활용합니다.
둘째, resize_token_embeddings()로 모델을 새 어휘 크기에 맞춥니다. 셋째, TrainingArguments로 학습률, 배치 크기, 에폭 수 등을 설정합니다.
넷째, Trainer 객체를 생성하여 모든 구성 요소를 통합합니다. 이러한 단계들이 성공적인 학습의 기반이 됩니다.
코드 예제
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer
# 사전 학습된 GPT-2 모델 로드
model = AutoModelForCausalLM.from_pretrained("gpt2")
# 토크나이저에 맞게 임베딩 크기 조정
model.resize_token_embeddings(len(tokenizer))
print(f"모델 어휘 크기: {model.config.vocab_size}")
# 학습 파라미터 설정
training_args = TrainingArguments(
output_dir="./smoltalk-chatbot", # 체크포인트 저장 경로
num_train_epochs=3, # 에폭 수
per_device_train_batch_size=4, # 배치 크기
per_device_eval_batch_size=4,
learning_rate=5e-5, # 학습률
warmup_steps=500, # 워밍업 스텝
logging_steps=100, # 로깅 주기
eval_steps=500, # 평가 주기
save_steps=1000, # 체크포인트 저장 주기
evaluation_strategy="steps", # 스텝마다 평가
save_total_limit=3, # 최대 3개의 체크포인트만 유지
fp16=True, # Mixed precision 사용 (GPU 필요)
load_best_model_at_end=True, # 학습 후 최고 모델 로드
)
print("모델 초기화 완료!")
설명
이것이 하는 일: GPT-2 사전 학습 모델을 로드하고, 확장된 어휘에 맞게 임베딩 레이어를 조정하며, 학습에 필요한 모든 하이퍼파라미터를 설정하여 학습 준비를 완료하는 작업입니다. 첫 번째로, AutoModelForCausalLM.from_pretrained("gpt2")는 Hugging Face Hub에서 GPT-2 모델을 다운로드하고 로드합니다.
'CausalLM'은 인과적 언어 모델을 의미하며, 이전 토큰들을 보고 다음 토큰을 예측하는 작업에 적합합니다. GPT-2는 약 1.5억 개의 파라미터를 가진 모델로, 대화 생성의 좋은 출발점이 됩니다.
그 다음으로, resize_token_embeddings()는 모델의 임베딩 레이어를 새로운 어휘 크기에 맞춰 확장합니다. 앞서 토크나이저에 특수 토큰을 추가했으므로, 모델도 그 토큰들에 대한 임베딩을 가져야 합니다.
새로 추가된 토큰의 임베딩은 무작위로 초기화되며, 학습 과정에서 적절한 값을 배우게 됩니다. 마지막으로, TrainingArguments는 학습 설정을 담는 객체입니다.
num_train_epochs=3은 전체 데이터셋을 3번 반복하고, learning_rate=5e-5는 Adam 옵티마이저의 학습률입니다. warmup_steps=500은 학습 초기에 학습률을 점진적으로 증가시켜 안정성을 높입니다.
evaluation_strategy="steps"는 일정 스텝마다 검증을 수행하고, fp16=True는 mixed precision을 사용하여 학습 속도를 2배 가까이 높입니다. 여러분이 이 코드를 사용하면 검증된 설정으로 학습을 시작할 수 있습니다.
TrainingArguments의 모든 파라미터는 여러 연구를 통해 효과가 입증된 기본값을 제공하므로, 초보자도 좋은 결과를 얻을 수 있습니다. 또한 체크포인트가 자동으로 저장되어, 학습 중 문제가 생겨도 복구할 수 있습니다.
실전 팁
💡 GPU가 없다면 fp16=False로 설정하고, CPU 또는 MPS (Mac)를 사용하세요
💡 메모리가 부족하면 gradient_accumulation_steps를 사용하여 가상으로 더 큰 배치 크기를 시뮬레이션하세요
💡 학습률은 모델 크기에 따라 조정해야 하며, 큰 모델일수록 작은 학습률이 필요합니다
💡 warmup_ratio=0.1을 사용하면 전체 스텝의 10%를 워밍업으로 사용할 수 있습니다
💡 logging_dir을 설정하면 TensorBoard로 학습 과정을 시각화할 수 있습니다
10. 학습 루프 구현
시작하며
여러분이 모든 준비를 마쳤다면, 이제 실제로 모델을 학습시킬 차례입니다. 학습 루프는 데이터를 모델에 입력하고, 손실을 계산하며, 가중치를 업데이트하는 과정을 반복합니다.
많은 초보자들이 학습 루프를 직접 작성하다가 옵티마이저 설정, 그래디언트 클리핑, 학습률 스케줄링 등을 놓쳐서 학습이 제대로 되지 않는 경험을 합니다. 또한 학습 중 검증을 수행하고, 체크포인트를 저장하고, 로그를 기록하는 등의 보일러플레이트 코드를 작성하는 것은 복잡하고 오류가 발생하기 쉽습니다.
바로 이럴 때 필요한 것이 Hugging Face의 Trainer API입니다. 이는 모든 학습 관련 작업을 자동화하여, 여러분은 단순히 train() 메서드를 호출하기만 하면 됩니다.
개요
간단히 말해서, 학습 루프는 모델이 데이터로부터 학습하는 반복 과정이며, Trainer API는 이 과정을 자동화하고 최적화하는 고수준 인터페이스입니다. 왜 Trainer API를 사용하는지 실무 관점에서 설명하자면, 학습 과정의 모든 복잡한 세부 사항을 신경 쓸 필요 없이 핵심에 집중할 수 있기 때문입니다.
Trainer는 분산 학습, mixed precision, 그래디언트 누적, 학습률 스케줄링, early stopping 등 프로덕션 레벨의 기능을 모두 내장하고 있습니다. 예를 들어, 여러 GPU로 학습을 확장하고 싶다면, 단순히 accelerate 설정만 바꾸면 코드 수정 없이 가능합니다.
기존에는 수백 줄의 학습 코드를 직접 작성해야 했다면, 이제는 Trainer 객체를 생성하고 train()을 호출하는 것만으로 동일한 기능을 얻을 수 있습니다. Trainer의 핵심 기능은 다음과 같습니다: 첫째, 자동으로 옵티마이저와 스케줄러를 설정합니다.
둘째, 매 스텝마다 손실을 계산하고 역전파를 수행합니다. 셋째, 정기적으로 검증을 수행하고 결과를 로깅합니다.
넷째, 최고 성능의 모델을 자동으로 저장합니다. 다섯째, 학습 진행 상황을 progress bar로 시각화합니다.
이러한 기능들이 학습 과정을 매우 간단하고 안정적으로 만들어줍니다.
코드 예제
# Trainer 객체 생성
trainer = Trainer(
model=model, # 학습할 모델
args=training_args, # 학습 설정
train_dataset=train_dataset, # 학습 데이터
eval_dataset=eval_dataset, # 검증 데이터
data_collator=data_collator, # 배치 생성 함수
tokenizer=tokenizer, # 토크나이저 (저장용)
)
# 학습 시작
print("학습을 시작합니다...")
train_result = trainer.train()
# 학습 결과 출력
print(f"\n학습 완료!")
print(f"총 학습 시간: {train_result.metrics['train_runtime']:.2f}초")
print(f"최종 학습 손실: {train_result.metrics['train_loss']:.4f}")
# 최종 평가
eval_result = trainer.evaluate()
print(f"\n검증 결과:")
print(f"검증 손실: {eval_result['eval_loss']:.4f}")
print(f"Perplexity: {eval_result['eval_loss']:.4f}")
# 모델과 토크나이저 저장
trainer.save_model("./smoltalk-chatbot-final")
tokenizer.save_pretrained("./smoltalk-chatbot-final")
print("모델 저장 완료!")
설명
이것이 하는 일: Trainer 객체를 생성하여 모델, 데이터, 학습 설정을 통합하고, train() 메서드로 실제 학습을 수행하며, 학습 후 평가와 모델 저장까지 완료하는 전체 학습 파이프라인입니다. 첫 번째로, Trainer 생성자에 모든 구성 요소를 전달합니다.
model은 학습할 모델, args는 TrainingArguments 객체, train_dataset과 eval_dataset은 앞서 준비한 데이터셋, data_collator는 배치 생성 함수, tokenizer는 모델과 함께 저장하기 위한 것입니다. Trainer는 이 정보를 바탕으로 내부적으로 DataLoader, 옵티마이저, 스케줄러를 자동으로 설정합니다.
그 다음으로, trainer.train()을 호출하면 실제 학습이 시작됩니다. 이 메서드는 설정된 에폭 수만큼 데이터를 반복하면서, 각 배치에 대해 forward pass(순전파), loss 계산, backward pass(역전파), 가중치 업데이트를 수행합니다.
동시에 설정된 주기마다 검증을 수행하고, 로그를 기록하며, 체크포인트를 저장합니다. progress bar가 표시되어 학습 진행 상황을 실시간으로 확인할 수 있습니다.
마지막으로, 학습이 완료되면 결과 메트릭을 출력하고, trainer.evaluate()로 최종 검증을 수행합니다. Perplexity는 언어 모델의 성능을 나타내는 지표로, exp(loss)로 계산됩니다.
낮을수록 좋으며, 좋은 대화 모델은 보통 10-30 사이의 perplexity를 가집니다. 마지막으로 save_model()로 학습된 모델과 토크나이저를 디스크에 저장합니다.
여러분이 이 코드를 사용하면 프로덕션 레벨의 학습 파이프라인을 몇 줄의 코드로 구현할 수 있습니다. Trainer는 자동으로 최적화를 적용하고, 문제를 조기에 감지하며, 학습 과정을 안정적으로 관리합니다.
학습이 완료되면 즉시 모델을 로드하여 대화 생성에 사용할 수 있습니다.
실전 팁
💡 학습 중 loss가 급증하거나 NaN이 나오면 학습률을 낮추거나 gradient clipping을 강화하세요
💡 resume_from_checkpoint="./path/to/checkpoint"로 중단된 학습을 이어서 할 수 있습니다
💡 callbacks를 추가하여 커스텀 로직(예: early stopping, custom logging)을 주입할 수 있습니다
💡 학습 후 모델을 Hugging Face Hub에 push_to_hub()로 업로드하여 공유할 수 있습니다
💡 실제 대화 품질을 평가하려면 generate()로 샘플 응답을 생성해보고 수동으로 확인하세요