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个神经元,对应四个特征
- 隐藏层1:8个神经元,激活函数ReLU
- 隐藏层2:8个神经元,激活函数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 paddlepaddle==2.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 python=3.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_size=0.4, stratify=y, random_state=42) X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42) 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(axis=0) std = X_train.std(axis=0) 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或者Tanh?ReLU在深度学习中更常用的原因是它能缓解梯度消失问题——Sigmoid函数在输入远离0时梯度会变得极小,多个隐藏层叠加后梯度几乎为0,导致参数无法更新。ReLU在正区间梯度恒为1,不会衰减,训练更稳定。对于只有两层隐藏层的网络来说,Sigmoid也许也能工作,但ReLU依然是更稳妥的选择。
4.2 损失函数和优化器的定义
criterion = nn.CrossEntropyLoss() optimizer = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters())这里有一个飞桨初学者经常困惑的地方:为什么优化器要传入model.parameters()?因为在训练过程中,优化器需要知道要更新哪些参数。传入model.parameters()就是告诉它:这个模型里所有的权重和偏置都是要学习的对象。
而损失函数不需要传入参数,它只是计算模型输出和真实标签之间的差异,这个差异作为梯度回传的起点。
4.3 训练循环的完整实现
训练循环是整个项目的核心。我会把代码完整列出,再逐段说明:
import paddle epochs = 200 batch_size = 16 train_loader = paddle.io.DataLoader(train_dataset, batch_size=batch_size, shuffle=True) val_loader = paddle.io.DataLoader(val_dataset, batch_size=batch_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, axis=1) 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, axis=1) 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(f'Epoch {epoch+1}: 发现更好的模型,验证准确率 {val_acc:.4f}') if (epoch + 1) % 20 == 0: print(f'Epoch {epoch+1}/{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, axis=1) 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_size=len(test_dataset)) with paddle.no_grad(): for features, labels in test_loader: logits = model(features) probs = paddle.nn.functional.softmax(logits, axis=1) 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,只在单一变量上做修改。
| 配置编号 | 隐藏层神经元数 | 学习率 | 优化器 | 测试集准确率 |
|---|---|---|---|---|
| 配置1 | 8 | 0.01 | Adam | 100% |
| 配置2 | 4 | 0.01 | Adam | 93.3% |
| 配置3 | 16 | 0.01 | Adam | 100% |
| 配置4 | 8 | 0.1 | Adam | 100% |
| 配置5 | 8 | 0.001 | Adam | 100% |
| 配置6 | 8 | 0.01 | SGD(0.9动量) | 96.7% |
配置2的教训是:模型容量太小时,即使数据简单也可能学不好。4个神经元的隐藏层拟合能力太弱,在类别重叠区域很难学到有效的决策边界。
配置6用SGD加动量也跑到了近97%的准确率,但收敛速度明显比Adam慢,而且对学习率的敏感度更高。对入门项目来说,Adam依然是更省心的选择。
7. 项目扩展:从鸢尾花到真实场景的迁移方法
鸢尾花分类做完之后,如果你只是把它当成一个作业交上去,那确实有点可惜。这个项目最大的价值在于,它的整套流程可以直接复用到其他表格分类任务中。
我做过一个小实验,把同一套代码稍作修改,跑了一个红酒品质分类的数据集——特征11个,类别3档,训练集里每个类别的样本数量更不均衡。改动的地方只有三处:输入维度从4改成11、添加了一个Dropout层来缓解不平衡数据下的过拟合、把训练轮数增加到300。其他逻辑全部复用,效果也不错。
如果你想进一步深挖,推荐几个方向:
特征可视化:用PCA把4维特征降维到2维,在平面上画出三类鸢尾花的分布。你会发现山鸢尾聚成一簇,与另外两类分得很开,而变色鸢尾和维吉尼亚鸢尾之间有明显的重叠区域。这个可视化能帮你更直觉地理解模型在学什么。
决策边界可视化:前面提到特征重叠是分类的主要难点,可以考虑只取两个特征(比如花瓣长度和花瓣宽度)训练一个简化模型,然后把模型的决策边界画出来。看着模型把平面划分成三个区域,比看准确率数字直观得多。
自定义损失函数:比如尝试带权重交叉熵——给少数类别更高的权重,模型会更多关注这些样本。这个技巧在真实的类别不平衡问题中很实用。
从我个人的经验来说,做完这个项目最值得反思的一点是:有些任务用传统机器学习(比如逻辑回归、决策树)可能更容易取得好效果,但深度学习框架的学习价值并不仅仅在于结果,而在于你完整走了一遍训练流程、理解了梯度回传的原理、学会了分析模型行为。这些底子打好了,后面迁移到更复杂的任务就会从容很多。
鸢尾花分类的150条数据里,你能学到的东西,远比“100%准确率”这几个字多得多。