Занятие 7

Лабораторная 7 (Transfer Learning)

In [ ]:
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split

import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import TensorDataset, DataLoader

from torchvision import transforms as tfs
import os
from torchvision.datasets import MNIST

1.Свёртка

Сверточная операция(свёртка) в сверточной нейронной сети (CNN) используется для извлечения признаков из входных данных, таких как изображения. Эта операция выполняется с помощью фильтров (ядер).

Двумерная свертка

В случае изображений, мы имеем двумерное изображение $I$ с размерностью$H \times W$ и двумерное ядро (или фильтр) $F$ размерностью $M \times N$.

Двумерная свертка изображения с ядром выполняется путем скольжения фильтра $F$ по изображению и вычисления суммы поэлементных произведений пикселей изображения и элементов фильтра:

$(I * F)[i, j]$ = $\sum_{m=0}^{M-1} \sum_{n=0}^{N-1} I[i+m, j+n] \cdot F[m, n] $

Это вычисление содержит операцию свертки. Процесс повторяется для каждой позиции на изображении и в результате получается новая матрица, называемая feature map, которая выявляет определенные признаки изображения.

Скорее всего сейчас ничего непонятно, давайте рассмотрим пример.

Предположим, у нас есть следующее изображение (представленное в виде матрицы): \begin{bmatrix} 1 & 2 & 3 & 4\\ 2 & 1 & 1 & 3\\ 4 & 3 & 1 & 0\\ 0 & 0 & 1 & 5\\ \end{bmatrix}

$a_{11} = 1, a_{12} = 2, a_{13} = 3, a_{14} = 4, a_{21} = 2$ и т.д

И пусть у нас будет следующее ядро свертки (или фильтр):

\begin{bmatrix} 1 & 2 \\ 7 & 1 \\ \end{bmatrix}

$b_{11} = 1, b_{12} = 2, b_{21} = 7, b_{22} = 1$

Выполним свертку изображения с этим ядром шаг за шагом.

Результатом свёртки будет матрица размером 3 $\times$ 3.

\begin{bmatrix} с_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \\ \end{bmatrix}

$c_{11} = a_{11} * b_{11} + a_{12} * b_{12} + a_{21} * b_{21} + a_{22} * b_{22}$

$c_{12} = a_{12} * b_{11} + a_{13} * b_{12} + a_{22} * b_{21} + a_{23} * b_{22}$

$c_{13} = a_{13} * b_{11} + a_{14} * b_{12} + a_{23} * b_{21} + a_{24} * b_{22}$

$c_{21} = a_{21} * b_{11} + a_{22} * b_{12} + a_{31} * b_{21} + a_{32} * b_{22}$

$c_{22} = a_{22} * b_{11} + a_{23} * b_{12} + a_{32} * b_{21} + a_{33} * b_{22}$

$c_{23} = a_{23} * b_{11} + a_{24} * b_{12} + a_{33} * b_{21} + a_{34} * b_{22}$

$c_{31} = a_{31} * b_{11} + a_{32} * b_{12} + a_{41} * b_{21} + a_{42} * b_{22}$

$c_{32} = a_{32} * b_{11} + a_{33} * b_{12} + a_{42} * b_{21} + a_{43} * b_{22}$

$c_{33} = a_{33} * b_{11} + a_{34} * b_{12} + a_{43} * b_{21} + a_{44} * b_{22}$

Процесс свёртки обычно включает в себя два основных гиперпараметра:

  • Размер окна свёртки: Это размер ядра. В примере выше размер ядра 2$×$2.

  • Шаг (Stride): Это расстояние между центрами соседних окон ядер. В примере выше stride = 1.

image.png

Задача1 (1 балл)

Дано входное изображение(двумерный тензор):

\begin{bmatrix} 1 & 2 & 3 & 4 & 6\\ 2 & 1 & 1 & 3 & 1\\ 4 & 3 & 1 & 0 & 0\\ 0 & 0 & 1 & 5 & 7\\ 3 & 10 & 4 & 5 & 1\\ \end{bmatrix}

Дана свёртка:

\begin{bmatrix} 1 & 0 & 0\\ 0 & 1 & 1\\ 1 & 0 & 1\\ \end{bmatrix}

Примените данную свёртку к изображению.

2.Pooling

