← Назад к вопросам

Как меняется bias и variance при увеличении k в k-NN модели?

1.8 Middle🔥 211 комментариев
#Машинное обучение

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Bias-Variance Tradeoff в k-NN: Полный анализ

k-Nearest Neighbors (k-NN) — это простой, но очень информативный алгоритм для понимания компромисса между смещением (bias) и дисперсией (variance).

1. Теоретические основы

Что такое Bias и Variance?

Ощибка модели = Bias² + Variance + Irreducible Error

Bias (смещение):

  • Способность модели захватить истинную зависимость
  • Ошибка от неправильных предположений в модели
  • Высокий bias → недообучение (underfitting)
  • Низкий bias → модель может приспособиться к сложным паттернам

Variance (дисперсия):

  • Чувствительность модели к колебаниям в обучающих данных
  • Насколько сильно меняются предсказания при изменении тренировочного набора
  • Высокая variance → переобучение (overfitting)
  • Низкая variance → стабильные предсказания

2. Как k влияет на bias в k-NN

При k=1:

from sklearn.neighbors import KNeighborsRegressor
import numpy as np
import matplotlib.pyplot as plt

# Создать простой dataset
np.random.seed(42)
X = np.linspace(0, 10, 50).reshape(-1, 1)
y_true = np.sin(X).ravel()
y_train = y_true + np.random.normal(0, 0.2, len(y_true))  # Добавить шум

# k=1
knn_1 = KNeighborsRegressor(n_neighbors=1)
knn_1.fit(X, y_train)
y_pred_1 = knn_1.predict(X)

print("k=1 (Очень низкий bias, очень высокая variance):")
print(f"  MSE на тренировочных данных: {np.mean((y_pred_1 - y_train)**2):.6f}")
print(f"  Модель практически интерполирует данные")
print(f"  Каждая точка просто = ближайшему соседу = самой себе")
print(f"  На новых данных может дать совсем другой результат")

Характеристика k=1:

  • Модель запоминает каждый обучающий пример
  • Каждая точка тренировочного набора идеально предсказывается
  • На неизвестных данных может быть очень шумно
  • Bias близок к 0 (модель может приспособиться к любому паттерну)
  • Variance максимальна (любое изменение в данных сильно влияет)

При k=5, k=10, ..., k=n:

# Сравнение разных k
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle("Влияние k на предсказания k-NN")

k_values = [1, 5, 15, 30]
for idx, k in enumerate(k_values):
    ax = axes[idx // 2, idx % 2]
    
    knn = KNeighborsRegressor(n_neighbors=k)
    knn.fit(X, y_train)
    y_pred = knn.predict(X)
    
    ax.scatter(X, y_train, alpha=0.5, label="Training data")
    ax.plot(X, y_true, "g-", label="True function", linewidth=2)
    ax.plot(X, y_pred, "r--", label="k-NN prediction", linewidth=2)
    
    ax.set_title(f"k={k}")
    ax.set_ylim([-1.5, 1.5])
    ax.legend()
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Характеристика большого k:

  • Модель усредняет по большему количеству соседей
  • Предсказания становятся более гладкими
  • Bias растёт (модель не может захватить сложные локальные паттерны)
  • Variance падает (предсказания менее чувствительны к шуму)

3. Количественный анализ

from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score
import numpy as np

# Создать dataset
X, y = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=15,
    n_redundant=5,
    random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

k_values = np.arange(1, 51, 2)
train_errors = []
test_errors = []

for k in k_values:
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train, y_train)
    
    train_acc = knn.score(X_train, y_train)
    test_acc = knn.score(X_test, y_test)
    
    train_errors.append(1 - train_acc)  # Error = 1 - Accuracy
    test_errors.append(1 - test_acc)

plt.figure(figsize=(12, 6))
plt.plot(k_values, train_errors, "o-", label="Train Error", linewidth=2)
plt.plot(k_values, test_errors, "s-", label="Test Error", linewidth=2)
plt.xlabel("k (Number of Neighbors)", fontsize=12)
plt.ylabel("Error Rate", fontsize=12)
plt.title("k-NN: Train vs Test Error при изменении k", fontsize=14)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.show()

