线性回归模型
求解模型权重和偏置
。一维情况下,
和
是直线的斜率和截距。
线性回归模型使用均方误差作为损失函数(Loss),用以衡量预测值与真实值的差异。公式:
使用均方误差的原因
均方误差是指将模型在每个训练样本上的预测误差加和,以衡量整体样本的准确性。因为损失函数不仅要考虑“合理性”,也要考虑“易解性”。
神经网络标准结构
每个神经元由加权和非线性变换构成,再将多个神经元分层摆放并连接形成网络。线性回归模型可认为是神经网络模型的一种极简特例:仅有加权和、没有非线性变换的神经元,即无需形成网络。
常规构建模型步骤(处理数据集)
- 数据处理:从本地或URL读取数据,并完成预处理操作(如数据校验、格式转化等),保证模型可读取
- 模型设计:网络结构设计,相当于模型的假设空间,即模型能够表达的关系集合
- 训练配置:设定模型采用的寻解算法,即优化器,并指定计算资源
- 训练过程:循环调用训练过程,每轮都包括前向计算、损失函数(优化目标)和后向传播三个步骤
- 模型保存:将训练好的模型保存,预测时调用
以下为飞桨课程中实操步骤简记,用以记录某些不常用的知识点,比如 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维array,其中array的第0-13项是第一条数据,第14-27项是第二条数据,以此类推....# 这里对原始数据做reshape,变成N x 14的形式feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS','RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]feature_num = len(feature_names)data = data.reshape([data.shape[0] // feature_num, feature_num])# 查看数据x = data[0]print(x.shape)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%)。
ratio = 0.8offset = int(data.shape[0] * ratio)training_date = data[:offset]training_data.shape
(404, 14)
2.1.4 数据归一化处理
对每个特征进行归一化处理,使每个特征的取值缩放道0~1之间。
好处:①模型训练更高效;②特征前的权重大小可以代表该变量对预测结果的贡献度(因为每个特征值本身的范围相同)。
# 计算train数据集的最大值、最小值、平均值maximums,minimums,avgs = \training_data.max(axis=0), \training_data.min(axis=0), \training_data.sum(axis=0) / training_data.shape[0]# 对数据进行归一化处理for i in range(feature_num):#print(maximums[i],minimums[i],avgs[i])data[:, i]=(data[:, i] - minimums[i])/(maximums[i]-minimums[i])
2.1.5 封装成 load data 函数
def load_data():# 从文件导入数据datafile = './work/housing.data'data = np.fromfile(datafile, sep=' ')# 每条数据包括14项,其中前13项时影响因素,第14项时相应的房屋价格中位数feature_names = ['CRIM','ZN','INDUS','CHAS','NOX','RM','AGE',\'DIS','RAD','TAX','PTRATIO','B','LSTAT','MEDV']feature_num = len(feature_names)# 将原始数据进行Reshape(重塑?)变成[N,14]这样的形状data = data.reshape([data.shape[0] // feature_num, feature_num])# np.arange(n).reshape(a, b):依次生成n个自然数,并且以a行b列的数组显示# 拆分数据集为训练集与测试集ratio = 0.8offset = int(data.shape[0] * ratio)training_data = data[:offset]# 计算训练集的最大值、最小值、平均值maximums,minimums,avgs = training_data.max(axis=0),training_data.min(axis=0),\training_data.sum(axis=0) / training_data.shape[0]# 对数据进行归一化处理for i in range(feature_num):data[:, i]=(data[:, i] - avgs[i]) / (maximums[i] - minimums[i])# 训练集和测试集划分training_data = data[:offset]test_data = data[offset:]return training_data, test_data
# 获取数据training_data, test_date = load_data()x = training_data[:, :-1]y = training_data[:, -1:]
# 查看数据print(x[0])print(y[0])
又到了喜闻乐见的 这些数据打印出来代表什么呢看不懂啊 环节了
训练集为 x ,测试集为 y
模型设计(求得初始方程)
深度学习模型的关键要素之一,亦称网络结构设计,相当于模型的假设空间,即实现模型的“前向计算”(从输入到输出)的过程
初始化参数 w
令输入特征和输出预测值以向量表示,则输入特征有 13 个分量,
有 1 个分量,则参数权重的形状(shape)时 13 ×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]w = np.array(w).reshape([13,1])
计算dot点积
取出第 1 条样本数据,观察样本的特征向量与参数向量相乘的结果
x1 = x[0]t = np.dot(x1, w)# dot方法计算矩阵的点积# 参考https://blog.csdn.net/wyf_zyrs/article/details/84847599print(t) # 得到一个只有一个浮点数的数组
[0.03395597]
初始化偏移量 b
完整的线性回归公式还需要初始化偏移量,同样随意赋初值 -0.2 。则线性回归模型的完整输出是
,这个【从特征和参数来计算输出值】的过程称为“前向计算”。
b = -0.2z = t + bprint(z) # 得到一个只有一个浮点数的数组,即 0.03395597-0.2
[-0.16604403]
将上述计算预测输出的过程以“类和对象”的方式来描述,类成员变量又参数和
。通过写一个
forward函数(代表“前向计算”)完成上述从特征和参数到输出预测值的计算过程,代码如下:
class Network(object):def __init__(self, num_of_weights):# 随机产生参数 w 的初始值# 为了保持程序每次运行结果的一致性# 此处设置固定的随机数种子np.random.seed(0) # 还真是个种子方法self.w = np.random.randn(num_of_weights, 1)self.b = 0.def forward(self, x):z = np.array(x, self.w) + self.breturn z
基于 Network 类的定义,模型计算过程如下:
net = Network(13)x1 = x[0]y1 = y[0]z = net.forward(x1)print(z)
[-0.63182506]
由此可见,线性回归也可以表示成一种简单的神经网络:只有一个神经元,且激活函数为恒等式。这也是机器学习模型普遍为深度学习模型替代的原因:很多传统机器学习模型的学习能力几乎等同于相对简单的深度学习模型,因为深度学习网络具有更强大的表示能力。
训练配置
这一步用来寻找模型的最优值,通过损失函数来衡量模型的好坏。
通过模型计算,可知影响因素对应的房价应该是
,但实际时
。此时便需要衡量预测值
与真实值
之间的差距。回归问题常用均方误差
Loss = (y1 - z)*(y1 - z)print(Losss)
[0.39428312]
在分类问题中,常采用交叉熵作为损失函数。
计算损失函数时需要考虑到所有样本的损失函数值,所以需要求和再除以样本总数,公式:
在 Network 类下面添加损失函数的计算过程如下:
class Network(object):def __init__(self, num_of_weights):# 随机产生参数 w 的初始值# 为了保持程序每次运行结果的一致性# 此处设置固定的随机数种子np.random.seed(0) # 还真是个种子方法self.w = np.random.randn(num_of_weights, 1)self.b = 0.def forward(self, x):z = np.array(x, self.w) + self.breturn zdef loss(self,z,y):error = z - ycost = error * errorcost = np.mean(cost)return cost
使用定义的Network类,可以方便地计算预测之和损失函数。类中变量等均是向量。以变量
为例,共两个维度,一个代表特征数量(值为13),一个代表样本数量。
net = Network(13)# 此处可以一次性计算多个样本的预测值和损失函数x1 = x[0:3]y1 = y[0:3]z = net.forward(x1)print('predict:',z)loss = net.loss(z, y1)print('loss:',loss)
predict: [[-0.613182506],[-0.55793096],[-1.00062009]] loss: 0.7229825055441158
训练过程
此处有微积分知识(曲线斜率等于导数值,导数符号为),简单讲就是在内曲线求斜率为0时的坐标值。令损失函数取极小值的
和
应是以下方程组的解:
其中表示损失函数的值,
为模型权重,
为偏置项。
和
均为要学习的模型参数。
把损失函数表示成矩阵的形式为:
两个 || 代表围起来的是个行列式
其中为
个样本的标签值构成的向量,形状为
;
为
个样本特征向量构成的矩阵,形状为
,
为数据特征长度;
为权重向量,形状为
;
为所有元素都为
的向量,形状为
。
计算公式 7 对参数的偏导数:
上述公式忽略了稀疏,这不影响最后结构,因为其中 1 为
维的全 1 向量。
令公式 8 等于0,得到
【没看懂】
其中为所有标签的平均值,
为所有特征向量的平均值。将
代入公式7中并对参数
求偏导得到:
令公式10等于0,得到最优参数:
将样本数据代入公式 11 和公式 12 即可求出
和
的值,但此法仅对线性回归有效。更普适的解法是梯度下降法。
梯度下降法的推导
从当前参数取值,逐步找到一组值,尝试使
导数为 0,即使损失函数
取得极小值。
以下假设损失函数仅随参数变化,其余参数固定,则可绘制出三维曲面图形。相关代码如下:
net = Network(13)losses = []# 只画出参数 w5 和 w9 在区间[-160, 160]的曲线部分,以及包含损失函数的极值w5 = np.arange(-160.0, 160.0, 1.0)w9 = np.arange(-160.0, 160.0, 1.0)losses = np.zeros([len(w5), len(w9)])# 计算设定区域内每个参数取值所对应的Lossfor i in range(len(w5)):for j in range(len(w9)):net.w[5] = w5[i]net.w[9] = w9[j]z = net.forward(x)loss = net.loss(z,y)losses[i,j] = loss#使用matplotlib将两个变量和对应的Loss作3D图import matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3Dfig = plt.figure()ax = Axes3D(fig)w5, w9 = np.meshgrid(w5, w9)ax.plot_surface(w5, w9, losses, rstride=1, cstride=1, cmap='rainbow')plt.show()

