Developer's Development
3.2.21 [딥러닝] 최적 모델 학습 (과적합 해결) 본문
과적합(Overfitting) 해결
과적합은 모델이 학습 데이터에 과하게 적응하여 일반화 성능이 떨어지는 현상을 말한다.
과적합이 발생하면 학습 데이터에서는 높은 성능을 보이지만, 실제 새로운 데이터에서는 성능이 저하된다.
이를 해결하기 위한 대표적인 기법으로 배치 정규화, 드롭아웃, 하이퍼파라미터 최적화가 있다.
- 배치 정규화 (Batch Normalization, BN)
신경망의 각 층에서 입력되는 데이터의 분포를 정규화는 기법이다.
신경망의 학습 속도를 높이고, 초깃값에 대한 의존성을 줄이며, 과적합을 방지하는 데 도움을 준다.
👉🏻 배치 정규화의 주요 특징
- 각 미니 배치에서 평균과 분산을 이용하여 정규화 진행한다.
- 학습 과정에서 데이터의 스케일 변화를 줄여 학습을 안정적으로 수행한다.
- 과적합을 방지하고, 일반적으로 더 적은 학습률로도 빠른 학습이 가능하다.
- 드롭아웃 (Dropout)
학습 과정에서 뉴런을 랜덤하게 제거하여 과적합을 방지하는 기법이다.
특정 확률로 뉴런을 비활성화하여 특정 뉴런에 대한 의존도를 줄이고, 신경망이 더 일반화되도록 돕는다.
👉🏻 드롭아웃 과정
1. 학습 과정에서 랜덤하게 일부 뉴런을 제거한다.
2. 테스트 과정에서는 모든 뉴런을 활성화하지만, 드롭아웃 확률을 고려하여 가중치를 조정한다.
3. 네트워크의 강건성을 높이고, 과적합을 방지한다.
- 하이퍼파라미터 최적화 (Hyperparameter Optimization)
모델의 성능을 최적화하기 위해 학습률, 배치 크기, 드롭아웃 비율 등의 하이퍼파라미터를 조정하는 과정이다.
👉🏻 대표적인 최적화 방법
- 그리드 서치(Grid Search) : 미리 정의된 하이퍼파라미터 조합을 탐색하며 최적의 조합을 찾는 방법이다.
- 랜덤 서치 (Random Search) : 하이퍼파라미터를 무작위로 선택하여 최적의 값을 찾는 방법이다.
- 베이지안 최적화 (Bayesian Optimization) : 이전 실험 결과를 바탕으로 최적의 조합을 찾는 기법이다.
- 조기 종료 (Early Stopping)
학습 과정에서 검증 데이터의 손실(validation loss)이 더 이상 감소하지 않을 때 학습을 중단하는 기법이다.
일반적으로 patience 값을 설정하여 일정 횟수 동안 성능이 개선되지 않으면 학습을 멈춘다.
과적합을 방지하는 동시에 불필요한 연산을 줄여 학습 시간을 단축할 수 있다.
- 데이터 증강 (Data Augmentation)
훈련 데이터의 크기를 인위적으로 늘려 모델이 다양한 패턴을 학습하도록 유도하는 방법이다.
특히 이미지 데이터의 경우 회전, 이동, 크기 조정, 밝기 변경, 뒤집기 등을 통해 새로운 데이터를 생성할 수 있다.
- L1, L2 정규화 (가중치 감쇠, Weight Decay)
신경망의 가중치 값이 너무 커지면 특정 뉴런에 대해 의존성이 증가하여 과적합이 발생할 수 있다.
이를 방지하기 위해 L1 정규화(Lasso), L2 정규화(Ridge) 기법을 사용하여 손실 함수에 패널티를 추가한다.
- L1 정규화 : 가중치의 절대값을 기준으로 패널티 부여
- L2 정규화 : 가중치의 제곱을 기준으로 패널티 부여
실습
- 배치 정규화 적용
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, random_split
import torchvision.transforms as transforms
import torchvision.datasets as datasets
# 데이터셋 로드
# ToTensor = 이미지를 PyTorch가 이해할 수 있는 숫자(텐서)로 변환, Normalize로 정규화
transforms = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
# 이미지 데이터로드 train = 학습용 데이터 로드, transforms 전처리 후 가져오기, download 로컬에 저장
dataset = datasets.MNIST(root='./data', train=True, transform=transforms, download=True)
# 학습셋-검증셋 사이즈 지정
train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
# 데이터 분리 및 미니 배치로 로드 가능하도록 DataLoader 생성
train_data, val_data = random_split(dataset, [train_size, val_size])
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
val_load = DataLoader(val_data, batch_size=64, shuffle=False)
# 배치 정규화 반영된 간단한 신경망
# use_bn=True: nn.BatchNorm1d()로 정규화 계층 활성화
# use_bn=False: nn.Identity()로 아무것도 하지 않음 (즉, 배치 정규화 생략)
class SimpleNN(nn.Module):
def __init__(self, use_bn=False):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(28 * 28, 128)
self.bn1 = nn.BatchNorm1d(128) if use_bn else nn.Identity()
self.fc2 = nn.Linear(128, 64)
self.bn2 = nn.BatchNorm1d(64) if use_bn else nn.Identity()
self.fc3 = nn.Linear(64, 10)
def forward(self, x):
x = x.view(x.size(0), -1) # (배치 크기, h, w) -> (배치 크기, feature)
x = torch.relu(self.bn1(self.fc1(x)))
x = torch.relu(self.bn2(self.fc2(x)))
x = self.fc3(x)
return x
# 학습
def train(model, train_loader, val_loader, epochs=5):
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(epochs):
model.train()
# 학습 루프
# 학생이 문제를 풀고 (forward) 틀린 걸 고쳐가며 반복 학습 (backward)
for images, labels in train_loader:
images = images.view(images.size(0), -1)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 검증
# 학습은 멈추고 검증용 데이터를 가지고 성능 확인
model.eval()
val_loss = 0
correct = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.view(images.size(0), -1)
outputs = model(images)
val_loss += criterion(outputs, labels).item()
preds = outputs.argmax(dim=1)
correct += (preds == labels).sum().item()
print(f'Epoch {epoch + 1} | Val Loss: {val_loss / len(val_loader):.4f},\
Acccuracy: {correct / len(val_data):.4f}')
# 모델 학습 및 결과 비교 (배치 정규화 유무에 따른 성능 차이 비교)
print("배치 정규화 적용 안함")
model_without_bn = SimpleNN(use_bn=False)
train(model_without_bn, train_loader, val_loader)
print()
print("배치 정규화 적용")
model_with_bn = SimpleNN(use_bn=True)
train(model_without_bn, train_loader, val_loader)
👉🏻 배치 사이즈별 모델 학습 결과
for batch_size in [16, 32, 128]:
print(f"batch size: {batch_size}")
train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_data, batch_size=batch_size, shuffle=False)
model = SimpleNN(use_bn=True)
train(model ,train_loader, val_loader)
- 드롭아웃 적용
# 드롭아웃 반영된 간단한 신경망
# dropout_rate는 뉴런을 꺼버릴 확률
# Dropout()은 훈련할 때만 동작하고, 검증/추론 시에는 꺼지지 않음
class DropoutNN(nn.Module):
def __init__(self, dropout_rate):
super(DropoutNN, self).__init__()
self.fc1 = nn.Linear(28 * 28, 128)
self.dropout1 = nn.Dropout(dropout_rate)
self.fc2 = nn.Linear(128, 64)
self.dropout2 = nn.Dropout(dropout_rate)
self.fc3 = nn.Linear(64, 10)
# 뉴런을 랜덤으로 꺼버리기
def forward(self, x):
x = x.view(x.size(0), -1)
x = torch.relu(self.dropout1(self.fc1(x)))
x = torch.relu(self.dropout2(self.fc2(x)))
x = self.fc3(x)
return x
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
val_loader = DataLoader(val_data, batch_size=64, shuffle=False)
# 드롭아웃 비율에 따른 성능 변화
for rate in [0.2, 0.5, 0.8]:
print(f"Dropout Rate: {rate}")
model = DropoutNN(dropout_rate=rate)
train(model, train_loader, val_loader)
"""
Dropout Rate: 0.2
Epoch 1 | Val Loss: 0.2535, Acccuracy: 0.9241
Epoch 2 | Val Loss: 0.1752, Acccuracy: 0.9447
Epoch 3 | Val Loss: 0.1541, Acccuracy: 0.9542
Epoch 4 | Val Loss: 0.1464, Acccuracy: 0.9543
Epoch 5 | Val Loss: 0.1358, Acccuracy: 0.9578
Dropout Rate: 0.5
Epoch 1 | Val Loss: 0.3448, Acccuracy: 0.8981
Epoch 2 | Val Loss: 0.2797, Acccuracy: 0.9186
Epoch 3 | Val Loss: 0.2471, Acccuracy: 0.9297
Epoch 4 | Val Loss: 0.2525, Acccuracy: 0.9225
Epoch 5 | Val Loss: 0.2305, Acccuracy: 0.9299
Dropout Rate: 0.8
Epoch 1 | Val Loss: 1.4659, Acccuracy: 0.6122
Epoch 2 | Val Loss: 1.2908, Acccuracy: 0.6469
Epoch 3 | Val Loss: 1.2270, Acccuracy: 0.5821
Epoch 4 | Val Loss: 1.2185, Acccuracy: 0.6366
Epoch 5 | Val Loss: 1.1414, Acccuracy: 0.6629
"""
# 드롭아웃 비율이 20%인 모델 생성
model = DropoutNN(dropout_rate=0.2)
# 드롭아웃은 학습 시에만 적용됨
model.eval() # 평가 모드 (Dropout 비활성화)
with torch.no_grad():
images, _ = next(iter(val_loader))
images = images.view(images.size(0), -1)
out1 = model(images)
out2 = model(images)
print(torch.allclose(out1, out2)) # True (드롭아웃이 적용되지 않으므로 매번 같은 출력을 냄)
model.train() # 학습 모드 (Dropout 활성화)
out1 = model(images)
out2 = model(images)
print(torch.allclose(out1, out2)) # False (드롭아웃이 적용되어 랜덤하게 뉴런이 비활성화되므로 다른 출력을 냄)
👉🏻 Learning Late Scheduler 적용
class RealSimpleNN(nn.Module):
def __init__(self):
super(RealSimpleNN, self).__init__()
self.fc1 = nn.Linear(28 * 28, 128)
self.fc2 = nn.Linear(128, 64)
self.fc3 = nn.Linear(64, 10)
def forward(self, x):
x = x.view(x.size(0), -1)
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
def train_lr(model, train_loader, val_loader, optimizer, scheduler, epochs=10):
criterion = nn.CrossEntropyLoss()
for epoch in range(epochs):
model.train()
for images, labels in train_loader:
images = images.view(images.size(0), -1)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
model.eval()
val_loss = 0
correct = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.view(images.size(0), -1)
outputs = model(images)
val_loss += criterion(outputs, labels).item()
preds = outputs.argmax(dim=1)
correct += (preds == labels).sum().item()
# 학습율 스케쥴러 적용
scheduler.step()
print(f'Epoch {epoch + 1} | Val Loss: {val_loss / len(val_loader):.4f},\
Acccuracy: {correct / len(val_data):.4f}')
model = RealSimpleNN()
optimizer = optim.Adam(model.parameters(), lr=0.01)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.1) # 3번의 에폭마다 0.1배씩 학습율 변화
train_lr(model, train_loader, val_loader, optimizer, scheduler)
- 조기 종료
def train_es(model, train_loader, val_loader, optimizer, epochs=50, early_stopping=None):
criterion = nn.CrossEntropyLoss()
train_losses = []
val_losses = []
for epoch in range(epochs):
model.train()
train_loss = 0
for images, labels in train_loader:
images = images.view(images.size(0), -1)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
train_losses.append(train_loss)
model.eval()
val_loss = 0
correct = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.view(images.size(0), -1)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
preds = outputs.argmax(dim=1)
correct += (preds == labels).sum().item()
val_loss /= len(val_loader)
val_losses.append(val_loss)
accuracy = correct / len(val_data)
print(f'Epoch {epoch + 1} | Val Loss: {val_loss:.4f}, Accuracy: {accuracy:.4f}')
# 조기 종료 반영
# 매 epoch 끝날 때마다 검증 손실 체크해서, 더 이상 개선이 없으면 break로 학습 종료
if early_stopping and early_stopping(val_loss):
print(f'조기 종료 epoch {epoch + 1}')
break
return train_losses, val_losses
# 조기 종료를 위한 클래스(객체)
# patience: 성능이 좋아지지 않아도 최대 5번은 기다려줌
# min_delta: 이만큼 이상 성능 개선이 없으면 좋아진 걸로 안 침
class EarlyStopping:
def __init__(self, patience=5, min_delta=0.001):
self.patience = patience
self.min_delta = min_delta
self.best_loss = float('inf')
self.counter = 0
def __call__(self, val_loss):
if val_loss < self.best_loss - self.min_delta:
# 성능 좋아짐
self.best_loss = val_loss
self.counter = 0
else:
# 성능 개선 없음
self.counter += 1
return self.counter >= self.patience # True를 반환하면 학습 중단
# 결과 비교
# 조기 종료 없이 50 epoch
model_no_es = RealSimpleNN()
optim_no_es = optim.Adam(model_no_es.parameters(), lr=0.01)
train_losses, val_losses = train_es(model_no_es, train_loader, val_loader, optim_no_es)
# 조기 종료 적용
model_es = RealSimpleNN()
optim_es = optim.Adam(model_es.parameters(), lr=0.01)
early_stopping = EarlyStopping(patience=5)
train_es_losses, val_es_losses = train_es(model_es, train_loader, val_loader,
optim_es, early_stopping=early_stopping)
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 4))
# 조기 종료 없이 학습한 모델의 학습 손실과 검증 손실
plt.plot(train_losses, label='Train Loss (All epoch)', linestyle='dashed')
plt.plot(val_losses, label='Valid Loss (All epoch)')
# 조기 종료 적용된 모델의 학습 손실과 검증 손실
plt.plot(train_es_losses, label='Train Loss (Early Stopping)', linestyle='dashed')
plt.plot(val_es_losses, label='Valid Loss (Early Stopping)')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()

