Tensorflow-Keras Neural Network
Tensorflow è un framework utile per costruire un Neural Network che ha due fondamentali componenti, i Tensor e la Computational Graph. Il Tensor è una rappresentazione particolare di un array di n dimensiomi e la dimensione di un array viene indicata con il termine rank.
Tensorflow ha diverse API in diversi livelli come si evince dallo schema sottostante :
HIGH LEVEL Estimator
Keras
TF-Learn
TF-Slim


MID LEVEL Layers
Datasets
Metrics
Losses


LOW LEVEL TF-Session
TF-graph


La differenza tra un tensor ed un array classico come ad esempio con numpy sono che un tensor può essere applicato sia alle GPU che alle TPU con maggiore velocità di calcolo, possono automaticamente calcolare i gradients e possono essere applicati anche su più elaboratori contemporaneamente.

tensor = tf.constant([[5, 25], [3, 7]])
print(tensor)
tf.Tensor([[ 5 25] [ 3 7]], shape=(2, 2), dtype=int32)
matrice = tensor.numpy()
print(matrice)
[[ 5 25] [ 3 7]]

La Computational Graph è un metodo che rappresenta un processo in steps, un data flow graph in cui si inseriscono le singole operazioni.

I Low level sono tf.Session e tf.Graph.
I Mid Level sono tf.layer per i Layer, tf.Data per i Datasets, tf.metrics per i Metrics e tf.losses per i Loss packages.
I Layers packages ci forniscono tutta una serie di strumenti per costruire i Neural Network e successivamente ne vedremo alcuni esempi.
I Datasets packages forniscono degli strumenti per acquisire e preparare i files in input per i NN.
Per High level abbiamo strumenti che ci permettono di creare NN in modo astratto senza dover gestire tutti i componenti necessari.
Negli Estimator esistono diversi modelli già pronti da utilizzare per la Linear Regression, Random Forest Deep Neural Network sia per regression che classification. Un esempio è tf.compat.v1.estimator.DNNClassifier in cui inserendo i dati e altri parametri si ottiene un modello di classificazione.

è una high level API di tensorflow per creare modelli di NN con tutti i componenti.
Sequential model è una modalità di creare un modello aggiungendo i layer dove ognuno ha un input ed un output come nell'esempio seguente:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Conv2D, Dropout, Flatten, MaxPooling2D
# Creazione Sequential Model con layers
model = Sequential()
model.add(Conv2D(28, kernel_size=(3,3), input_shape=input_shape))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten()) # Flattening the 2D arrays for fully connected layers
model.add(Dense(128, activation=tf.nn.relu))
model.add(Dropout(0.2))
#model.add(linear)
model.add(Dense(10,activation=tf.nn.softmax))

model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.fit(x=x_train,y=y_train, epochs=2)

# estrarre i dati di input o di output nei diversi layer
estrattore = Model(inputs=model.inputs, outputs=[layer.output for layer in model.layers])
dati = estrattore(x_train)
dati
tf.Tensor: shape=(60000, 10), dtype=float32, numpy= array([[2.8488181e-10, 5.4973871e-08, 1.5923844e-08, ..., 4.6386925e-09, 1.6970350e-08, 6.8226524e-07], .... 5.9996935e-04]], dtype=float32)

è un'altra modalità utile per creare modelli in modo flessibile per inserire layer con diversi input o output.

from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Conv2D, Dropout, Flatten, MaxPooling2D, Input

inputs = Input(input_shape)
l1 = Conv2D(28, kernel_size=(3,3)) (inputs)
l2 = MaxPooling2D(pool_size=(2, 2)) (l1)
l3 = Flatten() (l2)
l4 = Dense(128, activation=tf.nn.relu) (l3)
l5 = Dropout(0.2) (l4)
l6 = Dense(64, activation=tf.nn.relu) (l5)
output = Dense(10,activation='softmax')(l6)
model = Model(inputs=inputs, outputs=output)

model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x=x_train,y=y_train, epochs=2)

model.evaluate(x_test, y_test)


model.summary() # per avere i dati del modello
keras.utils.plot_model(model, show_shapes=True) # per avere il grafico del modello
model.save("path/nome_modello") # save modello
del model # delete del modello dalla memoria
model = keras.models.load_model("path/nome_modello") # load modello salvato precedentemente



