梯度提升分类树损失函数化简:从对数损失到伪残差与叶子权重

做分类任务时第一次手动推导梯度提升树的损失函数,我对着满屏的公式发了半小时呆。原因很简单:梯度提升回归树的残差是 y - f(x),一目了然,但分类树的“残差”却藏在损失函数的导数里,不把对数几率损失展开化简,根本看不出来它到底在拟合什么。这篇文章就是把那段化简过程完整记录下来——从损失函数的原始形态出发,一步步整理到每个叶子节点上该填什么值,以及最终预测概率是如何还原的。无论你是想自己实现一个简易版GBDT,还是单纯想弄明白开源库内部的分裂增益和叶子权重是怎么算出来的,这篇内容都值得你对着一杯咖啡慢慢读。

1. 梯度提升分类树的核心:到底在拟合什么

1.1 梯度提升的本质是沿着损失下降的方向修正模型

想理解损失函数化简,先得把梯度提升的整体逻辑掰开揉碎。梯度提升树不是一棵树在战斗,而是用 M 棵树做加法:

F(x) = F₀(x) + η * h₁(x) + η * h₂(x) + ... + η * h_M(x)

每一轮迭代的目标,是找一棵新树 h_m(x),让它尽可能弥补当前模型 F_{m-1}(x) 的不足。回归任务里,这个“不足”就是残差 y - F(x),直接算出来就能当训练目标。但分类任务里,模型输出的是样本属于正类的对数几率 f(x),不是概率本身,所以“残差”不能直接定义。这时就要靠损失函数的负梯度来充当“伪残差”——这是梯度提升家族统一的思路,也是化简的起点。

1.2 回归与分类的本质差异:从连续数值到概率输出

回归树拟合的是连续数值,平方损失 L = (y - f)² 的负梯度是 2(y - f),残差天然存在。分类树则不同,它的输出 f(x) 要经过 sigmoid 或 softmax 转换成概率,损失函数也不是平方形式,而是交叉熵。

交叉熵的妙处在于:它对“置信度”敏感。预测概率 p=0.6 但真实标签 y=1,损失是 -log(0.6) ≈ 0.51;预测 p=0.1 但真实 y=1,损失是 -log(0.1) ≈ 2.30。这种非线性惩罚让模型对错误分类更警惕,也是分类损失函数能化简出优美形式的基础。

1.3 为什么分类场景默认选择对数几率损失

有人说分类任务可以用指数损失(AdaBoost那种),也可以用平方损失硬套。实际工程里,对数几率损失是默认选项,原因有三:

  • 数学性质好:对 f 的一阶导和二阶导都有简洁闭式解,方便后续牛顿步长计算。
  • 输出可解释:模型输出经 sigmoid 转换后直接是概率,且对数几率与贝叶斯最优决策天然对齐。
  • 对异常值稳健:相比平方损失,交叉熵在预测极端错误时的惩罚是线性的,不会产生过大梯度导致树被个别样本带偏。

注意:对数几率损失中的 p 是模型当前预测概率,不是真实分布。真实分布由标签 y 的 one-hot 编码提供。

2. 从原始对数损失到负梯度:发现“伪残差” y - p

2.1 二分类对数损失的标准表达式拆解

二分类的损失函数长这样:

L = -[y · log(p) + (1-y) · log(1-p)]

其中 y ∈ {0, 1},p = P(y=1 | x) = σ(f(x)) = 1 / (1 + e^{-f})。表面看这个式子对 p 求导就行,但梯度提升的模型输出是 f,不是 p,所以必须把 p 替换成 f 的表达式,再对 f 求导。

这里有个新手常犯的混淆:对 p 求导和对 f 求导结果完全不同。很多资料直接说“残差是 y - p”,跳过了中间推导,导致读者只知道结论不知道为什么。实际上,链条法拆开就清楚了:

∂L/∂f = (∂L/∂p) · (∂p/∂f)

第一步,∂L/∂p = -y/p + (1-y)/(1-p),化成一个分式:= (p - y) / (p(1-p))。

第二步,p 对 f 求导,sigmoid 的导数性质是 dp/df = p(1-p)。

两者相乘,p(1-p) 直接约掉,剩下:

