Занятие 7
Лабораторная 7 (Transfer Learning)¶
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.
Задача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:
Max Pooling: Для каждой области во входной признаковой карте выбирается максимальное значение. Max pooling помогает выделить самые активные признаки из пространственных областей.
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:
Выбираем первую область 2x2 из исходной матрицы: \begin{bmatrix} 1 & 1 \\ 3 & 2 \\ \end{bmatrix} Максимальное значение здесь: 3
Выбираем вторую область 2x2 (с пропуском каждой второй строки и столбца) из исходной матрицы: \begin{bmatrix} 1 & 0 \\ 2 & 1 \\ \end{bmatrix} Максимальное значение здесь: 2
Выбираем третью область 2x2 (с пропуском каждой второй строки и столбца) из исходной матрицы: \begin{bmatrix} 3 & 2 \\ 0 & 1 \\ \end{bmatrix} Максимальное значение здесь: 3
Таким образом, после применения операции Max Pooling с окном 2x2 и шагом 2 получаем новую матрицу: \begin{bmatrix} 3 & 2 \\ 3 & 3 \\ \end{bmatrix}
Задача2 (2 балла)¶
Реализуйте функцию, принимающую на вход матрицу, свёртку для неё(размера 2 на 2) и вычисляющую результат свёртки с stride = 1.
3.Пэддинг¶
Пэддинг (padding) - это процесс добавления нулей или других значения по краям входной матрицы в глубоком обучении. Пэддинг обычно используется в сверточных нейронных сетях перед применением операции свертки или пулинга. Основная цель пэддинга состоит в том, чтобы сохранить размерность входных данных или упростить реализацию операций свертки и пулинга.
Задача3 (1 балл)¶
Реализуйте функцию, принимающую на вход матрицу и осуществяющую padding.
4.Свёрточная Нейронная сеть¶
Свёрточная нейронная сеть (Convolutional Neural Network, CNN) - это особый вид нейронных сетей, разработанный для обработки и анализа многомерных данных, таких как изображения. CNN успешно применяются в компьютерном зрении, распознавании образов, анализе временных рядов и других областях, где важна обработка многомерных данных.
Основные компоненты свёрточной нейронной сети:
- Сверточный слой (Convolutional Layer): Этот слой выполняет операцию свертки, когда ядро фильтра применяется к входным данным для извлечения признаков.
- Пулинговый слой (Pooling Layer): Данный слой уменьшает размерность пространства признаков путем объединения (например, максимум или среднее значения в каждой области).
- Полносвязные слои (Fully Connected Layers): Эти слои используются для принятия решений на основе признаков, извлеченных предыдущими слоями.
- Функции активации (Activation Functions): Обычно применяются функции активации, такие как ReLU (Rectified Linear Unit), чтобы вводить нелинейность в сеть и улучшить ее способность обобщения.
Преимущества свёрточных нейронных сетей:
- Работа с пространственной структурой данных: CNN способны учитывать пространственную локальность и общие закономерности во входных данных, таких как пиксели изображений.
- Работа с сокращенной размерностью: Пулинговые слои позволяют уменьшить размерность пространства признаков, упрощая задачу обработки.
- Способность извлекать признаки: Свёрточные слои способны извлекать локальные признаки из входных данных, позволяя сети автоматически выявлять особенности в данных.
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)
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(ядро: 5$×$5, пэддинг: 2, каналов на выходе: 6). На выходе: 28(height)$×$28(widht)$×$(6 channels)
Pooling1(ядро: 2$×$2, stride: 2). На выходе: 14(height)$×$14(widht)$×$(6 channels)
Свёртка2(ядро: 5$×$5, пэддинг: 0, каналов на выходе: 16). На выходе: 10(height)$×$10(widht)$×$(16 channels)
Pooling2(ядро: 2$×$2, stride: 2). На выходе: 5(height)$×$5(widht)$×$(16 channels)
Линейный слой1 (120 нейронов)
Линейный слой2 (84 нейрона)
Выходной слой (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.
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
device = "cuda" if torch.cuda.is_available() else "cpu"
model = LeNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
loaders = {"train": train_dataloader, "valid": valid_dataloader}
Обратите внимание! Мы используем .to(device), что означает, что мы перемещаем наши объекты на GPU и обучаемся там же.
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(ядро: 3$×$3, пэддинг: 0, каналов на выходе: 6). На выходе: ВАШ ОТВЕТ.
Pooling1(ядро: 2$×$2, stride: 2). На выходе: ВАШ ОТВЕТ.
Свёртка2(ядро: 3$×$3, пэддинг: 0, каналов на выходе: 12). На выходе: ВАШ ОТВЕТ.
Pooling2(ядро: 2$×$2, stride: 2). На выходе: ВАШ ОТВЕТ.
Линейный слой1 (256 нейронов)
Линейный слой2 (128 нейрона)
Линейный слой3 (64 нейрона)
Выходной слой (10 нейронов)
После Свёртки1 и Свёртки два следует функция активации: ReLU.
После Pooling2 следует flatten.
Сколько параметров имеет данная архитектура? ВАШ ОТВЕТ.
Если всё сделает правильно, то легко получите точность выше 60 на 10 эпохах!
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¶
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])и
# ---------------------------------------------------------
# Подготовка данных
# ---------------------------------------------------------
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")
# ---------------------------------------------------------
# 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¶
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)
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¶
# ---------------------------------------------------------
# Функция загрузки предобученных весов
# ---------------------------------------------------------
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 Итоги¶
# Стратегия 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)
# ---------------------------------------------------------
# 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()