Bonjour, Habr!
Dans ce court article, je vais vous parler de deux pièges qui sont à la fois faciles à heurter et faciles à contourner.
Il s'agira de créer un réseau neuronal trivial sur Keras, avec lequel nous prédirons la moyenne arithmétique de deux nombres.
Il semblerait que cela pourrait être plus facile. Et vraiment, rien de compliqué, mais il y a des nuances.
Pour qui le sujet est intéressant, bienvenue sous la coupe, il n'y aura pas de longues descriptions ennuyeuses, juste un code court et des commentaires à ce sujet.
La solution ressemble à ceci:
import numpy as np from keras.layers import Input, Dense, Lambda from keras.models import Model import keras.backend as K
Essayer d'apprendre ... mais rien n'en sort. Et ici, dans cet endroit, vous pouvez organiser des danses avec un tambourin et perdre beaucoup de temps.
Epoch 1/100 1000/1000 [==============================] - 2s 2ms/step - loss: 1044.0806 Epoch 2/100 1000/1000 [==============================] - 2s 2ms/step - loss: 713.5198 Epoch 3/100 1000/1000 [==============================] - 3s 3ms/step - loss: 708.1110 ... Epoch 98/100 1000/1000 [==============================] - 2s 2ms/step - loss: 415.0479 Epoch 99/100 1000/1000 [==============================] - 2s 2ms/step - loss: 416.6932 Epoch 100/100 1000/1000 [==============================] - 2s 2ms/step - loss: 417.2400 [array([[73., 57.]])] [array([[65.]])] [[49.650894]]
49 était prévu, ce qui est loin de 65.
Mais dès qu'on refait un peu le générateur, tout commence à fonctionner tout de suite.
def train_iterator_1(batch_size=64): x = np.zeros((batch_size, 2)) x_mean = np.zeros((batch_size,)) while True: for i in range(batch_size): x[i][0] = np.random.randint(0, 100) x[i][1] = np.random.randint(0, 100) x_mean[::] = (x[::,0] + x[::,1]) / 2 x_mean_ex = np.expand_dims(x_mean, -1) yield [x], [x_mean_ex]
Et il est clair que déjà à la troisième ère, le réseau converge.
Epoch 1/5 1000/1000 [==============================] - 2s 2ms/step - loss: 648.9184 Epoch 2/5 1000/1000 [==============================] - 2s 2ms/step - loss: 0.0177 Epoch 3/5 1000/1000 [==============================] - 2s 2ms/step - loss: 0.0030
La principale différence est que dans le premier cas, l'objet x_mean est créé en mémoire à chaque fois, et dans le second cas, il apparaît lorsque le générateur est créé et ensuite il est seulement réutilisé.
Nous comprenons davantage si tout est correct dans ce générateur. Il s'avère que pas vraiment.
L'exemple suivant montre que quelque chose ne va pas.
def train_iterator(batch_size=1): x = np.zeros((batch_size, 2)) while True: for i in range(batch_size): x[i][0] = np.random.randint(0, 100) x[i][1] = np.random.randint(0, 100) x_mean = (x[::,0] + x[::,1]) / 2 yield x, x_mean it = train_iterator() print(next(it), next(it))
(array([[44., 2.]]), array([10.])) (array([[44., 2.]]), array([23.]))
La valeur moyenne du premier appel d'itérateur ne coïncide pas avec les nombres sur la base desquels elle est calculée. En fait, la valeur moyenne a été calculée correctement, mais parce que le tableau a été transmis par référence, la deuxième fois que l'itérateur a été appelé, les valeurs du tableau ont été écrasées et la fonction print () a renvoyé ce qui était dans le tableau et non ce que nous attendions.
Il existe deux façons de résoudre ce problème. Les deux sont coûteux mais corrects.
1. Déplacez la création de la variable x à l'intérieur de la boucle while, de sorte qu'un nouveau tableau soit créé à chaque rendement.
def train_iterator_1(batch_size=1): while True: x = np.zeros((batch_size, 2)) for i in range(batch_size): x[i][0] = np.random.randint(0, 100) x[i][1] = np.random.randint(0, 100) x_mean = (x[::,0] + x[::,1]) / 2 yield x, x_mean it_1 = train_iterator_1() print(next(it_1), next(it_1))
(array([[82., 4.]]), array([43.])) (array([[77., 34.]]), array([55.5]))
2. Renvoyez une copie du tableau.
def train_iterator_2(batch_size=1): x = np.zeros((batch_size, 2)) while True: x = np.zeros((batch_size, 2)) for i in range(batch_size): x[i][0] = np.random.randint(0, 100) x[i][1] = np.random.randint(0, 100) x_mean = (x[::,0] + x[::,1]) / 2 yield np.copy(x), x_mean it_2 = train_iterator_2() print(next(it_2), next(it_2))
(array([[63., 31.]]), array([47.])) (array([[94., 25.]]), array([59.5]))
Maintenant, tout va bien. Allez-y.
Expand_dims doit-il être fait? Essayons de supprimer cette ligne et le nouveau code sera comme ceci:
def train_iterator(batch_size=64): while True: x = np.zeros((batch_size, 2)) for i in range(batch_size): x[i][0] = np.random.randint(0, 100) x[i][1] = np.random.randint(0, 100) x_mean = (x[::,0] + x[::,1]) / 2 yield [x], [x_mean]
Tout apprend bien, bien que les données renvoyées aient une forme différente.
Par exemple, il y avait [[49.]], et il est devenu [49.], mais à l'intérieur de Keras cela, apparemment, est correctement réduit à la dimension souhaitée.
Donc, nous savons à quoi devrait ressembler le bon générateur de données. Jouons maintenant avec la fonction lambda et examinons le comportement expand_dims.
Nous ne prédirons rien, nous considérons simplement la valeur correcte à l'intérieur de lambda.
Le code est le suivant:
def calc_mean(x): res = (x[::,0] + x[::,1]) / 2 res = K.expand_dims(res, -1) return res def create_model(): x = Input(name = 'x', shape=(2,)) x_mean = Lambda(lambda x: calc_mean(x), output_shape=(1,))(x) model = Model(inputs=x, outputs=x_mean) return model
Nous commençons et voyons que tout va bien:
Epoch 1/5 100/100 [==============================] - 0s 3ms/step - loss: 0.0000e+00 Epoch 2/5 100/100 [==============================] - 0s 2ms/step - loss: 0.0000e+00 Epoch 3/5 100/100 [==============================] - 0s 3ms/step - loss: 0.0000e+00
Essayons maintenant de modifier légèrement notre fonction lambda et de supprimer expand_dims.
def calc_mean(x): res = (x[::,0] + x[::,1]) / 2 return res
Lors de la compilation du modèle, aucune erreur sur la dimension n'est apparue, mais le résultat est déjà différent, la perte est considérée comme incompréhensible. Donc, ici expand_dims doit être fait, rien ne se passera automatiquement.
Epoch 1/5 100/100 [==============================] - 0s 3ms/step - loss: 871.6299 Epoch 2/5 100/100 [==============================] - 0s 3ms/step - loss: 830.2568 Epoch 3/5 100/100 [==============================] - 0s 2ms/step - loss: 830.8041
Et si vous regardez le résultat retourné Predict (), vous pouvez voir que la dimension est incorrecte, la sortie est [46.] et attendue [[46.]].
Quelque chose comme ça. Merci à tous ceux qui l'ont lu. Et soyez prudent dans les détails, leur effet peut être significatif.