Key Word(s): model selection, cross validation, ridge, lasso, regularization, standarization, normalization
CS109A Introduction to Data Science
Standard Section 4: Regularization and Model Selection¶
Harvard University
Fall 2020
Instructors: Pavlos Protopapas, Kevin Rader, and Chris Tanner
Section Leaders: Marios Mattheakis, Hayden Joy
#RUN THIS CELL
import requests
from IPython.core.display import HTML
styles = requests.get("http://raw.githubusercontent.com/Harvard-IACS/2018-CS109A/master/content/styles/cs109.css").text
HTML(styles)
from IPython.display import Image
For this section, our goal is to get you familiarized with Regularization in Multiple Linear Regression and to start thinking about Model and Hyper-Parameter Selection.
Specifically, we will:
- Load in the King County House Price Dataset
- Perform some basic EDA
- Split the data up into a training, validation, and test set (we'll see why we need a validation set)
- Scale the variables (by standardizing or normalizing them) and seeing why we need to do this
- Make our multiple & polynomial regression models (like we did in the previous section)
- Learn what regularization is and how it can help
- Understand ridge and lasso regression
- Get an introduction to cross-validation using RidgeCV and LassoCV
# Data and Stats packages
import numpy as np
import pandas as pd
pd.set_option('max_columns', 200)
# Visualization packages
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
import warnings
warnings.filterwarnings("ignore")
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn import preprocessing
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
#from prettytable import PrettyTable
EDA: House Prices Data From Kaggle¶
For our dataset, we'll be using the house price dataset from King County, WA. The dataset is from Kaggle.
The task is to build a regression model to predict the price, based on different attributes. First, let's do some EDA.
# Load the dataset
house_df = pd.read_csv('../data/kc_house_data.csv')
house_df = house_df.sample(frac=1, random_state=42)[0:4000]
print(house_df.shape)
print(house_df.dtypes)
house_df.head()
Now let's check for null values and look at the datatypes within the dataset.
house_df.info()
house_df.describe()
Let's choose a subset of columns here. NOTE: The way I'm selecting columns here is not principled and is just for convenience. In your homework assignments (and in real life), we expect you to choose columns more rigorously.
bedrooms
bathrooms
sqft_living
sqft_lot
floors
sqft_above
sqft_basement
lat
long
price
: Our response variable
cols_of_interest = ['bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors', 'sqft_above', 'sqft_basement',
'lat', 'long', 'price']
house_df = house_df[cols_of_interest]
# Convert house price to 1000s of dollars
house_df['price'] = house_df['price']/1000
Let's see how the response variable (price
) is distributed
fig, ax = plt.subplots(figsize=(12,5))
ax.hist(house_df['price'], bins=100)
ax.set_title('Histogram of house price (in 1000s of dollars)');
# This takes a bit of time but is worth it!!
sns.pairplot(house_df);
Train-Validation-Test Split¶
Up until this point, we have only had a train-test split. Why are we introducing a validation set? What's the point?
This is the general idea:
- Training Set: Data you have seen. You train different types of models with various different hyper-parameters and regularization parameters on this data.
- Validation Set: Used to compare different models. We use this step to tune our hyper-parameters i.e. find the optimal set of hyper-parameters (such as $k$ for k-NN or our $\beta_i$ values or number of degrees of our polynomial for linear regression). Pick your best model here.
- Test Set: Using the best model from the previous step, simply report the score e.g. $R^2$ score, MSE or any metric that you care about, of that model on your test set. DON'T TUNE YOUR PARAMETERS HERE!. Why, I hear you ask? Because we want to know how our model might do on data it hasn't seen before. We don't have access to this data (because it may not exist yet) but the test set, which we haven't seen or touched so far, is a good way to mimic this new data.
Let's do 60% train, 20% validation, 20% test for this dataset.
from sklearn.model_selection import train_test_split
# first split the data into a train-test split and don't touch the test set yet
train_df, test_df = train_test_split(house_df, test_size=0.2, random_state=42)
# next, split the training set into a train-validation split
# the test-size is 0.25 since we are splitting 80% of the data into 20% and 60% overall
train_df, val_df = train_test_split(train_df, test_size=0.25, random_state=42)
print('Train Set: {0:0.2f}%'.format(100*train_df.size/house_df.size))
print('Validation Set: {0:0.2f}%'.format(100*val_df.size/house_df.size))
print('Test Set: {0:0.2f}%'.format(100*test_df.size/house_df.size))
Modeling¶
In the last section, we went over the mechanics of Multiple Linear Regression and created models that had interaction terms and polynomial terms. Specifically, we dealt with the following sorts of models.
$$ y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \dots + \beta_M x_M + \epsilon $$Let's adopt a similar process here and get a few different models.
Creating a Design Matrix¶
From our model setup in the equation in the previous section, we obtain the following:
$$ Y = \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_n \end{bmatrix}, \quad X = \begin{bmatrix} x_{1} \\ x_{2} \\ \vdots \\ x_{n} \\ \end{bmatrix} = \begin{bmatrix} x_{1,1} & x_{1,2} & \dots & x_{1,M} \\ x_{2,1} & x_{2,2} & \dots & x_{2,M} \\ \vdots & \vdots & \ddots & \vdots \\ x_{n,1} & x_{n,2} & \dots & x_{n,M} \\ \end{bmatrix}, \quad \beta = \begin{bmatrix} \beta_1 \\ \beta_2 \\ \vdots \\ \beta_M \end{bmatrix}, \quad \epsilon = \begin{bmatrix} \epsilon_1 \\ \epsilon_2 \\ \vdots \\ \epsilon_n \end{bmatrix}, $$$X$ is an n$\times$M matrix: this is our design matrix (and ${x_1}$ is a vector consisting of the values of the predictors for the first data point), $\beta$ is an M-dimensional vector (an M$\times$1 matrix), and $Y$ is an n-dimensional vector (an n$\times$1 matrix). In addition, we know that $\epsilon$ is an n-dimensional vector (an n$\times$1 matrix).
We have $M$ predictors and $n$ data points.
X = train_df[cols_of_interest]
y = train_df['price']
print(X.shape)
print(y.shape)
Scaling our Design Matrix¶
Warm-Up Exercise¶
$$\text{Euclidean Distance} : \sqrt{ (X_{1A}-X_{1B})^2 + (X_{2A} - X_{2B})^2 }$$
Warm-Up Exercise: for which of the following do the units of the predictors matter (e.g., trip length in minutes vs seconds; temperature in F or C)? A similar question would be: for which of these models do the magnitudes of values taken by different predictors matter?
(We will go over Ridge and Lasso Regression in greater detail later)
- k-NN (Nearest Neighbors regression)
- Linear regression
- Lasso regression
- Ridge regression
Solutions
- kNN: yes. Scaling affects distance metric, which determines what "neighbor" means
- Linear regression: no. Multiply predictor by $c$ -> divide coef by $c$.
- Lasso: yes: If we divided coef by $c$, then corresponding penalty term is also divided by $c$.
- Ridge: yes: Same as Lasso, except penalty divided by $c^2$.
Standard Scaler (Standardization)¶
Here's the scikit-learn implementation of the standard scaler. What is it doing though? Hint: you may have seen this in STAT 110 or another statistics course multiple times.
$$ z = \frac{x-\mu}{\sigma} $$In the above setup:
- $z$ is the standardized variable
- $x$ is the variable before standardization
- $\mu$ is the mean of the variable before standardization
- $\sigma$ is the standard deviation of the variable before standardization
Let's see an example of how this works:
from sklearn.preprocessing import StandardScaler
x = house_df['sqft_living']
mu = x.mean()
sigma = x.std()
z = (x-mu)/sigma
# reshaping x to be a n by 1 matrix since that's how scikit learn likes data for standardization
x_reshaped = np.array(x).reshape(-1,1)
z_sklearn = StandardScaler().fit_transform(x_reshaped)
# Plotting the histogram of the variable before standardization
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(24,5))
ax = ax.ravel()
ax[0].hist(x, bins=100)
ax[0].set_title('Histogram of sqft_living before standardization')
ax[1].hist(z, bins=100)
ax[1].set_title('Manually standardizing sqft_living')
ax[2].hist(z_sklearn, bins=100)
ax[2].set_title('Standardizing sqft_living using scikit learn');
# making things a dataframe to check if they work
pd.DataFrame({'x': x, 'z_manual': z, 'z_sklearn': z_sklearn.flatten()}).describe()
Min-Max Scaler (Normalization)¶
Here's the scikit-learn implementation of the standard scaler. What is it doing though?
$$ x_{new} = \frac{x-x_{min}}{x_{max}-x_{min}} $$In the above setup:
- $x_{new}$ is the normalized variable
- $x$ is the variable before normalized
- $x_{max}$ is the max value of the variable before normalization
- $x_{min}$ is the min value of the variable before normalization
Let's see an example of how this works:
from sklearn.preprocessing import MinMaxScaler
x = house_df['sqft_living']
x_new = (x-x.min())/(x.max()-x.min())
# reshaping x to be a n by 1 matrix since that's how scikit learn likes data for normalization
x_reshaped = np.array(x).reshape(-1,1)
x_new_sklearn = MinMaxScaler().fit_transform(x_reshaped)
# Plotting the histogram of the variable before normalization
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(24,5))
ax = ax.ravel()
ax[0].hist(x, bins=100)
ax[0].set_title('Histogram of sqft_living before normalization')
ax[1].hist(x_new, bins=100)
ax[1].set_title('Manually normalizing sqft_living')
ax[2].hist(x_new_sklearn, bins=100)
ax[2].set_title('Normalizing sqft_living using scikit learn');
# making things a dataframe to check if they work
pd.DataFrame({'x': x, 'x_new_manual': x_new, 'x_new_sklearn': x_new_sklearn.flatten()}).describe()
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(16,5))
ax.hist((x - np.mean(x))/np.std(x), bins=100, alpha = 0.25, label = "sq feet standardized", color = "blue")
ax.hist(x_new, bins=100, label = "sq feet normalized", color = "red", alpha = .5)
ax.set_title('Histogram of sqft_living Comparing normalization and standardization')
plt.legend()
The million dollar question
Should I standardize or normalize my data? This, this and this are useful resources that I highly recommend. But in a nutshell, what they say is the following:
Pros of Normalization
- Normalization (which makes your data go from 0-1) is widely used in image processing and computer vision, where pixel intensities are non-negative and are typically scaled from a 0-255 scale to a 0-1 range for a lot of different algorithms.
- Normalization is also very useful in neural networks (which we will see later in the course) as it leads to the algorithms converging faster. #next semester also we will see
- Normalization is useful when your data does not have a discernible distribution and you are not making assumptions about your data's distribution.
Pros of Standardization
- Standardization maintains outliers (do you see why?) whereas normalization makes outliers less obvious. In applications where outliers are useful, standardization should be done.
- Standardization is useful when you assume your data comes from a Gaussian distribution (or something that is approximately Gaussian).
Some General Advice
- We use the data to calculate the parameters for standardization ($\mu$ and $\sigma$) and for normalization ($x_{min}$ and $x_{max}$). Make sure these parameters are learned on the training set i.e use the training set parameters even when normalizing/standardizing the test set. In sklearn terms, fit your scaler on the training set and use the scaler to transform your test set and validation set (don't re-fit your scaler on test set data!).
- The point of standardization and normalization is to make your variables take on a more manageable scale. You should ideally standardize or normalize all your variables at the same time.
- Standardization and normalization is not always needed and is not an automatic thing you have to do on any data science homework!! Do so sparingly and try to justify why this is needed.
Interpreting Coefficients
A great quote from here
[Standardization] makes it so the intercept term is interpreted as the expected value of 𝑌𝑖 when the predictor values are set to their means. Otherwise, the intercept is interpreted as the expected value of 𝑌𝑖 when the predictors are set to 0, which may not be a realistic or interpretable situation (e.g. what if the predictors were height and weight?)
Standardizing our Design Matrix¶
features = ['bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors', 'sqft_above', 'sqft_basement',
'lat', 'long']
X_train = train_df[features]
y_train = np.array(train_df['price']).reshape(-1,1)
X_val = val_df[features]
y_val = np.array(val_df['price']).reshape(-1,1)
X_test = test_df[features]
y_test = np.array(test_df['price']).reshape(-1,1)
scaler = StandardScaler().fit(X_train)
# This converts our matrices into numpy matrices
X_train_t = scaler.transform(X_train)
X_val_t = scaler.transform(X_val)
X_test_t = scaler.transform(X_test)
# Making the numpy matrices pandas dataframes
X_train_df = pd.DataFrame(X_train_t, columns=features)
X_val_df = pd.DataFrame(X_val_t, columns=features)
X_test_df = pd.DataFrame(X_test_t, columns=features)
display(X_train_df.describe())
display(X_val_df.describe())
display(X_test_df.describe())
scaler = StandardScaler().fit(y_train)
y_train = scaler.transform(y_train)
y_val = scaler.transform(y_val)
y_test = scaler.transform(y_test)
One-Degree Polynomial Model¶
import statsmodels.api as sm
from statsmodels.regression.linear_model import OLS
model_1 = OLS(np.array(y_train).reshape(-1,1), sm.add_constant(X_train_df)).fit()
model_1.summary()
Two-Degree Polynomial Model¶
def add_square_terms(df):
df = df.copy()
cols = df.columns.copy()
for col in cols:
df['{}^2'.format(col)] = df[col]**2
return df
X_train_df_2 = add_square_terms(X_train)
X_val_df_2 = add_square_terms(X_val)
# Standardizing our added coefficients
cols = X_train_df_2.columns
scaler = StandardScaler().fit(X_train_df_2)
X_train_df_2 = pd.DataFrame(scaler.transform(X_train_df_2), columns=cols)
X_val_df_2 = pd.DataFrame(scaler.transform(X_val_df_2), columns=cols)
print(X_train_df.shape, X_train_df_2.shape)
# Also check using the describe() function that the mean and standard deviations are the way we want them
X_train_df_2.head()
model_2 = OLS(np.array(y_train).reshape(-1,1), sm.add_constant(X_train_df_2)).fit()
model_2.summary()
Three-Degree Polynomial Model¶
# generalizing our function from above
def add_square_and_cube_terms(df):
df = df.copy()
cols = df.columns.copy()
for col in cols:
df['{}^2'.format(col)] = df[col]**2
df['{}^3'.format(col)] = df[col]**3
return df
X_train_df_3 = add_square_and_cube_terms(X_train_df)
X_val_df_3 = add_square_and_cube_terms(X_val_df)
# Standardizing our added coefficients
cols = X_train_df_3.columns
scaler = StandardScaler().fit(X_train_df_3)
X_train_df_3 = pd.DataFrame(scaler.transform(X_train_df_3), columns=cols)
X_val_df_3 = pd.DataFrame(scaler.transform(X_val_df_3), columns=cols)
print(X_train_df.shape, X_train_df_3.shape)
# Also check using the describe() function that the mean and standard deviations are the way we want them
X_train_df_3.head()
model_3 = OLS(np.array(y_train).reshape(-1,1), sm.add_constant(X_train_df_3)).fit()
model_3.summary()
N-Degree Polynomial Model¶
# generalizing our function from above
def add_higher_order_polynomial_terms(df, N=7):
df = df.copy()
cols = df.columns.copy()
for col in cols:
for i in range(2, N+1):
df['{}^{}'.format(col, i)] = df[col]**i
return df
N = 6
X_train_df_N = add_higher_order_polynomial_terms(X_train_df,N)
X_val_df_N = add_higher_order_polynomial_terms(X_val_df,N)
# Standardizing our added coefficients
cols = X_train_df_N.columns
scaler = StandardScaler().fit(X_train_df_N)
X_train_df_N = pd.DataFrame(scaler.transform(X_train_df_N), columns=cols)
X_val_df_N = pd.DataFrame(scaler.transform(X_val_df_N), columns=cols)
print(X_train_df.shape, X_train_df_N.shape)
# Also check using the describe() function that the mean and standard deviations are the way we want them
X_train_df_N.head()
model_N = OLS(np.array(y_train).reshape(-1,1), sm.add_constant(X_train_df_N)).fit()
model_N.summary()
You can also create a model with interaction terms or any other higher order polynomial term of your choice. Note: Can you see how creating a function that takes in a dataframe and a degree and creates polynomial terms up until that degree can be useful? This is what we have you do in your homework!
# Don't change this cell
df1 = pd.read_csv("../data/bo_room2.csv")
#importing the custom make_plots function:
from make_plots import make_plots
make_plots_ = make_plots.make_plots
#split the data
train_, test = train_test_split(df1, train_size=0.8, random_state = 42)
train, val = train_test_split(train_, train_size=0.8, random_state = 42)
#sort the data
train = train.sort_values(by = ["x1"])
val = val.sort_values(by = ["x1"])
test = test.sort_values(by = ["x1"])
display(train_.head());
Here we examine a synthetic dataset to motivate both standardization and regularization.¶
1) First simply run the make_plots function line below and answer the questions in discussion with your classmates.
2) Create standardized training, validation and test datasets and then pass the modified dataframes to the make_plots function.
3) Comment on what you observe by answering the questions below.
1) Run the cell below and talk with your breakout room members and discuss the patterns we see in the MSE and $R^2$ plots. Also comment on the size of the beta coefficients. Do they seem large or small to you?¶
#run make_plots on the data, which will make the plots of the various polynomial regressions and save the r2 of the best model.
best_r2 = make_plots_(train, val, test, max_degree = 12)
print("best r2: {}".format(best_r2))
#please note that the best R^2 reported is based on the moodel chosen from the validation set but
# reported on the test set.
#The faint red lines are the different models being fit for each degree polynomial regression.
2) Now standardize the data on the training set and pass the standardized training, validation, and test sets to the make_plots function.¶
#hint use the fit and transform calls with the StandardScaler class from sklearn.
scaler = #TODO
#assign the standardized datasets here.
train_standard = #TODO
val_standard = #TODO
test_standard = #TODO
# %load '../solutions/solution1.py'
3) Run the make_plots function again by running the cell below then answer the following questions:¶
1) What degree would you select for a polynomial model based on the $R^2$ plot?
2) Did the $R^2$ and MSE plots change after standardization?
3) Did the values of the beta coefficients change?
train_standard= pd.DataFrame(train_standard)
val_standard= pd.DataFrame(val_standard)
test_standard = pd.DataFrame(test_standard)
train_standard.columns = ["x1","x2","x3","x4", "y"]
val_standard.columns = ["x1","x2","x3","x4","y"]
test_standard.columns = ["x1","x2","x3","x4","y"]
best_OLS_r2 = make_plots_(train_standard, val_standard, test_standard, max_degree = 12)
best_OLS_r2
Bonus question:¶
when we split into X_train and y_train as Pavlos does in the class exercises, why is the X capitalized?
x = [1,2,3,N]
models = [model_1, model_2, model_3, model_N]
X_trains = [X_train_df, X_train_df_2, X_train_df_3, X_train_df_N]
X_vals = [X_val_df, X_val_df_2, X_val_df_3, X_val_df_N]
r2_train = []
r2_val = []
for i,model in enumerate(models):
y_pred_tra = model.predict(sm.add_constant(X_trains[i]))
y_pred_val = model.predict(sm.add_constant(X_vals[i]))
r2_train.append(r2_score(y_train, y_pred_tra))
r2_val.append(r2_score(y_val, y_pred_val))
fig, ax = plt.subplots(figsize=(8,6))
ax.plot(x, r2_train, 'o-', label=r'Training $R^2$')
ax.plot(x, r2_val, 'o-', label=r'Validation $R^2$')
ax.set_xlabel('Number of degree of polynomial')
ax.set_ylabel(r'$R^2$ score')
ax.set_title(r'$R^2$ score vs polynomial degree')
ax.legend();
We notice a big difference between training and validation R^2 scores: seems like we are overfitting. Introducing: regularization.
What about Multicollinearity?¶
There's seemingly a lot of multicollinearity in the data. Take a look at this warning that we got when showing our summary for our polynomial models:
What is multicollinearity? Why do we have it in our dataset? Why is this a problem?
Does regularization help solve the issue of multicollinearity?
What does Regularization help with?¶
We have some pretty large and extreme coefficient values in our most recent models. These coefficient values also have very high variance. We can also clearly see some overfitting to the training set. In order to reduce the coefficients of our parameters, we can introduce a penalty term that penalizes some of these extreme coefficient values. Specifically, regularization helps us:
- Avoid overfitting. Reduce features that have weak predictive power.
- Discourage the use of a model that is too complex
Big Idea: Reduce Variance by Increasing Bias¶
Image Source: here
Ridge Regression¶
Ridge Regression is one such form of regularization. In practice, the ridge estimator reduces the complexity of the model by shrinking the coefficients, but it doesn’t nullify them. We control the amount of regularization using a parameter $\lambda$. NOTE: sklearn's ridge regression package represents this $\lambda$ using a parameter alpha. In Ridge Regression, the penalty term is proportional to the L2-norm of the coefficients.
Lasso Regression¶
Lasso Regression is another form of regularization. Again, we control the amount of regularization using a parameter $\lambda$. NOTE: sklearn's lasso regression package represents this $\lambda$ using a parameter alpha. In Lasso Regression, the penalty term is proportional to the L1-norm of the coefficients.
Some Differences between Ridge and Lasso Regression¶
- Since Lasso regression tend to produce zero estimates for a number of model parameters - we say that Lasso solutions are sparse - we consider to be a method for variable selection.
- In Ridge Regression, the penalty term is proportional to the L2-norm of the coefficients whereas in Lasso Regression, the penalty term is proportional to the L1-norm of the coefficients.
- Ridge Regression has a closed form solution! Lasso Regression does not. We often have to solve this iteratively. In the sklearn package for Lasso regression, there is a parameter called
max_iter
that determines how many iterations we perform.
Why Standardizing Variables was not a waste of time¶
Lasso regression puts constraints on the size of the coefficients associated to each variable. However, this value will depend on the magnitude of each variable. It is therefore necessary to standardize the variables.
Let's use Ridge and Lasso to regularize our degree N polynomial¶
Exercise: Play around with different values of alpha. Notice the new $R^2$ value and also the range of values that the predictors take in the plot.
from sklearn.linear_model import Ridge
# some values you can try out: 0.01, 0.1, 0.5, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 10000
alpha = 100
ridge_model = Ridge(alpha=alpha).fit(X_train_df_N, y_train)
print('R squared score for our original OLS model: {}'.format(r2_val[-1]))
print('R squared score for Ridge with alpha={}: {}'.format(alpha, ridge_model.score(X_val_df_N,y_val)))
fig, ax = plt.subplots(figsize=(18,8), ncols=2)
ax = ax.ravel()
ax[0].hist(model_N.params, bins=10, alpha=0.5)
ax[0].set_title('Histogram of predictor values for Original model with N: {}'.format(N))
ax[0].set_xlabel('Predictor values')
ax[0].set_ylabel('Frequency')
ax[1].hist(ridge_model.coef_.flatten(), bins=20, alpha=0.5)
ax[1].set_title('Histogram of predictor values for Ridge Model with alpha: {}'.format(alpha))
ax[1].set_xlabel('Predictor values')
ax[1].set_ylabel('Frequency');
from sklearn.linear_model import Lasso
# some values you can try out: 0.00001, 0.0001, 0.001, 0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10, 20
alpha = 0.01
lasso_model = Lasso(alpha=alpha, max_iter = 1000).fit(X_train_df_N, y_train)
print('R squared score for our original OLS model: {}'.format(r2_val[-1]))
print('R squared score for Lasso with alpha={}: {}'.format(alpha, lasso_model.score(X_val_df_N,y_val)))
fig, ax = plt.subplots(figsize=(18,8), ncols=2)
ax = ax.ravel()
ax[0].hist(model_N.params, bins=10, alpha=0.5)
ax[0].set_title('Histogram of predictor values for Original model with N: {}'.format(N))
ax[0].set_xlabel('Predictor values')
ax[0].set_ylabel('Frequency')
ax[1].hist(lasso_model.coef_.flatten(), bins=20, alpha=0.5)
ax[1].set_title('Histogram of predictor values for Lasso Model with alpha: {}'.format(alpha))
ax[1].set_xlabel('Predictor values')
ax[1].set_ylabel('Frequency');
Model Selection and Cross-Validation¶
Here's our current setup so far:
So we try out 10,000 different models on our validation set and pick the one that's the best? No! Since we could also be overfitting the validation set!
One solution to the problems raised by using a single validation set is to evaluate each model on multiple validation sets and average the validation performance. This is the essence of cross-validation!
Image source: here
from sklearn.linear_model import RidgeCV
from sklearn.linear_model import LassoCV
alphas = (0.001, 0.01, 0.1, 10, 100, 1000, 10000)
# Let us do k-fold cross validation
k = 4
fitted_ridge = RidgeCV(alphas=alphas).fit(X_train_df_N, y_train)
fitted_lasso = LassoCV(alphas=alphas).fit(X_train_df_N, y_train)
print('R^2 score for our original OLS model: {}\n'.format(r2_val[-1]))
ridge_a = fitted_ridge.alpha_
print('Best alpha for ridge: {}'.format(ridge_a))
print('R^2 score for Ridge with alpha={}: {}\n'.format(ridge_a, fitted_ridge.score(X_val_df_N,y_val)))
lasso_a = fitted_lasso.alpha_
print('Best alpha for lasso: {}'.format(lasso_a))
print('R squared score for Lasso with alpha={}: {}'.format(lasso_a, fitted_lasso.score(X_val_df_N,y_val)))
We can also look at the coefficients of our CV models.
Final Step: report the score on the test set for the model you have chosen to be the best.
Sklearn's cross_validate function:¶
You may find this useful on the homework. Say you have fit a model on the training set. You will be asked to perform cross validation and score models on the homework based on this function. Below is an example of how to do this. You will also want to choose an appropriate scoring method. Please read the documentation for this function. I've included some psuedo-code below:
loss_score = cross_validate(model, X, y, cv=5, scoring = 'r2')
from sklearn.model_selection import cross_validate
#scale the design matrix
X_df = X_train_df_N
X_df = StandardScaler().fit_transform(X_df)
y_train= StandardScaler().fit_transform(y_train)
#alpha values to optimize
alphas = [np.exp(i) for i in np.linspace(-5, 0, 25)]
sklearn_models = []
#fit our various models
for i, alpha in enumerate(alphas):
model = Lasso(alpha=alpha, max_iter = 1000).fit(X_df, y_train)
sklearn_models.append(model)
#get the cross validation scores
for i, model in enumerate(sklearn_models):
#if i == 0 initialize our lists
if not i:
r2_train = []
r2_val = []
y_pred_tra = model.predict(X_df)
r2_cv_scores = cross_validate(model, X_df, y_train, cv=5, scoring = 'r2')
r2_avg_score = np.mean(r2_cv_scores["test_score"])
r2_train.append(r2_avg_score)
#plot
fig, ax = plt.subplots(figsize=(8,6))
ax.plot(np.log10(alphas), r2_train, 'o-', label=r'CV $R^2$ score')
ax.set_xlabel('log10(alpha)')
ax.set_ylabel(r'Cross Validated $R^2$ score')
ax.set_title(r'Ridge Regression $R^2$ score vs log10(alpha)')
ax.legend()
print("Best R^2 Score: {}: ".format(np.max(r2_train)))
Breakout Room 2¶
1) Fix the fit_ridge_and_lasso_cv function
2) Play with the arguments of the preprocess and fit_ridge_and_lasso_cv helper functions to try and improve the performance on the test set.
3) Talk with your break out room members about the questions posed
df1 = pd.read_csv("../data/bo_room2.csv")
#Here are your helper functions.
def preprocess(df, standardize = False):
"""Splits the data into training and validation sets.
arguments:
df: the dataframe of training and test data you want to split.
standardize: if True returns standardized data.
"""
#split the data
train, test = train_test_split(df, train_size=0.8, random_state = 42)
#sort the data
train = train.sort_values(by = ["x1"])
test = test.sort_values(by = ["x1"])
train.describe()
X_train, y_train = train[["x1"]], train["y"]
X_test, y_test = test[["x1"]], test["y"]
X_train_N = add_higher_order_polynomial_terms(X_train, N=15)
X_test_N = add_higher_order_polynomial_terms(X_test, N=15)
if standardize:
scaler = StandardScaler().fit(X_train_N)
X_train_N = scaler.transform(X_train_N)
X_test_N = scaler.transform(X_test_N)
#"X_val" : X_val_N, "y_val" : y_val,
datasets = {"X_train": X_train_N, "y_train": y_train, "X_test" : X_test_N, "y_test": y_test}
return(datasets)
1) Fill out the missing lines in this helper function in order to perform cross validated ridge and lasso regression on the synthetic dataset.¶
def fit_ridge_and_lasso_cv(X_train, y_train, X_test, y_test, k = None, alphas = [10**7],
best_OLS_r2 = best_OLS_r2): #X_val, y_val,
""" takes in train and validation test sets and reports the best selected model using ridge and lasso regression.
Arguments:
X_train: the train design matrix
y_train: the reponse variable for the training set
X_val: the validation design matrix
y_train: the reponse variable for the validation set
k: the number of k-fold cross validation sections to be fed to Ridge and Lasso Regularization.
"""
# Let us do k-fold cross validation. make sure to provide k and alphas as arguments to both ridge and lasso.
fitted_ridge = #TODO
fitted_lasso = #TODO
print('R^2 score for our original OLS model: {}\n'.format(best_OLS_r2))
ridge_a = fitted_ridge.alpha_
ridge_score = fitted_ridge.score(X_test, y_test)
print('Best alpha for ridge: {}'.format(ridge_a))
print('R^2 score for Ridge with alpha={}: {}\n'.format(ridge_a, ridge_score))
lasso_a = fitted_lasso.alpha_
lasso_score = fitted_lasso.score(X_test, y_test)
print('Best alpha for lasso: {}'.format(lasso_a))
print('R^2 score for Lasso with alpha={}: {}'.format(lasso_a, lasso_score))
r2_df = pd.DataFrame({"OLS": best_OLS_r2, "Lasso" : lasso_score, "Ridge" : ridge_score}, index = [0])
r2_df = r2_df.melt()
r2_df.columns = ["model", "r2_Score"]
plt.title("Validation set")
sns.barplot(x = "model", y = "r2_Score", data = r2_df)
plt.show()
# %load "../solutions/solution2.py"
1) Run the cell below to see the performance of the OLS, Lasso, and Ridge models on the test set
datasets = preprocess(df1)
#Here we input arguments to a function from a dictionary using the ** syntax option.
# X_train, y_train, X_val, y_val,
fit_ridge_and_lasso_cv(**datasets)
2) How can we improve our results using ridge and lasso (ie boost the $R^2$ score?). Try to play with the arguments of the helper functions to increase the $R^2$ scores and discuss with your group why this works. Don't split the data or try to write your own functions here.
#TODO (hint copy and paste the previous cell)
# %load "../solutions/solution3.py"
3) Would we in effect be cheating if we optimize our model over k at this stage? Why?