Der Verlust nimmt bei Pytorch CNN nicht ab

8

Ich mache ein CNN mit Pytorch für eine Aufgabe, aber es wird nicht lernen und die Genauigkeit verbessern. Ich habe eine Version erstellt, die mit dem MNIST-Datensatz arbeitet, damit ich sie hier veröffentlichen kann. Ich suche nur nach einer Antwort, warum es nicht funktioniert. Die Architektur ist in Ordnung, ich habe sie in Keras implementiert und hatte nach 3 Epochen eine Genauigkeit von über 92%. Hinweis: Ich habe den MNIST in 60x60-Bilder umgeformt, da die Bilder in meinem "echten" Problem so sind.

import numpy as np
from PIL import Image
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.autograd import Variable
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()


def resize(pics):
    pictures = []
    for image in pics:
        image = Image.fromarray(image).resize((dim, dim))
        image = np.array(image)
        pictures.append(image)
    return np.array(pictures)


dim = 60

x_train, x_test = resize(x_train), resize(x_test) # because my real problem is in 60x60

x_train = x_train.reshape(-1, 1, dim, dim).astype('float32') / 255
x_test = x_test.reshape(-1, 1, dim, dim).astype('float32') / 255
y_train, y_test = y_train.astype('float32'), y_test.astype('float32') 

if torch.cuda.is_available():
    x_train = torch.from_numpy(x_train)[:10_000]
    x_test = torch.from_numpy(x_test)[:4_000] 
    y_train = torch.from_numpy(y_train)[:10_000] 
    y_test = torch.from_numpy(y_test)[:4_000]


class ConvNet(nn.Module):

    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3)
        self.conv2 = nn.Conv2d(32, 64, 3)
        self.conv3 = nn.Conv2d(64, 128, 3)

        self.fc1 = nn.Linear(5*5*128, 1024) 
        self.fc2 = nn.Linear(1024, 2048)
        self.fc3 = nn.Linear(2048, 1)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv3(x)), (2, 2))

        x = x.view(x.size(0), -1) 
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.dropout(x, 0.5)
        x = torch.sigmoid(self.fc3(x))
        return x


net = ConvNet()

optimizer = optim.Adam(net.parameters(), lr=0.03)

loss_function = nn.BCELoss()


class FaceTrain:

    def __init__(self):
        self.len = x_train.shape[0]
        self.x_train = x_train
        self.y_train = y_train

    def __getitem__(self, index):
        return x_train[index], y_train[index].unsqueeze(0)

    def __len__(self):
        return self.len


class FaceTest:

    def __init__(self):
        self.len = x_test.shape[0]
        self.x_test = x_test
        self.y_test = y_test

    def __getitem__(self, index):
        return x_test[index], y_test[index].unsqueeze(0)

    def __len__(self):
        return self.len


train = FaceTrain()
test = FaceTest()

train_loader = DataLoader(dataset=train, batch_size=64, shuffle=True)
test_loader = DataLoader(dataset=test, batch_size=64, shuffle=True)

epochs = 10
steps = 0
train_losses, test_losses = [], []
for e in range(epochs):
    running_loss = 0
    for images, labels in train_loader: 
        optimizer.zero_grad()
        log_ps = net(images)
        loss = loss_function(log_ps, labels)
        loss.backward()
        optimizer.step()        
        running_loss += loss.item()        
    else:
        test_loss = 0
        accuracy = 0        

        with torch.no_grad():
            for images, labels in test_loader: 
                log_ps = net(images)
                test_loss += loss_function(log_ps, labels)                
                ps = torch.exp(log_ps)
                top_p, top_class = ps.topk(1, dim=1)
                equals = top_class.type('torch.LongTensor') == labels.type(torch.LongTensor).view(*top_class.shape)
                accuracy += torch.mean(equals.type('torch.FloatTensor'))
        train_losses.append(running_loss/len(train_loader))
        test_losses.append(test_loss/len(test_loader))
        print("[Epoch: {}/{}] ".format(e+1, epochs),
              "[Training Loss: {:.3f}] ".format(running_loss/len(train_loader)),
              "[Test Loss: {:.3f}] ".format(test_loss/len(test_loader)),
              "[Test Accuracy: {:.3f}]".format(accuracy/len(test_loader)))
