
딥러닝 모델의 성능을 극대화하기 위해 Neural Architecture Search (NAS)는 이제 선택이 아닌 필수가 되어가고 있습니다. 하지만 현업 개발자들에게 가장 큰 장벽은 역시 '비용'과 '시간'입니다. 구글의 초창기 NAS 연구처럼 수천 대의 GPU를 사용하는 것은 현실적으로 불가능합니다. 본 포스팅에서는 파이썬 환경에서 NAS를 구축할 때, 성능 손실 없이 컴퓨팅 자원을 획기적으로 절약할 수 있는 실무 지향적 전략을 다룹니다.
1. NAS의 비용 문제와 현실적인 해결 패러다임
전통적인 NAS는 수천 개의 후보 모델을 처음부터 끝까지 학습시키는 방식(Black-box optimization)을 취했습니다. 하지만 최신 트렌드는 Weight Sharing(가중치 공유)과 One-Shot Architecture Search로 이동했습니다. 이는 하나의 거대한 'Supernet'을 학습시킨 뒤, 그 하위 경로를 선택하는 방식으로 전체 학습 시간을 수백 배 단축시킵니다.
NAS 접근 방식 비교 분석
| 비교 항목 | 전통적 NAS (RL/EA 기반) | One-Shot NAS (Weight Sharing) | Differentiable NAS (DARTS) |
|---|---|---|---|
| 학습 비용 | 매우 높음 (GPU Days 1000+) | 낮음 (GPU Days 1~10) | 매우 낮음 (GPU Days 1 이내) |
| 구현 난이도 | 보통 | 높음 (Supernet 설계 필요) | 중간 (Gradient 기반) |
| 유연성 | 매우 높음 (모든 탐색 공간) | 제한적 (Sub-network 구조) | 중간 (Continuous space) |
| 현업 추천도 | 비추천 (R&D 전용) | 강력 추천 (대규모 모델) | 추천 (빠른 프로토타이핑) |
2. 실무 적용 가능한 NAS 최적화 코드 예제 (Python)
현업에서 바로 활용할 수 있는 PyTorch 기반의 NAS 핵심 컴포넌트 7가지를 소개합니다. 각 예제는 메모리 효율성과 학습 속도 개선에 초점을 맞췄습니다.
Example 1: 가중치 공유를 위한 Supernet Layer 정의
import torch
import torch.nn as nn
class MixedOp(nn.Module):
"""
여러 연산(3x3 conv, 5x5 conv, maxpool) 중 하나를 선택하는 Supernet의 핵심 단위.
가중치를 공유하여 메모리 점유율을 최소화합니다.
"""
def __init__(self, in_channels, out_channels, stride):
super(MixedOp, self).__init__()
self._ops = nn.ModuleList([
nn.Sequential(nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False), nn.BatchNorm2d(out_channels)),
nn.Sequential(nn.Conv2d(in_channels, out_channels, 5, stride=stride, padding=2, bias=False), nn.BatchNorm2d(out_channels)),
nn.MaxPool2d(3, stride=stride, padding=1)
])
def forward(self, x, weights):
# DARTS 방식: 가중치(alpha)를 통해 연산들의 소프트맥스 합을 계산
return sum(w * op(x) for w, op in zip(weights, self._ops))
Example 2: Gumbel-Softmax를 이용한 하드웨어 친화적 탐색
import torch.nn.functional as F
def select_architecture(alphas, temperature=1.0):
"""
미분 가능한 방식으로 하나의 경로만 선택(Discrete selection)하여
학습 시 연산 비용을 30% 이상 절감합니다.
"""
# Gumbel-Softmax는 학습 시에는 미분 가능하고, 추론 시에는 One-hot에 가깝게 동작함
return F.gumbel_softmax(alphas, tau=temperature, hard=True)
Example 3: 효율적인 탐색 공간(Search Space) 제한 전략
# 현업에서는 무한한 탐색 공간보다 '성능이 검증된 블록' 내에서 탐색하는 것이 효율적입니다.
SEARCH_SPACE = {
'kernel_size': [3, 5, 7],
'expansion_ratio': [3, 4, 6],
'depth': [2, 3, 4]
}
def get_search_space_size(space):
import numpy as np
return np.prod([len(v) for v in space.values()])
print(f"Total combinations: {get_search_space_size(SEARCH_SPACE)}")
Example 4: Early Exit 기반의 비용 절감 스케줄러
def should_stop_search(current_top1, history, threshold=0.01):
"""
성능 향상이 미미할 경우 탐색을 조기 종료하여 GPU 비용을 아낍니다.
"""
if len(history) < 5: return False
recent_avg = sum(history[-5:]) / 5
return (current_top1 - recent_avg) < threshold
Example 5: Latency-Aware Loss Function (지연 시간 제약 조건)
def nas_loss(prediction, target, model_latency, target_latency, lambda_val=0.1):
"""
성능뿐만 아니라 추론 속도(Latency)를 손실 함수에 포함시켜
현업 모바일/엣지 디바이스 최적화를 강제합니다.
"""
ce_loss = nn.CrossEntropyLoss()(prediction, target)
# 지연 시간이 목표치를 초과할 경우 페널티 부여
latency_penalty = torch.log(model_latency / target_latency) ** 2
return ce_loss + lambda_val * latency_penalty
Example 6: Random Sampling Base-line 구축
import random
def random_search_baseline(search_space, n_trials=10):
"""
복잡한 NAS를 돌리기 전, 반드시 Random Search 결과를 확인하십시오.
종종 NAS는 Random Search보다 아주 약간 나은 결과를 위해 막대한 비용을 씁니다.
"""
best_config = None
for _ in range(n_trials):
config = {k: random.choice(v) for k, v in search_space.items()}
# evaluate(config) ...
return best_config
Example 7: 텐서보드를 활용한 아키텍처 수렴 시각화
from torch.utils.tensorboard import SummaryWriter
def log_nas_progress(writer, epoch, alphas, ops_names):
"""
어떤 연산이 선택되고 있는지 실시간 모니터링하여
잘못된 탐색 경로(Collapse)를 조기에 발견합니다.
"""
probs = F.softmax(alphas, dim=-1)
for i, name in enumerate(ops_names):
writer.add_scalar(f'Arch_Prob/{name}', probs[i], epoch)
3. 현업에서 NAS 적용 시 마주하는 3가지 문제와 해결책
문제 01: 메모리 부족 (OOM)
Supernet은 모든 후보 연산을 메모리에 올려야 하므로 일반 모델보다 메모리 사용량이 몇 배나 높습니다. 이를 해결하기 위해 ProxylessNAS에서 제안한 Path Binarization 기법을 사용하십시오. 한 번에 하나의 경로만 활성화하여 메모리 부하를 일반 모델 수준으로 낮출 수 있습니다.
문제 02: 아키텍처 붕괴 (Architecture Collapse)
학습 초기에 Skip-connection 같은 단순한 연산의 가중치가 급격히 높아져 탐색이 제대로 이뤄지지 않는 현상입니다. Dropout을 후보 연산에 적용하거나, 초기 Epoch 동안 아키텍처 파라미터 업데이트를 고정(Warm-up)하는 방법으로 해결 가능합니다.
문제 03: 하드웨어 의존성
V100에서 빠른 모델이 모바일 기기에서도 빠른 것은 아닙니다. 반드시 실제 타겟 디바이스의 Lookup Table(LUT)을 구축하여 NAS 비용 함수에 연산 속도를 포함시켜야 합니다.
4. 결론: 성공적인 NAS 도입을 위한 체크리스트
- Small Proxy Task: 전체 데이터셋이 아닌 10%의 데이터로 먼저 탐색하세요.
- Standard Backbone: 처음부터 끝까지 다 찾으려 하지 말고, MobileNetV3나 EfficientNet의 특정 블록만 Search 공간으로 설정하세요.
- Cost vs Performance: NAS로 얻는 정확도 1% 향상이 GPU 비용 1,000만 원의 가치가 있는지 사업적 관점에서 검토하세요.
참고 문헌 및 출처
- Liu, H., Simonyan, K., & Yang, Y. (2018). DARTS: Differentiable Architecture Search. arXiv.
- Cai, H., Zhu, L., & Han, S. (2018). ProxylessNAS: Direct Neural Architecture Search on Target Task and Hardware. ICLR.
- Bender, G., et al. (2018). Understanding and Simplifying One-Shot Architecture Search. ICML.
- Phuong, M., & Lampert, C. H. (2019). Distillation-Based Training for Neural Architecture Search.