Verwenden von PyTorch für Kaggles berühmte Herausforderung "Hunde vs. Katzen" Teil 1 (Vorverarbeitung und Training)

Für Anfänger des maschinellen Lernens, die Bildklassifizierungsprobleme ausprobieren möchten, ist es möglicherweise eine gute Übung, ein binäres Klassifizierungsmodell zu erstellen. Eine Herausforderung zwischen Hunden und Katzen ist genau das! Wirklich einfaches Konzept, Sie müssen nur einen Computer unterrichten, um Hunde und Katzen zu unterscheiden. Man kann dies als „Hallo Welt!“ Des maschinellen Lernens zusammen mit MNIST bezeichnen. Für Anfänger ist es jedoch möglicherweise schwierig, eine gute Architektur auszuwählen, die Ausgabe im richtigen Format für die Einreichung zu erstellen usw. Aus diesem Grund schreibe ich diesen Beitrag, in dem ich beschreibe, was ich für diesen Wettbewerb getan habe. Ich habe auch versucht, einen Kaggle-Kernel zu erstellen, aber es wurde festgestellt, dass Kaggle-Kernel nur lesbar ist, sodass ich meine Datenstruktur nicht verschieben und meine Übermittlungsdatei erstellen kann, sodass ich meinen Code an der Veröffentlichung vorbeikopiere. Zu Ihrer Information, ich habe diesen Wettbewerb auf meinem Macbook ohne eine einzige GPU durchgeführt.

Was Sie in diesem Beitrag erwarten können, ist: 1) Organisieren von Zug- / Validierungsdatensätzen, 2) Übertragen von Lerninhalten, 3) Speichern / Laden des besten Modells, 4) Erstellen von Schlussfolgerungen aus dem Testdatensatz, 5) Erstellen der Übermittlungsdatei im richtigen Format und Übermitteln an kaggle und noch einiges mehr. Kommen wir ohne weiteres dazu.

1. Daten organisieren

Ihre Daten kommen mit Zugdaten und Testdaten. Zugdaten haben sowohl Katzen als auch Hunde, aber sie haben eine Klasse im Dateinamen (Katze. .jpg für Katzenbilder, Hunde. .jpg für Hundebilder.). Da PyTorch das Laden von Bilddaten aus Unterordnern des Datenverzeichnisses unterstützt, müssen alle Katzenbilder in den Katzenordner und alle Hundebilder in den Hundeordner verschoben werden. Wir müssen auch ein Validierungsset einrichten, um zu überprüfen, ob unser Modell richtig lernt. Kisten Sie also die Unterordner Katzen und Hunde im Ordner Zug, erstellen Sie den Ordner val unter dem Eingabeordner und erstellen Sie die gleichen Unterordner im Ordner val. Die Testdaten sind unbeschriftet und können so belassen werden, wie sie sind.

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 ("Druckdatenverzeichnis")
print (os.listdir ("data")) # Zeigt train, val folder sind unter data
print ("Zugrichtung drucken")
! ls {train_dir} | head -n 5 # Zeigt an, dass sich die Bilddateien im Zugordner befinden
print ("Zughundedir drucken")
! ls {train_dogs_dir} | head -n 5 # Überprüfe, ob der (leere) Ordner existiert
print ("Zugkatze drucken")
! ls {train_cats_dir} | head -n 5 # Überprüfe, ob der (leere) Ordner existiert
print ("Val Dir drucken")
! ls {val_dir} | head -n 5 # Zeigt an, dass Unterordner Hunde und Katzen existieren
print ("Drucken von val dog dir")
! ls {val_dogs_dir} | head -n 5 # Überprüfe, ob der (leere) Ordner existiert
print ("Val cat dir drucken")
! ls {val_cats_dir} | head -n 5 # Überprüfe, ob der (leere) Ordner existiert

Führen Sie den obigen Code in Jupyter Notebook aus und überprüfen Sie, ob wir die richtige Ordnerstruktur vorbereitet haben. Der nächste Schritt ist das Verschieben von Dateien in den richtigen Ordner.

