66  Data splitting

Letzte Änderung am 23. March 2024 um 21:46:15

Ein Kapitel nur über Daten? Haben wir nicht schon ein Kapitel über Daten und deren Verteilung? Hier soll es aber nicht um Daten und deren Verteilungen gehen. In diesem Kapitel beschäftigen wir uns mit der Idee der Trainings-, Test- und Validierungsdaten. Wir brauchen alle drei Arten von Daten um zum einen auf den Trainingsdaten unsere Modelle zu trainieren und dann am Ende auf den Testdaten unsere Klassifikation zu überprüfen. Dazwischen nutzen wir unsere Validierungsdaten für die Optimierung oder das Tuning des Modells. Der erste Schritt ist aber immer sich einen Traingsdaten und einen Testdatensatz zu erschaffen. Wie wir das machen, wollen wir uns hier in diesem Kapitel einmal von vielen Seiten annähern. Wir nutzen im ganzen Kapitel das R Paket {resample}. Es gibt sicherlich noch andere Möglichkeiten sich die Daten zu teilen, aber das R Paket {resample} hat einige Vorteile, da das Paket auch im Universium der {tidymodels} angesiedelt ist.

66.1 Genutzte R Pakete

Wir wollen folgende R Pakete in diesem Kapitel nutzen.

pacman::p_load(tidyverse, tidymodels, magrittr, conflicted)
conflicts_prefer(magrittr::extract)

An der Seite des Kapitels findest du den Link Quellcode anzeigen, über den du Zugang zum gesamten R-Code dieses Kapitels erhältst.

66.2 Daten

In dieser Einführung nehmen wir die infizierten Ferkel als Beispiel um einmal die verschiedenen Verfahren zu demonstrieren. Ich füge hier noch die ID mit ein, die nichts anderes ist, als die Zeilennummer. Dann habe ich noch die ID an den Anfang gestellt. Auch brauchen wir nicht alle Spalten, da wir hier um die Zeilen und damit die Beobachtungen geht.

pig_tbl <- read_excel("data/infected_pigs.xlsx") |> 
  mutate(pig_id = 1:n()) |> 
  select(pig_id, infected, age:crp) |> 
  select(pig_id, infected, everything())  

In Tabelle 69.1 siehst du nochmal einen Auschnitt aus den Daten. Wir haben noch die ID mit eingefügt, damit wir einzelne Beobachtungen nachvollziehen können.

Tabelle 66.1— Auszug aus dem Daten zu den kranken Ferkeln.
pig_id infected age sex location activity crp
1 1 61 male northeast 15.31 22.38
2 1 53 male northwest 13.01 18.64
3 0 66 female northeast 11.31 18.76
4 1 59 female north 13.33 19.37
5 1 63 male northwest 14.71 21.57
6 1 55 male northwest 15.81 21.45
407 1 54 female north 11.82 21.5
408 0 56 male west 13.91 20.8
409 1 57 male northwest 12.49 21.95
410 1 61 male northwest 15.26 23.1
411 0 59 female north 13.13 20.23
412 1 63 female north 10.01 19.89

Gehen wir jetzt mal die verschiedenen Datensätze und Begrifflichkeiten, die wir für das maschinelle Lernen später brauchen, einmal durch.

66.3 Trainingsdatensatz und Testdatensatz

Um zu beginnen, teilen wir unseren einen Datensatz in zwei: einen Trainingssatz und einen Testsatz. Die meisten Zeilen und damit Beobachtungen des Originaldatensatzes werden im Trainingssatz sein. Wir nutzen die Trainingsdaten zum Anpassen des Modells. Wir trainieren das Modell auf den Daten des Trainingsdatensatzes. Wir messen dann das Modell auf den Testdatensatz. Warum machen wir das? Wenn wir auf dem Trainingsdatensatz auch die Modelgüte testen würden, dann könnten wir eine Überanpassung (eg. overfitting) auf die Trainingsdaten beobachten. Das Modell ist so gut an die spezifischen Trainingsdaten angepasst, dass es mit neuen Daten schwer umgehen kann.

Das R Paket {resample} stellt die Common Resampling Patterns nochmal da. Auch findest unter Resampling for Evaluating Performance noch eine Menge mehr Ideen für das Resampling.

