Los clasificadores Naive Bayes (NBC por su siglas en inglés) son algoritmos de aprendizaje automático simples pero potentes. Se basan en la probabilidad condicional y el teorema de Bayes.

En esta publicación, explico "el truco" detrás de NBC y les daré un ejemplo que podemos usar para resolver un problema de clasificación.

En las próximas secciones, hablaré sobre las matemáticas detrás de NBC. Siéntete libre de omitir esas secciones y pasar a la parte de implementación si no estás interesado en las matemáticas.

En la sección de implementación, te mostraré un algoritmo NBC simple. Luego lo usaremos para resolver un problema de clasificación. La tarea será determinar si cierto pasajero del Titanic sobrevivió al accidente o no.

Probabilidad condicional

Antes de hablar sobre el algoritmo en sí, hablemos de las matemáticas detrás de él. Necesitamos entender qué es la probabilidad condicional y cómo podemos usar el teorema de Bayes para calcularla.

Piense en un dado equilibrado con seis lados. ¿Cuál es la probabilidad de obtener un seis al lanzar el dado? Eso es fácil, es 1/6. Tenemos seis resultados posibles e igualmente probables, pero solo nos interesa uno de ellos. Entonces, 1/6 lo es.

Pero, ¿qué pasa si te digo que ya lancé el dado y el resultado es un número par? ¿Cuál es la probabilidad de que tengamos un seis ahora?

Esta vez, los posibles resultados son solo tres porque solo hay tres números pares en el dado. Todavía estamos interesados en solo uno de esos resultados, por lo que ahora la probabilidad es mayor: 1/3. ¿Cuál es la diferencia entre ambos casos?

En el primer caso, no teníamos información previa sobre el resultado. Por lo tanto, necesitábamos considerar todos los resultados posibles.

En el segundo caso, se nos dijo que el resultado era un número par, por lo que podíamos reducir el espacio de posibles resultados a solo los tres números pares que aparecen en un dado normal de seis caras.

En general, al calcular la probabilidad de un evento A, dada la ocurrencia de otro evento B, decimos que estamos calculando la probabilidad condicional de A dado B, o simplemente la probabilidad de A dado B. Lo denotamos P(A|B).

Por ejemplo, la probabilidad de obtener un seis dado que el número que tenemos es par: P(Seis|Par) = 1/3. Aquí, denotamos con Seis el evento de obtener un seis y con Par el evento de obtener un número par.

Pero, ¿cómo calculamos las probabilidades condicionales? ¿Existe una fórmula?

Cómo calcular probabilidades condicionales y el teorema de Bayes

Ahora, te daré un par de fórmulas para calcular probabilidades condicionales. Prometo que no serán difíciles y son importantes si deseas comprender la ideas detrás de los algoritmos de aprendizaje automático de los que hablaremos más adelante.

La probabilidad de un evento A dada la ocurrencia de otro evento B se puede calcular de la siguiente manera:

P(A|B) = P(A,B)/P(B)

Donde P(A,B) denota la probabilidad de A y B ocurriendo al mismo tiempo, y P(B) denota la probabilidad de B.

Observa que necesitamos P(B) > 0 porque no tiene sentido hablar de la probabilidad de A dado B si la ocurrencia de B no es posible.

También podemos calcular la probabilidad de un evento A, dada la ocurrencia de múltiples eventos B1, B2, ..., Bn:

P(A|B1,B2,...,Bn) = P(A,B1,B2,...,Bn)/P(B1,B2,...,Bn)

Hay otra forma de calcular probabilidades condicionales. Esta forma es el llamado Teorema de Bayes.

P(A|B) = P(B|A)P(A)/P(B)

P(A|B1,B2,...,Bn) = P(B1,B2,...,Bn|A)P(A)/P(B1,B2,...,Bn)

Observa que estamos calculando la probabilidad del evento A dado el evento B, invirtiendo el orden de ocurrencia de los eventos.

