Paquetes

  • numpy
  • pandas
  • matplotlib
  • scikit-learn
import numpy as np
import pandas as pd
import random
import matplotlib.pyplot as plt
from df.display_algebra import draw_covariance_ellipse

Descomposición matricial

¿Recuerdas los factores primos? recuerdas que un número puede ser obtenido a partir de sus factores primos, por ejemplo:

\(42 = 2 \times 3 \times 7\)

Este proceso se conoce como descomposición matemática. Como el título de esta sección lo indica, las matrices también se pueden descomponer de esta manera en objetos relativamente más simples.

Estas descomposiciones pueden ser usadas para diversos fines:

  • Análisis de datos representados como matrices
  • Ejecución eficiente de operaciones matriciales

En ejemplos más tangibles: podemos usar descomposiciones para sistemas de recomendación, motores de búsqueda, compresión de imágenes, reconocimiento facial, y otras aplicaciones.

Puntos fijos

Supongamos que tenemos un punto \(x\), tal que si le aplicamos una función \(f\), el resultado es \(x\). Es decir si:

\(f(x) = x\)

Podemos decir que \(x\) es un punto fijo de \(f\). Por ejemplo, supongamos que \(f(x) = x^2\):

  • Cuando \(x = 0\), \(f(x) = 0\)
  • Cuando \(x = 1\), \(f(1) = 1\)

Tanto \(x = 0\) como \(x = 1\) son considerados puntos fijos de \(x^2\).

🤔 existen dos tipos de puntos fijos: estables y no estables. Se dice que un punto fijo es estable si cualquier valor en su vecindad hace que la función se acerque al punto fijo, mientras que se dice que uno no es estable si cualquier valor en su vecindad hace que la función se aleje de este.

Podemos obtener los puntos fijos de una función de manera ingenua eligiendo un valor aleatorio \(x\) y aplicando sucesivamente la regla \(x_t=f(x_{t-1})\) hasta llegar a un punto estable, ese es tu punto fijo:

def find_fixed_point(f, r, eps=1e-5):

    fr = f(r)
    history = [(r, fr)]
    # ¿Ya dejamos de movernos?
    while np.abs(fr - r) > eps and np.abs(fr) < np.inf:
        r = fr
        fr = f(r)
        history.append((r, fr))

    return fr, np.array(history)
def function(x):
    return np.cos(x)


r = np.random.uniform(-5, 5)  # random starting point
fixed, history = find_fixed_point(function, r)

print(f"Punto fijo {fixed:0.5}")
print(f"f({fixed:0.5}) = {function(fixed):0.5}")
Punto fijo 0.73908
f(0.73908) = 0.73909

Eigen.. ¿qué?

De cierto modo, cada matriz representan una función conocida como aplicación lineal, dentro de las aplicaciones lineales existen las [transformaciones lineales] que transforman elementos entre espacios vectoriales.

Eigenvalores y eigenvectores

Si bien las transformaciones lineales tienen puntos fijos, existen valores análogos, y aún más interesantes para analizar de una matriz:

  • Eigenvectores
  • Eigenvalores

eigen = propio (o característico).

Método de las potencias

Vamos a tomar una matriz cuadrada \(A\) y aplicarla a un vector aleatorio \(\vec{x}\), al resultado volverle a aplicar \(A\) y luego volverle a aplicar \(A\)... es decir, calculamos:

\(AAA \dots AA\vec{x}\) \(A^n\vec{x}\)

En la práctica esto causaría que el resultado crezca hasta infinito o colapse a cero. Podemos normalizar el resultado hacer algo para contrarrestar este efecto:

\(\begin{equation}x_n = \frac{Ax_{n-1}}{\|Ax_{n-1}\|_\infty}\end{equation}\)

El resultado a cada paso será forzado a ser un vector unitario usando al norma mínima \(L_\infty\). Finalmente, a este método lo vamos a conocer como el método de las potencias o power iteration method.

def power_iterate(A, x, n):
    for i in range(n):
        x = A @ x  # multiply
        x = x / np.linalg.norm(x, np.inf)  # normalize

    return x / np.linalg.norm(x, 2)

