12. Evaluación de modelos#
Introducción
Después de discutir los fundamentos del aprendizaje supervisado y sus algoritmos, abordaremos la evaluación de modelos y la selección de parámetros. Nos enfocaremos en modelos supervisados, específicamente en regresión y clasificación, ya que la evaluación en aprendizaje no supervisado es más cualitativa.
Para evaluar modelos supervisados, dividimos el conjunto de datos en entrenamiento y prueba usando
train_test_split
, construimos un modelo confit
, y lo evaluamos en el conjunto de prueba conscore
, que calcula la fracción de muestras correctamente clasificadas.
import warnings
warnings.filterwarnings('ignore')
from sklearn.datasets import make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
Creamos un conjunto de datos sintético
X, y = make_blobs(random_state=0)
Dividimos los datos y las etiquetas en un conjunto de entrenamiento y otro de prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
Instanciar el modelo y ajustarlo al conjunto de entrenamiento
logreg = LogisticRegression().fit(X_train, y_train)
Evaluar el modelo en el conjunto de prueba
print("Test set score: {:.2f}".format(logreg.score(X_test, y_test)))
Test set score: 0.88
Evaluar el modelo en el conjunto de entrenamiento
print("Train set score: {:.2f}".format(logreg.score(X_train, y_train)))
Train set score: 0.91
Observación
Recuerde que la razón por la que dividimos nuestros datos en conjuntos de entrenamiento y de prueba, es que estamos interesados en medir lo bien que nuestro modelo se generaliza a nuevos datos (no vistos anteriormente).
No nos interesa lo bien que nuestro modelo se ajusta al conjunto de entrenamiento, sino lo bien que puede hacer predicciones sobre datos no observados durante el entrenamiento.
En esta sección, ampliaremos dos aspectos de esta evaluación. En primer lugar, introduciremos la validación cruzada (
cross-validation
), una forma más sólida de evaluar el rendimiento de la generalización, y discutiremos los métodos para evaluar el rendimiento de la clasificación y la regresión que van más allá de las medidas por defectoaccuracy
y \(R^2\) proporcionadas por el métodoscore
.También hablaremos del
GridSearchCV
, un método eficaz para ajustar los parámetros de los modelos supervisados y obtener el mejor rendimiento de la generalización.
12.1. Cross-Validation#
Introducción
La validación cruzada (
cross-validation
) es un método estadístico para evaluar el rendimiento de la generalización que es más estable y exhaustivo que el uso de una división en un conjunto de entrenamiento y otro de prueba.En la validación cruzada, los datos se dividen repetidamente y se entrenan múltiples modelos. La versión más utilizada de la validación cruzada es
k-fold cross-validation
, dondek
es un número especificado por el usuario, normalmente 5 o 10.
Cuando se realiza la validación cruzada
five-fold
, los datos se dividen primero en cinco partes de tamaño (aproximadamente) igual, llamadas pliegues (folds
). A continuación, se entrena una secuencia de modelos. El primer modelo se entrena utilizando el primer pliegue como conjunto de prueba, y los pliegues restantes (2-5) se utilizan como conjunto de entrenamiento. El modelo se construye utilizando los datos de los pliegues 2-5, y luego se evalúa accuracy en el pliegue 1.A continuación, luego se construye otro modelo, esta vez utilizando el pliegue 2 como conjunto de prueba y los datos de los pliegues 1, 3, 4 y 5 como conjunto de entrenamiento. Este proceso se repite utilizando los pliegues 3, 4 y 5 como conjuntos de prueba. Para cada una de estas cinco divisiones de los datos en conjuntos de entrenamiento y de prueba, calculamos
accuracy
. Al final, hemos recogido cinco valores deaccuracy
.
import mglearn
mglearn.plots.plot_cross_validation()

Normalmente, la primera quinta parte de los datos es conocida como el primer fold, la segunda quinta parte de los datos es el segundo fold, y así sucesivamente.
12.1.1. Validación cruzada en scikit-learn#
La validación cruzada se implementa en
scikit-learn
utilizando la funcióncross_val_score
del módulomodel_selection
. Los parámetros de la funcióncross_val_score
son, el modelo que queremos evaluar, los datos de entrenamiento y las etiquetas reales. Vamos a evaluarLogisticRegression
en el conjunto de datosiris
.Utilizaremos los parámetros por defecto de este modelo, más adelante estudiaremos como conseguir los más óptimos por medio de
grid-search
, por ahora solo estamos interesados en evaluar el modelo por defecto usandocross_val_score
. Para más información acerca de los argumentos del modelo (ver sklearn.linear_model.LogisticRegression).
Iris dataset
Objetivo Principal: Predecir la especie de una flor
iris
basándose en medidas de sus características morfológicas. El dataset contiene tres especies deiris
:setosa
,versicolor
yvirginica
, y las características medidas son el largo y el ancho del sépalo y el pétalo.Uso: Este problema de clasificación permite evaluar la capacidad de un modelo para diferenciar entre las especies basándose en características continuas.

from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
import numpy as np
iris = load_iris()
iris.data.shape
(150, 4)
iris.target.shape
(150,)
for i, feature in enumerate(iris.feature_names):
data = iris.data[:, i]
print(f"{feature}:")
print(f" Mean: {np.mean(data)}")
print(f" Std: {np.std(data)}")
print(f" Min: {np.min(data)}")
print(f" Max: {np.max(data)}")
print(f" 25th percentile (Q1): {np.percentile(data, 25)}")
print(f" Median (Q2): {np.median(data)}")
print(f" 75th percentile (Q3): {np.percentile(data, 75)}")
print()
sepal length (cm):
Mean: 5.843333333333334
Std: 0.8253012917851409
Min: 4.3
Max: 7.9
25th percentile (Q1): 5.1
Median (Q2): 5.8
75th percentile (Q3): 6.4
sepal width (cm):
Mean: 3.0573333333333337
Std: 0.4344109677354946
Min: 2.0
Max: 4.4
25th percentile (Q1): 2.8
Median (Q2): 3.0
75th percentile (Q3): 3.3
petal length (cm):
Mean: 3.7580000000000005
Std: 1.759404065775303
Min: 1.0
Max: 6.9
25th percentile (Q1): 1.6
Median (Q2): 4.35
75th percentile (Q3): 5.1
petal width (cm):
Mean: 1.1993333333333336
Std: 0.7596926279021594
Min: 0.1
Max: 2.5
25th percentile (Q1): 0.3
Median (Q2): 1.3
75th percentile (Q3): 1.8
print("Target (species):")
unique, counts = np.unique(iris.target, return_counts=True)
for i, count in zip(unique, counts):
print(f" {iris.target_names[i]}: {count} samples")
Target (species):
setosa: 50 samples
versicolor: 50 samples
virginica: 50 samples
Queda como ejercicio para el estudiante, realizar un EDA exahustivo para el dataset, teniendo en cuenta cada una de las variables y sus categorías. Evaluemos el uso de la función
cross_val_score
logreg = LogisticRegression()
scores = cross_val_score(logreg, iris.data, iris.target);
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96666667 1. 0.93333333 0.96666667 1. ]
Por defecto,
cross_val_score
realiza unafive-fold cross validation
, devolviendo cinco valores deaccuracy
. Podemos cambiar el número de pliegues (folds) utilizados, cambiando el parámetrocv
:
scores = cross_val_score(logreg, iris.data, iris.target, cv=10);
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [1. 0.93333333 1. 1. 0.93333333 0.93333333
0.93333333 1. 1. 1. ]
Una forma habitual de resumir la precisión de la validación cruzada es calcular la media
print("Average cross-validation score: {:.2f}".format(scores.mean()))
Average cross-validation score: 0.97
Utilizando la validación cruzada media podemos concluir que, esperamos que el modelo sea de precisión en torno al 97% de media. Si observamos las cinco puntuaciones producidas por la validación cruzada de cinco pliegues five-fold cross validation, también podemos concluir que hay una varianza relativamente alta en la precisión entre pliegues, que va del 100% de precisión al 93.33% de precisión aproximadamente.
Esto podría implicar que
el modelo es muy dependiente de los pliegues particulares utilizados para el entrenamiento
, pero también podría ser simplemente una consecuencia del pequeño tamaño del conjunto de datos. Normalmente, si los resultados deaccuracy
varían considerablemente durante la validación cruzada de 5 pliegues, esto puede indicar problemas en el modelo, en los datos, o en el proceso de validación.
Varianza alta de accuracy entre pliegues
Varianza en los datos: Si los datos no están bien distribuidos o existen diferencias significativas entre los pliegues, los resultados de accuracy pueden variar mucho entre cada uno. Esto sucede comúnmente cuando los datos presentan sesgos o cuando ciertos pliegues contienen muestras que son más difíciles de clasificar.
Modelo inestable o de alta varianza: Algunos modelos, especialmente los que son complejos o sensibles a los datos de entrenamiento (como los árboles de decisión sin poda o redes neuronales profundas con pocos datos), pueden tener un rendimiento inconsistente en diferentes subconjuntos de los datos. En estos casos, el modelo se ajusta demasiado a los pliegues específicos, produciendo fluctuaciones en el
accuracy
.Tamaño de la muestra: Si el conjunto de datos es pequeño, la variabilidad en los pliegues puede ser mayor, ya que cada subconjunto de datos tiene un peso proporcionalmente alto. En estos casos, la validación cruzada puede reflejar variaciones amplificadas.
Datos atípicos: La presencia de datos atípicos (outliers) en algunos de los pliegues puede afectar el
accuracy
y producir resultados más dispares. Es importante revisar si los pliegues contienen estos datos y considerar métodos para manejarlos.
12.1.2. Ventajas de la validación cruzada#
Observación
Son varios los beneficios de utilizar la validación cruzada en lugar de una única división en un conjunto de entrenamiento y otro de prueba. En primer lugar, recuerde que
train_test_split
realiza una división aleatoria de los datos. Imaginemos que tenemos “suerte” al dividir aleatoriamente los datos, y todos los ejemplos que son difíciles de clasificar acaban en el conjunto de entrenamiento.En ese caso, el conjunto de prueba sólo contendrá ejemplos “fáciles”, y nuestra precisión en el conjunto de prueba será irrealmente alto. Por el contrario, si tenemos “mala suerte”, es posible que pongamos al azar todos los ejemplos difíciles de clasificar en el conjunto de prueba y en consecuencia, obtener una score irrealmente bajo.
La validación cruzada garantiza que cada ejemplo esté en el conjunto de prueba una vez, obligando al modelo a generalizar bien en todo el conjunto. Proporciona un rango de precisión que muestra el rendimiento en el mejor y peor caso. A diferencia de dividir una sola vez, la validación cruzada usa más datos para entrenamiento (por ejemplo, 80% en cinco pliegues o 90% en diez pliegues), lo que mejora la precisión. Sin embargo, su desventaja es el mayor coste computacional, ya que se entrenan varios modelos en lugar de uno solo.
Observación
Es importante tener en cuenta que la validación cruzada no es una forma de construir un modelo que pueda aplicarse a nuevos datos. La validación cruzada no devuelve un modelo.
Cuando se llama a
cross_validation_score
, se construyen internamente múltiples modelos, pero el propósito de la validación cruzada es evaluar lo bien que un algoritmo determinado generalizará cuando es entrenado en un conjunto de datos específico.
12.1.3. Validación cruzada estratificada \(k\)-fold y otras estrategias#
Dividir el conjunto de datos en
k
pliegues comenzando por la primera parte de los datos, como descrito en la sección anterior, puede no ser siempre una buena idea. Por ejemplo, veamos el conjunto de datosiris
from sklearn.datasets import load_iris
iris = load_iris()
print("Iris labels:\n{}".format(iris.target))
Iris labels:
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2]
En este conjunto de datos, el primer tercio corresponde a la clase 0, el segundo a la clase 1 y el último a la clase 2. Al realizar una validación cruzada de 3 pliegues, cada pliegue contendría solo una clase en el conjunto de prueba y las otras dos en el conjunto de entrenamiento. Esto causaría que la precisión fuera del 0%, ya que las clases en entrenamiento y prueba no coincidirían en ninguna división, lo cual es ineficaz para evaluar el modelo.
Como la estrategia simple de
k-fold
falla aquí,scikit-learn
no la utiliza para clasificación, sino que utiliza lavalidación cruzada estratificada k-fold
. En la validación cruzada estratificada, dividimos los datos de forma que las proporciones entre las clases sean las mismas en cada pliegue como en todo el conjunto de datos.
mglearn.plots.plot_stratified_cross_validation()

