Perl, 2 · 70525 + 326508 = 467558
Preditor
$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 executar este programa, você precisa deste arquivo aqui , que deve ser nomeado B
. (Você pode alterar esse nome de arquivo na segunda instância do caractere B
acima.) Veja abaixo como gerar esse arquivo.
O programa usa uma combinação de modelos de Markov essencialmente como nesta resposta do usuário 2699 , mas com algumas pequenas modificações. Isso produz uma distribuição para o próximo caractere. Usamos a teoria da informação para decidir se devemos aceitar um erro ou gastar bits de armazenamento em B
dicas de codificação (e, se sim, como). Utilizamos a codificação aritmética para otimizar os bits fracionários do modelo.
O programa tem 582 bytes (incluindo uma nova linha final desnecessária) e o arquivo binário B
tem 69942 bytes; portanto, de acordo com as regras de pontuação de vários arquivos , obtemos L
582 + 69942 + 1 = 70525.
O programa quase certamente requer uma arquitetura de 64 bits (little-endian?). Demora aproximadamente 2,5 minutos para executar em uma m5.large
instância no Amazon EC2.
Código de teste
# 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
O equipamento de teste assume que o envio está no arquivo submission.pl
, mas isso pode ser facilmente alterado na segunda linha.
Comparação 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
Este exemplo (escolhido em outra resposta ) ocorre bastante tarde no texto, portanto o modelo é bastante desenvolvido nesse ponto. Lembre-se de que o modelo é aumentado por 70 kilobytes de "dicas" que ajudam diretamente a adivinhar os personagens; não é direcionado simplesmente pelo pequeno trecho de código acima.
Gerando dicas
O programa a seguir aceita o código exato de envio acima (na entrada padrão) e gera o B
arquivo exato acima (na saída padrão):
@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],'}}'
Demora aproximadamente o tempo necessário para executar o envio, pois ele executa cálculos semelhantes.
Explicação
Nesta seção, tentaremos descrever o que essa solução faz com detalhes suficientes para que você possa "experimentá-la em casa". A principal técnica que diferencia essa resposta das outras é algumas seções abaixo, como o mecanismo de "rebobinar", mas antes de chegarmos lá, precisamos configurar o básico.
Modelo
O ingrediente básico da solução é um modelo de linguagem. Para nossos propósitos, um modelo é algo que pega uma certa quantidade de texto em inglês e retorna uma distribuição de probabilidade no próximo caractere. Quando usamos o modelo, o texto em inglês será um prefixo (correto) de Moby Dick. Observe que a saída desejada é uma distribuição e não apenas um palpite para o caracter mais provável.
No nosso caso, usamos essencialmente o modelo nesta resposta do usuário2699 . Não usamos o modelo da resposta de maior pontuação (exceto a nossa) de Anders Kaseorg justamente porque não conseguimos extrair uma distribuição em vez de apenas um palpite. Em teoria, essa resposta calcula uma média geométrica ponderada, mas obtivemos resultados um pouco ruins quando a interpretamos literalmente. "Roubamos" um modelo de outra resposta, porque nosso "molho secreto" não é o modelo, mas a abordagem geral. Se alguém tiver um modelo "melhor", deverá conseguir melhores resultados usando o restante de nossas técnicas.
Como observação, a maioria dos métodos de compactação, como o Lempel-Ziv, pode ser vista como um "modelo de linguagem" dessa maneira, embora seja necessário apertar os olhos um pouco. (É particularmente complicado para algo que transforma uma Burrows-Wheeler!) Além disso, observe que o modelo por user2699 é uma modificação de um modelo de Markov; essencialmente, nada mais é competitivo para esse desafio ou talvez até modelar texto em geral.
Arquitetura geral
Para fins de entendimento, é bom dividir a arquitetura geral em várias partes. Da perspectiva de mais alto nível, é necessário haver um pouco de código de gerenciamento de estado. Isso não é particularmente interessante, mas, para ser completo, queremos enfatizar que, em todo momento, o programa é solicitado a adivinhar o seguinte, ele tem disponível um prefixo correto de Moby Dick. Nós não usamos nossas suposições incorretas do passado de forma alguma. Por uma questão de eficiência, o modelo de linguagem provavelmente pode reutilizar seu estado dos primeiros N caracteres para calcular seu estado para os primeiros (N + 1) caracteres, mas, em princípio, ele pode recompilar as coisas do zero toda vez que é invocado.
Vamos deixar esse "driver" básico de lado e espiar dentro da parte que adivinha o próximo caractere. Conceitualmente, ajuda a separar três partes: o modelo de linguagem (discutido acima), um arquivo de "dicas" e um "intérprete". A cada passo, o intérprete solicitará ao modelo de linguagem uma distribuição para o próximo caractere e possivelmente lerá algumas informações do arquivo de dicas. Em seguida, combinará essas partes em um palpite. Precisamente, quais informações estão no arquivo de dicas e como são usadas serão explicadas mais tarde, mas, por enquanto, ajuda a manter essas partes separadas mentalmente. Observe que, em termos de implementação, o arquivo de dicas é literalmente um arquivo (binário) separado, mas poderia ter sido uma string ou algo armazenado dentro do programa. Como uma aproximação,
Se alguém estiver usando um método de compactação padrão, como bzip2, como nesta resposta , o arquivo "dicas" corresponderá ao arquivo compactado. O "intérprete" corresponde ao descompressor, enquanto o "modelo de linguagem" é um pouco implícito (como mencionado acima).
Por que usar um arquivo de dica?
Vamos escolher um exemplo simples para analisar melhor. Suponha que o texto tenha N
caracteres longos e bem aproximados por um modelo em que cada caractere seja (independentemente) a letra E
com probabilidade ligeiramente menor que a metade, T
similarmente com probabilidade ligeiramente menor que a metade e A
com probabilidade 1/1000 = 0,1%. Vamos supor que nenhum outro personagem seja possível; de qualquer forma, A
é bem parecido com o de um personagem anteriormente invisível do nada.
Se operamos no regime L0 (como a maioria, mas não todas, das outras respostas a essa pergunta), não há melhor estratégia para o intérprete do que escolher uma das opções E
e T
. Em média, ele receberá cerca da metade dos caracteres corretos. Então E ≈ N / 2 e a pontuação ≈ N / 2 também. No entanto, se usarmos uma estratégia de compactação, podemos compactar para um pouco mais de um bit por caractere. Como L é contado em bytes, obtemos L ≈ N / 8 e, portanto, obtemos ≈ N / 4, duas vezes melhor que a estratégia anterior.
Atingir essa taxa de pouco mais de um bit por caractere para este modelo não é trivial, mas um método é a codificação aritmética.
Codificação aritmética
Como é comumente conhecido, uma codificação é uma maneira de representar alguns dados usando bits / bytes. Por exemplo, ASCII é uma codificação de 7 bits / caracteres do texto em inglês e caracteres relacionados, e é a codificação do arquivo Moby Dick original em consideração. Se algumas letras são mais comuns que outras, uma codificação de largura fixa como ASCII não é ideal. Em tal situação, muitas pessoas buscam a codificação de Huffman . Isso é ideal se você deseja um código fixo (sem prefixo) com um número inteiro de bits por caractere.
No entanto, a codificação aritmética é ainda melhor. Grosso modo, é capaz de usar bits "fracionários" para codificar informações. Existem muitos guias para codificação aritmética disponíveis online. Iremos pular os detalhes aqui (especialmente a implementação prática, que pode ser um pouco complicada do ponto de vista da programação) por causa dos outros recursos disponíveis online, mas se alguém reclamar, talvez esta seção possa ser mais aprofundada.
Se houver texto realmente gerado por um modelo de linguagem conhecido, a codificação aritmética fornecerá uma codificação essencialmente ótima do texto desse modelo. Em certo sentido, isso "resolve" o problema de compactação desse modelo. (Portanto, na prática, a questão principal é que o modelo não é conhecido e alguns são melhores que outros na modelagem de texto humano.) Se não foi permitido cometer erros neste concurso, então no idioma da seção anterior , uma maneira de produzir uma solução para esse desafio seria usar um codificador aritmético para gerar um arquivo "dicas" a partir do modelo de linguagem e, em seguida, usar um decodificador aritmético como "intérprete".
Nesta codificação essencialmente ideal, acabamos gastando -log_2 (p) bits para um caractere com probabilidade p, e a taxa de bits geral da codificação é a entropia de Shannon . Isso significa que um caractere com probabilidade próxima a 1/2 leva cerca de um bit para codificar, enquanto um caractere com probabilidade 1/1000 leva cerca de 10 bits (porque 2 ^ 10 é aproximadamente 1000).
Mas a métrica de pontuação para esse desafio foi bem escolhida para evitar a compactação como a estratégia ideal. Teremos que descobrir uma maneira de cometer alguns erros como compensação por obter um arquivo de dicas mais curto. Por exemplo, uma estratégia que se pode tentar é uma estratégia simples de ramificação: geralmente tentamos usar a codificação aritmética quando possível, mas se a distribuição de probabilidade do modelo é "ruim", de alguma forma, apenas adivinhamos o personagem mais provável e não ' tente codificá-lo.
Por que cometer erros?
Vamos analisar o exemplo anterior para motivar por que podemos querer cometer erros "intencionalmente". Se usarmos a codificação aritmética para codificar o caractere correto, gastaremos aproximadamente um bit no caso de um E
ou T
, mas cerca de dez bits no caso de um A
.
No geral, essa é uma codificação muito boa, gastando um pouco mais de um pouco por caractere, embora haja três possibilidades; basicamente, A
é bastante improvável e não acabamos gastando seus dez bits correspondentes com muita frequência. No entanto, não seria bom se pudéssemos cometer um erro no caso de um A
? Afinal, a métrica para o problema considera 1 byte = 8 bits de comprimento como equivalente a 2 erros; portanto, parece que se deve preferir um erro ao invés de gastar mais de 8/2 = 4 bits em um caractere. Gastar mais de um byte para salvar um erro definitivamente parece subótimo!
O mecanismo de "rebobinar"
Esta seção descreve o principal aspecto inteligente desta solução, que é uma maneira de lidar com suposições incorretas sem nenhum custo.
Para o exemplo simples que analisamos, o mecanismo de rebobinagem é particularmente direto. O intérprete lê um bit do arquivo de dicas. Se é um 0, adivinha E
. Se for um 1, adivinha T
. Na próxima vez que for chamado, ele verá qual é o caractere correto. Se o arquivo de dica estiver bem configurado, podemos garantir que, no caso de um E
ou T
, o intérprete adivinhe corretamente. Mas que tal A
? A ideia do mecanismo de retrocesso é simplesmente não codificar A
em tudo . Mais precisamente, se o intérprete mais tarde descobrir que o caractere correto era um A
, metaforicamente " rebobina a fita": retorna o bit lido anteriormente. A parte que ele lê pretende codificar E
ouT
, mas agora não; será usado mais tarde. Neste exemplo simples, isso basicamente significa que continua adivinhando o mesmo caractere ( E
ou T
) até acertar; depois lê outro pedaço e continua.
A codificação para esse arquivo de dicas é muito simples: transforme todos os E
s em 0 bits T
es em 1 bits, ignorando-os A
completamente. Pela análise no final da seção anterior, esse esquema comete alguns erros, mas reduz a pontuação geral ao não codificar nenhum dos A
s. Como efeito menor, ele também economiza o tamanho do arquivo de dicas, porque acabamos usando exatamente um bit para cada um E
e T
, em vez de um pouco mais do que um pouco.
Um pouco de teorema
Como decidimos quando cometer um erro? Suponha que nosso modelo nos dê uma distribuição de probabilidade P para o próximo caractere. Separaremos os caracteres possíveis em duas classes: codificado e não codificado . Se o caractere correto não estiver codificado, usaremos o mecanismo "rebobinar" para aceitar um erro sem nenhum custo. Se o caractere correto for codificado, usaremos outra distribuição Q para codificá-lo usando codificação aritmética.
Mas qual distribuição Q devemos escolher? Não é difícil ver que todos os caracteres codificados devem ter maior probabilidade (em P) do que os caracteres não codificados. Além disso, a distribuição Q deve incluir apenas os caracteres codificados; afinal, não estamos codificando os outros, por isso não devemos "gastar" entropia neles. É um pouco mais complicado ver que a distribuição de probabilidade Q deve ser proporcional a P nos caracteres codificados. Reunir essas observações significa que devemos codificar os caracteres mais prováveis, mas possivelmente não os menos prováveis, e que Q é simplesmente P redimensionado nos caracteres codificados.
Além disso, verifica-se que existe um teorema interessante sobre qual "corte" deve-se escolher para os caracteres de codificação: você deve codificar um caractere contanto que seja pelo menos 1 / 5.393 tão provável quanto os outros caracteres codificados combinados. Isso "explica" a aparência da constante aparentemente aleatória 5.393
mais próxima do final do programa acima. O número 1 / 5.393 ≈ 0,18542 é a solução para a equação -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
Talvez seja uma idéia razoável escrever esse procedimento no código. Este fragmento está em 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;
}
Juntando tudo
Infelizmente, a seção anterior é um pouco técnica, mas se juntarmos todas as outras peças, a estrutura será a seguinte. Sempre que o programa é solicitado a prever o próximo caractere após um determinado caractere correto:
- Adicione o caractere correto ao prefixo correto conhecido de Moby Dick.
- Atualize o modelo (Markov) do texto.
- O segredo : se o palpite anterior estiver incorreto, rebobine o estado do decodificador aritmético para o estado anterior ao palpite anterior!
- Peça ao modelo de Markov para prever uma distribuição de probabilidade P para o próximo caractere.
- Transforme P em Q usando a sub-rotina da seção anterior.
- Peça ao decodificador aritmético que decodifique um caractere do restante do arquivo de dicas, de acordo com a distribuição Q.
- Adivinha o personagem resultante.
A codificação do arquivo de dicas funciona da mesma forma. Nesse caso, o programa sabe qual é o próximo caractere correto. Se é um caractere que deve ser codificado, é claro que se deve usar o codificador aritmético; mas se for um caractere não codificado, ele simplesmente não atualizará o estado do codificador aritmético.
Se você entende o contexto teórico da informação como distribuições de probabilidade, entropia, compressão e codificação aritmética, mas tentou e não conseguiu entender este post (exceto por que o teorema é verdadeiro), informe-nos e podemos tentar esclarecer as coisas. Obrigado pela leitura!