Files
ML/task13/[Base]_Linear_Models_regression_[2025_2026].ipynb
2025-11-12 11:34:34 +03:00

328 KiB
Raw Permalink Blame History

Машинное обучение. ВМК МГУ

Практическое задание 5: Линейные модели: регрессия

Уровень: Базовый (Base)

О формате сдачи

🔷 При решении ноутбука используйте данный шаблон

✅ Можно добавлять новые ячейки любых типов
❌ Не нужно удалять текстовые ячейки c разметкой частей ноутбука и формулировками заданий

🔷 При оценивании задач учитывается код

✅ Задания, в которых необходим код, обычно помечаются фразами "Your code here"/"Ваш код" и аналогичными
❌ Ответы на вопросы без сопутствующего кода оцениваются в 0 баллов
❌ Наличе работоспособного кода в ноутбуке, если на сказано иного, обязательно

🔷 При оценивании задач учитываются выводы

✅ Задания, в которых необходимы выводы, обычно помечаются фразами Вывод"/"Ответ на вопрос"/"Ваш текст" и аналогичными
✅ Обычно выводы подразумевают под собой текстовый ответ (можно писать markdown, latex).
✅ Сопутствующие изображения, графики, таблички - приветствуются!
❌ При отсутствии выводов задание не засчитается на полный балл

Цель данного задания:

  • Узнать, что такое регуляризация, зачем она нужна, и чем отличаются разные регуляризаторы.
  • Научиться решать задачу регрессии линейными моделями.

Примерное время выполнения (execution time/время выполнения, если нажать run all) всех ячеек ноутбука при правильной реализации: 5 минут

Подготовка рабочей среды

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

После установки нужных версий, возможно, нужно перезагрузить среду (runtime), но скорее всего вам это не понадобится

На скачивание файла и установку понадобится не более 5 минут.

Важно!

Устанавливать нужные версии нужно каждый раз, когда создается новый рантайм. Например, если вы 2 часа подряд делаете это задание, то подготовить библиотеки достаточно 1 раз. Но если вы, например, начали в понедельник, затем закрыли/выключили ноутбук, то при продолжении в среду, вам нужно будет запустить рантайм заново и следовательно заново установить библиотеки.

Важно! Если вы предпочитаете делать практические задания на своем личном ноутбуке, то проверьте, что вы установили рабочее окружение в соответствии с гайдом

In [ ]:
! curl https://raw.githubusercontent.com/MSU-ML-COURSE/ML-COURSE-25-26/refs/heads/master/requirements/requirements.txt -o ./requirements_2025_26_for_colab_small.txt
! pip install -q -r ./requirements_2025_26_for_colab_small.txt
[notice] A new release of pip is available: 25.2 -> 25.3
[notice] To update, run: pip install --upgrade pip

Проверим версию библиотеки:

In [3]:
import catboost
assert(catboost.__version__ == '1.2.8')

Теперь можно приступать к выполнению задания! :)


In [31]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.simplefilter("ignore")
sns.set(style="darkgrid")
%matplotlib inline

Линейная регрессия и регуляризация

Напомним, что линейная регрессия — это модель следующего вида: a(x) = \langle w, x \rangle + b где w \in \mathbb{R}^d, b \in \mathbb{R}. Обучить линейную регрессию — значит найти w и b.

Для обучения линейной регрессии, равно как и для обучения остальных простых моделей (линейные модели, решающие деревья, knn и т.д.) отлично подходит библиотека scikit-learn (sklearn): в ней очень понятный и простой интерфейс.

Однако для обучения более сложных моделей вроде бустинга и нейронных сетей всё же нужно пользоваться специализированными библиотеками: XGBoost, LightGBM, CatBoost и пр. для градиентного бустинга над деревьями, PyTorch, Tensorflow и пр. для нейронных сетей.


Напомним, что линейная регрессия — это модель вида

a(x) = \langle w, x \rangle + b где w \in \mathbb{R}^d, b \in \mathbb{R}. Для обучения параметров w решается оптимизационная задача следующего вида:

\frac{1}{M} ∑_{i=1}^M (w_1 \cdot x_{i1} + \dots + w_n \cdot x_{in} + b - y_i)^2 + \alpha \cdot R(w) \rightarrow \min_{w_1, \dots, w_n, b}

