좋은 모델은 한 번에 나오지 않는다
딥러닝 모델을 처음 학습시키면 묘한 일이 자주 일어납니다. 코드는 맞는 것 같은데 정확도가 낮습니다. 손실이 줄어들다가 갑자기 튑니다. 학습 데이터에서는 거의 완벽한데 검증 데이터에서는 성능이 떨어집니다. 같은 모델인데 학습률 하나 바꿨을 뿐인데 결과가 완전히 달라지기도 합니다.
이때 많은 사람이 숫자를 감으로 바꾸기 시작합니다.
"학습률을 조금 줄여 볼까?"
"은닉층을 더 크게 만들면 좋아지지 않을까?"
"드롭아웃을 0.5로 해 볼까?"
물론 처음에는 이런 시도가 필요합니다. 하지만 계속 감으로만 움직이면 금방 길을 잃습니다. 어제는 좋아 보였던 설정이 오늘은 나빠 보이고, 어떤 변경이 실제로 성능을 올렸는지 알기 어려워집니다. 하이퍼파라미터 튜닝은 숫자 맞히기 게임이 아닙니다. 실험을 설계하고, 결과를 비교하고, 다음 선택을 더 똑똑하게 만드는 과정입니다.
지난 장까지 우리는 과적합, 드롭아웃, 배치 정규화를 다루었습니다. 이제 질문은 더 현실적으로 바뀝니다. "그럼 드롭아웃 비율은 얼마로 해야 하지?", "배치 정규화는 어디에 넣어야 하지?", "학습률은 어떻게 정하지?" 이 장에서는 모델 구조와 학습 설정을 체계적으로 조정해 성능을 개선하는 방법을 익히겠습니다.

