Comencemos por explorar las propiedades algebráicas de una operación común como la suma.
La suma tiene algunas propiedades que son relevantes para el procesamiento de datos, una en particular es la asociatividad,
La razón por la que esto es importante es por que si aplicamos la operación de suma en un sistema distribuido, no importa que las sumas se realicen de forma distribuida siempre y cuando todas las sumas se apliquen eventualmente.
Veamos algunos ejemplos
In [68]:
val a = List(1,2,3,4,5,6)
println(a.reduceLeft(_ + _))
println(a.reduceRight(_ * _))
println(a.reduce((a,b) => if (a > b) a else b))
println(a.reduce((a,b) => if (a < b) a else b))
Ahora supongamos que queremos operar sobre un elemento neutro.
In [8]:
println(a.fold(0)(_ + _))
println(a.fold(1)(_ * _))
println(a.fold(Int.MinValue)((a,b) => if (a > b) a else b))
println(a.fold(Int.MaxValue)((a,b) => if (a < b) a else b))
En este punto es más o menos claro que este principio puede aplicar a otras operaciones, por ejemplo obtener el máximo o el mínimo de un conjunto de datos, etc.
Ahora generalicemos este comportamiento a un objeto abstracto.
In [13]:
trait Monoid[T] {
def zero: T
def plus(a: T, b: T): T
}
object IntMax extends Monoid[Int] {
def zero = Int.MinValue
def plus(a: Int,b: Int) = if (a > b) a else b
}
object IntMin extends Monoid[Int] {
def zero = Int.MaxValue
def plus(a: Int,b: Int) = if (a < b) a else b
}
object IntSum extends Monoid[Int] {
def zero = 0
def plus(a: Int,b: Int) = (a + b)
}
object StringSum extends Monoid[String] {
def zero = ""
def plus(a: String,b: String) = (a + b)
}
Out[13]:
In [15]:
def printSum(a:Seq[Int], op: Monoid[Int]) {
println(a.fold(op.zero)(op.plus))
}
println(a.fold(IntMax.zero)(IntMax.plus))
println(a.fold(IntMin.zero)(IntMin.plus))
println(a.fold(IntSum.zero)(IntSum.plus))
println(a.map(_.toString).fold(StringSum.zero)(StringSum.plus))
El objeto abstracto que definimos es una representación de una estructura algebráica conocida como monoide.
Un semigrupo es un conjunto $M$ que satisface la propiedad de asociatividad bajo la operación $\circ$:
Un monoide es un semigrupo que adicionalmente satisface la propiedad de identidad.
Es importante recalcar la importancia de poder abstraer estas operaciones:
Un grupo es es un monoide que adicionalmente satisface la propiedad de inversa
Un grupo abeliano satisface además la propiedad de conmutatividad
Un anillo es un grupo abeliano sobre la suma que además:
Típicamente se utilizan anillos en computo distribuido es para resolver problemas matriciales, con objetos que no son núméricos.
Un ejemplo de es el uso de un anillo para resolver el problema de caminos más cortos sobre un grafo.
In [ ]:
%libraryDependencies += "org.apache.spark" %% "spark-core" % "0.9.1"
In [16]:
%libraryDependencies += "com.twitter" %% "algebird-core" % "0.5.0"
In [ ]:
%resolvers += "Apache Spark" at "http://repo.maven.apache.org/maven2/"
In [ ]:
%update
A continuación utilizaremos la librería para algebra abstracta Algebird, estos ejemplos provienen de https://github.com/twitter/algebird/wiki/Algebird-Examples-with-REPL
In [ ]:
import com.twitter.algebird._
Comencemos por definir un filtro de Bloom
In [23]:
val NUM_HASHES = 6
val WIDTH = 32
val SEED = 1
val bfMonoid = new BloomFilterMonoid(NUM_HASHES, WIDTH, SEED)
val bf = bfMonoid.create("1", "2", "3", "4", "100")
println(bf.contains("1"))
println(bf.contains("0"))
Ahora sumemos operemos sobre varios filtros "provenientes de otras máquinas"
In [30]:
val bfList = List(bf,
bfMonoid.create("0", "2", "3", "4", "400"),
bfMonoid.create("8.5"))
val bfSum =bfList.reduce(bfMonoid.plus(_,_))
println(bfSum.contains("1"))
println(bfSum.contains("43"))
println(bfSum.contains("0"))
println(bfSum.contains("8.5"))
Apliquemos el conteo de elementos con HyperLogLog
In [2]:
import HyperLogLog._
val hll = new HyperLogLogMonoid(4)
Out[2]:
In [36]:
val data = List(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 2, 2, 8, 9, 11, 30, 40)
val sumHll = data.map(hll(_)).reduce(hll.plus(_,_))
val approxSizeOf = hll.sizeOf(sumHll)
println(approxSizeOf.estimate)
println(data.toSet.size)
Ahora sumemos el resultado "proveniente de cada máquina"
In [42]:
val data2 = List(20, 35, 40)
val sumHll2 = data2.map(hll(_)).reduce(hll.plus(_,_))
Out[42]:
In [43]:
val totalHll = hll.sum(List(sumHll, sumHll2))
Out[43]:
In [46]:
println(hll.sizeOf(totalHll).estimate)
println((data ++ data2).toSet.size)
Un ejemplo más con Min-Hashing
In [47]:
val numHashes = 10
val numBands = MinHasher.pickBands(0.9, numHashes)
val minHasher = new MinHasher32(numHashes, numBands)
Out[47]:
In [51]:
val sig1 = List(1,2,3,4,5).map(minHasher.init(_)).reduce(minHasher.plus)
val sig2 = List(1,2,3,4,7).map(minHasher.init(_)).reduce(minHasher.plus)
val sig3 = List(8,9,2,3,1).map(minHasher.init(_)).reduce(minHasher.plus)
println(minHasher.similarity(sig1, sig2))
println(minHasher.similarity(sig1, sig3))
println(minHasher.similarity(sig2, sig3))
In [66]:
val delta= 1E-5
val eps = 0.001
val seed = 1
val cmsMonoid = new CountMinSketchMonoid(eps, delta, seed)
val data = List(1L, 1L, 2L, 2L, 3L, 3L, 4L, 4L, 5L, 5L, 2L, 2L, 8L, 9L, 11L, 30L, 40L)
val cms = cmsMonoid.create(data)
println(cms.totalCount)
println(data.size)
println(cms.frequency(3L).estimate)
println(cms.frequency(2L).estimate)
Agreguemos los conteos provenientes de otra máquina
In [64]:
val data2 = List(1L, 1L, 2L, 2L, 3L, 3L, 4L, 4L, 5L, 5L, 2L, 2L, 8L, 9L, 11L, 30L, 40L)
val cms2 = cmsMonoid.create(data2)
val totalCms = List(cms, cms2).reduce(cmsMonoid.plus(_,_))
println(totalCms.totalCount)
println(totalCms.frequency(3L).estimate)
println(totalCms.frequency(2L).estimate)
In [ ]: