【第四周】李宏毅机器学习笔记番外篇01:房价预测模型实战

摘要

本周根据前三周学习的内容进行了代码实践,利用单层全连接神经网络训练了一个线性回归模型。通过这个实例,我加深了对模型训练三个步骤的理解,即函数建模、定义损失函数和寻找最佳参数,并且在这个过程中也更加熟悉了Python的语法与网络搭建的步骤。

Abstract

This week, I engaged in coding practice based on the material learned over the previous three weeks, training a linear regression model using a single-layer fully connected neural network. Through this example, I have deepened my understanding of the three steps of model training, namely function modeling, defining loss functions, and finding optimal parameters. Additionally, I have become more familiar with Python syntax and network construction steps during this process.

1.房价预测模型实战

1.1.数据导入

数据集链接来源于kaggle:

数据集来源

首先,在python导入我们所需的package:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns #生成热力图
import numpy as np
import random

Pandas是一个开源的Python数据分析库,它提供了大量高效的数据处理工具和数据分析功能,是Python生态中最受欢迎的数据科学工具之一。Pandas的核心数据结构是两种灵活的数据表:Series(序列)和DataFrame(数据帧)。Series可以被视为一维数组,带标签的列表,而DataFrame则是二维表格型数据结构,类似于电子表格或SQL表,它包含带有行和列标签(即索引和列名)的数据。

matplotlib.pyplot是matplotlib库中的一个子模块,它提供了类似MATLAB的绘图接口,使得用户能够以更简洁、更交互的方式创建静态、动态、嵌入式图形。matplotlib是Python中最常用的绘图库,广泛应用于数据可视化领域。

将下好的数据集按照我们自己设定好的路径读入:

# 读入训练数据
data = pd.read_csv("D:\\PythonProjects\\housePrice\\datasets\\train.csv")
```D

观察一下我们的数据是否能够正确读取:
```python
print(data.head())

在这里插入图片描述
成功!

data.info()

在这里插入图片描述

观察读取进来的数据,可以看到RangeIndex共有1460条,也就是说明共有1460条数据。而列数有81列,说明除了房价之外起码有80个features。不过我们不需要把80维的数据全部提取出来作为特征,毕竟许多维度表示的信息是相似且非必要的,我们仅仅需要关注那些我们需要关注的信息就好了,这样能够大大降低特征表示的难度和计算机的计算量。

在Python的pandas库中,DataFrame是一个非常强大的数据结构,用于处理和分析表格型数据。当你创建一个DataFrame时,每个列都可以包含不同类型的元素,比如整数、浮点数、字符串等。Pandas会自动根据列中的数据为每一列分配一个数据类型,这个数据类型由.dtype属性表示。pandas在读取数据时默认将文件(如CSV、Excel等)的第一行作为列名,而不是将第一行的数据作为列名。当你使用诸如pd.read_csv(), pd.read_excel()等函数读取文件时,如果不特别指定,pandas会自动将文件的第一行解读为列标题(列名),从第二行开始视为数据记录。pandas提供了参数让你可以自定义这一行为,比如你可以通过header=None来告诉pandas不要将任何行作为列名,或者用header=integer_value来指定哪一行应该作为列名。

1.2.数据可视化

想要找到我们需要关注的特征就需要对数据进行可视化。首先提取数值化的列,使其能够在图上显示:

# 数值化的列名
columns_numerical = {i: data[i].dtype for i in data.columns if data[i].dtype != object}
for j in columns_numerical:    #遍历每一列
    plt.scatter(data[j], data['SalePrice'], label=j)    #label=j表示图例上显示的标签名
    plt.xlabel(j)
    plt.ylabel('SalePrice')
    plt.legend()    #显示图例
    plt.show()

生成图片过多,展示其中两张如下:
在这里插入图片描述

在这里插入图片描述
我们可以观察到一些变量和房价还是有相关性的,这样子我们就可以找到需要关注的变量。但是这样一个一个去看图效率很慢,所以我们可以分析各个特征的相关系数。

