资讯动态

深度学习数学函数实战指南:从激活函数到优化器的原理与代码实现

发布时间:2026/10/13 15:25:16 来源:尧图企业网站定制
1. 为什么搞深度学习还得回头翻数学课本刚入门深度学习那会儿我也觉得数学是拦路虎。打开一篇讲反向传播的博客满屏的偏导数、链式法则、雅可比矩阵看得人头皮发麻。后来硬着头皮啃了几个月代码跑通了模型也训起来了回头再看那些公式突然有种“原来你在这儿等着我”的感觉。这篇内容就是把我自己踩过的坑、绕过的弯整理出来把深度学习里真正高频出现、绕不开的那些数学函数一个一个掰开揉碎讲清楚。不管你是刚学完Python基础想往AI方向走还是已经能跑通几个Demo但看到公式就跳过这篇都适合你。我会尽量用生活化的类比加上可直接运行的代码片段让你不仅知道这个函数长什么样更知道它为什么长这样、在模型里到底干了什么活。深度学习说到底就是一件事找一个函数让它在给定输入下输出我们想要的结果。这个“找”的过程就是不断调整参数让预测值和真实值之间的差距变小。而衡量差距、调整参数、更新权重每一步都离不开具体的数学函数。你可以把神经网络想象成一个巨大的调料台每个神经元就是一味调料激活函数决定这味料加不加、加多少损失函数是最后尝一口判断味道差在哪优化函数则是根据差在哪决定下一勺怎么调。这一整套流程背后全是数学函数在支撑。很多人学深度学习喜欢直接上框架model.fit()一调结果出来了但模型为什么不收敛、梯度为什么消失、损失为什么震荡一概不知。遇到问题只能瞎试调参全靠玄学。我的经验是把核心数学函数搞明白至少能让你在模型出问题的时候知道该往哪个方向排查。比如损失不下降你会想到是不是激活函数选错了导致梯度太小比如训练后期loss突然爆炸你会怀疑是不是学习率配合优化器出了问题。这种“知道为什么”的能力才是从调包侠进阶到真正理解模型的关键。接下来我会按照“这个函数是什么、数学表达式长什么样、代码怎么实现、在深度学习里用在哪、有什么坑”这个逻辑把线性函数、激活函数、损失函数、优化相关函数这几大类逐个讲透。每个函数都会配上PyTorch或NumPy的代码示例你可以直接复制到自己的环境里跑一遍亲眼看看输入输出长什么样。这比只看公式直观得多。2. 线性函数与非线性激活神经网络的骨架与灵魂2.1 线性函数最基础但最容易被轻视的组件线性函数是神经网络里最底层的运算单元。一个神经元做的事情本质上就是先把输入特征加权求和再加上一个偏置项。用数学语言写出来就是 ( y wx b )其中 ( w ) 是权重( b ) 是偏置( x ) 是输入。如果你把多个神经元排成一层那就变成了矩阵乘法形式 ( Y XW b )这里的 ( X ) 是输入矩阵( W ) 是权重矩阵( b ) 是偏置向量。为什么线性函数这么重要因为它决定了神经网络如何从输入空间中提取信息。每一个权重 ( w ) 代表对应特征的重要性偏置 ( b ) 则控制神经元激活的阈值。训练的过程就是不断调整这些 ( w ) 和 ( b )让网络输出越来越接近真实值。但线性函数有个致命问题多个线性层堆叠起来本质上还是一个线性变换。你用两层线性网络去拟合一个非线性关系结果和一层线性网络没区别。这就是为什么必须引入非线性激活函数。没有激活函数再深的网络也只是一个大型线性回归模型。用NumPy实现一个简单的线性层import numpy as np def linear_forward(x, w, b): x: 输入形状 (batch_size, in_features) w: 权重形状 (in_features, out_features) b: 偏置形状 (out_features,) return np.dot(x, w) b # 示例 x np.array([[1.0, 2.0], [3.0, 4.0]]) w np.array([[0.5, -0.3], [0.8, 0.2]]) b np.array([0.1, -0.1]) output linear_forward(x, w, b) print(output)这段代码跑出来的结果就是两个神经元对两个样本的响应。你可以手动算一下验证比如第一个样本的第一个输出是 ( 1.0 \times 0.5 2.0 \times 0.8 0.1 2.2 )。这种手动验证的习惯在初学阶段特别有用能帮你建立对矩阵维度和运算的直觉。注意线性层的权重初始化不能全设为0否则所有神经元的输出都一样反向传播时梯度也相同网络永远学不到东西。常见的做法是用高斯分布随机初始化或者用Xavier、He初始化方法根据输入输出维度自动调整方差。2.2 Sigmoid函数曾经的主流现在的边缘角色Sigmoid函数的数学表达式是 ( \sigma(x) \frac{1}{1 e^{-x}} )。它的输出范围是(0, 1)形状像一个拉长的S。在早期神经网络里Sigmoid几乎是激活函数的默认选择因为它能把任意实数压缩到0到1之间很适合表示概率。但Sigmoid有两个大问题。第一是梯度消失当输入很大或很小时Sigmoid的导数趋近于0反向传播时梯度一层层乘下去传到前面几层就几乎没了导致浅层参数更新极慢。第二是输出不是零中心的Sigmoid的输出恒大于0这意味着后一层的输入总是正数梯度更新方向会受限收敛速度变慢。用代码看看Sigmoid的导数有多小import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def sigmoid_derivative(x): s sigmoid(x) return s * (1 - s) x np.array([-10, -5, 0, 5, 10]) print(sigmoid值:, sigmoid(x)) print(sigmoid导数:, sigmoid_derivative(x))跑一下你会发现当x10时导数只有约0.000045几乎为零。这就是梯度消失的直观体现。所以现在除了二分类的输出层偶尔用Sigmoid隐藏层基本不会用它了。2.3 Tanh函数零中心但仍有梯度消失Tanh函数是 ( \tanh(x) \frac{e^x - e^{-x}}{e^x e^{-x}} )输出范围是(-1, 1)。它解决了Sigmoid输出非零中心的问题但梯度消失依然存在。当输入绝对值较大时Tanh的导数同样趋近于0。在实际项目中Tanh常用于RNN的隐藏层因为它的输出范围对称能更好地处理序列数据中的正负信息。但在CNN和全连接网络中ReLU系列已经全面取代了Tanh。2.4 ReLU家族现代深度学习的标配ReLURectified Linear Unit的表达式简单到令人发指( \text{ReLU}(x) \max(0, x) )。输入大于0就原样输出小于等于0就输出0。就这么一个简单的函数解决了梯度消失的大问题因为当输入为正时导数恒为1梯度可以畅通无阻地反向传播。但ReLU也有自己的坑。当输入为负时输出为0导数也为0这个神经元可能永远不再被激活相当于“死掉了”。如果学习率设得太大很多神经元会在训练初期就进入这种死亡状态导致网络容量下降。为了解决这个问题后续出现了Leaky ReLU、PReLU、ELU等变体。Leaky ReLU在负半轴给一个很小的斜率比如0.01保证梯度不为零。PReLU则把这个斜率作为可学习参数让网络自己决定负半轴怎么处理。import torch import torch.nn as nn # 几种ReLU变体的对比 relu nn.ReLU() leaky_relu nn.LeakyReLU(negative_slope0.01) prelu nn.PReLU() elu nn.ELU(alpha1.0) x torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0]) print(ReLU:, relu(x)) print(Leaky ReLU:, leaky_relu(x)) print(PReLU:, prelu(x)) print(ELU:, elu(x))实测下来大部分情况下直接用ReLU就够了简单高效。如果发现训练过程中损失下降很慢或者很多神经元输出为0可以试试Leaky ReLU。ELU在理论上更优雅但计算量稍大实际收益不一定明显。实操心得用ReLU时学习率不要设得太大一般从0.001开始试。如果发现损失一直不降可以打印每一层输出的均值看看是不是大量神经元输出为0。如果是要么降低学习率要么换Leaky ReLU。2.5 Softmax函数多分类的输出担当Softmax函数把一组实数转换成概率分布公式是 ( \text{Softmax}(x_i) \frac{e^{x_i}}{\sum_j e^{x_j}} )。它的作用是让所有输出加起来等于1每个输出代表对应类别的概率。多分类任务的最后一层基本都用Softmax。Softmax有一个数值稳定性问题当某个 ( x_i ) 特别大时( e^{x_i} ) 会溢出。实际实现中通常会先减去最大值再计算即 ( \text{Softmax}(x_i) \frac{e^{x_i - \max(x)}}{\sum_j e^{x_j - \max(x)}} )。这样既不改变结果又避免了溢出。def softmax(x): x_shifted x - np.max(x, axis-1, keepdimsTrue) exp_x np.exp(x_shifted) return exp_x / np.sum(exp_x, axis-1, keepdimsTrue) logits np.array([2.0, 1.0, 0.1]) print(Softmax输出:, softmax(logits)) print(概率和:, np.sum(softmax(logits)))在PyTorch里通常不需要手动实现Softmax因为nn.CrossEntropyLoss已经把Softmax和负对数似然损失合在一起了数值上更稳定。但理解Softmax的原理对于调试模型输出、分析预测置信度很有帮助。3. 损失函数模型到底在优化什么3.1 均方误差回归任务的默认选择均方误差MSE的公式是 ( \text{MSE} \frac{1}{n} \sum_{i1}^{n} (y_i - \hat{y}_i)^2 )。它衡量的是预测值和真实值之间距离的平方的平均。平方的作用是让正负误差都变成正数同时放大大误差的惩罚力度。MSE的导数很简洁( \frac{\partial \text{MSE}}{\partial \hat{y}_i} \frac{2}{n} (\hat{y}_i - y_i) )。这个导数与误差成正比误差越大梯度越大参数更新幅度也越大。这符合直觉错得越离谱改得越狠。但MSE对异常值很敏感。如果数据里有个别极端值平方之后会主导整个损失导致模型偏向于拟合这些异常点。这时候可以考虑用MAE平均绝对误差或者Huber损失。Huber损失在误差小的时候用平方误差大的时候用绝对值兼顾了稳定性和收敛速度。import torch import torch.nn as nn mse_loss nn.MSELoss() mae_loss nn.L1Loss() huber_loss nn.HuberLoss(delta1.0) y_true torch.tensor([1.0, 2.0, 3.0]) y_pred torch.tensor([1.1, 1.9, 3.5]) print(MSE:, mse_loss(y_pred, y_true)) print(MAE:, mae_loss(y_pred, y_true)) print(Huber:, huber_loss(y_pred, y_true))3.2 交叉熵损失分类任务的核心交叉熵损失衡量的是两个概率分布之间的差异。对于二分类公式是 ( L -[y \log(\hat{y}) (1-y) \log(1-\hat{y})] )。对于多分类公式是 ( L -\sum_{c1}^{C} y_c \log(\hat{y}_c) )其中 ( y_c ) 是真实标签的one-hot编码( \hat{y}_c ) 是预测概率。交叉熵的物理意义是如果用预测分布去编码真实分布平均需要多少比特。预测越准交叉熵越小。当预测概率接近真实标签时( \log(\hat{y}) ) 接近0损失很小当预测完全错误时( \log(\hat{y}) ) 趋向负无穷损失爆炸。这种强烈的惩罚机制让模型有动力把正确类别的概率推高。在PyTorch中nn.CrossEntropyLoss接收的是未经过Softmax的logits内部会自动做Softmax和负对数似然。这样做的好处是数值稳定性更好避免先算Softmax再算log时可能出现的溢出。import torch import torch.nn as nn ce_loss nn.CrossEntropyLoss() # 三个样本三个类别 logits torch.tensor([[2.0, 1.0, 0.1], [0.5, 2.5, 0.3], [0.1, 0.2, 3.0]]) labels torch.tensor([0, 1, 2]) loss ce_loss(logits, labels) print(交叉熵损失:, loss.item())注意如果手动实现交叉熵一定要在log里面加一个极小值比如1e-10防止log(0)导致NaN。用框架自带的损失函数就不用担心这个问题。3.3 对比损失与三元组损失让模型学会“比较”有些任务不是要预测一个值或一个类别而是要判断两个输入是否相似。比如人脸验证、图像检索、文本匹配。这时候就需要对比损失Contrastive Loss或三元组损失Triplet Loss。对比损失的思路是如果两个样本是正对相似就让它们的特征距离尽量小如果是负对不相似就让距离大于一个阈值。公式是 ( L y \cdot d^2 (1-y) \cdot \max(0, m - d)^2 )其中 ( d ) 是特征距离( m ) 是阈值。三元组损失更直接取一个锚点、一个正样本、一个负样本让锚点到正样本的距离小于锚点到负样本的距离并且至少小一个边界值。公式是 ( L \max(0, d(a,p) - d(a,n) m) )。这两种损失在训练时有个关键技巧样本挖掘。随机选的三元组往往太简单模型学不到东西。需要专门挑那些“难”的样本即锚点到正样本的距离已经接近甚至大于锚点到负样本的距离。这种难样本挖掘能显著提升模型效果但也会增加训练时间。4. 优化函数参数是怎么一步步变好的4.1 梯度下降最朴素的优化思想梯度下降的核心思想是沿着损失函数梯度的反方向更新参数因为梯度指向函数上升最快的方向反方向就是下降最快的方向。公式是 ( \theta_{t1} \theta_t - \eta \nabla L(\theta_t) )其中 ( \eta ) 是学习率。学习率是梯度下降里最重要的超参数。太小了收敛慢太大了可能震荡甚至发散。我一般会先用一个中等大小的学习率比如0.01跑几百步观察损失曲线。如果损失下降很慢就适当调大如果损失上下跳动就调小。批量梯度下降用全部数据算梯度准确但慢。随机梯度下降每次只用一个样本快但噪声大。小批量梯度下降折中每次用一小批数据既保证了速度又降低了噪声。实际训练中batch size通常取32、64、128、256这些值具体取决于显存大小和数据集规模。4.2 动量法给优化器装上惯性动量法在梯度下降的基础上加了一个“速度”项让参数更新不仅看当前梯度还看之前的更新方向。公式是 ( v_t \beta v_{t-1} (1-\beta) \nabla L )( \theta_{t1} \theta_t - \eta v_t )。( \beta ) 通常取0.9。动量的好处是能加速收敛尤其是在梯度方向变化不大的情况下。它还能帮助跳出局部极小值和鞍点因为即使当前梯度为零之前积累的动量还能推着参数继续走。4.3 Adam自适应学习率的集大成者Adam结合了动量和RMSProp的思想为每个参数维护独立的学习率。它计算梯度的一阶矩估计和二阶矩估计然后做偏差校正。公式稍微复杂一些但核心思想是梯度变化大的参数学习率自动调小梯度变化小的参数学习率自动调大。Adam的默认参数是学习率0.001( \beta_10.9 )( \beta_20.999 )( \epsilon10^{-8} )。大部分情况下直接用默认值就能有不错的效果。但有些任务上Adam的泛化能力不如带动量的SGD尤其是在图像分类任务中。这时候可以试试先用Adam快速收敛再切换到SGD精调。import torch import torch.optim as optim model torch.nn.Linear(10, 2) # 几种优化器的对比 sgd optim.SGD(model.parameters(), lr0.01, momentum0.9) adam optim.Adam(model.parameters(), lr0.001) adamw optim.AdamW(model.parameters(), lr0.001, weight_decay0.01) rmsprop optim.RMSprop(model.parameters(), lr0.01) print(优化器初始化完成)实操心得AdamW是Adam的改进版把权重衰减从梯度更新中分离出来正则化效果更好。现在大部分Transformer类模型都用AdamW。如果你在做NLP任务直接上AdamW准没错。4.4 学习率调度让学习率动态变化固定学习率往往不是最优的。训练初期需要大学习率快速下降训练后期需要小学习率精细调整。学习率调度器就是干这个的。常见的调度策略有StepLR每隔固定轮数把学习率乘以一个衰减系数CosineAnnealingLR让学习率按余弦曲线从大到小变化ReduceLROnPlateau在损失不再下降时自动降低学习率。from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR, ReduceLROnPlateau optimizer optim.Adam(model.parameters(), lr0.001) # 每30轮学习率乘以0.1 scheduler_step StepLR(optimizer, step_size30, gamma0.1) # 余弦退火 scheduler_cosine CosineAnnealingLR(optimizer, T_max100) # 损失不降时降低学习率 scheduler_plateau ReduceLROnPlateau(optimizer, modemin, factor0.1, patience10)我个人的习惯是先用CosineAnnealingLR跑一个完整的训练周期看看损失曲线。如果发现后期损失还在下降说明训练不够可以增加轮数如果发现损失早早平稳说明模型容量不够或者数据有问题。5. 常见问题与排查技巧实录5.1 损失不下降怎么办这是最常见的问题。排查顺序一般是先检查数据有没有问题比如标签是不是错了、输入是不是归一化了再检查模型结构比如输出维度对不对、激活函数有没有漏掉然后检查学习率试试调大调小各跑几百步看看最后检查损失函数分类任务用交叉熵回归任务用MSE别搞反了。有个容易被忽略的点如果最后一层是线性层但任务是多分类忘了加Softmax或者用了MSE损失损失也会不下降。因为MSE对分类任务的梯度性质不好容易陷入平坦区域。5.2 损失变成NaN了NaN通常意味着数值溢出或除零。常见原因有学习率太大导致参数更新爆炸log里面出现了0除以了0梯度爆炸导致权重变成无穷大。排查方法先把学习率调小一个数量级试试检查损失函数里有没有log或除法操作加个极小值保护用torch.nn.utils.clip_grad_norm_做梯度裁剪限制梯度范数。# 梯度裁剪示例 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step()5.3 训练集表现好但验证集很差这是典型的过拟合。解决方法有增加数据量、做数据增强、加Dropout层、加L2正则化、减小模型容量、早停。Dropout是我最常用的手段实现简单效果明显。在PyTorch里直接在层之间加nn.Dropout(p0.5)就行。注意Dropout只在训练时生效推理时要关掉用model.eval()切换。5.4 梯度消失或梯度爆炸梯度消失表现为浅层参数几乎不更新梯度爆炸表现为损失突然变成NaN。排查方法是打印每一层的梯度范数看看哪一层的梯度异常小或异常大。梯度消失的解决方案用ReLU替代Sigmoid/Tanh用BatchNorm用残差连接。梯度爆炸的解决方案梯度裁剪降低学习率用权重归一化。5.5 常见问题速查表问题现象可能原因排查方法解决方案损失不下降学习率太小、数据未归一化、模型结构错误打印输入输出统计量、检查标签调大学习率、归一化数据、修正模型损失变NaN学习率太大、log(0)、梯度爆炸检查损失函数、打印梯度范数调小学习率、加极小值、梯度裁剪训练好验证差过拟合对比训练验证损失曲线加Dropout、L2、数据增强、早停梯度消失激活函数饱和、网络太深打印各层梯度范数换ReLU、加BatchNorm、残差连接梯度爆炸学习率太大、权重初始化不当打印梯度范数梯度裁剪、降低学习率、换初始化预测全为同一类数据不平衡、损失函数不当统计标签分布重采样、加权损失、换损失函数最后再分享一个小技巧训练模型时先在一个很小的子集上跑比如几十个样本看看模型能不能过拟合。如果连小数据集都过拟合不了说明模型或代码有问题别急着上全量数据。这个“小数据过拟合测试”能帮你快速定位大部分低级错误。6. 把数学函数串起来看一个完整的前向与反向流程6.1 前向传播从输入到损失假设我们有一个两层全连接网络做十分类任务。前向传播的流程是输入经过第一层线性变换得到隐藏层输出隐藏层经过ReLU激活再经过第二层线性变换得到输出层logits最后用交叉熵损失计算预测和真实标签的差距。用代码表示就是import torch import torch.nn as nn class TwoLayerNet(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim): super().__init__() self.fc1 nn.Linear(in_dim, hidden_dim) self.relu nn.ReLU() self.fc2 nn.Linear(hidden_dim, out_dim) def forward(self, x): h self.fc1(x) h self.relu(h) logits self.fc2(h) return logits model TwoLayerNet(784, 256, 10) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) # 模拟一个batch x torch.randn(32, 784) y torch.randint(0, 10, (32,)) logits model(x) loss criterion(logits, y) print(损失值:, loss.item())6.2 反向传播链式法则的实际应用反向传播的本质就是链式法则。损失对第一层权重的梯度等于损失对输出的梯度乘以输出对隐藏层的梯度再乘以隐藏层对第一层权重的梯度。每一层都把自己的局部梯度传给前一层层层递推。PyTorch的自动求导机制帮我们处理了这些繁琐的链式运算。调用loss.backward()之后所有参数的梯度就计算好了存在param.grad里。我们只需要调用optimizer.step()更新参数再调用optimizer.zero_grad()清空梯度准备下一轮。# 完整训练步骤 for epoch in range(10): optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() print(fEpoch {epoch}, Loss: {loss.item():.4f})6.3 数学函数在其中的角色总结线性函数负责特征变换激活函数引入非线性损失函数定义优化目标优化器决定参数更新方式。这四个环节环环相扣任何一个出问题都会影响最终效果。我个人的体会是刚开始学的时候不用追求把每个公式都推导一遍但至少要理解每个函数的作用和适用场景。遇到模型不收敛的时候能根据现象快速定位到是哪个环节出了问题。这种“对症下药”的能力比死记公式有用得多。后续如果想深入可以看看每个函数的梯度推导过程自己用NumPy实现一遍简单的前向和反向传播。手写一遍之后对框架里那些backward()到底干了什么会有完全不一样的理解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