
Esta es una serie de artículos sobre mi viaje en curso en el bosque oscuro de
las competencias de
Kaggle como desarrollador de .NET.
Me centraré en redes neuronales (casi) puras en este y en los siguientes artículos. Significa que la mayoría de las partes aburridas de la preparación del conjunto de datos, como completar valores perdidos, selección de características, análisis de valores atípicos, etc. será omitido intencionalmente.
La pila tecnológica será C # + TensorFlow
tf.keras API. A partir de hoy también requerirá Windows. Los modelos más grandes en los próximos artículos pueden necesitar una GPU adecuada para que su tiempo de entrenamiento permanezca cuerdo.
¡Vamos a predecir los precios inmobiliarios!
Los precios de la vivienda son una gran competencia para los principiantes. Su conjunto de datos es pequeño, no hay reglas especiales, la tabla de clasificación pública tiene muchos participantes y puede enviar hasta 4 entradas por día.
Regístrese en Kaggle, si aún no lo ha hecho, únase a esta competencia y descargue los datos. El objetivo es predecir el precio de venta (columna SalePrice) para las entradas en
test.csv . El archivo contiene
train.csv , que tiene alrededor de 1500 entradas con un precio de venta conocido para entrenar. Comenzaremos cargando ese conjunto de datos y explorándolo un poco antes de entrar en las redes neuronales.
Analizar datos de entrenamiento
¿Dije que omitiremos la preparación del conjunto de datos? Mentí! Tienes que echar un vistazo al menos una vez.Para mi sorpresa, no encontré una manera fácil de cargar un archivo .csv en la biblioteca de clases estándar .NET, así que instalé un paquete NuGet, llamado
CsvHelper . Para simplificar la manipulación de datos, también obtuve mi nuevo paquete de extensión LINQ favorito
MoreLinq .
Carga de datos .csv en DataTablestatic DataTable LoadData(string csvFilePath) { var result = new DataTable(); using (var reader = new CsvDataReader(new CsvReader(new StreamReader(csvFilePath)))) { result.Load(reader); } return result; }
ML.NETUsar
DataTable para la manipulación de datos de entrenamiento es, de hecho, una mala idea.
Se supone que
ML.NET tiene la carga .csv y muchas de las operaciones de exploración y peparación de datos. Sin embargo, todavía no estaba listo para ese propósito en particular, cuando recién ingresé a la competencia de Precios de la vivienda.
Los datos se ven así (solo unas pocas filas y columnas):
Id | MSSubClass | Msoning | LotFrontage | Lotarea |
1 | 60 60 | RL | 65 | 8450 |
2 | 20 | RL | 80 | 9600 |
3 | 60 60 | RL | 68 | 11250 |
4 4 | 70 | RL | 60 60 | 9550 |
Después de cargar los datos, debemos eliminar la columna
Id , ya que en realidad no está relacionada con los precios de la vivienda:
var trainData = LoadData("train.csv"); trainData.Columns.Remove("Id");
Analizando los tipos de datos de columna
DataTable no deduce automáticamente los tipos de datos de las columnas y supone que todas son cadenas. Entonces, el siguiente paso es determinar lo que realmente tenemos. Para cada columna, calculé las siguientes estadísticas: número de valores distintos, cuántos de ellos son enteros y cuántos de ellos son números de coma flotante (un código fuente con todos los métodos auxiliares se vinculará al final del artículo):
var values = rows.Select(row => (string)row[column]); double floats = values.Percentage(v => double.TryParse(v, out _)); double ints = values.Percentage(v => int.TryParse(v, out _)); int distincts = values.Distinct().Count();
Columnas numéricas
Resulta que la mayoría de las columnas son en realidad ints, pero dado que las redes neuronales funcionan principalmente en números flotantes, las convertiremos a dobles de todos modos.
Columnas categóricas
Otras columnas describen categorías a las que pertenecía la propiedad en venta. Ninguno de ellos tiene demasiados valores diferentes, lo cual es bueno. Para usarlos como entrada para nuestra futura red neuronal, también deben convertirse al
doble .
Inicialmente, simplemente les asigné números de 0 a distinctValueCount - 1, pero eso no tiene mucho sentido, ya que en realidad no hay progresión de "Fachada: Azul" a "Fachada: Verde" a "Fachada: Blanco". Tan temprano que cambié eso a lo que se llama una
codificación de un
solo punto , donde cada valor único obtiene una columna de entrada separada. Por ejemplo, "Fachada: Azul" se convierte en [1,0,0], y "Fachada: Blanco" se convierte en [0,0,1].
Reuniéndolos a todos
Gran producción de exploración de datos.CentralAir: 2 valores, ints: 0.00%, flotantes: 0.00%
Calle: 2 valores, entradas: 0.00%, carrozas: 0.00%
Utilidades: 2 valores, ints: 0.00%, flotantes: 0.00%
Callejón: 3 valores, entradas: 0.00%, carrozas: 0.00%
BsmtHalfBath: 3 valores, ints: 100.00%, flotantes: 100.00%
HalfBath: 3 valores, ints: 100.00%, flotantes: 100.00%
LandSlope: 3 valores, ints: 0.00%, flota: 0.00%
PavedDrive: 3 valores, ints: 0.00%, flotantes: 0.00%
BsmtFullBath: 4 valores, ints: 100.00%, flotantes: 100.00%
ExterQual: 4 valores, ints: 0.00%, flotantes: 0.00%
Chimeneas: 4 valores, entradas: 100.00%, flotadores: 100.00%
Baño completo: 4 valores, entradas: 100.00%, flotantes: 100.00%
GarageFinish: 4 valores, ints: 0.00%, flotantes: 0.00%
KitchenAbvGr: 4 valores, ints: 100.00%, flotadores: 100.00%
KitchenQual: 4 valores, ints: 0.00%, flota: 0.00%
LandContour: 4 valores, ints: 0.00%, flotantes: 0.00%
LotShape: 4 valores, ints: 0.00%, flotantes: 0.00%
PoolQC: 4 valores, ints: 0.00%, flotantes: 0.00%
BldgType: 5 valores, ints: 0.00%, flotantes: 0.00%
BsmtCond: 5 valores, ints: 0.00%, flotantes: 0.00%
Bsmt Exposición: 5 valores, ints: 0.00%, flotantes: 0.00%
BsmtQual: 5 valores, ints: 0.00%, flotantes: 0.00%
ExterCond: 5 valores, ints: 0.00%, flotantes: 0.00%
Valla: 5 valores, entradas: 0.00%, flotadores: 0.00%
GarageCars: 5 valores, ints: 100.00%, flotadores: 100.00%
Calefacción QC: 5 valores, ints: 0.00%, flotadores: 0.00%
LotConfig: 5 valores, ints: 0.00%, flotantes: 0.00%
MasVnrType: 5 valores, ints: 0.00%, flotantes: 0.00%
MiscFeature: 5 valores, ints: 0.00%, flotantes: 0.00%
MS Zoning: 5 valores, ints: 0.00%, flotantes: 0.00%
Año de venta: 5 valores, entradas: 100.00%, flotantes: 100.00%
Eléctrico: 6 valores, ints: 0.00%, flotadores: 0.00%
FireplaceQu: 6 valores, ints: 0.00%, flota: 0.00%
Fundación: 6 valores, ints: 0.00%, flotadores: 0.00%
GarageCond: 6 valores, ints: 0.00%, flotantes: 0.00%
GarageQual: 6 valores, ints: 0.00%, flotantes: 0.00%
Calefacción: 6 valores, ints: 0.00%, flotadores: 0.00%
RoofStyle: 6 valores, ints: 0.00%, flotantes: 0.00%
Condición de venta: 6 valores, ints: 0.00%, flotadores: 0.00%
BsmtFinType1: 7 valores, ints: 0.00%, flotantes: 0.00%
BsmtFinType2: 7 valores, ints: 0.00%, flotantes: 0.00%
Funcional: 7 valores, ints: 0.00%, flotantes: 0.00%
GarageType: 7 valores, ints: 0.00%, flotantes: 0.00%
BedroomAbvGr: 8 valores, ints: 100.00%, flotantes: 100.00%
Condición2: 8 valores, ints: 0.00%, flotantes: 0.00%
HouseStyle: 8 valores, ints: 0.00%, flotantes: 0.00%
PoolArea: 8 valores, ints: 100.00%, flotantes: 100.00%
RoofMatl: 8 valores, ints: 0.00%, flotantes: 0.00%
Condición1: 9 valores, ints: 0.00%, flotantes: 0.00%
OverallCond: 9 valores, ints: 100.00%, flota: 100.00%
SaleType: 9 valores, ints: 0.00%, floats: 0.00%
OverallQual: 10 valores, ints: 100.00%, flota: 100.00%
MoSold: 12 valores, entradas: 100.00%, flotantes: 100.00%
TotRmsAbvGrd: 12 valores, ints: 100.00%, flotantes: 100.00%
Exterior1: 15 valores, ints: 0.00%, flotantes: 0.00%
MSSubClass: 15 valores, ints: 100.00%, flotantes: 100.00%
Exterior 2: 16 valores, ints: 0.00%, flotantes: 0.00%
3SsnPorch: 20 valores, ints: 100.00%, flotantes: 100.00%
MiscVal: 21 valores, ints: 100.00%, flotantes: 100.00%
LowQualFinSF: 24 valores, ints: 100.00%, flotantes: 100.00%
Barrio: 25 valores, entradas: 0.00%, flotadores: 0.00%
YearRemodAdd: 61 valores, ints: 100.00%, flotantes: 100.00%
ScreenPorch: 76 valores, ints: 100.00%, flota: 100.00%
GarageYrBlt: 98 valores, ints: 94.45%, flotadores: 94.45%
LotFrontage: 111 valores, ints: 82.26%, flotantes: 82.26%
Año Construido: 112 valores, entradas: 100.00%, flotantes: 100.00%
Porche cerrado: 120 valores, entradas: 100.00%, flotadores: 100.00%
BsmtFinSF2: 144 valores, ints: 100.00%, flotantes: 100.00%
OpenPorchSF: 202 valores, ints: 100.00%, flotantes: 100.00%
WoodDeckSF: 274 valores, entradas: 100.00%, flotadores: 100.00%
MasVnrArea: 328 valores, ints: 99.45%, flotantes: 99.45%
2ndFlrSF: 417 valores, entradas: 100.00%, flotantes: 100.00%
Área de garaje: 441 valores, entradas: 100.00%, flotadores: 100.00%
BsmtFinSF1: 637 valores, ints: 100.00%, flotantes: 100.00%
Precio de venta: 663 valores, entradas: 100.00%, flotadores: 100.00%
TotalBsmtSF: 721 valores, ints: 100.00%, flotantes: 100.00%
1stFlrSF: 753 valores, entradas: 100.00%, flotantes: 100.00%
BsmtUnfSF: 780 valores, ints: 100.00%, flotantes: 100.00%
GrLivArea: 861 valores, entradas: 100.00%, flotantes: 100.00%
LotArea: 1073 valores, ints: 100.00%, flotantes: 100.00%
Muchas columnas de valor:
Exterior1st: AsbShng, AsphShn, BrkComm, BrkFace, CBlock, CemntBd, HdBoard, ImStucc, MetalSd, Plywood, Stone, Stucco, VinylSd, Wd Sdng, WdShing
Exterior2nd: AsbShng, AsphShn, Brk Cmn, BrkFace, CBlock, CmentBd, HdBoard, ImStucc, MetalSd, Other, Plywood, Stone, Stucco, VinylSd, Wd Sdng, Wd Shng
Vecindad: Blmngtn, Blueste, BrDale, BrkSide, ClearCr, CollgCr, Crawfor, Edwards, Gilbert, IDOTRR, MeadowV, Mitchel, NAmes, NoRidge, NPkVill, NridgHt, NWAmes, OldTown, Sawyer, SawyerW, Somerst, Stone, Stoner Veenker
flotadores no analizables
GarageYrBlt: NA
LotFrontage: NA
MasVnrArea: NA
rangos de flotación:
BsmtHalfBath: 0 ... 2
Medio baño: 0 ... 2
BsmtFullBath: 0 ... 3
Chimeneas: 0 ... 3
Baño completo: 0 ... 3
KitchenAbvGr: 0 ... 3
GarageCars: 0 ... 4
Año de venta: 2006 ... 2010
DormitorioAbvGr: 0 ... 8
Área de piscina: 0 ... 738
OverallCond: 1 ... 9
General Qualual: 1 ... 10
MoSold: 1 ... 12
TotRmsAbvGrd: 2 ... 14
MSSubClass: 20 ... 190
3SsnPorche: 0 ... 508
Miscelánea: 0 ... 15500
LowQualFinSF: 0 ... 572
Año Modificado Agregar: 1950 ... 2010
ScreenPorch: 0 ... 480
GarageYrBlt: 1900 ... 2010
LotFrontage: 21 ... 313
Año de construcción: 1872 ... 2010
Porche cerrado: 0 ... 552
BsmtFinSF2: 0 ... 1474
OpenPorchSF: 0 ... 547
WoodDeckSF: 0 ... 857
MasVnrArea: 0 ... 1600
2ndFlrSF: 0 ... 2065
Área de garaje: 0 ... 1418
BsmtFinSF1: 0 ... 5644
Precio de venta: 34,900 ... 755,000
TotalBsmtSF: 0 ... 6110
1stFlrSF: 334 ... 4692
BsmtUnfSF: 0 ... 2336
GrLivArea: 334 ... 5642
LotArea: 1300 ... 215245
Con eso en mente, construí el siguiente
ValueNormalizer , que toma información sobre los valores dentro de la columna y devuelve una función, que transforma un valor (una
cadena ) en un vector de características numéricas para la red neuronal (
doble [] ):
ValueNormalizer static Func<string, double[]> ValueNormalizer( double floats, IEnumerable<string> values) { if (floats > 0.01) { double max = values.AsDouble().Max().Value; return s => new[] { double.TryParse(s, out double v) ? v / max : -1 }; } else { string[] domain = values.Distinct().OrderBy(v => v).ToArray(); return s => new double[domain.Length+1] .Set(Array.IndexOf(domain, s)+1, 1); } }
Ahora tenemos los datos convertidos a un formato, adecuado para una red neuronal. Es hora de construir uno.
Construye una red neuronal
A partir de hoy, necesitaría usar una máquina Windows para eso.Si ya tiene Python y TensorFlow 1.1x instalados, todo lo que necesita es
<PackageReference Include="Gradient" Version="0.1.10-tech-preview3" />
en tu archivo .csproj moderno. De lo contrario, consulte el
manual Gradient para hacer la configuración inicial.
Una vez que el paquete está en funcionamiento, podemos crear nuestra primera red profunda poco profunda.
using tensorflow; using tensorflow.keras; using tensorflow.keras.layers; using tensorflow.train; ... var model = new Sequential(new Layer[] { new Dense(units: 16, activation: tf.nn.relu_fn), new Dropout(rate: 0.1), new Dense(units: 10, activation: tf.nn.relu_fn), new Dense(units: 1, activation: tf.nn.relu_fn), }); model.compile(optimizer: new AdamOptimizer(), loss: "mean_squared_error");
Esto creará una red neuronal no entrenada con 3 capas de neuronas y una capa de abandono, que ayuda a prevenir el sobreajuste.
tf.nn.relu_fntf.nn.relu_fn es la función de activación de nuestras neuronas. Se sabe que
ReLU funciona bien en redes profundas, porque resuelve el
problema del gradiente de fuga : las derivadas de las funciones de activación no lineales originales tienden a ser muy pequeñas cuando el error se propaga desde la capa de salida en las redes profundas. Eso significaba que las capas más cercanas a la entrada solo se ajustarían muy ligeramente, lo que ralentizó significativamente el entrenamiento de redes profundas.
AbandonoLa deserción es una capa de función especial en las redes neuronales, que en realidad no contiene neuronas como tales. En cambio, funciona tomando cada entrada individual y la reemplaza aleatoriamente con 0 en la salida automática (de lo contrario, solo pasa el valor original). Al hacerlo, ayuda a evitar el
sobreajuste a características menos relevantes en un pequeño conjunto de datos. Por ejemplo, si no eliminamos la columna
Id , la red podría haber memorizado exactamente el mapeo <Id> -> <SalePrice> exactamente, lo que nos daría una precisión del 100% en el conjunto de entrenamiento, pero números completamente no relacionados en cualquier otro dato.
¿Por qué necesitamos abandonar? Nuestros datos de entrenamiento solo tienen ~ 1500 ejemplos, y esta pequeña red neuronal que hemos construido tiene> 1800 pesos ajustables. Si fuera un polinomio simple, podría coincidir con la función de precio que estamos tratando de aproximar exactamente. Pero entonces tendría valores enormes en cualquier entrada fuera del conjunto de entrenamiento original.
Alimentar los datos
TensorFlow espera sus datos en matrices NumPy o tensores existentes. Estoy convirtiendo DataRows en matrices NumPy:
using numpy; ... const string predict = "SalePrice"; ndarray GetInputs(IEnumerable<DataRow> rowSeq) { return np.array(rowSeq.Select(row => np.array( columnTypes .Where(c => c.column.ColumnName != predict) .SelectMany(column => column.normalizer( row.Table.Columns.Contains(column.column.ColumnName) ? (string)row[column.column.ColumnName] : "-1")) .ToArray())) .ToArray() ); } var predictColumn = columnTypes.Single(c => c.column.ColumnName == predict); ndarray trainOutputs = np.array(predictColumn.trainValues .AsDouble() .Select(v => v ?? -1) .ToArray()); ndarray trainInputs = GetInputs(trainRows);
En el código anterior, convertimos cada
DataRow en un
ndarray al tomar cada celda y aplicar el
ValueNormalizer correspondiente a su columna. Luego colocamos todas las filas en otro
ndarray , obteniendo una matriz de matrices.
No se necesita tal transformación para las salidas, donde simplemente convertimos los valores del tren a otro
ndarray .
Es hora de bajar el gradiente
Con esta configuración, todo lo que necesitamos hacer para entrenar nuestra red es llamar a la función de
ajuste del modelo:
model.fit(trainInputs, trainOutputs, epochs: 2000, validation_split: 0.075, verbose: 2);
Esta llamada realmente reservará el último 7.5% del conjunto de entrenamiento para la validación, luego repita las siguientes 2000 veces:
- dividir el resto de entradas de tren en lotes
- alimentar estos lotes uno por uno en la red neuronal
- calcular error usando la función de pérdida que definimos anteriormente
- propaga el error por los gradientes de las conexiones neuronales individuales, ajustando los pesos
Durante el entrenamiento, generará el error de la red en los datos que dejó de lado para la validación como
val_loss y el error en los datos de entrenamiento en sí mismo como una
pérdida . En general, si
val_loss se vuelve mucho mayor que la
pérdida , significa que la red comenzó a sobreajustarse. Abordaré eso con más detalle en los siguientes artículos.
Si hiciste todo correctamente, una
raíz cuadrada de una de tus pérdidas debería ser del orden de 20000.