Comencemos con nuestra matriz \(A\):

A = np.random.normal(0, 1, (2, 2))
print(A)
[[ 0.71243771  1.2818496 ]
 [ 1.32566962 -0.61522188]]

Seguimos con un vector cualquiera:

cualquiera = np.random.normal(0, 3, (2,))
print(cualquiera)
[0.99530246 0.09380106]
# El vector resultante -casi siempre- es el mismo
eigenvector = power_iterate(A, cualquiera, n=500)
print(eigenvector)
[0.84862822 0.52898974]

Este vector es conocido como el eigenvector principal, y es una característica de la matriz.

La matriz \(A\) toma nuestro vector y la única transformación que le hace es escalarlo (hacerlo más grande) sin rotaciones ni sesgos.

Para calcular el factor de escalamiento podemos...

ratio = (A @ eigenvector) / eigenvector
eigenvalue = ratio[0]  # Todos los elementos tienen el mismo valor.
print(eigenvalue)
1.5114746452729027

El escalar que ves más arriba es conocido como el eigenvalor principal, llamémoslo \(\lambda\) por el momento, y satisface esta ecuación:

\(A\vec{x}_i = \lambda_i\vec{x}_i\)

uno = A @ eigenvector
dos = eigenvector * eigenvalue

np.allclose(uno, dos)
True

Cada vector \(\vec{x}_i\) que satisfaga la ecuación es un eigenvector y cada \(\lambda_i\) que cumpla esta ecuación es un eigenvalue. Estos elementos vienen en pares.

Otros eigenvalores

El método de las potencias nos ayuda a encontrar un solo vector, sin embargo pueden existir múltiples pares de eigen cosas, para encontrar los otros, podemos seguir este algoritmo:

Encontrar más valores...

Mientras tanto en NumPy...

evals, evecs = np.linalg.eig(A)
print(evals[0])
print(evecs[:, 0])
1.5114746452729062
[0.84862822 0.52898974]

El espectro... eigenspectro.

A la secuencia eigenvalores ordenados por su valor absoluto se le conoce como el eigenespectro de una matriz; esto nos puede ayudar a encontrar versiones simplificadas de nuestras matrices.

Supongamos que tenemos un dataset de 18K jugadores de fútbol:

fifa19 = pd.read_csv("assets/06/fifa19.csv")
# https://fifauteam.com/fifa-19-attributes-guide/

attributes = [
    "Acceleration",
    "SprintSpeed",  # Pace
    "Finishing",
    "LongShots",
    "Penalties",
    "Positioning",
    "ShotPower",
    "Volleys",  # Shooting
    "Crossing",
    "Curve",
    "FKAccuracy",
    "LongPassing",
    "ShortPassing",
    "Vision",  # Passing
    "Agility",
    "Balance",
    "BallControl",
    "Composure",
    "Dribbling",
    "Reactions",  # Dribbling
    "HeadingAccuracy",
    "Interceptions",
    "Marking",
    "SlidingTackle",
    "StandingTackle",  # Defending
    "Aggression",
    "Jumping",
    "Stamina",
    "Strength",  # Physical
    # 'GKDiving', 'GKHandling', 'GKKicking', 'GKPositioning', 'GKReflexes', # Goalkeeping
]

player_stats = fifa19[attributes].copy().fillna(axis="index", method="backfill")
player_stats.head()
Acceleration SprintSpeed Finishing LongShots Penalties Positioning ShotPower Volleys Crossing Curve ... Reactions HeadingAccuracy Interceptions Marking SlidingTackle StandingTackle Aggression Jumping Stamina Strength
0 91.0 86.0 95.0 94.0 75.0 94.0 85.0 86.0 84.0 93.0 ... 95.0 70.0 22.0 33.0 26.0 28.0 48.0 68.0 72.0 59.0
1 89.0 91.0 94.0 93.0 85.0 95.0 95.0 87.0 84.0 81.0 ... 96.0 89.0 29.0 28.0 23.0 31.0 63.0 95.0 88.0 79.0
2 94.0 90.0 87.0 82.0 81.0 89.0 80.0 84.0 79.0 88.0 ... 94.0 62.0 36.0 27.0 33.0 24.0 56.0 61.0 81.0 49.0
3 57.0 58.0 13.0 12.0 40.0 12.0 31.0 13.0 17.0 21.0 ... 90.0 21.0 30.0 15.0 13.0 21.0 38.0 67.0 43.0 64.0
4 78.0 76.0 82.0 91.0 79.0 87.0 91.0 82.0 93.0 85.0 ... 91.0 55.0 61.0 68.0 51.0 58.0 76.0 63.0 90.0 75.0

