paddle实现波士顿房价预测

  • 准备:
  • 导包:
  • 数据处理
  • 数据归一化:
  • 最值归一化:
  • 其他类型:
  • 模型设计
  • 训练配置
  • 训练过程
  • 保存并测试模型
  • 保存模型
  • 测试模型


准备:

导包:

本节课我们将尝试使用飞桨重写房价预测模型,大家可以体会一下二者的异同。在数据处理之前,需要先加载飞桨框架的相关类库。

#加载飞桨、Numpy和相关类库
import paddle
from paddle.nn import Linear
import paddle.nn.functional as F
import numpy as np
import os
import random

代码中参数含义如下:

paddle:飞桨的主库,paddle 根目录下保留了常用API的别名,当前包括:paddle.tensor、paddle.framework目录下的所有API。

paddle.nn:组网相关的API,例如 Linear 、卷积 Conv2D 、 循环神经网络 LSTM 、损失函数 CrossEntropyLoss 、 激活函数 ReLU 等。

Linear:神经网络的全连接层函数,即包含所有输入权重相加的基本神经元结构。在房价预测任务中,使用只有一层的神经网络(全连接层)来实现线性回归模型。

paddle.nn.functional:与paddle.nn一样,包含组网相关的API,例如Linear、激活函数ReLu等。两者下的同名模块功能相同,运行性能也基本一致。 但是,paddle.nn下的模块均是类,每个类下可以自带模块参数;paddle.nn.functional下的模块均是函数,需要手动传入模块计算需要的参数。在实际使用中,卷积、全连接层等层本身具有可学习的参数,建议使用paddle.nn模块,而激活函数、池化等操作没有可学习参数,可以考虑直接使用paddle.nn.functional下的函数代替。

数据处理

def load_data():
    # 从文件导入数据
    datafile = './work/housing.data'
    data = np.fromfile(datafile, sep=' ', dtype=np.float32)

    # 每条数据包括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]

    # 计算train数据集的最大值,最小值,平均值
    maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), \
                                 training_data.sum(axis=0) / training_data.shape[0]
    
    # 记录数据的归一化参数,在预测时对数据做归一化
    global max_values
    global min_values
    global avg_values
    max_values = maximums
    min_values = minimums
    avg_values = avgs

    # 对数据进行归一化处理
    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

数据归一化:

首先,为什么需要数据归一化?举个简答的例子。样本间的距离时间所主导,这样在样本1以[1, 200]输入到模型中去的时候,由于200可能会直接忽略到1的存在,因此我们需要将数据进行归一化。比如将天数转换为占比1年的比例,200/365=0.5479, 100/365=0.2740。

最值归一化:

最值归一化(Normalization):把所有数据映射到0-1之间。适用于分布有明显边界的情况,受 outliner影响较大。

PaddleNLP Taskflow information_extraction准确度不高 paddlepaddle tensorflow_归一化

import numpy as np
import matplotlib.pyplot as plt

x = np.random.randint(0, 100, size=100)
x

结果:

array([84, 18, 75, 75, 78, 30, 39, 33, 29, 30, 48, 77, 54, 30,  1, 32, 91,
       60, 73, 78, 89, 16, 71, 47, 87, 43, 24, 67, 70, 50, 58, 56, 69, 11,
       19, 97, 64, 53, 37, 18, 84, 77,  6,  3, 91, 48, 14,  6, 70, 36, 93,
       43, 78, 78, 73, 18, 96, 58, 77, 78, 29, 96, 75, 59, 58, 19, 65, 90,
       67, 73, 72,  1, 89, 70, 59, 96, 42, 73, 58,  8, 61, 65, 78, 86, 98,
       94, 52,  1, 59, 86, 44, 28, 87,  2, 91, 75, 19, 91, 46, 92])
(x-np.min(x)) / (np.max(x) - np.min(x))

输出结果:

array([0.8556701 , 0.17525773, 0.7628866 , 0.7628866 , 0.79381443,
       0.29896907, 0.39175258, 0.32989691, 0.28865979, 0.29896907,
       0.48453608, 0.78350515, 0.54639175, 0.29896907, 0.        ,
       0.31958763, 0.92783505, 0.60824742, 0.74226804, 0.79381443,
       0.90721649, 0.15463918, 0.72164948, 0.4742268 , 0.88659794,
       0.43298969, 0.2371134 , 0.68041237, 0.71134021, 0.50515464,
       0.58762887, 0.56701031, 0.70103093, 0.10309278, 0.18556701,
       0.98969072, 0.64948454, 0.53608247, 0.37113402, 0.17525773,
       0.8556701 , 0.78350515, 0.05154639, 0.02061856, 0.92783505,
       0.48453608, 0.13402062, 0.05154639, 0.71134021, 0.36082474,
       0.94845361, 0.43298969, 0.79381443, 0.79381443, 0.74226804,
       0.17525773, 0.97938144, 0.58762887, 0.78350515, 0.79381443,
       0.28865979, 0.97938144, 0.7628866 , 0.59793814, 0.58762887,
       0.18556701, 0.65979381, 0.91752577, 0.68041237, 0.74226804,
       0.73195876, 0.        , 0.90721649, 0.71134021, 0.59793814,
       0.97938144, 0.42268041, 0.74226804, 0.58762887, 0.07216495,
       0.6185567 , 0.65979381, 0.79381443, 0.87628866, 1.        ,
       0.95876289, 0.5257732 , 0.        , 0.59793814, 0.87628866,
       0.44329897, 0.27835052, 0.88659794, 0.01030928, 0.92783505,
       0.7628866 , 0.18556701, 0.92783505, 0.46391753, 0.93814433])