optimal_k = k_values[np.argmin(test_errors)]
print(f"\nОптимальное k: {optimal_k}")
print(f"Min test error: {min(test_errors):.3f}")

4. Bias-Variance разложение в k-NN

from sklearn.neighbors import KNeighborsRegressor
import numpy as np

def estimate_bias_variance(X_train, y_train, X_test, y_test, k, n_bootstrap=100):
    """
    Оценить bias и variance через bootstrap
    """
    predictions = np.zeros((n_bootstrap, len(X_test)))
    
    for i in range(n_bootstrap):
        # Bootstrap sample
        indices = np.random.choice(
            len(X_train),
            size=len(X_train),
            replace=True
        )
        X_boot = X_train[indices]
        y_boot = y_train[indices]
        
        # Обучить модель
        model = KNeighborsRegressor(n_neighbors=k)
        model.fit(X_boot, y_boot)
        predictions[i] = model.predict(X_test)
    
    # Средние предсказания
    mean_prediction = predictions.mean(axis=0)
    
    # Bias: ошибка от среднего предсказания
    bias = np.mean((mean_prediction - y_test) ** 2)
    
    # Variance: ошибка от вариабельности предсказаний
    variance = np.mean(predictions.var(axis=0))
    
    # Total squared error
    total_error = np.mean((predictions.mean(axis=0) - y_test) ** 2) + variance
    
    return bias, variance, total_error

# Создать regression dataset
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = np.sin(X).ravel() + np.random.normal(0, 0.2, len(X))

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

k_values = [1, 3, 5, 10, 20, 50]
biases = []
variances = []
total_errors = []

for k in k_values:
    bias, variance, total_error = estimate_bias_variance(
        X_train, y_train, X_test, y_test, k
    )
    biases.append(bias)
    variances.append(variance)
    total_errors.append(total_error)
    
    print(f"\nk={k}:")
    print(f"  Bias²: {bias:.4f}")
    print(f"  Variance: {variance:.4f}")
    print(f"  Total Error: {total_error:.4f}")

# Визуализировать
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Слева: Bias vs Variance trade-off
ax1.plot(k_values, biases, "o-", label="Bias²", linewidth=2, markersize=8)
ax1.plot(k_values, variances, "s-", label="Variance", linewidth=2, markersize=8)
ax1.plot(k_values, total_errors, "^-", label="Total Error", linewidth=2, markersize=8)
ax1.set_xlabel("k", fontsize=12)
ax1.set_ylabel("Error", fontsize=12)
ax1.set_title("Bias-Variance Tradeoff в k-NN", fontsize=14)
ax1.legend(fontsize=10)
ax1.grid(True, alpha=0.3)

# Справа: Визуальное объяснение
ax2.text(0.05, 0.95, "При k=1:", transform=ax2.transAxes, fontsize=11, weight="bold", va="top")
ax2.text(0.05, 0.88, "• Bias → минимум", transform=ax2.transAxes, fontsize=10, va="top")
ax2.text(0.05, 0.82, "• Variance → максимум", transform=ax2.transAxes, fontsize=10, va="top")
ax2.text(0.05, 0.75, "• Model: очень гибкая, переобучение", transform=ax2.transAxes, fontsize=10, va="top")

ax2.text(0.05, 0.65, "При k=n (все соседи):", transform=ax2.transAxes, fontsize=11, weight="bold", va="top")
ax2.text(0.05, 0.58, "• Bias → максимум", transform=ax2.transAxes, fontsize=10, va="top")
ax2.text(0.05, 0.52, "• Variance → минимум", transform=ax2.transAxes, fontsize=10, va="top")
ax2.text(0.05, 0.45, "• Model: всегда средний класс, недообучение", transform=ax2.transAxes, fontsize=10, va="top")

ax2.text(0.05, 0.35, "Оптимальный k:", transform=ax2.transAxes, fontsize=11, weight="bold", va="top")
ax2.text(0.05, 0.28, "• Баланс между bias и variance", transform=ax2.transAxes, fontsize=10, va="top")
ax2.text(0.05, 0.22, "• Обычно через кросс-валидацию", transform=ax2.transAxes, fontsize=10, va="top")

ax2.set_xlim(0, 1)
ax2.set_ylim(0, 1)
ax2.axis("off")

plt.tight_layout()
plt.show()

5. Практическое влияние k

