
Dies ist eine Reihe von Artikeln über meine weitere Reise in den dunklen Wald der
Kaggle- Wettbewerbe als .NET-Entwickler.
Ich werde mich in diesem und den folgenden Artikeln auf (fast) reine neuronale Netze konzentrieren. Dies bedeutet, dass die meisten langweiligen Teile der Datensatzvorbereitung, wie das Ausfüllen fehlender Werte, die Auswahl von Features, die Analyse von Ausreißern usw. wird absichtlich übersprungen.
Der Tech-Stack ist C # + TensorFlow
tf.keras API. Ab heute wird auch Windows benötigt. Größere Modelle in zukünftigen Artikeln benötigen möglicherweise eine geeignete GPU, damit ihre Trainingszeit gesund bleibt.
Lassen Sie uns die Immobilienpreise vorhersagen!
House Prices ist ein großartiger Wettbewerb für Anfänger. Der Datensatz ist klein, es gibt keine besonderen Regeln, die öffentliche Rangliste hat viele Teilnehmer und Sie können bis zu 4 Einträge pro Tag einreichen.
Registrieren Sie sich bei Kaggle. Wenn Sie dies noch nicht getan haben, nehmen Sie an diesem Wettbewerb teil und laden Sie die Daten herunter. Ziel ist es, den Verkaufspreis (Spalte "Verkaufspreis") für Einträge in
test.csv vorherzusagen . Das Archiv enthält
train.csv , das ungefähr 1500 Einträge mit bekanntem Verkaufspreis zum Trainieren enthält. Wir beginnen mit dem Laden dieses Datensatzes und untersuchen ihn ein wenig, bevor wir uns mit neuronalen Netzen befassen.
Trainingsdaten analysieren
Habe ich gesagt, wir werden die Datensatzvorbereitung überspringen? Ich habe gelogen! Sie müssen mindestens einmal einen Blick darauf werfen.Zu meiner Überraschung fand ich keine einfache Möglichkeit, eine CSV-Datei in die .NET-Standardklassenbibliothek zu laden, und installierte daher ein NuGet-Paket namens
CsvHelper . Um die Datenmanipulation zu vereinfachen, habe ich auch mein neues Lieblings-LINQ-Erweiterungspaket
MoreLinq erhalten .
Laden von CSV-Daten in 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.NETDie Verwendung von
DataTable zum Trainieren der Datenmanipulation ist eigentlich eine schlechte Idee.
ML.NET soll das Laden von .csv und viele der Datenaufbereitungs- und Explorationsoperationen haben. Es war jedoch noch nicht für diesen speziellen Zweck bereit, als ich gerade am Hauspreiswettbewerb teilnahm.
Die Daten sehen folgendermaßen aus (nur wenige Zeilen und Spalten):
Id | MSSubClass | Msoning | LotFrontage | Lotarea |
1 | 60 | RL | 65 | 8450 |
2 | 20 | RL | 80 | 9600 |
3 | 60 | RL | 68 | 11250 |
4 | 70 | RL | 60 | 9550 |
Nach dem Laden der Daten müssen wir die
ID- Spalte entfernen, da sie eigentlich nichts mit den Immobilienpreisen zu tun hat:
var trainData = LoadData("train.csv"); trainData.Columns.Remove("Id");
Analysieren der Spaltendatentypen
DataTable leitet Datentypen der Spalten nicht automatisch ab und geht davon aus, dass es sich nur um Zeichenfolgen handelt. Der nächste Schritt besteht also darin, festzustellen, was wir tatsächlich haben. Für jede Spalte habe ich die folgenden Statistiken berechnet: Anzahl der unterschiedlichen Werte, wie viele davon sind Ganzzahlen und wie viele davon sind Gleitkommazahlen (ein Quellcode mit allen Hilfsmethoden wird am Ende des Artikels verknüpft):
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();
Numerische Spalten
Es stellt sich heraus, dass die meisten Spalten tatsächlich Ints sind, aber da neuronale Netze hauptsächlich mit schwebenden Zahlen arbeiten, werden wir sie trotzdem in Doubles konvertieren.
Kategoriale Spalten
Andere Spalten beschreiben Kategorien, zu denen die zum Verkauf stehende Immobilie gehört. Keiner von ihnen hat zu viele verschiedene Werte, was gut ist. Um sie als Eingabe für unser zukünftiges neuronales Netzwerk zu verwenden, müssen sie ebenfalls in
double konvertiert werden.
Anfangs habe ich ihnen einfach Zahlen von 0 bis differentValueCount - 1 zugewiesen, aber das macht wenig Sinn, da es eigentlich keinen Fortschritt von "Fassade: Blau" über "Fassade: Grün" zu "Fassade: Weiß" gibt. So früh habe ich das in eine sogenannte
One-Hot-Codierung geändert, bei der jeder eindeutige Wert eine separate Eingabespalte erhält. ZB wird "Fassade: Blau" zu [1,0,0] und "Fassade: Weiß" zu [0,0,1].
Sie alle zusammenbringen
Große Ausgabe der DatenexplorationCentralAir: 2 Werte, Ints: 0,00%, Floats: 0,00%
Straße: 2 Werte, Ints: 0,00%, Floats: 0,00%
Dienstprogramme: 2 Werte, Ints: 0,00%, Floats: 0,00%
Gasse: 3 Werte, Ints: 0,00%, Floats: 0,00%
BsmtHalfBath: 3 Werte, Ints: 100,00%, Floats: 100,00%
Halbbad: 3 Werte, Ints: 100,00%, Floats: 100,00%
LandSlope: 3 Werte, Ints: 0,00%, Floats: 0,00%
PavedDrive: 3 Werte, Ints: 0,00%, Floats: 0,00%
BsmtFullBath: 4 Werte, Ints: 100,00%, Floats: 100,00%
ExterQual: 4 Werte, Ints: 0,00%, Floats: 0,00%
Kamine: 4 Werte, Ints: 100,00%, Floats: 100,00%
FullBath: 4 Werte, Ints: 100,00%, Floats: 100,00%
GarageFinish: 4 Werte, Ints: 0,00%, Floats: 0,00%
KitchenAbvGr: 4 Werte, Ints: 100,00%, Floats: 100,00%
KitchenQual: 4 Werte, Ints: 0,00%, Floats: 0,00%
LandContour: 4 Werte, Ints: 0,00%, Floats: 0,00%
LotShape: 4 Werte, Ints: 0,00%, Floats: 0,00%
PoolQC: 4 Werte, Ints: 0,00%, Floats: 0,00%
BldgType: 5 Werte, Ints: 0,00%, Floats: 0,00%
BsmtCond: 5 Werte, Ints: 0,00%, Floats: 0,00%
BsmtExposure: 5 Werte, Ints: 0,00%, Floats: 0,00%
BsmtQual: 5 Werte, Ints: 0,00%, Floats: 0,00%
ExterCond: 5 Werte, Ints: 0,00%, Floats: 0,00%
Zaun: 5 Werte, Ints: 0,00%, Floats: 0,00%
GarageCars: 5 Werte, Ints: 100,00%, Floats: 100,00%
HeatingQC: 5 Werte, Ints: 0,00%, Floats: 0,00%
LotConfig: 5 Werte, Ints: 0,00%, Floats: 0,00%
MasVnrType: 5 Werte, Ints: 0,00%, Floats: 0,00%
MiscFeature: 5 Werte, Ints: 0,00%, Floats: 0,00%
MSZoning: 5 Werte, Ints: 0,00%, Floats: 0,00%
YrSold: 5 Werte, Ints: 100,00%, Floats: 100,00%
Elektrik: 6 Werte, Ints: 0,00%, Floats: 0,00%
FireplaceQu: 6 Werte, Ints: 0,00%, Floats: 0,00%
Foundation: 6 Werte, Ints: 0,00%, Floats: 0,00%
GarageCond: 6 Werte, Ints: 0,00%, Floats: 0,00%
GarageQual: 6 Werte, Ints: 0,00%, Floats: 0,00%
Heizung: 6 Werte, Ints: 0,00%, Floats: 0,00%
RoofStyle: 6 Werte, Ints: 0,00%, Floats: 0,00%
Verkaufsbedingung: 6 Werte, Ints: 0,00%, Floats: 0,00%
BsmtFinType1: 7 Werte, Ints: 0,00%, Floats: 0,00%
BsmtFinType2: 7 Werte, Ints: 0,00%, Floats: 0,00%
Funktionell: 7 Werte, Ints: 0,00%, Floats: 0,00%
GarageType: 7 Werte, Ints: 0,00%, Floats: 0,00%
BedroomAbvGr: 8 Werte, Ints: 100,00%, Floats: 100,00%
Bedingung 2: 8 Werte, Ints: 0,00%, Floats: 0,00%
HouseStyle: 8 Werte, Ints: 0,00%, Floats: 0,00%
PoolArea: 8 Werte, Ints: 100,00%, Floats: 100,00%
RoofMatl: 8 Werte, Ints: 0,00%, Floats: 0,00%
Bedingung1: 9 Werte, Ints: 0,00%, Floats: 0,00%
OverallCond: 9 Werte, Ints: 100,00%, Floats: 100,00%
SaleType: 9 Werte, Ints: 0,00%, Floats: 0,00%
OverallQual: 10 Werte, Ints: 100,00%, Floats: 100,00%
MoSold: 12 Werte, Ints: 100,00%, Floats: 100,00%
TotRmsAbvGrd: 12 Werte, Ints: 100,00%, Floats: 100,00%
Exterior1st: 15 Werte, Ints: 0,00%, Floats: 0,00%
MSSubClass: 15 Werte, Ints: 100,00%, Floats: 100,00%
Exterior2nd: 16 Werte, Ints: 0,00%, Floats: 0,00%
3SsnPorch: 20 Werte, Ints: 100,00%, Floats: 100,00%
MiscVal: 21 Werte, Ints: 100,00%, Floats: 100,00%
LowQualFinSF: 24 Werte, Ints: 100,00%, Floats: 100,00%
Nachbarschaft: 25 Werte, Ints: 0,00%, Floats: 0,00%
YearRemodAdd: 61 Werte, Ints: 100,00%, Floats: 100,00%
ScreenPorch: 76 Werte, Ints: 100,00%, Floats: 100,00%
GarageYrBlt: 98 Werte, Ints: 94,45%, Floats: 94,45%
LotFrontage: 111 Werte, Ints: 82,26%, Floats: 82,26%
YearBuilt: 112 Werte, Ints: 100,00%, Floats: 100,00%
EnclosedPorch: 120 Werte, Ints: 100,00%, Floats: 100,00%
BsmtFinSF2: 144 Werte, Ints: 100,00%, Floats: 100,00%
OpenPorchSF: 202 Werte, Ints: 100,00%, Floats: 100,00%
WoodDeckSF: 274 Werte, Ints: 100,00%, Floats: 100,00%
MasVnrArea: 328 Werte, Ints: 99,45%, Floats: 99,45%
2ndFlrSF: 417 Werte, Ints: 100,00%, Floats: 100,00%
GarageArea: 441 Werte, Ints: 100,00%, Floats: 100,00%
BsmtFinSF1: 637 Werte, Ints: 100,00%, Floats: 100,00%
Verkaufspreis: 663 Werte, Ints: 100,00%, Floats: 100,00%
TotalBsmtSF: 721 Werte, Ints: 100,00%, Floats: 100,00%
1stFlrSF: 753 Werte, Ints: 100,00%, Floats: 100,00%
BsmtUnfSF: 780 Werte, Ints: 100,00%, Floats: 100,00%
GrLivArea: 861 Werte, Ints: 100,00%, Floats: 100,00%
LotArea: 1073 Werte, Ints: 100,00%, Floats: 100,00%
Viele Wertespalten:
Außen 1st: AsbShng, AsphShn, BrkComm, BrkFace, CBlock, CemntBd, HdBoard, ImStucc, MetalSd, Sperrholz, Stein, Stuck, VinylSd, Wd Sdng, WdShing
Exterior2nd: AsbShng, AsphShn, Brk Cmn, BrkFace, CBlock, CmentBd, HdBoard, ImStucc, MetalSd, Andere, Sperrholz, Stein, Stuck, VinylSd, Wd Sdng, Wd Shng
Nachbarschaft: Blmngtn, Blueste, BrDale, BrkSide, ClearCr, CollgCr, Crawfor, Edwards, Gilbert, IDOTRR, MeadowV, Mitchel, NAmes, NoRidge, NPkVill, NridgHt, NWAmes, OldTown, Sawyer, SawyerW, Somerst, Stone, Stoner Veenker
nicht analysierbare Schwimmer
GarageYrBlt: NA
LotFrontage: NA
MasVnrArea: NA
Schwimmerbereiche:
BsmtHalfBath: 0 ... 2
Halbbad: 0 ... 2
BsmtFullBath: 0 ... 3
Kamine: 0 ... 3
Vollbad: 0 ... 3
KitchenAbvGr: 0 ... 3
GarageCars: 0 ... 4
YrSold: 2006 ... 2010
BedroomAbvGr: 0 ... 8
PoolArea: 0 ... 738
OverallCond: 1 ... 9
OverallQual: 1 ... 10
MoSold: 1 ... 12
TotRmsAbvGrd: 2 ... 14
MSSubClass: 20 ... 190
3SsnPorch: 0 ... 508
Verschiedenes: 0 ... 15500
LowQualFinSF: 0 ... 572
YearRemodAdd: 1950 ... 2010
ScreenPorch: 0 ... 480
GarageYrBlt: 1900 ... 2010
LotFrontage: 21 ... 313
Baujahr: 1872 ... 2010
EnclosedPorch: 0 ... 552
BsmtFinSF2: 0 ... 1474
OpenPorchSF: 0 ... 547
WoodDeckSF: 0 ... 857
MasVnrArea: 0 ... 1600
2ndFlrSF: 0 ... 2065
GarageArea: 0 ... 1418
BsmtFinSF1: 0 ... 5644
Verkaufspreis: 34.900 ... 755.000
TotalBsmtSF: 0 ... 6110
1stFlrSF: 334 ... 4692
BsmtUnfSF: 0 ... 2336
GrLivArea: 334 ... 5642
LotArea: 1300 ... 215245
Vor diesem Hintergrund habe ich den folgenden
ValueNormalizer erstellt , der einige Informationen zu den Werten in der Spalte enthält und eine Funktion zurückgibt, die einen Wert (eine
Zeichenfolge ) in einen numerischen Merkmalsvektor für das neuronale Netzwerk (
double [] ) umwandelt:
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); } }
Jetzt haben wir die Daten in ein Format konvertiert, das für ein neuronales Netzwerk geeignet ist. Es ist Zeit, einen zu bauen.
Bauen Sie ein neuronales Netzwerk auf
Ab heute müssten Sie dafür einen Windows-Computer verwenden.Wenn Sie Python und TensorFlow 1.1x bereits installiert haben, brauchen Sie nur
<PackageReference Include="Gradient" Version="0.1.10-tech-preview3" />
in Ihrer modernen .csproj-Datei. Andernfalls lesen Sie das
Gradient-Handbuch , um die Ersteinrichtung durchzuführen.
Sobald das Paket betriebsbereit ist, können wir unser erstes flaches, tiefes Netzwerk erstellen.
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");
Dadurch wird ein untrainiertes neuronales Netzwerk mit 3 Neuronenschichten und einer Dropout-Schicht erstellt, um eine Überanpassung zu verhindern.
tf.nn.relu_fntf.nn.relu_fn ist die Aktivierungsfunktion für unsere Neuronen.
Es ist bekannt, dass
ReLU in tiefen Netzwerken gut funktioniert, da es das
Problem des
verschwindenden Gradienten löst: Ableitungen der ursprünglichen nichtlinearen Aktivierungsfunktionen neigten dazu, sehr klein zu werden, wenn sich der Fehler von der Ausgabeschicht in tiefen Netzwerken zurück ausbreitete. Das bedeutete, dass sich die Schichten, die näher am Eingang liegen, nur geringfügig anpassen würden, was das Training tiefer Netzwerke erheblich verlangsamte.
AussteigerDropout ist eine Schicht mit speziellen Funktionen in neuronalen Netzen, die tatsächlich keine Neuronen als solche enthält. Stattdessen wird jede einzelne Eingabe übernommen und bei der Selbstausgabe zufällig durch 0 ersetzt (andernfalls wird nur der ursprüngliche Wert weitergegeben). Auf diese Weise wird verhindert, dass in einem kleinen Datensatz eine
Überanpassung an weniger relevante Features erfolgt. Wenn wir beispielsweise die
ID- Spalte nicht entfernt hätten, hätte das Netzwerk möglicherweise die <Id> -> <SalePrice> -Zuordnung genau gespeichert, was uns eine 100% ige Genauigkeit des Trainingssatzes, aber völlig unabhängige Zahlen für andere Daten ergeben würde.
Warum brauchen wir einen Aussteiger? Unsere Trainingsdaten enthalten nur ~ 1500 Beispiele, und dieses winzige neuronale Netzwerk, das wir aufgebaut haben, hat> 1800 einstellbare Gewichte. Wenn es sich um ein einfaches Polynom handelt, könnte es mit der Preisfunktion übereinstimmen, die wir genau approximieren möchten. Aber dann hätte es enorme Werte für Eingaben außerhalb des ursprünglichen Trainingssatzes.
Füttere die Daten
TensorFlow erwartet seine Daten entweder in NumPy-Arrays oder in vorhandenen Tensoren. Ich konvertiere DataRows in NumPy-Arrays:
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);
Im obigen Code konvertieren wir jede
DataRow in ein
ndarray, indem wir jede Zelle darin nehmen und den
ValueNormalizer anwenden, der ihrer Spalte entspricht. Dann
fügen wir alle Zeilen in ein anderes
ndarray ein und erhalten ein Array von Arrays.
Für Ausgaben, bei denen wir nur
Zugwerte in ein anderes
ndarray konvertieren, ist keine solche Transformation erforderlich.
Zeit, den Gradienten hinunterzukommen
Bei diesem Setup müssen wir lediglich die
Anpassungsfunktion des Modells aufrufen, um unser Netzwerk zu trainieren:
model.fit(trainInputs, trainOutputs, epochs: 2000, validation_split: 0.075, verbose: 2);
Dieser Aufruf legt tatsächlich die letzten 7,5% des Trainingssatzes für die Validierung beiseite und wiederholt dann die folgenden 2000 Male:
- Teilen Sie den Rest von trainInputs in Stapel auf
- Diese Chargen einzeln in das neuronale Netzwerk einspeisen
- Berechnen Sie den Fehler mit der oben definierten Verlustfunktion
- Backpropagieren Sie den Fehler durch die Gradienten einzelner Neuronenverbindungen und passen Sie die Gewichte an
Während des Trainings wird der Netzwerkfehler für die zur Validierung
reservierten Daten als
val_loss und der Fehler für die Trainingsdaten selbst als
Verlust ausgegeben . Wenn
val_loss viel größer als der
Verlust wird , bedeutet dies im Allgemeinen, dass das Netzwerk überanpasst. Ich werde darauf in den folgenden Artikeln näher eingehen.
Wenn Sie alles richtig gemacht haben, sollte eine
Quadratwurzel eines Ihrer Verluste in der Größenordnung von 20000 liegen.

