在Javascript中种子随机数生成器

577

在JavaScript中,是否可能对随机数生成器 (Math.random) 进行种子初始化?


4
不清楚你是想使用相同的种子来重复获得相同的测试结果,还是想为每个用户提供“独特的东西”以获得更好的随机性。 - simbo1905
5
抱歉,很遗憾它是不可能的。jsrand 是我写的一个小型库,用于生成可种子化的伪随机数。你也可以在谷歌上搜索到其他更复杂的库。 - Domenico De Felice
14
补充问题:提供没有种子生成方法的伪随机数生成器可能是个好主意吗?这样做有什么合理的原因吗? - Alan
1
请参见 https://dev59.com/KHRC5IYBdhLWcg3wD8xV - Palimondo
2
这是此页面上一些生成器的可视化 https://observablehq.com/@tidwall/hello-randomness - tidwall
7
我认为可能没有种子是因为底层算法取决于浏览器 - 如果 Math.random() 有一个种子,那么这些种子不能保证在不同的浏览器中产生相同的结果。 - codeulike
21个回答

453
不,无法对Math.random()进行种子化。ECMAScript规范在这个问题上故意模糊不清,既没有提供种子化的方法,也没有要求浏览器使用相同的算法。因此,必须从外部提供这样的函数,幸运的是这并不太困难。
我已经在纯JavaScript中实现了许多好的、简短而快速的伪随机数生成器(PRNG)函数。所有这些函数都可以进行种子化,并提供高质量的数字。它们并不适用于安全目的——如果您需要一个可种子化的CSPRNG,请查看ISAAC。
首先,要注意正确初始化您的伪随机数生成器(PRNGs)。为了简单起见,下面的生成器没有内置的种子生成过程,但接受一个或多个32位数字作为PRNG的初始“种子状态”。类似或稀疏的种子(例如简单的1和2)具有低熵,并且可能导致相关性或其他随机性质量问题,有时会导致输出具有相似的特性(例如随机生成的关卡相似)。为了避免这种情况,最好的做法是使用分布均匀、高熵的种子来初始化PRNGs,或者在前15个左右的数字之后再进行进一步操作。
有很多方法可以做到这一点,但以下是两种方法。首先,哈希函数非常适合从短字符串生成种子。一个好的哈希函数即使在两个字符串相似的情况下也会生成非常不同的结果,因此您不必对字符串进行太多思考。以下是一个示例哈希函数:
function cyrb128(str) {
    let h1 = 1779033703, h2 = 3144134277,
        h3 = 1013904242, h4 = 2773480762;
    for (let i = 0, k; i < str.length; i++) {
        k = str.charCodeAt(i);
        h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
        h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
        h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
        h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
    }
    h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
    h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
    h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
    h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
    h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
    return [h1>>>0, h2>>>0, h3>>>0, h4>>>0];
}

调用 cyrb128 将从字符串生成一个 128 位哈希值,可用于种子 PRNG(伪随机数生成器)。以下是使用方法:

// Create cyrb128 state:
var seed = cyrb128("apples");
// Four 32-bit component hashes provide the seed for sfc32.
var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);

// Only one 32-bit component hash is needed for mulberry32.
var rand = mulberry32(seed[0]);

// Obtain sequential random numbers like so:
rand();
rand();

注意:如果您需要稍微更强大的128位哈希,请考虑使用MurmurHash3_x86_128,它更彻底,但适用于大型数组。

或者,简单地选择一些虚拟数据来填充种子,并在事先多次(12-20次迭代)推进发生器以充分混合初始状态。这样做的好处是更简单,并且通常在随机数生成器的参考实现中使用,但会限制初始状态的数量:

var seed = 1337 ^ 0xDEADBEEF; // 32-bit seed with optional XOR value
// Pad seed with Phi, Pi and E.
// https://en.wikipedia.org/wiki/Nothing-up-my-sleeve_number
var rand = sfc32(0x9E3779B9, 0x243F6A88, 0xB7E15162, seed);
for (var i = 0; i < 15; i++) rand();

注意:这些伪随机数生成函数的输出产生一个正的32位数字(从0到232-1),然后转换为一个介于0-1之间(包括0,不包括1)的浮点数,相当于Math.random()。如果你想要一个特定范围的随机数,请阅读这篇MDN上的文章。如果你只想要原始位,只需删除最后的除法运算。

JavaScript的数字只能表示最大为53位的整数。而在使用位操作时,这个范围被减小到32位。其他语言中的现代伪随机数生成器通常使用64位操作,在将其移植到JS时需要适配层,这可能会极大地降低性能。这里的算法只使用32位操作,因为它与JS直接兼容。

现在,进入生成器部分。(我在这里维护了完整的列表,包含参考和许可信息此处


sfc32(简单快速计数器)

sfc32PractRand 随机数测试套件的一部分(当然它通过了测试)。sfc32 有128位状态,在 JS 中非常快速。

function sfc32(a, b, c, d) {
    return function() {
      a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0; 
      var t = (a + b) | 0;
      a = b ^ b >>> 9;
      b = c + (c << 3) | 0;
      c = (c << 21 | c >>> 11);
      d = d + 1 | 0;
      t = t + d | 0;
      c = c + t | 0;
      return (t >>> 0) / 4294967296;
    }
}

你可能会想知道| 0>>>= 0的作用是什么。这些实际上是32位整数转换,用于性能优化。在JS中,Number基本上是浮点数,但在位运算期间,它们会切换到32位整数模式。这种模式由JS解释器更快地处理,但任何乘法或加法都会导致它切换回浮点数,从而影响性能。

Mulberry32

Mulberry32是一个简单的生成器,具有32位状态,但速度非常快,并且具有良好的随机性(作者称其通过了gjrand测试套件的所有测试,并具有完整的232周期,但我没有验证)。

function mulberry32(a) {
    return function() {
      var t = a += 0x6D2B79F5;
      t = Math.imul(t ^ t >>> 15, t | 1);
      t ^= t + Math.imul(t ^ t >>> 7, t | 61);
      return ((t ^ t >>> 14) >>> 0) / 4294967296;
    }
}

我会推荐这个,如果你只需要一个简单但是体面的伪随机数生成器,并且不需要数十亿个随机数(参见生日问题)。

xoshiro128**

截至2018年5月,xoshiro128**Xorshift家族的新成员,由Vigna和Blackman开发(Vigna教授还负责Xorshift128+算法,该算法驱动了大多数Math.random实现)。它是提供128位状态的最快生成器。
function xoshiro128ss(a, b, c, d) {
    return function() {
        var t = b << 9, r = b * 5; r = (r << 7 | r >>> 25) * 9;
        c ^= a; d ^= b;
        b ^= c; a ^= d; c ^= t;
        d = d << 11 | d >>> 21;
        return (r >>> 0) / 4294967296;
    }
}

作者声称它在随机性测试中表现良好(尽管有一些限制条件)。其他研究人员指出它未能通过TestU01的某些测试(特别是LinearComp和BinaryRank)。在实践中,当使用浮点数时(例如在这些实现中),它不应引起问题,但如果依赖于原始的最低位,则可能会引起问题。

JSF(Jenkins的小型快速)

这是Bob Jenkins(2007年)的JSF或"smallprng",他还创造了ISAACSpookyHash。它通过了PractRand测试,速度应该相当快,尽管不及sfc32快。

function jsf32(a, b, c, d) {
    return function() {
        a |= 0; b |= 0; c |= 0; d |= 0;
        var t = a - (b << 27 | b >>> 5) | 0;
        a = b ^ (c << 17 | c >>> 15);
        b = c + d | 0;
        c = d + t | 0;
        d = a + t | 0;
        return (d >>> 0) / 4294967296;
    }
}

2
@Lachmanski 是的,但它们受到32位(和Park-Miller 31位)的限制。使用Math.imul允许它像在32位整数上使用乘法时一样溢出。您建议的是使用JS整数空间的完整范围来使用LCG,这绝对是一个有趣的探索领域。 :) - bryc
2
这太棒了!我能否将您的sfc32复制到LGPL程序中? - user334639
5
当然,随意使用这段代码以实现任何目的 :) - bryc
1
希望能够在一个页面上提供这些示例,以便可以引用而不需要 SO 的 URL(用于归属)。 - user12416582
5
@blobber2,我不确定你的意思,但这个原始代码是从这里(和其他人)得来的:https://github.com/bryc/code/blob/master/jshash/PRNGs.md。基本上是一个存储库中的要点 :-) - bryc
显示剩余27条评论

244

214

注意:尽管这个算法的简洁和明显的优势,但在随机性方面并不是高质量的。请查看此答案中列出的更好的选择。

(原始想法改编自对另一个答案的评论。)

var seed = 1;
function random() {
    var x = Math.sin(seed++) * 10000;
    return x - Math.floor(x);
}

您可以将seed设置为任何数字,只需避免零(或任何Math.PI的倍数)。

在我看来,这种解决方案的优雅之处在于没有任何“魔法”数字(除了10000,它代表必须扔掉的最小位数,以避免奇怪的模式 - 参见值101001000的结果)。简洁也很好。

它的速度比Math.random()慢一些(2或3倍),但我认为它与使用JavaScript编写的任何其他解决方案的速度差不多。


