$$ \newcommand{\floor}[1]{\left\lfloor{#1}\right\rfloor} \newcommand{\ceil}[1]{\left\lceil{#1}\right\rceil} \renewcommand{\mod}{\,\mathrm{mod}\,} \renewcommand{\div}{\,\mathrm{div}\,} \newcommand{\metar}{\,\mathrm{m}} \newcommand{\cm}{\,\mathrm{cm}} \newcommand{\dm}{\,\mathrm{dm}} \newcommand{\litar}{\,\mathrm{l}} \newcommand{\km}{\,\mathrm{km}} \newcommand{\s}{\,\mathrm{s}} \newcommand{\h}{\,\mathrm{h}} \newcommand{\minut}{\,\mathrm{min}} \newcommand{\kmh}{\,\mathrm{\frac{km}{h}}} \newcommand{\ms}{\,\mathrm{\frac{m}{s}}} \newcommand{\mss}{\,\mathrm{\frac{m}{s^2}}} \newcommand{\mmin}{\,\mathrm{\frac{m}{min}}} \newcommand{\smin}{\,\mathrm{\frac{s}{min}}} $$

Prijavi problem


Obeleži sve kategorije koje odgovaraju problemu

Još detalja - opišite nam problem


Uspešno ste prijavili problem!
Status problema i sve dodatne informacije možete pratiti klikom na link.
Nažalost nismo trenutno u mogućnosti da obradimo vaš zahtev.
Molimo vas da pokušate kasnije.

Припрема из претходних свезака

Све свеске се међусобно надограђују, тако да нам је неопходно да поновимо неке делове из претходних свески, што без додатног објашњења чинимо у овој секцији.

Овде нам је ради анализе резултата неопходан списак класа. Такође, да би смо могли да користимо модел поновићемо поступак из претходне свеске. Дакле, увеземо неопходне библиотеке и учитамо модел. Ово се наводи без објашњења, с обзиром да је поступак идентичан као у претходним свескама

In [1]:
classes = [ 'AnnualCrop',
            'Forest',   
            'HerbaceousVegetation',
            'Highway',
            'Industrial',
            'Pasture',
            'PermanentCrop',
            'Residential',
            'River',
            'SeaLake']
In [2]:
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
In [3]:
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("Модел учитан")
Модел учитан

Извршавање модела на примерима из базе

Учитавање и припрема базе

Најпре учитавамо базу података помоћу пакета torchvision. Овај корак је заправо јако једноставан ако је база адекватно представљена у фајл систему. Довољно је да за сваку од класа имамо фолдер који носи њено име, а унутар тог фолдера да се налазе одговарајуће слике. Када је база тако припремљена само користимо класу DatasetFolder да је учитамо без додатног труда.

Остатак ћелије испод садржи помоћне параметре за учитавање. Наиме неопхоно је дефинисати функцију која ће учитавати слике, јер се у зависности од примене и типа слике то може различито радити. У нашој функцији смо рецимо ми слике скалирали са 255.0 да би оне имале вредност у опсегу 0 до 1, што би значило да смо вредност сваког пиксела слике поделили са 255.0 што је максимална вредност коју пиксел може имати за тип слика које овде користимо.

Поред тога, над улазним сликама се могу извршити и различите трансформације, овај поступак је битнији за тренирање модела, али и сада је неопходно слике повећати на величину 224x224 коју очекује дата архитектура неуралне мреже.

In [4]:
def image_loader(path):
    image = (cv2.imread(path).astype("float32") / 255.0)[:, :, ::-1].copy()
    return torch.from_numpy(image.transpose(2,0,1))

transforms = torchvision.transforms.Compose([
    torchvision.transforms.ToPILImage(),
    torchvision.transforms.Resize(224),
    torchvision.transforms.ToTensor(),
])
dataset = torchvision.datasets.DatasetFolder(root=r"mldata\EuroSAT\2750", loader=image_loader, transform=transforms, extensions="jpg")

Иако нећемо тренирати модел у овој вежбанци, проћићемо кроз веома битан поступак поделе скупа података на тренирајући, валидирајући и тестирајући.

Један од кључних проблема са којим се инжењер или научник који се бави машинским учењем сусреће је борба против "учења напамет". Слично као наставник у школи, ми желимо да из процеса обучавања модела извучемо суштинске концепте тако да се може применити и на примерима које није видео у току обуке. Дакле желимо да модел добро генерализује. Оно што не желимо је да модел просто меморише парове улаз-очекивани излаз без креирања генерализованих обележја и класификационог приступа. За случај учења напамет користи се стручан термин преобучавање (енг. overfitting), а све технике које се боре против преобучавања и доводе до боље генерализације се називају регуларизација.

Подела сета података са којим радимо на више скупова управо служи као алат за детекцију да ли је дошло до преобучавања. Наиме, сам модел се обучава на тренирајућем скупу података, а његова тачност се испитује на одвојеном скупу података који модел није видео током тренинга. Уколико модел даје значајно лошије предикције на издвојеном скупу података у односу на тренирајући то је вероватан знак да је дошло до преобучавања.

Питате се можда зашто се издвајају два скупа поред тренирајућег - скуп за валидацију и за тестирање. Наиме, у току развоја модела користи се скуп за валидацију где ће особа која ради на моделу доносити разне одлуке покушавајући да поправи резултат на валидацији. Тиме је могуће да се у току развоја модела унесу различите претпоставке и кроз њих у некој мери дође до преобучавања и на валидационом скупу података. Због тога имамо и тестирајући скуп података који се идеално користи само једном - онда када смо завршили развој модела и желимо да га испоручимо кориснику, где ћемо као тачност модела пријавити резултат на тестирајућем скупу, који ни модел ни особа која га је правила раније није видела.

Највећи део података се користи за тренирање, док се за валидацију и тестирање користе значајно мањи удели. Честа подела је 70%/15%/15% као што је и учињено у ћелији испод.

In [5]:
train_ratio = 0.7
val_ratio = 0.15
test_ratio = 0.15

dataset_size = len(dataset)

train_samples = int(train_ratio * dataset_size)
val_samples = int(val_ratio * dataset_size)
test_samples = dataset_size - train_samples - val_samples  # Еквивалентно са int(val_ratio * test_ratio) али избегава грешку заокруживања.

print(f"Укупан број примера у скупу за тренирање: {train_samples}")
print(f"Укупан број примера у скупу за валидацију: {val_samples}")
print(f"Укупан број примера у скупу за тестирање: {val_samples}")
Укупан број примера у скупу за тренирање: 18900
Укупан број примера у скупу за валидацију: 4050
Укупан број примера у скупу за тестирање: 4050

Пакет torch већ има функцију за поделу учитаног скупа података на дисјунктне подскупове, односно подскупове који немају никакав пресек (јер не би желели да нам неки од података из тестирајућег сета рецимо заврше у тренирајучем). Као улаз му само треба специфицирати удео сваког од скупа, што смо већ дефинисали у претходној ћелији. Поред тога, с обзиром да жељена функција из пакета torch, која врши поделу на подскупове, ту поделу ради насумично, односно сваки пут кад је позовемо добићемо другачију расподелу примера по тренирајућем, валидирајућем и тестирајућем скупу. Ово понашање није идеално за нас, па ћемо зато фиксирати семе генератора насумичних бројева да бисмо имали поновљивост - т.ј. да бисмо сваки пут када покренемо свеску имали исте тренинг, валидација и тест скупове.

In [6]:
DATASET_SEED = 12345

train_dataset, val_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_samples, val_samples, test_samples], generator=torch.Generator().manual_seed(DATASET_SEED))

