本文最后更新于:3 个月前
[√] 第二章 机器学习概述
机器学习(Machine Learning,ML)就是让计算机从数据中进行自动学习,得到某种知识(或规律)。作为一门学科,机器学习通常指一类问题以及解决这类问题的方法,即如何从观测数据(样本)中寻找规律,并利用学习到的规律(模型)对未知或无法观测的数据进行预测。
在学习本章内容前,建议您先阅读《神经网络与深度学习》第 2 章:机器学习概述的相关内容,关键知识点如图2.1所示,以便更好的理解和掌握相应的理论知识,及其在实践中的应用方法。
本章内容基于《神经网络与深度学习》第 2 章:机器学习概述 相关内容进行设计,主要包含两部分:
- 模型解读:介绍机器学习实践五要素(数据、模型、学习准则、优化算法、评估指标)的原理剖析和相应的代码实现。通过理论和代码的结合,加深对机器学习的理解;
- 案例实践:基于机器学习线性回归方法,通过数据处理、模型构建、训练配置、组装训练框架Runner、模型训练和模型预测等过程完成波士顿房价预测任务。
[√] 2.1 - 机器学习实践五要素
要通过机器学习来解决一个特定的任务时,我们需要准备5个方面的要素:
- 数据集:收集任务相关的数据集用来进行模型训练和测试,可分为训练集、验证集和测试集;
- 模型:实现输入到输出的映射,通常为可学习的函数;
- 学习准则:模型优化的目标,通常为损失函数和正则化项的加权组合;
- 优化算法:根据学习准则优化机器学习模型的参数;
- 评价指标:用来评价学习到的机器学习模型的性能.
图2.2给出实现一个完整的机器学习系统的主要环节和要素。从流程角度看,实现机器学习系统可以分为两个阶段:训练阶段和评价阶段。训练阶段需要用到训练集、验证集、待学习的模型、损失函数、优化算法,输出学习到的模型;评价阶段也称为测试阶段,需要用到测试集、学习到的模型、评价指标体系,得到模型的性能评价。图2.2给出实现一个完整的机器学习系统的主要环节和要素。从流程角度看,实现机器学习系统可以分为两个阶段:训练阶段和评价阶段。训练阶段需要用到训练集、验证集、待学习的模型、损失函数、优化算法,输出学习到的模型;评价阶段也称为测试阶段,需要用到测试集、学习到的模型、评价指标体系,得到模型的性能评价。
在本节中,我们分别对这五个要素进行简单的介绍。
《神经网络与深度学习》第 2.2 节详细介绍了机器学习的三个基本要素:“模型”、“学习准则”和“优化算法”.在机器学习实践中,“数据”和“评价指标”也非常重要.因此,本书将机器学习在实践中的主要元素归结为五要素.
[√] 2.1.1 - 数据
在实践中,数据的质量会很大程度上影响模型最终的性能,通常数据预处理是完成机器学习实践的第一步,噪音越少、规模越大、覆盖范围越广的数据集往往能够训练出性能更好的模型。数据预处理可分为两个环节:先对收集到的数据进行基本的预处理,如基本的统计、特征归一化和异常值处理等;再将数据划分为训练集、验证集(开发集)和测试集。
- 训练集:用于模型训练时调整模型的参数,在这份数据集上的误差被称为训练误差;
- 验证集(开发集):对于复杂的模型,常常有一些超参数需要调节,因此需要尝试多种超参数的组合来分别训练多个模型,然后对比它们在验证集上的表现,选择一组相对最好的超参数,最后才使用这组参数下训练的模型在测试集上评估测试误差。
- 测试集:模型在这份数据集上的误差被称为测试误差。训练模型的目的是为了通过从训练数据中找到规律来预测未知数据,因此测试误差是更能反映出模型表现的指标。
数据划分时要考虑到两个因素:更多的训练数据会降低参数估计的方差,从而得到更可信的模型;而更多的测试数据会降低测试误差的方差,从而得到更可信的测试误差。如果给定的数据集没有做任何划分,我们一般可以大致按照7:3或者8:2的比例划分训练集和测试集,再根据7:3或者8:2的比例从训练集中再次划分出训练集和验证集。
需要强调的是,测试集只能用来评测模型最终的性能,在整个模型训练过程中不能有测试集的参与。
[√] 2.1.2 模型
有了数据后,我们可以用数据来训练模型。我们希望能让计算机从一个函数集合 $\mathcal{F} = {f_1(\boldsymbol{x}), f_2(\boldsymbol{x}), \cdots }$中
自动寻找一个“最优”的函数$f^∗(\boldsymbol{x})$ 来近似每个样本的特征向量 $\boldsymbol{x}$ 和标签 $y$ 之间
的真实映射关系,实际上这个函数集合也被称为假设空间,在实际问题中,假设空间$\mathcal{F}$通常为一个参数化的函数族
$$
\mathcal{F}=\left{f(\boldsymbol{x} ; \theta) \mid \theta \in \mathbb{R}^{D}\right}, (2.1)
$$
其中$f(\boldsymbol{x} ; \theta)$是参数为$\theta$的函数,也称为模型,𝐷 为参数的数量。
常见的假设空间可以分为线性和非线性两种,对应的模型 $f$ 也分别称为线性模型和非线性模型。线性模型的假设空间为一个参数化的线性函数族,即:
$$
f(\boldsymbol{x} ; \theta)=\boldsymbol{w}^{\top} \boldsymbol{x}+b, (2.2)
$$
其中参数$\theta$ 包含了权重向量$\boldsymbol{w}$和偏置$b$。
线性模型可以由非线性基函数$\phi(\boldsymbol{x})$变为非线性模型,从而增强模型能力:
$$
f(\boldsymbol{x} ; \theta)=\boldsymbol{w}^{\top} \phi(\boldsymbol{x})+b, (2.3)
$$
其中$\phi(\boldsymbol{x})=\left[\phi_{1}(\boldsymbol{x}), \phi_{2}(\boldsymbol{x}), \cdots, \phi_{K}(\boldsymbol{x})\right]^{\top}$为𝐾 个非线性基函数组成的向量,参数 $\theta$ 包含了权重向量$\boldsymbol{w}$和偏置$b$。
[√] 2.1.3 学习准则
为了衡量一个模型的好坏,我们需要定义一个损失函数$\mathcal{L}(\boldsymbol{y},f(\boldsymbol{x};\theta))$。损失函数是一个非负实数函数,用来量化模型预测标签和真实标签之间的差异。常见的损失函数有 0-1 损失、平方损失函数、交叉熵损失函数等。
机器学习的目标就是找到最优的模型$𝑓(𝒙;\theta^∗)$在真实数据分布上损失函数的期望最小。然而在实际中,我们无法获得真实数据分布,通常会用在训练集上的平均损失替代。
一个模型在训练集$\mathcal{D}={(\boldsymbol{x}^{(n)},y^{(n)})}_{n=1}^N$上的平均损失称为经验风险{Empirical Risk},即:
$$
\mathcal{R}^{emp}\mathcal{D}(\theta)=\frac{1}{N}\sum{n=1}^{N}\mathcal{L}(y^{(n)},f({x}^{(n)};\theta))。 (2.4)
$$
$\mathcal{L}(\boldsymbol{y},f(\boldsymbol{x};\theta))$为损失函数。损失函数是一个非负实数函数,用来量化模型预测和真实标签之间的差异。常见的损失函数有0-1损失、平方损失函数、交叉熵损失函数等。
在通常情况下,我们可以通过使得经验风险最小化来获得具有预测能力的模型。然而,当模型比较复杂或训练数据量比较少时,经验风险最小化获得的模型在测试集上的效果比较差。而模型在测试集上的性能才是我们真正关心的指标.当一个模型在训练集错误率很低,而在测试集上错误率较高时,通常意味着发生了过拟合(Overfitting)现象。为了缓解模型的过拟合问题,我们通常会在经验损失上加上一定的正则化项来限制模型能力。
过拟合通常是由于模型复杂度比较高引起的。在实践中,最常用的正则化方式有对模型的参数进行约束,比如$\ell_1$或者$\ell_2$范数约束。这样,我们就得到了结构风险(Structure Risk)。
$$
\mathcal{R}^{struct}{\mathcal{D}}(\theta)=\mathcal{R}^{emp}{\mathcal{D}}(\theta)+\lambda \ell_p(\theta), (2.5)
$$
其中$\lambda$为正则化系数,$p=1$或$2$表示$\ell_1$或者$\ell_2$范数。
[√] 2.1.4 优化算法
在有了优化目标之后,机器学习问题就转化为优化问题,我们可以利用已知的优化算法来学习最优的参数。当优化函数为凸函数时,我们可以令参数的偏导数等于0来计算最优参数的解析解。当优化函数为非凸函数时,我们可以用一阶的优化算法来进行优化。
目前机器学习中最常用的优化算法是梯度下降法(Gradient Descent Method)。
当使用梯度下降法进行参数优化时,还可以利用验证集来早停法(Early-Stop)来中止模型的优化过程,避免模型在训练集上过拟合。早停法也是一种常用并且十分有效的正则化方法。
[√] 2.1.5 评估指标
评估指标(Metric)用于评价模型效果,即给定一个测试集,用模型对测试集中的每个样本进行预测,并根据预测结果计算评价分数。回归任务的评估指标一般有预测值与真实值的均方差,分类任务的评估指标一般有准确率、召回率、F1值等。
对于一个机器学习任务,一般会先确定任务类型,再确定任务的评价指标,再根据评价指标来建立模型,选择学习准则。由于评价指标不可微等问题有时候学习准则并不能完全和评价指标一致,我们往往会选择一定的损失函数使得两者尽可能一致。
[√] 2.2 - 实现一个简单的线性回归模型
回归任务是一类典型的监督机器学习任务,对自变量和因变量之间关系进行建模分析,其预测值通常为一个连续值,比如房屋价格预测、电源票房预测等。线性回归(Linear Regression)是指一类利用线性函数来对自变量和因变量之间关系进行建模的回归任务,是机器学习和统计学中最基础和最广泛应用的模型。
在本节中,我们动手实现一个简单的线性回归模型,并使用最小二乘法来求解参数,以对机器学习任务有更直观的认识。
[√] 2.2.1 - 数据集构建
首先,我们构造一个小的回归数据集。假设输入特征和输出标签的维度都为 1,需要被拟合的函数定义为:
1 2 3 4
| def linear_func(x,w=1.2,b=0.5): y = w*x + b return y
|
然后,使用paddle.rand()
函数来进行随机采样输入特征xxx,并代入上面函数得到输出标签𝑦𝑦y。为了模拟真实环境中样本通常包含噪声的问题,我们采样过程中加入高斯噪声和异常点。
生成样本数据的函数create_toy_data
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import paddle
def create_toy_data(func, interval, sample_num, noise = 0.0, add_outlier = False, outlier_ratio = 0.001): """ 根据给定的函数,生成样本 输入: - func:函数 - interval: x的取值范围 - sample_num: 样本数目 - noise: 噪声均方差 - add_outlier:是否生成异常值 - outlier_ratio:异常值占比 输出: - X: 特征数据,shape=[n_samples,1] - y: 标签数据,shape=[n_samples,1] """
X = paddle.rand(shape = [sample_num]) * (interval[1]-interval[0]) + interval[0] y = func(X)
epsilon = paddle.normal(0,noise,paddle.to_tensor(y.shape[0])) y = y + epsilon if add_outlier: outlier_num = int(len(y)*outlier_ratio) if outlier_num != 0: outlier_idx = paddle.randint(len(y),shape = [outlier_num]) y[outlier_idx] = y[outlier_idx] * 5 return X, y
|
利用上面的生成样本函数,生成 150 个带噪音的样本,其中 100 个训练样本,50 个测试样本,并打印出训练数据的可视化分布。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from matplotlib import pyplot as plt
func = linear_func interval = (-10,10) train_num = 100 test_num = 50 noise = 2 X_train, y_train = create_toy_data(func=func, interval=interval, sample_num=train_num, noise = noise, add_outlier = False) X_test, y_test = create_toy_data(func=func, interval=interval, sample_num=test_num, noise = noise, add_outlier = False)
X_train_large, y_train_large = create_toy_data(func=func, interval=interval, sample_num=5000, noise = noise, add_outlier = False)
X_underlying = paddle.linspace(interval[0],interval[1],train_num) y_underlying = linear_func(X_underlying)
plt.scatter(X_train, y_train, marker='*', facecolor="none", edgecolor='#e4007f', s=50, label="train data") plt.scatter(X_test, y_test, facecolor="none", edgecolor='#f19ec2', s=50, label="test data") plt.plot(X_underlying, y_underlying, c='#000000', label=r"underlying distribution") plt.legend(fontsize='x-large') plt.savefig('ml-vis.pdf') plt.show()
|
[√] 2.2.2 - 模型构建
在线性回归中,自变量为样本的特征向量$\boldsymbol{x}\in \mathbb{R}^D$(每一维对应一个自变量),因变量是连续值的标签$y\in R$。
线性模型定义为:
$$
f(\boldsymbol{x};\boldsymbol{w},b)=\boldsymbol{w}^T\boldsymbol{x}+b。 (2.6)
$$
其中权重向量$\boldsymbol{w}\in \mathbb{R}^D$和偏置$b\in \mathbb{R}$都是可学习的参数。
注意:《神经网络与深度学习》中为了表示的简洁性,使用增广权重向量来定义模型。而在本书中,为了和代码实现保持一致,我们使用非增广向量的形式来定义模型。
在实践中,为了提高预测样本的效率,我们通常会将$N$样本归为一组进行成批地预测,这样可以更好地利用GPU设备的并行计算能力。
$$
\boldsymbol{y} =\boldsymbol{X} \boldsymbol{w} + b, (2.7)
$$
其中$\boldsymbol{X}\in \mathbb{R}^{N\times D}$为$N$个样本的特征矩阵,$\boldsymbol{y}\in \mathbb{R}^N$为$N$个预测值组成的列向量。
注意:在实践中,样本的矩阵$\boldsymbol{X}$是由$N$个$\boldsymbol{x}$的行向量组成。而原教材中$\boldsymbol{x}$为列向量,其特征矩阵与本书中的特征矩阵刚好为转置关系。
[√] 2.2.2.1 - 线性算子
实现公式(2.7)中的线性函数非常简单,我们直接利用如下张量运算来实现。
1 2 3 4 5
|
y_pred = paddle.matmul(X,w)+b
|
使用飞桨构建一个线性回归模型,代码如下:
说明
在飞桨框架中,可以直接调用模型的forward()
方法进行前向执行,也可以调用__call__()
,从而执行在 forward()
当中定义的前向计算逻辑。
在飞桨框架中,模型一般继承nn.Layer,在成员函数forward()
中执行模型的前向运算。由于本案例较简单,所以没有继承nn.Layer
,但是保留了在forward()
函数中执行模型的前向运算的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import paddle from nndl.op import Op
paddle.seed(10)
class Linear(Op): def __init__(self, input_size): """ 输入: - input_size:模型要处理的数据特征向量长度 """
self.input_size = input_size
self.params = {} self.params['w'] = paddle.randn(shape=[self.input_size,1],dtype='float32') self.params['b'] = paddle.zeros(shape=[1],dtype='float32')
def __call__(self, X): return self.forward(X)
def forward(self, X): """ 输入: - X: tensor, shape=[N,D] 注意这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致 输出: - y_pred: tensor, shape=[N] """
N,D = X.shape
if self.input_size==0: return paddle.full(shape=[N,1], fill_value=self.params['b']) assert D==self.input_size
y_pred = paddle.matmul(X,self.params['w'])+self.params['b'] return y_pred
input_size = 3 N = 2 X = paddle.randn(shape=[N, input_size],dtype='float32') model = Linear(input_size) y_pred = model(X) print("y_pred:",y_pred)
|
1 2 3
| y_pred: Tensor(shape=[2, 1], dtype=float32, place=CPUPlace, stop_gradient=True, [[0.54838145], [2.03063798]])
|
[√] 2.2.3 - 损失函数
回归任务是对连续值的预测,希望模型能根据数据的特征输出一个连续值作为预测值。因此回归任务中常用的评估指标是均方误差。
令$\boldsymbol{y}\in \mathbb{R}^N$,$\hat{\boldsymbol{y}}\in \mathbb{R}^N$分别为$N$个样本的真实标签和预测标签,均方误差的定义为:
$$
\mathcal{L}(\boldsymbol{y},\hat{\boldsymbol{y}})=\frac{1}{2N}|\boldsymbol{y}-\hat{\boldsymbol{y}}|^2=\frac{1}{2N}|\boldsymbol{X}\boldsymbol{w}+\boldsymbol{b}-\boldsymbol{y}|^2, (2.8)
$$
其中$\boldsymbol{b}$为$N$维向量,所有元素取值都为$b$。
均方误差的代码实现如下:
注意:代码实现中没有除2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import paddle
def mean_squared_error(y_true, y_pred): """ 输入: - y_true: tensor,样本真实标签 - y_pred: tensor, 样本预测标签 输出: - error: float,误差值 """
assert y_true.shape[0] == y_pred.shape[0] error = paddle.mean(paddle.square(y_true - y_pred))
return error
y_true= paddle.to_tensor([[-0.2],[4.9]],dtype='float32') y_pred = paddle.to_tensor([[1.3],[2.5]],dtype='float32')
error = mean_squared_error(y_true=y_true, y_pred=y_pred).item() print("error:",error)
|
1
| error: 4.005000114440918
|
[√] 2.2.4 - 模型优化
采用经验风险最小化,线性回归可以通过最小二乘法求出参数$\boldsymbol{w}$和$b$的解析解。计算公式(2.8)中均方误差对参数$b$的偏导数,得到
$$
\frac{\partial \mathcal{L}(\boldsymbol{y},\hat{\boldsymbol{y}})}{\partial b} = \mathbf{1}^T (\boldsymbol{X}\boldsymbol{w}+\boldsymbol{b}-\boldsymbol{y}), (2.9)
$$
其中$\mathbf{1}$为$N$维的全1向量。这里为了简单起见省略了均方误差的系数$\frac{1}{N}$,并不影响最后的结果。
令上式等于0,得到
$$
b^* =\bar{y}-\bar{\boldsymbol{x}}^T \boldsymbol{w},(2.10)
$$
其中$\bar{y} = \frac{1}{N}\mathbf{1}^T\boldsymbol{y}$为所有标签的平均值,$\bar{\boldsymbol{x}} = \frac{1}{N}(\mathbf{1}^T \boldsymbol{X})^T$ 为所有特征向量的平均值。将$b^*$代入公式(2.8)中均方误差对参数$\boldsymbol{w}$的偏导数,得到
$$
\frac{\partial \mathcal{L}(\boldsymbol{y},\hat{\boldsymbol{y}})}{\partial \boldsymbol{w}} = (\boldsymbol{X}-\bar{\boldsymbol{x}}^T)^T \Big((\boldsymbol{X}-\bar{\boldsymbol{x}}^T)\boldsymbol{w}-(\boldsymbol{y}-\bar{y})\Big).(2.11)
$$
令上式等于0,得到最优的参数为
$$
\boldsymbol{w}^*=\Big((\boldsymbol{X}-\bar{\boldsymbol{x}}^T)^T(\boldsymbol{X}-\bar{\boldsymbol{x}}^T)\Big)^{\mathrm{-}1}(\boldsymbol{X}-\bar{\boldsymbol{x}}^T)^T (\boldsymbol{y}-\bar{y}),(2.12)
$$
$$
b^* = \bar{y}-\bar{\boldsymbol{x}}^T \boldsymbol{w}^*.(2.13)
$$
若对参数$\boldsymbol{w}$加上$\ell_2$正则化,则最优的$\boldsymbol{w}^*$变为
$$
\boldsymbol{w}^*=\Big((\boldsymbol{X}-\bar{\boldsymbol{x}}^T)^T(\boldsymbol{X}-\bar{\boldsymbol{x}}^T)+\lambda \boldsymbol{I}\Big)^{\mathrm{-}1}(\boldsymbol{X}-\bar{\boldsymbol{x}}^T)^T (\boldsymbol{y}-\bar{y}),(2.14)
$$
其中$\lambda>0$为预先设置的正则化系数,$\boldsymbol{I}\in \mathbb{R}^{D\times D}$为单位矩阵。
尝试验证公式(2.14)。
==通过最小二乘法优化参数==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| def optimizer_lsm(model, X, y, reg_lambda=0): ''' 输入: - model: 模型 - X: tensor, 特征数据,shape=[N,D] - y: tensor,标签数据,shape=[N] - reg_lambda: float, 正则化系数,默认为0 输出: - model: 优化好的模型 '''
N, D = X.shape
x_bar_tran = paddle.mean(X,axis=0).T y_bar = paddle.mean(y) x_sub = paddle.subtract(X,x_bar_tran)
if paddle.all(x_sub==0): model.params['b'] = y_bar model.params['w'] = paddle.zeros(shape=[D]) return model tmp = paddle.inverse(paddle.matmul(x_sub.T,x_sub)+ reg_lambda*paddle.eye(num_rows = (D)))
w = paddle.matmul(paddle.matmul(tmp,x_sub.T),(y-y_bar)) b = y_bar-paddle.matmul(x_bar_tran,w) model.params['b'] = b model.params['w'] = paddle.squeeze(w,axis=-1)
return model
|
[√] 2.2.5 - 模型训练
在准备了数据、模型、损失函数和参数学习的实现之后,我们开始模型的训练。
在回归任务中,模型的评价指标和损失函数一致,都为均方误差。
通过上文实现的线性回归类来拟合训练数据,并输出模型在训练集上的损失。
1 2 3 4 5 6 7 8
| input_size = 1 model = Linear(input_size) model = optimizer_lsm(model,X_train.reshape([-1,1]),y_train.reshape([-1,1])) print("w_pred:",model.params['w'].item(), "b_pred: ", model.params['b'].item())
y_train_pred = model(X_train.reshape([-1,1])).squeeze() train_error = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item() print("train error: ",train_error)
|
# 真实函数的参数缺省值为 w=1.2,b=0.5
1 2
| w_pred: 1.200244426727295 b_pred: 0.41967445611953735 train error: 3.8776581287384033
|
1 2 3 4 5 6 7
| model_large = Linear(input_size) model_large = optimizer_lsm(model_large,X_train_large.reshape([-1,1]),y_train_large.reshape([-1,1])) print("w_pred large:",model_large.params['w'].item(), "b_pred large: ", model_large.params['b'].item())
y_train_pred_large = model_large(X_train_large.reshape([-1,1])).squeeze() train_error_large = mean_squared_error(y_true=y_train_large, y_pred=y_train_pred_large).item() print("train error large: ",train_error_large)
|
1 2
| w_pred large: 1.1942962408065796 b_pred large: 0.5079830884933472 train error large: 4.086254119873047
|
从输出结果看,预测结果与真实值$\boldsymbol{w}=1.2$,$b=0.5$有一定的差距。
[√] 2.2.6 - 模型评估
下面用训练好的模型预测一下测试集的标签,并计算在测试集上的损失。
1 2 3
| y_test_pred = model(X_test.reshape([-1,1])).squeeze() test_error = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item() print("test error: ",test_error)
|
1
| test error: 4.075982570648193
|
1 2 3
| y_test_pred_large = model_large(X_test.reshape([-1,1])).squeeze() test_error_large = mean_squared_error(y_true=y_test, y_pred=y_test_pred_large).item() print("test error large: ",test_error_large)
|
1
| test error large: 4.081584453582764
|
动手练习:
为了加深对机器学习模型的理解,请自己动手完成以下实验:
(1) 调整训练数据的样本数量,由 100 调整到 5000,观察对模型性能的影响。
(2) 调整正则化系数,观察对模型性能的影响。
(1)训练集数量5000,测试集数量1000
1 2
| w_pred: 1.2003157138824463 b_pred: 0.5225465893745422 train error: 4.16726541519165
|
1
| test error: 3.9572794437408447
|
1
| test error large: 3.96787428855896
|
模型的错误率有所下降
[√] 2.3 - 多项式回归
多项式回归是回归任务的一种形式,其中自变量和因变量之间的关系是$M$次多项式的一种线性回归形式,即:
$$
f(\boldsymbol{x};\boldsymbol{w})=w_1x+w_2x^2+…+w_Mx^M+b=\boldsymbol{w}^T\phi(x)+b, (2.10)
$$
其中$M$为多项式的阶数,$\boldsymbol{w}=[w_1,…,w_M]^T$为多项式的系数,$\phi(x)=[x,x^2,\cdots,x^M]^T$为多项式基函数,将原始特征$x$映射为$M$维的向量。当$M=0$时,$f(\boldsymbol{x};\boldsymbol{w})=b$。
公式(2.10)展示的是特征维度为1的多项式表达,当特征维度大于1时,存在不同特征之间交互的情况,这是线性回归无法实现。公式(2.11)展示的是当特征维度为2,多项式阶数为2时的多项式回归:
$$
f(\boldsymbol{x};\boldsymbol{w})=w_1x_1+w_2x_2+w_3x_1^2+w_4x_1x_2+w_5x_2^2+b, (2.11)
$$
当自变量和因变量之间并不是线性关系时,我们可以定义非线性基函数对特征进行变换,从而可以使得线性回归算法实现非线性的曲线拟合。
接下来我们基于特征维度为1的自变量介绍多项式回归实验。
[√] 2.3.1 - 数据集构建
假设我们要拟合的非线性函数为一个缩放后的sin函数。
1 2 3 4 5 6
| import math
def sin(x): y = paddle.sin(2 * math.pi * x) return y
|
这里仍然使用前面定义的create_toy_data
函数来构建训练和测试数据,其中训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func = sin interval = (0,1) train_num = 15 test_num = 10 noise = 0.1 X_train, y_train = create_toy_data(func=func, interval=interval, sample_num=train_num, noise = noise) X_test, y_test = create_toy_data(func=func, interval=interval, sample_num=test_num, noise = noise)
X_underlying = paddle.linspace(interval[0],interval[1],num=100) y_underlying = sin(X_underlying)
plt.rcParams['figure.figsize'] = (8.0, 6.0) plt.scatter(X_train, y_train, facecolor="none", edgecolor='#e4007f', s=50, label="train data")
plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$") plt.legend(fontsize='x-large') plt.savefig('ml-vis2.pdf') plt.show()
|
[√] 2.3.2 - 模型构建
通过多项式的定义可以看出,多项式回归和线性回归一样,同样学习参数$\boldsymbol{w}$,只不过需要对输入特征$\phi(x)$根据多项式阶数进行变换。因此,我们可以套用求解线性回归参数的方法来求解多项式回归参数。
首先,我们实现多项式基函数polynomial_basis_function
对原始特征$x$进行转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| def polynomial_basis_function(x, degree = 2): """ 输入: - x: tensor, 输入的数据,shape=[N,1] - degree: int, 多项式的阶数 example Input: [[2], [3], [4]], degree=2 example Output: [[2^1, 2^2], [3^1, 3^2], [4^1, 4^2]] 注意:本案例中,在degree>=1时不生成全为1的一列数据;degree为0时生成形状与输入相同,全1的Tensor 输出: - x_result: tensor """ if degree==0: return paddle.ones(shape = x.shape,dtype='float32')
x_tmp = x x_result = x_tmp
for i in range(2, degree+1): x_tmp = paddle.multiply(x_tmp,x) x_result = paddle.concat((x_result,x_tmp),axis=-1)
return x_result
data = [[2], [3], [4]] X = paddle.to_tensor(data = data,dtype='float32') degree = 3 transformed_X = polynomial_basis_function(X,degree=degree) print("转换前:",X) print("阶数为",degree,"转换后:",transformed_X)
|
1 2 3 4 5 6 7 8
| 转换前: Tensor(shape=[3, 1], dtype=float32, place=CPUPlace, stop_gradient=True, [[2.], [3.], [4.]]) 阶数为 3 转换后: Tensor(shape=[3, 3], dtype=float32, place=CPUPlace, stop_gradient=True, [[2. , 4. , 8. ], [3. , 9. , 27.], [4. , 16., 64.]])
|
[√] 2.3.3 - 模型训练
对于多项式回归,我们可以同样使用前面线性回归中定义的LinearRegression
算子、训练函数train
、均方误差函数mean_squared_error
。拟合训练数据的目标是最小化损失函数,同线性回归一样,也可以通过矩阵运算直接求出$\boldsymbol{w}$的值。
我们设定不同的多项式阶,$M$的取值分别为0、1、3、8,之前构造的训练集上进行训练,观察样本数据对$\sin$曲线的拟合结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| plt.rcParams['figure.figsize'] = (12.0, 8.0)
for i, degree in enumerate([0, 1, 3, 8]): model = Linear(degree) X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), degree) X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), degree) model = optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1]))
y_underlying_pred = model(X_underlying_transformed).squeeze()
print(model.params) plt.subplot(2, 2, i + 1) plt.scatter(X_train, y_train, facecolor="none", edgecolor='#e4007f', s=50, label="train data") plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$") plt.plot(X_underlying, y_underlying_pred, c='#f19ec2', label="predicted function") plt.ylim(-2, 1.5) plt.annotate("M={}".format(degree), xy=(0.95, -1.4))
plt.legend(loc='lower left', fontsize='x-large') plt.savefig('ml-vis3.pdf') plt.show()
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| {'w': Tensor(shape=[1], dtype=float32, place=CPUPlace, stop_gradient=True, [0.]), 'b': Tensor(shape=[1], dtype=float32, place=CPUPlace, stop_gradient=True, [0.06668118])} {'w': Tensor(shape=[1], dtype=float32, place=CPUPlace, stop_gradient=True, [-1.34165502]), 'b': Tensor(shape=[1], dtype=float32, place=CPUPlace, stop_gradient=True, [0.74084926])} {'w': Tensor(shape=[3], dtype=float32, place=CPUPlace, stop_gradient=True, [ 11.50885582, -33.70772171, 22.45395279]), 'b': Tensor(shape=[1], dtype=float32, place=CPUPlace, stop_gradient=True, [-0.17524421])} {'w': Tensor(shape=[8], dtype=float32, place=CPUPlace, stop_gradient=True, [ 6.39199781 , 38.72201538 , -377.10943604, 912.26202393, -1013.89459229, 374.96646118, 198.60220337, -150.03273010]), 'b': Tensor(shape=[1], dtype=float32, place=CPUPlace, stop_gradient=True, [3.29901624])}
|
观察可视化结果,红色的曲线表示不同阶多项式分布拟合数据的结果:
- 当 $M=0$ 或 $M=1$ 时,拟合曲线较简单,模型欠拟合;
- 当 $M=8$ 时,拟合曲线较复杂,模型过拟合;
- 当 $M=3$ 时,模型拟合最为合理。
[√] 2.3.4 - 模型评估
下面通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下sin
函数值与多项式回归值之间的误差,更加真实地反映拟合结果。多项式分布阶数从0到8进行遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| training_errors = [] test_errors = [] distribution_errors = []
for i in range(9): model = Linear(i)
X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), i) X_test_transformed = polynomial_basis_function(X_test.reshape([-1,1]), i) X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), i) optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1])) y_train_pred = model(X_train_transformed).squeeze() y_test_pred = model(X_test_transformed).squeeze() y_underlying_pred = model(X_underlying_transformed).squeeze()
train_mse = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item() training_errors.append(train_mse)
test_mse = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item() test_errors.append(test_mse)
print ("train errors: \n",training_errors) print ("test errors: \n",test_errors)
plt.rcParams['figure.figsize'] = (8.0, 6.0) plt.plot(training_errors, '-.', mfc="none", mec='#e4007f', ms=10, c='#e4007f', label="Training") plt.plot(test_errors, '--', mfc="none", mec='#f19ec2', ms=10, c='#f19ec2', label="Test")
plt.legend(fontsize='x-large') plt.xlabel("degree") plt.ylabel("MSE") plt.savefig('ml-mse-error.pdf') plt.show()
|
1 2 3 4
| train errors: [0.4672910273075104, 0.2713072597980499, 0.25924286246299744, 0.007656396366655827, 0.007648141589015722, 0.004982923157513142, 0.055855292826890945, 0.01697038859128952, 13.178251266479492] test errors: [0.48431509733200073, 0.20496585965156555, 0.22155332565307617, 0.02112610451877117, 0.021505268290638924, 0.009555519558489323, 0.07138563692569733, 0.014405457302927971, 8.89521312713623]
|
观察可视化结果:
- 当阶数较低的时候,模型的表示能力有限,训练误差和测试误差都很高,代表模型欠拟合;
- 当阶数较高的时候,模型表示能力强,但将训练数据中的噪声也作为特征进行学习,一般情况下训练误差继续降低而测试误差显著升高,代表模型过拟合。
此处多项式阶数大于等于5时,训练误差并没有下降,尤其是在多项式阶数为7时,训练误差变得非常大,请思考原因?提示:请从幂函数特性角度思考。
对于模型过拟合的情况,可以引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。下面加入$\mathcal{l_{2}}$正则化项,查看拟合结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| degree = 8 reg_lambda = 0.0001
X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), degree) X_test_transformed = polynomial_basis_function(X_test.reshape([-1,1]), degree) X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), degree)
model = Linear(degree)
optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1]))
y_test_pred=model(X_test_transformed).squeeze() y_underlying_pred=model(X_underlying_transformed).squeeze()
model_reg = Linear(degree)
optimizer_lsm(model_reg,X_train_transformed,y_train.reshape([-1,1]),reg_lambda=reg_lambda)
y_test_pred_reg=model_reg(X_test_transformed).squeeze() y_underlying_pred_reg=model_reg(X_underlying_transformed).squeeze()
mse = mean_squared_error(y_true = y_test, y_pred = y_test_pred).item() print("mse:",mse) mes_reg = mean_squared_error(y_true = y_test, y_pred = y_test_pred_reg).item() print("mse_with_l2_reg:",mes_reg)
plt.scatter(X_train, y_train, facecolor="none", edgecolor="#e4007f", s=50, label="train data") plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$") plt.plot(X_underlying, y_underlying_pred, c='#e4007f', linestyle="--", label="$deg. = 8$") plt.plot(X_underlying, y_underlying_pred_reg, c='#f19ec2', linestyle="-.", label="$deg. = 8, \ell_2 reg$") plt.ylim(-1.5, 1.5) plt.annotate("lambda={}".format(reg_lambda), xy=(0.82, -1.4)) plt.legend(fontsize='large') plt.savefig('ml-vis4.pdf') plt.show()
|
1 2
| mse: 8.89521312713623 mse_with_l2_reg: 0.009005023166537285
|
观察可视化结果,其中黄色曲线为加入$\mathcal{l_{2}}$正则后多项式分布拟合结果,红色曲线为未加入$\mathcal{l_{2}}$正则的拟合结果,黄色曲线的拟合效果明显好于红色曲线。
[√] 2.4 - Runner类介绍
通过上面的实践,我们可以看到,在一个任务上应用机器学习方法的流程基本上包括:数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。
为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个Runner类。除上述提到的要素外,再加上模型保存、模型加载等功能。
Runner类的成员函数定义如下:
- __init__函数:实例化Runner类时默认调用,需要传入模型、损失函数、优化器和评价指标等;
- train函数:完成模型训练,指定模型训练需要的训练集和验证集;
- evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
- predict函数:选取一条数据对训练好的模型进行预测;
- save_model函数:模型在训练过程和训练结束后需要进行保存;
- load_model函数:调用加载之前保存的模型。
Runner
类的框架定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Runner(object): def __init__(self, model, optimizer, loss_fn, metric): self.model = model self.optimizer = optimizer self.loss_fn = loss_fn self.metric = metric
def train(self, train_dataset, dev_dataset=None, **kwargs): pass
def evaluate(self, data_set, **kwargs): pass
def predict(self, x, **kwargs): pass
def save_model(self, save_path): pass
def load_model(self, model_path): pass
|
Runner类的流程如图2.8所示,可以分为 4 个阶段:
- 初始化阶段:传入模型、损失函数、优化器和评价指标。
- 模型训练阶段:基于训练集调用
train()
函数训练模型,基于验证集通过evaluate()
函数验证模型。通过save_model()
函数保存模型。
- 模型评价阶段:基于测试集通过
evaluate()
函数得到指标性能。
- 模型预测阶段:给定样本,通过
predict()
函数得到该样本标签。
图2.8 Runner类
[√] 2.5 - 基于线性回归的波士顿房价预测
在本节中,我们使用线性回归来对马萨诸塞州波士顿郊区的房屋进行预测。实验流程主要包含如下5个步骤:
- 数据处理:包括数据清洗(缺失值和异常值处理)、数据集划分,以便数据可以被模型正常读取,并具有良好的泛化性;
- 模型构建:定义线性回归模型类;
- 训练配置:训练相关的一些配置,如:优化算法、评价指标等;
- 组装训练框架Runner:
Runner
用于管理模型训练和测试过程;
- 模型训练和测试:利用
Runner
进行模型训练和测试。
[√] 2.5.1 - 数据处理
[√] 2.5.1.1 - 数据集介绍
本实验使用波士顿房价预测数据集,共506条样本数据,每条样本包含了12种可能影响房价的因素和该类房屋价格的中位数,各字段含义如表2.1所示:
预览前5条数据:
1 2 3 4 5 6
| import pandas as pd
data=pd.read_csv("/home/aistudio/work/boston_house_prices.csv")
data.head()
|
[√] 2.5.1.2 - 数据清洗
对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。
通过isna()
方法判断数据中各元素是否缺失,然后通过sum()
方法统计每个字段缺失情况,代码实现如下:
可以看出不存在缺失数据的情况
通过箱线图直观的显示数据分布,并观测数据中的异常值。箱线图一般由五个统计值组成:最大值、上四分位、中位数、下四分位和最小值。一般来说,观测到的数据大于最大估计值或者小于最小估计值则判断为异常值,其中
$$
最大估计值 = 上四分位 + 1.5 * (上四分位 - 下四分位)\
最小估计值=下四分位 - 1.5 * (上四分位 - 下四分位)
$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import matplotlib.pyplot as plt
def boxplot(data, fig_name): data_col = list(data.columns) plt.figure(figsize=(5, 5), dpi=300) plt.subplots_adjust(wspace=0.6) for i, col_name in enumerate(data_col): plt.subplot(3, 5, i+1) plt.boxplot(data[col_name], showmeans=True, meanprops={"markersize":1,"marker":"D","markeredgecolor":"#C54680"}, medianprops={"color":"#946279"}, whiskerprops={"color":"#8E004D", "linewidth":0.4, 'linestyle':"--"}, flierprops={"markersize":0.4}, ) plt.title(col_name, fontdict={"size":5}, pad=2) plt.yticks(fontsize=4, rotation=90) plt.tick_params(pad=0.5) plt.xticks([]) plt.savefig(fig_name) plt.show()
boxplot(data, 'ml-vis5.pdf')
|
箱线图的具体含义:
从输出结果看,数据中存在较多的异常值(图中上下边缘以外的空心小圆圈)。
使用四分位值筛选出箱线图中分布的异常值,并将这些数据视为噪声,其将被临界值取代,代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| num_features=data.select_dtypes(exclude=['object','bool']).columns.tolist()
for feature in num_features: if feature =='CHAS': continue Q1 = data[feature].quantile(q=0.25) Q3 = data[feature].quantile(q=0.75) IQR = Q3-Q1 top = Q3+1.5*IQR bot = Q1-1.5*IQR values=data[feature].values values[values > top] = top values[values < bot] = bot data[feature] = values.astype(data[feature].dtypes)
boxplot(data, 'ml-vis6.pdf')
|
从输出结果看,经过异常值处理后,箱线图中异常值得到了改善。
[√] 2.5.1.3 - 数据集划分
由于本实验比较简单,将数据集划分为两份:训练集和测试集,不包括验证集。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import paddle
paddle.seed(10)
def train_test_split(X, y, train_percent=0.8): n = len(X) shuffled_indices = paddle.randperm(n) train_set_size = int(n*train_percent) train_indices = shuffled_indices[:train_set_size] test_indices = shuffled_indices[train_set_size:]
X = X.values y = y.values
X_train=X[train_indices] y_train = y[train_indices] X_test = X[test_indices] y_test = y[test_indices]
return X_train, X_test, y_train, y_test
X = data.drop(['MEDV'], axis=1) y = data['MEDV']
X_train, X_test, y_train, y_test = train_test_split(X,y)
|
[√] 2.5.1.4 - 特征工程
为了消除纲量对数据特征之间影响,在模型训练前,需要对特征数据进行归一化处理,将数据缩放到[0, 1]区间内,使得不同特征之间具有可比性。
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import paddle
X_train = paddle.to_tensor(X_train,dtype='float32') X_test = paddle.to_tensor(X_test,dtype='float32') y_train = paddle.to_tensor(y_train,dtype='float32') y_test = paddle.to_tensor(y_test,dtype='float32')
X_min = paddle.min(X_train,axis=0) X_max = paddle.max(X_train,axis=0)
X_train = (X_train-X_min)/(X_max-X_min)
X_test = (X_test-X_min)/(X_max-X_min)
train_dataset=(X_train,y_train)
test_dataset=(X_test,y_test)
|
[√] 2.5.2 - 模型构建
实例化一个线性回归模型,特征维度为 12:
1 2 3 4 5
| from nndl.op import Linear
input_size = 12 model=Linear(input_size)
|
[√] 2.5.3 - 完善Runner类
模型定义好后,围绕模型需要配置损失函数、优化器、评估、测试等信息,以及模型相关的一些其他信息(如模型存储路径等)。
在本章中使用的Runner类为V1版本。其中训练过程通过直接求解解析解的方式得到模型参数,没有模型优化及计算损失函数过程,模型训练结束后保存模型参数。
训练配置中定义:
- 训练环境,如GPU还是CPU,本案例不涉及;
- 优化器,本案例不涉及;
- 损失函数,本案例通过平方损失函数得到模型参数的解析解;
- 评估指标,本案例利用MSE评估模型效果。
在测试集上使用MSE对模型性能进行评估。本案例利用飞桨框架提供的MSELoss API实现。
1 2
| import paddle.nn as nn mse_loss = nn.MSELoss()
|
具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import paddle import os from nndl.opitimizer import optimizer_lsm
class Runner(object): def __init__(self, model, optimizer, loss_fn, metric):
self.model=model self.metric = metric self.optimizer = optimizer def train(self,dataset,reg_lambda,model_dir): X,y = dataset self.optimizer(self.model,X,y,reg_lambda)
self.save_model(model_dir) def evaluate(self, dataset, **kwargs): X,y = dataset
y_pred = self.model(X) result = self.metric(y_pred, y)
return result
def predict(self, X, **kwargs): return self.model(X) def save_model(self, model_dir): if not os.path.exists(model_dir): os.makedirs(model_dir) params_saved_path = os.path.join(model_dir,'params.pdtensor') paddle.save(model.params,params_saved_path)
def load_model(self, model_dir): params_saved_path = os.path.join(model_dir,'params.pdtensor') self.model.params=paddle.load(params_saved_path)
optimizer = optimizer_lsm
runner = Runner(model, optimizer=optimizer,loss_fn=None, metric=mse_loss)
|
[√] 2.5.4 - 模型训练
在组装完成Runner
之后,我们将开始进行模型训练、评估和测试。首先,我们先实例化Runner
,然后开始进行装配训练环境,接下来就可以开始训练了,相关代码如下:
1 2 3 4 5
| saved_dir = '/home/aistudio/work/models'
runner.train(train_dataset,reg_lambda=0,model_dir=saved_dir)
|
打印出训练得到的权重:
1 2 3 4 5 6 7 8 9
| columns_list = data.columns.to_list() weights = runner.model.params['w'].tolist() b = runner.model.params['b'].item()
for i in range(len(weights)): print(columns_list[i],"weight:",weights[i])
print("b:",b)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| CRIM weight: -6.7268967628479 ZN weight: 1.28081214427948 INDUS weight: -0.4696650803089142 CHAS weight: 2.235346794128418 NOX weight: -7.0105814933776855 RM weight: 9.76220417022705 AGE weight: -0.8556219339370728 DIS weight: -9.265738487243652 RAD weight: 7.973038673400879 TAX weight: -4.365403175354004 PTRATIO weight: -7.105883598327637 LSTAT weight: -13.165120124816895 b: 32.1007522583008
|
[√] 2.5.5 - 模型测试
加载训练好的模型参数,在测试集上得到模型的MSE指标。
1 2 3 4 5
| runner.load_model(saved_dir)
mse = runner.evaluate(test_dataset) print('MSE:', mse.item())
|
[√] 2.5.6 - 模型预测
使用Runner
中load_model
函数加载保存好的模型,使用predict
进行模型预测,代码实现如下:
1 2 3 4
| runner.load_model(saved_dir) pred = runner.predict(X_test[:1]) print("真实房价:",y_test[:1].item()) print("预测的房价:",pred.item())
|
1 2
| 真实房价: 33.099998474121094 预测的房价: 33.04654312133789
|