问题描述:

波士顿房价预测是一个经典的机器学习问题,类似于程序员世界的“Hello World”。

波士顿地区的房价是由诸多因素影响的,该数据集统计了13种可能影响房价的因素和该类型房屋的均价,期望构建一个基于13个因素预测房价的模型。

预测问题根据预测输出的类型是连续的实数值,还是离散的标签,区分为回归任务和分类任务。因为房价是一个连续值,所以房价预测显然是一个回归任务。下面我们尝试用最简单的线性回归模型解决这个问题,并用神经网络来实现这个模型。

已知,一个房子的13种影响因素,存储在x矩阵中,以及该房子的房价y。

那么N个房子的表示则是:x[ ] N x13, 即N行13列,y[ ] Nx1,即N行1列。 

去N*0.8的房子数据,进行训练,求出模型,即 w 和 b,再用N*0.2 的数据进行校对,看w 和b 是否可接受。

公式推导:

一个房子:房价y(一个值) ,由M个因素影响,x矩阵表示影响因素矩阵,即x是一个M行1列的向量,

每个因素的权重由w表示,权重w也是一个M行1列的向量,且有一个偏置b,

最终:

pytorch房价预测模型 房价预测python_pytorch房价预测模型

N个房子:由于波士顿不止一个房子,所以第 i 个房子的房价

pytorch房价预测模型 房价预测python_数据_02

   ,上角标(i) 表示第i 个房子。

N个房子用矩阵表示:N行y(i),1列。 

根据拿到的数据,其中y 都是真实的价格,x 是实际影响因素,w 和 b 是需要我们计算的。

初始随机一组权重 w 和偏置 b,设预测的房价为 z ,则:z = xw+b, 

pytorch房价预测模型 房价预测python_pytorch房价预测模型_03

, z 和 b 均为一个数。使用损失函数来计算预测的房价是否正确: 

pytorch房价预测模型 房价预测python_数据集_04

则第 i 个房子的损失函数为:

pytorch房价预测模型 房价预测python_数据_05

所有的房子的损失函数:

pytorch房价预测模型 房价预测python_pytorch房价预测模型_06

 

对L求w的偏导数之前,引入一个1/2的系数: 

pytorch房价预测模型 房价预测python_数据_07

然后继续求偏导数,由于存在负号,所以y 和 z 换了位置,也就变成了 : (xw+b) - y

pytorch房价预测模型 房价预测python_数据_08

pytorch房价预测模型 房价预测python_数据集_09

 

由于w 有13个参数,所以求其中任一 一个参数的导数时,都需要 每个房子的 x 乘以 w ,加上 b ,然后减去这个房子的 真实房价y, 然后再乘以这个房子 i  的 第 j 项因素,把所有房子的上述结果加和,然后除以房子个数N,就求出了这个w的第 j  个偏导数。

如果一共有N个房子,且M个因素,那么要计算 N*M次的“加和”,才完成1次迭代,后面有优化,这里先解释到此。

对L求b的偏导数:

pytorch房价预测模型 房价预测python_pytorch房价预测模型_10

 

解释:每一个房子的xw+b 减去 真实房价y ,所有N个房子求和,然后除以N ,即可。

求出偏导数▽之后,按照梯度下降的方法,每次减少一个步长γ*▽,更换上一次的x,如下:

pytorch房价预测模型 房价预测python_数据集_11

按照上面的公式,一直迭代,直到满足迭代的次数。

小批量随机梯度下降法(Mini-batch Stochastic Gradient Descent)

在上述程序中,每次迭代的时候均基于数据集中的全部数据进行计算,才能完成一次迭代更新一次w和b。也就是计算一个wj 的时候,要把所有房子第 j 个因素都计算一遍。

但在实际问题中数据集往往非常大,如果每次计算都使用全部的数据来计算损失函数和梯度,效率非常低。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失,然后更新参数。这种方法被称作小批量随机梯度下降法(Mini-batch Stochastic Gradient Descent),简称SGD。也就是取部分房子的 第 j 个因素,进行计算,然后更新 w和b 。

每次迭代时抽取出来的一批数据被称为一个min-batch(一小群数据),一个mini-batch所包含的样本数目称为batch_size(这群数据是几所房子的)。当程序迭代的时候,按mini-batch逐渐抽取出样本,当把整个数据集都遍历到了的时候,则完成了一轮的训练,也叫一个epoch(一轮训练)

启动训练时,可以将训练的轮数num_epochsbatch_size作为参数传入。

对于求w 的偏导数时,计算量偏大的问题,可以进行优化,也就是每更新一个wj 值的时候,不用所有的房子都扔进公式算一遍,可以把N个数据分成M份,每次计算一个wj 的时候,只需要从M份中取一份,计算即可。

python代码:

  1. 取数据的80%的部分,用于测试,得到model模型,并保存输出到文件中。
  2. 取10%的数据,用于开发集测试,如果开发集测试结果 达不到预期,则返回第一步,修改参数继续测试。如果开发集测试结果达到预期,则进入第三步,。
  3. 使用测试集,进行测试。

