Thursday, June 20, 2013

Técnicas LSH (Locality Sensitive Hashing)


En I2Talentia necesitamos buscar relaciones en grandes cantidades de datos, habitualmente textos que componen los distintos cursos. La búsqueda de relaciones entre datos es un asunto ampliamente estudiado en el ámbito de la Ingeniería del Software. Cuando el volumen de datos es relativamente pequeño, es sencillo encontrar estas relaciones mediante técnicas convencionales, basadas en la comparación e iteración de los elementos entre sí. Sin embargo, cuando el volumen de datos es considerable este tipo de aproximaciones no son adecuadas ya que el tiempo de procesamiento es excesivo e inaceptable.

Una de las técnicas empleadas para la resolución de este tipo de problemas es la estrategia Locality Sensitive Hashing (LSH) basada en la asociación de un mismo Hash a datos similares frente al comportamiento clásico de una función Hash, que evita colisiones entre los datos de entrada. Por lo tanto, con LSH elementos similares se clasificarán dentro del mismo “cubo” con una alta probabilidad. Es importante la apreciación de "alta probabilidad" ya que existe la posibilidad de que dos elementos muy parecidos caigan en cubos distintos por lo que a la hora de usar técnicas de LSH debe tenerse esto en cuenta.

La formulación matemática del problema es bastante compleja de manera que mediante ejemplos gráficos sencillos trataré de explicar brevemente en qué consiste la técnica LSH. En ningún caso trato de cubrir todos los aspectos de LSH, es simplemente un ejemplo de aplicación.

Imaginemos una nube de puntos que definidos como un par de coordenadas (x,y) para los que necesitamos encontrar aquellos puntos que son similares (próximos en distancia) a uno dado. Comparar iterativamente las distancias entre el punto objetivo y el resto es aceptable, como anteriormente comenté, cuando el número de puntos es relativamente pequeño. Sin embargo, si tuviéramos una nube de millones de ellos, esta solución no es nada eficiente.

Para abordar este problema usando la estrategia LSH, deberemos dividir el espacio dimensional (plano en este caso) en distintas regiones. En este ejemplo dividiré el plano en cuadrículas de igual tamaño aunque sería válida cualquier otro tipo de división.

Tal como se muestra en la figura, cada punto está contenido en una y sólo una cuadrícula. Si necesitamos obtener los puntos similares respecto a uno dado, simplemente identificaríamos todos aquellos que están en la misma cuadrícula o dicho de otro modo, aquellos que tienen el mismo valor de Hash.

A continuación describiré el procedimiento para analizar la similitud de un punto respecto a otro dato. Para ello utilizaré el concepto de vector cuyas coordenadas indicarán si un punto sobrepasa un determinado lado de estas cuadrículas en las que he dividido el espacio.



Al punto identificado en la figura de color verde, le asignaríamos un valor de Hash 11101100 en el que las primeras cuatro cifras se corresponderían con valores asociados al eje X y las siguientes cuatro al eje Y. El razonamiento para calcular el Hash de un punto es el siguiente:

Respecto al eje X, el punto verde está más a la derecha que los lados 1,2,3 y a la izquierda del lado 4, por tanto su valor sería 1110.

Siguiendo un razonamiento similar para el eje Y, el punto verde está más abajo de los lados 1,2 y más arriba de los lados 3, 4 siendo por tanto su valor 1100.

Uniendo ambos valores resultaría el valor del Hash 11101100.

De este modo, queda claro que los tres puntos rojos de la figura que están dentro de la misma cuadrícula compartirán el mismo valor de hash 11001100.

Si creamos un índice inverso que relacione hashes con los puntos ya tenemos todo lo necesario.

El índice inverso de este ejemplo sería:

11001100 = punto rojo 1, punto rojo 2, punto rojo 3
11101100 = punto verde
11111111 = punto negro

Si necesitásemos obtener los puntos parecidos a un nuevo punto muy cerca de los tres puntos rojos, simplemente calcularíamos el valor del Hash de este punto que, con alta probabilidad, sería también 11001100. Finalmente, a través del índice inverso obtendríamos los puntos rojos 1,2 y 3.