Zu diesem Zweck können wir das R Paket {rsample} verwenden. Wir nutzen dann die Funktion initial_split() um die Daten in einen Trainingsdatensatz und einen Testdatensatz aufzuteilen. Dann müssen wir noch den Trainingsdatensatz und den Testdatensatz einmal getrennt in einem Objekt abspeichern.

pig_split <- initial_split(pig_tbl, prop = 3/4)

pig_split
<Training/Testing/Total>
<309/103/412>

Wie wir sehen, sehen wir gar nichts. Das ist auch so gewollt. Da wir im maschinellen Lernen gerne mal mit Datensätzen mit mehreren tausend Zeilen arbeiten würde es wenig helfen, wenn wir gleich alles auf der R Console ausgegeben kriegen. Die Information wie viel wir in den jeweiligen Gruppen haben, hilft schön genug.

train_pig_tbl <- training(pig_split)
test_pig_tbl <- testing(pig_split)

Nun haben wir die beiden Datensätze jeweils separat und können auf dem Trainingsdatensatz die jeweiligen Algorithmen bzw. Modelle trainieren.

Es ist schön, wenn wir Funktionen wie initial_split(), die für uns die Arbeit machen. Wir haben dann aber auch sehr schnell das Gefühl mit einer Black Box zu arbeiten. Man weiß gar nicht, was da eigentlich passiert ist. Deshalb hier nochmal der Code, den ich dann auch immer zur Demonstration nutze. Wenn wir eine ID Spalte haben, dann können wir auch über die Funktion sample_frac() und dem Anteil der ausgewählten Beobachtungen und der Funktion anti_join(), die Trainings- und Testdaten erstellen.

pig_train_tbl <- pig_tbl |> sample_frac(0.75)
pig_test_tbl <- anti_join(pig_tbl,
                          pig_train_tbl, by = 'pig_id')

Wir können dann auch überprüfen, ob wir die gleichen Anteile von den infizierten Ferkeln in den jeweiligen Datensätzen haben. Wir berechnen dafür einfach die relativen Anteile. Ein wenig komplizierter als nötig, aber hier geht es jetzt um die Veranschaulichung.

table(pig_train_tbl$infected)/sum(table(pig_train_tbl$infected))

        0         1 
0.3300971 0.6699029 
table(pig_test_tbl$infected)/sum(table(pig_test_tbl$infected))

        0         1 
0.3300971 0.6699029 

Du kannst die Generierung häufiger wiederholen und du wirst sehen, dass wir es mit einem Zufallsprozess zu tun haben. Mal sind die Anteile ähnlicher mal eher nicht. Das ist dann auch der Grund warum wir unsere Modelle tunen müssen und Modelle häufig wiederholt rechnen und die Ergebnisse dann zusammenfassen.

66.4 Validierungsdatensatz

Die finalen Modelle sollten nur einmal anhand ihres Testdatensatzes evaluieren werden. Das Überpfrüfen auf dem Testdatensatz geschieht nachdem die Optimierung und das Training der Modelle vollständig abgeschlossen ist. Was natürlich für uns nicht so schön ist, wir wollen ja auch zwischendurch mal schauen, ob wir auf dem richtigen Weg mit dem Training sind. Wir solle es auch sonst mit dem Tuning funktionieren? Deshalb ist möglich, zusätzliche Datensätze aus dem Trainingsprozess herauszuhalten, die zur mehrmaligen Evaluierung von Modellen verwendet werden können. Das machen wir dann solange bis wir bereit sind anhand des endgültigen Testsatzes zu evaluieren.

Diese zusätzlichen, aufgeteilten Datensätze werden oft als Validierungssätze bezeichnet und können in über die Funktion validation_split() erstellt werden.

val_pig_lst <- validation_split(pig_tbl, prop = 0.8)
Warning: `validation_split()` was deprecated in rsample 1.2.0.
ℹ Please use `initial_validation_split()` instead.
val_pig_lst
# Validation Set Split (0.8/0.2)  
# A tibble: 1 × 2
  splits           id        
  <list>           <chr>     
1 <split [329/83]> validation

In diesem Fall lassen wir den Validierungsdatensatz einmal so in der Liste stehen. Es ist faktisch wider ein Split der Daten, nur das wir jetzt auf diesem Datensatz unser Modell während des Tunings testen.

