Программирование и научные вычисления на языке Python/§18
Используя математические правила из теории вероятностей можно вычислить вероятность того, что произойдет какое-то событие, например, как это случается в задачах, вероятность достать из шляпы шар какого-то цвета. К несчастью, уже при небольшом изменении условий теоретический расчет вероятностей может зайти в тупик и приходится использовать численные методы. К таким методам относится и метод Монте-Карло.
Принципы моделирования методом Монте-Карло
[править]Представим, мы провели N экспериментов и результат каждого эксперимента случайное число. В этих N экспериментах некоторое событие случается M раз. Оценка вероятности тогда M/N и она становится более точной при стремлении числа экспериментов N к бесконечности (заметьте, что дробь при этом не обращается в ноль, поскольку и число M также стремится к бесконечности). Математический метод, заключающийся в использовании большого числа генерируемых случайных чисел, получил название метод Монте-Карло. Этот метод оказался чрезвычайно полезным в науке и промышленности, там где случайное поведение нельзя не учитывать, или, например, там где и не подразумевается наличие случайных чисел — в случаях сложного интегрирования.
Бросание костей
[править]Пусть у нас спрашивают: какова вероятность при броске костей, чтобы по крайне мере на двух из них были шестерки? Итак, эксперимент в том, что кидаются четыре кости, нужное событие - не менее двух кубиков с шестью точками. Собственно, это нам и нужно записать еще раз перефразировав на Python:
import random as random_number
N = int(raw_input('Enter number of experiments: '))
M = 0
for i in range(N):
six = 0
r1 = random_number.randint(1, 6)
if r1 == 6:
six += 1
r2 = random_number.randint(1, 6)
if r2 == 6:
six += 1
r3 = random_number.randint(1, 6)
if r3 == 6:
six += 1
r4 = random_number.randint(1, 6)
if r4 == 6:
six += 1
if six >= 2:
M += 1
p = float(M)/N
print 'probability:', p
Более общая задача — ведь мы можем задать и не только число экспериментов, а, например, число бросаемых костей (ndice) и минимальное число одинаковых кубиков (nsix). Таким образом, мы получаем более общий и даже более короткий код, а более общие решения гораздо проще модифицировать далее:
import random as random_number
import sys
N = int(raw_input('Number of experiments: '))
ndice = int(raw_input('Number of dice: '))
nsix = int(raw_input('Number of dice with six eyes: '))
M = 0
for i in range(N):
six =0
for j in range(ndice):
r = random_number.randint(1, 6)
if r == 6:
six += 1
if six >= nsix:
M += 1
p = float(M)/N
print 'Probability:', p
Для малых вероятностей число M мало и аппроксимация M/N не очень хорошая аппроксимация, требуется большое число экспериментов, которые для стандартного модуля обходятся дорого, то есть долго. Конечно же, мы знаем, что при нехватке скорости надо перейти к более быстрым векторным операциям.
Векторизация состоит в том, что мы создаем двухмерный массив случайных чисел, в котором число строк определяется числом экспериментов (бросков), а число столбцов — числом испытаний (костей):
eyes = random.random_integers(1, 6, (N, ndice))
Следующий шаг — посчитать число нужных нам событий в каждом эксперименте. Для того, чтобы программа работала быстрее мы должны постараться избежать циклов. В предыдущем уроке мы использовали прием создания массива по выполнению условия, этот же прием мы используем ниже. Далее в полученном массиве суммируем элементы строк, а это нули и единицы, и в случае если число единиц равно или больше требуемому числу шестерок, то считаем, что событие произошло:
from numpy import random, sum
N = int(raw_input('Number of experiments: '))
ndice = int(raw_input('Number of dice: '))
nsix = int(raw_input('Number of dice with six eyes: '))
eyes = random.random_integers(1, 6, (N, ndice))
compare = eyes == 6
nthrows_with_6 = sum(compare, axis=1) # суммирование по столбцам - элементам строки (axis = 1)
nsuccesses = nthrows_with_6 >= nsix
M = sum(nsuccesses)
p = float(M)/N
print 'probability:', p
И подсчет при этом для большого числа экспериментов, происходит существенно быстрее.
Шары из шляпы
[править]В шляпе 12 шаров: четыре черных, четыре красных и четыре синих. И мы хотим написать программу, которая достает из шляпы три случайных шара. Шары у нас отличаются цветом, при этом эти цвета неизменны, значит их удобно представить в виде кортежа, а саму шляпу в качестве списка строк:
colors = 'black', 'red', 'blue' # (кортеж строк)
hat = []
for color in colors:
for i in range(4):
hat.append(color)
Чтобы достать шар:
import random as random_number
color = random_number.choice(hat)
print color
Но нам нужно достать несколько шаров. При этом мы не возвращаем их обратно в шляпу, а это значит что элементы (шары) удаляются из списка hat. Три способа: 1) использовать hat.remove(color), то есть достали, например, синий шар - значит одним синим шаром в списке меньше, 2) выбирать шар по случайному индексу, а затем по нему же его удалять через del hat[index], 3) то же самое, но более коротким способом через hat.pop(index):
def draw_ball(hat):
color = random_number.choice(hat)
hat.remove(color)
return color, hat
# или:
def draw_ball(hat):
index = random_number.randint(0, len(hat)-1)
color = hat[index]
del hat[index]
return color, hat
# или:
def draw_ball(hat):
index = random_number.randint(0, len(hat)-1)
color = hat.pop(index)
return color, hat
balls = []
for i in range(n): # n - число доставаемых шаров
color, hat = draw_ball(hat)
balls.append(color)
print 'Got the balls', balls
Продлим наше решение на более конкретную задачу, чем просто доставание из шляпы шаров: какова вероятность достать два и более черных шара из этой шляпы. В таком случае мы можем проделать N экспериментов и подсчитать сколько M раз мы достали не менее двух черных шаров и найти вероятность такого события как M/N. Один эксперимент для нас состоит в создании нового списка hat (перемешивании шаров), доставании шаров и подсчете числа черных. Последнее может быть легко подсчитано с помощью метода списков count, то есть в нашем случае hat.count('black'). Тогда оставим одну понравившуюся нам функцию draw_ball(hat) и допишем нашу программу следующим образом:
import random as random_number
def draw_ball(hat):
color = random_number.choice(hat)
hat.remove(color)
return color, hat
def new_hat():
colors = 'black', 'red', 'blue'
hat = []
for color in colors:
for i in range(4):
hat.append(color)
return hat
n = int(raw_input('How many balls are to be drawn? '))
N = int(raw_input('How many experiments? '))
M = 0
for e in range(N):
hat = new_hat()
balls = []
for i in range(n):
color, hat = draw_ball(hat)
balls.append(color)
if balls.count('black') >= 2:
M += 1
print 'Probability:', float(M)/N
Ограничение роста населения
[править]В Китае уже в течение нескольких лет супружеским парам разрешено иметь только одного ребенка. Однако успех в проведении этой политики ограничивается рядом факторов. Одна из проблем в том, что семьи чаще оставляют сыновей, которые могут помочь семье, и отсюда все сильнее растет доля мужского населения. Отсюда альтернативная политика в том, что семьям разрешается завести дочь пока не родится сын. Мы можем промоделировать эту ситуацию и посмотреть как будет расти численность в таком обществе. Поскольку мы работаем с большой популяцией, сразу зададимся тем, что поиск решения будем искать в векторизованном варианте.
Представим, у нас есть n индивидуумов, назовем их parents, которым мы придаем некоторые равномерно распределенные случайные значения. Далее из статистических данных мы знаем долю мужчин (male_portion), и всех индивидуумов, у которых их случайное число меньше male_portion, считаем мужчинами и присваиваем значение 1, а у кого выше — женщинами и присваиваем значение 2. Чтобы код было удобнее читать, введем константы MALE=1 и FEMALE=2. Наша задача в том, чтобы посмотреть как меняется массив parents для каждого нового поколения при учете двух указанных политик. Итак, создаем исходный массив:
from numpy import random, zeros
r = random.random(n)
parents = zeros(n, int) # массив нулей размером n
MALE = 1; FEMALE = 2
parents[r < male_portion] = MALE
parents[r >= male_portion] = FEMALE
Число потенциально возможных супружеских пар это минимум от числа мужчин и числа женщин. Однако, даже если мы посчитаем, что все эти пары сложатся, то все равно только часть из них дадут детей. При учете политики «одна семья — один ребенок» получаем:
males = len(parents[parents==MALE]) # число мужчин
females = len(parents) - males # число женщин
couples = min(males, females) # число потенциальных супружеских пар
n = int(fertility*couples) # пары, родившие ребенка, fertility - рождаемость
# следующее поколение
r = random.random(n)
children = zeros(n, int)
children[r < male_portion] = MALE
children[r >= male_portion] = FEMALE
Код, написанный для рождении нового поколения будет нужен при каждой новой генерации. Поэтому эти инструкции более естественно заключить в соответствующую функцию:
def get_children(n, male_portion, fertility):
n = int(fertility*n)
r = random.random(n)
children = zeros(n, int)
children[r < male_portion] = MALE
children[r >= male_portion] = FEMALE
return children
При условии более мягкой политики, назовем ее «политикой одного сына», родители могут рожать детей, пока у них не появится сын. В рамках нашей оценочной задачи будем упрощенно считать, что они так и делают:
# сначала обычное
children = get_children(couples, male_portion, fertility)
# в случае, если дочь:
daughters = children[children == FEMALE]
while len(daughters) > 0:
new_children = get_children(len(daughters), male_portion, fertility)
children = concatenate((children, new_children))
daughters = new_children[new_children == FEMALE]
Накопленный опыт мы можем представить в виде общей функции анализа популяции для обеих политик:
def advance_generation(parents, policy='one child',
male_portion=0.5, fertility=1.0,
law_breakers=0, wanted_children=4):
males = len(parents[parents==MALE])
females = len(parents) - males
couples = min(males, females)
if policy == 'one child':
# у каждой пары только один ребенок:
children = get_children(couples, male_portion, fertility)
max_children = 1
elif policy == 'one son':
# каждая пара продолжает рожать детей до тех пор, пока у них не будет сына
children = get_children(couples, male_portion, fertility)
max_children = 1
daughters = children[children == FEMALE]
while len(daughters) > 0:
new_children = get_children(len(daughters), male_portion, fertility)
children = concatenate((children, new_children))
daughters = new_children[new_children == FEMALE]
max_children += 1
# кроме того часть граждан нарушает закон и имеет столько детей, сколько хочет (wanted_children)
illegals = get_children(int(len(children)*law_breakers)*wanted_children, male_portion, fertility=1.0)
children = concatenate((children, illegals))
return children, max_children
Законченная программа с заданием исходных данных тогда будет выглядеть так:
from numpy import random, concatenate, zeros
MALE = 1; FEMALE = 2
def get_children(n, male_portion, fertility):
n = int(fertility*n)
r = random.random(n)
children = zeros(n, int)
children[r < male_portion] = MALE
children[r >= male_portion] = FEMALE
return children
def advance_generation(parents, policy='one child',
male_portion=0.5, fertility=1.0,
law_breakers=0, wanted_children=4):
males = len(parents[parents==MALE])
females = len(parents) - males
couples = min(males, females)
if policy == 'one child':
# у каждой пары только один ребенок:
children = get_children(couples, male_portion, fertility)
max_children = 1
elif policy == 'one son':
# каждая пара продолжает рожать детей до тех пор, пока у них не будет сына
children = get_children(couples, male_portion, fertility)
max_children = 1
daughters = children[children == FEMALE]
while len(daughters) > 0:
new_children = get_children(len(daughters), male_portion, fertility)
children = concatenate((children, new_children))
daughters = new_children[new_children == FEMALE]
max_children += 1
# кроме того часть граждан нарушает закон и имеет столько детей, сколько хочет (wanted_children)
illegals = get_children(int(len(children)*law_breakers)*wanted_children, male_portion, fertility=1.0)
children = concatenate((children, illegals))
return children, max_children
N = 1000000
male_portion = 0.51
fertility = 0.92
law_breakers = 0.06
wanted_children = 6
generations = 10
# начнем с "идеального поколения" родителей:
start_parents = get_children(N, male_portion=0.5, fertility=1.0)
parents = start_parents.copy()
print 'one child policy, start: %d' % len(parents)
for i in range(generations):
parents, mc = advance_generation(parents, 'one child',
male_portion, fertility,
law_breakers, wanted_children)
print '%3d: %d' % (i+1, len(parents))
parents = start_parents.copy()
print 'one son policy, start: %d' % len(parents)
for i in range(generations):
parents, mc = advance_generation(parents, 'one son',
male_portion, fertility,
law_breakers, wanted_children)
print '%3d: %d (max children in a family: %d)' % (i+1, len(parents), mc)
Интегрирование
[править]Одним из самых ранних применений генерируемых случайных чисел было вычисление интегралов. Пусть мы генерируем равномерно распределенные между a и b случайные числа x1, ..., xn, тогда аппроксимация решения будет найдена как
(18.1) |
Этот метод обычно и называется интегрирование по Монте-Карло. Мы можем представить выражение (18.1) в виде небольшой программы:
import random as random_number
def MCint(f, a, b, n):
s = 0
for i in range(n):
x = random_number.uniform(a, b)
s += f(x)
I = (float(b-a)/n)*s
return I
Обычно, достаточная точность метода задается большим числом n, поэтому векторизованная версия будет более удобной:
from numpy import *
def MCint_vec(f, a, b, n):
x = random.uniform(a, b, n)
s = sum(f(x))
I = (float(b-a)/n)*s
return I
Рассмотрим интегрирование методом Монте-Карло на примере простой линейной функции , пределы интегрирования — от 1 до 2. Было бы интересно посмотреть как метод справляется с решением задачи для различных n. Оценку произведем следующим немного измененным MCint методом:
def MCint2(f, a, b, n):
s = 0
I = zeros(n)
for k in range(1, n+1):
x = random_number.uniform(a, b)
s += f(x)
I[k-1] = (float(b-a)/k)*s
return I
Заметим, что k' изменяется от 1 до n, в то время как индексы I как и раньше идут от 0 до n-1. Поскольку n может быть очень большим, массив I может переполнить память. Поэтому следует записывать только каждое N-ое значение аппроксимации. Это возможно с помощью известной нам функции определения остатка:
for k in range(1, n+1):
...
if k % N == 0:
# store
Итак, каждый раз, когда k делится на N без остатка, мы записываем значение (в нашем случае каждое сотое). Соответствующая функция представлена ниже.
def MCint3(f, a, b, n, N=100):
'''Cохраняет каждое N приближение интеграла
в массив I и записываем соответствующее значение k'''
s = 0
I_values = []
k_values = []
for k in range(1, n+1):
x = random_number.uniform(a, b)
s += f(x)
if k % N == 0:
I = (float(b-a)/k)*s
I_values.append(I)
k_values.append(k)
return k_values, I_values
Теперь у нас есть инструмент для того, чтобы посмотреть как изменяется ошибка в интегрировании методом Монте-Карло с ростом n. Законченная программа выглядит следующим образом, результат работы программы (может несколько отличаться ввиду случайности) представлен справа:
import random as random_number
import matplotlib.pyplot as plt
from numpy import array
def MCint3(f, a, b, n, N=100):
'''Cохраняет каждое N приближение интеграла
в массив I и записываем соответствующее значение k'''
s = 0
I_values = []
k_values = []
for k in range(1, n+1):
x = random_number.uniform(a, b)
s += f(x)
if k % N == 0:
I = (float(b-a)/k)*s
I_values.append(I)
k_values.append(k)
return k_values, I_values
def f1(x):
return 2 + 3*x
k, I = MCint3(f1, 1, 2, 1000000, N=10000)
error = 6.5 - array(I)
plt.title('Monte Carlo integration')
plt.xlabel('n')
plt.ylabel('error')
plt.plot(k, error)
plt.show()
Интегрирование через вычисление площади под кривой
[править]Представим, у нас есть некоторая сложная фигура G , площадь которой мы хотим вычислить. Эту фигуру мы можем заключить в прямоугольник B определяемый координатами [X1, X2] x [Y1, Y2]. Далее в этот прямоугольник мы "бросаем" случайные числа из заданного массива [X, Y] и считаем сколько точек попало внутрь контура фигуры G. Далее, зная отношение числа точек, "упавших" в контур G к общему числу "брошенных" точек, и умножив это отношение на площадь простой фигуры B, мы получаем площадь сложной фигуры G.
Таким же образом мы могли бы находить интеграл от функции, поскольку он и определяет площадь под ней. Представим, мы знаем как выглядит функция, область интегрирования лежит в пределе от a до b, а максимум функции в указанном интервале равен m. Тогда аналогично, предыдущему рассмотрению, интеграл будет выражен через число "выброшенных" точек N и число упавших "под функцию" M как .
Векторизованное решение может в простейшем случае выглядит так:
from numpy import random
def MCint_area_vec(f, a, b, m, N):
x = random.uniform(a, b, N)
y = random.uniform(0, m, N)
M = y[y < f(x)].size # в квадратных скобках условие,
# size считает число ему удовлетворяющих элементов
area = M/float(N)*m*(b-a)
return area
Решение можно написать и через стандартный random в обычном виде с помощью цикла, но скорость его будет существенно меньшей.