Что такое линейная регрессия

Линейная регрессия — один из старейших и наиболее фундаментальных алгоритмов в машинном обучении. Несмотря на простоту, именно с её изучения начинается понимание оптимизации, функций потерь и обобщения модели. Все сложные алгоритмы — нейронные сети, метод опорных векторов, бустинг — имеют те же базовые принципы.

Задача: по матрице признаков X (n объектов × d признаков) предсказать вещественный вектор целевых значений y. Модель строит линейное отображение:

ŷ = X · w + b

где:
  X — матрица признаков  (n × d)
  w — вектор весов       (d × 1)
  b — свободный член (bias)
  ŷ — вектор предсказаний

Для удобства свободный член включают в вектор весов, добавляя к матрице X столбец из единиц:

ŷ = X̃ · w̃    где X̃ = [1 | X],  w̃ = [b; w]

Функция потерь: среднеквадратичная ошибка

Качество модели измеряется через среднеквадратичную ошибку (MSE) на обучающей выборке:

L(w) = (1/n) · ‖Xw - y‖²
     = (1/n) · Σᵢ (ŷᵢ - yᵢ)²

MSE выбрана не произвольно — она соответствует максимальному правдоподобию при предположении, что остатки (ошибки) распределены нормально с нулевым средним. Это даёт глубокий статистический смысл: линейная регрессия с MSE является оптимальной оценкой в классе несмещённых линейных оценщиков (теорема Гаусса–Маркова).

Метод наименьших квадратов: аналитическое решение

MSE — квадратичная функция весов, выпуклая. Её минимум достигается в единственной точке, которую можно найти аналитически, приравняв градиент к нулю:

∇L(w) = (2/n) · Xᵀ(Xw - y) = 0

Xᵀ Xw = Xᵀ y

w* = (XᵀX)⁻¹ Xᵀ y   ← нормальное уравнение
Условие существования решения: матрица XᵀX должна быть обратимой. Это выполняется, когда признаки линейно независимы (нет мультиколлинеарности) и n > d. На практике, если признаки коррелируют, используют L2-регуляризацию: w* = (XᵀX + λI)⁻¹ Xᵀy — это Ridge-регрессия.

Реализация в Python:

import numpy as np

class OLSRegression:
    def fit(self, X, y):
        # Добавляем столбец единиц (bias)
        X_b = np.c_[np.ones(len(X)), X]
        # Нормальное уравнение
        self.w_ = np.linalg.pinv(X_b.T @ X_b) @ X_b.T @ y
        return self

    def predict(self, X):
        X_b = np.c_[np.ones(len(X)), X]
        return X_b @ self.w_

# Пример
X_train = np.array([[1.2], [2.4], [3.1], [4.8], [5.5]])
y_train = np.array([2.3, 4.1, 5.2, 8.4, 9.7])

model = OLSRegression()
model.fit(X_train, y_train)
print("Веса:", model.w_)        # [0.52, 1.74] примерно
print("Предсказание для x=3.0:", model.predict([[3.0]]))

Сложность МНК и когда он неприменим

Инверсия матрицы XᵀX имеет сложность O(d³). При d = 1000 признаков это уже ~10⁹ операций. При d = 100 000 (например, NLP-задачи) — вычислительно неприемлемо. В таких случаях применяют итерационные методы: градиентный спуск или метод сопряжённых градиентов.

НОРМАЛЬНОЕ УРАВНЕНИЕ
  • ✓ Точное решение за 1 шаг
  • ✓ Нет гиперпараметров
  • ✗ O(d³) по времени
  • ✗ O(d²) по памяти
  • ✗ Плохо при d > 10 000
ГРАДИЕНТНЫЙ СПУСК
  • ✓ Работает при любом d
  • ✓ Масштабируется на GPU
  • ✗ Нужно подобрать lr
  • ✗ Много итераций
  • ✗ Требует нормализации

Градиентный спуск для линейной регрессии

Градиент MSE по весам:

∇L(w) = (2/n) · Xᵀ (Xw - y)

Шаг обновления весов на итерации t:

w(t+1) = w(t) - η · ∇L(w(t))
       = w(t) - (2η/n) · Xᵀ (Xw(t) - y)

