В прошлом уроке мы разобрались, что такое машинное обучение в целом: есть объекты, есть ответы, и мы хотим научиться предсказывать ответы для новых объектов. Сегодня берём самую простую и самую важную модель — линейную регрессию. Это фундамент: поймёшь её по-настоящему, и половина остального курса станет очевидной, потому что нейросети, бустинг и даже трансформеры устроены вокруг той же идеи «подбираем числа, чтобы ошибка стала меньше».
Урок длинный и неспешный. Мы не просто запустим 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()
Точки лежат не на одной линии, но видна закономерность: чем больше площадь, тем выше цена. Это и есть сигнал, что задачу можно решать — между признаком и ответом есть связь.
Теперь пришёл новый клиент: у него квартира 50 м², сколько она стоит? В данных именно такой нет — есть 48 и 52, но не 50. Значит, нужна функция, которая по любой площади выдаёт предсказанную цену:
Эту функцию принято называть алгоритмом или моделью. Наша задача — выбрать её так, чтобы предсказания были близки к правде.
Идея: почему именно прямая?
Точки лежат почти на одной линии. Самая простая функция, описывающая такой тренд, — это прямая:
Здесь:
- — наклон прямой: на сколько растёт цена при увеличении площади на 1 м²;
- — сдвиг (или свободный член): значение прямой при .
Числа называются весами (или коэффициентами). Именно их модель будет подбирать по данным. Пока их не подобрали, прямую можно провести как угодно: круто или полого, выше или ниже. Можно нарисовать несколько разных прямых и «на глаз» увидеть, что одни проходят ближе к точкам, другие дальше. Но «на глаз» — это не способ. Нужна формула, которая численно скажет, насколько прямая хорошая.
Как измерить качество прямой — MSE
Для каждой точки посчитаем ошибку — на сколько предсказание отличается от реального значения:
Эта ошибка бывает положительной (модель завысила) и отрицательной (занизила). Если просто сложить их, плюсы и минусы погасят друг друга, и даже плохая прямая может дать «ноль в среднем». Чтобы получить честное одно число для всей прямой, делаем три шага:
- берём ошибку на каждом объекте ;
- возводим в квадрат — так минусы исчезают, а большие промахи штрафуются особенно сильно (промах в 2 раза даёт штраф в 4 раза);
- складываем и делим на количество объектов, чтобы получить среднее.
Получается MSE (mean squared error, среднеквадратичная ошибка):
Здесь — число объектов (сколько у нас точек), а знак означает «сложить по всем объектам с до ». Чем меньше MSE, тем лучше прямая. Теперь задача обучения формулируется чётко:
Найти такие и , при которых MSE минимальна.
В коде MSE — это буквально перевод формулы на Python:
def mse(y_true, y_pred):
return np.mean((y_pred - y_true) ** 2)
Давай посмотрим на «хорошую» прямую, ту, что подобрал алгоритм, и на её ошибки. Каждый вертикальный пунктир от точки до прямой — это и есть одна ошибка , которую мы возводим в квадрат:
Обрати внимание: прямая на этом графике не нарисована от руки — её коэффициенты вычислены по данным (как именно, расскажу через секунду). Поэтому она действительно проходит «по центру» облака, а не приблизительно.
Где живёт «лучшая» прямая
Зафиксируем на секунду сдвиг и будем менять только наклон . Для каждого наклона можно посчитать 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()
Вот что значит «модель учится»: она ищет дно чаши ошибки. Для линейной регрессии чаша всегда гладкая и с одним минимумом, поэтому дно находится надёжно. (В нейросетях рельеф будет куда более изрезанным — но об этом в следующем уроке про градиентный спуск.) К счастью, перебирать веса вручную не нужно: дно уже умеет находить 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 нашёл те самые и , при которых MSE минимальна — то самое дно чаши с прошлого графика. Никакого перебора: для линейной регрессии есть точная формула (покажу её в конце урока в блоке со звёздочкой).
Как читать коэффициенты
- — «базовая» цена (значение модели при нулевой площади). Физического смысла у квартиры в 0 м² нет, но математически сдвиг нужен, чтобы прямая не была обязана проходить через начало координат.
- — на сколько вырастет предсказание, если площадь увеличить на 1 м²: примерно на 0.11 млн ₽ (110 тыс. ₽) за каждый дополнительный метр. Это главное число, которое нужно уметь интерпретировать.
Предсказание для новых объектов
Модель обучена — теперь можно спросить её про любую площадь, даже про ту, которой нет в данных.
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 м². Это экстраполяция — модель продолжает прямую за пределы виденного. Иногда это нормально, иногда опасно: реальность не обязана оставаться линейной (например, сверхбольшие квартиры могут дорожать медленнее). Правило: чем дальше от диапазона обучающих данных, тем меньше доверия предсказанию.
Несколько признаков
В жизни цена зависит не только от площади. Пусть добавится расстояние до центра. Теперь у каждой квартиры два признака, и формула становится:
Вместо прямой модель задаёт плоскость. Представь: по одной оси — площадь, по другой — расстояние до центра, а высота показывает цену. Точки висят в воздухе облаком, и модель натягивает на них наклонный лист — это и есть плоскость. Покрути сцену ниже мышью, чтобы увидеть это с разных сторон:
В коде меняется только форма 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 — слегка отрицательный. Но он крошечный, почти ноль. Почему? В наших данных расстояние и площадь сильно связаны (большие квартиры оказались ближе к центру), поэтому площадь уже «объясняет» почти всё, и второму признаку добавить нечего. Это важный урок: маленький коэффициент не всегда значит, что признак не важен — иногда он просто дублирует информацию другого. Если признаков три, четыре или тысяча — принцип ровно тот же, просто нарисовать уже нельзя:
Train / test: честная проверка
Мы хотим понять, насколько хороша модель. Но есть ловушка: если измерять ошибку на тех же данных, на которых обучались, модель «жульничает» — она их уже видела. Это как проверять ученика на задачах, которые он заранее заучил наизусть: оценка будет завышенной.
Решение — разделить данные на две части:
- train (70–80%) — на этих данных модель учится;
- test (20–30%) — эти данные модель не видела при обучении, по ним честно меряем качество.
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 (средняя абсолютная ошибка) — «в среднем модель ошибается на столько-то», в тех же единицах, что и ответ:
Например: модель сказала 5.0, а правда 5.6 — промах 0.6; усреднили такие промахи по всем объектам и получили, скажем, MAE = 0.36 — «в среднем мажем на 0.36 млн ₽».
MSE — уже знакома, сильнее штрафует большие промахи, но измеряется в «квадратных единицах» (млн ₽ в квадрате, что неудобно интерпретировать).
RMSE — корень из MSE, возвращает нас к исходным единицам:
R² (коэффициент детерминации) — насколько модель лучше «тупой» модели, которая всегда предсказывает среднее:
Здесь — среднее значение ответа. В дроби числитель — суммарная квадратичная ошибка нашей модели, а знаменатель отражает ошибку «тупой» модели, которая всегда предсказывает среднее . Если наша ошибка больше, чем у «тупой», дробь становится больше 1 — тогда уходит в минус. Отсюда три ориентира:
- — идеальное предсказание;
- — модель не лучше, чем «всегда среднее»;
- — модель хуже среднего (так тоже бывает, и это тревожный знак).
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
— модель объясняет 97% разброса цен. Отлично, но это и неудивительно: данные мы сами сгенерировали почти линейными. На реальных данных так красиво бывает редко.
Какую метрику выбирать? Если все ошибки одинаково важны — MAE. Если большие промахи особенно нежелательны — RMSE. Чтобы понять общее качество одним числом «от 0 до 1» — R².
Полезнее любой одной цифры — график «предсказание против реальности». По горизонтали кладём настоящую цену, по вертикали — предсказанную. Идеальная модель легла бы точно на диагональ :
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 году статистик Фрэнсис Анскомб придумал четыре набора данных, у которых совпадают почти все числа: среднее, дисперсия, коэффициент корреляции и даже уравнение прямой регрессии ( во всех четырёх). Если смотреть только на метрики — наборы неотличимы. А вот если их нарисовать:
- I — честная линейная зависимость, регрессия уместна.
- II — данные лежат на кривой (парабола); прямая здесь в корне неверна, хотя метрики «хорошие».
- III — почти идеальная прямая, но один выброс тянет линию вверх.
- IV — почти все точки на одной вертикали, а наклон задаёт единственная точка-«рычаг» справа.
Мораль: всегда смотри на данные глазами, а не только на . Хорошая метрика не гарантирует, что модель подходит, поэтому график «предсказание vs реальность» из предыдущего раздела и анализ ошибок так же важны, как сами числа.
Откуда sklearn берёт «лучшие» веса
Минимум MSE для линейной модели находится не перебором, а точной формулой (метод наименьших квадратов). Если собрать признаки в матрицу (с колонкой единиц под ), то оптимальные веса:
LinearRegression считает именно эту формулу под капотом, поэтому обучение мгновенное и всегда даёт один и тот же ответ. Для очень больших данных обращать матрицу дорого, и вместо точной формулы используют градиентный спуск — шаговый спуск по «чаше ошибки» из этого урока. Ему посвящён следующий урок.
Итог урока
- Линейная регрессия — модель вида .
- «Обучить» её — значит подобрать веса так, чтобы модель как можно лучше описывала данные.
- «Как можно лучше» формализуется через MSE, а обучение — это поиск дна чаши ошибки.
- В
sklearnобучение — это две строки:LinearRegression()и.fit(X, y); предсказание —model.predict(X_new). - Качество проверяем на тесте, а не на обучающих данных — иначе модель «жульничает».
- Основные метрики регрессии — MAE, MSE, RMSE, R².
- Одной метрике верить нельзя: смотри на данные и на график ошибок (урок квартета Анскомба).
В следующих уроках разберём, что делать, когда линейной модели не хватает, и как именно модель «находит» дно чаши, когда точной формулы нет.
Домашнее задание
Задания идут от простых к сложным. 🧠 — теория, 💻 — код, 🔮 — «что будет, если…». Сначала попробуй сам, потом открой разбор.
- 🧠 Дана модель . Посчитай вручную предсказание для . Как интерпретировать коэффициент ?
Разбор
.
Коэффициент означает: при росте признака на 1 (а неизменном) предсказание падает на 0.5. Знак «минус» — признак работает «в минус» для ответа.
- 🧠 Кто-то предложил модель (всегда ноль) для задачи про цены. Чему примерно равны её MSE и ? Почему модель плохая, хотя формально линейная?
Разбор
MSE будет большой — это среднее от (все ответы около 6 млн, значит MSE ≈ 6² ≈ 36). окажется сильно отрицательным: «тупая» модель-среднее предсказывала бы и ошибалась бы куда меньше, чем нулевая. Модель плохая, потому что игнорирует признак полностью: линейность — необходимое, но не достаточное условие полезности.
- 💻 Создай свой датасет из 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()Коэффициент при сне положительный — больше сна, выше балл (в пределах данных).
- 💻 Возьми модель из задания 3, выбери три новых значения признака и получи предсказания. Одно из значений возьми за пределами диапазона данных — что с ним не так?
Разбор
m.predict([[5.0],[7.0],[12.0]]). Значение 12 часов — экстраполяция далеко за данные: модель механически продолжит прямую и предскажет нереалистично высокий балл. На практике связь «сон → продуктивность» не линейна (избыток сна не помогает), поэтому такому прогнозу доверять нельзя.
- 🧠 Модель зарплаты получила коэффициенты: стаж +8.5, возраст +0.3, часы сна +2.1, часы в соцсетях −3.7. Какой признак сильнее всего повышает прогноз? Какой понижает? Значит ли маленький коэффициент у возраста, что он не важен?
Разбор
Сильнее всего повышает прогноз стаж (+8.5), а понижает — соцсети (−3.7). Маленький коэффициент у возраста не обязательно значит «не важен»: величина коэффициента зависит и от масштаба признака (возраст в годах меняется в широком диапазоне), и от того, не дублирует ли его другой признак (например, стаж). Сравнивать «важность» по голым коэффициентам можно только после масштабирования признаков — об этом будет отдельный урок.
- 💻 Для
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...- 🔮 Сгенерируй 100 точек с , обучи модель и запиши MSE. Повтори с . Как изменились MSE и коэффициенты? Почему?
Разбор
С ростом шума MSE заметно растёт (ошибка неустранима — это сам шум), а коэффициенты остаются около истинных , но «гуляют» сильнее от запуска к запуску. Вывод: шум ограничивает достижимое качество снизу — идеального на шумных данных не бывает.
- 🔮 Обучи модель и сравни MSE на train и на test. Какая обычно больше? Ожидаемо ли это?
Разбор
Обычно MSE на test чуть больше, чем на train: модель подгонялась под train, поэтому там ей «удобнее». Небольшой разрыв — норма. Большой разрыв (train хорошо, test плохо) — признак переобучения.
- 🔮 Что будет с коэффициентом , если мерить площадь не в м², а в см² (1 м² = 10000 см²)? Сначала подумай, потом проверь на коде. Изменится ли качество (MSE, R²)?
Разбор
уменьшится ровно в 10000 раз (цена «за см²» во столько же раз меньше цены «за м²»), а не изменится. Качество (MSE, R²) не изменится вообще: линейное преобразование признака модель компенсирует коэффициентом. Это показывает, что величина коэффициента сама по себе мало что говорит без учёта единиц измерения.
- 🧠💻 На датасете из мини-кейса обучи три модели: только по
area, только поdistance, по обоим сразу. Сравни на тесте. Может ли добавление признака ухудшить качество на тесте?
Разбор
Да, может. На train добавление признака почти никогда не ухудшает (у модели больше свободы), но на test лишний или шумный признак может слегка ухудшить — модель начинает «ловить» случайные закономерности. Это первый намёк на то, зачем нужны отбор признаков и регуляризация (отдельные уроки впереди).