∂L/∂f = p - y

于是负梯度(也就是我们要拟合的伪残差)就是:

r = -∂L/∂f = y - p

这个化简过程堪称优雅——交叉熵经过 sigmoid 链式传导后,分母的 p(1-p) 被精确抵消,只剩下预测概率与真实标签的差值。这就是分类梯度提升树的“残差”真面目。

2.2 叶节点分裂时的关键:引入二阶导

如果只做到负梯度,那只是拟合了梯度方向。但梯度提升树建树过程中,无论是分裂增益计算还是叶子节点取值,都需要二阶导。因为树的节点分裂不是单纯回归拟合,而是对损失函数做二阶泰勒展开时,用一阶梯度和二阶梯度的统计量来决定“这个节点值到底填多少损失下降最大”。

对 f 再求一次导:

∂²L/∂f² = ∂/∂f (p - y) = dp/df = p(1-p)

于是每个样本身上背着一阶梯度 g = p - y 和二阶梯度 h = p(1-p)。在建树时,节点分裂的增益公式用的是:

Gain = (G_L² / (H_L + λ)) + (G_R² / (H_R + λ)) - (G² / (H + λ))

其中 G 是节点内所有样本 g 的和,H 是 h 的和,λ 是正则化系数。因为 h = p(1-p) 恒非负,所以分母不会出现负数或者零的尴尬,分裂计算非常稳定,这也是选对数损失的一个隐性福利——换成指数损失,二阶导就没有这么规整。

2.3 三行代码验证化简结果

我在自己写了一个最小实现时,用一组假数据验证了这个推导:

import numpy as np y = np.array([1, 0, 1, 0, 1]) f = np.array([0.2, -0.1, 0.5, -0.3, 0.8]) p = 1 / (1 + np.exp(-f)) # 手动算负梯度 neg_grad = y - p # 数值差分验证 dL/df eps = 1e-6 f_plus = f + eps f_minus = f - eps p_plus = 1 / (1 + np.exp(-f_plus)) p_minus = 1 / (1 + np.exp(-f_minus)) L_plus = -(y * np.log(p_plus + 1e-12) + (1-y) * np.log(1 - p_plus + 1e-12)) L_minus = -(y * np.log(p_minus + 1e-12) + (1-y) * np.log(1 - p_minus + 1e-12)) numerical_grad = (L_plus - L_minus) / (2 * eps) print("解析梯度:", (p - y)) print("数值梯度:", numerical_grad)

跑一遍会发现两者差异在 1e-9 级别,说明推导完全正确。这种验证习惯建议保留——尤其是自己改损失函数做自定义目标时,数值差分是检验公式推导的硬标准。

3. 叶子节点填充值:从牛顿步长到化简闭式解

3.1 为什么叶子节点不能直接填伪残差均值

有些简化的GBDT讲解会把叶子节点值设为该节点内伪残差的均值。这在单纯MSE损失下成立,但换成交叉熵就不对。原因是梯度提升用的不是梯度下降的普通步长,而是在每个叶子节点上求解一个“损失函数最小化”的子问题。

叶子节点上的优化是:已知一组样本落入该叶子节点,它们的当前模型输出都是 f(上一轮的累积预测),现在要找一个增量 w,让新损失最小。

对每个样本,损失从 L(f) 变成 L(f + w),做二阶泰勒展开:

L(f + w) ≈ L(f) + g·w + 0.5·h·w²

所有样本求和,并对 w 求导置零:

∑(g + h·w) = 0

解出来:

w = -∑g / ∑h

代入 g = p - y,h = p(1-p):

w = ∑(y - p) / ∑(p(1-p))

这就是叶子节点的闭式解。分子是该节点内所有样本伪残差之和,分母是所有样本 p(1-p) 之和。这个值不是残差均值,而是“残差总和除以二阶梯度和”——本质是牛顿法的一次迭代步长。

3.2 数值示例:三样本叶子节点计算全过程

假设某个叶子节点落入3个样本,真实标签 y = [1, 0, 1],上一轮模型输出都是 f = 0.2:

p = σ(0.2) = 0.5498

