你有没有注意过:有些厨师做菜全凭感觉,围裙口袋里塞着各种私藏调料,上次多放了盐,这次就可能少放糖——你永远没法预测他端出来的是什么味道。而另一些餐厅,每道菜的配方都写成一张“菜谱卡”:食材、火候、调料用量,全部写得明明白白。只要食材一模一样,任何厨师照着做,出来的味道也一模一样。这张菜谱卡,就是计算机世界里的“纯函数”。 这篇文章要讲的 JAX 纯函数范式,正是把深度学习的模型代码改写成一张张“菜谱卡”:模型参数不再是藏在对象口袋里的秘密,而是像食材一样摆在桌面上,显式地传给函数;训练过程也不是把旧菜谱涂改掉,而是拿着旧菜谱生成一张新菜谱。你会看到,这种“把事情摊开做”的风格,让代码变得可复现、可并行、可组合,也让 JAX 的自动微分、即时编译等强大能力有了施展的舞台。
掌握纯函数范式,意味着你能写出可复现、易并行、好调试的深度学习代码,同时吃透 JAX 的设计哲学,为迁移到函数式深度学习、构建大规模训练系统打下坚实基础。
可视化演示
展示标记-清除垃圾回收算法的执行过程:从GC根出发标记可达对象,然后清除不可达对象释放内存。
本节配有交互动效(进入视口后自动加载,约 3KB)。
初始状态 对象图中包含GC根(R1/R2)和8个堆对象 部分对象已无引用路径,等待回收 GC Roots = Stack + Globals + Registers
纯函数本质
回到那家餐厅。有张菜谱卡,写的是“西红柿炒蛋:番茄两个、鸡蛋三枚、盐三克,中火三分钟”。请问:换个厨师,食材不变,味道会变吗?不会。你可以把这张卡复印一百份,寄到一百家分店,只要买到的番茄鸡蛋一样,出锅的菜就一模一样。在编程世界里,这种“只靠输入就能完全决定输出,且绝不碰外界一根手指”的函数,就叫纯函数。
深度学习的代码,在大多数框架里并不纯粹。模型训练时可能悄悄改了全局日志、依赖了系统时间、或者在某次前向传播里顺手更新了缓冲区。而 JAX 把“纯函数”当作第一公民:每个算子、每个模型,最好都写成“给什么返回什么”的形态。用数学语言说,一个纯函数就是一条从输入集合到输出集合的确定映射。
纯函数有两条铁律:第一,引用透明——同一个表达式在任何地方求值,结果都一样;第二,无副作用——不修改全局变量、不写文件、不依赖外部状态。对深度学习而言,这意味着:同一份参数 $\theta$ 和同一样本 $x$,loss 永远一样。这正是复现实验、定位 bug、并行计算的根基。JAX 的 jit 变换会把函数重新执行很多次,如果函数里藏了副作用(比如打印日志),日志就会被重复输出,这种“不干净”的写法在函数式世界观里是头号大忌。
所以,纯函数就是一张“老实巴交”的菜谱卡:它不偷吃食材,不弄脏厨房,更不会因为今天厨师心情好就多放一勺糖。学会辨认和书写纯函数,是你走进 JAX 世界的第一步——接下来,我们就要把藏着掖着的模型状态,一张一张地摆到餐桌上来。
- x(输入食材)函数唯一的输入来源。在神经网络中,它可以是样本特征、模型参数等一切外部数据。
- f(映射规则)从输入到输出的确定变换。对应前向传播函数,内部只做计算,不看、不改任何外界的东西。
- y(确定输出)由 x 唯一决定的输出。同一个 x 在任何时刻、任何设备上调用,得到的 y 都完全相同,这就是可复现性。
显式状态传递
传统的面向对象厨房里,厨师会把私藏调料塞在围裙口袋里。你喊一声“做道菜”,他翻翻口袋自己找料,你根本不知道他用了什么。JAX 的哲学恰恰相反:所有食材必须全部摆在桌面上,一眼看尽。模型参数 $\theta$ 不再是对象的内部属性,而是作为一个普通参数,明明白白地写进函数签名里。
在 PyTorch 中,你写 model(x) 就能完成前向传播,因为 model 对象内部自己保存着权重。但在 JAX 中,标准的写法是 f(params, x):第一份资料是模型参数,第二份才是输入数据。没有隐藏状态,没有“函数内部自己知道的东西”——所有变量都来自参数,所有结果都流向返回值。
那么多个参数怎么传递?JAX 提供了 Pytree 结构,它像一个多格子的收纳盒:把神经网络的权重、偏置、甚至优化器的动量状态,全部塞进同一个盒子,作为单个参数传入函数。函数内部需要用哪些格子,就打开哪个格子取。收纳盒让“显式传递”不再繁琐,反而比一堆散落的全局变量更整洁、更有条理。
把食材从口袋里掏出来摆在桌上,一开始可能不习惯,但好处立刻显现:任何人看到 f(params, x) 这个签名,就知道这个函数需要什么;任何人看到返回值,就知道它产出了什么。状态不再神出鬼没,代码变得像菜谱卡一样容易被“读懂和复现”。
- $\theta$(显式参数)模型权重作为第一个参数显式传入,不再隐藏在对象内部。
- x(样本输入)当前批次的数据,与参数并列作为函数输入。
- f(纯函数)只依赖 $\theta$ 和 $x$,不读取任何外部状态,也不修改任何外部变量。
- y(输出结果)预测值或损失值,通过返回值流出函数,而不是写进某个全局位置。
不可变更新
假设餐厅老板想改良菜谱,他会怎么做?把旧菜谱卡涂掉重写吗?不会——他更可能先复印一张,在新副本上改:万一新配方失败,旧配方还完好无损。JAX 的训练循环正是这种“复印改稿”的思路:参数一旦创建,就永远不被修改;每次更新,都是基于旧参数生成一份全新的参数结构。
这就是“函数式更新”。传统写法里,optimizer.step() 会悄悄地把 model.weight 原地修改掉;而 JAX 中,你写 params = update(params, grads),右边用旧参数计算出新参数,左边的变量名虽然还是 params,但指向的是全新的对象。旧参数对象依然存在于内存中,随时可以拿出来回溯、对比、甚至继续训练。
随机数也要这样管理。在 Python 里,random.seed(0) 之后调用 random.random(),生成器内部状态在悄悄变化——这不是纯函数。JAX 的做法是把随机数“钥匙” key 当作显式参数传递,每次用完就用 jax.random.split(key) 分裂出新钥匙,旧钥匙永远保持原样。这样,每一次随机采样都变成了可复现的纯函数调用。
“旧卡永远不改”听起来像浪费内存,但正是这种不可变性,让训练过程像一本拍立得相册:每一轮的参数都是一张永久保存的照片。你可以翻回第 1200 轮的照片重新推理,也可以把同一张照片发给一百家分店同时试菜——这就是可复现与可并行的秘密。
- $\theta$(旧参数)当前训练轮次的参数,保持不可变,作为更新过程的输入。
- g(梯度)损失对参数的偏导,指明参数应该往哪个方向调整。
- $\eta$(学习率)每步更新的步长标量,控制参数移动的幅度。
- $\theta'$(新参数)更新后生成的全新参数结构,旧参数对象没有被修改。
模块化设计
一张完整的宴席菜单,从来不是一句话写成的,而是由几十张小菜谱卡组合而成:前菜、主菜、甜点、汤羹,各司其职。JAX 的模块化设计也是一样:复杂的神经网络,由一个个纯函数小块组合而成,每个小块只负责一件事。
组合的方式就是“函数复合”。如果 f 是特征提取层,g 是分类层,那么整个模型就是先经过 f 再经过 g,写成数学记号就是 $g \circ f$。在代码里,你甚至不需要特殊的容器类,直接写 h = lambda x: g(f(x)) 就完成了组合。每个小函数都可以独立测试、独立复用,就像单独检验一张菜谱卡的正确性。
JAX 还大量使用高阶函数——把“变换”当作参数传递的函数。比如 jax.grad 接收一个函数,返回它的梯度函数;jax.jit 接收一个函数,返回编译加速后的版本。这种“函数吃函数”的玩法,让能力的组合变得极其灵活:你可以把纯函数理解成一个积木,高阶变换就是积木之间的通用接口,任意积木都能咬合。
积木式的设计让团队协作变得顺畅:每个人只负责打磨几张“小菜谱卡”,然后用统一的接口把它们拼起来。哪张卡出了问题,单独测试那一张就能定位;哪张卡想升级,换一张新卡接上去即可,不影响厨房里的其他任何东西。
- f(前段工序)第一个小纯函数,接收原始输入并产生中间结果。
- g(后段工序)第二个小纯函数,接收中间结果并产生最终输出。
- $\circ$(复合操作)函数复合的数学符号,表示“先做 f,再做 g”。
- h(组合函数)复合后得到的新函数,整体依然是一个纯函数,可以继续参与复合。
PyTorch对比
到这一步,你可能想问:那 PyTorch 里的模型该怎么对应?在 PyTorch 里,模型是一位“围裙厨师”:net = nn.Linear(10, 1),权重存在 net.weight 这个围裙口袋里,你只管喊 net(x),他翻翻口袋自己找参数。而 JAX 的菜谱卡风格是:把 $\theta$ 摆上餐桌,写清楚 f(params, x)。
两种风格都能做出同一道菜,但状态存放的位置决定了代码的属性。nn.Module 将参数作为对象属性保存在 self 里,调用 model(x) 时隐式读取;JAX 则要求所有可变状态全部显式出现在函数签名中。对比来看:
从 PyTorch 迁移到 JAX,最需要改变的不是语法,而是心智模型:从“我是谁、我有什么属性”切换成“数据怎么流、变换怎么组合”。实操上的迁移路径很简单:第一步,把你模型里所有 self.xxx 列成一张状态清单;第二步,把这些状态打包成 Pytree,作为函数的第一个参数;第三步,把每个方法改写成“参数进、结果出”的纯函数。三步走完,你的代码就脱下了围裙,换上了菜谱卡。
- model(x)(对象式调用)PyTorch 中模型是带状态的对象,调用时偷偷读取内部 self 里的权重。
- f(θ, x)(函数式调用)JAX 中参数 θ 与输入 x 并列,作为显式参数传入函数。
- 状态位置对比两者算出的 y 可能完全等价,但依赖关系一个藏在对象里,一个摆在明面上。
工程优势
连锁餐厅的老板最头疼什么?一是每家分店口味不稳定,二是新菜推广太慢。菜谱卡模式完美解决了这两个问题:口味不稳定?同一张卡加同一批食材,味道必然一样;推广太慢?菜谱卡复印一千份,一千家分店同时开火。深度学习的工程实践,面临的正是同样的两个难题:训练不可复现、并行扩展困难。
纯函数范式让“可复现训练”变成一件自然的事。参数不可变、随机数显式管理、函数无副作用——训练过程中的每一个中间状态都被记录、可回溯。上周跑出来的 loss 曲线,这周重新跑一遍,几乎逐位一致。调试时,你只需要保存当时的参数快照和输入数据,就能像回放录像一样还原现场,不需要猜测“那个对象当时的状态是什么”。
并行方面,纯函数天然安全:多个设备、多个线程可以同时调用同一个函数,因为它们互不干扰、不共享可写状态。批量训练时,每个样本的损失计算都是独立的纯函数调用,把它们的结果平均起来就是整个 batch 的损失:
除此之外,无副作用还意味着“缓存友好”:同样的输入,输出必然相同,所以计算结果可以安全地被缓存复用;分布式训练时,梯度同步不需要加锁,因为不存在共享的可变状态。一套菜谱卡,既保证了口味统一,又让千百家分店可以同时开工——这正是工业级深度学习最需要的两个特质。
- N(样本数量)当前批次中的样本总数,对应一桌客人的数量。
- $\ell$(单样本损失)衡量单个预测与真实标签差距的损失函数。
- $\sum$(求和)把每个样本的损失加起来,代表整桌菜的总分。
- $\frac{1}{N}$(平均)除以样本数得到平均损失,作为训练优化的目标。
自动微分
训练神经网络,本质上是要回答一个问题:菜谱哪里该调整?传统做法靠厨师经验,机器学习靠梯度。在 JAX 里,求梯度不需要你手写反向传播,而是请来一位“营养分析师”——jax.grad。它拿到你的损失函数(菜谱卡),不改变原卡,而是生成一张新的“梯度菜谱卡”:输入同样的参数和数据,输出的是损失对参数的偏导数。
为什么 grad 能这么神奇?恰恰因为它要求被处理的函数是纯函数。营养分析师分析一份菜谱,只需要看食材和做法,不需要关心厨师的心情、厨房的温度、上次做菜留下的锅巴——这些外部因素(副作用)只会干扰分析。对于纯函数 L(θ, x, y),jax.grad 可以精确地计算出“参数 θ 动一点点,损失 L 会动多少”:
有了梯度卡,配合 jax.jit 把函数编译成底层机器码、用 jax.vmap 把批量维度自动向量化,JAX 就把“纯函数”这个抽象变成了强大的性能引擎。营养分析师不只会看一份菜谱——它能把菜谱复印到所有分店、并行分析全部菜品。整个训练循环,就是“分析、调整、复印、再分析”的循环,而每一轮都是纯粹、干净、可复现的。
grad 这张魔法纸复印出一张梯度卡:喂给它同样的参数和数据,它告诉你每个调料该加还是该减、力度多大。- $\mathcal{L}$(损失函数)衡量预测与真实值差距的纯函数,是“被分析”的对象。
- θ(可微参数)grad 默认对第一个参数求导,所以要把需要训练的参数放在函数第一位。
- grad(变换)JAX 的高阶变换,接收函数返回新函数,本身不修改原函数。
- $\nabla_\theta \mathcal{L}$(梯度)损失对参数的偏导数向量,指明参数更新的方向和大小。
AI 应用场景
大规模分布式训练
阶段:模型训练与加速
解决的问题:传统框架中权重散落在各设备的模型对象里,多卡同步需要小心翼翼处理锁和状态拷贝;纯函数范式将参数显式传出,每张卡各持一份参数副本做前向和反向,梯度聚合后统一生成新参数,天然无锁、无竞态。
具体案例:在 8 张 GPU 上使用 jax.pmap 并行训练视觉 Transformer。每张卡独立执行同一份“菜谱卡”,前向和反向互不干扰,最后对梯度做 all-reduce,再函数式地更新参数快照。整个流程没有共享可变状态,扩展性瓶颈只在于通信带宽。
训练实验复现与回溯
阶段:实验管理与调试
解决的问题:PyTorch 中参数被原地修改,想回到第 1200 轮的状态只能靠定期保存 checkpoint;纯函数范式下,每一轮更新都产生全新的参数对象,保存一个引用就等于拍下一张不会褪色的照片,复现和回溯成本极低。
具体案例:调参团队发现第 3000 轮的模型在验证集上表现异常,直接加载当时保存的参数快照,配合当时的输入数据重新执行前向推理,几分钟内定位到是某一层数值溢出,而非随机性干扰。
模型服务无状态切换
阶段:模型部署与在线推理
解决的问题:在线服务要切换模型版本时,传统做法需要重启进程或迁移内存中的对象状态;纯函数范式下,一次请求 = (params, input),服务进程完全不记忆任何模型状态,加载一份新参数就能瞬时完成版本切换。
具体案例:推荐系统在服务端同时持有两套参数快照,新模型上线前先以 10% 流量做 A/B 对比。由于推理函数是纯函数,两套参数可以并存于同一进程,互不污染,对比结果干净可信。
Python 演示
# -*- coding: utf-8 -*-# 纯函数范式演示: 用显式参数传递 + 函数式更新训练一个 MLPfrom copy import deepcopyfrom random import Random def relu(v): return max(0.0, v) def init_params(layers, seed): rng = Random(seed) params = [] for i in range(len(layers) - 1): w = [[rng.uniform(-0.5, 0.5) for _ in range(layers[i + 1])] for _ in range(layers[i])] b = [0.0] * layers[i + 1] params.append((w, b)) return params def forward(params, x): """纯函数: 只读 params 和 x, 不修改任何外部状态""" h = x for (w, b) in params[:-1]: h = [relu(sum(h[j] * w[j][k] for j in range(len(h))) + b[k]) for k in range(len(b))] w, b = params[-1] return [sum(h[j] * w[j][k] for j in range(len(h))) + b[k] for k in range(len(b))] def loss_fn(params, x, y): out = forward(params, x) return sum(0.5 * (out[k] - y[k]) ** 2 for k in range(len(y))) def numerical_grad(params, x, y, eps=1e-5): """数值梯度: 利用纯函数可复现性, 扰动参数后重新计算损失""" base = loss_fn(params, x, y) grads = [] for li, (w, b) in enumerate(params): gw = [] for j in range(len(w)): row = [] for k in range(len(w[j])): p2 = deepcopy(params) p2[li][0][j][k] += eps row.append((loss_fn(p2, x, y) - base) / eps) gw.append(row) gb = [] for k in range(len(b)): p2 = deepcopy(params) p2[li][1][k] += eps gb.append((loss_fn(p2, x, y) - base) / eps) grads.append((gw, gb)) return grads def update(params, grads, lr): """函数式更新: 返回新参数, 旧参数对象保持不可变""" new_params = [] for (w, b), (gw, gb) in zip(params, grads): nw = [[w[j][k] - lr * gw[j][k] for k in range(len(w[j]))] for j in range(len(w))] nb = [b[k] - lr * gb[k] for k in range(len(b))] new_params.append((nw, nb)) return new_params def train(): # XOR 问题: 输入两个二进制位, 输出异或结果 data = [ ([0.0, 0.0], [0.0]), ([0.0, 1.0], [1.0]), ([1.0, 0.0], [1.0]), ([1.0, 1.0], [0.0]), ] params = init_params([2, 8, 1], seed=42) for epoch in range(60): total = 0.0 for x, y in data: loss_before = loss_fn(params, x, y) # 旧参数只读 grads = numerical_grad(params, x, y) params = update(params, grads, lr=0.5) # 生成新参数 total += loss_fn(params, x, y) if epoch % 10 == 0: print("epoch %d loss %.6f" % (epoch, total / 4.0)) for x, y in data: pred = forward(params, x) print("input %s => %.3f (expect %.0f)" % (x, pred[0], y[0])) if __name__ == "__main__": train()
params = init_params([2, 8, 1], seed=42)用固定种子创建初始参数,保证每次运行起点一致,这是可复现的第一步。def forward(params, x):前向传播是纯函数,只读取 params 和 x,不修改任何外部状态。loss_before = loss_fn(params, x, y)更新前先记录旧参数下的损失,旧参数在这个过程中只读不改。p2 = deepcopy(params)计算梯度时复制一份参数再扰动,原参数对象完全不受影响。params = update(params, grads, lr=0.5)函数式更新:右边用旧参数算出一份全新参数,左边变量名重新绑定,旧参数仍可回溯。if epoch % 10 == 0:周期性打印平均损失,观察纯函数式训练循环是否在稳定收敛。
常见误区
误区我以为纯函数范式就是消灭一切状态,模型变成无记忆的“一次性工具”,那训练还怎么保存学到的知识?
正解纯函数不是没有状态,而是把状态从“隐藏的角落”搬到“明面的参数”。模型权重、优化器动量、随机数 key 全部作为显式参数传入传出。状态依然存在,只是不再偷偷摸摸,而是像食材一样摆在餐桌上,任何人一眼就能看到这道菜用了什么。
为何常见:因为“无副作用”常被误解为“无状态”,其实副作用指的是对外界隐式的影响,而显式传入传出的状态完全合法。
误区我在 PyTorch 里习惯先 `torch.manual_seed(0)` 再采样,到了 JAX 也照猫画虎,结果每跑一次结果都不一样,太诡异了。
正解JAX 的随机数生成器本身是有状态的,而这个状态必须显式传递。正确做法是把 key 作为函数参数传入,每次用 jax.random.split(key) 分裂出新 key,旧 key 保持原样。只有这样才能保证“相同输入相同输出”的纯函数性质。
为何常见:全局种子是面向对象时代的习惯,它依赖生成器内部状态,而这正是纯函数范式要消除的隐藏状态。
误区每次训练迭代都要生成一份全新参数,那不是每步都要复制一遍几十亿参数的模型?这不得慢死?
正解JAX 的数组是不可变对象,但底层通过 XLA 编译和缓冲区复用,params = update(params, grads) 并不会真的逐元素复制。函数式更新表达的是“逻辑上的新对象”,编译器会在不破坏不可变语义的前提下复用内存,实际运行时非常高效。
为何常见:我们习惯了 Python 对象的深拷贝思维,但 JAX 的不可变更新有编译器级别的优化,不能拿普通 Python 列表的复制成本来类比。
误区写论文用 JAX 炫一下可以,但真实生产系统还是 PyTorch 的 nn.Module 靠谱,纯函数太抽象了。
正解纯函数范式在工程上反而有独特优势:天然无锁并行、精确复现、缓存友好、模块可独立测试。多家大厂已经在 JAX 上跑工业级推荐系统和大模型训练。范式本身不分“学术”和“工业”,只分“是否适合你的团队和场景”。
为何常见:面向对象是主流入门教学的心智模型,人们对不熟悉的东西天然缺乏安全感,容易把“不习惯”误判成“不实用”。
误区我只要在函数里不写 `self.`,把所有变量都当作局部变量,就算“函数式”了吧?
正解真正的纯函数范式是一整套约定:参数必须显式传递、可变状态必须不可变更新、随机数必须用 key 管理、副作用必须彻底清除。只改表面写法,仍然可能偷偷依赖全局变量或外部状态。写 JAX 需要的是换一套心智模型,而不是换一套语法糖。
为何常见:语言语法相似会让人低估思维范式的差异,就像换了菜谱的排版格式,但厨师还是那个乱抓调料的厨师。
面试题
Q1. 下列对纯函数的描述,正确的是哪一项?
- A.纯函数可以修改全局变量来记录调用次数
- B.相同输入必然产生相同输出且无副作用← 正确答案
- C.纯函数可以依赖随机数生成器的内部状态
- D.纯函数输出与对象的内部属性和历史有关
答案:B。纯函数的两条铁律是“相同输入必得相同输出”和“无副作用”。选项 A 修改全局变量是副作用,C 依赖生成器内部状态会让输出不稳定,D 依赖对象内部属性属于隐藏状态,都与纯函数定义相悖。
Q2. 在 JAX 纯函数范式中,模型参数通常如何传递?
- A.通过全局变量在模块间共享模型权重
- B.将权重保存在类的实例属性中随时访问
- C.以 Pytree 结构作为函数参数显式传递← 正确答案
- D.使用进程间共享内存传递参数状态
答案:C。JAX 要求所有可变状态显式出现在函数签名中,参数打包成 Pytree 结构作为一个整体传入。全局变量、实例属性和共享内存都是把状态藏在函数外部,违背纯函数原则。
Q3. JAX 中随机数应如何管理才能保持纯函数性?
- A.设置全局随机种子后直接调用随机采样
- B.每次用 key 作为参数传入并分裂新 key← 正确答案
- C.用系统时间为种子保证每次结果不同
- D.把随机种子写死在函数内部即可复用
答案:B。全局种子依赖生成器的内部状态,系统时间引入外部不确定因素,种子写死在函数内则无法灵活控制。JAX 的做法是把 key 作为显式参数传入,采样后用 split 分裂出新 key,保证每次调用都是可复现的纯函数。
Q4. 纯函数范式带来的工程优势包括哪些?
- A.相同输入输出稳定确保训练可复现← 正确答案
- B.无隐藏状态天然适配并行与分布式← 正确答案
- C.自动降低了所有矩阵运算的计算量
- D.调试时可通过输入输出快照回溯问题← 正确答案
答案:ABD。可复现、易并行、可回溯都是纯函数无副作用的直接结果。但纯函数并不会自动降低矩阵运算量,计算复杂度取决于算法本身,与函数是否纯粹无关,因此 C 错误。
Q5. 与 PyTorch nn.Module 相比,JAX 纯函数范式的核心区别是什么?
- A.参数显式作为函数输入而非对象属性← 正确答案
- B.JAX 框架内部不支持神经网络的训练
- C.PyTorch 无法计算损失函数的梯度值
- D.两种范式在代码写法上没有任何区别
答案:A。PyTorch 把参数保存在 self 属性中由 model(x) 隐式读取,JAX 则把参数 θ 与输入 x 并列显式传入 f(θ, x)。B、C 均为错误陈述,D 与事实相反,两者在状态管理和心智模型上差异明显。
Q6. 关于 jax.grad 应用于纯函数,下列说法正确的有哪些?
- A.grad 默认只对函数的第一个参数求导← 正确答案
- B.要求被求导函数是消除副作用的纯函数← 正确答案
- C.grad 会原地修改原函数的参数值
- D.grad 返回新函数并支持和其他变换组合← 正确答案
答案:ABD。jax.grad 默认对第一个参数求导,所以可微参数要放首位;它要求函数是纯函数,否则无法精确分析;它返回一个新函数而不修改原函数,且可与 jit、vmap 组合。C 说反了,函数式范式中旧参数永远不会被原地修改。
Q7. 在函数式更新 params = update(params, grads) 中,旧参数对象发生了什么?
- A.旧参数对象被原地覆盖以节约内存空间
- B.旧参数保持不变新参数是全新对象结构← 正确答案
- C.旧参数立刻被回收且无法再被任何引用
- D.只有开启缓存模式时旧参数才不改变
答案:B。函数式更新的核心是不可变性:右边的 params 作为输入只读,update 基于它构建并返回全新的参数结构,旧对象依然存在。选项 A 是原地修改的面向对象做法,C 忽略了旧参数仍可被其他引用持有可能,D 与是否开启缓存无关。
Q8. jax.vmap 变换的主要作用是什么?
- A.将函数一次性编译为底层机器码显著加速
- B.自动把批量维度向量化映射进目标函数← 正确答案
- C.将纯函数转换为有状态的模块化类对象
- D.自动保存训练断点并支持任务的恢复
答案:B。vmap 自动把批量维度映射进函数内部,让你无需手动写 for 循环就能逐样本调用纯函数,同时保持向量化性能。A 描述的是 jit,C 与纯函数方向相反,D 是 checkpoint 的功能,均与 vmap 无关。
Q9. 关于纯函数模块化设计,下列说法错误的是哪一项?
- A.小纯函数可通过组合构建出大型模型
- B.高阶函数可以把变换当作参数传递
- C.纯函数模块易于独立测试和复用
- D.模块化设计强制要求使用类与继承← 正确答案
答案:D。函数式模块化靠的是函数复合和高阶函数,并不需要类与继承——D 是错误说法。A、B、C 都是纯函数模块化的正确描述:小函数可组合、变换可作为参数、无副作用让测试与复用变得简单。
Q10. 下列哪些行为违反了纯函数原则?
- A.在函数内打印日志并写入全局文件← 正确答案
- B.在函数内修改传入的列表参数元素← 正确答案
- C.函数返回输入经过计算后的全新结果
- D.函数依赖系统当前时间决定输出值← 正确答案
答案:ABD。写文件是副作用,修改传入参数是原地篡改,依赖系统时间会让相同输入产生不同输出,三者都违反纯函数原则。C 返回新计算结果正是纯函数的典型行为,不违反任何规则。