机器学习练习五:模型的评估(偏置和方差的影响)

967 阅读8分钟

数据介绍:在练习的前半部分,您将使用水库水位的变化实现正则化线性回归来预测大坝的出水量。在下半部分中,您将通过调试学习算法的一些诊断并观察偏置和方差的影响。

1.正则化线性回归

1.1 数据展示

import numpy as np
from scipy.io import loadmat
import scipy.optimize as opt
import pandas as pd
import matplotlib.pyplot as plt

数据分成了训练集,验证集和测试集。

# 数据的加载
# 需借助loadmat函数
data = loadmat('ex5data1.mat')
# 训练集
X_train, y_train = data['X'], data['y'] 
# 验证集
Xval, yval = data['Xval'], data['yval']
# 测试集
X_test, y_test = data['Xtest'], data['ytest']
# 数据可视化
plt.figure(figsize=(15,8))
plt.scatter(X_train, y_train)
plt.xlabel('水库水位的变化')
plt.ylabel('出水量')
plt.show()

1.2 正则化线性回归的代价函数

代码实现

def Cost(theta, X, y, lambd):
    """
    构建损失函数
    """
    # 转换为矩阵类型
    theta = np.mat(theta)
    X = np.mat(X)
    y = np.mat(y)
    # 计算
    inner = np.power(((X @ theta.T) - y), 2)
    # 这里的np.sum()是将数组中所有的值相加
    first = np.sum(inner) / (2 * X.shape[0])
    # 这里有个坑,theta值在这里改动以后会影响到后面的调用(浅拷贝),需要使用深拷贝避免这个问题。
    t = theta.copy()
    # 偏置项不需要正则化
    t[:, 0] = 0
    last = lambd / (2 * X.shape[0]) * np.sum(np.power(t, 2))
    return first + last

需注意的问题

偏置项不需要正则化,可以通过将\theta_0置为0的方式来实现。但如果在程序中直接对theta进行修改,会将theta的值永久的改变,从而影响到后面程序对theta的调用。

而如果执行t = theta,再对t进行修改,也会直接改变theta的值,这就是浅拷贝的问题。

简单的说,浅拷贝只是复制了原来theta的地址,即t与theta同时指向相同的地址单元,所以修改其中任何一个都会影响这两个变量的值;而t = theta.copy()这样的深拷贝则是复制了原来theta变量地址单元的内容,然后将这个内容存放到新的地址单元去,即t与theta指向不同的地址单元,但内容是相同的。

此时对t或者theta进行修改,就不会影响另一个变量的值了。

计算损失

# 插入全1列x0
X_train, Xval, X_test = [np.insert(x, 0, np.ones(x.shape[0]), axis=1) for x in (X_train, Xval, X_test)]
# 初始化theta
theta = np.ones((1, 2))
# 计算损失
Cost(theta, X_train, y_train, 0)

结果:303.9515255535976

1.3 正则化梯度

这里也有与1.2相同的问题,不再赘述

代码实现:

def regularized_gra(theta, X, y, lambd):
    """
    正则化梯度函数
    """
    # 转换为矩阵类型
    theta = np.mat(theta)
    X = np.mat(X)
    y = np.mat(y)
    # 计算
    first = 1 / X.shape[0] * (X.T @ (X @ theta.T - y))
    # 深拷贝
    t = theta.copy()
    # 偏置项不需要正则化
    t[:, 0] = 0
    last = lambd / X.shape[0] * t.T
    return first + last

1.4 模型训练和拟合图

1.4.1 模型训练

这个模型只有两个参数,正则化没有效果,故设置λ=0。

# 使用 scipy.optimize.minimize 去寻找参数
import scipy.optimize as opt
# 将超参数λ置为0,因为在线性拟合中正则化没什么用
res = opt.minimize(fun=Cost, x0=theta, args=(X_train, y_train, 0), method='TNC', jac=regularized_gra)
res

结果:

1.4.2 绘制拟合图

# 绘制拟合图
x = np.linspace(X_train[:, 1].min(), X_train[:, 1].max(), 100)  # 返回在X_train范围内的100个等间隔数
# 计算预测的y值
f = res.x[0] + (res.x[1] * x)
# 画图
fig, ax = plt.subplots(figsize=(15, 8))
# 预测值
ax.plot(x, f, 'r', label='Prediction')
# 训练数据
ax.scatter(X_train[:, 1], y_train, label='Train Data')
# 标签
ax.legend(loc=2)
plt.xlabel('水库水位的变化')
plt.ylabel('出水量')
plt.show()

结果:

显然拟合度很差。

