26 feb 2021

Regresión Logística Multinomial

Algunas veces es necesario hacer clasificación para más de dos clases. Quizas se quiere clasificar tres formas de sentimientos (positivo, neutral o negativo). Esto se podría hacer analizando el contenido del habla y asignado un etiquetado semático a cada una de las palabras para poder valores el sentimiento del habla, sin embargo en esta publicación no vamos a hablar de esto por ahora. Vamos a dedicar la atención a la regresión logística multinomial o softmax.

En estas situaciones en donde es necesario hacer una clasificación para más de dos clases, se puede hacer uso de la regresión logística multinomial, o tambien regresión softmax. En este tipo de regresión la variable objetivo tiene un rango que varia sobre un conjunto de más de dos clases; el objetivo aquí será determinar cuá es la probabilidad de $y$ de pertenecer a cada una de las clases potenciales $c\in C$, $P(y=c\;|\;x)$.

La regresión logística multinomial clasifica usando una generalización de la función sigmoide, conocida como la función softmax, para calcular la probabilidad $P(y=c\;|\;x)$. La función softmax toma un vector $z=[z_1, z_2, \dots, z_k]^{\top}$ de $k$ valores arbitrarios y los mapea en una distribucción de probalicada, con cada valore en el rango $(0, 1)$, y todos los valores sumando uno. Como la función sigmoide. es una función exponencial.

Para un vector $z$ de dimensionalidad $k$, la función softmax es definida como:

$$s(z_i)=\frac{e^{z_i}}{\sum_{j=1}^{k}e^{z_{j}}},\; 1\leq i\leq k.$$

Así, la función softmax de un vector $z=[z_1, z_2, \dots, z_k]^{\top}$ es por lo tanto una función vectorial:

$$s(z)=\left[\frac{e^{z_1}}{\sum_{j=1}^{k}e^{z_{1}}}, \frac{e^{z_2}}{\sum_{j=1}^{k}e^{z_{2}}},\dots, \frac{e^{z_k}}{\sum_{j=1}^{k}e^{z_{k}}}\right]$$

El denominador $\frac{e^{z_i}}{\sum_{j=1}^{k}e^{z_{j}}}$ es usado para normalizar todos los valores en probabilidades. Por ejemplo:

In [1]:
import numpy as np
from scipy.special import softmax
In [2]:
z = np.array([0.6, 1.1, -1,5, 1.2, 3.2, -1.1])
In [3]:
softmax(z)
Out[3]:
array([0.01002305, 0.01652522, 0.00202362, 0.81638616, 0.01826319,
0.13494772, 0.00183105])