Случай 1: k=1

Большой соседей: 1
Предсказание: класс единственного ближайшего соседа

Проблема: 
- Очень чувствителен к выбросам
- Если сосед - выброс, дать неправильный класс
- На новых данных может быть совсем другой результат

Случай 2: k=sqrt(n)

import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target
n_samples = len(X)

# Рекомендация: k = sqrt(n) для классификации
recommended_k = int(np.sqrt(n_samples))
print(f"Dataset size: {n_samples}")
print(f"Recommended k: sqrt({n_samples}) ≈ {recommended_k}")

model = KNeighborsClassifier(n_neighbors=recommended_k)
model.fit(X, y)
print(f"Training accuracy: {model.score(X, y):.3f}")

Случай 3: k=n (все соседи)

Большой соседей: все примеры
Предсказание: класс большинства (самый частый класс)

Проблема:
- Модель полностью игнорирует признаки
- Всегда предсказывает класс большинства
- Accuracy = % наиболее частого класса
- Никакой способности к обучению

6. Таблица: Как меняются метрики

┌────┬──────────┬──────────┬────────────┬─────────────┐
│ k  │  Bias    │ Variance │ Train Acc  │ Test Acc    │
├────┼──────────┼──────────┼────────────┼─────────────┤
│ 1  │ Очень    │ Очень    │ 100% или   │ Может быть  │
│    │ низкий   │ высокая  │ близко     │ низкой      │
├────┼──────────┼──────────┼────────────┼─────────────┤
│ 3  │ Низкий   │ Высокая  │ Высокая    │ Хорошая     │
├────┼──────────┼──────────┼────────────┼─────────────┤
│ 5  │ Средний  │ Средняя  │ Хорошая    │ Хорошая     │
├────┼──────────┼──────────┼────────────┼─────────────┤
│ 10 │ Выше     │ Ниже     │ Ниже       │ Может быть  │
│    │ среднего │ среднего │            │ хорошая     │
├────┼──────────┼──────────┼────────────┼─────────────┤
│ n  │ Очень    │ Очень    │ % большин. │ % большин.  │
│    │ высокий  │ низкая   │            │             │
└────┴──────────┴──────────┴────────────┴─────────────┘

7. Выбор оптимального k

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

# Метод 1: Cross-validation
k_values = np.arange(1, 31)
mean_scores = []
std_scores = []

for k in k_values:
    model = KNeighborsClassifier(n_neighbors=k)
    scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
    mean_scores.append(scores.mean())
    std_scores.append(scores.std())

# Найти оптимальный k
optimal_idx = np.argmax(mean_scores)
optimal_k = k_values[optimal_idx]

plt.figure(figsize=(10, 5))
plt.errorbar(k_values, mean_scores, yerr=std_scores, marker="o")
plt.xlabel("k")
plt.ylabel("Cross-validated Accuracy")
plt.title(f"Выбор оптимального k (оптимум в k={optimal_k})")
plt.grid(True, alpha=0.3)
plt.axvline(x=optimal_k, color="red", linestyle="--", label=f"Optimal k={optimal_k}")
plt.legend()
plt.show()

print(f"\nОптимальное k: {optimal_k}")
print(f"Mean accuracy: {mean_scores[optimal_idx]:.3f}")
print(f"Std deviation: {std_scores[optimal_idx]:.3f}")

8. Итоговые выводы

При увеличении k в k-NN:

  1. Bias РАСТЁТ:

    • Модель усредняет по большему числу соседей
    • Локальные паттерны теряются
    • Становится сложнее захватить сложные зависимости
  2. Variance ПАДАЕТ:

    • Предсказания становятся стабильнее
    • Меньше влияния шума в обучающих данных
    • Более гладкие границы решения
  3. Общая ошибка:

    • При маленьких k: высокая variance доминирует (переобучение)
    • При больших k: высокий bias доминирует (недообучение)
    • Оптимум k - компромисс между ними

Практические рекомендации:

  • Начни с k=sqrt(n) для классификации
  • Используй кросс-валидацию для поиска оптимума
  • При дисбалансе классов: чётное k помогает
  • Для регрессии: часто k больше, чем для классификации
Как меняется bias и variance при увеличении k в k-NN модели? | PrepBro