La validación cruzada estratificada de
k
pliegues distribuye las clases en cada pliegue según su proporción en el conjunto de datos, evitando que un pliegue carezca de muestras de alguna clase. Esto ofrece estimaciones más fiables del rendimiento del clasificador en comparación con la validación cruzada estándar dek
pliegues, especialmente en conjuntos de datos desbalanceados.Para la regresión,
scikit-learn
utiliza la validación cruzadak-fold
estándar por defecto. Sería posible también tratar de hacer cada pliegue representativo de los diferentes valores objetivo de la regresión, pero esta no es una estrategia comúnmente utilizada.
from sklearn.model_selection import StratifiedKFold
stratified_kfold = StratifiedKFold(n_splits=3)
scores = cross_val_score(logreg, iris.data, iris.target, cv=stratified_kfold)
print("Cross-validation scores:\n{}".format(scores))
Cross-validation scores:
[0.98 0.96 0.98]
12.1.4. Más control sobre la validación cruzada#
Podemos ajustar el número de pliegues en
cross_val_score
con el parámetrocv
. No obstante,scikit-learn
permite mayor control al aceptar un divisor de validación cruzada como parámetrocv
. Aunque los valores predeterminados suelen ser adecuados, en ciertos casos puede ser útil una estrategia diferente.Por ejemplo, para replicar resultados con validación cruzada
k-fold
en clasificación, se puede importar la claseKFold
demodel_selection
e instanciarla con el número deseado de pliegues.
from sklearn.model_selection import KFold
kfold = KFold(n_splits=3)
Entonces, podemos pasar el objeto
kfold splitter
como el parámetrocv
across_val_score
:
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores:
[0. 0. 0.]
Usar validación cruzada triple (no estratificada) en el conjunto de datos iris es ineficaz, ya que cada pliegue corresponde a una clase, impidiendo el aprendizaje. Para evitarlo, se recomienda aleatorizar los datos en lugar de estratificarlos, configurando
shuffle=True
enKFold
y fijandorandom_state
para obtener resultados reproducibles. Esta aleatorización mejora significativamente los resultados.
kfold = KFold(n_splits=3, shuffle=True, random_state=0)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores:
[0.98 0.96 0.96]
12.1.5. Validación cruzada con exclusión (leave-one-out
)#
El método de validación cruzada
leave-one-out
es una variante dek
pliegues donde cada pliegue de prueba contiene solo una muestra. Aunque es más lento en conjuntos de datos grandes, puede ofrecermejores estimaciones en conjuntos de datos pequeños
.
from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
scores = cross_val_score(logreg, iris.data, iris.target, cv=loo)
print("Number of cv iterations: ", len(scores))
print("Mean accuracy: {:.2f}".format(scores.mean()))
Number of cv iterations: 150
Mean accuracy: 0.97
12.1.6. Validación cruzada aleatoria y dividida#
Otra estrategia muy flexible para la validación cruzada es la validación cruzada aleatoria (
shuffle-split cross-validation
). En la validación cruzada de división aleatoria, cada división (split
) está compuesta de tantostrain_size
puntos (disyuntos) para el conjunto de entrenamiento y tantostest_size
puntos (disjuntos) para el conjunto de prueba, se fijen inicialmente.Esta división se repite
n_iter
veces, de forma aleatoria. A continuación se muestra la ejecución de cuatro iteraciones de división de un conjunto de datos que consta de10 puntos
, con un conjunto de entrenamiento de5 puntos
y conjuntos de prueba de2 puntos
cada uno
mglearn.plots.plot_shuffle_split()

