Im Rahmen der in den beiden vorherigen Teilen bereits präsentierten deskriptiven Analysen zum Training-Datensatz der Kaggle-Competition ASHRAE III werden wir nun einen Blick auf einen Lerner werfen, der den Einfluss der im Datensatz enthaltenen Messgrößen auf den Energiebedarf eines Gebäudes sinnvoll abschätzen kann. Wir werden uns dabei auf einen Überblick beschränken und den Lerner und seine Funktionsweise im Kontext der Competition als Beispiel betrachten.
Zunächst noch einmal die vorliegende Situation: Wir haben einen Datensatz mit einer quantitativ messbaren Zielgröße, dem meter_reading und eine Ansammlung an möglichen Features, von zeitlichen Größen wie dem Alter des Gebäudes über Wetterdaten bis hin zu weiteren Informationen über das Gebäude wie die Anzahl Stockwerke. Im letzten Teil haben wir unter Anwendung unterschiedlicher Methoden aus der deskriptiven Statistik diese Menge auf die drei Messgrößen dew_temperature, air_temperature und age begrenzt. Bei all diesen Merkmalen handelt es sich ebenfalls um quantitative Größen. Somit kann der im Machine Learning für solche Szenarien häufig gewählte Ansatz des Gradient Boosting hier vereinfacht angewendet werden.
Die Grundidee ist hierbei die Minimierung einer Verlustfunktion. In dieser Kaggle-Competition ist diese Funktion durch den RMSLE (root mean squared logarithmic error) gegeben.
Zum besseren Verständnis hier nun ein wenig Hintergrundwissen zu Lernern aus der Kategorie des Gradient Boosting:
Mathematischer Hintergrund
Zur Bestimmung einer möglichst gut passenden Abschätzung der Messwerte einer Zielgröße wird grundlegend angenommen, dass dies über eine Funktion mit einem schrittweisen Update möglich ist.
Betrachtet man den univariaten Fall, also den Fall einer Einflussgröße mit einer Dimension, so lässt sich dies über die Annahme einer gemischten Wahrscheinlichkeitsverteilung für die Einflussgröße realisieren. Dabei wird eine Approximation für die Funktion zur Minimierung des Erwartungswertes der Verlustfunktion gewählt. Realisiert wird dies hier über eine gewichtete Summe an Funktionen aus einer Klasse an schwachen Lernern. Die abschätzende Funktion ist dabei als Linearkombination darstellbar. Der Algorithmus sucht so schrittweise nach dem Minimum einer solchen Linearkombination.
Der erste Schritt ist dabei die Initialisierung des Algorithmus mit einem vorher bestimmten Set an Faktoren. Diese Startwerte der Faktoren generieren sich aus der Bestimmung des aus diesen resultierenden Minimums für die Verlustfunktion bei Verrechnung mit den vorliegenden Werten der Zielgröße und der Einflussgrößen. Hieraus resultiert eine initiale Version der abschätzenden Funktion des Lerners. In jedem weiteren Schritt werden Pseudo-Residuen über den Gradienten der Verlustfunktion mit der letzten Version der abschätzenden Funktion bestimmt. Basierend auf den so bestimmten Gradienten kann eine Anpassung der schwachen Lerner geschehen, über den Steepest Descent wird mit jedem Update je eine neue Approximation bestimmt. Für die Realisierungen dieser neu berechneten Funktion über die Verlustfunktion ergibt sich dann die Anpassungsgüte. Welche schwachen Lerner dabei zum Einsatz kommen, ist von der Implementierung abhängig. Möglich sind in diesem Zusammenhang lineare Funktionen, P‑Splines oder auf Entscheidungsbäumen basierende Lerner.
Übertragung auf das ASHRAE-Projekt
Interessant ist im vorliegenden Fallbeispiel die Beachtung der in den letzten beiden Teilen dieser Blogserie festgestellten spezifischen Eigenheiten des Trainings-Datensatzes. So liegen vermehrt Beobachtungen mit größerem Einfluss auf eine Abschätzung des meter_reading vor. Diese sind sehr ungleich über den gesamten Datensatz an Messungen aus einem Jahr über die verfügbaren Gebäude anhand ihrer building_id und die damit verbundenen Wetterstationen über ihre site_id verteilt. Somit liefert eine differenzierte Betrachtung der Daten eine zuverlässigere Approximation der Messwerte für die Größe meter_reading.
Für den hier beschriebenen Algorithmus beschränken wir uns im Folgenden auf die über jeden Tag gemittelten Messwerte für site_id 2. Im letzten Teil hatten wir bereits vereinzelte Gebäude mit unverhältnismäßig hohen Messwerten aus unserer Stichprobe aussortiert und festgestellt, dass ohne diese Gebäude der Zusammenhang zwischen meter_reading und den drei Einflussgrößen air_temperature, dew_temperature und age merkbar größer wird. Um also festzustellen, inwieweit unsere Analysen im letzten Teil zu Verbesserungen hinsichtlich der Anpassungsgüte eines Lerners führen könnten, werden wir die Ergebnisse des Lerners für diesen modifizierten Datensatz den Ergebnissen für den nicht modifizierten Datensatz mit allen site_id-Einträgen und verfügbaren Einflussgrößen gegenüberstellen.
Der LightGBM-Lerner
Tatsächlich verwendet haben wir an dieser Stelle den sogenannten LightGBM als Lerner. Ein guter Einstiegspunkt in die Funktionsweise dieses Lerners stellt der Artikel von Ke et al. dar. Dieser Lerner basiert auf dem hier beschriebenen Ansatz des Gradient Boosting und führt weitere Features ein. Die hier verwendete Implementierung findet sich im Python-Paket lightgbm.
Charakteristisch für diesen Lerner ist die Verwendung zweier Features:
- GOSS (Gradient-based One-Side Sampling): Auslassen von Datenpunkten innerhalb der Stichprobe bei messbar zu geringem Einfluss auf die Anpassungsgüte, hierbei über einen unter einer gewissen Toleranzgrenze liegenden Gradienten für diese Beobachtungen innerhalb eines Updateschrittes
- EFB (Exclusive Feature Bundling): Zusammenfassen von Einflussgrößen mit exklusiven Werten (sehr einfaches Beispiel: zwei numerische Einflussgrößen sind nie gleichzeitig 0, vielmehr findet sich ein exklusiver Zusammenhang wie Linearität), z.B. über lineare Transformationen oder das Auslassen von Features
Ke et al. stellen dabei über Simulationen heraus, dass dieser Lerner für den gleichen Trainingsdatensatz im Schnitt 20-mal schneller durchläuft als ein Gradient Boosting ohne diese beiden Features bei einer vergleichbaren Genauigkeit. Die beiden aufgeführten Features passen dabei zum ASHRAE-Datensatz, da dieser aufgrund seiner bereits festgestellten Mängel hinsichtlich ungleichmäßig verteilter Messwerte und vielen fehlenden Werten insbesondere durch das Feature GOSS bereinigt werden kann und so eine merkbare Verbesserung der Anpassungsgüte erreicht werden kann.
Dies zeigt sich auch in der praktischen Anwendung. Alle nachfolgenden Analysen wurden dabei anhand des Datensatzes für den meter_type Chilled Water durchgeführt. Hierzu nun der relevante Teil des Python-Codes:
Laden der notwendigen Pakete
# Import der relevanten Pakete
import boto3
import os
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import sys
from io import StringIO
# Voreinstellungen für einen Learner - hier LGBM mit MSLE
# Import weiterer Pakete
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error
import lightgbm as lgbm
import graphviz as graphviz
Anwendung des Lerners auf den modifizierten Datensatz
num_folds = 5 # Anzahl Teildatensätze für Kreuzvalidierung
kf = KFold(n_splits = num_folds, shuffle = False, random_state = 42)
error = 0
models = []
for i, (train_index, val_index) in enumerate(kf.split(features)):
if i + 1 < num_folds:
continue
print(train_index.max(), val_index.min())
train_X = features.iloc[train_index]
val_X = features.iloc[val_index]
train_y = target.iloc[train_index]
val_y = target.iloc[val_index]
# Initialisierung des Datensatzes
lgb_train = lgbm.Dataset(train_X, train_y > 0)
lgb_eval = lgbm.Dataset(val_X, val_y > 0)
# Dictionary mit den gewählten Parametern
params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': {'binary_logloss'},
'learning_rate': 0.1,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq' : 5
}
gbm_class = lgbm.train(params,
lgb_train,
num_boost_round = 2000,
valid_sets = (lgb_train, lgb_eval),
early_stopping_rounds = 20,
verbose_eval = 20)
# Aufteilung in einen Datensatz mit den Features und den zu evaluierenden
Variablen
lgb_train = lgbm.Dataset(train_X[train_y > 0], train_y[train_y > 0])
lgb_eval = lgbm.Dataset(val_X[val_y > 0] , val_y[val_y > 0])
# Parameter-Einstellungen
params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': {'rmse'},
'learning_rate': 0.5,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq' : 5
}
# Eigentlicher Trainingsvorgang
gbm_regress = lgbm.train(params,
lgb_train,
num_boost_round = 2000,
valid_sets = (lgb_train, lgb_eval),
early_stopping_rounds = 20,
verbose_eval = 20)
y_pred = (gbm_class.predict(val_X, num_iteration =
gbm_class.best_iteration) > .5) *\
(gbm_regress.predict(val_X, num_iteration =
gbm_regress.best_iteration))
error += np.sqrt(mean_squared_error(y_pred, (val_y))) / num_folds
print(np.sqrt(mean_squared_error(y_pred, (val_y))))
break
print(error)
Anwendung des Lerners auf den gesamten Datensatz
num_folds = 5 # Anzahl
kf = KFold(n_splits = num_folds, shuffle = False, random_state = 42)
error = 0
models = []
for i, (train_index, val_index) in enumerate(kf.split(all_features)):
if i + 1 < num_folds:
continue
print(train_index.max(), val_index.min())
train_X_all = all_features.iloc[train_index]
val_X_all = all_features.iloc[val_index]
train_y_all = target_alldata.iloc[train_index]
val_y_all = target_alldata.iloc[val_index]
# Initialisierung des Datensatzes
lgb_train_all = lgbm.Dataset(train_X_all, train_y_all > 0)
lgb_eval_all = lgbm.Dataset(val_X_all, val_y_all > 0)
# Dictionary mit den gewählten Parametern
params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': {'binary_logloss'},
'learning_rate': 0.1,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq' : 5
}
# Training und Wahl des Learners
gbm_class_all = lgbm.train(params,
lgb_train_all,
num_boost_round = 2000,
valid_sets = (lgb_train_all, lgb_eval_all),
early_stopping_rounds = 20,
verbose_eval = 20)
# Aufteilung in einen Datensatz mit den Features und den zu evaluierenden
Variablen
lgb_train_all = lgbm.Dataset(train_X_all[train_y_all > 0],
train_y_all[train_y_all > 0])
lgb_eval_all = lgbm.Dataset(val_X_all[val_y_all > 0] ,
val_y_all[val_y_all > 0])
# Parameter-Einstellungen
params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': {'rmse'},
'learning_rate': 0.5,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq' : 5
}
# Eigentlicher Trainingsvorgang unter Trennung von Features und zu
evaluierenden Variablen
gbm_regress_all = lgbm.train(params,
lgb_train_all,
num_boost_round = 2000,
valid_sets = (lgb_train_all, lgb_eval_all),
early_stopping_rounds=20,
verbose_eval = 20)
y_pred_all = (gbm_class_all.predict(val_X_all, num_iteration =
gbm_class_all.best_iteration) > .5) *\
(gbm_regress_all.predict(val_X_all, num_iteration =
gbm_regress_all.best_iteration))
error += np.sqrt(mean_squared_error(y_pred_all, (val_y_all))) / num_folds
print(np.sqrt(mean_squared_error(y_pred_all, (val_y_all))))
break
# Ausgabe des RMSE
print(error)
Zu vermerken sind hier noch die folgenden ausgewählten Features, für die Pseudo-Residuen werden die Werte im Zähler und Nenner von der logarithmierten Skala über entsprechende Funktionen zurückgerechnet:
- Aufteilung des Datensatzes in 5 Teildatensätze für eine Kreuzvalidierung
- boosting_type: GBDT (Gradient Boosting Decision Tree) – grundlegender Algorithmus, Abwandlung des Gradient Boosting , dieser zeigte sich in ersten Durchläufen im direkten Vergleich am Effektivsten
- objective: binary für den Zähler und regression für den Nenner der Approximation
- metric: binary_logloss für den Zähler und rmse für den Nenner – durch Logarithmierung der Zielvariable im ersten Schritt gleichwertig zu RMSLE, siehe Code
- learning_rate: Faktor zur Steuerung des Einflusses des Updateschrittes
- feature_fraction: Anteil der auszuwählenden Features pro Schritt zwischen 0 und 1, siehe hier.
- bagging_fraction: Äquivalent zur feature_fraction für das Feature GOSS, Anteil der Stichprobe, zwischen 0 und 1
- bagging_freq: alle wieviel Iterationen soll das Bagging für GOSS durchgeführt werden
Hierbei werden folgende Ergebnisse geliefert: Der Lerner braucht für den modifizierten Datensatz mit 27375 Datenpunkten 80 Iterationen und erreicht bei Iteration 68 sein bestes Ergebnis für den Binary Log-Loss bei der Auswahl der Features und benötigt dann 20 Iterationen, um bei Iteration 13 sein Optimum zu finden.
Hier der Output:
Für den gesamten Datensatz mit 4182440 verfügbaren Datenpunkten werden weitaus mehr Iterationen benötigt. Alle Kenngrößen, sowohl der Binary Log-Loss für die Features als auch der RMSE und der sich daraus ergebende Wert für den RMSLE liegen merkbar oberhalb der Werte aus Abbildung 1. Der Lerner läuft dabei jeweils solange, bis für 20 Schritte keine merkbare Verbesserung erreicht wird. “Merkbar” äußert sich hierbei durch die Nicht-Überschreitung einer gewissen Toleranzgrenze, wie bereits beschrieben.
Fazit
Aus diesen Ergebnissen ist abschließend erkennbar: Der Lerner LightGBM kann für Datensätze mit unvollständigen, ungleichmäßig verteilten Messwerten wie den vorliegenden ASHRAE-Trainingsdatensatz verwendet werden. In diesem Blogbeitrag wurde dies anhand eines Beispiels für einen modifizierten Datensatz aufgezeigt. Der Algorithmus LGBM bietet jedoch noch weitaus mehr Möglichkeiten und ist dabei selbst nur einer von einer ganzen Palette an hier einsetzbaren Lerner, um einen Zusammenhang in den Daten erkennbar zu machen. Ohne die deskriptiven Analysen aus dem vorherigen Teil wären diese Verbesserungen aber auch nicht erreichbar gewesen. Es ist also durchaus mit viel Aufwand verbunden, einen Datensatz aus der Realität sinnvoll auszuwerten und merkbare Korrelationen für die Schätzung einer Zielgröße zu erkennen. Dies hat dann auch Auswirkungen auf den entsprechenden Lerner. Zu beachten ist trotz der Optimierung des Ergebnisses für den Lerner das Problem des Overfittings. Dieses ist an den gemessenen Kenngrößen für die Features und dem daraus resultierenden Fit erkennbar. Hier muss also eine gewisse Balance an Datenbereinigung und verbleibendem Informationsgehalt gewahrt werden.