Excel VBA 的 Rnd() 真的有这么糟糕吗?
Is Excel VBA's Rnd() really this bad?
我需要一个用于 2D Monte Carlo 模拟的伪随机数生成器,它没有使用简单的 LCG 获得的特征超平面。我在 Excel 2013 年使用以下代码测试了随机数生成器 Rnd()(大约需要 5 秒到 运行):
Sub ZoomRNG()
Randomize
For i = 1 To 1000
Found = False
Do
x = Rnd() ' 2 random numbers between 0.0 and 1.0
y = Rnd()
If ((x > 0.5) And (x < 0.51)) Then
If ((y > 0.5) And (y < 0.51)) Then
' Write if both x & y in a narrow range
Cells(i, 1) = i
Cells(i, 2) = x
Cells(i, 3) = y
Found = True
End If
End If
Loop While (Not Found)
Next i
End Sub
这是来自运行上述代码
的 x 与 y 的简单图
它不仅看起来不是很随机,而且它比声名狼藉的 RANDU 算法在 2D 中有更明显的超平面。基本上,我是否错误地使用了该函数,或者 VBA 中的 Rnd() 函数实际上一点也不可用?
为了比较,这是我用 C++ 获得的 Mersenne Twister MT19937。
为了产生更好的随机生成器并使其性能更快,我修改了您的代码:
Const N = 1000 'Put this on top of your code module
Sub ZoomRNG()
Dim RandXY(1 To N, 1 To 3) As Single, i As Single, x As Single, y As Single
For i = 1 To N
Randomize 'Put this in the loop to generate a better random numbers
Do
x = Rnd
y = Rnd
If x > 0.5 And x < 0.51 Then
If y > 0.5 And y < 0.51 Then
RandXY(i, 1) = i
RandXY(i, 2) = x
RandXY(i, 3) = y
Exit Do
End If
End If
Loop
Next
Cells(1, 9).Resize(N, 3) = RandXY
End Sub
我在绘制结果后得到这个
结果看起来比您的代码的输出更好。将上面的代码稍微修改成这样
Const N = 1000
Sub ZoomRNG()
Dim RandXY(1 To N, 1 To 3) As Single, i As Single, x As Single, y As Single
For i = 1 To N
Randomize
Do
x = Rnd
If x > 0.5 And x < 0.51 Then
y = Rnd
If y > 0.5 And y < 0.51 Then
RandXY(i, 1) = i
RandXY(i, 2) = x
RandXY(i, 3) = y
Exit Do
End If
End If
Loop
Next
Cells(1, 9).Resize(N, 3) = RandXY
End Sub
比上一个更好
当然 C++ 中的 Mersenne Twister MT19937 仍然更好,但最后的结果对于进行 Monte-Carlo 模拟来说是相当不错的。 FWIW,您可能有兴趣阅读这篇论文:On the accuracy of statistical procedures in Microsoft Excel 2010。
这似乎平均需要 1000 * 100 * 100 次迭代才能完成,而且 VBA 通常比原生 Excel 公式慢一点。考虑这个例子
Sub ZoomRNG()
t = Timer
[a1:a1000] = "=ROW()"
[b1:c1000] = "=RAND()/100+0.5"
[a1:c1000] = [A1:C1000].Value
Debug.Print CDbl(Timer - t) ' 0.0546875 seconds
End Sub
更新
一点也不差!即使没有 Randomize
,这也会起作用
Sub ZoomRNGs() ' VBA.Rnd returns Single
t = Timer
For i = 1 To 1000
Cells(i, 1) = i
Cells(i, 2) = Rnd / 100 + 0.5
Cells(i, 3) = Rnd / 100 + 0.5
Next i
Debug.Print Timer - t ' 0.25 seconds
End Sub
Sub ZoomRNGd() ' the Excel Function RAND() returns Double
t = Timer
For i = 1 To 1000
Cells(i, 1) = i
Cells(i, 2) = [RAND()] / 100 + 0.5
Cells(i, 3) = [RAND()] / 100 + 0.5
Next i
Debug.Print Timer - t ' 0.625 seconds
End Sub
和Single
的精度大约是Double
的一半:
s = Rnd: d = [RAND()]
Debug.Print s; d; Len(Str(s)); Len(Str(d)) ' " 0.2895625 0.580839555868045 9 17 "
更新 2
我找到了与 VBA Rnd.
一样快的 C 替代方案
C:\Windows\System32\msvcrt.dll
是 Microsoft C 运行库:
Declare Function rand Lib "msvcrt" () As Long ' this in a VBA module
然后您可以在您的代码中像这样使用它x = rand / 32767
:
Sub ZoomRNG()
t = Timer
Dim i%, x#, y#, Found As Boolean
For i = 1 To 1000
Found = False
Do
x = rand / 32767 ' RAND_MAX = 32,767
y = rand / 32767
If ((x > 0.5) And (x < 0.51)) Then
If ((y > 0.5) And (y < 0.51)) Then
' Write if both x & y in a narrow range
Cells(i, 1) = i
Cells(i, 2) = x
Cells(i, 3) = y
Found = True
End If
End If
Loop While (Not Found)
Next i
Debug.Print Timer - t ' 2.875 seconds
End Sub
为了在速度和优点之间取得平衡,我正在考虑像
那样将它们结合起来
for...
z = [rand()] ' good but slow.
for .. ' just a few
t = z + rnd()
t = t - int(t)
...
记住好的熵 + 坏的熵 = 更好的熵。
也就是说,每个 [rand()] 只有 0.05 毫秒。
看完这个问题我很好奇,找到了这篇论文
“Assessing Excel VBA Suitability for Monte Carlo Simulation" by Alexei Botchkarev that is available here。不推荐 RAND 和 RND 函数,但正如论文中指出的那样,Jerry Wang 在 VBA 中已经实现了 Mersenne Twister。
快速搜索让我找到了这个评论很好的版本,该版本已于 2015 年 2 月 28 日更新:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/BASIC/MTwister.xlsb
来源:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/BASIC/basic.html
所有LCG都会生成超平面。 LCG 的质量随着这些超平面之间距离的减小而增加。因此,拥有比 RANDU 更多的超平面是一件好事。
MT 情节看起来好多了,因为它不是 LCG。事实上,任何非 LCG pRNG 都可能有一个随机的情节,但仍然很糟糕。
为避免二维相关性问题,您可以对 x 和 y 使用相同的 LCG,但对 x 和 y 使用不同的种子。当然,这不适用于 RND,因为您不能有两个独立的流。您将需要一个 LCG pRNG,它通过引用将种子作为参数。
我需要一个用于 2D Monte Carlo 模拟的伪随机数生成器,它没有使用简单的 LCG 获得的特征超平面。我在 Excel 2013 年使用以下代码测试了随机数生成器 Rnd()(大约需要 5 秒到 运行):
Sub ZoomRNG()
Randomize
For i = 1 To 1000
Found = False
Do
x = Rnd() ' 2 random numbers between 0.0 and 1.0
y = Rnd()
If ((x > 0.5) And (x < 0.51)) Then
If ((y > 0.5) And (y < 0.51)) Then
' Write if both x & y in a narrow range
Cells(i, 1) = i
Cells(i, 2) = x
Cells(i, 3) = y
Found = True
End If
End If
Loop While (Not Found)
Next i
End Sub
这是来自运行上述代码
的 x 与 y 的简单图它不仅看起来不是很随机,而且它比声名狼藉的 RANDU 算法在 2D 中有更明显的超平面。基本上,我是否错误地使用了该函数,或者 VBA 中的 Rnd() 函数实际上一点也不可用?
为了比较,这是我用 C++ 获得的 Mersenne Twister MT19937。
为了产生更好的随机生成器并使其性能更快,我修改了您的代码:
Const N = 1000 'Put this on top of your code module
Sub ZoomRNG()
Dim RandXY(1 To N, 1 To 3) As Single, i As Single, x As Single, y As Single
For i = 1 To N
Randomize 'Put this in the loop to generate a better random numbers
Do
x = Rnd
y = Rnd
If x > 0.5 And x < 0.51 Then
If y > 0.5 And y < 0.51 Then
RandXY(i, 1) = i
RandXY(i, 2) = x
RandXY(i, 3) = y
Exit Do
End If
End If
Loop
Next
Cells(1, 9).Resize(N, 3) = RandXY
End Sub
我在绘制结果后得到这个
结果看起来比您的代码的输出更好。将上面的代码稍微修改成这样
Const N = 1000
Sub ZoomRNG()
Dim RandXY(1 To N, 1 To 3) As Single, i As Single, x As Single, y As Single
For i = 1 To N
Randomize
Do
x = Rnd
If x > 0.5 And x < 0.51 Then
y = Rnd
If y > 0.5 And y < 0.51 Then
RandXY(i, 1) = i
RandXY(i, 2) = x
RandXY(i, 3) = y
Exit Do
End If
End If
Loop
Next
Cells(1, 9).Resize(N, 3) = RandXY
End Sub
比上一个更好
当然 C++ 中的 Mersenne Twister MT19937 仍然更好,但最后的结果对于进行 Monte-Carlo 模拟来说是相当不错的。 FWIW,您可能有兴趣阅读这篇论文:On the accuracy of statistical procedures in Microsoft Excel 2010。
这似乎平均需要 1000 * 100 * 100 次迭代才能完成,而且 VBA 通常比原生 Excel 公式慢一点。考虑这个例子
Sub ZoomRNG()
t = Timer
[a1:a1000] = "=ROW()"
[b1:c1000] = "=RAND()/100+0.5"
[a1:c1000] = [A1:C1000].Value
Debug.Print CDbl(Timer - t) ' 0.0546875 seconds
End Sub
更新
一点也不差!即使没有 Randomize
Sub ZoomRNGs() ' VBA.Rnd returns Single
t = Timer
For i = 1 To 1000
Cells(i, 1) = i
Cells(i, 2) = Rnd / 100 + 0.5
Cells(i, 3) = Rnd / 100 + 0.5
Next i
Debug.Print Timer - t ' 0.25 seconds
End Sub
Sub ZoomRNGd() ' the Excel Function RAND() returns Double
t = Timer
For i = 1 To 1000
Cells(i, 1) = i
Cells(i, 2) = [RAND()] / 100 + 0.5
Cells(i, 3) = [RAND()] / 100 + 0.5
Next i
Debug.Print Timer - t ' 0.625 seconds
End Sub
和Single
的精度大约是Double
的一半:
s = Rnd: d = [RAND()]
Debug.Print s; d; Len(Str(s)); Len(Str(d)) ' " 0.2895625 0.580839555868045 9 17 "
更新 2
我找到了与 VBA Rnd.
一样快的 C 替代方案
C:\Windows\System32\msvcrt.dll
是 Microsoft C 运行库:
Declare Function rand Lib "msvcrt" () As Long ' this in a VBA module
然后您可以在您的代码中像这样使用它x = rand / 32767
:
Sub ZoomRNG()
t = Timer
Dim i%, x#, y#, Found As Boolean
For i = 1 To 1000
Found = False
Do
x = rand / 32767 ' RAND_MAX = 32,767
y = rand / 32767
If ((x > 0.5) And (x < 0.51)) Then
If ((y > 0.5) And (y < 0.51)) Then
' Write if both x & y in a narrow range
Cells(i, 1) = i
Cells(i, 2) = x
Cells(i, 3) = y
Found = True
End If
End If
Loop While (Not Found)
Next i
Debug.Print Timer - t ' 2.875 seconds
End Sub
为了在速度和优点之间取得平衡,我正在考虑像
那样将它们结合起来for...
z = [rand()] ' good but slow.
for .. ' just a few
t = z + rnd()
t = t - int(t)
...
记住好的熵 + 坏的熵 = 更好的熵。
也就是说,每个 [rand()] 只有 0.05 毫秒。
看完这个问题我很好奇,找到了这篇论文 “Assessing Excel VBA Suitability for Monte Carlo Simulation" by Alexei Botchkarev that is available here。不推荐 RAND 和 RND 函数,但正如论文中指出的那样,Jerry Wang 在 VBA 中已经实现了 Mersenne Twister。
快速搜索让我找到了这个评论很好的版本,该版本已于 2015 年 2 月 28 日更新:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/BASIC/MTwister.xlsb
来源:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/BASIC/basic.html
所有LCG都会生成超平面。 LCG 的质量随着这些超平面之间距离的减小而增加。因此,拥有比 RANDU 更多的超平面是一件好事。
MT 情节看起来好多了,因为它不是 LCG。事实上,任何非 LCG pRNG 都可能有一个随机的情节,但仍然很糟糕。
为避免二维相关性问题,您可以对 x 和 y 使用相同的 LCG,但对 x 和 y 使用不同的种子。当然,这不适用于 RND,因为您不能有两个独立的流。您将需要一个 LCG pRNG,它通过引用将种子作为参数。