5 rows × 29 columns

Calculando la matriz de covarianza y vemos los eigenvectores y los valores correspondientes a cada uno de ellos:

players_cov = np.cov(player_stats.values, rowvar=False)
eigs, eigv = np.linalg.eig(players_cov)

order = np.argsort(eigs)
eigs, eigv = eigs[order], eigv[order]

fig = plt.figure()
ax = fig.gca()
ax.bar(np.arange(len(eigs)), list(reversed(eigs)))
ax.set_xlabel("Eigenvalor")
ax.set_ylabel("Amplitud")
ax.set_title("Eigenspectrum of the FIFA 19 dataset")
Text(0.5, 1.0, 'Eigenspectrum of the FIFA 19 dataset')

png

Los eigenvectores nos ayudan a identificar los ejes a lo largo de los que nuestro dataset varía:

def print_vector(vector):
    properties = []
    for attribute, value in zip(attributes, vector):
        properties.append(f"{attribute:<20s} {value:+.03}")
    results = "\n".join(properties)
    print(results)


print("Eigenvector 1")
print_vector(eigv[0])
print()
print("Eigenvector 2")
print_vector(eigv[0])
Eigenvector 1
Acceleration         -0.202
SprintSpeed          -0.0314
Finishing            -0.0103
LongShots            +0.0974
Penalties            -0.0264
Positioning          -0.171
ShotPower            -0.223
Volleys              -0.104
Crossing             +0.0122
Curve                -0.139
FKAccuracy           -0.239
LongPassing          +0.182
ShortPassing         +0.49
Vision               +0.249
Agility              +0.12
Balance              -0.6
BallControl          -0.000102
Composure            -0.0518
Dribbling            -0.00559
Reactions            -0.038
HeadingAccuracy      -0.055
Interceptions        +0.0145
Marking              -0.104
SlidingTackle        -0.0457
StandingTackle       +0.0985
Aggression           +0.0528
Jumping              -0.156
Stamina              -0.0677
Strength             +0.0961

Eigenvector 2
Acceleration         -0.202
SprintSpeed          -0.0314
Finishing            -0.0103
LongShots            +0.0974
Penalties            -0.0264
Positioning          -0.171
ShotPower            -0.223
Volleys              -0.104
Crossing             +0.0122
Curve                -0.139
FKAccuracy           -0.239
LongPassing          +0.182
ShortPassing         +0.49
Vision               +0.249
Agility              +0.12
Balance              -0.6
BallControl          -0.000102
Composure            -0.0518
Dribbling            -0.00559
Reactions            -0.038
HeadingAccuracy      -0.055
Interceptions        +0.0145
Marking              -0.104
SlidingTackle        -0.0457
StandingTackle       +0.0985
Aggression           +0.0528
Jumping              -0.156
Stamina              -0.0677
Strength             +0.0961

Estos dos vectores son las direcciones con mayor varianza en nuestro dataset... difícil de ver e imaginar porque estamos hablando de un espacio de 29 dimensiones.

PCA

Lo que acabamos de hacer acá arriba se conoce como análisis de componentes principales o principal component análisis (PCA).

Los componentes principales de un dataset son los eigenvectores de su matriz de covarianza. Usando este análisis de componentes principales podemos encontrar una proyección de bajas dimensiones para nuestros datos si proyectamos nuestro dataset en los vectores principales:

def pca(data, n_components):
    cov = np.cov(data, rowvar=False)
    eigs, eigv = np.linalg.eig(cov)

    order = np.argsort(eigs)
    eigs, eigv = eigs[order], eigv[order]
    eig_order = np.argsort(np.abs(eigs))
    components = []
    for i in range(n_components):
        components.append(data @ eigv[eig_order[-i - 1]])
    return np.stack(components).T
