Mësimi i 9-të në kursin e të mësuarit të thellë të fast.ai vazhdon zhytjen në Rrjetet Generative Adversarial (GANs) nga këndvështrimi i të bërit art. Kjo më së shumti ka të bëjë me fletoret e Jupyter "neural-style" dhe "neural-sr" në depon e kurseve (siç është shkruar). Shënim: ky postim është i gjatë. Fillon thjeshtë dhe vazhdon më thellë.

Në "Mësimi 8" mësuam të marrim një imazh, ta ngjitim atë në një funksion humbjeje që përbëhet nga humbja e stilit dhe humbja e përmbajtjes, duke nxjerrë humbjen dhe gradientët, dhe t'i përdorim ato gradientë për të përditësuar imazhin dhe për të zvogëluar humbjen në tjetrën. përsëritje - dhe shpëlajeni dhe përsërisni. Si kështu:

Sipas Jeremy Howard në leksion, kjo është pothuajse mënyra më e mirë për të bërë transferimin e stilit për shumë stile dhe imazhe të ndryshme, si në një aplikacion ueb. Megjithatë, nëse doni të aplikoni një stil të vetëm për çdo imazh: këtu mund t'i ndryshojmë gjërat për të bërë shumë më mirë. Arsyeja është që ju nuk keni nevojë të bëni ciklin e mësipërm të optimizimit për të prodhuar diçka. Në vend të kësaj, thjesht mund të stërvitni një CNN që të mësojë se si të nxjerrë imazhe në një stil të caktuar (dhe një kalim përpara përmes një CNN është shumë i shpejtë - shkëmbimi duke qenë se CNN është trajnuar në atë stil të veçantë).

Këtë herë ne fillojmë duke ushqyer imazhe të shumta në të njëjtin funksion të humbjes si më parë (Style + Content). Thuaj në këtë shembull, për Style-Loss ne përdorim Irises të Van Gogh, dhe për Humbjen e Përmbajtjes ne përdorim imazhin aktual që futet në funksion. Dallimi i madh këtu është se ne do të vendosim një CNN midis imazheve hyrëse dhe funksionit të humbjes. Ai CNN do të mësojë të nxjerrë një imazh të tillë që kur të hidhet në funksionin e humbjes, do të nxjerrë një vlerë të vogël për humbjen. Në anglisht kjo do të thotë se CNN ka mësuar të prodhojë imazhe që janë të ngjashme në përmbajtje me imazhin origjinal të hyrjes, ndërsa janë gjithashtu stilistikisht të ngjashme me çdo gjë që është zgjedhur për stil.

Disa shënime këtu. Me CNN-të mund të zgjidhni çdo funksion humbjeje që dëshironi (kilometrazhet tuaja mund të ndryshojnë). "Deep Learning I" & "II" deri më tani ka përdorur mjaft të thjeshta si "MSE" dhe "Cross-Entropy". Sidoqoftë, këtu, ne do të përdorim Style + Humbja e Përmbajtjes, si në shembullin e parë më lart. Shënim: sepse funksioni Style+Content Loss u krijua nga një rrjet nervor: është i diferencueshëm; Dhe çdo funksion i humbjes së diferencuar mund të optimizohet.

Tani nëse marrim gradientët e daljes në lidhje me peshat e CNN, ne mund t'i përdorim ato për të përditësuar ato pesha në mënyrë që CNN të jetë më i mirë në shndërrimin e një imazhi në një imazh që përputhet me stilin.

Ajo që është e fuqishme në lidhje me këtë, përsëri, është se nuk ka asnjë hap optimizimi për të ngadalësuar përfundimin - ose funksionimin e 'parashikimit' të modelit. Kalimi i vetëm përpara përmes CNN është pak a shumë i menjëhershëm.

Ka më shumë që mund të bëni.

Rezulton se Rrjetet Neurale të Mësimit të Thellë mund të përdoren për të zmadhuar imazhet dhe për të konstatuar detajet.

Ne fillojmë duke vendosur një CNN midis imazheve LoRes dhe HiRes. CNN merr imazhin LoRes si hyrje dhe dërgon daljen e tij në një funksion humbjeje që llogarit vetëm humbjen e përmbajtjes. Humbja e përmbajtjes llogaritet duke krahasuar hyrjen e saj nga CNN me aktivizimet nga imazhi HiRes. Me fjalë të tjera, funksioni i humbjes po kontrollon nëse CNN ka krijuar një imazh më të madh nga LoRes origjinal, që ka të njëjtat aktivizime si ai i HiRes.

Ky diskutim nxiti një nga pyetjet më të mira që kam dëgjuar në klasë:

A mund të vendosim një CNN midis dy gjërave dhe do të mësojë marrëdhënien midis tyre?

Jeremy Howard:

Po, absolutisht.

Dhe kjo hyn në diçka që ne po e bëjmë ndryshe nga më parë me CNNS. Një "letër 2016" detajon duke përdorur një funksion të ndryshëm humbjeje për CNN për të bërë Super-Rezolucioni. Përpara kësaj, "MSE" përdorej në mënyrë tipike - midis daljeve të pikselit të rrjetit të rritjes së shkallës dhe imazhit HiRes. Problemi është: një imazh i paqartë do të vazhdojë të performojë mirë nën "MSE", por kjo është disi e papranueshme për detyrat e imazhit. Ne anen tjeter:

…nëse merrni bllokun e 2-të ose të tretë të konvertimit të VGG, atëherë duhet të dijë se ky është një kokërr syri ose nuk do të duket mirë.

Mësimi: përdorni Humbjen e Përmbajtjes në vend të Humbjes së Pixelit.(si mënjanë: me sa duket u desh 1 vit për të kaluar nga përdorimi i "MSE" në piksel në humbjen e përmbajtjes. Ka diçka interesante për të parë kohën që duhet për të zbuluar gjëra që duken intuitive në retrospektivë.)

