矿物分类实战:从传统机器学习到深度学习(含PyTorch实现)

引言

在前序文章中,已经完成矿物数据集预处理、缺失值填充,并训练了逻辑回归、随机森林、SVM、AdaBoost、高斯朴素贝叶斯、XGBoost六种传统机器学习模型。本文在此基础上引入深度学习,基于PyTorch搭建全连接神经网络(Net)一维卷积神经网络(ConvNet),尝试提升矿物分类效果。文章重点讲解全连接神经网络实现,一维卷积网络侧重说明与前者的差异。


一、数据准备(简要回顾)

1.1 代码

复制代码
import pandas as pd
import warnings
warnings.filterwarnings("ignore")

datas1 = pd.read_csv("data/训练数据集_均值填充.csv")
datas2 = pd.read_csv("data/测试数据集_均值填充.csv")

x_train = datas1.iloc[:, :-1]   # 特征(共13个)
y_train = datas1.iloc[:, -1]    # 标签(0,1,2,3)
x_test = datas2.iloc[:, :-1]
y_test = datas2.iloc[:, -1]

1.2 逐行讲解

  • pd.read_csv :读取CSV文件为DataFrame。训练集和测试集是预先划分好 的两个独立文件,避免了在代码中再做train_test_split
  • iloc[:, :-1]iloc是按位置索引。第一个:表示取所有行;:-1表示取除最后一列外的所有列(即特征列)。
  • iloc[:, -1]:取最后一列,即标签列。标签值为0、1、2、3,对应4种矿物类别。
  • warnings.filterwarnings("ignore"):屏蔽运行时警告(如版本弃用提示),让输出更干净,不影响实际计算。

1.3 为什么数据已经标准化?

神经网络对输入尺度非常敏感。如果某个特征范围是01000,另一个是01,梯度更新时大尺度特征会主导权重更新,导致小尺度特征学不到东西。所以在前序预处理中已经做了标准化(Z-Score)x = (x - mean) / std,使每个特征均值为0、方差为1,保证各特征对模型的贡献处于同一量级。


二、传统机器学习模型(回顾)

模型 核心思想 适用场景
逻辑回归 线性组合 + Sigmoid,输出类别概率 线性可分、特征少的基线
随机森林 多棵决策树投票,Bagging集成 抗过拟合、特征重要性可解释
SVM(多项式核) 找最大间隔超平面,核函数映射高维 小样本、非线性边界
AdaBoost 串行提升,每轮聚焦错分样本 弱分类器集成
高斯朴素贝叶斯 特征条件独立假设 + 贝叶斯公式 文本/小样本、速度快
XGBoost 梯度提升树,正则化 + 二阶泰勒展开 表格数据竞赛常胜模型

这些模型在中小规模表格数据上通常已经很强,尤其是随机森林和XGBoost这类集成方法。引入神经网络的目的是探索更复杂的特征交叉组合,但并不保证一定超越。


三、全连接神经网络(Net)

3.1 什么是全连接层?

全连接层(Fully Connected Layer,也称线性层)的数学公式:

y = xW\^T + b

其中:

  • x:输入向量,形状 (batch, in_features)
  • W:权重矩阵,形状 (out_features, in_features),是可学习参数
  • b:偏置向量,形状 (out_features,),是可学习参数
  • y:输出向量,形状 (batch, out_features)

nn.Linear(13, 32) 就是创建一个权重矩阵 W 形状为 (32, 13)、偏置 b 形状为 (32,) 的线性变换。每个输出神经元都和所有输入神经元相连,所以叫"全连接"。

3.2 模型定义详解

复制代码
import torch
from torch import nn

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(13, 32)   # 输入13特征 → 32神经元
        self.fc2 = nn.Linear(32, 64)   # 32 → 64
        self.fc3 = nn.Linear(64, 4)    # 64 → 4,输出4分类logits

    def forward(self, x):
        x = torch.relu(self.fc1.forward(x))
        x = torch.relu(self.fc2.forward(x))
        x = self.fc3.forward(x)        # 输出原始得分,不做激活
        return x

逐行解析

  • class Net(nn.Module):所有PyTorch模型都继承nn.Module,这是PyTorch的基类,提供参数管理、.to(device).train()/.eval()等能力。
  • super(Net, self).__init__():调用父类构造函数,必须写,否则参数注册机制失效。
  • self.fc1/fc2/fc3:在__init__中定义的nn.Module子模块会被自动注册,其参数会被model.parameters()收集,供优化器更新。
  • forward(self, x):定义前向传播逻辑。调用model(x)时实际执行的就是forward
  • torch.relu(x)ReLU(x) = max(0, x),负数置零、正数保留。

网络结构数据流

复制代码
输入(13) → fc1线性变换 → ReLU → (32)
       → fc2线性变换 → ReLU → (64)
       → fc3线性变换 → (4) 输出logits

3.3 为什么用ReLU而不是Sigmoid?

激活函数 公式 问题
Sigmoid 1/(1+e^-x) 输出范围(0,1),深层网络梯度消失(两端导数趋近0);输出非零均值,影响梯度方向
ReLU max(0,x) 正区间梯度恒为1,缓解梯度消失;计算极快(只需比较);但负区间"死亡ReLU"问题

原代码注释了Sigmoid版本,采用ReLU是因为:训练更快、收敛更稳定、深层网络不易梯度消失

3.4 为什么最后一层不加激活函数?

因为损失函数用的是nn.CrossEntropyLoss(),它的内部实现是:

复制代码
CrossEntropyLoss = LogSoftmax + NLLLoss

具体来说:

  1. 先对logits做Softmaxp_i = e^(z_i) / Σe^(z_j),转成概率分布
  2. 再取loglog(p_i)
  3. 最后用负对数似然NLLLossloss = -log(p_true_class)

如果模型最后一层再加Softmax,等于做了两次Softmax,数值会出问题。所以模型输出原始logits即可。

3.5 数据转为Tensor

复制代码
X_train = torch.tensor(x_train.values, dtype=torch.float32)
Y_train = torch.tensor(y_train.values)
X_test = torch.tensor(x_test.values, dtype=torch.float32)
Y_test = torch.tensor(y_test.values)
  • x_train.values:DataFrame转NumPy数组
  • dtype=torch.float32:特征用32位浮点数,这是深度学习的标准精度(比float64省显存、计算快)
  • 标签Y_train默认推断为int64(即long类型),这是CrossEntropyLoss的硬性要求------标签必须是torch.long,否则报错。

3.6 损失函数与优化器

复制代码
model = Net()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

CrossEntropyLoss多分类交叉熵

  • 单个样本损失:L = -log(p_y),其中p_y是真实类别的预测概率
  • 真实类别概率越接近1,损失越小;越接近0,损失越大
  • 对整个batch取平均

Adam优化器

  • 结合了Momentum(动量,积累历史梯度方向)和RMSprop(自适应学习率,每个参数单独调整步长)
  • lr=0.01:初始学习率。全连接网络用0.01,CNN用0.001(CNN更敏感,需要更小步长)
  • model.parameters():收集模型中所有可学习参数(W和b),交给优化器更新

3.7 评估函数详解

复制代码
def evaluate_model(model, X_data, Y_data, train_or_test):
    size = len(X_data)
    with torch.no_grad():
        preds = model(X_data)
        correct = (preds.argmax(1) == Y_data).type(torch.float).sum().item()
        correct /= size
        print(f"{train_or_test}:\tAccuracy:{(100*correct)}%")
    return correct

逐行拆解:

  • with torch.no_grad():关闭梯度计算。评估时不需要反向传播,关闭梯度可以省显存、加速。这是评估/推理阶段的标准写法。
  • preds = model(X_data):前向传播,输出形状(batch, 4),每行是4个类别的logits。
  • preds.argmax(1)dim=1表示按列方向取最大值索引,即每行取分数最高的类别编号,输出形状(batch,)
  • (preds.argmax(1) == Y_data):布尔张量,预测对为True,错为False。
  • .type(torch.float):布尔转浮点(True→1.0,False→0.0),方便求和。
  • .sum().item():求和得到正确样本数,.item()把单元素张量转成Python标量。
  • correct /= size:除以总样本数,得到0~1的准确率。

3.8 训练循环------深度学习的核心四步

复制代码
epochs = 6000
accs = []

for epoch in range(epochs):
    outputs = model.forward(X_train)       # ① 前向传播
    loss = criterion(outputs, Y_train)     # ② 计算损失

    optimizer.zero_grad()                  # ③ 梯度清零
    loss.backward()                        # ④ 反向传播
    optimizer.step()                       # ⑤ 参数更新

    if (epoch + 1) % 100 == 0:
        print(f"Epoch {epoch+1}/{epochs}, Loss {loss.item():.4f}")
        train_acc = evaluate_model(model, X_train, Y_train, 'train')
        test_acc = evaluate_model(model, X_test, Y_test, 'test')
        accs.append(test_acc * 100)

五步训练法是所有PyTorch训练的标准模板

步骤 代码 作用 不写会怎样
① 前向 model(X) 计算预测值 没有预测就没法算损失
② 损失 criterion(out, y) 量化预测和真实的差距 没有优化目标
③ 清零 optimizer.zero_grad() 清空上一轮累积的梯度 梯度会累加,更新方向错误
④ 反向 loss.backward() 自动求导,计算每个参数的梯度 参数不知道该往哪个方向更新
⑤ 更新 optimizer.step() 按梯度和学习率更新参数 学了等于没学,参数不变

