线性回归模型

使用python和numpy构建神经网络模型 - 图1
求解模型权重使用python和numpy构建神经网络模型 - 图2和偏置使用python和numpy构建神经网络模型 - 图3。一维情况下,使用python和numpy构建神经网络模型 - 图4使用python和numpy构建神经网络模型 - 图5是直线的斜率和截距。
线性回归模型使用均方误差作为损失函数(Loss),用以衡量预测值与真实值的差异。公式:
使用python和numpy构建神经网络模型 - 图6

使用均方误差的原因

均方误差是指将模型在每个训练样本上的预测误差加和,以衡量整体样本的准确性。因为损失函数不仅要考虑“合理性”,也要考虑“易解性”。

神经网络标准结构

每个神经元由加权和非线性变换构成,再将多个神经元分层摆放并连接形成网络。线性回归模型可认为是神经网络模型的一种极简特例:仅有加权和、没有非线性变换的神经元,即无需形成网络。

常规构建模型步骤(处理数据集)
  1. 数据处理:从本地或URL读取数据,并完成预处理操作(如数据校验、格式转化等),保证模型可读取
  2. 模型设计:网络结构设计,相当于模型的假设空间,即模型能够表达的关系集合
  3. 训练配置:设定模型采用的寻解算法,即优化器,并指定计算资源
  4. 训练过程:循环调用训练过程,每轮都包括前向计算、损失函数(优化目标)和后向传播三个步骤
  5. 模型保存:将训练好的模型保存,预测时调用

以下为飞桨课程中实操步骤简记,用以记录某些不常用的知识点,比如 numpy 的使用

2.1.1 读入数据

data = np.fromfile('/housing.data',sep=' ')
array([6.320e-03,1.800e+01,……])

2.1.2 数据变换

