Desencriptando (II): la avaricia es mala
El otro día propuse y resolví un problema de encriptación con R. Utilizaba uno de los llamados métodos avariciosos (o greedy) para hallar el máximo de una función (que era, en esencia, la función de verosimilitud de una determinada permutación de caracteres dentro del espacio probabilístico de todas ellas).
Este método funcionó con una cadena relativamente larga para desencriptar pero falla con otras más cortas. Por ejemplo, con
cadena <-c("u","r","i","b","y","r","l","g","m","h","e","r","y",
"b","g","m","a","c","p","y","c","m","d","r","h","z","y",
"r","e","i","c","l","r","i","n","e","c","t","d","t","c","z",
"c","y","c","v","r","o","d","y","s","e","r","q","c","y","c",
"n","g","q","c","i","g","m","r","y","d","i","v","r")
Si ejecuto el código que presenté el otro día,
quijote <- readLines( "http://www.gutenberg.org/cache/epub/2000/pg2000.txt", encoding = "UTF-8" )
tmp <- sapply( quijote, function(x) strsplit(x, ""))
tmp <- do.call( c, tmp )
tmp <- tolower(tmp)
tmp[ tmp == "á" ] <- "a"
tmp[ tmp == "é" ] <- "e"
tmp[ tmp == "í" ] <- "i"
tmp[ tmp == "ó" ] <- "o"
tmp[ tmp == "ú" ] <- "u"
tmp <- tmp[tmp %in% letters]
names(tmp) <- NULL
b <- as.factor(tmp)
b.from <- b[-length(b)]
b.to <- b[-1]
res <- tapply( b.to, b.from, table )
res <- do.call( rbind, res ) + 1
res <- res / rowSums(res)
m <- res
markov <- function( x, m ){
sum(log( m[ cbind(x[-length(x)], x[-1] ) ] ) )
}
p.0 <- markov( cadena, m )
while( TRUE ){
cadena.alt <- factor( cadena )
cambiar <- sample( nlevels(cadena.alt), 2 )
levels.alt <- levels( cadena.alt )
char.tmp <- levels.alt[cambiar[1]]
levels.alt[cambiar[1]] <- levels.alt[ cambiar[2] ]
levels.alt[cambiar[2]] <- char.tmp
levels( cadena.alt ) <- levels.alt
cadena.alt <- as.character( cadena.alt )
p.1 <- markov( cadena.alt, m )
if( p.1 > p.0 ){
print( c(p.0, p.1) )
cadena <- cadena.alt
p.0 <- p.1
print( cadena ); flush.console()
}
}
obtengo
hostrocunylortunzagranioyprolsacosqlabibaparadovirelomaraqumasunorisdo
que es, ciertamente, bastante ininteligible. Nótese, sin embargo, que casi puede leerse en español: respeta de alguna manera la secuencia de caracteres habituales en nuestra lengua. Pero no significa realmente nada.
Nota: ciertamente, por diseño, el código no termina propiamente, pero uno puede observar que se acaba estabilizándose en esa cadena. no termina (no tiene condición de fin porque no me entretuve en eso) pero que saca por pantalla las progresivas versiones cada vez más probables de cuál pudo ser la cadena original, al cabo de un rato se detiene en la bastante ininteligible
Lo que propongo hoy es una pequeña modificación del algoritmo del otro día para que, en lugar de buscar un óptimo, navegue por entre las permutaciones más probables con la esperanza de que la óptima (y, probablemente, la original) sea una de ellas. El código queda así:
quijote <- readLines( "http://www.gutenberg.org/cache/epub/2000/pg2000.txt", encoding = "UTF-8" )
tmp <- sapply( quijote, function(x) strsplit(x, ""))
tmp <- do.call( c, tmp )
tmp <- tolower(tmp)
tmp[ tmp == "á" ] <- "a"
tmp[ tmp == "é" ] <- "e"
tmp[ tmp == "í" ] <- "i"
tmp[ tmp == "ó" ] <- "o"
tmp[ tmp == "ú" ] <- "u"
tmp <- tmp[tmp %in% letters]
names(tmp) <- NULL
b <- as.factor(tmp)
b.from <- b[-length(b)]
b.to <- b[-1]
res <- tapply( b.to, b.from, table )
res <- do.call( rbind, res ) + 1
res <- res / rowSums(res)
m <- res
markov <- function( x, m ){
exp(sum(log( m[ cbind(x[-length(x)], x[-1] ) ] ) ))
}
intercambiar.dos.letras <- function(cadena){
cadena.alt <- factor( cadena, levels = letters )
cambiar <- sample( nlevels(cadena.alt), 2 )
levels.alt <- levels( cadena.alt )
char.tmp <- levels.alt[cambiar[1]]
levels.alt[cambiar[1]] <- levels.alt[ cambiar[2] ]
levels.alt[cambiar[2]] <- char.tmp
levels( cadena.alt ) <- levels.alt
as.character( cadena.alt )
}
p.0 <- markov( cadena, m )
contador <- numeric(0)
for (i in 1:100000){
cadena.alt <- intercambiar.dos.letras(cadena)
p.1 <- markov( cadena.alt, m )
azar <- runif(1)
if( p.1 > p.0 | azar < p.1 / p.0 ){
cadena <- cadena.alt
p.0 <- p.1
a <- paste(cadena, collapse ="")
print( c(p.0, p.1) ); print(a)
contador[a] <- ifelse( is.na(contador[a]), 1, contador[a] + 1)
}
}
sort(contador)
Las cadenas más probables (las asociadas a un contador más elevado), son
fermueviclteumichaquacoelbuetraverstayoyabauadegountejauasijariceuorde
wentrelosmuertosyagrasiembreunalenpuacicabaradevirqueharapohanoserinde
gentrecospuertosyabrasieplreunacenmuavivalaradefirqueharamohanoserinde
wentrelosmuertosfagrasiembreunalenpuacicabaradevirqueharapohanoserinde
yentrecosmuertoshafrasiembreunacenpuadidabarajevirquelarapolanoserinje
bentrelosmuertosyafrasiempreunalenguacicaparadevirqueharagohanoserinde
ferilemosnteliospaylasuenzletramerctabubazaladequlgtehalacoharoselurde
kentrelosmuertosyabrasiempreunalencuagigaparadevirqueharacohanoserinde
jentrelosmuertosyabrasiempreunalenguacicaparadevirqueharagohanoserinde
y no sé si mis lectores reconocerán en ellas una frase de Galdós que aparecía en tiempos en los billetes de 1000 pesetas.
Dista de ser perfecto, pero como que, más o menos, podemos sentirnos satisfechos con la descencriptación.
En la próxima entrega de esta serie daré un pequeño rodeo y dejaré para la última la teoría de la cosa.