이미지 로딩 중...
AI Generated
2025. 11. 20. · 2 Views
파인튜닝을 위한 데이터셋 준비 완벽 가이드
AI 모델을 내 데이터로 학습시키기 위한 데이터셋 준비 과정을 단계별로 알아봅니다. HuggingFace Dataset 변환부터 메모리 효율적인 데이터 로딩까지, 실무에서 바로 활용할 수 있는 방법을 친절하게 설명합니다.
목차
- HuggingFace Dataset으로 변환
- Tokenization 및 최대 길이 설정
- Padding과 Truncation 전략
- Batch 크기 결정 기준
- Data Collator 설정
- 메모리 효율적인 데이터 로딩
1. HuggingFace Dataset으로 변환
시작하며
여러분이 CSV 파일이나 JSON 파일에 담긴 소중한 데이터로 AI 모델을 학습시키고 싶을 때, "어떻게 시작해야 하지?"라는 막막함을 느낀 적 있나요? 엑셀 파일에 정리된 고객 문의 데이터나, JSON으로 저장된 채팅 로그를 AI에게 학습시키고 싶은데 어디서부터 손을 대야 할지 모르는 상황 말이죠.
이런 문제는 실제 개발 현장에서 정말 자주 발생합니다. 데이터는 있는데 AI 모델이 이해할 수 있는 형태가 아니어서 답답한 경우가 많거든요.
마치 외국인 친구에게 한국어로 된 편지를 주는 것과 비슷합니다. 바로 이럴 때 필요한 것이 HuggingFace Dataset입니다.
여러분의 평범한 데이터 파일을 AI가 학습할 수 있는 특별한 형태로 변신시켜주는 마법 같은 도구예요. 마치 번역기처럼 데이터를 AI의 언어로 바꿔주는 거죠.
개요
간단히 말해서, HuggingFace Dataset은 여러분의 데이터를 AI 모델이 먹기 좋은 형태로 바꿔주는 변환 도구입니다. 왜 이 변환이 필요할까요?
AI 모델들은 특별한 형식의 데이터만 이해할 수 있기 때문입니다. 예를 들어, 여러분이 1000개의 고객 리뷰 데이터로 감성 분석 모델을 만들고 싶다면, 그냥 엑셀 파일을 던져주는 게 아니라 AI가 이해할 수 있는 Dataset 형태로 변환해야 합니다.
전통적인 방법에서는 pandas DataFrame으로 데이터를 읽고 복잡한 전처리를 직접 해야 했습니다. 하지만 이제는 HuggingFace Dataset을 사용하면 단 몇 줄의 코드로 자동으로 변환할 수 있습니다.
이 도구의 핵심 특징은 세 가지입니다. 첫째, 다양한 형식(CSV, JSON, 텍스트 등)을 모두 지원합니다.
둘째, 메모리를 효율적으로 사용해서 큰 데이터도 문제없습니다. 셋째, AI 학습에 필요한 기능들이 이미 내장되어 있습니다.
이러한 특징들이 데이터 준비 시간을 10배 이상 단축시켜주기 때문에 정말 중요합니다.
코드 예제
from datasets import Dataset, load_dataset
import pandas as pd
# CSV 파일을 HuggingFace Dataset으로 변환
df = pd.read_csv('customer_reviews.csv')
# DataFrame의 각 행이 하나의 학습 샘플이 됩니다
dataset = Dataset.from_pandas(df)
# 또는 CSV 파일을 직접 로드
dataset = load_dataset('csv', data_files='customer_reviews.csv')
# JSON 파일도 동일하게 변환 가능
dataset = load_dataset('json', data_files='chat_logs.json')
# 데이터셋 구조 확인
print(dataset)
print(dataset[0]) # 첫 번째 샘플 출력
설명
이것이 하는 일: 여러분의 평범한 데이터 파일을 AI 모델이 학습할 수 있는 특별한 Dataset 객체로 변환합니다. 마치 음식 재료를 요리하기 좋게 손질하는 것처럼, 데이터를 AI가 소화하기 좋은 형태로 만들어줍니다.
첫 번째로, pandas로 CSV 파일을 읽어온 후 Dataset.from_pandas()를 사용하면 DataFrame이 Dataset으로 변환됩니다. 이렇게 하는 이유는 Dataset이 메모리를 훨씬 효율적으로 사용하고, AI 학습에 필요한 기능들(셔플링, 배치 처리 등)을 자동으로 제공하기 때문입니다.
두 번째로, load_dataset() 함수를 사용하면 파일을 직접 Dataset으로 읽어올 수 있습니다. 내부적으로 파일의 각 행을 읽으면서 자동으로 데이터 타입을 분석하고, AI 학습에 최적화된 구조로 저장합니다.
CSV든 JSON이든 같은 방식으로 처리할 수 있어서 정말 편리합니다. 세 번째로, 변환된 Dataset은 일반 리스트처럼 인덱싱이 가능합니다.
dataset[0]을 출력하면 첫 번째 데이터 샘플을 볼 수 있고, 각 필드(컬럼)가 제대로 변환되었는지 확인할 수 있습니다. 최종적으로 이 Dataset 객체는 학습 루프에서 바로 사용할 수 있는 상태가 됩니다.
여러분이 이 코드를 사용하면 데이터 준비 시간을 대폭 줄일 수 있습니다. 예를 들어 10만 개의 리뷰 데이터를 변환하는데 기존 방식으로는 10분이 걸렸다면, 이 방법으로는 1분도 안 걸립니다.
또한 메모리 걱정 없이 대용량 데이터도 처리할 수 있고, 데이터 로딩 속도도 5배 이상 빨라집니다.
실전 팁
💡 여러 파일을 한 번에 로드하려면 data_files=['file1.csv', 'file2.csv'] 형태로 리스트를 전달하세요. 자동으로 하나의 Dataset으로 합쳐집니다.
💡 대용량 파일은 streaming=True 옵션을 추가하세요. 전체 파일을 메모리에 올리지 않고 필요한 만큼만 읽어오기 때문에 100GB 파일도 처리 가능합니다.
💡 DataFrame에 결측치(NaN)가 있으면 에러가 발생할 수 있으니, df.fillna('') 또는 df.dropna()로 먼저 처리하세요.
💡 데이터셋을 디스크에 저장하려면 dataset.save_to_disk('my_dataset')을 사용하세요. 다음에 load_from_disk('my_dataset')로 빠르게 불러올 수 있습니다.
💡 변환 후 반드시 print(dataset.features)로 각 필드의 데이터 타입을 확인하세요. 숫자가 문자열로 인식되는 등의 실수를 미리 잡을 수 있습니다.
2. Tokenization 및 최대 길이 설정
시작하며
여러분이 "안녕하세요. 반갑습니다."라는 문장을 AI에게 학습시키고 싶을 때, AI는 이 문장을 어떻게 이해할까요?
사람은 글자를 보면 바로 의미를 알지만, AI는 숫자만 이해할 수 있습니다. 마치 외국인이 한글을 읽을 수 없는 것처럼요.
이런 문제는 모든 AI 모델 학습에서 반드시 해결해야 하는 핵심 과제입니다. 문장을 숫자로 변환하지 않으면 AI는 아무것도 배울 수 없거든요.
게다가 문장마다 길이가 다른데, 어떤 건 10글자고 어떤 건 1000글자라면 AI가 혼란스러워합니다. 바로 이럴 때 필요한 것이 Tokenization과 최대 길이 설정입니다.
문장을 AI가 이해하는 숫자 코드로 바꾸고, 모든 문장을 같은 길이로 맞춰주는 과정입니다. 마치 책을 복사할 때 모든 페이지를 같은 크기로 복사하는 것과 비슷합니다.
개요
간단히 말해서, Tokenization은 문장을 AI가 이해하는 숫자 코드로 변환하는 과정이고, 최대 길이 설정은 모든 문장을 같은 길이로 맞추는 작업입니다. 왜 이 과정이 필요할까요?
AI 모델은 문자를 직접 처리할 수 없고 오직 숫자만 이해하기 때문입니다. 예를 들어, GPT 모델로 챗봇을 만들 때 "오늘 날씨 어때?"라는 질문을 [1234, 5678, 9012] 같은 숫자 배열로 변환해야 합니다.
또한 문장 길이가 제각각이면 배치 처리를 할 수 없어서 학습 속도가 엄청나게 느려집니다. 전통적인 방법에서는 직접 단어 사전을 만들고 일일이 인덱스를 매핑해야 했습니다.
하지만 이제는 HuggingFace의 Tokenizer를 사용하면 자동으로 변환되고, max_length 파라미터 하나로 길이 조절까지 완료됩니다. 이 과정의 핵심 특징은 세 가지입니다.
첫째, 서브워드 토크나이제이션으로 모르는 단어도 처리할 수 있습니다. 둘째, 최대 길이를 설정하면 긴 문장은 자르고 짧은 문장은 나중에 패딩으로 채웁니다.
셋째, 특수 토큰([CLS], [SEP] 등)이 자동으로 추가되어 모델이 문장의 시작과 끝을 인식합니다. 이러한 특징들이 학습의 정확도와 속도를 동시에 높여주기 때문에 필수적입니다.
코드 예제
from transformers import AutoTokenizer
# 사용할 모델의 토크나이저 로드 (예: BERT)
tokenizer = AutoTokenizer.from_pretrained('bert-base-multilingual-cased')
# 단일 문장 토크나이제이션
text = "안녕하세요. 파인튜닝을 배우고 있습니다."
# max_length: 최대 토큰 수, truncation: 길면 자르기, padding: 짧으면 채우기
tokens = tokenizer(text, max_length=128, truncation=True, padding='max_length')
print(f"입력 문장: {text}")
print(f"토큰 IDs: {tokens['input_ids'][:10]}...") # 처음 10개만 출력
print(f"길이: {len(tokens['input_ids'])}") # 128이 출력됨
# Dataset 전체에 적용
def tokenize_function(examples):
return tokenizer(examples['text'], max_length=128, truncation=True, padding='max_length')
tokenized_dataset = dataset.map(tokenize_function, batched=True)
설명
이것이 하는 일: 사람이 읽는 문장을 AI가 처리할 수 있는 숫자 배열로 변환하고, 모든 문장의 길이를 통일합니다. 마치 서로 다른 크기의 상자들을 같은 규격으로 포장하는 것처럼, 데이터를 AI가 한꺼번에 처리하기 좋은 형태로 만들어줍니다.
첫 번째로, AutoTokenizer.from_pretrained()로 모델에 맞는 토크나이저를 불러옵니다. 각 AI 모델마다 고유한 단어 사전이 있기 때문에, BERT를 학습시킬 거라면 BERT의 토크나이저를 사용해야 합니다.
이렇게 하는 이유는 모델이 사전 학습할 때 사용한 것과 같은 방식으로 토크나이징해야 학습이 제대로 되기 때문입니다. 두 번째로, tokenizer() 함수를 호출하면 내부적으로 여러 단계가 실행됩니다.
문장을 서브워드 단위로 쪼개고(예: "배우고" → "배우", "##고"), 각 서브워드를 숫자 ID로 변환하며(예: "배우" → 1234), 특수 토큰을 앞뒤에 추가합니다([CLS] 문장 [SEP]). max_length=128이면 결과는 항상 128개의 숫자를 가진 배열이 됩니다.
세 번째로, dataset.map()을 사용하면 전체 데이터셋의 모든 문장에 토크나이제이션을 한 번에 적용할 수 있습니다. batched=True 옵션을 주면 한 문장씩이 아니라 여러 문장을 묶어서 처리하기 때문에 속도가 10배 이상 빨라집니다.
최종적으로 tokenized_dataset은 원본 텍스트와 함께 input_ids, attention_mask 같은 필드가 추가된 상태가 됩니다. 여러분이 이 코드를 사용하면 1만 개의 문장을 몇 초 만에 토크나이징할 수 있습니다.
수동으로 처리하면 몇 시간 걸릴 작업이죠. 또한 max_length를 적절히 설정하면 메모리 사용량을 크게 줄일 수 있습니다.
예를 들어 대부분의 문장이 50토큰 이하라면 max_length=512 대신 128로 설정해서 메모리를 4분의 1로 절약할 수 있습니다.
실전 팁
💡 max_length는 데이터셋의 평균 길이보다 약간 크게 설정하세요. 너무 크면 메모리 낭비, 너무 작으면 중요한 정보가 잘립니다. tokenizer(texts, return_length=True)로 길이 분포를 먼저 확인하는 게 좋습니다.
💡 truncation='only_first'를 사용하면 질문-답변 쌍에서 질문은 보존하고 답변만 자를 수 있습니다. 질문이 잘리면 학습이 망가지니까요.
💡 한국어 모델을 학습한다면 'bert-base-multilingual-cased' 대신 'klue/bert-base' 같은 한국어 특화 모델의 토크나이저를 사용하세요. 토크나이징 품질이 훨씬 좋습니다.
💡 dataset.map()에 num_proc=4를 추가하면 4개의 CPU 코어로 병렬 처리됩니다. 대용량 데이터는 이렇게 하면 속도가 4배 빨라집니다.
💡 토크나이징 결과를 꼭 확인하세요. tokenizer.decode(tokens['input_ids'])로 다시 문장으로 복원해보면 정보가 제대로 보존되었는지 알 수 있습니다.
3. Padding과 Truncation 전략
시작하며
여러분이 100개의 문장을 한꺼번에 AI에게 학습시키려고 할 때, 어떤 문장은 10개 단어, 어떤 문장은 100개 단어라면 어떻게 될까요? AI는 "한 번에 10개 처리해야 해?
아니면 100개?" 하면서 혼란스러워합니다. 마치 크기가 제각각인 책들을 책장에 깔끔하게 정리하려는 것과 비슷한 상황이죠.
이런 문제는 배치 학습을 할 때 반드시 마주치는 도전입니다. 배치 학습이란 데이터를 한 개씩이 아니라 여러 개씩 묶어서 한 번에 학습시키는 방법인데, 이렇게 해야 GPU를 효율적으로 사용해서 학습 속도가 10배 이상 빨라지거든요.
하지만 길이가 다르면 배치를 만들 수가 없어요. 바로 이럴 때 필요한 것이 Padding과 Truncation 전략입니다.
짧은 문장은 뒤를 빈 공간으로 채우고(Padding), 긴 문장은 적당히 잘라내서(Truncation) 모두 같은 길이로 만드는 똑똑한 방법입니다. 마치 다양한 크기의 사진들을 같은 크기의 액자에 맞추는 것처럼요.
개요
간단히 말해서, Padding은 짧은 문장 뒤에 0을 채워서 길이를 늘리는 것이고, Truncation은 긴 문장을 잘라서 길이를 줄이는 작업입니다. 왜 이 전략이 필요할까요?
AI 모델은 행렬 연산을 하기 때문에 입력 데이터가 모두 같은 크기여야 하기 때문입니다. 예를 들어, 챗봇을 학습시킬 때 32개의 대화를 한 번에 처리하려면(batch_size=32) 모든 대화가 정확히 같은 길이여야 합니다.
하나라도 길이가 다르면 GPU가 에러를 내며 멈춰버립니다. 전통적인 방법에서는 가장 긴 문장의 길이를 찾아서 나머지를 수동으로 0으로 채워야 했습니다.
하지만 이제는 padding='max_length' 옵션 하나면 자동으로 처리되고, truncation=True 옵션으로 긴 문장도 알아서 잘립니다. 이 전략의 핵심 특징은 세 가지입니다.
첫째, 다양한 padding 옵션으로 상황에 맞게 최적화할 수 있습니다(배치별 최대 길이, 고정 길이 등). 둘째, attention_mask가 자동 생성되어 AI가 실제 단어와 패딩을 구별할 수 있습니다.
셋째, truncation='only_second' 같은 옵션으로 중요한 부분을 보존할 수 있습니다. 이러한 특징들이 메모리 효율과 학습 품질을 동시에 높여주기 때문에 전략적 선택이 중요합니다.
코드 예제
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('bert-base-multilingual-cased')
texts = [
"짧은 문장",
"이것은 중간 길이의 문장입니다.",
"이것은 아주 아주 아주 긴 문장으로 최대 길이를 초과할 수도 있습니다."
]
# 전략 1: 고정 길이로 패딩 (가장 간단)
tokens = tokenizer(texts, max_length=20, truncation=True, padding='max_length')
# 전략 2: 배치 내 최대 길이로 패딩 (메모리 효율적)
tokens = tokenizer(texts, truncation=True, padding='longest')
# 전략 3: truncation 방향 지정 (질문-답변 쌍에서 유용)
qa_texts = ["질문: 이것은 무엇인가요? [SEP] 답변: 이것은 매우 긴 답변..."]
tokens = tokenizer(qa_texts, max_length=20, truncation='only_second', padding='max_length')
# attention_mask 확인 (1=실제 토큰, 0=패딩)
print(f"토큰: {tokens['input_ids'][0]}")
print(f"마스크: {tokens['attention_mask'][0]}")
설명
이것이 하는 일: 길이가 제각각인 문장들을 AI가 한꺼번에 처리할 수 있도록 모두 같은 길이로 통일합니다. 동시에 attention_mask를 만들어서 AI가 실제 내용과 빈 공간을 구별할 수 있게 해줍니다.
마치 높이가 다른 사람들이 단체 사진을 찍을 때 계단에 서서 높이를 맞추는 것과 비슷합니다. 첫 번째로, padding='max_length' 전략은 모든 문장을 max_length로 지정한 길이로 만듭니다.
"짧은 문장"이 5개 토큰이라면 뒤에 15개의 0(패딩 토큰)을 추가해서 20개로 만드는 거죠. 이렇게 하는 이유는 배치의 모든 샘플이 정확히 같은 크기여야 GPU에서 병렬 처리가 가능하기 때문입니다.
단점은 짧은 문장이 많으면 메모리가 낭비된다는 점입니다. 두 번째로, padding='longest' 전략은 더 똑똑합니다.
배치 안에서 가장 긴 문장을 찾아서 그 길이에 맞춰 패딩합니다. 위 예제에서 가장 긴 문장이 15개 토큰이라면 모두 15로 맞추는 거죠.
내부적으로 배치를 만들 때마다 최대 길이를 계산하고 그만큼만 패딩하기 때문에 메모리를 30~50% 절약할 수 있습니다. 세 번째로, truncation='only_second'는 질문-답변 데이터에 특히 유용합니다.
문장이 [질문] [SEP] [답변] 형태일 때, 길이가 초과되면 답변 부분만 잘립니다. 질문은 절대 잘리지 않아요.
최종적으로 tokens['attention_mask']를 보면 실제 토큰 위치는 1, 패딩 위치는 0으로 표시되어 있습니다. AI는 이 마스크를 보고 계산할 때 패딩 부분을 무시합니다.
여러분이 이 전략들을 잘 활용하면 학습 속도와 메모리 효율을 크게 개선할 수 있습니다. 예를 들어 평균 50토큰짜리 문장 1만 개를 학습할 때, max_length=512로 고정하면 10GB 메모리가 필요하지만 padding='longest'를 쓰면 1GB면 충분합니다.
또한 truncation 전략을 잘못 선택하면 중요한 정보가 잘려서 학습 정확도가 20% 이상 떨어질 수 있으니 데이터 특성에 맞게 선택하는 게 중요합니다.
실전 팁
💡 대부분의 경우 padding='longest'가 최선입니다. 메모리도 절약하고 학습 속도도 빠릅니다. padding='max_length'는 모델을 실제 서비스에 배포할 때만 사용하세요.
💡 데이터를 길이순으로 정렬한 후 배치를 만들면 패딩이 최소화됩니다. dataset.sort('length')로 정렬 후 학습하면 메모리를 추가로 20% 절약할 수 있습니다.
💡 truncation=True만 쓰지 말고 truncation='longest_first'를 사용하세요. 질문과 답변이 동시에 길 때 둘 다 조금씩 자르기 때문에 정보 손실이 적습니다.
💡 pad_to_multiple_of=8 옵션을 추가하면 길이가 8의 배수가 됩니다. GPU가 8의 배수 크기를 처리할 때 가장 빠르기 때문에 학습 속도가 10~15% 향상됩니다.
💡 학습 전에 반드시 패딩 비율을 확인하세요. 배치의 50% 이상이 패딩이라면 max_length를 줄이거나 데이터를 정렬해야 합니다. 패딩이 많으면 GPU 연산의 절반이 쓸데없는 계산에 낭비됩니다.
4. Batch 크기 결정 기준
시작하며
여러분이 AI 모델을 학습시키려고 할 때 "한 번에 몇 개씩 학습시켜야 하지?"라는 고민을 해본 적 있나요? 8개?
16개? 32개?
아니면 그냥 크게 128개? 마치 식당에서 음식을 몇 개씩 주문해야 할지 고민하는 것과 비슷한 상황입니다.
너무 적게 주문하면 여러 번 주문해야 하고, 너무 많이 주문하면 먹다 남기거든요. 이런 문제는 실제로 AI 학습 성능에 엄청난 영향을 미칩니다.
배치 크기를 잘못 선택하면 학습이 10배 느려지거나, 심하면 GPU 메모리가 부족해서 "Out of Memory" 에러가 나며 멈춰버립니다. 또는 학습이 불안정해져서 정확도가 오르락내리락하는 문제가 생기기도 합니다.
바로 이럴 때 필요한 것이 Batch 크기 결정 기준입니다. GPU 메모리, 모델 크기, 데이터 특성을 고려해서 최적의 배치 크기를 찾는 과학적인 방법입니다.
마치 차에 짐을 실을 때 트럭 크기와 짐의 무게를 고려해서 최대한 효율적으로 싣는 것처럼요.
개요
간단히 말해서, 배치 크기는 AI가 한 번에 학습하는 데이터 샘플의 개수이며, 이를 결정할 때는 GPU 메모리 용량, 모델 크기, 학습 안정성을 고려해야 합니다. 왜 이 결정이 중요할까요?
배치 크기가 학습 속도, 메모리 사용량, 최종 모델 성능에 모두 영향을 미치기 때문입니다. 예를 들어, BERT 모델을 파인튜닝할 때 배치 크기를 8에서 32로 늘리면 학습 속도가 2배 빨라지지만, 메모리는 4배 더 필요합니다.
반대로 너무 작게 하면 GPU가 놀고 있는 시간이 많아져서 비효율적입니다. 전통적인 방법에서는 배치 크기를 32로 고정하고 에러가 나면 줄이는 식으로 시행착오를 겪었습니다.
하지만 이제는 GPU 메모리를 확인하고, gradient accumulation을 활용하면 작은 GPU에서도 큰 배치 효과를 낼 수 있습니다. 배치 크기 결정의 핵심 기준은 세 가지입니다.
첫째, GPU 메모리 한계 - 사용 가능한 메모리의 80% 정도까지 사용하는 게 이상적입니다. 둘째, 학습 안정성 - 너무 작으면 학습이 불안정하고, 너무 크면 학습률 조정이 어렵습니다.
셋째, 데이터셋 크기 - 작은 데이터셋에서는 배치 크기도 작아야 과적합을 방지할 수 있습니다. 이러한 기준들을 균형있게 고려해야 최적의 성능을 얻을 수 있습니다.
코드 예제
from transformers import TrainingArguments
# GPU 메모리 확인 (PyTorch 기준)
import torch
if torch.cuda.is_available():
# 사용 가능한 총 GPU 메모리 (GB 단위)
total_memory = torch.cuda.get_device_properties(0).total_memory / 1e9
print(f"GPU 메모리: {total_memory:.1f}GB")
# 권장 배치 크기 가이드
# 12GB GPU: batch_size=8~16
# 24GB GPU: batch_size=16~32
# 40GB GPU: batch_size=32~64
training_args = TrainingArguments(
per_device_train_batch_size=8, # 실제 배치 크기
gradient_accumulation_steps=4, # 4번 누적 = 실질적으로 32 효과
# 실효 배치 크기 = 8 * 4 = 32
learning_rate=2e-5,
output_dir='./results',
)
# 작은 GPU에서 큰 배치 효과 내기
# 메모리가 부족하면: batch_size 줄이고 accumulation_steps 늘리기
설명
이것이 하는 일: 여러분의 GPU 메모리 크기를 확인하고, 그에 맞는 최적의 배치 크기를 설정합니다. 동시에 gradient accumulation이라는 기법으로 작은 GPU에서도 큰 배치 크기의 효과를 얻을 수 있게 해줍니다.
마치 작은 차로 여러 번 나눠서 짐을 실어도 큰 트럭 한 번에 실은 것과 같은 효과를 내는 것처럼요. 첫 번째로, torch.cuda.get_device_properties()로 GPU의 총 메모리를 확인합니다.
예를 들어 12GB GPU라면 약 8~10GB 정도를 학습에 사용할 수 있습니다(나머지는 시스템이 사용). 이렇게 하는 이유는 메모리를 100% 다 쓰면 시스템이 불안정해지고, 90% 이상 사용하면 속도가 급격히 느려지기 때문입니다.
두 번째로, per_device_train_batch_size는 GPU 하나당 실제로 로드되는 배치 크기입니다. BERT 모델에서 sequence_length=128일 때, batch_size=8이면 약 4GB 메모리를 사용합니다.
내부적으로 입력 데이터, 모델 파라미터, 중간 계산 결과(activations), 그래디언트가 모두 메모리에 올라가기 때문에 생각보다 많은 메모리가 필요합니다. 세 번째로, gradient_accumulation_steps=4는 정말 똑똑한 기법입니다.
배치 크기 8로 4번 학습한 후 한 번에 업데이트하는 방식이에요. 메모리는 8개분만 쓰지만 실제로는 32개 배치처럼 학습됩니다.
최종적으로 실효 배치 크기는 8 * 4 = 32가 되어 GPU 메모리가 작아도 큰 배치의 이점(학습 안정성, 속도)을 모두 얻을 수 있습니다. 여러분이 이 방법을 사용하면 제한된 하드웨어에서도 최고의 성능을 뽑아낼 수 있습니다.
예를 들어 12GB GPU로 GPT-2를 파인튜닝할 때, 단순히 batch_size=32를 쓰면 메모리 부족으로 실패하지만, batch_size=8 + accumulation=4를 쓰면 성공합니다. 또한 학습 시간도 최적화되어 배치 크기가 2배 증가하면 학습 시간은 약 40% 감소합니다(2배는 아니지만 상당한 개선).
실전 팁
💡 처음에는 작은 배치 크기(4~8)로 시작해서 점점 늘려보세요. 메모리 에러가 나면 마지막 성공한 크기의 80% 정도가 안전한 배치 크기입니다.
💡 학습 초반에 nvidia-smi 명령어로 실시간 메모리 사용량을 모니터링하세요. 메모리 사용률이 85%를 넘으면 배치 크기를 줄여야 합니다.
💡 배치 크기를 2배로 늘릴 때는 학습률도 √2배(약 1.4배) 늘려야 같은 학습 효과를 얻습니다. 이건 많은 연구에서 검증된 공식입니다.
💡 데이터셋이 1000개 미만으로 작다면 배치 크기를 4~8로 제한하세요. 너무 크면 한 에폭에 업데이트 횟수가 너무 적어져서 학습이 제대로 안 됩니다.
💡 gradient_accumulation_steps는 2의 배수(2, 4, 8, 16)로 설정하세요. GPU가 2의 배수를 더 효율적으로 처리해서 속도가 5~10% 빨라집니다.
5. Data Collator 설정
시작하며
여러분이 AI에게 데이터를 먹일 때, 마치 손님에게 음식을 서빙하듯 예쁘게 담아서 한 번에 여러 개를 내야 합니다. 근데 음식(데이터)마다 크기가 다르고, 어떤 건 양념(레이블)이 있고 어떤 건 없다면?
식당 주인(여러분)은 어떻게 일괄적으로 서빙할까요? 일일이 손으로 담으면 너무 오래 걸리겠죠.
이런 문제는 실제 AI 학습에서 매 배치마다 발생합니다. 데이터를 배치로 묶을 때 길이를 맞추고, 텐서로 변환하고, 레이블을 붙이는 작업을 매번 해야 하는데, 이걸 수동으로 하면 코드가 복잡해지고 버그도 많이 생깁니다.
특히 마스크드 언어 모델(MLM)처럼 특수한 학습 방식은 추가 전처리가 더 필요해서 더욱 까다롭습니다. 바로 이럴 때 필요한 것이 Data Collator입니다.
배치를 만들 때 필요한 모든 전처리를 자동으로 해주는 똑똑한 도우미입니다. 마치 식당에 자동화 기계가 있어서 음식을 예쁘게 담고 트레이에 올려주는 것처럼, 데이터를 AI가 먹기 좋은 형태로 자동으로 포장해줍니다.
개요
간단히 말해서, Data Collator는 여러 개의 데이터 샘플을 하나의 배치로 묶을 때 필요한 패딩, 타입 변환, 특수 전처리를 자동으로 처리해주는 유틸리티입니다. 왜 이 설정이 필요할까요?
배치 생성 과정에서 매번 반복되는 지루한 작업을 자동화하고, 학습 방식에 맞는 특수 전처리를 쉽게 적용할 수 있기 때문입니다. 예를 들어, BERT의 Masked Language Model을 학습할 때는 입력 토큰의 15%를 [MASK]로 바꿔야 하는데, DataCollatorForLanguageModeling을 쓰면 이게 자동으로 됩니다.
직접 구현하면 100줄 코드가 한 줄로 줄어드는 거죠. 전통적인 방법에서는 DataLoader의 collate_fn을 직접 구현해야 했습니다.
패딩 추가, 텐서 변환, attention mask 생성을 모두 수동으로 코딩해야 했죠. 하지만 이제는 HuggingFace의 다양한 Data Collator를 선택하기만 하면 모든 게 자동입니다.
Data Collator의 핵심 특징은 세 가지입니다. 첫째, 동적 패딩으로 각 배치마다 최소한의 패딩만 추가합니다(메모리 절약).
둘째, 학습 방식에 맞는 여러 종류가 제공됩니다(MLM, Seq2Seq, Classification 등). 셋째, 커스터마이징이 쉬워서 특수한 요구사항도 간단히 구현할 수 있습니다.
이러한 특징들이 코드를 단순하게 만들고 버그를 줄여주기 때문에 필수적입니다.
코드 예제
from transformers import DataCollatorWithPadding, DataCollatorForLanguageModeling
from transformers import AutoTokenizer, Trainer
tokenizer = AutoTokenizer.from_pretrained('bert-base-multilingual-cased')
# 1. 분류 작업용: 동적 패딩만 수행
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
# 각 배치마다 가장 긴 샘플에 맞춰 패딩 (메모리 효율적)
# 2. Masked Language Model 학습용: 자동으로 15% 마스킹
data_collator_mlm = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=True, # Masked LM 활성화
mlm_probability=0.15 # 15%의 토큰을 [MASK]로 변경
)
# Trainer에 전달
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
data_collator=data_collator, # 여기서 배치 생성 자동화
)
설명
이것이 하는 일: 데이터 로더가 배치를 만들 때마다 자동으로 호출되어 여러 샘플을 하나로 묶고, 필요한 전처리를 수행합니다. 사용자는 어떤 종류의 Collator를 선택할지만 정하면 나머지는 자동으로 처리됩니다.
마치 자판기에 돈을 넣으면 음료가 나오는 것처럼, 데이터를 넣으면 학습 준비된 배치가 나옵니다. 첫 번째로, DataCollatorWithPadding은 가장 기본적인 Collator입니다.
배치 안의 샘플들을 보고 가장 긴 길이를 찾은 후, 나머지를 그 길이에 맞춰 패딩합니다. 이렇게 하는 이유는 고정 길이 패딩(max_length=512)보다 메모리를 훨씬 적게 쓰기 때문입니다.
예를 들어 배치 안의 최대 길이가 50이면 50으로만 패딩하지, 512로 패딩하지 않습니다. 내부적으로 PyTorch 텐서로 변환하고 attention_mask도 자동 생성합니다.
두 번째로, DataCollatorForLanguageModeling은 더 특별합니다. BERT를 처음부터 학습시킬 때 사용하는데, 입력 토큰의 15%를 무작위로 [MASK]로 바꿔줍니다.
내부 동작은 이렇습니다: 배치를 받으면 각 토큰마다 15% 확률로 선택하고, 선택된 토큰 중 80%는 [MASK]로, 10%는 랜덤 토큰으로, 10%는 그대로 둡니다(BERT 논문의 원래 방식). 이렇게 복잡한 로직을 직접 짜면 버그 투성이가 되기 쉬운데, Collator가 검증된 구현을 제공합니다.
세 번째로, Trainer에 data_collator를 전달하면 학습 루프에서 자동으로 사용됩니다. 매 스텝마다 배치를 만들 때 Collator가 호출되어 데이터를 준비하고, 그 결과가 모델에 입력됩니다.
최종적으로 여러분은 전처리 로직을 전혀 신경 쓰지 않아도 되고, 코드는 엄청나게 간결해지며, 버그 가능성은 거의 사라집니다. 여러분이 Data Collator를 사용하면 개발 시간을 크게 줄일 수 있습니다.
배치 전처리 코드를 직접 짜면 2~3시간 걸리고 디버깅에 또 2시간 걸리지만, Collator는 한 줄로 끝입니다. 또한 성능도 더 좋습니다.
HuggingFace의 구현은 C++ 백엔드를 사용해서 순수 Python보다 35배 빠릅니다. 메모리 효율도 뛰어나서 동적 패딩으로 메모리 사용량을 3050% 줄일 수 있습니다.
실전 팁
💡 분류 작업(Classification)에는 DataCollatorWithPadding, 언어 모델 사전학습에는 DataCollatorForLanguageModeling, 번역/요약에는 DataCollatorForSeq2Seq를 사용하세요. 작업에 맞는 Collator를 선택하는 게 핵심입니다.
💡 커스텀 전처리가 필요하면 기존 Collator를 상속받아 수정하세요. 예: class MyCollator(DataCollatorWithPadding): def call(self, features): ... 이렇게 하면 기본 기능은 유지하면서 필요한 부분만 추가할 수 있습니다.
💡 return_tensors='pt' 옵션으로 PyTorch 텐서, 'tf'로 TensorFlow 텐서를 반환할 수 있습니다. 기본값은 자동 감지이지만 명시적으로 지정하면 에러를 방지할 수 있습니다.
💡 DataCollatorForLanguageModeling에서 mlm=False로 설정하면 Causal LM(GPT 스타일) 학습용으로 전환됩니다. 다음 단어 예측 작업에 사용하세요.
💡 배치가 제대로 생성되는지 확인하려면 DataLoader를 직접 돌려보세요: for batch in dataloader: print(batch); break로 첫 배치의 shape과 내용을 확인할 수 있습니다.
6. 메모리 효율적인 데이터 로딩
시작하며
여러분이 100GB짜리 거대한 데이터셋으로 AI를 학습시켜야 하는데, 컴퓨터 메모리는 16GB밖에 없다면 어떻게 할까요? 일반적인 방법으로는 데이터를 메모리에 올리는 순간 컴퓨터가 멈춰버립니다.
마치 작은 컵에 바닷물을 담으려는 것과 같은 상황이죠. 이런 문제는 대규모 AI 프로젝트에서 늘 마주치는 큰 장벽입니다.
최근의 언어 모델들은 수십 GB에서 수백 GB의 텍스트 데이터로 학습됩니다. 이 모든 데이터를 RAM에 올리는 건 불가능하고, 그렇다고 작은 데이터만 쓰면 AI 성능이 형편없어집니다.
데이터가 많을수록 AI는 똑똑해지거든요. 바로 이럴 때 필요한 것이 메모리 효율적인 데이터 로딩 기법입니다.
전체 데이터를 메모리에 올리지 않고, 필요한 부분만 조금씩 읽어오면서 학습하는 똑똑한 방법입니다. 마치 책 전체를 외우는 게 아니라 필요한 페이지만 펼쳐서 읽는 것처럼, 데이터를 조금씩 스트리밍하면서 처리합니다.
개요
간단히 말해서, 메모리 효율적인 데이터 로딩은 전체 데이터를 메모리에 한 번에 올리지 않고 필요한 만큼만 디스크에서 읽어오면서 학습하는 방법입니다. 왜 이 방법이 필요할까요?
현대의 AI 학습은 데이터가 너무 커서 일반적인 컴퓨터 메모리에 다 올릴 수 없기 때문입니다. 예를 들어, Wikipedia 전체 텍스트로 언어 모델을 학습하려면 원본 데이터만 80GB가 넘습니다.
이걸 메모리에 올리면 다른 프로그램은 하나도 못 돌리고, 학습 중 메모리 부족으로 시스템이 다운될 위험도 큽니다. 전통적인 방법에서는 데이터를 작게 쪼개서 여러 파일로 나누고, 파일 하나씩 로드해서 학습했습니다.
하지만 이 방법은 코드가 복잡하고 파일 관리가 어렵습니다. 이제는 HuggingFace의 streaming 기능과 IterableDataset을 사용하면 단 한 줄로 해결됩니다.
메모리 효율적 로딩의 핵심 특징은 세 가지입니다. 첫째, streaming 모드로 데이터를 순차적으로 읽어오기 때문에 메모리 사용량이 일정하게 유지됩니다(데이터가 1GB든 1TB든 상관없음).
둘째, 디스크에서 데이터를 읽는 동안 GPU는 이전 배치를 계산하기 때문에 시간 낭비가 없습니다(파이프라이닝). 셋째, 데이터 전처리를 lazy evaluation으로 처리해서 실제 사용하기 직전에만 계산합니다.
이러한 특징들이 제한된 하드웨어에서도 대규모 학습을 가능하게 만들어주기 때문에 필수적입니다.
코드 예제
from datasets import load_dataset
from torch.utils.data import DataLoader
# 방법 1: Streaming 모드 (가장 메모리 효율적)
# 데이터를 메모리에 올리지 않고 디스크에서 조금씩 읽어옴
dataset = load_dataset('json', data_files='huge_dataset.json', streaming=True, split='train')
# Streaming dataset은 IterableDataset이 됨
print(type(dataset)) # <class 'datasets.IterableDataset'>
# 방법 2: Memory-mapped 로딩 (중간 크기 데이터에 적합)
# 데이터를 디스크에 특수 형식으로 저장하고 필요한 부분만 메모리에 로드
dataset = load_dataset('json', data_files='medium_dataset.json')
dataset.save_to_disk('dataset_cache') # Arrow 포맷으로 저장
cached_dataset = load_from_disk('dataset_cache') # 메모리 맵핑으로 로드
# 방법 3: DataLoader의 num_workers로 병렬 로딩
# 데이터 로딩을 별도 프로세스에서 처리 (GPU 대기 시간 최소화)
dataloader = DataLoader(
dataset,
batch_size=32,
num_workers=4, # 4개 프로세스가 데이터를 미리 준비
prefetch_factor=2 # 각 worker가 2개 배치를 미리 로드
)
설명
이것이 하는 일: 거대한 데이터셋을 작은 메모리에서도 처리할 수 있게 해줍니다. 전체 데이터를 한 번에 올리는 대신, 필요한 배치만 순차적으로 읽어오고, 동시에 다음 배치를 미리 준비해서 학습 속도도 유지합니다.
마치 컨베이어 벨트처럼 데이터가 끊임없이 흘러가면서 처리되는 구조입니다. 첫 번째로, streaming=True 옵션은 게임 체인저입니다.
이 옵션을 켜면 데이터셋이 IterableDataset으로 변환되어, 파일을 처음부터 끝까지 순차적으로 읽어옵니다. 메모리에는 현재 배치와 다음 몇 개 배치만 올라가고, 사용한 데이터는 즉시 메모리에서 제거됩니다.
이렇게 하는 이유는 100GB 데이터셋도 실제로는 한 번에 100MB만 메모리에 있으면 되기 때문입니다. 마치 비디오 스트리밍처럼 전체를 다운로드하지 않고 보는 부분만 버퍼링하는 거죠.
두 번째로, save_to_disk()와 load_from_disk()는 Apache Arrow 포맷을 활용합니다. 이 포맷은 메모리 맵핑을 지원해서, 파일이 디스크에 있지만 마치 메모리에 있는 것처럼 빠르게 접근할 수 있습니다.
내부적으로 OS가 필요한 부분만 메모리에 올리고, 안 쓰는 부분은 자동으로 내리기 때문에 수동으로 관리할 필요가 없습니다. 중간 크기 데이터(10~50GB)에 특히 유용합니다.
세 번째로, DataLoader의 num_workers는 멀티프로세싱을 활용합니다. GPU가 현재 배치를 학습하는 동안, 4개의 워커 프로세스가 다음 배치들을 미리 준비합니다.
prefetch_factor=2는 각 워커가 2개씩 미리 준비하니까 총 8개 배치가 대기 상태가 되는 거죠. 최종적으로 GPU는 데이터를 기다리는 시간이 거의 0이 되어, 학습 속도가 2~3배 빨라집니다.
여러분이 이 기법들을 조합하면 제한된 자원으로도 대규모 프로젝트를 수행할 수 있습니다. 예를 들어 16GB RAM 노트북에서 100GB 데이터셋으로 학습하는 게 가능해집니다.
streaming 모드는 메모리를 1~2GB만 사용하고, num_workers=4는 학습 시간을 50% 단축시킵니다. 또한 SSD를 사용하면 디스크 I/O 병목도 거의 없어져서 전체 학습 시간이 HDD 대비 5배 빨라집니다.
실전 팁
💡 streaming=True를 쓸 때는 데이터를 셔플할 수 없으니 dataset.shuffle(buffer_size=10000)를 추가하세요. buffer_size만큼만 메모리에 올려서 셔플하기 때문에 메모리 걱정 없이 랜덤성을 확보할 수 있습니다.
💡 num_workers는 CPU 코어 수의 절반 정도가 적당합니다. 너무 많으면 오히려 메모리 사용량만 늘고 속도는 안 빨라집니다. 8코어 CPU라면 num_workers=4가 최적입니다.
💡 데이터를 여러 번 재사용한다면 첫 로드 시 save_to_disk()로 캐싱하세요. 두 번째 로드부터는 10배 이상 빨라집니다. 특히 전처리가 복잡한 경우 필수입니다.
💡 prefetch_factor를 너무 크게 하면 메모리를 많이 먹습니다. 기본값 2가 메모리와 속도의 균형이 가장 좋습니다. 4 이상은 거의 도움이 안 됩니다.
💡 윈도우에서는 num_workers > 0일 때 에러가 날 수 있습니다. if name == 'main': 블록 안에서 DataLoader를 생성하세요. 이건 Python의 멀티프로세싱 제약 때문입니다.