2.3 미니 배치와 데이터 로더

유명곤·2026년 9월 13일

데이터를 한 바퀴 도는 동안에도, 모델은 여러 번 달라질 수 있다. 전체 데이터를 작은 묶음으로 나누고, 한 묶음을 처리할 때마다 매개변수를 갱신하기 때문이다. 이렇게 하면 전체 데이터를 한꺼번에 모델에 넣지 않고도 학습을 진행할 수 있다. 이때 한 번의 계산에 사용할 데이터 묶음이 미니 배치(mini-batch)이며, PyTorch의 DataLoader는 이 묶음을 차례로 전달한다.

한 번의 갱신에 사용할 데이터

미니 배치는 전체 학습 데이터 중 한 번의 학습 계산에 사용하는 작은 표본 묶음이다. 배치 크기(batch size)는 한 묶음에 담도록 설정한 표본 수이다. 입력과 정답이 있는 지도 학습에서는 입력 하나와 그에 대응하는 정답을 하나의 표본으로 다룬다.

전체 표본 수가 NN일 때, 모든 표본으로 손실과 기울기를 계산한 뒤 한 번 갱신하는 방식이 배치 경사 하강법(batch gradient descent)이다. 미니 배치 경사 하강법(mini-batch gradient descent)은 그중 일부로 계산한 기울기를 이용해 갱신한다. 표본 하나씩 갱신하는 방식은 확률적 경사 하강법(stochastic gradient descent)의 기본 형태이며, 실제로는 미니 배치를 사용하는 방식까지 넓게 SGD라고 부르기도 한다.

미니 배치에 속한 표본의 인덱스 집합을 B\mathcal{B}, 그 안의 실제 표본 수를 mm이라 하자. 매개변수 θ\boldsymbol{\theta}에서 표본 ii의 손실이 ℓi(θ)\ell_i(\boldsymbol{\theta})라면, 미니 배치의 평균 손실은 다음과 같다.

JB(θ)=1m∑i∈Bℓi(θ)J_{\mathcal{B}}(\boldsymbol{\theta}) = \frac{1}{m}\sum_{i\in\mathcal{B}}\ell_i(\boldsymbol{\theta})

이 식은 현재 묶음에 들어온 표본의 손실만 평균낸다는 뜻이다. 마지막 묶음이 설정한 배치 크기보다 작다면, 평균을 낼 때도 그 묶음의 실제 표본 수를 사용한다.

학습률을 η\eta, tt번째로 처리하는 미니 배치를 Bt\mathcal{B}_t라 하면, 기본적인 경사 하강 갱신은 다음과 같다.

θt+1=θt−η∇θJBt(θt)\boldsymbol{\theta}_{t+1} = \boldsymbol{\theta}_t - \eta\nabla_{\boldsymbol{\theta}}J_{\mathcal{B}_t}(\boldsymbol{\theta}_t)

다음 배치는 이미 갱신된 매개변수로 처리한다. 따라서 같은 데이터를 모두 사용하더라도, 여러 미니 배치로 나누어 순서대로 갱신한 결과가 전체 데이터로 한 번 갱신한 결과와 일반적으로 같지는 않다.

배치를 작게 하면 한 번의 모델 계산에 필요한 데이터와 중간 계산 결과의 메모리를 줄일 수 있고, 전체 데이터를 다 보기 전에 갱신할 수 있다. 반면 각 배치의 구성에 따라 기울기가 달라져 갱신 방향의 변동이 커질 수 있다. 배치를 크게 하면 여러 표본을 함께 계산하는 효율을 높일 수 있지만 메모리 사용량도 늘어난다. 배치 크기는 메모리, 계산 효율, 학습의 변화 양상을 함께 보고 정하는 값이다. 반드시 2의 거듭제곱이어야 하거나, 작을수록 학습이 빨라지는 것은 아니다.

에포크와 갱신 횟수는 다르다

에포크(epoch)는 학습 데이터를 한 차례 순회하는 단위이다. 이터레이션(iteration)은 학습 루프의 반복 한 번이며, 여기서는 미니 배치 하나를 받아 예측·손실 계산·미분·매개변수 갱신을 수행하는 한 번을 뜻한다.

배치마다 한 번씩 갱신하는 학습에서는, 한 에포크 안에 배치 수만큼의 갱신이 일어난다. 에포크 수가 같아도 배치 크기가 다르면 갱신 횟수는 달라질 수 있다.

표본 NN개를 중복 없이 한 번씩 읽고 배치 크기를 BB로 설정하면, 마지막에 남은 표본까지 사용하는 경우 한 에포크의 배치 수는 다음과 같다.

batches per epoch=⌈NB⌉\text{batches per epoch}=\left\lceil\frac{N}{B}\right\rceil

