이미지 로딩 중...
AI Generated
2025. 11. 23. · 0 Views
PyTorch Dataset과 DataLoader 완벽 가이드
딥러닝 모델을 학습시킬 때 데이터를 효율적으로 다루는 방법을 배웁니다. PyTorch의 Dataset과 DataLoader를 사용하여 대용량 데이터를 메모리 효율적으로 처리하고, 배치 처리와 셔플링을 자동화하는 방법을 실무 예제와 함께 알아봅니다.
목차
- Dataset 클래스 기본 이해
- DataLoader로 배치 처리하기
- 커스텀 전처리와 변환
- 대용량 데이터 효율적으로 다루기
- 불균형 데이터셋 처리하기
- 멀티모달 데이터 처리하기
- 동적 배치 크기와 커스텀 Collate 함수
- 데이터 증강과 온라인 증강
1. Dataset 클래스 기본 이해
시작하며
여러분이 딥러닝 모델을 학습시킬 때 이런 상황을 겪어본 적 있나요? 수만 장의 이미지나 수백만 개의 텍스트 데이터를 한 번에 메모리에 올리려다가 컴퓨터가 느려지거나 심지어 멈춰버린 적이 있으신가요?
이런 문제는 실제 개발 현장에서 자주 발생합니다. 모든 데이터를 한꺼번에 메모리에 올리면 메모리 부족 에러가 발생하고, 그렇다고 데이터를 하나씩만 처리하면 학습 속도가 너무 느려집니다.
특히 대용량 데이터를 다룰 때는 이 문제가 더욱 심각해집니다. 바로 이럴 때 필요한 것이 PyTorch의 Dataset 클래스입니다.
Dataset은 마치 도서관 사서처럼, 필요한 데이터만 그때그때 찾아서 가져다주는 역할을 합니다. 전체 책을 다 들고 오는 게 아니라, 필요한 책만 찾아주는 거죠.
개요
간단히 말해서, Dataset 클래스는 여러분의 데이터를 PyTorch가 이해할 수 있는 형태로 포장해주는 도구입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 딥러닝 학습에는 데이터 전처리, 증강, 인덱싱 등 다양한 작업이 필요합니다.
Dataset 클래스를 사용하면 이러한 모든 작업을 하나의 클래스 안에 깔끔하게 정리할 수 있습니다. 예를 들어, 10만 장의 고양이 사진으로 분류 모델을 학습시킨다면, Dataset 클래스 안에서 이미지를 읽고, 리사이즈하고, 텐서로 변환하는 모든 과정을 관리할 수 있습니다.
기존에는 데이터를 리스트나 넘파이 배열로 관리하면서 직접 인덱싱하고 전처리 함수를 따로 만들어야 했다면, 이제는 Dataset 클래스 하나로 모든 것을 체계적으로 관리할 수 있습니다. Dataset 클래스의 핵심 특징은 세 가지입니다.
첫째, __len__ 메서드로 전체 데이터 개수를 알려줍니다. 둘째, __getitem__ 메서드로 특정 인덱스의 데이터를 가져옵니다.
셋째, 데이터를 실제로 사용할 때까지 메모리에 올리지 않는 지연 로딩(lazy loading) 방식을 사용합니다. 이러한 특징들이 대용량 데이터를 효율적으로 다룰 수 있게 만들어줍니다.
코드 예제
import torch
from torch.utils.data import Dataset
import pandas as pd
# 커스텀 Dataset 클래스 정의
class CustomDataset(Dataset):
def __init__(self, csv_file, transform=None):
# CSV 파일에서 데이터 정보만 읽어옴 (실제 데이터는 아직 로드하지 않음)
self.data_info = pd.read_csv(csv_file)
self.transform = transform
def __len__(self):
# 전체 데이터의 개수를 반환
return len(self.data_info)
def __getitem__(self, idx):
# 특정 인덱스의 데이터를 실제로 로드하여 반환
image_path = self.data_info.iloc[idx, 0]
label = self.data_info.iloc[idx, 1]
# 이미지를 읽고 전처리 적용
image = self._load_image(image_path)
if self.transform:
image = self.transform(image)
return image, label
설명
이것이 하는 일: Dataset 클래스는 여러분의 원본 데이터(CSV, 이미지 파일, 텍스트 등)를 PyTorch 모델이 학습할 수 있는 형태로 변환하고, 필요할 때마다 하나씩 꺼내 쓸 수 있게 만들어줍니다. 첫 번째로, __init__ 메서드는 Dataset 객체가 생성될 때 실행됩니다.
여기서 중요한 점은 실제 데이터를 모두 메모리에 올리는 게 아니라, 데이터의 "목록"만 읽어온다는 것입니다. 마치 도서관에서 책 목록만 먼저 확인하는 것과 같습니다.
CSV 파일을 읽어서 어떤 이미지가 어디에 있고, 라벨이 무엇인지 정보만 저장해둡니다. 그 다음으로, __len__ 메서드가 실행되면 전체 데이터의 개수를 반환합니다.
이것은 나중에 DataLoader가 전체 데이터를 몇 개의 배치로 나눌지 계산할 때 사용됩니다. 예를 들어 전체 데이터가 1000개이고 배치 크기가 32라면, 약 32개의 배치가 필요하다는 것을 알 수 있습니다.
__getitem__ 메서드는 가장 핵심적인 부분입니다. 특정 인덱스(예: 0, 1, 2...)를 받으면, 그 인덱스에 해당하는 데이터를 실제로 로드합니다.
이때 비로소 이미지 파일을 읽고, 필요한 전처리(리사이즈, 정규화 등)를 수행하고, 텐서로 변환합니다. 이렇게 필요할 때만 데이터를 로드하는 방식을 지연 로딩이라고 합니다.
여러분이 이 코드를 사용하면 메모리를 효율적으로 관리하면서도 깔끔한 코드를 작성할 수 있습니다. 데이터 로딩 로직이 한 곳에 모여 있어 유지보수가 쉽고, 전처리 파이프라인을 쉽게 수정할 수 있으며, 다양한 데이터셋에 재사용 가능한 구조를 만들 수 있습니다.
실전 팁
💡 __getitem__ 메서드 안에서 예외 처리를 꼭 추가하세요. 손상된 이미지 파일이나 잘못된 데이터가 있을 때 전체 학습이 중단되는 것을 방지할 수 있습니다.
💡 데이터 전처리는 __init__이 아닌 __getitem__에서 수행하세요. 그래야 메모리를 절약하고 멀티프로세싱 시 오류를 피할 수 있습니다.
💡 디버깅할 때는 작은 데이터셋(예: 100개)으로 먼저 테스트하세요. __len__과 __getitem__이 올바르게 동작하는지 확인한 후 전체 데이터로 확장하세요.
💡 transform 파라미터를 사용하면 학습용과 검증용 데이터에 서로 다른 전처리를 적용할 수 있습니다. 학습 시에는 데이터 증강을, 검증 시에는 기본 전처리만 적용하는 식으로 활용하세요.
💡 __getitem__의 반환값은 항상 일관된 형식을 유지하세요. 튜플 (data, label) 형태로 반환하는 것이 일반적이며, 이렇게 해야 DataLoader와 잘 호환됩니다.
2. DataLoader로 배치 처리하기
시작하며
여러분이 딥러닝 모델을 학습시킬 때 이런 고민을 해본 적 있나요? "데이터를 하나씩 학습시키면 너무 느린데, 몇 개씩 묶어서 처리하려면 코드가 너무 복잡해진다..." 이런 문제는 실제 개발 현장에서 항상 발생합니다.
데이터를 배치로 묶고, 매 에폭마다 순서를 섞고, 여러 CPU 코어를 활용해서 병렬로 데이터를 로드하는 작업은 생각보다 까다롭습니다. 직접 구현하려면 수십 줄의 복잡한 코드가 필요하고, 버그가 생기기도 쉽습니다.
바로 이럴 때 필요한 것이 DataLoader입니다. DataLoader는 마치 자동화된 물류 시스템처럼, Dataset에서 데이터를 가져와서 배치로 묶고, 섞고, 병렬로 처리하는 모든 과정을 자동으로 해줍니다.
개요
간단히 말해서, DataLoader는 Dataset을 받아서 배치 단위로 데이터를 제공하는 반복자(iterator)입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 딥러닝 학습은 배치 단위로 진행됩니다.
32개 또는 64개의 샘플을 한 번에 모델에 넣어서 gradient를 계산하는 것이 일반적입니다. DataLoader는 이러한 배치 처리를 자동화하고, 데이터 로딩 속도를 최적화하며, 메모리를 효율적으로 관리해줍니다.
예를 들어, 100만 개의 데이터를 배치 크기 32로 학습시킨다면, DataLoader가 자동으로 31,250개의 배치로 나누어 제공합니다. 기존에는 for 루프로 인덱스를 관리하고, 직접 배치를 만들고, numpy나 torch의 stack 함수로 텐서를 합쳐야 했다면, 이제는 DataLoader를 for 루프로 순회하기만 하면 자동으로 배치가 만들어집니다.
DataLoader의 핵심 특징은 네 가지입니다. 첫째, 자동 배치 생성으로 지정한 크기만큼 데이터를 묶어줍니다.
둘째, 셔플링 기능으로 매 에폭마다 데이터 순서를 무작위로 섞습니다. 셋째, 멀티프로세싱을 지원하여 여러 워커가 병렬로 데이터를 로드합니다.
넷째, pin_memory 옵션으로 GPU 전송 속도를 높입니다. 이러한 특징들이 학습 속도를 크게 향상시키고 코드를 간결하게 만들어줍니다.
코드 예제
from torch.utils.data import DataLoader
# Dataset 인스턴스 생성
dataset = CustomDataset(csv_file='data.csv')
# DataLoader 설정
train_loader = DataLoader(
dataset,
batch_size=32, # 한 번에 32개의 샘플을 배치로 묶음
shuffle=True, # 매 에폭마다 데이터 순서를 섞음
num_workers=4, # 4개의 병렬 프로세스로 데이터 로드
pin_memory=True, # GPU 전송 속도 향상 (GPU 사용 시)
drop_last=True # 마지막 불완전한 배치 제거
)
# 학습 루프에서 사용
for epoch in range(10):
for batch_idx, (images, labels) in enumerate(train_loader):
# images: [32, C, H, W] 형태의 텐서
# labels: [32] 형태의 텐서
# 모델 학습 코드
outputs = model(images)
loss = criterion(outputs, labels)
설명
이것이 하는 일: DataLoader는 Dataset 객체를 받아서, 학습 루프에서 사용할 수 있는 배치 형태의 데이터를 반복적으로 제공합니다. 마치 컨베이어 벨트처럼 계속해서 다음 배치를 준비해줍니다.
첫 번째로, batch_size=32로 설정하면 DataLoader는 Dataset에서 데이터를 32개씩 가져와서 하나의 배치로 만듭니다. 예를 들어 이미지가 각각 [3, 224, 224] 크기라면, 32개를 묶어서 [32, 3, 224, 224] 형태의 텐서로 만들어줍니다.
이 과정에서 torch.stack이나 torch.cat 같은 함수를 내부적으로 사용하여 자동으로 텐서를 합칩니다. 그 다음으로, shuffle=True로 설정하면 매 에폭이 시작될 때마다 데이터의 순서를 무작위로 섞습니다.
이것이 중요한 이유는, 모델이 데이터의 순서를 학습하는 것을 방지하고, 더 일반화된 패턴을 학습하게 만들기 때문입니다. 검증 데이터셋에서는 보통 shuffle=False로 설정합니다.
num_workers=4는 매우 강력한 기능입니다. 이 설정은 4개의 별도 프로세스를 만들어서 병렬로 데이터를 로드합니다.
메인 프로세스가 GPU에서 모델을 학습시키는 동안, 워커 프로세스들이 미리 다음 배치를 준비해놓는 것입니다. 이렇게 하면 GPU가 데이터를 기다리는 시간이 줄어들어 학습 속도가 크게 향상됩니다.
다만, 워커 수가 너무 많으면 메모리 사용량이 증가하므로 CPU 코어 수와 메모리를 고려해서 설정해야 합니다. 여러분이 이 코드를 사용하면 학습 루프가 매우 간결해집니다.
for 루프 하나로 전체 데이터셋을 배치 단위로 순회할 수 있고, 데이터 로딩과 전처리가 백그라운드에서 자동으로 진행되며, GPU 사용률을 최대화하여 학습 시간을 단축할 수 있습니다. 또한 코드의 가독성이 높아져 다른 개발자들도 쉽게 이해할 수 있습니다.
실전 팁
💡 num_workers는 CPU 코어 수보다 작거나 같게 설정하세요. 보통 4-8 정도가 적당하며, 너무 많으면 오히려 오버헤드가 발생합니다. Windows에서는 0으로 시작해서 점진적으로 늘려보세요.
💡 pin_memory=True는 GPU를 사용할 때만 설정하세요. CPU만 사용할 때는 False로 두는 것이 메모리 효율적입니다.
💡 drop_last=True는 배치 크기를 일정하게 유지하고 싶을 때 유용합니다. Batch Normalization 같은 레이어는 배치 크기에 민감하므로, 마지막 불완전한 배치를 제거하는 것이 안정적입니다.
💡 데이터 로딩이 병목이 되는지 확인하려면 num_workers=0과 num_workers=4의 학습 시간을 비교해보세요. 차이가 크다면 데이터 로딩 최적화가 더 필요한 것입니다.
💡 멀티프로세싱 사용 시 if __name__ == '__main__': 블록 안에서 코드를 실행하세요. 특히 Windows에서는 이것이 필수입니다.
3. 커스텀 전처리와 변환
시작하며
여러분이 이미지 분류 모델을 만들 때 이런 상황을 겪어본 적 있나요? 학습 데이터는 데이터 증강(회전, 자르기 등)을 해야 하는데, 검증 데이터는 원본 그대로 써야 한다고 하면 코드가 복잡해지기 시작합니다.
이런 문제는 실제 개발 현장에서 매우 흔합니다. 학습 시에는 이미지를 랜덤하게 회전시키고 색상을 조정해서 모델을 강건하게 만들어야 하지만, 검증과 테스트 시에는 일관된 전처리만 적용해야 합니다.
또한 이미지 크기 조정, 정규화, 텐서 변환 같은 기본적인 전처리도 매번 다르게 적용해야 할 때가 있습니다. 바로 이럴 때 필요한 것이 torchvision의 transforms입니다.
transforms는 마치 요리의 레시피처럼, 여러 전처리 단계를 순서대로 정의하고 조합할 수 있게 해줍니다. 한 번 정의해두면 모든 데이터에 자동으로 적용됩니다.
개요
간단히 말해서, transforms는 이미지나 데이터에 적용할 변환 작업들을 체인처럼 연결해주는 도구입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 딥러닝 모델의 성능은 데이터 전처리에 크게 좌우됩니다.
같은 모델이라도 적절한 데이터 증강을 사용하면 정확도가 5-10% 향상될 수 있습니다. transforms를 사용하면 이러한 전처리 파이프라인을 재사용 가능한 형태로 만들 수 있고, 실험할 때 쉽게 조합을 바꿔볼 수 있습니다.
예를 들어, ResNet 논문에서 사용한 전처리를 그대로 재현하거나, 여러 증강 기법을 조합해서 최적의 조합을 찾을 수 있습니다. 기존에는 각 전처리 단계를 함수로 만들고, Dataset의 __getitem__ 안에서 일일이 호출해야 했다면, 이제는 Compose로 여러 transform을 묶어서 한 번에 적용할 수 있습니다.
transforms의 핵심 특징은 세 가지입니다. 첫째, Compose를 사용해 여러 변환을 순차적으로 적용할 수 있습니다.
둘째, 학습용과 검증용으로 서로 다른 transform을 쉽게 만들 수 있습니다. 셋째, 커스텀 transform을 직접 만들어서 프로젝트에 특화된 전처리를 추가할 수 있습니다.
이러한 특징들이 실험을 빠르게 하고 코드를 깔끔하게 유지할 수 있게 해줍니다.
코드 예제
from torchvision import transforms
# 학습용 transform: 데이터 증강 포함
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224), # 랜덤하게 자르고 224x224로 리사이즈
transforms.RandomHorizontalFlip(), # 50% 확률로 좌우 반전
transforms.ColorJitter(brightness=0.2, contrast=0.2), # 밝기와 대비 조정
transforms.ToTensor(), # PIL Image를 Tensor로 변환
transforms.Normalize(mean=[0.485, 0.456, 0.406], # ImageNet 평균으로 정규화
std=[0.229, 0.224, 0.225])
])
# 검증/테스트용 transform: 증강 없이 기본 전처리만
val_transform = transforms.Compose([
transforms.Resize(256), # 짧은 쪽을 256으로 리사이즈
transforms.CenterCrop(224), # 중앙에서 224x224 크기로 자름
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# Dataset에 적용
train_dataset = CustomDataset('train.csv', transform=train_transform)
val_dataset = CustomDataset('val.csv', transform=val_transform)
설명
이것이 하는 일: transforms는 여러 이미지 변환 작업을 하나의 파이프라인으로 묶어서, Dataset의 __getitem__이 호출될 때마다 자동으로 적용되도록 만듭니다. 첫 번째로, transforms.Compose는 여러 변환을 리스트로 받아서 순서대로 실행합니다.
위 코드에서 학습용 transform은 5단계로 구성됩니다. 먼저 이미지를 랜덤하게 자르고, 좌우 반전할지 결정하고, 색상을 조정하고, 텐서로 변환하고, 마지막으로 정규화합니다.
각 단계는 이전 단계의 출력을 입력으로 받습니다. 그 다음으로, 학습용과 검증용 transform의 차이를 살펴보면 핵심을 이해할 수 있습니다.
학습용에는 RandomResizedCrop, RandomHorizontalFlip, ColorJitter 같은 랜덤 증강이 포함되어 있습니다. 이것들은 매번 호출될 때마다 다른 결과를 만들어내서, 모델이 다양한 변형에 강건해지도록 합니다.
반면 검증용에는 Resize와 CenterCrop만 사용하여, 항상 같은 방식으로 전처리합니다. 이렇게 해야 검증 결과가 일관되게 나옵니다.
Normalize의 mean과 std 값은 ImageNet 데이터셋의 통계값입니다. 이것은 매우 중요한데, 사전 학습된 모델(pretrained model)을 사용할 때는 반드시 같은 정규화 값을 써야 합니다.
만약 여러분의 커스텀 데이터로 처음부터 학습한다면, 데이터셋의 평균과 표준편차를 계산해서 사용하는 것이 좋습니다. ToTensor() 변환은 PIL Image나 numpy 배열을 PyTorch 텐서로 바꾸고, 픽셀 값을 [0, 255] 범위에서 [0, 1] 범위로 자동으로 스케일링합니다.
그 다음 Normalize가 평균을 빼고 표준편차로 나누어서 대략 [-1, 1] 범위로 만듭니다. 이런 정규화는 학습을 안정화시키고 수렴 속도를 높입니다.
여러분이 이 코드를 사용하면 데이터 증강을 매우 쉽게 적용할 수 있고, 전처리 파이프라인을 명확하게 문서화할 수 있으며, 다양한 증강 조합을 빠르게 실험해볼 수 있습니다. 또한 torchvision이 제공하는 최적화된 구현을 사용하므로 성능도 좋습니다.
실전 팁
💡 ImageNet pretrained 모델을 사용할 때는 반드시 ImageNet의 mean과 std로 정규화하세요. 다른 값을 사용하면 성능이 크게 떨어집니다.
💡 RandomHorizontalFlip이나 RandomRotation 같은 증강은 데이터의 특성을 고려하세요. 예를 들어 숫자 인식에서 상하 반전은 의미가 없을 수 있습니다.
💡 자신만의 transform을 만들려면 __call__ 메서드를 구현하면 됩니다. 예를 들어 특정 도메인의 노이즈를 추가하거나, 의료 영상의 특수한 전처리를 넣을 수 있습니다.
💡 transforms.RandomApply를 사용하면 특정 증강을 확률적으로 적용할 수 있습니다. 너무 강한 증강을 매번 적용하지 않고 50% 확률로만 적용하는 식으로 조절하세요.
💡 데이터 증강이 제대로 적용되는지 확인하려면 DataLoader에서 몇 개의 배치를 뽑아서 시각화해보세요. matplotlib로 증강된 이미지를 확인하면 문제를 빨리 찾을 수 있습니다.
4. 대용량 데이터 효율적으로 다루기
시작하며
여러분이 수백 GB에 달하는 대용량 이미지 데이터셋으로 학습할 때 이런 상황을 겪어본 적 있나요? 데이터를 로드하는 속도가 너무 느려서 GPU는 놀고 있는데 CPU만 바쁘게 돌아간다거나, 메모리 부족 에러가 계속 발생한다거나 하는 문제 말입니다.
이런 문제는 대규모 프로젝트에서 반드시 맞닥뜨리게 됩니다. GPU는 초당 수천 개의 샘플을 처리할 수 있지만, 디스크에서 이미지를 읽어오는 속도가 따라가지 못하면 GPU 사용률이 낮아지고 학습 시간이 몇 배로 늘어납니다.
또한 메모리 관리가 제대로 되지 않으면 학습 중간에 크래시가 발생할 수 있습니다. 바로 이럴 때 필요한 것이 효율적인 데이터 로딩 전략입니다.
PyTorch는 멀티프로세싱, 메모리 피닝, 프리페칭 같은 다양한 최적화 기법을 제공하며, 이것들을 적절히 조합하면 데이터 로딩 속도를 몇 배로 향상시킬 수 있습니다.
개요
간단히 말해서, 대용량 데이터를 효율적으로 다루려면 병렬 처리와 메모리 최적화 기법을 활용해야 합니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 현대의 딥러닝 모델은 수백만에서 수억 개의 샘플로 학습됩니다.
ImageNet만 해도 120만 개의 이미지가 있고, 실무 프로젝트에서는 이보다 훨씬 큰 데이터셋을 다루기도 합니다. 이런 규모에서 1초 차이가 몇 시간, 며칠의 차이로 이어집니다.
예를 들어, 배치당 로딩 시간을 0.5초에서 0.1초로 줄이면, 10만 배치 학습 시 11시간을 절약할 수 있습니다. 기존에는 단순히 num_workers를 늘리는 것만 생각했다면, 이제는 데이터 저장 포맷, 캐싱 전략, 프리페칭, 메모리 관리까지 종합적으로 고려해야 합니다.
효율적인 데이터 로딩의 핵심 기법은 네 가지입니다. 첫째, 적절한 num_workers 설정으로 CPU 병렬 처리를 활용합니다.
둘째, pin_memory를 사용해서 CPU-GPU 데이터 전송을 가속화합니다. 셋째, persistent_workers로 워커 프로세스 재생성 오버헤드를 제거합니다.
넷째, prefetch_factor로 미리 준비할 배치 수를 조절합니다. 이러한 기법들이 조합되면 GPU 사용률을 최대화하고 학습 시간을 크게 단축할 수 있습니다.
코드 예제
from torch.utils.data import DataLoader
import torch
# 효율적인 DataLoader 설정
efficient_loader = DataLoader(
dataset,
batch_size=64,
shuffle=True,
num_workers=8, # CPU 코어 수에 맞춰 설정 (보통 4-8)
pin_memory=True, # GPU 전송 가속화
persistent_workers=True, # 워커 프로세스 재사용 (PyTorch 1.7+)
prefetch_factor=2, # 워커당 미리 로드할 배치 수
drop_last=True
)
# 메모리 효율적인 학습 루프
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for epoch in range(num_epochs):
for batch_idx, (data, target) in enumerate(efficient_loader):
# non_blocking=True로 비동기 GPU 전송
data = data.to(device, non_blocking=True)
target = target.to(device, non_blocking=True)
# 모델 학습
output = model(data)
loss = criterion(output, target)
# 메모리 관리: 불필요한 그래프 삭제
loss.backward()
optimizer.step()
optimizer.zero_grad()
설명
이것이 하는 일: 이 설정들은 데이터 로딩 파이프라인을 최적화하여, GPU가 데이터를 기다리는 시간을 최소화하고 학습 처리량을 극대화합니다. 첫 번째로, num_workers=8은 8개의 별도 프로세스를 생성하여 병렬로 데이터를 로드합니다.
메인 프로세스가 GPU에서 모델을 학습시키는 동안, 이 워커들이 디스크에서 이미지를 읽고 전처리를 수행합니다. 이렇게 하면 CPU와 GPU가 동시에 일하므로 전체 처리 속도가 빨라집니다.
적절한 워커 수는 CPU 코어 수, 데이터 크기, 전처리 복잡도에 따라 다르므로 실험을 통해 찾아야 합니다. 보통 4-8이 적당하며, 너무 많으면 컨텍스트 스위칭 오버헤드가 발생합니다.
그 다음으로, pin_memory=True는 데이터를 페이지 고정 메모리(pinned memory)에 올립니다. 일반 메모리는 운영체제가 디스크로 스왑할 수 있지만, 고정 메모리는 항상 RAM에 유지됩니다.
GPU가 데이터를 가져갈 때 고정 메모리에서 직접 전송할 수 있어서 훨씬 빠릅니다. 대신 메모리 사용량이 약간 증가하므로, 메모리가 부족한 환경에서는 주의해야 합니다.
persistent_workers=True는 매우 유용한 최적화입니다. 기본적으로 DataLoader는 매 에폭마다 워커 프로세스를 종료하고 새로 생성합니다.
이 과정에서 프로세스 생성 시간과 메모리 할당 오버헤드가 발생합니다. persistent_workers를 켜면 워커들이 한 번 생성된 후 계속 재사용되므로, 에폭 간 전환이 빨라집니다.
특히 많은 에폭을 학습할 때 효과가 큽니다. prefetch_factor=2는 각 워커가 현재 사용 중인 배치 외에 2개의 배치를 미리 준비해둡니다.
이렇게 하면 모델이 현재 배치 처리를 끝내는 순간 바로 다음 배치를 받을 수 있습니다. 너무 큰 값은 메모리를 많이 사용하므로 2-4 정도가 적당합니다.
또한 non_blocking=True를 사용하여 GPU 전송을 비동기로 수행하면, 데이터 전송과 동시에 다른 작업을 할 수 있습니다. 여러분이 이 코드를 사용하면 GPU 사용률이 크게 향상되고, 같은 하드웨어로 학습 시간을 30-50% 단축할 수 있으며, 대용량 데이터셋을 안정적으로 처리할 수 있습니다.
실제로 num_workers=0과 num_workers=8의 차이는 시간당 처리 배치 수가 2-3배 차이날 수 있습니다.
실전 팁
💡 최적의 num_workers를 찾으려면 0, 2, 4, 8로 학습 속도를 측정해보세요. 모니터링 도구로 GPU 사용률을 확인하면서 조절하면 됩니다.
💡 메모리가 부족하면 batch_size를 줄이거나 pin_memory=False로 설정하세요. 또는 gradient accumulation으로 효과적인 배치 크기를 유지할 수 있습니다.
💡 SSD를 사용하면 데이터 로딩 속도가 크게 향상됩니다. HDD에서는 순차 읽기가 빠르므로 데이터를 순차적으로 저장하는 것이 좋습니다.
💡 이미지를 JPEG 대신 HDF5나 LMDB 같은 바이너리 포맷으로 저장하면 로딩 속도가 몇 배 빨라질 수 있습니다. 특히 작은 이미지가 많을 때 효과적입니다.
💡 프로파일링 도구(PyTorch Profiler)를 사용해서 데이터 로딩이 병목인지 확인하세요. 만약 데이터 로딩 시간이 전체의 50% 이상이라면 더 많은 최적화가 필요합니다.
5. 불균형 데이터셋 처리하기
시작하며
여러분이 이미지 분류 모델을 만들 때 이런 상황을 겪어본 적 있나요? 클래스별 샘플 수가 극단적으로 다를 때, 예를 들어 정상 데이터는 10,000개인데 비정상 데이터는 100개밖에 없을 때 모델이 다수 클래스만 예측하는 문제 말입니다.
이런 문제는 실제 개발 현장에서 매우 흔합니다. 의료 영상에서 질병 케이스는 정상보다 훨씬 적고, 이상 탐지에서 이상 샘플은 전체의 1%도 안 되는 경우가 많습니다.
모델은 항상 "정상"이라고 예측해도 99% 정확도를 달성할 수 있지만, 이것은 전혀 유용하지 않습니다. 바로 이럴 때 필요한 것이 WeightedRandomSampler입니다.
이것은 마치 소수 의견을 더 자주 듣는 것처럼, 적은 클래스의 샘플을 더 자주 선택하여 학습 시 균형을 맞춰줍니다.
개요
간단히 말해서, WeightedRandomSampler는 각 샘플에 가중치를 부여하여, 적은 클래스의 샘플이 더 자주 선택되도록 만드는 도구입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 불균형 데이터로 학습하면 모델이 편향됩니다.
다수 클래스만 잘 예측하고 소수 클래스는 거의 무시하게 됩니다. WeightedRandomSampler를 사용하면 학습 중에 소수 클래스를 과대표집(oversampling)하여, 모든 클래스를 균등하게 학습할 기회를 만들어줍니다.
예를 들어, 사기 거래 탐지에서 정상 거래 10,000개, 사기 거래 100개가 있다면, 사기 거래를 100배 더 자주 샘플링하여 균형을 맞출 수 있습니다. 기존에는 소수 클래스 데이터를 직접 복제하거나, 손실 함수에 가중치를 주는 방법을 사용했다면, 이제는 샘플러가 자동으로 균형잡힌 배치를 만들어줍니다.
WeightedRandomSampler의 핵심 특징은 세 가지입니다. 첫째, 각 샘플의 선택 확률을 개별적으로 제어할 수 있습니다.
둘째, replacement=True로 설정하면 같은 샘플을 한 에폭에 여러 번 사용할 수 있습니다. 셋째, DataLoader의 shuffle 기능을 대체하면서도 무작위성을 유지합니다.
이러한 특징들이 불균형 데이터 문제를 효과적으로 해결해줍니다.
코드 예제
from torch.utils.data import WeightedRandomSampler
import numpy as np
# 클래스별 샘플 수 계산 (예: 클래스 0=9000개, 클래스 1=1000개)
class_counts = np.array([9000, 1000])
num_samples = sum(class_counts)
# 각 클래스의 가중치 계산 (역수로 계산하여 적은 클래스에 높은 가중치)
class_weights = 1.0 / class_counts
# class_weights = [1/9000, 1/1000] = [0.00011, 0.001]
# 각 샘플에 해당 클래스의 가중치 할당
sample_weights = [class_weights[label] for label in labels]
# labels는 전체 데이터셋의 레이블 리스트 [0, 1, 0, 0, 1, ...]
# WeightedRandomSampler 생성
sampler = WeightedRandomSampler(
weights=sample_weights,
num_samples=len(sample_weights),
replacement=True # 중복 샘플링 허용
)
# DataLoader에 적용 (shuffle=False 필수!)
balanced_loader = DataLoader(
dataset,
batch_size=32,
sampler=sampler, # sampler 사용 시 shuffle=False
num_workers=4
)
설명
이것이 하는 일: WeightedRandomSampler는 각 샘플에 가중치를 부여하고, 이 가중치에 비례하는 확률로 샘플을 선택하여 불균형을 보정합니다. 첫 번째로, 클래스 가중치를 계산하는 부분을 이해해야 합니다.
class_weights = 1.0 / class_counts는 샘플 수가 적은 클래스에 높은 가중치를 부여합니다. 예를 들어 클래스 0이 9000개, 클래스 1이 1000개라면, 클래스 0의 가중치는 1/9000=0.00011, 클래스 1의 가중치는 1/1000=0.001이 됩니다.
클래스 1의 가중치가 9배 더 크므로, 클래스 1 샘플이 선택될 확률이 9배 높아집니다. 그 다음으로, 각 개별 샘플에 해당 클래스의 가중치를 할당합니다.
만약 샘플 0의 레이블이 0이라면 가중치 0.00011을, 샘플 1의 레이블이 1이라면 가중치 0.001을 받습니다. 이렇게 모든 샘플에 대한 가중치 리스트를 만들면, WeightedRandomSampler가 이것을 기반으로 샘플링합니다.
replacement=True는 중요한 설정입니다. True로 하면 같은 샘플을 한 에폭에 여러 번 뽑을 수 있습니다.
이것이 필요한 이유는, 소수 클래스 샘플이 100개밖에 없는데 균형잡힌 학습을 위해 1000번 필요하다면, 같은 샘플을 여러 번 사용해야 하기 때문입니다. 만약 False로 설정하면 각 샘플을 한 번씩만 사용하므로, 에폭당 볼 수 있는 샘플 수가 제한됩니다.
중요한 주의사항은 sampler를 사용할 때 shuffle=False로 설정해야 한다는 것입니다. sampler와 shuffle을 동시에 사용하면 에러가 발생합니다.
sampler 자체가 이미 무작위 샘플링을 수행하므로 shuffle이 필요 없습니다. 매 에폭마다 sampler가 새로운 무작위 순서로 샘플을 선택합니다.
여러분이 이 코드를 사용하면 불균형 데이터에서도 모든 클래스를 공정하게 학습할 수 있고, 소수 클래스의 재현율(recall)이 크게 향상되며, 실무에서 정말 중요한 희귀 케이스를 잘 탐지할 수 있게 됩니다. 특히 의료, 금융, 보안 분야에서 효과적입니다.
실전 팁
💡 가중치 계산 방식을 조절할 수 있습니다. 1.0 / class_counts 대신 1.0 / np.sqrt(class_counts)를 사용하면 덜 극단적인 균형을 만들 수 있습니다.
💡 손실 함수에도 클래스 가중치를 함께 적용하면 효과가 더 좋습니다. nn.CrossEntropyLoss(weight=torch.FloatTensor(class_weights))처럼 사용하세요.
💡 validation set에는 WeightedRandomSampler를 사용하지 마세요. 검증은 실제 데이터 분포를 반영해야 하므로 원본 불균형 그대로 사용해야 합니다.
💡 데이터 증강과 함께 사용하면 소수 클래스의 다양성을 높일 수 있습니다. 같은 샘플을 여러 번 사용하더라도 증강 덕분에 매번 다른 변형이 생깁니다.
💡 성능 지표는 accuracy가 아닌 F1-score, precision, recall을 사용하세요. 불균형 데이터에서는 accuracy가 오해를 불러일으킬 수 있습니다.
6. 멀티모달 데이터 처리하기
시작하며
여러분이 딥러닝 프로젝트를 진행하다 보면 이런 상황을 겪을 때가 있습니다. 이미지와 텍스트를 함께 사용해야 하거나, 이미지, 메타데이터, 텍스트를 모두 결합해서 예측을 해야 하는 경우 말입니다.
예를 들어 상품 이미지와 설명을 함께 보고 카테고리를 분류한다거나, 의료 영상과 환자 기록을 함께 분석해야 할 때입니다. 이런 문제는 실무에서 점점 더 흔해지고 있습니다.
단일 모달리티만으로는 충분한 정보를 얻기 어렵고, 여러 종류의 데이터를 결합하면 훨씬 좋은 성능을 얻을 수 있습니다. 하지만 서로 다른 형태의 데이터를 어떻게 Dataset과 DataLoader에서 다룰지 고민이 됩니다.
바로 이럴 때 필요한 것이 멀티모달 Dataset 설계 패턴입니다. __getitem__에서 여러 종류의 데이터를 함께 반환하고, 각각에 맞는 전처리를 적용하는 방법을 알면 복잡한 멀티모달 프로젝트도 쉽게 다룰 수 있습니다.
개요
간단히 말해서, 멀티모달 Dataset은 __getitem__에서 여러 종류의 데이터를 딕셔너리나 튜플로 묶어서 반환하는 방식으로 구현합니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 현대의 많은 AI 시스템은 멀티모달입니다.
CLIP 같은 모델은 이미지와 텍스트를 함께 처리하고, 추천 시스템은 이미지, 텍스트, 사용자 메타데이터를 결합합니다. 멀티모달 Dataset을 제대로 설계하면 이런 복잡한 데이터를 깔끔하게 관리할 수 있습니다.
예를 들어, 전자상거래 상품 분류에서 상품 이미지, 제목, 가격, 브랜드를 모두 사용하여 카테고리를 예측할 수 있습니다. 기존에는 이미지와 텍스트를 별도의 Dataset으로 만들고 수동으로 인덱스를 맞춰야 했다면, 이제는 하나의 Dataset에서 모든 모달리티를 함께 다룰 수 있습니다.
멀티모달 Dataset의 핵심 특징은 세 가지입니다. 첫째, 각 모달리티별로 독립적인 전처리 파이프라인을 적용할 수 있습니다.
둘째, 딕셔너리 형태로 반환하면 가독성이 좋고 확장이 쉽습니다. 셋째, DataLoader의 collate_fn을 커스터마이즈하여 배치 생성 방식을 제어할 수 있습니다.
이러한 특징들이 복잡한 멀티모달 프로젝트를 체계적으로 관리할 수 있게 해줍니다.
코드 예제
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from transformers import BertTokenizer
class MultimodalDataset(Dataset):
def __init__(self, data_df, image_transform=None, max_text_length=128):
self.data = data_df
self.image_transform = image_transform
# 텍스트 토크나이저 초기화
self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
self.max_length = max_text_length
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
row = self.data.iloc[idx]
# 이미지 로드 및 전처리
image = self._load_image(row['image_path'])
if self.image_transform:
image = self.image_transform(image)
# 텍스트 토크나이징
text = row['description']
encoded = self.tokenizer.encode_plus(
text,
max_length=self.max_length,
padding='max_length',
truncation=True,
return_tensors='pt'
)
# 메타데이터 (숫자형)
metadata = torch.tensor([row['price'], row['rating']], dtype=torch.float32)
# 딕셔너리 형태로 반환
return {
'image': image,
'input_ids': encoded['input_ids'].squeeze(0),
'attention_mask': encoded['attention_mask'].squeeze(0),
'metadata': metadata,
'label': torch.tensor(row['category'], dtype=torch.long)
}
설명
이것이 하는 일: 멀티모달 Dataset은 서로 다른 형태의 데이터를 동시에 로드하고, 각각에 적합한 전처리를 적용한 후, 하나의 구조화된 형태로 묶어서 반환합니다. 첫 번째로, __init__ 메서드에서 각 모달리티에 필요한 도구들을 준비합니다.
이미지용 transform, 텍스트용 tokenizer를 미리 초기화해둡니다. 이렇게 하면 __getitem__이 호출될 때마다 매번 초기화하지 않아도 되므로 효율적입니다.
BERT 토크나이저는 텍스트를 숫자로 변환하는 도구로, 사전 학습된 언어 모델과 호환되는 형식으로 만들어줍니다. 그 다음으로, __getitem__에서 각 모달리티를 개별적으로 처리합니다.
이미지는 PIL로 읽어서 torchvision transform을 적용하고, 텍스트는 토크나이저로 토큰화하며, 메타데이터는 텐서로 변환합니다. 각 처리 과정이 독립적이므로, 나중에 하나의 모달리티만 수정하거나 추가하기 쉽습니다.
텍스트 토크나이징에서 max_length와 padding이 중요합니다. 텍스트 길이가 다양하므로, 모든 텍스트를 같은 길이로 맞춰야 배치로 묶을 수 있습니다.
max_length=128로 설정하면 긴 텍스트는 잘리고(truncation), 짧은 텍스트는 패딩이 추가됩니다. attention_mask는 어디가 실제 토큰이고 어디가 패딩인지 표시하여, 모델이 패딩을 무시할 수 있게 합니다.
딕셔너리로 반환하는 것이 핵심입니다. 튜플 (image, input_ids, metadata, label) 형태로 반환할 수도 있지만, 딕셔너리 {'image': ..., 'input_ids': ...}가 훨씬 명확합니다.
나중에 모델에서 batch['image'], batch['input_ids'] 같은 식으로 접근할 수 있어 코드 가독성이 좋아집니다. 또한 새로운 모달리티를 추가해도 기존 코드를 크게 수정할 필요가 없습니다.
여러분이 이 코드를 사용하면 복잡한 멀티모달 데이터를 체계적으로 관리할 수 있고, 각 모달리티의 전처리를 독립적으로 수정할 수 있으며, 모델 코드에서 데이터 접근이 명확해집니다. 실무에서 이미지+텍스트, 비디오+오디오, 센서 데이터 결합 등 다양한 시나리오에 적용할 수 있습니다.
실전 팁
💡 DataLoader의 기본 collate_fn은 딕셔너리도 잘 처리합니다. 각 키별로 자동으로 배치를 만들어주므로 대부분의 경우 커스텀 collate_fn이 필요 없습니다.
💡 텍스트와 이미지의 배치 크기를 다르게 하고 싶다면, 두 개의 별도 DataLoader를 만들고 zip()으로 묶어서 사용하는 방법도 있습니다.
💡 메타데이터를 정규화하세요. 가격이 0-10000 범위고 평점이 0-5 범위라면, StandardScaler나 MinMaxScaler로 스케일을 맞춰야 모델이 잘 학습합니다.
💡 누락된 데이터(missing data)를 처리하는 로직을 추가하세요. 텍스트가 없거나 이미지가 손상된 경우를 대비하여 기본값이나 에러 처리를 구현하세요.
💡 모달리티별로 별도의 인코더를 사용하는 것이 일반적입니다. 예를 들어 이미지는 ResNet, 텍스트는 BERT로 인코딩한 후, 출력을 concat하거나 attention으로 융합합니다.
7. 동적 배치 크기와 커스텀 Collate 함수
시작하며
여러분이 자연어 처리 모델을 학습시킬 때 이런 상황을 겪어본 적 있나요? 문장 길이가 제각각인데, 가장 긴 문장에 맞춰서 모든 문장을 패딩하니 메모리가 낭비되고, 짧은 문장들은 대부분 패딩으로 채워져서 비효율적이다 하는 고민 말입니다.
이런 문제는 시퀀스 데이터를 다룰 때 항상 발생합니다. 고정된 길이로 패딩하면 간단하지만, 배치 내에서 가장 긴 시퀀스에만 맞춰서 패딩하면 메모리와 계산량을 크게 절약할 수 있습니다.
하지만 이것을 구현하려면 DataLoader의 기본 동작을 커스터마이즈해야 합니다. 바로 이럴 때 필요한 것이 커스텀 collate 함수입니다.
collate 함수는 여러 개의 샘플을 받아서 배치로 만드는 역할을 하는데, 이것을 직접 정의하면 배치 생성 과정을 완전히 제어할 수 있습니다.
개요
간단히 말해서, collate 함수는 Dataset에서 가져온 개별 샘플들을 받아서 하나의 배치 텐서로 합치는 함수입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 기본 collate 함수는 모든 샘플이 같은 크기라고 가정합니다.
하지만 텍스트, 시계열, 가변 길이 시퀀스를 다룰 때는 샘플마다 크기가 다릅니다. 커스텀 collate 함수를 사용하면 동적 패딩, 샘플 필터링, 복잡한 배치 구조 생성 등 다양한 작업을 할 수 있습니다.
예를 들어, 번역 모델 학습 시 소스 문장과 타겟 문장의 길이가 각각 다른데, 배치 내에서 각각의 최대 길이에만 맞춰 패딩하면 효율적입니다. 기존에는 Dataset의 __getitem__에서 고정 길이로 패딩하거나, 길이별로 데이터를 미리 정렬해야 했다면, 이제는 collate 함수가 배치를 만들 때 동적으로 처리합니다.
커스텀 collate 함수의 핵심 특징은 세 가지입니다. 첫째, 배치 내 샘플들의 실제 크기를 보고 최소한의 패딩만 추가할 수 있습니다.
둘째, 복잡한 데이터 구조(리스트, 딕셔너리, 중첩 텐서)도 자유롭게 만들 수 있습니다. 셋째, 배치 생성 시 추가 로직(정렬, 필터링, 검증)을 넣을 수 있습니다.
이러한 특징들이 메모리 효율을 높이고 유연한 데이터 파이프라인을 만들 수 있게 합니다.
코드 예제
import torch
from torch.nn.utils.rnn import pad_sequence
def custom_collate_fn(batch):
"""
batch: Dataset의 __getitem__이 반환한 샘플들의 리스트
예: [{'text': tensor([1,2,3]), 'label': 0},
{'text': tensor([4,5]), 'label': 1}, ...]
"""
# 각 샘플에서 텍스트와 레이블 분리
texts = [item['text'] for item in batch]
labels = [item['label'] for item in batch]
# 동적 패딩: 배치 내 가장 긴 시퀀스에만 맞춤
# pad_sequence는 자동으로 가장 긴 길이를 찾아 패딩
padded_texts = pad_sequence(texts, batch_first=True, padding_value=0)
# batch_first=True이면 [batch_size, seq_length] 형태
# 실제 길이 정보 저장 (패딩 제외)
lengths = torch.tensor([len(text) for text in texts])
# 레이블을 텐서로 변환
labels = torch.tensor(labels, dtype=torch.long)
# 배치 반환
return {
'text': padded_texts,
'lengths': lengths, # RNN 등에서 pack_padded_sequence 사용 시 필요
'label': labels
}
# DataLoader에 적용
train_loader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
collate_fn=custom_collate_fn # 커스텀 collate 함수 지정
)
설명
이것이 하는 일: collate 함수는 DataLoader가 Dataset에서 가져온 여러 개의 샘플을 받아서, 모델이 처리할 수 있는 배치 형태로 변환합니다. 첫 번째로, 함수의 입력을 이해해야 합니다.
batch 파라미터는 리스트인데, 각 요소는 Dataset의 __getitem__이 반환한 값입니다. 예를 들어 batch_size=32라면, 이 리스트는 32개의 딕셔너리나 튜플을 포함합니다.
우리는 이 32개의 샘플을 하나의 텐서로 합쳐야 합니다. 그 다음으로, 동적 패딩이 어떻게 작동하는지 살펴봅시다.
pad_sequence 함수는 입력으로 받은 텐서 리스트에서 가장 긴 것을 찾아서, 나머지를 그 길이에 맞춥니다. 예를 들어 길이가 [5, 10, 7]인 세 시퀀스가 있으면, 모두 길이 10으로 패딩합니다.
고정 길이 128로 패딩하는 것보다 훨씬 효율적입니다. padding_value=0은 패딩에 사용할 값이고, batch_first=True는 배치 차원을 첫 번째로 만듭니다.
lengths 정보는 매우 유용합니다. RNN이나 LSTM 같은 시퀀스 모델은 pack_padded_sequence를 사용하여 패딩 부분을 건너뛸 수 있는데, 이때 각 시퀀스의 실제 길이가 필요합니다.
이렇게 하면 패딩 부분에서 불필요한 계산을 하지 않아 속도가 빨라집니다. 마지막으로 반환하는 딕셔너리 구조를 보면, 모델에서 사용하기 편리하게 구성되어 있습니다.
학습 루프에서 batch['text'], batch['lengths'], batch['label']로 쉽게 접근할 수 있습니다. 이러한 명확한 구조는 코드 가독성을 높이고 버그를 줄여줍니다.
여러분이 이 코드를 사용하면 메모리 사용량이 크게 줄어들고 (특히 짧은 시퀀스가 많을 때), 학습 속도가 향상되며 (패딩 계산 감소), 가변 길이 데이터를 자연스럽게 처리할 수 있습니다. NLP, 시계열 분석, 음성 인식 등에서 필수적인 기법입니다.
실전 팁
💡 pad_sequence는 기본적으로 왼쪽 정렬 패딩입니다. 오른쪽 정렬이 필요하면 수동으로 구현하거나 패딩 후 뒤집어야 합니다.
💡 배치 내 샘플을 길이순으로 정렬하면 pack_padded_sequence가 더 효율적으로 작동합니다. collate 함수 안에서 정렬하는 로직을 추가하세요.
💡 에러 처리를 추가하세요. 예를 들어 빈 시퀀스가 있거나 텐서 타입이 맞지 않으면 명확한 에러 메시지를 출력하도록 하세요.
💡 복잡한 중첩 구조(리스트 안의 딕셔너리 등)를 다룰 때는 재귀적으로 collate하는 유틸리티 함수를 만들면 편리합니다.
💡 성능이 중요하다면 collate 함수를 프로파일링하세요. 특히 큰 배치 크기에서 collate가 병목이 될 수 있으므로, 필요하면 최적화하세요.
8. 데이터 증강과 온라인 증강
시작하며
여러분이 이미지 분류 모델을 학습시킬 때 이런 딜레마를 겪어본 적 있나요? 데이터 증강을 미리 해서 디스크에 저장해두면 로딩은 빠르지만 저장 공간이 몇 배로 늘어나고, 실시간으로 증강하면 공간은 절약되지만 학습이 느려질까봐 걱정되는 상황 말입니다.
이런 문제는 데이터 증강을 활용하는 모든 프로젝트에서 발생합니다. 데이터 증강은 모델의 일반화 성능을 크게 향상시키지만, 언제 어떻게 적용할지가 중요합니다.
오프라인 증강(미리 생성)과 온라인 증강(실시간 생성) 각각의 장단점이 있습니다. 바로 이럴 때 필요한 것이 온라인 데이터 증강 전략입니다.
PyTorch의 transforms를 활용하면 학습 중에 실시간으로 다양한 증강을 적용하면서도, 멀티프로세싱으로 성능 저하를 최소화할 수 있습니다.
개요
간단히 말해서, 온라인 데이터 증강은 Dataset의 __getitem__이나 transform 파이프라인에서 매번 다른 증강을 실시간으로 적용하는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터 증강은 제한된 데이터로 더 강건한 모델을 만드는 핵심 기법입니다.
온라인 증강을 사용하면 같은 이미지도 매번 다르게 보이므로, 사실상 무한한 다양성을 만들 수 있습니다. 디스크 공간도 절약되고, 증강 방식을 실험하기도 쉽습니다.
예를 들어, 1000장의 이미지로 10가지 증강을 미리 만들면 10,000장을 저장해야 하지만, 온라인 증강은 1000장만 저장하고 매번 랜덤하게 증강합니다. 기존에는 증강된 이미지를 미리 생성해서 저장하거나, 복잡한 증강 파이프라인을 직접 구현해야 했다면, 이제는 torchvision이나 albumentations의 transform을 조합하면 끝입니다.
온라인 데이터 증강의 핵심 특징은 세 가지입니다. 첫째, 매 에폭마다 같은 이미지가 다르게 증강되어 모델이 더 다양한 패턴을 학습합니다.
둘째, 디스크 공간을 절약하고 증강 파이프라인을 쉽게 수정할 수 있습니다. 셋째, DataLoader의 num_workers와 결합하면 증강 계산을 병렬화하여 성능 저하를 방지할 수 있습니다.
이러한 특징들이 실무에서 온라인 증강을 표준으로 만들었습니다.
코드 예제
from torchvision import transforms
import albumentations as A
from albumentations.pytorch import ToTensorV2
# torchvision 기반 온라인 증강
torchvision_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1),
transforms.RandomGrayscale(p=0.1),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# albumentations 기반 증강 (더 많은 옵션과 빠른 성능)
albumentation_transform = A.Compose([
A.RandomResizedCrop(224, 224, scale=(0.8, 1.0)),
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),
A.OneOf([ # 여러 증강 중 하나만 랜덤 선택
A.GaussNoise(var_limit=(10, 50)),
A.GaussianBlur(blur_limit=3),
A.MotionBlur(blur_limit=3),
], p=0.3),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
# Dataset에서 사용
class AugmentedDataset(Dataset):
def __init__(self, image_paths, labels, transform=None):
self.image_paths = image_paths
self.labels = labels
self.transform = transform
def __getitem__(self, idx):
image = Image.open(self.image_paths[idx]).convert('RGB')
# 매번 다른 증강 적용!
if self.transform:
image = self.transform(image)
return image, self.labels[idx]
설명
이것이 하는 일: 온라인 증강은 Dataset에서 이미지를 읽어올 때마다 랜덤한 변형을 적용하여, 사실상 무한히 다양한 학습 샘플을 만들어냅니다. 첫 번째로, torchvision과 albumentations의 차이를 이해하면 좋습니다.
torchvision의 transforms는 PyTorch에 기본 포함되어 있어 편리하지만, albumentations는 더 많은 증강 옵션과 빠른 성능을 제공합니다. 특히 의료 영상, 위성 이미지 등 특수한 도메인에서는 albumentations의 고급 증강이 유용합니다.
예를 들어 GridDistortion, ElasticTransform 같은 증강은 의료 영상 분석에 효과적입니다. 그 다음으로, RandomHorizontalFlip(p=0.5) 같은 확률 기반 증강을 보세요.
p=0.5는 50% 확률로 적용된다는 의미입니다. 이렇게 하면 같은 이미지도 어떤 때는 반전되고 어떤 때는 원본 그대로 사용됩니다.
여러 개의 확률 기반 증강을 조합하면, 매번 다른 조합이 적용되어 엄청난 다양성이 생깁니다. albumentations의 OneOf는 매우 강력한 기능입니다.
리스트의 증강 중 하나만 랜덤하게 선택합니다. 예를 들어 GaussNoise, GaussianBlur, MotionBlur 중 하나만 적용되므로, 너무 강한 증강이 중복으로 적용되는 것을 방지합니다.
이렇게 증강 강도를 조절하면서도 다양성을 유지할 수 있습니다. Dataset의 __getitem__에서 transform을 적용하는 시점이 중요합니다.
이것은 각 워커 프로세스에서 실행되므로, num_workers=8이면 8개의 프로세스가 병렬로 증강을 수행합니다. GPU가 현재 배치를 처리하는 동안, CPU 워커들이 다음 배치를 증강하고 있으므로 학습이 멈추지 않습니다.
이것이 온라인 증강이 실용적인 이유입니다. 여러분이 이 코드를 사용하면 제한된 데이터로 더 좋은 모델을 만들 수 있고, 증강 전략을 빠르게 실험할 수 있으며, 저장 공간을 절약하면서도 성능 저하 없이 학습할 수 있습니다.
특히 의료, 위성 영상, 작은 데이터셋에서 효과가 큽니다.
실전 팁
💡 증강 강도는 점진적으로 늘려보세요. 처음에는 약한 증강으로 시작해서, 성능을 보면서 강도를 높이는 것이 안전합니다.
💡 검증 데이터셋에는 증강을 적용하지 마세요. 검증은 일관된 조건에서 이루어져야 하므로 기본 전처리만 사용하세요.
💡 TTA (Test Time Augmentation)를 시도해보세요. 테스트 시에도 여러 번 증강을 적용하고 결과를 평균내면 성능이 향상될 수 있습니다.
💡 albumentations는 OpenCV 기반이라 빠릅니다. 설치는 pip install albumentations로 하면 됩니다.
💡 증강이 제대로 되는지 시각화하세요. 몇 개의 샘플을 증강해서 matplotlib로 확인하면, 너무 강한 증강이나 의도치 않은 결과를 빨리 발견할 수 있습니다.
댓글 (0)
함께 보면 좋은 카드 뉴스
데이터 증강과 정규화 완벽 가이드
머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.
ResNet과 Skip Connection 완벽 가이드
딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.
CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet
컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.
CNN 기초 Convolution과 Pooling 완벽 가이드
CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.
TensorFlow와 Keras 완벽 입문 가이드
머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.