Perl, 2 · 70525 + 326508 = 467558
предсказатель
$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}
Чтобы запустить эту программу, вам нужен этот файл , который должен быть назван B
. (Вы можете изменить это имя файла во втором экземпляре символа B
выше.) Ниже описано, как создать этот файл.
Программа использует комбинацию моделей Маркова в основном как в этом ответе пользователя 2699 , но с несколькими небольшими модификациями. Это создает распределение для следующего символа. Мы используем теорию информации, чтобы решить, принимать ли ошибку или тратить биты памяти на B
подсказки кодирования (и если да, то как). Мы используем арифметическое кодирование для оптимального хранения дробных битов из модели.
Длина программы составляет 582 байта (включая ненужный заключительный B
символ новой строки), а длина двоичного файла составляет 69942 байта, поэтому по правилам оценки нескольких файлов мы получаем L
582 + 69942 + 1 = 70525.
Программа почти наверняка требует 64-битной (little-endian?) Архитектуры. Для запуска m5.large
экземпляра на Amazon EC2 требуется примерно 2,5 минуты .
Тестовый код
# 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
Тестовый жгут предполагает, что отправка находится в файле submission.pl
, но это можно легко изменить во второй строке.
Сравнение текста
"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
Этот образец (выбранный в другом ответе ) встречается довольно поздно в тексте, поэтому модель достаточно развита к этому моменту. Помните, что модель дополнена 70 килобайтами «подсказок», которые напрямую помогают ей угадывать символы; он не управляется просто коротким фрагментом кода выше.
Генерация подсказок
Следующая программа принимает точный код отправки выше (при стандартном вводе) и генерирует точный B
файл выше (при стандартном выводе):
@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],'}}'
Это занимает примерно столько же времени, сколько и представление, поскольку выполняет аналогичные вычисления.
объяснение
В этом разделе мы попытаемся описать, что делает это решение достаточно подробно, чтобы вы могли «попробовать его дома» самостоятельно. Основной метод, который отличает этот ответ от других, - это несколько разделов, называемых механизмом «перемотки», но прежде чем мы доберемся до этого, нам нужно настроить основы.
модель
Основным компонентом решения является языковая модель. Для наших целей модель - это то, что занимает некоторое количество английского текста и возвращает распределение вероятностей для следующего символа. Когда мы используем модель, текст на английском языке будет иметь некоторый (правильный) префикс Moby Dick. Обратите внимание, что желаемым результатом является распределение , а не просто предположение для наиболее вероятного символа.
В нашем случае мы по существу используем модель в этом ответе пользователя 2699 . Мы не использовали модель из ответа с наивысшим баллом (кроме нашего) Андерса Касеорга именно потому, что нам не удалось извлечь распределение, а не единственное лучшее предположение. Теоретически, этот ответ вычисляет средневзвешенное геометрическое значение, но мы получили несколько плохие результаты, когда интерпретировали это слишком буквально. Мы «украли» модель из другого ответа, потому что наш «секретный соус» - не модель, а общий подход. Если у кого-то есть «лучшая» модель, тогда он сможет добиться лучших результатов, используя остальные наши методы.
Как примечание, большинство методов сжатия, таких как Lempel-Ziv, можно рассматривать как «модель языка», хотя, возможно, придется немного щуриться. (Это особенно сложно для того, что выполняет преобразование Берроуза-Уилера!) Также обратите внимание, что модель user2699 является модификацией модели Маркова; по сути, ничто иное не является конкурентоспособным для этой задачи или, возможно, даже для моделирования текста в целом.
Общая архитектура
В целях понимания полезно разбить общую архитектуру на несколько частей. С точки зрения самого высокого уровня, должен быть небольшой код управления состоянием. Это не особенно интересно, но для полноты картины мы хотим подчеркнуть, что в каждой точке программы запрашивается следующая догадка, в ней есть правильный префикс Moby Dick. Мы никоим образом не используем наши прошлые неверные догадки. Ради эффективности, языковая модель, вероятно, может повторно использовать свое состояние из первых N символов для вычисления своего состояния для первых (N + 1) символов, но в принципе она может пересчитывать вещи с нуля каждый раз, когда она вызывается.
Давайте отложим этот основной «драйвер» программы и заглянем внутрь части, которая угадывает следующий символ. Концептуально это помогает разделить три части: языковую модель (обсуждаемую выше), файл «подсказок» и «интерпретатор». На каждом шаге переводчик будет запрашивать у языковой модели распределение следующего символа и, возможно, считывать некоторую информацию из файла подсказок. Тогда он объединит эти части в догадку. Точно какая информация содержится в файле подсказок, а также как она используется, будет объяснено позже, но пока она помогает мысленно разделить эти части. Обратите внимание, что в плане реализации файл подсказок является буквально отдельным (двоичным) файлом, но он мог быть строкой или чем-то другим, хранящимся внутри программы. В качестве приближения
Если используется стандартный метод сжатия, такой как bzip2, как в этом ответе , файл «подсказок» соответствует сжатому файлу. «Интерпретатор» соответствует декомпрессору, в то время как «языковая модель» немного неявна (как упомянуто выше).
Зачем использовать файл подсказки?
Давайте выберем простой пример для дальнейшего анализа. Предположим, что текст состоит из N
длинных символов и хорошо аппроксимируется моделью, в которой каждый символ (независимо) представляет собой букву E
с вероятностью чуть меньше половины, T
аналогично с вероятностью чуть меньше половины и A
с вероятностью 1/1000 = 0,1%. Давайте предположим, что другие символы невозможны; в любом случае, A
это очень похоже на случай ранее невидимого символа на ровном месте.
Если мы работаем в режиме L 0 (как большинство, но не все другие ответы на этот вопрос), нет лучшей стратегии для переводчика, чем выбрать один из E
и T
. В среднем примерно половина символов будет правильной. Так что E ≈ N / 2 и оценка ≈ N / 2 тоже. Однако, если мы используем стратегию сжатия, мы можем сжать до чуть более одного бита на символ. Поскольку L считается в байтах, мы получаем L ≈ N / 8 и, таким образом, получаем значение ≈ N / 4, что вдвое больше, чем в предыдущей стратегии.
Достижение этой скорости чуть более одного бита на символ для этой модели немного нетривиально, но одним из методов является арифметическое кодирование.
Арифметическое кодирование
Как известно, кодирование - это способ представления некоторых данных с использованием битов / байтов. Например, ASCII - это 7-битная / символьная кодировка английского текста и связанных с ним символов, и это кодировка исходного файла Moby Dick, который мы рассматриваем. Если некоторые буквы встречаются чаще, чем другие, то кодирование с фиксированной шириной, например ASCII, не является оптимальным. В такой ситуации многие люди тянутся к кодированию Хаффмана . Это оптимально, если вам нужен фиксированный (без префикса) код с целым числом битов на символ.
Однако арифметическое кодирование еще лучше. Грубо говоря, он может использовать «дробные» биты для кодирования информации. В Интернете доступно множество руководств по арифметическому кодированию. Здесь мы пропустим детали (особенно практической реализации, которая может быть немного хитрой с точки зрения программирования) из-за других ресурсов, доступных онлайн, но если кто-то пожалуется, возможно, этот раздел может быть расширен более подробно.
Если у вас есть текст, фактически сгенерированный известной языковой моделью, то арифметическое кодирование обеспечивает по существу оптимальное кодирование текста из этой модели. В некотором смысле это «решает» проблему сжатия для этой модели. (Таким образом, на практике основная проблема заключается в том, что модель неизвестна, и некоторые модели лучше, чем другие, моделируют человеческий текст.) Если в этом конкурсе не было допущено ошибок, то на языке предыдущего раздела Одним из способов решения этой проблемы было бы использование арифметического кодера для генерации файла «подсказок» из языковой модели, а затем использование арифметического декодера в качестве «интерпретатора».
В этом по существу оптимальном кодировании мы в конечном итоге тратим -log_2 (p) битов на символ с вероятностью p, а общая скорость кодирования - энтропия Шеннона . Это означает, что для символа с вероятностью, близкой к 1/2, требуется около одного бита для кодирования, а для символа с вероятностью 1/1000 - около 10 битов (поскольку 2 ^ 10 составляет примерно 1000).
Но показатель выигрыша для этой задачи был выбран правильно, чтобы избежать сжатия в качестве оптимальной стратегии. Мы должны найти способ сделать несколько ошибок в качестве компромисса для получения более короткого файла подсказок. Например, одна стратегия, которую можно попробовать, - это простая стратегия ветвления: мы обычно пытаемся использовать арифметическое кодирование, когда можем, но если распределение вероятностей из модели «плохое», мы просто угадываем наиболее вероятный символ и не не пытайтесь его кодировать.
Зачем делать ошибки?
Давайте проанализируем предыдущий пример, чтобы мотивировать, почему мы можем захотеть делать ошибки «намеренно». Если мы используем арифметическое кодирование для кодирования правильного символа, мы потратим примерно один бит в случае с E
или T
, но около десяти бит в случае с A
.
В целом, это довольно хорошая кодировка, тратящая чуть больше на символ, хотя есть три возможности; в принципе, A
это маловероятно, и мы не заканчиваем тратить соответствующие десять бит слишком часто. Однако разве не было бы неплохо, если бы мы могли просто сделать ошибку вместо случая A
? В конце концов, метрика для проблемы считает, что 1 байт = 8 бит длины эквивалентен 2 ошибкам; таким образом, кажется, что следует предпочесть ошибку, а не тратить более 8/2 = 4 бита на символ. Тратить больше байта на сохранение одной ошибки определенно звучит неоптимально!
Механизм «перемотки»
В этом разделе описывается основной умный аспект этого решения, который позволяет обрабатывать неправильные догадки без затрат на длину.
Для простого примера, который мы анализировали, механизм перемотки особенно прост. Интерпретатор читает один бит из файла подсказок. Если это 0, он угадывает E
. Если это 1, он угадает T
. В следующий раз, когда он вызывается, он видит правильный символ. Если файл подсказки настроен правильно, мы можем убедиться, что в случае E
или T
интерпретатор правильно угадывает. Но как насчет A
? Идея механизма перемотки заключается в том, чтобы просто не кодировать A
вообще . Точнее, если интерпретатор позже узнает, что правильный символ был A
, он метафорически « перематывает ленту»: он возвращает бит, который он прочитал ранее. Бит, который он читает, намеревается закодировать E
илиT
, но не сейчас; это будет использовано позже. В этом простом примере это в основном означает, что он продолжает угадывать один и тот же символ ( E
или T
) до тех пор, пока не получит его правильно; затем он читает еще один бит и продолжает идти.
Кодировка для этого файла подсказок очень проста: превратить все E
s в 0 бит и T
s в 1 бит, полностью игнорируя A
s. В результате анализа в конце предыдущего раздела эта схема допускает некоторые ошибки, но снижает общий балл, не кодируя ни одну из A
s. В меньшем эффекте, он на самом деле экономит на длину намеков файла , а также, потому что мы в конечном итоге , используя ровно один бит для каждого E
и T
, вместо того , чтобы немного больше , чем немного.
Маленькая теорема
Как мы решаем, когда сделать ошибку? Предположим, что наша модель дает нам распределение вероятности P для следующего символа. Мы разделим возможные символы на два класса: кодированные и не кодированные . Если правильный символ не закодирован, то мы в конечном итоге будем использовать механизм «перемотки», чтобы принять ошибку бесплатно. Если правильный символ закодирован, то мы будем использовать какой-то другой дистрибутив Q для его кодирования с использованием арифметического кодирования.
Но какое распределение Q мы должны выбрать? Нетрудно понять, что все кодированные символы должны иметь более высокую вероятность (в P), чем не кодированные символы. Также в дистрибутив Q должны входить только кодированные символы; в конце концов, мы не кодируем другие, поэтому мы не должны «тратить» на них энтропию. Немного сложнее увидеть, что распределение вероятностей Q должно быть пропорционально P на кодированных символах. Объединение этих наблюдений означает, что мы должны кодировать наиболее вероятные символы, но, возможно, не менее вероятные символы, и что Q - это просто P, масштабируемый на кодированных символах.
Более того, оказывается, что есть классная теорема о том, какой «обрез» нужно выбрать для кодирующих символов: вы должны кодировать символ, если он по меньшей мере равен 1 / 5,393 с той же вероятностью, что и другие кодированные символы вместе взятые. Это «объясняет» появление, казалось бы, случайной константы 5.393
ближе к концу указанной выше программы. Число 1 / 5,393 ≈ 0,18542 является решением уравнения -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
Возможно, разумно написать эту процедуру в коде. Этот фрагмент находится в 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;
}
Собираем все вместе
Предыдущий раздел, к сожалению, немного технический, но если мы соберем вместе все остальные части, структура будет следующей. Всякий раз, когда программу просят предсказать следующий символ после заданного правильного символа:
- Добавьте правильный символ к известному правильному префиксу Моби Дика.
- Обновите (марковскую) модель текста.
- Секретный соус : Если предыдущее предположение было неверным, перемотать состояние арифметического декодера его состояние перед предыдущей догадкой!
- Попросите модель Маркова предсказать распределение вероятности P для следующего символа.
- Преобразуйте P в Q, используя подпрограмму из предыдущего раздела.
- Попросите арифметический декодер декодировать символ из оставшейся части файла подсказок в соответствии с распределением Q.
- Угадай получившегося персонажа.
Кодировка файла подсказок работает аналогично. В этом случае программа знает, какой правильный следующий символ. Если это символ, который должен быть закодирован, тогда, конечно, следует использовать арифметический кодер на нем; но если это не кодированный символ, он просто не обновляет состояние арифметического кодировщика.
Если вы понимаете теоретико-информационный фон, такой как распределение вероятностей, энтропия, сжатие и арифметическое кодирование, но пытались и не смогли понять этот пост (за исключением того, почему теорема верна), дайте нам знать, и мы можем попытаться прояснить ситуацию. Спасибо за чтение!