← Программа курса
Занятие 03 · Модуль 1 · Классический ML · 45 мин чтения

Линейная регрессия

Первая модель: предсказываем число. MSE как «чаша ошибки», обучение в scikit-learn, train/test, метрики MAE/RMSE/R² и квартет Анскомба — почему нельзя верить одной цифре.

MSEsklearntrain/testквартет Анскомба
Скачать ноутбук урока ↓

В прошлом уроке мы разобрались, что такое машинное обучение в целом: есть объекты, есть ответы, и мы хотим научиться предсказывать ответы для новых объектов. Сегодня берём самую простую и самую важную модель — линейную регрессию. Это фундамент: поймёшь её по-настоящему, и половина остального курса станет очевидной, потому что нейросети, бустинг и даже трансформеры устроены вокруг той же идеи «подбираем числа, чтобы ошибка стала меньше».

Урок длинный и неспешный. Мы не просто запустим LinearRegression() в две строки — мы разберём, что именно происходит внутри, увидим это на графиках, которые строятся настоящим кодом, и в конце поймём, почему одной красивой метрике верить нельзя.

Задача: предсказать цену квартиры

Представь, что мы хотим предсказывать цену квартиры по её площади. У нас есть 15 наблюдений — это обучающие примеры. Сначала просто посмотрим на точки, без всякой модели.

import numpy as np
import pandas as pd

# площадь в квадратных метрах
area = np.array([25, 28, 32, 35, 38, 42, 45, 48, 52, 55, 60, 65, 70, 75, 80])
# цена в миллионах рублей
price = np.array([3.6, 3.9, 4.3, 4.5, 4.9, 5.4, 5.6, 6.1, 6.5, 6.8, 7.5, 8.0, 8.5, 9.1, 9.6])

data = pd.DataFrame({"area": area, "price": price})

import matplotlib.pyplot as plt
plt.scatter(area, price)
plt.xlabel("площадь, м²")
plt.ylabel("цена, млн ₽")
plt.show()
Облако из 15 точек: по горизонтали площадь, по вертикали цена; точки растут слева направо
15 квартир: чем больше площадь, тем выше цена. Идеально на одной линии точки не лежат, но тренд виден.

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

Теперь пришёл новый клиент: у него квартира 50 м², сколько она стоит? В данных именно такой нет — есть 48 и 52, но не 50. Значит, нужна функция, которая по любой площади выдаёт предсказанную цену:

площадь    a(площадь)    предсказанная цена\text{площадь} \;\longrightarrow\; a(\text{площадь}) \;\longrightarrow\; \text{предсказанная цена}

Эту функцию aa принято называть алгоритмом или моделью. Наша задача — выбрать её так, чтобы предсказания были близки к правде.

Идея: почему именно прямая?

Точки лежат почти на одной линии. Самая простая функция, описывающая такой тренд, — это прямая:

a(x)=w0+w1xa(x) = w_0 + w_1 x

Здесь:

Числа w0,w1w_0, w_1 называются весами (или коэффициентами). Именно их модель будет подбирать по данным. Пока их не подобрали, прямую можно провести как угодно: круто или полого, выше или ниже. Можно нарисовать несколько разных прямых и «на глаз» увидеть, что одни проходят ближе к точкам, другие дальше. Но «на глаз» — это не способ. Нужна формула, которая численно скажет, насколько прямая хорошая.

Как измерить качество прямой — MSE

Для каждой точки посчитаем ошибку — на сколько предсказание отличается от реального значения:

ошибка на объекте i=a(xi)yi\text{ошибка на объекте } i = a(x_i) - y_i

Эта ошибка бывает положительной (модель завысила) и отрицательной (занизила). Если просто сложить их, плюсы и минусы погасят друг друга, и даже плохая прямая может дать «ноль в среднем». Чтобы получить честное одно число для всей прямой, делаем три шага:

  1. берём ошибку на каждом объекте a(xi)yia(x_i) - y_i;
  2. возводим в квадрат — так минусы исчезают, а большие промахи штрафуются особенно сильно (промах в 2 раза даёт штраф в 4 раза);
  3. складываем и делим на количество объектов, чтобы получить среднее.