Здесь R(w) — это регуляризация параметров модели, \alpha — коэффициент регуляризации, задаваемый перед началом обучения.

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

  • LinearRegression — линейная регрессия без регуляризации R(w) = 0 (метод наименьших квадратов)
  • Ridge — линейная регрессия с оптимизацией MSE и $\ell_2$-регуляризацией R(w) = \frac{1}{2} \cdot \left( w_1^2 + \dots + w_n^2 \right)
  • Lasso — линейная регрессия с оптимизацией MSE и $\ell_1$-регуляризацией R(w) = |w_1| + \dots + |w_n|

Также есть SVR, ElasticNet и пр., но не будем сегодня о них

У моделей из sklearn есть методы fit и predict. Первый принимает на вход обучающую выборку и вектор целевых переменных и обучает модель, второй, будучи вызванным после обучения модели, возвращает предсказание на выборке.


Рассмотрим, модельные данные для задачи регрессии. Пусть x будет обычным числом из равномерного распределения, а y = 0.5 \cdot x + 0.1 — целевая переменная. При этом наблюдаем мы \overline{y} = y + \varepsilon,~\varepsilon \sim N(0, 0.01).

In [32]:
np.random.seed(1)
X = np.random.uniform(0, 1, 100)
Y = X * 0.5 + 0.1 + np.random.randn(100) * 0.1

plt.figure(figsize=(8, 8))
plt.scatter(X, Y)
plt.title("Обучающая выборка зависимости y от x", size=15)
plt.xlabel("x", size=15)
plt.ylabel(r'$\overline{y}$', size=15)
plt.show()

Обучим линейную регрессию с l_2 регуляризацией, и посмотрим как регуляризация влияет на качество модели. В реализации библиотеки sklearn (класс Ridge) коэффициент регуляризации задаётся параметром alpha

In [33]:
from sklearn.linear_model import Ridge
In [34]:
x_axis = np.linspace(0, 1, 200)
fig, axs = plt.subplots(figsize=(14, 7), ncols=2)
axs[0].scatter(X, Y)

w = []
b = []

alphas = [0.0, 0.1, 1.0, 10.0, 100.0, 1000.0]

for alpha in alphas:
    reg = Ridge(alpha=alpha) # Задаем параметр alpha
    reg.fit(X[:, None], Y)
    pred = reg.predict(x_axis[:, None])
    w.append(reg.coef_[0])
    b.append(reg.intercept_)
    axs[0].plot(x_axis, pred, label="alpha=" + str(alpha))

axs[0].legend()
axs[0].set_xlabel("x", size=15)
axs[0].set_ylabel("y", size=15)
axs[0].set_title("Ridge регрессия с разными коэффициентами регуляризации")
axs[1].plot(alphas, w, label="w")
axs[1].plot(alphas, b, label="b")
axs[1].set_xlabel("alpha", size=15)
axs[1].set_ylabel("Значение параметров", size=15)
axs[1].set_title("Значение параметров w и b при разных значениях регуляризации")
axs[1].set_xscale("symlog", linthresh=0.01)
axs[1].legend()
plt.show()

Задание 1 [2 баллa]

Как зависят параметры модели от константы регуляризации? А качество?

Ваши выводы тут:

Зависимость параметров:

  • С увеличением константы веса модели начинают уменьшаться. При очень больших значениях они становятся практически нулевыми
  • При больших значениях alpha практически весь вклад вносится за счёт коэффициента смещения
  • С увеличением значений alpha возникает высокий риск недообучения

Качество модели:

  • При значительном увеличении alpha качество модели сильно падает

Казалось бы, зачем нам регуляризация?

Давайте рассмотрим ещё один модельный пример. Представим, что кто-то добавил в данные к переменной x её же умноженную на 2. То есть, теперь у нас два признака x_1 и x_2 = 2 \cdot x_1. Тогда, y = c \cdot 0.5 \cdot x_1 + \frac{1 - c}{4} \cdot x_2 + 0.1, где c любое сколь угодно большое вещественное число. Это может привести к тому, что без регуляризации мы рискуем выучить очень большие веса!

In [35]:
np.random.seed(1)
X2 = np.hstack((X[:, None], 2 * X[:, None]))
Y2 = X2[:, 0] * 0.5 + 0.1 + np.random.randn(100) * 0.1

reg = Ridge(alpha=0.0)
reg.fit(X2, Y2)
print("w1:", reg.coef_[0], "\tw2:", reg.coef_[1])
w1: 0.003110369132959204 	w2: 0.24999999999999994

