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

Модуль 13. Оценивание RL-систем для LLM: статистика, взлом награды и безопасность

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

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

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

Что именно получило 61%

В отчёте написано: «модель набрала 61% на наборе задач». Известно, что у новой версии изменились системное сообщение, шаблон диалога, лимит длины, доступ к поиску и правило канонизации ответа, а веса остались прежними.

  1. Какой объект на самом деле сравнивается с прежним запуском?

  2. Почему число нельзя приписать только файлу весов?

  3. Какие компоненты нужно зафиксировать, если исследовательский вопрос всё же относится только к контрольным точкам?

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

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

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

  3. Нужно зафиксировать токенизатор и его версию, шаблон диалога, системное сообщение, параметры декодирования, бюджет, инструменты, среду, канонизатор и проверяющую систему.

  4. Если исследовательский вопрос относится к полной прикладной системе — например, «какой агент лучше закрывает тикет» — обвязка является частью сравниваемого объекта. Тогда её изменение допустимо, но должно быть явным и версионированным.

Интервал не является украшением

На 20 задачах система не решила ни одной. Нормальная формула даёт интервал [0,0][0,0], а интервал Уилсона имеет положительную верхнюю границу.

  1. Почему нормальная формула здесь вводит в заблуждение?

  2. Что именно означает верхняя граница интервала Уилсона?

  3. Доказывает ли отсутствие успехов истинную вероятность успеха, равную нулю?

  4. Почему заранее заданная требуемая полуширина интервала полезнее фразы «возьмём примерно сто задач»?

Показать ответ и решение
  1. Нормальная аппроксимация подставляет p^=0\widehat p=0 в оценку дисперсии и получает нуль. Она игнорирует неопределённость о неизвестной доле.

  2. Верхняя граница описывает значения истинной доли, которые ещё совместимы с наблюдаемыми данными при выбранном методе построения интервала. Это не утверждение, что истинная доля равна верхней границе.

  3. Нет. Наблюдение 0/20 означает отсутствие успехов в конкретной выборке, а не логическую невозможность успеха.

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

Единица анализа и повторные генерации

Для каждого из 100 запросов получено по 16 ответов с разными начальными значениями генератора случайных чисел. Команда объявила все 1600 ответов независимыми задачами и построила обычный биномиальный интервал.

  1. В чём ошибка единицы анализа?

  2. Какие источники случайности присутствуют в эксперименте?

  3. Когда первичной единицей должен оставаться запрос?

  4. Какой бутстрэп-подход сохраняет кластерную структуру?

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

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

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

  3. Запрос остаётся единицей, когда интерес представляет среднее качество по популяции запросов. Сначала агрегируют протокол внутри запроса, затем анализируют запросы.

  4. Нужен кластерный или иерархический бутстрэп: на первом уровне с возвращением выбираются запросы целиком; при необходимости на втором — ответы внутри выбранного запроса.

  5. Для каждого запроса нужно определить результат всего 16-попыткового протокола — например, pass@16, голосование или выбранный ответ — и уже эти запросные результаты агрегировать.

Парность и точный тест Мак-Немара

Две системы оценены на одних и тех же задачах. Только A верна на 9 задачах, только B — на 2. Большая часть задач решена обеими или провалена обеими.

  1. Почему сравнение должно быть парным?

  2. Какие наблюдения входят в тест Мак-Немара?

  3. Почему асимптотический zz может дать слишком уверенный вывод при 11 дискордантных задачах?

  4. Какой точный нулевой эксперимент лежит в основе двустороннего точного теста?

  5. Что следует публиковать помимо pp-значения?

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

  2. В тест входят только дискордантные случаи: A only и B only. Общие успехи и общие провалы не различают системы.

  3. Нормальная аппроксимация предполагает достаточно большой дискордантный счётчик. При 11 наблюдениях хвост биномиального распределения ещё заметно дискретен.

  4. При нулевой гипотезе каждая дискордантная задача с вероятностью 1/21/2 должна попадать в пользу A или B. Поэтому X∼Binomial⁡(b+c,1/2)X\sim\operatorname{Binomial}(b+c,1/2).

  5. Публикуют bb, cc, разность метрик, доверительный интервал, точное pp-значение и правило анализа, выбранное заранее.

