VKВлад КуликовОткрытые лекции
RL для LLM10 / 13

Модуль 10. Поиск, проверка и вычислительный бюджет на этапе генерации

Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.

Скачать файлы курса (.zip) 382,1 КБ

Архив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.

Четыре вида вычислительного бюджета

Система делает следующее: для простых запросов генерирует один ответ, для трудных — восемь; каждый ответ может содержать до 2 000 токенов; для двух лучших префиксов выполняется дополнительное ветвление.

Разнесите элементы по четырём категориям: параллельный, последовательный, поисковый и адаптивный бюджет. Объясните, почему одно число «8 попыток» не описывает всю стоимость.

Показать ответ и решение
  • Адаптивный бюджет: простому запросу выделяется один ответ, трудному — восемь. Число попыток зависит от запроса.
  • Параллельный бюджет: для трудного запроса создаются восемь кандидатов.
  • Последовательный бюджет: один кандидат может продолжаться до 2 000 токенов.
  • Поисковый бюджет: два наиболее перспективных префикса ветвятся и получают дополнительные продолжения.

Число «8» описывает только ширину одного этапа. Оно не сообщает длину кандидатов, число раскрытых ветвей, стоимость оценивания, задержку, возможность пакетной генерации и число вызовов внешней проверяющей системы. Для воспроизводимого сравнения нужны хотя бы токены, прямые проходы генератора, вызовы проверки, задержка и денежная стоимость.

Покрытие и качество вывода

Для задачи pass@32 равен 0.98, но точность ответа, который выдаёт система после ранжирования 32 кандидатов, равна 0.61.

Что можно заключить о генераторе и ранжировщике? Почему нельзя назвать 0.98 «точностью системы»?

Показать ответ и решение

pass@32 = 0.98 означает, что в 98% запросов среди 32 кандидатов присутствует хотя бы один правильный ответ — при заданном критерии проверки и протоколе генерации. Итоговая точность 0.61 означает, что ранжировщик часто не выбирает доступный правильный кандидат.

Из этого следует:

  1. генератор обладает высоким покрытием при данном бюджете;

  2. этап выбора является существенным ограничением системы;

  3. полезно измерить точность ранжировщика условно на событиях, где правильный кандидат вообще присутствует;

  4. стоит проверить, не ухудшает ли ранжирование ответ по сравнению с более простыми правилами: голосованием, точной проверкой или случайным выбором среди прошедших фильтр.

Нельзя называть 0.98 точностью всей системы, потому что пользователь получает один выбранный ответ, а не множество из 32 кандидатов.

Повторяющиеся ответы и условная независимость

Формула 1−(1−p)N1-(1-p)^N предсказывает заметный рост покрытия, но двадцать ответов модели почти дословно повторяют один и тот же неверный шаблон.

Доказывает ли одно это наблюдение нарушение условной независимости? Назовите альтернативные объяснения и две диагностики разнообразия, которые полезно добавить к pass@N.

Показать ответ и решение

Нет. Независимые выборки из распределения с низкой энтропией могут повторять одну ошибку. Расхождение также объясняют малая вероятность успеха на данном запросе, неточная оценка pp или объединение запросов с разными pp. Переиспользование префиксов и ветвей поиска — возможные механизмы зависимости, которые нужно установить по протоколу. Сравните покрытие по повторным группам с условно независимым прогнозом для каждого запроса и его неопределённостью.

Полезные диагностики:

  • число уникальных канонических финальных ответов;
  • средняя попарная текстовая или семантическая близость;
  • энтропия распределения канонических ответов;
  • доля крупнейшего кластера;
  • различие между фактической кривой pass@N и кривой, предсказанной по pass@1 в независимой модели.

Одна метрика разнообразия недостаточна: разные формулировки могут скрывать одну и ту же ошибку, а почти одинаковые рассуждения иногда приводят к разным итоговым ответам.

Когда самосогласованность ухудшает ответ

Правильный канонический ответ имеет вероятность 0.32, а одна типовая ошибка — 0.44. Остальная масса распределена между многими редкими ответами.

Что произойдёт с голосованием при росте числа кандидатов? Может ли pass@N при этом стремиться к единице?

