第2章(下)基于线性回归完成波士顿房价预测任务

2.4 Runner类介绍

通过上面的实践,我们可以看到,在一个任务上应用机器学习方法的流程基本上包括:数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。

为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个Runner类。除上述提到的要素外,再加上模型保存、模型加载等功能。

Runner类的成员函数定义如下:

  • __init__函数:实例化Runner类时默认调用,需要传入模型、损失函数、优化器和评价指标等;
  • train函数:完成模型训练,指定模型训练需要的训练集和验证集;
  • evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
  • predict函数:选取一条数据对训练好的模型进行预测;
  • save_model函数:模型在训练过程和训练结束后需要进行保存;
  • load_model函数:调用加载之前保存的模型。

Runner类的框架定义如下:

class Runner(object):
    def __init__(self, model, optimizer, loss_fn, metric):
        self.model = model         # 模型
        self.optimizer = optimizer # 优化器
        self.loss_fn = loss_fn     # 损失函数   
        self.metric = metric       # 评估指标

    # 模型训练
    def train(self, train_dataset, dev_dataset=None, **kwargs):
        pass

    # 模型评价
    def evaluate(self, data_set, **kwargs):
        pass

    # 模型预测
    def predict(self, x, **kwargs):
        pass

    # 模型保存
    def save_model(self, save_path):
        pass

    # 模型加载
    def load_model(self, model_path):
        pass

Runner类的流程如图2.8所示,可以分为 4 个阶段:

  1. 初始化阶段:传入模型、损失函数、优化器和评价指标。
  2. 模型训练阶段:基于训练集调用train()函数训练模型,基于验证集通过evaluate()函数验证模型。通过save_model()函数保存模型。
  3. 模型评价阶段:基于测试集通过evaluate()函数得到指标性能。
  4. 模型预测阶段:给定样本,通过predict()函数得到该样本标签。
图2.8 Runner类

2.5 基于线性回归的波士顿房价预测

在本节中,我们使用线性回归来对马萨诸塞州波士顿郊区的房屋进行预测。实验流程主要包含如下5个步骤:

  • 数据处理:包括数据清洗(缺失值和异常值处理)、数据集划分,以便数据可以被模型正常读取,并具有良好的泛化性;
  • 模型构建:定义线性回归模型类;
  • 训练配置:训练相关的一些配置,如:优化算法、评价指标等;
  • 组装训练框架Runner:Runner用于管理模型训练和测试过程;
  • 模型训练和测试:利用Runner进行模型训练和测试。

2.5.1 数据处理

2.5.1.1 数据集介绍

本实验使用波士顿房价预测数据集,共506条样本数据,每条样本包含了12种可能影响房价的因素和该类房屋价格的中位数,各字段含义如表2.1所示:

字段名类型含义
CRIMfloat该镇的人均犯罪率
ZNfloat占地面积超过25,000平方呎的住宅用地比例
INDUSfloat非零售商业用地比例
CHASint是否邻近 Charles River 1=邻近;0=不邻近
NOXfloat一氧化氮浓度
RMfloat每栋房屋的平均客房数
AGEfloat1940年之前建成的自用单位比例
DISfloat到波士顿5个就业中心的加权距离
RADint到径向公路的可达性指数
TAXint全值财产税率
PTRATIOfloat学生与教师的比例
LSTATfloat低收入人群占比
MEDVfloat同类房屋价格的中位数
表2.1 波士顿房价字段含义

预览前5条数据,代码实现如下:

import pandas as pd # 开源数据分析和操作工具

# 利用pandas加载波士顿房价的数据集
data=pd.read_csv("/home/aistudio/work/boston_house_prices.csv")
# 预览前5行数据
data.head()
CRIMZNINDUSCHASNOXRMAGEDISRADTAXPTRATIOLSTATMEDV
00.0063218.02.3100.5386.57565.24.0900129615.34.9824.0
10.027310.07.0700.4696.42178.94.9671224217.89.1421.6
20.027290.07.0700.4697.18561.14.9671224217.84.0334.7
30.032370.02.1800.4586.99845.86.0622322218.72.9433.4
40.069050.02.1800.4587.14754.26.0622322218.75.3336.2

2.5.1.2 数据清洗

对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。

  • 缺失值分析

通过isna()方法判断数据中各元素是否缺失,然后通过sum()方法统计每个字段缺失情况,代码实现如下:

# 查看各字段缺失值统计情况
data.isna().sum()
CRIM       0
ZN         0
INDUS      0
CHAS       0
NOX        0
RM         0
AGE        0
DIS        0
RAD        0
TAX        0
PTRATIO    0
LSTAT      0
MEDV       0
dtype: int64

从输出结果看,波士顿房价预测数据集中不存在缺失值的情况。

  • 异常值处理

