Source code for pyinference.inference

# coding=utf-8

# TODO шаблонное задание факторов
# TODO бинарные переменные
# TODO шаблонное добавление переменной (и фактора) в сеть

""" Модуль предоставляет возможность работы с логическим выводом.

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


Пример "Студенты"
+++++++++++++++++

.. note::
    Данный пример взят из курса "Probabilistic graphical models" Стэнфордского университета.

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

Для упрощения, положим что курсы однозначно делятся на сложные и простые (в реальной жизни
можно воспользоваться нечетким классификатором сложности курса, состоящим из двух термов, на дальнейшие рассуждения это
не повлияет), причем из всех возможных курсов 60% - простые, а 40% - сложные. Соответственно, вероятность того, что курс
окажется сложным - 0,4.

Также, положим, что студенты бывают умные (30%) и все остальные (70%).

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

Inputs

=========== =========== =================== ===========
Сложность   Интеллект   Оценка              Вероятность
=========== =========== =================== ===========
простой     низкий      отлично             0,3
простой     низкий      хорошо              0,4
простой     низкий      удовлетворительно   0,3
простой     высокий     отлично             0,9
простой     высокий     хорошо              0,08
простой     высокий     удовлетворительно   0,02
сложный     низкий      отлично             0,05
сложный     низкий      хорошо              0,25
сложный     низкий      удовлетворительно   0,7
сложный     высокий     отлично             0,5
сложный     высокий     хорошо              0,3
сложный     высокий     удовлетворительно   0,2
=========== =========== =================== ===========

Приступим к созданию наших переменных.

Импортируем модуль :mod:`numpy`, который нам понадобится для работы с распределениями

    >>> import numpy as np

Создаем переменные и соответствующие им факторы.

    >>> d = Variable(name='Difficulty', terms=['easy', 'hard'])
    >>> D = Factor(name='D', cons=[d])
    >>> D.cpd = np.array([0.6, 0.4])

    >>> i = Variable(name='Intelligence', terms=['low', 'high'])
    >>> I = Factor(name='I', cons=[i])
    >>> D.cpd = np.array([0.7, 0.3])

    >>> g = Variable(name='Grade', terms=['exellent', 'good', 'bad'])
    >>> G = Factor(name='G|I,D', cond=[i, d], cons=[g])
    >>> G.cpd = np.array([0.3, 0.4, 0.3,
    ...                   0.05, 0.25, 0.7,
    ...                   0.9, 0.08, 0.02,
    ...                   0.5, 0.3, 0.2]).reshape((2, 2, 3))

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

    >>> s = Variable(name='SAT', terms=['failed', 'passed'])
    >>> S = Factor(name='S|I', cond=[i], cons=[s])
    >>> S.cpd = np.array([[0.95, 0.05], [0.2, 0.8]])

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

    >>> l = Variable(name='Letter', terms=['denied', 'provided'])
    >>> L = Factor(name='L|G', cond=[g], cons=[l])
    >>> L.cpd = np.array([[0.1, 0.9], [0.4, 0.6], [0.99, 0.1]])

Создаем байесовскую сеть, куда включам все созданныем нами факторы:

    >>> BN = Net(name='student', nodes=[D, I, G, S, L])

Таким образом, наша сеть соответствует графу::

    digraph student{
        D -> G;
        I -> G -> L;
        I -> S
    }

Вычисляем и сохраняем запросы к данной сети.

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

    >>> q1 = BN.query(query=[i], evidence=[g, d])
    >>> print q1.cpd
    [[[ 0.85714286  0.14285714]
      [ 0.61538462  0.38461538]
      [ 0.3         0.7       ]]
    <BLANKLINE>
     [[ 0.64285714  0.35714286]
      [ 0.21052632  0.78947368]
      [ 0.09090909  0.90909091]]]

Какова вероятность того, что студент умный, если мы знаем только сложность курса:

    >>> q2 = BN.query(query=[i], evidence=[d])
    >>> print q2.cpd
    [[ 0.49138756  0.50861244]
     [ 0.4959897   0.5040103 ]]

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

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

    >>> q3 = BN.query(query=[i], evidence=[g])
    >>> print q3.cpd
    [[ 0.72180451  0.27819549]
     [ 0.53427065  0.46572935]
     [ 0.28198433  0.71801567]]

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

    >>> q4 = BN.query(query=[l], evidence=[i])
    >>> print q4.cpd
    [[ 0.37612807  0.62387193]
     [ 0.6374464   0.3625536 ]]


Пример "Диагностика"
++++++++++++++++++++

.. note::
    Данный пример взят из курса "Machine learning" Стэнфордского университета.

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

Представим имеющиеся у нас данные в виде факторов. У нас есть две переменные (и соответствующие им факторы):

- наличие болезни:

=============== ===========
Наличие болезни Вероятность
=============== ===========
есть            0,001
нет             0,999
=============== ===========

- результаты теста:

=============== =============== ===========
Наличие болезни Результат теста Вероятность
=============== =============== ===========
есть            положительный   0,001
есть            отрицательный   0,999
нет             положительный   0,001
нет             отрицательный   0,999
=============== =============== ===========

Построим нашу Байесовскую сеть:

    >>> import numpy as np

    >>> BN = Net()

    >>> c = Variable(name='cander', terms=['no', 'yes'])
    >>> C = Factor(name='C', cons=[c])
    >>> C.cpd = np.array([0.999, 0.001])
    >>> BN.add_node(C)

    >>> t = Variable(name='test', terms=['pos', 'neg'])
    >>> T = Factor(name='T|C', cons=[t], cond=[c])
    >>> T.cpd = np.array([[0.2, 0.8], [0.9, 0.1]])
    >>> BN.add_node(T)

Выполним запрос к сети:

    >>> q1 = BN.query(query=[c], evidence=[t])
    >>> print q1.cpd
    [[  9.95515695e-01   4.48430493e-03]
     [  9.99874891e-01   1.25109471e-04]]

Выведем отдельно только интересующую нас вероятность

    >>> "%0.4f" % q1.cpd[0,1]
    '0.0045'

Как мы видим, несмотря на положительный результат теста, вероятность наличия этой болезни выросла с 0,1% до всего 4,5%.
Несмотря на парадоксальность, это абсолютно верный результат при таких исходных данных. Надежность теста
компенсируется редкостью болезни.

Детерминистический вывод
++++++++++++++++++++++++

Для иллюстрации детерминистического вывода создадим сеть, соответствующую операции конъюнкции алгебры логики.
У нас есть три бинарные переменные (A, B и C), связанные соотношением C = A & B. Напомним таблицу истинности для
конъюнкции

= = =
A B C
= = =
0 0 0
0 1 0
1 0 0
1 1 1
= = =

Эта таблица может быть представлена как фактор P(C|A,B):

= = = ===
A B C P
= = = ===
0 0 0 1.0
0 0 1 0.0
0 1 0 1.0
0 1 1 0.0
1 0 0 1.0
1 0 1 0.0
1 1 0 0.0
1 1 1 1.0
= = = ===

Так как распределение значений переменных A и B нам неизвестно, оставим значения по умолчанию (равномерное
распределение):

    >>> import numpy as np

    >>> BN = Net()

    >>> a = Variable(name='a', terms=[0, 1])
    >>> A = Factor(name='A', cons=[a])
    >>> BN.add_node(A)

    >>> b = Variable(name='b', terms=[0, 1])
    >>> B = Factor(name='B', cons=[b])
    >>> BN.add_node(B)

    >>> c = Variable(name='c', terms=[0, 1])
    >>> C = Factor(name='C', cons=[c], cond=[a, b])
    >>> C.cpd = np.array([1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0]).reshape(C.shape)
    >>> BN.add_node(C)

    >>> q1 = BN.query(query=[a], evidence=[c, b])
    >>> q1
    Conditional:
    [a]|[b, c]
    [b, c, a]
    (2, 2, 2), (2, 2, 2)
    [0, 0, 0]    0.5
    [0, 0, 1]    0.5
    [0, 1, 0]    0.0
    [0, 1, 1]    0.0
    [1, 0, 0]    1.0
    [1, 0, 1]    0.0
    [1, 1, 0]    0.0
    [1, 1, 1]    1.0
    Sum: 3.0
    <BLANKLINE>

Классы модуля
+++++++++++++

"""

