sábado, 5 de noviembre de 2011

La máquina predictora de estados de ánimo




No es ningún secreto que un tonto "leal" tiene más posibilidades de ascender en una empresa que un listo librepensador, pero si el tonto, además, disfruta gritando a los demás, sus posibilidades de ascender rozan el 100%. En eso andaba Gaznápiro, entrenando sus mejores gritos con el Sr. Lego dentro de su despacho (su faceta de tonto no necesitaba entrenarla más porque, probablemente, había llegado ya a lo más alto que nadie pueda llegar). Los gritos podían oírse en toda la oficina, entre nasales y con un tono agudo que daban más risa que miedo, y que agudizaban, aún más si cabía, la condición de tonto "leal" que le hace el trabajo sucio al gran jefe.
El Sr. Lego salió del despacho con la cabeza baja y maldiciendo hasta en arameo antiguo. Sofía, que se sentaba en la mesa contigua a la mía no paraba de anotar cosas en una libreta pequeña de color rojo, de esas de anillas en la parte de arriba.
¿Qué anotas con tanto interés? -pregunté a Sofía intentando esconder mi curiosidad.
Anoto el estado de ánimo que tiene Gaznápiro. Lo hago desde hace nueve días. -respondió.
Me quedé mirandola un rato esperando que me dijera que estaba de broma, pero no lo hizo. En vez de eso, prosiguió contándome detalles.
Intento encontrar un patrón de comportamento para ver si descubro qué es lo que hace que tenga un día de perros o un día de buen humor. Así sabré cuando pedir un ascenso o un día de vacaciones. Anoto lo que ha desayunado, el color de la corbata, si toma o no café… en fin todo lo que pueda observar lo anoto.
Lo peor de todo es que, conociendo a Sofía, lo estaba diciéndolo totalmente en serio.
El Sr. Lego, que andaba pegando el oído preguntó a Sofía si podía decirle qué tal día iba a tener mañana, ya que tocaba reunión de proyecto y andaba algo retrasado con la programación.
Bueno, -respondió Sofía- desgraciadamente aún no he encontrado ningún patrón.
Ambos me miraron como esperando que me sacara un as de la manga.
Venga Alberto, seguro que conoces algún truco para convertir todos estos datos en algo utilizable -dijo el Sr. Lego dándome palmaditas en la espalda y sonriendo como si me hubiera pillado en un renuncio.
Claro, si la información que ha anotado Sofía es la mitad de precisa de lo que esperaría de ella podemos utilizar un clasificador bayesiano para modelar el comportamiento de Gaznápiro e intentar predecir con qué estado de ánimo llegará mañana a la oficina.
Ahora eran Sofía y el Sr. Lego los que me miraban a mí como si fuera un bicho raro, lo que me hizo anotar mentalmente: "No volver a mirar a mis compañeros como si se hubieran escapado de un manicomio".

Usando los datos que había tomado Sofía obtuvimos la siguiente tabla en la que anotamos el color de la corbata, lo que había comido en el desayuno y lo que había bebido. En la última columna anotamos si ese día había estado o no de buen humor.

díaCorbataDesayunoBebidaHumor
1RojaPanCaféMalo
2RojaPanZumoMalo
3RojaFrutaCaféBueno
4AmarillaPanCaféMalo
5AmarillaFrutaCaféBueno
6AzulPanZumoBueno
7AzulFrutaCaféMalo
8AzulFrutaZumoBueno
9AzulPanCaféMalo

Un clasificador bayesiano -comencé a explicar- selecciona la mejor clasificación según los atributos de los que disponemos. En nuestro caso, las clasificaciones posibles son Bueno o Malo, que describen el estado de humor de Gaznápiro. Nuestros atributos son: Corbata, Desayuno y Bebida, que pueden tomar los siguientes valores:

Corbata={Roja | Amarilla | Azul}
Desayuno={Pan | Fruta}
Bebida={Café | Zumo}

Espero que recordéis bien el teorema de Bayes, porque lo que sigue se basa en él. No es por casualidad que se llame clasificador bayesiano.

Nuestro clasificador va a basarse en la siguiente expresión:


Como sabéis P(c) es la probabilidad de que ocurra la clasificación c, siendo c={Bueno | Malo}. Por otro lado, P(ai|c) es lo que llamamos probabilidad condicionada de a sabiendo que ocurre c. Por ejemplo, P(Azul|Bueno) es la probabilidad de que Gaznápiro lleve corbata azul sabiendo que hoy tiene buen humor.

Para calcular P(ai|c) vamos a usar la siguiente fórmula:


Donde:
n es el número total de ocurrencias de la clasificación c.
nc es el número de ocurrencias de la clasificación c para el atributo ai.
p es el valor a priori estimado para P(ai|c).
m es el tamaño de muestra equivalente.