Letra rendit disa aplikacione të dobishme të kësaj metode — duke i inkuadruar ato si detyra transformimi i imazhit:

  • Përpunimi i imazhit: Denoising, super-rezolucion, ngjyrosje
  • Vizioni kompjuterik: segmentimi semantik, vlerësimi i thellësisë

(një tjetër mënjanë këtu është me modelet gjenerative që mund të prodhoni sa më shumë të dhëna të etiketuara që dëshironi duke shtuar vetëm zhurmë ose duke bërë imazhe me ngjyra në shkallë gri, etj.)

2. Tani për të hyrë në kod. Shihni neural-style.ipynb nëse doni ta ndiqni (Shënim: përditësimet e ardhshme të kursit mund të ndryshojnë kodin ose fletoret fare).

Kodi i mësipërm fillon duke marrë një grup imazhesh me rezolucion të ulët si tensor me dimensione arr_lr.shape[1:] (që këtu do të jetë: (72, 72, 3)). Në fletore, arr_lr dhe arr_hr janë vargje NumPy.

inp = Input(arr_lr.shape[1:])

Kjo hyrje vendoset më pas në një bllok konvolucional (të përcaktuar më herët në fletore) me 64 filtra të madhësisë 9, dhe një hap të njësisë (dmth. pa ndryshime në madhësinë e hyrjes).

x = conv_block(inp, 64, 9, (1,1))

Kjo është një madhësi e madhe filtri sepse ne duam një fushë të madhe pranuese fillestare. Me sa duket, të kesh 64 prej tyre siguron që asnjë informacion të mos humbasë (që vijnë nga 3 kanale ngjyrash), por nuk e di se si duket ajo kurbë e humbjes së informacionit të filtrave të hyrjes - kundër -. Por mesa duket edhe kjo po bëhet e zakonshme.

Më pas është vendi ku do të bëhet pjesa më e madhe e llogaritjes. Kjo është pjesa ku GAN duhet të kuptojë, për shembull, është ajo pikë e zezë një kokërr syri dhe më pas si do të dukej një kokërr syri me rezolucion më të lartë.

for i in range(4): x = res_block(x) # res_block() është një bllok ResNet i përcaktuar diku tjetër.

Në modelet gjeneruese ne do të duam ta bëjmë këtë hap kryesor llogaritës me një rezolucion të ulët. Rezolucioni i ulët do të thotë se ka më pak punë, kështu që llogaritja funksionon më shpejt, por më e rëndësishmja: do të kemi një fushë pranuese më të madhe. Pse ka rëndësi kjo? Ju mund të imagjinoni lehtësisht se si duket një version i shkallëzuar i një fotografie të vogël të fytyrës së dikujt sepse e dini se si duken fytyrat. Ju e dini se ajo gjë e errët është një sy dhe ajo zonë e errët është një hije e hedhur nga nofullat e tyre. Ju i dini të gjitha këto për të filluar, sepse mund ta shikoni të gjithë foton me një kalim dhe ta njihni atë si një fytyrë dhe jo si një pjatë darke ose një pjesë të artit abstrakt. GAN duhet të bëjë të njëjtën gjë.

Ka disa gjëra që përcaktojnë një fushë pritëse: thellësia e shtresës konvolucionale, madhësia e filtrit dhe kampionimi poshtë (hapësi jo njësi, Max-Pooling, etj).

Kthehu te kodi. Blloqet ResNet janë përcaktuar kështu:

Ky bllok merr një hyrje, e përdor atë për të ndërtuar dy blloqe Convolutional (nëpërmjet API funksionale të Keras), më pas shton rezultatin e këtyre blloqeve Conv përsëri në hyrjen origjinale: return merge([x, ip], mode='sum') Blloku përfundimtar Conv nuk ka aktivizim (act=False) për shkak në një dokument të fundit (nuk jam i sigurt se cilin) ​​gjetja e blloqeve ResNet në përgjithësi funksionojnë më mirë pa.

Nga ajo që kam kuptuar deri më tani në "Deep Residual Networks", forca e ResNet është se i jep çdo shtrese të rrjetit një kontekst rreth hyrjes origjinale. Në leksion, Jeremy Howard thotë se një grumbull blloqesh ResNet mund të përmirësojnë çdo gjë që duhet të bëjnë për të përmbushur një detyrë (këtu: përmirësimi i një imazhi).

y = f(x) + x;f(x) = y — x; ku y — x = the residual

Pra, nëse funksioni është diferenca midis daljes y dhe hyrjes x, - mbetjes - dhe kjo jep disa informacione kontekstuale se si të minimizohet funksioni i humbjes? Do të më duhet të mësoj më shumë për këtë me kalimin e kohës.

conv_block është mjaft standard:

Në këtë pikë ne duhet të ndryshojmë dimensionet e imazhit. Ligjërata origjinale përdori blloqe Dekonvolucioni të cilat kryen një "de/transpozim/përthyerje fraksionale - me hapa të shpejtë" për të rikrijuar se si do të dukej një imazh nëse versioni aktual (hyrja) do të ishte rezultat i konvolucioneve. E thënë thjesht: është një konvolucion në një hyrje (zero) - të mbushur. Nëse dëshironi të zhyteni më tej, kjo është lidhja e referuar në leksion. Në depon e kursit github, versioni aktual i fletores së mësimit, neural-style.ipynb si dhe neural-sr.ipynb përdorin të dyja një up_block(..) të përcaktuar si:

Pra, nëse e kuptoj saktë, ky është paksa një evolucion logjik i kodit të kursit. Një problem me Transposed/Fract-Strided/Deconvolutions është shami: ju merrni objekte në një model shahu ku filtrat konvolucionalë mbivendosen, në varësi të gjatësisë së hapit. Ekziston një "punë e shkëlqyer për këtë" e shkruar nga disa studiues në Google Brain dhe Université de Montréal.