Pooling (или пулинг) в сверточных нейронных сетях (CNN) используется для уменьшения размерности признаковых карт, получаемых после сверточных слоев. Он помогает упрощать и концентрировать информацию, уменьшая объем вычислений и сделав модель более устойчивой к масштабным вариациям входных данных.

Основные типы пулинга в CNN:

  1. Max Pooling: Для каждой области во входной признаковой карте выбирается максимальное значение. Max pooling помогает выделить самые активные признаки из пространственных областей.

  2. Average Pooling: Для каждой области входной признаковой карты вычисляется среднее значение. Average pooling позволяет усреднить информацию из пространственных областей.

Процесс пулинга обычно включает в себя два основных параметра:

  • Размер окна пулинга: Это размер области, для которой вычисляется максимальное или среднее значение. Обычно используются окна размером 2x2 или 3x3.

  • Шаг (Stride): Это расстояние между центрами соседних окон пулинга. Обычно используется шаг 2, что делает пулинг-слои в полтора раза меньше по размерам.

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

Исходная матрица: \begin{bmatrix} 1 & 1 & 1 & 0 \\ 3 & 2 & 2 & 4 \\ 3 & 2 & 3 & 2 \\ 0 & 1 & 3 & 3 \\ \end{bmatrix}

Применение Max Pooling с окном 2x2 и шагом 2:

  1. Выбираем первую область 2x2 из исходной матрицы: \begin{bmatrix} 1 & 1 \\ 3 & 2 \\ \end{bmatrix} Максимальное значение здесь: 3

  2. Выбираем вторую область 2x2 (с пропуском каждой второй строки и столбца) из исходной матрицы: \begin{bmatrix} 1 & 0 \\ 2 & 1 \\ \end{bmatrix} Максимальное значение здесь: 2

  3. Выбираем третью область 2x2 (с пропуском каждой второй строки и столбца) из исходной матрицы: \begin{bmatrix} 3 & 2 \\ 0 & 1 \\ \end{bmatrix} Максимальное значение здесь: 3

Таким образом, после применения операции Max Pooling с окном 2x2 и шагом 2 получаем новую матрицу: \begin{bmatrix} 3 & 2 \\ 3 & 3 \\ \end{bmatrix}

pooling.png

Задача2 (2 балла)

Реализуйте функцию, принимающую на вход матрицу, свёртку для неё(размера 2 на 2) и вычисляющую результат свёртки с stride = 1.

3.Пэддинг

Пэддинг (padding) - это процесс добавления нулей или других значения по краям входной матрицы в глубоком обучении. Пэддинг обычно используется в сверточных нейронных сетях перед применением операции свертки или пулинга. Основная цель пэддинга состоит в том, чтобы сохранить размерность входных данных или упростить реализацию операций свертки и пулинга.

Задача3 (1 балл)

Реализуйте функцию, принимающую на вход матрицу и осуществяющую padding.

4.Свёрточная Нейронная сеть

Свёрточная нейронная сеть (Convolutional Neural Network, CNN) - это особый вид нейронных сетей, разработанный для обработки и анализа многомерных данных, таких как изображения. CNN успешно применяются в компьютерном зрении, распознавании образов, анализе временных рядов и других областях, где важна обработка многомерных данных.

Основные компоненты свёрточной нейронной сети:

  1. Сверточный слой (Convolutional Layer): Этот слой выполняет операцию свертки, когда ядро фильтра применяется к входным данным для извлечения признаков.
  2. Пулинговый слой (Pooling Layer): Данный слой уменьшает размерность пространства признаков путем объединения (например, максимум или среднее значения в каждой области).
  3. Полносвязные слои (Fully Connected Layers): Эти слои используются для принятия решений на основе признаков, извлеченных предыдущими слоями.
  4. Функции активации (Activation Functions): Обычно применяются функции активации, такие как ReLU (Rectified Linear Unit), чтобы вводить нелинейность в сеть и улучшить ее способность обобщения.

Преимущества свёрточных нейронных сетей:

  1. Работа с пространственной структурой данных: CNN способны учитывать пространственную локальность и общие закономерности во входных данных, таких как пиксели изображений.
  2. Работа с сокращенной размерностью: Пулинговые слои позволяют уменьшить размерность пространства признаков, упрощая задачу обработки.
  3. Способность извлекать признаки: Свёрточные слои способны извлекать локальные признаки из входных данных, позволяя сети автоматически выявлять особенности в данных.
In [ ]:
data_tfs = tfs.Compose([
    tfs.ToTensor(),
    tfs.Normalize((0.5), (0.5))
])