变一维数组为二维矩阵,14列,含13个X(影响房价的特征)和一个Y(该类型房屋的均价)

  1. # 读入之后的数据被转化成1维array,其中array的第0-13项是第一条数据,第14-27项是第二条数据,以此类推....
  2. # 这里对原始数据做reshape,变成N x 14的形式
  3. feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS',
  4. 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
  5. feature_num = len(feature_names)
  6. data = data.reshape([data.shape[0] // feature_num, feature_num])
  7. # 查看数据
  8. x = data[0]
  9. print(x.shape)
  10. print(x)

(14,) [6.320e-03 1.800e+01 2.310e+00 0.000e+00 5.380e-01 6.575e+00 6.520e+01 4.090e+00 1.000e+00 2.960e+02 1.530e+01 3.969e+02 4.980e+00 2.400e+01]

2.1.3 数据集划分

分为训练集(80%)和测试题(20%)。

  1. ratio = 0.8
  2. offset = int(data.shape[0] * ratio)
  3. training_date = data[:offset]
  4. training_data.shape

(404, 14)

2.1.4 数据归一化处理

对每个特征进行归一化处理,使每个特征的取值缩放道0~1之间。
好处:①模型训练更高效;②特征前的权重大小可以代表该变量对预测结果的贡献度(因为每个特征值本身的范围相同)。

  1. # 计算train数据集的最大值、最小值、平均值
  2. maximums,minimums,avgs = \
  3. training_data.max(axis=0), \
  4. training_data.min(axis=0), \
  5. training_data.sum(axis=0) / training_data.shape[0]
  6. # 对数据进行归一化处理
  7. for i in range(feature_num):
  8. #print(maximums[i],minimums[i],avgs[i])
  9. data[:, i]=(data[:, i] - minimums[i])/(maximums[i]-minimums[i])

2.1.5 封装成 load data 函数
  1. def load_data():
  2. # 从文件导入数据
  3. datafile = './work/housing.data'
  4. data = np.fromfile(datafile, sep=' ')
  5. # 每条数据包括14项,其中前13项时影响因素,第14项时相应的房屋价格中位数
  6. feature_names = ['CRIM','ZN','INDUS','CHAS','NOX','RM','AGE',\
  7. 'DIS','RAD','TAX','PTRATIO','B','LSTAT','MEDV']
  8. feature_num = len(feature_names)
  9. # 将原始数据进行Reshape(重塑?)变成[N,14]这样的形状
  10. data = data.reshape([data.shape[0] // feature_num, feature_num])
  11. # np.arange(n).reshape(a, b):依次生成n个自然数,并且以a行b列的数组显示
  12. # 拆分数据集为训练集与测试集
  13. ratio = 0.8
  14. offset = int(data.shape[0] * ratio)
  15. training_data = data[:offset]
  16. # 计算训练集的最大值、最小值、平均值
  17. maximums,minimums,avgs = training_data.max(axis=0),training_data.min(axis=0),\
  18. training_data.sum(axis=0) / training_data.shape[0]
  19. # 对数据进行归一化处理
  20. for i in range(feature_num):
  21. data[:, i]=(data[:, i] - avgs[i]) / (maximums[i] - minimums[i])
  22. # 训练集和测试集划分
  23. training_data = data[:offset]
  24. test_data = data[offset:]
  25. return training_data, test_data
  1. # 获取数据
  2. training_data, test_date = load_data()
  3. x = training_data[:, :-1]
  4. y = training_data[:, -1:]
  1. # 查看数据
  2. print(x[0])
  3. print(y[0])

又到了喜闻乐见的 这些数据打印出来代表什么呢看不懂啊 环节了

训练集为 x ,测试集为 y

模型设计(求得初始方程)

深度学习模型的关键要素之一,亦称网络结构设计,相当于模型的假设空间,即实现模型的“前向计算”(从输入到输出)的过程

初始化参数 w

令输入特征和输出预测值以向量表示,则输入特征使用python和numpy构建神经网络模型 - 图7有 13 个分量,使用python和numpy构建神经网络模型 - 图8有 1 个分量,则参数权重的形状(shape)时 13 ×1。假设以任意数字赋值参数做初始化:
使用python和numpy构建神经网络模型 - 图9

  1. w = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, -0.1, -0.2, -0.3, -0.4, 0.0]
  2. w = np.array(w).reshape([13,1])

计算dot点积

取出第 1 条样本数据,观察样本的特征向量与参数向量相乘的结果

  1. x1 = x[0]
  2. t = np.dot(x1, w)
  3. # dot方法计算矩阵的点积
  4. # 参考https://blog.csdn.net/wyf_zyrs/article/details/84847599
  5. print(t) # 得到一个只有一个浮点数的数组

[0.03395597]

初始化偏移量 b

完整的线性回归公式还需要初始化偏移量使用python和numpy构建神经网络模型 - 图10,同样随意赋初值 -0.2 。则线性回归模型的完整输出是使用python和numpy构建神经网络模型 - 图11,这个【从特征和参数来计算输出值】的过程称为“前向计算”。

  1. b = -0.2
  2. z = t + b
  3. print(z) # 得到一个只有一个浮点数的数组,即 0.03395597-0.2

[-0.16604403]

将上述计算预测输出的过程以“类和对象”的方式来描述,类成员变量又参数使用python和numpy构建神经网络模型 - 图12使用python和numpy构建神经网络模型 - 图13。通过写一个forward函数(代表“前向计算”)完成上述从特征和参数到输出预测值的计算过程,代码如下:

  1. class Network(object):
  2. def __init__(self, num_of_weights):
  3. # 随机产生参数 w 的初始值
  4. # 为了保持程序每次运行结果的一致性
  5. # 此处设置固定的随机数种子
  6. np.random.seed(0) # 还真是个种子方法
  7. self.w = np.random.randn(num_of_weights, 1)
  8. self.b = 0.
  9. def forward(self, x):
  10. z = np.array(x, self.w) + self.b
  11. return z

基于 Network 类的定义,模型计算过程如下:

  1. net = Network(13)
  2. x1 = x[0]
  3. y1 = y[0]
  4. z = net.forward(x1)
  5. print(z)

[-0.63182506]

由此可见,线性回归也可以表示成一种简单的神经网络:只有一个神经元,且激活函数为恒等式。这也是机器学习模型普遍为深度学习模型替代的原因:很多传统机器学习模型的学习能力几乎等同于相对简单的深度学习模型,因为深度学习网络具有更强大的表示能力。

训练配置

这一步用来寻找模型的最优值,通过损失函数来衡量模型的好坏。
通过模型计算,可知影响因素使用python和numpy构建神经网络模型 - 图14对应的房价应该是使用python和numpy构建神经网络模型 - 图15,但实际时使用python和numpy构建神经网络模型 - 图16。此时便需要衡量预测值使用python和numpy构建神经网络模型 - 图17与真实值使用python和numpy构建神经网络模型 - 图18之间的差距。回归问题常用均方误差使用python和numpy构建神经网络模型 - 图19

  1. Loss = (y1 - z)*(y1 - z)
  2. print(Losss)

[0.39428312]

在分类问题中,常采用交叉熵作为损失函数。
计算损失函数时需要考虑到所有样本的损失函数值,所以需要求和再除以样本总数使用python和numpy构建神经网络模型 - 图20,公式:
使用python和numpy构建神经网络模型 - 图21
在 Network 类下面添加损失函数的计算过程如下:

  1. class Network(object):
  2. def __init__(self, num_of_weights):
  3. # 随机产生参数 w 的初始值
  4. # 为了保持程序每次运行结果的一致性
  5. # 此处设置固定的随机数种子
  6. np.random.seed(0) # 还真是个种子方法
  7. self.w = np.random.randn(num_of_weights, 1)
  8. self.b = 0.
  9. def forward(self, x):
  10. z = np.array(x, self.w) + self.b
  11. return z
  12. def loss(self,z,y):
  13. error = z - y
  14. cost = error * error
  15. cost = np.mean(cost)
  16. return cost

使用定义的Network类,可以方便地计算预测之和损失函数。类中变量使用python和numpy构建神经网络模型 - 图22等均是向量。以变量使用python和numpy构建神经网络模型 - 图23为例,共两个维度,一个代表特征数量(值为13),一个代表样本数量。

  1. net = Network(13)
  2. # 此处可以一次性计算多个样本的预测值和损失函数
  3. x1 = x[0:3]
  4. y1 = y[0:3]
  5. z = net.forward(x1)
  6. print('predict:',z)
  7. loss = net.loss(z, y1)
  8. print('loss:',loss)

predict: [[-0.613182506],[-0.55793096],[-1.00062009]] loss: 0.7229825055441158

训练过程

此处有微积分知识(曲线斜率等于导数值,导数符号为使用python和numpy构建神经网络模型 - 图24),简单讲就是在内曲线求斜率为0时的坐标值。令损失函数取极小值的使用python和numpy构建神经网络模型 - 图25使用python和numpy构建神经网络模型 - 图26应是以下方程组的解:
使用python和numpy构建神经网络模型 - 图27
使用python和numpy构建神经网络模型 - 图28
其中使用python和numpy构建神经网络模型 - 图29表示损失函数的值,使用python和numpy构建神经网络模型 - 图30为模型权重,使用python和numpy构建神经网络模型 - 图31为偏置项。使用python和numpy构建神经网络模型 - 图32使用python和numpy构建神经网络模型 - 图33均为要学习的模型参数。
把损失函数表示成矩阵的形式为:
使用python和numpy构建神经网络模型 - 图34
两个 || 代表围起来的是个行列式
其中使用python和numpy构建神经网络模型 - 图35使用python和numpy构建神经网络模型 - 图36个样本的标签值构成的向量,形状为使用python和numpy构建神经网络模型 - 图37使用python和numpy构建神经网络模型 - 图38使用python和numpy构建神经网络模型 - 图39个样本特征向量构成的矩阵,形状为使用python和numpy构建神经网络模型 - 图40使用python和numpy构建神经网络模型 - 图41为数据特征长度;使用python和numpy构建神经网络模型 - 图42为权重向量,形状为使用python和numpy构建神经网络模型 - 图43使用python和numpy构建神经网络模型 - 图44为所有元素都为使用python和numpy构建神经网络模型 - 图45的向量,形状为使用python和numpy构建神经网络模型 - 图46
计算公式 7 对参数使用python和numpy构建神经网络模型 - 图47的偏导数:
使用python和numpy构建神经网络模型 - 图48
上述公式忽略了稀疏使用python和numpy构建神经网络模型 - 图49,这不影响最后结构,因为其中 1 为使用python和numpy构建神经网络模型 - 图50维的全 1 向量。
令公式 8 等于0,得到
使用python和numpy构建神经网络模型 - 图51【没看懂】
其中使用python和numpy构建神经网络模型 - 图52为所有标签的平均值,使用python和numpy构建神经网络模型 - 图53 为所有特征向量的平均值。将使用python和numpy构建神经网络模型 - 图54代入公式7中并对参数使用python和numpy构建神经网络模型 - 图55求偏导得到:
使用python和numpy构建神经网络模型 - 图56
令公式10等于0,得到最优参数:
使用python和numpy构建神经网络模型 - 图57
使用python和numpy构建神经网络模型 - 图58
将样本数据使用python和numpy构建神经网络模型 - 图59代入公式 11 和公式 12 即可求出使用python和numpy构建神经网络模型 - 图60使用python和numpy构建神经网络模型 - 图61的值,但此法仅对线性回归有效。更普适的解法是梯度下降法

梯度下降法的推导

从当前参数取值,逐步找到一组值使用python和numpy构建神经网络模型 - 图62,尝试使使用python和numpy构建神经网络模型 - 图63导数为 0,即使损失函数使用python和numpy构建神经网络模型 - 图64取得极小值。
以下假设损失函数仅随参数使用python和numpy构建神经网络模型 - 图65变化,其余参数固定,则可绘制出三维曲面图形。相关代码如下:

  1. net = Network(13)
  2. losses = []
  3. # 只画出参数 w5 和 w9 在区间[-160, 160]的曲线部分,以及包含损失函数的极值
  4. w5 = np.arange(-160.0, 160.0, 1.0)
  5. w9 = np.arange(-160.0, 160.0, 1.0)
  6. losses = np.zeros([len(w5), len(w9)])
  7. # 计算设定区域内每个参数取值所对应的Loss
  8. for i in range(len(w5)):
  9. for j in range(len(w9)):
  10. net.w[5] = w5[i]
  11. net.w[9] = w9[j]
  12. z = net.forward(x)
  13. loss = net.loss(z,y)
  14. losses[i,j] = loss
  15. #使用matplotlib将两个变量和对应的Loss作3D图
  16. import matplotlib.pyplot as plt
  17. from mpl_toolkits.mplot3d import Axes3D
  18. fig = plt.figure()
  19. ax = Axes3D(fig)
  20. w5, w9 = np.meshgrid(w5, w9)
  21. ax.plot_surface(w5, w9, losses, rstride=1, cstride=1, cmap='rainbow')
  22. plt.show()

1659967672327.png
使用python和numpy构建神经网络模型 - 图67使用python和numpy构建神经网络模型 - 图68即可比较直观地发现极值点。对比绝对值误差(只将每个样本的误差累加,不做平方处理)与均方误差的曲线图,会发现以下特点:

  1. 均方误差图的最低点是可导的,绝对值误差不可导;
  2. 均方误差图中,越接近最低点,曲线的坡度逐渐放缓,有助于通过当前的梯度来判断最接近最低点的程度(是否逐渐减少步长,以免错过最低点)。绝对值误差不具备此特点。

以上两个特点关乎“易解性”,故均方误差更胜一筹。
找出一组令损失函数值最小的使用python和numpy构建神经网络模型 - 图69的值的梯度下降法方案如下:

  1. 随机一组初始值,如使用python和numpy构建神经网络模型 - 图70
  2. 选取下一点使用python和numpy构建神经网络模型 - 图71,使使用python和numpy构建神经网络模型 - 图72
  3. 重复步骤2,直到损失函数几乎不再下降

根据微积分知识:沿梯度的反方向是函数值下降最快的方向。

函数在某一个点的梯度方向是曲线斜率最大的反方向,但梯度方向向上,故下降最快的是梯度的反方向。

1659968896263.png

梯度计算

引入因子使用python和numpy构建神经网络模型 - 图74,定义损失函数如下:
使用python和numpy构建神经网络模型 - 图75
其中使用python和numpy构建神经网络模型 - 图76是网络对第使用python和numpy构建神经网络模型 - 图77个样本的预测值:
使用python和numpy构建神经网络模型 - 图78
梯度的定义:
使用python和numpy构建神经网络模型 - 图79
可算出使用python和numpy构建神经网络模型 - 图80使用python和numpy构建神经网络模型 - 图81使用python和numpy构建神经网络模型 - 图82的偏导数:
使用python和numpy构建神经网络模型 - 图83
使用python和numpy构建神经网络模型 - 图84
计算过程中因子使用python和numpy构建神经网络模型 - 图85被消掉,因为二次函数的求导会产生因子使用python和numpy构建神经网络模型 - 图86,故互相抵消,也是先前将损失函数改写的原因。
假设仅有一个样本,梯度计算公式为:
使用python和numpy构建神经网络模型 - 图87
使用python和numpy构建神经网络模型 - 图88
可计算出使用python和numpy构建神经网络模型 - 图89使用python和numpy构建神经网络模型 - 图90使用python和numpy构建神经网络模型 - 图91的偏导数:
使用python和numpy构建神经网络模型 - 图92
使用python和numpy构建神经网络模型 - 图93
相应代码展示如下:

  1. x1 = x[0]
  2. y1 = y[0]
  3. z1 = net.forward(x1)
  4. print('x1 {}, shape{}'.format(x1, x1.shape))
  5. # 打印 [-0.02146321 0.03767327 -0.28552309 -0.08663366 0.01289726 0.04634817
  6. # 0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528 0.0519112
  7. # -0.17590923], shape (13,)
  8. print('y1 {}, shape{}'.format(y1, y1.shape))
  9. # 打印 [-0.00390539], shape (1,)
  10. print('z1 {}, shape{}'.format(z1, z1.shape))
  11. # 打印 [-12.05947643], shape (1,)

按以上公式,当只有一个样本时可以计算某个使用python和numpy构建神经网络模型 - 图94使用python和numpy构建神经网络模型 - 图95的梯度。

  1. gradient_w0 = (z1-y1)*x1[0]
  2. # 结果为[0.25875126]
  3. gradient_w1 = (z1-y1)*x1[1]
  4. # 结果为[-0.45417275]
  5. gradient_w2 = (z1-y1)*x1[2]
  6. # 结果为[3.44214394]

依此类推,此处可以用 for 循环计算 0 到 12 的每个权重的梯度。

使用numpy进行梯度计算

基于numpy的广播机制,即对向量和矩阵计算如同对一个单一变量做计算一样,可以更快速实现梯度计算。直接使用使用python和numpy构建神经网络模型 - 图96计算,得到 13 维向量,每个向量分别代表该维度的梯度。

  1. gradient_w = (z1-y1)*x1
  2. # 此法仅计算只有样本1时的梯度

用numpy可以简化运算,以求得每个样本对梯度的贡献再做平均:

  1. x3samples = x[0:3]
  2. y3samples = y[0:3]
  3. z3samples = net.forward(x3samples)
  4. # x3samples.shape = shape(3,13)
  5. # y3samples.shape = shape(3,1)
  6. # z3samples.shape = shape(3,1)

以上三者的第一维大小均为 3 ,表示有 3 个样本。计算该 3 个样本对梯度的贡献度:

  1. gradient_w = (z3samples - y3samples)*x3samples
  2. # gradient_w.shape = shape(3,13)

可见 gradient_w 的维度是使用python和numpy构建神经网络模型 - 图97,且其第一行与第一个样本计算的梯度 gradient_w_by_sample1 一致,第二行与第二个样本计算的梯度 gradient_w_by_sample2 一致,以此类推第三个也一样。故使用矩阵操作更加便利。

小结 numpy 的广播功能

  1. 可扩展参数维度,代替 for 循环来计算一个样本对从 w0 到 w12 的所有参数的梯度
  2. 可扩展样本维度,代替 for 循环计算样本 0 到样本 403 对参数的梯度

对于有 N 个样本的情形,可直接使用如下方式计算出所有样本对梯度的贡献。

  1. z = net.forward(x)
  2. gradient_w = (z-y)*x
  3. # gradient_w = shape(404, 13)

根据梯度计算公式,总梯度是每个样本对梯度贡献的平均值
使用python和numpy构建神经网络模型 - 图98
使用 numpy 的均值函数完成此过程:

  1. gradient_w = np.mean(gradient_w, axis=0)
  2. # gradient_w.shape = (13,)
  3. # net.w.shape = (13, 1)

显而易见,gradient_w 形状与 w 不一致((13, )和(13,1)),因为np.mean函数消除了第 0 维,故需要补齐,以方便计算。

  1. gradient_w = gradient_w[:, np.newaxis]
  2. # gradient_w.shape = (13, 1)

综合得出计算梯度的代码如下:

  1. z = net.format(x)
  2. gradient_w = (z-y)*x
  3. gradient_w = np.mean(gradient_w, axis=0)
  4. gradient_w = gradient_w[:,np.newaxis]

以上代码完成了梯度使用python和numpy构建神经网络模型 - 图99的计算,同样使用python和numpy构建神经网络模型 - 图100的梯度也可以计算出来。

  1. gradient_b = (z-y)
  2. gradient_b = np.mean(gradient_b)

整合以上代码逻辑为 gradient 方法如下:

  1. class Network(object):
  2. def __init__(self, num_of_weights):
  3. # 随机产生w的初始值
  4. # 为了保持程序每次运行结果的一致性,设定固定的随机数种子
  5. np.random.seed(0)
  6. self.w = np.random.randn(num_of_Weights, 1)
  7. self.b = 0
  8. def forward(self, x):
  9. z = np.dot(x, self.w) + self.b
  10. return z
  11. def loss(self, z, y):
  12. error = z - y
  13. num_samples = error.shape[0]
  14. cost = error * error
  15. cost = np.sum(cost) / num_samples
  16. return cost
  17. def gradient(self, x, y):
  18. z = self.forward(x)
  19. gradient_w = (z-y)*x
  20. gradient_w = np.mean(gradient_w, axis=0)
  21. gradient_w = gradient_w[:, np.newaxis]
  22. return gradient_w,gradient_b
  1. # 调用上面初始化的gradient函数,计算梯度
  2. # 初始化网络
  3. net = Network(13)
  4. # 设置[w5, w9]=[-100.,-100.]
  5. net.w[5] = -100.0
  6. net.w[9] = -100.0
  7. z = net.forwar(x)
  8. loss = net.loss(z,y)
  9. gradient_w, gradient_b = net.gradient(x,y)
  10. gradient_w5 = gradient_w[5][0]
  11. gradient_w9 = gradient_w[9][0]
  12. point = [net.w[5][0],net.w[9][0]]
  13. graditnt = [gradient_w5,gradient_w9]

梯度更新

研究更新梯度的方法,以确定损失函数更小的点。
首先沿梯度反方向移动一小步。找到下一点 P1,观察损失函数的变化。

  1. # 在[w5, w9]平面上,沿梯度的反方向移动到下一点P1
  2. # 定义移动步长 eta
  3. eta = 0.1
  4. # 更新参数 w5 和 w9
  5. net.w[5] = net.w[5] - eta * gradient_w5
  6. net.w[9] = net.w[9] - eta * gradient_w9
  7. # 重新计算z和loss
  8. z = net.forward(X)
  9. loss = net.loss(z, y)
  10. gradient_w, gradient_b = net.gradient(x, y)
  11. gradient_w5 = gradient_w[5][0]
  12. gradient_w9 = gradient_w[9][0]
  13. point = [net.w[5][0], net.w[9][0]]
  14. gradient = [gradient_w5, gradient_w9]

在上述代码中更新参数的net.w[5] = net.w[5] - eta * gradient_w5,有以下两个知识点:

  1. 相减:因为参数要向梯度的反方向移动
  2. eta:控制每次参数沿梯度反方向变动的大小,即每次移动的步长,亦称学习率

之前的归一化即是为了此处设定的步长更合适,使训练更高效。

封装 Train 函数

将上述循环计算过程封装在trainupdate函数中:

  1. class Network(object):
  2. def __init__(self, num_of_weights):
  3. # 随机产生w的初始值
  4. # 为了保持程序每次运行结果的一致性,设定固定的随机数种子
  5. np.random.seed(0)
  6. self.w = np.random.randn(num_of_Weights, 1)
  7. self.b = 0
  8. def forward(self, x):
  9. z = np.dot(x, self.w) + self.b
  10. return z
  11. def loss(self, z, y):
  12. error = z - y
  13. num_samples = error.shape[0]
  14. cost = error * error
  15. cost = np.sum(cost) / num_samples
  16. return cost
  17. def gradient(self, x, y):
  18. z = self.forward(x)
  19. gradient_w = (z-y)*x
  20. gradient_w = np.mean(gradient_w, axis=0)
  21. gradient_w = gradient_w[:, np.newaxis]
  22. return gradient_w,gradient_b
  23. def update(self, gradient_w5, gradient_w9, eta=0.01):
  24. net.w[5] = net.w[5] - eta*gradient_w5
  25. net.w[9] = net.w[9] - eta*gradient_w9
  26. def train(self, x, y, iterations=100, eta=0.01):
  27. points = []
  28. losses = []
  29. for i in range(iterations):
  30. points.append([net.w[5][0], net.w[9][0]])
  31. z = self.forward(x)
  32. L = self.loss(z, y)
  33. gradient_w, gradient_b = self.gradient(x, y)
  34. gradient_w5 = gradient_w[5][0]
  35. gradient_w9 = gradient_w[9][0]
  36. self.update(gradient_w5, gradient_w9, eta)
  37. losses.append(L)
  38. if i%50 == 0:
  39. print('iter {}, point {}, loss'.format(i, [net.w[5][0],net.w[9][0]], L))
  40. return points, losses
  41. # 获取数据
  42. train_data, test_data = load_data()
  43. x = train_data[:, :-1]
  44. y = train_data[:, -1:]
  45. # 创建网络
  46. net = Network(13)
  47. num_iterations = 2000
  48. # 启动训练
  49. points, losses = net.train(x, y, iterations=num_iterations, eta=0.01)
  50. # 画出损失函数的变化趋势
  51. plot_x = np.arange(num_iterations)
  52. plot_y = np.array(losses)
  53. plt.plot(plot_x, plot_y)
  54. plt.show()

1660402774976.png

训练过程扩展到全部参数

修改updatetrain函数。实现逻辑:①前向计算输出,②根据输出和真实值计算 Loss,③基于 Loss 和输入来计算梯度,④根据梯度更新参数值。以此四个步骤反复执行,直至损失函数最小。代码如下:

  1. class Network(object):
  2. def __init__(self,num_of_weights):
  3. # 随机产生w的初始值
  4. # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
  5. np.random.seed(0)
  6. self.w = np.random.randn(num_of_weights,1)
  7. self.b = 0.
  8. return
  9. def forward(self,x):
  10. z = np.dot(x, self.w) + self.b
  11. return z
  12. def loss(self, z, y):
  13. error = z - y
  14. num_samples = error.shape[0]
  15. cost = error * error
  16. cost = np.sum(cost) / num_smaples
  17. return cost
  18. def gradient(self, x, y):
  19. z = self.forward(x)
  20. gradient_w = (z-y)*x
  21. gradient_w = np.mean(gradient_w, axis = 0)
  22. gradient_w = gradient_w[:, np.newaxis]
  23. gradient_b = (z-y)
  24. gradient_b = np.mean(gradient_b)
  25. return gradient_w, gradient_b
  26. def update(self, x, y, iterations=100, eta=0.01):
  27. losses = []
  28. for i in range(iterations):
  29. z = self.forward(x)
  30. L = self.loss(z, y)
  31. gradient_w, gradient_b = self.gradient(x,y)
  32. self.update(gradient_w, gradient_b, eta)
  33. losses.append(L)
  34. if (i+1)%10==0:
  35. print('iter {},loss {}'.format(i,L))
  36. return losses
  37. # 获取数据
  38. train_data, test_data = load_data()
  39. x = train_data[:, :-1]
  40. y = train_data[:, -1:]
  41. # 创建网络
  42. net = Network(13)
  43. num_iterations = 1000
  44. # 启动训练
  45. losses = net.train(x, y, iterations = num_iterations, eta=0.01)
  46. # 画出损失函数的变化趋势
  47. plot_x = np.arange(num_iterations)
  48. plot_y = np.array(losses)
  49. plt.plot(plot_x,plot_y)
  50. plt.show()

随机梯度下降法

在上述过程中,每次损失函数和梯度计算均基于数据集的全量数据,在实际应用中,需应用与 ”抽样“ 类似的原理,每次从总的数据集中随机抽取小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法即为随机梯度下降法(Stochastic Gradient Descent,SGD)。
核心概念如下:

  • mini-batch:每次迭代时抽取出的数据集;
  • batch_size:每个mini-batch所含样本数目
  • epoch:每次迭代完成的一轮训练,启动训练时,可将轮数 num_epoch 和 batch_size 作为参数传入
    数据处理代码修改
    ```python

    获取数据

    train_data, test_data = load_data() train_data.shape

    取 0-9 号样本计算梯度并更新网络参数

    train_data1 = train_data[0:10] train_data1.shape net = Network(13) x = train_data1[:, :-1] y = train_data1[:, -1:] loss = net.train(x, y, iterations=1, eta=0.01)

    loss = [0.9001866101467375]

再取 10-19 号样本进行同样操作

train_data2 = train_data[10:20] x = train_data2[:, :-1] y = train_data2[:, -1:] loss = net.train(x, y, iterations=1, eta=0.01)

loss = [0.1171681170130832]

后续同理递推

  1. 依此方法不断取出新的mini-batch,并更新网络参数。<br />而后将 train_data 分成大小为 batch_size 的多个 mini_batch
  2. ```python
  3. batch_size = 10
  4. n = len(train_data)
  5. mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]
  6. # total number of mini_batches is 41
  7. # first mini_batch shape (10, 14)
  8. # last mini_batch shape (4, 14)

以上操作会按顺序读取 mini_batch,而SGD需要随机抽样,故适用np.random.shuffle函数打乱顺序。

说明: 大量实验发现,模型受训练后期的影响更大,类似于人脑总是对近期发生的事情记忆的更加清晰。为了避免数据样本集合的顺序干扰模型的训练效果,需要进行样本乱序操作。但如果训练样本的顺序就是样本产生的顺序,则期望模型更重视近期产生的样本(预测样本会和近期的训练样本分布更接近),即不需要乱序这个步骤。

集成乱序操作后的代码如下:

  1. # 获取数据
  2. train_data, test_data = load_data()
  3. # 打乱样本顺序
  4. np.random.shuffle(train_data)
  5. # 将train_data分成多个mini_batch
  6. batch_size = 10
  7. n = len(train_data)
  8. mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]
  9. # 创建网络
  10. net = Network(13)
  11. # 依次使用每个mini_batch的数据
  12. for mini_batch in mini_batches:
  13. x = mini_batch[:, :-1]
  14. y = mini_batch[:, -1:]
  15. loss = net.train(x, y, iterations=1)

训练过程代码修改

将每个随机抽取到的 mini-batch 输入到模型中用于训练。核心是两层循环

  1. 第一层:样本集合需要被训练遍历的次数(epoch)

for epoch_id in range(num_epochs):

  1. 第二层:每次遍历时,样本集需被拆分成的批次数(iteration)

for iter_id,mini_batch in emumerate(mini_batches):
循环内的操作是前向计算、计算损失、计算梯度、更新参数,顺序依次,代码如下:

  1. x = mini_batch[:, :-1]
  2. y = mini_batch[:, -1:]
  3. a = self.forward(x) #前向计算
  4. loss = self.loss(a, y) #计算损失
  5. gradient_w, gradient_b = self.gradient(x, y) #计算梯度
  6. self.update(gradient_w, gradient_b, eta) #更新参数

最终合并代码如下:

  1. import numpy as np
  2. class Network(object):
  3. def __init__(self, num_of_weights):
  4. # 随机产生w的初始值
  5. # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
  6. #np.random.seed(0)
  7. self.w = np.random.randn(num_of_weights, 1)
  8. self.b = 0.
  9. def forward(self, x):
  10. z = np.dot(x, self.w) + self.b
  11. return z
  12. def loss(self, z, y):
  13. error = z - y
  14. num_samples = error.shape[0]
  15. cost = error * error
  16. cost = np.sum(cost) / num_samples
  17. return cost
  18. def gradient(self, x, y):
  19. z = self.forward(x)
  20. N = x.shape[0]
  21. gradient_w = 1. / N * np.sum((z-y) * x, axis=0)
  22. gradient_w = gradient_w[:, np.newaxis]
  23. gradient_b = 1. / N * np.sum(z-y)
  24. return gradient_w, gradient_b
  25. def update(self, gradient_w, gradient_b, eta = 0.01):
  26. self.w = self.w - eta * gradient_w
  27. self.b = self.b - eta * gradient_b
  28. def train(self, training_data, num_epochs, batch_size=10, eta=0.01):
  29. n = len(training_data)
  30. losses = []
  31. for epoch_id in range(num_epochs):
  32. # 在每轮迭代开始之前,将训练数据的顺序随机打乱
  33. # 然后再按每次取batch_size条数据的方式取出
  34. np.random.shuffle(training_data)
  35. # 将训练数据进行拆分,每个mini_batch包含batch_size条的数据
  36. mini_batches = [training_data[k:k+batch_size] for k in range(0, n, batch_size)]
  37. for iter_id, mini_batch in enumerate(mini_batches):
  38. #print(self.w.shape)
  39. #print(self.b)
  40. x = mini_batch[:, :-1]
  41. y = mini_batch[:, -1:]
  42. a = self.forward(x)
  43. loss = self.loss(a, y)
  44. gradient_w, gradient_b = self.gradient(x, y)
  45. self.update(gradient_w, gradient_b, eta)
  46. losses.append(loss)
  47. print('Epoch {:3d} / iter {:3d}, loss = {:.4f}'.
  48. format(epoch_id, iter_id, loss))
  49. return losses
  50. # 获取数据
  51. train_data, test_data = load_data()
  52. # 创建网络
  53. net = Network(13)
  54. # 启动训练
  55. losses = net.train(train_data, num_epochs=50, batch_size=100, eta=0.1)
  56. # 画出损失函数的变化趋势
  57. plot_x = np.arange(len(losses))
  58. plot_y = np.array(losses)
  59. plt.plot(plot_x, plot_y)
  60. plt.show()

image.png
依据上述结果,可见随机梯度下降加快了训练过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降的曲线出现了震荡。

模型保存

使用 numpy 提供的 save 接口,直接将模型权重数组保存为 .npy 格式的文件。

  1. np.save('w.npy', net.w)
  2. np.save('b.npy', net.b)

小结
  1. 构建网络,初始化参数使用python和numpy构建神经网络模型 - 图103使用python和numpy构建神经网络模型 - 图104,定义预测函数和损失函数的计算方法;
  2. 随机选择初始点,建立梯度的计算方法和参数更新方式;
  3. 从总的数据集中抽取部分数据作为一个 mini_batch,计算梯度并更新参数,不断迭代直到损失函数几乎不再下降。

附录:

numpy.dot()

参考:

numpy.random.shuffle()
  1. # 新建一个array
  2. a = np.array([1,2,3,4,5,6,7,8,9,10,11,12])
  3. # before shuffle [ 1 2 3 4 5 6 7 8 9 10 11 12]
  4. np.random.shuffle(a)
  5. # after shuffle [ 7 2 11 3 8 6 12 1 4 5 10 9]
  1. # 新建一个array
  2. a = np.array([1,2,3,4,5,6,7,8,9,10,11,12])
  3. a = a.reshape([6, 2])
  4. # before shuffle
  5. # [[ 1 2]
  6. # [ 3 4]
  7. # [ 5 6]
  8. # [ 7 8]
  9. # [ 9 10]
  10. # [11 12]]
  11. np.random.shuffle(a)
  12. # after shuffle
  13. # [[ 1 2]
  14. # [ 3 4]
  15. # [ 5 6]
  16. # [ 9 10]
  17. # [11 12]
  18. # [ 7 8]]