E thënë thjesht: një hap prej 2 — stride=(2,2) — do të thotë se filtri lëviz në hapa prej 2 pikselësh ose qelizave ndërsa skanon në të gjithë hyrjen, dhe size=3 do të thotë se filtri është një katror 3x3. Shikoni figurën e mësipërme dhe problemi bëhet i dukshëm. Një mënyrë për ta korrigjuar këtë është të siguroheni që madhësia e filtrit tuaj të jetë shumëfish i hapit tuaj. Metoda tjetër është të mos përdorni Deconvolutions fare.

Në vend të kësaj, punimi sugjeron që së pari të bëni një Upsampling, i cili është në thelb e kundërta e një operacioni Max-Pooling: kështu për shembull çdo piksel i vetëm bëhet një rrjet 2x2 i të njëjtit piksel. Bërja e një ngritjeje të mostrës e ndjekur nga një konvolucion i rregullt rezulton në mungesë shami. Dhe ja, kjo duket saktësisht si ajo që ka bërë Jeremy Howard në versionin më të ri të kodit. I ftohtë.

Pra, për të shqyrtuar pak më shumë këtë: kodi origjinal përdori dy blloqe Deconv për të zmadhuar imazhin lo-res nga 72x72144x144288x288. Kodi më i ri e realizon këtë me dy blloqe Upsampling, të cilat secili kryen një operacion të vetëm Upsampling2D() të ndjekur nga një konvolucion dhe një normalizim batch. Dokumentacioni Keras për UpSampling2D() tregon:

keras.layers.convolutional.UpSampling2D(size=(2,2), data_format=None)

Pjesa e rëndësishme për ne është parametri size:

madhësia: int, ose tuple prej 2 numrash të plotë. Faktorët e ngritjes së mostrës për rreshtat dhe kolonat.

Pra, sjellja e parazgjedhur është thjesht dyfishimi i madhësisë së imazhit, kjo është arsyeja pse dy thirrje boshe në UpSampling2D() realizojnë efektin e dëshiruar.

Pas rritjes, kemi një shtresë përfundimtare 9x9 konvolucionale në mënyrë që të kthehemi në 3 kanale me ngjyra. Shpjegimi në leksion është se duke shkuar nga 64 filtra ose kanale në 3 për daljen e imazhit, ne duam shumë kontekst (prandaj 9x9) që të mund ta bëjmë këtë. Pra, duke vendosur tensorin e mëparshëm (288, 288, 64) përmes shtresës përfundimtare Convolutional nxjerr një tensor (288, 288, 3): një imazh. Linja përfundimtare outp = Lambda(lambda x: (x+1) * 127.5)(x) ka të bëjë me vendosjen e secilit piksel në vlerësimin e saktë të intensitetit të ngjyrës 0,255 nga aktivizimi i mëparshëm tanh. Tanh është në rangun 0±1 mendoj, kështu që → ([-1:+1] + 1) * 127.5 = [0:255] . Lambda(..) është një mbështjellës Keras që trajton gjithçka brenda si një shtresë të personalizuar. Shënim: një nga autorët e "Perceptual Loss's paper" përmendi në reddit se ata drejtuan rrjetin pa një aktivizim tanh dhe pa shtresën përfundimtare të përpunimit Lambda dhe morën rezultate sa më të mira ose më të mira (pa lidhje).

Rrjeti tani mund të trajnohet, por ne ende duhet të përcaktojmë funksionin tonë të humbjes. Dmth: cila është metrika me të cilën rrjeti po matë veten? Së pari do të marrim Rrjetin tonë të Upsampling dhe do ta bashkojmë atë në VGG. VGG do të përdoret vetëm si një funksion humbjeje për Humbjen e Përmbajtjes. Përpara se outp tensori dalës i rrjetit Upsamling të vendoset në VGG, ai duhet të përpunohet paraprakisht (normalizimi i përdorur në modelin VGG). Ne e bëjmë këtë duke përcaktuar një shtresë të personalizuar të parapërpunimit Keras dhe duke përcaktuar një tensor të ri dalës outp_λ si rezultat i ekzekutimit të outp përmes shtresës së parapërpunimit:

vgg_λ = Lambda(preproc)
outp_λ = vgg_λ(outp)
# I just discovered code blocks in Medium :D (```)

Më pas krijojmë një rrjet VGG16 dhe vendosim të gjitha shtresat e tij në të patrajnueshme. Kjo është... disi e rëndësishme. Për shkak se ky rrjet është funksioni ynë i humbjes - shkopi ynë matës - ai duhet të jetë statik. Imagjinoni sikur kilogrami ose metri të ndryshojë. Nëse doni të shihni kaos të drejtpërdrejtë, thjesht hidhini një sy tregjeve FOREX.

shp = arr_hr.shape[1:]
vgg_inp = Input(shp)
vgg     = VGG16(include_top=False, input_tensor=vgg_λ(vgg_inp))
for λ in vgg.layers: λ.trainable=False

Forma e tensorit të hyrjes shp për rrjetin VGG është dimensionet e imazhit HiRes. Prandaj: shp = arr_hr.shape[1:] dhe vgg_inp = Input(shp). arr_hr është një grup NumPy i krijuar nga një grup i ngjeshur bcolz në disk, dhe dimensionet (forma) e tij janë: (num-elemente, dim1, dim2, dim3, ...). Në rastin tonë është: (nr. imazhe, lartësi, gjerësi, kanale ngjyrash). Pra, duke marrë arr_hr.shape[1:] do të kthejë një tensor të formës: (288, 288 , 3). include_top=False për VGG16(..) do të heqë bllokun e klasifikimit "Plotësisht i lidhur" në fund të rrjetit. Ky është zbatimi fast.ai i VGG16 me Normalizimin e Bashkimit mesatar dhe Batch që gjendet në: vgg16_avg.py (lidhjet mund të prishen nëse struktura e drejtorisë së kursit ndryshon).