components = pca(player_stats.values, 2)
player_ids = [158023, 20801, 190871, 238789, 231747, 194765, 208722, 209331] + [
    156519,
    186302,
    192041,
    187478,
    167524,
    213445,
    217167,
    224103,
    173434,
    186551,
    187208,
    199569,
    190485,
    233260,
    187705,
    186979,
    167532,
    212377,
    183743,
    139229,
    203283,
    171377,
    222645,
    192015,
    #    186513, 232316, 239545, 187722, 237498, 169415, 202786, 192046,
    #    235368, 235123, 236434, 237978, 246188, 243157, 236842, 240177,
    #    237657, 244349, 244611, 193164, 242823, 210426, 244743, 233535,
    244615,
    212475,
    246294,
    244831,
    240107,
    239548,
    244637,
]


index = fifa19[fifa19["ID"].isin(set(player_ids))].index
comp = components[index]
len(comp), len(index), len(set(player_ids)), len(player_ids)
(39, 39, 39, 39)
fifa19[fifa19["Name"].str.contains("Mané")]
Unnamed: 0 ID Name Age Photo Nationality Flag Overall Potential Club ... Composure Marking StandingTackle SlidingTackle GKDiving GKHandling GKKicking GKPositioning GKReflexes Release Clause
58 58 208722 S. Mané 26 https://cdn.sofifa.org/players/4/19/208722.png Senegal https://cdn.sofifa.org/flags/136.png 86 87 Liverpool ... 80.0 42.0 42.0 38.0 10.0 10.0 15.0 7.0 14.0 €102.7M
1920 1920 216749 Carlos Mané 24 https://cdn.sofifa.org/players/4/19/216749.png Portugal https://cdn.sofifa.org/flags/38.png 75 81 Sporting CP ... 73.0 38.0 34.0 33.0 16.0 14.0 10.0 9.0 11.0 €19.8M

2 rows × 89 columns

fig = plt.figure(figsize=(10, 5), dpi=100)
ax = fig.gca()

ax.scatter(comp[:, 0], comp[:, 1])
for i, player_id in enumerate(player_ids):
    ax.text(
        comp[i, 0],
        comp[i, 1],
        fifa19[fifa19["ID"] == player_id]["Name"].values[0],
        fontsize=6,
    )

ax.set_xlabel("Componente 1")
ax.set_ylabel("Componente 2")
ax.set_title("Componentes principales FIFA19")
Text(0.5, 1.0, 'Componentes principales FIFA19')

png

Proyecciones de pocas dimensiones

Las proyecciones de pocas dimensiones son herramientas de vital importancia en la ciencia de datos exploratoria. PCA es una forma de lograr esta tarea, sin embargo, no es la única. Existe otro algoritmo llamado t-SNE que, al igual que PCA nos permite descomponer nuestra información para proyectar nuestro dataset:

from sklearn.manifold import TSNE

t_reduce = TSNE()
xy_2d = t_reduce.fit_transform(player_stats.values)[index]
fig = plt.figure(figsize=(10, 5), dpi=100)
ax = fig.add_subplot(1, 1, 1)

ax.scatter(xy_2d[:, 0], xy_2d[:, 1])
for i, player_id in enumerate(player_ids):
    ax.text(
        xy_2d[i, 0],
        xy_2d[i, 1],
        fifa19[fifa19["ID"] == player_id]["Name"].values[0],
        fontsize=6,
    )


ax.set_xlabel("t-SNE componente 1")
ax.set_ylabel("t-SNE componente 2")
ax.set_title("t-SNE transformation of the whisky dataset")
Text(0.5, 1.0, 't-SNE transformation of the whisky dataset')

png

Otras propiedades de las matrices

Traza

La traza \(\text{tr}\) de una matriz cuadrada es la suma de los elementos en la diagonal:

\(\text{tr}(A) = a_{1,1} + a_{2,2} + \dots + a_{n,n}\)

Esto es igual que la suma de los eigenvalores:

\(\text{tr}(A) = \sum_{i=1}^n \lambda_i\)

