Použití PyTorch pro Kaggleovu slavnou výzvu Psi vs. Kočky část 1 (předzpracování a školení)

Pro začátečníky strojového učení, kteří si chtějí vyzkoušet problémy s klasifikací obrazu, může být dobrým cvičením budování binárního klasifikačního modelu. Výzvou pro psy vs. kočky je právě to! Opravdu snadný koncept, stačí naučit počítač rozeznat psy a kočky odděleně. Dá se to argumentovat jako „Ahoj svět!“ Strojového učení spolu s MNIST. Pro úplné začátečníky však může být obtížné vybrat si dobrou architekturu, vyrobit výstup, který je ve správném formátu pro podání atd. Z tohoto důvodu píšu tento příspěvek popisující, co jsem udělal pro tuto soutěž. Také jsem se pokusil vytvořit jádro kaggle, ale uvědomil jsem si, že jádro kaggle je pouze pro čtení, takže nemůžu přesouvat svoji datovou strukturu a vytvořit svůj soubor pro odeslání, takže zkopíruji svůj kód na příspěvek. FYI, udělal jsem tuto soutěž na svém Macbooku bez jediného GPU.

Co můžete očekávat v tomto příspěvku je 1) organizování vlakových / validačních datových sad, 2) transfer učení, 3) ukládání / načítání nejlepšího modelu, 4) vytváření závěrů z testovacího datového souboru, 5) vytvoření souboru pro podání ve správném formátu a odeslání do kaggle a další. Bez dalšího nálezu se k tomu dostaneme.

1. Organizace dat

Vaše data jsou dodávána s údaji o vlacích a testovacích datech. Údaje o vlacích obsahují kočky i psy, ale mají název třídy (cat. .jpg pro obrázky koček, psy. .jpg pro obrázky psů.). Protože PyTorch podporuje načítání obrazových dat z podsložek datového adresáře, budeme muset umístit všechny obrázky koček do složky koček a všechny obrázky psů do složky psů. Budeme také muset od sebe oddělit ověřovací sadu, abychom zjistili, zda se náš model správně učí. Takže podsložky přepravek kočky a psi uvnitř vlakové složky, vytvořte složku val pod vstupní složkou a vytvořte stejné podsložky uvnitř složky val. Testovací data jsou neznačená a je v pořádku nechat tak, jak jsou.

import os
train_dir = "./data/train"
train_dogs_dir = f '{train_dir} / dogs'
train_cats_dir = f '{train_dir} / cats'
val_dir = "./data/val"
val_dogs_dir = f '{val_dir} / dogs'
val_cats_dir = f '{val_dir} / cats'
print ("Tisk dat dir")
print (os.listdir ("data")) # Ukazuje vlak, složky s adresami jsou pod daty
tisk („Tisk vlakového nádraží“)
! ls {train_dir} | head -n 5 # Zobrazuje obrazové soubory ve složce vlaku
tisk („Tisk psa psíkem“)
! ls {train_dogs_dir} | head -n 5 # Zkontrolujte, zda existuje (prázdná) složka
tisk („Tisk kočičí kočky“)
! ls {train_cats_dir} | head -n 5 # Zkontrolujte, zda existuje (prázdná) složka
print ("Printing val dir")
! ls {val_dir} | head -n 5 # Ukazuje, že existují podsložky a kočky
tisk ("Printing val dog dir")
! ls {val_dogs_dir} | head -n 5 # Zkontrolujte, zda existuje (prázdná) složka
tisk ("Tisk val cat dir")
! ls {val_cats_dir} | head -n 5 # Zkontrolujte, zda existuje (prázdná) složka

Spusťte výše uvedený kód v notebooku Jupyter a zkontrolujte, zda jsme připravili správnou strukturu složek. Dalším krokem je přesunutí souborů do správné složky.

