Member preview

Chatbots, chatbots, chatbots… Parte 4

A Deep dive into Tensorflow

Nelle storie precedenti, abbiamo raccolto e lucidato il nostro bel dataset pronto per Tensorflow e finalmente è arrivato il momento di scrivere il nostro modello di Machine Learning.

Ok, come abbiamo visto la nostra rete seq2seq, a livello teorico, sara composta da un encoder ed un decoder formati da una serie di celle LSTM tenute insiene da un layer di RNN e con un layer di Softmax alla fine.

In un’altra maniera schematica:

Ora vediamo cosa significa davvero in pratica.

Innanzitutto in questa immagine, per quanto estremamente schematica, abbiamo una informazione molto importante. Il nostro modello ha delle frecce che danno in input la nostra frase alla rete e delle frecce che ci permettono di ricevere in output una risposta.

Per quanto questo possa sembrare un espediente descrittivo in verità ci dice che non possiamo costruire una rete neurale senza occuparci anche di come scambiamo le informazioni con la nostra rete.

In altre parole, Tensorflow non ci permetterà di far funzionare una rete che non è in grado di comunicare con noi.

Andiamo quindi con ordine e dichiariamo a Tensorflow i nostri input in modo che lui li possa digerire:

#placeholders
enc_inputs = tf.placeholder(tf.int32, shape=(None, batch_size), name="enc_inputs")
targets = tf.placeholder(tf.int32, shape=(None, batch_size), name="targets")
dec_inputs = tf.placeholder(tf.int32, shape=(None, batch_size), name="dec_inputs")

Usiamo tre tf placeholders; sono sostanzialmente tre variabili vuote, modificabili dagli umani e pre-allocate, che tf si aspetta siano riempite dai nostri dati.

Creiamo i layer che incorporeranno i pesi iniziali associati agli input. Questa volta usiamo variabili ‘private’ gestite direttamente da tf ed associate ai due input con funzione di embedding_lookup.

#input embedding layers
emb_weights = tf.Variable(tf.truncated_normal([vocab_size, embedding_size], stddev=truncated_std), name="emb_weights")
enc_inputs_emb = tf.nn.embedding_lookup(emb_weights, enc_inputs, name="enc_inputs_emb")
dec_inputs_emb = tf.nn.embedding_lookup(emb_weights, dec_inputs, name="dec_inputs_emb")

Possiamo dargli degli input ora.

Entriamo nel core della nostra rete e iniziamo a definire effettivamente le singole celle che andranno a comporla, anche qui tf ci viene in aiuto con una implementazione delle celle LSTM.

Creiamo due semplici array dove ogni elemento sara’ una cella LSTM e la sua funzione di dropout, e le uniamo insieme in un due MultiRNNWrapper per formare i nostri layer.

#cell definiton
enc_cell_list=[]
dec_cell_list=[]
for i in xrange(num_layers):
single_cell = tf.contrib.rnn.LSTMCell(
num_units=hidden_size,
num_proj=projection_size,
state_is_tuple=True
)
if i < num_layers-1 or num_layers == 1:
single_cell = tf.contrib.rnn.DropoutWrapper(cell=single_cell, output_keep_prob=keep_prob)
enc_cell_list.append(single_cell)
for i in xrange(num_layers):
single_cell = tf.contrib.rnn.LSTMCell(
num_units=hidden_size,
num_proj=projection_size,
state_is_tuple=True
)
if i < num_layers-1 or num_layers == 1:
single_cell = tf.contrib.rnn.DropoutWrapper(cell=single_cell, output_keep_prob=keep_prob)
dec_cell_list.append(single_cell)
enc_cell = tf.contrib.rnn.MultiRNNCell(cells=enc_cell_list, state_is_tuple=True)
dec_cell = tf.contrib.rnn.MultiRNNCell(cells=dec_cell_list, state_is_tuple=True)

Definiamo finalmente i nostri encoder e decoder con i loro relativi stati.

#encoder & decoder defintion
_, enc_states = tf.nn.dynamic_rnn(cell = enc_cell,
inputs = enc_inputs_emb,
dtype = tf.float32,
time_major = True,
scope="encoder")
dec_outputs, dec_states = tf.nn.dynamic_rnn(cell = dec_cell, 
inputs = dec_inputs_emb,
initial_state = enc_states,
dtype = tf.float32,
time_major = True,
scope="decoder")

Bellissimo, abbiamo la nostra rete ora, possiamo parlarci, ma non capire quello che dice…

Ok ok, quello che ci serve allora è aggiungervi un Output layers e Softmax.

#output layers
project_w = tf.Variable(tf.truncated_normal(shape=[output_size, embedding_size], stddev=truncated_std), name="project_w")
project_b = tf.Variable(tf.constant(shape=[embedding_size], value = 0.1), name="project_b")
softmax_w = tf.Variable(tf.truncated_normal(shape=[embedding_size, vocab_size], stddev=truncated_std), name="softmax_w")
softmax_b = tf.Variable(tf.constant(shape=[vocab_size], value = 0.1), name="softmax_b")
dec_outputs = tf.reshape(dec_outputs, [-1, output_size], name="dec_ouputs")
dec_proj = tf.matmul(dec_outputs, project_w) + project_b
logits = tf.nn.log_softmax(tf.matmul(dec_proj, softmax_w) + softmax_b, name="logits")

Semplice no?

Ok, c’è bisogno di un attimo di spiegazione qui.

Quello che fa la nostra rete, ed ogni rete neurale per essere precisi, è in sostanza fare delle ipotesi plausibili in base a quello che ha imparato durante il training.

In parole povere, ci dice quali sono le probabilità che degli output, in questo caso parole, vadano bene con degli input.

Se ci riflettete, è sostanzialmente quello che facciamo anche noi ad ogni domanda o situazione, cerchiamo, in base all'esperienza, la nostra migliore ipotesi su quella che dovrebbe essere la risposta/reazione giusta.

A noi interessa solo l’ipotesi più probabile, quindi quello che facciamo è prendere pesi e biases dal nostro decoder, ‘comprimerle’ in un layer di softmax e…

Prendere quelle con probabilità più’ alta:

logit = logits[-1]
top_values, top_indexs = tf.nn.top_k(logit, k = 10, sorted=True)

E il gioco è fatto.

Manca qualcosa…

Innanzitutto, cosa diavolo è tutta quella roba come batch_size, hidden_size, projection_size, num_layers sparsa in giro?!?!?!

Se li avete notati, complimenti, il sussurro delle macchine è forte in voi. Quelli sono i sacri HyperParameters. La salsa segreta nella nostra rete neurale, il punto G del nostro training. Vedremo in un articolo dedicato come trovarli.

Ma, per il momento, quello che abbiamo appena costruito è il nostro cervello, la nostra materia grigia, potente e pronta ad imparare.

Nei prossimi articoli vedremo come collegarlo alla spina dorsale e al resto del corpo. Stay tuned!