X = np.random.randint(0, 100, (50, 2))
X[:10, :]
X = np.array(X, dtype=float)
X[:, 0] = (X[:, 0] - np.min(X[:, 0])) / (np.max(X[:, 0]) - np.min(X[:, 0]))
X[:, 0]
X[:, 1] = (X[:, 1] - np.min(X[:, 1])) / (np.max(X[:, 1]) - np.min(X[:, 1]))
X[:, 1]
X[:10, :]
plt.scatter(X[:,0], X[:,1])
plt.show()
np.mean(X[:,0])
np.std(X[:, 0])
np.mean(X[:,1])
np.std(X[:, 1])
其他类型:

链接: https://zhuanlan.zhihu.com/p/76682561.

模型设计

模型定义的实质是定义线性回归的网络结构,飞桨建议通过创建Python类的方式完成模型网络的定义,该类需要继承paddle.nn.Layer父类,并且在类中定义init函数和forward函数。forward函数是框架指定实现前向计算逻辑的函数,程序在调用模型实例时会自动执行forward方法。在forward函数中使用的网络层需要在init函数中声明。

实现过程分如下两步:

  1. 定义init函数:在类的初始化函数中声明每一层网络的实现函数。在房价预测模型中,只需要定义一层全连接层,模型结构和使用Python和Numpy构建神经网络模型》章节模型保持一致。
  2. 定义forward函数:构建神经网络结构,实现前向计算过程,并返回预测结果,在本任务中返回的是房价预测结果。
class Regressor(paddle.nn.Layer):

    # self代表类的实例自身
    def __init__(self):
        # 初始化父类中的一些参数
        super(Regressor, self).__init__()
        
        # 定义一层全连接层,输入维度是13,输出维度是1
        self.fc = Linear(in_features=13, out_features=1)
    
    # 网络的前向计算
    def forward(self, inputs):
        x = self.fc(inputs)
        return x

训练配置

  1. 声明定义好的回归模型Regressor实例,并将模型的状态设置为训练。
  2. 使用load_data函数加载训练数据和测试数据。
  3. 设置优化算法和学习率,优化算法采用随机梯度下降SGD,学习率设置为0.01。
# 声明定义好的线性回归模型
model = Regressor()
# 开启模型训练模式
model.train()
# 加载数据
training_data, test_data = load_data()
# 定义优化算法,使用随机梯度下降SGD
# 学习率设置为0.01
opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())

说明:

模型实例有两种状态:训练状态.train()和预测状态.eval()。训练时要执行正向计算和反向传播梯度两个过程,而预测时只需要执行正向计算,为模型指定运行状态,有两点原因:

  1. 部分高级的算子(例如Dropout和BatchNorm,在计算机视觉的章节会详细介绍)在两个状态执行的逻辑不同;
  2. 从性能和存储空间的考虑,预测状态时更节省内存(无需记录反向梯度),性能更好。

训练过程

  1. 数据准备:将一个批次的数据先转换成np.array格式,再转换成paddle内置tensor格式。
  2. 前向计算:将一个批次的样本数据灌入网络中,计算输出结果。
  3. 计算损失函数:以前向计算结果和真实房价作为输入,通过损失函数square_error_cost API计算出损失函数值(Loss)。飞桨所有的API接口都有完整的说明和使用案例,在后续教程中我们会详细介绍API的查阅方法。
  4. 反向传播:执行梯度反向传播backward函数,即从后到前逐层计算每一层的梯度,并根据设置的优化算法更新参数。
EPOCH_NUM = 10   # 设置外层循环次数
BATCH_SIZE = 10  # 设置batch大小

