Im Rah­men der in den bei­den vor­he­ri­gen Tei­len bereits prä­sen­tier­ten deskrip­ti­ven Ana­ly­sen zum Trai­ning-Daten­satz der Kaggle-Com­pe­ti­tion ASHRAE III wer­den wir nun einen Blick auf einen Ler­ner wer­fen, der den Ein­fluss der im Daten­satz ent­hal­te­nen Mess­grö­ßen auf den Ener­gie­be­darf eines Gebäu­des sinn­voll abschät­zen kann. Wir wer­den uns dabei auf einen Über­blick beschrän­ken und den Ler­ner und seine Funk­ti­ons­weise im Kon­text der Com­pe­ti­tion als Bei­spiel betrachten.

Zunächst noch ein­mal die vor­lie­gende Situa­tion: Wir haben einen Daten­satz mit einer quan­ti­ta­tiv mess­ba­ren Ziel­größe, dem meter_reading und eine Ansamm­lung an mög­li­chen Fea­tures, von zeit­li­chen Grö­ßen wie dem Alter des Gebäu­des über Wet­ter­da­ten bis hin zu wei­te­ren Infor­ma­tio­nen über das Gebäude wie die Anzahl Stock­werke. Im letz­ten Teil haben wir unter Anwen­dung unter­schied­li­cher Metho­den aus der deskrip­ti­ven Sta­tis­tik diese Menge auf die drei Mess­grö­ßen dew_temperature, air_temperature und age begrenzt. Bei all die­sen Merk­ma­len han­delt es sich eben­falls um quan­ti­ta­tive Grö­ßen. Somit kann der im Machine Lear­ning für sol­che Sze­na­rien häu­fig gewählte Ansatz des Gra­di­ent Boos­ting hier ver­ein­facht ange­wen­det werden.

Die Grund­idee ist hier­bei die Mini­mie­rung einer Ver­lust­funk­tion. In die­ser Kaggle-Com­pe­ti­tion ist diese Funk­tion durch den RMSLE (root mean squared log­arith­mic error) gegeben.

Abbil­dung 1 Aus­zug aus der Projektbeschreibung

Zum bes­se­ren Ver­ständ­nis hier nun ein wenig Hin­ter­grund­wis­sen zu Ler­nern aus der Kate­go­rie des Gra­di­ent Boosting:

Mathe­ma­ti­scher Hintergrund

Zur Bestim­mung einer mög­lichst gut pas­sen­den Abschät­zung der Mess­werte einer Ziel­größe wird grund­le­gend ange­nom­men, dass dies über eine Funk­tion mit einem schritt­wei­sen Update mög­lich ist.

Betrach­tet man den uni­va­ria­ten Fall, also den Fall einer Ein­fluss­größe mit einer Dimen­sion, so lässt sich dies über die Annahme einer gemisch­ten Wahr­schein­lich­keits­ver­tei­lung für die Ein­fluss­größe rea­li­sie­ren. Dabei wird eine Appro­xi­ma­tion für die Funk­tion zur Mini­mie­rung des Erwar­tungs­wer­tes der Ver­lust­funk­tion gewählt. Rea­li­siert wird dies hier über eine gewich­tete Summe an Funk­tio­nen aus einer Klasse an schwa­chen Ler­nern. Die abschät­zende Funk­tion ist dabei als Line­ar­kom­bi­na­tion dar­stell­bar. Der Algo­rith­mus sucht so schritt­weise nach dem Mini­mum einer sol­chen Linearkombination.

Der erste Schritt ist dabei die Initia­li­sie­rung des Algo­rith­mus mit einem vor­her bestimm­ten Set an Fak­to­ren. Diese Start­werte der Fak­to­ren gene­rie­ren sich aus der Bestim­mung des aus die­sen resul­tie­ren­den Mini­mums für die Ver­lust­funk­tion bei Ver­rech­nung mit den vor­lie­gen­den Wer­ten der Ziel­größe und der Ein­fluss­grö­ßen. Hier­aus resul­tiert eine initiale Ver­sion der abschät­zen­den Funk­tion des Ler­ners. In jedem wei­te­ren Schritt wer­den Pseudo-Resi­duen über den Gra­di­en­ten der Ver­lust­funk­tion mit der letz­ten Ver­sion der abschät­zen­den Funk­tion bestimmt. Basie­rend auf den so bestimm­ten Gra­di­en­ten kann eine Anpas­sung der schwa­chen Ler­ner gesche­hen, über den Stee­pest Des­cent wird mit jedem Update je eine neue Appro­xi­ma­tion bestimmt. Für die Rea­li­sie­run­gen die­ser neu berech­ne­ten Funk­tion über die Ver­lust­funk­tion ergibt sich dann die Anpas­sungs­güte. Wel­che schwa­chen Ler­ner dabei zum Ein­satz kom­men, ist von der Imple­men­tie­rung abhän­gig. Mög­lich sind in die­sem Zusam­men­hang lineare Funk­tio­nen, P‑Splines oder auf Ent­schei­dungs­bäu­men basie­rende Lerner. 

Über­tra­gung auf das ASHRAE-Projekt

