ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从Iris数据集开始,零基础掌握神经网络多分类实战

从Iris数据集开始,零基础掌握神经网络多分类实战 很多新手学人工智能往往第一脚就踩进理论泥潭里什么线性代数、概率论、反向传播公式推导轮番轰炸最后连门都没摸着就放弃了。我自己刚接触神经网络那会儿也是这样直到有一天耐着性子把Iris鸢尾花分类的代码从头到尾跑通盯着控制台里loss一点点降下去、accuracy一点点涨上来才突然明白那些抽象概念到底是什么意思。这个项目简单到什么程度呢数据只有150条特征只有4个类别只有3种你甚至不需要一台带独立显卡的电脑普通笔记本跑几十轮训练也就是几秒钟的事。但它五脏俱全数据加载、预处理、模型构建、训练、评估、预测、可视化一个完整的AI项目流程全都能在这套代码里走一遍。这篇文章我就拿Iris当教材把神经网络做多分类这件事从头到尾拆开讲。你不需要有深厚的数学基础只要会一点Python能跟着敲代码就能体会到模型在你手里从“乱猜”到“有判断力”的整个过程。我会把每一步为什么要这么写、背后是什么原理、实际跑起来会看到什么输出、遇到问题怎么排查全部摊开说清楚。1. Iris数据集为什么能成为AI入门的“第一课”1.1 数据规模与结构小但完整的训练素材Iris鸢尾花数据集最早由统计学家Ronald Fisher在1936年整理发表距今快一个世纪了但依旧是机器学习领域使用频率最高的入门数据集。原因很简单它足够小小到你可以在几秒内完成整个训练流程它又足够完整完整到包含了一个标准分类问题的所有要素。数据集中一共150条样本均匀分布在3个类别里每个类别恰好50条。这3个类别分别是山鸢尾Setosa、变色鸢尾Versicolor和维吉尼亚鸢尾Virginica。每条样本有4个特征花萼长度sepal length、花萼宽度sepal width、花瓣长度petal length和花瓣宽度petal width单位都是厘米。任务是给出一组花萼和花瓣的尺寸数据让模型判断这朵花属于哪一种鸢尾。你看这个过程本质上就是在做一件我们平时也会做的事情。比如你去植物园看到一朵花你观察它的高矮、颜色、叶片形状然后根据这些特征判断这是什么品种。神经网络做的事情和你没本质区别只不过它不会“看”只会处理数字。我们把花朵的尺寸数据输进去它通过内部的计算推导出一个类别标签。1.2 数据探索先用代码看看数据长什么样写模型之前我强烈建议你先自己亲手摸一摸这份数据。不是让你对着屏幕发呆而是用代码做一些快速检查搞清楚数据的分布情况。很多初学者上来就把load_iris()加载完的数据直接扔给模型训练从不去看数据本身长什么样这样其实错过了很重要的一个环节。import pandas as pd from sklearn.datasets import load_iris iris load_iris() df pd.DataFrame(iris.data, columnsiris.feature_names) df[target] iris.target df[species] df[target].map({0: setosa, 1: versicolor, 2: virginica}) print(df.head()) print(\n数据集统计信息) print(df.describe()) print(\n类别分布) print(df[species].value_counts())输出结果里你能看到df.describe()会给出每个特征的均值、标准差、最小值、最大值等信息。通过这些基础统计量可以大致了解数据的量纲和波动范围。比如花萼长度大致在4.3到7.9厘米之间花瓣宽度在0.1到2.5厘米之间。这些数值的量纲不一样后面会直接影响模型训练的效率和稳定性我们在预处理阶段需要处理这个问题。1.3 为什么Iris适合做多分类教学很多人在学深度学习时陷入一个误区觉得只有跑ImageNet那个量级的数据集才能学会神经网络。恰恰相反对于理解核心原理Iris这种小数据集反而有不可替代的优势。第一它的特征维度低。4个特征意味着输入层的神经元只需要4个这让整个网络结构变得极其清晰你用笔就能画出来。第二它的类别是均衡的。3个类别的样本数完全一样不存在类别不平衡带来的额外困扰。第三它有一个天然的“硬骨头”——Setosa和另外两类用简单的规则就能分开但Versicolor和Virginica之间的边界是模糊的。这意味着你的模型如果只学到了“死记硬背”而没学到真正的判别能力测试集上的表现很快就会暴露问题。这一点我们在后面评估模型时会专门说道。换句话说Iris虽然小但该有的挑战一点不少。2. 多分类神经网络的底层逻辑从神经元到Softmax2.1 单神经元到底在算什么神经网络听上去很玄乎但拆开来看最基本的计算单元就是一个神经元在做一个极简单的数学操作加权求和再加一个偏置然后过一层激活函数。你可以这样理解每个输入特征都会带一个“权重”权重的大小表示这个特征对最终判断的影响程度。拿Iris数据集来说4个特征就是4个输入模型要学的东西本质上就是找到一组合适的权重让每一个特征的贡献度恰到好处。比如花瓣宽度这个特征对区分Setosa非常关键那么模型训练完成之后与花瓣宽度相关的权重值大概率就会比较大。数学表达就是z w1*x1 w2*x2 w3*x3 w4*x4 b这里x1到x4是输入特征w1到w4是对应的权重b是偏置。z是加权求和的结果但z本身只是一堆数字的线性组合它没有非线性表达能力。这就需要一个关键的东西——激活函数。2.2 隐藏层让网络具备“抽象能力”如果你只有一个神经元没有激活函数那整个网络无论搭多少层本质上还是一个线性模型只能处理线性可分的问题。但Iris数据集里Versicolor和Virginica的分类边界并非纯线性所以我们需要引入非线性能力。解决方案就是在每层计算之后套一个激活函数。常用的有ReLU、sigmoid、tanh等。其中ReLU的表达式极其简单ReLU(z) max(0, z)意思是如果z大于0就保留原值小于等于0就输出0。就是这么简单的一个“砍一刀”操作却能让网络获得非线性表达能力。因为多个非线性函数的复合理论上可以逼近任意复杂的函数。我在构建Iris分类网络时中间用了两个隐藏层。隐藏层的神经元数量选了8个。你可能想问为什么是8个不是16个说实话这个数字没有唯一解。对于只有4个特征的任务隐藏层神经元数量设置得太大反而容易过拟合。8个神经元已经足够让网络提取出有区分度的特征组合了。这就像一个小团队4个输入信息进来经过8个“中层干部”重新组合加工最后汇聚成3个类别的判断。2.3 Softmax把得分变成概率多分类任务的输出层有一个和回归任务完全不同的设计。对于三分类问题输出层的神经元数量就是3个每个神经元对应一个类别。模型会为每个类别计算出一个“得分”但得分是实数可正可负大小范围不固定。直接拿这样的得分去和真实标签比既不直观也不好定义损失函数。所以我们在输出层使用Softmax激活函数。它的作用是把原始得分转换成一个概率分布保证三个类别的输出值之和等于1并且每个值都在0到1之间。假设输出层的原始得分是z [z1, z2, z3]Softmax会把它们变成p_i exp(z_i) / (exp(z1) exp(z2) exp(z3))这样算出来每个p_i就代表模型认为“这一朵花属于第i个类别”的概率。最后判断类别时取概率最大的那个作为预测结果。你可能会问为什么要用exp而不是直接归一化原因在于指数函数能把得分之间的差距放大。比如某两个得分是2和1直接归一化得到0.67和0.33但经过指数变换后得到0.73和0.27差距更明显。这样模型在做判断时就会更加“笃定”有利于训练收敛。2.4 反向传播与梯度下降网络怎么“学会”的这是神经网络的核心机制。简单来说训练过程就是不断地让模型的预测结果和真实标签做比较算出误差然后根据误差去调整每个权重让下一次预测更准确。具体流程是这样的前向传播输入一批数据经过网络每一层的计算最终得到预测概率。计算损失用交叉熵损失函数比较预测概率和真实标签之间的差距。反向传播利用链式法则把损失函数对每一层权重的梯度计算出来。梯度下降更新每个权重往梯度的反方向移动一小步步长就是学习率。重复以上步骤直到损失值收敛。你不需要立刻把链式法则的数学推导手推一遍那样容易劝退。你只需要先建立一幅直观的画面一个球从山坡上滚下来每次根据当前位置的坡度决定往哪个方向走、走多远最终抵达山谷。权重就是球的位置梯度就是坡度学习率就是一步迈多大。这个概念通了后面看代码就不会觉得云里雾里。3. 基于Keras的完整代码实现与逐段解读3.1 环境准备与依赖安装在写模型代码之前先确保你的Python环境里装好了必要的依赖库。我这里用了TensorFlow的Keras接口来搭建神经网络。你可能会问框架选型这么多为什么用TensorFlow不用PyTorch我的回答是都是好框架各有所长。对于Iris这种入门级项目Keras的API设计更直白遵循“模型即层堆叠”的设计哲学对新手极其友好。PyTorch的调试更灵活在高阶研究和动态图场景里优势明显但入门阶段的代码会更绕一些。安装命令如下pip install tensorflow scikit-learn pandas numpy matplotlib如果电脑没有GPU装CPU版本的TensorFlow完全够用。Iris数据集这么小CPU跑几十个epoch也就是几秒钟的事情。GPU在这里反而有点“杀鸡用牛刀”的感觉。3.2 数据加载与预处理完整代码的第一部分是加载数据并进行预处理。这一步看着简单但有几个细节值得停下来想一想。import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 设置随机种子保证结果可复现 np.random.seed(42) # 加载Iris数据集 iris load_iris() X iris.data y iris.target # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 数据标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) print(f训练集样本数{X_train_scaled.shape[0]}) print(f测试集样本数{X_test_scaled.shape[0]})我这里做了三件关键的事。第一固定随机种子random_state42。这一行看着不起眼但意义很大。神经网络的权重初始化是随机的如果不固定种子每次跑出来的结果都会略有差异。固定种子后你能保证自己的实验结果和复现的人一致这对学习和交流都非常重要。第二按7:3的比例划分训练集和测试集并且设置stratifyy做分层抽样。为什么强调分层因为Iris数据集本身三个类别各50条如果简单随机划分万一某个类别在训练集里特别少模型就学不到这个类别的好特征测试结果会失真。分层抽样能保证划分后的训练集和测试集里三个类别的比例和原始数据集保持一致。这是数据划分里很实用的一个细节。第三做标准化。4个特征的量纲不同比如花萼长度4到8厘米花瓣宽度只有0.1到2.5厘米。如果没有标准化数值大的特征对模型的初始梯度影响更大训练过程会很不稳定甚至让loss波动得厉害。标准化就是让每个特征变成均值约0、标准差约1的分布。注意fit_transform和transform的使用区别在训练集上“拟合”标准化器的参数均值和标准差在测试集上只能“应用”同一套参数不能重新拟合否则测试集的评估就失真了。3.3 模型构建三个Dense层的选择理由接下来是构建模型。这是全篇文章核心中的核心。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense # 构建神经网络模型 model Sequential([ Dense(8, activationrelu, input_shape(4,), namehidden_layer_1), Dense(8, activationrelu, namehidden_layer_2), Dense(3, activationsoftmax, nameoutput_layer) ]) # 打印模型结构 model.summary()这段代码短短几行但每一行背后都有设计逻辑。输入层的input_shape(4,)代表每个样本有4个特征值。第一个隐藏层有8个神经元用ReLU激活第二个隐藏层同样8个神经元用ReLU激活输出层有3个神经元正好对应3个鸢尾花类别用Softmax归一化成概率。两个隐藏层的设计是我测试过几次之后确定的。很多人熟悉了“隐藏层 越多越好”的说法但在小数据集上层数过多带来的收益微乎其微反而增加训练负担。Iris数据集只有150条样本两层的非线性拟合能力已经绰绰有余。你可以试试只用一层隐藏层测试准确率可能略低一点点也可能差不多因为这个问题本身并不复杂。但两层在Iris上通常表现得稳定一些。model.summary()的输出非常有用。你会看到每一层的输出形状和参数量。第一层Dense的参数是(41)*840个第二层是(81)*872个输出层是(81)*327个。总共139个参数。你没有看错整个神经网络只需要学习139个可训练参数是不是比你想象中少得多这也是小数据集的魅力你可以清清楚楚地知道网络里每一个参数的意义和规模。3.4 训练与评估一次完整的训练流程模型构建好之后就是编译和训练了。# 编译模型 model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) # 训练模型 history model.fit( X_train_scaled, y_train, epochs100, batch_size8, validation_split0.2, verbose1 ) # 在测试集上评估 test_loss, test_acc model.evaluate(X_test_scaled, y_test, verbose0) print(f\n测试集准确率{test_acc:.4f})这一大段代码有几个关键选择需要逐个说明。optimizeradam。Adam是目前深度学习里最主流的优化器之一它结合了Momentum和RMSProp的优点能够自适应地调整每个参数的学习率。简单说你不用费心思去调学习率衰减策略Adam会帮你办妥。对于入门项目所有任务都可以先默认用Adam等你熟练了再去尝试SGD动量优化等更“古典”的方案。losssparse_categorical_crossentropy。这个损失函数是多分类的标准选择。注意这里用的是sparse_前缀。为什么要区分因为我们的标签y是整数0、1、2不是one-hot向量。sparse_categorical_crossentropy可以直接和整数标签搭配。如果你把标签做成了one-hot编码比如[1,0,0]表示Setosa那就需要用categorical_crossentropy。两者数学上等价只是输入的标签表示方式不同。新手最容易犯的错就是标签格式和损失函数不匹配报了错还不知道问题出在哪儿。训练参数epochs100batch_size8validation_split0.2。意思是一共迭代100轮每轮把训练集切成小批每批8条样本更新一次梯度同时每轮抽20%的数据做验证用来监控模型是否过拟合。Iris训练集划分出来大概105条150×0.7实际参与训练的有105里的80%。这个量级的数据100轮完全可以收敛。跑完之后测试集准确率一般会在0.9333到1.0之间取决于权重初始化和验证集划分。如果你固定了随机种子结果会更稳定一般在0.95左右。3.5 训练日志怎么看loss和accuracy的波动训练过程中每跑完一个epoch你会看到一行日志列出训练损失、训练准确率、验证损失、验证准确率。很多人对着一堆数字不知道看什么我分享一个简单的观察顺序。先看训练损失有没有稳定下降。如果训练损失在某个数值附近震荡不再下降说明模型已经收敛了。接着看验证准确率和训练准确率的差距。如果训练准确率一直涨但验证准确率不涨或者反而下降那就是过拟合的前兆。对于Iris这个数据集100轮训练下来训练准确率通常是100%验证准确率基本在95%左右。如果验证集上只有90%也不用着急可以先重启训练或者检查数据标准化的代码看是不是测试集的标准化方式写错了。你也可以用代码把训练曲线画出来比较直观plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Loss) plt.subplot(1, 2, 2) plt.plot(history.history[accuracy], labeltrain_acc) plt.plot(history.history[val_accuracy], labelval_acc) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.title(Accuracy) plt.show()画出来的曲线里你会看到loss曲线从接近1左右的初始值一路往下走在20到30轮之后进入平缓区。accuracy曲线从50%附近爬升逐步稳定在90%以上。这个过程让我第一次直观理解了什么叫做“梯度下降”原来训练就是不断修正自己的过程。4. 训练中的关键决策损失函数、优化器与评估体系4.1 交叉熵为什么是分类任务的默认选择交叉熵损失函数衡量的是模型预测的概率分布和真实标签概率分布之间的差值。用一句话解释如果模型对正确类别的预测概率越高损失就越低如果模型对正确类别的预测概率低损失就急剧上升。这里有一个很多人忽视的关键点为什么分类任务不用均方误差MSE简单对比一下。MSE是用预测概率和真实标签one-hot形式直接算平方差。但它在Softmax场景下有一个致命问题梯度会变得很“软”收敛速度慢。因为Softmax的指数运算会让输出的分布“两极分化”MSE在输出接近0或接近1时梯度会变得非常小导致训练像挤牙膏一样。交叉熵的底数是指数函数和Softmax天然匹配形成了一个漂亮的对称形式。你只需要知道在实际项目里多分类任务无脑选交叉熵就对了这是被无数实验验证过的经验。4.2 Adam优化器为什么它“默认好用”优化器是真正干活的角色。梯度下降是最朴素的思想但朴素思想在面对复杂地形时容易出问题。比如学习率设置大了loss会在谷底附近来回震荡学习率小了收敛速度又慢得让人着急。Adam的全称是Adaptive Moment Estimation它给每个参数单独维护一个自适应学习率能根据历史梯度的均值和方差动态调整更新幅度。你可以把它理解成一个“路感特别好的司机”路面平坦的时候开快一点坑洼路段自动减速。这个特性让Adam几乎不需要手动调学习率就能在各种任务上拿到不错的结果。不过Adam也不是完全没有缺点。在需要极精细调优的场景里Adam的泛化性有时候比精心调过的SGD动量方法略差一点。但那是进阶话题对Iris项目来说Adam就是最省心的选择。4.3 光看accuracy远远不够——混淆矩阵才是关键准确率是一个很方便的指标但在多分类问题里它容易掩盖细节。比如100条测试样本如果某一个类别的样本占比很高而模型又恰好把它们分得很准accuracy看起来很高但另一个类别可能基本没分对。要看清每个类别的表现你需要画混淆矩阵from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns # 预测 y_pred model.predict(X_test_scaled) y_pred_classes np.argmax(y_pred, axis1) # 混淆矩阵 cm confusion_matrix(y_test, y_pred_classes) print(混淆矩阵) print(cm) # 分类报告 print(\n分类报告) print(classification_report(y_test, y_pred_classes, target_namesiris.target_names)) # 可视化 plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsiris.target_names, yticklabelsiris.target_names) plt.xlabel(Predicted) plt.ylabel(True) plt.show()混淆矩阵的行代表真实类别列代表预测类别。对角线上的数字是正确预测的数量非对角线是错误预测。在Iris上你大概率会看到这样的结果Setosa全部正确但Versicolor和Virginica之间存在一两条混淆。这个现象符合数据本身的分布特征因为Setosa在花瓣特征上和另外两类分离得很开而Versicolor和Virginica有特征重叠区。看classification_report里的precision和recall能更精确地定位问题。如果某个类别的recall偏低说明模型倾向于把它判成别的类别如果precision偏低说明别的类别的样本经常被误判到这个类。这些信息比单个准确率数值有价值得多。4.4 数据标准化的“隐藏影响”我一开始做这个项目时偷懒没做标准化把原始数据直接扔进模型结果训练过程loss降得很慢而且final accuracy不稳定。后来在数据里加了一步StandardScaler瞬间训练曲线就变得干净利落速度快了不止一个档次。标准化的原理并不复杂。神经网络的初始权重通常在0附近的小随机数如果你的某个特征数值在10厘米级别另一个特征数值在0.1厘米级别那么同样权重的微小变化在不同特征上带来的输出变化幅度是截然不同的。这会让梯度更新变得“偏科”优化过程不稳定。实际操作中一定要记住把标准化器在训练集上fit然后用这同一套参数去transform测试集。如果对测试集单独fit你就把测试集的分布信息泄露给了模型评估结果就是虚假的。5. 跑通之后进阶实验与实际部署思考5.1 先做三组对照实验理解“调参”到底在调什么训练跑通只是起点接下来我建议你做几组对照实验每一组都只改动一个变量观察结果的变化。这个过程比盲目读十篇教程都有用。第一组修改隐藏层神经元数量。把8改成4、16、32分别训练并记录测试准确率。你会发现神经元少时模型有点“学不进去”准确率偏低但神经元多到一定程度后准确率不会继续提升反而可能因为数据量不足而过拟合。第二组去掉标准化步骤。拿未标准化的数据直接训练观察loss曲线的波动情况和标准化后的曲线对比。这能让你直观地感受到数据预处理的价值。第三组修改激活函数。把ReLU换成sigmoid。你会看到训练速度明显变慢因为sigmoid在深层网络中容易引发梯度消失问题即使Iris这种浅层网络收敛速度也比ReLU慢。这个实验能让你在将来选择激活函数时有更深的体会。5.2 模型的保存与加载训练完的模型怎么用训练好的模型当然不能只用一次。Keras提供了非常简洁的模型保存与加载接口# 保存模型 model.save(iris_model.h5) # 加载模型 from tensorflow.keras.models import load_model loaded_model load_model(iris_model.h5) # 用加载回来的模型做预测 new_sample np.array([[5.1, 3.5, 1.4, 0.2]]) new_sample_scaled scaler.transform(new_sample) prediction loaded_model.predict(new_sample_scaled) predicted_class np.argmax(prediction) print(f预测结果{iris.target_names[predicted_class]}) print(f各类别概率{prediction[0]})注意实际部署时模型和标准化器要一起保存。我见过不少人只把模型文件保存了部署的时候忘了标准化处理输入还是原始量纲的数据模型输出自然是一堆无意义的数字。5.3 从Iris到真实项目差距到底在哪里看完这篇文章你能跑通Iris分类。但我也要说句实话真实世界的分类项目远比这个复杂。数据量动辄几十万条特征可能是上百维的数值加文本加图像类别不平衡、噪声数据、缺失值、分布漂移各种问题层出不穷。但Iris项目的价值恰恰在于它让你在不被这些庞杂问题淹没的前提下掌握了模型训练最核心的心智模型数据是模型的养料预处理决定训练的天花板网络结构影响拟合能力损失函数定义优化方向评估指标决定你怎么判断好坏。这套思维框架一旦建立起来后续学任何框架、做任何项目都是在这套框架上做减法或者加法。如果你还想接着往下学我建议按照这个路径走先用同款方法做手写数字识别MNIST感受一下图像数据和表格数据的区别然后学卷积神经网络CNN的卷积层和池化层原理理解为什么计算机视觉任务需要这种特殊结构再往后可以接触循环神经网络RNN和Transformer处理文本和序列数据。每一步都坚持“跑通代码再总结原理”你会发现自己成长得比想象中快。我个人练这套Iris项目已经不下几十遍了。每次带新人入门都会让他们重跑一遍跑完再讲原理效果比反过来好太多。这个项目就是你建立“代码手感”的地方——什么叫做设置随机种子、什么叫分层采样、为什么标准化要fit训练数据而不是全部数据、怎么判断模型是否过拟合这些老油条的常识全都能在Iris上亲手体验一遍。等你把这个项目练透了你会发现后面学什么模型都有一种“不过如此”的底气。所以别嫌它简单很多事情的道理都是在小而完整的东西里先通一遍然后再放大到真实场景中检验的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进