一、学习曲线与高偏差、高方差
学习曲线描述了训练集样本数与训练误差、在验证集误差的关系。通过学习曲线,我们能够清楚地知道我们的模型是高偏差还是高方差,即欠拟合还是过拟合。
下图为高偏差情况:
在高偏差情况下,可以看出训练误差
J
t
r
a
i
n
J_{train}
Jtrain以及验证集误差
J
c
v
J_{cv}
Jcv都很大,且当我们横坐标的训练集样本数增大到一定程度,训练误差和验证误差都不在增加和减少。这很好理解,因为高误差意味着我们的模型不能很好描述所给训练集样本的情况。但我们的训练集样本增多到一定程度,由于模型的限制,训练样本的增加并不能再改善我们模型的拟合情况,所以最终
J
t
r
a
i
n
J_{train}
Jtrain和
J
c
v
J_{cv}
Jcv都会趋于稳定。
高方差情况如下图:
在高方差情况下,
J
t
r
a
i
n
J_{train}
Jtrain会小,
J
c
v
J_{cv}
Jcv很大,且当训练样本数增加到很大的程度,
J
t
r
a
i
n
J_{train}
Jtrain和
J
c
v
J_{cv}
Jcv才会趋于稳定。这也很好理解:高方差情况对应着过拟合,过拟合情况下训练模型往往在训练集上拟合情况很好,但是模型把训练集的样本的特征当做所有样本的特征,所以往往过拟合情况在验证集上误差较大。当样本数
m
m
m很大时,模型把个别样本特征当做所有样本特征情况就会有所缓解。所以随着
m
m
m的增大,
J
t
r
a
i
n
J_{train}
Jtrain增大,
J
c
v
J_{cv}
Jcv减小。
二、学习曲线的应用
下面我们应用绘制学习曲线来帮助我们选择模型参数
1、先导入需要使用的模块:
import numpy as np
from scipy.io import loadmat
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
from scipy.optimize import minimize
2、读取并查看处理数据:
data = loadmat('F:\\MachineLearning\data\ex5data1.mat')
print(data)
我们可以发现data中包含训练集X
,训练集标记y
,测试集Xtest
,测试集标记ytest
,验证集Xval
以及验证集标记yval
。
我们分别取出x,y,Xtest,ytest,Xval以及yval。并为了之后的计算方便,把标记全部由矩阵形式改为数组形式:
X=data['X']
y=data['y']
Xval = data['Xval']
yval = data['yval']
Xtest = data['Xtest']
ytest = data['ytest']
X1=np.ravel(X)
y1=np.ravel(y)
m_Xval,d_Xval = Xval.shape
Xval1 = np.ravel(Xval)
yval1 = np.ravel(yval)
Xtest1 = np.ravel(Xtest)
ytest1 = np.ravel(ytest)
m,d = X.shape
m_test,d_test=Xtest.shape
下面我们查看训练集样本样本分布:
#查看训练集
df = pd.DataFrame({'water_level':X1,'flow':y1})
sns.lmplot('water_level','flow',data=df,fit_reg=False)
plt.show()
得到图:
由于常数项的存在,我们需要对X,Xtest,Xval添加全为1的列:
X1=X
Xval1=Xval
Xtest1=Xtest
X=np.insert(X,0,values=np.ones(m),axis=1)
Xval=np.insert(Xval,0,values=np.ones(m_Xval),axis=1)
Xtest=np.insert(Xtest,0,values=np.ones(m_test),axis=1)
3、用线性拟合数据
首先我们是正则化的代价函数和不带正则化的代价函数,这里之所以要有一个带正则化和不带正则化,是因为在训练参数是我们需要使用的是带正则化的代价函数和梯度函数,然而在绘制学习曲线时,我们需要使用不带正则化的代价函数:
def regularized_cost(theta,X,y,regularized_param):
m = X.shape[0]
delta= X @ theta - y
cost_term = (delta.T @ delta) / (2*m)
regularized_term = (theta[1:].T @ theta[1:]) * (regularized_param/(2*m))
cost = regularized_term + cost_term
return cost
def cost(theta,X,y):
m = X.shape[0]
delta= X @ theta - y
total_cost = (delta.T @ delta) / (2*m)
return total_cost
然后是带正则化的梯度函数:
def regularized_gradient(theta,X,y1,regularized_param):
m = X.shape[0]
delta = X @ theta - y1
gradient_term = (X.T @ delta) / m
theta1 = np.insert(theta[1:],0,values=0)
regularized_term = (regularized_param / m) * theta1
gradient=gradient_term + regularized_term
return gradient
用scipy
中的minimize
函数去求参数,因为这里我们的数据只有一个属性,如果加上常数项对应的参数,我们应该得到的是二维数组:
#拟合数据
theta=np.ones(2)
res = minimize(fun=regularized_cost,x0=theta,args=(X,y1,1),method='Newton-CG',\
jac=regularized_gradient,options={'disp':True})
x=res.x
我们绘制曲线图查看模型与数据的拟合情况:
#对比线性拟合情况
plt.scatter(X1,y1,label='Trainning Set',color='blue')
plt.plot(X1,x[1]*X1+x[0],label='Prediction',color='red')
plt.legend()
plt.show()
得到图:
从图上可以看出我们的模型过于简单不能很好的描述训练集数据的特点。下面我们看看学习曲线的情况:
#画出学习曲线
Jtraining = []
Jcv = []
for i in range(m):
res=minimize(fun=regularized_cost,x0=theta,args=(X[:i+1,:],y1[:i+1],1),method='Newton-CG',\
jac=regularized_gradient,options={'disp':True})
x=res.x
jtraining = cost(x,X[:i+1,:],y1[:i+1])
Jtraining.append(jtraining)
jcv = cost(x,Xval,yval1)
Jcv.append(jcv)
plt.plot(np.arange(1,m+1),Jcv,label='Jcv',color='red')
plt.plot(np.arange(1,m+1),Jtraining,label='Jtraining',color='blue')
plt.legend()
plt.show()
这里使用迭代,从i=0逐渐扩大i,取X[:i+1,:]
,这就相当与训练集样本数从1逐渐扩大,然后把不同样本集样本数下的训练集代价和验证集代价分别存于列表中。这样我们可以到学习曲线图:
我们可以看到
J
t
r
a
i
n
J_{train}
Jtrain和
J
c
v
J_{cv}
Jcv都比较大,而且最终趋于稳定,所以对应与高偏差情况,即模型太简单不能很好描述我们的数据分布特点。下面我们考虑用更为复杂的多项式取拟合数据。
4、用多项式拟合数据
由于我们用多项式拟合数据时,会把我们原有数据进行高次计算形成新的属性,这就导致了我们的不同的属性之间的范围不同,从未导致各属性贡献的不平衡,所以我们要对我们的数据进行归一化,归一化的公式为:
x
i
j
,
N
o
r
m
a
l
=
x
i
j
−
μ
j
δ
j
x_{ij,Normal}=\frac{x_{ij}-μ_{j}}{δ_{j}}
xij,Normal=δjxij−μj
其中
x
N
o
r
m
a
l
,
i
j
x_{Normal,ij}
xNormal,ij代表i个样本第j个属性归一化后的取值,
x
i
j
x_{ij}
xij代表i个样本第j个属性原始取值,
μ
j
μ_{j}
μj代表所有样本在第j个属性上的平均值,
δ
j
δ_{j}
δj代表所有样本在第j个属性上的标准差。这样我们就有归一化函数:
def normalize(x):
x1=(x-np.mean(x)) / (np.std(x))
return x1
接下来我们需要把我们原来一维的数据通过高次幂处理,拓展成八维数据,然后归一化,最后添加全为1的列,这样数据处理函数为:
def prepare_data(X,n):
m = X.shape[0]
prepared_data = np.zeros((m,n))
for i in range(n):
x = X[:,0]**(i+1)
prepared_data[:,i] = (x-np.mean(x)) / (np.std(x))
prepared_data = np.insert(prepared_data,0,values=np.ones(m),axis=1)
return prepared_data
将我们的原始数据利用此函数处理:
prepared_data = prepare_data(X1,8)
prepared_xval = prepare_data(Xval1,8)
接着是绘制学习曲线的函数,其原理和一维情况相同:
def plot_learning_curve(prepared_data,y1,prepared_xval,yval1,regularized_param):
m,d = prepared_data.shape
theta8=np.ones(d)
Jtraining=[]
Jcv=[]
m = prepared_data.shape[0]
for i in range(m):
theta8=np.ones(9)
res=minimize(fun=regularized_cost,x0=theta8,args=(prepared_data[:i+1,:],y1[:i+1],regularized_param),\
method='TNC',jac=regularized_gradient,options={'disp':True})
x1 = res.x
jtraining = cost(x1,prepared_data[:i+1,:],y1[:i+1])
jcv = cost(x1,prepared_xval,yval1)
Jtraining.append(jtraining)
Jcv.append(jcv)
plt.plot(np.arange(1,m+1),Jtraining,label='Jtraing',color='blue')
plt.plot(np.arange(1,m+1),Jcv,label='Jcv',color='red')
plt.legend()
接着我们从0到100调整不同的正则化常数,查看学习曲线:
L=[0,0.001,0.003,0.01,0.03,0.1,0.3,1,3,10,30,100]
for l in L:
plot_learning_curve(prepared_data,y1,prepared_xval,yval1,l)
plt.show()
得到:
l=0
l=0.001
l=0.003
l=0.01
l=0.03
l=0.1
l=0.3
l=1
l=3
l=10
l=30
l=100
我们从图上可以知道如果选择l正则化常数为0.3或者1的话,
J
t
r
a
i
n
i
n
g
J_{training}
Jtraining不会太小而过拟合,
J
c
v
J_{cv}
Jcv不会太大表明在新样本上依然拥有较好的泛化能力。
我们还可以计算在测试集上的误差来选择正则化常数:
#得到测试集上的代价值
prepared_test = prepare_data(Xtest1,8)
theta8=np.ones(9)
for l in L:
test_fmin=minimize(fun=regularized_cost,x0=theta8,args=(prepared_data[:i+1,:],y1[:i+1],l),\
method='TNC',jac=regularized_gradient,options={'disp':True})
test_cost = cost(test_fmin.x,prepared_test,ytest1)
print(str(l)+' : '+str(test_cost))
得到:
0 : 9.873941227352864
0.001 : 11.008145910102536
0.003 : 11.301862220853764
0.01 : 10.988460477608943
0.03 : 10.218881836544037
0.1 : 8.953968731131386
0.3 : 7.745110198919966
1 : 7.851586049767016
3 : 11.770306579735657
10 : 26.894076292237678
30 : 52.76679321644176
100 : 79.07925641027286
我们发现在测试集上正则化常数为0.3时,测试集上的代价最小,所以我们选择正则化常数为0.3。