Phystech@DataScience¶

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

Правила, прочитайте внимательно:

  • Выполненную работу нужно отправить телеграм-боту @thetahat_pds_bot. Для начала работы с ботом каждый раз отправляйте /start. Дождитесь подтверждения от бота, что он принял файл. Если подтверждения нет, то что-то не так. Работы, присланные иным способом, не принимаются.
  • Дедлайн см. в боте. После дедлайна работы не принимаются кроме случаев наличия уважительной причины.
  • Прислать нужно ноутбук в формате ipynb. Если вы строите интерактивные графики, их стоит прислать в формате html.
  • Следите за размером файлов. Бот не может принимать файлы весом более 20 Мб. Если файл получается больше, заранее разделите его на несколько.
  • Выполнять задание необходимо полностью самостоятельно. При обнаружении списывания всем участникам списывания дается штраф -2 балла к итоговой оценке за семестр.
  • Решения, размещенные на каких-либо интернет-ресурсах, не принимаются. Кроме того, публикация решения в открытом доступе может быть приравнена к предоставлении возможности списать.
  • Обратите внимание на правила использования ИИ-инструментов при решении домашнего задания.
  • Код из рассказанных на занятиях ноутбуков можно использовать без ограничений.
  • Для выполнения задания используйте этот ноутбук в качестве основы, ничего не удаляя из него. Можно добавлять необходимое количество ячеек.
  • Комментарии к решению пишите в markdown-ячейках.
  • Выполнение задания (ход решения, выводы и пр.) должно быть осуществлено на русском языке.
  • Решение проверяется системой ИИ-проверки No description has been provided for this image ThetaGrader. Результат проверки валидируется и исправляется человеком, после чего комментарии отправляются студентам.
  • Если код будет не понятен проверяющему, оценка может быть снижена.
  • Никакой код из данного задания при проверке запускаться не будет. Если код студента не выполнен, недописан и т.д., то он не оценивается.

Важно!!! Правила заполнения ноутбука:

  • Запрещается удалять имеющиеся в ноутбуке ячейки, менять местами положения задач.
  • Сохраняйте естественный линейный порядок повествования в ноутбуке сверху-вниз.
  • Отвечайте на вопросы, а также добавляйте новые ячейки в предложенных местах, которые обозначены <...>.
  • В markdown-ячейках, содержащих описание задачи, находятся специальные отметки, которые запрещается модифицировать.
  • При нарушении данных правил работа может получить 0 баллов.

Баллы за задание:

Легкая часть (достаточно на "хор"):

  • Задача 1 — 60 баллов
  • Задача 2 — 60 баллов

Сложная часть (необходимо на "отл"):

  • Задача 3 — 30 баллов
  • Задача 4 — 30 баллов

In [ ]:
# Bot check

# HW_ID: phds_hw11
# Бот проверит этот ID и предупредит, если случайно сдать что-то не то.

# Status: not final
# Перед отправкой в финальном решении удали "not" в строчке выше.
# Так бот проверит, что ты отправляешь финальную версию, а не промежуточную.
# Никакие значения в этой ячейке не влияют на факт сдачи работы.
In [44]:
import numpy as np
import pandas as pd
import seaborn as sns
import torch
from torch import nn, optim
import matplotlib.pyplot as plt

from IPython.display import clear_output

from sklearn.metrics import mean_absolute_percentage_error, r2_score
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.preprocessing import LabelBinarizer, StandardScaler
from sklearn.model_selection import train_test_split

import warnings
warnings.filterwarnings('ignore')

Легкая часть¶

Задача 1¶

Профиль биология¶

Мы будем исследовать датасет по экспрессиям различных генов (RNA-seq), используемых для предсказания возраста пациентов.

In [ ]:
df = pd.read_csv("Rnaseq_age_reg.csv")
df.head()

Разбейте датасет на признаки и таргет, где в качестве таргета будет использоваться столбец Age, а признаки - все остальные. В том числе разбейте на подвыборки для обучения и теста.

In [ ]:
 

Переходите к общей части.

Профиль физика¶

In [ ]:
df = pd.read_csv("physics_data.csv", index_col=0)
df.head()

Разбейте датасет на признаки и таргет, где в качестве таргета будет использоваться столбец Eat, а признаки - все остальные. В том числе разбейте на подвыборки для обучения и теста.

In [ ]:
 

Общая часть¶

Отмасштабируйте данные:

In [174]:
 

Обучите модель линейной регрессии и посмотрите на значения метрик на тесте. Что вы можете сказать про результат обучения?

In [ ]:
 

Обучите линейные модели с регуляризациями, которые мы проходили ранее. Для каждой из моделей постройте графики зависимости метрик r2 и MAPE от коэфициента регуляризации. Можно пользоваться кодом из домашнего задания по регуляризации. Сильно ли улучшился результат?

In [176]:
 

Выберите оптимальный, на вашь взгляд, параметр для L1-регуляризации, обучите модель Lasso-регрессии, выведите ещё раз метрики r2 и MAPE и проведите отбор признаков: уберите из датасета все те, для которых коэффициент регрессии оказался нулевым.

In [ ]:
 

Теперь обучите простейшую нейронную сеть на уменьшенном датасете, сравните результат с результатами обучения других моделей.

Подсказка Используйте nn.Sequential()

In [96]:
model = 
In [97]:
optim_func = <...>
optimizer = <...>
In [98]:
 
In [ ]:
with torch.no_grad():
    y_pred = model(torch.FloatTensor(<Тестовая выборка>)).numpy()

print(f'R2: {round(r2_score(y_test, y_pred), 2)} \nMAPE: {round(mean_absolute_percentage_error(y_test, y_pred), 2)}')

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

<...>

Сравните все модели, поясняя полученные результаты и значения метрик.

Вывод:


Задача 2¶

Профиль физика¶

Скачайте датасет, описывающий распады Z-бозонов двух типов: Zeeи Zmumu. Создайте и обучите нейросеть, разделяющую эти два класса.

In [ ]:
data = pd.read_csv('Z_boson.csv')
data.head()

Удалите столбцы Unnamed: 0, Run и Event, так как это не физические величины. Удалите строки, где есть пропуски, если таковые имеются.

In [ ]:
 

Также можно как-нибудь взглянуть на признаки. Возможно, не все они вносят вклад в разделение классов. Не забудьте преобразовать таргет (столбец class) к формату 0 и 1. Вам может пригодиться sklearn.preprocessing.LabelBinarizer.

In [ ]:
plt.figure(figsize=(20, 10))
sns.set_theme(font_scale=2.0)
sns.pairplot(data, hue="class", palette="deep")

Какие признаки вы бы использовали для разделения людей по классам? Выберите эти столбцы и создайте наборы train и test с помощью функции train_test_split, а также выделите набор данных для валидации при обучении.

In [142]:
# исходя из графиков, отберём признаки для обучения
selected_features = [<...>]
X = data[selected_features]
In [143]:
# таргет преобразуем из строк "Zee", "Zmumu" к 0 и 1
<...>

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7)
X_val, X_test, y_val, y_test  = train_test_split(X_test, y_test, train_size=0.3)

Переходите к общей части.

Профиль биология¶

Скачайте датасет, описывающий влияние курения и алкоголя на человека. Создайте и обучите нейросеть, разделяющую эти два класса.

In [151]:
df = pd.read_csv("smoking_driking_dataset_Ver01.csv")
In [ ]:
df.describe()
In [ ]:
df.columns, df.shape
In [154]:
column_names = df.columns[1:-2]# нам не нужны гендер и таргеты сейчас
In [ ]:
column_names

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

In [156]:
def smoking_preprocessing(x):
  if x == 3 or x == 2:
    return 1
  else:
    return 0

def drinking_preprocessing(x):
  if x == 'Y':
    return 1
  else:
    return 0
In [157]:
df['SMK_stat_type_cd'] = df['SMK_stat_type_cd'].apply(func = smoking_preprocessing)
df['DRK_YN'] = df['DRK_YN'].apply(func = drinking_preprocessing)
In [158]:
df['bad_habits'] = df['DRK_YN']+df['SMK_stat_type_cd']-df['DRK_YN']*df['SMK_stat_type_cd']

Теперь смотрим на разделение по привычкам

In [ ]:
graph = sns.PairGrid(df.iloc[:1000], hue='bad_habits', vars = column_names[:7])
graph.map_upper(sns.scatterplot)
graph.map_lower(sns.kdeplot)
graph.map_diag(sns.kdeplot)
graph.add_legend()
In [ ]:
graph = sns.PairGrid(df.iloc[:1000], hue='bad_habits', vars = column_names[7:15])
graph.map_upper(sns.scatterplot)
graph.map_lower(sns.kdeplot)
graph.map_diag(sns.kdeplot)
graph.add_legend()
In [ ]:
graph = sns.PairGrid(df.iloc[:1000], hue='bad_habits', vars = column_names[15:])
graph.map_upper(sns.scatterplot)
graph.map_lower(sns.kdeplot)
graph.map_diag(sns.kdeplot)
graph.add_legend()
In [159]:
target_1 = df.pop('SMK_stat_type_cd')
target_2 = df.pop('DRK_YN')
target_3 = df.pop('bad_habits')

Какие признаки вы бы использовали для разделения людей по классам? Выберите эти столбцы и создайте наборы train и test с помощью функции train_test_split, а также выделите набор данных для валидации при обучении.

In [160]:
# исходя из графиков, отберём признаки для обучения (нас интересуют вредные привычки)
selected_features = [<...>]
X = data[selected_features]
In [161]:
# данных очень много, поэтому для экономии времени автор ноутбука отводит на обучение всего треть датасета
X_train, X_test, y_train, y_test = train_test_split(X, target_3, train_size=0.3)
X_val, X_test, y_val, y_test  = train_test_split(X_test, y_test, train_size=0.3)

Общая часть¶

Как вы помните, в задаче классификации предсказывается вероятность. На основании этой вероятности можно делать вывод о принадлежности объекта к тому или иному классу. Причём не всегда используется порог $P=0.5$. Например, если классы несбалансированы, это значение можно варьировать на интервале (0, 1). Предоставляем вам возможность самим выбрать этот порог и поэкспериментировать.

In [162]:
class_lim_proba = <...> # критерий принадлежности к тому или иному классу

Стандартизируйте данные

In [163]:
 

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

In [164]:
def plot_learning_curves(history):
    '''
    Функция для отображения лосса и метрики во время обучения.
    '''
    clear_output(wait=True)

    fig = plt.figure(figsize=(20, 7))
    fontsize = 15   # размер шрифта

    plt.subplot(1,2,1)
    plt.title('Лосс', fontsize=fontsize)
    plt.plot(history['loss_train'], label='train')
    plt.plot(history['loss_val'],   label='val')
    plt.ylabel('лосс',  fontsize=fontsize)
    plt.xlabel('эпоха', fontsize=fontsize)
    plt.legend()

    plt.subplot(1,2,2)
    plt.title('Метрика', fontsize=fontsize)
    plt.plot(history['metric_train'], label='train')
    plt.plot(history['metric_val'],   label='val')
    plt.ylabel('Значение метрики', fontsize=fontsize)
    plt.xlabel('эпоха',    fontsize=fontsize)
    plt.legend()
    plt.show()

Напишите функцию метрики, которую вы будете использовать, например accuracy.

In [ ]:
def metric(y_true, y_pred):
  <...>
  return metric

Создание модели.¶

В семинаре вы у промежуточных слоёв задавали in_features = out_features = 1, а в данном случае вам надо будет создать нейросеть из нескольких слоёв, поставив только у последнего из них out_features = 1.

Какой должна быть размерность входа первого слоя?

In [ ]:
model = <...>

model

Обучение¶

В качетсве функции потерь возьмите бинарную кросс-энтропию, а шаг градиентного спуска установите равным 0.5. Можете взять и другие loss и lr, если хотите поэкспериментировать.

In [148]:
optim_func = <...>
optimizer = <...>
In [ ]:
batch_size = 2000 # этот
num_iter = 200    # и этот параметры можете также поварьировать
history = {
    'loss_train': [],
    'loss_val': [],
    'metric_train': [],
    'metric_val': [],
}