# install for train and test
root = './'
train_dataset = MNIST(root, train=True,  transform=data_tfs, download=True)
val_dataset  = MNIST(root, train=False, transform=data_tfs, download=True)
In [ ]:
train_dataloader =  DataLoader(train_dataset, batch_size=128)
valid_dataloader =  DataLoader(val_dataset, batch_size=128)

Архитектура LeNet

LeNet — это структура(архитектура) сверточной нейронной сети, предложенная в 1998 году.

Архитектура:

Входное изображение размером 28(height)$×$28(widht)$×$(1 channel).

  1. Свёртка1(ядро: 5$×$5, пэддинг: 2, каналов на выходе: 6). На выходе: 28(height)$×$28(widht)$×$(6 channels)

  2. Pooling1(ядро: 2$×$2, stride: 2). На выходе: 14(height)$×$14(widht)$×$(6 channels)

  3. Свёртка2(ядро: 5$×$5, пэддинг: 0, каналов на выходе: 16). На выходе: 10(height)$×$10(widht)$×$(16 channels)

  4. Pooling2(ядро: 2$×$2, stride: 2). На выходе: 5(height)$×$5(widht)$×$(16 channels)

  5. Линейный слой1 (120 нейронов)

  6. Линейный слой2 (84 нейрона)

  7. Выходной слой (10 нейронов)

После Свёртки1 и Свёртки два следует функция активации.

После Pooling2 следует flatten.

Параметры LeNet

Поскольку обучение нейронной сети это поиск оптимальных параметров, давайте посчитаем количество параметров (весов) LeNet.

Свёртка1: 6 ядер размера 5$×$5 + 6 параметров bias = 156.

Pooling1: параметров нет!

Свёртка2: 16 ядер размера 5$×$5$×$6 + 16 параметров bias = 2416.

Pooling2: параметров нет!

Линейный слой1: входной слой: 5$×$5$×$16 = 400, выходной: 120. Число параметров 400$×$120 + 120 = 48120.

Линейный слой2: входной слой: 120, выходной: 84. Число параметров 120$×$84 + 84 = 10164.

Выходной слой: входной слой: 84, выходной: 10. Число параметров 84$×$10 + 10 = 850.

Итоговое число параметров: 156 + 2416 + 48120 + 10164 + 850 = 61706.

Реализуем архитектуру на python в виде класса LeNet.

In [ ]:
class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        # Свёртка1
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding = 2)
        # Свёртка2
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5)
        # Pooling1
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride = 2)
        # Pooling2
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride = 2)
        # Линейный слой1
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        # Линейный слой2
        self.fc2 = nn.Linear(120, 84)
        # Выходной слой
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # Свёртка1 + активация Тангес
        x = F.tanh(self.conv1(x))
        # Pooling1
        x = self.pool1(x)
        # Свёртка1 + активация Тангес
        x = F.tanh(self.conv2(x))
        # Pooling2
        x = self.pool2(x)
        # Flatten
        size_ = int(x.nelement() / x.shape[0])
        x = x.view(-1, size_)
        # Линейный слой1 + активация Тангес
        x = F.tanh(self.fc1(x))
        # Линейный слой2 + активация Тангес
        x = F.tanh(self.fc2(x))
        # Выходной слой
        x = self.fc3(x)
        return x
In [ ]:
device = "cuda" if torch.cuda.is_available() else "cpu"
In [ ]:
model = LeNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
loaders = {"train": train_dataloader, "valid": valid_dataloader}

Обратите внимание! Мы используем .to(device), что означает, что мы перемещаем наши объекты на GPU и обучаемся там же.

In [ ]:
max_epochs = 10
accuracy = {"train": [], "valid": []}
for epoch in range(max_epochs):
    for k, dataloader in loaders.items():
        epoch_correct = 0
        epoch_all = 0
        for x_batch, y_batch in dataloader:
            if k == "train":
                model.train()
                optimizer.zero_grad()
                outp = model(x_batch.to(device))
            else:
                model.eval()
                with torch.no_grad():
                    outp = model(x_batch.to(device))
            preds = outp.argmax(-1)
            correct =  (preds == y_batch).sum()
            all = y_batch.shape[0]
            epoch_correct += correct.item()
            epoch_all += all
            if k == "train":
                loss = criterion(outp, y_batch.to(device))
                loss.backward()
                optimizer.step()
        if k == "train":
            print(f"Epoch: {epoch+1}")
        print(f"Loader: {k}. Accuracy: {epoch_correct/epoch_all}")
        accuracy[k].append(epoch_correct/epoch_all)