importovat
importovat znovu
files = os.listdir (train_dir)
# Přesuňte všechny obrázky vlakových koček do složky koček, obrázky psů do složky psů
pro soubory f:
    catSearchObj = re.search ("kočka", f)
    dogSearchObj = re.search ("pes", f)
    pokud catSearchObj:
        shutil.move (f '{train_dir} / {f}', train_cats_dir)
    elif dogSearchObj:
        shutil.move (f '{train_dir} / {f}', train_dogs_dir)

Zkontrolujte, zda jsme soubory přesunuli správně.

print („Printing train dir“) # ukazuje pouze kočky, podsložky psů
! ls {train_dir} | hlava -n 5
print ("Printing train dog dir") # nyní jsou obrázky psů ve složce psů
! ls {train_dogs_dir} | hlava -n 5
print ("Printing train cat dir") # nyní jsou ve složce koček obrázky koček
! ls {train_cats_dir} | hlava -n 5

Nyní oddělte několik obrázků psů pro ověřovací sadu. Mnoho případů budete chtít oddělit 20% vašich celých dat jako ověřovací sadu. V tomto případě máte v tréninkové sadě 25 000 obrázků, což je celkem mnoho, protože kočky a psi jsou jako data ImageNet. Myslel jsem si, že 20% z nich je příliš mnoho a stačí vyfotit 1 000 obrázků pro kočky a psy.

files = os.listdir (train_dogs_dir)
pro soubory f:
    validationDogsSearchObj = re.search ("5 \ d \ d \ d", f)
    pokud validationDogsSearchObj:
        shutil.move (f '{train_dogs_dir} / {f}', val_dogs_dir)
tisk ("Printing val dog dir")
! ls {val_dogs_dir} | hlava -n 5

Nad kódem se přesouvají obrázky psů, jejichž id se pohybuje od 5 000–5 999 do složky pro ověření. A to samé udělejte pro obrázek koček.

files = os.listdir (train_cats_dir)
pro soubory f:
    validationCatsSearchObj = re.search ("5 \ d \ d \ d", f)
    pokud validationCatsSearchObj:
        shutil.move (f '{train_cats_dir} / {f}', val_cats_dir)
tisk ("Tisk val cat dir")
! ls {val_cats_dir} | hlava -n 5

2. Tréninkový model

Nyní, když jsou data ve správné struktuře, je čas trénovat náš model. Nejprve importuji, co pro tento notebook potřebuji. Není to však úplný seznam dovozů, zbytek importujeme podle potřeby.

importní svítilna
importovat pochodeň.nn jako nn
import torch.optim jako optim
z torch.optim import lr_scheduler
importovat numpy jako np
import torchvision
z importních datových sad torchvisionů, modelů, transformací
import matplotlib.pyplot jako plt
importovat čas
import os
importovat kopii
importovat matematiku
tisk (pochodeň .__ verze__)
plt.ion () # interaktivní režim

Definujme rozšíření dat školení a transformaci validačních dat.

# Zvětšení a normalizace dat pro školení
# Pouze normalizace pro ověření
data_transforms = {
    'vlak': transforms.Compose ([
        transforms.RandomRotation (5),
        transforms.RandomHorizontalFlip (),
        transformaceRandomResizedCrop (224, scale = (0,96, 1,0), poměr = (0,95, 1,05)),
        transforms.ToTensor (),
        transformaceNormalizace ([0,485, 0,456, 0,406], [0,229, 0,224, 0,225])
    ]),
    'val': transforms.Compose ([
        transforms.Resize ([224,224]),
        transforms.ToTensor (),
        transformaceNormalizace ([0,485, 0,456, 0,406], [0,229, 0,224, 0,225])
    ]),
}

Jako doplněk dat používám trochu rotace, náhodné převrácení a změnu velikosti + oříznutí. Měřítko změny velikosti je 0,96–1,0. Snažím se vyhnout tomu, aby měřítko bylo menší než 0,96, protože stále můžete získat požadovanou odchylku v údajích a existuje mnohem menší riziko odříznutí některé důležité části dat (např. Hlava kočky nebo psa, pokud nemáme část hlavy) , bude pro stroj mnohem těžší zjistit, jak by měla vypadat každá třída). Také mírná změna poměru by měla být v pořádku pro náš účel (tenčí nebo tlustší, kočka je kočka v pořádku?). O Normalizaci jsem použil nějakou pevně zakódovanou hodnotu pro střední a standardní odchylku. Je známo, že tyto hodnoty fungují dobře a často se používají. Podívejte se na doporučení tohoto inženýra Facebook AI pro použití těchto hodnot a také na oficiální příklad PyTorch používající stejnou hodnotu.

