问题
为什么genimi 能快速获取刚发生的新闻报道并分析,是模型一直在训练吗
“检索增强生成”(RAG, Retrieval-Augmented Generation) 的技术
RAG = 强大的大脑(大模型)+ 随时更新的图书馆(外部数据库)
| 维度 | 纯大模型(LLM Only) | RAG 模式 |
|---|---|---|
| 准确性 | ❗ 容易产生“幻觉”(模型凭内部统计生成答案) | ✅ 高 — 基于真实文档生成,回答更有依据 |
| 时效性 | 🕒 取决于上次训练时间(不能实时获取最新信息) | ⏱ 实时 — 取决于外部数据源/知识库更新频率 |
| 成本 | 💰 重新训练极其昂贵(大规模数据+算力消耗巨大) | 💸 极低 — 无需重新训练模型,只更新数据库/向量库 |
| 透明度 | 🔒 不能给出来源证据(“黑箱”) | 📌 可列出参考原文/链接(更可解释、更可信) |
两个关键概念
优化(optimization): 用模型拟合观测数据的过程泛化: 数学原理和实践者的智慧, 指导生出超出模型的数据
导数和微分
理解导数

导数 (Derivative):它是变化率。就像时速表上的 60km/h。它告诉你此刻位置相对于时间的变化快慢。微分 (Differential):它是变化量。就像你在接下来的 0.001 秒内实际位移了多少距离。
import numpy as np
import matplotlib.pyplot as plt # 替换掉 inline 魔法命令
# 解决中文显示异常:按顺序尝试常见中文字体(Linux/Windows/macOS)
plt.rcParams['font.sans-serif'] = [
'Noto Sans CJK SC', 'Source Han Sans SC', 'WenQuanYi Zen Hei',
'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Heiti SC',
'Arial Unicode MS', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False # 解决坐标轴负号显示为方块的问题
# 目标函数:f(x) = 3x^2 - 4x
# 你可以把它看成一条“弯曲的抛物线”。
# 我们后面要在这条曲线上找某一点的“瞬时变化率”,这就是导数的直观含义。
def f(x):
return 3 * x ** 2 - 4 * x
# 导函数:f'(x) = 6x - 4
def f_prime(x):
return 6 * x - 4
# --------------------------
# 1) 准备自变量 x 的取值范围
# --------------------------
# np.arange(0, 3, 0.1) 表示从 0 到 3(不含 3),每隔 0.1 取一个点。
# 这些点用于在坐标系中画出函数曲线。
x = np.arange(0, 3, 0.01)
# --------------------------
# 2) 导数概念(结合本例)
# --------------------------
# 对函数 f(x)=3x^2-4x 求导:
# f'(x) = 6x - 4
# 导数 f'(x) 的几何意义:
# 在 x 这一点,曲线切线的斜率(也就是“瞬时变化率”)。
#
# 例如在 x=1 处:
# f'(1) = 6*1 - 4 = 2 -> 切线斜率是 2
# f(1) = 3*1^2 - 4*1 = -1 -> 切点是 (1, -1)
#
# 过点 (1, -1),斜率为 2 的直线方程:
# y - (-1) = 2(x - 1)
# y = 2x - 3
# 这就是下面第二条线 2*x-3 的来源(x=1 处的切线)。
# 选多个观察点,研究每个点附近的变化率
x_points = np.array([0.4, 0.9, 1.4, 2.0])
# 固定一个较小的 Δx 来演示“微小变化”
h_demo = 0.2
# --------------------------
# 3) 画图(上下两个子图)
# 上图:原函数 + 多个变化点的 Δx、Δy
# 下图:导函数 f'(x)=6x-4
# --------------------------
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 10), sharex=True)
# ===== 上图:f(x) 与多个变化点 =====
ax1.plot(x, f(x), label='f(x) = 3x^2 - 4x', linewidth=2.5)
# 对每个观察点画出 Δx 与 Δy
for i, x0 in enumerate(x_points):
y0 = f(x0)
x1 = x0 + h_demo
y1 = f(x1)
delta_x = x1 - x0
delta_y = y1 - y0
change_rate = delta_y / delta_x
# 起点和终点
point_label = '观察点与变化后点' if i == 0 else None
ax1.scatter([x0, x1], [y0, y1], s=45, zorder=6, label=point_label)
# 水平段 Δx
dx_label = 'Δx (水平变化)' if i == 0 else None
ax1.plot([x0, x1], [y0, y0], color='tab:green', linewidth=2, alpha=0.9, label=dx_label)
# 竖直段 Δy
dy_label = 'Δy (垂直变化)' if i == 0 else None
ax1.plot([x1, x1], [y0, y1], color='tab:orange', linewidth=2, alpha=0.9, label=dy_label)
# 在每组变化旁边写差商(变化率)
ax1.text(x1 + 0.03, y1, f'Δy/Δx={change_rate:.2f}', fontsize=9)
ax1.set_title('f(x) 上多个变化点:微小 Δx 引起 Δy')
ax1.set_ylabel('y = f(x)')
ax1.grid(alpha=0.25)
ax1.legend(loc='best', fontsize=9)
# ===== 下图:导函数 f'(x) =====
y_prime = f_prime(x)
ax2.plot(x, y_prime, color='tab:red', linewidth=2.5, label="f'(x) = 6x - 4")
# 在导函数图上标出同样的 x 位置,帮助对应“该点导数值”
prime_values = f_prime(x_points)
ax2.scatter(x_points, prime_values, color='black', s=40, zorder=6, label='各观察点的 f\'(x)')
for x0, p in zip(x_points, prime_values):
ax2.text(x0 + 0.03, p + 0.12, f'{p:.2f}', fontsize=9)
ax2.axhline(0, color='gray', linestyle=':', linewidth=1)
ax2.set_title("导函数图像:f'(x) = 6x - 4")
ax2.set_xlabel('x')
ax2.set_ylabel("y = f'(x)")
ax2.grid(alpha=0.25)
ax2.legend(loc='best', fontsize=9)
plt.tight_layout()
# 在终端打印多个观察点的差商和导数值,便于对照
print(f"固定 Δx = {h_demo}")
for x0 in x_points:
y0 = f(x0)
x1 = x0 + h_demo
y1 = f(x1)
diff_quotient = (y1 - y0) / h_demo
derivative_value = f_prime(x0)
print(
f"x0={x0:.2f}, Δy/Δx={diff_quotient:.6f}, f'(x0)={derivative_value:.6f}, 差值={abs(diff_quotient-derivative_value):.6f}"
)
# 在 .py 脚本中需要显式调用 show(),否则图像窗口可能一闪而过
plt.show()
偏导数 和 梯度
- 偏导数(Partial Derivatives)= 单一方向的坡度
- 梯度(Gradient = 所有偏导数组成的向量,指引最快上升的方向
| 维度 | 偏导数 (Partial Derivative) | 梯度 (Gradient) |
|---|---|---|
| 形式 | 一个标量(单个数字) | 一个向量(由多个偏导数组成) |
| 数量 | 有几个自变量,就有几个偏导数 | 对一个函数来说,只有一个梯度向量 |
| 信息量 | 只描述某一个轴向的变化率 | 描述函数在所有方向上的整体变化趋势 |
| 几何意义 | 沿某个坐标轴方向的切线斜率 | 指向函数增长最快方向的向量 |
| 数学表达 | (\frac{\partial f}{\partial x_i}) | (\nabla f = \left(\frac{\partial f}{\partial x_1},…,\frac{\partial f}{\partial x_n}\right)) |

import numpy as np
import matplotlib.pyplot as plt
# 中文显示设置(常见 Linux/Windows/macOS 字体回退)
plt.rcParams['font.sans-serif'] = [
'Noto Sans CJK SC', 'Source Han Sans SC', 'WenQuanYi Zen Hei',
'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Heiti SC',
'Arial Unicode MS', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False
# 二元函数:z = f(x, y)
def f(x, y):
return x ** 2 + 2 * y ** 2
# 解析偏导
def dfdx(x, y):
return 2 * x
def dfdy(x, y):
return 4 * y
def grad_f(x, y):
return np.array([dfdx(x, y), dfdy(x, y)])
# 选择一个观察点 (x0, y0)
x0, y0 = 1.0, 1.0
z0 = f(x0, y0)
g0 = grad_f(x0, y0)
# 网格用于画三维曲面
x = np.linspace(-2, 2, 120)
y = np.linspace(-2, 2, 120)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)
# 分别构造两条“只动一个变量”的截线
x_line = np.linspace(-2, 2, 300)
y_line = np.linspace(-2, 2, 300)
zx = f(x_line, y0) # 固定 y=y0,只改变 x
zy = f(x0, y_line) # 固定 x=x0,只改变 y
# 偏导对应的切线(在观察点附近)
tx = z0 + dfdx(x0, y0) * (x_line - x0)
ty = z0 + dfdy(x0, y0) * (y_line - y0)
fig = plt.figure(figsize=(13, 8))
# ---------- 子图1:三维曲面 ----------
ax3d = fig.add_subplot(2, 2, 1, projection='3d')
ax3d.plot_surface(X, Y, Z, cmap='viridis', alpha=0.75, linewidth=0)
# 在三维图中画出两条截线,强调“固定一个变量”
ax3d.plot(x_line, np.full_like(x_line, y0), zx, color='tab:orange', linewidth=2.5, label='固定 y=y0,改变 x')
ax3d.plot(np.full_like(y_line, x0), y_line, zy, color='tab:green', linewidth=2.5, label='固定 x=x0,改变 y')
ax3d.scatter([x0], [y0], [z0], color='red', s=50)
ax3d.set_title('三维曲面 z=f(x,y):偏导是在点处沿坐标方向的变化率')
ax3d.set_xlabel('x')
ax3d.set_ylabel('y')
ax3d.set_zlabel('z')
ax3d.view_init(elev=25, azim=-55)
ax3d.legend(loc='upper left', fontsize=8)
# ---------- 子图2:梯度概念(等高线 + 梯度向量) ----------
ax_g = fig.add_subplot(2, 2, 2)
levels = np.linspace(Z.min(), Z.max(), 18)
contours = ax_g.contour(X, Y, Z, levels=levels, cmap='viridis', alpha=0.9)
ax_g.clabel(contours, inline=True, fontsize=7, fmt='%.0f')
# 在 (x0, y0) 处画梯度向量 ∇f = [∂f/∂x, ∂f/∂y]
ax_g.scatter([x0], [y0], color='red', s=45, zorder=5)
ax_g.quiver(
x0, y0, g0[0], g0[1],
angles='xy', scale_units='xy', scale=8,
color='tab:red', width=0.007, zorder=6
)
ax_g.text(
x0 + 0.15, y0 + 0.15,
f'∇f=({g0[0]:.2f}, {g0[1]:.2f})',
fontsize=10, color='tab:red'
)
ax_g.text(
0.03, 0.95,
'梯度 ∇f = [∂f/∂x, ∂f/∂y]\n方向:函数增长最快方向\n大小:单位步长下的最大增长率',
transform=ax_g.transAxes,
fontsize=9,
verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.85)
)
ax_g.set_title('梯度图:等高线上的最陡上升方向')
ax_g.set_xlabel('x')
ax_g.set_ylabel('y')
ax_g.grid(alpha=0.2)
ax_g.set_aspect('equal', adjustable='box')
# ---------- 子图2:固定 y,看 ∂f/∂x ----------
ax_x = fig.add_subplot(2, 2, 3)
ax_x.plot(x_line, zx, color='tab:orange', linewidth=2.5, label=f'z=f(x,{y0:g})')
ax_x.plot(x_line, tx, '--', color='black', linewidth=1.8, label=f'切线斜率=∂f/∂x={dfdx(x0, y0):.2f}')
ax_x.scatter([x0], [z0], color='red', s=45)
ax_x.axvline(x0, color='gray', linestyle=':')
ax_x.grid(alpha=0.25)
ax_x.set_title('固定 y,不动 y,只让 x 变化 → 偏导 ∂f/∂x')
ax_x.set_xlabel('x')
ax_x.set_ylabel('z')
ax_x.legend(fontsize=9)
# ---------- 子图3:固定 x,看 ∂f/∂y ----------
ax_y = fig.add_subplot(2, 2, 4)
ax_y.plot(y_line, zy, color='tab:green', linewidth=2.5, label=f'z=f({x0:g},y)')
ax_y.plot(y_line, ty, '--', color='black', linewidth=1.8, label=f'切线斜率=∂f/∂y={dfdy(x0, y0):.2f}')
ax_y.scatter([y0], [z0], color='red', s=45)
ax_y.axvline(y0, color='gray', linestyle=':')
ax_y.grid(alpha=0.25)
ax_y.set_title('固定 x,不动 x,只让 y 变化 → 偏导 ∂f/∂y')
ax_y.set_xlabel('y')
ax_y.set_ylabel('z')
ax_y.legend(fontsize=9)
fig.suptitle('偏导数直观理解:多元函数中“控制变量”的局部变化率', fontsize=14)
plt.tight_layout(rect=[0, 0.02, 1, 0.96])
print(f'观察点: (x0, y0)=({x0}, {y0}), z0={z0:.3f}')
print(f'∂f/∂x(x0,y0) = {dfdx(x0, y0):.3f} (固定 y,只看 x 方向变化率)')
print(f'∂f/∂y(x0,y0) = {dfdy(x0, y0):.3f} (固定 x,只看 y 方向变化率)')
print(f'∇f(x0,y0) = ({g0[0]:.3f}, {g0[1]:.3f}), |∇f| = {np.linalg.norm(g0):.3f}')
plt.show()
链式法则
链式法则:大系统的总变化率,等于各级子系统变化率的乘积
神经网络其实就是一个超级巨大的复合函数

