Perl, 2 · 70525 + 326508 = 467558
Predictor
$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}
Per eseguire questo programma, è necessario questo file qui , che deve essere nominato B
. (Puoi modificare questo nome file nella seconda istanza del carattere B
sopra.) Vedi sotto per come generare questo file.
Il programma utilizza una combinazione di modelli Markov essenzialmente come in questa risposta dall'utente 2699 , ma con alcune piccole modifiche. Questo produce una distribuzione per il personaggio successivo. Usiamo la teoria dell'informazione per decidere se accettare un errore o spendere bit di memoria in B
suggerimenti per la codifica (e in tal caso, come). Usiamo la codifica aritmetica per memorizzare in modo ottimale i bit frazionari del modello.
Il programma ha una lunghezza di 582 byte (inclusa una nuova riga finale non necessaria) e il file binario B
è lungo 69942 byte, quindi in base alle regole per il calcolo del punteggio di più file , L
calcoliamo 582 + 69942 + 1 = 70525.
Il programma richiede quasi certamente un'architettura a 64 bit (little-endian?). Sono necessari circa 2,5 minuti per l'esecuzione su m5.large
un'istanza su Amazon EC2.
Codice di prova
# 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
Il cablaggio di test presuppone che l'invio sia nel file submission.pl
, ma questo può essere facilmente modificato nella seconda riga.
Confronto testuale
"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
Questo esempio (scelto in un'altra risposta ) si presenta piuttosto tardi nel testo, quindi il modello è abbastanza sviluppato da questo punto. Ricorda che il modello è aumentato di 70 kilobyte di "suggerimenti" che aiutano direttamente a indovinare i personaggi; non è guidato semplicemente dal breve frammento di codice sopra.
Generazione di suggerimenti
Il seguente programma accetta il codice di invio esatto sopra (sull'input standard) e genera il B
file esatto sopra (sull'output standard):
@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],'}}'
Ci vuole circa il tempo necessario per eseguire l'invio, poiché esegue calcoli simili.
Spiegazione
In questa sezione, tenteremo di descrivere ciò che questa soluzione fa in modo sufficientemente dettagliato da poterti "provare a casa" da solo. La tecnica principale che differenzia questa risposta dalle altre è una parte del meccanismo di "riavvolgimento", ma prima di arrivarci, dobbiamo impostare le basi.
Modello
L'ingrediente di base della soluzione è un modello linguistico. Per i nostri scopi, un modello è qualcosa che richiede una certa quantità di testo inglese e restituisce una distribuzione di probabilità sul carattere successivo. Quando usiamo il modello, il testo inglese sarà un prefisso (corretto) di Moby Dick. Si noti che l'output desiderato è una distribuzione e non solo una singola ipotesi per il personaggio più probabile.
Nel nostro caso, utilizziamo essenzialmente il modello in questa risposta dall'utente2699 . Non abbiamo utilizzato il modello dalla risposta con il punteggio più alto (diverso dal nostro) di Anders Kaseorg proprio perché non siamo stati in grado di estrarre una distribuzione piuttosto che una singola ipotesi. In teoria, quella risposta calcola una media geometrica ponderata, ma abbiamo ottenuto risultati piuttosto scarsi quando l'abbiamo interpretata in modo troppo letterale. Abbiamo "rubato" un modello a un'altra risposta perché la nostra "salsa segreta" non è il modello ma piuttosto l'approccio globale. Se qualcuno ha un modello "migliore", dovrebbe essere in grado di ottenere risultati migliori usando il resto delle nostre tecniche.
Come osservazione, la maggior parte dei metodi di compressione come Lempel-Ziv può essere visto come un "modello di linguaggio" in questo modo, anche se si potrebbe dover socchiudere leggermente gli occhi. (È particolarmente complicato per qualcosa che trasforma una Burrows-Wheeler!) Inoltre, si noti che il modello di user2699 è una modifica di un modello di Markov; essenzialmente nient'altro è competitivo per questa sfida o forse anche per modellare il testo in generale.
Architettura generale
Ai fini della comprensione, è bello suddividere l'architettura complessiva in più pezzi. Dal punto di vista del livello più alto, deve esserci un po 'di codice di gestione dello stato. Questo non è particolarmente interessante, ma per completezza vogliamo sottolineare che ad ogni punto il programma viene chiesto per la prossima ipotesi, ha a disposizione un prefisso corretto di Moby Dick. Non utilizziamo in alcun modo le nostre ipotesi errate passate. Per motivi di efficienza, il modello di linguaggio può probabilmente riutilizzare il suo stato dai primi N caratteri per calcolare il suo stato per i primi (N + 1) caratteri, ma in linea di principio potrebbe ricalcolare le cose da zero ogni volta che viene invocato.
Mettiamo da parte questo "driver" di base del programma e sbirciano dentro la parte che indovina il personaggio successivo. Aiuta concettualmente a separare tre parti: il modello linguistico (discusso sopra), un file "suggerimenti" e un "interprete". Ad ogni passo, l'interprete chiederà al modello di lingua una distribuzione per il carattere successivo e possibilmente leggerà alcune informazioni dal file dei suggerimenti. Quindi combinerà queste parti in un'ipotesi. Le informazioni contenute nel file dei suggerimenti e il modo in cui vengono utilizzate verranno spiegate in seguito, ma per ora aiuta a mantenere separate queste parti mentalmente. Si noti che dal punto di vista dell'implementazione, il file dei suggerimenti è letteralmente un file separato (binario) ma potrebbe essere stato una stringa o qualcosa memorizzato all'interno del programma. Per approssimazione,
Se si utilizza un metodo di compressione standard come bzip2 come in questa risposta , il file "suggerimenti" corrisponde al file compresso. L '"interprete" corrisponde al decompressore, mentre il "modello di linguaggio" è un po' implicito (come menzionato sopra).
Perché usare un file di suggerimento?
Facciamo un semplice esempio per analizzare ulteriormente. Supponiamo che il testo sia N
composto da caratteri lunghi e ben approssimati da un modello in cui ogni carattere è (indipendentemente) la lettera E
con probabilità leggermente inferiore alla metà, in T
modo simile con probabilità leggermente inferiore alla metà e A
con probabilità 1/1000 = 0,1%. Supponiamo che non siano possibili altri personaggi; in ogni caso, A
è abbastanza simile al caso di un personaggio mai visto prima di punto in bianco.
Se operassimo nel regime L 0 (come fanno la maggior parte, ma non tutte, delle altre risposte a questa domanda), non c'è strategia migliore per l'interprete che scegliere una delle E
e T
. In media, otterrà circa la metà dei caratteri corretti. Quindi E ≈ N / 2 e il punteggio ≈ N / 2 anche. Tuttavia, se utilizziamo una strategia di compressione, possiamo comprimere a poco più di un bit per carattere. Dato che L viene conteggiato in byte, otteniamo L ≈ N / 8 e quindi segniamo ≈ N / 4, due volte meglio della strategia precedente.
Raggiungere questo tasso di poco più di un bit per carattere per questo modello è leggermente non banale, ma un metodo è la codifica aritmetica.
Codifica aritmetica
Come è noto, una codifica è un modo per rappresentare alcuni dati usando bit / byte. Ad esempio, ASCII è una codifica a 7 bit / carattere del testo inglese e dei caratteri correlati, ed è la codifica del file Moby Dick originale in esame. Se alcune lettere sono più comuni di altre, una codifica a larghezza fissa come ASCII non è ottimale. In una situazione del genere, molte persone cercano la codifica di Huffman . Questo è ottimale se si desidera un codice fisso (senza prefisso) con un numero intero di bit per carattere.
Tuttavia, la codifica aritmetica è ancora migliore. In parole povere, è in grado di usare bit "frazionari" per codificare le informazioni. Ci sono molte guide alla codifica aritmetica disponibili online. Salteremo i dettagli qui (soprattutto dell'implementazione pratica, che può essere un po 'complicata dal punto di vista della programmazione) a causa delle altre risorse disponibili online, ma se qualcuno si lamenta, forse questa sezione può essere arricchita di più.
Se uno ha il testo effettivamente generato da un modello di linguaggio noto, la codifica aritmetica fornisce una codifica essenzialmente ottimale del testo da quel modello. In un certo senso, questo "risolve" il problema di compressione per quel modello. (Quindi, in pratica, il problema principale è che il modello non è noto e alcuni modelli sono migliori di altri nel modellare il testo umano.) Se non è stato permesso di fare errori in questo contesto, allora nella lingua della sezione precedente , un modo per produrre una soluzione a questa sfida sarebbe stato quello di utilizzare un codificatore aritmetico per generare un file "suggerimenti" dal modello di linguaggio e quindi utilizzare un decodificatore aritmetico come "interprete".
In questa codifica essenzialmente ottimale, finiamo per spendere -log_2 (p) bit per un personaggio con probabilità p, e il bit-rate complessivo della codifica è l' entropia di Shannon . Ciò significa che un carattere con probabilità vicino a 1/2 impiega circa un bit per codificare, mentre uno con probabilità 1/1000 impiega circa 10 bit (perché 2 ^ 10 è approssimativamente 1000).
Ma la metrica del punteggio per questa sfida è stata ben scelta per evitare la compressione come strategia ottimale. Dovremo trovare un modo per fare alcuni errori come un compromesso per ottenere un file di suggerimenti più breve. Ad esempio, una strategia che si potrebbe provare è una semplice strategia di ramificazione: generalmente possiamo provare a usare la codifica aritmetica quando possiamo, ma se la distribuzione di probabilità dal modello è "cattiva" in qualche modo indoviniamo semplicemente il carattere più probabile e non prova a codificarlo.
Perché commettere errori?
Analizziamo l'esempio di prima per motivare il motivo per cui potremmo voler commettere errori "intenzionalmente". Se usiamo la codifica aritmetica per codificare il carattere corretto, impiegheremo all'incirca un bit nel caso di un E
o T
, ma circa dieci bit nel caso di un A
.
Nel complesso, questa è una codifica abbastanza buona, che spende poco più di un po 'per personaggio, anche se ci sono tre possibilità; fondamentalmente, A
è abbastanza improbabile e non finiamo per spendere troppo spesso i suoi corrispondenti dieci bit. Tuttavia, non sarebbe bello se potessimo semplicemente fare un errore nel caso di un A
? Dopotutto, la metrica per il problema considera 1 byte = 8 bit di lunghezza equivalente a 2 errori; quindi sembra che si dovrebbe preferire un errore invece di spendere più di 8/2 = 4 bit su un personaggio. Trascorrere più di un byte per salvare un errore sembra decisamente non ottimale!
Il meccanismo di "riavvolgimento"
Questa sezione descrive il principale aspetto intelligente di questa soluzione, che è un modo per gestire ipotesi errate senza costi di lunghezza.
Per il semplice esempio che abbiamo analizzato, il meccanismo di riavvolgimento è particolarmente semplice. L'interprete legge un bit dal file dei suggerimenti. Se è uno 0, indovina E
. Se è un 1, indovina T
. La prossima volta che viene chiamato, vede qual è il personaggio corretto. Se il file di suggerimento è impostato correttamente, possiamo assicurarci che, nel caso di un E
o T
, l'interprete indovini correttamente. Ma che dire A
? L'idea del meccanismo di riavvolgimento è semplicemente di non codificare A
affatto . Più precisamente, se l'interprete in seguito apprende che il carattere corretto era un A
" riavvolge metaforicamente il nastro": restituisce il bit letto in precedenza. Il bit che legge intende codificare E
oT
, ma non ora; sarà usato in seguito. In questo semplice esempio, ciò significa fondamentalmente che continua a indovinare lo stesso personaggio ( E
o T
) fino a quando non lo fa bene; poi legge un altro po 'e continua.
La codifica per questo file di suggerimenti è molto semplice: trasforma tutte le E
s in 0 bit e T
s in 1 bit, il tutto ignorando A
completamente s. Dall'analisi alla fine della sezione precedente, questo schema fa degli errori ma riduce il punteggio complessivo non codificando nessuno dei messaggi A
. Come effetto minore, risparmia anche sulla lunghezza del file dei suggerimenti, perché finiamo per usare esattamente un bit per ciascuno E
e T
invece di leggermente più di un bit.
Un piccolo teorema
Come decidiamo quando fare un errore? Supponiamo che il nostro modello ci dia una distribuzione di probabilità P per il prossimo personaggio. Separeremo i possibili caratteri in due classi: codificati e non codificati . Se il carattere corretto non è codificato, finiremo per utilizzare il meccanismo di "riavvolgimento" per accettare un errore senza costi di lunghezza. Se il carattere corretto è codificato, useremo qualche altra distribuzione Q per codificarlo usando la codifica aritmetica.
Ma quale distribuzione Q dovremmo scegliere? Non è troppo difficile vedere che tutti i caratteri codificati dovrebbero avere una probabilità maggiore (in P) rispetto ai caratteri non codificati. Inoltre, la distribuzione Q dovrebbe includere solo i caratteri codificati; dopo tutto, non stiamo codificando gli altri, quindi non dovremmo "spendere" entropia su di essi. È un po 'più complicato vedere che la distribuzione di probabilità Q dovrebbe essere proporzionale a P sui caratteri codificati. Mettere insieme queste osservazioni significa che dovremmo codificare i caratteri più probabili, ma forse non quelli meno probabili, e che Q è semplicemente ridimensionato sui caratteri codificati.
Si scopre inoltre che esiste un teorema interessante riguardo a quale "cutoff" si dovrebbe scegliere per i caratteri di codifica: è necessario codificare un carattere fintanto che è almeno 1 / 5,393 con la probabilità degli altri caratteri codificati combinati. Questo "spiega" la comparsa della costante apparentemente casuale 5.393
più vicina alla fine del programma sopra. Il numero 1 / 5.393 ≈ 0.18542 è la soluzione all'equazione -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
Forse è una buona idea scrivere questa procedura nel codice. Questo frammento è in 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;
}
Mettere tutto insieme
La sezione precedente è purtroppo un po 'tecnica, ma se mettiamo insieme tutti gli altri pezzi, la struttura è la seguente. Ogni volta che viene richiesto al programma di prevedere il carattere successivo dopo un determinato carattere corretto:
- Aggiungi il carattere corretto al prefisso corretto noto di Moby Dick.
- Aggiorna il modello (Markov) del testo.
- La salsa segreta : se l'ipotesi precedente era errata, riavvolgi lo stato del decodificatore aritmetico al suo stato prima dell'ipotesi precedente!
- Chiedi al modello di Markov di prevedere una distribuzione di probabilità P per il personaggio successivo.
- Trasforma P in Q usando la subroutine della sezione precedente.
- Chiedere al decodificatore aritmetico di decodificare un carattere dal resto del file dei suggerimenti, secondo la distribuzione Q.
- Indovina il personaggio risultante.
La codifica del file dei suggerimenti funziona in modo simile. In tal caso, il programma sa qual è il prossimo personaggio corretto. Se è un carattere che dovrebbe essere codificato, allora ovviamente si dovrebbe usare l'encoder aritmetico su di esso; ma se è un carattere non codificato, semplicemente non aggiorna lo stato dell'encoder aritmetico.
Se capisci il background teorico dell'informazione come distribuzioni di probabilità, entropia, compressione e codifica aritmetica ma hai provato e non hai capito questo post (tranne perché il teorema è vero), faccelo sapere e possiamo provare a chiarire le cose. Grazie per aver letto!