Модуль 10. Алгоритмическая теория информации: кратчайшие описания, MDL и универсальное предсказание
Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.
Скачать файлы курса (.zip) 33,9 КБАрхив содержит девять Python-файлов решений, общих для RU/EN, с сохранением структуры каталогов.
Упражнение 1. Сколько строк можно сжать
Для фиксированной универсальной префиксной машины покажите, что среди -битных строк доля строк, удовлетворяющих
меньше .
-
Сколько бинарных программ имеют длину меньше ?
-
Почему одна завершившаяся программа не может описывать две разные строки?
-
Как изменится рассуждение для простой сложности ?
Показать ответ и решение
Рассмотрим бинарные программы длины строго меньше . Всего бинарных строк таких длин
Допустимых завершившихся программ префиксной машины не больше этого числа. Одна детерминированная программа при фиксированной машине имеет не более одного конечного выхода. Поэтому
Делим на общее число строк:
Итого
Сам подсчёт использует только число двоичных программ и детерминированность их выхода. Префиксность важна для выбранного варианта , для связи с неравенством Крафта и для последующих вероятностных весов .
Для простой сложности такой же элементарный подсчёт коротких программ даёт аналогичную долю. Различия между и проявляются в самоделимитации, цепных правилах и алгоритмической вероятности.
Упражнение 2. Короткий генератор и плохое практическое сжатие
Рассмотрите байт, сгенерированных детерминированным PRNG из фиксированного начального значения (seed).
-
Дайте верхнюю границу на через длину программы генератора, начального значения и описание .
-
Объясните, почему gzip всё же может вернуть почти бит.
-
Почему из этого не следует, что gzip «ошибся» в собственной задаче?
-
Какие дополнительные сведения должен получить декодер, чтобы воспользоваться коротким генеративным описанием?
Показать ответ и решение
Пусть — фиксированная программа псевдослучайного генератора (PRNG), — его начальное значение (seed), а — число выходных байтов. Программа для содержит:
-
описание ;
-
начальное значение ;
-
самоделимитирующееся описание ;
-
команду «сгенерировать первые байт».
Поэтому
При фиксированных и правая часть растёт лишь логарифмически с , тогда как сырой объект занимает бит.
Gzip ищет словари, повторения и локальные статистические закономерности определённых типов. Хороший PRNG специально выдаёт поток, который выглядит случайным для широкого класса ограниченных тестов. Короткая закономерность «это с начальным значением » существует, но не входит в язык gzip.
Это не ошибка gzip. Его задача — найти хороший код в фиксированном формате за ограниченное время, а не минимизировать по всем программам.
Декодеру нужны точная реализация и версия PRNG, начальное значение, длина , соглашения о преобразовании состояния в байты и, при необходимости, платформенные детали. Все они входят в полную длину описания.
Наконец, короткое описание не гарантирует быстрого декодирования. Определение не штрафует время исполнения; программа минимальной длины может работать чрезвычайно долго.
Упражнение 3. Энтропия и ожидаемая колмогоровская сложность
Пусть
-
Найдите .
-
Объясните, почему для префиксной сложности.
-
Если описание распределения стоит бит, какую верхнюю границу даёт
-
Почему эта граница становится содержательнее для блока из i.i.d. наблюдений? Какую дополнительную стоимость даёт описание ?
Показать ответ и решение
Для
получаем
Выбранные кратчайшие префиксные программы образуют префиксный код для исходов. Средняя длина любого такого кода не меньше энтропии:
При бит верхняя граница равна
Для одного исхода эта граница груба: стоимость модели доминирует. Для i.i.d. наблюдений декодеру нужно также передать длину блока. Более явная граница имеет вид
где . Поэтому
Фиксированная стоимость модели исчезает на символ при .
По одному наблюдению нельзя восстановить единственное распределение . Бесконечно много распределений назначают положительную вероятность тому же исходу. Поэтому короткое описание конкретного объекта и хорошая модель новых данных — разные требования.
Упражнение 4. Точка переключения MDL
Две модели имеют длины
Средняя длина кода данных на одном объекте равна
-
Запишите полные длины для выборки размера .
-
При каком минимальном модель становится короче?
-
Объясните, почему ответ иллюстрирует зависимость допустимой сложности модели от количества данных.
-
Что изменится, если стоимость зависит от точности представления параметров?
Показать ответ и решение
Полные длины:
Модель выигрывает, когда
то есть
При обе длины равны битам. Минимальный целый размер для строгого выигрыша :
Сложная модель платит дополнительные бит один раз, но экономит бита на каждом примере. На малой выборке доминирует цена модели; на большой накапливается выигрыш в кодировании данных.
Если зависит от точности параметров, стоимость модели сама может расти с размером выборки: большая выборка оправдывает более точную передачу весов. Тогда оптимизация идёт совместно по модели и по точности квантования.
MAP выбирает одну модель, минимизируя . Байесовский смешанный код использует и не обязан сначала объявлять одну гипотезу. Поэтому два протокола могут давать разные длины и разный выбор.
Упражнение 5. NCD вручную
Пусть компрессор дал
байт.
-
Найдите .
-
Как изменится ответ, если и используется симметризация через минимум?
-
Почему результат нельзя интерпретировать как точную NID?
-
Назовите два возможных артефакта для коротких файлов.
Показать ответ и решение
Без симметризации:
Если
то
Это не точная NID, потому что не равна , компрессор видит ограниченный класс закономерностей, длины включают заголовки и формат, а порядок конкатенации может менять словарь и окно.
Для коротких файлов особенно опасны фиксированные заголовки, недостаточная длина для построения словаря, блочное выравнивание и нерелевантные служебные данные.
Пригодность компрессора проверяют эмпирически: сравнивают несколько кодеков, оба порядка конкатенации, устойчивость к формату и предобработке, а затем оценивают метод ближайшего соседа или кластеризацию на отложенной задаче.
Упражнение 6. Python: компрессоры, предиктивно-последовательный MDL и NCD
Запустите файл M10_algorithmic_mdl_experiments.py.
Он выполняет три эксперимента:
-
сравнивает gzip, bz2 и LZMA на регулярных и псевдослучайных объектах;
-
сравнивает предиктивно-последовательную длину двух представлений с похожей итоговой точностью;
-
строит матрицу NCD для похожих текстов, фрагментов кода и ДНК-подобных последовательностей.
Ответьте:
- почему PRNG- и SHA-потоки плохо сжимаются обычными компрессорами, хотя имеют короткое генеративное описание;
- почему представление с меньшим числом нерелевантных измерений (
nuisance dimensions) получает меньшую последовательную длину; - почему NCD группирует пары внутри домена, но абсолютные значения зависят от bz2;
- какие проверки нужны перед использованием NCD как рабочей метрики.
Показать ответ и решение
Скрипт использует фиксированные начальные значения и параметры, поэтому результат воспроизводим.
1. Практические компрессоры
Типичный запуск даёт следующие значения в битах на входной байт:
| Объект | gzip | bz2 | LZMA |
|---|---|---|---|
| Нули | 0.0117 | 0.0052 | 0.0171 |
| Периодическая строка | 0.0168 | 0.0078 | 0.0181 |
| Повторяющийся англоязычный текст | 0.0343 | 0.0216 | 0.0244 |
| PRNG, начальное значение 7 | 8.0046 | 8.0789 | 8.0078 |
| Поток SHA-256 со счётчиком | 8.0046 | 8.0756 | 8.0078 |
Первые три объекта содержат регулярности, входящие в индуктивное смещение кодеков. Последние два имеют короткие генеративные программы, но выглядят статистически случайными для этих алгоритмов. Служебные расходы даже делают архив немного длиннее исходных восьми бит на байт.
2. Предиктивно-последовательный MDL
Типичный результат:
| Представление | бит/метку | итоговая точность |
|---|---|---|
| 4 информативных измерения | 0.7423 | 0.7440 |
| 4 сигнальных + 96 нерелевантных | 0.9086 | 0.7270 |
Второе представление содержит тот же основной сигнал, но лишние измерения ухудшают эффективность обучения на малой выборке. Пробник медленнее находит полезное направление и в ранних блоках назначает меткам худшие вероятности. Предиктивно-последовательный код накапливает эту цену, тогда как одна итоговая точность частично её скрывает.
Даже при точно одинаковой итоговой точности предиктивно-последовательные длины могли бы различаться: они зависят от всей траектории обучения, а не только от последней точки.
3. NCD
Внутридоменные пары получают наименьшие внедиагональные расстояния:
text-A/text-B: около ;code-A/code-B: около ;dna-A/dna-B: около .
Большинство междоменных расстояний лежит примерно между и .
Диагональ не обязана быть строго нулевой: реальные заголовки и неидеальность свойства создают остаток. Это наглядное отличие практической NCD от теоретической NID.
Перед рабочим использованием следует проверить выбор компрессора, оба порядка конкатенации, влияние размера и заголовков, предобработку, качество на отложенной задаче и базовые расстояния для переставленных или доменно согласованных отрицательных пар.
Программная часть решения · один исходный файл для RU/EN
from __future__ import annotations
import bz2
import gzip
import hashlib
import json
import lzma
import math
import random
import zlib
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, log_loss
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
ROOT = Path(__file__).resolve().parent
OUT = ROOT / "generated_experiment"
OUT.mkdir(exist_ok=True)
SEED = 20260713
rng = np.random.default_rng(SEED)
def repeat_to_length(pattern: bytes, n: int) -> bytes:
return (pattern * (n // len(pattern) + 1))[:n]
def python_prng_bytes(n: int, seed: int = 7) -> bytes:
# A short generator plus a short seed describes this object, even though
# generic compressors will usually see random-looking bytes.
r = random.Random(seed)
return bytes(r.randrange(256) for _ in range(n))
def hash_counter_stream(n: int, seed: bytes = b"course-m10") -> bytes:
out = bytearray()
counter = 0
while len(out) < n:
out.extend(hashlib.sha256(seed + counter.to_bytes(8, "big")).digest())
counter += 1
return bytes(out[:n])
COMPRESSORS: dict[str, Callable[[bytes], bytes]] = {
"gzip": lambda x: gzip.compress(x, compresslevel=9),
"bz2": lambda x: bz2.compress(x, compresslevel=9),
"lzma": lambda x: lzma.compress(x, preset=9),
}
def compressor_experiment(n: int = 65536) -> list[dict[str, float | str]]:
corpus = {
"zeros": b"\x00" * n,
"periodic": repeat_to_length(b"abc123|", n),
"English-like": repeat_to_length(
b"prediction and compression are two views of the same log-loss; ", n
),
"PRNG seed=7": python_prng_bytes(n, 7),
"SHA256 stream": hash_counter_stream(n),
}
rows: list[dict[str, float | str]] = []
for name, data in corpus.items():
for comp_name, comp in COMPRESSORS.items():
packed = comp(data)
rows.append(
{
"object": name,
"compressor": comp_name,
"raw_bytes": len(data),
"compressed_bytes": len(packed),
"bits_per_byte": 8.0 * len(packed) / len(data),
}
)
names = list(corpus)
x = np.arange(len(names))
width = 0.24
fig, ax = plt.subplots(figsize=(10.5, 5.6))
for j, comp_name in enumerate(COMPRESSORS):
vals = [
next(
float(row["bits_per_byte"])
for row in rows
if row["object"] == name and row["compressor"] == comp_name
)
for name in names
]
ax.bar(x + (j - 1) * width, vals, width=width, label=comp_name)
ax.axhline(8.0, linewidth=1.2, linestyle="--", label="raw: 8 bits/byte")
ax.set_xticks(x, names, rotation=18, ha="right")
ax.set_ylabel("bits per input byte")
ax.set_title("Practical compressors are computable upper-bound mechanisms, not K-oracles")
ax.legend(ncol=2)
fig.tight_layout()
fig.savefig(OUT / "M10_experiment_compressor_bounds.png", dpi=190)
plt.close(fig)
return rows
@dataclass
class PrequentialResult:
name: str
total_bits: float
bits_per_label: float
final_accuracy: float
cumulative_points: list[tuple[int, float]]
def make_classification_representations(n: int = 5000) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
z = rng.normal(size=(n, 4))
logits = 1.3 * z[:, 0] - 0.9 * z[:, 1] + 0.5 * z[:, 2]
probs = 1.0 / (1.0 + np.exp(-logits))
y = rng.binomial(1, probs)
easy = z + rng.normal(scale=0.08, size=z.shape)
nuisance = rng.normal(size=(n, 96))
hard = np.concatenate([z + rng.normal(scale=0.18, size=z.shape), nuisance], axis=1)
return easy, hard, y.astype(int)
def prequential_code(
X: np.ndarray,
y: np.ndarray,
name: str,
boundaries: list[int],
) -> PrequentialResult:
n = len(y)
if boundaries[-1] != n:
raise ValueError("last boundary must equal dataset size")
first = boundaries[0]
total_bits = float(first) # uniform binary code for first labels
points: list[tuple[int, float]] = [(first, total_bits)]
for left, right in zip(boundaries[:-1], boundaries[1:]):
model = make_pipeline(
StandardScaler(),
LogisticRegression(max_iter=2500, C=1.0, solver="lbfgs", random_state=SEED),
)
model.fit(X[:left], y[:left])
probs = model.predict_proba(X[left:right])
block_nats = log_loss(y[left:right], probs, labels=[0, 1], normalize=False)
total_bits += float(block_nats / math.log(2.0))
points.append((right, total_bits))
final_model = make_pipeline(
StandardScaler(),
LogisticRegression(max_iter=2500, C=1.0, solver="lbfgs", random_state=SEED),
)
split = int(0.8 * n)
final_model.fit(X[:split], y[:split])
acc = accuracy_score(y[split:], final_model.predict(X[split:]))
return PrequentialResult(
name=name,
total_bits=total_bits,
bits_per_label=total_bits / n,
final_accuracy=float(acc),
cumulative_points=points,
)
def prequential_experiment() -> list[PrequentialResult]:
X_easy, X_hard, y = make_classification_representations()
boundaries = [64, 128, 256, 512, 1024, 2048, 3500, len(y)]
results = [
prequential_code(X_easy, y, "4 informative dimensions", boundaries),
prequential_code(X_hard, y, "4 signal + 96 nuisance dimensions", boundaries),
]
fig, ax = plt.subplots(figsize=(9.6, 5.4))
for result in results:
xs = [p[0] for p in result.cumulative_points]
ys = [p[1] / p[0] for p in result.cumulative_points]
ax.plot(xs, ys, marker="o", label=result.name)
ax.axhline(1.0, linestyle="--", linewidth=1.2, label="uniform binary code")
ax.set_xscale("log", base=2)
ax.set_xlabel("number of labels encoded")
ax.set_ylabel("cumulative bits per label")
ax.set_title("Prequential MDL rewards representations that are learned quickly")
ax.legend()
fig.tight_layout()
fig.savefig(OUT / "M10_experiment_prequential.png", dpi=190)
plt.close(fig)
return results
def ncd(x: bytes, y: bytes, compressor: Callable[[bytes], bytes]) -> float:
cx = len(compressor(x))
cy = len(compressor(y))
cxy = min(len(compressor(x + y)), len(compressor(y + x)))
return (cxy - min(cx, cy)) / max(cx, cy)
def mutate(data: bytes, every: int, replacement: bytes) -> bytes:
out = bytearray(data)
for i in range(0, len(out), every):
out[i : i + len(replacement)] = replacement[: min(len(replacement), len(out) - i)]
return bytes(out)
def ncd_experiment() -> tuple[list[str], np.ndarray]:
n = 24000
objects = {
"text-A": repeat_to_length(b"the model predicts the next token and pays in bits. ", n),
"text-B": mutate(repeat_to_length(b"the model predicts the next token and pays in bits. ", n), 337, b"loss"),
"code-A": repeat_to_length(b"for(i=0;i<n;i++){sum+=a[i];}\n", n),
"code-B": mutate(repeat_to_length(b"for(i=0;i<n;i++){sum+=a[i];}\n", n), 401, b"while"),
"dna-A": repeat_to_length(b"ACGTACGTGATTACA", n),
"dna-B": mutate(repeat_to_length(b"ACGTACGTGATTACA", n), 389, b"TTAA"),
}
labels = list(objects)
comp = lambda b: bz2.compress(b, compresslevel=9)
matrix = np.zeros((len(labels), len(labels)))
for i, a in enumerate(labels):
for j, b in enumerate(labels):
matrix[i, j] = ncd(objects[a], objects[b], comp)
fig, ax = plt.subplots(figsize=(7.5, 6.4))
im = ax.imshow(matrix, vmin=0.0, vmax=max(1.0, float(matrix.max())))
ax.set_xticks(np.arange(len(labels)), labels, rotation=35, ha="right")
ax.set_yticks(np.arange(len(labels)), labels)
ax.set_title("Normalized compression distance: shared regularity becomes proximity")
for i in range(len(labels)):
for j in range(len(labels)):
ax.text(j, i, f"{matrix[i, j]:.2f}", ha="center", va="center", fontsize=8)
fig.colorbar(im, ax=ax, label="NCD")
fig.tight_layout()
fig.savefig(OUT / "M10_experiment_ncd.png", dpi=190)
plt.close(fig)
return labels, matrix
def main() -> None:
compressor_rows = compressor_experiment()
prequential_results = prequential_experiment()
labels, matrix = ncd_experiment()
summary = {
"compressor_rows": compressor_rows,
"prequential": [
{
"name": r.name,
"total_bits": r.total_bits,
"bits_per_label": r.bits_per_label,
"final_accuracy": r.final_accuracy,
}
for r in prequential_results
],
"ncd_labels": labels,
"ncd_matrix": matrix.tolist(),
}
(OUT / "M10_experiment_results.json").write_text(
json.dumps(summary, indent=2, ensure_ascii=False), encoding="utf-8"
)
print("Compression experiment (bits per byte):")
for row in compressor_rows:
print(
f" {row['object']:14s} {row['compressor']:4s}: "
f"{float(row['bits_per_byte']):.3f}"
)
print("\nPrequential results:")
for r in prequential_results:
print(
f" {r.name}: {r.bits_per_label:.4f} bits/label, "
f"final accuracy={r.final_accuracy:.4f}"
)
print("\nNCD matrix:")
print(np.array2string(matrix, precision=3, suppress_small=True))
if __name__ == "__main__":
main()
Бонус. Универсальное доминирование и избыточная логарифмическая потеря
Пусть универсальная полумера удовлетворяет
для всех конечных префиксов вычислимого источника .
-
Покажите, что
-
Разложите отношение правдоподобий по времени с помощью цепного правила.
-
Объясните, почему это даёт конечную суммарную доплату по логарифмической потере, но не делает универсальный предиктор вычислимым.
Показать ответ и решение
Из
следует
Берём двоичный логарифм:
Для последовательности при корректно определённых условных вероятностях
После усреднения по каждый ожидаемый член становится условной KL-дивергенцией. Суммарная ожидаемая избыточная логарифмическая потеря ограничена битами.
Сложность истинной модели оплачивается именно множителем , а после логарифмирования — аддитивным членом .
Граница не делает вычислимой. Можно находить всё новые программы и увеличивать частичную сумму, но невозможно узнать, что никакая ещё программа не внесёт вклад.