Sumisión
No hablaré mucho sobre generar el archivo para enviar aquí. El código para calcular las salidas es simple:
const string SubmissionInputFile = "test.csv"; DataTable submissionData = LoadData(SubmissionInputFile); var submissionRows = submissionData.Rows.Cast<DataRow>(); ndarray submissionInputs = GetInputs(submissionRows); ndarray sumissionOutputs = model.predict(submissionInputs);
que utiliza principalmente funciones, que se definieron anteriormente.
Luego, debe escribirlos en un archivo .csv, que es simplemente una lista de Id, pares de valores pronosticados.
Cuando envíe su resultado, debería obtener una puntuación del orden de 0.17, que estaría en algún lugar en el último trimestre de la tabla de clasificación pública. Pero bueno, si fuera tan simple como una red de 3 capas con 27 neuronas, esos molestos científicos de datos no obtendrían compensaciones totales de $ 300k + / a de las principales compañías estadounidenses
Terminando
El código fuente completo para esta entrada (con todos los ayudantes, y algunas de las partes comentadas de mis anteriores exploraciones y experimentos) es de aproximadamente 200 líneas en el
PasteBin .
En el próximo artículo, verás a mis travesuras tratando de llegar al 50% superior de esa tabla de clasificación pública. Va a ser la aventura de un viajero aficionado, una pelea con The Windmill of Overfitting con la única herramienta que tiene el peregrino: un modelo más grande (por ejemplo, ¡NN profundo, recuerda, sin ingeniería manual de características!). Será menos un tutorial de codificación, y más una búsqueda de pensamiento con matemáticas realmente maliciosas y una conclusión extraña. Estén atentos!
Enlaces
KaggleConcurso de precios de la vivienda en KaggleTutorial de regresión de TensorFlowPágina de inicio de TensorFlowReferencia de la API de TensorFlowGradiente (enlace TensorFlow)