Haskell中随机数的采样序列

时间:2010-01-21 15:44:29

标签: haskell functional-programming random referential-transparency

我需要用于模拟的小型高斯随机数列表,所以我尝试了以下内容:

import System.Random

seed = 10101
gen = mkStdGen seed

boxMuller mu sigma (r1,r2) =  mu + sigma * sqrt (-2 * log r1) * cos (2 * pi * r2) 

这只是Box-Muller算法 - 给定r1,r2在[0,1]区间内的均匀随机数,它返回一个高斯随机数。

normals 0 g = [] 
normals n g = take n $ map (boxMuller 0 1) $ pairs $ randoms g
    where pairs (x:y:zs) = (x,y):(pairs zs)

每次我需要我的随机数列表时,我都使用了这个normals函数。

问题必须明显:它始终生成相同的序列,因为我总是使用相同的种子!我没有得到新的序列,我只是一直得到序列的前n个值。

我明确假装的是,当我输入时:

x = normal 10 
y = normal 50

我要将x作为map (boxMuller 0 1) $ pairs $ randoms g的前10个值,将y作为此列表中的下50个值,依此类推。

当然这是不可能的,因为在给定相同输入的情况下,函数必须始终返回相同的值。我如何逃脱这个陷阱?

3 个答案:

答案 0 :(得分:27)

我认为你的计算需要在monad中抽象生成器的随机数是最干净的。这是代码的样子:

我们将把StdGen实例放在一个状态monad中,然后在状态monad的get和set方法上提供一些糖来给我们随机数字。

首先,加载模块:

import Control.Monad.State (State, evalState, get, put)
import System.Random (StdGen, mkStdGen, random)
import Control.Applicative ((<$>))

(通常我可能不会指定导入,但这样可以很容易地理解每个函数的来源。)

然后我们将定义我们的“需要随机数的计算”monad;在这种情况下,State StdGen的别名称为R。 (因为“随机”和“兰德”已经意味着别的东西。)

type R a = State StdGen a

R的工作方式是:定义一个需要随机数流(monadic“action”)的计算,然后用runRandom“运行”:

runRandom :: R a -> Int -> a
runRandom action seed = evalState action $ mkStdGen seed

这将采取操作和种子,并返回操作的结果。就像通常的evalStaterunReader等一样。

现在我们只需要州立大学附近的糖。我们使用get来获取StdGen,我们使用put来安装新状态。这意味着,为了得到一个随机数,我们会写:

rand :: R Double
rand = do
  gen <- get
  let (r, gen') = random gen
  put gen'
  return r

我们得到随机数生成器的当前状态,用它来获取一个新的随机数和一个新的生成器,保存随机数,安装新的生成器状态,并返回随机数。

这是一个可以使用runRandom运行的“动作”,所以让我们试试吧:

ghci> runRandom rand 42
0.11040701265689151                           
it :: Double     

这是一个纯函数,因此如果使用相同的参数再次运行它,您将得到相同的结果。杂质留在你传递给runRandom的“动作”中。

无论如何,你的函数想要成对的随机数,所以让我们写一个动作来产生随机数的

randPair :: R (Double, Double)
randPair = do
  x <- rand
  y <- rand
  return (x,y)

使用runRandom运行此选项,您会看到对中有两个不同的数字,正如您所期望的那样。但请注意,您不必提供带有参数的“rand”;这是因为函数是纯粹的,但“rand”是一个动作,它不一定是纯粹的。

现在我们可以实现您的normals功能。 boxMuller就像你在上面定义的那样,我只是添加了一个类型签名,这样我就可以理解发生了什么,而不必阅读整个函数:

boxMuller :: Double -> Double -> (Double, Double) -> Double
boxMuller mu sigma (r1,r2) =  mu + sigma * sqrt (-2 * log r1) * cos (2 * pi * r2)

在实现所有辅助函数/动作后,我们最终可以编写normals,一个0参数的动作,返回一个(懒惰生成的)无限正常分布的双精度列表:

normals :: R [Double]
normals = mapM (\_ -> boxMuller 0 1 <$> randPair) $ repeat ()

如果你想要,你也可以不那么简洁地写这个:

oneNormal :: R Double
oneNormal = do
    pair <- randPair
    return $ boxMuller 0 1 pair

normals :: R [Double]
normals = mapM (\_ -> oneNormal) $ repeat ()

repeat ()给monadic动作一个无限的无任何动作来调用函数(并且是法线结果无限长的原因)。我最初写过[1..],但我重写了它以从程序文本中删除无意义的1。我们不是在整数上运行,我们只想要一个无限的列表。

无论如何,就是这样。要在真实程序中使用它,您只需在R动作中执行需要随机法线的工作:

someNormals :: Int -> R [Double]
someNormals x = liftM (take x) normals

myAlgorithm :: R [Bool]
myAlgorithm = do
   xs <- someNormals 10
   ys <- someNormals 10
   let xys = zip xs ys
   return $ uncurry (<) <$> xys

runRandom myAlgorithm 42

编程monadic动作的常用技巧适用;

哦,另一方面:懒惰可以干净地“泄漏”在monad边界之外。这意味着你可以写:

R

它会起作用。

答案 1 :(得分:6)

randoms获得的列表是无限的,当你使用有限前缀时,你不需要扔掉无限的尾巴。您可以将随机数作为附加参数传递,并将未使用的随机数作为附加结果返回,或者您可以将无限序列的随机数存储在状态monad中。

编译器和其他需要提供唯一符号的代码也会出现类似的问题。这只是Haskell真正的痛苦,因为你在整个代码中处理状态(随机数生成器或符号生成器)。

我已经完成了具有显式参数和monad的随机算法,并且没有一个真正令人满意。如果你修改monad我可能会建议使用一个包含尚未使用的无限随机数列表的状态monad。

答案 2 :(得分:1)

您也可以使用newStdGen来回避问题,然后每次都会得到一个不同的种子(虚拟)。