Neurokuvatreenit, harj 2

Ensimmäisessä osassa tutustuimme kuvan luomiseen tavoitefunktion avulla, siitä syntyi vihreä neliö. Alustimme uuden kuvan satunnaisesti ja tavoitekuvan taas alustimme pelkästään vihreillä pikseleillä. Annoimme sitten koulutusprosessin mitata eroa tavoitteeseen nähden ja sen avulla ohjasimme pikseleiden arvot kohti tavoitetta. Tässä ei toki tapahtunut vielä mitään uutta tai hyödyllistä, saimme kopion vihreästä neliöstä. Tutustuimme kuitenkin toimintamalliin, jossa kuvan syntyä ohjaa tavoitefunktio. Tämä malli toistuu kaikissa myöhemmissäkin harjoituksissa ja teemme sen avulla paljon mutkikkaampia asioita.

Seuraavaksi katsotaan kuinka tavoitteeksi voi ottaa jonkin tietyn kuvan. Kokeillaan samalla kuinka käy jos meillä onkin kaksi eri tavoitetta.

Kuvan lukemiseen käytämme PIL-kirjastoa, ja määrittelemme kuvan esikäsittelyketjun, jossa kuva skaalataan haluttuun kokoon, muunnetaan tensoriksi ja normalisoidaan välille -1..1. Käytimme näistä normalisointia jo ensimmäisessä osassa, mutta silloin loimme kuvan valmiiksi oikeankokoiseksi tensoriksi pytorchia varten. Nyt, kun kuva luetaan tiedostosta, se pitää skaalata tarpeen mukaan ja muuttaa tensoriksi ennen normalisointia, ja sitä varten määrittelemme esikäsittelyketjun.

import torch
from torchvision.utils import save_image
from torchvision.transforms import Compose, Resize, ToTensor, Normalize
from PIL import Image

# define preprocessing pipeline

preprocess = Compose([
    Resize((256, 256)),    # resize image
    ToTensor(),            # convert to pytorch tensor
    Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))            # normalize to range -1...1
])

Nyt voimme sitten lukea kuvan tiedostosta ja esikäsitellä sen tensoriksi. Sen jälkeen meillä on valmis imgT, joka vastaa ykkösharjoituksessa vihreäksi alustamaamme tensoria.

filename = "test.png"
imgT = preprocess(Image.open(filename).convert("RGB"))

Määrittelemme vielä kokeeksi toisen tavoitekuvan, samanlaisen vihreän neliön kuin ykkösharjoituksessa. Luomme myös satunnaisesti alustetun tensorin josta sitten syntyy se tekeillä oleva kuva. Tässä ei käytetä esikäsittelyketjua, koska imgT2 ja imgG ovat valmiiksi tensoreita, mutta imgG:lle tehdään normalisointi koska pikselit ovat alustuksen jälkeen välillä 0..1. Huomaa, että voisimme kyllä alustaa imgG:n valmiiksi välille -1..1, mutta koska jatkossa joudutaan joka tapauksessa muuntamaan eri esitystapojen välillä, se kannattaa oppia jo nyt, ennenkuin käydään muuten vaativampien sovellusten kimppuun.

# just for fun, lets define an alternative target too

imgT2 =  torch.zeros(3,256,256)
imgT2[1,:,:] = 1
imgT2 = norm(imgT2)

# make a 256x256 rgb image with random pixels
# pixel values here float in range is 0..1 

imgG = torch.Tensor(3,256,256).normal_(mean=0.5, std=0.5).clamp_(0,1) 
norm = Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
imgG = norm(imgG)

Nyt meillä on tyhjä, alustettu tensori josta on tarkoitus kasvattaa kuva. Määritellään siis se optimoinnin kohteeksi.

# We are going to optimize imgG, so we need gradients for it

lr = 0.05 # you might want to change this and see what happens

imgG.requires_grad = True

optimizer = torch.optim.Adam([imgG], lr)

Nyt kaikki on valmista itse optimoinnille. Luodaan silmukka, jossa mitataan tavoitefunktiolla kuinka kaukana ollaan tavoitteesta, ja tehdään vastavirtavalutus, jonka pohjalta optimoija säätää imgG:n pikseleitä. Talletamme kuvaa levylle jotta voimme seurata sen kehittymistä.

niter = 150

for i in range(0, niter):
    optimizer.zero_grad()

    # calculate loss
    loss1 = torch.abs(imgT - imgG).mean()

    # an alternative loss: green target
    loss2 = torch.abs(imgT2 - imgG).mean()
    
    b = 0  # by default, do not use green target, but you can test varying it later
    
    loss = loss1 + b * loss2

    # run backwards to find gradient (how to change imgG to make loss smaller) 
    loss.backward()

    # run optimizer to change imgT
    optimizer.step()

    # print loss to show how we are doing
    print(i, loss.item(), loss1.item(), loss2.item())

    # save image
    if (i < 10) or (i % 10 == 0):
        save_image(imgG, "harj2-"+str(i)+".jpg")

Katsotaan tuosta tarkemmin paria yksityiskohtaa. Kuvien välistä eroa laskemme vertaamalla pikseleittäin lasketun eron keskiarvoa. Virhe lasketaan erikseen kumpaankin tavoitekuvaan verrattuna. Muuttamalla muuttujan b arvoa voimme säätää, kuinka paljon kakkostavoite vaikuttaa.

   # calculate loss
    loss1 = torch.abs(imgT - imgG).mean()

    # an alternative loss: green target
    loss2 = torch.abs(imgT2 - imgG).mean()
    
    b = 0  # by default, do not use green target, but you can test varying it later
    
    loss = loss1 + b * loss2

Huomaa, että imgG ja imgT ovat kokonaisia rgb-kuvia, siis 3 x H x W -tensoreita. Kun laskemme niiden erotuksen, tuloksena on myös 3 x H x W -tensori, josta löytyy erotus jokaisen pikselin kohdalla. Otamme vielä itseisarvon, koska on sama onko erotus plus- vai miinusmerkkinen, ja sitten laskemme kaikkien pikselien virheen keskiarvon. Selvästikin, jos saamme muuttumaan sen kohti nollaa, kuva lähestyy haluttua.

Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *