<jupyter_start><jupyter_text>Impacto Profundo en el MLDeep Learning es un conjunto de técnicas de *Machine Learing* basadas en lo que se conoce como Redes Neuronales Artificiales. Algunas características del Deep Learning son:* Se basa en Redes Neuronales Artificiales con muchas capas.* Utiliza aprendizaje por gradiente.* Reducen el esfuerzo necesario para extraer características  de los datos originales.* Funciona mejor cuando mayor cantidad de datos hay disponibles. Aprendizaje con métodos basados en el gradienteEl método más común de entrenar redes neuronales son los métodos basados en gradiente. Consideremos el siguiente conjunto de datos generados, donde $x$ es la variable que conocemos e $y$ la que se quiere predecir.$$y=3 * x+(rand-0.5)$$<jupyter_code>%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt


def gen_random_data(mult):
    _x = np.linspace(-1, 1, 50)
    _error = (np.random.rand(*_x.shape) - .5)
    #TODO: Complete el valor de _y
    return _x, _y


x, y = gen_random_data(3)
#Utilice el método plot para crear un figura de X e Y. 'ro' permite mostrar los 
#puntos en rojo.
plt.show()<jupyter_output><empty_output><jupyter_text>ObjetivoConsiderando la varaible independiente $x$ y la variable dependiente $y$, el objetivo de un regresión lineal es encontrar $w$ y $b$ tal que dada una función de error $E(y, \hat{y})$ sea mínimo. Es decir:$$\underset{w,b}{arg\,min}=E(y,xw+b)$$ Función de errorUna función de error utilizada para este tipo de problemas es el error medio cuadrático (_mean squared error_), que se define como:$$MSE(y,\hat{y})=\frac{1}{N}\sum(y-\hat{y})^{2}$$<jupyter_code>#Función de predicción
def lineal(x, w, b):
    return None #TODO: Implemente la función lineal
  

plt.plot(x, y, 'ro', x, lineal(x, 3, 0))
plt.show()
#Función de error
def mse(y_true, y_pred):
    return None #TODO: Implemente el error cuadrado medio. Utilice la función np.average

print('El MSE es {}'.format(mse(y, lineal(x, 3, 0))))<jupyter_output><empty_output><jupyter_text>OptimizaciónEl problema en la regresión lineal es encontrar los parámetros que minimiza el valor de la función de error. A continuación, se presenta un gráfico mostrando el valor de la función de $mse$ para diversos valores de $w$ y $b$.<jupyter_code>from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

fig = plt.figure()
ax = fig.gca(projection='3d')

# Construyendo datos
w = np.arange(1, 5, 0.1)
b = np.arange(-1, 1, 0.01)
w, b = np.meshgrid(w, b)
e = np.empty_like(w)
#TODO: Complete la matriz e, tal que en cada celda i, j
#Contenga el error cuadrado medio de utilizar los parámetros
#w[i,j] y b[i,j] en el predictro lineal


# Plot the surface.
surf = ax.plot_surface(w, b, e, cmap=cm.coolwarm,
                       linewidth=0, antialiased=False)


plt.show()<jupyter_output><empty_output><jupyter_text>Obviamente, calculando el error para diversos valores de $w$ y $b$ se puede seleccionar el mínimo. Sin embargo, esto es impracticable cuando existen muchos parámetros o puntos de datos.Por simplicidad, vamos a suponer que se conoce $b=0$ resultando en que $\hat{y}=xw$, por simplicidad la llamaremos $h(x)$. Entonces, nuestro único problema sería encontrar $w$. En este caso, si graficamos la curva de error obtendríamos lo siguiente.<jupyter_code>def exp_error(y, x, ws):
    def single_error(w):
        return mse(y, lineal(x, w, 0))
    _s = np.vectorize(single_error)
    return _s(ws)

