前文案例探讨了一个分类问题,并用算法获得一个分类模型并将其训练过程实现了可视化 水果分类(香蕉、苹果大战), 本文通过油漆混合的虚拟数据集讨论一个多元线性回归问题的可视化。
文章目录
本文的运行环境为 jupyter notebook
python版本为3.7
本文所用到的库包括
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['xtick.labelsize'] = 15
plt.rcParams['ytick.labelsize'] = 15
plt.rcParams['legend.fontsize'] = 15
plt.rcParams['axes.labelsize'] = 15
plt.rcParams['axes.titlesize'] = 15
考虑这样一个场景:汽车工厂为调制车身颜色,需要用到两种不同颜色的油漆,油漆A和油漆B不同的掺和比例会调出不同的光泽,于是我们用称重设备记录每一次油漆的加入量(变量 f e a 1 fea_1 fea1、 f e a 2 fea_2 fea2代表油漆A、B的加入量),均匀混合后,通过仪器测量调制好油漆的光泽( y y y)。我们希望拟合出以下公式:
y = w 1 ∗ f e a 1 + w 2 ∗ f e a 2 + b y=w_1*fea_1+w_2*fea_2+b y=w1∗fea1+w2∗fea2+b
该公式用以指导车身光泽的设计,这是一个典型的多元线性回归求解问题,我们希望通过算法求得权值( w 1 w_1 w1和 w 2 w_2 w2)和偏置( b b b)。
多元线性回归-梯度下降
梯度下降是用来解决此类问题最常用的机器学习算法,其数学过程为:
为数学推导方便,偏置项被加入至特征矩阵 x x x中,即 x x x在原特征矩阵基础上,增加一列,该列值全部为1
梯度下降一共分为四步:
- 根据当前权值 w w w计算出预测值;
- 预测值与真实值的平方,作为本次训练的损失值;
- 损失值对权值求导;
- 权值更新。
每一步涉及的数学公式如下:
x ∗ w = y ^ x*w=\hat{y} x∗w=y^
J = ( y ^ − y ) 2 = ( x ∗ w − y ) 2 J=(\hat{y}-y)^2=(x*w-y)^2 J=(y^−y)2=(x∗w−y)2
∂ J ∂ w = 2 ∗ x T ∗ ( x ∗ w − y ) = 2 ∗ x T ∗ ( y ^ − y ) \frac{\partial J}{\partial w}=2 * x^T * (x*w-y)=2 * x^T * (\hat{y}-y) ∂w∂J=2∗xT∗(x∗w−y)=2∗xT∗(y^−y)
w ′ = w − l r ∗ ∂ J ∂ w w^{'}=w-lr * \frac{\partial J}{\partial w} w′=w−lr∗∂w∂J
定义函数-计算前向传播
x ∗ w = y ^ x*w=\hat{y} x∗w=y^
def forward(x, w):
# x shape: N*M
# w shape: M*1
return np.matmul(x, w)
定义函数-计算权值(weights)梯度
∂ J ∂ w = 2 ∗ x T ∗ ( x ∗ w − y ) = 2 ∗ x T ∗ ( y ^ − y ) \frac{\partial J}{\partial w}=2 * x^T * (x*w-y)=2 * x^T * (\hat{y}-y) ∂w∂J=2∗xT∗(x∗w−y)=2∗xT∗(y^−y)
def grad_w(x, w, y_true):
y_pred = forward(x, w)
return 2 * np.matmul(x.T, y_pred-y_true)
定义函数-训练权值
w ′ = w − l r ∗ ∂ J ∂ w w^{'}=w-lr * \frac{\partial J}{\partial w} w′=w−lr∗∂w∂J
def train(lr, x, w, y_true):
grad = grad_w(x, w, y_true)
return w-lr*grad
构造数据集
为方便读者复现,本文仍然用人造数据集,本数据集包括100个样本,2个特征,特征1的权值为5,特征2的权值为2,偏置项为1。
y = 5 ∗ x 1 + 2 ∗ x 2 + 1 y=5*x_1+2*x_2+1 y=5∗x1+2∗x2+1
N = 100
feas = 2
x = np.random.random(size=(N, feas+1))
x[:, feas] = 1 # 最后一列赋值为1
w_true = np.array([5, 2, 1]).reshape(-1, 1)
y_true = np.matmul(x, w_true)
# 等价于 y_true = w_true[0]*x[:, 0]+w_true[1]*x[:, 1] + w_true[2] * x[:, 2]
y_true = y_true.reshape(-1, 1)
x.shape, w_true.shape, y_true.shape
输出:
((100, 3), (3, 1), (100, 1))
从输出数组形状可以看出,特征矩阵的形状为 100*3(其中最后一列全为1),权值为长度为3的一维向量( w 1 w_1 w1、 w 2 w_2 w2和 b b b), y y y的真实值为长度为100的一维向量。
开始训练
np.random.seed(121)
w = np.random.random(size=(feas+1, 1)) # 随机生成一个初始权值
print('primary weights: ', w.ravel())
epochs = 300 # 训练次数
lr = 0.002 # 学习率
ws = [w]
for i in range(epochs):
w = train(lr, x, w, y_true)
ws.append(w)
if i % 30 == 0:
print('epoch : %d ; weights: ' % (i+1), w.ravel())
输出:
primary weights: [0.11133083 0.21076757 0.23296249]
epoch : 1 ; weights: [1.20849512 1.20876055 1.9654924 ]
epoch : 31 ; weights: [3.51332004 1.77287304 1.97401403]
epoch : 61 ; weights: [4.30687965 1.84893202 1.47955548]
epoch : 91 ; weights: [4.6740756 1.9123483 1.23486055]
epoch : 121 ; weights: [4.84571974 1.95245021 1.11458731]
epoch : 151 ; weights: [4.92659705 1.9751822 1.05575436]
epoch : 181 ; weights: [4.96494165 1.98735568 1.02707471]
epoch : 211 ; weights: [4.98320698 1.99365938 1.01312882]
epoch : 241 ; weights: [4.99193865 1.99685458 1.00635965]
epoch : 271 ; weights: [4.99612396 1.99845132 1.00307828]
300次迭代,每30次打印一次权值,从以上输出结果可以看出,初始权值被随机赋值为 [0.11133083 0.21076757 0.23296249],训练至第271次时,训练结果已十分接近我们构造源数据时用的权值([5, 2 ,1])
源数据的映射
源数据可通过散点图进行表达,将两个油漆的加入量分别映射到横、纵坐标,将光泽映射为标记的颜色,plt.cm.hot色板( 色板传送门)亮度越接近黄色表示数值越大,从图中可以看出光泽随两个特征线性增加。
plt.title('Features')
plt.xlim(0, 1)
plt.ylim(0, 1)
# 绘制特征数据,并将y真实标签以颜色的形式映射到标记的颜色上
plt.scatter(x[:, 0], x[:, 1], c=y_true.ravel(), cmap=plt.cm.hot)
plt.xlabel('fea_1')
plt.ylabel('fea_2')
那么,如何通过可视化的过程,表达训练过程模型的变化情况和准确度呢?
以下通过等高面图恰当地描述了某一步的训练状态。等高面图采用相同的色板映射在任一位置坐标下,当前训练步权值对光泽的预测结果。由于色板相同,当标记颜色与背景颜色变化趋势一致时,可以认为模型训练结果好。
可视化训练过程中的一步
以下将第10次训练状态做了可视化
epoch = 10
plt.title('Prediction after train epoch: %d' % epoch)
plt.xlim(0, 1)
plt.ylim(0, 1)
# 绘制特征数据,并将y真实标签以颜色的形式映射到标记的颜色上
plt.scatter(x[:, 0], x[:, 1], c=y_true.ravel(), cmap=plt.cm.hot)
# 绘制预测等高面
xx = np.linspace(0, 1, 100)
yy = np.linspace(0, 1, 100)
X, Y = np.meshgrid(xx, yy)
Z = ws[epoch][0]*X.ravel()+ws[epoch][1]*Y.ravel()+ws[epoch][2]
Z = Z.reshape(X.shape)
plt.contourf(X, Y, Z, cmap=plt.cm.hot,alpha=0.4)
可视化N步
以下可视化了其中的9次过程
nrows, ncols = 3, 3
# showepochs=np.linspace(0,100,nrows*ncols) # 线性地选择9个训练步骤
showepochs = [0, 1, 2, 3, 10, 20, 50, 100, 300]
fig, axs = plt.subplots(nrows, ncols, sharex=True,
sharey=True, figsize=(12, 12))
fig.suptitle('Multiple linear regression train process', fontsize=20, va='top')
axs = axs.ravel()
for i, ax in enumerate(axs):
epoch = int(showepochs[i]) # 若训练步骤被输入为小数,需要将其强转为整型
ax.set_title('Pred after train epoch: %d' % epoch)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
# 绘制特征数据,并将y真实标签以颜色的形式映射到标记的颜色上
ax.scatter(x[:, 0], x[:, 1], c=y_true.ravel(),
cmap=plt.cm.hot, ec='tab:blue')
# 绘制预测等高面
xx = np.linspace(0, 1, 100)
yy = np.linspace(0, 1, 100)
X, Y = np.meshgrid(xx, yy)
Z = ws[epoch][0]*X.ravel()+ws[epoch][1]*Y.ravel()+ws[epoch][2]
Z = Z.reshape(X.shape)
ax.contourf(X, Y, Z, cmap=plt.cm.hot, alpha=0.4)
# 第一列左侧添加y轴标题
if ax.is_first_col():
ax.set_ylabel('fea_2')
# 最后一行底部添加x轴标题
if ax.is_last_row():
ax.set_xlabel('fea_1')
plt.subplots_adjust(bottom=0.05, top=0.92, left=0.08, right=0.95)
从图中可以看出,迭代到第300次时,标记颜色与背景颜色趋势已趋近一致,等高线将各标记的色阶有效地分开。
多元线性回归-解析解
除梯度下降以外,常用解析解计算权值,公式如下:
w = ( x T ∗ x ) − 1 ∗ x T ∗ y w=(x^T*x)^{-1}*x^T*y w=(xT∗x)−1∗xT∗y
此处省略数学过程,详细可参考: 多元线性回归求解过程 解析解求解
W = np.matmul(x.T, x)
W = np.matrix(W).I # 求逆矩阵
W = np.matmul(W, x.T)
W = np.matmul(W, y_true)
W.ravel()
输出:
matrix([[5., 2., 1.]])
以上过程可以写成一行代码,只是这种风格理解起来就有点难受了
W = np.matmul(np.matmul(np.matrix(np.matmul(x.T, x)).I, x.T), y_true)
W.ravel()
输出:
matrix([[5., 2., 1.]])
解析解的效率
numpy库对矩阵计算进行了大量优化,如下段代码所示,该数据集包含20000条样本,3000个特征(事实上这个级别的特征工程是比较罕见的),但只用了5.43秒就完成了求解。通过np.sum(w_true-W)计算真实权值与计算权值的差异,求和结果无限接近0说明其求解完全正确。
那为什么还要用梯度下降呢?
原因一:在诸多机器学习算法中,能直接求解解析解的情况是极少的,梯度下降不仅仅适用于多元线性回归,更适用于逻辑回归、神经网络等情境,是更普适的一种权值训练算法;
原因二:多元线性回归中,包含矩阵求逆的步骤,事实上,并不是所有矩阵都有逆矩阵,稍后的案例将会看到,在没有逆矩阵的情况下,是无法用解析解求解权值的。
import time
start_time = time.time()
N = 20000
feas = 3000
np.random.seed(198)
x = np.random.random(size=(N, feas+1))
x[:, feas] = 1
w_true = np.random.randint(low=-10, high=10, size=(feas+1, 1))
y_true = np.matmul(x, w_true)
y_true = y_true.reshape(-1, 1)
W = np.matmul(np.matmul(np.matrix(np.matmul(x.T, x)).I, x.T), y_true)
print('the time cost: ',time.time()-start_time, '\n',
'the weights_true - weights_after_trained: ',np.sum(w_true-W) )
输出:
the time cost: 5.433446884155273
the weights_true - weights_after_trained: 2.0153858582006023e-09
机器学习sklearn
sklearn库是目前机器学习用到的最多的库,只需要一行代码即可求解本问题,amazing !事实上sklearn的接口都是如此简约,寥寥几行代码,该做的就都做完了。
from sklearn.linear_model import LinearRegression
l_reg=LinearRegression().fit(x[:,:-1],y_true)
l_reg.coef_,l_reg.intercept_
输出:
(array([[5., 2.]]), array([1.]))
下面,讨论下一个问题,如何尽可能多地在一张图上表达尽可能多的特征,如本例中,我们通过散点图表达了两种油漆的加入量,和光泽真实值,通过等高面图表达了光泽的预测值,通过子图表达了训练次数。一共可视化了5个维度,那还可以再增加维度吗?
在散点图绘图对象中,我们介绍过散点图是有态度,有深度的图(散点图传送门), 主要就在于它能尽可能多地将多维数据映射到标记上。
增加特征数
以下数据集加入了一个新特征,可以理解为工厂做的工艺改进研究,此研究加入了新的油漆C。
为了让后面训练过程图有更好的可读性,此处将油漆B和油漆C做了一些人工处理,详见代码注释
构建3个特征的数据集
N = 200
feas = 3
np.random.seed(198)
x = np.random.random(size=(N, feas+1))
x[:, feas] = 1 # 最后一列赋值为1
x[:, 1] = np.linspace(1,0,N) # 第二个特征从1递减至0
x[:, 2] = np.linspace(0,1,N) # 第三个特征从0递增至1
w_true = np.array([5, 2, 20, 1]).reshape(-1, 1)
y_true = np.matmul(x, w_true)
y_true = y_true.reshape(-1, 1)
np.random.seed(121)
w = np.random.random(size=(feas+1, 1)) # 随机生成一个初始权值
print('primary weights: ', w.ravel())
epochs = 300 # 训练次数
lr = 0.002 # 学习率
ws = [w]
for i in range(epochs):
w = train(lr, x, w, y_true)
ws.append(w)
if i % 30 == 0:
print('epoch : %d ; weights: ' % (i+1), w.ravel())
输出:
primary weights: [0.11133083 0.21076757 0.23296249 0.15194456]
epoch : 1 ; weights: [ 5.8440494 4.6022534 7.03524946 11.34571736]
epoch : 31 ; weights: [ 4.84617485 -4.77325555 13.01990605 7.954865 ]
epoch : 61 ; weights: [ 4.97457751 -4.8967529 13.09990026 7.91136186]
epoch : 91 ; weights: [ 4.99585269 -4.90195983 13.09789044 7.90414511]
epoch : 121 ; weights: [ 4.99932412 -4.9026139 13.09736686 7.90296745]
epoch : 151 ; weights: [ 4.99988986 -4.90271795 13.09727898 7.90277553]
epoch : 181 ; weights: [ 4.99998205 -4.90273487 13.09726463 7.90274425]
epoch : 211 ; weights: [ 4.99999708 -4.90273763 13.09726229 7.90273916]
epoch : 241 ; weights: [ 4.99999952 -4.90273808 13.09726191 7.90273833]
epoch : 271 ; weights: [ 4.99999992 -4.90273815 13.09726185 7.90273819]
从训练过程可以看出,经过训练,权值已收敛,但与我们设定的权重[5, 2, 20, 1]差异明显,其原因是因为我们对油漆B和油漆C做的人工处理造成了特征间的共线性,当特征存在共线情况时,最优解就不是唯一解,以下过程证明了训练得到的权值与设定的权值在回归预测上并无差异。
证明
油漆B和油漆C的赋值可写成如下形式,t为 np.linspace(0,1,N) 向量。
x 2 = 1 − 1 200 ∗ t x_2=1-\frac{1}{200} * t x2=1−2001∗t
x 3 = 0 + 1 200 ∗ t x_3=0+\frac{1}{200} * t x3=0+2001∗t
化简得到:
x 2 + x 3 = 1 x_2+x_3=1 x2+x3=1
将原公式与模型计算出的公式求差:
( 5 ∗ x 1 + 2 ∗ x 2 + 20 ∗ x 3 + 1 ) − ( 5 ∗ x 1 − 4.9 ∗ x 2 + 13.09 ∗ x 3 + 7.9 ) = 6.9 ∗ x 2 + 6.91 ∗ x 3 − 6.9 ≈ 0 (5*x_1+2*x_2+20*x_3+1)-(5*x_1-4.9*x_2+13.09*x_3+7.9)\\ =6.9*x_2+6.91*x_3-6.9\\ \approx 0 (5∗x1+2∗x2+20∗x3+1)−(5∗x1−4.9∗x2+13.09∗x3+7.9)=6.9∗x2+6.91∗x3−6.9≈0
可视化N步
nrows, ncols = 3, 3
# showepochs=np.linspace(0,100,nrows*ncols) # 线性地选择9个训练步骤
showepochs = [0, 1, 2, 3, 10, 20, 50, 100, 300]
fig, axs = plt.subplots(nrows, ncols, sharex=True,
sharey=True, figsize=(12, 12))
fig.suptitle('Multiple linear regression of 3 features train process', fontsize=20, va='top')
axs = axs.ravel()
for i, ax in enumerate(axs):
epoch = int(showepochs[i]) # 若训练步骤被输入为小数,需要将其强转为整型
ax.set_title('Pred after train epoch: %d' % epoch)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
# 绘制特征数据,并将y真实标签以颜色的形式映射到标记的颜色上
ax.scatter(x[:, 0], x[:, 1], s=x[:, 2]*100, c=y_true.ravel(),ec='tab:blue',
cmap=plt.cm.hot, )
# 绘制预测等高面
xx = np.linspace(0, 1, 100)
yy = np.linspace(0, 1, 100)
X, Y = np.meshgrid(xx, yy)
aa = np.linspace(0, 1, 100)
bb = np.linspace(0, 1, 100)
cc= np.linspace(0, 1, 100)
A, B, C= np.meshgrid(aa, bb,cc)
Z = ws[epoch][0]*A.ravel()+ws[epoch][1]*B.ravel()+ws[epoch][2]*C.ravel()+ws[epoch][3]
Z = Z.reshape(A.shape)
Z = np.mean(Z,axis=2)
ax.contourf(X, Y, Z, cmap=plt.cm.hot, alpha=0.4)
# 第一列左侧添加y轴标题
if ax.is_first_col():
ax.set_ylabel('fea_2')
# 最后一行底部添加x轴标题
if ax.is_last_row():
ax.set_xlabel('fea_1')
plt.subplots_adjust(bottom=0.05, top=0.92, left=0.08, right=0.95)
从上图可以看出,油漆A,油漆B分别被映射到横、纵坐标,油漆C被映射成标记的大小,三种油漆的权值分别为[5,2,20](三种油漆的加入量均为归一化的,因此权值的绝对值大小才有意义),即油漆C对光泽的影响最为显著,从图中也可以看出,圆圈越大的地方颜色越接近黄色。
解析解
对该数据集求解解析解,程序报错LinAlgError: singular matrix,原因是因为人工处理造成了变量的共线性,矩阵不可逆。
W = np.matmul(np.matmul(np.matrix(np.matmul(x.T, x)).I, x.T), y_true)
W.ravel()
输出:
LinAlgError: singular matrix
sklearn
尝试用sklearn计算,得出一个与梯度下降不同的解,再次说明了特征共线时解不唯一:
from sklearn.linear_model import LinearRegression
l_reg=LinearRegression().fit(x[:,:-1],y_true)
l_reg.coef_,l_reg.intercept_
输出:
(array([[ 5., -9., 9.]]), array([12.]))
检查
以下过程证明了该解是该数据集的正确解:
( 5 ∗ x 1 + 2 ∗ x 2 + 20 ∗ x 3 + 1 ) − ( 5 ∗ x 1 − 9 ∗ x 2 + 9 ∗ x 3 + 12 ) = 11 ∗ x 2 + 11 ∗ x 3 − 11 = 0 (5*x_1+2*x_2+20*x_3+1)-(5*x_1-9*x_2+9*x_3+12)\\ =11*x_2+11*x_3-11\\ = 0 (5∗x1+2∗x2+20∗x3+1)−(5∗x1−9∗x2+9∗x3+12)=11∗x2+11∗x3−11=0
至此,我们成功地可视化了6个维度的数据,与此同时,散点图的标记形状可以增加一个维度,横向的子图与纵向的子图可分别表达一个维度(即,子图方面还可以再增加一个维度)。
因此,本图最高可表达8个维度的数据,维度是很重要的,特征工程大部分的工作实际都是在处理维度、压缩维度和转换维度。
维度是很神奇的东西,有些坍缩在很小的局部,有些充斥着整个宇宙!
三体人造出高维的水滴打败了人类,神级文明却无法阻止二向箔!
文末,欣赏一下诺兰《星际穿越》带给我们的三维虫洞和五维空间吧!
希望对你有所启发和帮助!