.NET, TensorFlow y los molinos de viento de Kaggle: el viaje comienza

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 DataTable
static DataTable LoadData(string csvFilePath) { var result = new DataTable(); using (var reader = new CsvDataReader(new CsvReader(new StreamReader(csvFilePath)))) { result.Load(reader); } return result; } 


ML.NET
Usar 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):

IdMSSubClassMsoningLotFrontageLotarea
160 60RL658450
220RL809600
360 60RL6811250
4 470RL60 609550


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_fn
tf.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.

Abandono
La 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:

  1. dividir el resto de entradas de tren en lotes
  2. alimentar estos lotes uno por uno en la red neuronal
  3. calcular error usando la función de pérdida que definimos anteriormente
  4. 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


Kaggle
Concurso de precios de la vivienda en Kaggle
Tutorial de regresión de TensorFlow
Página de inicio de TensorFlow
Referencia de la API de TensorFlow
Gradiente (enlace TensorFlow)

Source: https://habr.com/ru/post/437174/


All Articles