ws = np.linspace(1, 5, 51)
plt.plot(ws, exp_error(y, x, ws))
plt.show()<jupyter_output><empty_output><jupyter_text>SoluciónDado que la función de error tiene un solo mínimo, se podrían tomar 2 valores cercanos de manera de conocer en qué dirección es conveniente explorar. La función lineal en realidad es una función que depende no solo de los datos $x$, sino que también del parámetro a aprender $w$, entonces la notaremos como $h(x,w)$. Para conocer la pendiente de la función de error dado el parámetro a conocer debemos hacer:$$pendiente_w(w_{1}, w_{0})=\frac{MSE(y,h(x,w_{1}))-MSE(y,h(x,w_{0}))}{w_{1}-w_{0}}$$<jupyter_code>errors = exp_error(y, x, ws)
pendiente = (errors[10]-errors[20])/(ws[10]-ws[20])
correccion_ordenada_origen = -pendiente*ws[10] + errors[10]
plt.plot(ws, errors, ws[10:21], lineal(pendiente, ws[10:21], 0)+correccion_ordenada_origen)
plt.show()<jupyter_output><empty_output><jupyter_text>Entonces, se podría inicializar $w$ de forma aleatoria e ir actualizando el valor en contra de la pendiente.```for i in range(ciclos):    pw, pb = pendiente(w, b)    w = w - lr * pw    b = b - lr * pb````lr` y `ciclos` son lo que se conocen como hiperparámetros del algoritmo y dependiendo su selección será cuan bien y rápido el algoritmo llegue a un resultado. Por ejemplo, si `lr` es muy pequeño el algoritmo tardará mucho en ajustar los parámetros. Sin embargo, si `lr` es muy grande el algoritmo rebotará entre valores de error muy alto.<jupyter_code>def pendiente(y_true, x, w, b, delta=1e-6):
    pw = 0#TODO: Calcule la pendiente del error al variar entre w+delta y w
    pb = 0#TODO: Calcule la pendiente del error al variar entre b+delta y b
    return pw, pb

w = 0 #Podría ser cualquier valor
b = 1 
ciclos = 100
lr = 0.1 ## Valores pequeños y grandes: 0.01 y 10
errors = []
for i in range(ciclos):
    pw, pb = pendiente(y, x, w, b)
    errors.append(mse(y, lineal(x, w, b)))
    w = w - lr * pw
    b = b - lr * pb
print('Errores a medida que se actualiza el valor de w')
plt.plot(errors)
plt.show()
print('El w final es {}'.format(w))
print('El b final es {}'.format(b))<jupyter_output><empty_output><jupyter_text>Gradient DescentCuando consideramos la pendiente entre dos puntos muy cercanos, en realidad podemos considerar que la pendiente es la derivada para esa variable en ese punto.$$\lim_{\Delta \rightarrow 0} pendiente_w(w_{0}+\Delta,w_{0})= \lim_{\Delta \rightarrow 0} \frac{MSE(y,h(x,w_{0}+\Delta, b))-MSE(y,h(x,w_{0}, b))}{\Delta} =\frac{dMSE(y,h(x,w,b))}{dw}$$$$\lim_{\Delta \rightarrow 0} pendiente_b(b_{0}+\Delta,b_{0})= \lim_{\Delta \rightarrow 0} \frac{MSE(y,h(x,w, b_{0}+\Delta))-MSE(y,h(x,w,b_{0}))}{\Delta} =\frac{dMSE(y,h(x,w,b))}{db}$$Cuando generalizamos esto para todas las variables que se desean optimizar, en realidad trabajamos sobre el gradiente:$$\nabla MSE(w,b)=(\frac{dMSE(w,b)}{dw},\frac{dMSE(w,b)}{db})$$Utilizar el gradiente tiene dos ventajas: 1. Reduce la cantidad de computo. 2. Reduce errores de precisión de punto flotante.  Con esto, el algoritmo anterior se transforma en:<jupyter_code>def gradiente(y_true, x, w, b):
    gw = -2 * np.average((y_true -lineal(w, x, b) ) * x)
    gb = -2 * np.average((y_true -lineal(w, x, b) ))
    return gw, gb
  