import numpy as np
import matplotlib.pyplot as plt
# 中文显示设置(常见 Linux/Windows/macOS 字体回退)
plt.rcParams['font.sans-serif'] = [
'Noto Sans CJK SC', 'Source Han Sans SC', 'WenQuanYi Zen Hei',
'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Heiti SC',
'Arial Unicode MS', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False
# 新增一层:
# v = h(x) -> u = g(v) -> y = f(u)
# 最终复合:y = f(g(h(x)))
def h(x):
return x ** 2 + 1
def dh_dx(x):
return 2 * x
def g(v):
return v ** 3
def dg_dv(v):
return 3 * v ** 2
def f(u):
return np.sin(u)
def df_du(u):
return np.cos(u)
def y_of_x(x):
return f(g(h(x)))
def dy_dx_chain(x):
return df_du(g(h(x))) * dg_dv(h(x)) * dh_dx(x)
# 选一个观察点
x0 = 0.6
v0 = h(x0)
u0 = g(v0)
y0 = f(u0)
slope_h = dh_dx(x0) # dv/dx
slope_g = dg_dv(v0) # du/dv
slope_f = df_du(u0) # dy/du
slope_chain = dy_dx_chain(x0) # dy/dx
# 曲线数据
x = np.linspace(-1.5, 1.5, 500)
v = np.linspace(0.8, 3.5, 500)
u = np.linspace(0.0, 35.0, 500)
h_curve = h(x)
g_curve = g(v)
f_curve = f(u)
y_curve = y_of_x(x)
# 四张图:三层链式关系
fig, axes = plt.subplots(2, 2, figsize=(13, 8))
# 1) v = h(x)
ax1 = axes[0, 0]
ax1.plot(x, h_curve, linewidth=2.3, label='v = h(x) = x² + 1')
ax1.scatter([x0], [v0], color='red', s=45, zorder=5)
line1 = v0 + slope_h * (x - x0)
ax1.plot(x, line1, '--', color='black', linewidth=1.8, label=f'切线斜率 dv/dx={slope_h:.3f}')
ax1.axvline(x0, linestyle=':', color='gray')
ax1.grid(alpha=0.25)
ax1.set_title('第1层:x → v')
ax1.set_xlabel('x')
ax1.set_ylabel('v')
ax1.legend(fontsize=9)
# 2) u = g(v)
ax2 = axes[0, 1]
ax2.plot(v, g_curve, linewidth=2.3, color='tab:orange', label='u = g(v) = v³')
ax2.scatter([v0], [u0], color='red', s=45, zorder=5)
line2 = u0 + slope_g * (v - v0)
ax2.plot(v, line2, '--', color='black', linewidth=1.8, label=f'切线斜率 du/dv={slope_g:.3f}')
ax2.axvline(v0, linestyle=':', color='gray')
ax2.grid(alpha=0.25)
ax2.set_title('第2层:v → u')
ax2.set_xlabel('v')
ax2.set_ylabel('u')
ax2.legend(fontsize=9)
# 3) y = f(u)
ax3 = axes[1, 0]
ax3.plot(u, f_curve, linewidth=2.3, color='tab:green', label='y = f(u) = sin(u)')
ax3.scatter([u0], [y0], color='red', s=45, zorder=5)
line3 = y0 + slope_f * (u - u0)
ax3.plot(u, line3, '--', color='black', linewidth=1.8, label=f'切线斜率 dy/du={slope_f:.3f}')
ax3.axvline(u0, linestyle=':', color='gray')
ax3.grid(alpha=0.25)
ax3.set_title('第3层:u → y')
ax3.set_xlabel('u')
ax3.set_ylabel('y')
ax3.legend(fontsize=9)
# 4) y = f(g(h(x)))
ax4 = axes[1, 1]
ax4.plot(x, y_curve, linewidth=2.3, color='tab:purple', label='y = sin((x²+1)³)')
ax4.scatter([x0], [y0], color='red', s=45, zorder=5)
line4 = y0 + slope_chain * (x - x0)
ax4.plot(x, line4, '--', color='black', linewidth=1.8, label=f'切线斜率 dy/dx={slope_chain:.3f}')
ax4.axvline(x0, linestyle=':', color='gray')
ax4.grid(alpha=0.25)
ax4.set_title('复合函数:y = f(g(h(x)))')
ax4.set_xlabel('x')
ax4.set_ylabel('y')
ax4.legend(fontsize=9)
formula_text = (
f'三层链式法则:\n'
f'dy/dx = (dy/du)·(du/dv)·(dv/dx)\n'
f'= ({slope_f:.3f}) × ({slope_g:.3f}) × ({slope_h:.3f})\n'
f'= {slope_chain:.3f}'
)
ax4.text(
0.04, 0.95, formula_text,
transform=ax4.transAxes,
fontsize=10,
verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.88)
)
fig.suptitle('链式法则(再加一层):复合导数等于每层导数连乘', fontsize=14)
plt.tight_layout(rect=[0, 0.02, 1, 0.94])
# 终端数值验证(用差商近似对比)
h_eps = 1e-6
finite_diff = (y_of_x(x0 + h_eps) - y_of_x(x0 - h_eps)) / (2 * h_eps)
print(f'x0 = {x0:.3f}, v0 = h(x0) = {v0:.6f}, u0 = g(v0) = {u0:.6f}, y0 = f(u0) = {y0:.6f}')
print(f'dv/dx = {slope_h:.6f}')
print(f'du/dv = {slope_g:.6f}')
print(f'dy/du = {slope_f:.6f}')
print(f'链式法则 dy/dx = (dy/du)*(du/dv)*(dv/dx) = {slope_chain:.6f}')
print(f'差商近似 dy/dx ≈ {finite_diff:.6f}, 误差 = {abs(finite_diff - slope_chain):.6e}')
plt.show()
自动微分
一个例子
import torch
import numpy as np
import matplotlib.pyplot as plt
# 中文显示设置
plt.rcParams['font.sans-serif'] = [
'Noto Sans CJK SC', 'Source Han Sans SC', 'WenQuanYi Zen Hei',
'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Heiti SC',
'Arial Unicode MS', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False
x = torch.arange(4.0)
# 这时 x 是一个普通张量:x=[x0,x1,x2,x3]
# 还没有开启自动求导,PyTorch 不会记录它参与的计算图
print(x) # 输出:tensor([0., 1., 2., 3.])
# 开启自动求导:后续基于 x 的运算都会被记录
# 这样在 backward() 之后,梯度会存到 x.grad
x.requires_grad_(True)
# 定义标量目标函数:
# y = 2 * (x0^2 + x1^2 + x2^2 + x3^2)
# 代码里用 torch.dot(x, x) 表示 x0^2 + x1^2 + x2^2 + x3^2
y = 2 * torch.dot(x, x)
# y 是标量,适合直接调用 backward()
# 反向传播时会自动计算每个分量的偏导:
# ∂y/∂xi = ∂(2*xi^2)/∂xi = 4*xi
# 所以梯度向量应为:
# ∇y = [4*x0, 4*x1, 4*x2, 4*x3] = 4*x
print(y) # 输出:tensor(28., grad_fn=<MulBackward0>)
# 执行反向传播,计算并累加梯度到 x.grad
y.backward()
# 输出梯度并与手算结果对照:
# 当 x=[0,1,2,3],理论梯度应为 [0,4,8,12]
print(x.grad) # 输出:tensor([ 0., 4., 8., 12.])
图形解释这个例子

# ==================================================
# 可视化补充:用“4个分量”直观理解4维
# ==================================================
# 关键想法:4维向量 x=[x1,x2,x3,x4] 本质上就是 4 个数字。
# 所以我们按“分量”来画:
# 1) 每个维度当前值 x_i
# 2) 每个维度梯度 ∂y/∂x_i = 4x_i
# 3) 做一步梯度下降后,每个分量如何变化,以及 y 是否下降
# 当前 4 维点与梯度
x_np = x.detach().numpy()
grad_np = x.grad.detach().numpy()
# 维度索引(用于横轴)
dim_idx = np.arange(1, 5)
# 做一步梯度下降:x_new = x - lr * grad
lr = 0.1
x_new = x_np - lr * grad_np
y_new = 2 * np.dot(x_new, x_new)
print('\n4维分量视角:')
print(f'x(更新前) = {x_np.tolist()}')
print(f'∇y = {grad_np.tolist()}')
print(f'x(更新后) = {x_new.tolist()}')
print(f'y(更新前) = {y.item():.3f}, y(更新后) = {y_new:.3f}')
fig, axes = plt.subplots(1, 3, figsize=(14, 4.8))
# ---------- 子图1:x 的4个分量 ----------
ax1 = axes[0]
ax1.bar(dim_idx, x_np, color='tab:blue', alpha=0.85)
ax1.set_xticks(dim_idx)
ax1.set_xticklabels(['x1', 'x2', 'x3', 'x4'])
ax1.set_title('当前4维向量 x 的各分量')
ax1.set_ylabel('数值')
ax1.grid(axis='y', alpha=0.25)
# ---------- 子图2:梯度的4个分量 ----------
ax2 = axes[1]
ax2.bar(dim_idx, grad_np, color='tab:red', alpha=0.85)
ax2.set_xticks(dim_idx)
ax2.set_xticklabels(['∂y/∂x1', '∂y/∂x2', '∂y/∂x3', '∂y/∂x4'])
ax2.set_title('梯度 ∇y = 4x 的各分量')
ax2.set_ylabel('数值')
ax2.grid(axis='y', alpha=0.25)
# ---------- 子图3:一步梯度下降前后对比 ----------
ax3 = axes[2]
ax3.plot(dim_idx, x_np, marker='o', linewidth=2.2, color='tab:blue', label='更新前 x')
ax3.plot(dim_idx, x_new, marker='s', linewidth=2.2, color='tab:green', label='更新后 x_new')
ax3.set_xticks(dim_idx)
ax3.set_xticklabels(['x1', 'x2', 'x3', 'x4'])
ax3.set_title(f'一步更新:x_new = x - {lr}·∇y')
ax3.set_ylabel('分量数值')
ax3.grid(alpha=0.25)
ax3.legend(loc='upper left', fontsize=9)
ax3.text(
0.03, 0.03,
f'y前 = {y.item():.2f}\ny后 = {y_new:.2f}\n(沿 -∇y 方向,y 下降)',
transform=ax3.transAxes,
fontsize=10,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.88)
)
fig.suptitle('4维可视化(易懂版):分量、梯度分量、一步更新', fontsize=14)
plt.tight_layout(rect=[0, 0.02, 1, 0.95])
动画显示梯度下降过程