Determinante

El determinante \(\text{det}\) de una matriz es igual a la multiplicación de los eigenvalores:

\(\text{det}(A) = \prod_{i=1}^n \lambda_i\)

Cuando alguno de los eigenvalores de la matriz es \(0\) el determinante es \(0\), lo cual tiene implicaciones para la matriz.

⚠️ Hay matrices con eigenvalores complejos, sin embargo... no las veremos aquí.

Inversion de matrices

Recordarás las operaciones básicas sobre las matrices:

  • Multiplicación por un escalar: \(cA\)
  • Suma matricial: \(A + B\)
  • Multiplicación matricial: \(AB\)
  • Transposición: \(A^T\)

Ves la suma y la multiplicación y tal vez te preguntes... ¿hay división?

La operación más parecida es la inversa de una matriz, que nos permite:

  • \(A^{-1}(A\vec{x}) = \vec{x}\)
  • \(A^{-1}A = I\)
  • \((A^{-1})^{-1} = A\)

Sin embargo esta operación, (la inversa) no está siempre definida para todas las matrices. En particular, la inversa no está definida para matrices no cuadradas y con determinante = \(0\).

Una matriz es llamada singular si su determinante es 0 (y por tanto no invertible) y es llamada no singular si es posible invertirla.

Algoritmo

Hay diversas maneras de calcular la inversa de una matriz, incluyendo un algoritmo recursivo; este algoritmo recursivo funciona, pero solo para matrices pequeñas. Más adelante hablaremos de una forma efectiva para invertir matrices.

¿Qué problemas resuelve la inversión de matrices?

Sistemas lineales

Imagina que tenemos la siguiente matriz:

\[ A = \begin{bmatrix} 0.5 & 1.0 & 2.0\\ 1.0 & 0.5 & 0.0\\ 0.6 & 1.1 & -0.3\\ \end{bmatrix} \]

Esta matriz representa una aplicación lineal que opera sobre vectores tridimensionales \(\vec{x}\), y que produce vectores 3D \(\vec{y}\). Cada uno de las entradas de este vector es una suma ponderada de las entradas del vector \(\vec{x}\):

\[ y_1 = 0.5x_1 + 1.0x_2 +2.0x_3\\ y_2 = 1.0x_1 +0.5x_2 +0.0x_3\\ y_3 = 0.6x_1 + 1.1x_2 - 0.3x_3 \]
A = np.array([[0.5, 1.0, 2.0], [1.0, 0.5, 0.0], [0.6, 1.1, -0.3]])
x = np.array([1, -1, 1])

print(A @ x)
[ 1.5  0.5 -0.8]

Viéndolo desde otra perspectiva, esta es la forma en la que sistemas de ecuaciones simultaneas pueden ser representadas de la siguiente forma:

\(A\vec{x} = \vec{y}\)

Esto es conocido como un sistema de ecuaciones lineares.

Partiendo de esto, y de que ahora sabemos que la inversa de una matriz existe, podríamos pensar que la solución es bastante trivial:

\[ A^{-1}A\vec{x}=A^{-1}\vec{y} \\ I \vec{x} = A^{-1} \vec{y} \\ \vec{x} = A^{-1}\vec{y} \]

Recuerda que la inversa de una matriz solamente está definida si la matriz es cuadrada

En la práctica, los sistemas lineales casi nunca son resueltos invirtiendo directamente la matriz; hay problemas de estabilidad numérica así como de complejidad algorítmica que lo hacen un procedimiento inviable.

Soluciones aproximadas

En lugar de tratar de invertir de una vez la matriz, resuelven el problema de forma iterativa, una de las formas es utilizando optimización, algo que veremos en el futuro.

Descomposición en valores singulares

La eigendescomposición que vimos hace poco solamente funciona para matrices cuadradas, sin embargo no siempre nuestros problemas van a venir en esta forma; es aquí en donde entra otra forma de descomponer matrices:

La singular value decomposition (SVD) es una forma general de descomponer cualquier matriz \(A\). Es una de las grandes herramientas del álgebra lineal.

SVD descompone una matriz en tres partes:

\(A = U \Sigma V\)

