Sistemas y matrices

Multiplicación escalar con Python

El producto por un escalar, la norma y la normalización, con la similitud coseno entre embeddings y la tasa de aprendizaje del descenso por gradiente como aplicaciones.

De las operaciones del álgebra lineal, el producto por un escalar es la más simple de definir y la que menos atención suele recibir. Sin embargo, dos de las decisiones más consecuentes en aprendizaje automático —cómo se compara la similitud entre representaciones y a qué velocidad aprende un modelo— se reducen a elegir un número por el que multiplicar.

Definición

Dado λR\lambda \in \mathbb{R} y ARm×nA \in \mathbb{R}^{m\times n}, el producto λA\lambda A se define entrada a entrada:

(λA)ij=λaij(\lambda A)_{ij} = \lambda\, a_{ij}
import numpy as np A = np.array([[1., 2.], [3., 4.]]) 2 * A # array([[2., 4.], # [6., 8.]])

La expresión 2 * A no recorre la matriz en Python. El escalar se trata como un array de forma vacía y el mecanismo de broadcasting lo extiende sobre las mnmn entradas, que se procesan en un bucle compilado.

Propiedades

El producto por un escalar satisface cuatro identidades que, junto con las de la suma, constituyen los axiomas de espacio vectorial mencionados al tratar Rm×n\mathbb{R}^{m\times n}:

λ(A+B)=λA+λB(distributiva sobre la suma de matrices)(λ+μ)A=λA+μA(distributiva sobre la suma de escalares)λ(μA)=(λμ)A(asociativa)1A=A(elemento neutro)\begin{aligned} \lambda(A + B) &= \lambda A + \lambda B & \text{(distributiva sobre la suma de matrices)}\\ (\lambda + \mu)A &= \lambda A + \mu A & \text{(distributiva sobre la suma de escalares)}\\ \lambda(\mu A) &= (\lambda\mu)A & \text{(asociativa)}\\ 1 \cdot A &= A & \text{(elemento neutro)} \end{aligned}
B = np.ones((2, 2)) lam, mu = 3.0, -0.5 np.allclose(lam * (A + B), lam * A + lam * B) # True np.allclose(lam * (mu * A), (lam * mu) * A) # True

El escalar además conmuta con la transposición y se desplaza libremente entre los factores de un producto matricial:

(λC)=λC,λ(AB)=(λA)B=A(λB)(\lambda C)^\top = \lambda\, C^\top, \qquad \lambda(AB) = (\lambda A)B = A(\lambda B)
C = np.random.randn(2, 3) np.allclose((lam * C).T, lam * C.T) # True

Esa libertad de movimiento tiene una consecuencia práctica: en una expresión larga, el escalar puede aplicarse donde resulte más barato. Multiplicar una matriz 1000×10001000\times 1000 por λ\lambda cuesta un millón de operaciones; aplicar λ\lambda al vector resultante, mil.

La norma

El efecto geométrico del escalar se enuncia en términos de la longitud del vector. La norma euclídea de vRn\vec{v} \in \mathbb{R}^n es

v2=i=1nvi2\lVert \vec{v} \rVert_2 = \sqrt{\sum_{i=1}^{n} v_i^2}

Es la notación que apareció en la lección anterior al acotar la propagación del error; aquí queda definida. Con ella, el efecto del escalar se expresa exactamente:

λv=λv\lVert \lambda\vec{v} \rVert = |\lambda|\,\lVert \vec{v} \rVert
v = np.array([3., 4.]) np.linalg.norm(v) # 5.0 np.linalg.norm(2 * v) # 10.0

El valor absoluto es necesario: con λ<0\lambda < 0 el vector invierte su sentido, pero su longitud es una magnitud no negativa. En términos geométricos, λ>1|\lambda| > 1 dilata, λ<1|\lambda| < 1 contrae y λ<0\lambda < 0 refleja respecto del origen. La dirección, entendida como la recta que el vector genera, no cambia en ningún caso.

La norma euclídea no es la única en uso. np.linalg.norm admite el parámetro ord:

NormaDefiniciónDónde aparece
v1\lVert\vec{v}\rVert_1ivi\sum_i \lvert v_i \rvertregularización L1, que induce dispersión
v2\lVert\vec{v}\rVert_2ivi2\sqrt{\sum_i v_i^2}regularización L2, mínimos cuadrados
v\lVert\vec{v}\rVert_\inftymaxivi\max_i \lvert v_i \rvertcotas, recorte de gradientes

Las tres cumplen λv=λv\lVert \lambda\vec{v} \rVert = |\lambda| \lVert \vec{v} \rVert, propiedad conocida como homogeneidad absoluta y exigida por la definición axiomática de norma.

Normalización

Para v0\vec{v} \neq \vec{0}, elegir λ=1/v\lambda = 1/\lVert\vec{v}\rVert produce un vector de norma unitaria en la misma dirección:

