赞
踩
回归任务是一类典型的监督机器学习任务,对自变量和因变量之间关系进行建模分析,其预测值通常为一个连续值,比如房屋价格预测、电源票房预测等。
线性回归(Linear Regression)是指一类利用线性函数来对自变量和因变量之间关系进行建模的回归任务,是机器学习和统计学中最基础和最广泛应用的模型。
首先,我们构造一个小的回归数据集。假设输入特征和输出标签的维度都为 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,跟下面的代码无关
在线性回归中,自变量为样本的特征向量 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]])
回归任务是对连续值的预测,希望模型能根据数据的特征输出一个连续值作为预测值。因此回归任务中常用的评估指标是均方误差。
令
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依然能够在后续优化时正确计算。
采用经验风险最小化,线性回归可以通过最小二乘法求出参数
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为单位矩阵。
参数学习的过程通过优化器完成。由于我们可以基于最小二乘方法可以直接得到线性回归的解析解,此处的训练是求解析解的过程,代码实现如下:
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 )
最小二乘法(又称最小平方法)是一种数学优化技术。它通过最小化误差(真实目标对象与拟合目标对象的差)的平方和寻找数据的最佳函数匹配。利用最小二乘法可以简便地求得未知的数据,并使得这些求得的数据与实际数据之间误差的平方和为最小。简而言之,最小二乘法同梯度下降类似,都是一种求解无约束最优化问题的常用方法,
在准备了数据、模型、损失函数和参数学习的实现之后,我们开始模型的训练。在回归任务中,模型的评价指标和损失函数一致,都为均方误差。
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有一定的差距。
下面用训练好的模型预测一下测试集的标签,并计算在测试集上的损失。
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的影响很大
多项式回归是回归任务的一种形式,其中自变量和因变量之间的关系是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的自变量介绍多项式回归实验。
假设我们要拟合的非线性函数为一个缩放后的 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 函数曲线,蓝色的圆圈为生成的训练样本数据,红色的圆圈为生成的测试样本数据。
通过多项式的定义可以看出,多项式回归和线性回归一样,同样学习参数 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.]])
对于多项式回归,我们可以同样使用前面线性回归中定义的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])}
观察可视化结果,红色的曲线表示不同阶多项式分布拟合数据的结果:
下面通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下 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 思考
如果训练数据中存在一些异常样本,会对最终模型有何影响?怎样处理可以尽可能减少异常样本对模型的影响?
- 会使最终模型偏离正确值
- 检测数据中的离群点,异常数据的特征值与正常数据的特征值距离较远,减少异常样本
通过上面的实践,我们可以看到,在一个任务上应用机器学习方法的流程基本上包括:数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。
为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个 R u n n e r Runner Runner类。除上述提到的要素外,再加上模型保存、模型加载等功能。
R u n n e r Runner Runner类的成员函数定义如下:
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 个阶段:
在本节中,我们使用线性回归来对马萨诸塞州波士顿郊区的房屋进行预测。实验流程主要包含如下5个步骤:
数据处理:包括数据清洗(缺失值和异常值处理)、数据集划分,以便数据可以被模型正常读取,并具有良好的泛化性;
本实验使用波士顿房价预测数据集,共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
对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。
# 查看各字段缺失值统计情况
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')
运行结果:
从输出结果看,经过异常值处理后,箱线图中异常值得到了改善。
由于本实验比较简单,将数据集划分为两份:训练集和测试集,不包括验证集。
具体代码如下:
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]
为了消除纲量对数据特征之间影响,在模型训练前,需要对特征数据进行归一化处理,将数据缩放到[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)
实例化一个线性回归模型,特征维度为 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
模型定义好后,围绕模型需要配置损失函数、优化器、评估、测试等信息,以及模型相关的一些其他信息(如模型存储路径等)。
在本章中使用的Runner类为V1版本。其中训练过程通过直接求解解析解的方式得到模型参数,没有模型优化及计算损失函数过程,模型训练结束后保存模型参数。
训练配置中定义:
在测试集上使用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)
在组装完成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房价高。
加载训练好的模型参数,在测试集上得到模型的MSE指标。
# 加载模型权重
runner.load_model(saved_dir)
mse = runner.evaluate(test_dataset)
print('MSE:', mse.item())
运行结果:
MSE: 11.21076488494873
使用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
机器学习(Machine Learning,ML)就是让计算机从数据中进行自动学习,得到某种知识(或规律)。作为一门学科,机器学习通常指一类问题以及解决这类问题的方法,即如何从观测数据(样本)中寻找规律,并利用学习到的规律(模型)对未知或无法观测的数据进行预测。
在学习本章内容前,建议您先阅读《神经网络与深度学习》第 2 章:机器学习概述的相关内容,关键知识点如图2.1所示,以便更好的理解和掌握相应的理论知识,及其在实践中的应用方法。
本章内容基于《神经网络与深度学习》第 2 章:机器学习概述 相关内容进行设计,主要包含两部分:
要通过机器学习来解决一个特定的任务时,我们需要准备5个方面的要素:
图2.2给出实现一个完整的机器学习系统的主要环节和要素。从流程角度看,实现机器学习系统可以分为两个阶段:训练阶段和评价阶段。训练阶段需要用到训练集、验证集、待学习的模型、损失函数、优化算法,输出学习到的模型;评价阶段也称为测试阶段,需要用到测试集、学习到的模型、评价指标体系,得到模型的性能评价。
在本节中,我们已经对这五个要素进行简单的介绍。
在实践中,数据的质量会很大程度上影响模型最终的性能,通常数据预处理是完成机器学习实践的第一步,噪音越少、规模越大、覆盖范围越广的数据集往往能够训练出性能更好的模型。数据预处理可分为两个环节:先对收集到的数据进行基本的预处理,如基本的统计、特征归一化和异常值处理等;再将数据划分为训练集、验证集(开发集)和测试集。
数据划分时要考虑到两个因素:更多的训练数据会降低参数估计的方差,从而得到更可信的模型;而更多的测试数据会降低测试误差的方差,从而得到更可信的测试误差。如果给定的数据集没有做任何划分,我们一般可以大致按照7:3或者8:2的比例划分训练集和测试集,再根据7:3或者8: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
θ的函数,也称为模型,
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。