为什么要zero_grad() PyTorch的.backward()默认是累加梯度而不是覆盖。这是为了支持RNN等需要多次反向传播的场景。如果不清零,第2轮的梯度 = 第1轮梯度 + 第2轮梯度,越积越大,学习率等效越来越大,训练直接崩掉。

训练6000轮、每100轮评估一次

  • 小数据集(矿物数据通常几百~几千条)不需要mini-batch,直接全量训练(Full Batch)
  • 每100轮记录一次测试集准确率,最后取max(accs)作为最佳结果
  • 注意:取max可能有"挑选最佳epoch"的乐观偏差,严格来说应该用验证集选epoch再在测试集测一次

四、卷积神经网络(ConvNet)

4.1 一维卷积是什么?

图像用的是二维卷积(Conv2d),在高和宽两个维度滑动卷积核。对于表格数据,我们把13个特征排成一个长度为13的一维序列,用一维卷积(Conv1d)在这个序列上滑动,提取相邻特征间的局部模式。

一维卷积的数学操作:卷积核在序列上滑动,每个位置做对应元素相乘再求和,输出一个值。

4.2 Conv1d参数详解

复制代码
self.conv1 = nn.Conv1d(in_channels=1, out_channels=16, kernel_size=3, padding=1)
  • in_channels=1:输入通道数。表格数据每个样本只有1条特征序列,所以是1。(图像是3通道RGB,对应in_channels=3)
  • out_channels=16:输出通道数,也是卷积核的个数。每个卷积核学一种局部模式,16个卷积核能学16种不同模式。
  • kernel_size=3:卷积核大小,每次看相邻的3个特征。
  • padding=1:在序列两端各补1个0。输入长度13,卷积核3,padding=1时输出长度 = (13 + 2*1 - 3)/1 + 1 = 13,长度不变

三层卷积的通道变化

复制代码
(batch, 1, 13)  → conv1 → (batch, 16, 13)
                → conv2 → (batch, 32, 13)
                → conv3 → (batch, 64, 13)

4.3 模型定义详解

复制代码
class ConvNet(nn.Module):
    def __init__(self, num_features, hidden_size, num_classes):
        super(ConvNet, self).__init__()
        self.conv1 = nn.Conv1d(1, 16, 3, padding=1)
        self.conv2 = nn.Conv1d(16, 32, 3, padding=1)
        self.conv3 = nn.Conv1d(32, 64, 3, padding=1)
        self.relu = nn.ReLU()
        self.fc = nn.Linear(64, num_classes)

    def forward(self, x):
        x = x.unsqueeze(1)          # 关键:增加通道维度
        x = torch.relu(self.conv1(x))
        x = torch.relu(self.conv2(x))
        x = torch.relu(self.conv3(x))
        x = x.mean(dim=2)           # 全局平均池化
        x = self.fc(x)
        return x

关键操作逐行讲

  1. x.unsqueeze(1)
    • 输入x形状是(batch, 13)
    • Conv1d要求输入形状是(batch, channels, length)
    • unsqueeze(1)在第1维(0-based)插入一个维度,变成(batch, 1, 13)
    • 不加这一步会直接报错:维度不匹配
  2. 三层卷积 + ReLU
    • 每层卷积后接ReLU引入非线性
    • 通道数从1→16→32→64逐步增加,学更抽象的特征
    • 因为padding=1,序列长度始终保持13不变
  3. x.mean(dim=2)------全局平均池化(GAP)
    • 此时x形状是(batch, 64, 13)
    • dim=2表示在第2维(长度维)上求平均
    • 每个通道的13个值取平均,得到(batch, 64)
    • 作用:把变长/固定长度的序列压缩成固定长度的特征向量,同时减少参数量、防止过拟合
    • 替代方案:也可以用Flatten展平,但参数量更大(64*13=832 vs 64)
  4. 全连接分类(batch, 64)fc(batch, 4),输出4类logits。

4.4 ConvNet vs Net 核心区别总结

对比项 全连接Net 卷积ConvNet
核心操作 矩阵乘法,每个输出连接所有输入 卷积核滑动,参数共享,只看局部
参数量 大(13×32+32×64+64×4=2528) 小(卷积核参数共享,三层仅约2300)
特征视角 全局特征组合 局部相邻特征模式
输入形状 (batch, 13) (batch, 1, 13)需加通道维
归纳偏置 无(暴力学所有组合) 局部性、平移不变性
适合数据 表格、低维特征 序列、信号、图像

注意:__init__中的hidden_size参数在forward中完全没用到,是冗余参数,可以删除。这里保留是为了和原代码一致。

4.5 训练流程差异

复制代码
model = ConvNet(13, hidden_size=10, num_classes=4)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)  # 学习率更小