Tal como se observa esta operación es realmente ligera aún cuando el volumen de datos fuese enorme, millones o incluso billones de puntos.

Con este mecanismo, también sería posible calcular la similitud entre puntos que pertenezcan a distintas cuadrículas haciendo uso de lo que se denomina la distancia de Hamming que no es más que el número de posiciones en los que dos series (vectores) tienen distintos valores.

Por ejemplo, ¿cómo de distintos son los puntos rojos?

Punto rojo 1 = 11001100
Punto rojo 2 = 11001100
Punto rojo 3 = 11001100


La distancia de Hamming es 0 ya que todas las posiciones de los tres puntos tienen el mismo valor. Por tanto, decimos que los puntos son "iguales" en el sentido de que pertenecen a la misma cuadrícula.

La diferencia entre un punto rojo y el punto verde es:
Punto rojo 1 = 11001100
Punto verde  = 11101100

Sólo difieren en la tercera posición por lo que su distancia es 1.

La diferencia entre un punto rojo y el punto negro es:
Punto rojo 1 = 11001100
Punto negro  = 11111111

Difieren en la tercera, cuarta, séptima y octava posiciones por lo que su distancia es 4.

Claramente un punto rojo es más parecido al punto verde (distancia 1) que a al punto negro (distancia 4).

Ahora bien, ¿para qué puede ser útil buscar la similitud entre puntos? Bueno, a raíz de esta pregunta no se me ocurren demasiadas aplicaciones. Sin embargo, vamos a aplicar inteligencia lateral y darle la vuelta a la pregunta, ¿qué cosas podría querer comparar que puedan modelarse como puntos? Ahora sí que puedo ver aplicaciones prácticas.

Si por ejemplo en vez de X le llamo edad y en vez de Y le llamo nota, un punto podría ser una abstracción de un alumno de una determinada edad y una nota media = (edad, nota). Si parto de un alumno del que conozco su edad y su nota media y necesitara obtener todos los alumnos que tienen edades similares (no necesariamente la misma) y notas similares (no necesariamente la misma), podríamos aplicar los mismos conceptos vistos anteriormente para obtenerlos de forma rápida y sencilla.

Además, no tenemos por qué quedarnos en un espacio bidimensional, podemos usar el número de coordenadas que necesitemos, por ejemplo (x,y,z) o (x,y,z,h). Podemos por ejemplo, clasificar alumnos en función de la edad, nota y color de pelo o incluso de más categorías: edad, nota, color de pelo y nivel de estudios de sus padres. Los conceptos y aplicación de LSH serían los mismos.

Wednesday, May 29, 2013

Saber y Sabiduría

Después de un largo tiempo, retomo el blog aprovechando el comienzo de mi nuevo proyecto en el que me encuentro inmerso: I2Talentia, una plataforma de aprendizaje adaptativo muy interesante e innovadora.

Para el desarrollo de esta plataforma me encontré el pasado fin de semana con la necesidad de calcular ciertos parámetros de una elipse. Después de años en el colegio y tras estudiar una ingeniería en la universidad, estaba completamente en blanco, no sabía ni siquiera cómo empezar. No me quedó más remedio que empezar a tirar de Google para empezar a recordar: coordenadas polares, derivadas, métodos numéricos, etc. Conceptos que aprendí en su día perfectamente pero que ya tenía olvidados, entre otros motivos, por no usarlos. Esto me llevó a pensar en que pasamos años estudiando cosas que al final se terminan olvidando porque no se usan en la vida real o porque realmente no aprendes, simplemente memorizas conceptos, reglas y fórmulas que el día del examen sueltas como un papagayo y se acabó, via libre para el olvido.

Para mí, en el sistema educativo actual, lo que se enseña es a Saber. Es decir, te enseñan a usar ciertas fórmulas y técnicas para resolver unos determinados tipos de problemas. Esto no dista mucho de memorizar. De ahí las típicas quejas de los alumnos, "para qué me sirve esto", "no entiendo por qué, pero sé que tengo que resolverlo así", etc.