# ==================================================
# 动画补充:连续多步梯度下降(10步)
# ==================================================
steps = 10
traj = [x_np.copy()]
y_traj = [2 * np.dot(x_np, x_np)]
x_curr = x_np.copy()
for _ in range(steps):
grad_curr = 4 * x_curr
x_curr = x_curr - lr * grad_curr
traj.append(x_curr.copy())
y_traj.append(2 * np.dot(x_curr, x_curr))
traj = np.array(traj)
y_traj = np.array(y_traj)
print('\n连续10步梯度下降:')
print(f'初始 y = {y_traj[0]:.6f}, 第10步 y = {y_traj[-1]:.6f}')
fig_anim, (ax4, ax5) = plt.subplots(1, 2, figsize=(13, 4.8))
# 左图:4个分量随步数动态变化(当前帧柱状图)
bars = ax4.bar(dim_idx, traj[0], color='tab:blue', alpha=0.88)
ax4.set_xticks(dim_idx)
ax4.set_xticklabels(['x1', 'x2', 'x3', 'x4'])
max_abs = np.max(np.abs(traj)) * 1.15
ax4.set_ylim(-max_abs, max_abs)
ax4.set_title('第0步:x 的4个分量')
ax4.set_ylabel('分量值')
ax4.grid(axis='y', alpha=0.25)
step_text = ax4.text(0.03, 0.92, '', transform=ax4.transAxes, fontsize=10)
# 右图:目标函数 y 的下降曲线
ax5.set_xlim(0, steps)
ax5.set_ylim(0, y_traj[0] * 1.1)
ax5.set_xlabel('步数')
ax5.set_ylabel('y = 2·||x||²')
ax5.set_title('梯度下降过程:y 持续下降')
ax5.grid(alpha=0.25)
line_y, = ax5.plot([], [], color='tab:red', linewidth=2.2)
point_y, = ax5.plot([], [], 'o', color='black')
def update(frame):
current_x = traj[frame]
current_y = y_traj[frame]
for bar, val in zip(bars, current_x):
bar.set_height(val)
ax4.set_title(f'第{frame}步:x 的4个分量')
step_text.set_text(f'x = {np.round(current_x, 3).tolist()}')
steps_axis = np.arange(frame + 1)
line_y.set_data(steps_axis, y_traj[:frame + 1])
point_y.set_data([frame], [current_y])
return [*bars, line_y, point_y, step_text]
anim = FuncAnimation(fig_anim, update, frames=steps + 1, interval=700, repeat=True, blit=False)
fig_anim.suptitle('4维梯度下降动画(10步)', fontsize=14)
plt.tight_layout(rect=[0, 0.02, 1, 0.93])
# 保存动画文件(用于 GitHub Blog)
# 默认保存为 GIF:兼容性最好,Markdown 可直接插入
output_dir = Path(__file__).resolve().parent / 'assets'
output_dir.mkdir(exist_ok=True)
gif_path = output_dir / 'gradient_descent_4d.gif'
anim.save(gif_path, writer='pillow', fps=2)
print(f'动画已保存:{gif_path}')
# 可选:如果本机装了 ffmpeg,也可以导出 MP4(体积通常更小)
# mp4_path = output_dir / 'gradient_descent_4d.mp4'
# anim.save(mp4_path, writer='ffmpeg', fps=2, dpi=140)
# print(f'动画已保存:{mp4_path}')
plt.show()
非标量变量的反向传播
90% 的深度学习实践者,不需要深入理解“向量输出的反向传播”细节
Python 控制流的梯度计算
这段代码展示了深度学习框架(如 PyTorch)中一个非常强大的特性:控制流(Control Flow)的自动微分
在深度学习中,PyTorch 的自动微分(Autograd)之所以被誉为“灵魂”,是因为它把程序员从繁重的数学苦力中解放了出来。
你只需要写好“前向传播”(数据怎么算出来的),PyTorch 会自动构建一副计算图(Computational Graph)。当你调用 .backward() 时,它会根据链式法则自动逆向遍历这张图,瞬间算出所有参数的梯度。
import torch
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from pathlib import Path
# 中文显示(避免图中文字乱码)
plt.rcParams['font.sans-serif'] = [
'Noto Sans CJK SC', 'Source Han Sans SC', 'WenQuanYi Zen Hei',
'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Heiti SC',
'Arial Unicode MS', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False
# ==================================================
# 第1部分:原示例(计算图中含 while / if)
# ==================================================
# 这个函数里既有循环又有条件分支,用于说明:
# PyTorch 依然可以对“实际执行路径”做自动求导。
def f(a):
b = a * 2
# 避免 a=0 时进入无限循环(0 乘 2 仍然是 0)
if b.norm() == 0:
return b
# 循环:不断放大 b,直到 |b| >= 1000
while b.norm() < 1000:
b = b * 2
# 分支:根据 b 的符号选择输出
if b.sum() > 0: # b 的分量和大于 0 就直接输出 b,否则放大 100 倍后输出
c = b
else:
c = b * 100
return c
# 标量输入 a,开启自动求导
# 为了便于核对,这里固定为 2.0(你也可以改成其他值)
a_value = 2.0
a = torch.tensor(a_value, requires_grad=True)
# 前向计算
d = f(a)
print(f'函数 f(a) 的输出 d: {d.item():.4f}')
# 反向传播:求 ∂d/∂a
d.backward()
# 查看 a 的值与梯度
print(f'输入 a 的值: {a.item():.4f}')
print(f'输入 a 的梯度: {a.grad.item():.4f}')

# ==================================================
# 第2部分:GIF 动画演示“d=f(a) 的梯度变化过程”
# ==================================================
# 这里不再用 L(x)=x^2,而是直接用上面的 f(a) 本身:
# 每一帧选择一个 a,计算 d=f(a) 与梯度 dd/da。
def calc_d_and_grad(a_value: float):
a_tensor = torch.tensor(float(a_value), requires_grad=True)
d_tensor = f(a_tensor)
d_tensor.backward()
return d_tensor.item(), a_tensor.grad.item()
# 采样区间(避开 0,防止不必要的边界问题)
# 范围包含 a=2,便于与你手动代入结果对照
a_grid = np.linspace(-2.2, 2.2, 280)
a_grid[np.isclose(a_grid, 0.0, atol=1e-12)] = 1e-3
d_grid = []
g_grid = []
for a_val in a_grid:
d_val, g_val = calc_d_and_grad(a_val)
d_grid.append(d_val)
g_grid.append(g_val)
d_grid = np.array(d_grid)
g_grid = np.array(g_grid)
# 动画轨迹:让 a 从左到右扫描,直观看 d 和梯度如何变化
steps = 60
a_path = np.linspace(-2.2, 2.2, steps + 1)
a_path[np.isclose(a_path, 0.0, atol=1e-12)] = 1e-3
a_check = 2.0
# 强制把 a=2 放进动画轨迹,确保有一帧精确显示 d=1024、dd/da=512
a_path = np.unique(np.sort(np.append(a_path, a_check)))
d_path = []
g_path = []
for a_val in a_path:
d_val, g_val = calc_d_and_grad(a_val)
d_path.append(d_val)
g_path.append(g_val)
d_path = np.array(d_path)
g_path = np.array(g_path)
print('\n基于 f(a) 的动画数据:')
print(f'a 起点={a_path[0]:.4f}, d={d_path[0]:.4f}, dd/da={g_path[0]:.4f}')
print(f'a 终点={a_path[-1]:.4f}, d={d_path[-1]:.4f}, dd/da={g_path[-1]:.4f}')
# 专门输出 a=2 的数值,便于核验
d_check, g_check = calc_d_and_grad(a_check)
print(f'a={a_check:.1f} 时:d=f(a)={d_check:.4f}, dd/da={g_check:.4f}(理论应为 d=1024, 梯度=512)')
# 绘图:左图 d=f(a);右图 梯度 dd/da
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.8))
ax1.plot(a_grid, d_grid, color='tab:blue', linewidth=2.2, label='d = f(a)')
point_d, = ax1.plot([], [], 'o', color='tab:red', markersize=8, label='当前点')
ax1.axvline(a_check, linestyle=':', color='gray', linewidth=1.2)
ax1.scatter([a_check], [d_check], color='tab:purple', s=35, zorder=5, label='a=2 对应点')
ax1.set_title('函数值 d=f(a)')
ax1.set_xlabel('a')
ax1.set_ylabel('d')
# 由于负半轴分支会到很大绝对值,使用 symlog 让 1024 更容易观察
ax1.set_yscale('symlog', linthresh=1000)
ax1.grid(alpha=0.25)
ax1.legend(loc='upper center', bbox_to_anchor=(0.5, -0.18), ncol=3, fontsize=9)
ax2.plot(a_grid, g_grid, color='tab:green', linewidth=2.2, label='梯度 dd/da')
point_g, = ax2.plot([], [], 'o', color='black', markersize=7, label='当前点')
ax2.axvline(a_check, linestyle=':', color='gray', linewidth=1.2)
ax2.scatter([a_check], [g_check], color='tab:purple', s=35, zorder=5, label='a=2 对应点')
ax2.set_title('梯度曲线 dd/da')
ax2.set_xlabel('a')
ax2.set_ylabel('dd/da')
# 同样使用 symlog,避免正负梯度量级差异导致细节不明显
ax2.set_yscale('symlog', linthresh=1000)
ax2.grid(alpha=0.25)
ax2.legend(loc='upper center', bbox_to_anchor=(0.5, -0.18), ncol=3, fontsize=9)
info_text = ax1.text(
0.03, 0.95, '',
transform=ax1.transAxes,
va='top',
fontsize=10,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.85)
)
def update(frame):
a_now = a_path[frame]
d_now = d_path[frame]
g_now = g_path[frame]
point_d.set_data([a_now], [d_now])
point_g.set_data([a_now], [g_now])
info_text.set_text(
f'帧: {frame}\n'
f'a = {a_now:.4f}\n'
f'd = f(a) = {d_now:.4f}\n'
f'dd/da = {g_now:.4f}'
)
return point_d, point_g, info_text
anim = FuncAnimation(fig, update, frames=len(a_path), interval=450, repeat=True)
# 保存 GIF(可直接放到 GitHub Blog)
output_dir = Path(__file__).resolve().parent / 'assets'
output_dir.mkdir(exist_ok=True)
gif_path = output_dir / 'f_a_gradient_change.gif'
anim.save(gif_path, writer='pillow', fps=3)
print(f'GIF 已保存:{gif_path}')
fig.suptitle('f(a) 的梯度变化过程(真实 dd/da 动画)', fontsize=13)
plt.tight_layout(rect=[0, 0.08, 1, 0.94])
plt.show()
深度学习开发标准五部曲(工程视角)
定义函数 → 定义目标 → 自动求导 → 用梯度改参数 → 循环
| 步骤 | 开发者做什么 (You) | PyTorch 做什么 (Framework) |
|---|---|---|
| 1️⃣ 建模 | 定义函数 f(a)(前向传播逻辑) |
实时构建动态计算图 |
| 2️⃣ 运行 | 传入数据,得到输出 d |
记录每一步的中间变量和操作 |
| 3️⃣ 求导 | 调用 d.backward() |
自动应用链式法则计算梯度 |
| 4️⃣ 优化 | 根据 a.grad 更新参数 |
提供优化算法(如 SGD、Adam) |

0
次点赞