Текст
                    Ави Пфеффер
Вероятн
остное
мирование
практике
MANNING
програм
на


Ави Пфеффер Вероятностное программирование на практике
Practical Probabilistic Programming AVIPFEFFER MANNING Shelter Island
Вероятностное программирование на практике АВИ ПФЕФФЕР ЛfrMkit lit*ll Москва, 2017
УДК 004.42 ББК 32.973 П91 Ави Пфеффер Вероятностное программирование на практике. / Пер. с англ. Слин- кин А. А. - М.: ДМК Пресс, 2017. - 462 с: ил. ISBN 978-5-97060-410-6 Книга представляет собой введение в вероятностное программирование для программистов-практиков. Описан вероятностный вывод, где алгоритмы помогают прогнозировать использование социальных сетей. Приведены примеры построения фильтра спама, диагностики ошибок в вычислительной системе, восстановления цифровых изображений. Представлен функциональный стиль программирования для анализа текстов, объектно-ориентированных моделей и моделей с открытой вселенной. Издание рассчитано на широкий круг читателей: специалистов по анализу данных и машинному обучению, программистов, студентов вузов и др. УДК 004.42 ББК 32.973 Original English language edition published by Manning Publications Co., Rights and Contracts Special Sales Department, 20 Baldwin Road, PO Box 261, Shelter Island, NY 11964. © 2016 by Manning Publications Co. All rights reserved. Russian-language edition copyright © 2016 by DMK Press. All rights reserved. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но, поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 978-1-61729-233-0 (англ.) ISBN 978-5-97060-410-6 (рус.) © 2016 by Manning Publications Co. © Оформление, перевод на русский язык, издание, ДМК Пресс, 2017
Памяти любимой матушки, Клэр Пфеффер. Да будет память её благословенна.
ОГЛАВЛЕНИЕ Предисловие 13 Вступление 15 Благодарности 17 Об этой книге 19 Структура книги 20 О коде и упражнениях 21 Об авторе 21 Автор в сети 22 Об иллюстрации на обложке 22 ЧАСТЬ I Введение в вероятностное программирование и систему Figaro 23 Глава 1. О вероятностном программировании в двух словах 24 1.1. Что такое вероятностное программирование? 24 1.1.1. Как мы высказываем субъективное суждение? 25 1.1.2. Системы вероятностных рассуждений помогают принимать решения 26 1.1.3. Система вероятностных рассуждений может рассуждать тремя способами 28 1.1.4. Система вероятностного программирования: система вероятностных рассуждений, выраженная на языке программирования 32 1.2. Зачем нужно вероятностное программирование? 37 1.2.1. Улучшенные вероятностные рассуждения 37 1.2.2. Улучшенные языки имитационного моделирования 38 1.3. Введение в Figaro, язык вероятностного программирования 40 1.3.1. Figaro и Java: построение простой системы вероятностного программирования 43 1.4. Резюме 48 1.5. Упражнения 48 Глава 2. Краткое руководство по языку Figaro 50 2.1. Введение в Figaro 50 2.2. Создание модели и выполнение алгоритма вывода на примере Hello World 52 2.2.1. Построение первой модели 53 2.2.2. Выполнение алгоритма вывода и получение ответа на запрос 54
2.2.3. Построение моделей и задание наблюдений 55 2.2.4. Анатомия построения модели 56 2.2.5. Повторяющиеся элементы: когда они совпадают, а когда различаются? 58 2.3. Базовые строительные блоки: атомарные элементы 59 2.3.1. Дискретные атомарные элементы 60 2.3.2. Непрерывные атомарные элементы 61 2.4. Комбинирование атомарных элементов с помощью составных 64 2.4.1. Элемент If 64 2.4.2. Элемент Dist 65 2.4.3. Составные версии атомарных элементов 66 2.5. Построение более сложных моделей с помощью Apply и Chain 67 2.5.1. Элемент Apply 67 2.5.2. Элемент Chain 70 2.6. Задание фактов с помощью условий и ограничений 73 2.6.1. Наблюдения 73 2.6.2. Условия 74 2.6.3. Ограничения 75 2.7. Резюме 78 2.8. Упражнения 78 Глава 3. Создание приложения вероятностного программирования 80 3.1. Общая картина 80 3.2. Выполнение кода 83 3.3. Архитектура приложения фильтра спама 86 3.3.1. Архитектура аналитического компонента 86 3.3.2. Архитектура компонента обучения 90 3.4. Проектирование модели почтового сообщения 92 3.4.1. Выбор элементов 93 3.4.2. Определение зависимостей 95 3.4.3. Определение функциональных форм 97 3.4.4. Использование числовых параметров 100 3.4.5. Работа с дополнительными знаниями 102 3.5. Разработка аналитического компонента 104 3.6. Разработка компонента обучения 108 3.7. Резюме 112 3.8. Упражнения 113 ЧАСТЬ II Написание вероятностных программ 115 Глава 4. Вероятностные модели и вероятностные программы 116 4.1. Определение вероятностной модели 117 Оглавление 7
4.1.1. Выражение общих знаний в виде распределения вероятности возможных миров 117 4.1.2. Подробно о распределении вероятности 120 4.2. Использование вероятностной модели для ответа на запросы 121 4.2.1. Применение условий для получения апостериорного распределения вероятности 122 4.2.2. Получение ответов на запросы 124 4.2.3. Применение вероятностного вывода 126 4.3. Составные части вероятностных моделей 127 4.3.1. Переменные 127 4.3.2. Зависимости 129 4.3.3. Функциональные формы 134 4.3.4. Числовые параметры 138 4.4. Порождающие процессы 140 4.5. Модели с непрерывными переменными 144 4.5.1. Бета-биномиальная модель 145 4.5.2. Представление непрерывных переменных 146 4.6. Резюме 150 4.7. Упражнения 150 Глава 5. Моделирование зависимостей с помощью байесовских и марковских сетей 152 5.1. Моделирование зависимостей 153 5.1.1. Направленные зависимости 153 5.1.2. Ненаправленные зависимости 159 5.1.3. Прямые и косвенные зависимости 162 5.2. Байесовские сети 163 5.2.1. Определение байесовской сети 163 5.2.2. Как байесовская сеть определяет распределение вероятности 166 5.2.3. Рассуждения с применением байесовской сети 166 5.3. Изучение примера байесовской сети 169 5.3.1. Проектирование модели диагностики компьютерной системы 169 5.3.2. Рассуждения с помощью модели диагностики компьютерной системы 174 5.4. Применение вероятностного программирования для обобщения байесовских сетей: предсказание успешности продукта 179 5.4.1. Проектирование модели для предсказания успешности продукта.... 180 5.4.2. Рассуждения с помощью модели для предсказания успешности продукта 185 5.5. Марковские сети 187 5.5.1. Определение марковской сети 187 5.5.2. Представление марковских сетей и рассуждения с их помощью 191 5.6. Резюме 195 5.7. Упражнения 195 8 Оглавление
Глава 6. Использование коллекций Scala и Figaro для построения моделей 198 6.1. Работа с коллекциями Scala 199 6.1.1. Моделирование зависимости многих переменных от одной 200 6.1.2. Создание иерархических моделей 203 6.1.3. Моделирование одновременной зависимости от двух переменных.... 205 6.2. Работа с коллекциями Figaro 208 6.2.1. Почему коллекции Figaro полезны? 208 6.2.2. Иерархическая модель и коллекции Figaro 210 6.2.3. Совместное использование коллекций Scala и Figaro 212 6.3. Моделирование ситуаций с неизвестным числом объектов 215 6.3.1. Открытая вселенная с неизвестным числом объектов 215 6.3.2. Массивы переменной длины 216 6.3.3. Операции над массивами переменной длины 217 6.3.4. Пример: прогнозирование продаж неизвестного числа новых продуктов 218 6.4. Работа с бесконечными процессами 220 6.4.1. Характеристика Process 220 6.4.2. Пример: моделирование состояния здоровья во времени 222 6.4.3. Использование процесса 224 6.5. Резюме 225 6.6. Упражнения 226 Глава 7. Объектно-ориентированное вероятностное моделирование 228 7.1. Объектно-ориентированные вероятностные модели 229 7.1.1. Элементы объектно-ориентированного моделирования 230 7.1.2. Еще раз о модели принтера 232 7.1.3. Рассуждения о нескольких принтерах 236 7.2. Добавление связей в объектно-ориентированные модели 240 7.2.1. Описание общей модели на уровне классов 240 7.2.2. Описание ситуации 243 7.2.3. Представление модели социальной сети на Figaro 246 7.3. Моделирование реляционной неопределенности и неопределенности типа 249 7.3.1. Коллекции элементов и ссылки 249 7.3.2. Модель социальной сети с реляционной неопределенностью 252 7.3.3. Модель принтера с неопределенностью типа 254 7.4. Резюме 257 7.5. Упражнения 257 ГЛАВА 8. Моделирование динамических систем 259 8.1. Динамические вероятностные модели 260 8.2. Типы динамических моделей 261 Оглавление 9
8.2.1. Марковские цепи 261 8.2.2. Скрытые марковские модели 265 8.2.3. Динамические байесовские сети 268 8.2.4. Модели с нестационарной структурой 272 8.3. Моделирование систем, работающих неопределенно долго 277 8.3.1. Универсумы в Figaro 277 8.3.2. Использование универсумов для моделирования постоянно работающих систем 279 8.3.3. Следящее приложение 281 8.4. Резюме 284 8.5. Упражнения 284 ЧАСТЬ III Вывод 287 Глава 9. Три правила вероятностного вывода 288 9.1. Цепное правило: построение совместных распределений по условным распределениям вероятности 290 9.2. Правило полной вероятности: получение ответов на простые запросы из совместного распределения 294 9.3. Правило Байеса: вывод причин из следствий 297 9.3.1. Понимание, причина, следствие и вывод 297 9.3.2. Правило Байеса на практике 299 9.4. Байесовское моделирование 301 9.4.1. Оценивание асимметрии монеты 303 9.4.2. Предсказание результата следующего подбрасывания 307 9.5. Резюме 312 9.6. Упражнения 312 Глава 10. Факторные алгоритмы вывода 314 10.1. Факторы 315 10.1.1. Что такое фактор? 315 10.1.2. Факторизация распределения вероятности с помощью цепного правила 318 10.1.3. Задание запросов с факторами с помощью правила полной вероятности 320 10.2. Алгоритм исключения переменных 324 10.2.1. Графическая интерпретация ИП 325 10.2.2. Исключение переменных как алгебраическая операция 329 10.3. Использование алгоритма ИП 332 10.3.1. Особенности ИП в Figaro 332 10.3.2. Проектирование модели, эффективно поддерживающей ИП 334 10.3.3. Приложения алгоритма ИП 338 10.4. Распространение доверия 342 10 Оглавление
10.4.1. Основные принципы РД 342 10.4.2. Свойства циклического РД 343 10.5. Использование алгоритма РД 345 10.5.1. Особенности РД в Figaro 346 10.5.2. Проектирование модели, эффективно поддерживающей РД 347 10.5.3. Приложения алгоритма РД 349 10.6. Резюме 350 10.7. Упражнения 350 Глава 11. Выборочные алгоритмы 353 11.1. Принцип работы выборочных алгоритмов 354 11.1.1. Прямая выборка 355 11.1.2. Выборка с отклонением 360 11.2. Выборка по значимости 363 11.2.1. Как работает выборка по значимости 364 11.2.2. Выборка по значимости в Figaro 367 11.2.3. Полезность выборки по значимости 368 11.2.4. Приложения алгоритма выборки по значимости 370 11.3. Алгоритм Монте-Карло по схеме марковской цепи 373 11.3.1. Как работает МСМС 374 11.3.2. Алгоритм МСМС в Figaro: алгоритм Метрополиса-Гастингса 378 11.4. Настройка алгоритмами 382 11.4.1. Специальные схемы предложения 384 11.4.2. Избежание жестких условий 388 11.4.3. Приложения алгоритма МГ 389 11.5. Резюме 391 11.6. Упражнения 392 Глава 12. Решение других задач вывода 394 12.1. Вычисление совместных распределений 395 12.2. Вычисление наиболее вероятного объяснения 397 12.2.1. Вычисление и запрос НВО в Figaro 400 12.2.2. Использование алгоритмов для ответа на запросы НВО 402 12.2.3. Приложения алгоритмов НВО 409 12.3. Вычисление вероятности фактов 410 12.3.1. Наблюдение фактов для вычисления вероятности фактов 411 12.3.2. Выполнение алгоритмов вычисления вероятности фактов 414 12.4. Резюме 415 12.5. Упражнения 415 Глава 13. Динамические рассуждения и обучение параметров 417 13.1. Мониторинг состояния динамической системы 418 13.1.1. Механизм мониторинга 419 13.1.2. Алгоритм фильтрации частиц 421 Оглавление и
13.1.3. Применения фильтрации 424 13.2. Обучение параметров модели 425 13.2.1. Байесовское обучение 426 13.2.2. Обучение методом максимального правдоподобия и МАВ 430 13.3. Дальше вместе с Figaro 439 13.4. Резюме 440 13.5. Упражнения 440 Приложение А. Получение и установка Scala и Figaro 443 А.1. Использование sbt 443 А.2. Установка и запуск Figaro без sbt 444 А.З. Сборка из исходного кода 445 Приложение В. Краткий обзор систем вероятностного программирования 447 Предметный указатель 450 12 Оглавление
ПРЕДИСЛОВИЕ В 1814 году Пьер-Симон Лаплас писал: «по большей части важнейшие жизненные вопросы являются на самом деле лишь задачами теории вероятностей». Спустя сто лет после этих слов на такие вопросы можно было ответить только одним способом (сохраняя верность мнению Лапласа): проанализировать каждую задачу на бумаге, выразить результат в виде формулы и вычислить значение формулы, вручную подставив в нее числа. Наступление эры компьютеров мало что изменило. Просто стало возможно вычислять более сложные формулы, а анализ с помощью «пера и бумаги» теперь может занимать сотни страниц. Для анализа вероятностной задачи необходимо построить вероятностную модель, в которой описывается пространство возможных исходов и каждому из них каким-то образом сопоставляется числовая вероятность. Раньше вероятностные модели формулировались на смеси естественного языка и полуформальной математической нотации. На основе модели с помощью некоторых математических манипуляций выводилась формула или алгоритм для вычисления ответов. Обе стадии были трудоемкими, чреватыми ошибками и зависели от конкретной задачи, поэтому применение теории вероятностей на практике сталкивалось с серьезными ограничениями. Вопреки Лапласу, важнейшие жизненные вопросы оставались неразрешенными. Первым крупным продвижением стала разработка формальных языков, в частности байесовских и марковских сетей, для выражения вероятностных моделей. У формального языка имеется точный синтаксис, определяющий, какие выражения допустимы, и точная семантика, определяющая, что означает каждое допустимое выражение (т. е. какая именно вероятностная модель представлена данным выражением). Поэтому появилась возможность описывать вероятностные модели в машиночитаемом виде и разработать единый алгоритм вычисления следствий любой выразимой вероятностной модели. Но в этой бочке меда есть одна ложка дегтя: отсутствие выразимых вероятностных моделей. Байесовские и марковские сети как формальные языки обладают очень ограниченными выразительными возможностями. В каком-то смысле их можно назвать вероятностными аналогами булевых схем. Чтобы понять, в чем состоит ограничение, рассмотрим написание программы расчета платежной ведомости для крупной компании. На языке высокого уровня, например Java, она может состоять из десятков тысяч строк кода. А теперь представьте себе реализацию той же функциональности посредством соединения логических вентилей. Эта задача,
Предисловие по всей видимости, абсолютно неразрешима. Схема оказалась бы невообразимо огромной, сложной и непонятной, поскольку логическим схемам недостает выразительной силы, соответствующей структуре задачи. В 1997 году Ави Пфеффер, автор этой книги, тогда еще студент, в соавторстве со своим научным руководителем Дафной Коллер и коллегой Дэвидом Макал- лестером, опубликовал пионерскую работу по языкам вероятностного программирования (ЯВП) (probabilistic programming language - PPL), в которой высказана важнейшая идея, связывающая теорию вероятностей с выразительными возможностями языков программирования высокого уровня. Идея заключалась в том, что программу можно рассматривать как вероятностную модель, если ввести некоторые стохастические элементы и определить смысл программы как вероятность каждого возможного пути выполнения. Эта идея вводит полезную связь между двумя важнейшими областями математики, и мы еще только приступаем к исследованию открывающихся на этом пути возможностей. Книга представляет собой неформальное введение в круг этих идей на примере языка Figaro для иллюстрации основных концепций и их применения. Автор избегает ненужной математики и акцентирует внимание на реальных тщательно подобранных примерах, которые подробно объясняет. Книга доступна любому человеку со стандартной подготовкой в области программирования. Заодно трудолюбивый читатель с меньшими, чем обычно, усилиями освоит принципы и технику байесовского вывода и статистического обучения. Но, пожалуй, еще важнее тот факт, что читатель приобретет навыки моделирования - одно из важнейших умений любого ученого или инженера. Figaro и другие ЯВП позволяют выражать такие модели непосредственно, быстро и точно. Эта книга - важный шаг на пути вывода вероятностного программирования из научно-исследовательских лабораторий, где оно зародилось, в реальный мир. Без сомнения, такое столкновение с реальностью выявит ограничения имеющихся систем ВП, и у лабораторий появятся новые задачи. С другой стороны, читатели этой книги обязательно откроют неожиданные способы применения Figaro и подобных ему языков к широкому кругу новых задач, о которых авторы даже не подозревали. Стюарт Рассел профессор информатики Калифорнийский университет в Беркли 14
ВСТУПЛЕНИЕ Вероятностное программирование - новая захватывающая область исследований, которая привлекает все больший интерес. Постепенно она прокладывает себе дорогу из академических кругов в мир программистов. По сути дела, вероятностное программирование - это новый способ создания вероятностных моделей, позволяющих предсказывать или выводить новые факты, которых нет в результатах наблюдений. Вероятностные рассуждения давно считаются одним из основных подходов к машинному обучению, где модель описывает то, что известно из опыта. Но раньше такие системы были ограничены простыми фиксированными структурами типа байесовских сетей. Вероятностное программирование освобождает системы вероятностных рассуждений от этих оков, предоставляя всю мощь языков программирования для представления моделей. Можно считать, что это аналог перехода от логических схем к языкам высокого уровня. Я начал заниматься вероятностным программированием с подростковых лет, когда писал футбольный симулятор на BASIC, хотя в то время еще не осознавал этого. В программе встречались предложения вида «GOTO 1730 + RANDOM * 5», призванные выразить случайную последовательность событий. После тщательной настройки эмулятор оказался настолько реалистичным, что я мог развлекаться с ним часами. Разумеется, с тех пор вероятностное программирование далеко ушло от предложений GOTO со случайным адресом. В 1997 я в соавторстве с Дафной Коллер и Дэвидом Макаллестером написал одну из первых работ по вероятностному программированию. В ней описывался Lisp-подобный язык, но главным новшеством был алгоритм выведения вероятных аспектов программы на основе наблюдаемых результатов ее работы. Это и вывело вероятностное программирование из разряда типичных языков вероятностного моделирования, предоставив средства, позволявшие программе не только продвигаться вперед по одному из возможных путей выполнения, но и строить обратные рассуждения и делать выводы о том, почему получен наблюдаемый результат. В начале 2000-х годов я разработал IBAL (произносится «айболл») - первую систему вероятностного программирования общего назначения, основанную на функциональном программировании. IBAL обладала высокой выразительной способностью и содержала инновационные алгоритмы вывода, но с годами меня все сильнее раздражали ее ограничения и главное из них - трудность взаимодействия с данными и интеграции с приложениями. Поэтому в 2009 году я начал разрабатывать новую систему вероятностного программирования, которую назвал
Вступление Figaro. При проектировании Figaro на первый план выдвигалась практичность, но не жертвуя мощью вероятностного программирования. Это навело меня на мысль реализовать Figaro в виде библиотеки на языке Scala, что упрощает интеграцию вероятностных моделей с приложениями для виртуальной машины Java. В то же время Figaro предлагала, пожалуй, самый широкий спектр средств представления и алгоритмов выводов из всех известных мне систем вероятностного программирования. В настоящее время Figaro - проект с открытым исходным кодом на GitHub, его текущая версия имеет номер 3.3. Овладеть вероятностным программированием не так-то просто, потому что необходимы разнообразные навыки и, прежде всего, умение строить вероятностные модели и писать программы. Для многих программистов написание программ - естественный процесс, но вероятностные модели окутаны тайной. Эта книга призвана сорвать покров таинственности, показать, как можно эффективно программировать в процессе создания моделей и помочь в освоении систем вероятностного программирования. Не предполагается, что читатель имеет подготовку в области машинного обучения или вероятностных рассуждений. Знакомство с функциональным программированием и языком Scala будет полезно, но для чтения книги не нужно быть опытным специалистом по Scala. Зато вполне может статься, что после прочтения вы станете программировать на Scala увереннее. Прочитав книгу, вы сможете проектировать вероятностные модели для различных приложений, извлекающих осмысленную информацию из данных, и степень доктора по машинному обучению для этого не потребуется. Если вы специализируетесь в какой-то предметной области, то книга поможет выразить модели, которые имеются у вас в голове или на бумаге, и сделать их операционными, т. е. допускающими вычисление и анализ вариантов. Если вы - специалист по анализу данных, то, прочитав книгу, сможете разрабатывать более развитые, детальные и потенциально более точные модели, чем позволяют другие средства. Программисту или архитектору, стремящемуся включить в свою систему умение рассуждать в условиях неопределенности, книга поможет не только построить вероятностную модель, но и интегрировать ее с приложением. По какой бы причине вы ни выбрали эту книгу, я надеюсь, что она доставит вам удовольствие и окажется полезной. 16
БЛАГОДАРНОСТИ Эта книга потребовала многих лет работы: от первых идей, касающихся вероятностного программирования, через создание систем IBAL и Figaro до замысла, написания и шлифовки книги в сотрудничестве с издательством Manning. He счесть людей, усилия которых помогали созданию книги. Своим существованием эта книга в значительной степени обязана трудам моих коллег из компании Charles River Analytics: Джо Гормана (Joe Gorman), Скотта Харрисона (Scott Harrison), Майкла Ховарда (Michael Howard), Ли Келлога (Lee Kellogg), Элисон О'Коннор (Alison O'Connor), Майка Репоза (Mike Reposa), Брайна Раттенберга (Brian Ruttenberg) и Гленна Таката (Glenn Takata). Также спасибо Скотту Нилу Рейли (Scott Neal Reilly), который поддерживал Figaro с самого начала. Большую часть того, что я знаю об искусственном интеллекте и машинном обучении, я почерпнул от Дафны Коллер, моего научного руководителя и коллеги. Стюарт Рассел предоставил мне первую возможность изучать искусственный интеллект и воодушевлял на протяжении всей моей карьеры. А недавно мы начали работать в одном проекте, и он написал предисловие к этой книге. Майкл Стоун- брейкер предоставил мне шанс заняться исследованиями в своем проекте СУБД Postgres, и я много узнал о построении систем, работая в его группе. Алон Халеви (Alon Halevy) пригласил меня провести лето вместе с ним с в AT&T Labs, где я имел первые беседы о вероятностном программировании с Дэвидом Макаллесте- ром; в результате родилась на свет статья о вероятностном диалекте Lisp, написанная в соавторстве с Дафной. С Лайзой Гетур (Lise Getoor), делившей со мной кабинет и общую работу, я мог обсуждать идеи, которые вынашивал. Я глубоко признателен Алексу Ихлеру (Alex Ihler), который любезно проверил книгу на предмет отсутствия технических ошибок. Алекс также оказался великолепным слушателем, на котором последние пару лет я опробовал все идеи, относящиеся к логическому выводу. Многие люди делились своими замечаниями на различных этапах разработки, в том числе Равишанкар Раджагопалан (Ravishankar Rajagopalan) и Шабеш Ба- лан (Shabeesh Balan), Крис Хенеган (Chris Heneghan), Клеменс Баадер (Clemens Baader), Кроистофер Вебер (Cristofer Weber), Эрл Бингэм (Earl Bingham), Джу- зеппе де Марко (Giuseppe de Marco), Джауме Валлс (Jaume Vails), Хавьер Гуэр- ра Гиральдес (Javier Guerra Giraldez), Костас Пассадис (Kostas Passadis), Лука Кампобассо (Luca Campobasso), Лукас Наллиндо (Lucas Gallindo), Марк Элстон
Благодарности (Mark Elston), Марк Миллер (Mark Miller), Нитин Годе (Nitin Gode), Одиссеас Пентаколос (Odisseyas Pentakolos), Петр Рабинович (Peter Rabinovitch), Филлип Брэдфорд (Phillip Bradford), Стивен Уэйкли (Stephen Wakely), Тапош Дутта Рой (Taposh Dutta Roy), Унникришнан Кумар (Unnikrishnan Kumar). Я благодарен многим замечательным сотрудникам издательства Manning, которые способствовали выходу книги из печати. Отдельное спасибо редактору Дэну Махарри (Dan Maharry), который сделал книгу куда лучше, чем мог бы я сам, и Фрэнку Полманну (Frank Pohlmann), который посоветовал мне написать книгу и помогал на подготовительных этапах. Спасибо Исследовательской лаборатории ВВС и Управлению перспективных научно-исследовательских проектов Министерства обороны США (DARPA) за финансирование части описанной в этой книге работы в рамках программы PPAML (вероятностное программирование для развития методов машинного обучения). Отдельная благодарность нескольким руководителям программ в DARPA: Бобу Кохоуту (Bob Kohout), Тони Фальконе (Tony Falcone), Кэтлин Фишер (Kathleen Fisher) и Сурешу Джаганнатану (Suresh Jagannathan), которые поверили в вероятностное программирование и прилагали все силы, чтобы оно стало реальностью. Этот материал основан на работе, которая финансировалась ВВС США по контракту № FA8750-14-C-0011. Мнения, открытия и заключения или рекомендации автора, встречающиеся в этом материале, не обязательно отражают точку зрения ВВС США. И наконец, эта книга не состоялась бы без любви и поддержки со стороны моей семьи. Спасибо моей супруге Дебби Гелбер и моим детям Дине, Номи и Рути за то, что они такие чудесные. И вечная благодарность моей маме Клэр Пфеффер, воспитавшей меня с любовью. Эту книгу я посвящаю твоей памяти. 18
ОБ ЭТОЙ КНИГЕ Многие решения, будь то в бизнесе, науке, военном деле или повседневной жизни, принимаются в условиях неопределенности. Если разные факторы толкают в разных направлениях, то как узнать, на что обращать больше внимания? Вероятностные модели дают средство выразить всю информацию, относящуюся к конкретной ситуации. Вероятностные рассуждения позволяют использовать эти модели, чтобы найти вероятности величин, наиболее существенных для принятия решения. С помощью вероятностных рассуждений можно предсказать наиболее вероятное развитие событий: хорошо ли будет продаваться ваш продукт по назначенной цене, будет ли пациент отвечать на предложенное лечение, сможет ли кандидат победить на выборах, если займет определенную позицию? Вероятностные рассуждения можно использовать и для вывода вероятных причин уже случившихся событий: если продажи провалились, то не потому ли, что была назначена слишком высокая цена? Вероятностное рассуждение также является одним из основных подходов к машинному обучению. Мы представляем исходные допущения о предметной области в виде вероятностной модели, например общей модели поведения пользователей в ответ на появление продуктов на рынке. Затем, получив обучающие данные, скажем о реакции пользователей на конкретные продукты, мы модифицируем первоначальные представления и получаем новую модель. Эту модель можно уже использовать для предсказания будущих событий, например успеха перспективного продукта, или для объяснения причин наблюдавшихся событий, скажем провала нового продукта. Раньше для выражения вероятностных рассуждения применялись специализированные языки представления вероятностных моделей. В последние годы мы поняли, что можно использовать обычные языки программирования, и это положило начало вероятностному программированию. У такого подхода три основных достоинства. Во-первых, при построении моделей в нашем распоряжении все средства языка программирования: развитые структуры данных, поток управления и т. д. Во-вторых, вероятностную модель легко интегрировать с другими приложениями. И, в-третьих, для рассуждений о моделях можно использовать универсальные алгоритмы логического вывода. Задача этой книги - вооружить вас знаниями, необходимыми для использования вероятностного программирования в повседневной деятельности. В частности, объясняется:
• как строить вероятностные модели и выражать их в виде вероятностных программ; • как устроены вероятностные рассуждения и как они реализованы в различных алгоритмах вывода; • как с помощью системы вероятностного программирования Figaro создавать практичные вероятностные программы. Система Figaro реализована в виде библиотеки на языке Scala. Как и в Scala, в ней сочетаются функциональный и объектно-ориентированный стили программирования. Некоторое знакомство с функциональным программированием было бы полезно. Но в этой книге не используются сложные концепции функционального программирования, так что для понимания хватит и скромных познаний. Знание Scala также не помешало бы. Хотя встречающиеся конструкции Scala часто объясняются, эта книга не является введением в Scala. Но экзотические особенности Scala почти не используются, так что поверхностного знакомства будет достаточно. Структура книги Часть 1 представляет собой введение в вероятностное программирование и систему Figaro. В начале главы 1 объясняется, что такое вероятностное программирование и почему оно полезно, а затем приводится краткое введение в Figaro. Глава 2 - учебное пособие по работе с Figaro, она позволит вам быстро приступить к написанию вероятностных программ. В главе 3 вы найдете полное приложение вероятностного программирования - фильтр спама - включающее компонент, который рассуждает о том, является данное почтовое сообщение хорошим или спамным, и компонент, который обучает вероятностную модель на данных. Цель главы 3 - дать общую картину взаимосвязей разных частей, перед тем как приступать к детальному изучению приемов моделирования. Часть 2 целиком посвящена построению вероятностных программ. В главе 4 представлены основные сведения о вероятностных моделях и вероятностных программах, которые необходимы для понимания того, что в действительности стоит за созданием таких программ. В главе 5 описаны две системы моделирования, лежащие в основе вероятностного программирования: байесовские и марковские сети. Главы 6-8 содержат набор полезных приемов программирования, применяемых при создании более сложных программ. В главе 6 речь идет о коллекциях в Scala и Figaro, позволяющих скомпоновать много переменных одного типа. Глава 7 посвящена объектно-ориентированному программированию, которое в функциональных программах не менее полезно, чем в обычных. В главе 8 рассказано о моделировании динамических систем. Динамической называется система, состояние которой изменяется со временем, и в этой главе подробно рассматривается это весьма распространенное и важное применение вероятностных рассуждений. Из части 3 вы узнаете о вероятностных алгоритмах вывода. Понимать, что такое вывод, необходимо для эффективного применения вероятностного программирования; это позволит выбрать подходящий для решения задачи алгоритм, правиль- 20 Об этой книге
Об авторе но настроить его и выразить модель так, чтобы она эффективно поддерживала рассуждения. Я старался соблюсти баланс между изложением теории, стоящей за алгоритмами, и практическими примерами их использования. В основополагающей главе 9 сформулированы три правила, в которых заключены основные идеи вероятностного вывода. В главах 10 и 11 описаны два основных семейства алгоритмов вывода. В главе 10 рассматриваются факторные алгоритмы, в том числе введение в факторы и принципы их работы, а также алгоритмы исключения переменных и распространения доверия. В главе 11 обсуждаются выборочные алгоритмы с акцентом на выборку по значимости и методы Монте-Карло по схеме марковской цепи. Если в главах 10 и 11 упор сделан на базовом запросе - вычислении вероятностей величин, представляющих интерес, то в главе 12 показано, как факторные и выборочные алгоритмы можно использовать для вычисления других запросов, например, совместной вероятности нескольких переменных, наиболее вероятных значений переменных и вероятности эмпирических данных. Наконец, в главе 13 обсуждаются две более сложных, но важных задачи вывода: мониторинг динамической системы, изменяющейся во времени, и нахождение числовых параметров вероятностной модели на основе обучающих данных. Каждая глава сопровождается упражнениями. Это могут быть как простые вычисления, так и задачи на размышление, не предполагающие однозначного ответа. В книге есть еще два приложения. Приложение А содержит инструкцию по установке Figaro, а приложение В - краткий обзор других систем вероятностного программирования. О коде и упражнениях Код набран моноширинным шрифтом, чтобы не путать его с обычным текстом. Многие листинги сопровождаются аннотациями, в которых объяснены важные концепции. В некоторых случаях за листингом следует несколько комментариев, оформленных как отдельные пункты. Многие примеры кода имеются в сети, найти их можно на сайте книги по адресу www.manning.com/books/practical-probabilistic-programming. Там же есть ответы на избранные упражнения. Об авторе Ави Пфеффер - пионер вероятностного программирования, работающий в этой области с момента ее зарождения. Ави - ведущий проектировщик и разработчик Figaro. В компании Charles River Analytics Ави занимается применением Figaro к различным задачам, в том числе к анализу вредоносного ПО, мониторингу исправности транспортных средств, моделированию климата и оценке инженерных систем. В свободное время Ави поет, сочиняет музыку и продюсирует музыкальные произведения. Ави живет в Кембридже, штат Массачусетс, с женой и тремя детьми. 21
Об этой книге Автор в сети Приобретение книги «Практическое вероятностное программирование» открывает бесплатный доступ к закрытому форуму, организованному издательством Manning Publications, где вы можете оставить свои комментарии к книге, задать технические вопросы и получить помощь от автора и других пользователей. Получить доступ к форуму и подписаться на список рассылки можно на странице www.manning.com/books/practical-probabilistic-programming. Там же написано, как зайти на форум после регистрации, на какую помощь можно рассчитывать, и изложены правила поведения в форуме. Издательство Manning обязуется предоставлять читателям площадку для общения с другими читателями и автором. Однако это не означает, что автор обязан как-то участвовать в обсуждениях; его присутствие на форуме остается чисто добровольным (и не оплачивается). Мы советуем задавать автору какие-нибудь хитроумные вопросы, чтобы его интерес к форуму не угасал! Форум автора в сети и архивы будут доступны на сайте издательства до тех пор, пока книга не перестанет печататься. Об иллюстрации на обложке Рисунок на обложке книги называется «The Venetian», или «Житель Венеции». Он взят из книги Жака Грассе де Сен-Совера «Энциклопедия путешествий», опубликованной в 1796 году. Путешествие ради удовольствия в те времена было в новинку, а путеводители, подобные этому, были весьма популярны - они знакомили настоящих туристов и путешественников, не покидающих своего кресла, с жителями других регионов Франции и всего мира. Разнообразие иллюстраций в «Энциклопедии путешествий» красноречиво свидетельствует об уникальности и индивидуальности городков и провинций, которое можно было наблюдать каких-то 200 лет назад. То было время, когда по манере одеваться, отличающейся в местах, разделенных всего несколькими десятками миль, можно было сказать, откуда человек родом. Этот путеводитель возвращает к жизни ощущение изолированности и удаленности, свойственное тому периоду, да и всем остальным историческим периодам, кроме нашего гиперактивного настоящего. С тех пор манера одеваться сильно изменилась, и различия между областями, когда-то столь разительные, сгладились. Теперь трудно отличить друг от друга даже выходцев с разных континентов, что уж говорить о странах или областях. Но можно взглянуть на это и с оптимизмом - мы обменяли культурное и визуальное разнообразие на иное устройство личной жизни - основанное на многостороннем и стремительном технологическом и интеллектуальном развитии. Издательство Manning откликается на новации и инициативы в компьютерной отрасли обложками своих книг, на которых представлено широкое разнообразие местных укладов быта в позапрошлом веке. Мы возвращаем его в том виде, в каком оно запечатлено на рисунках из разных собраний, в том числе из этого путеводителя. 22
Часть I ВВЕДЕНИЕ В ВЕРОЯТНОСТНОЕ ПРОГРАММИРОВАНИЕ И СИСТЕМУ FIGARO -Lto такое вероятностное программирование? Почему оно полезно? Как им пользоваться? Ответы на эти вопросы составляют основное содержание первой части. В главе 1 излагаются основные идеи вероятностного программирования. Прежде всего, мы познакомимся с концепцией системы вероятностных рассуждений и покажем, как вероятностное программирование соединяет традиционные системы вероятностных рассуждений с технологией языков программирования. В этой книге мы будем использовать систему вероятностного программирования Figaro. В главе 1 дается краткое введение в Figaro, а глава 2 представляет собой пособие по основным понятиям Figaro, позволяющее быстро приступить к написанию вероятностных программ. Глава 3 содержит законченное вероятностное приложение, из которого вы поймете, как реальная программа собирается из различных частей. Эта глава находится в начале книги, чтобы можно было составить общую картину, но имеет смысл возвращаться к ней и позже, после более глубокого знакомства с различными концепциями.
ГЛАВА 1 . О вероятностном программировании в двух словах В этой главе. ■ Что такое вероятностное программирование? ■ Какое мне до него дело? А моему начальнику? ■ Как оно работает? ■ Figaro - система вероятностного программирования. ■ Сравнение вероятностных приложений, написан ных с применением и без применения вероятностного программирования. Из этой главы вы узнаете, как принимать решения с помощью вероятностной модели и алгоритма вывода - двух главных составных частей системы вероятностных рассуждений. Вы также узнаете, как современные языки вероятностного программирования упрощают создание подобных систем по сравнению с такими универсальными языками, как Java или Python. Здесь же вы познакомитесь с Figaro, основанным на Scala языком вероятностного программирования, которым мы будем пользоваться в этой книге. 1.1. Что такое вероятностное программирование? Вероятностное программирование - это способ создания систем, помогающих принимать решения в условиях неопределенности. Многие решения, принимаемые нами каждодневно, подразумевают учет релевантных факторов, которые мы не наблюдаем непосредственно. Исторически одним из способов принять решение в таких условиях неопределенности стали системы вероятностного рассужде-
1.1. Что такое вероятностное программирование щи ш ния. Вероятностное рассуждение позволяет объединить наши знания о ситуации с вероятностными законами и таким образом учесть ненаблюдаемые факторы, важные для принятия решения. До недавнего времени область применения систем вероятностного рассуждения была ограничена, и ко многим возникающим на практике задачам их удавалось применить с большим трудом. Вероятностное программирование - это новый подход, позволивший упростить построение систем вероятностного рассуждения и расширить их применимость. Чтобы понять, в чем смысл вероятностного программирования, начнем с рассмотрения того, как принимается решение в условиях неопределенности и какую роль в этом играют субъективные суждения. Затем посмотрим, как может помочь система вероятностного рассуждения. Мы познакомимся с тремя видами рассуждений, присущих таким системам. После этого станет понятно, что такое вероятностное программирование и как оно позволяет использовать всю мощь языков программирования для построения систем вероятностного рассуждения. 7.7.7. Как мы высказываем субъективное суждение? В реальном мире интересующие нас вопросы редко допускают недвусмысленный ответ: да или нет. Например, при выводе на рынок нового продукта хотелось бы знать, хорошо ли он будет продаваться. Вы полагаете, что его ждет успех, потому что продукт хорошо спроектирован и изучение рынка показало, что в нем есть потребность, но полной уверенности нет. Быть может, ваш конкурент представит лучший продукт, а, быть может, в вашем продукте обнаружится фатальный изъян, из-за которого рынок от него отвернется. Или экономическая ситуация повернется к худшему. Если вы хотите стопроцентной уверенности, то никогда не сможете принять решение о начале продаж (см. рис. 1.1). Для принятия решений такого рода помогает язык вероятностей. Запуская продукт, мы можем воспользоваться прошлым опытом продажи похожих продуктов для оценки вероятности того, что этот окажется успешным. А зная эту вероятность, мы сможем решить, выводить продукт на рынок или нет. Возможно, нас интересует не только сам факт успешности продукта, но и размер ожидаемого дохода или, напротив, размер убытков, которые мы понесем в случае неудачи. Знание вероятностей различных исходов помогает принимать более обоснованные решения. Ну хорошо, вероятностное мышление может помочь в принятии трудных решений с элементами субъективности. Но как это выглядит на практике? Общий принцип выражен в виде следующего факта. Факт. Субъективное суждение основано на знании + логике. Вы располагаете некоторыми знаниями об интересующей вас проблеме. Например, вы много знаете о своем продукте и, возможно, провели исследование рынка, чтобы понять, кто будет его покупать. Возможно, вы также кое-что знаете о конкурентах и знакомы с прогнозом развития экономики. Логика же позволяет получать ответы на вопросы с использованием имеющихся знаний.
Рис. 1 -1 ■ В прошлом году мой продукт всем нравился, но как будет в следующем? Нам необходим способ описания своих знаний и логика получения ответов на вопросы с использованием этих знаний. Вероятностное программирование дает то и другое. Но прежде чем переходить к описанию системы вероятностного программирования, я опишу более общую концепцию системы вероятностных рассуждений, которая предоставляет базовые средства для спецификации знаний и реализации логики. 1.1.2. Системы вероятностных рассуждений помогают принимать решения Вероятностные рассуждения - это подход, в котором модель предметной области используется для принятия решений в условиях неопределенности. Возьмем пример из области футбола. Предположим, что по статистике 9 % угловых завершаются голом. Требуется предсказать исход конкретного углового удара. Рост центрального нападающего атакующей команды равен 193 см, и известно, что он отлично играет головой. Основного вратаря защищающейся команды только что унесли на носилках, и на замену ему вышел вратарь, который проводит свой пер- 26 Глава 1. О вероятностном программировании в двух словах Показатели прошлого года Показатели будущего года
1.1. Что такое вероятностное программирование вый матч. Кроме того, дует сильный ветер, который затрудняет контроль над полетом мяча. И как при таких условиях вычислить вероятность? 1. Вы кодируете свои знания об угловых ударах и релевантных факторах 5. Система возвращает ответ в виде набора вероятностей Система вероятностных рассуждений С Модель углового Л I удара ) Алгоритм вывода NJ Ответ • Да: 20%; Нет: 80% 2. Вы предоставляете знания о предусловиях конкретного углового удара Факт • Высокий центральный нападающий • Неопытный вратарь • Сильный ветер Запрос • Будет ли забит гол? 4. Система применяет алгоритм вывода для предсказания исхода 3. Вы говорите системе, что хотите узнать Рис. 1 .2. Как система вероятностных рассуждений предсказывает исход углового удара На рис. 1.2 показано, как получать ответ с помощью системы вероятностных рассуждений. Все свои знания об угловых ударах и релевантных факторах мы кодируем в виде модели углового. Затем мы предоставляем факты о конкретном угловом ударе: что центральный нападающий высокий, что вратарь неопытный и что дует сильный ветер. Мы говорим системе, что хотели бы узнать, будет ли забит гол. Алгоритм вывода возвращает ответ: гол будет забит с вероятностью 20 %. Основные определения Общие знания - что известно о предметной области в общем, без деталей, описывающих конкретную ситуацию. Вероятностная модель - представление общих знаний о предметной области в количественном виде, в терминах вероятностей. Факты - имеющаяся информация о конкретной ситуации. Запрос - свойство ситуации, о котором мы хотели бы узнать. Вывод - процесс использования вероятностной модели для получения ответа на запрос при известных фактах. В системе вероятностных рассуждений мы создаем модель, в которой отражены все релевантные общие знания о предметной области в виде вероятностей. В нашем примере это может быть описание ситуации углового удара и всех релевант- 27
Глава 1. О вероятностном программировании в двух словах ных характеристик игроков и окружающих условии, которые могут повлиять на исход. Затем модель применяется к конкретной ситуации, для которой мы хотим получить заключение. Для этого мы передаем модели имеющуюся информацию, которая называется фактами. В данном случае есть три факта: центральный нападающий высокий, вратарь неопытный и дует сильный ветер. Полученное от модели заключение поможет принять решение - например, что на следующую игру нужно поставить другого вратаря. Сами заключения предстают в виде вероятностей, например, вероятностей других игровых навыков вратаря. Соотношение между моделью, предоставляемой вами информацией и ответами на запросы математически строго определено законами теории вероятностей. Процесс использования модели для получения ответов на запросы при известных фактах называется вероятностным выводом, или просто выводом. По счастью, разработаны алгоритмы, которые выполняют необходимые вычисления, скрывая от вас всю математику. Они называются алгоритмами вывода. На рис. 1.3 схематически изображены описанные выше понятия. Вероятностная модель выражает общие знания о ситуации Алгоритм вывода используется моделью для нахождения ответов на запросы при известных фактах Ответы на запросы имеют вид вероятностей различных исходов Система вероятностных рассуждений с Вероятностная модель j Алгоритм вывода Факты содержат конкретную информацию о ситуации Факты Запросы Ответ Запросы выражают то, что поможет принять решение Рис. 1.3. Основные компоненты системы вероятностных рассуждений Итак, мы вкратце описали основные компоненты системы вероятностных рассуждений и способ взаимодействия с ней. Но что можно делать с такой системой? Как она помогает принимать решения? В следующем разделе описаны три вида рассуждений, которые может выполнять такая система. 7.7.3. Система вероятностных рассуждений может рассуждать тремя способами Системы вероятностных рассуждений обладают гибкостью. Они могут отвечать на запросы о любом аспекте ситуации, если заданы факты, относящиеся к любым другим аспектам. На практике система выполняет рассуждения трех видов. 28
Что такое вероятностное программирование Предсказание будущих событий. Мы уже видели это на рис. 1.2, где система предсказывает, будет ли в данной ситуации забит гол. Факты обычно включают информацию о текущей ситуации, например рост центрального нападающего, опытность вратаря и силу ветра. Вывод причины событий. Прокрутим пленку вперед на 10 секунд. Высокий центральный нападающий только что забил гол головой, протолкнув мяч под корпусом вратаря. Что можно сказать об этом вратаре-новобранце при таких фактах? Можно ли заключить, что ему не хватает умения? На рис. 1.4 показано, как использовать систему вероятностных рассуждений для ответа на этот вопрос. Берется та же модель углового удара, что и для прогноза гола. (Это полезное свойство вероятностных рассуждений: одну и ту же модель можно использовать как для предсказания будущего результата, так и для объяснения причин, приведших к известному результату.) Факты те же, что и прежде, плюс тот факт, что гол забит. Запрос касается квалификации вратаря, а ответ дает вероятности различных уровней квалификации. Факты включают знание предусловий и исходов Алгоритм вывода рассуждает о запросе, зная все известные предусловия и исходы Система вероятностных рассуждений Модель углового удара Г Алгоритм вывода Ответ • Плохая: 50 %; Средняя: 30%; Хорошая: 20% Факты • Предусловия > Высокий центральный нападающий > Неопытный вратарь > Сильный ветер • Исход > Забит гол Запрос * Какова квалификация вратаря? Задается запрос о возможной причине данного исхода Рис. 1.4. Если изменить запрос и факты, то система сможет сделать вывод о том, почему был забит гол Немного поразмыслив, вы поймете, что первый способ - рассуждения с прямым ходом времени, т. е. предсказание будущих событий на основе того, что известно о текущей ситуации, а второй - рассуждения с обратным ходом времени, когда требуется вывести прошлые условия из известных результатов. Типичная вероятностная модель следует естественному ходу времени. 29
Глава 1. О вероятностном программировании в двух словах Игрок подает угловой, затем ветер воздействует на летящий мяч, затем центральный нападающий прыгает, пытаясь достать мяч головой, и, наконец, вратарь пытается спасти ворота. Но процесс рассуждения может происходить как в прямом, так и в обратном порядке. Это ключевая особенность вероятностного рассуждения, которую я не раз буду подчеркивать: направление рассуждения необязательно совпадает с направлением модели. Обучение па прошлых событиях, чтобы лучше предсказывать будущие. Прокрутим пленку еще на 10 минут вперед. Та же команда заработала еще один угловой. Все так же, как и раньше - высокий центральный нападающий, неопытный вратарь - только ветер теперь стих. Вероятностное рассуждение позволяет использовать информацию о том, что случилось при подаче предыдущего углового, чтобы лучше предсказать исход следующего. На рис. 1.5 показано, как это делается. В состав фактов входит все то, что и в прошлый раз (с пометкой, что это было в прошлый раз), а также новая информация о текущей ситуации. Отвечая на вопрос, будет ли забит гол на этот раз, алгоритм вывода сначала определяет свойства ситуации, которые привели к голу в первый раз, например, квалификацию нападающего и вратаря. А затем эти свойства используются для предсказания исхода в новой ситуации. Факты включают знание предусловий и исходов прошлых ситуаций, а также предусловия текущей ситуации Факты Алгоритм вывода рассуждает о предыдущих ситуациях, пытаясь определить факторы (например, квалификацию вратаря), существенные и в новой ситуации Алгоритм использует эти факторы, чтобы улучшить предсказание исхода в новой ситуации Система вероятностных рассуждений С Модель углового ^ удар* J Алгоритм вывода Ответ • Да: 25%; Нет: 75% Предыдущий угловой удар > Высокий центральный нападающий > Неопытный вратарь > Сильный ветер > Был забит гол Текущий угловой удар > Высокий центральный нападающий > Неопытный вратарь > Слабый ветер Запрос • Будет ли забит гол в этот раз? Задается запрос об исходе в новой ситуации Рис. 1.5. Принимая во внимание факты, относящиеся к результату прошлого углового, система вероятностного рассуждения может лучше предсказать результат следующего 30
1.1. Что такое вероятностное программирование III! ЕЛ Запросы перечисленных видов могут помочь в принятии различных решений. • Зная вероятность гола при наличии или отсутствии дополнительного защитника, можно решить, не стоит ли заменить атакующего защитником. • На основе оценке квалификации вратаря можно решить, какую зарплату предложить ему в следующем контракте. • Получив информацию о вратаре, можно решить, стоит ли ставить его на следующую игру. Обучение улучшенной модели Выше были описаны способы рассуждения о конкретных ситуациях при известных фактах. Но система вероятностных рассуждений позволяет сделать еще одну вещь: обучаясь на прошлом опыте, улучшить общие знания. Третий способ рассуждений - это использование прошлых результатов для лучшего предсказания исхода в конкретной новой ситуации. А сейчас мы говорим об улучшении самой модели. Если имеется обширный прошлый опыт, т. е. информация о многих угловых ударах, то почему бы не обучить новую модель, представляющую общие знания о том, что обычно происходит при подаче углового? На рис. 1.6 показано, что это достигается с помощью алгоритма обучения. В отличие от алгоритма вывода, его задача состоит не в том, чтобы отвечать на вопросы, а в том, чтобы породить новую модель. На вход алгоритма обучения подается исходная модель, а он обновляет ее на основе опыта. Затем новую модель можно использовать для ответа на будущие запросы. Предположительно ответы новой модели будут более обоснованы, чем ответы исходной. Системы вероятностных рассуждений и точные предсказания Как и в любой системе машинного обучения, чем больше данных у системы вероятностных рассуждений, тем точнее ее ответы. Качество предсказания зависит от двух факторов: верности отражения реального мира в исходной модели и объема предоставленных данных. Вообще говоря, чем больше данных, тем менее существенно качество исходной модели. Причина в том, что новая модель - комбинация исходной и информации, содержащейся в данных. Если данных очень мало, то доминирует исходная модель, поэтому чем она точнее, тем лучше. Если же данных много, то доминируют именно они, так что в новой модели остается очень мало от исходной, поэтому ее точность не слишком важна. Например, если мы обучаем модель по данным за весь футбольный сезон, то факторы, от которых зависит результативность углового, будут учтены весьма точно. Если же имеются данные только об одном матче, то придется изначально задать хорошие значения факторов, иначе на точность предсказаний можно не рассчитывать. Системы вероятностного программирования умеют пользоваться моделью и имеющимися данными так, что предсказания получаются максимально точными.
Глава 1. О вероятностном программировании в двух словах Обучающий алгоритм использует исходную модель и данные для порождения новой модели Алгоритм вывода использует новую модель для ответа на запросы о новой ситуации Система вероятностных рассуждений Исходная модель углового,адара Алгоритм обучения До После Новая модель углового удара Алгоритм вывода Ответ • Да: 15%; Нет: 85% Данными являются факты о предыдущих ситуациях Прошлый опыт 1 • Высокий центральный нападающий • Неопытный вратарь • Сильный ветер Прошлый опыт 2 Прошлый опыт 3 И т. д. Факты о новой ситуации Новые факты • Высокий центральный нападающий • Неопытный вратарь • Слабый ветер Запрос • Будет ли забит гол в этот раз? Рис. 1.6. С помощью алгоритма обучения можно обучить новую модель на основе прошлого опыта, а затем использовать ее для будущих выводов Теперь вы знаете, что такое вероятностное рассуждение. Ну а что же тогда такое вероятностное программирование? 7.7.4. Система вероятностного программирования: система вероятностных рассуждений, выраженная на языке программирования В любой системе вероятностных рассуждений используется язык представления, на котором выражаются вероятностные модели. Таких языков много. Возможно, о некоторых вы слыхали, например, о байесовских сетях (их еще называют сетями доверия) или скрытых марковских моделях. От языка представления зависит, какие модели сможет обработать система и как они выглядят. Множество моделей, 32
1.1. Что такое вероятностное программирование представимых языком, называется выразительной силой языка. Для разработки приложений требуется, чтобы выразительная сила была как можно больше. Система вероятностного программирования - это просто система вероятностных рассуждений, для которой языком представления является язык программирования. Говоря «язык программирования», я имею в виду, что он обладает всеми возможностями, которые принято ожидать от типичного языка программирования: переменными, развитой системой типов данных, средствами управления потоком выполнения, функциями и т. д. Как вы вскоре увидите, языки вероятностного программирования способны выразить широкий спектр вероятностных моделей, далеко выходящий за рамки большинства традиционных систем вероятностных рассуждений. Выразительная сила языков вероятностного программирования очень высока. На рис. 1.7 иллюстрируется соотношение между системами вероятностного программирования и вероятностных рассуждений в общем случае. Сравните этот рисунок с рис. 1.3, чтобы стали яснее различия между двумя системами. Основное изменение состоит в том, что модели выражаются в виде программ на некотором языке программирования, а не в виде математических конструкций типа байесовской сети. Поэтому факты, запросы и ответы становятся переменными программы. Факты можно представить конкретными значениями переменных, запрос - это получение значения переменной, а ответ - вероятности того, что переменные принимают те или иные значения. Кроме того, система вероятностного программирования обычно включает набор алгоритмов вывода. Эти алгоритмы применяются к моделям, написанным на языке системы. Модель выражена в виде программы на некотором языке программирования, а не в виде математической конструкции Система вероятностного программирования предоставляет набор алгоритмов вывода, которые применяются к моделям,написанным на языке системы Ответы представляются в виде вероятностей значений переменных, указанных в запросе Figaro с Вероятностная модель пая | Алгоритм вывода Факты — это значения переменных программы Факты Запросы Ответ Запросы — это значения других переменных программы Рис. 1.7. Система вероятностного программирования - это система вероятностных рассуждений, в которой для представления вероятностных моделей применяется язык программирования 33
Глава 1. О вероятностном программировании в двух словах Существует много систем вероятностного программирования (см. обзор в приложении В), но в этой книге рассматриваются только функциональные, полные по Тьюрингу системы. Функциональные означает, что они основаны на функциональном языке программирования, но пусть это вас не пугает - чтобы пользоваться функциональной системой вероятностного программирования, не нужно знать, что такое лямбда-функция. Просто функциональное программирование составляет теоретическое основание, благодаря которому язык имеет возможность представлять вероятностные модели. А полным по Тьюрингу называется язык, на котором можно закодировать любое вычисление, которое способен выполнить цифровой компьютер. Если нечто вообще можно сделать с помощью компьютера, то это можно сделать на любом полном по Тьюрингу языке. Большинство языков программирования, с которыми вы знакомы, например С, Java и Python, являются полными по Тьюрингу. Поскольку системы вероятностного программирования основаны на языках программирования, полных по Тьюрингу, то они обладают выдающейся гибкостью в части типов моделей, которые можно построить с их помощью. Основные определения Язык представления - язык для кодирования знаний о предметной области в модели. Выразительная сила - способность языка представления кодировать различные виды знаний. Полный по Тьюрингу - язык, позволяющий выразить любое вычисление, которое можно выполнить на цифровом компьютере. Язык вероятностного программирования - язык представления системы вероятностных рассуждений, в котором для представления знаний используется полный по Тьюрингу язык программирования. В приложении В приведен обзор некоторых систем вероятностного программирования, помимо используемой в этой книге системы Figaro. В большинстве из них используются полные по Тьюрингу языки. В некоторых, например BUGS и Dimple, это не так, но они все равно полезны для тех приложений, на которые рассчитаны. В этой книге мы будем заниматься исключительно возможностями полных по Тьюрингу языков вероятностного программирования. Представление вероятностных моделей в виде программ Но каким образом язык программирования становится языком вероятностного моделирования? Как представить вероятностную модель в виде программы? Сейчас я дам очень краткий ответ на этот вопрос, а более глубокое обсуждение отложу на потом, когда мы станем лучше понимать, как выглядит вероятностная программа. Главная идея любого языка программирования - выполнение. Мы выполняем программу, порождающую некоторый выход. Вероятностная программа в этом смысле аналогична, только она может иметь не один, а несколько путей выполнения, каждый из которых порождает свой выход. По какому пути следовать, определяется случайным выбором, совершаемым внутри программы. Каждый случайный выбор 34
1.1. Что такое вероятностное программирование имеет несколько возможных исходов, и в программе закодирована вероятность каждого исхода. Поэтому можно считать, что вероятностная программа - это программа, при выполнении которой случайным образом генерируются выходные данные. Эта идея иллюстрируется на рис. 1.8. Здесь система вероятностного программирования содержит программу углового удара, которая описывает случайный процесс генерации исхода углового. Программа принимает ряд входных данных; в нашем примере это рост центрального нападающего, опытность вратаря и сила ветра. На основе этих данных программа случайным образом выполняется и генерирует выходные данные. Каждое случайное выполнение дает на выходе конкретный результат. Поскольку каждый случайный выбор может иметь несколько исходов, то существует много возможных путей выполнения, дающих различные результаты. Любой заданный результат, например взятие ворот, может быть сгенерирован на различных путях выполнения. 1. Вероятностная программа описывает процесс случайной генерации выходных данных по известным входным Высокий центральный нападающий Неопытный вратарь Сильный ветер Вероятностная система программирования с Программа углового удара Случайное выполнение Нет гола Гол!!! Нет гола Чуть не забили При заданных входных данных программа может случайно выполняться многократно и каждый раз порождать новые результаты Вероятность некоторого результата — это вероятность выбора пути выполнения, порождающего такой результат. В данном случае вероятность гола равна % Рис. 1.8. Вероятностная программа определяет процесс случайной генерации выходных данных по известным входным Рассмотрим, как эта программа определяет вероятностную модель. Любое конкретное выполнение программы является результатом последовательности случайных выборов. Каждый случайный выбор происходит с некоторой вероятностью. Если перемножить все эти вероятности, то получится вероятность пути выполнения. Таким образом, программа определяет вероятность каждого пути выполнения. Если представить себе, что программа прогоняется многократно, то доля прогонов, на которых генерируется заданный путь выполнения, равна его вероятности. Вероятность некоторого результата равна доле прогонов, на которых 35
Глава 1. О вероятностном программировании в двух словах был получен этот результат. На рис. 1.8 гол получается в 1/4 всех прогонов, поэтому вероятность гола равна 1/4. Примечание. Возможно, вы недоумеваете, почему один из блоков на рис. 1.8 назван «Случайное выполнение», а не «Алгоритм вывода», как на других рисунках. На рис. 1.8 показан смысл вероятностной программы - определение процесса случайного выполнения, а не как использовать систему вероятностного программирования - выполнить алгоритм вывода для получения ответа на запрос при заданных фактах. Поэтому хотя структурно рисунки похожи, они иллюстрируют разные концепции. На самом деле, случайное выполнение лежит в основе и некоторых алгоритмов вывода, но есть много алгоритмов, которые не основаны на простом случайном выполнении. Принятие решений с помощью вероятностного программирования Легко видеть, как можно использовать вероятностную программу для предсказания будущего. Нужно просто выполнить ее случайное число раз, подавая на вход известные данные о настоящем, и посмотреть, сколько раз порождается каждый результат. В примере с угловым ударом на рис. 1.8 мы выполняли программу много раз, подавая на вход такие данные: высокий центральный нападающий, неопытный вратарь и сильный ветер. Поскольку в 1/4 прогонов получился гол, можно сказать, что при этих входных данных вероятность гола равна 25 %. Однако прелесть вероятностного программирования заключается в том, что его можно с тем же успехом использовать для любых видов вероятностных рассуждений, описанных в разделе 1.3.1. Оно позволяет не только предсказывать будущее, но и выводить факты, приведшие к наблюдаемым результатам; можно «открутить» программу назад и посмотреть, какие причины породили данный исход. Можно также применить программу в одной ситуации, обучиться на полученном результате и использовать новую программу для получения более точных предсказаний в будущем. Вероятностное программирование помогает принимать любые решения в рамках вероятностных рассуждений. Как все это работает? Вероятностное программирование стало применяться на практике, когда было осознано, что алгоритмы вывода, работающие для более простых языков представления, например байесовских сетей, можно обобщить и на программы. В части 3 описаны разнообразные алгоритмы вывода, благодаря которым это возможно. К счастью, в состав вероятностных систем программирования входит ряд встроенных алгоритмов вывода, которые применяются к программам автоматически. Вам нужно лишь предоставить знания о предметной области в виде вероятностной программы и описать факты, а система сама позаботится о выводе и обучении. В этой книге мы будем изучать вероятностные рассуждения через призму вероятностного программирования. Прежде всего, мы узнаем, что такое вероятностная модель и как ее использовать для вывода заключений. Мы также научимся производить некоторые простые манипуляции для вывода таких заключений из модели, составленной из простых компонентов. Мы рассмотрим различные приемы 36
1.2. Зачем нужно вероятностное программирование ни Шум моделирования и покажем, как реализовать их с помощью вероятностного программирования. Мы разберемся в том, как работают вероятностные алгоритмы вывода, и это позволит эффективно проектировать и использовать модели. К концу книги вы сможете уверенно пользоваться вероятностным программированием для вывода полезных заключений и принятия обоснованных решений в условиях неопределенности. 1.2. Зачем нужно вероятностное программирование? Вероятностные рассуждения - одна из фундаментальных технологий машинного обучения. Такие системы используются компаниями Google, Amazon и Microsoft для извлечения смысла из имеющихся данных. Вероятностные рассуждения применялись в таких разных приложениях, как прогнозирование цены акций, ре- комендование фильмов, диагностика компьютеров и обнаружение вторжений в вычислительные системы. Во многих из этих приложений используются методы, описанные в этой книге. В предыдущем разделе следует выделить два важнейших положения: • вероятностные рассуждения можно использовать для предсказания будущего, объяснения прошлого и обучения на прошлом опыте для лучшего предсказания будущего; • вероятностное программирование - это вероятностное рассуждение, в котором для представления используется полный по Тьюрингу язык программирования. В сочетании эти два положения позволяют отчеканить следующую формулу. Факт. Вероятностное рассуждение + полнота по Тьюрингу = вероятностное программирование. Обоснованием вероятностного программирования является тот факт, что из двух концепций, которые сами по себе являются достаточно мощными, составляется новая. В результате получается более простой и гибкий способ применения компьютеров для принятия решений в условиях неопределенности. 7.2.7. Улучшенные вероятностные рассуждения У большинства современных вероятностных языков представления имеются ограничения на сложность представляемых систем. В таких относительно простых языках, как байесовские сети, предполагается фиксированный набор переменных, поэтому они не обладают достаточной гибкостью для моделирования предметных областей, в которых состав переменных может изменяться. В последние годы были разработаны более гибкие языки. В некоторых (например, BUGS) даже имеются возможности, характерные для языков программирования, напри-
Глава 1. О вероятностном программировании в двух словах мер итерация и массивы, хотя полными по Тьюрингу они все же не являются. Успех языков, подобных BUGS, ясно говорит о необходимости более развитых и структурированных представлений. Но движение в сторону полноценных полных по Тьюрингу языков открывает целый мир новых возможностей для вероятностных рассуждений. Теперь стало возможно моделировать длительные процессы с большим числом событий и взаимодействующих сущностей. Снова рассмотрим пример на футбольную тематику, но теперь представьте, что вы занимаетесь спортивной аналитикой и хотите порекомендовать решения по подбору игроков в команду. Можно было бы воспользоваться для этой цели накопленной статистикой, но статистика не улавливает контекст, в котором была собрана. Можно провести более тонкий контекстный анализ, построив детальную модель футбольного сезона. Для этого придется моделировать много взаимосвязанных событий и взаимодействие игроков и команд. Трудно представить себе, как построить такую модель без структур данных и управляющих конструкций, имеющихся в полноценном языке программирования. Вернемся к вопросу о выводе продукта на рынок и взглянем на процесс принятия решений по развитию бизнеса во всей его полноте. Запуск продукта - не изолированное событие, ему предшествуют этапы анализа рынка, исследований и разработок, и исход каждого этапа не предрешен. Результаты запуска продукта зависят от всех предшествующих этапов, а также от анализа того, что еще имеется на рынке. Полный анализ должен также учитывать, как отреагируют на наш продукт конкуренты и какие новые продукты они смогут предложить. Это трудная задача, т. к. приходится выдвигать гипотезы о конкурирующих продуктах. Возможно даже, что существуют конкуренты, о которых вы еще не знаете. В этом примере продукты оказываются структурами данных, порождаемыми сложными процессами. Поэтому наличие полноценного языка программирования было бы крайне полезно для создания модели. У вероятностного программирования есть одна приятная особенность: если вы хотите использовать более простой каркас вероятностных рассуждений - пожалуйста. Системы вероятностного программирования позволяют представить широкий спектр существующих каркасов, а также такие системы, которые ни один из существующих каркасов представить не в состоянии. В этой книге вы узнаете о многих каркасах, в которых используется вероятностное программирование. Поэтому, изучая вероятностное программирование, вы заодно освоите многие современные каркасы вероятностных рассуждений. 1.2.2. Улучшенные языки имитационного моделирования Полные по Тьюрингу языки вероятностного моделирования уже существуют. Обычно они называются языками имитационного моделирования (simulation languages). Мы знаем, что можно построить имитационную модель такого сложного процесса, как футбольный сезон, пользуясь языками программирования. В этом контексте я буду использовать термин язык имитационного моделирова- 38
1.2. Зачем нужно вероятностное программирование III! Еа ния для описания языка, который позволяет представить выполнение сложного процесса с элементами случайности. Как и вероятностные программы, такие имитационные модели выполняются случайным образом и порождают различные результаты. Имитационное моделирование широко используется как вероятностное рассуждение и применяется в самых разных областях, например: военном планировании, проектировании компонентов, здравоохранении и прогнозировании спортивных результатов. На самом деле, широкое распространение изощренных имитационных моделей говорит о потребности в развитых языках вероятностного моделирования. Но вероятностная программа - нечто гораздо большее, чем имитационная модель. Имитационное моделирование позволяет реализовать лишь один аспект вероятностного программирования: предсказание будущего. Его нельзя использовать для объяснения причин наблюдаемых результатов. И хотя модель можно улучшить, дополнив ее новой актуальной информацией, трудно включить неизвестную информацию, которую нужно вывести. Поэтому способность к обучению на прошлом опыте с целью улучшения будущих прогнозов и анализа оказывается ограниченной. Использовать имитационные модели в машинном обучении невозможно. Вероятностную программу можно уподобить имитационной модели, которая допускает не только выполнение, но и анализ. При разработке вероятностного программирования пришло осознание того, что многие алгоритмы вывода, используемые в более простых системах моделирования, можно использовать и для имитационного моделирования. Поэтому открывается возможность создать вероятностную модель, написав имитационную модель и применив к ней алгоритмы вывода. И последнее. Системы вероятностных рассуждений уже существуют, например программы Hugin, Netica и BayesiaLab предлагают реализацию байесовских сетей. Но более выразительные языки представления, применяемые в вероятностном программировании, настолько новые, что мы лишь начинаем открывать для себя таящуюся в них мощь. Положа руку на сердце, не могу сказать, что вероятностное программирование уже используется во многих прикладных системах. Но некоторые нетривиальные приложения все же существуют. Microsoft с помощью вероятностного программирования научилась определять истинный уровень умения игроков в онлайновые игры. Стюарт Рассел из Калифорнийского университета в Беркли написал программу, которая следит за соблюдением Договора о всеобъемлющем запрещении ядерных испытаний, определяя сейсмическую активность, которая могла бы указывать на ядерный взрыв. Джош Тененбаум из Массачусет- ского технологического института и Ноа Гудман из Стэнфордского университета создали вероятностные программы для моделирования когнитивных способностей человека, они обладают очень неплохими объяснительными возможностями. В компании Charles River Analytics мы использовали вероятностное программирование для вывода заключений о компонентах вредоносного ПО и определения их эволюции. Но я полагаю, что все это - только вершина айсберга. Системы вероятностного программирования достигли такого уровня, что могут использоваться
Глава 1. О вероятностном программировании в двух словах гораздо большим числом специалистов для принятия решений в своих предметных областях. Прочитав эту книгу, вы получите шанс присоединиться к новой технологии, пока не стало слишком поздно. 1.3. Введение в Figaro, язык вероятностного программирования В этой книге мы будем работать с системой вероятностного программирования Figaro. (Я назвал ее в память персонажа оперы Моцарта «Женитьба Фигаро». Я люблю Моцарта и играл доктора Бартоло в Бостонской постановке этой оперы.) Главная цель книги - научить читателя принципам вероятностного программирования, так чтобы изученные методы можно было перенести на другие системы, некоторые из которых перечислены в приложении В. Но есть и другая цель - приобрести практический опыт создания вероятностных программ и предоставить инструменты, которыми можно воспользоваться немедленно. Поэтому многие примеры конкретизированы с помощью кода на Figaro. Вероятностная модель имеет вид набора структур данных на Figaro, называемых элементами Элементы соответствуют переменным модели, например росту центрального нападающего или исходу углового удара Для создания элементов вы пишете код на Scala Вы осуществляете вывод, прогоняя один из имеющихся в Figaro алгоритмов вывода для своей модели, подавая ему на вход факты Вывод производится путем вызова функции, написанной на Scala Ответ имеет вид набора вероятностей различных значений элементов Figaro ' Вероятностная модель Элементы Figaro; Scala V Г Алгоритм вывода Алгоритмы Figaro; Scala Ответы Факты — это информация о значениях элементов Факты Запросы Запрашивается информация о значениях элементов Рис. 1 -9. Как в Figaro используется язык Scala для построения системы вероятностного программирования 40
1.3. Введение в Figaro, язык вероятностного программирования Исходный код Figaro открыт, поддержка осуществляется с помощью GitHub, а сама система разрабатывается с 2009 года. Она реализована в виде библиотеки, написанной на Scala. На рис. 1.9 показано, как в Figaro используется Scala для реализации системы вероятностного программирования. Это уточнение рис. 1.7, где описаны основные компоненты такой системы. Начнем с вероятностной модели. В Figaro модель состоит из структур данных, называемых элементами. Каждый элемент представляет переменную, которая может принимать произвольное число значений. Структуры данных реализованы на Scala, и для создания модели, в которой они используются, вы пишете программу на Scala. Программе можно подать на вход факты, содержащие информацию о значениях элементов, а в запросе указать, какие выходные элементы вас интересуют. Вы выбираете один из встроенных в Figaro алгоритмов выбора и применяете его к своей модели, чтобы получить ответ на запрос при заданных фактах. Алгоритмы вывода реализованы на Scala, а применение алгоритм сводится к вызову функции Scala. Результатом вывода являются вероятности различных значений элементов, указанных в запросе. Тот факт, что Figaro написана на Scala, дает ряд преимуществ. Некоторые из них связаны с использованием языка общего назначения вместо специализированного, другие - со спецификой именно Scala. Сначала остановимся на преимуществах вложения в язык общего назначения. • Факты можно представлять, используя программу на объемлющем языке. Например, программа может прочитать данные из файла, каким-то образом обработать их и представить в виде фактов для модели Figaro. На специализированном независимом языке сделать это было бы гораздо труднее. • Аналогично ответы, возвращенные Figaro, можно использовать в программе. Например, если менеджер футбольной команды работает с некоторой программой, то эта программа может запросить вероятность гола с тем, чтобы порекомендовать менеджеру варианты действий. • Код на языке общего назначения можно включить в вероятностную программу. Рассмотрим, к примеру, физическую модель траектории мяча после удара головой. Эту модель можно было бы сделать частью элемента Figaro. • Для построения модели Figaro можно использовать стандартные приемы программирования. Например, можно завести словарь, который содержит элементы Figaro, соответствующие всем игрокам команды, и выбирать из него элементы, исходя из ситуации, складывающейся на поле. Теперь перечислим причины, по которым Scala особенно хорошо подходит на роль объемлющего языка для системы вероятностного программирования. Поскольку Scala - функциональный язык программирования, Figaro также получает все преимущества функционального программирования, что позволяет естественно записывать многие модели. Я продемонстрирую это во второй части книги. Scala - объектно-ориентированный язык, и то, что он сочетает в себе функциональные и объектно-ориентированные черты, является дополнительным преимуществом. Figaro также является объектно-ориентированным. Во второй части 41
Глава 1. О вероятностном программировании в двух словах я покажу, что это полезно для выражения некоторых паттернов проектирования в вероятностном программировании. Наконец, ряд преимуществ Figaro не связан с вложением в Scala. • На Figaro можно представить чрезвычайно широкий спектр вероятностных моделей. Элементы Figaro могут принимать значения любого типа, в том числе: булевы, целые, с двойной точностью, массивы, деревья, графы и т. д. Связи между элементами можно определить с помощью произвольной функции. • Figaro предоставляет развитые средства задания фактов с помощью условий и ограничений. • В Figaro имеется обширный набор алгоритмов вывода. • Figaro позволяет представлять динамические модели ситуаций, изменяющихся во времени, и рассуждать о них. • Figaro дает возможность включать в модель явные решения и поддерживает вывод оптимальных решений. Использование Scala Поскольку система Figaro реализована в виде библиотеки на Scala, для работы с ней необходимы практические навыки работы со Scala. Эта книга посвящена вероятностному программированию, поэтому я не ставил себе задачу научить Scala. Для этой цели есть немало замечательных ресурсов, например Школа Scala в Твиттере (http:// twitter.github.io/scala_school). Но на случай, если вы пока не уверены в своих познаниях, я буду объяснять используемые средства Scala по ходу дела. Вы сможете читать книгу, даже если совсем не знаете Scala. Чтобы пользоваться всеми преимуществами вероятностного программирования и Figaro, не нужно знать Scala в совершенстве. В этой книге я избегал продвинутых и малопонятных возможностей Scala. С другой стороны, чем лучше вы знаете Scala, тем больших успехов достигнете в работе с Figaro. Возможно даже, что после прочтения этой книги вы станете лучше владеть Scala. Есть несколько причин, по которым Figaro - удобный язык для изучения вероятностного программирования. • Будучи реализован в виде библиотеки на Scala, Figaro может быть использован в программах, написанных на Java и Scala, что упрощает его интеграцию с приложениями. • По той же причине Figaro позволяет использовать для построения моделей все богатство объемлющего языка. Scala - современный передовой язык программирования, обладающий множеством полезных средств для организации программ, и все это вы автоматически получаете в свое распоряжение, работая с Figaro. • Figaro располагает полным набором алгоритмов. В этой книге упор сделан на практические приемы и примеры. Всюду, где возможно, я объясняю общие принципы моделирования и описываю, как они реали- 42
1.3. Введение в Figaro, язык вероятностного программирования зованы в Figaro. Это сослужит вам добрую службу, если вы решите обратиться к другой системе вероятностного программирования. Не во всех системах описанные здесь приемы реализуются с одинаковой легкостью. Так, в настоящее время еще мало объектно-ориентированных систем вероятностного программирования. Но обладая фундаментальными знаниями, вы найдете способ выразить свои потребности на выбранном языке. 7.3.7. Figaro и Java: построение простой системы вероятностного программирования Для иллюстрации достоинств вероятностного программирования и языка Figaro я покажу два способа написания простого приложения: сначала на языке Java, с которым вы, возможно, знакомы, а потом на Figaro. Хотя у Scala есть некоторые преимущества по сравнению с Java, не в этом состоит различие, которое я хочу подчеркнуть. Основная идея в том, что Figaro содержит такие средства для представления вероятностных моделей и выполнения вывода из них, которые недоступны без вероятностного программирования. Наше скромное приложение будет играть роль примера «Hello World» для Figaro. Представьте, что человек просыпается утром, смотрит, ясно ли на улице, и произносит приветствие, зависящее от погоды. Так происходит два дня подряд. Кроме того, погода во второй день зависит от первого: если в первый день было ясно, то вероятность, что и на завтра будет солнечно, повышается. Эти предложения на естественном языке можно записать количественно, как показано в табл. 1.1. Таблица 1.1. Количественные вероятности в примере «Hello World» Ясно Пасмурно Погода сегодня 0.2 0.8 Приветствие сегодня Если сегодня на улице ясно Если сегодня на улице пасмурно Если сегодня на улице ясно Если сегодня на улице пасмурно «Здравствуй, мир!» «Здравствуй, вселенная!» «Здравствуй, мир!» «О нет, только не это» Погода завтра Ясно Пасмурно Ясно Пасмурно 0.6 0.4 0.2 0.8 0.8 0.2 0.05 0.95 43
Глава 1. О вероятностном программировании в двух словах Если завтра на улице ясно Если завтра на улице пасмурно Приветствие завтра «Здравствуй, мир!» «Здравствуй, вселенная!» «Здравствуй, мир!» «О нет, только не это» 0.6 0.4 0.2 0.8 В следующих главах будет точно объяснено, как интерпретировать эти числа. А пока достаточно интуитивного понимания того, что означает фраза «сегодня будет ясно с вероятностью 0.2». Аналогично, если завтра на улице будет ясно, то с вероятностью 0.6 будет произнесено приветствие «Здравствуй, мир!» и с вероятностью 0.4 - «Здравствуй, вселенная!». Наметим три задачи, которые должна решать эта модель. В разделе 1.1.3 мы видели три типа рассуждений, доступных вероятностной модели: предсказание будущего, вывод прошлых событий, приведших к наблюдаемому результату, и обучение на прошлом опыте для лучшего предсказания будущего. Все это мы сделаем с помощью нашей простой модели. Точнее, задачи формулируются следующим образом. 1. Предсказать сегодняшнее приветствие. 2. Зная, что сегодня было произнесено приветствие «Здравствуй, мир!», сделать вывод о том, ясно ли на улице. 3. Зная, что сегодня было произнесено приветствие «Здравствуй, мир!», научиться предсказывать завтрашнее приветствие. Посмотрим, как решить эти задачи на Java. Листинг 1,1, Программа Hello World на Java class HelloWorldJava { static String greetingl static String greeting2 static String greeting3 "Здравствуй, мир!"; "Здравствуй, вселенная!"; "О нет, только не это"; <ь-о static Double pSunnyToday = 0.2; static Double pNotSunnyToday = 0.8; static Double pSunnyTomorrowIfSunnyToday = 0.8; static Double pNotSunnyTomorrowIfSunnyToday = 0.2; static Double pSunnyTomorrowIfNotSunnyToday = 0.05; static Double pNotSunnyTomorrowIfNotSunnyToday = 0.95; static Double pGreetinglTodaylfSunnyToday = 0.6; static Double pGreeting2TodayIfSunnyToday = 0.4; static Double pGreetinglTodaylfNotSunnyToday = 0.2; static Double pGreeting3IfNotSunnyToday = 0.8; static Double pGreetinglTomorrowIfSunnyTomorrow = 0.5; static Double pGreeting2TomorrowIfSunnyTomorrow = 0.5; static Double pGreetinglTomorrowIfNotSunnyTomorrow = 0.1; static Double pGreeting3TomorrowIfNotSunnyTomorrow = 0.95; <-e 44
1.3. Введение в Figaro, язык вероятностного программирования static void predict() { Double pGreetinglToday = pSunnyToday * pGreetinglTodaylfSunnyToday + pNotSunnyToday * pGreetinglTodayIfNotSunnyToday; System.out.printIn("Сегодня будет приветствие " + greetingl + " с вероятностью " + pGreetinglToday + "."); } static void infer() { Double pSunnyTodayAndGreetinglToday = pSunnyToday * pGreetinglTodaylfSunnyToday; Double pNotSunnyTodayAndGreetinglToday = pNotSunnyToday * pGrееtinglTodayIfNotSunnyToday; Double pSunnyTodayGivenGreetinglToday = pSunnyTodayAndGreetinglToday / (pSunnyTodayAndGreetinglToday + pNotSunnyTodayAndGreetinglToday); System.out.printIn("Если сегодня произнесено приветствие " + greetingl + ", то сегодня ясно с вероятностью " + pSunnyTodayGivenGreetinglToday + "."); } static void learnAndPredict() { Double pSunnyTodayAndGreetinglToday = pSunnyToday * pGreetinglTodaylfSunnyToday; Double pNotSunnyTodayAndGreetinglToday = pNotSunnyToday * pGreetinglTodayIfNotSunnyToday; Double pSunnyTodayGivenGreetinglToday = pSunnyTodayAndGreetinglToday / (pSunnyTodayAndGreetinglToday + pNotSunnyTodayAndGreetinglToday); Double pNotSunnyTodayGivenGreetinglToday = 1 - pSunnyTodayGivenGreetinglToday; Double pSunnyTomorrowGivenGreetinglToday = pSunnyTodayGivenGreetinglToday * pSunnyTomorrowIfSunnyToday + pNotSunnyTodayGivenGreetinglToday * pSunnyTomorrowlfNotSunnyToday; Double pNotSunnyTomorrowGivenGreetinglToday = 1 - pSunnyTomorrowGivenGreetinglToday; Double pGreetinglTomorrowGivenGreetinglToday = pSunnyTomorrowGivenGreetinglToday * pGreetinglTomorrowIfSunnyTomorrow + pNotSunnyTomorrowGivenGreetinglToday * pGreetinglTomorrowIfNotSunnyTomorrow; System.out.printIn("Если сегодня произнесено приветствие " + greetingl + ", то завтра будет сказано " + greetingl + " с вероятностью " + pGreetinglTomorrowGivenGreetinglToday); } public static void main(String[] args) { predict(); infer () ; learnAndPredict(); 45
Глава 1. О вероятностном программировании в двух словах О — Определяем приветствия © — Задаем числовые параметры модели © — Предсказываем сегодняшнее приветствие, применяя правила вероятностного вывода © — Из того, что было произнесено приветствие «Здравствуй, мир!», выводим сегодняшнюю погоду, применяя правила вероятностного вывода © — Из того, что было произнесено приветствие «Здравствуй, мир!», обучаемся предсказывать завтрашнее приветствие, применяя правила вероятностного вывода © — Метод main, который выполняет все задачи Не буду здесь описывать, как выполняются вычисления с применением правил вероятностного вывода. В коде используются три правила вывода: цепное правило, правило полной вероятности и правило Байеса. Все они будут объяснены в главе 9. А пока выделим две серьезные проблемы в этой программе. • Не существует способа определить структуру модели. Определение модели содержится в списке имен переменных, принимающих значения типа double. Когда в начале этого раздела я описывал модель и приводил числа в табл. 1.1, структура модели была очевидна и более-менее понятна, пусть и на интуитивном уровне. У списка же переменных нет вообще никакой структуры. Назначение переменных закодировано только в их именах, что нельзя назвать хорошей идеей. Следовательно, написать модель таким образом трудно, и этот путь чреват ошибками. К тому же, написанный код трудно читать и сопровождать. Если потребуется модифицировать модель (например, сделать так, чтобы приветствие зависело еще и от того, хорошо ли вы спали), то, скорее всего, придется переписать большие куски кода. • Самостоятельно кодировать правила вывода трудно и чревато ошибками. Вторая серьезная проблема связана с кодом, в котором для ответа на запросы используются правила вероятностного вывода. Чтобы написать такой код, необходимо хорошо знать правила вывода. Даже если такие знания имеются, написать код правильно все равно тяжело. И проверить, получен ли правильный ответ, тоже трудно. А ведь это очень простой пример. В сложном приложении написать код рассуждений подобным способом вообще нереально. А теперь познакомимся с кодом, написанным на Scala/Figaro. Листинг 1 -2, Программа Hello World на Figaro import com.era.figaro.language.{Flip, Select} import com.era.figaro. library. compound. If import com.era.figaro.algorithm.factored.VariableElimination <M> object HelloWorld { val sunnyToday = Flip(0.2) © ^ val greetingToday = If(sunnyToday, Select (0.6 -> "Здравствуй, мир!", 0.4 -> "Здравствуй, вселенная!"), 46
1.3. Введение в Figaro, язык вероятностного программирования Select (0.2 -> "Здравствуй, мир!", 0.8 -> "О нет, только не это")) val sunnyTomorrow = If(sunnyToday, Flip(0.8), Flip(0.05)) val greetingTomorrow = If(sunnyTomorrow, Select (0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"), Select (0.2 -> "Hello, world!", 0.8 -> "Oh no, not again")) def predict() { val result = VariableElimination.probability(greetingToday, "Здравствуй, мир!") printIn("Сегодня будет приветствие \"Здравствуй, мир!\" " + 'с вероятностью + result + ') ©-> <ь-® def infer () { Q [> greetingToday.observe("Здравствуй, мир!") val result = VariableElimination.probability(sunnyToday, true) println("Если сегодня произнесено приветствие \"3дравствуй, мир\", " + "то будет солнечно с вероятностью " + result + ".") } def learnAndPredict() { greetingToday.observe("Здравствуй, мир!") val result = VariableElimination.probability(greetingTomorrow, "Hello, world!") println("Если сегодня произнесено приветствие \"3дравствуй, мир!\", "то завтра будет сказано \"Здравствуй, мир!\" " + 0-> 'с вероятностью + result + ') def main(args: Array[String]) predict() infer () learnAndPredict() } <!-© О — Импортируем конструкции Figaro © — Определяем модель © — Предсказываем сегодняшнее приветствие, применяя алгоритм вывода © — Применяем алгоритм вывода, чтобы вывести сегодняшнюю погоду из того, что сегодня было произнесено приветствие «Здравствуй, мир!» © — Применяя алгоритм вывода, обучаемся предсказывать завтрашнее приветствие, зная, что сегодня было произнесено «Здравствуй, мир!» © — Метод main, который выполняет все задачи Я подробно объясню этот код в следующей главе. А пока хочу отметить, что он решает обе проблемы, встретившиеся в программе на Java. Во-первых, определение модели точно описывает ее структуру - в полном соответствии с табл. 1.1. Мы определяем четыре переменные: sunnyToday, greetingToday, sunnyTomorrow И greetingTomorrow, Как ОПИСанО В табл. 1.1. Вот, например, определение greetingToday! val greetingToday = If(sunnyToday, Select (0.6 -> "Здравствуй, мир!", 0.4 -> "Здравствуй, вселенная!"), Select(0.2 -> "Здравствуй, мир!", 0.8 -> "О нет, только не это")) 47
Глава 1. О вероятностном программировании в двух словах Здесь говорится, что если сегодня ясно, то с вероятностью 0.6 будет произнесено приветствие «Здравствуй, мир!» и с вероятностью 0.4 - «Здравствуй, вселенная!». Если же сегодня пасмурно, то с вероятностью 0.2 будет сказано «Здравствуй, мир!» и с вероятностью 0.8 - «О нет, только не это». Это в точности то же самое, что говорит табл. 1.1 о сегодняшнем приветствии. Поскольку код явно описывает модель, то писать, читать и сопровождать его гораздо проще. А если потребуется изменить модель (например, добавить переменную sieepQuaiity), то это можно будет сделать модульно. Теперь рассмотрим код задач рассуждения. В нем нет никаких вычислений, а просто создается экземпляр объекта-алгоритма (в данном случае алгоритма исключения переменных, одного из встроенных в Figaro), и у него запрашивается искомая вероятность. В части 3 будет показано, что этот алгоритм основан на тех же правилах вывода, что и алгоритм из программы на Java. Всю сложную работу по организации и применению правил вывода берет на себя алгоритм. Даже если модель велика и сложна, можно прогнать алгоритм, и он отлично справится с выводом. 1.4. Резюме • Для субъективных суждений нужны знания + логика. • В системах вероятностных рассуждений вероятностная модель выражает знания, а алгоритм вывода инкапсулирует логику. • Вероятностные рассуждения можно использовать для предсказания будущих событий, для вывода причин произошедших событий и обучения на прошлом опыте с целью улучшения предсказаний. • Вероятностное программирование - это вероятностное рассуждение, в котором вероятностная модель выражена на языке программирования. • В системе вероятностного программирования используется полный по Тьюрингу язык программирования для представления моделей и включены алгоритмы вывода для работы с этими моделями. • Figaro - это система вероятностного программирования, реализованная на языке Scala, предлагающем функциональный и объектно-ориентированный стили программирования. 1.5. Упражнения Решения избранных упражнений имеются на сайте www.manning.com/books/ practical-probabilistic-programming. 1. Пусть требуется использовать систему вероятностных рассуждений для рассуждения об исходе сдачи карт в покере. a. Какие общие знания вы закодировали бы в этой модели? b. Опишите, как можно воспользоваться системой для предсказания будущего. Что в этом случае является фактами? А что - запросом? 48
1.5. Упражнения c. Опишите, как бы вы использовали систему для вывода причин наблюдаемого результата. Что является фактами и что - запросом? d. Опишите, как выведенные причины произошедшего события могут улучшить будущие предсказания. 2. В примере «Hello World» измените вероятность ясной погоды сегодня, как показано в таблице ниже. Как изменится результат работы программы? Почему вы так думаете? Погода сегодня Ясно Пасмурно 0.9 0.1 Добавьте в пример «Hello World» еще одно приветствие «Здравствуй, галактика!». Назначьте ему некоторую вероятность при ясной погоде, не забыв уменьшить вероятности других приветствий, так чтобы сумма вероятностей осталась равной 1. Кроме того, модифицируйте программу, так чтобы там, где раньше ответом на запрос было приветствие «Здравствуй, мир!», теперь печаталось «Здравствуй, галактика!». Попробуйте сделать это для обеих версий программы: на Java и на Figaro. Сравните трудоемкость. 49
ГЛАВА 2. Краткое руководство по языку Figaro В этой главе. ■ Создание моделей, задание фактов, прогон алгоритма вывода и получение ответов на запросы. ■ Основные строительные блоки моделей. ■ Построение сложных моделей из строительных блоков. Итак, мы узнали, что такое вероятностное программирование и теперь готовы ближе познакомиться с системой Figaro, начать писать простые программы и получать от них ответы на свои запросы. В этой главе я хочу как можно быстрее ввести вас в курс основных идей Figaro. А в последующих главах подробно объясню, что означает модель и как ее следует интерпретировать. Приступим. 2.1. Введение в Figaro Для начала взглянем на Figaro с высоты птичьего полета. Как было сказано в главе 1, Figaro - это система вероятностных рассуждений. Но прежде чем рассматривать ее компоненты, вспомним о компонентах общей системы вероятностных рассуждений, чтобы была основа для сопоставления с Figaro. На рис. 2.1 повторена самая суть системы вероятностных рассуждений. Напомню, что в вероятностной модели закодированы общие знания о ситуации, а факты сообщают информацию о конкретной ситуации. Алгоритм вывода пользуется моделью и фактами для получения ответов на запросы о ситуации.
2.1. Введение в Figaro Факты содержат информацию о конкретной ситуации В запросе описывается, что мы хотим узнать Ответы на запросы представлены в виде вероятностей различных исходов Факты Запросы- Ответы Система вероятностных рассуждений ' N Вероятностная модель v > Алгоритм вывода Вероятностная модель выражает общие знания о ситуации Алгоритм вывода пользуется моделью для получения ответов на запросы при заданных фактах Рис. 2.1. Обзор основных частей системы вероятностных рассуждений Теперь обратимся к Figaro. На рис. 2.2 показаны его основные концепции. Как видим, компоненты те же самые, что на рис. 2.1. Общие знания выражаются в виде модели Figaro. Знания о конкретной ситуации представлены в виде фактов. Запрос говорит системе, что мы хотим узнать. Алгоритм вывода Figaro принимает запрос и использует модель для получения ответов на запросы. Факты включают наблюдения, условия и ограничения на значения элементов Г Факты Замечания Условия Ограничения Г Запросы Целевые элементы В запросах указываются целевые элементы Модель Figaro состоит из набора структур данных, называемых элементами Г Модель Figaro Атомарные элементы Составные элементы Apply Chain Алгоритмы вывода Figaro Instantiate Run Clean up r Ответы Вероятности значений Наиболее вероятные значения Атомарные элементы являются базовыми строительными блоками Составные элементы связывают более простые элементы Apply и Chain — важные примеры составных элементов Алгоритмы Figaro вычисляют информацию о запрошенных элементах Обычно экземпляр алгоритма создается, исполняется и в конце уничтожается В Figaro имеются методы для выполнения всех перечисленных шагов В ответах содержится информация о целевых элементах Рис. 2.2. Взаимосвязь основных концепций Figaro 51
Глава 2. Краткое руководство по языку Figaro Рассмотрим каждую часть поочередно. Большую часть интерфейса Figaro занимают различные способы спецификации моделей. Модель Figaro состоит из набора структур данных, которые называются элементами. Каждый элемент представляет одну из переменных ситуации и может принимать одно из множества значений. В элементе закодирована информация, определяющая вероятности различных событий. В примере 2.2.1 мы увидим базовое определение элементов в контексте примера «Hello World». Элементы бывают двух видов: атомарные и составные. Можно считать, что Figaro - это конструктор для сборки моделей. Атомарные элементы - это основные строительные блоки, они представляют базовые вероятностные переменные, которые не зависят от других элементов. Атомарные элементы обсуждаются в разделе 2.3, где приведены различные примеры. Составные элементы - это соединители. Из одного или нескольких элементов они конструируют более сложные элементы. Подробнее о составных элементах вы узнаете в разделе 2.4. В Figaro много составных элементов, но два - Apply и Chain - особенно важны, речь о них пойдет в разделе 2.5. Переходим к фактам. В Figaro имеется развитый механизм для задания фактов. Чаще всего вы будете использовать простейшую форму фактов - наблюдение. Наблюдение говорит, что значение элемента известно и равно заданному. Как специфицируются наблюдения, рассказано в разделе 2.2.3. Иногда необходим более общий способ задания фактов. Для этой цели в Figaro имеются условия и ограничения. То и другое рассматривается в разделе 2.6. В запросах указывается, какие целевые элементы представляют интерес и что мы хотим о них знать. Для получения информации о целевых элементах используется алгоритм вывода, которому передаются факты. Как правило, необходимо создать экземпляр алгоритма, выполнить его и в конце уничтожить. Я предоставил простые методы, которые выполняют все эти шаги с подразумеваемыми по умолчанию параметрами. После прогона алгоритма можно получить ответы на запросы. Чаще всего ответы имеют вид вероятностей значений целевых элементов. Иногда вместо вероятностей сообщаются наиболее вероятные значения каждого целевого элемента, т. е. значения с наибольшей вероятностью. В разделе 2.2.2 показано, как задавать запросы, выполнять алгоритмы и получать ответы. 2.2. Создание модели и выполнение алгоритма вывода на примере Hello World Познакомившись с основными понятиями Figaro, посмотрим, как они взаимосвязаны. Вернемся к примеру «Hello World» из главы 1 и покажем, как в нем материализуются концепции, показанные на рис. 2.2. Мы увидим, как построить модель из атомарных и составных элементов, задать наблюдаемые факты, сформулировать запрос, прогнать алгоритм вывода и получить ответ. 52
2.2. Создание модели и выполнение алгоритма вывода ... Выполнить код из этой главы можно двумя способами. Первый - воспользоваться консолью Scala и вводить в ней предложения последовательно, сразу же получая результаты. Для этого зайдите в корневой каталог проекта PracticalProbProg/examples и наберите sbt console. Вы увидите приглашение Scala. После этого можете вводить строки приведенного кода и наблюдать за реакцией системы. Второй способ традиционный: написать программу, содержащую метод main. В этой главе я не буду описывать стереотипный способ превращения кода в исполняемую программу Здесь приведен лишь код, имеющий отношение к Figaro. Но я обязательно отмечу, что необходимо импортировать и откуда это берется. 2.2.7. Построение первой модели Начнем с построения простейшей модели Figaro, в которой будет всего один атомарный элемент. Но прежде необходимо импортировать необходимые конструкции Figaro: import com.cra.figaro. language._ В результате ИМПОрТИруЮТСЯ ВСе КЛаССЫ ИЗ пакета com.cra.figaro. language, KO- торый содержит базовые конструкции Figaro. Один из этих классов называется Flip. С его помощью можно построить простую модель: val sunnyToday = Flip(0.2) Эта строка объясняется на рис. 2.3. Важно четко понимать, что относятся к Scala, а что - к Figaro. В этой строке мы создали переменную Scala sunnyToday и присвоили ей значение Flip(0.2). Значение Flip(0.2) - это элемент Figaro, представляющий случайный процесс, который порождает значение true с вероятностью 0.2 и false - с вероятностью 0.8. Элемент - это структура данных, представляющая процесс, который случайным образом порождает некоторое значение. Число исходов случайного процесса может быть произвольным. Каждый возможный исход называется значением процесса. Таким образом, Flip (0.2) - элемент с двумя возможными булевыми значениями: true и false. Итак, мы имеем переменную Scala, принимающую значение в смысле Scala. Это значение является элементом Figaro и представляет различные возможные исходы процесса. Scala: Figaro: sunnyToday Переменная \ Элемент - _^ - Flip(0.2) Значение -"^ * ^--Возможные значения true false «-^ Рис. 2.3. Связь между переменными и значениями Scala и элементами Figaro и их возможными значениями 53
Глава 2. Краткое руководство по языку Figaro В Scala тип может быть параметризован другим типом, описывающим его содержимое. Возможна, эта идея знакома вам по универсальным типам Java, где можно, например, определить список целых чисел или список строк. Все элементы Figaro являются экземплярами класса Element, который параметризуется типом значения элемента. Поскольку элемент Flip (0.2) может принимать булевы значения, то типом его значения является Boolean. Формально говоря, Flip (0.2) является Экземпляром Класса Element [Boolean]. Основные определения Элемент- структура данных Figaro, представляющая случайный процесс. Значение - возможный исход случайного процесса. Тип значения - тип Scala, представляющий возможные значения элемента. Как много слов для такой простой модели! Но, к счастью, все, что вы сейчас узнали, относится ко всем моделям Figaro. В Figaro модели создаются из простых элементов - строительных блоков, которые комбинируются в более сложные элементы и наборы взаимосвязанных элементов. Прежде чем переходить к более сложным моделям, посмотрим, как модель позволяет проводить рассуждения. 2.2.2. Выполнение алгоритма вывода и получение ответа на запрос Итак, мы построили простую модель. Теперь прогоним алгоритм вывода и запросим вероятность того, что переменная sunnyToday равна true. Сначала нужно импортировать используемый алгоритм вывода: import com.era.figaro.algorithm.factored.VariableElimination Здесь импортируется алгоритм исключения переменных, относящийся к классу точных алгоритмов, т. е. он точно вычисляет вероятности, исходя из модели и фактов. Вероятностный вывод - сложная задача, поэтому иногда точные алгоритмы работают очень долго или им не хватает памяти. Figaro предлагает также приближенные алгоритмы, которые обычно вычисляют ответы, близкие к точным. Поскольку все модели в этой главе простые, то алгоритм исключения переменных будет работать без проблем. В Figaro имеется команда, позволяющая за одно действие задать запрос, выполнить алгоритм и получить ответ: println(VariableElimination.probability(sunnyToday, true)) Эта команда напечатает 0.2. Наша модель состоит только из элемента Flip (0.2), который принимает значение true с вероятностью 0.2. Алгоритм исключения переменных правильно вычисляет вероятность, что sunnyToday равно true, - она равна 0.2. 54
2.2. Создание модели и выполнение алгоритма вывода ... щи 1*1 Еще раз поясним, что показанная выше команда выполняет несколько операций. Сначала она создает экземпляр алгоритма исключения переменных. Затем сообщает этому экземпляру, что целевым элементом является sunnyToday. Далее она выполняет алгоритм и возвращает вероятность того, что значение sunnyToday равно true. Команда также подчищает за собой, т. е. освобождает ресурсы, захваченные алгоритмом. 2.2.3. Построение моделей и задание наблюдений Теперь приступим к построению более интересной модели. Нам понадобится конструкция Figaro if, поэтому импортируем ее. Еще будет нужна конструкция select, НО Она уже импортирована В СОСТаве пакета com. era . figaro . language! import com.era.figaro.library.compound.If Воспользуемся элементами if и select для построения более сложного элемента: val greetingToday = If(sunnyToday, Select (0.6 -> "Здравствуй, мир!", 0.4 -> "Здравствуй, вселенная!"), Select (0.2 -> "Здравствуй, мир!", 0.8 -> "О нет, только не это")) Мы интерпретируем элемент как случайный процесс. В таком случае элемент greetingToday представляет процесс, КОТОрыЙ Сначала проверяет Значение sunnyToday. Если это значение равно true, то он выбирает приветствие «Здравствуй, мир!» с вероятностью 0.6, а приветствие «Здравствуй, вселенная!» с вероятностью 0.4. Если же значение sunnyToday равно false, то «Здравствуй, мир!» выбирается с вероятностью 0.2, а «О нет, только не это» с вероятностью 0.8. Элемент greeting- today составной, потому что он построен из трех элементов. Так как возможные значения greetingToday - строки, то этот элемент является экземпляром класса Element[String]. Теперь допустим, что сегодня мы наблюдали приветствие «Здравствуй, мир!». Этот факт можно задать в виде наблюдения следующим образом: greetingToday.observe("Здравствуй, мир!") Мы можем найти вероятность того, что сегодня ясно, зная, какое было приветствие: println(VariableElimination.probability(sunnyToday, true)) Печатается 0.4285714285714285. Отметим, что этот ответ значительно больше предыдущего (0.2). Причина в том, что приветствие «Здравствуй, мир!» гораздо вероятнее, если сегодня ясно, поэтому факт говорит в пользу того, что сегодня действительно ясно. Этот вывод - простой пример применения правила Байеса, о котором мы узнаем в главе 9.
Глава 2. Краткое руководство по языку Figaro Далее мы разовьем эту модель и выполним дополнительные запросы с другими фактами, НО Предварительно удаЛИМ Наблюдение ДЛЯ Переменной greetingToday. Это делает такая команда: greetingToday.unobserve() Если теперь выполнить запрос println(VariableElimination.probability(sunnyToday, true)) то получится тот же ответ 0.2, что и раньше, до того как был сообщен этот факт. Прежде чем закончить этот раздел, усложним модель: val sunnyTomorrow = If (sunnyToday, Flip(0.8), Flip(0.05)) val greetingTomorrow = If(sunnyTomorrow, Select (0.6 -> "Здравствуй, мир!", 0.4 -> "Здравствуй, вселенная!"), Select (0.2 -> "Здравствуй, мир!", 0.8 -> "О нет, только не это")) Вычислим вероятность того, что завтра будет произнесено приветствие «Здравствуй, мир!», если известно, какое приветствие было сегодня, и если неизвестно: println(VariableElimination.probability(greetingTomorrow, "Здравствуй, мир!")) // печатается 0.27999999999999997 greetingToday.observe("Здравствуй, мир!!") println(VariableElimination.probability(greetingTomorrow, "Здравствуй, мир!")) // печатается 0.3485714285714286 Как видим, если известно, что сегодня произнесено приветствие «Здравствуй, мир!», то вероятность того, что оно будет произнесено и завтра, возрастает. Почему так? Потому что если сегодня было сказано «Здравствуй, мир!», то высока вероятность, что на улице ясно, а это значит, что с высокой вероятностью и завтра будет ясно, а это, в свою очередь, значит, что и завтра будет сказано «Здравствуй, мир!». В главе 1 мы назвали это выводом прошлого для лучшего предсказания будущего, и Figaro производит все необходимые вычисления самостоятельно. 2.2Л. Анатомия построения модели Итак, мы рассмотрели все шаги создания модели, задания фактов и запросов, выполнения алгоритма и получения ответа. Теперь внимательнее приглядимся к модели «Hello World», чтобы понять, как она конструируется из строительных блоков (атомарных элементов) и соединителей (составных элементов). На рис. 2.4 приведено графическое изображение этой модели. Сначала повторено ее определение, причем каждая переменная Scala показана в отдельном блоке. А в блоках ниже представлены соответствующие элементы модели. Некоторые элементы являются значениями самих переменных Scala. Например, значение переменной sunnyToday представлено элементом Flip (0.2). Если элементом является значение переменной Scala, то в графе показано имя и переменной и элемента. В модели присутствуют также элементы, не являющиеся значениями переменных Scala. Например, ПОСКОЛЬКУ переменная sunnyTomorrow определена Как If (sunnyTo- 56
2.2. Создание модели и выполнение алгоритма вывода day, Flip (0.8), Flip (0 . 05) ), ТО ЭЛвМеНТЫ Flip (0 . 8) И Flip (0.05) ТОЖе ЯВЛЯЮТСЯ частью модели, поэтому они показаны в вершинах графа. val sunnyToday = Flip(0-2) val greetingToday = If(sunnyToday, Select (0.6->"Hello, world!",0^4->MHowdy, universe!"), Select (C^2->"Hello, world! ", 0 . 8->"Oh no, not again") ) val sunnyTomorrow=If(sunnyToday,Flip(0,8), Flip(0.05)) i 1 | val greetingTomorrow = If (sunnyTomorrow, Select (£L_£->"Hello, world!",0.4->"Howdyr universe!"), Select (Q^2->"Hello, world!",QJJ->"0h no, not again")) sunnyToday: Flip(0.2) Select(0,6->"Hello, world!", 0.4->"Howdy, universe!") Select(0.2->"Hello, world!", 0.8->"Oh no, not again") sunnyTomorrow: If( , , ) Select(0.6->"Hello, world!", ч 0.4->"Howdy, universe!") Рис. 2.4. Структура модели «Hello World» в виде графа. Каждая вершина соответствует элементу. Ребра графа показывают, что один элемент используется другим 57 greetingToday: If( , , ) Select{0.2->"Hello, world!", 0,8->"Oh no, not again") greetingTomorrow: If{ , , ) Flip(0.8) Flip(O-OS)
Глава 2. Краткое руководство по языку Figaro Элементы в вершинах графа соединены ребрами. Например, существует ребро, идущее из Flip(0.8) в элемент if, являющийся значением переменной sunnyTo- morrow. Элемент Flip(0.8) используется элементом if. В общем случае ребро из одного элемента в другой существует, если в определении второго элемента используется первый. Поскольку только составные элементы конструируются из других элементов, конечная вершина любого ребра обязательно является составным элементом. 2.2.5. Повторяющиеся элементы: когда они совпадают, а когда различаются? Обратите внимание, ЧТО элемент Select (0.6 -> "Здравствуй, мир!", 0.4 -> "Здравствуй, вселенная!") встречается В Графе ДВаЖДЫ, Как И Элемент Select (0.2 -> "Здравствуй, мир!", 0.8 -> "О нет, только не это"). Причина В ТОМ, ЧТО Само определение дважды встречается в коде, один раз для greetingToday, другой - для greetingTomorrow. Это два разных элемента, хотя их определения одинаковы. Они могут принимать различные значения в ходе одного и того же выполнения случайного процесса, определяемого моделью Figaro. Например, первый экземпляр элемента мог бы принять значение «Здравствуй, мир!», а второй - «Здравствуй, вселенная!». Это имеет смысл, потому что один используется в определении переменной greetingToday, а Другой - В определении greetingTomorrow. Вполне ВОЗМОЖНО, что сегодняшнее и завтрашнее приветствия будут различаться. Тут уместна аналогия с обычным программированием. Пусть имеется такой Класс Greeting! class Greeting { var string = "Здравствуй, мир!" } val greetingToday = new Greeting val greetingTomorrow = new Greeting greetingTomorrow.string = "Здравствуй, вселенная!" ХОТЯ определения переменных greetingToday И greetingTomorrow В ТОЧНОСТИ Совпадают, ЭТО два разных экземпляра Класса Greeting. Поэтому значение greetingTomorrow. string МОЖет ОТЛИЧатьСЯ ОТ Значения greetingToday. string, KOTO- рое по-прежнему равно «Здравствуй, мир!». Точно так же конструкторы Figaro, в частности select, создают новые экземпляры соответствующего класса элемента. Так, greetingToday И greetingTomorrow - два раЗНЫХ экземпляра Элемента Select И потому могут принимать различные значения при одном и том же прогоне. С другой стороны, заметим, что переменная Scala sunnyToday также дважды встречается в коде: один раз в определении greetingToday, другой - в определении sunnyTomorrow. Но Элемент, ЯВЛЯЮЩИЙСЯ Значением sunnyToday, встречается В графе только один раз. Почему? Потому что sunnyToday - переменная Scala, а не определение элемента Figaro. Переменная Scala, несколько раз встречающаяся в коде, - это одна и та же переменная, поэтому ее значение всюду одинаково. В на- 58
2.3. Базовые строительные блоки: атомарные элементы Ш шей МОДеЛИ ЭТО имеет СМЫСЛ: В определениях greetingToday И sunnyTomorrow речь идет о погоде в один и то же день, поэтому при любом случайном выполнении модели значения в обоих случаях должны быть одинаковы. То же самое происходит в обычной программе. Если написать val greetingToday = new Greeting val anotherGreetingToday = greetingToday anotherGreetingToday.string = "Здравствуй, вселенная!" TO anotherGreetingToday - В ТОЧНОСТИ та Же переменная Scala, ЧТО И greetingToday, поэтому после выполнения этого кода greetingToday будет иметь значение «Здравствуй, вселенная!». Аналогично, если одна и та же переменная Scala, представляющая некоторый элемент, встречается в программе несколько раз, то она всюду будет иметь одно и то же значение. Этот момент важно понимать, чтобы правильно конструировать модели Figaro, поэтому я рекомендую перечитать этот подраздел, пока вы не будете уверены, что все усвоили. Теперь у вас должно сложиться общее представление об основных концепциях Figaro и их взаимосвязях. В следующих разделах мы изучим некоторые концепции более детально. И начнем с атомарных элементов. 2.3. Базовые строительные блоки: атомарные элементы Настало время поближе познакомиться с элементами Figaro. Начнем с базовых строительных блоков - атомарных элементов. Слово «атомарный» означает, что элемент не зависит ни от каких других элементов. Не буду здесь приводить полный список атомарных элементов, а ограничусь наиболее употребительными. Атомарные элементы подразделяются на дискретные и непрерывные, в зависимости от типа значения. Значения дискретных элементов имеют такие типы, Как Boolean ИЛИ Integer, а непрерывных - ТИП Double. Технически ДИСКретНОСТЬ означает, что значения достаточно далеко отстоят друг от друга. Например, между целыми числами 1 и 2 нет других целых чисел, поэтому можно сказать, что они отделены друг от друга. С другой стороны, значения непрерывных элементов образуют неразрывный континуум, как, например, вещественные числа. Между любыми двумя вещественными числами найдется еще одно вещественное число. Различие между дискретными и непрерывными элементами существенно для вычисления вероятностей, мы увидим это в главе 4. Предупреждение. Некоторые думают, что дискретный означает конечный. Это не так. Целых чисел бесконечно много, но между любыми двумя соседними имеется промежуток, в котором нет ни одного целого числа, так что они образуют дискретное множество. Основные определения Атомарный элемент - элемент, не зависящий ни от каких других элементов. Составной элемент - элемент, построенный из других элементов.
Глава 2. Краткое руководство по языку Figaro Дискретный элемент - элемент, значения которого отстоят «далеко» друг от друга. Непрерывный элемент- элемент, между значениями которого нет промежутков. 2.3.1. Дискретные атомарные элементы Рассмотрим Примеры ДИСКреТНЫХ аТОМарНЫХ Элементов: Flip, Select И Binomial. Элемент Flip Мы уже встречались с дискретным атомарным элементом Flip. Его код находится в пакете com.era.figaro.language вместе со многими другими широкоупо- требительными элементами. Я рекомендую импортировать этот пакет целиком в начале программы. Элемент Flip принимает один аргумент/? - вероятность того, что значение элемента равно true; р должно быть числом от 0 до 1 включительно. Вероятность того, что элемент принимает значение false, равна 1 - р. Например: import com.era.figaro. language._ val sunnyToday = Flip(0.2) println(VariableElimination.probability(sunnyToday, true)) // печатается 0.2 println(VariableElimination.probability(sunnyToday, false)) // печатается 0.8 Элемент Flip (0.2) Имеет Официальный ТИП AtomicFlip - ПОДКЛаСС Класса Element [Boolean]. Этим он отличается от элемента compoundFiip, который будет рассмотрен ниже. Элемент Select С элементом select мы тоже встречались в программе «Hello World». Вот пример: Select(0.6 -> "Здравствуй, мир!", 0.3 -> "Здравствуй, вселенная!", 0.1 -> "о нет, только не это"). На рис. 2.5 показано, как строится этот элемент. Внутри скобок находится несколько частей. Каждая часть состоит из вероятности, стрелки и возможного исхода. Количество частей произвольно. На рисунке мы видим три части. Поскольку все исходы имеют тип string, то элемент принадлежит классу Element [String]. Официально его ТИПОМ является AtomicSelect [String] - ПОД- КЛаСС Element [String]. Естественно, элемент select соответствует процессу, возможные исходы которого выбираются с указанными вероятностями. Вот как это будет работать для примера на рисунке: val greeting = Select (0.6 -> "Здравствуй, мир!", 0.3 -> "Здравствуй, вселенная!", 0.1 -> "О нет, только не это") println(VariableElimination.probability(greeting, "Здравствуй, вселенная!")) // печатается 0.30000000000000004 60
2.3. Базовые строительные блоки: атомарные элементы III! HI Select (0.6 -> "Hello, world!", 0.3 "Howdy, iverse!", 0Л -> "Oh no, not again1') Возможные исходы Рис. 2.5. Структура элемента select Отметим, что сумма вероятностей в select не обязательно равна 1. Если она отлична от 1, то вероятности нормируются, т. е. умножаются на некоторый (один и тот же) коэффициент, так чтобы получилась сумма 1. В следующем примере вероятности вдвое больше, чем в предыдущем, поэтому их сумма равна 2. Нормировка приводит к таким же вероятностям, как и выше, поэтому результаты будут совпадать. val greeting = Select(1.2 -> "Здравствуй, мир!", 0.6 -> "Здравствуй, вселенная!", 0.2 -> "О нет, только не это") println(VariableElimination.probability(greeting, "Здравствуй, вселенная!")) // печатается 0.30000000000000004 Элемент Binomial Binomial- еще один полезный дискретный элемент. С каждым из семи дней недели может быть связан элемент Flip(0.2), определяющий будет ли в этот день ясно. А нам нужен элемент, значением которого является число ясных дней на неделе. Для этого можно взять элемент Binomial (7, 0.2). Его значением является количество испытаний, результат которых равен true, при том, что всего проводится семь испытаний, и каждое дает true с вероятностью 0.2. Используется этот элемент так: import com.era.figaro.library.atomic.discrete.Binomial val numSunnyDaysInWeek = Binomial(7, 0.2) println(VariableElimination.probability(numSunnyDaysInWeek, 3)) //печатается 0.114688 Элемент Binomial принимает два аргумента: количество испытаний и вероятность того, что испытание даст результат true. В определении Binomial предполагается, что испытания независимы; от того, что первое дало true, вероятность получения true во втором не меняется. 2.3.2. Непрерывные атомарные элементы В этом разделе описаны два распространенных непрерывных элемента: Normal и uniform. В главе 4 непрерывные элементы рассматриваются подробно. Непрерывное распределение вероятности отличается от дискретного тем, что задается не Вероятности Стрелки связывают вероятности с исходами
Глава 2. Краткое руководство по языку Figaro вероятность каждого значения, а плотность вероятности, описывающая вероятность интервала вокруг значения. Тем не менее, можно считать, что плотность вероятности похожа на обычную вероятность, т. е. показывает, насколько данное значение вероятно по сравнению с другими. Поскольку эта глава - учебное пособие по Figaro, то я отложу более подробное обсуждение до главы 4, где рассматриваются вероятностные модели. Но не беспокойтесь, в свое время все станет ясно и понятно. Элемент Normal Вы, наверное, знакомы с нормальным распределением вероятности. У него есть и другие названия, например: колоколообразная кривая или гауссово распределение. На рис. 2.6 показана функция плотности вероятности для нормального распределения. (Строго говоря, это одномерное нормальное распределение\ потому что определено для одной вещественной переменной, а бывают и многомерные нормальные распределения нескольких переменных, но не будем придираться.) У этой функции есть среднее значение - центральная точка (1 на рисунке) - и стандартное отклонение, т. е. степень разброса функции вокруг центральной точки (0.5 на рисунке). Примерно в 68 % процентах случаев значение с нормальным распределением будет отстоять от среднего не далее, чем на одно стандартное отклонение. В статистике и в вероятностных рассуждениях нормальное распределение обычно задается средним и дисперсией, равной квадрату стандартного отклонения. Поскольку на рисунке ниже стандартное отклонение равно 0.5, то дисперсия равна 0.25. Следовательно, это нормальное распределение задается как Normal а, 0.25). Normal (1,0.25) 0.9 п Среднее Рис- 2.6. Функция плотности вероятности нормального распределения 62
2.3. Базовые строительные блоки: атомарные элементы El Figaro точно следует этому соглашению. Он предоставляет элемент Normal, принимающий в качестве аргументов среднее и дисперсию, например: import com.era.figaro.1ibrary.atomic.continuous.Normal val temperature = Normal(40, 100) Здесь среднее равно 40, а дисперсия 100, т. е. стандартное отклонение равно 10. Предположим теперь, что мы хотим выполнить вывод с помощью этого элемента. Увы, алгоритм исключения переменных в Figaro работает только для элементов, принимающих конечное число значений, а, значит, к непрерывным элементам он неприменим. Поэтому придется взять другой алгоритм. Мы воспользуемся приближенным алгоритмом выборки по значимости, который хорошо работает с непрерывными элементами. Выполнить этот алгоритм можно так: import com.era.figaro.algorithm.sampling.Importance def greaterThan50(d: Double) = d > 50 println(Importance.probability(temperature, greaterThan50 _)) Выборка по значимости - это рандомизированный алгоритм, который каждый раз дает новый ответ, близкий к истинному значению - в данном случае 0.1567. Отметим, что запрос немного отличается от того, что мы видели раньше. Вероятность, что значение непрерывного элемента будет в точности равно заданному, например 50, очень близка к 0, поскольку значений бесконечно много, и промежутков между ними нет. Шансы на то, что процесс вернет значение, равное точно 50, а не, скажем, 50.000000000000001, бесконечно малы. Поэтому мы обычно не запрашиваем у непрерывного элемента вероятность точного значения. Вместо этого указывается диапазон вероятностей. В данном случае в запросе задан предикат greaterThanSO, который принимает в качестве аргумента значение типа Double и возвращает true, если аргумент больше 50. Предикатом называется булева функция от значения элемента. Спрашивая, удовлетворяет ли элемент предикату, мы на самом деле интересуемся, с какой вероятностью применение предиката к значению элемента вернет true. В этом примере запрос вычисляет вероятность того, что температура больше 50. Замечание о Scala. Знак подчеркивания после greaterThanSO говорит Scala, что greaterThanSO следует рассматривать как функциональное значение, передаваемое методу importance .probability. Без подчеркивания интерпретатор Scala мог бы подумать, что мы пытаемся вызвать функцию без аргументов, а это ошибка. Иногда Scala может разобраться в такой ситуации автоматически, даже если подчерка нет, а иногда сдается и просит добавить подчерк. Элемент Uniform Рассмотрим еще элемент uniform (равномерное распределение), который возвращает значения в указанном диапазоне с равной вероятностью:
Глава 2. Краткое руководство по языку Figaro import com.era.figaro.library.atomic.continuous.Uniform val temperature = Uniform(10, 70) Importance.probability(temperature, greaterThan50 _) // печатается число, близкое к 0.3334 Элемент uniform принимает два аргумента: начало и конец диапазона. Плотность вероятности всех значений в этом диапазоне одинакова. В примере выше минимальное значение равно 10, а максимальное 70, так что длина диапазона равна 60. Предикат в запросе проверяет, находится ли значение между 50 и 70; длина этого промежутка равна 20. Следовательно, для этого предиката вероятность равна 20/60, или 1/3, и легко видеть, что алгоритм выборки по значимости дает близкий результат. И напоследок одно замечание: официально этот элемент называется непрерывный uniform. Существует также дискретный uniform, находящийся в пакете com. era. figaro. library, atomic, discrete. Как легко догадаться, ДИСКретныЙ uniform возвращает каждое значение из переданного списка с одинаковой вероятностью. Итак, мы познакомились со строительными блоками, теперь посмотрим, как из них собираются более крупные модели. 2.4. Комбинирование атомарных элементов с помощью составных В этом разделе мы рассмотрим несколько составных элементов. Напомним, что составной элемент строит из нескольких элементов новый, более сложный. Составных элементов много, но для начала мы рассмотрим только два: if и Dist, а затем посмотрим, как использовать составные версии большинства атомарных элементов. 2.4.7. Элементе Мы уже встречались с одним примером составного элемента - if. Он состоит из трех элементов: условия, предложения then и предложения else. Случайный процесс, представленный элементом if, сначала проверяет условие. Если результат вычисления условия равен true, то процесс порождает значение предложения then, иначе значение предложения else. На рис. 2.7 показан пример элемента if. Как видим, if принимает три аргумента. Первый аргумент - представляющий условие - имеет тип Element [Boolean] и в данном случае является элементом sunnyToday. Второй аргумент - предложение then; если значение элемента-условия равно true, то выбирается этот элемент. Если же значение элемента-условия равно false, то выбирается третий аргумент - предложение else. Типы значений предложений then и else должны совпадать, и этот общий тип является типом значения if. 64
Предложение then Предложение else Select (0.2 -> "Hello, world!", 0.8 -> "Oh no, not again")) Рис. 2.7. Структура элемента if Ниже показан элемент if в действии: val sunnyToday = Flip(0.2) val greetingToday = If(sunnyToday, Select (0.6 -> "Здравствуй, мир!", 0.4 -> "Здравствуй, вселенная!"), Select (0.2 -> "Здравствуй, мир!", 0.8 -> "О нет, только не это")) println(VariableElimination.probability(greetingToday, "Здравствуй, мир!")) // печатается 0.27999999999999997 Поясним, почему печатается 0.28 (с некоторой погрешностью). Предложение then выбирается с вероятностью 0.2 (когда sunnyToday равен true), и в этом случае строка «Здравствуй, мир!» выбирается с вероятностью 0.6. С другой стороны, предложение else выбирается с вероятностью 0.8, и в этом случае строка «Здравствуй, мир!» выбирается с вероятностью 0.2. Следовательно, полная вероятность выбора строки «Здравствуй, мир!» равна (0.2 х 0.6) + (0.8 х 0.2) = 0.28. В этом можно убедиться, явно вычислив два случая: sunnyToday.observe(true) println(VariableElimination.probability(greetingToday, "Здравствуй, мир!")) // печатается 0.6, потому что всегда выбирается предложение then sunnyToday.observe(false) println(VariableElimination.probability(greetingToday, "Здравствуй, мир!")) // печатается 0.2, потому что всегда выбирается предложение else 2.4.2. Элемент Dist Dist - еще один полезный составной элемент. Он похож на select, но выбор производится не из множества значений, а из множества элементов. Поскольку каждый вариант выбора сам является элементом, Dist оказывается составным элементом. Элемент Dist полезен, когда нужно выбрать один из случайных процессов. Например, угловой удар в футболе может быть коротким пасом или сразу навесом на ворота. Это можно представить элементом Dist, выбирающим один из двух процессов: короткий пас и навес на ворота. Элемент Dist находится в пакете com.era.figaro.language. Ниже приведен пример его использования: val goodMood = Dist(0.2 -> Flip(0.6), 0.8 -> Flip(0.2)) По структуре (см. рис. 2.8) он похож на элемент select (рис. 2.5). Разница в том, что исходами являются не значения, а элементы. Можно представлять себе это 2.4. Комбинирование атомарных элементов с помощью составных 65 Условие If(sunnyToday, « Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),
Глава 2. Краткое руководство по языку Figaro так: select выбирает одно из множества значений непосредственно, без промежуточного процесса, а Dist опосредованно - сначала выбирается процесс, а тот уже генерирует значение в ходе выполнения. В данном случае с вероятностью 0.2 будет выбран процесс, представленный элементом Flip (0. б), и с вероятностью 0.8 - процесс, представленный элементом Flip (0.2). Стрелки связывают вероятности с Вероятности исходами Dist (0.2 -> Flip(0.6), 0.8 Flip(0.2)) Возможные элементы-исходы Рис- 2.8. Структура элемента Dist Вот что получается в случае запроса к этому элементу: println (VariableElimination.probability(goodMood, true)) // печатается 0.28 = 0.2 * 0.6 + 0.8 * 0.2 2.4.3. Составные версии атомарных элементов Мы видели примеры атомарных элементов, принимающих числовые аргументы. Например, Flip принимает вероятность, a Normal - среднее и дисперсию. А если мы не знаем точно значения числовых аргументов? В Figaro ответ прост: сделайте их элементами. Когда числовые аргументы являются элементами, мы получаем составные версии исходных атомарных элементов. Например, ниже определен составной Flip, к которому предъявляется запрос: val sunnyTodayProbability = Uniform(0, 0.5) val sunnyToday = Flip(sunnyTodayProbability) println(Importance.probability(sunnyToday, true)) // печатается число, близкое к 0.2548 Здесь элемент sunnyTodayProbability представляет неизвестную верОЯТНОСТЬ ясной погоды сегодня; мы полагаем, что равновероятно любое значение от 0 до 0.5. Тогда sunnyToday paBHO true С ВерОЯТНОСТЬЮ, раВНОЙ Значению Элемента sunnyTodayProbability. В общем случае составной Flip принимает один аргумент типа Element [Double], ПреДСТаВЛЯЮЩИЙ ВерОЯТНОСТЬ ТОГО, ЧТО Flip Вернет true. Элемент Normal предоставляет больше возможностей. В вероятностных рассуждениях часто предполагают, что дисперсия нормального распределения известна, а вот насчет среднего полной уверенности нет. Так, можно предположить, что дисперсия температуры равна 100, а среднее примерно равно 40, но точное его значение неизвестно. Эту ситуацию можно смоделировать следующим образом: 66
2.5. Построение более сложных моделей с помощью Apply и Chain val tempMean = Normal(40, 9) val temperature = Normal(tempMean, 100) println(Importance.probability(temperature, (d: Double) => d > 50)) // печатается число, близкое к 0.164 С другой стороны, может быть неизвестна дисперсия. Допустим, у нас есть основания полагать, что она равна 80 или 105. Тогда можно написать такой код: val tempMean = Normal(40, 9) val tempVariance = Select (0.5 -> 80.0, 0.5 -> 105.0) val temperature = Normal(tempMean, tempVariance) println(Importance.probability(temperature, (d: Double) => d > 50) // печатается число, близкое к 0.154 9 Дополнительные сведения. В этой главе описана лишь небольшая часть атомарных и составных элементов, имеющихся в Figaro. Другие примеры можно найти в документации Scaladoc no Figaro. Scaladoc (www.cra.com/Figaro) - это автоматически генерируемая HTML-документация по библиотеке Figaro в формате, похожем на Javadoc. Она также имеется в двоичном дистрибутиве Figaro, выложенном на веб-странице Figaro. Дистрибутив находится в файле с именем вида figaro_2.11-2.2.2.0-javadoc.jar. Распаковать архив можно с помощью программы 7-Zip, WinZip или их аналогов. 2.5. Построение более сложных моделей с помощью Apply и Chain В Figaro имеются два весьма полезных для построения моделей элементов: Apply и chain. Apply позволяет выполнять из Figaro произвольный код на Scala, предоставляя тем самым всю мощь Scala в распоряжение разработчика, chain дает возможность создавать бесконечно разнообразные зависимости между элементами. Составные элементы if и Flip, которые мы рассматривали до сих пор, умеют создавать только зависимости предопределенного вида. A chain позволяет создать любую нужную вам зависимость. 2.5.1. Элемент Apply Элемент Apply находится в пакете com. era .figaro. language. Он принимает на входе произвольный элемент и функцию Scala. Представляемый им процесс применяет функцию к значению элемента с целью получения нового значения. Например: val sunnyDaysInMonth = Binomial(30, 0.2) def getQuality(i: Int): String = if (i > 10) "хорошее"; else if (i > 5) "среднее"; else "плохое" val monthQuality = Apply(sunnyDaysInMonth, getQuality) println(VariableElimination.probability(monthQuality, "хорошее")) // печатается 0.025616255335326698 Во второй и третьей строке определена функция getQuality. Она принимает аргумент типа integer, который внутри функции именуется i, и возвращает строку. 67
Глава 2. Краткое руководство по языку Figaro В четвертой строке определен элемент Apply с именем monthQuaiity. Структура элемента Apply показана на рис. 2.9. Apply принимает два аргумента. Первый является Элементом, В ДаННОМ Случае ЭТО Элемент sunnyDaysInMonth ТИПа Element [Int]. Второй - функцией, аргумент которой должен иметь тот же тип, что и тип значения элемента. В нашем примере функция getQuaiity принимает аргумент типа integer, так что это условие соблюдается. Функция может возвращать значение любого типа, в нашем случае она возвращает string. Элемент, к которому применяется функция Apply(sunnyDaysInMonth, getQuaiity) Функция, применяемая к значениям элемента Рис. 2.9. Структура элемента Apply Элемент Apply определяет случайный процесс следующим образом. Сначала генерируется значение элемента, переданного в первом аргументе. В нашем примере это число ясных дней в месяце. Допустим, что получено значение 7. Затем процесс применяет к этому значению функцию, переданную во втором аргументе. В нашем случае вызывается функция getQuaiity с аргументом 7, которая возвращает строку среднее. Эта строка и становится значением элемента Apply. Таким образом, значения, возвращаемые Apply, имеют тот же тип, что возвращаемое значение фуНКЦИИ. В Нашем Случае ЭЛемеНТ Apply Принадлежит ТИПу Element [String]. Тех, кто раньше не работал со Scala, я хочу познакомить с анонимными функциями. Определять отдельную функцию для каждого элемента Apply скучно и утомительно, особенно если мы собираемся использовать ее только в одном месте, а сама функция совсем коротенькая. В языке Scala имеется механизм анонимных функций, которые определяются прямо в месте использования. На рис. 2.10 показана структура анонимной функции, эквивалентной функции getQuaiity. Все выглядит так же, как для именованной функции. У функции есть аргумент с именем i. Он имеет тип integer. Символ => означает, что мы определяем анонимную фуНКЦИЮ. И За НИМ Следует ТеЛО фуНКЦИИ - Такое Же, Как у getQuaiity. Аргумент Символ определения функции функции (i: Int) => if (i > 10) "good"; else if (i > 5) "average"; else "poor" Тип аргумента Тело функции Рис. 2.10. Структура анонимной функции 68
2.5. Построение более сложных моделей с помощью Apply и Chain Ниже показан элемент Apply, эквивалентный предыдущему, но определенный с помощью анонимной функции: val monthQuality = Apply(sunnyDaysInMonth, (i: Int) => if (i > 10) "хорошее"; else if (i > 5) "среднее"; else "плохое") Теперь можно опросить элемент monthQuality. Обе версии дают одинаковый ответ: println(VariableElimination.probability(monthQuality, "хорошее")) // печатается 0.025616255335326698 Это, конечно, искусственный пример, но есть много практических причин для применения Apply. Назовем лишь некоторые из них. • Имеется элемент типа Double, значение которого нужно округлить до ближайшего целого. • Имеется элемент, значение которого имеет тип структуры данных, и требуется как-то агрегировать некоторое свойство этой структуры. Например, элемент определен над списками, и мы хотим узнать вероятность того, что количество элементов в списке больше 10. • Связь между двумя элементами лучше всего представить в виде физической модели. В таком случае мы можем воспользоваться функцией Scala для представления физической связи и включить эту модель в Figaro посредством Apply. Apply с несколькими аргументами Элемент Apply принимает также функции Scala с несколькими аргументами, но не более пяти. Такое применение Apply полезно, когда для воздействия на некоторый элемент нужно более одного элемента. Приведем пример. val teamWinsInMonth = Binomial(5, 0.4) val monthQuality = Apply(sunnyDaysInMonth, teamWinsInMonth, (days: Int, wins: Int) => { val x = days * wins if (x > 20) "хорошее"; else if (x > 10) "среднее"; else "плохое" }) Здесь Apply передаются в качестве аргументов два элемента: sunnyDaysInMonth и teamwinsinMonth, оба они имеют тип Element [int ]. Переданная в третьем аргументе функция принимает два аргумента типа integer, названные days и wins, и создает локальную переменную х со значением days * wins. Отметим, что поскольку days и wins - обычные переменные Scala типа integer, то х также обычная переменная, а не элемент Figaro. На самом деле, все внутри переданной Apply функции - обычный код на Scala. Элемент Apply принимает эту обычную функцию Scala, работающую с обычными значениями Scala, и «возводит» ее в ранг функции, работающей с элементами Figaro. Теперь ПреДЪЯВИМ Запрос ЭТОЙ ВерСИИ monthQuality! 69
Глава 2. Краткое руководство по языку Figaro println(VariableElimination.probability(monthQuality, "хорошее")) // печатается 0.15100056576418375 Вероятность немного подросла. Похоже, софтбольная команда, за которую я болею, имеет шанс порадовать меня. Важнее, впрочем, тот факт, что несмотря на простоту примера, без помощи Figaro вычислить эту вероятность было бы затруднительно. 2.5.2. Элемент Chain Как явствует из названия, элемент chain служит для сцепления элементов в модель, где один элемент зависит от другого, который, в свою очередь зависит от других, и так далее. Он имеет отношение к цепному правилу из теории вероятностей, с которым мы познакомимся в главе 5. Но чтобы понимать работу chain, знать цепное правило необязательно. Элемент chain также находится в пакете com.era.figaro.language. Проще всего объяснить принцип его работы с помощью картинки. На рис. 2.11 изображены два элемента; goodMood зависит от monthQuality. Соответствующий случайный процесс сначала генерирует значение monthQuality, а затем на его основе генерирует значение goodMood. Это простой пример байесовской сети, о которой речь пойдет в главе 4. Заимствуя терминологию байесовских сетей, элемент monthQuality на рисунке называется родителем, а элемент goodMood - потомком. Родитель Потомок Рис. 2.11. Модель с двумя переменными, одна из которых зависит от другой ПОСКОЛЬКУ goodMood зависит ОТ monthQuality, TO goodMood определен С ПОМОЩЬЮ chain. Элемент monthQuality уже был определен в предыдущем разделе. А вот как ВЫГЛЯДИТ Определение goodMood! val goodMood = Chain(monthQuality, (s: String) => if (s == "good") Flip(0.9) else if (s == "average") Flip(0.6) else Flip(O.l)) На рис. 2.12 показана структура этого элемента. Как и Apply, элемент chain принимает два аргумента: элемент и функцию. В данном случае элементом является родитель, а в роли функции выступает так называемая цепная функция. Разница между chain и Apply состоит в том, что передаваемая функция в случае Apply возвращает обычные значения Scala, а в случае chain - элемент. В нашем примере функция возвращает элемент Flip, а какой именно, зависит от значения month- 70
2.5. Построение более сложных моделей с помощью Apply и Chain Quality. Таким образом, эта функция принимает аргумент типа string и возвращает экземпляр Класса Element [Boolean]. Родитель \ Chain(monthQuality, (s: String) => if (s == "good") Flip(0.9); else if (s == "average") Flip(0.6); else Flip(O.l)) 1 1 ' Цепная функция Рис. 2.12. Структура элемента chain Случайный процесс, определенный этим элементом chain, показан на рис. 2.13. Он состоит из трех шагов. Во-первых, генерируется значение родителя. У нас это значение среднее элемента monthQuality. Во-вторых, к этому значению применяется цепная функция, которая возвращает результирующий элемент. В данном случае из определения цепной функции видно, что это элемент Шр(О.б). В-третьих, результирующий элемент генерирует значение - в нашем случае % Это значение и становится значением потомка. Значение родителя Результирующий элемент Значение цепочки true Рис. 2.13. Случайный процесс, определяемый элементом chain. Сначала генерируется значение родителя. Затем с помощью цепной функции выбирается результирующий элемент. И наконец, генерируется значение результирующего элемента Подведем итог, перечислив типы всех компонентов, участвующих в работе chain. Элемент chain параметризован двумя типами: типом значения родителя (обозначим его т) и типом значения потомка (обозначим его и): • Родитель Имеет ТИП Element [T]. • Значение родителя имеет тип т. • Цепная функция имеет тип т => Element [и]. Это означает, что функция получает аргумент типа т и возвращает значение типа Element [и]. • РезуЛЬТИруЮЩИЙ Элемент Имеет ТИП Element [U]. • Значение цепочки имеет тип и. • ПОТОМОК Имеет ТИП Element [U]. Это И еСТЬ ТИП СаМОГО Элемента Chain. В нашем примере goodMood имеет тип Element [Boolean], поэтому можно запросить вероятность того, что он равен true: println(VariableElimination.probability(goodMood, true)) // печатается 0.3939286578054374 71
Глава 2. Краткое руководство по языку Figaro Элемент Chain с несколькими аргументами Рассмотрим чуть более сложную модель, в которой goodMood зависит как от monthQuality, так И ОТ sunnyToday (СМ. рис. 2.14). Рис. 2.14. Модель с тремя переменными, в которой goodMood зависит от двух других переменных Для этого нам понадобится элемент chain с двумя аргументами. В данном случае цепная функция принимает два аргумента: quality типа string и sunny типа Boolean, а ВОЗВращает Элемент ТИПа Element [Boolean]. goodMood, Как И раньше, ИМе- ет ТИП Element [Boolean]. Вот Как ВЫГЛЯДИТ КОД! val sunnyToday = Flip(0.2) val goodMood = Chain(monthQuality, sunnyToday, (quality: String, sunny: Boolean) => if (sunny) { if (quality == "хорошее") Flip(0.9) else if (quality == "среднее") Flip(0.7) else Flip(0.4) } else { if (quality == "хорошее") Flip(0.6) else if (quality == "среднее") Flip(0.3) else Flip(0.05) }) println(VariableElimination.probability(goodMood, true)) // печатается 0.2896316752495942 Примечание. В отличие от Apply, у элемента Chain может быть только один или два аргумента. Если необходимо больше, то можно скомбинировать chain с Apply. Сначала Apply используется для объединения переданных в аргументах элементов в один элемент, значением которого является кортеж, содержащий значения аргументов. Затем этот один элемент передается chain. После этого цепочка получает доступ ко всей необходимой информации через значение этого элемента. Реализация тар и flatMap с применением Apply и Chain Эта врезка для тех, кто знаком со Scala. Элементы Figaro в каком-то смысле напоминают коллекции Scala, например List. Если List содержит список значений, то элемент - случайное значение. И точно так же, как функцию можно применить к каждому значению в списке для получения нового списка, так можно применить ее и к случайному значению в элементе для получения нового элемента. Но именно это и monthQuality sunnyToday goodMood 72
2.6. Задание фактов с помощью условий и ограничений III! ш делает Apply! Для списков применение функции к каждому значению осуществляет метод шар. То есть с помощью Apply мы просто определили тар для элементов. Поэтому вместо Apply (Flip {0 ,2) , (b: Boolean) => lb) можно написать Flip{0.2),map{!_). Аналогично мы можем применить к каждому значению в списке функцию, которая возвращает список, а затем объединить все получившиеся списки в один с помощью метода flatMap. Точно так же Chain применяет функцию к случайному значению, содержащемуся в элементе, для получения другого элемента, а затем получает от этого элемента значение. Поэтому с помощью Chain определяется flatMap для элементов. Таким образом, можно написать Uniform(0, 0.5) -flatMap(Flip (_) ) вместо Chain (Uniform (0, 0-5), (d: Double) => Flip (d) ). (Попутно заметьте, что мы определяли составной Flip посредством Chain. Многие составные элементы в Figaro можно определить с помощью Chain.) У Scala есть одна замечательная особенность: любой тип, обладающий методами тар и flatMap можно использовать в операторе генерации списков for. А значит, этот оператор применим и к элементам. Можно написать такое предложение: for { winProb <- Uniform{0, 0.5); win <- Flip(winProb) } yield !win На этом заканчивается краткое пособие по конструированию сложных моделей из простых элементов. Но прежде чем завершить эту главу, я хочу описать несколько способов задания фактов и наложения ограничений на модели. 2.6. Задание фактов с помощью условий и ограничений Мы уже достаточно подробно познакомились с процедурой построения моделей. Именно этим вы в основном и будете заниматься при работе с Figaro. Но не стоит забывать и о задании фактов. Figaro предлагает три механизма для этой цели: наблюдения, условия и ограничения. 2.6.1. Наблюдения Мы уже видели один способ задания фактов - с помощью наблюдений. В примере программы «Hello World» это делалось так: greetingToday.observe("Здравствуй, мир!") Вообще, метод элемента observe принимает в качестве аргумента возможное значение элемента и говорит, что элемент должен действовать на основе этого значения. Запрещаются случайные вычисления, в которых предполагается другое значение. Это наблюдение оказывает влияние на вероятности связанных элементов. Например, в программе «Hello World» мы видели, что после задания такого наблюдения переменная sunnyToday с большей вероятностью будет равна true, потому что, согласно модели, в ясный день приветствие «Здравствуй, мир!» более вероятно, чем в пасмурный. Мы видели также другой метод, относящийся к наблюдениям:
Глава 2. Краткое руководство по языку Figaro greetingToday.unobserve() Он удаляет наблюдение, заданное для элемента greetingToday (если таковое было), ПОЭТОМУ ДОПуСКаЮТСЯ Любые Случайные ВЫЧИСЛеНИЯ. В результате greetingToday не оказывает влияние на вероятности других элементов. 2.6.2. Условия Метод observe задает конкретное значение элемента. А что, если нам известно что- то о значении элемента, но чему оно точно равно мы не знаем? Figaro позволяет задавать в качестве факта произвольный предикат, который называется условием. Значение допустимо, только если условие принимает значение true. Например: val sunnyDaysInMonth = 31110111131(30, 0.2) println(VariableElimination.probability(sunnyDaysInMonth, 5) <3—О sunnyDaysInMonth.setCondition((i: Int) => i > 8) <3—© println(VariableElimination.probability(sunnyDaysInMonth, 5) <]—© О - Печатается 0.172279182850003 © — Факт означает, что ясных дней больше 8 © — Печатается 0, потому что, согласно условию, 5 ясных дней быть не может Использование наблюдаемых фактов для вывода вероятностей других переменных - самое главное в вероятностных рассуждениях. Например, из наблюдения за количеством ясных дней в месяце мы можем заключить, насколько вероятно, что у человека будет хорошее настроение. Воспользуемся примером из раздела 2.5: val sunnyDaysInMonth = Binomial(30, 0.2) val monthQuality = Apply(sunnyDaysInMonth, (i: Int) => if (i > 10) "хорошее"; else if (i > 5) "среднее"; else "плохое") val goodMood = Chain(monthQuality, (s: String) => if (s == "good") Flip(0.9) else if (s == "average") Flip(0.6) else Flip(O.l)) println(VariableElimination.probability(goodMood, true)) // печатается 0.3939286578054374 with no evidence Теперь Зададим уСЛОВИе, КОТОрое ГОВОРИТ, ЧТО Значение sunnyDaysInMonth ДОЛЖНО быть больше 8, И ПОСМОТрИМ, Как ЭТО ВЛИЯеТ На goodMood! sunnyDaysInMonth.setCondition((i: Int) => i > 8) println(VariableElimination.probability(goodMood, true)) // печатается 0.6597344078195809 Вероятность хорошего настроения резко возросла, потому что мы запретили рассматривать случаи, когда в месяце только 8 или меньше ясных дней. Figaro позволяет задать несколько условий, которым должен удовлетворять элемент. Для этого предназначен метод addcondition, который добавляет условие к уже существующим. В результате значение элемента должно удовлетворять как прежним условиям, так и новому. sunnyDaysInMonth.addCondition((i: Int) => i % 3 == 2) 74
2.6. Задание фактов с помощью условий и ограничений III! ша ЗдеСЬ МЫ ГОВОРИМ, ЧТО Значение sunnyDaysInMonth ДОЛЖНО быть Не ТОЛЬКО боЛЬ- ше 8, но и давать при делении на 3 остаток 2. Тем самым значения 9 и 10 исключаются, и наименьшее возможное значение равно 11. Понятно, что вероятность хорошего настроения при таких условиях будет еще больше: println(VariableElimination.probability(goodMood, true)) // печатается 0.9 Метод removeconditions удаляет все условия, заданные для элемента: sunnyDaysInMonth.removeConditions() println(VariableElimination.probability(goodMood, true)) // снова печатается 0.3939286578054374 again Кстати, наблюдение - это частный случай условия, в котором требуется, чтобы элемент принимал в точности заданное значение. Резюмируем. • Метод элемента setcondition принимает в качестве аргумента предикат, т. е. функцию, которая принимает аргумент того же типа, что значение элемента, и возвращает значение типа Boolean. После выполнения этого метода в элементе остается только заданное им условие, все ранее существовавшие условия и наблюдения удаляются. • Метод addcondition тоже принимает предикат такого же типа, как описано выше, и добавляет его к существующим условиям и наблюдениям. • Метод removeconditions удаляет все условия и наблюдения, ранее заданные для элемента. 2.6.3. Ограничения Ограничения - это более общий способ что-то сказать об элементе. Ограничения служат двум целям: (1) для задания «неточного» факта; (2) для организации дополнительных связей между элементами модели. Ограничения как неточные факты Предположим, что вам известен какой-то факт об элементе, но вы в нем не вполне уверены. Например, вам кажется, что я сегодня угрюм, но определенно судить о моем настроении во внешнему виду вы не решаетесь. Поэтому вы не задаете точный факт - goodMood равно false, а сообщаете, что goodMood скорее равно false, чем true. Для этого можно применить ограничение - функцию, которая принимает значение элемента и возвращает Double. Хотя Figaro этого не требует, ограничения работают лучше всего, когда значение функции - число от 0 до 1 (включительно). Например, чтобы выразить тот факт, что мое настроение ближе к угрюмому, но вы в этом не вполне уверены, можно было бы добавить к goodMood ограничение, КОТОрое Порождает Значение 0.5, еСЛИ goodMood раВНО true, И 1.0 - еСЛИ goodMood равно false! goodMood.addConstraint((b: Boolean) => if (b) 0.5; else 1.0) Это ограничение интерпретируется следующим образом: при прочих равных условиях вероятность, что переменная goodMood равно true, вдвое меньше, чем веро-
Глава 2. Краткое руководство по языку Figaro ятность, что она равна false, потому что 0.5 вдвое больше 1.0. Точная математика, стоящая за этим утверждением, обсуждается в главе 4, а пока просто отметим, что вероятность значения элемента умножается на результат вычисления ограничения для этого значения. Поэтому в данном примере вероятность значения true умножается на 0.5, а вероятность значения false - на 1.0. После такой операции сумма вероятностей может оказаться отличной от 1, поэтому они нормируются, так чтобы в сумме получилась 1. Предположим, что без этого факта я полагаю, что значения true и false переменной goodMood равновероятны, т. е. обе вероятности равны 0.5. Увидев факт, я должен буду сначала умножить обе вероятности на результат вычисления ограничения, т. е. получу, что true имеет вероятность 0.25, a false - 0.5. Сумма этих чисел не равна 1, поэтому я нормирую их и получаю окончательный ответ: вероятность, ЧТО goodMood paBHO true Составляет 1/3, а ЧТО goodMood paBHO false - 2/3. Разница между условием и ограничением состоит в том, что в случае условия некоторые состояния объявляются невозможными (т. е. имеющими вероятность 0), а ограничения лишь изменяют вероятности различных состояний, но не запрещают их, если только результат вычисления ограничения не равен 0. Иногда условия называют жесткими условиями, потому что они устанавливают непреложное правило касательно возможных состояний, а ограничения - мягкими ограничениями. Вернемся к нашей программе. Если после добавления этого ограничения опросить goodMood, то мы увидим, что вероятность уменьшилась, но не стала равна 0, как было бы в случае сообщения о моей угрюмости, как о точном факте: println(VariableElimination.probability(goodMood, true)) // печатается 0.24527469450215497 С ограничениями связан дополнительный набор методов - как для условий. • Метод элемента setconstraint принимает в качестве аргумента предикат, принимающий значение элемента и возвращающий Double. Метод setconstraint делает этот предикат единственным ограничением для элемента. • Метод addconstraint тоже принимает аналогичный предикат и добавляет его к существующим ограничениям. • Метод removeconstraints удаляет все ограничения, ранее заданные для элемента. Условия и ограничения отделены друг от друга, т. е. установка условия или удаление всех условий никак не затрагивает существующие ограничения. И наоборот. Ограничения для связывания элементов Это еще одно полезное применение ограничений. Допустим, у нас есть основания полагать, что значения двух элементов взаимосвязаны, но эта связь не улавливается определениями элементов. Например, предположим, что ваша любимая софтбольная команда выигрывает в 40 % матчей, поэтому каждый матч определен элементом Flip (0.4). Предположим также, что вы полагаете, что в игре команды случаются полосы везения и неудач, т. е. последовательные матчи с высокой вероятностью закончатся одинаково. Эту гипотезу можно выразить, добавив ограничение на пары последовательных матчей, которое говорит, что вероятность одинако- 76
2.6. Задание фактов с помощью условий и ограничений шгш вых результатов в них выше, чем различных. Ниже показан соответствующий код. Я написал его для трех матчей, но он легко обобщается на любое число с помощью массивов и оператора генерации for. Сначала определим результаты трех матчей: val resultl = Flip(0.4) val result2 = Flip(0.4) val result3 = Flip(0.4) Теперь, чтобы показать, что происходит, создадим элемент aiiwins, принимающий значение true, когда все результаты равны true: val allWins = Apply(resultl, result2, result3, (wl: Boolean, w2: Boolean, w3: Boolean) => wl && w2 && w3) Спросим, какова вероятность выигрыша всех матчей до того, как добавлены ограничения: printIn(VariableElimination.probability(allWins, true)) // печатается 0.064000000000000002 Теперь добавим ограничения. Определим функцию makestreaky, которая принимает два результата и добавляет для них ограничение «полосатости жизни»: def makeStreaky(rl: Element[Boolean], r2: Element[Boolean]) { val pair = Apply(rl, r2, (bl: Boolean, b2: Boolean) => (bl, b2)) pair.setConstraint((bb: (Boolean, Boolean)) => if (bb._l == bb._2) 1.0; else 0.5 )} Эта функция принимает в качестве аргументов два булевых элемента, представляющих результаты двух матчей. Поскольку ограничение можно применить только к одному элементу, а мы хотим создать связь между двумя элементами, то сначала упакуем ДВа Элемента В ОДИН. ЭТО Делает КОНСТРУКЦИЯ Apply (rl, r2, (Ы: Boolean, Ь2: Boolean) => (bi, Ь2)). Теперь у нас есть один элемент, значением которого является пара результатов двух матчей. Наложим на эту пару ограничение в виде функции, которая принимает пару ьь значений типа Boolean и возвращает 1.0, если ьь._1 == ьь._2 (первый и второй элементы пары равны), и 0.5 в противном случае. Это ограничение означает, что при прочих равных условиях вероятность совпадения двух результатов вдвое больше, чем вероятность их различия. Теперь можно наложить это ограничение на каждую пару соседних результатов и спросить, с какой вероятностью все матчи будут выиграны: makeStreaky(resultl, result2) makeStreaky(result2, result3) printIn(VariableElimination.probability(allWins, true)) // печатается 0.11034482758620691 Как видим, при наличии ограничения вероятность резко возросла. Примечание. Читатели, знакомые с графическими вероятностными моделями, заметят, что продемонстрированное в этом разделе использование ограничений позволяет Figaro представить неориентированные модели, например марковские сети.
Глава 2. Краткое руководство по языку Figaro Наша краткая экскурсия по Figaro подошла к концу. В следующей главе мы рассмотрим законченный пример использования Figaro в приложении. 2.7. Резюме • В Figaro используется такая же общая структура, как в других системах вероятностных рассуждений: модели, факты, запросы и алгоритмы вывода, дающие ответы. Модель в Figaro состоит из множества элементов. Элементы Figaro - это структуры данных Scala, представляющие случайные процессы, которые генерируют экземпляры типа значения. Для построения модели Figaro мы начинаем с атомарных элементов и объединяем их, используя составные элементы. Элемент Apply позволяет включить в модель Figaro произвольную функцию, написанную на Scala. Элемент chain позволяет создавать интересные и сложные зависимости между элементами. Условия и ограничения предоставляют средства для задания фактов и дополнительных связей между элементами. 2.8. Упражнения Решения избранных упражнений имеются на сайте www.manning.com/books/ practical-probabilistic-programming. 1. Обобщите программу «Hello World», добавив переменную, показывающую, с какой ноги вы встали (правой или левой). Встав не с той ноги, вы всегда произносите приветствие «О нет, только не это», в противном случае логика остается прежней. 2. В исходной версии программы «Hello World» задайте наблюдение, что сегодня было произнесено приветствие «О нет, только не это», и спросите, какая сегодня погода. Теперь добавьте то же наблюдение в модифицированную программу из упражнения 1. Как изменился ответ? Можете ли вы объяснить результат на интуитивном уровне? 3. В Figaro код выражение х ===== z означает то же самое, что Apply(x, z, (bl: Boolean, Ь2: Boolean) => bl === Ь2 Другими словами, оно порождает элемент, принимающий значение true, если значения обоих его аргументов равны. Не прибегая к Figaro, попробуйте угадать, что порождают следующие две программы: a. val х = Flip(0.4) val у = Flip(0.4) val z = x 78
2.8. Упражнения val w = х === z println(VariableElimination.probability(w, true)) b.val x = Flip(0.4) val у = Flip(0.4) val z = у val w = x === z println(VariableElimination.probability(w, true)) Проверьте свой ответ, выполнив программы. 4. Следующие упражнения убедят вас в полезности элемента FromRange, который принимает два целых числа т и п и порождает случайное целое число от т до п - 1. Например, FromRange (о, 3) порождает числа 0, 1, 2 с равной вероятностью. Напишите программу Figaro, которая вычисляет вероятность выпадения 11 при бросании двух правильных шестигранных костей. 5. Напишите программу Figaro, которая вычисляет вероятность того, что на первой кости выпало 6, если бросаются две правильные шестигранные кости и общее число выпавших очков больше 8. 6. В игре «Монополия» дублем называется выпадение одинаковых чисел на двух шестигранных костях. Три раза подряд выкинув дубль, игрок отправляется в тюрьму. Напишите программу Figaro, которая вычисляет вероятность такого исхода. 7. Представьте игру, в которой имеется волчок и пять костей с разным числом граней. Волчок может с равной вероятностью остановиться в одном из пяти положений: 4, 6, 8, 12, 20. Сначала игрок вращает волчок, а затем бросает правильную кость с выпавшим числом граней. Напишите программу Figaro, представляющую эту игру. a. Вычислите вероятность бросания 12-гранной кости. b. Вычислите вероятность выпадения 7 на кости. c. Вычислите вероятность, что вы бросали 12-гранную кость при условии, что выпало 7. d. Вычислите вероятность, что выпадет 7 при условии, что вы бросали 12-гранную кость. 8. Теперь модифицируем игру из упражнения 6, считая, что волчок склонен залипать и показывать одно и то же число при двух последовательных вращениях. Применяя такую же логику, как в функции makestreaky, напишите ограничение, согласно которому результаты двух последовательных вращений с большей вероятностью окажутся одинаковыми, чем различными. Допустим, что вы играете два раза подряд. a. Вычислите вероятность, что во второй раз выпадет 7. b. Вычислите вероятность, что во второй раз выпадет 7 при условии, что в первый раз выпало 7. 79
ГЛАВА 3. Создание приложения вероятностного программирования В этой главе. ■ Общая архитектура приложений вероятностного программирования. ■ Проектирование реалистичных моделей с помощью одних лишь простых средств языка. ■ Обучение моделей на данных и использование результатов для рассуждения о будущих примерах. На данный момент вы имеете какое-никакое представление о многих возможностях Figaro. Что с этим можно сделать? Как написать на этой основе полезную программу? В этой главе вы увидите, как с помощью Figaro построить реальное приложение. Мы спроектируем фильтр спама, основанный на вероятностном программировании: модель, компонент, который оценивает входящие сообщения и классифицирует их как нормальные или спамные, и компонент, который обучает модель фильтрации спама на наборе почтовых сообщений. По ходу дела мы обсудим архитектуру, которая часто применяется в приложениях вероятностного программирования. 3.1. Общая картина Мы собираемся создать фильтр спама. Как он встраивается в объемлющее приложение электронной почты? На рис. 3.1 показано, как все работает. Допустим, что у нас имеется почтовый сервер. Одна из его задач - получать входящую почту и рас-
3.1. Общая картина сылать ее пользователям. Когда поступает сообщение, сервер передает его оценивающему компоненту фильтра спама. Этот компонент с помощью вероятностных рассуждений вычисляет вероятность того, что сообщение спамное, и передает ее фильтру, который на основе полученной вероятности и заданной политики решает, пропустить ли сообщение. Сообщение поступает на сервер Входящее сообщение Сервер передает сообщение аналитическому компоненту фильтра спама, перед тем как отправить его пользователю Почтовый сервер с Политика •с Фильтр Аналитический компонент фильтра спама Модель почтового сообщения С Алгоритм вывода J ■d *4 Вероятность спама 3. Аналитический компонент использует вероятностное рассуждение, применяя алгоритм вывода к модели почтового сообщения и к входящему сообщению Фильтр оценивает вероятность спама с помощью политики. Если вероятность не слишком высока, то сообщение переправляется пользователю ^ Отфильтрованное сообщение Аналитический компонент возвращает выведенную вероятность того, что сообщение является спамом Рис. 3.1. Как фильтр спама встраивается в почтовый сервер. Эта процедура выполняется всякий раз, как поступает новое почтовое сообщение Напомним (см. главу 1), что система вероятностных рассуждений применяет алгоритм вывода к модели, чтобы получить ответ на запрос при заданных фактах. В данном случае система применяет модель почтового сообщения, описывающую, что такое нормальные и спамные сообщения. В запросе спрашивается, является ли сообщение спамом, а ответ содержит вероятность спама. Аналитическому компоненту для работы нужно откуда-то получить модель сообщения. Откуда она берется? Строится на основе обучающих данных компонентом обучения. В системах вероятностных рассуждений обучением называется процесс построения модели по обучающим данным. В главе 1 мы вкратце описали обучение, как нечто такое, что умеет делать система вероятностных рассуждений. Например, система, рассуждающая о футболе, могла бы использовать данные обо всех угловых в сезоне, чтобы обучить модель углового удара, которую затем можно будет применять для предсказания результата следующего углового. В реальных прило- 81
Глава 3. Создание приложения вероятностного программирования жениях обучение действительно используется для построения модели. Другой метод - ручное создание модели с использованием знаний о моделируемой системе. Набор данных включает много примеров почтовых сообщений Компонент обучения пользуется априорной моделью сообщений и набором данных для построения модели, с которой работает аналитический компонент 1 'X ■N. Набор данных / Компонент обучения фильтра спама ( Априорная модель | 1 сообщений \ Алгоритм обучения 1^ Модель сообщений Априорная модель сообщений описывает все, что известно о почтовых сообщениях, до обучения на данных Рис. 3.2. Как компонент обучения порождает модель почтового сообщения. Обучение может занимать много времени и производится в пакетном режиме На рис. 3.2 показан процесс построения модели почтового сообщения компонентом обучения. Он запускается в пакетном режиме, а не для каждого поступающего сообщения, так что может работать настолько долго, насколько необходимо для построения наилучшей возможной модели. Компонент обучения должен закончить работу, чтобы аналитический компонент мог воспользоваться построенной им моделью. После этого компонент обучения не нужен до тех пор, пока не понадобится обновить модель. Компонент обучения использует обучающие данные, представляющие собой набор почтовых сообщений. В него входят как нормальные, так и спамные сообщения, но не требуется, чтобы все они обязательно несли метку типа. Нельзя сказать, что в начале работы компонент обучения не располагает вообще никакими знаниями: в его распоряжении имеется алгоритм обучения, который принимает априорную модель почтовых сообщений и преобразует ее в модель, с которой работает аналитический компонент. Возможно, вам показалось, что имеет место порочный круг: если задача компонента обучения - создать модель почтового сообщения, то откуда же берется априорная модель сообщения? В данном случае априорная модель содержит минимальные структурные предположения, необходимые для построения фильтра спама. В частности, модель говорит, что спамность сообщения зависит от наличия или отсутствия некоторых слов, но ничего не говорит ни о том, какие это слова, ни о вероятности того, что сообщение окажется спамом. Эта информация появляется в результате обучения 82
3.2. Выполнение кода на данных. Априорная модель также утверждает, что количество необычных слов в сообщении имеет некоторое отношение к его спамности, но не говорит, что это за отношение. Его характер также определяется из данных. Ниже я расскажу подробнее о том, что именно выясняет компонент обучения, но вкратце речь идет о следующем: • Какие слова полезны для классификации сообщения как нормального или спамного. Такие слова называются признаками. • Множество параметров, представляющих следующие вероятности: что данное почтовое сообщение окажется спамом еще до его рассмотрения; что в сообщении встречается конкретное слово при условии, что оно нормальное или спамное: что сообщение содержит много необычных слов, опять-таки при условии, что оно нормальное или спамное. Множеств слов извлекается непосредственно из набора данных при его первом чтении. Смысл же компонента обучения - вычисление параметров на основе обучающих данных. Это те самые параметры, которые затем используются аналитическим компонентом. Построению эффективных фильтров спама посвящено много исследований. В этой главе описано сравнительно простое решение, достаточное для демонстрации основных принципов, но для сборки практически полезного приложения можно сделать гораздо больше. 3.2. Выполнение кода В этой главе будет приведен весь код той части приложения, которая имеет отношение к вероятностному программированию. Но в приложении также много кода, связанного с файловым вводом-выводом, в частности с чтением почтовых сообщений из файла, записью обученной модели в файл и последующим считыванием ее. Этот код я не привожу, потому что иначе глава получилась бы слишком длинной. Чтобы выполнить программу, вы можете скачать файл http://manning. com/pfeffer/PPP_SourceCode.zip. содержащий полный код. Компоненту обучения нужны данные. Данные нужны также для тестирования и оценки системы на этапе разработки. Существует несколько реальных наборов данных, относящихся к спаму, но приводить их адреса в Интернете бессмысленно, т. к. они могут измениться. Поэтому, чтобы у вас гарантированно был набор, с которым можно работать, я подготовил свой собственный. Он состоит не из реальных почтовых сообщений, каждое «сообщение» представлено в виде списка слов, не образующих предложений; заголовки также исключены. Однако распределение слов между нормальными и спамными сообщениями такое же, как в действительности, так что с этой точки зрения набор можно считать реалистичным. Для объяснения встречающихся в этой главе концепций такого набора достаточно. В реальном приложении лучше бы работать с настоящими сообщениями. В каталоге Chapter3Data этот набор занимает два подкаталога: Training, содержащий 100 примеров сообщений (60 нормальных и 40 спамных), используемых 83
Глава 3. Создание приложения вероятностного программирования для обучения, и Test, содержащий 100 сообщений для тестирования результатов обучения. Кроме того, имеется файл Labels.txt, содержащий метки всех сообщений (1 - спам, 0 - нормальное). В самой модели метки не используются - это было бы жульничеством. Но при ее оценке метки нужны, чтобы проверить правильность ответа, формируемого аналитическим компонентом. В принципе, необязательно помечать все обучающие данные. Алгоритм обучения может работать даже тогда, когда помечена только часть сообщений, и приведенный в этой главе код в действительности работает только с подмножеством меток. Но для простоты включены метки для всех примеров из набора данных. При разработке приложения, обучающего модель, необходим какой-то способ оценки качества модели. В репозитории кода имеется программа для решения этой задачи. Но поскольку она не входит в состав развернутого приложения, то и описывать ее в книге я не стану. Программа прямолинейная, вы легко разберетесь в ее коде в файле Evaluator.scala, руководствуясь пояснениями в тексте. Для исполнения приведенного в этой главе кода проще всего воспользоваться средством сборки Scala-программ sbt. Сначала нужно прогнать компонент обучения на каких-то обучающих данных. Этот компонент принимает три аргумента: путь к каталогу, содержащему обучающие примеры, путь к файлу меток и путь к файлу, в котором сохраняются результаты обучения. Например: sbt "runMain chap03.LearningComponent Chapter3Data/Training Chapter3Data/ Labels.txt Chapter3Data/LearnedModel.txt" В результате выводятся следующие сообщения (показаны только самые интересные строки, а не полная - довольно длинная - распечатка): Number of elements: 31005 Training time: 4876.629 Здесь мы видим, что было создано 31 005 элементов Figaro, а обучение заняло 4876 секунд. Самый важный результат работы компоненты обучения состоит в том, что теперь у нас в каталоге проекта есть файл LearnedModel.txt. Это текстовый файл, содержащий информацию, необходимую аналитическому компоненту, и, в частности, список всех релевантных слов, используемых для классификации, и значения параметров модели. Формат файла ориентирован специально на приложение для фильтрации спама, и аналитический компонент умеет его читать. Примечание. Компонент обучения потребляет очень много памяти. Возможно, придется явно увеличить размер кучи для виртуальной машины. Обычно для этого задается флаг вида -Xmx8096m при запуске Java-программы. При работе с Eclipse этот флаг указывается в файле eclipse.ini в установочном каталоге Eclipse. В данном случае мы просим Java выделить для кучи 8096 МБ (8 ГБ). Имея модель, мы можем использовать аналитический компонент для классификации почтового сообщения. Аналитический компонент принимает в качестве 84
3.2. Выполнение кода аргументов путь к классифицируемому сообщению и путь к обученной модели. Например: sbt "runMain chap03.ReasoningComponent Chapter3Data/Test/TestEmail_3.txt Chapter3Data/LearnedModel.txt" Эта команда выводит вероятность того, что третье сообщение - спам. Наконец, можно проверить результаты обучения на тестовом наборе данных. Для этого вызывается программа оценки, которая принимает четыре аргумента. Первые три - путь к каталогу, содержащему тестовые сообщения, путь к файлу меток и путь к файлу обученной модели. Последний аргумент соответствует политике, показанной на рис. 3.1, и равен пороговой вероятности классификации сообщения как спама. Если аналитический компонент сочтет, что сообщение является спамом с вероятностью выше пороговой, то классифицирует его как спам, иначе как нормальное сообщение. Пороговая вероятность определяет чувствительность фильтра. Если задать ее равной 99 %, то фильтр будет задерживать только сообщения, которые почти наверняка являются спамом, однако при этом может просочиться много спамных сообщений. Если же установить порог 50 %, то может быть заблокировано много нормальных сообщений. Например, команда sbt "runMain chap03.Evaluator Chapter3Data/Test Chapter3Data/Labels.txt Chapter3Data/LearnedModel.txt 0.5" выводит такую информацию: True positives: 44 False negatives: 2 False positives: 0 True negatives: 54 Threshold: 0.5 Accuracy: 0.98 Precision: 1.0 Recall: 0.9565217391304348 Эти строки описывают общее качество фильтра спама. Строка «true positives» {истинно положительных) показывает, сколько раз спам был классифицирован как спам, а строка «false positives» (ложноположительных) - сколько раз нормальные сообщения были ошибочно классифицированы как спам. Строка «false negatives» (ложноотрицателъных) прямо противоположна: сколько раз спамные сообщения были классифицированы как нормальные. Наконец, строка «true negatives» (истинно отрицательных) показывает, сколько раз нормальные сообщения были правильно классифицированы как нормальные. В следующей строке показана использованная пороговая вероятность. Последние три строки содержат различные показатели качества. Вопрос об измерении качества классификаторов интересен, но к вероятностному программированию как таковому имеет косвенное отношение, поэтому детали вынесены на врезку «Измерение качества классификаторов». Код из этой главы работает достаточно быстро на обучающих наборах, содержащих до 200 сообщений. Figaro позволяет обучать модели и на гораздо более объемных наборах с помощью более изощренных методов, обсуждаемых в главе 12. 85
Глава 3. Создание приложения вероятностного программирования Измерение качества классификаторов Количественная оценка качества классификаторов - непростая задача. Самый очевидный показатель - верность (accuracy), т. е. доля правильных классификаций. Но он может вводить в заблуждение, если в большинстве случаев классификация отрицательна, а нас интересуют только положительные результаты. Допустим, к примеру, что в 99 % случаев классификатор дает отрицательный результат. Тогда для получения верности 99 % достаточно было бы классифицировать все вообще примеры как отрицательные, но такой классификатор никуда не годится, потому что не находит интересующие нас случаи. Поэтому часто используют показатели точности (precision) и полноты (recall). Точность измеряет, насколько хорошо классификатор избегает лож- ноположительных результатов, а полнота - насколько хорошо он распознает положительные примеры (т. е. избегает лож неотрицательных результатов). Обозначим ТР - количество истинно положительных результатов, FP - количество ложноположительных, FN - количество ложноотрицательных, a TN - количество истинно отрицательных. Тогда Верность Точность = Полнота = TP + TN TP + FP + FN + TN ТР ГР + FP ТР TP + FN Разобравшись, как обучить и выполнить фильтр спама, посмотрим, как он работает. Начнем с общей архитектуры приложения, а затем перейдем к деталям модели и кода. 3.3. Архитектура приложения фильтра спама Напомним (см. раздел 3.1), что фильтр спама состоит из двух компонентов. Оперативный аналитический компонент выполняет вероятностное рассуждение - классификацию почтового сообщения - и решает, пропустить его или нет. Пакетный компонент обучает модель почтового сообщения на обучающем наборе данных. В следующих разделах описана архитектура аналитического компонента, а затем и компонента обучения. Как мы увидим, между ними много общего. 3.3.1. Архитектура аналитического компонента Обдумывая архитектуру компонента, нужно, прежде всего, определить входные и выходные данные и связи между ними. Задача нашего приложения - получить на входе почтовое сообщение и определить, является оно нормальным или спамным. 86
3.3. Архитектура приложения фильтра спама Поскольку речь идет о вероятностном программировании, приложение выдает не просто булев признак: спам-неспам, а вероятность, с которой сообщение, на его взгляд, является спамом. Кроме того, для простоты я буду предполагать, что входное сообщение поступает в виде текстового файла. Итак, для аналитического компонента нашего фильтра спама определены: • вход - текстовый файл, представляющий почтовое сообщение; • выход - число типа Double, равное вероятности того, что сообщение является спамом. Теперь можно шаг за шагом разрабатывать архитектуру фильтра. Шаг 1: определение верхнеуровневой архитектуры как системы вероятностных рассуждений Для начала можно вернуться к базовой архитектуре системы вероятностных рассуждений, которые была описана в главе 1, и на ее основе нарисовать первый вариант архитектуры приложения фильтра спама (рис. 3.3). Аналитический компонент фильтра спама должен определить, является сообщение спамным или нормальным. Факт - это текст сообщения, запрос - является ли сообщение спамом, а ответ - вероятность того, что сообщение является спамом. Факт • Текст сообщения Запрос • Является ли сообщение спамом? Аналитический компонент фильтра спама г Модель почтового ^ ^ сообщения j j Г Л Алгоритм вывода V. ) у *- г Ответ • Вероятность спама ^— Модель нормальных и у/ спамных сообщений Алгоритм вывода вероятности того,что сообщение является спамом, по фактам, касамщптии сииищсппи Рис. 3.3. Первый вариант архитектуры аналитического компонента. В фильтре спама используется алгоритм вывода, который получает на входе модель почтового сообщения и вычисляет вероятность того, что сообщение является спамом при заданном тексте сообщения Для получения ответа в аналитическом компоненте используется вероятностная модель почтового сообщения и алгоритм вывода. В модели инкапсулированы общие знания о почтовых сообщениях, в том числе свойства самого сообщения и сведения о том, как анализировать его на предмет спамности. Алгоритм вывода пользуется этой моделью, чтобы ответить на вопрос, является ли сообщение спамом при условии имеющихся фактов. Шаг 2: уточнение архитектуры приложения фильтра спама Вы хотим представить свою вероятностную модель с помощью вероятностной программы. В Figaro вероятностная программа состоит из элементов. Напомним, 87
Глава 3. Создание приложения вероятностного программирования что в Figaro факты применяются к отдельным элементам в виде условий или ограничений. Поэтому необходимо как-то превратить текст сообщения в факт, который можно применить к элементам модели. В машинном обучении компонент, который преобразует исходные данные в факты об элементах модели, обычно называют выделителем признаков. А те аспекты данных, которые применяются в модели в качестве фактов, называются признаками. Как показано на рис. 3.4, выделитель признаков получает на входе текст сообщения и преобразует его в набор признаков сообщения, который становится фактом для алгоритма вывода. ^ Текст сообщения Аналитический компонент фильтра Г > Выделитель признаков L J ' спама Модель почтового сообщения г Факт - Признаки сообщения V J 1 ' Г Алгоритм вывода 1 ) < ' Ответ • Вероятность спама Рис. 3.4. Второй вариант архитектуры аналитического компонента фильтра спама. Добавлен выделитель признаков и удален запрос, потому что он всегда один и тот же Кроме того, это конкретное приложение всегда отвечает на один и тот же запрос: является ли сообщение спамом? Делать запрос входными данными приложения нет необходимости, поэтому мы исключили его из архитектуры. Шаг 3: детальная спецификация архитектуры Пора присмотреться к модели поближе. На рис. 3.5 показана уточненная модель, в которой выделено три части: процесс, содержащий структурные знания, запрограммированные автором модели; параметры, полученные в результате обучения на данных, и дополнительные знания, которые напрямую не связаны с элементами, но используются в рассуждениях. Для создания модели нужен навык, и в этой книге мы встретим много примеров, но, вообще говоря, любая вероятностная модель содержит одни и те же составные части. В разделе 3.4 мы подробно рассмотрим, как они выглядят в фильтре спама, но уже сейчас я хотел бы кратко описать их. Модель состоит из пяти частей. • Шаблоны, определяющие, какие элементы встречаются в модели. Например, один элемент представляет спамность сообщения, а другие - присутствие в нем определенных слов. Конкретные слова не указываются; эту 88
3.3. Архитектура приложения фильтра спама часть знаний я поместил в конец списка. Выбор элементов фильтра спама обсуждается в разделе 3.4.1. С\ Текст сообще ния Аналитический компонент фильтра спама Выделитель признаков Модель почтового сообщения { Факт ~~Л 'Признаки ► I сообщения J Алгоритм вывода Шаблоны элементов, представляющих спайность сообщения и присутствие слов; зависимости между этими элементами; 'функциональные формы этих зависимостей и т. д. Числовые параметры функциональных форм \Параметры Знания Результаты обучения Дополнительные знания, например, слова, представляющие особый интерес Ответ • Вероятность спама Рис- 3.5. Третий вариант архитектуры аналитического компонента фильтра спама. В модели выделен процесс, параметры и знания. Параметры и знания получены в результате обучения Зависимости между элементами. Например, элемент, обозначающий присутствие конкретного слова, зависит от элемента, представляющего спам- ность сообщения. Напомним, что в вероятностной модели направление зависимости необязательно совпадает с направлением рассуждения. Мы используем слова, чтобы определить, является ли сообщение спамом, но в модели можно считать, что отправитель первым делом решает, какое сообщение послать («А пошлю-ка я спам»), а уже потом подбирает слова. Обычно зависимости моделируются в направлении от причины к следствию. Поскольку вид сообщения определяет выбор слов, зависимости направлены от элемента, представляющего спамность сообщения, к каждому слову. Определение зависимостей в модели фильтра спама подробно обсуждается в разделе 3.4.2. Функциональные формы этих зависимостей. Функциональная форма элемента определяет его тип, например: if, атомарный Binomial или составной Normal. В функциональной форме не задаются параметры элемента. Например, вышеупомянутая зависимость имеет форму if с двумя Flip: если сообщение спамное, то слово rich (богатый) присутствует с вероятностью 1, 89
Глава 3. Создание приложения вероятностного программирования иначе с какой-то другой вероятностью. Функциональные формы модели фильтра спама определены в разделе 3.4.3. Первые три части, составляющие процесс на рис. 3.5, - это то, что известно приложению до обучения. Они определяются проектировщиком приложения и образуют структуру модели почтового сообщения. • Числовые параметры модели, находящиеся в блоке «Параметры» на рис. 3.5. Например, одним из параметров является вероятность спамности сообщения. Другой - вероятность присутствия слова rich, если сообщение спамное, третий - вероятность присутствия слова rich, если сообщение нормальное. Параметры отделены от процесса, потому что они определяются в ходе обучения. Параметры модели обсуждаются в разделе 3.4.4. • Знания. На рис. 3.5 показаны дополнительные знания, необходимые для построения модели и применения к ней фактов в конкретной ситуации. Термин «знания» я употребляю для всего, что было получено в результате обучения и не связано с элементами, их зависимостями, функциональными формами и числовыми параметрами. В нашем приложении знания включают перечень слов, встречающихся в обучающих примерах сообщений, и счетчики эти слов. Эти знания нужны, к примеру, для того чтобы решить, представляет ли интерес слово rich. Таким образом, знания помогут определить параметры модели: вероятность вхождения слова rich в сообщение является параметром, только если это слово нас интересует. Дополнительные знания обсуждаются в разделе 3.4.5. Итак, с архитектурой аналитического компонента мы определились. Пора рассмотреть компонент обучения. 3.3.2. Архитектура компонента обучения Возвращаясь к разделу 3.1, напомним, что задача компонента обучения - по обучающему набору примеров построить модель почтового сообщения, которая будет использоваться аналитическим компонентом. Как и раньше, первым делом нужно определить вход и выход компонента обучения. Из рис. 3.5 понятно, что на выходе должны быть параметры и знания о модели. Но что подается на вход? Можно предположить, что частью входа является заданный обучающий набор сообщений. Однако обучение заметно упростится, если хотя бы некоторые сообщения будут кем-то помечены как нормальные или спамные. Поэтому будем считать, что задан обучающий набор сообщений и метки для некоторых из них. Необязательно помечать все сообщения, достаточно, если будет помечена небольшая часть. Итак, для компонента обучения нашего фильтра спама определены: • вход - набор текстовых файлов, представляющих почтовые сообщения; - файл, содержащий метки (нормальное или спамное) подмножества сообщений. 90
3.3. Архитектура приложения фильтра спама выход - числовые параметры, характеризующие процесс построения модели; - дополнительные знания о модели. Выделитель признаков извлекает признаки из всех сообщений и включает их в состав фактов Метки 1. Выделитель знаний порождает дополнительные знания, например, представляющие интерес слова Знания в сочетании с априорными параметрами и процессом образуют априорную модель почтового сообщения Компонент обучения фильтра спама Выделитель знаний Выделитель признаков Факты ■ Признаки всех сообщений * Метки «нормальное- сламное» некоторых сообщений Априорная модель почтового сообщения Знания Априорные параметры Процесс Г Алгоритм обучения Знания К аналитическому компоненту Параметры К аналитическому компоненту 4. В случае, когда сообщение помечено, его метка также включается в состав фактов Алгоритм обучения использует априорную модель для вычисления новых параметров при заданных фактах Рис. 3.6. Архитектура компонента обучения На рис. 3.6 показана архитектуры компонента обучения фильтра спама. Многие ее элементы такие же, как в архитектуре аналитического компонента, но есть и существенные различия. • В центре компонента обучения находится априорная модель почтового сообщения. Она состоит из тех же частей, что модель сообщения в аналитическом компоненте, но место параметров, полученных в результате обучения, занимают априорные параметры. Как мы увидим в главе 5, априорными называются предполагаемые параметры модели до ознакомления с данными. Поскольку до начала работы компонент обучения не видел никаких данных, он может пользоваться только априорными значениями параметров. Оставшиеся два части - процесс и знания - ничем не отличаются от аналитического компонента. 91
Глава 3. Создание приложения вероятностного программирования • Знания извлекаются непосредственно из обучающих сообщений выделителем знаний. Например, знания могут состоять из слов, которые чаще всего встречаются в сообщениях. Эти знания являются частью выхода компонента обучения и передаются аналитическому компоненту. • Как и в аналитическом компоненте, выделитель признаков извлекает признаки из всех сообщений и преобразует их в факты. Если сообщение помечено, то метка тоже включается в состав фактов. • Место алгоритма вывода занимает алгоритм обучения. Он получает на входе факты для всех сообщений и с помощью модели вычисляет значения параметров. Figaro предоставляет не только алгоритмы выводы, но и алгоритмы обучения, речь о них пойдет ниже. Удобная особенность описанной архитектуры с разделением на два компонента состоит в том, что компонент обучения можно выполнить один раз, а результат многократно использовать в аналитическом компоненте. Обучение на примерах данных - длительный процесс, и повторять его при поступлении каждого сообщения нежелательно. Наш дизайн позволяет обучить модель один раз, сохранить вычисленные параметры и добытые знания и использовать их для быстрого анализа поступающих сообщений. Познакомившись с архитектурой, посмотрим, как все это работает на практике. В следующем разделе представлен дизайн модели и приведен код, который строит модель для каждого сообщения. Затем будет показана реализация аналитического компонента и, наконец, компонента обучения. 3.4. Проектирование модели почтового сообщения В этом разделе показан дизайн вероятностной модели нормальных и спамных сообщений. Поскольку в аналитическом компоненте и в компоненте обучения используется один и тот же процесс и знания, а отличаются только параметры, то этот дизайн применим в обоих случаях. При построении вероятностной модели в Figaro необходимо смешать четыре основных ингредиента: • элементы модели; • способ соединения элементов между собой (зависимости); • функциональные формы элементов. Это конструкторы классов элементов, используемые для реализации зависимостей. Например, атомарный Flip - конструктор класса булевых элементов, не зависящих ни от каких других элементов, тогда как составной Flip - конструктор элементов типа Double, зависящий от другого элемента, определяющего его среднее; • числовые параметры функциональных форм. Ниже мы рассмотрим эти ингредиенты, а в конце я опишу дополнительные знания, являющиеся частью модели. 92
3.4. Проектирование модели почтового сообщения 3.4.1. Выбор элементов При проектировании фильтра спама необходимо решить, какие характеристики сообщения помогут выяснить, нормальное оно или спамное. Перечислим некоторые из них. • Конкретные слова в заголовке или теле сообщения. Например, слово rich скорее встретится в спамном сообщении, чем в нормальном. • Слова с орфографическими ошибками и другие необычные слова. Спам- ные сообщения часто содержат неправильно написанные слова и слова, не встречающиеся в нормальных сообщениях. • Появление определенных слов рядом с другими словами. • Особенности естественного языка, например, части речи. • Поля заголовка, например адреса отправителя и получателя, задействованные при доставке серверы и т. д. Чтобы не слишком усложнять приложение, примем следующие проектные решения. • Использовать только первые две из перечисленных выше характеристик: набор слов в сообщении и присутствие необычных слов. • Хотя анализировать заголовок и тело сообщения раздельно имеет смысл, мы не будем их различать. • При создании элемента, описывающего слова сообщения, есть две возможности: использовать целое число, равное количеству вхождений слова в сообщение, или использовать булев элемент, показывающий, встречается слово или нет, не обращая внимания на количество вхождений. Мы выберем второй вариант. • Что касается необычных слов, то можно искать каждое слово в словаре - это покажет, является ли слово «узаконенным» или необычным. Чтобы не зависеть от внешних приложений, мы выберем другой подход - будем считать необычными слова, которые встречаются в одном и только одном сообщении. Сформулировав предположения, займемся выбором элементов модели. У нас будет три набора элементов. Первый набор показывает, является ли сообщение спамом. Для этого достаточно одного элемента типа Boolean. Второй набор отражает наличие или отсутствие конкретных слов в сообщении, а третий - количество необычных слов в сообщении. Представления наличия и отсутствия слов При создании элементов, представляющих наличие или отсутствие слов, нужно подумать, какие слова включать в модель. Можно было бы завести по одному элементу для каждого слова, встречающегося хотя бы в одном обучающем сообщении. Но тогда количество элементов будет огромно. Например, в типичном обучающем наборе из 1000 сообщений число различных слов превышает 40 000. 93
Глава 3. Создание приложения вероятностного программирования Слишком много элементов плохо по двум причинам. Во-первых, это ведет к переобучению, когда обученная модель точно соответствует обучающим данным, но плохо обобщается на новые. Если количество элементов слишком велико, то велики шансы, что некоторые из них по чистой случайности объясняют обучающие данные, хотя и не способны предсказать, является ли сообщение спамом. Чтобы убедиться в этом, представим, что некоторое слово случайным образом включили в пять сообщений. Предположим, что 1/3 обучающих примеров - спам. Вероятность, что все пять сообщений, в которых встречается это слово, - спам, составляет примерно 1/250. Это означает, что если имеется 40 000 слов, встречающихся по пять раз, то приблизительно 160 из них будут встречаться только в спамных сообщениях. Эти 160 слов могли бы объяснить все спамные сообщения. Но, по условиям эксперимента, они были включены в обучающие сообщения случайным образом, поэтому никакой предсказательной силы не имеют. Еще одна причина не создавать слишком много элементов состоит в том, что это замедляет как анализ, так и обучение. Мы хотим, чтобы аналитический компонент работал максимально быстро, поскольку он применяется к каждому приходящему на сервер сообщению. Что касается обучения вероятностной модели, то это и так медленный процесс, особенно когда обучающих примеров тысячи и более. Если при этом еще и элементов чересчур много, то он может стать недопустимо медленным. С учетом этих соображений создадим элементы для 100 слов. Тогда возникает следующий вопрос: какие 100 слов выбрать? Решить, какие элементы, или признаки, включать в модель, поможет заимствованная из машинного обучения техника отбора признаков. В случае фильтра спама цель отбора признаков - найти слова, являющиеся наиболее убедительными индикаторами спамности или нормальности сообщения. Методы отбора признаков бывают весьма изощренными. При построении фильтра промышленного качества одним из таких методов и следовало бы воспользоваться. Но в этой главе мы ограничимся техникой попроще. Эта техника основана на двух наблюдениях. Во-первых, использование слов, которые встречаются редко, ведет к переобучению, поскольку их предсказательная сила невелика. Во-вторых, такие часто встречающиеся слова, как the и and, тоже вряд ли могут что-то предсказать, потому что встречаются чуть ли не всех нормальных и спамных сообщениях. Они называются стоп-словами. Поэтому при создании списка слов для модели, мы удалим слова, встречающиеся слишком часто, а из оставшихся выберем первые 100 по частоте встречаемости. Представление числа необычных слов Желание включить в модель необычные слова продиктовано тем, что спам обычно содержит больше необычных слов, чем нормальное сообщение. На самом деле, в обучающем наборе из 1000 сообщений 28 % слов были необычными, тогда как в нормальных сообщениях таковых было всего 18 %. Но при более внимательном изучении обнаруживаются нюансы. В некоторых спамных сообщениях число необычных слов очень велико, тогда как другие в этом отношении не отличаются 94
3.4. Проектирование модели почтового сообщения от нормальных сообщений. Чтобы уловить эту деталь, мы включим в модель элемент hasManyUnusuaiwords, который показывает, что в сообщении много необычных слов. Это скрытый элемент. Непосредственно в данных он не присутствует, но включение его в модель повышает ее качество. Итак, в модель почтового сообщения включаются следующие элементы: • isspam - элемент типа Boolean, принимающий значение true, если сообщение спамное; • hasWordl, ..., hasWordlOO - Элемент ТИПа Boolean ДЛЯ КаЖДОГО ИЗ 100 СЛОВ, принимающий значение true, если слово присутствует в сообщении; • hasManyUnusuaiwords - элемент типа Boolean, принимающий значение true, если в сообщении много необычных слов; • numUnusualWords - Элемент ТИПа Integer, Значение КОТОрОГО paBHO ЧИСЛу слов, встречающихся только в данном сообщении. Все описанные выше элементы сведены в показанный ниже класс Model. Это абстрактный класс, поскольку в нем имеются лишь объявления элементов, но отсутствуют определения. Мы завели его, имея в виду расширить классами Rea- soningModel И LearningModel. Ниже МЫ уВИДИМ, ЧТО Существуют ТОНКИе ОТЛИЧИЯ, из-за которых необходимы два разных класса для модели анализа и обучения. Абстрактный базовый класс позволяет писать методы, например применение фактов, общие для обеих моделей. В главе 12 мы познакомимся с общим способом создания аналитического и обучающего компонентов с единым классом модели, так что не придется создавать два разных класса. Но этот способ опирается на такие средства Figaro, о которых мы еще не говорили. Листинг 3.1. Абстрактный класс Model abstract class Model(val dictionary: Dictionary) { <]—О val isSpam: Element[Boolean] I val hasManyUnusuaiwords: Element[Boolean] <3—© val numUnusualWords: Element[Int] I val hasWordElements: List[(String, Element[Boolean])] <)—© } О — Класс модели принимает словарь в качестве аргумента. В разделе 3.4.5 мы увидим, что словарь содержит дополнительные знания о словах в обучающем наборе © — Объявления описанных выше элементов модели © — hasWordElements— это список пар (слово, элемент), каждый элемент которого описывает, встречается ли слово в сообщении 3.4.2. Определение зависимостей Для начала определим зависимости между элементом isspam и всеми элементами word. При определении зависимостей в вероятностной модели действует общее эвристическое правило: класс объекта определяет свойства объекта, т. е. свойства зависят от класса в модели зависимости. В нашем примере от того, является ли сообщение спамом, зависит наличие или отсутствие всех слов. Я уже отмечал этот 95
Глава 3. Создание приложения вероятностного программирования момент выше, но он заслуживает повторения, потому что многие в этом месте путаются. Мы используем слова, чтобы определить, является ли сообщение спамом. Так почему же я говорю, что класс сообщения определяет слова, а не наоборот? Очень важно запомнить, что в вероятностных рассуждениях направление вывода необязательно совпадает с направлением зависимостей в модели. С тем же успехом, оно может быть и противоположно, коль скоро мы пытаемся определить факторы, обусловившие наблюдаемый результат. В данном случае мы моделируем сущностную характеристику почтового сообщения (является оно нормальным или спамным) как ненаблюдаемый фактор, обуславливающий наблюдаемые слова. Поэтому зависимость направлена от класса сообщения к словам. Разобравшись с этим вопросом, мы должны еще решить, будем ли моделировать зависимости между словами. В английском и многих других языках выбор первого слова предложения тесно связан со вторым словом. В действительности слова не являются независимыми. Но чтобы не усложнять эту главу сверх меры, я буду предполагать, что слова независимы при условии, что известен класс сообщения (нормальное или спамное). Мы можем нарисовать так называемую байесовскую сеть, на которой показаны зависимости в модели. Подробно байесовские сети рассматриваются в главе 5. Пока же отметим лишь, что между двумя узлами сети существует ребро, если второй узел зависит от первого. Байесовская сеть для нашего фильтра спама на данный момент выглядит, как показано на рис. 3.7. Сеть такого вида называется наивной байесовской моделью, причем слово «наивный» употребляется из-за предположения о независимости слов. Рис- 3-7. Наивная байесовская модель зависимостей Элементов isSpam И hasWord Теперь рассмотрим элементы, относящиеся к необычным словам. Напомним, ЧТО у Нас ДВа ТаКИХ Элемента: hasManyUnusualWords И numUnusualWords. Элемент has- ManyUnusualWords ПОКаЗЫВаеТ, МНОГО ЛИ В Сообщении НеобыЧНЫХ СЛОВ, a numUnusualWords содержит точное их количество. Понятно, что numunusuaiwords зависит от hasManyUnusualWords, поскольку если необычных слов много, то их число заведомо больше НуЛЯ. В СВОЮ ОЧереДЬ, Элемент hasManyUnusualWords, ЯВЛЯЮЩИЙСЯ СВОЙСТВОМ сообщения, зависит от is spam, представляющего класс сообщения. Наконец, в соответствии с наивным байесовским допущением мы считаем, что наличие необычных слов не зависит от наличия любого конкретного слова при условии, что класс известен. Получается байесовская сеть, изображенная на рис. 3.8. Это окончательная модель зависимостей в нашем фильтре спама. 96
3.4. Проектирование модели почтового сообщения Рис. 3.8. Полная модель зависимостей, включающая необычные слова 3.4.3. Определение функциональных форм Необходимо определить функциональные формы наших элементов. Напомним, что функциональная форма - это конструктор класса элемента, используемый при построении модели. Они содержат все необходимое для выражения модели за исключением числовых параметров. Пойдем по порядку. Сначала определим функциональные формы для аналитического компонента, предполагая, что значения параметров вычисляет компонент обучения. Ниже приведен код модели аналитического компонента. Подробные объяснения приведены после кода. Листинг 3.2. Модель аналитического компонента i class ReasoningModel( dictionary: Dictionary, parameters: LearnedParameters ) extends Model(dictionary) { val isSpam = Flip(parameters.spamProbability) val hasWordElements = { for { word <- dictionary.featureWords } yield { val givenSpamProbability = parameters.wordGivenSpamProbabilities(word) val givenNormalProbability = parameters.wordGivenNormalProbabilities(word) val hasWordlfSpam = Flip(givenSpamProbability) val hasWordlfNormal = Flip(givenNormalProbability) val hasWord = If(isSpam, hasWordlfSpam, hasWordlfNormal) (word, hasWord) В val hasManyUnusuallfSpam = Flip(parameters.hasManyUnusualWordsGivenSpamProbability) val hasManyUnusuallfNormal = Flip(parameters.hasManyUnusualWordsGivenNormalProbability) val hasManyUnusualWords = If(isSpam, hasManyUnusuallfSpam, hasManyUnusuallfNormal) В val numUnusuallfHasMany = Binomial(Model.binomialNumTrials, В isSpam hasManyUnusualWords numUnusualWords hasWordlOO hasWordl 97
Глава 3. Создание приложения вероятностного программирования parameters.unusualWordGivenManyProbability), val numUnusuallfHasFew = Binomial(Model.binomialNumTrials, parameters.unusualWordGivenFewProbability), val numUnusualWords = If(hasManyUnusualWords, numUnusuallfHasMany, numUnusuallfHasFew) } Й О — Аргументы модели аналитического компонента — это словарь и параметры, найденные в процессе обучения © — ReasoningModel реализует абстрактный класс Model © — Элемент, показывающий, что сообщение спамное © — Элементы, показывающие, присутствует ли слово в сообщении. Этот код создает список пар (слово, элемент), по одной для каждого слова-признака © — Элемент, показывающий, много ли необычных слов в сообщении © — Элемент, представляющий количество необычных слов Теперь рассмотрим каждый элемент подробно. • isSpam - вероятность, что этот элемент типа Boolean равен true, совпадает с вероятностью того, что сообщение спамное. Поэтому определяем его с помощью элемента Flip. В Figaro это записывается в виде val isSpam = Flip (parameter. spamProbability). Это Отражает нашу априорную - ДО предъявления каких-либо слов - веру в то, что сообщение является спам- ным. Поскольку слова зависят от спамности сообщения, и мы наблюдаем именно слова, то на основе слов делается вывод о том, спамное сообщение или нет. Как видим, в этом случае направление зависимости отличается от направления вывода. В вероятностных рассуждениях иногда идут от следствия к причине. • wordi,..., wordioo - каждое слово присутствует с некоторой вероятностью. Эта вероятность зависит от того, является сообщение нормальным или спамным. Поэтому элемент if подходит идеально. Базовая форма имеет вид: val hasWordlfSpam = Flip(givenSpamProbability) val hasWordlfNormal = Flip(givenNormalProbability) val hasWord = If(isSpam, hasWordlfSpam, hasWordlfNormal) Предупреждение. Возможно, вам показалось, что определение hasWord можно упростить, исключив hasWordlf Spam и hasWordlfNormal: val hasWord = If(isSpam, Flip(givenSpamProbability), Flip(givenNormalProbability) ) К сожалению, в Figaro есть ограничение: элемент, который зависит от параметра, вычисляемого в процессе обучения (например, Flip (givenSpamProbability), зависящий от параметра givenSpamProbability), нельзя определять внутри Chain. A if - частный случай Chain, поэтому приведенная выше конструкция недопустима. Мы надеемся снять это ограничение в будущей версии Figaro. 98
<-© 3.4. Проектирование модели почтового сообщения • givenSpamProbability И givenNormalProbability - ЭТИ параметры ПСфОЖДа- ются в результате обучения, они свои для каждого слова. Указанные выше формы if создаются для каждого слова, выделенного в качестве признака. В Scala мы создаем список пар, состоящих из слова и соответствующего ему элемента hasword. Ниже приведен соответствующий код: val hasWordElements = { for { word <- dictionary.featureWords } yield { <|—Q val givenSpamProbability = parameters.wordGivenSpamProbabilities(word) val givenNormalProbability = parameters.wordGivenNormalProbabilities(word) val hasWordlfSpam = Flip(givenSpamProbability) . val hasWordlfNormal = Flip(givenNormalProbability) <1_£| val hasWord = If(isSpam, hasWordlfSpam, hasWordlfNormal) | (word, hasWord) <}—ф } } О - dictionary.featureWords порождает список 100 слов-признаков. В этом цикле производится обход этого списка © - Вероятность вхождения слова в зависимости от того, является сообщение нормальным или спамным © - Создаем элемент Figaro, показывающий, присутствует ли слово © - Пара, состоящая из слова и соответствующего ему элемента. Все такие пары помещаются в список • :: i :; : i . i i - этот элемент принимает значение true с вероятностью, зависящей от спамности сообщения. И в этом случае подходящей формой является if: val hasManyUnusuallfSpam = Flip(parameters.hasManyUnusualWordsGivenSpamProbability) val hasManyUnusuallfNormal = Flip(parameters.hasManyUnusualWordsGivenNormalProbability) val hasManyUnusualWords = If(isSpam, hasManyUnusuallfSpam, hasManyUnusuallfNormal) • numunusuaiwords - этот элемент типа integer представляет количество нео- бычных слов в сообщении. Чем больше в сообщении слов, тем больше среди них может быть необычных. В модели каждому слову сопоставляется вероятность того, что оно необычное. Это естественно представить биномиальным распределением, в котором общее число испытаний равно количеству слов в сообщении. Однако слов может быть больше 1000, а при таком большом числе испытаний вероятность многих значений numunusu- aiwords будет очень малой. Поэтому и вероятность сообщения может оказаться очень малой. При большом числе сообщений это может привести к ошибкам из-за потери значимости: вероятности округляются до 0, потому что столь малые числа непредставимы в компьютере. 99
Глава 3. Создание приложения вероятностного программирования Чтобы обойти эту проблему, мы устанавливаем фиксированное число испытаний, например 20. Это число представлено в коде константой Model. binomiaiNumTriais. Тогда число необычных слов в сообщении масштабируется и представляет долю 20. Мы вспомним об этом позже, когда будем применять факты. При описанном подходе определение numunusuaiwords выглядит так: val numUnusuallfHasMany = Binomial(Model.binomiaiNumTriais, parameters.unusualWordGivenManyProbability), val numUnusuallfHasFew = Binomial(Model.binomiaiNumTriais, parameters.unusualWordGivenFewProbability), val numUnusualWords = If(hasManyUnusualWords, numUnusuallfHasMany, numUnusuallfHasFew) С моделью аналитического компонента всё. Модель компонента обучения отличается только тем, что вместо вероятности, вычисленной в процессе обучения, используется элемент, представляющий априорную вероятность. Например: val isSpam = Flip(parameters.spamProbability) Сравните с тем, как это выглядит в модели аналитического компонента: val isSpam = Flip(parameters.spamProbability) На первый взгляд, совершенно одинаково. Однако имеется существенное различие. В модели аналитического компонента parameters.spamProbability - фиксированное число. А в модели компонента обучения :■ » i .-.,•« , ; i > r ::; • - случайный элемент, который может принимать много разных значений. В процессе обучения мы не знаем, с какой вероятностью конкретное сообщение является спа- мом, поэтому представляем эту вероятность случайным элементом. В результате обучения вычисляется значение этой вероятности, которое затем используется в аналитическом компоненте. Вот как выглядит код модели компонента обучения: class LearningModel( dictionary: Dictionary, parameters: PriorParameters <3—О ) extends Model(dictionary) { <}—ф // тело точно такое же, как в модели аналитического компонента } О — Элементы, представляющие априорные параметры © — LearningModel также расширяет абстрактный класс Model 3.4.4. Использование числовых параметров Вот мы и подошли к обсуждению числовых параметров. В аналитическом компоненте эти параметры берутся из результатов обучения. Класс LearnedParam- eters определяет все параметры модели: 100
3.4. Проектирование модели почтового сообщения Листинг 3-3- Класс LearnedParameters class LearnedParameters( val spamProbability: Double, val hasManyUnusualWordsGivenSpamProbability: Double, val hasManyUnusualWordsGivenNormalProbability: Double, val unusualWordGivenManyProbability: Double, val unusualWordGivenFewProbability: Double, val wordGivenSpamProbabilities: Map[String, Double], val wordGivenNormalProbabilities: Map[String, Double] ) Для компонента обучения необходимы априорные значения параметров. В нашей модели используются составные элементы Flip и Binomial. Напомним (см. главу 2), что оба принимают в качестве аргумента объект Element [Double], т. е. Element [Double] является параметром составных Flip и Binomial. Обычно мы хотим, чтобы в качестве этого параметра выступал непрерывный атомарный элемент. И существует непрерывный атомарный элемент Beta, прекрасно подходящий на роль параметра составных Flip и Binomial, так что им мы и воспользуемся. Подробнее об элементе Beta будет сказано в главах 4 и 5. Атомарный элемент Beta принимает в качестве аргументов два значения типа Double. В показанном ниже коде эти значения тщательно подобраны, но чтобы понять, почему они именно такие, придется немного подождать - объяснения будут Даны В ПОСЛедуЮЩИХ Главах. ВОТ КОД Класса PriorParameters. Листинг 3.4. КлассPriorParameters class PriorParameters(dictionary: Dictionary) { val spamProbability = Beta (2,3) <]—© val wordGivenSpamProbabilities = dictionary.featureWords.map(word => (word, Beta(2,2))) val wordGivenNormalProbabilities = dictionary.featureWords.map(word => (word, Beta(2,2))) val hasManyUnusualWordsGivenSpamProbability = Beta (2,2) val hasManyUnusualWordsGivenNormalProbability = Beta(2, val unusualWordGivenManyProbability = Beta (2,2) val unusualWordGivenFewProbability = Beta(2,7) <-Q 21) <-© <-o val fullParameterList = spamProbability :: hasManyUnusualWordsGivenSpamProbability :: hasManyUnusualWordsGivenNormalProbability :: unusualWordGivenManyProbability :: unusualWordGivenFewProbability :: wordGivenSpamProbabilities.map(pair => pair._2) : wordGivenNormalProbabilities.map(pair => pair._2) <^-0 101
Глава 3. Создание приложения вероятностного программирования О — Априорная вероятность спама © — Вероятность, что слово встречается в спамном или нормальном сообщении © — Априорные вероятности необычности слова при условии, что сообщение имеет много и мало необычных слов © — Априорные вероятности наличия многих необычных сообщений в спамном и нормальном сообщении © — Говорит алгоритму обучения, какие параметры нужно определить. См. врезку Рассмотрим более внимательно следующие строки: val wordGivenSpamProbabilities = dictionary.featureWords.map(word => (word, Beta(2,2))) val wordGivenNormalProbabilities = dictionary.featureWords.map(word => (word, Beta(2,2))) Для каждого слова существуют вероятности его появления в спамном и в нормальном сообщении. Априорная вероятность в обоих случаях равна Beta(2,2). В этом коде создается список пар (слово, элемент), по одной паре для каждого слова-признака. В роли априорного элемента во всех парах выступает Beta (2,2). Одна важная деталь: элементы Beta для каждого случая различны. У каждого слова свои собственные вероятности встретиться в нормальном и спамном сообщении. Нотация Scala Рассмотрим более пристально код определения full Parameters. В нем создается список всех параметров модели. Следует обратить внимание на две вещи. Во-первых, взгляните на Строку wordGivenNormalProbabilities.map (pair => pair ._2) . Здесь мы перебираем все пары (слово, элемент) в списке wordGivenNormalProbabilities. К каждой паре применяется функция, которая получает на входе пару и возвращает ее второй компонент, т. е. элемент. Поэтому результатом этого предложения является набор всех элементов, представляющих вероятности появления слов-признаков в нормальных сообщениях. Предложение wordGivenSpamProbabilities .map (pair => pair .2) делает то же самое в отношении спамных сообщений. Второе, что нужно отметить, - как элементы объединяются в список. Взглянув на определение, вы увидите, что в каждой из первых пяти строк находится по одному элементу, а последние две строки содержат списки элементов. В Scala оператор ; : принимает элемент х и список xs и помещает х в начало xs, А оператор : : : конкатенирует два списка. Таким образом, этот код создает один список, содержащий все параметры модели. 3.4.5. Работа с дополнительными знаниями Чтобы построить модель конкретного сообщения, нужно знать, какие слова являются признаками. Кроме того, нужно как-то определять, является ли слово необычным. Для этого мы используем структуру данных Dictionary. Ниже приведен код класса Dictionary. Все пояснения приведены в аннотациях. 102
3.4. Проектирование модели почтового сообщения Листинг 3-5. Класс Dictionary class Dictionary(initNumEmails: Int) { О—О val counts: Map [String, Int] = Map () 0—© var numEmails = initNumEmails <3—© def addWord(word: String) { counts += word -> (getCount(word) + 1) } E def addEmail(email: Email) { numEmails += 1 for { word <- email.allWords addWord(word) <-0 object OrderByCount extends Ordering[String] { def compare(a: String, b: String) = getCount(b) - getCount(a) } def words = counts.keySet.toList.sorted(OrderByCount) В в def nonStopWords = words.dropWhile(counts(_) >= numEmails * Dictionary.stopWordFraction) def featureWords = nonStopWords.take(Dictionary.numFeatures) def getCount(word: String) = counts.getOrElse(word, 0) E def isUnusual(word: String, learning: Boolean) = if (learning) getCount(word) <= 1 else getCount(word) <= 0 W О — В классе Dictionary запоминается количество сообщений, по которым он был построен. initNumEmails — начальное значение © — Отображает слово на счетчик сообщений, в которых оно встречается © — Число сообщений, по которым построен этот словарь Dictionary © — Добавляем слово в словарь, увеличивая его счетчик на 1 © — Добавляем сообщение в словарь, увеличивая numEmails и добавляя каждое слово, встречающееся в сообщении © — Слова, хранящиеся в словаре, отсортированные в порядке убывания счетчика © — Получить слова, встречающиеся не слишком часто © — Отобрать numFeatures самых часто встречающихся из оставшихся слов © — Получить количество сообщений, в которых встречается данное слово © — Определить, является ли слово необычным Несколько комментариев по поводу этого кода. • Аргумент initNumEmails класса Dictionary - это начальное число сообщений в словаре. В аналитическом компоненте общее число сообщений уже 103
Глава 3. Создание приложения вероятностного программирования известно от компонента обучения, поэтому initNumEmaiis устанавливается равным этому числу В компоненте обучения сообщения добавляются по ОДНОМу, ПОЭТОМУ initNumEmaiis paBHO 0. • numEmails - ЧИСЛО Сообщений В СЛОВаре. В Начале ОНО раВНО initNumEmaiis и увеличивается на единицу при добавлении каждого сообщения. Сообщения добавляются в словарь только компонентом обучения. • words - список всех слов словаре, отсортированный в порядке убывания счетчика - от наиболее частых к наименее частым. Метод sorted принимает аргумент ordering - объект, предоставляющий метод compare для сравнения двух элементов списка. Порядок сортировки реализован объектом OrderByCount. • Множество слов-признаков формируется из всего множества слов в два этапа. На первом этапе мы обходим отсортированный список слов и удаляем слова, которые встречаются слишком часто (стоп-слова). Слово считается стоп-словом, если доля сообщений, в которых оно встречается, не меньше Dictionary. stopWordFraction. Например, еСЛИ Dictionary. stopWordFraction равно 0.2 и всего существует 50 сообщений, то стоп-словом будет считаться слово, встречающееся как минимум в 10 сообщениях. На втором этапе мы Отбираем первые Dictionary. numFeatures ИЗ СПИСКа nonStopWords. • В методе get count используется метод getorEise из библиотеки Scala. Он ищет слово в отображении counts и, если не найдет, возвращает 0. • В методе isunusuai реализована логика определения необычных слов. Слово считается необычным, если не встречается ни в каких других сообщениях. В процессе обучения слово заведомо встречается в рассматриваемом в данный момент сообщении, поэтому его счетчик <= 1. В аналитическом компоненте словарь состоит только из сообщений, встречавшихся в процессе обучения, поэтому если слово необычно, то оно вообще не может оказаться в словаре. 3.5. Разработка аналитического компонента Еще раз рассмотрим архитектуру аналитического компонента, повторенную на рис. 3.9. Мы уже видели части, относящиеся к модели сообщения, порождающему процессу, параметрам, найденным на этапе обучения, и знаниям - в виде словаря, построенного в ходе обучения. Осталось разобраться с выделителем признаков, применением фактов и выполнением алгоритма вывода. Для выделения признаков и формирования наблюдаемых фактов используется класс Email. Ниже приведен аннотированный код этого класса. После уже проделанной работы выделение признаков и формирование фактов не представляет сложностей. 104
3.5. Разработка аналитического компонента ^ Текст сообщения Аналитический ко ( > Выделитель признаков L J 1 мпоне! t f Факт Л ■ Признаки ^ сообщения j нт фильтра спама Модель почтового V сообщения Процесс Параметры Знания \ ) * Алгоритм вывода V ) 1 > Ответ • Вероятность спама Параметры Знания Результаты обучения Рис. 3.9. Архитектура аналитического компонента (повтор) Листинг 3.6. Класс Email class Email (file: File) { def getAHWords () = ... val allWords: Set [String] = getAHWords () def observeEvidence( model: Model, <3—© label: Option[Boolean], <]—Q learning: Boolean <]—Q ) { label match { case Some(b) => model.isSpam.observe(b) case None => () } for { (word, element) <- model.hasWordElements } { element.observe(allWords.contains(word)) П В В val obsNumUnusualWords = allWords.filter ( (word: String) => model.dictionary.isUnusual(word, learning)).size Ш 105
Глава 3. Создание приложения вероятностного программирования val unusualWordFraction = obsNumUnusualWords * Model.binomialNumTrials / allWords.size model.numUnusualWords.observe(unusualWordFraction) } } О — Получить все слова сообщения с помощью операций ввода-вывода (не показано) © — Метод для передачи фактов модели, основанной на признаках сообщения © — Модель, которой передаются наблюдаемые факты © — Метка, которая может и отсутствовать © — Флаг режима: обучение или анализ © — Если метка имеется, применить ее к элементу модели isSpam © — Для каждого слова и соответствующего ему элемента модели наблюдаемым фактом является вхождение слова в множество allWords © — Подсчитать число необычных слов в сообщении © — Применить наблюдаемый факт к элементу модели numUnusualWords И снова несколько комментариев. • При создании объект Email читает указанный в аргументе файл для получения всех слов, встречающихся в сообщении. Они сохраняются в поле aii- words. Отметим, что aiiwords - множество, что согласуется с моделью, в которой нас интересует только, присутствует ли слово в сообщении или нет, а сколько раз оно встречается, неважно. • Аргумент label метода observeEvidence имеет ТИП Option [Boolean], ЭТО означает, что он может и отсутствовать. В Scala структура данных option может принимать значение some (с) или None. В данном случае, если класс сообщения известен, то label будет содержать этот класс, а в противном случае равно None. Если метка имеется (значение label равно some (b)), то она будет применена в качестве факта к model.isSpam. • Для подсчета необычных слов в сообщении используется стандартный метод Scala niter. Здесь filter получает множество строк aiiwords и удаляет из него все строки, не удовлетворяющие заданному в аргументе условию. Аргументом filter является функция, которая возвращает true, если слово необычное. Таким образом, niter удаляет все строки, кроме необычных. Размер оставшегося множества и является количеством необычных слов. Отметим, что метод isunusuai принимает флаг режима обучения во втором аргументе. • В последних трех строках задается наблюдаемое количество необычных слов. Напомним, что элемент numUnusualWords определен с помощью биномиального распределения, в котором число испытаний задано константой Model.binomialNumTrials. Поэтому МЫ ПРОИЗВОДИМ НОрМИрОВКу, Чтобы ВЫ- раЗИТЬ ЧИСЛО obsNumUnusualWords Как ДОЛЮ ОТ Model.binomialNumTrials. Зная, как выделять признаки и применять факты, и имея модель вместе с найденными в результате обучения параметрами, мы теперь можем классифицировать сообщения как нормальные или спамные. Для этого служит алгоритм вывода. 106
3.5. Разработка аналитического компонента Мы воспользуемся точным алгоритмом исключения переменных (ИП). Для одного сообщения этот алгоритм работает достаточно быстро, а, в силу своей точности, дает наилучший из возможных ответов. Ниже приведен его код. Листинг 3.7. Метод classify def classify( I dictionary: Dictionary, I <^—U parameters: LearnedParameters, fil eName: String ) = { val file = new File (fileName) val email = new Email (file) «-© val model = new ReasoningModel(dictionary, parameters) O—© email.observeEvidence(model, None, false) <]—© val algorithm = VariableElimination(model.isSpam) algorithm.start() <^-0 val isSpamProbability = algorithm.probability(model.isSpam, true) <J—© println("Вероятность спама: " + isSpamProbability) | algorithm, kill () <—© isSpamProbability O—© } © — Метод classify принимает результаты обучения в виде словаря и параметров, а также имя файла, содержащего классифицируемое сообщение, и возвращает вероятность того, что это сообщение спамное © — Создать объект Email на основе файла с именем filename © — Создать аналитическую модель с применением результатов обучения; © — Передать модели наблюдения относительно этого сообщения. Поскольку мы сейчас не занимаемся обучением, необязательная метка равна None, а флаг режима обучения равен false © — Создать и выполнить алгоритм исключения переменных, передав в качестве запроса элемент модели isSpam © — Получить вероятность спамности сообщения и напечать сообщение © — Почистить за собой, уничтожив алгоритм. Алгоритм нельзя уничтожать, пока не будет получен ответ на запрос © — Вернуть вероятность того, что сообщение спамное Наконец, в аналитическом компоненте имеется метод main. Он вызывается, когда приложение запускается из командной строки. Листинг 3-8- Метод main аналитического компонента def main(args: Array[String]) = { val emailFileName = args(O) <3—© 107
Глава 3. Создание приложения вероятностного программирования val learningFileName = args(l) <3—© val (dictionary, parameters) = loadResults(learningFileName) <}—© classify(dictionary, parameters, emailFileName) <3"-0 } О — Получить имя файла сообщения из первого аргумента командной строки © — Получить имя файла с результатами обучения из второго аргумента командной строки © — Загрузить результаты обучения из файла с помощью метода loadResults (не показан) © — Использовать результаты обучения для классификации сообщения, прочитанного из файла Я не привожу код метода loadResults, поскольку он длинный и не содержит ничего, кроме обычных операций ввода-вывода. Опишу лишь вкратце, что он делает. 1. Прочитать общие параметры модели, например вероятность спама и вероятность, что в сообщении много необычных слов. 2. Прочитать все слова вместе со счетчиками вхождения. 3. Прочитать слова-признаки вместе с вероятностями присутствия при условии, что сообщение нормальное или спамное. Формат файла разработан специально для этого приложения. В Figaro нет общего формата файла с результатами обучения. 3.6. Разработка компонента обучения Определив модель и разработав аналитический компонент, мы имеем почти все необходимое для создания компонента обучения. Еще раз посмотрим на архитектуру этого компонента, повторенную на рис. 3.10. Модель почтового сообщения мы уже видели. Как отмечалось выше, различие между моделью сообщения в компоненте обучения и в аналитическом компоненте заключается в том, что первом случае используются априорные параметры, состоящие из элементов Beta. При разработке аналитического компонента мы уже видели, как работает выделитель признаков и как применяются факты при наличии сообщения и факультативной метки «нормальное-спам». В аналитическом компоненте мы анализировали одно сообщение. Напротив, в компоненте обучения имеется весь обучающий набор сообщений. Выделитель признаков извлекает из всех сообщений признаки и преобразует их в факты. Он также читает файл Labels, содержащий метки некоторых обучающих сообщений, и если метка имеется, то применяет соответствующий факт. В архитектуре на рис. 3.10 присутствует также выделитель знаний, задача которого - извлечь дополнительные знания из текстов сообщений. Мы уже знаем, что эти знания представлены в виде словаря - класса Dictionary, имеющего метод addEmaii, поэтому выделитель знаний в нашем фильтре спама несложен. Мы определим объект Scala с именем Dictionary. В Scala объект можно считать классом, 108
3.6. Разработка компонента обучения имеющим единственный экземпляр. Его можно использовать для определения аналога статических методов в Java. Мы определяем метод fromEmaiis, который конструирует объект Dictionary по экземпляру класса Traversable, параметризованного КЛаССОМ Email (Traversable - ЭТО баЗОВЫЙ КЛаСС Scala, Которому НаСЛеду- ют многие коллекции, в том числе списки и множества). Для вызова этого метода НуЖНО Написать Dictionary. fromEmaiis (emails). ОТМСТИМ, ЧТО у нас уже еСТЬ КЛаСС Dictionary, т. е. разрешается использовать одинаковые имена для класса и объекта. Тексты сообщений Метки Компонент обучения фильтра спама Выделитель знаний Выделитель признаков Факты * Признаки всех сообщений * Метки «нормалычое- спамное» некоторых^ сообщений Модель почтового сообщения в компоненте обучения Знания Априорные параметры Процесс Алгоритм обучения Знания Найденные параметры Рис. 3.10. Архитектура компонента обучения (повтор) Листинг 3-9- Объект Dictionary object Dictionary { def fromEmaiis(emails: Traversable[Email]) = { val result = new Dictionary(0) for { email <- emails } { result.addEmail(email) result } } <M> О — Создать экземпляр класса Dictionary, для которого начальное число сообщений равно 0. Добавить в словарь все сообщения и вернуть его Вот мы и добрались до самого главного кода в компоненте обучения - применения алгоритма обучения с целью определить параметры модели. Мы воспользуемся ЕМ-алгоритмом (expectation-maximization). Это «мета-алгоритм», поскольку в его внутреннем цикле используется регулярный алгоритм вывода. Подробнее о 109
Глава 3. Создание приложения вероятностного программирования ЕМ-алгоритме будет сказано в главе 12. Опуская детали, можно сказать, что ЕМ сначала выдвигает гипотезу о значениях параметров, вычисляет вероятности элементов модели в предположении этой гипотезы, а затем с помощью вычисленных вероятностей улучшает гипотезу Этот процесс повторяется столько раз, сколько необходимо. Для вычисления вероятностей элементов используется алгоритм вывода. Figaro предоставляет реализации ЕМ-алгоритма с различными алгоритмами вывода. В данном приложении мы воспользуемся алгоритмом распространения доверия (belief propagation - РД). Алгоритм РД похож на ИП, но для больших моделей работает быстрее. Принцип его работы основан на распространении сообщений между элементами, повторяемом на протяжении нескольких итераций. При выполнении ЕМ-алгоритма с РД в качестве алгоритма вывода необходимо задать число итераций для того и другого. В данном случае мы оставим для обоих значение по умолчанию (10), которое дает хорошие результаты. Ниже показан метод learnMAP, в котором применяется алгоритм обучения. Он принимает априорные параметры и возвращает параметры, найденные в результате обучения. Акроним MAP означает «maximum a posteriori» {максимум апостериорной вероятности - МАВ) - используемый нами метод обучения: мы возвращаем значения параметров, доставляющие максимум апостериорной вероятности параметров при известных данных. ЕМ-алгоритм порождает именно МАВ-значе- ния. Листинг 3.10. Метод learnMAP LearnedParameters = { def learnMAP(params: PriorParameters): val algorithm = EMWithBP(params.fullParameterList:_*] algorithm.start() val spamProbability = params.spamProbability.MAPValue val hasUnusualWordsGivenSpamProbability = params.hasManyUnusualWordsGivenSpamProbability.MAPValue val hasUnusualWordsGivenNormalProbability = params.hasManyUnusualWordsGivenNormalProbability.MAPValue val unusualWordGivenHasUnusualProbability = params.unusualWordGivenManyProbability.MAPValue val unusualWordGivenNotHasUnusualProbability = params.unusualWordGivenFewProbability.MAPValue Я val wordGivenSpamProbabilities = for { (word, param) <- params.wordGivenSpamProbabilities } yield (word, param.MAPValue) val wordGivenNormalProbabilities = for { (word, param) <- params.wordGivenNormalProbabilities } yield (word, param.MAPValue) В algorithm.kill() В new LearnedParameters( spamProbability, Й no i
3.6. Разработка компонента обучения hasUnusualWordsGivenSpamProbability, hasUnusualWordsGivenNormalProbability, unusualWordGivenHasUnusualProbability, unusualWordGivenNotHasUnusualProbability, wordGivenSpamProbabilities.toMap, wordGivenNormalProbabilities.toMap ) g О — Создать и выполнить экземпляр ЕМ-алгоритма, в котором для вычисления вероятностей используется алгоритм РД. Число итераций в алгоритмах ЕМ и РД оставлено подразумеваемым по умолчанию © — Вычислить самое вероятное значение типа Double для каждого параметра-элемента © — Для каждого слова-признака вычислить наиболее вероятное значение параметра, т. е. вероятность присутствия слова в спамном сообщении, и аналогично для нормальных сообщений. Поместить найденные значения в список пар (слово, значение) © — Почистить за собой, уничтожив экземпляр алгоритма. Алгоритм нельзя уничтожать, пока не будут получены МАВ-значения, поскольку после уничтожения они будут неверны. © — Вернуть экземпляр класса Learned Parameters, содержащий найденные в процессе обучения параметры Наконец, в компоненте обучения имеется метод main, который вызывается при запуске из командной строки. Он принимает три аргумента: путь к каталогу, содержащему все сообщения из обучающего набора, путь к файлу меток некоторых обучающих сообщений и путь к файлу, в котором будут сохранены результаты обучения. Сначала метод читает входные файлы и строит словарь. Затем создаются модели всех сообщений. И наконец, метод выполняет алгоритм обучения и сохраняет результаты. Впоследствии эти результаты могут быть многократно использованы аналитическим компонентом. Важно отметить, что параметры всех сообщений одинаковы. Однако элементы в моделях различных сообщений различны. Например, у каждого сообщения свой элемент is spam. Если бы это было не так, то этот элемент принимал бы одно и то же значение для всех сообщений, т. е. все сообщения были бы либо спамными, либо нормальными. С другой стороны, по структуре и параметрам модели всех сообщений идентичны. Ниже приведен код метода main. Листинг 3.11. Метод main компонента обучения def main(args: Array[String]) { val trainingDirectoryName = args(O) val labelFileName = args(l) val learningFileName = args(2) val emails = readEmails(trainingDirectoryName) ^3—© val labels = readLabels(labelFileName) in s
Глава 3. Создание приложения вероятностного программирования val dictionary = Dictionary.fromEmails(emails.values) O—© val params = new PriorParameters (dictionary) i <|_Л val models = for { (fileName, email) <- emails } yield { val model = new LearningModel(dictionary, params) email.observeEvidence(model, labels.get(fileName), true) model } val learnedParameters = learnMAP(params) <]—© saveResults(learningFileName, dictionary, learnedParameters) 0—© } О — Прочитать все сообщения из каталога, содержащего обучающий набор. Метод readEmails (не показан) возвращает отображение имен файлов на экземпляры Email © — Прочитать метки из файла меток. Метод readLabels (не показан) возвращает отображение имен файлов на метки. Файлы, для которых нет меток, в этом отображении не представлены © — Построить словарь по значениям отображения emails (всем сообщениям) © — Построить модели и сформировать наблюдаемые факты. Отметим, что у всех моделей одинаковые априорные параметры, но разные экземпляры LearningModel. Еще отметим, что для получения необязательного аргумента метода observeEvidence используется метод labels.get(filename). Он возвращает None, если метки нет, и Some(class), если метка есть и обозначает определенный класс © — Выполнить алгоритм обучения © — Сохранить результаты обучения в файле для аналитического компонента (метод не показан) Ну, вот и все. Мы рассмотрели всю процедуру создания полного приложения: разработку архитектуры, проектирование модели, реализацию аналитического компонента, а затем и компонента обучения. Сейчас вы знаете об основных возможностях Figaro и располагаете более-менее приличными средствами для написания приложений. В следующей части книги мы рассмотрим моделирование более детально. Сначала мы отступим на шаг назад и углубим понимание вероятностных моделей и вывода, а затем разберем различные способы использования Figaro для создания развитых практически полезных вероятностных моделей. 3.7. Резюме • Вероятностные программы части применяются в приложениях, где требуется обучить модель на данных, а затем многократно применять ее к новым примерам. • У многих вероятностных программ схожая архитектура, в которой выделяются два компонента: аналитический и обучения. 112
3.8. Упражнения В обоих компонентах используется модель, включающая спецификацию порождающего процесса, параметры этого процесса и дополнительные знания. Компоненту обучения передаются априорные параметры, а он на основе данных определяет их истинные значения, которые затем используются в аналитическом компоненте. 3.8. Упражнения Решения избранных упражнений имеются на сайте www.manning.com/books/ practical-probabilistic-programming. Упражнения к этой главе требуют от читателя размышлений общего характера. Вам предлагается подумать о том, как бы вы спроектировали приложение с вероятностными рассуждениями и компонентом обучения для решения реальной задачи - по образцу примера из этой главы. Прорабатывая упражнения, обращайте внимание на общую архитектуру и дизайн, не отвлекаясь на детали модели. 1. Вы разрабатываете поисковую систему, которая ищет изображения, содержащие указанные объекты, даже если эти объекты явно не аннотированы в изображении. Система содержит блок распознавания объектов, который снабжает изображение меткой - аннотацией, содержащей сведения об изображенных объектах. Вы решили разработать распознаватель как приложение с вероятностными рассуждениями и компонентом обучения. a. Набросайте архитектуру поисковой системы и опишите роль распознавателя объектов. b. Набросайте архитектуру аналитического компонента. Каковы вход и выход распознавателя? Возможно, вы захотите работать не с пикселями, а сначала извлечь высокоуровневые признаки, например гистограмму распределения цветов и выделенные границы. Как это укладывается в архитектуру? c. Набросайте архитектуру компонента обучения. Каковы входы и выходы? Как результаты обучения передаются аналитическому компоненту? 2. Вы проектируете компонент охранного приложения, задача которого - распознавать аномальную активность в сети универсама. Вы решили, что аномальной считается активность, имеющая низкую вероятность. Вы хотите обучить вероятностную модель сетевой активности. Всякий раз, замечая некоторую активность в сети, система вычисляет ее вероятность. Если вероятность оказывается низкой, то система сигнализирует об аномалии. a. Каковы входы и выходы детектора аномалий и как он вписывается в структуру охранного приложения? b. При таком дизайне не существует переменной, представляющей класс активности. Вместо этого вы создаете вероятностную модель, основанную на переменных, описывающих активность. Что это могут из
быть за переменные? Какие между ними существуют связи и зависимости? с. Что должно входить в состав обучающих данных для детектора аномалий? Какого рода знания можно получить в результате обучения на этих данных? Вы проектируете программу для игры в покер. Перед ней стоят две задачи: хорошо играть с противником в первый раз и улучшать игру со временем. a. В покере есть два основных источника случайности: сдача карт и действия игроков. Опишите некоторые переменные, связанные с вашей программой, и каждую пометьте источником случайности. Как вы думаете, почему с точки зрения обучения следует различать, обусловлена переменная сдачей карт или действиями игроков? b. Для программы игры в покер нужны два вида обучения. Во-первых, она должна знать, как вообще играют люди. Это поможет ей при первой встрече с соперником. Во-вторых, в ходе нескольких игр против одного и того же соперника программа должна обучаться особенностям его манеры игры. Опишите архитектуру системы обучения, которая решала бы обе эти задачи. Какая информация передается между различными компонентами? 114 Глава 3. Создание приложения вероятностного программирования
Часть II НАПИСАНИЕ ВЕРОЯТНОСТНЫХ ПРОГРАММ Ч .асть 2 целиком посвящена тому, как писать вероятностные программы, описывающие любую интересующую вас ситуацию. Моя задача - не только вооружить вас инструментами для разработки программ, но и добиться, чтобы вы хорошо понимали, что эти программы означают и когда следует выбирать ту или иную технику программирования. Глава 4 содержит введение в вероятностное моделирование и основные идеи вероятностного программирования; хотя собственно программирования в этой главе почти нет, она позволит заложить принципиальный фундамент. В главе 5 представлены две основных парадигмы моделирования, находящиеся в центре вероятностного программирования: байесовские и марковские сети. Главы с 6 по 8 базируются на материале глав 4 и 5 и содержат продвинутые методы моделирования, в т. ч. коллекции, объектно-ориентированное программирование и моделирование динамических систем.
ГЛАВА 4. Вероятностные модели и вероятностные программы В этой главе. ■ Определение вероятностной модели. ■ Как вероятностные модели используются для ответа на запросы, ■ Ингредиенты вероятностной модели: переменные, зависимости, функциональные формы и числа. ■ Как в вероятностной программе представлены ингредиенты вероятностной модели. Первая часть книги представляла собой введение в вероятностное программирование. Мы узнали, что в системах вероятностных рассуждений вероятностная модель используется для получения ответов на запросы при заданных фактах и что вероятностное программирование - это создание программ для представления вероятностных моделей. В этой части мы глубже изучим представление вероятностных моделей и познакомимся с различными приемами написания вероятностных программ. Но сначала необходимо в общих чертах понимать, что такое вероятностная модель, как она конструируется и как используется для ответов на запросы. Этим вопросам и посвящена данная глава. Интуитивное понятие о предмете вы получили в главе 1, теперь самое время заняться фундаментальными принципами. В этой главе мы разовьем темы, затронутые в главе 1, поэтому полезно было бы сначала ее прочитать. Здесь же будет описано, как в вероятностной программе и, в частности, в программе на Figaro определяется вероятностная модель, поэтому не помешает знакомство с базовыми сведениями о Figaro, изложенными в главе 2.
4.1. Определение вероятностной модели 4.1. Определение вероятностной модели Вероятностная модель - это способ кодирования общих знаний о неопределенной ситуации. Допустим, к примеру, что вы - искусствовед, пытающийся определить, является ли некая картина подлинным Рембрандтом или подделкой. Вы располагаете следующей информацией: 1. Рембрандт любил темные цвета. 2. Он часто рисовал людей. 3. Новые подлинники великих мастеров открывают редко, но подделок на рынке пруд пруди. 4. На этой картине имеется большое пятно ярко-желтого цвета. 5. На картине изображен моряк. 6. Картина была продана на аукционе в 2003 году. Пункты 1-3 - это общие знания, а пункты 4-6 - конкретные знания. Отметим, что общие знания формулируются в виде тенденций: «любил», «часто», «редко», «пруд пруди», а конкретные предельно специфичны: «на этой картине», «на картине изображен», «картина была продана». В вероятностном моделировании общие знания представляются в виде вероятностной модели, которая применяется к конкретным знаниям с целью рассуждения о конкретной имеющейся ситуации. Конкретные знания также называют фактами, а то, что вы пытаетесь выяснить - является ли картина подлинным Рембрандтом, - запросом. Таким образом, вы применяете общие знания о живописи и Рембрандте, содержащиеся в утверждениях 1-3, к фактам, сообщаемым в утверждениях 4-6, чтобы получить ответ на запрос, является ли данная картина подлинником. 4.1.1. Выражение общих знаний в виде распределения вероятности возможных миров На рис. 4.1 приведены примеры общих знаний о предметной области. Общие знания сообщают нам две вещи: (1) что является возможным и (2) что является вероятным. Сначала поговорим о том, что возможно: при создании вероятностной модели мы рисуем в воображении много возможных состояний дел - и каждое называется возможным миром. Так, один из возможных миров - тот, в котором картина является подделкой, другой - тот, в котором это подлинный ландшафт, написанный Рембрандтом. Каждый возможный мир описывает одну из ситуаций, которые вы считаете возможными до знакомства с фактами. Например, ландшафт Рембрандта - один из возможных миров, пока вам не предъявили факты. Позже, узнав, что на картине изображены люди, вы отвергнете этот мир. Это хорошо сочетается с описанными нами различиями между общими и конкретными знаниями. Возможный мир - то, что может произойти в принципе: определение картины как ландшафта Рембрандта возможно. Факты характеризуют то, что вам известно здесь и сейчас: это портретная живопись. 117
Глава 4. Вероятностные модели и вероятностные программы Варианты,которые мы считаем возможными Знания об общих характеристиках предметной области г Общие знания о предметной области Возможности: • Рембрандт или подделка? - Яркие или темные цвета? • Люди или ландшафт? • В каком году продана? Тенденции: • Подделки встречаются часто, подлинники Рембрандта редко - Рембрандт любил темные цвета • Рембрандт часто рисовал людей Рис. 4.1. Общие знания о предметной области включают знания о том, что возможно, и о том, что вероятно Так, в нашей ситуации можно вообразить следующие возможные миры: w1 - картина поддельная; w2 - картина действительно написана Рембрандтом, это изображение людей в темных тонах; w3 - картина действительно написана Рембрандтом, это изображение людей в ярких тонах; w4 - картина действительно написана Рембрандтом, это изображение ландшафта. Отметим, что возможные миры - это то, что мы считаем возможным до ознакомления с фактами. Важно понимать, что факты - не часть вероятностной модели. Модель описывает общие знания. В вероятностных рассуждениях модель используется для кодирования общих знаний и применяется к фактам о конкретной ситуации с целью получить ответы на запросы об этой ситуации. Поскольку факты не являются частью модели, а модель составлена из возможных миров, то факты нельзя использовать для определения того, какими должны быть возможные миры. Поэтому мы имеем, например, миры w2 и w4, хотя они и противоречат фактам в утверждениях 4 и 5. Теперь поговорим о второй составляющей общих знаний: что является возможным? Для кодирования этой информации каждому возможному миру сопоставляется число - его вероятность. В табл. 4.1 показано, как назначены вероятности всем четырем возможным мирам. Сумма вероятностей всех миров должна быть равна 1. Назначение вероятностей возможным мирам называется распределением вероятности. Этим термином обозначается любое назначение вероятностей мирам, вне зависимости от того, на каких знаниях оно базируется. Таблица 4.1. Распределение вероятности возможных миров Возможный мир wl Описание Подделка Вероятность 0.9 118
4.1. Определение вероятностной модели Возможный мир w2 wz w4 Описание Рембрандт, люди, темные Рембрандт, люди, яркие Рембрандт, ландшафт Вероятность 0.09 0.009 0.001 Процесс представления общих знаний, включающих все мыслимые возможности и применимые к ним тенденции, в виде распределения вероятности возможных миров описан на рис. 4.2. Возможности диктуют определение возможных миров, а тенденции - вероятности этих миров. Варианты,которые мы считаем возможными Знания об общих характеристиках предметной области Общие знания о предметной области Возможности: ■ Рембрандт или подделка? - Яркие или темные цвета? • Люди или ландшафт? • В каком году продана? Тенденции: • Подделки встречаются часто, подлинники Рембрандта редко - Рембрандт любил темные цвета ■ Рембрандт часто рисовал людей Возможные миры: 1. Подделка 2. Рембрандт, люди, темные 3. Рембрандт, люди, яркие 4. Рембрандт, ландшафт Распределение вероятности 00 © © 0.9 0.09 0.009 0.001 Возможные миры описывают все ситуации, считающиеся возможными до ознакомления с фактами С каждым миром связана вероятность. Сумма вероятностей равна 1 Рис. 4.2. Знания о предметной области представлены в виде распределения вероятности возможных миров. На этом рисунке величина вероятности представлена размером соответствующего мира Разобравшись с возможными мирами и распределением вероятности, можно перейти к определению вероятностной модели. Вероятностная модель - это формальное представление распределения вероятности возможных миров. И это все. В определении не говорится, что вероятностная модель является распределением вероятности. На самом деле, это лишь представление такого рас- 119
пределения. Само распределение - это математическая концепция явного сопоставления числа каждому возможному миру Возможных миров может быть очень много, а распределение сопоставляет вероятность каждому из них. Представление же - это нечто, что можно записать, и с чем можно работать. Простейшая вероятностная модель - это таблица, в которой явно перечислены вероятности возможных миров, но, как мы увидим далее, модель может быть весьма компактной, пусть даже количество миров огромно. В определении говорится, что представление должно быть формальным. Это значит, что имеются точные правила определения вероятности каждого мира, хотя явной формулы ее вычисления может и не быть. Я определил вероятностную модель, но не описал, как ее создать - как описать все возможные миры и их вероятности. Искусство и наука вероятностного моделирования в том и состоят, чтобы представить распределение вероятности точно и компактно. Как это делать, вы узнаете, читая книгу. Узнав о важной роли распределений вероятности в вероятностном моделировании, познакомимся с это концепцией поближе. 4.7.2. Подробно о распределении вероятности Распределение вероятности несет информацию не только о вероятностях возможных миров, но и о вероятностях различных фактов. Любой факт в одних мирах имеет место, а в других - нет. Чтобы узнать вероятность этого факта, нужно сложить вероятности тех миров, в которых он имеет место. Например, рассмотрим факт - картина действительно написана Рембрандтом. Этот факт имеет место в мирах wv w3 и wv но не в мире w{. Его вероятность равна сумме вероятностей миров, в которых он имеет место, т. е. 0.09 + 0.009 + 0.001 =0.1. Поэтому мы говорим, что Рембрандт написал эту картину с вероятностью 0.1, или - в разговорной речи - 10 %. Стандартно это записывается в виде Р(Рембрандт) = 0.1. Изложенная концепция изображена на рис. 4.3. Распределение вероятности до ознакомления с фактами принято называть априорным распределением вероятности. Противоположностью ему служит апостериорное распределение вероятности - после ознакомления с фактами. Соответственно, величина Р(Рембрандт), вычисленная выше, называется априорной вероятностью факта в противовес апостериорной вероятности, вычисляемой после ознакомления с фактами. В следующем разделе мы увидим, как принимать в расчет факты для нахождения апостериорной вероятности. Возможные миры: 1. Подделка 2. Рембрандт, люди, темные 3. Рембрандт, люди, яркие 4. Рембрандт, ландшафт -~""***""^ Распределение ^^^ вероятности Q0 © © 0.9 0.09 0.009 0.001 Вероятность, что картина написана Рембрандтом £ 0.09 V )© © *- 0,009 + 0,001 =0.1 J Рис. 4.3. Вероятность факта равна сумме вероятностей возможных миров, в которых этот факт имеет место Глава 4. Вероятностные модели и вероятностные программы 120
4.2. Использование вероятностной модели для ответа на запросы Существует много способов перебрать возможные миры и назначить им вероятности. Распределение вероятностей, показанное в табл. 4.1 и повторенное в табл. 4.2, соответствует общим знаниям, описанным в пунктах 1, 2 и 3 в начале раздела. В пункте 3 говорится, что подделки встречаются чаще, чем подлинники Рембрандта, и в нашем распределении подделка имеет место в мире wv вероятность которого равна 0.9, а подлинники в остальных трех мирах с суммарной вероятностью 0.1. В пункте 1 говорится, что Рембрандт любил темные цвета. И в таблице мы видим, что мир w2 (Рембрандт, люди, темные) в 10 раз вероятнее мира w3 (Рембрандт, люди, яркие). И хотя в оставшемся мире тл ничего не говорится о цветах, в нашем распределении его вероятность крайне мала - всего 0.001. Наконец, в пункте 2 говорится, что Рембрандт часто рисовал людей. Миры w2 и w3 согласуются с этим фактом, поэтому согласно нашей модели вероятность, что картина написана Рембрандтом и на ней изображены люди, равна сумме вероятностей w2 и wy т. е. 0.099. А вероятность, что ландшафт написан Рембрандтом, равна всего 0.001 - в 99 раз меньше, чем для картины с людьми. Таблица 4.2. Все та же вероятностная модель. Подделка гораздо вероятнее подлинника, поэтому вероятность w} больше, чем трех других миров вместе взятых. Аналогично, если картина написана Рембрандтом, то на ней скорее будут изображены люди, чем ландшафт, а темные тона гораздо вероятнее ярких Возможный мир иг, w2 wz w4 Описание Подделка Рембрандт, люди, темные Рембрандт, люди, яркие Рембрандт, ландшафт Вероятность 0.9 0.09 0.009 0.001 В данном примере построить правдоподобную модель можно, не применяя никаких методов, но в более сложных ситуациях эта затея наталкивается на серьезные трудности. В разделе 4.3 мы начнем изучать структурированные методы представления распределений вероятности. Но прежде чем вдаваться в детали представления моделей, нужно в общих чертах понять, как модели используются для рассуждений о конкретной ситуации. 4.2. Использование вероятностной модели для ответа на запросы Мы закодировали общие знания в виде распределения вероятности миров. Как применить эту модель к фактам (конкретным знаниям) и получить ответ на запрос? Например, мы знаем, что на картине есть большое ярко-желтое пятно, что это портрет моряка и что она была продана на аукционе в 2003 году. Это факты. Мы хотим узнать, написана ли картина Рембрандтом. Это запрос. Как с помощью 121
Глава 4. Вероятностные модели и вероятностные программы модели вывести ответ на запрос при заданных фактах? Состояние наших знаний сведено в табл. 4.3. Таблица 4.3. Модель и факты - все наши знания о ситуации. Каждый факт аннотирован его релевантностью модели Модель Подделка Рембрандт, люди, темные Рембрандт, люди, яркие Рембрандт, ландшафт 0.9 0.09 0.009 0.001 Факты Большое ярко-желтое пятно ==> Яркие Портрет моряка ==> Люди Продана в 2003 году (не релевантен) 4.2.7. Применение условий для получения апостериорного распределения вероятности Процедура использования распределения вероятности с целью учесть факты называется применением условий, или обусловливанием. Результатом применения условий является апостериорное распределение вероятности. Процедура проста и состоит из двух шагов. 1. Исключить все возможные миры, не согласующиеся с фактами. В данном случае таких миров два: wv в котором используются только темные тона, и w4, в котором на картине изображен ландшафт. Вычеркиваем эти меры, сопоставляя им вероятность 0. Тот факт, что картина была продана на аукционе в 2003 году, не является поводом для вычеркивания - этот факт не релевантен модели. 2. Увеличить вероятности оставшихся миров, так чтобы их сумма осталась равной 1. Это называется нормировкой вероятности. Смысл нормировки в том, что вероятности вычеркнутых миров распределяются между оставшимися. Вероятность каждого мира увеличивается пропорционально его априорной вероятности. Например, раз априорная вероятность wx была в 100 раз больше априорной вероятности wv то из суммы вероятностей вычеркнутых миров на долю wx придется в 100 раз больше, чем на долю wy В результате после распределения вероятность w{ по-прежнему будет в 100 раз больше вероятности w3. Существует математически простой метод корректной нормировки вероятностей. Сначала вычислим сумму вероятностей миров, которые совместимы с фактами и не вычеркнуты. Эта сумма называется нормировочным коэффициентом. В нашем случае он равен 0.9 + 0.009 = 0.909. Затем поделим вероятность каждого совместимого мира на нормировочный коэффициент. Этим гарантируется, что сумма вероятностей по-прежнему равна 1, а пропорции сохранены. В результате получится распределение вероятности, показанное в табл. 4.4. 122
4.2. Использование вероятностной модели для ответа на запросы Таблица 4.4. Апостериорное распределение вероятности после применения условий. Сначала вероятности несовместимых миров обнуляются. Затем оставшиеся вероятности нормируются путем деления на сумму вероятностей совместимых миров. Тем самым гарантируется, что сумма вероятностей равна 1 Возможный мир IV, w2 W3 wA Описание Подделка Рембрандт, люди, темные Рембрандт, люди, яркие Рембрандт, ландшафт Вероятность 0.9/0.909 = 0.9901 0 0.009 / 0.909 = 0.0099 0 Применение условий, основанных на фактах, сводится к вычеркиванию возможных миров, не совместимых с фактами, и нормировки вероятностей оставшихся миров. Полученное распределение вероятности называется апостериорным, поскольку оно вычислено после ознакомления с фактами. Весь процесс - априорное распределение, применение условий и получение апостериорного распределения - показан на рис. 4.4. Факты представляют конкретные знания о ситуации Распределение вероятности до ознакомления с фактами Возможные миры: 1. Подделка 2. Рембрандт, люди, темные 3. Рембрандт, люди, яркие 4. Рембрандт, ландшафт Факты: • Ярко-желтое пятно • «Пьяный моряк» • Продана на аукционе в 2003 году Совместимые миры: 1. Подделка 2. Рембрандт, люди, темные 3. Рембрандт, люди, яркие 4. Рембрандт, ландшафт Вычеркиваем миры, не совместимые с заданными фактами Распределение вероятности после применения условий, основанных на фактах Нормируем вероятности оставшихся миров, чтобы их сумма была равна 1 Рис- 4.4. Применение условий, основанных на фактах. Мы вычеркиваем миры, не совместимые с фактами, и нормируем вероятности оставшихся миров, чтобы их сумма была равна 1 123 Распределение вероятности 0.9 0.09 0.009 0.001 Апостериорное распределение вероятности 0.9/(0.9 + 0.009) 0.009/(0.9 + 0.009)
Глава 4. Вероятностные модели и вероятностные программы 4.2.2. Получение ответов на запросы Как, зная апостериорное распределение, получить ответы на запросы? Нас интересует, является ли Рембрандт автором картины. В предыдущем разделе было сказано, что распределение вероятности задает не только вероятности отдельных возможных миров, но и фактов, которые в одних мирах имеют место, а в других - нет. Например, w3 - единственный возможный мир, совместимый с авторством Рембрандта, для которого апостериорная вероятность больше 0. Поэтому апостериорная вероятность того, что картина написана Рембрандтом, равна 0.0099. Для записи ответа на запрос при условии известных фактов имеется специальная нотация: Р(Рембрандт | ярко-желтый, портрет моряка, продана в 2003 году) = 0.0099. Здесь вертикальная черта (|) отделяет запрос от фактов. Слева от черты находится событие, вероятность которого мы хотим знать, а справа - известные факты. Такая нотация называется условной вероятностью, поскольку представляет вероятность запрошенного события при условии некоторых фактов. На рис. 4.5 показано, как устроена нотация условной вероятности. Запрос Факты Р(Rembrandt I bright yellow, picture of sailor, sold in 2003) = 0.0099 / Символ условия Вероятность Рис. 4.5- Строение нотации условной вероятности Предупреждение. Если рассматривать только саму нотацию, то кажется, что можно применить условия, основанные на фактах, которые мы считаем невозможными (факты с нулевой вероятностью). Например, что произойдет, если попытаться указать в качестве факта, что на картине изображен натюрморт, написанный Рембрандтом? Предположим, что мы хотим запросить яркость картины. С применением нотации условной вероятности надо было бы написать Р(яркая | натюрморт, Рембрандт). Но вероятность, что натюрморт написан Рембрандтом, равна нулю, поскольку это не совместимо ни с одним из возможных миров. Применяя условие, основанное на таком факте, мы вычеркнем вообще все возможные миры. Следовательно, мы не сможем вычислить апостериорное распределение вероятности и сказать, с какой вероятностью картина яркая. В попытках придать смысл условной вероятности при невозможных условиях было израсходовано немало чернил, но итог таков: этого следует избегать. Применение условий, основанных на невозможных фактах, приводит к непредсказуемым результатам, зависящим от конкретной системы вероятностного программирования. На рис. 4.6 все идеи из двух предшествующих разделов сведены в общую картину вероятностного моделирования. Общие знания о предметной области представлены в виде распределения вероятности возможных миров. Конкретные знания принимают вид фактов, которые используются для вычисления апостериорного распределения вероятности. Наконец, апостериорное распределение 124
4.2. Использование вероятностной модели для ответа на запросы используется для получения ответов на запросы об интересующих нас сведениях в свете имеющихся фактов. 1. Варианты, которые мы считаем возможными 2. Знания об общих характеристиках предметной области Общие знания о предметной области Возможности: " Рембрандт или подделка? • Яркие или темные цвета? " Люди или ландшафт? • В каком году продана? Тенденции: " Подделки встречаются часто, подлинники Рембрандта редко • Рембрандт любил темные цвета * Рембрандт часто рисовал людей Возможные миры: 1. Подделка 2. Рембрандт, люди, темные| 3. Рембрандт, люди, яркие 4. Рембрандт, ландшафт Факты: • Ярко-желтое пятно • «Пьяный моряк» • Продана на аукцио-] не в 2003 году Распределение вероятности 0.9 0.09 0.009 0001 Совместимые миры: 1. Подделка 2. Рембрандт, люди, темные 3. Рембрандт, люди, яркие 4. Рембрандт, ланд- шафт Факты представляют конкретные знания об этой ситуации Апостериорное распределение вероятности 0,9/(0.9 + 0,009) 0.009/(0.9 + 0.009) 3. Распределение вероятности до ознакомления с фактами Вероятность, что автор картины — Рембрандт, при заданных фактах 0.009/(0,9 + 0.009) = 0.0099 Распределение вероятности после применения условий, основанных на фактах 6. Вероятность события при условии имеющихся фактов равна сумме вероятностей миров, совместимых с этим событием Рис. 4.6. Общая картина использования вероятностной модели Основные определения Возможные миры - все состояния, которые мы считаем возможными. Распределение вероятности - сопоставление вероятности от 0 до 1 каждому возможному миру, так что сумма всех вероятностей равна 1. Вероятностная модель - формальное представление распределения вероятности возможных миров. Факты - имеющиеся знания о конкретной ситуации. Априорное распределение вероятности - распределение вероятности до ознакомления с фактами. 125 1 2 3 4 1 3 3
Глава 4. Вероятностные модели и вероятностные программы Применение условий, основанных на фактах - процедура применения фактов к распределению вероятности. Апостериорное распределение вероятности - распределение вероятности после ознакомления с фактами; результат применения условий. Нормировка - процедура пропорционального изменения чисел из некоторого множества, так чтобы их сумма стала равна 1. Теперь вы знаете, что такое вероятностная модель и на каких принципах основано получение ответов на вопросы. Но как все это выглядит на практике? Возможных миров может быть очень много, поэтому проверка их совместимости с фактами не всегда осуществима. Задача вероятностного алгоритма вывода - эффективное получение ответов. 4.2.3. Применение вероятностного вывода Основная задача вероятностного вывода - вычислить апостериорное распределение вероятности представляющих интерес переменных при заданных фактах. В нашем примере было легко вычеркнуть миры, не совместимые с фактами, и нормировать вероятности оставшихся миров. В реальных же задачах число возможных миров обычно очень велико, количество переменных модели может расти экспоненциально. Поэтому миры нельзя даже выписать, что уж говорить о вычислении их вероятностей. Нам необходим алгоритм вероятностного вывода, умеющий эффективно вычислять апостериорные вероятности. Системы вероятностного программирования и, в частности, Figaro включают такие алгоритмы. Подобные алгоритмы основаны на трех правилах вероятностного вывода: цепное правило, правило полной вероятности и правило Байеса. Подробно о них вы узнаете в третьей части книги. А пока достаточно знать, что алгоритмы вероятностного вывода сводятся к механическому применению этих правил и вытекающих из них. Алгоритмы ориентированы на работу с конструкциями языка вероятностного программирования, для которого определены, поэтому могут использоваться даже в сложных программах на этом языке. Это одно из преимуществ вероятностного программирования: если вы можете написать программу на некотором языке, то можете применить в ней алгоритм вывода. На практике алгоритм вероятностного вывода может столкнуться с трудностями, даже если он в принципе работает. Легко написать обычную программу, которая имеет экспоненциальную сложность и, стало быть, очень долго работает на интересующих нас задачах. И с вероятностными программами точно так же - для получения точного ответа алгоритму вывода иногда требуется много времени. Во многих типичных применениях вероятностный вывод в вашей программе будет работать так, как нужно. Однако в больших и сложных задачах даже самые эффективные алгоритмы могут работать долго. Алгоритмы вероятностного вывода могут быть точными или приближенными. Точный здесь означает, что апостериорные вероятности вычислены в точном соответствии с тремя правилами вывода. Поскольку точный вывод иногда обходится 126
4.3. Составные части вероятностных моделей дорого, возникает нужда в приближенных алгоритмах, которые обычно - но не всегда - дают ответ, близкий к точному. В системах вероятностного вывода часто предлагается широкий ассортимент алгоритмов для решения данной задачи - как точных, так и приближенных. Некоторые из них еще и допускают настройку. На сложность вывода также оказывает сильное влияние выбранный способ выражения модели. Поэтому, если вы собираетесь использовать язык вероятностного программирования при создании больших и сложных моделей, то стоит потратить некоторое время на то, чтобы понять, как работает вероятностный вывод, и приобрести навыки выбора наиболее эффективного алгоритма. Этим вопросам посвящена третья часть книги. 4.3. Составные части вероятностных моделей Вероятностная модель - это формальное представление распределения вероятности, и таких представлений может быть много. Одно из них - таблица с явно выписанными вероятностями, но оно пригодно разве что для самых простых задач. В этом разделе мы познакомимся с общим, практически применимым подходом к построению вероятностных моделей, включающих четыре составные части. Это не единственный, но широко распространенный способ создания моделей. Он также лежит в основе вероятностного программирования. В главе 3 мы уже познакомились с четырьмя ингредиентами модели фильтра спама, а здесь рассмотрим их более подробно. • Переменные, например, является картина подлинным Рембрандтом или подделкой. • Зависимости между переменными, например, яркость красок зависит от того, писал ли картину Рембрандт. • Функциональные формы, которые принимают зависимости, например, можно ли смоделировать подлинность картины как результат подбрасывания асимметричной монеты. • Числовые параметры этих форм, например, степень асимметрии монеты при определении подлинности картины. В следующих разделах мы увидим, как задается каждый ингредиент. 4.3.1. Переменные Первым делом следует решить, какие переменные включать в модель. Как и в языке программирования, переменная - это нечто, способное принимать различные значения. Возможный мир определяет конкретные значения каждой переменной. В любом возможном мире переменная имеет вполне определенное значение, но в разных мирах ее значения могут различаться. Хотя выбор переменных - всего лишь первый шаг построения вероятностной модели, здесь, как и в программировании, 127
Глава 4. Вероятностные модели и вероятностные программы полная свобода творчества. Принимаемые на этом этапе решения оказывают сильное влияние на эффективность модели. В примере из раздела4.1 в описаниях возможных миров неявно подразумевались следующие переменные: • Рембрандт: является ли картина подлинным Рембрандтом или подделкой? • Яркость: написана она в ярких или темных тонах? • Тема: изображены люди или ландшафт? Отметим, что мы не включили переменную, касающуюся продажи на аукционе. Хотя она и входила в состав конкретных знаний, мы посчитали ее нерелевантной. У каждой переменной есть тип, определяющий, какого рода значения она может принимать. Непрерывные переменные принимают в качестве значений вещественные числа (как переменные типа Double); например, высоту можно измерить в сантиметрах. Существуют также перечисления, целые числа или структурные переменные типа списков. Противоположностью непрерывным являются дискретные переменные. Все переменные, с которыми мы до сих пор встречались, были дискретными, более того - перечислениями. Переменные типа перечисления часто называют категориальными. У переменной есть также область значений, т. е. множество значений, считающихся допустимыми. Например, в модели может присутствовать целая переменная, принимающая только значения от 1 до 10, в этом случае областью ее значений является множество целых чисел от 1 до 10. В табл. 4.5 приведены примеры переменных, которые можно было бы использовать в модели картины, вместе с типами и некоторыми возможными значениями. Таблица 4.5. Примеры переменных, их типы и возможные значения Переменная Рембрандт Размер Высота (в см) Год последней продажи Годы всех продаж Тип Boolean Enumeration Real Integer List[Integer] Примеры значений true,false малая, средняя, большая 25.3, 14.9,68.24 1937,2003 List(1937), List( 1969, 2003) Между переменными вероятностной модели и вероятностной программы существует очевидная связь. В Figaro элементы программы соответствуют переменным модели, а типы их значений будут вычислительными представлениями соответствующих типов модели. Например: val rembrandt: Element[Boolean] = // здесь будет определение val size: Element[Symbol] = // здесь будет определение val height: Element[Double] = // здесь будет определение val lastYearSold: Element[Int] = // здесь будет определение val allYearsSold: Element[List[Integer]] = // здесь будет определение Если известен набор переменных, то построить возможные миры проще всего, создав таблицу, в которой будет по одному столбцу для каждой переменной и по 128
4.3. Составные части вероятностных моделей одной строке для каждой возможной комбинации значений переменных. Например, в табл. 4.6 перечислено восемь возможных миров с переменными Рембрандт, Яркость и Тема. Таблица 4.6. Возможные миры для переменных Рембрандт, Яркость и Тема. Всего существует восемь комбинаций их значений и, следовательно, восемь миров W, w2 W3 W4 W5 we w7 WB Рембрандт False False False False True True True True Яркость Темная Темная Яркая Яркая Темная Темная Яркая Яркая Тема Люди Ландшафт Люди Ландшафт Люди Ландшафт Люди Ландшафт 4.3.2. Зависимости Я говорил, что зависимости можно использовать для структурирования распределения вероятности. Зависимости характеризуют, как переменные связаны между собой. Интуитивно представляется, что распределение вероятности огромного числа возможных миров можно разбить на локальные компоненты модели, характеризующие эти связи. В нашем примере есть три переменные: Рембрандт, Яркость и Тема. Как они связаны? В вероятностном моделировании связи между переменными характеризуются зависимостями, которые описывают, как значение одной переменной зависит от значений других. Не менее чем зависимости, важны отношения независимости, описывающие ситуации, когда значение некоторой переменной вообще не зависит от значения какой-то другой переменной. Отношение независимости Две переменные независимы, если знание одной ничего не говорит о другой. Например, допустим, что есть еще переменная Размер (с допустимыми значениями Малая, Средняя и Большая), описывающая размер картины. Что означает фраза «Размер не зависит от Яркости»? У нее есть совершенно точный смысл. Рассмотрим какое-нибудь значение Яркости, например, Темная. Пусть известна априорная вероятность Р(Яркость = Темная). Теперь рассмотрим произвольное значение Размера, например, Большая. Спрашивается, дает ли знание того, что Размер = Большая, какую-то дополнительную информацию об истинности утверждения Яркость = Темная. В данном случае фактом является утверждение Размер = Большая, а запросом - утверждение Яркость = Темная, поскольку нас 129
Глава 4. Вероятностные модели и вероятностные программы интересует именно его истинность. Если применить к модели условие Размер = Большая, то получится апостериорная вероятность Р(Яркость = Темная | Размер = Большая). Так вот, если переменные Яркость и Размер независимы, то апостериорная вероятность будет совпадать с априорной. Знание того, что картина большая, ни на йоту не изменяет нашей веры в том, что она написана в темных тонах. Чтобы Яркость и Размер были независимы, это утверждение должно выполняться при любых значениях этих переменных. Должны быть справедливы следующие равенства (как их читать, объяснено на рис. 4.7). Р(Яркость = Темная | Размер = Большая) = Р(Яркость = Темная) Р(Яркость = Темная | Размер = Средняя) = Р(Яркость = Темная) Р(Яркость = Темная | Размер = Малая) = Р(Яркость = Темная) Р(Яркость = Яркая | Размер = Большая) = Р(Яркость = Яркая) Р(Яркость = Яркая | Размер = Средняя) = Р(Яркость = Яркая) Р(Яркость = Яркая | Размер = Малая) = Р(Яркость = Яркая) Вероятность, что Яркость = Темная Вероятность, что Яркость = при условии, что Размер = Большая Темная безо всяких условий Р(Яркость = Темная I Размер = Большая) = Р(Яркость = Темная) Запрос Символ Факт условия Рис. 4.7. Объяснение равенства, описывающего отношение независимости Независимость - симметричное свойство. Если известно, что знание размера картины не сообщает никакой новой информации о ее яркости, то верно и обратное: знание яркости ничего не говорит о размере. В математических обозначениях из того, что Р(Яркость = Темная | Размер = Большая) = Р(Яркость = Темная) следует, что Р(Размер = Большая | Яркость = Темная) = Р(Размер = Большая) Уравнение независимости иллюстрируется на рис. 4.8. Мы начинаем с априорного распределения вероятности Размера и Яркости. Вероятность, что Яркость = Темная, равна сумме вероятностей всех возможных миров, в которых Яркость = Темная, т. е. 0.8. Затем мы наблюдаем факт Размер = Малая и вычеркиваем несовместимые с ним миры. Нормируя вероятности оставшихся миров, мы вычисляем апостериорную вероятность, что Яркость = Темная. Она оказывается равной 0.8, т. е. такой же, как до наблюдения факта. Легко видеть, что для любого значения Размера отношение вероятности мира, в котором Яркость = Темная, к вероятности 130
4.3. Составные части вероятностных моделей мира, в котором Яркость = Яркая, равно 4:1. Поскольку отношение вероятностей одинаково, то наблюдение конкретного значения Размера не изменяет отношение Темной к Яркой по сравнению с априорным распределением. Факт • Размер= Малая Размер Яркость Вероятность Малая Малая Средняя Средняя Большая Большая Малая Малая Средняя- Средняя - Большая- Большая - Темная Яркая. Темная Яркая Темная Яркая 0.2 0.05 0.2 0.05 0.4 0.1 Размер Яркость Вероятность Темная Яркая I емная 0.2 0.05 -&т2— Яркая- -Темная- -&S5- -&4- -Вркая- -9+ Р(Яркость = Темная): 0.2 + 0.2 + 0.4 = 0.8 Р(Яркость = Темная | Размер = Малая): 0.2 / (0.2 + 0.05) = 0.8 Рис. 4.8- Независимость: вероятность, что Яркость = Темная, не изменяется после ознакомления с фактом Размер = Малая Условная независимость Следующий шаг - рассмотреть случай, когда связь между двумя переменными зависит от того, что мы уже знаем. Бывает, что две переменные не являются независимыми, если никакой дополнительной информации нет, но становятся таковыми после добавления информации. Например, мы можем сказать, что ландшафты чаще бывают большими и яркими. Тогда переменные Размер и Яркость не являются независимыми, поскольку знание того, что картина большая, повышает нашу веру в том, что это ландшафт, а это, в свою очередь, увеличивает шансы, что картина яркая. С другой стороны, если мы уже знаем, что на картине изображен ландшафт, то известие о том, что она большая, не дает новой информации о типе картины и потому не изменяет нашу веру в том, что она яркая. В таком случае мы говорим, что Яркость и Размер условно независимы при условии Тема = Ландшафт. Можно выписать такие равенства: Р(Яркость = Темная | Размер = Малая, Тема = Ландшафт) = Р(Яркость = Темная | Тема = Ландшафт) Подобные равенства можно написать для всех возможных значений Яркости, Размера и Темы. 131
Глава 4. Вероятностные модели и вероятностные программы Факт • Размер= Малая Р(Яркость=Темная | Тема= Ландшафт, Размер = Малая) = 0.015/(0.15 + 0.35) = 0.3 Тема Размер Яркость Вероятность Люди Люди Люди Люди Люди Люди Ландшафт Ландшафт Ландшафт Ландшафт Ландшафт Ландшафт Малая Малая Средняя Средняя Большая Большая Малая Малая Средняя Средняя Большая Большая Темная Яркая Темная Яркая Темная Яркая Темная Яркая Темная Яркая Темная Яркая 0.16 0.04 0 16 0+04 032 0.08 0.015 0.035 0.03 0.07 0.015 0.035 Р(Яркость = Темная) = 0.16 + 0.16 + 0.32 + 0.015 + 0.03 + 0.015 = 0.7 Тема Размер Яркость Вероятность -Люди -Люди —Малая - -Темная- -вгНЗ- -Мэлэя- -Люди -Люди -Средняя Темная- -е^- -е-46- Люди Люди -Средняя Яркая- Большая Темная- -Ъ&г- -Gt32- Ландшафт Ландшафт Малая Малая Яркая — Темная Яркая -Тетч -9:08- 0.015 0.035 -Ландшафт—Средняя Темная 9т9в— -Ландшафт—Средняя Яркая &9?— -Ландшафт—Большая Темная 0015 Ландшафт—Большая Яркая 0035 Факт • Тема = Ландшафт Р(Яркость = Темная | Тема = Ландшафт) = 0.015 + 0.03 + 0.015/ (0.015 + 0.03 + 0.015 + 0.07 + 0.015 + 0.035) = 0.3 Тема Размер Яркость Вероятность -Люди -Люди -Малая- -Темная- -6т4€- "Малая- -л ркв -Ы±- Люди Люди -Средняя Темная- -fr46- Люди -Люди Средняя Яркая — "Ьольшая I емная- -0:0^ -&32- "Ьоль Ландшафт Ландшафт Ландшафт Ландшафт Ландшафт Ландшафт Малая Малая Средняя Средняя Большая Большая -Яркая- Темная Яркая Темная Яркая Темная Яркая -От£в- 0.015 0.035 0.03 007 0015 0.035 Рис. 4.9. Условная независимость: факт Тема = Ландшафт изменяет нашу веру в то, что Яркость = Темная, но следующий факт Размер = Малая не дает новой информации Чтобы понять это равенство, представим себе двухшаговую процедуру, показанную на рис. 4.9. В начале мы ничего не знаем о картине и имеем априорное распределение вероятности Темы, Размера и Яркости. Согласно этому распределению, Р(Яркость = Темная) = 0.7. Затем мы наблюдаем факт Тема = Ландшафт и вычисляем апостериорное распределение вероятности Р(Яркость = Темная | Тема = Ландшафт) = 0.3. Согласно априорному распределению, ландшафты бывают темными реже, чем портреты, поэтому знание факта Тема = Ландшафт уменьшило нашу веру в то, что Яркость = Темная. А теперь хитрость: это апостериорное распределение становится априорным распределением для следующего наблюдаемого факта: Размер = Малая. Мы вычисляем новое апостериорное распределение, 132
4.3. Составные части вероятностных моделей согласно которому Р(Яркость = Темная | Размер = Малая, Тема = Ландшафт)= 0.3, т. е. дополнительный факт Размер = Малая не дал никакой информации о Яркости в дополнение к той, что мы уже имели после ознакомления с фактом Тема = Ландшафт. Внимательно взглянув на априорное распределение, мы заметим, что в нижней части таблицы, где Тема = Ландшафт, отношение Темной Яркости к Яркой везде равно 3:7, тогда как в верхней половине, где Тема = Люди, оно равно 4:1. Эти отношения различны, но после фиксации Темы отношение тоже становится фиксированным, так что Размер и Яркость условно независимы при условии Темы. Кодирование зависимостей в вероятностной программе Зависимостям посвящена глава 5. Там в качестве представления в основном используются байесовские сети. Сейчас я хочу дать их предварительное описание. Одна из сквозных тем этой книги заключается в том, что зависимости между переменными можно закодировать в виде сети, в которой от родительской переменной к дочерней направлена стрелка, если значение родителя влияет на значение потомка. В нашем примере можно представить себе, что художник сначала выбирает Тему картины, а это влияет на Размер и Яркость, но друг на друга Размер и Яркость не влияют. Эти связи улавливаются сетью на рис. 4.10. Ребра в байесовской сети всегда направлены от родителя к потомку, и обычно наличие ребра означает, что родитель как-то влияет на потомка. В главе 5 рассматриваются детали эти причинно-следственных связей и правила изображения байесовской сети. Размер зависит от Темы Яркость зависит от Темы, но не зависит от Размера Рис. 4.10. Простая байесовская сеть, отражающая условную независимость Размера и Яркость при условии заданной Темы Сеть, в которой таким образом показаны зависимости между переменными, называется байесовской сетью. Узлы сети соответствуют переменным модели, и два узла соединены ребром, если первый влияет на второй. Как станет ясно в главе 5, в байесовской сети хранится очень много информации о зависимостях, существующих в модели. Структура байесовской сети естественно транслируется в структуру вероятностной программы. Если одна переменная является родителем другой в сети, то 133
Глава 4. Вероятностные модели и вероятностные программы первая влияет на вторую. В обычной программе мы считаем, что одна переменная влияет на другую, если первая встречается в определении второй. Точно так же обстоит дело и в вероятностном программировании: если в байесовской сети существует ребро, ведущее из одной переменной в другую, то первая будет входить в определение второй. Это одна из фундаментальных связей между вероятностным моделированием и программированием, благодаря которой вероятностное программирование вообще возможно. Уяснив этот момент, уже просто понять, как представляются зависимости в вероятностных программах. В Figaro программа имеет следующую структуру: val subject = // определение val size = // в определении используется subject val brightness = // в определении используется subject Важно, что в определении brightness не используется size. В главе 5 мы узнаем, что таким образом отражается факт условной независимости brightness от size При УСЛОВИИ subject. Сейчас у нас есть четыре переменных, и мы знаем их зависимости, выраженные в виде сети. Но мы еще не знаем, как выглядят эти зависимости и какую форму они принимают. Для описания этих аспектов нужны еще два ингредиента: функциональные формы и числовые параметры. 4.3.3. Функциональные формы Итак, мы выбрали переменные и описали их зависимости в виде сети. Я уже говорил, что зависимости - ключ к описанию распределения вероятности нескольких переменных с помощью простых компонентов. Эти компоненты должны характеризовать переменные только в терминах тех переменных, с которыми они связаны. Следующий шаг - определить точные формы компонентов, характеризующих такие связи. Для примера на рис. 4.10 нам надо было бы описать, как выбирается значение Темы и как оно используется для выбора значений Размера и Яркости. Базовые функциональные формы Начнем с первого вопроса: как выбирается значение Темы. Поскольку Тема ни от каких переменных не зависит, то нужно всего лишь задать распределение вероятности ее значений. В нашем примере тип Темы - перечисление, она может принимать два значения: Люди и Ландшафт, вот их вероятности и нужно задать. Явное задание вероятностей - самый простой способ описать распределение вероятности. Но если значений много или непосредственно выразить вероятности затруднительно, то такой подход неосуществим. Вместо этого распределение вероятности часто задается в виде одной из стандартных функциональных форм. Вообще говоря, выбор формы зависит от типа переменной. У каждой формы имеются специфические свойства, и каждая рас- 134
4.3. Составные части вероятностных моделей считана на определенное применение. Чтобы выбрать форму, подходящую для приложения, нужно понимать, как различные формы используются. Например, для непрерывных переменных нередко применяется нормальное или экспоненциальное распределение. А для целочисленных переменных - биномиальное и геометрическое распределение. В этой книге мы, как правило, не будем вдаваться в детали конкретных функциональных форм. На эту тему есть масса материалов, в том числе и в википедии. В Figaro имеются представления многих функциональных форм, мы видели это в главе 2. Для представления формы одной переменной обычно используется атомарный элемент. Например, элемент select представляет явное назначение вероятностей с заданным средним и дисперсией, а элемент Binomial - биномиальное распределение с заданными числом испытаний и вероятностью успеха в одном испытании. Условные распределения вероятности Ну а что сказать о двух других переменных - Размер и Яркость, - зависящих от Темы? Для них недостаточно просто задать базовую функциональную форму, нужно еще определить, как изменяется распределение вероятности для разных значений Темы. С этой целью мы определяем условное распределение вероятности, т. е. для каждого значения Темы задается распределение вероятности Размера. Например, задается вероятность, что Размер = Большая при условии Тема = Ландшафт, или в нашей нотации Р(Размер = Большая | Тема = Ландшафт). Проще всего это сделать, задав для каждого возможного значения Темы соответствующую функциональную форму Размера. В нашем примере Размер является перечислением, поэтому функциональная форма естественно принимает вид явного задания вероятностей. Представить условное распределение Размера при условии Темы можно в табличном виде, как показано в табл. 4.7. Я пользуюсь стандартным представлением, в котором показана зависимая переменная и переменные, от которых она зависит. Последние находятся слева. В данном случае независимая переменная всего одна, Тема, и она занимает левый столбец. Но можно представить себе модель, в которой одна переменная зависит от нескольких, тогда все они располагались бы слева. Справа мы видим переменную, для которой определяется условное определение, под каждое возможное значение отведен отдельный столбец. Здесь зависимая переменная Размер принимает три значения: Малая, Средняя и Большая. Для каждого возможного значения зависимой переменной имеется отдельная строка. У нас таких значений два: Люди и Ландшафт. Если бы независимых переменных было больше, то в таблице было бы по одной строке для каждой комбинации их возможных значений. Наконец, в ячейке на пересечении строки г и столбца^ находится вероятность того, что зависимая переменная принимает значение в столбце^ при условии, что независимые имеют значения, указанные в строке L Например, в ячейке (Люди, Малая) находится вероятность Р(Размер = Малая | Тема = Люди). Я сопоставил вероятностям метки от pi до рб, которые используются во фрагменте кода ниже и позволяют сравнить код с таблицей. 135
Глава 4. Вероятностные модели и вероятностные программы Таблица 4.7. Форма условного распределения вероятности Размера при условии Темы Тема Люди Ландшафт Размер Малая р1 = Р( Размер = Малая | Тема = Люди) р4 = Р( Размер = Малая | Тема = Ландшафт) Средняя р2 = Р( Размер = Средняя | Тема = Люди) р5 = Р( Размер = Средняя | Тема = Ландшафт) Большая рЗ = Р(Размер = Большая | Тема = Люди) рб = Р(Размер = Большая | Тема = Ландшафт) В Figaro условное распределение вероятности можно реализовать с помощью составного элемента, который точно описывает, как одна переменная зависит от других. В главе 2 мы видели, что в общем виде определить произвольную функциональную зависимость между независимой переменной и элементом над зависимой переменной позволяет составной элемент chain. Но существует также много конкретных функциональных форм. Одна полезная форма, естественно, называется cpd (conditional probability distribution - условное распределение вероятности). В Figaro ее можно использовать следующим образом: val size = CPD(subject, fpeople -> Select (pi -> 'small, p2 -> fmedium, p3 -> flarge), 'landscape -> Select(p4 -> 'small, p5 -> 'medium, рб -> 'large) ) Переменные Scala pi,..., рб представляют числовые вероятности, о которых мы будем говорить в следующем разделе. Примечание. Одиночная кавычка ' означает, что дальше идет символ. Символом в Scala называется уникальное имя чего-то. Например, символ 'small имеет одно и то же значение всюду, где встречается, и отличается от значения любого другого символа. Символы можно только сравнивать между собой на равенство. Они полезны, когда требуется создать конечный набор значений переменной. Форму cpd можно использовать и когда переменных больше двух. Например, допустим, что переменная Цена зависит от того, написана ли картина Рембрандтом, и от ее темы. В этом случае можно написать такую форму: val price = CPD(rembrandt, subject, (false, 'people) -> Flip(pi), (false, 'landscape) -> Flip(p2), (true, 'people) -> Flip(p3), (true, 'landscape) -> Flip(p4) Конструктор cpd принимает два набора аргументов: 1. Родители определяемой переменной. Их может быть не больше пяти. 136
4.3. Составные части вероятностных моделей 2. Несколько ветвей, число которых зависит от возможных значений родителей. Каждая ветвь (clause) включает условие - комбинацию значений родителей - и результирующий элемент. Например, в первой ветви cpd для size условием является 'people, а результатом - элемент select (pi -> 'small, p2 -> 'medium, рЗ -> ' large). Это Означает, ЧТО еСЛИ subject paBHO 'people, ТО Значение size Выбирается В СООТВетСТВИИ С Элементом Select. Если родитель всего один, то условие состоит просто из значения родителя. Если же родителей несколько, то условием будет кортеж значений каждого родителя. Например, в первой ветви cpd для price условие имеет вид (false, 'people), т. е. ветвь применяется, когда переменная rembrandt равна false, а переменная subject равна 'people. Другие способы задания зависимостей в Figaro В Figaro имеется также конструктор RichCPD, позволяющий представить условные распределения вероятности гораздо более компактно - не выписывая целиком всю таблицу. RichCPD похож на cpd, но условие в каждой ветви описывает множество возможных значений, а не одно значение. Множество можно задать одним из трех способов: (1) oneof (values) означает, что ветвь применяется, если родитель принимает любое из перечисленных значений; (2) Noneof (values) означает, что родитель не должен принимать ни одно из перечисленных значений; (3) * означает, что ветвь применяется, если родитель принимает любое значение в предположении, что у остальных родителей подходящие значения. Для конкретного множества значений родителей выбирается первая подходящая ветвь, которая и определяет распределение для элемента. RichCPD полезен, когда имеются условные распределения вероятности с несколькими родителями. Обычно число строк в условном распределении равно произведению количества возможных значений каждого родителя. Если один родитель может принимать два значения, второй - три, а третий - четыре, то число строк в условном распределении равно 2 х 3 х 4 = 24. Как видим, явно описывать распределение, когда родителей несколько или они принимают достаточно много значений, утомительно. Как раз для таких ситуаций и предназначена форма RichCPD. Приведем пример: val xl = Select (0.1 -> 1, 0.2 -> 2, 0.3 -> 3, 0.4 -> 4) val x2 = Flip(0.6) val у = RichCPD (xl, x2, <—© (OneOf(l, 2), *) -> Flip(O.l), <3—© (NoneOf(4), OneOf(false)) -> Flip(0.7), <3—© (*, *) -> Flip(0.9)) <J—О О — RichCPD с двумя родителями © — Соответствует случаю, когда х1 равно 1 или 2, а х2 любое © — Соответствует случаю, когда х1 — любое значение, кроме 4, а х2 равно false © — Ветвь выбирается по умолчанию во всех остальных случаях 137
Глава 4. Вероятностные модели и вероятностные программы Предупреждение. Важно, чтобы условия в конструкторах cpd и RichCPD покрывали все значения родителей, имеющие ненулевую вероятность. В противном случае Figaro не будет знать, что делать, когда встретится значение, не соответствующее ни одному условию. Для cpd это означает, что таблица должна быть задана полностью, а для RichCPD можно последней указать ветвь «по умолчанию», в которой для значения каждого родителя задана *, - как в приведенном выше примере. cpd и RichCPD - два явных способа записи зависимостей, полезные для дискретных переменных. Но не думайте, что ничего другого в Figaro нет - это лишь самые простые и явные методы. А в более общих случаях зависимости можно описать с помощью элементов Apply и chain. Напомним (см. главу 2), что: • Apply принимает в качестве аргументов элементы и функцию. Результатом Apply является элемент, значение которого получено применением функции к значениям аргументов. Таким образом, Apply определяет зависимость между аргументами и результатом. • chain принимает в качестве аргументов один элемент и функцию, которая принимает значение этого элемента и возвращает новый элемент. То есть результатом chain является элемент, значение которого порождается следующим образом: взять значение аргумента, применить к нему функцию для получения нового элемента и сгенерировать значение этого элемента. Таким образом, chain также определяет зависимость между аргументом и результатом. • Многие элементы, например if и составной Normal, определены с использованием chain. Поэтому они тоже определяют зависимости. До сих пор мы говорили о том, что такое функциональные формы, и убедились, что Figaro предоставляет их в изобилии. Но без чисел, подставляемых вместо параметров, это не более чем пустая оболочка. Поэтому к ним и перейдем. 4.3.4. Числовые параметры Последний ингредиент вероятностной модели - числовые параметры. Концептуально это самая простая часть. У любой функциональной формы имеются некоторые параметры, вместо которых нужно подставить числа. Однако необходимо, чтобы подставляемые значения были допустимы. Например, если явно задаются вероятности, то их сумма должна быть равна 1. В табл. 4.8 показано конкретное условное распределение вероятности Размера при условии Темы. Заметьте, что суммы значений в каждой строке равны 1. Это обязательно, поскольку в строке находится распределение вероятности Размера. Таблица 4.8. Конкретное условное распределение вероятности Размера при условии Темы Тема Люди Ландшафт Размер Малая 0.5 0.1 Средняя 0.4 0.6 Большая 0.1 0.3 138
4.3. Составные части вероятностных моделей Несмотря на концептуальную простоту, подстановка числовых значений на практике может оказаться самым трудным шагом. Оценка вероятностей на основе опыта - непростое дело. Напомним, что в начале этой главы приводились общие экспертные суждения о картинах: Рембрандт «любил» темные тона, он «часто» рисовал людей, открытие новых подлинников случается «редко», а подделок «пруд пруди». Эксперт в предметной области с легкостью высказывает такие общие суждения, но приписать им числовые значения - совсем другое дело. Рембрандт «любил» темные тона - значит ли это, что Р(Яркость = Темная | Рембрандт = True) = 0.73? Трудно сказать. Но есть и две хорошие новости. Во-первых, как выясняется, во многих случаях точные значения и не нужны. Ответы на запросы обычно оказываются примерно одинаковыми, даже если числовые значения вероятностей немного различаются. Например, если модель говорит, что Р(Яркость = Темная | Рембрандт = True) = 0.77, то заключение о поддельности картины будет почти таким же, как в случае, когда эта вероятность равна 0.73. Это, однако, уже не так, если вероятности близки к 0. Переход от вероятности 0.001 к вероятности 0.0001 может радикально изменить заключение. На границах диапазона важны порядки величины. С другой стороны, эксперту проще дать оценку с точностью до порядка. Во-вторых, эти числа обычно можно вывести из данных методами машинного обучения. Пример мы видели в главе 3. Проблема в том, что для вывода параметров из данных нужно задать какие то априорные значения. Но для них достаточно минимальных предположений, и при наличии достаточно большого объема данных априорные значения слабо влияют на окончательные. Вообще возможность задавать априорные параметры - благо, т. к. это позволяет эксперту в предметной области оказывать влияние на обучение. Если же экспертных знаний нет, то можно задать нейтральные априорные значения, которые не будут смещать результаты обучения ни в каком направлении. В главах 9 и 12 мы узнаем об основных принципах выведения параметров из данных. В главе 9 описываются два общих подхода к машинному обучению. В первом - байесовском обучении - в модель включается распределение вероятности самих значений параметров, и тогда для обучения параметров достаточно обычного вероятностного вывода, в результате которого получается апостериорное распределение их значений. Во втором подходе апостериорное распределение не вычисляется, а генерируется детерминированная оценка значений параметров по результатам обучения на данных. В главе 12 показано, как эти подходы реализованы в Figaro. Итак, мы рассмотрели все ингредиенты практического подхода к представлению вероятностных моделей. Но один важный вопрос остался без ответа. Предположим, что мы строим модель из этих ингредиентов. Как представить в ней распределение вероятности возможных миров? В следующем разделе описываются порождающие процессы, эта концепция очень важна для понимания вероятностного программирования и представления вероятностных моделей. 139
Глава 4. Вероятностные модели и вероятностные программы 4.4. Порождающие процессы Мы начали эту главу с определения вероятностной модели как формального представления распределения вероятности возможных миров. В разделе 4.3 было показано, как строить вероятностные модели из переменных, зависимостей, функциональных форм и числовых параметров, и высказана мысль, что такой подход к представлению - неотъемлемая часть вероятностного программирования. Для ясного понимания смысла вероятностных программ остается замкнуть круг и показать, как в определенных таким образом моделях задается распределение вероятности возможных миров. Здесь нам не обойтись без концепции порождающего процесса. Вероятностное программирование опирается на аналогию между вероятностными моделями и программами на языке программирования. Суть этой аналогии в следующем. • Вероятностные модели, например байесовские сети, можно рассматривать как процесс порождения значений переменных. Этот процесс определяет распределение вероятности возможных миров. • Но и обычная, не вероятностная программа определяет процесс порождения значений переменных. • Объединяя то и другое, получаем, что вероятностная программа пользуется языком программирования, чтобы описать процесс порождения переменных, основанный на случайном выборе. Этот порождающий процесс и определяет распределение вероятности возможных миров. Последнее утверждение нуждается в обосновании. Я объясню, как байесовские сети определяет распределение вероятности с помощью порождающего процесса. А затем проведу аналогию с вероятностными программами. Посмотрим, как байесовская сеть определяет процесс порождения значений переменных. На рис. 4.11 воспроизведена сеть с рисунка 4.10 вместе с условным распределением вероятности каждой переменной при условии переменной, от которой она зависит. Представим себе процесс порождения значений всех переменных в этой сети. Нельзя породить значение переменной, пока не станут известны значения всех переменных, от которых она зависит. Этот процесс иллюстрируется на рис. 4.12. Мы начинаем с Темы, потому что она не зависит ни от каких переменных. Согласно условному распределению вероятности Темы, значение Люди имеет вероятность 0.8, а значение Ландшафт - вероятность 0.2. Случайно выберем одно из этих значений, соблюдая распределение вероятности. Допустим, что выбран Ландшафт. Затем переходим к выбору Размера. Это можно сделать, потому что значение переменной Тема, от которой зависит Размер, уже известно. Ищем в таблице условного распределения вероятности Размера строку, соответствующую Ландшафту. В ней значение Малая имеет вероятность 0.25, Средняя - 0.5, а Большая - 0.25. Случайно выберем одно из трех значений, не забывая о вероятностях. Допустим, выбрано значение Средняя. 140
4.4. Порождающие процессы Значения вероятности Темы Тема Люди 0.8 Ландшафт 0.2 Значения вероятности Размера при условии Темы Значения вероятности Яркости при условии Темы Тема Люди Ландшафт Малая 0.25 0.25 Размер Средняя 0.25 0.5 Большая 0.5 0.25 Тема Люди Ландшафт Яркость Темная | Яркая: 0.8 0.2 0.3 0,7 Рис. 4-11. Байесовская сеть с тремя узлами и условные распределения вероятности 1. Выбираем значение Темы из этой строки РЕЗУЛЬТАТ: Ландшафт Значения вероятности Темы Тема Люди 0.8 Ландшафт 0.2 Значения вероятности Размера при условии Темы Значения вероятности Яркости при условии Темы Тема Люди Ландшафт Размер Малая Средняя Большая 0.25 0.25 0.5 0.25 0.5 0.25 ; Тема Люди Ландшафт Яркость Темная Яркая 0.8 0.2 0.3 0.7 Выбираем значение Размера из этой строки РЕЗУЛЬТАТ: Средняя Строка условного распределения вероятности, соответствующая значению Тема = Ландшафт Выоираем значение Яркости из этой строки РЕЗУЛЬТАТ: Яркая Рис. 4.12. Процесс порождения значений всех переменных в сети с тремя узлами. Значение переменной порождается путем выбора значения с соблюдением условного распределения вероятностей в строке, зависящей от значений родителей 141 Тема Яркость ; Размер Тема Яркость Размер
Глава 4. Вероятностные модели и вероятностные программы Наконец, мы выбираем значение Яркости. Делается это аналогично выбору значения Размера, потому что Яркость зависит только от Темы. Допустим, выбрано значение Яркая. В итоге сгенерированы значения всех трех переменных: Тема = Ландшафт, Размер = Средняя, Яркость = Яркая. В процессе порождения значений для каждой переменной есть несколько возможностей. Можно нарисовать дерево, на котором все они будут изображены. Такое дерево для нашего примера показано на рис. 4.13. В корне находится первая переменная, Тема. Из нее исходят ветви для обоих возможных значений: Люди и Ландшафт. Каждая ветвь аннотирована вероятностью ее выбора. Так, согласно распределению вероятности Темы, ветвь Ландшафт помечена вероятностью 0.2. Затем мы переходим к следующей переменной, Размер, которая находится в двух узлах дерева, соответствующих различным выборам значения Темы. Из каждого узла Размер исходят три ветви, соответствующие трем возможным значениям. Отметим, что для разных узлов этим ветвям сопоставлены разные вероятности, поскольку их значения берутся из разных строк таблицы условного распределения Размера. На конце каждой ветви, исходящей из обоих узлов Размер, находится узел Яркость с двумя исходящими ветвями, соответствующими значениям Яркая и Темная. Вероятности, проставленные на этих ветвях, как и раньше, берутся из соответствующих строк таблицы условного распределения Яркости. Отметим, что для всех трех узлов в левой части вероятности ветвей одинаковы, и то же самое верно для трех узлов в правой части. Объясняется это тем, что Яркость зависит только от Темы и не зависит от Размера. А для трех узлов в левой части значение Темы одно и то же. Тема Люди 0.8 J Ландшафт 0.2 Размер Размер Малая 0,25 Большая 0,5 Малая 0,25 Средняя 0.25 I Яркость Яркость Яркость Яркость Темная 0,8 Темная 0,8; Темная 0,8 Темная 0,3 Темная 0,3 Яркая 0,2 / Яркая 0,2 / Яркая 0.2 / Яркая 0.7 / Яркая 0.7 I / ' \ I ' \ I \ I ' \ 0.16 0.04 0.16 0.04 0.32 0.08 0.015 0.035 0.03 0.07 0.015 Большая 0,25 Средняя 0.5 I Яркость Яркость Темная 0,3 Яркая 0,7 0.035 Вероятности возможных миров вычисляются путем перемножения вероятностей на пути к миру. В сумме эти вероятности дают 1 Рис. 4.13. Дерево порождения для байесовской сети с тремя узлами 142
4.4. Порождающие процессы Спускаясь по дереву на рис. 4.13, мы делаем выбор в каждом узле. Получается последовательность выборов, изображенная на рис. 4.14. Путь в дереве определяет один из возможных миров. Какова его вероятность? Ответ следует из цепного правила - одного из правил вывода, о которых мы узнаем в третьей части. Согласно этому правилу, вероятность мира равна произведению вероятностей ветвей, выбиравшихся при порождении этого мира. В нашем примере вероятность мира Тема = Ландшафт, Размер = Средняя, Яркость = Яркая равна 0.2 х 0.5 х 0.7 = 0.07. Нетрудно доказать, что сумма вероятностей всех возможных миров равна 1. Поэтому байесовская сеть определяет распределение вероятности возможных миров. Теперь стало понятно, как байесовская сеть описывает процесс порождения значений переменных. Но и программа на обычном языке программирования тоже описывает процесс порождения значений переменных, только в ней нет места случайному выбору. Стало быть, вероятностная программа похожа на обычную с тем отличием, что процесс порождения значений включает случайный выбор. Вероятностная программа напоминает байесовскую сеть, только для описания порождающего процесса в ней используются конструкции языка программирования. А раз в нашем распоряжении имеется язык программирования, то возможностей гораздо больше, чем в байесовской сети. У нас есть средства управления потоком выполнения, например циклы и функции, и сложные структуры данных такие, как множества и деревья. Но принцип тот же самый: программа описывает процесс порождения значений всех переменных. Малая 0.25 Темная 0.8 Яркость / Тема Люди 0.8 Ландшафт 0.2 Размер Большая 0.5 Средняя 0.25 I Яркость Яркость I Яркость Темная 0.8 / \ Темная 0.8 / \ Темная 0.3 / \ Темная Большая 0.25 Яркая 0.2 I 0.16 0.04 Яркая 0.2 \ 0.16 0.04 Яркая 0.2 \ 0.32 0.08 Яркая 0.7 \ 0.015 0.035 Яркая 0.7 \ 0.03 0.07 Яркая 0.7 \ 0.015 0.035 Вероятность выбранного мира равна 0.2 х 0.5 х 0.7 = 0.07 Рис. 4.14. Последовательность выборов и ее вероятность Выполняя вероятностную программу, мы порождаем значения каждой переменной, пользуясь определением, в котором указано, от каких переменных она Малая 0.25 Размер Средняя 0.5 Яркость Яркость Темная 0.3 143
■Illl Глава 4. Вероятностные модели и вероятностные программы зависит. По завершении программы переменные из некоторого набора получают значения. Этот набор переменных вместе с порожденными значениями определяет возможный мир. Как и в байесовской сети, вероятность этого мира равна произведению вероятностей каждого случайного выбора. Но существует и важное различие между вероятностными программами и байесовскими сетями: набор переменных, которым программа присваивает значения, может изменяться от прогона к прогону. Рассмотрим такую программу на Figaro: Chain(Flip(0.5), (b: Boolean) => if (b) Constant(0.3) else Uniform(0, 1)) Элемент constant (0.3) создается, только если Flip принимает значение true, а элемент uniform(0, 1) - если Flip равен false. Ни в каком прогоне программы не могут быть созданы оба элемента. Тем не менее, каждый прогон дает возможный мир с корректно определенной вероятностью. И хотя доказать это несколько труднее, сумма вероятностей всех возможных миров по-прежнему равна 1. Поэтому вероятностная программа описывает распределение вероятности возможных миров. Предостережение. На языке программирования можно написать программу, которая никогда не завершается, и то же верно в отношении вероятностных программ. Если существует ненулевая вероятность, что случайный процесс, определяемый вероятностной программой, не завершится, то эта программа не определяет корректное распределение вероятности возможных миров. Представьте вероятностную программу, в которой определена рекурсивная функция, продолжающая вызывать себя до бесконечности; процесс, описываемый такой программой, не завершается. Теперь вы, надо думать, понимаете, что такое вероятностная модель и как ей пользоваться. А равно и то, как вероятностная программа представляет все составные части вероятностной модели. Прежде чем закончить эту главу, осталось рассмотреть один технический вопрос: как использовать в вероятностной модели непрерывные переменные? 4.5. Модели с непрерывными переменными В примерах вероятностных моделей, которым нам встречались до сих пор, использовались дискретные переменные, значения которых далеко отстоят друг от друга, как у перечислений и целых чисел. В то же время значения непрерывных элементов не разделены промежутками - как в случае вещественных чисел. В главе 2 мы мельком отмечали, что непрерывные переменные требуют другого обращения, нежели дискретные, и обещали дать подробные объяснения позже. Теперь для этого настало время. Проблема заключается в том, что трудно определить распределение вероятности всех значений непрерывного элемента. Рассмотрим произвольный интервал вещественных чисел. Если приписать всем числам из этого интервал положитель-
4.5. Модели с непрерывными переменными ные вероятности, то сумма вероятностей будет бесконечна. И это верно даже для самого маленького интервала. Ну и как же тогда определять вероятности непрерывных переменных? Ответ дается в этом разделе. В качестве конкретного примера возьмем бета-биномиальную модель, которая применяется во многих приложениях. В главе 3 мы использовали ее для описания количества необычных слов в почтовом сообщении. А сейчас детализируем эту модель, чтобы проиллюстрировать изученные выше идеи. Модель содержит непрерывную переменную, и я объясню, как эту переменную представить, а попутно мы поговорим о непрерывных переменных вообще. 4.5.1. Бета-биномиальная модель Модель называется бета-биномиальной, потому что в ней одновременно используется два распределения: бета и биномиальное. Рассмотрим простой пример. Пусть имеется несимметричная монета, т. е. вероятность выпадения орла и решки различна. Наша цель - предсказать вероятность выпадения орла при условии, что известны результаты прошлых подбрасываний монеты. Допустим, что мы наблюдали 100 подбрасываний и хотим узнать, с какой вероятностью в 101-ый раз выпадет орел. Сначала определим переменные. В этом примере их будет три: • Степень асимметрии монеты, так и назовем ее Асимметрия. • Сколько раз в предыдущих 100 подбрасываниях выпадал орел. Назовем эту переменную ЧислоОрлов. Это целочисленная переменная, принимающая значения от 0 до 100. • Результат 101-го подбрасывания - Бросок101. Модель зависимостей представлена байесовской сетью на рис. 4.15. Напомним, что хотя мы выводим степень асимметрии из числа выпавших орлов, зависимости в модели направлены в противоположную сторону. В причинно-следственном смысле именно асимметрия монеты определяет результат подбрасывания, что, в свою очередь, определяет число выпавших орлов, поэтому ребро и направлено от Асимметрии к ЧислуОрлов. Рис. 4.15. Байесовская сеть в примере с подбрасыванием монеты. Результаты первых 100 подбрасываний представлены в виде одной переменной, равной количеству выпавших орлов Вы, наверное, задаетесь вопросом, почему нет ребра из ЧислаОрлов в Бросок101. Ведь число орлов, выпавших в первых 100 подбрасываниях, дает информацию, 145 Ассиметрия ЧислоОрлов Бросок 10;
Глава 4. Вероятностные модели и вероятностные программы имеющую прямое отношение к 101-му броску. Причина в том, что вся эта информация поглощается асимметрией монеты. Первые 100 подбрасываний говорят нам о степени асимметрии, а эта информация затем используется для предсказания значения Броска10Г Если мы уже знаем степень асимметрии, то первые 100 подбрасываний к этому ничего не добавят. Другими словами, переменная Бросок101 условно независима от ЧислаОрлов при условии Асимметрии. Теперь можно выбирать функциональные формы. Для каждой переменной у нас будет своя форма. Начнем с Броскашг Это подбрасывание монеты, в результате которого выпадает орел или решка. Тут подойдет одна из простейших форм. Требуется только задать вероятность выпадения орла или решки. В нашем случае вероятность орла определяется значением переменной Асимметрия. Орел выпадает с вероятностью Асимметрия, а решка - с вероятностью 1 - Асимметрия. В Figaro мы можем воспользоваться составным элементом Flip. Далее нужна функциональная форма, характеризующая вероятность того, что при 100 подбрасываниях выпадает заданное число орлов при условии, что вероятность одного выпадения орла равна значению Асимметрии. В главе 2 мы видели, что для этой цели подходит биномиальное распределение, которое описывает результат повторения эксперимента, имеющего два возможных исхода. Оно показывает, с какой вероятностью будет иметь место заданное число исходов одного вида. В нашем примере это вероятность выпадения заданного числа орлов после 100 подбрасываний. В общем случае биномиальное распределение зависит от двух параметров: количества испытаний (в нашем примере - 100) и вероятности желаемого исхода испытания (в нашем примере - выпадения орла). Зная эту вероятность, легко вычислить вероятность выпадения заданного количества орлов, скажем 63. Но в нашем примере эта вероятность определена значением переменной Асимметрия. Нам нужна функциональная форма для Асимметрии. Вы, конечно, догадались - по названию модели - что асимметрия будет подчиняться бета-распределению. Вообще, бета-биномиальной называется вероятностная модель, которая содержит биномиальную переменную, представляющую количество успешных исходов испытаний таких, что вероятность успеха в одном испытании определяется переменной, подчиняющейся бета-распределению. Итак, мы знаем, что асимметрия имеет бета-распределение. Но в чем точный смысл этого утверждения? Как все-таки определить распределение вероятности непрерывной переменной? 4.5.2. Представление непрерывных переменных Значением Асимметрии является вещественное число от 0 до 1, и это непрерывная переменная. Для непрерывных переменных необходимо по-другому определять распределение вероятности. Пусть, например, мы предполагаем, что высота картины находится в пределах от 50 до 150 см и что нет оснований полагать, что одна высота более вероятна, чем любая другая. Тогда область значений - диапазон от 50 до 150, и предполагается, что вероятность принадлежности этому диапазону 146
4.5. Модели с непрерывными переменными распределена равномерно. Пусть мы хотим узнать, с какой вероятностью высота картина в точности равна 113.296 см. Проблема в том, что возможных значений высоты бесконечно много. Если бы у любой возможной высоты была положительная вероятность, скажем 0.2, и мы считаем, что все значения высоты равновероятны, то оказалось бы бесконечно много значений с вероятностью 0.2, так что сумма всех вероятностей попадания в диапазон от 50 до 150 см была бы бесконечна. А мы знаем, что полная вероятность должна быть равна 1. Решение заключается в том, чтобы рассматривать не вероятности отдельных значений высоты, а вероятности интервалов высот. Например, на вопрос, какова вероятность, что высота находится между ИЗ и 114 см, можно было бы ответить «1 из 100», потому что длина этого интервала равна 1, т. е. это один из 100 равновероятных интервалов такой длины. На самом деле, можно поинтересоваться вероятностью любого, сколь угодно малого интервала вокруг значения 113.296. Например, вероятность интервала от 113.2 до 113.3 равна 1 из 1000, интервала от 113.29 до 113.30 - 1 из 10 000 и т. д. Итак, для непрерывных переменных вероятность отдельного значения обычно равна 0. Рассмотрим степень асимметрии монеты. Вероятность, что она равна в точности 0.49326486..., нулевая, поскольку значений бесконечно много. Но у интервалов, содержащих значение, вероятность положительна. Например, асимметрия может находиться в интервале от 0.49326 до 0.49327. Существует математически корректный способ определить вероятность любого интервала. Он изображен на рис. 4.16 и называется функцией плотности распределения вероятности (ФПР). ФПР позволяет задать вероятность любого интервала. Плотность задается в каждой точке. Если интервал мал, то плотность часто оказывается приблизительно одинаковой в каждой его точке, поэтому вероятность интервала равна значению этой константы, умноженному на длину интервала. На рис. 4.16 показана равномерная ФПР, т. е. плотность всюду равна 0.01, поэтому вероятность интервала единичной длины (ИЗ, 114) равна 0.01. Если функция плотности распределения постоянна, то умножение этой константы на длину интервала равно площади под кривой плотности на этом интервале. В общем случае плотность в разных точках может быть различна. Но и тогда вероятность интервала равна площади под кривой на этом интервале. Если вы еще не забыли математический анализ, то знаете, что эта площадь равна интегралу от функции плотности распределения на интервале. Я не стану в этой книге ни экзаменовать вас по матанализу, ни вычислять площади под кривыми - Figaro об этом позаботится, но я хочу, чтобы вы понимали различие между непрерывными и дискретными распределениями и знали, что такое функция плотности. Вернемся к нашему примеру. Мы ищем функциональную форму для Асимметрии, для этого нужно задать ФПР. Мы знаем, что значение асимметрия - число от 0 до 1, поскольку это вероятность, поэтому плотность должна быть равна 0 вне интервала (0, 1). Оказывается, что есть функциональная форма, прекрасно подходящая как раз к этому случаю, - бета-распределение. Оно применяется для моделирования вероятности определенного исхода в одном испытании, если исходов всего два. На рис. 4.17 показан пример бета-распределения. Как видим, оно 147
Глава 4. Вероятностные модели и вероятностные программы положительно в интервале от 0 до 1 и имеет один пик. Чуть ниже мы покажем, как охарактеризовать местоположение и остроту этого пика. Плотность распределения 0.0 Н Функция плотности распределения (ФПР) Вероятность интервала (113,114) = площадь под кривой ФПР = 0.01 50 113114 150 Высота (см) Рис. 4.16. Функция плотности распределения вероятности (ФПР): вероятность интервала равна площади под кривой ФПР на этом интервале Мы выбрали именно бета-распределение для характеристики асимметрии монеты, потому что оно отлично уживается с биномиальным распределением. Точнее, если имеется факт - ЧислоОрлов - и известно, что Асимметрия имеет априорное бета-распределение, то ее апостериорное распределение после ознакомления с числом выпавших орлов тоже будет бета-распределением. В третьей части книги мы увидим, что это позволяет без труда предсказать исход 101-го подбрасывания. Подведем итоги. У нас имеются следующие переменные и функциональные формы: • Асимметрия, характеризуемая бета-распределением; • ЧислоОрлов, характеризуемое биномиальным распределением с общим числом испытаний 100 и вероятностью выпадения орла в одном испытании, равной значению Асимметрии; • Бросок101, характеризуемый элементом шр, в котором вероятность выпадения орла равна значению Асимметрии. Программу на Figaro для этой модели можно начать так: val bias = Beta(?,?) val numberOfHeads = Binomial(100, bias) val tosslOl = Flip(bias) Теперь время выполнить четвертый шаг: выбрать числовые параметры. Из заготовки программы видно, что параметры для переменных numberofHeads и tossioi определяются значением bias, которой уже является элементом программы. Остается задать только параметры bias. Для этого нужно понимать, как выбираются параметры бета-распределения. 148
4.5. Модели с непрерывными переменными У бета-распределения много вариантов, на рис. 4.7 изображен только один из них. Каждый вариант характеризуется двумя числовыми параметрами: а и (3. на рисунке показано распределение beta (2, 5), для которого а = 2, а (3 = 5. Как выясняется, у а и (3 имеется естественная интерпретация. Это число наблюдений каждого из двух исходов плюс 1. В нашем примере а - число выпавших орлов плюс 1, а р - число выпавших решек плюс 1. Следовательно, априорные значения а и (3 - до подбрасывания монеты - это наша гипотеза о поведении монеты, выраженная в терминах воображаемых предыдущих подбрасываний. Если на основе экспериментов с подобными монетами вы полагаете, что эта монета будет чаще выпадать орлом, чем решкой, то можете задать а > р. И чем больше вы доверяете своему опыту, тем больше будут значения аир. Плотность распределения 2.5 1.5 Ч И 0.5 0 О СМ "^ Ю 00 . О О О О q о о о о ^с\1^сососчс\1^союсосч^<ода^сч^юдаюсч^ю»сосч^юдаг^сч^юда Qr•т•^т-Q(^Jc^lc^^(^J^nAcocoQ^^тr^QlOюююQ<DЮ(o<DQN^^^QOOooooooQO><»o>0) оооо оооо оооо оооо оооо оооо оооо оооо оооо Асимметрия Рис. 4,17. Функция плотности распределения beta (2, 5) Так, на рис. 4.17, а = 2, (3 = 5. Из того, что (3 больше а, следует, что априорно мы склонны считать, что асимметрия в пользу решки (вероятность выпадения орла мала). И действительно, мы видим, что плотность вероятности малых значений асимметрии (< 0.5) выше плотности вероятности больших значений (> 0.5). Кроме того, такой выбор значений свидетельствует о том, что в воображаемом эксперименте выпал один орел и четыре решки, и мы заложили в модель эти априорные знания об асимметрии - потому-то ФПР не является постоянной функцией. Ну, вот, наконец, и всё. Мы рассмотрели все четыре составные части вероятностной модели и видели, как они представляются в вероятностной программе. Теперь можно с головой погрузиться в методы моделирования и паттерны проектирования. В следующей главе мы начнем детально обсуждать зависимости и две системы их представления: байесовские и марковские сети. 149 2
Глава 4. Вероятностные модели и вероятностные программы 4.6. Резюме • Вероятностные модели представляют общие знания о предметной области в виде распределения вероятности возможных миров. • Факты используются для исключения несовместимых с ними миров и нормировки вероятностей оставшихся. В системах вероятностного программирования для этого применяются алгоритмы вывода. • Для построения вероятностной модели необходимо определить переменные и их типы, задать зависимости между переменными в виде сети, а также выбрать функциональную форму и числовые параметры для каждой переменной. • Функциональная форма непрерывной переменной задается в виде функции плотности распределения вероятности. • Составные части вероятностной модели определяют процесс порождения значений всех переменных. • В вероятностных программах порождающие процессы описываются на языке программирования. 4.7. Упражнения Решения избранных упражнений имеются на сайте www.manning.com/books/ practical-probabilistic-programming. 1. Рассмотрим игру в покер с колодой из пяти карт: туз пик, король пик, король червей, дама червей и дама пик. Каждый игрок получает по одной карте. Каковы возможные миры? В предположении, что колода хорошо перетасована, какова вероятность каждого возможного мира? 2. В той же игре мы наблюдаем факт: у одного игрока на руках картинка (дама или король). Какова вероятность, что у другого игрока на руках пика? 3. Усложним правила этой игры, добавив торговлю. Правила торговли таковы: • Игрок 1 может сделать ставку или спасовать. • Если игрок 1 делает ставку, то: - игрок 2 может сделать свою ставку или сбросить карты. • Если игрок 1 пасует, то: - игрок 2 может сделать ставку или спасовать. • Если игрок 2 делает ставку, то: - игрок 1 может сделать свою ставку или сбросить карты. Возможные исходы раунда торговли таковы: (1) оба игрока сделали ставки; (2) оба игрока спасовали; (3) один игрок сделал ставку, а другой спасовал. Если оба спасовали, то никто ничего не выиграл и не проиграл. Если один сделал ставку, а другой сбросил карты, то сделавший ставку выигрывает 1 доллар. Если оба сделали ставку, то игрок, имеющий на руках карту 150
1.7. Упражнения более высокого достоинства, выигрывает 2 доллара. Если у обоих на руках карты одного достоинства, то пики бьют червы. Требуется построить вероятностную модель этой игры в покер. Цель модели - помочь выбрать действие в любой момент игры, основываясь на прогнозе развития событий при каждом действии. a. Какие переменные будут в модели? b. Каковы зависимости между переменными? c. Каковы функциональные формы этих зависимостей? d. Какие числовые параметры точно известны? А какие придется оценивать или получать методами машинного обучения? 4. Напишите на Figaro программу, представляющую вероятностную модель этой игры. Сделайте какие-нибудь предположения о значениях параметров, подлежащих оцениванию. С помощью своей программы примите следующие решения: a. Вы игрок 1 и получили короля пик. Пасовать или поставить? b. Вы игрок 2 и получили короля червей, противник поставил. Поставить или сбросить карты? c. Посмотрите, можно ли изменить значения оцениваемых параметров, так чтобы программа подсказывала другие решения. 5. Опишите три разных последовательности выборов при выполнении порождающего процесса в программе из упражнения 4. В предположении, что выбраны параметры для первой версии программы, какова вероятность каждой последовательности в отсутствие наблюдаемых фактов? 6. Вы подозреваете, что противник сдает нечестно. Точнее, вы полагаете, что с некоторой неизвестной вероятностью р он сдает вам даму червей, а с вероятностью 1 - р сдает карты случайно, с равномерным распределением. Вы полагаете, чтор подчиняется бета-распределению Beta(l, 20). В 100 сдачах вы 40 раз получили даму червей. С помощью Figaro вычислите, с какой вероятностью следующая сданная карта окажется дамой червей. 151
ГЛАВА 5. Моделирование зависимостей с помощью байесовских и марковских сетей В этой главе. в Типы связей между переменными в вероятностной модели и как эти связи трансформируются в зависимости, в Как выразить различные типы зависимостей в Figaro. в Байесовские сети: модели, представляющие направленные зависимости между переменными. в Марковские сети: модели, представляющие ненаправленные зависимости между переменными. в Практические примеры байесовских и марковских сетей, В главе 4 мы узнали о связях между вероятностными моделями и вероятностными программами, а также рассмотрели составные части вероятностной модели: переменные, зависимости, функциональные формы и числовые параметры. В этой главе мы займемся двумя системами моделирования: байесовскими и марковскими сетями. Эти системы основаны на различных способах представления зависимостей. Зависимости отражают связи между переменными. Ясное понимание типов связей и их трансформаций в зависимости вероятностной модели - одно из важнейших умений проектировщика моделей. Вообще говоря, существуют два вида зависимостей между переменными: направленные (асимметричные) и ненаправленные (симметричные). Байесовские сети представляют направленные зависи-
5.1. Моделирование зависимостей мости, а марковские сети - ненаправленные. Мы также узнаем, как язык программирования позволяет обогатить байесовские сети и тем самым воспользоваться всей мощью вероятностного программирования. Усвоив материал этой главы, вы получите твердые знания об основах вероятностного программирования. Все вероятностные модели сводятся к набору направленных и ненаправленных зависимостей. Вы узнаете, когда следует заводить зависимость между переменными, делать ли ее направленной или ненаправленной и, если направленной, то в какую сторону. В главе 6 мы на основе этих знаний будем создавать более сложные модели с использованием структур данных, а в главе 7 включим в свой арсенал еще и объектно-ориентированное моделирование. В этой главе предполагаются знания об основах Figaro в объеме главы 2. В частности, необходимо понимать элемент chain, лежащий в основе направленных зависимостей, а также условия и ограничения, составляющие основу ненаправленных зависимостей. Еще мы будем использовать элементы cpd и RichCPD, описанные в главе 4. Если вы что-то забыли, ничего страшного - я напомню, когда понадобится. 5.1. Моделирование зависимостей В основе вероятностных рассуждений лежат зависимости между переменными. Две переменные называются зависимыми, если знания об одной дают информацию и о другой. Наоборот, если никакие знания об одной переменной не позволяют сказать что-то о другой, то переменные независимы. Рассмотрим приложение для диагностики компьютерной системы, в котором мы пытаемся рассуждать о сбоях в процессе печати. Пусть есть две переменные: Кнопка Питания Принтера Нажата и Состояние Принтера. Если мы замечаем, что кнопка не нажата, то можем заключить, что принтер не работает. Наоборот, если мы знаем, что принтер не работает, то можем заключить, что, возможно, кнопка питания не нажата. Две переменные, очевидно, зависимы. Зависимости используются для моделирования переменных, которые как-то связаны между собой. Между переменными может быть много разных связей, но только два вида зависимостей. • Направленная зависимость идет от одной переменной к другой. Обычно так моделируются причинно-следственные связи между переменными. • Ненаправленная зависимость моделирует связи, в которых нет очевидного влияния одной переменной на другую. В следующих двух разделах эти виды зависимостей объясняются подробнее и приводятся примеры. 5.7.7. Направленные зависимости Направленная зависимость ведет от одной переменной к другой, обычно они используются для представления причинно-следственных связей. Например, если кнопка питания принтера не нажата, то принтер не работает, поэтому существует 153
Глава 5. Моделирование зависимостей с помощью байесовских. направленная зависимость между переменными Кнопка Питания Принтера Нажата и Состояние Принтера. Эта зависимость показана на рис. 5.1, где между узлами, представляющими переменные, проведено ориентированное ребро {ребро - стандартный термин для обозначения стрелки между двумя вершинами, или узлами графа). Первая Рис, 5.1. Направленная переменная (в данном случае Кнопка Питания зависимость, выражающая Принтера Нажата) называется родителем, вторая причинно-следственную связь (Состояние Принтера) - потомком. Почему стрелка направлена от причины к следствию? Объяснение, лежащее на поверхности, - потому что причина случается раньше следствия. Более глубокий ответ связан с концепцией порождающей модели, о которой шла речь в главе 4. Напомним, что порождающая модель описывает процесс порождения значений всех переменных модели. Типичный порождающий процесс имитирует то, что происходит в реальном мире. Если некая причина вызывает некое следствие, то сначала мы хотели бы породить значение причины, а затем использовать его для порождения следствия. В примере с моделью принтера сначала нужно породить значение переменной Кнопка Питания Принтера Нажата, а затем на его основе - значение Состояния Принтера. Еще раз повторю, что направление зависимости необязательно совпадает с направлением рассуждения. Можно рассудить, что если кнопка питания не нажата, то принтер не работает, но можно и в обратном направлении: если принтер включен, то кнопка питания наверняка нажата. Многие совершают типичную ошибку: строят модель в том же направлении, в каком рассуждают. В приложении для диагностики мы наблюдаем, что принтер не работает, и пытаемся определить причины, поэтому рассуждение идет в направлении от Состояния Принтера к Кнопка Питания Принтера Нажата. Возможно, вы испытываете соблазн провести стрелку в том же направлении, но это было бы ошибкой. Стрелка описывает порождающий процесс, который развивается в направлении причинно-следственной связи. Я сказал, что направленные зависимости, как правило, моделируют причинно- следственные связи. На самом деле, связь причины со следствием - лишь один пример общего класса асимметричных связей между переменными. Рассмотрим различные виды асимметричных связей более пристально, начав с причинно- следственных. Разновидности причинно-следственных связей Существует несколько видов причинно-следственных связей. • Произошедшее раньше с произошедшим позже - самый очевидный тип причинно-следственной связи: сначала происходит одна вещь, затем другая. Например, сначала кто-то выключает питание, затем принтер перестает работать. Такая временная последовательность - настолько обычная характеристика причинно-следственной связи, что кажется, будто время - его неотъемлемое свойство. Однако я с этим не согласен. 154 Состояние Принтера Кнопка Питания Принтера Нажата
5.1. Моделирование зависимостей • Связь состояний - иногда две переменные описывают различные аспекты состояния объекта в заданный момент времени. Например, одна переменная может представлять состояние кнопки питания, а другая - работоспособность принтера. Оба состояния имеют место одновременно. В данном случае, ненажатая кнопка влечет за собой неработающий принтер. • Связь истинного значения с измерением - если одна переменная является результатом измерения значения другой переменной, то мы говорим, что истинное значение - причина измерения. Предположим, к примеру, что имеется переменная Индикатор Питания Горит, показывающая состояние лампочки питания принтера. Существует асимметричная связь между переменными Кнопка Питания Принтера Нажата и Индикатор Питания Горит. Обычно измерения производятся датчиками, и для одного и того же значения может быть несколько измерений. Кроме того, наблюдаются обычно именно результаты измерений, и мы хотим на основе измерений делать выводы об истинных значениях. Таким образом, налицо еще один пример, когда направление зависимости противоположно направлению рассуждения. • Связь между параметром и переменной, в которой он используется - рассмотрим, к примеру, степень асимметрии монеты, представляющую вероятность выпадения орла, и подбрасывание этой монеты. Для определения исхода подбрасывания используется асимметрия. Ясно, что сначала порождается асимметрия, а лишь потом подбрасывание монеты. Если подбрасываний несколько, то все они порождаются после асимметрии. Дополнительные асимметричные связи Рассмотренные выше случаи - самые важные и не вызывающие недоразумений. Если вы их поняли, то в 95 % случаев не затруднитесь с определением правильного направления зависимости. Но давайте копнем глубже и рассмотрим другие связи, которые, несмотря на очевидную асимметричность, вызывают сложности при определении направления зависимости. Я перечислю эти связи, а затем опишу эвристическое правило, помогающее разрешить неоднозначность. • Связь части с целым - нередко свойства части объекта определяют свойства всего объекта. Например, рассмотрим принтер с тонером и устройством подачи бумаги. Неполадки того и другого могут привести к неполадкам принтера в целом. А иногда, наоборот, свойства целого определяют свойства его части. Например, если принтер плохо изготовлен, то, вероятно, плохо изготовлены также устройство подачи бумаги и тонер. • Связь частного и общего - и эта связь может быть направлена в обе стороны. Пользователь может сталкиваться с самыми разными ошибками принтера, например, с замятием бумаги или с плохим качеством печати. Столкнувшись с любой частной проблемой, пользователь сталкивается и с более общей проблемой плохой работы принтера. В этом случае частное является причиной общего. С другой стороны, представьте себе процесс порождения 155
Глава 5. Моделирование зависимостей с помощью байесовских. объекта. Обычно, сначала порождаются общие свойства, а затем уточняются частности. Так, в случае принтера сначала нужно решить, является он лазерным или струйным, а затем уже порождать отдельные свойства. Действительно, не имеет смысла порождать частные свойства, не зная тип принтера - ведь свойство может быть применимо только к принтеру определенного типа. Связь конкретного/детального с абстрактным/суммарным - примером связи между конкретным и абстрактным может служить связь между экзаменационными баллами и общей оценкой. Различные комбинации баллов приводят к одной и той же оценке. Понятно, что преподаватель выставляет оценку, исходя из результатов экзаменов, поэтому баллы являются причиной оценки. С другой стороны, рассмотрим процесс порождения студента и результатов экзамена. Сначала порождается абстрактный тип студента (например, студент А или студент В), а только потом - конкретная оценка на экзамене. Устранение неоднозначности причинно-следственных связей Как видно из предыдущих примеров, даже в случае, когда асимметричность зависимости не вызывает сомнений, определить ее направление может быть нелегко. Иногда в таких случаях помогает следующее соображение. Представьте, что один человек произносит предложение, а другой слушает. Говорящий знает, что именно он хочет сказать и соответственно подбирает слова. Это связь между целым и частью. Слушающий уясняет смысл предложения по отдельным словам - это связь между частью и целым. Целое Целое Произнесение и восприятие предложения: говорящий вкладывает смысл в целое предложение, произнося слова (части). Из этих слов слушающий восстанавливает смысл всего предложения Внимательно проанализировав этот пример, мы увидим, что в процессе произнесения предложения сначала определяется его общий смысл, а потом подбираются слова. А в процессе восприятия сначала собираются слова, а затем на их основе устанавливается 156 Вкладываемый смысл Воспринимаемый смысл
5.1. Моделирование зависимостей общий смысл. Это правило не высечено в камне, но часто в жизни мы сначала делаем нечто общее/абстрактное/целое, а потом уточняем это с целью получить нечто частное/ конкретное/детальное, состоящее из нескольких частей. В случае принтера мы сначала порождаем общий класс принтера в целом. Затем порождаем конкретный тип принтера и детальную информацию о его компонентах. Точно так же в случае студента мы сначала порождаем абстрактный класс студента, а затем наполняем его конкретными результатами ответов на экзаменационные вопросы. С другой стороны, в процессе восприятия мы сначала воспринимаем частную/конкретную/детальную информацию, а затем путем обобщения выводим из нее общие/абстрактные представления о целом. Например, пользователь, столкнувшийся с конкретной неполадкой принтера, пытается обобщить ее, а преподаватель, оценивающий студента, сначала видит его экзаменационные баллы, а потом выводит общую оценку. Можно сформулировать такое эвристическое правило: При моделировании процесса создания или определения свойств зависимость направлена от общего, абстрактного или целого к частному, конкретному или части. При моделировании восприятия и перечислении свойств зависимость направлена от частного, конкретного или части к общему, абстрактному или целому. Как я уже сказал, понимая основные причинно-следственные связи, вы почти никогда не будете испытывать затруднений с выбором направления зависимости. Но в исключительных случаях даже опытные разработчики иногда расходятся во мнениях о направлении. Надеюсь, что приведенное эвристическое правило поможет вам при создании моделей. Направленные зависимости в Figaro Вы еще не забыли о четырех ингредиентах вероятностной модели? Повторим: переменные, зависимости, функциональные формы и числовые параметры. До сих пор мы предполагали, что переменные уже даны и акцентировали внимание на зависимостях. Желая выразить зависимость в Figaro, мы должны выбрать функциональную форму и задать числовые параметры. В Figaro направленные зависимости можно выразить разными способами. Общий принцип - использовать тот или иной элемент chain в качестве функциональной формы. Напомним, что chain принимает два аргумента: • родительский элемент; • цепную функцию, которая получает значение родительского элемента и возвращает результирующий элемент. ВЫЗОВ Chain (parentElement, chainFunction) определяет следующий порождающий Процесс: ПОЛуЧИТЬ Значение рОДИТеЛЯ ОТ parentElement, Применить chainFunction ДЛЯ Получения resultElement И, наконец, ПОЛуЧИТЬ Значение ОТ resultElement. Когда элемент chain применяется для выражения направленной зависимости, parentElement, естественно, является родителем. Цепная функция описывает распределение вероятности потомка для каждого значения родителя. В Figaro име- 157
Глава 5. Моделирование зависимостей с помощью байесовских... ется немало конструкций с использованием chain, так что во многих случаях вы будете работать с chain, даже не осознавая этого. Ниже приведено несколько эквивалентных примеров, в каждом из которых выражается одно и то же: если кнопка питания принтера не нажата, то принтер определенно не работает, но если кнопка питания нажата, то принтер может как работать, так и не работать. val printerPowerButtonOn = Flip(0.95) val printerState = <-0 Chain(printerPowerButtonOn, <]—© (on: Boolean) => <3—© H if (on) Select(0.2 -> 'down, 0.8 -> fup) else Constant('down) val printerState = 0—© If(printerPowerButtonOn, <J—© Select (0.2 -> fdown, 0.8 -> fup), <—© Constant(fdown)) <—© val printerState = O—© CPD (printerPowerButtonOn, O—© false -> Constant('down), <3—© true -> Select(0.2 -> 'down, 0.8 -> 'up)) <|—© v.; pi.:глог.чч". <МЕ) RichCPD(printerPowerButtonOn, <\—© OneOf(false) -> Constant (' down) , <—© -> Select (0.2 -> 'down, 0.8 -> 'up)) <—© • © — Использование Chain © — Родительский элемент © — Аргумент цепной функции © — Тело цепной функции © — Использование If © — Родительский элемент © — Использование CPD © — Исход, если родитель равен true © — Исход, если родитель равен false © — Родительский элемент ф — Ветвь, описывающая исход, когда родитель равен false © — Ветвь, описывающая исход, когда родитель равен true © — Использование RichCPD ф — Родительский элемент © — Ветвь, описывающая исход, когда значение родителя принадлежит множеству {false} © — Ветвь, описывающая исход, когда родитель принимает любое другое значение Как уже отмечалось, одним из видов асимметричной связи является связь между параметром и переменной, зависящей от этого параметра, например, между 158
5.1. Моделирование зависимостей асимметрией монеты и подбрасыванием этой монеты. Такую связь тоже можно выразить с помощью chain или составного варианта атомарного элемента. Вот два эквивалентных примера: val toss = Chain(bias, (d: Double) => Flip(d)) val toss = Flip(bias) 5.7.2. Ненаправленные зависимости Мы видели, что направленные зависимости представляют различные виды асимметричных связей. Что касается ненаправленных зависимостей, то они моделируют связи между переменными, не имеющие очевидного направления. Такие связи называются симметричными. Если имеются коррелированные переменные, но не видно, какой порождающий процесс должен был бы породить одну раньше другой, то подойдет ненаправленная зависимость. Симметричные связи возникают двумя способами. • Два следствия одной причины в случае, когда причина явно не присутствует в модели. Например, два измерения одной величины, когда для этой величины не заведена переменная, или два последствия одного события, которому не соответствует никакая переменная. Понятно, что если мы не знаем истинного значения величины или события, то два измерения или последствия связаны. Допустим, что в примере с принтером имеются две переменные, представляющие качество и скорость печати. Если бы не было переменной, представляющей состояние принтера, то эти две переменные были бы связаны, поскольку это два аспекта печати, у которых может быть общая причина. Может возникнуть вопрос, что мешает включить в модель переменную, представляющую эту причину. Один из возможных ответов состоит в том, что причина может быть гораздо сложнее следствий, так что ее точное моделирование проблематично. В этой главе мы будем рассматривать пример реконструкции изображения, являющегося двумерным следствием сложной трехмерной сцены. Создать корректную вероятностную модель трехмерной сцены труднее, чем смоделировать связи между пикселями изображения. • Две причины известного следствия. Это интересная ситуация. Обычно между причинами одного следствия нет никакой связи. Например, и состояние устройства подачи бумаги, и уровень тонера влияют на состояние принтера в целом, но эти два фактора независимы. Однако если мы знаем, что принтер работает плохо, то внезапно они оказываются зависимыми. Если мы знаем, что тонера осталось мало, то можем предположить, что качество печати плохое именно по этой причине, а не из-за препятствий на тракте движения бумаги. В этом примере состояние принтера в целом - следствие, а уровень тонера и состояние устройства подачи бумаги - возможные причи- 159
Глава 5. Моделирование зависимостей с помощью байесовских... ны, которые становятся зависимыми, если известно следствие. Это пример наведенной зависимости, о которой подробнее будет сказано в разделе 5.2.3. Если переменной для следствия в модели нет, то появляется симметричная связь между двумя причинами. Но вывод за пределы модели общего следствия двух причин - явление не слишком частое. Выражение ненаправленных зависимостей в Figaro Симметричные зависимости можно выразить в Figaro двумя способами: с помощью ограничений и условий. У каждого есть свои плюсы и минусы. Достоинством ограничений является концептуальная простота. Но числовые значения, входящие в состав ограничений, зашиты в код, и Figaro не может найти их методами машинного обучения, потому что они недоступны алгоритму обучения. С другой стороны у условий есть то достоинство, что их числовые параметры можно вывести с помощью обучения. Общий принцип обоих подходов один и тот же. В разделе 5.5 о кодировании ненаправленных зависимостей рассказано детально, здесь же дадим конспективное описание. Если между двумя переменными существует ненаправленная зависимость, то некоторые пары их значений являются более предпочтительными. Это можно обеспечить, назначив веса различным парам значений. В случае ограничения веса задаются с помощью функции, которая сопоставляет паре значений вещественное число. А в случае условия та же по существу информация кодируется более сложным способом. Давайте, например, закодируем связь между двумя соседними пикселями изображения. Это связь типа «при прочих равных условиях». Так, мы, возможно, имеем основания считать, что при прочих равных условиях два соседних пикселя в три раза вероятнее будут одноцветными, чем разноцветными. На самом деле, на цвета пикселей могут влиять различные факторы, поэтому их шансы оказаться одноцветными не равны в точности 3:1. Как с помощью ограничений выразить эту связь в Figaro? Обозначим цвета пикселей coiori и coior2. Чтобы не усложнять пример, будем считать цвета булевыми величинами. Напомним, что ограничение - это функция, отображающая значение элемента в Double. Мы должны создать элемент, который представляет пару color 1, color2 в виде, допускающем задание ограничения. Можно поступить так: import com.cra.figaro. library. compound. Л/ч val pair = ЛЛ(со1ог1, color2) <\—Q О — л/ч в Figaro обозначает конструктор пары Теперь нужно определить функцию, реализующую ограничение: def sameColorConstraint(pair: (Boolean, Boolean)) = if (pair._l == pair._2) 0.3; else 0.1 <—О О — Здесь проверяется, что первый компонент пары равен второму 160
5.1. Моделирование зависимостей Наконец, применяем ограничение к паре цветов: pair.setConstraint(sameColorConstraint _) <J—О О — Знак подчеркивания говорит, что мы хотим получить саму функцию, а не вызвать ее Figaro интерпретирует ограничение именно так, как мы и ожидаем. При прочих равных условиях состояние, в котором оба компонента пары равны (т. е. цвета одинаковы), в три раза вероятнее, чем противоположное. Эти ограничения корректно учитываются при определении распределения вероятности, как будет показано в разделе 5.5. Но, к сожалению, числа 0.3 и 0.1 зашиты в код функции ограничения, так что получить их из обучающих данных не получится. Подход с применением условий я сначала продемонстрирую на примере кода, а затем объясню, почему он работает правильно. Первым делом определим вспомогательный булев элемент samecoiorconstraintvaiue, так чтобы он принимал значение true с вероятностью, заданной в постановке задачи. Затем добавим условие, которое говорит, что этот элемент должен быть равен true. Это можно сделать с помощью наблюдения: val sameColorConstraintValue = Chain(colorl, color2, (Ы: Boolean, Ь2: Boolean) => if (bl == Ь2) Flip(0.3); else Flip(O.l)) sameColorConstraintValue.observe(true) Этот код эквивалентен коду с ограничением. Действительно, условие означает, что любое нарушающее его значение имеет вероятность 0, а удовлетворяющее ему - вероятность 1. Полная вероятность любого состояния вычисляется путем комбинирования вероятностей, следующих из определения элементов и условий или ограничений. Например, допустим, что два цвета совпадают. Из определения Chain Следует, ЧТО sameColorConstraintValue будет Иметь Значение true С ВерОЯТ- НОСТЬЮ 0.3, a false - С верОЯТНОСТЬЮ 0.7. ЕСЛИ sameColorConstraintValue ОКажет- ся равным true, то условие будет иметь вероятность 1, а если false - то вероятность 0. Следовательно, комбинированная вероятность условия равна (0.3 х 1) + (0.7 х 0) = 0.3. Аналогично, если цвета различаются, то комбинированная вероятность равна (0.1 х 1) + (0.9 х 0) = 0.1. Мы видим, что при прочих равных условиях вероятность одинаковых цветов в три раза больше, чем различных. Это именно тот результат, который мы получили с применением ограничений. Конструкция на основе условий достаточно общая, она применима к любым асимметричным связям. Достоинство этого подхода в том, что числа 0.3 и 0.1 встречаются внутри элементов Flip, поэтому их можно было бы сделать зависимыми от других аспектов модели. Допустим, к примеру, что мы хотим с помощью обучающих данных узнать, насколько одинаковый цвет соседних пикселей вероятнее различного. Можно создать элемент, скажем имеющий бета-распределение, который будет представлять вес, а затем использовать этот элемент внутри Flip вместо 0.3. Тогда, воспользовавшись обучающими данными, мы сможем узнать распределение вероятности веса. 161
Глава 5. Моделирование зависимостей с помощью байесовских... 5.7.3. Прямые и косвенные зависимости Прежде чем переходить к байесовским и марковским сетям, я хочу подчеркнуть один важный момент. В типичной вероятностной модели много пар переменных таких, что знание одной изменяет наши предположения о другой. По определению, все это примеры зависимостей между переменными. Но большинство этих зависимостей косвенные: они связывают две переменные не напрямую, а опосредованно, через промежуточные переменные. Точнее, наши знания о первой переменной изменяют предположения о второй, потому что они изменяют предположения о некоей промежуточной переменной, а это, в свою очередь, изменяет предположения о второй переменной. Например, на рис. 5.2 показаны три переменные: Кнопка Питания Принтера Нажата, Состояние Принтера и Число Напечатанных Страниц. Очевидно, что между переменными Кнопка Питания Принтера Нажата и Число Напечатанных Страниц существует зависимость. Если мы знаем, что кнопка питания не нажата, то полагаем, что число напечатанных страниц равно нулю. Но на рисунке зависимость между этими переменными проходит через промежуточную переменную Состояние Принтера. Это означает, что причина, по которой знание о переменной Кнопка Питания Принтера Нажата изменяет предположения о Числе Напечатанных Страниц, заключается в том, что сначала изменяются предположения о Состоянии Принтера, а уже эти измененные предположения меняют наши предположения о числе напечатанных страниц. Зная, что кнопка питания не нажата, мы понимаем, что принтер не работает, а, значит, не напечатано ни одной страницы. Кнопка Питания Принтера Нажата Состояние Принтера Число Напечатанных Страниц Рис. 5.2. Переменная Кнопка Питания Принтера Нажата косвенно связана с Числом Напечатанных Страниц, эта связь опосредована промежуточной переменной Состояние Принтера Рассмотрим еще один пример косвенной зависимости. В примере с соседними пикселями изображения зависимость была прямой. А если пиксели не соседние? Взгляните на рис. 5.3. Если мы знаем, что пиксель 11 красный, то есть основания полагать, что пиксель 12 тоже красный, а отсюда следует, что и пиксель 13 с большой вероятностью красный. Это очевидный пример косвенной зависимости, поскольку знания о пикселе 11 влияют на предположения о пикселе 13 через промежуточный пиксель 12. 162
5.2. Байесовские сети Важно понимать, какие зависимости в предметной области прямые, а какие косвенные. И в байесовских, и в марковских сетях мы строим граф, в котором ребра представляют зависимости между переменными. В байесовской сети ребра направленные, в марковской ненаправленные. Ребра проводятся только для прямых зависимостей. Если две переменные связаны лишь косвенной зависимостью, то ребра между ними не будет. Рис- 5-3. Между пикселями 11 и 13 имеется косвенная связь, опосредованная промежуточным пикселем 12 Далее мы рассмотрим байесовские сети, моделирующие направленные зависимости, а затем марковские сети, моделирующие ненаправленные зависимости. Однако хочу подчеркнуть - из того, что для моделирования зависимостей разных видов используются разные системы, не следует, что нужно выбирать для модели только ту или другую. Существуют способы, позволяющие комбинировать направленные и ненаправленные зависимости в одной модели. В вероятностной программе это сделать легко: для представления направленных зависимостей используем порождающие определения элементов, а для ненаправленных - добавляем ограничения. 5.2. Байесовские сети Мы видели, что представление связей между переменными - одна из основ вероятностного моделирования. В этом разделе мы изучим байесовские сети - стандартную систему представления асимметричных связей с применением направленных зависимостей. Мы уже встречались с байесовскими сетями в главе 4, когда рассматривали пример экспертизы картины Рембрандта. А сейчас дадим полное определение и объясним, какие существуют способы рассуждений. 5.2.7. Определение байесовской сети Байесовской сетью называется представление вероятностной модели, состоящее из трех компонентов: • множество переменных, каждая со своей областью значений; • ориентированный ациклический граф, вершинами которого являются эти переменные; • условное распределение вероятности каждой переменной при условии ее родителей. Множество переменных со своими областями значений На рис. 5.4 показаны три переменные: Тема, Размер и Яркость. Область значений переменной описывает, какие значения она может принимать. Областью значений переменной Тема является множество {Люди, Ландшафт}, переменной Раз- Пиксель 11 Пиксель 12 Пиксель 13 163
Глава 5. Моделирование зависимостей с помощью байесовских. мер - множество {Малая, Средняя, Большая}, а переменной Яркость - множество {Темная, Яркая}. Значения вероятности Темы Тема Люди 0,8 Ландшафт 0,2 Значения вероятности Размера при условии Темы Значения вероятности Яркости при условии Темы Тема Размер Малая Средняя Большая Люди 0.25 0.25 0,5 Ландшафт 0.25 0.5 0.25 Тема Люди Ландшафт Яркость Темная | Яркая | 0.8 0.2 0.3 0.7 Рис. 5.4. Байесовская сеть с тремя узлами Ориентированный ациклический граф Слово ориентированный означает, что у каждого ребра графа имеется ориентация, или направление; ребро направлено от одной переменной к другой. Начальная переменная называется родителем, конечная - потомком. На рис. 5.4 Тема является родителем Размера и Яркости. Слово ациклический означает, что в графе нет ориентированных циклов, т. е. нельзя, отправившись из одной вершины и следуя в направлении стрелок, вернуться в ту же самую вершину. Однако неориентированные циклы, в которых направление стрелки игнорируется, допускаются. Эта концепция изображена на рис. 5.5. В графе слева имеется ориентированный цикл A-B-D-C-A. А в графе справа в цикле A-B-D-C-A встречаются ребра с разными ориентациями, т. е. этот цикл неориентированный. Ориентированный цикл Ориентированного цикла нет Рис. 5.5. Обход ориентированного цикла производится по стрелкам и заканчивается в исходной вершине 164 Тема Размер Яркость к А С D В С D В
5.2. Байесовские сети Условное распределение вероятности переменной Речь идет об условном распределении переменной-потомка при условии известных значений ее родителей. Учитываются все возможные комбинации допустимых значений родителей. Для каждой комбинации определяется распределение вероятности потомка. На рис. 5.6 у каждой переменной имеется условное распределение вероятности. Переменная Тема - корневая вершина сети, у нее нет родителей. Если у переменной нет родителей, то условное распределение описывает единственное распределение вероятности этой переменной. В нашем примере переменная Тема принимает значение Люди с вероятностью 0.8 и значение Ландшафт с вероятностью 0.2. У переменной Размер есть родитель - Тема, поэтому в ее условном распределении имеется по одной строке для каждого значения Темы. Мы видим, что если Тема принимает значение Люди, то в распределении Размера значение Малая имеет вероятность 0.25, Средняя - 0.25 и Большая - 0.5. Если же Тема принимает значение Ландшафт, то у Размера другое распределение. Наконец, родителем Яркости тоже является Тема, и в ее условном распределении тоже имеется по одной строке для каждого значения Темы. Р(Тема + Люди) Тема Люди 0.8 Ландшафт 0.2 Малая Люди Яркая Р(Размер = Малая | Тема = Люди) Тема Люди Ландшафт Малая 0,25 0.25 Размер Средняя Большая 0.25 0.5 0.5 0.25 Р(Яркость = Яркая Тема = Люди) Тема Яркость Темная 1 Яркая 1 Люди 0.8 0.2 Ландшафт Q.3 0.7 Р((Тема = Люди, Размер = Малая, Яркость = Яркая) = 0.8 х 0.25 х 0.2 = 0.04 Рис. 5.6. Вычисление вероятности возможного мира путем перемножения элементов условных распределений вероятности 165
Глава 5. Моделирование зависимостей с помощью байесовских... 5.2.2. Как байесовская сеть определяет распределение вероятности Байесовскую сеть мы определили. Теперь посмотрим, как она определяет распределение вероятности. Прежде всего, необходимо определить возможные миры. Для байесовской сети возможный мир - это комбинация значений всех переменных, причем значение каждой должно принадлежать ее области значений. Например, <Тема = Люди, Размер = Малая, Яркость = Яркая> - возможный мир. Затем мы определяем вероятность возможного мира. Это просто. Нужно лишь найти в условном распределении вероятности каждой переменной запись, соответствующую значениям родителей и потомка в данном возможном мире. Этот процесс иллюстрируется на рис. 5.6. Так, для мира <Тема = Люди, Размер = Малая, Яркость = Яркая> в записи, соответствующей Теме, находится 0.8, это значение взято из столбца Люди. Для Размера мы ищем строку, соответствующую значению Тема = Люди, и в столбце, соответствующем значению Размер = Малая, находим число 0.25. Наконец, для Яркости мы снова ищем строку, соответствующую значению Тема = Люди, и берем число 0.2 из столбца Яркая. Вероятность мира получается путем перемножения найденных чисел: 0.8 х 0.25 х 0.2 = 0.04. Если повторить эту процедуру для каждого возможного мира, то сумма вероятностей окажется равна 1, как и должно быть. Это справедливо для любой байесовской сети. Итак, мы рассмотрели, как байесовская сеть определяет допустимое распределение вероятности. Поняв, как строится байесовская сеть и что она означает, посмотрим, как ее можно использовать для высказывания предположений об одних переменных, если что-то известно о других. 5.2.3. Рассуждения с применением байесовской сети В байесовской сети закодирован большой объем информации о независимости переменных. Напомним, что две переменные называются независимыми, если, зная что-то об одной из них, мы ничего не можем сказать о другой. Из предыдущего примера понятно, что переменные Число Напечатанных Страниц и Кнопка Питания Принтера Нажата независимыми не являются. Знание о том, что не напечатано ни одной страницы, уменьшает вероятность того, что кнопка питания нажата. С условной независимостью дело обстоит похоже. Две переменные называются условно независимыми, если после получения информации о некоей третьей переменной знание чего-то об одной из них не позволяет сказать ничего нового о другой. Так называемый критерий d-разделенности позволяет определить, когда две переменные в байесовской сети являются условно независимыми относительно набора переменных. Критерий звучит довольно сложно, поэтому я не буду приводить его точную формулировку, а опишу основные принципы и приведу несколько примеров. Основная идея заключается в том, что рассуждение протекает вдоль пути от одной переменной к другой. В примере на рис. 5.4 рассуждение может протекать от Размера к Яркости вдоль пути Размер-Тема-Яркость. Отблеск этой идеи мы виде- 166
5.2. Байесовские сети ли в разделе 5.1.3, когда говорили о прямых и косвенных зависимостях. В случае косвенной зависимости рассуждения протекает от одной переменной к другой, проходя через промежуточные переменные. В данном случае Тема - промежуточная переменная на пути от Размера к Яркости. В байесовской сети рассуждение может протекать вдоль пути при условии, что он не блокирован в некоторой переменной. В большинстве случаев путь блокирован в переменной, если эта переменная наблюдается. Таким образом, если Тема наблюдается, то путь Размер-Тема-Яркость блокирован. Это означает, что, наблюдая Размер, мы не узнаем ничего нового о Яркости, если Тема тоже наблюдается. По-другому можно сказать, что Размер условно независим от Яркости при условии Темы. В нашей модели выбор темы художником определяет размер и яркость, но после выбора темы размер и яркость порождаются независимо. Сходящиеся стрелки и наведенные зависимости Есть еще один случай, который поначалу может показаться противоречащим интуиции: путь блокирован в переменной, если переменная не наблюдается, но разблокируется, когда переменную наблюдают. Я проиллюстрирую эту ситуацию, дополнив пример, как показано на рис. 5.7. Здесь появилась новая переменная Материал: масло, акварель или еще что-то. Естественно, Материал обусловливает Яркость (допустим, масляные картины ярче акварелей), поэтому в сети имеется ориентированное ребро от Материала к Яркости. В данном случае у одного потомка два родителя. Эта ситуация называется сходящимися стрелками, потому что ребра, исходящие из Темы и Материала, сходятся в Яркости. Рис. 5.7. Дополненный пример с живописью, включающий сходящиеся стрелки от двух родителей к общему потомку Теперь обратимся к вопросу о рассуждении, касающемся Темы и Материала. Согласно модели, Тема и Материал порождаются независимо. Поэтому справедливо следующее утверждение: (1) Тема и Материал независимы, когда ничего не наблюдается. Ну а если мы наблюдаем, что картина яркая? Согласно нашей модели, ландшафты обычно ярче, чем изображения людей. Зная, что картина яркая, мы заключаем, что на ней с большей вероятностью изображен ландшафт, а не люди. Предположим, что затем мы узнали, что картина написана маслом, а это также тяготеет к большей яркости. Это наблюдение дает другое объяснение тому, почему картина яркая. Следовательно, вероятность ландшафта несколько уменьшается по срав- 167 Размер Тема Сходящиеся стрелки Материал Яркость
Глава 5. Моделирование зависимостей с помощью байесовских. нению с тем, что было после наблюдения яркости, но до наблюдения масляной краски. Как видим, рассуждение протекает от материала к Теме вдоль пути Материал-Яркость-Тема. Поэтому можно высказать такое утверждение: (2) Тема и Материал не являются условно независимыми при условии Яркости. Встретившийся здесь паттерн противоположен обычному. Мы имеем путь, который блокирован, когда промежуточная переменная не наблюдается, и разблокируется, когда эта переменная наблюдается. Такая ситуация называется наведенной зависимостью - зависимость между двумя переменными появляется в результате наблюдения третьей. Любые сходящиеся стрелки в байесовской сети могут приводить к наведенной зависимости. Путь между двумя переменными может включать как обычные ситуации, так и сходящиеся стрелки. Рассуждение может протекать вдоль пути, только если оно не блокируется ни в какой вершине, лежащей на этом пути. На рис. 5.8 показаны четыре примера для пути Размер-Тема-Яркость-Материал. В левом верхнем примере ни Тема, ни Яркость не наблюдаются, а путь блокирован в Яркости, потому что в нем имеются сходящиеся стрелки. В следующем примере (справа) Тема наблюдается, поэтому путь блокирован и в Теме, и в Яркости. В следующем примере (слева) Тема не наблюдается, но наблюдается Яркость, это в точности то условие, которое необходимо, чтобы путь не блокировался ни в Теме, ни в Яркости. Наконец, в последнем примере наблюдаются Тема и Яркость, поэтому путь блокирован. Размер Блокирован в Теме и Яркости Яркость Рис. 5.8. Примеры блокированных и неблокированных путей, в которых обычный паттерн сочетается со сходящимися стрелками. На всех рисунках показан путь от Размера к Материалу, когда переменные Тема и Яркость наблюдаются и не наблюдаются 168 Размер Тема Материал Тема Блокирован в Яркости Материал Яркость Тема Не блокирован Размер Яркость Тема Размер Блокирован в Теме Материал Яркость
5.3. Изучение примера байесовской сети 5.3. Изучение примера байесовской сети Рассмотрев основные концепции байесовских сетей, исследуем пример диагностики неисправности принтера. Сначала я покажу, как проектируется сеть, а затем продемонстрирую все способы рассуждения в этой сети. Вопрос обучения параметров сети отложим до главы 12, где будет приведен полезный паттерн проектирования, относящийся к этой теме. 5.3.7. Проектирование модели диагностики компьютерной системы Представьте, что вы проектируете приложение для отдела технической поддержки. Ваша задача - помочь сотруднику как можно быстрее идентифицировать причины неисправности. В этом приложении можно использовать систему вероятностных рассуждений: зная факты - сообщение пользователя и результаты диагностических тестов, - определить внутреннее состояние системы. Для представления вероятностной модели было бы естественно воспользоваться байесовской сетью. В проектировании байесовской сети обычно выделяются три этапа: выбор переменных и их областей значений, задание структуры сети и представление условных распределений вероятности. Мы увидим, как это делается в Figaro. На практике эти этапы обычно не выполняются линейно: сначала выбрать все переменные, потом построить структуру всей сети и напоследок выписать условные распределения. Как правило, сеть строится по частям, и сделанное ранее уточняется впоследствии. Так мы и поступим. Сначала построим сеть для модели общей неисправности принтера, а затем будем уточнять детали. Модель общей неисправности принтера: переменные Мы хотим смоделировать возможные сообщения пользователя, потенциальные неисправности и факторы внутри системы, которые могли бы стать причиной неисправностей. Для всего этого нам понадобятся переменные. Начнем с Общего Результата Печати. Эта переменная представляет общее впечатление пользователя от результатов печати на верхнем уровне абстракции. Когда пользователь впервые обращается в отдел поддержки, он сообщает лишь такую общую информацию. Возможны три варианта: (1) к печати нет претензий (вы ставите метку «отлично»); (2) печатается, но не так, как надо (неудовлетворительно); (3) вообще ничего не печатается (ничего). Затем рассматриваются конкретные аспекты результата печати, а именно: Число Напечатанных Страниц - ни одной, несколько страниц, все страницы; Печатает Быстро - булева переменная, показывающая, завершилась ли печать за разумное время; Хорошее Качество Печати - еще одна булева переменная. Эти аспекты моделируются по отдельности, потому что помогут дифференцировать различные неисправности. Например, если не напечатано ни одной страницы, то, возможно, 169
Глава 5. Моделирование зависимостей с помощью байесовских... не работает сеть. Если же несколько страниц (но не все) все же напечатано, то проблема, скорее всего, не в сети, а в ошибке пользователя. Рассматриваем все элементы системы, которые могут повлиять на результат печати, а именно: Состояние Принтера - хорошее, плохое, не работает; Состояние Программы - корректное, глючит, крах; Состояние Сети - работает, нестабильна, не работает; Команда Пользователя Правильна - булева переменная. Модель общей неисправности принтера: структура сети Отобранные нами переменные можно скомпоновать в три группы: абстрактный Общий Результат Печати, различные конкретные аспекты результата печати и состояния системы, влияющие на результат печати. Соответственно имеет смысл заложить в проект сети три уровня. Но каким должен быть порядок уровней? Между переменными состояния системы и переменными, описывающими конкретный результат печати, существуют причинно-следственные связи. Например, Состояние Сети «не работает» - причина того, что переменная Печатает Быстро равна false. Кроме того, между индивидуальными переменными, описывающими результат печати, и Общим Результатом Печати существует связь типа конкретное-абстрактное. В разделе 5.1.1 я говорил, что такие связи могут быть направлены в любую сторону. В нашем приложении моделируется восприятие системы пользователем и его сообщение о результатах печати, поэтому, согласно введенному там же эвристическому правилу, правильным будет направление от конкретных результатов печати к абстрактному общему результату. Поэтому порядок уровней в нашей сети таков: (1) переменные состояния системы, (2) переменные, описывающие конкретный результат печати, (3) Общий Результат Печати. Структура сети показана на рис. 5.9. Мы видим три уровня, но не всегда существует ребро от переменной на одном уровне ко всем переменным на следующем уровня. Дело в том, что некоторые переменные, описывающие результат печати, зависят от состояния лишь некоторых компонентов системы. Например, качество печати зависит от состояния принтера, но не зависит от сети. Аналогично скорость печати, согласно нашей модели, зависит только от сети и программного обеспечения. О том, справедливы эти утверждения или нет, можно спорить; я хотел показать, что в любом приложении можно привести аргументы в пользу удаления некоторых ребер. А чем меньше ребер, тем короче описание условного распределения вероятности. Модель общей неисправности принтера: условные распределения вероятности Я продемонстрирую проектирование условного распределения вероятности в виде кода на Figaro, объясняя наиболее интересные моменты. В разделе 5.1.1 были показаны различные способы определения условных распределений на Figaro. Здесь мы встретим многие из них. 170
5.3. Изучение примера байесовской сети Ком ан да П о л ьзо вате л я Правильна Число Напечатанных Страниц Рис. 5.9- Структура сети той части модели диагностики компьютерной системы, которая относится к общим неисправностям принтера Листинг 5-1. Реализация модели общей неисправности принтера val printerState = ... О—О val softwareState = Select(0.8 -> 'correct, 0.15 -> 'glitchy, 0.05 -> 'crashed) val networkState = Select (0.7 -> 'up, 0.2 -> 'intermittent, 0.1 -> 'down) val userCommandCorrect = Flip(0.65) val numPrintedPages = RichCPD(userCommandCorrect, networkState, softwareState, printerState, (*, *, *, OneOf('out)) -> Constant('zero), (*, *, OneOf('crashed), *) (*, OneOf('down), * (OneOf(false), *, * Select(0.3 -> 'zero, 0.6 (OneOf(true), *, *, *) -> Select(0.01 -> 'zero, 0.01 -> 'some, 1 -> Constant('zero), *) -> Constant('zero), *) -> -> 'some, 0.1 -> 'all), 0.98 -> 'all)) 1 val printsQuickly = Chain(networkState, softwareState, (network: Symbol, software: Symbol) => if (network == 'down || software == 'crashed) Constant(false) else if (network == 'intermittent I I software == 'glitchy) Flip(0.5) else Flip(0.9)) И val goodPrintQuality = CPD(printerState, 'good -> Flip(0.95) , 'poor -> Flip(0.3), 'out -> Constant(false)) I 171 Состояние Сети Печатает Быстро Состояние Программы Состояние Принтера Хорошее Качество Печати Общий Результат Печати
Глава 5. Моделирование зависимостей с помощью байесовских. val printResultSummary = I <^1Л Apply(numPrintedPages, printsQuickly, goodPrintQuality, (pages: Symbol, quickly: Boolean, quality: Boolean) => if (pages == 'zero) 'none else if (pages == 'some || Iquickly || Iquality) 'poor else 'excellent) О — Берется из детальной модели принтера, которая будет показана ниже © — Для корневых переменных имеем атомарные условные распределения вероятности, например Select или Flip © — для переменной numPrintedPages используется элемент RichCPD, представляющий зависимость от команды пользователя и от состояний сети, программы и принтера © — для printsQuickly используется элемент Chain, представляющий зависимость от состояний сети и программы © — для goodPrintQuality используется элемент simpleCPD, представляющий зависимость от состояния принтера © — используется элемент Apply, т. к. переменная printResultSummary однозначно определена своими родителями Здесь используется несколько приемов представления вероятностной зависимости потомка от родителей. • Для переменной numPrintedPages используется элемент RichCPD, реализую- щий следующую логику: если принтер не работает или сеть не работает или программа завершилась аварийно, то не будет напечатано ни одной страницы вне зависимости от состояния других родителей. В противном случае, если пользователь дал неправильную команду, то вряд ли будут напечатаны все страницы, а если команда правильна, то с высокой вероятностью будут напечатаны все страницы. • Для переменной printsQuickly используется элемент chain, реализующий следующую логику: есть сеть не работает или программа завершилась аварийно, то принтер безусловно не будет печатать быстро. В противном случае, если сеть работает нестабильно или программа «глючит», то будет принтер печатать быстро или нет - вопрос везения. Если сеть и программа работают нормально, то обычно принтер печатает быстро (хотя гарантии нет). • ДЛЯ Переменной goodPrintQuality ИСПОЛЬЗуеТСЯ ПрОСТОЙ Элемент CPD. Если принтер не работает, то ожидать хорошего качества печати точно не приходится. Но даже если принтер в хорошем состоянии, высокое качество печати не гарантируется (ну так уж они устроены, эти принтеры). • Переменная printsummary детерминированная, т. е. однозначно определена своими родителями, без какой-либо неопределенности. Для детерминированной переменной можно применить элемент Apply вместо chain. Детальная модель принтера В этом разделе мы бегло рассмотрим детальную модель принтера, потому что многие принципы те же самые. Структура сети показана на рис. 5.10. На состояние 172
5.3. Изучение примера байесовской сети принтера влияют три фактора: Подача Бумаги, Уровень Тонера и Кнопка Питания Принтера Нажата. В модели имеется также переменная еще не встречавшегося нам вида: индикатор, или измерение. Переменная Индикатор Замятия Бумаги Горит - это результат измерения Подачи Бумаги, а переменная Индикатор Низкого Уровня Тонера Горит - результат измерения уровня тонера. В разделе 5.1.1 отмечалось, что между истинным значением и результатом его измерения имеется причинно-следственная связь, поэтому от Подачи Бумаги к Индикатор Замятия Бумаги Горит и от Уровня Тонера к Индикатор Низкого Уровня Тонера Горит проведены ребра. Индикатор Замятия Бумаги Горит Рис. 5.10. Структура сети для детальной модели принтера Ниже приведен код определения условных распределений вероятности, по большей части прямолинейный. Листинг 5.2. Детальная модель принтера на Figaro val printerPowerButtonOn = Flip(0.95) val tonerLevel = Select (0.7 -> 'high, val tonerLowIndicatorOn = If(printerPowerButtonOn, CPD(paperFlow, 'high -> Flip(0.2) , 'low -> Flip(0.6) , 'out -> Flip(0.99)) , Constant(false) ) val paperFlow = Select(0.6 -> 'smooth, 0.2 -> 'low, 0.1 -> 'out) 0.2 -> 'uneven, 0.2 -> 'jammed) val paperJamlndicatorOn = If(printerPowerButtonOn, CPD(tonerLevel, 'high -> Flip(O.l), 'low -> Flip(0.3), 'out -> Flip(0.99)), Constant(false)) val printerState = Apply(printerPowerButtonOn, tonerLevel, paperFlow, (power: Boolean, toner: Symbol, paper: Symbol) => В Ё 173 Подача Бумаги Уровень Тонера Кнопка Питания Принтера Нажата Состояние Принтера 1
Глава 5. Моделирование зависимостей с помощью байесовских. if (power) { if (toner == 'high && paper == 'smooth) 'good else if (toner == 'out || paper == 'out) 'out else 'poor } else 'out >: § О — Напомним, что одиночная кавычка в Scala обозначает тип Symbol © — Элемент CPD вложен в If. Если кнопка питания принтера нажата, то состояние индикатора низкого уровня тонера зависит от уровня тонера. Если кнопка питания принтера не нажата, то индикатор низкого уровня тонера заведомо не горит, потому что нет напряжения © — Состояние принтера — детерминированный результат определяющих его факторов На рис. 5.11 показана итоговая структура полной байесовской сети. Полный исходный текст программы имеется в каталоге chap05/PrinterProblem.scala прилагаемого к книге кода. Рис. 5.11. Полная сеть для модели диагностики компьютерной системы 5.3.2. Рассуждения с помощью модели диагностики компьютерной системы В этом разделе показано, как рассуждать с помощью только что построенной модели диагностики компьютерной системы. В Figaro механизм рассуждения прост, но возможные паттерны рассуждений весьма интересны и служат иллюстрацией к концепциям, изложенным в разделе 5.2.3. Все рассматриваемые в этом разделе 174 Подача Бумаги Уровень Тонера Кнопка Питания Принтера Индикатор Замятия Бумаги Горит Индикатор Низкого Уровня Тонера Горит Команда Пользователя Правильна Состояние Сети Состояние Принтера Состояние Программы Число Напечатанных Страниц Печатает Быстро Хорошее Качество Печати Общий Результат Печати
5.3. Изучение примера байесовской сети паттерны рассуждений разбиты на отдельные шаги в прилагаемом к главе коде. Можете закомментировать шаги, которые не хотите выполнять. Запрос априорной вероятности Прежде всего, запросим вероятность того, что кнопка питания принтера нажата, в отсутствие каких-либо фактов, т. е. априорную вероятность. Для запроса можно выполнить такой код: val answerWithNoEvidence = Л_{> VariableElimination.probability(printerPowerButtonOn, true) printIn("Априорная вероятность, что кнопка питания принтера нажата = " + answerWithNoEvidence) О — Вычислить Р(Кнопка Питания Принтера Нажата = true) В результате печатается: Априорная вероятность, что кнопка питания принтера нажата = 0.95 Вернувшись к модели, мы увидим, что переменная printerPowerButtonOn определена в такой строке: val printerPowerButtonOn = Flip(0.95) Таким образом, ответ на наш запрос в точности такой, как если бы мы проигнорировали всю модель, кроме этого определения. Это проявление общего правила: все, что находится в сети после определения переменной, имеет значение, лишь если имеются факты. Для априорных вероятностей никаких фактов не нужно, поэтому то, что расположено в сети ниже, нас не интересует. Запрос с фактами А что произойдет, если добавить факты? Спросим у модели, какова вероятность, что кнопка питания нажата при условии, что результат печати неудовлетворительный, т. е. какой-то результат имеется, но не тот, что хотел бы видеть пользователь. Напишем такой код: printResultSummary.observe ('poor) О—D> val answerlfPrintResultPoor = VariableElimination.probability(printerPowerButtonOn, true) println("Вероятность, что кнопка питания принтера нажата, при условии, " + "что результат неудовлетворительный = " + answerlfPrintResultPoor) О — Вычислить Р(Кнопка Питания Принтера Нажата = true | Результат Печати = неудовлетворительный) В результате печатается: Вероятность, что кнопка питания принтера нажатия, при условии, что результат неудовлетворительный = 1.0 175
Глава 5. Моделирование зависимостей с помощью байесовских... Однако же это странно! Вероятность выше той, что была в отсутствии фактов, касающихся результата печати. Но если обратиться к модели, то становится понятно, почему Результат печати может оказаться неудовлетворительным, только если хотя бы одна страница напечатана, а это невозможно, если нет питания. Поэтому питание включено с вероятностью 1. Теперь спросим то же самое, указав в качестве факта, что ничего не напечатано. printResultSummary.observe ('none) Л_Г> val answerlfPrintResultNone = VariableElimination.probability(printerPowerButtonOn, true) println("Вероятность, что кнопка питания принтера нажата, при условии, " + "ничего не напечатано = " + answerlfPrintResultNone) О — Вычислить Р(Кнопка Питания Принтера Нажата = true | Результат Печати = ничего) В результате печатается: Вероятность, что кнопка питания принтера нажатия, при условии, ничего не напечатано =0.8573402523786461 Что и следовало ожидать. Не нажатая кнопка питания объясняет отсутствие напечатанных страниц, поэтому задание такого факта повышает вероятность. Независимость и блокировка В разделе 5.2.3 было введено понятие блокированного пути и описана его связь с условной независимостью. Эту концепцию иллюстрируют три переменные: Общий Результат Печати, Кнопка Питания Принтера Нажата и Состояние Принтера. На рис. 5.11 видно, что путь от переменной Кнопка Питания Принтера Нажата к Общему Результату Печати проходит через Состояние Принтера. Здесь нет сходящихся стрелок, поэтому путь блокирован, если Состояние Принтера наблюдается. И так оно и есть, как мы сейчас увидим: printResultSummary.unobserve() A printerState.observe(f out) " ^ val answerlfPrinterStateOut = VariableElimination.probability(printerPowerButtonOn, true) println("Вероятность, что кнопка питания принтера нажата, при условии, " + "что принтер не работает = " + answerlfPrinterStateOut) printResultSummary.observe('none) val answerlfPrinterStateOutAndResultNone = d~4> VariableElimination.probability(printerPowerButtonOn, true) println("Вероятность, что кнопка питания принтера нажата, при условии, " + "что принтер не работает и ничего не напечатано = " + answerIfPrinterStateOutAndResultNone) О — Вычислить Р(Кнопка Питания Принтера Нажата = true | Состояние Принтера = не работает) © — Вычислить Р(Кнопка Питания Принтера Нажата = true | Состояние Принтера = не работает, Общий Результат Печати = ничего) 176
5.3. Изучение примера байесовской сети В результате печатается: Вероятность, что кнопка питания принтера нажата, при условии, что принтер не работает = 0.6551724137931032 Вероятность, что кнопка питания принтера нажата, при условии, что принтер не работает и ничего не напечатано = 0.6551724137931033 Как видим, знание того, что ничего не напечатано, не изменяет вероятность того, что кнопка питания принтера нажата, если мы уже знаем, что принтер не работает. Объясняется это тем, что Общий Результат Печати условно независим от переменной Кнопка Питания Принтера Нажата при заданном Состоянии Принтера. Рассуждения относительно различных следствий одной причины До сих пор все пути рассуждения вели вверх по сети. Но можно комбинировать в одном рассуждении разные направления. Это происходит, например, когда мы рассматриваем, что говорит результат измерения об измеряемой величине, и какие выводы из этого можно сделать. В нашем примере переменная Индикатор Низкого Уровня Тонера Горит является потомком переменной Уровень Тонера, а Уровень Тонера - родитель Состояния Принтера. Если уровень тонера низкий, то вероятность хорошего качества печати будет ниже. В то же время горящий индикатор низкого уровня тонера свидетельствует о том, что уровень тонера низкий. Поэтому вполне логично, что наблюдение горящего индикатора низкого уровня тонера понижает вероятность хорошего качества печати. Убедиться в этом поможет следующий код: <з-о <^© printResultSummary.unobserve() printerState.unobserve() val printerStateGoodPrior = VariableElimination.probability(printerState, 'good) printIn("Априорная вероятность, что состояние принтера хорошее = " + printerStateGoodPrior) tonerLowIndicatorOn.observe(true) val printerStateGoodGivenTonerLowIndicatorOn = VariableElimination.probability(printerState, 'good) println("Вероятность, что состояние принтера хорошее, при условии + "горящего индикатора низкого уровня тонера = " + printerStateGoodGivenTonerLowIndicatorOn) О — Вычислить Р(Состояние Принтера = хорошее) © — Вычислить Р(Состояние Принтера = хорошее | Индикатор Низкого Уровня Тонера Горит = true) В результате печатается: Априорная вероятность, что состояние принтера хорошее = 0.39899999999999997 Вероятность, что состояние принтера хорошее, при условии горящего индикатора низкого уровня тонера = 0.23398328690807796 177
Глава 5. Моделирование зависимостей с помощью байесовских... Как видим, вероятность хорошего состояния принтера уменьшается, если наблюдается горящий индикатор низкого уровня тонера. Этого следовало ожидать. Рассуждения относительно различных причин одного следствия: наведенные зависимости В разделе 5.2.3 мы видели, что рассуждения в ситуации, когда имеются различные причины одного следствия, отличаются от других видов рассуждений из-за феномена сходящихся стрелок, который приводит к наведенной зависимости. Рассмотрим, например, переменные Состояние Программы и Состояние Сети, обе они являются родителями переменной Печатает Быстро. Сначала получим априорную вероятность корректного состояния программы: tonerLowIndicatorOn . unobserve () Л_Г> val softwareStateCorrectPrior = VariableElimination.probability(softwareState, f correct) println("Априорная вероятность корректного состояния программы = " + softwareStateCorrectPrior) О — Вычислить Р(Состояние Программы = корректное) В результате печатается: Априорная вероятность корректного состояния программы = 0.8 Далее мы наблюдаем, что сеть работает, и снова запрашиваем состояние программы: networkState.observe(f up) val softwareStateCorrectGivenNetworkUp = VariableElimination.probability(softwareState, f correct) println("Вероятность корректного состояния программы при условии, что " + "сеть работает = " + softwareStateCorrectGivenNetworkUp) О — Вычислить Р(Состояние Программы = корректное | Состояние Сети = работает) В результате печатается: Вероятность корректного состояния программы при условии, что сеть работает = 0.8 Вероятность не изменилась, несмотря на то, что имеется чистый путь из Состояния Сети в Состояние Программы через узел Печатает Быстро! Таким образом, мы видим, что в общем случае две причины одного следствия независимы. Это согласуется с интуицией: тот факт, что сеть работает, никак не сказывается на корректности состояния программы. Но если мы знаем, что принтер печатает не быстро, то все меняется. Наша модель предлагает два объяснения медленной печати: проблема с сетью и проблема с программой. Если мы наблюдаем, что сеть работает, значит, дело должно быть в программе. В этом позволяет убедиться следующий код: networkState.unobserve() ft—Г> printsQuickly.observe(false) val softwareStateCorrectGivenPrintsSlowly = 178
5.4. Применение вероятностного программирования для обобщения ... VariableElimination.probability(softwareState, f correct) println("Вероятность корректного состояния программы при условии, что " + "печатается медленно = " + softwareStateCorrectGivenPrintsSlowly) networkState . observe (f up) Л_гч val softwareStateCorrectGivenPrintsSlowlyAndNetworkUp = " VariableElimination.probability(softwareState, f correct) println("Вероятность корректного состояния программы при условии, что " + "печатается медленно и сеть работает = " + softwareStateCorrectGivenPrintsSlowlyAndNetworkUp) О — Вычислить Р(Состояние Программы = корректное | Печатает Быстро = false) © — Вычислить Р(Состояние Программы = корректное | Печатает Быстро = false, Состояние Сети = работает) В результате печатается: Вероятность корректного состояния программы при условии, что печатается медленно = 0.6197991391678623 Вероятность корректного состояния программы при условии, что печатается медленно и сеть работает = 0.39024390243902435 Знание о том, что сеть работает, заметно уменьшает вероятность корректности программы. Поэтому переменные Состояние Программы и Состояние Сети независимы, но не являются условно независимыми при условии Печатает Быстро. Это пример наведенной зависимости. Подведем итоги. • Когда мы выполняем рассуждение от следствия X к его непрямой причине Y, переменная X не является независимой от Y, но становится условно независимой при условии Z, если Z блокирует путь от X к Y. • То же самое справедливо, когда выполняется рассуждение от причины к непрямому следствию или в отношении двух следствий одной причины. • Для двух причин X и Y одного следствия Z верно обратное. X и Y независимы, но не являются условно независимыми при условии Z; это результат наведенной зависимости. На этом можно закончить пример диагностики компьютерной системы. Мы видели не вполне тривиальную сеть с интересными паттернами рассуждений. В следующем разделе мы выйдем за рамки традиционных байесовских сетей. 5.4. Применение вероятностного программирования для обобщения байесовских сетей: предсказание успешности продукта В этом разделе мы покажем, как обобщить модель байесовской сети для предсказания успешности маркетинговой кампании и размещения скрытой рекла- 179
Глава 5. Моделирование зависимостей с помощью байесовских... мы. Цель примера двоякая: во-первых, продемонстрировать возможности языков программирования в плане обобщения байесовских сетей, а, во-вторых, показать, что байесовские сети можно использовать не только для вывода причин наблюдаемых событий, но и для предсказания будущих событий. Как и в примере диагностики компьютерной системы, я сначала покажу, как спроектировать модель и выразить ее на Figaro, а затем как с ее помощью рассуждать. 5.4.1. Проектирование модели для предсказания успешности продукта Представим, что имеется новый продукт, и мы хотим обеспечить ему успех. Этого можно достичь разными способами. Можно вложить деньги в упаковку и другие приманки для покупателей. Можно поставить такую цену, при которой продукт купят с большей вероятностью. Или продвигать продукт, раздавая бесплатные версии, в надежде, что пользователи расскажут о них своим друзьям. Прежде чем выбирать стратегию, нужно знать относительную важность каждого фактора. В этом разделе описывается каркас модели, который можно использовать в приложении. Я говорю «каркас», потому что это не более чем скелет реальной модели, но для понимания идеи его достаточно. Ниже будет представлена простая байесовская сеть всего с четырьмя узлами, но интересны типы узлов и условные распределения вероятности. В модели имеются четыре переменные, показанные на рис. 5.12. • Типом переменной Социальная Сеть Целевого Покупателя является социальная сеть. Это один из способов с помощью языка программирования выйти за рамки обыкновенной байесовской сети, где переменные могут быть только булевыми, перечислениями, целыми или вещественными. Условное распределение вероятности этой переменной случайным образом генерирует сеть на основе популярности целевого покупателя. Что касается самой популярности, то, поскольку это управляющий параметр, мы моделируем ее как известную константу, а не переменную. Но при желаниии можно было бы внести неопределенность и сделать популярность еще одной переменной. • Целевому Покупателю Нравится - булева переменная, показывающая, нравится ли продукт целевому покупателю, это функция качества продукта. Качество продукта тоже считается известной константой, но можно было бы сделать ее переменной, зависящей от капиталовложений в продукт. • Скольким Друзьям Нравится - целая переменная, но при вычислении ее условного распределения вероятности производится обход Социальной Сети Целевого Покупателя, чтобы узнать количество друзей, которым продукт был показан и понравился. Это условное распределение гораздо богаче, чем возможно в традиционных байесовских сетях. • Число Купивших - целая переменная. При ее моделировании считается, что любой человек, которому понравился продукт, купит его с вероятное- 180
5.4. Применение вероятностного программирования для обобщения ... тыо, зависящей от ценовой доступности, каковая является еще одним управляющим параметром с известным постоянным значением. Следовательно, число купивших продукт подчиняется биномиальному распределению. Ниже приведен код этой модели на Figaro. Сначала я опишу, как он работает на верхнем уровне, а потом перейду к деталям. Тип: Network Социальная сеть целевого покупателя Тип: Boolean Нравится ли продукт целевому покупателю Социальная Сеть Целевого Покупателя Тип: Integer Сколько друзей целевого покупателя купят продукт после его предложения покупателю Целевому Покупателю Нравится Скольким Друзьям Нравится Тип: Integer Скольким друзьям целевого покупателя понравится продукт после его предложения покупателю Рис. 5.12. Сеть для предсказания успешности продукта Листинг 5-3- Модель для предсказания успешности продукта на Figaro class Network(popularity: Double) { I <]—A val numNodes = Poisson(popularity) } class Model(targetPopularity: Double, productQuality: Double, affordability: Double) { def generateLikes(numFriends: Int, productQuality: Double): Element[Int] = { def helper(friendsVisited: Int, totalLikes: Int, unprocessedLikes: Int): Elementflnt] = { if (unprocessedLikes == 0) Constant(totalLikes) else { val unvisitedFraction = 1.0 - (friendsVisited.toDouble - 1) / (numFriends - 1) val newlyVisited = Binomial(2, unvisitedFraction) val newlyLikes = Binomial(newlyVisited, Constant(productQuality)) Chain(newlyVisited, newlyLikes, (visited: Int, likes: Int) => <ъ-в <h-© 181
Глава 5. Моделирование зависимостей с помощью байесовских. helper(friendsVisited + unvisited, totalLikes + likes, unprocessedLikes + likes - 1) } helper(1, 1, 1) val targetSocialNetwork = new Network(targetPopularity) <]—© val targetLikes = Flip(productQuality) <]—0 ' <-0 ■ ;:i.t. ( i : :«• •:'<Г., ■) ) val numberBuy = Binomial(numberFriendsLike, Constant(affordability)) } <ь-о О — Определяем класс Network с единственным атрибутом, определенным с помощью элемента Poisson (см. в тексте) © — Создаем класс Model, принимающий известные управляющие параметры в качестве аргументов © — Определяем рекурсивный процесс для порождения числа людей, которым нравится продукт (см. в тексте) © — Социальная сеть целевого покупателя определяется как случайная сеть, основанная на популярности этого покупателя © — Нравится ли продукт целевому покупателю — булев элемент, зависящий от качества продукта © — Если целевому покупателю продукт нравится, вычисляем число людей, вызывая метод generateLikes. Если не нравится, то он не расскажет о нем своим друзьям, значит, это число будет равно нулю © — Число купивших продукт, подчиняется биномиальному распределению (см. в тексте) Три места в коде нуждаются в дополнительных пояснениях: элемента Poisson в Классе Network, процесс generateLikes И определение переменной numberBuy. Сначала поговорим об элементе Poisson и логике, стоящей за numberBuy, а затем перейдем к процессу generateLikes - самой интересной части модели. • Элемент Poisson - это целочисленный элемент, в котором используется распределение Пуассона. Как правило, оно применяется для моделирования числа возникновений события в течение некоторого промежутка времени, например, числа отказов сети за месяц или числа угловых ударов в футбольном матче. Проявив творческий подход, распределение Пуассона можно использовать для моделирования любой ситуации, в которой нужно знать количество некоторых объектов в области. В данном случае мы с его 182
5.4. Применение вероятностного программирования для обобщения ... помощью моделируем число людей в социальной сети целевого покупателя; это отличается от типичного применения, но является вполне разумным выбором. Элемент Poisson принимает в качестве аргумента среднее число событий, ожидаемых в течение заданного промежутка времени, но допускает, что фактическое число будет больше или меньше среднего. В нашей модели аргументом является популярность целевого покупателя; по определению, это оценка среднего числа людей в его социальной сети. • Здесь сосредоточена логика вычисления числа купивших продукт. Каждый человек, которому продукт понравился, купит его с вероятностью, равной значению параметра ценовой доступности. Поэтому общее число купивших подчиняется биномиальному распределению, в котором число испытаний равно числу друзей, которым понравился продукт, а вероятность покупки зависит от доступности продукта. Поскольку число людей, которым понравился продукт, само является элементом, то необходимо использовать составной элемент Binomial, принимающий элементы в качестве аргументов. Составной элемент Binomial требует, чтобы вероятность успеха испытания также задавалась элементом, потому-то мы и обернули параметр доступности af f ordability Элементом Constant. Элемент Constant принимает обыч- ное значение Scala и порождает элемент Figaro, который всегда принимает это значение. ЦеЛЬ фуНКЦИИ generateLikes - Определить, СКОЛЬКИМ ЛЮДЯМ ПОНраВИТСЯ продукт после того, как он будет предложен целевому покупателю, социальная сеть которого содержит заданное количество людей. В этой функции предполагается, что самому целевому покупателю продукт нравится, иначе она не была бы вызвана вовсе. Функция моделирует случайный процесс рекламирования понравившегося продукта друзьям. Она принимает два аргумента: (1) количество людей в социальной сети целевого покупателя, это переменная типа integer и (2) качество продукта - переменная типа Double, принимающая значения от 0 до 1. • Точная логика функции generateLikes не так важна, главное для нас - показать, что можно использовать нетривиальную рекурсивную функцию в качестве условного распределения вероятности. Но я все же объясню ЛОГИКу, Чтобы Пример был ПОНЯТеН. ОСНОВНУЮ ЧаСТЬ работы generateLikes поручает вспомогательной функции, которая вычисляет три переменные. - friendsvisited равна числу людей в социальной сети целевого покупателя, уже проинформированных о продукте. В начальный момент она равна 1, т. к. проинформирован только один человек. - totaiLikes равна числу людей из числа уже посещенных, которым понравился продукт. В начальный момент она равна 1, поскольку мы предполагаем, что generateLikes вызывается, только если целевому покупателю продукт понравился. 183
Глава 5. Моделирование зависимостей с помощью байесовских... - unprocessedLikes - ЧИСЛО ЛЮДвЙ, КОТОрЫМ Продукт ПОНраВИЛСЯ, НО ДЛЯ которых мы еще не успели смоделировать рекламирование продукта друзьям. Логика вспомогательной функции подробно объяснена ниже. Листинг 5.4. Вспомогательная функция для обхода социальной сети def helper(friendsVisited: Int, totalLikes: Int, unprocessedLikes: Int): Element[Int] = <-0 if (unprocessedLikes == 0) Constant(totalLikes) <3—© else { val unvisitedFraction = 1.0 - (friendsVisited.toDouble - 1) / (numFriends - 1) val newlyVisited = Binomial(2, unvisitedFraction) <3—© <^© val newlyLikes = Binomial(newlyVisited, Constant(productQuality)) <-0 Chain(newlyVisited, newlyLikes, (visited: Int, likes: Int) => helper(friendsVisited + visited, <}—© } totalLikes + likes, <]_ф unprocessedLikes + likes - 1)) <]—© О — Вспомогательная функция принимает в качестве аргументов обычные значения Scala, а возвращает элемент. Ее можно использовать внутри chain © — Критерий остановки: если больше некого обрабатывать, то число людей, которым понравился продукт, равно числу найденных к этому моменту © — Вычисляем вероятность, что случайный член социальной сети (за исключением обрабатываемого в данный момент лица) еще не посещался © — Обрабатываемый человек рекламирует продукт двум друзьям. Вероятность, что каждый из этих друзей еще не посещался, задается параметром unvisitedFraction © — Вероятность, что данному человеку понравится продукт, определяется параметром productQuality, который обернут элементом Constant, поскольку этого требует интерфейс составного элемента Binomial 184
5.4. Применение вероятностного программирования для обобщения ... © — Рекурсивный процесс часто программируется с помощью chain © — Каждый новый человек, которому понравился продукт, — еще не обработанное лицо, а единица вычитается, чтобы учесть того, кто только что обработан © — Новое значение числа посещенных людей равно старому значению плюс число тех, кто был посещен в этом вызове © — Новое значение числа людей, которым продукт понравился, равно старому значению плюс число тех, кому он понравился в этом вызове В этом примере необходимо больше навыков работы со Scala, но принципиально техника моделирования та же, что для байесовских сетей. Основная задача - представить процесс порождения возможных миров. В данном случае мы имеем сравнительно простой процесс распространения продукта по социальной сети, но можно закодировать и более сложные процессы. 5.4.2. Рассуждения с помощью модели для предсказания успешности продукта Поскольку мы спроектировали модель для предсказания успеха, то ее типичное применение сводится к заданию управляющих параметров и предсказанию числа людей, купивших продукт. Так как число людей - переменная типа integer с широкой областью значений, то предсказание вероятности конкретного значения не интересно. А интересно узнать среднее ожидаемое значение, или математическое ожидание. В теории вероятностей математическое ожидание - общее понятие. Берется какая-нибудь функция, определенная для переменной, и возвращается среднее значение этой функции. Пример приведен на рис. 5.13. Начинаем с распределения вероятности значений произвольного типа, в данном случае типа integer. Затем применяем к каждому значению функцию, возвращающую значение типа Double. В данном случае функция преобразует число типа integer в его представление типа Double. Далее вычисляется взвешенное среднее этих значений типа Double с весами, равными вероятностям значений. То есть мы умножаем каждое значение типа Double на его вероятность и суммируем результаты. В нашем примере требуется вычислить математическое ожидание числа людей, купивших продукт, т. е. переменную типа integer. Для этого напишем такую строку на Figaro: algorithm.expectation(model.numberBuy, (i: Int) => i.toDouble) Здесь algorithm - ссылка на используемый алгоритм вывода. В данном случае используется алгоритм выборки по значимости, который особенно хорошо подходит для предсказания исхода сложного порождающего процесса. Поскольку нам нужна ссылка на алгоритм, для выполнения вывода придется написать чуть более сложный код, чем раньше. Он показан в следующем фрагменте. Весь процесс получения управляющих констант и вычисления ожидаемого числа купивших продукт Заключен В фуНКЦИЮ predict! 185
Глава 5. Моделирование зависимостей с помощью байесовских. 0.1 -► 0 0.2 -^ 1 0.4 —» 2 (i: Int) => i.toDouble 0.1 -► 0.0 0.2 -► 1.0 0.4 -► 2.0 Expectation Начинаем с О.з -► з «--"^распределения значений произвольного типа .Преобразуем каждое значение к типу Double, применяя некоторую функцию 0.3 -*- 3.0 Вычисляем произведения "каждого значения типа Double на его вероятность 0.1 х 0.0 = 0.0 0.2 х 1.0 = 0.2 0.4 х 2.0 = 0. 0.3 х 3.0 = 0.9 -Складываем результаты 0.0 + 0.2 + 0.8 + 1.0 = 1.9 Рис. 5.13. Вычисление математического ожидания распределения целых значений элемента. Сначала значения элемента преобразуются к типу Double. Затем каждое полученное значение умножается на его вероятность, и результаты суммируются - получается среднее def predict(targetPopularity: Double, productQuality: Double, affordability: Double): Double = { val model = new Model(targetPopularity, productQuality, affordability) val algorithm = Importance(1000, model.numberBuy) <]—© algorithm, start () O—© val result = algorithm.expectation(model.numberBuy, (i: Int) => i.toDouble) algorithm.kill() <3—© result D И О — Создаем экземпляр модели предсказания с заданными управляющими константами © — Создаем экземпляр алгоритма выборки по значимости. 1000 — размер выборки, a model. numberBuy — то, что мы хотим предсказать © — Прогоняем алгоритм © — Вычисляем ожидаемое число людей, купивших продукт © — Очистка и освобождение ресурсов, захваченных алгоритмом Чтобы понять, как различные параметры влияют на число людей, купивших продукт, нужно выполнить функцию predict много раз с различными входными параметрами. Ниже приведен потенциальный результат. 186
100 100 100 100 10 10 10 10 0.5 0.5 0.9 0.9 0.5 0.5 0.9 0.9 5.5. Марковские сети Популярность Качество продукта Доступность Предсказанное число купивших 0.5 2.0169999999999986 0.9 3.7759999999999962 0.5 29.21499999999997 0.9 53.13799999999996 0.5 0.7869999999999979 0.9 1.4769999999999976 0.5 3.3419999999999885 0.9 6.066999999999985 Из этой таблицы можно вывести несколько заключений. Число покупателей примерно пропорционально доступности продукта. Но зависимость от качества продукта нелинейна: для каждой комбинации популярности и доступности число покупателей при качестве 0.9 в несколько раз выше, чем при качестве 0.5. Если популярность равна 100, то аж в 15 раз выше. Похоже, существует связь между популярностью и качеством продукта, причем популярность определяет предел числа охваченных рекламой людей при высоком качестве. Если качество низкое, то популярность не играет особой роли. В этом примере мы видели, как Figaro используется в качестве языка имитационного моделирования. Прогнозирование того, что случится в будущем, - типичная задача имитационного моделирования, и именно этим мы здесь и занимались. Ничуть не сложнее использовать эту модель для рассуждений в обратном направлении. Например, после того как продукт предложен нескольким целевым покупателям и стало известно, сколько друзей они убедили купить продукт, можно оценить качество продукта. Это тоже ценное применение модели. 5.5. Марковские сети В предыдущих разделах нас интересовали байесовские сети, представляющие направленные зависимости. Теперь настало время обратиться к ненаправленным зависимостям. Их моделируют марковские сети. Принцип работы марковской сети я объясню на примере типичного приложения для восстановления изображений. А затем покажу, как представить ее в Figaro и как с ее помощью проводить рассуждения. 5.5.1. Определение марковской сети Марковская сеть - это представление вероятностной модели, состоящее из трех частей: • Множество переменных - у каждой переменной имеется область допустимых значений. • Неориентированный граф, вершинами которого являются переменные - ребра, соединяющие вершины, не ориентированы, т. е. лишены стрелок. Допускаются циклы в графе. • Множество потенциалов - потенциалы определяют числовые параметры модели. Подробнее о них чуть ниже. 187
Глава 5. Моделирование зависимостей с помощью байесовских. На рис. 5.14 изображена марковская сеть для восстановления изображений. Каждому пикселю изображения соответствует переменная. В данном случае мы имеем массив пикселей 4x4, но сеть легко обобщить на изображение любого размера. В принципе, пиксель может быть любого цвета, но для простоты предположим, что его значением является булева величина, описывающая, является пиксель ярким или темным. Между любой парой пикселей, соседних по горизонтали или по вертикали, проведено ребро. Ребра представляют тот факт, что при прочих равных условиях два соседних пикселя с большей вероятностью будут одинаковыми, а не различными. Пиксель 31 Рис. 5.14. Марковская сеть для пиксельного изображения Оговорка «при прочих равных условиях» важна для понимания семантики модели. Если на время забыть о ребрах между пикселями и рассмотреть индивидуальные распределения вероятности значений каждого пикселя, то может оказаться, что они с большой вероятностью различны. Например, принимая во внимание все известные дополнительные факты, мы можем предполагать, что пиксель 11 яркий с вероятностью 90 %, а пиксель 12 - с вероятностью 10 %. В таком случае пиксели 11 и 12 с большой вероятностью различны. Но наличие ребра между пикселями 11 и 12 означает, что они одинаковы с большей вероятностью, чем были бы в отсутствие ребра. Ребро добавляет знание о том, что пиксели, вероятно, одинаковы. Этому знанию противостоит знание о том, что они, вероятно, различаются, но полностью изменить общий вывод оно неспособно. Конкретное знание, выраженное в ребре между пикселями И и 12, представлено потенциалом на этом ребре. А теперь дадим определение потенциалов. Потенциалы Как определяются числовые параметры марковской сети? В байесовской сети у каждой переменной имеется условное распределение вероятности. В марковской же сети все не так просто. У переменных нет своих числовых параметров. Их заменяют функции, называемые потенциалами, которые определены на множествах переменных. Если существует симметричная зависимость, то некоторые совместные состояния переменных, зависящих друг от друга, более вероятны, 188 Пиксель 41 Пиксель 42 Пиксель 43 Пиксель 44 Пиксель 32 Пиксель 33 Пиксель 34 Пиксель 21 Пиксель 22 Пиксель 23 Пиксель 24 Пиксель 11 Пиксель 12 Пиксель 13 Пиксель 14
5.5. Марковские сети чем другие, - при прочих равных условиях. Потенциал определяет вес каждого совместного состояния. Совместные состояния с большими весами вероятнее совместных состояний с малыми весами, при прочих равных условиях. Относительная вероятность двух совместных состояний равна отношению их весов, опять же при прочих равных условиях. Математически потенциал - это просто функция, отображающая значения переменных в неотрицательные вещественные числа. В табл. 5.1 приведен пример унарного потенциала одиночного пикселя в приложении восстановления изображения, а в табл. 5.2 - бинарный потенциал, определенный для пар пикселей. Таблица 5.1. Унарный потенциал одиночного пикселя, кодирующий тот факт, что при прочих равных условиях пиксель освещен с вероятностью 0.4 Пиксель 31 F Т Значение потенциала 0.6 0.4 Таблица 5.2. Бинарный потенциал пар пикселей, кодирующий тот факт, что при прочих равных условиях вероятность, что два пикселя одинаковы, в девять раз больше, чем вероятность, что они различны Пиксель 31 F F Т Т Пиксель 32 F Т F Т Значение потенциала 0.9 0.1 0.1 0.9 Как потенциальные функции взаимодействуют со структурой графа? Есть два правила: • в потенциальной функции могут упоминаться только переменные соединенные ребром в графе; • если две переменные соединены ребром, то они должны упоминаться совместно в какой-то потенциальной функции. В нашем примере с каждой переменной связана копия унарного потенциала из табл. 5.1, а с каждой парой соседних по горизонтали или по вертикали пикселей - копия бинарного потенциала из табл. 5.2. При таком назначении потенциалов оба правила соблюдены. Как марковская сеть определяет распределение вероятности Мы только что дали определение марковской сети. Но как она определяет распределение вероятности? Каким образом назначить вероятность каждому воз- 189
Глава 5. Моделирование зависимостей с помощью байесовских... можному миру, так чтобы сумма вероятностей была равна 1? Ответ не такой простой, как для байесовской сети, но и не слишком сложный. Как и в байесовской сети, возможный мир в марковской сети - это сопоставление каждой переменной значения из ее области значений. Какова вероятность такого возможного мира? Будем действовать последовательно. Для простоты рассмотрим массив пикселей 2x2 со следующими значениями: пиксель 11= true, пиксель 12 = true, пиксель 21 = true, пиксель 22 = false. Для этого мира рассмотрим значения всех потенциалов в модели. Унарные потенциалы определены в табл. 5.3. Для пикселей, равных true, значение потенциала равно 0.4, а для пикселей, равных false, оно равно 0.6. В табл. 5.4 показаны бинарные потенциалы четырех пар пикселей. Если оба пикселя совпадают, то потенциал пары равен 0.9, в остальных случаях - 0.1. Других потенциалов в модели нет. Таблица 5.3. Значения унарных потенциалов для примера возможного мира Переменная Пиксель 11 Пиксель 12 Пиксель 21 Пиксель 22 Значение потенциала 0.4 0.4 0.4 0.6 Таблица 5.2. Бинарные потенциалы для примера возможного мира Переменная 1 Пиксель 11 Пиксель 21 Пиксель 11 Пиксель 12 Переменная 2 Пиксель 12 Пиксель 22 Пиксель 21 Пиксель 22 Значение потенциала 0.9 0.1 0.9 0.1 Теперь перемножим значения всех потенциалов: 0.4 х 0.4 х 0.4 х 0.6 х 0.9 х 0.1 х 0.9 х 0.1 = 0.00031104. Зачем нужно перемножать? Вспомните об оговорке «при прочих равных условиях». Если в двух мирах вероятность одинакова всюду, кроме одного потенциала, то вероятности миров пропорциональны значениям этого потенциала. Это именно то, что мы получаем, помножив вероятности на значение потенциала. Продолжая рассуждать в том же духе, мы придем к произведению значений всех потенциалов как «вероятности» возможного мира. Я заключил слово «вероятность» в кавычки, потому что на самом деле это не вероятность. Вычисляя произведение потенциалов таким образом, мы обнаружим, что сумма «вероятностей» не равна 1. Но это легко исправить. Нужно лишь нормировать «вероятности», полученные перемножением потенциалов. Исходные произведения называются ненормированными вероятностями, а их сумма - нормировочным коэффициентом, который принято обозначать буквой Z. Таким образом, для получения вероятностей миров мы делим ненормированные вероятности на Z. Не пугайтесь - Figaro принимает на себя все хлопоты. 190
5.5. Марковские сети По ходу обсуждения обнаружился удивительный факт. В байесовской сети вероятность возможного мира можно было вычислить, перемножив релевантные условные распределения вероятности. В марковской сети вероятность одного мира невозможно определить без учета всех вообще миров. Чтобы вычислить нормировочный коэффициент, необходимо знать ненормированные вероятности всех возможных миров. По этой причине многие считают, что представление с помощью марковской сети труднее, чем с помощью байесовской, т. к. числа сложнее интерпретировать как вероятности. Но я считаю, что если вы будете помнить о принципе «при прочих равных условиях», то сможете уверенно определить параметры марковской сети. А вычисление нормировочного коэффициента оставьте Figaro. Разумеется, параметры байесовских и марковских сетей можно получить с помощью машинного обучения. Используйте структуру, которая лучше отвечает связям в приложении. 5.5.2. Представление марковских сетей и рассуждения с их помощью В одном отношении марковские сети определенно проще байесовских: когда речь заходит о паттернах рассуждений. В марковской сети нет понятия наведенной зависимости. Рассуждения могут проходить по любому пути между переменными, не блокированному наблюдавшейся переменной. Две переменные зависимы, если между ними существует путь, но становятся условно независимыми при условии некоторого множества переменных, если эти переменные блокируют все пути между ними. Вот и всё. Кроме того, поскольку все ребра марковской сети неориентированны, то нельзя говорить о причине и следствии или о прошлом и будущем. Обычно не ставится задача предсказания будущих исходов или вывода причин сделанных наблюдений. Требуется просто вывести значения одних переменных, если известны значения других. Представление модели восстановления изображения на Figaro В приложении для восстановления изображения предполагается, что одни пиксели наблюдаемы, а другие - нет. Требуется восстановить ненаблюдаемые пиксели. Для этого используется описанная в предыдущем разделе модель, в которой потенциалы соседних пикселей одинаковы. Ниже показан код представления этой модели на Figaro. Напомним, что в разделе 5.1.2 были упомянуты два способа задания симметричных связей: ограничения и условия. Сейчас мы воспользуемся ограничениями. val pixels = Array .fill (10, 10) (Flip (0 . 4) ) def setConstraint (il: Int, jl: Int, i2: Int, j2: Int) { val pixell = pixels(il)(jl) <^-© 191
Глава 5. Моделирование зависимостей с помощью байесовских. val pixel2 = pixels(i2)(j2) val pair = /4/4(pixell/ pixel2) pair.addConstraint(bb => if (bb._l == bb._2) 0.9; else 0.1) } <-o <ь-® for { i <- 0 until 10 j <- 0 until 10 } { if (i <= 8) setConstraint(i, j, i + 1, j) if (j <= 8) setConstraint(i, j, i, j+1) О — Задать для каждой переменной унарное ограничение © — Задать для пар переменных бинарные ограничения, зависящие от координат © — Применить бинарные ограничения ко всем парам соседних переменных Уместно будет сделать несколько замечаний. • Выражение Array, пи (Ю, Ю) (Fiip(0.4)) в определении переменной pixels создает массив 10x10 и заполняет его элементы различными экземплярами элемента Flip (0.4). Каждому пикселю соответствует свой элемент Flip, и это важно, поскольку значения пикселей должны быть различны. • Возможно, вы недоумеваете, зачем вообще для унарных потенциалов использовать элемент Flip, а не ограничение. Но в этом случае безразлично, к чему прибегнуть: к обычному для Figaro способу или к ограничению - результат один и тот же. Flip возвращает true с вероятностью 0.4 и false - с вероятностью 0.6. Произведение этих вероятностей дает ненормированную вероятность возможного мира - точно так же, как при задании с помощью ограничения. На самом деле, каждый элемент Figaro следует определять обычным способом, с помощью того или иного конструктора, даже при использовании языка для представления марковской сети. Если для элемента нет унарного ограничения, то обычный конструктор Figaro должен быть нейтральным, т. е. не дающим одному возможному миру предпочтение перед другими. ДЛЯ ЭТОГО МОЖНО ВОСПОЛЬЗОВаТЬСЯ ЭЛемеНТОМ Flip (0 . 5) ИЛИ Uniform. • КОНСТРУКЦИЯ АА В Определении setConstraint - ЭТО КОНСТруКТОр Пары В Figaro. Следовательно, АА (pixeli, pixei2) создает элемент, значением которого является пара значений элементов pixeii и pixei2. • В операторе спискового включения for конструкция о until ю означает диапазон целых чисел от 0 до 10, не включая правый конец, т. е. множество целых чисел 0, 1,..., 9. Если нужно включить правый конец, то следует написать о to ю. • Тот же оператор for дает пример вложенного цикла. В других языках нужно было бы написать два цикла for, один внутри другого. В Scala разрешается поместить оба цикла в один заголовок for. 192
5.5. Марковские сети Рассуждения с помощью модели восстановления изображения Мы хотим использовать модель восстановления изображения для вывода значений ненаблюдаемых пикселей по известным наблюдаемым. Для этого нужно три вещи: способ задания и обработки фактов, способ вычисления наиболее вероятных состояний пикселей и способ получения результатов. Рассмотрим их поочередно. Обработка фактов: массив пикселей 10x10 можно было бы построить по массиву символов 10x10, в котором символ равен 0, если пиксель не освещен, 1 - если освещен, и ? - если состояние неизвестно. Для обработки этих данных можно применить Следующую Простую фуНКЦИЮ setEvidence: def setEvidence(data: String) = { for { n <- 0 until data.length } { val i = n / 10 val j = n % 10 data(n) match { case '0' => pixels (i) (j) .observe(false) case '1' => pixels(i)(j).observe(true) case => () <M> О — Используется механизм сопоставления с образцом в Scala, который в данном случае напоминает предложение switch в других языках. Знак _ обозначает вариант по умолчанию. Следовательно, если символ равен ' ? ', то пиксель ненаблюдаемый Вычисление наиболее вероятных состояний пикселей: в этом примере встретился новый вид запроса. Раньше нас интересовала оценка апостериорных вероятностей элементов, а на этот раз - наиболее вероятные значения элементов. Однако нас интересуют не наиболее вероятные значения по отдельности, безотносительно к другим элементам, а наиболее вероятное совместное распределение значений всех переменных. Мы хотим знать, какой из возможных миров наиболее вероятен. Запрос такого вида в Figaro называется наиболее вероятным объяснением (НВО), потому что нам нужен мир, дающий наиболее вероятное объяснение данных. Алгоритмы обработки НВО-запросов отличаются от встречавшихся ранее алгоритмов вычисления вероятности, хотя они и взаимосвязаны. В этом примере мы воспользовались вариантом алгоритма распространения доверия MPEBelief Propagation, Предназначенным ДЛЯ ВЫЧИСЛеНИЯ НВО. Это ИТераТИВНЫЙ алгоритм, и количество итераций можно задать с помощью параметра. В данном случае мы проведем 10 итераций. Чтобы создать и выполнить экземпляр алгоритма MPEBelief Propagation, нуЖНО написать: val algorithm = MPEBeliefPropagation(10) algorithm.start() 193
Глава 5. Моделирование зависимостей с помощью байесовских. Просмотр результатов: нам нужно просто перебрать все пиксели и напечатать их наиболее вероятные значения. Для получения наиболее вероятного значения Элемента Предназначен МеТОД mostLikelyValue Класса MPEBeliefPropagation. Код приведен ниже. for { i <- 0 until 10 } { for { j <- 0 until 10 } { val mlv = algorithm.mostLikelyValue(pixels(i)(j)) if (mlv) print(flf) else print('O') } println () } Для прогона модели нужно подать на вход какие-то данные. Обычно они читаются из файла или формируются программно другим модулем. Но для простоты мы определим входные данные прямо в этой программе: D val data = f,,,,,00?000?000 0?010?0010 1107010011 11??000111 1101100071 1707100710 0000170700 0010770100 0170100170 077000110?""".filterNot(_.isWhitespace) setEvidence(data) О — В Scala конструктор " " " позволяет создавать длинные строки, занимающие несколько строчек на экране. Для создания строки длиной 100 мы отфильтровываем все пробельные символы При прогоне с этими данными программы выводит такой результат: 0000000000 0001000010 1100010011 1100000111 1101100011 1000100010 0000100000 0010000100 0100100100 0000001100 Это все, что я хотел сказать о марковских сетях. Глава получилась длинной, зато и узнали вы много. Теперь вы знакомы со всеми основными принципами вероят- 194
5.7. Упражнения ностных моделей и можете писать вероятностные программы для различных приложений. В следующих главах мы закрепим и расширим материал, чтобы можно было писать еще более интересные программы. И начнем с рассмотрения того, как контейнеры Scala и Figaro позволят создавать более крупные и структурированные модели. 5.6. Резюме • Предметом вероятностного моделирования являются связи между переменными. Симметричные связи порождают ненаправленные зависимости, асимметричные - направленные. • Направленная зависимость ведет от причины к следствию. Существуют разные виды причинно-следственных связей. • В байесовских сетях направленные зависимости представляются с помощью ориентированного ациклического графа. • Направление стрелок в байесовской сети необязательно совпадает с направлением рассуждения. Байесовская сеть позволяет проводить рассуждение в любом направлении. • Марковские сети служат для представления ненаправленных зависимостей с помощью неориентированного графа. • Идентификация типов связей между переменными в модели и написание программы на этой основе - шаг в правильном направлении. 5.7. Упражнения Решения избранных упражнений имеются на сайте www.manning.com/books/ practical-probabilistic-programming. 1. Для каждой из перечисленных ниже пар переменных решите, является ли зависимость между ними направленной или ненаправленной, и, если направленной, то в какую сторону. a. Карты игрока в покер и его торговля. b. Карты первого и второго игрока в покер. c. Мое настроение и сегодняшняя погода. d. Мое настроение и позавтракал я или нет. e. Температура в моей гостиной и настройки домашнего термостата. f. Температура в моей гостиной и показания находящегося в ней термометра. g. Температура в моей гостиной и на кухне, h. Тема и содержание статьи. i. Краткое и полное содержание статьи. 2. Для каждого из перечисленных ниже наборов переменных нарисуйте байесовскую сеть. 195
a. Карты первого игрока в покер, карты второго игрока, торговля первого игрока, последующая торговля второго игрока. b. Мое настроение сразу после просыпания, мое настроение в 10 утра, сегодняшняя погода и позавтракал я или нет. c. Температура в моей гостиной, температура на кухне, настройки домашнего термостата, показания термометра в гостиной, показания термометра на кухне. d. Тема статьи, краткое содержание статьи, полное содержание статьи, комментарии к статье. Вы должны спроектировать байесовскую сеть, моделирующую процесс приготовления супа. Ее задача - помочь в принятии решения о том, какие ингредиенты использовать и в каком количестве, а также в определении таких переменных, как время и температура готовки, всё это с целью оптимизировать различные качества пищи, например остроту и консистенцию. a. Какие переменные будут в вашей модели? b. Нарисуйте байесовскую сеть с этими переменными. c. Выберите функциональную форму Figaro для каждой переменной.. d. Определите числовые параметры функциональных форм. e. Воспользуйтесь моделью Figaro для ответа на различные вопросы, например, сколько времени следует готовить суп из заданных ингредиентов для достижения оптимальной консистенции. Теннисный матч состоит из нескольких сетов, а каждый сет - из нескольких геймов. Игрок, первым выигравший два сета, выигрывает матч. Игрок, первым выигравший шесть геймов, выигрывает сет. (Пока не будем обращать внимание на тай-брейки, но, выполнив упражнение 5, вы сумеете смоделировать и их тоже.) Подача чередуется при смене геймов. Напишите на Figaro программу, которая принимает два аргумента: вероятности, что каждый игрок выигрывает гейм, в котором подает. С ее помощью предскажите победителя матча. Усложним модель тенниса, включив очки. В рамках одного гейма игроки набирают очки. Игрок, первым набравший четыре очка, выигрывает гейм, если только перед этим оба игрока не набрали по три очка. Напишите на Figaro программу, которая также принимает два аргумента, но теперь это вероятности, что каждый игрок наберет очко на своей подаче. Снова воспользуйтесь ей для предсказания победителя. Еще усложним модель, включив моделирование очков в виде результата обмена ударами. Игрок характеризуется таким переменными, как результативность подачи, скорость и частота ошибок. Можете включить в модель столько деталей, сколько сочтете нужным, в том числе положения игроков и мяча при каждом ударе. В моем доме имеется центральное кондиционирование, управляемое термостатом на первом этаже. На втором этаже обычно теплее, чем на первом. Глава 5. Моделирование зависимостей с помощью байесовских... 196
5.7. Упражнения Создайте марковскую сеть для представления температуры в доме (пока не принимая во внимание термостат). Напишите на Figaro модель для представления этой сети. С помощью своей модели вычислите вероятность, что температура на втором этаже не ниже 27 градусов, если на первом этаже она равна 22 градуса. 8. Теперь добавим в модель термостат, а также температуру на улице и информацию о том, открыты ли окна. В этой модели сочетаются направленные и ненаправленные зависимости, поэтому она не является чистой марковской сетью, однако в Figaro такие комбинации вполне возможны. Напишите на Figaro программу и воспользуйтесь ей, чтобы решить, стоит ли мне открывать окно на втором этаже. 9. Рассмотрим фильтр спама из главы 3. В этом приложении все почтовые сообщения считались независимыми. Теперь предположим, что имеется несколько сообщений от одного отправителя. Их спамность заведомо сильно коррелирована. a. Создайте марковскую сеть, улавливающую такие корреляции. b. Байесовская сеть для спамных сообщений показана на рис. 3.8. Воспроизведите эту сеть в марковской сети из упражнения 9а. В этом случае мы также имеем комбинацию направленной и ненаправленной сети. Примечание. Хотя я говорил, что между классом объекта и его свойствами имеется причинно-следственная связь, и именно такой подход принят в фильтре спама, иногда имеет смысл двигаться в обратном направлении. Так обстоит дело, когда все признаки всегда наблюдаемы, как в фильтре спама. В таком случае расточительно явно моделировать распределение вероятности наблюдаемых признаков. Вместо этого можно создать модель, в которой признаки почтового сообщения определяют его класс. Классы сообщений затем связываются с помощью марковской сети из упражнения 9а. Модель такого вида называется условным случайным полем, они широко используются для понимания естественных языков и в компьютерном зрении. Я не собираюсь здесь вдаваться в детали условных случайных полей, но для тех, кто с ними знаком, отмечу, что их легко представить в Figaro. Особенность состоит в том, чтобы сделать наблюдаемые признаки сообщений не элементами Figaro, а переменными Scala, которые помогают определить распределение вероятности элемента, представляющего спамность сообщения. Все обучаемые параметры модели, конечно, будут элементами Figaro, и они смогут взаимодействовать с переменными Scala, которые представляют признаки, определяющие вероятность спама. 197
ГЛАВА 6. Использование коллекций Seal а и Figaro для построения моделей В этой главе, ■ Как коллекции используются для организации вероятностных моделей. ■ Различия между коллекциями Scala и Figaro, роли тех и других и совместное применение. ■ Типичные паттерны моделирования, выражаемые с помощью коллекций, в том числе иерархическое байесовское моделирование, моделирование сетей с неизвестным числом объектом и модели над непрерывной областью. В предыдущих двух главах был заложен солидный фундамент вероятностного моделирования. В этой главе мы сконцентрируемся на программировании и покажем, чем различные возможности языка программирования могут быть полезны для построения вероятностных моделей. В частности, мы обратим внимание на коллекции Коллекции - один из наиболее полезных механизмов высокоуровневого языка, позволяющий рассматривать несколько объектов одного типа как единое целое. Например, если нужно обработать много целых чисел, то можно поместить их в массив, а затем написать цикл, в котором все элементы массива умножаются на два и суммируются. Или, если говорить в терминах функционального программирования, то можно написать функцию тар, которая умножает каждый элемент массива на 2, и функцию fold, выполняющую сложение. То же самое справедливо и для вероятностного программирования; если имеется много переменных одного типа, то мы можем поместить их в коллекцию и применять к ней функции типа тар И fold.
6.1. Работа с коллекциями Seal а В Figaro это можно сделать двумя способами. Первый - воспользоваться обычными коллекциями Scala. Если вы опытный программист, то, без сомнения, знакомы с такими коллекциями, как массивы, списки, множества, отображения и т. д. В составе Scala имеется обширная библиотека коллекций, которой вы можете пользоваться для организации элементов и создания вероятностных программ. Figaro также предоставляет библиотеку коллекций, которая добавляет возможности для работы со многими элементами. Коллекции Figaro позволяют делать такие вещи, которые с помощью обычных коллекций Scala выполнить нелегко. С другой стороны, и коллекции Scala могут предложить такое, чего нет в коллекциях Figaro, так что для работы с вероятностными моделями полезны и те, и другие. Начнем с обычных коллекций Scala, а затем перейдем к коллекциям Figaro и посмотрим, что они позволяют. После этого обсудим, когда лучше воспользоваться коллекциями Scala, а когда применить коллекции Figaro. В последних двух разделах этой главы мы рассмотрим два весьма полезных применения коллекций Figaro: использование массивов переменной длины для моделирования ситуаций с неизвестным числом объектов и определение вероятностной модели над непрерывной областью пространства или времени. Примечание. Поскольку эта глава посвящена программированию, в ней вы найдете много кода. Приведено семь полных программ, код которых имеется на сайте книги. Знакомство с коллекциями Scala было бы полезно, хотя в коде и не используются особенно заумные конструкции. Кроме того, вы должны уверенно владеть концепциями байесовских сетей в объеме главы 5. 6.1. Работа с коллекциями Scala До сих пор в рассмотренных примерах было всего по одному. В примере с установлением подлинности Рембрандта была одна картина, у которой была одна тема, один размер и одна яркость. А если требуется работать с несколькими объектами, например, построить модель, где много картин, у каждой из которых может быть больше одной темы? В программировании для хранения нескольких объектов одного типа, обрабатываемых как единое целое, применяются коллекции. Например, можно завести массив чисел и использовать его для применения некоторой операции к каждому числу или для сложения всех чисел. Возможность в цикле обойти все числа в массиве намного сокращает программы и повышает их гибкость, потому что размер массива достаточно изменить только в одном месте. Не будь массивов, цикл пришлось бы полностью развернуть, и было бы гораздо труднее написать общий код для массивов разной длины в одной программе. Коллекции Scala привносят те же удобства в вероятностное программирование. Я приведу примеры нескольких типичных ситуаций, в которых полезны коллекции Scala, начав со случая, когда есть много переменных одного типа, зависящих от одной переменной. 199
Глава 6. Использование коллекций Scala и Figaro для построения... 6.1.1. Моделирование зависимости многих переменных от одной Рассмотрим классический пример, с которым впервые столкнулись в главе 4. Имеется монета сомнительного происхождения. Монета может оказаться асимметричной, и мы не знаем, с какой вероятностью при подбрасывании выпадет орел. Мы хотим оценить степень асимметрии монеты (вероятность выпадения орла). И, кроме того, хотелось бы предсказать результаты последующих подбрасываний. На рис. 6.1 показана байесовская сеть для этой задачи. В корневом узле находится переменная Асимметрия. Эта вещественная переменная представляет вероятность выпадения орла при одном подбрасывании. На рисунке показаны узлы для 101 подбрасывания, но вообще число подбрасываний может быть любым. При каждом подбрасывании вероятность выпадения орла равная значению Асимметрии. Можно представить себе, что мы наблюдали результаты первых 100 подбрасываний и хотим предсказать результат 101-го. Результат каждого подбрасывания зависит от степени асимметрии Асимметрия — корневой узел Рис. 6.1. Байесовская сеть для подбрасывания асимметричной монеты Представление модели подбрасывания монеты на Figaro Этот пример легко представить на Figaro, воспользовавшись массивом Scala для хранения результатов предыдущих подбрасываний монеты. Наша программа будет читать заданную в командной строке последовательность символов Н (орел) и Т (решка), описывающих результаты предыдущих подбрасываний. Для установления размера массива достаточно этого аргумента. Ниже приведен код модели: val outcomes = args(O) val numTosses = outcomes.length val bias = Beta(2,5) val tosses = Array.fill (numTosses) (Flip (bias) ) val nextToss = Flip(bias) «-o О — Создаем массив размера numTosses, состоящий из отдельных экземпляров элемента Flip(bias). Элементы массива представляют подбрасывания монеты 200
6.1. Работа с коллекциями Seal а Для моделирования асимметрии мы используем элемент Beta. В главе 3 при проектировании фильтра спама мы уже пользовались элементом Beta для моделирования вхождений слов в сообщение. Я говорил, что этот элемент отлично работает совместно с Flip и Binomial, так что использование Beta для подбрасывания монеты оправдано. На врезке ниже сказано еще несколько слов о связи между Элементами Beta И Flip. Далее мы строим массив для представления всех ранее наблюдавшихся подбрасываний. Для этого используется метод Array.пи, который создает массив заданной длины и предоставляет определение элемента. Это определение вычисляется заново для каждого элемента массива, т. е. в данном случае каждый элемент массива - отдельный экземпляр элемента Figaro Flip (bias), определяющего выпадение орла с вероятностью bias. И в самом конце мы создаем еще один элемент Flip, описывающий следующее подбрасывание, исход которого требуется предсказать. Сопряженное априорное распределение и почему элементы Beta и Flip работают совместно В теории вероятностей элементу Flip соответствует стандартное название - распределение Бернулли. А бета-распределение называется сопряженным априорным к распределению Бернулли. Это означает, что если имеются переменные, подчиняющиеся распределениям бета и Бернулли, и бета-переменная представляет вероятность того, что переменная Бернулли принимает значение true, то распределение бета-переменной при условии исхода переменной Бернулли также является бета-распределением. Присмотримся внимательнее к двум аргументам элемента Beta. Первый аргумент, а, представляет воображаемое число уже наблюдавшихся орлов плюс 1. Второй аргумент, р, -тоже самое, но для решек. В нашем примере используется элемент Beta (2, 5), т. е. предполагается, что еще до обучения мы видели, что выпал один орел и четыре решки. (Кстати, прибавление единицы к числу орлов и решек - математическое удобство; как мы увидим ниже, оно упрощает предсказание исхода следующего подбрасывания.) Это позволяет закодировать тот факт, что мы имеем априорные предположения об асимметрии еще до ознакомления с данными. Если никаких априорных предположений нет, можете взять Beta (1,1). Предположим теперь, что в результате подбрасывания выпал орел. Тогда для получения апостериорного распределения прибавляем к а единицу. А если выпала решка, то прибавляем 1 к р. Вообще, если наблюдается h орлов и t решек, то а увеличивается на h, a р - на t. Так, если априорное распределение описывалось элементом Beta (2r 5) и выпало три орла и две решки, то апостериорное распределение будет описываться элементом Beta {5, 7). Потому-то бета-распределение и является сопряженным априорным к распределению Бернулли. Также легко предсказать, выпадет ли при следующем подбрасывании орел, если асимметрия подчиняется бета-распределению. Это произойдет с вероятностью а/(а + р), т. е. 5/12 для случая Beta (5,7). Все вычисления в этом примере просты, для них не нужен никакой язык программирования. Но так бывает далеко не всегда, обычно вычислить апостериорное распределение и сделать прогноз с помощью такой простой формулы не удается. И, кроме того, в Figaro не обязательно использовать сопряженные априорные распределения. Но лучше все же использовать, если это возможно, поскольку это разумный способ представления априорных знаний о параметрах. 201
Глава 6. Использование коллекций Scala и Figaro для построения... Массивы Scala также упрощают задание фактов. Напомним, что переменная outcomes читается из командной строки и является последовательностью символов Н и Т, по одному для каждого подбрасывания. Чтобы указать исходы ранее наблюдавшихся подбрасываний, можно написать такой код: for { toss <- 0 until numTosses } { val outcome = outcomes(toss) == fHf tosses(toss).observe(outcome) } Вывод с помощью алгоритма с отсечением по времени Итак, мы создали модель и задали факты, пора приступать к выводу. В этой главе описан новый способ вывода, при котором вы полностью контролируете время работы: использование алгоритмов с отсечением по времени. Так называется алгоритм, который выдает наилучшее решение на тот момент, когда его прервали. Вы можете точно указать, сколько времени алгоритм должен работать, и он постарается сделать все возможное. В Figaro алгоритмы с отсечением по времени используются, как обычные алгоритмы. Мы создаем экземпляр алгоритма и запускаем его. Алгоритм с отсечением по времени работает в отдельном потоке, поэтому параллельно программа может делать что-то еще. Обычно она спит в течение времени, отведенного алгоритму для работы. Затем мы запрашиваем у алгоритма интересующую нас информацию. И в конце уничтожаем экземпляр алгоритма. Это важно, потому что вместе с экземпляром уничтожается и поток, иначе алгоритм будет и дальше потреблять ресурсы. Ниже приведен код выполнения вывода для нашей модели подбрасывания монеты с использованием алгоритм выборки по значимости. У этого алгоритма есть как версия без отсечения по времени, которая после запуска отрабатывает до естественного завершения, так и версия с отсечением. Две версии есть и у многих других алгоритмов в Figaro. В случае версии без отсечения мы сообщаем алгоритму объем выборки. А в версии с отсечением объем выборки заранее не задается, поскольку алгоритм сам выбирает столько примеров, сколько может обработать за имеющееся в его распоряжении время. val algorithm = Importance(nextToss, bias) О—О algorithm.start() Thread.sleep(1000) <3—© algorithm.stop() println("Средняя асимметрия = " + algorithm.mean(bias)) println("Вероятность орла при следующем подбрасывании = " + algorithm.probability(nextToss, true)) algorithm.kill() <3—® О — Создаем экземпляр алгоритма выборки по значимости, намереваясь опросить nextToss и bias. Figaro знает, что это версия с отсечением по времени, поскольку объем выборки не задан 202
6.1. Работа с коллекциями Seal а © — Ждать 1000 миллисекунд © — Освободить все ресурсы При запуске с аргументом нтнннннтнтнннтнннннн эта программа работает 1 секунду и печатает такой результат: Средняя асимметрия = 0.6641810675997326 Вероятность орла при следующем подбрасывании = 0.6414832927224574 6.1.2. Создание иерархических моделей В этом разделе показано, как создать последовательность последовательностей и тем самым обобщить предыдущий пример, добавив иерархию. Предположим, что мы подбрасываем монету, выбрав ее из многих монет в мешке, который выбирается из многих мешков в ящике... Ну и так далее. Для простоты будем моделировать подбрасывание монет из одного и того же мешка, так что у нас будет всего два уровня последовательностей. Но подход легко обобщается на произвольное число уровней. В предыдущем разделе мы видели, что свойство подбрасывания (выпадение орла) зависит от свойства монеты (асимметрии). В этом разделе асимметрия монеты зависит от свойства мешка. Точнее, некоторые монеты в мешке правильные, т. е. орел выпадает точно в 50 % случаев, тогда как другие - неправильные. У мешка имеется свойство: вероятность, что выбранная наугад монета окажется правильной. На рис. 6.2 изображена байесовская сеть для этого примера. В ней три уровня, соответствующих уровням иерархии. На первом уровне представлен мешок и ассоциированная с ним переменная ВероятностьПравильной. На втором уровне представлены монет и их асимметрия. Здесь монет три, но их число может быть произвольным. На третьем уровне находятся подбрасывания монет. С каждым подбрасыванием связаны два нижних индекса: номер монеты и номер подбрасывания. Результат подбрасывания зависит от асимметрии монеты. Количество подбрасываний для разных монет может различаться; в данной сети третья монета подбрасывается три раза, а первые две - по два раза. Эту ситуацию можно описать с помощью двух уровней последовательностей. На первом уровне мы имеем две последовательности, содержащие элементы Figaro, относящиеся к отдельным монетам: является ли монета правильной и какова степень ее асимметрии. Для каждой монеты на втором уровне находится последовательность элементов Figaro, представляющих подбрасывания монеты. В Scala последовательность легко создать с помощью оператора генерации списков for. Ниже приведен код модели. Программа ожидает, что в командной строке будет по одному аргументу для каждой наблюдавшейся монеты. Один аргумент содержит последовательность символов Н и Т, описывающих отдельные подбрасывания, - так же, как в предыдущем примере. val numCoins = args.length val fairProbability = Uniform(0.0, 1.0) 203
val isFair = for { coin <- 0 until numCoins yield Flip(fairProbability) Глава 6. Использование коллекций Scala и Figaro для построения. val biases = for { coin <- 0 until numCoins } yield If (isFair (coin), Constant (0.5), Beta(2,5)) val tosses = for { coin <- 0 until numCoins } yield { | for { toss <- 0 until args(coin).length } yield Flip(biases(coin) ) } |«-® <^-в <i-o О — Создаем последовательность длины numCoins элементов Flip(fairProbability) © — Если монета правильная, то ее асимметрия заведомо равна 0.5, иначе описывается элементом Beta(2, 5) © — Порождается последовательность последовательностей подбрасываний, по одной для каждой монеты © — Для данной монеты порождается последовательность элементов Flip(biases(coin)), длина которой равна числу подбрасываний монеты, полученному из аргумента в командной строке Первый уровень представляет мешок Второй уровень представляет монеты в мешке Третий уровень представляет подбрасывания одной монеты Рис. 6.2. Байесовская сеть для иерархической модели Теперь можно написать двумерный оператор for для задания фактов: for { coin <- 0 until numCoins toss <- 0 until args(coin).length } { 204 Вероятность- Правильной Асимметрия З Асимметрия 1 Асимметрия 2 Подоpa сывание ■ i Подбра сывание а Подбра сывание 21 Подбра сывание jj Подбра сывание ti Подбра сывание ц Подбра сывание 3J
6.1. Работа с коллекциями Seal а val outcome = args(coin) (toss) == fHf tosses (coin) (toss).observe(outcome) } И наконец, выполнить вывод: val algorithm = Importance(fairProbability, biases(0)) algorithm.start() Thread.sleep(lOOO) algorithm.stop() val averageFairProbability = algorithm.mean(fairProbability) val firstCoinAverageBias = algorithm.mean(biases(0)) println("Средняя вероятность правильности: " + averageFairProbability) println("Средняя асимметрия первой монеты: " + firstCoinAverageBias) algorithm.kill() Если этой программе передать аргументы нтннт н тннн ннт, то она напечатает такой результат: Средняя вероятность правильности: 0.7079517451620699 Средняя асимметрия первой монеты: 0.4852371044437457 6.1.3. Моделирование одновременной зависимости от двух переменных В предыдущем разделе мы видели, как исход подбрасывания монеты зависит от степени ее асимметрии, которая, в свою очередь, зависит от вероятности выборки правильной монеты из мешка. Это иерархически организованная модель. Альтернативой является зависимость переменной сразу от двух других переменных, не связанных никакой иерархией. В последнем примере использования коллекций Scala мы рассмотрим моделирование таких ситуаций с помощью двумерных массивов, когда одна переменная зависит от двух других, каждая из которых является коллекцией. Например, объем продаж продукта в регионе может зависеть от качества продукта и от известности в этом регионе торговой марки. В таком случае мы имеем массив продуктов, массив регионов и двумерный массив продаж, в котором каждый элемент зависит от соответствующего продукта и региона. Примечание. В этом разделе речь идет только о двумерных массивах. Но, вообще говоря, число измерений может быть любым. Это массивы Scala, поэтому работа с ними устроена так же, как в обычных программах. На рис. 6.3 показана байесовская сеть для этого примера. Разобраться в ней не сложно. Имеются две переменные, относящиеся к качеству продуктов, две переменные, относящиеся к известности марки, и шесть переменных, относящихся к продажам, - по одной для каждой комбинации качества продукта и известности в регионе. Несмотря на простоту сети, она допускает содержательные рассуждения, особенно если известны результаты наблюдения продаж, а требуется вывести каче- 205
Глава 6. Использование коллекций Scala и Figaro для построения.. ство продукта и известность марки. Например, объем продаж продукта 1 может предоставить информацию о качестве продукта 2. Чтобы понять, как это возможно, предположим, что в регионе 1 высоки продажи продукта 1. Отсюда можно сделать вывод, что марка хорошо известна в регионе 1. А если продажи продукта 2 в регионе 1 низкие, то можно предположить, что качество продукта 2 оставляет желать лучшего. С другой стороны, если продажи продукта 1 в регионе 1 также низкие, то мы уже не будем так уверены, что продукт 2 низкокачественный, потому что есть основания полагать, что в регионе 1 плохо известна сама марка. С каждым Продуктом I связано Качество Продукта I Продажи., связана с Качество Продукта I и Известностью В Регионе J С каждым Регионом J связана Известность В Регионе J Рис- 6.3. Байесовская сеть для двумерной модели продаж Если вы еще не забыли обсуждение паттернов рассуждений в байесовских сетях (глава 5), то сразу заметите, что путь Продажии - Известность В Регионе 1 - Продажи^ - Качество Продукта 2 активен. Действительно, единственная наблюдаемая на этом пути промежуточная переменная - Продажи2Г Но в ней сходятся стрелки, а, значит, путь не блокируется наблюдением этой переменной. Вместе с тем, переменная Известность В Регионе 1 не наблюдается, и стрелки в ней не сходятся, так что путь не блокирован и здесь. Следовательно, путь активен. Как видим, при наблюдении продаж в этой модели все переменные связаны друг с другом. Значит, при выполнении вывода будут вычислены сразу все переменные, относящиеся к качеству продуктов и известности марки в регионе. Такой вывод называется коллективным, и подобные рассуждения встречаются в вероятностном программировании достаточно часто. Двумерная модель продаж на Figaro Эту ситуацию легко представить на Figaro с помощью двумерных массивов. Я приведу весь код целиком, потому что его составные части мы уже видели. 206 Качество Продукта 1 Качество Продукта 2 Качество Продукта 3 Продажи м Продажи \7 Продажи г Продажи 22 Продажи Г Продажи V? Известность В Регионе 1 Известность В Регионе 2
6.1. Работа с коллекциями Seal а Листинг 6.1. Двумерная модель продаж import com.era.figaro.library.atomic.continuous.Beta import com. era.figaro. language . Flip import com.era.figaro.algorithm.sampling.Importance object Sales { def main(args: Array[String]) { val numProducts = args.length I <l—ft val numRegions = args(0).length val productQuality = Array.fill(numProducts)(Beta(2,2)) val regionPenetration = Array.fill(numRegions)(Beta(2,2)) def makeSales(i: Int, j: Int) = Flip(productQuality(i) * regionPenetration(j)) val highSales = Array.tabulate(numProducts, numRegions)(makeSales m for { i <- 0 until numProducts j <- 0 until numRegions } { val observation = args(i)(j) == fTf highSales(i)(j).observe(observation) } s val targets = productQuality ++ regionPenetration val algorithm = Importance(targets:_*) algorithm.start() Thread.sleep(lOOO) algorithm.stop() for { i <- 0 until numProducts } { println("Качество продукта " + i + ": " + algorithm.mean(productQuality(i))) } for { j <- 0 until numRegions } { println("Известность в регионе " + j + ": " + algorithm.mean(regionPenetration(j) ) ) } algorithm.kill() E § } О — В командной строке задается несколько аргументов — по одному для каждого продукта. Каждый аргумент — строка, содержащая по одному символу для каждого региона, все строки должны быть одинаковой длины © — Конструкция Array.tabulate(numProducts, numRegions) создает двумерный массив, элементы которого соответствуют комбинациям продукта и региона. Каждый элемент массива — это элемент Flip, в котором вероятность равна произведению качества продукта на известность в регионе 207
лава 6. Использование коллекций Scala и Figaro для построения.. © — Результаты наблюдения продаж берутся из командной строки; Т означает высокие продажи для комбинации продукта и региона. Для каждого элемента массива highSales задается соответствующий ему факт О — Создаем экземпляр алгоритма выборки по значимости с отсечением по времени, указывая, что нас интересует качество каждого продукта и известность марки в каждом регионе © — Печатаем среднее качество каждого продукта и среднюю известность в каждом регионе Итак, мы рассмотрели три практически полезные модели, которые можно представить с помощью коллекций Scala. Пора расширить арсенал, включив него коллекции Figaro. 6.2. Работа с коллекциями Figaro В предыдущем разделе мы узнали, как коллекции Scala помогают создавать интересные модели со многими переменными определенного вида. В этом разделе мы познакомимся с коллекциями Figaro. Коллекция Figaro - это структура данных для хранения нескольких элементов Figaro одного вида. Вы наверняка подумали, что это ничем не отличается от коллекции Scala - так зачем же еще какие-то коллекции? 6.2.7. Почему коллекции Figaro полезны? Особенность коллекций Figaro состоит в том, что они содержат элементы, определяющие распределение вероятности значений, - и знают об этом! Они позволяют заглядывать внутрь элементов и производить операции над значениями. Как это работает, показано на рис. 6.4. Коллекция Figaro Элементы Значения Новые коллекции Новые элементы Новые значения Рис. 6.4. Коллекция Figaro содержит элементы, определяющие распределение вероятности значений. Она позволяет применять к этим значениям операции и тем самым порождать новые элементы и новые коллекции Это открывает возможность делать разные полезные вещи, некоторые из них перечислены ниже. • ЕСЛИ с - КОЛЛеКЦИЯ Figaro Элементов ТИПа Integer, ТО ВЫЗОВ стар ( (i: Int) => i * 2) порождает новую коллекцию Figaro. Каждому элементу е исходной коллекции соответствует элемент, эквивалентный результату Apply (e, d: int) => i * 2), в новой коллекции. Поэтому новая коллекция состоит 208
6.2. Работа с коллекциями Figaro из элементов типа integer. Каждый ее элемент соответствует процессу, который начинается с элемента исходной коллекции, порождает его значение и умножает это значение на 2. Например, если с - коллекция Figaro, содержащая два элемента uniform (2, 3) и constant (5), то коллекция Figaro c.map((i: Int) => i * 2) СОДерЖИТ Элементы Apply (Uniform (2, 3) , (i: Int) => i * 2) И Apply (Constant (5) , (i: Int) => i * 2). На Практике ЭТО означает, что коллекция содержит два элемента, один из которых определяет равномерное распределение значений 4 (2 * 2) и б (з * 2), а другой является константой ю (5 * 2). • ЕСЛИ с - КОЛЛеКЦИЯ Figaro Элементов ТИПа Double, TO ВЫЗОВ с. chain ((d: Double) => Fiip(d)) порождает новую коллекцию Figaro, в которой каждому Элементу е ИСХОДНОЙ КОЛЛеКЦИИ СООТВеТСТВуеТ Элемент Chain (e, (d: Double) => Fiip(d)). Первая коллекция содержит параметры, а вторая - элементы Flip, так что каждый Flip зависит от соответственного параметра. Например, еСЛИ с СОДерЖИТ ЭЛемеНТЫ Beta (2, 1) И Beta(l, 2), ТО В НОВОЙ КОЛЛеКЦИИ ИМ будут СООТВеТСТВОВаТЬ ЭЛемеНТЫ Flip (Beta (2, 1) ) И Flip (Beta (l, 2)) (напомним, что составной Flip - это сокращенная запись Chain). • Если с - коллекция Figaro, содержащая элементы типа integer, то вызов c.exists((i: int) => i < 0) возвращает булев элемент, показывающий, если ли в исходной коллекции элементы с отрицательными значениями. Аналогично вызов с.count ((i: int) => i < 0) возвращает количество таких элементов. • К контейнеру можно применить любую операцию сворачивания или агрегирования. Опытные программисты на Scala наверняка знакомы с операциями типа foidLeft. Например, если с - коллекция Figaro, содержащая элементы типа integer, то вызов с. foidieft (_ + _) создает элемент, представляющий сумму значений всех элементов коллекции. Рассмотрим, к примеру, коллекцию Figaro, содержащую элементы uniform (2, з) и constant (5). Сумма может принимать два значения: 2 + 5 = 7иЗ + 5 = 8. Поскольку вероятность значений 2 и 3 в первом элементе равна 1/2, то и вероятность каждой из этих сумм равна 1/2. Следовательно, элемент с. f oidief t (_ + _) определяет равномерное распределение значений 7 и 8. • Существует целый ряд других операций над контейнерами Figaro, подробности смотрите в Scaladoc. Это основная причина полезности коллекций Figaro. Но есть и другие. • Коллекции Figaro переменной длины позволяют представить ситуацию, когда количество объектов заранее неизвестно. • Они также позволяют представить ситуации, когда множество переменных бесконечное и даже не счетное. Обо всем этом будет рассказано ниже. 209
Глава 6. Использование коллекций Scala и Figaro для построения... 6.2.2. Иерархическая модель и коллекции Figaro В качестве первого примера вернемся к иерархической модели подбрасывания монет, выбираемых из мешка, и посмотрим, как ее можно представить с помощью коллекций Figaro. Для этого мы воспользуемся простейшей коллекцией: FixedsizeArray. Она содержит фиксированное число элементов, каждый из которых генерируется тем или иным способом. На рис. 6.5 показано, что конструктор FixedsizeArray принимает два аргумента: количество элементов и генератор элементов. Генератор - это функция, которая принимает аргумент типа integer, равный индексу элемента в массиве, и возвращает элемент, зависящий от этого индекса. Код на рис. 6.5 создает коллекцию Figaro из 10 элементов, в которой i-ый элемент (нумерация начинается с 0) равен Flip(l.o / ц + щ. Количество элементов Генератор элементов л new FixedsizeArray (10, i => Flipd.O / (i + 1))) Индекс в Определение массиве элемента Рис. 6.5. Конструирование массива фиксированной длины Поскольку иерархическую модель мы уже видели, я приведу код, выделив изменения полужирным шрифтом. Для иллюстрации использования контейнеров Figaro я немного усложнил пример, разрешив включать в аргумент командной строки также символы ?, означающие, что исход подбрасывания монеты не наблюдался. Тогда можно будет запросить у модели, выпадал ли орел при каком-либо подбрасывании конкретной монеты. Листинг 6.2. Иерархическая модель с применением контейнеров Figaro import com.era.figaro.language.{Flip, Constant} import com.era.figaro.library.atomic.continuous.{Uniform, Beta} import com.era.figaro.library.compound.If import com.era.figaro.algorithm.sampling.Importance import com. era.figaro. library. process . FixedsizeArray <3—© object HierarchicalContainers { def main(args: Array[String]) { val numCoins = args.length val fairProbability = Uniform(0.0, 1.0) val isFair = new FixedsizeArray (numCoins + 1, i => Flip (fairProbability)) <]—© 210
6.2. Работа с коллекциями Figaro val biases = isFair.chain(if (_) Constant(0.5) else Beta(2,5)) val tosses = for { coin <- 0 until numCoins } yield new FixedSizeArray(args(coin).length, i => Flip(biases(coin))) val hasHeads = for { coin <- 0 until numCoins tosses(coin).exists(b => b) yield a for { coin <- 0 until numCoins toss <- 0 until args(coin).length } { args (coin) (toss) match { case fHf => tosses(coin)(toss).observe(true) case 'T' => tosses(coin)(toss).observe(false) case => () } } В val algorithm = Importance(fairProbability, hasHeads(2)) algorithm.start() Thread.sleep(lOOO) algorithm.stop() printIn("Вероятность, что хотя бы в одном подбрасывании третьей "монеты выпал орел = " + algorithm.probability(hasHeads(2), true)) algorithm.kill() } } О — com.cra.figaro.library.process — пакет, содержащий библиотеку коллекций Figaro © — В этом массиве фиксированной длины каждый элемент — Flip(fairProbability), вне зависимости от индекса © — Для каждой монеты создаем новый элемент, равный либо Constant(0.5), либо Beta(2, 5) в зависимости от значения isFair(coin) © — Переменная tosses ссылается на последовательность Scala (одну для каждой монеты), в которой каждый элемент — это коллекция Figaro, содержащая элементы Flip, для которых вероятность выпадения орла равна асимметрии соответствующей монеты © — hasHeads(coin) — элемент, описывающий, выпал ли орел хотя бы при одном подбрасывании монеты. Пояснения к конструкции tosses(coin).exists(b => b) приведены в тексте © — Задание фактов производится почти так же, как и раньше, tosses(coin) — коллекция FixedSizeArray, для получения элемента из нее можно использовать выражение tosses(coin)(toss) — так же, как для массива Scala В пояснениях больше всего нуждается строка tosses (coin) .exists (b => b). Выделим в ней отдельные части. Начнем с того, что переменная tosses определена как следующий код: for { coin <- 0 until numCoins } yield new FixedSizeArray(args(coin).length, i => Flip(biases(coin))) 211
Глава 6. Использование коллекций Scala и Figaro для построения... Конструкция for...yieid создает последовательность Scala, в которой каждой монете соответствует массив фиксированной длины. Длина этого массива равна числу подбрасываний монеты, а эта величина определяется соответствующим аргументом в командной строке. В каждом элементе массива находится элемент Figaro Flip, аргумент которого равен асимметрии монеты. Таким образом, tosses (coin) - массив фиксированной длины. exists - это метод коллекций Figaro, который создает новый элемент, описывающий, есть ли среди элементов коллекции такие, которые удовлетворяют заданному предикату. В данном случае задан предикат ь => ь, т. е. функция, которая принимает значение ь и возвращает его же. Поскольку исход подбрасывания монеты - булева величина, то ь имеет тип Boolean, и этот предикат возвращает true тогда и только тогда, когда элемент принимает значение true (т. е. при подбрасывании выпал орел). Следовательно, tosses (coin) .exists (b => ь) возвращает элемент, принимающий значение true, если хотя бы при одном подбрасывании монеты выпал орел. Итак, мы познакомились с первым примером коллекций Figaro, теперь пойдем дальше и изучим связь между коллекциями Scala и Figaro. 6.2.3. Совместное использование коллекций Scala и Figaro Увидев, как коллекции Figaro позволяют применять операции к значениям элементов, вы, наверное, задаетесь вопросом, а нужны ли вообще в Figaro коллекции Scala. Нужны - потому что различных коллекций Scala больше, и многие методы коллекций Scala для коллекций Figaro не определены. Если вам не нужны операции над значениями элементов коллекции, то лучше работать с коллекцией Scala. Преобразование коллекции Scala в коллекцию Figaro и наоборот Иногда нужно то и другое: гибкость коллекций Scala в сочетании с возможностью применять операции к значениям. По счастью, совсем нетрудно преобразовать коллекцию Scala в коллекцию Figaro и наоборот. Опишем несколько способов такого преобразования. • Конструктор класса container. В Figaro container - это общий класс коллекций, содержащих конечное число элементов. Его конструктор принимает переменное число аргументов, все они должны быть элементами, принимающими значения одного и того же типа. Конструктор возвращает коллекцию Figaro, содержащую эти элементы. Для создания коллекции Figaro, состоящей из трех подбрасываний можно написать container (toss i, toss2, toss3). Если коллекция Scala - последовательность (т. е. реализует характеристику seq, как массив или список), то ее можно преобразовать в переменное чис- 212
6.2. Работа с коллекциями Figaro ЛО аргументов С ПОМОЩЬЮ КОНСТРУКЦИИ :_*. Так, еСЛИ tosses = List(tossl, toss2, toss3), TO KOHCTpyKTOp Container (tosses :_*) порождает ту Же КОЛ- лекцию Figaro, что и выше. Если же коллекция больше походит на множество, не являющееся последовательностью, то для преобразования можно сначала воспользоваться методом toList, а затем применить конструкцию :_*, например! Container (Set (tossl, toss2, toss3) . toList :_*). Любую коллекцию Scala, реализующую характеристику Traversable, - а это большинство коллекций - можно преобразовать в список указанным способом, а затем преобразовать список в коллекцию Figaro. • Перечисление элементов контейнера. Если имеется экземпляр класса Figaro container (конечная коллекция), то его можно преобразовать в Последовательность Scala Seq С ПОМОЩЬЮ Метода elements. Так, Container (tossi, toss2, toss3) .elements возвращает последовательность Scala, содержащую элементы tossi, toss2 и toss3. Затем эту последовательность seq можно преобразовать в любую коллекцию Scala, например, список или множество. • Порождение отображения индексов на элементы. Фундаментальное определение коллекции Figaro - это отображение значений множества индексов на элементы. Мы более внимательно изучим эту концепцию в разделе 6.4, когда будем рассматривать бесконечные процессы, в которых множество индексов состоит из вещественных чисел. Для коллекции Fixedsize- Array множество индексов состоит из целых чисел от 0 до числа элементов минус 1. Для класса container множество индексов конечно. Поэтому легко преобразовать container в явное отображение Scala (мар) индексов на значения. Для этого предназначен метод toMap класса container. Пример: предсказание продаж Для иллюстрации совместной работы Scala и Figaro усложним пример с продажами из раздела 6.1.3. Как и раньше, имеется некоторый набор продуктов, каждый со своим качеством, и некоторый набор регионов, каждый со своей степенью известности торговой марки. Мы наблюдаем продажи каждого продукта в каждом регионе за последний год и хотим предсказать продажи в будущем году. Кроме того, мы хотим узнать, сколько людей могла бы нанять компания для поддержки каждой линейки продуктов и в региональные отделы продаж. Как и раньше, начнем с одномерных массивов Scala, содержащих продукты и регионы, и с двумерного массива продаж: val productQuality = Array.fill(numProducts)(Beta(2,2)) val regionPenetration = Array.fill(numRegions)(Beta(2,2)) def makeSales(i: Int, j: Int) = Flip(productQuality(i) * regionPenetration(j)) val highSalesLastYear = Array.tabulate(numProducts, numRegions)(makeSales _) val highSalesNextYear = Array.tabulate(numProducts, numRegions)(makeSales ) 213
Глава 6. Использование коллекций Scala и Figaro для построения... Это полная модель всех продуктов, регионов и продаж в прошлом и будущем году. Ее можно использовать, чтобы спрогнозировать продажи в будущем году на основе данных о продажах в прошлом году. Для этой цели достаточно коллекций Scala, так что если вам не нужны дополнительные возможности коллекций Figaro, то и не используйте их. Но мы хотим также предсказать количество новых сотрудников в разрезе продуктов и регионов, а вот для этого понадобятся коллекции Figaro. Приведенный ниже код создает массив Scala, содержащий по одному элементу на каждый продукт, причем этим элементом является объект Figaro container, в котором хранятся элементы, представляющие прогноз продаж соответствующего продукта во всех регионах: def getSalesByProduct(i: Int) = for { j <- 0 until numRegions } yield highSalesNextYear(i)(j) val salesPredictionByProduct = Array.tabulate(numProducts)(i => Container(getSalesByProduct(i):_*)) А вот теперь воспользуемся коллекциями Figaro для прогнозирования количества вновь нанятых сотрудников. Сначала для каждого продукта создаем элемент, представляющий количество регионов с высоким объемом продаж в будущем году: val numHighSales = for { predictions <- salesPredictionByProduct } yield predictions.count(b => b) numHighSales - массив Scala, содержащий по одному объекту - элементу Figaro ТИПа Integer - ДЛЯ КаЖДОГО продукта. Мы СДелаЛИ salesPredictionByProduct мас- сивом коллекций Figaro, чтобы для каждого объекта-коллекции можно было вызвать метод predictions.count(b => b) и получить элемент, представляющий количество продуктов, для которых прогнозируется высокий объем продаж. Это было бы трудно сделать по-другому, но совсем легко с помощью коллекции Figaro. Затем для каждого продукта создаем элемент, представляющий число вновь нанятых под него сотрудников, оно зависит от соответствующего продукта элемента numHighSales. Как обычно в Figaro, такого рода зависимости создаются с помощью chain. Я покажу два эквивалентных способа добиться нужного результата: с помощью коллекций Scala и коллекций Figaro. Сначала вариант с коллекциями Scala: val numHiresByProduct = for { i <- 0 until numProducts } yield Chain(numHighSales(i), (n: Int) => Poisson(n + 1)) Здесь создается массив Scala элементов chain. Теперь - с помощью коллекций Figaro: val numHiresByProduct = Container(numHighSales:_*).chain((n: Int) => Poisson(n + 1)) Здесь создается контейнер Figaro, содержащий элементы chain. 214
6.3. Моделирование ситуаций с неизвестным числом объектов ни НЕ С моделью мы закончили. Код задания фактов и выполнения вывода по существу не отличается от рассмотренного выше. Но одна тонкость достойна упоминания. Мы Запрашиваем Сведения об Элементах В КОЛЛеКЦИИ numHiresByProduct, поэтому для варианта, когда numHiresBy Product является контейнером Figaro, необходим какой-то способ получить эти элементы и передать их алгоритму выборки по значимости. Делается это так: val targets = numHiresByProduct.elements val algorithm = Importance(targets:_*) Осмыслим, что же мы сделали. У коллекций Scala и Figaro есть свои - важные - роли. С одной стороны, в Scala имеются удобные и простые многомерные массивы. Создать объекта Figaro container с двумерным множеством индексов тоже можно, но это куда более утомительно. С другой стороны, подсчет, т. е. операция агрегирования, легко реализуется с помощью контейнеров, но гораздо сложнее в Scala. Собственно говоря, для агрегирования пришлось бы повторить логику, уже реализованную в Figaro, а это не тривиально. Теперь, освоив принципы работы с коллекциями в Figaro, рассмотрим более экзотические, но от того не менее полезные виды коллекции. 6.3. Моделирование ситуаций с неизвестным числом объектов Встречавшиеся нам до сих пор коллекции Figaro содержали фиксированное число объектов. Но во многих ситуациях число объектов неизвестно и может быть переменным. Для таких случаев в Figaro имеются массивы переменной длины. Сначала я расскажу, когда такие массивы полезны, затем представлю структуру данных variabiesizeArray и, наконец, приведу пример массивов переменной длины в действии. 6.3.7. Открытая вселенная с неизвестным числом объектов Пусть мы реализуем систему наблюдения за дорожным движением на автомагистрали. Наша цель - следить за потоком машин и предсказывать возникновение пробок. В стратегически важных точках установлены видеокамеры. Для решения задачи нужны две вещи. Во-первых, требуется вывести число проезжающих в каждой точке машин с учетом отождествления одной и той же машины, проехавшей мимо двух разных камер. Во-вторых, нужно уметь прогнозировать появление новых машин на автомагистрали и возникающие в результате пробки. В обоих случаях требуется рассуждать о системе с неизвестным числом объектов. Когда мы анализируем поток машин, находящихся на магистрали в данный момент, мы не знаем, сколько их, и не можем получить точную информацию на этот счет от видеокамер. Иногда на изображении несколько машин сливаются в
Глава 6. Использование коллекций Scala и Figaro для построения... одну, а какие-то машины и вовсе остаются незамеченными. А уж при прогнозировании пробок мы совсем не знаем, сколько будет машин. Ситуация, когда число объектов неизвестно, называется открытой вселенной (объяснение названия см. на врезке ниже) и характеризуется двумя свойствами. • Точное число объектов, например транспортных средств, неизвестно. Это называется неопределенностью количества. • Нет уверенности в том, что два объекта совпадают. Например, мы не знаем, запечатлена ли на двух изображениях одна и та же машина. Это называется неопределенностью тождества. В этом разделе мы сосредоточимся на неопределенности количества, для разрешения этой проблемы предназначены массивы переменной длины. Figaro позволяет справиться и с неопределенностью тождества, но тут вам придется потерпеть до следующей главы. Моделирование открытой вселенной Термин открытая вселенная (open universe) ввел в вероятностное программирование Стюарт Рассел {Stuart Russell) со своей группой при работе над языком BLOG, который предназначен в первую очередь для представления ситуаций с открытой вселенной и рассуждения о них. Истоки термина следует искать в математической логике, где замкнутый мир или замкнутая вселенная означает, что существуют лишь объекты, явно упомянутые в модели или выводимые из нее. Читатели, знакомые с языком программирования Prolog, знают, что в нем применяется принцип отрицания как неудачи: если невозможно доказать, что некоторое утверждение является истинным, то оно предполагается ложным. Например, если вы пытаетесь доказать, что на изображении имеется зеленая машина, но не можете это сделать, опираясь на имеющуюся информацию о машинах, то предполагаете, что такой машины не существует. Не требуется доказывать, что не существует никакой другой машины, о которой вы просто не знаете. Отрицание как неудача - вид рассуждений в замкнутой вселенной. Напротив, логика первого порядка является открытой вселенной. Чтобы доказать, что на изображении нет зеленой машины, необходимо доказать, что ее не может быть, даже если вы о ней не знаете. Моделирование открытой вселенной - вещь, которую можно сделать с помощью вероятностных языков программирования, но трудно в других системах вероятностных рассуждений. 6,3.2. Массивы переменной длины В Figaro для моделирования открытой вселенной применяются массивы переменной длины. Конструктор такого массива принимает два аргумента: элемент, значение которого равно размеру массива, и генератор элементов, аналогичный генератору элементов для массива фиксированной длины. На рис. 6.6 показан пример конструирования массива переменной длины. Первым аргументом конструктора VariableSizeArray ЯВЛЯеТСЯ Элемент ТИПа Integer, ПреДСТаВЛЯЮЩИЙ КОЛИЧеСТВО элементов в массиве. Второй аргумент - генератор элементов, т. е. функция, полу- 216
6.3. Моделирование ситуаций с неизвестным числом объектов ни НЕ чающая индекс массива и возвращающая определение элемента. На рис. 6.6 массив СОСТОИТ ЦеЛИКОМ ИЗ ЭЛемеНТОВ Beta, Значения КОТОРЫХ ИМеЮТ ТИП Double. Что происходит при конструировании массива переменной длины? Технически массив переменной длины и не массив вовсе, а случайная переменная, значением которой может быть один из многих массивов различной длины, номер которого ЗавИСИТ ОТ Значения аргумента number. При ВЫЗОВе КОНСТруКТОра VariableSizeAr- гау создается элемент MakeArray, представляющий эту случайную переменную. Все операции над массивом переменной длины применяются к этому элементу. Под капотом Figaro реализует MakeArray максимально эффективно, так чтобы массивы различного размера разделяли как можно больше данных. В частности, короткие массивы являются префиксами более длинных и включают одни и те же элементы. От программиста все это скрыто, а говорю я об этом только для того, чтобы вы знали об элементе MakeArray, потому что можете встретить упоминание этого типа. Элемент, представляющий количество элементов в массиве Генератор элементов VariableSizeArray(Binomial(20, 0.3), i => Beta(l, i + 1)) ; \ Индекс в Определение массиве элемента Рис. 6.6. Устройство конструктора массива переменной длины 6.3.3. Операции над массивами переменной длины Что можно делать с массивами переменной длины? В общем, то же самое, что и с массивами фиксированной длины, но есть несколько важных различий. В примерах ниже vsa обозначает массив переменной длины, состоящий из элементов типа string. Вот некоторые операции над массивами переменной длины. • Получение элемента в позиции с указанным индексом. В принципе, vsa (5) возвращает элемент с индексом 5 (шестой элемент массива). Но тут есть опасность. В массиве должно быть не менее шести элементов, иначе вызов ЭТОГО Метода ЗаКОНЧИТСЯ Исключением IndexOutOfRangeException. • Безопасное получение элемента в позиции с указанным индексом. Во избежание описанной выше проблемы Figaro предоставляет безопасный способ получить элемент с указанным индексом. Вызов vsa. get (5) возвращает Element [Option [String] ]. Это ТИП Scala, Совпадающий С Some [String], еСЛИ строка имеется, и None, если строки нет. Выполнение vsa.get (5) представляет следующий случайный процесс:
Глава 6. Использование коллекций Scala и Figaro для построения... - выбрать элементы в количестве, определяемом аргументом number; - получить массив фиксированной длины соответствующего размера; - если в массиве есть хотя бы шесть элементов, положить s равным значению элемента с индексом 5 и вернуть some (s); - Иначе Вернуть None. Как видим, в обоих случаях случайный процесс возвращает элемент типа Option [String], а ЗНаЧИТ, Представляет ТИП Element [Option [String] ]. ЭТОТ элемент можно использовать по-разному, например, передать его элементу Apply, который делает что-то интересное, если аргумент имеет тип some (s), и порождает значение по умолчанию, если аргумент равен None. • Отображение массива переменной длины на значения с помощью какой-то функции. То же самое, что для массива фиксированной длины, только теперь Figaro сначала заглядывает внутрь массива переменной длины, находит массив фиксированной длины, на который тот указывает, а затем заглядывает внутрь этого массива и получает значения элементов. Например, вызов vsa .map (.length) порождает новый массив переменной длины, в котором вместо каждой строки находится ее длина. • Подать массив переменной длины на вход цепной функции, которая отображает значения на элементы. И эта операция аналогична операции над массивом фиксированной длины, но Figaro сначала заглядывает внутрь массива переменной длины, находит массив фиксированной длины и подает каждый из его элементов на вход цепной функции. Так, vsa.map( (s: String) => discrete .Uniform (s :_*)) СОЗДает маССИВ переменной ДЛИНЫ, в котором каждый элемент содержит случайный символ, взятый из соответствующий строки. В этом примере s:_* преобразует строку в последовательность СИМВОЛОВ, a discrete. Uniform (s :_* ) Выбирает Случайный Член этой последовательности. • Свертки и агрегаты. Все свертки и агрегаты, имеющиеся для массивов фиксированной длины, доступны и для массивов переменной длины. Например, vsa.count (_. length > 2) ВОЗВращаеТ Элемент Element [Int], Пред- ставляющий число строк длины больше 2 в массиве. Интерпретировать этот вызов можно как случайный выбор массива фиксированной длины в соответствии с аргументом number и последующий подсчет числа строк в это массиве, длина которых больше 2. В документации Scaladoc описаны все имеющиеся свертки и агрегаты. 6.3.4. Пример: прогнозирование продаж неизвестного числа новых продуктов В этом примере предполагается, что мы планируем финансирование отдела исследований и разработок на будущий год. Чем больше финансирование, тем больше будет разработано новых продуктов, что повысит продажи. Однако, принимая решение об уровне финансирования, мы еще не знаем точно, сколько новых продуктов будет разработано при таком уровне. 218
6.3. Моделирование ситуаций с неизвестным числом объектов Поэтому для представления новых продуктов используется массив переменной длины. Модель определяется следующим образом: 1. Она принимает аргумент rNDLevei типа Double, представляющий уровень финансирования исследований и разработок. 2. Элемент numNewProducts, представляющий число новых разработанных продуктов, - это элемент типа integer, определяемый элементом Geometric (rNDLevei). Элемент Geometric характеризует процесс, СОСТОЯЩИЙ из нескольких шагов. После каждого шага процесс может завершиться или перейти к следующему шагу. Вероятность перехода к следующему шагу определяется параметром элемента (а данном случае rNDLevei). Значением процесса является число шагов, выполненных до завершения. Вероятность числа шагов убывает в геометрической прогрессии со знаменателем rNDLevei. Чем больше rNDLevei, тем дольше будет продолжаться процесс и тем больше будет разработано новых продуктов. 3. Мы создаем массив переменной длины productQuaiity, представляющий качество вновь созданных продуктов. Аргумент number конструктора этого массива равен numNewProducts, а генератор элементов - функция, сопоставляющая индексу i элемент Beta a, i + 1). В разделе 6.1.1 мы говорили, что математическое ожидание Beta(1, i + d равно i / ц + 2), так что чем больше новых продуктов разрабатывается, тем ниже их качество, т. е. эффективность инвестиций падает. 4. Затем мы преобразуем качество продуктов в прогноз объема продаж каждого продукта. Это делается в два шага. Сначала генерируется объем продаж в виде элемента Normal со средним, равным качеству продукта. Но элемент Normal может принимать отрицательные значения, а отрицательные продажи - вещь невозможная, поэтому на втором шаге мы отсекаем все отрицательные значения, устанавливая нулевую нижнюю границу. Это делается Комбинированием МеТОДОВ chain И тар Класса VariableSizeArray. 5. Наконец, мы вычисляем общий объем продаж, применяя функцию sum к маССИВу Переменной ДЛИНЫ productSales. Ниже приведен полный код модели: val numNewProducts = Geometric(rNDLevei) val productQuaiity = VariableSizeArray(numNewProducts, i => Beta(l, i + 1)) val productSalesRaw = productQuaiity.chain(Normal(_, 0.5)) val productSales = productSalesRaw.map(_.max(0)) val totalSales = productSales.foldLeft(0.0)(_ + _) В этом примере вероятностное программирование позволило реализовать весь содержательный процесс с помощью всего нескольких строк кода. Далее мы рассмотрим фундаментальные концепции, лежащие в основе коллекций Figaro, и увидим, как они применяются к бесконечным коллекциям. 219
Глава 6. Использование коллекций Scala и Figaro для построения... 6.4. Работа с бесконечными процессами В этом разделе показано, как работать с коллекциями Figaro, определенными над бесконечным пространством. Сразу возникает вопрос: как можно определить коллекцию с бесконечным числом элементов в памяти конечного объема? И как произвести обработку такой коллекции за конечное время? Ответ - элементы коллекции определяются неявно. Мы никогда не обращаемся к бесконечно большому числу элементов. Но любой элемент доступен, коль скоро в нем возникает необходимость. Предупреждение. Материал в этом разделе довольно сложный. Можете пропустить его при первом чтении, нигде в книге он не понадобится. Но возвращайтесь, когда захотите глубже понять коллекции Figaro и оценить всю их мощь. Характеристика (trait) Process является в Figaro общим представлением любой коллекции, конечной или бесконечной. Далее объясняется, как процесс позволяет неявно представить элементы и как работать с процессом. После этого мы рассмотрим пример процесса, определенного для моментов времени. 6.4.1. Характеристика Process В Figaro процесс - это неявное представление коллекции элементов, определенных над множеством индексов. Говоря «неявно», я имею в виду, что по любому индексу можно получить соответствующий элемент. Для массивов это определение естественно: зная индекс из некоторого диапазона, мы можем получить элемент в позиции с этим индексом. Но в процессах Figaro тип индекса может быть любым. Таким образом, характеристика Process параметризуется двумя типами: типом индекса и типом значения, который представляет типы значений элементов процесса. ОСНОВНОЙ МеТОД, КОТОрЫЙ Необходимо реализовать ДЛЯ Process [Index, Value], - ЭТО generate. В простейшей форме generate принимает ИНДеКС ТИПа Index И ВОЗВра- щает Element [value]. Будучи вызван, этот метод должен создать и вернуть элемент, соответствующий индексу. При работе с процессом не следует вызывать метод generate напрямую. Выше мы видели, что если р - массив фиксированного размера, то р (5) получает элемент р в позиции с индексом 5. Вызывать generate нет необходимости. На самом деле, нотация р(5) даже безопаснее, чем явный вызов generate. Конструкция р(5) приводит к вызову generate для порождения элемента, который затем кэшируется, и таким образом мы будем получать тот же самый элемент при повторном вызове. Если же вызывать generate напрямую, то при каждом вызове с одним и те же индексом будет возвращаться новый элемент, хотя может статься, что это нежелательно. Примечание. В Scala р (5) - сокращенная запись для р. apply (5). У характеристики Process имеется метод apply, который вызывает generate и кэширует результат, что позволяет использовать нотацию р (5) для получения нужного элемента. 220
6.4. Работа с бесконечными процессами И снова о контейнерах В разделе 6.2 было сказано, что Container - это обобщенный суперкласс, представляющий конечные коллекции в Figaro. Теперь, когда мы знаем о еще более общей характеристике Process, самое время вернуться к классу Container, Класс Container - это процесс, предоставляющий конкретную конечную последовательность индексов. Индексы, принадлежащие этой последовательности, и только они считаются допустимыми. В классе Containe r, как в любом процессе, имеются методы generate для получения одного или нескольких элементов. Поскольку число элементов в контейнере конечно, мы можем определить свертки и агрегаты. Операция свертки требует перебора всех элементов коллекции, а это можно сделать только для конечных коллекций. Поэтому характеристика Process не поддерживает ни сверток, ни агрегатов. Однако же она поддерживает операции тар и chain - как и Container. FixedSizeArray является подклассом Container, специализированным в двух отношениях. Во-первых, допустимыми индексами являются целые числа отО до размера массива минус 1. Во-вторых, предполагается, что элементы FixedSizeArray независимы. Неважно генерируем мы один элемент или сразу несколько, каждый из них создается генератором элементов, и никакие дополнительные элементы для кодирования зависимостей не генерируются. Другая форма generate позволяет получать элементы, указав сразу несколько индексов. Эта специальная возможность иллюстрирует одно из различий между коллекциями Figaro и обычными коллекциями. В процессе Figaro могут существовать зависимости между элементами с разными индексами. Предположим, к примеру, что процесс представляет количество осадков, выпавших в разных точках региона. Очевидно, что элементы, представляющие осадки в близких точках, должны быть зависимы. Если брать элементы по отдельности, то мы не сможем уловить эти зависимости, но если выбрать сразу все элементы для интересующих нас точек, то зависимости удастся построить. Вторая форма generate принимает список индексов и возвращает отображение Map [Index, Element [Value] ] ИНДеКСОВ На элементы. В ЭТОМ отображении ДОЛЖеН присутствовать элемент для каждого переданного индекса. За кулисами generate порождает также элементы, представляющие зависимости между элементами, соответствующими индексам, но в отображение они не включаются. Например, если имеется Процесс, Содержащий КОЛИЧеСТВО ОСаДКОВ В раЗНЫХ ТОЧКаХ, ТО ВЫЗОВ generate CO списком этих точек породит элементы, представляющие количество осадков, а также элементы, описывающие зависимости между количеством осадков в разных точках. Как это работает, мы скоро увидим. У характеристики Process имеется еще один метод, который необходимо реализовать. Не каждое значение типа index является допустимым индексом процесса, т. е. не с каждым значением ассоциирован элемент. Например, в случае массива допустимы только целые индексы от 0 до размера массива минус 1. Метод Process под названием rangecheck принимает в качестве аргумента индекс и возвращает булево значение, которое равно true, если индекс допустим. Когда мы вызываем для получения элемента 221
Глава 6. Использование коллекций Scala и Figaro для построения... метод процесса apply, сначала проверяется, принадлежит ли индекс допустимому диапазону; еСЛИ нет, возбуждается ИСКЛЮЧение IndexOutOfRangeException. 6.4.2. Пример: моделирование состояния здоровья во времени И напоследок мы рассмотрим темпоральный процесс, который представляет значение, изменяющееся с течением времени. Это моделирование состояния здоровья пациента. К моделированию темпоральных процессов есть два подхода. Первый - задать дискретные моменты через одинаковые интервалы (например, каждую минуту) и определить случайную переменную, представляющую состояние здоровья в каждый из этих моментов. Второй - считать время непрерывным, а переменную состояния здоровья - определенной в каждый момент времени. Это позволяет получать значение переменной в любой интересующий нас момент. Для иллюстрации беконечных процессов в Figaro я выберу второй подход. Определим класс HeaithProcess, расширяющий Process, для которого индекса- ми являются числа двойной точности, представляющие моменты времени, а элементы имеют тип Boolean и говорят, здоров ли пациент в данный момент. Вот как ВЫГЛЯДИТ объявление HeaithProcess! object HeaithProcess extends Process[Double, Boolean] Мы должны реализовать три метода: (1) вариант generate, порождающий элемент для одного момента времени; (2) вариант generate, порождающий элементы ДЛЯ НеСКОЛЬКИХ МОМеНТОВ, а Также ЗаВИСИМОСТИ Между НИМИ; (3) rangeCheck. Начнем с самого простого - метода rangeCheck. Предположим, что у процесса существует начальный момент 0, и допустимым считается любой момент, больший или равный 0. Тогда rangeCheck определяется следующим образом: def rangeCheck(time: Double) = time >= 0 Теперь реализуем метод generate, который порождает состояние здоровья пациента в один момент времени. Если рассматривать один момент изолированно от всех остальных, то состояние здоровья определяется простым элементом Flip. Предположим, однако, что мы не знаем ассоциированную с Flip вероятность и хотим вывести ее из обучающих данных. Можно ввести параметр heaithyPrior для представления этой вероятности. Переменная heaithyPrior и одиночный вариант метода generate определены ниже: val heaithyPrior = Uniform((0.05 to 0.95 by 0.1):_*) def generate(time: Double): Element[Boolean] = Flip(heaithyPrior) В качестве функциональной формы heaithyPrior мы взяли дискретный эле- мент uniform, выбирающий одно из значений 0.05, 0.15,..., 0.95. Переходим к самому интересному. Мы хотим реализовать функцию generate, которая умеет порождать элементы для нескольких моментов времени, а также 222
6.4. Работа с бесконечными процессами зависимости между этими элементами. Как моделировать зависимости между моментами времени? Естественно считать, что в близких точках высока вероятность одинаковых значений. Истинность этого утверждения зависит от близости точек. Будем предполагать, что влияние одной точки на временной оси на другую экспоненциально убывает с ростом расстояния между точками. Заведем параметр heaithChangeRate для описания скорости изменения состояния здоровья со временем и выведем его значение из обучающих данных. Таким образом, нам предстоит создать два набора элементов. Первый - это элементы, соответствующие моментам времени, они создаются одиночным методом generate. Второй - зависимости между парами соседних моментов. Мы перебираем моменты времени по порядку и для каждой пары создаем элемент, кодирующий тот факт, что в соседних точках вероятность одинакового состояния здоровья больше, чем различного. Сила этого ограничения зависит от расстояния между ТОЧКамИ И регулируется параметром heaithChangeRate! def generate(times: List[Double]): Map[Double, Element[Boolean]] = { val sortedTimes = times.sorted <)—О val healthy = sortedTimes.map(time => (time, generate(time))).toMap <]—© def makePairs(remaining: List[Double]) { if (remaining.length >= 2) { val timel :: time2 :: rest = remaining <ь-© «-O val probChange = Apply(heaithChangeRate, (d: Double) => 1 - math.exp(- (time2 - timel) / d)) val equalHealth = healthy(timel) === healthy(time2) val healthStatusChecker = If(equalHealth, Constant(true), Flip(probChange)) healthStatusChecker.observe(true) makePairs (time2 : : rest) O—© } ' } makePairs(sortedTimes) healthy } О — Сортируем моменты времени, чтобы можно было без опаски предполагать, что они упорядочены © — Порождаем отображение временных индексов на элементы, описывающие состояние здоровья © — Перебираем моменты времени по порядку, обрабатывая каждую пару соседних © — Создаем элемент, кодирующий зависимость между состоянием здоровья в соседние моменты времени Опишем, как создается элемент, кодирующий зависимость между состоянием здоровья в соседние моменты времени. Будем моделировать эту зависимость как 223
Глава 6. Использование коллекций Scala и Figaro для построения... косвенную. Напомним (см. главу 5), что в Figaro есть два способа представления косвенных зависимостей. Самый простой метод - ограничение. Но он не позволяет обучить параметры, характеризующие зависимость, а нам хотелось бы таким образом узнать значение параметра heaithchangeRate. Поэтому воспользуемся вторым методом - создадим булев элемент, который принимает значение true с вероятностью, зависящей от двух состояний здоровья, и зададим в качестве наблюдаемого факта, что этот элемент равен true. Вот как выглядят входные данные: • healthy (timei) - состояние здоровья в первый момент времени; • healthy (time2) - состояние здоровья во второй момент времени; • heaithchangeRate - СКОрОСТЬ Изменения СОСТОЯНИЯ ЗДОрОВЬЯ. В показанном выше коде создается элемент heaithstatuschecker, для которого наблюдается значение true. Если healthy (timei) и healthy (time2) равны, то heaithstatuschecker определенно равен true. Это то же самое, что сказать: когда healthy (timei) И healthy (time2) равны, Значение Ограничения равно 1. ЕСЛИ healthy (timei) И healthy (time2) не равны, ТО верОЯТНОСТЬ, ЧТО heaithstatuschecker принимает значение true, зависит от расстояния между time2 и timei. Интуитивно кажется, что чем больше расстояние между time2 и timei, тем вероятнее, что состояние здоровья изменилось. Согласно определению probchange, вероятность, что состояние здоровья не изменилось, экспоненциально затухает с увеличением раЗНОСТИ time2 - timei, ПОДеленНОЙ на heaithchangeRate. Следовательно, верОЯТ- ность, что состояние здоровья изменилось тем ближе к 1, чем больше разность во времени. 6.4.3. Использование процесса Итак, процесс мы определили. Теперь надо его использовать. Для этого предположим, что имеются данные, описывающие состояние здоровья в некоторые моменты времени. Мы хотим запросить состояние здоровья в какие-то другие моменты времени. Кроме того, мы хотим узнать значения параметров heaithyPrior и heaithchangeRate, полученные на ОСНОВе Данных. Самый важный шаг - одновременная генерация элементов для всех интересующих нас моментов времени - как заданных изначально, так и указываемых в запросе. Попутно будут созданы все необходимые зависимости. Если бы мы генерировали элементы для данных и запроса порознь, то не смогли бы получить зависимости между ними, а, стало быть, не сумели бы использовать имеющиеся данные для предсказания состояния здоровья в другие моменты. В этом примере для вывода применяется алгоритм исключения переменных. Это точный алгоритм, который хорошо работает для данной модели, поэтому наш выбор оптимален. Однако метод исключения переменных в Figaro работает только тогда, когда переменные могут принимать конечное число значений. Именно ПОЭТОМУ При Моделировании Параметров heaithyPrior И heaithchangeRate МЫ ВЗЯЛИ дискретное конечное множество значений. Ниже приведен код, который генерирует необходимые элементы, задает факты, выполняет вывод и получает ответы на запросы. 224
6.5. Резюме val data = Map(0.1 -> true, 0.25 -> true, 0.3 -> false, 0.31 -> false, 0.34 -> false, 0.36 -> false, 0.4 -> true, 0.5 -> true, 0.55 -> true) D val queries = List(0.35, 0.37, 0.45, 0.6) val targets = queries ::: data.keys.toList val healthy = generate(targets) H for { (time, value) <- data } healthy(time).observe(value) В val queryElements = queries.map(healthy(_)) val queryTargets = healthyPrior :: healthChangeRate val algorithm = VariableElimination(queryTargets:_*) algorithm.start() queryElements H for { query <- queries } { printIn("Вероятность, что пациент здоров в момент " + query + " = " + algorithm.probability(healthy(query), true)) } println("Ожидаемая априорная вероятность здоровья = " + algorithm.mean(healthyPrior)) println("Ожидаемая скорость изменения состояния здоровья = " + algorithm.mean(healthChangeRate)) algorithm.kill() О — data — отображение моментов времени на состояние здоровья © — Порождаем элементы для всех изначально заданных и интересующих моментов времени © — Задаем факты. Переменная healthy, возвращенная методом generate, содержит отображение моментов времени на элементы © — Выполняем вывод для целевых элементов. И снова для получения релевантных элементов используется отображение healthy Это пример несколько сложнее разобранных ранее в этой главе. Класс Fixed- si zeArray берет на себя заботу об определении метода rangecheck и обоих вариантов generate. Но теперь вы знаете, что делать, если нужен процесс или контейнер, отсутствующий в библиотеке. Вот мы и подошли к концу главы. Мы начали с относительно простых, но полезных паттернов проектирования и постепенно перешли к более развитым и многообещающим концепциям. В следующей главе мы узнаем, как с помощью Figaro создавать объектно-ориентированные вероятностные модели, которые расширят возможности применения моделей на практике. 6.5. Резюме • Как и в обычном программировании, коллекции позволяют организовать множество объектов и применить к ним единообразный код. 225
Глава 6. Использование коллекций Scala и Figaro для построения... • Коллекции Scala предоставляют полезные и эффективные структуры для организации элементов Figaro, позволяя создавать модели с иерархическими или многомерными зависимостями. • Коллекции Figaro предоставляют дополнительные средства организации элементов, наделенные возможностью заглядывать внутрь элементов и манипулировать их значениями. Тем самым мы можем реализовать такие операции, как свертывание и кванторы. • Кроме того, коллекции Figaro позволяют моделировать открытую вселенную с неизвестным числом объектов. • Процессы Figaro дают возможность моделировать коллекции с бесконечным множеством индексов, например, временные или пространственные. 6.6. Упражнения Решения избранных упражнений имеются на сайте www.manning.com/books/ practical-probabilistic-programming. 1. В вашем городе три школы. В каждой школе имеются классы от первого до шестого. В каждом классе 30 учеников. Каждый ученик сдает экзамен по математике. Оценка на экзамене зависит от способностей ученика и мастерства учителя. Напишите вероятностную программу для представления этой ситуации и выведите мастерство учителя из результатов экзамена. При написании программы используйте коллекции Scala. 2. Вы повели свое семейство в парк развлечений. В парке семь аттракционов, одни - захватывающие дух, другие - спокойные. Каждый аттракцион характеризуется качеством: высоким, средним или низким. Является ли аттракцион захватывающим дух или спокойным - наблюдаемый факт, чего нельзя сказать о качестве. У вас трое детей 16, И и 7 лет. Нравится аттракцион ребенку или нет, зависит от его возраста, качества аттракциона и его рискованности; дети постарше предпочитают захватывающие дух аттракционы, и все без исключения любят аттракционы высокого качества. Ваш 16-летний отпрыск уже бывал в парке развлечений прежде и сказал, какие аттракционы ему нравятся. Воспользовавшись коллекциями Scala, напишите программу, которая будет предсказывать, какие аттракционы понравятся детям 11 и 7 лет. 3. В гольфе у каждой лунки имеется пар, т. е. количество ударов, за которое игрок должен закатить мячик в лунку по регламенту. Обычно пар равен 3, 4 или 5. Определим квалификацию гольфиста s как математическое ожидание числа ударов, необходимых ему для прохождения лунки. Квалификация - это вещественное число, равномерно распределенное в диапазоне от 0 до 8/13. Точнее, вероятность, что понадобится заданное число ударов, определяется следующей таблицей. Пар-2 s/8 Пар- 1 s/2 Пар s Пар+1 4/5x(1-13s/8) Пар+ 2 1/5 х(1 -13S/8) 226
Упражнения Создайте структуру данных, описывающую поле для гольфа с 18 лунками разных паров. Напишите на Figaro программу, представляющую игрока в гольф. a. С помощью этой программы предскажите, с какой вероятностью игрок наберет больше 80 очков, если известна его квалификация. b. Ответьте на тот же запрос при условии, что квалификация не меньше 0.3. c. С помощью своей программы найдите вероятность, что квалификация игрока не меньше 0.3, если известно, что он набрал 80 очков. 4. В игре «Сапер» вы должны определить, в каких ячейках сетки находятся мины. Каждая ячейка может находиться в одном из четырех состояний: (1) в ней точно мина, (2) точно безопасна, (3) есть мина, но это еще неизвестно (4) безопасна, но это еще неизвестно. В каждой заведомо безопасной ячейке находится число, показывающее, сколько мин (известных и известных) находится в восьми соседних ячейках (по горизонтали, по вертикали и по диагонали). Напишите вероятностную программу, которая получает на входе сетку «Сапера» и для каждой ячейки с неизвестным состоянием предсказывает вероятность нахождения в ней мины. 5. Вы владелец кондитерского магазина и хотите спрогнозировать, сколько конфет продадите в один день. Предположим, что число детей, заглянувших в магазин в определенный день, подчиняется распределению Binomial(100, 0.1). Каждый ребенок покупает конфеты с вероятностью 0.5 и, если вообще покупает, то количество купленных конфет равномерно распределено в диапазоне от 1 до 10. Воспользовавшись массивом переменной длины, предскажите, сколько конфет вы продадите в день. 6. Вы снова владелец кондитерского магазина, но теперь дети заходят в магазин группами, и решение одного купить конфеты влияет на решения других. Точнее, если один ребенок покупает, то вероятность, что другой тоже купит, возрастает. Количество конфет, купленных одним ребенком (если он решил купить), неизменно. Предположим, что количество групп детей, заходящих каждый день в магазин, подчиняется распределению Binomial(40, 0.1) и что число детей в группе равномерно распределено в диапазоне от 1 до 5. Создайте процесс Figaro, описывающий логику этой ситуации, и с его помощью предскажите, сколько конфет будет продано за день. 227
ГЛАВА 7. Объектно-ориентированное вероятностное моделирование В этой главе. я Применение методов объектно-ориентированного (ОО) программирования для организации сложных вероятностных моделей. в Комбинирование ОО с идеями реляционных баз данных для создания гибких моделей, содержащих объекты и связи между ними. в Конструкции Figaro, поддерживающие ОО-мо- делирование, в том числе коллекции элементов и ссылки. в Использование конструкций Figaro для представления неопределенности знаний о типах объектов и связях. В предыдущей главе мы узнали о применении коллекций для структурирования вероятностных программ. В этой главе мы продолжим тему использования типичных языковых конструкций для создания вероятностных программ. В фокусе этой главы находится применение методов объектно-ориентированного программирования для представления вероятностных моделей. Объектная ориентация - это весьма эффективная общая техника программирования, которая, как мы убедимся, прекрасно сочетается с вероятностным моделированием, т. к. позволяет естественно описать ситуацию в терминах объектов и связей между ними. Язык Scala особенно хорошо подходит для вероятностного программирования, поскольку является одновременно функциональным и объектно-ориентированным. Это одна из основных причин, по которой именно Scala был выбран в качестве объемлющего языка для Figaro.
7.1. Объектно-ориентированные вероятностные модели III! УШ Мы начнем эту главы с изложения основных объектно-ориентированных концепций в применении к вероятностному программированию. Мы увидим, как с помощью классов, экземпляров, подклассов и наследования собирать программы из объектов, рассматриваемых как строительные блоки. В разделе 7.2 представлены реляционные вероятностные модели, в которых центральное место занимают связи между объектами. Начав работать со связями, мы будем естественно сталкиваться с ситуациями, когда неизвестно, какие связи существуют. Эта реляционная неопределенность является основной темой раздела 7.3. Вы узнаете о средствах Figaro, позволяющих представлять реляционную неопределенность и рассуждать о ней. Эти же средства можно использовать для моделирования неопределенности типа, когда неточно из