2.学习曲线的绘制

偏差-方差权衡是机器学习中的一个重要概念。高偏置模型对数据容易出现欠拟合,而高方差模型对训练数据过拟合。

2.1 按顺序抽取的学习曲线

学习曲线: 横轴是训练集的样本数量,纵轴是误差。

绘制学习曲线时要注意三点:

1.使用训练集的子集来拟合模型,而不是整个训练集

2.使用全部的验证集获取验证集误差,而不是测试集子集

3.在计算训练误差和交叉验证误差时,不用正则化

代码实现:

def plot_learning_curves(X_train, Xval, lambd):
    """
    绘制学习曲线
    """
    # 定义存放误差的列表
    train_cost, cv_cost = [], []
    # 定义样本总数m
    m = X_train.shape[0]
    
    # 设置for-loop,获取在样本数量不断增加时的误差
    for i in range(1, m + 1):
        # 获取训练集误差
        res_train = opt.minimize(fun=Cost, x0=theta, args=(X_train[:i, :], y_train[:i], lambd), method='TNC', jac=regularized_gra)
        train_cost.append(res_train.fun)
        # 获取测试集误差
        cv_cost.append(Cost(res_train.x, Xval, yval, lambd = lambd))
        
    # 绘制学习曲线
    m_list = [i for i in range(1, m + 1)]  # x轴
    fig, ax = plt.subplots(figsize=(15, 8))
    ax.plot(m_list, train_cost, 'r', label='Train')
    ax.plot(m_list, cv_cost, 'b', label='CV')
    # 标签
    ax.legend(loc=2)
    plt.xlabel('样本数')
    plt.ylabel('损失')
    plt.title('学习曲线')
    plt.show()

注意res_train = opt.minimize(fun=Cost, x0=theta, args=(X_train[:i, :], y_train[:i], lambd), method='TNC', jac=regularized_gra)在这一行训练模型的代码中,特征值和目标值要同时取前i项。

结果:

显然训练集误差和测试集误差都偏大,且随着样本数的增加逐渐趋向平稳,故模型处于高偏置状态,即欠拟合。

2.2 随机抽取的学习曲线

在实践中,特别是对于小的训练集,当您绘制学习曲线来调试您的算法时,跨多个随机选择的示例集平均以确定训练误差和交叉验证误差通常是有帮助的。

在该函数中,每次在训练集和验证集中随机抽取i个样本,用训练集进行训练,同时获取训练集和验证集的损失,重复50次并取各自损失的平均值。

代码实现(λ=0.01):

def plot_random_lc(X_train, Xval):
    """
    用随机选择的例子绘制学习曲线
    """
    # 定义存放误差的列表
    train_cost_list, cv_cost_list = [], []
    # 定义样本总数m
    m = X_train.shape[0]
    
    # 设置for-loop,获取在样本数量不断增加时的误差
    for i in range(m):
        # 定义存放误差平均值的列表
        t_cost, cv_cost = [], []
        for j in range(50):
            # 生成随机数组
            index = np.random.randint(0, m-1, size=[1, i+1])[0]
            # 获取训练集误差
            res_train = opt.minimize(fun=Cost, x0=theta, args=(X_train[index, :], y_train[index], 0.01), method='TNC', jac=regularized_gra)
            t_cost.append(res_train.fun)
            # 获取验证集误差
            cv_cost.append(Cost(res_train.x, Xval[index, :], yval[index], lambd = 0.01))
        # 存训练集误差
        train_cost_list.append(np.array(t_cost).mean())
        # 存验证集误差
        cv_cost_list.append(np.array(cv_cost).mean())
        
    # 绘制学习曲线
    m_list = [i for i in range(1, m + 1)]  # x轴
    fig, ax = plt.subplots(figsize=(15, 8))
    ax.plot(m_list, train_cost_list, 'r', label='Train')
    ax.plot(m_list, cv_cost_list, 'b', label='CV')
    # 标签
    ax.legend(loc=2)
    plt.xlabel('随机抽取的样本数')
    plt.ylabel('损失')
    plt.title('学习曲线')
    plt.show()

结果:

训练集损失过小且与验证集相差较大,过拟合。

3.模型的评估

那么,什么方法对于模型优化是有效的呢?

最常见的是以下几种:

模型状况 学习曲线的表现 改进方法
高方差(过拟合) 验证集误差一直比较大,且与训练集误差有较大差距 获取更多的训练样本
减少特征值的数量
增大正则化超参数λ的值
高偏置(欠拟合) 两个误差都比较大,且随样本数量的增加趋向平稳 增加其他的特征值
减小正则化超参数λ的值
较好的拟合(偏置与方差平衡) 两个误差都比较小且较为接近