v^=vv,v^=1\hat{v} = \frac{\vec{v}}{\lVert \vec{v} \rVert}, \qquad \lVert \hat{v} \rVert = 1
u = v / np.linalg.norm(v) np.linalg.norm(u) # 1.0

La operación separa la información de un vector en dos partes independientes: la dirección, que retiene v^\hat{v}, y la magnitud, que queda en el escalar v\lVert\vec{v}\rVert. Esa separación es la razón de su uso en aprendizaje automático.

Aplicación: similitud coseno

Al comparar dos embeddings, lo relevante suele ser la dirección y no la magnitud. Dos textos sobre el mismo tema deberían considerarse próximos aunque uno sea mucho más largo que el otro, y la longitud del vector tiende a reflejar esa extensión antes que el contenido.

La similitud coseno descarta la magnitud por construcción:

cosθ=uvuv=u^v^\cos\theta = \frac{\vec{u} \cdot \vec{v}}{\lVert \vec{u} \rVert\,\lVert \vec{v} \rVert} = \hat{u} \cdot \hat{v}

Los extremos son arrastrables. Las flechas interiores son el par normalizado, sobre la circunferencia unidad.

‖λu‖ = 3.16 · ‖v‖ = 2.69

cos θ = 0.6459 · θ = 49.8°

λ cambia ‖λu‖ pero nunca cos θ: es exactamente la propiedad que se busca al comparar embeddings.

El deslizador λ\lambda reescala u\vec{u}. Su norma cambia, su flecha se alarga o se acorta, y el valor de cosθ\cos\theta permanece idéntico. La verificación algebraica es inmediata:

(λu)vλuv=λ(uv)λuv\frac{(\lambda\vec{u}) \cdot \vec{v}}{\lVert \lambda\vec{u} \rVert \lVert \vec{v} \rVert} = \frac{\lambda\,(\vec{u} \cdot \vec{v})}{|\lambda|\,\lVert \vec{u} \rVert \lVert \vec{v} \rVert}

Para λ>0\lambda > 0 los factores se cancelan. Esa invariancia frente al escalado es la propiedad que hace de la similitud coseno la medida habitual en recuperación semántica.

def cosine(u, v): return (u @ v) / (np.linalg.norm(u) * np.linalg.norm(v)) cosine(v, 100 * v) # 1.0: misma dirección, magnitudes incomparables

Cuando los vectores se normalizan de antemano, la similitud se reduce a un producto escalar, y la comparación de un vector contra un corpus completo pasa a ser un único producto matricial:

E = E / np.linalg.norm(E, axis=1, keepdims=True) # filas unitarias sims = E @ q # similitud contra la consulta q

Esa es la operación que ejecuta una base de datos vectorial en cada consulta.

Aplicación: la tasa de aprendizaje

El descenso por gradiente es, en su forma básica, producto por un escalar:

wwαf(w)\vec{w} \leftarrow \vec{w} - \alpha\,\nabla f(\vec{w})

El gradiente indica la dirección de máximo crecimiento; el escalar α\alpha, la tasa de aprendizaje, decide cuánto se avanza en sentido contrario. Es un único número, y de él depende que el entrenamiento converja.

Las elipses son las curvas de nivel de f. Lo único que cambia es α.

Converge

estable mientras α < 0.50

‖w‖ tras 24 pasos = 0.011

Con f(w)=12(w12+4w22)f(\vec{w}) = \tfrac{1}{2}(w_1^2 + 4w_2^2), cada paso contrae la coordenada ii por un factor 1αci1 - \alpha c_i, donde cic_i es la curvatura en ese eje. El método converge si y solo si 1αci<1|1 - \alpha c_i| < 1 para toda coordenada, es decir

0<α<2maxici0 < \alpha < \frac{2}{\max_i c_i}

En la figura ese umbral vale 0.50.5. Tres regímenes son observables: con α\alpha pequeño el avance es correcto pero lento; cerca del umbral aparece la oscilación característica en zigzag a lo largo del eje de mayor curvatura; por encima, la iteración diverge.

grad = np.array([0.5, -1.2]) w = np.array([2.0, 1.0]) lr = 0.1 w = w - lr * grad # un paso

La dificultad práctica es que α\alpha se elige una vez y la curvatura varía según la dirección. Esa disparidad —el cociente entre la curvatura mayor y la menor, que es el número de condición de la Hessiana— es la que fuerza a tomar un α\alpha pequeño y hace lenta la convergencia. Los métodos adaptativos como Adam sustituyen el escalar único por un factor por coordenada, y toda su ventaja procede de ahí.


Ejercicio. Verificar que cosine(v, -v) vale 1-1 y explicar por qué la invariancia del coseno frente al escalado exige λ>0\lambda > 0. Determinar después, para f(w)=12(w12+4w22)f(\vec{w}) = \tfrac{1}{2}(w_1^2 + 4w_2^2), el valor de α\alpha que anula en un solo paso la coordenada de mayor curvatura, y observar qué le ocurre a la otra.