#!/usr/bin/env python
# coding: utf-8

# # Perceptron.  Regresión lineal simple.  Ejemplo básico

# ## Importamos las librerías

# In[1]:


# Para este ejemplo, necesitamos numpy sí o sí.
import numpy as np

# Utilizamos estas 2 librerías sólo para visualizar los datos.
# En condiciones normales no las usaremos.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


# ## Generamos los datos (aleatorios)

# In[2]:


# Generamos 1000 observaciones
observations = 1000

np.random.seed(123)

# Usaremos 2 variables como datos de entrada.  Podemos verlas como x1 y x2 de lo visto en la presentación
# Las nombraremos x y z
# Generamos los datos de manera aleatoria a través de una distribución uniforme. Necesitamos 3 argumentos; low, high, size.
# El tamaño de xs (x size) y zs (z size) es el de las observaciones por 1. En este caso: 1000 x 1.
xs = np.random.uniform(low=-10, high=10, size=(observations,1))
zs = np.random.uniform(-10, 10, (observations,1))

# Combinamos las 2 dimensiones de entrada en una matriz de entrada
inputs = np.column_stack((xs,zs))

# Comprobamos si las dimensiones de los valores de entrada que deberán ser 1000x2
print (inputs.shape)


# ## Generamos los datos de la variable objetivo

# In[3]:


# Usaremos la siguiente función lineal:
# f(x,z) = 2x - 3z + 5 + <ruido>
np.random.seed(123)
noise = np.random.uniform(-1, 1, (observations,1))

# Generamos las salidas, de acuerdo a la funcion
# Donde los pesos son 2 y -3 y el bias es 5
targets = 2*xs - 3*zs + 5 + noise

# Comprobamos las dimensiones de targets, que deberían ser de 1000x1
print (targets.shape)


# ## Pintamos los datos

# In[4]:


# Necesitamos que los datos tengan las dimensiones correctas
xs = xs.reshape(observations,)
zs = zs.reshape(observations,)
targets = targets.reshape(observations,)


# Declaramos el objeto imagen
fig = plt.figure()

# Metodo para crear un plot 3d
ax = fig.add_subplot(111, projection='3d')

# Elegimos los ejes
ax.plot(xs, zs, targets)

# Definimos las etiquetas
ax.set_xlabel('xs')
ax.set_ylabel('zs')
ax.set_zlabel('Targets')

# A través de esta variable podemos modificar el ángulo de visión.
# Prueba con diferentes valores
ax.view_init(azim= 200)

# Pintamos
plt.show()

# Devolvemos los datos a sus dimensiones originales.
# El cambio era sólo para pintarlos.
targets = targets.reshape(observations,1)
xs = xs.reshape(observations,1)
zs = zs.reshape(observations,1)


# ## Inicialización de las variables

# In[63]:


# Inicializamos los pesos y bias de manera aletoria, a través de init_range
# Valores altos en init_range evitarán que el algoritmo sea capaz de aprender.
np.random.seed(123)
init_range = 0.1

# Los pesos tienen unas dimesniones de k x m, donde k es el número de variables de entrada y and m es el número de variables de salida
# En nuestro caso, la matriz de pesos es de 2x1 (2 variables de entrada, 'x' y 'z' y una variable de salida 'y')
weights = np.random.uniform(low=-init_range, high=init_range, size=(2, 1))

# El bías, sólo tendrá una dimensión, puesto que sólo hay una variable de salida. Por tanto, será un escalar
biases = np.random.uniform(low=-init_range, high=init_range, size=1)

print (weights)
print (biases)


# ## Definimos el ratio de aprendizaje

# In[64]:


# Ratios demasiado altos harán que nuestra red no aprenda y demasiado bajos, que no alcance el objetivo para las repeticiones definidas.
# Juega con este ratio para ver los resultados
learning_rate = 0.02


# ## Entrenamos el modelo

# In[60]:


# Vamos a iterar 100 veces a lo largo de nuestro set de entrenamiento que funciona bien para el ratio de entrenamiento de 0.02
for i in range (100):
    
    # Generamos el modelo lineal basado en: y = xw + b
    # np.dot permite la multiplicación de matrices
    outputs = np.dot(inputs,weights) + biases
    # Los deltas, son las diferencias entre los targets y los outputs
    # Los deltas en este caso son un vector de 1000x1
    deltas = outputs - targets
        
    # La idea es iterar para conseguir reducir el error cuadrático medio
    
    loss = np.sum(deltas ** 2)/2 / observations
    
    # Printamos el error cuadrático medio
    print (loss)
    
    # Otro truco que vamos a usar, es escalar los deltas en base a las observaciones
    # Ésto nos ayuda a seleccionar mejor el ratio de aprendizaje, ya que estará
    # en la misma escala (propoción) que el error cuadrático medio
    deltas_scaled = deltas / observations
    
    # Para terminar, aplicamos el descenso del gradiente para poder encontrar el mínimo local
    # Las dimensiones de los pesos son 2x1, el ratio de entrenamiento 1x1 (escalar), inputs 1000x2 y los
    # deltas escalados 1000x1.
    # Para poder aplicar el procedimiento, debemos transponer los inputs
    weights = weights - learning_rate * np.dot(inputs.T,deltas_scaled)
    biases = biases - learning_rate * np.sum(deltas_scaled)
    
    # The weights are updated in a linear algebraic way (a matrix minus another matrix)
    # The biases, however, are just a single number here, so we must transform the deltas into a scalar.
    # The two lines are both consistent with the gradient descent methodology. 


# ## Pintamos los pesos y los biases y comprobamos sin son correctos

# In[61]:


# Cuando hemos declarado f(x,z), los pesos eran 2 y -3, mientras que el bias era 5.
# Deberíamos obtener datos iguales
print (weights, biases)

# Vemos que CASI convergen, por lo que son necesarias más iteraciones.


# ## Pintamos los outputs vs los targets

# In[62]:


# Cuanto más cercanos sea el gráfico a una línea de 45 grados, más cercanos son los valores output y target.
# Ésto es un ejercicio académico para entender el procedimiento puesto que no se realiza habitualmente.

plt.plot(outputs,targets)
plt.xlabel('outputs')
plt.ylabel('targets')
plt.show()