逐个计算: 样本1:y=1,g = 0.5498 - 1 = -0.4502,h = 0.5498 × 0.4502 = 0.2475 样本2:y=0,g = 0.5498 - 0 = 0.5498,h = 0.2475 样本3:y=1,g = -0.4502,h = 0.2475

∑g = -0.4502 + 0.5498 - 0.4502 = -0.3506 ∑h = 0.2475 + 0.2475 + 0.2475 = 0.7425

w = -∑g / ∑h = 0.3506 / 0.7425 = 0.4722

所以这棵树的这个叶子节点输出值应该是 0.4722,而不是伪残差均值 (-0.4502 + 0.5498 - 0.4502)/3 ≈ -0.1169。两者方向甚至都可能不同。这就是为什么必须用二阶导加权。

3.3 实际库实现与理论公式的对应关系

我在验证某开源梯度提升库的源码时,发现它内部维护的grad和hess数组,分别对应这里的 g 和 h。构建直方图时,每个特征分箱累加的也是 G 和 H。所以在源码层面,“损失函数化简”不仅仅是个数学游戏——它直接决定了分裂查找的数据结构设计:每个节点只需要维护两个统计量,任何特征的分裂评估都可以用常数时间完成。

还有一点容易忽视:实际库在计算 w 的时候通常会加正则项 λ。公式变成 w = -∑g / (∑h + λ)。λ 本质是对叶子权重做 L2 收缩,防止单棵树过拟合。这跟线性回归里的岭参数是同一个道理,只是跑到树模型里了。

提示:如果你自己实现GBDT时遇到训练震荡、loss 不降的情况,检查一下叶子节点计算是否漏了二阶导分母。直接填残差均值是新手最容易犯的错误。

4. 多分类的化简链条:Softmax 与 K 组对数损失

4.1 从二分类到 K 分类:需要 K 组输出

多分类不能只用一个 f 了,模型输出是一个 K 维向量 (f₁, f₂, ..., f_K),经过 softmax 变成概率分布:

p_k = e^{f_k} / ∑_{j=1}^K e^{f_j}

损失函数是 K 个类别的交叉熵总和:

L = -∑_{k=1}^K y_k · log(p_k)

其中 y_k 是 one-hot 标签,只有样本真实类别那一维是 1,其余是 0。目标是对每个 f_k 求偏导。

4.2 对第 k 维求导:softmax 的雅可比矩阵化简

求 ∂L/∂f_k。先看 L 对 p_j 的偏导:

∂L/∂p_j = -y_j / p_j

再看 p_j 对 f_k 的偏导。softmax 有一个著名性质,分两种情况:

当 j = k 时:∂p_k/∂f_k = p_k(1 - p_k) 当 j ≠ k 时:∂p_j/∂f_k = -p_j p_k

用链式法则把 L 对 f_k 的偏导展开:

∂L/∂f_k = ∑_{j=1}^K (∂L/∂p_j) · (∂p_j/∂f_k)

= (-y_k / p_k) · p_k(1-p_k) + ∑_{j≠k} (-y_j / p_j) · (-p_j p_k)

= -y_k(1-p_k) + p_k · ∑_{j≠k} y_j

由于 one-hot 标签中 ∑_{j≠k} y_j = 1 - y_k(要么 k 是真实类,要么其他类中恰有一个是1),所以:

∂L/∂f_k = -y_k(1-p_k) + p_k(1-y_k) = p_k - y_k

负梯度自然就是 y_k - p_k。形式和二分类完全统一,只是现在每个类别都有一个这样的残差。某个开源库在多分类时会输出 shape 为 (n_samples, n_classes) 的梯度矩阵,每一列就是这个类别的伪残差。

4.3 多分类叶子权重的闭式解与对角近似

多分类的每个叶子节点需要输出一个 K 维向量。求解思路与二分类一致,但二阶导变成 Hessian 矩阵:

H 的每个元素 H_ij = ∂²L/(∂f_i ∂f_j)

可以推出: 当 i = j 时,∂²L/∂f_i² = p_i(1 - p_i) 当 i ≠ j 时,∂²L/(∂f_i ∂f_j) = -p_i p_j

