多层感知机(MLP)算法笔记

摘要:多层感知机(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、定义网络与超参数


  1. 网络定义: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)
  1. 损失函数:训练过程使用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()
  1. 优化器: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_Xtrain_Y划分为训练集和验证集。具体如下:

  • 将数据集分为 K K K等份 D 1 , D 2 , . . . , D K D_1,D_2,...,D_K D1,D2,...,DK, 令 i ← 1 i\gets 1 i1
  • 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_errtest_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]

以上就是全部的训练和测试过程。

都看到这里了,不如点个免费的赞吧~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值