num_epochs = 7000  # 训练轮数更多
  • 学习率0.001:CNN比全连接网络更敏感,大学习率容易震荡不收敛,所以调小10倍
  • 训练7000轮:CNN收敛更慢,需要更多轮次
  • 其余训练五步(前向→损失→清零→反向→更新)和评估逻辑完全复用

五、性能比较汇总

复制代码
all_models = [
    ("逻辑回归", float(LR_result['lr测试集正确率'])),
    ("随机森林", float(RF_result['rf测试集正确率'])),
    ("SVM", float(SVM_result['svm测试集正确率'])),
    ("AdaBoost", float(ABF_result['abf测试集正确率'])),
    ("高斯朴素贝叶斯", float(GNB_result['gnb测试集正确率'])),
    ("XGBoost", float(XGBoost_result['xgb测试集正确率'])),
    ("全连接神经网络", float(net_result['acc'])),
    ("卷积神经网络", float(cnn_result['acc']))
]

best_name, best_score = max(all_models, key=lambda t: t[1])
print("正确率最高模型\t", best_name, best_score)
  • all_models是一个元组列表,每个元组是(模型名, 准确率)
  • max(..., key=lambda t: t[1]):按元组第2个元素(准确率)取最大值
  • 返回(best_name, best_score),即最优模型名称和分数
  • float(...):确保所有值都是Python float类型,避免numpy/tensor类型混用导致比较异常

六、总结与延伸思考

6.1 本文核心结论

  1. 全连接网络:结构简单,通过多层线性变换+ReLU学习非线性特征组合,适合表格数据
  2. 一维CNN:把特征当序列,用卷积核捕捉相邻特征的局部模式,参数共享更高效
  3. 中小规模表格数据:传统集成方法(随机森林、XGBoost)往往已经很强,深度学习不一定显著超越,但值得尝试

6.2 可以进一步优化的方向

  • Dropout正则化 :在全连接层之间加nn.Dropout(0.5),随机失活神经元,防止过拟合
  • Batch Normalization :加nn.BatchNorm1d,稳定训练、允许更大学习率
  • 学习率调度 :用torch.optim.lr_scheduler动态衰减学习率,后期更精细
  • Mini-Batch训练 :数据量大时用DataLoader分batch,而不是全量一次喂入
  • 交叉验证:用K-Fold验证模型稳定性,而不是单次划分
  • 早停(Early Stopping):验证集损失连续N轮不下降就停止,避免过拟合
  • 取max的问题:本文取测试集准确率最大值作为结果,存在"挑选最佳epoch"的乐观偏差。严格做法是用验证集选最优epoch,再在测试集上测一次

6.3 关键知识点速查表

知识点 核心一句话
nn.Linear y = xW^T + b,全连接线性变换
ReLU max(0,x),缓解梯度消失、计算快
CrossEntropyLoss 内部=LogSoftmax+NLLLoss,模型输出logits即可
torch.no_grad() 评估时关闭梯度,省显存加速
argmax(1) 按行取最大类别索引
训练五步 前向→损失→清零→反向→更新
zero_grad() 必须清零,否则梯度累加
unsqueeze(1) 给Conv1d加通道维度
Conv1d 一维序列上滑动卷积核,参数共享
mean(dim=2) 全局平均池化,压缩序列维

完整代码参考文中片段,数据集与缺失值处理见前文,欢迎交流。

相关推荐
咖啡忍者41 分钟前
【SAP】100小时学会SAP-5生产计划PP
笔记
春风解人意1 小时前
从零开始学习嵌入式P35----数据库
数据库·嵌入式硬件·学习
sunoo-2291 小时前
【网络编程 + 数据库】select 与 epoll 核心区别详解 + SQLite 入门到 C 接口全攻略
linux·网络·数据库·vscode·学习·sqlite
小白的后端世界1 小时前
跨境电商数据分析与 AI Agent 自动化:从指标体系到决策闭环
人工智能·深度学习·数据分析·自动化
leihefeng1 小时前
邮件文本分类:CountVectorizer / TF-IDF + 朴素贝叶斯
python·分类·数据挖掘·tf-idf·sklearn
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis MyBatis 简单开发 阅读笔记 39
spring boot·笔记·mybatis
LUSTER凌云光1 小时前
工业AI视觉检测系统设计:传统视觉与深度学习如何融合?
人工智能·深度学习·视觉检测
hans汉斯1 小时前
【计算机科学与应用】基于联合熵驱动改进麻雀搜索优化VMD的DAS信号去噪方法
深度学习·算法·yolo·软件工程·汉斯出版社
xieliyu.1 小时前
计算机网络:Fiddler 抓包工具使用教程 + HTTP 协议报文格式详解
java·笔记·计算机网络·测试工具·http·java-ee·fiddler