人工智能入门系列之一: 鸢尾花(Iris)多分类的神经网络详解及代码实现
如果你搜过“人工智能入门”“神经网络练手项目”,八成会撞见同一个名字:鸢尾花(Iris)数据集。这个数据集火了几十年,不是因为它有多炫,而是因为它把“多分类”这件事压缩到了一个极小但完整的闭环里——数据够小、特征够少、类别够清晰,非常适合把神经网络的每个环节掰开揉碎讲清楚。
这篇文章想带你做的,不只是跑通一段代码,而是搞明白神经网络在这类任务上到底干了什么。我会从数据本身说起,讲清楚多分类和普通回归在原理上的区别,再把损失函数、输出层设计、训练流程这些关键节点逐一拆开,最后给出一份可以在本地直接运行的完整代码。适合刚接触人工智能、会一点Python、想跨过“调包跑通”这层窗户纸的同学,也适合做课程设计或复试项目时想真正理解模型原理的朋友。
1. 鸢尾花数据集:先把这个经典赛道看清楚
1.1 数据集到底长什么样
鸢尾花数据集最早由统计学家R. A. Fisher在1936年整理发表,距今快九十年了,但依然是分类算法实验的标准数据之一。数据集本身很简单:一共150条样本,每条样本对应一株鸢尾花,包含4个特征——花萼长度(sepal length)、花萼宽度(sepal width)、花瓣长度(petal length)、花瓣宽度(petal width),单位都是厘米。标签则是3个品种:Setosa、Versicolour、Virginica,每个品种恰好50条样本。
用几行Python就能把这些数据加载出来看个究竟:
import pandas as pd from sklearn.datasets import load_iris iris = load_iris() df = pd.DataFrame(iris.data, columns=iris.feature_names) df['target'] = iris.target df['target_name'] = iris.target_names[iris.target] print(df.head()) print(df['target_name'].value_counts())输出结果大概是这样的:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) target target_name 0 5.1 3.5 1.4 0.2 0 setosa 1 4.9 3.0 1.4 0.2 0 setosa 2 4.7 3.2 1.3 0.2 0 setosa 3 4.6 3.1 1.5 0.2 0 setosa 4 5.0 3.6 1.4 0.2 0 setosa setosa 50 versicolor 50 virginica 50数据量不大,几百KB都不到,加载、训练、验证都非常快。但别因为数据小就轻视它,这个数据集的设计其实很讲究:4个特征里,花瓣长度和花瓣宽度对三个类别有很强的区分度,花萼特征则相对模糊;三个类别中,Setosa和另外两类几乎是线性可分的,而Versicolour和Virginica存在重叠区域。也就是说,它天然带有“部分容易、部分有挑战”的特性,特别适合用来观察模型到底学到了什么。
1.2 为什么入门项目选它而不是MNIST或CIFAR
现在一提到深度学习入门,很多人第一反应是手写数字识别MNIST,或者更“现代”的CIFAR-10。这些数据集确实经典,但对刚接触神经网络的人来说,我反而更推荐先做鸢尾花。
原因有三点:
第一,特征维度低,问题足够聚焦。MNIST一张图是28×28=784个像素,CIFAR-10一张图是32×32×3=3072个数值,而鸢尾花只有4个特征。特征少意味着你可以把注意力放在模型结构和训练流程上,而不是花大量时间去处理图像、做数据增强、调卷积核参数。神经网络的核心机制——前向传播、反向传播、梯度下降、损失函数——在这4个特征上就能完整展示。
第二,训练极快,适合反复实验。用CPU跑一个两三百轮的鸢尾花分类模型,通常几秒钟就结束了。这个速度带来的好处是你可以大胆地改结构、调学习率、换激活函数,马上就能看到结果差异。这种“高频试错”的体验,对建立直觉特别重要。我见过不少初学者一上来就在GPU上跑图像分类,改一次参数等半小时,注意力全被等待消耗掉了。
第三,多分类问题的“最小完备系统”。二分类只需要一个输出节点、一个Sigmoid就能完成,而多分类需要你真正理解Softmax、交叉熵、类别索引这些概念。跳过多分类直接上手图像分类,往往会让人把“分类”和“回归”混为一谈。鸢尾花恰好把多分类的每个细节都暴露出来了,非常适合逐个击破。
如果你已经有了一些基础,也可以把它当成一个“实验台”,去测试不同的初始化方法、优化器、正则化手段,因为数据跑得快,你一天能做的实验数量远超在图像数据集上的体验。
2. 多分类神经网络的原理拆解
2.1 从单层感知机到多层神经网络
先回忆一下最早的神经元模型——感知机(Perceptron)。它做的事情很简单:把输入加权求和,再加上一个偏置,然后通过一个阶跃函数输出0或1。用公式表示就是:
output = activation(w1*x1 + w2*x2 + w3*x3 + w4*x4 + b)感知机只能解决线性可分问题,也就是在特征空间里能用一条直线(或一个超平面)把类别分开。但鸢尾花的三个类别里,Versicolour和Virginica并不是严格线性可分的,单层感知机在这里表现会很差。
多层神经网络(也叫多层感知机,MLP)的改进在于中间加了隐藏层,隐藏层的每个神经元都接收上一层所有神经元的输出,经过加权、求和、非线性激活后再传给下一层。这样叠加出来的模型就具备了拟合非线性决策边界的能力。一层隐藏层理论上就能逼近任意连续函数(万能逼近定理),当然实际效果还取决于神经元数量、激活函数、训练策略等。
在鸢尾花任务上,一个输入维度为4、隐藏层维度为8、输出维度为3的全连接网络,就已经能取得很好的效果。这里的“好”不是碰巧,而是因为这个数据集的复杂程度不高,非线性决策边界相对平滑,一个规模适中的网络就足够拟合。
2.2 Softmax与交叉熵:多分类的两大支柱
二分类问题常用的做法是输出层只有一个节点,接Sigmoid函数,把输出压缩到(0,1)之间,代表正类的概率。但多分类需要给每个类别都输出一个概率,并且所有类别的概率之和要等于1。
Softmax函数就是做这件事的。假设输出层有3个原始分数(称为logits),分别是z1、z2、z3,Softmax把每个分数做指数运算后归一化:
p_i = exp(z_i) / (exp(z_1) + exp(z_2) + exp(z_3))为什么要用指数而不是直接除以总和?因为指数运算能放大分数之间的差距,让最大分数对应的类别概率更突出;同时指数函数恒大于零,天然保证了概率的非负性。这套机制让Softmax成为了多分类输出层的标准配置。
有了概率输出,怎么衡量预测得好不好呢?这里用交叉熵损失。交叉熵衡量的是模型预测的概率分布和真实标签的概率分布之间的差异。真实标签一般写成one-hot向量,比如Virginica类别的标签就是[0, 0, 1],对应位置为1,其余为0。交叉熵的公式为:
L = -sum(y_i * log(p_i))因为one-hot向量里只有一个位置是1,所以交叉熵实际上就等价于:取真实类别对应的那个预测概率,求它的负对数。如果模型给正确类别的概率接近1,损失就接近0;如果给正确类别的概率很小,损失就会很大。
这里有个常见的困惑:既然交叉熵这么简单,为什么不直接用“正确类别概率的负数”作为损失,而非要写成一个看起来复杂的形式?从数学角度看,one-hot写法能保持公式的对称性,方便推导梯度;从代码角度看,PyTorch的CrossEntropyLoss在内部已经帮我们做了Softmax和负对数概率的结合,所以直接用即可,但理解底层原理对排查问题很有帮助。
补充一句:还有一个容易踩的坑是误把均方误差(MSE)用于分类。MSE是为回归设计的,它假设误差分布近似高斯,而分类任务的输出是离散类别概率,用MSE会导致梯度非常小、收敛极慢。所以只要做分类,就用交叉熵,这是“选型”层面的常识,不是偏好问题。
2.3 网络结构怎么定:输入层、隐藏层、输出层
设计神经网络时,输入层和输出层的维度通常是任务决定的。输入维度等于特征数,鸢尾花是4;输出维度等于类别数,鸢尾花是3。真正需要设计的是隐藏层:层数、每层神经元个数、激活函数、正则化手段等。
对于鸢尾花这个量级的数据集,我的建议是:从一层隐藏层开始,神经元数量设置成8~16个就足够了,不用一上来就堆大网络。原因很简单:
- 数据只有150条,特征只有4个,模型容量太大容易记住训练数据,泛化到测试数据时反而变差,这就是过拟合。
- 小网络训练速度快,方便你做对比实验。等你把数据加载、训练循环、评估流程都跑顺了,再去扩展网络规模也不迟。
激活函数方面,隐藏层用ReLU是当前最稳妥的选择。ReLU的计算是max(0, x),梯度在正区间恒为1,能有效缓解Sigmoid在深层网络中容易出现的梯度消失问题;在浅层网络里虽然没有那么明显的梯度消失压力,但ReLU的稀疏激活特性也会让训练更稳定。Sigmoid/tanh更适合用在输出层做二分类概率或数值压缩,用在隐藏层反而容易让深层梯度变小。
这里给出一个初始结构建议:
输入层(4维) → 全连接层(4 → 16) → ReLU → 全连接层(16 → 8) → ReLU → 输出层(8 → 3)这个结构比单隐藏层稍微复杂一点,但不多,能让你感受到“深度”的含义,又不至于难调。如果你只想跑通最简单的版本,去掉第二层隐藏层也完全可行。
2.4 为什么梯度下降能找到最优参数
神经网络训练的本质是:初始化一组随机参数,计算损失对每个参数的梯度,然后沿着梯度的反方向更新参数,不断重复,直到损失降到较低水平。这个“沿着反方向走”的过程就叫梯度下降。
用一个生活类比来解释:假设你被蒙着眼放在一座山坡上,任务是走到山谷最低点。你没法看到全貌,只能通过脚下感觉哪个方向最陡、最陡的方向就是梯度反方向。你沿着最陡的方向迈一步,再重新感受,再迈一步,最终就能走到山谷。步长就是学习率,一步迈太大可能跨过山谷跑到对面山坡上,一步迈太小则需要走很久。
在代码里,这个过程的体现是optimizer.step():它根据损失计算出的梯度,更新网络里的所有权重和偏置。很多框架把梯度计算封装好了,初学者容易忽略的一点是:每次更新前要调用optimizer.zero_grad()清空上一次的梯度,否则梯度会累加,导致参数更新方向混乱。这个细节我在后面代码部分会重点标注。
3. 代码实现:从数据到训练再到评估
3.1 环境准备与数据加载
我下面的代码基于PyTorch,因为它在学术圈和工业界都够主流,API设计也比TensorFlow更Pythonic一些,适合入门理解。你需要在本地安装好:
pip install torch scikit-learn pandas matplotlib如果你用的是GPU版本的PyTorch,没问题,这段代码在CPU上跑也足够快。接下来加载数据并做初步划分:
import torch import torch.nn as nn import torch.optim as optim import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 加载鸢尾花数据集 iris = load_iris() X = iris.data # 形状:(150, 4) y = iris.target # 形状:(150,),取值 0、1、2 print("特征矩阵形状:", X.shape) print("标签取值:", np.unique(y)) print("类别名称:", iris.target_names)输出:
特征矩阵形状: (150, 4) 标签取值: [0 1 2] 类别名称: ['setosa' 'versicolor' 'virginica']这里标签已经是用0、1、2编码的整数,PyTorch的CrossEntropyLoss要求标签是整数索引形式,不需要再额外做one-hot编码。千万不要多此一举转成one-hot,否则会浪费内存还容易出错。
3.2 数据预处理:标准化是必须的
神经网络对输入特征的尺度很敏感。如果某些特征数值在0~1之间,另一些在50~100之间,网络在初始化时就会对数值大的特征更敏感,导致训练不稳定、收敛慢。解决办法是标准化:把每个特征减去均值再除以标准差,让所有特征都在0附近、方差为1。
这一步用StandardScaler实现:
# 先划分训练集和测试集,再进行标准化 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 标准化:用训练集的均值和标准差去标准化训练集和测试集 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 转为PyTorch张量 X_train = torch.tensor(X_train, dtype=torch.float32) X_test = torch.tensor(X_test, dtype=torch.float32) y_train = torch.tensor(y_train, dtype=torch.long) y_test = torch.tensor(y_test, dtype=torch.long) print("训练集大小:", X_train.shape, "测试集大小:", X_test.shape)这里有两个值得注意的细节。第一,划分数据时要加stratify=y,这是分层采样,保证训练集和测试集中三个类别的比例和原始数据一致,避免某个类别在测试集里特别多或特别少,从而让评估结果失真。第二,标准化时只能用训练集的均值和标准差去做fit和transform,再用同一个scaler去transform测试集。如果你对测试集也做fit_transform,相当于让模型在验证时偷看到了测试集的统计信息,会低估真实场景下的误差。
3.3 定义神经网络模型
用PyTorch定义网络的方式很直观:
class IrisNet(nn.Module): def __init__(self): super(IrisNet, self).__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) # 注意:输出层不加激活函数,直接输出logits return x model = IrisNet() print(model)输出:
IrisNet( (fc1): Linear(in_features=4, out_features=16, bias=True) (fc2): Linear(in_features=16, out_features=8, bias=True) (fc3): Linear(in_features=8, out_features=3, bias=True) (relu): ReLU() )有一个初学者很容易迷惑的地方:为什么输出层不加Softmax?原因是PyTorch的nn.CrossEntropyLoss在内部已经做了LogSoftmax和负对数似然的计算,所以训练时模型的原始输出(logits)直接传给损失函数就行。如果在输出层手动加了Softmax,再把结果传给CrossEntropyLoss,就会做两次Softmax,虽然数值上不完全等价,但会严重影响训练的数值稳定性,而且没必要。只有在推理阶段想得到“每个类别的概率”,你才需要额外对logits做一次Softmax。
3.4 训练循环:每一步在做的事
训练代码并不多,但每一行都有自己的职责。我先给出完整写法,然后逐段拆开解释:
# 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.01) # 训练轮次 epochs = 300 train_losses = [] train_accs = [] for epoch in range(epochs): # 1. 清空梯度 optimizer.zero_grad() # 2. 前向传播 outputs = model(X_train) loss = criterion(outputs, y_train) # 3. 反向传播 loss.backward() # 4. 更新参数 optimizer.step() # 记录训练损失和准确率 _, predicted = torch.max(outputs, 1) acc = (predicted == y_train).float().mean().item() train_losses.append(loss.item()) train_accs.append(acc) if (epoch + 1) % 50 == 0: print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}, Accuracy: {acc:.4f}")输出样式:
Epoch [50/300], Loss: 0.6421, Accuracy: 0.8571 Epoch [100/300], Loss: 0.4785, Accuracy: 0.9143 Epoch [150/300], Loss: 0.2968, Accuracy: 0.9714 Epoch [200/300], Loss: 0.1523, Accuracy: 0.9905 Epoch [250/300], Loss: 0.0972, Accuracy: 1.0000 Epoch [300/300], Loss: 0.0741, Accuracy: 1.0000每个步骤的意义:
第1步:
optimizer.zero_grad()。PyTorch的梯度是累加机制,如果不手动清零,上一个batch的梯度会叠加到当前batch的梯度上。在这个小数据集上我们相当于全量梯度下降,每次只用一份数据,但习惯上仍然要清零,防止在循环中出错。第2步:前向传播。把训练数据
X_train送入模型,得到每个样本在3个类别上的logits。outputs的形状是(105, 3)(训练集105条样本)。然后和真实标签y_train一起传入交叉熵损失函数,计算出一个标量损失。第3步:
loss.backward()。这一步自动计算损失对每个参数(权重和偏置)的梯度,并把结果保存在每个参数的grad属性里。你不需要自己推导链式法则,框架都做完了,但你要知道这个动作是在“计算下山方向”。第4步:
optimizer.step()。优化器根据param.grad和学习率,更新所有参数值。学习率lr=0.01是Adam优化器的一个常见初始值,对小数据集来说通常够用。
到这里,一轮训练就完成了。循环300轮的意义是让模型有足够多的“机会”去观察数据、修正参数。你可以看到损失在逐步下降、准确率逐步上升的轨迹,这个收敛过程正是神经网络学习的直观体现。
3.5 模型评估与分类报告
训练完成后,用测试集评估模型的泛化能力:
model.eval() # 进入评估模式 with torch.no_grad(): test_outputs = model(X_test) _, test_pred = torch.max(test_outputs, 1) test_acc = accuracy_score(y_test.numpy(), test_pred.numpy()) print(f"测试集准确率: {test_acc:.4f}") print("\n分类报告:") print(classification_report(y_test.numpy(), test_pred.numpy(), target_names=iris.target_names)) print("混淆矩阵:") print(confusion_matrix(y_test.numpy(), test_pred.numpy()))一个典型的输出:
测试集准确率: 0.9778 分类报告: precision recall f1-score support setosa 1.00 1.00 1.00 15 versicolor 0.93 1.00 0.96 15 virginica 1.00 0.93 0.96 15 accuracy 0.98 45 macro avg 0.98 0.98 0.98 45 weighted avg 0.98 0.98 0.98 45 混淆矩阵: [[15 0 0] [ 0 15 0] [ 0 1 14]]这里有两个容易忽略的代码习惯值得强调。第一个是model.eval()。这个操作会把Dropout和BatchNorm层切换到推理模式。虽然我们这个网络没有这两类层,但养成“评估前调用eval()”的习惯不会出错,以后迁移到更复杂的网络时能避免很多诡异问题。第二个是with torch.no_grad()。评估阶段不需要计算梯度,关掉梯度记录可以节省大量内存和计算时间,同时防止不小心调用backward()导致的意外错误。
从结果看,模型在测试集上表现相当好,Setosa全部分对,Versicolour有一个样本被错分成Virginica。这种轻微错误其实很正常,因为这两个类别在特征空间里本来就有重叠区域,即便人类手工分类也可能犯错。如果某个模型在鸢尾花测试集上做到100%准确率,也不用太惊讶,因为这个数据集本身比较简单,但作为入门,我们要关注的不是“刷分”,而是完整地理解从数据到模型的每个环节。
3.6 绘制训练过程的损失曲线
可视化是理解训练过程最直观的手段。把每轮的损失值画出来,能一眼看出模型是否收敛、是否出现过拟合或不稳定的迹象:
plt.figure(figsize=(8, 4)) plt.plot(train_losses, label='Train Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.title('训练损失下降曲线') plt.legend() plt.grid(True) plt.show()如果你看到损失曲线持续下降后趋于平缓,说明模型已经收敛。如果损失在某个区间反复震荡、噪声很大,可能是学习率过高,可以尝试调低。如果损失降到很低后测试集准确率反而下降,那就典型的过拟合信号,需要引入正则化手段(Dropout、权重衰减)或减小模型容量。
4. 常见问题与排查技巧实录
4.1 损失不下降,卡在一个固定值附近
这是初学者最常遇到的问题之一。如果你发现损失从第一轮开始就几乎不动,需要按顺序检查以下三点:
第一,确认标签类型和输出层维度匹配。nn.CrossEntropyLoss要求标签是long类型,值域是0到类别数减一。如果你把标签传成了float32类型,PyTorch会直接报错;如果标签值域超过类别数,比如输出了标签3但网络只有3个输出节点(索引0、1、2),也会报错或出现异常损失。
第二,确认学习率没有设置得过小。学习率太小会让参数更新极其缓慢,损失看似卡住不动。可以尝试把lr从0.01调到0.1或0.3观察是否有改善,或者改用lr=0.001配合更多训练轮次。
第三,看数据是否标准化。如果忘记对输入做标准化,特征尺度差异大,神经网络很可能会出现训练不稳定、损失不降的情况。我见过不少同学的代码,跑起来损失从一大串数字开始不断下降,好像“正常”,但准确率却一直很低,原因往往就是没标准化。
4.2 训练集准确率100%,测试集却只有80%多
这是典型的过拟合表现,但放在鸢尾花这种小数据集上,往往不是模型太复杂,而是训练集太小导致模型记住了噪声。150条样本,划分后训练集只有105条,一个16+8的隐藏层网络在这样小的数据上确实有记忆能力。
复盘时可以用下面几个办法逐步缓解:
- 减少隐藏层神经元数量,比如改成
nn.Linear(4, 8)单隐藏层结构。 - 在隐藏层后加Dropout,比如
nn.Dropout(0.2),强制模型不依赖单个神经元的输出。 - 增加训练数据扩充手段,但鸢尾花本身就是固定数据集,没有额外数据可用,所以这个方法在这里不适用。
- 在优化器中加入权重衰减参数,比如
optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4)。
不过说实话,在鸢尾花这个数据集上,只要训练流程正确、数据标准化做了,即便出现过拟合也不会太严重。这里的核心是想让你明白:过拟合本质上是模型容量和数据量不匹配,解决办法有从数据角度入手的,也有从模型角度入手的,需要灵活取舍。
4.3 用Sigmoid替代ReLU后训练变慢了很多
如果你实验过把隐藏层激活函数从ReLU换成Sigmoid,可能会发现损失下降明显变慢。这背后的原因是Sigmoid函数在输入绝对值较大时梯度趋近于0,导致反向传播时梯度“消失”,参数几乎得不到有效更新。而ReLU在正区间的梯度恒为1,能更好地把损失信息传到前层。
这个现象说明一个道理:激活函数不是随便选的。在隐藏层,ReLU及其变体(LeakyReLU、ELU等)几乎总是优于Sigmoid和tanh;在输出层,根据任务选择Sigmoid(二分类概率)或线性激活(回归)或Softmax(多分类概率),这些都是有明确依据的,而不是凭感觉换着试。
4.4 同样的代码重复运行,结果不一样
这不是bug,而是神经网络的本质特征。模型参数是随机初始化的,不同初始点会收敛到不同的局部最优解;数据划分时的随机种子也会影响训练集和测试集的组成,进而影响最终分数。
想让结果可复现,只需要在代码开头加上:
torch.manual_seed(42) np.random.seed(42)如果还用了train_test_split,也可以给它传random_state=42。这样多次运行的初始条件完全一致,结果就能稳定复现。注意,在GPU上完全复现会比CPU上困难一些,因为某些并行操作存在非确定性,但对这个项目来说CPU复现完全够用。
4.5 损失函数已经很低,但准确率不升反降
这种情况往往不是模型问题,而是评估方式和训练目标不一致。比如训练时用的是交叉熵损失,但你误用了MSE,导致模型优化方向和分类目标错位。还有一种可能是,你手动在输出层加了Softmax导致梯度不稳定,或者评估时忘记调用model.eval(),让Dropout在推理时仍然生效,结果随机性变大。
如果你确认代码逻辑没问题,但准确率就是上不去,可以试着把训练轮次增加、降低学习率、增加隐藏层容量,逐一排查。
5. 写在最后:这套流程还能往哪走
如果你完整地跑通了上面的代码,并且能说出每一行在做什么,那么恭喜你,你已经跨进了神经网络实用门槛。很多人刚开始学深度学习,沉迷于各种高大上的模型名称,却忽视了最基础的训练流程,结果代码一报错就手足无措。鸢尾花这个项目,本质上是一套可以复用到任何表格型分类任务的操作框架:加载数据→标准化→划分训练测试集→定义网络→定义损失和优化器→循环训练→评估。
按照我个人带项目的经验,初学阶段最值得投入时间的不是急着上卷积神经网络或Transformer,而是把一个简单的全连接网络吃透。你可以在这个基础上做几个小扩展,难度都不高,但收获很大:
- 把手写的训练循环改用PyTorch的
DataLoader和Dataset,为以后处理更大数据集做准备。 - 尝试把网络层数加深,看准确率和训练时间的权衡关系。
- 把模型保存下来(
torch.save),再写一段加载预测的代码,体会模型部署的基本流程。 - 加一个L2正则化或Dropout,对比过拟合程度的变化。
动手试一遍,比看十遍教程都管用。等你能独立完成这些小扩展,再去看CNN、RNN或者Transformer,会觉得很多概念都是熟悉的——因为底层的训练机制,永远是那套前向传播、反向传播、梯度下降的老三样。