实践中几乎都采用对角近似——只保留 H_ii = p_i(1-p_i),忽略交叉项。这有两个原因:

  • 计算量可控:叶子节点权重变成逐维度独立计算 w_k = ∑(y_k - p_k) / ∑(p_k(1-p_k)),不用求 K×K 矩阵的逆。
  • 效果损失可接受:交叉项在概率值较小时数值很小,忽略后对最终预测精度影响有限,几乎所有主流实现都这么做。

4.4 初始预测怎么定:从零开始的第一棵树

梯度提升的初始化 F₀ 不是设为常数0,而是直接从损失函数最小化求解。二分类时,对所有样本求 L = -∑[y log p + (1-y) log(1-p)] 关于常数 f 的偏导,置零可得:

f₀ = log(p̄ / (1 - p̄))

其中 p̄ 是训练集正样本占比。如果数据集正样本率30%,初始预测是 log(0.3/0.7) ≈ -0.847。这样一开始模型就有合理基准,第一棵树的伪残差是以这个基准为起点的,不是从零出发的空转。

多分类类似:第 k 类的初始预测是 log(该类别样本占比),也就是经验先验。自己实现时,如果省略这一步直接 f₀=0,训练初期损失下降会慢很多,因为前面的树得额外学一个常数偏移。

实用建议:在做类别不平衡分类时,把初始 f₀ 设为标签分布的 log-odds,然后再叠加树模型。很多时候这比直接调整样本权重更平滑,模型收敛更快。

5. 实操中的坑与避坑记录

5.1 数值稳定性:sigmoid 和 softmax 的溢出问题

化简过程在数学上完美,但计算机浮点数不这么想。f 很大(比如 f=20)时,e^{-f} 几乎等于0,sigmoid 算出来就是1。f 很小(f=-20)时,e^{-f} 溢出。实际代码里需要做分段处理:

def stable_sigmoid(f): return np.where(f >= 0, 1 / (1 + np.exp(-f)), np.exp(f) / (1 + np.exp(f)))

softmax 同理,实现时必须减掉最大值:

def stable_softmax(f): f_max = np.max(f, axis=1, keepdims=True) exp_f = np.exp(f - f_max) return exp_f / np.sum(exp_f, axis=1, keepdims=True)

不要小看这一步。我在实践里遇到过用朴素实现训练上千棵树后,概率输出出现 NaN,查了半天发现就是某个极端样本的 f 值导致 e^{-f} 溢出。加上稳定性处理后再也没出过问题。

5.2 学习率与负梯度的缩放关系

标准梯度提升里,每棵树的输出会乘以学习率 η(也叫收缩率),公式是 F_m = F_{m-1} + η * h_m。注意 η 乘在“树的输出值”上,不是乘在负梯度上。

有些实现为了对齐某些库的语义,会把 η 直接乘在负梯度上再建树。这在单步优化上等价,但会影响后续分裂时的梯度统计量尺度,实际效果略有差异。我的建议是保持标准做法:负梯度只作为训练目标来建树,树建完后输出叶子权重,最后统一乘 η。

η 的大小直接决定需要多少棵树。η=0.1 通常要几百棵树才能收敛,η=1 可能几十棵就过拟合。实际调参时先固定 η=0.1,用早停机制决定树的数量,比单独调 η 更高效。

5.3 损失函数化简结果在回调函数里的应用

我常用的一个调试技巧:训练过程中监控训练集和验证集上的平均伪残差绝对值。如果训练集上一阶梯度绝对值已经普遍小于 0.01,说明模型基本拟合到位了;如果验证集梯度绝对值还很大且持续不降,说明模型容量不足或特征质量差。这个指标比直接看 loss 曲线更直观,因为 loss 是累计值,梯度是逐样本的。

另外,自定义目标函数时,化简的中间结果(g 和 h)就是你要返回的两个数组。如果你需要做排序、加权或分组等个性化操作,记住一条铁律:g 和 h 必须基于当前模型的 f 重新计算,不能缓存——每一轮迭代的 f 都在变化,伪残差跟着变,这是梯度提升的内在逻辑,也是很多人改出 bug 的地方。

5.4 化简过程给到的额外启发:样本加权与正则化

