1. Datos, características y evaluación
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Un modelo de aprendizaje automático es una función que no escribes tú: das ejemplos de entradas y de las respuestas que esperas, y un algoritmo elige la función. Antes de cualquier algoritmo, esta lección responde a dos preguntas que deciden si el resultado significa algo. ¿Qué son exactamente las entradas y las respuestas? ¿Y cómo se mide si la función es buena, en ejemplos que no ha visto?
| ML.NET | Tribuo | IX | |
|---|---|---|---|
| Un ejemplo | una fila de un IDataView |
un Example<T> |
una fila de un Array2<f64> |
| Las características | una columna Features de tipo vector |
las características del Example |
las columnas de la matriz |
| La etiqueta | una columna Label |
la salida T: Label, Regressor |
un Array1<f64> (regresión) o Array1<usize> (clases 0, 1, 2…) |
| División | TrainTestSplit |
TrainTestSplitter |
train_test_split |
| Puntuaciones | RegressionMetrics, MulticlassClassificationMetrics |
RegressionEvaluation, LabelEvaluation |
funciones de ix_supervised::metrics |
Ejecutar el código
Sección titulada «Ejecutar el código»El código del curso es un proyecto Cargo en code/machine-learning-ix, con un ejemplo por lección y uno por cada serie de soluciones de ejercicios. Necesitas Rust; el primer build descarga IX de GitHub y lo compila. La comprobación cruzada necesita Python con numpy y scikit-learn.
cd code\machine-learning-ixcargo run --release --example l01_evaluationpip install numpy==2.4.2 scikit-learn==1.8.0python crosscheck\crosscheck.pycd code/machine-learning-ixcargo run --release --example l01_evaluationpython3 -m pip install numpy==2.4.2 scikit-learn==1.8.0python3 crosscheck/crosscheck.pycd code/machine-learning-ixcargo run --release --example l01_evaluationpython3 -m pip install numpy==2.4.2 scikit-learn==1.8.0python3 crosscheck/crosscheck.pybash check.sh ejecuta lo mismo que el CI: formato, clippy, las pruebas unitarias y luego cada ejemplo, comparado con expected/.
Características y etiquetas
Sección titulada «Características y etiquetas»Un conjunto de datos para aprendizaje supervisado son dos cosas: una matriz X con una fila por ejemplo y una columna por característica (feature), y un vector y con una etiqueta por fila. En IX ambos son arrays de ndarray, así que el curso carga sus archivos CSV con el crate csv en los mismos tipos (src/data.rs, líneas 35-46):
pub fn load_builds(path: impl AsRef<Path>) -> Builds { let (_, rows) = records(path.as_ref()); let n = rows.len(); let pages = Array2::from_shape_fn((n, 1), |(i, _)| rows[i][2].parse().unwrap()); let seconds = rows.iter().map(|r| r[3].parse().unwrap()).collect(); // …}pages es una matriz de una columna, no un vector: todos los modelos de IX toman un Array2, incluso con una sola característica.
El primer conjunto de datos plantea una pregunta de regresión: ¿cuántos segundos tarda el build de este sitio, dado el número de páginas? Las primeras filas de builds.csv, y la primera salida de examples/l01_evaluation.rs:
created_at,sha,pages,build_seconds2026-09-13T15:53:16Z,eda2608,18,102026-09-13T16:05:01Z,f43ba7e,110,12== builds.csvx: [65, 1] [18.0, 110.0, 122.0], y: 65y mean 18.846, std 5.424 (hand)y mean 18.846, std 5.424 (ix_math::stats)Ambas desviaciones estándar dividen por n, no por n - 1: ix_math::stats::std_dev es la poblacional, como la de numpy por defecto; sample_variance, a su lado, divide por n - 1.
Dónde lee IX sus datos
Sección titulada «Dónde lee IX sus datos»El lector CSV propio de IX, ix_io::csv_io::read_csv, interpreta cada campo como un f64, y un campo que no es un número se convierte en NaN, sin error. DataBatch::to_array2 apila después las filas en la matriz. Leído con él, builds.csv tendría una columna de fechas NaN y una columna de hashes de commit NaN. El pipeline de ML detrás de la herramienta ix_ml_pipeline descarta por defecto cada fila que contiene un NaN, así que con un archivo así descartaría todas las filas y se detendría con All rows contain NaN values (según la lectura de ml_pipeline.rs, líneas 167-188; por verificar ejecutando la herramienta). Deja solo columnas numéricas en un archivo que le pases a IX.
Entrenamiento y prueba
Sección titulada «Entrenamiento y prueba»Un modelo se juzga con filas de las que no aprendió: si no, un modelo que memoriza las respuestas obtendría una puntuación perfecta. Así que las filas se dividen en dos: un conjunto de entrenamiento para ajustar el modelo y un conjunto de prueba para puntuarlo, usado una sola vez, al final.
Cómo dividir depende de la pregunta. Los builds están en orden temporal, y la pregunta trata del próximo build: la división honesta entrena con el pasado y prueba con el 20 % más reciente (src/evaluation.rs, líneas 5-10). ix_math::preprocessing::train_test_split primero baraja las filas, como TrainTestSplit de ML.NET y train_test_split de scikit-learn:
== splitchronological: train 52, test 13, test pages 274..289ix train_test_split: train 52, test 13, test pages [138, 253, 253, 130, 280, 241, 271, 130, 138, 138, 289, 274, 138]El conjunto de prueba aleatorio mezcla builds del primer día con builds de la última hora: se le preguntaría al modelo por el pasado, con el futuro en su conjunto de entrenamiento. Para series temporales, scikit-learn tiene TimeSeriesSplit; train_test_split de IX siempre baraja.
Una línea base y cuatro métricas de regresión
Sección titulada «Una línea base y cuatro métricas de regresión»Antes de cualquier modelo, una línea base (baseline): la predicción más simple, que no usa ninguna característica. Para un número, es la media de las etiquetas de entrenamiento, 17.385 segundos, para cada build de prueba. Un modelo que no la supera no ha aprendido nada.
Con yᵢ los valores reales, pᵢ las predicciones e ȳ la media de los valores reales:
| Métrica | Fórmula | Unidad | Se lee como |
|---|---|---|---|
| MSE, error cuadrático medio | 1/n Σ (yᵢ - pᵢ)² |
segundos² | castiga sobre todo los errores grandes |
| RMSE | √MSE |
segundos | un error típico, ponderado hacia los grandes |
| MAE, error absoluto medio | 1/n Σ abs(yᵢ - pᵢ) |
segundos | el error medio |
| R², coeficiente de determinación | 1 - Σ (yᵢ - pᵢ)² / Σ (yᵢ - ȳ)² |
ninguna | 1 es perfecto, 0 equivale a predecir ȳ |
Escritas a mano en src/evaluation.rs, líneas 47-61, y en IX en metrics.rs, líneas 48-87:
== baseline on the chronological test set: always 17.385 shand: mse 107.000, rmse 10.344, mae 7.308, r2 -0.996ix: mse 107.000, rmse 10.344, mae 7.308, r2 -0.996R² es negativo: la ȳ de su fórmula es la media de las etiquetas de prueba, 24.69 segundos, y la línea base predice la media de entrenamiento. Los builds se volvieron más lentos a medida que crecía el sitio, así que la media del pasado es peor apuesta que la media de los propios builds de prueba, que ningún modelo puede conocer de antemano. r2_score de scikit-learn da el mismo −0.996 en la comprobación cruzada.
Escalar, y el conjunto de prueba que se filtra
Sección titulada «Escalar, y el conjunto de prueba que se filtra»Muchos algoritmos comparan las características entre sí, o dan pasos del mismo tamaño en todas las direcciones: una característica medida en cientos (páginas) pesa entonces más que una medida en unidades (segundos). Estandarizar expresa cada característica en desviaciones estándar respecto a su media: z = (x - μ) / σ.
μ y σ se aprenden de los datos, como los parámetros de un modelo, así que deben venir solo de las filas de entrenamiento y aplicarse sin cambios a las filas de prueba. StandardScaler::fit y transform están separados por esa razón, igual que el estimador NormalizeMeanVariance de ML.NET y su transformador, y fit y transform de NormalizerStandardize en Deeplearning4j:
== scaling pageshand, train rows: mean 184.038, std 62.476ix, train rows: mean 184.038, std 62.476ix, all rows: mean 203.277, std 67.883first test row scaled: 1.440 (train statistics), 1.042 (all rows)Ajustado con todas las filas, el escalador ha visto el conjunto de prueba: el primer build de prueba, 274 páginas, queda a 1.042 desviaciones estándar por encima de la media en lugar de 1.440. El modelo se probaría con datos moldeados por el propio conjunto de prueba: eso es una fuga de datos (leakage), y hace que las puntuaciones parezcan mejores de lo que serán con datos nuevos (scikit-learn, errores comunes).
El pipeline de ML de IX, el código detrás de la herramienta MCP ix_ml_pipeline, lo hace en el orden equivocado cuando normalize está activado (está desactivado por defecto). run_pipeline ajusta el escalador y el PCA sobre la matriz completa, y solo después run_classification y run_regression (líneas 704-705) dividen las filas:
flowchart LR subgraph IX["ix_ml_pipeline en 490c395"] direction TB A1[cargar CSV] --> A2[descartar filas NaN] --> A3["si normalize: StandardScaler,<br/>luego PCA si se pide,<br/>ajustados con todas las filas"] --> A4[inferir la tarea] --> A5[división aleatoria] --> A6[entrenar, puntuar en prueba] end subgraph OK["Sin fuga de datos"] direction TB B1[cargar CSV] --> B2[dividir] --> B3["ajustar el escalador<br/>con las filas de entrenamiento"] --> B4["transformar filas de<br/>entrenamiento y de prueba"] --> B5[entrenar, puntuar en prueba] endEl efecto sobre una puntuación depende de los datos; el mecanismo es el que se imprime arriba. Leí este orden en el código pero no he ejecutado la herramienta (por verificar).
¿Es regresión o clasificación?
Sección titulada «¿Es regresión o clasificación?»El mismo pipeline adivina la tarea cuando no se le indica, con infer_task_type: si cada etiqueta es un entero no negativo y hay como mucho 20 valores distintos (el umbral que pasa run_pipeline), es clasificación. Los tiempos de build son segundos enteros:
== infer_task_type: 18 distinct values -> MulticlassClassification { n_classes: 18 }same seconds with 0.5 added to one row -> trueSi se le pide predecir los segundos de build, el pipeline entrenaría un clasificador con 18 clases, en el que 23 segundos es tan distinto de 24 como de 48. Sumar medio segundo a una fila lo vuelve a convertir en regresión. Las etiquetas enteras son habituales en regresión (conteos, precios en céntimos, duraciones), así que indica la tarea explícitamente.
Métricas de clasificación
Sección titulada «Métricas de clasificación»El segundo conjunto de datos plantea una pregunta de clasificación: ¿qué sistema operativo ejecutó un job de CI, a partir de los tiempos de sus pasos? 121 de los 186 jobs se ejecutaron en Ubuntu, 34 en Windows y 31 en macOS. La línea base es la clase más frecuente: responder siempre ubuntu.
== jobs.csv: [186, 5] ["queue_s", "setup_s", "checkout_s", "post_checkout_s", "complete_s"], classes ["ubuntu", "windows", "macos"] = [121, 34, 31]confusion matrix (rows = true class, columns = predicted):[[121, 0, 0], [34, 0, 0], [31, 0, 0]]accuracy 0.651 (hand), 0.651 (ix)La exactitud (accuracy), la proporción de respuestas correctas, es del 65 % para un modelo que nunca mira un job. Con clases desequilibradas, la exactitud sola oculta si las clases raras se encuentran alguna vez. La matriz de confusión lo muestra: m[t][p] cuenta las filas de clase real t predichas como p. Para una clase c:
- precisión =
m[c][c] / Σₜ m[t][c]: de los jobs predichos comoc, la proporción que realmente sonc; - exhaustividad (recall) =
m[c][c] / Σₚ m[c][p]: de los jobs que sonc, la proporción predicha comoc; - F1 =
2 · precision · recall / (precision + recall): alta solo cuando ambas lo son.
El promedio macro toma la media sobre las clases, así que cada clase cuenta lo mismo, sea cual sea su tamaño (src/evaluation.rs, líneas 78-90; IX metrics.rs, líneas 91-148):
ubuntu precision 0.651 recall 1.000 f1 0.788 | ix 0.651 1.000 0.788windows precision 0.000 recall 0.000 f1 0.000 | ix 0.000 0.000 0.000macos precision 0.000 recall 0.000 f1 0.000 | ix 0.000 0.000 0.000macro f1 0.263 (hand), 0.263 (ix f1_avg)Confusion Matrix:pred → 0 1 2true 0: 121 0 0true 1: 34 0 0true 2: 31 0 0Un F1 macro de 0.263 dice lo que la exactitud ocultaba. Una precisión sin nada predicho es 0/0: IX y la versión a mano devuelven 0, como scikit-learn por defecto, con una advertencia. MacroAccuracy de ML.NET es la misma idea: la exactitud de cada clase, promediada sobre las clases.
Puntos clave
Sección titulada «Puntos clave»- Las características son una matriz y las etiquetas un vector; los modelos de IX toman
Array2<f64>incluso para una sola característica, y el lector CSV de IX convierte el texto enNaNen silencio. - Divide antes de aprender nada de los datos, incluido el escalador; divide por tiempo cuando la pregunta trata del futuro.
- Compara cada modelo con una línea base. El R² en un conjunto de prueba puede ser negativo.
- Con clases desequilibradas, lee la matriz de confusión y las puntuaciones por clase o macro, no solo la exactitud.
- El pipeline de ML de IX, cuando se le pide normalizar, ajusta su escalador antes de dividir; además considera clasificación las etiquetas enteras con pocos valores: indica la tarea, y no te fíes de sus puntuaciones de prueba sin comprobar el orden.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en examples/l01_exercises.rs, y su salida en expected/l01_exercises.txt: el CI las comprueba con el resto.
- Una línea base mejor para una serie temporal: predice cada build de prueba con los segundos del build inmediatamente anterior. Puntúala con el MAE, el RMSE y el R² de IX, junto a la media de entrenamiento.
Solución
let previous: Array1<f64> = test.iter().map(|&i| y[i - 1]).collect();let mean = Array1::from_elem(test.len(), hand::mean(&pick(y, &train)));for (name, p) in [("previous build", &previous), ("training mean", &mean)] { println!( "{name:14}: mae {:.3}, rmse {:.3}, r2 {:.3}", metrics::mae(&y_test, p), metrics::rmse(&y_test, p), metrics::r_squared(&y_test, p) );}== exercise 1previous build: mae 4.923, rmse 8.485, r2 -0.343training mean : mae 7.308, rmse 10.344, r2 -0.996El build anterior es mejor línea base en todas las métricas: sigue la tendencia. El R² sigue siendo negativo, porque el último build tardó 48 segundos, más del doble de los 21 segundos del anterior.
- ¿Cuántos jobs de macOS caen en un conjunto de prueba aleatorio del 20 %? Cuéntalos para
train_test_splitcon las semillas 0 a 9, y luego cuéntalos en cada fold de prueba deStratifiedKFoldde IX con 5 folds.
Solución
let per_seed: Vec<usize> = (0..10) .map(|seed| { let split = train_test_split(&jobs.features, &labels, 0.2, seed).unwrap(); split.y_test.iter().filter(|&&c| c == 2.0).count() }) .collect();let folds = StratifiedKFold::new(5).split(&jobs.os);== exercise 2macos jobs in the 37 test rows of train_test_split, seeds 0 to 9: [6, 7, 6, 5, 7, 4, 7, 1, 7, 5]StratifiedKFold(5), (test rows, macos jobs) per fold: [(39, 7), (37, 6), (37, 6), (37, 6), (36, 6)]31 jobs de macOS de 186 son el 17 %, unos 6 de cada 37 filas. Una división aleatoria da cualquier cosa entre 1 y 7: con la semilla 7, el conjunto de prueba tiene un solo job de macOS, y una exhaustividad para macOS sería 0 o 1. Una división estratificada mantiene la proporción de cada clase en cada fold: StratifiedKFold::split baraja las filas de cada clase y las reparte entre los folds por turnos. train_test_split toma labels como f64, el tipo de la regresión, y no tiene opción estratificada.
Fuentes
Sección titulada «Fuentes»- scikit-learn: evaluación de modelos y errores comunes
- ML.NET: métricas de evaluación
- James, Witten, Hastie, Tibshirani, Taylor, An Introduction to Statistical Learning, capítulos 2 y 5
- IX en
490c395:ix-math/src/preprocessing.rs,ix-supervised/src/metrics.rs,ix-agent/src/ml_pipeline.rs