Capítulo 14 de 21 10 secciones 7 min

Compartir

Derivadas parciales y el gradiente: hacia dónde subir

Qué es una derivada parcial, qué es el gradiente, y por qué apunta hacia arriba cuando lo que quieres es bajar.

Una derivada parcial es cuánto cambia el error si mueves un peso y dejas los demás quietos. El gradiente es el vector con todas juntas, y apunta hacia donde el error sube más rápido. Por eso para bajar hay que restarlo, y por eso el algoritmo lleva un signo menos.

Un peso era fácil. Ahora son dos 🌸

En el capítulo 13 había un solo peso, así que el valle era una curva y la pregunta era izquierda o derecha.

Con dos pesos el valle es una superficie, como un terreno con lomas. Y la pregunta ya no es izquierda o derecha: es en qué dirección de todas las posibles.

Volvemos a abrir el archivo

Cada capítulo arranca de cero, así que estas líneas se repiten. Son las mismas del capítulo 1 y las vas a ver en todos.

import pandas as pd
import numpy as np

df = pd.read_csv('ventas-miss-yera.csv')
for c in ('unidades', 'monto', 'satisfaccion'):
    df[c] = pd.to_numeric(df[c], errors='coerce')
X = df[['unidades', 'monto', 'satisfaccion']].dropna().to_numpy()
print('X.shape:', X.shape)
X.shape: (2238, 3)

Dos pesos: intercepto y unidades

y = X[:, 1]
A = np.column_stack([np.ones(X.shape[0]), X[:, 0]])

def error(w):
    return float(((y - A @ w) ** 2).mean())

def gradiente(w):
    return -2 * (A.T @ (y - A @ w)) / len(y)

w = np.array([0.0, 0.0])
print('error en (0, 0):', round(error(w), 2))
print('gradiente      :', np.round(gradiente(w), 4))
error en (0, 0): 1213444.56
gradiente      : [ -1606.7725 -18794.463 ]
Ewj

la derivada parcial respecto a un peso es cuánto cambia el error si mueves ese peso y dejas todos los demás quietos

Ese vector tiene dos números y cada uno contesta una pregunta distinta:

  • -1606.77: si muevo el intercepto y dejo el peso de las unidades quieto, el error baja con esta inclinación.
  • -18794.46: lo mismo para el peso de las unidades.

Eso es una derivada parcial: la derivada de siempre, pero mirando un solo peso y congelando los demás. La palabra parcial no esconde nada raro, quiere decir "de a uno".

E=(Ew1,,Ewd)

el gradiente es el vector con todas las derivadas parciales juntas, y como vector apunta hacia donde el error sube más rápido

SímboloQué es
∇Eel gradiente, y ese triangulito al revés se llama nabla
∂E/∂wjla parcial respecto al peso j
la de es curva y no recta solo para avisar que hay más variables

El gradiente es un vector, con todo lo que eso quiere decir desde el capítulo 2: tiene componentes, tiene una dirección y tiene un largo.

Y ahora lo importante: apunta hacia arriba

w = np.array([500.0, 30.0])
g = gradiente(w)
paso = 1e-5
print('error donde estoy      :', round(error(w), 2))
print('error restando el grad :', round(error(w - paso * g), 2))
print('error sumando el grad  :', round(error(w + paso * g), 2))
error donde estoy      : 599329.38
error restando el grad : 599235.21
error sumando el grad  : 599423.86

Restando baja, sumando sube. El gradiente apunta cuesta arriba.

Y no es un capricho de la definición: el gradiente apunta hacia donde la función crece más rápido, de todas las direcciones posibles. Es la dirección de máxima subida.

Como nosotras queremos bajar, vamos exactamente al revés. De ahí sale el signo menos que vas a ver en todas las fórmulas de entrenamiento, y de ahí sale el nombre del algoritmo 🐣

En el fondo vale cero

optimo = np.linalg.solve(A.T @ A, A.T @ y)
print('intercepto   :', round(float(optimo[0]), 4))
print('peso unidades:', round(float(optimo[1]), 4))
print('gradiente ahi:', np.round(gradiente(optimo), 6))
print('error ahi    :', round(error(optimo), 2))
intercepto   : 797.5076
peso unidades: 0.5035
gradiente ahi: [-0.  0.]
error ahi    : 568006.6

Gradiente cero en las dos componentes. Ninguna dirección baja: estás en el fondo.

Y ese punto es el mismo que salió por las ecuaciones normales del capítulo 6. Tercera vez que dos caminos distintos llegan al mismo sitio en este libro, y no va a ser la última 🌟

Cuidado con leer los pesos

Fíjate en el gradiente de partida: -1606 contra -18794. El segundo es once veces más grande.

La tentación es decir "las unidades importan once veces más". Y no. Ese número es grande porque la columna de unidades tiene números grandes comparada con la columna de unos, exactamente el mismo problema de escala del capítulo 7.

