전체 코드 구조
# 1. 데이터셋 정의
dataset = CustomDataset('data.csv', device)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
# 2. 모델 정의
model = MLP(input_dim, hidden_dim, output_dim).to(device)
# 3. 손실 함수 & 옵티마이저
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 4. 훈련 루프
for epoch in range(epochs):
for x, y in dataloader:
y_pred = model(x) # (1) 순전파
loss = criterion(y_pred, y) # (2) 손실 계산
optimizer.zero_grad() # (3) 그래디언트 초기화
loss.backward() # (4) 역전파
optimizer.step() # (5) 가중치 업데이트1. 커스텀 데이터셋 정의
코드
class CustomDataset(Dataset):
def __init__(self, csv_file, device):
self.data = pd.read_csv(csv_file)
self.features = self.data.iloc[:, :-1].values # X (입력)
self.labels = self.data.iloc[:, -1].values # y (정답)
# 라벨이 0부터 시작하지 않으면 shift
if self.labels.min() != 0:
self.labels = self.labels - self.labels.min()
self.device = device
def __len__(self):
return len(self.data)
def __getitem__(self, index):
x = torch.tensor(self.features[index], dtype=torch.float32).to(self.device)
y = torch.tensor(self.labels[index], dtype=torch.long).to(self.device)
return x, y상세 설명
init: 초기화
self.data = pd.read_csv(csv_file)
self.features = self.data.iloc[:, :-1].values # 마지막 열 제외
self.labels = self.data.iloc[:, -1].values # 마지막 열만| 동작 | 설명 |
|---|---|
| CSV 로드 | pandas로 데이터 파일 읽기 |
| 특성 분리 | 마지막 열을 제외한 모든 열 → 입력 데이터 (X) |
| 라벨 분리 | 마지막 열 → 정답 레이블 (y) |
라벨 정규화:
if self.labels.min() != 0:
self.labels = self.labels - self.labels.min()- PyTorch의
CrossEntropyLoss는 라벨이 0부터 시작해야 함 - 예: 라벨이 1
7이면 → 06으로 변환
데이터 예시:
원본 CSV:
feature1, feature2, feature3, label
0.5, 0.3, 0.8, 3
0.2, 0.7, 0.1, 1
변환 후:
features = [[0.5, 0.3, 0.8], [0.2, 0.7, 0.1]]
labels = [3, 1] (또는 라벨이 1부터 시작하면 [2, 0])
len: 크기 반환
def __len__(self):
return len(self.data)- 데이터셋의 전체 샘플 개수 반환
- DataLoader가 배치 개수를 계산할 때 사용
getitem: 샘플 반환
def __getitem__(self, index):
x = torch.tensor(self.features[index], dtype=torch.float32).to(self.device)
y = torch.tensor(self.labels[index], dtype=torch.long).to(self.device)
return x, y| 단계 | 설명 |
|---|---|
| 인덱싱 | index번째 특성과 라벨 가져오기 |
| 텐서 변환 | numpy array → PyTorch Tensor |
| 데이터 타입 | float32 (입력), long (정수 라벨) |
| 디바이스 이동 | GPU/CPU로 데이터 전송 |
주의사항:
dtype=torch.float32: 모델 입력용 (실수)dtype=torch.long: 분류 라벨용 (정수).to(device): 미리 디바이스로 이동 (학습 루프에서 이동하지 않아도 됨)
2. 모델 정의 (MLP)
코드
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super(MLP, self).__init__()
self.layers = nn.Sequential(
nn.Linear(input_dim, hidden_dim), # 입력층 → 은닉층
nn.ReLU(), # 활성화 함수
nn.Linear(hidden_dim, output_dim) # 은닉층 → 출력층
)
def forward(self, x):
return self.layers(x)아키텍처
입력 (input_dim)
↓
Linear(input_dim → hidden_dim)
↓
ReLU()
↓
Linear(hidden_dim → output_dim)
↓
출력 (output_dim)
구성 요소
| 레이어 | 역할 | 예시 |
|---|---|---|
nn.Linear | 선형 변환 (가중치 곱셈) | 54차원 → 64차원 |
nn.ReLU | 비선형 활성화 | max(0, x) |
nn.Linear | 출력층 | 64차원 → 7차원 (클래스 수) |
예시 구조:
input_dim = 54 # 입력 특성 개수
hidden_dim = 64 # 은닉층 뉴런 개수
output_dim = 7 # 클래스 개수 (7개 분류)
# 모델 크기
Layer 1: 54 × 64 + 64(bias) = 3,520개 파라미터
Layer 2: 64 × 7 + 7(bias) = 455개 파라미터
Total: 3,975개 파라미터3. 데이터 로더 & 모델 준비
코드
# 데이터셋 & 데이터로더
dataset = CustomDataset('../../data/covtype.csv', device=device)
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
# 모델 설정
input_dim = dataset.features.shape[1] # 입력 차원 (자동 계산)
output_dim = len(set(dataset.labels)) # 클래스 개수
hidden_dim = 64 # 은닉층 차원 (하이퍼파라미터)
model = MLP(input_dim, hidden_dim, output_dim).to(device)
# 손실 함수 & 옵티마이저
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 학습률 스케줄러 (선택)
scheduler = torch.optim.lr_scheduler.LambdaLR(
optimizer=optimizer,
lr_lambda=lambda epoch: 0.95 ** epoch
)주요 설정
DataLoader
DataLoader(dataset, batch_size=4, shuffle=True)| 파라미터 | 값 | 설명 |
|---|---|---|
dataset | CustomDataset | 사용할 데이터셋 |
batch_size | 4 | 한 번에 처리할 샘플 수 |
shuffle | True | 매 에포크마다 데이터 섞기 |
손실 함수 (Loss Function)
criterion = nn.CrossEntropyLoss()| 항목 | 설명 |
|---|---|
| 용도 | 다중 클래스 분류 |
| 입력 | 모델의 raw logits (Softmax 전) |
| 출력 | 예측과 정답 간의 차이 (scalar) |
| 공식 | -log(softmax(y_pred)[y_true]) |
동작 원리:
# 모델 출력 (logits)
y_pred = [2.1, 0.3, -1.5] # 3개 클래스
# 정답
y_true = 0 # 첫 번째 클래스
# CrossEntropyLoss 내부 동작
1. Softmax: [0.77, 0.13, 0.02] # 확률로 변환
2. Log: [-0.26, -2.04, -3.91]
3. 정답 클래스 선택: -(-0.26) = 0.26 # Loss옵티마이저 (Optimizer)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)| 항목 | 설명 |
|---|---|
| 알고리즘 | Adam (Adaptive Moment Estimation) |
| 학습률 | 0.01 |
| 대상 | model.parameters() (모든 학습 가능한 파라미터) |
Adam의 특징:
- 각 파라미터마다 적응적 학습률 적용
- 모멘텀 + RMSprop 결합
- 일반적으로 SGD보다 빠르고 안정적
학습률 스케줄러 (Learning Rate Scheduler)
scheduler = torch.optim.lr_scheduler.LambdaLR(
optimizer=optimizer,
lr_lambda=lambda epoch: 0.95 ** epoch
)동작:
Epoch 0: lr = 0.01 × 0.95^0 = 0.01
Epoch 1: lr = 0.01 × 0.95^1 = 0.0095
Epoch 2: lr = 0.01 × 0.95^2 = 0.009025
Epoch 3: lr = 0.01 × 0.95^3 = 0.00857
...효과:
- 초기: 큰 학습률로 빠르게 학습
- 후기: 작은 학습률로 미세 조정
- 과적합 방지 및 수렴 안정성 향상
4. 기본 훈련 루프
전체 코드
epochs = 5
for epoch in range(epochs):
for x, y in dataloader:
# (1) 순전파 (Forward pass)
y_pred = model(x)
# (2) 손실 계산 (Loss computation)
loss = criterion(y_pred, y)
# (3) 그래디언트 초기화 (Zero gradients)
optimizer.zero_grad()
# (4) 역전파 (Backward pass)
loss.backward()
# (5) 가중치 업데이트 (Weight update)
optimizer.step()
print(f"Epoch {epoch+1}/{epochs}, Loss: {loss.item():.4f}")5. 훈련 루프 단계별 상세 설명
(1) 순전파 (Forward Pass)
y_pred = model(x)| 항목 | 설명 |
|---|---|
| 입력 | x: 배치 크기만큼의 입력 데이터 (예: [4, 54]) |
| 동작 | 모델의 forward() 메서드 호출 |
| 출력 | y_pred: 각 클래스에 대한 예측값 (logits) |
예시:
# 입력 배치
x.shape = [4, 54] # 4개 샘플, 54개 특성
# 모델 통과
y_pred = model(x)
y_pred.shape = [4, 7] # 4개 샘플, 7개 클래스 점수
# 예측 예시
y_pred = [[2.1, 0.3, -1.5, ...], # 샘플 1
[1.5, 3.2, 0.1, ...], # 샘플 2
...](2) 손실 계산 (Loss Computation)
loss = criterion(y_pred, y)| 항목 | 설명 |
|---|---|
| 입력 | y_pred: 예측값, y: 정답 라벨 |
| 동작 | 예측과 정답 간의 차이 계산 |
| 출력 | loss: 스칼라 값 (배치 평균 손실) |
| 예시: |
y_pred = [[2.1, 0.3, -1.5, 0.2, 1.1, 0.5, -0.8], # 샘플 1
[1.5, 3.2, 0.1, 0.9, 0.7, 1.2, 0.3]] # 샘플 2
y = [0, 1] # 정답: 샘플1은 클래스0, 샘플2는 클래스1
# CrossEntropyLoss 계산
loss_1 = -log(softmax(y_pred[0])[0]) # 샘플 1 손실
loss_2 = -log(softmax(y_pred[1])[1]) # 샘플 2 손실
loss = (loss_1 + loss_2) / 2 # 평균손실 값의 의미:
- 낮은 손실 (0에 가까움): 예측이 정답에 가까움
- 높은 손실: 예측이 틀림
(3) 그래디언트 초기화 (Zero Gradients)
optimizer.zero_grad()| 항목 | 설명 |
|---|---|
| 목적 | 이전 배치의 그래디언트 제거 |
| 이유 | PyTorch는 그래디언트를 누적하므로 초기화 필수 |
| 시점 | 역전파 전에 항상 호출 |
| 왜 필요한가? |
# 그래디언트 누적 예시
배치 1: loss.backward() → gradient = [0.5, 0.3, ...]
배치 2: loss.backward() → gradient = [0.5, 0.3, ...] + [0.2, 0.1, ...]
= [0.7, 0.4, ...] # 잘못된 누적!
# 올바른 방법
배치 1: optimizer.zero_grad() → gradient = [0, 0, ...]
loss.backward() → gradient = [0.5, 0.3, ...]
배치 2: optimizer.zero_grad() → gradient = [0, 0, ...] # 초기화
loss.backward() → gradient = [0.2, 0.1, ...]예외 상황 (Gradient Accumulation):
# 여러 배치의 그래디언트를 의도적으로 누적
for i, (x, y) in enumerate(dataloader):
loss = criterion(model(x), y)
loss.backward() # 그래디언트 누적
if (i + 1) % 4 == 0: # 4배치마다
optimizer.step() # 업데이트
optimizer.zero_grad() # 초기화(4) 역전파 (Backward Pass)
loss.backward()| 항목 | 설명 |
|---|---|
| 목적 | 손실에 대한 각 파라미터의 그래디언트 계산 |
| 방법 | 체인 룰(Chain Rule)을 이용한 자동 미분 |
| 결과 | 각 파라미터의 .grad 속성에 그래디언트 저장 |
동작 원리:
# 모델 구조
x → Linear1 → ReLU → Linear2 → y_pred → loss
# 역전파 (뒤에서 앞으로)
loss → ∂loss/∂y_pred → ∂loss/∂Linear2 → ∂loss/∂ReLU → ∂loss/∂Linear1그래디언트 확인:
# 역전파 전
print(model.layers[0].weight.grad) # None
# 역전파 후
loss.backward()
print(model.layers[0].weight.grad)
# tensor([[-0.01, 0.02, ...],
# [ 0.03, -0.01, ...],
# ...])그래디언트의 의미:
- 양수: 파라미터 증가 시 손실 증가 → 파라미터 감소 필요
- 음수: 파라미터 증가 시 손실 감소 → 파라미터 증가 필요
- 절댓값: 손실에 대한 민감도 (큰 값 = 큰 영향)
(5) 가중치 업데이트 (Weight Update)
optimizer.step()| 항목 | 설명 |
|---|---|
| 목적 | 계산된 그래디언트로 파라미터 업데이트 |
| 방법 | 옵티마이저 알고리즘 적용 (Adam, SGD 등) |
| 결과 | 모델 파라미터 값 변경 |
업데이트 공식 (기본 SGD):
# 학습률 = 0.01, 그래디언트 = -0.5
new_weight = old_weight - learning_rate × gradient
= 1.0 - 0.01 × (-0.5)
= 1.0 + 0.005
= 1.005Adam의 경우 (더 복잡):
# Adam은 다음을 추가로 고려
1. 모멘텀 (과거 그래디언트의 이동 평균)
2. 적응적 학습률 (그래디언트 제곱의 이동 평균)
# 의사 코드
m = β1 × m + (1 - β1) × gradient # 모멘텀
v = β2 × v + (1 - β2) × gradient² # 적응적 학습률
new_weight = old_weight - lr × m / √v업데이트 전후 비교:
# 업데이트 전
print(model.layers[0].weight[0, 0]) # 0.523
# 업데이트 후
optimizer.step()
print(model.layers[0].weight[0, 0]) # 0.518 (감소)훈련 루프 흐름도
Epoch 시작 DataLoader에서 배치 가져오기 (x, y) (1) 순전파: y_pred = model(x)
- 입력 데이터를 모델에 통과
- 예측값 계산 (2) 손실 계산: loss = criterion(y_pred, y)
- 예측과 정답 비교
- 손실 값 계산 (3) 그래디언트 초기화: optimizer.zero_grad()
- 이전 배치 그래디언트 제거 (4) 역전파: loss.backward()
- 손실에 대한 그래디언트 계산
- 체인 룰 적용 (5) 가중치 업데이트: optimizer.step()
- 그래디언트로 파라미터 업데이트 다음 배치 없을 시 Epoch 종료(손실 출력)
실행 결과 예시
Epoch 1/5, Loss: 1.8234
Epoch 2/5, Loss: 1.3421
Epoch 3/5, Loss: 0.9876
Epoch 4/5, Loss: 0.7234
Epoch 5/5, Loss: 0.5432
해석:
- Loss가 점차 감소 → 모델이 학습 중
- 5 에포크 후 손실이 1.8234 → 0.5432로 감소
- 모델의 예측 정확도가 향상됨
핵심 포인트 정리
훈련 루프 5단계
- 순전파: 예측값 계산
- 손실 계산: 예측과 정답 비교
- 그래디언트 초기화: 이전 배치 제거
- 역전파: 그래디언트 계산
- 가중치 업데이트: 파라미터 갱신
중요한 순서
optimizer.zero_grad() # 반드시 backward() 전에!
loss.backward() # 그래디언트 계산
optimizer.step() # 그래디언트 사용 후 업데이트자주하는 실수
| 실수 | 결과 | 해결 |
|---|---|---|
zero_grad() 생략 | 그래디언트 누적 | 항상 역전파 전 호출 |
backward() 생략 | 그래디언트 계산 안 됨 | 반드시 호출 |
step() 생략 | 가중치 업데이트 안 됨 | 반드시 호출 |
| 순서 틀림 | 잘못된 학습 | 5단계 순서 준수 |
학습 확인 방법
- 손실 감소: 학습이 진행되는 중
- 손실 증가: 학습률이 너무 높거나 문제 발생
- 손실 정체: 학습률이 너무 낮거나 수렴 완료