ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸢尾花数据集入门:用PyTorch从零实现多分类神经网络

鸢尾花数据集入门:用PyTorch从零实现多分类神经网络 做AI入门基本都绕不开鸢尾花Iris这个数据集。它相当于机器学习世界里的“Hello World”——150条样本、4个特征、3个类别信息量不大却恰好能把“多分类”和“神经网络”这两块硬骨头啃明白。这篇文章我打算从数据集底层展开把网络结构、损失函数、优化过程讲透再给一套可直接运行的Python代码带你从零复现一个鸢尾花多分类神经网络。适合刚接触人工智能、想动手写第一个模型又不想只停留在调库层面的读者。1. 为什么从鸢尾花下手数据集与多分类的底层逻辑1.1 鸢尾花数据集到底长什么样鸢尾花数据集收集了3个品种的花山鸢尾Setosa、变色鸢尾Versicolour、维吉尼亚鸢尾Virginica每个品种各50条样本一共150条。每条样本记录了4个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。这4个特征看起来普通但实际上区分度非常好。尤其是花瓣长度和花瓣宽度山鸢尾明显短一截另外两个品种之间有交叠但不严重。也就是说这个数据天然就适合做分类——特征和标签之间有关系又没有简单到一眼能看出来正好用来学模型。150条数据对深度学习来说非常小但对入门来说刚刚好。样本少训练快几秒就能迭代完一轮你可以反复改结构、调参数立即看到结果反馈这种即时性是入门阶段最宝贵的体验。真实项目动辄几万条数据改一个参数要等半天反而不适合学习。1.2 多分类与二分类的本质区别很多初学者先接触的逻辑回归、感知机默认都是处理二分类问题输出一个0到1之间的概率超过0.5归为正类否则归为负类。而鸢尾花是3个类别不能简单用一个神经元判断“是或否”。多分类问题的标准做法是“互斥类别做归一化概率分布”。什么意思模型最后输出3个数值每个数值对应一个类别的“得分”再用softmax函数把这些得分变成“和为1的概率”。比如输出可能是0.70、0.20、0.10表示模型认为有70%概率属于山鸢尾、20%属于变色鸢尾、10%属于维吉尼亚鸢尾。最终取最大值对应的类别就行。这里有一个关键点多分类要求类别之间互斥一只鸢尾花不可能同时是两个品种。如果是“多标签”问题比如一张图片里同时有猫和狗就不能用softmax而要用sigmoid对每个标签单独判断。这是两种完全不同的任务鸢尾花属于前者。1.3 为什么神经网络能解决这个分类任务传统方法比如决策树、支持向量机也能做鸢尾花分类那为什么偏要选神经网络因为神经网络提供了一个非常优雅的通用框架输入特征经过一系列线性变换加非线性激活逐层提取抽象表示最后输出分类结果。一个三层神经网络大致是这样运作的第一层把4个原始特征映射到隐藏层的若干个神经元每个神经元相当于学了某种特征组合第二层再组合这些特征形成更高阶的表示最后一层输出到3个类别得分。整个过程不依赖人工设计特征模型自己从数据里学规律。对鸢尾花这个任务神经网络未必比随机森林强多少但它的价值在于让你看清“分类问题是怎么被端到端解决的”。从原始数据进来到标签出去中间所有环节都由梯度下降自动调整这种“端到端学习”的思维方式是所有现代AI应用的核心。2. 跑神经网络的第一个5分钟环境与数据准备2.1 开发环境与依赖版本动手之前先说明一下环境。我用的是Python 3.10PyTorch 2.0scikit-learn 1.3numpy 1.24matplotlib 3.7。PyTorch逐渐成为学术界和工业界的主流框架它的自动求导机制让反向传播实现变得非常简洁适合用来学习底层原理。安装命令很简单pip install torch scikit-learn numpy matplotlib如果你的电脑有NVIDIA显卡可以装CUDA版本的PyTorch但鸢尾花这个任务用CPU就够了。150条样本、几千个参数CPU跑几百个epoch也只要几秒钟。不建议入门阶段纠结GPU配置把精力留在理解模型上。2.2 加载数据与探索性观察加载鸢尾花数据集最方便的方式是用scikit-learn自带接口from sklearn.datasets import load_iris import pandas as pd data load_iris() X data[data] y data[target] feature_names data[feature_names] target_names data[target_names] df pd.DataFrame(X, columnsfeature_names) df[label] y print(df.head()) print(df.groupby(label).size())运行之后你会看到150条样本中标签0、1、2各有50条类别分布完全均衡。这个细节很重要均衡数据最后看准确率就有意义如果某个类别占了90%模型全猜那一类也能有90%准确率指标就失真了。我强烈建议先做一步可视化把特征关系看一遍import matplotlib.pyplot as plt colors [red, blue, green] for i in range(3): subset df[df[label] i] plt.scatter(subset[petal length (cm)], subset[petal width (cm)], ccolors[i], labeltarget_names[i]) plt.xlabel(petal length (cm)) plt.ylabel(petal width (cm)) plt.legend() plt.show()你会看到三类点在花瓣长度和宽度平面上分得比较清晰。这个观察能帮你建立“数据有规律可学”的直觉——如果画出来全部随机混在一起再厉害的神经网络也很难学到东西。2.3 数据预处理标准化与数据集划分神经网络训练有两个很关键的预处理步骤标准化和划分训练测试集。标准化是把每个特征缩放到均值0、方差1附近公式是(x - mean) / std。为什么要做这一步因为花萼长度单位是厘米数值在5左右而花瓣宽度也在厘米级别看起来量纲差不多但实际分布范围仍有差异。如果特征尺度差异大梯度更新会被大数值的特征主导导致收敛变慢。scikit-learn的StandardScaler可以一键完成from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)这里有一个新手容易犯的错误对训练集用fit_transform对测试集只用transform。fit_transform会在数据上计算均值和标准差如果对测试集再fit一次等于用测试集的信息去变换测试集会造成信息泄漏影响评估结果的可信度。正确的做法是把scaler在训练集上“拟合”好然后原封不动地套到测试集上。另一个细节是stratifyy。它表示划分时保持每个类别的比例与原始数据一致也就是每类训练40条、测试10条。如果不设置stratify随机划分可能让某一类在测试集中偏多或偏少小数据集下容易导致评估结果波动很大。3. 网络结构拆解三层神经网络每一层在做什么3.1 输入层、隐藏层与输出层的设计神经网络的结构说白了就是“一层接一层的矩阵运算”。以鸢尾花分类为例我设计了这样一个网络输入层4个神经元对应花萼长、花萼宽、花瓣长、花瓣宽隐藏层116个神经元ReLU激活隐藏层28个神经元ReLU激活输出层3个神经元对应3个类别得分为什么中间要用16和8没有特别神秘的公式主要考虑是输入特征只有4个隐藏层容量不用太大。16个神经元足以学出各种特征组合再大就容易过拟合。你可以自己试试32个、64个训练速度几乎没差别但性能提升很有限说明模型容量已经不是瓶颈。用PyTorch定义这个网络代码非常直观import torch import torch.nn as nn class IrisNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(4, 16) self.fc2 nn.Linear(16, 8) self.fc3 nn.Linear(8, 3) self.relu nn.ReLU() def forward(self, x): x self.relu(self.fc1(x)) x self.relu(self.fc2(x)) x self.fc3(x) return x注意输出层没有加ReLU因为最后要交给softmax转换成概率而softmax要求输入是实数域的得分不能是经过挤压的正值。很多入门教程会在输出层误加激活函数这是一个隐性坑。3.2 隐藏层为什么必须用非线性激活函数如果所有层都只是线性变换那多层网络可以压缩成一层。两个线性变换相乘本质上还是一个线性变换表达能力没有任何提升。只有加入非线性激活函数比如ReLU才能让网络逼近复杂的非线性边界。ReLU的公式是f(x) max(0, x)当输入大于0时原样输出小于0时截断为0。它计算简单、梯度不衰减是目前隐藏层最常用的激活函数。早期流行的sigmoid在深层网络中容易造成梯度消失——反传误差时梯度越乘越小前面几层几乎学不到东西。ReLU在正区间的梯度恒为1从根本上规避了这个问题。那为什么不用ReLU后的常数项即负区间梯度为0导致某些神经元“死掉”在鸢尾花这种小网络里基本不会出现但在很深的网络里如果学习率设置过大会出现大量死亡神经元这时可以考虑LeakyReLU。3.3 输出层与softmax从得分到概率输出层产生3个数值但我们要的是“属于每个类别的概率”这需要softmax函数转换。softmax的公式是[ p_i \frac{e^{z_i}}{\sum_{j1}^{C} e^{z_j}} ]其中z_i是第i个类别的原始得分。指数运算把得分变成正数再除以所有类别的指数和就得到一个合计为1的概率分布。这样处理的好处有两点一是概率值在0到1之间有明确语义二是放大了得分差异如果某个类别得分明显高它的概率会指数级碾压其他类别。PyTorch的CrossEntropyLoss实际上把softmax和损失计算合并在一起了所以我们训练时不需要手动调用softmax直接把最后一层原始输出传给损失函数就行。但在推理阶段如果你想看概率值可以在forward输出后加一层torch.softmax。3.4 损失函数交叉熵为什么适合分类问题分类任务最常用的损失函数是交叉熵Cross Entropy。它可以理解为衡量两个概率分布之间的差异一个是真实标签的分布比如[1, 0, 0]表示真的是山鸢尾另一个是模型预测的概率分布比如[0.7, 0.2, 0.1]。模型越接近真实分布交叉熵越小。交叉熵的公式是[ L -\sum_{i1}^{C} y_i \log(p_i) ]由于真实标签y_i只有一个位置是1、其余为0整个求和就简化为“真实类别对应概率的负对数”。如果模型对正确类别的预测概率接近1损失接近0如果预测概率只有0.1损失就是2.3左右。这个性质让交叉熵对错误分类施加了很强的惩罚梯度信号清晰比均方误差更适合分类。为什么不直接用均方误差因为均方误差假设输出是连续值用于分类时梯度容易被输出层的饱和区域压制收敛慢且容易卡在局部最优。交叉熵配合softmax是分类问题的黄金组合这个组合一定要理解透。4. 核心代码实现从训练到评估的完整流程4.1 数据张量转换与数据集封装PyTorch模型不能直接接收numpy数组需要转换成张量。把训练数据转成浮点张量标签转成长整型张量CrossEntropyLoss要求标签是Long类型不能用Floatimport torch X_train_tensor torch.tensor(X_train, dtypetorch.float32) y_train_tensor torch.tensor(y_train, dtypetorch.long) X_test_tensor torch.tensor(X_test, dtypetorch.float32) y_test_tensor torch.tensor(y_test, dtypetorch.long)数据集特别小直接用TensorDataset封装就行from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset(X_train_tensor, y_train_tensor) test_dataset TensorDataset(X_test_tensor, y_test_tensor) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) test_loader DataLoader(test_dataset, batch_size16, shuffleFalse)batch_size设成16表示每轮从训练集随机抽16条样本算一次梯度。为什么不用全量120条一起算全量计算梯度方向最准确但每迭代一次只更新一次参数收敛路径单调小批量随机采样虽然梯度有噪声但这种噪声反而有帮助能让模型跳出不好的局部点而且计算速度快。16这个值对120条训练数据来说比较合理。4.2 训练循环前向传播、反向传播、参数更新完整的训练循环是神经网络最核心的“三件套”前向传播算损失、反向传播算梯度、优化器更新参数。代码如下model IrisNet() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.01) epochs 200 train_losses [] for epoch in range(epochs): model.train() epoch_loss 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() outputs model(batch_X) loss criterion(outputs, batch_y) loss.backward() optimizer.step() epoch_loss loss.item() avg_loss epoch_loss / len(train_loader) train_losses.append(avg_loss) if (epoch 1) % 20 0: print(fEpoch {epoch1:3d}, Loss: {avg_loss:.4f})逐一拆解一下optimizer.zero_grad()是清空上一轮的梯度。PyTorch的梯度是累加的如果不手动清零下一轮backward会把新梯度和旧梯度叠加在一起。loss.backward()计算当前损失对每个参数的偏导数并把梯度缓存在每个参数的.grad属性里。这就是反向传播PyTorch的自动求导机制自动完成了链式法则的推导。optimizer.step()拿到梯度后按照优化算法更新参数。这里用Adam它可以自适应调整每个参数的学习率在大多数场景下不需要手动调整学习率就能稳定收敛。如果换成SGD可能需要更小的学习率、更长的训练时间才能达到同样效果。loss.item()把损失值从张量中取成Python数值用于记录和打印。注意不能用loss.item()强制类型转换到int因为那样会丢失精度。4.3 测试评估准确率到底怎么算训练完之后在测试集上评估泛化能力。评估时一定要先调用model.eval()它会关闭Dropout、BatchNorm等训练期才生效的机制。再用torch.no_grad()包裹预测代码告诉PyTorch这里不需要计算梯度省内存、跑得快。model.eval() correct 0 total 0 all_preds [] all_labels [] with torch.no_grad(): for batch_X, batch_y in test_loader: outputs model(batch_X) _, predicted torch.max(outputs, dim1) total batch_y.size(0) correct (predicted batch_y).sum().item() all_preds.extend(predicted.numpy()) all_labels.extend(batch_y.numpy()) accuracy correct / total print(fTest Accuracy: {accuracy:.4f} ({correct}/{total}))torch.max(outputs, dim1)返回两个东西最大值本身和最大值所在的索引。对于输出[0.2, 0.5, 0.9]最大值是0.9索引是2对应类别2。索引就是预测标签直接和真实标签比较得到准确率。正常情况下这个简单网络在测试集上的准确率应该在0.93到1.0之间。如果只有0.7或更低先别急着改模型八成是数据预处理或训练设置出了问题后面第五节会专门讲排查思路。另外要说明的是随机种子会影响划分结果我固定random_state42后测试集会稳定在30条样本结果可复现方便对照。4.4 完整训练脚本与画损失曲线把上面的代码拼到一起就是一个完整的训练脚本。建议在训练过程中记录每轮的损失值最后画出来观察收敛情况import matplotlib.pyplot as plt plt.plot(train_losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss Curve) plt.show()正常情况损失曲线应该平滑下降然后在某个低位震荡。如果看到损失曲线反复大起大落说明学习率过大如果一直居高不下说明学习率太小或模型结构有问题。我在实验里把300个epoch的全过程跑了一遍前50轮损失从1.1降到0.3左右后面基本在0.1附近波动。测试准确率最终稳定在1.0。说实话面对这么简单的数据1.0准确率并不算特别惊艳因为特征本身区分度就高。它真正的意义在于验证了整个训练流程是通的数据流、梯度流、参数更新每一环都正常工作。5. 混淆矩阵与常见坑跑通之后必须掌握的排障能力5.1 混淆矩阵代码实现与解读准确率只有一个数字看不出模型在哪两类上容易搞混。混淆矩阵把真实类别和预测类别的对应关系展开成一张表格每一行是真实类别每一列是预测类别对角线上的数字就是预测正确。用scikit-learn的confusion_matrix几行就能出来from sklearn.metrics import confusion_matrix, classification_report import numpy as np cm confusion_matrix(all_labels, all_preds) print(cm) print(classification_report(all_labels, all_preds, target_namestarget_names))更直观的做法是用matplotlib画热力图import matplotlib.pyplot as plt plt.figure(figsize(6, 5)) plt.imshow(cm, interpolationnearest, cmapplt.cm.Blues) plt.colorbar() tick_marks np.arange(len(target_names)) plt.xticks(tick_marks, target_names) plt.yticks(tick_marks, target_names) plt.xlabel(Predicted Label) plt.ylabel(True Label) thresh cm.max() / 2 for i in range(cm.shape[0]): for j in range(cm.shape[1]): plt.text(j, i, format(cm[i, j], d), hacenter, vacenter, colorwhite if cm[i, j] thresh else black) plt.tight_layout() plt.show()在鸢尾花这个例子里混淆矩阵大概率看到对角线全是数字非对角线全是0。这说明模型把三个类别全部分对了。如果真的出现某个非对角线位置有数字比如第1行第2列有值说明模型把真实的变色鸢尾错分成了维吉尼亚鸢尾这时候回去查一下原始数据的特征分布很可能这两类在花瓣宽度上有重叠区域。classification_report还会输出每个类别的精确率precision、召回率recall和F1分数。在多分类场景下这三个指标比整体准确率更能反映模型的细粒度表现值得养成每次评估都打印的习惯。5.2 常见问题速查表一次说清症状与对策问题现象可能原因排查方向与对策训练损失出现NaN学习率过大梯度爆炸降低学习率从0.01改到0.001再试检查数据是否有缺失值准确率一直很低大约0.3~0.4数据没标准化网络没训练起来确认是否做了StandardScaler尝试增大隐藏层神经元数量增加训练轮数训练准确率100%测试准确率只有70%过拟合模型记训练集太死增加训练数据本项目可以加大test_size比例减少隐藏层神经元加Dropout正则化损失缓慢下降但准确率不涨学习率太小或网络容量不足适度调大学习率增加一个隐藏层试用AdamW优化器每次运行结果差异很大没有固定随机种子数据划分和初始化不同在训练前设置torch.manual_seed(42)、np.random.seed(42)测试集准确率高于训练集小样本下的随机波动或Dropout在训练时未关闭确认model.eval()是否调用多跑几次取平均值判断真实水平5.3 新手最容易踩的3个隐形坑第一个坑在测试集上做标准化时重新fit_transform。这会泄露测试集信息导致评估结果虚高让人觉得模型很牛一上真实数据就崩。第二个坑忘记model.eval()就做推理。在BatchNorm和Dropout存在的网络里不切到eval模式会让预测结果不稳定而且这种误差很隐蔽不容易发现。虽然鸢尾花这个小网络不用BatchNorm但养成先eval再推理的习惯很重要。第三个坑用accuracy作为唯一评估指标。在类别不均衡的数据集上准确率特别容易骗人。比如95%的数据是类别A模型全猜A也有95%准确率但实际对B类完全无效。我自己的习惯是同时看混淆矩阵和每类F1分数综合判断模型能力。5.4 如何确认模型真的“学到东西”而不是“背下数据”一个经典验证方法是观察特征重要性。把测试样本的某个特征数值方向变动看模型输出会不会跟着变化。比如把一条真实山鸢尾样本的花瓣长度从1.4改成4.0模型预测应该从类别0变为类别2。如果模型对微小扰动完全无感说明它没有学到特征与标签的真实关系而是在单纯记忆训练样本。另一个方法是可视化中间层输出。把测试集喂给模型提取第一层ReLU后的16维特征用PCA降到2维画散点图。如果三类点能明显分开说明网络内部确实形成了有效表示。这一步稍微进阶但很能加深对神经网络“分层提特征”的理解。6. 后续能力进化从Iris到真实分类任务扩写6.1 模型结构上的可扩展方向鸢尾花只有4个特征所以网络做得很浅。换到真实数据集比如图像分类输入可能是一张64x64x3的图片改成神经网络时输入维度会变成12288这种规模直接用全连接层参数量会爆炸需要引入卷积层来提取局部特征。但底层逻辑仍然一样前向传播、算损失、反向传播、更新参数。可以尝试在这个小项目上做几个结构变体观察效果差异加宽隐藏层从16改成32、64训练速度几乎不变但准确率上限可能提高加深加一个包含32个神经元的隐藏层观察收敛速度和最终效果变化加正则化在隐藏层之间加Dropout(0.2)看是否会影响测试准确率这种改动成本极低但能让你直观感受到“模型容量”和“正则化”对结果的影响。我建议每个变体都跑5次取平均准确率因为小数据集上随机性很大。6.2 损失函数与评价指标的延伸场景鸢尾花使用的交叉熵损失面对是单标签多分类。现实中还有多标签分类、回归、排序等任务对应的损失函数也各不相同。回归用均方误差或平均绝对误差多标签用多标签软间隔损失排序用ListMLE或者LambdaRank。入门阶段先把交叉熵吃透后面遇到新任务时自然知道要去了解哪种损失。评价指标也一样。鸢尾花用准确率没问题但如果换成类别不均衡的信贷违约预测就要重点看AUC、F1、召回率尤其是少数类别的召回率。模型开发早期就把评估指标选对比事后修补模型重要得多。6.3 我的亲身体会先跑通再优化最后才谈创新做这个项目的过程中我最大的感受是“跑通一个简单网络比看十篇教程都管用”。第一次动手写代码时我栽在标准化上没对测试集做正确的transform结果是模型效果虚高之后怎么调结构都觉得不对劲。后来把数据预处理修正过来效果立刻恢复正常。还有一个我常用的习惯每次改动只动一个变量。想调学习率就只改学习率想加层就只加层其他保持不动记录训练曲线和测试指标。这样改动的效果才能归因到具体因素上不然同时改三个参数出了问题根本不知道是哪一步引起的。最后给你一个实操建议把epoch数从200改到500观察损失曲线的变化把batch_size从16改成4和64对比训练过程的平滑程度。这些小实验花不了十分钟但比任何教程都更容易让你建立“超参数影响模型行为”的直觉。做AI入门就是这样从鸢尾花开始一步步把底层的坑全踩一遍后面遇到再复杂的任务也不慌。
返回列表