En donde:

  • \(A\) es la matriz inicial de \(m \times n\),
  • \(U\) es una matriz ortonormal de \(m \times m\), conocida como vectores singulares izquierdos,
  • \(V\) es una matriz ortonormal de \(n \times n\), conocida como vectores singulares derechos,
  • \(\Sigma\) es una matriz diagonal de \(m \times n\), la diagonal son los valores singulares

Una matriz ortonormal \(U\) es una matriz que cumple que \(U^{-1} =U^T\) y que las columnas y filas tienen norma unitaria.

La diagonal de \(\Sigma\) son los valores singulares que son parecidos a los eigenvectores sin embargo no son lo mismo. Por ejemplo, los valores singulares siempre son números positivos.

En NumPy...

A = np.array([[3, 2, 3, 2], [2, 9, 4, 2], [4, 1, 9, 8]])


u, sigma, v = np.linalg.svd(A)
true_sigma = np.zeros_like(A, dtype=np.float64)
np.fill_diagonal(true_sigma, sigma)
A_reconstructed = u @ true_sigma @ v


print(u)
print()
# print(sigma)
print(true_sigma)
print()
print(v)

print()
print(A_reconstructed)
[[-0.32057561  0.02380932 -0.94692365]
 [-0.52903899  0.82473129  0.19984007]
 [-0.78571561 -0.56502338  0.25179268]]

[[15.22220181  0.          0.          0.        ]
 [ 0.          7.6735174   0.          0.        ]
 [ 0.          0.          1.54974283  0.        ]]

[[-0.33915378 -0.4065258  -0.66674476 -0.52455973]
 [-0.07026804  0.89987114 -0.22347735 -0.36790245]
 [-0.92526324  0.10098838  0.14500694  0.335652  ]
 [ 0.15467725  0.12153212 -0.69604762  0.69052343]]

[[3. 2. 3. 2.]
 [2. 9. 4. 2.]
 [4. 1. 9. 8.]]

SVD 🤝 eigendescomposición

La SVD es:

  • Obtener los eigenvectores de \(A^T A\) para obtener \(U\)
  • Obtener los eigenvectores de \(A A^T\) para obtener \(V\)
  • Calcular la raíz cuadrada de los eigenvalores de \(A^T A\)

Aplicaciones de las descomposiciones

Una vez que tenemos la descomposición hay operaciones que se vuelven triviales:

\(A = U\Sigma V\) \(A^{-1} = V^T \Sigma^\dagger U^T\)

  • \(\Sigma^\dagger\) es \(\text{diag}(1.0/\Sigma_{ii})^T\)

Pseudo-inversa...

\(\dots\)

Los valores singulares

Como te imaginarás, los valores singulares capturan algunos aspectos esenciales de una matriz.

  • Se dice que una matriz es de rango completo si su número de valores singulares distintos de cero es igual al número de elementos en la diagonal de la matriz; de otro modo se trata de una matriz de rango deficiente.

  • El número condicional de una matriz es la relación entre su valor singular más grande y el más pequeño; este número nos dice qué tan sensible es la aplicación lineal a cambios, es decir en la operación \(A\vec{x} = \vec{y}\) nos dice si \(\vec{y}\) cambia mucho o poco cuando realizamos pequeños cambios a \(\vec{x}\). Se relaciona con los conceptos de matrices bien condicionadas y mal condicionadas.

Los valores singulares y la singularidad

Habíamos dicho que una matriz es singular si es no invertible y tiene \(\text{det}(A) = 0\), esta definición es binaria, los conceptos de rango y número condicional nos ayudan a convertir esta distinción en un gradiente, usando estos dos números podemos responder la pregunta "¿Qué tan singular es la matriz?"

Aplicaciones de las descomposiciones

Operaciones avanzadas.

Ahora que podemos descomponer una matriz, podemos plantearnos el realizar operaciones como:

  • Elevar una matriz a una potencia fraccionaria \(A^{\frac{1}{3}}\)
  • Invertir una matriz \({A^{-1}}\)
  • Calcular el logaritmo de una matriz \(\ln{A}\)