推导过程中 ∑g / ∑h 这个形态让我想到另一个点:gbdt 里样本加权其实有两种等价实现——一是直接对损失加权,二是对 g 和 h 乘权重。但加权后的 h 不能失去正则意义。

举例:如果样本权重 w 被应用在损失上,那么该样本对 g 和 h 的贡献应分别乘以 w。但有些库会把权重只乘在 g 上,忽略 h,导致分裂增益计算扭曲。看过某开源库的 issue 区,确实有人在自定义样本权重时发现树结构怪怪的,最后定位到这里。

我之前在某个二分类项目里,对少数类样本加权 2 倍,结果验证集准确率没变化,但召回率明显提升。对照公式后发现:加权 2 倍等价于把少数类样本的 g 和 h 都放大了,分裂时更容易倾向少数类方向。这种数学层面的理解,比盲目调参数更容易达到想要的业务效果。

5.5 特征分裂阈值与损失化简的关联

还有一个容易被忽略的细节:节点分裂时寻找最佳阈值,靠的不只是 g 和 h 的累加。在直方图算法中,需要按特征值排序后做前缀和累加 G 和 H,然后对每个分割位置计算增益。这部分逻辑和损失函数的化简式子完全脱钩——只要给定 g 和 h,分裂算法不关心损失函数长什么样。

这意味着:你完全可以用同一套分裂框架去适配不同的损失函数,只要你能提供 g 和 h。我试过把二分类的对数损失换成 Focal Loss 的自定义实现,改动点只有三个:计算 g、计算 h、叶子权重公式。这让我意识到,深入理解“损失函数化简过程”的真正价值,在于获得了一种可扩展的能力——遇到新任务时,你不再被现成库限制,而是有能力从数学层面设计自己的目标函数。

最后想说的话

从 L = -[y log p + (1-y) log(1-p)] 到 g = p - y,从二级泰勒展开到 w = ∑(y-p) / ∑p(1-p),每一步化简都不是单纯的数学炫技。这些式子直接决定了梯度提升分类树在工程上的数据结构、分裂逻辑、正则化方式和数值技巧。弄懂了这条链路,再看各种梯度提升库的源码,会发现一切都顺理成章——所谓参数名、接口设计,都是这些数学式子在代码层面的影子。给别人讲清楚梯度提升分类树,难点从来不在“树怎么建”,而在“损失函数怎么化简到能直接算”。希望这篇文章能把这条最关键的推导链路,给你彻底讲透。

相关新闻

CMSIS-DAP协议核心命令数据包逐字节拆解与调试实战

CMSIS-DAP协议核心命令数据包逐字节拆解与调试实战

平时我自己做调试器固件和工具链适配的时候,最常干的一件事就是对着CMSIS-DAP协议的数据包发呆。命令手册翻过很多遍,但真正到了要自己组包、抓包、定位“为什么上位机连不上”的时候,需要的不是“知道有个命令叫DAP_Transfer”,而…

2026/10/22 12:53:07 阅读更多 →
Codex插件机制:从“写代码”到“会办事”的AI代理闭环

Codex插件机制:从“写代码”到“会办事”的AI代理闭环

1. Codex 插件机制的核心变化:从“写代码”到“会办事”1.1 这不是功能叠加,而是执行链路的第一次闭环说真的,我上一次对 Codex 有这么强的感知冲击,还是第一次看到它动态补全一段完整业务函数的时候。但那是“生成能力”的进步&a…

2026/10/21 7:35:00 阅读更多 →
从Prompt到Skill:数据分析AI能力封装四层结构设计指南

从Prompt到Skill:数据分析AI能力封装四层结构设计指南

1. 从"会写Prompt"到"能造Skill":为什么单次对话解决不了的问题需要新范式很多人第一次接触AI能力封装时,脑子里想的都是"我写一段提示词不就行了"。我刚开始也是这么想的,直到有一次需要让模型连续处理三十多…

2026/10/21 7:35:00 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/22 10:18:38 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/22 11:01:53 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/22 9:44:47 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/22 10:42:00 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/22 10:40:08 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/22 10:56:37 阅读更多 →

日新闻

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/22 1:47:45 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/21 21:32:28 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/22 5:12:39 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:50:56 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:51:55 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 16:24:48 阅读更多 →