Показать ответ и решение

При большом числе независимо сгенерированных кандидатов доли канонических ответов сходятся к их вероятностям. Типовая ошибка с вероятностью 0.44 является единственной модой, поэтому голосование по относительному большинству с вероятностью, стремящейся к единице, выберет именно её. Точность голосования стремится к нулю.

Одновременно pass@N может стремиться к единице, потому что правильный ответ имеет положительную вероятность 0.32:

1−(1−0.32)N⟶1.1-(1-0.32)^N\longrightarrow1.

Это точная демонстрация различия между наличием правильного кандидата и правилом выбора. Голосование помогает, когда правильный канонический ответ является наиболее вероятной категорией, а ошибки достаточно рассеяны.

Область действия предела двоичной проверки

В учебной модели система случайно выбирает среди принятых двоичным проверяющим правилом, поэтому при большом NN точность стремится к доле правильных среди принятых.

Почему этот предел нельзя переносить на ранжировщик с непрерывным баллом и выбором максимума?

Показать ответ и решение

В учебном протоколе каждый кандидат либо принят, либо отклонён. Если принятые кандидаты выбираются равновероятно, при большом NN вероятность отсутствия принятых стремится к нулю, а качество выбранного ответа стремится к доле правильных среди принятых.

Ранжировщик с непрерывным баллом реализует другой статистический объект: выбирается максимум из NN шумных оценок. При росте NN экстремальное значение может всё сильнее определяться положительной ошибкой оценивателя. Предел зависит от совместного распределения истинного качества и оценки, хвостов ошибок, числа кандидатов и правила разрешения ничьих. Формула для случайного выбора среди двоично принятых кандидатов к такому максимуму не относится.

Прокси растёт, качество падает

При увеличении BoN с 32 до 512 средняя оценка модели награды выросла, а независимая ручная оценка снизилась.

Приведите два возможных объяснения и три изменения протокола, которые помогут отличить реальное улучшение от переоптимизации прокси.

Показать ответ и решение

Возможные объяснения:

  1. Переоптимизация прокси: большой пул увеличивает шанс найти ответ, использующий систематическую ошибку модели награды.

  2. Сдвиг распределения: ответы-победители при N=512N=512 находятся дальше от данных, на которых оцениватель был обучен и проверен.

  3. Изменение длины или стиля: модель награды может предпочитать признаки, слабо связанные с полезностью.

  4. Шум ручной оценки: падение может быть случайным, если независимая выборка мала.

Изменения протокола:

  • заранее выделить независимый набор запросов и независимый оцениватель;
  • строить кривые прокси и целевой метрики на одной сетке NN с доверительными интервалами;
  • анализировать длину, стиль, домен, уверенность и расхождение ансамбля оценивателей;
  • проводить слепую ручную проверку выбранных ответов и случайных кандидатов;
  • задавать условие остановки по независимой метрике, а не по прокси;
  • обновлять данные оценивателя ответами из нового распределения.

Рост прокси сам по себе доказывает лишь то, что система научилась находить кандидаты с более высокой прокси-оценкой.

Что именно оценивает PRM

Для каждого префикса запускают десять продолжений текущей политики и записывают долю финальных успехов. Затем политика продолжения меняется.

Является ли старая метка «истинной ценностью состояния»? Что именно она оценивала и почему могла устареть?

Показать ответ и решение

Метка оценивала эмпирическую вероятность финального успеха при продолжении конкретной версией политики, конкретном декодировании, горизонте и проверяющей системе:

V^(s)=1M∑j=1M1{продолжение j успешно}.\widehat V(s) = \frac1M\sum_{j=1}^{M}\mathbf 1\{\text{продолжение }j\text{ успешно}\}.

Это оценка ценности относительно заданной политики продолжения, а не вечное свойство префикса и не «истинная ценность» вне постановки. После изменения политики распределение продолжений меняется; меняются и вероятность успеха, и ошибки Монте-Карло. Метка может устареть, особенно для состояний, в которых новая политика ведёт себя иначе.

Нужно хранить версию политики, параметры декодирования, число продолжений, проверяющую систему и дату генерации меток.

