H Вероятностное программирование Из песочницы. Информационный портал по безопасности

Можно выделить два основных класса вероятностных языков программирования – это языки, допускающие задание генеративных моделей только в форме Байесовских сетей (или других графических вероятностных моделей), или Тьюринг-полные языки.

Типичным представителем первых является Infer.NET, разрабатываемый в Microsoft. В нем благодаря использованию в качестве генеративных моделей Байесовких сетей оказывается возможным применять известные для них эффективные методы вывода. Естественно, использование хорошо известного класса моделей с известными методами вывода не приводит к возможности решения каких-то принципиально новых задач (и даже такие генеративные модели, как сети глубокого обучения на основе ограниченных машинах Больцмана оказываются не представимы в таких языках), но дает вполне практичный инструмент. Как говорят разработчики, с использованием этого инструмента можно за пару часов реализовать нетривиальную вероятностную модель, такую как полная Байесовская версия анализа главных компонент, которая будет занимать всего пару десятков строк кода и для которой отдельная реализация эффективной процедуры вывода на обычном языке потребовала бы заметно большего объема знаний и нескольких недель работы. Таким образом, за счет вероятностного программирования использование графических моделей становится гораздо более простым и доступным.


Гораздо большим потенциалом, однако, обладают Тьюринг-полные вероятностные языки. Они позволяют выйти за рамки того класса задач, которые существующие методы машинного обучения уже умеют решать. Естественно, в таких языках возникает проблема эффективности вывода, которая пока далека от решения, что приводит к плохой масштабируемости на задачи реального мира. Однако это направление активно развивается, и существует ряд работ, показывающих как в вероятностных языках общего назначения достичь эффективного вывода для интересных практических задач. Можно надеяться, что в ближайшем будущем эти решения станут доступными для использования в конкретных языках. Кроме того, Тьюринг-полные вероятностные языки уже сейчас оказываются весьма полезными в исследованиях, связанных с когнитивным моделированием и общим искусственным интеллектом. По этим причинам мы и рассмотрим основные принципы вероятностного программирования именно на примере Тьюринг-полных языков, из которых мы выбрали Чёрч (Church), являющийся расширением языка Лисп (конкретнее, его диалекта – Scheme). Удобство этого языка (по крайней мере, в целях начального знакомства с ним) заключается в существовании для него web-реализации (web-church), с которой можно экспериментировать без установки дополнительного программного обеспечения.

Итак, к делу

Программа на вероятностном языке может, на первый взгляд, ничем не отличаться от программы на обычном языке. Именно так сделано в Чёрче. Как и в обычном Лиспе, в этом языке могут быть определены переменные, функции, выполнены детерминированные вычисления. Например, следующая программа задает функцию от одного аргумента, вычисляющую факториал по рекурсивной формуле n!=n*(n–1)!, и вызывает эту функцию для n=10

(define (f n)
(if (= n 0) 1 (* n (f (– n 1)))))
(f 10)

Также в этом языке могут быть обращения к (псевдо)случайным функциям. Например, при выполнении вызова (flip 0.3) с вероятностью 0.3 будет возвращено значение #t, а с вероятностью 0.7 – #f. Такая функция элементарно реализуется и в Лиспе как



(define (flip p) (< (random) p))

Чёрч, как и другие вероятностные языки, включает много встроенных функций, которые возвращают случайные значения в соответствии с тем или иным распределением. Например, (gaussian x0 s) возвращает вещественную случайную величину, распределенную по гауссиане с заданными параметрами. В качестве других реализованных распределений вероятностей обычно присутствуют равномерное, мультиномиальное, Дирихле, бета, гамма. Все эти распределения не так сложно реализовать вручную в обычном языке, и здесь пока нет принципиального отличия между Чёрчем и Лиспом.

Однако помимо обычной семантики программа на Чёрче обладает вероятностной семантикой, в рамках которой полагается, что программа, содержащая вызовы случайных функций, не просто при своем запуске порождает какие-то конкретные значения случайных величин, но задает распределение вероятностей над ними. Так, (gaussian x0 s) – это не просто функция, возвращающая некоторое конкретное значение случайной величины, распределенной по гауссиане, но именно само Гауссово распределение.


Но как получать эти распределения вероятностей, задаваемые программой? Представим, например, программу



(if (flip 0.4) (flip 0.1) (flip 0.6))

