-- coding: utf-8 --

"""Копия блокнота "Regularization (HW).ipynb"

Automatically generated by Colab.

Original file is located at
https://colab.research.google.com/drive/16thpe5RaQJCbOXJJr1G-7r7yuLEvRljP
"""

import numpy as np
from sklearn.datasets import make_regression

"""# Регуляризация

Абсолютно для всех алгоритмов машинного обучения регуляризация делает одно и то же — борется со сложностью модели. А чересчур сложная модель – это прямой путь к переобучению. Но в разных алгоритмах сложность может проявляться по-разному. В линейных моделях сложность проявляется в больших значениях весов.

Рассмотрим такой пример:

1x0+10000x1+2x2=y

Очевидно, что вес θ1 оказывает очень сильное влияние на конечный результат, что делает модель нестабильной. И задача регуляризации в линейных моделях – бороться с такими аномально большими значениями весов, уменьшая их. Достигается это путем штрафования функции потерь за большие веса. Рассмотрим три вида регуляризации в линейных моделях.

L1 регуляризация (или Lasso регрессия)

В L1 мы добавляем сумму модулей весов к функции потерь:

Loss=1mi=1m(yiy^i)2+α1j=1n|θj|

α отвечает за то, насколько сильно регуляризация будет влиять на модель.

Градиент по функции MSE – это вектор частных производных по независимым переменным. Теперь к нему добавился градиент по функции регуляризации:

(LassoMSE)=(MSE)+α1(j=1n|θj|)(LassoMSE)=[MSEθ0MSEθ1MSEθn]+α1[L1θ0L1θ1L1θn]L1θj=θj|θj|=sign(θj)

Соответственно градиент будет таким:

(LassoMSE)=[2mi=1m(y^iyi)xi02mi=1m(y^iyi)xi12mi=1m(y^iyi)xin]+α1[sign(θ0)sign(θ1)sign(θn)]sign(θi)={1 if θi<00 if θi=01 if θi>0

или в виде матричного перемножения:

(LassoMSE)=2m(Y^Y)X+α1sign(θ)

где θ – вектор весов.

L2 регуляризация (или Ridge регрессия)

То же самое что и L1, только берется не модуль весов, а квадрат:

Loss=1mi=1m(yiy^i)2+α2j=1nθj2

А градиент, соответственно, будет таким:

(RidgeMSE)=2m(Y^Y)X+α22θ

ElasticNet

Это комбинация регуляризаций L1 и L2:

Loss=1mi=1m(yiy^i)2+α1j=1n|θj|+α2j=1nθj2

Градиент:

(ElasticNetMSE)=2m(Y^Y)X+α1sign(θ)+α22θ

Реализация

  1. Добавьте в класс LinReg три параметра:
  1. Добавьте регуляризацию к вычислению функции потерь.
  2. Добавьте регуляризацию к вычислению градиента.

Примечания:

Проверка

Пример входных данных:

None

Ожидаемый результат:

474.8503115919559
"""

class LinReg:

Ваш код здесь

"""Тестирование"""

def prepare_data():
X, y = make_regression(n_samples=100, n_features=10, noise=1, random_state=42)
return X, y

def test_lin_reg_regular():
X, y = prepare_data()

expected_scores = [474.8503115919559, 472.01263129842266, 314.3302508642142, 313.08392714109925]

for id, reg_params in enumerate([{"reg": "None"},
{"reg": "l1", "l1_coef":0.5},
{"reg": "l2", "l2_coef":0.5},
{"reg": "elasticnet", "l1_coef":0.5,"l2_coef":0.5}]):
lin_reg = LinReg(n_iter=50, learning_rate=0.1, metric="mse", **reg_params)
lin_reg.fit(X, y)
expected_score = expected_scores[id]
try:
assert np.round(np.sum(lin_reg.get_coef()),10) == np.round(expected_score,10)
except:
print(f"Тест №{id+1} не пройден")
else:
print(f"Тест №{id+1} пройден")

test_lin_reg_regular()