通过箱线图直观的显示数据分布,并观测数据中的异常值。箱线图一般由五个统计值组成:最大值、上四分位、中位数、下四分位和最小值。一般来说,观测到的数据大于最大估计值或者小于最小估计值则判断为异常值,其中
最大估计值 = 上四分位 + 1.5 ∗ ( 上四分位 − 下四分位 ) 最小估计值 = 下四分位 − 1.5 ∗ ( 上四分位 − 下四分位 ) 最大估计值 = 上四分位 + 1.5 * (上四分位 - 下四分位)\\ 最小估计值=下四分位 - 1.5 * (上四分位 - 下四分位) 最大估计值=上四分位+1.5(上四分位下四分位)最小估计值=下四分位1.5(上四分位下四分位)

import matplotlib.pyplot as plt # 可视化工具
%matplotlib inline

# 箱线图查看异常值分布, 将可视化图片保存在fig_name中
def boxplot(data, fig_name):
    # 绘制每个属性的箱线图
    data_col = list(data.columns)
    
    # 连续画几个图片,图片大小为3*3,像素为300dpi
    plt.figure(figsize=(3, 3), dpi=300)
    # 子图调整,wspace和 hspace则分别表示水平方向上图像间的距离和垂直方向上图像间的距离
    plt.subplots_adjust(wspace=0.6)
    # 每个特征画一个箱线图
    for i, col_name in enumerate(data_col):
        plt.subplot(3, 5, i+1)
        # 画箱线图
        plt.boxplot(data[col_name], 
                    showmeans=True,   #是否显示均值,默认不显示;
                    meanprops={"markersize":1,"marker":"D","markeredgecolor":"#C54680"}, # 设置均值的属性,如点的大小、颜色等;
                    medianprops={"color":"#946279"},                                     # 设置中位数的属性,如线的类型、粗细等;
                    whiskerprops={"color":"#8E004D", "linewidth":0.4, 'linestyle':"--"}, # 设置垂直虚线的属性,如颜色、粗细、线的类型等;
                    flierprops={"markersize":0.4, 'markerfacecolor':'red'},              # 设置异常值属性,点的形状、填充色和边框色
                    ) 
        # 图名,fontdict:控制标题文本的外观,pad:标题与轴顶部的距离
        plt.title(col_name, fontdict={"size":5}, pad=2)
        # y方向刻度,fontsize:刻度值的大小,pad:刻度线与刻度值之间的距离
        plt.yticks(fontsize=4)
        plt.tick_params(pad=0.5)
        # x方向刻度,[]:不需要刻度
        plt.xticks([])
    plt.savefig(fig_name)
    plt.show()

boxplot(data, 'ml-vis5.pdf')

png

图2.4是箱线图的一个示例,可对照查看具体含义。

图2.4 箱线图示例

从输出结果看,数据中存在较多的异常值(图中上下边缘以外的空心小圆圈)。

使用四分位值筛选出箱线图中分布的异常值,并将这些数据视为噪声,其将被临界值取代,代码实现如下:

# 四分位处理异常值,select_dtypes返回排除数据类型为 ['object','bool'] 的列的 DataFrame
num_features=data.select_dtypes(exclude=['object','bool']).columns.tolist()

for feature in num_features:
    # 'CHAS'那列数据都是0
    if feature =='CHAS':
        continue
    
    Q1  = data[feature].quantile(q=0.25) # 下四分位
    Q3  = data[feature].quantile(q=0.75) # 上四分位
    
    IQR = Q3-Q1 
    top = Q3+1.5*IQR # 最大估计值
    bot = Q1-1.5*IQR # 最小估计值
    
    values=data[feature].values
    values[values > top] = top # 临界值取代噪声
    values[values < bot] = bot # 临界值取代噪声
    data[feature] = values

# 再次查看箱线图,异常值已被临界值替换(数据量较多或本身异常值较少时,箱线图展示会不容易体现出来)
boxplot(data, 'ml-vis6.pdf')

png

从输出结果看,经过异常值处理后,箱线图中异常值得到了改善。

2.5.1.3 数据集划分

由于本实验比较简单,将数据集划分为两份:训练集和测试集,不包括验证集。

具体代码如下:

import paddle

paddle.seed(10)

# 划分训练集和测试集
def train_test_split(X, y, train_percent=0.8):
    n = len(X)
    # 得到打乱的每行样本索引shuffled_indices
    shuffled_indices = paddle.randperm(n) # 返回一个数值在0到n-1、随机排列的1-D Tensor
    train_set_size = int(n*train_percent)
    train_indices = shuffled_indices[:train_set_size]
    test_indices = shuffled_indices[train_set_size:]

    # 以列表的形式返回一个字典中所有的值。
    X = X.values
    y = y.values

    X_train=X[train_indices]
    y_train = y[train_indices]
    
    X_test = X[test_indices]
    y_test = y[test_indices]

    return X_train, X_test, y_train, y_test 


X = data.drop(['MEDV'], axis=1)
y = data['MEDV']

# X_train的每一行是都是一个样本,shape[N,D]
X_train, X_test, y_train, y_test = train_test_split(X,y)

2.5.1.4 特征工程

为了消除纲量对数据特征之间影响,在模型训练前,需要对特征数据进行归一化处理,将数据缩放到[0, 1]区间内,使得不同特征之间具有可比性。

代码实现如下:

import paddle

