Прочитав несколько известных статей по сегментации спутниковых снимков земли, я решил попробовать создать и обучить свою модель нейросети для этой задачи. И конечно, в процессе возникало много вопросов, своими ответами на которые я решил поделиться в рамках этого туториала. Поделиться так подробно и просто, как это было бы понятно таким новичкам, как я.
Идея
Одним из главных источников идеи и подхода для создания и обучения моей нейросети стала статья LULC Segmentation of RGB Satellite Image Using FCN-8 [перевод на русский]: берем датасет спутниковых изображений земли Gaofen Image Dataset → разделяем изображения на подизображения, вместо уменьшения и аугментации исходных изображений: так она будет лучше обучаться → определяем модель нейросети с архитектурой Fully Convolutional Network (FCN, FCN-8) → обучаем ее. Единственное, вместо модели FCN-8 я решил выбрать доступную в PyTorch предобученную модель ResNet101, написанную по архитектуре FCN (FCN.ResNet101). Также вместо большой версии датасета (Large) я выбрал упрощенную версию (Fine) — она включает в себя 10 оригинальных и сегментированных по 15 классам изображений в формате tif и размером 7200x6800. Модель FCN.ResNet101 предполагает размер входных изображений 224x224, а так-как размер исходных изображений чуть больше, чем может вместиться целое кол-во подизображений размера 224x224, мы обрезаем исходные изображения до 7168x6720 (именно обрезаем, чтобы избежать потери детализации); таким образом, у нас получается 960 подизображений с одного исходного изображения.
Приступим к реализации этой идеи!
Импорты
Импортируем все необходимые библиотеки:
import torch
import torchvision
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from sklearn.model_selection import train_test_split
from os import listdir
from typing import Tuple, List, Dict, Generator, Any
from IPython.display import clear_output
Датасет
Сайт датасета находится по этой ссылке, а две версии датасета (Fine и Large) на OneDrive. Но здесь я буду загружать его со своего Google Drive.
Загрузка
Если вы пользуетесь не Google Colab, вам может понадобиться установить модуль gdown:
pip3 install gdown
Загрузим архивы датасета:
%%capture # clear output
!gdown "https://drive.google.com/uc?id=1--fNMFRXmBRDQPwdFqh54Sx7_RNE6z4a&confirm=t" # download image_RGB.zip
!gdown "https://drive.google.com/uc?id=1-3y-bJK-QZapo3nwZEXdVLWDjkVO4AYN&confirm=t" # download label_15classes.zip
И разархивируем их:
%%capture
!unzip image_RGB.zip
!unzip label_15classes.zip
Организация
Чтобы не нагружать оперативную память всеми подизображениями, можно сохранить каждое как файл и организовать их подгрузку. Сделаем это, но перед этим определимся с классами.
Упрощенная версия датасета, как уже было сказано, содержит сегментированные по 15 классам изображения. Их описание можно посмотреть в readme.txt на OneDrive:
label information of 15 classes:
industrial land
RGB: 200, 0, 0
urban residential
RGB: 250, 0, 150
rural residential
RGB: 200, 150, 150
traffic land
RGB: 250, 150, 150
paddy field
RGB: 0, 200, 0
irrigated land
RGB: 150, 250, 0
...
И так-как в вышеупомянутой статье модели создавали и обучали для сегментации одного целевого класса, выберем один из них, например, Irrigated Land: он достаточно хорошо представлен на изображениях датасета, а потому с ним будет достаточно просто. Но также важно добавить еще один класс — Background, так-как он важен для более корректного обучения модели.
Инициализируем словари с классами — classes
и classes_by_id
(последний пригодится для одной из функций далее):
classes = {
(0, 0, 0): (0, '__background__'),
(150, 250, 0): (1, 'irrigated_land')
}
classes_by_id = dict()
for rgb, (id, name) in classes.items():
classes_by_id[id] = (rgb, name)
Теперь подизображения. Напишем функцию, которая будет возвращать генератор подизображений из исходного изображения:
def get_subimages_generator(
image: Image.Image,
subimage_size: Tuple[int, int, int]
) -> Generator[Image.Image, None, None]:
for r in range(image.size[1] // subimage_size[1]):
for c in range(image.size[0] // subimage_size[0]):
yield image.crop(box=(
c * subimage_size[0],
r * subimage_size[1],
(c + 1) * subimage_size[0],
(r + 1) * subimage_size[1]
)
)
Создадим директорию dataset с директориями для каждого типа подизображений изображений датасета (оригинальные и сегментированные):
!mkdir dataset dataset/originals dataset/labeleds
Наконец, напишем функцию, которая будет сохранять все подизображения в соответствующие директории под строковыми id (они понадобятся нам далее). Функция будет проходится по каждым сегментированным подизображениям, фильтровать их по минимальным процентам представленности определенного цвета и, собственно, сохранять:
def save_dataset_subimages(classes_filter: Dict[Tuple[int, int, int], float]):
for i, filename in enumerate(listdir('image_RGB/')):
basename = filename[:filename.find('.tif')]
image = Image.open(fp=f'image_RGB/{basename}.tif').crop(box=(16, 40, 7200 - 16, 6800 - 40))
image_labeled = Image.open(fp=f'label_15classes/{basename}_label.tif').crop(box=(16, 40, 7200 - 16, 6800 - 40))
subimages = get_subimages_generator(image=image, subimage_size=(224,224))
subimages_labeleds = get_subimages_generator(image=image_labeled, subimage_size=(224,224))
for si, subimage in enumerate(subimages):
subimage_labeled = next(subimages_labeleds)
# classes filter
do_continue = False
subimage_labeled_colors_dict = {rgb: count for count, rgb in subimage_labeled.getcolors()}
for rgb, min_percent in classes_filter.items():
if rgb not in subimage_labeled_colors_dict \
or subimage_labeled_colors_dict[rgb] * 100 / 50176 < min_percent:
# 50176 = subimage width * subimage height
do_continue = True
break
if do_continue:
continue
subimage.save(fp=f'dataset/originals/i{i}si{si}.tif')
subimage_labeled.save(fp=f'dataset/labeleds/i{i}si{si}_labeled.tif')
Задействуем ее:
save_dataset_subimages(
classes_filter={
(150, 250, 0): 5
}
)
Команда ниже поможет нам узнать, сколько всего оригинальных, а значит и сегментированных, подизображений было сохранено:
!ls -lR dataset/originals/*.tif | wc -l
Всего для целевого класса Irrigated Land сохранено 4166 оригинальных, а значит и сегментированных, подизображений.
Вспомогательные функции и класс датасета
Для подгрузки и использования подизображений необходимо определить несколько функций и класс датасета, которые будут их обрабатывать и представлять так, как это будет удобно для работы с моделью. А для этого необходимо представлять оригинальные и сегментированные подизображения в виде тензоров (типе torch.Tensor
), причем для большинства моделей, в том числе и для FCN.ResNet101, размерность тензоров должна предполагать наличие партий (batches): (batches, width, height, channels), где width, height и channels — ширина, высота и кол-во каналов изображения соответственно, в нашем случае — 224, 224 и 3 (R, G, B) соответственно. В случае с сегментированными изображениями, оно должно быть представлено еще и в размерности (batches, classes, width, height), где classes — количество сегментируемых классов, в нашем случае — 2; то есть должно представлять из себя маску, где для каждого класса указывается отдельная маска 224x224, которая указывает 0 или 1, на каком пикселе присутствует определенный класс, а на каком нет. Собственно, эти значения (0 или 1) для каждого пикселя каждой маски модель для семантической сегментации и должна предсказывать, оперируя вероятностью от 0 до 1.
Наконец, приступим к коду. Определим функции чтобы…
собирать два соответствующих подизображения (оригинальное и сегментированное) по их строковым id, которые были определены при их сохранении:
def get_dataset_subimage(dataset_subimage_id: str) -> Tuple[Image.Image, Image.Image]:
subimage = Image.open(fp=f'dataset/originals/{dataset_subimage_id}.tif')
subimage_labeled = Image.open(fp=f'dataset/labeleds/{dataset_subimage_id}_labeled.tif')
return subimage, subimage_labeled
получать маску из сегментированного изображения и наоборот:
В первой функции проходимся по каждому пикселю сегментированного изображения и присваиваем маске для определенного класса определенного пикселя маски значение 1.0, при этом, если для пикселя не находится класса, относим его к классу Background. Получаем массив размерностью (classes, width, height)
:
def get_image_mask_from_labeled(
image_labeled: Image.Image,
classes: Dict[Tuple[int, int, int], Tuple[int, str]]
) -> np.ndarray:
image_mask = np.zeros(shape=(len(classes),image_labeled.size[0],image_labeled.size[1]))
image_labeled_ndarray = np.array(object=image_labeled)
for r in np.arange(stop=image_labeled_ndarray.shape[0]):
for c in np.arange(stop=image_labeled_ndarray.shape[1]):
class_rgb = tuple(image_labeled_ndarray[r][c])
class_value = classes.get(class_rgb)
if class_value != None:
image_mask[class_value[0]][r][c] = 1.0
else:
image_mask[0][r][c] = 1.0
return image_mask
Во второй функции получаем массив из индексов классов с максимальными вероятностями среди других классов для каждого пикселя, а затем присваиваем каждому пикселю выходного изображения цвет в соответствии с его классом. То есть, обратно получаем массив размерностью (width, height, channels)
:
def get_image_labeled_from_mask(
image_mask: np.ndarray,
classes_by_id: Dict[Tuple[int, int, int], Tuple[int, str]]
) -> Image.Image:
image_labeled_ndarray = np.zeros(
shape=(image_mask.shape[1],image_mask.shape[2],3),
dtype=np.uint8
)
image_mask_hot = image_mask.argmax(axis=0)
for r in np.arange(stop=image_mask_hot.shape[0]):
for c in np.arange(stop=image_mask_hot.shape[1]):
class_id = image_mask_hot[r][c]
class_by_id_value = classes_by_id.get(class_id)
image_labeled_ndarray[r][c] = np.array(object=class_by_id_value[0])
image_labeled = Image.fromarray(obj=image_labeled_ndarray)
return image_labeled
чтобы предобрабатывать изображения (переводить в тензор):
def image_preprocess(image: Image.Image) -> torch.Tensor:
return torchvision.transforms.ToTensor()(pic=image)
переводить два соответствующих подизображения в тензоры:
def get_dataset_subimage_tensor(
subimage: Image.Image,
subimage_labeled: Image.Image,
classes: Dict[Tuple[int, int, int], Tuple[int, str]],
dtype: torch.FloatType = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
subimage_tensor = image_preprocess(image=subimage)
subimage_mask_tensor = torch.tensor(
data=get_image_mask_from_labeled(
image_labeled=subimage_labeled,
classes=classes
),
dtype=dtype
)
return subimage_tensor, subimage_mask_tensor
Проверим, как они работают. Загружаем два соответствующих подизображения по их строковому id:
subimage, subimage_labeled = get_dataset_subimage(dataset_subimage_id='i0si0')
Выводим их с помощью модуля matplotlib.pyplot
:
fig, ax = plt.subplots(ncols=2)
ax[0].imshow(subimage)
ax[1].imshow(subimage_labeled)
plt.show()
Проверим корректность обработки масок. Переводим сегментированное подизображение в маску:
subimage_mask = get_image_mask_from_labeled(
image_labeled=subimage_labeled,
classes=classes
)
Переводим маску обратно в сегментированное подизображение:
subimage_labeled_from_mask = get_image_labeled_from_mask(
image_mask=subimage_mask,
classes_by_id=classes_by_id
)
Сравниваем оригинальное подизображение с сегментированным из маски:
fig, ax = plt.subplots(ncols=2)
ax[0].imshow(subimage)
ax[1].imshow(subimage_labeled_from_mask)
plt.show()
Работает отлично!
Осталось только определить специальный класс датасета (с помощью абстрактного класса torch.utils.data.Dataset
), который вместе с даталоадером (типом torch.utils.data.DataLoader
) помогут удобно подгружать подизображения. И чтобы все они не занимали много памяти, в самом классе датасета мы определим их строковые id, а по запросу на получение элемента (вызову перегруженного метода __getitem__
) будем уже загружать их, переводить в тензоры и возвращать. При этом важно создать несколько датасетов — обучающий и тестовый.
Но перед этим, нам нужно собрать все id подизображений:
def get_dataset_subimages_id() -> List[str]:
return [
filename[:filename.find('.tif')]
for filename in listdir(path='dataset/originals/')
]
Теперь разобъем список id на обучающий и тестовый. Для этого используем функцию train_test_split
из модуля sklearn
, которая поможет разбить их на обучающую и тестовую выборки, где длина обучающей выборки будет составлять 90%, а тестовой — 10%:
train_dataset_subimages_id, test_dataset_subimages_id = train_test_split(get_dataset_subimages_id(), train_size=0.9)
Наконец, определим класс датасета. Дополнительно для даталоадера нам понадобится перегрузить метод __len__
(для получение длины датасета) — в качестве длины датасета в нашем случае будет выступать длина списка id:
class Dataset(torch.utils.data.Dataset):
def __init__(self,
dataset_subimages_id: List[str],
classes: Dict[Tuple[int, int, int], Tuple[int, str]],
):
self.dataset_subimages_id = dataset_subimages_id
self.classes = classes
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
subimage = Image.open(fp=f'dataset/originals/{self.dataset_subimages_id[idx]}.tif')
subimage_labeled = Image.open(fp=f'dataset/labeleds/{self.dataset_subimages_id[idx]}_labeled.tif')
subimage_tensor, subimage_mask_tensor = get_dataset_subimage_tensor(
subimage=subimage,
subimage_labeled=subimage_labeled,
classes=self.classes
)
return subimage_tensor, subimage_mask_tensor
def __len__(self) -> int:
return len(self.dataset_subimages_id)
Используем вышеописанный класс для создания датасетов:
train_dataset = Dataset(
dataset_subimages_id=train_dataset_subimages_id,
classes=classes,
)
test_dataset = Dataset(
dataset_subimages_id=test_dataset_subimages_id,
classes=classes
)
Даталоадеры удобны тем, что с помощью них можно удобно разбивать датасет на партии и перемешивать. Оба этих параметра оказываются достаточно важными при обучении. Для нашей модели наиболее подходящим для обучающей выборки, по результатам моих экспериментов, оказывается batch_size=32
и shuffle=True
.
train_dataloader = torch.utils.data.DataLoader(
dataset=train_dataset,
batch_size=32,
shuffle=True
)
test_dataloader = torch.utils.data.DataLoader(
dataset=test_dataset,
batch_size=1
)
Модель
Можно было бы написать свой класс модели с помощью абстрактного класса torch.nn.Module
, однако, как уже говорилось в начале, я решил использовать готовую модель с предобученными на датасете ImageNet1K весами из модуля torchvision
— FCN.ResNet101. Причем, c предобученными весами не всей модели, а только слоев backbone’а (так называемых извлекателей признаков), которые изображены на рисунке архитектуры FCN ниже между изображением котика с собачкой и слоя pixelwise prediction.
Но для начала, нужно инициализировать переменную device
, которая поможет нам автоматически определять, на какой оперативной памяти (CPU или GPU) она будет находится и на какой оперативной памяти будут проводится вычисления нейросети. При обучении нейросети, особенно с большой архитектурой, важно пользоваться преимуществами распараллеливания вычислений на GPU, чтобы модель быстрее обрабатывала входные данные:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
Наконец, инициализируем модель с помощью модуля torchvision
и определим кол-во выходных классов и веса backbone’а:
%%capture
model = torchvision.models.segmentation.fcn_resnet101(
num_classes=len(classes),
weights_backbone=torchvision.models.ResNet101_Weights.IMAGENET1K_V1
).to(device=device)
Напишем функцию для предсказывания по входному изображению:
def predict(
image: Image.Image,
model: torch.nn.Module,
device: torch.DeviceObjType,
) -> Image.Image:
image_tensor = image_preprocess(image=image)
with torch.no_grad():
output_image_mask = model(image_tensor.unsqueeze(0).to(device))['out'][0].cpu().numpy()
predicted_image_labeled = get_image_labeled_from_mask(
image_mask=output_image_mask,
classes_by_id=classes_by_id
)
return predicted_image_labeled
И проверим ее:
predicted_image_labeled = predict(
image=subimage,
model=model,
device=device
)
fig, ax = plt.subplots(ncols=3)
ax[0].imshow(subimage)
ax[1].imshow(predicted_image_labeled)
ax[2].imshow(subimage_labeled)
plt.show()
Хорошо, она на что-то способна.
Обучение и тестирование модели
Метрики
При обучении и тестировании модели важно пользоваться различными метриками, чтобы следить за ее развитием. И если метрику потерь мы получаем из функции потерь (ее мы определим после), то в модулях torch
и torchvision
таких важных для семантической сегментации метрик как Pixel Accuracy и Intersection over Union (IoU) не содержится. Хотя они есть в torchmetrics
, но я решил написать свои версии Pixel Accuracy и IoU, чтобы понять, что они из себя представляют. Хороший и простой обзор по этим и другим метрикам для задач сегментации провел Джереми Джордан на своем сайте.
Pixel Accuracy
Альтернативной метрикой для оценки семантической сегментации является простой отчет о проценте пикселей на изображении, которые были правильно классифицированы. Точность пикселей обычно указывается для каждого класса отдельно, а также глобально для всех классов. (Джереми Джордан)
Мы можем вычислить ее с помощью формулы:
Напишем функцию для вычисления метрики Pixel Accuracy. Причем, определим классификацированные пиксели так, как мы бы их отрисовывали, то есть с помощью получения масива из индексов классов с максимальными вероятностями среди других классов для каждого пикселя:
def metric_pixel_accuracy(
y_pred: torch.Tensor,
y_true: torch.Tensor
) -> float:
y_pred_argmax = y_pred.argmax(dim=1)
y_true_argmax = y_true.argmax(dim=1)
correct_pixels = (y_pred_argmax == y_true_argmax).count_nonzero()
uncorrect_pixels = (y_pred_argmax != y_true_argmax).count_nonzero()
result = (correct_pixels / (correct_pixels + uncorrect_pixels)).item()
return result
IoU
Метрика Intersection over Union (IoU), также называемая индексом Жаккара, по сути, является методом количественной оценки процентного перекрытия между целевой маской и нашим прогнозируемым результатом. Эта метрика тесно связана с коэффициентом Дайса, который часто используется в качестве функции потерь во время обучения. (Джереми Джордан)
Ее мы можем вычислить с помощью формулы:
Наишем функцию для вычисления метрики IoU. Определим классифицированными только те пиксели, вероятность которых выше 0.51 (51%):
def metric_iou(
y_pred: torch.Tensor,
y_true: torch.Tensor
) -> float:
y_pred_hot = y_pred >= 0.51
intersection = torch.logical_and(y_pred_hot, y_true).count_nonzero()
union = torch.logical_or(y_pred_hot, y_true).count_nonzero()
result = (intersection / union).item()
return result
Обучение
Определим функцию для обучения, которая будет подгружать подизображения, подавать модели входные тензоры и получать выходные, вычислять на их основе потерю, оптимизировать обучение, вычислять и выводить историю метрик по каждой партии, и все это определенное кол‑во эпох. Причем, чтобы оперативная память не перегружалась мы будем удалять уже не нужные громоздкие переменные и очищать оперативную память графического процессора, если мы его используем:
def train(
model: torch.nn.Module,
device: torch.DeviceObjType,
train_dataloader: torch.utils.data.DataLoader,
loss_fn: Any,
optim_fn: Any,
epochs: int
) -> Dict[str, List[float]]:
history_metrics = {
'loss': list(),
'pixel_accuracy': list(),
'iou': list()
}
for e in range(1, epochs + 1):
for b, data in enumerate(train_dataloader, start=1):
subimage_tensor, subimage_mask_tensor = data
if device.type == 'cuda':
subimage_tensor = subimage_tensor.to(device)
subimage_mask_tensor = subimage_mask_tensor.to(device)
optim_fn.zero_grad()
output = model(subimage_tensor)
loss = loss_fn(output['out'], subimage_mask_tensor)
loss.backward()
optim_fn.step()
loss_item = loss.item()
pixel_accuracy = metric_pixel_accuracy(output['out'], subimage_mask_tensor)
iou = metric_iou(output['out'], subimage_mask_tensor)
history_metrics['loss'].append(loss_item)
history_metrics['pixel_accuracy'].append(pixel_accuracy)
history_metrics['iou'].append(iou)
# dynamic output
clear_output()
print(
'Epoch: {}. Batch: {}. Loss: {:.3f} | Pixel Accuracy: {:.3f} | IoU: {:.3f}'.format(
e, b,
loss, pixel_accuracy, iou
)
)
# memory clear
del subimage_tensor, subimage_mask_tensor, output, loss
if device.type == 'cuda':
torch.cuda.empty_cache()
return history_metrics
Определим также функции потерь и оптимизации, а также кол‑во эпох. Для нашей модели и для большинства моделей нейросетей они являются необходимыми, ведь они помогают проводить и оптимизировать обучение. В качестве функции потерь я выбрал популярную для задач семантической сегментации CrossEntropyLoss (torch.nn.CrossEntropyLoss
). В качестве функции оптимизации — AdamW (torch.optim.AdamW
), и важно определиться с ее параметрами: по результатам моих экспериментов, наиболее подходящим начальным коэффициентом обучения (learning rate, lr
) оказывается 0.0001 (1e-4). Наиболее подходящее кол‑во эпох, опять‑же, по результатам моих экспериментов, оказывается 8, ведь по моим наблюдениям дальше он перестает обучаться, хотя в упомянутом в начале исследовании использовали целых 100 эпох, но для своей версии датасета.
Все готово к обучению. Запустим его:
history_metrics = train(
model=model,
device=device,
train_dataloader=train_dataloader,
loss_fn=torch.nn.CrossEntropyLoss(),
optim_fn=torch.optim.AdamW(params=model.parameters(), lr=1e-4),
epochs=8
)
Этот процесс оказывается достаточно долгим даже при использовании GPU в Google Colab. Возможно, часть кода оказывается недостаточно эффективным, поэтому будет очень хорошо, если вы сможете оптимизировать его для своих задач.
Исследование обучения
Выведем историю метрик с помощью matplotlib.pyplot
:
plt.plot(
history_metrics['loss'], 'red',
history_metrics['pixel_accuracy'], 'green',
history_metrics['iou'], 'blue',
)
plt.title('History Metrics in Training\nep=8, bs=32, loss_fn=CrossEntopyLoss(), optim_fn=AdamW(lr=1e-4)')
plt.xlabel('Batch')
plt.ylabel('Value')
plt.legend(('Loss', 'Pixel Accuracy', 'IoU'))
plt.show()
Хотя развитие модели видно достаточно явно: потери уменьшаются, метрики Pixel Accuracy и IoU увеличиваются, а значит точность и предсказания увеличивается. Однако показатели представляются для меня слишком колеблющимися. Это может значит, что модель после каждой партии модель проводит слишком резкие изменения. Все это можно регулировать с помощью дополнительной обработки датасета, изменения размера партий (batch_size
), выбора определенных функций потерь, параметров функции оптимизации (learning rate и других) и т. п. Все это входит в спектр моего дальнейшего изучения, поэтому, если вы в этом хорошо разбираетесь, я буду рад, если вы поделитесь со мной своими знаниями.
Сохранение модели
Сохраним модель с помощью функции save
из модуля torch
в формате h5:
torch.save(
model.state_dict(),
'remezova_fcn_resnet101_ep8bs32lr1e-4.h5'
)
Тестирование
Напишем похожую на train
функцию test
, только без элементов обучения и c выводом медианы метрик из истории метрик с каждой партией:
def test(
model: torch.nn.Module,
device: torch.DeviceObjType,
test_dataloader: torch.utils.data.DataLoader,
loss_fn: Any,
) -> Dict[str, List[float]]:
history_metrics = {
'pixel_accuracy': list(),
'iou': list()
}
for b, data in enumerate(test_dataloader, start=1):
subimage_tensor, subimage_mask_tensor = data
if device.type == 'cuda':
subimage_tensor = subimage_tensor.to(device)
subimage_mask_tensor = subimage_mask_tensor.to(device)
with torch.no_grad():
output = model(subimage_tensor)
pixel_accuracy = metric_pixel_accuracy(output['out'], subimage_mask_tensor)
iou = metric_iou(output['out'], subimage_mask_tensor)
history_metrics['pixel_accuracy'].append(pixel_accuracy)
history_metrics['iou'].append(iou)
clear_output()
print(
'Batch: {}. median Pixel Accuracy: {:.3f} | median IoU: {:.3f}'.format(
b,
np.median(a=history_metrics['pixel_accuracy']),
np.median(a=history_metrics['iou'])
)
)
# memory clear
del subimage_tensor, subimage_mask_tensor, output
if device.type == 'cuda':
torch.cuda.empty_cache()
return history_metrics
Проведем тестирование:
test_history_metrics = test(
model=model,
device=device,
test_dataloader=test_dataloader,
loss_fn=torch.nn.CrossEntropyLoss()
)
И тем не менее получаем достаточно хороший результат:
Batch: 417. median Pixel Accuracy: 0.703 | median IoU: 0.523
Визуализируем результаты предсказаний. Выведем предсказания по пяти тестовым подизображениям:
fig, ax = plt.subplots(nrows=3, ncols=5, figsize=(16,8))
for si, (subimage_tensor, subimage_mask_tensor) in enumerate(test_dataloader):
subimage = torchvision.transforms.ToPILImage()(pic=subimage_tensor[0])
subimage_labeled = get_image_labeled_from_mask(
image_mask=subimage_mask_tensor[0].cpu().numpy(),
classes_by_id=classes_by_id
)
predicted_subimage_labeled = predict(
image=subimage,
model=model,
device=device
)
ax[0][si].imshow(subimage)
ax[1][si].imshow(predicted_subimage_labeled)
ax[2][si].imshow(subimage_labeled)
if si == 5 - 1:
break
plt.show()
Заключение
Мы подготовили датасет, создали, обучили и протестировали модель для сегментации спутниковых снимков. Не смотря на некоторые недостатки, модель показывает достаточно хорошие результаты. Однако этот туториал и весь код будут продолжать изменяться или дополняться с развитием моих знаний или ваших рекомендаций.
Этот туториал вышел достаточно большим, но я надеюсь, я смог достаточно хорошо ответить на те вопросы по нейросетям для семантической сегментации, которые, возможно, возникли и у вас; особенно, если вы такой же новичок, как и я.
Также вы можете воспользоваться более структурированным кодом этой модели в репозитории Remezova на GitHub. ❤️