나만의 미니 프레임워크 만들기
지금까지 뉴런(neuron), 층(layer), 네트워크(network), 역전파(backpropagation), 활성화 함수(activation function), 손실 함수(loss function), 옵티마이저(optimizer), 정규화(regularization), 초기화(initialization), 학습률 스케줄(LR schedule)을 모두 따로 만들었습니다. 이제 이 조각들을 하나의 프레임워크(framework)로 연결합니다. PyTorch도 아니고 TensorFlow도 아닙니다. 여러분 스스로 만든 프레임워크입니다.
유형: Build
언어: Python
선수 지식: Phase 03 전체(Lessons 01-09)
소요 시간: 약 120분
학습 목표
Module, Linear, ReLU, Sigmoid, Dropout, BatchNorm, Sequential, 손실 함수(loss function), 옵티마이저(optimizer), DataLoader를 갖춘 완전한 딥러닝 프레임워크(deep learning framework)를 약 500줄로 만듭니다.
Module 추상화(abstraction; forward, backward, parameters)와 학습/평가 모드 전환(train/eval mode toggle)이 필요한 이유를 설명합니다.
- 모든 구성 요소(component)를 동작하는 학습 루프(training loop)로 연결해, 원 형태 분류(circle classification) 문제에서 4층 신경망(4-layer network)을 학습시킵니다.
- 우리가 만든 프레임워크의 각 구성 요소를 PyTorch의
nn.Module, nn.Sequential, optim.Adam, DataLoader와 일대일로 대응시킵니다.
문제
지금까지 만든 구성 블록(building block)은 여러 파일에 흩어져 있습니다. 어느 파일에는 Value 클래스, 다른 파일에는 학습 루프(training loop), 또 다른 파일에는 가중치 초기화(weight initialization), 또 어딘가에는 학습률 스케줄(learning rate schedule)이 있습니다. 신경망을 학습하려면 다섯 개 레슨의 코드를 복사해서 직접 손으로 연결해야 합니다.
프레임워크(framework)는 바로 이 문제를 해결합니다. PyTorch는 nn.Module, nn.Sequential, optim.Adam, DataLoader, 그리고 이들을 묶는 학습 루프 패턴(training loop pattern)을 제공합니다. TensorFlow는 keras.Layer, keras.Sequential, keras.optimizers.Adam을 제공합니다. 이것들은 마법(magic)이 아닙니다. 신경망을 정의하고, 학습하고, 평가할 때 매번 동일한 배관 작업(plumbing)을 다시 만들지 않도록 해주는, 잘 정리된 구조적 패턴(organizational pattern)일 뿐입니다.
이 레슨에서는 같은 것을 약 500줄의 Python 코드로 직접 만듭니다. NumPy도 사용하지 않고, 외부 의존성(external dependency)도 없습니다. 어떤 순방향 신경망(feedforward network)이든 정의하고, SGD 또는 Adam으로 학습시키며, 데이터를 배치(batch) 단위로 나누고, 드롭아웃(dropout)과 배치 정규화(batch normalization)를 적용하고, 원하는 활성화 함수(activation)를 사용하고, 학습률(learning rate)을 일정에 따라 조정(schedule)할 수 있는 프레임워크입니다.
완성하고 나면 PyTorch에서 model = nn.Sequential(...)을 쓸 때 실제로 어떤 일이 일어나는지 정확히 이해하게 됩니다. model.train()과 model.eval()이 왜 있는지, optimizer.zero_grad()가 왜 별도의 호출로 분리되어 있는지도 이해하게 됩니다. 모두 직접 만들어 보았기 때문에, 모두 자연스럽게 이해됩니다.
개념
모듈 추상화(Module Abstraction)
PyTorch의 모든 층(layer)은 nn.Module을 상속합니다. 하나의 모듈(Module)은 다음 세 가지 책임을 가집니다.
- forward(): 입력으로부터 출력을 계산합니다.
- parameters(): 학습 가능한 가중치(trainable weights)를 반환합니다.
- backward(): 기울기(gradient)를 계산합니다. PyTorch에서는 자동 미분(autograd)이 처리하지만, 이 레슨의 프레임워크에서는 직접 명시적으로(explicit) 구현합니다.
선형 층(Linear layer)도 모듈이고, ReLU 활성화 함수(activation)도 모듈입니다. 드롭아웃 층(dropout layer)도 모듈이고, 배치 정규화 층(batch normalization layer)도 모듈입니다. 모두 동일한 인터페이스(interface)를 가집니다.
순차 컨테이너(Sequential Container)
nn.Sequential은 모듈들을 차례로 연결합니다. 순방향 패스(forward pass)에서는 모듈 1, 2, 3 순서로 데이터가 흘러갑니다. 역방향 패스(backward pass)에서는 같은 사슬을 거꾸로 거슬러 올라갑니다. 이 컨테이너(container) 자체도 하나의 모듈입니다. 즉 forward(), parameters(), backward()를 모두 가집니다. 이는 컴포지트 패턴(composite pattern)에 해당합니다. 모듈들의 연속(sequence)이 다시 하나의 모듈이 되는 구조입니다.
학습 모드와 평가 모드(Training/Evaluation Mode)
드롭아웃은 학습 중에는 일부 뉴런을 무작위로 0으로 만들지만, 평가 중에는 모든 값을 그대로 통과시킵니다. 배치 정규화는 학습 중에는 배치 통계량(batch statistics)을 사용하고, 평가 중에는 누적된 이동 평균(running average)을 사용합니다. train()과 eval() 메서드는 이러한 동작(behavior)을 전환(toggle)하는 역할을 합니다. 모든 모듈은 training이라는 플래그(flag)를 가집니다.
옵티마이저(Optimizer)
옵티마이저는 파라미터와 그 기울기를 사용해 파라미터를 갱신(update)합니다. SGD는 param -= lr * grad 형태이고, Adam은 모멘텀(momentum)과 분산 추정값(variance estimate)을 유지한 뒤 갱신합니다. 옵티마이저는 신경망 구조(architecture)를 알 필요가 없습니다. 단지 평탄하게 펼친(flat) 파라미터 목록과 기울기만 바라봅니다.
데이터 로더(DataLoader)
배치 처리(batching)는 두 가지 이유로 중요합니다. 첫째, 큰 문제에서는 전체 데이터셋(dataset)을 한 번에 메모리에 올릴 수 없습니다. 둘째, 미니배치 경사 하강법(mini-batch gradient descent)에서 발생하는 잡음(noise)은 지역 최소점(local minima)에서 빠져나오는 데 도움이 됩니다. 데이터 로더(DataLoader)는 데이터를 배치 단위로 나누고, 에포크(epoch) 사이에 선택적으로 섞어 줍니다(shuffle).
프레임워크 구조(Framework Architecture)
graph TD
subgraph "Modules"
Linear["Linear<br/>W*x + b"]
ReLU["ReLU<br/>max(0, x)"]
Sigmoid["Sigmoid<br/>1/(1+e^-x)"]
Dropout["Dropout<br/>random zero mask"]
BatchNorm["BatchNorm<br/>normalize activations"]
end
subgraph "Containers"
Sequential["Sequential<br/>chains modules"]
end
subgraph "Loss Functions"
MSE["MSELoss<br/>(pred - target)^2"]
BCE["BCELoss<br/>binary cross-entropy"]
end
subgraph "Optimizers"
SGD["SGD<br/>param -= lr * grad"]
Adam["Adam<br/>adaptive moments"]
end
subgraph "Data"
DataLoader["DataLoader<br/>batching + shuffle"]
end
Sequential --> |"contains"| Linear
Sequential --> |"contains"| ReLU
Sequential --> |"forward/backward"| MSE
SGD --> |"updates"| Sequential
DataLoader --> |"feeds"| Sequential
학습 루프(Training Loop)
sequenceDiagram
participant DL as DataLoader
participant M as Model
participant L as Loss
participant O as Optimizer
loop Each Epoch
DL->>M: batch of inputs
M->>M: forward pass (layer by layer)
M->>L: predictions
L->>L: compute loss
L->>M: backward pass (gradients)
M->>O: parameters + gradients
O->>M: updated parameters
O->>O: zero gradients
end
모듈 계층(Module Hierarchy)
classDiagram
class Module {
+forward(x)
+backward(grad)
+parameters()
+train()
+eval()
}
class Linear {
-weights
-biases
+forward(x)
+backward(grad)
}
class ReLU {
+forward(x)
+backward(grad)
}
class Sequential {
-modules[]
+forward(x)
+backward(grad)
+parameters()
}
Module <|-- Linear
Module <|-- ReLU
Module <|-- Sequential
Sequential *-- Module
만들어 보기
Step 1: 모듈 기반 클래스(Module Base Class)
모든 층이 구현해야 하는 추상 인터페이스(abstract interface)입니다.
class Module:
def __init__(self):
self.training = True
def forward(self, x):
raise NotImplementedError
def backward(self, grad):
raise NotImplementedError
def parameters(self):
return []
def train(self):
self.training = True
def eval(self):
self.training = False
Step 2: 선형 층(Linear Layer)
가장 기본이 되는 구성 블록(building block)입니다. 가중치(weight)와 편향(bias)을 저장하고, 순방향에서는 Wx + b를 계산하며, 역방향에서는 가중치와 입력에 대한 기울기(weight/input gradient)를 계산합니다.
import math
import random
class Linear(Module):
def __init__(self, fan_in, fan_out):
super().__init__()
std = math.sqrt(2.0 / fan_in)
self.weights = [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
self.biases = [0.0] * fan_out
self.weight_grads = [[0.0] * fan_in for _ in range(fan_out)]
self.bias_grads = [0.0] * fan_out
self.fan_in = fan_in
self.fan_out = fan_out
self.input = None
def forward(self, x):
self.input = x
output = []
for i in range(self.fan_out):
val = self.biases[i]
for j in range(self.fan_in):
val += self.weights[i][j] * x[j]
output.append(val)
return output
def backward(self, grad):
input_grad = [0.0] * self.fan_in
for i in range(self.fan_out):
self.bias_grads[i] += grad[i]
for j in range(self.fan_in):
self.weight_grads[i][j] += grad[i] * self.input[j]
input_grad[j] += grad[i] * self.weights[i][j]
return input_grad
def parameters(self):
params = []
for i in range(self.fan_out):
for j in range(self.fan_in):
params.append((self.weights, i, j, self.weight_grads))
params.append((self.biases, i, None, self.bias_grads))
return params
Step 3: 활성화 함수 모듈(Activation Modules)
ReLU, Sigmoid, Tanh를 각각 모듈로 구현합니다. 각 모듈은 역방향 패스에 필요한 값을 미리 저장(cache)해 둡니다.
class ReLU(Module):
def __init__(self):
super().__init__()
self.mask = None
def forward(self, x):
self.mask = [1.0 if v > 0 else 0.0 for v in x]
return [max(0.0, v) for v in x]
def backward(self, grad):
return [g * m for g, m in zip(grad, self.mask)]
class Sigmoid(Module):
def __init__(self):
super().__init__()
self.output = None
def forward(self, x):
self.output = []
for v in x:
v = max(-500, min(500, v))
self.output.append(1.0 / (1.0 + math.exp(-v)))
return self.output
def backward(self, grad):
return [g * o * (1 - o) for g, o in zip(grad, self.output)]
class Tanh(Module):
def __init__(self):
super().__init__()
self.output = None
def forward(self, x):
self.output = [math.tanh(v) for v in x]
return self.output
def backward(self, grad):
return [g * (1 - o * o) for g, o in zip(grad, self.output)]
Step 4: 드롭아웃 모듈(Dropout Module)
학습 중에는 일부 원소(element)를 무작위로 0으로 만듭니다. 살아남은 원소는 1/(1-p)로 비율 조정(scaling)해서 기댓값(expected value)이 동일하게 유지되도록 합니다. 평가 모드(eval)에서는 아무 동작도 하지 않습니다.
class Dropout(Module):
def __init__(self, p=0.5):
super().__init__()
self.p = p
self.mask = None
def forward(self, x):
if not self.training:
return x
self.mask = [0.0 if random.random() < self.p else 1.0 / (1 - self.p) for _ in x]
return [v * m for v, m in zip(x, self.mask)]
def backward(self, grad):
if self.mask is None:
return grad
return [g * m for g, m in zip(grad, self.mask)]
Step 5: 배치 정규화 모듈(BatchNorm Module)
배치 전체에서 특성(feature) 단위로 활성값을 평균 0, 분산 1(zero mean, unit variance)이 되도록 정규화(normalize)합니다. 평가 모드를 위해 누적 통계량(running statistics)도 함께 유지합니다.
class BatchNorm(Module):
def __init__(self, size, momentum=0.1, eps=1e-5):
super().__init__()
self.size = size
self.gamma = [1.0] * size
self.beta = [0.0] * size
self.gamma_grads = [0.0] * size
self.beta_grads = [0.0] * size
self.running_mean = [0.0] * size
self.running_var = [1.0] * size
self.momentum = momentum
self.eps = eps
self.x_norm = None
self.std_inv = None
self.batch_input = None
def forward_batch(self, batch):
batch_size = len(batch)
output_batch = []
if self.training:
mean = [0.0] * self.size
for sample in batch:
for j in range(self.size):
mean[j] += sample[j]
mean = [m / batch_size for m in mean]
var = [0.0] * self.size
for sample in batch:
for j in range(self.size):
var[j] += (sample[j] - mean[j]) ** 2
var = [v / batch_size for v in var]
self.std_inv = [1.0 / math.sqrt(v + self.eps) for v in var]
self.x_norm = []
self.batch_input = batch
for sample in batch:
normed = [(sample[j] - mean[j]) * self.std_inv[j] for j in range(self.size)]
self.x_norm.append(normed)
output = [self.gamma[j] * normed[j] + self.beta[j] for j in range(self.size)]
output_batch.append(output)
for j in range(self.size):
self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j]
self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j]
else:
std_inv = [1.0 / math.sqrt(v + self.eps) for v in self.running_var]
for sample in batch:
normed = [(sample[j] - self.running_mean[j]) * std_inv[j] for j in range(self.size)]
output = [self.gamma[j] * normed[j] + self.beta[j] for j in range(self.size)]
output_batch.append(output)
return output_batch
def forward(self, x):
result = self.forward_batch([x])
return result[0]
def backward(self, grad):
if self.x_norm is None:
return grad
for j in range(self.size):
self.gamma_grads[j] += self.x_norm[0][j] * grad[j]
self.beta_grads[j] += grad[j]
return [grad[j] * self.gamma[j] * self.std_inv[j] for j in range(self.size)]
def parameters(self):
params = []
for j in range(self.size):
params.append((self.gamma, j, None, self.gamma_grads))
params.append((self.beta, j, None, self.beta_grads))
return params
Step 6: 순차 컨테이너(Sequential Container)
모듈들을 차례로 연결합니다. 순방향(forward)은 왼쪽에서 오른쪽으로, 역방향(backward)은 오른쪽에서 왼쪽으로 흐릅니다.
class Sequential(Module):
def __init__(self, *modules):
super().__init__()
self.modules = list(modules)
def forward(self, x):
for module in self.modules:
x = module.forward(x)
return x
def backward(self, grad):
for module in reversed(self.modules):
grad = module.backward(grad)
return grad
def parameters(self):
params = []
for module in self.modules:
params.extend(module.parameters())
return params
def train(self):
self.training = True
for module in self.modules:
module.train()
def eval(self):
self.training = False
for module in self.modules:
module.eval()
Step 7: 손실 함수(Loss Functions)
평균 제곱 오차(MSE)와 이진 교차 엔트로피(Binary Cross-Entropy)를 구현합니다. 각각 손실 값(loss value)을 반환하고, backward()에서는 기울기를 반환합니다.
class MSELoss:
def __call__(self, predicted, target):
self.predicted = predicted
self.target = target
n = len(predicted)
self.loss = sum((p - t) ** 2 for p, t in zip(predicted, target)) / n
return self.loss
def backward(self):
n = len(self.predicted)
return [2 * (p - t) / n for p, t in zip(self.predicted, self.target)]
class BCELoss:
def __call__(self, predicted, target):
self.predicted = predicted
self.target = target
eps = 1e-7
n = len(predicted)
self.loss = 0
for p, t in zip(predicted, target):
p = max(eps, min(1 - eps, p))
self.loss += -(t * math.log(p) + (1 - t) * math.log(1 - p))
self.loss /= n
return self.loss
def backward(self):
eps = 1e-7
n = len(self.predicted)
grads = []
for p, t in zip(self.predicted, self.target):
p = max(eps, min(1 - eps, p))
grads.append((-t / p + (1 - t) / (1 - p)) / n)
return grads
Step 8: SGD와 Adam 옵티마이저(SGD and Adam Optimizers)
둘 다 파라미터 목록(parameter list)을 받아, 기울기를 이용해 가중치를 갱신합니다.
class SGD:
def __init__(self, parameters, lr=0.01):
self.params = parameters
self.lr = lr
def step(self):
for container, i, j, grad_container in self.params:
if j is not None:
container[i][j] -= self.lr * grad_container[i][j]
else:
container[i] -= self.lr * grad_container[i]
def zero_grad(self):
for container, i, j, grad_container in self.params:
if j is not None:
grad_container[i][j] = 0.0
else:
grad_container[i] = 0.0
class Adam:
def __init__(self, parameters, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
self.params = parameters
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.t = 0
self.m = [0.0] * len(parameters)
self.v = [0.0] * len(parameters)
def step(self):
self.t += 1
for idx, (container, i, j, grad_container) in enumerate(self.params):
if j is not None:
g = grad_container[i][j]
else:
g = grad_container[i]
self.m[idx] = self.beta1 * self.m[idx] + (1 - self.beta1) * g
self.v[idx] = self.beta2 * self.v[idx] + (1 - self.beta2) * g * g
m_hat = self.m[idx] / (1 - self.beta1 ** self.t)
v_hat = self.v[idx] / (1 - self.beta2 ** self.t)
update = self.lr * m_hat / (math.sqrt(v_hat) + self.eps)
if j is not None:
container[i][j] -= update
else:
container[i] -= update
def zero_grad(self):
for container, i, j, grad_container in self.params:
if j is not None:
grad_container[i][j] = 0.0
else:
grad_container[i] = 0.0
Step 9: 데이터 로더(DataLoader)
데이터를 배치 단위로 나누고, 각 에포크마다 선택적으로 데이터를 섞어 줍니다(shuffle).
class DataLoader:
def __init__(self, data, batch_size=32, shuffle=True):
self.data = data
self.batch_size = batch_size
self.shuffle = shuffle
def __iter__(self):
indices = list(range(len(self.data)))
if self.shuffle:
random.shuffle(indices)
for start in range(0, len(indices), self.batch_size):
batch_indices = indices[start:start + self.batch_size]
batch = [self.data[i] for i in batch_indices]
inputs = [item[0] for item in batch]
targets = [item[1] for item in batch]
yield inputs, targets
def __len__(self):
return (len(self.data) + self.batch_size - 1) // self.batch_size
Step 10: 원 형태 분류(Circle Classification)에서 4층 신경망 학습
지금까지 만든 모든 요소를 연결합니다. 모델을 정의하고, 손실 함수와 옵티마이저를 고른 뒤, 학습 루프를 실행합니다.
def make_circle_data(n=500, seed=42):
random.seed(seed)
data = []
for _ in range(n):
x = random.uniform(-2, 2)
y = random.uniform(-2, 2)
label = 1.0 if x * x + y * y < 1.5 else 0.0
data.append(([x, y], [label]))
return data
def train():
random.seed(42)
model = Sequential(
Linear(2, 16),
ReLU(),
Linear(16, 16),
ReLU(),
Linear(16, 8),
ReLU(),
Linear(8, 1),
Sigmoid(),
)
criterion = BCELoss()
optimizer = Adam(model.parameters(), lr=0.01)
data = make_circle_data(500)
split = int(len(data) * 0.8)
train_data = data[:split]
test_data = data[split:]
loader = DataLoader(train_data, batch_size=16, shuffle=True)
model.train()
for epoch in range(100):
total_loss = 0
total_correct = 0
total_samples = 0
for batch_inputs, batch_targets in loader:
batch_loss = 0
for x, t in zip(batch_inputs, batch_targets):
pred = model.forward(x)
loss = criterion(pred, t)
batch_loss += loss
optimizer.zero_grad()
grad = criterion.backward()
model.backward(grad)
optimizer.step()
predicted_class = 1.0 if pred[0] >= 0.5 else 0.0
if predicted_class == t[0]:
total_correct += 1
total_samples += 1
total_loss += batch_loss
avg_loss = total_loss / total_samples
accuracy = total_correct / total_samples * 100
if epoch % 10 == 0 or epoch == 99:
print(f"Epoch {epoch:3d} | Loss: {avg_loss:.6f} | Train Accuracy: {accuracy:.1f}%")
model.eval()
correct = 0
for x, t in test_data:
pred = model.forward(x)
predicted_class = 1.0 if pred[0] >= 0.5 else 0.0
if predicted_class == t[0]:
correct += 1
test_accuracy = correct / len(test_data) * 100
print(f"\nTest Accuracy: {test_accuracy:.1f}% ({correct}/{len(test_data)})")
return model, test_accuracy
사용하기
방금 만든 것과 같은 구조를 PyTorch로 쓰면 다음과 같습니다.
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
model = nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 16),
nn.ReLU(),
nn.Linear(16, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid(),
)
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(100):
model.train()
for inputs, targets in dataloader:
optimizer.zero_grad()
predictions = model(inputs)
loss = criterion(predictions, targets)
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
test_predictions = model(test_inputs)
구조는 동일합니다. Sequential, Linear, ReLU, Sigmoid, BCELoss, Adam, zero_grad, backward, step, train, eval이 모두 일대일로 대응됩니다. 차이가 있다면, PyTorch는 자동 미분(autograd)을 자동으로 처리해 주어 모듈마다 backward()를 직접 구현할 필요가 없고, GPU에서 실행되며, 수년에 걸쳐 잘 최적화되어 왔다는 점입니다. 그러나 그 뼈대 구조는 동일합니다.
이제 PyTorch 코드를 볼 때 각 줄(line)에서 어떤 일이 일어나는지 정확히 알 수 있게 됩니다. 그러한 이해가 바로 이 레슨의 핵심입니다.
산출물 만들기
이 레슨의 최종 산출물은 다음과 같습니다.
outputs/prompt-framework-architect.md: 프레임워크 추상화(framework abstraction)를 사용해 신경망 구조(neural network architecture)를 설계하기 위한 프롬프트(prompt)
연습문제
- (쉬움) 다중 클래스 분류(multi-class classification)를 위한
SoftmaxCrossEntropyLoss 클래스를 추가합니다. 예측값에 소프트맥스(softmax)를 적용하고 교차 엔트로피 손실(cross-entropy loss)을 계산하며, 결합된 역방향 패스(combined backward pass)를 처리합니다. 3개 클래스로 구성된 나선형 데이터셋(3-class spiral dataset)에서 검증합니다.
- (중간) 옵티마이저에 학습률 스케줄링(learning rate scheduling)을 구현합니다.
set_lr() 메서드를 추가하고, Lesson 09의 코사인 스케줄(cosine schedule)을 연결합니다. 원 분류기(circle classifier)를 워밍업과 코사인 스케줄(warmup + cosine)을 적용한 경우와 일정한 학습률(constant LR)을 사용한 경우로 각각 학습해 비교합니다.
- (중간)
Sequential에 save()와 load() 메서드를 추가해 모든 가중치를 JSON 파일로 직렬화(serialize)하고 다시 불러옵니다. 불러온 모델이 원래 모델과 동일한 예측값을 내는지 검증합니다.
- (중간) Adam 옵티마이저에 가중치 감쇠(weight decay; L2 정규화)를 구현합니다. 각 갱신 단계마다 가중치를 0 쪽으로 조금씩 줄이는
weight_decay 파라미터를 추가합니다. decay=0과 decay=0.01로 학습한 결과를 비교합니다.
- (어려움) 표본 단위 학습 루프(per-sample training loop)를 올바른 형태의 미니배치 기울기 누적(mini-batch gradient accumulation)으로 바꿉니다. 배치 안의 모든 표본에 대해 기울기를 누적한 뒤 배치 크기로 나누고, 옵티마이저 단계(optimizer step)는 한 번만 수행합니다. 이러한 변경이 수렴 속도(convergence speed)에 어떤 영향을 주는지 측정합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 모듈(Module) | "층(layer)" | 프레임워크의 가장 기본이 되는 추상화. forward(), backward(), parameters()를 가진 모든 것 |
| 순차 컨테이너(Sequential) | "층을 순서대로 쌓는 것" | 순방향에서는 차례로, 역방향에서는 역순으로 모듈을 적용하는 컨테이너 |
| 순방향 패스(Forward pass) | "신경망 실행" | 입력을 각 모듈에 순서대로 통과시켜 출력을 계산하는 과정 |
| 역방향 패스(Backward pass) | "기울기 계산" | 손실의 기울기를 모듈 역순으로 전파해 파라미터 기울기를 계산하는 과정 |
| 파라미터(Parameters) | "학습 가능한 가중치" | 옵티마이저가 갱신할 수 있는 신경망의 모든 값. 가중치와 편향 |
| 옵티마이저(Optimizer) | "가중치를 갱신하는 도구" | 기울기를 사용해 파라미터를 갱신하는 알고리즘. SGD, Adam 등이 이에 해당 |
| 데이터 로더(DataLoader) | "데이터를 공급하는 도구" | 데이터셋을 배치 단위로 나누고, 에포크 사이에 선택적으로 섞어주는 반복자(iterator) |
| 학습 모드(Training mode) | "model.train()" | 드롭아웃이나 배치 통계 기반 배치 정규화처럼 확률적 동작(stochastic behavior)을 켜는 플래그 |
| 평가 모드(Evaluation mode) | "model.eval()" | 드롭아웃을 끄고, 배치 정규화에서 누적 통계량(running statistics)을 사용하도록 하는 플래그 |
| 기울기 초기화(Zero grad) | "기울기 리셋" | 다음 배치의 기울기를 계산하기 전에, 모든 파라미터 기울기를 0으로 다시 설정(reset)하는 작업 |
더 읽을거리
- Paszke 외, "PyTorch: An Imperative Style, High-Performance Deep Learning Library" (2019) — PyTorch의 설계 결정(design decision)을 설명한 논문
- Chollet, "Deep Learning with Python, Second Edition" (2021) — 3장에서 동일한 모듈/층 추상화(module/layer abstraction) 관점으로 Keras 내부 구조를 다룹니다.
- Johnson, "Tiny-DNN" (https://github.com/tiny-dnn/tiny-dnn) — 프레임워크의 내부 구조를 이해하기 좋은, 헤더 전용(header-only) C++ 딥러닝 프레임워크
- micrograd — 아주 작은 자동 미분 엔진(autograd engine)을 통해 프레임워크의 핵심 구조를 살펴볼 수 있습니다.