Статистика экспериментов: шум, доверительные интервалы, бутстрэп
Модель A набрала 71%, модель B — 73% на 500 вопросах: есть ли разница? Выборка и стандартная ошибка, ЦПТ на симуляции, доверительный интервал (откуда 1.96), бутстрэп, парное сравнение моделей, p-value без ритуала, множественные сравнения и размер eval-набора.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Вы сравниваете две модели на тестовом наборе из 500 вопросов (бенчмаркебенчмаркEN benchmark, урок 1.3.1). Модель A ответила верно на 71%, модель B — на 73%. Берём B? Через месяц вы поменяли промпт, и точность выросла с 60% до 64%. Это улучшение или случайность? Такие решения инженер по LLM принимает каждый день: выбирает модель, промпт, гиперпараметры, решает, работает ли новая идея.
Беда в том, что любой результат на тесте содержит шум. Возьмите другие 500 вопросов того же типа — и обе цифры сдвинутся на пару пунктов. Этот урок даёт инструменты, чтобы отличить реальную разницу от шума: стандартную ошибку, доверительный интервал, бутстрэп и парное сравнение. Сквозной пример построен по мотивам исследования Anthropic «Adding Error Bars to Evals» (2024): его главный тезис — у каждого числа в отчёте должен быть интервал. Всё проверяем симуляциями на NumPy, формулы — после интуиции. Эти инструменты понадобятся в модуле 7.3 (оценка моделей), при сравнении промптов на ступени 8 и в итоговом проекте.
Генеральная совокупность и выборка
Заголовок раздела «Генеральная совокупность и выборка»Что вообще значит «точность модели 72%»? Здесь и дальше точность — это accuracyдоля правильных ответов (accuracy)EN accuracyПо-русски accuracy иногда тоже называют «точностью» — из-за этого её путают с precision. Безопаснее писать «accuracy»., доля верных ответов. (В уроке 2.2.3 появится ещё precision, которую тоже иногда переводят как «точность», — не путайте.) Представим все вопросы такого типа, какие только можно задать, — бесконечный запас. Это генеральная совокупностьгенеральная совокупностьEN populationВсе объекты, о которых хотим сделать вывод, — например, все возможные вопросы данного типа. Истинная точность модели — доля верных ответов на всей совокупности; её оценивают по выборке.. Доля вопросов из неё, на которые модель отвечает верно, — истинная точность . Её мы никогда не узнаем: все вопросы не задашь.
Бенчмарк из вопросов — выборкавыборкаEN sampleЧасть генеральной совокупности, которую мы реально наблюдаем: бенчмарк из n вопросов. Метрика на выборке — оценка истинной величины с шумом. из этой совокупности. Каждый ответ — испытание Бернулли из урока 1.3.1: 1 (верно) с вероятностью или 0. Измеренная точность («пэ с крышкой», урок 1.0.3) — среднее этих нулей и единиц, оценка истинной .
Почему ? Потому что выборка случайна: в наш набор могли попасть чуть более лёгкие или чуть более трудные вопросы. Есть и второй источник шума: если модель генерирует с температурой (урок 4.1.3), на тот же вопрос она отвечает то верно, то нет. Оба шума одинаково сдвигают .
Выборочное среднее — тоже случайная величина
Заголовок раздела «Выборочное среднее — тоже случайная величина»Самый наглядный способ понять шум — притвориться, что мы знаем истину, и много раз повторить эксперимент. Пусть истинная точность . Сгенерируем 10 000 «бенчмарков» по 500 вопросов и посмотрим, какие точности выйдут:
import numpy as np
rng = np.random.default_rng(0)p_true, n = 0.72, 500 # «истинная» точность и размер бенчмарка# binomial(n, p) — число верных ответов из n (биномиальное распределение, урок 1.3.1)accs = rng.binomial(n, p_true, size=10_000) / n # 10 000 бенчмарков: доля верных в каждомprint(accs[:6])print(accs.mean().round(4), accs.std().round(4)) # центр и разбросprint(accs.min(), accs.max())[0.746 0.718 0.72 0.704 0.694 0.724]0.72 0.020.638 0.804Одна и та же модель получила от 63.8% до 80.4%. Типичное отклонение от истины — 0.020, то есть два процентных пункта (п.п.: разница между 71% и 73% — это 2 п.п.). Распределение по повторным экспериментам — это колокол:
Теперь пример из начала урока выглядит иначе. Разница между 71% и 73% — как раз одно типичное отклонение. Такие расхождения шум даёт постоянно, даже если модели одинаково сильны.
Стандартная ошибка
Заголовок раздела «Стандартная ошибка»Разброс оценки называют стандартной ошибкойстандартная ошибкаEN standard errorСтандартное отклонение оценки метрики из-за конечного числа вопросов. Для доли — √(p(1−p)/n). (SE, standard error). Это стандартное отклонение по воображаемым повторениям эксперимента. Формулу вы уже видели в уроках 0.3.3 и 1.3.1: среднее независимых величин со стандартным отклонением имеет разброс . У ответа 0/1 , поэтому
Читаем: «стандартная ошибка доли — корень из на , делённого на число вопросов». Проверка: — ровно то, что дала симуляция. В жизни неизвестна, и в формулу подставляют : на больших это почти не меняет ответ.
correct = np.array([1] * 355 + [0] * 145) # ответы модели A: 355 верных из 500p_hat = correct.mean()se = np.sqrt(p_hat * (1 - p_hat) / len(correct))print(p_hat, round(se, 4)) # 0.71 0.0203# общая формула s / √n для любой метрики — например, оценки судьи от 1 до 10print(round(correct.std(ddof=1) / np.sqrt(len(correct)), 4)) # 0.0203Вторая строка — формула для любой метрики, которая усредняется по примерам: loss, длина ответа, баллы, которые
ставит ответам модель-«судья» (урок 7.3.2).
Берём стандартное отклонение по примерам и делим на . Параметр ddof=1 делит сумму квадратов на
вместо : так оценка разброса по выборке получается чуть честнее (на сотнях примеров разницы не видно).
Главный практический вывод — закон из урока 0.3.3: чтобы уменьшить шум вдвое, нужно вчетверо больше вопросов.
Центральная предельная теорема
Заголовок раздела «Центральная предельная теорема»Почему гистограмма выше похожа на колокол нормального распределениянормальное (гауссовское) распределениеEN normal (Gaussian) distribution«Колокол» N(μ, σ²). Правило 68–95–99.7 — доля значений в пределах 1, 2 и 3 σ от среднего.? Это не совпадение, а центральная предельная теоремацентральная предельная теорема (ЦПТ)EN central limit theorem (CLT)Среднее многих независимых одинаково распределённых величин распределено почти нормально со стандартным отклонением σ/√n — каким бы ни было исходное распределение. Отсюда интервал ± 1.96·SE. (ЦПТ): среднее многих независимых величин распределено почти нормально, каким бы ни было распределение каждой из них. Условия: величины независимы, распределены одинаково и не имеют «бесконечного» разброса; приближение тем точнее, чем больше величин усредняем.
Проверим на величине, совсем не похожей на колокол, — длине ответов модели. Обычно ответы короткие, но иногда модель
«задумывается» на тысячи токенов. Смоделируем это экспоненциальным распределением: маленькие значения у него
самые частые, большие — редкие, но встречаются (rng.exponential(300) выдаёт такие числа со средним 300). Сравним
отдельные ответы со средним по 50 ответам. У нормального распределения в пределах лежит 68.3% значений,
а выше — 2.3% (правило 68–95–99.7 из урока 1.3.1):
rng = np.random.default_rng(1)lengths = rng.exponential(300, size=(100_000, 50)) # 100 000 экспериментов по 50 ответовone = lengths[:, 0] # отдельные ответыmeans = lengths.mean(axis=1) # средняя длина в каждом экспериментеfor name, x in [("один ответ", one), ("среднее по 50", means)]: m, s = x.mean(), x.std() inside = np.mean(np.abs(x - m) <= s) # доля в пределах ±σ tail = np.mean(x > m + 2 * s) # доля в правом хвосте print(f"{name:14} среднее {m:6.1f} σ {s:6.1f} в пределах ±σ: {inside:.3f} выше m + 2σ: {tail:.3f}")один ответ среднее 299.5 σ 300.0 в пределах ±σ: 0.865 выше m + 2σ: 0.049среднее по 50 среднее 300.1 σ 42.5 в пределах ±σ: 0.685 выше m + 2σ: 0.030Отдельные длины совсем не нормальны: 86.5% вместо 68.3%, длинный правый хвост. А средние по 50 уже почти колокол (68.5%), и разброс сжался ровно в раза: . Чем больше , тем ближе к колоколу.
Отличное наглядное объяснение ЦПТ — видео 3Blue1Brown «But what is the Central Limit Theorem?» (ссылка в источниках, с русскими автосубтитрами): первые 15 минут — откуда берётся колокол и корень из , с 24:50 — диапазон 95% и условия теоремы.
Доверительный интервал
Заголовок раздела «Доверительный интервал»Шум мы измерили. Теперь превратим его в ответ на вопрос «где может быть истинная точность?». Раз почти нормально распределена вокруг со стандартным отклонением SE, то в 95% экспериментов отстоит от не дальше чем на . Переворачиваем: интервал
накрывает истинную в 95% экспериментов. Это 95-процентный доверительный интервалдоверительный интервалEN confidence intervalДиапазон вокруг измеренного значения метрики, построенный так, что при многократном повторении эксперимента он накрывает истинное значение с заданной частотой (обычно 95%). Для доли верных ответов ≈ p ± 1.96·√(p(1−p)/n).. Для модели A: , то есть от 67.0% до 75.0%. Для B: , от 69.1% до 76.9%. Интервалы почти целиком перекрываются.
Откуда 1.96. Правило 68–95–99.7 говорит, что 95% значений нормального распределения лежат в пределах примерно . Точнее — в пределах . Проверим симуляцией:
z = np.random.default_rng(2).standard_normal(1_000_000) # миллион значений из N(0, 1)print(np.quantile(np.abs(z), 0.95)) # 1.96026… — 95% значений по модулю меньше этого числа# точное значение даёт scipy (по желанию): from scipy.stats import norm; norm.ppf(0.975) → 1.959963984540054Для 90%-интервала множитель 1.645, для 99% — 2.576. Чем увереннее хотим быть, тем шире интервал.
Что значат эти 95%. Это свойство процедуры: если повторять эксперимент снова и снова и каждый раз строить интервал, 95% интервалов накроют истину. Проверим на 10 000 повторов и заодно посмотрим, когда формула врёт:
rng = np.random.default_rng(3)for p_true, n in [(0.72, 500), (0.95, 30)]: p_hat = rng.binomial(n, p_true, size=10_000) / n se = np.sqrt(p_hat * (1 - p_hat) / n) # SE по данным: настоящей p мы «не знаем» covered = (p_hat - 1.96 * se <= p_true) & (p_true <= p_hat + 1.96 * se) print(p_true, n, covered.mean())0.72 500 0.94680.95 30 0.7816На 500 вопросах покрытие честные 95%. А на 30 вопросах при точности 95% формула накрывает истину лишь в 78% случаев: модель часто решает все 30 задач, , и формула выдаёт «интервал» нулевой ширины. ЦПТ требует, чтобы было достаточно большим, а — не у самого края. Для маленьких наборов (AIME — 30 задач) и точностей около 0 или 100% используйте бутстрэп или специальные интервалы (например, интервал Уилсона).
Бутстрэп
Заголовок раздела «Бутстрэп»Для доли есть формула. А для медианы длины ответа, F1-меры (урок 2.2.3) или pass@k (урок 1.0.5) — нет. Здесь выручает бутстрэпбутстрэп (выборка с возвращением)EN bootstrap samplingОснова бэггинга (bagging) и случайного леса. (bootstrap, «вытянуть себя за шнурки»). Идея: генеральную совокупность мы не видим, но наша выборка — её лучшее приближение. Будем «повторять эксперимент» на ней самой:
- Выбираем из своих примеров штук с возвращением: один пример может попасть несколько раз, другой — ни разу.
- Считаем метрику на этом «новом бенчмарке».
- Повторяем тысячи раз; 2.5-й и 97.5-й перцентили (урок 0.3.3) полученных значений — границы 95%-интервала.
def bootstrap_ci(values, stat=np.mean, n_boot=10_000, seed=0): rng = np.random.default_rng(seed) values = np.asarray(values) n = len(values) stats = [] for _ in range(n_boot): idx = rng.integers(0, n, size=n) # n случайных номеров с повторами: выборка с возвращением stats.append(stat(values[idx])) # метрика на «новом бенчмарке» return np.percentile(stats, [2.5, 97.5]) # между ними — средние 95% значений
print(bootstrap_ci(correct)) # [0.67 0.75] — почти как формула 0.71 ± 0.040lengths = np.random.default_rng(4).exponential(300, size=200) # длины 200 ответовprint(np.median(lengths).round(1), bootstrap_ci(lengths, stat=np.median).round(1))[0.67 0.75]217.6 [187.5 265.8]На доле бутстрэп совпал с формулой, а для медианы дал интервал там, где формулы у нас нет. Обратите внимание: в каждую бутстрэп-выборку не попадает около трети примеров — доля (число из урока 1.0.2). Тот же приём «выборки с возвращением» лежит в основе случайного леса (урок 2.3.1).
Сравнение двух моделей
Заголовок раздела «Сравнение двух моделей»Вернёмся к вопросу из начала: A — 71%, B — 73%, по 500 вопросов.
Непарное сравнение
Заголовок раздела «Непарное сравнение»Если бы модели решали разные наборы вопросов, оценки были бы независимы. Дисперсии независимых величин складываются (урок 1.3.1), поэтому SE разницы — , а 95%-интервал разницы — п.п. Ноль внутри с большим запасом. Вывод «B лучше» не обоснован.
Парное сравнение
Заголовок раздела «Парное сравнение»Но модели решали одни и те же вопросы! А задачи бывают лёгкие (обе модели решают) и трудные (обе проваливают). Такие вопросы о разнице моделей ничего не говорят. Значение имеют только вопросы, где модели разошлись. В исследовании Anthropic корреляция ответов передовых моделей по вопросам была от 0.3 до 0.7 — общий шум очень велик, и парное сравнение его убирает. Пусть на наших 500 вопросах картина такая:
| B верно | B неверно | |
|---|---|---|
| A верно | 340 | 15 |
| A неверно | 25 | 120 |
Модели разошлись всего на 40 вопросах: 25 решила только B, 15 — только A. Посчитаем разность по каждому вопросу и её стандартную ошибку, а затем — парный бутстрэппарное сравнение (парный тест)EN paired testСравнение двух вариантов на одних и тех же примерах через разность по каждому примеру (например, парный бутстрэп). Убирает общий шум лёгких и трудных вопросов, поэтому точнее сравнения двух независимых оценок.: пересэмплируем вопросы, одни и те же для обеих моделей.
# ответы двух моделей на одни и те же 500 вопросов: 1 — верно, 0 — неверноa = np.array([0] * 25 + [1] * 15 + [1] * 340 + [0] * 120) # модель Ab = np.array([1] * 25 + [0] * 15 + [1] * 340 + [0] * 120) # модель Bprint(a.mean(), b.mean()) # 0.71 0.73
d = b - a # разница на каждом вопросеse_paired = d.std(ddof=1) / np.sqrt(len(d))print(d.mean(), round(se_paired, 4), round(1.96 * se_paired, 4)) # 0.02 0.0126 0.0248
rng = np.random.default_rng(0)diffs = []for _ in range(10_000): idx = rng.integers(0, len(d), size=len(d)) # пересэмплируем ВОПРОСЫ — одни и те же для обеих моделей diffs.append(b[idx].mean() - a[idx].mean())diffs = np.array(diffs)print(np.percentile(diffs, [2.5, 97.5]).round(3), np.mean(diffs <= 0).round(3))0.71 0.730.02 0.0126 0.0248[-0.004 0.046] 0.066Парный интервал — п.п. вместо : вдвое уже, и бутстрэп с формулой согласны. Но ноль всё равно внутри. Последнее число, 0.066, — доля бутстрэп-разниц, которые не больше нуля. Не читайте его как «вероятность того, что B не лучше A»: это лишь грубый аналог одностороннего p-value (о p-value — ниже). Честный ответ: по этим данным нельзя утверждать, что B лучше. Если бы при тех же 71% и 73% модели расходились только на 20 вопросах (15 против 5), парный интервал был бы п.п. и ноль остался бы снаружи. Всё решают вопросы, на которых модели расходятся. Поэтому сохраняйте ответы по каждому вопросу, а не только итоговую цифру: без них парное сравнение невозможно.
p-value без ритуала
Заголовок раздела «p-value без ритуала»Второй способ задать тот же вопрос. Предположим, что разницы нет (это называют нулевой гипотезойнулевая гипотезаEN null hypothesisПредположение «эффекта нет» (модели одинаковы), относительно которого считают p-value. Обозначается H₀.). Тогда на каждом из 40 «спорных» вопросов прав A или B — как монетка. Насколько часто монетка даёт перекос не меньше нашего, 25 против 15? Эта частота и есть p-valuep-value (p-значение)EN p-valueКак часто случай дал бы результат не менее экстремальный, чем наблюдаемый, если бы эффекта не было. Не вероятность того, что эффекта нет, и не размер эффекта. Порог «значимости» по соглашению — 0.05.:
rng = np.random.default_rng(0)disc = d[d != 0] # 40 вопросов, где модели разошлисьobserved = disc.sum() # 25 − 15 = 10coins = rng.choice([-1, 1], size=(100_000, len(disc))) # «мир без разницы»: кто прав — решает монеткаfake = coins.sum(axis=1) # перекос в каждом воображаемом миреprint(observed, np.mean(np.abs(fake) >= abs(observed))) # 10 0.152В 15% «миров без разницы» случай даёт такой же или больший перекос. Это обычное дело, а не повод праздновать. По соглашению результат называют статистически значимым, если p-value меньше 0.05; обычно это совпадает с тем, что 95%-интервал разницы не содержит ноль. Два частых заблуждения: p-value — не вероятность того, что модели одинаковы, и не размер разницы. Крошечное улучшение на миллионе примеров может иметь p = 0.0001 и при этом быть бесполезным. Смотрите на интервал: он показывает и есть ли эффект, и насколько он велик.
Множественные сравнения и p-hacking
Заголовок раздела «Множественные сравнения и p-hacking»Допустим, вы проверили 20 идей — например, по очереди убирали части модели и смотрели, что изменится (такие эксперименты называют абляциямиабляционный эксперимент, абляцияEN ablation studyУбрать или заменить один компонент и сравнить с полной моделью, чтобы измерить вклад этого компонента.), — и ни одна на самом деле ничего не меняет. Каждая проверка на уровне 5% ложно «срабатывает» в 5% случаев. Вероятность, что сработает хотя бы одна из 20 независимых проверок, — (обратное событие «все 20 молчат», урок 1.0.5):
rng = np.random.default_rng(5)n, p, k, runs = 500, 0.70, 20, 5000a = rng.binomial(n, p, size=(runs, k)) / n # базовая модель, k сравнений в каждом из runs «исследований»b = rng.binomial(n, p, size=(runs, k)) / n # «улучшения», которые на деле ничего не меняютse = np.sqrt(a * (1 - a) / n + b * (1 - b) / n)signif = np.abs(b - a) > 1.96 * se # какие сравнения выглядят «значимыми»print(signif.mean().round(3), signif.any(axis=1).mean().round(3)) # 0.051 0.644Каждое сравнение ошибается в 5% случаев, но в 64% «исследований» найдётся хотя бы одно «открытие». Это проблема множественных сравнениймножественные сравненияEN multiple comparisonsЧем больше проверок, тем больше ложных «открытий»: среди 20 бесполезных идей хотя бы одна окажется «значимой» на уровне 5% с вероятностью ≈ 64%. Лечат заранее зафиксированным протоколом, перепроверкой и поправками (Бонферрони).. Её злая версия — p-hacking: перебирать промпты, seed, подмножества бенчмарка и способы считать метрику, пока что-нибудь не станет «значимым», и сообщить только это.
Родственный эффект — проклятие победителя. Возьмите 10 промптов, которые на самом деле одинаково хороши (точность 70%), и проверьте каждый на 100 вопросах. Лучший в среднем покажет около 77%:
rng = np.random.default_rng(6)scores = rng.binomial(100, 0.7, size=(100_000, 10)) / 100 # 10 одинаковых промптов × 100 вопросов, 100 000 разprint(scores.max(axis=1).mean().round(3)) # 0.769 — «победитель» лучше истины на 7 п.п.Как защититься:
- Заранее решите, что и как сравниваете: набор, метрику, число seed.
- Выбирайте лучший вариант на одном наборе (валидационном), а итоговое число сообщайте на другом, который не трогали при выборе (тестовом, урок 2.1.1).
- Перепроверяйте «победителя» на свежих данных и других seed. Если эффект настоящий, он повторится. Как сравнивать обучения по 3–5 seed с честным интервалом — в уроке 11.1.3.
- Если сравнений , грубая поправка Бонферрони — требовать p-value меньше .
Сколько примеров нужно
Заголовок раздела «Сколько примеров нужно»Из формулы интервала можно найти размер набора. Хотим полуширину интервала (например, п.п.):
Если заранее неизвестна, берут худший случай , где максимально:
| Полуширина | ±10 п.п. | ±5 п.п. | ±3 п.п. | ±2 п.п. | ±1 п.п. |
|---|---|---|---|---|---|
| Нужно вопросов | 97 | 385 | 1068 | 2401 | 9604 |
Для грубой проверки «не сломали ли мы модель» хватит сотни примеров (±10 п.п.). Чтобы различать изменения в 1–2 п.п., нужны тысячи — или парное сравнение. Для него вместо подставляют дисперсию парной разности; для ответов 0/1 она примерно равна доле вопросов, на которых модели расходятся.
Последний шаг — мощностьмощность экспериментаEN statistical powerВероятность, что эксперимент обнаружит эффект, если он действительно есть. Обычно планируют 80%; для этого считают нужный размер выборки до эксперимента.: вероятность, что эксперимент заметит разницу, если она действительно есть. Чтобы настоящая разница оказывалась «значимой» в 80% повторений, к 1.96 добавляют запас 0.84: . Пример: модели расходятся на 8% вопросов (), хотим надёжно замечать п.п.: вопросов. На 500 вопросах из нашего примера такую разницу мы чаще пропустим, чем заметим. Считайте мощность до эксперимента — это одна из пяти рекомендаций статьи Anthropic. Та же формула задаёт размер A/B-теста в продукте: чтобы заметить рост доли заказавших в ленте маркетплейса с 4% до 4.08%, нужно около 950 000 пользователей в каждой группе (урок 11.2.3).
Углубление: многомерное нормальное распределение (понадобится в VAE и диффузии)
До сих пор колокол был одномерным. Часто нужно сэмплировать сразу вектор чисел. Самый простой случай — независимых величин : это многомерное нормальное распределение ( — единичная матрица). Именно такой шум добавляют к картинкам в диффузионных моделях (урок 9.1.2) и из него сэмплируют скрытый вектор в VAE (урок 9.1.3). Сдвиг и масштаб дают , где .
Координаты могут быть и связаны — как ответы двух моделей на одни и те же вопросы. Связь описывает ковариация : больше нуля, если и обычно отклоняются в одну сторону. Дисперсии и ковариации собирают в матрицу ковариаций , и распределение пишут :
rng = np.random.default_rng(7)cov = np.array([[1.0, 0.8], [0.8, 1.0]]) # дисперсии 1, ковариация 0.8: координаты «ходят вместе»x = rng.multivariate_normal([0.0, 0.0], cov, size=100_000)print(np.cov(x.T).round(2)) # оценка матрицы по сэмплам ≈ исходнаяprint(np.var(x[:, 0] - x[:, 1]).round(2)) # 0.4 = 1 + 1 − 2·0.8Последняя строка объясняет парное сравнение одной формулой: . Чем сильнее связаны оценки двух моделей, тем меньше шум их разности.
flowchart TD
A["Есть ответы двух вариантов<br/>на одних и тех же вопросах?"] -->|да| B["Парная разность d = b − a<br/>парный бутстрэп"]
A -->|нет| C["Интервал разницы<br/>√(SE_A² + SE_B²)"]
B --> D{"95%-интервал<br/>содержит 0?"}
C --> D
D -->|да| E["«Значимой разницы<br/>не обнаружено»"]
D -->|нет| F["Разница есть, её размер — интервал.<br/>Сравнений было много? Перепроверьте"]Практика
Заголовок раздела «Практика»Реализуйте инструменты урока: стандартную ошибку, интервал точности, бутстрэп для любой метрики, парный бутстрэп и расчёт размера набора. Тесты проверят их на данных урока, а одна симуляция — что ваш интервал действительно накрывает истину примерно в 95% повторов.
import math
import numpy as np
def standard_error(p, n):
"""Стандартная ошибка доли верных ответов: sqrt(p · (1 − p) / n).
Пример: standard_error(0.72, 500) ≈ 0.0201.
"""
# TODO
...
def accuracy_ci(correct, z=1.96):
"""correct — массив (или список) из 0 и 1: верно ли модель ответила на каждый вопрос.
Верните кортеж (p_hat, lo, hi): точность p_hat = доля единиц и границы интервала p_hat ± z · SE,
где SE считается по p_hat и числу вопросов.
Пример: 355 единиц из 500 → (0.71, ≈0.6702, ≈0.7498).
"""
# TODO
...
def bootstrap_ci(values, stat=np.mean, n_boot=2000, alpha=0.05, seed=0):
"""Перцентильный бутстрэп-интервал для метрики stat (любая функция массива → число).
1) rng = np.random.default_rng(seed);
2) n_boot раз: выберите n случайных индексов с возвращением (rng.integers(0, n, size=n)),
посчитайте stat на values[idx];
3) верните (lo, hi) — перцентили 100·alpha/2 и 100·(1 − alpha/2) полученных значений.
"""
# TODO
...
def paired_bootstrap(a, b, n_boot=5000, alpha=0.05, seed=0):
"""a, b — ответы (0/1) двух моделей на ОДНИ И ТЕ ЖЕ вопросы (одинаковой длины).
Пересэмплируйте вопросы с возвращением — одни и те же индексы для a и b — и каждый раз считайте
a[idx].mean() − b[idx].mean(). Верните кортеж из четырёх чисел:
(diff, lo, hi, p_not_better)
diff — разница mean(a) − mean(b) на исходных данных; lo, hi — перцентили 100·alpha/2 и 100·(1 − alpha/2);
p_not_better — доля бутстрэп-разниц, которые меньше или равны 0 (повторы, где «a не лучше b»).
Это не вероятность того, что a не лучше b, а лишь грубый аналог одностороннего p-value.
"""
# TODO
...
def samples_needed(half_width, p=0.5, z=1.96):
"""Сколько вопросов нужно, чтобы полуширина интервала z · sqrt(p(1 − p) / n) была ≤ half_width.
Верните целое число, округлив вверх. Пример: samples_needed(0.01) → 9604.
"""
# TODO: выразите n из формулы полуширины
...
# Проверьте себя кнопкой «Запустить»:
correct = np.array([1] * 355 + [0] * 145)
print(standard_error(0.72, 500)) # ≈ 0.0201
print(accuracy_ci(correct)) # ≈ (0.71, 0.6702, 0.7498)
print(bootstrap_ci(correct)) # ≈ (0.67, 0.75)
a = np.array([0] * 25 + [1] * 15 + [1] * 340 + [0] * 120)
b = np.array([1] * 25 + [0] * 15 + [1] * 340 + [0] * 120)
print(paired_bootstrap(b, a)) # ≈ (0.02, −0.004, 0.044, 0.068): ноль внутри интервала
print(samples_needed(0.01)) # 9604
Проверь себя
Заголовок раздела «Проверь себя»- Бенчмарк — выборка из генеральной совокупности вопросов, и точность на нём — случайная величина. Её разброс — стандартная ошибка (в общем случае ); вдвое меньше шум — вчетверо больше вопросов.
- ЦПТ: среднее многих независимых величин почти нормально. Отсюда интервал , который накрывает истину в 95% экспериментов. На маленьких наборах и у краёв (0% или 100%) формула врёт — берите бутстрэп.
- Бутстрэп — пересэмплирование своих данных с возвращением; даёт интервал для любой метрики.
- Модели сравнивают парно, на одних и тех же вопросах: всё решают вопросы, где они расходятся. Значимость — это «95%-интервал разницы не содержит 0», а p-value — частота такого перекоса в «мире без разницы».
- Много сравнений рождают ложные открытия и проклятие победителя: фиксируйте протокол заранее и перепроверяйте на свежих данных. Размер набора и мощность считайте до эксперимента.
Дальше: модуль 1.4 «Оптимизация» — градиентный спуск: как модель находит параметры, которые минимизируют cross-entropy, и почему шум мини-батча (тот самый ) ему даже помогает.