取和
即可比较直观地发现极值点。对比绝对值误差(只将每个样本的误差累加,不做平方处理)与均方误差的曲线图,会发现以下特点:
- 均方误差图的最低点是可导的,绝对值误差不可导;
- 均方误差图中,越接近最低点,曲线的坡度逐渐放缓,有助于通过当前的梯度来判断最接近最低点的程度(是否逐渐减少步长,以免错过最低点)。绝对值误差不具备此特点。
以上两个特点关乎“易解性”,故均方误差更胜一筹。
找出一组令损失函数值最小的的值的梯度下降法方案如下:
- 随机一组初始值,如
- 选取下一点
,使
- 重复步骤2,直到损失函数几乎不再下降
根据微积分知识:沿梯度的反方向是函数值下降最快的方向。
函数在某一个点的梯度方向是曲线斜率最大的反方向,但梯度方向向上,故下降最快的是梯度的反方向。
梯度计算
引入因子,定义损失函数如下:
其中是网络对第
个样本的预测值:
梯度的定义:
可算出对
和
的偏导数:
计算过程中因子被消掉,因为二次函数的求导会产生因子
,故互相抵消,也是先前将损失函数改写的原因。
假设仅有一个样本,梯度计算公式为:
可计算出对
和
的偏导数:
相应代码展示如下:
x1 = x[0]y1 = y[0]z1 = net.forward(x1)print('x1 {}, shape{}'.format(x1, x1.shape))# 打印 [-0.02146321 0.03767327 -0.28552309 -0.08663366 0.01289726 0.04634817# 0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528 0.0519112# -0.17590923], shape (13,)print('y1 {}, shape{}'.format(y1, y1.shape))# 打印 [-0.00390539], shape (1,)print('z1 {}, shape{}'.format(z1, z1.shape))# 打印 [-12.05947643], shape (1,)
按以上公式,当只有一个样本时可以计算某个如
的梯度。
gradient_w0 = (z1-y1)*x1[0]# 结果为[0.25875126]gradient_w1 = (z1-y1)*x1[1]# 结果为[-0.45417275]gradient_w2 = (z1-y1)*x1[2]# 结果为[3.44214394]
依此类推,此处可以用 for 循环计算 0 到 12 的每个权重的梯度。
使用numpy进行梯度计算
基于numpy的广播机制,即对向量和矩阵计算如同对一个单一变量做计算一样,可以更快速实现梯度计算。直接使用计算,得到 13 维向量,每个向量分别代表该维度的梯度。
gradient_w = (z1-y1)*x1# 此法仅计算只有样本1时的梯度
用numpy可以简化运算,以求得每个样本对梯度的贡献再做平均:
x3samples = x[0:3]y3samples = y[0:3]z3samples = net.forward(x3samples)# x3samples.shape = shape(3,13)# y3samples.shape = shape(3,1)# z3samples.shape = shape(3,1)
以上三者的第一维大小均为 3 ,表示有 3 个样本。计算该 3 个样本对梯度的贡献度:
gradient_w = (z3samples - y3samples)*x3samples# gradient_w.shape = shape(3,13)
可见 gradient_w 的维度是,且其第一行与第一个样本计算的梯度 gradient_w_by_sample1 一致,第二行与第二个样本计算的梯度 gradient_w_by_sample2 一致,以此类推第三个也一样。故使用矩阵操作更加便利。
小结 numpy 的广播功能
- 可扩展参数维度,代替 for 循环来计算一个样本对从 w0 到 w12 的所有参数的梯度
- 可扩展样本维度,代替 for 循环计算样本 0 到样本 403 对参数的梯度
对于有 N 个样本的情形,可直接使用如下方式计算出所有样本对梯度的贡献。
z = net.forward(x)gradient_w = (z-y)*x# gradient_w = shape(404, 13)
根据梯度计算公式,总梯度是每个样本对梯度贡献的平均值
使用 numpy 的均值函数完成此过程:
gradient_w = np.mean(gradient_w, axis=0)# gradient_w.shape = (13,)# net.w.shape = (13, 1)
显而易见,gradient_w 形状与 w 不一致((13, )和(13,1)),因为np.mean函数消除了第 0 维,故需要补齐,以方便计算。
gradient_w = gradient_w[:, np.newaxis]# gradient_w.shape = (13, 1)
综合得出计算梯度的代码如下:
z = net.format(x)gradient_w = (z-y)*xgradient_w = np.mean(gradient_w, axis=0)gradient_w = gradient_w[:,np.newaxis]
以上代码完成了梯度的计算,同样
的梯度也可以计算出来。
gradient_b = (z-y)gradient_b = np.mean(gradient_b)
整合以上代码逻辑为 gradient 方法如下:
class Network(object):def __init__(self, num_of_weights):# 随机产生w的初始值# 为了保持程序每次运行结果的一致性,设定固定的随机数种子np.random.seed(0)self.w = np.random.randn(num_of_Weights, 1)self.b = 0def forward(self, x):z = np.dot(x, self.w) + self.breturn zdef loss(self, z, y):error = z - ynum_samples = error.shape[0]cost = error * errorcost = np.sum(cost) / num_samplesreturn costdef gradient(self, x, y):z = self.forward(x)gradient_w = (z-y)*xgradient_w = np.mean(gradient_w, axis=0)gradient_w = gradient_w[:, np.newaxis]return gradient_w,gradient_b
# 调用上面初始化的gradient函数,计算梯度# 初始化网络net = Network(13)# 设置[w5, w9]=[-100.,-100.]net.w[5] = -100.0net.w[9] = -100.0z = net.forwar(x)loss = net.loss(z,y)gradient_w, gradient_b = net.gradient(x,y)gradient_w5 = gradient_w[5][0]gradient_w9 = gradient_w[9][0]point = [net.w[5][0],net.w[9][0]]graditnt = [gradient_w5,gradient_w9]
梯度更新
研究更新梯度的方法,以确定损失函数更小的点。
首先沿梯度反方向移动一小步。找到下一点 P1,观察损失函数的变化。
# 在[w5, w9]平面上,沿梯度的反方向移动到下一点P1# 定义移动步长 etaeta = 0.1# 更新参数 w5 和 w9net.w[5] = net.w[5] - eta * gradient_w5net.w[9] = net.w[9] - eta * gradient_w9# 重新计算z和lossz = net.forward(X)loss = net.loss(z, y)gradient_w, gradient_b = net.gradient(x, y)gradient_w5 = gradient_w[5][0]gradient_w9 = gradient_w[9][0]point = [net.w[5][0], net.w[9][0]]gradient = [gradient_w5, gradient_w9]
在上述代码中更新参数的net.w[5] = net.w[5] - eta * gradient_w5,有以下两个知识点:
- 相减:因为参数要向梯度的反方向移动
- eta:控制每次参数沿梯度反方向变动的大小,即每次移动的步长,亦称学习率
封装 Train 函数
将上述循环计算过程封装在train和update函数中:
class Network(object):def __init__(self, num_of_weights):# 随机产生w的初始值# 为了保持程序每次运行结果的一致性,设定固定的随机数种子np.random.seed(0)self.w = np.random.randn(num_of_Weights, 1)self.b = 0def forward(self, x):z = np.dot(x, self.w) + self.breturn zdef loss(self, z, y):error = z - ynum_samples = error.shape[0]cost = error * errorcost = np.sum(cost) / num_samplesreturn costdef gradient(self, x, y):z = self.forward(x)gradient_w = (z-y)*xgradient_w = np.mean(gradient_w, axis=0)gradient_w = gradient_w[:, np.newaxis]return gradient_w,gradient_bdef update(self, gradient_w5, gradient_w9, eta=0.01):net.w[5] = net.w[5] - eta*gradient_w5net.w[9] = net.w[9] - eta*gradient_w9def train(self, x, y, iterations=100, eta=0.01):points = []losses = []for i in range(iterations):points.append([net.w[5][0], net.w[9][0]])z = self.forward(x)L = self.loss(z, y)gradient_w, gradient_b = self.gradient(x, y)gradient_w5 = gradient_w[5][0]gradient_w9 = gradient_w[9][0]self.update(gradient_w5, gradient_w9, eta)losses.append(L)if i%50 == 0:print('iter {}, point {}, loss'.format(i, [net.w[5][0],net.w[9][0]], L))return points, losses# 获取数据train_data, test_data = load_data()x = train_data[:, :-1]y = train_data[:, -1:]# 创建网络net = Network(13)num_iterations = 2000# 启动训练points, losses = net.train(x, y, iterations=num_iterations, eta=0.01)# 画出损失函数的变化趋势plot_x = np.arange(num_iterations)plot_y = np.array(losses)plt.plot(plot_x, plot_y)plt.show()

