web-dev-qa-db-fra.com

Comment gérer une entrée de taille variable dans CNN avec Keras?

J'essaie d'effectuer la classification habituelle sur la base de données MNIST mais avec des chiffres recadrés au hasard. Les images sont rognées de la manière suivante: supprimées au hasard en premier/dernier et/ou ligne/colonne.

Je voudrais utiliser un réseau neuronal convolutif utilisant Keras (et le backend Tensorflow) pour effectuer la convolution puis la classification habituelle.

Les entrées sont de taille variable et je n'arrive pas à le faire fonctionner.

Voici comment j'ai recadré les chiffres

import numpy as np
from keras.utils import to_categorical
from sklearn.datasets import load_digits

digits = load_digits()

X = digits.images
X = np.expand_dims(X, axis=3)

X_crop = list()
for index in range(len(X)):
    X_crop.append(X[index, np.random.randint(0,2):np.random.randint(7,9), np.random.randint(0,2):np.random.randint(7,9), :])
X_crop = np.array(X_crop)

y = to_categorical(digits.target)

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X_crop, y, train_size=0.8, test_size=0.2)

Et voici l'architecture du modèle que je veux utiliser

from keras.layers import Dense, Dropout
from keras.layers.convolutional import Conv2D
from keras.models import Sequential

model = Sequential()

model.add(Conv2D(filters=10, 
                 kernel_size=(3,3), 
                 input_shape=(None, None, 1), 
                 data_format='channels_last'))

model.add(Dense(128, activation='relu'))
model.add(Dropout(0.2))

model.add(Dense(10, activation='softmax'))


model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])

model.summary()

model.fit(X_train, y_train, epochs=100, batch_size=16, validation_data=(X_test, y_test))
  1. Quelqu'un a-t-il une idée sur la façon de gérer les entrées de taille variable dans mon réseau de neurones?

  2. Et comment effectuer la classification?

15
Thomas Grsp

TL/DR - aller au point 4