Получается MSE (mean squared error, среднеквадратичная ошибка):

MSE=1i=1(a(xi)yi)2\text{MSE} = \frac{1}{\ell} \sum_{i=1}^{\ell} \bigl( a(x_i) - y_i \bigr)^2

Здесь \ell — число объектов (сколько у нас точек), а знак \sum означает «сложить по всем объектам с i=1i=1 до i=i=\ell». Чем меньше MSE, тем лучше прямая. Теперь задача обучения формулируется чётко:

Найти такие w0w_0 и w1w_1, при которых MSE минимальна.

В коде MSE — это буквально перевод формулы на Python:

def mse(y_true, y_pred):
    return np.mean((y_pred - y_true) ** 2)

Давай посмотрим на «хорошую» прямую, ту, что подобрал алгоритм, и на её ошибки. Каждый вертикальный пунктир от точки до прямой — это и есть одна ошибка a(xi)yia(x_i) - y_i, которую мы возводим в квадрат:

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

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

Где живёт «лучшая» прямая

Зафиксируем на секунду сдвиг w0w_0 и будем менять только наклон w1w_1. Для каждого наклона можно посчитать MSE. Если нарисовать зависимость «наклон → MSE», получится чаша (парабола): по краям ошибка большая, а в одной точке (на дне) — минимальная.

w0 = 0.72  # сдвиг зафиксируем в оптимуме
w1_grid = np.linspace(0.05, 0.17, 200)
errors = [mse(price, w0 + w1 * area) for w1 in w1_grid]

plt.plot(w1_grid, errors)
plt.xlabel("наклон w₁")
plt.ylabel("MSE")
plt.show()
Парабола: по горизонтали наклон w1, по вертикали MSE; внизу отмечена точка минимума
«Обучить модель» = найти дно этой чаши. Здесь минимум при w₁ ≈ 0.111.

Вот что значит «модель учится»: она ищет дно чаши ошибки. Для линейной регрессии чаша всегда гладкая и с одним минимумом, поэтому дно находится надёжно. (В нейросетях рельеф будет куда более изрезанным — но об этом в следующем уроке про градиентный спуск.) К счастью, перебирать веса вручную не нужно: дно уже умеет находить scikit-learn.

Обучение модели в scikit-learn

В sklearn любая модель обучается по одному и тому же шаблону из трёх действий:

model = ИмяМодели()   # 1. создаём модель
model.fit(X, y)       # 2. обучаем на данных (ищем дно чаши)
model.predict(X_new)  # 3. делаем предсказания

Важно: sklearn ожидает, что X — это матрица (строка отвечает за объект, столбец — за признак), даже если признак один. Поэтому одномерный массив area превращаем в матрицу формы (n, 1) методом reshape(-1, 1).

from sklearn.linear_model import LinearRegression

X = area.reshape(-1, 1)   # форма (15, 1)
y = price                 # форма (15,)

model = LinearRegression()
model.fit(X, y)

w0 = model.intercept_
w1 = model.coef_[0]
print(f"a(x) = {w0:.3f} + {w1:.4f} * x")
# → a(x) = 0.721 + 0.1113 * x

Под капотом sklearn нашёл те самые w00.721w_0 \approx 0.721 и w10.111w_1 \approx 0.111, при которых MSE минимальна — то самое дно чаши с прошлого графика. Никакого перебора: для линейной регрессии есть точная формула (покажу её в конце урока в блоке со звёздочкой).

Как читать коэффициенты

Предсказание для новых объектов

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

new_areas = np.array([[30], [50], [72], [100]])
print(model.predict(new_areas))
# → [ 4.061  6.287  8.735 11.852]