训练过程扩展到全部参数
修改update和train函数。实现逻辑:①前向计算输出,②根据输出和真实值计算 Loss,③基于 Loss 和输入来计算梯度,④根据梯度更新参数值。以此四个步骤反复执行,直至损失函数最小。代码如下:
class Network(object):def __init__(self,num_of_weights):# 随机产生w的初始值# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子np.random.seed(0)self.w = np.random.randn(num_of_weights,1)self.b = 0.returndef forward(self,x):z = np.dot(x, self.w) + self.breturn zdef loss(self, z, y):error = z - ynum_samples = error.shape[0]cost = error * errorcost = np.sum(cost) / num_smaplesreturn costdef gradient(self, x, y):z = self.forward(x)gradient_w = (z-y)*xgradient_w = np.mean(gradient_w, axis = 0)gradient_w = gradient_w[:, np.newaxis]gradient_b = (z-y)gradient_b = np.mean(gradient_b)return gradient_w, gradient_bdef update(self, x, y, iterations=100, eta=0.01):losses = []for i in range(iterations):z = self.forward(x)L = self.loss(z, y)gradient_w, gradient_b = self.gradient(x,y)self.update(gradient_w, gradient_b, eta)losses.append(L)if (i+1)%10==0:print('iter {},loss {}'.format(i,L))return losses# 获取数据train_data, test_data = load_data()x = train_data[:, :-1]y = train_data[:, -1:]# 创建网络net = Network(13)num_iterations = 1000# 启动训练losses = net.train(x, y, iterations = num_iterations, eta=0.01)# 画出损失函数的变化趋势plot_x = np.arange(num_iterations)plot_y = np.array(losses)plt.plot(plot_x,plot_y)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]
后续同理递推
依此方法不断取出新的mini-batch,并更新网络参数。<br />而后将 train_data 分成大小为 batch_size 的多个 mini_batch。```pythonbatch_size = 10n = len(train_data)mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]# total number of mini_batches is 41# first mini_batch shape (10, 14)# last mini_batch shape (4, 14)
以上操作会按顺序读取 mini_batch,而SGD需要随机抽样,故适用np.random.shuffle函数打乱顺序。
说明: 大量实验发现,模型受训练后期的影响更大,类似于人脑总是对近期发生的事情记忆的更加清晰。为了避免数据样本集合的顺序干扰模型的训练效果,需要进行样本乱序操作。但如果训练样本的顺序就是样本产生的顺序,则期望模型更重视近期产生的样本(预测样本会和近期的训练样本分布更接近),即不需要乱序这个步骤。
集成乱序操作后的代码如下:
# 获取数据train_data, test_data = load_data()# 打乱样本顺序np.random.shuffle(train_data)# 将train_data分成多个mini_batchbatch_size = 10n = len(train_data)mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]# 创建网络net = Network(13)# 依次使用每个mini_batch的数据for mini_batch in mini_batches:x = mini_batch[:, :-1]y = mini_batch[:, -1:]loss = net.train(x, y, iterations=1)
训练过程代码修改
将每个随机抽取到的 mini-batch 输入到模型中用于训练。核心是两层循环
- 第一层:样本集合需要被训练遍历的次数(epoch)
for epoch_id in range(num_epochs):
- 第二层:每次遍历时,样本集需被拆分成的批次数(iteration)
for iter_id,mini_batch in emumerate(mini_batches):
循环内的操作是前向计算、计算损失、计算梯度、更新参数,顺序依次,代码如下:
x = mini_batch[:, :-1]y = mini_batch[:, -1:]a = self.forward(x) #前向计算loss = self.loss(a, y) #计算损失gradient_w, gradient_b = self.gradient(x, y) #计算梯度self.update(gradient_w, gradient_b, eta) #更新参数
最终合并代码如下:
import numpy as npclass Network(object):def __init__(self, num_of_weights):# 随机产生w的初始值# 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子#np.random.seed(0)self.w = np.random.randn(num_of_weights, 1)self.b = 0.def forward(self, x):z = np.dot(x, self.w) + self.breturn zdef loss(self, z, y):error = z - ynum_samples = error.shape[0]cost = error * errorcost = np.sum(cost) / num_samplesreturn costdef gradient(self, x, y):z = self.forward(x)N = x.shape[0]gradient_w = 1. / N * np.sum((z-y) * x, axis=0)gradient_w = gradient_w[:, np.newaxis]gradient_b = 1. / N * np.sum(z-y)return gradient_w, gradient_bdef update(self, gradient_w, gradient_b, eta = 0.01):self.w = self.w - eta * gradient_wself.b = self.b - eta * gradient_bdef train(self, training_data, num_epochs, batch_size=10, eta=0.01):n = len(training_data)losses = []for epoch_id in range(num_epochs):# 在每轮迭代开始之前,将训练数据的顺序随机打乱# 然后再按每次取batch_size条数据的方式取出np.random.shuffle(training_data)# 将训练数据进行拆分,每个mini_batch包含batch_size条的数据mini_batches = [training_data[k:k+batch_size] for k in range(0, n, batch_size)]for iter_id, mini_batch in enumerate(mini_batches):#print(self.w.shape)#print(self.b)x = mini_batch[:, :-1]y = mini_batch[:, -1:]a = self.forward(x)loss = self.loss(a, y)gradient_w, gradient_b = self.gradient(x, y)self.update(gradient_w, gradient_b, eta)losses.append(loss)print('Epoch {:3d} / iter {:3d}, loss = {:.4f}'.format(epoch_id, iter_id, loss))return losses# 获取数据train_data, test_data = load_data()# 创建网络net = Network(13)# 启动训练losses = net.train(train_data, num_epochs=50, batch_size=100, eta=0.1)# 画出损失函数的变化趋势plot_x = np.arange(len(losses))plot_y = np.array(losses)plt.plot(plot_x, plot_y)plt.show()

依据上述结果,可见随机梯度下降加快了训练过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降的曲线出现了震荡。
模型保存
使用 numpy 提供的 save 接口,直接将模型权重数组保存为 .npy 格式的文件。
np.save('w.npy', net.w)np.save('b.npy', net.b)
小结
- 构建网络,初始化参数
和
,定义预测函数和损失函数的计算方法;
- 随机选择初始点,建立梯度的计算方法和参数更新方式;
- 从总的数据集中抽取部分数据作为一个 mini_batch,计算梯度并更新参数,不断迭代直到损失函数几乎不再下降。
附录:
numpy.dot()
参考:
numpy.random.shuffle()
# 新建一个arraya = np.array([1,2,3,4,5,6,7,8,9,10,11,12])# before shuffle [ 1 2 3 4 5 6 7 8 9 10 11 12]np.random.shuffle(a)# after shuffle [ 7 2 11 3 8 6 12 1 4 5 10 9]
# 新建一个arraya = np.array([1,2,3,4,5,6,7,8,9,10,11,12])a = a.reshape([6, 2])# before shuffle# [[ 1 2]# [ 3 4]# [ 5 6]# [ 7 8]# [ 9 10]# [11 12]]np.random.shuffle(a)# after shuffle# [[ 1 2]# [ 3 4]# [ 5 6]# [ 9 10]# [11 12]# [ 7 8]]