Сложный поиск против простой базы

MCTS с PRM улучшает pass@1, но использует в четыре раза больше токенов и в двадцать раз больше вызовов оценивателя, чем повторная генерация. Какие сравнения нужны перед выводом, что MCTS «лучше»?

Показать ответ и решение

Нужно сравнить методы как минимум при нескольких паритетах:

  1. одинаковое число сгенерированных токенов;

  2. одинаковое число прямых проходов генератора;

  3. одинаковое число вызовов PRM или другой проверки;

  4. одинаковая задержка или денежная стоимость;

  5. одинаковый максимальный контекст и правила остановки.

Следует измерять pass@1, pass@k/покрытие, качество выбранного ответа, долю технических ошибок, токены, задержку и стоимость. Нужны также сильные простые контрольные варианты: повторная генерация, BoN, самосогласованность и последовательное исправление.

MCTS может быть предпочтительнее при заданном ограничении качества или задержки, но утверждение «лучше вообще» без выбранного бюджета и метрики не определено.

pass@N руками

Одна попытка успешна с вероятностью p=0.35p=0.35. Попытки условно независимы.

  1. Вычислите pass@6.

  2. Вычислите прирост от шестой попытки относительно пяти попыток.

  3. Сформулируйте, что не следует из этих чисел без правила выбора.

Показать ответ и решение

При p=0.35p=0.35:

pass@⁡6=1−(1−0.35)6=1−0.656=0.924581109375.\operatorname{pass@}6 = 1-(1-0.35)^6 = 1-0.65^6 = 0.924581109375.

Прирост именно от шестой попытки:

Δ5=p(1−p)5=0.35⋅0.655=0.040610171875.\Delta_5 = p(1-p)^5 = 0.35\cdot0.65^5 = 0.040610171875.

То есть шестой кандидат добавляет примерно 4.06 процентного пункта покрытия. Без правила выбора из этого не следует, что система выдаёт правильный ответ с вероятностью 0.9246: это вероятность наличия хотя бы одного успеха в корзине.

Конечновыборочный pass@k

Для одного запроса сгенерировано n=100n=100 кандидатов, из которых c=7c=7 прошли точную проверку. Вычислите

1−(n−ck)(nk)1-\frac{\binom{n-c}{k}}{\binom{n}{k}}

при k=10k=10. Дайте ответ с четырьмя знаками после запятой.

Показать ответ и решение
pass@⁡10^=1−(9310)(10010)≈0.5332595857.\widehat{\operatorname{pass@}10} = 1- \frac{\binom{93}{10}}{\binom{100}{10}} \approx 0.5332595857.

С четырьмя знаками:

0.5333.\boxed{0.5333}.

Здесь мысленно выбираются 10 элементов без возвращения из уже сгенерированной сотни. Оцениватель не доказывает независимость исходных 100 генераций и не заменяет описание протокола генерации.

Двоичная проверяющая система и два резервных протокола

Кандидат правильный с вероятностью p=0.25p=0.25. Проверяющая система имеет TPR α=0.8\alpha=0.8 и FPR β=0.05\beta=0.05. Если принят хотя бы один кандидат, выбирается случайный принятый.

Для N=6N=6 вычислите:

  1. вероятность принятия qq;

  2. долю правильных среди принятых ρacc\rho_{\mathrm{acc}};

  3. долю правильных среди отклонённых ρrej\rho_{\mathrm{rej}};

  4. итоговую вероятность успеха, если при пустом множестве принятых выбирают случайного кандидата из той же отклонённой корзины;

  5. итоговую вероятность успеха, если вместо этого порождают свежего непроверенного кандидата;

  6. предел при N→∞N\to\infty.

Объясните, почему два резервных правила нельзя обозначать одним и тем же слагаемым.

Показать ответ и решение

Вероятность принятия:

q=pα+(1−p)β=0.25⋅0.8+0.75⋅0.05=0.2375.q=p\alpha+(1-p)\beta =0.25\cdot0.8+0.75\cdot0.05 =0.2375.

Точность среди принятых:

ρacc=pαq=0.20.2375≈0.8421053.\rho_{\mathrm{acc}} =\frac{p\alpha}{q} =\frac{0.2}{0.2375} \approx0.8421053.

Условная доля правильных среди отклонённых:

ρrej=p(1−α)1−q=0.050.7625≈0.0655738.\rho_{\mathrm{rej}} =\frac{p(1-\alpha)}{1-q} =\frac{0.05}{0.7625} \approx0.0655738.

Вероятность, что все шесть отклонены:

P0=(1−q)6=0.76256≈0.1965346.P_0=(1-q)^6=0.7625^6\approx0.1965346.

Если резерв выбирается из той же отклонённой корзины,

S6rej=(1−P0)ρacc+P0ρrej≈0.6894899.S_6^{\mathrm{rej}} =(1-P_0)\rho_{\mathrm{acc}}+P_0\rho_{\mathrm{rej}} \approx0.6894899.

Если создаётся свежий непроверенный кандидат,

S6fresh=(1−P0)ρacc+P0p≈0.7257361.S_6^{\mathrm{fresh}} =(1-P_0)\rho_{\mathrm{acc}}+P_0p \approx0.7257361.

При q>0q>0 вероятность пустого множества принятых стремится к нулю, поэтому оба протокола имеют один предел:

S∞=ρacc≈0.8421053.\boxed{S_\infty=\rho_{\mathrm{acc}}\approx0.8421053}.

Различие при конечном NN принципиально: наблюдение «кандидат отклонён» меняет вероятность его правильности. Слагаемое pp допустимо только для новой независимой попытки, а не для выбора среди уже отклонённых.

Предел голосования

Сравните два распределения канонических ответов:

p(1)=(0.34,0.33,0.20,0.13),p^{(1)}=(0.34,0.33,0.20,0.13), p(2)=(0.30,0.40,0.30).p^{(2)}=(0.30,0.40,0.30).

В каждом случае первый ответ правильный. Каков предел точности голосования по относительному большинству (plurality voting) при независимо сгенерированных кандидатах и отсутствии точной ничьей в пределе?

Показать ответ и решение

В первом распределении правильный ответ имеет вероятность 0.34, а ближайшая ошибка — 0.33. Правильный ответ является единственной модой. По закону больших чисел его эмпирическая доля в пределе превосходит доли остальных категорий, поэтому

Pr⁡(голосование верно)⟶1.\Pr(\text{голосование верно})\longrightarrow1.

Сходимость может быть медленной из-за малого разрыва 0.01.

Во втором распределении неправильный второй ответ имеет вероятность 0.40, превышающую 0.30 правильного. Поэтому

Pr⁡(голосование верно)⟶0.\Pr(\text{голосование верно})\longrightarrow0.

При этом pass@N для правильного ответа во втором случае всё равно стремится к единице, поскольку его вероятность положительна.

Четыре агрегации PRM

Для шагов получены оценки

(0.9,0.7,0.8).(0.9,0.7,0.8).

Вычислите минимум, среднее, произведение и последний балл. Как изменится произведение, если к цепочке добавить ещё десять шагов с оценкой 0.9?

Показать ответ и решение

Для (0.9,0.7,0.8)(0.9,0.7,0.8):

min⁡=0.7,\min=0.7, mean⁡=0.9+0.7+0.83=0.8,\operatorname{mean}=\frac{0.9+0.7+0.8}{3}=0.8, product⁡=0.9⋅0.7⋅0.8=0.504,\operatorname{product}=0.9\cdot0.7\cdot0.8=0.504, last⁡=0.8.\operatorname{last}=0.8.

После добавления десяти шагов с оценкой 0.9 произведение станет

0.504⋅0.910≈0.1757339338.0.504\cdot0.9^{10} \approx0.1757339338.

Хотя все добавленные шаги имеют высокий балл, произведение уменьшается. Это корректно только при вероятностной семантике совместного события; иначе получается неявный штраф за длину.

Адаптивный бюджет и окупаемость

Часть 1. Для трёх запросов известны вероятности успеха одной независимой попытки

p=(0.1,0.3,0.6).p=(0.1,0.3,0.6).