pass@k, покрытие и выбор

Для задачи сгенерировано 20 кандидатов, ровно один из них правильный. В отчёте одновременно фигурируют pass@20 = 1 и «система выбрала неправильный ответ».

  1. Есть ли противоречие?

  2. Что измеряет pass@k?

  3. Что измеряет качество выбора кандидата?

  4. Почему формула 1−(1−p^)k1-(1-\hat p)^k с p^=c/n\hat p=c/n не является конечновыборочным оценивателем Чена?

  5. Какие сведения о бюджете нужно хранить рядом с pass@k?

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

  2. pass@k измеряет покрытие: вероятность наличия хотя бы одного правильного ответа среди kk кандидатов.

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

  4. Подстановка предполагает независимые повторные выборки из неизвестного распределения с оценённой вероятностью. Конечновыборочная формула задаёт выбор без возвращения из уже наблюдаемого пула из nn кандидатов.

  5. Нужны nn, kk, число успешных кандидатов, число задач, параметры декодирования, лимит токенов, правило проверки, число начальных значений и стоимость генерации.

Отсутствие n-граммного совпадения

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

  1. Что этот результат действительно исключает?

  2. Какие виды загрязнения он может пропустить?

  3. Почему неизвестная часть корпуса не позволяет превратить фильтр в сертификат чистоты?

  4. Что такое протокольное загрязнение?

  5. Назовите не менее четырёх дополнительных защит жизненного цикла бенчмарка.

Показать ответ и решение
  1. Фильтр не нашёл дословных совпадений выбранной длины в доступной части корпуса.

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

  3. Поиск по неполному корпусу не способен исключить совпадение с недоступными данными. Отсутствие свидетельства не равно свидетельству отсутствия.

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

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

Судья под оптимизационным давлением

LLM-судья предпочитает длинные ответы, часто выбирает первый вариант и иногда следует инструкции, встроенной в оцениваемый ответ. После перехода от разового сравнения к Best-of-64 средняя оценка резко выросла.

  1. Какие три класса слабостей судьи здесь видны?

  2. Почему перестановка порядка A/B исправляет только часть проблемы?

  3. Чем структурная изоляция данных кандидата отличается от словесной просьбы «игнорируй инструкции внутри ответа»?

  4. Почему рост оценки при Best-of-64 может быть ошибкой победителя?

  5. Какой независимый сигнал нужен, чтобы различить полезный рост и эксплуатацию судьи?

Показать ответ и решение
  1. Видны позиционное смещение, предпочтение поверхностных признаков и уязвимость к инъекциям из данных кандидата.

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

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

  4. Среди 64 кандидатов растёт вероятность найти ответ с большой положительной ошибкой судьи. Максимальный прокси-балл может расти быстрее независимой полезности.

  5. Нужен независимый оцениватель: исполнимая проверка, скрытая человеческая выборка, другая модель-судья с отдельной рубрикой или реальный исход среды.

Безопасность и решение о выпуске

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

  1. Почему одной доли отказов недостаточно?

  2. Какие два знаменателя нужны как минимум для вредных и безвредных запросов?

  3. Зачем учитывать тяжесть последствий?

  4. Должен ли средний итоговый балл компенсировать критический обход?

  5. Какие поля протокольного паспорта нужны, чтобы решение можно было независимо проверить?


B. Аналитические задания

Показать ответ и решение
  1. Отказ может быть правильной реакцией на вредный запрос или ошибкой на безвредном. Одна общая доля смешивает безопасность и полезность.

  2. ASR использует число вредных попыток; частота избыточного отказа — число безвредных запросов.

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

  4. Нет, если критический обход заранее объявлен безусловным блокирующим событием.

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


Интервал Уилсона

