1. Caso Real Big Data
Análisis de weblogs de empresa de
contenidos multimedia online
2. Problemática
● Empresa que pretende mejorar su servicio de
películas online mediante un motor de
recomendaciones personalizadas al usuario,
teniendo en cuenta su historial de descargas,
eventos, etc.
● La única información con la que contamos son
los logs del último año en formato JSON
{"created_at": "2013-05-08T08:00:00Z", "payload": {"item_id": "11086", "marker":
3540}, "session_id": "b549de69-a0dc-4b8a-8ee1-01f1a1f5a66e", "type": "Play",
"user": 81729334, "user_agent": "Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X)
AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A405"}
3. Requisitos básicos
● El equipo desea:
1) Conocer cuales son los contenidos consumidos en
mayor medida por usuarios jóvenes
2) Identificar patrones de conducta de los usuarios
en sus sesiones, con el objetivo de mejorar la
experiencia/usabilidad de la página web
3) Aplicar un motor de recomendaciones a su web
que ayude a los usuarios a encontrar el contenido que
buscan, con el objetivo de que visiten más el portal
4. ¿Cómo abordamos el problema?
● Como expertos en Big Data, dividimos el
problema en:
Clasificación binaria Clústering Análisis predictivo
Se clasifican los usuarios en dos
grupos (adulto, niño) para ofrecer
contenidos acordes al grupo al
que pertenecen
Agrupación de usuarios según
su comportamiento
Recomendador de películas
basado en la idoneidad de un
item concreto para un usuario
específico
5. ¿Cómo funciona el servicio online?
● Debemos entender cual es el funcionamiento natural del
servicio ofertado por la empresa
– El usuario paga una suscripción para poder acceder a los
contenidos online
– Una vez logueado en el sistema, se le presenta al usuario una
serie de recomendaciones y contenidos populares
– El usuario puede reproducir el contenido online de su cola,
buscar nuevos contenidos, y reproducirlos directamente o
añadirlos a la cola de reproducción
– La página soporta puntuaciones y reviews
– El usuario puede realizar otras operaciones como pagos y
gestión de contraseñas
7. Explorando los datos
● Partimos de los weblogs
● Además, se nos dice que:
– Los logs contienen eventos de control parental que
pueden ser utilizados para identificar si algunas cuentas
son utilizadas por adultos o niños
– El campo “marker” indica la posición del reproductor en el
contenido
– Las puntuaciones van de 1 a 5 (5 es el máximo)
– Identificadores de contenido que contienen una 'e' son
series de television ('e' de “episode”)
● El número que le sigue indica el número de episodio
8. Explorando los datos
– Cuando el contenido alcanza la marca “end” se
registra un evento “stop”
● Si el usuario abandona la página o cambia de contenido
puede que no se registre el evento
– La mayor parte de los usuarios residen en EEUU
{"auth": "1208d4c:279737f7", "createdAt": "2013-05-12T00:00:11-08:00", "payload":
{"itemId"": "3702e4", "marker": 780}, "refId": "7586e549", "sessionID": "d4a244cb-
d502-4c94-a80d-3d26ca54a449", "type": "Play", "user": 18910540, "userAgent":
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.2; SLCC2; .NET
CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0;
InfoPath.2)"}
9. Explorando los datos
● A parte del “Play” que hemos visto en el
ejemplo, ¿qué otros eventos existen?
● Usamos Hadoop streaming para poder pasarle
un script que filtre los resultados
grep: filtra los resultados de acuerdo al
patrón indicado
E: activa las expresiones regulares
o: hace que 'grep' saque únicamente
(output) la porción del input que cumple el
patrón
La expresión regular está dividida en 3 partes por el elemento
pipe |
""$1": [^,]+" $1 hace referencia al argumento 1 que le
pasemos (en este caso será “type” para
comprobar qué eventos existen)
cut -d: -f2- cortar delimitando (-d) por ':' el campo 2
tr -d '" ' borra (-d) el caracter “
#!/bin/bash
grep -Eo ""$1": [^,]+" | cut -d: -f2- | tr -d '" '
10. Explorando los datos
● Lanzamos el script con Hadoop Streaming
-D: evitamos que grep devuelva 1 si no encuentra nada
-stream.non.zero.exit.is.failure=false: Por defecto es true
-input: datos de entrada (directorio o ficheros) para el mapper
-ouptut: directorio de salida para el reducer
-mapper/-reducer: qué ejecutar como map/reduce
-file: indica a Hadoop que se asegure de que el fichero es accesible
desde todos los nodos que van a ejcutar la tarea
uniq -c es un reducer que agrega los campos y nos devuelve la cantidad
de repeticiones
$ hadoop jar $STREAMING -D stream.non.zero.exit.is.failure=false -input data/heckle/
-input data/jeckle/ -output types -mapper "grep_field.sh type" -file grep_field.sh
-reducer "uniq -c"
Filtrado por
campo type
11. Explorando los datos
● Visualizamos la salida
– 19 eventos
Antes dijimos que el
fichero de salida se iba a
llamar type. Así es como
HDFS gestiona los
resultados
12. Explorando los datos
● Visualizamos un ejemplo de weblog de cada
evento
– Saldrán 19 elementos JSON completos, cada uno
con un “type” diferente
$ hadoop fs -cat types/part* | awk '{ print $2 }' | xargs -n 1 grep -Rhm 1 data -e
-cat y awk para extraer los type únicos
Xargs y grep para encontrar una ocurrencia
de una línea que contengan cada tipo único
13. Explorando los datos
● Comprobamos la estructura de los datos de los
que disponemos
– ¿Comparten la misma estructura?
● Necesitamos un MapReduce que pueda manejar datos
JSON
● summary_map.py #!/usr/bin/python
import json
import sys
# Read all lines from stdin
for line in sys.stdin:
data = json.loads(line)
for field in data.keys():
print field
Parseamos el JSON
Mostramos cada campo
14. Explorando los datos
● Ejecutamos el script
Problema: hay datos con
campos que no coinciden
15. Explorando los datos
● Algunos JSON están mal formados y tienen
varias dobles comillas consecutivas
– Arreglamos el script
#!/usr/bin/python
import json
import sys
for line in sys.stdin:
try:
data = json.loads(line.replace('""', '"'))
for field in data.keys():
print field
except ValueError:
# Log the error so we can see it
sys.stderr.write("%sn" % line)
exit(1)
16. Explorando los datos
● Resultado
Problemas:
- Campos iguales con diferente nombre
- Campos que no aparecen siempre
17. Explorando los datos
● Volvemos a editar el script
para evitar confusiones
– Simplemente
normalizamos los campos
“problemáticos”
#!/usr/bin/python
import json
import sys
for line in sys.stdin:
try:
data = json.loads(line.replace('""', '"'))
for field in data.keys():
if field == 'type':
print "%s" % (data[field])
else:
# Normalize the file name
real = field
if real == 'user_agent':
real = 'userAgent'
elif real == 'session_id':
real = 'sessionID'
elif real == 'created_at' or real == 'craetedAt':
real = 'createdAt'
# Emit the normalized field
print "%s:%s" % (data['type'], real)
# Emit all subfields, if there are any
if type(data[field]) is dict:
for subfield in data[field]:
print "%s:%s:%s" % (data['type'], real, subfield)
except ValueError:
sys.stderr.write("%sn" % line)
exit(1)
20. Explorando los datos
● Volvemos a editar el script
...
for line in sys.stdin:
try:
...
elif real == 'created_at' or real == 'craetedAt':
real = 'createdAt'
# Emit all subfields, if there are any
if type(data[field]) is dict:
print "%s:%s" % (data['type'], real)
# Normalize and print the subfields
for subfield in data[field]:
subreal = subfield
if subreal == 'item_id':
subreal = 'itemId'
print "%s:%s:%st%s" % (data['type'], real, subreal, data[field][subfield])
else:
# Emit the normalized field
print "%s:%st%s" % (data['type'], real, data[field])
except ValueError:
sys.stderr.write("%sn" % line)
exit(1)
Este cambio aborda dos problemas.
1. Normaliza los subcampos
2. Añade el valor del campo y del
subcampo a la salida
21. Explorando los datos
● Tenemos que hacer un resumen de los
datos → REDUCE
– Resumir los valores de campos y subcampos
– Identificar cada valor de campo (numérico,
fecha…)
– Valores categóricos (pocas posibilidades para
un campo)
– Identificadores sin patrón aparente
23. Explorando los datos
● Resumen de los datos
– Identificadores de usuario entre 1000000 y 1000000
– No todos los Item ids son numéricos (de lo contrario no apaecerían como
identificadores)
– La media para los eventos de puntuaciones (Rating) es de 3.5, y para las
reviews (WriteReview) de 4
– Los timestamps no están todos en la misma zona. Algunos están en UTC
otros en UTC-8. Puede que haya otros. Tener en cuenta a la hora de
limpiar los datos
– Account tiene 3 posibles sub-acciones: updatePassword,
updatePaymentInfo, parentalControl
– Account:playload → no hay control parental que deshabilite opciones
(extraño)
– Play:playload → Faltan algunos valores de ytemID y marker (en ambos
casos, aparecen 543129)
24. Explorando los datos
● Número de usuarios diferentes y sesiones
– 2195 usuarios
– 5308 sesiones
$ hadoop jar $STREAMING -D stream.non.zero.exit.is.failure=false -input data/heckle/
-input data/jeckle/ -output users -mapper "grep_field.sh user" -file grep_field.sh -reducer
'bash -c "uniq | wc -l"'
...
$ hadoop fs -cat users/part*
2195
$ hadoop jar $STREAMING -D stream.non.zero.exit.is.failure=false -input data/heckle/
-input data/jeckle/ -output sessions -mapper 'grep_field.sh session(ID|_id)' -file
grep_field.sh -reducer 'bash -c "uniq | wc -l"'
…
$ hadoop fs -cat sessions/part*
5308
26. Limpiando los datos
● Tenemos que solucionar los problemas que
hemos ido identificando en la fase de
preparación
● Vamos a reducir el volumen de los datos para
hacerlos más manejables
● La idea es tener un único fichero con un único
registro agregado por reducer
– Mapper
27. Limpiando los datos
● Mapper → clean_map.py
● Reducer → clean_reduce.py (con modificación
de playload por estar vacío)
Limpiamos el directorio (se
supone que hemos
lanzado antes un reduce
con error)
Volvemos a lanzar
nuestro MapReduce
personalizado
¡Éxito! Ahora tenemos un
único fichero limpio y
agregado con cada registro
conteniendo sesiones
completas
28. Limpiando los datos
● Visualizamos el resultado
● ¿Qué hemos conseguido?
– Logs reducidos a 4 MB
30. 3.1 Extrayendo contenido de los
items reproducidos
● Necesitamos extraer el contenido reproducido, puntuado o
revisado (review) por cada usuario
– Mapper que nos permita agregar por usuario pero también diferenciar
por fecha
● Sacaremos una clave compuesta que contendrá el id del usuario, la fecha de
inicio de sesión, y la fecha de fin
– Reducer
● Las claves que recibe el Reducer están agregadas por usuario y agrupadas
por fecha
● Tratamiento:
– Si vemos una etiqueta nunca vista → adoptamos la nueva
– Si vemos una etiqueta repetida → nada cambia
– Si vemos una etiqueta conflictiva → lo tratamos como un usuario diferente
– Si no vemos nunca una etiqueta de un usuario → usuario sin etiqueta
– Para usuarios etiquetados como adultos → añadimos una 'a'
– Para usuarios etiquetados como niños → añadimos una 'k'
31. 3.1 Extrayendo contenido de los
items reproducidos
● Mapper
– kid_map.py
#!/usr/bin/python
import json
import sys
def main():
# Read all lines from stdin
for line in sys.stdin:
data = json.loads(line)
# Collect all items touched
items = set()
items.update(data['played'].keys())
items.update(data['rated'].keys())
items.update(data['reviewed'].keys())
# Generate a comma-separated list
if items:
itemstr = ','.join(items)
else:
itemstr = ','
# Emit a compound key and compound value
print "%s,%010d,%010dt%s,%s" % (data['user'], long(data['start']),
long(data['end']), data['kid'], itemstr)
if __name__ == '__main__':
main()
34. 3.2 Preparando el algoritmo
SimRank
● En nuestro caso tenemos:
– 2000 usuarios que han visto 0 o más contenidos
– 8500 items que han sido vistos por 0 o más usuarios
● Grafo bipartido
● Solución basada en grafos
– Las etiquetas conocidas para los usuarios se propagarán
por los items que han visualizado, y de ahí a otros
usuarios que hayan visualizado el mismo contenido
● Lo mismo para comentarios y puntuaciones
– Progagación influenciada → SimRank
35. 3.2 Preparando el algoritmo
SimRank
● SimRank necesita una matriz de adyacencia y
una lista de nodos
36. 3.3 Construyendo la matriz
● Mapper → item_map.py
#!/usr/bin/python
import sys
def main():
# Read all lines from stdin
for line in sys.stdin:
key, value = line.strip().split('t')
items = value.split(',')
# Emit every item in the set paired with the user ID
for item in items:
print "%st%s" % (item, key)
if __name__ == '__main__':
main()
37. 3.3 Construyendo la matriz
● Reducer → item_reduce.py
#!/usr/bin/python
import sys
def main():
last = None
# Read all lines from stdin
for line in sys.stdin:
item, user = line.strip().split('t')
if item != last:
if last != None:
# Emit the previous key
print "%st%s" % (last, ','.join(users))
last = item
users = set()
users.add(user)
# Emit the last key
print "%st%s" % (last, ','.join(users))
if __name__ == '__main__':
main()
38. 3.3 Construyendo la matriz
● Lanzamos el job
– Tenemos una matriz de adyacencia que
implementa SimRank
39. 3.4 Implementación de SimRank
● Mapper → simrank_map.py
– Lo único que tiene que hacer es leer el vector
SimRank y calcular el producto de la matriz con la
matriz de adyacencia
● Primero lee del vector SimRank de HDFS y lo guarda en
un diccionario obteniendo las columnas de la matriz de
adyacencia como registros de entrada
● Para cada entrada distinta de cero en una columna, se
multiplica por la correspondiente entrada en el vector
SimRank y se emite el resultado con la etiqueta de fila
como clave
40. 3.4 Implementación de SimRank
● Mapper → simrank_map.py
#!/usr/bin/python
import sys
def main():
if len(sys.argv) < 2:
sys.stderr.write("Missing args: %sn" % ":".join(sys.argv))
sys.exit(1)
v = {}
# Read in the vector
with open(sys.argv[1]) as f:
for line in f:
(key, value) = line.strip().split("t")
v[key] = float(value)
# Now read the matrix from the mapper and do the math
for line in sys.stdin:
col, value = line.strip().split("t")
rows = value.split(',')
for row in rows:
try:
# Add the product to the sum
print "%st%.20f" % (row, v[col] / float(len(rows)))
except KeyError:
# KeyError equates to a zero, which we don't need to output.
pass
if __name__ == '__main__':
main()
41. 3.4 Implementación de SimRank
● Reducer → simrank_reduce.py
– Se suman los valores intermedios de cada fila, se
suma la “teleport contribution” y se emite el
resultado final como el valor de la fila en un nuevo
vector SimRank
● El “teleport contribution” se obtiene extrayendo del
training set
42. 3.4 Implementación de SimRank
● Testeando la convergencia → simrank_diff.py
● Lanzador → simrank.sh
$ chmod a+x simrank.sh
$ ./simrank.sh adults_train
rm: v: No such file or directory
rm: `v': No such file or directory
Beginning pass 1
/tmp/hadoop-training/hadoop-unjar2754804698549065604/]
...
Beginning pass 2
...
Así hasta 24
iteraciones
Ojo a los permisos
de ejecución
43. 3.4 Implementación de SimRank
$ hadoop fs -cat simrank24/part* | head
10081e1 0.00001761487440064474
10081e10 0.00000278115643514088
10081e11 0.00000278115643514088
10081e2 0.00001761487440064474
10081e3 0.00001761487440064474
10081e4 0.00001761487440064474
10081e5 0.00001761487440064474
10081e7 0.00000278115643514088
10081e8 0.00000278115643514088
10081e9 0.00000278115643514088
cat: Unable to write to output stream.
Vector SimRank
final
44. 3.4 Implementación de SimRank
● Movemos el resultado a adult_final y repetimos
el proceso, esta vez con kids
$ hadoop fs -mv simrank24 adult_final
$ hadoop fs -rm -R simrank*
$ ./simrank.sh kids_train
rm: v: No such file or directory
rm: `v': No such file or directory
Beginning pass 1
…
$ hadoop fs -mv simrank24 kid_final
Así hasta 24
iteraciones
45. 3.4 Implementación de SimRank
● Interpretamos y comparamos los vectores
SimRank obtenidos en 2 pasos:
1) Normalizamos ambos vectores
2) Comparamos cada entrada y asignamos una
etiqueta basada en el valor más alto
46. 3.4 Implementación de SimRank
1) Normalizando vectores
● Mapper → adult_map.py
#!/usr/bin/python
import sys
def main():
if len(sys.argv) < 3:
sys.stderr.write("Missing args: %sn" % sys.argv)
# Calculate conversion factor
num_adults = float(sys.argv[1])
num_kids = float(sys.argv[2])
factor = -num_adults / num_kids
# Apply the conversion to every record and emit it
for line in sys.stdin:
key, value = line.strip().split('t')
print "%st%.20f" % (key, float(value) * factor)
if __name__ == "__main__":
main()
48. 3.4 Implementación de SimRank
1) Normalizando vectores
● Lanzamos los jobs
$ hadoop fs -cat adults_train | wc -l
84
$ hadoop fs -cat kids_train | wc -l
96
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -input adult_final -output adult_mod -mapper "adult_map.py 84 96" -file
adult_map.py
...
$ hadoop jar $STREAMING -D mapred.textoutputformat.separator=, -input adult_mod -input kid_final -output final -reducer
combine_reduce.py -file combine_reduce.py -inputformat org.apache.hadoop.mapred.KeyValueTextInputFormat
…
Formato separado por comas
49. 3.4 Implementación de SimRank
1) Normalizando vectores
● Testeamos los resultados
● Los IDs de los adultos terminan con 'a'
● Los IDs de los niños terminan con 'k'
$ hadoop fs -cat final/part* | grep a, | grep -v ,0
$ hadoop fs -cat final/part* | grep k, | grep -v ,1
Haciendo grep por cada entrada de
adulto ('a') y a continuación por
aquellos que no incluyen el valor ',0'
encontramos todas las entradas de
adulto mal etiquetadas.
Lo mismo para kids
Los comandos no devuelven nada,
luego hemos obtenido el resultado
esperado
51. 3.4 Implementación de SimRank
1) Normalizando vectores
Using the scoring methodology from the challenge, this solution scores an accuracy of 99.64%, mislabeling a total of 9
records.
53. 4. Clusterizando sesiones
● El segundo desafío es clusterizar las sesiones de usuario basadas
en propiedades (features) en los datos
●
Mediante el clústering obtenemos qué grupos de sesiones son
notablemente más similares entre ellos que hacia otras sesiones
●
Súper importante elegir bien las propiedades o features
●
El análisis de clúster nos aportará
– El número de grupos naturales existentes en los datos y las propiedades
– Qué sesiones pertenecen a cada grupo
54. 4. Clusterizando sesiones
● El proceso a seguir para análisis de clúster es
básicamente heurístico (prueba y error)
– Iremos probando algunas propiedades y acotando
aquellas que mejores resultados arrojen
55. 4.1 Eligiendo propiedades
● ¿Qué propiedades podemos sacar de los datos
disponibles?
$ hadoop fs -cat clean/part* | head -1
{"session": "2b5846cb-9cbf-4f92-a1e7-b5349ff08662", "hover": ["16177",
"10286", "8565", "10596", "29609", "13338"], "end": "1368189995", "played":
{"16316": "4990"}, "browsed": [], "recommendations": ["13338", "10759",
"39122", "26996", "10002", "25224", "6891", "16361", "7489", "16316",
"12023", "25803", "4286e89", "1565", "20435", "10596", "29609", "14528",
"6723", "35792e23", "25450", "10143e155", "10286", "25668", "37307"],
"actions": ["login"], "reviewed": {}, "start": "1368189205", "recommended":
["8565", "10759", "10002", "25803", "10286"], "rated": {}, "user": "10108881",
"searched": [], "popular": ["16177", "26365", "14969", "38420", "7097"], "kid":
null, "queued": ["10286", "13338"], "recent": ["18392e39"]}
cat: Unable to write to output stream.
Propiedades extraíbles:
● Actions (a feature for each,
except login and logout)
● Number of items hovered over
● Session duration (end)
● Number of items played
● Number of items browsed
● Number of items reviewed
● Number of items rated
● Number of items searched
● Number of recommendations
that were reviewed
● Kid (parental controls)
● Number of items queued
56. 4.1 Eligiendo propiedades
● Podemos calcular otras propiedades menos directas:
– Mean play time
– Shortest play time
– Longest play time
– Total play time
– Total play time as fraction of session duration
– Longest play: less than 5 minutes, between 5 and 60 minutes, more than 60 minutes
– Shortest play: less than 5 minutes, between 5 and 60 minutes, more than 60 minutes
– Number of items played less than 5 minutes
– Number of items played more than 60 minutes
– Number of items hovered over that were played
– Number of browsed items that were played
– Number of reviewed items that were played
– Number of recommended items that were played
– Number of rated items that were played
– Number of searched items that were played
– Number of popular items that were played
– Number of queued items that were played
– Number of recent items that were played
– Number of recent items that were reviewed
– Number of recent items that were rated
57. 4.2 Fusionando los datos
● Tenemos que parsear los datos y generar los
vectores de propiedades
– Usaremos Python de nuevo
– Vamos a volver a unir los datos de sesión
separados en el apartado anterior
58. 4.2 Fusionando los datos
– Para volver a fusionar los datos:
● Mapper → merge_map.py
#!/usr/bin/python
import re
import sys
def main():
p = re.compile('.*"session": "([^"]+)".*')
# Read all the lines from stdin
for line in sys.stdin:
m = p.match(line)
if m:
print "*%s*t*%s*" % (m.group(1), line.strip())
else:
sys.strerr.write("Failed to find ID in line: *%s*" % line)
if __name__ == '__main__':
main()
Utilizamos una expresión regular en
lugar de parsear el JSON porque la
expresión consume menos
recursos
59. 4.2 Fusionando los datos
– Para volver a fusionar los datos:
● Reducer → merge_reduce.py
60. 4.2 Fusionando los datos
– Lanzamos el job
$ hadoop jar $STREAMING -mapper merge_map.py -file merge_map.py
-reducer merge_reduce.py -file merge_reduce.py -input clean -output merged
61. 4.3 Generamos los vectores de prop
● Mapper → features_map.py
#!/usr/bin/env python
import sys
import json
def main():
# Read all lines from stdin
for line in sys.stdin:
session = json.loads(line)
fields = []
fields.append(session['session'])
fields.append('updatePassword' in session['actions'])
fields.append('updatePaymentInfo' in session['actions'])
fields.append('verifiedPassword' in session['actions'])
fields.append('reviewedQueue' in session['actions'])
fields.append(session['kid'])
played = set(session['played'].keys())
fields.append(len(played))
print ','.join(map(str, fields))
if __name__ == '__main__':
main()
62. 4.3 Generamos los vectores de prop
● Lo lanzamos
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D
mapred.textoutputformat.separator=, -D stream.map.output.field.separator=, -mapper
features_map.py -file features_map.py -input merged -output features0
...
$ hadoop fs -getmerge features0 features.csv
$ hadoop fs -put features.csv
63. 4.4 Construimos el workflow de ML
de Cloudera
● Generamos el fichero de cabeceras
– headers.csv
session_id,identifier
updatePassword,categorical
updatePaymentInfo,categorical
verifiedPassword,categorical
reviewedQueue,categorical
kid,categorical
num_plays
64. 4.4 Construimos el workflow de ML
de Cloudera
● Creamos el fichero de resumen utilizando las
cabeceras generadas
● Normalizamos las características/propiedades
– 2 opciones: unit normal o rango
– Salida en Avro porque vamos a acceder a los datos
normalizados programáticamente
$ ml summary --summary-file summary.json --header-file header.csv --format
text --input-paths features.csv
$ ml normalize --summary-file summary.json --format text --id-column 0
--transform Z --input-paths features.csv --output-path part2normalized --output-
type avro
65. 4.5 K-means++ sketch
● Utilizando ls datos normalizados
$ ml ksketch --format avro --input-paths part2normalized --output-file
part2sketch.avro --points-per-iteration 1000 --iterations 10 --seed 1729
66. 4.5 K-means++ sketch
● Especificamos la semilla (seed)
– De esta forma podemos comparar los resultados a
través de múltiples ejecuciones
● Sin una semilla en cada ejecución se seleccionaría un
conjunto de centroides aleatorios, produciendo
resultados que podrían ser arbitrariamente mejores o
peores que anteriores ejecuciones
67. 4.5 K-means++ sketch
● Especificamos la semilla (seed)
– Especificamos la semilla para poder comparar los
resultados y ejecutamos el algoritmo paralelo k-
means en los sketch de datos
$ ml kmeans --input-file part2sketch.avro --centers-file part2centers.avro
--clusters 40,60,80,100,120,140,160,180,200 --best-of 3 --seed 1729 --num-
threads 1 --eval-details-file part2evaldetails.csv --eval-stats-file
part2evalstats.csv
La lista de clústers indica los
diferentes tamaños de clúster
que va a intentar el algoritmo
El parámetro best-of le
indica a Cloudera ML que
ejecute el algoritmo 3 veces
por cada tamaño del clúster
Indicamos 1 único hilo, puesto
que la MV está configurada con
un procesador únicamente
La ejecución puede tardar bastante,
mejor asignar a la MV otro procesador
y poner 2 hilos en paralelo
68. 4.5 K-means++ sketch
● Analizamos el resultado
– Buscamos una predictive strength
de al menos 0.8 con buenos
números de estabilización
– Predective strength es una métrica
que indica cuánto de bien
describen los datos los clústers
– Las métricas de estabilidad indican
cuánto cambian los clústers y los
puntos de datos entre los datos de
entrenamiento y los de test
● Nos da 1.0, lo cual es perfecto
69. 4.5 K-means++ sketch
● Añadimos características al mapper
#!/usr/bin/env python
import sys
import json
def main():
# Read all lines from stdin
for line in sys.stdin:
session = json.loads(line)
fields = []
fields.append(session['session'])
fields.append('updatePassword' in session['actions'])
fields.append('updatePaymentInfo' in session['actions'])
fields.append('verifiedPassword' in session['actions'])
fields.append('reviewedQueue' in session['actions'])
fields.append(session['kid'])
session_duration = (long(session['end']) - long(session['start']))
fields.append(session_duration)
played = set(session['played'].keys())
browsed = set(session['browsed'])
hovered = set(session['hover'])
queued = set(session['queued'])
recommendations = set(session['recommendations'])
rated = set(session['rated'].keys())
reviewed = set(session['reviewed'].keys())
searched = set(session['searched'])
fields.append(len(played))
fields.append(len(browsed))
fields.append(len(hovered))
fields.append(len(queued))
fields.append(len(recommendations))
fields.append(len(rated))
fields.append(len(reviewed))
fields.append(len(searched))
print ','.join(map(str, fields))
if __name__ == '__main__':
main()
72. 4.5 K-means++ sketch
● Analizamos el resultado
– Tenemos una predective
strength muy baja
● Infinity es malo
– Esto indica que los clústers
no son suficientemente
diferentes
– ¡¡Prueba y error!!
● Quitamos algunas
características añadidas en el
paso anterior y volvemos a
probar
73. 4.5 K-means++ sketch
● Comentamos características del mapper
#!/usr/bin/env python
import sys
import json
def main():
# Read all lines from stdin
for line in sys.stdin:
session = json.loads(line)
fields = []
fields.append(session['session'])
fields.append('updatePassword' in session['actions'])
fields.append('updatePaymentInfo' in session['actions'])
fields.append('verifiedPassword' in session['actions'])
fields.append('reviewedQueue' in session['actions'])
fields.append(session['kid'])
# session_duration = (long(session['end']) - long(session['start']))
# fields.append(session_duration)
played = set(session['played'].keys())
# browsed = set(session['browsed'])
# hovered = set(session['hover'])
# queued = set(session['queued'])
recommendations = set(session['recommendations'])
rated = set(session['rated'].keys())
reviewed = set(session['reviewed'].keys())
searched = set(session['searched'])
fields.append(len(played))
# fields.append(len(browsed))
# fields.append(len(hovered))
# fields.append(len(queued))
fields.append(len(recommendations))
fields.append(len(rated))
fields.append(len(reviewed))
fields.append(len(searched))
print ','.join(map(str, fields))
if __name__ == '__main__':
main()
74. 4.5 K-means++ sketch
● Eliminamos algunas características del fichero
de cabeceras
session_id,identifier
updatePassword,categorical
updatePaymentInfo,categorical
verifiedPassword,categorical
reviewedQueue,categorical
kid,categorical
num_plays
num_recommendations
num_rated
num_reviewed
num_searched
76. 4.5 K-means++ sketch
● Analizamos el resultado
– Mejores resultados que
antes
– Podemos probar a volver a
añadir alguna de las
características que hemos
borrado en el paso anterior
77. 5. Prediciendo las valoraciones de
los usuarios
(construyendo un recomendador)
78. 5. Recomendador
● Tenemos que predecir valoraciones para un
conjunto dado de usuarios e items
● ¿Qué opciones tenemos?
– Construirnos nuestro recomendador
matemáticamente
– Taste (Mahout) para experimentar con sus
algoritmos
79. 5. Recomendador
● Revisando los datos tenemos:
– 926 valoraciones
● De 751 usuarios sobre 757 items
– Unos 500.000 eventos que pueden considerarse como
“Play”
● El propio “Play”
● Añadir a la cola de reproducción
● Si los sumamos tenemos 2193 usuarios y 6504 items reproducidos
– Vamos a trabajar únicamente con las valoraciones explícitas
e implícitas
80. 5.1 Preparando los datos
● Mahout solo acepta entradas en .csv
– Vamos a necesitar 2 entradas
● Valoraciones explícitas
● Valoraciones implícitas → Eventos tipo “play” para cada usuario e item
● Generamos los ficheros (3 opciones):
1) JSON loader en Pig para cargar los datos y extraer los campos
correctos
2) Definir una tabla Hive usando JSON SerDe y seleccionar los campos
deseados de la tabla
3) Tarea de streaming map-only en Hadoop
• Es la opción más sencilla por la facilidad de parsear JSON en Python sin
necesidad de definir un schema
81. 5.1 Preparando los datos
● Mapper para datos explícitos
– Recorremos todas las valoraciones y extraemos el
userID, itemID y tripleta de valoración y las
emitimos si cumplen el criterio dado
#!/usr/bin/python
import datetime
import dateutil.parser
import json
import sys
def main():
before = sys.argv[1] == 'before'
cutoff = dateutil.parser.parse(sys.argv[2])
epoch = datetime.datetime(1970,1,1,tzinfo=cutoff.tzinfo)
# Read all lines from stdin
for line in sys.stdin:
data = json.loads(line)
start = cutoff - datetime.timedelta(seconds=long(data['start']))
if (before and start > epoch) or (not before and start <= epoch):
for item, rating in data['rated'].iteritems():
print "%s,%s,%s" % (data['user'], item, rating)
for item, dict in data['reviewed'].iteritems():
print "%s,%s,%s" % (data['user'], item, dict['rating'])
if __name__ == '__main__':
main()
Para evitar problemas en
Python que son
dependientes de la versión y
complicadas
82. 5.1 Preparando los datos
● Mapper para datos implícitos
– Recorremos las valoraciones, reviews, películas
reproducidas, encoladas y buscadas
#!/usr/bin/python
import json
import sys
def main():
# Read all lines from stdin
for line in sys.stdin:
data = json.loads(line)
for item in data['rated'].keys() + data['reviewed'].keys() +
data['played'].keys() + data['browsed'] + data['queued']:
print "%s,%s" % (data['user'], item)
if __name__ == '__main__':
main()
83. 5.1 Preparando los datos
● Ejecutamos ambos
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D mapred.textoutputformat.separator=, -D
stream.map.output.field.separator=, -mapper 'explicit.py before "2013-05-11 00:00:00-08:00"' -file explicit.py
-input clean -output explicit_train
...
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D mapred.textoutputformat.separator=, -D
stream.map.output.field.separator=, -mapper 'explicit.py after "2013-05-11 00:00:00-08:00"' -file explicit.py
-input clean -output explicit_test
...
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D mapred.textoutputformat.separator=, -D
stream.map.output.field.separator=, -mapper implicit.py -file implicit.py -input clean -output implicit
$ hadoop fs -cat explicit_train/part* | head
10142325,9614,5
10760746,27597,4
10796192,30975e17,3
10905688,36598,3
10905688,15337,5
1091145,14303e57,1
11657116,9146e38,4
11749679,27017e20,4
11751432,38193e81,4
11764279,21482e41,3
$ hadoop fs -cat implicit/part* | head
10108881,16316
10108881,10286
10108881,13338
10108881,9107
10108881,39122
10142325,9614
10142325,9614
10142325,38579
10151338,34645
10151338,34645
Mahout solo permite
identificadores numéricos
→ Hay que traducirlos
84. 5.1 Preparando los datos
● Traducimos los itemID
– ¿Cómo de grande es el problema?
– La 'e' pertenece únicamente a los itemIDs, y aparecía en aquellos
que se referían a episodios de series de TV
$ hadoop fs -cat implicit/part* | cut -d, -f1 | tr -d '1234567890' | sort | uniq
$ hadoop fs -cat implicit/part* | cut -d, -f2 | tr -d '1234567890' | sort | uniq
e
cut -d, -f1: Cortar por columna 1 en la coma
tr -d '123456789': eliminar de f1 los números
sort: para ordenar la salida
uniq: no repetir en la salida (nos saldrían
muchas 'e')
85. 5.1 Preparando los datos
● Traducimos los itemID
– Determinemos el rango de episodios
$ hadoop fs -cat implicit/part* | cut -d, -f2 | cut -de -f1 | sort -n | head -1
1094
$ hadoop fs -cat implicit/part* | cut -d, -f2 | cut -de -f1 | sort -n | tail -1
39984
cut -d, -f2: Cortar por columna 2 en la coma
cut -d, -f1: De la nueva columna, cortar por la 'e'
sort -n: ordenar numéricamente
head -1: mostrar el primero
tail -1: mostrar el último
86. 5.1 Preparando los datos
● Traducimos los itemID
– Determinemos número de episodios
– No parece que vayan a dar problemas
● Traducimos la 'e' por '00'
$ hadoop fs -cat implicit/part* | cut -d, -f2 | grep e | cut -de -f2 | sort -n | uniq | head
1
2
3
4
5
6
7
8
9
10
$ hadoop fs -cat implicit/part* | cut -d, -f2 | grep e | cut -de -f2 | sort -n | tail -1
217
87. 5.1 Preparando los datos
● Traducimos los itemID
– Utilizamos 'sed'
– Ya tenemos un conjunto de entrenamiento y test
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D
mapred.textoutputformat.separator=, -D stream.map.output.field.separator=, -mapper 'sed
"s/e/00/"' -input explicit_train -output explicit_train_clean
...
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D
mapred.textoutputformat.separator=, -D stream.map.output.field.separator=, -mapper 'sed
"s/e/00/"' -input explicit_test -output explicit_test_clean
...
$ hadoop jar $STREAMING -D mapred.reduce.tasks=0 -D
mapred.textoutputformat.separator=, -D stream.map.output.field.separator=, -mapper 'sed
"s/e/00/"' -input implicit -output implicit_clean
...
88. 5.2 Establecer los promedios base
● Necesitamos establecer unas bases contra las
cuales evaluar el recomendador
● La métrica más sencilla sería calcular el
promedio global para todos nuestros datos de
test
– Se puede hacer de forma simple en Hive
$ hive
hive> create external table explicit_train (user int, item bigint, rating int) row format delimited
fields terminated by ',' location '/user/cloudera/explicit_train_clean';
OK
Time taken: 0.215 seconds
hive> create external table explicit_test (user int, item bigint, rating int) row format delimited
fields terminated by ',' location '/user/cloudera/explicit_test_clean';
OK
Time taken: 0.191 seconds
89. 5.2 Establecer los promedios base
● Una vez tenemos las tablas, podemos utilizar
las funciones matemáticas de Hive para
calcular el promedio y el RMSE (root-mean-
square-deviation)
hive> select avg(rating) from explicit_train;
...
OK
3.597826086956522
Time taken: 6.132 seconds
hive> select sqrt(sum(pow(rating - 3.597826086956522, 2))/count(*)) from explicit_test;
...
OK
1.2733209628271343
Time taken: 4.222 seconds
90. 5.2 Establecer los promedios base
● Comparamos el promedio con las puntuaciones
de los usuarios
– El promedio global es muy superior a los promedios
de los usuarios → datos muy dispersos
hive> create table baseline (user int, item bigint, rating float) row format delimited fields
terminated by ',';
OK
Time taken: 1.099 seconds
hive> insert into table baseline select explicit_test.user, explicit_test.item, if
(avg.avg_rating > 0, avg.avg_rating, 3.597826086956522) from (select user, avg(rating)
as avg_rating from explicit_train group by user) avg full outer join explicit_test on
explicit_test.user == avg.user;
...
OK
Time taken: 7.462 seconds
hive> select sqrt(sum(pow(e.rating - b.rating, 2))/count(*)) from baseline b join
explicit_test e on b.user == e.user and b.item == e.item;
OK
1.362417948479424
Time taken: 4.768 seconds
91. 5.3 Recomendadores de Mahout
● Dos clases de recomendadores en Mahout:
– Basados en similaridad
● El algoritmo determina la similaridad entre usuarios e
items y estima valoraciones desconocidas basándose en
esas similitudes y valoraciones conocidas
– Factorización matricial
● ¿Cual escogemos?
– Prueba y error
● Se puede utilizar la línea de comandos, pero es
más recomendable usar el API Java de Mahout
92. 5.3 Recomendadores de Mahout
● Trabajamos en local → Nos descargamos los
datos
$ hadoop fs -getmerge implicit_clean implicit.csv
$ hadoop fs -cat explicit_train_clean/part* explicit_test_clean/part* > explicit.csv
Con merge volvemos a juntar ambos
conjuntos de datos (train y test)
93. 5.3 Recomendadores de Mahout
● Preparamos el proyecto Maven
● Configuramos el pom.xml →
$ mvn archetype:create
-DarchetypeGroupId=org.apache.maven.archetypes
-DgroupId=ccp.challenge1.recommend -DartifactId=recommend
Debemos tener especial cuidado en donde se quedan descargados los csv, ya
que la clase Java que los accederá hace un ../fichero.csv
final DataModel ratingDataModel = new FileDataModel(new File("../explicit.csv"));
final DataModel allDataModel = new FileDataModel(new File("../implicit.csv"));
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-core</artifactId>
<version>0.7</version>
</dependency>
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-math</artifactId>
<version>0.7</version>
</dependency>
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-math</artifactId>
<version>0.7</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-utils</artifactId>
<version>0.5</version>
</dependency>
94. 5.3 Recomendadores de Mahout
● Compilamos y ejecutamos
$ mvn compile
…
BUILD SUCCESS
...
$ mvn exec:java
-Dexec.mainClass="ccp.challenge1.recommender.Recommend"
…
Item-item with Tanimoto: 1.2142
Item-item with log-likelihood: 1.2151
SVD with EM: 1.2284
SVD with ALS: 1.9169
SVD with implicit linear regression: NaN
Slope One: 0.7175
User-user n-nearest neighbor with Tanimoto: NaN
User-user n-nearest neighbor with log-likelihood: NaN
User-user threshold with Tanimoto: NaN
User-user threshold with log-likelihood: NaN
NaN: not a number (no
hay suficientes datos)
Mejor que el promedio
base que obtuvimos
anteriormente
Mejor resultado
96. 5.4 Generando ratings
● Refinamos los algoritmos
– Ahora que hemos elegido el algoritmo (Slope One)
podemos generar las valoraciones
– Recalculamos el promedio global para todo el
dataset
$ hive
hive> SELECT avg(IF(a.rating IS NULL, b.rating, a.rating)) AS avg FROM explicit_train a FULL
OUTER JOIN explicit_test b ON (a.user == b.user and a.item == b.item);
...
OK
3.683585313174946
Time taken: 7.881 seconds
97. 5.4 Generando ratings
● Creamos el recomendador
– FinalRecommend.java
public class FinalRecommend {
private static final Float GLOBAL_AVERAGE = 3.6836f;
public static void main(String[] args) throws Exception {
DataModel explicitDataModel = new FileDataModel(new
File("../explicit.csv"));
Recommender recommender = new
SlopeOneRecommender(explicitDataModel);
PrintWriter out = new PrintWriter("../Task3Solution.csv");
File in = new File("../rateme.csv");
for (String testDatum : new FileLineIterable(in)) {
String[] tokens = testDatum.split(",");
String itemIdString = tokens[1];
long userId = Long.parseLong(tokens[0]);
long itemId = Long.parseLong(itemIdString);
float estimate;
try {
estimate = recommender.estimatePreference(userId, itemId);
} catch(NoSuchUserException e) {
estimate = GLOBAL_AVERAGE;
} catch(NoSuchItemException e) {
estimate = GLOBAL_AVERAGE;
}
if (Float.isNaN(estimate)) {
estimate = GLOBAL_AVERAGE;
}
if (itemId > 50000) {
int i = itemIdString.lastIndexOf("00");
itemIdString = itemIdString.substring(0, i) + 'e' +
itemIdString.substring(i+2);
}
out.printf("%d,%s,%.4fn", userId, itemIdString, estimate);
}
out.close();
}
}
99. 5.4 Generando ratings
● Analizamos los resultados
$ wc -l ../Task3Solution.csv
1000 ../Task3Solution.csv
$ head ../Task3Solution.csv
91059173,6155,3.6836
34025317,39419,3.6836
15309904,11248,3.6836
60633959,18963,3.6836
31917316,36814,3.6836
53736706,26212,3.6836
33961447,22606,3.6836
93036062,39815,3.6836
93165942,32989,3.6836
91328973,26806,3.6836
Los 10 primeros resultados
utilizan el promedio global
calculado antes en Hive
100. 5.4 Generando ratings
● Para saber cuantos
● Sobre el 85% de los ratings generados utilizan
el promedio global calculado
– Dado el esparcimiento de los datos no es un mal
resultado
$ grep 3.6836 ../Task3Solution.csv | wc -l
852