Implementación de Regresión Logística: Algoritmo de Clasificación desde Cero

Implementación de Regresión Logística: Algoritmo de Clasificación desde Cero

La regresión logística es uno de los algoritmos fundamentales en el aprendizaje automático para resolver problemas de clasificación binaria. A diferencia de la regresión lineal que produce valores continuos, este método transforma las predicciones en probabilidades comprendidas entre 0 y 1, permitiendo establecer límites de decisión claros para la categorización de elementos.

Fundamentos del Algoritmo

La Función Sigmoide

El núcleo de la regresión logística es la función sigmoide, también conocida como función logística. Esta trensformación matemática conveirte cualquier valor real en un valor dentro del intervalo (0,1), resolviendo el problema de los valores fuera de rango que presenta la regresión lineal tradicional.

import math

def sigmoid(z):
    """Transforma valores lineales en probabilidades"""
    if z < 0:
        return 1 - 1 / (1 + math.exp(z))
    return 1 / (1 + math.exp(-z))

Esta función presenta una curva característica en forma de S que es ideal para modelar probabilidades. Cuando el valor de entrada es 0, la salida es 0.5; valores positivos producen salidas mayores a 0.5, mientras que valores negativos producen salidas menores a 0.5.

Límite de Decisión

El umbral de 0.5 determina la frontera entre las dos clases. En un espacio bidimensional, este límite corresponde a una línea recta que separa las regiones de decisión. La predicción se realiza mediante el producto punto entre los pesos del modelo y las características del ejemplo:

def predict(features, weights):
    """Calcula la probabilidad de pertenencia a la clase positiva"""
    linear_combination = sum(f * w for f, w in zip(features, weights))
    return sigmoid(linear_combination)

Optimización del Modelo

Función de Costo

Para entrenar el modelo, necesitamos una función que cuantifique el error entre las predicciones y los valores reales. La función de pérdida de entropía cruzada binaria (binary cross-entropy) es la estándar para problemas de clasificación binaria:

def compute_cost(training_data, labels, parameters):
    """
    Calcula el costo total usando la función de pérdida logística
    """
    m = len(training_data)
    total_cost = 0.0
    
    for features, label in zip(training_data, labels):
        prediction = sigmoid(sum(f * p for f, p in zip(features, parameters)))
        total_cost -= label * math.log(prediction + 1e-9)
        total_cost -= (1 - label) * math.log(1 - prediction + 1e-9)
    
    return total_cost / m

Descenso de Gradiente

El algoritmo de optimización ajusta iterativamente los pesos del modelo en la dirección opuesta al gradiente de la función de costo:

def compute_gradient(training_data, labels, parameters):
    """
    Calcula el gradiente de la función de pérdida
    """
    m = len(training_data)
    gradient = [0.0] * len(parameters)
    
    for features, label in zip(training_data, labels):
        error = sigmoid(sum(f * p for f, p in zip(features, parameters))) - label
        for i, feature in enumerate(features):
            gradient[i] += error * feature
    
    return [g / m for g in gradient]

def train_model(training_data, labels, learning_rate=0.1, iterations=1000):
    """
    Entrena el modelo usando descenso de gradiente
    """
    parameters = [random.random() - 0.5 for _ in range(len(training_data[0]))]
    
    for _ in range(iterations):
        gradient = compute_gradient(training_data, labels, parameters)
        parameters = [p - learning_rate * g 
                      for p, g in zip(parameters, gradient)]
    
    return parameters

Aplicación Práctica

Preparación de Datos

Antes del entrenamiento, es fundamental normalizar las características para asegurar que todas contribuyan equitativamente al modelo:

def normalize_features(dataset):
    """Estandariza las características"""
    columns = list(zip(*dataset))
    means = [sum(col) / len(col) for col in columns]
    stds = [math.sqrt(sum((x - m) ** 2 for x in col) / len(col)) 
            for col, m in zip(columns, means)]
    
    normalized = []
    for row in dataset:
        normalized.append([(x - m) / s for x, m, s in zip(row, means, stds)])
    
    return normalized, means, stds

Evaluación del Rendimiento

Para medir la efectividad del clasificador, se utilizan métricas derivadas de la matriz de confusión:

def evaluate_model(predictions, actual_labels):
    """
    Calcula métricas de evaluación del clasificador
    """
    true_positives = sum(1 for p, a in zip(predictions, actual_labels) 
                         if p == 1 and a == 1)
    false_positives = sum(1 for p, a in zip(predictions, actual_labels) 
                          if p == 1 and a == 0)
    false_negatives = sum(1 for p, a in zip(predictions, actual_labels) 
                          if p == 0 and a == 1)
    
    precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
    recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0
    f1_score = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
    
    return {
        'precision': precision,
        'recall': recall,
        'f1_score': f1_score
    }

Ejemplo de Uso

A continuación se presenta un flujo completo de entrenamiento y evaluación:

# Datos de ejemplo: características normalizadas y etiquetas
sample_data = [[1.2, 3.4], [2.1, 2.8], [0.8, 4.2], [1.5, 3.1]]
sample_labels = [1, 1, 0, 1]

# Entrenamiento
weights = train_model(sample_data, sample_labels, 
                      learning_rate=0.05, iterations=500)

# Predicciones
threshold = 0.5
predictions = [1 if predict(features, weights) >= threshold else 0 
               for features in sample_data]

# Evaluación
metrics = evaluate_model(predictions, sample_labels)
print(f"Precisión: {metrics['precision']:.2f}")
print(f"Exhaustividad: {metrics['recall']:.2f}")
print(f"Puntuación F1: {metrics['f1_score']:.2f}")

Comparación con Regresión Lineal

Una diferencia fundamental entre ambos algoritmos radica en el rango de valores que pueden producir. Mientras la regresión lineal genera valores que pueden estar completamente fuera del intervalo [0,1], la regresión logística siempre produce probabilidades válidas mediante la transformación sigmoide. Esto la hace apropiada para problemas donde se necesita interpretar los resultados como probabilidades.

Interpretación de Coeficientes

Los pesos entrenados proporcionan información sobre la influencia de cada característica. Un coeficiente positivo indica que el incremento de esa característica aumenta la probabilidad de la clase positiva, mientras que un coeficiente negativo tiene el efecto contrario. La magnitud del coeficiente refleja la fuerza de esta relación.

Consideraciones de Implementación

Algunos aspectos importantes a considerar incluyen la selección del tamaño de aprendizaje (learning rate), el número de iteraciones, y el umbral de clasificación. Un learning rate muy grande puede causar oscilaciones o divergencia, mientras que uno muy pequeño ralentiza la convergencia. El umbral puede ajustarse según los requisitos específicos de la aplicación: un umbral más alto favorece la precisión, mientras que uno más bajo favorece la exhaustividad.

Etiquetas: machine-learning Python classification logistic-regression gradient-descent

Publicado el 7-12 17:55