Общий бюджет — 9 попыток, минимум одна на запрос. Последовательно назначайте каждую дополнительную попытку запросу с максимальным pi(1−pi)nip_i(1-p_i)^{n_i}. Найдите итоговое распределение и среднее покрытие.

Часть 2. Дополнительное обучение стоит 5 000 условных единиц и уменьшает стоимость запроса с 0.08 до 0.03. Найдите точку окупаемости только по стоимости.

Показать ответ и решение

Начинаем с (1,1,1)(1,1,1). Предельный прирост для запроса ii при текущем числе попыток nin_i равен

Δi=pi(1−pi)ni.\Delta_i=p_i(1-p_i)^{n_i}.

Последовательное жадное распределение шести дополнительных попыток даёт

(n1,n2,n3)=(2,4,3).\boxed{(n_1,n_2,n_3)=(2,4,3)}.

Среднее покрытие:

13[1−0.92+1−0.74+1−0.43]=0.6286333333.\frac13\left[ 1-0.9^2 + 1-0.7^4 + 1-0.4^3 \right] = 0.6286333333.

Точка окупаемости по стоимости решает

5000+0.03Q=0.08Q.5000+0.03Q=0.08Q.

Отсюда

Q=50000.05=100000.\boxed{Q=\frac{5000}{0.05}=100000}.

Это расчёт только денежных затрат. В реальном решении нужно добавить качество, задержку, риск и стоимость внедрения.

Кривые pass@N

Реализуйте устойчивые функции pass_at_n и aggregate_pass_curve. Обрабатывайте p=0p=0, p=1p=1, пустой список и некорректные вероятности.

Python
from __future__ import annotations

from collections.abc import Iterable, Sequence
import numpy as np


def pass_at_n(p: float, n: int) -> float:
    """Вероятность хотя бы одного успеха."""
    raise NotImplementedError


def aggregate_pass_curve(ps: Sequence[float], budgets: Iterable[int]) -> np.ndarray:
    """Среднее покрытие по запросам для каждого бюджета."""
    raise NotImplementedError

Проверка:

Python
assert abs(pass_at_n(0.5, 4) - 0.9375) < 1e-12
curve = aggregate_pass_curve([0.5, 0.2, 0.05, 0.9], [1, 2, 4])
assert np.allclose(curve, [0.4125, 0.549375, 0.6783234375])
Показать ответ и решение
Python
from __future__ import annotations

from collections.abc import Iterable, Sequence
import math
import numpy as np


def pass_at_n(p: float, n: int) -> float:
    p = float(p)
    if not math.isfinite(p) or not 0.0 <= p <= 1.0:
        raise ValueError("p must lie in [0, 1]")
    if isinstance(n, bool) or int(n) != n or int(n) <= 0:
        raise ValueError("n must be a positive integer")
    n = int(n)
    if p == 0.0:
        return 0.0
    if p == 1.0:
        return 1.0
    return float(-np.expm1(n * np.log1p(-p)))


def aggregate_pass_curve(ps: Sequence[float], budgets: Iterable[int]) -> np.ndarray:
    probabilities = np.asarray(ps, dtype=np.float64)
    budgets = list(budgets)
    if probabilities.ndim != 1 or probabilities.size == 0:
        raise ValueError("ps must be non-empty")
    if not np.isfinite(probabilities).all() or np.any((probabilities < 0) | (probabilities > 1)):
        raise ValueError("every probability must lie in [0, 1]")
    if not budgets:
        raise ValueError("budgets must be non-empty")
    return np.asarray(
        [np.mean([pass_at_n(float(p), int(n)) for p in probabilities]) for n in budgets],
        dtype=np.float64,
    )

log1p и expm1 уменьшают потерю точности, когда pp мал, а NN велик.

Голосование с честным разрешением ничьей

Реализуйте моделирование голосования по относительному большинству. При ничьей победитель выбирается равновероятно среди всех категорий с максимальным числом голосов. Правильной считается категория с индексом correct_index.

Python
from __future__ import annotations
from collections.abc import Sequence
import numpy as np


def plurality_accuracy_mc(
    probabilities: Sequence[float],
    k: int,
    *,
    correct_index: int = 0,
    trials: int = 100_000,
    seed: int = 10,
) -> float:
    raise NotImplementedError