Por tanto, el alumno adquiere Saber, pero no tiene Sabiduría. La sabiduría es un paso más, es saber por qué las cosas son cómo son, saber cómo aplicar lo que has aprendido, saber extrapolar a otros ámbitos de la vida que pueden no tener nada que ver. Cuando tienes sabiduría te das cuenta realmente que todas las piezas del puzzle encajan y que el engranaje funciona perfectamente. Si en las escuelas se enseñara Sabiduría, la motivación y nivel de aprendizaje aumentaría sustancialmente permitiendo a la larga que la gente aproveche esa Sabiduría para innovar, algo realmente necesario en los días que nos ha tocado vivir.

Saturday, June 16, 2012

Ggravity Opensource

After a year in the Apple Store, I decided to release the source code of Ggravity.

You can download it from Ggravitysoft


Enjoy it!

Saturday, February 26, 2011

Tools for the Objective Assessment of Psicophysicological States

My final degree project: Tools for the Objective Assessment of Psicophysicological States.

I defended this project in 2004 at ETS of Telecommunication Engineering (Málaga University), graduating with Honours.

It is a C/C++ framework to implement real time processing systems focused to study mental states through the analysis of ECG (Electro Cardiography), Photopletismography and GSR (Galvanic Skin Response)

This is the book in spanish (sorry, but I wish I had time to translate 450 pages)

This is the presentation (PowerPoint)

This is a zip with the sources, binaries, documents, test signals, etc.

Some videos:
  • ECG signal and two processors, one to detect the pulses and the other one to calculate the AR spectrum of the HRV (Heart Rate Variability) - Video
  • Photopletismography signal and two processors, one to detect the pulses and the other one to calculate the AR spectrum of the HRV (Heart Rate Variability) - Video
  • Use of the Wizard to create real time processors - Video

Tuesday, February 8, 2011

Nemo of Persia

This is a J2ME mobile game I developed during a master degree in 2005.






You can download de source code of this great game from: NemoPersia

I developed it using the J2ME Wireless Toolkit v2.2 so download it and play!



Sunday, February 6, 2011

Asociaciones en Grails (GORM)

A continuación voy a hacer un pequeño resumen de las asociaciones más típicas que podemos hacer en GRAILS mediante GORM.

1.- Asociaciones Unidireccionales

1.1.- Muchos a uno (m:1) sin relación de pertenencia

class Face {
Nose nose
}

class Nose {
}

En este caso la foreign key va en la tabla padre referenciando a la tabla hija.
insert into nose (id, version) values (null, ?)
insert into face (id, version, nose_id) values (null, ?, ?)

Aunque parezca que la relación es uno a uno (1:1), realmente es muchos a uno ya que una instancia Face sólo puede referenciar a una instancia Nose sin embargo una instancia Nose puede estar referenciada por muchas instancias Face distintas ya que no se está indicando ninguna restricción en este sentido.

Como la tabla hija no pertenece a la padre, es decir no se ha especificado belogsTo, los saves y deletes no se propagarán desde la tabla padre a la hija.

Es decir,

def face = new Face(nose:new Nose())
face.save()

generará una excepción porque al no propagarse los saves la instancia Nose no se persitirá no pudiéndose persistir por tanto la instancia de Face al referenciar a un Nose que no existe a nivel de base de datos (transient).

En este caso es necesario hacer

def nose = new Nose()
nose.save()

def face = new Face(nose:nose)
face.save()

Al invocar a nose.save(), estamos diciendo que la instancia, que en ese momento es de tipo transient, pase a ser persistent y gestionada por la sesión. Esto no quiere decir que en ese momento se grabe realmente en base de datos.

Hibernate grabará en base de datos los objetos de la sesión en base a unas reglas, por ejemplo siempre que termine de generarse una vista. Si necesitamos que en un preciso momento, un save se refleje en base de datos deberemos hacer obj.save(flush:true)

Por la misma razón, el borrado de una instancia Face no borrará su Nose asociada.

Sin embargo sí que se podrá actualizar una instancia Nose desde una Face que la referencie. Por ejemplo:

def face = Face.get(1)
face.nose.cualquierPropiedad=valor
face.save()

En este caso, aún no especificandose belongsTo en la clase Nose, los cambios hechos se persistirán en base de datos. El motivo es que al invocar a

