【深度学习】基于线性回归实现波士顿房价预测任务
波士顿房价预测任务
本篇文章按照飞桨深度学习步骤,运行步骤中的代码,学习目标:
1、了解机器学习的实施方法
2、学习numpy的一些方法
构建神经网络/深度学习模型的基本步骤
思考:
1、为什么使用线性回归模型?
2、为什么要以均方误差作为损失函数?即将模型在每个训练样本上的预测误差加和,来衡量整体样本的准确性。这是因为损失函数的设计不仅仅要考虑“合理性”(有物理意义),同样需要考虑“易解性”(易于求解),这个问题在后面的内容中会详细阐述。
1.数据处理
数据处理包含五个部分:数据导入、数据形状变换、数据集划分、数据归一化处理和封装load data函数。数据预处理后,才能被模型调用。
1.1.数据下载
1、从公开数据集、爬虫、传感器等渠道收集数据。
2、确保数据量足够大且具有代表性。
https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data
代码目录结构如下:
1.2.数据读取
通过如下代码读入数据,了解下波士顿房价的数据集结构,数据存放在本地目录下housing.data文件中。
# 导入需要用到的package
import numpy as np
import json
# 读入训练数据
datafile = './work/housing.data'
data = np.fromfile(datafile, sep=' ')
data
1.3.数据形状变换
由于读入的原始数据是1维的,所有数据都连在一起。因此需要我们将数据的形状进行变换,形成一个2维的矩阵,每行为一个数据样本(14个值),每个数据样本包含13个x(影响房价的特征)和一个
y(该类型房屋的均价)。
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])
print(data.shape)
1.4.数据集划分
将数据集划分成训练集和测试集,其中训练集用于确定模型的参数,测试集用于评判模型的效果。为什么要对数据集进行拆分,而不能直接应用于模型训练呢?这与学生时代的授课和考试关系比较类似,如 图4所示。
ratio = 0.8
offset = int(data.shape[0] * ratio)
training_data, test_data = data[:offset], data[offset:]
1.5.数据归一化处理
归一化处理是将数据按比例缩放到一个特定范围(通常是[0, 1]或[-1, 1])的过程,常用于消除量纲影响,提升模型性能。以下是常见的归一化方法:
from sklearn.preprocessing import MinMaxScaler
import numpy as np
data = np.array([[1, 2], [2, 3], [3, 4]])
scaler = MinMaxScaler()
normalized_data = scaler.fit_transform(data)
from sklearn.preprocessing import StandardScaler
import numpy as np
data = np.array([[1, 2], [2, 3], [3, 4]])
scaler = StandardScaler()
standardized_data = scaler.fit_transform(data)
import numpy as np
data = np.array([[1, 2], [2, 3], [3, 4]])
j = np.ceil(np.log10(np.max(np.abs(data))))
normalized_data = data / (10 ** j)
print(normalized_data)
from sklearn.preprocessing import MaxAbsScaler
import numpy as np
data = np.array([[1, 2], [2, 3], [3, 4]])
scaler = MaxAbsScaler()
normalized_data = scaler.fit_transform(data)
print(normalized_data)
此次任务选择Min-Max 归一化方法进行数据归一化处理,在机器学习中,Min-Max归一化的最大值和最小值通常是从训练集中计算的,而不是整个数据集。这是为了避免**数据泄露(Data Leakage)**问题。
# 使用paddle原始代码
maximums, minimums = \
training_data.max(axis=0), \
training_data.min(axis=0),
# 对数据进行归一化处理
for i in range(feature_num):
data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
scaler = MinMaxScaler()
# 只在训练集上拟合
scaler.fit(training_data)
data = scaler.transform(data)
对比两者的归一化结果是一致的
1.6.封装成load data函数
# 导入需要用到的package
import numpy as np
import json
from sklearn.preprocessing import MinMaxScaler
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])
# 将原数据集拆分成训练集和测试集
# 这里使用80%的数据做训练,20%的数据做测试
# 测试集和训练集必须是没有交集的
ratio = 0.8
offset = int(data.shape[0] * ratio)
training_data, test_data = data[:offset], data[offset:]
# # 计算训练集的最大值,最小值
# maximums, minimums = training_data.max(axis=0), \
# training_data.min(axis=0)
#
# # 对数据进行归一化处理
# for i in range(feature_num):
# data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
# 使用训练集计算最大值和最小值
scaler = MinMaxScaler()
# 只在训练集上拟合
scaler.fit(training_data)
data = scaler.transform(data)
# 训练集和测试集的划分比例
training_data = data[:offset]
test_data = data[offset:]
return training_data, test_data
2.模型设计
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])
取出第1条样本数据,观察样本的特征向量与参数向量相乘的结果。
training_data, test_data = load_data()
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])
# 注意paddle上只取了第一条数据,没有取前13个数据,因为最后一个是房价y
x1=training_data[0][:13]
t = np.dot(x1, w)
print(t)
完整的线性回归公式,还需要初始化偏移量b,同样随意赋初值-0.2。那么,线性回归模型的完整输出是z=t+b,这个从特征和参数计算输出值的过程称为“前向计算”。
t = np.dot(x1, w)
b = -0.2
z = t + b
print(z)
将上述计算预测输出的过程以“类和对象”的方式来描述,类成员变量有参数w和b。通过写一个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.dot(x, self.w) + self.b
return z
training_data, test_data = load_data()
# 提取前 13 列作为 X
x = training_data[:, :-1] # 所有行,列从第 0 列到倒数第 2 列
# 提取最后一列作为 Y
y = training_data[:, -1] # 所有行,最后一列
x1 = x[0]
net = Network(13)
z = net.forward(x1)
print(z)
3.训练配置
# 提取前 13 列作为 X
x = training_data[:, :-1] # 所有行,列从第 0 列到倒数第 2 列
# 提取最后一列作为 Y
y = training_data[:, -1] # 所有行,最后一列
x1 = x[0]
y1 = y[0]
net = Network(13)
z = net.forward(x1)
Loss = (y1 - z)*(y1 - z)
print(Loss)
在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.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
training_data, test_data = load_data()
# 提取前 13 列作为 X
x = training_data[:, :-1] # 所有行,列从第 0 列到倒数第 2 列
# 提取最后一列作为 Y
y = training_data[:, -1] # 所有行,最后一列
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)
4.训练过程
4.1.梯度下降法
4.2.计算梯度
4.3.使用Numpy进行梯度计算
基于Numpy广播机制(对向量和矩阵计算如同对1个单一变量计算一样),可以更快速的实现梯度计算。计算梯度的代码中直接用(z1−y1)⋅x1,得到的是一个13维的向量,每个分量分别代表该维度的梯度。
1、下面我们考虑只有一个样本的情况下,计算梯度:
net = Network(13)
x1 = x[0]
y1 = y[0]
z1 = net.forward(x1)
gradient_w = (z1 - y1) * x1
print('gradient_w_by_sample1 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
输入数据中有多个样本,每个样本都对梯度有贡献。如上代码计算了只有样本1时的梯度值,同样的计算方法也可以计算其他样本对梯度的贡献。
2、注意这里是一次取出3个样本的数据,不是取出第3个样本
# 注意这里是一次取出3个样本的数据,不是取出第3个样本
x3samples = x[0:3]
y3samples = y[0:3].reshape(3, 1)
z3samples = net.forward(x3samples)
gradient_w = (z3samples - y3samples) * x3samples
print('gradient_w {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
3、那么对于有N个样本的情形,我们可以直接使用如下方式计算出所有样本对梯度的贡献,这就是使用Numpy库广播功能带来的便捷。
z = net.forward(x)
gradient_w = (z - y) * x
print('gradient_w shape {}'.format(gradient_w.shape))
print(gradient_w)
我们也可以使用Numpy的均值函数来完成此过程:
z = net.forward(x)
gradient_w = (z - y) * x
gradient_w = np.mean(gradient_w, axis=0)
print('gradient_w ', gradient_w.shape)
print('w ', net.w.shape)
print(gradient_w)
print(net.w)
同样,计算b的梯度的代码也是类似的原理。
gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
# 此处b是一个数值,所以可以直接用np.mean得到一个标量
gradient_b
将上面计算w和b的梯度的过程,写成Network类的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 = 0.
def forward(self, x):
z = np.dot(x, self.w) + self.b
return z
def loss(self, z, y):
error = z - y
num_samples = error.shape[0]
cost = error * error
cost = np.sum(cost) / num_samples
return cost
def gradient(self, x, y):
z = self.forward(x)
gradient_w = (z - y) * x
gradient_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_b
调用上面定义的gradient函数,计算梯度
# 设置[w5, w9] = [-100., -100.]
net.w[5] = -100.0
net.w[9] = -100.0
z = 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]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
4.4.确定损失函数更小的点
下面我们开始研究更新梯度的方法。首先沿着梯度的反方向移动一小步,找到下一个点P1,观察损失函数的变化。
# 设置[w5, w9] = [-100., -100.]
net.w[5] = -100.0
net.w[9] = -100.0
z = 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]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
# 在[w5, w9]平面上,沿着梯度的反方向移动到下一个点P1
# 定义移动步长 eta
eta = 0.1
# 更新参数w5和w9
net.w[5] = net.w[5] - eta * gradient_w5
net.w[9] = net.w[9] - eta * gradient_w9
# 重新计算z和loss
z = 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]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
相减:参数需要向梯度的反方向移动。
eta:控制每次参数值沿着梯度反方向变动的大小,即每次移动的步长,又称为学习率。
4.5.代码封装Train函数
def update(self, gradient_w5, gradient_w9, eta=0.01):
self.w[5] = self.w[5] - eta * gradient_w5
self.w[9] = self.w[9] - eta * gradient_w9
def 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
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()
4.6.训练扩展到全部参数
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, 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
# 创建网络
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()
4.7.随机梯度下降法( Stochastic Gradient Descent)
在上述程序中,每次损失函数和梯度计算都是基于数据集中的全量数据。对于波士顿房价预测任务数据集而言,样本数比较少,只有404个。但在实际问题中,数据集往往非常大,如果每次都使用全量数据进行计算,效率非常低,通俗地说就是“杀鸡焉用牛刀”。由于参数每次只沿着梯度反方向更新一点点,因此方向并不需要那么精确。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:
minibatch:每次迭代时抽取出来的一批数据被称为一个minibatch。
batch size:每个minibatch所包含的样本数目称为batch size。
Epoch:当程序迭代的时候,按minibatch逐渐抽取出样本,当把整个数据集都遍历到了的时候,则完成了一轮训练,也叫一个Epoch(轮次)。启动训练时,可以将训练的轮数num_epochs和batch_size作为参数传入。
1)数据处理需要实现拆分数据批次和样本乱序(为了实现随机抽样的效果)两个功能。
# 获取数据
train_data, test_data = load_data()
# 打乱样本顺序
np.random.shuffle(train_data)
# 将train_data分成多个minibatch
batch_size = 10
n = 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)
2)训练过程代码修改。将每个随机抽取的minibatch数据输入到模型中用于参数训练。训练过程的核心是两层循环:
第一层循环,代表样本集合要被训练遍历几次,称为“epoch”,代码如下:
for epoch_id in range(num_epochs):
第二层循环,代表每次遍历时,样本集合被拆分成的多个批次,需要全部执行训练,称为“iter (iteration)”,代码如下:
for iter_id,mini_batch in emumerate(mini_batches):
在两层循环的内部是经典的四步训练流程:前向计算->计算损失->计算梯度->更新参数,这与大家之前所学是一致的,代码如下:
将两部分改写的代码集成到Network类中的train函数中,最终的实现如下。
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
num_samples = error.shape[0]
cost = error * error
cost = np.sum(cost) / num_samples
return cost
def 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_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, num_epochs, batch_size=10, eta=0.01):
n = len(train_data)
losses = []
for epoch_id in range(num_epochs):
# 在每轮迭代开始之前,将训练数据的顺序随机打乱
# 然后再按每次取batch_size条数据的方式取出
np.random.shuffle(train_data)
# 将训练数据进行拆分,每个mini_batch包含batch_size条的数据
mini_batches = [train_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
# 启动训练
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()
6.总结
本节我们详细介绍了如何使用Numpy实现梯度下降算法,构建并训练了一个简单的线性模型实现波士顿房价预测,可以总结出,使用神经网络建模房价预测有三个要点:
- 构建网络,初始化参数w和b,定义预测和损失函数的计算方法。
- 随机选择初始点,建立梯度的计算方法和参数更新方式。
- 将数据集的数据按batch size的大小分成多个minibatch,分别灌入模型计算梯度并更新参数,不断迭代直到损失函数几乎不再下降。
深度学习模型的基本步骤的最后一个步骤:模型保存及使用后续再来学习。