importiere es
Import wieder
files = os.listdir (train_dir)
# Verschiebe alle Zugkatzenbilder in den Katzenordner, Hundebilder in den Hundeordner
für f in Dateien:
    catSearchObj = re.search ("cat", f)
    dogSearchObj = re.search ("Hund", f)
    wenn catSearchObj:
        shutil.move (f '{train_dir} / {f}', train_cats_dir)
    elif dogSearchObj:
        shutil.move (f '{train_dir} / {f}', train_dogs_dir)

Überprüfen wir, ob wir die Dateien richtig verschoben haben.

print ("Printing train dir") # zeigt nur die Unterordner für Katzen und Hunde an
! ls {train_dir} | head -n 5
print ("Zughundedir drucken") # Es befinden sich jetzt Hundebilder im Hundeordner
! ls {train_dogs_dir} | head -n 5
print ("Train Cat Dir drucken") # Es befinden sich jetzt Katzenbilder im Katzenordner
! ls {train_cats_dir} | head -n 5

Lassen Sie uns nun einige Hundebilder für den Validierungssatz trennen. In vielen Fällen möchten Sie 20% Ihrer gesamten Daten als Validierungssatz trennen. In diesem Fall befinden sich 25.000 Bilder im Trainingsset. Dies sind ziemlich viele, da Katzen und Hunde wie ImageNet-Daten sind. Ich dachte, 20% davon sind zu viele und es ist genug, um jeweils 1.000 Bilder für Katzen und Hunde aufzunehmen.

files = os.listdir (train_dogs_dir)
für f in Dateien:
    validationDogsSearchObj = re.search ("5 \ d \ d \ d", f)
    if validationDogsSearchObj:
        shutil.move (f '{train_dogs_dir} / {f}', val_dogs_dir)
print ("Drucken von val dog dir")
! ls {val_dogs_dir} | head -n 5

Der obige Code verschiebt Hundebilder mit einer ID zwischen 5000 und 5999 in den Validierungsordner. Und machen Sie dasselbe für das Katzenbild.

files = os.listdir (train_cats_dir)
für f in Dateien:
    validationCatsSearchObj = re.search ("5 \ d \ d \ d", f)
    if validationCatsSearchObj:
        shutil.move (f '{train_cats_dir} / {f}', val_cats_dir)
print ("Val cat dir drucken")
! ls {val_cats_dir} | head -n 5

2. Trainingsmodell

Wenn die Daten nun die richtige Struktur haben, ist es Zeit, unser Modell zu trainieren. Zunächst importiere ich, was ich für dieses Notebook brauche. Dies ist jedoch nicht die vollständige Liste der Importe. Den Rest importieren wir nach Bedarf.

Fackel importieren
fackel.nn als nn importieren
fackel.optim als optim importieren
aus torch.optim import lr_scheduler
numpy als np importieren
fackelvision importieren
aus Torchvision importieren Datensätze, Modelle, Transformationen
importiere matplotlib.pyplot als plt
Importzeit
import os
Kopie importieren
Mathe importieren
Drucken (Fackel .__ Version__)
plt.ion () # interaktiver Modus

Definieren wir die Erweiterung der Trainingsdaten und die Transformation der Validierungsdaten.

# Datenerweiterung und -normalisierung für das Training
# Nur Normalisierung zur Validierung
data_transforms = {
    'train': transforms.Compose ([
        transforms.RandomRotation (5),
        transforms.RandomHorizontalFlip (),
        transforms.RandomResizedCrop (224, Maßstab = (0,96, 1,0), Verhältnis = (0,95, 1,05)),
        transforms.ToTensor (),
        transforms.Normalize ([0,485, 0,456, 0,406], [0,229, 0,224, 0,225])
    ]),
    'val': transforms.Compose ([
        transforms.Resize ([224,224]),
        transforms.ToTensor (),
        transforms.Normalize ([0,485, 0,456, 0,406], [0,229, 0,224, 0,225])
    ]),
}

