web-dev-qa-db-fra.com

Le réseau siamois de Pytorch ne converge pas

Bonjour à tous

Voici mon implémentation d'un réseau siamois pytorch. J'utilise 32 tailles de lots, perte MSE et SGD avec un élan de 0,9 comme optimiseur.

class SiameseCNN(nn.Module):
    def __init__(self):
        super(SiameseCNN, self).__init__()                                      # 1, 40, 50
        self.convnet = nn.Sequential(nn.Conv2d(1, 8, 7), nn.ReLU(),             # 8, 34, 44
                                    nn.Conv2d(8, 16, 5), nn.ReLU(),             # 16, 30, 40
                                    nn.MaxPool2d(2, 2),                         # 16, 15, 20
                                    nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), # 32, 15, 20
                                    nn.Conv2d(32, 64, 3, padding=1), nn.ReLU()) # 64, 15, 20
        self.linear1 = nn.Sequential(nn.Linear(64 * 15 * 20, 100), nn.ReLU())
        self.linear2 = nn.Sequential(nn.Linear(100, 2), nn.ReLU())
        
    def forward(self, data):
        res = []
        for j in range(2):
            x = self.convnet(data[:, j, :, :])
            x = x.view(-1, 64 * 15 * 20)
            res.append(self.linear1(x))
        fres = abs(res[1] - res[0])
        return self.linear2(fres)

Chaque lot contient des paires alternées, c'est-à-dire [pos, pos], [pos, neg], [pos, pos] Etc ... Cependant, le réseau ne converge pas, et le problème semble que fres dans le réseau est le même pour chaque paire (indépendamment du fait que c'est une paire positive ou négative), et la sortie de self.linear2(fres) est toujours approximativement égale à [0.0531, 0.0770]. Ceci est en contraste avec ce que j'attends, à savoir que la première valeur de [0.0531, 0.0770] Se rapprocherait de 1 pour une paire positive au fur et à mesure que le réseau apprend, et la deuxième valeur se rapprocherait de 1 pour une paire négative . Ces deux valeurs doivent également totaliser 1.

J'ai testé exactement la même configuration et les mêmes images d'entrée pour une architecture de réseau à 2 canaux, où, au lieu d'alimenter [pos, pos], Vous empileriez ces 2 images de manière approfondie, par exemple numpy.stack([pos, pos], -1). La dimension de nn.Conv2d(1, 8, 7) change également en nn.Conv2d(2, 8, 7) dans cette configuration. Cela fonctionne parfaitement bien.