X_train = paddle.to_tensor(X_train,dtype='float32')
X_test = paddle.to_tensor(X_test,dtype='float32')
y_train = paddle.to_tensor(y_train,dtype='float32')
y_test = paddle.to_tensor(y_test,dtype='float32')

X_min = paddle.min(X_train,axis=0)
X_max = paddle.max(X_train,axis=0)

# 最大最小值归一化,也可采用Z分数标准化
X_train = (X_train-X_min)/(X_max-X_min)
X_test  = (X_test-X_min)/(X_max-X_min)

# 训练集构造
train_dataset=(X_train,y_train)
# 测试集构造
test_dataset=(X_test,y_test)

2.5.2 模型构建

实例化一个线性回归模型,特征维度为 12:

from nndl.op import Linear

# 模型实例化
input_size = 12
model=Linear(input_size)

2.5.3 完善Runner类

模型定义好后,围绕模型需要配置损失函数、优化器、评估、测试等信息,以及模型相关的一些其他信息(如模型存储路径等)。

在本章中使用的Runner类为V1版本。其中训练过程通过直接求解解析解的方式得到模型参数,没有模型优化及计算损失函数过程,模型训练结束后保存模型参数。

训练配置中定义:

  • 训练环境,如GPU还是CPU,本案例不涉及;
  • 优化器,本案例不涉及;
  • 损失函数,本案例通过平方损失函数得到模型参数的解析解;
  • 评估指标,本案例利用MSE评估模型效果。

在测试集上使用MSE对模型性能进行评估。本案例利用飞桨框架提供的MSELoss API实现。

import paddle.nn as nn
mse_loss = nn.MSELoss()

具体实现如下:

import paddle
import os
from nndl.opitimizer import optimizer_lsm

class Runner(object):
    def __init__(self, model, optimizer, loss_fn, metric):
        # 优化器和损失函数为None,不再关注

        # 模型
        self.model=model
        # 评估指标
        self.metric = metric
        # 优化器
        self.optimizer = optimizer
    
    def train(self, dataset, reg_lambda, model_dir):
        X,y = dataset
        self.optimizer(self.model,X,y,reg_lambda)

        # 保存模型
        self.save_model(model_dir)
    
    # **kwargs为有关键字参数,必须传入字典形式的参数
    def evaluate(self, dataset, **kwargs):
        X,y = dataset

        y_pred = self.model(X)
        result = self.metric(y_pred, y)

        return result

    def predict(self, X, **kwargs):
        return self.model(X)
    
    def save_model(self, model_dir):
        if not os.path.exists(model_dir):
            os.makedirs(model_dir)
        
        params_saved_path = os.path.join(model_dir,'params.pdtensor')
        paddle.save(self.model.params,params_saved_path)

    def load_model(self, model_dir):
        params_saved_path = os.path.join(model_dir,'params.pdtensor')
        self.model.params=paddle.load(params_saved_path)

# 实例化Runner
runner = Runner(model, optimizer_lsm, loss_fn=None, mse_loss)

2.5.4 模型训练

在组装完成Runner之后,我们将开始进行模型训练、评估和测试。首先,我们先实例化Runner,然后开始进行装配训练环境,接下来就可以开始训练了,相关代码如下:

# 模型保存文件夹
saved_dir = '/home/aistudio/work/models'

# 启动训练
runner.train(train_dataset, reg_lambda=0, model_dir=saved_dir)

打印出训练得到的权重:

columns_list = data.columns.to_list()

w = runner.model.params['w'].tolist()
b = runner.model.params['b'].item()

for i in range(len(w)):
    print(columns_list[i],"weight:",w[i])

print("b:",b)

CRIM weight: -6.7268967628479
ZN weight: 1.28081214427948
INDUS weight: -0.4696650803089142
CHAS weight: 2.235346794128418
NOX weight: -7.0105814933776855
RM weight: 9.76220417022705
AGE weight: -0.8556219339370728
DIS weight: -9.265738487243652
RAD weight: 7.973038673400879
TAX weight: -4.365403175354004
PTRATIO weight: -7.105883598327637
LSTAT weight: -13.165120124816895
b: 32.12007522583008

从输出结果看,CRIM、PTRATIO等的权重为负数,表示该镇的人均犯罪率与房价负相关,学生与教师比例越大,房价越低。RAD和CHAS等为正,表示到径向公路的可达性指数越高,房价越高;临近Charles River房价高。

2.5.5 模型测试

加载训练好的模型参数,在测试集上得到模型的MSE指标。

# 加载模型权重
runner.load_model(saved_dir)

mse = runner.evaluate(test_dataset)
print('MSE:', mse.item())
MSE: 12.345974922180176

2.5.6 模型预测

使用Runnerload_model函数加载保存好的模型,使用predict进行模型预测,代码实现如下:

runner.load_model(saved_dir)
pred = runner.predict(X_test[:1])
print("真实房价:",y_test[:1].item())
print("预测的房价:",pred.item())
真实房价: 33.099998474121094
预测的房价: 33.04654312133789

从输出结果看,预测房价接近真实房价。

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绿洲213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值