# Cross Validation

Occasionally, our measures for model accuracy can be misleading. This typically occurs when our model fitting overly-generalizes to whatever data it was trained on, and the way we split out train/test sets don’t do a very good job of exposing the oversight.

To this end, we employ Validation Sets– data held out of our Training Sets– to approximate our Test Error.

## Sample Model

Before we get started, let’s make our work reproducible.

%pylab inline

np.random.seed(0)
Populating the interactive namespace from numpy and matplotlib


Now let’s build a model

from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split

We’re going to deliberately make a lopsided balance between how much of our data is training and how much is test.

def make_data():
X, y = make_regression(n_informative=10, n_features=20,
n_samples=1000, noise=2)

return train_test_split(X, y, test_size=.01)
train_X, test_X, train_y, test_y = make_data()
[arr.shape for arr in make_data()]
[(990, 20), (10, 20), (990,), (10,)]


And build a Decision Tree on top of it

from sklearn.tree import DecisionTreeRegressor

model = DecisionTreeRegressor()

model.fit(train_X, train_y)
DecisionTreeRegressor(criterion='mse', max_depth=None, max_features=None,
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
presort=False, random_state=None, splitter='best')


### Evaluating the Model

And if we revisit the Root Mean Squared Error as our measure of model accuracy, we see that this is perhaps a respectable level of error.

from sklearn.metrics import mean_squared_error

mse = mean_squared_error(test_y, model.predict(test_X))
rmse = np.sqrt(mse)
rmse
200.2823964921704


However, looking at the distribution of predictions vs actuals, we can see that a lot of this error comes a few points in y_train.

plt.scatter(model.predict(test_X), test_y); The model was fit to predict on a target distribution that looked basically normal

plt.hist(train_y); by the (bad) luck of sampling, the 1% of y that made up test_y didn’t really look like that at all.

plt.hist(test_y); So how do we ensure that we don’t get “gotcha’d” when the distribution/variation of the 1% we held out isn’t accurately captured by our training set?

## Cross-Validation

Cross-Validation involves a second splitting step after you’ve removed your Test data from your starting population.

Moreover, when used to direct training/hyperparameter search, withholding your data helps correct overfitting during training.

### Validation Set

And so we’ll further split our Training dataset

train_X.shape, train_y.shape
((990, 20), (990,))


A third will now be used for Validation

train_X, test_X, train_y, test_y = make_data()

train_X, validation_X, train_y, validation_y = train_test_split(train_X, train_y, test_size=.33)
train_X.shape, train_y.shape
((663, 20), (663,))

validation_X.shape, validation_y.shape
((327, 20), (327,))

test_X.shape, test_y.shape
((10, 20), (10,))


Refitting the model with the Validation records omitted

model = DecisionTreeRegressor()

model.fit(train_X, train_y)
DecisionTreeRegressor(criterion='mse', max_depth=None, max_features=None,
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
presort=False, random_state=None, splitter='best')


And our performance on the Validation set is about 60 points better than our last test!

mse = mean_squared_error(validation_y, model.predict(validation_X))
rmse = np.sqrt(mse)
rmse
143.2710672835756


Indeed, our model happens to be less overfit this time around.

mse = mean_squared_error(test_y, model.predict(test_X))
rmse = np.sqrt(mse)
rmse
168.02073456013332


But the fact that our Validation MSE wound up being lower than our Test MSE was completely arbitrary, another consequence of the random sample.

Indeed, in Chapter 5 ISL uses multiple Validation sets to determine what the appropriate Polynomial Degree is for a simulated regression problem. The True Test Error is shown in the left chart, but the right chart demonstrates that your MSE can vary wildly by the sheer luck of how you sampled your data– though generally moving the same way as the Degree increases.

from IPython.display import Image

Image('images/diff_val_sets.PNG') ### K-Fold Cross-Validation

What we want to do instead is repeat that test/train splitting process across multiple samples of data within our dataset.

We do this with K-Fold Cross Validation, which splits the dataset up into k sets, then trains on k-1 of them, testing against the holdout, ultimately returning each accuracy score.

Image('images/crossval.jpg') model = DecisionTreeRegressor()
train_X, test_X, train_y, test_y = make_data()
from sklearn.model_selection import cross_val_score

scores = cross_val_score(# what model we use
model,
# data we'll feed it
train_X,
train_y,
# how we score each split
scoring='neg_mean_squared_error',
# how many cuts to make
cv=10)

accuracies = np.sqrt(-scores)
accuracies
array([ 92.59567985, 106.70686122, 111.12034656, 100.3642714 ,
90.38853955,  94.77325883,  85.00335984,  97.94133301,
98.682085  ,  98.72319924])


As we can see, there’s still a good amount of variation depending on which slice of data we were testing against.

accuracies.std()
7.208537550908835


Therefore, we’ll average out all of the cross-validation scores to get a more stable estimate.

accuracies.mean()
97.62989345115201


Effectively, taking these k different looks at our data means that our model has been evaluated in against every point in our training set. Thus when we read an MSE of 97.63, we don’t have to worry about whether or not we scored on some fringe part of the distribution or that we overfit.

Indeed, more-sophisticated approaches to Machine Learning leverage K-fold Cross Validation as an intermediate training step, the algorithm proceeds in the direction that minimizes the Cross Validation Error, then redefines the folds to prevent overfitting.