이미지 로딩 중...
AI Generated
2025. 11. 11. · 5 Views
FineWeb-Edu 데이터셋 전처리 완벽 가이드
ChatGPT를 바닥부터 만들기 위한 핵심 단계인 FineWeb-Edu 데이터셋 전처리 과정을 다룹니다. HuggingFace datasets 라이브러리를 활용한 대용량 데이터 처리부터 토크나이저 적용, 배치 처리까지 실전에서 바로 활용할 수 있는 전처리 기법을 배웁니다.
목차
- FineWeb-Edu 데이터셋 로딩 - HuggingFace datasets 활용법
- 토크나이저 초기화 - GPT-2 BPE 토크나이저 설정
- 토큰화 함수 구현 - 배치 처리 최적화
- 시퀀스 길이 필터링 - 짧은 문서 제거
- 데이터 청킹 - 고정 길이 시퀀스 생성
- 어텐션 마스크 재생성 - 청킹 후 마스크 업데이트
- 데이터셋 셔플링 - 학습 안정성 향상
- 데이터로더 생성 - PyTorch 학습 준비
- 데이터 저장 - 전처리 결과 캐싱
- 학습 루프 통합 - 실제 모델 학습 예제
1. FineWeb-Edu 데이터셋 로딩 - HuggingFace datasets 활용법
시작하며
여러분이 언어 모델을 학습시키려고 할 때 가장 먼저 마주하는 벽이 무엇인가요? 바로 "어떻게 수십 GB, 수백 GB의 데이터를 효율적으로 불러올 것인가"입니다.
일반적인 pandas나 파일 읽기 방식으로는 메모리가 부족하거나 로딩 시간이 너무 오래 걸리는 문제가 발생합니다. 이런 문제는 실제 AI 개발 현장에서 자주 발생합니다.
특히 GPT 같은 대형 언어 모델을 학습할 때는 수백만 개의 문서를 처리해야 하는데, 잘못된 데이터 로딩 방식은 프로젝트 전체를 지연시킬 수 있습니다. 바로 이럴 때 필요한 것이 HuggingFace의 datasets 라이브러리입니다.
이 라이브러리는 스트리밍 방식으로 데이터를 불러와 메모리 효율성을 극대화하고, Apache Arrow 기반으로 초고속 처리를 가능하게 합니다.
개요
간단히 말해서, FineWeb-Edu 데이터셋은 교육적 가치가 높은 웹 문서들을 모아놓은 대규모 텍스트 데이터셋입니다. HuggingFace Hub에서 공개되어 있으며, ChatGPT와 같은 언어 모델 학습에 최적화되어 있습니다.
왜 일반 웹 크롤링 데이터가 아닌 FineWeb-Edu를 사용할까요? 이 데이터셋은 품질 필터링이 적용되어 있어 모델이 더 정확하고 유용한 지식을 학습할 수 있습니다.
예를 들어, 광고나 스팸이 제거되고 교육적 콘텐츠만 선별된 상태입니다. 기존에는 웹 데이터를 직접 크롤링하고 정제해야 했다면, 이제는 datasets 라이브러리로 단 몇 줄의 코드로 고품질 데이터를 즉시 사용할 수 있습니다.
이 라이브러리의 핵심 특징은 다음과 같습니다: 1) 스트리밍 모드로 메모리 절약, 2) 자동 캐싱으로 재사용 시 빠른 로딩, 3) 다양한 데이터 포맷 지원. 이러한 특징들이 대용량 데이터 처리를 가능하게 만듭니다.
코드 예제
# HuggingFace datasets 라이브러리 임포트
from datasets import load_dataset
# FineWeb-Edu 데이터셋을 스트리밍 모드로 로딩
# streaming=True로 설정하면 전체 데이터를 다운로드하지 않고 필요한 만큼만 가져옴
dataset = load_dataset(
"HuggingFaceFW/fineweb-edu",
name="sample-10BT", # 10B 토큰 샘플 버전
split="train",
streaming=True
)
# 첫 5개 샘플 확인
for idx, example in enumerate(dataset.take(5)):
print(f"Document {idx}: {example['text'][:100]}...") # 처음 100자만 출력
print(f"URL: {example['url']}\n")
설명
이 코드가 하는 일은 HuggingFace Hub에서 FineWeb-Edu 데이터셋을 메모리 효율적으로 불러오는 것입니다. 전체 데이터를 한 번에 다운로드하지 않고 필요한 만큼만 가져오는 스트리밍 방식을 사용합니다.
첫 번째로, load_dataset 함수는 데이터셋의 이름과 설정을 받아 자동으로 HuggingFace Hub에서 데이터를 가져옵니다. streaming=True 옵션을 설정하면 전체 데이터를 디스크에 저장하지 않고 필요한 부분만 메모리에 로드합니다.
이렇게 하면 100GB짜리 데이터셋도 8GB RAM에서 처리할 수 있습니다. 그 다음으로, name="sample-10BT" 파라미터가 실행되면서 전체 데이터셋이 아닌 10억 토큰 샘플 버전을 선택합니다.
실험 단계에서는 샘플로 시작하고, 나중에 전체 데이터셋으로 확장하는 것이 효율적입니다. split="train"은 학습용 데이터를 선택하는 옵션입니다.
마지막으로, dataset.take(5)가 첫 5개 문서를 가져와서 각 문서의 텍스트와 URL을 출력합니다. 이를 통해 데이터의 구조와 품질을 빠르게 확인할 수 있습니다.
각 example은 딕셔너리 형태로 'text', 'url', 'date' 등의 필드를 포함하고 있습니다. 여러분이 이 코드를 사용하면 대용량 데이터셋을 빠르게 탐색하고 실험할 수 있습니다.
전체 다운로드 없이 바로 데이터를 확인하고, 필요한 전처리 파이프라인을 설계할 수 있어 개발 시간을 크게 단축시킵니다.
실전 팁
💡 스트리밍 모드에서는 len(dataset)이 작동하지 않습니다. 데이터 개수를 알고 싶다면 streaming=False로 먼저 메타데이터를 확인하세요.
💡 처음 실험할 때는 take(1000) 같은 작은 샘플로 시작하여 파이프라인을 검증한 후 전체 데이터로 확장하세요.
💡 cache_dir 파라미터를 설정하면 다운로드된 데이터를 원하는 위치에 저장할 수 있어 디스크 관리가 편리합니다.
💡 네트워크가 불안정한 환경에서는 download_mode="reuse_cache_if_exists"를 사용하여 중단된 다운로드를 이어받으세요.
2. 토크나이저 초기화 - GPT-2 BPE 토크나이저 설정
시작하며
여러분이 텍스트 데이터를 신경망에 입력하려고 할 때 이런 고민을 해본 적 있나요? "한글과 영어가 섞인 텍스트를 어떻게 숫자로 변환할까?
단어 단위? 문자 단위?
아니면 다른 방법?" 이 선택이 모델 성능을 크게 좌우합니다. 이런 문제는 모든 자연어 처리 프로젝트에서 필수적으로 마주치는 과제입니다.
잘못된 토크나이저 선택은 어휘 크기를 불필요하게 늘려 메모리를 낭비하거나, 너무 작은 어휘로 표현력을 떨어뜨립니다. 바로 이럴 때 필요한 것이 BPE(Byte Pair Encoding) 토크나이저입니다.
GPT-2에서 사용된 이 방식은 자주 등장하는 문자 조합을 효율적으로 압축하면서도 미등록 단어 문제를 완벽히 해결합니다.
개요
간단히 말해서, 토크나이저는 텍스트를 모델이 이해할 수 있는 숫자 시퀀스로 변환하는 도구입니다. GPT-2 BPE 토크나이저는 OpenAI가 개발한 서브워드 기반 토크나이저로, 50,257개의 어휘를 사용합니다.
왜 BPE 토크나이저가 필요할까요? 단어 단위 토크나이저는 어휘 크기가 수십만 개로 커지고 신조어를 처리하지 못합니다.
반면 문자 단위는 시퀀스가 너무 길어집니다. BPE는 이 둘의 장점을 결합하여 'running'을 'run'+'ning'처럼 분리해 효율성과 표현력을 동시에 달성합니다.
기존에는 각 프로젝트마다 커스텀 토크나이저를 학습해야 했다면, 이제는 검증된 사전학습 토크나이저를 바로 사용할 수 있습니다. GPT-2 토크나이저는 다양한 언어와 도메인에서 이미 성능이 입증되었습니다.
핵심 특징은 다음과 같습니다: 1) 서브워드 단위로 미등록 단어 없음, 2) 빠른 인코딩/디코딩 속도, 3) 다국어 지원. 이러한 특징들이 실제 프로덕션 환경에서 안정적인 텍스트 처리를 보장합니다.
코드 예제
# Transformers 라이브러리에서 GPT-2 토크나이저 임포트
from transformers import GPT2TokenizerFast
# 사전학습된 GPT-2 토크나이저 로딩
tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")
# 특수 토큰 설정 (문장 끝을 나타내는 토큰)
tokenizer.pad_token = tokenizer.eos_token
# 토크나이저 테스트
sample_text = "Hello, world! 안녕하세요, GPT를 학습하고 있습니다."
tokens = tokenizer.encode(sample_text)
print(f"Original: {sample_text}")
print(f"Tokens: {tokens}")
print(f"Token count: {len(tokens)}")
# 다시 텍스트로 디코딩
decoded = tokenizer.decode(tokens)
print(f"Decoded: {decoded}")
설명
이 코드가 하는 일은 HuggingFace Transformers 라이브러리에서 사전학습된 GPT-2 토크나이저를 불러와 텍스트를 숫자로 변환하는 것입니다. 이 과정이 모든 언어 모델 학습의 첫 단계입니다.
첫 번째로, GPT2TokenizerFast.from_pretrained("gpt2")는 OpenAI가 공개한 GPT-2의 토크나이저 가중치와 어휘 사전을 자동으로 다운로드합니다. 'Fast' 버전은 Rust로 구현되어 Python 버전보다 10배 이상 빠릅니다.
이 토크나이저는 이미 50,257개의 서브워드를 학습한 상태입니다. 그 다음으로, tokenizer.pad_token = tokenizer.eos_token 설정이 실행됩니다.
GPT-2는 원래 패딩 토큰이 없지만, 배치 처리를 위해서는 필요합니다. EOS(End of Sentence) 토큰을 패딩으로 재사용하면 추가 어휘 없이 문제를 해결할 수 있습니다.
마지막으로, encode 메서드가 텍스트를 토큰 ID 리스트로 변환합니다. 예를 들어 "Hello"는 [15496]으로, "world"는 [6894]로 변환됩니다.
한글도 서브워드 단위로 자동 처리되며, decode 메서드로 원본 텍스트를 복원할 수 있습니다. 여러분이 이 코드를 사용하면 어떤 텍스트든 일관된 방식으로 숫자화할 수 있습니다.
이는 신경망 학습의 필수 전제조건이며, 검증된 토크나이저를 사용함으로써 안정성과 성능을 동시에 확보할 수 있습니다.
실전 팁
💡 GPT2TokenizerFast 대신 GPT2Tokenizer를 사용하면 속도가 느립니다. 항상 Fast 버전을 사용하세요.
💡 커스텀 특수 토큰이 필요하면 tokenizer.add_special_tokens({'additional_special_tokens': ['[CUSTOM]']})로 추가할 수 있습니다.
💡 토큰 개수 제한이 있다면 tokenizer.encode(text, max_length=1024, truncation=True)를 사용하여 자동 자르기를 설정하세요.
💡 tokenizer.save_pretrained('./my_tokenizer')로 토크나이저를 저장하면 나중에 정확히 같은 설정으로 재사용할 수 있습니다.
💡 다국어 프로젝트에서는 GPT-2보다 facebook/opt-125m이나 EleutherAI/gpt-neo-125M의 토크나이저가 더 나은 성능을 보일 수 있습니다.
3. 토큰화 함수 구현 - 배치 처리 최적화
시작하며
여러분이 수백만 개의 문서를 토큰화할 때 가장 답답한 순간이 언제인가요? 바로 한 문서씩 처리하면서 몇 시간씩 기다려야 할 때입니다.
1초에 10개 문서를 처리한다면 100만 개는 27시간이 걸립니다. 이런 비효율은 실제 프로덕션 환경에서 치명적입니다.
데이터가 업데이트될 때마다 하루씩 기다려야 한다면 빠른 실험과 개선이 불가능합니다. 특히 GPU를 사용할 때 배치 처리 없이는 하드웨어 성능의 10%도 활용하지 못합니다.
바로 이럴 때 필요한 것이 datasets 라이브러리의 map 함수입니다. 이 함수는 자동으로 멀티프로세싱을 적용하고, 배치 단위로 처리하여 처리 속도를 수십 배 향상시킵니다.
개요
간단히 말해서, 배치 처리는 여러 개의 데이터를 한 번에 묶어서 처리하는 기법입니다. 토큰화에서는 100개의 문서를 개별적으로 처리하는 대신 한 번에 처리하여 오버헤드를 극적으로 줄입니다.
왜 배치 처리가 필수일까요? 토크나이저는 내부적으로 많은 최적화를 수행하는데, 배치로 입력을 받으면 이 최적화를 극대화할 수 있습니다.
예를 들어, Rust 기반 Fast 토크나이저는 배치 처리 시 SIMD 명령어를 활용하여 병렬 처리합니다. 기존에는 for 루프로 하나씩 처리하며 토크나이저를 반복 호출했다면, 이제는 한 번의 호출로 수백 개를 동시에 처리할 수 있습니다.
이는 I/O 대기 시간을 줄이고 CPU 캐시 효율성을 높입니다. 핵심 특징은 다음과 같습니다: 1) 멀티프로세싱으로 CPU 코어 완전 활용, 2) 메모리 효율적인 청크 단위 처리, 3) 자동 에러 핸들링과 재시도.
이러한 특징들이 대규모 데이터 전처리를 현실적으로 만듭니다.
코드 예제
# 배치 토큰화 함수 정의
def tokenize_function(examples):
# examples는 여러 문서를 담은 딕셔너리 (batch)
# 'text' 필드에서 모든 텍스트를 한 번에 토큰화
outputs = tokenizer(
examples['text'],
truncation=True, # 최대 길이 초과 시 자르기
max_length=1024, # GPT-2의 최대 컨텍스트 길이
padding=False, # 메모리 절약을 위해 패딩은 나중에
return_attention_mask=True # 어텐션 마스크 생성
)
return outputs
# 데이터셋에 토큰화 함수 적용
tokenized_dataset = dataset.map(
tokenize_function,
batched=True, # 배치 모드 활성화
batch_size=1000, # 한 번에 1000개 문서 처리
remove_columns=['text', 'url'] # 원본 텍스트는 제거하여 메모리 절약
)
설명
이 코드가 하는 일은 대량의 텍스트 문서를 효율적으로 토큰화하는 파이프라인을 구축하는 것입니다. 개별 처리 대신 배치 처리로 전환하여 처리 속도를 극대화합니다.
첫 번째로, tokenize_function은 examples라는 딕셔너리를 받습니다. 이 딕셔너리의 'text' 키에는 여러 문서의 텍스트가 리스트 형태로 들어 있습니다.
예를 들어 batch_size=1000이면 1000개의 텍스트가 한 번에 처리됩니다. truncation=True와 max_length=1024는 긴 문서를 자동으로 잘라 메모리 오버플로우를 방지합니다.
그 다음으로, dataset.map 함수가 모든 데이터에 이 함수를 적용합니다. batched=True 옵션이 핵심인데, 이것이 활성화되면 함수가 한 번에 하나의 예제가 아닌 배치를 받습니다.
batch_size=1000으로 설정하면 토크나이저가 1000개를 동시에 처리하여 단일 처리보다 20-50배 빠릅니다. 마지막으로, remove_columns=['text', 'url']이 원본 텍스트와 URL을 삭제합니다.
토큰화 후에는 이 정보가 불필요하며, 제거하면 디스크와 메모리 사용량을 70% 이상 줄일 수 있습니다. 결과적으로 'input_ids'와 'attention_mask'만 남아 학습에 바로 사용할 수 있는 형태가 됩니다.
여러분이 이 코드를 사용하면 수백만 개의 문서도 몇 시간 내에 처리할 수 있습니다. 멀티프로세싱이 자동으로 적용되어 8코어 CPU에서는 거의 8배의 속도 향상을 얻을 수 있으며, 진행 상황 표시줄로 실시간 모니터링도 가능합니다.
실전 팁
💡 batch_size를 너무 크게 설정하면 메모리 오류가 발생합니다. 16GB RAM에서는 1000-2000이 적당하며, 더 많은 메모리가 있다면 5000까지 시도해보세요.
💡 num_proc=4처럼 프로세스 개수를 명시하면 멀티프로세싱을 제어할 수 있습니다. CPU 코어 수만큼 설정하되, 1-2개는 시스템용으로 남겨두세요.
💡 map 함수는 자동으로 캐싱합니다. 같은 처리를 다시 실행하면 즉시 완료되므로, 실험 중에는 캐시를 활용하세요.
💡 에러가 발생하면 map(..., load_from_cache_file=False)로 캐시를 무시하고 재실행해보세요.
💡 진행 상황을 보고 싶지 않다면 map(..., disable_tqdm=True)로 진행 표시줄을 끌 수 있습니다.
4. 시퀀스 길이 필터링 - 짧은 문서 제거
시작하며
여러분이 모델을 학습시킬 때 이런 데이터를 본 적 있나요? "404 Not Found", "Copyright 2023", "Loading..." 같은 의미 없는 짧은 텍스트들.
이런 데이터가 학습 세트에 섞여 있으면 모델이 무엇을 배울까요? 이런 문제는 웹에서 수집한 데이터에서 필연적으로 발생합니다.
짧은 텍스트는 정보가 부족하여 모델 학습에 도움이 되지 않을 뿐만 아니라, 배치 처리 시 패딩으로 인한 계산 낭비를 초래합니다. 예를 들어 1024 토큰 중 10개만 실제 내용이고 나머지는 패딩이라면 GPU의 95%가 낭비됩니다.
바로 이럴 때 필요한 것이 시퀀스 길이 기반 필터링입니다. 최소 길이 기준을 설정하여 의미 있는 문서만 남기면 학습 효율성과 모델 품질을 동시에 향상시킬 수 있습니다.
개요
간단히 말해서, 시퀀스 길이 필터링은 토큰 개수가 일정 기준 이하인 문서를 제거하는 전처리 단계입니다. ChatGPT 같은 모델 학습에서는 일반적으로 50-100 토큰 이상의 문서만 사용합니다.
왜 이 필터링이 중요할까요? 언어 모델은 문맥을 학습합니다.
짧은 텍스트는 충분한 문맥을 제공하지 못해 모델이 다음 단어를 예측하는 능력을 키우기 어렵습니다. 예를 들어, "딥러닝은 인공지능의 한 분야로서 여러 층의 신경망을 사용합니다"라는 문장은 "딥러닝"만 있는 것보다 훨씬 많은 것을 학습할 수 있게 합니다.
기존에는 필터링 없이 모든 데이터를 학습에 사용했다면, 이제는 품질 기준을 적용하여 학습 시간과 비용을 줄이면서도 더 나은 결과를 얻을 수 있습니다. 데이터 양보다 품질이 더 중요합니다.
핵심 특징은 다음과 같습니다: 1) 계산 효율성 향상 (패딩 감소), 2) 데이터 품질 개선, 3) 학습 안정성 증가. 이러한 특징들이 실제 모델 성능 향상으로 이어집니다.
코드 예제
# 시퀀스 길이 필터링 함수 정의
def filter_short_sequences(example):
# input_ids의 길이가 최소 기준 이상인지 확인
# 50 토큰 미만은 너무 짧아 학습에 부적합
min_length = 50
return len(example['input_ids']) >= min_length
# 필터링 적용
filtered_dataset = tokenized_dataset.filter(
filter_short_sequences,
num_proc=4 # 4개의 프로세스로 병렬 처리
)
# 필터링 전후 비교
print("Before filtering: ~unknown (streaming mode)")
sample_before = list(tokenized_dataset.take(100))
sample_after = list(filtered_dataset.take(100))
print(f"Sample size before: {len(sample_before)}")
print(f"Sample size after: {len(sample_after)}")
설명
이 코드가 하는 일은 토큰화된 데이터셋에서 너무 짧은 문서들을 제거하여 학습 데이터의 품질을 높이는 것입니다. 이는 모델 성능에 직접적인 영향을 미치는 중요한 단계입니다.
첫 번째로, filter_short_sequences 함수는 각 예제의 'input_ids' 길이를 확인합니다. 'input_ids'는 토큰화 단계에서 생성된 토큰 ID 리스트로, 그 길이가 곧 문서의 토큰 개수입니다.
min_length=50은 경험적으로 검증된 기준으로, 이보다 짧으면 의미 있는 문맥을 형성하기 어렵습니다. 함수가 True를 반환하면 데이터가 유지되고, False면 제거됩니다.
그 다음으로, dataset.filter 메서드가 전체 데이터셋에 이 필터를 적용합니다. num_proc=4로 4개의 CPU 코어를 사용하여 병렬로 처리하면 속도가 크게 향상됩니다.
각 프로세스가 데이터의 일부를 담당하여 동시에 필터링을 수행합니다. 마지막으로, 필터링 전후의 샘플을 비교하여 얼마나 많은 데이터가 제거되었는지 확인합니다.
일반적으로 웹 데이터의 10-20%가 짧은 텍스트로 필터링됩니다. 스트리밍 모드에서는 전체 개수를 알 수 없지만, 샘플링을 통해 필터링 비율을 추정할 수 있습니다.
여러분이 이 코드를 사용하면 학습 데이터의 품질을 보장할 수 있습니다. 짧은 문서 제거로 배치 처리 시 패딩이 줄어들어 GPU 메모리 사용량이 감소하고, 학습 속도가 20-30% 향상되는 효과를 볼 수 있습니다.
또한 모델이 더 풍부한 문맥에서 학습하여 생성 품질도 개선됩니다.
실전 팁
💡 최소 길이를 너무 높게 설정하면 데이터가 과도하게 줄어듭니다. 50-100 토큰이 적당하며, 작업에 따라 조정하세요.
💡 최대 길이 필터도 함께 적용하면 좋습니다. len(example['input_ids']) <= 1024처럼 상한선을 설정하여 극단적으로 긴 문서도 제외하세요.
💡 필터링된 데이터의 통계를 확인하세요. lengths = [len(ex['input_ids']) for ex in dataset.take(1000)]로 길이 분포를 분석하면 적절한 기준을 찾을 수 있습니다.
💡 도메인에 따라 기준이 달라집니다. 대화 데이터는 20-30 토큰도 충분하지만, 기술 문서는 100 토큰 이상이 필요할 수 있습니다.
5. 데이터 청킹 - 고정 길이 시퀀스 생성
시작하며
여러분이 가변 길이의 문서들을 배치로 묶을 때 어떤 문제를 겪나요? 100 토큰짜리와 900 토큰짜리를 같은 배치에 넣으면 800 토큰만큼 패딩이 필요합니다.
이는 메모리와 계산량을 800% 낭비하는 것과 같습니다. 이런 비효율은 언어 모델 학습에서 가장 큰 병목 중 하나입니다.
GPU는 고정된 크기의 텐서를 처리할 때 최고 성능을 발휘하는데, 가변 길이 데이터는 이를 방해합니다. 특히 대규모 학습에서는 이 비효율이 수천 달러의 추가 비용으로 이어집니다.
바로 이럴 때 필요한 것이 데이터 청킹(chunking)입니다. 모든 문서를 동일한 고정 길이로 잘라서 패딩 없이 효율적으로 배치를 구성하는 기법입니다.
개요
간단히 말해서, 청킹은 여러 문서의 토큰들을 이어붙인 후 고정 길이로 자르는 전처리 기법입니다. 예를 들어, 1024 토큰 단위로 자르면 모든 샘플이 정확히 1024 토큰이 되어 패딩이 완전히 불필요해집니다.
왜 이 방식이 효과적일까요? 언어 모델은 문서 경계를 명시적으로 학습하지 않습니다.
따라서 여러 문서를 연결해도 학습에 문제가 없으며, 오히려 다양한 주제 전환을 경험하여 더 강건해질 수 있습니다. GPT-3 같은 대형 모델들도 이 방식을 사용합니다.
기존에는 각 문서를 개별적으로 패딩하며 계산량을 낭비했다면, 이제는 청킹으로 GPU 활용률을 95% 이상으로 높일 수 있습니다. 이는 학습 시간을 절반 이하로 줄이는 효과가 있습니다.
핵심 특징은 다음과 같습니다: 1) 패딩 제로로 메모리 효율 극대화, 2) GPU throughput 향상, 3) 배치 크기 증가 가능. 이러한 특징들이 대규모 학습을 경제적으로 만듭니다.
코드 예제
# 청킹 함수 정의
def chunk_examples(examples, chunk_size=1024):
# 모든 input_ids를 하나로 연결
concatenated_ids = []
for ids in examples['input_ids']:
concatenated_ids.extend(ids)
concatenated_ids.append(tokenizer.eos_token_id) # 문서 사이 구분
# chunk_size 단위로 자르기
total_length = len(concatenated_ids)
# 마지막 불완전한 청크는 버림
total_length = (total_length // chunk_size) * chunk_size
# 청크들로 분할
result = {
'input_ids': [
concatenated_ids[i:i+chunk_size]
for i in range(0, total_length, chunk_size)
]
}
return result
# 배치 단위로 청킹 적용
chunked_dataset = filtered_dataset.map(
lambda x: chunk_examples(x, chunk_size=1024),
batched=True,
batch_size=1000,
remove_columns=filtered_dataset.column_names
)
설명
이 코드가 하는 일은 가변 길이 문서들을 모두 동일한 길이의 청크로 재구성하여 학습 효율을 최대화하는 것입니다. 이는 프로덕션 환경에서 필수적인 최적화 기법입니다.
첫 번째로, chunk_examples 함수는 배치 내 모든 문서의 토큰들을 concatenated_ids라는 하나의 긴 리스트로 연결합니다. 각 문서 끝에 tokenizer.eos_token_id를 추가하여 문서 경계를 표시합니다.
예를 들어 100개 문서가 평균 500 토큰이면 약 50,000 토큰의 긴 시퀀스가 만들어집니다. 그 다음으로, 이 긴 시퀀스를 chunk_size=1024 단위로 정확히 자릅니다.
(total_length // chunk_size) * chunk_size로 마지막 불완전한 청크를 버리는데, 이는 모든 청크가 정확히 같은 길이를 보장하기 위함입니다. 50,000 토큰이면 48개의 완전한 1024 토큰 청크가 생성되고, 나머지 928 토큰은 버려집니다.
마지막으로, 리스트 컴프리헨션으로 청크들을 생성합니다. range(0, total_length, chunk_size)는 0, 1024, 2048, ...
식으로 인덱스를 생성하고, 각 위치에서 1024개씩 슬라이싱하여 청크를 만듭니다. 결과적으로 모든 샘플이 정확히 [1024] 길이의 input_ids를 가지게 됩니다.
여러분이 이 코드를 사용하면 배치 처리 시 패딩이 완전히 사라집니다. 예를 들어 배치 크기 32에서 각 샘플이 1024 토큰이면 (32, 1024) 크기의 텐서 하나로 처리되며, 패딩으로 인한 낭비가 0입니다.
이는 학습 속도를 2-3배 높이고, 같은 하드웨어에서 배치 크기를 2배 늘릴 수 있게 합니다.
실전 팁
💡 chunk_size는 모델의 최대 컨텍스트 길이와 같게 설정하세요. GPT-2는 1024, GPT-3는 2048이 표준입니다.
💡 버려지는 토큰이 아까우면 batch_size를 크게 하여 연결되는 토큰 수를 늘리세요. 10000개를 연결하면 버림 비율이 0.1% 미만이 됩니다.
💡 문서 경계에 EOS 토큰을 추가하면 모델이 주제 전환을 학습합니다. 이를 생략하면 서로 다른 문서가 자연스럽게 이어진 것처럼 학습됩니다.
💡 메모리가 부족하면 chunk_examples 내에서 제너레이터를 사용하여 청크를 즉시 반환하고 리스트를 비우세요.
💡 청킹 후에는 attention_mask도 재생성해야 합니다. 모든 토큰이 실제 데이터이므로 전부 1로 설정하면 됩니다.
6. 어텐션 마스크 재생성 - 청킹 후 마스크 업데이트
시작하며
여러분이 청킹 후 데이터를 확인할 때 이런 생각을 해본 적 있나요? "어텐션 마스크가 이전 문서의 것을 그대로 가지고 있는데, 이게 맞나?" 실제로 청킹은 새로운 시퀀스를 만들기 때문에 기존 마스크는 의미가 없어집니다.
이런 문제는 조용히 모델 성능을 저하시킵니다. 잘못된 어텐션 마스크는 모델이 특정 토큰을 무시하게 만들어 학습 효율을 떨어뜨립니다.
특히 패딩이 없는 청킹 데이터에서는 모든 토큰이 유효하므로 마스크를 올바르게 설정하는 것이 필수입니다. 바로 이럴 때 필요한 것이 어텐션 마스크 재생성입니다.
청킹 후 모든 토큰이 실제 데이터임을 나타내는 새로운 마스크를 만들어야 합니다.
개요
간단히 말해서, 어텐션 마스크는 모델에게 어느 토큰에 주목해야 하는지 알려주는 바이너리 배열입니다. 1은 "이 토큰을 봐라", 0은 "이 토큰은 무시해라"를 의미합니다.
왜 청킹 후 마스크를 재생성해야 할까요? 원본 문서들은 서로 다른 길이를 가졌고, 짧은 문서는 패딩이 있어서 마스크에 0이 포함되어 있었습니다.
하지만 청킹은 이 모든 것을 섞어 고정 길이로 만들었으므로, 이제 모든 토큰이 실제 데이터입니다. 따라서 마스크를 전부 1로 설정해야 합니다.
기존에는 마스크를 그대로 두어 모델이 일부 유효한 토큰을 무시했다면, 이제는 올바른 마스크로 모든 토큰을 활용할 수 있습니다. 이는 학습 데이터 활용률을 100%로 만듭니다.
핵심 특징은 다음과 같습니다: 1) 모든 토큰의 학습 참여 보장, 2) 정확한 손실 계산, 3) 어텐션 메커니즘의 올바른 작동. 이러한 특징들이 모델이 데이터를 완전히 학습하도록 합니다.
코드 예제
# 어텐션 마스크 재생성 함수
def add_attention_mask(examples):
# 청킹된 데이터는 패딩이 없으므로 모든 토큰이 유효
# input_ids와 같은 길이의 1로 채워진 마스크 생성
attention_masks = []
for input_ids in examples['input_ids']:
# 모든 위치를 1로 설정 (모든 토큰에 어텐션 적용)
attention_masks.append([1] * len(input_ids))
examples['attention_mask'] = attention_masks
return examples
# 데이터셋에 어텐션 마스크 추가
final_dataset = chunked_dataset.map(
add_attention_mask,
batched=True,
batch_size=1000
)
# 결과 확인
sample = next(iter(final_dataset.take(1)))
print(f"Input IDs length: {len(sample['input_ids'])}")
print(f"Attention mask length: {len(sample['attention_mask'])}")
print(f"All attention mask values are 1: {all(x == 1 for x in sample['attention_mask'])}")
설명
이 코드가 하는 일은 청킹으로 재구성된 데이터에 정확한 어텐션 마스크를 부여하여 모델이 모든 토큰을 올바르게 처리하도록 하는 것입니다. 이는 학습의 정확성을 보장하는 중요한 단계입니다.
첫 번째로, add_attention_mask 함수는 각 샘플의 input_ids 길이를 확인합니다. 청킹 후에는 모든 샘플이 정확히 1024 토큰(또는 설정한 chunk_size)을 가지고 있습니다.
이 길이만큼 1로 채워진 리스트를 생성하여 어텐션 마스크로 사용합니다. [1] * len(input_ids)는 Python에서 리스트를 반복하는 간결한 방법입니다.
그 다음으로, 생성된 마스크들을 examples['attention_mask']에 할당합니다. datasets 라이브러리는 이를 자동으로 새로운 컬럼으로 추가합니다.
배치 처리 모드에서는 여러 샘플의 마스크를 한 번에 생성하므로 효율적입니다. 마지막으로, map 함수가 전체 데이터셋에 이 작업을 적용합니다.
결과 확인 코드는 마스크가 올바르게 생성되었는지 검증합니다. all(x == 1 for x in sample['attention_mask'])는 모든 값이 1인지 확인하며, 청킹된 데이터에서는 항상 True여야 합니다.
여러분이 이 코드를 사용하면 모델 학습 시 정확한 어텐션 계산이 보장됩니다. 잘못된 마스크로 인한 성능 저하를 방지하고, 모든 토큰이 손실 계산에 포함되어 학습 신호가 강화됩니다.
특히 Transformer 모델에서 어텐션 마스크는 핵심 요소이므로, 이 단계를 건너뛰면 예측 불가능한 결과가 나올 수 있습니다.
실전 팁
💡 만약 특정 토큰(예: EOS)을 어텐션에서 제외하고 싶다면, 해당 위치만 0으로 설정할 수 있습니다.
💡 모델에 따라서는 token_type_ids도 필요할 수 있습니다. GPT-2는 불필요하지만, BERT 계열은 필요합니다.
💡 디버깅 시 assert len(input_ids) == len(attention_mask)로 길이 일치를 확인하세요. 불일치는 즉시 에러를 발생시킵니다.
💡 float16 학습을 사용한다면 마스크도 float16으로 변환해야 할 수 있습니다. torch.tensor(mask, dtype=torch.float16)처럼 사용하세요.
7. 데이터셋 셔플링 - 학습 안정성 향상
시작하며
여러분이 모델을 학습시킬 때 이런 현상을 본 적 있나요? 처음 몇 에폭은 손실이 잘 줄어들다가 갑자기 발산하거나, 특정 주제의 문서가 연속으로 나와 모델이 편향되는 문제.
이는 데이터 순서 때문일 가능성이 높습니다. 이런 문제는 데이터가 특정 패턴으로 정렬되어 있을 때 발생합니다.
FineWeb-Edu 같은 데이터셋은 종종 도메인별, 날짜별로 정렬되어 있어 모델이 순차적으로 학습하면 특정 패턴에 과적합될 수 있습니다. 예를 들어, 처음 1000개가 모두 수학 문서라면 모델은 수학 용어에만 최적화됩니다.
바로 이럴 때 필요한 것이 데이터셋 셔플링입니다. 무작위로 데이터 순서를 섞어 각 배치가 다양한 주제와 스타일을 포함하도록 만들어 학습을 안정화시킵니다.
개요
간단히 말해서, 셔플링은 데이터셋의 순서를 무작위로 재배치하는 과정입니다. 카드를 섞듯이 데이터를 섞어 모델이 편향되지 않은 순서로 학습하게 합니다.
왜 셔플링이 필수일까요? 딥러닝 모델은 경사 하강법으로 학습하는데, 이는 각 배치의 그래디언트를 평균 내어 파라미터를 업데이트합니다.
비슷한 데이터가 계속 나오면 그래디언트가 한 방향으로 치우쳐 학습이 불안정해집니다. 반면 다양한 데이터가 섞이면 그래디언트가 균형을 이루어 안정적으로 수렴합니다.
기존에는 데이터를 원본 순서대로 학습하며 불안정성을 겪었다면, 이제는 셔플링으로 일반화 성능과 학습 안정성을 동시에 높일 수 있습니다. 모든 최신 언어 모델 학습에서 셔플링은 표준 관행입니다.
핵심 특징은 다음과 같습니다: 1) 학습 안정성 향상, 2) 과적합 방지, 3) 더 나은 일반화 성능. 이러한 특징들이 모델 품질을 크게 개선합니다.
코드 예제
# 셔플링 적용 (스트리밍 모드에서는 버퍼 기반)
# buffer_size만큼 메모리에 로드한 후 그 범위 내에서 셔플
shuffled_dataset = final_dataset.shuffle(
seed=42, # 재현성을 위한 시드
buffer_size=10000 # 10000개 샘플을 버퍼에 두고 셔플
)
# 스트리밍이 아닌 경우, 전체 셔플링
# shuffled_dataset = final_dataset.shuffle(seed=42)
# 셔플 확인 - 처음 10개 샘플의 ID 패턴 확인
print("First 10 samples after shuffling:")
for idx, example in enumerate(shuffled_dataset.take(10)):
# 처음 20 토큰만 출력하여 다양성 확인
tokens = example['input_ids'][:20]
print(f"Sample {idx}: {tokens[:5]}... (first 5 tokens)")
설명
이 코드가 하는 일은 전처리된 데이터의 순서를 무작위화하여 모델이 편향 없이 학습할 수 있도록 준비하는 것입니다. 이는 고품질 모델 학습의 핵심 요소입니다.
첫 번째로, shuffle 메서드는 데이터셋의 순서를 재배치합니다. seed=42는 난수 생성기의 시드로, 같은 시드를 사용하면 항상 같은 셔플 결과를 얻어 실험의 재현성을 보장합니다.
이는 디버깅과 결과 비교에 필수적입니다. 그 다음으로, buffer_size=10000은 스트리밍 모드의 특수한 매개변수입니다.
전체 데이터를 메모리에 로드할 수 없으므로, 10000개 샘플을 버퍼에 두고 그 범위 내에서만 셔플합니다. 버퍼가 클수록 더 무작위적이지만 메모리를 더 사용합니다.
16GB RAM에서는 10000-50000이 적당합니다. 마지막으로, 셔플 결과를 확인하는 코드는 데이터가 실제로 섞였는지 검증합니다.
처음 몇 개 샘플의 토큰을 출력하면 서로 다른 주제와 스타일의 문서들이 섞여 있는 것을 볼 수 있습니다. 만약 비슷한 패턴이 반복된다면 buffer_size를 늘려야 합니다.
여러분이 이 코드를 사용하면 학습 곡선이 훨씬 부드러워집니다. 손실이 지그재그로 튀는 현상이 줄어들고, 검증 세트 성능이 향상됩니다.
실제로 셔플링은 모델 성능을 5-10% 향상시킬 수 있으며, 학습 시간도 20-30% 단축되는 경우가 많습니다.
실전 팁
💡 스트리밍 모드가 아니라면 buffer_size를 생략하여 전체 데이터를 완전히 셔플하세요. 이것이 가장 이상적입니다.
💡 에폭마다 다른 셔플 순서를 원한다면 각 에폭마다 다른 시드를 사용하세요. shuffle(seed=epoch_num)처럼 설정할 수 있습니다.
💡 매우 큰 데이터셋에서는 셔플링 자체가 시간이 걸립니다. 사전에 셔플된 데이터를 저장해두면 재사용 시 빠릅니다.
💡 분산 학습 시에는 각 워커가 다른 데이터를 보도록 shard 후 shuffle해야 합니다. 순서를 바꾸면 중복 데이터가 발생할 수 있습니다.
8. 데이터로더 생성 - PyTorch 학습 준비
시작하며
여러분이 전처리된 데이터를 실제 모델 학습에 사용하려고 할 때 어떤 도구가 필요할까요? datasets 라이브러리의 데이터셋은 훌륭하지만, PyTorch나 TensorFlow 같은 학습 프레임워크에 직접 연결하려면 추가 단계가 필요합니다.
이런 문제는 데이터 형식과 학습 프레임워크 간의 인터페이스 차이에서 발생합니다. datasets는 딕셔너리 형태로 데이터를 제공하지만, PyTorch는 텐서와 배치 처리를 기대합니다.
이 간극을 메우지 않으면 학습 루프를 직접 구현해야 하는 번거로움이 있습니다. 바로 이럴 때 필요한 것이 PyTorch DataLoader입니다.
datasets 라이브러리는 PyTorch와 완벽히 통합되어, 단 몇 줄로 효율적인 데이터로더를 생성할 수 있습니다.
개요
간단히 말해서, DataLoader는 데이터셋을 배치 단위로 모델에 공급하는 반복자(iterator)입니다. 자동으로 배치를 구성하고, 멀티프로세싱으로 데이터 로딩을 병렬화하며, 학습 중에도 백그라운드에서 다음 배치를 준비합니다.
왜 DataLoader가 중요할까요? GPU는 빠르게 계산하지만, 데이터 로딩이 느리면 GPU가 대기 상태가 됩니다.
DataLoader는 여러 워커 프로세스로 데이터를 미리 준비하여 GPU가 쉬지 않고 작동하도록 합니다. 예를 들어, GPU가 현재 배치를 처리하는 동안 다음 4개 배치를 백그라운드에서 준비합니다.
기존에는 데이터 로딩과 배치 구성을 수동으로 구현했다면, 이제는 DataLoader가 모든 것을 자동화합니다. 배치 크기 조정, 셔플링, 멀티프로세싱을 간단한 파라미터로 제어할 수 있습니다.
핵심 특징은 다음과 같습니다: 1) 자동 배치 처리, 2) 비동기 데이터 로딩, 3) GPU 대기 시간 최소화. 이러한 특징들이 학습 처리량을 극대화합니다.
코드 예제
# PyTorch 데이터셋 형식으로 변환
# 'torch' 포맷으로 변환하면 자동으로 텐서가 됨
final_dataset.set_format(
type='torch',
columns=['input_ids', 'attention_mask']
)
# PyTorch DataLoader 생성
from torch.utils.data import DataLoader
dataloader = DataLoader(
final_dataset,
batch_size=32, # 한 번에 32개 샘플
num_workers=4, # 4개 프로세스로 병렬 로딩
pin_memory=True, # GPU 전송 속도 향상
prefetch_factor=2 # 프로세스당 2배치 미리 준비
)
# 데이터로더 테스트
for batch in dataloader:
print(f"Batch input_ids shape: {batch['input_ids'].shape}")
print(f"Batch attention_mask shape: {batch['attention_mask'].shape}")
break # 첫 배치만 확인
설명
이 코드가 하는 일은 전처리된 데이터를 PyTorch 모델이 직접 사용할 수 있는 형태로 변환하고, 효율적인 학습 파이프라인을 구축하는 것입니다. 이는 실제 학습으로 가는 마지막 단계입니다.
첫 번째로, set_format(type='torch')은 datasets의 데이터를 PyTorch 텐서로 자동 변환합니다. columns 파라미터로 필요한 컬럼만 선택하면 메모리를 절약할 수 있습니다.
이 설정 후에는 데이터를 가져올 때마다 자동으로 torch.Tensor 객체가 반환됩니다. 그 다음으로, DataLoader 객체를 생성합니다.
batch_size=32는 한 번에 32개 샘플을 묶어 (32, 1024) 크기의 텐서를 만듭니다. num_workers=4는 4개의 별도 프로세스가 동시에 데이터를 준비하게 하여, 학습 중에도 다음 배치들이 백그라운드에서 계속 로딩됩니다.
CPU 코어 수의 절반 정도가 적당합니다. 마지막으로, pin_memory=True는 CPU 메모리를 고정하여 GPU로의 전송 속도를 높입니다.
prefetch_factor=2는 각 워커가 2개의 배치를 미리 준비하도록 하여 데이터 로딩 대기 시간을 최소화합니다. 테스트 코드는 실제로 배치가 올바른 형태로 생성되는지 확인합니다.
여러분이 이 코드를 사용하면 학습 루프에서 간단히 for batch in dataloader로 데이터를 받을 수 있습니다. 모든 배치 처리, 텐서 변환, 병렬 로딩이 자동으로 처리되어 여러분은 모델 학습 로직에만 집중할 수 있습니다.
GPU 활용률도 90% 이상으로 유지되어 학습 시간이 크게 단축됩니다.
실전 팁
💡 num_workers를 너무 크게 설정하면 메모리 부족이 발생합니다. 4-8이 대부분의 시스템에 적합합니다.
💡 Windows에서는 멀티프로세싱 이슈가 있을 수 있습니다. num_workers=0으로 시작해서 점진적으로 늘려보세요.
💡 디버깅 시에는 num_workers=0으로 설정하면 에러 메시지가 명확해집니다. 멀티프로세싱은 디버깅을 어렵게 만듭니다.
💡 배치 크기는 GPU 메모리에 맞춰 조정하세요. OOM(Out of Memory) 에러가 나면 절반으로 줄이고, 여유가 있으면 늘리세요.
💡 collate_fn을 커스터마이즈하면 배치 구성 방식을 자유롭게 변경할 수 있습니다. 예를 들어, 동적 패딩이 필요한 경우 유용합니다.
9. 데이터 저장 - 전처리 결과 캐싱
시작하며
여러분이 몇 시간에 걸쳐 데이터 전처리를 완료했는데, 다음 날 다시 실행해야 한다면 어떻게 하시겠어요? 처음부터 다시 전처리하는 것은 시간과 컴퓨팅 자원의 낭비입니다.
이런 문제는 실험을 반복할 때마다 발생합니다. 하이퍼파라미터를 바꾸거나, 모델 아키텍처를 수정할 때마다 데이터 전처리를 다시 한다면 개발 속도가 크게 느려집니다.
특히 팀 환경에서는 동료들도 같은 전처리를 각자 수행해야 하는 비효율이 생깁니다. 바로 이럴 때 필요한 것이 전처리된 데이터를 디스크에 저장하는 것입니다.
한 번 전처리한 데이터를 재사용하면 다음 실행부터는 몇 초 만에 준비가 완료됩니다.
개요
간단히 말해서, 데이터 저장은 전처리된 데이터셋을 디스크에 영구적으로 보관하는 과정입니다. datasets 라이브러리는 Apache Arrow 포맷으로 저장하여 매우 빠른 로딩 속도를 제공합니다.
왜 전처리 결과를 저장해야 할까요? 토큰화와 청킹은 CPU 집약적인 작업으로, 대용량 데이터에서는 수 시간이 걸립니다.
하지만 한 번 저장하면 다음 로딩은 몇 초 만에 완료되며, 디스크에서 직접 읽어 메모리 사용량도 줄일 수 있습니다. 또한 팀원들과 공유하여 중복 작업을 방지할 수 있습니다.
기존에는 pickle이나 JSON으로 저장하며 로딩 속도가 느렸다면, 이제는 Arrow 포맷으로 압축되고 최적화된 형태로 저장할 수 있습니다. Arrow는 컬럼 기반 포맷으로 부분 로딩도 가능합니다.
핵심 특징은 다음과 같습니다: 1) 초고속 로딩 (원본보다 100배 빠름), 2) 압축으로 디스크 공간 절약, 3) 메모리맵 지원으로 RAM 절약. 이러한 특징들이 효율적인 데이터 관리를 가능하게 합니다.
코드 예제
# 스트리밍 모드를 일반 데이터셋으로 변환 (저장을 위해)
# take를 사용하여 원하는 만큼만 저장 (또는 전체)
import os
# 저장 경로 설정
save_path = "./preprocessed_fineweb"
os.makedirs(save_path, exist_ok=True)
# 데이터셋 저장 (Arrow 포맷)
# 스트리밍 모드라면 먼저 일부를 로컬로 변환
if hasattr(final_dataset, 'save_to_disk'):
final_dataset.save_to_disk(save_path)
print(f"Dataset saved to {save_path}")
else:
# 스트리밍 데이터셋은 먼저 리스트로 변환
print("Converting streaming dataset to disk format...")
# 필요한 만큼만 저장 (예: 100만 샘플)
# 전체를 저장하려면 take 제거
# 저장된 데이터셋 로딩 테스트
from datasets import load_from_disk
loaded_dataset = load_from_disk(save_path)
print(f"Loaded dataset size: {len(loaded_dataset)}")
print(f"First sample: {loaded_dataset[0]['input_ids'][:10]}")
설명
이 코드가 하는 일은 시간이 많이 걸린 전처리 작업의 결과를 영구적으로 저장하여 재사용 가능하게 만드는 것입니다. 이는 효율적인 개발 워크플로우의 핵심입니다.
첫 번째로, os.makedirs로 저장 디렉토리를 생성합니다. exist_ok=True는 디렉토리가 이미 있어도 에러를 발생시키지 않습니다.
경로는 프로젝트 루트나 별도의 데이터 저장소로 설정할 수 있습니다. 그 다음으로, save_to_disk 메서드가 데이터셋을 Arrow 포맷으로 저장합니다.
이 포맷은 컬럼 지향적이어서 특정 컬럼만 로딩할 수 있고, 압축도 적용되어 공간을 절약합니다. 예를 들어, 100GB의 원본 텍스트가 토큰화 후 저장하면 30GB 정도로 줄어듭니다.
스트리밍 데이터셋의 경우 먼저 필요한 만큼을 메모리에 로드한 후 저장해야 합니다. 마지막으로, load_from_disk로 저장된 데이터를 빠르게 불러옵니다.
이 과정은 원본 로딩과 전처리를 합친 것보다 100배 이상 빠릅니다. 10GB 데이터셋도 몇 초 내에 로딩되며, 메모리맵을 사용하면 전체를 RAM에 로드하지 않고도 사용할 수 있습니다.
여러분이 이 코드를 사용하면 반복 실험이 매우 빨라집니다. 첫 실행에서는 전처리에 3시간이 걸려도, 두 번째부터는 10초 만에 데이터가 준비됩니다.
팀원들과 전처리된 데이터를 공유하면 모두가 시간을 절약할 수 있으며, 버전 관리도 용이합니다.
실전 팁
💡 저장 경로에는 날짜나 버전을 포함하세요. ./preprocessed_fineweb_2025_01_15처럼 하면 여러 버전을 관리할 수 있습니다.
💡 용량이 크다면 save_to_disk(..., num_shards=10)으로 여러 파일로 분할 저장하면 병렬 로딩이 가능합니다.
💡 클라우드 스토리지(S3, GCS)에 저장하려면 s3://bucket/path 같은 URI를 사용할 수 있습니다.
💡 저장 전에 final_dataset.cleanup_cache_files()로 불필요한 캐시를 정리하면 디스크 공간을 절약할 수 있습니다.
💡 load_from_disk에 keep_in_memory=False를 설정하면 메모리맵 모드로 작동하여 RAM 사용량을 최소화합니다.
10. 학습 루프 통합 - 실제 모델 학습 예제
시작하며
여러분이 모든 전처리를 완료했는데, 실제로 어떻게 모델과 연결해서 학습을 시작하는지 막막하신가요? 전처리 데이터와 모델 학습 사이에는 여러 추가 설정이 필요합니다.
이런 간극은 초보자들이 가장 자주 마주치는 장벽입니다. 데이터는 준비되었지만 손실 함수, 옵티마이저, 학습 루프를 어떻게 구성할지, GPU를 어떻게 활용할지 등 실전 지식이 부족하면 막히게 됩니다.
바로 이럴 때 필요한 것이 완전한 학습 루프 예제입니다. 전처리된 데이터부터 실제 모델 학습까지 이어지는 전체 파이프라인을 보여드리겠습니다.
개요
간단히 말해서, 학습 루프는 배치 데이터를 모델에 입력하고, 손실을 계산하고, 역전파로 파라미터를 업데이트하는 반복 과정입니다. PyTorch에서는 이를 명시적으로 구현해야 합니다.
왜 완전한 예제가 중요할까요? 데이터 전처리만 알아서는 실제 모델을 학습시킬 수 없습니다.
옵티마이저 설정, 학습률 스케줄링, 그래디언트 클리핑, 체크포인트 저장 등 실전에서 필요한 모든 요소를 통합해야 합니다. 예를 들어, GPT-2 학습에는 AdamW 옵티마이저와 코사인 학습률 감쇠가 표준입니다.
기존에는 각 부분을 따로 배워 통합하기 어려웠다면, 이제는 전체 흐름을 한눈에 볼 수 있습니다. 이 예제를 템플릿 삼아 자신의 프로젝트에 맞게 수정할 수 있습니다.
핵심 특징은 다음과 같습니다: 1) GPU 자동 감지 및 활용, 2) 안정적인 학습을 위한 best practice 적용, 3) 모니터링과 체크포인트 저장. 이러한 요소들이 프로덕션급 학습을 가능하게 합니다.
코드 예제
# 필요한 라이브러리 임포트
import torch
from transformers import GPT2LMHeadModel, AdamW, get_cosine_schedule_with_warmup
# 모델 초기화 (GPT-2 Small)
model = GPT2LMHeadModel.from_pretrained("gpt2")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
# 옵티마이저 설정
optimizer = AdamW(model.parameters(), lr=5e-5, weight_decay=0.01)
# 학습률 스케줄러 (코사인 감쇠)
num_epochs = 3
num_training_steps = num_epochs * len(dataloader)
scheduler = get_cosine_schedule_with_warmup(
optimizer, num_warmup_steps=500, num_training_steps=num_training_steps
)
# 학습 루프
model.train()
for epoch in range(num_epochs):
total_loss = 0
for step, batch in enumerate(dataloader):
# 데이터를 GPU로 이동
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
# 순전파 (labels=input_ids는 언어 모델링의 표준)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids)
loss = outputs.loss
# 역전파 및 업데이트
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 그래디언트 클리핑
optimizer.step()
scheduler.step()
optimizer.zero_grad()
total_loss += loss.item()
# 진행 상황 출력
if step % 100 == 0:
print(f"Epoch {epoch}, Step {step}, Loss: {loss.item():.4f}")
avg_loss = total_loss / len(dataloader)
print(f"Epoch {epoch} completed. Average Loss: {avg_loss:.4f}")
# 체크포인트 저장
model.save_pretrained(f"./checkpoints/epoch_{epoch}")
설명
이 코드가 하는 일은 전처리된 FineWeb-Edu 데이터로 GPT-2 언어 모델을 실제로 학습시키는 전체 프로세스를 구현하는 것입니다. 이는 지금까지의 모든 전처리 작업이 결실을 맺는 단계입니다.
첫 번째로, GPT2LMHeadModel.from_pretrained("gpt2")로 사전학습된 GPT-2 모델을 불러옵니다. 이 모델은 이미 영어로 학습되어 있으므로, 우리의 데이터로 fine-tuning하거나 continued pretraining을 수행합니다.
model.to(device)는 GPU가 있으면 자동으로 GPU로 이동시켜 학습 속도를 100배 이상 높입니다. 그 다음으로, AdamW 옵티마이저를 설정합니다.
lr=5e-5는 언어 모델에 적합한 학습률이며, weight_decay=0.01은 과적합을 방지합니다. get_cosine_schedule_with_warmup은 학습률을 처음에 천천히 높였다가 코사인 함수를 따라 감소시켜 안정적인 수렴을 도와줍니다.
마지막으로, 학습 루프가 실행됩니다. 각 배치에 대해 1) 데이터를 GPU로 이동, 2) 순전파로 손실 계산(labels=input_ids는 다음 토큰 예측을 의미), 3) 역전파로 그래디언트 계산, 4) 그래디언트 클리핑으로 폭발 방지, 5) 파라미터 업데이트 순으로 진행됩니다.
100 스텝마다 손실을 출력하고, 각 에폭 후 체크포인트를 저장합니다. 여러분이 이 코드를 사용하면 자신만의 GPT 모델을 학습시킬 수 있습니다.
FineWeb-Edu의 고품질 데이터와 올바른 전처리 덕분에 몇 시간의 학습으로도 의미 있는 텍스트 생성 능력을 갖춘 모델을 만들 수 있습니다. GPU가 없어도 CPU로 실행 가능하며(느리지만), Colab의 무료 GPU로도 실험할 수 있습니다.
실전 팁
💡 GPU 메모리가 부족하면 batch_size를 줄이고 gradient_accumulation_steps를 사용하여 효과적인 배치 크기를 유지하세요.
💡 torch.cuda.empty_cache()를 에폭 종료 시 호출하면 GPU 메모리 누수를 방지할 수 있습니다.
💡 Mixed precision training(torch.cuda.amp)을 사용하면 메모리를 절반으로 줄이고 속도를 2배 높일 수 있습니다.
💡 Weights & Biases나 TensorBoard로 손실 곡선을 시각화하면 학습 상태를 모니터링하기 쉽습니다.
💡 검증 세트를 따로 준비하여 에폭마다 perplexity를 측정하면 과적합을 조기에 발견할 수 있습니다.