Полиномиальная регрессия
Узнайте, как работает полиномиальная регрессия, когда её применять и как построить, оценить и настроить модель в Python с помощью scikit-learn.
Линейная регрессия проводит прямую линию через данные. Когда зависимость между входными и выходными значениями криволинейная — например, траектория мяча, рост бактериальной колонии или кривая доза-реакция — прямая линия плохо аппроксимирует данные, сколько бы признаков вы ни добавляли. Полиномиальная регрессия решает эту проблему, добавляя степенные версии существующих признаков (x², x³, …), что позволяет модели изгибаться в соответствии с кривизной данных, при этом по-прежнему используя метод наименьших квадратов под капотом.
На этой странице рассматривается:
- Как полиномиальная регрессия математически расширяет линейную регрессию
- Когда использовать полиномиальную регрессию и какую степень выбирать
- Полный конвейер scikit-learn: преобразование признаков, обучение, оценка
- Ловушка переобучения и способы её обнаружения с помощью кривых обучения/теста
- Сравнение качества моделей с помощью RMSE и R²
Как работает полиномиальная регрессия
Уравнение
Линейная регрессия строит модель:
y = β₀ + β₁xПолиномиальная регрессия степени n строит:
y = β₀ + β₁x + β₂x² + β₃x³ + … + βₙxⁿКлючевая идея состоит в том, что x², x³ и так далее рассматриваются как дополнительные признаки. Модель по-прежнему линейна по коэффициентам (значениям β); нелинейными являются только признаки. Это означает, что метод наименьших квадратов по-прежнему работает — нужно лишь предварительно обработать входную матрицу.
Степени и их смысл
| Степень | Название | Форма |
|---|---|---|
| 1 | Линейная | Прямая линия |
| 2 | Квадратичная | Одна кривая (парабола) |
| 3 | Кубическая | Одна точка перегиба |
| 4+ | Высшего порядка | Более сложные кривые |
Выбор правильной степени — ключевой навык. Слишком низкая степень ведёт к недообучению (модель не улавливает реальную кривизну). Слишком высокая — к переобучению (модель запоминает шум и плохо работает на новых данных).
Когда использовать полиномиальную регрессию
Используйте полиномиальную регрессию, когда:
- На точечной диаграмме видна чёткая кривая в данных, которую прямая линия не может уловить
- Вам нужна быстрая и интерпретируемая альтернатива древовидным моделям при умеренной кривизне
- У вас уже есть работающая базовая линейная регрессия, но она недообучается
Предпочтите другие подходы, когда:
- Зависимость очень сложная или имеет много признаков → попробуйте деревья решений или градиентный бустинг
- Нужно предсказывать далеко за пределами диапазона обучения (экстраполяция) → полиномы высокой степени резко расходятся за пределами диапазона данных
- У вас много входных признаков → каждый признак получает
nполиномиальных членов, поэтому матрица признаков быстро растёт
Построение модели полиномиальной регрессии
Шаг 1: Импорт библиотек
import numpy as np
import matplotlib
matplotlib.use('Agg') # non-interactive backend for scripts
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_scoreТрансформер PolynomialFeatures из scikit-learn автоматически выполняет расширение x → [1, x, x², …, xⁿ]. Объединение его с LinearRegression в Pipeline делает код чистым и предотвращает утечку данных при кросс-валидации.
Шаг 2: Создание тестовых данных
rng = np.random.default_rng(42)
X = rng.uniform(-3, 3, 80).reshape(-1, 1) # 80 points from -3 to 3
y = 0.5 * X.ravel()**2 - X.ravel() + 2 + rng.normal(0, 0.5, 80)
# True relationship: y ≈ 0.5x² - x + 2 (a parabola with noise)Базовая зависимость является квадратичной, поэтому полином степени 2 должен хорошо её восстановить. Именно в таких ситуациях линейная регрессия систематически недообучается.
Шаг 3: Разделение на обучающую и тестовую выборки
Всегда разделяйте данные перед обучением, чтобы иметь отложенные данные для оценки. Подробное объяснение см. в разделе Train/Test Split.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print(f"Training samples: {len(X_train)}") # 64
print(f"Test samples: {len(X_test)}") # 16Шаг 4: Построение и обучение конвейера
degree = 2
model = make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
LinearRegression()
)
model.fit(X_train, y_train)make_pipeline соединяет два шага в цепочку: PolynomialFeatures преобразует входные данные, затем LinearRegression обучается на расширенных признаках. Вы вызываете .fit(), .predict() и .score() на конвейере точно так же, как на обычном оценщике.
Шаг 5: Оценка модели
y_pred_train = model.predict(X_train)
y_pred_test = model.predict(X_test)
rmse_train = np.sqrt(mean_squared_error(y_train, y_pred_train))
rmse_test = np.sqrt(mean_squared_error(y_test, y_pred_test))
r2_train = r2_score(y_train, y_pred_train)
r2_test = r2_score(y_test, y_pred_test)
print(f"Train RMSE: {rmse_train:.4f} Test RMSE: {rmse_test:.4f}")
print(f"Train R²: {r2_train:.4f} Test R²: {r2_test:.4f}")Ожидаемый вывод:
Train RMSE: 0.4605 Test RMSE: 0.4538
Train R²: 0.9480 Test R²: 0.9514Значение R² около 0,95 на отложенных данных означает, что модель объясняет около 95% дисперсии — отличный результат для таких зашумлённых данных. Близкие значения на обучающей и тестовой выборках говорят о том, что модель обобщает данные, а не переобучается.
Интерпретация метрик:
- RMSE (корень из среднеквадратичной ошибки) — средняя ошибка в тех же единицах, что и
y. Чем меньше, тем лучше. - R² (коэффициент детерминации) — доля объяснённой дисперсии. Значения ближе к 1,0 лучше; 0 означает, что модель не лучше предсказания среднего значения.
Шаг 6: Визуализация подгонки
X_line = np.linspace(-3.5, 3.5, 200).reshape(-1, 1)
y_line = model.predict(X_line)
plt.figure(figsize=(7, 5))
plt.scatter(X_train, y_train, alpha=0.6, label='Train', color='steelblue', s=25)
plt.scatter(X_test, y_test, alpha=0.8, label='Test', color='orange', s=40, zorder=5)
plt.plot(X_line, y_line, color='red', linewidth=2, label=f'Degree-{degree} fit')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Polynomial Regression (degree 2)')
plt.legend()
plt.tight_layout()
plt.savefig('poly_reg_fit.png', dpi=120)
print("Plot saved.")Красная кривая должна плавно проходить через точки рассеяния — достаточно близко, чтобы уловить параболическую форму, но не змеиться через отдельные точки шума.
Ловушка переобучения
Главный риск полиномиальной регрессии — выбор слишком высокой степени. Полином степени 15 может идеально запомнить все 80 обучающих точек (RMSE ≈ 0), но будет дико осциллировать между ними и провалится на новых данных.
Стандартный диагностический инструмент — кривая валидации: отображение ошибок обучения и теста в зависимости от степени.
degrees = range(1, 12)
train_rmses, test_rmses = [], []
for d in degrees:
m = make_pipeline(PolynomialFeatures(d, include_bias=False), LinearRegression())
m.fit(X_train, y_train)
train_rmses.append(np.sqrt(mean_squared_error(y_train, m.predict(X_train))))
test_rmses.append(np.sqrt(mean_squared_error(y_test, m.predict(X_test))))
plt.figure(figsize=(7, 4))
plt.plot(degrees, train_rmses, 'o-', label='Train RMSE', color='steelblue')
plt.plot(degrees, test_rmses, 's-', label='Test RMSE', color='orange')
plt.xlabel('Polynomial Degree')
plt.ylabel('RMSE')
plt.title('Validation Curve: Choosing the Best Degree')
plt.legend()
plt.tight_layout()
plt.savefig('poly_reg_validation_curve.png', dpi=120)
print("Plot saved.")Что вы увидите:
- Степень 1 (линейная): и обучающий, и тестовый RMSE высокие — недообучение.
- Степень 2: оба резко падают — модель улавливает истинную форму.
- Степень 5+: обучающий RMSE продолжает падать, но тестовый RMSE растёт — разрыв между обучающими и тестовыми значениями сигнализирует о переобучении.
Лучшая степень — та, при которой тестовый RMSE минимален до того, как начнёт снова расти. В этом примере это степень 2, что соответствует истинному процессу генерации данных.
Для более надёжного выбора степени используйте кросс-валидацию вместо единственного разделения на обучающую и тестовую выборки.
Использование numpy.polyfit (быстрая альтернатива)
Для простых одномерных задач функция polyfit из NumPy предлагает подгонку в одну строку без scikit-learn. Она удобна для исследовательского анализа, но не интегрируется с конвейерами и кросс-валидацией.
import numpy as np
x = np.array([1, 2, 3, 4, 5], dtype=float)
y = np.array([2.1, 4.0, 9.2, 16.1, 25.0])
# Fit a degree-2 polynomial
# Returns coefficients from highest to lowest degree: [a2, a1, a0]
coefficients = np.polyfit(x, y, 2)
poly = np.poly1d(coefficients)
print("Coefficients (highest to lowest degree):", np.round(coefficients, 3))
print("Prediction at x=6:", round(poly(6), 2))Ожидаемый вывод:
Coefficients (highest to lowest degree): [ 1.121 -0.939 1.76 ]
Prediction at x=6: 36.5Подогнанная кривая приближённо равна y ≈ 1.12x² - 0.94x + 1.76, что близко к истинной зависимости y = x² в этих данных. Коэффициенты не равны в точности [1, 0, 0], поскольку для подгонки имеется лишь пять зашумлённых точек.
np.poly1d оборачивает коэффициенты так, что вы можете вызывать полином как функцию: poly(6) вычисляет 1.121(36) - 0.939(6) + 1.76 ≈ 36.5.
Полный конвейер (все шаги вместе)
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
# 1. Generate data (true relationship: y = 0.5x² - x + 2 + noise)
rng = np.random.default_rng(42)
X = rng.uniform(-3, 3, 80).reshape(-1, 1)
y = 0.5 * X.ravel()**2 - X.ravel() + 2 + rng.normal(0, 0.5, 80)
# 2. Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 3. Build and fit
model = make_pipeline(PolynomialFeatures(degree=2, include_bias=False), LinearRegression())
model.fit(X_train, y_train)
# 4. Evaluate
rmse = np.sqrt(mean_squared_error(y_test, model.predict(X_test)))
r2 = r2_score(y_test, model.predict(X_test))
print(f"Test RMSE: {rmse:.4f}")
print(f"Test R²: {r2:.4f}")
# 5. Inspect learned coefficients
lr = model.named_steps['linearregression']
pf = model.named_steps['polynomialfeatures']
feature_names = pf.get_feature_names_out(['x'])
for name, coef in zip(feature_names, lr.coef_):
print(f" {name}: {coef:.4f}")
print(f" intercept: {lr.intercept_:.4f}")Ожидаемый вывод:
Test RMSE: 0.4538
Test R²: 0.9514
x: -0.9625
x^2: 0.4909
intercept: 1.9654Восстановленные коэффициенты (x^2 ≈ 0.49, x ≈ -0.96, перехват ≈ 1.97) близко совпадают с истинными значениями (0.5, -1, 2), что подтверждает: модель научилась правильной форме.
Масштабирование признаков и полиномиальная регрессия
При работе с полиномами высокой степени значения признаков быстро возрастают — x = 100 даёт x² = 10 000 и x³ = 1 000 000. Это может сделать задачу наименьших квадратов численно нестабильной.
Стандартное решение — масштабировать признаки с помощью StandardScaler внутри конвейера, до полиномиального расширения:
from sklearn.preprocessing import StandardScaler
model = make_pipeline(
StandardScaler(),
PolynomialFeatures(degree=3, include_bias=False),
LinearRegression()
)Размещение StandardScaler первым означает, что он обучается только на обучающих данных и последовательно применяется к тестовым — без утечки данных. Подробнее о важности масштабирования см. в разделе Feature Scaling.
Распространённые ошибки
Пропуск разделения на обучающую и тестовую выборки. Если вы обучаете модель и оцениваете её на одних и тех же данных, полиномы высокой степени будут казаться идеальными, полностью проваливаясь на новых входных данных. Всегда оценивайте на отложенных данных.
Экстраполяция за пределы диапазона обучения. Полиномиальные кривые осциллируют и расходятся за пределами диапазона обучающих данных. Модель, обученная на x ∈ [0, 5], может давать абсурдные предсказания при x = 10. Линейная регрессия экстраполирует более консервативно.
Отсутствие масштабирования перед членами высокой степени. Большие значения признаков в сочетании с полиномиальным расширением высокой степени могут вызвать численное переполнение или плохо обусловленные матрицы. Используйте StandardScaler в конвейере.
Выбор степени только по ошибке на обучающей выборке. Обучающий RMSE всегда уменьшается при увеличении степени. Используйте тестовый RMSE или кросс-валидацию для нахождения степени, которая хорошо обобщается.
Забывать про include_bias=False. PolynomialFeatures по умолчанию добавляет константный столбец (член перехвата). LinearRegression тоже добавляет свой перехват. Передача include_bias=False в PolynomialFeatures позволяет избежать избыточного столбца.
Дальнейшие шаги
- Линейная регрессия — основа прямой линии, на которой строится полиномиальная регрессия
- Множественная регрессия — объединение множества признаков (полиномиальная регрессия, применённая к нескольким входным данным, также порождает члены взаимодействия)
- Train/Test Split — правильный способ оценки любой регрессионной модели
- Кросс-валидация — более надёжный способ настройки степени полинома, чем единственное разделение
- Feature Scaling — почему и как стандартизировать входные данные перед обучением