tonellotto / ircodecs

ircodecs: compresión de enteros aplicada a IR (para Python)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ircodecs: compresión de enteros aplicada a IR (para Python)

ircodecs es un repositorio que permite la compresión de listas de enteros. Es compatible con las versiones 2 y 3 de Python, siendo útil para prototipos o pruebas, o para quien ya posea conocimientos en este lenguaje y desee incursionar en el área de compresión contextualizada en el ámbito de la Recuperación de Información (Information Retrieval).

Motivación

Este repositorio surgió a partir de la realización de un trabajo titulado Esquema Multicompresión para Índices Invertidos de Motores de Búsqueda (https://github.com/gustingonzalez/irmulticompression) en el contexto de la materia Recuperación de Información, de la carrera Licenciatura en Sistemas de Información, dictada en la Universidad Nacional de Luján.

Descripción

Los distintos módulos aquí implementados permiten la codificación y decodificación de listas de enteros con una serie de métodos estado del arte, en concreto: Unario, Gamma, Variable Byte, Empaquetado Binario, Elias Fano (ver notas en el siguiente subapartado), Simple16 y PFor (NewPFD/OptPFD). También contiene un módulo que permite realizar Delta Gaps (gapsencoder.py) y otro que permite tratar secuencias de bytes como arrays de bits de bytes (bitbytearray). A su vez, estas implementaciones se basan en la bibliografía expuesta en la sección de Referencias y en desarrollos ya conocidos para lograr cierto grado de eficiencia (teniendo en cuenta las limitaciones que un lenguaje interpretado supone). Por ejemplo, los desarrollos de Simple16 y PFor, están fuertemente basados en la implementación kamikaze expuesta en el repositorio de @lemire (basada, a su vez, en el repositorio de @javasoze) mientras que, tanto la decodificación de Unario, como de Elias Fano, se fundamentan en la implementación de @catenamatteo.

Acerca de la implementación de Elias Fano: EF Local

Con el fin de mejorar el ratio de compresión de Elias Fano (nativo), la versión aquí implementada tiene una premisa similar a la variación Multinivel presentada en el paper Partitioned Elias Fano Indexes de Ottaviano y Venturini. Sin embargo, dado que esta versión Multinivel descomprime cada partición (chunk) de lista teniendo en cuenta el máximo número de la ith-1 partición, es incompatible con la propuesta del esquema múltiple de compresión en la que originalmente se gestó este repositorio. En efecto, para suplir lo mencionado, dada una secuencia de chunks C pertenecientes a una lista, para cada ci∈C se definen y=ci,1 como el menor elemento del ith chunk, y F con z=min(F2, y)-1, una secuencia creciente que se comprime utilizando Elias Fano. Si se analiza el algoritmo utilizado para computar F, el establecer z como su primer número permite que su codificación no pierda un posible alineamiento en caso de haber definido un tamaño de chunk múltiplo de 8. En adición, el valor de F1 será siempre lo más cercano posible a F2, lo cual tiene sentido si se tiene en cuenta que el ratio de compresión de Elias Fano depende únicamente del mayor elemento de la lista. Finalmente, se define x=y-z, que se comprime utilizando Variable Byte: a esta codificación se concatena F. Por otra parte, VByte también se utiliza en caso de que la lista a comprimir sea de tamaño 1 ya que, para definir F, se requieren como mínimo 2 elementos. En el caso de que y=0, F se define como F=C y x como x=y, de otro modo el valor de z resultaría negativo. Para salvar ineficiencias en la compresión, cuando la secuencia F es densa, esta se comprime utilizando vectores de bits siempre que |F|>u/4 con u=max(F). Para rearmar la lista original, luego de la descompresión de x y de F, simplemente se redefine F1=x+F1 y se adiciona este valor a cada f∈F:f>F1. La ventaja de la variante propuesta, es que cada partición de lista es independiente de las demás.

¿Python v2 o Python v3?

Si bien esta librería es compatible con las versiones 2 y 3 de Python, la primera brinda mejores resultados en términos de eficiencia. Esto se debe a la forma en que se implementa el tipo entero en cada versión del lenguaje. En concreto, mientras que la versión 2 diferencia entre un tipo entero de 64 bits y un long (de tamaño arbitrario), la versión 3 presupone toda variable numérica como long (arbitraria): en efecto las operaciones a nivel bit sobre este último tipo de variables, son más costosas que sobre las primeras. Referencias a este hecho, se pueden encontrar en https://docs.python.org/2/c-api/long.html y https://docs.python.org/3/c-api/long.html.

El siguiente cuadro expone una comparativa de los tiempos de decodificación, especificados en enteros por segundo, en relación a cada versión del lenguaje:

Códec Py2 (2.7.9) Py3 (3.4.2) Δ%
PFD (NewPFD) 1367019,0 1167724,5 -14,6%
Bit Packing 1019746,3 867998,7 -14,9%
EF Local 617343,7 561147,9 -9,1%
Gamma 354815,1 318848,3 -10,1%
Vbyte 3050411,3 2436618,0 -20,1%
Simple16 3044125,2 2328077,2 -23,5%
Unario 658449,6 612273,8 -7,0%

Para las mediciones se ha utilizado el script decodingspeedtest.py, cuya metodología consiste en comprimir, con cada códec, una secuencia de gaps derivada de una lista creciente de 1 millón de enteros, para finalmente obtener el tiempo de cada decodificación. Tener en cuenta que, dado que Elias Fano comprime la lista original utilizando dgaps internamente, para equiparar, el tiempo de decodificación delta se añade a los resultados de los restantes métodos. Esta operación se lleva a cabo 5 veces, aunque también se realiza una inicial (a modo de warm-up), que no se toma en cuenta para el promedio final. Además, por cada repetición, la distancia entre números se duplica. Es decir, en la primera (pertinente a la medición), se prueba la secuencia S=[1, 2, 3], en la segunda S=[1, 3, 5], en la tercera S=[1, 5, 9] y así sucesivamente. El entorno de pruebas utilizado ha sido un Intel® Xeon® X5650 @ 2.67Ghz de 24 núcleos con 32 GB de memoria RAM.

¿Cómo usar? ¡Muy simple!

Suponiendo que se requiere codificar una lista de 128 números...

PFor (NewPFD/OptPFD)

from irencoder import pforencoder

numbers = list(range(1, 129))
encoded = pforencoder.encode(numbers)
decoded = pforencoder.decode(encoded, 128)

Tener en cuenta que el resultado de la codificación es una secuencia enteros.

Simple16

from irencoder import simple16encoder

numbers = list(range(1, 129))
encoded = simple16encoder.encode(numbers)
decoded = simple16encoder.decode(encoded, 128)

Tener en cuenta que el resultado de la codificación es una secuencia enteros.

Empaquetado binario (Bit Packing)

from irencoder import bitpackingencoder

numbers = list(range(1, 129))
encoded, padding = bitpackingencoder.encode(numbers)
decoded = bitpackingencoder.decode(encoded, 128)

Tener en cuenta que el resultado de la codificación es una secuencia bytes.

Elias Fano (Local)

from irencoder import eliasfanoencoder

numbers = list(range(1, 129))
encoded, padding = eliasfanoencoder.encode(numbers)
decoded = eliasfanoencoder.decode(encoded, 128)

Tener en cuenta que el resultado de la codificación es una secuencia bytes.

Unario

Nota preliminar: aunque la implementación ha sido diseñada para comprimir un único número por vez, la codificación de una lista tan sólo requiere la importación de la clase BitByteArray del módulo bitbytearray. Esta funcionalidad no ha sido desarrollada debido a que, en la propuesta del esquema de compresión múltiple en la que se gestó esta librería, esta tarea se lleva a cabo en una capa superior. De todas formas, sería útil su implementación. Tener en cuenta que, alternativamente, se podría utilizar la función write_binary_in_barray(array, offset, number, bits) de bitutils.py: aun así, la clase BitByteArray abstrae la complejidad inherente a las escrituras de secuencias de bits como, por ejemplo, el control de offset (puntero de bit relativo al array de bytes). Por su parte, el uso de write_binary_in_barray(array, offset, number, bits), se recomienda en los casos en los que se utilicen cantidades fijas de bits o en los que se requiera mayor eficiencia en la codificación: por ejemplo, el módulo bitpackingencoder.py utiliza esta función de forma interna tanto para el proceso de codificación, como para el de decodificación.

from irencoder import unaryencoder
from irencoder.bitbytearray import BitByteArray

numbers = list(range(1, 129))
optimized = True  # Unario optimizado

# Encode
encoded = BitByteArray()
for number in numbers:
    e, padding = unaryencoder.encode(number)
    encoded.extend(e, padding)

# Decode
decoded = unaryencoder.decode(encoded, 128, optimized)

También es posible especificar el inicio de la lectura, desde un bit arbitrario. Por ejemplo, en la siguiente sentencia, se leen los 127 números restantes desde el offset 1:

decoded = unaryencoder.decode(encoded, 127, True, offset=1)

Tener en cuenta que no es estrictamente necesario que la función decode reciba un BitByteArray: bien pudiera recibir un array de bytes (bytearray) o de enteros.

Gamma

Nota preliminar: para este códec aplican las mismas observaciones señaladas para Unario.

from irencoder import gammaencoder
from irencoder.bitbytearray import BitByteArray

numbers = list(range(1, 129))

# Encode
encoded = BitByteArray()
for number in numbers:
    e, padding = gammaencoder.encode(number)
    encoded.extend(e, padding)

# Decode
decoded = gammaencoder.decode(encoded, 128)

Tener en cuenta que el resultado de la codificación es una secuencia bytes.

Variable Byte

Nota preliminar: para la decodificación con este método no se requiere el parámetro de la cantidad de números a descomprimir, puesto que se lee la totalidad de los bytes pasados por parámetro. Quizás, a modo de aunar criterios, sería útil la futura implementación de lo descrito.

from irencoder import vbencoder

numbers = list(range(1, 129))

# Encode
encoded = bytearray()
for n in numbers:
    encoded.extend(vbencoder.encode(n))

# Decode
decoded = vbencoder.decode(encoded)

También es posible realizar la lectura de un único número desde un bit especifico. Por ejemplo, en la siguiente sentencia, el valor de number, leído desde el bit 8, es 2 y el nuevo offset, 16:

number, offset = vbencoder.decode_number(encoded, 8)

Nota: como Variable Byte realiza la lectura en grupos de octetos, si el offset no es múltiplo de 8, esta se inicia desde el byte relativo.

Sólo necesito un único códec ¿qué debo tener en cuenta?

En caso de requerir utilizar algún módulo en concreto y de que querer evitar la descarga completa del repositorio, hay que tener en las dependencias internas de cada uno:

Referencias

Este repositorio está basado en diversas lecturas:

  • C. D. Manning, P. Raghavan, H. Schütze. Introduction to Information Retrieval. Cambridge University Press, 2008.
  • M. Catena, C. MacDonald, and I. Ounis. On inverted index compression for search engine efficiency. Lect. Notes Comput. Sci. (including Subser. Lect. Notes Artif. Intell. Lect. Notes Bioinformatics), vol. 8416 LNCS, pp. 359–371, 2014.
  • J. Zhang, X. Long, y T. Suel. Performance of Compressed Inverted List Caching in Search Engine. Proceedings of the 17th international conference on World Wide Web, WWW '08, pp. 387–396, 2008.
  • D. Lemire, L. Boytsov. Decoding billions of integers per second through vectorization. Software: Practice & Experience, Vol. 45 (1), pp. 1-29, 2015.
  • M. Zukowski, S. Heman, N. Nes y P. Boncz. Super-Scalar RAM-CPU Cache Compression. 22nd International Conference on Data Engineering (ICDE'06), pp. 59-59, 2006.
  • G. Ottaviano, R. Venturini. Partitioned Elias-Fano Indexes. Proceedings of the 37th International ACM SIGIR Conference on Research & Development in Information Retrieval, SIGIR ‘14, pp. 273-282, 2014.

También se han utilizado (y se recomiendan) las siguientes implementaciones como referencia:

Requerimientos

Python v2 o Python v3

Autor

Agustín González

About

ircodecs: compresión de enteros aplicada a IR (para Python)


Languages

Language:Python 100.0%