{
"cells": [
{
"cell_type": "markdown",
"id": "e41aceab",
"metadata": {
"id": "2PQW7OXEoWEZ"
},
"source": [
"# Введение в анализ данных\n",
"## Домашнее задание 4, часть 1. Нейронные сети."
]
},
{
"cell_type": "markdown",
"id": "c8f6d08c",
"metadata": {
"id": "aRt8VO2EoWEa"
},
"source": [
"**Правила, прочитайте внимательно:**\n",
"\n",
"* Выполненную работу нужно отправить телеграм-боту `@thetahat_ds25_bot`. Для начала работы с ботом каждый раз отправляйте `/start`. Дождитесь подтверждения от бота, что он принял файл. Если подтверждения нет, то что-то не так. **Работы, присланные иным способом, не принимаются.**\n",
"* Дедлайн см. в боте. После дедлайна работы не принимаются кроме случаев наличия уважительной причины.\n",
"* Прислать нужно **ноутбук в формате `ipynb`**. Если вы строите интерактивные графики, их стоит прислать в формате html.\n",
"* Следите за размером файлов. **Бот не может принимать файлы весом более 20 Мб.** Если файл получается больше, заранее разделите его на несколько.\n",
"* Выполнять задание необходимо полностью самостоятельно. **При обнаружении списывания всем участникам списывания дается штраф -2 балла к итоговой оценке за семестр.**\n",
"* Решения, размещенные на каких-либо интернет-ресурсах, не принимаются. Кроме того, публикация решения в открытом доступе может быть приравнена к предоставлении возможности списать.\n",
"* Обратите внимание на правила использования ИИ-инструментов при решении домашнего задания.\n",
"* **Код из рассказанных на занятиях ноутбуков** можно использовать без ограничений.\n",
"* Для выполнения задания используйте этот ноутбук в качестве основы, ничего не удаляя из него. Можно добавлять необходимое количество ячеек.\n",
"* Комментарии к решению пишите в markdown-ячейках.\n",
"* Выполнение задания (ход решения, выводы и пр.) должно быть осуществлено на русском языке.\n",
"* Решение проверяется системой ИИ-проверки
**ThetaGrader**. Результат проверки валидируется и исправляется человеком, после чего комментарии отправляются студентам.\n",
"* Если код будет не понятен проверяющему, оценка может быть снижена.\n",
"* Никакой код из данного задания при проверке запускаться не будет. *Если код студента не выполнен, недописан и т.д., то он не оценивается.*\n",
"* **Код из рассказанных на занятиях ноутбуков** можно использовать без ограничений.\n",
"\n",
"\n",
"Важно!!! Правила заполнения ноутбука:\n",
"* Запрещается удалять имеющиеся в ноутбуке ячейки, менять местами положения задач.\n",
"* Сохраняйте естественный линейный порядок повествования в ноутбуке сверху-вниз.\n",
"* Отвечайте на вопросы, а также добавляйте новые ячейки в предложенных местах, которые обозначены `<...>`.\n",
"* В markdown-ячейка, содержащих описание задачи, находятся специальные отметки, которые запрещается модифицировать.\n",
"* При нарушении данных правил работа может получить 0 баллов.\n",
"\n",
"\n",
"**Баллы за задание:**\n",
"\n",
"Легкая часть (достаточно на \"хор\"):\n",
"\n",
"* Задача 1 — 60 баллов\n",
"* **Остальные задачи будут выложены в части 2 с отдельным дедлайном.**\n",
"\n",
"Баллы учитываются в обязательной части курса и не влияют на оценку по факультативной части."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "504cd593",
"metadata": {
"id": "TLUn-L0HoWEb"
},
"outputs": [],
"source": [
"# Bot check\n",
"\n",
"# HW_ID: fpmi_ad4_part1\n",
"# Бот проверит этот ID и предупредит, если случайно сдать что-то не то.\n",
"\n",
"# Status: not final\n",
"# Перед отправкой в финальном решении удали \"not\" в строчке выше.\n",
"# Так бот проверит, что ты отправляешь финальную версию, а не промежуточную.\n",
"# Никакие значения в этой ячейке не влияют на факт сдачи работы."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1e8aa725",
"metadata": {},
"outputs": [],
"source": [
"from typing import Tuple\n",
"import numpy as np\n",
"import torch\n",
"from torchvision import transforms\n",
"from torchvision.datasets import MNIST\n",
"\n",
"# Допишите сюда необходимые импорты"
]
},
{
"cell_type": "markdown",
"id": "58a2d143",
"metadata": {
"id": "N5pKs47noWEc"
},
"source": [
"---\n",
"### Легкая часть"
]
},
{
"cell_type": "markdown",
"id": "23ebacac",
"metadata": {},
"source": [
"\n",
"\n",
"---\n",
"### Задача 1.\n",
"\n",
"**При решении данной задачи можно использовать ИИ-инструменты только для построения графиков и оформления документаций к коду.**"
]
},
{
"cell_type": "markdown",
"id": "3766d31c",
"metadata": {},
"source": [
"Обратимся вновь к уже знакомому нам по первому занятию датасету **MNIST**. Как вы помните, этот набор данных содержит изображения рукописных цифр, каждое из которых имеет размер **28x28**. Всего в датасете содержится **60 000** изображений в обучающей выборке и еще **10 000** — в тестовой.\n",
"\n",
"На первом занятии мы работали с сокращенной версией этого датасета, содержащей **1797** изображений размером **8x8**.\n",
"\n",
"В этой домашней работе мы выберем более \"сложный\" вариант, а именно:\n",
"\n",
"* вернемся к исходной версии датасета с изображениями размером **28x28**,\n",
"* сожмем изображения в два раза по каждой из размерностей, то есть до **14x14**,\n",
"* оставим **10 000** изображений в обучающей выборке.\n",
"\n",
"> *Примечание.* При желании вы можете усложнить задачу — отказаться от сжатия изображений и/или увеличить количество данных. Однако учтите, что в таком случае модели будут работать дольше, и, возможно, вам будет сложнее достичь желаемых результатов."
]
},
{
"cell_type": "markdown",
"id": "7ca2dd9b",
"metadata": {},
"source": [
"**Наша задача остается прежней:** *по изображению определить, какая цифра на нем нарисована.*\n",
"На этот раз мы будем решать ее с помощью простых нейронных сетей. Также мы заглянем \"под капот\" нейросетей, чтобы лучше понять процесс их обучения.\n",
"\n",
"> *Примечание.* Обычно подобные задачи, связанные с изображениями, решаются с помощью сверточных нейронных сетей, которые лучше подходят для работы с изображениями. Они станут темой нашего следующего занятия, ждем всех!"
]
},
{
"cell_type": "markdown",
"id": "8e0bea5e",
"metadata": {},
"source": [
"Ниже представлена готовая функция для загрузки и предварительной обработки данных.\n",
"В рамках этой функции мы выполняем следующие шаги.\n",
"* Загрузка исходных данных.\n",
"* Сохранение **10 000** изображений в качестве обучающей выборки.\n",
"* Сжатие изображений в два раза по каждой из осей, что приводит к размеру **14x14**..\n",
"* Растягиваем изображение размером **14x14** в вектор длиной $14 \\cdot 14 = 196$. Более подробно эта операция описана в первой лекции."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "22295a1d",
"metadata": {},
"outputs": [],
"source": [
"def load_mnist(\n",
" train_size: int = 6000, target_size: int = 14\n",
") -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:\n",
" \"\"\"Загружает и подготавливает данные MNIST для обучения и тестирования.\n",
"\n",
" Параметры:\n",
" train_size (int): Количество обучаемых данных. По умолчанию 6000.\n",
" target_size (int): Размер, до которого уменьшаются изображения. По умолчанию 14.\n",
"\n",
" Возвращает:\n",
" train_images (torch.Tensor): Тензор с изображениями для обучения.\n",
" train_labels (torch.Tensor): Тензор с метками для обучения.\n",
" test_images (torch.Tensor): Тензор с изображениями для тестирования.\n",
" test_labels (torch.Tensor): Тензор с метками для тестирования.\n",
"\n",
" Данные загружаются из набора MNIST, нормализуются, уменьшаются в\n",
" размере и преобразуются в векторы. Для обучения используется только\n",
" часть (train_fraction) от исходного набора данных.\n",
" \"\"\"\n",
"\n",
" PIXEL_MAX_VALUE = 255.0 # Для нормализации значений пикселей\n",
"\n",
" # Создаем преобразование для данных: конвертируем изображения в тензоры PyTorch\n",
" transform = transforms.Compose(\n",
" [\n",
" # Преобразует изображение в тензор и нормализует значения в диапазоне [0, 1]\n",
" transforms.ToTensor(),\n",
" ]\n",
" )\n",
"\n",
" # Загружаем обучающий и тестовый наборы данных MNIST\n",
" train_dataset = MNIST(root=\"./data\", train=True, transform=transform, download=True)\n",
" test_dataset = MNIST(root=\"./data\", train=False, transform=transform, download=True)\n",
"\n",
" # Выбираем часть обучающего набора данных\n",
" train_indices = np.random.choice(len(train_dataset), train_size, replace=False)\n",
"\n",
" # Преобразуем изображения в тензоры и нормализуем их, деля на PIXEL_MAX_VALUE\n",
" train_images = train_dataset.data[train_indices].unsqueeze(1).float() / PIXEL_MAX_VALUE\n",
" test_images = test_dataset.data.unsqueeze(1).float() / PIXEL_MAX_VALUE\n",
"\n",
" # Уменьшаем размер изображений до target_size x target_size с использованием билинейной интерполяции\n",
" train_images = torch.nn.functional.interpolate(\n",
" train_images, size=(target_size, target_size), mode=\"bilinear\", align_corners=False\n",
" )\n",
" test_images = torch.nn.functional.interpolate(\n",
" test_images, size=(target_size, target_size), mode=\"bilinear\", align_corners=False\n",
" )\n",
"\n",
" # Преобразуем изображения в векторы (разворачиваем в одномерные массивы)\n",
" train_images = train_images.view(train_size, -1)\n",
" test_images = test_images.view(len(test_dataset), -1)\n",
"\n",
" # Получаем метки для выбранных обучающих и всех тестовых данных\n",
" train_labels = train_dataset.targets[train_indices]\n",
" test_labels = test_dataset.targets\n",
"\n",
" return train_images, train_labels, test_images, test_labels"
]
},
{
"cell_type": "markdown",
"id": "01e2ff3d",
"metadata": {},
"source": [
"Загрузим данные и посмотрим на их размерности"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "33fa1ac4",
"metadata": {},
"outputs": [],
"source": [
"train_images, train_labels, test_images, test_labels = load_mnist()\n",
"\n",
"print(\"Train:\", train_images.shape, train_labels.shape)\n",
"print(\"Test:\", test_images.shape, test_labels.shape)"
]
},
{
"cell_type": "markdown",
"id": "58a20ab4",
"metadata": {},
"source": [
"\n",
"\n",
"**1.** Используя `Sequential` подход в PyTorch, напишите модель нейронной сети по следующему описанию.\n",
"\n",
"* **Вход**: изображение в виде вектора.\n",
"* **Два линейных слоя** с промежуточной размерностью 64.\n",
"* **Функция активации**: между линейными слоями используется `ReLU`, а на выходе второго слоя — отсутствует (или используется тождественная функция).\n",
"* **Выход**: вектор логитов размером 10, соответствующий количеству классов.\n",
"\n",
"> Эта нейронная сеть для изображения $x$ оценивает вектор логитов $\\left(\\ell_0(x),..., \\ell_9(x)\\right)$ принадлежности к каждому из классов, аналогично логистической регрессии. Имея оценку логитов $\\left(\\ell_0(x),..., \\ell_9(x)\\right)$ , можно получить оценку вероятности $p_k$ для каждого класса по следующей формуле, используя softmax-функцию (обобщение логистической сигмоиды)\n",
"> $$\n",
"\\widehat{p}_k(x) = \\frac{\\exp\\left(\\widehat{\\ell}(x)\\right)}{\\exp\\left(\\widehat{\\ell}_0(x)\\right) +... + \\exp\\left(\\widehat{\\ell}_9(x)\\right)},\n",
"$$\n",
"> которая реализуется с помощью функции `nn.functional.softmax(..., dim=-1)`. В качестве оценки класса можно взять класс с наибольшей вероятностью, что можно реализовать с помощью метода `argmax(axis=...)` у тензора в PyTorch."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "89b571f0",
"metadata": {},
"outputs": [],
"source": [
"..."
]
},
{
"cell_type": "markdown",
"id": "f0078531",
"metadata": {},
"source": [
"\n",
"\n",
"**2.** **Напишите цикл обучения нейросети**, используя кросс-энтропию `nn.CrossEntropyLoss()` качестве лосс-функции. Это обобщение бинарной кросс-энтропии, которую мы рассматривали на лекции по логистической регрессии. Ее реализация в PyTorch принимает на вход логиты (что и возвращает наша нейросеть) и истинные метки классов. Обучайте сеть на полном наборе данных с помощью метода градиентного спуска `torch.optim.SGD`, не разбивая данные на случайные батчи (как это делается в SGD).\n",
"\n",
"\n",
"Примерно каждые 5-10 итераций выполняйте следующие действия:\n",
"\n",
"* Получите текущие предсказания классов для обучающей и тестовой выборок.\n",
"* Посчитайте точность классификации для этих выборок.\n",
"* Постройте график зависимости точности классификации от номера итерации. Перед построением графика используйте `clear_output(wait=True)` для плавной очистки холста.\n",
"* Сохраните следующие значения:\n",
" * Значение лосс-функции.\n",
" * Точность классификации для обучающей и тестовой выборок.\n",
" * Матрицы весов для каждого слоя.\n",
"\n",
"*Замечания:*\n",
"* Рекомендуем сохранять данные в заранее подготовленный словарь, ключи которого соответствуют именам переменных (например, `\"weights_layer_2\"`), а значения словаря представляют собой списки значений этих переменных по итерациям.\n",
"* При сохранении не забывайте вызывать метод `.detach().numpy()` для преобразования матриц в массивы numpy. Также может потребоваться заново инициализировать матрицы с помощью `np.array(...)`, чтобы сохранить сами матрицы, а не ссылки на них, по которым значения меняются в процессе обучения сети.\n",
"\n",
"Выполните 10 000 итераций обучения. Сколько времени это заняло?"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c234fd25",
"metadata": {},
"outputs": [],
"source": [
"..."
]
},
{
"cell_type": "markdown",
"id": "5a1cbec2",
"metadata": {},
"source": [
"Проверьте себя, точность классификации на тестовой выборке должна быть около $90\\%$. Если вы получили значительно меньший результат (менее $85\\%$), попробуйте явно инициализировать веса сети и настроить разные значения learning_rate для разных параметров (посмотрите примеры с занятия). Если эти меры не помогут, то стоит поискать ошибку. Да, обучение нейронных сетей — это непростой процесс."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "448e9876",
"metadata": {},
"outputs": [],
"source": [
"..."
]
},
{
"cell_type": "markdown",
"id": "209a8dc9",
"metadata": {},
"source": [
"\n",
"\n",
"**3.** Теперь самое интересное — заглянем под капот нашей нейросети!\n",
"\n",
"
"
]
},
{
"cell_type": "markdown",
"id": "501e052f",
"metadata": {},
"source": [
"Для более глубокого понимания работы нейросети **визуализируем матрицы весов** для нескольких итераций обучения. Тем самым, вы сможете оценить, как меняется поведение модели в процессе обучения.\n",
"\n",
"1. Выберите примерно 10 итераций обучения, включая первую и последнюю.\n",
"2. Для каждого слоя настройте график, используя предоставленный шаблон.\n",
"\n",
"Обратите внимание, что для корректной визуализации матрицы весов ее необходимо **транспонировать**. Например, для первого слоя вертикальная ось должна соответствовать входу сети, а горизонтальная — промежуточной размерности.\n",
"\n",
"В качестве цветовой палитры выбрана `\"RdBu\"`, что позволяет отображать положительные числа матрицы красным цветом, а отрицательные — синим. Чтобы достичь этого, также **необходимо установить параметры `vmin` и `vmax`** симметрично относительно нуля, чтобы нули отображались белым цветом. Подберите эти значения таким образом, чтобы получить наиболее четкую картинку, на которой выделяются определенные группы пикселей."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6c10c03f",
"metadata": {},
"outputs": [],
"source": [
"# Для каждого слоя\n",
"\n",
"plt.figure(figsize=(12, 10))\n",
"for ...: # по выбранным итерациям\n",
" plt.subplot(...) # для отображения всех итераций в ряд\n",
" plt.imshow(..., cmap=\"RdBu\", vmin=..., vmax=...) # настройте vmin = -vmax\n",
" plt.title(...) # укажите номер итерации\n",
" plt.xticks([]) # уберем координаты по x\n",
" plt.yticks([]) # уберем координаты по y"
]
},
{
"cell_type": "markdown",
"id": "4b3c0ecd",
"metadata": {},
"source": [
"Какие выводы можно сделать, основываясь на полученных визуализациях? \n",
"Подумайте, почему на больших итерациях для первого слоя начинают выделяться определенные пиксели? Почему именно эти пиксели? \n",
"\n",
"*Подсказка: вспомните, как устроены входные изображения цифр, с которыми мы работаем.*"
]
},
{
"cell_type": "markdown",
"id": "1853eadb",
"metadata": {},
"source": [
"**Ответ:**\n",
"\n",
"..."
]
},
{
"cell_type": "markdown",
"id": "98ab00b0",
"metadata": {},
"source": [
"\n",
"\n",
"Попробуйте **улучшить точность классификации**, рассмотрев больше нейронов в промежуточном слое."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "27ed8310",
"metadata": {},
"outputs": [],
"source": [
"..."
]
},
{
"cell_type": "markdown",
"id": "9c9dbef1",
"metadata": {},
"source": [
"Теперь попробуйте добавить к сети еще один слой."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5774cc51",
"metadata": {},
"outputs": [],
"source": [
"..."
]
},
{
"cell_type": "markdown",
"id": "943d6e05",
"metadata": {},
"source": [
"Насколько дольше обучается нейросеть? Получилось ли улучшить качество?"
]
},
{
"cell_type": "markdown",
"id": "ba5cedca",
"metadata": {},
"source": [
"**Ответ:**\n",
"\n",
"..."
]
},
{
"cell_type": "markdown",
"id": "2f6f12b6",
"metadata": {},
"source": [
"\n",
"\n",
"Сделайте выводы."
]
},
{
"cell_type": "markdown",
"id": "36c7e0c8",
"metadata": {},
"source": [
"**Ответ:**\n",
"\n",
"..."
]
},
{
"cell_type": "markdown",
"id": "28341a21",
"metadata": {},
"source": [
"\n",
"\n",
"---\n",
"\n",
"**Мы продолжим работу с нейросетями во второй части этого домашнего задания, которую вы можете сдать на неделю позже первой.**\n",
"\n",
"Вы могли заметить, что качество наших моделей было не таким высоким, как можно было бы получить современными моделями. Тем не менее отметим, что сравнивать с моделью KNN, которую мы рассмотрели на первом занятии, было бы некорректно, так как там использовалась более простая выборка.\n",
"\n",
"Обучение нейронных сетей — это довольно сложная задача. Чтобы глубокие сети обучались более стабильно, используются специальные технологии, изучение которых требует больших усилий. Например, на следующей лекции мы рассмотрим сверточные слои и другие подходы к созданию и обучению нейросетей для анализа изображений.\n",
"Полное погружение в мир нейросетей, включая самые современные архитектуры, ожидает вас на третьем курсе. Например, на DS-потоке будет порядка 14 лекций по этой теме."
]
},
{
"cell_type": "markdown",
"id": "2686624c",
"metadata": {},
"source": [
"
"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}