데이터¶
dt.DataModule은 학습·검증 dataloader를 한 객체로 묶는다.
서브클래스가 구현할 것은 get_dataloader(train) 하나뿐이다.
train_dataloader()와 val_dataloader()는 각각 train=True/False로
이것을 부르는 얇은 래퍼다.
텐서로 시작할 때¶
이미 메모리에 텐서가 있으면 get_tensorloader가 TensorDataset과
DataLoader 조립을 대신한다. train=True면 셔플한다.
class SyntheticRegression(dt.DataModule):
def __init__(self, n=200, batch_size=32):
super().__init__()
self.save_hyperparameters()
torch.manual_seed(0)
self.X = torch.randn(n, 2)
self.y = self.X @ torch.tensor([[2.0], [-3.4]]) + 4.2
def get_dataloader(self, train):
idx = slice(0, 160) if train else slice(160, None)
return self.get_tensorloader((self.X, self.y), train, idx)
세 번째 인자가 학습·검증을 가르는 슬라이스다. 모든 텐서에 같은 슬라이스가 적용되므로 X와 y가 어긋날 일이 없다.
호출 순서¶
super().__init__() 먼저, save_hyperparameters() 나중
DataModule.__init__은 root, num_workers, batch_size 기본값을
hparams에 넣는다. 순서를 바꾸면 당신이 넘긴 batch_size가
부모 기본값 32로 덮인다.
torchvision 데이터셋 쓰기¶
torchvision은 deeptool의 의존성이 아니다. 데이터셋을 끼워 팔지 않는다는
설계 결정이고, 필요한 사람만 설치하면 된다.
get_tensorloader는 텐서 묶음용이므로 여기선 쓰지 않는다.
torchvision Dataset을 DataLoader로 직접 감싼다.
import torchvision
from torch.utils import data
from torchvision import transforms
class FashionMNIST(dt.DataModule):
def __init__(self, batch_size=256, root="./data"):
super().__init__()
self.save_hyperparameters()
trans = transforms.ToTensor()
self.train_ds = torchvision.datasets.FashionMNIST(
root=root, train=True, transform=trans, download=True)
self.val_ds = torchvision.datasets.FashionMNIST(
root=root, train=False, transform=trans, download=True)
def get_dataloader(self, train):
ds = self.train_ds if train else self.val_ds
return data.DataLoader(ds, self.batch_size, shuffle=train,
num_workers=self.num_workers)
확인:
d = FashionMNIST()
X, y = next(iter(d.train_dataloader()))
print(X.shape, X.dtype, y.shape, y.dtype)
print(len(d.train_dataloader()), len(d.val_dataloader()))
Hugging Face 데이터셋¶
datasets도 deeptool의 의존성이 아니다. 필요할 때만 설치한다.
deeptool의 배치 규약이 이미지와 텍스트를 모두 받아낸다. 손댈 것은
collate 하나뿐이다.
이미지 batch = (image, label) → self(*batch[:-1]) = self(image)
텍스트 batch = (input_ids, attention_mask, label) → self(*batch[:-1]) = self(ids, mask)
HF 가 주는 두 가지 함정¶
dict 를 내놓는다. HF 데이터셋은 {"image": …, "label": …} 형태다.
deeptool은 batch[:-1]을 입력으로, batch[-1]을 정답으로 읽으므로 그대로는
안 된다.
이미지가 uint8 0-255 다. with_format("torch")는 텐서로 바꿔줄 뿐
정규화하지 않는다. torchvision ToTensor()가 하던 /255와 float 변환이 없다.
이름 때문에 특히 놓치기 쉽다. shape은 (N, 1, 28, 28)로 이미 channel-first라
그건 손댈 필요가 없다.
이미지¶
import torch
from torch.utils.data import DataLoader, default_collate
from datasets import load_dataset
class HFMnist(dt.DataModule):
def __init__(self, batch_size=64):
super().__init__()
self.save_hyperparameters()
d = load_dataset("ylecun/mnist")
self.splits = {True: d["train"].with_format("torch"),
False: d["test"].with_format("torch")}
def get_dataloader(self, train):
def collate(examples):
b = default_collate(examples)
return b["image"].float() / 255, b["label"]
return DataLoader(self.splits[train], self.batch_size,
shuffle=train, collate_fn=collate)
collate가 두 함정을 한 번에 해결한다 — dict를 튜플로 바꾸고 /255로
정규화한다. 그 뒤로는 평소와 같다.
data = HFMnist()
trainer = dt.Trainer(max_epochs=5, patience=3)
trainer.fit(model, data) # model 은 dt.Module 서브클래스
trainer.restore_best()
trainer.predict(data).accuracy
텍스트¶
토크나이저가 필요하다. 이것도 deeptool의 의존성이 아니다.
from transformers import AutoTokenizer
TOK = AutoTokenizer.from_pretrained("distilbert-base-uncased")
class IMDB(dt.DataModule):
def __init__(self, batch_size=16, max_length=128):
super().__init__()
self.save_hyperparameters()
d = load_dataset("stanfordnlp/imdb")
self.splits = {True: d["train"].shuffle(seed=0),
False: d["test"].shuffle(seed=0)}
def get_dataloader(self, train):
def collate(examples):
enc = TOK([e["text"] for e in examples], truncation=True,
padding="max_length", max_length=self.max_length,
return_tensors="pt")
labels = torch.tensor([e["label"] for e in examples])
return enc["input_ids"], enc["attention_mask"], labels
return DataLoader(self.splits[train], self.batch_size,
shuffle=train, collate_fn=collate)
여기서 with_format("torch")를 쓰지 않는다. 문자열은 어차피 텐서가 안 되고,
토크나이저가 원문 str을 받아야 하기 때문이다.
배치가 3원소가 되지만 deeptool은 그대로 받는다. batch[:-1]이
(input_ids, attention_mask)이므로 forward가 인자 둘을 받으면 된다.
class TextNet(dt.Module):
def __init__(self, vocab_size=TOK.vocab_size, dim=64, lr=1e-3):
super().__init__()
self.save_hyperparameters()
self.emb = nn.Embedding(vocab_size, dim, padding_idx=TOK.pad_token_id)
self.head = nn.Linear(dim, 2)
def forward(self, input_ids, attention_mask):
h = self.emb(input_ids) * attention_mask.unsqueeze(-1)
return self.head(h.sum(1) / attention_mask.sum(1, keepdim=True))
형식은 균일하지 않다¶
여기까지 읽고 "HF 데이터셋은 다 이렇겠지" 하면 안 된다. 같은 것은 dict 껍데기 하나뿐이다.
| 데이터셋 | with_format("torch") 후 |
default_collate 후 |
|---|---|---|
ylecun/mnist |
Tensor, Tensor |
(4,1,28,28) uint8, (4,) int64 |
stanfordnlp/imdb |
str, Tensor |
list, (4,) int64 |
rajpurkar/squad |
str×4, 중첩 dict |
list×4, 중첩 dict |
Helsinki-NLP/opus_books |
str, 중첩 dict |
list, 중첩 dict |
with_format("torch")는 문자열을 텐서로 바꾸지 못한다. 그대로 str로
남긴다. HF의 한계가 아니라, 토크나이저가 모델마다 달라 데이터셋 단에서 정할 수
없기 때문이다.
QA와 번역은 중첩 dict라 default_collate로도 텐서가 되지 않는다. 이런 태스크는
전처리 파이프라인이 본체이고 데이터로더는 마지막 단계다. deeptool이 대신해줄
수 있는 부분이 아니다.
왜 HFDataModule을 만들지 않았나
datasets는 설치하면 35개, transformers는 46개 패키지를 끌고 온다.
런타임 의존성을 torch·matplotlib·ipython 셋으로 유지하는 것이 이
라이브러리의 방침이고, 절감되는 코드는 이미지 기준 6줄이다.
모달리티마다 collate가 완전히 달라 하나로 묶이지도 않는다.
위 두 예제는 2026-08-03에 실행해 확인했다. MNIST 4,000/1,000 샘플에 선형
모델 5 epoch로 정확도 0.857, IMDB 800/400 샘플에 embedding + mean-pool
6 epoch로 0.6575가 나왔다. 두 경우 모두 predict·restore_best·patience가
수정 없이 동작했다.
두 가지 함정¶
이미지 배치는 (N, 1, 28, 28)이다. 4차원이므로 모델 첫 층에
nn.Flatten()이 필요하다. 없으면 shape 에러가 난다.
num_workers 기본값이 0인 이유. 노트북에서 0보다 크게 두면 macOS의
프로세스 시작 방식 때문에 멈추는 일이 있다. 스크립트로 돌릴 때만 올린다.
검증 데이터가 없을 때¶
get_dataloader(train=False)가 None을 돌려주면 검증을 건너뛴다.
def get_dataloader(self, train):
if not train:
return None
return self.get_tensorloader((self.X, self.y), train)
이 경우 history["val_loss"]는 비어 있고,
restore_best()와 patience는 쓸 수 없다.
둘 다 검증 손실을 기준으로 삼기 때문이다.