1.3.相关性分析

numeric_data = data.select_dtypes(include=[np.number]) #提取数值型数据
corr = numeric_data.corr() #求任意两个变量的相关系数矩阵
plt.figure(figsize=(12, 8))
sns.heatmap(corr, square=True, annot=False)
plt.show()

在这里插入图片描述

为了排除一些无关数值,我们只选择前15个与房价相关性最高的变量。


k = 15    # 指定变量个数
cols = corr.nlargest(k, 'SalePrice').index
corr_15 = corr.loc[cols, cols]
plt.figure(figsize=(12, 8))
sns.heatmap(corr_15, annot=True, square=True, fmt='.2f', annot_kws={'size': 10}, 
yticklabels=cols.values, xticklabels=cols.values)
plt.show()

结果如下:
在这里插入图片描述

其中有一些变量其实是类似的,例如1stFlrSF指的是一层的面积,这和房屋面积表示的信息相似,又比如GarageArea和GarageCar表示相似,等等。所以我们把些变量当中主要的部分留下来,次要或者重复的部分去除,留下的我们可以称为主要变量。其中房价是因变量的值,所以也选择留下,把这些变量组成一个字典:

main_factor = ['OverallQual', 'GrLivArea', 'TotalBsmtSF', 
'FullBath', 'TotRmsAbvGrd', 'YearBuilt', 'YearRemodAdd', 
'GarageYrBlt', 'MasVnrArea',  'SalePrice']
  • OverallQual: Overall material and finish quality
  • GrLivArea: Above grade (ground) living area square feet
  • TotalBsmtSF: Total square feet of basement area
  • FullBath: Full bathrooms above grade
  • TotRmsAbvGrd: Total rooms above grade (does not include bathrooms)
  • YearBuilt: Original construction date
  • YearRemodAdd: Remodel date
  • GarageYrBlt: Year garage was built
  • MasVnrArea: Masonry veneer area in square feet

绘制房价和主要变量之间的散点图:

data_copy = data[main_factor]
plt.figure(figsize=(12, 8))
sns.pairplot(data_copy, size=2.5)
plt.show()

在这里插入图片描述

1.4.数据预处理

提取主要变量形成训练数据,并检查是否正确:

train_data = data[main_factor]
print(train_data.head())

在这里插入图片描述
由于我们的数据已经是DataFrame格式,列名(也即列的标签)只是用来标识DataFrame中每列的数据类型或含义的,它们并不参与实际的数据处理或模型训练过程,因此不用考虑移除第一行。

1.4.1.缺失值处理

检查是否有缺失值:

data.isnull().sum()

在这里插入图片描述
可以看到有两项数据含缺失值,由于含缺失值的数据项少于10%,我们决定将含缺失值的行直接进行删除处理:

train_data = train_data.dropna(how='any')

删除后结果如下:

在这里插入图片描述]
在这里插入图片描述
可以看到删除了缺失的数据之后行数确实也下降了。

1.4.2.数据归一化

# 定义最大最小归一化函数
def min_max_scaling(column):
    min_val = column.min()
    max_val = column.max()
    scaled_column = (column - min_val) / (max_val - min_val)
    return scaled_column
# 对DataFrame的进行最大最小归一化
normalized_df = train_data.apply(min_max_scaling, axis=0)

结果如下:

在这里插入图片描述

也可以采用均值归一化:

# 均值归一化
def mean_normalization(column):
    mean = np.mean(column)
    std = np.std(column)
    std = 1.0 if std == 0 else std
    std_column = (column - mean) / std
    return std_column

1.3.数据集处理

1.3.1.数据集重排

# 对DataFrame的索引进行shuffled
shuffled_index = np.random.permutation(normalized_df.index)
# 使用新的索引重新排序DataFrame的行
shuffled_df = normalized_df.loc[shuffled_index]

在这里插入图片描述

1.3.2.数据集划分

数据集划分比例为训练集:测试集=8:2