# 定义外层循环
for epoch_id in range(EPOCH_NUM):
    # 在每轮迭代开始之前,将训练数据的顺序随机的打乱
    np.random.shuffle(training_data)
    # 将训练数据进行拆分,每个batch包含10条数据
    mini_batches = [training_data[k:k+BATCH_SIZE] for k in range(0, len(training_data), BATCH_SIZE)]
    # 定义内层循环
    for iter_id, mini_batch in enumerate(mini_batches):
        x = np.array(mini_batch[:, :-1]) # 获得当前批次训练数据
        y = np.array(mini_batch[:, -1:]) # 获得当前批次训练标签(真实房价)
        # 将numpy数据转为飞桨动态图tensor形式
        house_features = paddle.to_tensor(x)
        prices = paddle.to_tensor(y)
        
        # 前向计算
        predicts = model(house_features)
        
        # 计算损失
        loss = F.square_error_cost(predicts, label=prices)
        avg_loss = paddle.mean(loss)
        if iter_id%20==0:
            print("epoch: {}, iter: {}, loss is: {}".format(epoch_id, iter_id, avg_loss.numpy()))
        
        # 反向传播
        avg_loss.backward()
        # 最小化loss,更新参数
        opt.step()
        # 清除梯度
        opt.clear_grad()
epoch: 0, iter: 0, loss is: [0.08927596]
epoch: 0, iter: 20, loss is: [0.04351359]
epoch: 0, iter: 40, loss is: [0.06200176]
epoch: 1, iter: 0, loss is: [0.02089665]
epoch: 1, iter: 20, loss is: [0.08222677]
epoch: 1, iter: 40, loss is: [0.20478992]
epoch: 2, iter: 0, loss is: [0.04145242]
epoch: 2, iter: 20, loss is: [0.15099502]
epoch: 2, iter: 40, loss is: [0.02584986]
epoch: 3, iter: 0, loss is: [0.05165129]
epoch: 3, iter: 20, loss is: [0.12128074]
epoch: 3, iter: 40, loss is: [0.01223269]
epoch: 4, iter: 0, loss is: [0.0389842]
epoch: 4, iter: 20, loss is: [0.07199501]
epoch: 4, iter: 40, loss is: [0.03861689]
epoch: 5, iter: 0, loss is: [0.0647126]
epoch: 5, iter: 20, loss is: [0.03957129]
epoch: 5, iter: 40, loss is: [0.09043328]
epoch: 6, iter: 0, loss is: [0.08471931]
epoch: 6, iter: 20, loss is: [0.06079114]
epoch: 6, iter: 40, loss is: [0.01805439]
epoch: 7, iter: 0, loss is: [0.01560107]
epoch: 7, iter: 20, loss is: [0.02061713]
epoch: 7, iter: 40, loss is: [0.04313775]
epoch: 8, iter: 0, loss is: [0.08785576]
epoch: 8, iter: 20, loss is: [0.0171585]
epoch: 8, iter: 40, loss is: [0.01056823]
epoch: 9, iter: 0, loss is: [0.0491889]
epoch: 9, iter: 20, loss is: [0.03958869]
epoch: 9, iter: 40, loss is: [0.03789285]

保存并测试模型

保存模型

将模型当前的参数数据model.state_dict()保存到文件中(通过参数指定保存的文件名 LR_model),以备预测或校验的程序调用,代码如下所示。

保存模型参数,文件名为LR_model.pdparams

paddle.save(model.state_dict(), 'LR_model.pdparams')
print("模型保存成功,模型参数保存在LR_model.pdparams中")

测试模型

下面我们选择一条数据样本,测试下模型的预测效果。测试过程和在应用场景中使用模型的过程一致,主要可分成如下三个步骤:

配置模型预测的机器资源。本案例默认使用本机,因此无需写代码指定。
将训练好的模型参数加载到模型实例中。由两个语句完成,第一句是从文件中读取模型参数;第二句是将参数内容加载到模型。加载完毕后,需要将模型的状态调整为eval()(校验)。上文中提到,训练状态的模型需要同时支持前向计算和反向传导梯度,模型的实现较为臃肿,而校验和预测状态的模型只需要支持前向计算,模型的实现更加简单,性能更好。
将待预测的样本特征输入到模型中,打印输出的预测结果。
通过load_one_example函数实现从数据集中抽一条样本作为测试样本,具体实现代码如下所示。

def load_one_example():
    # 从上边已加载的测试集中,随机选择一条作为测试数据
    idx = np.random.randint(0, test_data.shape[0])
    idx = -10
    one_data, label = test_data[idx, :-1], test_data[idx, -1]
    # 修改该条数据shape为[1,13]
    one_data =  one_data.reshape([1,-1])

    return one_data, label
# 参数为保存模型参数的文件地址
model_dict = paddle.load('LR_model.pdparams')
model.load_dict(model_dict)
model.eval()

# 参数为数据集的文件地址
one_data, label = load_one_example()
# 将数据转为动态图的variable格式 
one_data = paddle.to_tensor(one_data)
predict = model(one_data)

# 对结果做反归一化处理
predict = predict * (max_values[-1] - min_values[-1]) + avg_values[-1]
# 对label数据做反归一化处理
label = label * (max_values[-1] - min_values[-1]) + avg_values[-1]

print("Inference result is {}, the corresponding label is {}".format(predict.numpy(), label))