根据我上一篇关于波士顿房价预测一文可以知道,如果使用梯度下降法,需要将所有的样本对梯度的贡献取平均,根据梯度更新参数。
但是面对海量样本的数据集,如果每次计算都使用全部的样本来计算损失函数和梯度,性能会很差,也就是计算的会慢。
随机梯度下降法(SGD)
为了解决性能差的问题,我们引入了随机梯度下降法(SGD)对其进行优化,改进如下:
反正参数每次只沿着梯度反方向更新一点点,那么方向大差不差即可,所以我们每次只从总数据集中随机抽取一部分数据来代表整体,基于这部分数据来计算梯度和损失函数来更新参数,这便是随机梯度下降法。
对于此次优化,我们主要对上文中的代码进行了两部分改进,这里为了方便我直接把改进前的代码复制过来:
- 数据处理部分
- 训练过程部分
改进前的代码:
import numpy as np
from matplotlib import pyplot as plt
def load_data():
# 从文件导入数据
datafile = 'housing.data'
data = np.fromfile(datafile, sep=' ')
print(data.shape)
# 每条数据包括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])
print(data.shape)
# 将原数据集拆分成训练集和测试集
# 这里使用80%的数据做训练,20%的数据做测试
# 测试集和训练集必须是没有交集的
ratio = 0.8
offset = int(data.shape[0] * ratio)
data_slice = data[:offset]
# 计算train数据集的最大值、最小值和平均值
maxinums, mininums, avgs = data_slice.max(axis=0), data_slice.min(axis=0), data_slice.sum(axis=0) / data_slice.shape[0]
# 对数据进行归一化处理
for i in range(feature_num):
# print(maxinums[i], mininums[i], avgs[i])
data[:, i] = (data[:, i] - avgs[i]) / (maxinums[i] - mininums[i])
# 训练集和测试集的划分比例
# ratio = 0.8
train_data = data[:offset]
test_data = data[offset:]
return train_data, test_data
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.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
cost = error * error
cost = np.mean(cost)
return cost
def gradient(self, x, y):
z = self.forward(x)
gradient_w = (z - y) * x
gradient_w = np.mean(gradient_w, axis=0) # axis=0表示把每一行做相加然后再除以总的行数
gradient_w = gradient_w[:, np.newaxis]
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
# 此处b是一个数值,所以可以直接用np.mean得到一个标量(scalar)
return gradient_w, gradient_b
def update(self, gradient_w, gradient_b, eta=0.01): # eta代表学习率,是控制每次参数值变动的大小,即移动步长,又称为学习率
self.w = self.w - eta * gradient_w # 相减: 参数向梯度的反方向移动
self.b = self.b - eta * gradient_b
def train(self, x, y, iterations=1000, 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()
print(train_data.shape)
x = train_data[:, :-1]
y = train_data[:, -1:]
# 创建网络
net = NetWork(13)
num_iterations = 2000
# 启动训练
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()
在修改之前我们先介绍几个变量:
- min-batch:
每次迭代的时候抽取出来的一批数据被称为一个min-batch。 - batch_size:
一个min-batch所包含的样本数量。 - epoch:
按mini-batch逐次取出样本,当将整个样本集遍历一次后,即完成了一轮的训练,称为一个epoch。
数据处理部分修改
- 拆分数据批次
在上述代码中数据处理部分,我们将总的数据集按照8:2的比例进行了训练集和测试集的分配。
训练集train_data中一共包含了506 * 0.8 = 404条数据集(这里取整)。
如果将batch_size=10,那么我们将取训练集中的前10个样本作为第一个mini-batch,计算梯度和损失函数并更新网络参数。代码如下:
train_data1 = train_data[0:10]
print(train_data1.shape)
# 输出(10, 14)
net = NetWork(13)
x = train_data1[:, :-1]
y = train_data1[:, -1]
loss = net.train(x, y, iterations=1, eta=0.01)
print(loss)
# 输出 [0.9001866101467376]
同理,再取出样本10-19作为第二个mini-batch计算梯度并更新参数,依次类推,直到完成一轮的训练,再根据num_epoches的轮数停止或者再来一轮。
注意:
如果batch_size=10,那下述程序将train_data分成404/10 + 1 = 41个mini_batch。前40个mini_batch,每个均含有10个样本,最后一个mini_batch只含有4个样本。
batch_size = 10
n = len(train_data)
mini_batches = [train_data[k: k + batch_size] for k in range(0, n, batch_size)]
# 这里运用了列表生成式,在列表内部使用for循环依次将tran_data分割成n / batch_size个mini_batch
print('总的mini_batches是:', len(mini_batches))
print('第一个mini_batch维度是', mini_batches[0].shape)
print('最后一个mini_batch维度是', mini_batches[-1].shape)
# 输出
# 总的mini_batches是:41
# 第一个mini_batch维度是(10, 14)
# 最后一个mini_batch维度是(4,14)
- 随机抽取mini_batch的实现
在上述过程中,我们抽取mini_batch的方式是按顺序逐渐取出mini_batch,而在随机梯度下降法中,我们需要随机抽取一部分样本代表总体。
所以我们使用np.random.shuffle来打乱mini_batches中的mini_batch顺序。举个二维数组的例子:
import numpy as np
# 新建一个array
a = np.arange(1, 13).reshape([6, 2])
np.random.shuffle(a)
print(a)
print(a)
# 以下为输出结果
[[ 1 2]
[ 3 4]
[ 5 6]
[ 7 8]
[ 9 10]
[11 12]]
[[11 12]
[ 9 10]
[ 1 2]
[ 3 4]
[ 7 8]
[ 5 6]]
多次运行上面的代码可以发现,shuffle之后的array每次都不一样,二维的数组默认只改变第0维的元素顺序,也就是[1,2]和[3,4]这样的顺序,这正好符合我们的需求。
注:随机的好处在于避免样本顺序对训练过程的影响(人和模型一样都更重视最近的样本),只有在特定情况下才会有意安排样本的训练顺序
训练过程部分的修改
加入多轮和多批次训练的双层循环
- 第一层循环,代表样本集合要被训练遍历的次数,即“epoch”。
for epoch_id in range(num_epoches):
- 第二层循环,代表每次循环时,样本集合被拆分成的多个批次,需要全部执行训练,称为“iter(iteration)”
for iter_id, mini_batch in enumerate(mini_batches):
这里运用了enumerate枚举法,iter_id代表索引值,mini_batch代表每一次索引的数据。
3. 两层训练内是经典的四步
前向计算-> 计算损失-> 计算梯度-> 更新参数
深度学习的一招鲜:两层循环+四个步骤
完整代码实现
import pandas as pd
import numpy as np
import torch.nn as nn
from torch.nn import Linear
import torch.nn.functional as F
from matplotlib import pyplot as plt
def load_data():
# 从文件导入数据
datafile = '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])
# 将原数据集拆分成训练集和测试集
# 这里使用80%的数据做训练,20%的数据做测试
# 测试集和训练集必须是没有交集的
ratio = 0.8
offset = int(data.shape[0] * ratio)
data_slice = data[:offset]
# 计算train数据集的最大值、最小值和平均值
maxinums, mininums, avgs = data_slice.max(axis=0), data_slice.min(axis=0), data_slice.sum(axis=0) / \
data_slice.shape[0]
# 对数据进行归一化处理
for i in range(feature_num):
# print(maxinums[i], mininums[i], avgs[i])
data[:, i] = (data[:, i] - avgs[i]) / (maxinums[i] - mininums[i])
# 训练集和测试集的划分比例
# ratio = 0.8
train_data = data[:offset]
test_data = data[offset:]
return train_data, test_data
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.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
cost = error * error
cost = np.mean(cost)
return cost
def gradient(self, x, y):
z = self.forward(x)
gradient_w = (z - y) * x
gradient_w = np.mean(gradient_w, axis=0) # axis=0表示把每一行做相加然后再除以总的行数
gradient_w = gradient_w[:, np.newaxis]
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
# 此处b是一个数值,所以可以直接用np.mean得到一个标量(scalar)
return gradient_w, gradient_b
def update(self, gradient_w, gradient_b, eta=0.01): # eta代表学习率,是控制每次参数值变动的大小,即移动步长,又称为学习率
self.w = self.w - eta * gradient_w # 相减: 参数向梯度的反方向移动
self.b = self.b - eta * gradient_b
def train(self, training_data, num_epoches, batch_size, eta):
n = len(training_data)
losses = []
for epoch_id in range(num_epoches):
# 在每轮迭代开始之前,将训练数据的顺序随机的打乱
# 然后在按每次取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)] # 这里运用列表生成式,将training_data分为n/batch_size个mini_batch
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 + 1, iter_id + 1, loss))
return losses
# 获取数据
train_data, test_data = load_data()
# 创建网络
net = NetWork(13)
# 启动训练
losses = net.train(train_data, num_epoches=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()
结果如下:
损失函数变化趋势可视化
随机梯度下降加快了训练的过程,但是由于每次仅仅基于少量样本计算梯度损失和更新参数,所以损失下降曲线会出现震荡,但是这无伤大雅。
注意:本案例由于房价预测的数据量过少,所以难以感受到随机梯度下降带来的性能提升。我们以后可以在数据集大的案例上使用随机梯度下降法来对比性能的提升。