"""# Скорость обучения

До сих пор у нас была статичная скорость обучения. Но это может быть не оптимальным решением. Слишком большое значение чревато тем, что мы либо проскочим минимум, либо вовсе будем отдаляться от него. Слишком малое значение приведет к длительному процессу обучения. Более разумным было бы динамически менять шаг обучения в зависимости от сделанного количества шагов: в начале двигаемся быстро, чтобы ускорить обучение, а в конце замедляемся, чтобы не пропустить минимум.

Реализуем динамический подход мы посредством лямбда-функции. Примерно такой:

lambda iter: 0.5 * (0.85 ** iter),

где iter – это номер текущей итерации градиентного спуска. Можете у себя локально проверить: запустите код в цикле и убедитесь, что чем больше итерация, тем меньше получается возвращаемое значение.

Возьмите код из предыдущего шага и модифицируйте в нем параметр learning_rate следующим образом:

Можете дополнительно для контроля вывести значение learning_rate в лог обучения.

Примечания:

Т.к. теперь результат зависит от нумерации шагов, то формализуем их нумерацию: они должна считаться от 1 до n_iter (включительно).

Проверка

class LinReg:

Ваш код здесь

"""Тестирование"""

def prepare_data():
X, y = make_regression(n_samples=100, n_features=10, noise=1, random_state=42)
return X, y

def test_lin_reg_lr():
X, y = prepare_data()

expected_scores = [467.5815395326395, 471.88506173264466, 474.7602357788915]

for id, lr_params in enumerate([{"learning_rate" : 0.05},
{"learning_rate" : lambda iter: 0.5 / (1 + 0.5 * iter)},
{"learning_rate" : lambda iter: 0.05 * 0.9 * (np.floor(iter/10))}]):
lin_reg = LinReg(n_iter=50, metric="mse", **lr_params)
lin_reg.fit(X, y)
expected_score = expected_scores[id]
try:
assert np.round(np.sum(lin_reg.get_coef()),10) == np.round(expected_score,10)
except:
print(f"Тест №{id+1} не пройден")
else:
print(f"Тест №{id+1} пройден")

test_lin_reg_lr()

"""# Стохастический градиентный спуск

Это метод оптимизации, используемый в различных алгоритмах машинного обучения. Он призван ускорить и упростить процесс обучения. В реальных задачах обучающая выборка может иметь довольно большой объем. И вычисление градиента функции потерь может потребовать довольно много процессорного времени и других ресурсов компьютера. А это может существенно замедлить обучение модели. Основная идея стохастического градиентного спуска (Stochastic Gradient Descent, SGD) состоит в том, что мы на каждом шаге вычисляем градиент функции потерь не на всей обучающей выборке, а на небольшом её подмножестве.

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

Есть несколько вариаций стохастического градиентного спуска.

Во-первых, выделяют подходы по количеству используемых объектов:

Во-вторых, SGD различаются по способу отбора объектов:

В нашей реализации мы задействуем случайный отбор заданного количества элементов.

Реализация

Добавьте в класс LinReg два новых параметра:

Внесем изменение в алгоритм обучения:

Случайная генерация

Т.к. у нас формальная проверка кода, то у всех должны получиться одинаковые случайные подвыборки. Поэтому и способ у всех будет одинаковый.

В начале обучения посредством модуля random фиксирум сид:

random.seed(<random_state>)

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

sample_rows_idx = random.sample(range(X.shape[0]), self.sgd_sample)

или

sample_rows_idx = random.sample(range(X.shape[0]), int(X.shape[0] * self.sgd_sample))

В этом случае при каждом запуске будут генерироваться одни и те же номера объектов. Что позволит нам добиться воспроизводимости.

Проверка

import random

class LinReg:

Ваш код здесь

"""Тестирование"""

def prepare_data():
X, y = make_regression(n_samples=100, n_features=10, noise=1, random_state=42)
return X, y

def test_lin_reg_sgd():
X, y = prepare_data()

expected_scores = [47.48608054738073, 47.48842798505551]

for id, sgd_params in enumerate([{"sgd_sample": 80},
{"sgd_sample":0.75}]):
lin_reg = LinReg(n_iter=50, metric="mse", learning_rate=0.1, random_state=42, **sgd_params)
lin_reg.fit(X, y)
expected_score = expected_scores[id]
try:
assert np.round(np.mean(lin_reg.get_coef()),10) == np.round(expected_score,10)
except:
print(f"Тест №{id+1} не пройден")
else:
print(f"Тест №{id+1} пройден")

test_lin_reg_sgd()