Sofía no quitaba la vista del folio donde andaba garabateando las fórmulas, y el Sr. Lego no dejaba de mirarme tratando de seguir todo aquel maremagnum de formulitas y conceptos de Probabilidad.

Está bien, veámoslo con un ejemplo concreto -concedí. Imaginad que mañana Gaznápiro llega a la oficina con una corbata amarilla, y desayuna pan y zumo. Fijaos en que ese caso concreto no está en nuestra tabla, sin embargo, vamos a tratar de inferir con los datos que tenemos cuál será su estado de ánimo. Necesitamos calcular las siguientes probabilidades:
P(Amarilla|Bueno); P(Pan|Bueno); P(Zumo|Bueno)
P(Amarilla|Malo); P(Pan|Malo); P(Zumo|Malo)

Y también las probabilidades de:
P(Bueno) y P(Malo)

Para luego poder aplicar la primera fórmula y obtener un valor que nos permita clasificar los atributos. Según la primera formula, el cálculo se realiza así:

clasificar_dia_bueno = P(Bueno)*P(Amarilla|Bueno)*P(Pan|Bueno)*P(Zumo|Bueno)

Y para el caso de que tenga un mal día será:

clasificar_dia_malo = P(Malo)*P(Amarilla|Malo)*P(Pan|Malo)*P(Zumo|Malo)

Vamos a ir calculando cada una de las probabilidades condicionadas usando la segunda fórmula. Empecemos con P(Amarilla|Bueno). Tenemos que n=4, ya que hay un total de 4 casos en la tabla en el que humor=Bueno. Para nc tenemos que nc=1, ya que sólo hay un caso en el que el día haya sido bueno cuando llevaba corbata amarilla. Para p tenemos que p=1/3, ya que tenemos 3 posibles valores del atributo Corbata (Rojo, Amarillo y Azul). Finalmente, m es un valor arbitrario, pero ha de ser consistente para todos los casos. Vamos a tomar un valor m=3.
Es decir:
n=4
nc=1
p=1/3=0.333
m=3

P(Amarilla|Bueno)=(1+(3*0.333))/(4+3)=0.285

No vamos a desarrollar aquí el cálculo de todas las probabilidades, ya que se calcula siempre igual. Simplemente, calcularemos otra más correspondiente al caso humor=malo y después pondremos todos los resultados ya calculados.
Como ejemplo calcularemos la probabilidad P(Pan|Malo):
Ahora tenemos:
n=5 (hay 5 casos en la tabla en el que Humor=Malo)
nc=4 (hay 4 casos en el que comió pan teniendo un día malo)
p=1/2=0.5 (hay dos posibilidades: Pan o Fruta, por lo que la probabilidad es del 50% para cada una).
m=3

P(Pan|Malo)=(4+(3*0.5))/(5+3)=0.687

Resumiendo, estos son los valores que hemos obtenido después de calcular todas las probabilidades condicionadas:

P(Amarilla|Bueno)=0.285
P(Pan|Bueno)=0.357
P(Zumo|Bueno)=0.5

P(Amarilla|Malo)=0.249
P(Pan|Malo)=0.687
P(Zumo|Malo)=0.312

Nos resta calcular la probabilidad de P(Bueno) y P(Malo) que son:
P(Bueno)=4/9=0.444 (4 ocurrencias de Bueno sobre 9)
P(Malo)=5/9=0.555 (5 ocurrencias de Malo sobre 9)
Aunque aquí nos hemos limitado a dos, evidentemente podemos hacer una clasificación en más de dos categorías.

Usando la primera fórmula obtenemos el siguiente resultado para el caso Humor=Bueno:
clasificar_dia_bueno = 0.444*0.285*0.357*0.5 = 0.0225

y para el caso Humor=Malo:
clasificar_dia_malo = 0.555*0.249*0.687*0.312 = 0.0296

Por lo que, en principio, podemos prever que, dados los condicionantes observados (corbata amarilla y desayuno de pan y zumo), lo más probable es que tenga un mal día.

Sofía y el Sr. Lego levantaron la vista del papel donde estaba haciendo los cálculos y se me quedaron mirando sin saber qué decir, así que me adelanté a la pregunta que suponía que estaban a punto de hacerme:
Evidentemente, esto no es más que un modelo estadístico y no el oráculo de Delfos, esto no quiere decir que siempre que Gaznápiro pida pan y zumo para desayunar y traiga una corbata amarilla venga de mal humor. Además, el número de muestras (días) que hemos usado es bajo. En todo caso, este método, pese a su sencillez, es una poderosa herramienta para clasificar casi cualquier cosa. Los clasificadores bayesianos son muy utilizados, por ejemplo, en la clasificación de documentos. Un buen ejemplo de ello son los programas anti-spam que utilizan los servidores de correo electrónico. Si os interesa, quizá otro día os explique como se utilizan para filtrar el spam. También se utiliza en minería de datos, traducción automática, correctores ortográficos, predicción (meteorológica, bursátil, etc…). Y en general tiene multitud de aplicaciones dentro del campo de la Inteligencia Artificial.