66.5 Kreuzvalidierung

Bei der Abstimmung von Hyperparametern und der Modellanpassung ist es oft nützlich, das Modell anhand von mehr als nur einem einzigen Validierungssatz zu bewerten, um eine stabilere Schätzung der Modellleistung zu erhalten. Wir meinen hier mit Hyperparametern die Optionen, die ein Algorithmus hat um diesen Algorithmus zu optimieren. Aus diesem Grund verwenden Modellierer häufig ein Verfahren, das als Kreuzvalidierung bekannt ist und bei dem die Daten mehrfach in Analyse- und Valisierungsdaten aufgeteilt werden.

Die vielleicht häufigste Methode der Kreuzvalidierung ist die \(V\)-fache Kreuzvalidierung. Bei dieser auch als \(k\)-fold cross-validation bezeichneten Methode werden \(V\) neue Stichproben bzw. Datensätze erstellt, indem die Daten in \(V\) Gruppen (auch folds genannt) von ungefähr gleicher Größe aufgeteilt werden. Der Analysesatz jeder erneuten Stichprobe besteht aus \(V-1\) Gruppen, wobei die verbleibende Gruppe als Validierungsdatensatz verwendet wird. Insgesamt führen wir dadurch dann den Algorithmus \(V\)-mal durch. Auf diese Weise wird jede Beobachtung in Daten in genau einem Beurteilungssatz verwendet.

In R können wir dafür die Funktion vfold_cv() nutzen. Im Folgenden einmal Split für \(V = 5\). Wir führen also eine \(5\)-fache Kreuzvalidierung durch.

vfold_cv(pig_tbl, v = 3)
#  3-fold cross-validation 
# A tibble: 3 × 2
  splits            id   
  <list>            <chr>
1 <split [274/138]> Fold1
2 <split [275/137]> Fold2
3 <split [275/137]> Fold3

Als ein Nachteil wird oft angesehen, dass die Kreuzvalidierung eine hohe Varianz in den Daten verursacht. Dagegen hilft dann die wiederholte Kreuzvalidierung (eng. repeated cross-validation). Wir bauen in jede Kreuzvalidierung nochmal eine oder mehr Wiederholungen ein. In unserem Fall dann drei Wiederholungen je Kreuzvalidierung \(V\).

vfold_cv(pig_tbl, v = 3, repeats = 2)
#  3-fold cross-validation repeated 2 times 
# A tibble: 6 × 3
  splits            id      id2  
  <list>            <chr>   <chr>
1 <split [274/138]> Repeat1 Fold1
2 <split [275/137]> Repeat1 Fold2
3 <split [275/137]> Repeat1 Fold3
4 <split [274/138]> Repeat2 Fold1
5 <split [275/137]> Repeat2 Fold2
6 <split [275/137]> Repeat2 Fold3

Wir sehen das der Split ungefähr immer gleich groß ist. Manchmal haben wir durch die Trennung eine Beobachtung mehr in dem Analysedatensatz mit \(n = 329\) oder \(n = 330\) Beobachtungen. Dementsprechend hat der Validierungsdatensatz einmal \(n = 82\) und einmal \(n = 83\) Beobachtungen.

66.6 Monte-Carlo Kreuzvalidierung

Wir haben als eine Alternative zur V-fachen Kreuzvalidierung die Monte-Carlo-Kreuzvalidierung vorliegen. Während bei der V-fachen Kreuzvalidierung jede Beobachtung in den Daten einem - und zwar genau einem - Validierungsdatensatz zugewiesen wird, wird bei der Monte-Carlo-Kreuzvalidierung für jeden Validierungsdatensatz eine zufällige Teilmenge der Daten ausgewählt, d. h. jede Beobachtung kann in 0, 1 oder vielen Validierungsdatensätzen verwendet werden. Der Analysesatz besteht dann aus allen Beobachtungen, die nicht ausgewählt wurden. Da jeder Validierungsdatensatz unabhängig ausgewählt wird, können wir diesen Vorgang so oft wie gewünscht wiederholen. Das stimt natürlich nur bedingt, denn irgendwann haben wir auch bei perfekter Permutation dann Wiederholungen der Datensätze.