Для нашего клиента с 50 м² модель уверенно отвечает: ≈ 6.29 млн ₽. А для 100 м² она тоже выдаёт ответ (≈ 11.85), хотя самая большая квартира в данных была 80 м². Это экстраполяция — модель продолжает прямую за пределы виденного. Иногда это нормально, иногда опасно: реальность не обязана оставаться линейной (например, сверхбольшие квартиры могут дорожать медленнее). Правило: чем дальше от диапазона обучающих данных, тем меньше доверия предсказанию.

Несколько признаков

В жизни цена зависит не только от площади. Пусть добавится расстояние до центра. Теперь у каждой квартиры два признака, и формула становится:

a(x1,x2)=w0+w1x1+w2x2a(x_1, x_2) = w_0 + w_1 x_1 + w_2 x_2

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

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

В коде меняется только форма X, алгоритм тот же:

# добавим к нашим 15 квартирам второй признак — расстояние до центра (км)
data["distance"] = [12, 10, 14, 8, 11, 6, 9, 7, 5, 8, 4, 6, 3, 5, 2]

X2 = data[["area", "distance"]].values   # матрица (15, 2)
y2 = data["price"].values

model2 = LinearRegression().fit(X2, y2)
print(model2.intercept_, model2.coef_)
# → 0.739 [ 0.1111 -0.0011]

Коэффициент при area остался положительным (больше площадь → дороже), а при distance — слегка отрицательный. Но он крошечный, почти ноль. Почему? В наших данных расстояние и площадь сильно связаны (большие квартиры оказались ближе к центру), поэтому площадь уже «объясняет» почти всё, и второму признаку добавить нечего. Это важный урок: маленький коэффициент не всегда значит, что признак не важен — иногда он просто дублирует информацию другого. Если признаков три, четыре или тысяча — принцип ровно тот же, просто нарисовать уже нельзя:

a(x)=w0+w1x1+w2x2++wdxda(x) = w_0 + w_1 x_1 + w_2 x_2 + \dots + w_d x_d

Train / test: честная проверка

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

Решение — разделить данные на две части:

from sklearn.model_selection import train_test_split

# сгенерируем 100 квартир, чтобы проверка была честной
rng = np.random.default_rng(42)
area_big = rng.uniform(20, 100, size=100)
price_big = 0.5 + 0.12 * area_big + rng.normal(0, 0.5, size=100)
X_full, y_full = area_big.reshape(-1, 1), price_big

X_train, X_test, y_train, y_test = train_test_split(
    X_full, y_full, test_size=0.2, random_state=42  # 20% в тест; random_state фиксирует разбиение
)
model = LinearRegression().fit(X_train, y_train)   # учимся ТОЛЬКО на train

random_state=42 — любое фиксированное число: оно делает случайное разбиение воспроизводимым (у тебя и у меня train/test совпадут, и результаты будут сравнимы).

Главное правило. Качество модели оценивается только на тех данных, которых она не видела при обучении.

Когда модель хорошо отвечает на train, но заметно хуже на test — это переобучение (overfitting): она «зазубрила» обучающие примеры вместо того, чтобы уловить общую закономерность. Бороться с ним мы будем на протяжении всего курса.

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

MSE — не единственная метрика. Вот четыре главных, которые стоит знать.

MAE (средняя абсолютная ошибка) — «в среднем модель ошибается на столько-то», в тех же единицах, что и ответ:

MAE=1i=1a(xi)yi\text{MAE} = \frac{1}{\ell} \sum_{i=1}^{\ell} \bigl| a(x_i) - y_i \bigr|

Например: модель сказала 5.0, а правда 5.6 — промах 0.6; усреднили такие промахи по всем объектам и получили, скажем, MAE = 0.36 — «в среднем мажем на 0.36 млн ₽».

MSE — уже знакома, сильнее штрафует большие промахи, но измеряется в «квадратных единицах» (млн ₽ в квадрате, что неудобно интерпретировать).