Donc - avant d'en arriver au fait - réglons certains problèmes avec votre réseau:

  1. Votre réseau ne fonctionnera pas en raison de l'activation : avec categorical_crossentropy Vous devez avoir une activation softmax:

    model.add(Dense(10, activation='softmax'))
    
  2. Vectoriser les tenseurs spatiaux: comme Daniel l'a mentionné - vous devez, à un moment donné, basculer vos vecteurs de spatial (images) à vectorisé (vecteurs). Actuellement, appliquer Dense à la sortie d'une Conv2D Équivaut à (1, 1) Convolution. Donc, fondamentalement - la sortie de votre réseau est spatiale - pas vectorisée ce qui cause le décalage de dimensionnalité (vous pouvez le vérifier en exécutant votre réseau ou en vérifiant la model.summary(). Pour changer cela, vous devez utiliser soit GlobalMaxPooling2D ou GlobalAveragePooling2D . Par exemple:

    model.add(Conv2D(filters=10, 
                 kernel_size=(3, 3), 
                 input_shape=(None, None, 1),
                 padding="same",
                 data_format='channels_last'))
    model.add(GlobalMaxPooling2D())
    model.add(Dense(128, activation='relu'))
    model.add(Dropout(0.2))
    
    model.add(Dense(10, activation='softmax'))
    
  3. Les tableaux numpy concaténés doivent avoir la même forme: si vous vérifiez la forme de X_crop, Vous verrez que ce n'est pas une matrice spatiale. C'est parce que vous avez concaténé des matrices de formes différentes. Malheureusement - il est impossible de surmonter ce problème car numpy.array Doit avoir une forme fixe.

  4. Comment faire fonctionner votre réseau sur des exemples de formes différentes: La chose la plus importante à faire est de comprendre deux choses. Premièrement - est que dans un seul lot, chaque image doit avoir la même taille. Deuxièmement, le fait d'appeler fit plusieurs fois est une mauvaise idée, car vous réinitialisez les états du modèle interne. Voici donc ce qui doit être fait:

    une. Écrire une fonction qui recadre un lot unique - par exemple un get_cropped_batches_generator qui, étant donné une matrice, en découpe un lot et le recadre au hasard.

    b. Utilisez la méthode train_on_batch . Voici un exemple de code:

    from six import next
    
    batches_generator = get_cropped_batches_generator(X, batch_size=16)
    losses = list()
    for Epoch_nb in range(nb_of_epochs):
        Epoch_losses = list()
        for batch_nb in range(nb_of_batches):
            # cropped_x has a different shape for different batches (in general)
            cropped_x, cropped_y = next(batches_generator) 
            current_loss = model.train_on_batch(cropped_x, cropped_y)
            Epoch_losses.append(current_loss)
        losses.append(Epoch_losses.sum() / (1.0 * len(Epoch_losses))
    final_loss = losses.sum() / (1.0 * len(losses))
    

Donc - quelques commentaires pour coder ci-dessus: Premièrement, train_on_batch n'utilise pas la barre de progression Nice keras. Il renvoie une seule valeur de perte (pour un lot donné) - c'est pourquoi j'ai ajouté une logique pour calculer la perte. Vous pouvez également utiliser Progbar pour cela. Deuxièmement - vous devez implémenter get_cropped_batches_generator - Je n'ai pas écrit de code pour garder ma réponse un peu plus claire. Vous pourriez poser une autre question sur la façon de le mettre en œuvre. Dernière chose - j'utilise six pour garder la compatibilité entre Python 2 Et Python 3.

23
Marcin Możejko

Habituellement, un modèle contenant Dense couches ne peut pas avoir d'entrées de taille variable, sauf si les sorties sont également variables. Mais voyez la solution de contournement ainsi que l'autre réponse en utilisant GlobalMaxPooling2D - La solution de contournement est équivalente à GlobalAveragePooling2D. Ce sont des couches qui peuvent éliminer la taille variable avant une couche dense et supprimer les dimensions spatiales.

Pour un cas de classification d'images, vous souhaiterez peut-être redimensionner les images en dehors du modèle.

Lorsque mes images sont au format numpy, je les redimensionne comme ceci:

from PIL import Image
im = Image.fromarray(imgNumpy)
im = im.resize(newSize,Image.LANCZOS) #you can use options other than LANCZOS as well
imgNumpy = np.asarray(im)

Pourquoi?

Une couche convolutionnelle a ses poids comme filtres. Il existe une taille de filtre statique et le même filtre est appliqué à l'image encore et encore.

Mais une couche dense a ses poids en fonction de l'entrée. S'il y a 1 entrée, il y a un ensemble de poids. S'il y a 2 entrées, vous avez deux fois plus de poids. Mais les poids doivent être entraînés, et changer la quantité de poids changera certainement le résultat du modèle.

Comme l'a commenté @Marcin, ce que j'ai dit est vrai lorsque votre forme d'entrée pour les couches Denses a deux dimensions: (batchSize,inputFeatures).

Mais en réalité, les couches denses de keras peuvent accepter des entrées avec plus de dimensions. Ces dimensions supplémentaires (qui sortent des couches convolutives) peuvent varier en taille. Mais cela rendrait également la sortie de ces couches denses de taille variable.

Néanmoins, à la fin, vous aurez besoin d'une taille fixe pour la classification: 10 classes et c'est tout. Pour réduire les dimensions, les gens utilisent souvent des calques Flatten, et l'erreur apparaîtra ici.


Solution de contournement possible (non testée):

À la fin de la partie convolutionnelle du modèle, utilisez une couche lambda pour condenser toutes les valeurs dans un tenseur de taille fixe, en prenant probablement une moyenne des dimensions latérales et en gardant les canaux (les canaux ne sont pas variables)

Supposons que la dernière couche convolutionnelle soit:

model.add(Conv2D(filters,kernel_size,...))
#so its output shape is (None,None,None,filters) = (batchSize,side1,side2,filters)

Ajoutons une couche lambda pour condenser les dimensions spatiales et ne garder que la dimension des filtres:

import keras.backend as K

def collapseSides(x):

    axis=1 #if you're using the channels_last format (default)   
    axis=-1 #if you're using the channels_first format

    #x has shape (batchSize, side1, side2, filters)
    step1 = K.mean(x,axis=axis) #mean of side1
    return K.mean(step1,axis=axis) #mean of side2

    #this will result in a tensor shape of (batchSize,filters)

Puisque la quantité de filtres est fixe (vous avez supprimé les dimensions None), les couches denses devraient probablement fonctionner:

model.add(Lambda(collapseSides,output_shape=(filters,)))
model.add(Dense.......)
.....

Pour que cela fonctionne, je suggère que le nombre de filtres dans la dernière couche convolutionnelle soit d'au moins 10.

Avec cela, vous pouvez faire input_shape=(None,None,1)

Si vous faites cela, n'oubliez pas que vous ne pouvez transmettre que des données d'entrée avec une taille fixe par lot. Vous devez donc séparer toutes vos données en lots plus petits, chaque lot ayant des images toutes de la même taille. Voir ici: Keras interprète mal la forme des données d'entraînement

3
Daniel Möller