25
有没有办法证明这个随机数生成器生成的数字是均匀分布的?实验上看起来是这样的:http://jsfiddle.net/bhrLT/。 - Nathan Breit
11
“每秒 6,000,000 次操作”(http://jsperf.com/math-random-vs-seeded-rand)非常快,我不打算一次生成超过约 3,000,000 次。开玩笑,这太棒了。 - A.M.K
72
这个采样器并不是完全均衡的,它对0和1有相当大的偏向性(请参考http://jsfiddle.net/bhrLT/17/,可能需要一些时间来计算)。连续的值是相关的 - 每355个值,甚至每710个值,都是相关的。请使用更加慎重考虑过的方法! - spencer nelson
59
问题不是要创建一个密码学安全的随机数生成器,而是要在JavaScript中使用,适用于快速演示等场景。我需要一个快速简单的方法来生成一百万个随机数并呈现出好看的分布图形。请注意不要改变原意。 - Jason Goemaat
26
小心使用Math.sin()函数,客户端和服务器端可能得到不同的结果。我使用Meteor框架(在客户端和服务器端都使用JavaScript语言)。 - Obiwahn
显示剩余11条评论

52
不,但是这里有一个简单的伪随机生成器,是我从Wikipedia(已被删除)改编的Multiply-with-carry实现:
var m_w = 123456789;
var m_z = 987654321;
var mask = 0xffffffff;

// Takes any integer
function seed(i) {
    m_w = (123456789 + i) & mask;
    m_z = (987654321 - i) & mask;
}

// Returns number between 0 (inclusive) and 1.0 (exclusive),
// just like Math.random().
function random()
{
    m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
    m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
    var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
    result /= 4294967296;
    return result;
}

3
这个函数的随机性是否有人测试过? - Justin
3
这是一个周期相当长的乘积累加随机生成器(MWC),源自于wikipedia随机数生成器。请您帮我查看一下翻译是否合适,谢谢! - Michael_Scharf
当我使用这段代码时,我没有得到良好分布的结果。无论种子如何,输出序列都非常相似。这使得它对我的游戏没有帮助。 - Martin Omander
@Justin 这个回复晚了几年,但是在数百万次运行中,多个种子几乎是完美分布的。 分布的方差约为 3.469446951953614e-18,非常均匀。这个 是我用来测试分布的脚本,只需要一个具有 next() 方法的对象。 - Qix - MONICA WAS MISTREATED
更改随机数种子不会产生完全不同的随机数序列,存在某种潜在的模式。这是使用种子1和55进行比特图的异或[XOR]结果(https://i.imgur.com/ly3Y1Pk.png)。白色像素<0.5,黑色像素>0.5。将具有不同种子的两个输出进行XOR操作会显示对>0.5的偏向。 - bryc
显示剩余4条评论

33

安蒂·西卡里的算法很好而且很短。我最初做了一个变体,它在调用Math.seed(s)时替换了JavaScript的Math.random,但后来Jason评论说返回函数会更好:


Math.seed = function(s) {
    return function() {
        s = Math.sin(s) * 10000; return s - Math.floor(s);
    };
};

// usage:
var random1 = Math.seed(42);
var random2 = Math.seed(random1());
Math.random = Math.seed(random2());

这为您提供了JavaScript所没有的另一种功能:多个独立的随机生成器。如果您想同时运行多个可重复模拟,则这尤其重要。


4
如果你返回函数而不是设置 Math.random,这将允许你拥有多个独立的生成器,对吗? - Jason Goemaat
2
如果这对你很重要,请确保查看上面关于随机分布的评论:https://dev59.com/QHRB5IYBdhLWcg3wxZ1K#-6ifEYcBWogLw_1brucm - jocull
这个程序生成的随机数如何重复?它每次都会给出新的数字。 - SMUsamaShah
6
请勿使用此方法。请花些时间使用一个名为 random 的局部变量代替覆盖原生的JavaScript函数。覆盖 Math.random 可能会导致JIST编译器取消所有代码的优化。 - Jack G
当 s = 0 时,警告不起作用。在构造时应该抛出错误。 - Rok Burgar
显示剩余2条评论

12
请查看皮耶尔·莱古耶(Pierre L'Ecuyer)的研究,该研究可以追溯到20世纪80年代末和90年代初。还有其他人也在做类似的研究。如果你不是专家,自己创建(伪)随机数生成器非常危险,因为很可能结果不具备统计学上的随机性或者周期很小。皮耶尔 (和其他人) 已经开发出了一些好用的(伪)随机数生成器,并且易于实现。我使用他的LFSR生成器之一。
链接:https://www.iro.umontreal.ca/~lecuyer/myftp/papers/handstat.pdf

2
很好的回答,但与JavaScript无关 :) - Nikolay Fominyh
5
实现L'Ecuyer教授工作的代码已经公开发布,适用于Java,并且大多数程序员可以轻松将其转换为Javascript。 - user2383235

7

结合之前的答案,这就是你要寻找的可种子化随机函数:

Math.seed = function(s) {
    var mask = 0xffffffff;
    var m_w  = (123456789 + s) & mask;
    var m_z  = (987654321 - s) & mask;

    return function() {
      m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
      m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;

      var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
      result /= 4294967296;
      return result;
    }
}

var myRandomFunction = Math.seed(1234);
var randomNumber = myRandomFunction();

4
使用不同的种子,序列开头产生非常相似的结果。例如,Math.seed(0)() 返回 0.2322845458984375,而 Math.seed(1)() 返回 0.23228873685002327。根据种子改变 m_wm_z 都能产生较好的效果。使用 var m_w = 987654321 + s; var m_z = 123456789 - s; 能够在不同的种子下生成具有良好分布的首个值。 - undefined
2
@undefined,您所描述的问题已在最后一次编辑中得到修复,这是MWC实现中的一个错误。 - bryc
现在运行良好,截至2020年1月。种子为0,得到0.7322976540308446。种子为1,得0.16818441334180534,种子为2:0.6040864314418286,种子为3:0.03998844954185188。谢谢你们两个! - ProfDFrancis
我对 >>> 0 感到好奇,想知道为什么它比更直接的 & mask 更好。 - President James K. Polk

7

不能为内置的 Math.random 函数设置种子,但是可以用非常少的代码在 Javascript 中实现高质量的 RNG(随机数生成器)。

Javascript 数字是 64 位浮点精度,可以表示小于 2^53 的所有正整数。这为我们的算术设定了硬性限制,但在这些限制内,您仍然可以选择参数来创建高质量 Lehmer / LCG 随机数生成器。

function RNG(seed) {
    var m = 2**35 - 31
    var a = 185852
    var s = seed % m
    return function () {
        return (s = s * a % m) / m
    }
}

Math.random = RNG(Date.now())

如果你想要更高质量的随机数,但速度会减慢约10倍,你可以使用BigInt进行算术运算并选择参数,使得m刚好能够适合于double。

function RNG(seed) {
    var m_as_number = 2**53 - 111
    var m = 2n**53n - 111n
    var a = 5667072534355537n
    var s = BigInt(seed) % m
    return function () {
        return Number(s = s * a % m) / m_as_number
    }
}

请参考Pierre l'Ecuyer的论文,以获取上述实现所使用的参数: https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf 不管你做什么操作,请避免使用所有其他答案中使用的Math.sin!

为什么 m=2 ** 35?为了使用整数值的完整范围,应该将 m=Math.floor(2 ** 53 / a) 吗? - undefined
m和a的值是一起选择的,以产生一个看起来随机的序列。如果你选择了不好的值,可能会遇到RANDU的情况。请参阅答案中引用的论文以及ACM文章《随机数生成器:好的生成器很难找到》(可以在scihub上找到)。 - undefined

6
编写自己的伪随机生成器非常简单。
Dave Scotese的建议很有用,但正如其他人所指出的那样,它并不完全均匀分布。
然而,这不是因为sin的整数参数。这只是因为sin的范围是圆的一维投影。如果您取圆的角度,它将是均匀的。
因此,使用arg(exp(i * x)) / (2 * PI)代替sin(x)。
如果你不喜欢线性顺序,可以用异或稍微混合一下。实际因素也没有那么重要。
要生成n个伪随机数,可以使用以下代码:
function psora(k, n) {
  var r = Math.PI * (k ^ n)
  return r - Math.floor(r)
}
n = 42; for(k = 0; k < n; k++) console.log(psora(k, n))

请注意,在需要真正的熵时,您不能使用伪随机序列。

我不是专家,但连续的种子遵循一个恒定模式。彩色像素>=0.5。我猜这只是一遍又一遍地迭代半径? - bryc
3
编写自己的伪随机生成器非常简单,但编写正确的伪随机生成器则不是那么容易。 - Jason S

3

Math.random不行,但是ran可以解决这个问题。它几乎包含了你能想到的所有分布,并支持种子随机数生成。例如:

ran.normal(0, 1)([42, 43, 44])

这将返回一个由平均值为0,标准差为1的正态分布生成的随机数数组,种子为[42, 43, 44]。

ran.core.seed(0)
myDist = new ran.Dist.Uniform(0, 1)
samples = myDist.sample(1000)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接