Het doel is om te achterhalen hoe goed we data kunnnen voorspellen die niet aanwezig is in de trainingsdata. De fit op de trainingsdata zelf is dus eigenlijk niet zo belangrijk. Wel het generalisatievermogen van het model. We kunnen dit op 2 verschillende manieren verbeteren :
Cross-validatie : de test set wordt op een meer robuuste manier gekozen
Grid-search : een effectieve methode om parameters goed te leren instellen
metrieken gebruikt tot dusver :
voor classificatie : accuracy : tel het aantal juist geclassificeerde data-elementen in de testset tov de grootte van de testset
voor regressie : MSE : mean-squared-error : bereken het verschil tussen voorspelde en werkelijke waarde, kwadrateer deze fouten en neem hier een gemiddelde van
Er zijn echter nog veel metrieken die gebruikt kunnen worden en beter geschikt zijn afhankelijk van de applicatie.
De typische manier totnogtoe om een model te evalueren is door gebruik te maken van de train_test_split methode, daarna met de trainingsdata een model op te stellen via de fit methode, vervolgens de predict methode te gebruiken op de test set en tenslotte via de score methode na te gaan wat de accuracy is op de test set.
from sklearn.datasets import make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# create a synthetic dataset
X, y = make_blobs(random_state=0)
# split data and labels into a training and a test set
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# instantiate a model and fit it to the training set
logreg = LogisticRegression(solver='liblinear', multi_class='auto').fit(X_train, y_train)
# evaluate the model on the test set
print("Test set score: {:.2f}".format(logreg.score(X_test, y_test)))
Test set score: 0.88
Tijdens cross-validatie wordt de data meerdere keren gesplit en worden er dus ook meerdere modellen getrained. Typisch wordt k-fold cross-validatie gebruikt waarbij $\bf{k}$ de parameter is die aangeeft in hoeveel gelijke delen (folds genaamd) de data wordt opgesplitst. Vervolgens worden $\bf{k}$ modellen getraind : het eerste model gebruikt de eerste fold als test-set en al de rest als trainingsset, het tweede model gebruikt de tweede fold als testset en de rest als trainingsset enz..
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
iris = load_iris()
logreg = LogisticRegression(solver='liblinear', multi_class='auto')
scores = cross_val_score(logreg, iris.data, iris.target, cv = 3)
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96 0.96 0.94]
scores = cross_val_score(logreg, iris.data, iris.target)
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [1. 0.96666667 0.93333333 0.9 1. ]
print("Average cross-validation score: {:.2f}".format(scores.mean()))
Average cross-validation score: 0.96
from sklearn.model_selection import cross_validate
res = cross_validate(logreg, iris.data, iris.target, cv=5,
return_train_score=True)
display(res)
{'fit_time': array([0.00200438, 0.00100017, 0.00099945, 0.00099897, 0.00199986]),
'score_time': array([0.00099516, 0.00099921, 0.00099945, 0.00099778, 0. ]),
'test_score': array([1. , 0.96666667, 0.93333333, 0.9 , 1. ]),
'train_score': array([0.95 , 0.96666667, 0.96666667, 0.975 , 0.95833333])}
import pandas as pd
res_df = pd.DataFrame(res)
display(res_df)
print("Mean times and scores:\n", res_df.mean())
| fit_time | score_time | test_score | train_score | |
|---|---|---|---|---|
| 0 | 0.002004 | 0.000995 | 1.000000 | 0.950000 |
| 1 | 0.001000 | 0.000999 | 0.966667 | 0.966667 |
| 2 | 0.000999 | 0.000999 | 0.933333 | 0.966667 |
| 3 | 0.000999 | 0.000998 | 0.900000 | 0.975000 |
| 4 | 0.002000 | 0.000000 | 1.000000 | 0.958333 |
Mean times and scores: fit_time 0.001401 score_time 0.000798 test_score 0.960000 train_score 0.963333 dtype: float64
Er is een variantie in accuracy tussen de verschillende folds : sommige folds halen $100\%$, andere $90\%$. Dit kan 2 dingen betekenen:
- het model is sterk afhankelijk van de geselecteerde fold
- dit voorbeeld bevat te weinig data
Merk op : in het geval van classificatie :
Bij het opdelen van de datasets in folds kan je best rekening houden met hoe de verschillende klassen verspreid zijn over de data. Een fold met alleen voorbeelden uit 1 klasse is geen nuttige testset, en dus ook geen nuttige fold!
from sklearn.datasets import load_iris
iris = load_iris()
print("Iris labels:\n{}".format(iris.target))
Iris labels: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]
Stel dat er hier 3 folds gemaakt worden, met per fold telkens de data van 1 klasse ???
-> zorg ervoor dat per fold alle klassen gelijkwaardig verdeeld zijn, dit noemt men Stratified cross-validation
Uit de documentatie van de cv parameter : sklearn.model_selection.cross_val_score :
cv : int, cross-validation generator or an iterable, optional Determines the cross-validation splitting strategy. Possible inputs for cv are:
- None, to use the default 5-fold cross validation, - integer, to specify the number of folds in a (Stratified)KFold, - CV splitter, - An iterable yielding (train, test) splits as arrays of indices.For integer/None inputs, if the estimator is a classifier and y is either binary or multiclass, StratifiedKFold is used. In all other cases, KFold is used.
Er is echter ook een shuffle parameter !
from sklearn.model_selection import KFold
kfold = KFold(n_splits=3)
print("Cross-validation scores:\n{}".format(
cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores: [0. 0. 0.]
kfold = KFold(n_splits=3, shuffle=True, random_state=0)
print("Cross-validation scores:\n{}".format(
cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores: [0.9 0.96 0.96]
Beschouw het extreme geval waarbij $k = \; de \; grootte \; van \; de \; dataset$. Elke fold bevat dan exact 1 data element. Deze techniek heet leave-one-out cross validation, deze is uiteraard time-consuming maar vooral interessant voor kleine datasets.
Het zou kunnen dat los van de klassen er nog een andere groepsindeling in de data gemaakt kan worden, bvb. :
- speech recognition : je hebt meerdere recordings van dezelfde stem in je data
- beeldherkenning : je hebt meerdere fotos van dezelfde persoon in je data
- medische applicaties : je hebt meerdere samples van dezelfde patient.
GroupKfold zal ervoor zorgen dat de groep samen blijft en dus ofwel integraal in de training of in de test zit, merk op dit heeft niets te maken met de klaslabels.
Doel : Zoek de betere waarden van de parameters van een model zodat ze optimaal generaliseren.
Voorbeeld : Elbow methode, evalueer de waarde van parameter $k$ op de test-set
Gevaar : Als de testset gebruikt wordt om de parameters te leren instellen, kunnen we diezelfde set dan nog gebruiken om te evalueren hoe goed het model generaliseert? (want de parameters zijn al getuned voor deze set en er bestaat gevaar op overfitting op de testset)
Oplossing : We hebben nog een derde onafhankelijke set van data nodig eentje die specifiek gebruikt kan worden om de parameter tuning te doen : deze set noemen we de validatie set
from sklearn.model_selection import train_test_split
# split data into train+validation set and test set
X_trainval, X_test, y_trainval, y_test = train_test_split(iris.data, iris.target, random_state=0)
# split train+validation set into training and validation sets
X_train, X_valid, y_train, y_valid = train_test_split(X_trainval, y_trainval, random_state=1)
print("Size of training set: {} size of validation set: {} size of test set:"
" {}\n".format(X_train.shape[0], X_valid.shape[0], X_test.shape[0]))
Size of training set: 84 size of validation set: 28 size of test set: 38
from sklearn.tree import DecisionTreeClassifier
best_score = 0
# make a grid for parameter tuning
for depth in [1,2,3,4,5]:
for rs in [0,5,10,20,40]:
# for each combination of parameters train a decisiontree
tree = DecisionTreeClassifier(max_depth=depth, random_state=rs)
tree.fit(X_train, y_train)
# evaluate for the validation set
score = tree.score(X_valid, y_valid)
# store the best scores
if score > best_score:
best_score = score
best_parameters = {'max_depth': depth, 'random_state': rs}
print("Best parameters: ", best_parameters)
Best parameters: {'max_depth': 3, 'random_state': 0}
# rebuild a model on the combined training and validation set,
# and evaluate it on the test set
tree = DecisionTreeClassifier(**best_parameters)
tree.fit(X_trainval, y_trainval)
training_score = tree.score(X_train, y_train)
test_score = tree.score(X_test, y_test)
print("Training set score with best parameters : {:.2f}".format(training_score))
print("Best score on validation set: {:.2f}".format(best_score))
print("Test set score with best parameters: {:.2f}".format(test_score))
Training set score with best parameters : 1.00 Best score on validation set: 0.93 Test set score with best parameters: 0.97
Cross-validatie wordt vaak gebruikt in combinatie met grid search, men verwijst vaak gewoon naar de term cross-validatie om beide technieken samen aan te duiden : in plaats van een enkele split te maken tussen training en validatie set, wordt hier cross-validatie gebruikt
# make a grid for parameter tuning
for depth in [1,2,3,4,5]:
for rs in [0,5,10,20,40]:
# for each combination of parameters train a decisiontree
tree = DecisionTreeClassifier(max_depth=depth, random_state=rs)
# Extra step : perform cross-validation here (trainval will be split in training en validation several times)
scores = cross_val_score(tree, X_trainval, y_trainval, cv = 5)
# compute mean cross-validation accuracy
score = scores.mean()
# store the best scores
if score > best_score:
best_score = score
best_parameters = {'max_depth': depth, 'random_state': rs}
print("Best parameters: ", best_parameters)
Best parameters: {'max_depth': 3, 'random_state': 0}
tree = DecisionTreeClassifier(**best_parameters)
tree.fit(X_trainval, y_trainval)
training_score = tree.score(X_trainval, y_trainval)
test_score = tree.score(X_test, y_test)
print("Training set score with best parameters : {:.2f}".format(training_score))
print("Best score on validation set: {:.2f}".format(best_score))
print("Test set score with best parameters: {:.2f}".format(test_score))
Training set score with best parameters : 0.98 Best score on validation set: 0.96 Test set score with best parameters: 0.97
De combinatie van Grid search met CV is zo populair dat sklearn een aparte klasse GridSearchCV voorziet
param_grid = {'max_depth': [1,2,3,4,5],
'random_state': [0,5,10,20,40]}
print("Parameter grid:\n{}".format(param_grid))
Parameter grid:
{'max_depth': [1, 2, 3, 4, 5], 'random_state': [0, 5, 10, 20, 40]}
from sklearn.model_selection import GridSearchCV
grid_search = GridSearchCV(DecisionTreeClassifier(), param_grid, cv=5,
return_train_score=True)
# do not overfit the parameters !
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)
grid_search.fit(X_train, y_train)
print("Test set score: {:.2f}".format(grid_search.score(X_test, y_test)))
print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))
print("Best estimator:\n{}".format(grid_search.best_estimator_))
Test set score: 0.97
Best parameters: {'max_depth': 3, 'random_state': 0}
Best cross-validation score: 0.96
Best estimator:
DecisionTreeClassifier(max_depth=3, random_state=0)
Merk op : Al deze modellen uitrekenen is rekenintensief ! De grid moet m.a.w. goed gekozen worden.
Doel : generalisatie vermogen van het model verbeteren op reƫle data.
1 test set gebruiken om accuracy te berekenen is erg beperkt
parametertuning en gridsearch technieken helpen om generalisatievermogen te verhogen, let wel dat je hierbij de testset niet gaat overfitten :
- maak gebruik van een validatieset
- doe de combinatie van gridsearch / cross-validatie (opnieuw : rekenintensief!)
metrieken gebruikt tot dusver :
voor classificatie : accuracy : tel het aantal juist geclassificeerde data-elementen in de testset tov de grootte van de testset
voor regressie : MSE : mean-squared-error : bereken het verschil tussen voorspelde en werkelijke waarde, kwadrateer deze fouten en neem hier een gemiddelde van
Er zijn echter nog veel metrieken die gebruikt kunnen worden en beter geschikt zijn afhankelijk van de applicatie.
We spreken hier meestal over een $positieve$ versus een $negatieve$ klasse.
Zijn we alleen geĆÆnteresseerd in het aantal fouten (= accuracy) dat het geleerd model maakt?
Bvb. een zieke patiƫnt die toch als gezond geclassificeerd wordt is een grotere fout dan een gezonde patiƫnt die doorverwezen wordt naar extra testen omdat hij foutief als ziek geclassificeerd wordt. M.a.w de vals positieven zijn minder erg dan de vals negatieven in dit geval !
Een reƫle (business) targetfunctie zou kunnen zijn : minimaliseer het aantal overlijdens. Het meten van de accuracy zal hier dan geen goede evaluator zijn voor deze target. Eerder moet het aantal vals negatieven geminimaliseerd worden. Wat in dit geval veel specifieker is.
Dit wil zeggen dat er veel meer data voorhanden is van de ene klasse dan van de andere klasse. Stel dat ik heel veel voorbeelden heb van de negatieve klasse, dan zal mijn model : Klassificeer steeds als "NO" best een goede score behalen !
Dus als iemand beweert dat zijn model een test accuracy van $90\%$ haalt wil dat eigenlijk helemaal niet zo veel zeggen als je de eigenschappen van de dataset niet kent!
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_digits
digits = load_digits()
y = digits.target == 9 # the target becomes binary, classify a number as 9 or not
# this makes the dataset unbalanced : there will be approximately 9 more false than true examples in the dataset
X_train, X_test, y_train, y_test = train_test_split(digits.data, y, random_state=0)
import numpy as np
from sklearn.dummy import DummyClassifier
#the dummy classifier wil always predict false = not 9
dummy_majority = DummyClassifier(strategy='most_frequent').fit(X_train, y_train)
pred_most_frequent = dummy_majority.predict(X_test)
print("Test score: {:.2f}".format(dummy_majority.score(X_test, y_test)))
Test score: 0.90
from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(max_depth=2).fit(X_train, y_train)
pred_tree = tree.predict(X_test)
print("Test score: {:.2f}".format(tree.score(X_test, y_test)))
Test score: 0.92
from sklearn.linear_model import LogisticRegression
random = DummyClassifier().fit(X_train, y_train)
pred_dummy = random.predict(X_test)
print("dummy score: {:.2f}".format(random.score(X_test, y_test)))
logreg = LogisticRegression(C=0.1, solver = 'liblinear').fit(X_train, y_train)
pred_logreg = logreg.predict(X_test)
print("logreg score: {:.2f}".format(logreg.score(X_test, y_test)))
dummy score: 0.90 logreg score: 0.98
from sklearn.metrics import confusion_matrix
print("Confusion matrix Most frequent :\n{}".format(confusion_matrix(y_test, pred_most_frequent)))
print("Confusion matrix Random dummy :\n{}".format(confusion_matrix(y_test, pred_dummy)))
print("Confusion matrix Decision Tree :\n{}".format(confusion_matrix(y_test, pred_tree)))
print("Confusion matrix LogistRegression :\n{}".format(confusion_matrix(y_test, pred_logreg)))
Confusion matrix Most frequent : [[403 0] [ 47 0]] Confusion matrix Random dummy : [[403 0] [ 47 0]] Confusion matrix Decision Tree : [[390 13] [ 24 23]] Confusion matrix LogistRegression : [[401 2] [ 8 39]]
Hoe lees je de confusion matrix die $sklearn.metrics$ genegereert : (matrix voor het Logistic Regression model)
beperk het aantal valse positieven : verhoog precision
\begin{equation} \text{Precision} = \frac{\text{TP}}{\text{TP} + \text{FP}} \end{equation}beperk het aantal valse negatieven : verhoog recall (ook sensitivity genoemd)
\begin{equation} \text{Recall} = \frac{\text{TP}}{\text{TP} + \text{FN}} \end{equation}balanceer de trade-off tussen precision en recall:
\begin{equation} \text{F-score} = 2 \cdot \frac{\text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}} \end{equation}Al deze scores zijn terug te vinden in sklearn.metrics
Er is een trade-off tussen het optimizeren van recall en precision. Stel je voorspelt alle samples als postief - dan bekom je optimale recall, maar een lage precision want er er zullen veel vals positieven zijn. Omgekeerd, stel er zijn geen valse positieven, dan is de precision perfect, maar dan kunnen veel valse negatieven toch een hele kleine recall geven.
Deze ROC (receiver operating characteristic curve) vertelt hoe goed een binair classificatiemodel een onderscheid kan maken tussen klassen gegeven een treshold. Hoe hoger de AUC (area under the curve) hoe beter het model de 2 klassen van het binaire classificatieprobleem kan onderscheiden.
De ROC curve is een plot waarbij de FPR (false positive rate = aandeel valse positieve tov totaal aantal negatieven - the probability of false alarm) uitgezet wordt t.o.v. de TPR (true positive rate = recall) als je de threshold waarmee een geleerd model een beslissing tot positieve of negatieve classificatie neemt laat variƫren </font>
Een model dat een classificatieprobleem geleerd heeft zal voor elk datapunt steeds een kans uitrekenen waarmee het kan beslissen of een datapunt positief of negatief is. Hoe groter of lager die kansen hoe zekerder het algoritme is. Met een probabiliteit van $0.5$ is het moeilijk om een beslissing te nemen. Standaard ligt er een treshold op $0.5$. Is de kans groter dan deze waarde wordt het punt positief geclassificeerd.
We kunnen de kansen die het model bekomt voor alle punten uit een validatieset uitzetten in een histogram. Op de x-as staat de voorspelde kans (getal tussen 0 en 1), op de y-as lees je een aantal. We maken een verdeling van de positieve punten uit de validatieset in rood, en een verdeling van de negatieve punten in groen. In onderstaande situatie blijkt dat alle postive punten telkens een kans groter dan $0.5$ voorspeld krijgen, en alle negatieve punten een kans onder $0.5$ krijgen. Alles zal dus perfect voorspeld worden.
Een ROC curve zal nu de threshold laten variƫren van 0 tot 1 en telkens de FPR en TPR uitzetten. Voor een perfect voorspeller geeft dit een perfecte hoek in de curve, de AUC is maximaal. </font>
meestal overlappen die verdelingen echter: in onderstaande bijvoorbeeld zijn er behoorlijk wat datapunten waarvoor het model een probabiliteit van 0.5 berekende, maar de helft daarvan bleek positief en de andere helft negatief. Door de threshold op een positive klassificatie te verhogen naar 0.7 verkleint het aantal FP, maar vergroot het aantal FN en omgekeerd wanneer we de threshold zouden verlagen naar $0.3$. De ROC curve ontstaat door elke threshold te bekijken en telkens de TPR uit te zetten t.o.v. de FPR. Merk op een accuracy wordt steeds berekend voor 1 threshold waarde ($0.5$)
De keuze voor een treshold van $0.6$ zal hier de TPR redelijk houden terwijl de FPR toch $0$ blijft
Link met de confusion matrix
De AUC is een getal tussen $0$ en $1$ dat samenvat hoe het classificatiemodel presteert over de verschillende thresholds. Je kan het ook bekijken als de kans waarmee een classificatiemodel een ad random gekozen positief voorbeeld hoger zou inschatten als een ad random gekozen negatief voorbeeld.
Stel dat $AUC = 0.75$, dan zal het model met $75\%$ zekerheid een voorbeeld van de positieve klasse hoger gaan ranken dan eentje van de negatieve klasse.
In python : gebruik de scoring parameter om de metriek te wijzigen van accuracy naar bvb AUC