Nicolas Gervais
quelle
1
In den comp.ai.neural-netsFAQs finden Sie einige gute Vorschläge, wo Sie suchen müssen, wenn Ihr neuronales Netz nicht lernt. Ich würde empfehlen, dort zu beginnen.
Ari Cooper-Davis
Die Verlustfunktion, die Ausgabeform des Netzwerks und die Zielbezeichnungen sind hier nicht sinnvoll (diese Kombination ist zumindest falsch). MNIST hat 10 Klassen und die Beschriftungen sind ganze Zahlen zwischen 0 und 9. BCELoss erwartet für jedes Ziel einen einzelnen Wert zwischen 0 und 1. Sie sollten stattdessen 10 Logs ausgeben (nicht unbedingt sigmoided) und dann nn.CrossEntropyLossfür das Klassifizierungsproblem einer einzelnen Klasse verwenden. nn.CrossEntropyLossWendet sowohl Softmax als auch NLLLoss als eine einzige Operation an, also nicht zuerst Softmax.
Jodag
Wenn Sie alternativ ein Regressionsproblem durchführen möchten, dh eine tatsächliche Anzahl als Ausgabe schätzen (nicht empfohlen für Probleme mit Klassifizierungstypen), können Sie es versuchen, nn.MSELossaber dann müssen Sie entweder die Ziele so anpassen, dass sie in den Bereich der Sigmoid-Ausgabe fallen, oder nicht. t Sigmoid nach der letzten Schicht auftragen.
Jodag
Hast du es so gemacht? Jetzt sagt es mir das RuntimeError: multi-target not supported.
Nicolas Gervais
Sie müssen eine Dimension von Etiketten quetschen (es sollte ein 1D-Tensor von ganzen Zahlen sein, die die Größe der
Stapelgröße haben

Antworten:

7

Zuerst die Hauptprobleme ...

1. Das Hauptproblem bei diesem Code ist, dass Sie die falsche Ausgabeform und die falsche Verlustfunktion für die Klassifizierung verwenden.

nn.BCELossberechnet den binären Kreuzentropieverlust. Dies gilt, wenn Sie ein oder mehrere Ziele haben, die entweder 0 oder 1 sind (daher die Binärdatei). In Ihrem Fall ist das Ziel eine einzelne Ganzzahl zwischen 0 und 9. Da es nur eine geringe Anzahl potenzieller Zielwerte gibt, wird am häufigsten der kategoriale Kreuzentropieverlust verwendet ( nn.CrossEntropyLoss). Die "theoretische" Definition des Kreuzentropieverlusts erwartet, dass die Netzwerkausgänge und die Ziele beide 10-dimensionale Vektoren sind, wobei das Ziel alle Nullen außer an einem Ort ist (One-Hot-codiert). Aus Gründen der Rechenstabilität und der Raumeffizienz nn.CrossEntropyLossnimmt pytorch's jedoch die ganze Zahl direkt als Ziel . jedochmüssen Sie ihm noch einen 10-dimensionalen Ausgabevektor aus Ihrem Netzwerk bereitstellen.

# pseudo code (ignoring batch dimension)
loss = nn.functional.cross_entropy_loss(<output 10d vector>, <integer target>)

Um dieses Problem in Ihrem Code zu beheben, muss fc3ein 10-dimensionales Feature ausgegeben werden, und die Beschriftungen müssen Ganzzahlen (keine Gleitkommazahlen) sein. Außerdem muss .sigmoidfc3 nicht verwendet werden, da die Entropieverlustfunktion von pytorch intern log-softmax anwendet, bevor der endgültige Verlustwert berechnet wird.

2. Wie von Serget Dymchenko hervorgehoben, müssen Sie das Netzwerk evalwährend der Inferenz in den Modus und trainwährend des Zugs in den Modus schalten. Dies betrifft hauptsächlich Dropout- und Batch_Norm-Ebenen, da sie sich während des Trainings und der Inferenz unterschiedlich verhalten.

3. Eine Lernrate von 0,03 ist wahrscheinlich etwas zu hoch. Es funktioniert gut mit einer Lernrate von 0,001 und in ein paar Experimenten sah ich, dass das Training bei 0,03 divergierte.


Um diese Korrekturen zu berücksichtigen, mussten einige Änderungen vorgenommen werden. Die minimalen Korrekturen am Code sind unten aufgeführt. Ich habe alle Zeilen kommentiert, die geändert wurden, ####gefolgt von einer kurzen Beschreibung der Änderung.

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.autograd import Variable
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()


def resize(pics):
    pictures = []
    for image in pics:
        image = Image.fromarray(image).resize((dim, dim))
        image = np.array(image)
        pictures.append(image)
    return np.array(pictures)


dim = 60

x_train, x_test = resize(x_train), resize(x_test) # because my real problem is in 60x60

x_train = x_train.reshape(-1, 1, dim, dim).astype('float32') / 255
x_test = x_test.reshape(-1, 1, dim, dim).astype('float32') / 255
#### float32 -> int64
y_train, y_test = y_train.astype('int64'), y_test.astype('int64')

#### no reason to test for cuda before converting to numpy

#### I assume you were taking a subset for debugging? No reason to not use all the data
x_train = torch.from_numpy(x_train)
x_test = torch.from_numpy(x_test)
y_train = torch.from_numpy(y_train)
y_test = torch.from_numpy(y_test)


class ConvNet(nn.Module):

    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3)
        self.conv2 = nn.Conv2d(32, 64, 3)
        self.conv3 = nn.Conv2d(64, 128, 3)

        self.fc1 = nn.Linear(5*5*128, 1024)
        self.fc2 = nn.Linear(1024, 2048)
        #### 1 -> 10
        self.fc3 = nn.Linear(2048, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv3(x)), (2, 2))

        x = x.view(x.size(0), -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.dropout(x, 0.5)
        #### removed sigmoid
        x = self.fc3(x)
        return x


net = ConvNet()

#### 0.03 -> 1e-3
optimizer = optim.Adam(net.parameters(), lr=1e-3)

#### BCELoss -> CrossEntropyLoss
loss_function = nn.CrossEntropyLoss()


class FaceTrain:

    def __init__(self):
        self.len = x_train.shape[0]
        self.x_train = x_train
        self.y_train = y_train

    def __getitem__(self, index):
        #### .unsqueeze(0) removed
        return x_train[index], y_train[index]

    def __len__(self):
        return self.len


class FaceTest:

    def __init__(self):
        self.len = x_test.shape[0]
        self.x_test = x_test
        self.y_test = y_test

    def __getitem__(self, index):
        #### .unsqueeze(0) removed
        return x_test[index], y_test[index]

    def __len__(self):
        return self.len


train = FaceTrain()
test = FaceTest()

train_loader = DataLoader(dataset=train, batch_size=64, shuffle=True)
test_loader = DataLoader(dataset=test, batch_size=64, shuffle=True)

epochs = 10
steps = 0
train_losses, test_losses = [], []
for e in range(epochs):
    running_loss = 0
    #### put net in train mode
    net.train()
    for idx, (images, labels) in enumerate(train_loader):
        optimizer.zero_grad()
        log_ps = net(images)
        loss = loss_function(log_ps, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    else:
        test_loss = 0
        accuracy = 0

        #### put net in eval mode
        net.eval()
        with torch.no_grad():
            for images, labels in test_loader:
                log_ps = net(images)
                test_loss += loss_function(log_ps, labels)
                #### removed torch.exp() since exponential is monotone, taking it doesn't change the order of outputs. Similarly with torch.softmax()
                top_p, top_class = log_ps.topk(1, dim=1)
                #### convert to float/long using proper methods. what you have won't work for cuda tensors.
                equals = top_class.long() == labels.long().view(*top_class.shape)
                accuracy += torch.mean(equals.float())
        train_losses.append(running_loss/len(train_loader))
        test_losses.append(test_loss/len(test_loader))
        print("[Epoch: {}/{}] ".format(e+1, epochs),
              "[Training Loss: {:.3f}] ".format(running_loss/len(train_loader)),
              "[Test Loss: {:.3f}] ".format(test_loss/len(test_loader)),
              "[Test Accuracy: {:.3f}]".format(accuracy/len(test_loader)))

Trainingsergebnisse sind jetzt ...

[Epoch: 1/10]  [Training Loss: 0.139]  [Test Loss: 0.046]  [Test Accuracy: 0.986]
[Epoch: 2/10]  [Training Loss: 0.046]  [Test Loss: 0.042]  [Test Accuracy: 0.987]
[Epoch: 3/10]  [Training Loss: 0.031]  [Test Loss: 0.040]  [Test Accuracy: 0.988]
[Epoch: 4/10]  [Training Loss: 0.022]  [Test Loss: 0.029]  [Test Accuracy: 0.990]
[Epoch: 5/10]  [Training Loss: 0.017]  [Test Loss: 0.066]  [Test Accuracy: 0.987]
[Epoch: 6/10]  [Training Loss: 0.015]  [Test Loss: 0.056]  [Test Accuracy: 0.985]
[Epoch: 7/10]  [Training Loss: 0.018]  [Test Loss: 0.039]  [Test Accuracy: 0.991]
[Epoch: 8/10]  [Training Loss: 0.012]  [Test Loss: 0.057]  [Test Accuracy: 0.988]
[Epoch: 9/10]  [Training Loss: 0.012]  [Test Loss: 0.041]  [Test Accuracy: 0.991]
[Epoch: 10/10]  [Training Loss: 0.007]  [Test Loss: 0.048]  [Test Accuracy: 0.992]

Einige andere Probleme, die Ihre Leistung und Ihren Code verbessern.

4. Sie verschieben das Modell niemals auf die GPU. Dies bedeutet, dass Sie keine GPU-Beschleunigung erhalten.

5. torchvision wurde mit allen Standardtransformationen und Datensätzen entwickelt und ist für die Verwendung mit PyTorch ausgelegt. Ich empfehle es zu benutzen. Dadurch wird auch die Abhängigkeit von Keras in Ihrem Code aufgehoben.

6. Normalisieren Sie Ihre Daten, indem Sie den Mittelwert subtrahieren und durch die Standardabweichung dividieren, um die Leistung Ihres Netzwerks zu verbessern. Mit Fackelvision können Sie verwenden transforms.Normalize. Dies wird bei MNIST keinen großen Unterschied machen, da es bereits zu einfach ist. Bei schwierigeren Problemen erweist es sich jedoch als wichtig.


Weiter verbesserter Code wird unten gezeigt (viel schneller auf der GPU).

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision import transforms

dim = 60

class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3)
        self.conv2 = nn.Conv2d(32, 64, 3)
        self.conv3 = nn.Conv2d(64, 128, 3)

        self.fc1 = nn.Linear(5 * 5 * 128, 1024)
        self.fc2 = nn.Linear(1024, 2048)
        self.fc3 = nn.Linear(2048, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv3(x)), (2, 2))

        x = x.view(x.size(0), -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.dropout(x, 0.5)
        x = self.fc3(x)
        return x


net = ConvNet()
if torch.cuda.is_available():
    net.cuda()

optimizer = optim.Adam(net.parameters(), lr=1e-3)

loss_function = nn.CrossEntropyLoss()

train_dataset = MNIST('./data', train=True, download=True,
                      transform=transforms.Compose([
                          transforms.Resize((dim, dim)),
                          transforms.ToTensor(),
                          transforms.Normalize((0.1307,), (0.3081,))
                      ]))
test_dataset = MNIST('./data', train=False, download=True,
                     transform=transforms.Compose([
                         transforms.Resize((dim, dim)),
                         transforms.ToTensor(),
                         transforms.Normalize((0.1307,), (0.3081,))
                     ]))

train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True, num_workers=8)
test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False, num_workers=8)

