【机器学习】模型泛化
欠拟合和过拟合
概念
欠拟合就是拟合效果非常差,比如一个一元二次方程,却用一元一次方程去拟合,这样的拟合效果肯定会很差。过拟合则是模型过于复杂,将训练数据拟合的十分到位,这样就容易拟合上许多噪音数据,这样对于训练数据他的误差会很小,但是对于预测数据即一些从来没有碰到过的数据,他的预测效果会特别差。对于未知数据的预测好坏通常叫做泛化能力。
如何判定模型是过拟合还是欠拟合
通常的办法是将训练数据分成2部分,一部分用来训练,让另一部分作为测试数据,检查他的泛化能力,如果泛化能力很差就是欠拟合,如果训练数据预测的很好,但是测试数据的精度却很差就是过拟合,我们可以通过学习曲线来将他可视化
学习曲线
学习曲线其实就是把训练数据集取前i个样本(i 从1到训练数据样本总数)把模型的训练误差与预测误差都给表示出来,从而看误差的走势以及两者误差之间的关系,会发现最后误差一般都趋于稳定,如果模型较好,两者之间的差距也比较小。
要学习的模型
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(666)
x = np.random.uniform(-3.0, 3.0, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x**2 + x + x + np.random.normal(0, 1, size=100)
学习曲线的函数
# 将数据进行分离, 以便观察模型对训练数据以及测试数据的拟合能力
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 10)
from sklearn.metrics import mean_squared_error
def plot_learning_curve(function, X_train, X_test, y_train, y_test):
train_score = []
test_score = []
for i in range(1, len(X_train)+1): # 因为前闭后开,所以+1
function.fit(X_train[:i], y_train[:i]) # 模型训练
y_train_predict = function.predict(X_train[:i])
train_score.append(mean_squared_error(y_train_predict, y_train[:i]))
y_test_preict = function.predict(X_test[:i])
test_score.append(mean_squared_error(y_test_preict, y_test[:i]))
plt.plot([i for i in range(1, len(X_train)+1)], np.sqrt(train_score), label = \'train\')
plt.plot([i for i in range(1, len(X_train)+1)], np.sqrt(test_score), label = \'test\')
plt.legend()
plt.axis([0, len(X_train)+1, 0, 4]) #显示坐标的范围,前两个为横坐标后两个为纵坐标
plt.show()
绘制各个模型的学习曲线
# 线性模型
plot_learning_curve(LinearRegression(), X_train, X_test, y_train, y_test)
# 多项式模型
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
def PolynomialRegression(degree): #为了方便传入,将Pipeline封装起来
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("lin_reg", LinearRegression())
])
poly_reg1 = PolynomialRegression(degree=2)
plot_learning_curve(poly_reg1, X_train, X_test, y_train, y_test)
poly_reg2 = PolynomialRegression(degree=26)
plot_learning_curve(poly_reg2, X_train, X_test, y_train, y_test)
验证集与交叉验证
测试数据集的意义以及问题
测试数据集其实就是为了提前评判模型的好坏,而不是到实际应用中在去修正模型。如果没有测试数据集我们可能训练数据拟合的很好,却不知道可能发生了过拟合。可是将数据集分为训练数据集与测试数据集真的就不会发生拟合状态吗?思考一下,每次我们根据测试训练集反馈的结过进行调参,直到测试训练集的反馈结果很好,那么极有可能会针对特定的测试数据集发生过拟合,一个比较成熟的解决方案是:交叉验证。
交叉验证(Cross validation)
交叉验证是将原本数据集随机分成k个数据集, 每次拿一个作为验证数据集,其他k-1个作为训练数据集,最后k个“模型”的均值作为反馈的结果进行调参。这样就防止针对某一个特定数据集发生过拟合。他也有比较明显的缺点,就是每次调参都要训练k个模型,整体性能满了k倍左右。下面用kNN来熟习一下交叉验证
import numpy as np
from sklearn import datasets
from sklearn.neighbors import KNeighborsClassifier
digits = datasets.load_digits()
X = digits.data
y = digits.target
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=666) #分离比例与随机种子
#在模型选择中引入交叉验证
from sklearn.model_selection import cross_val_score
knn_clf = KNeighborsClassifier()
cross_val_score(knn_clf, X_train, y_train,cv = 3 )# 代表分成几份
#输出:array([0.98895028, 0.97777778, 0.96629213]) 代表对于每一份数据模型的分数
#使用交叉验证获取最优超参数,k与p
best_k, best_p, best_score = 0, 0, 0
for k in range(2, 11):
for p in range(1, 6):
knn_clf = KNeighborsClassifier(weights="distance", n_neighbors=k, p=p)
scores = cross_val_score(knn_clf, X_train, y_train) #获取每个模型的R^2
score = np.mean(scores) #取平均值作为反馈结果
if score > best_score:
best_k, best_p, best_score = k, p, score
print("Best K =", best_k)
print("Best P =", best_p)
print("Best Score =", best_score)
Best K = 2
Best P = 2
Best Score = 0.9823599874006478
#以上只是获取了最优参数,最后还是带到分离得到的测试数据进行性能评测,这个测试数据从未被训练过
best_knn_clf = KNeighborsClassifier(weights="distance", n_neighbors=2, p=2)
best_knn_clf.fit(X_train, y_train)
best_knn_clf.score(X_test, y_test)
0.980528511821975
sklean中的网格搜索其实就是利用了交叉验证的方式
from sklearn.model_selection import GridSearchCV
param_grid = [
{
\'weights\': [\'distance\'],
\'n_neighbors\': [i for i in range(2, 11)],
\'p\': [i for i in range(1, 6)]
}
]
grid_search = GridSearchCV(knn_clf, param_grid, verbose=1, n_jobs = -1)
grid_search.fit(X_train, y_train)
grid_search.best_score_ #得到最好的分数
grid_search.best_params_ #得到最好分数的参数
best_knn_clf = grid_search.best_estimator_ #或者那个最好的分类器
best_knn_clf.score(X_test, y_test)
Fitting 3 folds for each of 45 candidates, totalling 135 fits
[Parallel(n_jobs=-1)]: Done 42 tasks | elapsed: 25.8s
[Parallel(n_jobs=-1)]: Done 135 out of 135 | elapsed: 1.1min finished
0.980528511821975
留一法
Leave-out-one cross validation 完全不受随机影响,接近于真正的模型性能指标,只不过计算量太太太大了
偏差方差平衡
偏差是指离着目标点很远,方差是指结果是否集中,下面图可以很好的理解这两个概念,可以把机器模型的目标看做红点,机器学习的每次预测看作是打的枪。模型的误差 = 偏差(Bias)+方差(Variance)+不可避免的误差(比如数据有误)
偏差产生原因: 欠拟合,以及特征选取不对,参数学习通常偏差较大,会提前假设数据符合这个模型
方差产生原因: 过拟合,对数据很敏感,泛化能力差,非参数学习算法通常方差较大,因为对数据较为敏感
通常模型的偏差与方差都是矛盾的,降低偏差往往会提高方差,降低方差往往会提高偏差
解决高方差的通常手段:
1.降低模型复杂度
2.减少数据纬度,降噪
3.增加样本
4.使用验证集
5.模型正则化
模型正则化(泛化)
通常状况下,模型越复杂越容易过拟合,所以我们需要控制模型的复杂度,在损失函数后面加上一个正则项,正则项通常有L0正则,L1正则(岭回归),L2正则(LASSO回归),用来平衡损失函数值的大小与模型的复杂度。
岭回归(Ridge Regression)
岭回归是损失函数加上参数向量的L2范数(这里并没有开根号),a用来调节损失函数值大小与模型复杂度。
下面进行代码,不加正则化模型的MSE是167.94010867293571,最后会发现随着alpha增大多项式的图像会趋近一条直线
# 预准备
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
x = np.random.uniform(-3.0, 3.0, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x + 3 + np.random.normal(0, 1, size=100)
from sklearn.model_selection import train_test_split
np.random.seed(666)
X_train, X_test, y_train, y_test = train_test_split(X, y)
#封装画图函数
def plot_model(model):
X_plot = np.linspace(-3, 3, 100).reshape(100, 1)
y_plot = model.predict(X_plot)
plt.scatter(x, y)
plt.plot(X_plot[:,0], y_plot, color=\'r\')
plt.axis([-3, 3, 0, 6])
plt.show()
#使用岭回归构建模型
from sklearn.linear_model import Ridge #从线性模型中引入Ridge
def RidgeRegression(degree, alpha): #创建一个岭回归的管道函数
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("ridge_reg", Ridge(alpha=alpha)) #这里是Ridge, alpha用来平衡两者之间的关系
])
#如果alpha = 0.0001
ridge1_reg = RidgeRegression(20, 0.0001) # 构建一个实例
ridge1_reg.fit(X_train, y_train) #进行训练
y1_predict = ridge1_reg.predict(X_test) #利用训练数据的模型进行预测
print(mean_squared_error(y_test, y1_predict)) #求MSE
plot_model(ridge1_reg)
ridge3_reg = RidgeRegression(20, 100)
ridge3_reg.fit(X_train, y_train)
y3_predict = ridge3_reg.predict(X_test)
print(mean_squared_error(y_test, y3_predict))
plot_model(ridge3_reg)
1.323349275406402
1.3196456113086197
LASSO Regression
LASSO回归其实是将损失函数后面加一个L1范数
LASSO 比较容易将某个系数变成0,可以作为特征选择,但也容易把一些比较重要的特征的系数变成0,可以看一下他与岭回归的对比,alpha不用很大一些特征的系数就是0了
#Lasso调用
from sklearn.linear_model import Lasso
def LassoRegression(degree, alpha):
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("lasso_reg", Lasso(alpha=alpha))
])
lasso1_reg = LassoRegression(20, 0.01)
lasso1_reg.fit(X_train, y_train)
y1_predict = lasso1_reg.predict(X_test)
print(mean_squared_error(y_test, y1_predict))
plot_model(lasso1_reg)
lasso2_reg = LassoRegression(20, 0.1)
lasso2_reg.fit(X_train, y_train)
y2_predict = lasso2_reg.predict(X_test)
print(mean_squared_error(y_test, y2_predict))
plot_model(lasso2_reg)
1.149608084325997
1.1213911351818648
L0正则与弹性网
L0正则
弹性网
关于正则化的总结
实际应用中,在模型正则化时,同常先尝试岭回归,因为岭回归是相对精准的,岭回归的缺点的是如果特征偏多,不能把某些特征的系数变成0,计算耗时会很长,这时候就应该优先选择弹性网,弹性网结合了岭回归计算的优点也结合了LASSO可以将某些系数变为0,LASSO回归的缺点是,他有时会急于把某些参数设为0,这个过程可能会产生某些错误,导致最后得到的模型偏差比较大