Как решать задачу регрессии с помощью нейронных сетей?

«Как решать задачу регрессии с помощью нейронных сетей?» — вопрос из категории Нейронные сети и Deep Learning, который задают на 26% собеседований Data Scientist / ML Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

При построении нейросетевой модели для регрессии я фокусируюсь на архитектуре, функции потерь и методах стабилизации обучения.

Мой типичный рабочий процесс:

  1. Подготовка данных:

    • Стандартизую или нормализую числовые признаки (использую StandardScaler из sklearn).
    • Разделяю данные на train/validation/test.
  2. Проектирование архитектуры:

    • Для табличных данных начинаю с полносвязной сети (MLP).
    • В качестве активаций в скрытых слоях использую ReLU или его вариации (Leaky ReLU).
    • Выходной слой — один нейрон без функции активации для предсказания непрерывного значения.
    • Добавляю Batch Normalization и Dropout слои для ускорения сходимости и борьбы с переобучением.
  3. Выбор функции потерь и оптимизатора:

    • Функция потерь: Чаще всего Mean Squared Error (MSE). Если в данных есть выбросы, использую Mean Absolute Error (MAE) или Huber Loss, которая менее чувствительна к ним.
    • Оптимизатор: Adam с learning rate по умолчанию или подобранным с помощью ReduceLROnPlateau.
  4. Обучение с регуляризацией:

    • Использую раннюю остановку (EarlyStopping) по валидационной потере.
    • Добавляю L2-регуляризацию (weight decay) в оптимизатор.

Пример реализации на PyTorch:

import torch
import torch.nn as nn
import torch.optim as optim

class RegressionNN(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.BatchNorm1d(128),
            nn.ReLU(),
            nn.Dropout(0.2),

            nn.Linear(128, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Dropout(0.1),

            nn.Linear(64, 1)  # Выходной слой без активации
        )

    def forward(self, x):
        return self.net(x)

# Инициализация
model = RegressionNN(input_dim=X_train.shape[1])
criterion = nn.MSELoss()  # Функция потерь MSE
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)  # L2-регуляризация

# Цикл обучения с ранней остановкой
best_val_loss = float('inf')
patience = 10
for epoch in range(100):
    model.train()
    optimizer.zero_grad()
    predictions = model(X_train_tensor)
    loss = criterion(predictions, y_train_tensor)
    loss.backward()
    optimizer.step()

    # Валидация
    model.eval()
    with torch.no_grad():
        val_pred = model(X_val_tensor)
        val_loss = criterion(val_pred, y_val_tensor)

    # Логика ранней остановки
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        patience_counter = 0
    else:
        patience_counter += 1
        if patience_counter >= patience:
            print(f'Early stopping at epoch {epoch}')
            break

Оценка модели: Для итоговой оценки на тестовом наборе использую не только MSE, но и MAE (среднюю абсолютную ошибку) и R² score, чтобы понять абсолютную ошибку и долю объяснённой дисперсии.