11. Häufige Fallstricke und empfohlene Vorgehensweisen#
Der Zweck dieses Kapitels ist es, einige häufige Fallstricke und Anti-Patterns zu veranschaulichen, die bei der Verwendung von scikit-learn auftreten. Es bietet Beispiele dafür, was man **nicht** tun sollte, zusammen mit einem entsprechenden korrekten Beispiel.
11.1. Inkonsistente Vorverarbeitung#
scikit-learn stellt eine Bibliothek für Datentransformationen bereit, die Merkmalsrepräsentationen bereinigen (siehe Datenvorverarbeitung), reduzieren (siehe Unüberwachte Dimensionsreduktion), erweitern (siehe Kernel-Approximation) oder generieren (siehe Merkmalsextraktion) können. Wenn diese Datentransformationen beim Trainieren eines Modells verwendet werden, müssen sie auch auf nachfolgende Datensätze angewendet werden, sei es Testdaten oder Daten in einem Produktionssystem. Andernfalls ändert sich der Merkmalsraum, und das Modell kann nicht effektiv funktionieren.
Für das folgende Beispiel erstellen wir einen synthetischen Datensatz mit einem einzelnen Merkmal
>>> from sklearn.datasets import make_regression
>>> from sklearn.model_selection import train_test_split
>>> random_state = 42
>>> X, y = make_regression(random_state=random_state, n_features=1, noise=1)
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, test_size=0.4, random_state=random_state)
Falsch
Der Trainingsdatensatz wird skaliert, aber nicht der Testdatensatz, sodass die Modellleistung auf dem Testdatensatz schlechter als erwartet ist
>>> from sklearn.metrics import mean_squared_error
>>> from sklearn.linear_model import LinearRegression
>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> X_train_transformed = scaler.fit_transform(X_train)
>>> model = LinearRegression().fit(X_train_transformed, y_train)
>>> mean_squared_error(y_test, model.predict(X_test))
62.80...
Richtig
Anstatt die nicht transformierten X_test an predict zu übergeben, sollten wir die Testdaten transformieren, genau wie wir die Trainingsdaten transformiert haben
>>> X_test_transformed = scaler.transform(X_test)
>>> mean_squared_error(y_test, model.predict(X_test_transformed))
0.90...
Alternativ empfehlen wir die Verwendung einer Pipeline, die es einfacher macht, Transformationen mit Schätzern zu verketten und die Möglichkeit vergisst eine Transformation zu vergessen reduziert
>>> from sklearn.pipeline import make_pipeline
>>> model = make_pipeline(StandardScaler(), LinearRegression())
>>> model.fit(X_train, y_train)
Pipeline(steps=[('standardscaler', StandardScaler()),
('linearregression', LinearRegression())])
>>> mean_squared_error(y_test, model.predict(X_test))
0.90...
Pipelines helfen auch, einen weiteren häufigen Fallstrick zu vermeiden: das „Leaken“ von Testdaten in die Trainingsdaten.
11.2. Datenleck#
Datenlecks treten auf, wenn Informationen, die zum Zeitpunkt der Vorhersage nicht verfügbar wären, beim Aufbau des Modells verwendet werden. Dies führt zu übermäßig optimistischen Leistungsschätzungen, z. B. aus der Kreuzvalidierung, und somit zu einer schlechteren Leistung, wenn das Modell auf tatsächlich neuen Daten verwendet wird, z. B. während der Produktion.
Eine häufige Ursache ist die Nicht-Trennung von Test- und Trainingsdatensubsets. Testdaten sollten niemals verwendet werden, um Entscheidungen über das Modell zu treffen. **Die allgemeine Regel lautet: Rufen Sie niemals** fit **auf den Testdaten auf.** Das mag offensichtlich klingen, wird aber in einigen Fällen leicht übersehen, z. B. bei der Anwendung bestimmter Vorverarbeitungsschritte.
Obwohl sowohl Trainings- als auch Testdatensubsets die gleiche Vorverarbeitungstransformation (wie im vorherigen Abschnitt beschrieben) erhalten sollten, ist es wichtig, dass diese Transformationen nur aus den Trainingsdaten gelernt werden. Wenn Sie beispielsweise einen Normalisierungsschritt haben, bei dem Sie durch den Durchschnittswert dividieren, sollte der Durchschnitt der Durchschnitt des Trainingssubset sein, **nicht** der Durchschnitt aller Daten. Wenn das Testsubset in die Durchschnittsberechnung einbezogen wird, beeinflussen Informationen aus dem Testsubset das Modell.
11.2.1. So vermeiden Sie Datenlecks#
Hier sind einige Tipps, wie Sie Datenlecks vermeiden können
Teilen Sie die Daten immer zuerst in Trainings- und Testsubsets auf, insbesondere vor allen Vorverarbeitungsschritten.
Schließen Sie niemals Testdaten ein, wenn Sie die Methoden
fitundfit_transformverwenden. Die Verwendung aller Daten, z. B.fit(X), kann zu übermäßig optimistischen Ergebnissen führen.Umgekehrt sollte die Methode
transformsowohl auf Trainings- als auch auf Testsubsets angewendet werden, da die gleiche Vorverarbeitung auf alle Daten angewendet werden sollte. Dies kann durch die Verwendung vonfit_transformauf dem Trainingssubset undtransformauf dem Testsubset erreicht werden.Die scikit-learn Pipeline ist eine großartige Möglichkeit, Datenlecks zu verhindern, da sie sicherstellt, dass die entsprechende Methode auf dem richtigen Datensubset ausgeführt wird. Die Pipeline ist ideal für die Verwendung in Funktionen zur Kreuzvalidierung und Hyperparameter-Abstimmung.
Ein Beispiel für Datenlecks während der Vorverarbeitung wird nachfolgend detailliert.
11.2.2. Datenlecks während der Vorverarbeitung#
Hinweis
Wir entscheiden uns hier, Datenlecks mit einem Schritt zur Merkmalsauswahl zu veranschaulichen. Das Risiko von Lecks ist jedoch bei fast allen Transformationen in scikit-learn relevant, einschließlich (aber nicht beschränkt auf) StandardScaler, SimpleImputer und PCA.
Eine Reihe von Merkmalsauswahlfunktionen sind in scikit-learn verfügbar. Sie können helfen, irrelevante, redundante und verrauschte Merkmale zu entfernen sowie die Erstellungszeit und Leistung Ihres Modells zu verbessern. Wie bei jeder anderen Art von Vorverarbeitung sollte die Merkmalsauswahl **nur** die Trainingsdaten verwenden. Die Einbeziehung der Testdaten in die Merkmalsauswahl wird Ihr Modell optimistisch verzerren.
Zur Veranschaulichung erstellen wir dieses binäre Klassifikationsproblem mit 10.000 zufällig generierten Merkmalen
>>> import numpy as np
>>> n_samples, n_features, n_classes = 200, 10000, 2
>>> rng = np.random.RandomState(42)
>>> X = rng.standard_normal((n_samples, n_features))
>>> y = rng.choice(n_classes, n_samples)
Falsch
Die Verwendung aller Daten zur Merkmalsauswahl führt zu einer Genauigkeit, die viel höher ist als der Zufall, obwohl unsere Zielwerte völlig zufällig sind. Diese Zufälligkeit bedeutet, dass unsere X und y unabhängig sind und wir daher eine Genauigkeit von etwa 0,5 erwarten. Da der Schritt der Merkmalsauswahl die Testdaten „sieht“, hat das Modell jedoch einen unfairen Vorteil. Im folgenden falschen Beispiel verwenden wir zuerst alle Daten zur Merkmalsauswahl und teilen dann die Daten in Trainings- und Testsubsets für die Modellanpassung auf. Das Ergebnis ist eine viel höhere als erwartete Genauigkeitsbewertung
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.feature_selection import SelectKBest
>>> from sklearn.ensemble import HistGradientBoostingClassifier
>>> from sklearn.metrics import accuracy_score
>>> # Incorrect preprocessing: the entire data is transformed
>>> X_selected = SelectKBest(k=25).fit_transform(X, y)
>>> X_train, X_test, y_train, y_test = train_test_split(
... X_selected, y, random_state=42)
>>> gbc = HistGradientBoostingClassifier(random_state=1)
>>> gbc.fit(X_train, y_train)
HistGradientBoostingClassifier(random_state=1)
>>> y_pred = gbc.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.76
Richtig
Um Datenlecks zu verhindern, ist es gute Praxis, Ihre Daten **zuerst** in Trainings- und Testsubsets aufzuteilen. Die Merkmalsauswahl kann dann nur mit dem Trainingsdatensatz durchgeführt werden. Beachten Sie, dass wir immer dann, wenn wir fit oder fit_transform verwenden, nur den Trainingsdatensatz verwenden. Die Bewertung liegt nun im erwarteten Bereich für die Daten, nahe am Zufall
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, random_state=42)
>>> select = SelectKBest(k=25)
>>> X_train_selected = select.fit_transform(X_train, y_train)
>>> gbc = HistGradientBoostingClassifier(random_state=1)
>>> gbc.fit(X_train_selected, y_train)
HistGradientBoostingClassifier(random_state=1)
>>> X_test_selected = select.transform(X_test)
>>> y_pred = gbc.predict(X_test_selected)
>>> accuracy_score(y_test, y_pred)
0.5
Auch hier empfehlen wir die Verwendung einer Pipeline, um die Merkmalsauswahl und die Modellschätzer zu verketten. Die Pipeline stellt sicher, dass beim Ausführen von fit nur die Trainingsdaten verwendet werden und die Testdaten nur zur Berechnung der Genauigkeitsbewertung verwendet werden
>>> from sklearn.pipeline import make_pipeline
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, random_state=42)
>>> pipeline = make_pipeline(SelectKBest(k=25),
... HistGradientBoostingClassifier(random_state=1))
>>> pipeline.fit(X_train, y_train)
Pipeline(steps=[('selectkbest', SelectKBest(k=25)),
('histgradientboostingclassifier',
HistGradientBoostingClassifier(random_state=1))])
>>> y_pred = pipeline.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.5
Die Pipeline kann auch an eine Kreuzvalidierungsfunktion wie cross_val_score übergeben werden. Auch hier stellt die Pipeline sicher, dass während des Anpassens und Vorhersagens die richtigen Datensubsets und Schätzermethoden verwendet werden
>>> from sklearn.model_selection import cross_val_score
>>> scores = cross_val_score(pipeline, X, y)
>>> print(f"Mean accuracy: {scores.mean():.2f}+/-{scores.std():.2f}")
Mean accuracy: 0.43+/-0.05
11.3. Kontrolle der Zufälligkeit#
Einige scikit-learn-Objekte sind von Natur aus zufällig. Dies sind in der Regel Schätzer (z. B. RandomForestClassifier) und Kreuzvalidierungs-Splitter (z. B. KFold). Die Zufälligkeit dieser Objekte wird über ihren random_state-Parameter gesteuert, wie im Glossar beschrieben. Dieser Abschnitt erweitert den Glossareintrag und beschreibt bewährte Verfahren und häufige Fallstricke in Bezug auf diesen subtilen Parameter.
Hinweis
Zusammenfassung der Empfehlungen
Für eine optimale Robustheit der Kreuzvalidierungsergebnisse (CV) übergeben Sie RandomState-Instanzen bei der Erstellung von Schätzern oder lassen Sie random_state auf None. Die Übergabe von ganzen Zahlen an CV-Splitter ist in der Regel die sicherste Option und wird bevorzugt; die Übergabe von RandomState-Instanzen an Splitter kann manchmal nützlich sein, um sehr spezifische Anwendungsfälle zu erreichen. Sowohl für Schätzer als auch für Splitter führt die Übergabe einer ganzen Zahl im Vergleich zur Übergabe einer Instanz (oder None) zu subtilen, aber signifikanten Unterschieden, insbesondere bei CV-Verfahren. Diese Unterschiede sind wichtig zu verstehen, wenn Ergebnisse berichtet werden.
Um reproduzierbare Ergebnisse über mehrere Ausführungen hinweg zu erzielen, entfernen Sie jede Verwendung von random_state=None.
11.3.1. Verwendung von None oder RandomState-Instanzen und wiederholte Aufrufe von fit und split#
Der Parameter random_state bestimmt, ob mehrere Aufrufe von fit (für Schätzer) oder von split (für CV-Splitter) zu denselben Ergebnissen führen, gemäß diesen Regeln
Wenn eine ganze Zahl übergeben wird, liefern wiederholte Aufrufe von
fitodersplitimmer dieselben Ergebnisse.Wenn
Noneoder eineRandomState-Instanz übergeben wird:fitundsplitliefern jedes Mal unterschiedliche Ergebnisse, wenn sie aufgerufen werden, und die Abfolge der Aufrufe erkundet alle Entropiequellen.Noneist der Standardwert für allerandom_state-Parameter.
Hier veranschaulichen wir diese Regeln sowohl für Schätzer als auch für CV-Splitter.
Hinweis
Da die Übergabe von random_state=None der globalen RandomState-Instanz von numpy (random_state=np.random.mtrand._rand) entspricht, werden wir None hier nicht explizit erwähnen. Alles, was für Instanzen gilt, gilt auch für die Verwendung von None.
11.3.1.1. Schätzer#
Die Übergabe von Instanzen bedeutet, dass wiederholte Aufrufe von fit nicht zu denselben Ergebnissen führen, auch wenn der Schätzer mit denselben Daten und denselben Hyperparametern angepasst wird
>>> from sklearn.linear_model import SGDClassifier
>>> from sklearn.datasets import make_classification
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(n_features=5, random_state=rng)
>>> sgd = SGDClassifier(random_state=rng)
>>> sgd.fit(X, y).coef_
array([[ 8.85418642, 4.79084103, -3.13077794, 8.11915045, -0.56479934]])
>>> sgd.fit(X, y).coef_
array([[ 6.70814003, 5.25291366, -7.55212743, 5.18197458, 1.37845099]])
Wir können aus dem obigen Ausschnitt sehen, dass wiederholte Aufrufe von sgd.fit unterschiedliche Modelle erzeugt haben, auch wenn die Daten gleich waren. Das liegt daran, dass der Zufallszahlengenerator (RNG) des Schätzers beim Aufruf von fit verbraucht wird (d. h. mutiert wird), und dieser mutierte RNG wird bei den nachfolgenden Aufrufen von fit verwendet. Darüber hinaus wird das rng-Objekt über alle Objekte geteilt, die es verwenden, und infolgedessen werden diese Objekte etwas voneinander abhängig. Zwei Schätzer, die dieselbe RandomState-Instanz teilen, beeinflussen sich beispielsweise gegenseitig, wie wir später sehen werden, wenn wir über das Klonen sprechen. Dieser Punkt ist wichtig zu beachten, wenn man Fehler behebt.
Wenn wir eine ganze Zahl an den Parameter random_state des SGDClassifier übergeben hätten, hätten wir jedes Mal dieselben Modelle und somit dieselben Bewertungen erhalten. Wenn wir eine ganze Zahl übergeben, wird derselbe RNG über alle Aufrufe von fit verwendet. Intern geschieht Folgendes: Auch wenn der RNG beim Aufruf von fit verbraucht wird, wird er zu Beginn von fit immer auf seinen ursprünglichen Zustand zurückgesetzt.
11.3.1.2. CV-Splitter#
Zufällige CV-Splitter verhalten sich ähnlich, wenn eine RandomState-Instanz übergeben wird; wiederholte Aufrufe von split liefern unterschiedliche Datenaufteilungen
>>> from sklearn.model_selection import KFold
>>> import numpy as np
>>> X = y = np.arange(10)
>>> rng = np.random.RandomState(0)
>>> cv = KFold(n_splits=2, shuffle=True, random_state=rng)
>>> for train, test in cv.split(X, y):
... print(train, test)
[0 3 5 6 7] [1 2 4 8 9]
[1 2 4 8 9] [0 3 5 6 7]
>>> for train, test in cv.split(X, y):
... print(train, test)
[0 4 6 7 8] [1 2 3 5 9]
[1 2 3 5 9] [0 4 6 7 8]
Wir sehen, dass die Aufteilungen ab dem zweiten Aufruf von split unterschiedlich sind. Dies kann zu unerwarteten Ergebnissen führen, wenn Sie die Leistung mehrerer Schätzer vergleichen, indem Sie split mehrmals aufrufen, wie wir im nächsten Abschnitt sehen werden.
11.3.2. Häufige Fallstricke und Feinheiten#
Obwohl die Regeln für den Parameter random_state scheinbar einfach sind, haben sie doch einige subtile Auswirkungen. In einigen Fällen kann dies sogar zu falschen Schlussfolgerungen führen.
11.3.2.1. Schätzer#
**Unterschiedliche** random_state **-Typen führen zu unterschiedlichen Kreuzvalidierungsverfahren**
Je nach Typ des Parameters random_state verhalten sich Schätzer unterschiedlich, insbesondere in Kreuzvalidierungsverfahren. Betrachten Sie den folgenden Ausschnitt
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import cross_val_score
>>> import numpy as np
>>> X, y = make_classification(random_state=0)
>>> rf_123 = RandomForestClassifier(random_state=123)
>>> cross_val_score(rf_123, X, y)
array([0.85, 0.95, 0.95, 0.9 , 0.9 ])
>>> rf_inst = RandomForestClassifier(random_state=np.random.RandomState(0))
>>> cross_val_score(rf_inst, X, y)
array([0.9 , 0.95, 0.95, 0.9 , 0.9 ])
Wir sehen, dass die kreuzvalidierten Bewertungen von rf_123 und rf_inst unterschiedlich sind, wie es zu erwarten wäre, da wir nicht denselben random_state-Parameter übergeben haben. Der Unterschied zwischen diesen Bewertungen ist jedoch subtiler als er aussieht, und **die Kreuzvalidierungsverfahren, die von** cross_val_score **durchgeführt wurden, unterscheiden sich in jedem Fall erheblich**
Da
rf_123eine ganze Zahl übergeben wurde, verwendet jeder Aufruf vonfitdenselben RNG: Das bedeutet, dass alle zufälligen Eigenschaften des Random-Forest-Schätzers für jede der 5 Folds des CV-Verfahrens gleich sind. Insbesondere ist die (zufällig ausgewählte) Teilmenge der Merkmale des Schätzers über alle Folds hinweg gleich.Da
rf_insteineRandomState-Instanz übergeben wurde, beginnt jeder Aufruf vonfitmit einem anderen RNG. Infolgedessen ist die zufällige Teilmenge der Merkmale für jede Fold unterschiedlich.
Obwohl ein konstanter Schätzer-RNG über die Folds hinweg nicht per se falsch ist, wünschen wir uns in der Regel CV-Ergebnisse, die robust gegenüber der Zufälligkeit des Schätzers sind. Infolgedessen kann die Übergabe einer Instanz anstelle einer ganzen Zahl vorzuziehen sein, da sie es dem Schätzer-RNG ermöglicht, sich für jede Fold zu ändern.
Hinweis
Hier wird cross_val_score einen nicht-zufälligen CV-Splitter verwenden (wie standardmäßig), sodass beide Schätzer auf denselben Splits ausgewertet werden. In diesem Abschnitt geht es nicht um die Variabilität der Splits. Auch spielt es für unseren Illustrationszweck keine Rolle, ob wir make_classification eine ganze Zahl oder eine Instanz übergeben: Entscheidend ist, was wir dem RandomForestClassifier-Schätzer übergeben.
Klonen#
Eine weitere subtile Nebenwirkung der Übergabe von RandomState-Instanzen ist, wie clone funktioniert
>>> from sklearn import clone
>>> from sklearn.ensemble import RandomForestClassifier
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> a = RandomForestClassifier(random_state=rng)
>>> b = clone(a)
Da a eine RandomState-Instanz übergeben wurde, sind a und b keine Klone im strengen Sinne, sondern eher Klone im statistischen Sinne: a und b werden immer noch unterschiedliche Modelle sein, auch wenn fit(X, y) auf denselben Daten aufgerufen wird. Darüber hinaus werden sich a und b gegenseitig beeinflussen, da sie denselben internen RNG teilen: Der Aufruf von a.fit verbraucht den RNG von b, und der Aufruf von b.fit verbraucht den RNG von a, da sie identisch sind. Dieser Teil gilt für alle Schätzer, die einen random_state-Parameter teilen; er ist nicht spezifisch für Klone.
Wenn eine ganze Zahl übergeben würde, wären a und b exakte Klone und würden sich nicht gegenseitig beeinflussen.
Warnung
Obwohl clone selten im Benutzercode verwendet wird, wird es im gesamten scikit-learn-Codebase ausgiebig aufgerufen: Insbesondere die meisten Meta-Schätzer, die nicht angepasste Schätzer akzeptieren, rufen clone intern auf (GridSearchCV, StackingClassifier, CalibratedClassifierCV usw.).
11.3.2.2. CV-Splitter#
Wenn ein RandomState-Instanz übergeben wird, liefern CV-Splitter jedes Mal unterschiedliche Splits, wenn split aufgerufen wird. Beim Vergleich verschiedener Schätzer kann dies zu einer Überschätzung der Varianz des Leistungsunterschieds zwischen den Schätzern führen
>>> from sklearn.naive_bayes import GaussianNB
>>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import KFold
>>> from sklearn.model_selection import cross_val_score
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(random_state=rng)
>>> cv = KFold(shuffle=True, random_state=rng)
>>> lda = LinearDiscriminantAnalysis()
>>> nb = GaussianNB()
>>> for est in (lda, nb):
... print(cross_val_score(est, X, y, cv=cv))
[0.8 0.75 0.75 0.7 0.85]
[0.85 0.95 0.95 0.85 0.95]
Der direkte Vergleich der Leistung des LinearDiscriminantAnalysis-Schätzers mit dem GaussianNB-Schätzer **auf jeder Fold** wäre ein Fehler: **die Splits, auf denen die Schätzer ausgewertet werden, sind unterschiedlich**. Tatsächlich ruft cross_val_score intern cv.split auf derselben KFold-Instanz auf, aber die Splits werden jedes Mal unterschiedlich sein. Dies gilt auch für jedes Werkzeug, das Modellselektion mittels Kreuzvalidierung durchführt, z. B. GridSearchCV und RandomizedSearchCV: Die Bewertungen sind nicht von Fold zu Fold über verschiedene Aufrufe von search.fit vergleichbar, da cv.split mehrmals aufgerufen worden wäre. Innerhalb eines einzigen Aufrufs von search.fit ist jedoch ein Vergleich von Fold zu Fold möglich, da der Suchschätzer cv.split nur einmal aufruft.
Für vergleichbare Ergebnisse von Fold zu Fold in allen Szenarien sollte eine ganze Zahl an den CV-Splitter übergeben werden: cv = KFold(shuffle=True, random_state=0).
Hinweis
Während der Vergleich von Fold zu Fold mit RandomState-Instanzen nicht ratsam ist, kann man davon ausgehen, dass durchschnittliche Bewertungen ausreichen, um zu schlussfolgern, ob ein Schätzer besser ist als ein anderer, solange genügend Folds und Daten verwendet werden.
Hinweis
Was in diesem Beispiel zählt, ist das, was an KFold übergeben wurde. Ob wir eine RandomState-Instanz oder eine ganze Zahl an make_classification übergeben, ist für unseren Illustrationszweck nicht relevant. Auch weder LinearDiscriminantAnalysis noch GaussianNB sind randomisierte Schätzer.
11.3.3. Allgemeine Empfehlungen#
11.3.3.1. Reproduzierbare Ergebnisse über mehrere Ausführungen hinweg erzielen#
Um reproduzierbare (d. h. konstante) Ergebnisse über mehrere **Programmausführungen** zu erzielen, müssen wir alle Verwendungen von random_state=None, was der Standardwert ist, entfernen. Die empfohlene Methode ist, am Anfang des Programms eine rng-Variable zu deklarieren und sie an jedes Objekt weiterzugeben, das einen random_state-Parameter akzeptiert
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import train_test_split
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(random_state=rng)
>>> rf = RandomForestClassifier(random_state=rng)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y,
... random_state=rng)
>>> rf.fit(X_train, y_train).score(X_test, y_test)
0.84
Wir sind nun sicher, dass das Ergebnis dieses Skripts immer 0,84 sein wird, egal wie oft wir es ausführen. Das Ändern der globalen rng-Variablen auf einen anderen Wert sollte die Ergebnisse wie erwartet beeinflussen.
Es ist auch möglich, die rng-Variable als ganze Zahl zu deklarieren. Dies kann jedoch zu weniger robusten Kreuzvalidierungsergebnissen führen, wie wir im nächsten Abschnitt sehen werden.
Hinweis
Wir empfehlen nicht, den globalen numpy-Seed durch Aufruf von np.random.seed(0) zu setzen. Siehe hier für eine Diskussion.
11.3.3.2. Robustheit der Kreuzvalidierungsergebnisse#
Wenn wir die Leistung eines randomisierten Schätzers mittels Kreuzvalidierung bewerten, möchten wir sicherstellen, dass der Schätzer genaue Vorhersagen für neue Daten liefern kann, aber wir möchten auch sicherstellen, dass der Schätzer robust gegenüber seiner zufälligen Initialisierung ist. Zum Beispiel möchten wir, dass die zufällige Gewichtsinitialisierung eines SGDClassifier über alle Folds hinweg durchweg gut ist: Andernfalls könnten wir Pech haben, wenn wir diesen Schätzer auf neue Daten trainieren, und die zufällige Initialisierung zu schlechter Leistung führen. Ebenso möchten wir, dass ein Random Forest robust gegenüber der Menge der zufällig ausgewählten Merkmale ist, die jeder Baum verwenden wird.
Aus diesen Gründen ist es vorzuziehen, die Kreuzvalidierungsleistung zu bewerten, indem man dem Schätzer erlaubt, auf jeder Fold einen anderen RNG zu verwenden. Dies geschieht durch Übergabe einer RandomState-Instanz (oder None) an die Schätzerinitialisierung.
Wenn wir eine ganze Zahl übergeben, verwendet der Schätzer auf jeder Fold denselben RNG: Wenn der Schätzer gut (oder schlecht) abschneidet, wie von der CV bewertet, könnte es einfach daran liegen, dass wir Glück (oder Pech) mit diesem spezifischen Seed hatten. Die Übergabe von Instanzen führt zu robusteren CV-Ergebnissen und macht den Vergleich zwischen verschiedenen Algorithmen fairer. Es hilft auch, die Versuchung zu begrenzen, den RNG des Schätzers als einen zu stimmenden Hyperparameter zu behandeln.
Ob wir RandomState-Instanzen oder ganze Zahlen an CV-Splitter übergeben, hat keinen Einfluss auf die Robustheit, solange split nur einmal aufgerufen wird. Wenn split mehrmals aufgerufen wird, ist ein Vergleich von Fold zu Fold nicht mehr möglich. Infolgedessen ist die Übergabe von ganzen Zahlen an CV-Splitter in der Regel sicherer und deckt die meisten Anwendungsfälle ab.