Për marrjen e pjesëve të rrjetit VGG që duam për Humbjen e Përmbajtjes, ka disa implementime. Leksioni origjinal i klasës krijoi një model të vetëm duke zgjedhur një bllok të vetëm në të cilin do të nxirren veçoritë (veçanërisht conv layer 2 në bllokun 2). Në depon e kursit kjo ka evoluar në një funksion për të tërhequr veçori nga blloqe të shumta.

Zbatimi i leksionit:

vgg_content = Model(vgg_inp, vgg.get_layer('block2_conv2').output)
vgg1 = vgg_content(vgg_inp)
vgg2 = vgg_content(outp_λ)

Zbatimi i repove të kursit të përditësuar në neural-sr.ipynb:

Në secilin rast, ne përdorim aktivizime relativisht të hershme nga modeli. Në L8 zbuluam se mund të rindërtonim plotësisht një imazh duke përdorur aktivizimet e hershme të shtresave. Më pas krijojmë dy versione të daljes së VGG duke përdorur API funksionale të Keras. Një gjë e mrekullueshme për këtë Keras API është se çdo shtresë (dhe modelet trajtohen si shtresa) mund të trajtohet sikur të ishte një funksion. Më pas mund të kalojmë në atë "funksion" çdo tensor që na pëlqen, dhe Keras do të krijojë një model të ri ku këto dy pjesë të bashkohen së bashku. Pra, vgg2 është outp, rrjeti UpSampling në fund, dhe vgg_content, pjesa e VGG16 që duam të përdorim si funksionin tonë të humbjes, në krye.

Mënjanë: këtu mund të jeni pak konfuz. A nuk duhet të them outp_λ dhe jo outp? Epo, është outp_λ në kodin e leksionit: ky është tensori i daljes pas kalimit përmes një shtrese Lambda të përpunimit paraprak. Megjithatë, ky hap përfshihet në thirrjen e inicializimit për modelin VGG16 në kodin e përditësuar të kursit. Thirrja e re është:

vgg = VGG16(include_top=False, input_tensor=Lambda(preproc)(vgg_inp))

Pra, tani kemi dy versione të daljes së shtresës VGG. vgg1 bazohet në hyrjen HiRes, dhe vgg2 në bazë të daljes së Rrjetit UpScaling (i cili mori hyrjen LoRes). Ne do të krahasojmë imazhin e objektivit të HiRes dhe rezultatin e përmbledhur të HiRes, kështu që të dy tensorët kanë formën hi-res (288, 288, 3). Thjesht për të qenë të qartë: vgg1 është përmbajtja/aktivizimet perceptuese të HiRes dhe vgg2 është përmbajtja/aktivizimet perceptuese të riprodhuara nga LoRes.

Shënim: Ka dallime të rëndësishme midis leksionit origjinal dhe kodit të përditësuar të depove për përcaktimin e funksionit të humbjes dhe modelit përfundimtar. Së pari do të kaloj versionin e leksionit, më pas do të shqyrtoj të rejat.

Më pas marrim gabimin mesatar në katror ndërmjet tyre. Sa herë që dëshironi të vendosni diçka në një rrjet në Keras, duhet ta ktheni atë në një shtresë, dhe kjo bëhet duke e hedhur atë në një Lambda(..):

loss = Lambda(lambda x: K.sqrt(K.mean((x[0]-x[1])**2, (1,2))))([vgg1, vgg2])

Modeli përfundimtar do të marrë hyrjen LoRes dhe hyrjen HiRes si dy hyrjet e tij, dhe do të kthejë funksionin e humbjes të përcaktuar si dalje të tij:

m_final = Model([inp, vgg_inp], loss)

Gjithashtu, kur po përpiqeni të përshtatni gjërat në Keras, supozohet se po përpiqeni të merrni një rezultat dhe t'i afroni një objektivi. Këtu humbja është funksioni aktual i humbjes që duam; nuk ka asnjë objektiv, ne e duam atë sa më të vogël. Meqenëse funksioni ynë i humbjes po përdor MSE, mund të themi vetëm se objektivi ynë është zero. Sidoqoftë, objektivat në Keras janë etiketa, kështu që duhet të ketë një për çdo rresht (çdo hyrje), kështu që ne kemi nevojë për një grup me zero:

targ = np.zeros((arr_hr.shape[0], 128))

Pse 128 zero për çdo imazh? [Përgjigjur në leksion]: shtresa Lambda, loss, që korrespondon me grupin e synuar ka 128 filtra. 128 sepse ajo shtresë po merr të dyja vgg1 dhe vgg2, që të dyja kanë 64 filtra.

Më pas ne përpilojmë modelin duke përdorur optimizuesin Adam dhe humbjen MSE, dhe e përshtatim atë:

m_final.compile('adam', 'mse')
m_final.fit([arr_lr, arr_hr], targ, 8, 2, **pars)

Shënim: pars përmban disa argumente parametrash të përcaktuar në këtë mënyrë: pars = {'verbose': 0, 'callbacks': [TQDMNotebookCallback(leave_inner=True)]} — kjo çaktivizon verbozitetin dhe përdor TQDM për raportimin e statusit.

Pas raundit të parë të trajnimit, shkalla e të mësuarit lehtësohet pak (i ​​quajtur 'Pjekja e shkallës së të mësuarit'):

K.set_value(m_final.optimizer.lr, 1e-4)
m_final.fit([arr_lr, arr_hr], targ, 16, 2, **pars)

Pas trajnimit, pjesa e modelit tonë që na intereson është rrjeti Upsamling. Pasi të kemi mbaruar stërvitjen dhe duke u përpjekur të minimizojmë funksionin e humbjes, nuk na intereson më humbja: ne kujdesemi për imazhet dalëse që dalin nga rrjeti Upsampling. Pra, tani ne përcaktojmë një model që merr hyrjen LoRes inp dhe kthen një dalje HiRes outp:

top_model = Model(inp, outp)