Проверьте в пределах статистической погрешности два случая:

Python
assert abs(plurality_accuracy_mc([0.4, 0.15, 0.15, 0.15, 0.15], 16) - 0.7942) < 0.01
assert abs(plurality_accuracy_mc([0.3, 0.45, 0.25], 64) - 0.0771) < 0.01

Почему np.argmax без дополнительной обработки является не нейтральным правилом ничьей?

Показать ответ и решение
Python
from __future__ import annotations
from collections.abc import Sequence
import numpy as np


def plurality_accuracy_mc(
    probabilities: Sequence[float],
    k: int,
    *,
    correct_index: int = 0,
    trials: int = 100_000,
    seed: int = 10,
) -> float:
    p = np.asarray(probabilities, dtype=np.float64)
    if p.ndim != 1 or p.size < 2:
        raise ValueError("at least two categories are required")
    if not np.isfinite(p).all() or np.any(p < 0) or not np.isclose(p.sum(), 1.0):
        raise ValueError("probabilities must form a distribution")
    if isinstance(k, bool) or int(k) != k or int(k) <= 0:
        raise ValueError("k must be a positive integer")
    if isinstance(trials, bool) or int(trials) != trials or int(trials) <= 0:
        raise ValueError("trials must be a positive integer")
    if not 0 <= int(correct_index) < p.size:
        raise ValueError("correct_index is out of range")

    rng = np.random.default_rng(seed)
    counts = rng.multinomial(int(k), p, size=int(trials))
    correct = 0
    for row in counts:
        winners = np.flatnonzero(row == row.max())
        selected = int(rng.choice(winners))
        correct += selected == int(correct_index)
    return correct / int(trials)

np.argmax возвращает первый максимальный индекс. Если правильный ответ хранится под индексом 0, все ничьи искусственно повышают измеренную точность; при другом индексе — понижают. Это свойство представления массива, а не протокола голосования.

Конечный N и явное резервное правило

Реализуйте формулу двоичной проверяющей системы с двумя вариантами fallback_mode: "rejected_pool" и "fresh_unverified". Функция должна возвращать qq, точность среди принятых и отклонённых, вероятность отсутствия принятых и итоговую вероятность успеха.

Python
from __future__ import annotations


def binary_verifier_success(
    n: int,
    p_correct: float,
    true_positive_rate: float,
    false_positive_rate: float,
    *,
    fallback_mode: str = "rejected_pool",
) -> dict[str, float]:
    raise NotImplementedError

Для (8, 0.2, 0.9, 0.1) ожидаются:

Python
r = binary_verifier_success(8, 0.2, 0.9, 0.1, fallback_mode="rejected_pool")
assert abs(r["q"] - 0.26) < 1e-12
assert abs(r["accepted_precision"] - 9/13) < 1e-12
assert abs(r["rejected_precision"] - 0.02/0.74) < 1e-12
assert abs(r["success"] - 0.632486004809728) < 1e-12

Защищённые тесты должны проверять граничные вероятности и ошибочное имя режима.

Показать ответ и решение
Python
from __future__ import annotations
import math


def binary_verifier_success(
    n: int,
    p_correct: float,
    true_positive_rate: float,
    false_positive_rate: float,
    *,
    fallback_mode: str = "rejected_pool",
) -> dict[str, float]:
    if isinstance(n, bool) or int(n) != n or int(n) <= 0:
        raise ValueError("n must be a positive integer")
    values = [float(p_correct), float(true_positive_rate), float(false_positive_rate)]
    if any((not math.isfinite(v)) or not 0.0 <= v <= 1.0 for v in values):
        raise ValueError("probabilities must lie in [0, 1]")
    if fallback_mode not in {"rejected_pool", "fresh_unverified"}:
        raise ValueError("unknown fallback mode")

    p, tpr, fpr = values
    q = p * tpr + (1.0 - p) * fpr
    accepted_precision = p if q == 0.0 else p * tpr / q
    rejected_precision = p if q == 1.0 else p * (1.0 - tpr) / (1.0 - q)
    no_accept = (1.0 - q) ** int(n)
    fallback = rejected_precision if fallback_mode == "rejected_pool" else p
    success = (1.0 - no_accept) * accepted_precision + no_accept * fallback
    return {
        "q": q,
        "accepted_precision": accepted_precision,
        "rejected_precision": rejected_precision,
        "no_accept": no_accept,
        "fallback_success": fallback,
        "success": success,
    }

