Perl, 2 · 70525 + 326508 = 467558
Vaticinador
$m=($u=1<<32)-1;open B,B;@e=unpack"C*",join"",<B>;$e=2903392593;sub u{int($_[0]+($_[1]-$_[0])*pop)}sub o{$m&(pop()<<8)+pop}sub g{($h,%m,@b,$s,$E)=@_;if($d eq$h){($l,$u)=(u($l,$u,$L),u($l,$u,$U));$u=o(256,$u-1),$l=o($l),$e=o(shift@e,$e)until($l^($u-1))>>24}$M{"@c"}{$h}++-++$C{"@c"}-pop@c for@p=($h,@c=@p);@p=@p[0..19]if@p>20;@c=@p;for(@p,$L=0){$c="@c";last if" "ne pop@c and@c<2 and$E>99;$m{$_}+=$M{$c}{$_}/$C{$c}for sort keys%{$M{$c}};$E+=$C{$c}}$s>5.393*$m{$_}or($s+=$m{$_},push@b,$_)for sort{$m{$b}<=>$m{$a}}sort keys%m;$e>=u($l,$u,$U=$L+$m{$_}/$s)?$L=$U:return$d=$_ for sort@b}
Para ejecutar este programa, necesita este archivo aquí , que debe nombrarse B
. (Puede cambiar este nombre de archivo en la segunda instancia del carácter B
anterior). Consulte a continuación cómo generar este archivo.
El programa utiliza una combinación de modelos de Markov esencialmente como en esta respuesta del usuario 2699 , pero con algunas pequeñas modificaciones. Esto produce una distribución para el siguiente personaje. Utilizamos la teoría de la información para decidir si aceptar un error o gastar bits de almacenamiento en B
sugerencias de codificación (y si es así, cómo). Usamos codificación aritmética para almacenar de manera óptima bits fraccionales del modelo.
El programa tiene una longitud de 582 bytes (incluida una nueva línea final innecesaria) y el archivo binario B
tiene una longitud de 69942 bytes, por lo que, según las reglas para la puntuación de varios archivos , calificamosL
como 582 + 69942 + 1 = 70525.
El programa casi seguramente requiere una arquitectura de 64 bits (little-endian?). Se tarda aproximadamente 2,5 minutos en ejecutarse en una m5.large
instancia en Amazon EC2.
Código de prueba
# Golfed submission
require "submission.pl";
use strict; use warnings; use autodie;
# Scoring length of multiple files adds 1 penalty
my $length = (-s "submission.pl") + (-s "B") + 1;
# Read input
open my $IN, "<", "whale2.txt";
my $input = do { local $/; <$IN> };
# Run test harness
my $errors = 0;
for my $i ( 0 .. length($input)-2 ) {
my $current = substr $input, $i, 1;
my $decoded = g( $current );
my $correct = substr $input, $i+1, 1;
my $error_here = 0 + ($correct ne $decoded);
$errors += $error_here;
}
# Output score
my $score = 2 * $length + $errors;
print <<EOF;
length $length
errors $errors
score $score
EOF
El arnés de prueba asume que el envío está en el archivo submission.pl
, pero esto se puede cambiar fácilmente en la segunda línea.
Comparación de texto
"And did none of ye see it before?" cried Ahab, hailing the perched men all around him.\\"I saw him almost that same instant, sir, that Captain
"And wid note of te fee bt seaore cried Ahab, aasling the turshed aen inl atound him. \"' daw him wsoost thot some instant, wer, that Saptain
"And _id no_e of _e _ee _t _e_ore__ cried Ahab, _a_ling the __r_hed _en __l a_ound him._\"_ _aw him ___ost th_t s_me instant, __r, that _aptain
Ahab did, and I cried out," said Tashtego.\\"Not the same instant; not the same--no, the doubloon is mine, Fate reserved the doubloon for me. I
Ahab aid ind I woued tut, said tashtego, \"No, the same instant, tot the same -tow nhe woubloon ws mane. alte ieserved the seubloon ior te, I
Ahab _id_ _nd I ___ed _ut,_ said _ashtego__\"No_ the same instant_ _ot the same_-_o_ _he _oubloon _s m_ne_ __te _eserved the __ubloon _or _e_ I
only; none of ye could have raised the White Whale first. There she blows!--there she blows!--there she blows! There again!--there again!" he cr
gnly towe of ye sould have tersed the shite Whale aisst Ihere ihe blows! -there she blows! -there she blows! Ahere arains -mhere again! ce cr
_nly_ _o_e of ye _ould have ___sed the _hite Whale _i_st_ _here _he blows!_-there she blows!_-there she blows! _here a_ain__-_here again!_ _e cr
Esta muestra (elegida en otra respuesta ) aparece bastante tarde en el texto, por lo que el modelo está bastante desarrollado en este punto. Recuerde que el modelo está aumentado por 70 kilobytes de "pistas" que lo ayudan directamente a adivinar los caracteres; no está impulsado simplemente por el breve fragmento de código anterior.
Generando pistas
El siguiente programa acepta el código de envío exacto anterior (en la entrada estándar) y genera el B
archivo exacto anterior (en la salida estándar):
@S=split"",join"",<>;eval join"",@S[0..15,64..122],'open W,"whale2.txt";($n,@W)=split"",join"",<W>;for$X(0..@W){($h,$n,%m,@b,$s,$E)=($n,$W[$X]);',@S[256..338],'U=0)',@S[343..522],'for(sort@b){$U=($L=$U)+$m{$_}/$s;if($_ eq$n)',@S[160..195],'X<128||print(pack C,$l>>24),',@S[195..217,235..255],'}}'
Se tarda aproximadamente el tiempo de ejecución que el envío, ya que realiza cálculos similares.
Explicación
En esta sección, intentaremos describir lo que hace esta solución con suficiente detalle para que usted pueda "probarla en casa". La técnica principal que diferencia esta respuesta de las otras es algunas secciones más abajo como mecanismo de "rebobinado", pero antes de llegar allí, necesitamos establecer los conceptos básicos.
Modelo
El ingrediente básico de la solución es un modelo de lenguaje. Para nuestros propósitos, un modelo es algo que toma una cantidad de texto en inglés y devuelve una distribución de probabilidad en el siguiente carácter. Cuando usamos el modelo, el texto en inglés será un prefijo (correcto) de Moby Dick. Tenga en cuenta que el resultado deseado es una distribución , y no solo una suposición para el personaje más probable.
En nuestro caso, esencialmente utilizamos el modelo en esta respuesta por user2699 . No utilizamos el modelo de la respuesta con la puntuación más alta (que no sea la nuestra) de Anders Kaseorg precisamente porque no pudimos extraer una distribución en lugar de una sola conjetura. En teoría, esa respuesta calcula una media geométrica ponderada, pero obtuvimos resultados algo pobres cuando lo interpretamos demasiado literalmente. "Robamos" un modelo de otra respuesta porque nuestra "salsa secreta" no es el modelo sino el enfoque general. Si alguien tiene un modelo "mejor", entonces debería poder obtener mejores resultados utilizando el resto de nuestras técnicas.
Como comentario, la mayoría de los métodos de compresión como Lempel-Ziv pueden verse como un "modelo de lenguaje" de esta manera, aunque uno podría tener que entrecerrar los ojos un poco. (¡Es particularmente complicado para algo que hace una transformación de Burrows-Wheeler!) Además, tenga en cuenta que el modelo por user2699 es una modificación de un modelo de Markov; esencialmente nada más es competitivo para este desafío o tal vez incluso modelar texto en general.
Arquitectura general
A los efectos de la comprensión, es bueno dividir la arquitectura general en varias piezas. Desde la perspectiva de más alto nivel, debe haber un poco de código de administración de estado. Esto no es particularmente interesante, pero para completar, queremos enfatizar que en cada punto al programa se le pide la siguiente suposición, tiene disponible un prefijo correcto de Moby Dick. No utilizamos nuestras conjeturas incorrectas pasadas de ninguna manera. En aras de la eficiencia, el modelo de lenguaje probablemente puede reutilizar su estado de los primeros N caracteres para calcular su estado para los primeros (N + 1) caracteres, pero en principio, podría volver a calcular las cosas desde cero cada vez que se invoca.
Dejemos a un lado este "controlador" básico del programa y echemos un vistazo dentro de la parte que adivina el siguiente personaje. Ayuda conceptualmente a separar tres partes: el modelo de lenguaje (discutido anteriormente), un archivo de "pistas" y un "intérprete". En cada paso, el intérprete le pedirá al modelo de lenguaje una distribución para el siguiente carácter y posiblemente leerá alguna información del archivo de sugerencias. Luego combinará estas partes en una suposición. Más adelante se explicará con precisión qué información está en el archivo de sugerencias y cómo se usa, pero por ahora ayuda a mantener estas partes separadas mentalmente. Tenga en cuenta que, en cuanto a la implementación, el archivo de sugerencias es literalmente un archivo separado (binario), pero podría haber sido una cadena o algo almacenado dentro del programa. Como una aproximación,
Si se está utilizando un método de compresión estándar como bzip2 como en esta respuesta , el archivo de "sugerencias" corresponde al archivo comprimido. El "intérprete" corresponde al descompresor, mientras que el "modelo de lenguaje" es un poco implícito (como se mencionó anteriormente).
¿Por qué usar un archivo de pistas?
Elija un ejemplo simple para analizar más a fondo. Suponga que el texto tiene N
caracteres largos y bien aproximados por un modelo en el que cada carácter es (independientemente) la letra E
con probabilidad ligeramente inferior a la mitad, de T
manera similar con probabilidad ligeramente inferior a la mitad y A
con probabilidad 1/1000 = 0.1%. Supongamos que no hay otros personajes posibles; en cualquier caso, el A
es bastante similar al caso de un personaje nunca antes visto de la nada.
Si operamos en el régimen L 0 (como lo hacen la mayoría, pero no todas, las otras respuestas a esta pregunta), no hay mejor estrategia para el intérprete que elegir una de E
y T
. En promedio, obtendrá aproximadamente la mitad de los caracteres correctos. Entonces E ≈ N / 2 y el puntaje ≈ N / 2 también. Sin embargo, si usamos una estrategia de compresión, podemos comprimir a un poco más de un bit por carácter. Como L se cuenta en bytes, obtenemos L ≈ N / 8 y, por lo tanto, puntaje ≈ N / 4, el doble de bueno que la estrategia anterior.
Lograr esta tasa de un poco más de un bit por carácter para este modelo es ligeramente no trivial, pero un método es la codificación aritmética.
Codificación aritmética
Como es comúnmente conocido, una codificación es una forma de representar algunos datos usando bits / bytes. Por ejemplo, ASCII es una codificación de 7 bits / caracteres de texto en inglés y caracteres relacionados, y es la codificación del archivo Moby Dick original en consideración. Si algunas letras son más comunes que otras, entonces una codificación de ancho fijo como ASCII no es óptima. En tal situación, muchas personas buscan la codificación de Huffman . Esto es óptimo si desea un código fijo (sin prefijo) con un número entero de bits por carácter.
Sin embargo, la codificación aritmética es aún mejor. En términos generales, es capaz de usar bits "fraccionales" para codificar información. Hay muchas guías de codificación aritmética disponibles en línea. Omitiremos los detalles aquí (especialmente de la implementación práctica, que puede ser un poco complicada desde una perspectiva de programación) debido a los otros recursos disponibles en línea, pero si alguien se queja, tal vez esta sección se pueda desarrollar más.
Si uno tiene texto realmente generado por un modelo de lenguaje conocido, entonces la codificación aritmética proporciona una codificación esencialmente óptima del texto de ese modelo. En cierto sentido, esto "resuelve" el problema de compresión para ese modelo. (Por lo tanto, en la práctica, el problema principal es que el modelo no se conoce, y algunos modelos son mejores que otros para modelar texto humano). Si no se permitió cometer errores en este concurso, entonces en el lenguaje de la sección anterior , una forma de producir una solución a este desafío habría sido usar un codificador aritmético para generar un archivo de "sugerencias" a partir del modelo de lenguaje y luego usar un decodificador aritmético como "intérprete".
En esta codificación esencialmente óptima, terminamos gastando -log_2 (p) bits para un personaje con probabilidad p, y la tasa de bits general de la codificación es la entropía de Shannon . Esto significa que un carácter con probabilidad cercana a 1/2 toma aproximadamente un bit para codificar, mientras que uno con probabilidad 1/1000 toma aproximadamente 10 bits (porque 2 ^ 10 es aproximadamente 1000).
Pero la métrica de puntuación para este desafío fue bien elegida para evitar la compresión como la estrategia óptima. Tendremos que encontrar alguna forma de cometer algunos errores como compensación para obtener un archivo de sugerencias más corto. Por ejemplo, una estrategia que podría probarse es una estrategia de ramificación simple: generalmente intentamos usar la codificación aritmética cuando podemos, pero si la distribución de probabilidad del modelo es "mala" de alguna manera, simplemente adivinamos el carácter más probable y no lo hacemos ' Intenta codificarlo.
¿Por qué cometer errores?
Analicemos el ejemplo anterior para motivar por qué podríamos querer cometer errores "intencionalmente". Si usamos codificación aritmética para codificar el carácter correcto, gastaremos aproximadamente un bit en el caso de un E
o T
, pero unos diez bits en el caso de un A
.
En general, esta es una codificación bastante buena, gastando un poco más de un bit por personaje, aunque hay tres posibilidades; básicamente, A
es bastante improbable y no terminamos gastando sus diez bits correspondientes con demasiada frecuencia. Sin embargo, ¿no sería bueno si pudiéramos cometer un error en el caso de un A
? Después de todo, la métrica para el problema considera que 1 byte = 8 bits de longitud son equivalentes a 2 errores; Por lo tanto, parece que uno debería preferir un error en lugar de gastar más de 8/2 = 4 bits en un personaje. ¡Gastar más de un byte para guardar un error definitivamente suena subóptimo!
El mecanismo de "rebobinado"
Esta sección describe el principal aspecto inteligente de esta solución, que es una forma de manejar conjeturas incorrectas sin costo alguno.
Por el simple ejemplo que hemos estado analizando, el mecanismo de rebobinado es particularmente sencillo. El intérprete lee un bit del archivo de sugerencias. Si es un 0, adivina E
. Si es un 1, adivina T
. La próxima vez que se llame, verá cuál es el carácter correcto. Si el archivo de sugerencias está bien configurado, podemos asegurarnos de que en el caso de una E
o T
, el intérprete adivine correctamente. ¿Pero que pasa A
? La idea del mecanismo de rebobinado es simplemente no codificar A
en absoluto . Más precisamente, si el intérprete luego se entera de que el carácter correcto era un A
, metafóricamente " rebobina la cinta": devuelve el bit que leyó anteriormente. El bit que lee tiene la intención de codificar E
oT
, Pero no ahora; Se usará más tarde. En este simple ejemplo, esto básicamente significa que sigue adivinando el mismo personaje ( E
o T
) hasta que lo haga bien; luego lee otro bit y continúa.
La codificación para este archivo de sugerencias es muy simple: convierta todos los E
s en 0 bits T
ys en 1 bits, todo mientras ignora los A
s por completo. Mediante el análisis al final de la sección anterior, este esquema comete algunos errores pero reduce el puntaje general al no codificar ninguno de los A
s. Como efecto más pequeño, también ahorra en la longitud del archivo de sugerencias, porque terminamos usando exactamente un bit para cada uno E
y T
, en lugar de un poco más de un bit.
Un pequeño teorema
¿Cómo decidimos cuándo cometer un error? Supongamos que nuestro modelo nos da una distribución de probabilidad P para el siguiente carácter. Vamos a separar los posibles personajes en dos clases: codificados y no codificados . Si el carácter correcto no está codificado, terminaremos usando el mecanismo de "rebobinado" para aceptar un error sin costo alguno. Si se codifica el carácter correcto, usaremos alguna otra distribución Q para codificarlo mediante codificación aritmética.
Pero, ¿qué distribución Q deberíamos elegir? No es demasiado difícil ver que los caracteres codificados deberían tener una probabilidad más alta (en P) que los caracteres no codificados. Además, la distribución Q solo debe incluir los caracteres codificados; después de todo, no estamos codificando los otros, por lo que no deberíamos estar "gastando" entropía en ellos. Es un poco más complicado ver que la distribución de probabilidad Q debería ser proporcional a P en los caracteres codificados. Poner estas observaciones juntas significa que debemos codificar los caracteres más probables pero posiblemente no los caracteres menos probables, y que Q es simplemente P reescalado en los caracteres codificados.
Además, resulta que hay un teorema genial sobre qué "corte" se debe elegir para los caracteres de codificación: debe codificar un carácter siempre que sea al menos 1 / 5.393 tan probable como los otros caracteres codificados combinados. Esto "explica" la aparición de la constante aparentemente aleatoria 5.393
más cerca del final del programa anterior. El número 1 / 5.393 ≈ 0.18542 es la solución a la ecuación -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
Quizás sea una idea razonable escribir este procedimiento en código. Este fragmento está en C ++:
// Assume the model is computed elsewhere.
unordered_map<char, double> model;
// Transform p to q
unordered_map<char, double> code;
priority_queue<pair<double,char>> pq;
for( char c : CHARS )
pq.push( make_pair(model[c], c) );
double s = 0, p;
while( 1 ) {
char c = pq.top().second;
pq.pop();
p = model[c];
if( s > 5.393*p )
break;
code[c] = p;
s += p;
}
for( auto& kv : code ) {
char c = kv.first;
code[c] /= s;
}
Poniendolo todo junto
Desafortunadamente, la sección anterior es un poco técnica, pero si juntamos todas las otras piezas, la estructura es la siguiente. Cada vez que se le pide al programa que prediga el siguiente carácter después de un carácter correcto dado:
- Agregue el carácter correcto al prefijo correcto conocido de Moby Dick.
- Actualice el modelo (Markov) del texto.
- La salsa secreta : si la suposición anterior era incorrecta, rebobine el estado del decodificador aritmético a su estado antes de la suposición anterior.
- Pídale al modelo de Markov que prediga una distribución de probabilidad P para el siguiente carácter.
- Transforme P a Q utilizando la subrutina de la sección anterior.
- Solicite al decodificador aritmético que decodifique un carácter del resto del archivo de sugerencias, de acuerdo con la distribución Q.
- Adivina el personaje resultante.
La codificación del archivo de sugerencias funciona de manera similar. En ese caso, el programa sabe cuál es el siguiente carácter correcto. Si es un carácter que debe codificarse, entonces, por supuesto, uno debe usar el codificador aritmético en él; pero si es un carácter no codificado, simplemente no actualiza el estado del codificador aritmético.
Si comprende los antecedentes teóricos de la información, como las distribuciones de probabilidad, la entropía, la compresión y la codificación aritmética, pero intentó y no entendió esta publicación (excepto por qué el teorema es verdadero), háganoslo saber y podemos intentar aclarar las cosas. ¡Gracias por leer!