Переобучение, недообучение, смещение и дисперсия

В мире машинного обучения построение модели похоже на обучение студента. Наша цель состоит в том, чтобы этотстудентмог не только запомнить примеры задач из учебника (обучающие данные), но и глубоко понять лежащие в основе принципы, чтобы достичь хороших результатов на совершенно невиданных экзаменационных вопросах (тестовые данные). Однако этотстудентможет столкнуться с двумя типичными проблемами в процессе обучения:

  • Одна — учиться слишком жёстко, лишь механически применяя примеры (недообучение);
  • Другая — учиться слишком хитро, запоминая даже знаки препинания и особенности почерка в примерах, так что они теряются перед новыми вопросами (переобучение)。

Пониманиепереобученияинедообучения, а также более глубоких теоретических концепций, стоящих за ними, —смещениеидисперсия, — ключевой шаг для каждого практика машинного обучения на пути от новичка до мастера. Они объясняют, почему модели ошибаются, и показывают нам направление улучшения модели.


I. Основные понятия: производительность модели и состояние «подгонки»

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

Пример

import numpy as np
import matplotlib.pyplot as plt

# -------------------------- Начало настройки китайского шрифта --------------------------
plt.rcParams['font.sans-serif'] = [
    # Сначала Windows
    'SimHei', 'Microsoft YaHei',
    # Сначала macOS
    'PingFang SC', 'Heiti TC',
    # Сначала Linux
    'WenQuanYi Micro Hei', 'DejaVu Sans'
]
# Исправление проблемы, когда знаки минуса отображаются в виде квадратов
plt.rcParams['axes.unicode_minus'] = False
# -------------------------- Конец настройки китайского шрифта --------------------------

# Сгенерировать имитационные данные: добавить случайный шум на основе синусоиды
np.random.seed(42)
X = np.linspace(0, 10, 20)
y_true = np.sin(X)                     # Истинная лежащая в основе закономерность (которую мы не знаем)
y_noise = np.random.randn(20) * 0.3   # Случайный шум
y = y_true + y_noise                  # Данные, которые мы фактически наблюдаем

plt.scatter(X, y, label='Наблюдаемые данные (с шумом)', color='blue', alpha=0.6)
plt.plot(X, y_true, label='Истинная закономерность (y=sin(x))', color='green', linewidth=2)
plt.xlabel('X')
plt.ylabel('y')
plt.title('Данные и лежащая в основе закономерность')
plt.legend()
plt.grid(True)
plt.show()

Наша цель — найти кривую (модель), которая лучше всего описывает закономерность, отражённую этими синими точками диаграммы рассеяния (данными).

Степень, в которой модель описывает данные, — этоподгонка。

1. Недообучение

Недообучениеозначает модель, которая слишком проста, чтобы уловить базовые закономерности или структуры в данных. Это как студент, который выучил только сложение, но его просят решать задачи по математическому анализу.

  • Поведение: модель работает очень плохо наобучающих данных(например, низкая точность, высокая ошибка).
  • Причины: сложность модели слишком низкая, признаков недостаточно или обучение недостаточное.
  • Аналогия: использование прямой линии (полинома первой степени) для подгонки данных с очевидной криволинейной тенденцией.

Пример

from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# Попробуйте выполнить подгонку полиномом 1-й степени (прямой линией)
poly = PolynomialFeatures(degree=1)
X_poly1 = poly.fit_transform(X.reshape(-1, 1))
model_under = LinearRegression()
model_under.fit(X_poly1, y)
y_pred_under = model_under.predict(X_poly1)

mse_train_under = mean_squared_error(y, y_pred_under)
print(f"Среднеквадратичная ошибка (MSE) недообученной модели на обучающем наборе: {mse_train_under:.4f}")

Вывод:

欠拟合模型在训练集上的均方误差 (MSE): 0.4402
欠拟合模型在训练集上的均方误差 (MSE): 0.4402

