На предыдущих занятиях
мы с вами подробно разобрали принцип работы сверточной нейронной сети и классы Conv2d, MaxPool2d, с помощью
которых реализуются сверточные слои при работе с изображениями (двумерными
пространственными сигналами). Пришло время применить эти знания, и сделаем это
на примере определения координат центра солнечного диска на изображениях:
Для этого мы с
вами сформируем сверточную НС следующей архитектуры:
На вход будем
подавать полноцветное изображение размером 256 x 256 пикселей в
формате RGB (три цветовых
канала). То есть, входной тензор будет иметь размерность:
(batch, 3, 256, 256)
Выходные
значения карт признаков каждого сверточного слоя пропускаются через функцию
активации ReLU, затем,
выделяются только значимые величины с помощью слоя MaxPooling с
непересекающимися окнами 2x2, то есть, линейные размеры карт
признаков будут уменьшаться в два раза. И так реализуем три сверточных слоя.
Последние выходные значения вытягиваются в вектор (по батчам) с помощью слоя Flatten. В результате
на вход первого полносвязного слоя будет подаваться тензор размерностью:
(batch, count_element)
А на последнем
слое будут формироваться вещественные значения координат (x, y) центра
солнечного диска, присутствующего на изображении.
Выборки изображений и класс SunDataset
Далее нам
потребуются обучающая и тестовая выборки. Они сформированы заранее и
представлены следующим образом:
В каждом
каталоге имеется файл format.json со следующим
содержимым:
{
"sun_reg_1.png": [113, 131],
"sun_reg_2.png": [123, 185],
…
}
Здесь для
каждого изображения указаны координаты центра солнца, которые являются целевыми
значениями при обучении и тестировании НС.
Эту обучающую
выборку можно сгенерировать с помощью скрипта:
https://github.com/selfedu-rus/neuro-pytorch/blob/main/dataset_gen_reg.zip
Для работы с
этой выборкой объявим класс SunDataset следующим образом:
class SunDataset(data.Dataset):
def __init__(self, path, train=True, transform=None):
self.path = os.path.join(path, "train" if train else "test")
self.transform = transform
with open(os.path.join(self.path, "format.json"), "r") as fp:
self.format = json.load(fp)
self.length = len(self.format)
self.files = tuple(self.format.keys())
self.targets = tuple(self.format.values())
def __getitem__(self, item):
path_file = os.path.join(self.path, self.files[item])
img = Image.open(path_file).convert('RGB')
if self.transform:
img = self.transform(img)
return img, torch.tensor(self.targets[item], dtype=torch.float32)
def __len__(self):
return self.length
Здесь все должно
быть вам уже знакомо и понятно. Далее создается объект этого класса вместе с
классом DataLoader, например,
следующим образом:
transforms = tfs.Compose([tfs.ToImage(), tfs.ToDtype(torch.float32, scale=True)])
d_train = SunDataset("dataset_reg", transform=transforms)
train_data = data.DataLoader(d_train, batch_size=32, shuffle=True)
Все обучающая выборка
готова к использованию.
Реализация сверточной НС
Следующим шагом
создадим модель НС и, так как все слои отрабатывают последовательно, то опишем
ее с помощью класса Sequential:
model = nn.Sequential(
nn.Conv2d(3, 32, 3, padding='same'),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 8, 3, padding='same'),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(8, 4, 3, padding='same'),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(4096, 128),
nn.ReLU(),
nn.Linear(128, 2)
)
Первый
сверточный слой на входе принимает трехканальное изображение, анализирует его
32 различными ядрами размером 3x3 каждое с добавлением нулевых отсчетов (padding='same') так, чтобы
выходные карты признаков по размеру были равны входному сигналу, то есть, 256x256 элементов.
Затем, полученные значения пропускаются через функцию активации ReLU и слой MaxPool2d с
непересекающимися окнами 2x2. На выходе первого слоя MaxPool2d получаем тензор
размерностью:
(batch, 32, 128, 128)
Этот тензор
подается на следующие три таких же слоя. На выходе получаем тензор:
(batch, 8, 64, 64)
А на последней
свертке – тензор:
(batch, 4, 32, 32)
Слой Flatten вытягивает
последние три размерности в один вектор и получается тензор:
(batch, 4096)
В таком виде он
поступает на первый полносвязный слой со 128 нейронами и функцией активации ReLU. Последний
выходной полносвязный слой выдает два значения – координаты (x, y).
У вас здесь
может возникнуть вопрос, почему в этой сети именно три сверточных слоя? Почему
именно такое количество фильтров в каждом из них? Почему первый полносвязный
слой имеет 128 нейронов, а не какое-то другое? На все эти вопросы нет точного,
четкого ответа. Как мы с вами уже говорили, структура НС определяется самим
разработчиком, исходя из его опыта и поставленной задачи. Мне показалось, что
можно попробовать именно такую. А вы, в качестве домашнего задания, попробуйте
ее упростить с сохранением итогового результата (качества ее работы).
Обучение сверточной нейронной сети
Итак, данные
готовы, модель создана. Теперь можно приступать к ее обучению. Для этого
воспользуемся оптимизатором Adam (наиболее часто применяемым при
обучении НС) и среднеквадратичной функцией потерь (так как мы решаем задачу
регрессии):
optimizer = optim.Adam(params=model.parameters(), lr=0.001, weight_decay=0.001)
loss_function = nn.MSELoss()
Обратите
внимание, что в оптимизаторе присутствует параметр weight_decay=0.001, который
добавляет L2-регуляризацию
для оптимизируемых параметров с целью снижения эффекта переобучения модели.
Далее, зададим
число эпох, переведем модель в режим обучения и запишем главный цикл обучения в
следующем виде:
epochs = 5
model.train()
for _e in range(epochs):
loss_mean = 0
lm_count = 0
train_tqdm = tqdm(train_data, leave=True)
for x_train, y_train in train_tqdm:
predict = model(x_train)
loss = loss_function(predict, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
lm_count += 1
loss_mean = 1/lm_count * loss.item() + (1 - 1/lm_count) * loss_mean
train_tqdm.set_description(f"Epoch [{_e+1}/{epochs}], loss_mean={loss_mean:.3f}")
Я не стал здесь
использовать выборку валидации, т.к., во-первых, всего 5 эпох и этого мало,
чтобы делать какие-либо выводы по графикам, и, во-вторых, для ускорения
процесса обучения.
После обучения
сохраним состояние сети, чтобы не обучать ее повторно:
st = model.state_dict()
torch.save(st, 'model_sun.tar')
и выполним
тестирование по отложенной выборке:
d_test = SunDataset("dataset_reg", train=False, transform=transforms)
test_data = data.DataLoader(d_test, batch_size=50, shuffle=False)
# тестирование обученной НС
Q = 0
count = 0
model.eval()
test_tqdm = tqdm(test_data, leave=True)
for x_test, y_test in test_tqdm:
with torch.no_grad():
p = model(x_test)
Q += loss_function(p, y_test).item()
count += 1
Q /= count
print(Q)
После обучения и
тестирования сети в консоли увидим следующие строчки:
Epoch
[1/5], loss_mean=2705.431:
100%|██████████|
313/313 [03:54<00:00, 1.34it/s]
Epoch
[2/5], loss_mean=31.895:
100%|██████████|
313/313 [03:42<00:00, 1.40it/s]
Epoch
[3/5], loss_mean=18.037: 100%|██████████|
313/313 [03:36<00:00, 1.45it/s]
Epoch
[4/5], loss_mean=14.064:
100%|██████████|
313/313 [03:38<00:00, 1.43it/s]
Epoch
[5/5], loss_mean=11.095:
100%|██████████|
313/313 [03:43<00:00, 1.40it/s]
100%|██████████|
20/20 [00:16<00:00, 1.21it/s]
9.456019759178162
Получили средний
квадрат ошибки, равный 9,46, что очень неплохо для такой сети. Кроме того,
можно попробовать еще раз запустить обучение с другими начальными значениями
весов, в надежде получить лучший результат.
Проверка модели на изображении
Но значение
среднего квадрата ошибки само по себе не особо понятно. Давайте попробуем взять
нашу обученную модель и посмотреть визуально, какие координаты для изображений
она будет выдавать. Для этого я написал следующую небольшую программу:
neuro_net_29_view.py: https://github.com/selfedu-rus/neuro-pytorch
После ее запуска
для изображения с номером 100:
увидим
результат:
Большая красная
точка на солнечном диске и есть результат работы сверточной НС (координаты x, y). Как видите, в
целом, она справляется с поставленной задачей.