目前下面的代码,只实现了第一步。

import numpy as np
import json
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


def load_data():
    # 加载文件
    datafile = './housing.data'  # 原始文件,506行14列
    data = np.fromfile(datafile, sep=' ')  # 7084 = 14 * 506,一行数据显示
    # 每条包含14列
    feature_names = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS',
 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT',
                     'MEDV']
    feature_num = len(feature_names)  # 14列
    # print(data.shape[0] // feature_num)  # 7084//14 向下取整保留整数
    data = data.reshape([data.shape[0] // feature_num, feature_num])  # 506行,14列
    # print(data[0])  # 14个数据
    # print(data.shape)  # 506行,14列
    ratio = 0.8  # shape[0] 表示506行,shape[1] 表示14列,取data的80%的数据,即506*0.8,404行样本
    offset = int(data.shape[0] * ratio)  
    training_data = data[:offset]
    # print(training_data.shape)  # 获得 404行14列 的数据
    # print(training_data.max(axis=0))  # 列最大值 axis=0,列; axis=1,行;
    # print(training_data.min(axis=0))  # 列最小值
    # print(training_data.sum(axis=0))  # 列求和
    # 最大值,最小值,平均值
    maximums = training_data.max(axis=0)
    minimums = training_data.min(axis=0)
    avgs = training_data.sum(axis=0) / training_data.shape[0]

    # 对所有的数据进行归一化处理
    for i in range(feature_num):  # 从0到13列
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])  # 逐列的归一化
    training_data = data[:offset]  # 重新覆盖原有的training_data, 404行14列
    test_data = data[offset:]  # 剩下20%的数据,用于预测模型
    return training_data, test_data


class NetWork(object):
    def __init__(self, num_of_weights):
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)  # 生成num_of_weights 行,1列的 [0,1)之间的数据
        self.b = 0.  # 初始设为0

    def forward(self, x):  # x 是向量,404行13列
        z = np.dot(x, self.w) + self.b  # z是404行一列;w 13行1列
        return z

    def loss(self, z, y):  # y是404行1列
        error = z - y
        cost = error * error
        cost = np.mean(cost)
        return cost

    def gradient(self, x, y):
        z = self.forward(x)  # z = xw+b, 404行1列
        gradient_w = (z - y) * x  # 一组数据
        gradient_w = np.mean(gradient_w, axis=0)  # 除以N
        gradient_w = gradient_w[:, np.newaxis]  # 更改维数
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)
        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
    # x 404行13列,y 404行1列,
    def train(self, x, y, iterations=100, eta=0.01):  # x 404行13列,y 404行1列,迭代100次,每一移动0.01
        losses = []
        for i in range(iterations):
            z = self.forward(x)  # z = xw+b, 404行1列
            L = self.loss(z, y)  # L 是 404行1列
            gradient_w, gradient_b = self.gradient(x, y)  # gradient_w 404行1列,gradient_b 仅为1个数
            self.update(gradient_w, gradient_b, eta)
            losses.append(L)
            if i % 50 == 0:
                print('iter {}, loss {}'.format(i, L))
        return losses

    # 输入训练数据,训练轮数,每次取多少个数据,迭代率
    def train_min(self, trainint_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)]  # 比如仅取了10行样本
            for iter_id, mini_batch in enumerate(mini_batches):
                x = mini_batch[:, :-1]  # 10行13列
                y = mini_batch[:, -1:]  # 10行1列
                a = self.forward(x)  # 计算a = xw+b
                loss = self.loss(a, y)  # (a - y)^2 / 10
                gradient_w, gradient_b = self.gradient(x, y)  # 仅用10行样本,就得到了一次gradient_w, gradient_b
                self.update(gradient_w, gradient_b, eta)  # 更新 gradient_w, gradient_b
                losses.append(loss)  # 将损失值记录下来
                print('Epoch {:3d} / iter {:3d}, loss = {:.4f}'.format(epoch_id, iter_id, loss))
        return losses

    def saveModel(self, fileName):
        np.savez(fileName, self.w, self.b)


# === 不优化的时候===
training_data, test_data = load_data()  # 404行14列,102行14列
x = training_data[:, :-1]  # 取所有行的前13列, 404行13列
y = training_data[:, -1:]  # 取所有行的最后1列, 404行 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_x = np.arange(len(losses))  # 0-len(losses) 的数,且不含len(losses), 步长为1
# plot_y = np.array(losses)
# plt.plot(plot_x, plot_y)
# plt.show()


# === 优化之后===
training_data, test_data = load_data()  # 404行14列,102行14列
# 输入原始数据,迭代50次,将样本分成100份,每次迭代使用4份,所以一共是50*100次更新。
losses = net.train_min(training_data, num_epoches=50, batch_size=100, eta=0.1)
# 保存训练好的模型
net.saveModel("model")
# 打印输出查看模型
print(np.load('model.npz')["arr_0"], np.load('model.npz')["arr_1"])