face.nose.cualquierPropiedad=valor

se lee la instancia Nose de base de datos (queda en estado persistent) y se actualiza en memoria por lo que cuando se persiste la sesión, se guardan los cambios de Nose en base de datos.

1.2.- Muchos a uno (m:1) con relación de pertenencia

class Face {
Nose nose
}
class Nose {
static belongsTo = Face
}

En este caso también la foreign key va en la tabla padre referenciando a la tabla hija.
insert into nose (id, version) values (null, ?)
insert into face (id, version, nose_id) values (null, ?, ?)

Mediante la clausula belongsTo estamos especificando que una instancia de Nose pertenece a una instancia de Face. Las implicaciones son que los saves y deletes son propagados desde el padre al hijo. Es decir, podemos crear una instancia Face con una Nose transient:

def face = new Face(nose:new Nose())
face.save()

De igual forma, un delete sobre una instancia de Face borrará también la instancia Nose a la que referencia.

1.3.- Uno a uno (1:1) sin relación de pertenencia
Para modelar una relación muchos a uno (m:1) simplemente deberemos tomar una relación muchos a uno (m:1) y hacer que la referencia de la parte muchos sea única:
class Face {
Nose nose
static constraints = { nose unique: true }
}
class Nose {
}
En este caso también la foreign key va en la tabla padre referenciando a la tabla hija.
insert into nose (id, version) values (null, ?)
insert into face (id, version, nose_id) values (null, ?, ?)
Ahora una instancia Face sólo puede referenciar a una instancia Nose y una instancia Nose sólo puede estar en una Face al declararse unique.

Esta forma tan rara y poco intuitiva de modelar relaciones 1:1 viene heredada de Hibernate.

Al no especificarse relación de pertenencia con belongsTo los saves y deletes no son propagados desde la instancia padre a la hija.

1.4.- Uno a uno (1:1) con relación de pertenencia

class Face {
Nose nose
static constraints = { nose unique: true }
}
class Nose {
static belongsTo = Face
}
En este caso también la foreign key va en la tabla padre referenciando a la tabla hija.
insert into nose (id, version) values (null, ?)
insert into face (id, version, nose_id) values (null, ?, ?)

En este caso al especificarse relación de pertenencia con belongsTo los saves y deletes son propagados desde la instancia padre a la hija.

1.5.- Uno a muchos (1:m) sin relación de pertenencia
class Author {
static hasMany = [ books : Book ]
}
class Book {
String title
}

En este caso, a nivel de base de datos esta relación se modela mediante una tabla auxiliar (join)

Siempre, en las relaciones uno a muchos (1:m) por defecto se propagan los saves desde la clase padre a la hija aunque no se haya especificado relación de pertenencia con belongsTo. En este caso, como no se ha especificado una relación de pertenencia (belongsTo) si borramos la clase padre no se borrará la hija referenciada.

En las relaciones uno a muchos, lo más normal es añadir instancias en la clase padre mediante el método dinámico addTo.

Por ejemplo:

aAuthor.addToBooks(title:"Mi Libro")
aAuthor.save()

Debemos tener cuidado en este caso ya que a nivel de base de datos, no se persistiría inmediatamente la lista de los libros de un autor aunque invoquemos a save. En este caso si leemos de base de datos justo despues de hacer el save no obtendremos ningún registro:

println Book.list()*.title
El motivo es que Hibernate decide que en ese momento no es necesario persistir a base de datos posponiendolo para más adelante. Para hacer que en ese momento los datos queden registrados en base de datos es necesario invocar a save con flush:true

Sin embargo, en las relaciones 1:1 y m:1 que hemos visto una vez que hacemos:

def author = new Author(new Book())
author.save
las instancias Book y Author son creadas en base de datos por lo que una query a base de datos devolverían estos nuevos registros.

La razón es que en la relación 1:m, una instancia de la clase padre (Author) puede referenciar a cero o N libros (Book) mientras que en la relación 1:1 o m:1 una instancia de la clase padre tiene que referenciar necesariamente a un libro ya que la foreign key está en la tabla padre. Por lo tanto, en el primer caso Hibernate puede posponer la inserción en base de datos para más tarde mientras que en el segundo caso la inserción debe realizarse en el momento.