Вспомните, какую точность давали другие алгоритмы на данном датасете. Свёрточная нейронная сеть показала лучший результат!

Задача (3 балла)

Для датасета CIFAR10, постройте и обучите следующую нейронную сеть.

Архитектура:

Входное изображение размером 32(height)$×$32(widht)$×$(3 channel).

  1. Свёртка1(ядро: 3$×$3, пэддинг: 0, каналов на выходе: 6). На выходе: ВАШ ОТВЕТ.

  2. Pooling1(ядро: 2$×$2, stride: 2). На выходе: ВАШ ОТВЕТ.

  3. Свёртка2(ядро: 3$×$3, пэддинг: 0, каналов на выходе: 12). На выходе: ВАШ ОТВЕТ.

  4. Pooling2(ядро: 2$×$2, stride: 2). На выходе: ВАШ ОТВЕТ.

  5. Линейный слой1 (256 нейронов)

  6. Линейный слой2 (128 нейрона)

  7. Линейный слой3 (64 нейрона)

  8. Выходной слой (10 нейронов)

  • После Свёртки1 и Свёртки два следует функция активации: ReLU.

  • После Pooling2 следует flatten.

Сколько параметров имеет данная архитектура? ВАШ ОТВЕТ.

Если всё сделает правильно, то легко получите точность выше 60 на 10 эпохах!

In [ ]:

5. Transfer learning

Source Task (Исходная задача): Обучить эту же архитектуру (CustomCNN) на датасете CIFAR-100.

Почему: CIFAR-100 имеет те же размеры изображений (32x32), но содержит 100 классов (более сложная задача). Модель научится выделять более гранулярные признаки (текстуры, формы объектов).

Target Task (Целевая задача): Использовать полученные веса для классификации CIFAR-10.

Связь: Классы CIFAR-10 являются укрупненными группами классов CIFAR-100 (например, в CIFAR-100 есть "яблоко", "груша", "апельсин", а в CIFAR-10 это просто "фрукты/еда" или соответствующие категории).

5.1 Обучение на CIFAR-10

In [ ]:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import time
import copy

# Проверка GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# ---------------------------------------------------------
# TODO: Реализуйте архитектуру сети согласно заданию
# ---------------------------------------------------------
class CustomCNN(nn.Module):
    def __init__(self, num_classes=10):
        super(CustomCNN, self).__init__()

        # Свёрточный блок 1
        self.conv1 = nn.Conv2d(____, ____, kernel_size=3, padding=0)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)

        # Свёрточный блок 2
        self.conv2 = nn.Conv2d(____, ____, kernel_size=3, padding=0)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)

        # Полносвязные слои
        self.fc1 = nn.Linear(____, 256)  # TODO: вычислите входную размерность
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 64)
        self.out = nn.Linear(64, num_classes)

        self.relu = nn.ReLU()

    def forward(self, x):
        # TODO: Реализуйте forward pass
        # Conv1 -> ReLU -> Pool1
        # Conv2 -> ReLU -> Pool2
        # Flatten
        # FC1 -> ReLU -> FC2 -> ReLU -> FC3 -> ReLU -> Output
        return x

# Проверка архитектуры
model = CustomCNN(num_classes=10)
print(model)

# Тестовый прогон
test_input = torch.randn(1, 3, 32, 32)
test_output = model(test_input)
print(f"Output shape: {test_output.shape}")  # Должно быть: torch.Size([1, 10])и
In [ ]:
# ---------------------------------------------------------
# Подготовка данных
# ---------------------------------------------------------
transform_train = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomCrop(32, padding=4),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                             download=True, transform=transform_train)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                            download=True, transform=transform_test)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=2)

# ---------------------------------------------------------
# Функция обучения
# ---------------------------------------------------------
def train_model(model, train_loader, test_loader, num_epochs=15, lr=0.001):
    model = model.to(device)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=lr)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

    history = {'train_loss': [], 'test_acc': [], 'train_time': []}

    for epoch in range(num_epochs):
        start_time = time.time()
        model.train()
        running_loss = 0.0

        for inputs, labels in train_loader:
            inputs, labels = inputs.to(device), labels.to(device)

            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            running_loss += loss.item()

        scheduler.step()

        # Оценка на тесте
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for inputs, labels in test_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                outputs = model(inputs)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

        epoch_time = time.time() - start_time
        test_acc = 100 * correct / total

        history['train_loss'].append(running_loss / len(train_loader))
        history['test_acc'].append(test_acc)
        history['train_time'].append(epoch_time)

        print(f'Epoch [{epoch+1}/{num_epochs}] | Loss: {running_loss/len(train_loader):.4f} | '
              f'Acc: {test_acc:.2f}% | Time: {epoch_time:.2f}s')

    return model, history

# ---------------------------------------------------------
# TODO: Обучите модель с нуля
# ---------------------------------------------------------
print("=" * 50)
print("ЭКСПЕРИМЕНТ 1: Обучение с нуля на CIFAR-10")
print("=" * 50)

model_scratch = CustomCNN(num_classes=10)
model_scratch, history_scratch = train_model(model_scratch, train_loader, test_loader,
                                              num_epochs=15, lr=0.001)

# Сохранение весов
torch.save(model_scratch.state_dict(), 'cifar10_scratch.pth')
print("Веса модели сохранены: cifar10_scratch.pth")
In [ ]:
# ---------------------------------------------------------
# TODO: Постройте графики обучения
# ---------------------------------------------------------
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# График потерь
axes[0].plot(history_scratch['train_loss'], 'b-', label='Train Loss')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].set_title('Training Loss (Scratch)')
axes[0].legend()
axes[0].grid(True)

# График точности
axes[1].plot(history_scratch['test_acc'], 'g-', label='Test Accuracy')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy (%)')
axes[1].set_title('Test Accuracy (Scratch)')
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.show()

5.2 Предобучение на CIFAR-100

In [ ]:
train_dataset_100 = torchvision.datasets.CIFAR100(root='./data', train=True,
                                                  download=True, transform=transform_train)
test_dataset_100 = torchvision.datasets.CIFAR100(root='./data', train=False,
                                                 download=True, transform=transform_test)

train_loader_100 = DataLoader(train_dataset_100, batch_size=64, shuffle=True, num_workers=2)
test_loader_100 = DataLoader(test_dataset_100, batch_size=64, shuffle=False, num_workers=2)
In [ ]:
train_dataset_100 = torchvision.datasets.CIFAR100(root='./data', train=True,
                                                  download=True, transform=transform_train)
test_dataset_100 = torchvision.datasets.CIFAR100(root='./data', train=False,
                                                 download=True, transform=transform_test)

train_loader_100 = DataLoader(train_dataset_100, batch_size=64, shuffle=True, num_workers=2)
test_loader_100 = DataLoader(test_dataset_100, batch_size=64, shuffle=False, num_workers=2)

# ---------------------------------------------------------
# TODO: Обучите модель на CIFAR-100 (Source Task)
# ---------------------------------------------------------
print("=" * 50)
print("ЭКСПЕРИМЕНТ 2A: Предобучение на CIFAR-100")
print("=" * 50)

model_pretrained = CustomCNN(num_classes=100)  # 100 классов для CIFAR-100
model_pretrained, history_pretrained = train_model(model_pretrained,
                                                    train_loader_100,
                                                    test_loader_100,
                                                    num_epochs=15, lr=0.001)

# Сохранение предобученных весов
torch.save(model_pretrained.state_dict(), 'cifar100_pretrained.pth')
print("Предобученные веса сохранены: cifar100_pretrained.pth")

5.3 Перенос знаний на CIFAR-10

In [ ]:
# ---------------------------------------------------------
# Функция загрузки предобученных весов
# ---------------------------------------------------------
def load_pretrained_for_transfer(model, pretrained_path, freeze_conv=True):
    """
    Загружает веса предобученной модели, исключая последний слой,
    и опционально замораживает сверточные слои.
    """
    pretrained_dict = torch.load(pretrained_path)
    model_dict = model.state_dict()

    # Исключаем веса выходного слоя (разная размерность: 100 vs 10)
    pretrained_dict = {k: v for k, v in pretrained_dict.items()
                       if k not in ['out.weight', 'out.bias']}

    # Обновляем словарь весов
    model_dict.update(pretrained_dict)
    model.load_state_dict(model_dict)

    # Замораживаем сверточные слои если нужно
    if freeze_conv:
        for param in model.conv1.parameters():
            param.requires_grad = False
        for param in model.conv2.parameters():
            param.requires_grad = False
        print("✅ Сверточные слои заморожены (Feature Extraction)")
    else:
        print("✅ Все слои разморожены (Fine-Tuning)")

    return model