# 数据集划分
ratio = 0.8
offset = int(shuffled_df.shape[0] * ratio) #shape属性是一个包含行和列的元组
training_data = shuffled_df[:offset]
testing_data = shuffled_df[offset:]

1.3.3.数据集分批

mini_batch = 10    #batch size
train_data1 = training_data    #复制一份数据进行处理
batch_num = int(train_data1.shape[0]/mini_batch)    #确定批次数
train_data_batch = []    #初始化列表
for i in range(batch_num):
    train_data_batch.append(train_data1[i*mini_batch:(i+1)*mini_batch])    
    #每十个数据加到列表中
train_data_batch.append(train_data1[batch_num*mini_batch:])    #处理剩下不满十个的数据
print('batch_num:', len(train_data_batch))
test_data1 = testing_data
batch_num = int(test_data1.shape[0]/mini_batch)
test_data_batch = []
for i in range(batch_num):
    test_data_batch.append(test_data1[i*mini_batch:(i+1)*mini_batch])
test_data_batch.append(test_data1[batch_num*mini_batch:])
print('test_batch_num:', len(test_data_batch))

在这里插入图片描述

1.4.模型设计与配置

在这里插入图片描述
该模型使用单层全连接网络结构。

1.4.1.前向计算

假设空间 y = x w + b y=xw+b y=xw+b,其中 x x x w w w均为向量。

def forward(self, input_data):
    y = input_data @ self.w + self.b  # “@”是矩阵乘法符号
    return y

1.4.2.初始化参数

使用标准正态分布随机生成w和b。

def __init__(self, input_num):
    # 第三个参数代表size,指明了生成self.w的维度,此时是将self.w设为列向量
    self.w = np.random.normal(0, 1, (input_num, 1)) 
    self.b = np.random.normal(0, 1)

1.4.3.计算损失函数

损失函数采用均方误差: L ( w , b ) = 1 N ∑ i = 1 N ( y i − y ^ i ) 2 \displaystyle L(w,b) = {1\over N}\sum_{i=1}^{N}{(y_i-\hat{y}_{i})^2} L(w,b)=N1i=1N(yiy^i)2

def loss(self, input_data, label):
    loss_list = (self.forward(input_data) - label) ** 2
    ls = sum(loss_list) / loss_list.shape[0]
    return ls

1.4.4.后向计算

后向计算的数学原理来自于链式法则:
∂ L ∂ w j k = 1 N ∑ i = 1 N 2 ( y i − y ^ i ) ⋅ ( − 1 ) ⋅ ∂ y ^ i ∂ w j k \displaystyle \frac{\partial L}{\partial w_{j k}}=\frac{1}{N} \sum_{i=1}^{N} 2\left(y_{i}-\hat{y}_{i}\right) \cdot\left(-1\right) \cdot \frac{\partial \hat{y}_{i}}{\partial w_{j k}} wjkL=N1i=1N2(yiy^i)(1)wjky^i

因为预测值 y ^ i = x i T ⋅ w + b \displaystyle \hat{y}_i=x^T_i\cdot w+b y^i=xiTw+b ,所以 ∂ y ^ i ∂ w j k = x k j \displaystyle \frac{\partial \hat{y}_{i}}{\partial w_{j k}}=x_{kj} wjky^i=xkj

于是 ∂ L ∂ w j k = − 2 N ∑ i = 1 N ( y i − y ^ i ) ⋅ x k j \displaystyle \frac{\partial L}{\partial w_{j k}}=-\frac{2}{N} \sum_{i=1}^{N} \left(y_{i}-\hat{y}_{i}\right) \cdot x_{kj} wjkL=N2i=1N(yiy^i)xkj

假设 e r r o r error error是将所有的 y ^ i \hat{y}_{i} y^i减去 y i y_{i} yi形成的一个列向量,因此,在代码中我们一般简化为:

g r a d i e n t w = 1 N ⋅ e r r o r T ⋅ i n p u t s \displaystyle gradient_w={1\over N} \cdot error^T⋅inputs gradientw=N1errorTinputs