Die Funktion mc_cv() liefert uns dann die Datensätze für die Monte-Carlo Kreuzvalidierung. Wir geben dabei an, wieviel der Daten in den jeweiligen Datensatz hinein permutiert werden soll.

mc_cv(pig_tbl, prop = 0.6, times = 3)
# Monte Carlo cross-validation (0.6/0.4) with 3 resamples  
# A tibble: 3 × 2
  splits            id       
  <list>            <chr>    
1 <split [247/165]> Resample1
2 <split [247/165]> Resample2
3 <split [247/165]> Resample3

66.7 Bootstraping

Die letzte Stichprobengenierungsmethode ist der Bootstrap. Eine Bootstrap Stichprobe ist eine Stichprobe des Datensatzes mit der gleichen Größe wie der Datensatz. Nur werden die Bootstrap Stichproben mit Ersetzung gezogen, so dass eine einzelne Beobachtung mehrfach in die Stichprobe aufgenommen werden können. Der Validierungsdatensatz besteht dann aus allen Beobachtungen, die nicht für den Analysesatz ausgewählt wurden. Im Allgemeinen führt das Bootstrap-Resampling zu pessimistischen Schätzungen der Modellgenauigkeit.

Wir können die Funktion bootstraps() für die Generierung der Bootstrap Stichprobe nutzen.

pig_boot_tbl <- pig_tbl |> 
  extract(1:10, 1:5)

pig_boot <- bootstraps(pig_boot_tbl, times = 3)

Nun haben wir auch die Möglichkeit uns die einzelnen Bootstraps Stichproben mit pluck() rauszuziehen. Hier sehen wir auch, dass einzelne Beobachtungen doppelt in der Bootstrap Stich probe vorkommen.

pluck(pig_boot, "splits", 1) |> 
  as_tibble() 
# A tibble: 10 × 5
   pig_id infected   age sex    location 
    <int>    <dbl> <dbl> <chr>  <chr>    
 1      3        0    66 female northeast
 2      4        1    59 female north    
 3      8        0    53 male   northwest
 4     10        1    57 male   northwest
 5      2        1    53 male   northwest
 6      9        1    58 female west     
 7      6        1    55 male   northwest
 8      9        1    58 female west     
 9     10        1    57 male   northwest
10      5        1    63 male   northwest

66.8 Weitere Valdierungen

Neben den hier vorgestellten Varianten gibt es noch weitere Möglichkeiten in dem R Paket {rsample} sich Stichprobendatensätze zu generieren. Wir gehen jetzt hier nicht mehr im Detail auf die verschiedenen Möglichkeiten ein. Dafür dann einfach die Links auf die {rsample} Hilfeseite nutzen.

  • Stratifiziertes Resampling nutzen wir, wenn wir eine Gruppe in den Daten haben, die nicht gleichmäßig über die Daten verteilt ist. Das heißt, wir haben ein nicht balanciertes Design. Kann plakativ wäre das der Fall, wenn wir fast nur Frauen oder Männer in unseren Daten vorliegen hätten. Hier kann es dann passieren, dass wir zufällig Datensätze ziehen, die nur Frauen oder nur Männer beinhalten. Das wollen wir natürlich verhindern.
  • Gruppiertes Resampling nutzen wir, wenn wir korrelierte Beobachtungen haben. Oft sind einige Beobachtungen in deinen Daten ähnlicher als es der Zufall vermuten ließe, z. B. weil sie wiederholte Messungen desselben Probanden darstellen oder alle an einem einzigen Ort gesammelt wurden. Dann müssen wir eventuell auch hierfür das Resampling anpassen.
  • Zeitpunkt basiertes Resampling sind in dem Sinne eine Besonderheit, da wir natürlich berücksichtigen müssen, wann eine Beobachtung im zeitlichen Verlauf gemacht wurde. Hier hat die Zeit einen Einfluss auf das Resampling.

Am Ende musst du entscheiden, welche der Resamplingmethoden für dich am besten geeignet ist. Wir müssen eben einen Trainingsdatensatz und einen Testdatensatz haben. Die Validierungsdaten dienen dann zum Tuning deiner Modelle. Nicht immer nutzen wir auch Validierungsdatensätze. In dem einfachsten Anwendungsfall nutzt du immer wieder deine Traingsdaten mit unterschiedlichen Einstellungen in deinem Algorithmus.