Материал подготовлен в рамках курса «Машинное обучение. Экспертный уровень».
Привет, Хабр!
Обучили классификатор, он выдаёт predict_proba, вы берёте эти числа как вероятности и строите на них логику: если модель уверена больше чем на 0.8 — принимаем автоматически, меньше — отправляем человеку. Порог 0.8 выбран как «модель права в 80% случаев».
Вот только модель, скорее всего, права совсем не в 80% случаев на этом пороге. Число 0.8 на выходе классификатора — это не вероятность в обычном смысле, а внутренний скор, который модель выдала, оптимизируя свою функцию потерь. Он неплохо ранжирует объекты — те, кому модель дала выше, обычно правее тех, кому ниже. Но как абсолютная вероятность он врёт, иногда сильно.
Это проблема калибровки, и она отдельная от точности. Модель может отлично разделять классы и при этом чудовищно врать о своей уверенности. Разберём, откуда это берётся, как измерить, и как починить, не трогая саму модель.
Что вообще значит «откалибрована»
Определение простое и проверяемое. Модель откалибрована, если среди всех объектов, которым она дала вероятность 0.8, доля реально положительных — ровно 80%. Среди тех, кому дала 0.3, положительных — 30%. И так на каждом уровне.
Это то самое свойство, которое вы неявно предполагаете, когда используете predict_proba как вероятность. Если оно не выполняется, ваши пороги означают не то, что вы думаете.
Проверяется калибровка калибровочной кривой: разбивают предсказания на корзины по уровню вероятности и для каждой корзины смотрят, какая доля объектов в ней реально положительная.
Откалиброванная модель ляжет на диагональ — предсказанная вероятность совпадает с наблюдаемой частотой.
from sklearn.calibration import calibration_curve frac_pos, mean_pred = calibration_curve(y_test, probs, n_bins=10) # frac_pos — реальная доля положительных в корзине # mean_pred — средняя предсказанная вероятность в корзине # идеал: frac_pos == mean_pred
Обычно кривая от диагонали уезжает. Куда именно — зависит от модели, и это важно, потому что разные семейства врут по‑разному.
Кто и как врёт
У характерных ошибок калибровки есть узнаваемые формы, и по ним видно природу проблемы.
Нейросети с софтмаксом почти всегда переуверены. Они выдают 0.99 там, где реальная частота — процентов восемьдесят.
Причина — обучение до предела: сеть с большой ёмкостью загоняет логиты в крайние значения, минимизируя кросс‑энтропию на обучении, и вероятности прижимаются к нулю и единице сильнее, чем оправдано. Чем дольше учили и чем больше модель, тем сильнее эффект.
Градиентный бустинг обычно врёт иначе — он избегает крайних вероятностей, стягивая предсказания к середине. Уверенные случаи получают не
0.99, а0.85, потому что усреднение по деревьям сглаживает.SVM в исходном виде вообще не выдаёт вероятностей — он выдаёт расстояние до разделяющей гиперплоскости, которое приходится превращать в вероятность отдельным преобразованием, и по умолчанию оно тоже далеко от честного.
Наивный байес печально известен экстремальными вероятностями —
0.9999и0.0001— из‑за предположения о независимости признаков, которое почти никогда не выполняется, и модель множит вероятности так, будто признаки независимы, получая абсурдно уверенные числа.
Отсюда практический вывод: если вы используете
predict_probaдля логики с порогами, сначала посмотрите на калибровочную кривую своей модели. Форма кривой сразу скажет, врёт модель в сторону переуверенности или недоуверенности, и насколько.
Чем это мерить в одном числе
Кривая — это наглядно, но для мониторинга и сравнения нужно число. Основная метрика — expected calibration error, ECE.
Считается она так: предсказания разбивают на корзины, в каждой берут разницу между средней предсказанной вероятностью и реальной долей положительных, и усредняют эти разницы по корзинам, взвешивая по числу объектов.
def expected_calibration_error(y_true, probs, n_bins=10): bins = np.linspace(0, 1, n_bins + 1) ece = 0.0 for i in range(n_bins): mask = (probs >= bins[i]) & (probs < bins[i + 1]) if mask.sum() > 0: acc = y_true[mask].mean() # реальная доля положительных conf = probs[mask].mean() # средняя уверенность ece += mask.mean() * abs(acc - conf) return ece
ECE, равный нулю, — идеальная калибровка. Чем больше, тем сильнее модель врёт о своей уверенности. У ECE есть недостатки — она зависит от числа корзин и усредняет, скрывая, что на одном участке модель сильно переуверена, а на другом недоуверена, и они компенсируются.
Поэтому ECE смотрят вместе с кривой, а не вместо неё.
И не путайте калибровку с качеством. Есть метрика Брайера — средний квадрат разницы между вероятностью и истиной — которая ловит и то, и другое разом. Но полезнее разделять: точность отвечает на вопрос «правильно ли модель ранжирует», калибровка — «честны ли её числа». Модель бывает точной и плохо откалиброванной одновременно, и лечится это по‑разному.
Platt scaling: калибровка логистикой
Раз калибровка отдельна от точности, чинить её можно, не трогая модель, — отдельным преобразованием поверх её выходов. Это post‑hoc калибровка, и для неё нужен отдельный кусок данных, который модель не видела при обучении.
Первый классический метод — Platt scaling. Идея: взять сырые скоры модели и обучить поверх них простую логистическую регрессию, которая переведёт скор в честную вероятность.
from sklearn.linear_model import LogisticRegression # calibrator учится на отложенной выборке переводить скор в вероятность raw_scores = model.decision_function(X_cal).reshape(-1, 1) calibrator = LogisticRegression().fit(raw_scores, y_cal) # на новом объекте calibrated = calibrator.predict_proba(model.decision_function(X_new).reshape(-1, 1))
Platt scaling подгоняет сигмоиду — растягивает и сдвигает скоры так, чтобы они легли на диагональ. Он хорошо работает, когда искажение калибровки имеет сигмоидную форму, что типично для SVM и вообще для случаев, когда модель врёт «плавно». Плюс он экономный по данным — у него всего два параметра, так что калибровочная выборка может быть небольшой.
Ограничение в том, что он навязывает форму. Если реальное искажение не сигмоидное, Platt его не выправит — он умеет только то, что умеет сигмоида.
Изотоническая регрессия: калибровка без формы
Второй метод свободнее — изотоническая регрессия. Она не предполагает никакой формы искажения, а просто учит монотонное преобразование скора в вероятность, повторяющее любую кривую, лишь бы та не убывала.
from sklearn.isotonic import IsotonicRegression iso = IsotonicRegression(out_of_bounds='clip') iso.fit(model.predict_proba(X_cal)[:, 1], y_cal) calibrated = iso.predict(model.predict_proba(X_new)[:, 1])
Монотонность здесь ключевая и разумная: если модель дала объекту A скор выше, чем B, то и откалиброванная вероятность A должна быть не ниже — порядок объектов не должен ломаться. В остальном изотоническая регрессия свободна и повторит искажение любой формы.
Плата за гибкость — аппетит к данным. Гибкий метод легко переобучается на маленькой калибровочной выборке, подгоняясь под её шум. Ориентир простой: мало калибровочных данных — Platt, много — изотоническая. На нескольких сотнях объектов Platt надёжнее, на нескольких тысячах изотоническая обычно точнее.
Калибровка нейросетей: temperature scaling
Для нейросетей есть свой метод, элегантный до неприличия, — temperature scaling. Он лечит ровно ту переуверенность софтмакса, о которой шла речь.
Идея: перед софтмаксом поделить все логиты на одно число — температуру
T. Больше единицы — вероятности смягчаются, растягиваются от краёв к середине, переуверенность спадает. Ровно единица — ничего не меняется. Меньше — наоборот, обостряет.
# подбираем одну T, минимизируя потерю на калибровочной выборке def apply_temperature(logits, T): return softmax(logits / T)
T подбирают на отложенной выборке, минимизируя кросс‑энтропию. И вот что важно: это один‑единственный параметр на всю модель. Он не трогает предсказанный класс — деление всех логитов на одно число не меняет, какой из них максимальный, — а значит, точность модели остаётся ровно прежней. Меняется только уверенность: 0.99 превращается в честные 0.85.
Именно то, что temperature scaling не портит точность и правит только калибровку, сделало его стандартом для нейросетей. Один параметр, никакого риска сломать модель, и переуверенность софтмакса уходит.
Что калибровка делает с обучающей потерей
Тонкость, которая объясняет, почему модели переуверены, и почему это не лечится обучением.
Кросс‑энтропия — стандартная функция потерь классификации — минимизируется, когда модель предсказывает единицу для правильного класса. То есть само обучение толкает модель к крайним вероятностям: чем ближе к единице на правильных, тем меньше потеря. У модели нет стимула быть честной о неуверенности — за скромность в вероятностях её штрафуют так же, как за ошибку.
Отсюда следует, что переуверенность — не дефект конкретной модели, а прямое следствие того, чем её обучали. Нельзя «обучить получше» и получить калибровку в подарок: цель обучения и цель калибровки расходятся. Чем лучше модель минимизирует кросс‑энтропию, тем увереннее — и часто тем хуже откалибрована.
Именно поэтому калибровку выносят в отдельный шаг после обучения. Пытаться получить и точность, и калибровку одной функцией потерь — значит требовать от неё двух разных вещей сразу.
Проще разделить: обучение отвечает за то, чтобы модель хорошо разделяла классы, отдельный слой — за то, чтобы её числа были честными. Есть подходы, которые встраивают калибровку в обучение через специальные регуляризаторы, но post‑hoc обычно проще, дешевле и не рискует испортить точность.
Главная ошибка: калибровка на тех же данных
Теперь про то, на чём калибровку чаще всего портят. Калибратор нельзя учить на тех данных, на которых училась модель.
Логика та же, что с утечкой в обычном ML. Модель на обучающих данных переуверена особенно сильно — она их запомнила. Если калибровать на них же, калибратор увидит эту фальшивую уверенность как норму и настроится под неё, а на новых данных калибровка развалится.
Калибратору нужен свой кусок, отложенный и не участвовавший в обучении модели. Данные делят на три части: обучение модели, калибровка калибратора, тест для проверки. Либо, если данных мало, используют кросс‑валидацию — и ровно это делает встроенный инструмент scikit‑learn:
from sklearn.calibration import CalibratedClassifierCV # cv='prefit' — если модель уже обучена и есть отдельная калибровочная выборка # cv=5 — если хотим кросс-валидацию на общих данных calibrated_model = CalibratedClassifierCV(model, method='isotonic', cv=5) calibrated_model.fit(X_train, y_train)
CalibratedClassifierCV внутри делает всё правильно — обучает калибратор на отложенных фолдах, а не на данных модели. Написать это руками и не наступить на утечку сложнее, чем взять готовое.
Beta‑калибровка и другие варианты
Platt и изотоническая — два полюса, но между ними есть промежуточные методы, полезные, когда ни один полюс не подходит.
Beta‑калибровка — обобщение Platt scaling, которое использует более гибкое семейство кривых, чем простая сигмоида, но всё ещё параметрическое и экономное по данным. Она справляется с искажениями, которые Platt не берёт из‑за жёсткой формы сигмоиды, и при этом не так прожорлива до данных, как изотоническая. Разумный средний вариант, когда данных не то чтобы мало, но и не тысячи, а форма искажения явно не сигмоидная.
Есть сплайн‑калибровка — гладкая кривая, которая, в отличие от ступенчатой изотонической, не даёт резких скачков вероятности между соседними значениями. Изотоническая регрессия выдаёт кусочно‑постоянную функцию, и на границах корзин вероятность прыгает скачком, что иногда нежелательно. Сплайн сглаживает.
Выбор между всем этим — компромисс между гибкостью и аппетитом к данным, всегда один и тот же.
Жёсткая параметрическая форма — мало данных, но рискуете не выправить сложное искажение.
Гибкая непараметрическая — выправит что угодно, но нужна большая калибровочная выборка и есть риск переобучения.
Beta и сплайны занимают середину. На практике начинают с простого — Platt или temperature для сетей — и переходят к гибкому, только если простое не выправило кривую.
Мультикласс, дрейф и прочее.
Мультикласс калибруют обычно по схеме «один против всех» — отдельный калибратор на каждый класс, — после чего вероятности перестают суммироваться в единицу, и их нужно перенормировать. Temperature scaling в этом смысле удобнее: одна температура на все классы, сумма остаётся корректной автоматически.
Калибровка может уезжать при дрейфе. Она подгонялась под определённое распределение данных, и когда прод‑данные меняются, калибратор устаревает так же, как сама модель, — вероятности снова начинают врать. Поэтому ECE стоит мониторить в проде, а не измерить один раз при внедрении. Растущий ECE — сигнал перекалибровать, иногда раньше, чем потребуется переобучать саму модель.
Отдельная тонкость с несбалансированными классами. При сильном перекосе калибровочная кривая в области редкого класса строится по горстке объектов и получается шумной. Тут помогает больше калибровочных данных именно для редкого класса и осторожность с изотонической регрессией, которая на малой выборке переобучится первой.
Если модель нужна только чтобы ранжировать — выбрать топ кандидатов, отсортировать по риску, — калибровка не важна, важен порядок, а его модель и так держит.
Калибровка нужна ровно тогда, когда абсолютное значение вероятности идёт в решение: порог автоматизации, ожидаемая ценность, стоимость ошибки, объединение с вероятностями из других источников. Вот там 0.8, которое на самом деле 0.65, стоит реальных денег.
Калибровка и порог решения — разные вещи
Частая путаница: калибровку смешивают с подбором порога, хотя это про разное, и делать надо оба.
Порог решения — это где вы проводите границу между классами. По умолчанию 0.5, но если ошибки стоят по‑разному (пропустить больного дороже, чем перепроверить здорового), порог сдвигают. Это оптимизация под стоимость ошибок, и она работает с любыми вероятностями, хоть калиброванными, хоть нет, — важно лишь, чтобы модель правильно ранжировала.
Калибровка — про то, чтобы само число было честным. Она не двигает границу, она делает так, чтобы
0.8означало 80%.
Путаница в том, что многие подбирают порог и считают, что решили проблему уверенности. Не решили: сдвинутый порог по‑прежнему стоит на неоткалиброванных числах. Если ваша логика — просто «класс A или класс B», хватит порога, калибровка не нужна.
Но если в логике участвует само значение вероятности — «автоматизируем при уверенности выше 0.9», «ожидаемая ценность равна вероятность на выигрыш», — тогда нужна калибровка, и порог её не заменяет. Часто нужны обе: сначала откалибровать, чтобы числа стали честными, потом выбрать порог под стоимость ошибок на этих честных числах.
Когда калибровка обязательна, а когда бесполезна
Стоит чётко разделить случаи, потому что калибровать всё подряд — трата сил.
Калибровка обязательна, когда абсолютное значение вероятности идёт в расчёт. Ожидаемая ценность действия, где вероятность умножается на исход. Порог автоматизации, привязанный к конкретному числу. Объединение предсказаний нескольких моделей или источников — складывать можно только честные вероятности, иначе перекос одной модели поедет в общий ответ. Отображение вероятности человеку, который будет на неё опираться, — врач, аналитик, оператор.
Калибровка бесполезна, когда важен только порядок. Ранжирование кандидатов и выбор топа. Сортировка по риску для очереди на обработку. Метрики вроде AUC, которые зависят только от порядка, а не от абсолютных значений, — их калибровка вообще не меняет, потому что порядок объектов остаётся прежним. Если вы оптимизируете AUC и по нему же принимаете решения, калибровка ничего не даст.
Граница ровно здесь: значение вероятности идёт в решение — калибруйте; важен только порядок — не тратьте время.
Коротко о порядке действий
Если свести к последовательности: сначала посмотреть на калибровочную кривую и ECE своей модели — врёт ли она вообще и в какую сторону. Если врёт и вероятности идут в решение — калибровать post‑hoc, на отдельной отложенной выборке, ни в коем случае не на данных обучения. Метод по ситуации: нейросеть — temperature scaling, мало данных — Platt, много — изотоническая. Проверить результат по ECE на тесте. И держать ECE под мониторингом в проде, потому что калибровка уезжает вместе с данными.
Если хочется понять, какие темы ML уже освоены, а к каким стоит вернуться, пройдите вступительный тест для программы «Машинное обучение. Экспертный уровень».
Всё это не трогает саму модель и не портит её точность — калибровка живёт отдельным слоем поверх выходов. Что делает её одним из самых дешёвых улучшений, какие бывают: пара десятков строк и отложенная выборка превращают числа, которым нельзя было верить, в вероятности, на которых можно строить логику.

Если хочется продолжить разбираться, как модели устроены, где проходят границы их применимости и что происходит после обучения, можно присоединиться к бесплатным урокам:
6 августа, 18:00. «Практика работы с Docker — ввод модели в эксплуатацию». Записаться
10 августа, 18:00. «Технологии продвинутого Data Science: что под капотом». Записаться
24 августа, 20:00. «Современные модели прогнозирования — TimesNet, TimeGPT и новое поколение моделей временных рядов». Записаться
Больше бесплатных уроков августа смотрите в дайджесте.
Peternsk
Похожая проблема встаёт и в LLM судьях — когда модель сама оценивает качество чужих ответов и выдаёт уверенность.