Ради ефикасног учитавања података при тренирању и коришћењу мреже они се најчешће учитавају у хрпама (енг. batch). Поред тога неопходно је дефинисати и друге параметре самог процеса учитавања ради његове ефикасности. То се чини креирањем DataLoader. инстанце, а ми смо у примеру испод то урадили само за валидирајући скуп, с обзиром да у овој свесци нећемо тренирати модел. Такође дефинисали смо и да је величина појединачне хрпе која се учитава 32 слике.

In [7]:
BATCH_SIZE = 32

# train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0, drop_last=True, pin_memory=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, drop_last=True, pin_memory=True)

Хајде да учитамо неколико слика из валидирајућег скупа података и да их прикажемо. Команда испод нам довлачи следећу хрпу из валидационог сета. Наравно, довлаче се и одговарајуће лабеле.

In [8]:
example_batch, example_labels = next(iter(val_loader))

У следећој ћелији приказујемо облик хрпе података коју смо учитали. Препознајете да задње две димензије представљају величину слике од 224x224 пиксела коју неурална мрежа очекује.

Размислите, шта представљају прве две димензије?

In [9]:
print(example_batch.shape)
torch.Size([32, 3, 224, 224])

Напишите сами команду која приказује облик учитаних лабела. Да ли су дате димензије оно што очекујете? Зашто?