2. Оптимальная подгонка

Это идеальное состояние. Модель достаточно сложна, чтобы выучить ключевые закономерности в данных, но не настолько сложна, чтобы выучить случайный шум. Она хорошо работает как на обучающем наборе, так и на невидимых тестовых наборах.

  • Поведение: Ошибки и на обучающем наборе, и на тестовом наборе низкие, и они близки.
  • Аналогия: Использование полинома подходящей степени (например, 3-й степени) для подгонки данных.

Пример

# Попробуйте подогнать полиномом 3-й степени
poly = PolynomialFeatures(degree=3)
X_poly3 = poly.fit_transform(X.reshape(-1, 1))
model_good = LinearRegression()
model_good.fit(X_poly3, y)
y_pred_good = model_good.predict(X_poly3)

mse_train_good = mean_squared_error(y, y_pred_good)
print(f"Среднеквадратичная ошибка (MSE) хорошо подогнанной модели на обучающем наборе: {mse_train_good:.4f}")

Вывод:

欠拟合模型在训练集上的均方误差 (MSE): 0.4402
良好拟合模型在训练集上的均方误差 (MSE): 0.3988

3. Переобучение

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

  • Поведение: Модель показывает чрезвычайно хорошие результаты наобучающих данных(с очень малой ошибкой), но нановых, не встречавшихся ранее данныхеё производительность резко падает, демонстрируя плохую способность к обобщению.
  • Причины: Сложность модели слишком высока, а объём обучающих данных слишком мал.
  • Аналогия: Использовать полином очень высокой степени (например, 15-й степени) для подгонки данных, из-за чего кривая проходит почти через каждую точку данных и становится крайне искажённой.

Пример

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# -------------------------- Начало настройки китайского шрифта --------------------------
plt.rcParams['font.sans-serif'] = [
    # Сначала Windows
    'SimHei', 'Microsoft YaHei',
    # Сначала macOS
    'PingFang SC', 'Heiti TC',
    # Сначала Linux
    'WenQuanYi Micro Hei', 'DejaVu Sans'
]
# Исправление проблемы, когда знаки минуса отображаются в виде квадратов
plt.rcParams['axes.unicode_minus'] = False
# -------------------------- Конец настройки китайского шрифта --------------------------

# Сгенерировать имитационные данные: добавить случайный шум на основе синусоиды
np.random.seed(42)
X = np.linspace(0, 10, 20)
y_true = np.sin(X)                     # Истинная лежащая в основе закономерность (которую мы не знаем)
y_noise = np.random.randn(20) * 0.3   # Случайный шум
y = y_true + y_noise                  # Данные, которые мы фактически наблюдаем

# Попробуйте выполнить подгонку полиномом 1-й степени (прямой линией)
poly = PolynomialFeatures(degree=1)
X_poly1 = poly.fit_transform(X.reshape(-1, 1))
model_under = LinearRegression()
model_under.fit(X_poly1, y)
y_pred_under = model_under.predict(X_poly1)

mse_train_under = mean_squared_error(y, y_pred_under)
print(f"Среднеквадратичная ошибка (MSE) недообученной модели на обучающем наборе: {mse_train_under:.4f}")

# Попробуйте подогнать полиномом 3-й степени
poly = PolynomialFeatures(degree=3)
X_poly3 = poly.fit_transform(X.reshape(-1, 1))
model_good = LinearRegression()
model_good.fit(X_poly3, y)
y_pred_good = model_good.predict(X_poly3)

mse_train_good = mean_squared_error(y, y_pred_good)
print(f"Среднеквадратичная ошибка (MSE) хорошо подогнанной модели на обучающем наборе: {mse_train_good:.4f}")

# Попробуйте выполнить подгонку полиномом 15-й степени (крайне склонен к переобучению)
poly = PolynomialFeatures(degree=15)
X_poly15 = poly.fit_transform(X.reshape(-1, 1))
model_over = LinearRegression()
model_over.fit(X_poly15, y)
y_pred_over = model_over.predict(X_poly15)