Inter­es­sant ist im vor­lie­gen­den Fall­bei­spiel die Beach­tung der in den letz­ten bei­den Tei­len die­ser Blog­se­rie fest­ge­stell­ten spe­zi­fi­schen Eigen­hei­ten des Trai­nings-Daten­sat­zes. So lie­gen ver­mehrt Beob­ach­tun­gen mit grö­ße­rem Ein­fluss auf eine Abschät­zung des meter_reading vor. Diese sind sehr ungleich über den gesam­ten Daten­satz an Mes­sun­gen aus einem Jahr über die ver­füg­ba­ren Gebäude anhand ihrer building_id und die damit ver­bun­de­nen Wet­ter­sta­tio­nen über ihre site_id ver­teilt. Somit lie­fert eine dif­fe­ren­zierte Betrach­tung der Daten eine zuver­läs­si­gere Appro­xi­ma­tion der Mess­werte für die Größe meter_reading. 

Für den hier beschrie­be­nen Algo­rith­mus beschrän­ken wir uns im Fol­gen­den auf die über jeden Tag gemit­tel­ten Mess­werte für site_id 2.  Im letz­ten Teil hat­ten wir bereits ver­ein­zelte Gebäude mit unver­hält­nis­mä­ßig hohen Mess­wer­ten aus unse­rer Stich­probe aus­sor­tiert und fest­ge­stellt, dass ohne diese Gebäude der Zusam­men­hang zwi­schen meter_reading und den drei Ein­fluss­grö­ßen air_temperature, dew_temperature und age merk­bar grö­ßer wird. Um also fest­zu­stel­len, inwie­weit unsere Ana­ly­sen im letz­ten Teil zu Ver­bes­se­run­gen hin­sicht­lich der Anpas­sungs­güte eines Ler­ners füh­ren könn­ten, wer­den wir die Ergeb­nisse des Ler­ners für die­sen modi­fi­zier­ten Daten­satz den Ergeb­nis­sen für den nicht modi­fi­zier­ten Daten­satz mit allen site_id-Ein­trä­gen und ver­füg­ba­ren Ein­fluss­grö­ßen gegenüberstellen.

Der LightGBM-Ler­ner 

Tat­säch­lich ver­wen­det haben wir an die­ser Stelle den soge­nann­ten LightGBM als Ler­ner. Ein guter Ein­stiegs­punkt in die Funk­ti­ons­weise die­ses Ler­ners stellt der Arti­kel von Ke et al. dar. Die­ser Ler­ner basiert auf dem hier beschrie­be­nen Ansatz des Gra­di­ent Boos­ting und führt wei­tere Fea­tures ein. Die hier ver­wen­dete Imple­men­tie­rung fin­det sich im Python-Paket lightgbm

Cha­rak­te­ris­tisch für die­sen Ler­ner ist die Ver­wen­dung zweier Features: 

  • GOSS (Gra­di­ent-based One-Side Sam­pling): Aus­las­sen von Daten­punk­ten inner­halb der Stich­probe bei mess­bar zu gerin­gem Ein­fluss auf die Anpas­sungs­güte, hier­bei über einen unter einer gewis­sen Tole­ranz­grenze lie­gen­den Gra­di­en­ten für diese Beob­ach­tun­gen inner­halb eines Updateschrittes 
  • EFB (Exclu­sive Fea­ture Bund­ling): Zusam­men­fas­sen von Ein­fluss­grö­ßen mit exklu­si­ven Wer­ten (sehr ein­fa­ches Bei­spiel: zwei nume­ri­sche Ein­fluss­grö­ßen sind nie gleich­zei­tig 0, viel­mehr fin­det sich ein exklu­si­ver Zusam­men­hang wie Linea­ri­tät), z.B. über lineare Trans­for­ma­tio­nen oder das Aus­las­sen von Features

Ke et al. stel­len dabei über Simu­la­tio­nen her­aus, dass die­ser Ler­ner für den glei­chen Trai­nings­da­ten­satz im Schnitt 20-mal schnel­ler durch­läuft als ein Gra­di­ent Boos­ting ohne diese bei­den Fea­tures bei einer ver­gleich­ba­ren Genau­ig­keit. Die bei­den auf­ge­führ­ten Fea­tures pas­sen dabei zum ASHRAE-Daten­satz, da die­ser auf­grund sei­ner bereits fest­ge­stell­ten Män­gel hin­sicht­lich ungleich­mä­ßig ver­teil­ter Mess­werte und vie­len feh­len­den Wer­ten ins­be­son­dere durch das Fea­ture GOSS berei­nigt wer­den kann und so eine merk­bare Ver­bes­se­rung der Anpas­sungs­güte erreicht wer­den kann. 

Dies zeigt sich auch in der prak­ti­schen Anwen­dung. Alle nach­fol­gen­den Ana­ly­sen wur­den dabei anhand des Daten­sat­zes für den meter_type Chil­led Water durch­ge­führt. Hierzu nun der rele­vante Teil des Python-Codes:

Laden der not­wen­di­gen 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

Anwen­dung des Ler­ners auf den modi­fi­zier­ten 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)