In [10]:
# Решење

# print(example_labels.shape)

Сада можемо проћи кроз целокупну хрпу података коју смо учитали и приказати сваку од слика заједно са пратећом лабелом.

Погледајте приказане слике и размислите која обележја би била добра да раздвоје дате класе.

In [11]:
batch_np = example_batch.numpy()
for i in range(BATCH_SIZE):
    plt.figure(figsize=(2, 2))
    plt.imshow(batch_np[i, ...].transpose(1, 2, 0))
    plt.show()
    print(dataset.classes[example_labels[i]])
Highway
River
AnnualCrop
HerbaceousVegetation
Highway
SeaLake
AnnualCrop
Pasture
AnnualCrop
Forest
HerbaceousVegetation
SeaLake
SeaLake
AnnualCrop
Residential
SeaLake
Pasture
AnnualCrop
Pasture
SeaLake
Pasture
HerbaceousVegetation
SeaLake
AnnualCrop
HerbaceousVegetation
PermanentCrop
Industrial
SeaLake
Forest
SeaLake
HerbaceousVegetation
AnnualCrop

Извршавање модела над појединачним примерима

Када имамо припремљене податке у облику у којем их модел очекује, можемо јако једноставно да добијемо предикцију за сваку од слика. Најпре селектујемо прву слику у хрпи example_batch[0:1, ...], где три тачке за остале координате означавају да их узимамо без измена. Модел онда позовемо као најобичнију функцију над тим подацима - резултат ће бити предикције.

In [12]:
with torch.no_grad():
    prediction = model(example_batch[0:1, ...])
In [13]:
prediction
Out[13]:
tensor([[ -3.1037, -11.7114,  -4.8120,  22.1648,   2.4975,  -4.7359,   0.0357,
          -4.5473,   8.4084,  -3.3049]])

Погледајте резултат предикција - модел је за дату улазну слику дао 10 вредности на излазу. Као што смо раније рекли, свака од датих вредности одговара једној од класа. Саме те вредности представљају скорове, а у случају да нам је модел вештачка неурална мрежа, у питању су активације излазних неурона. Но, сасвим довољно је о овоме размишљати апстрактно: модел нам даје скорове за сваку од класа и тамо где је скор највећи та класа је оно што модел мисли да је добио на улазу.

Вежба 1: Утврдити сами коју класу је предвидео модел и да ли је та предикција тачна.

Вежба 2: Промените ћелије изнад тако да дају предикцију за другу по реду слику у хрпи. Поновите вежбу 1 за тај излаз.

У ћелијама испод ћемо програмским путем извести претходну вежбу и као излаз дати коју класу је модел предвидео.

Дакле, занима нас на ком положају у низу свих предикција се налази максимална вредност, јер знамо да редни број тог положаја одговара класи коју је модел предвидео - математичким формализмима речено - неопходан нам је argmax.

In [14]:
max_val, argmax = torch.max(prediction, 1)
In [15]:
argmax
Out[15]:
tensor([3])