w = np.random.uniform(-50, 50) #genera un flotante aleatorio
b = np.random.uniform(-50, 50) #genera un flotante aleatorio
print('Valores iniciales. w={} b={}'.format(w, b))
ciclos = 100
lr = 0.1
errors = []
for i in range(ciclos):
    pw, pb = gradiente(y, x, w, b)
    errors.append(mse(y, lineal(x, w, b)))
    #TODO: Actualice los valores de w y b
print('Errores a medida que se actualiza el valor de w')
plt.plot(errors)
plt.show()
print('El w final es {}'.format(w))
print('El b final es {}'.format(b))<jupyter_output><empty_output><jupyter_text>En el caso general,  una instancia puede tener muchas características, por lo tanto $\bar{x}=(x_1, x_2, ..., x_m)$ y $\bar{w}=(w_1, w_2, ..., w_m)$ es un vectores y la fórmula de la regresión lineal es:$$f(x)=x_1 * w_1+ x_2 * w_2 + ... + x_m * w_m +b$$Si consideramos el producto interno entre los vectores, se puede expresar como:$$f(x)=\bar{x} \cdot \bar{w} +b$$Para acelerar la computación, se suele calcular sobre muchas instancias, con lo que:$$ X = \left[\begin{array}{c}\bar{x}_1^T\\\bar{x}_2^T\\...\\\bar{x}_n^T\\\end{array}\right] =\left[\begin{array}{cccc}x_{1, 1} & x_{1, 2} & ... & x_{1, m}\\x_{2, 1} & x_{2, 2} & ... & x_{2, m}\\... & ... & ... & ...\\x_{n, 1} & x_{n, 2} & ... & x_{n, m}\\\end{array}\right]$$En este caso, el producto interno sigue funcionando, ya que el vector se comporta como una matriz de $(m, 1)$. Regresión logísticaLo anterior se conoce como regresión lineal, que sirve para cuando se quiere predecir un valor en un continuo. Sin embargo, no funciona muy bien cuando queremos resolver problemas de clasificación. La regresión logística (_Logistic Regression_) es un tipo de regresión cuyo objetivo es determinar la probabilidad de que una instancia pertesca a una clase $y$, dado un conjunto de variables independientes $x_i$ que la definen. En este contexto, las instancias están representadas como un vector de variables independientes $x=\mathbb{R}^{n}$ y una clase $y=\{0,1\}$. Es decir:$$P(y|\bar{x})=h(\bar{x})$$En este contexto, la función seleccionada para hacer esta estimación por excelencia es la sigmoide.$$sigmoid(z)=\frac{1}{1+e^{-z}}$$donde: $$z=\bar{x} \cdot \bar{w}+b$$<jupyter_code>%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

def sigmoid(x):
    return x#TODO: Implemente la función sigmoide

