하이퍼파라미터 튜닝
파라미터 vs. 하이퍼파라미터
- 파라미터
- 데이터로부터 학습되는 변수
- 신경망에서 가중치
- 경사하강법을 통해 찾음 → 점진적 개선
- 하이퍼파라미터
- 모형 또는 학습의 특성을 정의하는 변수
- 학습 전에 결정 → 데이터로 정할 수 없으므로 시행착오를 통해 개선
- 종류:
- 신경망의 구조: 은닉층의 수, 각 층의 폭, 활성화 함수
- 학습 및 최적화: 학습률, 배치의 크기, 최적화 알고리즘, 에포크 수, 학습 중단 방식
- 정칙화: 드롭아웃 방식, 데이터 증강 등
- 조합이 매우 많으므로 코드를 손으로 고쳐 가며 시도하기보다 자동으로 조합을 바꿔 가며 탐색
학습률 찾기
- 경사하강법에는 적절한 학습률이 필요
- 학습률이 너무 낮으면 → 손실이 느리게 줄어듦
- 학습률이 너무 높으면 → 손실이 오히려 늘어남
- 학습률을 서서히 변화시키면서 적절한 학습률을 찾음
- 기존 모형을 상속받아 학습률을 외부에서 받을 수 있게 재정의(처음부터 이렇게 만들어도 됨)
- 모형 구조 등 공통 부분은 그대로 물려받고, 고정되어 있던 학습률만 인자로 받도록 변경
class MLPClassifierAutoLR(MLPClassifier):
def __init__(self, learning_rate):
super().__init__()
self.learning_rate = learning_rate
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=(self.learning_rate))
- 파이토치 라이트닝의
Tuner로 학습률을 자동 탐색
from lightning.pytorch.tuner import Tuner
trainer = Trainer(max_epochs=3, default_root_dir=log_dir)
tuner = Tuner(trainer)
model = MLPClassifierAutoLR(learning_rate=0)
tuner.lr_find(model, train_loader) # 최적 학습률을 찾으면 예외 발생
lr_finder = trainer.callbacks[0].optimal_lr
lr_finder.plot(suggest=True); # 학습률 찾기 과정 시각화
- 학습률 찾기 그래프 읽기
- 가로축은 로그 스케일의 학습률(10⁻⁸부터 점점 키움)
- 초반: 학습률이 너무 작아 사실상 학습이 안 됨 → 미니배치에 따라 손실이 오르내리기만 함
- 중반: 손실이 줄어들기 시작
- 후반: 학습률이 너무 커서 손실이 다시 늘어남
- 손실이 가장 빠르게 감소하는(기울기가 가장 급한) 학습률을 빨간 점으로 표시
# 제안된 학습률 확인
lr_finder.suggestion()
실행 결과
0.01
# 제안된 학습률로 설정되어 있음
model.learning_rate
실행 결과
0.01
- 미니배치를 무작위로 뽑으므로 실행할 때마다 그래프와 제안값이 조금씩 다름
- 로그 스케일이므로 0.01과 0.001, 0.1은 서로 근방에 해당
- 손실이 꺾이기 시작하는 지점부터 다시 늘어나기 전까지는 대체로 괜찮은 학습률
- 제안값이 항상 정확하지는 않음
- 기울기가 가장 급한 곳을 기계적으로 고르므로, 데이터가 이상하게 걸려 손실이 잠깐 떨어진 구간이 선택되기도 함
- 그래프를 눈으로 확인하고, 이상하면 다시 실행하거나
model.learning_rate = 0.01처럼 직접 설정
- 한 번보다 여러 번 실행해 일관된 값이 나오는지 확인하는 것이 좋음
미니배치 크기 찾기
- 미니 배치도 적절한 크기가 필요
- 너무 작으면 → 학습 불안정
- 너무 크면 → 학습 느림, 메모리 용량 문제(메모리에 올리는 데이터가 많아짐)
Tuner는 메모리가 허용하는 최대 배치 크기를 자동으로 찾아줌- 2, 4, 8, 16, … 처럼 두 배씩 늘리면서 메모리가 감당하지 못할 때까지 시도
- 성능에 최적인 배치 크기를 찾아주는 것은 아니므로 따로 찾아야 함
- 모형이 한 번에 처리할 수 있는 최대량을 알고 싶을 때 유용
- 데이터 로더를 따로 만들지 않고, 모형 안에
train_dataloader를 정의해 배치 크기를 외부에서 받음
class MLPClassifierAutoBatchSize(MLPClassifier):
def __init__(self, batch_size):
super().__init__()
self.batch_size = batch_size
def train_dataloader(self):
return DataLoader(binary_train_dataset, batch_size=self.batch_size)
model = MLPClassifierAutoBatchSize(batch_size=32)
trainer = Trainer(max_epochs=3, default_root_dir=log_dir)
tuner = Tuner(trainer)
tuner.scale_batch_size(model) # 메모리에 올릴 수 있을 때까지 미니 배치 크기를 자동으로 키움
model.batch_size # 최대 크기로 설정되어 있음
실행 결과
16384
- 학습률과 배치 크기를 둘 다 인자로 받도록 모형을 정의하면 두 가지를 함께 자동 조정 가능
하이퍼파라미터 튜닝 과정
- 학습률이나 배치 크기는 값을 점점 키워 가며 찾을 수 있지만, 은닉층 크기처럼 그렇게 찾기 어려운 하이퍼파라미터도 있음
- 일반적인 순서
- 전체 데이터를 훈련 데이터와 테스트 데이터로 분할
- 훈련 데이터를 다시 나눠 교차 검증으로 하이퍼파라미터를 평가
- 가장 좋은 하이퍼파라미터로 훈련 데이터 전체를 다시 학습(생략하기도 함)
- 테스트 데이터로 최종 평가
- 결과적으로 데이터는 훈련 / 검증 / 테스트의 세 부분으로 나뉨
- 검증 데이터는 모의고사, 테스트 데이터는 수능에 비유할 수 있음
Validation 또는 Development set
- 테스트 데이터셋을 이용해서 하이퍼파라미터를 결정하면 특정 테스트셋에 과적합될 수 있음
- 별도의 Validation 또는 Development set을 이용해서 하이퍼파라미터를 결정
- 테스트 데이터셋은 최종 평가에 마지막으로 한 번만 사용
교차 검증 cross-validation
- 하나의 검증셋만 사용할 경우, 하이퍼파라미터가 정확하지 않을 수 있음
- 어떤 검증 데이터를 뽑느냐에 따라 최적의 하이퍼파라미터가 달라질 수 있음
- 출제 회사마다 경향이 다른 모의고사처럼, 한 번의 검증 결과만으로는 믿기 어려움
- 다양한 검증셋을 이용하여 검증
- 어떤 검증셋에서도 잘 작동하는 하이퍼파라미터가 신뢰할 만함
- K-fold
- 가장 많이 사용하는 방법
- 훈련 데이터를 k개의 겹치지 않는 부분으로 나눔
- k-1개로 훈련하고 나머지 하나로 검증하는 것을 k번 반복(매번 모형을 초기화)
- Hold-out
- 훈련 데이터와 검증 데이터로 1번만 분할해서 사용
- 딥러닝에서는 훈련에 시간이 오래 걸리므로 자주 사용
- 데이터가 적을 때는 신뢰성에 문제
데이터 분할
from sklearn.model_selection import train_test_split
train_indices, val_indices = train_test_split(
range(len(binary_train_dataset)), # 데이터 번호를
test_size=0.2) # 80:20으로 나눔
# 훈련 데이터 번호에 해당하는 부분집합
train_subset = torch.utils.data.Subset(binary_train_dataset, train_indices)
train_loader = DataLoader(train_subset, batch_size=32, shuffle=True)
# 검증 데이터 번호에 해당하는 부분집합
val_subset = torch.utils.data.Subset(binary_train_dataset, val_indices)
val_loader = DataLoader(val_subset, batch_size=32, shuffle=False)
은닉층 크기 바꿔 보기
- 은닉층 크기를 인자로 받을 수 있게 수정
class MLPClassifierHiddenSize(MLPClassifier):
def __init__(self, hidden_size, input_size=784): # 은닉층 크기를 인자로 받음
super().__init__()
self.linear1 = nn.Linear(input_size, hidden_size) # 은닉층
self.linear2 = nn.Linear(hidden_size, 1) # 한 개의 출력 뉴런
- 은닉층 크기를 다양하게 시도하고 검증 데이터로 평가
results = []
for hidden_size in [16, 64, 128]: # 은닉층 크기를 다양하게 시도
model = MLPClassifierHiddenSize(hidden_size)
trainer = Trainer(max_epochs=3, default_root_dir=log_dir)
trainer.fit(model, train_loader)
result = trainer.test(model, val_loader)
result[0]['hidden_size'] = hidden_size
results.append(result[0])
results
실행 결과
[{'test_accuracy': 0.9566666483879089, 'hidden_size': 16},
{'test_accuracy': 0.9399999976158142, 'hidden_size': 64},
{'test_accuracy': 0.9574999809265137, 'hidden_size': 128}]
격자 탐색과 무작위 탐색
- 격자 탐색 grid search: 각 하이퍼파라미터의 설정값을 정하고, 이들을 조합하여 하나씩 시도
- 무작위 탐색 random search: 하이퍼파라미터를 무작위로 조합하여 하나씩 시도
- 하이퍼파라미터를 더 다양하고 촘촘하게 탐색하여 격자 탐색보다 더 높은 성능을 내는 경향
정수 난수 생성
import scipy
hp = scipy.stats.randint.rvs(low=1, high=8, size=3) # 1 이상 8 미만의 무작위 정수 3개
# 은닉층의 크기 등 배수로 사용해야 할 경우는 다음과 같이
hidden_size = 2 ** hp
- 무작위로 은닉층 크기를 뽑아 10번 시도
results = []
for _ in range(10):
hp = scipy.stats.randint.rvs(low=2, high=10, size=1)
hidden_size = (2 ** hp)[0] # 4에서 512 사이의 2의 거듭제곱
model = MLPClassifierHiddenSize(hidden_size)
trainer = Trainer(max_epochs=3, default_root_dir=log_dir)
trainer.fit(model, train_loader)
result = trainer.test(model, val_loader)
result[0]['hidden_size'] = hidden_size
results.append(result[0])
실수 난수 생성
# -8에서 -1(+7)까지 무작위 실수 3개
hp = scipy.stats.uniform.rvs(loc=-8, scale=7, size=3)
# 학습률 등 배수로 사용해야 할 경우는 다음과 같이
learning_rate = 10 ** hp
# 10⁻⁸에서 10⁻¹까지 로그 균등 분포를 사용해도 똑같음
learning_rate = scipy.stats.loguniform.rvs(a=1e-8, b=1e-1, size=1000)
Sequential Model-Based Optimization
- 무작위 탐색의 문제점:
- 하이퍼파라미터의 종류가 많아질 수록 더 많은 탐색이 필요
- 하이퍼파라미터는 학습 전체가 끝나야 성능을 알 수 있음 (비쌈)
- 무작위 탐색은 이전의 학습 결과를 활용하지 않음
- SMBO: 하이퍼파라미터와 성능의 관계를 모형화
- 모형을 바탕으로 최적의 하이퍼파라미터를 선택하여 학습
- 학습 결과를 바탕으로 모형을 업데이트하는 과정을 반복
- 적은 데이터에서 높은 성능을 내는 모형을 사용
- 기본적으로 어느 정도의 데이터를 요구하므로 다양한 하이퍼파라미터를 선제적으로 시도해야 함 → 효율성이 떨어짐
베이즈 오차 Bayes Error
- 오차 0%는 다양한 이유로 실현 불가능(예: 테스트 이미지의 레이블이 잘못 붙어 있음. 식별 불가능한 이미지 등)
- 베이즈 오차: 데이터로부터 도달할 수 있는 이론적으로 최소의 오차
- 베이즈 오차를 실제로 추정하기는 불가능
- 보통 사람의 오차를 베이즈 오차 대용으로 사용
- 예: MNIST의 경우 사람의 오차 0.2% 수준