Индексирање класа почиње од 0, а листа класа је дата у првој ћелији ове вежбанке. Хајде да видимо којој класи одговара предикција, а којој очекивана вредност (лабела).

In [16]:
print(classes[argmax])
print(classes[example_labels[0]])
Highway
Highway

Модел је тачно предвидео класу! Можете се сами играти и видети како предвиђа остале слике из учитане хрпе - модификацијом примера кода изнад.

Можда вам је већ интуитивно да, с обзиром да модел предвиђа скор за сваку од класа, је могуће дати и естимацију вероватноће са којом је модел сигуран у своју предикцију. У машинском учењу се за те потребе најчешће користи sofmax функција у коју нећемо даље залазити, али је довољно запамтити да она вектор скорова претвара у апроксимативну расподелу вероватноће.

In [17]:
with torch.no_grad():
    softmax = F.softmax(prediction, dim=1)

Ако сада узмемо вредност апроксимативне вероватноће на месту предикције - видећемо да је модел практично потпуно сигуран у своју предикцију (вероватноћа је блиска 1). Ово је донекле и очекивано с обзиром да валидациони скуп података потпуно кореспондира тренирајућем. С друге стране, када дођемо до обраде података из Србије, видећете да модел неће бити тако сигуран у своје предикције.

In [18]:
print(float(softmax[0, argmax]))
0.999998927116394

Процесирање у хрпама (енг. batch processing)

Једна од супер моћи које нам омогућавају наменски процесори са пуно језгара (као што су графичке картице) је процесирање веће хрпе података у паралели. Тиме се за теоријски исто време одједном обрађује велика количина података.

Овакав вид обраде је најприроднији у пакету torch, и заправо је само довољно моделу проследити целокупан учитани batch и добићемо резултат. Слично, израчунавање argmax-a се спроводи без измена и над хрпом предикција.

In [19]:
with torch.no_grad():
    predictions = model(example_batch)
In [20]:
max_val, argmax = torch.max(predictions, 1)

Хајде да упоредимо низ предвиђених индекса класа са лабелама за целу учитану хрпу.

In [21]:
print(argmax)
print(example_labels)
tensor([3, 8, 0, 2, 3, 9, 0, 5, 0, 1, 2, 9, 9, 0, 7, 9, 5, 0, 5, 9, 5, 2, 9, 0,
        2, 6, 4, 9, 1, 9, 2, 0])
tensor([3, 8, 0, 2, 3, 9, 0, 5, 0, 1, 2, 9, 9, 0, 7, 9, 5, 0, 5, 9, 5, 2, 9, 0,
        2, 6, 4, 9, 1, 9, 2, 0])

Пробајте сами да напишете команду која враћа укупан број предикција. Да ли је он очекиван и зашто?

In [22]:
# Решење
# len(argmax)
# ili
# argmax.shape

На исти начин као и раније ћемо за целу хрпу приказати апроксимативне расподеле вероватноћа израчунате softmax функцијом. Овај пут ћемо исштампати цео вектор. Видимо да за сваку од 32 улазне слике имамо по 10 вредности.

In [23]:
with torch.no_grad():
    softmaxes = F.softmax(predictions, dim=1)

