Пример того, как трюк log-sum-exp работает в наивном байесовском

14

Я читал о трюке log-sum-exp во многих местах (например, здесь и здесь ), но никогда не видел пример того, как он применяется конкретно к наивному байесовскому классификатору (например, с дискретными функциями и двумя классами)

Как именно можно избежать проблемы числового снижения при использовании этого трюка?

мистифицировать
источник
2
Здесь есть несколько примеров его использования, хотя это не обязательно явно для наивного Байеса. Однако это вряд ли имеет значение, так как идея трюка довольно проста и легко адаптируется.
Glen_b
Проблема, скорее всего, заключается в недостаточном объеме, а не в переполнении.
Генри
Я бы посоветовал вам попробовать поиск по недополнению , а затем обновить свой вопрос, чтобы более конкретно указать, что уже не охватывалось.
Glen_b
Не могли бы вы также уточнить - это модель Бернулли наивного Байеса? что-то еще, возможно?
Glen_b
Посмотрите пример здесь , прямо внизу (как раз перед «См. Также», где они берут журналы; возводят в степень обе стороны, но оставляют RHS «как есть» (как exp из суммы журналов), будет примером журнала Трюк -sum-exp. Дает ли это вам достаточную информацию, касающуюся его использования в
Наивном Байесе,

Ответы:

26

В

p(Y=C|x)=p(x|Y=C)p(Y=C) k=1|C|p(x|Y=Ck)p(Y=Ck)

и знаменатель, и числитель могут стать очень маленькими, как правило, потому что может быть близко к 0, и мы умножаем многие из них друг на друга. Чтобы предотвратить потери, можно просто взять журнал числителя, но для знаменателя нужно использовать трюк log-sum-exp.p(xi|Ck)


Более конкретно, для предотвращения недопущения:

  • Если нам важно знать, к какому классу вход скорее всего, относится с максимальным апостериорным (MAP) правилом принятия решений, мы не будем нужно применить трюк log-sum-exp, так как нам не нужно вычислять знаменатель в этом случае. Для числителя можно просто взять журнал, чтобы предотвратить потери: . Более конкретно:( х = х 1 , ... , х п ) л о г ( р ( х | Y = С ) р ( Y = С ) )(y^)(x=x1,,xn)log(p(x|Y=C)p(Y=C))

    y^=argmaxk{1,,|C|}p(Ck|x1,,xn)=argmaxk{1,,|C|} p(Ck)i=1np(xi|Ck)

    который становится после взятия журнала:

y^=argmaxk{1,,|C|}log(p(Ck|x1,,xn))=argmaxk{1,,|C|}log( p(Ck)i=1np(xi|Ck))=argmaxk{1,,|C|}(log(p(Ck))+ i=1nlog(p(xi|Ck)))
  • Если мы хотим вычислить вероятность класса , нам нужно вычислить знаменатель:p(Y=C|x)

    log(p(Y=C|x))=log(p(x|Y=C)p(Y=C) k=1|C|p(x|Y=Ck)p(Y=Ck))=log(p(x|Y=C)p(Y=C)numerator)log( k=1|C|p(x|Y=Ck)p(Y=Ck)denominator)

    Элемент может опуститься из-за того, что может быть очень маленьким: это та же проблема, что и в числителе, но на этот раз у нас есть суммирование внутри логарифма, что не позволяет нам преобразовать (может быть близко к 0) в (отрицательно и больше не близко к 0, поскольку ). Чтобы обойти эту проблему, мы можем использовать тот факт, что чтобы получить:log( k=1|C|p(x|Y=Ck)p(Y=Ck))p(xi|Ck)p(xi|Ck)log(p(xi|Ck))0p(xi|Ck)1p(xi|Ck)=exp(log(p(xi|Ck)))

    log( k=1|C|p(x|Y=Ck)p(Y=Ck))=log( k=1|C|exp(log(p(x|Y=Ck)p(Y=Ck))))

    В этот момент возникает новая проблема: может быть весьма отрицательным, что означает, что может стать очень близким к 0, т.е. Вот где мы используем трюк log-sum-exp :log(p(x|Y=Ck)p(Y=Ck))exp(log(p(x|Y=Ck)p(Y=Ck)))

    logkeak=logkeakeAA=A+logkeakA

    с участием:

    • ak=log(p(x|Y=Ck)p(Y=Ck)) ,
    • A=maxk{1,,|C|}ak.

    Мы можем видеть, что введение переменной позволяет избежать потерь. Например, с , имеем:Ak=2,a1=245,a2=255

    • exp(a1)=exp(245)=3.96143×10107
    • exp(a2)=exp(255)=1.798486×10111

    Используя трюк log-sum-exp, мы избегаем : : A=max(245,255)=245logkeak=logkeakeAA=A+logkeakA=245+logkeak+245=245+log(e245+245+e255+245)=245+log(e0+e10)

    Мы избежали потери, поскольку намного дальше от 0, чем или .e103.96143×101071.798486×10111

Франк Дернонкур
источник
2

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

Теперь найдите вторую ссылку, которую вы опубликовали. Здесь будет представлять собой общую вероятность соблюдения предложения для данной базы данных, а s будет представлять вероятность наблюдения каждого из слов в предложении.aebt

Sid
источник
1

Из этого ответа мы можем видеть, что наименьшее число в Python (просто возьмем его, например) 5e-324связано с IEEE754 , а аппаратная причина относится и к другим языкам.

In [2]: np.nextafter(0, 1)
Out[2]: 5e-324

И любое число с плавающей запятой меньше этого приведет к 0.

In [3]: np.nextafter(0, 1)/2
Out[3]: 0.0

И давайте посмотрим на функцию наивного Байеса, with discrete features and two classesкак вам нужно:

p(S=1|w1,...wn)=p(S=1)i=1np(wi|S=1) s={0,1}p(S=s)i=1np(wi|S=s)

Позвольте мне описать эту функцию простой задачей НЛП ниже.

Мы решили определить, является ли приходящее письмо спамом ( ) или не спамом ( ), и у нас есть словарный запас размером 5000 слов ( ), и единственное беспокойство вызывает то, что слово ( ) встречается ( ) в электронном письме или нет ( ) для простоты ( Бернулли наивный Байес ).S=1S=0n=5,000wip(wi|S=1)1p(wi|S=1)

In [1]: import numpy as np
In [2]: from sklearn.naive_bayes import BernoulliNB
# let's train our model with 200 samples
In [3]: X = np.random.randint(2, size=(200, 5000))
In [4]: y = np.random.randint(2, size=(200, 1)).ravel()
In [5]: clf = BernoulliNB()
In [6]: model = clf.fit(X, y)

Мы можем видеть, что будет очень маленьким из-за вероятностей (оба и будет между 0 и 1) в , и, следовательно, мы уверены, что произведение будет меньше и мы просто получим .p(S=s)i=1np(wi|S=s)p(wi|S=1)1p(wi|S=1)i50005e3240/0

In [7]: (np.nextafter(0, 1)*2) / (np.nextafter(0, 1)*2)
Out[7]: 1.0

In [8]: (np.nextafter(0, 1)/2) / (np.nextafter(0, 1)/2)
/home/lerner/anaconda3/bin/ipython3:1: RuntimeWarning: invalid value encountered in double_scalars
  #!/home/lerner/anaconda3/bin/python
Out[8]: nan
In [9]: l_cpt = model.feature_log_prob_
In [10]: x = np.random.randint(2, size=(1, 5000))
In [11]: cls_lp = model.class_log_prior_
In [12]: probs = np.where(x, np.exp(l_cpt[1]), 1-np.exp(l_cpt[1]))
In [13]: np.exp(cls_lp[1]) * np.prod(probs)
Out[14]: 0.0

Тогда возникает проблема: как мы можем вычислить вероятность того, что письмо является спамом ? Или как мы можем вычислить числитель и знаменатель?p(S=1|w1,...wn)

Мы можем увидеть официальную реализацию в sklearn :

jll = self._joint_log_likelihood(X)
# normalize by P(x) = P(f_1, ..., f_n)
log_prob_x = logsumexp(jll, axis=1)
return jll - np.atleast_2d(log_prob_x).T

Для числителя он преобразовал произведение вероятностей в сумму логарифмического правдоподобия, а для знаменателя он использовал logsumexp в scipy, который:

out = log(sum(exp(a - a_max), axis=0))
out += a_max

Потому что мы не можем добавить две совместные вероятности, добавив их совместную логарифмическую вероятность, и мы должны выйти из логарифмического пространства в вероятностное пространство. Но мы не можем добавить две истинные вероятности, потому что они слишком малы, и мы должны масштабировать их и сделать сложение: и вернуть результат обратно в пространство журнала затем измените его масштаб: в пространстве журнала, добавив .s={0,1}ejllsmax_jlllogs={0,1}ejllsmax_jllmax_jll+logs={0,1}ejllsmax_jllmax_jll

А вот и вывод:

logs={0,1}ejlls=logs={0,1}ejllsemax_jllmax_jll=logemax_jll+logs={0,1}ejllsmax_jll=max_jll+logs={0,1}ejllsmax_jll

где - это в коде.max_jlla_max

Как только мы получим как числитель, так и знаменатель в лог-пространстве, мы можем получить условную вероятность ( ), вычитая знаменатель из числителя : logp(S=1|w1,...wn)

return jll - np.atleast_2d(log_prob_x).T

Надеюсь, это поможет.

Ссылка:
1. Наивный байесовский классификатор Бернулли
2. Фильтрация спама наивным байесовским фильтром - какие наивные байесовские?

Лернер Чжан
источник