14.7 Vieno praėjimo daugiastačiakampis aptikimas
14.3 skyrelis–14.6 skyrelis pristatėme apibrėžiamuosius stačiakampius, inkarinius stačiakampius, daugiamastelį objektų aptikimą ir objektų aptikimo duomenų rinkinį. Dabar esame pasirengę panaudoti šias pagrindines žinias objektų aptikimo modeliui suprojektuoti: vieno praėjimo daugiastačiakampiam aptikimui (angl. single shot multibox detection, SSD) (Liu ir kt., 2016). Šis modelis yra paprastas, greitas ir plačiai naudojamas. Nors tai tik vienas iš daugybės objektų aptikimo modelių, kai kurie šiame skyrelyje aprašyti projektavimo principai ir įgyvendinimo detalės taikomi ir kitiems modeliams.
14.7.1 Modelis
14.7.1 pav. pateikiama vieno praėjimo daugiastačiakampio aptikimo dizaino apžvalga. Šį modelį daugiausia sudaro bazinis tinklas, po kurio eina keli daugiamasteliai požymių žemėlapių blokai. Bazinis tinklas skirtas požymiams iš įvesties vaizdo išgauti, todėl jame gali būti naudojamas gilusis CNN. Pavyzdžiui, originaliame vieno praėjimo daugiastačiakampio aptikimo straipsnyje naudojamas VGG tinklas, nupjautas prieš klasifikavimo sluoksnį (Liu ir kt., 2016), nors dažnai naudojamas ir ResNet. Savo dizainu galime padaryti, kad bazinis tinklas išvestų didesnius požymių žemėlapius, kad būtų sugeneruota daugiau inkarinių stačiakampių mažesniems objektams aptikti. Vėliau kiekvienas daugiamastelis požymių žemėlapių blokas sumažina (pvz., perpus) ankstesnio bloko požymių žemėlapių aukštį ir plotį ir leidžia kiekvienam požymių žemėlapių vienetui padidinti savo lokalią matymo sritį įvesties vaizde.
Prisiminkite daugiamastelio objektų aptikimo dizainą, naudojant giliųjų neuroninių tinklų sluoksninius vaizdų vaizdavimus, 14.5 skyrelis. Kadangi daugiamasteliai požymių žemėlapiai, esantys arčiau 14.7.1 pav. viršaus, yra mažesni, bet turi didesnes lokalias matymo sritis, jie tinka aptikti mažiau, bet didesnių objektų.
Trumpai tariant, per savo bazinį tinklą ir kelis daugiamastelius požymių žemėlapių blokus vieno praėjimo daugiastačiakampis aptikimas generuoja kintamą skirtingo dydžio inkarinių stačiakampių skaičių ir aptinka skirtingo dydžio objektus prognozuodamas šių inkarinių stačiakampių (taigi ir apibrėžiamųjų stačiakampių) klases ir poslinkius; taigi tai yra daugiamastelis objektų aptikimo modelis.
14.7.1 pav. Kaip daugiamastelis objektų aptikimo modelis, vieno praėjimo daugiastačiakampį aptikimą daugiausia sudaro bazinis tinklas, po kurio eina keli daugiamasteliai požymių žemėlapių blokai.
Toliau aprašysime skirtingų blokų įgyvendinimo detales 14.7.1 pav.. Pradėkime nuo to, kaip įgyvendinti klasių ir apibrėžiamųjų stačiakampių prognozavimą.
14.7.1.1 Klasių prognozavimo sluoksnis
Tegul objektų klasių skaičius yra . Tada inkariniai stačiakampiai turi klasių, kur 0 klasė yra fonas. Tam tikru masteliu tarkime, kad požymių žemėlapių aukštis ir plotis yra atitinkamai ir . Kai inkarinių stačiakampių generuojama kiekvieną šių požymių žemėlapių erdvinę padėtį laikant jų centru, iš viso reikia klasifikuoti inkarinių stačiakampių. Dėl galimai didelių parametrizavimo sąnaudų tai dažnai daro klasifikavimą pilnai sujungtais sluoksniais neįgyvendinamu. Prisiminkite, kaip 8.3 skyrelis naudojome konvoliucinių sluoksnių kanalus klasėms prognozuoti. Vieno praėjimo daugiastačiakampis aptikimas naudoja tą pačią techniką modelio sudėtingumui sumažinti.
Konkrečiai, klasių prognozavimo sluoksnis naudoja konvoliucinį sluoksnį, nekeisdamas požymių žemėlapių pločio ar aukščio. Taip išvestys ir įvestys gali atitikti viena kitą tose pačiose požymių žemėlapių erdvinėse dimensijose (plotis ir aukštis). Konkrečiau, išvesties požymių žemėlapių kanalai bet kurioje erdvinėje padėtyje (, ) vaizduoja klasių prognozes visiems inkariniams stačiakampiams, kurių centras yra įvesties požymių žemėlapių (, ). Kad būtų gautos galiojančios prognozės, turi būti išvesties kanalų, kur toje pačioje erdvinėje padėtyje išvesties kanalas su indeksu vaizduoja klasės () prognozę inkariniam stačiakampiui ().
Toliau apibrėžiame tokį klasių prognozavimo sluoksnį,
nurodydami ir argumentais num_anchors ir num_classes atitinkamai.
Šis sluoksnis naudoja konvoliucinį sluoksnį su
1 papildymu.
Šio konvoliucinio sluoksnio įvesties ir išvesties plotis bei aukštis
nesikeičia.
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, gluon, image, init, np, npx
from mxnet.gluon import nn
npx.set_np()
def cls_predictor(num_anchors, num_classes):
return nn.Conv2D(num_anchors * (num_classes + 1), kernel_size=3,
padding=1)
%matplotlib inline
from d2l import torch as d2l
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
def cls_predictor(num_inputs, num_anchors, num_classes):
return nn.Conv2d(num_inputs, num_anchors * (num_classes + 1),
kernel_size=3, padding=1)
14.7.1.2 (Apibrėžiamųjų stačiakampių prognozavimo sluoksnis)
Apibrėžiamųjų stačiakampių prognozavimo sluoksnio dizainas panašus į klasių prognozavimo sluoksnio. Vienintelis skirtumas yra kiekvieno inkarinio stačiakampio išvesčių skaičius: čia reikia prognozuoti keturis poslinkius, o ne klasių.
def bbox_predictor(num_anchors):
return nn.Conv2D(num_anchors * 4, kernel_size=3, padding=1)
def bbox_predictor(num_inputs, num_anchors):
return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)
14.7.1.3 Kelių mastelių prognozių konkatenavimas
Kaip minėjome, vieno praėjimo daugiastačiakampis aptikimas naudoja daugiamačio mastelio požymių žemėlapius inkariniams stačiakampiams generuoti ir jų klasėms bei poslinkiams prognozuoti. Skirtingais masteliais požymių žemėlapių formos arba to paties vieneto centre esančių inkarinių stačiakampių skaičius gali skirtis. Todėl prognozių išvesčių formos skirtingais masteliais gali skirtis.
Toliau pateiktame pavyzdyje
sukonstruojame požymių žemėlapius dviem skirtingais masteliais,
Y1 ir Y2,
tam pačiam minirinkiniui,
kur Y2 aukštis ir plotis
yra perpus mažesni nei Y1.
Paimkime klasių prognozavimą kaip pavyzdį.
Tarkime, kad
5 ir 3 inkariniai stačiakampiai
generuojami kiekvienam Y1 ir Y2 vienetui atitinkamai.
Be to, tarkime,
kad objektų klasių skaičius yra 10.
Požymių žemėlapiams Y1 ir Y2
klasių prognozių išvesčių kanalų skaičius
yra atitinkamai ir ,
kur abiejų išvesčių forma yra
(rinkinio dydis, kanalų skaičius, aukštis, plotis).
def forward(x, block):
block.initialize()
return block(x)
Y1 = forward(np.zeros((2, 8, 20, 20)), cls_predictor(5, 10))
Y2 = forward(np.zeros((2, 16, 10, 10)), cls_predictor(3, 10))
Y1.shape, Y2.shape
def forward(x, block):
return block(x)
Y1 = forward(torch.zeros((2, 8, 20, 20)), cls_predictor(8, 5, 10))
Y2 = forward(torch.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10))
Y1.shape, Y2.shape
Kaip matome, išskyrus rinkinio dydžio dimensiją, kitos trys dimensijos turi skirtingus dydžius. Kad sukonkatenuotume šias dvi prognozių išvestis efektyvesniam skaičiavimui, paversime šiuos tenzorius nuoseklesniu formatu.
Atkreipkite dėmesį, kad kanalų dimensijoje saugomos to paties centro inkarinių stačiakampių prognozės. Pirmiausia perkeliame šią dimensiją į giliausią. Kadangi rinkinio dydis skirtingais masteliais išlieka tas pats, prognozių išvestį galime paversti dvimačiu tenzoriumi, kurio forma (rinkinio dydis, aukštis plotis kanalų skaičius). Tada galime sukonkatenuoti tokias išvestis skirtingais masteliais pagal 1 dimensiją.
def flatten_pred(pred):
return npx.batch_flatten(pred.transpose(0, 2, 3, 1))
def concat_preds(preds):
return np.concatenate([flatten_pred(p) for p in preds], axis=1)
def flatten_pred(pred):
return torch.flatten(pred.permute(0, 2, 3, 1), start_dim=1)
def concat_preds(preds):
return torch.cat([flatten_pred(p) for p in preds], dim=1)
Taip,
nors Y1 ir Y2 turi skirtingus dydžius
kanalų, aukščio ir pločio atžvilgiu,
vis tiek galime sukonkatenuoti šias dvi dviem skirtingais masteliais gautas prognozių išvestis tam pačiam minirinkiniui.
concat_preds([Y1, Y2]).shape
14.7.1.4 Mažinimo blokas
Kad aptiktume objektus keliais masteliais,
apibrėžiame šį mažinimo bloką down_sample_blk, kuris
perpus sumažina įvesties požymių žemėlapių aukštį ir plotį.
Iš tikrųjų
šis blokas taiko VGG blokų dizainą
iš 8.2.1 skyrelis.
Konkrečiau,
kiekvieną mažinimo bloką sudaro
du konvoliciniai sluoksniai su 1 papildymu,
po kurių eina sutelkimo sluoksnis imant maksimumą su 2 žingsniu.
Kaip žinome, konvoliciniai sluoksniai su 1 papildymu nekeičia požymių žemėlapių formos.
Tačiau paskesnis sutelkimo sluoksnis imant maksimumą perpus sumažina įvesties požymių žemėlapių aukštį ir plotį.
Tiek šio mažinimo bloko įvesties, tiek išvesties požymių žemėlapiams,
kadangi ,
kiekvienas išvesties vienetas
turi lokalią matymo sritį įvestyje.
Todėl mažinimo blokas padidina kiekvieno savo išvesties požymių žemėlapių vieneto lokalią matymo sritį.
def down_sample_blk(num_channels):
blk = nn.Sequential()
for _ in range(2):
blk.add(nn.Conv2D(num_channels, kernel_size=3, padding=1),
nn.BatchNorm(in_channels=num_channels),
nn.Activation('relu'))
blk.add(nn.MaxPool2D(2))
return blk
def down_sample_blk(in_channels, out_channels):
blk = []
for _ in range(2):
blk.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
blk.append(nn.BatchNorm2d(out_channels))
blk.append(nn.ReLU())
in_channels = out_channels
blk.append(nn.MaxPool2d(2))
return nn.Sequential(*blk)
Toliau pateiktame pavyzdyje mūsų sukonstruotas mažinimo blokas pakeičia įvesties kanalų skaičių ir perpus sumažina įvesties požymių žemėlapių aukštį ir plotį.
forward(np.zeros((2, 3, 20, 20)), down_sample_blk(10)).shape
forward(torch.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape
14.7.1.5 Bazinio tinklo blokas
Bazinio tinklo blokas naudojamas požymiams iš įvesties vaizdų išgauti. Paprastumo dėlei sukonstruojame mažą bazinį tinklą, sudarytą iš trijų mažinimo blokų, kurie kiekviename bloke padvigubina kanalų skaičių. Kai įvesties vaizdas yra , šis bazinio tinklo blokas išveda požymių žemėlapius ().
def base_net():
blk = nn.Sequential()
for num_filters in [16, 32, 64]:
blk.add(down_sample_blk(num_filters))
return blk
forward(np.zeros((2, 3, 256, 256)), base_net()).shape
def base_net():
blk = []
num_filters = [3, 16, 32, 64]
for i in range(len(num_filters) - 1):
blk.append(down_sample_blk(num_filters[i], num_filters[i+1]))
return nn.Sequential(*blk)
forward(torch.zeros((2, 3, 256, 256)), base_net()).shape
14.7.1.6 Pilnas modelis
Pilną vieno praėjimo daugiastačiakampio aptikimo modelį sudaro penki blokai. Kiekvieno bloko sukurti požymių žemėlapiai naudojami tiek (i) inkariniams stačiakampiams generuoti, tiek (ii) šių inkarinių stačiakampių klasėms ir poslinkiams prognozuoti. Iš šių penkių blokų pirmasis yra bazinio tinklo blokas, nuo antrojo iki ketvirtojo yra mažinimo blokai, o paskutinis blokas naudoja globalų sutelkimą imant maksimumą, kad sumažintų ir aukštį, ir plotį iki 1. Techniškai nuo antrojo iki penktojo blokai yra visi tie daugiamasteliai požymių žemėlapių blokai iš 14.7.1 pav..
def get_blk(i):
if i == 0:
blk = base_net()
elif i == 4:
blk = nn.GlobalMaxPool2D()
else:
blk = down_sample_blk(128)
return blk
def get_blk(i):
if i == 0:
blk = base_net()
elif i == 1:
blk = down_sample_blk(64, 128)
elif i == 4:
blk = nn.AdaptiveMaxPool2d((1,1))
else:
blk = down_sample_blk(128, 128)
return blk
Dabar apibrėžiame tiesioginį sklidimą
kiekvienam blokui.
Skirtingai nei
vaizdų klasifikavimo užduotyse,
išvestis čia apima
(i) CNN požymių žemėlapius Y,
(ii) inkarinius stačiakampius, sugeneruotus naudojant Y dabartiniu masteliu,
ir (iii) šių inkarinių stačiakampių prognozuotas (remiantis Y) klases ir poslinkius.
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
Y = blk(X)
anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio)
cls_preds = cls_predictor(Y)
bbox_preds = bbox_predictor(Y)
return (Y, anchors, cls_preds, bbox_preds)
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
Y = blk(X)
anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio)
cls_preds = cls_predictor(Y)
bbox_preds = bbox_predictor(Y)
return (Y, anchors, cls_preds, bbox_preds)
Prisiminkite, kad
14.7.1 pav.
daugiamastelis požymių žemėlapių blokas,
esantis arčiau viršaus,
skirtas didesniems objektams aptikti;
todėl jam reikia generuoti didesnius inkarinius stačiakampius.
Aukščiau pateiktame tiesioginiame sklidime
kiekviename daugiamasteliame požymių žemėlapių bloke
perduodame dviejų mastelio reikšmių sąrašą
per iškviestos funkcijos multibox_prior (aprašytos 14.4 skyrelis)
argumentą sizes.
Toliau
intervalas tarp 0,2 ir 1,05
tolygiai padalijamas
į penkias dalis, kad būtų nustatytos
mažesnės mastelio reikšmės penkiuose blokuose: 0,2, 0,37, 0,54, 0,71 ir 0,88.
Tada jų didesnės mastelio reikšmės
nustatomos pagal
, ir t. t.
Hyperparameters for each block
sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79],
[0.88, 0.961]]
ratios = [[1, 2, 0.5]] * 5
num_anchors = len(sizes[0]) + len(ratios[0]) - 1
Dabar galime apibrėžti pilną modelį TinySSD taip.
class TinySSD(nn.Block):
def __init__(self, num_classes, **kwargs):
super(TinySSD, self).__init__(**kwargs)
self.num_classes = num_classes
for i in range(5):
# Equivalent to the assignment statement `self.blk_i = get_blk(i)`
setattr(self, f'blk_{i}', get_blk(i))
setattr(self, f'cls_{i}', cls_predictor(num_anchors, num_classes))
setattr(self, f'bbox_{i}', bbox_predictor(num_anchors))
def forward(self, X):
anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
for i in range(5):
# Here `getattr(self, 'blk_%d' % i)` accesses `self.blk_i`
X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
anchors = np.concatenate(anchors, axis=1)
cls_preds = concat_preds(cls_preds)
cls_preds = cls_preds.reshape(
cls_preds.shape[0], -1, self.num_classes + 1)
bbox_preds = concat_preds(bbox_preds)
return anchors, cls_preds, bbox_preds
class TinySSD(nn.Module):
def __init__(self, num_classes, **kwargs):
super(TinySSD, self).__init__(**kwargs)
self.num_classes = num_classes
idx_to_in_channels = [64, 128, 128, 128, 128]
for i in range(5):
# Equivalent to the assignment statement `self.blk_i = get_blk(i)`
setattr(self, f'blk_{i}', get_blk(i))
setattr(self, f'cls_{i}', cls_predictor(idx_to_in_channels[i],
num_anchors, num_classes))
setattr(self, f'bbox_{i}', bbox_predictor(idx_to_in_channels[i],
num_anchors))
def forward(self, X):
anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
for i in range(5):
# Here `getattr(self, 'blk_%d' % i)` accesses `self.blk_i`
X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
anchors = torch.cat(anchors, dim=1)
cls_preds = concat_preds(cls_preds)
cls_preds = cls_preds.reshape(
cls_preds.shape[0], -1, self.num_classes + 1)
bbox_preds = concat_preds(bbox_preds)
return anchors, cls_preds, bbox_preds
Sukuriame modelio egzempliorių
ir naudojame jį tiesioginiam sklidimui atlikti
su vaizdų minirinkiniu X.
Kaip parodyta anksčiau šiame skyrelyje, pirmasis blokas išveda požymių žemėlapius. Prisiminkite, kad nuo antrojo iki ketvirtojo mažinimo blokai perpus sumažina aukštį ir plotį, o penktasis blokas naudoja globalų sutelkimą. Kadangi 4 inkariniai stačiakampiai generuojami kiekvienam vienetui išilgai požymių žemėlapių erdvinių dimensijų, visais penkiais masteliais kiekvienam vaizdui sugeneruojama iš viso inkariniai stačiakampiai.
net = TinySSD(num_classes=1)
net.initialize()
X = np.zeros((32, 3, 256, 256))
anchors, cls_preds, bbox_preds = net(X)
print('output anchors:', anchors.shape)
print('output class preds:', cls_preds.shape)
print('output bbox preds:', bbox_preds.shape)
net = TinySSD(num_classes=1)
X = torch.zeros((32, 3, 256, 256))
anchors, cls_preds, bbox_preds = net(X)
print('output anchors:', anchors.shape)
print('output class preds:', cls_preds.shape)
print('output bbox preds:', bbox_preds.shape)
14.7.2 Apmokymas
Dabar paaiškinsime, kaip apmokyti vieno praėjimo daugiastačiakampio aptikimo modelį objektams aptikti.
14.7.2.1 Duomenų rinkinio skaitymas ir modelio inicializavimas
Pirmiausia nuskaitykime bananų aptikimo duomenų rinkinį, aprašytą 14.6 skyrelis.
batch_size = 32
train_iter, _ = d2l.load_data_bananas(batch_size)
Bananų aptikimo duomenų rinkinyje yra tik viena klasė. Apibrėžę modelį, turime (inicializuoti jo parametrus ir apibrėžti optimizavimo algoritmą).
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
net.initialize(init=init.Xavier(), ctx=device)
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'learning_rate': 0.2, 'wd': 5e-4})
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4)
14.7.2.2 Nuostolių ir vertinimo funkcijų apibrėžimas
Objektų aptikimas turi du nuostolių tipus.
Pirmasis nuostolis susijęs su inkarinių stačiakampių klasėmis:
jam apskaičiuoti
galima tiesiog pakartotinai naudoti
kryžminės entropijos nuostolių funkciją,
kurią naudojome vaizdų klasifikavimui.
Antrasis nuostolis
susijęs su teigiamų (nefono) inkarinių stačiakampių poslinkiais:
tai yra regresijos uždavinys.
Tačiau šiam regresijos uždaviniui
čia nenaudojame kvadratinio nuostolio,
aprašyto 3.1.3 skyrelis.
Vietoj to
naudojame normos nuostolį,
prognozės ir tikrosios reikšmės skirtumo
absoliučiąją vertę.
Kaukės kintamasis bbox_masks nuostolio skaičiavime išfiltruoja
neigiamus inkarinius stačiakampius ir neleistinus (papildymo)
inkarinius stačiakampius.
Galiausiai susumuojame
inkarinių stačiakampių klasių nuostolį
ir inkarinių stačiakampių poslinkių nuostolį,
kad gautume modelio nuostolių funkciją.
cls_loss = gluon.loss.SoftmaxCrossEntropyLoss()
bbox_loss = gluon.loss.L1Loss()
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
cls = cls_loss(cls_preds, cls_labels)
bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks)
return cls + bbox
cls_loss = nn.CrossEntropyLoss(reduction='none')
bbox_loss = nn.L1Loss(reduction='none')
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
cls = cls_loss(cls_preds.reshape(-1, num_classes),
cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1)
bbox = bbox_loss(bbox_preds * bbox_masks,
bbox_labels * bbox_masks).mean(dim=1)
return cls + bbox
Klasifikavimo rezultatams įvertinti galime naudoti tikslumą. Kadangi poslinkiams naudojamas normos nuostolis, prognozuotiems apibrėžiamiesiems stačiakampiams įvertinti naudojame vidutinę absoliučiąją paklaidą. Šie prognozavimo rezultatai gaunami iš sugeneruotų inkarinių stačiakampių ir jiems prognozuotų poslinkių.
def cls_eval(cls_preds, cls_labels):
# Because the class prediction results are on the final dimension,
# `argmax` needs to specify this dimension
return float((cls_preds.argmax(axis=-1).astype(
cls_labels.dtype) == cls_labels).sum())
def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
return float((np.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())
def cls_eval(cls_preds, cls_labels):
# Because the class prediction results are on the final dimension,
# `argmax` needs to specify this dimension
return float((cls_preds.argmax(dim=-1).type(
cls_labels.dtype) == cls_labels).sum())
def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())
14.7.2.3 Modelio apmokymas
Apmokydami modelį,
tiesioginiame sklidime turime generuoti daugiamastelius inkarinius stačiakampius (anchors)
ir prognozuoti jų klases (cls_preds) bei poslinkius (bbox_preds).
Tada pažymime tokių sugeneruotų inkarinių stačiakampių klases (cls_labels) ir poslinkius (bbox_labels)
pagal žymenų informaciją Y.
Galiausiai apskaičiuojame nuostolių funkciją
naudodami prognozuotas ir pažymėtas
klasių bei poslinkių reikšmes.
Glaustiems įgyvendinimams
testavimo duomenų rinkinio vertinimas čia praleistas.
num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['class error', 'bbox mae'])
for epoch in range(num_epochs):
# Sum of training accuracy, no. of examples in sum of training accuracy,
# Sum of absolute error, no. of examples in sum of absolute error
metric = d2l.Accumulator(4)
for features, target in train_iter:
timer.start()
X = features.as_in_ctx(device)
Y = target.as_in_ctx(device)
with autograd.record():
# Generate multiscale anchor boxes and predict their classes and
# offsets
anchors, cls_preds, bbox_preds = net(X)
# Label the classes and offsets of these anchor boxes
bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors,
Y)
# Calculate the loss function using the predicted and labeled
# values of the classes and offsets
l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
bbox_masks)
l.backward()
trainer.step(batch_size)
metric.add(cls_eval(cls_preds, cls_labels), cls_labels.size,
bbox_eval(bbox_preds, bbox_labels, bbox_masks),
bbox_labels.size)
cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter._dataset) / timer.stop():.1f} examples/sec on '
f'{str(device)}')
num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['class error', 'bbox mae'])
net = net.to(device)
for epoch in range(num_epochs):
# Sum of training accuracy, no. of examples in sum of training accuracy,
# Sum of absolute error, no. of examples in sum of absolute error
metric = d2l.Accumulator(4)
net.train()
for features, target in train_iter:
timer.start()
trainer.zero_grad()
X, Y = features.to(device), target.to(device)
# Generate multiscale anchor boxes and predict their classes and
# offsets
anchors, cls_preds, bbox_preds = net(X)
# Label the classes and offsets of these anchor boxes
bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)
# Calculate the loss function using the predicted and labeled values
# of the classes and offsets
l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
bbox_masks)
l.mean().backward()
trainer.step()
metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(),
bbox_eval(bbox_preds, bbox_labels, bbox_masks),
bbox_labels.numel())
cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on '
f'{str(device)}')
14.7.3 Prognozavimas
Prognozavimo metu tikslas yra aptikti visus dominančius objektus vaizde. Toliau nuskaitome ir pakeičiame testinio vaizdo dydį, paversdami jį keturmačiu tenzoriumi, kurio reikalauja konvoliciniai sluoksniai.
img = image.imread('../img/banana.jpg')
feature = image.imresize(img, 256, 256).astype('float32')
X = np.expand_dims(feature.transpose(2, 0, 1), axis=0)
X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float()
img = X.squeeze(0).permute(1, 2, 0).long()
Naudojant toliau pateiktą funkciją multibox_detection,
prognozuojami apibrėžiamieji stačiakampiai
gaunami
iš inkarinių stačiakampių ir jų prognozuotų poslinkių.
Tada nemaksimalių reikšmių slopinimas naudojamas
panašiems prognozuojamiems apibrėžiamiesiems stačiakampiams pašalinti.
def predict(X):
anchors, cls_preds, bbox_preds = net(X.as_in_ctx(device))
cls_probs = npx.softmax(cls_preds).transpose(0, 2, 1)
output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
return output[0, idx]
output = predict(X)
def predict(X):
net.eval()
anchors, cls_preds, bbox_preds = net(X.to(device))
cls_probs = F.softmax(cls_preds, dim=2).permute(0, 2, 1)
output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
return output[0, idx]
output = predict(X)
Galiausiai kaip išvestį parodome visus prognozuojamus apibrėžiamuosius stačiakampius, kurių pasitikėjimas yra 0,9 arba didesnis.
def display(img, output, threshold):
d2l.set_figsize((5, 5))
fig = d2l.plt.imshow(img.asnumpy())
for row in output:
score = float(row[1])
if score < threshold:
continue
h, w = img.shape[:2]
bbox = [row[2:6] * np.array((w, h, w, h), ctx=row.ctx)]
d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')
display(img, output, threshold=0.9)
def display(img, output, threshold):
d2l.set_figsize((5, 5))
fig = d2l.plt.imshow(img)
for row in output:
score = float(row[1])
if score < threshold:
continue
h, w = img.shape[:2]
bbox = [row[2:6] * torch.tensor((w, h, w, h), device=row.device)]
d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')
display(img, output.cpu(), threshold=0.9)
14.7.4 Santrauka
- Vieno praėjimo daugiastačiakampis aptikimas yra daugiamastelis objektų aptikimo modelis. Per savo bazinį tinklą ir kelis daugiamastelius požymių žemėlapių blokus vieno praėjimo daugiastačiakampis aptikimas generuoja kintamą skirtingo dydžio inkarinių stačiakampių skaičių ir aptinka skirtingo dydžio objektus prognozuodamas šių inkarinių stačiakampių (taigi ir apibrėžiamųjų stačiakampių) klases ir poslinkius.
- Apmokant vieno praėjimo daugiastačiakampio aptikimo modelį, nuostolių funkcija apskaičiuojama pagal prognozuotas ir pažymėtas inkarinių stačiakampių klasių ir poslinkių reikšmes.
14.7.5 Pratimai
- Ar galite pagerinti vieno praėjimo daugiastačiakampį aptikimą patobulindami nuostolių funkciją? Pavyzdžiui, pakeiskite normos nuostolį glotniuoju normos nuostoliu prognozuotiems poslinkiams. Ši nuostolių funkcija aplink nulį naudoja kvadratinę funkciją glotnumui užtikrinti, kurį valdo hiperparametras :
Kai yra labai didelis, šis nuostolis panašus į normos nuostolį. Kai jo reikšmė mažesnė, nuostolių funkcija yra glotnesnė.
sigmas = [10, 1, 0.5]
lines = ['-', '--', '-.']
x = np.arange(-2, 2, 0.1)
d2l.set_figsize()
for l, s in zip(lines, sigmas):
y = npx.smooth_l1(x, scalar=s)
d2l.plt.plot(x.asnumpy(), y.asnumpy(), l, label='sigma=%.1f' % s)
d2l.plt.legend();
def smooth_l1(data, scalar):
out = []
for i in data:
if abs(i) < 1 / (scalar ** 2):
out.append(((scalar * i) ** 2) / 2)
else:
out.append(abs(i) - 0.5 / (scalar ** 2))
return torch.tensor(out)
sigmas = [10, 1, 0.5]
lines = ['-', '--', '-.']
x = torch.arange(-2, 2, 0.1)
d2l.set_figsize()
for l, s in zip(lines, sigmas):
y = smooth_l1(x, scalar=s)
d2l.plt.plot(x, y, l, label='sigma=%.1f' % s)
d2l.plt.legend();
Be to, eksperimente klasėms prognozuoti naudojome kryžminės entropijos nuostolį: pažymėję prognozuotą tikrosios klasės tikimybę, kryžminės entropijos nuostolis yra . Taip pat galime naudoti židininį nuostolį (angl. focal loss) (Lin ir kt., 2017): turint hiperparametrus ir , šis nuostolis apibrėžiamas taip:
Kaip matome, didinant galima veiksmingai sumažinti santykinį nuostolį gerai suklasifikuotiems pavyzdžiams (pvz., ), todėl apmokymas gali labiau susitelkti į tuos sunkius pavyzdžius, kurie suklasifikuoti neteisingai.
def focal_loss(gamma, x):
return -(1 - x) ** gamma * np.log(x)
x = np.arange(0.01, 1, 0.01)
for l, gamma in zip(lines, [0, 1, 5]):
y = d2l.plt.plot(x.asnumpy(), focal_loss(gamma, x).asnumpy(), l,
label='gamma=%.1f' % gamma)
d2l.plt.legend();
def focal_loss(gamma, x):
return -(1 - x) ** gamma * torch.log(x)
x = torch.arange(0.01, 1, 0.01)
for l, gamma in zip(lines, [0, 1, 5]):
y = d2l.plt.plot(x, focal_loss(gamma, x), l, label='gamma=%.1f' % gamma)
d2l.plt.legend();
- Dėl vietos stokos šiame skyrelyje praleidome kai kurias vieno praėjimo daugiastačiakampio aptikimo modelio įgyvendinimo detales. Ar galite toliau pagerinti modelį šiais aspektais:
- Kai objektas yra daug mažesnis už vaizdą, modelis galėtų padidinti įvesties vaizdo dydį.
- Paprastai yra daugybė neigiamų inkarinių stačiakampių. Kad klasių pasiskirstymas būtų labiau subalansuotas, galėtume sumažinti neigiamų inkarinių stačiakampių imtį.
- Nuostolių funkcijoje klasių nuostoliui ir poslinkių nuostoliui priskirkite skirtingus svorio hiperparametrus.
- Naudokite kitus objektų aptikimo modelio vertinimo metodus, pavyzdžiui, aprašytus vieno praėjimo daugiastačiakampio aptikimo straipsnyje (Liu ir kt., 2016).