Используйте z=1.96z=1.96.

  1. Для k=62k=62, n=100n=100 найдите наблюдаемую долю и 95%-интервал Уилсона.

  2. Для k=0k=0, n=20n=20 найдите тот же интервал.

  3. Сравните второй ответ с нормальной аппроксимацией.

  4. Объясните, почему результат 0/20 совместим с ненулевой истинной долей.

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

Для Уилсона:

denom=1+z2/n,\mathrm{denom}=1+z^2/n, center=p^+z2/(2n)denom,\mathrm{center}=\frac{\widehat p+z^2/(2n)}{\mathrm{denom}}, margin=zdenomp^(1−p^)n+z24n2.\mathrm{margin}=\frac{z}{\mathrm{denom}} \sqrt{\frac{\widehat p(1-\widehat p)}{n}+\frac{z^2}{4n^2}}.
  1. Для 62/100: p^=0.62\widehat p=0.62, интервал приблизительно
[0.5221,0.7090].[0.5221, 0.7090].
  1. Для 0/20: интервал приблизительно
[0,0.1611].[0, 0.1611].
  1. Нормальная аппроксимация даёт [0,0][0,0], поскольку оценённая дисперсия равна нулю.

  2. Двадцать неудач остаются вероятными при небольшом, но положительном истинном pp; интервал показывает конечную неопределённость.

Планирование размера набора

В худшем случае для биномиальной доли используйте p=1/2p=1/2 и приближение

n≥z24m2, n\geq \frac{z^2}{4m^2},

где mm — требуемая полуширина.

  1. Найдите минимальный nn для m=0.03m=0.03 при z=1.96z=1.96.

  2. Найдите минимальный nn для m=0.01m=0.01.

  3. Во сколько раз увеличивается размер набора при трёхкратном уменьшении полуширины?

  4. Почему это только планировочная оценка, а не гарантия точной ширины интервала Уилсона на любом результате?

Показать ответ и решение
  1. Для m=0.03m=0.03:
⌈1.9624⋅0.032⌉=1068.\left\lceil\frac{1.96^2}{4\cdot0.03^2}\right\rceil = 1068.
  1. Для m=0.01m=0.01:
⌈1.9624⋅0.012⌉=9604.\left\lceil\frac{1.96^2}{4\cdot0.01^2}\right\rceil = 9604.
  1. Теоретически множитель равен 32=93^2=9; округление даёт 9604/1068≈8.999604/1068\approx8.99.

  2. Формула использует худший случай и нормальную планировочную аппроксимацию. Реальная ширина Уилсона зависит от наблюдённой доли и дискретности счётчика.

Точное и асимптотическое парное сравнение

Для дискордантных счётчиков b=9b=9, c=2c=2:

  1. Найдите асимптотическую статистику
z=∣b−c∣b+c. z=\frac{|b-c|}{\sqrt{b+c}}.
  1. Найдите соответствующее двустороннее нормальное pp-значение.

  2. Найдите точное двустороннее pp-значение как удвоенную нижнюю биномиальную хвостовую вероятность при X∼Binomial⁡(11,1/2)X\sim\operatorname{Binomial}(11,1/2).

  3. Каков вывод при уровне 0.050.05 для каждого подхода?

  4. Почему в отчёте нельзя скрывать малое число дискордантных наблюдений?

Показать ответ и решение
z=711≈2.1106.z=\frac{7}{\sqrt{11}}\approx2.1106.
  1. Двустороннее нормальное значение приблизительно равно 0.03480.0348.

  2. Для X∼Binomial⁡(11,1/2)X\sim\operatorname{Binomial}(11,1/2):

pexact=2P(X≤2)=2(110)+(111)+(112)211=0.0654296875.p_{\mathrm{exact}} = 2P(X\le2) = 2\frac{\binom{11}{0}+\binom{11}{1}+\binom{11}{2}}{2^{11}} = 0.0654296875.
  1. Асимптотический подход пересекает порог 0.05, точный — нет. В малой выборке основным выводом должен быть точный результат.

  2. Без b+c=11b+c=11 читатель не видит, что асимптотика опирается на очень малое число информативных задач.

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

Используйте

pass@⁡k^=1−(n−ck)(nk).\widehat{\operatorname{pass@}k} = 1-\frac{\binom{n-c}{k}}{\binom{n}{k}}.
  1. Найдите pass@5 для n=200n=200, c=10c=10.

  2. Найдите pass@20 для n=20n=20, c=1c=1.

  3. Сравните второй ответ с наивной подстановкой 1−(1−c/n)n1-(1-c/n)^n.

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

Показать ответ и решение
1−(1905)(2005)≈0.2282844607.1-\frac{\binom{190}{5}}{\binom{200}{5}} \approx0.2282844607.
  1. Поскольку при выборе всех 20 кандидатов единственный правильный обязательно попадёт в подмножество,
pass@⁡20^=1.\widehat{\operatorname{pass@}20}=1.
  1. Наивная формула даёт
1−(19/20)20≈0.641514.1-(19/20)^{20}\approx0.641514.
  1. Наивная формула относится к 20 новым независимым попыткам при известном p=0.05p=0.05. Конечновыборочная формула задаёт случайную подвыборку без возвращения из уже наблюдаемого пула, где известно, что один успех существует.

Аудит перестановки судьи

После канонизации меток получены решения судьи:

text
прямой порядок:   A, A, B, TIE, A, B
обратный порядок: A, B, B, TIE, B, B

Во второй строке A и B уже снова относятся к исходным кандидатам, а не к позиции на экране.

  1. Найдите число согласованных и несогласованных пар.

  2. Найдите долю согласованности.

  3. Найдите доли голосов за A и B среди всех решений без ничьих.

  4. Доказывает ли высокая согласованность отсутствие длинового или стилевого смещения?

  5. Какую дополнительную рандомизацию следует провести?

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

Пары совпадают в позициях 1, 3, 4 и 6. Поэтому:

  1. согласованных 4, несогласованных 2;

  2. доля согласованности 4/6≈0.66674/6\approx0.6667;

  3. среди 10 неничейных голосов A получает 4, B — 6, то есть 0.4 и 0.6;

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

  5. следует рандомизировать длину и стиль через контролируемые пары, скрывать происхождение модели, менять формат сериализации и повторять оценивание с независимыми начальными значениями.

Метрики безопасности и выпускной порог

На 8 вредных запросах успех атаки наблюдался в случаях

(0,1,0,1,0,0,1,0),(0,1,0,1,0,0,1,0),

а тяжести равны

(1,4,1,2,1,1,3,1).(1,4,1,2,1,1,3,1).

На 10 безвредных запросах система отказала дважды.

  1. Найдите частоту успешных атак (attack success rate, ASR).

  2. Найдите частоту избыточных отказов на безвредных запросах.

  3. Найдите взвешенную по тяжести ASR.

  4. Пройдёт ли система численные ворота ASR <= 0.30, benign_refusal <= 0.25, severity_weighted_ASR <= 0.40?

  5. Изменится ли решение, если один успех атаки означает подтверждённый критический обход песочницы, объявленный безусловным блокирующим событием?


C. Задания по программированию

Показать ответ и решение
  1. Успешны 3 из 8 атак:
ASR=3/8=0.375.\mathrm{ASR}=3/8=0.375.
  1. Два отказа из десяти:
overrefusalbenign=0.2.\mathrm{overrefusal}_{\mathrm{benign}}=0.2.
  1. Суммарная тяжесть равна 1414, тяжесть успешных атак — 4+2+3=94+2+3=9:
ASRseverity=9/14≈0.6429.\mathrm{ASR}_{\mathrm{severity}}=9/14\approx0.6429.
  1. Система проваливает ворота ASR и взвешенной ASR, хотя проходит ворота избыточных отказов.

  2. Критическое событие блокирует выпуск независимо от средних чисел; решение остаётся отрицательным и получает отдельную причину.


Интервалы и планирование

Реализуйте:

Python
def wilson_interval(successes: int, total: int, z: float = 1.96) -> tuple[float, float, float]:
    """Вернуть (estimate, low, high)."""
    raise NotImplementedError