Einreichung
Ich werde nicht viel über das Generieren der Datei sprechen, die hier eingereicht werden soll. Der Code zum Berechnen von Ausgaben ist einfach:
const string SubmissionInputFile = "test.csv"; DataTable submissionData = LoadData(SubmissionInputFile); var submissionRows = submissionData.Rows.Cast<DataRow>(); ndarray submissionInputs = GetInputs(submissionRows); ndarray sumissionOutputs = model.predict(submissionInputs);
die meistens Funktionen verwendet, die zuvor definiert wurden.
Dann müssen Sie sie in eine CSV-Datei schreiben, die einfach eine Liste von ID- und Predicted_Wert-Paaren ist.
Wenn Sie Ihr Ergebnis einreichen, sollten Sie eine Punktzahl in der Größenordnung von 0,17 erhalten, die sich irgendwo im letzten Viertel der öffentlichen Rangliste befindet. Aber hey, wenn es so einfach wäre wie ein 3-Schicht-Netzwerk mit 27 Neuronen, würden diese lästigen Datenwissenschaftler von den großen US-Unternehmen keine Gesamtvergütung von über 300.000 USD pro Jahr erhalten
Einpacken
Der vollständige Quellcode für diesen Eintrag (mit allen Helfern und einigen
auskommentierten Teilen meiner früheren Erkundungen und Experimente) enthält ungefähr 200 Zeilen im
PasteBin .
Im nächsten Artikel werden Sie sehen, wie meine Spielereien versuchen, in die Top 50% dieser öffentlichen Rangliste zu gelangen. Es wird ein Abenteuer für Amateurgesellen, ein Kampf mit The Windmill of Overfitting mit dem einzigen Werkzeug, das der Wanderer hat - einem größeren Modell (z. B. Deep NN, denken Sie daran, kein manuelles Feature-Engineering!). Es wird weniger ein Coding-Tutorial sein, sondern eher eine Gedankensuche mit wirklich krummer Mathematik und einer seltsamen Schlussfolgerung. Bleib dran!
Links
KaggleHauspreiswettbewerb auf KaggleTensorFlow-Regressions-TutorialTensorFlow-HomepageTensorFlow API-ReferenzGradient (TensorFlow-Bindung)