news 2026/10/1 2:13:14

动手学深度学习之梯度下降:从一维泰勒展开到牛顿法与预处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动手学深度学习之梯度下降:从一维泰勒展开到牛顿法与预处理
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

导读:本文对应《动手学深度学习》(d2l-zh)优化算法章节中的梯度下降(gradient descent)指南,讲解梯度下降的基本原理、一维与多元实现、学习率的影响、局部最小值问题,以及牛顿法、收敛性分析、预处理与线搜索等自适应方法。读完本文,你将掌握梯度下降的数学推导与可运行代码(支持 MXNet / PyTorch / TensorFlow / Paddle 四种框架),并理解学习率过大导致发散、预处理按坐标缩放等关键现象,为后续学习随机梯度下降(SGD)与动量、Adam 等高级优化算法打下基础。

一维梯度下降

尽管梯度下降很少被直接用于深度学习,但理解它是理解下一节随机梯度下降算法的关键。例如,由于学习率过大,优化问题可能会发散,这种现象早在梯度下降中就已经出现;同样地,预处理(preconditioning)是梯度下降中的一种常用技术,还会被沿用到更高级的算法中。因此我们从最简单的一维梯度下降开始。

考虑一类连续可微实值函数 $f: \mathbb{R} \rightarrow \mathbb{R}$,利用泰勒展开可以得到:

$$f(x + \epsilon) = f(x) + \epsilon f'(x) + \mathcal{O}(\epsilon^2).$$

即在一阶近似中,$f(x+\epsilon)$ 可通过 $x$ 处的函数值 $f(x)$ 和一阶导数 $f'(x)$ 得出。我们可以假设在负梯度方向上移动的 $\epsilon$ 会减少 $f$。为了简单起见,选择固定步长 $\eta > 0$,然后取 $\epsilon = -\eta f'(x)$,将其代入泰勒展开式得到:

