Nöral Mimari Arama ve AutoML: Ağ Tasarımını Otomatikleştirme
Bir sinir ağı mimarisinin tasarlanması geleneksel olarak manuel bir işlemdir. Yıllar süren uzmanlık, sezgi ve deneme amaçlı hesaplama kaynakları. ResNet, EfficientNet, MobileNet — bu ikonik mimarilerin her biri insanın tasarım tercihlerinin sonucudur derinlemesine bilgilendirildi. Yine de onunla Sinir Mimarisi Araması (NAS), bu süreç otomatikleştirilebilir: bir hesaplama bütçesi ve bir görev verildiğinde, bir algoritma olası mimarilerin alanını araştırır ve en uygun olanı tanımlar.
Pratik ve şaşırtıcı sonuç. EfficientNet – muhtemelen en etkili CNN ailesi son yıllarda NAS aracılığıyla keşfedildi. NASNet, DARTS, Once-for-All ve diğer modeller NAS, standartlaştırılmış kıyaslamalarda manuel olarak tasarlanmış mimarileri sürekli olarak geride bıraktı. Ancak gerçek devrim demokratikleşmedir: gibi araçlarla Optuna, Ray Ayarı, ENAS e Timm, bugün bunu yapmak mümkün NAS'ı tüketici GPU'larında saatler içinde kullanarak mimariyi özel donanımınıza göre optimize edin.
Bu kılavuzda, GridSearch'ten DARTS'a kadar NAS tekniklerini sıfırdan inceliyoruz. ve derin öğrenme mimarilerini optimize etmek için Optuna ile pratik bir AutoML hattı oluşturuyoruz.
Ne Öğreneceksiniz
- NAS nedir ve neden manuel tasarımı geride bırakıyor?
- Arama alanları: mikro (hücre tabanlı) ve makro (katman tabanlı)
- Arama stratejileri: Rastgele Arama, RL, Evrimsel, DARTS
- Tek Seferde NAS ve Ağırlık Paylaşımı: Yıllar süren maliyet nasıl saatlere düşürülür?
- Optuna ile NAS uygulaması: hiperparametre + mimari arama
- Tam PyTorch ile Farklılaştırılabilir Mimari Arama (DARTS)
- Hepsi Bir Arada Ağlar: Heterojen donanımlara yönelik mimariler
- Donanım Duyarlı NAS: Gecikme, FLOP'lar ve parametreler için optimize edin
- Uç cihazlar için AutoKeras ve NAS ile AutoML
- Gerçek Örnek Olay İncelemesi: Jetson Nano'da Tıbbi Sınıflandırma için NAS
NAS Neden Manuel Tasarımı Aşıyor?
Manuel mimari tasarımı üç yapısal sınırlamadan muzdariptir. İlk olarak,uzmanlık önyargı: araştırmacılar tanıdık kalıpları yeniden kullanma eğilimindedir (ResBlock'lar, bağlantıları atlama) belirli bir görev için ideal olmasalar bile. İkincisi,donanım uyuşmazlığı: A100'de optimal mimari nadiren Cortex-A55'te bulunur. Üçüncüsü, kombinatoryal patlama: olası mimarilerin alanı e astronomik derecede büyük - yalnızca 8 katmandaki katmanların, kanalların ve çekirdek boyutlarının sayısı değiştirildiğinde bile 10^14'ten fazla konfigürasyon elde edersiniz.
NAS, tasarım problemini resmi olarak tanımlayarak bu sorunları çözer:
# FORMALIZZAZIONE DEL PROBLEMA NAS
#
# Input:
# - Spazio di ricerca A: insieme di possibili architetture
# - Dataset D = (D_train, D_val)
# - Funzione di costo c(a, D): misura la bonta di a su D
# - Budget computazionale B
#
# Output:
# - a* = argmin_{a in A} c(a, D_val) s.t. cost(search) <= B
#
# Il costo tipicamente include:
# - Validation accuracy (minimizzare errore)
# - Latenza su hardware target
# - Numero di parametri
# - Consumo energetico
#
# Esempio di funzione costo multi-obiettivo:
# c(a) = (1 - val_acc) + lambda * latency_ms / latency_target
#
# Dove lambda bilancia accuracy e efficiency
import torch
import torch.nn as nn
from typing import Dict, Any, Optional
class NASObjective:
"""
Funzione obiettivo per NAS multi-obiettivo.
Combina accuracy, latenza e dimensione del modello.
"""
def __init__(
self,
accuracy_weight: float = 1.0,
latency_weight: float = 0.1,
params_weight: float = 0.01,
latency_target_ms: float = 10.0,
params_target_M: float = 5.0
):
self.w_acc = accuracy_weight
self.w_lat = latency_weight
self.w_par = params_weight
self.lat_target = latency_target_ms
self.par_target = params_target_M * 1e6
def __call__(
self,
val_accuracy: float,
latency_ms: float,
n_params: int
) -> float:
"""
Calcola il costo composito. Più basso = meglio.
val_accuracy: [0, 1] - vogliamo massimizzarla
latency_ms: millisecondi - vogliamo minimizzarla
n_params: numero parametri - vogliamo minimizzarli
"""
acc_cost = self.w_acc * (1.0 - val_accuracy)
lat_cost = self.w_lat * max(0, latency_ms / self.lat_target - 1.0)
par_cost = self.w_par * max(0, n_params / self.par_target - 1.0)
return acc_cost + lat_cost + par_cost
# Esempio di uso:
obj = NASObjective(latency_target_ms=5.0, params_target_M=2.0)
cost = obj(val_accuracy=0.92, latency_ms=4.2, n_params=1_800_000)
print(f"Costo composito: {cost:.4f}") # ~0.08
NAS Araştırma Alanı
NAS'ın kalbi ve tanımı arama alanı: tümünün kümesi Algoritmanın keşfedebileceği olası mimariler. Arama alanı seçimi Temeldir; çok dardır ve optimuma ulaşılamaz, çok geniştir ve arama hesaplama açısından zorlu hale gelir.
İki ana yaklaşım vardır:
- Hücre tabanlı NAS (mikro arama alanı): optimal yapısını arıyoruz tek bir hücre (hücre), daha sonra hücre ağı oluşturmak için birkaç kez kopyalanır. Bu esnekliği korurken arama alanını büyük ölçüde azaltır. NASNet, DARTS, ENAS tarafından kullanılır.
- Makro arama alanı: gibi global mimari parametrelerini arıyorsunuz katman sayısı, kanalların boyutu, bağlantı türü. EfficientNet tarafından kullanılır (NAS + ölçeklendirme), MobileNet v3, Hepsi Bir Arada.
# Esempio di spazio di ricerca macro per una CNN
# Ogni dimensione e una scelta discreta o continua
SEARCH_SPACE = {
# Struttura globale
"n_layers": [4, 6, 8, 10, 12], # Numero di layer
"initial_channels": [16, 32, 48, 64], # Canali iniziali
"width_multiplier": [0.5, 0.75, 1.0, 1.25, 1.5], # Moltiplicatore larghezza
# Per ogni layer:
"kernel_sizes": [3, 5, 7], # Dimensione kernel
"expansion_ratios": [1, 2, 4, 6], # Ratio espansione MBConv
"se_ratios": [0.0, 0.25, 0.5], # Squeeze-and-Excitation ratio
"skip_ops": ["identity", "conv", "pool"], # Tipo skip connection
# Configurazione attention (per reti ibride CNN+Transformer)
"use_attention": [False, True],
"attention_heads": [1, 2, 4, 8],
}
# Stima dimensione spazio di ricerca
import math
n_configs = (
len(SEARCH_SPACE["n_layers"]) *
len(SEARCH_SPACE["initial_channels"]) *
len(SEARCH_SPACE["width_multiplier"]) *
len(SEARCH_SPACE["kernel_sizes"]) ** 8 * # 8 layer
len(SEARCH_SPACE["expansion_ratios"]) ** 8
)
print(f"Configurazioni possibili: {n_configs:.2e}")
# ~10^14: impossibile esplorazione esaustiva!
# CELL-BASED SEARCH SPACE (molto più compatto)
# In NASNet/DARTS: cerca solo la struttura della cell
CELL_SEARCH_SPACE = {
"n_nodes": [3, 4, 5], # Nodi interni per cella
"ops_per_edge": [ # Operazioni candidate per edge
"sep_conv_3x3",
"sep_conv_5x5",
"dil_conv_3x3",
"dil_conv_5x5",
"avg_pool_3x3",
"max_pool_3x3",
"skip_connect",
"none"
],
"n_cells": [6, 8, 10, 12, 14], # Quante celle nella rete
"init_channels": [16, 24, 32, 36], # Canali iniziali
}
# Spazio ridotto: ~10^4 configurazioni invece di 10^14!
Araştırma Stratejileri
Arama stratejileri, algoritmanın mimari alanda nasıl gezineceğini belirler. Strateji seçimi arama alanının kendisi kadar önemlidir:
| Strateji | Yaklaşmak | Profesyonel | Aykırı | Tipik maliyet |
|---|---|---|---|---|
| Rastgele Arama | Rastgele örnekleme | Basit, güçlü temel | Verimsiz | N * eğitimi tamamla |
| Izgara Arama | Kapsamlı ızgara | Alan küçükse tamamlayın | Üstel boyut | K^D * eğitimi |
| Bayes Opt. | Taşıyıcı model + satın alma | Verimli, yönlendirilmiş | Geniş alanlar için pahalı | 50-200 deneme |
| RL (NASNet) | RNN denetleyicileri | Karmaşık mimariler | 400 orijinal GPU günü | 1000'den fazla deneme |
| Evrimsel | Genetik algoritmalar | Mutlu keşifler | Çok yavaş | 500+ deneme |
| DART | Sürekli farklılaşma | 1-4 GPU günü, optimum | Bellek yoğun | 1 eğitim döngüsü |
| Tek Çekim / ENAS | Ağırlık paylaşımı süper ağı | Tek GPU'da geçirilen saatler | Sıralama yaklaşımı | 1 süper ağ + örnekleme |
Optuna ile Pratik NAS
Optuna ve hiperparametreleri ve mimarileri aramak için en çok kullanılan kütüphane. Bayes örneklemesini (TPE), ümit vermeyen denemelerin budanmasını ve bir API'yi birleştirir. PyTorch ile doğal bir şekilde bütünleşen zarif.
# pip install optuna optuna-integration[pytorch] torch torchvision
import optuna
from optuna.trial import Trial
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# ============================================================
# SPAZIO DI RICERCA: architettura CNN flessibile
# ============================================================
class FlexibleCNN(nn.Module):
"""
CNN con architettura parametrizzata per NAS.
Supporta 2-5 convolutional blocks con kernel e canali variabili.
"""
def __init__(self, n_conv_layers: int, channels: list,
kernel_sizes: list, use_bn: bool,
dropout_rate: float, n_classes: int = 10):
super().__init__()
layers = []
in_channels = 3
for i in range(n_conv_layers):
out_channels = channels[i]
k = kernel_sizes[i]
layers.extend([
nn.Conv2d(in_channels, out_channels, k, padding=k//2),
nn.BatchNorm2d(out_channels) if use_bn else nn.Identity(),
nn.ReLU(inplace=True),
nn.MaxPool2d(2) if i < n_conv_layers - 1 else nn.AdaptiveAvgPool2d(4)
])
in_channels = out_channels
self.features = nn.Sequential(*layers)
final_size = channels[-1] * 16
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Dropout(dropout_rate),
nn.Linear(final_size, 512),
nn.ReLU(),
nn.Dropout(dropout_rate / 2),
nn.Linear(512, n_classes)
)
def forward(self, x):
return self.classifier(self.features(x))
# ============================================================
# OBJECTIVE FUNCTION per Optuna
# ============================================================
def objective(trial: Trial) -> float:
"""
Funzione obiettivo: addestra una architettura e restituisce val_accuracy.
Optuna chiamera questa funzione centinaia di volte con configurazioni diverse.
"""
# === SPAZIO DI RICERCA ===
n_conv_layers = trial.suggest_int("n_conv_layers", 2, 5)
channels = [
trial.suggest_categorical(f"channels_{i}", [32, 64, 96, 128, 192, 256])
for i in range(n_conv_layers)
]
kernel_sizes = [
trial.suggest_categorical(f"kernel_{i}", [3, 5])
for i in range(n_conv_layers)
]
use_bn = trial.suggest_categorical("use_bn", [True, False])
dropout_rate = trial.suggest_float("dropout", 0.1, 0.5)
# Iperparametri di training
lr = trial.suggest_float("lr", 1e-4, 1e-2, log=True)
batch_size = trial.suggest_categorical("batch_size", [64, 128, 256])
optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "SGD", "AdamW"])
weight_decay = trial.suggest_float("weight_decay", 1e-5, 1e-2, log=True)
# === DATI ===
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761))
])
transform_val = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761))
])
train_set = torchvision.datasets.CIFAR100(
root='./data', train=True, download=True, transform=transform_train
)
val_set = torchvision.datasets.CIFAR100(
root='./data', train=False, download=True, transform=transform_val
)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=4)
val_loader = DataLoader(val_set, batch_size=256, shuffle=False, num_workers=4)
# === MODELLO ===
device = "cuda" if torch.cuda.is_available() else "cpu"
model = FlexibleCNN(n_conv_layers, channels, kernel_sizes, use_bn,
dropout_rate, n_classes=100).to(device)
# Optimizer
if optimizer_name == "Adam":
optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
elif optimizer_name == "AdamW":
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
else:
optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9,
weight_decay=weight_decay)
criterion = nn.CrossEntropyLoss()
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=15)
# === TRAINING (15 epoche per trial veloce) ===
for epoch in range(15):
model.train()
for imgs, labels in train_loader:
imgs, labels = imgs.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(imgs), labels)
loss.backward()
optimizer.step()
scheduler.step()
# Pruning: elimina trial non promettenti dopo le prime 5 epoche
if epoch >= 4:
model.eval()
correct = total = 0
with torch.no_grad():
for imgs, labels in val_loader:
imgs, labels = imgs.to(device), labels.to(device)
preds = model(imgs).argmax(1)
correct += (preds == labels).sum().item()
total += labels.size(0)
val_acc = correct / total
# Segnala a Optuna per pruning
trial.report(val_acc, epoch)
if trial.should_prune():
raise optuna.exceptions.TrialPruned()
# Accuracy finale
model.eval()
correct = total = 0
with torch.no_grad():
for imgs, labels in val_loader:
imgs, labels = imgs.to(device), labels.to(device)
preds = model(imgs).argmax(1)
correct += (preds == labels).sum().item()
total += labels.size(0)
return correct / total # Optuna massimizza questo valore
# ============================================================
# AVVIO STUDIO OPTUNA
# ============================================================
study = optuna.create_study(
direction="maximize",
sampler=optuna.samplers.TPESampler(seed=42), # Bayesian (Tree-structured Parzen Estimator)
pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=5)
)
# Esegui 100 trial (parallelizzabile con n_jobs)
study.optimize(objective, n_trials=100, timeout=3600)
# === RISULTATI ===
best_trial = study.best_trial
print(f"Miglior accuratezza: {best_trial.value:.4f}")
print(f"Miglior configurazione:")
for key, val in best_trial.params.items():
print(f" {key}: {val}")
# Visualizzazione importanza parametri
fig = optuna.visualization.plot_param_importances(study)
fig.show() # Richiede plotly
DARTS: Türevlenebilir Mimari Araması
DART (Liu ve diğerleri, 2019) ve en şık NAS algoritmalarından biri etkili. Anahtar fikir: Operasyon seçimini yapın sürekli ve türevlenebilir, ayrık arama yerine degrade inişle mimariyi optimize etmenize olanak tanır.
Bir DARTS hücresinde düğümler arasındaki her kenarın bir değeri vardır. yumuşak karışım mümkün olan her şeyden
işlemler (dönüşüm 3x3, dönüşüm 5x5, atlama, havuz). Karışım ağırlıkları (mimari parametreler)
alpha) model ağırlıklarıyla birlikte gradyan inişiyle optimize edilmiştir.
Sonunda her kenar için en yüksek ağırlığa sahip işlemi seçiyoruz (ayrıklaştırma).
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import List
# ============================================================
# PRIMITIVE OPERATIONS per la cella DARTS
# ============================================================
OPS = {
'none': lambda C, stride: Zero(stride),
'skip_connect': lambda C, stride: nn.Identity() if stride == 1 else FactorizedReduce(C),
'sep_conv_3x3': lambda C, stride: SepConv(C, C, 3, stride, 1),
'sep_conv_5x5': lambda C, stride: SepConv(C, C, 5, stride, 2),
'dil_conv_3x3': lambda C, stride: DilConv(C, C, 3, stride, 2, 2),
'avg_pool_3x3': lambda C, stride: nn.AvgPool2d(3, stride, 1, count_include_pad=False),
'max_pool_3x3': lambda C, stride: nn.MaxPool2d(3, stride, 1),
}
PRIMITIVES = list(OPS.keys())
class SepConv(nn.Module):
"""Depthwise separable convolution."""
def __init__(self, C_in, C_out, kernel_size, stride, padding):
super().__init__()
self.op = nn.Sequential(
nn.ReLU(),
nn.Conv2d(C_in, C_in, kernel_size, stride, padding, groups=C_in, bias=False),
nn.Conv2d(C_in, C_out, 1, bias=False),
nn.BatchNorm2d(C_out),
nn.ReLU(),
nn.Conv2d(C_out, C_out, kernel_size, 1, padding, groups=C_out, bias=False),
nn.Conv2d(C_out, C_out, 1, bias=False),
nn.BatchNorm2d(C_out)
)
def forward(self, x):
return self.op(x)
class Zero(nn.Module):
def __init__(self, stride):
super().__init__()
self.stride = stride
def forward(self, x):
if self.stride == 1:
return x.mul(0.)
return x[:, :, ::self.stride, ::self.stride].mul(0.)
# ============================================================
# MIXED OPERATION: softmax su tutte le operazioni
# ============================================================
class MixedOp(nn.Module):
"""
Soft mixture di K operazioni con pesi alpha (architecture parameters).
output = sum_k(softmax(alpha)[k] * op_k(input))
"""
def __init__(self, C: int, stride: int):
super().__init__()
self._ops = nn.ModuleList()
for prim in PRIMITIVES:
op = OPS[prim](C, stride)
# Aggiungi BN dopo pool ops (standard DARTS)
if 'pool' in prim:
op = nn.Sequential(op, nn.BatchNorm2d(C))
self._ops.append(op)
def forward(self, x: torch.Tensor, weights: torch.Tensor) -> torch.Tensor:
"""weights: softmax(alpha) per questo edge."""
return sum(w * op(x) for w, op in zip(weights, self._ops))
# ============================================================
# CELLA DARTS (Normal Cell)
# ============================================================
class DARTSCell(nn.Module):
"""
Cella DARTS con N_NODES nodi intermedi.
Ogni nodo e la somma di tutti gli input precedenti pesati da alpha.
"""
N_NODES = 4
def __init__(self, C: int, reduction: bool = False):
super().__init__()
self.reduction = reduction
stride = 2 if reduction else 1
# Input preprocessing
self.preprocess0 = nn.Sequential(
nn.ReLU(), nn.Conv2d(C, C, 1, bias=False), nn.BatchNorm2d(C)
)
self.preprocess1 = nn.Sequential(
nn.ReLU(), nn.Conv2d(C, C, 1, bias=False), nn.BatchNorm2d(C)
)
# Mixed operations per ogni edge
self._ops = nn.ModuleList()
for i in range(self.N_NODES):
for j in range(2 + i): # Ogni nodo connesso a tutti i precedenti
s = stride if j < 2 else 1
self._ops.append(MixedOp(C, s))
def forward(self, s0: torch.Tensor, s1: torch.Tensor,
weights: torch.Tensor) -> torch.Tensor:
s0 = self.preprocess0(s0)
s1 = self.preprocess1(s1)
states = [s0, s1]
offset = 0
for i in range(self.N_NODES):
s = sum(
self._ops[offset + j](h, weights[offset + j])
for j, h in enumerate(states)
)
offset += len(states)
states.append(s)
# Output = concatenazione degli N_NODES interni
return torch.cat(states[2:], dim=1)
# ============================================================
# DARTS NETWORK con alpha parameters
# ============================================================
class DARTSNetwork(nn.Module):
def __init__(self, C: int = 16, n_classes: int = 10,
n_layers: int = 8, n_nodes: int = 4):
super().__init__()
C_curr = C
self.stem = nn.Sequential(
nn.Conv2d(3, C_curr, 3, padding=1, bias=False),
nn.BatchNorm2d(C_curr)
)
self.cells = nn.ModuleList()
for i in range(n_layers):
reduction = i in [n_layers // 3, 2 * n_layers // 3]
cell = DARTSCell(C_curr, reduction)
if reduction:
C_curr *= 2
self.cells.append(cell)
n_ops = len(PRIMITIVES)
n_edges = n_nodes * (n_nodes + 1) // 2 + 2 * n_nodes
# Alpha: architecture parameters (learnable!)
self._arch_parameters = nn.ParameterList([
nn.Parameter(1e-3 * torch.randn(n_edges, n_ops)) # Normal cell
for _ in range(n_layers)
])
self.global_pool = nn.AdaptiveAvgPool2d(1)
self.classifier = nn.Linear(C_curr * 4, n_classes)
def arch_parameters(self):
return list(self._arch_parameters)
def model_parameters(self):
ids = set(id(p) for p in self.arch_parameters())
return [p for p in self.parameters() if id(p) not in ids]
def forward(self, x: torch.Tensor) -> torch.Tensor:
s0 = s1 = self.stem(x)
for i, cell in enumerate(self.cells):
weights = F.softmax(self._arch_parameters[i], dim=-1)
s0, s1 = s1, cell(s0, s1, weights)
out = self.global_pool(s1)
return self.classifier(out.view(out.size(0), -1))
def genotype(self):
"""Estrae l'architettura discreta (argmax degli alpha)."""
result = []
for alpha in self._arch_parameters:
ops = F.softmax(alpha, dim=-1).argmax(dim=-1)
result.append([PRIMITIVES[op.item()] for op in ops])
return result
# ============================================================
# TRAINING LOOP DARTS: Bi-level optimization
# ============================================================
def train_darts(model: DARTSNetwork, train_loader, val_loader,
n_epochs: int = 50, arch_lr: float = 3e-4, model_lr: float = 3e-3):
"""
DARTS usa bi-level optimization:
- Passo 1: ottimizza pesi modello su train set
- Passo 2: ottimizza alpha (architettura) su val set
"""
device = next(model.parameters()).device
# Due optimizer separati: pesi e architettura
optimizer_model = torch.optim.SGD(
model.model_parameters(), lr=model_lr,
momentum=0.9, weight_decay=3e-4
)
optimizer_arch = torch.optim.Adam(
model.arch_parameters(), lr=arch_lr,
betas=(0.5, 0.999), weight_decay=1e-3
)
criterion = nn.CrossEntropyLoss()
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer_model, T_max=n_epochs
)
val_iter = iter(val_loader)
for epoch in range(n_epochs):
model.train()
total_loss = 0.0
for imgs_train, labels_train in train_loader:
imgs_train = imgs_train.to(device)
labels_train = labels_train.to(device)
# Passo 1: Aggiorna alpha su validation
try:
imgs_val, labels_val = next(val_iter)
except StopIteration:
val_iter = iter(val_loader)
imgs_val, labels_val = next(val_iter)
imgs_val = imgs_val.to(device)
labels_val = labels_val.to(device)
optimizer_arch.zero_grad()
loss_arch = criterion(model(imgs_val), labels_val)
loss_arch.backward()
optimizer_arch.step()
# Passo 2: Aggiorna pesi modello su training
optimizer_model.zero_grad()
loss_model = criterion(model(imgs_train), labels_train)
loss_model.backward()
nn.utils.clip_grad_norm_(model.model_parameters(), 5.0)
optimizer_model.step()
total_loss += loss_model.item()
scheduler.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}/{n_epochs} | Loss: {total_loss/len(train_loader):.4f}")
print(f"Genotype: {model.genotype()[:2]}...")
return model.genotype()
DARTS ve Tek Atışlı NAS: Karşılaştırma
| bekliyorum | DART | Tek Çekim (ENAS, OFA) |
|---|---|---|
| Araştırma maliyeti | 1-4 GPU-gün | Saatler (eğitim sonrası süper ağ) |
| mimari kalite | Çok yüksek | Yüksek (hafif bir yaklaşım) |
| Hedef donanımı | Tek hedef | Çoklu hedef (OFA) |
| GPU belleği | Yüksek (iki seviyeli tercih) | Ortalama |
| Uygulama | Karmaşık | Ilıman |
Optuna ve Gecikme Kısıtlamalarına Sahip Donanım Duyarlı NAS
Teorik NAS maksimum doğruluğu arar. Pratik NAS birini optimize eder çok amaçlı takas doğruluk, gecikme, FLOP'lar ve model boyutu arasında. Optuna, NSGA-II algoritmasıyla çok amaçlı aramayı yerel olarak destekler.
import optuna
from optuna.samplers import NSGAIISampler
import torch
import time
def estimate_latency_ms(model: nn.Module, input_shape=(1, 3, 224, 224),
n_runs: int = 50, device: str = "cpu") -> float:
"""Misura latenza media in millisecondi."""
model = model.to(device).eval()
x = torch.randn(*input_shape).to(device)
# Warmup
with torch.no_grad():
for _ in range(10):
model(x)
t0 = time.perf_counter()
with torch.no_grad():
for _ in range(n_runs):
model(x)
elapsed = (time.perf_counter() - t0) / n_runs * 1000
return elapsed
def count_flops(model: nn.Module, input_shape=(1, 3, 224, 224)) -> int:
"""Stima FLOPs (usa fvcore o ptflops in produzione)."""
# Versione semplificata: conta operazioni nei layer Linear e Conv2d
total_flops = 0
x = torch.randn(*input_shape)
def hook(module, input, output):
nonlocal total_flops
if isinstance(module, nn.Conv2d):
B, C_out, H_out, W_out = output.shape
kernel_ops = module.kernel_size[0] * module.kernel_size[1] * module.in_channels
total_flops += 2 * B * C_out * H_out * W_out * kernel_ops
elif isinstance(module, nn.Linear):
B = input[0].shape[0]
total_flops += 2 * B * module.in_features * module.out_features
hooks = []
for m in model.modules():
if isinstance(m, (nn.Conv2d, nn.Linear)):
hooks.append(m.register_forward_hook(hook))
with torch.no_grad():
model(x)
for h in hooks:
h.remove()
return total_flops
# Multi-obiettivo NAS: massimizza accuracy, minimizza latency
def multi_objective(trial: optuna.Trial):
# Definisci architettura
n_channels = trial.suggest_categorical("channels", [32, 64, 128])
n_layers = trial.suggest_int("layers", 2, 6)
kernel = trial.suggest_categorical("kernel", [3, 5])
use_se = trial.suggest_categorical("use_se", [True, False])
# Costruisci modello
model = FlexibleCNN(
n_conv_layers=n_layers,
channels=[n_channels] * n_layers,
kernel_sizes=[kernel] * n_layers,
use_bn=True,
dropout_rate=0.2,
n_classes=10
)
# Addestra brevemente (versione reale: training completo)
# ...
# Metriche
val_accuracy = 0.80 + 0.05 * (n_channels / 128) # Simulato
latency = estimate_latency_ms(model, input_shape=(1, 3, 32, 32))
flops = count_flops(model, input_shape=(1, 3, 32, 32))
return val_accuracy, -latency # Massimizza accuracy, massimizza -latency
# Studio multi-obiettivo con NSGA-II (algoritmo evolutivo Pareto-ottimale)
study_mo = optuna.create_study(
directions=["maximize", "maximize"],
sampler=NSGAIISampler(seed=42)
)
study_mo.optimize(multi_objective, n_trials=100)
# Pareto front: architetture ottimali sul trade-off accuracy/latency
pareto_trials = study_mo.best_trials
print(f"Architetture Pareto-ottimali: {len(pareto_trials)}")
for t in pareto_trials[:5]:
acc, neg_lat = t.values
print(f" Acc: {acc:.3f}, Latency: {-neg_lat:.1f} ms | {t.params}")
Hepsi Bir Arada: Heterojen Donanım için NAS
Hepsi Bir Arada (OFA) MIT'den temel bir pratik sorunu çözüyor: eğitim Her hedef ve engelleyici cihaz için ayrı bir ağ. OFA tek kişiyi eğitiyor süper ağ binlerce alt mimariyi destekleyen, daha sonra evrimsel bir yöntem kullanan Her cihaz için en uygun alt mimariyi bulmak için hızlı arama.
OFA eğitimi şunları kullanır: aşamalı daralma: Süper ağ eğitilmiştir maksimum konfigürasyondan başlayarak boyutlar giderek küçültülür (önce çekirdek boyutları, sonra derinlik, son olarak genişlik). Bu, bir dizi paylaşılan ağırlık oluşturur tüm konfigürasyonlarda iyi çalışır.
# Uso di OFA tramite la libreria ufficiale
# pip install ofa
from ofa.model_zoo import ofa_net
import torch
import time
# Carica rete OFA pre-addestrata (OFA-MobileNetV3)
ofa_network = ofa_net('ofa_mbv3_d234_e346_k357_w1.0', pretrained=True)
# Ricerca architettura ottimale per un budget specifico
# (esempio: 200M FLOPs per deployment su mobile)
def evaluate_subnet(subnet_config):
"""Valuta una sotto-architettura dell'OFA network."""
ofa_network.set_active_subnet(
ks=subnet_config['ks'], # kernel sizes
e=subnet_config['e'], # expansion ratios
d=subnet_config['d'] # depths
)
subnet = ofa_network.get_active_subnet(preserve_weight=True)
return subnet
# Esempio di configurazione ottimale trovata da evolutionary search:
# per iPhone XS (7.5ms latency target)
iphone_config = {
'ks': [3, 3, 5, 3, 5, 3, 5, 5, 3, 5, 5, 3, 5, 5, 3, 5, 5, 3, 5, 5],
'e': [3, 3, 6, 3, 6, 3, 6, 6, 3, 6, 6, 3, 6, 6, 3, 6, 6, 3, 6, 6],
'd': [2, 3, 3, 3, 3]
}
iphone_subnet = evaluate_subnet(iphone_config)
print(f"Subnet per iPhone XS: {sum(p.numel() for p in iphone_subnet.parameters()):,} params")
# Configurazione per Raspberry Pi 4 (latency target 50ms)
rpi_config = {
'ks': [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
'e': [3, 3, 3, 3, 3, 3, 4, 4, 3, 4, 4, 3, 4, 4, 3, 4, 4, 3, 4, 4],
'd': [2, 2, 2, 2, 2]
}
rpi_subnet = evaluate_subnet(rpi_config)
# Benchmark latenza su CPU (simula RPi4)
rpi_subnet.eval()
x = torch.randn(1, 3, 224, 224)
with torch.no_grad():
for _ in range(10): rpi_subnet(x) # warmup
t0 = time.perf_counter()
for _ in range(50): rpi_subnet(x)
lat_ms = (time.perf_counter() - t0) / 50 * 1000
flops = sum(p.numel() for p in rpi_subnet.parameters()) * 2 / 1e6
print(f"RPi4 subnet: {lat_ms:.1f}ms, {flops:.1f}M FLOPs")
print(f"Parametri: {sum(p.numel() for p in rpi_subnet.parameters()):,}")
AutoKeras ile Uçtan Uca AutoML
NAS'ı sıfırdan uygulamaya vakti olmayanlar için, Otomatik Keralar teklifler Mimari aramayı otomatik olarak gerçekleştiren birinci sınıf bir API, ön işleme ve hiperparametre ayarlama. Dahili olarak algoritmalarla Keras Tuner'ı kullanır Bayesian ve rastgele arama ve dağıtım için TensorFlow ile entegre edilmiştir.
# pip install autokeras tensorflow
# NOTA: AutoKeras richiede TensorFlow 2.x
import autokeras as ak
import numpy as np
import tensorflow as tf
# ============================================================
# IMAGE CLASSIFICATION con AutoKeras
# ============================================================
# Carica dataset
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0
# Crea ricercatore con vincoli di complessità
clf = ak.ImageClassifier(
max_trials=30, # Numero massimo di architetture da provare
overwrite=True,
project_name='nas_cifar10',
seed=42,
# Limiti hardware (opzionali)
# max_model_size=1e6, # Max 1M parametri
)
# Avvia NAS (ricerca + training)
clf.fit(
x_train, y_train,
epochs=20,
validation_split=0.15,
callbacks=[
tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True)
]
)
# Valutazione
loss, acc = clf.evaluate(x_test, y_test)
print(f"Test accuracy: {acc:.4f}")
# Esporta modello Keras migliore
best_model = clf.export_model()
best_model.summary()
# Conta parametri
total_params = best_model.count_params()
print(f"Parametri totali: {total_params:,}")
# Ispezione architettura trovata
print("\nArchitettura trovata da AutoKeras:")
for i, layer in enumerate(best_model.layers):
config = layer.get_config()
params_str = f"params={layer.count_params():,}" if hasattr(layer, 'count_params') else ""
print(f" Layer {i}: {type(layer).__name__} {params_str}")
# Export per deployment
best_model.save('best_nas_model.h5')
# Conversione a TFLite per edge deployment
converter = tf.lite.TFLiteConverter.from_keras_model(best_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT] # INT8 quantization
tflite_model = converter.convert()
with open('best_nas_model_int8.tflite', 'wb') as f:
f.write(tflite_model)
print(f"Modello TFLite: {len(tflite_model)/1024:.1f} KB")
Örnek Olay: Jetson Nano'da Tıbbi Sınıflandırmaya Yönelik NAS
Gerçek hayattan bir örnek, donanıma duyarlı NAS'ın pratik değerini açıklığa kavuşturuyor. Bir projede dermatoskopik görüntülerin sınıflandırılması (8 sınıf cilt lezyonu) NVIDIA Jetson Nanokısıtlamalar şunlardı: görüntü başına 100 ms'nin altında gecikme, doğruluk %88'in üzerinde, model 10 MB'ın altında. Standart mimariler tatmin etmedi tüm kısıtlamalar aynı anda.
import optuna
from optuna.samplers import NSGAIISampler
import torch
import torch.nn as nn
import time
# ============================================================
# CASO STUDIO: NAS per dermoscopia su Jetson Nano
# ============================================================
class DermatologyNASModel(nn.Module):
"""
Modello flessibile per classificazione dermoscopica.
Architettura MobileNet-style ottimizzata via NAS.
"""
def __init__(
self,
n_stages: int, # 3-5 stages di downsampling
channels: list, # Canali per stage
expansion: list, # Expansion ratios MBConv
use_se: list, # SE module per stage
n_classes: int = 8
):
super().__init__()
# Stem
self.stem = nn.Sequential(
nn.Conv2d(3, channels[0], 3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(channels[0]),
nn.ReLU6()
)
# Stages con MBConv
stages = []
in_ch = channels[0]
for i in range(n_stages):
out_ch = channels[i]
exp = expansion[i]
mid_ch = in_ch * exp
stage = nn.Sequential(
# Pointwise expansion
nn.Conv2d(in_ch, mid_ch, 1, bias=False),
nn.BatchNorm2d(mid_ch),
nn.ReLU6(),
# Depthwise
nn.Conv2d(mid_ch, mid_ch, 3, stride=2 if i < n_stages-1 else 1,
padding=1, groups=mid_ch, bias=False),
nn.BatchNorm2d(mid_ch),
nn.ReLU6(),
# SE module opzionale
SEModule(mid_ch, reduction=4) if use_se[i] else nn.Identity(),
# Pointwise projection
nn.Conv2d(mid_ch, out_ch, 1, bias=False),
nn.BatchNorm2d(out_ch)
)
stages.append(stage)
in_ch = out_ch
self.stages = nn.Sequential(*stages)
self.pool = nn.AdaptiveAvgPool2d(1)
self.classifier = nn.Linear(channels[-1], n_classes)
def forward(self, x):
x = self.stem(x)
x = self.stages(x)
x = self.pool(x).flatten(1)
return self.classifier(x)
class SEModule(nn.Module):
"""Squeeze-and-Excitation per channel attention."""
def __init__(self, channels, reduction=4):
super().__init__()
self.se = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(channels, channels // reduction),
nn.ReLU(),
nn.Linear(channels // reduction, channels),
nn.Sigmoid()
)
def forward(self, x):
scale = self.se(x).view(x.size(0), -1, 1, 1)
return x * scale
def jetson_nas_objective(trial: optuna.Trial):
"""Funzione obiettivo hardware-aware per Jetson Nano."""
# Spazio di ricerca compatto
n_stages = trial.suggest_int("n_stages", 3, 5)
channels = [
trial.suggest_categorical(f"ch_{i}", [16, 24, 32, 48, 64])
for i in range(n_stages)
]
expansions = [
trial.suggest_categorical(f"exp_{i}", [2, 4, 6])
for i in range(n_stages)
]
use_se = [
trial.suggest_categorical(f"se_{i}", [True, False])
for i in range(n_stages)
]
model = DermatologyNASModel(n_stages, channels, expansions, use_se, n_classes=8)
# Stima parametri
n_params = sum(p.numel() for p in model.parameters())
model_size_mb = n_params * 4 / (1024 ** 2)
# Stima latenza su CPU (proxy per Jetson Nano ARM)
x = torch.randn(1, 3, 224, 224)
model.eval()
with torch.no_grad():
for _ in range(5): model(x) # warmup
t0 = time.perf_counter()
for _ in range(20): model(x)
latency_ms = (time.perf_counter() - t0) / 20 * 1000
# Jetson Nano e ~3x più lento di CPU Intel i7
# Aggiungiamo un fattore di correzione
jetson_latency_ms = latency_ms * 3.5
# Vincoli hardware
if jetson_latency_ms > 150: # Hard constraint: max 150ms
raise optuna.exceptions.TrialPruned()
if model_size_mb > 15: # Max 15MB
raise optuna.exceptions.TrialPruned()
# Accuracy simulata (in pratica: training completo)
val_accuracy = 0.85 + 0.05 * (sum(channels) / (64 * n_stages))
val_accuracy = min(val_accuracy, 0.94) # Cap realistico
return val_accuracy, -jetson_latency_ms
# Ricerca multi-obiettivo
study = optuna.create_study(
directions=["maximize", "maximize"],
sampler=NSGAIISampler(seed=42),
pruner=optuna.pruners.MedianPruner()
)
study.optimize(jetson_nas_objective, n_trials=80, timeout=7200)
# Seleziona architettura ottimale dal Pareto front
# Criteri: accuracy > 88%, latenza Jetson < 100ms
best = [
t for t in study.best_trials
if t.values[0] > 0.88 and -t.values[1] < 100
]
best.sort(key=lambda t: t.values[0], reverse=True)
if best:
print(f"\nMiglior architettura per Jetson Nano:")
print(f" Accuracy: {best[0].values[0]:.3f}")
print(f" Latenza stimata: {-best[0].values[1]:.1f} ms")
print(f" Configurazione: {best[0].params}")
else:
print("Nessuna architettura soddisfa tutti i vincoli. Allarga lo spazio di ricerca.")
NAS'ın Sınırlamaları ve Tuzakları
- Arama alanı fazlalığı: bulunan mimariler iyi performans gösteriyor araştırma için seçilen ölçütlere göre belirlenir ancak farklı veri kümelerine zayıf bir şekilde genellenebilir. Her zaman arama sırasında kullanılmayan bağımsız bir uzatma setini değerlendirin.
- Gizli hesaplama maliyeti: DARTS 1-4 GPU günü gerektirir ancak eğitim gerektirir Bulunan mimariyle tamamlandığında daha fazla GPU saati eklenir. Toplam maliyet ve sıklıkla İyi bir manuel mimari eğitiminin 2-3 katı.
- DARTS kararsızlığı: Orijinal DARTS'ta eğitim istikrarsızlığı yaşanıyor ve bağlantıların atlanmasına doğru çökme eğilimi gösterir. Daha istikrarlı sonuçlar için DARTS+ veya R-DARTS'ı kullanın. Alfa ağırlıklarının entropisini izleyerek çöküşü kontrol edin.
- Veri kümeleri arasında aktarım: CIFAR-10'da optimal bir mimari ImageNet veya tıbbi veri kümelerinde mutlaka optimaldir. Son veri seti üzerinde araştırma yapın.
- Güvenilmez proxy görevleri: daha basit bir proxy görevi kullanın (örn. CIFAR ImageNet yerine) maliyeti düşürmek yanlış sıralamalara yol açabilir. Her zaman geçerli gerçek görevde bulunan mimari.
Mimari Karşılaştırması: NAS ve Standart Karşılaştırma Kılavuzları
| Mimarlık | Yöntem | ImageNet İlk-1 | Parametreler | FLOP'lar | Arama Maliyeti |
|---|---|---|---|---|---|
| ResNet-50 | Manuel | %76,1 | 25,6 milyon | 4.1G | Yok |
| MobileNetV3-Büyük | NAS + Manuel | %75,2 | 5,4 milyon | 0.22G | ~1000 GPU-saat |
| EfficientNet-B0 | NAS (MnasNet) | %77,1 | 5,3 milyon | 0,39G | ~6000 GPU-saat |
| NASNet-A Mobil | RL-NAS | %74,0 | 5,3 milyon | 0.56G | 400 GPU-gün |
| DARTLAR (2. sıra) | DART | %73,3 | 4,7 milyon | 0.6G | 4 GPU günü |
| OFA-595M (RPi) | OFA Tek Atış | %76,0 | ~4,5 milyon | 0.6G | OFA sonrası <1 GPU-saat |
Üretimde NAS için En İyi Uygulamalar
NAS ne zaman kullanılmalı ve nasıl doğru şekilde yapılmalı?
- NAS'tan önce ince ayarı kullanın: genellikle önceden eğitilmiş bir ViT-B veya EfficientNet-B4 Sıfırdan bulunan bir NAS mimarisini geride bırakıyor. Görev çok spesifik olduğunda NAS'ı kullanın (sabit hedef donanım, etki alanı ImageNet'ten çok farklı, katı donanım kısıtlamaları).
- Hiperparametre için Bayesian'ı tercih edin: arama mimarisi olmasa bile, LR için Optuna TPE, toplu boyut, büyütme ve optimize edici ve genellikle GridSearch'ten daha etkilidir ve 3-5 kat daha az deneme gerektirir. Ve tam NAS öncesi atılacak ilk adım.
- Başlangıçtan itibaren donanım bilincinde: gecikmeyi/FLOP'ları işleve dahil edin İlk denemeden itibaren hedef. %1 daha doğru ancak 2 kat daha yavaş olan bir model kullanışlı değildir uç cihazlarda gerçek zamanlı dağıtım için.
- Agresif erken durdurma: Optuna'nın MedianPruner'ını kullanın. %30-40'ı ortadan kaldırın erken dönemlerde ümit vermeyen denemelerin yapılması, toplam maliyetin 2-3 kat azaltılması.
- Birden fazla GPU arasında paralelleştirme: Optuna yerel paralelleştirmeyi destekliyor paylaşılan veritabanı (SQLite veya PostgreSQL) aracılığıyla. 4 GPU süreyi 3,5 kat azaltır kod değişikliği olmadan.
- Mimari kontrol noktalarını kaydedin: arama yaptıktan sonra yalnızca kaydetmeyin ağırlıkların yanı sıra mimari özellikleri (DARTS'ta genotip, Optuna'da config dict). Bu, aramayı yeniden yapmadan modeli yeniden oluşturmanıza olanak tanır.
Sonuçlar
Sinir Mimarisi Araması 2017'den günümüze önemli ölçüde olgunlaştı. Algoritmalardan Sahada tek bir GPU üzerinde saatlerce çalışan pratik araçlar için 400 GPU günü gerekiyordu otomatik mimari tasarımını uygulayıcılar için erişilebilir hale getirdi. 2026'da iş akışı en etkili kombinasyonlar: iyi tanımlanmış arama alanı, agresif budama özelliğine sahip Optuna Optimize edilmiş dağıtım için hiperparametre ve donanıma duyarlı hedefler.
Çoğu proje için önceden var olan mimarilerin kullanımı (ViT, Swin, EfficientNet) ince ayar ile NAS'ın sıfırdan daha verimli olmasını sağlar. Ancak görevin donanım gereksinimleri olduğunda çok spesifik — Raspberry Pi'de 5 ms'nin altındaki gecikme, mikro denetleyici için 1 MB'ın altındaki model, özel tıbbi sınıflandırma — donanıma duyarlı NAS vazgeçilmez bir araç haline gelir.
Uç bilişime yönelik eğilim NAS'ın değerini daha da artırıyor: Gartner ile SLM'lerin 2027 yılına kadar bulut LLM'lerinden 3 kat daha iyi performans göstermesini bekliyor, mimarileri optimize ediyor Belirli bir donanım artık akademik bir lüks değil, pratik bir zorunluluktur.
Sonraki Adımlar
- Sonraki makale: Bilginin Damıtılması: Karmaşık Modellerin Sıkıştırılması
- İlgili: Vision Transformer: Mimari ve Uygulamalar
- İlgili: Uç Cihazlarda Yüksek Lisans: Raspberry Pi ve Jetson
- MLOps Serisi: MLflow ve Optuna ile Deneme Takibi
- Yapay Zeka Mühendisliği Serisi: Üretim İçin Model Optimizasyonu