# ---------------------------------------------------------
# TODO: Реализуйте Transfer Learning
# ---------------------------------------------------------
print("=" * 50)
print("ЭКСПЕРИМЕНТ 2B: Transfer Learning на CIFAR-10")
print("=" * 50)

# Создаём модель для CIFAR-10
model_tl = CustomCNN(num_classes=10)

# Загружаем предобученные веса
model_tl = load_pretrained_for_transfer(model_tl, 'cifar100_pretrained.pth', freeze_conv=True)

# Оптимизатор только для обучаемых параметров
optimizer_tl = optim.Adam(filter(lambda p: p.requires_grad, model_tl.parameters()), lr=0.001)

# Модифицированная функция обучения для TL
def train_model_tl(model, train_loader, test_loader, optimizer, num_epochs=10):
    model = model.to(device)
    criterion = nn.CrossEntropyLoss()

    history = {'train_loss': [], 'test_acc': []}

    for epoch in range(num_epochs):
        model.train()
        running_loss = 0.0

        for inputs, labels in train_loader:
            inputs, labels = inputs.to(device), labels.to(device)

            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            running_loss += loss.item()

        # Оценка
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for inputs, labels in test_loader:
                inputs, labels = inputs.to(device), labels.to(device)
                outputs = model(inputs)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

        test_acc = 100 * correct / total
        history['train_loss'].append(running_loss / len(train_loader))
        history['test_acc'].append(test_acc)

        print(f'Epoch [{epoch+1}/{num_epochs}] | Loss: {running_loss/len(train_loader):.4f} | Acc: {test_acc:.2f}%')

    return model, history

# Обучение с Transfer Learning
model_tl, history_tl = train_model_tl(model_tl, train_loader, test_loader,
                                       optimizer_tl, num_epochs=10)

torch.save(model_tl.state_dict(), 'cifar10_transfer.pth')

5.4 Итоги

In [ ]:
# Стратегия 1: Feature Extraction (свертки заморожены)
model_fe = CustomCNN(num_classes=10)
model_fe = load_pretrained_for_transfer(model_fe, 'cifar100_pretrained.pth', freeze_conv=True)
optimizer_fe = optim.Adam(filter(lambda p: p.requires_grad, model_fe.parameters()), lr=0.001)
model_fe, history_fe = train_model_tl(model_fe, train_loader, test_loader, optimizer_fe, num_epochs=10)

# Стратегия 2: Fine-Tuning (все слои обучаются)
model_ft = CustomCNN(num_classes=10)
model_ft = load_pretrained_for_transfer(model_ft, 'cifar100_pretrained.pth', freeze_conv=False)
optimizer_ft = optim.Adam(model_ft.parameters(), lr=0.0001)  # Меньший LR для Fine-Tuning
model_ft, history_ft = train_model_tl(model_ft, train_loader, test_loader, optimizer_ft, num_epochs=10)
In [ ]:
# ---------------------------------------------------------
# TODO: Постройте сравнительные графики
# ---------------------------------------------------------
fig, axes = plt.subplots(1, 2, figsize=(15, 5))

# Сравнение точности
axes[0].plot(history_scratch['test_acc'], 'r-', label='Scratch', linewidth=2)
axes[0].plot(history_tl['test_acc'], 'b-', label='Transfer (Frozen)', linewidth=2)
axes[0].plot(history_fe['test_acc'], 'g--', label='Feature Extraction', linewidth=2)
axes[0].plot(history_ft['test_acc'], 'm--', label='Fine-Tuning', linewidth=2)
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Test Accuracy (%)')
axes[0].set_title('Сравнение стратегий обучения')
axes[0].legend()
axes[0].grid(True)

# Сравнение потерь
axes[1].plot(history_scratch['train_loss'], 'r-', label='Scratch', linewidth=2)
axes[1].plot(history_tl['train_loss'], 'b-', label='Transfer (Frozen)', linewidth=2)
axes[1].plot(history_fe['train_loss'], 'g--', label='Feature Extraction', linewidth=2)
axes[1].plot(history_ft['train_loss'], 'm--', label='Fine-Tuning', linewidth=2)
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Train Loss')
axes[1].set_title('Сравнение функций потерь')
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.show()