# теория · шаг 5 из 8
Арифметическое кодирование: меньше бита на символ
Коротко
Код Хаффмана даёт каждому символу целое число бит - не меньше одного, как бы часто символ ни встречался.
Арифметическое кодирование не кодирует символы по отдельности: оно превращает всё сообщение в одно число.
Отрезок от 0 до 1 сужается символ за символом, каждый раз в долю этого символа; ширина итогового отрезка
равна произведению долей, и чтобы назвать число внутри него, хватает log₂(1/ширина) бит плюс два.
Это энтропия плюс два бита на ВСЁ сообщение, а не на символ. Сто символов, из которых 95 - буква a:
Хаффман тратит 100 бит, арифметическое кодирование - 30, энтропия - 28,6. При долях, равных степеням
двойки, выигрыша нет.
Если ты сузишь отрезок для сообщения aab, объяснишь, почему число бит равно логарифму от ширины, и скажешь, что в настоящих кодировщиках заменяет дроби неограниченной точности, - листай до «Теперь сам».
Как думаешь, можно ли потратить треть бита
В сообщении два символа: a в 95 позициях из ста, b в пяти. Энтропия - 0,29 бита на символ. Код
Хаффмана даёт a код 0, а b код 1: один бит на символ, в три с половиной раза больше энтропии. И короче одного бита
код символа быть не может.
Прежде чем читать дальше, подумай: как вообще можно потратить на символ ТРЕТЬ бита? Запись состоит из целых битов. Что должно измениться в самом способе кодирования, чтобы дробные биты получили смысл?
На что это похоже
Представь почтовый адрес. Страна, область, город, улица, дом: каждое слово сужает место на карте. Но сужает по-разному. «Москва» отсекает меньше, чем название деревни, - в Москве живёт каждый десятый, и письму туда хватило бы совсем короткой пометки. Название деревни отсекает почти всё, и за такое уточнение приходится платить длинным словом.
Адрес не кодирует каждое слово отдельным ящиком фиксированного размера. Он задаёт одну точку на карте, и длина всей записи зависит от того, насколько мала итоговая область. Частое уточнение сужает слабо и стоит дёшево - в том числе меньше одного знака.
Механизм: отрезок, который сужается
Договоримся, как делить отрезок: каждому символу - часть, пропорциональная его доле. Пусть a - три
четверти, b - одна четверть, и a занимает нижнюю часть.
Кодируем aba. Начинаем с отрезка от 0 до 1. Каждый символ выбирает свою часть текущего отрезка, и
дальше делится уже она - в тех же пропорциях. В записи отрезка квадратная скобка значит «граница входит»,
круглая - «не входит»: число 3/4 принадлежит уже части символа b.
начало [0, 1) ширина 1
символ a - нижние 3/4 [0, 3/4) ширина 3/4
символ b - верхняя 1/4 [9/16, 3/4) ширина 3/16
символ a - нижние 3/4 [9/16, 45/64) ширина 9/64
Ширина на каждом шаге умножается на долю символа: 3/4 · 1/4 · 3/4 = 9/64. Любое число из итогового
отрезка однозначно называет сообщение aba: другое сообщение той же длины попало бы в другой отрезок, и
отрезки разных сообщений не пересекаются.
Осталось назвать число. Двоичная дробь читается как десятичная, только разряды после запятой - половина,
четверть, восьмая. Возьмём 5/8 = 1/2 + 1/8 - в двоичной записи 0,101. Оно лежит между 9/16 = 0,5625
и 45/64 ≈ 0,703. В код идут цифры после запятой: три бита 101 - и сообщение закодировано.
Сколько бит нужно в общем случае. Двоичные дроби из k бит стоят на числовой прямой с шагом 1/2ᵏ.
Чтобы хоть одна наверняка попала в отрезок ширины w, шаг должен быть не больше w: 2ᵏ ≥ 1/w, то
есть k ≥ log₂(1/w). Логарифм здесь - число удвоений точности, нужное, чтобы сетка стала мельче отрезка.
И последний шаг - откуда энтропия. Ширина - произведение долей, а логарифм произведения равен сумме
логарифмов: чтобы сетка стала мельче произведения двух долей, её надо удвоить столько раз, сколько нужно
для первой доли, и ещё столько, сколько для второй. Значит, каждый символ с долей p добавляет к длине
кода log₂(1/p) бит. Символ с долей p встречается в сообщении длины n примерно p · n раз, и сумма
по всему сообщению равна n · Σ p · log₂(1/p) - энтропии, умноженной на длину.
Правило
Ширина итогового отрезка равна произведению долей всех символов сообщения. Число бит - log₂(1/ширина)
плюс не больше двух. Логарифм произведения - сумма логарифмов, поэтому длина кода равна сумме
log₂(1/p) по всем символам сообщения, то есть энтропии, умноженной на длину, плюс два бита. Символ с
долей 0,95 добавляет к длине log₂(1/0,95) = 0,074 бита - дробные биты складываются внутри одного числа.
Распаковщику нужны те же доли и длина сообщения.
Разбор: программа на целых числах
Дроби с плавающей точкой не годятся: после нескольких десятков символов отрезок станет уже, чем их
точность. Программа ведёт отрезок точно - числитель нижней границы, числитель ширины и общий знаменатель.
Целые числа в Python не ограничены по длине, и это всё, что здесь нужно. Доли берутся из самого
сообщения. Отрезок - это [lo / znam, (lo + shir) / znam). Шаг «взять часть символа» сдвигает нижнюю
границу на долю nachalo[c] / n от текущей ширины и умножает ширину на kol[c] / n; чтобы остаться в
целых числах, знаменатель умножается на n, а вместе с ним и lo. Прибавка znam - 1 перед делением
нацело - округление вверх.
def arif(s):
kol = {}
for c in s:
kol[c] = kol.get(c, 0) + 1
nachalo = {} # сколько символов лежит на отрезке ниже данного
nizhe = 0
for c in sorted(kol.keys()):
nachalo[c] = nizhe
nizhe = nizhe + kol[c]
n = len(s)
lo = 0 # отрезок [lo / znam, (lo + shir) / znam)
shir = 1
znam = 1
for c in s:
lo = lo * n + shir * nachalo[c]
shir = shir * kol[c] # ширина умножилась на долю kol[c] / n
znam = znam * n
bit = 0
while True:
bit = bit + 1
m = (lo * 2 ** bit + znam - 1) // znam # первая дробь m / 2^bit не левее lo
if (m + 1) * znam <= (lo + shir) * 2 ** bit: # её клетка целиком внутри отрезка
break
return [bit, m]
print(arif("abracadabra")) # [23, 2338649]
Последний цикл ищет самую короткую двоичную дробь, у которой внутри отрезка лежит не только она сама, но и вся её клетка сетки - до следующей дроби. Тогда после этих бит можно дописывать что угодно, и распаковщик не собьётся. За это и платятся «плюс два бита».
Распаковка идёт тем же путём: смотрим, в чью часть текущего отрезка попало число, выписываем этот символ,
сужаем отрезок до этой части - и так n раз.
Разбор: замер против Хаффмана и граница выигрыша
сообщение символов энтропия · n Хаффман арифметическое
abracadabra 11 22,4 23 23
("a" * 19 + "b") * 5 100 28,6 100 30
"aaaab" * 20 100 72,2 100 74
"ab" * 50 100 100 100 100
Ответ на вопрос из начала урока - вторая строка. Тридцать бит на сто символов: 0,3 бита на символ.
Никакой отдельный символ не получил «треть бита»; закодировано число, и каждая буква a сузила отрезок
чуть-чуть - на множитель 0,95.
Первая и последняя строки - граница. Когда доли равны степеням двойки - половина, четверть, восьмая, - код Хаффмана стоит точно на пределе, а когда близки к ним - почти на пределе, и арифметическое кодирование ничего не добавляет. Выигрыш появляется при перекосе: чем ближе доля частого символа к единице, тем дальше Хаффман от энтропии.
Во всех четырёх строках не посчитана таблица долей, которую надо передать распаковщику, - её цена одинакова для обоих методов.
Что ломается без этого
Первое: числа растут без предела. В программе выше lo и znam - числа длиной во всё сообщение, и
каждое умножение стоит тем дороже, чем дальше мы зашли: квадратичное время. Рабочие кодировщики держат
границы в машинном слове. Как только старшие биты нижней и верхней границы совпали, они уже не изменятся -
их выдают в выходной поток, а границы сдвигают влево. То же в десятичной записи: у границ 0,4172 и
0,4188 цифры «0,41» уже общие и при любом сужении останутся такими - их можно записать и забыть. Такой вариант называют кодированием диапазона.
Второе: доли у сторон разошлись. Распаковщик делит отрезок по своим долям. Если они хоть немного отличаются от долей упаковщика - например, из-за округления дробей, - с какого-то символа число попадёт в чужую часть, и всё дальнейшее превратится в мусор. Поэтому доли задают целыми счётчиками, а не дробями.
Третье: конец сообщения не обозначен. Число 0,101 кодирует и aba, и abaa, и любое продолжение,
отрезок которого его содержит. Нужна либо длина, переданная заранее, либо особый символ конца.
Четвёртое: один испорченный бит. У кода Хаффмана ошибка портит несколько символов, после чего чтение часто возвращается в колею. Здесь всё сообщение - одно число: ошибка в бите меняет его, и с этого места до конца распаковка выдаёт другое сообщение.
Зачем это в работе
Первый сюжет - видео и изображения. В видеокодеках H.264 и H.265 последняя ступень - двоичное арифметическое кодирование с долями, которые подстраиваются под контекст. Перекос там огромный: «этот коэффициент равен нулю» верно в подавляющем большинстве случаев, и Хаффман терял бы на каждом таком ответе почти целый бит.
Второй - архиваторы. Формат xz кодирует диапазоном каждый бит выхода LZ77. Формат bzip2 мог бы делать так же: его предшественник использовал арифметическое кодирование и был переведён на код Хаффмана из-за патентов, которые тогда действовали. Патенты истекли, а формат остался.
Третий - то, что пришло на смену. Арифметическое кодирование на каждый символ тратит умножение и деление; код Хаффмана - одно обращение к таблице. В 2009 году Ярек Дуда предложил асимметричные системы счисления (ANS): сжатие как у арифметического кодирования при скорости таблиц. На них работает энтропийная ступень zstd и нескольких новых форматов изображений (энтропийная ступень - последняя часть архиватора, та, что кодирует символы по их долям). Идея та же - копить дробные биты в одном числе, - а арифметика другая.
Теперь сам
Сообщение aab. Доли возьми из него самого: a - 2/3, b - 1/3, a занимает нижнюю часть. Найди
итоговый отрезок и его ширину. Лежит ли в нём число 3/8, то есть 0,011 в двоичной записи? Сколько бит
потратил бы код Хаффмана?
Ответ.
начало [0, 1)
символ a - нижние 2/3 [0, 2/3)
символ a - нижние 2/3 [0, 4/9)
символ b - верхняя 1/3 [8/27, 4/9) ширина 4/27
Ширина - произведение долей: 2/3 · 2/3 · 1/3 = 4/27. Границы в десятичном виде - примерно 0,296 и
0,444; число 3/8 = 0,375 лежит внутри. Трёх бит 011 хватает, чтобы назвать сообщение, если длина
известна и после этих бит ничего не дописано. Программа из урока требует большего - чтобы внутри отрезка
лежала вся клетка сетки, - и для aab возвращает [4, 5]: четыре бита, 0,0101.
Код Хаффмана тратит по биту на символ - три бита. На трёх символах выигрыша нет и быть не может:
log₂(27/4) = 2,75, это округляется до трёх, а «плюс два бита» накладных съедают и это. Дробные биты
начинают складываться в заметную разницу только на длинных сообщениях с перекошенными долями.
Главное
Арифметическое кодирование снимает ограничение, на котором стоит код Хаффмана: целое число бит на символ. Сообщение кодируется целиком - числом из отрезка, ширина которого равна произведению долей его символов, - и длина кода отличается от энтропии на два бита на всё сообщение. Цена - арифметика на каждом символе и хрупкость: обе стороны обязаны делить отрезок одинаково до последнего знака. Выигрыш зависит от данных: при долях, равных степеням двойки, его нет, при доле частого символа 0,95 запись втрое короче хаффмановской.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий