Ejemplo de cómo funciona el truco log-sum-exp en Naive Bayes

14

He leído sobre el truco log-sum-exp en muchos lugares (por ejemplo , aquí y aquí ) pero nunca he visto un ejemplo de cómo se aplica específicamente al clasificador Naive Bayes (por ejemplo, con características discretas y dos clases)

¿Cómo se evitaría exactamente el problema del desbordamiento numérico con este truco?

Josh
fuente
2
Hay varios ejemplos de su uso aquí, aunque no necesariamente explícitamente para ingenuos Bayes. Sin embargo, eso apenas importa, ya que la idea del truco es bastante sencilla y fácilmente adaptable.
Glen_b -Reinstate Monica
Es más probable que el problema sea un desbordamiento que un desborde.
Henry
Le sugiero que intente una búsqueda en underflow y luego actualice su pregunta para abordar más específicamente lo que aún no está cubierto.
Glen_b -Reinstate Monica
¿Podría aclarar también: se trata de Bayes ingenuo modelo Bernoulli? algo más tal vez?
Glen_b -Reinstala a Monica el
Vea el ejemplo aquí , justo en la parte inferior (justo antes de 'Ver también' donde toman registros; exponiendo ambos lados pero dejando el RHS "tal cual" (como la exp de una suma de registros) sería un ejemplo del registro -sum-exp trick. ¿Te da suficiente información sobre su uso en Naive Bayes para hacer una pregunta más específica?
Glen_b -Reinstala Monica el

Respuestas:

26

En

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

tanto el denominador como el numerador pueden volverse muy pequeños, típicamente porque la puede estar cerca de 0 y multiplicamos muchos de ellos entre sí. Para evitar desbordamientos, uno simplemente puede tomar el registro del numerador, pero necesita usar el truco log-sum-exp para el denominador.p(xi|Ck)


Más específicamente, para evitar desbordamientos:

  • Si solo nos interesa saber a qué clase la entrada probablemente pertenece con la regla de decisión de máximo a posteriori (MAP), no tenemos que aplicar el truco log-sum-exp, ya que no tenemos que calcular el denominador en ese caso. Para el numerador, uno simplemente puede tomar el registro para evitar desbordamientos: . Más específicamente:( x = x 1 , ... , x n ) l o g ( p ( x | Y = C ) p ( Y = C ) )(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)

    que se convierte después de tomar el registro:

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)))
  • Si queremos calcular la probabilidad de clase , necesitaremos calcular el denominador: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)

    El elemento puede desbordarse porque puede ser muy pequeño: es el mismo problema que en el numerador, pero esta vez tenemos una suma dentro del logaritmo, que nos impide transformar el (puede estar cerca de 0) en (negativo y ya no está cerca de 0, ya que ). Para evitar este problema, podemos utilizar el hecho de que para obtener: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))))

    En ese punto, surge un nuevo problema: puede ser bastante negativo, lo que implica que puede llegar a estar muy cerca de 0, es decir, bajo flujo. Aquí es donde usamos el truco log-sum-exp :log(p(x|Y=Ck)p(Y=Ck))exp(log(p(x|Y=Ck)p(Y=Ck)))

    logkeak=logkeakeAA=A+logkeakA

    con:

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

    Podemos ver que la introducción de la variable evita los desbordamientos. Por ejemplo, con , tenemos:Ak=2,a1=245,a2=255

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

    Al usar el truco log-sum-exp evitamos el inferior, con : A=max(245,255)=245logkeak=logkeakeAA=A+logkeakA=245+logkeak+245=245+log(e245+245+e255+245)=245+log(e0+e10)

    Evitamos el desbordamiento ya que está mucho más lejos de 0 que o .e103.96143×101071.798486×10111

Franck Dernoncourt
fuente
2

Supongamos que queremos identificar cuál de las dos bases de datos es más probable que haya generado una frase (por ejemplo, de qué novela es más probable que haya surgido esta frase). Podríamos asumir la independencia de las palabras condicionales en la base de datos (supuesto de Naive Bayes).

Ahora busque el segundo enlace que ha publicado. Allí representaría la probabilidad conjunta de observar la oración dada una base de datos y los s representarían la probabilidad de observar cada una de las palabras en la oración.aebt

Sid
fuente
1

Podemos ver en esta respuesta que el número más pequeño en Python (solo tómalo , por ejemplo) se 5e-324debe al IEEE754 , y la causa del hardware también se aplica a otros idiomas.

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

Y cualquier flotador más pequeño que eso llevaría a 0.

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

Y veamos la función de Naive Bayes with discrete features and two classescomo lo requirió:

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

Permítanme crear una instancia de esa función con una simple tarea de PNL a continuación.

Decidimos detectar si el correo electrónico entrante es spam ( ) o no spam ( ) y tenemos un vocabulario de 5.000 palabras ( ) y la única preocupación es si aparece una palabra ( ) ( ) en el correo electrónico o no ( ) por simplicidad ( Bernoulli ingenuo Bayes ).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)

Podemos ver que sería muy pequeño debido a las probabilidades (tanto como estaría entre 0 y 1) en , y por lo tanto, estamos seguros de que el producto será menor que y solo obtenemos .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

Entonces surge el problema: ¿cómo podemos calcular la probabilidad de que el correo electrónico sea un spam ? ¿O cómo podemos calcular el numerador y el denominador?p(S=1|w1,...wn)

Podemos ver la implementación oficial en 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

Para el numerador, convirtió el producto de probabilidades en la suma de la probabilidad logarítmica y para el denominador usó logsumexp en scipy, que es:

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

Debido a que no podemos agregar dos probabilidades conjuntas agregando su probabilidad de registro conjunto, debemos salir del espacio logarítmico al espacio de probabilidad. Pero no podemos agregar las dos probabilidades verdaderas porque son demasiado pequeñas y debemos escalarlas y hacer la suma: y poner el resultado de nuevo en el espacio de registro luego vuelva a : en el espacio de registro agregando .s={0,1}ejllsmax_jlllogs={0,1}ejllsmax_jllmax_jll+logs={0,1}ejllsmax_jllmax_jll

Y aquí está la derivación:

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

donde es el en el código.max_jlla_max

Una vez que obtenemos tanto el numerador como el denominador en el espacio logarítmico, podemos obtener la probabilidad condicional logarítmica ( ) restando el denominador del numerador : logp(S=1|w1,...wn)

return jll - np.atleast_2d(log_prob_x).T

Espero que ayude.

Referencia:
1. Bernoulli Naive Bayes Classifier
2. Filtrado de spam con Naive Bayes - ¿Qué Naive Bayes?

Lerner Zhang
fuente