Download Motivación - DCC - Universidad de Chile

Document related concepts

Árbol binario wikipedia , lookup

Recorrido de árboles wikipedia , lookup

Árbol de Merkle wikipedia , lookup

Estructuras de datos persistentes wikipedia , lookup

Árbol-R wikipedia , lookup

Transcript
Universidad de Chile
Facultad de Ciencias Físicas y Matemáticas
Departamento de Ciencias de la Computación
Búsqueda de patrones frecuentes en un
conjunto de datos mediante el algoritmo
FPGrowth.
Autor:
Andrés Villavicencio.
Motivación.
En el mundo del retail, siempre se ha buscado ofrecer a los consumidores los productos
que necesiten. En la vida real, se encuentran cientos de productos que por sí solos, no son
necesarios, pero que adquieren importancia cuando están acompañados de otros
productos. Un ejemplo clásico sería la necesidad de llevar una bebida cola al comprar una
botella de pisco, productos a los que además se puede añadir maní o vasos plásticos. Las
empresas aprovechan estas relaciones al momento de realizar promociones y definir el
posicionamiento de los productos en sus estantes. Si bien es sabido que esta práctica no
atrae un mayor número de clientes a un local, logra que el valor de la compra sea mayor
en cada visita.
La dificultad de esta técnica es descubrir las relaciones que hay entre productos
complementarios. Para enfrentarse a este problema, las empresas de retail realizan
búsquedas en la historia de ventas de sus negocios, analizando qué patrones de compra
son los más frecuentes. Por ejemplo en Amazon.com le es indicado al visitante que
porcentaje de la gente que compró el producto A llevo además el producto B. El
problema de la búsqueda de patrones frecuentes está relacionado con el volumen de datos
a procesar. Esto pues una base de datos de venta fácilmente puede incluir 100 millones de
registros y decenas de miles de ítemes, comprados en millones de carros o transacciones
(transacción o carro se refiere a todos los ítemes que se llevaron en una instancia de
compra). Para resolver este problema se han desarrollado dos algoritmos: el algoritmo
APRIORI que basa su funcionamiento en múltiples lecturas de los datos y rápida
eliminación de candidatos de bajo soporte, y FPGrowth que almacena la información en
una estructura altamente comprimida conocida como FPTree. Es el algoritmo FPGrowth
el que se revisará en el presente documento.
Estructura Frequent Pattern Tree (FPTree).
Árbol FP.
El objetivo de un árbol FP es almacenar de manera altamente comprimida un conjunto de
transacciones, utilizando una estrategia similar a Patricia Tries (Un algoritmo de
compresión de conjuntos de Strings basada en uso de prefijos).
El Árbol FP no es más que una serie de listas de Nodos FP (una lista por cada tipo de
ítem presente en las transacciones), los cuales están vinculados entre sí en forma de árbol
(Fig 1).
Figura 1: Árbol FP.
El supuesto básico de la creación de un árbol FP es que los ítemes en una transacción
vienen ordenados de forma consistente, es decir, dado un ítem A y un ítem B, si A
precede a B en algún esquema de ordenamiento, entonces A siempre estará antes que B
en las transacciones (se ha comprobado en la práctica, que los mejores desempeños se
obtienen ordenando por soporte de los ítemes con A precede a B, si soporte de A >
soporte de B). De no cumplirse esto no habrá compresión de los datos mediante el uso de
prefijos y el podado no funcionará (El podado y su utilidad se verá más adelante).
La única información extraída de manera directa de un árbol FP es el soporte de los
ítemes contenidos en él. Esto se logra sumando los conteos de los ítemes que hay en cada
lista enlazada (Ver sección Nodo FP). En el diagrama anterior este total se muestra en las
cajas a la izquierda del árbol. Para obtener los patrones contenidos en un árbol FP se debe
usar la técnica de podado, descrita más adelante.
Nodo FP.
El Nodo FP es un híbrido entre un nodo de árbol y un nodo de lista enlazada. Hay
variados acercamientos para definir la estructura de un nodo FP. Las variaciones más
frecuentes están dadas por la estrategia de conexión entre nodos padre e hijo y si la lista
es de enlace simple o de doble enlace.
A continuación vemos un nodo FP en su forma más frecuente:
class FPNode {
//Identificador del nodo.
int id;
//Información relevante.
int item;
int conteo;
//Enlace padre-hijo.
FPNode padre;
//Enlace tipo lista.
FPNode siguiente;
}
Como se puede apreciar, cada nodo guarda dos campos de información: el identificador
de un ítem y el número de transacciones en que aparece, además de un puntero al padre y
el nodo siguiente. La principal ventaja de este nodo es su tamaño: 20 bytes. Un árbol FP
generado a partir de un conjunto de transacciones puede llegar sin problemas a contener
10 millones de nodos, lo que con el nodo mencionado, llevaría a un uso de memoria de
200 MB. Su principal desventaja es que se requieren pasos adicionales de procesamiento
de los datos iniciales para compensar la ausencia de punteros de padre a hijos.
A continuación vemos una posible variante del Nodo FP.
public class FPNode {
//Identificador del nodo.
int id;
//Información relevante.
int item;
int conteo;
//Enlace padre-hijo.
ListaEnlazada<FPNode> hijos;
//Enlace tipo lista.
FPNode siguiente;
FPNode anterior;
}
Esta variante incluye los dos cambios más comunes: punteros de padre a hijo (en vez de
hijo a padre) y lista de doble enlace para la relación siguiente-anterior. Entre las ventajas
de este tipo de nodo podemos señalar que: el árbol formado puede ser alimentado con las
transacciones prácticamente sin procesar, la existencia de un mecanismo que permite
acceder a los hijos y que la lista de doble enlace ayuda a eliminar nodos de forma más
rápida y sencilla (un caso frecuente de eliminación de nodos se da cuando un conjunto de
transacciones puede incluir devoluciones). Las desventajas de este esquema son: el
acceso a los hijos de un nodo (Es lento. Hay que iterar a través de la lista enlazada hijos)
y el tamaño del nodo crece considerablemente, lo que puede llevar a problemas de
memoria.
Podado.
El podado de un árbol FP permite, dado un ítem cualquiera de éste, encontrar un nuevo
árbol FP que represente las transacciones que comparten los antecesores del ítem con
éste.
Antes de explicar el algoritmo, veamos un ejemplo que muestre su utilidad. Dado el
siguiente árbol FP (Fig 2):
Figura 2: Árbol FP inicial.
Si podamos con C como parámetro obtendremos (Fig 3):
Figura 3: Árbol podado por C.
Esto se interpreta como que de las cinco veces que aparece el ítem C, aparece junto con B
cuatro veces y junto con A tres veces. Si podamos nuevamente con B como parámetro
obtendremos (Fig 4):
Figura 4: Árbol podado por C y por B.
Esto se interpreta de la siguiente manera: de las cuatro veces que aparece el patrón (C B)
tres de ellas también incluyen A, en otras palabras el patrón (C B A) tiene una frecuencia
de tres. Es importante entender que estos no son la totalidad de los patrones en los que
participa C, solo representan los patrones compuestos por C y sus antecesores.
El algoritmo para podar el árbol es el siguiente:
public FPTree podar(item, FPTree original){
FPTree resultado = new FPTree();
for(FPNode nodo =original.primerNodo(item);item != null; nodo=nodo.siguiente){
colgarRama(nodo.padre,nodo.cuenta,resultado);
}
retornar resultado;
}
public FPNode colgarRama(FPnode nodo, int cuenta, FPTree resultado){
if(nodo == null){
return nodo;
}
FPNode padre=colgarRama(nodo.padre, cuenta, resultado);
return resultado.addNodo(padre, nodo.id, nodo.item,cuenta);
}
El método podar itera por los nodos de la lista del ítem por el cual se está podando y
entrega al método colgarRama, los nodos padres de estos y el conteo del nodo. El conteo
del nodo es importante, pues los ítemes que aparecen en los nodos ancestros sólo
aparecen junto con el ítem de podado, una cantidad de transacciones igual al conteo del
nodo desde el que se llama inicialmente al método colgarRama en el método podar (esto
es porque el conteo del nodo es igual al número de transacciones que comparte con sus
ancestros).
Veamos un ejemplo del colgado de una rama, partiendo con el siguiente árbol (Fig 5):
Figura 5: Árbol a podar.
Si podamos por E, el primer llamado a colgarRama atravesaría los siguientes nodos (Fig
6):
Figura 6: Nodos seleccionados en primera llamada recursiva a colgarRama.
El árbol resultado quedaría de la siguiente forma (Fig 7):
Figura 7: Árbol resultado, después de primera llamada recursiva.
Es importante que el método addNodo pueda reconocer cuando se debe actualizar un
nodo, en vez de crear uno nuevo (para ello se usa el campo id de la estructura FPNode)
como se aprecia en la segunda ejecución del método colgarRama, la cual revisa tres
nodos que ya fueron agregados al árbol resultado (Fig 8).
Figura 8: Nodos seleccionado en segunda llamada recursiva.
El árbol “resultado” quedaría de la siguiente forma al finalizar este paso (Fig 9):
Figura 9: Árbol resultado después de segunda llamada recursiva.
No se generan nuevos nodos sino que se actualizan los totales de los nodos ya existentes.
Es posible realizar un podado en el cual cada rama es independiente de las otras,
manteniendo el resultado, pero esto implica perder gran parte de la compresión del
conjunto de transacciones, que es la principal ventaja de esta estructura.
Una vez completado el algoritmo retornaría el siguiente árbol (Fig 10):
Figura 10: Árbol resultado final.
Algoritmo FPGrowth.
El algoritmo FPGrowth es el proceso de obtención de todos los patrones frecuentes de un
conjunto de transacciones, mediante la generación del árbol FP que comprime las
transacciones y el podado recursivo de éste.
Creación del Árbol FP inicial.
La generación del árbol FP inicial es probablemente la parte más complicada del
algoritmo. Esta se puede dividir en dos fases:
Procesamiento Inicial de Datos.
En esta fase, transformamos el conjunto de transacciones desde el formato usado en la
mayoría de las bases de datos transaccionales, esto es, pares ordenados (transacción,
ítem), a un formato útil para el proceso de llenado de datos, donde cada transacción es un
arreglo ordenado de ítemes, y los datos se guardan en un arreglo ordenado de
transacciones (normalmente de manera lexicográfica).
Los pasos del procesamiento son los siguientes:
1. Calcular soportes de los ítemes: Se calcula la cantidad de veces que aparece
cada ítem en el conjunto.
2. Eliminar ítemes con soporte bajo: Se eliminan las entradas de los ítemes que no
cumplen con el soporte mínimo (si un ítem no cumple con él, no puede ser parte
de un patrón que lo cumpla). Esto se hace con la intención de lograr un llenado
más rápido del árbol.
3. Aplanar transacciones: Se transforman los pares ordenados (transacción, ítem)
en arreglos de ítemes. El identificador de la transacción se descarta.
4. Ordenar transacciones: Se ordenan las transacciones de manera lexicográfica.
Se puede ver un ejemplo claro de cómo se efectúan estos pasos y como transforman
los datos en las applets asociadas a este documento.
Llenado de Árbol FP.
El algoritmo de llenado del Árbol es el siguiente:
public void generarNodos(FPNode padre, int profundidad, Rango rango,
int[][] datos){
while(hayDatos(rango)){
(nuevo_rango, item)= buscar(profundidad, rango, datos);
nNodo=crearNodo(padre,item,rango.size());
generarNodos(nNodo,profundidad+1,nuevo_rango,datos);
}
}
public FPNode crearNodo(FPNode padre, String item, int conteo){
nodo=new FPNode(padre, item, conteo);
lista[item].ultimo().sgte=nodo;
}
Es importante entender que generarNodos y crearNodo son métodos del Árbol FP, es
decir, la información que se genere en ellos será agregada inmediatamente a la instancia
del árbol. El método crearNodo envuelve el proceso de generación de Nodos FP y se
preocupa que cada nodo creado sea añadido a la lista enlazada correspondiente.
El método generarNodos particiona los datos a cada nivel de profundidad y genera el
nodo correspondiente. La finalidad de esto se puede ver en el siguiente ejemplo:
Dado los siguientes datos (Fig 11):
Figura 11: Datos de entrada.
La primera partición elegida seria la siguiente (Fig 12):
Figura 12: Primera partición.
El estado del árbol después de agregar el nodo correspondiente (padre nulo, ítem A,
conteo 3) sería (Fig 13):
Figura 13: Árbol después de la primera partición.
Repitiendo el proceso 3 veces más tenemos (Fig 14):
Figura 14: Cuarta partición.
Después de agregar los nodos correspondientes el árbol es (Fig 15):
Figura 15: Árbol después de cuarta partición.
Repitiendo otras 3 veces (agregamos los nodos E y F con un conteo de 1) y luego
continuamos por la siguiente rama del árbol (Fig 16):
Figura 16: Séptima partición.
Agregando este nodo el árbol queda (Fig 17):
Figura 17: Árbol después de séptima partición.
Se continúa el proceso hasta acabar los datos. El objetivo de esta forma de llenado del
Árbol FP es pasar por cada nodo una sola vez (solo al momento de su creación), dado que
las posibles implementaciones de los Nodos FP hacen costoso encontrar un nodo
particular para actualizar su información (por ejemplo, el nodo E, cuyos antecesores sean
A, B y C).
Podado recursivo.
El algoritmo de podado del árbol es el siguiente:
public void buscarPatrones(String patron,int soporte_minimo, FPTree
patrones){
for(<Cada item contenido en el arbol>){
if(item.soporte()>soporte_minimo){
patrones.add(patron+" "item+" "+item.soporte());
buscarPatrones(patron+" "+item,soporte_minimo,
arbol.podar(item),patrones);
}
}
}
arbol,
Lista
En la primera llamada al método, la variable patrón es un String vacío y la variable
patrones es una lista vacía. El método agregará a la lista cada patrón+ítem que cumpla
con el soporte mínimo y se llamará a sí mismo, con el ítem agregado al patrón inicial,
además de entregar un árbol podado según el ítem correspondiente. Como apreciamos en
la sección podado esto permite encontrar los patrones frecuentes en que participa un ítem
y sus antecesores. Por el hecho de aplicar este algoritmo sobre todos los ítemes del árbol
obtendremos todos los patrones frecuentes en éste.
Compilación y Ejecución de Applets de Ejemplo.
El presente documento va acompañado del archivo fpgrowth.zip, el cual contiene el
código fuente de las applets de ejemplo.
Compilación
Se debe descomprimir el archivo fpgrowth.zip.:
[user@host:~]% unzip fpgrowth.zip
Al descomprimir se deben crear los siguientes directorios:
fpgrowth/
fpgrowth/html/
fpgrowth/bin/
fpgrowth/src/
Luego, se deben compilar los archivos fuente, contenidos en el directorio fpgrowth/src/ y
el resultado debe ser dejado en el directorio fpgrowth/bin/ (Para compilar se requiere JDK
1.4.2):
[user@host:~]% cd fpgrowth
[user@host:~/fpgrowth/]% javac -nowarn -d bin src/*
Una vez realizados estos pasos el programa esta listo para ser ejecutado.
Ejecución.
Para ejecutar basta con abrir el archivo fpgrowth/html/fpgrowth.html en un navegador que
tenga instalado el plugin de Java (preferentemente versión 1.4.2 o superior).