Files
ML/task13/Linear_Models_regression.ipynb
2025-11-12 11:34:34 +03:00

343 KiB
Raw Permalink Blame History

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Важно!

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

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

In [2]:
# !!! Данный блок будет работать только в Google-Colab !!!
! gdown 10k8Hwn9kpK9SpK4IEj4-EaWQZqgYT5-Q
! pip install -r /content/requirements_2024_25_for_colab_small.txt
Downloading...
From: https://drive.google.com/uc?id=10k8Hwn9kpK9SpK4IEj4-EaWQZqgYT5-Q
To: C:\Users\mozhu\PycharmProjects\ML_2024\Task5\Base\requirements_2024_25_for_colab_small.txt

  0%|          | 0.00/375 [00:00<?, ?B/s]
100%|##########| 375/375 [00:00<00:00, 249kB/s]
^C

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

In [3]:
import catboost

assert (catboost.__version__ == '1.2.7')

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


In [4]:
import numpy as np
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 [5]:
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 [6]:
from sklearn.linear_model import Ridge
In [7]:
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]

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

Ваши выводы тут: Параметры:

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

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

  1. При значительном увеличении 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 [8]:
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.10062207382659173 	w2: 0.20124414765318352

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

In [9]:
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: 20443435586638.3 	w2: -6814478528879.264

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

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

In [10]:
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.0197

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

In [11]:
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: 4641.811179703668
Предсказание для первого объекта с шумом:  -681.03515625
Предсказание для первого объекта без шума:  0.4140625

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

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

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

In [12]:
np.random.seed(2024)
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], "\tw2:", reg.coef_[1])

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], "\tw2:", reg.coef_[1])
w1: -78898433254335.61 	w2: 5259895550289.071
w1: 5865168356578.415 	w2: 4013009928184.904

Ответ: да, в случае линейно зависимости признаков, мы получаем весьма значительные по модулю веса


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

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

In [13]:
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 [14]:
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 [15]:
from sklearn.preprocessing import StandardScaler

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

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

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

In [16]:
scaler = StandardScaler()
# scaler.fit(X_train)
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()}, а дисперсия = {X_train_scaled.var()}")
print(f"Для X_train_scaled:\nСреднее значение = {X_test_scaled.mean()}, а дисперсия = {X_test_scaled.var()}")
Для X_train_scaled:
Среднее значение = -1.52766934084994e-14, а дисперсия = 1.0000000000000036
Для X_train_scaled:
Среднее значение = 0.009713241824084505, а дисперсия = 1.2685666097336878

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

In [17]:
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import Lasso
In [18]:
reg = Lasso(alpha=0.5)
reg.fit(X_train_scaled, y_train)
y_pred = reg.predict(X_test_scaled)
print("Test RMSE = %.4f" % mean_squared_error(y_test, y_pred, squared=False))
Test RMSE = 0.9798
In [19]:
print("Test MSE = %.4f" % mean_squared_error(y_test, y_pred))
Test MSE = 0.9601

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

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

Ваши выводы тут: Плюсы:

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

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

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

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

In [21]:
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 [22]:
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 [36]:
import pandas as pd

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

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

parametrs = {
    'regr__alpha': list(np.logspace(-5, 5, 100)),
}
lasso_cv = GridSearchCV(
    model_lasso,
    parametrs,
    cv=5,
    scoring="neg_root_mean_squared_error"
)
ridge_cv = GridSearchCV(
    model_ridge,
    parametrs,
    cv=5,
    scoring="neg_root_mean_squared_error"
)

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))
# Ваш код: о модели и измеряем качество на тесте
# Можно вызывать predict прямо от обученных lasso_cv и ridge_cv
18    0.722366
Name: mean_test_score, dtype: float64
56    0.722381
Name: mean_test_score, dtype: float64

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

In [37]:
lasso_cv.best_estimator_.named_steps["regr"].coef_
Out [37]:
array([ 0.84770117,  0.12584016, -0.27185324,  0.30168796, -0.        ,
       -0.04062777, -0.88125938, -0.85258618])

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

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

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