Коэффициенты адекватные, хотя и не похожи на изначальную зависимость. Но что, если x_2 будет равняться 3 \cdot x_1?

In [36]:
np.random.seed(1)
X3 = np.hstack((X[:, None], 3 * X[:, None]))
Y3 = X3[:, 0] * 0.5 + 0.1 + np.random.randn(100) * 0.1

reg = Ridge(alpha=0.0)
reg.fit(X3, Y3)
print("w1:", reg.coef_[0], "\tw2:", reg.coef_[1])
w1: 9734611262837.348 	w2: -3244870420945.615

Тут вот уже не повезло. Коэффициенты случайно выучились неадекватно большими.

Создадим обучающую выборку из того же распределения и посмотрим на качество:

In [37]:
np.random.seed(2)
X3_test = np.random.uniform(0, 1, 100)
X3_test = np.hstack((X3_test[:, None], 3 * X3_test[:, None]))
Y3_test = X3_test[:, 0] * 0.5 + 0.1 + np.random.randn(100) * 0.1

Y3_test_pred = np.sum(reg.coef_[None] * X3_test + reg.intercept_, axis=1)
print("MSE loss: %.4f" % np.mean((Y3_test_pred - Y3_test) ** 2))
MSE loss: 0.0219

Вроде бы неплохое, но что если мы добавим ко второму признаку одного из объектов небольшой шум?

In [38]:
X3_test[0, 1] = X3_test[0, 1] + 1e-10
Y3_test_pred_noisy = np.sum(reg.coef_[None] * X3_test + reg.intercept_, axis=1)
print("MSE loss:", np.mean((Y3_test_pred_noisy - Y3_test) ** 2))
print("Предсказание для первого с шумом: ", Y3_test_pred_noisy[0])
print("Предсказание для первого без шума: ", Y3_test_pred[0])
MSE loss: 1051.9490520394384
Предсказание для первого с шумом:  -324.0625
Предсказание для первого без шума:  0.4248046875

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

Задание 2 [2 баллa]

Рассмотрите больше примеров (хотя бы два) с двумя признаками x_1 и x_2, где x_2 линейно зависит от x_1. Убедитесь, что линейная модель без регуляризации крайне неустойчива.

In [39]:
X_4 = np.random.uniform(0, 1, 100)
X_4 = np.hstack((X_4[:, None], 15 * X_4[:, None]))
Y_4 = X_4[:, 0] * 0.5 + 0.1 + np.random.randn(100) * 0.1

reg = Ridge(alpha=0.0)
reg.fit(X_4, Y_4)
print("w1:", reg.coef_[0], "\nw2:", reg.coef_[1])
print("-----")

X_5 = np.random.uniform(0, 1, 100)
X_5 = np.hstack((X_5[:, None], -19 / 13 * X_5[:, None]))
Y_5 = X_5[:, 0] * 0.5 + 0.1 + np.random.randn(100) * 0.1

reg = Ridge(alpha=0.0)
reg.fit(X_5, Y_5)
print("w1:", reg.coef_[0], "\nw2:", reg.coef_[1])
w1: 1696626244189.3145 
w2: -113108416279.25912
-----
w1: -0.24676691369166734 
w2: -0.5

Масштабирование данных

Попробуем обучить линейную регрессию уже c l_1 регуляризацией (Lasso) на специальном датасете из sklearn

In [40]:
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

X, y = fetch_california_housing(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2024)

Взглянем немножко на данные. Выведем средние значения каждого признака

In [41]:
with np.printoptions(formatter={'float': '{: 0.3f}'.format}):
  print(X_train.mean(axis=0))
[ 3.868  28.712  5.411  1.093  1416.185  3.109  35.637 -119.583]

Нетрудно видеть, что масштаб у разных признаков сильно отличается. Это может приводить к разным неприятным эффектам. Подробнее эту проблему мы разберём в следующем задании.

In [42]:
from sklearn.preprocessing import StandardScaler

Задание 3 [1 балл]

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

Примечание Результат положите в переменные X_train_scaled и X_test_scaled , чтобы последующий код был рабочим

In [43]:
scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print(f"X_train_scaled:\nСреднее значение = {X_train_scaled.mean()}\nдисперсия = {X_train_scaled.var()}\n")
print(f"X_train_scaled:\nСреднее значение = {X_test_scaled.mean()}\nдисперсия = {X_test_scaled.var()}")
X_train_scaled:
Среднее значение = -1.5276078667068268e-14
дисперсия = 1.0000000000000033

