波士顿房价预测
本文以经典的波士顿房价预测模型为例,展示使用NumPy进行模型训练的过程。
关于波士顿房价数据集的说明:
波士顿房价预测是一个经典的机器学习任务,类似于程序员世界的“Hello World”。和大家对房价的普遍认知相同,波士顿地区的房价是由诸多因素影响的。该数据集统计了13种可能影响房价的因素和该类型房屋的均价,期望构建一个基于13个因素进行房价预测的模型,如下图所示。
其数据集 housing.data
格式如下:
每一行的前13列是影响房价的13个因素(对应上表),最后一列是真实房价。
梯度下降
经典的四步训练流程:前向计算->计算损失->计算梯度->更新参数
# 导入需要用到的package
import numpy as np
import json
# 使用matplotlib将两个变量和对应的Loss作3D图
import matplotlib.pyplot as plt
%matplotlib inline
# 从文件导入数据
def load_data(datafile):
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)
training_data = data[:offset]
# 计算训练集的最大值,最小值,平均值
maximums = training_data.max(axis=0) # 指定 axis=0 计算每列的最大值
minimums = training_data.min(axis=0) # 指定 axis=0 计算每列的最小值
avgs = training_data.mean(axis=0) # 指定 axis=0 计算每列的平均值
# 对数据进行归一化处理
for i in range(feature_num):
data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
# 训练集和测试集的划分比例
training_data = data[:offset]
test_data = data[offset:]
return training_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):
return np.dot(x, self.w) + self.b
# 计算损失值,使用均方误差
def loss(self, z, y):
return np.sum((z - y) ** 2) / (z - y).shape[0]
# 计算梯度
def gradient(self, x, y):
z = self.forward(x)
gradient_w = np.mean((z-y)*x, axis=0)
gradient_w = gradient_w[:, np.newaxis] # 由于计算均值后,维度损失掉了,所以需要将损失的维度补足
gradient_b = np.mean(z - y)
return gradient_w, gradient_b
# 更新参数
def update(self, gradient_w, gradient_b, eta = 0.01):
self.w = self.w - eta * gradient_w
self.b = self.b - eta * gradient_b
# 训练模型(梯度下降)
def train(self, train_data, iterations=100, eta=0.01):
x = train_data[:, :-1]
y = train_data[:, -1:]
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('./work/housing.data')
# 创建网络
net = Network(13) # 创建带13个参数的网络
# 启动训练(梯度下降)
losses = net.train(train_data, iterations=1000, eta=0.01)
# 画出损失函数的变化趋势
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.xlabel("num_iterations", fontsize=14)
plt.ylabel("loss", fontsize=14)
plt.plot(plot_x, plot_y)
plt.show()
训练输出:
iter 9, loss 5.143394325795511
iter 19, loss 3.097924194225988
iter 29, loss 2.082241020617026
iter 39, loss 1.5673801618157397
iter 49, loss 1.2966204735077431
iter 59, loss 1.1453399043319765
iter 69, loss 1.0530155717435201
iter 79, loss 0.9902292156463155
iter 89, loss 0.9426576903842504
iter 99, loss 0.9033048096880774
...
iter 889, loss 0.19304908885621125
iter 899, loss 0.1912555092527351
iter 909, loss 0.1894982936296714
iter 919, loss 0.18777618462820625
iter 929, loss 0.18608798277314595
iter 939, loss 0.18443254350353405
iter 949, loss 0.18280877436103968
iter 959, loss 0.18121563232764165
iter 969, loss 0.17965212130459232
iter 979, loss 0.1781172897250724
iter 989, loss 0.1766102282933619
iter 999, loss 0.17513006784373505
关于更新参数的说明
为了确定损失函数更小的点,更新参数时需要做以下操作:
def update(self, gradient_w, gradient_b, eta = 0.01):
self.w = self.w - eta * gradient_w
self.b = self.b - eta * gradient_b
- 相减:参数需要向梯度的反方向移动,即斜率更小的反向。
- eta:控制每次参数值沿着梯度反方向变动的大小,即每次移动的步长,又称为学习率。
随机梯度下降
# 导入需要用到的package
import numpy as np
import json
# 使用matplotlib将两个变量和对应的Loss作3D图
import matplotlib.pyplot as plt
%matplotlib inline
# 从文件导入数据
def load_data(datafile):
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)
training_data = data[:offset]
# 计算训练集的最大值,最小值,平均值
maximums = training_data.max(axis=0) # 指定 axis=0 计算每列的最大值
minimums = training_data.min(axis=0) # 指定 axis=0 计算每列的最小值
avgs = training_data.mean(axis=0) # 指定 axis=0 计算每列的平均值
# 对数据进行归一化处理
for i in range(feature_num):
data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
# 训练集和测试集的划分比例
training_data = data[:offset]
test_data = data[offset:]
return training_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):
return np.dot(x, self.w) + self.b
# 计算损失值,使用均方误差
def loss(self, z, y):
return np.sum((z - y) ** 2) / (z - y).shape[0]
# 计算梯度
def gradient(self, x, y):
z = self.forward(x)
gradient_w = np.mean((z-y)*x, axis=0)
gradient_w = gradient_w[:, np.newaxis] # 由于计算均值后,维度损失掉了,所以需要将损失的维度补足
gradient_b = np.mean(z - y)
return gradient_w, gradient_b
# 更新参数
def update(self, gradient_w, gradient_b, eta = 0.01):
self.w = self.w - eta * gradient_w
self.b = self.b - eta * gradient_b
# 训练模型(随机梯度下降)
def train(self, training_data, num_epoches, batch_size=10, eta=0.01):
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)]
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('./work/housing.data')
# 创建网络
net = Network(13) # 创建带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()
训练输出:
Epoch 0 / iter 0, loss = 10.1354
Epoch 0 / iter 1, loss = 3.8290
Epoch 0 / iter 2, loss = 1.9208
Epoch 0 / iter 3, loss = 1.7740
Epoch 0 / iter 4, loss = 0.3366
Epoch 1 / iter 0, loss = 1.6275
Epoch 1 / iter 1, loss = 0.9842
Epoch 1 / iter 2, loss = 0.9121
Epoch 1 / iter 3, loss = 0.9971
Epoch 1 / iter 4, loss = 0.6071
Epoch 2 / iter 0, loss = 0.7689
Epoch 2 / iter 1, loss = 0.8547
Epoch 2 / iter 2, loss = 0.9428
Epoch 2 / iter 3, loss = 0.9791
Epoch 2 / iter 4, loss = 0.4608
...
Epoch 47 / iter 0, loss = 0.0896
Epoch 47 / iter 1, loss = 0.0863
Epoch 47 / iter 2, loss = 0.0933
Epoch 47 / iter 3, loss = 0.0753
Epoch 47 / iter 4, loss = 0.0106
Epoch 48 / iter 0, loss = 0.0644
Epoch 48 / iter 1, loss = 0.1005
Epoch 48 / iter 2, loss = 0.0685
Epoch 48 / iter 3, loss = 0.0836
Epoch 48 / iter 4, loss = 0.1913
Epoch 49 / iter 0, loss = 0.0898
Epoch 49 / iter 1, loss = 0.0617
Epoch 49 / iter 2, loss = 0.0771
Epoch 49 / iter 3, loss = 0.0891
Epoch 49 / iter 4, loss = 0.0369
可以看出,跟普通梯度下降相比,随机梯度下降加快了训练过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降曲线会出现震荡。