Fazendo uma rede neural: como não quebrar o cérebro

Olá Habr!

Neste breve artigo, falarei sobre duas armadilhas fáceis de colidir e fáceis de resolver.

Será sobre a criação de uma rede neural trivial em Keras, com a qual preveremos a média aritmética de dois números.

Parece que poderia ser mais fácil. E realmente, nada complicado, mas existem nuances.

Para quem o tópico é interessante, seja bem-vindo, não haverá descrições longas e chatas, apenas um código curto e comentários sobre ele.

A solução é mais ou menos assim:

import numpy as np from keras.layers import Input, Dense, Lambda from keras.models import Model import keras.backend as K #   def train_iterator(batch_size=64): 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 x_mean_ex = np.expand_dims(x_mean, -1) yield [x], [x_mean_ex] #  def create_model(): x = Input(name = 'x', shape=(2,)) x_mean = Dense(1)(x) model = Model(inputs=x, outputs=x_mean) return model #    model = create_model() model.compile(loss=['mse'], optimizer = 'rmsprop') model.fit_generator(train_iterator(), steps_per_epoch = 1000, epochs = 100, verbose = 1) #  x, x_mean = next(train_iterator(1)) print(x, x_mean, model.predict(x)) 

Tentando aprender ... mas nada vem disso. E aqui neste lugar você pode organizar danças com um pandeiro e perder muito tempo.

 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 foi previsto, o que está longe de ser 65.

Mas assim que refazemos um pouco o gerador, tudo começa a funcionar imediatamente.

 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] 

E é claro que já literalmente na terceira era a rede está convergindo.

 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 

A principal diferença é que, no primeiro caso, o objeto x_mean é criado na memória a cada vez e, no segundo caso, aparece quando o gerador é criado e, em seguida, é reutilizado apenas.

Entendemos ainda se tudo está correto neste gerador. Acontece que não realmente.
O exemplo a seguir mostra que algo está errado.
 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.]))

O valor médio na primeira chamada do iterador não coincide com os números com base nos quais é calculado. De fato, o valor médio foi calculado corretamente, mas porque a matriz foi passada por referência, na segunda vez em que o iterador foi chamado, os valores na matriz foram substituídos e a função print () retornou o que estava na matriz e não o que esperávamos.

Existem duas maneiras de corrigir isso. Ambos são caros, mas corretos.
1. Mova a criação da variável x dentro do loop while, para que uma nova matriz seja criada a cada rendimento.
 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. Retorne uma cópia da matriz.
 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]))


Agora está tudo bem. Vá em frente.

Expand_dims precisa ser feito? Vamos tentar remover esta linha e o novo código será assim:

 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] 

Tudo aprende bem, embora os dados retornados tenham uma forma diferente.

Por exemplo, havia [[49.]], e tornou-se [49.], mas dentro de Keras isso, aparentemente, é corretamente reduzido à dimensão desejada.

Portanto, sabemos como deve ser o gerador de dados correto, agora vamos brincar com a função lambda e observar o comportamento expand_dims lá.

Não preveremos nada, apenas consideraremos o valor correto dentro do lambda.

O código é o seguinte:

 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 

Começamos e vemos que está tudo bem:

 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 

Agora vamos tentar modificar ligeiramente nossa função lambda e remover expand_dims.

 def calc_mean(x): res = (x[::,0] + x[::,1]) / 2 return res 

Ao compilar o modelo, nenhum erro na dimensão apareceu, mas o resultado já é diferente, a perda é considerada incompreensível. Portanto, aqui o expand_dims precisa ser feito, nada acontecerá automaticamente.

 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 

E se você observar o resultado retornado predict (), poderá ver que a dimensão está incorreta, a saída é [46.] e o esperado [[46.]].

Algo assim. Obrigado a todos que leram. E tenha cuidado nos detalhes, o efeito deles pode ser significativo.

Source: https://habr.com/ru/post/pt439038/


All Articles