3.1 多项式回归

那么对于第2节评估的模型来说,问题是,它的特征值太简单了,导致了拟合不足(高偏差)。在本部分练习中,您将通过添加更多特性来解决这个问题。

定义:

h_{\theta}(x)=\theta_0+\theta_1x_1+\theta_2x_1^2+...+\theta_px_1^p

即把特征值拓展到8次幂,代码实现:

注意忽略x_0列,对真正的特征值x_1进行展开。利用了字典-->DataFrame类型的转换。

def get_poly_featrue(x, power):
    """
    获取高阶特征值
    """
    # 定义存储特征值的字典
    poly_x = {}
    # 设置for-loop获取每阶的值
    for p in range(power + 1):
        poly_x['x_{}'.format(p)] = np.power(x[:, 1], p)
    # 转换为DF类型输出
    return pd.DataFrame(poly_x)

3.2 特征归一化

将特征的阶数升高后,可用的特征值变为了8个。但特征值之间的数值差距却很大,故需要进行归一化操作。

即用\frac{(x-x的平均值)}{x的标准差},需要注意x_0列不需要归一化。

def normalization(x, power):
    """
    特征归一化
    """
    # 特征展开为多项式
    x = get_poly_featrue(x, power)
    # 特征归一化
    x = (x.iloc[:, 1:] - x.iloc[:, 1:].mean()) / x.iloc[:, 1:].std()
    # 插入全1列
    x.insert(0, 'x_0', 1)
    return x

3.3 绘制学习曲线

经过上面的操作,我们得到了三个展开后的特征值:X_train_poly, Xval_poly, X_test_poly。

让我们看一下模型的拟合程度如何吧。

  • 当λ=0时

训练集损失过小,而验证集损失比训练集大很多,过拟合。

解决过度拟合(高方差)问题的一种方法是在模型中加入正则化。尝试不同的λ参数,看看正则化是否会创造一个更好的模型。

  • 当λ=1时

可以看出此时模型的拟合度较好。

  • 当λ=100时

可以看出此时的训练集和验证集损失的非常大,模型欠拟合严重。

3.4 交叉验证适合的λ

在本节中,您将实现一个自动方法来选择λ参数。具体地说,您将使用一套交叉验证评估每个λ值有多好。选择好λ值使用交叉验证设置,我们可以评估模型在验证集上估计在实际看不见的数据模型将有怎样的表现。

即测试什么样的λ值使模型的效果最好。

def plot_cv_lambda(X_train, Xval):
    """
    交叉验证λ
    """
    # 定义存放误差的列表
    train_cost, cv_cost = [], []
    # 定义λ
    lambd = [0, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1, 3, 10]
    
    # 设置for-loop,获取在样本数量不断增加时的误差
    for i in range(len(lambd)):
        # 获取训练集误差
        res_train = opt.minimize(fun=Cost, x0=theta, args=(X_train, y_train, lambd[i]), method='TNC', jac=regularized_gra)
        train_cost.append(res_train.fun)
        # 获取验证集误差
        cv_cost.append(Cost(res_train.x, Xval, yval, lambd = lambd[i]))
        
    # 绘制学习曲线
    fig, ax = plt.subplots(figsize=(15, 8))
    ax.plot(lambd, train_cost, 'r', label='Train')
    ax.plot(lambd, cv_cost, 'b', label='CV')
    # 标签
    ax.legend(loc=2)
    plt.xlabel('λ值')
    plt.ylabel('损失')
    plt.title('学习曲线')
    plt.show()
    return cv_cost

结果:

可以看出较好的λ值在λ=0.3附近。

3.5 使用测试集评估

为了保证客观性,我们使用没有用于训练和验证的数据计算模型的损失,对模型进行评估。

def test_error(X_train, X_test):
    """
    获取测试集误差
    """
    # 定义λ
    lambd = [0, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1, 3, 10]
    # 定义列表存储测试集误差
    test_cost = []
    # 设置for-loop,获取不同λ时的误差
    for i in range(len(lambd)):
        # 获取测试集误差
        res_train = opt.minimize(fun=Cost, x0=theta, args=(X_train, y_train, lambd[i]), method='TNC', jac=regularized_gra)
        test_cost.append(Cost(res_train.x, X_test, y_test, lambd[i]))
    plt.plot(lambd, test_cost)
    # 标签
    plt.xlabel('λ值')
    plt.ylabel('损失')
    plt.show()
    return test_cost

结果:

可以看出,较好的λ值仍然在λ=0.3附近。