Монте-Карло расчет Пи в Скала

Предположим, я хотел бы рассчитать Пи с помощью симуляции Монте-Карло в качестве упражнения.

Я пишу функцию, которая выбирает точку в квадрате (0, 1), (1, 0) наугад и проверяет, находится ли точка внутри круга.

import scala.math._
import scala.util.Random

def circleTest() = {
  val (x, y) = (Random.nextDouble, Random.nextDouble)
  sqrt(x*x + y*y) <= 1
}

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

def monteCarlo(trials: Int, test: () => Boolean) =
  (1 to trials).map(_ => if (test()) 1 else 0).sum * 1.0 / trials

... и я могу рассчитать Пи

monteCarlo(100000, circleTest) * 4

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

Например, поскольку количество испытаний велико, стоит ли view или же iterator вместо Range(1, trials) а также reduce вместо map а также sum?

6 ответов

Решение

Потоковая версия, для другой альтернативы. Я думаю, что это совершенно ясно.

def monteCarlo(trials: Int, test: () => Boolean) =
    Stream
      .continually(if (test()) 1.0 else 0.0)
      .take(trials)
      .sum / trials

(sum не специализируется на потоках, но реализация (в TraversableOnce) просто вызывает foldLeft это специализировано и "позволяет GC собирать по пути". Таким образом,.sum не заставит поток быть оцененным и, следовательно, не сохранит все испытания в памяти сразу)

Стоит отметить, что Random.nextDouble побочный эффект - когда вы вызываете его, он изменяет состояние генератора случайных чисел. Это может не беспокоить вас, но, поскольку здесь уже есть пять ответов, я полагаю, что ничего не помешает добавить чисто функциональный ответ.

Сначала вам понадобится реализация монады генерации случайных чисел. К счастью, NICTA предлагает действительно хороший вариант, интегрированный со Scalaz. Вы можете использовать это так:

import com.nicta.rng._, scalaz._, Scalaz._

val pointInUnitSquare = Rng.choosedouble(0.0, 1.0) zip Rng.choosedouble(0.0, 1.0)

val insideCircle = pointInUnitSquare.map { case (x, y) => x * x + y * y <= 1 }

def mcPi(trials: Int): Rng[Double] =
  EphemeralStream.range(0, trials).foldLeftM(0) {
    case (acc, _) => insideCircle.map(_.fold(1, 0) + acc)
  }.map(_ / trials.toDouble * 4)

А потом:

scala> val choosePi = mcPi(10000000)
choosePi: com.nicta.rng.Rng[Double] = com.nicta.rng.Rng$$anon$3@16dd554f

Ничего еще не было вычислено - мы только что создали вычисления, которые будут генерировать наше значение случайным образом при выполнении. Давайте просто выполним это на месте в IO Монада ради удобства:

scala> choosePi.run.unsafePerformIO
res0: Double = 3.1415628

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

Я не вижу проблем со следующей рекурсивной версией:

def monteCarlo(trials: Int, test: () => Boolean) = {
  def bool2double(b: Boolean) = if (b) 1.0d else 0.0d
  @scala.annotation.tailrec
  def recurse(n: Int, sum: Double): Double = 
    if (n <= 0) sum / trials
    else recurse(n - 1, sum + bool2double(test()))
  recurse(trials, 0.0d)
}

И версия foldLeft тоже:

def monteCarloFold(trials: Int, test: () => Boolean) = 
  (1 to trials).foldLeft(0.0d)((s,i) => s + (if (test()) 1.0d else 0.0d)) / trials

Это более эффективно, чем память map версия в вопросе.

Использование хвостовой рекурсии может быть идеей:

def recMonteCarlo(trials: Int, currentSum: Double, test:() => Boolean):Double = trials match {
  case 0 => currentSum
  case x => 
    val nextSum = currentSum + (if (test()) 1.0 else 0.0)
    recMonteCarlo(trials-1, nextSum, test)

def monteCarlo(trials: Int, test:() => Boolean) = {
  val monteSum = recMonteCarlo(trials, 0, test)
  monteSum / trials
}

С помощью aggregate на параллельной коллекции, как это,

def monteCarlo(trials: Int, test: () => Boolean) = {
  val pr = (1 to trials).par
  val s = pr.aggregate(0)( (a,_) => a + (if (test()) 1 else 0), _ + _) 
  s * 4.0 / trials
}

где частичные результаты суммируются параллельно с другими тестовыми расчетами.

Другие вопросы по тегам