Kjo më ngatërroi pak kur isha duke bërë detyrën për herë të parë: outp është trajnuar gjatë hapit tonë të trajnimit më lart. Në fakt, nëse mbani mend: inp dhe outp janë vetëm shtresa hyrëse dhe aktivizimet dalëse të rrjetit tonë Upsampling. Ne në thelb ndërtuam një makinë për të mostruar imazhe, përdorëm një modul ekzistues (VGG16) për të trajnuar atë makinë duke e ngjitur sipër saj, më pas e shkëputëm atë modul kur u krye dhe tani kemi një makinë të trajnuar që përmirëson imazhet.

Tani, mënyra se si e bëra, duke dalë nga mënyra se si është bërë në depon e kursit është pak më ndryshe. Seksioni pas përcaktimit të vgg1 dhe vgg2 dhe para stërvitjes duket kështu:

def mean_sqr_b(diff):
    dims = list(range(1, K.ndim(diff)))
    return K.expand_dims(K.sqrt(K.mean(diff**2, dims)), 0)
w = [0.1, 0.8, 0.1]
def content_fn(x):
    res = 0; n=len(w)
    for i in range(n): res += mean_sqr_b(x[i]-x[i+n]) * w[i]
    return res
m_sr = Model([inp, vgg_inp], Lambda(content_fn)(vgg1+vgg2)
m_sr.compile('adam', 'mae')

Mbani mend se si kodi më i ri përdorte një model me shumë dalje që merrte aktivizime në blloqe të ndryshme në VGG16? Epo, ne do të peshojmë ndikimin relativ të çdo blloku në funksionin tonë të humbjes: w = [0.1, 0.8, 0.1] do të thotë block1_conv2 peshohet me 10%, block_2_conv2 me 80%, etj.

Funksioni ynë i humbjes në vetvete është një funksion brenda një funksioni. content_fn(x) merr një tensor x dhe nxjerr një vlerë skalare të humbjes res. Për çdo peshë në w, content_fn(.) thërret mean_sqr_b(.) në diferencën ndërmjet elementit ith të x dhe elementit ith + n në x (ku n = numri i peshave në w), shumëzon atë që rezulton me peshën e ith në w, dhe shton ajo vlerë është res.

Nëse mendoni për atë x që hyn në content_fn(.) si bashkim i tensorit tonë HiRes dhe tensorit tonë LoRes Upsampled, dhe nëse të dy këta tensorë janë rezultat i një modeli me shumë dalje (3 aktivizimet e ndryshme të conv_block), atëherë gjithçka që bën content_fn(.) është MSE e aktivizimeve korresponduese të bllokut të peshuar të dy tensorëve.

Nëse është ende pak e dendur, provoni këtë:

Unë do të ndaloj një moment këtu dhe shpresoj që të mos e bëj më keq.

E drejta. Pra, ky është funksioni i humbjes.

Modeli përfundimtar, këtë herë i quajtur m_sr për modelin me super-rezolucion, merr si hyrje një grup tensorë: origjinalet LoRes dhe HiRes, dhe nxjerr rezultatin e funksionit të humbjes content_fn të aplikuar në vgg1 + vgg2.

m_sr = Model([inp, vgg_inp], Lambda(content_fn)(vgg1+vgg2)

Thjesht vizualizimi i këtij është një makth i lehtë: të imagjinosh grafikët e shumëfishtë të llogaritjes që ndryshojnë vetëm në cilin bllok të VGG-së marrin si dalje, se si rezultatet e tyre bashkohen së bashku në funksionin e humbjes; dhe kjo as që po mendon se çfarë po ndodh brenda çdo rrjeti Upsampling, ose se si përhapja e pasme po përditëson në të vërtetë rrjetin. Është mirë që kjo mund të abstragohet në vetëm disa rreshta kodi. Mendova të hartoja një diagram masiv … pas një pike të caktuar është Illuminati takon Matricën dhe ju jeni në një garë për të kuptuar komplotin.

Përpilimi është i thjeshtë, edhe pse "Mean Absolute Error" përdoret në vend të "MSE": m_sr.compile('adam', 'mae'). Dallimi tjetër është trajnimi. Ngarkimi i ~40 mijë imazheve vetëm mezi futet në memorie në stacionin tim të punës, ndonjëherë. Po sikur të dëshironi më shumë? Këtu hyn biblioteka bcolz. Ju i mbani të dhënat në disk dhe i ngarkoni ato në grupe. Kodi për këtë shkon diçka si kjo:

bs është madhësia e grupit. Duhet të jetë një shumëfish i chunklen gjatësisë së pjesës së bartës bcolz, të përcaktuar në kohën e ngjeshjes. Unë pata një problem me vargjet bcolz që kishin një gjatësi prej 64 copë, ndërsa stacioni im i punës mund të trajtonte vetëm një madhësi grupi prej 6; 8 nëse planetët janë në linjë. E zgjidha këtë duke krijuar vargje të reja me parametrin chunklen të vendosur në mënyrë eksplicite. Kjo mund ose nuk mund të ketë qenë kosher.

niter është numri i përsëritjeve. targ është vektori i synuar ashtu si më parë, por këtë herë është një grup zerosh, një për çdo element në grup. bc është një përsëritës që kthen grupin tjetër të në këtë rast arr_hr dhe arr_lr në çdo telefonatë në next(bc). Më pëlqen shumë kjo sepse kisha një problem me kufirin e memories së sistemit për një nga detyrat "Deep Learning I" dhe kam hakuar së bashku një sërë funksionesh të ngjashme në frymë për të tërhequr të dhëna nga disku në grupe dhe për të trajtuar saktë grupin e mbetur të fundit. Megjithatë, "Kodi" është pak më i avancuar dhe do të jetë interesante ta veçosh dhe të shohësh se si funksionon. Ai përdor threading dhe deklaratat with, të cilat nuk janë gjëra që më është dashur t'i përdor ende.

Për përsëritjet niter, train(..) ngarkon grupin tjetër të imazheve HiRes dhe LoRes dhe më pas thërret m_sr.train_on_batch([lr[:bs], hr[:bs]], targ). Pse saktësisht është lr[:bs] dhe jo vetëm lr kur lr është gjatësia bs gjithsesi, nuk jam i sigurt - ndoshta nuk është? Kam pasur probleme me grupet e hyrjes dhe objektivit që nuk përputhen në gjatësi në grupin përfundimtar. Nëse keni 16,007 imazhe dhe një madhësi grupi prej 8, madhësia e grupit tuaj të synuar caktohet në madhësinë e grupit → ju merrni një gabim "nga një". E zgjidha këtë duke specifikuar objektivin të jetë targ[:len(hr)], kështu që vargjet përfundimtare të hyrjes dhe objektivit janë me të njëjtën gjatësi. Për sa i përket niter, vendosa që të jetë: niter = len(arr_hr)//6 + 1 për të llogaritur pjesën e mbetur, megjithëse në retrospektivë shtimi i 1 do të ishte më mirë vetëm nëse ekzistonte një mbetje.

Pas gjithë kësaj, fundi është i lehtë. Stërvituni për disa epoka, lehtësoni shkallën e të mësuarit dhe më pas stërvitni disa të tjera. Pastaj merrni modelin tuaj të trajnuar të punës:

top_model = Model(inp, outp)

dhe filloni të rimarrëni gjërat. Mora këtë (Shënim: të dhënat këtu u ngarkuan të gjitha në memorie menjëherë dhe modeli u trajnua nëpërmjet model.fit(..)):

Edhe pse siç la të kuptohet fletorja e kursit, kjo performancë e mirë mund të jetë pjesërisht e tepërt. Këtu janë rezultatet me një foto të Jupiterit:

Kjo nuk është shumë e habitshme. Së pari, unë definitivisht harrova pjesën e pjekjes së shkallës së mësimit në të paktën një zbatim - isha më i interesuar për ta vënë atë në punë: shumë gabime në shterjen e burimeve dhe zgjidhjen e problemeve të përsëritësit të bcolz - dhe grupi i të dhënave me të cilin po punoja kishte vetëm 19,439 imazhe. Ndoshta kjo duhet të jetë më se e mjaftueshme, por nëse In: len(arr_hr)//16; Out: 62277 është diçka për të shkuar, modelja në kurs po trajnohej në të paktën 996,432 imazhe, edhe pse kjo mund të jetë pas regjistrimit të leksionit. Sido që të jetë rasti, në mënyrë ideale do të stërviteshit në të gjithë ImageNet dhe jo në një grup të vogël nëse do të kishit ndonjë fjalë për këtë çështje gjithsesi. Dhe në mënyrë ideale do ta merrnit regjimin e trajnimit të duhur gjithashtu. Dy përpjekjet e mia të suksesshme morën një kohë të kombinuar llogaritëse prej rreth 12 orësh, meqë ra fjala. Pra, 20 mijë ishin thjesht mirë.

Një shënim për Jupiterin dhe përshtatjen e tepërt: duket se ka një problem me rritjen e kontrastit dhe larjen e ngjyrave në të gjitha fotot, por gjithashtu nuk ka një kategori planetësh në ImageNet - me aq sa kam mundur të gjej. Pra, mund të ketë diçka që po ndodh atje.

3. Transferimi i shpejtë i stilit. Ne mund të përdorim të njëjtën ide bazë nga super-rezolucion për të bërë transferimin e shpejtë të stilit duke përdorur humbje perceptuese (përmbajtje). Këtë herë ne bëjmë një foto, e vendosim atë përmes një CNN, më pas e vendosim atë përmes një funksioni humbjeje duke bërë humbje të stilit dhe përmbajtjes kundrejt një imazhi të vetëm të stilit fiks. Ka një "letër" së bashku me "material shtesë" për këtë temë.

Letra përdor mbushjen e reflektimit në vend të mbushjes zero në hyrje - domethënë: pasqyrimi i imazhit rreth kufijve në vend që të ketë vetëm pikselë të zinj. Jeremy Howard e zbaton këtë me një shtresë të personalizuar e cila në Keras bëhet si një klasë Python.

Seksioni i parë është konstruktori. Pas kësaj janë dy gjëra që duhen bërë sa herë që përcaktohet një shtresë e personalizuar Keras: ju duhet të përcaktoni get_output_shape_for(self, input) që merr formën e tensorit hyrës dhe kthen formën e tensorit të daljes - në këtë rast është e njëjta madhësi e grupit si s (s[0]), i njëjti numër kanalesh (s[3]) dhe dyfishi i sasisë së mbushjes për rreshtat dhe kolonat: (s[1] + 2 * self.padding[0], s[2] + 2 * self.padding[1],). Keras e di "në mënyrë magjike" se sa i madh është hyrja/dalja e shtresave ndërmjetëse kur i bashkon gjërat, sepse çdo shtresë përmban këtë lloj informacioni. Gjëja e dytë për të përcaktuar është call(self, x, mask=None). call(..) merr të dhënat e shtresës tuaj x dhe kthen çfarëdo që bën shtresa juaj. “Dokumentacioni i Keras thotë:

call(x): këtu jeton logjika e shtresës.

Ne duam që kjo shtresë të shtojë mbushje reflektimi. TensorFlow ka një funksion të integruar për atë të quajtur tf.pad(..).

Ajo shtresë tani mund të përdoret në një përkufizim rrjeti:

inp = Input((288, 288, 3))
ref_model = Model(inp, ReflectionPadding2D((40, 2))(inp)
ref_model.compile('adam', 'mse')
p = ref_model.predict(arr_hr[10:11]) # run model on 10th img

Autorët e letrës përdorin mbushje reflektimi piksel 40x40 në imazhe. Meqenëse ata përdorin konvolucione 'të vlefshme' në blloqet e tyre ResNet, imazhi pritet gradualisht nëpër shtresa, kështu që mbushja e reflektimit ekziston për të ruajtur imazhin.

Në fakt, arkitektura e autorëve është shumë e ngjashme me tonën. Modeli i përgjithshëm është: zvogëloni mostrën e hyrjes nëpërmjet konvolucioneve dhe rrisni numrin e kanaleve dhe fushës pritëse duke krijuar kështu paraqitje më komplekse të të dhënave (rreshtat 3-5), bëni llogaritje në ato paraqitje (shtresat e mbetura), pastaj ekzaminoni atë (3 të fundit linjat). Hapi 1/2 është një dekonvolucion: është i njëjtë me një përthyerje me hapa të pjesshëm.

Pa hyrë në shumë detaje, duke e zbatuar këtë (kryesisht duke ndjekur nga fletorja e përditësuar e kursit) mora këtë imazh të stilit:

Dhe e aplikoi atë në këtë foto të një kulle mesjetare Waynakh:

Të dy imazhet u ndryshuan në 500x500. Duke parë kodin, po shoh disa gabime të trashë: Imazhi i stilit u pre në 288x288 për t'iu përshtatur tensorit të stilit… kështu që hodhi 33% të imazhit të stilit. Pas ndryshimit të madhësisë së imazhit të përmbajtjes së kullës në333x333 … nuk do të përshtatej, kështu që thjesht e preva për të përshtatur tensorin e hyrjes. Megjithatë, pas trajnimit të modelit të transferimit të stilit të shpejtë për rreth 6 orë në 4 epoka me pjekjen e shkallës së të mësuarit (2 ep @ default → 2 ep @ 0.0001), modeli bëri transferimin e stilit të tij në ndoshta një sekondë ose më pak dhe prodhoi këtë:

Duket si një kartë për një lojë me role. Një kështjellë e përhumbur apo diçka tjetër. Natyrisht do të ishte shumë më mirë nëse do të përdorja rezolucionin e plotë të përmbajtjes origjinale dhe imazheve të stilit, por nuk e kam shqyrtuar ende. Meqenëse ky model kryen trajnimin e tij fillimisht dhe jo në kohën e ekzekutimit si modeli i mëparshëm i transferimit të stilit… duket se do të jetë e ndërlikuar ta bësh atë të funksionojë për imazhe të mëdha me burime të kufizuara llogaritëse. Mund të provoni rastin e thjeshtë ekstrem të ri-kompresimit të vargjeve bcolz për të pasur një gjatësi prej 1, në mënyrë që të mund të përdorni një madhësi grupi prej 1, të filloni me një shkallë shumë më të ulët të të mësuarit, thjesht pranoni kohën më të gjatë të trajnimit dhe shkoni prej andej… por kjo duket shumë e thjeshtë si një zgjidhje. Duhet të ketë një mënyrë për të shkallëzuar në çdo madhësi imazhi dhe të paguajë vetëm një kosto në kohën e llogaritjes, duke pasur parasysh burimet fikse. Hmm..

Mund të provoni ta thyeni imazhin në një mozaik rrjetë, të vidhni në secilën pjesë dhe t'i bashkoni përsëri së bashku. Por tani ju paguani një kosto cilësore: nëse është një foto HiRes e një fytyre, atëherë ka informacione të rëndësishme për të cilat rrjeti juaj nuk do të jetë i fshehtë, sepse në fakt do të jetë shumë i zmadhuar për të parë pyllin për pemët.

Prisni, a ka ndonjë mënyrë për të përdorur tensorin 288x288x3 si një fushë shikimi dhe për ta skanuar atë mbi një imazh më të madh, duke ngarkuar atë që shihet në memorie dhe duke e mbajtur pjesën tjetër në disk ndoshta me bcolz? Hmm..

4. DeVISE: Modeli i ngulitjes së thellë vizuale-semantike. Pjesa e fundit e leksionit 9, dhe kjo odise e një postimi në blog është DeVISE. I përshkruar në një punim nga Andrea Frome (et al?), DeVISE kërkon të bëjë një hap më afër asaj që bëjmë për njohjen e imazhit duke zëvendësuar kodimet e njëpasnjëshme me ngulitje fjalësh.

Si një përmbledhje (shumë) e përafërt: në modelin tuaj tradicional të koduar një-hot, ka pak ngjashmëri midis një peshkaqeni dhe një peshku, ose ndonjë nocion i konceptit të "peshkaqenit" i cili është një superbashkë e të gjitha llojeve të peshkaqenëve. Sigurisht që nuk i perceptojmë gjërat kështu në jetën reale. Në një fushë tjetër, përpunimi i gjuhës natyrore, ekziston një metodë për kapjen e ndërlidhjes së kategorive që quhet ngulitje fjalësh. Në thelb është një matricë ku çdo rresht (kolona??) përbëhet nga një vektor që harton lidhjen e një fjale me të gjitha fjalët e tjera. Ne e përdorëm këtë në Deep Learning I për të krijuar një lloj bot Nietzsche dhe gjithashtu për rekomandimet e filmave (të integruara mund të vendosin filmat në kategori, jo vetëm fjalë në fjalë).

Pra, për imazhet, në vend që të keni një vektor prej 1000 elementësh për një argjilë, me një 1 në indeksin për kategorinë e argjilës dhe zero kudo tjetër, dhe zëvendësojeni atë me një vektor të dendur fjalësh për një argjilë (gjatësia e vektorit varet nga ngulitja ne' ripërdorim). Ne do të përdornim një shtresë Softmax për të bërë kodimin e vetëm (përgjigjja është 1 të gjitha të tjerat janë 0), por tani do të përdorim shtresën lineare.

Gjëja e ndërlikuar për këtë është kalimi përmes ImageNet. 1.5 milion imazhe janë shumë. Dhe këtu hyjnë bcolz dhe përsëritësi i grupit (i përdorur më herët këtu).

Një këshillë këtu nga Jeremy Howard: ai përcakton dy shtigje të dhënash — një për të dhënat që jetojnë në SSD/NVMe të shpejta dhe një tjetër për HDD të mëdhenj. Ai përdori një SSD RAID 1 për të aksesuar shpejt imazhet e ndryshuara në madhësi dhe grupet e veçorive, si dhe shtegun e HDD-së për të dhëna me shumicë. Ide e mirë.

Fjala embedding e përdorur është word2vec.

Një tjetër gjë interesante e përmendur në leksion është sintaksa me një yll (zip-star?) të Python. Nëse keni një strukturë të dhënash që lidh fjalët me vektorë dhe dëshironi të merrni një listë të fjalëve dhe një listë të vektorëve, nëse mund ta merrni atë strukturë të dhënash si një përsëritës, atëherë:

words, vectors = zip(*wordvec_iterator)

do t'i vendosë të gjitha fjalët në words dhe të gjithë vektorët përkatës në vectors.

Pra, në shembullin e leksionit, nëse wordvec_iterator përmban "fox:", array01, "wolf", array02, ..., atëherë kur vendosni zip-star wordvec_iterator është njësoj si t'i vendosni ato përmbajtje në një zip(.) si kjo:

zip("fox", array01, "wolf", array02)

Pra, atëherë testoni këtë:

>>> words = ["fox:","wolf:"]; vecs = [[1,2,3],[4,5,6]]; 
>>> zipped = zip(words, vecs)
>>> list(zipped)
Out: [('fox:', [1,2,3]), ('wolf', [4,5,6])]
>>> w, v = zip(*zip(words, vecs)) # or: zip(*zipped) ## for fresh `zipped`
>>> words == list(w) and vecs == list(v)
Out: True
>>> w, v
Out: (('fox:', 'wolf:'), ([1,2,3], [4,5,6]))

Ose të jesh pedant:

>>> word, vecs
Out: (['fox:', 'wolf:'], [[1, 2, 3], [4, 5, 6]])
>>> list(w), list(v)
Out: (['fox:', 'wolf:'], [[1, 2, 3], [4, 5, 6]])

Kështu që sot mësova diçka të re. Mësimi bazë: nëse dëshironi të shndërroni një listë me tuple në një tufë listash, përdorni zip-star (Python).

Jeremy Howard bën më shumë konfigurimin e fletores për aplikimin e futjeve të fjalëve në fotografitë ImageNet. Nuk do të hyj në detaje këtu pasi ky postim ka pësuar formën e tij të misionit të zvarritjes - dhe gjithashtu nuk e kam krijuar ende hapësirën e lirë të TB-së për një ImageNet të zbërthyer plotësisht. Howard bën disa përpunime në listat e fjalëve, duke i rirenditur ato në mënyrë që fjalët më të zakonshme të jenë në pjesën e përparme - dhe gjithashtu i vendos të gjitha fjalët me shkronja të vogla pasi shkronja e madhe nuk ka rëndësi këtu. Ai bën disa kontrolle të arsyeshme duke përdorur np.corrcoef(..) për të parë koeficientët e korrelacionit midis versioneve të shkronjave të mëdha dhe të vogla të emrit të tij kundrejt emrit të tij dhe objekteve që nuk kanë lidhje.

Ai më pas krijon dy harta (fjalorë Python) të vektorëve të fjalëve për 1000 kategoritë ImageNet dhe 82,000 emrat në WordNet - me qëllimin për të krijuar një sistem që mund të shkojë përtej 1,000 kategorive të Imagenet.

Më në fund ai krijoi një listë të kategorive ImageNet që nuk gjendeshin në WordNet, dhe thjesht i zhvendosi ato dosje imazhesh nga drejtoria.

Një këshillë tjetër e dobishme: Howard kursen prodhimin e gjithçkaje që kërkon një kohë jo të parëndësishme. Marrja e emrave të skedarëve të një milion imazhesh kërkon pak kohë, veçanërisht nëse ato janë në disqe rrotulluese.

Ai gjithashtu përfiton nga një trio teknikash që unë po shoh në klasën "Algjebra lineare llogaritëse/numerike" gjithashtu: Lokaliteti i kujtesës, SIMD/Vektorizimi dhe Përpunimi paralel. [Segment leksioni]. Më dukej shumë interesante pjesa në SIMD.

Pra, kjo përfundon atë që ka qenë deri tani mësimi më i gjatë për mua në kurs. Po ndodh shumë, por më pëlqen ideja për të parë kodin për sa i përket arkitekturës së tij në shkallë të gjerë. Është jashtëzakonisht e lehtë të humbësh në një pyll të errët dhe të harrosh se nga vjen një pjesë e modelit tënd. Është gjithashtu e lehtë të kesh frikë të bësh ndonjë ndryshim nga frika se mos mund t'i kuptosh më të gjitha. Por nëse keni temën e ekzekutimit të "Unë po vendos imazhin hyrës përmes një CNN dhe se prodhimi i CNN po krahasohet me atë që unë dua të prodhojë nëpërmjet një funksioni humbjeje", bëhet shumë më e lehtë të mbani në mend atë që ju' në fakt po bën. Dhe kjo rreth e mbyll atë!

Disa shënime për shënimin: Unë shpesh do t'i shkruaj me shkronjë të madhe disa terma si Convolutional ose Generative etj. Kam gjetur se ndihmon kur lexoni tema të reja teknike të theksohen disa nga termat kryesorë aq sa të veprojnë si shënues shteg, për të shmangur vetëm duke lexuar nëpër një mur teksti si prozë dhe duke u humbur.

Gjithashtu, sa herë që ka një të humbur l diku më vete në kod, unë do ta zëvendësoj atë me λ. Përveçse λ është e bukur, në shumë fonte 1 I l duken identike ose mjaft afër për të qenë konfuze; kështu që ka një përdorim shtesë të ruajtjes së shëndetit.