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
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.