epochs = 10
steps = 0
train_losses, test_losses = [], []
for e in range(epochs):
    running_loss = 0
    net.train()
    for images, labels in train_loader:
        if torch.cuda.is_available():
            images, labels = images.cuda(), labels.cuda()
        optimizer.zero_grad()
        log_ps = net(images)
        loss = loss_function(log_ps, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    else:
        test_loss = 0
        accuracy = 0

        net.eval()
        with torch.no_grad():
            for images, labels in test_loader:
                if torch.cuda.is_available():
                    images, labels = images.cuda(), labels.cuda()
                log_ps = net(images)
                test_loss += loss_function(log_ps, labels)
                top_p, top_class = log_ps.topk(1, dim=1)
                equals = top_class.flatten().long() == labels
                accuracy += torch.mean(equals.float()).item()
        train_losses.append(running_loss/len(train_loader))
        test_losses.append(test_loss/len(test_loader))
        print("[Epoch: {}/{}] ".format(e+1, epochs),
              "[Training Loss: {:.3f}] ".format(running_loss/len(train_loader)),
              "[Test Loss: {:.3f}] ".format(test_loss/len(test_loader)),
              "[Test Accuracy: {:.3f}]".format(accuracy/len(test_loader)))

Aktualisierte Trainingsergebnisse ...

[Epoch: 1/10]  [Training Loss: 0.125]  [Test Loss: 0.045]  [Test Accuracy: 0.987]
[Epoch: 2/10]  [Training Loss: 0.043]  [Test Loss: 0.031]  [Test Accuracy: 0.991]
[Epoch: 3/10]  [Training Loss: 0.030]  [Test Loss: 0.030]  [Test Accuracy: 0.991]
[Epoch: 4/10]  [Training Loss: 0.024]  [Test Loss: 0.046]  [Test Accuracy: 0.990]
[Epoch: 5/10]  [Training Loss: 0.020]  [Test Loss: 0.032]  [Test Accuracy: 0.992]
[Epoch: 6/10]  [Training Loss: 0.017]  [Test Loss: 0.046]  [Test Accuracy: 0.991]
[Epoch: 7/10]  [Training Loss: 0.015]  [Test Loss: 0.034]  [Test Accuracy: 0.992]
[Epoch: 8/10]  [Training Loss: 0.011]  [Test Loss: 0.048]  [Test Accuracy: 0.992]
[Epoch: 9/10]  [Training Loss: 0.012]  [Test Loss: 0.037]  [Test Accuracy: 0.991]
[Epoch: 10/10]  [Training Loss: 0.013]  [Test Loss: 0.038]  [Test Accuracy: 0.992]
Jodag
quelle
1
Es funktionierte! Ich habe tatsächlich einen großen Fehler gemacht, dieses vereinfachte MNIST-Problem hatte 10 Klassen und mein Problem hatte nur zwei. Ich konnte also nicht alles verwenden, was du getan hast. Aber als ich mit Ihren Empfehlungen herumgespielt habe, konnte ich dafür sorgen, dass es funktioniert. Vielen Dank!
Nicolas Gervais
4

Eines ist mir aufgefallen, dass Sie das Modell im Zugmodus testen. Sie müssen anrufen net.eval(), um Aussetzer zu deaktivieren (und dann net.train()wieder in den Zugmodus zu versetzen).

Vielleicht gibt es noch andere Probleme. Geht der Trainingsverlust zurück? Haben Sie versucht, ein einzelnes Beispiel zu überarbeiten?

Sergii Dymchenko
quelle