Anwen­dung des Ler­ners auf den gesam­ten 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 ver­mer­ken sind hier noch die fol­gen­den aus­ge­wähl­ten Fea­tures, für die Pseudo-Resi­duen wer­den die Werte im Zäh­ler und Nen­ner von der log­arith­mier­ten Skala über ent­spre­chende Funk­tio­nen zurückgerechnet: 

  • Auf­tei­lung des Daten­sat­zes in 5 Teil­da­ten­sätze für eine Kreuzvalidierung 
  • boosting_type: GBDT (Gra­di­ent Boos­ting Decis­ion Tree) – grund­le­gen­der Algo­rith­mus, Abwand­lung des Gra­di­ent Boos­ting , die­ser zeigte sich in ers­ten Durch­läu­fen im direk­ten Ver­gleich am Effektivsten
  • objec­tive: binary für den Zäh­ler und regres­sion für den Nen­ner der Approximation 
  • metric: binary_logloss für den Zäh­ler und rmse für den Nen­ner – durch Log­arith­mie­rung der Ziel­va­ria­ble im ers­ten Schritt gleich­wer­tig zu RMSLE, siehe Code
  • learning_rate: Fak­tor zur Steue­rung des Ein­flus­ses des Updateschrittes
  • feature_fraction: Anteil der aus­zu­wäh­len­den Fea­tures pro Schritt zwi­schen 0 und 1, siehe hier.
  • bagging_fraction: Äqui­va­lent zur feature_fraction für das Fea­ture GOSS, Anteil der Stich­probe, zwi­schen 0 und 1 
  • bagging_freq: alle wie­viel Ite­ra­tio­nen soll das Bag­ging für GOSS durch­ge­führt werden 

Hier­bei wer­den fol­gende Ergeb­nisse gelie­fert: Der Ler­ner braucht für den modi­fi­zier­ten Daten­satz mit 27375 Daten­punk­ten 80 Ite­ra­tio­nen und erreicht bei Ite­ra­tion 68 sein bes­tes Ergeb­nis für den Binary Log-Loss bei der Aus­wahl der Fea­tures und benö­tigt dann 20 Ite­ra­tio­nen, um bei Ite­ra­tion 13 sein Opti­mum zu finden.

Hier der Output:

Abbil­dung 2 Out­put des Ler­ners für den modi­fi­zer­ten Datensatz

Für den gesam­ten Daten­satz mit 4182440 ver­füg­ba­ren Daten­punk­ten wer­den weit­aus mehr Ite­ra­tio­nen benö­tigt. Alle Kenn­grö­ßen, sowohl der Binary Log-Loss für die Fea­tures als auch der RMSE und der sich dar­aus erge­bende Wert für den RMSLE lie­gen merk­bar ober­halb der Werte aus Abbil­dung 1. Der Ler­ner läuft dabei jeweils solange, bis für 20 Schritte keine merk­bare Ver­bes­se­rung erreicht wird. “Merk­bar” äußert sich hier­bei durch die Nicht-Über­schrei­tung einer gewis­sen Tole­ranz­grenze, wie bereits beschrieben.

Datenanalyse und Machine Learning anhand eines Use Cases Bild3
Abbil­dung 3 Out­put des Ler­ners für den gesam­ten Datensatz

Fazit

Aus die­sen Ergeb­nis­sen ist abschlie­ßend erkenn­bar: Der Ler­ner LightGBM kann für Daten­sätze mit unvoll­stän­di­gen, ungleich­mä­ßig ver­teil­ten Mess­wer­ten wie den vor­lie­gen­den ASHRAE-Trai­nings­da­ten­satz ver­wen­det wer­den. In die­sem Blog­bei­trag wurde dies anhand eines Bei­spiels für einen modi­fi­zier­ten Daten­satz auf­ge­zeigt. Der Algo­rith­mus LGBM bie­tet jedoch noch weit­aus mehr Mög­lich­kei­ten und ist dabei selbst nur einer von einer gan­zen Palette an hier ein­setz­ba­ren Ler­ner, um einen Zusam­men­hang in den Daten erkenn­bar zu machen. Ohne die deskrip­ti­ven Ana­ly­sen aus dem vor­he­ri­gen Teil wären diese Ver­bes­se­run­gen aber auch nicht erreich­bar gewe­sen. Es ist also durch­aus mit viel Auf­wand ver­bun­den, einen Daten­satz aus der Rea­li­tät sinn­voll aus­zu­wer­ten und merk­bare Kor­re­la­tio­nen für die Schät­zung einer Ziel­größe zu erken­nen. Dies hat dann auch Aus­wir­kun­gen auf den ent­spre­chen­den Ler­ner. Zu beach­ten ist trotz der Opti­mie­rung des Ergeb­nis­ses für den Ler­ner das Pro­blem des Over­fit­tings. Die­ses ist an den gemes­se­nen Kenn­grö­ßen für die Fea­tures und dem dar­aus resul­tie­ren­den Fit erkenn­bar. Hier muss also eine gewisse Balance an Daten­be­rei­ni­gung und ver­blei­ben­dem Infor­ma­ti­ons­ge­halt gewahrt werden.