2.- Asociaciones Bidireccionales

2.1.- Uno a uno (1:1) con FK desde padre a hijo
class Face {
Nose nose
}
class Nose {
static belongsTo = [face:Face]
}

En este caso la foreign key va en la tabla padre referenciando a la tabla hija.

insert into nose (id, version) values (null, ?)
insert into face (id, version, nose_id) values (null, ?, ?)

Resaltar que la clausula belongsTo es distinta a la de las asociaciones unidireccionales, en este caso en la clase Nose se indica una propiedad face de tipo Face (face:Face) que es la referencia hacia atrás entre Nose y Face.

Al especificarse una relación de pertenencia, los saves y deletes son propagados desde el padre al hijo.

2.2.- Uno a uno (1:1) con FK desde hijo a padre
class Face {
Nose nose
static hasOne = [nose:Nose]
}
class Nose {
Face face
}

Este caso es idéntico al anterior salvo que la foreign key va en la tabla hija referenciando a la tabla padre.
insert into face (id, version) values (null, ?)
insert into nose (id, version, face_id) values (null, ?, ?)

Resaltar que no tiene sentido definir hasOne en la clase padre y belongsTo en la clase hija ya que estaríamos indicando cómo almacenar la FK de forma contradictoria.

2.3.- Uno a muchos (1:m)
class Author {
static hasMany = [ books : Book ]
}
class Book {
static belongsTo = [author:Author]
}
Este caso es idéntico a la relación unidireccional 1:m salvo que la hemos hecho bidireccional creando una asociación entre Book y Author mediante una clausula de pertencia belongsTo a través de la propiedad author de la clase Book.

Ahora, al haberse especificado una relación de pertenencia, si borramos la clase padre se borrará también la clase hija referenciada.

A nivel de base de datos esta relación se modela mediante una tabla auxiliar (join)

Imaginemos que tenemos una asociación bidireccional entre dos entidades y que en la clase hija existen dos propiedades del tipo de la clase padre:
// Esto no funciona
class Airport {
static hasMany = [flights:Flight]
}
class Flight {
Airport departureAirport
Airport destinationAirport
}

Este ejemplo no funcionaría porque no se sabe cuál de las dos propiedades de Flight: departureAirport o destinationAirport correspondería a la referencia inversa entre Airport y Flight. Es decir, desde un Airport conocemos todos los vuelos mediante la relación hasMany llamada flights, sin embargo desde un vuelo (instancia Flight) no sabríamos a qué aeropuerto pertenece ya que existen dos propiedades que apuntan a Airport.

Para solventar esto es necesario mapear en la clase padre la propiedad de la hija que va a corresponderse con la referencia inversa.

class Airport {
static hasMany = [flights:Flight]
static mappedBy = [flights:"departureAirport"]
}
class Flight {
Airport departureAirport
Airport destinationAirport
}
Ahora una instancia de Airport tienen muchos vuelos (flights), y desde un vuelo podemos conocer desde qué aeropuero salió mediante la propiedad departureAirport.

2.4.- Muchos a muchos (m:m)
class Book {
static belongsTo = Author
static hasMany = [authors:Author]
}
class Author {
static hasMany = [books:Book]
}
Las relaciones muchos a muchos (m:m) se modelan a nivel de base de datos mediante una tabla join.

Esta tabla join no puede ser modelada para añadir propiedades de la relación entre la clase padre y la hija que podamos necesitar.

Por esta razón, a veces cuando pensamos en relaciones muchos a muchos, es más conveniente pensar en dos relaciones uno a muchos apuntando la parte de los muchos a una nueva entidad donde podremos almacenar información extra aunque este consejo quizás no aplique a nuestra relación entre Author y Book ya que es demasiado simple.

Friday, October 29, 2010

Sondasplorer - A bytecode instrumentalization framework (current state)

Here it is a small presentation about the current state of Sondasplorer, the framework to inspect J2EE apps that I am developing in my very sparse free time.

Sondasplorer Presentation in english
Presentación de Sondasplorer en español