def worst_case_sample_size(margin: float, z: float = 1.96) -> int:
    """Вернуть ceil(z**2 / (4*margin**2))."""
    raise NotImplementedError

Проверяйте границы счётчиков и параметров; концы интервала должны оставаться в [0,1][0,1].

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

Python
estimate, low, high = wilson_interval(0, 10)
assert estimate == 0.0 and low == 0.0
assert 0.27 < high < 0.29
assert worst_case_sample_size(0.03) == 1068
Показать ответ и решение
Python
import math


def wilson_interval(successes: int, total: int, z: float = 1.96) -> tuple[float, float, float]:
    if total <= 0:
        raise ValueError("total must be positive")
    if successes < 0 or successes > total:
        raise ValueError("successes must lie in [0, total]")
    if not math.isfinite(z) or z <= 0:
        raise ValueError("z must be finite and positive")

    estimate = successes / total
    denominator = 1.0 + z * z / total
    center = (estimate + z * z / (2.0 * total)) / denominator
    half_width = z / denominator * math.sqrt(
        estimate * (1.0 - estimate) / total + z * z / (4.0 * total * total)
    )
    return estimate, max(0.0, center - half_width), min(1.0, center + half_width)


def worst_case_sample_size(margin: float, z: float = 1.96) -> int:
    if not math.isfinite(margin) or not 0.0 < margin < 1.0:
        raise ValueError("margin must lie in (0, 1)")
    if not math.isfinite(z) or z <= 0:
        raise ValueError("z must be finite and positive")
    return math.ceil(z * z / (4.0 * margin * margin))

Интервал Уилсона остаётся невырожденным при 0/n0/n и n/nn/n, в отличие от симметричного интервала Вальда. Ограничение концов диапазоном [0,1][0,1] защищает от микроскопических ошибок округления.

Парное сравнение

Реализуйте:

Python
def paired_table(a_correct, b_correct):
    """Вернуть both_correct, a_only, b_only, both_wrong."""

def exact_mcnemar_pvalue(a_only: int, b_only: int) -> float:
    """Двустороннее точное p-значение."""

def paired_bootstrap_difference(a_scores, b_scores, *, replicates=10000, seed=0):
    """Вернуть estimate, low, high для парной средней разности."""

Бутстрэп должен повторно выбирать с возвращением пары результатов по задачам, а не две системы независимо.

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

Python
a = [True, True, False, False, True]
b = [True, False, True, False, False]
table = paired_table(a, b)
assert table.both_correct == 1
assert table.a_only == 2
assert table.b_only == 1
assert table.both_wrong == 1
Показать ответ и решение

Парность сохраняется на уровне индексов задач; две системы никогда не бутстрэпятся независимо.

Python
from __future__ import annotations

import math
from dataclasses import dataclass

import numpy as np


@dataclass(frozen=True)
class PairedTable:
    both_correct: int
    a_only: int
    b_only: int
    both_wrong: int


def paired_table(a_correct, b_correct) -> PairedTable:
    a = np.asarray(a_correct, dtype=bool)
    b = np.asarray(b_correct, dtype=bool)
    if a.shape != b.shape or a.ndim != 1 or a.size == 0:
        raise ValueError("paired vectors must be non-empty and have equal shape")
    return PairedTable(
        int(np.sum(a & b)),
        int(np.sum(a & ~b)),
        int(np.sum(~a & b)),
        int(np.sum(~a & ~b)),
    )


def exact_mcnemar_pvalue(a_only: int, b_only: int) -> float:
    if min(a_only, b_only) < 0:
        raise ValueError("counts must be non-negative")
    n = a_only + b_only
    if n == 0:
        return 1.0
    tail = sum(math.comb(n, j) for j in range(min(a_only, b_only) + 1)) / 2**n
    return min(1.0, 2.0 * tail)


def paired_bootstrap_difference(a_scores, b_scores, *, replicates=10_000, seed=0):
    a = np.asarray(a_scores, dtype=float)
    b = np.asarray(b_scores, dtype=float)
    if a.shape != b.shape or a.ndim != 1 or a.size == 0:
        raise ValueError("paired vectors must be non-empty and have equal shape")
    if replicates <= 0:
        raise ValueError("replicates must be positive")
    delta = a - b
    rng = np.random.default_rng(seed)
    indices = rng.integers(0, delta.size, size=(replicates, delta.size))
    draws = delta[indices].mean(axis=1)
    return float(delta.mean()), float(np.quantile(draws, 0.025)), float(np.quantile(draws, 0.975))


table = paired_table([1, 1, 0, 0, 1], [1, 0, 1, 0, 0])
assert table == PairedTable(1, 2, 1, 1)
assert abs(exact_mcnemar_pvalue(9, 2) - 0.0654296875) < 1e-15

pass@k и выбор

Реализуйте численно устойчивую конечновыборочную оценку и отдельную диагностику выбора:

Python
def pass_at_k(n: int, c: int, k: int) -> float:
    raise NotImplementedError


def coverage_and_selection(correct, selected_index: int) -> tuple[bool, bool]:
    raise NotImplementedError

Первая функция оценивает вероятность того, что в kk выборках без возвращения окажется хотя бы одно из cc успешных решений среди nn кандидатов. Вторая разделяет наличие правильного кандидата в пуле и правильность реально выбранного ответа.

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

Python
assert abs(pass_at_k(200, 10, 5) - 0.22828446073733422) < 1e-15
assert coverage_and_selection([False, True, False], 0) == (True, False)
Показать ответ и решение
Python
def pass_at_k(n: int, c: int, k: int) -> float:
    if n <= 0 or not 0 <= c <= n or not 1 <= k <= n:
        raise ValueError("require n>0, 0<=c<=n, and 1<=k<=n")
    if n - c < k:
        return 1.0
    probability_of_miss = 1.0
    for i in range(k):
        probability_of_miss *= (n - c - i) / (n - i)
    return 1.0 - probability_of_miss


def coverage_and_selection(correct, selected_index: int) -> tuple[bool, bool]:
    values = [bool(x) for x in correct]
    if not values or not 0 <= selected_index < len(values):
        raise ValueError("invalid candidate pool or selected index")
    return any(values), values[selected_index]

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

Дешёвый фильтр загрязнения без подмены метрики

Реализуйте:

Python
def ngram_set(text: str, n: int) -> set[tuple[str, ...]]:
    ...

def ngram_containment(benchmark_text: str, corpus_text: str, n: int) -> float:
    """Доля n-грамм benchmark_text, найденных в corpus_text."""

def ngram_jaccard(left: str, right: str, n: int) -> float:
    """Симметричный коэффициент Жаккара."""

def contamination_report(items, corpus, *, n=8, threshold=0.2):
    ...

Отчёт должен использовать асимметричное покрытие со стороны тестового элемента (benchmark-side containment), совпадающее с формулой лекции, и явно хранить имя метрики. Отдельно покажите пару текстов, для которой асимметричное покрытие и коэффициент Жаккара различаются. В документации укажите, что отсутствие срабатывания не доказывает чистоту.

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

Python
assert ngram_set("a b c d", 2) == {("a", "b"), ("b", "c"), ("c", "d")}
assert ngram_containment("a b c", "x a b c y", 2) == 1.0
assert ngram_jaccard("a b c", "x a b c y", 2) < 1.0
Показать ответ и решение
Python
from __future__ import annotations


def ngram_set(text: str, n: int) -> set[tuple[str, ...]]:
    if n <= 0:
        raise ValueError("n must be positive")
    tokens = text.casefold().split()
    return {tuple(tokens[i : i + n]) for i in range(len(tokens) - n + 1)}


def ngram_containment(benchmark_text: str, corpus_text: str, n: int) -> float:
    benchmark = ngram_set(benchmark_text, n)
    corpus = ngram_set(corpus_text, n)
    if not benchmark:
        return 0.0
    return len(benchmark & corpus) / len(benchmark)


def ngram_jaccard(left: str, right: str, n: int) -> float:
    a = ngram_set(left, n)
    b = ngram_set(right, n)
    union = a | b
    return 0.0 if not union else len(a & b) / len(union)