print(softmaxes)
tensor([[1.0617e-11, 1.9399e-15, 1.9237e-12, 1.0000e+00, 2.8747e-09, 2.0757e-12,
         2.4517e-10, 2.5066e-12, 1.0609e-06, 8.6829e-12],
        [3.7828e-04, 4.3271e-06, 2.3746e-07, 1.8257e-04, 4.2639e-06, 2.3032e-05,
         2.1748e-06, 6.1341e-07, 9.9920e-01, 2.0025e-04],
        [9.9988e-01, 1.1681e-08, 2.0134e-08, 1.1261e-05, 2.3045e-07, 3.3565e-06,
         3.8835e-05, 2.3027e-08, 6.3941e-05, 1.0029e-07],
        [1.7455e-04, 7.6217e-04, 9.9319e-01, 1.6963e-05, 1.3062e-05, 5.9099e-04,
         5.1270e-03, 8.5317e-05, 8.2149e-06, 3.1584e-05],
        [3.6404e-08, 3.7629e-10, 3.8279e-08, 9.9975e-01, 1.9204e-06, 1.3151e-07,
         2.6295e-07, 1.7250e-07, 2.4284e-04, 3.3014e-07],
        [9.6631e-06, 1.1062e-05, 2.0579e-06, 7.8792e-09, 1.5512e-09, 3.6191e-07,
         7.1304e-10, 8.1398e-09, 4.6139e-08, 9.9998e-01],
        [9.9922e-01, 3.1939e-07, 1.7265e-06, 1.1031e-04, 5.2308e-06, 2.9913e-05,
         3.9759e-04, 1.1306e-06, 2.3447e-04, 3.1273e-06],
        [2.6184e-07, 6.2368e-07, 1.5995e-06, 8.7228e-06, 5.3975e-06, 9.9992e-01,
         5.3498e-05, 1.9093e-06, 4.5985e-06, 1.2794e-06],
        [9.9977e-01, 6.0966e-09, 1.0933e-08, 1.9250e-05, 8.4731e-08, 4.6820e-07,
         2.3578e-06, 4.4778e-09, 2.0685e-04, 1.9241e-07],
        [1.3346e-06, 9.9998e-01, 5.7687e-06, 3.0854e-06, 4.6892e-08, 7.7317e-06,
         4.1949e-07, 2.4134e-07, 1.8567e-07, 1.4498e-06],
        [1.1896e-05, 9.9801e-04, 9.9122e-01, 8.7427e-06, 1.4485e-06, 2.0656e-04,
         7.5238e-03, 1.5862e-05, 4.8301e-06, 5.2431e-06],
        [5.5081e-07, 1.4762e-06, 7.7204e-07, 1.0495e-09, 2.0615e-10, 2.3178e-08,
         7.5751e-11, 1.6382e-09, 6.1484e-09, 1.0000e+00],
        [6.2219e-06, 2.1465e-05, 2.2832e-06, 9.1909e-09, 1.2653e-09, 3.7502e-07,
         7.2694e-10, 1.0212e-08, 2.7947e-08, 9.9997e-01],
        [9.9487e-01, 1.2438e-06, 3.8656e-06, 2.2175e-04, 9.6762e-06, 2.0584e-04,
         4.0976e-03, 7.8372e-06, 5.7364e-04, 5.6765e-06],
        [3.9772e-11, 9.2006e-10, 1.5881e-08, 1.0920e-07, 1.6351e-06, 6.6058e-10,
         2.9065e-07, 1.0000e+00, 3.8441e-09, 3.9571e-09],
        [6.2746e-06, 3.2814e-06, 9.8774e-07, 4.4169e-09, 1.0570e-09, 2.4168e-07,
         3.4622e-10, 4.8407e-09, 5.2154e-08, 9.9999e-01],
        [9.8261e-06, 9.1038e-07, 6.9811e-07, 2.5024e-07, 5.9565e-09, 9.9998e-01,
         1.4548e-08, 3.0553e-09, 3.4321e-06, 3.0676e-06],
        [9.8717e-01, 3.5390e-05, 5.5864e-06, 9.5411e-05, 6.2848e-06, 2.5033e-03,
         7.6662e-06, 1.5454e-06, 6.5347e-03, 3.6415e-03],
        [6.8257e-08, 3.8816e-07, 1.2790e-06, 5.1711e-06, 6.2192e-07, 9.9998e-01,
         8.2608e-07, 1.1189e-07, 7.1402e-06, 2.7607e-06],
        [1.5400e-03, 2.5724e-03, 1.6473e-04, 5.8245e-06, 7.4392e-07, 2.7506e-03,
         5.8746e-07, 2.3646e-06, 7.3065e-05, 9.9289e-01],
        [1.2240e-05, 1.7426e-05, 5.4445e-06, 5.2738e-06, 2.0960e-07, 9.9994e-01,
         1.0252e-05, 2.0389e-07, 5.0291e-06, 2.5289e-07],
        [4.0490e-07, 6.4022e-06, 9.9965e-01, 5.2296e-07, 1.3191e-06, 1.8389e-05,
         7.2006e-05, 2.4965e-04, 1.2176e-07, 1.2142e-06],
        [1.4904e-07, 2.2796e-07, 1.7438e-07, 1.3224e-10, 2.3410e-11, 3.6089e-09,
         6.5851e-12, 1.6144e-10, 1.7095e-09, 1.0000e+00],
        [9.9973e-01, 1.6562e-08, 2.3064e-07, 6.2186e-05, 1.0704e-06, 9.7093e-07,
         4.6906e-05, 8.1666e-08, 1.5391e-04, 2.7365e-07],
        [2.7389e-05, 2.8692e-04, 9.9645e-01, 1.9653e-05, 2.8352e-06, 7.0169e-04,
         2.3951e-03, 4.0017e-05, 3.5608e-05, 3.9413e-05],
        [2.1823e-01, 4.0341e-07, 7.6364e-06, 1.6168e-04, 2.7574e-05, 3.3018e-05,
         7.8146e-01, 5.8483e-06, 6.3518e-05, 7.2533e-07],
        [7.8615e-11, 1.0495e-11, 9.1356e-09, 2.4908e-07, 9.9999e-01, 2.2761e-08,
         6.4367e-06, 1.0184e-07, 9.2759e-09, 1.1617e-09],
        [2.0778e-05, 4.5221e-05, 1.1352e-05, 5.3290e-08, 1.1193e-08, 1.4063e-06,
         7.0152e-09, 6.6182e-08, 1.4850e-07, 9.9992e-01],
        [4.5017e-06, 9.9987e-01, 4.5296e-05, 1.2658e-05, 2.9046e-07, 5.9342e-05,
         2.9935e-06, 1.1107e-06, 1.3635e-06, 5.5970e-06],
        [2.0844e-03, 8.9212e-03, 1.3112e-04, 4.7463e-06, 7.2364e-07, 4.8905e-04,
         4.3870e-07, 1.3979e-06, 8.7788e-06, 9.8836e-01],
        [4.4659e-05, 1.9525e-04, 9.9830e-01, 7.1695e-06, 1.1830e-05, 7.4550e-04,
         6.3099e-04, 2.1555e-05, 1.3772e-05, 2.7573e-05],
        [9.9992e-01, 2.9959e-08, 9.1636e-07, 1.7430e-05, 5.5002e-07, 9.9716e-07,
         9.8926e-06, 6.2353e-08, 3.9581e-05, 5.6070e-06]])