- L2 적용
def train_reg(model, train_loader, val_loader, optimizer, epochs=10):
criterion = nn.CrossEntropyLoss()
train_losses = []
val_losses = []
weight_norms = []
for epoch in range(epochs):
model.train()
train_loss = 0
for images, labels in train_loader:
images = images.view(images.size(0), -1)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
train_losses.append(train_loss)
model.eval()
val_loss = 0
correct = 0
with torch.no_grad():
for images, labels in val_loader:
images = images.view(images.size(0), -1)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item()
preds = outputs.argmax(dim=1)
correct += (preds == labels).sum().item()
val_loss /= len(val_loader)
val_losses.append(val_loss)
accuracy = correct / len(val_data)
print(f'Epoch {epoch + 1} | Val Loss: {val_loss:.4f}, Accuracy: {accuracy:.4f}')
# 전체 파라미터 크기를 매 epoch마다 측정 (실제로 가중치가 얼마나 커지고 있는지 추적)
weight_norm = sum(torch.norm(p).item() for p in model.parameters())
weight_norms.append(weight_norm)
return train_losses, val_losses, weight_norms
print("L2 정규화 적용 안함")
model_no_reg = RealSimpleNN()
optim_no_reg = optim.SGD(model_no_reg.parameters(), lr=0.01, weight_decay=0.0, momentum=0.9)
train_losses, val_losses, weight_norms = train_reg(model_no_reg, train_loader, val_loader, optim_no_reg)
print()
print("L2 정규화 적용")
model_reg = RealSimpleNN()
optim_reg = optim.SGD(model_reg.parameters(), lr=0.01, weight_decay=0.01, momentum=0.9)
train_reg_losses, val_reg_losses, weight_norms_reg = train_reg(model_reg, train_loader, val_loader, optim_reg)
"""
Epoch 1 | Val Loss: 0.2547, Accuracy: 0.9191
Epoch 2 | Val Loss: 0.1778, Accuracy: 0.9454
Epoch 3 | Val Loss: 0.1385, Accuracy: 0.9565
Epoch 4 | Val Loss: 0.1420, Accuracy: 0.9566
Epoch 5 | Val Loss: 0.1065, Accuracy: 0.9673
Epoch 6 | Val Loss: 0.1057, Accuracy: 0.9680
Epoch 7 | Val Loss: 0.1131, Accuracy: 0.9673
Epoch 8 | Val Loss: 0.1178, Accuracy: 0.9631
Epoch 9 | Val Loss: 0.1006, Accuracy: 0.9692
Epoch 10 | Val Loss: 0.1005, Accuracy: 0.9704
Epoch 1 | Val Loss: 0.2998, Accuracy: 0.9143
Epoch 2 | Val Loss: 0.2530, Accuracy: 0.9237
Epoch 3 | Val Loss: 0.2413, Accuracy: 0.9314
Epoch 4 | Val Loss: 0.2121, Accuracy: 0.9389
Epoch 5 | Val Loss: 0.2016, Accuracy: 0.9434
Epoch 6 | Val Loss: 0.2076, Accuracy: 0.9451
Epoch 7 | Val Loss: 0.2037, Accuracy: 0.9473
Epoch 8 | Val Loss: 0.2115, Accuracy: 0.9411
Epoch 9 | Val Loss: 0.1897, Accuracy: 0.9476
Epoch 10 | Val Loss: 0.1846, Accuracy: 0.9500
"""
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(train_losses, label='Train Loss (No Reg)', linestyle='dashed')
plt.plot(val_losses, label='Valid Loss (No Reg)')
plt.plot(train_reg_losses, label='Train Loss (L2 reg)', linestyle='dashed')
plt.plot(val_reg_losses, label='Valid Loss (L2 reg)')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(weight_norms, label='Weight Norm (No Reg)')
plt.plot(weight_norms_reg, label='Weight Norm (L2 reg)')
plt.xlabel('Epoch')
plt.ylabel('Weight Norm')
plt.legend()
plt.show()

'데이터 분석과 머신러닝, 딥러닝 > 딥러닝' 카테고리의 다른 글
| 3.2.20 [딥러닝] 최적 모델 학습 (최적화 함수) (2) | 2025.08.06 |
|---|---|
| 3.2.19 [딥러닝] 최적 모델 학습 (오차역전파법) (1) | 2025.08.05 |
| 3.2.18 [딥러닝] 인공신경망(2) (4) | 2025.08.04 |
| 3.2.16 [딥러닝] 인공신경망 (4) | 2025.08.03 |
| 3.2.15 [딥러닝] 개요 (2) | 2025.08.03 |