data_dir = 'data'
CHECK_POINT_PATH = 'checkpoint.tar'
SUBMISSION_FILE = 'podání.csv'
image_datasets = {x: datasets.ImageFolder (os.path.join (data_dir, x),
                                          data_transforms [x])
                  pro x in ['vlak', 'val']}
dataloaders = {x: torch.utils.data.DataLoader (image_datasets [x], batch_size = 4,
                                              shuffle = True, num_workers = 4)
              pro x in ['vlak', 'val']}
dataset_sizes = {x: len (image_datasets [x]) pro x in ['train', 'val']}
class_names = image_datasets ['vlak']
device = torch.device ("cuda: 0" if torch.cuda.is_available () else "cpu")
print (class_names) # => ['cats', 'dogs']
print (f'Train image size: {dataset_sizes ["train"]} ')
print (f'Validation image size: {dataset_sizes ["val"]} ')

A pak definujte některé potřebné konstanty a definujte datové sady (vlak a val). Měli byste vidět 23000 pro velikost obrázku vlaku a 2000 pro velikost obrázku ověření, pokud jste vše správně dodržovali. FYI, protože pracuji bez GPU a frustrovaně trvá dlouhou dobu, než zpracovám tolik dat, že jsem šel vymazat celou hromadu dat a pokračoval s 950 celkovými obrázky vlaků a 71 ověřovacími obrázky. Pro mě to mělo za následek uspokojivou přesnost a cílem pro mě nebylo udělat nejpřesnější model, ale praktikovat používání webu PyTorch a Kaggle, proto jsem se rozhodl nepoužívat tolik dat, ale samozřejmě nechcete mazat data, pokud trénujete model pro výrobní účely. Dalším důvodem, proč jsem mohl trénovat svůj model s tak malými daty, bylo použití předpřipraveného modelu. To znamená, že jsem upravil a vycvičil pouze poslední vrstvu a všechny vrstvy jsem použil tak, jak byly, protože model byl již dobře vyškolen s daty ImageNet.

Podívejme se, jak vypadá malá šarže (4 obrázky) z tréninkové sady pomocí dalšího fragmentu kódu.

def imshow (inp, title = None):
    "" "Imshow pro Tensora." ""
    inp = inp.numpy (). transpose ((1, 2, 0))
    průměr = np. pole ([0,485, 0,456, 0,406])
    std = np.array ([0,229, 0,224, 0,225])
    inp = std * inp + průměr
    inp = np.clip (inp, 0, 1)
    plt.imshow (inp)
    pokud název není žádný:
        plt.title (title)
    plt.pause (0,001) # pauza trochu, takže grafy jsou aktualizovány
# Získejte dávku tréninkových dat
vstupy, třídy = další (iter (dataloaders ['vlak'])))
# Vytvořte mřížku z dávky
sample_train_images = torchvision.utils.make_grid (vstupy)
imshow (sample_train_images, title = classes)

Uvidíte náhodně vybrané 4 obrázky a titul řekne 0 pro kočku a 1 pro psa. Dále definujme funkci, která trénuje náš model a vrací nějakou metriku.

def train_model (model, kritérium, optimalizátor, plánovač, num_epochs = 2, kontrolní bod = žádný):
    since = time.time ()