Ветвь q == 1 нужна, потому что условное распределение отклонённых тогда не определено, но событие резервного выбора имеет нулевую вероятность. Возвращаемое условное значение не влияет на success.

Агрегации процессных оценок

Реализуйте min, mean, product, last и log_product. Проверьте диапазон [0,1][0,1] и пустой вход.

Python
from __future__ import annotations

from collections.abc import Sequence


def process_score(step_scores: Sequence[float], method: str) -> float:
    raise NotImplementedError

Для [0.95, 0.95, 0.30, 0.95] произведение должно быть 0.2572125.

Открытые проверки:

Python
assert abs(process_score([0.95, 0.95, 0.30, 0.95], "mean") - 0.7875) < 1e-12
assert abs(process_score([0.95, 0.95, 0.30, 0.95], "product") - 0.2572125) < 1e-12
Показать ответ и решение
Python
from __future__ import annotations

from collections.abc import Sequence
import numpy as np


def process_score(step_scores: Sequence[float], method: str) -> float:
    values = np.asarray(step_scores, dtype=np.float64)
    if values.ndim != 1 or values.size == 0:
        raise ValueError("step_scores must be non-empty")
    if not np.isfinite(values).all() or np.any((values < 0) | (values > 1)):
        raise ValueError("step scores must lie in [0, 1]")
    method = method.lower()
    if method == "min":
        return float(values.min())
    if method == "mean":
        return float(values.mean())
    if method == "product":
        return float(values.prod())
    if method == "last":
        return float(values[-1])
    if method == "log_product":
        if np.any(values == 0):
            return float("-inf")
        return float(np.log(values).sum())
    raise ValueError("unknown aggregation method")

Логарифм произведения численно устойчивее для длинных цепочек. Он не исправляет семантическую проблему: перемножать оценки можно осмысленно лишь при подходящей вероятностной интерпретации.

Переоптимизация прокси в BoN

Воспроизведите эксперимент:

proxy=g+h,\text{proxy}=g+h, goal=g−max⁡(h−1,0)2,\text{goal}=g-\max(h-1,0)^2,

где gg и hh независимы и стандартно нормальны. Для каждого бюджета выбирается кандидат с максимальным proxy.

Python
from __future__ import annotations

from collections.abc import Sequence
import numpy as np


def proxy_overoptimization_curve(
    budgets: Sequence[int],
    *,
    trials: int = 10_000,
    seed: int = 202_610,
) -> tuple[np.ndarray, np.ndarray]:
    raise NotImplementedError

Постройте обе кривые на сетке [1,2,4,8,16,32,64,128,256,512]. Сопоставьте рост выбранной прокси-оценки с поведением независимой цели: найдите наблюдаемый максимум и проверьте, сохраняется ли ухудшение при больших бюджетах для нескольких начальных состояний генератора (seed). Допустимы как общий пул кандидатов с вложенными префиксами, так и независимый пул для каждого бюджета; опишите выбранную схему. Точный бюджет максимума зависит от конечной выборки и порядка генерации случайных чисел и не является условием зачёта.

Открытые проверки:

Python
budgets = [1, 2, 4, 8, 16, 32, 64]
proxy, goal = proxy_overoptimization_curve(budgets, trials=2000, seed=1)
assert proxy.shape == goal.shape == (len(budgets),)
assert np.isfinite(proxy).all() and np.isfinite(goal).all()
again = proxy_overoptimization_curve(budgets, trials=2000, seed=1)
assert np.array_equal(proxy, again[0]) and np.array_equal(goal, again[1])
other = proxy_overoptimization_curve(budgets, trials=2000, seed=2)
assert not np.array_equal(proxy, other[0])
assert not np.array_equal(goal, other[1])
Показать ответ и решение
Python
from __future__ import annotations