Al día siguiente el Sr. Lego llegó con el siguiente programa en Python que había preparado para predecir el humor de Gaznápiro:

  1. def classify(attr, classif, data, ask):
  2.   # entrenar el clasificador
  3.   m=3
  4.   classes={}
  5.   attribs={}
  6.   attribs_count={}
  7.   attribs_count_per_class={}
  8.  
  9.   # obtener atributos
  10.   for i in range(len(attr)):
  11.     attribs[attr[i]]=[]
  12.  
  13.   for k,v in data.iteritems():
  14.     # obtener clases
  15.     if not v in classes:
  16.       classes[v]=1
  17.     else:
  18.       classes[v]=classes[v]+1
  19.  
  20.     # contabilizar atributos
  21.     for at in k:
  22.       if not at in attribs[attr[k.index(at)]]:
  23.         attribs[attr[k.index(at)]].append(at)
  24.       # cuenta total
  25.       if not at in attribs_count:
  26.         attribs_count[at]=1
  27.       else:
  28.         attribs_count[at]=attribs_count[at]+1
  29.       # cuenta por clase
  30.       if not (at,v) in attribs_count_per_class:
  31.         attribs_count_per_class[(at,v)]=1
  32.       else:
  33.         attribs_count_per_class[(at,v)]=attribs_count_per_class[(at,v)]+1
  34.  
  35.   # calculo valor por cada clase
  36.   calc={}
  37.   for i in classes:
  38.     calc[i]=float(classes[i])/float(len(data))
  39.     for j in ask:
  40.       n=classes[i]
  41.       nc=attribs_count_per_class[(j,i)]
  42.       p=1.0/len(attribs[attr[ask.index(j)]])
  43.       calc[i]=calc[i]*((nc+m*p)/(n+m))
  44.  
  45.   return max(calc)
  46.  
  47.  
  48. if __name__ == "__main__":
  49.   attr=['corbata','desayuno','bebida']
  50.   classif=['humor']
  51.  
  52.   data={('roja','pan','cafe'):'malo',
  53.     ('roja','pan','zumo'):'malo',
  54.     ('roja','fruta','cafe'):'bueno',
  55.     ('amarilla','pan','cafe'):'malo',
  56.     ('amarilla','fruta','cafe'):'bueno',
  57.     ('azul','pan','zumo'):'bueno',
  58.     ('azul','fruta','cafe'):'malo',
  59.     ('azul','fruta','zumo'):'bueno',
  60.     ('azul','pan','cafe'):'malo'}
  61.  
  62.   ask=['amarilla','pan','zumo']
  63.  
  64.   theclass = classify(attr, classif, data, ask)
  65.   print "Clasificado como: " + theclass

De todas formas hoy no necesito ejecutarlo para saber cómo vendrá Gaznápiro a la oficina -dijo el Sr. Lego sonriendo- He visto su coche en el parking y alguien acaba de hacerle un buen bollo en la puerta. ¿Crees que deberíamos añadir un atributo al programa para eso?

PD. Para los que tengan curiosidad, la ejecución del programa predijo que Gaznápiro tendría un mal día… y acertó.

4 comentarios:

  1. Hola, estuve buscando un lugar en el que pudieras leer mi comentario, espero que aquí lo hagas.

    Acabo de terminar de leer tu libro de programación de videojuegos en SDL, me fue MUY útil, quería agradecerte por escribirlo, porque ha logrado explicarme de manera que ninguno de los otros libros lo ha logrado hacer.

    Muchas Gracias, sigue con esto :D

    ResponderEliminar
  2. Hola Frodinzky,
    Me alegro mucho de que te haya gustado :-)
    Intentaré "seguir con esto" mientras el tiempo libre me lo permita ;-)

    Saludos y feliz año nuevo!!!

    ResponderEliminar
  3. Me gustaria saber de donde sale la fórmula del cálculo de la probabilidad condicional, ya que no la he visto en ningún otro lado. Saludos

    ResponderEliminar
    Respuestas
    1. Hola Julio,

      La formula habitual para el cálculo de la probabilidad condicionada es nc/n, que es la formula que seguramente conocerás. Sin embargo, para un número pequeño de muestras, la estimación de la probabilidad suele ser mala usando la formulación clásica. Además, si nc es 0 tenemos un problema.
      En este caso se suele usar la estimación m-estimate (http://www.giwebb.com/Doc/MOmestimate.html), que tiende a ser más robusta. A dicho estimador pertenece la formula que he usado en el post.
      Si te fijas, al usar un tamaño de muestra equivalente y un valor estimado a priori, lo que estamos haciendo es "simular" de alguna manera el uso de más muestras de las que realmente tenemos.

      Un saludo.

      Eliminar