mse_train_over = mean_squared_error(y, y_pred_over)
print(f"Среднеквадратичная ошибка (MSE) переобученной модели на обучающем наборе: {mse_train_over:.4f}")

# Визуализация трёх состояний подгонки
plt.figure(figsize=(15, 4))

# Недообучение
plt.subplot(1, 3, 1)
plt.scatter(X, y, alpha=0.6)
plt.plot(X, y_pred_under, color='red', linewidth=2, label='Недообучение (1-я степень)')
plt.plot(X, y_true, color='green', linestyle='--', label='Истинная закономерность')
plt.title(f'Недообучение\nMSE на обучении: {mse_train_under:.4f}')
plt.legend()
plt.grid(True)

# Хорошая подгонка
plt.subplot(1, 3, 2)
plt.scatter(X, y, alpha=0.6)
plt.plot(X, y_pred_good, color='red', linewidth=2, label='Хорошая подгонка (3-я степень)')
plt.plot(X, y_true, color='green', linestyle='--', label='Истинная закономерность')
plt.title(f'Хорошая подгонка\nMSE на обучении: {mse_train_good:.4f}')
plt.legend()
plt.grid(True)

# Переобучение
plt.subplot(1, 3, 3)
plt.scatter(X, y, alpha=0.6)
plt.plot(X, y_pred_over, color='red', linewidth=2, label='Переобучение (15-й степени)')
plt.plot(X, y_true, color='green', linestyle='--', label='Истинная закономерность')
plt.title(f'Переобучение\nMSE на обучении: {mse_train_over:.4f}')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

Из рисунка хорошо видно:

  • Недообучение (слева):Красная прямая линия совершенно не улавливает колеблющуюся тенденцию данных.
  • Хорошая подгонка (посередине):Красная кривая приблизительно следует тенденции зелёной истинной закономерности.
  • Переобучение (справа):Красная кривая сильно колеблется, пытаясь пройти через каждую синюю разбросанную точку, включая шумовые точки, полностью теряя гладкую форму синусоиды.

II. Теоретические основы: разложение на смещение и дисперсию

Смещение и дисперсия дают теоретическую основу для понимания переобучения и недообучения. Они описывают два разных источника ошибки модели.

Мы можем разложитьобщую ошибку моделина:Смещение² + Дисперсия + Неустранимая ошибка。

1. Смещение

  • Определение: разница междуожидаемым значением(то есть средним предсказанным значением) и истинным значением. Оно отражает присущую моделисистематическую ошибку, то есть верны ли предположения модели о природе задачи.
  • Поведение при высоком смещении: модель слишком проста, чтобы уловить характеристики данных, что приводит кнедообучению. Независимо от того, какие данные используются для обучения, результаты отклоняются от истинных значений.
  • Пример: всегда использовать простую линейную модель "цена дома = площадь × 1000" для предсказания всех видов домов, игнорируя такие важные факторы, как местоположение и этаж, — это высокое смещение.

2. Дисперсия

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

3. Компромисс между смещением и дисперсией

Это ключевой компромисс в машинном обучении.Мы не можем одновременно минимизировать и смещение, и дисперсию.

  • Увеличение сложности модели: обычно уменьшаетсмещение(модель становится более мощной), но увеличиваетдисперсию(она легче выучивает шум).
  • Уменьшение сложности модели: обычно уменьшаетдисперсию(модель становится более стабильной), но увеличиваетсмещение(модель становится слабее).

Наша цель — найти «оптимальную точку» на графике так, чтобы общая ошибка была минимизирована.


III. Диагностика и контрмеры

Как определить, в каком состоянии находится модель? Как это решить?

1. Метод диагностики: кривые обучения