La forma de hacerlo es "sencilla": ignoramos \(U\) y \(V\) y se le aplica la operación en cuestión a todos los elementos de \(\Sigma\).

Esferización (sphering)

El esferización (mas comúnmente conocido como blanqueamiento) consiste en remover todas las correlaciones lineales dentro de un dataset, es una forma de normalizar un conjunto de datos que se realiza antes de realizar un análisis.

Dado una matriz \(X\):

\(X^* =(X-\mu)\Sigma^{-\frac{1}{2}}\)

en donde \(\vec{\mu}\) es el vector promedio y \(\Sigma\) es la matriz de covarianza.

El resultado de aplicar esta operación centra el dataset y modifica el dataset para que la matriz de covarianza sea unitaria.

def apply_sphering(x):
    center_x = x - np.mean(x, axis=0)
    u, sigma, v = np.linalg.svd(np.cov(center_x, rowvar=False))

    # La magia
    sigma_inv_sqr = v.T @ np.diag(1.0 / np.sqrt(sigma)) @ u.T
    shphere_x = center_x @ sigma_inv_sqr

    return shphere_x
X = np.random.normal(0, 1, (200, 2)) @ np.array([[0.1, 0.5], [-0.9, 1.0]]) + np.array(
    [2, 3]
)
X_sphered = apply_sphering(X)

fig = plt.figure(figsize=(10, 10))
ax = fig.gca()

ax.scatter(X[:, 0], X[:, 1], c="#99d8c9", label="Original")
ax.scatter(X_sphered[:, 0], X_sphered[:, 1], c="#feb24c", label="Esferada")

for dataset in [X, X_sphered]:
    for std in [0.5, 1, 2]:
        draw_covariance_ellipse(ax, dataset, std)

for one, two in zip(X, X_sphered):
    ax.plot([one[0], two[0]], [one[1], two[1]], alpha=0.2)

# ax.set_xlim(-6, 6)
# ax.set_ylim(-6, 6)

ax.axhline(0)
ax.axvline(0)
ax.set_aspect(1.0)
ax.legend()
ax.set_title("Esferización de un dataset")
Text(0.5, 1.0, 'Esferización de un dataset')

png

Aproximaciones de bajo rango

¿Recuerdan las matrices dispersas de la sesión pasada?

Feregrino Alma Benito ... Terry Destiny
La Puerta Negra \({\bf 1}\) \(0\) \(0\) \(0\) \(0\)
Flux \(0\) \({\bf 1}\) \(0\) \({\bf 1}\) \(0\)
La mesa del rincón \({\bf 1}\) \(0\) \(0\) \(0\) \(0\)
Andar conmigo \({\bf 1}\) \({\bf 1}\) \(0\) \({\bf 1}\) \(0\)
... ...
Dark Horse \(0\) \({\bf 1}\) \({\bf 1}\) \(0\) \({\bf 1}\)

Datasets como este tipo son el pan de cada día de compañías como Spotify, Netflix y Amazon. Lo que siempre están tratando de hacer es encontrar nuevos objetos para recomendarles a sus usuarios.

Como vimos anteriormente, la gran mayoría de los usuarios han escuchado/visto/comprado una cantidad mínima de canciones/películas/productos.

Centrándonos en el ejemplo de las canciones, una forma simplista de verlos podríamos encapsular a los usuarios dentro de grupos como "el fan de los tigres del norte", "el que solo escucha canciones de my chemical romance", "el fan de música de tienda de ropa"... sin embargo la realidad no funciona así, un modelo más realista es uno que representa a los usuarios como una suma ponderada:

\(\text{usuario} = 0.2 \times \text{tigres del norte} + 0.7 \times \text{mcr} + 0.1 \times \text{música de zara}\)

Esto nos permite usar solamente una parcialidad de la información de los usuarios para realizar recomendaciones; usamos la SVD para realizar esto.

Podemos encontrar una aproximación de bajo rango truncando la SVD y manteniendo solamente una fracción, digamos \(K\), de los valores singulares, y las primeras \(K\) columnas y filas de \(U\) y \(V\) respectivamente.

Esta es una forma de reducción dimensional.

[Ver recursos al final]


Resources

Sitios web