Puede usar enteros para
train_size
ytest_size
para asignarles sus tamaños absolutos, o números de tipo flotante para usar fracciones del conjunto de datos. El siguiente código divide el conjunto de datos en un 50% de entrenamiento y un 50% de prueba para 10 iteraciones
from sklearn.model_selection import ShuffleSplit
shuffle_split = ShuffleSplit(test_size=.5, train_size=.5, n_splits=10)
scores = cross_val_score(logreg, iris.data, iris.target, cv=shuffle_split)
print("Cross-validation scores:\n{}".format(scores))
Cross-validation scores:
[0.94666667 0.93333333 0.96 0.96 0.96 0.92
0.98666667 0.94666667 0.97333333 0.97333333]
La validación cruzada aleatoria permite ajustar el número de iteraciones y
usar solo una parte de los datos en cada iteración, lo cual es útil para grandes conjuntos de datos
. La variante estratificada,StratifiedShuffleSplit
, ofrece resultados más fiables en tareas de clasificación.
12.1.7. Validación cruzada con grupos#
GroupKFold
Digamos que quieres construir un sistema para reconocer emociones a partir de imágenes de rostros (o imágenes médicas), y se recopila un conjunto de datos con imágenes de 100 personas, donde cada persona es capturada varias veces, mostrando varias emociones.
El objetivo es construir un clasificador que pueda identificar correctamente las emociones de las personas que no están en el conjunto de datos.
GroupKFold
es una variación dek-fold
que garantiza que el mismo grupo no esté representado en los conjuntos de prueba y de entrenamiento.
Observación
La validación cruzada estratificada puede medir el rendimiento de un clasificador, pero si hay imágenes de la misma persona en los conjuntos de entrenamiento y prueba, el modelo reconocerá más fácilmente emociones en rostros ya vistos. Para evaluar mejor la generalización a nuevas caras, es recomendable usar
GroupKFold
, que permiteseparar las imágenes por persona en los conjuntos de entrenamiento y prueba
.
Este ejemplo ilustra el uso de la validación cruzada en un conjunto de datos sintético con agrupación definida por la matriz groups. El conjunto tiene 12 datos, organizados en cuatro grupos: los primeros tres puntos pertenecen al primer grupo, los siguientes cuatro al segundo, y así sucesivamente.
from sklearn.model_selection import GroupKFold
import numpy as np
from sklearn.datasets import make_blobs
Creamos nuestro conjunto de datos sintético, y la lista de grupos
groups
X, y = make_blobs(n_samples=12, random_state=0)
y
array([1, 0, 2, 0, 0, 1, 1, 2, 0, 2, 2, 1])
groups = [0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3]
gkf = GroupKFold(n_splits=3)
for train, test in gkf.split(X, y, groups=groups):
print("GroupKFold: %s %s" % (train, test))
Xtrain, Xtest = X[train], X[test]
ytrain, ytest = y[train], y[test]
logreg = LogisticRegression()
logreg.fit(Xtrain, ytrain)
print("LogisticRegression Score: ", logreg.score(Xtest, ytest))
GroupKFold: [ 0 1 2 7 8 9 10 11] [3 4 5 6]
LogisticRegression Score: 0.75
GroupKFold: [0 1 2 3 4 5 6] [ 7 8 9 10 11]
LogisticRegression Score: 0.6
GroupKFold: [ 3 4 5 6 7 8 9 10 11] [0 1 2]
LogisticRegression Score: 0.6666666666666666
No es necesario que las muestras estén ordenadas por grupos; sólo lo hemos hecho con fines ilustrativos. Como puede ver, para cada división, cada grupo está completamente en el conjunto de entrenamiento o completamente en el conjunto de prueba. Ademas, observe que los pliegues no tienen exactamente el mismo tamaño debido al desequilibrio de los datos.
Hay más estrategias de división para la validación cruzada en
scikit-learn
, que pueden utilizarse para una variedad aún mayor de casos (ver Cross-validation: evaluating estimator performance). Sin embargo, elKFold
estándar, elStratifiedKFold
y elGroupKFold
son, como mucho, los más utilizados. En el siguiente link puede encontrar la documentición relacionada con el uso de cada parámetro de la funciónsklearn.model_selection.cross_val_score
(ver Evaluate a score by cross-validation).
12.2. Grid Search#
Ahora que sabemos cómo evaluar el grado de generalización de un modelo, podemos dar el siguiente paso y mejorar el rendimiento de la generalización del modelo ajustando sus parámetros. Es importante entender lo que significan los parámetros antes de intentar ajustarlos. Encontrar los valores de los parámetros relevantes de un modelo (los que proporcionan el mejor rendimiento de generalización) es una tarea complicada, pero necesaria para casi todos los modelos y conjuntos de datos.
Al ser una tarea tan común, existen métodos estándar en
scikit-learn
para ayudarle con ello. El método más utilizado es lagrid search
, que básicamente significa probar todas las combinaciones posibles de los parámetros de interés. Considere el caso de un SVM con unkernel RBF
(función de base radial), como implementado en la claseSVC
. Hay dos parámetros importantes: el ancho de banda del kernel,gamma
, y el parámetro de regularización,C
.Digamos que queremos probar los valores 0.001, 0.01, 0.1, 1, 10 y 100 para el parámetro
C
, y lo mismo paragamma
. Como tenemos seis ajustes diferentes paraC
ygamma
que queremos probar, tenemos 36 combinaciones de parámetros en total. Al ver todas las combinaciones posibles, se crea una tabla (o red) de parámetros para `SVM, como se muestra aquí:

12.2.1. Grid Search simple#
Podemos implementar un
grid search
sobre los dos parámetros usando un par de ciclos for, entrenando y evaluando un clasificador para cada combinación
from sklearn.svm import SVC
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)
print("Size of training set: {} size of test set: {}".format(X_train.shape[0], X_test.shape[0]))
best_score = 0
Size of training set: 112 size of test set: 38
for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
for C in [0.001, 0.01, 0.1, 1, 10, 100]:
svm = SVC(gamma=gamma, C=C)
svm.fit(X_train, y_train)
score = svm.score(X_test, y_test)
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
print("Best score: {:.2f}".format(best_score))
print("Best parameters: {}".format(best_parameters))
Best score: 0.97
Best parameters: {'C': 100, 'gamma': 0.001}
12.2.2. El peligro de sobreajustar los parámetros y el conjunto de validación#
Teniendo en cuenta este resultado, podríamos tener la tentación de decir que hemos encontrado un modelo que funciona con un 97% de precisión en nuestro conjunto de datos. Sin embargo, esta afirmación podría ser demasiado optimista (o simplemente errónea), por la siguiente razón: hemos probado muchos parámetros diferentes y se seleccionó el que tenía la mejor precisión en el conjunto de prueba, pero
esta precisión no necesariamente la obtendremos con nuevos datos
.Como hemos utilizado los datos de prueba para ajustar los parámetros, ya no podemos utilizarlos para evaluar la calidad del modelo. Esta es la misma razón por la que necesitamos dividir los datos en conjuntos de entrenamiento y de prueba;
necesitamos un conjunto de datos independiente para evaluar, uno que no se haya utilizado para crear el modelo
.Una forma de resolver este problema es dividir los datos de nuevo, de modo que tengamos tres conjuntos: el conjunto de entrenamiento para construir el modelo, el conjunto de validación (o desarrollo) para seleccionar los parámetros del modelo, y el conjunto de prueba para evaluar el rendimiento de los parámetros seleccionados
mglearn.plots.plot_threefold_split()

Después de seleccionar los mejores parámetros utilizando el conjunto de validación, podemos reconstruir un modelo utilizando los parámetros ajustados que encontramos, pero ahora entrenado tanto en los datos de entrenamiento y los datos de validación. De esta forma, podemos utilizar tantos datos como sea posible para construir nuestro modelo. Esto nos lleva a la siguiente implementación
from sklearn.svm import SVC
Dividimos los datos en conjunto de entrenamiento+validación y conjunto de prueba
X_trainval, X_test, y_trainval, y_test = train_test_split(iris.data, iris.target, random_state=0)
Dividimos el conjunto de entrenamiento+validación en conjuntos de entrenamiento y validación
X_train, X_valid, y_train, y_valid = train_test_split(X_trainval, y_trainval, random_state=1)
print("Size of training set: {} size of validation set: {} size of test set: {}\n".
format(X_train.shape[0], X_valid.shape[0], X_test.shape[0]))
Size of training set: 84 size of validation set: 28 size of test set: 38
best_score = 0
for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
for C in [0.001, 0.01, 0.1, 1, 10, 100]:
svm = SVC(gamma=gamma, C=C)
svm.fit(X_train, y_train) # Ajuste del modelo SVC
score = svm.score(X_valid, y_valid) # Score para selección de parámetros
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma} # Almacenamos el mejor score y sus parámetros
Reconstruimos el modelo en el conjunto combinado de entrenamiento y validación, y lo evaluamos en el conjunto de prueba
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)
test_score = svm.score(X_test, y_test)
print("Best score on validation set: {:.2f}".format(best_score))
print("Best parameters: ", best_parameters)
print("Test set score with best parameters: {:.2f}".format(test_score))
Best score on validation set: 0.96
Best parameters: {'C': 10, 'gamma': 0.001}
Test set score with best parameters: 0.92
El mejor score en el conjunto de validación es del 96%: ligeramente inferior a la anterior, probablemente porque utilizamos menos datos para entrenar el modelo (X_train es menor ahora porque dividimos nuestro conjunto de datos dos veces). Sin embargo, el score en el conjunto de prueba, el que realmente nos dice que tan buena es la generalización, es aún más bajo, un 92%. Así que solo podemos afirmar que clasificamos los nuevos datos con un 92% de acierto, y no con un 97% como pensábamos antes.
La distinción entre el conjunto de entrenamiento, el conjunto de validación y el conjunto de prueba es fundamentalmente importante para aplicar los métodos de aprendizaje automático en la práctica. Cualquier decisión tomada basada en la precisión del conjunto de prueba
“filtra” información del conjunto de prueba al modelo
. Por lo tanto, es fundamental mantener un conjunto de prueba separado, que solo se utiliza para la evaluación final.Es una buena práctica realizar todo el análisis exploratorio (EDA) y la selección del modelo utilizando la combinación de entrenamiento y validación, y reservar el conjunto de prueba para la evaluación final, incluso en el caso de la visualización exploratoria. En sentido estricto, evaluar más de un modelo en el conjunto de prueba y elegir el mejor de los dos resultará en una estimación demasiado optimista de la precisión del modelo.
12.2.3. Grid Search con validación cruzada#
Aunque el método de dividir los datos en un conjunto de entrenamiento, uno de validación y otro de prueba que acabamos de ver es factible y se utiliza con relativa frecuencia, es bastante sensible a la forma en que se dividen los datos. De la salida del fragmento de código anterior podemos ver que el
grid-search
selecciona'C': 10, 'gamma': 0.001
, como los mejores parámetros, mientras que la salida del código de la sección anterior selecciona'C': 100, 'gamma': 0.001
como los mejores parámetros.Para una mejor estimación del rendimiento de la generalización, en lugar de usar una única división en un conjunto de entrenamiento y otro de validación, podemos usar la validación cruzada para evaluar el rendimiento de cada combinación de parámetros. Este método puede codificarse como sigue:
import numpy as np
for gamma in [0.001, 0.01, 0.1, 1, 10, 100]:
for C in [0.001, 0.01, 0.1, 1, 10, 100]:
svm = SVC(gamma=gamma, C=C) # Entrena SVC para cada parámetro
scores = cross_val_score(svm, X_trainval, y_trainval, cv=5) # Calcula validación cruzada
score = np.mean(scores) # Calcula media de la validación cruzada para precisión
if score > best_score:
best_score = score
best_parameters = {'C': C, 'gamma': gamma}
Reconstruimos el modelo en el conjunto combinado de entrenamiento y validación
svm = SVC(**best_parameters)
svm.fit(X_trainval, y_trainval)
SVC(C=10, gamma=0.1)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SVC(C=10, gamma=0.1)
Para evaluar la precisión de
SVM
utilizando un ajuste particular deC
ygamma
con5-fold
validación cruzada, necesitamos entrenar 36 * 5 = 180 modelos. Como puede imaginarse el principal inconveniente del uso de la validación cruzada es el tiempo que lleva entrenar todos estos modelos. La siguiente visualización ilustra cómo se selecciona la mejor configuración de parámetros en el código anterior

Para cada ajuste de parámetros (sólo se muestra un subconjunto), se calculan cinco valores de precisión, uno para cada división en la validación cruzada. A continuación, se calcula la precisión media de la validación para cada parámetro. Se eligen los parámetros con la mayor precisión media de validación, marcados con un círculo.
Observación
Como hemos dicho antes, la validación cruzada es una forma de evaluar un determinado algoritmo en un conjunto de datos específico. Sin embargo, a menudo se utiliza junto con métodos de búsqueda de parámetros como Grid Search
. Por esta razón, normalmente se utiliza el término validación cruzada coloquialmente para referirse a un Grid Search
con validación cruzada.
El proceso general de división de los datos, la ejecución de
grid search
y la evaluación de los parámetros finales se ilustra en la siguiente figura

Fig. 12.1 Resumen del proceso de selección de parámetros y evaluación de modelos con GridSearchCV
.#
Debido a que
grid search
con validación cruzada es un método tan comúnmente utilizado para ajustar parámetros,scikit-learn
proporciona la claseGridSearchCV
, que lo implementa en la forma de un estimador. Para utilizar la claseGridSearchCV
, primero hay que especificar los parámetros sobre los que se quiere buscar utilizando un diccionario.A continuación,
GridSearchCV
realizará todos los ajustes necesarios del modelo. Las claves (keys) del diccionario son los nombres de los parámetros que queremos ajustar (tal y como se indican cuando se construye el modelo, en este caso,C
ygamma
), y los valores (values) son los ajustes de los parámetros que queremos probar. Probar los valores 0,001, 0,01, 0,1, 1, 10 y 100 paraC
ygamma
se traduce en lo siguiente diccionario
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
print("Parameter grid:\n{}".format(param_grid))
Parameter grid:
{'C': [0.001, 0.01, 0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
Ahora podemos instanciar la clase
GridSearchCV
con el modelo(SVC)
, el parámetro a buscar (param_grid
), y la estrategia de validación cruzada que queremos utilizar (digamos validación cruzada estratificada 5-fold):
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
grid_search = GridSearchCV(SVC(), param_grid, cv=5)
GridSearchCV
utilizará la validación cruzada en lugar de la división en un conjunto de entrenamiento y de prueba que utilizábamos antes. Sin embargo, todavía tenemos que dividir los datos en un conjunto de entrenamiento y otro de prueba, para evitar el sobreajuste de los parámetros
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)
El objeto
grid_search
que hemos creado se comporta como un clasificador; podemos llamar a los métodos estándarfit, predict
yscore
. Sin embargo, cuando llamamos a fit, se ejecutará una validación cruzada para cada combinación de parámetros que hayamos especificado enparam_grid
grid_search.fit(X_train, y_train)
GridSearchCV(cv=5, estimator=SVC(), param_grid={'C': [0.001, 0.01, 0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]})In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=5, estimator=SVC(), param_grid={'C': [0.001, 0.01, 0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]})
SVC(C=10, gamma=0.1)
SVC(C=10, gamma=0.1)
El objeto
GridSearchCV
no solo busca los mejores parámetros, sino que también automáticamente un nuevo modelo en todo el conjunto de datos de entrenamiento con los parámetros que han dado el mejor rendimiento en la validación cruzada. La claseGridSearchCV
proporciona una interfaz muy conveniente para acceder al modelo reentrenado utilizando los métodospredict
yscore
. Para evaluar lo bien que generalizan los mejores parámetros encontrados, podemos llamar ascore
en el conjunto de prueba
print("Test set score: {:.2f}".format(grid_search.score(X_test, y_test)))
Test set score: 0.97
Al elegir los parámetros mediante la validación cruzada, encontramos un modelo que alcanza el 97% de precisión en el conjunto de prueba. Lo importante aquí es que no utilizamos el conjunto de prueba para elegir los parámetros. Los parámetros encontrados se anotan en el atributo
best_params_
y la mejor precisión de la validación cruzada (la precisión media sobre las diferentes divisiones para esta configuración de parámetros) se almacena enbest_score_
print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))
Best parameters: {'C': 10, 'gamma': 0.1}
Best cross-validation score: 0.97
Observación
De nuevo, tenga cuidado de no confundir best_score_
con el rendimiento de generalización del modelo calculado por el método score en el conjunto de prueba. El uso del método score (o la evaluación de la salida del método de predicción) emplea un modelo entrenado en todo el conjunto de entrenamiento. El atributo best_score_
almacena la precisión media de la validación cruzada, con la validación cruzada realizada en el conjunto de entrenamiento.
A veces es útil tener acceso al modelo real que se encontró, por ejemplo, para ver los coeficientes o la importancia de las características. Puede acceder al modelo con los mejores parámetros entrenados en todo el conjunto de entrenamiento utilizando el atributo
best_estimator_
print("Best estimator:\n{}".format(grid_search.best_estimator_))
Best estimator:
SVC(C=10, gamma=0.1)
Como el propio
grid_search
tiene métodos de predicción y score, no es necesario utilizarbest_estimator_
para hacer predicciones o evaluar el modelo.
12.2.4. Análisis del resultado de la validación cruzada#
A menudo es útil visualizar los resultados de la validación cruzada, para entender cómo la generalización del modelo depende de los parámetros que estamos buscando. Como los
grid search
son bastante costosos desde el punto de vista computacional, a menudo es una buena idea empezar con grids de múltiples medidas, ya sean grandes o pequeños.A continuación, podemos inspeccionar los resultados del
grid search
validado, y posiblemente ampliar nuestra búsqueda. Los resultados de ungrid search
se pueden encontrar en el atributocv_results_
, que es un diccionario que almacena todos los aspectos de la búsqueda. Este contiene muchos detalles, como se puede ver en la siguiente salida, y es mejor verlo después de convertirlo en unDataFrame
depandas
.Mostramos solo algunas columnas, para que se puedan diferenciar en el jbook, pero en su máquina puede visualizarla todas usando la orden
results.head()
.GridSearchCV.cv_results_
incluye los resultados de tiempo para scoring y ajuste de parámetros en cada pliegue. Por ejemplomean_score_time
es la cantidad media de tiempo que se necesita para scoring en los datos de cada plieguecv
, para cada conjunto de parámetros que definió en elgrid-search
.
import pandas as pd
results = pd.DataFrame(grid_search.cv_results_)
results[['mean_fit_time', 'std_fit_time', 'mean_score_time',
'std_score_time', 'param_C', 'param_gamma']].head()
mean_fit_time | std_fit_time | mean_score_time | std_score_time | param_C | param_gamma | |
---|---|---|---|---|---|---|
0 | 0.000427 | 0.000093 | 0.000237 | 0.000026 | 0.001 | 0.001 |
1 | 0.000364 | 0.000006 | 0.000215 | 0.000005 | 0.001 | 0.010 |
2 | 0.000371 | 0.000014 | 0.000215 | 0.000009 | 0.001 | 0.100 |
3 | 0.000373 | 0.000007 | 0.000239 | 0.000018 | 0.001 | 1.000 |
4 | 0.000371 | 0.000013 | 0.000211 | 0.000002 | 0.001 | 10.000 |
Cada fila de resultados corresponde a un ajuste de parámetros concreto. Para cada ajuste, se registran los resultados de todas las divisiones de validación cruzada, así como la media y la desviación estándar de todas las divisiones. Como buscamos una red bidimensional de parámetros (
C y gamma
), esto se visualiza mejor como un mapa de calor. Primero extraemos las puntuaciones medias de la validación y luego las reformamos para que los ejes correspondan aC y gamma
.
scores = np.array(results.mean_test_score).reshape(6, 6)
import seaborn as sns
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 6))
sns.heatmap(scores, annot=True, fmt=".2f", cmap="viridis",
xticklabels=param_grid['gamma'], yticklabels=param_grid['C'])
plt.xlabel("gamma")
plt.ylabel("C")
plt.title("Grid Search Scores")
plt.show()

Cada punto del mapa de calor corresponde a una ejecución de validación cruzada, con un parámetro en particular. El color codifica la precisión de la validación cruzada, siendo los colores claros los relacionados con alta precisión y los colores oscuros con baja precisión. Se puede ver que SVC es muy sensible a la configuración de los parámetros. Para muchos de los ajustes de los parámetros, la precisión está en torno al 37%, lo que es bastante malo; para otros ajustes, la precisión está en torno al 96%.
De este gráfico se desprenden varias cosas. En primer lugar, los parámetros que ajustamos son muy importantes para obtener un buen rendimiento. Ambos parámetros (C y gamma) son muy importantes, ya que su ajuste puede cambiar la precisión del 37% al 96%. Además, los rangos que elegimos para los parámetros son rangos en los que vemos cambios significativos en el resultado. También es importante tener en cuenta que los rangos de los parámetros son lo suficientemente amplios: los valores óptimos de cada parámetro no están en los bordes del gráfico.
Veamos algunos gráficos en los que el resultado es menos ideal, porque los rangos de búsqueda no fueron elegidos correctamente
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
import mglearn
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
# Definir los grids de hiperparámetros
param_grid_linear = {'C': np.linspace(1, 2, 6), 'gamma': np.linspace(1, 2, 6)}
param_grid_one_log = {'C': np.linspace(1, 2, 6), 'gamma': np.logspace(-3, 2, 6)}
param_grid_range = {'C': np.logspace(-3, 2, 6), 'gamma': np.logspace(-7, -2, 6)}
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for param_grid, ax in zip([param_grid_linear, param_grid_one_log, param_grid_range], axes):
grid_search = GridSearchCV(SVC(), param_grid, cv=5)
grid_search.fit(X_train, y_train)
scores = grid_search.cv_results_['mean_test_score'].reshape(6, 6)
def format_tick(val):
"""Formatea el número con notación científica si tiene más de 4 cifras decimales"""
return f"{val:.3f}" if abs(val) >= 0.001 else f"{val:.1e}"
xticklabels = [format_tick(val) for val in param_grid['gamma']]
yticklabels = [format_tick(val) for val in param_grid['C']]
sns.heatmap(scores, annot=True, fmt=".2f", cmap="viridis", ax=ax)
ax.set_xticklabels(xticklabels)
ax.set_yticklabels(yticklabels)
ax.set_xlabel("gamma")
ax.set_ylabel("C")
plt.tight_layout()
plt.show()

El primer panel no muestra ningún cambio, con scores aproximadamente constantes en toda la red de parámetros. En este caso, esto se debe a una escala y un rango inadecuado para los parámetros
C
ygamma
. Sin embargo, si no se aprecia ningún cambio en la precisión a lo largo de los diferentes ajustes de los parámetros, también puede ser que un parámetro no sea importante en absoluto.Suele ser bueno probar primero valores muy extremos, para ver si hay algún cambio en la precisión como resultado de cambiar un parámetro. El segundo panel muestra un patrón de rayas verticales. Esto indica que solo el ajuste del parámetro
gamma
hace alguna diferencia. Esto podría significar que el parámetro gamma busca valores interesantes, pero el parámetro C no lo hace, o podría significar que el parámetro C no es importante.El tercer panel muestra cambios tanto en C como en gamma. Sin embargo, podemos ver que en toda la parte inferior izquierda del gráfico, no ocurre nada interesante. Probablemente, podemos excluir los valores muy pequeños de las futuras búsquedas en la red. La configuración óptima de los parámetros está en la parte superior derecha. Como el óptimo está en el borde del gráfico, podemos esperar que puede haber valores aún mejores más allá de este límite, y podríamos cambiar nuestro rango de búsqueda para incluir más parámetros en esta región.
Ajustar la red de parámetros basándose en las puntuaciones de validación cruzada es perfectamente correcto, y una buena manera de explorar la importancia de los diferentes parámetros. Sin embargo, no debería probar diferentes rangos de parámetros en el conjunto de pruebas final, ya que, como hemos dicho antes, la evaluación del conjunto de pruebas solo debería realizarse una vez que sepamos exactamente qué modelo queremos utilizar.
12.2.5. Búsqueda sobre espacios que no son una red#
En algunos casos, probar todas las combinaciones posibles de todos los parámetros, como suele hacer
GridSearchCV
, no es una buena idea. Por ejemplo,SVC
tiene un parámetro dekernel
, y dependiendo delkernel
que se elija, otros parámetros serán relevantes. Sikernel='linear'
, el modelo es lineal, y solo se utiliza el parámetroC
. Sikernel='rbf'
, se utilizan los parámetrosC y gamma
(pero no otros parámetros como el grado).En este caso, la búsqueda de todas las combinaciones posibles de
C, gamma y kernel
no tendría sentido: sikernel='linear', gamma
no se utiliza, y probar diferentes valores degamma
sería una pérdida de tiempo. Recuerde quekernel='rbf'
es el kernel de función de base radial (RBF) con mapeo de características \(\phi(\boldsymbol{x})=\exp(\|\boldsymbol{x}-x_{i}\|/2\sigma^2),~\gamma=1/\sigma^2\). Para ver todas las opciones de kernel gaussiano (ver Kernels for Gaussian Processes).Para tratar este tipo de parámetros “condicionales”,
GridSearchCV
permite queparam_grid
sea una lista de diccionarios. Cada diccionario de la lista se expande en una red(grid)
independiente. Una posible búsqueda en red que incluya el núcleo (kernel
) y los parámetros podría ser así:
param_grid = [{'kernel': ['rbf'],
'C': [0.001, 0.01, 0.1, 1, 10, 100],
'gamma': [0.001, 0.01, 0.1, 1, 10, 100]},
{'kernel': ['linear'],
'C': [0.001, 0.01, 0.1, 1, 10, 100]}]
print("List of grids:\n{}".format(param_grid))
List of grids:
[{'kernel': ['rbf'], 'C': [0.001, 0.01, 0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}, {'kernel': ['linear'], 'C': [0.001, 0.01, 0.1, 1, 10, 100]}]
En la primera red, el parámetro del
kernel
se establece siempre en'rbf'
(no que la entrada dekernel
es una lista de longitud uno), y se varían los parámetrosC
ygamma
. En la segunda red, el parámetro kernel siempre se establece como lineal, y sólo se varía C. Ahora apliquemos esta búsqueda de parámetros más compleja
grid_search = GridSearchCV(SVC(), param_grid, cv=5)
grid_search.fit(X_train, y_train)
print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))
Best parameters: {'C': 10, 'gamma': 0.1, 'kernel': 'rbf'}
Best cross-validation score: 0.97
Observemos de nuevo el
cv_results_
. Como era de esperar, si el núcleo es"lineal"
, sólo varíaC
. Nótese que el pandas tiene un total de 16 columnas.
results = pd.DataFrame(grid_search.cv_results_)
display(results.T.iloc[: , :6])
results.T.shape
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
mean_fit_time | 0.000445 | 0.000385 | 0.000366 | 0.000394 | 0.000364 | 0.000442 |
std_fit_time | 0.000102 | 0.000017 | 0.000007 | 0.000052 | 0.000006 | 0.000021 |
mean_score_time | 0.000251 | 0.000218 | 0.000254 | 0.000237 | 0.000217 | 0.000228 |
std_score_time | 0.000037 | 0.000004 | 0.000063 | 0.000026 | 0.000009 | 0.000005 |
param_C | 0.001 | 0.001 | 0.001 | 0.001 | 0.001 | 0.001 |
param_gamma | 0.001 | 0.01 | 0.1 | 1.0 | 10.0 | 100.0 |
param_kernel | rbf | rbf | rbf | rbf | rbf | rbf |
params | {'C': 0.001, 'gamma': 0.001, 'kernel': 'rbf'} | {'C': 0.001, 'gamma': 0.01, 'kernel': 'rbf'} | {'C': 0.001, 'gamma': 0.1, 'kernel': 'rbf'} | {'C': 0.001, 'gamma': 1, 'kernel': 'rbf'} | {'C': 0.001, 'gamma': 10, 'kernel': 'rbf'} | {'C': 0.001, 'gamma': 100, 'kernel': 'rbf'} |
split0_test_score | 0.347826 | 0.347826 | 0.347826 | 0.347826 | 0.347826 | 0.347826 |
split1_test_score | 0.347826 | 0.347826 | 0.347826 | 0.347826 | 0.347826 | 0.347826 |
split2_test_score | 0.363636 | 0.363636 | 0.363636 | 0.363636 | 0.363636 | 0.363636 |
split3_test_score | 0.363636 | 0.363636 | 0.363636 | 0.363636 | 0.363636 | 0.363636 |
split4_test_score | 0.409091 | 0.409091 | 0.409091 | 0.409091 | 0.409091 | 0.409091 |
mean_test_score | 0.366403 | 0.366403 | 0.366403 | 0.366403 | 0.366403 | 0.366403 |
std_test_score | 0.022485 | 0.022485 | 0.022485 | 0.022485 | 0.022485 | 0.022485 |
rank_test_score | 27 | 27 | 27 | 27 | 27 | 27 |
(16, 42)
Uso de diferentes estrategias de validación cruzada con la búsqueda en red
Al igual que cross_val_score, GridSearchCV
utiliza por defecto la validación cruzada estratificada k-fold
para la clasificación, y la validación cruzada k-fold para la regresión. Sin embargo, también puede pasar cualquier divisor de validación cruzada, como se describe en “Más control sobre la validación cruzada”, como parámetro cv
en GridSearchCV
. En particular, para obtener una única división en un conjunto de entrenamiento y otro de validación, puede utilizar ShuffleSplit
o StratifiedShuffleSplit
con n_iter=1
(número de iteraciones de reordenamiento y división). Esto puede ser útil para conjuntos de datos muy grandes o para modelos muy lentos.
12.2.6. Validación cruzada anidada#
En los ejemplos anteriores, pasamos de utilizar una única división de los datos en conjuntos de entrenamiento validación y prueba, a dividir los datos en conjuntos de entrenamiento y prueba, y luego de validación cruzada en el conjunto de entrenamiento. Pero al utilizar
GridSearchCV
como se ha descrito anteriormente, seguimos teniendo una única división de los datos en conjuntos de entrenamiento y de prueba, lo que puede hacer que nuestros resultados sean inestables y nos haga depender demasiado de esta única división de los datos. Podemos ir un paso más allá, y en lugar de dividir los datos originales en conjuntos de entrenamiento y prueba una vez, utilizar múltiples divisiones de validación cruzada. Esto dará lugar a lo que se denomina validación cruzada anidada.En la validación cruzada anidada, hay un bucle externo sobre las divisiones de los datos en conjuntos de entrenamiento y de prueba. Para cada uno de ellos, se ejecuta una búsqueda en red (que puede dar lugar a diferentes parámetros óptimos para cada división en el bucle externo). A continuación, para cada división exterior, se informa del scoring del conjunto de prueba utilizando los mejores parámetros. El resultado de este procedimiento es una lista de puntuaciones, no un modelo ni un conjunto de parámetros.
Las puntuaciones nos dicen lo bien que generaliza un modelo, dados los mejores parámetros encontrados por la red. Como no proporciona un modelo que pueda utilizarse con nuevos datos, la validación cruzada anidada no suele utilizarse cuando se busca un modelo predictivo para aplicarlo a datos futuros. Sin embargo, puede ser útil para evaluar lo bien que funciona un modelo determinado en un conjunto de datos concreto. La implementación de la validación cruzada anidada en
scikit-learn
es sencilla. Llamamos across_val_score
con una instancia deGridSearchCV
como modelo
scores = cross_val_score(GridSearchCV(SVC(), param_grid, cv=5), iris.data, iris.target, cv=5)
print("Cross-validation scores: ", scores)
print("Mean cross-validation score: ", scores.mean())
Cross-validation scores: [0.96666667 1. 0.9 0.96666667 1. ]
Mean cross-validation score: 0.9666666666666668
El resultado de nuestra validación cruzada anidada puede resumirse en que “SVC puede alcanzar un 96.67% de precisión media de validación cruzada en el conjunto de datos del iris”, nada más y nada menos. Aquí, utilizamos la validación cruzada estratificada de cinco pliegues (5-fold) tanto en el bucle interno como en el externo. Como nuestra red de parámetros contiene 36 combinaciones de parámetros, el resultado es la cantidad de 36 * 5 * 5 = 900 modelos construidos, lo que hace que la validación cruzada anidada sea un procedimiento muy costoso.
Aquí, utilizamos el mismo divisor de validación cruzada en el bucle interno y en el externo; sin embargo, esto no es necesario y se puede utilizar cualquier combinación de estrategias de validación cruzada en el bucle interno y en el externo. Puede ser un poco complicado entender lo que está sucediendo en la línea simple dada anteriormente, y puede ser útil visualizarlo como bucles for, como se hace en la siguiente implementación simplificada
def nested_cv(X, y, inner_cv, outer_cv, Classifier, parameter_grid):
outer_scores = []
for training_samples, test_samples in outer_cv.split(X, y):
best_parms = {}
best_score = -np.inf
for parameters in parameter_grid:
cv_scores = []
for inner_train, inner_test in inner_cv.split(X[training_samples], y[training_samples]):
clf = Classifier(**parameters)
clf.fit(X[inner_train], y[inner_train])
score = clf.score(X[inner_test], y[inner_test])
cv_scores.append(score)
mean_score = np.mean(cv_scores)
if mean_score > best_score:
best_score = mean_score
best_params = parameters
clf = Classifier(**best_params)
clf.fit(X[training_samples], y[training_samples])
outer_scores.append(clf.score(X[test_samples], y[test_samples]))
return np.array(outer_scores)
from sklearn.model_selection import ParameterGrid, StratifiedKFold
scores = nested_cv(iris.data, iris.target, StratifiedKFold(5),
StratifiedKFold(5), SVC, ParameterGrid(param_grid))
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96666667 1. 0.96666667 0.96666667 1. ]
12.2.7. Paralelización de la validación cruzada y la búsqueda en red#
Si bien la ejecución de un grid search sobre muchos parámetros y en grandes conjuntos de datos puede ser computacionalmente un reto, también es un paralelismo tedioso. Esto significa que la construcción de un modelo utilizando un ajuste de parámetros particular en una división de validación cruzada particular puede hacerse con total independencia de los demás ajustes de parámetros y modelos. Esto hace que la búsqueda en red y la validación cruzada sean candidatas ideales para la paralelización en múltiples núcleos de CPU o en un clúster. Se puede hacer uso de múltiples núcleos en
GridSearchCV
ycross_validation
estableciendo el parámetron_jobs
con el número de núcleos de CPU que desee utilizar. Puede establecer n_jobs=-1 para utilizar todos los núcleos disponibles.Debe tener en cuenta que
scikit-learn
no permite el anidamiento de operaciones paralelas. Por lo tanto, si está utilizando la opciónn_jobs
en su modelo (por ejemplo,random forest
), no puede utilizarla enGridSearchCV
para buscar sobre este modelo. Si su conjunto de datos y su modelo son muy grandes, puede ser que el uso de muchos núcleos consuma demasiada memoria, y deberías controlar el uso de la memoria cuando construyas modelos grandes en paralelo. También es posible paralelizar la búsqueda en red y la validación cruzada en varias máquinas en un clúster. Sin embargo, es posible utilizar el marco paralelo deIPython
para las búsquedas paralelas de la red, también puede escribir el bucle for sobre los parámetros como lo hicimos en “Búsqueda simple en la red”.Para los usuarios de Spark, también existe el paquete spark-sklearn, recientemente desarrollado, que permite ejecutar una búsqueda grid sobre un cluster Spark ya establecido
.
12.3. Métricas de evaluación y scoring#
Hasta ahora, hemos evaluado el rendimiento de la clasificación utilizando la precisión (accuracy) (la fracción de muestras correctamente clasificadas) y el rendimiento de la regresión mediante el \(R^2\). Sin embargo, éstas son sólo dos de las muchas formas posibles de resumir la eficacia de un modelo supervisado en un conjunto de datos determinado. En la práctica, estas métricas de evaluación pueden no ser apropiadas para su aplicación, y es importante elegir la métrica correcta cuando se selecciona entre modelos y se ajustan los parámetros.
12.3.1. Tenga en cuenta el objetivo final#
Al elegir una métrica en aprendizaje automático, debe alinearse con el objetivo final de la aplicación, conocido como métrica de negocio, ya que las predicciones influyen en la toma de decisiones y el impacto empresarial. Por ejemplo, podría buscar reducir accidentes, minimizar hospitalizaciones o aumentar ingresos.
La selección del modelo y sus parámetros debe maximizar el impacto positivo en la métrica de negocio, pero evaluarlo en producción puede ser riesgoso. Por ello, se emplean métricas sustitutas más fáciles de calcular, como la
precision
en la clasificación de imágenes. Sin embargo, estas métricas deben acercarse lo más posible al objetivo real.El impacto empresarial puede no reducirse a un solo número (por ejemplo, más clientes pero menor gasto por cliente), pero debe reflejar la influencia del modelo. En esta sección, se abordarán métricas para clasificación binaria, multiclase y regresión.
12.3.2. Métricas para la clasificación binaria#
La clasificación binaria es probablemente la aplicación más común y conceptualmente simple de aprendizaje automático en la práctica. Sin embargo, todavía hay una serie de advertencias en evaluar incluso esta sencilla tarea. Antes de entrar en las métricas alternativas, echemos un vistazo a la forma en que se mide la precisión, la cual puede ser engañosa. Recordemos que para la clasificación binaria, a menudo hablamos de una clase positiva y una clase negativa, entendiendo que la clase positiva es la que estamos buscando.
12.3.3. Tipos de errores#
precisión (
accuracy
) no siempre es una buena medida del rendimiento, ya que los errores cometidos no reflejan toda la información relevante. Por ejemplo, en la detección temprana de cáncer, una prueba positiva implica más exámenes, mientras que una negativa considera al paciente sano. Dado que ningún modelo es perfecto, es crucial evaluar el impacto real de sus errores.Un falso positivo ocurre cuando un paciente sano es clasificado como enfermo, causando pruebas innecesarias y posibles preocupaciones. En cambio, un falso negativo, donde un paciente enfermo es clasificado como sano, puede ser fatal al retrasar el tratamiento. Estos errores se conocen como tipo I y tipo II, pero los términos falso positivo y falso negativo son más claros. En este contexto, es crítico minimizar los falsos negativos (
recall
), aunque los falsos positivos sean una molestia.Las consecuencias de estos errores varían según la aplicación. En el ámbito comercial, se pueden asignar costos monetarios a cada tipo de error, permitiendo evaluar modelos más allá de la precisión, optimizando la toma de decisiones.
Un ejemplo donde el equilibrio es clave pede ser la detección de spam en correos electrónicos.
Reducir falsos negativos (correos spam que se clasifican como no spam): Si un correo malicioso pasa desapercibido, el usuario podría ser víctima de estafas o phishing.
Reducir falsos positivos (correos legítimos que se clasifican como spam): Correos importantes, como facturas o mensajes de trabajo, podrían perderse en la carpeta de spam.
12.3.4. Conjuntos de datos desequilibrados#
Los errores son cruciales cuando una clase es mucho más frecuente que otra, como en la predicción de clicks, donde la mayoría de los elementos mostrados no reciben interacción. En estos casos, los datos están desequilibrados, con una gran mayoría perteneciente a la clase “no click”.
Dado que los eventos de interés suelen ser raros, un modelo con 99% para
accuracy
podría simplemente predecir siempre “no click” sin aportar valor real. Por ello, la precisión no distingue entre un modelo trivial y uno efectivo. Para ilustrarlo, se creará un conjunto de datos desequilibrado 9:1 con el conjunto digits, clasificando el dígito 9 frente a los demás.
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
print("Data shape: ", digits.data.shape, "; Target shape", digits.target.shape)
Data shape: (1797, 64) ; Target shape (1797,)
np.unique(digits.target)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.unique(digits.data)
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.,
13., 14., 15., 16.])
y = digits.target == 9 # Boolean
X_train, X_test, y_train, y_test = train_test_split(digits.data, y, random_state=0)
Podemos utilizar
DummyClassifier
para predecir siempre la clase mayoritaria (aquí “not nine”) para ver lo poco informativa que puede ser el (accuracy)
from sklearn.dummy import DummyClassifier
import numpy as np
dummy_majority = DummyClassifier(strategy='most_frequent').fit(X_train, y_train)
pred_most_frequent = dummy_majority.predict(X_test)
print("Unique predicted labels: {}".format(np.unique(pred_most_frequent)))
print("Test score: {:.2f}".format(dummy_majority.score(X_test, y_test)))
Unique predicted labels: [False]
Test score: 0.90
Obtuvimos una precisión cercana al 90% sin aprender nada. Esto puede parecer sorprendente, pero pero piénselo un momento. Imagine que alguien le dice que su modelo tiene un 90% de precisión. Podrías pensar que han hecho un buen trabajo. Pero dependiendo del problema, ¡eso podría ser posible con sólo predecir una clase! Comparemos esto con el uso de un clasificador real.
from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(max_depth=2).fit(X_train, y_train)
pred_tree = tree.predict(X_test)
print("Test score: {:.2f}".format(tree.score(X_test, y_test)))
Test score: 0.92
Según la precisión, el DecisionTreeClassifier es sólo ligeramente mejor que el predictor constante. Esto podría indicar que algo está mal en la forma en que utilizamos DecisionTreeClassifier, o bien que accuracy no es una buena medida en este caso. Para comparar, evaluemos otro clasificador,
LogisticRegression
from sklearn.linear_model import LogisticRegression
logreg = LogisticRegression(C=0.1).fit(X_train, y_train)
pred_logreg = logreg.predict(X_test)
print("logreg score: {:.2f}".format(logreg.score(X_test, y_test)))
logreg score: 0.98
El clasificador
dummy
con resultados aleatorios es el peor segúnaccuracy
, mientras queLogisticRegression
muestra buenos resultados. Sin embargo,accuracy
es inadecuada en entornos desequilibrados, dificultando la evaluación real del modelo. Exploraremos métricas alternativas que permitan seleccionar modelos de manera más efectiva, priorizando aquellas que indiquen cuánto mejor es un modelo en lugar de favorecer predicciones frecuentes o aleatorias. Una métrica adecuada debería descartar predicciones sin sentido.
12.3.5. Matrices de confusión#
Una de las formas más completas de representar el resultado de la evaluación de la clasificación binaria es el uso de matrices de confusión. Inspeccionemos las predicciones de
LogisticRegres
de la sección anterior utilizando la funciónconfusion_matrix
. Ya hemos almacenado las predicciones del conjunto de prueba enpred_logreg
from sklearn.metrics import confusion_matrix
confusion = confusion_matrix(y_test, pred_logreg)
print("Confusion matrix:\n{}".format(confusion))
Confusion matrix:
[[402 1]
[ 6 41]]
La salida de
confusion_matrix
es una matriz de dos por dos, donde las filas corresponden a las clases verdaderas y las columnas corresponden a las clases predichas. Cada entrada cuenta la frecuencia con la que una muestra que pertenece a la clase correspondiente a la fila (aquí “not nine” y “nine”) fue clasificada como la clase correspondiente a la columna.
mglearn.plots.plot_confusion_matrix_illustration()

La diagonal principal de la matriz de confusión representa las clasificaciones correctas, mientras que las demás entradas indican errores. Si “nine” es la clase positiva, se pueden definir los términos falso positivo (FP) y falso negativo (FN). Además, las muestras correctamente clasificadas como positivas son verdaderos positivos (TP) y las negativas, verdaderos negativos (TN). Estos términos permiten interpretar la matriz de confusión.
mglearn.plots.plot_binary_confusion_matrix()

Ahora utilicemos la matriz de confusión para comparar los modelos que hemos ajustado antes (los dos modelos dummy, árbol de decisión y regresión logística)
print("\nDummy model:")
print(confusion_matrix(y_test, pred_most_frequent))
Dummy model:
[[403 0]
[ 47 0]]
print("\nDecision tree:")
print(confusion_matrix(y_test, pred_tree))
Decision tree:
[[390 13]
[ 24 23]]
print("\nLogistic Regression")
print(confusion_matrix(y_test, pred_logreg))
Logistic Regression
[[402 1]
[ 6 41]]
La matriz de confusión revela que
pred_most_frequent
es ineficaz, ya que siempre predice la misma clase y tiene cero verdaderos y falsos positivos. Aunque el árbol de decisión y la regresión logística presentan predicciones más razonables, la regresión logística supera al árbol en todos los aspectos, con más verdaderos positivos y negativos y menos errores. Sin embargo, analizar manualmente la matriz es un proceso cualitativo y laborioso. A continuación, exploraremos métodos para resumir su información de manera más eficiente.
Accuracy
Ya vimos una forma de resumir el resultado en la matriz de confusión, calculando su accuracy
, que puede expresarse como
En otras palabras, el accuracy es el número de predicciones correctas (TP y TN) dividido por el número de todas las muestras (todas las entradas de la matriz de confusión sumadas).
Precision, recall y f-score. Hay otras formas de resumir la matriz de confusión, siendo las más comunes: precision, recall y f-score.
Precision
Precision mide cuántas de las muestras predichas como positivas son realmente positivas, es decir, precision
intenta responder a la siguiente pregunta: ¿qué proporción de identificaciones positivas fue correcta?
Precision se utiliza como métrica de rendimiento cuando el objetivo es limitar el número de falsos positivos.
Un modelo que predice la eficacia de un medicamento en ensayos clínicos debe minimizar falsos positivos, ya que estos experimentos son costosos y solo deben realizarse con alta certeza de éxito. Por ello, es crucial que el modelo tenga alta precision (o valor predictivo positivo, VPP). Nótese que cuando precision → 1, FP → 0, y cuando recall → 1, FN → 0.
Recall
El recall
mide cuántas de las muestras de la clase positiva son realmente predichas positivas, es decir, recall
intenta responder a la siguiente pregunta: ¿qué proporción de positivos reales se identificó en forma correcta?
Recall se utiliza como métrica de rendimiento cuando el objetivo es limitar el número de falsos negativos.
Existe un equilibrio entre
recall
yprecision
. Si se predicen todas las muestras como positivas, se eliminarecall
, pero con muchos falsos positivos, reduciendoprecision
. En cambio, si solo se predice como positiva la muestra más segura,precision
será perfecta (si es realmente positiva), pero elrecall
será muy bajo.
Observación
Precision y recall son sólo dos de las muchas medidas de clasificación derivadas de TP, FP, TN y FN. Puede encontrar un gran resumen de todas las medidas en Sensitivity_and_specificity. En la comunidad del aprendizaje automático, precision
y recall
son las medidas más utilizadas para la clasificación binaria, aunque pueden utilizar otras métricas relacionadas.
\(f_{1}\)-score
Por lo tanto, aunque precision
y recall
sean medidas muy importantes, si sólo se tiene en cuenta una de ellas no se obtiene una visión completa. Una forma de resumirlas es usando el f-score o f-measure, que es la media armónica entre precision y recall:
Esta variante concreta también se conoce como \(f_{1}\)-score.
El F₁-score es mejor que la exactitud en conjuntos de datos desbalanceados, ya que considera precisión y recuperación. Lo aplicaremos al conjunto “nine vs. rest”, donde “nine” es la clase positiva y minoritaria (True
), mientras que el resto es negativa (False
).
from sklearn.metrics import f1_score
print("f1 score dummy: {:.2f}".format(f1_score(y_test, pred_most_frequent)))
f1 score dummy: 0.00
print("f1 score tree: {:.2f}".format(f1_score(y_test, pred_tree)))
f1 score tree: 0.55
print("f1 score logistic regression: {:.2f}".format(f1_score(y_test, pred_logreg)))
f1 score logistic regression: 0.92
El \(f_{1}\)-score distingue mejor las predicciones dummy de las del árbol que el
accuracy
, alineándose mejor con nuestra intuición sobre un buen modelo. Sin embargo, es menos interpretable. Para un resumen más completo deprecision
,recall
y \(f_{1}\)-score,classification_report
los calcula y muestra en un formato claro. Sus últimas filas incluyenmacro avg
, que pondera cada clase por igual, yweighted avg
, que ajusta los pesos según la proporción de datos. Más detalles en sklearn.metrics.classification_report.
from sklearn.metrics import classification_report
print(classification_report(y_test, pred_most_frequent, target_names=["not nine", "nine"]))
precision recall f1-score support
not nine 0.90 1.00 0.94 403
nine 0.00 0.00 0.00 47
accuracy 0.90 450
macro avg 0.45 0.50 0.47 450
weighted avg 0.80 0.90 0.85 450
La función
classification_report
produce una línea por clase (aquí,True
yFalse
) e informaprecision, recall
y \(f\)-score
. Si consideramos la clase positiva por “not nine”, podemos ver en la salida declassification_report
que obtenemos un \(f\)-score de 0.94 con el modelodummy
. Además, para la clase “not nine” tenemos unrecall
de 1, ya que clasificamos todas las muestras como “not nine”.La última columna junto al \(f\)-score proporciona el soporte de cada clase, lo que significa simplemente el número de muestras en esta clase según la verdad básica. La última fila del informe de clasificación muestra una media ponderada (por el número de muestras en la clase) de los números de cada clase. Aquí hay dos informes más, uno para el clasificador arbol de decisión y otro para la regresión logística
print(classification_report(y_test, pred_tree, target_names=["not nine", "nine"]))
precision recall f1-score support
not nine 0.94 0.97 0.95 403
nine 0.64 0.49 0.55 47
accuracy 0.92 450
macro avg 0.79 0.73 0.75 450
weighted avg 0.91 0.92 0.91 450
print(classification_report(y_test, pred_logreg, target_names=["not nine", "nine"]))
precision recall f1-score support
not nine 0.99 1.00 0.99 403
nine 0.98 0.87 0.92 47
accuracy 0.98 450
macro avg 0.98 0.93 0.96 450
weighted avg 0.98 0.98 0.98 450
Como puede observar al mirar los informes, las diferencias entre los modelos
dummy
y un modelo muy bueno ya no son tan claras. La elección de la clase que se declarada como clase positiva, tiene un gran impacto en las métricas. Mientras que el \(f\)-score de la clasificacióndummy
es de 0.13 (frente a 0.89 para la regresión logística) en la clase “nine” es de 0.90 frente a 0.99, lo que parece un resultado razonable. Sin embargo, si se observan todas las cifras juntas, se obtiene una imagen bastante precisa, y podemos ver claramente la superioridad de la regresión logística.
12.3.6. Teniendo en cuenta la incertidumbre#
La matriz de confusión y el informe de clasificación analizan detalladamente un conjunto de predicciones, pero las predicciones en sí contienen información clave del modelo. La mayoría de los clasificadores incluyen
decision_function
opredict_proba
para medir la certeza de las predicciones, utilizando umbrales fijos: 0 endecision_function
y 0.5 enpredict_proba
para clasificación binaria.Un ejemplo de clasificación binaria desequilibrada presenta 400 puntos negativos y 50 positivos. Se entrena un kernel SVM, y un mapa de calor ilustra la función de decisión. Un círculo negro en la gráfica central superior marca el umbral donde la función es cero: puntos dentro del círculo son positivos, los demás negativos.
from mglearn.datasets import make_blobs
X, y = make_blobs(n_samples=400, n_features=50, centers=2, cluster_std=[7.0, 2], random_state=22)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
svc = SVC(gamma=.05).fit(X_train, y_train)
mglearn.plots.plot_decision_threshold()

Podemos utilizar la función
classification_report
para evaluarprecision
yrecall
de ambas clases
print(classification_report(y_test, svc.predict(X_test)))
precision recall f1-score support
0 0.49 1.00 0.66 49
1 0.00 0.00 0.00 51
accuracy 0.49 100
macro avg 0.24 0.50 0.33 100
weighted avg 0.24 0.49 0.32 100
print(confusion_matrix(y_test, svc.predict(X_test)))
[[49 0]
[51 0]]
Se obtienen las puntuaciones de decisión para medir la certeza del modelo, luego se calcula un umbral óptimo como la media de estas puntuaciones. A partir de este umbral, se generan predicciones ajustadas, considerando como positivos los valores superiores.
La función
decision_function(X_test)
de un clasificador SVM (SVC
) devuelve un arreglo de valores numéricos que representan la distancia de cada muestra con respecto al hiperplano de decisión.
import numpy as np
decision_scores = svc.decision_function(X_test)
optimal_threshold = np.median(decision_scores)
print("Umbral óptimo:", optimal_threshold)
Umbral óptimo: -0.013244755286005602
y_pred_adjusted = decision_scores > optimal_threshold
print(classification_report(y_test, y_pred_adjusted))
precision recall f1-score support
0 0.98 1.00 0.99 49
1 1.00 0.98 0.99 51
accuracy 0.99 100
macro avg 0.99 0.99 0.99 100
weighted avg 0.99 0.99 0.99 100
print(confusion_matrix(y_test, y_pred_adjusted))
[[49 0]
[ 1 50]]
Como se esperaba, recall y precision para la clase 1 subió. Ahora estamos clasificando una región más grande del espacio como clase 1, como se ilustra en el panel superior derecho de la anterior figura. Si valora más
precision
querecall
, o al revés, o sus datos están muy desequilibrados, cambiar el umbral de decisión es la forma más fácil de obtener mejores resultados. Como la función de decisión puede tener rangos arbitrarios, es difícil proporcionar una regla general sobre cómo elegir un umbral.
Observación
Si establece un umbral, debe tener cuidado de no hacerlo utilizando el conjunto de prueba. Como con cualquier otro parámetro, establecer un umbral de decisión en el conjunto de prueba es probable que produzca resultados demasiado optimistas. Utilice un conjunto de validación o aplique validación cruzada.
La media geométrica o G-mean es una métrica de clasificación desequilibrada que, si se optimiza, buscará un equilibrio entre la precision y recall.
Un enfoque consistiría en probar el modelo con cada umbral devuelto por la llamada
precision_recall_curve()
y seleccionar el umbral con el mayor valor G-mean. Otras técnicas de oversampling, tales comoSMOTE
también pueden ser adecuadas, para datos de entrenamiento desbalanceados.
El umbral en modelos con
predict_proba
es más fácil de ajustar, ya que su salida va de 0 a 1. Por defecto, un umbral de 0.5 clasifica como positiva una instancia si la probabilidad supera el 50%. Aumentarlo exige mayor certeza para predecir positivo y menor para negativo.Trabajar con probabilidades es intuitivo, pero no todos los modelos reflejan bien la incertidumbre (ej., un
DecisionTree
profundo siempre está 100% seguro, aunque se equivoque). Esto se relaciona con la calibración: un modelo calibrado mide correctamente su incertidumbre. Para más detalles, consulte “Predicting Good Probabilities with Supervised Learning” de Niculescu-Mizil y Caruana.
12.4. Curvas precision-recall y ROC#
Ajustar el umbral de clasificación permite equilibrar
precision
yrecall
. Por ejemplo, para unrecall
del 90%, el umbral debe adaptarse según los objetivos empresariales. Sin embargo, un umbral extremo, como clasificar todo como positivo, garantiza unrecall
del 100% pero hace inútil el modelo.Fijar un punto operativo, como un
recall
del 90%, ayuda a garantizar el rendimiento en entornos empresariales. Al desarrollar un modelo, es clave explorar distintos umbrales y compromisosprecision-recall
para comprender mejor el problema.La herramienta clave para esto es la curva precision-recall, calculable con
precision_recall_curve()
desklearn.metrics
, que usa etiquetas reales e incertidumbres dedecision_function
opredict_proba
.
from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test, svc.decision_function(X_test))
La función
precision_recall_curve
devuelve listas de precisión y recall para todos los umbrales posibles, permitiendo trazar la curva correspondiente. Más puntos generan una curva más suave.make_blobs
crea datos gaussianos isotrópicos para agrupación.
X, y = make_blobs(n_samples=4000, n_features=500, centers=2, cluster_std=[7.0, 2], random_state=22)
También usaremos el conjunto de cáncer de mama con
load_breast_cancer
.
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import precision_recall_curve
import numpy as np
import matplotlib.pyplot as plt
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
svc = SVC(gamma=.05).fit(X_train, y_train)
precision, recall, thresholds = precision_recall_curve(y_test, svc.decision_function(X_test))
closest_eq_idx = np.argmin(np.abs(precision - recall))
best_threshold = thresholds[closest_eq_idx]
best_precision = precision[closest_eq_idx]
best_recall = recall[closest_eq_idx]
best_threshold, best_precision, best_recall
(0.39103354055345696, 0.9529411764705882, 0.9)
close_zero = np.argmin(np.abs(thresholds))
plt.figure(figsize=(8, 6))
plt.plot(precision, recall, label="Precision-Recall Curve")
plt.plot(precision[closest_eq_idx], recall[closest_eq_idx], 'o', markersize=10,
label=f"Best Threshold ({best_threshold:.2f})", fillstyle="none", c='r', mew=2)
plt.plot(precision[close_zero], recall[close_zero], 'o', markersize=10,
label="Threshold ~ 0", fillstyle="none", c='k', mew=2)
plt.xlabel("Precision")
plt.ylabel("Recall")
plt.legend()
plt.title("Precision-Recall Curve with Best Threshold and Threshold ~ 0")
plt.show()

Cada punto de la curva representa un umbral de
decision_function
. Se puede lograr un recall de 0.99 con una precisión de 0.63. El círculo negro indica el umbral 0, que es el predeterminado dedecision_function
, mostrando la compensación usada enpredict
. Cuanto más cerca de la esquina superior derecha esté la curva, mejor es el clasificador, pues indica altoprecision
yrecall
simultáneamente.La curva inicia en la esquina superior izquierda con un umbral bajo, clasificando todo como positivo. A medida que el umbral aumenta, la precisión mejora, pero el
recall
disminuye. Si el umbral es muy alto, solo los verdaderos positivos son clasificados correctamente, lo que maximizaprecision
pero reducerecall
. Paraprecision
mayor a 0.5, cada mejora en precisión cuesta una gran pérdida enrecall
.Diferentes clasificadores rinden mejor en distintas partes de la curva. Comparando un
SVM
con unRandomForestClassifier
, este último usapredict_proba
en lugar dedecision_function
. La funciónprecision_recall_curve
requiere una medida de certeza, por lo que se usarf.predict_proba(X_test)[:, 1]
. El umbral predeterminado depredict_proba
es 0.5 y está marcado en la curva.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=42)
svc = SVC(gamma=0.05, probability=True).fit(X_train, y_train)
rf = RandomForestClassifier(n_estimators=100, random_state=0, max_features=2).fit(X_train, y_train)
y_scores_svc = svc.predict_proba(X_test)[:, 1]
precision_svc, recall_svc, thresholds_svc = precision_recall_curve(y_test, y_scores_svc)
y_scores_rf = rf.predict_proba(X_test)[:, 1]
precision_rf, recall_rf, thresholds_rf = precision_recall_curve(y_test, y_scores_rf)
closest_eq_idx_svc = np.argmin(np.abs(precision_svc - recall_svc))
best_threshold_svc = thresholds_svc[closest_eq_idx_svc]
close_zero_svc = np.argmin(np.abs(thresholds_svc))
closest_eq_idx_rf = np.argmin(np.abs(precision_rf - recall_rf))
best_threshold_rf = thresholds_rf[closest_eq_idx_rf]
close_default_rf = np.argmin(np.abs(thresholds_rf - 0.5))
plt.figure(figsize=(8, 6))
plt.plot(precision_svc, recall_svc, label="SVC Precision-Recall Curve")
plt.plot(precision_rf, recall_rf, label="RandomForest Precision-Recall Curve")
plt.plot(precision_svc[closest_eq_idx_svc], recall_svc[closest_eq_idx_svc], 'o',
markersize=10, label=f"Best Threshold SVC ({best_threshold_svc:.2f})",
fillstyle="none", c='r', mew=2)
plt.plot(precision_svc[close_zero_svc], recall_svc[close_zero_svc], 'o',
markersize=10, label="Threshold ~ 0 (SVC)", fillstyle="none", c='k', mew=2)
plt.plot(precision_rf[closest_eq_idx_rf], recall_rf[closest_eq_idx_rf], '^',
markersize=10, label=f"Best Threshold RF ({best_threshold_rf:.2f})",
fillstyle="none", c='g', mew=2)
plt.plot(precision_rf[close_default_rf], recall_rf[close_default_rf], '^',
markersize=10, label="Threshold 0.5 (RF)", fillstyle="none", c='b', mew=2)
plt.xlabel("Precision")
plt.ylabel("Recall")
plt.legend()
plt.title("Precision-Recall Curve for SVC and RandomForest")
plt.show()

En el gráfico de comparación podemos ver que el bosque aleatorio funciona mejor en los extremos, para requisitos de
recall
o deprecision
muy altos. Alrededor de cualquier nivel de precision,RF
tiene un mejor rendimiento. Si sólo nos fijamos en el \(f_{1}\)-score para comparar el rendimiento general, habríamos pasado por alto estas sutilezas. El \(f_{1}\)-score sólo capta un punto de la curva precision-recall, el dado por el umbral por defecto.
print("f1_score of random forest: {:.3f}".format(f1_score(y_test, rf.predict(X_test))))
print("f1_score of svc: {:.3f}".format(f1_score(y_test, svc.predict(X_test))))
f1_score of random forest: 0.978
f1_score of svc: 0.767
La comparación de dos curvas precision-recall proporciona una visión muy detallada, pero es un proceso bastante manual. Para la comparación automática de modelos, es posible que queramos resumir la información contenida en la curva, sin limitarnos a un umbral o punto de operación. Una forma concreta de resumir la curva precisión-recall es
calcular la integral o el área bajo la curva precision-recall, también conocida como precisión media
. Para calcular la precisión media se puede utilizar la funciónaverage_precision_score
. Como tenemos que calcular la curva ROC y considerar múltiples umbrales, el resultado dedecision_function
opredict_proba
debe pasarse aaverage_precision_score
, no el resultado depredict
.
from sklearn.metrics import average_precision_score
ap_rf = average_precision_score(y_test, rf.predict_proba(X_test)[:, 1])
ap_svc = average_precision_score(y_test, svc.decision_function(X_test))
print("Average precision of random forest: {:.3f}".format(ap_rf))
print("Average precision of svc: {:.3f}".format(ap_svc))
Average precision of random forest: 0.998
Average precision of svc: 0.948
Al promediar todos los umbrales posibles, vemos que el bosque aleatorio y el SVC tienen un rendimiento similar, con
RF
ligeramente por delante. Esto es bastante diferente del resultado que obtuvimos antes con \(f_{1}\)-score. Como la precisión media es el área área bajo una curva que va de 0 a 1, la precisión media siempre devuelve un valor entre 0 (worst) y 1 (best). La precisión media de un clasificador que asignadecision_function
al azar es la fracción de muestras positivas en el conjunto de datos.
12.4.1. Características operativas del receptor (ROC) y AUC#
La curva ROC analiza el rendimiento de los clasificadores a distintos umbrales, graficando la tasa de verdaderos positivos (TPR) frente a la tasa de falsos positivos (FPR). Mientras que TPR equivale al
recall
, FPR mide la fracción de negativos reales clasificados incorrectamente. A diferencia de la curvaprecision-recall
, la curva ROC evalúa el equilibrio entre la detección de positivos y la generación de falsos positivos.
Nótese que \(FPR\) se utiliza como métrica de rendimiento cuando el objetivo es limitar el número de verdaderos negativos. La curva ROC puede calcularse mediante la función
roc_curve
. Nótese que si \(FPR\rightarrow 0\) entonces el número de \(TN\) crece.
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_test, svc.decision_function(X_test))
plt.plot(fpr, tpr, label="ROC Curve")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
close_zero = np.argmin(np.abs(thresholds))
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero", fillstyle="none", c='k', mew=2)
plt.legend(loc=4);

Nótese que, los valores más pequeños en el eje \(x\) indican menos falsos positivos (FP) y más verdaderos negativos (TN). Los valores más grandes en el eje \(y\) indican más verdaderos positivos (TP) y menos falsos negativos (FN). En cuanto a la curva
precision-recall
, a menudo queremos resumir la curva ROC utilizando un solo número, el área bajo la curva (comúnmente se denomina simplemente AUC, y se entiende que la curva en cuestión es la curva ROC). Podemos calcular el área bajo la curvaROC
con la funciónroc_auc_score
from sklearn.metrics import roc_auc_score
rf_auc = roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1])
svc_auc = roc_auc_score(y_test, svc.decision_function(X_test))
print("AUC for Random Forest: {:.3f}".format(rf_auc))
print("AUC for SVC: {:.3f}".format(svc_auc))
AUC for Random Forest: 0.997
AUC for SVC: 0.921
from sklearn.metrics import roc_curve
fpr_rf, tpr_rf, thresholds_rf = roc_curve(y_test, rf.predict_proba(X_test)[:, 1])
plt.plot(fpr, tpr, label="ROC Curve SVC")
plt.plot(fpr_rf, tpr_rf, label="ROC Curve RF")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero SVC", fillstyle="none", c='k', mew=2)
close_default_rf = np.argmin(np.abs(thresholds_rf - 0.5))
plt.plot(fpr_rf[close_default_rf], tpr_rf[close_default_rf], '^', markersize=10, label="threshold 0.5 RF", fillstyle="none", c='k', mew=2)
plt.legend(loc=4);

El bosque aleatorio supera ligeramente a la SVM en AUC-score. La precisión media, representada por el área bajo la curva (AUC), varía entre 0 (peor) y 1 (mejor). Una predicción aleatoria siempre tiene un AUC de 0.5, sin importar el desequilibrio de clases, lo que hace que AUC sea una mejor métrica que el accuracy en estos casos.
El AUC mide qué tan bien el modelo clasifica muestras positivas en comparación con negativas. Un AUC de 1 indica que todas las muestras positivas tienen un puntaje mayor que las negativas. En problemas con clases desequilibradas, el AUC es más útil que el accuracy para la selección del modelo. Por ejemplo, al clasificar el dígito 9 frente a otros en un conjunto de datos, podemos evaluar el rendimiento de una SVM con diferentes valores de
gamma
.
plt.figure()
for gamma in [1, 0.07, 0.01]:
svc = SVC(gamma=gamma).fit(X_train, y_train)
accuracy = svc.score(X_test, y_test)
auc = roc_auc_score(y_test, svc.decision_function(X_test))
fpr, tpr, _ = roc_curve(y_test , svc.decision_function(X_test))
print("gamma = {:.2f} accuracy = {:.2f} AUC = {:.2f}".format(gamma, accuracy, auc))
plt.plot(fpr, tpr, label="gamma={:.3f}".format(gamma))
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.xlim(-0.01, 1)
plt.ylim(0, 1.02)
plt.legend(loc="best");
gamma = 1.00 accuracy = 0.62 AUC = 0.53
gamma = 0.07 accuracy = 0.62 AUC = 0.91
gamma = 0.01 accuracy = 0.63 AUC = 0.93

Los valores de
accuracy
paragamma
de1.0, 0.07
y0.01
son0.51, 0.94
y0.95
, respectivamente. Congamma=1.0
, el AUC es aleatorio, mientras que congamma=0.07
mejora a0.94
, y congamma=0.01
, alcanza0.95
, indicando que los positivos están mejor clasificados que los negativos. Con un umbral adecuado, el modelo puede clasificar perfectamente.Usar solo
accuracy
no revela esto, por lo que recomendamos elAUC
, especialmente en datos desequilibrados. Sin embargo, dado queAUC
no usa un umbral fijo, puede ser necesario ajustarlo para mejorar la clasificación.
12.5. Métricas para la clasificación multiclase#
La evaluación de la clasificación multiclase se basa en métricas de clasificación binaria, pero promediadas en todas las clases. El
accuracy
sigue siendo la fracción de ejemplos clasificados correctamente, pero no es ideal cuando las clases están desequilibradas.Por ejemplo, en un problema con tres clases (A: 85%, B: 10%, C: 5%), un
accuracy
del 85% puede ser engañoso. La clasificación multiclase es más difícil de interpretar que la binaria, por lo que, además delaccuracy
, se utilizan la matriz de confusión y el informe de clasificación. Aplicaremos estos métodos al reconocimiento de dígitos escritos a mano en el conjunto de datosdigits
.
from sklearn.metrics import accuracy_score
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, random_state=0)
lr = LogisticRegression().fit(X_train, y_train)
pred = lr.predict(X_test)
print("Accuracy: {:.3f}".format(accuracy_score(y_test, pred)))
print("Confusion matrix:\n{}".format(confusion_matrix(y_test, pred)))
Accuracy: 0.951
Confusion matrix:
[[37 0 0 0 0 0 0 0 0 0]
[ 0 40 0 0 0 0 0 0 2 1]
[ 0 1 40 3 0 0 0 0 0 0]
[ 0 0 0 43 0 0 0 0 1 1]
[ 0 0 0 0 37 0 0 1 0 0]
[ 0 0 0 0 0 46 0 0 0 2]
[ 0 1 0 0 0 0 51 0 0 0]
[ 0 0 0 1 1 0 0 46 0 0]
[ 0 3 1 0 0 0 0 0 43 1]
[ 0 0 0 0 0 1 0 0 1 45]]
El modelo tiene un accuracy del 95.1%, lo que ya nos dice que lo estamos haciendo bastante bien. La matriz de confusión nos proporciona algunos detalles más. Como en el caso binario, cada fila corresponde a una etiqueta verdadera y cada columna a una etiqueta predicha. En la siguiente figura se puede encontrar una mejor representación visual
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix
conf_matrix = confusion_matrix(y_test, pred).astype(int)
plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt="d", cmap="gray_r",
xticklabels=digits.target_names,
yticklabels=digits.target_names)
plt.xlabel("Etiqueta Predicha")
plt.ylabel("Etiqueta Real")
plt.title("Matriz de Confusión")
plt.show()

Para el dígito 0, las 37 muestras fueron clasificadas correctamente, sin falsos negativos ni falsos positivos. Sin embargo, hubo confusiones entre otros dígitos, como el 2, que se clasificó erróneamente como 3 en tres casos y como 1 en uno. También un dígito 8 se clasificó como 2. La función
classification_report
permite calcularprecision
,recall
y \(f\)-score para cada clase.
print(classification_report(y_test, pred))
precision recall f1-score support
0 1.00 1.00 1.00 37
1 0.89 0.93 0.91 43
2 0.98 0.91 0.94 44
3 0.91 0.96 0.93 45
4 0.97 0.97 0.97 38
5 0.98 0.96 0.97 48
6 1.00 0.98 0.99 52
7 0.98 0.96 0.97 48
8 0.91 0.90 0.91 48
9 0.90 0.96 0.93 47
accuracy 0.95 450
macro avg 0.95 0.95 0.95 450
weighted avg 0.95 0.95 0.95 450
No es de extrañar que los valores para
precision
yrecall
sean un 1 perfecto para la clase 0, ya que no hay confusiones con esta clase. Para la clase 6, en cambio, elprecision
es de 1 porque ninguna otra clase se clasificó erróneamente como 6. La métrica más utilizada para conjuntos de datos desequilibrados en el entorno multiclase es la versión multiclase del \(f\)-score
.La idea detrás del \(f\)-score multiclase es calcular un \(f\)-score binario por clase, siendo esa clase la positiva y las otras clases las negativas. Luego, estos \(f\)-scores por clase se promedian utilizando una de las siguientes estrategias:
El promedio “macro” calcula los \(f\)-scores no ponderadas por clase. De este modo, se da el mismo peso a todas las clases, independientemente de su tamaño.
El promedio “weighted” calcula la media de los \(f\)-scores por clase, ponderada por su soporte. Esto es lo que se indica en el informe de clasificación.
El promedio “micro” calcula el número total de falsos positivos, falsos negativos y verdaderos positivos en todas las clases, y luego calcula
precision, recall
y \(f\)-score
utilizando estos recuentos.
Si se preocupa por igual de cada muestra, se recomienda utilizar el “micro” \(f\)-score; si le importa cada clase por igual, se recomienda utilizar la media macro \(f\)-score.
print("Macro average f1 score: {:.3f}".format(f1_score(y_test, pred, average="macro")))
print("Weighted average f1 score: {:.3f}".format(f1_score(y_test, pred, average="weighted")))
print("Micro average f1 score: {:.3f}".format(f1_score(y_test, pred, average="micro")))
Macro average f1 score: 0.952
Weighted average f1 score: 0.951
Micro average f1 score: 0.951
12.6. Métricas de regresión#
La evaluación en regresión puede ser tan detallada como en clasificación, analizando sobrepredicción y subpredicción. Sin embargo, en la mayoría de los casos, el \(R^2\) usado por defecto en los regresores es suficiente. En aplicaciones empresariales, métricas como MSE o MAE pueden influir en la optimización de modelos. No obstante, \(R^2\) suele ser más intuitivo. En el curso de Time Series Forecasting, se profundizará en métricas como MAE, MAPE, MSE y RMSE.
12.6.1. Uso de métricas de evaluación en la selección de modelos#
Para evaluar modelos con métricas como AUC en
GridSearchCV
ocross_val_score
,scikit-learn
permite usar el argumentoscoring
. Basta con proporcionar una cadena con la métrica deseada, como"roc_auc"
, para cambiar la evaluación predeterminada (accuracy) a AUC. Esto facilita la selección de modelos en tareas como clasificar “nueve vs. resto” endigits
.
Scoring por defecto para la clasificación es
accuracy
print("Default scoring: {}".format(cross_val_score(SVC(), digits.data, digits.target == 9)))
Default scoring: [0.975 0.99166667 1. 0.99442897 0.98050139]
Proporcionar
scoring="accuracy"
no cambia los resultados
explicit_accuracy = cross_val_score(SVC(), digits.data, digits.target == 9, scoring="accuracy")
print("Explicit accuracy scoring: {}".format(explicit_accuracy))
Explicit accuracy scoring: [0.975 0.99166667 1. 0.99442897 0.98050139]
Ahora asignemos
scoring="roc_auc"
para modificar la técnica descoring
utilizada
roc_auc = cross_val_score(SVC(), digits.data, digits.target == 9, scoring="roc_auc")
print("AUC scoring: {}".format(roc_auc))
AUC scoring: [0.99717078 0.99854252 1. 0.999828 0.98400413]
Del mismo modo, podemos cambiar la métrica utilizada para elegir los mejores parámetros en
GridSearchCV
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target == 9, random_state=0)
Proporcionamos una red a manera de ilustración del punto
param_grid = {'gamma': [0.0001, 0.01, 0.1, 1, 10]}
Utilizando el score
accuracy
por defecto
grid = GridSearchCV(SVC(), param_grid=param_grid)
grid.fit(X_train, y_train)
print("Grid-Search with accuracy")
print("Best parameters:", grid.best_params_)
print("Best cross-validation score (accuracy)): {:.3f}".format(grid.best_score_))
print("Test set AUC: {:.3f}".format(
roc_auc_score(y_test, grid.decision_function(X_test))))
print("Test set accuracy: {:.3f}".format(grid.score(X_test, y_test)))
Grid-Search with accuracy
Best parameters: {'gamma': 0.0001}
Best cross-validation score (accuracy)): 0.976
Test set AUC: 0.992
Test set accuracy: 0.973
Utilizando el scoring AUC en su lugar
grid = GridSearchCV(SVC(), param_grid=param_grid, scoring="roc_auc")
grid.fit(X_train, y_train)
print("\nGrid-Search with AUC")
print("Best parameters:", grid.best_params_)
print("Best cross-validation score (AUC): {:.3f}".format(grid.best_score_))
print("Test set AUC: {:.3f}".format(
roc_auc_score(y_test, grid.decision_function(X_test))))
print("Test set accuracy: {:.3f}".format(grid.score(X_test, y_test)))
Grid-Search with AUC
Best parameters: {'gamma': 0.01}
Best cross-validation score (AUC): 0.998
Test set AUC: 1.000
Test set accuracy: 1.000
Cuando se utiliza
accuracy
, se selecciona el parámetrogamma=0.0001
, mientras que cuando se utiliza el AUC se seleccionagamma=0.01
. Accuracy score para la validación cruzada es coherente con elaccuracy
del conjunto de prueba en ambos casos. Sin embargo, al utilizar AUC se encontró un mejor ajuste de los parámetros en en términos de AUC e incluso en términos de accuracy. Los valores más importantes del parámetro de scoring para la clasificación sonaccuracy
(por defecto);roc_auc
para el área bajo la curva ROC;average_precision
para el área bajo la curva deprecision-recall
;f1, f1_macro, f1_micro
yf1_weighted
para \(f_{1}\)-score binario y las diferentes variantes ponderadas.En cuanto a la regresión, los valores más utilizados son
r2
para el score \(R^{2}\),mean_squared_error
para el error medio al cuadrado ymean_absolute_error
para el error medio absoluto. Puede encontrar una lista completa de argumentos admitidos en la documentación o consultando el diccionario SCORER definido en el módulometrics.scorer
.
Resumen y conclusiones
La validación cruzada, el grid-search y las métricas de evaluación son fundamentales para mejorar los modelos de aprendizaje automático. Hay dos puntos clave a considerar:
Validación cruzada: Permite evaluar un modelo como se comportaría en el futuro. Sin embargo, si se usa para seleccionar modelos o hiperparámetros, los datos de prueba quedan “agotados”, lo que lleva a estimaciones optimistas. Para evitarlo, se deben separar los datos en entrenamiento, validación y prueba, usando validación cruzada en el conjunto de entrenamiento para la selección de modelos y parámetros.
Métrica de evaluación:
precision
no siempre es el mejor criterio, especialmente en problemas con clases desbalanceadas, donde los falsos positivos y negativos tienen impactos diferentes. Es crucial elegir una métrica adecuada según el contexto.
Las herramientas descritas son esenciales para cualquier científico de datos. Sin embargo, grid-search y validación cruzada solo aplican a modelos supervisados individuales. En la siguiente sección, se introducirá Pipeline
para optimizar procesos más complejos.