摘要:多层感知机(MLP)是一个经典且有效的前馈-反向传播神经网络模型。本文将结合Kaggle提供的HousePrice数据集,解析使用Pytorch实现MLP的全过程。
0、背景介绍
0.1、问题情景
HousePrice数据集以csv文件的形式,包括MSSubClass、MSZoning等近80个属性和SalePrice这1个标签值。训练数据集和测试数据集均有1400余条。
也就是说,我们需要通过建立模型,学习SalePrice和这些属性之间的关系,然后预测测试集的SalePrice标签输出。
0.2、多层感知机
多层感知机(MLP)包含一个输入层、一个输出层,以及在它们之间的若干个隐层(Hidden Layer)。每个层都由多个神经元组成。如下图所示:
- 输入层接收输入数据,并将其传递给下一层;
- 隐层处理输入数据,并通过一些激活函数来产生输出;
- 输出层将隐层的输出转换为最终的输出结果。
每个神经元都与前一层的所有神经元连接,并具有加权矩阵 W W W。这些权重确定了每个输入对于神经元的重要性。每个神经元还具有一个偏置项,用于调整神经元的激活阈值。
1、数据集加载与预处理
由于数据集是csv文件,因此我们先使用pandas库加载,经过一些清洗和特征工程后,再转换为torch.tensor向量。
1.1、读取数据集
加载相关的依赖:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import torch
import torch.nn as nn
读取数据集:
train_data = pd.read_csv("/kaggle/input/kaggle-house-data/kaggle_house_train.csv")
test_data = pd.read_csv("/kaggle/input/kaggle-house-data/kaggle_house_test.csv")
print(train_data.shape)
print(test_data.shape)
注意修改正确的数据路径。输出如下。这表明训练集有 1460 1460 1460行,测试集有 1459 1459 1459行。训练集比测试集多一列是因为测试集没有SalePrice标签列。
(1460, 81)
(1459, 80)
1.2、数据EDA
输出几行数据。这里,取的是前4行和部分列:
print(train_data.iloc[0:4, [0,1,2,3,-3,-2,-1]])
输出如下:
Id MSSubClass MSZoning LotFrontage SaleType SaleCondition SalePrice
0 1 60 RL 65.0 WD Normal 208500
1 2 20 RL 80.0 WD Normal 181500
2 3 60 RL 68.0 WD Normal 223500
3 4 70 RL 60.0 WD Abnorml 140000
接下来,我们寻找和标签相关度最高的几个属性(设 N = 10 N=10 N=10)。方法非常多,可以参考以下代码:
N = 10
numeric_train_data = train_data.iloc[:, 0:].select_dtypes(include='number')
correlations = numeric_train_data.corr()["SalePrice"].abs().sort_values(ascending=False)
top_N_columns = correlations[1:N+1].index
top_N_correlations = correlations[1:N+1].values
# 创建一个子 DataFrame,仅包含与 "SalePrice" 相关性最高的属性列
top_N_df = numeric_train_data[top_N_columns]
# 绘制直方图
plt.figure() # (figsize=(9, 5))
plt.bar(top_N_columns, top_N_correlations)
plt.xlabel("Attributes")
plt.ylabel("Correlation with SalePrice")
plt.title(f"Top {N} Attributes with Highest Correlation to `SalePrice`")
plt.xticks(rotation=45)
plt.tight_layout()
plt.grid(True)
plt.show()
对于相关度较高的几个属性,我们绘制散点图,从而判断有无异常值,即离群点:
# 创建一个子 DataFrame,仅包含与 "SalePrice" 相关性最高的属性列
top_N_df = numeric_train_data[top_N_columns]
# 计算行数和列数
num_rows = len(top_N_columns) // 3 + (len(top_N_columns) % 3 > 0)
num_cols = min(len(top_N_columns), 3)
# 设置图形大小
fig, axs = plt.subplots(num_rows, num_cols, figsize=(15, num_rows * 4))
# 遍历每个属性列,绘制散点图并输出
for i, column in enumerate(top_N_columns):
row = i // num_cols
col = i % num_cols
ax = axs[row, col] if num_rows > 1 else axs[col]
ax.scatter(top_N_df[column], numeric_train_data["SalePrice"])
ax.set_xlabel(column)
ax.set_ylabel("SalePrice")
ax.set_title(f"Scatter Plot of {column} with SalePrice")
# 删除多余的子图
if num_rows * num_cols > len(top_N_columns):
for j in range(len(top_N_columns), num_rows * num_cols):
row = j // num_cols
col = j % num_cols
fig.delaxes(axs[row, col] if num_rows > 1 else axs[col])
plt.tight_layout()
plt.show()
1.3、剔除离群点
你可以根据自己的标准,观察上述数据中是否存在离群值,然后将其剔除。比如说:
- 面积很大的房子价格却很低
- 建筑年代很久远的房子价格却很高
这些都可以认为是离群点。
1.4、数据标准化
现在的数据还存在的问题是:
- 有的列数值范围很大,有的数值范围很小,若不加以统一,对模型的影响可能有很大差异;
- 有的属性是字符串,无法参与训练。
因此,我们需要将所有数值属性各自按照 N ( 0 , 1 ) N(0,1) N(0,1)标准化:即对这一列的每个数 x i x_i xi,令 x i ← x i − μ σ x_i\gets \frac{x_i-\mu}{\sigma} xi←σxi−μ;然后再对所有非数值的属性用独热编码重新表示。
代码如下:
# 合并数据集
# all_features 只包含全部的特征,不包含 SalePrice
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
# 对于每一列:如果是数,将所有缺失值填充为均值,且使用x:=(x-x_mean)/x_std标准化数据
def clean_numeric_features(df):
numeric_columns = df.iloc[:, 0:].select_dtypes(include='number')
df[numeric_columns.columns] = numeric_columns.fillna(numeric_columns.mean())
# 标准化指定列
for column in numeric_columns.columns:
mean = df[column].mean()
std = df[column].std()
df[column] = (df[column] - mean) / std
#print(column)
return df
# 对于每一列:如果是文本,则使用独热编码扩展特征 设dummy_na=1
def clean_distribute_features(df):
df = pd.get_dummies(df, dummy_na=1)
return df
all_features = clean_numeric_features(all_features)
all_features = clean_distribute_features(all_features)
接下来,将数据集转换为torch向量:
# 将数据转化为torch向量形式
# 由此数据集预处理完成,共构造出train_X, test_X, train_Y三个数据
train_X = torch.tensor(all_features.head(train_data.shape[0]).astype('float32').values)
test_X = torch.tensor(all_features.tail(test_data.shape[0]).astype('float32').values)
train_Y = torch.tensor(train_data.iloc[:, -1].astype('float32').values)
train_Y = train_Y.reshape((-1,1)) # train_Y要调整为1列的向量
print(train_X.shape)
print(test_X.shape)
print(train_Y.shape)
2、定义网络与超参数
- 网络定义:MLP(多层感知机)模型,使用显式参数初始化
# v1.0-MLP模型
hidden_size = 256
net = nn.Sequential(
# nn.Flatten(),
nn.Linear(train_X.shape[1], hidden_size),
nn.ReLU(),
nn.Linear(hidden_size, 1)
)
# 模型参数初始化
for param in net.parameters():
nn.init.normal_(param,mean=0,std=0.01)
- 损失函数:训练过程使用
nn.MSELoss()
,进一步输出使用log_rmse()
:这是因为取对数后更加平缓,对预测偏低和预测偏高的敏感度相同;
# 损失函数
loss = nn.MSELoss()
# 对数RMSE
def log_rmse(net, features, labels):
with torch.no_grad():
#将小于1的值设成1,使的取对数时数值更稳定
clipped_preds=torch.max(net(features),torch.tensor(1.0))
rmse=torch.sqrt(loss(clipped_preds.log(),labels.log()))
return rmse.item()
- 优化器:Adam算法
# 学习率与优化函数
lr = 0.1
weight_decay = 300
optimizer = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=weight_decay)
3、模型训练
3.1、训练过程
我们使用
K
K
K折交叉验证法,将原有的train_X
和train_Y
划分为训练集和验证集。具体如下:
- 将数据集分为 K K K等份 D 1 , D 2 , . . . , D K D_1,D_2,...,D_K D1,D2,...,DK, 令 i ← 1 i\gets 1 i←1;
- 让 D i D_i Di充当验证集,剩余部分充当训练集;
- 每过 20 20 20 epochs,就让 D i + 1 D_{i+1} Di+1充当验证集,剩余部分充当训练集,且 i ← ( i + 1 ) % K i\gets (i+1) \%K i←(i+1)%K。
代码如下:
# K fold 迭代器的实现
def k_fold_iter(data, K):
# 计算每折的样本数量
fold_size = len(data) // K
# (暂时不)对数据进行随机打乱
# shuffled_data = data[torch.randperm(len(data))]
shuffled_data = data
# 进行 K 折交叉验证
# for k in range(K):
k = 0
while True:
start_idx = (k % K) * fold_size
end_idx = ((k % K) + 1) * fold_size
# 创建训练集和验证集
validation_set = shuffled_data[start_idx:end_idx]
train_set = torch.cat([shuffled_data[:start_idx], shuffled_data[end_idx:]], dim=0)
yield train_set, validation_set
k += 1
使用批量训练,定义batch_size初值为64:
epochs = 100
batch_size = 64
train_err_list = []
test_err_list = []
# best_net = net # 用于保存最优模型
best_err = 1.0 # 用于保存最小误差
best_epoch = 0 # 最优的那轮
# K fold 数据预处理工作
K = 5
iter_X = k_fold_iter(train_X, K)
iter_Y = k_fold_iter(train_Y, K)
# 进行第一轮迭代
train_X, val_X = next(iter_X)
train_Y, val_Y = next(iter_Y)
for epoch in range(epochs):
# 每批次训练过程
train_err = 0
net.train()
for batch in range(train_X.size(0) // batch_size):
# 获取当前批次的训练样本和标签
start_idx = batch * batch_size
end_idx = start_idx + batch_size
batch_X = train_X[start_idx:end_idx]
batch_Y = train_Y[start_idx:end_idx]
# 前向传播
# print(batch_X)
outputs = net(batch_X)
loss_val = loss(outputs, batch_Y)
# 反向传播和优化
optimizer.zero_grad()
loss_val.backward()
optimizer.step()
# 记录损失
train_err = log_rmse(net, batch_X, batch_Y) # abs_loss(torch.flatten(outputs), batch_Y)
train_err_list.append(train_err)
with torch.no_grad():
test_err = log_rmse(net, val_X, val_Y) # abs_loss(torch.flatten(net(val_X)), val_Y)
test_err_list.append(test_err)
if test_err < best_err:
best_err = test_err
# 保存最优模型
torch.save(net, "best_net.pth") # best_net = net
best_epoch = epoch + 1
if (epoch + 1) % 10 == 0:
print(f'At epoch {epoch+1}: train_err {train_err}, test_err {test_err}')
if (epoch + 1) % 20 == 0:
# 进行数据迭代
print("[INFO] K fold iterred.")
train_X, val_X = next(iter_X)
train_Y, val_Y = next(iter_Y)
# 最优模型
print('[INFO] best net is at epoch:', best_epoch, 'and best_test_err:', best_err)
best_net = torch.load("best_net.pth")
在上面的过程中,我们使用了5折交叉验证策略,每训练一轮会输出一次loss,同时,全部训练结束后,将会保存效果最优的那一次epoch的模型:
At epoch 10: train_err 0.11013912409543991, test_err 0.1301828771829605
At epoch 20: train_err 0.10268591344356537, test_err 0.12380051612854004
[INFO] K fold iterred.
At epoch 30: train_err 0.09366506338119507, test_err 0.11840552091598511
At epoch 40: train_err 0.09065182507038116, test_err 0.12025512754917145
[INFO] K fold iterred.
At epoch 50: train_err 0.08608008176088333, test_err 0.1177910789847374
At epoch 60: train_err 0.08290081471204758, test_err 0.12124277651309967
[INFO] K fold iterred.
At epoch 70: train_err 0.08388715982437134, test_err 0.09968393296003342
At epoch 80: train_err 0.08197204768657684, test_err 0.1020469143986702
[INFO] K fold iterred.
At epoch 90: train_err 0.07416059076786041, test_err 0.09930382668972015
At epoch 100: train_err 0.07139597088098526, test_err 0.10214640200138092
[INFO] K fold iterred.
[INFO] best net is at epoch: 81 and best_test_err: 0.0853213518857956
3.2、可视化损失
将训练过程和验证过程的误差train_err
,test_err
作可视化绘图:
# 创建一个新的图形,并设置子图的布局为1行2列
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))
# 绘制第一张子图(训练误差)
axes[0].plot(train_err_list, label='Train Error')
axes[0].set_xlabel('Training Process(batch)')
axes[0].set_ylabel('Error')
axes[0].set_title('Train Error vs Epoch')
axes[0].legend()
# 绘制第二张子图(测试误差)
axes[1].plot(test_err_list, label='Val Error')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Error')
axes[1].set_title('Validation Error vs Epoch')
axes[1].legend()
# 调整子图之间的间距
plt.tight_layout()
# 显示图形
plt.show()
3.3、可视化预测对比
- 将训练后的最优模型提出来
- 对比预测数据
output=net(val_X)
和源数据val_Y
的差异
# 计算预测输出
with torch.no_grad():
val_outputs = torch.flatten(best_net(val_X))
original_labels = val_Y
# print(test_outputs)
# print(original_labels)
# 绘图
x = range(len(val_outputs))
plt.figure(figsize=(8, 5))
plt.plot(x, val_outputs, linestyle='-', color='blue', label='Validation Outputs')
plt.plot(x, original_labels, linestyle='-', color='#ffa090', label='Original Labels')
plt.xlabel('Sample Index')
plt.ylabel('SalePrice')
plt.title('Comparison between `Test Outputs` and `Original Labels`')
plt.legend()
plt.grid(True)
plt.show()
从这个图里我们可以看出,预测的标签和其真实值几乎完全重合,效果还是比较可观的。
4、预测和写回
将test_data
输入模型,计算出预测的数据,写入上交的csv文件。
# 计算最终输出
with torch.no_grad():
test_outputs = torch.flatten(best_net(test_X))
# print(test_outputs.shape)
df = pd.DataFrame(test_outputs.numpy(), columns=["SalePrice"])
final_outputs = pd.concat([test_data['Id'], df], axis = 1)
print(final_outputs)
final_outputs.to_csv('submission.csv', index=False)
我们此次预测的数据为:
Id SalePrice
0 1461 113083.554688
1 1462 163210.953125
2 1463 183047.015625
3 1464 193696.359375
4 1465 183477.265625
... ... ...
1454 2915 84996.304688
1455 2916 78669.164062
1456 2917 169626.828125
1457 2918 116193.351562
1458 2919 222014.796875
[1459 rows x 2 columns]
以上就是全部的训练和测试过程。
⭐都看到这里了,不如点个免费的赞吧~