То есть с вероятностью 0.4 значение этого выражения – это P(#t)=0.1 и P(#f)=0.9, а с вероятностью 0.6 – P(#t)=0.6 и P(#f)=0.4. Откуда возьмется итоговое распределение, задаваемое этим выражением: P(#t)=0.4 и P(#f)=0.6? Эта вероятностная семантика зачастую реализуется через процесс сэмплирования: мы можем просто много раз запустить программу и построить выборку результатов ее выполнения. Такую процедуру, конечно, также несложно реализовать на обычном языке (и, действительно, еще Симула-67 таким способом регулярно использовалась для моделирования стохастических процессов).

Однако современные вероятностные языки идут дальше и добавляют в процесс сэмплирования условие, накладываемое на результаты выполнения программы. Эта идея ведет к простейшему сэмплированию с отказами, которая в Чёрче реализуется функцией rejection-query. Эта функция на вход принимает вероятностную программу (как совокупность define), предпоследнее выражение в которой вычисляет возвращаемое значение, а последнее выражение – это условие (предикат), который в процессе выполнения должен оказаться истинным. Рассмотрим программу



(rejection-query
(define A (flip 0.4))
(define B (flip 0.6))
B
(or A B))

rejection-query выполняет поданную ей программу до тех пор, пока не будет выполнено последнее условие – здесь (or A B) – и возвращает (один раз) значение предпоследнего выражения – здесь B. Чтобы получить выборку значений, можно воспользоваться функцией repeat. Также Чёрч имеет встроенные функции для построения гистограмм. Рассмотрим немного расширенную программу:



(define (get-sample) (rejection-query
(define A (flip 0.4))
(define B (flip 0.6))
B
(or A B)))
(hist (repeat 1000 get-sample))

При запуске мы получим следующий результат: #f — 21%, #t — 79% (цифры от запуска к запуску могут немного меняться). Этот результат означает, что значение B равно #t с вероятностью чуть меньше 0.8. Откуда взялась эта вероятность, если в программе B – это бинарная случайная величина, для которой P(#t)=0.6? Очевидно, дело в наложении условия: (or A B). В процессе сэмплирования мы принимаем только такие значения B, что верно или A, или само B. Фактически, мы считаем апостериорную вероятность P(B|A+B). Можно было бы воспользоваться правилом Байеса для того, чтобы вычислить эту вероятность вручную:



P(B|A+B) = P(A+B|B)P(B)/P(A+B) =
=(P(A|B)+P(B|B)–P(A|B)P(B|B))P(B)/(P(A)+P(B)–P(A)P(B))=
=(P(A)+1–P(A))P(B)/(P(A)+P(B)–P(A)P(B))=0.6/(0.4+0.6–0.4*0.6)=0.789.

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

Итак, сэмплирование позволяет нам вычислять апостериорные вероятности интересующих нас случайных величин при наложении тех или иных условий. Оно заменяет правило Байеса, широко используемое в машинном обучении для выбора моделей или выполнения предсказаний. При этом запись программы на вероятностном языке для многих людей может оказаться гораздо понятнее, чем применение правила Байеса. Конечно, само режекторное сэмплирование весьма просто можно реализовать на обычном языке программирования, но вероятностные языки этим не ограничиваются.

В Чёрче, в частности, реализована другая функция для сэмплирования – enumeration-query. Запустим программу



(enumeration-query
(define A (flip 0.4))
(define B (flip 0.6))
B
(or A B))

На выходе мы получим: ((#t #f) (0.7894736842105263 0.2105263157894737)). Здесь выведены точные значения (конечно, со скидкой на конечную разрядную сетку) вероятностей P(B|A+B). enumeration-query уже не просто запускает много раз программу, но анализирует пути ее выполнения и перебирает все возможные значения случайных переменных с учетом их вероятностей. Конечно, такое «сэмплирование» будет работать, только когда множество возможных комбинаций значений случайных переменных не слишком велико.


Есть в Чёрче и более продвинутая замена режекторному сэмплированию на основе MCMC (Monte Carlo Markov Chains), а именно Metropolis Hastings алгоритм, откуда и название у процедуры – mh-query. Эта процедура запроса сразу формирует заданное число сэмплов (а также получает на вход один дополнительный параметр – лаг). Эта процедура также нетривиальна в реализации, так что использование готового вероятностного языка (а не собственная реализация простых процедур сэмплирования на обычном языке) приобретает смысл.


Однако главное, что дает вероятностное программирование, – это стиль мышления.

От азов к применению

Разные разработчики находят разные применения вероятностному программированию. Многие применяют его непосредственно для решения задач машинного обучения. Авторы же языка Чёрч, Noah D. Goodman and Joshua B. Tenenbaum, в своей web-книге «Probabilistic Models of Cognition» показывают применение вероятностного программирования для когнитивного моделирования. Также известно, как решение задач планирования удобно представлять в терминах вывода в вероятностных языках. Оно также оказывается применимым для представления знаний и вывода над ними, а также для задач машинного восприятия (в том числе, распознавания изображений). Все эти приложения пока более или менее разрозненные, но наличие общего фреймворка для всех них свидетельствует о том, что вероятностное программирование может стать «теорией великого объединения» для ИИ. Посмотрим на простейшие примеры возможного использования.

Одним из наиболее классических примеров применения экспертных систем является медицинская диагностика. В частности, система MYCIN была построена на системе правил вида:


  1. THE SITE OF THE CULTURE IS BLOOD

  2. THE GRAM OF THE ORGANISM I S NEG

  3. THE MORPHOLOGY OF THE ORGANISM IS ROD

  4. THE BURN OF THE PATIENT IS SERIOUS

Then there is weakly suggestive evidence (0.4) that


  1. THE IDENTITY OF THE ORGANISM IS PSEUDOMONAS

Очевидно, правила такого вида хорошо описываются на языке типа Чёрч. При этом нет необходимости еще и реализовывать процедуру вывода – достаточно просто записать систему правил. Приведем пример из упомянутой книги «Probabilistic Models of Cognition»:



(define samples
(mh-query 1000 100
(define lung-cancer (flip 0.01))
(define TB (flip 0.005))
(define cold (flip 0.2))
(define stomach-flu (flip 0.1))
(define other (flip 0.1))

(define cough (or (and cold (flip 0.5)) (and lung-cancer (flip 0.3)) (and TB (flip 0.7)) (and other (flip 0.01))))
(define fever (or (and cold (flip 0.3)) (and stomach-flu (flip 0.5)) (and TB (flip 0.2)) (and other (flip 0.01))))
(define chest-pain (or (and lung-cancer (flip 0.4)) (and TB (flip 0.5)) (and other(flip 0.01))))
(define shortness-of-breath (or (and lung-cancer (flip 0.4)) (and TB (flip 0.5)) (and other (flip 0.01))))

(list lung-cancer TB)
(and cough fever chest-pain shortness-of-breath)))
(hist samples "Joint inferences for lung cancer and TB")

В этой программе определяются априорные вероятности появления у больного рака легких, туберкулеза, простуды и т.д. Далее определяются вероятности наблюдения кашля, жара, боли в груди и стесненного дыхания при тех или иных заболеваниях. Возвращаемая величина – это пара булевых значений, есть ли у пациента рак и/или туберкулез. И, наконец, условие – это совокупность наблюдаемых симптомов (то есть сэмплирование производится при условии, что значение всех переменных – cough fever chest-pain shortness-of-breath – #t).


Результат выполнения программы будет иметь следующий вид: (#f #f) — 4%, (#f #t) — 58%, (#t #f) — 37%, (#t #t) — 1%.

Несложно сделать, чтобы samples была функцией, в которую подается перечень симптомов, который далее в mh-query используется для сэмплирования, что даст возможность ставить диагнозы разным пациентам. Конечно, этот пример сильно упрощенный, но видно, что в стиле вероятностного программирования вполне можно представлять знания и делать вывод над ними.


Естественно, можно решать и задачи машинного обучения. Их отличие будет лишь в том, что неизвестными величинами будут параметры самой модели, а в качестве условия для сэмплирования будет выступать генерация этой моделью обучающей выборки. К примеру, в представленной выше программе мы бы числа в строках вида (define lung-cancer (flip 0.01)) могли бы заменить на переменные, которые сами бы задавались как случайные, например (define p-lung-cancer (uniform 0 1)), а далее для каждого пациента из обучающей выборки значение lung-cancer уже определялось бы с вероятностью p-lung-cancer.


Рассмотрим эту возможность на простом примере оценивания параметров многочлена по набору точек. В следующей программе функция calc-poly вычисляет значение многочлена с параметрами ws в точке x. Функция generate применяет calc-poly к каждому значению из заданного списка xs и возвращает список соответствующих ординат. Процедура noisy-equals? «приближенно» сравнивает два заданных значения (если эти значения равны, то функция возвращает #t с вероятностью 1; если же они не равны, то чем больше они отличаются, тем с меньшей вероятностью она вернет #t).



(define (calc-poly x ws)
(if (null? ws) 0
(+ (car ws) (* x (calc-poly x (cdr ws))))))

(define (generate xs ws)
(map (lambda (x) (calc-poly x ws)) xs))

(define (noisy-equals? x y)
(flip (exp (* -3 (expt (- x y) 2)))))

(define (samples xs ys)
(mh-query 1 100
(define n-coef 4)
ws
(samples "(0 1 2 3 4) "(0.01 1.95 6.03 12.01 20.00))

Внутри вызова mh-query параметр n-coef определяет число коэффициентов в многочлене (то есть его степень минус один); ws – это список, состоящий из случайных величин, сгенерированных в соответствии с нормальным распределением. Возвращаемое значение – список параметров многочлена. Условие для сэмплирования – «приближенное» равенство всех заданных значений ys всем ординатам, порожденным многочленом при данных ws. Здесь мы запрашиваем всего одну реализацию, которая проходит по условию (поскольку строить гистограмму для вектора параметров не очень удобно). Результатом этого запроса может быть, к примеру, список (2.69 1.36 0.53 -0.10), задающий многочлен 2.69+1.36x+0.53x^2–0.10x^3.


Вообще, вывод на моделях с вещественными параметрами – не самая сильная сторона языка Чёрч (но не стоит это считать глобальным недостатком вероятностного программирования вообще). Тем не менее, на этом примере mh-query кое-как работает. Чтобы в этом убедиться, вместо определения значений параметров в запросе можно попросить возвращать предсказание в некоторой точке. Перепишем последний фрагмент кода так:



(define (samples xs ys)
(mh-query 100 100
(define n-coef 4)
(define ws (repeat n-coef (lambda () (gaussian 0 3))))
(calc-poly 5 ws)
(all (map noisy-equals? (generate xs ws) ys))))
(hist (samples "(0 1 2 3 4) "(0.01 1.95 6.03 12.01 20.00)))

То есть мы запрашиваем наиболее вероятное (при имеющихся данных) значение в точке x=5. При разных запусках максимум гистограммы, к сожалению, будет приходиться на несколько различающиеся значения (метод MCMC, теоретически, гарантирует схождение к истинному распределению, но лишь в пределе), но обычно эти значения будут достаточно вразумительными. Стоит заметить, что здесь мы «бесплатно» (заменой одной строчки) получили полное байесовское предсказание: вместо выбора лучшей модели и предсказания лишь по ней, мы получили апостериорное распределение значений в точке x=5, усредненное сразу по множеству моделей с учетом их собственных вероятностей.

Но и это еще не все. Опять же, заменой одной строчки – (define n-coef 4) -> (define n-coef (random-integer 5)) мы можем сделать автоматический выбор между моделями с разным числом параметров. Причем сэмплирование величины n-coef показывает (хотя и не очень стабильно), что наиболее вероятным значением является n-coef=3 (то есть парабола, которая и заложена в заданный набор точек). При такой модификации более стабильным становится и предсказание. Иными словами, не возникает и эффекта переобучения! Почему же не выбираются многочлены более высокой степени, ведь они могут точнее проходить к заданным точкам? Дело в том, что при сэмплировании «угадать» подходящие значения параметров многочлена меньшей степени проще, чем многочлена более высокой степени, поэтому вероятность породить такие параметры, которые пройдут проверку, для многочлена второй степени выше, чем для третьей. В то же время, многочлен первой степени будет давать большие отклонения, для которых вероятность срабатывания noisy-equals? будет сильно понижаться.


Посмотрим еще на одно применение, которое в рамках вероятностного программирования может показаться неожиданным. Это решение «дедуктивных» задач. Возьмем приведенную в начале функцию вычисления факториала, но вместо вызова ее с фиксированным значением будем считать, что аргумент – случайная переменная, но на само значение факториала наложено ограничение:



(define (f n)
(if (= n 0) 1 (* n (f (- n 1)))))
(enumeration-query
(define n (random-integer 20))
n
(equal? (f n) 120))

В качестве ответа мы увидим n=5 с вероятностью 1. Если же мы вместо 120 зададим 100, то программа не зациклится (в отличие от случая использования rejection-query или mh-query, что можно считать их недостатком), а просто вернет пустое множество. Можно поставить в качестве условия и не строгое равенство, а какое-то другое ограничение.


Таким же образом можно решать и более сложные задачи. Допустим, мы хотим решить задачу о сумме подмножеств: в ней надо из заданного множества чисел найти такое подмножество, сумма в котором равна заданному числу (обычно в качестве этого числа берется 0 и требуется, чтобы подмножество было не пустым; но чтобы избавиться от проверки на нетривиальность решения, мы возьмем ненулевую сумму). Казалось бы, причем тут вероятностное программирование? Но случайные величины – это просто неизвестные величины (для которых заданы априорные вероятности). В любых задачах нам нужно найти что-то неизвестное, в том числе и в задаче о сумме подмножеств. Посмотрим на следующую элементарную программу (ее можно было бы даже еще упростить, записав summ через fold).



(define (solution xs v)
(rejection-query
(define ws (repeat (length xs) flip))
(define (summ xs ws)
(if (null? xs) 0
(+ (if (car ws) (car xs) 0) (summ (cdr xs) (cdr ws)))))
ws
(equal? (summ xs ws) v)))
(solution "(-1 3 7 5 -9 -1) 1)

Здесь ws – список случайных булевых значений. Процедура summ вычисляет сумму элементов списка xs, для которых соответствующие элементы списка ws истинны. Далее запрашивается значения ws, для которых выполняется условие равенства полученной суммы заданному числу v. Запустив эту программу, можно получить такой результат: (#f #t #t #f #t #f), который, конечно, является правильным (так как 3+7-9=1).


Естественно, Чёрч не делает чуда и при повышении размерности этой задачи он с ней не справится. Однако не может не удивлять то, что столь разные задачи ИИ могут быть хотя бы поставлены (и отчасти решены) с использованием одного и того же языка. Ну, а проблема эффективного вывода как была, так и остается. В вероятностных языках она хотя бы выделяется в чистом виде.


This entry passed through the Full-Text RSS service - if this is your content and you"re reading it on someone else"s site, please read the FAQ at http://ift.tt/jcXqJW.


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

Я, автор, Юра Перов, занимаюсь вероятностным программированием в течение уже двух лет в рамках своей основной учебно-научной деятельности. Продуктивное знакомство с вероятностным программированием у меня сложилось, когда будучи студентом Института математики и фундаментальной информатики Сибирского федерального университета, я проходил стажировку в Лаборатории компьютерных наук и искусственного интеллекта в Массачусетском технологическом институте под руководством профессора Джошуа Тененбаума и доктора Викаша Мансингхи, а затем продолжилось на Факультете технических наук Оксфордского университета, где на данный момент я являюсь студентом-магистром под руководством профессора Френка Вуда.

Вероятностное программирование я люблю определять как компактный , композиционный способ представления порождающих вероятностных моделей и проведения статистического вывода в них с учетом данных с помощью обобщенных алгоритмов. Хотя вероятностное программирование не вносит много фундаментального нового в теорию машинного обучения, этот подход привлекает своей простотой: «вероятностные порождающие модели в массы!»

«Обычное» программирование

Для знакомства с вероятностным программирование давайте сначала поговорим об «обычном» программировании. В «обычном» программировании основой является алгоритм, обычно детерминированный, который позволяет нам из входных данных получить выходные по четко установленным правилам.

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


А теперь вероятностное программирование

Однако часто мы знаем только результат, исход, и мы заинтересованы в том, чтобы узнать то, какие неизвестные значения привели именно к этому результату? Чтобы ответить на этот вопрос с помощью теории математического моделирования создается вероятностная модель, часть параметров которой не определены точно.

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


Итак, зная выходные данные, мы заинтересованы в том, чтобы узнать наиболее вероятные значения скрытых, неизвестных параметров.

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

Существует более 15 языков вероятностного программирования, перечень с кратким описанием каждого из них можно найти . В данной публикации приведен пример кода на вероятностных языках Venture /Anglican , который имеют очень схожий синтаксис и которые берут свое начало от вероятностного языка Church . Church в свою очередь основан на языке «обычного» программирования Lisp и Scheme. Заинтересованному читателю крайне рекомендуется ознакомиться с книгой , являющейся одним из лучших способов начать знакомство с языком «обычного» программирования Scheme.

Пример Байесовской линейной регрессии

Рассмотрим задание простой вероятностной модели Байесовской линейной регрессии на языке вероятностного программирования Venture/Anglican в виде вероятностной программы:

01: 02: 03: 04: 05: 06: 07: 08: 09: 10:

Скрытые искомые параметры - значения коэффициентов t1 и t2 линейной функции x = t1 + t2 * time . У нас есть априорные предположения о данных коэффициентах, а именно мы предполагаем, что они распределены по закону нормального распределения Normal(0, 1) со средним 0 и стандартным отклонением 1. Таким образом, мы определили в первых двух строках вероятностной программы априорную вероятность на скрытые переменные, P(T) . Инструкцию можно рассматривать как определение случайной величины с именем name , принимающей значение вычисляемого выражение (программного кода) expression , которое содержит в себе неопределенность.

Вероятностные языки программирования (имеются в виду конкретно Church, Venture, Anglican), как и Lisp/Scheme, являются функциональными языками программирования, и используют польскую нотацию при записи выражений для вычисления. Это означает, что в выражении вызова функции сначала располагается оператор, а уже только потом аргументы: (+ 1 2) , и вызов функции обрамляется круглыми скобками. На других языках программирования, таких как C++ или Python, это будет эквивалентно коду 1 + 2 .

В вероятностных языках программирования выражение вызова функции принято разделять на три разных вида:

  • Вызов детерминированных процедур (primitive-procedure arg1… argN) , которые при одних и тех же аргументах всегда возвращают одно и то же значение. К таким процедурам, например, относятся арифметические операции.
  • Вызов вероятностных (стохастических) процедур (stochastic-procedure arg1… argN) , которые при каждом вызове генерируют случайным образом элемент из соответствующего распределения. Такой вызов определяет новую случайную величину . Например, вызов вероятностной процедуры (normal 1 10) определяет случайную величину, распределенную по закону нормального распределения Normal(1, sqrt(10)) , и результатом выполнения каждый раз будет какое-то вещественное число.
  • Вызов составных процедур (compound-procedure arg1… argN) , где compound-procedure - введенная пользователем процедура с помощью специального выражения lambda : (lambda (arg1… argN) body) , где body - тело процедуры, состоящее из выражений. В общем случае составная процедура является стохастической (недетерминированной) составной процедурой, так как ее тело может содержать вызовы вероятностных процедур.

Вернемся к исходному коду на языке программирования Venture/Anglican. После первых двух строк мы хотим задать условную вероятность P(X | T) , то есть условную вероятность наблюдаемых переменных x1 , x2 , x3 при заданных значениях скрытых переменных t1 , t2 и параметра time .

Перед вводом непосредственно самих наблюдений с помощью выражения мы определяем общий закон для наблюдаемых переменных xi в рамках нашей модели, а именно мы предполагаем, что данные наблюдаемые случайные величины при заданных t1 , t2 и заданном уровне шума noise распределены по закону нормального распределения Normal(t1 + t2 * time, sqrt(noise)) со средним t1 + t2 * time и стандартным отклонением noise . Данная условная вероятность определена на строках 3 и 4 данной вероятностной программы. noisy_x определена как функция, принимающая параметр time и возвращающая случайное значение, определенное с помощью вычисления выражение и обусловленное значениями случайных величин t1 и t2 и переменной noise . Отметим, что выражение (normal (+ t1 (* t2 time)) noise) содержит в себе неопределенность, поэтому каждый раз при его вычислении мы будем получать в общем случае разное значение.

На строках 5-7 мы непосредственно вводим известные значения x1 = 10.3 , x2 = 11.1 , x3 = 11.9 . Инструкция вида фиксирует наблюдение о том, что случайная величина, принимающая значение согласно выполнению выражения expression , приняла значение value .

Повторим на данном этапе всё, что мы сделали. На строках 1-4 с помощью инструкций вида мы задали непосредственно саму вероятностную модель: P(T) и P(X | T) . На строках 5-7 мы непосредственно задали известные нам значения наблюдаемых случайных величин X с помощью инструкций вида .

На строках 8-9 мы запрашиваем у системы вероятностного программирования апостериорное распределение P(T | X) скрытых случайных величин t1 и t2 . Как уже было сказано, при большом объеме данных и достаточно сложных моделях получить точное аналитическое представление невозможно, поэтому инструкции вида генерируют выборку значений случайных величин из апостериорного распределения P(T | X) или его приближения. Инструкция вида в общем случае генерирует один элемент выборки из значений случайной величины, принимающей значение согласно выполнению выражения expression . Если перед инструкциями вида расположены инструкции вида , то выборка будет из апостериорного распределения (говоря точнее, конечно, из приближения апостериорного распределения), обусловленного перечисленными ранее введенными наблюдениями.

Отметим, что в завершении мы можем также предсказать значение функции x(time) в другой точке, например, при time = 4.0 . Под предсказанием в данном случае понимается генерация выборки из апостериорного распределения новой случайной величины при значениях скрытых случайных величин t1 , t2 и параметре time = 4.0 .

Для генерации выборки из апостериорного распределения P(T | X) в языке программирования Venture в качестве основного используется алгоритм Метрополиса-Гастингса, который относится к методам Монте-Карло по схеме Марковских цепей. Под обобщенным выводом в данном случае понимается то, что алгоритм может быть применен к любым вероятностным программам, написанным на данном вероятностном языке программирования.

В видео, прикрепленном ниже, можно посмотреть на происходящий статистический вывод в данной модели.

и Программирование , Блог компании Яндекс , Математика , Машинное обучение , Поисковые технологии

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

Лекция была прочитана Борисом hr0nix Янгелем на факультете компьютерных наук, открытом в Высшей школе экономики при поддержке Яндекса. Сам Борис окончил ВМиК МГУ и Школу анализа данных Яндекса. Работал в Microsoft Research Cambridge в группе Кристофера Бишопа над фреймворком Infer.NET . Сейчас Борис - ведущий разработчик поиска Яндекса.

Под катом - расшифровка рассказа.

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

Чем вообще мы занимаемся в машинном обучении по сути? На самом деле, мы хотим как-то рассуждать о процессах в реальном мире и о том, как эти процессы устроены. Есть некие объекты, которые в этих процессах участвуют. Например, пользователь, который просматривает поисковую выдачу и решает, интересен ему поисковый результат или нет, кликнуть на него, посмотреть следующий результат или вообще уйти со страницы поиска. Это пример объектов. Объекты обладают некоторыми свойствами. Например, у пользователя это могут быть его какие-то сиюминутные информационные потребности, его интересы в целом. Соответственно, есть среда, в которой эти объекты существуют и которая тоже обладает какими-то свойствами. И есть, собственно, сам сложный процесс: процесс подкидывания монетки или процесс просмотра пользователем поисковой выдачи, о котором мы хотим рассуждать. И, как правило, мы имеем какие-то наблюдаемые проявления этого процесса, хотим его обратить и понять, какими были свойства объектов и среды, которые привели к тому, что мы наблюдали. Например, посмотрев несколько раз на то, упала ли монетка орлом или решкой, мы хотим узнать честная монетка или нет. Или, посмотрев несколько раз на то, куда кликал пользователь, мы хотим понять, ушел ли он с поиска счастливым или несчастным, нашел он то, что нужно, или нет, и были ли те документы, которые мы ему показали релевантными или нет.

Тут есть небольшая проблема. Тот сложный процесс, который мы хотим изучать, как правило, довольно проблематично описать наперед так, как он происходит. В случае с нашим пользователем это какие-то сложные биохимические процессы, которые протекают у него в мозгу, и которые в итоге заставляют его принять то или иное решение. Понятно, что обратить его - невероятно сложная вычислительная задача (даже если его можно описать), и таким никто заниматься не будет. Поэтому мы прибегаем к приему, который называется вероятностное моделирование и который заключается в следующем.

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

Далее у нас есть сложный процесс, который мы заменяем некой его округленной версией, которая, с одной стороны, имеет что-то общее с тем, что происходит в действительности, но, с другой стороны, достаточно проста, чтобы с ней можно было работать. В нашем гипотетическом примере с пользователем мы могли бы сказать, что он просматривает выдачу сверху вниз, и, если очередной результат удовлетворяет его информационной потребности, соотносится с его текущими интересами, то значит, он кликает и останавливается, а если нет, то идет дальше. Тут сразу возникает небольшая проблема: если мы посмотрим на реальные клики пользователей, то окажется, что модели, которые я только что предложил, не удовлетворяют в том смысле, что модель все эти паттерны кликов, которые мы наблюдаем в действительности, произвести не может. Например, пользователи иногда кликают на то, что им совсем неинтересно. Почему? Захотелось ему так, или, может он увидел какой-то результат, который его удивил, и захотелось кликнуть и посмотреть, что там будет. Соответственно, наша модель такие данные произвести не может, и в этом проблема.

Что делать? Тут применяется трюк, из-за которого эти модели и называются вероятностными: мы добавляем в модель немного шума, который призван промоделировать те сложные явления, которые напрямую мы промоделировать не можем. Мы считаем, что они такие сложные, что от шума сильно неотличимы, но зато добавление этого шума позволит нашей модели исторгнуть из себя все возможные наблюдения и тогда уже ее можно будет обращать.

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

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

С вероятностным моделированием, к сожалению, есть ряд проблем. Саму модель разработать для какой-нибудь предметной области сравнительно легко. Не в том смысле, что разработать модель лучше всего существующего легко. Это, как правило, довольно сложно, потому что в отдельных предметных областях люди уже годами и десятилетиями только и делают, что придумывают модели. Но если, например, вы столкнулись с каким-то новым процессом, новой предметной областью, в которой вы более-менее разбираетесь и которую хотите промоделировать, и обладаете какими-то базовыми понятиями о том, как вообще вероятностные модели строятся, то придумать разумную модель, наверно, займет не очень много времени. Может быть, часы или дни. Затем вам нужно сделать, собственно, вывод в модели, воспользоваться теоремой Байеса, чтобы найти апостериорное распределение на интересующей вас переменной при условиях наблюдений. Ранее было сказано, что теорема Байеса дает вычислительный рецепт, который вам позволит это сделать для любой модели. С одной стороны, это правда, а с другой стороны, этот вычислительный рецепт может оказаться очень сложным. Грубо говоря, посчитать все члены в теореме Байеса для мало-мальски сложной модели просто не представляется вычислительно возможным. Поэтому уже несколько десятилетий разрабатываются приближенные методы Байесовского вывода, которые позволяют найти некое приближение для апостериорного распределения, которое в определенных условиях будет хорошим приближением. В общем, если вы хотите делать вероятностный вывод сложных вероятностных моделей, то вам неизбежно к таким методам приближенного вывода придется прибегать.

Здесь перечислены несколько таких методов, это EP, VMP, MCMC. Все эти методы довольно сложные в реализации, концептуальные и не очень простые. В результате чтобы написать вывод в какой-то вашей вероятностной модели, вам нужно потратить очень много времени, разобраться в этом. При этом для по-настоящему сложных моделей потребуются знания уровня PhD в машинном обучении, причем не просто в машинном обучении, а лучше специализированно в Байесовском выводе. Тогда у вас есть шансы правильно и без ошибок написать код вывода в этой модели, который действительно будет делать то, что вы думаете.

Соответственно, это все сильно осложняет экспериментирование. Например, потребовать одну модель займет несколько недель, и неизвестно, получится или нет. Что уж говорить о том, чтобы попробовать несколько альтернативных моделей – мало у кого хватит терпения и сил на такое. Наблюдается такая тенденция, что мало кто готов в своих задачах пробовать сложные модели, потому что, как правило, чем сложнее модель, тем сложнее код вывода, и мало кто может его написать. Поэтому все ограничиваются простыми моделями. Однако очень часто, более сложные модели имеют большую предсказательную мощь, и потенциально от их использования можно получить очень много профита.

Ученые думали, как бороться с этой проблемой, и в качестве одного из решений была предложена такая парадигма, как вероятностное программирование. Эта парадигма основана на двух идеях. Первая – вы не должны писать код вероятностного вывода сами. Это сложно, и лучше все это оставить экспертам. Все, что вы должны делать, это задавать вероятностные модели, говорить, что вы пронаблюдали, апостериорные распределения на какие переменные в вашей модели вас интересуют. Далее черная коробка, движок вероятностного вывода, должен сделать вывод за вас. Вторая идея - мы хотим, чтобы всей мощью вероятностного моделирования могли пользоваться как можно больше людей. Нужен такой способ формально описывать вероятностные модели, чтобы, с одной стороны, его мог понять движок вероятностного вывода, а с другой стороны, достаточно большое количество людей могло им пользоваться. Какие вообще формальные языки известны подавляющему большинству людей, которые возможно будут этим заниматься? Это языки программирования.

Идея вероятностного программирования заключается в следующем. Вы описываете вероятностную модель с помощью программы, которая фактически реализует вашу модель физического процесса. То есть это программа, которая, условно говоря, принимает на вход или генерирует в самом начале свойства интересующих вас объектов, а потом при помощи некоторого алгоритма генерирует наблюдаемые данные, периодически обращаясь к датчику случайных чисел, чтобы добавить туда немного шума. Такая программа задает вероятностную модель, задает совместное распределение, движок может проанализировать программу и понять, как делать вывод. Звучит, наверное, немного утопично, но в настоящее время действительно существует ряд движков, которые возможно работают не всегда и не для всех моделей, но позволяют делать вывод в интересных моделях среднего и высокого уровня сложности. Несколько перечислено на этом слайде.

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

Модель очень простая: мы подкидываем две монетки, смотрим, упали ли они орлом или решкой, затем смотрим, упали ли они вместе орлом или решкой, или нет.

Как задать такую модель с помощью программы? Во-первых, мы заведем по логической переменной на каждую монетку. Эти переменные будут принимать значение «истина», если монета упала орлом, и «ложь», если монета упала решкой. И заведем еще одну переменную, которая будет принимать значение «истина», только когда обе монетки упали орлом.

Функция, которая здесь называется Bernoulli Sample, это просто обращение к датчику случайных чисел, который генерирует sample из распределения Bernoulli с заданным параметром 0,5. То есть эта функция с вероятностью 0,5 вернет значение «истина» и с вероятностью 0,5 вернет значение «ложь». Ее вызов эквивалентен подбрасыванию монетки.

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

Вернемся к программе. Мы можем задать какой-то запрос к этой модели. Например, мы можем сказать, что мы пронаблюдали, что значение переменной bothHeads ложно, то есть обе монетки орлом не выпали. Теперь мы можем спросить, какая вероятность, что первая монетка выпала орлом? Какой ответ должен дать движок вероятностного программирования в данном случае? В нашем процессе есть четыре конфигурации орла и решки. Мы знаем, что конфигурация орел-орел исключена, и в оставшихся трех конфигурациях только в одной из них первая монетка выпадает орлом, true. Правильный ответ, который бы должен был дать любой движок вероятностного программирования в данном случае, это что апостериорное распределение на coin1Heads - это распределение Бернулли со значением параметра 1/3. С вероятностью 1/3 - орел, 2/3 - решка. Очень простая вероятностная программа.

Какая семантика у этих программ, как о них можно думать? Можно представить себе, что мы берем эту программу и запускаем ее бесконечное количество раз, каждый раз обращаясь к датчику случайных чисел, который генерирует числа из какого-то распределения, и у нас есть еще два оператора: infer – позволяет задавать вопросы, observe – говорит, что нам известно. Идея в том, что когда мы бесконечно запускаем эту программу, каждый раз, когда мы доходим до оператора infer, он каждый раз сохраняет куда-то себе текущее значение переменной и, условно говоря, запоминает все значения переменной, чтобы, когда вы его попросите, вернуть распределение на все значения. А оператор observe смотрит, равно ли в текущем исполнении программы значение переменной тому, чему оно должно быть равно. И если оно не равно, то он прерывает текущее исполнение программы и говорит всем операторам infer, что не надо было ничего в этом раунде запоминать, выбросьте все это, это плохое исполнение, не соответствует известным нам ограничениям.

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

Давайте рассмотрим еще не очень практичный, но уже более сложный пример вероятностной программы. А именно такую простую модель, как линейная регрессия. Посмотрим, как ее можно было на языке вероятностного программирования записать. Что такое линейная регрессия? У нас есть какое-то наблюдаемое значение, и мы считаем, что оно зависит от каких-то характеристик примерно линейно. Характеристики (они же features): допустим, у нас есть какие-то точки, каждая из них обладает вектором признаков, которые влияют линейно на наблюдаемое значение. То есть мы считаем его скалярное произведение с некоторым вектором весов, добавляем немного шума, чтобы эта линейная зависимость была не точной, а примерной. И так получаем наблюдаемое значение. Как это записать на языке вероятностного программирования? Для начала объявим массив признаков, который здесь обозначен Х, – это двумерный массив, где по первому измерению идут объекты нашей выборки, а по второму – значения признаков. Этот массив всегда наблюдаемый, мы можем, например, всегда вызвать функцию GetFeatures, которая считает его из файла, как в обычном программировании.

Теперь нам нужен вектор весов, собственно параметры этой модели. Это случайная переменная и мы зададим на нее нормальное априорное распределение. Каким образом? Мы просто сгенерируем все значения весов из нормального распределения с матожиданием 0 и дисперсией 1, вызвав функцию, которая просто генерирует число из распределения с заданными параметрами. Так, по сути, задается априорное распределение – мы просто пишем код, который генерирует из него. Теперь заведем вектор Y для значения регрессии и заполним его. Его значения равны скалярному произведению вектора весов на вектор признаков плюс немного случайного шума. Строка Y[n] инициализирует значение Y случайным шумом с матожиданием 0 и дисперсией 0,1 и затем этот цикл добавляет к значению, которое мы проинициализировали, скалярное произведение вектора признаков соответствующего объекта с вектором весов.

Теперь мы можем сказать, что значения Y мы пронаблюдали, считав их из файла или еще как-то, и спросить у движка вероятностного программирования, какое же апостериорное распределение на W при известном Y.

Это была пара простых примеров, которые должны были проиллюстрировать концепцию. Теперь давайте перейдем к интересным примерам - что вообще можно делать легко с помощью вероятностного программирования?

Сперва рассмотрим модель True Skill. Эта модель нужна, если у вас есть некая игра двух игроков, в которой исход – либо выиграл первый, либо выиграл второй. Также в ней есть результаты множества игр между игроками, а вы теперь про игроков хотите что-то понять. Например, вы хотите понять уровень мастерства игроков или вы хотите посчитать вероятность того, что первый игрок победит второго, для двух игроков, которые никогда не играли вместе. Зачем вообще это считать? Очевидные применения: спортивная аналитика или игровые сервисы, например онлайн.

В частности модель True Skill была впервые придумана Microsoft для Xbox Live, потому что в Halo при автоматическом выборе кому с кем играть, надо было обеспечить, чтобы игроки были примерно одинакового уровня, иначе никому бы не было интересно играть. Кроме того, с помощью таких моделей игр можно моделировать совершенно неигровые ситуации. В частности, в веб-поиске похожие модели используются для определения релевантности документов.

Когда, например, у нас есть сигнал о том, что кто-то сказал, что из данной пары документов при каких-то условиях лучше вот этот, а из данной пары - лучше вот этот, мы всегда можем эти сигналы сегрегировать, учесть и понять, какой документ действительно был самым лучшим, какой был не очень и т.д. Поэтому такие модели важно знать и полезно изучать. Одна из моделей решения этой задачи – True Skill. Она очень простая, но при этом удивительно мощная.

Идея в следующем: давайте каждого игрока опишем одним числом, которое показывает его мастерство. Чем число выше, тем игрок играет лучше, и наоборот. Когда два игрока встречаются вместе и играют в игру, как мы определим, кто выиграл, а кто проиграл? Мы возьмем мастерство игроков, прибавим к нему немного шума, чтобы даже тогда, когда первый игрок заведомо сильнее второго, он с некоторой вероятностью все равно мог ему проиграть. Чтобы наша модель могла сгенерировать все возможные исходы, а не только строго транзитивные, потому что в реальных данных такого не будет.

Как можно эту идею реализовать на языке вероятностного программирования? Заведем четыре массива. В первый запишем навыки каждого игрока, а остальные три массива будут описывать игры, в которые сыграли все игроки. Пусть у нас игроки для простоты нумеруются от 0 до PlayerCount-1, а игры - от 0 до GameCount-1. Во второй массив мы запишем в соответствующей игре, кто играл первым игроком, какой идентификатор у первого игрока от 0 до PlayerCount-1. В третий массив запишем всё соответственно для второго игрока, а в четвертый запишем величину, которая истинна, если в соответствующей игре выиграл первый игрок, и ложна, если выиграл второй.

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

Теперь мы можем сгенерировать исходы игры. Для каждой игры мы берем игрока, который был первый в ней, игрока, который был вторым, и считаем, насколько навыки первого игрока больше, чем навыки второго. Получаем некое преимущество первого над вторым. Теперь мы это преимущество «зашумляем» путем прибавления к нему нормального шума с нулевым матожиданием и небольшой дисперсией - так, чтобы тот, кто сильнее, с большой вероятностью выиграл, но мог и проиграть. Первый игрок выигрывает в том случае, если это зашумленное преимущество будет больше нуля. В ином случае выигрывает второй игрок. Вероятностная модель задана, и теперь мы можем назвать, результаты игр и то, какие навыки были у игроков. И получить апостериорное распределение на навыки.

С этой моделью можно делать довольно интересные вещи. В частности Microsoft Research сделали модификацию, которая называется True Skill through time.

По сути, модель та же самая, но теперь навыкам игроков разрешили каждый год меняться. То есть, каждый год к навыку прибавляется какой-то нормальный шум с нулевым матожиданием и небольшой дисперсией - навык может «плыть» во времени. Затем взяли историю шахматных матчей за 20 век, натравили на нее этот алгоритм и сравнили всех шахматных гроссмейстеров 20-го века. Даже тех, кто никогда вместе не играл. Этот график из статьи.

Другая интересная модель – DARE. Какую задачу решает эта модель? Представьте, что у вас есть некий тест, в нем сколько-то вопросов, на каждый вопрос сколько-то вариантов ответов, и у вас есть много студентов, которые прошли этот тест, ответили на вопросы, однако правильные ответы на вопросы вам неизвестны. Тем не менее, вы хотите понять, кто из студентов ответил правильно, кто - нет, и какие вообще были правильные ответы.

Зачем такую задачу решать? Есть такая область, особенно релевантная задачам веб-поиска, как краудсорсинг. Есть такие платформы, куда приходят люди, которые хотят подзаработать, а Яндекс им дает простые задания. Например, сказать, релевантен ли документ запросу или нерелевантен, есть ли собака на изображении или нет, и так далее. Проблема в том, что далеко не все люди, которые приходят на эти платформы, могут отвечать хорошо на поставленные вопросы. Особенно если вопросы не настолько тривиальны, как наличие или отсутствие животного на изображении. К тому же, многие из них пытаются хитрить. Например, отвечать как попало. Бывает, что сначала отвечают нормально, а потом устают. В итоге мы имеем большое количество очень шумных ответов, многие из которых неправильные. А какие правильные, мы не знаем, иначе бы мы эти задания на разметку не отправляли. И вот здесь нужны такие модели, которые позволят среди этого большого количества не всегда правильных ответов однозначно или с некоторым допущением определить правильные. Условно говоря, для каждой фотографии сказать, что на основании наблюдаемых данных с вероятностью 0,7 здесь была кошка. Как решать такую задачу? В литературе довольно много предложено моделей: Whitehil, BCC, DARE.

Давайте скажем, что каждый студент описывается одним числом – своими способностями. Чем выше у него способности, тем выше вероятность, что он на вопрос ответит правильно и наоборот. Каждый вопрос описывается двумя числами. Во-первых, сложностью – чем она больше, тем при прочих равных с меньшей вероятностью студент правильно ответит на вопрос. Во-вторых, discrimination – различительной способностью. Если discrimination у вопроса высокий, то тогда на ответ на вопрос навыки студента влияют, а если низкий, то не влияют. То есть можно представить себе вопрос, который был плохо сформулирован, и было непонятно, что там спрашивалось. И хороший студент или плохой – в итоге придется прибегнуть практически к случайному гаданию. Параметр discrimination призван такие ситуации моделировать.

Какая вероятностная программа будет соответствовать этой модели? Для начала нам нужно все параметры модели сгенерировать из соответствующих априорных распределений. Давайте скажем, что способности каждого студента мы генерируем из нормального распределения с центром в нуле и какой-то дисперсией. То есть мы говорим, что у всех примерно одинаковые способности, и экстремальные отклонения в сторону очень хороших и очень плохих от средних способностей маловероятны. Для каждого вопроса мы генерируем его сложность из такого же распределения - сложность и способность расположены как бы примерно на одной шкале. А различающую способность мы генерируем из гамма-распределения, в том числе по неким техническим причинам. Гамма-распределение – это распределение на положительной оси. То есть discrimination принимает значения больше нуля. При этом имеет среднее 1 и небольшую дисперсию. Истинные ответы мы не знаем, поэтому сгенерируем их, условно говоря, подкидывая кубик, у которого столько граней, сколько возможных вариантов ответа на вопрос.

Теперь нам нужно описать сам процесс ответа студентами на вопрос. Для каждого студента и для каждого вопроса мы сперва, как в модели True Skill, считаем преимущества студента над вопросом - то есть насколько способности студента превышают сложность вопроса. Теперь мы зашумляем это преимущество следующим образом: чем больше различающая способность вопроса discrimination, тем меньше мы зашумим преимущество, тем больше соотношение способностей и сложности действительно будет влиять на ситуацию. Для этого мы пользуемся функцией, которая генерирует из распределения с заданным матожиданием и точностью. Точность – это величина обратно пропорциональная дисперсии. Соответственно, чем больше discrimination, тем меньше дисперсия и наоборот.

Один из примеров из статьи про DARE. Взяли ответы студентов на IQ-тест и запустили модель DARE, которая правильных ответов не знает, но пытается их угадать и посчитать IQ каждого студента. И посчитали честный IQ. Если бы модель работала идеально, то все точки были бы расположены на черной прямой. По осям: по вертикали - IQ, который предсказала модель, по горизонтали – реальный IQ. Но, как видите, они расположены вдоль прямой, то есть модель довольно неплохо понимает, что происходит, хотя ничего о происходящем не знает.

Почему DARE работает? Если посмотреть на то, как работает вероятностный вывод в этой модели и постараться построить какую-то интуицию, то выяснится, что вывод работает примерно следующим образом. Мы сначала смотрим на то, что если много студентов ответили на какой-то вопрос одинаково, то, наверно, они все отвечали на него правильно и этот вопрос легкий. Если студент, который ответил на много вопросов так же, как на эти вопросы ответили остальные, то, наверно, этот студент знает, что делает, у него высокие способности. Тогда и на остальные ответы этого студента, где мы не так уже уверены, нужно смотреть более пристально – скорее всего, он там прав. Соответственно, если плохие и хорошие студенты отвечают одинаково на какой-то вопрос, то, наверно, у него довольно низкий discrimination. И так мы итеративно повторяем эти рассуждения много раз, пока наши апостериорные распределения не перестанут меняться с помощью этой логики.

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

Затем у нас есть такая задача, как обучение формулы ранжирования. То есть обучение тех самых моделей, которые из двух миллионов результатов, найденных по вашему запросу, должны понять, какие же десять нужно показать. Для того чтобы обучать такие модели, нужно очень много данных о том, какой документ релевантен какому запросу, а какой - не релевантен (обучающая выборка). ?

У нас есть специальные люди – асессоры. Их задача целый день сидеть и на заданные пары документ-запрос по сложной инструкции отвечать, релевантен ли документ запросу или нет. Но труд асессоров стоит денег, в больших количествах их нанять тяжело, так как это будет довольно дорого. А если пользоваться краудсорсинговыми платформами для таких сложных задач, то выясняется, что задачи оказываются неподъемно сложными для людей, которые туда приходят. Они просто не справляются с такими инструкциями, и получается что-то очень похожее на случай случайного угадывания. Тут возникает другая идея: если у нас есть клики пользователей по тому, что мы им уже показали, то, может, мы из этих кликов сами сможем восстановить, какие документы были релевантны, а какие - нет, и на этом уже обучать наши формулы ранжирования?

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

Идея такая. Пользователь смотрит выдачу сверху вниз. Вот он взял какой-то документ, и теперь ему нужно решить – кликнуть на этот документ или нет. Его решение зависит от того, насколько привлекательно краткое описание документа, которое есть на странице поисковых результатов. Даже если документ релевантен, а описание не очень, то пользователь никогда не узнает, что документ релевантен и туда не пойдет. В этой модели пользователь первым делом смотрит на описание и, если ему оно не понравилось, он с некоторой вероятностью решает посмотреть ещё документы и перейти на следующие, а с некоторой вероятностью получится так, что он устанет, разочаруется в нашем поиске и уйдёт в Google.

Если пользователь решает кликнуть на документ, то в этой модели он идет, смотрит на веб-страницу и решает, релевантен документ или нет. Эта переменная говорит, был ли релевантен документ. Если документ был релевантен, то пользователь в некоторой степени удовлетворяется этим документ и говорит, что он закончил и удовлетворил свою информационную потребность, а с некоторой вероятностью снова идет по этому циклу и смотрит следующий документ. И если следующий документ был нерелевантен, то опять же он может решить посмотреть еще или закончить. Такие модели в какой-то модификации используются. Здесь кода было бы слишком много, чтобы показывать программы, но принцип ясен, как такое можно было бы написать.

Как мы это применяем. Если есть клики, то можно понять релевантность и привлекательность сниппетов. Еще интересный пример. Допустим, у нас есть запрос, и мы по нему показываем какой-то набор документов. Да и вообще нам хорошо бы иметь какую-то метрику, которая говорит, хороший у нас поиск или плохой – какое-то число, за которым мы можем следить в течение времени и пытаться его всячески наращивать. Очень удобно иметь такое число. Как его построить? Можно в качестве метрики использовать вероятность того, что среднестатистический пользователь удовлетворился выдачей, и чем эта вероятность больше, тем всё работает лучше. Как рассчитать значение этой вероятности? Можно воспользоваться той же моделью – можно рассчитать ее параметры (вероятность устать, вероятность уйти в Google и т.д.) или на кликах, или, еще лучше, на реальной асессоровской разметке, чтобы значения параметров получились лучше, а дисперсия - меньше. И затем использовать это как формулу расчета какой-то целевой метрики, за которой мы в поиске следим. Это еще один способ применения этой модели.

Вероятностное моделирование – это зачастую довольно сложно, потому что нужно уметь писать код вероятностного вывода. В этом интересно разбираться, но постоянно этим заниматься не хочется. А вероятностное программирование – это, с некоторыми оговорками, сравнительно просто. Для того чтобы задавать вероятностные модели с помощью вероятностных программ, вам нужно знать основные принципы моделирования и уметь писать хоть какие-то программы. И движки вывода, которые существуют уже сейчас, позволяют работать в рамках этой парадигмы. Да, они иногда ломаются, иногда делают не то, что нужно, но в целом много интересных вещей с ними можно делать уже сейчас.

***
Коллоквиум факультета компьютерных наук ВШЭ – специальные семинары, на которые могут приходить не только студенты или сотрудники ФКН, но и все желающие. Их проводят учёные не только из Вышки, но и из других вузов и научных центров - МГУ, МФТИ, Математического института и института системного анализа РАН, MIT, Microsoft Research, Школы анализа данных Яндекса. Найти все прошедшие коллоквиумы можно

Уже более полутора лет назад прошла новость о том, что «DARPA намерено совершить революцию в машинном обучении» . Конечно, DARPA всего лишь выделила деньги на исследовательскую программу, связанную с вероятностным программированием. Само же вероятностное программирование существует и развивается без DARPA достаточно давно, причем исследования ведутся, как в ведущих университетах, таких как MIT, так и в крупных корпорациях, таких как Microsoft. И вовсе не зря DARPA, Microsoft, MIT и т.д. обращают пристальное внимание на эту область, ведь она по-настоящему перспективна для машинного обучения, а, может, и для искусственного интеллекта в целом. Говорят, что вероятностное программирование для машинного обучения будет играть ту же роль, что и высокоуровневые языки для обычного программирования. Мы бы привели другую параллель – с ролью Пролога, которую он сыграл для старого доброго ИИ. Вот только в Рунете по данной теме до сих пор можно найти лишь единичные ссылки, и то в основном содержащие лишь описания общих принципов. Возможно, это связано с тем, что потенциал вероятностного программирования еще только начал раскрываться и оно не стало основным трендом. Однако на что же способны или будут способны вероятностные языки?

Можно выделить два основных класса вероятностных языков программирования – это языки, допускающие задание генеративных моделей только в форме Байесовских сетей (или других графических вероятностных моделей), или Тьюринг-полные языки.

Типичным представителем первых является Infer.NET, разрабатываемый в Microsoft. В нем благодаря использованию в качестве генеративных моделей Байесовких сетей оказывается возможным применять известные для них эффективные методы вывода. Естественно, использование хорошо известного класса моделей с известными методами вывода не приводит к возможности решения каких-то принципиально новых задач (и даже такие генеративные модели, как сети глубокого обучения на основе ограниченных машинах Больцмана оказываются не представимы в таких языках), но дает вполне практичный инструмент. Как говорят разработчики, с использованием этого инструмента можно за пару часов реализовать нетривиальную вероятностную модель, такую как полная Байесовская версия анализа главных компонент, которая будет занимать всего пару десятков строк кода и для которой отдельная реализация эффективной процедуры вывода на обычном языке потребовала бы заметно большего объема знаний и нескольких недель работы. Таким образом, за счет вероятностного программирования использование графических моделей становится гораздо более простым и доступным.

Гораздо большим потенциалом, однако, обладают Тьюринг-полные вероятностные языки. Они позволяют выйти за рамки того класса задач, которые существующие методы машинного обучения уже умеют решать. Естественно, в таких языках возникает проблема эффективности вывода, которая пока далека от решения, что приводит к плохой масштабируемости на задачи реального мира. Однако это направление активно развивается, и существует ряд работ, показывающих как в вероятностных языках общего назначения достичь эффективного вывода для интересных практических задач. Можно надеяться, что в ближайшем будущем эти решения станут доступными для использования в конкретных языках. Кроме того, Тьюринг-полные вероятностные языки уже сейчас оказываются весьма полезными в исследованиях, связанных с когнитивным моделированием и общим искусственным интеллектом. По этим причинам мы и рассмотрим основные принципы вероятностного программирования именно на примере Тьюринг-полных языков, из которых мы выбрали Чёрч (Church), являющийся расширением языка Лисп (конкретнее, его диалекта – Scheme). Удобство этого языка (по крайней мере, в целях начального знакомства с ним) заключается в существовании для него web-реализации (web-church), с которой можно экспериментировать без установки дополнительного программного обеспечения.

Итак, к делу

Программа на вероятностном языке может, на первый взгляд, ничем не отличаться от программы на обычном языке. Именно так сделано в Чёрче. Как и в обычном Лиспе, в этом языке могут быть определены переменные, функции, выполнены детерминированные вычисления. Например, следующая программа задает функцию от одного аргумента, вычисляющую факториал по рекурсивной формуле n!=n*(n–1)!, и вызывает эту функцию для n=10

(define (f n) (if (= n 0 ) 1 (* n (f (– n 1 ) ) ) ) ) (f 10 )

Также в этом языке могут быть обращения к (псевдо)случайным функциям. Например, при выполнении вызова (flip 0.3) с вероятностью 0.3 будет возвращено значение #t, а с вероятностью 0.7 – #f. Такая функция элементарно реализуется и в Лиспе как

(define (flip p) (< (random ) p) )

Чёрч, как и другие вероятностные языки, включает много встроенных функций, которые возвращают случайные значения в соответствии с тем или иным распределением. Например, (gaussian x0 s) возвращает вещественную случайную величину, распределенную по гауссиане с заданными параметрами. В качестве других реализованных распределений вероятностей обычно присутствуют равномерное, мультиномиальное, Дирихле, бета, гамма. Все эти распределения не так сложно реализовать вручную в обычном языке, и здесь пока нет принципиального отличия между Чёрчем и Лиспом.

Однако помимо обычной семантики программа на Чёрче обладает вероятностной семантикой, в рамках которой полагается, что программа, содержащая вызовы случайных функций, не просто при своем запуске порождает какие-то конкретные значения случайных величин, но задает распределение вероятностей над ними. Так, (gaussian x0 s) – это не просто функция, возвращающая некоторое конкретное значение случайной величины, распределенной по гауссиане, но именно само Гауссово распределение.

Но как получать эти распределения вероятностей, задаваемые программой? Представим, например, программу

(if (flip 0.4 ) (flip 0.1 ) (flip 0.6 ) )

То есть с вероятностью 0.4 значение этого выражения – это P(#t)=0.1 и P(#f)=0.9, а с вероятностью 0.6 – P(#t)=0.6 и P(#f)=0.4. Откуда возьмется итоговое распределение, задаваемое этим выражением: P(#t)=0.4 и P(#f)=0.6? Эта вероятностная семантика зачастую реализуется через процесс сэмплирования: мы можем просто много раз запустить программу и построить выборку результатов ее выполнения. Такую процедуру, конечно, также несложно реализовать на обычном языке (и, действительно, еще Симула-67 таким способом регулярно использовалась для моделирования стохастических процессов).

Однако современные вероятностные языки идут дальше и добавляют в процесс сэмплирования условие, накладываемое на результаты выполнения программы. Эта идея ведет к простейшему сэмплированию с отказами, которая в Чёрче реализуется функцией rejection-query. Эта функция на вход принимает вероятностную программу (как совокупность define), предпоследнее выражение в которой вычисляет возвращаемое значение, а последнее выражение – это условие (предикат), который в процессе выполнения должен оказаться истинным. Рассмотрим программу

(rejection-query (define A (flip 0.4 ) ) (define B (flip 0.6 ) ) B (or A B) )

rejection-query выполняет поданную ей программу до тех пор, пока не будет выполнено последнее условие – здесь (or A B) – и возвращает (один раз) значение предпоследнего выражения – здесь B. Чтобы получить выборку значений, можно воспользоваться функцией repeat. Также Чёрч имеет встроенные функции для построения гистограмм. Рассмотрим немного расширенную программу:

(define (get-sample ) (rejection-query (define A (flip 0.4 ) ) (define B (flip 0.6 ) ) B (or A B) ) ) (hist (repeat 1000 get-sample) )

При запуске мы получим следующий результат: #f - 21%, #t - 79% (цифры от запуска к запуску могут немного меняться). Этот результат означает, что значение B равно #t с вероятностью чуть меньше 0.8. Откуда взялась эта вероятность, если в программе B – это бинарная случайная величина, для которой P(#t)=0.6? Очевидно, дело в наложении условия: (or A B). В процессе сэмплирования мы принимаем только такие значения B, что верно или A, или само B. Фактически, мы считаем апостериорную вероятность P(B|A+B). Можно было бы воспользоваться правилом Байеса для того, чтобы вычислить эту вероятность вручную:

P(B|A+B) = P(A+B|B)P(B)/P(A+B) = =(P(A|B)+P(B|B)–P(A|B)P(B|B))P(B)/(P(A)+P(B)–P(A)P(B))= =(P(A)+1–P(A))P(B)/(P(A)+P(B)–P(A)P(B))=0.6/(0.4+0.6–0.4*0.6)=0.789.

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

Итак, сэмплирование позволяет нам вычислять апостериорные вероятности интересующих нас случайных величин при наложении тех или иных условий. Оно заменяет правило Байеса, широко используемое в машинном обучении для выбора моделей или выполнения предсказаний. При этом запись программы на вероятностном языке для многих людей может оказаться гораздо понятнее, чем применение правила Байеса. Конечно, само режекторное сэмплирование весьма просто можно реализовать на обычном языке программирования, но вероятностные языки этим не ограничиваются.
В Чёрче, в частности, реализована другая функция для сэмплирования – enumeration-query. Запустим программу

(enumeration-query (define A (flip 0.4 ) ) (define B (flip 0.6 ) ) B (or A B) )

На выходе мы получим: ((#t #f) (0.7894736842105263 0.2105263157894737)). Здесь выведены точные значения (конечно, со скидкой на конечную разрядную сетку) вероятностей P(B|A+B). enumeration-query уже не просто запускает много раз программу, но анализирует пути ее выполнения и перебирает все возможные значения случайных переменных с учетом их вероятностей. Конечно, такое «сэмплирование» будет работать, только когда множество возможных комбинаций значений случайных переменных не слишком велико.

Есть в Чёрче и более продвинутая замена режекторному сэмплированию на основе MCMC (Monte Carlo Markov Chains), а именно Metropolis Hastings алгоритм, откуда и название у процедуры – mh-query. Эта процедура запроса сразу формирует заданное число сэмплов (а также получает на вход один дополнительный параметр – лаг). Эта процедура также нетривиальна в реализации, так что использование готового вероятностного языка (а не собственная реализация простых процедур сэмплирования на обычном языке) приобретает смысл.

Однако главное, что дает вероятностное программирование, – это стиль мышления.

От азов к применению

Разные разработчики находят разные применения вероятностному программированию. Многие применяют его непосредственно для решения задач машинного обучения. Авторы же языка Чёрч, Noah D. Goodman and Joshua B. Tenenbaum, в своей web-книге «Probabilistic Models of Cognition» показывают применение вероятностного программирования для когнитивного моделирования. Также известно, как решение задач планирования удобно представлять в терминах вывода в вероятностных языках. Оно также оказывается применимым для представления знаний и вывода над ними, а также для задач машинного восприятия (в том числе, распознавания изображений). Все эти приложения пока более или менее разрозненные, но наличие общего фреймворка для всех них свидетельствует о том, что вероятностное программирование может стать «теорией великого объединения» для ИИ. Посмотрим на простейшие примеры возможного использования.

Одним из наиболее классических примеров применения экспертных систем является медицинская диагностика. В частности, система MYCIN была построена на системе правил вида:

  1. THE SITE OF THE CULTURE IS BLOOD
  2. THE GRAM OF THE ORGANISM I S NEG
  3. THE MORPHOLOGY OF THE ORGANISM IS ROD
  4. THE BURN OF THE PATIENT IS SERIOUS

Then there is weakly suggestive evidence (0.4) that

  1. THE IDENTITY OF THE ORGANISM IS PSEUDOMONAS

Очевидно, правила такого вида хорошо описываются на языке типа Чёрч. При этом нет необходимости еще и реализовывать процедуру вывода – достаточно просто записать систему правил. Приведем пример из упомянутой книги «Probabilistic Models of Cognition»:

(define samples (mh-query 1000 100 (define lung-cancer (flip 0.01 ) ) (define TB (flip 0.005 ) ) (define cold (flip 0.2 ) ) (define stomach-flu (flip 0.1 ) ) (define other (flip 0.1 ) ) (define cough (or (and cold (flip 0.5 ) ) (and lung-cancer (flip 0.3 ) ) (and TB (flip 0.7 ) ) (and other (flip 0.01 ) ) ) ) (define fever (or (and cold (flip 0.3 ) ) (and stomach-flu (flip 0.5 ) ) (and TB (flip 0.2 ) ) (and other (flip 0.01 ) ) ) ) (define chest-pain (or (and lung-cancer (flip 0.4 ) ) (and TB (flip 0.5 ) ) (and other( flip 0.01 ) ) ) ) (define shortness-of-breath (or (and lung-cancer (flip 0.4 ) ) (and TB (flip 0.5 ) ) (and other (flip 0.01 ) ) ) ) (list lung-cancer TB) (and cough fever chest-pain shortness-of-breath) ) ) (hist samples "Joint inferences for lung cancer and TB" )

В этой программе определяются априорные вероятности появления у больного рака легких, туберкулеза, простуды и т.д. Далее определяются вероятности наблюдения кашля, жара, боли в груди и стесненного дыхания при тех или иных заболеваниях. Возвращаемая величина – это пара булевых значений, есть ли у пациента рак и/или туберкулез. И, наконец, условие – это совокупность наблюдаемых симптомов (то есть сэмплирование производится при условии, что значение всех переменных – cough fever chest-pain shortness-of-breath – #t).

Результат выполнения программы будет иметь следующий вид: (#f #f) - 4%, (#f #t) - 58%, (#t #f) - 37%, (#t #t) - 1%.
Несложно сделать, чтобы samples была функцией, в которую подается перечень симптомов, который далее в mh-query используется для сэмплирования, что даст возможность ставить диагнозы разным пациентам. Конечно, этот пример сильно упрощенный, но видно, что в стиле вероятностного программирования вполне можно представлять знания и делать вывод над ними.

Естественно, можно решать и задачи машинного обучения. Их отличие будет лишь в том, что неизвестными величинами будут параметры самой модели, а в качестве условия для сэмплирования будет выступать генерация этой моделью обучающей выборки. К примеру, в представленной выше программе мы бы числа в строках вида (define lung-cancer (flip 0.01)) могли бы заменить на переменные, которые сами бы задавались как случайные, например (define p-lung-cancer (uniform 0 1)), а далее для каждого пациента из обучающей выборки значение lung-cancer уже определялось бы с вероятностью p-lung-cancer.

Рассмотрим эту возможность на простом примере оценивания параметров многочлена по набору точек. В следующей программе функция calc-poly вычисляет значение многочлена с параметрами ws в точке x. Функция generate применяет calc-poly к каждому значению из заданного списка xs и возвращает список соответствующих ординат. Процедура noisy-equals? «приближенно» сравнивает два заданных значения (если эти значения равны, то функция возвращает #t с вероятностью 1; если же они не равны, то чем больше они отличаются, тем с меньшей вероятностью она вернет #t).

(define (calc-poly x ws) (if (null ? ws) 0 (+ (car ws) (* x (calc-poly x (cdr ws) ) ) ) ) ) (define (generate xs ws) (map (lambda (x ) (calc-poly x ws) ) xs) ) (define (noisy-equals ? x y) (flip (exp (* -3 (expt (- x y) 2 ) ) ) ) ) (define (samples xs ys) (mh-query 1 100 (define n-coef 4 ) (define ws (repeat n-coef (lambda () (gaussian 0 3 ) ) ) ) ws (all (map noisy-equals? (generate xs ws) ys) ) ) ) (samples "(0 1 2 3 4 ) "()

Внутри вызова mh-query параметр n-coef определяет число коэффициентов в многочлене (то есть его степень плюс один); ws – это список, состоящий из случайных величин, сгенерированных в соответствии с нормальным распределением. Возвращаемое значение – список параметров многочлена. Условие для сэмплирования – «приближенное» равенство всех заданных значений ys всем ординатам, порожденным многочленом при данных ws. Здесь мы запрашиваем всего одну реализацию, которая проходит по условию (поскольку строить гистограмму для вектора параметров не очень удобно). Результатом этого запроса может быть, к примеру, список (2.69 1.36 0.53 -0.10), задающий многочлен 2.69+1.36x+0.53x^2–0.10x^3.

Вообще, вывод на моделях с вещественными параметрами – не самая сильная сторона языка Чёрч (но не стоит это считать глобальным недостатком вероятностного программирования вообще). Тем не менее, на этом примере mh-query кое-как работает. Чтобы в этом убедиться, вместо определения значений параметров в запросе можно попросить возвращать предсказание в некоторой точке. Перепишем последний фрагмент кода так:

(define (samples xs ys) (mh-query 100 100 (define n-coef 4 ) (define ws (repeat n-coef (lambda () (gaussian 0 3 ) ) ) ) (calc-poly 5 ws) (all (map noisy-equals? (generate xs ws) ys) ) ) ) (hist (samples "(0 1 2 3 4 ) "(0.01 1.95 6.03 12.01 20.00 ) ) )

То есть мы запрашиваем наиболее вероятное (при имеющихся данных) значение в точке x=5. При разных запусках максимум гистограммы, к сожалению, будет приходиться на несколько различающиеся значения (метод MCMC, теоретически, гарантирует схождение к истинному распределению, но лишь в пределе), но обычно эти значения будут достаточно вразумительными. Стоит заметить, что здесь мы «бесплатно» (заменой одной строчки) получили полное байесовское предсказание: вместо выбора лучшей модели и предсказания лишь по ней, мы получили апостериорное распределение значений в точке x=5, усредненное сразу по множеству моделей с учетом их собственных вероятностей.
Но и это еще не все. Опять же, заменой одной строчки – (define n-coef 4) -> (define n-coef (random-integer 5)) мы можем сделать автоматический выбор между моделями с разным числом параметров. Причем сэмплирование величины n-coef показывает (хотя и не очень стабильно), что наиболее вероятным значением является n-coef=3 (то есть парабола, которая и заложена в заданный набор точек). При такой модификации более стабильным становится и предсказание. Иными словами, не возникает и эффекта переобучения! Почему же не выбираются многочлены более высокой степени, ведь они могут точнее проходить к заданным точкам? Дело в том, что при сэмплировании «угадать» подходящие значения параметров многочлена меньшей степени проще, чем многочлена более высокой степени, поэтому вероятность породить такие параметры, которые пройдут проверку, для многочлена второй степени выше, чем для третьей. В то же время, многочлен первой степени будет давать большие отклонения, для которых вероятность срабатывания noisy-equals? будет сильно понижаться.

Посмотрим еще на одно применение, которое в рамках вероятностного программирования может показаться неожиданным. Это решение «дедуктивных» задач. Возьмем приведенную в начале функцию вычисления факториала, но вместо вызова ее с фиксированным значением будем считать, что аргумент – случайная переменная, но на само значение факториала наложено ограничение:

(define (f n) (if (= n 0 ) 1 (* n (f (- n 1 ) ) ) ) ) (enumeration-query (define n (random-integer 20 ) ) n (equal ? (f n) 120 ) )

В качестве ответа мы увидим n=5 с вероятностью 1. Если же мы вместо 120 зададим 100, то программа не зациклится (в отличие от случая использования rejection-query или mh-query, что можно считать их недостатком), а просто вернет пустое множество. Можно поставить в качестве условия и не строгое равенство, а какое-то другое ограничение.

Таким же образом можно решать и более сложные задачи. Допустим, мы хотим решить задачу о сумме подмножеств: в ней надо из заданного множества чисел найти такое подмножество, сумма в котором равна заданному числу (обычно в качестве этого числа берется 0 и требуется, чтобы подмножество было не пустым; но чтобы избавиться от проверки на нетривиальность решения, мы возьмем ненулевую сумму). Казалось бы, причем тут вероятностное программирование? Но случайные величины – это просто неизвестные величины (для которых заданы априорные вероятности). В любых задачах нам нужно найти что-то неизвестное, в том числе и в задаче о сумме подмножеств. Посмотрим на следующую элементарную программу (ее можно было бы даже еще упростить, записав summ через fold).

(define (solution xs v) (rejection-query (define ws (repeat (length xs) flip) ) (define (summ xs ws) (if (null ? xs) 0 (+ (if (car ws) (car xs) 0 ) (summ (cdr xs) (cdr ws) ) ) ) ) ws (equal ? (summ xs ws) v) ) ) (solution "(-1 3 7 5 -9 -1 ) 1 )

Здесь ws – список случайных булевых значений. Процедура summ вычисляет сумму элементов списка xs, для которых соответствующие элементы списка ws истинны. Далее запрашивается значения ws, для которых выполняется условие равенства полученной суммы заданному числу v. Запустив эту программу, можно получить такой результат: (#f #t #t #f #t #f), который, конечно, является правильным (так как 3+7-9=1).

Естественно, Чёрч не делает чуда и при повышении размерности этой задачи он с ней не справится. Однако не может не удивлять то, что столь разные задачи ИИ могут быть хотя бы поставлены (и отчасти решены) с использованием одного и того же языка. Ну, а проблема эффективного вывода как была, так и остается. В вероятностных языках она хотя бы выделяется в чистом виде.

Сегодня мы публикуем внеочередной перевод — это будет обзорная статья блистательного Ноэля Уэлша о принципах вероятностного программирования. Статья публикуется по заявкам читателей, которые задают нашему блогу все более высокую планку — и это, безусловно, здорово!

Надеюсь, этих кратких примеров достаточно, и вы ухватили идею вероятностных моделей. Можно представить себе и множество других генеративных процессов - например, свойства небесного тела влияют на то, в каком виде мы его наблюдаем, поведение пользователя в онлайне зависит от его интересов, а деятельность НКО влияет на ситуацию со здравоохранением в том регионе, где действует такая организация.

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

Когда мы создали вероятностную модель (и вдоволь поприкалывались с липовыми данными), эту модель можно использовать для логического вывода. Логический вывод - это процесс «обратной перемотки модели». Мы смотрим на реальные данные, которые удалось наблюдать, и пытаемся догадаться, как могла бы выглядеть ситуация, недоступная для наблюдения. Например, можно воспользоваться текстом документа и попытаться выяснить, какой теме он посвящен, либо собрать астрономические данные и попытаться определить, какие звездные системы с наибольшей вероятностью пригодны для жизни. Поскольку мы не можем с уверенностью судить о состоянии этих ненаблюдаемых элементов нашей модели, можно построить вероятностное распределение всех возможностей. Создание такого распределения как раз и отличает байесовский вывод от альтернатив - например, от метода максимального правдоподобия, позволяющего выявить всего одно состояние ненаблюдаемых свойств.

Зачем нужно вероятностное программирование

Мы вкратце описали генеративные модели и проблему байесовского вывода. Эта область статистики и машинного обучения давно изучается, и со временем исследователи заметили ряд моментов:

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

Как было бы здорово, если бы можно было сделать алгоритм логического вывода, исходя лишь из описания модели. Ситуацию можно сравнить с программированием на ассемблере и на высокоуровневом языке. Пользуясь ассемблером, мы все оптимизируем вручную, реализуем собственные и управляющие структуры и т.д., в контексте конкретной проблемы, стоящей перед нами. То же самое происходит при создании специального алгоритма логического вывода для конкретной генеративной модели. При программировании на высокоуровневом языке мы пользуемся готовыми конструкциями, которые компилятор сам переводит на ассемблер. Продуктивность работы значительно повышается ценой небольшого снижения машинной производительности (чего мы зачастую даже не замечаем). Цель вероятностного программирования - точно такая.

Конечно же это монада!

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

Вернемся к примеру с генеративной моделью для создания документа. Чтобы создать документ, нужно:

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

Можно аннотировать эту модель типами, чтобы показать, как может решаться проблема. При помощи Distribution[A] представим распределение по типу A. Теперь документ генерируется так:

  • выбираем некоторые темы — так можем получить Distribution] (или просто Distribution для более простой модели).
  • из каждой темы получаем распределение по словам — это функция Topic => Distribution .
  • из этих распределений по словам выбираем конкретные слова, которые читаем на странице - извлечение делается из Distribution .

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

Основная часть этой модели заключается в соединении Distribution и Topic => Distribution для создания Distribution , из которого можно построить документ. Кстати, чем нужно заменить??? , чтобы следующее тождество соблюдалось?

Distribution ??? Topic => Distribution = Distribution

Ответ — flatMap , то есть, у нас есть монада. (Можете сами убедиться, что и законы монад тоже соблюдаются, однако, в данном случае нам нужно определить семантику flatMap . См. ниже.)

Если вы когда-либо работали со ScalaCheck или подобными системами - то, значит, пользовались монадой безопасности.

Построение алгоритмов логического вывода

Существует много способов реализовать монаду вероятности. Если мы имеем дело лишь с дискретными доменами, то можно все представить внутри предметной области как List[(A, Probability)] (где Probability может быть псевдонимом типа Double) и в точности вычислять результаты. В реальных приложениях польза от такого подхода невелика, поскольку нам, скорее всего, придется иметь дело со сплошными доменами, либо дискретными, но все равно крупными. Тогда размер представления в каждом flatMap будет расти экспоненциально. Пример реализации показан .

Можно использовать представление, основанное на выборке. Такой подход работает со сплошными доменами, и пользователь может создать столько образцов, сколько заблагорассудится - для достижения желаемой точности. Однако, такой вариант все равно неидеален. Алгоритмы логического вывода исследуются много лет, эта работа напоминает оптимизацию компиляторов, а нам бы хотелось создать такую систему, которая запускала бы вероятностные механизмы для анализа структуры программы и применяла к полученным результатам оптимальный алгоритм логического вывода. Точно как и при работе с компилятором, мы выражаем вероятностную программу в виде абстрактного синтаксического дерева, которым затем могут манипулировать алгоритмы оптимизации.

Теперь можно приступать к построению алгоритмов логического вывода, и именно здесь я самоустраняюсь от объяснений, по двум причинам: во-первых, полагаю, что читатель - не профи в машинном обучении, и обсуждать алгоритмы логического вывода ему будет не слишком интересно, а во-вторых, что, на самом деле, важнее - пока это передний край исследований. В современных системах реализуются обобщенные алгоритмы логического вывода, которые, однако, почти не приспособлены для проблемно-ориентированных оптимизаций. Общий принцип, как и при работе с компиляторами, таков: чем больше информации вы сохраняете, тем больше можете оптимизировать. В то же время, большинство оптимизаций полезны лишь в немногих программах. Во многом остается открытым вопрос, насколько можно полагаться на обобщенные алгоритмы логического вывода и на вычислительную мощность, чтобы такой вывод получался достаточно быстрым, и насколько стоит усердствовать со сложной математикой ради оптимизации конкретных случаев.

Заключение и дальнейшая работа

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

Кроме того, вероятностное программирование изрядно пересекается с моими интересами в сфере машинного обучения и языков программирования. Если вы заинтересовались - вот