전체 코드 구조

# 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부터 시작해야 함
  • 예: 라벨이 17이면 → 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)
파라미터설명
datasetCustomDataset사용할 데이터셋
batch_size4한 번에 처리할 샘플 수
shuffleTrue매 에포크마다 데이터 섞기

손실 함수 (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.005

Adam의 경우 (더 복잡):

# 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단계

  1. 순전파: 예측값 계산
  2. 손실 계산: 예측과 정답 비교
  3. 그래디언트 초기화: 이전 배치 제거
  4. 역전파: 그래디언트 계산
  5. 가중치 업데이트: 파라미터 갱신

중요한 순서

optimizer.zero_grad()  # 반드시 backward() 전에!
loss.backward()        # 그래디언트 계산
optimizer.step()       # 그래디언트 사용 후 업데이트

자주하는 실수

실수결과해결
zero_grad() 생략그래디언트 누적항상 역전파 전 호출
backward() 생략그래디언트 계산 안 됨반드시 호출
step() 생략가중치 업데이트 안 됨반드시 호출
순서 틀림잘못된 학습5단계 순서 준수

학습 확인 방법

  • 손실 감소: 학습이 진행되는 중
  • 손실 증가: 학습률이 너무 높거나 문제 발생
  • 손실 정체: 학습률이 너무 낮거나 수렴 완료