Als Datenerweiterung verwende ich ein wenig Rotation, zufällige Spiegelungen und Größenänderung + Zuschneiden. Die Skalierung der Größe beträgt 0,96–1,0. Ich versuche zu vermeiden, eine Skala von weniger als 0,96 anzugeben, da Sie immer noch die gewünschte Variation der Daten erhalten können und das Risiko, dass ein wichtiger Teil der Daten abgeschnitten wird (z. B. Kopf der Katze oder des Hundes, wenn wir keinen Kopfteil haben), viel geringer ist wird es für die Maschine viel schwieriger sein zu lernen, wie jede Klasse aussehen sollte). Auch eine moderate Änderung des Verhältnisses sollte für unseren Zweck in Ordnung sein (dünner oder fetter, eine Katze ist eine Katze, oder?). Bei der Normalisierung habe ich einen hartcodierten Wert für Mittelwert und Standardabweichung verwendet. Es ist bekannt, dass diese Werte gut funktionieren und häufig verwendet werden. Überprüfen Sie die Empfehlung dieses Facebook AI-Technikers zur Verwendung dieser Werte und auch das offizielle PyTorch-Beispiel mit demselben Wert.

data_dir = 'data'
CHECK_POINT_PATH = 'checkpoint.tar'
SUBMISSION_FILE = 'submission.csv'
image_datasets = {x: datasets.ImageFolder (os.path.join (data_dir, x),
                                          data_transforms [x])
                  für x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader (image_datasets [x], batch_size = 4,
                                              shuffle = True, num_workers = 4)
              für x in ['train', 'val']}
dataset_sizes = {x: len (image_datasets [x]) für x in ['train', 'val']}
class_names = image_datasets ['train']. classes
device = torch.device ("cuda: 0", wenn torch.cuda.is_available (), sonst "cpu")
print (class_names) # => ['Katzen', 'Hunde']
print (f'Train-Bildgröße: {dataset_sizes ["train"]} ')
print (f'Validierungsbildgröße: {dataset_sizes ["val"]} ')

Definieren Sie dann einige benötigte Konstanten und definieren Sie die Datensätze (train und val). Sie sollten 23000 für die Zugbildgröße und 2000 für die Validierungsbildgröße sehen, wenn Sie alles richtig befolgt haben. Zu Ihrer Information: Da ich mit keiner GPU arbeite und es frustrierend lange dauert, bis ich so viele Daten verarbeitet habe, habe ich eine ganze Reihe von Daten gelöscht und insgesamt 950 Zugbilder und 71 Validierungsbilder erstellt. Für mich führte dies zu einer zufriedenstellenden Genauigkeit, und der Zweck für mich bestand nicht darin, ein möglichst genaues Modell zu erstellen, sondern die Verwendung der PyTorch- und Kaggle-Website zu üben. Aus diesem Grund habe ich beschlossen, nicht so viele Daten zu verwenden, aber Sie möchten natürlich nicht löschen Daten, wenn Sie ein Modell für Produktionszwecke trainieren. Ein weiterer Grund, warum es mir möglich war, mein Modell mit so wenig Daten zu trainieren, war die Verwendung eines vortrainierten Modells. Das heißt, ich habe nur die letzte Ebene geändert und trainiert und alle Ebenen so verwendet, wie sie waren, da das Modell bereits mit ImageNet-Daten gut trainiert war.

Werfen wir einen Blick darauf, wie ein Mini-Stapel (4 Bilder) aus dem Trainingsset mit dem nächsten Codeausschnitt aussieht.

def imshow (inp, title = None):
    "Imshow für Tensor."
    inp = inp.numpy (). transponieren ((1, 2, 0))
    Mittelwert = np.array ([0,485, 0,456, 0,406])
    std = np.array ([0,229, 0,224, 0,225])
    inp = std * inp + mean
    inp = np.clip (inp, 0, 1)
    plt.imshow (inp)
    wenn title nicht None ist:
        plt.title (Titel)
    plt.pause (0.001) # pausiert ein wenig, damit die Grafiken aktualisiert werden
# Holen Sie sich einen Stapel Trainingsdaten
eingaben, klassen = next (iter (dataloaders ['train']))
# Erstellen Sie ein Raster aus einem Stapel
sample_train_images = torchvision.utils.make_grid (Eingaben)
imshow (sample_train_images, title = classes)

Sie sehen zufällig 4 Bilder ausgewählt und Titel wird 0 für Katze und 1 für Hund sagen. Als nächstes definieren wir eine Funktion, die unser Modell trainiert und eine Metrik zurückgibt.