from pyinference.fuzzy import set as fuzzy_set
import numpy as np


[docs]class Variable(object): """ Класс реализует переменную Переменная представляет собой некий параметр, могущий иметь определенный набор значений. Набор значений переменной может задаваться двумя способами. Во-первых, можно задать списком. В таком случае, этот список следует передать как аргумент terms конструктора. Во-вторых, с переменной может быть ассоциирован нечеткий классификатор (см. :class:`pyinference.fuzzy.set.FuzzySet`). Тогда передать коструктору следует его. Переменная в байесовском моделировании – некая сущность, обладающая именем и областью определения. Обычно, рассматриваются переменные двух типов: дискретные и непрерывные. Дискретные переменные принимают значения из некоторого конечного множества X, а непрерывные – определены на некотором подмножестве множества действительных чисел. В общем случае, переменная определяется упорядоченной парой V=(N, X), где N – имя переменной, а X – множество возможных значений. Примером случайной переменной может быть результат подбрасывания монеты в таком случае областью ее определения будет множество {“орел”, “решка”}. В общем случае, конкретные значения переменной не имеют синтаксического значения (имеют место только семантически, то есть по смыслу) и их обычно заменяют соответствующим по числу элементов подмножество множества натуральных числе. То есть в нашем примере, можно обозначить значение «решка» за 0, а «орел» - за 1, и тогда область определения переменной будет {0, 1}. Построим данную переменную с использованием класса Variable:: coin = Variable(name="coin", terms=["орел", "решка"]) Частным и довольно распространенным случаем дискретной переменной является переменная, способная принимать только два значения. Такая переменная называется бинарной. В примере выше – результат подбрасывания монеты – бинарная переменная. Наиболее важным производным свойством дискретной переменной является мощность переменной – количество значений, которые она может принимать. Мощность бинарной переменной равна двум. Синтаксис: >>> from pyinference import inference >>> a = inference.Variable(name='a', terms=[0, 1]) >>> a.value = 'low' >>> from pyinference.fuzzy import set as fuzzy_set >>> fs = fuzzy_set.Partition(peaks=[0.0, 0.5, 1.0]) >>> b = inference.Variable(name='B', terms=fs) Поля класса: card (`int`): мощность переменной (количество значений) classifier(`dict` or :class:`pyinference.fuzzy.set.FuzzySet`): связанный с переменной классификатор name (`str`): имя переменной terms (`list`): список значений переменной (терм-множество) value (`object`): текущее значение переемнной Именованные параметры: name (`str`): имя переменной terms (`list` or `dict` or :class:`pyinference.fuzzy.set.FuzzySet`): набор значений переменной Исключения: `TypeError`: ошибка возникает, если агрумент terms имеет неподдерживаемый тип. """ def __init__(self, name='', terms=None): self.terms = [] self.classifier = {} if isinstance(terms, list): self.terms = terms self.classifier = {} elif isinstance(terms, fuzzy_set.FuzzySet): self.terms = terms.sets self.classifier = terms elif isinstance(terms, dict): self.terms = terms.keys() self.classifier = terms else: raise TypeError self.card = len(self.terms) self.value = None self.name = name
[docs] def equals(self, value): """Проверка переменной на равенство значению. Данный метод принимает некое значение и вычисляет меру сходства его со значением атрибута `value`. Синтаксис: >>> from pyinference import inference >>> a = inference.Variable(name='a', terms=[0, 1]) >>> a.value = 'low' >>> '%.2f' % a.equals('low') '1.00' >>> '%.2f' % a.equals('high') '0.00' >>> from pyinference.fuzzy import set as fuzzy_set >>> from pyinference import inference >>> fs = fuzzy_set.Partition(peaks=[0.0, 0.5, 1.0]) >>> b = inference.Variable(name='B', terms=fs) >>> b.value = 0.5 >>> '%.2f' % b.equals(0.5) '1.00' >>> b.value = '1' >>> '%.2f' % b.equals(0.25) '0.50' Параметры: value (`object`): Значение переменной. Может быть как элементом терм-множества занчений, явно перечисленного в атрибуте `terms` класса, так и элементом области определения связанного с этой переменной классификатора. Возвращает: Действительное число x, где -1.0 <= x <= 1.0, характеризующую меру сходства переданного и текущего значений переменной. Исключения: `AttributeError`: если текущее значение переменной не было задано до вызова данного метода. """ if self.value is None: raise AttributeError if self.value in self.classifier: val1 = self.classifier[self.value] else: val1 = self.value if value in self.classifier: val2 = self.classifier[value] else: val2 = value return float(val1 == val2)
def __repr__(self): """ Краткое текстовое представление перееменной. Синтаксис: >>> from pyinference import inference >>> a = inference.Variable(name='A', terms=[0, 1]) >>> print a A Возвращает: Возвращает имя переменной """ return self.name
def _itershape(tup): """ Итерирование по кортежу. """ r = np.array(tup).prod() res = [] for i in range(r): row = [] index = i + 0 for j in range(len(tup)): row.insert(0, index % tup[-j - 1]) index = (index - (index % tup[-j - 1])) / tup[-j - 1] res.append(row) return res
[docs]class Factor(object): """ Фактор логического вывода. Фактор связывает несколько переменных в совместное распределение вероятности. Фактор это некая функция, которая ставит в соответствие каждому возможному набору значений некого множества переменных действительное число (значение фактора). Набор соответствующих значений A некоторого множества переменных V называется назначением (assignment), если: - мощность множества A равна мощности множества V; - каждый элемент множества A является элементом множества значений соответствующего элемента множества V Пример: допустим, M – бинарная переменная M=(“монета”, {“орел”, “решка”}), а V – набор переменных V={M}3={M, M, M}, тогда А={“орел”, “орел”, “решка”} – корректное назначение, а А1={“решка”, “решка”} – некорректное назначение, так как не указано значение третьей переменной. А2={“решка”, “решка”, “ребро”} – также не является корректным назначением, так как третий элемент («ребро») отсутствует в области определения третьей переменной M. Однако, назначение А2 является корректным для множества V’={M, M, M’}, где M’=(“другая монета”, {“орел”, “решка”, “ребро”}), так как теперь третья переменная во множестве V’ уже другая и она содержит значение «ребро» в своей области определения. По тем же причинам, назначение А2 является корректным для набора V’’={M’, M’, M’}. Таким образом, фактор определяется упорядоченной тройкой F=(N, V, X), где N – имя фактора, V – область определения фактора (набор переменных, на которых определен фактор), X – множество значений фактора соответствующей мощности. Фактор определен на некотором упорядоченном множестве переменных, называемом областью определения (scope) фактора. Область определения обозначается так: F(A,B,C) или так: scope(F)={A,B,C}, где F – фактор, A, B и C – переменные, входящие в фактор. Заметим, что определение фактора не накладывает никаких ограничений ни на отдельные значения, ни на сумму этих значений, несмотря на то, что теория вероятности предусматривает такие ограничения. Фактор – это базовый строительный блок в вероятностных графических моделях. и представление вероятностей – всего лишь одно из возможных применений факторов. Например, определим фактор на одной переменной M, следующим образом:: coin_var = Variable(name="coin", terms=["орел", "решка"]) coin_factor = Factor(name="coin", cons=[coin_var]) import numpy as np coin_factor.cpd = np.array([0.5, 0.5]) .. note:: Заметим, что по умолчанию, распределение фактора (атрибут `cpd`) принимает значения, соответствующие равномерному распределению. Так что последние дву строчки в данном примере можно было бы опустить. Факторы удобны для использования в вероятностных графических моделях тем, что на них определены некоторые операции, часто выполняемые в процессе логического вывода, в довольно общей форме, что позволяет использовать их как элементарные объекты для построения логических сетей. Также, факторы могут представлять условные вероятности. Рассмотрим пример из медицинской диагностики. Определим бинарную переменную с=(«болезнь», {«нет», «есть»}) и бинарную переменную t=(«тест», {«положительный», «отрицательный»}). Переменная с показывает, есть ли у пациента определенное заболевание. а переменная t – результат диагностического теста. Допустим, что априорная вероятность данного заболевания равна 1%. Сконструируем фактор С=(«P(c)», {c}, {0.99, 0.01}). Очевидно, что вероятность получения определенного результата теста зависит от того, есть ли данное заболевание у пациента, или нет, то есть мы предполагаем известной вероятность P(t|c). Допустим, что вероятность получения положительного результата теста равна 90%. Соответственно, вероятность ложноотрицательного результата равна 0,1. Вероятность получить отрицательный результат при отсутствии заболевания равна 0,8, а вероятность ложноположительного результата – 0,2. Создадим фактор, представляющий данную условную вероятность:: c = Variable(name='C', terms=['no', 'yes']) t = Variable(name='T', terms=['pos', 'neg']) C = Factor(name='C', cons=[c]) C.cpd = np.array([0.99, 0.01]) T = Factor(name='T|C', cons=[t], cond=[c]) T.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) Синтаксис: Создадим два фактора, представляющих распределение двух переменных: P(A) и P(B|A). Первая переменная имеет безусловное распределение, значение второй зависит от значения первой. Подготовка переменных (могут быть как дискретные, так и сос связанным классификатором): >>> from pyinference.fuzzy import set as fuzzy_set >>> a = Variable(name='A', terms=['low', 'high']) >>> fs = fuzzy_set.Partition(peaks=[0.0, 0.5, 1.0]) >>> b = Variable(name='B', terms=fs) Создание факторов: >>> A = Factor(name='A', cons=[a]) >>> B = Factor(name='B|A', cons=[b], cond=[a]) Поля класса: name (`str`): имя фактора cond (`list`): список условных переменных cons (`list`): список подусловных переменных vars (`list`): список всех переменных фактора (объединение предыдущих двух) shape (`tuple`): кортеж мощностей всех переменных фактора (сохраняя порядок атрибута `vars`). Соответствует форме массива `cpd`. cpd (:class:`numpy.array`): массив, хранящий распределение условной вероятности фактора. Именованные параметры: name (`str`): имя фактора cons (`list`): массив подусловных переменных cond (`list`): массив условных переменных Исключения: AttributeError: ошибка возникает, если массив подусловных переменных (cons) пуст """ def __init__(self, name='', cond=None, cons=None): if len(cons) == 0: raise AttributeError self.name = name self.cond = sorted(cond) if cond else [] self.cons = sorted(cons) self.vars = self.cond + self.cons self.shape = tuple([var.card for var in self.vars]) self.cpd = np.ones(self.shape) self._normalize() def _normalize(self): n, m = len(self.cons), len(self.cond) s = self.cpd.sum(axis=tuple(range(m, n + m))).flatten() koef = np.array(self.shape)[m:].prod() if isinstance(s, float): s = np.array([s]) s = np.repeat(s, koef) s[s == 0.0] = 1.0 self.cpd = self.cpd.flatten() / s self.cpd = self.cpd.reshape(self.shape) def _map(self, other): res = [] for i in xrange(len(other.vars)): for j in xrange(len(self.vars)): if self.vars[j].name == other.vars[i].name: res.append(j) break return res
[docs] def marginal(self, var): """ Выполняет маргинализацию переменной из фактора. Маргинализация позволяет исключить переменную из области определения фактора, просуммировав соответствующие значения назначений маргинализируемого фактора. Маргинализация является основой алгоритма variable elimination: - F(A,B,C) - B = F(A,C) - F(A,C|B) - B = F(A,C) - F(A,B|C) - B = F(A|C) Может вызываться как метод (``m = f1.marginal(var)``) или как оператор "-" (``m = f1 - var``). Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> C = Factor(name='C', cons=[c]) >>> C.cpd = np.array([0.99, 0.01]) >>> T = Factor(name='T|C', cons=[t], cond=[c]) >>> T.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> m = T - c >>> m.cpd array([ 1.1, 0.9]) >>> len(m.vars) 1 Параметры: var (:class:`Variable` or `list`): маргинализуемая (исключаемая) переменная. Также в данный метод может передаваться список исключаемых переменных. Возвращает: Маргинализированный фактор Исключения: `TypeError`: ошибка возникает, когда второй операнд имеет неподдерживаемый тип. """ ind = -1 for i in xrange(len(self.vars)): if var.name == self.vars[i].name: ind = i if ind == -1: raise AttributeError cond1 = set(self.cond) cons1 = set(self.cons) cons2 = {var} cond = cond1 - cons2 cons = cons1 - cons2 res = Factor(name="Marginal", cons=sorted(list(cons)), cond=sorted(list(cond))) res.cpd = self.cpd.sum(axis=ind) return res
[docs] def product(self, other): """ Реализует произведение факторов. Произведение факторов объединяет области определения двух факторов. Значением для каждого назначения является произведение соответствующих назначений множителей: - F(A) * F(B) = F(A,B) - F(A,|B) * F(B) = F(A,B) - F(A,B|C) * F(D|C) = F(A,B,D|C) - F(A,B|C) * F(D,C) = F(A,B,C,D) - None * F(A) = F(A) Может вызываться как метод (``p = f1.product(f2)``) или как оператор "*" (``p = f1 * f2``). .. note:: Также первым операндом произведения может выступать None (``None * f1``). Тогда метод вернет второй операнд. Это сделано для удобства множественного произведения. Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> C = Factor(name='C', cons=[c]) >>> C.cpd = np.array([0.99, 0.01]) >>> T = Factor(name='T|C', cons=[t], cond=[c]) >>> T.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> p = T * C >>> "%0.3f" % p.cpd[0,0] '0.198' >>> len(p.vars) 2 Параметры: other (:class:`Factor`): фактор-множитель. Возвращает: Фактор-произведение двух исходных Исключения: `AttributeError`: ошибка возникает, когда исключаемая переменная не входит в исходный фактор. `TypeError`: ошибка возникает, когда второй операнд имеет неподдерживаемый тип. """ cond1 = set(self.cond) cons1 = set(self.cons) cond2 = set(other.cond) cons2 = set(other.cons) everything = cons1 | cons2 | cond1 | cond2 cond = cond1 & cond2 cons = everything - cond res = Factor(name="Product", cons=list(cons), cond=list(cond)) flat = res.cpd.flatten() ass = _itershape(res.shape) map1 = res._map(self) map2 = res._map(other) for i in range(len(flat)): ass1, ass2 = [], [] for j in map1: ass1.append(ass[i][j]) for j in map2: ass2.append(ass[i][j]) cpd1 = self.cpd[tuple(ass1)] cpd2 = other.cpd[tuple(ass2)] flat[i] = cpd1 * cpd2 res.cpd = flat.reshape(res.shape) return res
[docs] def divide(self, other): """ Реализует деление факторов. Деление факторов позволяет получить условное распределение из безусловного: F(A,B) / F(B) = F(A|B) Может вызываться как метод (``p = f1.divide(f2)``) или как оператор "/" (``p = f1 / f2``). Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> C = Factor(name='C', cons=[c]) >>> C.cpd = np.array([0.99, 0.01]) >>> T = Factor(name='T|C', cons=[t], cond=[c]) >>> T.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> p = (C * T) / C >>> "%0.3f" % p.cpd[0,0] '0.200' >>> len(p.vars) 2 Параметры: other (:class:`Factor`): фактор-делитель. Возвращает: Фактор-частное двух исходных Исключения: NotImplementedError: ошибка возникает, когда: - делитель имеет условные переменные; - некоторые подусловные переменные делителя отсутствуют среди подусловных переменных делимого TypeError: ошибка возникает, когда второй операнд имеет неподдерживаемый тип. """ cond1 = set(self.cond) cons1 = set(self.cons) cond2 = set(other.cond) cons2 = set(other.cons) if len(cond2) > 0: raise NotImplementedError if len(cons2 & cond1) > 0: raise NotImplementedError if len(cons2 & cons1) < len(cons2): raise NotImplementedError cond = cond1 | cons2 cons = cons1 - cons2 res = Factor(name="Conditional", cons=sorted(list(cons)), cond=sorted(list(cond))) flat = res.cpd.flatten() ass = _itershape(res.shape) map1 = res._map(self) map2 = res._map(other) for i in range(len(flat)): ass1, ass2 = [], [] for j in map1: ass1.append(ass[i][j]) for j in map2: ass2.append(ass[i][j]) cpd1 = self.cpd[tuple(ass1)] cpd2 = other.cpd[tuple(ass2)] flat[i] = cpd1 / cpd2 res.cpd = flat.reshape(res.shape) res._normalize() return res
def __mul__(self, other): if other is None: return self elif not isinstance(other, Factor): raise TypeError return self.product(other) def __rmul__(self, other): if other is None: return self elif isinstance(other, Factor): return self.product(other) else: raise NotImplementedError def __sub__(self, other): if isinstance(other, Variable): return self.marginal(other) elif isinstance(other, list): res = self for var in other: res = res - var return res else: raise TypeError def __div__(self, other): if not isinstance(other, Factor): raise TypeError return self.divide(other) def __repr__(self): """ Подробное текстовое представление фактора. Возвращает: строку, содержащую слудующую информацию о факторе: - имя фактора; - имена подусловных и условных переменных, разделенных символом "|" (порядок внутри групп может не соблюдаться); - список всех переменных фактора в соответствующем порядке; - распределение условной вероятности по векторам значений переменных в соответственном порядке; - сумму вектора распределения (должна быть равна 1.0 для безусловных распределений). """ res = '' res += self.name + ':\n' flat = self.cpd.flatten() ass = _itershape(self.shape) res += str(self.cons) + '|' + str(self.cond) + '\n' res += str(self.vars) + '\n' res += str(self.shape) + ', ' + str(self.cpd.shape) + '\n' for i in range(len(ass)): res += str(ass[i]) + ' ' + str(flat[i]) + '\n' res += 'Sum: ' + str(self.cpd.sum()) + '\n' return res # TODO вычисление фактора (алгоритм Мамдани)
class _Node(object): def __init__(self): self.parents = [] self.conditional = None self.uncond = None self.name = '' def __repr__(self): return self.name
[docs]class Net(object): """ Данный класс реализует смешанную сеть вывода. Основным назначением данного класса является выполнение запросов к сети. Сеть формируется как набор факторов, характеризующих совместное распределение значений некогорого набора переменных. Сети Байса традиционно представляются в виде графа, в котором вершины представляют переменные, входящие в сеть, а ребра – причинно следственные связи, причем ребро направлено от причины к следствию. Это очень наглядное представление является одним из главных достоинств вероятностных графических моделей и позволяет отобразить условную вероятность в виде взаимосвязей переменных и факторов, а также зачастую построить граф по экспертным или эмпирическим данным для моделирования распределения вероятностей. В графе наглядно видна иерархичность условной вероятности. Если некая переменная X зависит от переменной Y, то переменная Y будет среди родителей переменной X на графе. Запрос к байесовской сети выглядит следующим образом: каково распределение полной вероятности набора переменных Q, при условии, что набор переменных E принимает назначение q? Множество переменных Q называется запросом (query) или целевыми переменными и может состоять из одной и более переменных. Множество условий E называется наблюдения (evidence) или наблюдаемыми переменными и, в общем случае, может быть пустым. Множества Q и E не должны пересекаться. Множество переменных, входящих в байесовскую сеть, но не входящих во множества Q и E называется скрытые (hidden) переменные. Семантика этих множеств довольно очевидна. Запрос – это целевые переменные, которые нас интересуют, исходя из контекста конкретной задачи. Наблюдение – это те переменные, значения которых мы можем измерить или предсказать. Скрытые переменные не являются ни тем, ни другим, но могут оказывать неявное влияние на запрос и/или на наблюдения. В таких обозначениях использование сети Байеса для логического вывода сводится к вычислению вероятности P(Q|E). Достоинством сетей Байеса является универсальность. Единожды сконструированная, сеть может использоваться для вычисления любых корректных запросов на области ее определения, то есть не нужно изменять конструкцию сети, чтобы выполнять запросы определенного вида. Запрос является корректным, если выполняются два условия: - все переменные входящие в множества наблюдений и запросов входят в область определения сети; - множества Q и E не пересекаются. Итак, каждый запрос разбивает множество переменных области определения сети на три непересекающихся множества: Q, E и H. Значение любого запроса к Байсовской сети на этих множествах может быть вычислен только из фактора, представляющего распределение полной вероятности P(Q,E,H). Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> c_node = Factor(name='C', cons=[c]) >>> c_node.cpd = np.array([0.99, 0.01]) >>> t_node = Factor(name='T|C', cons=[t], cond=[c]) >>> t_node.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> bn = Net(name='Cancer', nodes=[c_node, t_node]) Поля класса: name (`str`): имя сети; nodes(`list`): список факторов, составляющих сеть. Именованные параметры: name (`str`): имя сети; nodes (`list`): список факторов, составляющих сеть. Передача конструктору списока ``bn = Node(name='sample net', nodes=a_list)`` эквивалентна использованию метода :func:`add_node`:: bn = Net(name='sample net') for node in a_list: bn.add_node(node) Поэтому при использовании конструктора может генерироваться исключение метода :func:`add_node`. В частности, такое может произойти при неверном порядке факторов в передаваемом списке. Поэтому, рекомендуется использовать конструктор без второго параметра, а факторы в сеть добавлять явно. """ def __init__(self, name='', nodes=None): self.name = name self.nodes = [] for node in (nodes or []): self.add_node(node)
[docs] def joint(self): """ Рассчитывает распределение совместной вероятности всех переменных сети. Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> c_node = Factor(name='C', cons=[c]) >>> c_node.cpd = np.array([0.99, 0.01]) >>> t_node = Factor(name='T|C', cons=[t], cond=[c]) >>> t_node.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> bn = Net(name='Cancer', nodes=[c_node, t_node]) >>> j = bn.joint() >>> j.shape (2, 2) >>> len(j.vars) 2 >>> "%0.3f" % j.cpd[0,0] '0.198' >>> "%0.3f" % j.cpd[1,1] '0.001' Возвращает: Фактор (:class:`Factor`), представляющий рапределение полной вероятности всех переменных сети. """ res = None for node in self.nodes: res *= node.conditional return res
[docs] def add_node(self, factor): """ Метод добавляет фактор к сети. В процессе добавления фаактора к сети производится проверка корректности. Он заключается в том, что мы не можем добаить к сети фактор, если его родителя в сети нет. Например, у нас есть два фактора: F(C) и G(T|C). Так как второй фактор (G) представляет условную вероятность, его родителем должен быть фактор, определеяющий распределение вероятности переменной C, то есть, фактор F. Таким образом, если мы *сначала* попытаемся добавить к сети фактор G, то получим ошибку, так как его родителя в сети нет. Однако, если сперва добавить фактор F, а уже *затем* фактор G, то проблем не возникнет. Такая проверка гарантирует корректность графа, представляющего данную сеть. Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> c_node = Factor(name='C', cons=[c]) >>> c_node.cpd = np.array([0.99, 0.01]) >>> t_node = Factor(name='T|C', cons=[t], cond=[c]) >>> t_node.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> bn = Net(name='Cancer') >>> bn.add_node(c_node) >>> bn.add_node(t_node) Параметры: factor (:class:`Factor`): Исключения: `AttributeError`: ошибка возникает, если при добавлении фактора провалилась проверка корректности. """ correct = True node = _Node() node.conditional = factor node.name = factor.name for var in factor.cond: # проверка, есть ли распределение этого фактора в сети found = False for node_ in self.nodes: for cons in node_.conditional.cons: if var.name == cons.name: found = True node.parents.append(node_) correct = correct and found if not correct: raise AttributeError uncond = factor for parent in node.parents: uncond = uncond * parent.uncond uncond = uncond - parent.uncond.cons node.uncond = uncond self.nodes.append(node)
[docs] def query(self, query=None, evidence=None): """ Выполняет запрос к сети вывода. Синтаксис: >>> import numpy as np >>> c = Variable(name='C', terms=['no', 'yes']) >>> t = Variable(name='T', terms=['pos', 'neg']) >>> c_node = Factor(name='C', cons=[c]) >>> c_node.cpd = np.array([0.99, 0.01]) >>> t_node = Factor(name='T|C', cons=[t], cond=[c]) >>> t_node.cpd = np.array([[0.2, 0.8], [0.9, 0.1]]) >>> bn = Net(name='Cancer', nodes=[c_node, t_node]) >>> q = bn.query(query=[c], evidence=[t]) >>> len(q.vars) 2 >>> q.cond[0].name 'T' >>> q.cons[0].name 'C' >>> "%0.3f" % q.cpd[0,0] '0.957' >>> "%0.3f" % q.cpd[1,1] '0.001' Именованные параметры: query (`list`): список переменных (:class:`Variable`) запроса; evidence (`list`): список переменных (:class:`Variable`) свидетельств. Возвращает: Фактор (:class:`Factor`), представляющий рапределение условной вероятности, где условными переменными являются наблюдения (evidence), а подусловными - переменные запроса (query): F(Q|E). """ query = query or [] evidence = evidence or [] # TODO локальный вывод res = self.joint() # TODO проверка корректности hidden = list(set(res.vars) - set(query) - set(evidence)) for h in hidden: res -= h for e in evidence: for node in self.nodes: if node.uncond.vars == [e]: res /= node.uncond return res