飞桨PaddlePaddle实现鸢尾花分类:深度学习入门实战全流程

发布时间:2026/10/15 7:01:37
飞桨PaddlePaddle实现鸢尾花分类:深度学习入门实战全流程 1. 项目概述为什么第一个深度学习项目选它鸢尾花分类这名字听着像植物学课题其实是深度学习入门者的“Hello World”。数据集是1936年由一位植物学家整理的真实测量数据记录了三种鸢尾花各自的花萼长度、花萼宽度、花瓣长度、花瓣宽度一共150条样本每个类别50条。没有任何缺失值没有乱七八糟的噪声干净得就像教科书里的例题。我选择用飞桨PaddlePaddle来做这个分类任务而不是用其他框架有两个原因一是飞桨在国产深度学习框架里生态最完整从数据加载到模型训练再到部署全流程都能覆盖二是它对新手极其友好动态图的写法跟PyTorch非常接近但文档和中文社区资源更丰富遇到问题搜起来不费劲。这个项目能解决的问题很直接给定一朵鸢尾花的四项测量数据判断它是山鸢尾、变色鸢尾还是维吉尼亚鸢尾。放到现实场景里这本质是一个多分类问题——输入是4维特征向量输出是3个类别的概率分布。别小看这个看似简单的任务它涉及了深度学习中最重要的几个核心概念数据预处理、模型搭建、损失函数设计、训练循环、模型评估。把这些流程跑通一遍后续再去做图像分类、文本分类你会发现套路都是一样的只是数据和网络结构换了而已。适合来看这篇文章的是那些刚学完Python语法、对深度学习有概念但从未动手写过完整训练代码的人。我会把从环境搭建到代码实现再到结果分析的完整路径都走一遍你照着做应该一两个小时就能跑通而且能真正理解每一步在干什么而不是单纯复制粘贴跑个结果就完事。先说结论用飞桨实现鸢尾花分类我最终在测试集上达到了100%的准确率。数据集本身比较简单这个结果并不稀奇但重点是过程中踩过的坑、对模型结构的理解、对训练过程的分析方法这些才是真正有价值的东西。2. 项目设计模型结构和方案选型背后的思考2.1 为什么用全连接网络而不是更复杂的结构鸢尾花数据集只有4个特征150条样本。这决定了我们不可能也不需要使用卷积神经网络CNN或循环神经网络RNN这类复杂结构。4个特征排成一排就是一个简单的向量全连接网络Dense Layer天然就是为这种结构化数据设计的。我见过有人拿CNN来做这个项目说实话有点杀鸡用牛刀。CNN擅长提取空间特征比如图像的边缘、纹理而这些是像素点在二维空间里的关系。对表格数据来说特征之间没有空间上的邻居关系强行把它reshape成某种形状再塞给CNN不仅没有收益还会让模型更难训练——参数量暴涨但样本量只有150条很容易过拟合。全连接网络的基本结构是输入层4个神经元→ 隐藏层若干神经元→ 输出层3个神经元。每一层的每个神经元都与上一层的所有神经元相连通过权重矩阵完成线性变换再经过激活函数引入非线性。这个结构虽然简单但对鸢尾花这种低维数据已经足够。我用的具体结构是这样的输入层4个神经元对应四个特征隐藏层18个神经元激活函数ReLU隐藏层28个神经元激活函数ReLU输出层3个神经元不加激活函数配合Softmax损失函数使用隐藏层选8个神经元是我试出来的经验值。少了比如4个模型表达能力不足训练集上都很难收敛多了比如32个在这个数据量下容易过拟合测试集准确率反而不稳定。8个是一个比较平衡的选择既能把特征之间的非线性关系学出来又不至于让模型容量超出数据的承载能力。2.2 数据划分策略训练集、验证集和测试集的角色数据划分这件事新手最容易想当然。很多人随手拿全部数据训练、全部数据测试得到一个准确率就欢呼“模型完美”。这个做法在学术上叫“数据泄露”得到的结果完全不可信。正确的做法是把数据拆成三份训练集用于模型学习参数权重和偏置验证集用于训练过程中调整超参数比如学习率、隐藏层大小判断模型是否过拟合测试集模型完成训练后用它来评估最终泛化能力测试集在训练过程中绝对不能沾手因为样本总量只有150条我按6:2:2的比例划分也就是训练集90条、验证集30条、测试集30条。每个类别在三个集合中都要保持均衡否则可能出现训练集里山鸢尾特别多测试集里却特别少的情况导致评估结果有偏差。实现上我用的是train_test_split这种常见手法设置好随机种子保证结果可复现。随机种子这一步很容易被忽略但它特别重要——没有固定种子你每次跑代码数据划分都不一样实验结果就无法对比也没法定位问题是出在模型还是出在数据上。2.3 损失函数和优化器的选型逻辑这是一个分类问题输出3个类别的概率我最开始想到的是交叉熵损失函数。交叉熵衡量的是模型预测的概率分布与真实标签分布之间的差异值越小代表预测越准。配合最后一层用Softmax把输出变成概率这几乎是多分类任务的标准配置。一个容易踩的坑是最后一层到底要不要手动加Softmax。在飞桨里如果你使用CrossEntropyLoss这个损失函数它会自动在内部完成Softmax计算也就是说模型最后一层不需要手动加Softmax——直接输出原始logits就行。如果你在最后一层手动加了Softmax再用CrossEntropyLoss会出现“Softmax套Softmax”的问题导致梯度不稳定模型很难收敛。优化器我选择的是Adam。相比传统的SGD随机梯度下降Adam会自动调整每个参数的学习率对学习率的选择不那么敏感。特别是对新手来说SGD可能需要你反复调试学习率才能收敛Adam基本上只要设置一个合理的初始值比如0.001就能稳定训练。后续我也对比了SGD加动量效果差不多但Adam确实省心。学习率我设了0.01跑了大概200个epoch就能收敛得很好了。这里多说一句学习率不是越小越好也不是越大越好。太小人会让训练极慢太大可能导致损失函数震荡不收敛。0.01是我在鸢尾花这个数据集上试出来的比较合适的值你可以以此为起点调。3. 环境准备和数据加载从零搭起可复现的实验环境3.1 飞桨的安装与版本选择这一步看似基础但版本问题坑过不少人。我的建议是先确定你的Python版本和操作系统再选择对应的飞桨安装命令。我用的环境是Python 3.9 Windows 11安装命令是python -m pip install paddlepaddle2.6.0装完后在Python里验证一下import paddle print(paddle.__version__) # 输出 2.6.0如果你用的是Windows且想用GPU加速飞桨目前对NVIDIA显卡的支持比较成熟需要额外装CUDA和cuDNN再安装paddlepaddle-gpu。但鸢尾花这种小数据量CPU训练绰绰有余我全程用CPU跑每个epoch耗时不到0.1秒完全不需要纠结GPU。还有一点要注意虚拟环境一定要建。我见过太多人在系统Python环境里直接装包结果各种依赖冲突最后整个环境都崩了。用conda建一个干净的环境所有项目依赖都装在里面互不干扰这也是项目可持续复现的基础。conda create -n iris python3.9 conda activate iris3.2 数据集获取和自建Dataset类飞桨自带了一些经典数据集比如MNIST、CIFAR但鸢尾花不在其内置列表中。所以我们需要自己加载数据。数据来源很直接一个公开的鸢尾花数据集文件通常以.csv格式存储包含5列花萼长度、花萼宽度、花瓣长度、花瓣宽度、品种名称。我写了一个小函数来读取数据并转换为模型需要的格式import numpy as np import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(iris.csv) # 把品种名称映射为数值标签 species_map {setosa: 0, versicolor: 1, virginica: 2} df[label] df[species].map(species_map) # 取出特征和标签 X df[[sepal_length, sepal_width, petal_length, petal_width]].values y df[label].values # 转换数据类型 X X.astype(float32) y y.astype(int64) # 划分训练集和剩余数据剩余再划分为验证集和测试集 X_train, X_temp, y_train, y_temp train_test_split(X, y, test_size0.4, stratifyy, random_state42) X_val, X_test, y_val, y_test train_test_split(X_temp, y_temp, test_size0.5, stratifyy_temp, random_state42) print(f训练集大小: {X_train.shape}) print(f验证集大小: {X_val.shape}) print(f测试集大小: {X_test.shape})这里有一个我特别想强调的细节X.astype(float32)和y.astype(int64)。深度学习框架默认要求特征数据是float32类型因为GPU计算对32位浮点数最友好标签数据是int64类型这是索引类数据在飞桨里的标准格式。很多新手在这类数据加载上出错报错信息五花八门但根源都是数据类型不匹配。在飞桨里加载数据最规范的做法是继承paddle.io.Dataset类自定义一个数据集类from paddle.io import Dataset class IrisDataset(Dataset): def __init__(self, X, y): self.X X self.y y def __getitem__(self, idx): return self.X[idx], self.y[idx] def __len__(self): return len(self.y) train_dataset IrisDataset(X_train, y_train) val_dataset IrisDataset(X_val, y_val) test_dataset IrisDataset(X_test, y_test)等多熟悉一些你可能会觉得这一步麻烦想直接用数组一次性塞给模型训练。但我的建议一直是养成自定义Dataset类的习惯。因为真实项目中数据很少是直接整理好放在内存里的更多时候需要从文件读取、做数据增强、做样本均衡这些逻辑都应该放进Dataset类里。底子打好了后面处理复杂数据会顺畅很多。3.3 数据标准化的必要性鸢尾花四个特征的量纲不同——花萼长度在4.9到7.9之间花萼宽度在2.0到4.4之间花瓣长度在1.0到6.9之间花瓣宽度在0.1到2.5之间。如果直接拿原始值训练量纲大的特征可能在梯度更新中占据主导地位导致模型偏向这些特征而削弱了其他特征的影响。我做了标准化处理用均值和方差把每个特征缩放到均值0、方差1的分布。注意标准化参数只能用训练集的数据计算而不能用整个数据集。原因很简单——如果用测试集数据参与计算均值和方差相当于让模型在训练时“偷看”了测试集的信息属于轻微的数据泄露。mean X_train.mean(axis0) std X_train.std(axis0) X_train (X_train - mean) / std X_val (X_val - mean) / std X_test (X_test - mean) / std标准化之后四个特征的尺度统一了模型训练过程会更稳定收敛速度也会更快。这是一个微小但很关键的预处理步骤跳过它你可能会遇到损失函数一直不下降、准确率上不去的诡异情况。4. 手写模型训练代码核心实现与逐段解读4.1 通过Sequential容器快速搭建网络飞桨提供了两种搭建模型的方式一种是组网式写法用paddle.nn.Sequential把各层按顺序拼接起来另一种是类式写法继承paddle.nn.Layer自定义网络的forward方法。对于鸢尾花这种简单网络我用的是Sequential代码简洁直观import paddle import paddle.nn as nn class IrisClassifier(nn.Layer): def __init__(self): super().__init__() self.fc nn.Sequential( nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 8), nn.ReLU(), nn.Linear(8, 3) ) def forward(self, x): return self.fc(x) model IrisClassifier()这段代码做了什么第一层Linear(4, 8)把4维输入映射到8维隐藏空间ReLU把负值截断为0引入非线性第二层Linear(8, 8)继续在8维空间做变换最后一层Linear(8, 3)输出3维的logits。关于ReLU的选择一个常见疑问是为什么不选Sigmoid或者TanhReLU在深度学习中更常用的原因是它能缓解梯度消失问题——Sigmoid函数在输入远离0时梯度会变得极小多个隐藏层叠加后梯度几乎为0导致参数无法更新。ReLU在正区间梯度恒为1不会衰减训练更稳定。对于只有两层隐藏层的网络来说Sigmoid也许也能工作但ReLU依然是更稳妥的选择。4.2 损失函数和优化器的定义criterion nn.CrossEntropyLoss() optimizer paddle.optimizer.Adam(learning_rate0.01, parametersmodel.parameters())这里有一个飞桨初学者经常困惑的地方为什么优化器要传入model.parameters()因为在训练过程中优化器需要知道要更新哪些参数。传入model.parameters()就是告诉它这个模型里所有的权重和偏置都是要学习的对象。而损失函数不需要传入参数它只是计算模型输出和真实标签之间的差异这个差异作为梯度回传的起点。4.3 训练循环的完整实现训练循环是整个项目的核心。我会把代码完整列出再逐段说明import paddle epochs 200 batch_size 16 train_loader paddle.io.DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) val_loader paddle.io.DataLoader(val_dataset, batch_sizebatch_size) best_val_acc 0.0 for epoch in range(epochs): # 训练阶段 model.train() total_loss 0.0 correct 0 total 0 for features, labels in train_loader: # 前向计算 logits model(features) loss criterion(logits, labels) # 梯度清零反向传播更新参数 optimizer.clear_grad() loss.backward() optimizer.step() total_loss loss.item() * features.shape[0] # 计算训练准确率 preds paddle.argmax(logits, axis1) correct (preds labels).sum().item() total labels.shape[0] avg_train_loss total_loss / len(train_dataset) train_acc correct / total # 验证阶段 model.eval() val_correct 0 val_total 0 with paddle.no_grad(): for features, labels in val_loader: logits model(features) preds paddle.argmax(logits, axis1) val_correct (preds labels).sum().item() val_total labels.shape[0] val_acc val_correct / val_total if val_acc best_val_acc: best_val_acc val_acc paddle.save(model.state_dict(), best_model.pdparams) print(fEpoch {epoch1}: 发现更好的模型验证准确率 {val_acc:.4f}) if (epoch 1) % 20 0: print(fEpoch {epoch1}/{epochs}, Loss: {avg_train_loss:.4f}, Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f})这段代码有几个关键的“潜规则”需要说明。model.train()和model.eval()的切换这是新手最容易忽略的。训练模式下Dropout层会随机丢弃神经元如果有的话BatchNorm层会使用当前batch的统计数据而评估模式下这些层的行为必须不同——Dropout不再丢弃BatchNorm使用训练阶段累积的全局统计量。虽然我这个网络里没有Dropout和BatchNorm但养成切换的习惯非常重要否则以后模型复杂了会因为这个小问题得到错误评估结果。optimizer.clear_grad()必须在loss.backward()之前调用飞桨的反向传播会累加梯度如果不把上一次迭代的梯度清零本轮计算的梯度会叠加到旧梯度上导致参数更新方向错误。paddle.no_grad()用于评估阶段这个上下文管理器会让框架停止追踪计算图大幅减少内存占用同时加速评估过程。评估阶段不需要计算梯度自然就应该关掉。保存模型的时机我没有简单地在训练结束后保存模型而是记录验证集上表现最好的那些参数即时保存。这能防止过拟合导致后期模型参数反而变差的情况。4.4 训练过程中的数值观察训练结束后我把整个训练过程中的loss和准确率变化趋势记录下来最明显的特征是loss从初始的1.1左右开始快速下降大约在第30个epoch就降到了0.1以下之后缓慢趋近于0.01训练集准确率和验证集准确率在第40个epoch左右都达到了100%。一个有意思的现象是训练集准确率达到100%之后验证集准确率并没有下降这说明模型没有过拟合。原因有三点一是数据本身线性可分性较好二是样本量足够支撑这个规模的模型三是隐藏层只有8个神经元模型容量刚好合适。如果你跑的loss在某个数值附近震荡不下降不要急着调网络结构先检查两个地方一是学习率是否过大二是数据是否做了标准化。5. 结果分析准确率、混淆矩阵和模型行为解读5.1 测试集上的评估方法模型训练好了最终要用测试集来评估——测试集是整个训练过程中从未接触过的数据。加载之前保存的最好模型评估代码如下# 加载最优模型 model.load_state_dict(paddle.load(best_model.pdparams)) model.eval() test_correct 0 test_total 0 all_preds [] all_labels [] with paddle.no_grad(): for features, labels in test_loader: logits model(features) preds paddle.argmax(logits, axis1) test_correct (preds labels).sum().item() test_total labels.shape[0] all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) test_acc test_correct / test_total print(f测试集准确率: {test_acc:.4f})跑下来测试集准确率是1.0——30条测试数据全部预测正确。但这个结果本身没有太大信息量因为样本太少了100%和97%之间的差别可能只是1条数据的正确与否。所以我同时计算了混淆矩阵看模型在哪个类别上容易出错from sklearn.metrics import confusion_matrix import pandas as pd cm confusion_matrix(all_labels, all_preds) df_cm pd.DataFrame(cm, index[setosa, versicolor, virginica], columns[setosa, versicolor, virginica]) print(df_cm)得到的混淆矩阵是一个完美的对角矩阵——每个类别的测试样本都被正确分类。这时才能说模型在测试集上的表现确实优秀而非偶然的运势。5.2 花的类别之间的混淆情况虽然本次测试结果完美但你多跑几次、换不同的随机种子偶尔会遇到1到2个样本分类错误的情况。错误的模式大多是变色鸢尾和维吉尼亚鸢尾互相混淆。山鸢尾几乎不会与其他两类混淆原因是它的花瓣尺寸明显偏小——花瓣长度通常在1.5到2厘米之间而其他两类都在3厘米以上。这个区分度极高模型很容易抓住这个特征。变色鸢尾和维吉尼亚鸢尾的特征空间存在一定的重叠区域特别是那些处于类别边界的样本模型确实难以区分。这其实反映了数据本身的特性而不是模型的缺陷。如果希望在测试集上稳定达到100%的准确率有几个调节点一是适当增加隐藏层神经元数量比如改到16个让模型能捕捉更细致的边界信息二是训练更多epoch比如300个三是干脆用集成方法训练多个模型投票。但对入门项目而言这些优化不是必须的更重要的是理解模型表现背后的原因。5.3 从输出概率理解模型“自信”程度除了看分类对错我还习惯看模型输出的置信度。把一批测试数据喂给模型取出Softmax之后的概率分布# 在getitem里已经取了特征这里重新构造loader test_loader paddle.io.DataLoader(test_dataset, batch_sizelen(test_dataset)) with paddle.no_grad(): for features, labels in test_loader: logits model(features) probs paddle.nn.functional.softmax(logits, axis1) print(probs.numpy())结果很有意思对于大部分样本模型输出概率几乎是一边倒比如[0.99, 0.01, 0.00]而某些边界样本输出可能是[0.01, 0.55, 0.44]——虽然最大概率的那一类恰好是正确的但模型其实并没有那么笃定。现实项目里这种置信度信息非常有用。如果模型的置信度普遍偏低说明特征空间里类别边界不清晰需要更多数据或更强特征如果有个别样本置信度极高却分类错误那可能是数据标注错了。6. 常见问题与排查技巧实录6.1 新手必踩的五个坑及解决方案我把新手在实现鸢尾花分类时最容易遇到的几个问题整理成了表格每一条都是我自己或者身边朋友真实踩过的。问题特征表现原因解决方案数据没有标准化loss下降慢训练不稳定特征量纲差异大对特征做标准化参数只在训练集上计算最后一层手动加了Softmax又用CrossEntropyLoss模型收敛很慢准确率低Softmax被重复计算去掉最后一层的Softmax损失函数内部自动计算训练和评估模式没切换结果波动大或有异常值Dropout和BatchNorm在两种模式下行为不同训练前调用model.train()评估前调用model.eval()随机种子没固定每次运行结果差异大数据划分和参数初始化随机设置paddle.seed(42)和Python的random.seed(42)直接拿所有数据训练、所有数据测试准确率虚高数据泄露划分训练集、验证集、测试集测试集在训练中完全不使用6.2 用可视化手段快速定位训练问题我发现很多人训练深度学习模型时有个坏习惯只盯着最终准确率看中间过程完全不关注。这样出了问题很难定位。一个特别实用的方法是用图表观察训练曲线。我通常每隔一定步数记录loss和准确率绘制成曲线图。几种典型曲线对应的问题Loss持续不下降可能是学习率太大梯度震荡导致参数无法收敛也可能是数据没有标准化部分特征的梯度被其他特征淹没。Loss快速降低到很低后突然反弹一般是学习率过大模型跳出了收敛区间。可以在训练过程中做学习率衰减。训练集准确率高但验证集准确率低典型的过拟合。这时需要降低模型复杂度减少隐藏层神经元或者增加训练数据或者加正则化手段如Dropout。训练集和验证集准确率都低可能是模型容量不足欠拟合。这时需要增加网络层数或神经元数量。很多人一上来就追求“用什么模型结构最好”但我更建议先从简单的结构开始配合训练曲线观察模型行为然后再逐步增加复杂度。这样每一步改动都知道是为什么遇到问题也更容易定位。6.3 不同超参数组合的对比经验我在调参过程中用过几组不同的配置这里记录下来供你参考。每组都固定随机种子、固定200个epoch只在单一变量上做修改。配置编号隐藏层神经元数学习率优化器测试集准确率配置180.01Adam100%配置240.01Adam93.3%配置3160.01Adam100%配置480.1Adam100%配置580.001Adam100%配置680.01SGD(0.9动量)96.7%配置2的教训是模型容量太小时即使数据简单也可能学不好。4个神经元的隐藏层拟合能力太弱在类别重叠区域很难学到有效的决策边界。配置6用SGD加动量也跑到了近97%的准确率但收敛速度明显比Adam慢而且对学习率的敏感度更高。对入门项目来说Adam依然是更省心的选择。7. 项目扩展从鸢尾花到真实场景的迁移方法鸢尾花分类做完之后如果你只是把它当成一个作业交上去那确实有点可惜。这个项目最大的价值在于它的整套流程可以直接复用到其他表格分类任务中。我做过一个小实验把同一套代码稍作修改跑了一个红酒品质分类的数据集——特征11个类别3档训练集里每个类别的样本数量更不均衡。改动的地方只有三处输入维度从4改成11、添加了一个Dropout层来缓解不平衡数据下的过拟合、把训练轮数增加到300。其他逻辑全部复用效果也不错。如果你想进一步深挖推荐几个方向特征可视化用PCA把4维特征降维到2维在平面上画出三类鸢尾花的分布。你会发现山鸢尾聚成一簇与另外两类分得很开而变色鸢尾和维吉尼亚鸢尾之间有明显的重叠区域。这个可视化能帮你更直觉地理解模型在学什么。决策边界可视化前面提到特征重叠是分类的主要难点可以考虑只取两个特征比如花瓣长度和花瓣宽度训练一个简化模型然后把模型的决策边界画出来。看着模型把平面划分成三个区域比看准确率数字直观得多。自定义损失函数比如尝试带权重交叉熵——给少数类别更高的权重模型会更多关注这些样本。这个技巧在真实的类别不平衡问题中很实用。从我个人的经验来说做完这个项目最值得反思的一点是有些任务用传统机器学习比如逻辑回归、决策树可能更容易取得好效果但深度学习框架的学习价值并不仅仅在于结果而在于你完整走了一遍训练流程、理解了梯度回传的原理、学会了分析模型行为。这些底子打好了后面迁移到更复杂的任务就会从容很多。鸢尾花分类的150条数据里你能学到的东西远比“100%准确率”这几个字多得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询