set.seed(42) 只固定了四分之一:随机数流的三个旋钮,和 2019 年一次静默换轨
【锚点】这一篇只钉一个判断:【可复现性的最小记录单位不是 seed,而是「RNG 种类 × normal 种类 × sample 种类 × seed」这个四元组;只写 set.seed(42),等于只写了一个坐标轴上的一个数。】
【显式】R 3.6.0(2019-04-26)把 sample() 的默认 sample.kind 从 “Rounding” 改成了 “Rejection”。同一份脚本 set.seed(1); sample(10, 3),在 3.5.x 和 3.6.x 上给出的三个索引不同——不是「偶尔不同」,是必然不同,因为底层抽样流换了。RNGkind() 返回三个分量:kind(默认 Mersenne-Twister)、normal.kind(默认 Inversion)、sample.kind。set.seed() 只钉住第四个。
【隐式】所以「我设了 seed 啊」在跨年份、跨机器的复现里是一个不完整的承诺:它固定了起点,没有固定算法。R 官方为此留了一台时间机器:RNGversion(“3.5.0”) 可以把三个旋钮一起拨回去。
【细微】更隐蔽的一层:normal.kind 只影响 rnorm() 类,kind 影响 runif() 类。一份脚本如果同时用 rnorm 和 sample 做置换检验,两个旋钮各自分叉,而作者在 methods 里通常只写一句「random seed = 42」。跨语言同理:numpy 的 np.random.seed(42) 走 legacy RandomState(MT19937),np.random.default_rng(42) 走 PCG64(NumPy ≥1.17),两个 42 是两条完全不同的流。
【极难发现】旧方法「不可复现」的底层原因其实是「有偏」。unif_rand() 只有 232 个等可能取值;n 个桶去分 232 个点,桶大小最多差 1。当 n > 2^31 时,一半桶拿到 2 个点、一半拿到 1 个点,抽中概率比是 2:1。Rejection 修的就是这个。也就是说:复现性 bug 和统计正确性 bug 在同一行代码上重合——你以为你在修环境,其实你在修偏置。
【流程节点链路】论文写「seed = 42」 → 读者在 3.6+ 上重跑 → sample.kind 默认已是 Rejection → 抽样索引序列改变 → 下游置换检验的 p 值漂移 → 读者判定「结论不可复现」 → 终点:错误被归因为作者造假或数据操纵,而真实原因是环境漂移。这条链上没有任何一步是「有人做错了」,但它足以毁掉一篇论文的名誉。
最小处方(可直接抄进脚本头部): RNGkind(kind = “Mersenne-Twister”, normal.kind = “Inversion”, sample.kind = “Rejection”) set.seed(42) 并在补充材料里贴出 R 版本 + RNGkind() 的三个返回值和 sessionInfo()。冻结环境(renv / conda-lock)比冻结 seed 有用一个数量级。
值得辩论的问题:如果「随机」的定义本身随软件版本变化,那么可复现性的对象到底是数据,还是计算过程?我的立场是——可复现性的最小单位不是「一个种子」,而是「一次被完整记录的状态转移」;seed 只是它的第一个字节。







0 評論