x = np.linspace(-6, 6, 250)
plt.plot(x, sigmoid(x))
plt.show()<jupyter_output><empty_output><jupyter_text>Además de que la imagen de esta función está en $(0, 1)$, la derivada de esta función es:$$\frac{sigmoid(z)}{dz}=sigmoid(z)(1-sigmoid(z))$$Lo que facilitaba su implementación.En este contexto, $z$ es una combinación lineal de las variables $x$. Función de errorPara calcular el error, se utiliza la entropía cruzada entre el valor esperado y el valor obtenido.$$CE(y,\hat{y})=\frac{\sum(-y*log(\hat{y})-(1-y)*log(1-\hat{y}))}{N}$$En este contexto, la entropía cruzada se interpreta como la información promedio (en bits) necesaria para determinar el valor de $y$ dado que se conoce el valor de $\hat{y}$.__Nota__: Por simplicidad, se interpreta el logaritmo como logaritmo natural, pero se puede usar logaritmo en cualquier base, ya que solo afecta en una constante. EjemploPara el ejemplo de regresión logística se utilizará el conjunto de datos de cáncer de pecho provisto. Este conjuntos de datos fue recolectado por investigadores de la Universidad de Wisconsin y provisto por la [UCI](https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic)). Para acceder al conjunto de datos, no es necesario descargarlo y convertirlo al formato, ya que en encuentra provisto por el módulo de [_sklearn.datasets_](http://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.htmlsklearn.datasets.load_breast_cancer) de la librería sickit-learn, que es una librería de _machine learning_ que se utilizará durante el curso.El dataset tiene 569 instancias, con 30 atributos cada una. Las instancias pueden ser clasificadas entre Malignos y Benignos. El dataset está ligeramente desbalanceado, lo que significa que existen más instancias de una clase que de la otra. En particular, 37,25% de las instancias son Malignas y 62,75% son Benignas. La siguiente tabla resume el conjunto de datos:| Propiedad | Valor || --- | --- || Clases | 2 || Ejemplos por clase | 212(M-0), 357(B-1) | | Total de instancias | 569 || Dimensionalidad | 30|El siguiente código:1. Levanta los datos divididos en `x` (atributos) e `y` (clase).1. Divide los datos en entrenamiento y testing.1. Escala los datos de entrenamiento a valores entre 0 y 1.1. Aplica las correcciones de escalado al conjunto de testing.<jupyter_code>from sklearn.datasets import load_breast_cancer
from sklearn.metrics import confusion_matrix


x, y = load_breast_cancer(True)
x_train = x[:500,:]
y_train = y[:500]
x_test = x[500:,:]
y_test = y[500:]

maxs = np.max(x_train, axis=0)
mins = np.min(x_train, axis=0)
x_train = (x_train - mins) / (maxs - mins)
x_test = (x_test - mins) / (maxs - mins)

print('El conjuto de datos tiene {} instancias con {} caracteristicas.'.format(*x.shape))
print('Se dividio en {} instancias de entrenamiento y {} de test'.format(x_train.shape[0], x_test.shape[0]))


print('Si utilizamos T-SNE para visualizar el conjunto de entrenamiento...')
from sklearn.manifold import TSNE

ts_rep = TSNE().fit_transform(x_train)
for point, label in zip(ts_rep, y_train):
    rep = 'b*' if label == 1 else 'r*'
    plt.plot([point[0]], [point[1]], rep)
plt.show()<jupyter_output><empty_output><jupyter_text>Implementar los gradientes es una tarea compleja y propensa a errores, pero frameworks como [Tensorflow](https://www.tensorflow.org) proveen funcionalidad para derivar los gradientes desde las funciones definidas.<jupyter_code>import tensorflow as tf

'''Esta función dibuja bonita la matríz de confunsión.
'''
def show_confusion_matrix(cm, labels):
    fig = plt.figure()
    ax = fig.add_subplot(111)
    cax = ax.matshow(cm)
    plt.title('Matriz de confusión')
    fig.colorbar(cax)
    ax.set_xticklabels([''] + labels)
    ax.set_yticklabels([''] + labels)
    plt.xlabel('Predicho')
    plt.ylabel('Verdadero')
    for i, row in zip(range(len(cm)), cm):
        for j, val in zip(range(len(row)), row):
            ax.text(i, j, str(val), va='center', ha='center').set_backgroundcolor('white')
    plt.show()

rng = np.random
ciclos = 1000
learning_rate = 0.1

# Placeholder de las entradas
X = tf.placeholder(tf.float32, [None, 30])
Y = tf.placeholder(tf.float32, [None])

W = tf.Variable(rng.randn(30).astype(np.float32), name="weight")
b = tf.Variable(rng.randn(), name="bias")

# Modelo logístico
lineal = tf.add(tf.reduce_sum(tf.matmul(X, tf.expand_dims(W, axis=1)), axis=1), b)
logreg = 1.0 / tf.add(1.0, tf.exp(-lineal))
# Error de entropía cruzada
cost = 0#TODO: Immplemente la entropia cruzada tf.reduce_mean y tf.log
# Gradient descent
# minimize() sabe que hay que modificar W y b porque están configuradas como trainable=True por defecto
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)

# Initializa las variables
init = tf.global_variables_initializer()

# Comenzar una sessión
with tf.Session() as sess:

    # Inicializar
    sess.run(init)

    y_pred = sess.run(logreg, feed_dict={X: x_test, Y:y_test})
    print('Error: {}'.format(sess.run(cost, feed_dict={X: x_test, Y:y_test})))
    show_confusion_matrix(confusion_matrix(y_test, y_pred > 0.5), labels=['Maligno', 'Benigno'])
    errors = []
    errors_test = []
    print('Entrenando')
    for epoch in range(ciclos):
        sess.run(optimizer, feed_dict={X: x_train, Y: y_train})
        errors.append(sess.run(cost, feed_dict={X: x_train, Y:y_train}))
        errors_test.append(sess.run(cost, feed_dict={X: x_test, Y:y_test}))
    print('Error en entrenamiento')
    plt.plot(range(ciclos), errors, 'b-', range(ciclos), errors_test, 'r-')
    plt.show()
    y_pred = sess.run(logreg, feed_dict={X: x_test, Y:y_test})
    print('Error: {}'.format(sess.run(cost, feed_dict={X: x_test, Y:y_test})))
    show_confusion_matrix(confusion_matrix(y_test, y_pred > 0.5), labels=['Maligno', 'Benigno'])
    print('El w final es {}'.format(sess.run(W)))
    print('El b final es {}'.format(sess.run(b)))<jupyter_output><empty_output><jupyter_text>KerasDe hecho, este tipo de operaciones es tan común que frameworks de más alto nivel, como [Keras](https://keras.io/) ya lo traen implementado.Es más, elementos como regresiones lineales o logísticas son considerados como neuronas en una red neuronal ([Imagen de Wikipedia](https://commons.wikimedia.org/wiki/File:Artificial_neural_network.png)):![Esquema básido de una neurona](https://upload.wikimedia.org/wikipedia/commons/b/b6/Artificial_neural_network.png)<jupyter_code>from keras.layers import Dense, Input
from keras.models import Model
from keras import backend as K

#Defino la entrada que tiene forma  (None, 30)
i = Input((x_train.shape[1],))
#Defino una capa densa que es activation(x*w+b) 
#* es producto interno
#el kernel (w) tiene la forma (30, 1)
#el bias (b) tiene la forma (1,)
d = Dense(1, activation='sigmoid')(i)
model = Model(inputs=i, outputs=d)
model.compile(loss='binary_crossentropy', optimizer='sgd')

#imprimo el modelo
model.summary()

show_confusion_matrix(confusion_matrix(y_test, model.predict(x_test) > 0.5), labels=['Maligno', 'Benigno'])
#Por formato remuevo la última dimensión que es 1
print('El w final es {}'.format(K.get_value(model.layers[-1].kernel)[:, 0])) 
print('El b final es {}'.format(K.get_value(model.layers[-1].bias)))

h = model.fit(x_train, y_train, epochs=100, validation_data=(x_test, y_test), verbose=0)
plt.plot(h.history['loss'], 'b-', h.history['val_loss'], 'r-')
plt.show()

show_confusion_matrix(confusion_matrix(y_test, model.predict(x_test) > 0.5), labels=['Maligno', 'Benigno'])
#Por formato remuevo la última dimensión que es 1
print('El w final es {}'.format(K.get_value(model.layers[-1].kernel)[:, 0]))
print('El b final es {}'.format(K.get_value(model.layers[-1].bias)))<jupyter_output><empty_output><jupyter_text>Problema de OCR de dígitosPara este trabajo utilizaremos el conjunto de datos conocido como [MNIST](http://yann.lecun.com/exdb/mnist/). Este conjunto de datos ya se encuentra dividido entre entrenamiento y testing. El problema consiste en clasificar imágenes de dígitos escritos a mano al dígito correspondiente.| Propiedad | Valor || --- | --- || Clases | 10 || Tamaño de las imagenes | 28 X 28 || Instancias de entrenemiento | 60.000 || Instancias de testeo | 10.000 || Valor mínimo de cada pixel | 0 || Valor máximo de cada pixel | 255 |A continuación, se carga el dataset y se dibujan los primeros 100 ejemplos del conjunto de entrenamiento.<jupyter_code>from keras.datasets import mnist
from keras.utils import to_categorical

(x_train, y_train), (x_test, y_test) = mnist.load_data()

print('100 primeros elementos del conjunto de entrenamiento')
f = plt.figure(111)
for i in range(10):
    for j in range(10):
        ax = f.add_subplot(10, 10, i + j*10 + 1)
        ax.set_xticklabels('')
        ax.set_yticklabels('')
        ax.imshow(x_train[i + j*10, :, :], cmap='gray')
plt.show()

size = x_train.shape[1]*x_train.shape[2]
x_train = x_train.reshape((x_train.shape[0], size)) / 255
x_test = x_test.reshape((x_test.shape[0], size)) / 255<jupyter_output><empty_output><jupyter_text>Esto es un problema multiclase, que se podría pensar como una competencia entre 10 clasificadores que consideren cada uno de los $784$ pixeles de la imagen como sus características. Es decir, un clasificador para la clase 0, otro para la clase 1, otro para la clase 2, hasta llegar al 9. A la hora de entrenar, cada regresión logística se entrena por separado. Mientras que cuando se clasifica, la clase seleccionada es el clasificador que obtuvo la máxima probabilidad. Para acelerar el cálculo de los 10 clasificadores, se pueden unir los 10 vectores de peso $\bar{w}_c$ en una matriz, y los biases $b_c$ en un vector. Entonces:$$W=\left[\begin{array}{cccc}\bar{w}_0 & \bar{w}_1 & ... & \bar{w}_9\\\end{array}\right]=$\left[\begin{array}{cccc}w_{1,0} & w_{1,1} & ... & w_{1,9}\\w_{2,0} & w_{2,1} & ... & w_{2,9}\\...\\w_{784,0} & w_{784,1} & ... & w_{784,9}\\\end{array}\right]$$$$\bar{b}=(b_0, b_1, ..., b_9)$$Con esta definición, es fácil mostrar que:$$f(X) = X \cdot W + \bar{b} =\left[\begin{array}{cccc}X \cdot \bar{w_0} + b_0 & X \cdot \bar{w_1} + b_1 & ... & X \cdot \bar{w_9} + b_9 \end{array}\right]$$Con lo que cada columna de la matriz resultante, representa la salida de un clasificado para cada instancia.Por lo tanto, en este ejemplo, es necesario transformar las etiquetas a la codificación one-hot con el fin de que el formato de las etiquetas sea compatible con el formato de salida de los clasificadores.<jupyter_code>print('El las etiquetas en el conjunto de entrenamiento tienen la forma {}'.format(y_train.shape))
print('Las primeras 10 estiquetas son {}'.format(y_train[:10]))
print('Transformadas a categoricas tienen la siguiente forma:')
print(to_categorical(y_train[:10]))
from sklearn.metrics import accuracy_score as acc

def show_confusion_matrix_nl(cm):
    fig = plt.figure()
    ax = fig.add_subplot(111)
    cax = ax.matshow(cm)
    plt.title('Matriz de confusión')
    fig.colorbar(cax)
    plt.xlabel('Predicho')
    plt.ylabel('Verdadero')
    plt.show()

#Inicializo la entrada como un vector de tamaño size
i = Input(shape=(size,)) 
#Inicializo una capa densa, activación sigmoide y entrada i
#Para inicializar la capa densa se usa la API funcional de keras
d = Dense(10, activation='sigmoid')(i) 
#Inicializo el modelo a partir de sus entradas y salidas
model = Model(inputs=i, outputs=d)
#Compilo el modelo con la función de pedidad y utilizando 
#Stocastic Gradiant Descent como optimizador (una variante del Gradient Descent)
#metrics no es necesario, pero permite usar otra función de error para la validación
model.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['categorical_accuracy'])
#Muestro la esturctura del perceptrón
model.summary()
#Me quedo con la mayor predicción
predict = lambda x: np.argmax(model.predict(x), axis=-1)

show_confusion_matrix_nl(confusion_matrix(y_test, predict(x_test)))
print('La accuracy antes de entrenar es {}'.format(acc(y_test, predict(x_test))))
#Entreno y guardo el historial del errores en h. 
#verbose: 0: sin salida, 1: salida detallada, 2: salida solo al final del epoch
h = model.fit(x_train, to_categorical(y_train), 
              batch_size=100, epochs=10, 
              validation_data=(x_test, to_categorical(y_test)), verbose=0)

print('Función de pérdidad:')
plt.plot(h.history['loss'], 'b-', h.history['val_loss'], 'r-')
plt.show()
print('Accuracy:')
plt.plot(h.history['categorical_accuracy'], 'b-', h.history['val_categorical_accuracy'], 'r-')
plt.show()

show_confusion_matrix_nl(confusion_matrix(y_test, predict(x_test)))
print('La accuracy después de entrenar es {}'.format(acc(y_test, predict(x_test))))<jupyter_output><empty_output><jupyter_text>Competencias entre clasesUno de los problemas más grandes que tiene la versión de arriba es que no hay competencia entre las clases. Si agregamos información de que una imagen no puede pertenecer a dos clases, se puede mejorar el clasificador. Para esto, utilizamos una función de activación conocida como softmax:$$Softmax(\bar{z})_{i}=\frac{e^{z_{i}}}{\sum e^{z_{j}}} $$Como resultado de utilizar la función, la suma de las probabilidades para una instancia es 1.Además, se cambia la función de error a la entropía cruzada categórica$$CC(Y, \hat{Y})=-{\sum Y \circ log(\hat{Y})} $$Donde $y$ es una matriz de la cantidad de instancias por la cantidad de clases. En cada fila tiene un uno para la clase correspondiente a la instancia y ceros en todos los demás elementos. $\hat{Y}$ es una matriz de las mismas dimensiones, es la salida del clasificador. Y $\circ$ es el producto elemento a elemento, conocido como producto de Hadamard.<jupyter_code>#Inicializo la entrada como un vector de tamaño size
i = Input(shape=(size,)) 
#Inicializo una capa densa, activación sigmoide y entrada i
#Para inicializar la capa densa se usa la API funcional de keras
d = Dense(10, activation='softmax')(i) 
#Inicializo el modelo a partir de sus entradas y salidas
model = Model(inputs=i, outputs=d)
#Compilo el modelo con la función de pedidad y utilizando 
#Stocastic Gradiant Descent como optimizador (una variante del Gradient Descent)
#metrics no es necesario, pero permite usar otra función de error para la validación
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['categorical_accuracy'])
#Muestro la esturctura del perceptrón
model.summary()

show_confusion_matrix_nl(confusion_matrix(y_test, predict(x_test)))
print('La accuracy antes de entrenar es {}'.format(acc(y_test, predict(x_test))))
#Entreno y guardo el historial del errores en h. 
#verbose: 0: sin salida, 1: salida detallada, 2: salida solo al final del epoch
h = model.fit(x_train, to_categorical(y_train), 
              batch_size=100, epochs=10, 
              validation_data=(x_test, to_categorical(y_test)), verbose=0)

print('Función de pérdidad:')
plt.plot(h.history['loss'], 'b-', h.history['val_loss'], 'r-')
plt.show()
print('Accuracy:')
plt.plot(h.history['categorical_accuracy'], 'b-', h.history['val_categorical_accuracy'], 'r-')
plt.show()


show_confusion_matrix_nl(confusion_matrix(y_test, predict(x_test)))
print('La accuracy después de entrenar es {}'.format(acc(y_test, predict(x_test))))<jupyter_output><empty_output><jupyter_text>Hasta aquí, se puede observar que funciones lineales tienen una buena capacidad de predicción. Si vemos los resultados, en el $90\%$ de los casos la clasificación fue correcta. Como el dataset es balanceado, es decir, tiene la misma cantidad de instancias de cada clase podemos decir que tanto un clasificador aleatorio o un clasificador de clase mayoritaria obtienen, en promedio, clasificarían correctamente el $90\%$ de los casos. Red Neuronal ProfundaUno de los tipos redes neuronales profundas consisten en apilar varios de estos clasificadores. Estas se suelen llamar *Deep Feedforward Networks* y su expresión matemática es:$$L1(X)=act_1(X \cdot W_1 + \bar{b}_1)$$$$L2(L1(X)=act_2(L1(X) \cdot W_2 + \bar{b}_2)$$$$L3(L2(L1(X)))=act_3(L2(L1(X)) \cdot W_3 + \bar{b}_3)$$$$...$$$$Ln(...L3(L2(L1(X))...)=act_n(Ln(...(L3((L1(X))...) \cdot W_n + \bar{b}_n)$$El concepto es que $L1$ extraiga características lineales de los datos, $L2$ cuadráticas, y así hasta que $L_n$ haga la predicción. $act_i$ es lo que se denomina función de activación, generalmente se suelen usar:* **Sigmoide**: acotada entre $(0,1)$, con su valor medio en $s(0)=0.5$* **Tanh**: Similar a la sigmoide, pero acotada entre $(-1, 1)$* **Lineal o identidad**: una función que retorna el valor de entrada $f(x)=x$* **Relu (Rectified Linear Unit)**: función lineal si $x > 0$, sino es contante en $0$![Red Neuronal](https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Neural_network_bottleneck_achitecture.svg/800px-Neural_network_bottleneck_achitecture.svg.png)Fuente: [Wikimedia](https://commons.wikimedia.org/wiki/File:Neural_network_bottleneck_achitecture.svg)**NOTA:** Si bien está probado que una red con 3 capas puede aprender cualquier función, no se sabe como determinar el número necesario de unidades o características que la capa intermedia debe aprender. Además, son difíciles de entrenar con métodos basados en el gradiente, ya que son muy sensibles a los cambios.**NOTA 2:** Las funciones de activación Relu y Lineal son preferidas a para las capas intermedias, ya que tiende facilitar el entrenamiento. Funciones como la sigmoide o tanh, pueden tener gradientes pequeños que se pierden por la limitación en la representación de los números flotantes. Esto se conoce como ***vanishing gradient problem***.<jupyter_code>#Inicializo la entrada como un vector de tamaño size
i = Input(shape=(size,)) 
#Inicializo una capa densa, activación sigmoide y entrada i
#Para inicializar la capa densa se usa la API funcional de keras
d = Dense(100, activation='relu')(i)

#TODO: Apile 2 capas densas de 100 neuronas y activación relu en la variable d

d = Dense(10, activation='softmax')(d) 
#Inicializo el modelo a partir de sus entradas y salidas
model = Model(inputs=i, outputs=d)
#Compilo el modelo con la función de pedidad y utilizando 
#Stocastic Gradiant Descent como optimizador (una variante del Gradient Descent)
#metrics no es necesario, pero permite usar otra función de error para la validación
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['categorical_accuracy'])
#Muestro la esturctura del perceptrón
model.summary()

show_confusion_matrix_nl(confusion_matrix(y_test, predict(x_test)))
print('La accuracy antes de entrenar es {}'.format(acc(y_test, predict(x_test))))
#Entreno y guardo el historial del errores en h. 
#verbose: 0: sin salida, 1: salida detallada, 2: salida solo al final del epoch
h = model.fit(x_train, to_categorical(y_train), 
              batch_size=100, epochs=10, 
              validation_data=(x_test, to_categorical(y_test)), verbose=0)

print('Función de pérdidad:')
plt.plot(h.history['loss'], 'b-', h.history['val_loss'], 'r-')
plt.show()
print('Accuracy:')
plt.plot(h.history['categorical_accuracy'], 'b-', h.history['val_categorical_accuracy'], 'r-')
plt.show()


show_confusion_matrix_nl(confusion_matrix(y_test, predict(x_test)))
print('La accuracy después de entrenar es {}'.format(acc(y_test, predict(x_test))))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12671366325410038, 'fraction_numerical': 0.01587020439973077, 'mean_word_length': 5.070967741935484, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 66, 'https://': 7, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '186423', 'license': 'Unlicense', 'n_tokens_mistral': 10505, 'n_tokens_neox': 10138, 'n_words': 3191, 'path': 'Clear/TP SofiaV2.ipynb', 'repo_name': 'knife982000/SofIA2019', 'size': '28229'}