Con Keras possiamo costruirci dei modelli in base alle nostre necessità inserendo i layer forniti da Tensorflow o costruendono dei propri e utilizzanto gli optimizer, loss, metrics e le altre utilities.
Vediamo un esempio di un modello di CNN come i precedenti ma creato in modo custom inserendo un ciclo per esegure le diverse epoch senza utilizzare i metofi compile e fit. In questo modello inseriamo anche un altra utility, kerastuner, per scegliere gli hyperparameter in modo automatico

import tensorflow as tf
from tensorflow import keras
import numpy as np
import kerastuner as kt
from tensorflow.keras.layers import Dense, Flatten, Conv2D,MaxPooling2D,Dropout
from tensorflow.keras import Model

hp = kt.HyperParameters()

EPOCHS = 10
BATCH_SIZE= 32
# questa utility raggruppa in modo randum in gruppi in base a bacth_size
train = tf.data.Dataset.from_tensor_slices((x_train, y_train)).shuffle(10000).batch(BATCH_SIZE)
test = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(BATCH_SIZE)
test
BatchDataset shapes: ((None, 28, 28, 1), (None,)), types: (tf.float32, tf.uint8)

class Qcnn(Model):
def __init__(self):
super(Qcnn, self).__init__()
self.l1 = Conv2D(filters=hp.Int('units', min_value=32, max_value=512, step=32) ,kernel_size=(3,3), activation='relu')
#self.l1 = Conv2D(28, kernel_size=(3,3), activation='relu')
self.l2 = MaxPooling2D(pool_size=(2, 2))
self.l3 = Flatten()
self.l4 = Dense(256, activation=tf.nn.relu)
self.l5 = Dropout(0.2)
self.l6 = Dense(10, activation='softmax')
def call(self, x):
x = self.l1(x)
x = self.l2(x)
x = self.l3(x)
x = self.l4(x)
x = self.l5(x)
return self.l6(x)


model = Qcnn():

trainLoss = tf.keras.metrics.Mean(name='trainLoss')
trainAccuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='trainAccuracy')
testLoss = tf.keras.metrics.Mean(name='testLoss'):
testAccuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='testAccuracy')
lossF = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False):
optimizer = tf.keras.optimizers.Adam(hp.Choice('learning_rate', values=[1e-2, 1e-3, 1e-4]))

@tf.function #attiva tensorflow graf
def trainModel(x, y):
with tf.GradientTape() as gra:
# training=True Dropout
y_pred = model(x, training=True)
loss = lossF(y, y_pred)
gradients = gra.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
trainLoss(loss)
trainAccuracy(y, y_pred)

@tf.function
def testModel(x, y):
# training=False se Dropout
y_pred = model(x, training=False)
loss = lossF(y, y_pred)
testLoss(loss)
testAccuracy(y, y_pred)

for epoch in range(EPOCHS)::
# il metodo reset_states azzera i contatori ad ogni epoch
trainLoss.reset_states()
trainLoss.reset_states()
trainAccuracy.reset_states()
testLoss.reset_states()
testAccuracy.reset_states()
for x, y in train: # passa un blocco(batch_size) alla volta
x = tf.constant(x)
y = tf.constant(y)
trainModel(x, y)
for x, y in test:
testModel(x, y)
print(f'Epoch {epoch + 1}, ' f'Train Loss: {trainLoss.result():3.2f}, ' f'Train Accuracy: {trainAccuracy.result() * 100 :3.2f}, ' f'Test Loss: {testLoss.result() :3.2f}, ' f'Test Accuracy: {testAccuracy.result() * 100 :3.2f} ' )

Epoch 20, Train Loss: 0.07, Train Accuracy: 99.41, Test Loss: 0.53, Test Accuracy: 98.39



Vediamo ora come creare dei custom LAYER per ottenere la funzione y = w*x + b w sono i weights e b sono i bias per inserendo x in input il layer restituisce y in output che poi sarà l'input per il layer successivo.
I valori di w vengono inizializzati con numeri random mentre b con tutti zero.

