Modelos Lineales y Regularización en Machine Learning
Modelos Lineales Básicos
Los modelos lineales constituyen la base fundamental del machine learning supervisado, proporcionando una comprensión esencial de cómo las variables predictoras se relacionan con la variable objetivo de manera matemáticamente interpretable.
Regresión Lineal
La regresión lineal modela la relación entre variables mediante una función lineal, buscando la línea que mejor se ajuste a los datos minimizando los errores de predicción. La ecuación fundamental es:
y = β₀ + β₁x₁ + β₂x₂ + ... + βₘxₘ + ε
Donde:
- y: variable dependiente (objetivo)
- βᵢ: coeficientes de regresión
- xᵢ: variables independientes (características)
- ε: término de error
Implementación en Python
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
# Generar datos de ejemplo
np.random.seed(42)
X = np.random.randn(100, 3)
y = 2*X[:, 0] + 3*X[:, 1] - 1.5*X[:, 2] + np.random.randn(100)*0.1
# Dividir los datos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Crear y entrenar el modelo
modelo_lineal = LinearRegression()
modelo_lineal.fit(X_train, y_train)
# Realizar predicciones
y_pred = modelo_lineal.predict(X_test)
# Evaluación del modelo
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"Coeficientes: {modelo_lineal.coef_}")
print(f"Intercepto: {modelo_lineal.intercept_}")
print(f"MSE: {mse:.4f}")
print(f"R²: {r2:.4f}")
Características principales:
- Simplicidad: Fácil de interpretar y implementar
- Eficiencia computacional: Rápida de entrenar y predecir
- Transparencia: Los coeficientes revelan la importancia relativa de cada característica
Regresión Logística
La regresión logística extiende los conceptos lineales a problemas de clasificación, utilizando la función logística (sigmoid) para mapear cualquier valor real a un rango entre 0 y 1.
Función Logística
p = 1 / (1 + e^(-z))
donde z = β₀ + β₁x₁ + β₂x₂ + ... + βₘxₘ
Implementación en Python
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.metrics import accuracy_score, classification_report
# Generar datos de clasificación
X, y = make_classification(n_samples=1000, n_features=4, n_redundant=0,
n_informative=2, n_clusters_per_class=1, random_state=42)
# Dividir los datos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Crear y entrenar el modelo
modelo_logistico = LogisticRegression()
modelo_logistico.fit(X_train, y_train)
# Realizar predicciones
y_pred = modelo_logistico.predict(X_test)
y_prob = modelo_logistico.predict_proba(X_test)
# Evaluación
accuracy = accuracy_score(y_test, y_pred)
print(f"Precisión: {accuracy:.4f}")
print("\nReporte de clasificación:")
print(classification_report(y_test, y_pred))
Ventajas de la regresión logística:
- Probabilidades interpretables: Proporciona probabilidades de pertenencia a cada clase
- No requiere distribución normal: Funciona con variables categóricas y continuas
- Manejo de multicolinealidad: Mejor que otros métodos en presencia de características correlacionadas
Técnicas de Regularización
La regularización es fundamental para prevenir el sobreajuste y mejorar la capacidad de generalización de los modelos. Estas técnicas agregan términos de penalización a la función de pérdida original.
Regularización L1 (Lasso)
La regularización L1 añade la suma de los valores absolutos de los coeficientes como término de penalización.
Función de costo:
Costo = MSE + λ∑|βᵢ|
Características principales:
- Selección automática de características: Puede llevar coeficientes exactamente a cero
- Modelos parsimoniosos: Produce modelos más simples e interpretables
- Útil con alta dimensionalidad: Efectiva cuando hay muchas características irrelevantes
Implementación en Python
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
# Preparar datos
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train_scaled, X_test_scaled, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.2, random_state=42)
# Modelo Lasso con diferentes valores de alpha
alphas = [0.001, 0.01, 0.1, 1.0, 10.0]
for alpha in alphas:
lasso = Lasso(alpha=alpha)
lasso.fit(X_train_scaled, y_train)
y_pred = lasso.predict(X_test_scaled)
mse = mean_squared_error(y_test, y_pred)
# Contar coeficientes no cero
non_zero_coefs = np.sum(lasso.coef_ != 0)
print(f"Alpha: {alpha}")
print(f"MSE: {mse:.4f}")
print(f"Coeficientes no cero: {non_zero_coefs}")
print(f"Coeficientes: {lasso.coef_}")
print("-" * 40)
Regularización L2 (Ridge)
La regularización L2 añade la suma de los cuadrados de los coeficientes como penalización.
Función de costo:
Costo = MSE + λ∑βᵢ²
Características principales:
- Reducción uniforme: Reduce todos los coeficientes pero no los elimina completamente
- Manejo de multicolinealidad: Excelente para características altamente correlacionadas
- Estabilidad: Más estable que Lasso en presencia de ruido
Implementación en Python
from sklearn.linear_model import Ridge
# Comparación de Ridge con diferentes alphas
alphas = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]
results = []
for alpha in alphas:
ridge = Ridge(alpha=alpha)
ridge.fit(X_train_scaled, y_train)
y_pred = ridge.predict(X_test_scaled)
mse = mean_squared_error(y_test, y_pred)
# Calcular la norma L2 de los coeficientes
l2_norm = np.sqrt(np.sum(ridge.coef_**2))
results.append({
'alpha': alpha,
'mse': mse,
'l2_norm': l2_norm,
'coeficientes': ridge.coef_
})
# Mostrar resultados
for result in results:
print(f"Alpha: {result['alpha']}")
print(f"MSE: {result['mse']:.4f}")
print(f"Norma L2: {result['l2_norm']:.4f}")
print("-" * 40)
Red Elástica (Elastic Net)
La Red Elástica combina las penalizaciones L1 y L2, ofreciendo un equilibrio entre selección de características y estabilidad.
Función de costo:
Costo = MSE + λ₁∑|βᵢ| + λ₂∑βᵢ²
En scikit-learn se parametriza como:
Penalización = α × [l1_ratio × ∑|βᵢ| + (1 - l1_ratio) × ∑βᵢ²]
Implementación completa en Python
from sklearn.linear_model import ElasticNet, ElasticNetCV
from sklearn.model_selection import GridSearchCV
# Elastic Net con búsqueda de hiperparámetros
param_grid = {
'alpha': [0.001, 0.01, 0.1, 1.0, 10.0],
'l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9]
}
elastic_net = ElasticNet(max_iter=1000)
grid_search = GridSearchCV(
elastic_net, param_grid,
cv=5, scoring='neg_mean_squared_error',
n_jobs=-1
)
grid_search.fit(X_train_scaled, y_train)
# Mejor modelo
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test_scaled)
mse = mean_squared_error(y_test, y_pred)
print(f"Mejores parámetros: {grid_search.best_params_}")
print(f"MSE del mejor modelo: {mse:.4f}")
print(f"Coeficientes del mejor modelo: {best_model.coef_}")
# Usar ElasticNetCV para optimización automática
elastic_cv = ElasticNetCV(
alphas=[0.001, 0.01, 0.1, 1.0, 10.0],
l1_ratio=[0.1, 0.5, 0.9],
cv=5,
random_state=42
)
elastic_cv.fit(X_train_scaled, y_train)
print(f"\nElasticNetCV - Alpha óptimo: {elastic_cv.alpha_:.4f}")
print(f"ElasticNetCV - L1 ratio óptimo: {elastic_cv.l1_ratio_:.4f}")
Sesgo vs Varianza: El Trade-off Fundamental
El equilibrio entre sesgo y varianza es uno de los conceptos más importantes en machine learning, determinando la capacidad de generalización del modelo.
Definiciones Fundamentales
Sesgo (Bias): Error sistemático debido a suposiciones simplificadoras del modelo:
- Modelos con alto sesgo tienden al underfitting
- Hacen suposiciones demasiado fuertes sobre los datos
- Fallan en capturar patrones complejos
Varianza: Sensibilidad del modelo a pequeños cambios en los datos de entrenamiento:
- Modelos con alta varianza tienden al overfitting
- Se ajustan demasiado a ruido específico del conjunto de entrenamiento
- Funcionan mal con datos nuevos
Relación Matemática
El error total de un modelo se descompone en tres componentes:
Error Total = Sesgo² + Varianza + Error Irreducible
Demostración Práctica del Trade-off
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
# Función para generar datos con ruido
def generar_datos_complejos(n_samples=100, noise_level=0.1):
np.random.seed(42)
X = np.linspace(0, 1, n_samples).reshape(-1, 1)
y = np.sin(2 * np.pi * X.ravel()) + np.random.normal(0, noise_level, n_samples)
return X, y
X, y = generar_datos_complejos()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Modelos con diferentes niveles de complejidad
modelos = {
'Lineal (Alto Sesgo)': LinearRegression(),
'Polinomial Grado 3': Pipeline([
('poly', PolynomialFeatures(3)),
('linear', LinearRegression())
]),
'Polinomial Grado 10': Pipeline([
('poly', PolynomialFeatures(10)),
('linear', LinearRegression())
]),
'Árbol Profundo (Alta Varianza)': DecisionTreeRegressor(max_depth=None, random_state=42)
}
# Evaluar cada modelo
resultados = {}
for nombre, modelo in modelos.items():
modelo.fit(X_train, y_train)
y_pred_train = modelo.predict(X_train)
y_pred_test = modelo.predict(X_test)
error_train = mean_squared_error(y_train, y_pred_train)
error_test = mean_squared_error(y_test, y_pred_test)
resultados[nombre] = {
'error_train': error_train,
'error_test': error_test,
'gap': error_test - error_train
}
# Mostrar resultados
print("Análisis del Trade-off Sesgo-Varianza:")
print("-" * 50)
for nombre, metricas in resultados.items():
print(f"{nombre}:")
print(f" Error Entrenamiento: {metricas['error_train']:.4f}")
print(f" Error Prueba: {metricas['error_test']:.4f}")
print(f" Gap (Overfitting): {metricas['gap']:.4f}")
print()
Estrategias de Equilibrio
Para reducir el sesgo:
- Aumentar la complejidad del modelo
- Agregar más características
- Usar modelos más flexibles
Para reducir la varianza:
- Regularización: L1, L2, Elastic Net
- Más datos de entrenamiento: Especialmente efectivo para alta varianza
- Ensemble methods: Combinar múltiples modelos
- Validación cruzada: Para evaluación robusta
Overfitting y Underfitting
Overfitting (Sobreajuste)
Ocurre cuando el modelo aprende patrones específicos del conjunto de entrenamiento, incluyendo ruido, resultando en pobre generalización.
Síntomas del Overfitting:
- Alto rendimiento en datos de entrenamiento
- Bajo rendimiento en datos de prueba
- Gran diferencia entre errores de entrenamiento y validación
Código para detectar Overfitting:
from sklearn.model_selection import learning_curve
def plot_learning_curves(modelo, X, y, title="Curvas de Aprendizaje"):
"""Graficar curvas de aprendizaje para detectar overfitting"""
train_sizes, train_scores, val_scores = learning_curve(
modelo, X, y, cv=5, n_jobs=-1,
train_sizes=np.linspace(0.1, 1.0, 10),
scoring='neg_mean_squared_error'
)
# Convertir a valores positivos
train_scores = -train_scores
val_scores = -val_scores
# Calcular medias y desviaciones estándar
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
val_mean = np.mean(val_scores, axis=1)
val_std = np.std(val_scores, axis=1)
# Graficar
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, 'o-', color='blue', label='Entrenamiento')
plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std,
alpha=0.1, color='blue')
plt.plot(train_sizes, val_mean, 'o-', color='red', label='Validación')
plt.fill_between(train_sizes, val_mean - val_std, val_mean + val_std,
alpha=0.1, color='red')
plt.xlabel('Tamaño del conjunto de entrenamiento')
plt.ylabel('Error (MSE)')
plt.title(title)
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# Comparar modelo simple vs complejo
X_complex, y_complex = generar_datos_complejos(n_samples=200)
# Modelo con overfitting
modelo_overfitting = Pipeline([
('poly', PolynomialFeatures(15)),
('linear', LinearRegression())
])
plot_learning_curves(modelo_overfitting, X_complex, y_complex,
"Modelo con Overfitting (Polinomial Grado 15)")
Underfitting (Subajuste)
Sucede cuando el modelo es demasiado simple para capturar los patrones subyacentes en los datos.
Síntomas del Underfitting:
- Bajo rendimiento tanto en entrenamiento como en prueba
- El modelo no logra capturar la complejidad de los datos
- Error de sesgo alto
Validación Cruzada
La validación cruzada es esencial para evaluar la estabilidad y capacidad de generalización del modelo.
K-Fold Cross-Validation
from sklearn.model_selection import cross_val_score, KFold, StratifiedKFold
def evaluar_modelo_cv(modelo, X, y, cv_folds=5):
"""Evaluación completa con validación cruzada"""
# K-Fold Cross-Validation
kfold = KFold(n_splits=cv_folds, shuffle=True, random_state=42)
cv_scores = cross_val_score(modelo, X, y, cv=kfold,
scoring='neg_mean_squared_error', n_jobs=-1)
# Convertir a valores positivos
cv_scores = -cv_scores
print(f"Validación Cruzada ({cv_folds}-fold):")
print(f"Scores individuales: {cv_scores}")
print(f"Media: {cv_scores.mean():.4f}")
print(f"Desviación estándar: {cv_scores.std():.4f}")
print(f"Intervalo de confianza 95%: [{cv_scores.mean() - 2*cv_scores.std():.4f}, "
f"{cv_scores.mean() + 2*cv_scores.std():.4f}]")
return cv_scores
# Evaluar diferentes modelos con regularización
modelos_regularizados = {
'Sin regularización': LinearRegression(),
'Ridge (α=1.0)': Ridge(alpha=1.0),
'Lasso (α=0.1)': Lasso(alpha=0.1),
'Elastic Net (α=0.5, l1_ratio=0.5)': ElasticNet(alpha=0.5, l1_ratio=0.5)
}
print("Comparación de modelos con validación cruzada:")
print("=" * 60)
for nombre, modelo in modelos_regularizados.items():
print(f"\n{nombre}:")
print("-" * len(nombre))
evaluar_modelo_cv(modelo, X_train_scaled, y_train)
Validación Cruzada Estratificada para Clasificación
from sklearn.model_selection import StratifiedKFold
def evaluar_clasificacion_cv(modelo, X, y, cv_folds=5):
"""Validación cruzada para problemas de clasificación"""
# Usar StratifiedKFold para mantener proporciones de clases
skfold = StratifiedKFold(n_splits=cv_folds, shuffle=True, random_state=42)
# Múltiples métricas
metricas = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro']
for metrica in metricas:
scores = cross_val_score(modelo, X, y, cv=skfold, scoring=metrica)
print(f"{metrica.capitalize()}: {scores.mean():.4f} (+/- {scores.std() * 2:.4f})")
# Ejemplo con regresión logística regularizada
X_class, y_class = make_classification(n_samples=1000, n_features=10,
n_informative=5, n_redundant=3,
n_clusters_per_class=1, random_state=42)
modelos_clasificacion = {
'Logística sin regularización': LogisticRegression(penalty=None, max_iter=1000),
'Logística L1': LogisticRegression(penalty='l1', solver='liblinear', C=1.0),
'Logística L2': LogisticRegression(penalty='l2', C=1.0),
'Logística Elastic Net': LogisticRegression(penalty='elasticnet',
solver='saga', l1_ratio=0.5, C=1.0)
}
for nombre, modelo in modelos_clasificacion.items():
print(f"\n{nombre}:")
print("-" * len(nombre))
evaluar_clasificacion_cv(modelo, X_class, y_class)
Validación Cruzada Temporal (Time Series)
Para datos temporales, es crucial mantener el orden cronológico:
from sklearn.model_selection import TimeSeriesSplit
def validacion_temporal(modelo, X, y, n_splits=5):
"""Validación cruzada para series temporales"""
tscv = TimeSeriesSplit(n_splits=n_splits)
scores = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(X)):
X_train_fold, X_val_fold = X[train_idx], X[val_idx]
y_train_fold, y_val_fold = y[train_idx], y[val_idx]
modelo.fit(X_train_fold, y_train_fold)
y_pred = modelo.predict(X_val_fold)
score = mean_squared_error(y_val_fold, y_pred)
scores.append(score)
print(f"Fold {fold + 1}: MSE = {score:.4f}")
print(f"Promedio: {np.mean(scores):.4f} (+/- {np.std(scores) * 2:.4f})")
return scores
# Simular datos temporales
np.random.seed(42)
n_tiempo = 200
X_temporal = np.random.randn(n_tiempo, 3)
y_temporal = np.cumsum(X_temporal[:, 0]) + np.random.randn(n_tiempo) * 0.1
print("Validación Cruzada Temporal:")
validacion_temporal(Ridge(alpha=1.0), X_temporal, y_temporal)
Ejemplo Integral: Proyecto Completo
Implementemos un ejemplo completo que integre todos los conceptos:
import warnings
warnings.filterwarnings('ignore')
def proyecto_completo_regularizacion():
"""Ejemplo integral de regularización"""
# 1. Generar datos sintéticos complejos
print("1. GENERACIÓN DE DATOS")
print("=" * 40)
np.random.seed(42)
n_samples, n_features = 200, 15
# Características informativas y redundantes
X_informative = np.random.randn(n_samples, 5)
X_redundant = X_informative[:, [0, 1, 2]] + np.random.randn(n_samples, 3) * 0.1
X_noise = np.random.randn(n_samples, 7)
X = np.concatenate([X_informative, X_redundant, X_noise], axis=1)
# Variable objetivo con relación compleja
y = (2 * X[:, 0] + 3 * X[:, 1] - 1.5 * X[:, 2] + 0.5 * X[:, 3] +
np.random.randn(n_samples) * 0.1)
# División y escalado
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"Datos generados: {X.shape} muestras, {X.shape} características")
print(f"Entrenamiento: {X_train.shape} muestras")
print(f"Prueba: {X_test.shape} muestras")
# 2. Comparación de técnicas de regularización
print("\n2. COMPARACIÓN DE MODELOS")
print("=" * 40)
modelos = {
'Linear Regression': LinearRegression(),
'Ridge (α=0.1)': Ridge(alpha=0.1),
'Ridge (α=1.0)': Ridge(alpha=1.0),
'Ridge (α=10.0)': Ridge(alpha=10.0),
'Lasso (α=0.01)': Lasso(alpha=0.01),
'Lasso (α=0.1)': Lasso(alpha=0.1),
'Lasso (α=1.0)': Lasso(alpha=1.0),
'Elastic Net (α=0.1, l1=0.5)': ElasticNet(alpha=0.1, l1_ratio=0.5),
'Elastic Net (α=1.0, l1=0.7)': ElasticNet(alpha=1.0, l1_ratio=0.7)
}
resultados_completos = []
for nombre, modelo in modelos.items():
# Entrenamiento
modelo.fit(X_train_scaled, y_train)
# Predicciones
y_pred_train = modelo.predict(X_train_scaled)
y_pred_test = modelo.predict(X_test_scaled)
# Métricas
mse_train = mean_squared_error(y_train, y_pred_train)
mse_test = mean_squared_error(y_test, y_pred_test)
r2_train = r2_score(y_train, y_pred_train)
r2_test = r2_score(y_test, y_pred_test)
# Análisis de coeficientes
if hasattr(modelo, 'coef_'):
coefs = modelo.coef_
n_zero_coefs = np.sum(np.abs(coefs) < 1e-6)
l1_norm = np.sum(np.abs(coefs))
l2_norm = np.sqrt(np.sum(coefs**2))
else:
n_zero_coefs, l1_norm, l2_norm = 0, 0, 0
# Validación cruzada
cv_scores = cross_val_score(modelo, X_train_scaled, y_train,
cv=5, scoring='neg_mean_squared_error')
cv_mean = -cv_scores.mean()
cv_std = cv_scores.std()
resultado = {
'modelo': nombre,
'mse_train': mse_train,
'mse_test': mse_test,
'r2_train': r2_train,
'r2_test': r2_test,
'overfitting': mse_test - mse_train,
'coefs_zero': n_zero_coefs,
'l1_norm': l1_norm,
'l2_norm': l2_norm,
'cv_mean': cv_mean,
'cv_std': cv_std
}
resultados_completos.append(resultado)
# 3. Análisis de resultados
print("\n3. ANÁLISIS DETALLADO")
print("=" * 40)
df_resultados = pd.DataFrame(resultados_completos)
print("Rendimiento de modelos:")
print(df_resultados[['modelo', 'mse_train', 'mse_test', 'r2_test',
'overfitting', 'coefs_zero']].round(4))
# 4. Selección del mejor modelo
print("\n4. SELECCIÓN DEL MEJOR MODELO")
print("=" * 40)
# Criterio: menor MSE de prueba con menor overfitting
df_resultados['score_compuesto'] = (df_resultados['mse_test'] +
0.1 * df_resultados['overfitting'])
mejor_modelo = df_resultados.loc[df_resultados['score_compuesto'].idxmin()]
print(f"Mejor modelo: {mejor_modelo['modelo']}")
print(f"MSE Test: {mejor_modelo['mse_test']:.4f}")
print(f"R² Test: {mejor_modelo['r2_test']:.4f}")
print(f"Coeficientes cero: {mejor_modelo['coefs_zero']}")
# 5. Optimización de hiperparámetros
print("\n5. OPTIMIZACIÓN DE HIPERPARÁMETROS")
print("=" * 40)
# Grid Search para Elastic Net
param_grid = {
'alpha': np.logspace(-3, 1, 20),
'l1_ratio': np.linspace(0.1, 0.9, 9)
}
elastic_net = ElasticNet(max_iter=1000)
grid_search = GridSearchCV(
elastic_net, param_grid, cv=5,
scoring='neg_mean_squared_error', n_jobs=-1
)
grid_search.fit(X_train_scaled, y_train)
print(f"Mejores parámetros: {grid_search.best_params_}")
print(f"Mejor score CV: {-grid_search.best_score_:.4f}")
# Evaluación del modelo optimizado
modelo_optimizado = grid_search.best_estimator_
y_pred_opt = modelo_optimizado.predict(X_test_scaled)
mse_opt = mean_squared_error(y_test, y_pred_opt)
r2_opt = r2_score(y_test, y_pred_opt)
print(f"MSE optimizado: {mse_opt:.4f}")
print(f"R² optimizado: {r2_opt:.4f}")
return df_resultados, modelo_optimizado
# Ejecutar el proyecto completo
resultados, modelo_final = proyecto_completo_regularizacion()
La regularización es fundamental para construir modelos robustos que generalicen bien a datos nuevos. La elección entre L1, L2 o Elastic Net depende del problema específico: L1 para selección automática de características, L2 para estabilidad con características correlacionadas, y Elastic Net como compromiso entre ambos enfoques. La validación cruzada proporciona una evaluación confiable de la capacidad de generalización, mientras que el equilibrio sesgo-varianza guía las decisiones de diseño del modelo.
Hi :)
Matemáticas
Vectores
Álgebra Lineal
Geometría Analítica
Producto Punto
Espacios Vectoriales
Ortogonalidad
Normalización
Funciones
Álgebra
Composición de Funciones
Función Inversa
Combinación de Funciones
Transformaciones Gráficas
Aplicaciones Económicas
Interés Compuesto
Proporcionalidad
R
Data
Machine Learning
Aprendizaje Supervisado
Inteligencia Artificial
Clasificación
Regresión
Deep Learning