El gradiente no mide importancia, mide inclinación en unidades de esa columna. Y esa diferencia de escala entre componentes es la que hace que el descenso de gradiente zigzaguee. Eso lo trabajamos en el capítulo 16.

Lo que te llevas

  • Una parcial es la derivada de siempre moviendo un peso y congelando los demás.
  • El gradiente es el vector con todas las parciales juntas.
  • Apunta hacia donde la función SUBE más rápido.
  • Por eso el descenso de gradiente resta, y de ahí sale el menos de las fórmulas.
  • Gradiente cero en todas las componentes quiere decir que estás en el fondo.
  • Componentes grandes no quiere decir columnas importantes: quiere decir escalas distintas.

Comprueba que se entendió

Comprueba que lo tienes

El gradiente del error en tu punto actual es [93.79, 3069.82]. ¿Hacia dónde te mueves para bajar el error?

  • Restando el gradiente: hacia [-93.79, -3069.82]
  • Sumando el gradiente, porque apunta al mínimo
  • Solo hacia el segundo peso, porque su número es mucho mayor
  • Hacia [0, 0], que es donde el gradiente vale cero

Ejercicios

1. Cada parcial, una por una

Comprueba que el gradiente son dos derivadas separadas.

y = X[:, 1]
A = np.column_stack([np.ones(X.shape[0]), X[:, 0]])

def error(w):
    return float(((y - A @ w) ** 2).mean())

w = np.array([500.0, 30.0])
h = 1e-4
for j in range(2):
    mas, menos = w.copy(), w.copy()
    mas[j] += h
    menos[j] -= h
    print('parcial', j, ':', round((error(mas) - error(menos)) / (2 * h), 4))
parcial 0 : 93.7905
parcial 1 : 3069.8176

Mover un peso y congelar el otro, dos veces. El gradiente no es un objeto misterioso: es el resultado de hacer eso para cada peso y guardar los números en una lista.

2. Ninguna otra dirección baja más

Compara el gradiente contra direcciones al azar.

y = X[:, 1]
A = np.column_stack([np.ones(X.shape[0]), X[:, 0]])

def error(w):
    return float(((y - A @ w) ** 2).mean())

def gradiente(w):
    return -2 * (A.T @ (y - A @ w)) / len(y)

w = np.array([500.0, 30.0])
g = gradiente(w)
d = -g / np.linalg.norm(g)
paso = 0.01
print('bajando por el gradiente :', round(error(w + paso * d), 4))

rng = np.random.default_rng(0)
peor = 0
for _ in range(200):
    r = rng.normal(size=2)
    r = r / np.linalg.norm(r)
    if error(w + paso * r) < error(w + paso * d):
        peor += 1
print('direcciones al azar que bajaron mas:', peor, 'de 200')
bajando por el gradiente : 599298.6806
direcciones al azar que bajaron mas: 0 de 200

Doscientas direcciones y ninguna gana. Eso es lo que quiere decir "dirección de máxima pendiente", y es la razón de que el algoritmo se llame así y no de otra manera.

3. Escala las columnas y mira el gradiente

El mismo problema, con las columnas puestas en la misma escala.

y = X[:, 1]
u = X[:, 0]
uz = (u - u.mean()) / u.std()
Az = np.column_stack([np.ones(len(u)), uz])

def grad_z(w):
    return -2 * (Az.T @ (y - Az @ w)) / len(y)

print('sin escalar:', np.round(-2 * (np.column_stack([np.ones(len(u)), u]).T
                                     @ (y - np.column_stack([np.ones(len(u)), u])
                                        @ np.array([0.0, 0.0]))) / len(y), 2))
print('escalado   :', np.round(grad_z(np.array([0.0, 0.0])), 2))
sin escalar: [ -1606.77 -18794.46]
escalado   : [-1606.77    -5.83]

Sin escalar las dos componentes se llevaban once veces de diferencia. Con la columna escalada el segundo número se desploma a -5.83, porque ahora está medido en desviaciones y no en unidades sueltas. Los datos son los mismos: lo único que cambió es en qué se miden. Por eso escalar antes de entrenar no es cosmética.

Preguntas frecuentes

¿Qué son las derivadas parciales?

La derivada respecto de una variable dejando las otras quietas. Si tu error depende de cinco parámetros, hay cinco derivadas parciales, una por parámetro.

¿Qué es una derivada parcial, con un ejemplo?

Si el error depende del precio y del descuento, la parcial respecto del precio dice cuánto cambia el error al mover el precio un poquito y no tocar el descuento.

¿Qué es el gradiente?

El vector con todas las derivadas parciales juntas. Apunta hacia donde la función sube más rápido, y por eso entrenar es caminar en contra de él.

¿Para qué sirve el gradiente en machine learning?

Para saber en qué dirección mover cada parámetro para que el error baje. Es lo único que un modelo sabe hacer mientras entrena.

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?