J'ai également testé exactement la même configuration et les mêmes images d'entrée pour une approche CNN traditionnelle, où je ne fais que passer des images à échelle de gris positive et négative dans le réseau, au lieu de les empiler (comme avec l'approche 2-CH) ou de les transmettre comme des paires d'images (comme avec l'approche siamoise). Cela fonctionne également parfaitement, mais les résultats ne sont pas aussi bons qu'avec l'approche à 2 canaux.

EDIT (Solutions que j'ai essayées):

def forward(self, data):
    res = []
    for j in range(2):
        x = self.convnet(data[:, j, :, :])
        x = x.view(-1, 64 * 15 * 20)
        res.append(x)
    fres = self.linear2(self.linear1(abs(res[1] - res[0]))))
    return fres 
def forward(self, data):
    res = []
    for j in range(2):
        x = self.convnet(data[:, j, :, :])
        res.append(x)
    pdist = nn.PairwiseDistance(p=2)
    diff = pdist(res[1], res[0])
    diff = diff.view(-1, 64 * 15 * 10)
    fres = self.linear2(self.linear1(diff))
    return fres

Une autre chose à noter peut-être est que, dans le cadre de mes recherches, un réseau siamois est formé pour chaque objet. Ainsi, la première classe est associée aux images contenant l'objet en question, et la seconde classe est associée aux images contenant d'autres objets. Je ne sais pas si cela pourrait être la cause du problème. Ce n'est cependant pas un problème dans le contexte des approches CNN traditionnel et CNN 2 canaux.

Sur demande, voici mon code de formation:

model = SiameseCNN().cuda()
ls_fn = torch.nn.BCELoss()
optim = torch.optim.SGD(model.parameters(),  lr=1e-6, momentum=0.9)
epochs = np.arange(100)
eloss = []
for Epoch in epochs:
    model.train()
    train_loss = []
    for x_batch, y_batch in dp.train_set:
        x_var, y_var = Variable(x_batch.cuda()), Variable(y_batch.cuda())
        y_pred = model(x_var)
        loss = ls_fn(y_pred, y_var)
        train_loss.append(abs(loss.item()))
        optim.zero_grad()
        loss.backward()
        optim.step()
    eloss.append(np.mean(train_loss))
    print(Epoch, np.mean(train_loss))

Remarque dp in dp.train_set Est une classe avec des attributs train_set, valid_set, test_set, Où chaque ensemble est créé comme suit:

DataLoader(TensorDataset(torch.Tensor(x), torch.Tensor(y)), batch_size=bs)

Selon la demande, voici un exemple des probabilités prédites par rapport à l'étiquette vraie, où vous pouvez voir que le modèle ne semble pas apprendre:

Predicted:  0.5030623078346252 Label:  1.0
Predicted:  0.5030624270439148 Label:  0.0
Predicted:  0.5030624270439148 Label:  1.0
Predicted:  0.5030625462532043 Label:  0.0
Predicted:  0.5030625462532043 Label:  1.0
Predicted:  0.5030626654624939 Label:  0.0
Predicted:  0.5030626058578491 Label:  1.0
Predicted:  0.5030627250671387 Label:  0.0
Predicted:  0.5030626654624939 Label:  1.0
Predicted:  0.5030627846717834 Label:  0.0
Predicted:  0.5030627250671387 Label:  1.0
Predicted:  0.5030627846717834 Label:  0.0
Predicted:  0.5030627250671387 Label:  1.0
Predicted:  0.5030628442764282 Label:  0.0
Predicted:  0.5030627846717834 Label:  1.0
Predicted:  0.5030628442764282 Label:  0.0
4
Emile Beukes

Problème résolu. Il s'avère que le réseau prédira la même sortie à chaque fois si vous lui donnez les mêmes images à chaque fois ???? Petite erreur d'indexation de ma part lors du partitionnement des données. Merci pour l'aide et l'assistance de tous. Voici un exemple de la convergence telle qu'elle est maintenant:

0 0.20198837077617646
1 0.17636818194389342
2 0.15786472541093827
3 0.1412761415243149
4 0.126698794901371
5 0.11397973036766053
6 0.10332610329985618
7 0.09474560652673245
8 0.08779258838295936
9 0.08199785630404949
10 0.07704121413826942
11 0.07276330365240574
12 0.06907484836131335
13 0.06584368328005076
14 0.06295975042134523
15 0.06039590438082814
16 0.058096024941653016
0
Emile Beukes

Je pense que votre approche est correcte et que vous faites bien les choses. Ce qui me semble un peu étrange, c'est la dernière couche qui a une activation RELU. Habituellement, avec les réseaux siamois, vous souhaitez générer une probabilité élevée lorsque les deux images d'entrée appartiennent à la même classe et une probabilité faible dans le cas contraire. Vous pouvez donc l'implémenter avec une seule sortie neurone et une fonction d'activation sigmoïde.

Par conséquent, je réimplémenterais votre réseau comme suit:

class SiameseCNN(nn.Module):
    def __init__(self):
        super(SiameseCNN, self).__init__()                                      # 1, 40, 50
        self.convnet = nn.Sequential(nn.Conv2d(1, 8, 7), nn.ReLU(),             # 8, 34, 44
                                    nn.Conv2d(8, 16, 5), nn.ReLU(),             # 16, 30, 40
                                    nn.MaxPool2d(2, 2),                         # 16, 15, 20
                                    nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), # 32, 15, 20
                                    nn.Conv2d(32, 64, 3, padding=1), nn.ReLU()) # 64, 15, 20
        self.linear1 = nn.Sequential(nn.Linear(64 * 15 * 20, 100), nn.ReLU())
        self.linear2 = nn.Sequential(nn.Linear(100, 1), nn.Sigmoid())
        
    def forward(self, data):
        for j in range(2):
            x = self.convnet(data[:, j, :, :])
            x = x.view(-1, 64 * 15 * 20)
            res.append(self.linear1(x))
        fres = res[0].sub(res[1]).pow(2)
        return self.linear2(fres)

Ensuite, pour être cohérent avec l'entraînement, vous devez utiliser une crossentropie binaire:

criterion_fn = torch.nn.BCELoss()

Et n'oubliez pas de définir les étiquettes sur 1 lorsque les deux images d'entrée appartiennent à la même classe.

Aussi, je vous recommande d'utiliser un peu de décrochage, environ 30% de probabilité de faire tomber un neurone, après le linear1 couche.

1
Guillem