이미지 로딩 중...
AI Generated
2025. 11. 9. · 2 Views
AI 파인튜닝 5편 - Hugging Face 활용 완벽 가이드
Hugging Face 라이브러리를 활용하여 사전학습된 AI 모델을 실무 데이터로 파인튜닝하는 방법을 학습합니다. Trainer API, 데이터셋 처리, 모델 평가까지 전체 파이프라인을 다룹니다.
목차
- Hugging Face Transformers 라이브러리 소개 - 사전학습 모델 활용의 시작
- 데이터셋 준비와 전처리 - Datasets 라이브러리 활용
- Trainer API로 파인튜닝 시작하기 - 간단한 학습 파이프라인
- 사용자 정의 데이터로 파인튜닝 - 실무 데이터 적용
- 모델 평가와 성능 측정 - 메트릭 이해하기
- 하이퍼파라미터 튜닝 - 최적의 설정 찾기
- 모델 저장과 로드 - 재사용과 배포 준비
- 배치 추론 최적화 - 대량 데이터 효율적 처리
- 다중 레이블 분류 - 여러 카테고리 동시 예측
- 모델 경량화와 양자화 - 모바일과 엣지 배포
1. Hugging Face Transformers 라이브러리 소개 - 사전학습 모델 활용의 시작
시작하며
여러분이 AI 모델을 처음부터 학습시키려고 할 때 이런 고민을 해본 적 있나요? "수백만 개의 데이터를 어디서 구하지?
GPU 비용은 얼마나 들까? 학습에 몇 주가 걸릴까?" 실제로 BERT나 GPT 같은 대형 모델을 처음부터 학습시키려면 수천만 원의 비용과 전문 인력이 필요합니다.
하지만 다행히도 우리는 이미 학습된 모델을 가져와서 우리의 특정 업무에 맞게 조정할 수 있습니다. 이것이 바로 파인튜닝의 핵심 아이디어입니다.
이미 언어의 기본 패턴을 학습한 모델을 가져와서, 우리가 해결하려는 특정 문제에 맞게 미세 조정하는 것이죠. 바로 이럴 때 필요한 것이 Hugging Face Transformers 라이브러리입니다.
수천 개의 사전학습된 모델을 단 몇 줄의 코드로 불러와 사용할 수 있게 해주는 강력한 도구입니다.
개요
간단히 말해서, Hugging Face Transformers는 최신 AI 모델들을 쉽게 사용할 수 있게 해주는 파이썬 라이브러리입니다. BERT, GPT, T5 등 유명한 모델들을 모두 지원합니다.
왜 이 라이브러리가 필요한지 실무 관점에서 보면, 모델을 처음부터 구현하는 것은 매우 복잡합니다. 논문을 읽고, 수천 줄의 코드를 작성하고, 버그를 찾아야 하죠.
예를 들어, 감정 분석 서비스를 만들려면 텍스트 분류 모델이 필요한데, Transformers를 사용하면 이미 검증된 모델을 바로 활용할 수 있습니다. 기존에는 TensorFlow나 PyTorch로 모델 구조를 직접 코딩했다면, 이제는 AutoModel.from_pretrained()로 한 줄이면 충분합니다.
모델의 가중치, 토크나이저, 설정이 모두 자동으로 다운로드됩니다. 이 라이브러리의 핵심 특징은 첫째, 통일된 API로 다양한 모델을 동일한 방식으로 사용할 수 있다는 점, 둘째, Hugging Face Hub와 연동되어 커뮤니티가 공유한 모델을 즉시 활용할 수 있다는 점, 셋째, PyTorch와 TensorFlow를 모두 지원한다는 점입니다.
이러한 특징들이 개발 속도를 획기적으로 높이고 실험을 빠르게 반복할 수 있게 해줍니다.
코드 예제
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
# 사전학습된 BERT 모델과 토크나이저 로드
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
# 텍스트를 모델 입력 형식으로 변환
text = "This product is amazing!"
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
# 모델 추론 수행
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.softmax(outputs.logits, dim=1)
print(f"긍정 확률: {predictions[0][1].item():.2%}")
설명
이것이 하는 일: 위 코드는 Hugging Face Hub에서 사전학습된 BERT 모델을 다운로드하고, 입력 텍스트에 대한 감정 분류를 수행합니다. 모델의 복잡한 내부 구조를 몰라도 간단한 API만으로 AI를 활용할 수 있습니다.
첫 번째로, AutoTokenizer와 AutoModel 클래스를 사용하여 모델과 토크나이저를 자동으로 불러옵니다. "bert-base-uncased"라는 모델 이름만 지정하면, 라이브러리가 적절한 클래스를 자동으로 선택하고 사전학습된 가중치를 다운로드합니다.
num_labels=2는 이진 분류(긍정/부정)를 위한 설정입니다. 그 다음으로, 토크나이저가 일반 텍스트를 모델이 이해할 수 있는 숫자 시퀀스로 변환합니다.
return_tensors="pt"는 PyTorch 텐서 형식으로 반환하라는 의미이고, padding과 truncation은 모든 입력을 동일한 길이로 맞추는 역할을 합니다. 내부적으로는 단어를 서브워드 토큰으로 분해하고, 각 토큰을 숫자 ID로 매핑합니다.
마지막으로, model(**inputs)로 모델에 입력을 전달하여 예측을 수행합니다. torch.no_grad()는 그래디언트 계산을 비활성화하여 추론 속도를 높입니다.
출력된 logits에 softmax를 적용하면 각 클래스의 확률을 얻을 수 있습니다. 여러분이 이 코드를 사용하면 별도의 모델 학습 없이도 즉시 텍스트 분류를 시작할 수 있습니다.
실무에서는 이 기본 모델을 자신의 데이터로 파인튜닝하여 정확도를 크게 높일 수 있고, 다양한 도메인(리뷰 분석, 스팸 필터링, 감정 분석 등)에 적용할 수 있으며, API 서버로 배포하여 실시간 서비스를 구축할 수 있습니다.
실전 팁
💡 모델 이름 끝에 버전을 명시하면 재현 가능한 코드를 작성할 수 있습니다. 예: "bert-base-uncased-v2.0"
💡 첫 실행 시 모델 다운로드에 시간이 걸리므로, 프로덕션에서는 미리 다운로드하여 로컬 경로를 사용하세요
💡 GPU가 있다면 model.to("cuda")로 모델을 GPU로 이동시켜 추론 속도를 10배 이상 높일 수 있습니다
💡 여러 텍스트를 처리할 때는 배치로 묶어서 처리하면 효율적입니다: tokenizer(texts, batch=True)
💡 AutoModel 대신 AutoModelForSequenceClassification 같은 태스크별 클래스를 사용하면 적절한 헤드가 자동으로 추가됩니다
2. 데이터셋 준비와 전처리 - Datasets 라이브러리 활용
시작하며
여러분이 파인튜닝을 시작하려고 CSV 파일을 열었을 때 이런 문제를 겪어본 적 있나요? "메모리가 부족하다고?
10GB 데이터를 어떻게 로드하지?" 또는 "토큰화를 어떻게 효율적으로 처리하지?" 대용량 데이터 처리는 AI 개발에서 가장 흔한 병목 지점입니다. 전통적인 방법으로 pandas를 사용하면 모든 데이터를 메모리에 올려야 하고, 전처리 코드도 직접 작성해야 합니다.
데이터가 커질수록 메모리 오류가 발생하고, 처리 속도도 느려집니다. 바로 이럴 때 필요한 것이 Hugging Face Datasets 라이브러리입니다.
메모리 맵 기술을 사용하여 테라바이트급 데이터도 효율적으로 처리하고, 병렬 처리로 전처리 속도를 획기적으로 높여줍니다.
개요
간단히 말해서, Datasets는 대용량 데이터를 효율적으로 로드하고 전처리할 수 있게 해주는 라이브러리입니다. Apache Arrow 포맷을 사용하여 디스크에서 직접 데이터를 읽어 메모리를 절약합니다.
왜 이 라이브러리가 필요한지 보면, AI 모델 학습에는 수십만~수백만 개의 샘플이 필요한데, 이를 효율적으로 관리하지 않으면 시스템이 멈춥니다. 예를 들어, 고객 리뷰 100만 건을 분석하는 프로젝트에서는 데이터 로딩만 몇 시간 걸릴 수 있지만, Datasets를 사용하면 몇 분이면 충분합니다.
기존에는 데이터를 청크로 나누어 읽고, 수동으로 배치를 만들고, 캐싱 로직을 구현했다면, 이제는 dataset.map()으로 모든 전처리를 자동화할 수 있습니다. 한 번 처리된 결과는 캐시되어 재실행 시 즉시 로드됩니다.
이 라이브러리의 핵심 특징은 첫째, 메모리 효율성으로 RAM보다 큰 데이터도 처리 가능, 둘째, 멀티프로세싱을 통한 빠른 전처리, 셋째, Hugging Face Hub의 5000개 이상의 공개 데이터셋 즉시 사용 가능합니다. 이러한 특징들이 데이터 파이프라인 구축 시간을 대폭 단축시킵니다.
코드 예제
from datasets import load_dataset, Dataset
from transformers import AutoTokenizer
# 공개 데이터셋 로드 (IMDb 영화 리뷰)
dataset = load_dataset("imdb", split="train")
# 또는 자신의 CSV 파일 로드
# dataset = Dataset.from_csv("my_data.csv")
# 토크나이저 준비
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
# 전처리 함수 정의
def preprocess_function(examples):
# 배치 단위로 토큰화 수행
return tokenizer(examples["text"], truncation=True, padding="max_length", max_length=512)
# 병렬 처리로 전체 데이터셋 전처리 (멀티프로세싱 사용)
tokenized_dataset = dataset.map(preprocess_function, batched=True, num_proc=4)
# 학습/검증 데이터 분할
train_test = tokenized_dataset.train_test_split(test_size=0.2)
print(f"학습 샘플: {len(train_test['train'])}, 검증 샘플: {len(train_test['test'])}")
설명
이것이 하는 일: 위 코드는 IMDb 영화 리뷰 데이터셋을 로드하고, BERT 토크나이저로 전처리한 후, 학습/검증 세트로 분할합니다. 모든 과정이 메모리 효율적이고 병렬로 처리됩니다.
첫 번째로, load_dataset("imdb")는 Hugging Face Hub에서 IMDb 데이터셋을 자동으로 다운로드하고 로드합니다. 내부적으로는 Arrow 포맷으로 저장되어, 50,000개의 리뷰가 있어도 실제로는 필요한 부분만 메모리에 로드됩니다.
자신의 데이터를 사용하려면 Dataset.from_csv() 또는 Dataset.from_pandas()를 사용하면 됩니다. 그 다음으로, preprocess_function에서 토큰화를 정의합니다.
중요한 점은 examples가 단일 샘플이 아니라 여러 샘플의 딕셔너리라는 것입니다. batched=True 옵션 덕분에 한 번에 여러 텍스트를 처리하여 속도가 훨씬 빠릅니다.
truncation=True는 512토큰을 초과하는 텍스트를 잘라내고, padding="max_length"는 모든 시퀀스를 동일한 길이로 맞춥니다. dataset.map() 메서드가 실행되면서 전체 데이터셋에 전처리 함수가 적용됩니다.
num_proc=4는 4개의 CPU 코어를 사용하여 병렬 처리하라는 의미입니다. 처리된 결과는 자동으로 캐시되므로, 같은 코드를 다시 실행하면 캐시에서 즉시 로드됩니다.
마지막으로, train_test_split()으로 데이터를 80:20 비율로 나눕니다. 랜덤 시드를 설정하면 재현 가능한 분할이 됩니다.
이렇게 준비된 데이터셋은 바로 Trainer API에 전달할 수 있습니다. 여러분이 이 코드를 사용하면 몇 가지 큰 이점을 얻습니다.
첫째, 데이터 크기에 관계없이 일정한 메모리 사용량을 유지할 수 있습니다. 둘째, 멀티프로세싱으로 전처리 시간을 1/4로 단축할 수 있습니다.
셋째, 캐싱 덕분에 실험을 빠르게 반복할 수 있습니다. 넷째, 다양한 포맷(CSV, JSON, Parquet)을 통일된 방식으로 처리할 수 있습니다.
실전 팁
💡 num_proc는 CPU 코어 수보다 작게 설정하세요. 시스템이 멈출 수 있습니다. 보통 코어 수의 절반이 적당합니다
💡 전처리 중 에러가 발생하면 map()에 remove_columns 파라미터로 불필요한 컬럼을 제거하세요
💡 대용량 데이터는 streaming=True 옵션으로 로드하면 다운로드 없이 바로 사용할 수 있습니다
💡 dataset.shuffle(seed=42)로 데이터를 섞되, 재현성을 위해 시드를 고정하세요
💡 전처리 함수가 복잡하면 with_transform() 메서드로 온디맨드 변환을 사용하여 메모리를 더 절약할 수 있습니다
3. Trainer API로 파인튜닝 시작하기 - 간단한 학습 파이프라인
시작하며
여러분이 모델 학습 코드를 작성할 때 이런 고민을 해본 적 있나요? "학습 루프를 어떻게 구현하지?
체크포인트 저장은? 텐서보드 로깅은?" 학습 파이프라인을 처음부터 구현하면 수백 줄의 보일러플레이트 코드가 필요합니다.
PyTorch로 직접 구현하면 데이터로더 설정, 옵티마이저 선택, 그래디언트 계산, 가중치 업데이트, 검증 루프, 모델 저장 등을 모두 직접 코딩해야 합니다. 버그가 생기기 쉽고, 실험 추적도 어렵습니다.
바로 이럴 때 필요한 것이 Hugging Face Trainer API입니다. 단 몇 줄의 설정만으로 프로덕션 레벨의 학습 파이프라인을 구축할 수 있습니다.
자동 혼합 정밀도, 분산 학습, 조기 종료까지 모두 지원합니다.
개요
간단히 말해서, Trainer는 모델 학습의 모든 복잡한 과정을 자동화해주는 고수준 API입니다. 학습 루프, 평가, 로깅, 체크포인트 관리를 내장하고 있습니다.
왜 이것이 필요한지 보면, 실무에서는 빠른 실험과 안정적인 학습이 핵심입니다. 매번 학습 코드를 작성하면 시간이 오래 걸리고 실수하기 쉽습니다.
예를 들어, 감정 분석 모델을 3가지 다른 하이퍼파라미터로 실험하려면, Trainer를 사용하면 설정만 바꾸면 되지만 직접 구현하면 코드를 여러 번 수정해야 합니다. 기존에는 PyTorch로 for epoch in range(num_epochs): 루프를 작성하고, 옵티마이저, 스케줄러, 그래디언트 클리핑을 수동으로 관리했다면, 이제는 TrainingArguments로 모든 설정을 선언하고 trainer.train()만 호출하면 됩니다.
이 API의 핵심 특징은 첫째, 학습/검증/테스트의 전체 사이클을 자동화, 둘째, 다양한 학습 기법(mixed precision, gradient accumulation)을 간단히 활성화, 셋째, TensorBoard, Weights & Biases 같은 로깅 도구와의 통합, 넷째, 분산 학습(멀티 GPU, TPU)을 코드 변경 없이 지원합니다. 이러한 특징들이 연구와 개발 속도를 극대화합니다.
코드 예제
from transformers import Trainer, TrainingArguments, AutoModelForSequenceClassification
from datasets import load_metric
# 평가 메트릭 정의
metric = load_metric("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = logits.argmax(axis=-1)
return metric.compute(predictions=predictions, references=labels)
# 학습 설정
training_args = TrainingArguments(
output_dir="./results", # 체크포인트 저장 경로
evaluation_strategy="epoch", # 매 에포크마다 평가
learning_rate=2e-5, # 학습률
per_device_train_batch_size=16, # 배치 크기
num_train_epochs=3, # 총 에포크 수
weight_decay=0.01, # 가중치 감쇠 (정규화)
save_strategy="epoch", # 에포크마다 모델 저장
load_best_model_at_end=True, # 학습 종료 시 최고 모델 로드
logging_dir="./logs", # 텐서보드 로그
)
# 모델 로드
model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
# Trainer 초기화
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_test['train'],
eval_dataset=train_test['test'],
compute_metrics=compute_metrics,
)
# 학습 시작!
trainer.train()
# 평가 수행
results = trainer.evaluate()
print(f"정확도: {results['eval_accuracy']:.2%}")
설명
이것이 하는 일: 위 코드는 BERT 모델을 감정 분류 태스크로 파인튜닝합니다. TrainingArguments로 모든 학습 설정을 정의하고, Trainer가 자동으로 학습/평가를 수행합니다.
첫 번째로, compute_metrics 함수를 정의하여 평가 지표를 계산합니다. eval_pred는 모델의 예측 logits와 실제 레이블을 포함합니다.
argmax로 가장 높은 확률의 클래스를 선택하고, 정확도를 계산합니다. 여기서는 accuracy를 사용하지만, F1 score, precision, recall 등 다른 메트릭도 추가할 수 있습니다.
그 다음으로, TrainingArguments에서 학습 설정을 선언합니다. evaluation_strategy="epoch"는 매 에포크 종료 시 검증 세트로 평가하라는 의미입니다.
learning_rate=2e-5는 BERT 파인튜닝에 권장되는 학습률입니다. per_device_train_batch_size=16은 GPU당 배치 크기로, GPU 메모리에 따라 조정해야 합니다.
load_best_model_at_end=True는 학습 중 가장 좋은 성능의 체크포인트를 마지막에 자동으로 로드합니다. Trainer 객체를 생성할 때 모델, 설정, 데이터셋, 메트릭 함수를 전달합니다.
내부적으로 Trainer는 데이터로더를 생성하고, AdamW 옵티마이저를 설정하며, 선형 스케줄러를 초기화합니다. 이 모든 과정이 자동으로 처리됩니다.
trainer.train()을 호출하면 실제 학습이 시작됩니다. 매 스텝마다 손실이 계산되고, 백프로파게이션으로 그래디언트를 구하며, 옵티마이저가 가중치를 업데이트합니다.
매 에포크마다 검증 세트에서 평가하고, 결과를 로그에 기록하며, 체크포인트를 저장합니다. 진행 상황은 tqdm 프로그레스 바로 표시됩니다.
여러분이 이 코드를 사용하면 몇 가지 강력한 이점을 얻습니다. 첫째, 수백 줄의 보일러플레이트 코드를 몇 줄로 대체할 수 있습니다.
둘째, 체크포인트 저장으로 학습이 중단되어도 이어서 할 수 있습니다. 셋째, 자동 로깅으로 텐서보드에서 실시간으로 학습 곡선을 확인할 수 있습니다.
넷째, 하이퍼파라미터를 바꾸는 것만으로 다양한 실험을 빠르게 수행할 수 �습니다. 다섯째, 같은 코드로 CPU, GPU, 멀티 GPU, TPU에서 모두 실행 가능합니다.
실전 팁
💡 GPU 메모리 부족 시 per_device_train_batch_size를 줄이고 gradient_accumulation_steps를 늘려 effective batch size를 유지하세요
💡 fp16=True 옵션으로 혼합 정밀도 학습을 활성화하면 메모리 사용량과 학습 시간을 약 50% 줄일 수 있습니다
💡 save_total_limit=3으로 저장할 체크포인트 개수를 제한하여 디스크 공간을 절약하세요
💡 warmup_steps를 설정하면 초기 학습 불안정을 방지할 수 있습니다. 보통 전체 스텝의 10% 정도가 적당합니다
💡 eval_steps를 설정하면 에포크 중간에도 평가를 수행하여 더 세밀한 모니터링이 가능합니다
4. 사용자 정의 데이터로 파인튜닝 - 실무 데이터 적용
시작하며
여러분이 회사의 실제 데이터로 AI 모델을 학습시키려고 할 때 이런 상황에 직면한 적 있나요? "우리 데이터는 공개 데이터셋과 형식이 다른데, 어떻게 적용하지?" 또는 "고객 리뷰 데이터를 어떻게 BERT에 맞게 전처리하지?" 실무 데이터는 항상 예상과 다르고 복잡합니다.
공개 데이터셋으로 실험하는 것과 실제 비즈니스 데이터를 사용하는 것은 완전히 다른 문제입니다. 데이터에 결측치가 있거나, 레이블이 불균형하거나, 특수 문자가 포함되어 있을 수 있습니다.
이러한 문제를 해결하지 않으면 모델 성능이 크게 떨어집니다. 바로 이럴 때 필요한 것이 사용자 정의 데이터 처리 파이프라인입니다.
자신의 데이터 형식에 맞게 전처리를 커스터마이징하고, 도메인 특화 로직을 추가하여 모델의 실무 적용 가능성을 높입니다.
개요
간단히 말해서, 사용자 정의 데이터로 파인튜닝한다는 것은 자신의 비즈니스 데이터에 맞게 전처리 로직을 작성하고, 도메인 지식을 반영하여 모델을 학습시키는 것입니다. 단순히 API를 호출하는 것이 아니라 데이터의 특성을 이해하고 적절히 가공하는 과정입니다.
왜 이것이 중요한지 보면, 같은 모델이라도 데이터 품질에 따라 성능이 천차만별입니다. 예를 들어, 전자상거래 사이트의 상품 리뷰를 분석하는 경우, 이모티콘, 신조어, 오타가 많이 포함되어 있습니다.
이런 데이터를 그대로 사용하면 모델이 제대로 학습하지 못합니다. 적절한 정제와 증강이 필요합니다.
기존에는 pandas로 데이터를 로드하고 수동으로 반복문을 돌며 전처리했다면, 이제는 Datasets의 map() 함수에 커스텀 전처리 함수를 전달하여 효율적으로 처리할 수 있습니다. 클래스 불균형 문제도 가중치를 조정하거나 샘플링 기법을 적용하여 해결합니다.
이 접근법의 핵심 특징은 첫째, 비즈니스 도메인의 특수성을 반영할 수 있다는 점, 둘째, 데이터 품질 문제를 선제적으로 해결할 수 있다는 점, 셋째, 모델이 실제 사용 환경과 유사한 데이터로 학습되어 성능이 높아진다는 점입니다. 이러한 특징들이 프로토타입을 실제 서비스로 발전시키는 데 핵심적입니다.
코드 예제
import pandas as pd
from datasets import Dataset
from transformers import AutoTokenizer
import re
# 실무 CSV 데이터 로드 (예: 고객 리뷰)
df = pd.read_csv("customer_reviews.csv")
# 데이터 정제 함수
def clean_text(text):
if pd.isna(text): # 결측치 처리
return ""
text = re.sub(r'http\S+', '', text) # URL 제거
text = re.sub(r'[^가-힣a-zA-Z0-9\s]', '', text) # 특수문자 제거
return text.strip()
# pandas 데이터프레임을 Dataset으로 변환
df['text'] = df['review'].apply(clean_text)
df = df[df['text'].str.len() > 10] # 너무 짧은 리뷰 제거
dataset = Dataset.from_pandas(df[['text', 'rating']])
# 레이블 변환 (1-5점 -> 긍정/부정)
def convert_label(example):
example['label'] = 1 if example['rating'] >= 4 else 0
return example
dataset = dataset.map(convert_label)
# 토크나이저로 전처리
tokenizer = AutoTokenizer.from_pretrained("bert-base-multilingual-cased")
def tokenize_function(examples):
return tokenizer(
examples["text"],
padding="max_length",
truncation=True,
max_length=128 # 리뷰는 보통 짧으므로 128로 설정
)
tokenized = dataset.map(tokenize_function, batched=True, remove_columns=['text', 'rating'])
# 클래스 불균형 확인
labels = tokenized['label']
print(f"긍정: {sum(labels)}, 부정: {len(labels) - sum(labels)}")
설명
이것이 하는 일: 위 코드는 실제 고객 리뷰 CSV 파일을 로드하고, 데이터를 정제한 후, BERT 모델에 맞게 토크나이징합니다. 실무에서 발생하는 다양한 데이터 품질 문제를 해결하는 과정을 보여줍니다.
첫 번째로, clean_text 함수에서 실무 데이터의 노이즈를 제거합니다. 결측치가 있으면 빈 문자열로 대체하고, 정규표현식으로 URL과 특수문자를 제거합니다.
한글, 영문, 숫자만 남기는 이유는 이모티콘이나 이상한 기호가 토크나이징을 방해하기 때문입니다. 실무 데이터는 사용자가 직접 입력한 것이므로 예상치 못한 문자가 많이 포함됩니다.
그 다음으로, pandas 데이터프레임을 정제한 후 Dataset 객체로 변환합니다. df['text'].str.len() > 10 조건으로 너무 짧은 리뷰를 필터링합니다.
"좋음", "별로" 같은 한두 단어 리뷰는 학습에 도움이 되지 않습니다. Dataset.from_pandas()는 자동으로 컬럼을 인식하여 Dataset을 생성합니다.
convert_label 함수는 비즈니스 로직을 반영합니다. 원래 데이터는 1-5점 평점인데, 이를 이진 분류(긍정/부정)로 변환합니다.
4점 이상을 긍정으로 간주하는 것은 도메인 지식에 기반한 판단입니다. 이런 변환 로직은 비즈니스마다 다르므로 커스터마이징이 필수입니다.
토크나이징 단계에서는 다국어 BERT 모델을 사용합니다. 한글 데이터이므로 bert-base-multilingual-cased가 적합합니다.
max_length=128로 설정한 이유는 대부분의 리뷰가 짧기 때문에 512는 낭비이기 때문입니다. 메모리와 학습 속도를 고려한 실용적 선택입니다.
remove_columns로 원본 텍스트와 평점 컬럼을 제거하여 모델 입력에 필요한 데이터만 남깁니다. 마지막으로 클래스 분포를 확인합니다.
만약 긍정이 90%, 부정이 10%라면 클래스 불균형 문제가 있습니다. 이 경우 Trainer의 compute_loss 메서드를 오버라이드하여 가중치를 조정하거나, 오버샘플링/언더샘플링을 적용해야 합니다.
여러분이 이 코드를 사용하면 실무 데이터의 다양한 문제를 체계적으로 해결할 수 있습니다. 첫째, 데이터 정제로 노이즈를 제거하여 모델 성능을 10-20% 향상시킬 수 있습니다.
둘째, 도메인 지식을 반영한 레이블 변환으로 비즈니스 목적에 맞는 모델을 만들 수 있습니다. 셋째, 효율적인 전처리로 학습 시간을 단축할 수 있습니다.
넷째, 클래스 불균형을 조기에 발견하여 적절히 대응할 수 있습니다.
실전 팁
💡 데이터 정제 전후의 샘플을 직접 확인하여 중요한 정보가 손실되지 않았는지 검증하세요
💡 클래스 불균형이 심하면 (80:20 이상) class_weight 파라미터나 focal loss를 사용하여 소수 클래스에 더 많은 가중치를 부여하세요
💡 도메인 특화 단어(예: 전문 용어, 브랜드명)가 많다면 사전에 토큰을 추가하여 토크나이저를 확장하세요
💡 dataset.train_test_split(stratify_by_column='label')로 계층화 샘플링을 수행하면 학습/검증 세트의 클래스 비율이 동일하게 유지됩니다
💡 전처리 결과를 tokenized.save_to_disk()로 저장하면 다음 실험 시 재사용할 수 있어 시간을 절약할 수 있습니다
5. 모델 평가와 성능 측정 - 메트릭 이해하기
시작하며
여러분이 모델 학습을 끝내고 "Accuracy: 0.92"라는 결과를 봤을 때 이런 의문이 든 적 있나요? "92%면 좋은 건가?
실제로 잘 작동할까?" 또는 "정확도만 보면 충분한가?" 단일 메트릭만 보고 모델을 평가하는 것은 위험합니다. 실무에서는 정확도가 높아도 실패하는 경우가 많습니다.
예를 들어, 스팸 필터에서 정상 메일이 95%, 스팸이 5%라면, 모든 메일을 "정상"으로 분류해도 95% 정확도가 나옵니다. 하지만 이 모델은 쓸모가 없죠.
클래스 불균형이 있을 때는 다른 메트릭이 필요합니다. 바로 이럴 때 필요한 것이 다양한 평가 메트릭과 혼동 행렬(Confusion Matrix)입니다.
Precision, Recall, F1-score를 함께 보면 모델의 진짜 성능을 파악할 수 있고, 어떤 부분이 약한지 알 수 있습니다.
개요
간단히 말해서, 모델 평가는 단순히 정확도를 보는 것이 아니라 Precision(정밀도), Recall(재현율), F1-score를 종합적으로 분석하는 것입니다. 각 메트릭은 모델의 다른 측면을 보여줍니다.
왜 다양한 메트릭이 필요한지 보면, 비즈니스 목표에 따라 중요한 메트릭이 다르기 때문입니다. 예를 들어, 암 진단 모델에서는 Recall이 중요합니다.
실제 암 환자를 놓치면 안 되니까요(False Negative 최소화). 반면 스팸 필터에서는 Precision이 중요합니다.
정상 메일을 스팸으로 잘못 분류하면 중요한 메일을 놓칠 수 있으니까요(False Positive 최소화). 기존에는 단순히 정확도만 계산했다면, 이제는 classification_report로 클래스별 세부 메트릭을 확인하고, 혼동 행렬로 어떤 클래스를 혼동하는지 시각화할 수 있습니다.
이를 통해 모델의 약점을 정확히 파악하고 개선할 수 있습니다. 이 접근법의 핵심 특징은 첫째, 클래스별 성능을 개별적으로 분석할 수 있다는 점, 둘째, 비즈니스 요구사항에 맞는 메트릭을 선택할 수 있다는 점, 셋째, 모델의 오류 패턴을 시각화하여 인사이트를 얻을 수 있다는 점입니다.
이러한 특징들이 모델을 프로덕션에 배포하기 전 신뢰성을 검증하는 데 필수적입니다.
코드 예제
from transformers import Trainer
from sklearn.metrics import classification_report, confusion_matrix
import numpy as np
# 학습된 모델로 예측 수행
predictions = trainer.predict(tokenized_dataset['test'])
# 예측 결과에서 클래스 추출
preds = predictions.predictions.argmax(-1)
labels = predictions.label_ids
# 상세한 평가 리포트 생성
report = classification_report(
labels,
preds,
target_names=['부정', '긍정'],
digits=4
)
print("분류 리포트:\n", report)
# 혼동 행렬 계산
cm = confusion_matrix(labels, preds)
print("\n혼동 행렬:")
print(f" 예측: 부정 예측: 긍정")
print(f"실제: 부정 {cm[0][0]:6d} {cm[0][1]:6d}")
print(f"실제: 긍정 {cm[1][0]:6d} {cm[1][1]:6d}")
# 메트릭 개별 계산
tn, fp, fn, tp = cm.ravel()
precision = tp / (tp + fp)
recall = tp / (tp + fn)
f1 = 2 * (precision * recall) / (precision + recall)
print(f"\n긍정 클래스 메트릭:")
print(f"Precision: {precision:.4f} (모델이 긍정이라고 한 것 중 실제 긍정 비율)")
print(f"Recall: {recall:.4f} (실제 긍정 중 모델이 찾아낸 비율)")
print(f"F1-Score: {f1:.4f} (Precision과 Recall의 조화평균)")
설명
이것이 하는 일: 위 코드는 학습된 모델을 테스트 데이터로 평가하고, 다양한 메트릭과 혼동 행렬을 계산하여 모델의 실제 성능을 다각도로 분석합니다. 첫 번째로, trainer.predict()로 테스트 데이터에 대한 예측을 수행합니다.
반환되는 객체에는 predictions(로짓 값)와 label_ids(실제 레이블)가 포함됩니다. argmax(-1)로 가장 높은 확률의 클래스를 선택하여 최종 예측 클래스를 얻습니다.
그 다음으로, sklearn의 classification_report로 상세한 평가 리포트를 생성합니다. 이 리포트는 각 클래스별로 Precision, Recall, F1-score를 계산해줍니다.
target_names로 클래스 이름을 지정하면 결과를 읽기 쉽게 표시됩니다. digits=4는 소수점 4자리까지 표시하여 미세한 성능 차이도 확인할 수 있게 합니다.
혼동 행렬(Confusion Matrix)은 모델의 오류 패턴을 보여줍니다. cm[0][0]은 True Negative(부정을 부정으로 맞춤), cm[0][1]은 False Positive(부정을 긍정으로 잘못 예측), cm[1][0]은 False Negative(긍정을 부정으로 잘못 예측), cm[1][1]은 True Positive(긍정을 긍정으로 맞춤)입니다.
이 값들을 보면 모델이 어떤 타입의 실수를 주로 하는지 알 수 있습니다. 메트릭을 개별적으로 계산하여 의미를 명확히 합니다.
Precision은 "모델이 긍정이라고 예측한 것 중 실제로 긍정인 비율"로, 오탐지(False Positive)를 줄이는 것이 중요할 때 주목해야 합니다. Recall은 "실제 긍정 중 모델이 찾아낸 비율"로, 누락(False Negative)을 줄이는 것이 중요할 때 주목합니다.
F1-score는 둘의 조화평균으로, 균형 잡힌 성능을 원할 때 사용합니다. 예를 들어, 암 진단 모델에서 Precision=0.8, Recall=0.95라면, 실제 암 환자의 95%를 찾아냈지만(좋음), 모델이 암이라고 한 사람 중 20%는 오진(나쁨)입니다.
반대로 Precision=0.95, Recall=0.8이라면, 모델이 암이라고 하면 거의 확실하지만, 실제 암 환자의 20%를 놓칩니다. 이 경우 Recall이 더 중요하므로 모델을 개선해야 합니다.
여러분이 이 평가 방법을 사용하면 모델을 훨씬 정확히 이해할 수 있습니다. 첫째, 단순 정확도에 숨겨진 문제를 발견할 수 있습니다.
둘째, 비즈니스 요구사항에 맞는 메트릭을 선택하여 최적화할 수 있습니다. 셋째, 혼동 행렬로 모델의 약점을 찾아 데이터 증강이나 재학습 전략을 세울 수 있습니다.
넷째, 클래스별 성능 차이를 보고 불균형 문제를 해결할 수 있습니다.
실전 팁
💡 클래스 불균형이 있을 때는 accuracy 대신 macro F1-score나 weighted F1-score를 주 메트릭으로 사용하세요
💡 혼동 행렬을 히트맵으로 시각화하면 (seaborn 사용) 오류 패턴을 한눈에 파악할 수 있습니다
💡 ROC 곡선과 AUC 스코어를 추가로 계산하면 분류 임계값을 조정할 때 유용합니다
💡 검증 세트와 테스트 세트의 메트릭을 비교하여 과적합 여부를 확인하세요. 큰 차이가 있다면 과적합입니다
💡 프로덕션 배포 전 실제 사용자 분포와 유사한 데이터로 평가하여 실전 성능을 예측하세요
6. 하이퍼파라미터 튜닝 - 최적의 설정 찾기
시작하며
여러분이 모델을 학습시켰는데 성능이 기대에 못 미칠 때 이런 생각을 해본 적 있나요? "학습률을 바꿔볼까?
배치 크기를 늘려볼까? 에포크를 더 돌려볼까?" 하이퍼파라미터 조합은 무수히 많고, 하나하나 수동으로 실험하면 시간이 엄청나게 걸립니다.
잘못된 하이퍼파라미터는 모델 성능을 크게 떨어뜨립니다. 학습률이 너무 높으면 학습이 불안정하고, 너무 낮으면 학습이 느리거나 지역 최솟값에 갇힙니다.
배치 크기도 마찬가지로 성능과 학습 속도에 큰 영향을 줍니다. 바로 이럴 때 필요한 것이 체계적인 하이퍼파라미터 튜닝입니다.
Hugging Face는 Optuna 같은 라이브러리와 통합하여 자동으로 최적의 하이퍼파라미터를 찾아줍니다. 베이지안 최적화로 효율적으로 탐색 공간을 줄여나갑니다.
개요
간단히 말해서, 하이퍼파라미터 튜닝은 모델의 학습률, 배치 크기, 에포크 수, 가중치 감쇠 등의 값을 자동으로 조정하여 최고 성능을 내는 조합을 찾는 과정입니다. 수백 번의 실험을 자동화합니다.
왜 이것이 필요한지 보면, 하이퍼파라미터는 모델 성능에 결정적인 영향을 미치지만, 최적값은 데이터와 모델마다 다릅니다. 예를 들어, BERT 파인튜닝에서 학습률을 2e-5에서 5e-5로 바꾸는 것만으로도 F1-score가 0.85에서 0.92로 향상될 수 있습니다.
하지만 어떤 값이 최적인지는 실험해봐야 압니다. 기존에는 엑셀에 실험 결과를 기록하고, 수동으로 설정을 바꿔가며 수십 번 학습을 돌렸다면, 이제는 Trainer.hyperparameter_search()로 탐색 공간을 정의하고 자동으로 최적값을 찾을 수 있습니다.
Optuna가 스마트하게 다음 시도할 값을 선택하여 효율을 높입니다. 이 방법의 핵심 특징은 첫째, 베이지안 최적화로 무작위 탐색보다 훨씬 빠르게 최적값을 찾는다는 점, 둘째, 병렬로 여러 실험을 동시에 수행할 수 있다는 점, 셋째, 조기 종료(pruning)로 성능이 낮은 실험을 일찍 중단하여 시간을 절약한다는 점입니다.
이러한 특징들이 제한된 리소스로 최고 성능을 얻는 데 필수적입니다.
코드 예제
from transformers import Trainer, TrainingArguments
from transformers.integrations import OptunaConfig
import optuna
# 하이퍼파라미터 탐색 공간 정의
def optuna_hp_space(trial):
return {
"learning_rate": trial.suggest_float("learning_rate", 1e-5, 5e-5, log=True),
"per_device_train_batch_size": trial.suggest_categorical("per_device_train_batch_size", [8, 16, 32]),
"num_train_epochs": trial.suggest_int("num_train_epochs", 2, 5),
"weight_decay": trial.suggest_float("weight_decay", 0.0, 0.1),
}
# 기본 학습 설정
training_args = TrainingArguments(
output_dir="./hp_search",
evaluation_strategy="epoch",
save_strategy="no", # 중간 체크포인트 저장 안 함
logging_dir="./logs",
)
# Trainer 초기화
trainer = Trainer(
model_init=lambda: AutoModelForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2),
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_metrics,
)
# 하이퍼파라미터 탐색 실행 (20번 시도)
best_run = trainer.hyperparameter_search(
direction="maximize", # 메트릭을 최대화
backend="optuna",
hp_space=optuna_hp_space,
n_trials=20,
compute_objective=lambda metrics: metrics["eval_f1"], # F1 스코어 최적화
)
print(f"최적 하이퍼파라미터: {best_run.hyperparameters}")
print(f"최고 F1 스코어: {best_run.objective:.4f}")
설명
이것이 하는 일: 위 코드는 Optuna를 사용하여 하이퍼파라미터의 최적 조합을 자동으로 찾습니다. 20번의 실험을 수행하며, 각 실험마다 다른 하이퍼파라미터 조합을 시도하여 F1 스코어를 최대화합니다.
첫 번째로, optuna_hp_space 함수에서 탐색할 하이퍼파라미터의 범위를 정의합니다. suggest_float는 연속적인 값(학습률, 가중치 감쇠)을, suggest_categorical은 이산적인 선택지(배치 크기)를, suggest_int는 정수 값(에포크 수)을 탐색합니다.
log=True 옵션은 로그 스케일로 탐색하여 작은 값들을 더 세밀하게 시도합니다. TrainingArguments에서 save_strategy="no"로 설정하여 중간 체크포인트를 저장하지 않습니다.
20번의 실험을 하면 체크포인트가 수십 GB가 될 수 있으므로 디스크 공간을 절약하기 위함입니다. 최종 최적 모델만 나중에 다시 학습하면 됩니다.
model_init은 람다 함수로 제공되어, 각 실험마다 새로운 모델 인스턴스를 생성합니다. 이전 실험의 가중치가 다음 실험에 영향을 주지 않도록 하기 위함입니다.
매번 초기화된 BERT 모델에서 시작합니다. hyperparameter_search()가 핵심입니다.
n_trials=20으로 20번의 실험을 수행하며, compute_objective로 최적화할 메트릭을 F1 스코어로 지정합니다. Optuna는 베이지안 최적화를 사용하여 이전 실험 결과를 학습하고, 다음에 시도할 유망한 하이퍼파라미터를 선택합니다.
처음에는 넓게 탐색하다가 점점 좋은 영역에 집중합니다. 내부적으로 각 실험은 전체 학습/검증 사이클을 수행합니다.
검증 F1 스코어를 기록하고, Optuna에 반환합니다. Optuna는 이 값을 기반으로 다음 하이퍼파라미터를 제안합니다.
Pruning 기능도 사용되어, 초반에 성능이 현저히 낮은 실험은 조기 종료하여 시간을 절약합니다. 여러분이 이 방법을 사용하면 하이퍼파라미터 튜닝이 훨씬 체계적이고 효율적이 됩니다.
첫째, 수동 실험보다 3-5배 빠르게 최적값을 찾을 수 있습니다. 둘째, 사람의 편향 없이 객관적으로 탐색합니다.
셋째, 실험 이력이 자동으로 기록되어 나중에 분석할 수 있습니다. 넷째, 최적 하이퍼파라미터를 찾은 후 해당 설정으로 다시 학습하면 일관되게 높은 성능을 얻을 수 있습니다.
실전 팁
💡 n_trials는 리소스에 따라 조정하세요. GPU가 충분하면 50-100번도 가능하지만, 제한적이면 10-20번으로도 충분한 개선을 얻을 수 있습니다
💡 탐색 범위를 너무 넓게 설정하면 비효율적입니다. 논문이나 가이드에서 권장하는 범위를 참고하여 좁은 범위에서 시작하세요
💡 pruning을 활성화하면 (OptunaConfig에서 설정) 명백히 성능이 낮은 실험을 조기 종료하여 전체 탐색 시간을 30-50% 단축할 수 있습니다
💡 여러 GPU가 있다면 n_jobs 파라미터로 병렬 실험을 수행하여 속도를 크게 높일 수 있습니다
💡 최적 하이퍼파라미터를 찾은 후, 해당 설정으로 여러 번(3-5회) 학습하여 평균 성능을 확인하세요. 단일 실험은 운에 좌우될 수 있습니다
7. 모델 저장과 로드 - 재사용과 배포 준비
시작하며
여러분이 며칠 동안 학습시킨 모델을 사용하려고 할 때 이런 당황스러운 상황을 겪어본 적 있나요? "학습은 잘 됐는데, 어떻게 저장하지?
나중에 어떻게 불러오지?" 또는 "이 모델을 API 서버에 배포하려면 어떤 파일이 필요하지?" 모델을 저장하고 로드하는 것은 학습만큼이나 중요합니다. 학습된 모델을 제대로 저장하지 않으면 모든 노력이 물거품이 됩니다.
단순히 가중치만 저장하면 나중에 불러올 때 토크나이저 설정이나 모델 구조를 다시 맞춰야 하는 번거로움이 있습니다. 프로덕션 환경에서는 버전 관리도 중요합니다.
바로 이럴 때 필요한 것이 Hugging Face의 save_pretrained()와 from_pretrained() 메서드입니다. 모델, 토크나이저, 설정 파일을 한 번에 저장하고 불러올 수 있으며, Hugging Face Hub에 업로드하여 팀과 공유하거나 배포할 수도 있습니다.
개요
간단히 말해서, 모델 저장은 학습된 가중치뿐만 아니라 토크나이저, 설정(config), 어휘(vocabulary)를 모두 포함하여 나중에 정확히 같은 상태로 복원할 수 있게 하는 것입니다. 완전한 재현성이 핵심입니다.
왜 이것이 중요한지 보면, 실무에서는 학습과 배포가 분리되어 있습니다. 강력한 GPU 서버에서 학습한 후, 모델을 저장하여 API 서버나 모바일 기기로 이동해야 합니다.
예를 들어, 고객 센터의 챗봇 모델을 학습한 후, 실제 서비스 서버에 배포하려면 모든 컴포넌트를 정확히 저장하고 전송해야 합니다. 기존에는 PyTorch의 torch.save()로 state_dict만 저장하고, 토크나이저와 설정은 별도로 관리했다면, 이제는 model.save_pretrained()와 tokenizer.save_pretrained()로 모든 것을 하나의 디렉토리에 저장할 수 있습니다.
나중에 from_pretrained()로 한 줄에 모두 불러옵니다. 이 방법의 핵심 특징은 첫째, 모델과 토크나이저의 모든 상태를 완전히 저장한다는 점, 둘째, Hugging Face Hub와 통합되어 클라우드 저장과 버전 관리가 쉽다는 점, 셋째, 다른 프레임워크(TensorFlow, ONNX)로 변환도 가능하다는 점입니다.
이러한 특징들이 모델의 생명 주기를 효과적으로 관리하게 해줍니다.
코드 예제
from transformers import AutoModelForSequenceClassification, AutoTokenizer
# 학습된 모델과 토크나이저를 로컬에 저장
output_dir = "./my_finetuned_model"
model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"모델과 토크나이저가 {output_dir}에 저장되었습니다.")
# 저장된 모델 불러오기 (나중에 또는 다른 스크립트에서)
loaded_model = AutoModelForSequenceClassification.from_pretrained(output_dir)
loaded_tokenizer = AutoTokenizer.from_pretrained(output_dir)
# 불러온 모델로 즉시 추론 가능
text = "이 제품 정말 마음에 들어요!"
inputs = loaded_tokenizer(text, return_tensors="pt")
outputs = loaded_model(**inputs)
prediction = outputs.logits.argmax(-1).item()
print(f"예측 결과: {'긍정' if prediction == 1 else '부정'}")
# Hugging Face Hub에 업로드 (선택 사항)
# model.push_to_hub("my-username/my-sentiment-model")
# tokenizer.push_to_hub("my-username/my-sentiment-model")
# Hub에서 직접 불러오기
# model_from_hub = AutoModelForSequenceClassification.from_pretrained("my-username/my-sentiment-model")
설명
이것이 하는 일: 위 코드는 파인튜닝된 모델과 토크나이저를 로컬 디렉토리에 저장하고, 나중에 정확히 같은 상태로 불러옵니다. 저장된 모델은 즉시 추론에 사용하거나 클라우드에 업로드할 수 있습니다.
첫 번째로, save_pretrained(output_dir)는 지정된 디렉토리에 여러 파일을 생성합니다. pytorch_model.bin(모델 가중치), config.json(모델 구조 설정), vocab.txt(어휘), tokenizer_config.json(토크나이저 설정) 등이 포함됩니다.
이 파일들이 모여서 완전한 모델을 구성합니다. 단순히 가중치만 저장하는 것이 아니라 재현에 필요한 모든 정보를 저장합니다.
그 다음으로, from_pretrained(output_dir)로 저장된 디렉토리를 지정하면 자동으로 모든 파일을 읽어 모델을 재구성합니다. 내부적으로는 config.json을 먼저 읽어 모델 구조를 파악하고, 그에 맞는 모델 클래스를 초기화한 후, pytorch_model.bin에서 가중치를 로드합니다.
토크나이저도 마찬가지로 설정과 어휘를 함께 로드하여 정확히 학습 시와 동일한 토크나이징을 수행합니다. 불러온 모델은 즉시 사용 가능합니다.
새로운 텍스트를 입력하면 학습 시와 동일한 방식으로 토크나이징하고, 모델이 예측을 수행합니다. 이 과정에서 별도의 설정이나 초기화가 필요 없습니다.
모든 것이 저장된 파일에서 자동으로 복원되기 때문입니다. Hugging Face Hub에 업로드하면 더 강력한 기능을 사용할 수 있습니다.
push_to_hub()는 Git과 유사한 방식으로 모델을 버전 관리합니다. 각 업로드마다 커밋 해시가 생성되어, 나중에 특정 버전을 불러올 수 있습니다.
팀원들은 from_pretrained("my-username/model-name")로 클라우드에서 직접 모델을 다운로드하여 사용할 수 있습니다. 별도의 파일 전송이나 스토리지 설정이 필요 없습니다.
Hub에 업로드된 모델은 웹 UI에서 직접 테스트할 수 있는 Inference API도 제공됩니다. 사용자들이 브라우저에서 텍스트를 입력하고 결과를 바로 확인할 수 있어, 데모나 프로토타입 공유에 매우 유용합니다.
여러분이 이 방법을 사용하면 모델 관리가 훨씬 편리해집니다. 첫째, 학습과 배포를 완전히 분리할 수 있어 워크플로우가 깔끔해집니다.
둘째, 버전별로 모델을 관리하여 이전 버전으로 롤백하거나 성능을 비교할 수 있습니다. 셋째, 팀과 쉽게 공유하여 협업이 원활해집니다.
넷째, 프로덕션 배포 시 필요한 모든 파일이 한 곳에 있어 실수를 방지할 수 있습니다.
실전 팁
💡 저장 전에 디렉토리가 이미 존재하는지 확인하고, 중요한 모델은 타임스탬프를 디렉토리명에 포함하세요: ./models/sentiment_20250109_v1
💡 model.save_pretrained(output_dir, safe_serialization=True)를 사용하면 더 안전한 safetensors 포맷으로 저장됩니다
💡 Hub에 업로드할 때 private=True 옵션으로 비공개 리포지토리를 만들어 회사 내부에서만 사용할 수 있습니다
💡 큰 모델은 from_pretrained(output_dir, device_map="auto")로 로드하면 자동으로 여러 GPU에 분산 배치됩니다
💡 ONNX나 TensorFlow로 변환이 필요하면 model.save_pretrained(output_dir, export=True) 옵션을 사용하세요
8. 배치 추론 최적화 - 대량 데이터 효율적 처리
시작하며
여러분이 수만 건의 고객 리뷰를 분석해야 할 때 이런 고민을 해본 적 있나요? "한 번에 하나씩 처리하면 너무 오래 걸리는데, 어떻게 빠르게 하지?" 또는 "배치로 처리하면 메모리가 부족하다는 에러가 나는데?" 대량 추론은 모델 배포 후 가장 흔한 성능 병목입니다.
실시간 서비스에서는 사용자 요청을 하나씩 처리하지만, 배치 분석에서는 수십만 건을 한꺼번에 처리해야 합니다. 예를 들어, 어제 등록된 모든 상품 리뷰의 감정을 분석하거나, 고객 문의 수천 건을 자동 분류해야 하는 경우입니다.
순차 처리하면 몇 시간씩 걸립니다. 바로 이럴 때 필요한 것이 효율적인 배치 추론입니다.
DataLoader와 배치 처리를 적절히 사용하고, GPU를 최대한 활용하며, 메모리 관리를 최적화하면 처리 속도를 10-100배 높일 수 있습니다.
개요
간단히 말해서, 배치 추론은 여러 샘플을 하나의 배치로 묶어서 동시에 처리하는 것입니다. GPU의 병렬 처리 능력을 최대한 활용하여 처리량을 극대화합니다.
왜 이것이 필요한지 보면, GPU는 하나의 샘플을 처리할 때 계산 유닛의 일부만 사용합니다. 예를 들어, 단일 텍스트를 분류하면 GPU 사용률이 10%에 불과할 수 있습니다.
하지만 32개를 배치로 묶으면 GPU 사용률이 90%로 올라가고, 처리량은 20배 이상 증가합니다. 같은 시간에 훨씬 많은 작업을 처리할 수 있습니다.
기존에는 for 루프로 하나씩 모델에 입력했다면, 이제는 DataLoader로 자동으로 배치를 생성하고, 동적 패딩으로 메모리를 절약하며, 멀티프로세싱으로 데이터 로딩 속도를 높일 수 있습니다. Hugging Face의 Pipeline API를 사용하면 이 모든 최적화가 자동으로 적용됩니다.
이 방법의 핵심 특징은 첫째, 배치 크기를 조정하여 GPU 메모리와 처리 속도의 균형을 맞출 수 있다는 점, 둘째, 동적 패딩으로 불필요한 계산을 줄인다는 점, 셋째, tqdm으로 진행 상황을 모니터링할 수 있다는 점입니다. 이러한 특징들이 대규모 데이터 처리를 실용적으로 만듭니다.
코드 예제
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
from torch.utils.data import DataLoader
import torch
# 파이프라인 방식 (가장 간단, 자동 최적화)
classifier = pipeline(
"text-classification",
model="./my_finetuned_model",
device=0, # GPU 0 사용, -1은 CPU
batch_size=32 # 배치 크기
)
# 대량 텍스트 준비
texts = ["리뷰 내용 1", "리뷰 내용 2", ...] * 1000 # 예시: 수천 개
# 배치 추론 수행 (자동으로 배치 처리)
results = classifier(texts, truncation=True, max_length=128)
print(f"{len(results)}개 처리 완료")
# 수동 배치 처리 (더 세밀한 제어)
model = AutoModelForSequenceClassification.from_pretrained("./my_finetuned_model")
tokenizer = AutoTokenizer.from_pretrained("./my_finetuned_model")
model.to("cuda") # GPU로 이동
model.eval() # 평가 모드
# DataLoader로 배치 생성
def collate_fn(batch):
return tokenizer(batch, padding=True, truncation=True, return_tensors="pt", max_length=128)
dataloader = DataLoader(texts, batch_size=32, collate_fn=collate_fn, num_workers=4)
# 배치별 추론
all_predictions = []
with torch.no_grad(): # 그래디언트 계산 비활성화
for batch in dataloader:
batch = {k: v.to("cuda") for k, v in batch.items()} # GPU로 이동
outputs = model(**batch)
preds = outputs.logits.argmax(-1).cpu().numpy()
all_predictions.extend(preds)
print(f"총 {len(all_predictions)}개 예측 완료")
설명
이것이 하는 일: 위 코드는 수천 개의 텍스트를 효율적으로 배치 처리하여 감정을 분류합니다. Pipeline을 사용한 간단한 방법과 DataLoader를 사용한 세밀한 제어 방법 두 가지를 보여줍니다.
첫 번째로, Pipeline API는 가장 간단한 방법입니다. batch_size=32로 설정하면 내부적으로 텍스트를 32개씩 묶어서 처리합니다.
device=0은 첫 번째 GPU를 사용하라는 의미입니다. Pipeline은 자동으로 토크나이징, 배치 생성, 모델 추론, 후처리를 수행합니다.
texts 리스트를 그대로 전달하면 알아서 배치로 나누고 결과를 모아서 반환합니다. Pipeline의 내부 동작을 보면, 먼저 전체 텍스트를 배치 크기만큼씩 나눕니다.
각 배치마다 토크나이징을 수행하는데, padding=True로 배치 내에서 가장 긴 시퀀스에 맞춰 패딩합니다(동적 패딩). 이렇게 하면 모든 배치를 512토큰으로 패딩하는 것보다 메모리와 계산량을 크게 줄일 수 있습니다.
그 후 GPU로 데이터를 전송하고, 모델 추론을 수행하며, 결과를 CPU로 다시 가져옵니다. 수동 방식은 더 세밀한 제어가 필요할 때 사용합니다.
collate_fn은 DataLoader가 배치를 생성할 때 호출되는 함수로, 텍스트 리스트를 받아 토큰화하고 텐서로 변환합니다. padding=True는 배치 내에서만 패딩하므로 효율적입니다.
num_workers=4는 4개의 CPU 프로세스로 데이터 로딩을 병렬화하여, GPU가 계산하는 동안 다음 배치를 미리 준비합니다. torch.no_grad() 컨텍스트는 매우 중요합니다.
추론 시에는 그래디언트 계산이 필요 없으므로, 이를 비활성화하면 메모리 사용량이 절반으로 줄고 속도도 빨라집니다. 각 배치를 GPU로 이동시키고 모델에 입력하면, 출력 logits에서 argmax로 예측 클래스를 선택합니다.
.cpu().numpy()로 텐서를 넘파이 배열로 변환하여 결과를 수집합니다. 배치 크기 선택은 GPU 메모리에 따라 다릅니다.
V100 16GB GPU에서는 BERT 기준 배치 크기 32-64가 적당하고, T4 16GB에서는 16-32가 적당합니다. 메모리 부족 에러가 나면 배치 크기를 줄이세요.
반대로 GPU 사용률이 낮다면 배치 크기를 늘려서 처리량을 높일 수 있습니다. 여러분이 이 방법을 사용하면 대량 데이터 처리가 극적으로 빨라집니다.
첫째, 단일 샘플 처리 대비 10-50배 빠른 처리 속도를 얻을 수 있습니다. 둘째, GPU를 효율적으로 활용하여 하드웨어 투자 대비 성능을 최대화합니다.
셋째, 진행 상황을 모니터링하여 완료 시간을 예측할 수 있습니다. 넷째, 메모리 효율적인 처리로 더 큰 데이터셋도 처리할 수 있습니다.
실전 팁
💡 배치 크기는 2의 거듭제곱(8, 16, 32, 64)으로 설정하면 GPU 메모리 정렬이 최적화되어 약간 더 빠릅니다
💡 torch.cuda.empty_cache()를 주기적으로 호출하여 미사용 GPU 메모리를 해제하면 메모리 부족 에러를 예방할 수 있습니다
💡 매우 긴 텍스트가 섞여 있다면 길이별로 정렬한 후 배치를 만들면 패딩을 최소화하여 속도를 20-30% 높일 수 있습니다
💡 DataLoader의 pin_memory=True 옵션을 사용하면 CPU-GPU 데이터 전송 속도가 빨라집니다
💡 tqdm 라이브러리로 for batch in tqdm(dataloader): 형태로 감싸면 실시간 진행률과 예상 완료 시간을 확인할 수 있습니다
9. 다중 레이블 분류 - 여러 카테고리 동시 예측
시작하며
여러분이 뉴스 기사를 분류하는 시스템을 만들 때 이런 문제를 겪어본 적 있나요? "이 기사는 '정치'이면서 동시에 '경제'에도 속하는데, 어떻게 처리하지?" 지금까지 다룬 분류는 하나의 레이블만 예측하는 단일 레이블 분류였습니다.
하지만 실무에서는 하나의 샘플이 여러 카테고리에 속하는 경우가 많습니다. 예를 들어, 영화 리뷰는 "액션", "코미디", "로맨스"를 모두 포함할 수 있고, 상품은 "할인", "인기", "신제품" 태그를 동시에 가질 수 있습니다.
이런 경우 단일 레이블 분류로는 정보를 제대로 표현할 수 없습니다. 바로 이럴 때 필요한 것이 다중 레이블 분류(Multi-Label Classification)입니다.
출력층을 sigmoid로 바꾸고, 손실 함수를 Binary Cross Entropy로 변경하여, 각 레이블을 독립적으로 예측할 수 있게 합니다.
개요
간단히 말해서, 다중 레이블 분류는 하나의 입력에 대해 여러 레이블을 동시에 예측하는 것입니다. 각 레이블은 독립적으로 "참" 또는 "거짓"으로 판단됩니다.
왜 이것이 필요한지 보면, 실제 세계의 많은 문제가 다중 레이블 특성을 가지기 때문입니다. 예를 들어, 고객 문의 분류 시스템에서 하나의 문의가 "환불", "배송", "불만" 세 가지 카테고리에 모두 해당할 수 있습니다.
단일 레이블로 강제하면 중요한 정보를 잃게 됩니다. 기존 단일 레이블 분류에서는 softmax와 CrossEntropyLoss를 사용했다면, 이제는 sigmoid와 BCEWithLogitsLoss를 사용합니다.
Softmax는 모든 클래스의 확률 합이 1이 되도록 강제하지만, sigmoid는 각 클래스를 독립적으로 0-1 사이 확률로 변환합니다. 이 방법의 핵심 특징은 첫째, 각 레이블이 독립적으로 예측되어 여러 레이블이 동시에 활성화될 수 있다는 점, 둘째, 임계값(threshold)을 조정하여 정밀도와 재현율의 균형을 조절할 수 있다는 점, 셋째, 불균형한 레이블 분포를 가중치로 처리할 수 있다는 점입니다.
이러한 특징들이 복잡한 실무 문제를 정확히 모델링하게 해줍니다.
코드 예제
from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments
import torch
import torch.nn as nn
# 다중 레이블용 데이터 준비 (레이블이 리스트 형태)
# 예: [1, 0, 1, 0] -> "액션"과 "로맨스"는 True, 나머지 False
train_data = [
{"text": "재밌고 감동적인 영화", "labels": [1.0, 1.0, 0.0, 0.0]}, # 액션, 코미디
{"text": "슬프지만 아름다운 이야기", "labels": [0.0, 0.0, 1.0, 1.0]}, # 로맨스, 드라마
]
# 모델 로드 (num_labels = 레이블 개수)
num_labels = 4 # 액션, 코미디, 로맨스, 드라마
model = AutoModelForSequenceClassification.from_pretrained(
"bert-base-uncased",
num_labels=num_labels,
problem_type="multi_label_classification" # 중요!
)
# 커스텀 손실 함수 (자동으로 BCEWithLogitsLoss 사용됨)
# Trainer가 problem_type을 인식하여 자동 적용
training_args = TrainingArguments(
output_dir="./multi_label_model",
num_train_epochs=3,
per_device_train_batch_size=16,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
)
trainer.train()
# 추론 시 임계값 적용
def predict_multi_label(text, threshold=0.5):
inputs = tokenizer(text, return_tensors="pt")
outputs = model(**inputs)
probs = torch.sigmoid(outputs.logits) # Sigmoid로 확률 변환
predictions = (probs > threshold).int() # 임계값 적용
label_names = ["액션", "코미디", "로맨스", "드라마"]
result = [label_names[i] for i, pred in enumerate(predictions[0]) if pred == 1]
return result
print(predict_multi_label("웃기고 감동적인 영화"))
# 출력 예: ['코미디', '드라마']
설명
이것이 하는 일: 위 코드는 BERT 모델을 다중 레이블 분류로 파인튜닝하여 하나의 텍스트에 여러 장르를 동시에 예측합니다. 각 장르는 독립적으로 "해당함/해당 안 함"으로 판단됩니다.
첫 번째로, 데이터 형식이 단일 레이블과 다릅니다. 레이블이 단일 정수가 아니라 부동소수점 리스트입니다.
[1.0, 1.0, 0.0, 0.0]은 첫 두 레이블(액션, 코미디)은 참이고 나머지는 거짓임을 의미합니다. 부동소수점을 사용하는 이유는 BCE Loss가 실수 입력을 받기 때문입니다.
모델 초기화 시 problem_type="multi_label_classification"을 명시하는 것이 핵심입니다. 이 파라미터가 설정되면 Hugging Face Trainer는 자동으로 BCEWithLogitsLoss를 손실 함수로 사용합니다.
BCEWithLogitsLoss는 로짓에 sigmoid를 적용하고 Binary Cross Entropy를 계산하는 과정을 하나로 합쳐 수치적으로 안정적입니다. 단일 레이블 분류와의 차이를 보면, 단일 레이블은 softmax를 사용하여 [0.7, 0.2, 0.1] 같이 합이 1인 확률 분포를 만듭니다.
반면 다중 레이블은 sigmoid를 각 로짓에 독립적으로 적용하여 [0.8, 0.9, 0.2, 0.1] 같이 각각 독립적인 확률을 만듭니다. 합이 1일 필요가 없습니다.
추론 시 torch.sigmoid(outputs.logits)로 각 레이블의 확률을 계산합니다. 그 후 임계값(보통 0.5)을 적용하여 최종 예측을 결정합니다.
probs > threshold는 확률이 0.5 이상인 레이블만 1로, 나머지는 0으로 만듭니다. 이 임계값을 조정하여 성능을 튜닝할 수 있습니다.
임계값을 높이면(0.7) 더 확신할 때만 레이블을 부여하여 precision이 높아지고, 낮추면(0.3) 더 많은 레이블을 부여하여 recall이 높아집니다. 실무에서는 임계값을 레이블마다 다르게 설정하기도 합니다.
예를 들어, "긴급" 레이블은 중요하므로 임계값을 0.3으로 낮춰 놓치지 않도록 하고, "기타" 레이블은 임계값을 0.7로 높여 정확할 때만 부여할 수 있습니다. 여러분이 이 방법을 사용하면 복잡한 실무 문제를 더 정확히 모델링할 수 있습니다.
첫째, 하나의 샘플이 여러 특성을 가지는 현실을 제대로 반영할 수 있습니다. 둘째, 각 레이블의 확률을 독립적으로 얻어 비즈니스 로직에 활용할 수 있습니다.
셋째, 임계값 조정으로 운영 환경에 맞게 성능을 튜닝할 수 있습니다. 넷째, 새로운 레이블을 추가할 때 다른 레이블에 영향을 주지 않습니다.
실전 팁
💡 레이블 불균형이 심하면 BCEWithLogitsLoss에 pos_weight 파라미터를 설정하여 소수 레이블에 더 큰 가중치를 부여하세요
💡 검증 세트에서 각 레이블별로 최적 임계값을 찾아 사용하면 (예: precision-recall 곡선 활용) F1 스코어를 5-10% 향상시킬 수 있습니다
💡 다중 레이블 평가 시 hamming_loss, jaccard_score 같은 다중 레이블 전용 메트릭을 사용하세요
💡 레이블 간 상관관계가 강하다면 (예: "할인"과 "세일"은 거의 함께 나타남) 계층적 모델이나 조건부 확률 모델을 고려하세요
💡 많은 레이블(수십~수백 개)을 다룰 때는 label attention 메커니즘을 추가하면 성능이 향상될 수 있습니다
10. 모델 경량화와 양자화 - 모바일과 엣지 배포
시작하며
여러분이 학습한 모델을 모바일 앱이나 임베디드 기기에 배포하려고 할 때 이런 문제를 겪어본 적 있나요? "BERT 모델이 400MB인데, 모바일에 어떻게 넣지?" 또는 "추론 속도가 너무 느려서 실시간 서비스가 불가능한데?" 대형 모델은 서버에서는 잘 작동하지만 리소스가 제한된 환경에서는 사용하기 어렵습니다.
특히 모바일 기기나 IoT 디바이스는 메모리와 배터리가 제한적입니다. 사용자는 앱이 수백 MB를 차지하는 것을 원하지 않고, 배터리가 빨리 소모되는 것도 싫어합니다.
또한 추론 시간이 1초 이상이면 사용자 경험이 나빠집니다. 바로 이럴 때 필요한 것이 모델 경량화와 양자화입니다.
지식 증류(Knowledge Distillation)로 작은 모델을 학습시키거나, 양자화(Quantization)로 가중치를 16비트나 8비트로 압축하여 크기와 속도를 개선할 수 있습니다.
개요
간단히 말해서, 모델 경량화는 모델의 성능을 크게 유지하면서 크기와 추론 시간을 줄이는 기술입니다. 양자화, 증류, 가지치기(Pruning) 등 다양한 방법이 있습니다.
왜 이것이 필요한지 보면, 배포 환경의 제약 때문입니다. 예를 들어, 스마트폰에서 실시간 번역 서비스를 제공하려면 모델이 100MB 이하여야 하고, 응답 시간이 200ms 이하여야 합니다.
BERT-base(400MB, 500ms)로는 불가능하지만, DistilBERT(200MB, 200ms)로는 가능합니다. 기존 32비트 부동소수점 모델을 그대로 사용했다면, 이제는 8비트 정수(INT8) 양자화로 크기를 1/4로 줄이고 속도를 2-3배 높일 수 있습니다.
정확도 손실은 보통 1-2% 이내로 무시할 만합니다. Hugging Face는 ONNX Runtime과 통합하여 이런 최적화를 쉽게 적용할 수 있습니다.
이 방법의 핵심 특징은 첫째, 동적 양자화로 코드 한 줄만 추가하여 즉시 2배 빠르게 만들 수 있다는 점, 둘째, 지식 증류로 큰 모델의 성능을 작은 모델에 전이할 수 있다는 점, 셋째, ONNX로 변환하여 다양한 플랫폼에서 최적화된 추론을 수행할 수 있다는 점입니다. 이러한 특징들이 AI를 엣지 디바이스로 가져오는 것을 가능하게 합니다.
코드 예제
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
from torch.quantization import quantize_dynamic
# 원본 모델 로드
model = AutoModelForSequenceClassification.from_pretrained("./my_finetuned_model")
tokenizer = AutoTokenizer.from_pretrained("./my_finetuned_model")
# 동적 양자화 (가장 간단, 즉시 적용 가능)
quantized_model = quantize_dynamic(
model,
{torch.nn.Linear}, # Linear 레이어만 양자화
dtype=torch.qint8 # 8비트 정수로 변환
)
# 모델 크기 비교
import os
torch.save(model.state_dict(), "model_fp32.pth")
torch.save(quantized_model.state_dict(), "model_int8.pth")
original_size = os.path.getsize("model_fp32.pth") / (1024 * 1024)
quantized_size = os.path.getsize("model_int8.pth") / (1024 * 1024)
print(f"원본 크기: {original_size:.2f} MB")
print(f"양자화 크기: {quantized_size:.2f} MB")
print(f"압축률: {original_size / quantized_size:.2f}x")
# 추론 속도 비교
import time
text = "이 제품 정말 좋아요!"
inputs = tokenizer(text, return_tensors="pt")
# 원본 모델
start = time.time()
with torch.no_grad():
_ = model(**inputs)
original_time = time.time() - start
# 양자화 모델
start = time.time()
with torch.no_grad():
_ = quantized_model(**inputs)
quantized_time = time.time() - start
print(f"원본 추론 시간: {original_time*1000:.2f} ms")
print(f"양자화 추론 시간: {quantized_time*1000:.2f} ms")
print(f"속도 향상: {original_time / quantized_time:.2f}x")
설명
이것이 하는 일: 위 코드는 PyTorch의 동적 양자화를 사용하여 BERT 모델의 가중치를 32비트 부동소수점에서 8비트 정수로 변환합니다. 모델 크기와 추론 시간을 측정하여 개선 효과를 정량화합니다.
첫 번째로, quantize_dynamic()은 가장 간단한 양자화 방법입니다. torch.nn.Linear 레이어의 가중치를 INT8로 변환합니다.
"동적"이라는 이름은 활성화(activations)는 런타임에 동적으로 양자화한다는 의미입니다. 가중치는 사전에 변환되지만, 실제 계산 시 활성화는 그때그때 변환됩니다.
이 방식은 정확도 손실이 가장 적습니다. 양자화의 원리를 보면, 32비트 부동소수점 숫자(-3.14159...)를 8비트 정수(-128~127)로 매핑합니다.
예를 들어, 가중치 범위가 -1.0~1.0이라면, -1.0을 -128로, 1.0을 127로 매핑하고, 중간 값들은 선형 보간합니다. 정밀도는 떨어지지만, 대부분의 가중치는 0 근처에 분포하므로 실제 정확도 손실은 미미합니다.
모델 크기를 비교하면 극적인 차이를 볼 수 있습니다. BERT-base는 원래 약 440MB인데, 양자화 후 약 110MB가 됩니다.
정확히 1/4입니다. 이는 각 가중치가 32비트에서 8비트로 줄어들었기 때문입니다.
모바일 앱에서는 이 차이가 매우 중요합니다. 사용자는 400MB 다운로드를 주저하지만 100MB는 받아들일 수 있습니다.
추론 속도는 하드웨어에 따라 다르지만, 보통 CPU에서 2-3배 빨라집니다. Intel이나 ARM CPU는 INT8 연산을 위한 특별한 명령어를 지원하여, 32비트 부동소수점 연산보다 훨씬 빠릅니다.
GPU에서는 속도 향상이 적지만(1.2-1.5배), CPU 추론이 주된 모바일/엣지 환경에서는 큰 이득입니다. 정적 양자화(Static Quantization)를 사용하면 더 큰 압축을 얻을 수 있습니다.
이는 활성화도 미리 양자화하는 방식으로, 대표 데이터셋으로 calibration을 수행해야 합니다. torch.quantization.quantize 함수를 사용하며, 속도는 더 빠르지만 구현이 복잡하고 정확도 손실이 약간 더 클 수 있습니다.
ONNX Runtime을 사용하면 더 강력한 최적화를 얻을 수 있습니다. Hugging Face의 optimum 라이브러리로 모델을 ONNX로 변환하면, 그래프 최적화, 연산자 융합, 양자화가 모두 자동으로 적용됩니다.
CPU에서는 4-6배, GPU에서는 2-3배 속도 향상을 기대할 수 있습니다. 여러분이 이 기술을 사용하면 모델을 더 많은 환경에 배포할 수 있습니다.
첫째, 모바일 앱에 내장하여 오프라인에서도 작동하게 할 수 있습니다. 둘째, 라즈베리파이 같은 저전력 기기에서 실시간 추론이 가능해집니다.
셋째, 클라우드 비용을 절감할 수 있습니다. 같은 서버에서 2배 많은 요청을 처리할 수 있으니까요.
넷째, 사용자 경험이 개선됩니다. 응답 시간이 빨라지고 배터리 소모가 줄어듭니다.
실전 팁
💡 양자화 후 반드시 정확도를 검증하세요. 일부 모델은 양자화에 민감하여 성능이 크게 떨어질 수 있습니다
💡 DistilBERT 같은 증류된 모델을 사용하면 양자화와 결합하여 원본 BERT 대비 10배 빠르면서 정확도는 5% 이내 손실로 유지할 수 있습니다
💡 모바일 배포 시 ONNX Runtime Mobile이나 TensorFlow Lite로 변환하면 플랫폼 특화 최적화를 추가로 얻을 수 있습니다
💡 Quantization-Aware Training (QAT)을 사용하면 양자화 오류를 학습 과정에서 보상하여 정적 양자화보다 높은 정확도를 유지할 수 있습니다
💡 추론 전용이라면 torch.jit.script로 JIT 컴파일하여 양자화와 결합하면 추가로 20-30% 속도 향상을 얻을 수 있습니다