for i in range(num_iter):
    
    # Так как размер выборки слишком велик, то будем обучать лишь на части данных
    indexes_train = np.random.choice(np.arange(len(X_train)), batch_size, replace=False)
    local_X_train = X_train[indexes_train]
    local_y_train = y_train[indexes_train]

    indexes_val = np.random.choice(np.arange(len(X_val)), batch_size//10, replace=False)
    local_X_val = <...>
    local_y_val = <...>

    # Forward pass: предсказание модели по данным X_train
    y_pred_train = <...>
    with torch.no_grad():
        y_pred_val = <...>


    # Вычисление оптимизируемой функции (MSE) по предсказаниям
    loss_train = <...>
    with torch.no_grad():
        loss_val = <...>

    # Backward pass: вычисление градиентов оптимизируемой функции
    # по всем параметрам модели
    <...>

    # Оптимизация: обновление параметров по формулам соответствующего
    # метода оптимизации, используются вычисленные ранее градиенты
    <...>

    # Зануление градиентов
    <...>

    # Считаем метрику на эпохе (здесь посчитана accuracy, можете реализовать любую другую за доп. баллы)
    metric_train = np.sum((y_pred_train.detach().numpy() >= class_lim_proba).reshape(-1) == local_y_train) / len(local_y_train)
    metric_val = np.sum((y_pred_val.detach().numpy() >= class_lim_proba).reshape(-1) == local_y_val) / len(local_y_val)

    # Сохраняем результаты эпохи
    history['loss_train'].append(loss_train.item())
    history['loss_val'].append(loss_val)
    history['metric_train'].append(metric_train)
    history['metric_val'].append(metric_val)

    # График Метрики + Лосса для трейна и валидации каждую итерацию
    plot_learning_curves(history)

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

In [ ]:
with torch.no_grad():
      y_pred_test = <...>
      loss_test = <...>
      metric_test = <считается по аналогии с тем, как на обучении>
print(f" Test Loss: {loss_test} \n Test metric: {metric_test}")

Выводы:


Сложная часть¶

Задача 3¶

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

Запрещено использовать torch.nn (саму библиотеку torch использовать можно и нужно). Чтобы иметь перед глазами оставим здесь формулы:

$$\widehat{y}(x) = w_1u(x) + b_1,$$

$$u(x) = \sigma(w_0x + b_0),$$

$$\sigma(x) = \text{ReLU}(x) = \begin{equation*}\begin{cases}x, \; x \ge 0, \\ 0, \; \text{иначе,} \end{cases} \end{equation*}$$

$w_0, b_0 \in \mathbb{R}$ — обучаемые параметры первого слоя, $w_1, b_1 \in \mathbb{R}$ — обучаемые параметры второго слоя, $\sigma(x)$ — функция активации, в данном случае мы выбрали ReLU.

Реализуйте функцию активации:

In [ ]:
def act_func(x):
    return <...>

Задайте оптимизируемую функцию / функцию ошибки / лосс — MSE:

$$ MSE(\widehat{y}, y) = \frac{1}{n}\sum_{i=1}^n\left(\widehat y_i - y_i\right)^2 $$

In [ ]:
def optim_func(y_pred, y_true):
    return <...>

Обучите вашу модель на данных профиля, которые вы выбрали в 1-й задаче. Выберите 1 признак, на котором проводите обучение.

In [ ]:
# Инициализация параметров
w0 = <...>
b0 = <...>
w1 = <...>
b1 = <...>

# Количество итераций
num_iter = 1000

# Скорость обучения для параметров
lr_w = 0.01
lr_b = 0.05

for i in range(num_iter):

    # Forward pass: предсказание модели
    y_pred = <...>

    # Вычисление оптимизируемой функции (MSE)
    loss = <...>
    # Bakcward pass: вычисление градиентов
    loss.backward()

    # Оптимизация: обновление параметров
    <...>

    # Зануление градиентов
    <...>

Вывод:


Задача 4¶

Рассмотрим двуслойную нейронную сеть, которая принимает на вход $x\in\mathbb{R}$ и возвращает $y\in\mathbb{R}$. Выход первого слоя возвращает $u \in\mathbb{R}^2$. После первого слоя используется функция активации $\sigma(x) = \frac{1}{1 + \exp(-x)}$, после второго слоя функция активации не используется (или используется тождественная). Тем самым нашу нейронку можно представить в виде

$$\widehat{y}(x) = \sum_{h=1}^2 w_{2h}u_h(x) + b_2,$$

$$u_h(x) = \sigma(w_{1h}x + b_{1h}),$$

$$\text{где} \; h \in \{1, 2\}.$$

1. Нарисуйте схематически данную нейронную сеть. Сколько у нее обучаемых параметров?

...

2. Пусть нам дана обучающая выборка $(X_1, Y_1), ..., (X_n, Y_n)$, где $X_i \in \mathbb{R}$ и $Y_i \in \mathbb{R}$. Нейронная сеть обучается по этой выборке, минимизируя заданную функцию $L$ — функцию ошибки. Положим, что $L$ — это MSE: $$\text{MSE} = L(X, Y) = \frac{1}{n}\sum_{i=1}^n \big(Y_i - \widehat{y}(X_i)\big)^2.$$

Наша задача — найти оптимальные параметры нашей модели для минимизации $L(X, Y)$ на заданном наборе данных. Мы будем решать эту задачу с помощью градиентного спуска. Для этого нам понадобится выписать производные по всем параметрам сети. Конечно, в данном случае довольно просто выписать все производные напрямую. Однако мы воспользуемся следующей хитростью: мы будем считать производные поэтапно, причем начнем с конца вычислительной цепочки и, используя формулу производной сложной функции, последовательно посчитаем все необходимые производные. Этот процесс называется методом обратного распространения ошибки (backpropagation).

2.1. Начнем с производной MSE по выходам сети: $$\frac{\partial\:\text{MSE}}{\partial \widehat{y}(X_i)} = \; ...$$

2.2 Возьмем производные выходов сети по параметрам последнего слоя

$$\frac{\partial \widehat{y}(X_i)}{\partial w_{2h}} = \; ...$$

$$\frac{\partial \widehat{y}(X_i)}{\partial b_2} = \; ...$$

Также выпишем производные выходов сети по входам последнего слоя:

$$\frac{\partial \widehat{y}(X_i)}{\partial u_h(X_i)} = \; ...$$

Теперь выпишем производные MSE по параметрам и входам последнего слоя. Для этого вспомните правило производной сложной функции из математического анализа. Обратите внимание на то, что нам не нужно прописывать все производные до конца, достаточно заполнить пропуски в записи ниже:

$$\frac{\partial\:\text{MSE}}{\partial w_{2h}} = \sum_{i=1}^n \frac{\partial\:\text{MSE}}{\partial ...} \frac{\partial ...}{\partial w_{2h}}$$

$$\frac{\partial\:\text{MSE}}{\partial b_2} = \sum_{i=1}^n \frac{\partial\:\text{MSE}}{\partial ...} \frac{\partial ...}{\partial b_2}$$

$$\frac{\partial\:\text{MSE}}{\partial u_h} = \sum_{i=1}^n \frac{\partial\:\text{MSE}}{\partial ...} \frac{\partial ...}{\partial u_h}$$

2.3. Теперь будем разбираться с производными по параметрам первого слоя.

Для начала нам пригодится производная функции активации, запишите ее так, чтобы ответе осталась функция от $\sigma(x)$:

$$\frac{\partial\:\sigma(x)}{\partial x} = \; ...$$

Теперь возьмем производные выходов первого слоя по его параметрам:

$$\frac{\partial u_h(X_i)}{\partial w_{1h}} = \; ...$$

$$\frac{\partial u_h(X_i)}{\partial b_{1h}} = \; ...$$

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

$$\frac{\partial\:\text{MSE}}{\partial w_{1h}} = \; \sum_{i=1}^n \frac{\partial\:\text{MSE}}{\partial ...} \frac{\partial ...}{\partial w_{1h}}$$

$$\frac{\partial\:\text{MSE}}{\partial b_{1h}} = \; \sum_{i=1}^n \frac{\partial\:\text{MSE}}{\partial ...} \frac{\partial ...}{\partial b_{1h}}$$

3. Пусть обучающая выборка очень большая. Что нужно делать в таком случае? Запишите, как нужно поменять правило обновления параметров.

...

Вывод:

...