Све свеске се међусобно надограђују, тако да нам је неопходно да поновимо неке делове из претходних свески, што без додатног објашњења чинимо у овој секцији.
Овде нам је ради анализе резултата неопходан списак класа. Такође, да би смо могли да користимо модел поновићемо поступак из ранијих свезака - дакле, увеземо неопходне библиотеке и учитамо модел. Поред тога морамо поновити цео поступак учитавања сателитског снима Србије из претходне свеске. Ово се наводи без објашњења, с обзиром да је поступак идентичан као у претходним свескама
classes = [ 'AnnualCrop',
'Forest',
'HerbaceousVegetation',
'Highway',
'Industrial',
'Pasture',
'PermanentCrop',
'Residential',
'River',
'SeaLake']
from os import environ
environ["OPENCV_IO_ENABLE_JASPER"] = "true"
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import cv2
import numpy as np
from skimage import exposure
from sklearn import metrics
from matplotlib import pyplot as plt
with torch.no_grad():
model = torchvision.models.resnet50(pretrained=True)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 10)
device = "cpu"
model.to(device)
model.load_state_dict(torch.load(r"mldata\resnet_50_land_use.pt", map_location=torch.device(device)))
model.eval()
print("Модел учитан")
img_path_b02 = r"mldata\S2A_MSIL2A_20210630T093041_N0301_R136_T34TCQ_20210630T122807.SAFE\GRANULE\L2A_T34TCQ_A031450_20210630T093134\IMG_DATA\R10m\T34TCQ_20210630T093041_B02_10m.jp2"
img_path_b03 = r"mldata\S2A_MSIL2A_20210630T093041_N0301_R136_T34TCQ_20210630T122807.SAFE\GRANULE\L2A_T34TCQ_A031450_20210630T093134\IMG_DATA\R10m\T34TCQ_20210630T093041_B03_10m.jp2"
img_path_b04 = r"mldata\S2A_MSIL2A_20210630T093041_N0301_R136_T34TCQ_20210630T122807.SAFE\GRANULE\L2A_T34TCQ_A031450_20210630T093134\IMG_DATA\R10m\T34TCQ_20210630T093041_B04_10m.jp2"
def load_sentinel_2_image_rgb(img_path_b02, img_path_b03, img_path_b04):
img_b02 = cv2.imread(img_path_b02, cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
img_b03 = cv2.imread(img_path_b03, cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
img_b04 = cv2.imread(img_path_b04, cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
rgb = np.zeros((img_b02.shape[0], img_b02.shape[1], 3))
rgb[:, :, 0] = img_b04
rgb[:, :, 1] = img_b03
rgb[:, :, 2] = img_b02
rgb = rgb.astype(np.uint16)
return rgb
image = load_sentinel_2_image_rgb(img_path_b02, img_path_b03, img_path_b04)
def prepare_rgb(image):
p_low, p_high = np.percentile(image, (1, 99))
image = exposure.rescale_intensity(image, in_range=(p_low, p_high))
image = image / image.max()
image = exposure.adjust_gamma(image, 0.9)
image += 0.05
image = np.clip(image, 0.0, 1.0)
return image
# Шабац - севернији део и река Сава
CROP_START_COL = 9584
CROP_START_ROW = 4060
CROP_SIZE = 512
# Шабац - јужнији део
# CROP_START_COL = 9423
# CROP_START_ROW = 4329
# CROP_SIZE = 512
# Рума - јужни део
# CROP_START_COL = 10383
# CROP_START_ROW = 1626
# CROP_SIZE = 512
# Јарак - цело место и меандар Саве
# CROP_START_COL = 9929
# CROP_START_ROW = 2348
# CROP_SIZE = 512
# Вештачко језеро Ровни
# CROP_START_COL = 9721
# CROP_START_ROW = 9908
# CROP_SIZE = 512
sample_image_1 = image[CROP_START_ROW:CROP_START_ROW+CROP_SIZE, CROP_START_COL:CROP_START_COL+CROP_SIZE, :]
Извршавање модела над подацима за Србију¶
Припрема података за потребе модела¶
Као што знамо, наш модел машинског учења као улаз узима слике величине 64x64, док је наш исечак величине 512x512. Да бисмо обрадили цео исечак морамо га поделити у мање исечке величине 64x64. Таквих исечака ће бити 8 по дужини и 8 по ширини, што значи укупно 64. Можда већ и назирете да ће тих 64 мањих исечака чинити хрпу коју ћемо као тавку дати мрежи и од ње добити одговарајући број предикција.
Функције испод се баве управо овом припремом. Конкретно, функција get_images дати исечак дели у скуп малих плочица величине 64x64, а функција prepare_images_for_dnn пакује ту хрпу плочица у структуру падатака адекватне величине коју вештачка неурална мрежа може да прихвати.
def get_images(image):
image = prepare_rgb(image)
rows = image.shape[0] // 64
cols = image.shape[1] // 64
all_tiles = np.zeros((rows * cols, 64, 64, 3), dtype=np.float32)
for r in range(rows):
for c in range(cols):
all_tiles[r + rows * c, ...] = image[r*64:r*64+64, c*64:c*64+64, :]
return all_tiles
def prepare_images_for_dnn(images):
images_prepared = torch.zeros((images.shape[0], 3, 224, 224))
for i in range(images.shape[0]):
image_resized = cv2.resize(images[i, ...], (224, 224))
img_normalized = image_resized.transpose(2, 0, 1).astype(np.float32)
images_prepared[i, ...] = torch.from_numpy(img_normalized)
return images_prepared
all_tiles = get_images(image[CROP_START_ROW:CROP_START_ROW+CROP_SIZE, CROP_START_COL:CROP_START_COL+CROP_SIZE, :])
tiles_prepared_for_dnn = prepare_images_for_dnn(all_tiles).to(device)
Извршавање модела¶
Модел над овако припремљеним подацима из Србије извршавамо на исти начин као и у ранијим секцијама.
with torch.no_grad():
pred = model(tiles_prepared_for_dnn)
_, argmax = torch.max(pred, 1)
argmax
with torch.no_grad():
softmaxes = F.softmax(pred, dim=1).cpu()
Анализа сигурности модела¶
Анализу сигурности модела такође спроводимо као и у ранијим поглављима, али ће овде бити јако интересантно упоредити са резултатима од раније.
all_scores = np.take_along_axis(softmaxes.numpy().T, np.expand_dims(argmax.numpy(), 0), 0)
print(all_scores)
all_scores.mean()
Самостално анализирајте просечну сигурност модела и његове предикције и упоредите је са раније добијеном сигурношћу када се модел извршавао на валидационом скупу. Дакле, у питању је идентичан модел, примењен на различитим скупом података.
Даје анализу и интерпретацију зашто постоје ове разлике? Узмите у обзир шта би се десило да нисмо применили никакву доменску адаптацију (чак можете и пробати да је искључите и видите шта би се десило).
Визуелизација резултата¶
С обзиром да за податке из Србије немамо лабеле, није могуће урадити квантитативну анализу тачности модела. Зато ћемо се задржати на квалитативној анализи посматрајући резултате модела и видети да ли они одговарају реалности. За овакву анализу је потребно дати што бољу визуелизацију резултата, што ћемо и учинити у овој секцији.
Сваку од класа које модела предвиђа ћемо представити једном бојом да би се јасније видела на растерској подлози. Испод су за сваку од постојећих класа придружене боје у виду триплета који означавају црвену, плаву и зелену боју. Намерно смо ставили да поједине јако сличне класе имају исту боју - но, осећајте се потпуно слободно да вредности испод измените по жељи. Тренутно су зелене боје за разне врсте зелених поврина и ораница, црвена боја за насеља, магента за индустрију, наранџаста за аутопут, а плаве за реке/језера.
classes_to_rgb = {
'Highway': (1.0, 0.5, 0),
'Industrial': (1.0, 0, 0.5),
'Residential': (1.0, 0, 0),
'AnnualCrop': (0, 1.0, 0.5),
'PermanentCrop': (0, 1.0, 0.5),
'Forest': (0, 1.0, 0),
'HerbaceousVegetation': (0, 1.0, 0),
'Pasture': (0, 1.0, 0),
'River': (0.5, 0, 1.0),
'SeaLake': (0.5, 0, 1.0)}
Функције испод служе за лепу визуелизацију резултата. Уколико добро познајете Пајтон, требало би углавном да можете самостално да их разумете, мада то и није неопходно.
def show_results_tiles(all_tiles, predictions_argmax, classes):
for idx, prediction in enumerate(predictions_argmax):
plt.figure(figsize=(2, 2))
plt.imshow(all_tiles[idx])
plt.show()
print(classes[prediction])
def show_results_tiles_semantic(all_tiles, predictions_argmax, classes, softmaxes, num_rows):
# I am packing them row majro
img = np.zeros((num_rows * 64, (all_tiles.shape[0] // num_rows) * 64, 3))
img_sem = np.zeros((num_rows * 64, (all_tiles.shape[0] // num_rows) * 64, 3))
img_conf = np.zeros((num_rows * 64, (all_tiles.shape[0] // num_rows) * 64, 3))
for idx, prediction in enumerate(predictions_argmax):
img[(idx % num_rows) * 64 : (idx % num_rows + 1) * 64 , (idx // num_rows) * 64 : (idx // num_rows + 1) * 64, :] = all_tiles[idx] #if classes[prediction] in ["Residential", "Industrial"] else [0, 0, 0]
img_sem[(idx % num_rows) * 64 : (idx % num_rows + 1) * 64 , (idx // num_rows) * 64 : (idx // num_rows + 1) * 64, :] = classes_to_rgb[classes[prediction]] #if classes[prediction] in ["Residential", "Industrial"] else [0, 0, 0]
img_conf[(idx % num_rows) * 64 : (idx % num_rows + 1) * 64 , (idx // num_rows) * 64 : (idx // num_rows + 1) * 64, :] = softmaxes[idx, argmax[idx]]
plt.figure(figsize=(10, 10))
plt.imshow(img)
plt.show()
plt.figure(figsize=(10, 10))
plt.imshow(img * 0.7 + img_sem * 0.3)
plt.show()
plt.figure(figsize=(10, 10))
plt.imshow(img_sem)
plt.show()
plt.figure(figsize=(10, 10))
plt.imshow(img_conf)
Хајде сада да коначно погледамо визуелизације резултата. Добићемо четири слике:
- Исечак који обрађујемо
- Преклопљене предвиђене лабеле класа у дефинисаним бојама над улазном сликом
- Чисте предвиђене лабеле без преклопа
- Матрица где светлина ћелије означава сигурност модела. Тамније ћелије означавају мању сигурност.
Погледајте резултате детаљно. Уочите да ли модел добро разликује појединачне површине. Наравно, лакше је разликовати насеље од оранице, али пак обратите пажњу да ли модел успешно разликује индустрију од насеља, што је тежи задатак. Уочите и да ли модел предвиђа остале класе коректно (рецимо саобраћајнице). Детаљно анализирајте матрицу сигурности модела и размислите зашто је на појединим ћелијама та сигурност смањена.
show_results_tiles_semantic(all_tiles, argmax, classes, softmaxes, num_rows=CROP_SIZE // 64)
Гушћи скуп предикција¶
У претходној визуелизације смо модел применили на кораку од 64 пиксела, као кад радимо поплочавање - ни једна од улазних плочица модела се не преклапа. Наравно, могуће је постићи и гушћу расподелу предикција тако што ћемо узети мањи корак модела. Управо то је циљ ове секције.
Најпре ћемо модификовати функцију get_images да уједно и прима корак са који се праве предикције и тиме добијамо функцију get_images_advanced.
def get_images_advanced(image, crop_size=64, stride=32):
image = prepare_rgb(image)
rows = image.shape[0] // stride
cols = image.shape[1] // stride
all_tiles = np.zeros((rows * cols, crop_size, crop_size, 3), dtype=np.float32)
for r in range(rows):
for c in range(cols):
if r*stride + crop_size > image.shape[0] or c*stride + crop_size > image.shape[1]:
continue
all_tiles[r + rows * c, ...] = image[r*stride:r*stride+crop_size, c*stride:c*stride+crop_size, :]
return all_tiles
Функција prepare_images_for_dnn остаје иста и сад те две функције можемо позвати али са кораком 32.
all_tiles_2 = get_images_advanced(image[CROP_START_ROW:CROP_START_ROW+CROP_SIZE, CROP_START_COL:CROP_START_COL+CROP_SIZE, :])
tiles_prepared_for_dnn = prepare_images_for_dnn(all_tiles_2).to(device)
Слично као и у ранијим ћелијама, хајде да извршимо модел. С обзиром да сада треба дати много већи број предикција, очекујте да ће извршавање трајате дуже.
pred = model(tiles_prepared_for_dnn)
_, argmax = torch.max(pred, 1)
Функција испод нам служи за визуелизацију гушћих предикција над улазном мапом. Хајде да погледамо и анализирамо те резултате.
def show_results_tiles_semantic_2(all_tiles_2, predictions_argmax, classes, softmaxes, num_rows, stride):
plt.figure(figsize=(10, 10))
plt.imshow(prepare_rgb(sample_image_1) + 0.2) # Додајемо мало светлине (0.2) да би се тачке лепше виделе на растерској подлози
for idx, prediction in enumerate(predictions_argmax):
y = (idx % num_rows) * stride + 32
x = (idx // num_rows) * stride + 32
if x >= 512 or y >= 512:
continue
plt.scatter(x, y, color=classes_to_rgb[classes[prediction]], s=100)
plt.show()
show_results_tiles_semantic_2(all_tiles_2, argmax, classes, None, num_rows=CROP_SIZE // 32, stride=32)
Уочите да ли се са гушћим предикцијама добијају неке разлике. Најпре, свакако се може приметити да са гушћим предикцијама добијамо много прецизнију детекцију намене површине земље - рецимо сад је река Сава јасније детектована тако да предикције прецизније покривају њено корито.