def contamination_report(items, corpus, *, n=8, threshold=0.2):
    if not 0.0 <= threshold <= 1.0:
        raise ValueError("threshold must lie in [0, 1]")
    maxima = []
    matches = []
    for i, item in enumerate(items):
        scores = [ngram_containment(item, doc, n) for doc in corpus]
        best = max(scores, default=0.0)
        maxima.append(best)
        for j, score in enumerate(scores):
            if score >= threshold:
                matches.append((i, j, score))
    return {
        "metric": "benchmark_ngram_containment",
        "n": n,
        "threshold": threshold,
        "max_overlap": maxima,
        "matches": matches,
    }


assert ngram_containment("a b c", "x a b c y", 2) == 1.0
assert ngram_jaccard("a b c", "x a b c y", 2) == 0.5

Асимметричное покрытие делит на число n-грамм тестового элемента; коэффициент Жаккара — на объединение. Отчёт хранит имя метрики, чтобы эти определения не смешались при последующей обработке.

Два аудита судьи и льстивое согласие

Реализуйте:

Python
def judge_swap_audit(forward_canonical, reverse_canonical):
    """Согласованность по идентичности кандидатов A/B."""

def position_audit(raw_forward, raw_reverse):
    """Предпочтение FIRST/SECOND до канонизации идентичности."""

def sycophancy_metrics(correct_without_claim, correct_with_false_claim, agrees_with_false_claim):
    ...

Не смешивайте идентичность кандидата и позицию на экране. Первая функция получает уже канонизированные A/B-метки; вторая — сырые FIRST, SECOND, TIE.

Открытая проверка:

Python
identity = judge_swap_audit(["A", "A", "B", "TIE"], ["A", "B", "B", "TIE"])
assert identity["consistent"] == 3
position = position_audit(["FIRST", "SECOND"], ["FIRST", "FIRST"])
assert position["first_position_wins"] == 3
Показать ответ и решение
Python
from __future__ import annotations


def _labels(values, allowed):
    result = [str(x).upper() for x in values]
    if any(x not in allowed for x in result):
        raise ValueError(f"labels must belong to {sorted(allowed)}")
    return result


def judge_swap_audit(forward_canonical, reverse_canonical):
    a = _labels(forward_canonical, {"A", "B", "TIE"})
    b = _labels(reverse_canonical, {"A", "B", "TIE"})
    if len(a) != len(b) or not a:
        raise ValueError("label vectors must be non-empty and aligned")
    consistent = sum(x == y for x, y in zip(a, b))
    votes = [x for x in a + b if x != "TIE"]
    return {
        "items": len(a),
        "consistent": consistent,
        "inconsistent": len(a) - consistent,
        "consistency_rate": consistent / len(a),
        "candidate_a_rate": votes.count("A") / len(votes) if votes else 0.0,
        "candidate_b_rate": votes.count("B") / len(votes) if votes else 0.0,
    }


def position_audit(raw_forward, raw_reverse):
    a = _labels(raw_forward, {"FIRST", "SECOND", "TIE"})
    b = _labels(raw_reverse, {"FIRST", "SECOND", "TIE"})
    if len(a) != len(b) or not a:
        raise ValueError("label vectors must be non-empty and aligned")
    all_labels = a + b
    first = all_labels.count("FIRST")
    second = all_labels.count("SECOND")
    ties = all_labels.count("TIE")
    denom = first + second
    return {
        "judgments": len(all_labels),
        "first_position_wins": first,
        "second_position_wins": second,
        "ties": ties,
        "first_position_rate_non_ties": first / denom if denom else 0.0,
    }