Здесь η — шаг обучения (learning rate). Выбор η критически важен: слишком большой приведёт к расходимости, слишком маленький — к медленной сходимости.

class GDLinearRegression:
    def __init__(self, lr=0.01, n_iter=1000):
        self.lr = lr
        self.n_iter = n_iter

    def fit(self, X, y):
        n, d = X.shape
        X_b = np.c_[np.ones(n), X]
        self.w_ = np.zeros(d + 1)
        self.loss_history_ = []

        for i in range(self.n_iter):
            y_pred = X_b @ self.w_
            residuals = y_pred - y
            grad = (2 / n) * X_b.T @ residuals
            self.w_ -= self.lr * grad
            mse = np.mean(residuals ** 2)
            self.loss_history_.append(mse)

        return self

    def predict(self, X):
        X_b = np.c_[np.ones(len(X)), X]
        return X_b @ self.w_

Важность нормализации признаков

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

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train)

# Всегда используйте те же параметры нормализации для тестовой выборки
X_test_scaled = scaler.transform(X_test)
Правило: нормализуйте признаки перед градиентным спуском. При МНК нормализация не обязательна для качества модели, но облегчает интерпретацию коэффициентов.

Интерпретация коэффициентов

Каждый вес wⱼ показывает: при увеличении j-го признака на 1 единицу (при фиксированных остальных) предсказание изменяется на wⱼ единиц. Это делает линейную регрессию легко интерпретируемой.

Важно: интерпретация корректна только при выполнении предположений модели. Прежде всего — отсутствии мультиколлинеарности. Если признаки сильно коррелируют, коэффициенты становятся нестабильными и их интерпретация теряет смысл.

Диагностика модели: анализ остатков

Чтобы убедиться, что линейная регрессия подходит для задачи, анализируют остатки εᵢ = yᵢ - ŷᵢ:

  • График остатков vs предсказания — должен выглядеть как случайный «белый шум» без структуры. Паттерн (U-образный, расширяющийся) указывает на нелинейность или гетероскедастичность.
  • Q-Q график — проверяет нормальность остатков. Отклонение от диагонали говорит о выбросах или тяжёлых хвостах.
  • Гистограмма остатков — должна быть близка к нормальной.
import matplotlib.pyplot as plt

y_pred = model.predict(X_test)
residuals = y_test - y_pred

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].scatter(y_pred, residuals, alpha=0.5)
axes[0].axhline(0, color='red', linestyle='--')
axes[0].set_xlabel("Предсказание")
axes[0].set_ylabel("Остаток")
axes[0].set_title("Остатки vs предсказания")

axes[1].hist(residuals, bins=30, edgecolor='black')
axes[1].set_title("Распределение остатков")
plt.tight_layout()
plt.show()

Метрики качества регрессии

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import numpy as np

mse   = mean_squared_error(y_test, y_pred)
rmse  = np.sqrt(mse)
mae   = mean_absolute_error(y_test, y_pred)
r2    = r2_score(y_test, y_pred)

print(f"MSE:  {mse:.4f}")
print(f"RMSE: {rmse:.4f}")   # В тех же единицах, что y
print(f"MAE:  {mae:.4f}")    # Устойчива к выбросам
print(f"R²:   {r2:.4f}")     # 1.0 = идеально, 0 = как среднее

R² (коэффициент детерминации) показывает долю дисперсии целевой переменной, объяснённой моделью. R² = 0.87 означает, что модель объясняет 87% вариации y.

Когда линейная регрессия работает хорошо

Линейная регрессия эффективна при соблюдении условий:

  • Между признаками и целевой переменной действительно линейная зависимость
  • Остатки случайны, независимы и примерно нормально распределены
  • Нет мультиколлинеарности между признаками
  • Дисперсия остатков постоянна (гомоскедастичность)

Если зависимость нелинейна — рассмотрите полиномиальные признаки или перейдите к деревьям решений. Если признаков слишком много и многие из них нерелевантны — используйте Lasso-регрессию.

Итог урока

Линейная регрессия — это не просто «старый алгоритм». Это фундамент для понимания оптимизации (нормальное уравнение и градиентный спуск), функций потерь (MSE), регуляризации (Ridge, Lasso) и статистической интерпретации моделей. Понимание её математики — необходимое условие для осмысленной работы с любым ML-алгоритмом.