RMSE — корень из MSE, возвращает нас к исходным единицам:

RMSE=MSE\text{RMSE} = \sqrt{\text{MSE}}

(коэффициент детерминации) — насколько модель лучше «тупой» модели, которая всегда предсказывает среднее:

R2=1i(a(xi)yi)2i(yˉyi)2R^2 = 1 - \frac{\sum_i (a(x_i) - y_i)^2}{\sum_i (\bar{y} - y_i)^2}

Здесь yˉ\bar{y} — среднее значение ответа. В дроби числитель — суммарная квадратичная ошибка нашей модели, а знаменатель отражает ошибку «тупой» модели, которая всегда предсказывает среднее yˉ\bar{y}. Если наша ошибка больше, чем у «тупой», дробь становится больше 1 — тогда R2R^2 уходит в минус. Отсюда три ориентира:

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

pred = model.predict(X_test)
print("MAE :", round(mean_absolute_error(y_test, pred), 3))
print("RMSE:", round(np.sqrt(mean_squared_error(y_test, pred)), 3))
print("R²  :", round(r2_score(y_test, pred), 3))
# → MAE : 0.359
# → RMSE: 0.463
# → R²  : 0.971

R2=0.971R^2 = 0.971 — модель объясняет 97% разброса цен. Отлично, но это и неудивительно: данные мы сами сгенерировали почти линейными. На реальных данных так красиво бывает редко.

Какую метрику выбирать? Если все ошибки одинаково важны — MAE. Если большие промахи особенно нежелательны — RMSE. Чтобы понять общее качество одним числом «от 0 до 1» — R².

Полезнее любой одной цифры — график «предсказание против реальности». По горизонтали кладём настоящую цену, по вертикали — предсказанную. Идеальная модель легла бы точно на диагональ y=y^y = \hat{y}:

plt.scatter(y_test, model.predict(X_test))
lo, hi = y_test.min(), y_test.max()
plt.plot([lo, hi], [lo, hi], "--")   # линия идеала
plt.xlabel("реальная цена")
plt.ylabel("предсказанная цена")
plt.show()
Точки рассеяны вдоль пунктирной диагонали; по осям реальная и предсказанная цена
Точки кучкуются вдоль диагонали — модель почти не смещает предсказания ни вверх, ни вниз.

Если бы точки лежали систематически выше диагонали, модель бы завышала; ниже — занижала бы; веером — ошибалась бы сильнее на дорогих квартирах. Такой график показывает характер ошибок, а не только их размер.

Осторожно: одной метрике верить нельзя

У линейной регрессии есть ловушка, которую важно увидеть сразу. В 1973 году статистик Фрэнсис Анскомб придумал четыре набора данных, у которых совпадают почти все числа: среднее, дисперсия, коэффициент корреляции и даже уравнение прямой регрессии (a(x)3+0.5xa(x) \approx 3 + 0.5x во всех четырёх). Если смотреть только на метрики — наборы неотличимы. А вот если их нарисовать:

Четыре панели I–IV: у всех одинаковая прямая, но данные разные — линия, парабола, прямая с выбросом, вертикаль с одной точкой-рычагом
Квартет Анскомба: одна и та же прямая и одинаковые метрики при совершенно разных данных.

Мораль: всегда смотри на данные глазами, а не только на R2R^2. Хорошая метрика не гарантирует, что модель подходит, поэтому график «предсказание vs реальность» из предыдущего раздела и анализ ошибок так же важны, как сами числа.

Откуда sklearn берёт «лучшие» веса

Минимум MSE для линейной модели находится не перебором, а точной формулой (метод наименьших квадратов). Если собрать признаки в матрицу XX (с колонкой единиц под w0w_0), то оптимальные веса:

w=(XX)1Xyw = (X^\top X)^{-1} X^\top y