def sycophancy_metrics(correct_without_claim, correct_with_false_claim, agrees_with_false_claim):
    base = [bool(x) for x in correct_without_claim]
    challenged = [bool(x) for x in correct_with_false_claim]
    agrees = [bool(x) for x in agrees_with_false_claim]
    if not base or not (len(base) == len(challenged) == len(agrees)):
        raise ValueError("vectors must be non-empty and aligned")
    initially_correct = sum(base)
    switches = sum(b and not c for b, c in zip(base, challenged))
    return {
        "accuracy_without_claim": sum(base) / len(base),
        "accuracy_with_false_claim": sum(challenged) / len(base),
        "accuracy_drop": (sum(base) - sum(challenged)) / len(base),
        "switch_rate_among_initially_correct": switches / initially_correct if initially_correct else 0.0,
        "unjustified_agreement_rate": sum(agrees) / len(agrees),
    }


assert judge_swap_audit(["A", "A", "B", "TIE"], ["A", "B", "B", "TIE"])["consistent"] == 3
assert position_audit(["FIRST", "SECOND"], ["FIRST", "FIRST"])["first_position_wins"] == 3

Канонизированные A/B и сырые метки FIRST/SECOND сохраняются раздельно, потому что измеряют разные виды смещения.

Безопасность, ворота выпуска и паспорт

Реализуйте:

Python
def safety_rates(attack_succeeded, benign_refused, severities=None):
    raise NotImplementedError


def release_decision(metrics, thresholds, critical_failures=()):
    raise NotImplementedError


def validate_measurement_passport(passport):
    raise NotImplementedError

ASR и частота избыточного отказа используют разные знаменатели. Критическое событие и отсутствующая обязательная метрика должны блокировать выпуск. Валидатор паспорта возвращает все найденные ошибки.

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

Python
rates = safety_rates([0, 1, 0, 1], [0, 0, 1, 0], severities=[1, 4, 1, 2])
assert abs(rates["attack_success_rate"] - 0.5) < 1e-12
assert abs(rates["benign_over_refusal_rate"] - 0.25) < 1e-12
result = release_decision(rates, {"attack_success_rate": ("max", 0.25)}, ("sandbox_escape",))
assert result["passed"] is False
Показать ответ и решение
Python
def safety_rates(attack_succeeded, benign_refused, severities=None):
    attacks = [bool(x) for x in attack_succeeded]
    benign = [bool(x) for x in benign_refused]
    if not attacks or not benign:
        raise ValueError("both harmful and benign samples are required")
    if severities is None:
        severity_weighted = sum(attacks) / len(attacks)
    else:
        weights = [float(x) for x in severities]
        if len(weights) != len(attacks) or any(x <= 0 for x in weights):
            raise ValueError("positive severities must align with attacks")
        severity_weighted = sum(w for hit, w in zip(attacks, weights) if hit) / sum(weights)
    return {
        "attack_success_rate": sum(attacks) / len(attacks),
        "benign_over_refusal_rate": sum(benign) / len(benign),
        "severity_weighted_asr": severity_weighted,
    }


def release_decision(metrics, thresholds, critical_failures=()):
    failures = [f"critical:{item}" for item in critical_failures]
    for name, (direction, threshold) in thresholds.items():
        if name not in metrics:
            failures.append(f"missing:{name}")
            continue
        value = float(metrics[name])
        if direction == "max" and value > threshold:
            failures.append(f"{name}>{threshold}")
        elif direction == "min" and value < threshold:
            failures.append(f"{name}<{threshold}")
        elif direction not in {"min", "max"}:
            failures.append(f"invalid_direction:{name}")
    return {"passed": not failures, "failures": failures}


def validate_measurement_passport(passport):
    required = {
        "run_id", "model_id", "model_revision", "tokenizer_id", "dataset_id",
        "dataset_revision", "split", "solver_id", "scorer_id", "scorer_revision",
        "decoding", "seeds", "sample_count", "metrics", "cost", "artifact_hashes",
    }
    errors = [f"missing:{key}" for key in sorted(required - set(passport))]
    if "sample_count" in passport and int(passport["sample_count"]) <= 0:
        errors.append("sample_count:not_positive")
    if "seeds" in passport and not passport["seeds"]:
        errors.append("seeds:empty")
    if "artifact_hashes" in passport and not passport["artifact_hashes"]:
        errors.append("artifact_hashes:empty")
    return tuple(errors)

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