pokud kontrolní bod není:
        best_model_wts = copy.deepcopy (model.state_dict ())
        best_loss = math.inf
        best_acc = 0.
    jiný:
        print (f'Val ztráta: {checkpoint ["best_val_loss"]}}, Val přesnost: {checkpoint ["best_val_accuracy"]} '))
        model.load_state_dict (checkpoint ['model_state_dict'])
        best_model_wts = copy.deepcopy (model.state_dict ())
        optimizer.load_state_dict (checkpoint ['optimizer_state_dict'])
        scheduler.load_state_dict (checkpoint ['scheduler_state_dict'])
        best_loss = checkpoint ['best_val_loss']
        best_acc = checkpoint ['best_val_accuracy']
pro epochu v rozsahu (num_epochs):
        tisk ('Epoch {} / {}'. formát (epocha, num_epochs - 1))
        tisk ('-' * 10)
# Každá epocha má fázi školení a ověření
        pro fázi v ['vlak', 'val']:
            pokud fáze == 'vlak':
                scheduler.step ()
                model.train () # Nastaví model do tréninkového režimu
            jiný:
                model.eval () # Nastaví model pro vyhodnocení režimu
running_loss = 0.0
            running_corrects = 0
# Iterovat přes data.
            pro i, (vstupy, štítky) v výčtu (dataloadery [fáze]):
                vstupy = vstupy.to (zařízení)
                labels = labels.to (zařízení)
# nula gradientů parametrů
                optimizer.zero_grad ()
                
                pokud i% 200 == 199:
                    print ('[% d,% d] ztráta:% .3f'%
                          (epocha + 1, i, running_loss / (i * inputs.size (0))))
# vpřed
                # historie tratí, pouze pokud je ve vlaku
                s torch.set_grad_enabled (phase == 'vlak'):
                    výstupy = model (vstupy)
                    _, preds = torch.max (výstupy, 1)
                    ztráta = kritérium (výstupy, štítky)
# zpět + optimalizace pouze ve fázi školení
                    pokud fáze == 'vlak':
                        loss.backward ()
                        optimizer.step ()
# statistika
                running_loss + = loss.item () * inputs.size (0)
                running_corrects + = torch.sum (preds == labels.data)
epoch_loss = running_loss / dataset_sizes [fáze]
            epoch_acc = running_corrects.double () / dataset_sizes [fáze]
print ('{} Ztráta: {: .4f} Acc: {: .4f}'. (
                fáze, epoch_loss, epoch_acc))
# hluboká kopie modelu
            if phase == 'val' a epoch_loss 
