Hasta aquí hemos usado un solo modelo, la regresión logística, porque hacía falta uno para poder hablar de lo demás. Hoy probamos ocho 🎰
Y quiero que apuestes antes de mirar: ¿cuál crees que va a ganar? Apúntalo mentalmente, porque la respuesta es la lección del capítulo y funciona mucho mejor si primero te mojas 🎲
Y te adelanto el final, porque es lo interesante del capítulo: gana la regresión logística, que es el más simple y el más viejo de todos. Los que salen en las noticias quedan por debajo.
Esto pasa mucho más de lo que te cuentan, y saber por qué es más útil que saberse los ocho algoritmos.
El punto de partida
import pandas as pd from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder, StandardScaler URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv' def carga_limpia(url): v = pd.read_csv(url).drop_duplicates() v['ciudad'] = (v['ciudad'].str.strip().str.lower() .str.normalize('NFKD') .str.encode('ascii', 'ignore').str.decode('utf-8')) v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.')) for col in ['fecha', 'fecha_ultima_compra']: f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce') falta = f.isna() & v[col].notna() f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce') v[col] = f return v def prepara(v): v = v.sort_values(['cliente_id', 'fecha']).copy() v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int) v['sin_descuento'] = v['descuento'].isna().astype(int) v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int) v['precio_unitario'] = v['monto'] / v['unidades'] v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1 return v NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario', 'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero'] CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria'] def arma(clasificador): """El pipeline del capítulo 11 con el modelo que le pases.""" pre = ColumnTransformer([ ('num', Pipeline([('r', SimpleImputer(strategy='median')), ('e', StandardScaler())]), NUMERICAS), ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')), ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS), ]) return Pipeline([('pre', pre), ('mod', clasificador)]) datos = prepara(carga_limpia(URL)) X = datos[NUMERICAS + CATEGORICAS] y = datos['compro'] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) print(X_tr.shape, X_te.shape)
(2250, 13) (750, 13)
1. Regresión logística: la que hay que entender
el modelo suma las columnas con un peso cada una y esa suma la aplasta entre 0 y 1 para que se pueda leer como probabilidad
Le da un peso a cada columna, los suma y convierte esa suma en una probabilidad. Nada más. Es de 1958 y sigue ganando.
logistica = arma(LogisticRegression(max_iter=1000, random_state=42)).fit(X_tr, y_tr) print('exactitud:', round(logistica.score(X_te, y_te), 4)) print('AUC :', round(roc_auc_score(y_te, logistica.predict_proba(X_te)[:, 1]), 4))
exactitud: 0.6733 AUC : 0.7214
Sus dos ventajas son enormes y no se ven en el número: se puede explicar (cada peso dice cuánto suma o resta cada columna) y no se rompe con datos que no vio.
Su límite: solo sabe sumar. No puede aprender que "WhatsApp funciona solo para mayoristas", que en el capítulo 4 vimos que pasa. Esas interacciones hay que dárselas escritas.
2. Árbol de decisión: el que se lee
cuánto se mezclan las clases dentro de una hoja: cero si todas son iguales y máximo si están mitad y mitad
from sklearn.tree import DecisionTreeClassifier arbol = arma(DecisionTreeClassifier(max_depth=4, random_state=42)).fit(X_tr, y_tr) print('exactitud:', round(arbol.score(X_te, y_te), 4)) print('AUC :', round(roc_auc_score(y_te, arbol.predict_proba(X_te)[:, 1]), 4))
exactitud: 0.6413 AUC : 0.6643
Peor que la logística. Un árbol solo hace cortes rectos ("monto mayor que 532") y con cuatro niveles se queda corto.
Lo que sí tiene es que se puede dibujar y enseñar en una reunión, y eso a veces vale más que cinco puntos de AUC 🌳
3. El mismo árbol sin límite: la lección del capítulo
lo mismo medido en bits, o sea cuántas preguntas de sí o no harían falta para adivinar la clase de una fila
libre = arma(DecisionTreeClassifier(random_state=42)).fit(X_tr, y_tr) print('en train:', round(libre.score(X_tr, y_tr), 4)) print('en test :', round(libre.score(X_te, y_te), 4)) print('AUC :', round(roc_auc_score(y_te, libre.predict_proba(X_te)[:, 1]), 4))
en train: 1.0 en test : 0.5613 AUC : 0.5525
1,0 en entrenamiento y 0,5613 en examen. Cien por ciento perfecto en lo que ya vio, y peor que el listón en lo que no 😱
Eso es el sobreajuste, el overfitting, en su forma más pura: el árbol creció hasta que cada hoja tenía una sola venta, o sea que memorizó las 2.250 filas una por una. Un diccionario, no un modelo.
Guárdate el par de números. 1,0 en train es siempre una alarma, nunca un logro.
4. Bosque aleatorio: muchos árboles votando
from sklearn.ensemble import RandomForestClassifier bosque = arma(RandomForestClassifier(n_estimators=300, random_state=42, n_jobs=-1)).fit(X_tr, y_tr) print('en train:', round(bosque.score(X_tr, y_tr), 4)) print('AUC :', round(roc_auc_score(y_te, bosque.predict_proba(X_te)[:, 1]), 4))
en train: 1.0 AUC : 0.677
0,677. Mejor que el árbol solo (0,6643) y todavía por debajo de la logística (0,7214).
La idea del bosque es preciosa: entrena 300 árboles, cada uno con una muestra distinta de filas y de columnas, y los hace votar. Los errores de uno los corrigen los otros.
Pero fíjate en el train: 1,0 otra vez. Los
árboles siguen memorizando por dentro; lo que pasa es que el promedio de 300
memorizaciones distintas generaliza algo mejor. Y por defecto scikit-learn los
deja crecer sin límite 😬
podado = arma(RandomForestClassifier(n_estimators=300, max_depth=5, random_state=42, n_jobs=-1)).fit(X_tr, y_tr) print('en train:', round(podado.score(X_tr, y_tr), 4)) print('AUC :', round(roc_auc_score(y_te, podado.predict_proba(X_te)[:, 1]), 4))
en train: 0.704 AUC : 0.7115
Limitando la profundidad a 5: el train baja a 0,704 y el AUC sube a 0,7115. Casi cuatro puntos ganados por poner un freno.
Es de las cosas más rentables que existen y casi nadie la hace, porque
RandomForestClassifier() a secas ya "funciona" 🌲
5 y 6. Boosting: los árboles que se corrigen
el error se parte en tres, y bajar uno de los dos primeros normalmente sube el otro, mientras el tercero no se puede tocar por más modelo que le eches
from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier boosting = arma(GradientBoostingClassifier(random_state=42)).fit(X_tr, y_tr) print('boosting :', round(roc_auc_score(y_te, boosting.predict_proba(X_te)[:, 1]), 4)) rapido = arma(HistGradientBoostingClassifier(random_state=42)).fit(X_tr, y_tr) print('hist boosting:', round(roc_auc_score(y_te, rapido.predict_proba(X_te)[:, 1]), 4))
boosting : 0.6876 hist boosting: 0.6799
0,6876 y 0,6799. Los dos por debajo de la logística.
El boosting es distinto del bosque: en vez de 300 árboles independientes, entrena uno detrás de otro y cada uno se dedica a los casos que falló el anterior. Suele ser lo que gana las competencias con datos de tabla, y aquí no gana. Ya llegaremos a por qué.
HistGradientBoosting es la versión rápida, la que compite con
XGBoost y LightGBM, y además acepta nulos sin imputar, que es
comodísimo.
7 y 8. Los otros dos que hay que conocer
from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB vecinos = arma(KNeighborsClassifier(n_neighbors=25)).fit(X_tr, y_tr) print('vecinos :', round(roc_auc_score(y_te, vecinos.predict_proba(X_te)[:, 1]), 4)) bayes = arma(GaussianNB()).fit(X_tr, y_tr) print('naive bayes:', round(roc_auc_score(y_te, bayes.predict_proba(X_te)[:, 1]), 4))
vecinos : 0.6766 naive bayes: 0.7145
Vecinos no aprende nada: guarda todas las filas y, cuando le preguntas, busca las 25 más parecidas y vota. Es lentísimo con datos grandes y necesita sí o sí que las columnas estén escaladas, cosa que nuestro pipeline ya hace.
Naive Bayes saca 0,7145, que es el segundo mejor de los ocho. Y es el más simple de todos: asume que las columnas son independientes entre sí, cosa que es falsa siempre, y aun así funciona. Se llama "naive" (ingenuo) por eso 🤓
La tabla completa
| Modelo | AUC | Train | Para qué sirve |
|---|---|---|---|
| Regresión logística | 0,7214 | 0,6658 | Casi siempre el primero. Se explica y no se rompe |
| Naive Bayes | 0,7145 | 0,6480 | Rapidísimo, buen listón, muy usado en texto |
| Bosque podado | 0,7115 | 0,7040 | Cuando hay interacciones y no linealidad |
| Boosting | 0,6876 | 0,7493 | Suele ganar con más datos y con ajuste |
| SVM | 0,6826 | 0,7320 | Pocos datos y muchas columnas |
| Hist boosting | 0,6799 | 0,9387 | Como el boosting pero rápido y aguanta nulos |
| Bosque sin podar | 0,6770 | 1,0000 | Lo de arriba, mal configurado |
| Vecinos | 0,6766 | 0,6787 | Didáctico. En producción casi nunca |
| Árbol d=4 | 0,6643 | 0,6658 | Cuando hay que dibujarlo en una reunión |
| Árbol sin límite | 0,5525 | 1,0000 | Nada. Es el ejemplo de qué no hacer |
Mira la columna train de arriba abajo: los que están arriba
tienen train parecido al test, y los que están abajo lo tienen altísimo.
Esa columna predice el orden mejor que el nombre del algoritmo 📐
¿Y si los juntamos a todos?
Es la pregunta que sale sola mirando la tabla, y tiene nombre: apilar modelos, o stacking. La idea es bonita. Entrenas varios modelos distintos, y encima pones uno chiquito cuyo único trabajo es aprender cuándo hacerle caso a cada uno.
No es lo mismo que el bosque aleatorio, ojo. Ahí eran muchos árboles iguales votando. Acá son modelos de familias distintas, que se equivocan en cosas distintas, y alguien arriba decidiendo.
from sklearn.ensemble import StackingClassifier base = [('log', arma(LogisticRegression(max_iter=1000))), ('bosque', arma(RandomForestClassifier(n_estimators=300, random_state=42))), ('boost', arma(GradientBoostingClassifier(random_state=42)))] apilado = StackingClassifier(estimators=base, final_estimator=LogisticRegression(), cv=5) apilado.fit(X_tr, y_tr) print('apilado :', round(roc_auc_score(y_te, apilado.predict_proba(X_te)[:, 1]), 4)) for nombre, modelo in base: modelo.fit(X_tr, y_tr) print(f'{nombre:8}:', round(roc_auc_score(y_te, modelo.predict_proba(X_te)[:, 1]), 4))
apilado : 0.7206 log : 0.7214 bosque : 0.677 boost : 0.6876
Tres modelos, uno arriba coordinando, validación cruzada por dentro, bastante más tiempo de entrenamiento. Y saca 0,7206 contra los 0,7214 de la regresión logística sola.
Pierde. Por poquito, pero pierde, y encima cuesta varias veces más de entrenar y es muchísimo más difícil de explicar en una reunión.
Y tiene su explicación, que es la misma de todo el capítulo. Apilar gana cuando los modelos de abajo son parecidos de buenos y se equivocan en sitios distintos. Acá uno saca 0,72 y los otros dos 0,68, así que el de arriba no tiene gran cosa que combinar: lo mejor que puede hacer es copiar al primero, y le sale un pelín peor por el ruido de intentarlo 📊
Guarda la técnica igual. En competencias de datos apilar es lo que gana, y en esos datos sí se cumple la condición. Lo que no hay que hacer es traerla acá por defecto: si la complicación no se paga en el número, no se paga.
Por qué gana la simple, que es lo que hay que entender
Los modelos complicados ganan cuando hay algo complicado que aprender. Aquí no lo hay, y por tres razones concretas:
- 📉 Poca señal. El mejor AUC del archivo es 0,72. Cuando la señal es débil, lo que un modelo flexible encuentra de más suele ser ruido.
- 📊 Pocas filas. 2.250 para entrenar es poco. El boosting y los bosques empiezan a brillar con decenas de miles.
- ➕ Relaciones simples. Del capítulo 4: más monto, más cierre; más satisfacción, más cierre. Todo monótono, que es justo lo que una recta hace bien.
Y la regla que yo sigo: empieza siempre por la logística, y solo cambia si otro modelo le gana de verdad. Con "de verdad" quiero decir en validación cruzada, que es el capítulo 16, y no en una sola partición 💛
Ejercicios
Siete. Intenta antes de abrir 💛
1. El SVM, sin la trampa de las probabilidades
Entrena una máquina de vectores de soporte y saca su AUC sin pedirle probabilidades.
from sklearn.svm import SVC svm = arma(SVC(random_state=42)).fit(X_tr, y_tr) print('exactitud:', round(svm.score(X_te, y_te), 4)) print('AUC :', round(roc_auc_score(y_te, svm.decision_function(X_te)), 4))
exactitud: 0.644 AUC : 0.6826
Fíjate en decision_function en vez de
predict_proba: el SVM no da probabilidades de forma natural, da una
distancia a la frontera. Para el AUC da igual, porque el AUC solo mira el
orden 📐
Pedirle probabilidades con probability=True lo hace muchísimo más
lento y además está en camino de desaparecer de scikit-learn.
2. Cuántos vecinos
Prueba k = 1, 5, 25 y 100 y mira el patrón.
for k in (1, 5, 25, 100): m = arma(KNeighborsClassifier(n_neighbors=k)).fit(X_tr, y_tr) print(f'k={k:3} train {m.score(X_tr, y_tr):.4f} ' f'AUC {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')
k= 1 train 1.0000 AUC 0.5658 k= 5 train 0.7289 AUC 0.6201 k= 25 train 0.6787 AUC 0.6766 k=100 train 0.6520 AUC 0.6985
Con k=1 el train da 1,0 (cada fila es su propio vecino más cercano) y el AUC se hunde. Al subir k, el train baja y el AUC sube.
Ese k es el ejemplo más limpio que existe del equilibrio entre
memorizar y generalizar: es literalmente un dial 🎚️
3. Qué columnas usa el bosque
Las cinco columnas más importantes según el bosque podado.
import numpy as np nombres = podado.named_steps['pre'].get_feature_names_out() peso = podado.named_steps['mod'].feature_importances_ for i in np.argsort(peso)[::-1][:5]: print(f'{nombres[i]:30} {peso[i]:.4f}')
num__monto 0.1872 num__satisfaccion 0.1281 num__precio_unitario 0.1193 cat__segmento_Bodega 0.1046 num__sin_compra_previa 0.0894
Ojo con esta lista, que engaña: la importancia de un bosque favorece a las columnas con muchos valores distintos, porque ofrecen más sitios donde cortar.
Aquí se ve: el monto se lleva 0,1872 y sin_compra_previa, que en
el capítulo 4 medimos en veinte puntos de tasa de cierre, se queda en 0,0894, la
mitad. No es que valga la mitad: es que es una columna de dos valores y solo
ofrece un sitio donde cortar.
La forma honesta de medir importancia es otra y está en el capítulo 20 🔍
4. El árbol, dibujado en palabras
Enseña las primeras reglas del árbol de profundidad 3.
from sklearn.tree import export_text corto = arma(DecisionTreeClassifier(max_depth=3, random_state=42)).fit(X_tr, y_tr) nombres3 = corto.named_steps['pre'].get_feature_names_out() print(export_text(corto.named_steps['mod'], feature_names=list(nombres3))[:700])
|--- num__monto <= -0.64 | |--- num__satisfaccion <= 1.09 | | |--- num__sin_compra_previa <= 0.83 | | | |--- class: 0 | | |--- num__sin_compra_previa > 0.83 | | | |--- class: 0 | |--- num__satisfaccion > 1.09 | | |--- num__precio_unitario <= -0.46 | | | |--- class: 0 | | |--- num__precio_unitario > -0.46 | | | |--- class: 1 |--- num__monto > -0.64 | |--- num__sin_compra_previa <= 0.83 | | |--- num__satisfaccion <= 0.36 | | | |--- class: 1 | | |--- num__satisfaccion > 0.36 | | | |--- class: 1 | |--- num__sin_compra_previa > 0.83 | | |--- num__visita_numero <= -0.43 | | | |--- class: 1 | | |--- num__visita_
Eso se puede pegar en un correo y lo entiende cualquiera. Es la razón por la que los árboles sobreviven: en algunos sitios, poder explicar la regla vale más que acertar más.
Un banco que rechaza un crédito tiene que decir por qué. Con esto puede; con un bosque de 300 árboles, no 🏦
5. El boosting, cuánto le importa la tasa de aprendizaje
Prueba tres tasas y mira el sobreajuste.
for tasa in (0.01, 0.1, 0.5): m = arma(GradientBoostingClassifier(learning_rate=tasa, random_state=42)).fit(X_tr, y_tr) print(f'tasa {tasa:<5} train {m.score(X_tr, y_tr):.4f} ' f'AUC {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')
tasa 0.01 train 0.6796 AUC 0.6977 tasa 0.1 train 0.7493 AUC 0.6876 tasa 0.5 train 0.9044 AUC 0.6515
Con 0,01 aprende despacito y generaliza mejor; con 0,5 se lanza y memoriza. Esa es la perilla más importante del boosting, y la regla es tasa baja y muchos árboles.
Con 0,01 este boosting ya se acerca a la logística. Con más datos la pasaría 📈
6. El error de pedirle probabilidades a quien no las tiene
Pídele predict_proba a un SVM normal.
svm.predict_proba(X_te)
AttributeError: This 'Pipeline' has no attribute 'predict_proba'
No lo tiene. Y es un error honesto: en vez de inventarse un número, el objeto te dice que esa operación no existe para él.
Es un buen recordatorio de que no todos los modelos dan lo
mismo. Si tu código pide predict_proba a ciegas, el día que
alguien cambie el clasificador se rompe 🔌
7. Los ocho de una pasada
Un bucle que los entrena todos y los ordena por AUC. Es el que yo corro al empezar cualquier proyecto.
candidatos = {
'logistica': LogisticRegression(max_iter=1000, random_state=42),
'naive bayes': GaussianNB(),
'bosque podado': RandomForestClassifier(n_estimators=300, max_depth=5,
random_state=42, n_jobs=-1),
'boosting': GradientBoostingClassifier(random_state=42),
'vecinos': KNeighborsClassifier(n_neighbors=25),
'arbol d=4': DecisionTreeClassifier(max_depth=4, random_state=42),
}
resultados = []
for nombre, clf in candidatos.items():
m = arma(clf).fit(X_tr, y_tr)
resultados.append((roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]),
m.score(X_tr, y_tr), nombre))
for auc, train, nombre in sorted(resultados, reverse=True):
print(f'{nombre:15} AUC {auc:.4f} train {train:.4f}')
logistica AUC 0.7214 train 0.6658 naive bayes AUC 0.7145 train 0.6480 bosque podado AUC 0.7115 train 0.7040 boosting AUC 0.6876 train 0.7493 vecinos AUC 0.6766 train 0.6787 arbol d=4 AUC 0.6643 train 0.6658
Ese bucle son diez líneas y contesta en cinco segundos la pregunta que más tiempo se pierde discutiendo: ¿qué modelo usamos?
Y ojo con lo que no hace: no ajusta hiperparámetros ni valida en varias particiones. Sirve para descartar rápido, no para decidir. Decidir es el capítulo 16 🎯
Comprueba que lo tienes
La regresión logística le gana a un bosque aleatorio en estos datos. ¿Qué significa?
- Que la relación es sencilla y no hace falta un modelo complicado
- Que el bosque está mal configurado
- Que hay pocos datos
- Que hay que usar redes neuronales
El examen que dejó de serlo
La trampa
Entrenas los ocho, los ordenas por su nota en el examen y te quedas con el mejor. Es exactamente lo que hace la tabla de este capítulo.
for nombre, mod in modelos.items(): mod.fit(X_tr, y_tr) auc[nombre] = evalua(mod, X_te, y_te) ganador = max(auc, key=auc.get)
Qué está mal
Acabas de usar el examen ocho veces para tomar una decisión, así que el examen dejó de ser un examen 📄 El AUC del ganador ya no es lo que va a rendir con datos nuevos: es lo que rinde el mejor de ocho intentos sobre esas filas concretas, y parte de esa ventaja es suerte.
Cuantos más modelos comparas, más se infla. Con ocho es poco y con cincuenta combinaciones de hiperparámetros ya es mucho, que es justo lo que hace una búsqueda automática.
Lo correcto son tres trozos y no dos: uno para entrenar, otro para elegir entre modelos y un tercero que no se toca hasta el final y se usa una sola vez. Cuando no dan los datos para tres, se elige con validación cruzada sobre el entrenamiento y el examen se reserva para el número que vas a reportar. Eso es el capítulo 16.
Lo que te llevas
- 🥇 Aquí gana la regresión logística con 0,7214, por delante de bosques y boosting. Empieza siempre por ella.
- 🚨 Un árbol sin límite da 1,0 en train y 0,5525 en test. 1,0 en train es una alarma, nunca un logro.
- 🌲 El bosque por defecto crece sin límite: limitándolo a profundidad 5, el AUC sube de 0,677 a 0,7115.
- 📈 El boosting entrena árboles que corrigen al anterior. Brilla con muchos datos; con 2.250 filas, no.
- 🎚️ El
kde los vecinos y la tasa del boosting son diales de memorizar contra generalizar. - 📐 La columna
trainordena los modelos mejor que su nombre. - 🔍 La importancia de columnas de un bosque favorece a las que tienen muchos valores. No es de fiar.
Y si de todo el capítulo te llevas una sola frase, que sea esta:
El modelo complicado no inventa señal donde no la hay. Solo añade formas de equivocarse.
Y si quieres ver estos mismos modelos con redes neuronales al lado, y cuándo ganan y cuándo no, eso es el libro de deep learning desde cero 🧠
En el capítulo 14 dejamos de mirar un solo número: matriz de confusión, precisión, exhaustividad y por qué un 95% de exactitud puede ser una vergüenza.
Que tengas lindo día! 🌸
Preguntas frecuentes
¿Qué es el algoritmo KNN?
Vecinos más cercanos: para predecir algo mira los casos más parecidos que ya conoce y copia lo que pasó con ellos. No entrena nada, se guarda los datos y compara.
¿Qué es un árbol de decisión?
Una cadena de preguntas de sí o no que va partiendo los datos. Es el modelo más fácil de explicar a alguien que no es técnico, y por eso vale mucho más de lo que su precisión sugiere.
¿Cuál es la diferencia entre clasificación y regresión?
La clasificación predice una etiqueta, como compra o no compra. La regresión predice un número, como cuánto va a comprar.
¿Qué modelo elijo para empezar?
El más simple que resuelva tu caso, y compararlo siempre contra una línea base tonta. Si un modelo con árboles no le gana a "predice siempre el promedio", el problema no era el modelo.
Practica este capítulo 📓
Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.
¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.
Los términos que salen en este capítulo
Están todos en el glosario de IA, con su definición corta.