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

Ответ

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

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

  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, чтобы понять абсолютную ошибку и долю объяснённой дисперсии.

Ответ 18+ 🔞

Давай разберём, как из говна и палок собрать нейросеть для регрессии, чтобы она не просто тупо запомнила данные, а реально что-то предсказывала. Терпения ноль ебать, когда она учится, но результат того стоит.

Как я обычно это делаю, чтобы не накрыться медным тазом:

  1. Готовим данные, а то будет пиздец.

    • Все эти цифры в признаках нужно привести к общему знаменателю, иначе один признак с огромными значениями будет всех доминировать. Использую StandardScaler — он делает так, чтобы среднее было ноль, а разброс — единица. Проще говоря, приводит всё к нормальному виду.
    • Дальше делю данные на три кучки: для обучения, для проверки в процессе (валидация) и финальный тест, куда смотреть нельзя, пока всё не готово.
  2. Собираем архитектуру, как лего.

    • Для табличных данных — обычная полносвязная сетка (MLP), куда ж без неё.
    • Внутри ставим ReLU (или его более хитрожопого брата Leaky ReLU), чтобы сеть могла учиться нелинейным штукам.
    • Самое важное: выходной слой — это один нейрон БЕЗ ВСЯКОЙ АКТИВАЦИИ. Совсем. Никакого sigmoid или tanh. Мы же число предсказываем, а не вероятность. Если поставить активацию — всё, предсказания будут зажаты в её диапазон, и модель станет пиздапроебильной.
    • Чтобы не переобучалась и училась быстрее, впихиваем Batch Normalization и Dropout. Первый выравнивает внутренние распределения, второй — случайно отключает нейроны, чтобы сеть не стала слишком зависимой от каких-то конкретных связей.
  3. Выбираем, как наказывать за ошибки, и кто будет исправлять.

    • Функция потерь: Чаще всего Mean Squared Error (MSE). Она квадратом штрафует за большие ошибки, поэтому модель их очень боится. Но если в данных есть дикие выбросы (какой-нибудь один ебанутый заказ в сто раз больше всех), то MSE с ума сойдёт, пытаясь под него подстроиться. Тогда беру Mean Absolute Error (MAE) или Huber Loss — они поспокойнее.
    • Оптимизатор: Беру Adam. Он как умный универсальный солдат, сам подстраивает шаги обучения. Иногда уменьшаю learning rate, если модель перестаёт улучшаться.
  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 — он показывает, какую долю дисперсии моя модель объяснила. Если R² близок к 1 — я молодец, если к 0 — пора пересматривать подход, потому что нихуя не вышло.