vytisknout()
time_elapsed = time.time () - od té doby
    print ('Trénink dokončen v {: .0f} m {: .0f} s'.format (
        time_elapsed // 60, time_elapsed% 60))
    print ('Best val Acc: {: .4f} Best val loss: {: .4f}'. format (best_acc, best_loss))
# načíst nejlepší hmotnosti modelu
    model.load_state_dict (best_model_wts)
    návratový model, best_loss, best_acc

Funkce nejprve zkontroluje, zda je předán uložený kontrolní bod. Pokud ano, načte uložený parametr a začne trénink od místa, kde byl ukončen. Pokud ne, pak začne trénovat model, který byl předán (stále budeme používat předtrénovaný model od začátku). Funkce aktualizuje parametry pouze ve fázi vlaku a vytiskne některé metriky každou epochu nebo kdykoli má novou nejlepší ztrátu.

Nyní definujme náš model stažením předpřipraveného modelu. Spuštění dalšího kódu trvá nějakou dobu, pokud jste jej nikdy předtím nespustili.

model_conv = torchvision.models.resnet50 (pretrained = True)

resnet50 je konvoluční architektura neuronové sítě, která je opravdu výkonná pro řešení problémů s počítačovým viděním. Mezi méně výkonné, ale méně náročné modely, které by vás mohly zajímat, patří resnet18 a resnet34. Pokud jako argument zadáte předpřipravené = True, stáhnete model s parametry vyškolenými v sadě dat ImageNet. Protože potřebujeme změnit model pro naše potřeby (klasifikace binární třídy), změníme poslední plně spojenou vrstvu a definujeme funkci ztráty, která je užitečná pro klasifikační problém (ztráta cross entropie, která kombinuje log softmax a funkci ztráty pravděpodobnosti negativní log) . Optimzier je stochastický optimalizátor sestupového gradientu a plánovač je exponenciální, protože sníží rychlost učení faktorem 10 každých 7 epoch (ve skutečnosti jsem trénoval pouze 6 epoch).

pro param in model_conv.parameters ():
    param.requires_grad = False
# Parametry nově vytvořených modulů mají ve výchozím nastavení default_grad = True
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Lineear (num_ftrs, 2)
model_conv = model_conv.to (zařízení)
kritérium = nn.CrossEntropyLoss ()
# Všimněte si, že jsou optimalizovány pouze parametry konečné vrstvy
optimizer_conv = optim.SGD (model_conv.fc.parameters (), lr = 0,001, hybnost = 0,9)
# Rozpad LR faktorem 0,1 každých 7 epoch
exp_lr_scheduler = lr_scheduler.StepLR (optimizer_conv, step_size = 7, gama = 0,1)

Nakonec se můžeme dostat do skutečného tréninku.

Snaž se:
    checkpoint = torch.load (CHECK_POINT_PATH)
    print ("checkpoint načten")
až na:
    checkpoint = Žádný
    print ("checkpoint nenalezen")
model_conv, best_val_loss, best_val_acc = train_model (model_conv,
                                                      kritérium,
                                                      optimizer_conv,
                                                      exp_lr_scheduler,
                                                      num_epochs = 3,
                                                      checkpoint = checkpoint)
torch.save ({'model_state_dict': model_conv.state_dict (),
            'optimizer_state_dict': optimizer_conv.state_dict (),
            'best_val_loss': best_val_loss,
            'best_val_accuracy': best_val_acc,
            'scheduler_state_dict': exp_lr_scheduler.state_dict (),
            }, CHECK_POINT_PATH)

Kód nejprve zkontroluje, zda je nějaký kontrolní bod uložen z předchozího školení. Pokud ano, předejte funkci kontrolního bodu vlakovému modelu. Zde jsem určil 3 epochy a funkce vrátí model, ztrátu, přesnost, kdy byla ztráta nejnižší ve všech epochách. Uložíme, co jsme dostali z funkce, na kontrolní bod. Můžete upravit číslo epochy nebo znovu spustit tento úryvek, kolik chcete. Pokud vidíte, že se model již nezlepšuje, můžete zastavit. Protože jsem měl pouze 71 obrázků pro ověření, dosáhl jsem přesnosti 1,0 se ztrátou 0,036 za pouhé 2 běhy, takže jsem se rozhodl ukončit trénink.

Všimněte si, že jsme potřebovali pouze trénovat poslední vrstvu, kterou jsme změnili z původního resnet50. Je také možné trénovat všechny parametry ve všech vrstvách, ale když jsem to zkusil, viděl jsem jen ztrátu a přesnost zhoršování. Pokud chcete zkusit aktualizovat všechny parametry, můžete tak učinit následovně.

pro param in model_conv.parameters ():
    param.requires_grad = Pravda
model_conv = model_conv.to (zařízení)
# Všimněte si, že jsou optimalizovány všechny parametry
optimizer_ft = optim.SGD (model_conv.parameters (), lr = 0,001, hybnost = 0,9)

A pak spusťte stejný kód tréninkové smyčky jako předtím (blok kódu začíná zkouškou).

Možná už je tento příběh příliš zdlouhavý. Budu tedy psát část, kterou ve skutečnosti děláte, dovnitř modelu, pomocí sady testovacích dat a odesláním vaší odpovědi na kaggle v samostatném příběhu. Nalaďte se na další část a další. Dejte nám vědět, pokud se vám podařilo následovat můj kód a také, zda jste pomocí této metody dosáhli uspokojivého výsledku.

Druhá (poslední) část je venku. Pokud chcete pokračovat ve čtení, klikněte sem. Celý kód si můžete také prohlédnout v mém repozitáři Github. Kód pro předzpracování dat naleznete v datapreprocessor.ipynb a školení, závěry a zadávání najdete v catsanddogs.ipynb.