目录:
1. 实现一个简单的线性回归模型
回归任务是一类典型的监督机器学习任务,对自变量和因变量之间关系进行建模分析,其预测值通常为一个连续值,比如房屋价格预测、电源票房预测等。
线性回归(Linear Regression)是指一类利用线性函数来对自变量和因变量之间关系进行建模的回归任务,是机器学习和统计学中最基础和最广泛应用的模型。
1.1 数据集构建
首先,我们构造一个小的回归数据集。假设输入特征和输出标签的维度都为 1,需要被拟合的函数定义为:
# 真实函数的参数缺省值为 w=1.2,b=0.5
def linear_func(x,w=1.2,b=0.5):
y = w*x + b
return y
然后,使用torch.rand()函数来进行随机采样输入特征
x
x
x ,并代入上面函数得到输出标签
y
y
y 。为了模拟真实环境中样本通常包含噪声的问题,我们采样过程中加入高斯噪声和异常点。
生成样本数据的函数create_toy_data实现如下:
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]
"""
# 均匀采样
# 使用paddle.rand在生成sample_num个随机数
X = torch.rand(size=[sample_num]) * (interval[1] - interval[0]) + interval[0]
y = func(X)
# 生成高斯分布的标签噪声
# 使用torch.normal生成0均值,noise标准差的数据
epsilon = torch.normal(0, noise, y.shape)
y = y + epsilon
if add_outlier: # 生成额外的异常点
outlier_num = int(len(y) * outlier_ratio)
if outlier_num != 0:
# 使用torch.randint生成服从均匀分布的、范围在[0, len(y))的随机Tensor
outlier_idx = torch.randint(len(y), size=[outlier_num])
y[outlier_idx] = y[outlier_idx] * 5
return X, y
利用上面的生成样本函数,生成 150 个带噪音的样本,其中 100 个训练样本,50 个测试样本,并打印出训练数据的可视化分布。
from matplotlib import pyplot as plt # matplotlib 是 Python 的绘图库
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)
# torch.linspace返回一个Tensor,Tensor的值为在区间start和stop上均匀间隔的num个值,输出Tensor的长度为num
X_underlying = torch.linspace(interval[0], interval[1], train_num) # 取点
y_underlying = linear_func(X_underlying) # y的值
# 绘制数据
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') # 保存图像到PDF文件中
plt.show()
运行结果:
这里主要用来测试生成样本数据的函数create_toy_data,跟下面的代码无关
1.2 模型构建
在线性回归中,自变量为样本的特征向量 x ∈ R D x ∈ R^D x∈RD(每一维对应一个自变量),因变量是连续值的标签y∈R
线性模型定义为:
f
(
x
;
w
;
b
)
=
w
T
x
+
b
,
(
2.6
)
f(x;w;b)=w^Tx+b,(2.6)
f(x;w;b)=wTx+b,(2.6)
其中权重向量
w
∈
R
D
w∈R^D
w∈RD 和偏置
b
∈
R
b∈R
b∈R 都是可学习的参数。
在实践中,为了提高预测样本的效率,我们通常会将NNN样本归为一组进行成批地预测,这样可以更好地利用GPU设备的并行计算能力。
y
=
X
w
+
b
,
(
2.7
)
y=Xw+b ,(2.7)
y=Xw+b,(2.7)
其中
X
∈
R
N
×
D
X∈R ^{N×D}
X∈RN×D为N个样本的特征矩阵,
y
∈
R
N
y∈R^N
y∈RN为N个预测值组成的列向量。
注意:在实践中,样本的矩阵
X
\boldsymbol{X}
X是由
N
N
N个
x
\boldsymbol{x}
x的行向量组成。
线性算子:
实现公式(2.7)中的线性函数非常简单,我们直接利用如下张量运算来实现。
# X: tensor, shape=[N,D]
# y_pred: tensor, shape=[N]
# w: shape=[D,1]
# b: shape=[1]
y_pred = torch.matmul(X,w)+b # torch.matmul()两式相乘
构建一个线性回归模型,代码如下:
可以直接调用模型的forward()方法进行前向执行,可以调用__call__(),从而执行在 forward() 当中定义的前向计算逻辑。
# 线性算子
class Linear():
def __init__(self, input_size):
'''
输入:
- input_size:模型要处理的数据特征向量长度
'''
self.input_size = input_size
# 模型参数
self.params = {} # 定义为一个对象
self.params['w'] = torch.randn(size=[self.input_size, 1], dtype=torch.float32) # 随机初始化一个数值
self.params['b'] = torch.zeros(size=[1], dtype=torch.float32) # 全是1
def __call__(self, X):
return self.forward(X)
# 前向函数
def forward(self, X):
'''
:param X: tensor, shape=[N,D],注意这里的X矩阵是由N个x向量的转置拼接成的,
与原教材行向量表示方式不一致
:return:
- y_pred: tensor,shape=[N]
'''
N, D = X.shape
# 大小为0
if self.input_size == 0:
return torch.full(size=[N, 1], fill_value=self.params['b'])
assert D == self.input_size # 输入数据维度合法性验证
# 使用torch.matmul 计算两个tensor的乘积
# 这行代码就是公式 2.7
y_pred = torch.matmul(X, self.params['w']) + self.params['b']
return y_pred
# 注意这里我们为了和后面章节统一,这里X矩阵是由N个x向量的转置拼接成的,
# 与原教材行向量表示方式不一致
input_size = 3
N = 2
X = torch.randn(size=[N, input_size], dtype=torch.float32)
model = Linear(input_size)
y_pred = model(X)
print("y_pred:", y_pred)
运行结果:
y_pred: tensor([[1.8529],
[0.6011]])
1.3 损失函数
回归任务是对连续值的预测,希望模型能根据数据的特征输出一个连续值作为预测值。因此回归任务中常用的评估指标是均方误差。
令
y
∈
R
N
\boldsymbol{y}\in \mathbb{R}^N
y∈RN,
y
^
∈
R
N
\hat{\boldsymbol{y}}\in \mathbb{R}^N
y^∈RN分别为
N
N
N个样本的真实标签和预测标签,均方误差的定义为:
L
(
y
,
y
^
)
=
1
2
N
∥
y
−
y
^
∥
2
=
1
2
N
∥
X
w
+
b
−
y
∥
2
,
(
2.8
)
L(y, \hat y)= {1 \over 2N}∥y− \hat y∥^2 = {1 \over 2N}∥Xw+b−y∥^2 ,(2.8)
L(y,y^)=2N1∥y−y^∥2=2N1∥Xw+b−y∥2,(2.8)
其中
b
\boldsymbol{b}
b为
N
N
N维向量,所有元素取值都为
b
b
b。
均方误差的代码实现如下:
注意:代码实现中没有除2。
import torch
# 定义一个函数
def mean_squared_error(y_true, y_pred):
'''
:param y_true: tensor,样本真实标签
:param y_pred: tensor,样本预测标签
:return:
- error:float, 误差值
'''
assert y_true.shape[0] == y_pred.shape[0]
# torch.square计算输入的平方值
# torch.mean沿dim计算 x的平均值,默认axis是None,则对输入的全部元素计算平均值。
# 公式2.8的代码实现,没有除以2
error = torch.mean(torch.square(y_true - y_pred))
return error
# 构造一个简单的样例进行测试:[N,1],N=2
y_true = torch.tensor([[-0.2], [4.9]], dtype=torch.float32)
y_pred = torch.tensor([[1.3], [2.5]], dtype=torch.float32)
error = mean_squared_error(y_true=y_true, y_pred=y_pred).item()
print("error:", error)
运行结果:
error: 4.005000114440918
思考:没有除2合理么?谈谈自己的看法。
合理,均方误差除以2作用是在模型优化时,消除因为计算均方误差中平方的梯度时存在的2倍系数,不除以2依然能够在后续优化时正确计算。
1.4 模型优化
采用经验风险最小化,线性回归可以通过最小二乘法求出参数
w
\boldsymbol{w}
w 和
b
b
b 的解析解。计算公式(2.8)中均方误差对参数
b
b
b 的偏导数,得到
∂
L
(
y
,
y
^
)
∂
b
=
1
T
(
X
w
+
b
−
y
)
,
(
2.9
)
{∂L(y, \hat y ) \over ∂b} =1^T(Xw+b−y),(2.9)
∂b∂L(y,y^)=1T(Xw+b−y),(2.9)
其中
1
\mathbf{1}
1为
N
N
N维的全1向量。这里为了简单起见省略了均方误差的系数
1
N
\frac{1}{N}
N1,并不影响最后的结果。
令上式等于0,得到
b
∗
=
y
ˉ
−
x
ˉ
T
w
,
(
2.10
)
b^∗ = \bar y − \bar x^T w,(2.10)
b∗=yˉ−xˉTw,(2.10)
其中
y
ˉ
=
1
N
1
T
y
\bar{y} = \frac{1}{N}\mathbf{1}^T\boldsymbol{y}
yˉ=N11Ty为所有标签的平均值,
x
ˉ
=
1
N
(
1
T
X
)
T
\bar{\boldsymbol{x}} = \frac{1}{N}(\mathbf{1}^T \boldsymbol{X})^T
xˉ=N1(1TX)T 为所有特征向量的平均值。将
b
∗
b^*
b∗ 代入公式(2.8)中均方误差对参数
w
\boldsymbol{w}
w的偏导数,得到
∂
L
(
y
,
y
^
)
=
(
X
−
x
ˉ
T
)
T
(
(
X
−
x
ˉ
T
)
w
−
(
y
−
y
ˉ
)
)
.
(
2.11
)
∂L(y, \hat y ) =(X− \bar x^T ) T ((X− \bar x^T )w−(y−\bar y)).(2.11)
∂L(y,y^)=(X−xˉT)T((X−xˉT)w−(y−yˉ)).(2.11)
令上式等于0,得到最优的参数为
w
∗
=
(
(
X
−
x
ˉ
T
)
T
(
X
−
x
ˉ
T
)
)
−
1
(
X
−
x
ˉ
T
)
T
(
y
−
y
ˉ
)
,
(
2.12
)
b
∗
=
y
ˉ
x
ˉ
T
w
∗
.
(
2.13
)
w^∗ =((X− \bar x^T ) T (X− \bar x^T ))^−1 (X− \bar x^T )^T (y− \bar y ),(2.12)\\ b^* =\bar{y}\bar{\boldsymbol{x}}^T\boldsymbol{w}^*.(2.13)
w∗=((X−xˉT)T(X−xˉT))−1(X−xˉT)T(y−yˉ),(2.12)b∗=yˉxˉTw∗.(2.13)
若对参数
w
\boldsymbol{w}
w加上
ℓ
2
\ell_2
ℓ2正则化,则最优的
w
∗
\boldsymbol{w}^*
w∗变为
w
∗
=
(
(
X
−
x
ˉ
T
)
T
(
X
−
x
ˉ
T
)
+
λ
I
)
−
1
(
X
−
x
ˉ
T
)
T
(
y
−
y
ˉ
)
,
(
2.14
)
w ^* =((X− \bar x^T ) T (X− \bar x^T )+λI)^−1 (X− \bar x^T )^T (y− \bar y),(2.14)
w∗=((X−xˉT)T(X−xˉT)+λI)−1(X−xˉT)T(y−yˉ),(2.14)
其中
λ
>
0
\lambda>0
λ>0为预先设置的正则化系数,
I
∈
R
D
×
D
\boldsymbol{I}\in \mathbb{R}^{D\times D}
I∈RD×D为单位矩阵。
尝试验证公式(2.14)。
参数学习的过程通过优化器完成。由于我们可以基于最小二乘方法可以直接得到线性回归的解析解,此处的训练是求解析解的过程,代码实现如下:
def optimizer_lsm(model, X, y, reg_lambda=0):
'''
:param model: 模型
:param X: tensor,特征数据,shape=[N,D]
:param y: temsor,标签数据,shape=[N]
:param reg_lambda: float,正则化系数,默认为0
:return:
- model: 优化好的模型
'''
N, D = X.shape
# 对输入特征数据所有特征向量求平均
x_bar_tran = torch.mean(X, dim=0).t() # tensor.t()转置
# 对标签的均值,shape=[1]
y_bar = torch.mean(y)
# torch.subtract通过广播的方式实现矩阵减向量
x_sub = torch.subtract(X, x_bar_tran)
# 使用torch.all判断输入tensor是否全是0
if torch.all(x_sub == 0):
model.params['b'] = y_bar
model.params['w'] = torch.zeros(size=[D])
return model
# torch.inverse求方阵的逆,这里对应于公式(2.14)
tmp = torch.inverse(torch.matmul(x_sub.T, x_sub) + reg_lambda * torch.eye(n=(D)))
w = torch.matmul(torch.matmul(tmp, x_sub.T), (y - y_bar))
# 公式(2.13)
b = y_bar - torch.matmul(x_bar_tran, w)
model.params['b'] = b
model.params['w'] = torch.squeeze(w, dim=-1) # 矩阵压缩
return model
思考1. 为什么省略了 1 N \frac{1}{N} N1不影响效果?
1 N \frac{1}{N} N1为常数, 求偏导后不会影响收敛结果。
思考 2. 什么是最小二乘法 ( Least Square Method , LSM )
最小二乘法(又称最小平方法)是一种数学优化技术。它通过最小化误差(真实目标对象与拟合目标对象的差)的平方和寻找数据的最佳函数匹配。利用最小二乘法可以简便地求得未知的数据,并使得这些求得的数据与实际数据之间误差的平方和为最小。简而言之,最小二乘法同梯度下降类似,都是一种求解无约束最优化问题的常用方法,
1.5 模型训练
在准备了数据、模型、损失函数和参数学习的实现之后,我们开始模型的训练。在回归任务中,模型的评价指标和损失函数一致,都为均方误差。
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_pred: 1.2093340158462524 b_pred: 0.8263794779777527
train error: 3.646909713745117
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)
运行结果:
w_pred large: 1.2025229930877686 b_pred large: 0.4961366057395935
train error large: 4.008014678955078
从输出结果看,预测结果与真实值 w = 1.2 \boldsymbol{w}=1.2 w=1.2, b = 0.5 b=0.5 b=0.5有一定的差距。
1.6 模型评估
下面用训练好的模型预测一下测试集的标签,并计算在测试集上的损失。
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)
运行结果:
test error: 4.337008476257324
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)
运行结果:
test error large: 4.394468307495117
动手练习:
为了加深对机器学习模型的理解,请自己动手完成以下实验:
(1) 调整训练数据的样本数量,由 100 调整到 5000,观察对模型性能的影响。
(2) 调整正则化系数,观察对模型性能的影响。
第一题:
train_num = 5000 # 训练样本数目
test_num = 50 # 测试样本数目
# 后续一样
运行结果:
w_pred: 1.2008944749832153 b_pred: 0.5172439813613892
train error: 4.0114874839782715
训练样本增加后,w和b的预测值更接近真实值(w=1.2,b=0.5)
第二题:
当正则化系数变大后:(这里的代码是2.5 模型训练)
# 这里将正则化系数调整为50
input_size = 1
model = Linear(input_size)
model = optimizer_lsm(model, X_train.reshape([-1, 1]), y_train.reshape([-1, 1]),reg_lambda=50)
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)
model_large = Linear(input_size)
model_large = optimizer_lsm(model_large, X_train_large.reshape([-1, 1]), y_train_large.reshape([-1, 1]),reg_lambda=50)
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)
运行结果:
w_pred: 1.2039399147033691 b_pred: 0.8823827505111694
train error: 2.8416028022766113
w_pred large: 1.1993708610534668 b_pred large: 0.425101637840271
train error large: 4.069814682006836
正则化不合适时,对w与b的影响很大
2. 多项式回归
多项式回归是回归任务的一种形式,其中自变量和因变量之间的关系是MMM次多项式的一种线性回归形式,即:
f
(
x
;
w
)
=
w
1
x
+
w
2
x
2
+
.
.
.
+
w
M
x
M
+
b
=
w
T
ϕ
(
x
)
+
b
,
(
2.10
)
f(x;w)=w_1 x+w_2 x^2 +...+w_M x^M +b=w^Tϕ(x)+b,(2.10)
f(x;w)=w1x+w2x2+...+wMxM+b=wTϕ(x)+b,(2.10)
其中
M
M
M为多项式的阶数,
w
=
[
w
1
,
.
.
.
,
w
M
]
T
\boldsymbol{w}=[w_1,...,w_M]^T
w=[w1,...,wM]T为多项式的系数,
ϕ
(
x
)
=
[
x
,
x
2
,
⋯
,
x
M
]
T
\phi(x)=[x,x^2,\cdots,x^M]^T
ϕ(x)=[x,x2,⋯,xM]T为多项式基函数,将原始特征
x
x
x映射为
M
M
M维的向量。当
M
=
0
M=0
M=0时,
f
(
x
;
w
)
=
b
f(\boldsymbol{x};\boldsymbol{w})=b
f(x;w)=b。
公式(2.10)展示的是特征维度为1的多项式表达,当特征维度大于1时,存在不同特征之间交互的情况,这是线性回归无法实现。公式(2.11)展示的是当特征维度为2,多项式阶数为2时的多项式回归:
f
(
x
;
w
)
=
w
1
x
1
+
w
2
x
2
+
w
3
x
1
2
+
w
4
x
1
x
2
+
w
5
x
2
2
+
b
,
(
2.11
)
f(x;w)=w_1 x_1 +w_2 x_2 +w_3 x_1^2 +w_4 x_1 x_2 +w_5 x_2^2 +b,(2.11)
f(x;w)=w1x1+w2x2+w3x12+w4x1x2+w5x22+b,(2.11)
当自变量和因变量之间并不是线性关系时,我们可以定义非线性基函数对特征进行变换,从而可以使得线性回归算法实现非线性的曲线拟合。
接下来我们基于特征维度为1的自变量介绍多项式回归实验。
2.1 数据集构建
假设我们要拟合的非线性函数为一个缩放后的 s i n sin sin函数。
# sin函数: sin(2 * pi * x)
def sin(x):
y = torch.sin(2 * math.pi * x)
return y
这里仍然使用前面定义的 create_toy_data 函数来构建训练和测试数据,其中训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。
# 这里仍然使用前面定义的create_toy_data函数来构建训练和测试数据,
# 其中训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。
func = sin
interval = (0, 1)
train_num = 15
test_num = 10
noise = 0.5
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_underlying = torch.linspace(interval[0], interval[1], steps=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.scatter(X_test, y_test, facecolor="none", edgecolor="r", s=50, label="test 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()
运行结果:
在输出结果中,绿色的曲线是周期为 1 的
s
i
n
sin
sin 函数曲线,蓝色的圆圈为生成的训练样本数据,红色的圆圈为生成的测试样本数据。
2.2 模型构建
通过多项式的定义可以看出,多项式回归和线性回归一样,同样学习参数 w \boldsymbol{w} w ,只不过需要对输入特征 ϕ ( x ) \phi(x) ϕ(x) 根据多项式阶数进行变换。因此,我们可以套用求解线性回归参数的方法来求解多项式回归参数。
首先,我们实现多项式基函数polynomial_basis_function对原始特征 x x x进行转换。
def polynomial_basis_function(x, degree=2):
'''
:param x: tensor,输入的数据,shape=[N,1]
:param 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
:return:
- x_result: tensor
'''
#如果阶数为0
if degree == 0:
return torch.ones(size=x.shape, dtype=torch.float32)
x_tmp = x
x_result = x_tmp
for i in range(2, degree + 1):
x_tmp = torch.multiply(x_tmp, x) # 逐元素相乘
x_result = torch.concat((x_result, x_tmp), dim=-1)
return x_result
# 简单测试
data = [[2], [3], [4]]
X = torch.tensor(data=data, dtype=torch.float32)
degree = 3
transformed_X = polynomial_basis_function(X, degree=degree)
print("转换前:", X)
print("阶数为", degree, "转换后:", transformed_X)
运行结果:
转换前: tensor([[2.],
[3.],
[4.]])
阶数为 3 转换后: tensor([[ 2., 4., 8.],
[ 3., 9., 27.],
[ 4., 16., 64.]])
2.3 模型训练
对于多项式回归,我们可以同样使用前面线性回归中定义的LinearRegression算子、训练函数train、均方误差函数mean_squared_error。拟合训练数据的目标是最小化损失函数,同线性回归一样,也可以通过矩阵运算直接求出 w \boldsymbol{w} w的值。
我们设定不同的多项式阶, M M M的取值分别为0、1、3、8,之前构造的训练集上进行训练,观察样本数据对 s i n sin sin曲线的拟合结果。
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(bbox_to_anchor=(1.05, 0.64), loc=2, borderaxespad=0.)
plt.legend(loc='lower left', fontsize='x-large')
plt.savefig('ml-vis3.pdf')
plt.show()
运行结果:
{'w': tensor([0.]), 'b': tensor(-0.1441)}
{'w': tensor([-0.9384]), 'b': tensor([0.3150])}
{'w': tensor([ 12.5155, -34.8445, 23.1802]), 'b': tensor([-0.3165])}
{'w': tensor([ 28.5825, -102.2395, -195.0573, 1368.8866, -1590.4924, -1107.0732,
3001.4561, -1407.4734]), 'b': tensor([-0.8036])}
观察可视化结果,红色的曲线表示不同阶多项式分布拟合数据的结果:
- 当 M = 0 M=0 M=0 或 M = 1 M=1 M=1 时,拟合曲线较简单,模型欠拟合;
- 当 M = 8 M=8 M=8 时,拟合曲线较复杂,模型过拟合;
- 当 M = 3 M=3 M=3 时,模型拟合最为合理。
2.4 模型评估
下面通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下 s i n sin sin 函数值与多项式回归值之间的误差,更加真实地反映拟合结果。多项式分布阶数从0到8进行遍历。
# 训练误差和测试误差
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)
# distribution_mse = mean_squared_error(y_true=y_underlying, y_pred=y_underlying_pred).item()
# distribution_errors.append(distribution_mse)
print("train errors: \n", training_errors)
print("test errors: \n", test_errors)
# print ("distribution errors: \n", distribution_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.plot(distribution_errors, '-', mfc="none", mec="#3D3D3F", ms=10, c="#3D3D3F", label="Distribution")
plt.legend(fontsize='x-large')
plt.xlabel("degree")
plt.ylabel("MSE")
plt.savefig('ml-mse-error.pdf')
plt.show()
运行结果:
train errors:
[0.6044527888298035, 0.37083032727241516, 0.36677995324134827, 0.2636880576610565, 0.26361265778541565, 0.23196382820606232, 0.20245333015918732, 0.1918744593858719, 0.476275235414505]
test errors:
[0.33158332109451294, 0.1769583523273468, 0.18401207029819489, 0.1404038369655609, 0.13875068724155426, 0.3831053078174591, 0.6612722277641296, 2.1085290908813477, 2.7414708137512207]
观察可视化结果:
- 当阶数较低的时候,模型的表示能力有限,训练误差和测试误差都很高,代表模型欠拟合;
- 当阶数较高的时候,模型表示能力强,但将训练数据中的噪声也作为特征进行学习,一般情况下训练误差继续降低而测试误差显著升高,代表模型过拟合。
此处多项式阶数大于等于5时,训练误差并没有下降,尤其是在多项式阶数为7时,训练误差变得非常大,请思考原因?提示:请从幂函数特性角度思考。
对于模型过拟合的情况,可以引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。下面加入 l 2 \mathcal{l_{2}} l2正则化项,查看拟合结果。
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()
运行结果:
mse: 2.7414708137512207
mse_with_l2_reg: 0.2308414876461029
观察可视化结果,其中黄色曲线为加入
l
2
\mathcal{l_{2}}
l2正则后多项式分布拟合结果,红色曲线为未加入
l
2
\mathcal{l_{2}}
l2正则的拟合结果,黄色曲线的拟合效果明显好于红色曲线。
##2.5 思考
如果训练数据中存在一些异常样本,会对最终模型有何影响?怎样处理可以尽可能减少异常样本对模型的影响?
- 会使最终模型偏离正确值
- 检测数据中的离群点,异常数据的特征值与正常数据的特征值距离较远,减少异常样本
3. Runner类介绍
通过上面的实践,我们可以看到,在一个任务上应用机器学习方法的流程基本上包括:数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。
为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个 R u n n e r Runner Runner类。除上述提到的要素外,再加上模型保存、模型加载等功能。
R u n n e r Runner Runner类的成员函数定义如下:
- __init__函数:实例化Runner类时默认调用,需要传入模型、损失函数、优化器和评价指标等;
- train函数:完成模型训练,指定模型训练需要的训练集和验证集;
- evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
- predict函数:选取一条数据对训练好的模型进行预测;
- save_model函数:模型在训练过程和训练结束后需要进行保存;
- load_model函数:调用加载之前保存的模型。
Runner类的框架定义如下:
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()函数得到该样本标签。
3.0 基于线性回归的波士顿房价预测
在本节中,我们使用线性回归来对马萨诸塞州波士顿郊区的房屋进行预测。实验流程主要包含如下5个步骤:
数据处理:包括数据清洗(缺失值和异常值处理)、数据集划分,以便数据可以被模型正常读取,并具有良好的泛化性;
- 模型构建:定义线性回归模型类;
- 训练配置:训练相关的一些配置,如:优化算法、评价指标等;
- 组装训练框架Runner:Runner用于管理模型训练和测试过程;
- 模型训练和测试:利用Runner进行模型训练和测试。
3.1 数据处理
3.1.1 数据集介绍
本实验使用波士顿房价预测数据集,共506条样本数据,每条样本包含了12种可能影响房价的因素和该类房屋价格的中位数,各字段含义如表2.1所示:
预览前5条数据,代码实现如下:
import pandas as pd # 开源数据分析和操作工具
data = pd.read_csv("boston_house_prices.csv")
print(data.head()) # 输出前五行
运行结果:
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO LSTAT MEDV
0 0.00632 18.0 2.31 0 0.538 6.575 65.2 4.0900 1 296 15.3 4.98 24.0
1 0.02731 0.0 7.07 0 0.469 6.421 78.9 4.9671 2 242 17.8 9.14 21.6
2 0.02729 0.0 7.07 0 0.469 7.185 61.1 4.9671 2 242 17.8 4.03 34.7
3 0.03237 0.0 2.18 0 0.458 6.998 45.8 6.0622 3 222 18.7 2.94 33.4
4 0.06905 0.0 2.18 0 0.458 7.147 54.2 6.0622 3 222 18.7 5.33 36.2
3.1.2 数据清洗
对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。
- 缺失值分析
通过isna()方法判断数据中各元素是否缺失,然后通过sum()方法统计每个字段缺失情况,代码实现如下:
# 查看各字段缺失值统计情况
data.isna().sum()
运行结果:
CRIM 0
ZN 0
INDUS 0
CHAS 0
NOX 0
RM 0
AGE 0
DIS 0
RAD 0
TAX 0
PTRATIO 0
LSTAT 0
MEDV 0
dtype: int64
从输出结果看,波士顿房价预测数据集中不存在缺失值的情况。
- 异常值处理
通过箱线图直观的显示数据分布,并观测数据中的异常值。箱线图一般由五个统计值组成:最大值、上四分位、中位数、下四分位和最小值。一般来说,观测到的数据大于最大估计值或者小于最小估计值则判断为异常值,其中
最大估计值
=
上四分位
+
1.5
∗
(
上四分位
−
下四分位
)
最小估计值
=
下四分位
−
1.5
∗
(
上四分位
−
下四分位
)
最大估计值=上四分位+1.5∗(上四分位−下四分位)\\ 最小估计值=下四分位−1.5∗(上四分位−下四分位)
最大估计值=上四分位+1.5∗(上四分位−下四分位)最小估计值=下四分位−1.5∗(上四分位−下四分位)
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={},
medianprops={"color":"#946279"}, # 中位数线的属性
whiskerprops={"color":"#8E004D","linewidth":0.4,'linestyle':"--"},
flierprops={"markersize":0.4}
)
# 图名
plt.title(col_name,fontdict={"size":5},pad=2)
# y方向梯度
plt.yticks(fontsize=4,rotation=90)
plt.tick_params(pad=0.5)
# x方向梯度
plt.xticks([])
plt.savefig(fig_name)
plt.show()
boxplot(data, 'ml-vis5.pdf')
运行结果:
图2.4是箱线图的一个示例,可对照查看具体含义。
从输出结果看,数据中存在较多的异常值(图中上下边缘以外的空心小圆圈)。
使用四分位值筛选出箱线图中分布的异常值,并将这些数据视为噪声,其将被临界值取代,代码实现如下:
# 四分位处理异常值
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')
运行结果:
从输出结果看,经过异常值处理后,箱线图中异常值得到了改善。
3.1.3 数据集划分
由于本实验比较简单,将数据集划分为两份:训练集和测试集,不包括验证集。
具体代码如下:
import torch
def train_test_split(X, y, train_percent=0.8):
n = len(X)
shuffled_indices = torch.randperm(n) # 返回以恶个数值在0到n-1、随机排列的1-D Tensor
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)# X_train每一行是个样本,shape[N,D]
3.1.4 特征工程
为了消除纲量对数据特征之间影响,在模型训练前,需要对特征数据进行归一化处理,将数据缩放到[0, 1]区间内,使得不同特征之间具有可比性。
代码实现如下:
import torch
X_train = torch.tensor(X_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32)
X_min = torch.min(X_train, 0).values
X_max = torch.max(X_train, 0).values
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)
3.2 模型构建
实例化一个线性回归模型,特征维度为 12:
from op import Linear
# 模型实例化
input_size = 12
model=Linear(input_size)
op的代码如下:
import torch
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" # 这里报错
torch.manual_seed(10) #设置随机种子
class Op(object):
def __init__(self):
pass
def __call__(self, inputs):
return self.forward(inputs)
def forward(self, inputs):
raise NotImplementedError
def backward(self, inputs):
raise NotImplementedError
# 线性算子
class Linear(Op):
def __init__(self,input_size):
"""
输入:
- input_size:模型要处理的数据特征向量长度
"""
self.input_size = input_size
# 模型参数
self.params = {}
self.params['w'] = torch.randn(size=[self.input_size,1],dtype=torch.float32)
self.params['b'] = torch.zeros(size=[1],dtype=torch.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 torch.full(size=[N,1], fill_value=self.params['b'])
assert D==self.input_size # 输入数据维度合法性验证
# 使用torch.matmul计算两个tensor的乘积
y_pred = torch.matmul(X,self.params['w'])+self.params['b']
return y_pred
3.3 完善Runner类
模型定义好后,围绕模型需要配置损失函数、优化器、评估、测试等信息,以及模型相关的一些其他信息(如模型存储路径等)。
在本章中使用的Runner类为V1版本。其中训练过程通过直接求解解析解的方式得到模型参数,没有模型优化及计算损失函数过程,模型训练结束后保存模型参数。
训练配置中定义:
- 训练环境,如GPU还是CPU,本案例不涉及;
- 优化器,本案例不涉及;
- 损失函数,本案例通过平方损失函数得到模型参数的解析解;
- 评估指标,本案例利用MSE评估模型效果。
在测试集上使用MSE对模型性能进行评估。
mse_loss = torch.nn.MSELoss()
这里 o p t i m i z e r l s m . p y optimizer_lsm.py optimizerlsm.py的内容:
import torch
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 = torch.mean(X,dim=0).t()
# 求标签的均值,shape=[1]
y_bar = torch.mean(y)
# torch.subtract通过广播的方式实现矩阵减向量
x_sub = torch.subtract(X,x_bar_tran)
# 使用torch.all判断输入tensor是否全0
if torch.all(x_sub==0):
model.params['b'] = y_bar
model.params['w'] = torch.zeros(size=[D])
return model
# torch.inverse求方阵的逆
tmp = torch.inverse(torch.matmul(x_sub.T,x_sub)+
reg_lambda*torch.eye(n = (D)))
w = torch.matmul(torch.matmul(tmp,x_sub.T),(y-y_bar))
b = y_bar-torch.matmul(x_bar_tran,w)
model.params['b'] = b
model.params['w'] = torch.squeeze(w,dim=-1)
return model
具体实现如下:
import torch
from op import Linear
from opitimizer import optimizer_lsm
class Runner(object):
def __init__(self, model, optimizer, loss_fn, metric):
# 优化器和损失函数为None,不再关注
# 模型
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')
torch.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=torch.load(params_saved_path)
optimizer = optimizer_lsm
# 实例化Runner
runner = Runner(model, optimizer=optimizer,loss_fn=None, metric=mse_loss)
3.4 模型训练
在组装完成Runner之后,我们将开始进行模型训练、评估和测试。首先,我们先实例化Runner,然后开始进行装配训练环境,接下来就可以开始训练了,相关代码如下:
# 模型保存文件夹
saved_dir = 'models'
# 启动训练
runner.train(train_dataset,reg_lambda=0,model_dir=saved_dir)
# 打印出训练得到的权重:
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)
运行结果:
CRIM weight: -5.2611470222473145
ZN weight: 1.3627078533172607
INDUS weight: -0.02481861039996147
CHAS weight: 1.8001954555511475
NOX weight: -7.556739330291748
RM weight: 9.55705451965332
AGE weight: -1.3511563539505005
DIS weight: -9.967967987060547
RAD weight: 7.528563022613525
TAX weight: -5.082488059997559
PTRATIO weight: -6.9966607093811035
LSTAT weight: -13.183674812316895
b: 32.621612548828125
从输出结果看,CRIM、PTRATIO等的权重为负数,表示该镇的人均犯罪率与房价负相关,学生与教师比例越大,房价越低。RAD和CHAS等为正,表示到径向公路的可达性指数越高,房价越高;临近Charles River房价高。
3.5 模型测试
加载训练好的模型参数,在测试集上得到模型的MSE指标。
# 加载模型权重
runner.load_model(saved_dir)
mse = runner.evaluate(test_dataset)
print('MSE:', mse.item())
运行结果:
MSE: 11.21076488494873
3.6 模型预测
使用Runner中load_model函数加载保存好的模型,使用predict进行模型预测,代码实现如下:
runner.load_model(saved_dir)
pred = runner.predict(X_test[:1])
print("真实房价:",y_test[:1].item())
print("预测的房价:",pred.item())
运行结果:
真实房价: 18.899999618530273
预测的房价: 21.529163360595703
5. 机器学习概述
机器学习(Machine Learning,ML)就是让计算机从数据中进行自动学习,得到某种知识(或规律)。作为一门学科,机器学习通常指一类问题以及解决这类问题的方法,即如何从观测数据(样本)中寻找规律,并利用学习到的规律(模型)对未知或无法观测的数据进行预测。
在学习本章内容前,建议您先阅读《神经网络与深度学习》第 2 章:机器学习概述的相关内容,关键知识点如图2.1所示,以便更好的理解和掌握相应的理论知识,及其在实践中的应用方法。
本章内容基于《神经网络与深度学习》第 2 章:机器学习概述 相关内容进行设计,主要包含两部分:
- 模型解读:介绍机器学习实践五要素(数据、模型、学习准则、优化算法、评估指标)的原理剖析和相应的代码实现。通过理论和代码的结合,加深对机器学习的理解;
- 案例实践:基于机器学习线性回归方法,通过数据处理、模型构建、训练配置、组装训练框架Runner、模型训练和模型预测等过程完成波士顿房价预测任务。
机器学习实践五要素
要通过机器学习来解决一个特定的任务时,我们需要准备5个方面的要素:
- 数据集:收集任务相关的数据集用来进行模型训练和测试,可分为训练集、验证集和测试集;
- 模型:实现输入到输出的映射,通常为可学习的函数;
- 学习准则:模型优化的目标,通常为损失函数和正则化项的加权组合;
- 优化算法:根据学习准则优化机器学习模型的参数;
- 评价指标:用来评价学习到的机器学习模型的性能.
图2.2给出实现一个完整的机器学习系统的主要环节和要素。从流程角度看,实现机器学习系统可以分为两个阶段:训练阶段和评价阶段。训练阶段需要用到训练集、验证集、待学习的模型、损失函数、优化算法,输出学习到的模型;评价阶段也称为测试阶段,需要用到测试集、学习到的模型、评价指标体系,得到模型的性能评价。
在本节中,我们已经对这五个要素进行简单的介绍。
1. 数据
在实践中,数据的质量会很大程度上影响模型最终的性能,通常数据预处理是完成机器学习实践的第一步,噪音越少、规模越大、覆盖范围越广的数据集往往能够训练出性能更好的模型。数据预处理可分为两个环节:先对收集到的数据进行基本的预处理,如基本的统计、特征归一化和异常值处理等;再将数据划分为训练集、验证集(开发集)和测试集。
- 训练集: 用于模型训练时调整模型的参数,在这份数据集上的误差被称为训练误差;
- 验证集(开发集): 对于复杂的模型,常常有一些超参数需要调节,因此需要尝试多种超参数的组合来分别训练多个模型,然后对比它们在验证集上的表现,选择一组相对最好的超参数,最后才使用这组参数下训练的模型在测试集上评估测试误差。
- 测试集: 模型在这份数据集上的误差被称为测试误差。训练模型的目的是为了通过从训练数据中找到规律来预测未知数据,因此测试误差是更能反映出模型表现的指标。
数据划分时要考虑到两个因素:更多的训练数据会降低参数估计的方差,从而得到更可信的模型;而更多的测试数据会降低测试误差的方差,从而得到更可信的测试误差。如果给定的数据集没有做任何划分,我们一般可以大致按照7:3或者8:2的比例划分训练集和测试集,再根据7:3或者8:2的比例从训练集中再次划分出训练集和验证集。
需要强调的是,测试集只能用来评测模型最终的性能,在整个模型训练过程中不能有测试集的参与。
2. 模型
有了数据后,我们可以用数据来训练模型。我们希望能让计算机从一个函数集合
F
=
f
1
(
x
)
,
f
2
(
x
)
,
⋯
F={f1(x),f2(x),⋯ }
F=f1(x),f2(x),⋯ 中 自动寻找一个“最优”的函数
f
∗
(
x
)
f^∗(\boldsymbol{x})
f∗(x) 来近似每个样本的特征向量
x
\boldsymbol{x}
x 和标签
y
y
y 之间 的真实映射关系,实际上这个函数集合也被称为假设空间,在实际问题中,假设空间
F
\mathcal{F}
F通常为一个参数化的函数族
F
=
f
(
x
;
θ
)
∣
θ
∈
R
D
,
(
2.1
)
F={f(x;θ)∣θ∈R^D},(2.1)
F=f(x;θ)∣θ∈RD,(2.1)
其中
f
(
x
;
θ
)
f(\boldsymbol{x} ; \theta)
f(x;θ)是参数为
θ
\theta
θ的函数,也称为模型,𝐷 为参数的数量。
常见的假设空间可以分为线性和非线性两种,对应的模型
f
f
f 也分别称为线性模型和非线性模型。线性模型的假设空间为一个参数化的线性函数族,即:
f
(
x
;
θ
)
=
w
⊤
x
+
b
,
(
2.2
)
f(\boldsymbol{x};θ)=\boldsymbol{w}^⊤ x+b,(2.2)
f(x;θ)=w⊤x+b,(2.2)
其中参数
θ
\theta
θ 包含了权重向量
w
\boldsymbol{w}
w和偏置
b
b
b。
线性模型可以由非线性基函数
ϕ
(
x
)
\phi(\boldsymbol{x})
ϕ(x)变为非线性模型,从而增强模型能力:
f
(
x
;
θ
)
=
w
⊤
ϕ
(
x
)
+
b
,
(
2.3
)
f(\boldsymbol{x};θ)=\boldsymbol{w}^⊤\phi(\boldsymbol{x})+b,(2.3)
f(x;θ)=w⊤ϕ(x)+b,(2.3)
其中
ϕ
(
x
)
=
[
ϕ
1
(
x
)
,
ϕ
2
(
x
)
,
⋯
,
ϕ
K
(
x
)
]
⊤
\phi(\boldsymbol{x})=\left[\phi_{1}(\boldsymbol{x}), \phi_{2}(\boldsymbol{x}), \cdots, \phi_{K}(\boldsymbol{x})\right]^{\top}
ϕ(x)=[ϕ1(x),ϕ2(x),⋯,ϕK(x)]⊤为
𝐾
𝐾
K 个非线性基函数组成的向量,参数
θ
\theta
θ 包含了权重向量
w
\boldsymbol{w}
w和偏置
b
b
b。
3. 学习准则
为了衡量一个模型的好坏,我们需要定义一个损失函数 L ( y , f ( x ; θ ) ) \mathcal{L}(\boldsymbol{y},f(\boldsymbol{x};\theta)) L(y,f(x;θ))。损失函数是一个非负实数函数,用来量化模型预测标签和真实标签之间的差异。常见的损失函数有 0-1 损失、平方损失函数、交叉熵损失函数等。
机器学习的目标就是找到最优的模型 𝑓 ( 𝒙 ; θ ∗ ) 𝑓(𝒙;\theta^∗) f(x;θ∗)在真实数据分布上损失函数的期望最小。然而在实际中,我们无法获得真实数据分布,通常会用在训练集上的平均损失替代。
一个模型在训练集
D
=
{
(
x
(
n
)
,
y
(
n
)
)
}
n
=
1
N
\mathcal{D}=\{(\boldsymbol{x}^{(n)},y^{(n)})\}_{n=1}^N
D={(x(n),y(n))}n=1N上的平均损失称为经验风险{Empirical Risk},即:
R
D
e
m
p
(
θ
)
=
1
N
∑
n
=
1
N
L
(
y
(
n
)
,
f
(
x
(
n
)
;
θ
)
)
。
(
2.4
)
R_D^{emp}(θ)= {1 \over N}\sum^N_{n=1} L(y^{(n)},f(x^{(n)};θ))。(2.4)
RDemp(θ)=N1n=1∑NL(y(n),f(x(n);θ))。(2.4)
L
(
y
,
f
(
x
;
θ
)
)
L(y,f(x;θ))
L(y,f(x;θ))为损失函数。损失函数是一个非负实数函数,用来量化模型预测和真实标签之间的差异。常见的损失函数有0-1损失、平方损失函数、交叉熵损失函数等。
在通常情况下,我们可以通过使得经验风险最小化来获得具有预测能力的模型。然而,当模型比较复杂或训练数据量比较少时,经验风险最小化获得的模型在测试集上的效果比较差。而模型在测试集上的性能才是我们真正关心的指标.当一个模型在训练集错误率很低,而在测试集上错误率较高时,通常意味着发生了过拟合(Overfitting)现象。为了缓解模型的过拟合问题,我们通常会在经验损失上加上一定的正则化项来限制模型能力。
过拟合通常是由于模型复杂度比较高引起的。在实践中,最常用的正则化方式有对模型的参数进行约束,比如
ℓ
1
\ell_1
ℓ1或者
ℓ
2
\ell_2
ℓ2范数约束。这样,我们就得到了结构风险(Structure Risk)。
R
D
s
t
r
u
c
t
(
θ
)
=
R
D
e
m
p
(
θ
)
+
λ
ℓ
p
(
θ
)
,
(
2.5
)
R_D^{struct}(θ)=R_D^{emp}(θ)+λℓ_p(θ),(2.5)
RDstruct(θ)=RDemp(θ)+λℓp(θ),(2.5)
其中
λ
\lambda
λ为正则化系数,
p
=
1
p=1
p=1或
2
2
2表示
ℓ
1
\ell_1
ℓ1或者
ℓ
2
\ell_2
ℓ2 范数。
4. 优化算法
在有了优化目标之后,机器学习问题就转化为优化问题,我们可以利用已知的优化算法来学习最优的参数。当优化函数为凸函数时,我们可以令参数的偏导数等于0来计算最优参数的解析解。当优化函数为非凸函数时,我们可以用一阶的优化算法来进行优化。
目前机器学习中最常用的优化算法是梯度下降法(Gradient Descent Method)。 当使用梯度下降法进行参数优化时,还可以利用验证集来早停法(Early-Stop)来中止模型的优化过程,避免模型在训练集上过拟合。早停法也是一种常用并且十分有效的正则化方法。
5. 评估指标
评估指标(Metric)用于评价模型效果,即给定一个测试集,用模型对测试集中的每个样本进行预测,并根据预测结果计算评价分数。回归任务的评估指标一般有预测值与真实值的均方差,分类任务的评估指标一般有准确率、召回率、F1值等。
对于一个机器学习任务,一般会先确定任务类型,再确定任务的评价指标,再根据评价指标来建立模型,选择学习准则。由于评价指标不可微等问题有时候学习准则并不能完全和评价指标一致,我们往往会选择一定的损失函数使得两者尽可能一致。
本来想拆分出来,一点点的写,但是太多了,只好大片大片的写,但是基本的注释都写在代码里了
点赞 + 收藏 + 关注!!!
如有错误与建议,望告知!!!(将于下篇文章更正)
请多多关注我!!!谢谢!!