Последовательности выборки случайных чисел в Haskell - PullRequest
18 голосов
/ 21 января 2010

Мне нужны небольшие списки гауссовых случайных чисел для симуляции, поэтому я попробовал следующее:

import System.Random

seed = 10101
gen = mkStdGen seed

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

Это просто алгоритм Бокса-Мюллера - при заданных 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, чтобы быть первыми 10 значениями map (boxMuller 0 1) $ pairs $ randoms g, и y, чтобы быть следующими 50 значениями в этом списке, и так далее.

Конечно, это невозможно, потому что функция всегда должна возвращать одинаковые значения при одинаковых входных данных. Как мне избежать этой ловушки?

Ответы [ 3 ]

28 голосов
/ 24 января 2010

Я думаю, что выполнение ваших вычислений, которые требуют случайных чисел внутри монады, которая абстрагирует генератор, является самой чистой вещью. Вот как будет выглядеть этот код:

Мы собираемся поместить экземпляр StdGen в монаду состояний, а затем добавить сахар к методу get и set монады, чтобы получить случайные числа.

Сначала загрузите модули:

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

(Обычно я, вероятно, не буду указывать импорт, но это позволяет легко понять, откуда поступает каждая функция.)

Затем мы определим монаду «вычисления, требующие случайных чисел»; в этом случае псевдоним для State StdGen называется R. (Потому что «Случайный» и «Рэнд» уже означают что-то другое.)

type R a = State StdGen a

Способ работы R таков: вы определяете вычисление, для которого требуется поток случайных чисел (монадическое «действие»), а затем «запускаете его» с помощью runRandom:

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

Это принимает действие и семя и возвращает результаты действия. Как обычно evalState, runReader и т. Д.

Теперь нам просто нужен сахар вокруг государственной монады. Мы используем 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 () дает монадическому действию бесконечный поток, в котором ничто не может вызвать функцию (и это делает результат нормалей бесконечно длинным). Сначала я написал [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

Применяются обычные методы программирования монадических действий; сохраняйте как можно меньше своего приложения в R, и все будет не так уж и грязно.

Да, и еще: лень может «просочиться» за границу монады чисто. Это означает, что вы можете написать:

take 10 $ runRandom normals 42

и это будет работать.

6 голосов
/ 21 января 2010

Список, который вы получаете от randoms, бесконечен, и когда вы используете конечный префикс, вам не нужно выбрасывать бесконечный хвост. Вы можете передать случайные числа в качестве дополнительного параметра и вернуть неиспользованные случайные числа в качестве дополнительного результата, или вы можете оставить бесконечную последовательность случайных чисел в монаде состояния.

Аналогичная проблема возникает для компиляторов и других кодов, которые хотят предоставить уникальные символы. Это просто настоящая боль в Haskell, потому что вы пронизываете состояние (генератора случайных чисел или генератора символов) по всему коду.

Я сделал рандомизированные алгоритмы как с явными параметрами, так и с монадой, и ни один из них не является действительно удовлетворительным. Если вы любите монады, у меня, вероятно, есть небольшая рекомендация использовать монаду состояния, содержащую бесконечный список случайных чисел, которые еще не использовались.

1 голос
/ 21 января 2010

Вы также можете обойти проблему, используя newStdGen, и тогда вы будете получать разные (практически) новые семена каждый раз.

...