LinearRegression считает именно эту формулу под капотом, поэтому обучение мгновенное и всегда даёт один и тот же ответ. Для очень больших данных обращать матрицу XXX^\top X дорого, и вместо точной формулы используют градиентный спуск — шаговый спуск по «чаше ошибки» из этого урока. Ему посвящён следующий урок.

Итог урока

  1. Линейная регрессия — модель вида a(x)=w0+w1x1++wdxda(x) = w_0 + w_1 x_1 + \dots + w_d x_d.
  2. «Обучить» её — значит подобрать веса ww так, чтобы модель как можно лучше описывала данные.
  3. «Как можно лучше» формализуется через MSE, а обучение — это поиск дна чаши ошибки.
  4. В sklearn обучение — это две строки: LinearRegression() и .fit(X, y); предсказание — model.predict(X_new).
  5. Качество проверяем на тесте, а не на обучающих данных — иначе модель «жульничает».
  6. Основные метрики регрессии — MAE, MSE, RMSE, R².
  7. Одной метрике верить нельзя: смотри на данные и на график ошибок (урок квартета Анскомба).

В следующих уроках разберём, что делать, когда линейной модели не хватает, и как именно модель «находит» дно чаши, когда точной формулы нет.


Домашнее задание

Задания идут от простых к сложным. 🧠 — теория, 💻 — код, 🔮 — «что будет, если…». Сначала попробуй сам, потом открой разбор.

  1. 🧠 Дана модель a(x)=3+2x10.5x2a(x) = 3 + 2x_1 - 0.5 x_2. Посчитай вручную предсказание для x1=4, x2=10x_1 = 4,\ x_2 = 10. Как интерпретировать коэффициент 0.5-0.5?
Разбор

a=3+240.510=3+85=6a = 3 + 2\cdot4 - 0.5\cdot10 = 3 + 8 - 5 = 6.

Коэффициент 0.5-0.5 означает: при росте признака x2x_2 на 1 (а x1x_1 неизменном) предсказание падает на 0.5. Знак «минус» — признак работает «в минус» для ответа.

  1. 🧠 Кто-то предложил модель a(x)=0a(x) = 0 (всегда ноль) для задачи про цены. Чему примерно равны её MSE и R2R^2? Почему модель плохая, хотя формально линейная?
Разбор

MSE будет большой — это среднее от yi2y_i^2 (все ответы около 6 млн, значит MSE ≈ 6² ≈ 36). R2R^2 окажется сильно отрицательным: «тупая» модель-среднее предсказывала бы yˉ6\bar y \approx 6 и ошибалась бы куда меньше, чем нулевая. Модель плохая, потому что игнорирует признак полностью: линейность — необходимое, но не достаточное условие полезности.

  1. 💻 Создай свой датасет из 10–15 точек (например, «часы сна → производительность»), сохрани в DataFrame, обучи LinearRegression, выведи коэффициенты и постройте график с точками и прямой.
Разбор
import numpy as np, pandas as pd, matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

sleep = np.array([4,5,5.5,6,6.5,7,7.5,8,8.5,9])
score = np.array([40,52,55,63,70,74,80,85,88,90])
m = LinearRegression().fit(sleep.reshape(-1,1), score)
print(m.intercept_, m.coef_[0])

plt.scatter(sleep, score)
grid = np.linspace(4, 9, 50).reshape(-1, 1)
plt.plot(grid, m.predict(grid))
plt.xlabel("часы сна"); plt.ylabel("производительность"); plt.show()

Коэффициент при сне положительный — больше сна, выше балл (в пределах данных).

  1. 💻 Возьми модель из задания 3, выбери три новых значения признака и получи предсказания. Одно из значений возьми за пределами диапазона данных — что с ним не так?
Разбор