Ahora suponemos que ha ocurrido el evento A y queremos calcular la probabilidad del evento B (o eventos B1, B2, ..., Bn en el segundo y más general ejemplo).

Un dato importante que se puede derivar de este Teorema es la fórmula para calcular P(B1,B2,...,Bn,A). Eso se llama la regla de la cadena para las probabilidades.

P(B1,B2,...,Bn,A) = P(B1 | B2, B3, ..., Bn, A)P(B2,B3,...,Bn,A)
= P(B1 | B2, B3, ..., Bn, A)P(B2 | B3, B4, ..., Bn, A)P(B3, B4, ..., Bn, A)
= P(B1 | B2, B3, ..., Bn, A)P(B2 | B3, B4, ..., Bn, A)...P(Bn | A)P(A)

Esa es una fórmula fea, ¿no? Pero bajo algunas condiciones podemos hacer una solución y evitarlo.

Hablemos del último concepto que necesitamos saber para entender los algoritmos.

Independencia

El último concepto del que vamos a hablar es el de independencia. Decimos que los eventos A y B son independientes si

P(A|B) = P(A)

Eso significa que la probabilidad del evento A no se ve afectada por la ocurrencia del evento B. Una consecuencia directa es que P(A,B) = P(A)P(B).

En términos sencillos, esto significa que la probabilidad de la ocurrencia de A y B al mismo tiempo es igual al producto de las probabilidades de los eventos A y B que ocurren por separado.

Si A y B son independientes, también se sostiene que:

P(A,B|C) = P(A|C)P(B|C)

¡Ahora estamos listos para hablar sobre los clasificadores Naive Bayes!

Clasificadores Naive Bayes

Supongamos que tenemos un vector X de n características (features) y queremos determinar la clase de ese vector a partir de un conjunto de k clases y1, y2, ..., yk. Por ejemplo, si queremos determinar si lloverá hoy o no.

Tenemos dos clases posibles (k = 2): lluvia, no lluvia, y la longitud del vector de características podría ser 3 (n = 3).

La primera característica podría ser si está nublado o soleado, la segunda característica podría ser si la humedad es alta o baja, y la tercera característica sería si la temperatura es alta, media o baja.

Entonces, estos podrían ser posibles vectores de características.

<Nublado, H_Alta, T_Baja>
<Soleado, H_Baja, T_Media>
<Nublado, H_Baja, T_Alta>

Nuestra tarea es determinar si lloverá o no, dadas las características meteorológicas.

Después de conocer las probabilidades condicionales, parece natural abordar el problema tratando de calcular la probabilidad de que llueva dadas las características:

R = P(Llueve | Nublado, H_Alta, T_Baja)
NR = P(NoLlueve | Nublado, H_Alta, T_Baja)

Si R > NR respondemos que va a llover, de lo contrario decimos que no.

En general, si tenemos k clases y1, y2, ..., yk, y un vector de n características X = <X1, X2, ..., Xn>, queremos encontrar la clase yi que maximiza

P(yi | X1, X2, ..., Xn) = P(X1, X2,..., Xn, yi)/P(X1, X2, ..., Xn)

Observa que el denominador es constante y no depende de la clase yi. Entonces, podemos ignorarlo y enfocarnos en el numerador.

En una sección anterior, vimos cómo calcular P(X1, X2,..., Xn, yi) descomponiéndolo en un producto de probabilidades condicionales (la fórmula fea):

P(X1, X2,..., Xn, yi) = P(X1 | X2,..., Xn, yi)P(X2 | X3,..., Xn, yi)...P(Xn | yi)P(yi)

Suponiendo que todas las características Xi son independientes y usando el teorema de Bayes, podemos calcular la probabilidad condicional de la siguiente manera:

P(yi | X1, X2,..., Xn) = P(X1, X2,..., Xn | yi)P(yi)/P(X1, X2, ..., Xn)
= P(X1 | yi)P(X2 | yi)...P(Xn | yi)P(yi)/P(X1, X2, ..., Xn)

Y solo tenemos que centrarnos en el numerador.

Al encontrar la clase yi que maximiza la expresión anterior, estamos clasificando el vector de entrada. Pero, ¿cómo podemos obtener todas esas probabilidades?

Cómo calcular las probabilidades

Al resolver este tipo de problemas necesitamos tener un conjunto de ejemplos previamente clasificados.

Por ejemplo, en el problema de adivinar si lloverá o no, necesitamos tener varios ejemplos de vectores de características y sus clasificaciones que se obtendrían de pronósticos meteorológicos anteriores.

Entonces, tendríamos algo como esto:

...
<Nublado, H_Alta, T_Baja> -> Llueve
<Soleado, H_Baja, T_Media> -> No Llueve
<Nublado, H_Baja, T_Alta> -> No Llueve
...

Supongamos que necesitamos clasificar un nuevo vector <Nublado, H_Baja, T_Alta>. Necesitamos calcular:

P(Llueve | Nublado, H_Baja, T_Baja) = P(Nublado | H_Baja, T_Baja, Llueve)P(H_Baja | T_Baja, Llueve)P(T_Baja | Llueve)P(Llueve)/P(Nublado, H_Baja, T_Baja)

Obtenemos la expresión anterior aplicando la definición de probabilidad condicional y la regla de la cadena. Recuerda que solo necesitamos enfocarnos en el numerador por lo que podamos eliminar el denominador.

También necesitamos calcular la probabilidad para NoLlueve, pero podemos hacerlo de una forma similar.

Podemos encontrar P(Llueve) = # Llueve/Total. Eso significa contar las entradas en el conjunto de datos que se clasifican con Llueve y dividir ese número por el tamaño del conjunto de datos.

Para calcular P(Nublado | H_Baja, T_Baja, Llueve) necesitamos contar todas las entradas que tienen las características H_Baja, T_Baja y Nublado. Esas entradas también deben clasificarse como Llueve. Luego, ese número se divide por la cantidad total de datos. Calculamos el resto de factores de la fórmula de forma similar.

Hacer esos cálculos para todas las clases posibles es muy costoso y lento. Por tanto, necesitamos hacer suposiciones sobre el problema que simplifiquen los cálculos.

Los clasificadores Naive Bayes asumen que todas las características son independientes entre sí. Entonces podemos reescribir nuestra fórmula aplicando el teorema de Bayes y asumiendo la independencia entre cada par de características:

P(Llueve | Nublado, H_Baja, T_Baja) = P(Nublado | Llueve)P(H_Baja | Llueve)P(T_Baja | Llueve)P(Llueve)/P(Nublado, H_Baja, T_Baja)

Ahora calculamos P(Nublado | Llueve) contando el número de entradas que están clasificadas como Llueve y estaban Nublado.

El algoritmo se llama Naive (que significa ingenuo en inglés) debido a esta suposición de independencia. Hay dependencias entre las características (features) la mayor parte del tiempo. No podemos decir que en la vida real no existe una dependencia entre la humedad y la temperatura, por ejemplo. Los clasificadores Naive Bayes también se denominan Bayes Indepentientes o Bayes Simples.

La fórmula general sería:

P(yi | X1, X2, ..., Xn) = P(X1 | yi)P(X2 | yi)...P(Xn | yi)P(yi)/P(X1, X2, ..., Xn)

Recuerda que puedes deshacerte del denominador. Solo calculamos el numerador y respondemos la clase que lo maximiza.

Ahora, implementemos nuestro NBC y usémoslo en un problema.

¡Programemos!

Les mostraré una implementación de un NBC simple y luego lo veremos en la práctica.

El problema que vamos a resolver es determinar si un pasajero del Titanic sobrevivió o no, dadas algunas características como su género y su edad.

Aquí puedes ver la implementación de un NBC muy simple:

class NaiveBayesClassifier:
    
    def __init__(self, X, y):
        
        '''
        X e y denotan las características y las etiquetas de destino respectivamente
        '''
        self.X, self.y = X, y 
        
        self.N = len(self.X) # Tamaño del conjunto de entrenamiento

        self.dim = len(self.X[0]) # Dimensión del vector de características

        self.attrs = [[] for _ in range(self.dim)] # Aquí almacenaremos las columnas del conjunto de entrenamiento.

        self.output_dom = {} # Clases de salida con el número de ocurrencias en el conjunto de entrenamiento. En este caso solo tenemos 2 clases

        self.data = [] # To store every row [Xi, yi]
        
        
        for i in range(len(self.X)):
            for j in range(self.dim):
                # si nunca hemos visto este valor para este atributo antes, 
                # luego lo agregamos a la matriz attrs en la posición correspondiente
                if not self.X[i][j] in self.attrs[j]:
                    self.attrs[j].append(self.X[i][j])
                    
            # si nunca hemos visto esta clase de salida antes,
            # luego lo agregamos a output_dom y contamos una ocurrencia por ahora
            if not self.y[i] in self.output_dom.keys():
                self.output_dom[self.y[i]] = 1
            # de lo contrario, incrementamos la ocurrencia de esta salida en el conjunto de entrenamiento en 1
            else:
                self.output_dom[self.y[i]] += 1
            # almacenar la fila
            self.data.append([self.X[i], self.y[i]])
            
            

    def classify(self, entry):

        solve = None # Resultado final
        max_arg = -1 # máximo parcial

        for y in self.output_dom.keys():

            prob = self.output_dom[y]/self.N # P(y)

            for i in range(self.dim):
                cases = [x for x in self.data if x[0][i] == entry[i] and x[1] == y] # all rows with Xi = xi
                n = len(cases)
                prob *= n/self.N # P *= P(Xi = xi)
                
            # si tenemos una probabilidad mayor para esta salida que el máximo parcial ...
            if prob > max_arg:
                max_arg = prob
                solve = y

        return solve

Aquí, asumimos que cada característica tiene un dominio discreto. Eso significa que toman un valor de un conjunto finito de valores posibles.

Lo mismo ocurre con las clases. Ten en cuenta que almacenamos algunos datos en el método __init__ por lo que no es necesario repetir algunas operaciones. La clasificación de una nueva entrada se lleva a cabo en el método classify.

Este es un ejemplo simple de implementación. En las aplicaciones del mundo real, no necesitas (y es mejor si no creas) tu propia implementación. Por ejemplo, la biblioteca sklearn en Python contiene varias buenas implementaciones de NBC.

¡Observa lo fácil que es implementarlo!

Ahora, apliquemos nuestro nuevo clasificador para resolver un problema. Tenemos un conjunto de datos con la descripción de 887 pasajeros en el Titanic. También podemos ver si un pasajero determinado sobrevivió a la tragedia o no.

Entonces, nuestra tarea es determinar si otro pasajero que no está incluido en el conjunto de entrenamiento lo hizo o no.

En este ejemplo, usaré la biblioteca de pandas para leer y procesar los datos. No utilizo ninguna otra herramienta.

Los datos se almacenan en un archivo llamado titanic.csv, por lo que el primer paso es leer los datos y obtener una descripción general.

import pandas as pd

data = pd.read_csv('titanic.csv')

print(data.head())

La salida es:

Survived  Pclass                                               Name  \
0         0       3                             Mr. Owen Harris Braund   
1         1       1  Mrs. John Bradley (Florence Briggs Thayer) Cum...   
2         1       3                              Miss. Laina Heikkinen   
3         1       1        Mrs. Jacques Heath (Lily May Peel) Futrelle   
4         0       3                            Mr. William Henry Allen   

      Sex   Age  Siblings/Spouses Aboard  Parents/Children Aboard     Fare  
0    male  22.0                        1                        0   7.2500  
1  female  38.0                        1                        0  71.2833  
2  female  26.0                        0                        0   7.9250  
3  female  35.0                        1                        0  53.1000  
4    male  35.0                        0                        0   8.0500  