X_train_scaled:
Среднее значение = 0.009713241824084506
дисперсия = 1.2685666097336883

Измерим качество прогнозатора. Будем использовать метрику RMSE.

In [44]:
from sklearn.metrics import mean_squared_error, root_mean_squared_error
from sklearn.linear_model import Lasso
In [45]:
reg = Lasso(alpha=0.5)
reg.fit(X_train_scaled, y_train)
y_pred = reg.predict(X_test_scaled)
print("Test RMSE = %.4f" % root_mean_squared_error(y_test, y_pred))
Test RMSE = 0.9798

Задание 4 [2 баллa]

В чем плюсы RMSE по сравнению с MSE?

Ваши выводы тут:

Плюсы:

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

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

In [46]:
reg.coef_
Out [46]:
array([ 0.28811554,  0.        ,  0.        , -0.        , -0.        ,
       -0.        , -0.        , -0.        ])

А теперь обучим с l_2 регуляризацией.

In [47]:
reg = Ridge(alpha=0.5)
reg.fit(X_train_scaled, y_train)
print(reg.coef_)
[ 8.50854581e-01  1.25545440e-01 -2.78892640e-01  3.08812622e-01
 -1.99686054e-04 -4.12942247e-02 -8.88296917e-01 -8.60046905e-01]

Как видим, веса не разрежены, хотя и есть очень маленькие значения


Подбор гиперпараметра при регуляризации

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

In [48]:
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline

Воспользуемся классом GridSearch для перебора параметров по сетке.

  • Для линейных регрессий перебирается параметр \alpha - сила регуляризации. Обычно важнее перебирать порядок этого параметра, а не точное его значение. В силу этого сетку перебора будет удобно сделать через функцию np.logspace, например np.logspace(-3, 3, 10).

Задание 5 [3 баллa]

Воспользуйтесь классом GridSearch и подберите константы регуляризации для Lasso и Ridge регрессий. Измерьте качество обученных моделей. Сетка перебора должна быть логарифмической, из хотя бы 10 значений

Подсказка

  • Пример, как можно перебирать параметры в GridSearch у вложенных Pipeline можно найти вот тут
  • Обратите внимание, что сейчас мы сразу заносим масштабирование в Pipeline - чтобы иметь возможность сразу вызываться от оригинальных X_train, а также чтобы не было утечки данных при использовании GridSearch
  • В GridSearch в качестве скоринговой функции можно подавать строковое описание функции из sklearn, которое можно посмотреть вот тут, а также саму скоринговую функцию из sklearn или собственную функцию, сделанную через make_scorer

Примечание Итоговое качество должно быть не больше 0.75 RMSE. За меньшее качество балл будет снижаться

In [50]:
model_lasso = Pipeline([
    ("scaler", StandardScaler()),
    ("regr", Lasso())
])

model_ridge = Pipeline([
    ("scaler", StandardScaler()),
    ("regr", Ridge())
])

parametrs = {
    'regr__alpha': list(np.logspace(-5, 6, 12)),
}

lasso_cv = GridSearchCV(
    model_lasso,
    cv=5,
    scoring="neg_root_mean_squared_error",
    param_grid = parametrs
)

ridge_cv = GridSearchCV(
    model_ridge,
    cv=5,
    scoring="neg_root_mean_squared_error",
    param_grid = parametrs
)

lasso_cv.fit(X_train, y_train)
ridge_cv.fit(X_train, y_train)
lasso_res = pd.DataFrame(lasso_cv.cv_results_)
ridge_res = pd.DataFrame(ridge_cv.cv_results_)

print(-lasso_res['mean_test_score'].sort_values(ascending=False).head(1))
print(-ridge_res['mean_test_score'].sort_values(ascending=False).head(1))
2    0.72237
Name: mean_test_score, dtype: float64
5    0.722382
Name: mean_test_score, dtype: float64

Убедимся, что Lasso всё ещё зануляет признаки (скорее всего модель Lasso занулила хотя бы один).

In [51]:
lasso_cv.best_estimator_.named_steps["regr"].coef_
Out [51]:
array([ 0.84604739,  0.12597833, -0.26816238,  0.29792527,  0.        ,
       -0.04027369, -0.87740579, -0.84850035])

Выводы В первой части задания по линейным моделям мы должны были узнать: .

  1. Зачем нужна регуляризация.
  2. Как отбирать значащие признаки.
  3. Кaк подбирать параметры линейной модели.

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