Otra vez como en la función sigmoide, la entrada de la función softmax puede ser el producto punto entre un vector de pesos $w=(w_0, \dots, w_n)$ y un vector $x=(1, x_1,\dots, x_n$. Pero ahora ese necesario separar el vector de pesos para cada una de las clases.

$$P(y=c\;| \;x)=\frac{e^{w_c^\top x}}{\sum_{j=1}^{k}e^{w_j^\top x}}$$

Como la función sigmoide, la función softmax tiene la propiedad de transformar los valores hacía $0$ o $1$. Pos lo tanto , si una de las entradas es más grande que los otros, tenderá a aumentar su probabilidad hacia $1$, y suprime las probabilidades de las entradas más pequeñas.

Aplicaciones de la regresión logística multinomial

Para la clasificación de los datos de entrada es necesario definir una función que depende de la observación $x$ y de la potencial clase $c$. Para esto se usará la notación $f_i(c, x)$, que indicará el atributo $i$ para una clases particular $c$ dado por la observación $x$.

En clasificación binaria, un peso positivo en una característica apunta hacia $y = 1$ y un peso negativo hacia $y = 0$, pero en la clasificación multiclase una característica podría ser evidencia a favor o en contra de una clase individual.

Veamos algunas características de muestra para algunas tareas de PNL para ayudar a comprender este uso quizás poco intuitivo de características que son funciones tanto de la observación $x$ como de la clase $c$.

Supongamos que estamos haciendo una clasificación de texto y, en lugar de una clasificación binaria, nuestra tarea es asignar una de las 3 clases A, B o C (neutral) a un documento. Ahora, una función relacionada con los signos de exclamación puede tener un peso negativo para C documentos y un peso positivo para documentos A o B:

$$f_1(C, x)=\begin{cases}1 & \mbox{ si } ! \notin doc \\ 0 & \mbox{ si } ! \in doc \end{cases},\;w_1 = -4.5$$
$$f_2(A, x)=\begin{cases}1 & \mbox{ si } ! \notin doc \\ 0 & \mbox{ si } ! \in doc \end{cases},\;w_1 = 2.6$$
$$f_3(B, x)=\begin{cases}1 & \mbox{ si } ! \notin doc \\ 0 & \mbox{ si } ! \in doc \end{cases},\;w_1 = 1.3$$

¿Cómo aprende las regresión multinomial logística?

La regresión logística multinomial tiene una función de pérdida ligeramente diferente a la regresión logística binaria porque utiliza el clasificador softmax en lugar del sigmoide. La función de pérdida para un solo ejemplo $x$ es la suma de los registros de las $k$ clases de salida:

$$L_{CE}(\hat{y}, y)=-\sum_{h=1}^{k}\mathbb{1}_{\{y=k\}}\log P(y=k\;|\;x)=-\sum_{k=1}^{k}\mathbb{1}_{\{y=k\}}\log \frac{e^{w_{k}\cdot x + b_{k}}}{\sum_{j=1}^{k}e^{w_j\cdot x + b_{j}}}$$

La expersión $\mathbb{1}_{\{y=k\}}$ toma el valor de uno cuando la condición en las llaves es verdadera y cero en cualquier otro caso.

El gradiente para una muestra es muy similar a el gradiente para la regresión logistica, aunque no se mostrará aquí la derivación. Es la diferencia entre el valor de la clase verdadera $k$ y la probabilidad que el clasificador genera para la clase $k$, ponderada por el valor de la entrada $x_{k}$:

$$\frac{\partial L_{CE}}{\partial w_{k}} = -(\mathbb{1}_{\{y=k\}}-P(y=k\;|\;x))x_{k}=-\left(\mathbb{1}_{\{y=k\}} - \frac{e^{w_{k}\cdot x + b_{k}}}{\sum_{j=1}^{k}e^{w_j\cdot x + b_{j}}}\right)x_k$$

Implementación con Tensorflow - MNIST

MNIST es un conjunto de datos de digitos escritos a mano que se en muchos ejemplos introductorios al machine learning. El conjunto de datos contiene 60000 ejemplos para entrenamiento y 10000 ejemplos para testeo. El tamaño de los digitos ha sido normalizado y la imagen centrada (28 x 28 pixeles) con valores de 0 a 1. Por simplicidad, cada imagen ha sido convertido es una matriz númerica 1-D de 784 características (28 x 28). Para más información http://yann.lecun.com/exdb/mnist/.

Para nuestro ejemplo de regresión logística multinomial usaremos TensorFlow V2 y se implementará a bajo nivel para entender los detalles que hay en el proceso de entrenamiento. Los detalles de este ejemplo se puede encontrar en TensorFlow Examples - GitHub

In [4]:
import tensorflow as tf
import numpy as np

A continuación se definen las características generales de los datos:

In [5]:
num_classes = 10
num_features = 784

También se definen los parámetros de entrenamiento:

In [6]:
learning_rate = 0.01
training_steps = 1000
batch_size = 256

Lectura de los datos

Se cargan los datos y se identifican el conjunto de entrenamiento y el de testeo:

In [7]:
from tensorflow.keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

Para visualizar algunos ejemplos hacemos uso de matplotlib de la siguiente forma:

In [8]:
import matplotlib.pyplot as plt
In [9]:
plt.imshow(x_train[5], cmap='gray')
plt.show
Out[9]:
<function matplotlib.pyplot.show(close=None, block=None)>

Preparación de los datos

Estadarización del tipo de dato:

In [10]:
x_train, x_test = np.array(x_train, np.float32), np.array(x_test, np.float32)

Transformación de los datos a vectores de 784 características:

In [11]:
x_train, x_test = x_train.reshape([-1, num_features]), x_test.reshape([-1, num_features])

Normalización de los datos de [0, 255] a [0, 1]:

In [12]:
x_train, x_test = x_train / 255., x_test / 255.

A continuación particionamos los datos por lotes y los mezclamos:

In [13]:
train_data = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_data = train_data.repeat().shuffle(5000).batch(batch_size).prefetch(1)

Construcción del módelo:

In [14]:
W = tf.Variable(tf.ones([num_features, num_classes]), name="weight")
b = tf.Variable(tf.zeros([num_classes]), name="bias")

Regresión logistica de (Wx + b):

In [15]:
def logistic_regression(x):
return tf.nn.softmax(tf.matmul(x, W) + b)

La función de costo en este caso sería:

In [16]:
def cross_entropy(y_pred, y_true):
y_true = tf.one_hot(y_true, depth=num_classes)
y_pred = tf.clip_by_value(y_pred, 1e-9, 0.9)
return tf.reduce_mean(-tf.reduce_sum(y_true * tf.math.log(y_pred) + (1 - y_true) * tf.math.log(1 - y_pred),1))

La medida que se usará para determinar la calidad del modelo será:

In [17]:
def accuracy(y_pred, y_true):
correct_prediction = tf.equal(tf.argmax(y_pred, 1), tf.cast(y_true, tf.int64))
return tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

Para entrenar el modelo, se utilizará el optimizador definido para el gradiente estocástico:

In [18]:
optimizer = tf.optimizers.SGD(learning_rate)
In [19]:
def run_optimization(x, y):
with tf.GradientTape() as g:
pred = logistic_regression(x)
loss = cross_entropy(pred, y)
gradients = g.gradient(loss, [W, b])
optimizer.apply_gradients(zip(gradients, [W, b]))

A continuación, se ejecuta el proceso de entrenamiento:

In [20]:
display_step = 50
for step, (batch_x, batch_y) in enumerate(train_data.take(training_steps), 1):
run_optimization(batch_x, batch_y)
if step % display_step == 0:
pred = logistic_regression(batch_x)
loss = cross_entropy(pred, batch_y)
acc = accuracy(pred, batch_y)
print('step: %i, loss: %f, accuracy: %f' % (step, loss, acc))
step: 50, loss: 2.691798, accuracy: 0.707031
step: 100, loss: 2.201120, accuracy: 0.742188
step: 150, loss: 1.926627, accuracy: 0.789062
step: 250, loss: 1.502035, accuracy: 0.812500
step: 200, loss: 1.788280, accuracy: 0.773438 step: 300, loss: 1.421701, accuracy: 0.812500
step: 450, loss: 1.242893, accuracy: 0.847656
step: 350, loss: 1.371978, accuracy: 0.847656 step: 400, loss: 1.382541, accuracy: 0.789062 step: 500, loss: 1.199832, accuracy: 0.839844
step: 700, loss: 0.882333, accuracy: 0.914062
step: 550, loss: 1.111963, accuracy: 0.855469 step: 600, loss: 1.156131, accuracy: 0.828125 step: 650, loss: 1.010465, accuracy: 0.871094 step: 750, loss: 1.011202, accuracy: 0.863281
step: 1000, loss: 1.057235, accuracy: 0.820312
step: 800, loss: 0.971723, accuracy: 0.855469 step: 850, loss: 1.082722, accuracy: 0.832031 step: 900, loss: 0.912813, accuracy: 0.875000
step: 950, loss: 0.927843, accuracy: 0.863281

Finalmente se valida el modelo y se visualizan los resultados:

In [21]:
pred = logistic_regression(x_test)
print("Test Accuracy: %f" % accuracy(pred, y_test))
Test Accuracy: 0.877100
In [22]:
n_images = 2
test_images = x_test[:n_images]
predictions = logistic_regression(test_images)
for i in range(n_images):
plt.imshow(np.reshape(test_images[i], [28, 28]), cmap='gray')
plt.show()
print("Model prediction: %i" % np.argmax(predictions.numpy()[i]))
Model prediction: 7
Model prediction: 2

Conclusiones

En esta ocasión se introduccido los aspectos generales de la regresión logistica multinomial como un modelo de clasificación:

  • La regresión logística multinomial se puede utilizar con dos clases (por ejemplo determinar si un sentimiento es positivo o negativo) o con múltiples clases (por ejemplo la clasificación de libros de acuerdo con un genero literario).
  • Se usa la función softmax para calcular probabilidades.

Bibliografía

Contacto

  • Participa de la canal de Nerve a través de Discord.
  • Se quieres conocer más acerca de este tema me puedes contactar a través de Classgap.

16 feb 2021

Regresión Logística

La regresión lineal asume que existe una relación lineal entre dos variables $\mathcal{X}$ y $\mathcal{Y}$. Pero esto se viola rápidamente cuando la variable dependiente, $\mathcal{Y}$ es una variable categórica. La regresión logística expresa la regresión lineal múltiple en terminos de un logaritmo, superando así la no linealidad.

La regresión logística es uno de los métodos que se usan para el problema de clasificación. Usualmente se usa para estimar la probabilidad de que una muestra sea parte de una clase en particular (por ejemplo, ¿Cuál es la probabilidad de que una persona padeza cancer?). Si la probabilidad estimada es mayor que $50\%$, entonces el modelo predice que la muestra pertenece a esa clase y sino entonces predice que no pertenece. Esto es lo que hace en principio un clasificador binario.

Como se mencióno al principio, el modelo de regresión lineal se comporta pobremente cuando la variable $\mathcal{Y}$ es una variable discreta o categórica. Para resolver esto, hay que cambiar la forma de la hipótesis $h_{\theta}(x)$. Para esto se toma la familia de predictores de la forma, $$h_{\theta}(x) = \sigma(\theta^\top x) = \frac{1}{1 + e^{\theta^\top x}},$$ donde $$\sigma(z) = \frac{1}{1 + e^{-z}}$$ es la función logística o la función sigmoide. Graficamente $\sigma(z)$ luce de la siguiente forma:

In [1]:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
In [2]:
z = np.arange(-5, 5, 0.2)
g_z = sigmoid(z)
In [3]:
plt.plot(z, g_z)
plt.title('Figura 1. Función Sigmoide')
plt.show()

Observe que $\sigma(z)$ tiene a $1$ cuando $z\to \infty$, y $\sigma(z)$ tiene a $0$ cuándo $z\to -\infty$. Ademas, $\sigma(z)$, y por lo tanto $h_{\theta}$ está acotada entre $0$ y $1$. Acá se sigue manteniendo la convención de que $\theta = (\theta_0, \dots, \theta_n)^{\top}$ y $x=(1, x_1, \dots, x_n)^{\top}$, asi que $\theta^{\top}x=\theta\cdot x$.

Estimando probabilidades

Considere una conjunto de datos $S=\{(x_i, y_i)\}_{i=1}^m$, donde cada $x_i = (1, x_{i,1}, \dots, x_{i,n})^{\top}$ y cada $y_{i}\in \{0, 1\}$. Bajo estas condiciones, la variable $y_i$ se puede ver como una distribución de Bernoulli para la clasificación binaria. La regresión logística dice que la probabilidad de que la variable $y_i=1$, para $i=1,2, \dots, m$ puede ser modelado así:

$$ h_{\theta}(x_i)=E[y_i\,|\,x_i]=P(y_i=1\,|\,x_i, \theta)=\sigma(\theta^\top x_i)$$

donde $\sigma$ representa la función sigmode. ¿Pero esto por qué es así?

La razón viene de la generalización de los modelos lineales. Dado que $y_i$ es una variable binaria, parece natural la elección de una familia de distribuciones de Bernoulli para el modelo de probabilidad condicional de $y_i$ dado $x_i$. En la formulación de la distribución de Bernoulli como una familia de distribuciones exponenciales, se tiene que $p=\frac{1}{1 + e^{-\eta}}$ donde $\eta = \theta^\top x_i$. Además, observe que si $y_i\,|\, x_i; \theta \sim Ber(p)$, entonces $E[y_i\, |\, x_i]=p$.

Al asumir que $P(y_i=1\,|\,x_i;\theta)=h_{\theta}(x_i)$ y $P(y_i=0\,|\,x_i;\theta)=1 - h_{\theta}(x_i)$. Entonces de forma más compacta se puede escribir que:

$$P(y_i\,|\,x_i; \theta)=(h_{\theta}(x_i))^{y_i}(1-h_{\theta}(x_i))^{1-y_i}.$$

Como desde antes se ha asumido que se tiene un conjunto de entrenamiento con $m$ muestras que se supone han sido generadas independientemente, entonces la verosimilitud de los parámetros se puede expresar como:

$$L(\theta)=p(y\,|\, X; \theta) = \prod_{i=1}^{m}p(y_i\,|\,x_i; \theta),$$

donde $y=(y_1, \dots, y_m)^{\top}$ y $X$ es la matriz cuyas filas son $x_i^{\top}$ para todo $i=1, \dots, m$.

El objetivo es maximizar la verosimilitud $L(\theta)$, para esto es más fácil maximizar su logaritmo, es decir:

$$l(\theta) = \log L(\theta) = \sum_{i=1}^{m} y_{i}\log h_{\theta}(x_i)+(1+y_i)\log(1 - h_{ \theta}(x_i)).$$

Como en la regresión lineal para encontrar el parámetro $\theta$ hay que minimizar la función de costo, aquí también se mantiene la consistencia, ya que este problema se puede ver como un problema de minimización. Para esto consideramos el costo promedio sobre todo el conjutno de datos. En este caso, se considera $l(\theta)$. La maximización de $l(\theta)$ es equivalente a la minimización de $-l(\theta)$. Y usando la función promedio sobre todo el conjunto de datos, la función de costos para la regresión toma la forma:

$$J(\theta)=-\frac{1}{m}L(\theta)=-\frac{1}{m}\sum_{i=1}^{m}y_{i}\log(h_{\theta}(x_i)) + (1-y_i)\log(1-h_{\theta}(x_i)).$$

Esto nos conduce a entender el costo de un solo dato como $-\log(P(x_i\;|\;y_i))$,, el cual se puede escribir como:

$$-\log(P(x_i\;|\;y_i))=-\big(y_i\log(h_{\theta}(x_i)) + (1-y_i)\log(1-h_{\theta}(x_i))\big).$$

Esta expresión se puede expresar como una función a tramos, conocida como la entropía cruzada, dada por:

$$L_{EC}(h_{\theta}(\theta_i), y_i) = \begin{cases}-\log(h_\theta(x_i)) & \mbox{ si } y_i=1 \\ -\log(1-h_\theta(x_i)) & \mbox{ si } y_i = 0 \end{cases} $$

La entropía cruzada y el algoritmo del gradiente descendente

El objetivo con el gradiente descendente es encontrar el valor optimo para $\theta$: minimizar la función de costo. En la siguiente ecuación se representa el hecho explicito de la función de costo $J$ paramétrizada por $\theta$.Así el objetivo es encontrar $\theta$ que para todos los ejemplos del conjunto de entrenamiento, $\theta$ es tal que la función de costo se minimiza:

$$ \theta \in \operatorname*{argmin\,\,}_{ \theta\in \Omega} \frac{1}{m}\sum_{i=1}^{m}L_{CE}(f(x_i; \theta), y_i)= \operatorname*{argmin\,\,}_{ \theta\in \Omega} J(\theta). $$

Para emplear el algoritmo del gradiente descendente es necesario calcular $\nabla_\theta J(\theta)$ de tal manera que al regla de actualización para $\theta$ con base al gradiente es:

$$\theta_{t+1} = \theta_{t} -\eta \nabla J(\theta_t).$$

Teniendo presente la que función de entropía cruzada es:

$$L_{EC}(h_{\theta}(\theta_i), y_i)=-\big(y_i\log(h_{\theta}(x_i)) + (1-y_i)\log(1-h_{\theta}(x_i))\big).$$

Observe que la derivada para esta función en un vector de observación $x_i$ es:

$$ \frac{\partial L_{EC}(h_{\theta}(\theta_i), y_i)}{\partial \theta_j} = [h_\theta(x_i)-y]x_{i, j}.$$

Note que el gradiente con respeto a $\theta_j$ representa de forma intuitiva, la diferencia entre el valor real $y_i$ y el valor estimado $h_\theta(x_i)$ por la observación $x_i$, multiplicado por el valor correspodiente a $x_{ij}$.Así cada derivada parcial del gradiente es de la forma:

$$\frac{\partial J(\theta)}{\partial \theta_j } = \frac{1}{m}\sum_{i=1}^{m}[h_\theta(x_i)-y_i]x_{i,j}.$$

Finalmente de la ecuación anterior, es fácil concluir que:

$$\nabla_{\theta}J = X^{\top}[\hat{y}-y]$$

en donde $\hat{y} = [h_\theta(x_1),\dots, h_\theta(x_m)]$.

Implementación de la regresión logistica con Scikit-learn

Un ejercicio interesante para el lector es hacer la implementación de este módelo desde cero, sin embargo en esta ocasión no lo haremos así y veremos como hacer una implementación sencilla haciendo uso del modulo Scikit-learn de Python.

Para vamos a necesitar los siguiente modulos:

In [4]:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

Cargamos lo datos, en este caso disponemos de un pequeño conjunto de datos que contiene la información básica (genero, edad, y salario estimado) de unos usuarios que compran ciertos productos en una tienda. El objetivo acá es clasificar los usuarios entre aquellos que compran o no, esto con el objetivo de implementar alguna estrategía comercial

In [5]:
data = pd.read_csv('data.csv')
data.head()
Out[5]:
User ID Gender Age EstimatedSalary Purchased
0 15624510 Male 19 19000 0
1 15810944 Male 35 20000 0
2 15668575 Female 26 43000 0
3 15603246 Female 27 57000 0
4 15804002 Male 19 76000 0

Para hacernos una pequeña idea de la estructura de los datos, podemos construir varias visualizaciones entre las variables independientes (genero, edad y salario estimado) en relación con la variable objetivo (comprar). Por ejemplo, para la relación entre la Age y Purchased se obtiene el siguiente gráfico:

In [6]:
plt.scatter(data.Age, data.Purchased)
plt.show()

Como se puede apreciar los usuarios estan separados en dos clase, entre aquellos que hay comprado y aquellos que no.

Ahora vamos a dividir nuestro datos en dos partes, una parte sera el conjunto de entrenamiento y la otra el conjunto de testeo. Para hacer esto hacemos uso de train_test_split que se encuentra en el módulo sklearn.model_selection.

In [7]:
X_train, X_test, y_train, y_test = train_test_split(data.Age, data.Purchased, test_size=0.20)

Finalmente, hacemos uso de LogisticRegression que encuentra en el módulo sklearn.linear_model para entrenar nuestro modelo:

In [8]:
model = LogisticRegression()
model.fit(X_train.values.reshape(-1, 1), y_train.values.reshape(-1, 1).ravel())
Out[8]:
LogisticRegression()
In [9]:
y_pred = model.predict(X_test.values.reshape(-1, 1))

Una vez se ha estimado la probabilidad $\hat{p}_i=h_\theta(x_i)$ decidir si $x_i$ pertenece a la clase con etiqueta $y_i=1$. Esto se hace con base a la partición de clases que se obtiene al clasicar la probabilidad estimada en clases mediante:

$$\hat{y}_i= \begin{cases} 0 & \mbox{ si } \hat{p}_i<0.5; \\ 1 & \mbox{ si } \hat{p}_i \geq 0.5 \end{cases}$$

Observe que $\sigma(t) < 0$ cuando $t<0$ y $\sigma(t)\geq 0.5$ cuando $t\geq 0$, así, una regresión logistica predice $1$ si $\theta^\top x_i$ es positivo, y $0$ si es negativo.

Analizando la calidad de las prediciones del modelo con los datos de testeo y visualizando los resultados:

In [10]:
plt.scatter(X_test, y_test)
plt.scatter(X_test, y_pred, c='red')
plt.show()
In [11]:
print(f"Accuracy = {model.score(X_test.values.reshape(-1, 1), y_test.values.reshape(-1, 1))}")
Accuracy = 0.875

Bibliografía

Contacto

  • Participa de la canal de Nerve a través de Discord.
  • Se quieres conocer más acerca de este tema me puedes contactar a través de Classgap.