Observa que tenemos el nombre de cada pasajero. No usaremos esa característica para nuestro clasificador porque no es significativa para nuestro problema. También eliminaremos la característica Fare (tarifa en inglés) porque es continua y nuestras funciones deben ser discretas.

Hay clasificadores Naive Bayes que admiten caracterísitcas (features) continuas. Por ejemplo, el clasificador Naive Bayes Gausseano.
y = list(map(lambda v: 'yes' if v == 1 else 'no', data['Survived'].values)) # valores objetivo como cadena

# No usaremos el campo 'Nombre'(Name) ni 'Tarifa' (Fare)

X = data[['Pclass', 'Sex', 'Age', 'Siblings/Spouses Aboard', 'Parents/Children Aboard']].values # valores de características

Luego, necesitamos separar nuestro conjunto de datos en un conjunto de entrenamiento y un conjunto de validación. El último se utiliza para validar qué tan bien está funcionando nuestro algoritmo.

print(len(y)) # >> 887

# Tomaremos 600 ejemplos para entrenar y el resto para el proceso de validación.
y_train = y[:600]
y_val = y[600:]

X_train = X[:600]
X_val = X[600:]

Creamos nuestro NBC con el conjunto de entrenamiento y luego clasificamos cada entrada en el conjunto de validación.

Medimos la precisión de nuestro algoritmo dividiendo el número de entradas que clasificó correctamente por el número total de entradas en el conjunto de validación.

## Crear la instancia de Naive Bayes Classifier con los datos de entrenamiento

nbc = NaiveBayesClassifier(X_train, y_train)


total_cases = len(y_val) # tamaño del conjunto de validación

# Ejemplos bien clasificados y ejemplos mal clasificados
good = 0
bad = 0

for i in range(total_cases):
    predict = nbc.classify(X_val[i])
#     print(y_val[i] + ' --------------- ' + predict)
    if y_val[i] == predict:
        good += 1
    else:
        bad += 1

print('TOTAL EXAMPLES:', total_cases)
print('RIGHT:', good)
print('WRONG:', bad)
print('ACCURACY:', good/total_cases)

La salida:

TOTAL EXAMPLES: 287
RIGHT: 200
WRONG: 87
ACCURACY: 0.6968641114982579

No es genial pero es algo. Podemos obtener una mejora de aproximadamente un 10% en la precisión si eliminamos otras funciones como hermanos / cónyuges a bordo y padres / hijos a bordo.

Puedes ver un cuaderno con el código y el conjunto de datos aquí.

Conclusiones

Hoy en día, tenemos redes neuronales y otros algoritmos de ML complejos y costosos por todas partes.

Los NBC son algoritmos muy sencillos que nos permiten conseguir buenos resultados en algunos problemas de clasificación sin necesidad de muchos recursos. También escalan muy bien, lo que significa que podemos agregar muchas más funciones y el algoritmo seguirá siendo rápido y confiable.

Incluso en el caso de que los NBC no fueran adecuados para el problema que estábamos tratando de resolver, podrían ser muy útiles como referencia.

Primero podríamos intentar resolver el problema usando un NBC con unas pocas líneas de código y poco esfuerzo. Luego podríamos intentar lograr mejores resultados con algoritmos más complejos y costosos.

Este proceso puede ahorrarnos mucho tiempo y nos da una retroalimentación inmediata sobre si los algoritmos complejos realmente valen la pena para nuestra tarea.

En este artículo, leíste sobre las probabilidades condicionales, la independencia y el teorema de Bayes. Esos son los conceptos matemáticos detrás de los clasificadores Naive Bayes.

Después de eso, vimos una implementación simple de un NBC y resolvimos el problema de determinar si un pasajero del Titanic sobrevivió al accidente.

Espero que este artículo te haya resultado útil. Puedes leer sobre temas relacionados con la informática en mi blog personal y siguiéndome en Twitter.

Traducido del artículo de Jose J. Rodríguez - How Naive Bayes Classifiers Work – with Python Code Examples