目录
摘要
本周根据前三周学习的内容进行了代码实践,利用单层全连接神经网络训练了一个线性回归模型。通过这个实例,我加深了对模型训练三个步骤的理解,即函数建模、定义损失函数和寻找最佳参数,并且在这个过程中也更加熟悉了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=1∑N(yi−y^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}}
∂wjk∂L=N1i=1∑N2(yi−y^i)⋅(−1)⋅∂wjk∂y^i
因为预测值 y ^ i = x i T ⋅ w + b \displaystyle \hat{y}_i=x^T_i\cdot w+b y^i=xiT⋅w+b ,所以 ∂ y ^ i ∂ w j k = x k j \displaystyle \frac{\partial \hat{y}_{i}}{\partial w_{j k}}=x_{kj} ∂wjk∂y^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} ∂wjk∂L=−N2i=1∑N(yi−y^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=N1⋅errorT⋅inputs
同理,由于
∂
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
∂b∂L=−N2i=1∑N(yi−y^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=N1⋅∑error
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)以及内层循环控制等。第四,封装函数包括封装数据加载函数、封装网络结构和封装训练函数等等。通过这次的实践让我对理论理解得更加清晰,学习到了构建神经网络的一个标准流程。