m.predict([[5.0],[7.0],[12.0]]). Значение 12 часов — экстраполяция далеко за данные: модель механически продолжит прямую и предскажет нереалистично высокий балл. На практике связь «сон → продуктивность» не линейна (избыток сна не помогает), поэтому такому прогнозу доверять нельзя.

  1. 🧠 Модель зарплаты получила коэффициенты: стаж +8.5, возраст +0.3, часы сна +2.1, часы в соцсетях −3.7. Какой признак сильнее всего повышает прогноз? Какой понижает? Значит ли маленький коэффициент у возраста, что он не важен?
Разбор

Сильнее всего повышает прогноз стаж (+8.5), а понижает — соцсети (−3.7). Маленький коэффициент у возраста не обязательно значит «не важен»: величина коэффициента зависит и от масштаба признака (возраст в годах меняется в широком диапазоне), и от того, не дублирует ли его другой признак (например, стаж). Сравнивать «важность» по голым коэффициентам можно только после масштабирования признаков — об этом будет отдельный урок.

  1. 💻 Для y_true = [10, 12, 15, 18, 20] и y_pred = [11, 11, 17, 17, 22] посчитай MAE, MSE, RMSE вручную, затем сверь с sklearn.metrics.
Разбор

Ошибки: 1, −1, 2, −1, 2. |ошибки|: 1,1,2,1,2 → MAE = 7/5 = 1.40. Квадраты: 1,1,4,1,4 → MSE = 11/5 = 2.20. RMSE = √2.2 ≈ 1.483.

from sklearn.metrics import mean_absolute_error, mean_squared_error
import numpy as np
yt=[10,12,15,18,20]; yp=[11,11,17,17,22]
print(mean_absolute_error(yt,yp), mean_squared_error(yt,yp), np.sqrt(mean_squared_error(yt,yp)))
# → 1.4 2.2 1.4832...
  1. 🔮 Сгенерируй 100 точек y=3+2x+шумy = 3 + 2x + \text{шум} с σ=0.5\sigma = 0.5, обучи модель и запиши MSE. Повтори с σ=5.0\sigma = 5.0. Как изменились MSE и коэффициенты? Почему?
Разбор

С ростом шума MSE заметно растёт (ошибка неустранима — это сам шум), а коэффициенты остаются около истинных w0=3, w1=2w_0=3,\ w_1=2, но «гуляют» сильнее от запуска к запуску. Вывод: шум ограничивает достижимое качество снизу — идеального R2=1R^2=1 на шумных данных не бывает.

  1. 🔮 Обучи модель и сравни MSE на train и на test. Какая обычно больше? Ожидаемо ли это?
Разбор

Обычно MSE на test чуть больше, чем на train: модель подгонялась под train, поэтому там ей «удобнее». Небольшой разрыв — норма. Большой разрыв (train хорошо, test плохо) — признак переобучения.

  1. 🔮 Что будет с коэффициентом w1w_1, если мерить площадь не в м², а в см² (1 м² = 10000 см²)? Сначала подумай, потом проверь на коде. Изменится ли качество (MSE, R²)?
Разбор

w1w_1 уменьшится ровно в 10000 раз (цена «за см²» во столько же раз меньше цены «за м²»), а w0w_0 не изменится. Качество (MSE, R²) не изменится вообще: линейное преобразование признака модель компенсирует коэффициентом. Это показывает, что величина коэффициента сама по себе мало что говорит без учёта единиц измерения.

  1. 🧠💻 На датасете из мини-кейса обучи три модели: только по area, только по distance, по обоим сразу. Сравни R2R^2 на тесте. Может ли добавление признака ухудшить качество на тесте?
Разбор

Да, может. На train добавление признака почти никогда не ухудшает (у модели больше свободы), но на test лишний или шумный признак может слегка ухудшить R2R^2 — модель начинает «ловить» случайные закономерности. Это первый намёк на то, зачем нужны отбор признаков и регуляризация (отдельные уроки впереди).

Нужен разбор?

Застрял на теме — разберём один на один

Объясню сложное на пальцах и доведу до результата: код, формулы, проект.

Записаться на разбор