1
votes

Erreurs numériques dans Keras vs Numpy

Afin de vraiment comprendre les couches convolutives, j'ai réimplémenté la méthode forward d'une seule couche keras Conv2D dans numpy de base. Les sorties des deux coutures sont presque identiques, mais il y a quelques différences mineures.

Obtention de la sortie keras:

Mine: 2.6608338356018066
Keras: 2.660834312438965

Mine: 1.7892705202102661
Keras: 1.7892701625823975

Mine: 0.007190803997218609
Keras: 0.007190565578639507

Mine: 4.970898151397705
Keras: 4.970897197723389

Ma sortie:

def relu(x):
    return np.maximum(0, x)


def forward(inp, filter_weights, filter_biases):

    result = np.zeros((1, 64, 64, 32))

    inp_with_padding = np.zeros((1, 66, 66, 1))
    inp_with_padding[0, 1:65, 1:65, :] = inp

    for filter_num in range(32):
        single_filter_weights = filter_weights[:, :, 0, filter_num]

        for i in range(64):
            for j in range(64):
                prod = single_filter_weights * inp_with_padding[0, i:i+3, j:j+3, 0]
                filter_sum = np.sum(prod) + filter_biases[filter_num]
                result[0, i, j, filter_num] = relu(filter_sum)
    return result


my_output = forward(test_x, filter_weights, biases_weights)


2 commentaires

Utilisez-vous padding = 'same' dans Keras?


Oui je le suis, exactement!


3 Réponses :


2
votes

Compte tenu de l'importance des différences, je dirais qu'il s'agit d'erreurs d'arrondi.
Je recommande d'utiliser np.isclose (ou math.isclose ) pour vérifier si les flottants sont "égaux". < / p>


1 commentaires

Oui tres vrai! Cependant, je suis très intéressé à comprendre exactement pourquoi cela se produit, ce qui se passe à l'intérieur de keras et à faire en sorte que les sorties de mes fonctions correspondent parfaitement aux sorties de keras.



1
votes

La première chose à faire est de vérifier si vous utilisez padding = 'same' . Vous semblez utiliser le même remplissage dans votre implémentation.

Si vous utilisez d'autres types de remplissage, y compris la valeur par défaut qui est padding = 'valid' , il y aura une différence.

Une autre possibilité est que vous accumuliez des erreurs à cause de la triple boucle de petites sommes.

Vous pouvez le faire tout de suite et voir si cela devient différent. Comparez cette implémentation avec la vôtre, par exemple:

def forward3(inp, filter_weights, filter_biases):

    inShape = inp.shape           #(batch, imgX, imgY, ins)
    wShape = filter_weights.shape #(wx, wy, ins, out)
    bShape = filter_biases.shape  #(out,)

    ins = inShape[-1]
    out = wShape[-1]

    wx = wShape[0]
    wy = wShape[1]

    imgX = inShape[1]
    imgY = inShape[2]

    assert imgX >= wx
    assert imgY >= wy

    assert inShape[-1] == wShape[-2]
    assert bShape[-1] == wShape[-1]


    #you may need to invert this padding, exchange L with R
    loseX = wx - 1
    padXL = loseX // 2
    padXR = padXL + (1 if loseX % 2 > 0 else 0)

    loseY = wy - 1
    padYL = loseY // 2
    padYR = padYL + (1 if loseY % 2 > 0 else 0)

    padded_input = np.pad(inp, ((0,0), (padXL,padXR), (padYL,padYR), (0,0)))
        #(batch, paddedX, paddedY, in)


    stacked_input = np.stack([padded_input[:, i:imgX + i] for i in range(wx)],
                             axis=1) #(batch, wx, imgX, imgY, in)

    stacked_input = np.stack([stacked_input[:,:,:,i:imgY + i] for i in range(wy)],
                             axis=2) #(batch, wx, wy, imgX, imgY, in)

    stacked_input = stacked_input.reshape((-1, wx, wy, imgX, imgY, ins,   1))
    w =            filter_weights.reshape(( 1, wx, wy,    1,    1, ins, out))
    b =             filter_biases.reshape(( 1,   1,  1, out))

    result = stacked_input * w
    result = result.sum(axis=(1,2,-2))
    result += b

    result = relu(result)

    return result

Une troisième possibilité est de vérifier si vous utilisez le GPU et de tout passer au CPU pour le test. Certains algorithmes pour GPU sont même non déterministes.


Pour toute taille de noyau:

def forward2(inp, filter_weights, filter_biases):

    #inp: (batch, 64, 64, in)
    #w: (3, 3, in, out)
    #b: (out,)

    padded_input = np.pad(inp, ((0,0), (1,1), (1,1), (0,0))) #(batch, 66, 66, in)
    stacked_input = np.stack([
        padded_input[:,  :-2], 
        padded_input[:, 1:-1],
        padded_input[:, 2:  ]], axis=1) #(batch, 3, 64, 64, in)

    stacked_input = np.stack([
        stacked_input[:, :, :,  :-2],
        stacked_input[:, :, :, 1:-1],
        stacked_input[:, :, :, 2:  ]], axis=2) #(batch, 3, 3, 64, 64, in)


    stacked_input = stacked_input.reshape((-1, 3, 3, 64, 64, 1,   1))
    w =            filter_weights.reshape(( 1, 3, 3,  1,  1, 1, 32))
    b =            filter_biases.reshape (( 1, 1, 1, 32))


    result = stacked_input * w #(-1, 3, 3, 64, 64, 1, 32)
    result = result.sum(axis=(1,2,-2)) #(-1, 64, 64, 32)
    result += b

    result = relu(result)

    return result


4 commentaires

Merci d'avoir répondu! Le rembourrage est en effet "même", donc tout va bien là-bas. Pour être honnête, je ne comprends pas vraiment votre code. Quelle est l'entrée empilée? Pouvons-nous exprimer une convolution comme une simple multiplication sous une forme aussi simple? En outre, cette partie du code est collée deux fois différemment.


Bon conseil sur le GPU, mais les deux codes s'exécutent sur le CPU :)


@EliteKaffee La deuxième pâte doit utiliser stacked_input désolé. Fixé. Regardez les formes après chaque pile pour voir ce qu'elles font.


J'ai corrigé mes codes, les erreurs sont presque les mêmes que les vôtres. Il y a cependant un gros gain de vitesse avec ma mise en œuvre. Expliquant mieux, les entrées empilées répliquent les images avec glissement, créant une matrice kernel_size pour multiplier comme si le noyau glissait.



1
votes

Les opérations en virgule flottante ne sont pas commutables. Voici un exemple:

In [19]: 1.2 - 1.0 - 0.2
Out[19]: -5.551115123125783e-17

In [21]: 1.2 - 0.2 -  1.0
Out[21]: 0.0

Donc, si vous voulez des résultats complètement identiques, vous n'avez pas seulement besoin de faire les mêmes calculs analytiquement. Mais vous devez également les faire exactement dans le même ordre, avec les mêmes types de données et la même implémentation d'arrondi.

Pour déboguer cela. Commencez par le code Keras et changez-le ligne par ligne en fonction de votre code, jusqu'à ce que vous voyiez une différence.


0 commentaires