class myLayer(layers.Layer):
def __init__(self, batch_size=64, units=10,**kwargs):
super(myLayer(, self).__init__()
w_init = tf.random_normal_initializer()
self.w = tf.Variable(initial_value=w_init(shape=(input_dim, units),
dtype='float32'),
trainable=True)
b_init = tf.zeros_initializer()
self.b = tf.Variable(initial_value=b_init(shape=(units,),
dtype='float32'),
def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b


myLay = myLayer()

ilayer = myLay(inp)
ilayer2 = myLay(ilayer)
...


I gradient si usano per effettuare la backpropagation per ottenere la migliore y = w*x + con il modello che si vuole creare. è una tecnica per valutare la derivate di una funzione nella computaniotal graph.
Tensorflow ci fornisce una funzione che utilizzata con il comando with di python ci permette di effettuare i calcoli in modo automatico in un context manager e decidere quali variabili(tensor) possono essere visti ed elaborati dal gradient.
x = tf.Variable([3.0, 2.0, 5.0])
a = tf.Variable([1.0,2.0])

with tf.GradientTape(persistent=True,watch_accessed_variables=False ) as tape:
tape.watch(x)
y = x * x
z = y * y
print('x = ', x)
print('y = ', y)
z = y * y
q = a * a
print('z = ', z)
print('q = ', q)

derdzdx = tape.gradient(z, x) # target e source in input # si calcola (4*x^3 at x = ?)
derdydx = tape.gradient(y, x) # target e source in input # si calcola x*2 at x = ?)
derdqdx = tape.gradient(q, x)

print(`dz/dx` = derdzdx)

print(`dy/dx` = derdydx)

print(`(dq)/dx` = derdqdx)


RISULTATI:
x = tf.Variable 'Variable:0' shape=(3,) dtype=float32, numpy=array([3., 2., 5.], dtype=float32)
y = tf.Tensor([ 9. 4. 25.], shape=(3,), dtype=float32)
z = tf.Tensor([ 81. 16. 625.], shape=(3,), dtype=float32)
q = tf.Tensor([1. 0.], shape=(2,), dtype=float32)
`dz/dx` = tf.Tensor([108. 32. 500.], shape=(3,), dtype=float32)

`dy/dx` = tf.Tensor([ 6. 4. 10.], shape=(3,), dtype=float32)

`(dq)/dx` = None

Sopra nell'esempio inserito possiamo notare le peculiarità di questa funzione che con i parametri inseriti (persistent=True) mantiene copia dei calcoli effettuati e (watch_accessed_variables=False) non accede prende in considerazione le variabile se non espressamente previsto con il metodo watch() per il calcolo dei gradients come possiamo notare dal valore della variabile `(dq)/dx` che dopo il calcolo del gradient il suo valore è uguale a None.






è una intefaccia grafica interattiva che permette di vedere la creazione ed esecuzione della Computational graph.


è un programming environment dove le operazioni vengono valutate subito senza costruire la computational graph ritornando subito il risultato e successivamente verrà creato il graph. Questa modalità è operativa di default ma può essere disattivata con il comando tf.compat.v1.disable_eager_execution() ad esempio per eseguire operazioni all'interno della session. Per vedere lo stato il comando tf.executing_eagerly() return True se attiva.


dati strutturati con dei set di oggetti di operazioni tf.Operation e tf.Tensor(dati) visualizzabili tramite Tensordoard in grafici dove visualizzare il tracciato logico delle operazioni eseguite sui dati inseriti. Questi dati possono essere salvati e rieseguiti senza il codice python originale ad esmpio su smartphone ... o backend server.


questa property attiva nella funzione a cui viene applicata la tensorflow graf cioè alla funzione si crea una computational graph accelerando l'esecuzione.
I dati inseriti in input alla funzione devono essere passati con con tf.Constant o tf.Variable.

Vediamo alcuni comandi che si usano nei tensor:

tf.constant() - per creare costanti
tf.placeholder() - per il passaggio dei dati nella computational graph
tf.variable() - per inserire dati che poi verranno modificati successivamente


t1 = tf.constant(8)
t2 = tf.constant(4)

tf.Tensor(8, shape=(), dtype=int32)
tf.Tensor(4, shape=(), dtype=int32)

uno = tf.divide(t1,t2)
due = tf.subtract(t1,t2)
tre = tf.multiply(t1,t2)
quattro = tf.add(t1,t2)

with tf.compat.v1.Session() as sess:
r1 = uno.numpy()
r2 = due.numpy()
r3 = tre.numpy()
r4 = quattro.numpy()
result = [r1, r2,r3,r4]


tenres = tf.convert_to_tensor(result)
print(tenres)

tf.Tensor([ 2. 4. 32. 12.], shape=(4,), dtype=float64)

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() #load mnist dataset
x_train, x_test = x_train / 255.0, x_test / 255.0 #normalizzazione - riduzione valore numeri

# Add a channels dimension - 1 nuova dimensione
x_train = x_train[..., tf.newaxis].astype("float32")
x_test = x_test[..., tf.newaxis].astype("float32")
# shuffle e prepare the dataset in bach
train = tf.data.Dataset.from_tensor_slices(
(x_train, y_train)).shuffle(10000).batch(64)

test = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(32)


with tf.compat.v1.Session() as sess:
tf.compat.v1.disable_eager_execution() #disable eager
a = tf.compat.v1.placeholder(dtype=tf.float32, shape=None)
b = tf.compat.v1.placeholder(dtype=tf.float32, shape=None)

c = tf.sqrt(tf.pow(a,2) + tf.pow(b,2)) #pow = a^2 + b^2 = radice quadrata c^2
risultato = sess.run(c, feed_dict={a: 2, b: 5}) # feed_dict inserisce dati nel placeholder
print(risultato)

risultato = 5.3851647




tf.compat.v1.reset_default_graph()
a = tf.constant(10)
b = tf.constant(5)
c = tf.add(a,b)
print(c)

Tensor("mul:0", shape=(), dtype=int32)

with tf.compat.v1.Session() as sess:
print(c.eval())

50



una peculiarità è che i dati inseriti nelle variabili possono essere visti e modificati
in processi appartenenti a diverse sessioni.
- creare tf.Variable() o tf.get_variable()
- assegnare un nuovo valore tf.asssign()
- tf.compat.v1.assign_add() - aggiungere dati
- tf.compat.v1.assign_sub() - sottrarre dati
prima di manipolare una variabile in una session deve essere inizializzata con i comandi tf.compat.v1.global_variables_initializer()
per tutte le variabili o applicando il metodo .initialized().

v1 = tf.Variable(2, dtype=tf.float32)
v2 = tf.Variable(3, dtype=tf.float32)
tf.compat.v1.global_variables_initializer()

v3 = tf.compat.v1.assign_add(v1, v2)
tf.compat.v1.assign_sub(v3, v2) # sottrae v2 da v3 ed aggiorna il valore di v3


impedisce di usare variabili con lo stesso nome e per rieseguire comandi prima bisogna eseguire il comando tf.compat.v1.reset_default_graph()

tf.compat.v1.variable_scope
def fx(a, b):
a = tf.compat.v1.get_variable(name = "a", initializer = tf.constant(a, dtype=tf.float32))
b = tf.compat.v1.get_variable(name = "b", initializer = tf.constant(b, dtype=tf.float32))
return tf.add(a,b)

with tf.compat.v1.variable_scope("x1")
c = fx(3, 4)
print(c)

with tf.compat.v1.variable_scope("x2")
c = fx(3, 4) - 5
print(c)

tf.Tensor(7.0, shape=(), dtype=float32)
tf.Tensor(2.0, shape=(), dtype=float32)

I weights di un neural network vengono inseriti in variabili con un valore iniziale
iv = tf.random.normal(shape=(2, 2))
a = tf.Variable(iv)
print(a)

tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy= array([[ 0.22797322, -0.35832703], [ 0.24170108, 0.86340433]], dtype=float32)



tf.one_hot per OneHotEncoder - convertire in numeri valori alfanumerici
with tf.device('/gpu:0') per selezionare gpu/cpu/tpu e/o numero device


Tensor automaticamente fa retrieve dei gradients di alcune differentiable expression.
with tf.GradientTape() as tape:
tape.watch(x)

per vedere contenuto del gradient.

A Layer encapsulates a state (weights) and some computation (defined in the call method).
definiamo a layer e lo istanziamo
model.summary()
model.save()
history = model.fit()
history.history()
Using callbacks for checkpointing. save model.


inputs = tf.keras.preprocessing.sequence.pad_sequences(
input, padding="post"

)


- parti riempite da padding non considerate

1 Add a keras.layers.Masking layer or configure a keras.layers.Embedding layer with mask_zero=True.
2 embedding = layers.Embedding(input_dim=100, output_dim=30, mask_zero=True)
masked = embedding(inputs)

print(masked._keras_mask)

3 Passing mask tensors directly to layers custom

a = model.layers[5]
a.output
a.variables
b = a.kernel
c = b.numpy()
c.shape
c.argmax()
a.weights contiene kernel e bias

from tensorflow.keras import layers

class Linear(layers.Layer):

def __init__(self, units=32, input_dim=32):
super(Linear, self).__init__()

def call(self, inputs):
print("execute executing_eagerly =", tf.executing_eagerly())
print(1, inputs)
a = inputs.numpy() # ok
print(a, inputs.shape, 20)

return

se input :
x = tf.ones((1, 2) #ok

se x_train numpy non ok tf.executing_eagerly() = false