Анализа сигурности модела

Као што смо навели, излаз softmax-а се може интерпретирати као апроксимативна расподела вероватноће. Команде испод израчунавају такву вероватноћу (скор) предикције за сваки од 32 улаза у мрежу.

Уочите која је најмања вероватноћа предикције - на већем скупу већ можемо уочити да на појединим примерима модел није потпуно сигуран.

In [24]:
all_scores = np.take_along_axis(softmaxes.numpy().T, np.expand_dims(argmax.numpy(), 0), 0)
print(all_scores)
[[0.9999989  0.9992042  0.99988234 0.9931901  0.99975437 0.9999769
  0.9992163  0.99992204 0.9997706  0.99997973 0.9912237  0.99999726
  0.9999697  0.99487275 0.999998   0.99998915 0.99998176 0.9871688
  0.99998164 0.99288964 0.9999436  0.9996501  0.9999995  0.99973434
  0.9964514  0.7814647  0.9999932  0.99992096 0.99986696 0.9883581
  0.99830186 0.9999249 ]]

С друге стране, ако израчунамо средњу вредност скора за све предикције, видимо да је модел у глобалу практично поптуно сигуран у своје предикције на валидационом скупу. Запамтите овај број - упоредићемо га са истим на подацима из Србије.

In [25]:
all_scores.mean()
Out[25]:
0.99126804