파라미터와 하이퍼파라미터는 다르다
먼저 이름부터 정리해야 합니다. 딥러닝에는 파라미터와 하이퍼파라미터가 있습니다. 둘 다 숫자라서 헷갈리지만 역할은 다릅니다.
파라미터는 모델이 학습 과정에서 직접 조정하는 값입니다. 가중치와 편향이 대표적입니다. 모델은 예측하고, 손실을 계산하고, 역전파로 기울기를 구한 뒤, 옵티마이저를 통해 이 값들을 조금씩 바꿉니다.
반면 하이퍼파라미터는 학습 전에 사람이 정해 주는 설정값입니다. 모델이 스스로 직접 학습하지 않는 값입니다. 예를 들어 학습률, 배치 크기, 은닉층 크기, 층의 개수, 드롭아웃 비율, 가중치 감쇠 계수 등이 여기에 들어갑니다.
간단히 말하면 이렇습니다. 모델의 가중치와 편향은 학습 중에 변합니다. 하지만 학습률이 0.001인지 0.01인지는 우리가 정합니다. 은닉층의 뉴런 수가 64개인지 256개인지도 우리가 정합니다. 드롭아웃을 0.2로 할지 0.5로 할지도 우리가 정합니다.
그래서 하이퍼파라미터 튜닝은 모델에게 "어떤 조건에서 배울 것인가"를 정해 주는 일에 가깝습니다. 같은 학생이라도 너무 어려운 문제집을 주면 지치고, 너무 쉬운 문제집만 주면 실력이 늘지 않습니다. 너무 빠르게 진도를 나가면 놓치고, 너무 느리면 시간이 오래 걸립니다. 딥러닝 모델도 비슷합니다. 학습 환경을 잘 맞춰 주어야 가진 능력을 제대로 발휘합니다.
먼저 기준선을 세운다
튜닝을 시작하기 전에 가장 먼저 해야 할 일은 기준선 모델을 만드는 것입니다. 기준선은 앞으로의 실험을 비교할 출발점입니다.
처음부터 거대한 모델, 복잡한 정규화, 여러 스케줄러를 모두 넣으면 무엇이 효과를 냈는지 알 수 없습니다. 좋은 실험은 단순한 출발점에서 시작합니다. 예를 들어 이미지 분류라면 작은 CNN을 만들고, 텍스트 분류라면 간단한 임베딩 기반 모델이나 RNN 모델을 만들 수 있습니다. 그리고 학습률, 배치 크기, 은닉층 크기 같은 설정을 하나의 기본값으로 정합니다.
기준선에서 기록할 것은 최소한 다음과 같습니다.
학습 손실과 검증 손실
학습 정확도와 검증 정확도
사용한 모델 구조와 주요 하이퍼파라미터
학습에 걸린 시간과 에폭 수
이 기준선이 있어야 "좋아졌다"는 말을 숫자로 할 수 있습니다. 검증 정확도가 82%였던 모델이 86%가 되었다면 개선입니다. 학습은 빨라졌지만 검증 성능이 떨어졌다면 상황에 따라 실패일 수도 있습니다. 손실 곡선이 더 안정적으로 내려갔다면 성능이 같아도 의미 있는 변화일 수 있습니다.
튜닝은 어두운 방에서 스위치를 찾는 일이 아닙니다. 손전등을 켜고 한 걸음씩 이동하는 일입니다. 기준선은 그 손전등의 첫 불빛입니다.
무엇부터 조정해야 할까
하이퍼파라미터는 많습니다. 다 바꾸고 싶어집니다. 하지만 한꺼번에 많은 것을 바꾸면 결과를 해석하기 어렵습니다. 그래서 우선순위가 필요합니다.
대부분의 딥러닝 실험에서 먼저 살펴볼 값은 학습률입니다. 학습률은 가중치를 한 번에 얼마나 크게 수정할지 정합니다. 너무 크면 손실이 줄지 않고 튀거나 발산할 수 있습니다. 너무 작으면 안정적이지만 학습이 지나치게 느립니다. 학습률은 모델의 학습 궤적 전체를 좌우하므로 가장 먼저 확인할 만한 값입니다.
그다음은 배치 크기입니다. 배치 크기는 한 번의 업데이트에 몇 개의 데이터를 사용할지 정합니다. 작은 배치는 업데이트가 자주 일어나고 약간 흔들리는 방향으로 이동합니다. 이 흔들림이 때로는 좋은 해를 찾는 데 도움이 되기도 합니다. 큰 배치는 계산 효율이 좋고 기울기가 안정적이지만, 메모리를 많이 쓰고 일반화 성능이 달라질 수 있습니다.
이후에는 모델의 크기를 조정합니다. 은닉층의 뉴런 수, 층의 개수, 채널 수 같은 값이 여기에 해당합니다. 모델이 너무 작으면 데이터의 패턴을 충분히 표현하지 못합니다. 이를 과소적합이라고 합니다. 반대로 모델이 너무 크면 학습 데이터는 잘 외우지만 새로운 데이터에 약해질 수 있습니다. 이는 과적합과 연결됩니다.
마지막으로 정규화 강도를 조정합니다. 드롭아웃 비율, 가중치 감쇠, 데이터 증강 강도 등이 대표적입니다. 정규화는 모델이 학습 데이터에 과하게 달라붙지 않도록 도와줍니다. 다만 너무 강하면 모델이 필요한 패턴까지 배우지 못할 수 있습니다.
실무적으로는 다음 순서로 접근하면 길을 잃을 가능성이 줄어듭니다.
학습률을 먼저 맞춘다.
배치 크기를 조정해 학습 안정성과 속도를 본다.
은닉층 크기, 층 수, 채널 수 등 모델 용량을 조정한다.
드롭아웃, 가중치 감쇠, 데이터 증강 등 정규화 강도를 조정한다.
필요한 경우 학습률 스케줄러나 옵티마이저 설정을 세밀하게 다듬는다.
이 순서가 절대 법칙은 아닙니다. 하지만 처음에는 좋은 나침반이 됩니다. 특히 학습률이 크게 어긋나 있으면 다른 설정을 바꿔도 결과가 제대로 보이지 않습니다. 자동차 타이어가 터진 상태에서 운전 습관을 분석하는 것과 비슷합니다. 먼저 기본 주행이 가능하게 만들어야 합니다.
학습률은 모델의 보폭이다
학습률을 떠올릴 때는 산을 내려가는 사람을 상상하면 좋습니다. 손실 함수는 산의 지형이고, 경사하강법은 낮은 곳을 향해 내려가는 방법입니다. 학습률은 한 걸음의 크기입니다.
보폭이 너무 크면 어떻게 될까요? 낮은 곳을 향해 가기는커녕 반대쪽 언덕으로 뛰어넘을 수 있습니다. 손실이 감소하지 않고 크게 흔들립니다. 때로는 숫자가 nan이 되기도 합니다. 이는 계산이 불안정해졌다는 신호입니다.
보폭이 너무 작으면 어떨까요? 방향은 맞지만 하루 종일 몇 미터밖에 못 갑니다. 손실은 아주 조금씩 줄어들지만 학습이 너무 오래 걸립니다. 에폭을 많이 돌려도 만족스러운 성능에 도달하지 못할 수 있습니다.
학습률을 고를 때는 보통 로그 스케일로 탐색합니다. 0.001, 0.003, 0.01처럼 일정한 간격으로 더하는 것이 아니라 곱셈 간격으로 넓게 보는 방식입니다. 학습률은 0.001과 0.002의 차이보다 0.001과 0.01의 차이가 훨씬 큽니다. 그래서 0.0001, 0.001, 0.01 같은 후보를 먼저 살펴보고, 괜찮은 구간이 보이면 그 주변을 더 자세히 탐색합니다.
예를 들어 다음과 같은 후보를 둘 수 있습니다.
learning_rates = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2]여기서 1e-3은 0.001을 뜻합니다. 딥러닝 코드에서 자주 쓰는 표기입니다. 먼저 넓게 훑고, 좋은 결과가 나온 주변을 좁게 다시 보는 것이 좋습니다.
학습률을 바꾸어 실험할 때는 손실 곡선을 꼭 함께 봐야 합니다. 최종 정확도만 보면 놓치는 것이 많습니다. 손실이 처음부터 불안정하게 튄다면 학습률이 클 가능성이 있습니다. 손실이 너무 천천히 내려간다면 학습률이 작을 가능성이 있습니다. 검증 손실이 어느 순간부터 올라간다면 학습률 문제뿐 아니라 과적합도 의심해야 합니다.
배치 크기는 학습의 리듬을 바꾼다
배치 크기는 한 번의 업데이트에 사용할 데이터 개수입니다. 예를 들어 학습 데이터가 60,000개이고 배치 크기가 64라면, 모델은 64개씩 데이터를 보고 가중치를 업데이트합니다. 전체 데이터를 한 번 모두 보면 1에폭이 끝납니다.
배치 크기가 작으면 업데이트가 자주 일어납니다. 각 배치가 전체 데이터를 완벽히 대표하지는 않기 때문에 기울기 방향이 조금씩 흔들립니다. 이 흔들림은 나쁘기만 한 것이 아닙니다. 때로는 모델이 한 지점에 너무 빨리 갇히지 않도록 도와줍니다. 다만 너무 작으면 학습이 불안정해지고 GPU를 충분히 활용하지 못할 수 있습니다.
배치 크기가 크면 한 번의 업데이트가 더 많은 데이터를 바탕으로 계산됩니다. 기울기가 안정적이고 병렬 계산 효율이 좋아질 수 있습니다. 하지만 메모리를 많이 사용합니다. 또한 업데이트 횟수가 줄어들기 때문에 같은 에폭 수라도 학습의 세부 리듬이 달라집니다.
처음에는 32, 64, 128, 256 같은 값을 자주 사용합니다. 작은 모델이나 작은 데이터셋에서는 32나 64가 충분히 좋을 수 있습니다. 이미지 모델이나 큰 모델에서는 GPU 메모리가 허용하는 범위 안에서 더 큰 배치를 써 볼 수 있습니다.
중요한 점은 배치 크기를 바꾸면 학습률도 함께 영향을 받을 수 있다는 것입니다. 배치가 커지면 기울기가 더 안정적으로 계산되므로 더 큰 학습률을 사용할 수 있는 경우도 있습니다. 반대로 배치가 작으면 학습률을 조금 낮춰야 안정적일 수 있습니다.
그래서 튜닝은 하나의 숫자만 고립해서 보는 일이 아닙니다. 숫자들은 서로 영향을 주고받습니다. 배치 크기는 학습 속도, 메모리 사용량, 손실 곡선의 안정성, 일반화 성능을 동시에 건드립니다.
모델 크기는 표현력과 과적합 사이의 줄타기다
은닉층 크기와 층 수는 모델의 표현력을 결정합니다. 표현력이란 데이터를 얼마나 다양한 방식으로 바라보고 복잡한 패턴을 만들 수 있는 능력입니다.
은닉층이 너무 작으면 모델은 중요한 패턴을 담을 공간이 부족합니다. 손글씨 숫자에서 3과 8의 차이, 고양이와 강아지의 귀 모양 차이, 문장에서 긍정과 부정을 가르는 표현을 충분히 잡아내지 못할 수 있습니다. 이 경우 학습 손실과 검증 손실이 모두 높게 남습니다. 모델이 학습 데이터조차 잘 맞히지 못한다면 모델 용량이 부족하거나 학습 설정이 맞지 않을 가능성이 큽니다.
반대로 은닉층이 지나치게 크면 모델은 데이터의 일반적인 규칙뿐 아니라 우연한 잡음까지 외울 수 있습니다. 학습 정확도는 높지만 검증 정확도는 낮아지는 전형적인 과적합이 나타납니다. 이때는 모델을 줄이거나 정규화를 강화해야 합니다.
예를 들어 MLP에서 은닉층 크기를 다음처럼 실험할 수 있습니다.
hidden_sizes = [64, 128, 256, 512]CNN이라면 채널 수를 조정할 수 있습니다.
channels = [16, 32, 64]텍스트 모델이라면 임베딩 차원이나 RNN의 hidden size를 바꿀 수 있습니다.
embedding_dims = [64, 128, 256]
hidden_sizes = [128, 256, 512]여기서 중요한 것은 "크면 무조건 좋다"가 아니라는 점입니다. 모델이 커질수록 학습 시간과 메모리 사용량이 늘어납니다. 데이터가 적으면 과적합 위험도 커집니다. 좋은 모델은 가장 큰 모델이 아니라, 주어진 데이터와 목적에 맞는 모델입니다.
정규화 강도는 모델의 욕심을 조절한다
드롭아웃, 가중치 감쇠, 데이터 증강은 모델의 욕심을 조절하는 장치입니다. 모델은 학습 데이터에서 점수를 잘 받고 싶어 합니다. 문제는 너무 열심히 외워 버릴 때입니다. 시험 문제집의 문제 번호까지 외운 학생은 실제 시험에서 약할 수 있습니다.
드롭아웃은 학습 중 일부 뉴런을 임시로 꺼서 특정 뉴런에 지나치게 의존하지 않게 만듭니다. 드롭아웃 비율이 0.2라면 학습 중 대략 20%의 뉴런 출력을 꺼 둡니다. 이 값이 너무 작으면 효과가 약하고, 너무 크면 모델이 필요한 정보까지 잃어버릴 수 있습니다.
가중치 감쇠는 가중치가 지나치게 커지는 것을 억제합니다. PyTorch의 옵티마이저에서는 보통 weight_decay라는 이름으로 설정합니다. 가중치가 너무 커지면 모델이 특정 입력 특징에 과하게 반응할 수 있습니다. 가중치 감쇠는 이를 부드럽게 눌러 주는 역할을 합니다.
데이터 증강은 학습 데이터를 조금씩 변형해 모델이 더 다양한 상황을 보게 만드는 방법입니다. 이미지에서는 좌우 반전, 자르기, 회전, 색상 변화 등을 사용할 수 있습니다. 텍스트에서는 단어 삭제, 치환, 역번역 같은 방법이 있지만 의미가 바뀌지 않도록 더 조심해야 합니다.
정규화 강도를 조정할 때는 학습 손실과 검증 손실의 차이를 봅니다. 학습 성능은 매우 좋은데 검증 성능이 나쁘다면 정규화를 강화할 수 있습니다. 하지만 학습 성능 자체가 낮다면 정규화를 더 강하게 하는 것은 오히려 문제를 키울 수 있습니다. 아직 배우지도 못한 학생에게 "외우지 마"라고만 말하는 셈입니다.
검증 세트는 튜닝의 심판이다
하이퍼파라미터 튜닝에서 가장 중요한 원칙은 검증 세트를 기준으로 비교한다는 것입니다.
학습 세트는 모델이 직접 배우는 데이터입니다. 모델은 이 데이터를 반복해서 보고 가중치를 조정합니다. 그래서 학습 세트 성능이 높다는 것은 중요하지만 충분하지 않습니다. 이미 본 문제를 잘 푸는 것과 처음 보는 문제를 잘 푸는 것은 다릅니다.
검증 세트는 학습에는 사용하지 않고, 실험 설정을 비교하는 데 사용합니다. 예를 들어 학습률 0.001과 0.003 중 무엇이 좋은지 판단할 때 검증 성능을 봅니다. 은닉층 크기 128과 256 중 무엇이 좋은지도 검증 세트로 비교합니다.
테스트 세트는 더 조심해야 합니다. 테스트 세트는 최종 평가를 위해 마지막까지 아껴 두는 데이터입니다. 하이퍼파라미터를 고를 때 테스트 세트를 계속 보면, 결국 테스트 세트에 맞춘 선택을 하게 됩니다. 겉으로는 테스트 성능이 좋아 보이지만 실제 새로운 데이터에서는 성능이 떨어질 수 있습니다. 이것을 테스트 세트에 대한 과적합이라고 볼 수 있습니다.
세 데이터의 역할을 정리하면 이렇습니다.
| 데이터 | 역할 | 사용 시점 |
|---|---|---|
| 학습 세트 | 가중치와 편향을 학습 | 매 에폭 반복 사용 |
| 검증 세트 | 하이퍼파라미터와 모델 선택 | 실험 비교에 사용 |
| 테스트 세트 | 최종 일반화 성능 평가 | 마지막에 한 번 사용 |
검증 세트는 실험의 심판입니다. 심판이 없으면 우리는 학습 정확도라는 달콤한 숫자에 속기 쉽습니다. 모델이 진짜로 좋아졌는지 알고 싶다면, 모델이 직접 배우지 않은 검증 데이터에서 확인해야 합니다.
하나씩 바꿀 것인가, 여러 개를 탐색할 것인가
가장 단순한 방법은 한 번에 하나의 하이퍼파라미터만 바꾸는 것입니다. 기준선에서 학습률만 바꾸고, 가장 나은 학습률을 고른 뒤 배치 크기를 바꾸는 식입니다. 이 방식은 이해하기 쉽고 결과 해석이 편합니다. 초반에는 좋은 접근입니다.
하지만 하이퍼파라미터들은 서로 영향을 주기 때문에 한 번에 하나씩만 보면 놓치는 조합이 생길 수 있습니다. 예를 들어 큰 배치 크기에서는 학습률을 조금 키워도 괜찮지만, 작은 배치 크기에서는 같은 학습률이 불안정할 수 있습니다. 드롭아웃이 큰 모델에서는 도움이 되지만 작은 모델에서는 학습을 방해할 수도 있습니다.
그래서 일정 단계 이후에는 여러 하이퍼파라미터 조합을 함께 탐색합니다. 대표적인 방법이 격자 탐색과 랜덤 서치입니다.
격자 탐색은 꼼꼼하지만 비싸다
격자 탐색은 미리 후보 목록을 정하고 가능한 모든 조합을 실험하는 방법입니다. 예를 들어 학습률 후보가 3개, 배치 크기 후보가 3개, 드롭아웃 후보가 3개라면 총 27번 실험합니다.
learning_rates = [1e-4, 1e-3, 1e-2]
batch_sizes = [32, 64, 128]
dropouts = [0.0, 0.2, 0.5]이 방법은 단순하고 빠뜨리는 조합이 없습니다. 후보가 적을 때는 좋습니다. 하지만 하이퍼파라미터가 조금만 늘어나도 실험 수가 폭발합니다. 후보가 5개인 하이퍼파라미터가 6개라면 5의 6제곱, 즉 15,625번의 실험이 필요합니다. 현실적으로 어렵습니다.
또 다른 문제도 있습니다. 모든 하이퍼파라미터가 똑같이 중요하지는 않습니다. 어떤 문제에서는 학습률이 성능을 크게 좌우하지만, 드롭아웃의 세부 값은 상대적으로 덜 중요할 수 있습니다. 그런데 격자 탐색은 모든 축을 같은 밀도로 훑습니다. 중요하지 않은 축에도 많은 실험을 써 버리는 셈입니다.
랜덤 서치는 의외로 강하다
랜덤 서치는 후보 공간에서 조합을 무작위로 뽑아 실험하는 방법입니다. 처음 들으면 허술해 보일 수 있습니다. 하지만 실제로는 매우 실용적입니다.
이유는 간단합니다. 딥러닝 하이퍼파라미터 중에는 성능에 큰 영향을 주는 값과 그렇지 않은 값이 섞여 있습니다. 격자 탐색은 모든 축을 정해진 간격으로만 봅니다. 반면 랜덤 서치는 여러 조합을 넓게 흩뿌리기 때문에 중요한 축에서 더 다양한 값을 만날 가능성이 있습니다.
예를 들어 학습률이 가장 중요하다고 해 봅시다. 격자 탐색에서 학습률 후보를 [0.0001, 0.001, 0.01] 세 개만 두면 그 사이의 좋은 값, 예를 들어 0.003을 놓칠 수 있습니다. 반면 랜덤 서치는 로그 범위에서 값을 뽑으면 0.0007, 0.002, 0.005 같은 다양한 값을 시도할 수 있습니다.
랜덤 서치는 특히 다음 상황에서 유용합니다.
탐색해야 할 하이퍼파라미터가 많을 때
각 하이퍼파라미터의 중요도를 아직 모를 때
실험 예산이 제한되어 모든 조합을 돌릴 수 없을 때
넓은 범위에서 좋은 구간을 먼저 찾고 싶을 때
간단한 랜덤 서치 코드는 다음처럼 만들 수 있습니다.
import random
import math
def sample_log_uniform(low, high):
log_low = math.log10(low)
log_high = math.log10(high)
return 10 ** random.uniform(log_low, log_high)
def sample_config():
return {
"lr": sample_log_uniform(1e-4, 1e-2),
"batch_size": random.choice([32, 64, 128]),
"hidden_size": random.choice([128, 256, 512]),
"dropout": random.choice([0.0, 0.2, 0.4]),
"weight_decay": sample_log_uniform(1e-6, 1e-3),
}
for i in range(10):
config = sample_config()
print(i, config)여기서 학습률과 가중치 감쇠는 로그 스케일로 뽑았습니다. 이런 값들은 0.0001과 0.001처럼 자릿수 차이가 중요하기 때문입니다. 반면 배치 크기나 은닉층 크기는 후보 목록에서 선택했습니다.
랜덤 서치가 무작정 운에 맡기는 방법은 아닙니다. 넓게 탐색한 뒤 좋은 구간이 보이면 그 주변을 다시 좁혀 탐색합니다. 처음에는 숲 전체를 보고, 그다음에는 나무가 많은 구역으로 들어가는 방식입니다.
실험은 공정해야 한다
하이퍼파라미터를 비교할 때는 실험 조건을 최대한 공정하게 맞춰야 합니다. 한 모델은 5에폭만 학습하고 다른 모델은 50에폭 학습했다면 비교가 어렵습니다. 한 실험은 데이터 증강을 쓰고 다른 실험은 쓰지 않았다면, 성능 차이가 학습률 때문인지 데이터 증강 때문인지 알 수 없습니다.
공정한 비교를 위해서는 몇 가지 원칙이 필요합니다.
같은 학습 데이터, 검증 데이터 분할을 사용한다.
같은 에폭 수 또는 같은 조기 종료 기준을 사용한다.
비교하려는 하이퍼파라미터 외의 설정은 최대한 고정한다.
가능한 경우 랜덤 시드를 기록하거나 고정한다.
최종 정확도뿐 아니라 손실 곡선과 학습 시간도 함께 본다.
랜덤 시드는 무작위성을 재현하기 위한 숫자입니다. 데이터 섞기, 가중치 초기화, 랜덤 증강 등에는 무작위성이 들어갑니다. 같은 설정인데 결과가 조금씩 달라질 수 있습니다. 그래서 실험을 기록할 때는 시드도 함께 남기는 것이 좋습니다.
PyTorch에서는 보통 다음처럼 시드를 고정합니다.
import random
import numpy as np
import torch
seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)다만 시드를 고정해도 GPU 연산이나 일부 라이브러리 설정 때문에 완전히 같은 결과가 나오지 않을 수 있습니다. 중요한 것은 "내가 어떤 조건에서 이 결과를 얻었는지" 최대한 추적 가능하게 만드는 것입니다.
실험 로그는 기억보다 믿을 만하다
튜닝을 하다 보면 실험이 빠르게 늘어납니다. 처음에는 머릿속으로 기억할 수 있을 것 같습니다. "아까 0.001이 좋았고, 드롭아웃 0.2가 괜찮았지." 하지만 열 번만 넘어가도 기억은 흐려집니다. 특히 여러 날에 걸쳐 실험하면 거의 반드시 헷갈립니다.
그래서 실험 로그가 필요합니다. 실험 로그는 모델 개발의 일기장입니다. 어떤 설정으로 학습했고, 어떤 결과가 나왔고, 어떤 판단을 했는지 남깁니다.
최소한 다음 항목은 기록하는 것이 좋습니다.
실험 이름 또는 번호
날짜와 실행한 코드 버전
데이터셋 버전과 전처리 방식
모델 구조와 주요 하이퍼파라미터
학습률, 배치 크기, 옵티마이저, 에폭 수
학습 손실, 검증 손실, 학습 정확도, 검증 정확도
가장 좋은 에폭과 최종 선택 여부
관찰한 문제와 다음 실험 아이디어
처음에는 거창한 도구가 없어도 됩니다. CSV 파일 하나로도 충분합니다.
import csv
result = {
"run_name": "mlp_lr_1e-3_dropout_0.2",
"lr": 1e-3,
"batch_size": 64,
"hidden_size": 256,
"dropout": 0.2,
"weight_decay": 1e-4,
"best_val_loss": 0.312,
"best_val_acc": 0.908,
"best_epoch": 14,
}
with open("experiments.csv", "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=result.keys())
if f.tell() == 0:
writer.writeheader()
writer.writerow(result)더 깔끔하게 관리하고 싶다면 설정을 딕셔너리로 만들고, 그 설정을 그대로 저장할 수 있습니다.
config = {
"model": "SimpleMLP",
"lr": 1e-3,
"batch_size": 64,
"hidden_size": 256,
"dropout": 0.2,
"weight_decay": 1e-4,
"epochs": 20,
"seed": 42,
}이렇게 설정을 한곳에 모으면 실험이 훨씬 정돈됩니다. 코드 중간중간에 숫자가 흩어져 있으면 나중에 무엇을 바꿨는지 찾기 어렵습니다. 좋은 실험 코드는 중요한 설정이 한눈에 보입니다.
손실 곡선이 말해 주는 것
하이퍼파라미터 튜닝에서 숫자 하나만 보면 위험합니다. 검증 정확도가 90%라고 해서 모든 것이 괜찮은 것은 아닙니다. 손실 곡선을 보면 모델이 어떻게 그 성능에 도달했는지 알 수 있습니다.
학습 손실과 검증 손실의 관계는 모델 상태를 읽는 중요한 단서입니다.
| 관찰 | 가능한 원인 | 시도해 볼 일 |
|---|---|---|
| 학습 손실과 검증 손실이 모두 높다 | 과소적합, 학습 부족, 학습률 문제 | 모델 키우기, 더 오래 학습, 학습률 조정 |
| 학습 손실은 낮고 검증 손실은 높다 | 과적합 | 정규화 강화, 데이터 증강, 모델 축소 |
| 손실이 크게 흔들리거나 발산한다 | 학습률이 큼, 배치가 너무 작음 | 학습률 낮추기, 배치 크기 조정 |
| 손실이 너무 천천히 감소한다 | 학습률이 작음, 모델 용량 부족 | 학습률 키우기, 모델 구조 조정 |
| 검증 손실이 특정 시점 이후 증가한다 | 과적합 시작 | 조기 종료, 정규화 강화 |
이 표는 진단표입니다. 모델이 아프다고 해서 무조건 같은 약을 먹이면 안 됩니다. 열이 나는지, 기침을 하는지, 배가 아픈지 봐야 합니다. 손실 곡선은 모델의 증상 기록입니다.
예를 들어 학습 정확도와 검증 정확도가 둘 다 낮다면 드롭아웃을 더 키우는 것은 좋은 처방이 아닐 가능성이 큽니다. 모델이 아직 충분히 배우지 못했는데 더 어렵게 만드는 셈입니다. 이때는 학습률을 조정하거나 모델 용량을 키우거나 학습 시간을 늘려야 할 수 있습니다.
반대로 학습 정확도는 99%인데 검증 정확도가 82%라면 모델이 학습 데이터를 외우고 있을 가능성이 큽니다. 이때는 드롭아웃, 가중치 감쇠, 데이터 증강, 모델 축소를 생각해 볼 수 있습니다.
조기 종료는 튜닝의 안전장치다
하이퍼파라미터를 실험하다 보면 어떤 설정은 빨리 실패합니다. 학습률이 너무 커서 손실이 발산하거나, 검증 성능이 초반부터 전혀 좋아지지 않을 수 있습니다. 이런 실험을 끝까지 돌리는 것은 시간 낭비입니다.
조기 종료는 검증 성능이 일정 기간 좋아지지 않으면 학습을 멈추는 방법입니다. 예를 들어 검증 손실이 5에폭 동안 개선되지 않으면 멈출 수 있습니다. 이때 가장 좋은 검증 성능을 보였던 에폭의 모델을 저장해 둡니다.
조기 종료는 두 가지 장점이 있습니다. 첫째, 과적합이 심해지기 전에 멈출 수 있습니다. 둘째, 나쁜 설정에 시간을 덜 쓰고 더 많은 설정을 실험할 수 있습니다.
간단한 형태는 다음과 같습니다.
best_val_loss = float("inf")
patience = 5
wait = 0
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, optimizer, criterion)
val_loss, val_acc = evaluate(model, val_loader, criterion)
if val_loss < best_val_loss:
best_val_loss = val_loss
wait = 0
torch.save(model.state_dict(), "best_model.pt")
else:
wait += 1
if wait >= patience:
print("Early stopping")
break여기서 patience는 기다려 줄 에폭 수입니다. 검증 손실이 잠깐 나빠졌다가 다시 좋아지는 경우도 있기 때문에 한 번 나빠졌다고 바로 멈추지는 않습니다.
작은 실습: 설정을 바꿔 가며 MLP 튜닝하기
이제 이전에 만들었던 MLP 숫자 분류기를 떠올려 봅시다. 이번에는 모델 코드 자체보다 실험 구조에 집중하겠습니다. 핵심은 설정을 딕셔너리로 관리하고, 결과를 비교할 수 있게 만드는 것입니다.
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim, hidden_size, num_classes, dropout):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_size),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_size, hidden_size),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_size, num_classes),
)
def forward(self, x):
x = x.view(x.size(0), -1)
return self.net(x)모델을 만들 때 하이퍼파라미터를 직접 숫자로 박아 넣지 않고 config에서 가져오게 합니다.
config = {
"input_dim": 28 * 28,
"hidden_size": 256,
"num_classes": 10,
"dropout": 0.2,
"lr": 1e-3,
"weight_decay": 1e-4,
"batch_size": 64,
"epochs": 20,
}
model = MLP(
input_dim=config["input_dim"],
hidden_size=config["hidden_size"],
num_classes=config["num_classes"],
dropout=config["dropout"],
)
optimizer = torch.optim.Adam(
model.parameters(),
lr=config["lr"],
weight_decay=config["weight_decay"],
)
criterion = nn.CrossEntropyLoss()이 구조의 장점은 분명합니다. 실험할 때 config만 바꾸면 됩니다. 모델 코드와 학습 루프를 매번 고칠 필요가 없습니다.
여러 설정을 반복해서 실험하려면 다음처럼 구성할 수 있습니다.
configs = [
{"hidden_size": 128, "dropout": 0.2, "lr": 1e-3, "weight_decay": 1e-4},
{"hidden_size": 256, "dropout": 0.2, "lr": 1e-3, "weight_decay": 1e-4},
{"hidden_size": 256, "dropout": 0.4, "lr": 1e-3, "weight_decay": 1e-4},
{"hidden_size": 256, "dropout": 0.2, "lr": 3e-4, "weight_decay": 1e-4},
]
for i, config in enumerate(configs):
print(f"실험 {i}: {config}")
# 1. 모델 생성
# 2. 옵티마이저 생성
# 3. 학습
# 4. 검증 성능 기록실제 프로젝트에서는 이 반복 구조가 매우 중요합니다. 좋은 모델은 한 번의 영감으로 나오기보다, 잘 기록된 여러 번의 실험에서 나옵니다.
튜닝할 때 자주 하는 실수
하이퍼파라미터 튜닝은 익숙해질수록 재미있지만, 초반에는 함정이 많습니다. 특히 다음 실수를 조심해야 합니다.
테스트 세트를 반복해서 보며 설정을 고른다.
여러 하이퍼파라미터를 한꺼번에 바꾸고 결과를 해석하려 한다.
실험 로그를 남기지 않아 좋은 설정을 잊어버린다.
검증 정확도 하나만 보고 손실 곡선을 보지 않는다.
너무 작은 데이터 일부만 보고 최종 결론을 내린다.
랜덤 시드나 데이터 분할이 매번 달라 공정하게 비교하지 못한다.
이 중 가장 치명적인 것은 테스트 세트를 튜닝에 사용하는 것입니다. 테스트 세트는 마지막 시험지입니다. 공부 방향을 정하려고 마지막 시험지를 계속 훔쳐보면, 그 시험지에는 강해질 수 있지만 진짜 실력은 알 수 없습니다.
또 하나의 흔한 실수는 "마지막으로 한 설정이 최고"라고 믿는 것입니다. 사람은 최근 결과를 더 잘 기억합니다. 그래서 로그가 필요합니다. 로그는 감정을 빼고 기록을 보여 줍니다. 모델 개발에서 기억은 자주 과장하지만, 기록은 대체로 정직합니다.
성능만 보지 말고 비용도 보자
튜닝의 목표는 검증 성능을 높이는 것입니다. 하지만 성능만이 전부는 아닙니다. 모델이 너무 크면 학습 시간이 길어지고, 추론 속도가 느려지고, 배포 비용이 커질 수 있습니다.
예를 들어 검증 정확도가 90.1%인 작은 모델과 90.3%인 거대한 모델이 있다고 해 봅시다. 숫자만 보면 거대한 모델이 조금 더 좋습니다. 하지만 학습 시간이 10배 길고, 서비스에서 응답 속도가 느리다면 작은 모델이 더 나은 선택일 수 있습니다.
따라서 실험 로그에는 성능뿐 아니라 비용도 함께 기록하는 것이 좋습니다. 학습 시간, GPU 메모리 사용량, 모델 파라미터 수, 추론 시간 같은 값이 도움이 됩니다. 특히 앞으로 다룰 Transformer와 LLM에서는 이 문제가 훨씬 중요해집니다. 모델이 커질수록 성능만이 아니라 계산 비용이 핵심 제약이 됩니다.
하이퍼파라미터 튜닝은 단지 최고 점수를 찾는 일이 아닙니다. 주어진 자원 안에서 가장 좋은 균형점을 찾는 일입니다.
좋은 튜닝 과정의 예
이제 전체 흐름을 하나의 이야기로 묶어 보겠습니다. 손글씨 숫자 분류 모델을 만든다고 해 봅시다. 처음 기준선 모델은 은닉층 128개, 드롭아웃 0.0, 학습률 0.001, 배치 크기 64입니다. 검증 정확도는 92%입니다.
먼저 학습률을 탐색합니다. 0.0001은 너무 느리고, 0.01은 손실이 불안정합니다. 0.001과 0.003이 괜찮습니다. 검증 손실을 보니 0.001이 조금 더 안정적입니다. 학습률은 일단 0.001로 둡니다.
다음으로 배치 크기를 봅니다. 32, 64, 128을 비교합니다. 64가 속도와 성능의 균형이 좋습니다. 128은 빠르지만 검증 성능이 조금 낮습니다. 배치 크기는 64로 둡니다.
이제 모델 크기를 조정합니다. 은닉층 128, 256, 512를 비교합니다. 256에서 검증 정확도가 94%로 오릅니다. 512는 학습 정확도는 더 높지만 검증 정확도는 비슷하고 학습 시간이 늘어납니다. 은닉층은 256을 선택합니다.
마지막으로 정규화를 봅니다. 드롭아웃 0.0, 0.2, 0.4를 비교합니다. 0.2에서 검증 손실이 가장 낮습니다. 0.4는 학습 자체가 조금 어려워집니다. 드롭아웃은 0.2로 둡니다. 여기에 weight decay를 0, 1e-5, 1e-4로 비교해 보고 1e-4가 가장 안정적이면 최종 후보로 삼습니다.
이 과정은 화려하지 않습니다. 하지만 강력합니다. 숫자를 감으로 던진 것이 아니라, 하나씩 질문을 던지고 검증 세트로 답을 확인했습니다. 이것이 좋은 튜닝입니다.
하이퍼파라미터 튜닝은 실험 설계다
이 장의 핵심 문장을 다시 꺼내 보겠습니다. 하이퍼파라미터 튜닝은 감으로 숫자를 바꾸는 일이 아니라 실험을 설계하는 일입니다.
좋은 튜닝은 다음 흐름을 따릅니다.
기준선 모델을 만든다.
검증 세트를 기준으로 성능을 비교한다.
학습률, 배치 크기, 모델 크기, 정규화 강도 순서로 주요 설정을 조정한다.
격자 탐색과 랜덤 서치를 상황에 맞게 사용한다.
실험 로그를 남겨 재현 가능한 과정을 만든다.
성능뿐 아니라 학습 시간과 계산 비용도 함께 본다.
딥러닝 모델은 스스로 가중치를 배웁니다. 하지만 어떤 환경에서 배울지는 우리가 설계합니다. 좋은 하이퍼파라미터는 모델이 길을 잘 찾도록 도와주는 지도와 같습니다. 지도가 엉망이면 아무리 똑똑한 모델도 헤맵니다.
이제 우리는 모델을 더 잘 학습시키기 위해 어떤 설정을 조정해야 하는지 알게 되었습니다. 그런데 실험을 많이 하려면 피할 수 없는 문제가 하나 있습니다. 시간이 오래 걸립니다. 모델이 커지고 데이터가 많아질수록 CPU만으로는 답답해집니다. 다음 장에서는 딥러닝 학습 속도를 크게 바꾸는 장치, GPU를 살펴보겠습니다. 왜 GPU가 신경망 학습에 강한지, 그리고 PyTorch에서 GPU를 어떻게 사용하는지 이어서 알아봅니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.