from collections.abc import Sequence
import numpy as np


def proxy_overoptimization_curve(
    budgets: Sequence[int],
    *,
    trials: int = 10_000,
    seed: int = 202_610,
) -> tuple[np.ndarray, np.ndarray]:
    budgets = np.asarray(budgets, dtype=np.int64)
    if budgets.ndim != 1 or budgets.size == 0 or np.any(budgets <= 0):
        raise ValueError("budgets must be positive integers")
    if trials <= 0:
        raise ValueError("trials must be positive")
    rng = np.random.default_rng(seed)
    maximum = int(budgets.max())
    useful = rng.normal(size=(trials, maximum))
    spurious = rng.normal(size=(trials, maximum))
    proxy = useful + spurious
    goal = useful - np.maximum(spurious - 1.0, 0.0) ** 2
    rows = np.arange(trials)
    proxy_means = []
    goal_means = []
    for n in budgets:
        selected = np.argmax(proxy[:, : int(n)], axis=1)
        proxy_means.append(proxy[rows, selected].mean())
        goal_means.append(goal[rows, selected].mean())
    return np.asarray(proxy_means), np.asarray(goal_means)

Этот эталон использует общий пул кандидатов, поэтому выбранный максимум прокси не убывает при расширении префикса в каждом испытании. Для независимых пулов монотонность относится к ожиданию, а отдельные выборочные средние могут колебаться. В опубликованном протоколе этого эталона (trials=10000, seed=202610, указанная сетка) максимум независимой цели наблюдается при N=32N=32; другой допустимый порядок выборки может дать иной максимум. Открытые проверки проверяют форму, конечность, воспроизводимость и учёт параметра seed. Формулу цели и выбор кандидата дополнительно сверяйте по нескольким вручную рассчитанным пулам; сами эти проверки не доказывают правильность алгоритма.

Жадное распределение бюджета

Реализуйте распределение следующей попытки по максимальному предельному приросту

pi(1−pi)ni.p_i(1-p_i)^{n_i}.
Python
from __future__ import annotations

from collections.abc import Sequence
import numpy as np


def allocate_budget_greedy(
    ps: Sequence[float],
    total_samples: int,
    *,
    minimum_per_prompt: int = 1,
) -> np.ndarray:
    raise NotImplementedError

Контроль:

Python
allocation = allocate_budget_greedy([0.02, 0.08, 0.2, 0.5], 16)
assert np.array_equal(allocation, [1, 5, 6, 4])

Объясните, почему оптимальность этого жадного правила относится к заданным независимым кривым с невозрастающими предельными приростами, а не к произвольной реальной LLM-системе.

Показать ответ и решение
Python
from __future__ import annotations

from collections.abc import Sequence
import numpy as np


def allocate_budget_greedy(
    ps: Sequence[float],
    total_samples: int,
    *,
    minimum_per_prompt: int = 1,
) -> np.ndarray:
    p = np.asarray(ps, dtype=np.float64)
    if p.ndim != 1 or p.size == 0:
        raise ValueError("ps must be non-empty")
    if not np.isfinite(p).all() or np.any((p < 0) | (p > 1)):
        raise ValueError("probabilities must lie in [0, 1]")
    if minimum_per_prompt < 0 or int(minimum_per_prompt) != minimum_per_prompt:
        raise ValueError("minimum_per_prompt must be a non-negative integer")
    minimum = int(minimum_per_prompt)
    if total_samples < minimum * p.size:
        raise ValueError("budget is smaller than the required minimum")
    allocation = np.full(p.size, minimum, dtype=np.int64)
    for _ in range(int(total_samples) - minimum * p.size):
        gains = p * np.power(1.0 - p, allocation)
        allocation[int(np.argmax(gains))] += 1
    return allocation

Для независимых кривых 1−(1−pi)ni1-(1-p_i)^{n_i} предельные приросты невозрастают, поэтому последовательный выбор крупнейшего доступного прироста оптимален для дискретного бюджета. Реальная LLM-система может нарушать предположения: вероятность успеха неизвестна, кандидаты коррелированы, стоимость попыток неодинакова, а правило выбора несовершенно.