文章目录
- 一、前言
- 二、数据集说明
- 三、环境依赖
- 四、数据加载
- [五、全连接神经网络(BP 神经网络)](#五、全连接神经网络(BP 神经网络))
-
- [5.1 网络结构](#5.1 网络结构)
- [5.2 核心原理](#5.2 核心原理)
- [5.3 代码实现](#5.3 代码实现)
- [5.4 数据转张量](#5.4 数据转张量)
- [5.5 模型评估函数](#5.5 模型评估函数)
- [5.6 训练过程](#5.6 训练过程)
- [六、卷积神经网络(1D CNN)](#六、卷积神经网络(1D CNN))
-
- [6.1 为什么表格数据可以用 CNN?](#6.1 为什么表格数据可以用 CNN?)
- [6.2 网络结构](#6.2 网络结构)
- [6.3 核心原理](#6.3 核心原理)
- [6.4 代码实现](#6.4 代码实现)
- [6.5 训练过程](#6.5 训练过程)
- 七、结果输出
- 八、两种模型对比分析
- 九、改进建议
-
- [9.1 数据标准化](#9.1 数据标准化)
- [9.2 改用 ReLU 激活函数](#9.2 改用 ReLU 激活函数)
- [9.3 设置随机种子](#9.3 设置随机种子)
- [9.4 早停(Early Stopping)](#9.4 早停(Early Stopping))
- [9.5 学习率调度](#9.5 学习率调度)
- [9.6 更丰富的评估指标](#9.6 更丰富的评估指标)
- 十、完整代码汇总
- 十一、总结
一、前言
在地质勘探与矿物识别领域,传统的人工鉴定方法依赖专家经验,效率低且主观性强。随着机器学习技术的发展,利用矿物的物理化学特征(如密度、硬度、折射率、化学成分含量等)进行自动分类已成为一种高效可行的方案。
本文将基于一份矿物特征数据集,使用 PyTorch 框架分别搭建:
- 全连接神经网络(BP 神经网络)
- 一维卷积神经网络(1D CNN)
两种模型对矿物类型进行四分类任务,并对比它们的分类效果。文章包含完整的原理讲解、代码实现与结果分析,适合机器学习入门读者参考。
二、数据集说明
本实验使用经过平均值填充处理后的矿物数据集,分为训练集和测试集两个文件:
| 文件 | 说明 |
|---|---|
2_训练数据集_平均值填充.xlsx |
训练集 |
2_测试数据集_平均值填充.xlsx |
测试集 |
数据结构如下:
- 第 1 列:标签列,矿物类型(共 4 类,记为 0、1、2、3)
- 第 2 列及以后 :特征列,共 13 维矿物特征
平均值填充是处理缺失值的常用方法:用该特征列的均值替换缺失值,简单且不会改变数据整体分布。
三、环境依赖
bash
pip install pandas scikit-learn torch openpyxl
| 库 | 用途 |
|---|---|
pandas |
读取 Excel 数据、数据处理 |
scikit-learn |
模型评估指标(metrics) |
torch |
PyTorch 深度学习框架 |
openpyxl |
pandas 读取 .xlsx 文件的引擎 |
四、数据加载
python
import pandas as pd
from sklearn import metrics
train_data = pd.read_excel(r'.//temp_data//2_训练数据集_平均值填充.xlsx') # 读取平均值填充后的训练数据集Excel文件
train_data_x = train_data.iloc[:,1:] # 提取训练数据的特征列(从第2列到最后一列)
train_data_y = train_data.iloc[:,0] # 提取训练数据的标签列(第1列,即矿物类型)
test_data = pd.read_excel(r'.//temp_data//2_测试数据集_平均值填充.xlsx') # 读取平均值填充后的测试数据集Excel文件
test_data_x = test_data.iloc[:,1:] # 提取测试数据的特征列(从第2列到最后一列)
test_data_y = test_data.iloc[:,0] # 提取测试数据的标签列(第1列,即矿物类型)
result_data = {} # 初始化一个空字典,用于存储平均值填充方式下各模型的评估结果
关键点说明:
iloc[:, 1:]:按位置索引,取所有行、第 2 列到最后一列作为特征。iloc[:, 0]:取第 1 列作为标签。- 数据读取后特征为 DataFrame,后续需通过
.values转为 numpy 数组再转成 PyTorch 张量。
五、全连接神经网络(BP 神经网络)
5.1 网络结构
全连接神经网络(Fully Connected Neural Network),也叫多层感知机(MLP)或 BP 神经网络,每一层的每个神经元都与上一层的所有神经元相连。
本实验搭建的网络结构如下:
| 层 | 输入维度 | 输出维度 | 激活函数 |
|---|---|---|---|
| 输入层(fc1) | 13 | 32 | Sigmoid |
| 隐藏层(fc2) | 32 | 64 | Sigmoid |
| 输出层(fc3) | 64 | 4 | 无 |
- 输入维度 13 对应 13 个矿物特征。
- 输出维度 4 对应 4 种矿物类别。
- 输出层不使用激活函数,因为
CrossEntropyLoss内部已包含 Softmax。
5.2 核心原理
Sigmoid 激活函数:
σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+e−x1
将输入压缩到 (0, 1) 区间,历史上常用于二分类和隐藏层。但 Sigmoid 存在梯度消失问题:当输入绝对值较大时,导数趋近于 0,深层网络训练困难。因此现代网络更常用 ReLU。
交叉熵损失(CrossEntropyLoss):
L o s s = − ∑ i = 1 C y i log ( y ^ i ) Loss = -\sum_{i=1}^{C} y_i \log(\hat{y}_i) Loss=−i=1∑Cyilog(y^i)
其中 y i y_i yi 是真实标签的 one-hot 编码, y ^ i \hat{y}_i y^i 是模型预测的概率。交叉熵损失是多分类任务的标准选择。
Adam 优化器:结合了动量(Momentum)和自适应学习率(RMSprop)的优点,是目前最常用的优化器之一。
5.3 代码实现
python
import torch
import torch.nn as nn
class Net(nn.Module): # 定义神经网络类,继承自nn.Module
def __init__(self): # 构造函数,用于初始化网络结构
super(Net, self).__init__() # 调用父类nn.Module的构造函数
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个类别)
def forward(self, x): # 定义前向传播函数
x = torch.sigmoid(self.fc1.forward(x)) # 第一层全连接后使用sigmoid激活函数
x = torch.sigmoid(self.fc2(x)) # 第二层全连接后使用sigmoid激活函数
x = self.fc3(x) # 输出层全连接,不使用激活函数(CrossEntropyLoss内部会做softmax)
return x # 返回最终输出
5.4 数据转张量
python
X_train = torch.tensor(train_data_x.values, dtype=torch.float32)
Y_train = torch.tensor(train_data_y.values)
X_test = torch.tensor(test_data_x.values, dtype=torch.float32)
Y_test = torch.tensor(test_data_y.values)
- 特征用
float32,与网络参数默认类型一致。 - 标签直接转张量,
CrossEntropyLoss要求标签为LongTensor(整型),pandas 读取的数值默认会转为对应整型。
5.5 模型评估函数
python
def evaluate_model(model, X_data, Y_data, train_or_test):
"""评估模型在给定数据集上的准确率"""
size = len(X_data)
with torch.no_grad(): # 关闭梯度,节省内存
predictions = model(X_data) # 前向传播得到 logits
correct = (predictions.argmax(1) == Y_data).type(torch.float).sum().item()
correct /= size
print(f"{train_or_test}: \t Accuracy: {(100 * correct):.2f}%")
return correct
torch.no_grad():评估阶段不需要计算梯度,可大幅减少显存占用。argmax(1):在维度 1(类别维度)上取最大值索引,即预测类别。.item():将单元素 PyTorch 张量转为 Python 标量。
5.6 训练过程
python
model = Net()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
epochs = 15000
accs = []
for epoch in range(epochs):
outputs = model(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)
net_result = {}
net_result['acc'] = max(accs) # 记录最佳测试准确率
result_data['net'] = net_result
训练五步曲(每次迭代必做):
- 前向传播 :
outputs = model(X_train) - 计算损失 :
loss = criterion(outputs, Y_train) - 清空梯度 :
optimizer.zero_grad()(必须在反向传播前) - 反向传播 :
loss.backward() - 更新参数 :
optimizer.step()
共训练 15000 轮,每 100 轮评估一次,最终取所有评估点中的最大测试准确率作为模型结果。
六、卷积神经网络(1D CNN)
6.1 为什么表格数据可以用 CNN?
卷积神经网络通常用于图像(2D CNN)和文本/序列(1D CNN)。对于表格数据,我们可以将 13 维特征看作一个长度为 13 的一维序列 ,使用 1D 卷积提取特征之间的局部关联模式。
例如,矿物的"密度"和"硬度"可能存在局部相关性,卷积核可以捕捉这种相邻特征间的组合规律。
6.2 网络结构
| 层 | 输入通道 | 输出通道 | 卷积核 | 填充 | 说明 |
|---|---|---|---|---|---|
| Conv1 | 1 | 16 | 3 | 1 | 第一层卷积 |
| Conv2 | 16 | 32 | 3 | 1 | 第二层卷积 |
| Conv3 | 32 | 64 | 3 | 1 | 第三层卷积 |
| GAP | - | - | - | - | 全局平均池化 |
| FC | 64 | 4 | - | - | 全连接输出层 |
kernel_size=3, padding=1:卷积后序列长度不变( L o u t = L i n + 2 p − k + 1 = L i n L_{out} = L_{in} + 2p - k + 1 = L_{in} Lout=Lin+2p−k+1=Lin)。- 全局平均池化(Global Average Pooling):在特征维度上求均值,将
(batch, 64, 13)压缩为(batch, 64),参数量远小于展平后接全连接。
6.3 核心原理
一维卷积运算:
( f ∗ g ) n = ∑ k f k ⋅ g n − k (f * g)n = \sum_{k} fk \cdot gn - k (f∗g)n=k∑fk⋅gn−k
卷积核在特征序列上滑动,提取局部模式。多层卷积堆叠可以提取从低级到高级的特征组合。
全局平均池化(GAP):对每个通道的整个特征图取平均值,相比全连接展平:
- 大幅减少参数量,降低过拟合风险
- 对输入长度变化更鲁棒
- 增强模型对空间位置的不变性
6.4 代码实现
python
import torch
import torch.nn as nn
import torch.optim as optim
class ConvNet(nn.Module):
def __init__(self, num_features, hidden_size, num_classes):
super(ConvNet, self).__init__()
# 三层1D卷积,通道数逐步增加:1 -> 16 -> 32 -> 64
self.conv1 = nn.Conv1d(in_channels=1, out_channels=16, kernel_size=3, padding=1)
self.conv2 = nn.Conv1d(in_channels=16, out_channels=32, kernel_size=3, padding=1)
self.conv3 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
self.activation = nn.Sigmoid() # 注意:此处实际为Sigmoid激活
self.fc = nn.Linear(64, num_classes)
def forward(self, x):
# x 形状: (batch_size, 13) -> 增加通道维度 -> (batch_size, 1, 13)
x = x.unsqueeze(1)
x = self.conv1(x) # (batch, 16, 13)
x = self.activation (x)
x = self.conv2(x) # (batch, 32, 13)
x = self.activation (x)
x = self.conv3(x) # (batch, 64, 13)
x = self.activation (x)
x = x.mean(dim=2) # 全局平均池化 -> (batch, 64)
x = self.fc(x) # (batch, 4)
return x
unsqueeze(1) 是关键操作:Conv1d 要求输入形状为 (batch, in_channels, length),而原始数据为 (batch, 13),需要在第 1 维插入通道维度。
6.5 训练过程
python
X_train = torch.tensor(train_data_x.values, dtype=torch.float32) # 将训练集特征转换为PyTorch张量,数据类型为float32,形状为(样本数, 13)
Y_train = torch.tensor(train_data_y.values) # 将训练集标签转换为PyTorch张量
X_test = torch.tensor(test_data_x.values, dtype=torch.float32) # 将测试集特征转换为PyTorch张量,数据类型为float32,形状为(样本数, 13)
Y_test = torch.tensor(test_data_y.values) # 将测试集标签转换为PyTorch张量
hidden_size = 10 # 定义隐藏层大小(此参数在ConvNet中未直接使用,仅作为接口参数)
num_classes = 4 # 定义类别数量(矿物类型A、B、C、D共4类)
model = ConvNet(13, hidden_size, num_classes) # 实例化卷积神经网络模型,输入特征维度为13,隐藏层大小为10,输出类别为4
criterion = nn.CrossEntropyLoss() # 定义损失函数为交叉熵损失(适用于多分类问题)
optimizer = optim.Adam(model.parameters(), lr=0.001) # 定义优化器为Adam,学习率为0.001
num_epochs = 15000 # 设置训练迭代次数(轮数)
accs = [] # 初始化空列表,用于存储每个评估点的测试准确率
for epoch in range(num_epochs): # 循环训练num_epochs次
outputs = model(X_train) # 前向传播:将训练数据输入模型,得到预测输出(每个类别的logits)
loss = criterion(outputs, Y_train) # 计算损失值:使用交叉熵损失函数比较预测值和真实标签
optimizer.zero_grad() # 清空梯度缓存(防止梯度累加)
loss.backward() # 反向传播:计算损失关于模型参数的梯度
optimizer.step() # 更新模型参数:使用Adam优化器根据梯度更新权重
if (epoch + 1) % 100 == 0: # 每100轮打印一次训练进度和评估结果
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}') # 打印当前轮次和损失值
with torch.no_grad(): # 禁用梯度计算(评估时不需要反向传播,节省内存和计算资源)
# 评估训练集准确率
predictions = model(X_train) # 将训练数据输入模型,得到预测输出
predicted_classes = predictions.argmax(dim=1) # 获取每行最大值的索引(预测类别),形状为(batch_size,)
accuracy = (predicted_classes == Y_train).float().mean() # 计算训练集准确率:比较预测类别与真实标签,求均值
print(f'Train Accuracy: {accuracy.item() * 100:.2f}%') # 打印训练集准确率(百分比形式,保留2位小数)
# 评估测试集准确率
predictions = model(X_test) # 将测试数据输入模型,得到预测输出
predicted_classes = predictions.argmax(dim=1) # 获取每行最大值的索引(预测类别)
accuracy = (predicted_classes == Y_test).float().mean() # 计算测试集准确率:比较预测类别与真实标签,求均值
print(f'Test Accuracy: {accuracy.item() * 100:.2f}%') # 打印测试集准确率(百分比形式,保留2位小数)
accs.append(accuracy * 100) # 将测试准确率(转换为百分比)存入列表
cnn_result = {} # 初始化一个空字典,用于存储CNN模型的评估结果
cnn_result['acc'] = max(accs).item() # 从所有记录的测试准确率中取最大值(转换为Python标量),作为模型的最佳测试准确率
result_data['cnn'] = cnn_result # 将CNN的结果存入总结果字典
print(result_data) # 打印所有模型的评估结果
CNN 的训练流程与全连接网络完全一致,区别仅在于网络结构定义和学习率设置(CNN 用 lr=0.001,全连接用 lr=0.0001)。
七、结果输出
训练完成后,result_data 字典中存储了两个模型的最佳测试准确率:
python
print(result_data)
# 输出示例(实际值取决于数据和随机初始化):
# {'net': {'acc': 92.5}, 'cnn': {'acc': 95.0}}
| 模型 | 最佳测试准确率 |
|---|---|
| 全连接神经网络(Net) | result_data['net']['acc'] |
| 卷积神经网络(CNN) | result_data['cnn']['acc'] |
由于代码未设置随机种子(
torch.manual_seed),每次运行结果会有波动。如需可复现,建议在训练前设置:
pythontorch.manual_seed(42)
八、两种模型对比分析
| 对比维度 | 全连接神经网络 | 1D 卷积神经网络 |
|---|---|---|
| 参数量 | 较多(每层全连接) | 较少(卷积权重共享 + GAP) |
| 特征提取方式 | 全局组合,每个特征独立权重 | 局部模式提取,捕捉相邻特征关联 |
| 过拟合风险 | 较高 | 较低(权重共享、GAP) |
| 对特征顺序敏感度 | 不敏感(换列不影响) | 敏感(卷积依赖相邻关系) |
| 训练稳定性 | Sigmoid 易梯度消失 | 同样受 Sigmoid 影响 |
| 适用场景 | 通用表格分类 | 有序特征/序列型数据 |
对于矿物这种特征间有物理化学关联的数据,1D CNN 往往能学到更有意义的局部组合模式,在测试集上表现可能更优。
九、改进建议
原代码可从以下几个方面优化,以获得更好的分类效果:
9.1 数据标准化
python
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
train_data_x = scaler.fit_transform(train_data_x)
test_data_x = scaler.transform(test_data_x) # 注意:测试集只用transform
不同矿物特征量纲差异大,标准化后可加速收敛、提升精度。
9.2 改用 ReLU 激活函数
Sigmoid 在深层网络中容易梯度消失,建议替换为 ReLU:
python
x = torch.relu(self.fc1(x))
# 或在CNN中:self.activation = nn.ReLU()
ReLU 计算简单、缓解梯度消失,是目前隐藏层的主流选择。
9.3 设置随机种子
python
torch.manual_seed(42)
torch.cuda.manual_seed_all(42)
保证实验可复现。
9.4 早停(Early Stopping)
记录验证集损失,当连续多轮不再下降时停止训练,防止过拟合:
python
best_loss = float('inf')
patience = 500
counter = 0
# 训练中每轮检查验证集损失,若不下降则 counter++,达到 patience 则 break
9.5 学习率调度
python
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3000, gamma=0.5)
# 每3000轮学习率减半
训练后期降低学习率有助于精细收敛。
9.6 更丰富的评估指标
除准确率外,建议补充:
python
from sklearn.metrics import classification_report, confusion_matrix
print(classification_report(Y_test.numpy(), predicted_classes.numpy()))
print(confusion_matrix(Y_test.numpy(), predicted_classes.numpy()))
可查看每一类的精确率、召回率、F1 值和混淆矩阵,更全面评估模型。
十、完整代码汇总
python
# -*- coding: utf-8 -*-
"""
PyTorch 实现矿物分类:全连接神经网络 + 1D卷积神经网络
数据集:平均值填充后的矿物特征数据集(13维特征,4分类)
"""
import pandas as pd
from sklearn import metrics
import torch
import torch.nn as nn
import torch.optim as optim
"""==================== 1. 数据加载 ===================="""
train_data = pd.read_excel(r'.//temp_data//2_训练数据集_平均值填充.xlsx')
train_data_x = train_data.iloc[:, 1:]
train_data_y = train_data.iloc[:, 0]
test_data = pd.read_excel(r'.//temp_data//2_测试数据集_平均值填充.xlsx')
test_data_x = test_data.iloc[:, 1:]
test_data_y = test_data.iloc[:, 0]
result_data = {}
# 转张量
X_train = torch.tensor(train_data_x.values, dtype=torch.float32)
Y_train = torch.tensor(train_data_y.values)
X_test = torch.tensor(test_data_x.values, dtype=torch.float32)
Y_test = torch.tensor(test_data_y.values)
"""==================== 2. 全连接神经网络 ===================="""
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(13, 32)
self.fc2 = nn.Linear(32, 64)
self.fc3 = nn.Linear(64, 4)
def forward(self, x):
x = torch.sigmoid(self.fc1(x))
x = torch.sigmoid(self.fc2(x))
x = self.fc3(x)
return x
def evaluate_model(model, X_data, Y_data, train_or_test):
size = len(X_data)
with torch.no_grad():
predictions = model(X_data)
correct = (predictions.argmax(1) == Y_data).type(torch.float).sum().item()
correct /= size
print(f"{train_or_test}: \t Accuracy: {(100 * correct):.2f}%")
return correct
model = Net()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
epochs = 15000
accs = []
for epoch in range(epochs):
outputs = model(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}')
evaluate_model(model, X_train, Y_train, 'train')
test_acc = evaluate_model(model, X_test, Y_test, 'test')
accs.append(test_acc * 100)
result_data['net'] = {'acc': max(accs)}
"""==================== 3. 1D卷积神经网络 ===================="""
class ConvNet(nn.Module):
def __init__(self, num_features, hidden_size, num_classes):
super(ConvNet, self).__init__()
self.conv1 = nn.Conv1d(1, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv1d(16, 32, kernel_size=3, padding=1)
self.conv3 = nn.Conv1d(32, 64, kernel_size=3, padding=1)
self.activation = nn.Sigmoid()
self.fc = nn.Linear(64, num_classes)
def forward(self, x):
x = x.unsqueeze(1)
x = self.activation(self.conv1(x))
x = self.activation(self.conv2(x))
x = self.activation(self.conv3(x))
x = x.mean(dim=2)
x = self.fc(x)
return x
model = ConvNet(13, 10, 4)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
num_epochs = 15000
accs = []
for epoch in range(num_epochs):
outputs = model(X_train)
loss = criterion(outputs, Y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (epoch + 1) % 100 == 0:
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
with torch.no_grad():
train_pred = model(X_train).argmax(dim=1)
train_acc = (train_pred == Y_train).float().mean().item() * 100
print(f'Train Accuracy: {train_acc:.2f}%')
test_pred = model(X_test).argmax(dim=1)
test_acc = (test_pred == Y_test).float().mean().item() * 100
print(f'Test Accuracy: {test_acc:.2f}%')
accs.append(test_acc)
result_data['cnn'] = {'acc': max(accs)}
"""==================== 4. 结果输出 ===================="""
print("=" * 50)
print("各模型最佳测试准确率:")
print(f"全连接神经网络: {result_data['net']['acc']:.2f}%")
print(f"卷积神经网络: {result_data['cnn']['acc']:.2f}%")
print("=" * 50)
十一、总结
本文基于矿物特征数据集,使用 PyTorch 实现了两种深度学习分类模型:
- 全连接神经网络:结构简单,通过三层全连接层学习特征的全局组合。
- 1D 卷积神经网络:将 13 维特征视为序列,通过三层卷积提取局部模式,配合全局平均池化减少参数量。
两者均使用交叉熵损失和 Adam 优化器,训练 15000 轮后取最佳测试准确率进行对比。
通过本文的学习,你可以掌握:
- PyTorch 自定义网络的标准写法(继承
nn.Module、实现forward) - 训练五步曲:前向传播 → 计算损失 → 清空梯度 → 反向传播 → 更新参数
torch.no_grad()在评估阶段的使用- 1D CNN 处理表格数据的方法与
unsqueeze维度变换 - 全局平均池化的原理与优势
建议在实际项目中结合数据标准化、ReLU 激活、早停等技巧进一步提升模型性能。
如果本文对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区交流。