def train_model (Modell, Kriterium, Optimierer, Scheduler, num_epochs = 2, Checkpoint = None):
    seit = time.time ()
Wenn Checkpoint None ist:
        best_model_wts = copy.deepcopy (model.state_dict ())
        best_loss = math.inf
        best_acc = 0.
    sonst:
        print (f'Val loss: {checkpoint ["best_val_loss"]}, Wertgenauigkeit: {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']
für Epoche in Reichweite (num_epochs):
        print ('Epoch {} / {}'. Format (Epoche, Anzahl_Epochen - 1))
        print ('-' * 10)
# Jede Epoche hat eine Trainings- und Validierungsphase
        für die Phase in ['train', 'val']:
            wenn phase == 'train':
                scheduler.step ()
                model.train () # Versetzt das Modell in den Trainingsmodus
            sonst:
                model.eval () # Modell auf Auswertungsmodus einstellen
running_loss = 0.0
            running_corrects = 0
# Daten durchlaufen.
            für i, (Eingaben, Bezeichnungen) in Aufzählung (Datenlader [Phase]):
                Eingänge = Eingänge.zu (Gerät)
                labels = labels.to (Gerät)
# Null die Parameterverläufe
                optimizer.zero_grad ()
                
                wenn i% 200 == 199:
                    Druckverlust ('[% d,% d]:% .3f'%
                          (Epoche + 1, i, running_loss / (i * inputs.size (0)))
# nach vorne
                # Track-Geschichte, wenn nur im Zug
                mit torch.set_grad_enabled (phase == 'train'):
                    Ausgänge = Modell (Eingänge)
                    _, preds = torch.max (Ausgänge, 1)
                    Verlust = Kriterium (Outputs, Labels)
# rückwärts + nur in der Trainingsphase optimieren
                    wenn phase == 'train':
                        loss.backward ()
                        optimizer.step ()
# Statistiken
                running_loss + = loss.item () * inputs.size (0)
                running_corrects + = torch.sum (preds == labels.data)
epoch_loss = running_loss / dataset_sizes [phase]
            epoch_acc = running_corrects.double () / dataset_sizes [phase]
print ('{} Loss: {: .4f} Acc: {: .4f}'. format (
                phase, epoch_loss, epoch_acc))
# Kopieren Sie das Modell
            if phase == 'val' und epoch_loss 
drucken()
time_elapsed = time.time () - seit
    print ('Training abgeschlossen in {: .0f} m {: .0f} s'.format (
        time_elapsed // 60, time_elapsed% 60))
    print ('Bester Wert Acc: {: .4f} Bester Wertverlust: {: .4f}'. Format (best_acc, best_loss))
# Laden Sie die besten Modellgewichte
    model.load_state_dict (best_model_wts)
    Rückgabemodell, best_loss, best_acc

Die Funktion prüft zunächst, ob der gespeicherte Prüfpunkt überschritten wurde. Wenn ja, wird der gespeicherte Parameter geladen und das Training dort gestartet, wo es aufgehört hat. Wenn nein, beginnt es mit dem Training des Modells, an dem es bestanden wurde (wir verwenden weiterhin von Anfang an ein vortrainiertes Modell). Die Funktion aktualisiert die Parameter nur in der Zugphase und druckt alle Epochen oder immer dann, wenn ein neuer bester Verlust vorliegt, einige Messwerte aus.

Definieren wir nun unser Modell, indem wir ein vorgefertigtes Modell herunterladen. Das Ausführen des nächsten Codes dauert einige Zeit, wenn Sie ihn noch nie zuvor ausgeführt haben.

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

resnet50 ist eine Faltungsarchitektur für neuronale Netze, die sehr leistungsfähig ist, um Computer-Vision-Probleme zu lösen. Zu den weniger leistungsstarken, aber ressourcenschonenden Modellen, an denen Sie möglicherweise interessiert sind, gehören resnet18 und resnet34. Wenn Sie als Argument pretrained = True angeben, wird ein Modell mit den Parametern heruntergeladen, die mit dem ImageNet-Datensatz trainiert wurden. Da wir das Modell für unsere Anforderungen ändern müssen (Klassifizierung der Binärklasse), ändern wir die letzte vollständig verbundene Ebene und definieren eine Verlustfunktion, die für das Klassifizierungsproblem nützlich ist (Kreuzentropieverlust, der die logarithmische Softmax- und die negative logarithmische Likelihood-Verlustfunktion kombiniert). . Optimzier ist ein Optimierer für den stochastischen Gradientenabstieg, und Scheduler ist exponentiell, da er die Lernrate alle 7 Epochen um den Faktor 10 verringert (in Wirklichkeit habe ich nur 6 Epochen trainiert).

für param in model_conv.parameters ():
    param.requires_grad = False
# Parameter neu konstruierter Module haben standardmäßig require_grad = True
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear (num_ftrs, 2)
model_conv = model_conv.to (Gerät)
Kriterium = nn.CrossEntropyLoss ()
# Beachten Sie, dass nur die Parameter der letzten Ebene optimiert werden
optimizer_conv = optim.SGD (model_conv.fc.parameters (), lr = 0,001, momentum = 0,9)
# Verringern Sie LR alle 7 Epochen um den Faktor 0,1
exp_lr_scheduler = lr_scheduler.StepLR (optimizer_conv, step_size = 7, gamma = 0.1)

Wir können endlich mit dem eigentlichen Training beginnen.

Versuchen:
    checkpoint = torch.load (CHECK_POINT_PATH)
    print ("Checkpoint geladen")
außer:
    Checkpoint = Keine
    print ("Checkpoint nicht gefunden")
model_conv, best_val_loss, best_val_acc = train_model (model_conv,
                                                      Kriterium,
                                                      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)

Der Code prüft zunächst, ob ein Prüfpunkt aus dem vorherigen Training gespeichert wurde. Wenn ja, übergeben Sie den Checkpoint an trainmodel function. Hier habe ich 3 Epochen angegeben und die Funktion gibt das Modell, den Verlust und die Genauigkeit zurück, mit der der Verlust in allen Epochen am niedrigsten war. Wir speichern das, was wir von der Funktion erhalten haben, an einem Kontrollpunkt. Sie können die Epochennummer anpassen oder dieses Snippet beliebig oft wiederholen. Wenn Sie sehen, dass sich das Modell nicht mehr verbessert, können Sie aufhören. Da ich nur 71 Validierungsbilder hatte, erreichte ich in nur 2 Läufen eine Genauigkeit von 1,0 mit einem Verlust von 0,036 und beschloss, das Training abzubrechen.

Beachten Sie, dass wir nur die letzte Schicht trainieren mussten, die wir vom ursprünglichen resnet50 geändert haben. Es ist möglich, dass wir alle Parameter in allen Ebenen trainieren, aber als ich das versuchte, sah ich nur, dass sich der Verlust und die Genauigkeit verschlechterten. Wenn Sie versuchen möchten, alle Parameter zu aktualisieren, können Sie dies wie folgt tun.

für param in model_conv.parameters ():
    param.requires_grad = True
model_conv = model_conv.to (Gerät)
# Beachten Sie, dass alle Parameter optimiert werden
optimizer_ft = optim.SGD (model_conv.parameters (), lr = 0,001, momentum = 0,9)

Führen Sie dann den gleichen Code für die Trainingsschleife wie zuvor aus (der Codeblock beginnt mit try).

Vielleicht wird diese Geschichte schon zu lang. Also schreibe ich den Teil, den Sie tatsächlich anhand des Testdatensatzes an das Modell ableiten, und sende Ihre Antwort an kaggle in einer separaten Geschichte. Schalten Sie ein für den nächsten Teil und mehr. Lassen Sie mich wissen, ob es Ihnen gelungen ist, meinem Code zu folgen und ob Sie mit dieser Methode ein zufriedenstellendes Ergebnis erzielt haben.

Der zweite (letzte) Teil ist aus. Wenn Sie weiterlesen möchten, klicken Sie hier. Sie können den vollständigen Code auch in meinem Github-Repo sehen. Code für die Datenvorverarbeitung finden Sie in datapreprocessor.ipynb und Training, Inferenzen und Einreichungen in catsanddogs.ipynb.