Кривая обучения отображает производительность модели (например, ошибку) наобучающем набореивалидационном наборев зависимости отколичества обучающих примеровилисложности модели.

Пример

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.model_selection import learning_curve
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.metrics import mean_squared_error
import warnings
warnings.filterwarnings('ignore')

# -------------------------- Начало настройки китайского шрифта --------------------------
plt.rcParams['font.sans-serif'] = [
    # Сначала Windows
    'SimHei', 'Microsoft YaHei',
    # Сначала macOS
    'PingFang SC', 'Heiti TC',
    # Сначала Linux
    'WenQuanYi Micro Hei', 'DejaVu Sans'
]
# Исправить проблему отображения отрицательных знаков в виде блоков
plt.rcParams['axes.unicode_minus'] = False
# Установить стиль графика
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['axes.grid'] = True
plt.rcParams['grid.alpha'] = 0.3
# -------------------------- Конец настройки китайского шрифта --------------------------

# Загрузить данные
data = load_diabetes()
X, y = data.data, data.target
# Использовать только один признак (лучше для демонстрации полиномиальной регрессии)
X = X[:, np.newaxis, 2]  # Выбрать третий признак (BMI)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Определить функцию построения кривой обучения (оптимизированная версия)
def plot_learning_curve(estimator, title, X, y, cv=5, train_sizes=np.linspace(0.1, 1.0, 10)):
    """
Построить кривую обучения
Параметры:
estimator: оценщик модели
title: заголовок графика
X: данные признаков
y: целевая переменная
cv: количество фолдов кросс-валидации
train_sizes: пропорции обучающих выборок
    """

    # Получение данных кривой обучения
    train_sizes_abs, train_scores, test_scores = learning_curve(
        estimator, X, y, cv=cv, scoring='neg_mean_squared_error',
        train_sizes=train_sizes, random_state=42, n_jobs=-1
    )
   
    # Вычисление среднего и стандартного отклонения
    train_scores_mean = -train_scores.mean(axis=1)
    train_scores_std = train_scores.std(axis=1)
    test_scores_mean = -test_scores.mean(axis=1)
    test_scores_std = -test_scores.std(axis=1)
   
    # Построение кривых обучения
    plt.figure(figsize=(10, 6))
    plt.fill_between(train_sizes_abs,
                     train_scores_mean - train_scores_std,
                     train_scores_mean + train_scores_std,
                     alpha=0.1, color='r')
    plt.fill_between(train_sizes_abs,
                     test_scores_mean - test_scores_std,
                     test_scores_mean + test_scores_std,
                     alpha=0.1, color='g')
   
    # Построение средних кривых
    plt.plot(train_sizes_abs, train_scores_mean, 'o-', color='r', linewidth=2,
             markersize=8, label='MSE обучающей выборки')
    plt.plot(train_sizes_abs, test_scores_mean, 'o-', color='g', linewidth=2,
             markersize=8, label='MSE валидационной выборки')
   
    # Задать свойства графика
    plt.xlabel('Количество обучающих примеров', fontsize=12)
    plt.ylabel('Среднеквадратичная ошибка (MSE)', fontsize=12)
    plt.title(title, fontsize=14, pad=20)
    plt.legend(loc='upper right', fontsize=11)
    plt.tight_layout()
    plt.show()
   
    # Вывести производительность модели на тестовом наборе
    estimator.fit(X_train, y_train)
    y_pred = estimator.predict(X_test)
    mse = mean_squared_error(y_test, y_pred)
    print(f"{title} - MSE на тестовом наборе: {mse:.2f}")

# 1. Модель с недообучением (полином первой степени - линейная регрессия)
print("="*60)
print("Модель с недообучением (полином первой степени - линейная регрессия)")
print("="*60)
plot_learning_curve(
    make_pipeline(StandardScaler(), PolynomialFeatures(1), LinearRegression()),
    'Кривая обучения модели с недообучением (полином первой степени)',
    X, y
)

# 2. Хорошо подходящая модель (полином 2-й степени)
print("\n" + "="*60)
print("Хорошо подходящая модель (полином 2-й степени)")
print("="*60)
plot_learning_curve(
    make_pipeline(StandardScaler(), PolynomialFeatures(2), LinearRegression()),
    'Кривая обучения для хорошо подходящей модели (полином 2-й степени)',
    X, y
)

# 3. Переобученная модель (полином 8-й степени)
print("\n" + "="*60)
print("Переобученная модель (полином 8-й степени)")
print("="*60)
plot_learning_curve(
    make_pipeline(StandardScaler(), PolynomialFeatures(8), LinearRegression()),
    'Кривая обучения переобученной модели (полином 8-й степени)',
    X, y
)

# Дополнительно: визуализировать эффекты подгонки моделей с разными порядками
plt.figure(figsize=(12, 8))
X_plot = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)

# Построить исходные точки данных
plt.scatter(X_train, y_train, alpha=0.5, label='Обучающие данные', color='blue', s=30)
plt.scatter(X_test, y_test, alpha=0.5, label='Тестовые данные', color='orange', s=30)

# Построение кривых аппроксимации для разных порядков
orders = [1, 2, 8]
colors = ['red', 'green', 'purple']
labels = ['1-й порядок (недообучение)', '2-й порядок (хорошая подгонка)', '8-й порядок (переобучение)']

for i, order in enumerate(orders):
    model = make_pipeline(StandardScaler(), PolynomialFeatures(order), LinearRegression())
    model.fit(X_train, y_train)
    y_plot = model.predict(X_plot)
    plt.plot(X_plot, y_plot, color=colors[i], linewidth=2, label=labels[i])

plt.xlabel('признак ИМТ (стандартизированный)', fontsize=12)
plt.ylabel('Индекс прогрессирования диабета', fontsize=12)
plt.title('Сравнение эффектов подгонки полиномиальной регрессии с разными порядками', fontsize=14, pad=20)
plt.legend(fontsize=11)
plt.tight_layout()
plt.show()

Как интерпретировать кривые обучения?

Состояние подгонки Ошибка обучения Ошибка валидации Характеристики кривой
Недообучение Высокая Высокая Обе кривые высоки и очень близки; добавление данных не помогает.
Хорошая подгонка Низкая Низкая Обе кривые низки и близки друг к другу, достигая точки баланса.
Переобучение Очень низкая Высокая Ошибка обучения очень низкая, но ошибка валидации высокая, с заметным разрывом между ними. Добавление данных обычно сближает их.

2. Контрмеры

По результатам диагностики мы можем применять разные стратегии:

Борьба с недообучением (высокое смещение):

  • Увеличьте сложность модели: используйте более мощные модели (например, перейдите от линейных моделей к древовидным моделям, нейронным сетям).
  • Добавьте больше признаков: извлекайте или конструируйте более значимые признаки.
  • Уменьшите регуляризацию: если используется регуляризация (например, L1, L2), попробуйте ослабить её силу.
  • Увеличьте время обучения: для итеративных моделей (например, нейронных сетей) обучайте больше эпох.

Борьба с переобучением (высокая дисперсия):

  • Получите больше обучающих данных: один из самых эффективных методов.
  • Уменьшите сложность модели: выберите более простую модель (например, меньшую степень полинома, уменьшите глубину дерева, уменьшите число слоёв нейронной сети).
  • Отбор признаков: удалите нерелевантные или избыточные признаки.
  • Увеличьте регуляризацию:
    • L1-регуляризация (Lasso): склонна создавать разреженные веса и может использоваться для отбора признаков.
    • L2-регуляризация (Ridge): вызывает затухание весов, стремясь сделать все веса малыми.
    • Dropout(для нейронных сетей): Случайно "отключает" часть нейронов во время обучения.
  • Early stopping(для итеративных моделей): Остановите обучение, когда ошибка валидации перестаёт уменьшаться.