同理,由于 ∂ L ∂ b = − 2 N ∑ i = 1 N ( y i − y ^ i ) ⋅ 1 \displaystyle \frac{\partial L}{\partial b}=-\frac{2}{N} \sum_{i=1}^{N} \left(y_{i}-\hat{y}_{i}\right)\cdot 1 bL=N2i=1N(yiy^i)1,所以可简化为:
g r a d i e n t b = 1 N ⋅ ∑ e r r o r \displaystyle gradient_b={1\over N} \cdot \sum {error} gradientb=N1error

def backward(self, input_data, label, lr):
    y = self.forward(input_data)
    error = y - label.reshape(1, -1) 
    gradient_w = (error.T @ input_data) / input_data.shape[0]
    gradient_w = gradient_w[0]
    gradient_b = np.sum(error) / input_data.shape[0]
    self.w -= lr * gradient_w.reshape(-1, 1)  # 注意这里的 reshape 操作
    self.b -= lr

1.5.封装各函数

1.5.1.封装数据集加载

def load_data(data_path,nomalize,ratio):
    # 训练数据
    data = pd.read_csv(data_path)

    # 数值化的列名
    main_factor = ['OverallQual', 'GrLivArea', 'TotalBsmtSF', 
    'FullBath', 'TotRmsAbvGrd', 'YearBuilt', 'YearRemodAdd', 
    'GarageYrBlt', 'MasVnrArea',  'SalePrice']
    train_data = data[main_factor]
    train_data = train_data.dropna(how='any')

    # 归一化
    normalized_df = train_data.apply(nomalize, axis=0)    # 对DataFrame的索引进行随机重排
    shuffled_index = np.random.permutation(normalized_df.index)
    shuffled_df = normalized_df.loc[shuffled_index]    # 使用新的索引重新排序DataFrame的行

    # 数据集划分
    ratio = 0.8
    offset = int(shuffled_df.shape[0] * ratio)  # shape属性是一个包含行和列的元组
    training_data = shuffled_df[:offset]
    testing_data = shuffled_df[offset:]

    # 数据集分批次
    mini_batch = 10  # batch size
    train_data1 = training_data    # 复制一份数据进行处理
    batch_num = int(train_data1.shape[0] / mini_batch)  # 确定批次数
    train_data_batch = []  # 初始化列表
    for i in range(batch_num):
        train_data_batch.append(train_data1[i * mini_batch:(i + 1) * mini_batch])  # 每十个数据加到列表中
    train_data_batch.append(train_data1[batch_num * mini_batch:])  # 处理剩下不满十个的数据
    print('train_batch_num:', len(train_data_batch))

    test_data1 = testing_data
    batch_num = int(test_data1.shape[0] / mini_batch)
    test_data_batch = []
    for i in range(batch_num):
        test_data_batch.append(test_data1[i * mini_batch:(i + 1) * mini_batch])
    test_data_batch.append(test_data1[batch_num * mini_batch:])
    print('test_batch_num:', len(test_data_batch))

    return train_data_batch, test_data_batch

1.5.2.封装网络结构

# 网络架构
class Network(object):
    # 按正态分布初始化参数w和b
    def __init__(self, input_num):
        self.w = np.random.normal(0, 1, (input_num, 1)) # 第三个参数指明了生成self.w的维度,此时是将self.w设为列向量
        self.b = np.random.normal(0, 1)

    # 前向计算
    def forward(self, input_data):
        y = input_data @ self.w + self.b  # “@”是矩阵乘法符号
        return y

    # 计算损失函数
    def loss(self, input_data, label):
        loss_list = (self.forward(input_data) - label) ** 2
        ls = sum(loss_list) / loss_list.shape[0]
        return ls

    # 后向计算
    def backward(self, input_data, label, lr):
        y = self.forward(input_data)
        error = y - label.reshape(1, -1)  # 将error转化为一个行向量,-1表示维度大小自行计算
        gradient_w = (error.T @ input_data) / input_data.shape[0]
        gradient_w = gradient_w[0]
        gradient_b = np.sum(error) / input_data.shape[0]
        self.w -= lr * gradient_w.reshape(-1, 1)  # 注意这里的 reshape 操作
        self.b -= lr

1.5.3.封装训练函数

# 训练函数
def train(epoch, lr, data_path, normalize, ratio):
    trainloss = []
    testloss = []
    net = Network(9)
    train_data_batch, test_data_batch = load_data(data_path, normalize, ratio)

    # 超参数设置
    for e in range(epoch):
        # 训练
        random.shuffle(train_data_batch)    # 每个epoch开始之前进行一次shuffle
        train_loss = []
        test_loss = []
        for data in train_data_batch:
            x = data.iloc[:, :9].to_numpy()
            # 前9个值是特征值
            label = data.iloc[:, -1].to_numpy().reshape(-1, 1)  # 转换为NumPy数组再reshape
            loss = net.loss(x, label)  # 得到训练的loss值
            train_loss.append(loss)
            net.backward(x, label, lr)  # 反向传播更新参数

        # 测试
        for data in test_data_batch:
            x = data.iloc[:, :9].to_numpy()  # 将DataFrame转换为NumPy数组
            label = data.iloc[:, -1].to_numpy().reshape(-1, 1)  # 转换为NumPy数组再reshape
            loss = net.loss(x, label)
            test_loss.append(loss)  # 得到测试的Loss值

        trainloss.append(sum(train_loss) / len(train_loss))
        testloss.append(sum(test_loss) / len(test_loss))
    print('best_train_loss:{},best_test_loss:{}'.format(min(trainloss), min(testloss)))

    # 绘制折线图
    plt.plot(trainloss, label='train')
    plt.plot(testloss, label='test')

    # 添加标题和标签
    plt.title('epoch&train_loss relationship')
    plt.xlabel('epoch')
    plt.ylabel('train_loss')

    # 显示图例
    plt.legend()

    # 显示图表
    plt.show()

在Pandas中,iloc 是一个非常有用的索引器,它用于基于整数位置进行切片和数据选择,不需要你知道实际的索引标签。iloc提供了一种快速而灵活的方式来访问DataFrame或Series中的元素。其基本语法如下:

 df.iloc[row_selection, column_selection]
 # row_selection: 用于指定行位置的整数、整数数组、切片对象或布尔数组。
 # column_selection: 用于指定列位置的整数、整数数组、列名列表、切片对象或布尔数组。

1.6.模型训练

按经验设置超参数(hypermeters),并观察结果:

epoch = 100
lr = 0.001  # 学习率
data_path = "D:\\PythonProjects\\housePrice\\datasets\\train.csv"
ratio = 0.8
train(epoch, lr, data_path, min_max_scaling, ratio)

在这里插入图片描述

由图可以看出,随着epoch的增大,我们loss值的图像经历了一个诡异的先减小后增大的过程,但是测试集的loss一直小于训练集的loss,因此应该不会是过拟合造成的。我们猜测可能是学习率的原因。如果学习率设置得过高,初期模型虽然能快速收敛,但当接近最优解时,过高的学习率可能导致模型在最小值附近震荡或越过局部最优解,从而使损失值开始上升。相反,如果学习率过低,模型可能在训练后期学习速度过慢,导致优化过程停滞。

在这里插入图片描述

我们将学习率手动改为0.0001之后发现得到了一个比较不错的结果。

总结

神经网络的搭建通常通常分为数据处理、模型设计、训练网络和封装函数四个部分。第一,数据处理包括数据读入、缺失值处理、归一化、数据shuffle、数据分批等等。第二,模型设计包括初始化、损失函数设计、前向计算和后向计算等等。第三,训练网络包括外层循环控制(epoch)以及内层循环控制等。第四,封装函数包括封装数据加载函数、封装网络结构和封装训练函数等等。通过这次的实践让我对理论理解得更加清晰,学习到了构建神经网络的一个标准流程。

参考文章1

参考文章2

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值