⌈⋅⌉\lceil\cdot\rceil는 올림이다. 나머지가 있으면 설정한 크기보다 작은 배치가 하나 더 생긴다. 마지막 불완전 배치를 제외하도록 설정하면 배치 수는 ⌊N/B⌋\lfloor N/B\rfloor가 되고, 그 순회에서는 제외된 표본을 학습에 사용하지 않는다.

이 구분은 학습 코드의 중첩 반복문으로 이어진다. 바깥 반복문은 에포크를 세고, 안쪽 반복문은 해당 에포크의 배치를 차례로 처리한다. 매개변수 갱신은 안쪽 반복문에서 수행한다.

표본을 꺼내는 역할과 배치를 만드는 역할

PyTorch는 데이터를 읽는 방법을 모델의 학습 코드에서 분리한다. Dataset은 표본을 꺼내는 방법을 제공하고, 데이터 로더(data loader)인 DataLoader는 그 표본들을 배치로 묶어 반복문에 전달한다.

Dataset: 표본 하나를 어떻게 가져오는가

이 글에서 다루는 Dataset은 인덱스로 표본을 가져오는 형태(map-style dataset)이다. dataset[i]로 ii번째 표본을 가져오고, len(dataset)으로 표본 수를 확인할 수 있다.

입력과 정답이 이미 텐서로 준비되어 있다면 TensorDataset을 사용할 수 있다. TensorDataset(X, y)는 같은 인덱스의 X[i]와 y[i]를 한 쌍으로 반환한다. 따라서 두 텐서의 첫 번째 축 길이, 즉 표본 수가 같아야 한다. 입력의 특성 수와 정답의 차원까지 같을 필요는 없다.

표본을 읽는 규칙을 직접 정해야 한다면 Dataset을 상속해 클래스를 만든다. 이때 역할은 다음과 같이 나뉜다.

메서드역할
__init__텐서, 파일 경로, 변환 규칙 등 표본을 가져오는 데 필요한 정보를 준비한다.
__len__데이터셋의 표본 수를 반환한다.
__getitem__주어진 인덱스의 표본을 읽고, 필요한 변환을 거쳐 반환한다.

전체 데이터를 처음부터 메모리에 올려야 하는 것은 아니다. 파일 경로만 준비해 두고 __getitem__에서 필요한 표본을 읽을 수도 있다. 반대로 TensorDataset으로 이미 메모리에 올린 텐서를 감쌌다면, 이후 배치로 나누더라도 원본 텐서 전체가 차지하는 메모리까지 사라지는 것은 아니다.

DataLoader: 표본을 어떤 순서와 크기로 전달하는가

기본적인 자동 배치 구성에서 DataLoader는 여러 표본을 가져와 입력끼리, 정답끼리 묶는다. 각 표본의 텐서 shape가 서로 맞으면 맨 앞에 배치 축이 추가된다. 입력 특성이 dd개이고 정답이 하나라면 다음 관계가 성립한다.

구분입력 shape정답 shape
표본 하나(d,)(1,)
표본 mm개를 묶은 배치(m, d)(m, 1)

batch_size는 배치에 담을 표본 수를 정한다. drop_last=False가 기본값이므로 마지막에 남은 표본도 하나의 배치로 반환한다. drop_last=True로 설정하면 설정한 크기를 채우지 못한 마지막 배치를 제외한다.

shuffle=True는 표본을 읽을 인덱스 순서를 무작위로 정한다. 일반적인 에포크 반복문에서는 새로운 순회를 시작할 때마다 순서를 다시 정하므로 배치 구성과 처리 순서가 달라질 수 있다. 이때 입력과 정답을 따로 섞는 것이 아니므로 표본의 입력·정답 쌍은 유지된다. 섞기는 고정된 배치 구성과 처리 순서의 영향을 줄이는 방법이며, 데이터의 시간 순서 자체가 중요하다면 무조건 적용해서는 안 된다.

num_workers는 데이터 로딩에 사용할 보조 프로세스 수이며, 기본값 0에서는 주 프로세스가 데이터를 읽는다. DataLoader는 이렇게 데이터를 전달하는 역할을 맡고, 모델의 예측·손실 계산·매개변수 갱신은 학습 루프가 수행한다.

다섯 표본을 세 번의 갱신으로 연결하기

원문의 회귀 데이터를 이용해 표본에서 배치, 학습 루프로 이어지는 과정을 확인하자. 각 표본은 입력 특성 세 개와 정답 하나를 가진다. 전체 입력 X의 shape는 (5, 3), 정답 y의 shape는 (5, 1)이다.

배치의 크기와 shape 확인하기

먼저 묶이는 순서를 확인하기 위해 shuffle=False로 설정한다. 아래 코드는 PyTorch 2.13.0, CPU에서 실행한 예시이다.

import torch
from torch.utils.data import DataLoader, Dataset, TensorDataset

X = torch.tensor([
    [73, 80, 75],
    [93, 88, 93],
    [89, 91, 90],
    [96, 98, 100],
    [73, 66, 70],
], dtype=torch.float32)
y = torch.tensor([[152], [185], [180], [196], [142]], dtype=torch.float32)

dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=2, shuffle=False)

print(len(dataset), len(loader))
for x_batch, y_batch in loader:
    print(tuple(x_batch.shape), tuple(y_batch.shape))
5 3
(2, 3) (2, 1)
(2, 3) (2, 1)
(1, 3) (1, 1)

len(dataset)은 표본 수인 5이고, len(loader)는 배치 수인 3이다. 원문에 나열된 순서대로 표본에 1부터 번호를 붙이면 다음과 같이 묶인다.

배치포함한 표본 번호실제 배치 크기
첫 번째1, 22
두 번째3, 42
세 번째51

마지막 배치에서는 표본 수를 나타내는 첫 번째 축만 1로 줄어든다. 입력 특성 세 개와 정답 하나라는 구조는 유지된다. drop_last=True로 바꾸면 세 번째 배치를 제외하여 배치 수가 2가 된다.

배치를 받을 때마다 모델 갱신하기

앞 코드에 이어 다음 학습 루프를 실행할 수 있다. 학습에서는 shuffle=True로 로더를 새로 만들고, 세 입력을 하나의 값으로 예측하는 nn.Linear(3, 1) 모델을 사용한다. 모델과 옵티마이저는 에포크 반복문 바깥에서 한 번 만든다.

from torch import nn

train_loader = DataLoader(dataset, batch_size=2, shuffle=True)
model = nn.Linear(3, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-5)
loss_fn = nn.MSELoss()

for epoch in range(20):
    for x_batch, y_batch in train_loader:
        prediction = model(x_batch)
        loss = loss_fn(prediction, y_batch)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

배치의 실제 표본 수가 mm이면 입력은 (m, 3), 예측과 정답은 모두 (m, 1)이다. 이 예시는 표본마다 출력이 하나이므로 기본 nn.MSELoss()는 현재 배치의 표본별 제곱 오차를 평균낸다. 마지막 배치가 표본 하나여도 그 배치 크기에 맞게 평균을 계산한다.

안쪽 반복문 한 번이 끝날 때마다 매개변수가 갱신되고, 다음 배치는 그 값을 사용한다. 한 에포크에 배치가 3개이고 range(20)은 정확히 20번 반복하므로, 이 코드의 갱신 횟수는 3×20=603\times20=60번이다. drop_last=True를 사용한다면 한 에포크의 배치가 2개가 되어 갱신은 40번 일어난다.

이때 loss는 현재 배치에서, 갱신 직전에 계산한 손실이다. 다음 배치는 표본 구성도 매개변수도 달라질 수 있으므로, 배치별 손실이 매번 감소해야 하는 것은 아니다. 마지막 배치의 손실 하나를 전체 학습 데이터의 손실로 해석해서도 안 된다.

데이터를 가져오는 규칙만 바꾸기

같은 데이터를 사용자 정의 클래스로 제공하려면, 앞에서 설명한 표본 수와 인덱스 접근 규칙을 구현하면 된다. 아래 코드는 앞에서 만든 X, y와 import를 그대로 사용한다.

class RegressionDataset(Dataset):
    def __init__(self, inputs, targets):
        self.inputs = inputs
        self.targets = targets

    def __len__(self):
        return len(self.inputs)

    def __getitem__(self, index):
        return self.inputs[index], self.targets[index]

custom_dataset = RegressionDataset(X, y)
train_loader = DataLoader(custom_dataset, batch_size=2, shuffle=True)

이 클래스는 같은 인덱스의 입력과 정답을 돌려주므로, 위 예시에서는 TensorDataset(X, y)와 같은 표본을 제공한다. 이렇게 만든 train_loader를 앞의 학습 루프에 사용해도 모델의 입력·출력 구조는 유지된다. 실제 파일 읽기나 변환이 필요해지면 표본을 반환하는 규칙을 바꾸고, 학습 루프는 배치를 받는 형태로 유지할 수 있다.

요약·활용

미니 배치는 한 번의 계산과 갱신에 사용할 데이터의 범위를 정한다. 에포크는 데이터 순회 횟수이고, 배치마다 갱신하는 학습에서는 한 에포크 안에도 여러 번의 갱신이 일어난다.

Dataset은 표본 하나를 가져오는 규칙을, DataLoader는 표본의 순서와 배치 구성을 맡는다. 이 구분 덕분에 데이터가 저장된 형태를 바꾸더라도 모델과 학습 루프의 연결 방식을 유지할 수 있다.

학습 코드를 읽을 때는 표본 수, 배치 크기, 마지막 배치 처리, 배치당 갱신 횟수를 함께 확인하면 된다. 이 네 가지를 구분해야 모델이 데이터를 얼마나 읽었고, 그동안 몇 번 갱신되었는지 정확히 해석할 수 있다.

참고자료

profile
Werde, der du bist!

0개의 댓글