3  Algoritmos fundamentales

3.1 Regresión Lineal

3.2 Regresión Logística

La regresión logística no es una regresión como la regresión lineal, sino, es un método de clasificación. Acá queremos modelar \(y_{i}\) como una función lineal de los \(x_{i}\).

Se definen las etiquetas negativas como 0 y las positivas como 1 (en el caso de la clasificación binaria). Así, si el valor dado por el modelo es cercano a 0 se le asigna la etiqueta negativa y si es cercano a 1, la positiva. Una función que cumple lo anterior es la función sigmoide, la cual está definida como: \[ f(x) = \dfrac{1}{1+e^{-x}} \]

import math

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

x = np.linspace(-6, 6, 500)
y = 1 / (1 + np.exp(-x))
sns.lineplot(x=x, y=y)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("Función Sigmoide")
plt.show()

Si se optimizan los valores de \(\mathbb{x}\) y \(b\) apropiadamente, podemos interpretar la salida de \(f(x)\) como la probabilidad que \(y_{i}\) sea positivo. Así, si esta es mayor o igual a \(0.5\) podemos decir que la clase de \(\mathbb{x}\) es positiva; en el caso contrario, negativa.

Con base en lo anterior, se define el modelo de regresión logística como: \[ f_{\mathbb{w}, b}(\mathbb{x})=\dfrac{1}{1+e^{-(\mathbb{w}\cdot\mathbb{x}+b)}} \] donde el término \(\mathbb{w}\cdot\mathbb{x}+b\) es familiar de la regresión lineal.

Para hallar los valores óptimos de la regresión logística, queremos maximizar la verosimilitud basados en los datos de entrenamiento de nuestro modelo. Como estamos en el aprendizaje supervisado, asumimos que tenemos datos etiquetados \((x_i, y_i)\).

El criterio de optimización en la regresión logística es llamado máxima verosimilitud, entonces queremos maximizar: \[ L_{\mathbb{w}, b} := \prod_{i=1}^{N}f_{\mathbb{w}, b}(\mathbb{x}_{i})^{y_i}(1-f_{\mathbb{w}, b}(\mathbb{x}_{i}))^{1-y_i} \]

Para maximizar la ecuación anterior, es más sencillo con la Log-verosimilitud debido al uso de la función exponencial, la cual se define como: \[ \log{L_{\mathbb{w}, b}} := \ln(L_{\mathbb{w}, b}) = \displaystyle\sum_{i=1}^{N}y_{i}\ln(f_{\mathbb{w}, b}(\mathbb{x}_{i}))+(1-y_{i})\ln(1-f_{\mathbb{w}, b}(\mathbb{x}_{i})) \]

Una forma apropiada en la practica para solucionar el problema de optimizacción es usar el descenso del gradiente.

3.2.1 Ejemplo

Lo primero es definir la base de datos:

col_names = ['pregnant', 'glucose', 'bp', 'skin', 'insulin', 'bmi', 'pedigree', 'age', 'label']
# load dataset
pima = pd.read_csv("datos/diabetes.csv", header=None, names=col_names)
pima = pima.drop(0)
pima.head()
pregnant glucose bp skin insulin bmi pedigree age label
1 6 148 72 35 0 33.6 0.627 50 1
2 1 85 66 29 0 26.6 0.351 31 0
3 8 183 64 0 0 23.3 0.672 32 1
4 1 89 66 23 94 28.1 0.167 21 0
5 0 137 40 35 168 43.1 2.288 33 1

A continuación, tomamos las features o características y la etiqueta.

feature_cols = ["pregnant", "insulin", "bmi", "age", "glucose", "bp", "pedigree"]
X = pima[feature_cols]  # Features
y = pima.label  # Target variable

Ahora, dividimos el dataset en set de entrenamiento y prueba.

# split X and y into training and testing sets
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=16
)

Creamos el objetivo de regresión logística, se entrena y se aplica para predecir los datos de prueba

# import the class
from sklearn.linear_model import LogisticRegression

# instantiate the model (using the default parameters)
logreg = LogisticRegression(random_state=16)

# fit the model with data
logreg.fit(X_train, y_train)

y_pred = logreg.predict(X_test)
/usr/local/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:458: ConvergenceWarning:

lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression

Para validar que tan bien (o mal) está nuestra predicción podemos verificarlo con una matriz de confunsión:

# import the metrics class
from sklearn import metrics

cnf_matrix = metrics.confusion_matrix(y_test, y_pred)

class_names = [0, 1]  # name  of classes
fig, ax = plt.subplots()
tick_marks = np.arange(len(class_names))
plt.xticks(tick_marks, class_names)
plt.yticks(tick_marks, class_names)
# create heatmap
sns.heatmap(pd.DataFrame(cnf_matrix), annot=True, cmap="YlGnBu", fmt="g")
ax.xaxis.set_label_position("top")
plt.tight_layout()
plt.title("Confusion matrix", y=1.1)
plt.ylabel("Actual label")
plt.xlabel("Predicted label")
plt.show()

Esta matriz nos dice que:

  • 115 variables fueron asignadas como 0 cuando inicialmente son 0.
  • 10 variables fueron asignadas como 1 cuando inicialmente son 0 (error tipo 1).
  • 24 variables fueron asignadas como 0 cuando inicialmente son 1 (error tipo 2).
  • 43 variables fueron asignadas como 1 cuando inicialmente son 1.

3.2.2 Ventajas

  • No requiere alta potencia computacional debido a la eficiencia y sencillez.
  • No requiere escalar las variables.

3.2.3 Desventajas

  • No es capaz de manejar una gran cantidad de características categóricas.
  • Es vulnerable al overfitting.
  • No se puede resolver problemas no lineales con esta regresión, se deben aplicar ciertas transformaciones.
  • No funciona con variables independientes o no correlacionadas con la variable a predecir.

3.3 Decision Trees

3.3.1 Definición

Un árbol de decisión (DT) es un grafo no cíclico que se utiliza para tomar decisiones (clasificar). En cada nodo (rama) del grafo se evalúa uno de los features. Si el resultado de la evaluación es cierto (o está debajo de un umbral), se sigue la rama de la izquierda, si no se va a la derecha.

Un árbol de decisión.

Por lo tanto, los DT son un modelo no paramétrico.

Para crear el DT, se intenta optimizar el promedio de la máxima verosimilitud: \[ \frac{1}{N} \sum_{i=1}^{N}\left( y_i \ln{f_{ID3}(x_i)} + (1-y_i) \ln{(1-f_{ID3}(x_i))}\right) \] donde \(f_{ID3}\) es un DT y \(f_{ID3}(x) \stackrel{\text{def}}{=} Pr(y=1|x)\)

3.3.2 Construcción

Para construir el árbol, en cada nodo de decisión, se intenta minimizar la entropía de la información.

La entropía de un conjunto \(\cal{S}\) viene dada por: \[ H(S) \stackrel{\text{def}}{=} -f_{ID3}^{S} \log_2 (f_{ID3}^{S}) - (1-f_{ID3}^{S}) \log_2 (1-f_{ID3}^{S}) \]

Y si un grupo se divide en dos, la entropía es la suma ponderada de cada subconjunto: \[ H(S_-, S_+) \stackrel{\text{def}}{=} \frac{|S_-|}{|S|}H(S_-) + \frac{|S_+|}{|S|}H(S_+) \]

3.3.3 Ejemplo

Consideremos los siguientes datos:

Atributos:

  • Edad: viejo (v), media-vida(m), nuevo (nv)
  • Competencia: no(n), sí(s)
  • Tipo: software (swr), hardware (hwr)
Edad Competencia Tipo Ganancia
v s swr baja
v n swr baja
v n hwr baja
m s swr baja
m s hwr baja
m n hwr sube
m n swr sube
nv s swr sube
nv n hwr sube
nv n swr sube

Cálculo de las entropías: Primero se tiene que probar todos los features para ver cuál tiene mayor ganancia de información (reduce la entropía)

Entropía total: \[ H(S) = \text{Entropía de los casos baja} + \text{Entropía de los casos sube} \]

\[ H(s) = -\frac{5}{10}*\log_2(\frac{5}{10}) - \frac{5}{10}*\log_2(\frac{5}{10}) = 1 \]

Ahora vamos a decidir la primera separación con las edades \(H = \frac{3}{10} \cdot 0 + \frac{4}{10} \cdot 1 + \frac{3}{10} \cdot 0 = 0.4\)

Ahora vamos a decidir la primera separación con la competencia \(H = \frac{4}{10} \cdot 0.811 + \frac{6}{10} \cdot 0.918 = 0.8752\)

Ahora vamos a decidir la primera separación con las edades \(H = \frac{4}{10} \cdot 0.811 + \frac{6}{10} \cdot 0.918 = 0.8752\)

Ahora vamos a decidir la primera separación con el tipo \(H = \frac{6}{10} \cdot 1 + \frac{4}{10} \cdot 1 = 1\)

Concluimos que lo que nos da la máxima ganancia de información es primero decidir por edades, eso nos deja dos nodos hoja y un nodo rama que debemos volver a separar.

Ahora vamos a buscar el segundo nivel, donde vamos a separar el grupo que tiene edades medias por competencia:

\(H = \frac{2}{4} \cdot 0 + \frac{2}{4} \cdot 0 = 0\)

Con esto ya se clasificaron todos los datos, puesto que terminamos solo con nodos hojas:

Esto también se puede hacer con valores numéricos, que de hecho, es lo que se puede hacer con scikit learn

y con esto se obtiene este árbol de decisión:

3.3.4 Comandos básicos en python

Estos son los comandos básicos en python

#| label: dibujoArbol01
#| fig-cap: "Árbol de decisión"
from sklearn import tree
X = # Lista con los features (lista de listas)
Y = # Lista con los labels
# Se define la variable que tendrá el árbol
clf = tree.DecisionTreeClassifier()
# Se calcula el árbol
clf = clf.fit(X, Y)
# Se utiliza el árbol para predecir el label de un dato nuevo
clf.predict_proba(X0)
# Se dibuja el árbol
tree.plot_tree(clf)

y este sería un ejemplo sencillo en python:

Primero creamos los datos

from sklearn import tree
from sklearn.datasets import make_blobs
from sklearn.inspection import DecisionBoundaryDisplay
import matplotlib.pyplot as plt
import numpy as np

# Creación de los datos
X, Y = make_blobs(n_samples=200, centers=4, random_state=6)
plt.scatter(X[:, 0], X[:, 1], c=Y, s=30)
plt.title("Datos originales")
plt.xlabel("x1")
plt.ylabel("x2")
plt.show()

Ejemplo hecho en python: datos

Luego se crea el arbol

clf = tree.DecisionTreeClassifier()
clf = clf.fit(X, Y)
tree.plot_tree(clf)
plt.show()

Ejemplo hecho en python: arbol

y por último, dibujamos las separaciones

DecisionBoundaryDisplay.from_estimator(clf, X, response_method="predict")
plt.scatter(X[:, 0], X[:, 1], c=Y, s=30)
plt.show()

Ejemplo hecho en python: separación

y con esto se puede aplicar el árbol

print(clf.predict([[5.0, 1.0]]))
print(clf.predict([[-2.0, -1.0]]))
print(clf.predict([[6.0, -6.0]]))
[0]
[3]
[0]

y lo que devuelve es el número de grupo al que pertene el dato

3.4 Máquinas de Soporte Vectorial

El problema con SVM es que ocurre si los datos no se pueden separar con un hiperplano.

Recordemos que en SVM, se deben satisfacer las siguientes condiciones: a. \(wx_i-b \geq 1\) si \(y_i = 1\), y b. \(wx_i-b \leq -1\) si \(y_i = -1\)

Y la función a minimizar es:

\[\begin{equation*} \min_{w,b} \frac{1}{2}||w||^2 \text{ sujeto a } y_i(wx_i-b) \geq 1 \text{ para } i=1,2,\ldots,N. \end{equation*}\]

3.4.1 El truco del kernel

El truco del kernel consiste en transformar los datos a un espacio de mayor dimensión, donde se pueda separar con un hiperplano. El truco consiste en definir una función \(\phi\) tal que \(\phi: x\to \phi(x)\), donde \(\phi(x)\) es un vector de dimensión superior.

El problema de minimización se puede reescribir como:

\[\begin{equation*} \max_{\alpha_1, \dots, \alpha_N} \sum_{i=1}^{N} \alpha_i - \frac{1}{2} \sum_{i=1}^{N} \sum_{j=1}^{N} y_i \alpha_i <x_i, x_j> y_j \alpha_j \end{equation*}\]

sujeto a que \(\alpha_i \geq 0\) y \(\sum_{i=1}^{N} \alpha_i y_i = 0\), donde \(<x_i, x_j>\) es el producto punto entre los vectores \(x_i\) y \(x_j\).

En este caso los kernels \(<x_i, x_j>\) se pueden definir como:

  • Lineal: \(<x_i, x_j> = x_i^T x_j\)
  • Polinomial: \(<x_i, x_j> = (\gamma x_i^T x_j + r)^d\)
  • Gaussiano o RBF: \(<x_i, x_j> = \exp{(-\gamma \Vert x_{i} - x_{j}\Vert^2)}\)
  • Sigmoide: \(<x_i, x_j> = \tanh{(\gamma x_i^T x_j + r)}\)
import matplotlib.pyplot as plt
import numpy as np

X = np.array(
    [
        [0.4, -0.7],
        [-1.5, -1.0],
        [-1.4, -0.9],
        [-1.3, -1.2],
        [-1.1, -0.2],
        [-1.2, -0.4],
        [-0.5, 1.2],
        [-1.5, 2.1],
        [1.0, 1.0],
        [1.3, 0.8],
        [1.2, 0.5],
        [0.2, -2.0],
        [0.5, -2.4],
        [0.2, -2.3],
        [0.0, -2.7],
        [1.3, 2.1],
    ]
)

y = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])

# Plotting settings
fig, ax = plt.subplots(figsize=(4, 3))
x_min, x_max, y_min, y_max = -3, 3, -3, 3
ax.set(xlim=(x_min, x_max), ylim=(y_min, y_max))

# Plot samples by color and add legend
scatter = ax.scatter(X[:, 0], X[:, 1], s=150, c=y, label=y, edgecolors="k")
ax.legend(*scatter.legend_elements(), loc="upper right", title="Classes")
ax.set_title("Samples in two-dimensional feature space")
_ = plt.show()

from sklearn import svm
from sklearn.inspection import DecisionBoundaryDisplay


def plot_training_data_with_decision_boundary(kernel):
    # Train the SVC
    clf = svm.SVC(kernel=kernel, gamma=2).fit(X, y)

    # Settings for plotting
    _, ax = plt.subplots(figsize=(4, 3))
    x_min, x_max, y_min, y_max = -3, 3, -3, 3
    ax.set(xlim=(x_min, x_max), ylim=(y_min, y_max))

    # Plot decision boundary and margins
    common_params = {"estimator": clf, "X": X, "ax": ax}
    DecisionBoundaryDisplay.from_estimator(
        **common_params,
        response_method="predict",
        plot_method="pcolormesh",
        alpha=0.3,
    )
    DecisionBoundaryDisplay.from_estimator(
        **common_params,
        response_method="decision_function",
        plot_method="contour",
        levels=[-1, 0, 1],
        colors=["k", "k", "k"],
        linestyles=["--", "-", "--"],
    )

    # Plot bigger circles around samples that serve as support vectors
    ax.scatter(
        clf.support_vectors_[:, 0],
        clf.support_vectors_[:, 1],
        s=250,
        facecolors="none",
        edgecolors="k",
    )
    # Plot samples by color and add legend
    ax.scatter(X[:, 0], X[:, 1], c=y, s=150, edgecolors="k")
    ax.legend(*scatter.legend_elements(), loc="upper right", title="Classes")
    ax.set_title(f" Decision boundaries of {kernel} kernel in SVC")

    _ = plt.show()
plot_training_data_with_decision_boundary("linear")

plot_training_data_with_decision_boundary("poly")

plot_training_data_with_decision_boundary("rbf")

plot_training_data_with_decision_boundary("sigmoid")

3.5 K-Nearest Neighbors (KNN)

3.5.1 Carga de paquetes

import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.inspection import DecisionBoundaryDisplay

import pandas as pd
import seaborn as sns
from sklearn.datasets import make_blobs

4 Cargas datos

X, y = make_blobs(n_samples=1000, centers=3, random_state=6)

5 Visualizar los datos

sns.scatterplot(x=X[:,0],y=X[:,1], hue=y)
plt.show()

6 Se normaliza y se divide los datos

# Split the data into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y)

# Scale the features using StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

7 Ajuste y evaluación del modelo

knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)

# predecir con el modelo
y_pred = knn.predict(X_test)

# evaluarlo
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)
Accuracy: 1.0
DecisionBoundaryDisplay.from_estimator(knn, X_train)
sns.scatterplot(x=X[:,0],y=X[:,1], hue=y)
plt.show()