$$f(x - \eta f'(x)) = f(x) - \eta f'^2(x) + \mathcal{O}(\eta^2 f'^2(x)).$$

如果其导数 $f'(x) \neq 0$ 没有消失,就能继续推进,这是因为 $\eta f'^2(x)>0$。此外,我们总是可以令 $\eta$ 小到足以使高阶项变得不相关,因此有:

$$f(x - \eta f'(x)) \lessapprox f(x).$$

这意味着,如果我们使用

$$x \leftarrow x - \eta f'(x)$$

来迭代 $x$,函数 $f(x)$ 的值可能会下降。因此,在梯度下降中,我们首先选择初始值 $x$ 和常数 $\eta > 0$,然后使用它们连续迭代 $x$,直到停止条件达成。例如,当梯度 $|f'(x)|$ 的幅度足够小,或迭代次数达到某个值时停止。

代码实现:以 $f(x)=x^2$ 为例

为了演示实现,选用目标函数 $f(x)=x^2$。尽管已知 $x=0$ 时 $f(x)$ 取得最小值,我们仍使用这个简单的函数来观察 $x$ 的变化。定义目标函数与其梯度:

def f(x): # 目标函数 return x ** 2 def f_grad(x): # 目标函数的梯度(导数) return 2 * x

本仓库的d2l包为每种深度学习框架提供了同名接口,上述代码在 MXNet、PyTorch、TensorFlow、Paddle 四个后端下均可运行,导入方式分别为:

from d2l import mxnet as d2l # 或 torch / tensorflow / paddle

接下来使用 $x=10$ 作为初始值,并假设 $\eta=0.2$,用梯度下降迭代 $x$ 共 10 次。可以看到 $x$ 的值最终将接近最优解 $x=0$:

def gd(eta, f_grad): x = 10.0 results = [x] for i in range(10): x -= eta * f_grad(x) results.append(float(x)) print(f'epoch 10, x: {x:f}') return results results = gd(0.2, f_grad)

对 $x$ 优化的过程可以绘制如下。show_trace函数以results的最大绝对值为界生成函数曲线,并把每次迭代后的点用-o折线标记出来:

def show_trace(results, f): n = max(abs(min(results)), abs(max(results))) f_line = d2l.arange(-n, n, 0.01) d2l.set_figsize() d2l.plot([f_line, results], [[f(x) for x in f_line], [ f(x) for x in results]], 'x', 'f(x)', fmts=['-', '-o']) show_trace(results, f)

其中d2l.set_figsize、d2l.plot等绘图工具在仓库中均有统一实现,例如 d2l/torch.py 中set_figsize默认设置figure.figsize = (3.5, 2.5)并切换到 SVG 输出,plot负责归一化输入、设置坐标轴并逐条绘制曲线。d2l.arange、d2l.meshgrid、d2l.cos、d2l.sin等符号则在 d2l/torch.py 中直接映射到torch的对应函数,从而保证四个后端代码风格一致。

学习率:过大发散,过小停滞

学习率 $\eta$ 由算法设计者设置,它决定目标函数能否收敛到局部最小值,以及何时收敛到最小值。

  • 学习率太小:将导致 $x$ 更新非常缓慢,需要更多迭代。例如同一优化问题中取 $\eta = 0.05$,即使经过 10 步,仍离最优解很远:
show_trace(gd(0.05, f_grad), f)
  • 学习率过大:$\left|\eta f'(x)\right|$ 对一阶泰勒展开式可能太大,即展开式中的高阶项 $\mathcal{O}(\eta^2 f'^2(x))$ 变得显著,此时 $x$ 的迭代不能保证降低 $f(x)$ 的值。例如 $\eta=1.1$ 时,$x$ 超出最优解 $x=0$ 并逐渐发散:
show_trace(gd(1.1, f_grad), f)

这正是原文档强调的"学习率调节"难题的雏形:过大则发散、过小则停滞。后面会看到,二阶方法、预处理与线搜索都是为了自动或半自动地解决这一难题。

局部最小值:非凸函数的陷阱

为了演示非凸函数上的梯度下降,考虑函数 $f(x) = x \cdot \cos(cx)$,其中 $c$ 为某常数。这个函数有无穷多个局部最小值,根据选择的学习率与问题的条件状况,最终可能只得到众多解中的一个。下面的例子说明(不切实际的)高学习率如何导致较差的局部最小值:

c = d2l.tensor(0.15 * np.pi) def f(x): # 目标函数 return x * d2l.cos(c * x) def f_grad(x): # 目标函数的梯度 return d2l.cos(c * x) - c * x * d2l.sin(c * x) show_trace(gd(2, f_grad), f)

当 $\eta = 2$ 时,由于该函数存在无数波峰波谷,一阶迭代很容易越过极小值点,最终收敛到一个"较差的"局部最小值而非全局最优点。这提示我们:梯度下降只能保证下降到某个局部极小,无法保证全局最优,这也是深度学习中几乎所有优化问题都是非凸的背景下,需要更精巧算法的原因之一。

多元梯度下降

现在考虑 $\mathbf{x} = [x_1, x_2, \ldots, x_d]^\top$ 的情形,即目标函数 $f: \mathbb{R}^d \to \mathbb{R}$ 将向量映射成标量。相应地,梯度也是多元的,它是一个由 $d$ 个偏导数组成的向量:

$$\nabla f(\mathbf{x}) = \bigg[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_d}\bigg]^\top.$$

梯度中的每个偏导数元素 $\partial f(\mathbf{x})/\partial x_i$ 代表了 $f$ 在 $\mathbf{x}$ 处关于输入 $x_i$ 的变化率。对多元函数使用相应的泰勒近似:

$$f(\mathbf{x} + \boldsymbol{\epsilon}) = f(\mathbf{x}) + \mathbf{\boldsymbol{\epsilon}}^\top \nabla f(\mathbf{x}) + \mathcal{O}(|\boldsymbol{\epsilon}|^2).$$

换句话说,在 $\boldsymbol{\epsilon}$ 的二阶项内,最陡下降的方向由负梯度 $-\nabla f(\mathbf{x})$ 给出。选择合适的学习率 $\eta > 0$,便得到典型的梯度下降算法:

$$\mathbf{x} \leftarrow \mathbf{x} - \eta \nabla f(\mathbf{x}).$$

二维示例与轨迹可视化

构造目标函数 $f(\mathbf{x})=x_1^2+2x_2^2$,梯度为 $\nabla f(\mathbf{x}) = [2x_1, 4x_2]^\top$,从初始位置 $[-5, -2]$ 出发观察 $\mathbf{x}$ 的轨迹。原文档提供了两个辅助函数:

  • train_2d:接收自定义 trainer,从x1=-5, x2=-2, s1=0, s2=0出发迭代 20 步(s1、s2是预留的内部状态变量,供后续动量、AdaGrad 等算法使用),打印每一步结果并返回轨迹;
  • show_trace_2d:在 $x_1 \in [-5.5, 1.0]$、$x_2 \in [-3.0, 1.0]$ 的网格上绘制等值线(contour),并用橙色折线标出迭代轨迹。

这两个函数在仓库 d2l/torch.py 中有正式实现(标注为#@save,会随d2l包导出),并在 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py 中保持一致。完整演示代码如下:

def train_2d(trainer, steps=20, f_grad=None): # 定制训练器优化 2D 目标函数 x1, x2, s1, s2 = -5, -2, 0, 0 results = [(x1, x2)] for i in range(steps): if f_grad: x1, x2, s1, s2 = trainer(x1, x2, s1, s2, f_grad) else: x1, x2, s1, s2 = trainer(x1, x2, s1, s2) results.append((x1, x2)) print(f'epoch {i + 1}, x1: {float(x1):f}, x2: {float(x2):f}') return results def show_trace_2d(f, results): # 显示优化过程中 2D 变量的轨迹 d2l.set_figsize() d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1), d2l.arange(-3.0, 1.0, 0.1), indexing='ij') d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') d2l.plt.xlabel('x1') d2l.plt.ylabel('x2') def f_2d(x1, x2): # 目标函数 return x1 ** 2 + 2 * x2 ** 2 def f_2d_grad(x1, x2): # 目标函数的梯度 return (2 * x1, 4 * x2) def gd_2d(x1, x2, s1, s2, f_grad): g1, g2 = f_grad(x1, x2) return (x1 - eta * g1, x2 - eta * g2, 0, 0) eta = 0.1 show_trace_2d(f_2d, train_2d(gd_2d, f_grad=f_2d_grad))

学习率 $\eta = 0.1$ 时,经过 20 步,$\mathbf{x}$ 的值接近其位于 $[0, 0]$ 的最小值。注意 $x_2$ 方向因系数 2 梯度更强,收敛更快;整体进展相当顺利,但相当缓慢——这正是"每个坐标共享同一个学习率"带来的局限。

自适应方法:二阶信息与曲率

正如上文所见,选择"恰到好处"的学习率 $\eta$ 是很棘手的:选得太小没有进展,选得太大解会振荡甚至发散。那么能否自动确定 $\eta$,或者完全不必选择学习率?

除了考虑目标函数的值和梯度,还考虑其曲率的二阶方法可以帮我们解决这个问题。虽然由于计算代价的原因,这些方法不能直接应用于深度学习,但它们为设计高级优化算法提供了有用的直觉——后续的 AdaGrad、RMSProp、Adam 等算法正是模拟了其中许多理想特性。

牛顿法

回顾 $f: \mathbb{R}^d \rightarrow \mathbb{R}$ 的泰勒展开式,可以写出二阶项:

$$f(\mathbf{x} + \boldsymbol{\epsilon}) = f(\mathbf{x}) + \boldsymbol{\epsilon}^\top \nabla f(\mathbf{x}) + \frac{1}{2} \boldsymbol{\epsilon}^\top \nabla^2 f(\mathbf{x}) \boldsymbol{\epsilon} + \mathcal{O}(|\boldsymbol{\epsilon}|^3).$$

定义 $\mathbf{H} \stackrel{\mathrm{def}}{=} \nabla^2 f(\mathbf{x})$ 为 $f$ 的Hessian 矩阵,是一个 $d \times d$ 矩阵。当 $d$ 很小且问题简单时 $\mathbf{H}$ 很容易计算;但对于深度神经网络,$\mathcal{O}(d^2)$ 个条目的存储代价很高,通过反向传播计算也代价昂贵。先忽略这些考量,看看能得到什么算法。

由于 $f$ 的最小值满足 $\nabla f = 0$,对二阶泰勒展开关于 $\boldsymbol{\epsilon}$ 求导并忽略高阶项:

$$\nabla f(\mathbf{x}) + \mathbf{H} \boldsymbol{\epsilon} = 0 \text{ and hence } \boldsymbol{\epsilon} = -\mathbf{H}^{-1} \nabla f(\mathbf{x}).$$

也就是说,作为优化问题的一部分,需要对 Hessian 矩阵 $\mathbf{H}$ 求逆,更新规则为:

$$\mathbf{x} \leftarrow \mathbf{x} - \eta \mathbf{H}^{-1} \nabla f(\mathbf{x}).$$

理想情况:对于 $f(x) = \frac{1}{2} x^2$,有 $\nabla f(x) = x$ 和 $\mathbf{H} = 1$,因此对任何 $x$ 可得 $\epsilon = -x$——单步即完美收敛,无须任何调整。这里比较幸运:泰勒展开式是精确的,因为 $f(x+\epsilon)= \frac{1}{2} x^2 + \epsilon x + \frac{1}{2} \epsilon^2$。

凸函数验证:对凸双曲余弦函数 $f(x) = \cosh(cx)$($c$ 为常数,这里取 $c=0.5$),经过几次迭代即可到达 $x=0$ 处的全局最小值:

c = d2l.tensor(0.5) def f(x): # 目标函数 return d2l.cosh(c * x) def f_grad(x): # 目标函数的梯度 return c * d2l.sinh(c * x) def f_hess(x): # 目标函数的 Hessian return c**2 * d2l.cosh(c * x) def newton(eta=1): x = 10.0 results = [x] for i in range(10): x -= eta * f_grad(x) / f_hess(x) results.append(float(x)) print('epoch 10, x:', x) return results show_trace(newton(), f)

非凸函数的致命缺陷:考虑 $f(x) = x \cos(c x)$。在牛顿法中我们最终要除以 Hessian,这意味着如果二阶导数是负数,更新方向可能指向 $f$ 值增大的方向:

c = d2l.tensor(0.15 * np.pi) def f(x): # 目标函数 return x * d2l.cos(c * x) def f_grad(x): # 目标函数的梯度 return d2l.cos(c * x) - c * x * d2l.sin(c * x) def f_hess(x): # 目标函数的 Hessian return -2 * c * d2l.sin(c * x) - x * c**2 * d2l.cos(c * x) show_trace(newton(), f) # eta=1,结果"惊人地错误"

如何修正?一种方法是取 Hessian 的绝对值来修正符号问题;另一种策略是重新引入学习率。这看似违背初衷,但并不完全如此——拥有二阶信息可以使我们在曲率较大时保持谨慎(缩小步长),而在目标函数较平坦时采用较大的步长。例如将学习率降为 $\eta = 0.5$:

show_trace(newton(0.5), f)

此时算法变得相当高效。这直观地解释了为什么实践中会发展出"带阻尼的牛顿法"以及 AdaGrad / RMSProp 等按历史梯度规模自适应缩放步长的算法。

收敛性分析

对牛顿法的收敛速度,原文档给出了严格的一维证明。设目标函数 $f$ 凸、三次可微且二阶导数不为零($f'' > 0$);多元证明是一维论证的直接推广,因此省略。记 $x^{(k)}$ 为第 $k$ 次迭代的值,$e^{(k)} \stackrel{\mathrm{def}}{=} x^{(k)} - x^$ 为第 $k$ 次迭代与最优解的距离。由泰勒展开,条件 $f'(x^) = 0$ 可写为:

$$0 = f'(x^{(k)} - e^{(k)}) = f'(x^{(k)}) - e^{(k)} f''(x^{(k)}) + \frac{1}{2} (e^{(k)})^2 f'''(\xi^{(k)}),$$

对某个 $\xi^{(k)} \in [x^{(k)} - e^{(k)}, x^{(k)}]$ 成立。除以 $f''(x^{(k)})$ 得到:

$$e^{(k)} - \frac{f'(x^{(k)})}{f''(x^{(k)})} = \frac{1}{2} (e^{(k)})^2 \frac{f'''(\xi^{(k)})}{f''(x^{(k)})}.$$

代入更新方程 $x^{(k+1)} = x^{(k)} - f'(x^{(k)}) / f''(x^{(k)})$,两边取绝对值:

$$\left|e^{(k+1)}\right| = \frac{1}{2}(e^{(k)})^2 \frac{\left|f'''(\xi^{(k)})\right|}{f''(x^{(k)})}.$$

因此,只要处于 $\left|f'''(\xi^{(k)})\right| / (2f''(x^{(k)})) \leq c$ 的有界区域,就有二次递减误差(即误差每步按平方收缩):

$$\left|e^{(k+1)}\right| \leq c (e^{(k)})^2.$$

优化研究者称这种收敛为"线性"收敛,而把 $\left|e^{(k+1)}\right| \leq \alpha \left|e^{(k)}\right|$($\alpha<1$ 常数收缩)称为"恒定"收敛速度。需要指出该分析的两点局限:其一,我们无法估计何时进入快速收敛区域,只知道一旦接近极小值,收敛将变得非常快;其二,分析要求 $f$ 在高阶导数上表现良好,即确保 $f$ 在取值变化上没有"超常"特性。

预处理(Preconditioning)

计算和存储完整 Hessian 非常昂贵,改善的方法之一是预处理:只计算 Hessian 的"对角线"项,得到更新:

$$\mathbf{x} \leftarrow \mathbf{x} - \eta \mathrm{diag}(\mathbf{H})^{-1} \nabla f(\mathbf{x}).$$

虽然这不如完整的牛顿法精确,但远比不使用要好。为什么有效?假设一个变量以毫米表示高度、另一个变量以公里表示高度,若二者的自然尺度都以米为单位,参数化就出现严重的不匹配,共享一个学习率的梯度下降会在不同坐标上表现悬殊。预处理可以消除这种尺度不匹配——梯度下降的有效预处理,相当于为 $\mathbf{x}$ 的每个坐标选择各自不同的学习率。这一思想直接推动了后续随机梯度下降优化算法的创新,例如 AdaGrad 与 RMSProp 按各坐标的历史梯度平方根缩放学习率,本质上就是对坐标尺度差异的自适应预处理。

梯度下降与线搜索

梯度下降的关键问题之一是会"冲过头"(overshoot)或"进展不足"。简单的修复办法是结合线搜索:使用 $\nabla f(\mathbf{x})$ 给出的方向,然后做二分搜索,确定使 $f(\mathbf{x} - \eta \nabla f(\mathbf{x}))$ 取最小值的学习率 $\eta$。该算法收敛迅速(分析与证明可参考凸优化经典教材,对应参考文献收录于仓库 d2l.bib 的Boyd.Vandenberghe.2004条目)。

然而对深度学习而言这不太可行:线搜索的每一步都需要在整个数据集上评估目标函数,代价过高。这解释了为什么深度学习实践中更常采用启发式的学习率调度(见 学习率调度)而非精确线搜索。

小结

  • 学习率的大小很重要:学习率太大会使模型发散,学习率太小会没有进展。
  • 梯度下降可能陷入局部极小值,而得不到全局最小值。
  • 在高维模型中,调整学习率是很复杂的。
  • 预处理有助于调节各坐标的比例(相当于逐坐标的学习率)。
  • 牛顿法在凸问题中一旦开始正常工作,速度就会快得多(误差按平方收缩)。
  • 对于非凸问题,不要不作任何调整就使用牛顿法(二阶导数为负时可能反向更新)。

练习与动手实验

  1. 用不同的学习率和目标函数进行梯度下降实验,观察收敛、振荡与发散的分界。
  2. 在区间 $[a, b]$ 中实现线搜索以最小化凸函数:
    • 二分搜索决定选择 $[a, (a+b)/2]$ 还是 $[(a+b)/2, b]$ 时,是否需要导数?
    • 该算法的收敛速度有多快?
    • 实现该算法,并应用于最小化 $\log (\exp(x) + \exp(-2x -3))$。
  3. 设计一个定义在 $\mathbb{R}^2$ 上的目标函数,使梯度下降收敛极其缓慢。提示:不同坐标采用不同的缩放方式。
  4. 使用预处理实现牛顿法的轻量版本:
    • 用对角 Hessian 作为预条件子;
    • 使用它的绝对值而非实际(可能有符号的)值;
    • 将上述方法应用于练习 3 中的问题。
  5. 将上述算法应用于多个目标函数(凸或非凸),观察如果把坐标旋转 45 度,收敛轨迹会发生什么变化。

延伸阅读:在本仓库中继续深入

  • 本文对应的章节源码与后续内容见 chapter_optimization/index.md,其中梯度下降(gd)位于凸优化基础(convexity)之后、随机梯度下降 sgd.md 之前——SGD 正是以本文的梯度下降原理为基石展开的。
  • 微积分基础(泰勒展开、梯度、Hessian 的定义)参见 chapter_preliminaries/calculus.md。
  • train_2d、show_trace_2d等工具函数的正式实现位于 d2l/torch.py,并在 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py 中提供四框架一致的版本;绘图工具set_figsize/plot见 d2l/torch.py。
  • 阅读本章其余小节可依次探索小批量随机梯度下降、动量法、AdaGrad、RMSProp、AdaDelta、Adam 与学习率调度,它们都是本文梯度下降思想的直接延伸。
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

相关推荐

上一篇:终极Node-RED配置指南:5分钟搭建智能家居自动化系统
下一篇:React Router resolvePath 全解析:路径解析 API、相对路径规则与源码调用链

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 2:12:40

Pogo Pin压缩裕量的Python量化检测与产线闭环实践

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

作者头像 李华
网站建设 2026/10/1 2:10:59

汽车电子全产业链图谱:从车规芯片到整车功能安全的工程实践

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

作者头像 李华
网站建设 2026/10/1 2:10:58

SpringBoot露营装备租赁系统毕设指南:核心技术与实战拆解

这两年帮不少学弟学妹参谋毕业设计&#xff0c;发现“基于SpringBoot的XX管理系统”几乎成了默认选项&#xff0c;而露营装备租赁这个方向尤其多。你可能看过类似标题&#xff1a;计算机毕业设计springboot露营装备租赁系统、基于SpringBoot的户外露营装备共享租赁平台、基于Sp…

作者头像 李华
网站建设 2026/10/1 2:10:56

删繁就简:从断舍离到活出自我格调的实操指南

删繁就简&#xff0c;活成自己喜欢的格调我第一次正视“删繁就简”这件事&#xff0c;不是因为我突然领悟了什么高深的人生哲学&#xff0c;而是因为家里实在堆不下了。去年搬家前&#xff0c;我统计了一下自己住了五年的房子的物品总量——光是不穿的衣服就有三百多件&#xf…

作者头像 李华
网站建设 2026/10/1 2:09:42

Unity数字孪生实战:SolidWorks模型到WebGL交互空间

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

作者头像 李华
网站建设 2026/10/1 2:08:57

Spring事务失效避坑指南:从AOP代理到排查清单

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

作者头像 李华