IV. Практическое занятие: опыт работы с реальными наборами данных

Давайте попрактикуемся на классическом наборе данных о жилье в Бостоне (или на наборе данных по диабету, так как набор данных Boston устарел).

Пример

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error

# -------------------------- Начало настройки китайского шрифта --------------------------
plt.rcParams['font.sans-serif'] = [
    # Сначала Windows
    'SimHei', 'Microsoft YaHei',
    # Сначала macOS
    'PingFang SC', 'Heiti TC',
    # Сначала Linux
    'WenQuanYi Micro Hei', 'DejaVu Sans'
]
# Исправить проблему отображения отрицательных знаков в виде блоков
plt.rcParams['axes.unicode_minus'] = False
# -------------------------- Конец настройки китайского шрифта --------------------------

# Загрузить данные
data = load_diabetes()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Попробуйте деревья решений с разной сложностью
max_depths = [1, 3, 10, None]  # None означает отсутствие ограничения глубины; дерево продолжает расти, пока не станет "чистым"
train_errors = []
test_errors = []

for depth in max_depths:
    model = DecisionTreeRegressor(max_depth=depth, random_state=42)
    model.fit(X_train, y_train)

    y_train_pred = model.predict(X_train)
    y_test_pred = model.predict(X_test)

    train_error = mean_squared_error(y_train, y_train_pred)
    test_error = mean_squared_error(y_test, y_test_pred)

    train_errors.append(train_error)
    test_errors.append(test_error)

    print(f"Максимальная глубина дерева: {depth if depth is not None else 'Неограниченная'}")
    print(f" MSE на обучающей выборке: {train_error:.2f}")
    print(f" MSE на тестовой выборке: {test_error:.2f}")
    print("-" * 30)

# Визуализация
plt.figure(figsize=(10, 6))
depths = [str(d) if d else 'Неограниченная' for d in max_depths]
x_index = np.arange(len(depths))
width = 0.35

plt.bar(x_index - width/2, train_errors, width, label='Ошибка на обучающей выборке', color='skyblue')
plt.bar(x_index + width/2, test_errors, width, label='Ошибка на тестовой выборке', color='lightcoral')

plt.xlabel('Максимальная глубина дерева решений (сложность модели)')
plt.ylabel('Среднеквадратичная ошибка (MSE)')
plt.title('Компромисс между смещением и дисперсией: качество работы деревьев решений с разной сложностью')
plt.xticks(x_index, depths)
plt.legend()
plt.grid(True, axis='y')
plt.tight_layout()
plt.show()

Результаты анализа:

  • Depth=1: Модель очень простая, ошибки на обучающей и тестовой выборках высоки ->высокое смещение, недообучение。
  • Depth=3: Сложность модели растёт, обе ошибки значительно уменьшаются и относительно близки ->баланс смещения и дисперсии, хорошая подгонка。
  • Depth=10 или без ограничений: Модель очень сложная, ошибка на обучающей выборке чрезвычайно низкая, но ошибка на тестовой выборке начинает расти (или намного выше ошибки на обучающей выборке) ->высокая дисперсия, переобучение。

Итог

Понимание переобучения, недообучения, смещения и дисперсии — это краеугольный камень создания превосходных моделей машинного обучения. Запомните этот основной цикл:

  1. Обучите модель -> Оцените её эффективность на обучающем и валидационном наборах。
  2. Диагностируйте проблемы с помощью кривых обучения или сравнения ошибок: это высокое смещение (недообучение) или высокая дисперсия (переобучение)?
  3. Примените соответствующую стратегию(увеличьте сложность/данные, регуляризация и т. д.), чтобы улучшить результат.
  4. Вернитесь к шагу 1, пока на валидационном наборе не будет получена удовлетворительная модель с сильной способностью к обобщению.
Другие расширения