1. 项目概述为什么“数学不硬核模型就飘”是数据科学最真实的生存法则我带过三十多个从零起步转行的数据科学学员也给十几家中小企业的算法团队做过技术复盘。每次聊到模型效果瓶颈、训练不稳定、调参像玄学最后八成问题都卡在同一个地方——不是代码写错了也不是数据质量差而是对背后那几行数学公式的理解停留在“抄作业”层面。比如你用 PyTorch 写完一个线性层nn.Linear(784, 128)真清楚它内部做的矩阵乘法W x b中W的形状为什么是(128, 784)为什么不能反过来当梯度爆炸时你第一反应是加torch.nn.utils.clip_grad_norm_但有没有想过这个操作本质上是在约束哪个范数它和L2正则项在数学上是什么关系这些都不是“知道就行”的知识点而是你每天调试模型时键盘敲下的每一行代码所依赖的底层逻辑。这篇内容就是为那些已经写过pip install torch、跑通过 MNIST 分类、却在读论文时被∇_θ J(θ)吓退或在调优时对着learning_rate1e-3盲目试错的人准备的。它不讲抽象证明不堆积分符号只聚焦一个目标让你在写np.dot(A, B)时脑子里能立刻浮现出 A 的列空间如何被 B 的行向量线性组合在看到svd(A)输出三个矩阵时能马上对应到图像压缩中哪部分存了轮廓、哪部分存了纹理噪声在计算KL(p||q)时能意识到这不是一个对称距离而是在强制让 q 去拟合 p 的“信息重心”。核心关键词就是标量、向量、矩阵、张量、行列式、特征值与特征向量、范数、矩阵分解、熵、KL 散度、梯度下降——它们不是教科书里的静态定义而是你构建、调试、解释每一个深度学习模型时手边最趁手的几把螺丝刀。适合谁来读第一类是刚结束 Python 基础、正准备啃《深度学习》花书的自学者你需要的不是从极限开始的数学分析而是“这个公式现在就能用在哪”第二类是工作两年、能调参但总被问“为什么选这个 loss”的工程师你需要补上那层“原理透明”的底气第三类是带团队的技术负责人你需要一套能快速帮新人建立直觉的讲解框架而不是扔一本《矩阵论》过去。我不会假设你记得拉格朗日乘子法但会确保你亲手算出一个 2×2 矩阵的特征向量后能指着结果说“看这个方向上的向量被矩阵变换后只伸缩不旋转——这就是神经网络里‘主成分’的原始模样。”2. 核心概念解构从“是什么”到“为什么非得这样设计”2.1 标量、向量、矩阵、张量数据容器的层级进化论很多人一上来就被“张量”这个词吓住觉得是 TensorFlow 或 PyTorch 专属的高阶概念。其实完全不是。我们从最基础的物理世界说起你手机屏幕上一个像素点的亮度就是一个标量scalar——它只有一个数字比如128。这个数字本身没有方向没有结构它就是“多少”的终极答案。在 NumPy 里np.array(128)创建的就是一个标量。它的数学意义在于它是所有线性代数运算的“原子单位”是构成更复杂结构的基石。当你需要描述一个物体的运动时“速度”就不能只用一个数字了。你说“车速 60 公里/小时”这信息不完整——它往东开还是往西开所以你必须加上方向变成“向东 60 公里/小时”。这就是向量vector——一个有序的数字列表每个数字代表在某个坐标轴如 x, y, z上的分量。在 NumPy 中np.array([60, 0, 0])就是一个三维向量表示纯 x 方向的速度。关键点在于“有序”和“可运算”两个向量能相加[60,0,0] [0,30,0] [60,30,0]能被一个标量缩放2 * [60,0,0] [120,0,0]。这种运算能力正是神经网络中“权重更新”的数学原型w w - lr * grad本质就是用一个标量学习率lr去缩放梯度向量grad再与原权重向量w相加。再往上走当你需要描述一个力作用在物体上产生的效果时单靠一个向量就不够了。比如一个力不仅让物体移动还让它旋转。这时就需要矩阵matrix——一个二维的数字表格。你可以把它想象成一个“变换说明书”它规定了空间中的每一个点经过这个变换后应该挪到哪里。例如一个 2×2 矩阵[[0,-1],[1,0]]作用在向量[1,0]上得到[[0,-1],[1,0]] [1,0] [0,1]这恰好是将 x 轴正方向逆时针旋转了 90 度。神经网络里的全连接层y Wx bW就是这样一个矩阵它把输入特征x这个向量“扭曲”、“拉伸”、“旋转”成一个新的向量y为后续的非线性激活做准备。那么张量tensor是什么它就是矩阵的自然延伸。矩阵是二维的行×列张量可以是任意维度。一张彩色图片在计算机里就是一个三维张量(高度, 宽度, 通道)比如(224, 224, 3)。一个包含 32 张图片的 mini-batch就是一个四维张量(批次大小, 高度, 宽度, 通道)即(32, 224, 224, 3)。PyTorch 和 TensorFlow 的核心数据结构Tensor就是这个名字的由来——它不是一个特定的数学对象而是一个通用的、支持自动微分的多维数组容器。理解这一点至关重要你不需要背诵“张量的协变与逆变”只需要记住张量是数据的“形状”本身而矩阵运算是对这个形状施加的“规则”。当你调用torch.nn.Conv2d时卷积核就是一个小的四维张量(out_channels, in_channels, kernel_h, kernel_w)它在输入张量上滑动执行的是一系列小矩阵乘法最终输出另一个张量。整个过程就是不同维度的张量按照预设的矩阵运算规则进行着精密的“形状匹配”与“数值变换”。提示初学者最容易混淆的是“向量是列还是行”。在 NumPy 中np.array([1,2,3])默认创建的是一个一维数组它既不是严格的列向量也不是行向量。当你需要明确其方向时要用reshapev_col np.array([1,2,3]).reshape(-1,1)得到列向量(3,1)v_row np.array([1,2,3]).reshape(1,-1)得到行向量(1,3)。这个细节在矩阵乘法A B中极其关键A的列数必须等于B的行数。如果你不小心把一个(3,)的一维数组当作(3,1)来用操作会静默失败或给出错误结果。2.2 行列式与特征值矩阵的“灵魂刻度”与“不变方向”如果把矩阵比作一个黑箱变换器那么行列式determinant就是这个黑箱的“体积缩放因子”。它告诉你这个变换对空间做了多大程度的“拉伸”或“压缩”。一个 2×2 矩阵[[a,b],[c,d]]的行列式是ad - bc。这个公式看起来像凭空捏造但它有非常直观的几何解释它等于由矩阵的两列向量所张成的平行四边形的有向面积。如果行列式是 2意味着这个变换把任何图形的面积都放大了 2 倍如果是 0.5就缩小了一半如果是负数比如 -1则表示除了缩放还做了一次镜像翻转改变了空间的“手性”。为什么这很重要在深度学习里行列式直接关联到可逆性。一个矩阵只有在行列式不为零时才存在逆矩阵。而逆矩阵是求解线性方程组Ax b的钥匙。在某些生成模型如 Normalizing Flows中模型的核心就是构造一系列可逆变换其目标函数里就包含了对所有变换行列式的对数求和log-determinant。如果你不理解行列式代表“体积变化”你就无法理解为什么模型要费尽心思去保证每一步变换都是可逆的——因为不可逆就意味着信息丢失就像把一张高清图压缩成一个像素点再也无法还原。而特征值eigenvalue和特征向量eigenvector则是这个黑箱的“灵魂”。它们回答了一个根本问题在这个变换下有没有一些特殊的向量它们被变换后方向完全不变只是被拉长或缩短了这些“方向不变”的向量就是特征向量那个“拉长或缩短”的倍数就是对应的特征值。举个具体例子。假设你有一个简单的 2×2 矩阵A [[2,0],[0,0.5]]。它的特征向量很容易猜出来[1,0]和[0,1]。因为A [1,0] [2,0] 2 * [1,0]所以[1,0]是特征向量对应特征值2同理A [0,1] [0,0.5] 0.5 * [0,1]所以[0,1]是特征向量对应特征值0.5。这意味着沿着 x 轴的方向这个变换会把它拉长 2 倍而沿着 y 轴的方向会把它压缩到一半。整个空间被这个矩阵“拉扯”成了一个椭圆。这个概念在 PCA主成分分析中是核心。PCA 的目标就是找到数据分布中“方差最大”的方向这个方向恰恰就是数据协方差矩阵的最大特征值所对应的特征向量。它代表了数据最主要的“伸展”方向。在神经网络中权重矩阵的特征值谱所有特征值的集合是判断训练稳定性的关键指标。如果最大特征值远大于 1梯度在反向传播时就会指数级放大梯度爆炸如果远小于 1梯度就会指数级衰减梯度消失。很多初始化方法如 He 初始化、Xavier 初始化的理论依据就是通过控制权重矩阵的初始方差来让其特征值尽量落在[-1, 1]区间内从而保证梯度流动的健康。注意NumPy 的np.linalg.eig返回的特征向量是按列排列的。eigenvectors[:, i]才是第i个特征值eigenvalues[i]对应的特征向量。新手常犯的错误是直接用eigenvectors[i]这取到的是第i行完全不是特征向量。另外特征向量的长度是任意的因为A (2*v) 2*(A v) 2*λ*v所以np.linalg.eig返回的通常是单位长度的特征向量这是为了标准化。2.3 范数、矩阵分解与伪逆模型优化的三大支柱在深度学习的日常工作中你几乎每分钟都在和范数norm打交道只是可能没意识到。L2范数也就是欧几里得范数||x||₂ √(x₁² x₂² ... xₙ²)它衡量的是向量x的“长度”。在 PyTorch 里torch.norm(x, p2)就是计算这个。它的平方||x||₂²就是我们最熟悉的L2正则化项Weight Decay。为什么加这个因为它惩罚的是权重向量的“总长度”迫使模型倾向于选择一组“小而分散”的权重而不是一组“大而稀疏”的权重从而提升泛化能力防止过拟合。L1范数||x||₁ |x₁| |x₂| ... |xₙ|则不同它倾向于产生稀疏解。因为L1的等高线是菱形它与损失函数等高线相切的点更容易落在坐标轴上即某个xᵢ 0。这就是 Lasso 回归的原理也是为什么在某些需要特征选择的场景下L1正则化比L2更受青睐。矩阵分解Matrix Factorization则是将一个复杂的矩阵“拆解”成几个更简单、更有意义的矩阵的乘积。这就像把一道复杂的菜谱拆解成几味基础调料的组合。最常见的几种LU 分解把一个方阵A拆成一个下三角矩阵L和一个上三角矩阵U的乘积A L U。它的主要价值在于高效求解线性方程组。一旦你有了L和U解Ax b就变成了先解Ly b前向替换再解Ux y后向替换这两个步骤都比直接解Ax b快得多。虽然在现代深度学习框架中我们很少手动做 LU但理解它能让你明白为什么torch.linalg.solve这样的函数如此高效。QR 分解把一个m×n矩阵A拆成一个正交矩阵QQ.T Q I和一个上三角矩阵R的乘积A Q R。正交矩阵的性质是“保距”的它不会改变向量的长度和夹角。因此QR 分解常用于最小二乘问题的求解它比直接计算(A.T A)⁻¹ A.T b更加数值稳定尤其是在A接近奇异行列式接近零时。SVD奇异值分解这是最强大、应用最广的分解。它把任意m×n矩阵A拆成A U Σ V.T其中U和V都是正交矩阵Σ是一个对角矩阵对角线上的元素叫奇异值singular values它们都是非负实数并且按降序排列。U的列向量叫左奇异向量V的列向量叫右奇异向量。SVD 的威力在于它揭示了矩阵A的内在秩结构。最大的几个奇异值对应了A中最主要的信息而后面很小的奇异值往往对应着噪声。因此截断 SVDTruncated SVD是一种强大的降维和去噪工具。在推荐系统中用户-物品评分矩阵通常非常稀疏用 SVD 可以将其近似为低秩矩阵从而预测用户对未评分物品的喜好。Moore-Penrose 伪逆Pseudoinverse记作A⁺是当矩阵A不可逆比如不是方阵或者行列式为零时“最接近”逆矩阵的东西。它的定义是满足四个 Penrose 条件的唯一矩阵。在实践中它最常用的地方就是求解最小二乘解。对于超定方程组Ax ≈ b方程数多于未知数x A⁺ b给出的是使||Ax - b||₂最小的那个x。在 NumPy 中np.linalg.pinv(A)就是计算它。值得注意的是A⁺的计算通常就是通过 SVD 来完成的A⁺ V Σ⁺ U.T其中Σ⁺是将Σ中非零的奇异值取倒数零奇异值保持为零。这再次印证了 SVD 的基础地位——它是连接矩阵结构、优化问题和数值计算的桥梁。3. 实操精要从代码片段到可复现的工程化理解3.1 向量与矩阵运算不只是,-,*更是空间直觉的构建让我们从最基础的 NumPy 代码开始但这次我们要“看见”它背后的几何意义。首先创建两个向量import numpy as np import matplotlib.pyplot as plt # 创建两个二维向量 a np.array([3, 1]) b np.array([1, 2]) # 向量加法平行四边形法则 c a b # [4, 3] # 可视化 plt.figure(figsize(8, 6)) plt.quiver(0, 0, a[0], a[1], anglesxy, scale_unitsxy, scale1, colorr, labela[3,1]) plt.quiver(0, 0, b[0], b[1], anglesxy, scale_unitsxy, scale1, colorb, labelb[1,2]) plt.quiver(0, 0, c[0], c[1], anglesxy, scale_unitsxy, scale1, colorg, labelcab[4,3]) plt.quiver(a[0], a[1], b[0], b[1], anglesxy, scale_unitsxy, scale1, colorb, alpha0.5, linestyledashed) plt.quiver(b[0], b[1], a[0], a[1], anglesxy, scale_unitsxy, scale1, colorr, alpha0.5, linestyledashed) plt.xlim(0, 5) plt.ylim(0, 4) plt.grid(True) plt.legend() plt.title(Vector Addition: The Parallelogram Rule) plt.show()运行这段代码你会看到一个清晰的平行四边形。红色箭头是a蓝色箭头是b绿色箭头是c。两条虚线分别表示“把b平移到a的头顶”和“把a平移到b的头顶”。这个图景就是你大脑里应该有的向量加法模型。它不再是314, 123的机械计算而是空间中两个力的合成。接下来理解矩阵乘法。创建一个 2×2 矩阵M它代表一个“剪切”shear变换# 创建一个剪切矩阵 M [[1, 1], [0, 1]] # 它的作用是x x y, y y M np.array([[1, 1], [0, 1]]) # 对向量 a 和 b 分别进行变换 a_transformed M a # [31, 1] [4, 1] b_transformed M b # [12, 2] [3, 2] # 可视化变换前后的对比 fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 5)) # 原始空间 ax1.quiver(0, 0, a[0], a[1], anglesxy, scale_unitsxy, scale1, colorr, labela) ax1.quiver(0, 0, b[0], b[1], anglesxy, scale_unitsxy, scale1, colorb, labelb) ax1.set_xlim(0, 5) ax1.set_ylim(0, 4) ax1.grid(True) ax1.set_title(Original Space) ax1.legend() # 变换后空间 ax2.quiver(0, 0, a_transformed[0], a_transformed[1], anglesxy, scale_unitsxy, scale1, colorr, labelMa) ax2.quiver(0, 0, b_transformed[0], b_transformed[1], anglesxy, scale_unitsxy, scale1, colorb, labelMb) ax2.set_xlim(0, 5) ax2.set_ylim(0, 4) ax2.grid(True) ax2.set_title(After Shear Transformation (M)) ax2.legend() plt.show()你会发现b的 y 分量没变但 x 分量增加了y的值123这就是剪切的效果。更重要的是a b的变换结果一定等于a的变换结果加上b的变换结果M (a b) M a M b。这就是线性变换的叠加性。这个性质是神经网络能够层层堆叠、每一层都只做线性变换Wx b然后接非线性激活σ的数学根基。没有叠加性多层网络就失去了可组合的意义。实操心得在调试自定义层时一个极其实用的检查是“线性性验证”。随机生成两个输入x1,x2和一个标量alpha然后检查layer(alpha*x1 x2)是否等于alpha*layer(x1) layer(x2)允许微小的浮点误差。如果不等说明你的层里混入了非线性操作比如torch.mean在 batch 维度上求均值它就不是线性的这可能会在分布式训练或某些优化器下引发难以察觉的 bug。3.2 SVD 与伪逆从图像压缩到最小二乘的完整闭环SVD 是一个“重武器”我们用一个具体的图像压缩例子来展示它的威力。我们将一张灰度图一个二维矩阵进行 SVD然后只保留前k个最大的奇异值看看重建的图像质量如何变化。from PIL import Image import numpy as np import matplotlib.pyplot as plt # 加载并转换为灰度图这里用一个简单的 8x8 矩阵模拟 # 实际中你可以用 Image.open(your_image.jpg).convert(L) original_img np.array([ [255, 200, 150, 100, 50, 0, 0, 0], [200, 180, 160, 140, 120, 100, 80, 60], [150, 160, 170, 180, 190, 200, 210, 220], [100, 140, 180, 220, 255, 255, 255, 255], [ 50, 120, 190, 255, 255, 255, 255, 255], [ 0, 100, 200, 255, 255, 255, 255, 255], [ 0, 80, 210, 255, 255, 255, 255, 255], [ 0, 60, 220, 255, 255, 255, 255, 255] ]) # 执行 SVD U, s, Vt np.linalg.svd(original_img, full_matricesFalse) # 计算原始图像的 Frobinius 范数总能量 original_norm np.linalg.norm(original_img, fro) print(fOriginal image shape: {original_img.shape}) print(fNumber of singular values: {len(s)}) print(fOriginal Frobenius norm: {original_norm:.2f}) # 尝试不同的 k 值进行重建 ks [1, 2, 4, 8] fig, axes plt.subplots(2, 2, figsize(10, 10)) axes axes.flatten() for i, k in enumerate(ks): # 用前 k 个奇异值重建 # s_k 是一个长度为 k 的向量我们需要把它变成一个 k x k 的对角矩阵 s_k np.diag(s[:k]) # U_k 是 U 的前 k 列Vt_k 是 Vt 的前 k 行 U_k U[:, :k] Vt_k Vt[:k, :] # 重建图像 reconstructed_img U_k s_k Vt_k # 计算重建误差 error_norm np.linalg.norm(original_img - reconstructed_img, fro) error_ratio error_norm / original_norm * 100 # 绘制 im axes[i].imshow(reconstructed_img, cmapgray, vmin0, vmax255) axes[i].set_title(fk{k}\nError: {error_ratio:.1f}%) axes[i].axis(off) plt.colorbar(im, axaxes, shrink0.8) plt.suptitle(Image Reconstruction using Truncated SVD, fontsize16) plt.tight_layout() plt.show()运行这段代码你会看到当k1时重建图像是一个非常模糊的、只保留了大致明暗对比的“影子”当k4时已经能看到主要的结构当k8即使用全部奇异值时重建图像是完美的误差为 0%。这个过程完美诠释了 SVD 如何将一个矩阵的信息按重要性奇异值大小进行排序和剥离。现在我们把这个知识迁移到一个实际的机器学习问题上线性回归的最小二乘解。假设我们有一组数据点(x_i, y_i)想用一条直线y wx b来拟合。我们可以把这个问题写成矩阵形式X β y其中X是设计矩阵每行是[x_i, 1]β [w, b]是参数向量y是目标向量。由于数据点通常多于参数个数这是一个超定系统没有精确解只有最小二乘解β (X.T X)⁻¹ X.T y。但直接计算(X.T X)⁻¹在X接近奇异时是病态的。更好的方法是用伪逆β np.linalg.pinv(X) y。# 生成模拟数据 np.random.seed(42) x_data np.linspace(0, 10, 50) y_true 2.5 * x_data 1.0 # 真实的直线 y_noise y_true np.random.normal(0, 2, sizex_data.shape) # 加入噪声 # 构建设计矩阵 X: 每行是 [x_i, 1] X np.column_stack((x_data, np.ones_like(x_data))) y y_noise # 方法1直接计算 (X.T X)⁻¹ X.T y try: beta_direct np.linalg.inv(X.T X) X.T y except np.linalg.LinAlgError: print(Direct method failed: X.T X is singular or ill-conditioned.) # 方法2使用伪逆推荐 beta_pinv np.linalg.pinv(X) y # 方法3使用 SVD 手动计算理解伪逆的本质 U, s, Vt np.linalg.svd(X, full_matricesFalse) # 计算 Σ⁺对 s 中的非零值取倒数零值保持为零 s_inv np.where(s 1e-10, 1.0 / s, 0.0) # Σ⁺ 是一个 n x m 矩阵其中 n 是 Vt 的行数m 是 U 的列数 # 这里 X 是 (50, 2)所以 U 是 (50, 2), s 是 (2,), Vt 是 (2, 2) # Σ⁺ 应该是 (2, 50)但我们可以通过广播来简化 # 更标准的做法是A⁺ V diag(s_inv) U.T Sigma_plus np.diag(s_inv) beta_svd Vt.T Sigma_plus U.T y print(fDirect method result: w{beta_direct[0]:.4f}, b{beta_direct[1]:.4f}) print(fPseudo-inverse result: w{beta_pinv[0]:.4f}, b{beta_pinv[1]:.4f}) print(fSVD manual result: w{beta_svd[0]:.4f}, b{beta_svd[1]:.4f}) # 绘制拟合结果 plt.figure(figsize(10, 6)) plt.scatter(x_data, y_noise, alpha0.6, labelNoisy Data) plt.plot(x_data, 2.5*x_data 1.0, g--, labelTrue Line (y2.5x1)) plt.plot(x_data, beta_pinv[0]*x_data beta_pinv[1], r-, labelfFitted Line (w{beta_pinv[0]:.2f}, b{beta_pinv[1]:.2f})) plt.xlabel(x) plt.ylabel(y) plt.legend() plt.title(Linear Regression via Pseudo-Inverse) plt.grid(True) plt.show()这段代码展示了三种求解方法。你会发现直接法在数据量大或X条件数差时容易失败而伪逆和 SVD 手动法总是能给出稳定的结果。更重要的是通过手动实现 SVD你彻底明白了伪逆A⁺的计算过程它本质上是利用 SVD 将A的“有效信息”大的奇异值保留下来而将“无效信息”小的、接近零的奇异值过滤掉从而获得一个鲁棒的解。这正是深度学习中许多正则化思想的源头。3.3 熵与 KL 散度从信息论到模型评估的量化语言熵Entropy和 KL 散度Kullback-Leibler Divergence是连接概率论、信息论和机器学习的桥梁。它们提供了一套精确的、可计算的“语言”来描述模型的不确定性、数据的分布差异以及学习过程的效率。首先澄清一个常见误区scipy.stats.entropy函数的输入必须是概率质量函数PMF或概率密度函数PDF的离散采样值且这些值的和必须为 1。在你提供的原始代码中scipy.stats.norm.pdf(a)计算的是正态分布在点a处的概率密度这些密度值的和并不等于 1因为 PDF 是连续函数其积分等于 1而非求和。直接将它们传给entropy函数得到的结果是没有严格信息论意义的。正确的做法是先对数据进行直方图统计得到经验 PMF。import numpy as np import matplotlib.pyplot as plt from scipy import stats # 生成两组不同的数据分布 np.random.seed(42) data_p np.random.normal(loc0, scale1, size1000) # 标准正态分布 data_q np.random.normal(loc0.5, scale1.2, size1000) # 偏移且更宽的正态分布 # 方法1使用直方图估计 PMF推荐更符合信息论直觉 def estimate_pmf(data, bins50): hist, bin_edges np.histogram(data, binsbins, densityTrue) # densityTrue 返回的是 PDF我们需要 PMF所以乘以 bin width bin_width bin_edges[1] - bin_edges[0] pmf hist * bin_width # 确保和为1浮点精度 pmf pmf / pmf.sum() return pmf, bin_edges pmf_p, _ estimate_pmf(data_p) pmf_q, _ estimate_pmf(data_q) # 计算熵 entropy_p stats.entropy(pmf_p, base2) # 以2为底单位是比特 entropy_q stats.entropy(pmf_q, base2) # 计算 KL 散度 D(P||Q) 和 D(Q||P) kl_pq stats.entropy(pmf_p, pmf_q, base2) kl_qp stats.entropy(pmf_q, pmf_p, base2) print(fDistribution P (N(0,1)):) print(f Entropy: {entropy_p:.4f} bits) print(fDistribution Q (N(0.5,1.2)):) print(f Entropy: {entropy_q:.4f} bits) print(fKL Divergence D(P||Q): {kl_pq:.4f} bits) print(fKL Divergence D(Q||P): {kl_qp:.4f} bits) print(fNote: D(P||Q) ! D(Q||P), confirming asymmetry.) # 可视化 fig, (ax1