Руководство разработчика по Laya: решения zero-shot и калибровка
В этом руководстве мы работаем с Laya — движком принятия решений с открытым исходным кодом от Convai Innovations, который стал одним из наиболее отмеченных звёздами репозиториев в области машинного обучения в сентябре 2026 года. Laya — нерегрессионная модель System 1: вместо генерации текста энкодер с 421 миллионом параметров читает фрагмент текста и набор типизированных вопросов — выбор между метками, оценку по шкале или вопрос «да/нет» — и за один прямой проход возвращает вероятность для каждого варианта, не генерируя ни одного выходного токена. Её преимущества — скорость и откалиброванные вероятности, открытая альтернатива Jev от TypeSafe. Вместо того чтобы повторять примеры из README, мы проверяем эти обещания на реальных размеченных данных с известными ответами: на банковском домене датасета намерений CLINC150. Мы измеряем то, что действительно важно для производственного маршрутизатора: точность в режиме zero-shot по сравнению с обученным классификатором, влияние формулировки и порядка вариантов, честность поставляемых вероятностей, что исправляет подгонка температуры на валидационных данных и что она незаметно ломает, порог отказа, настроенный под допустимый уровень ошибок, запросы вне области применения, вопрос «да/нет», который температура не может исправить, а также типизированные результаты из схемы pydantic.
Мы устанавливаем выпущенный пакет laya 0.3.27 и загружаем англоязычный чекпойнт. Для воспроизводимости запуска важны два решения. По умолчанию laya.load использует основную ветку Hugging Face, поэтому мы фиксируем ревизию, проверенную самими авторами библиотеки; она доступна через laya.PINNED_REVISIONS. Кроме того, на CUDA Laya автоматически использует вычисления в половинной точности, поэтому мы отключаем это, оставляя все устройства в fp32 и позволяя запуску на GPU воспроизводить приведённые здесь результаты CPU. Вывод поставляемых температур чекпойнта сразу показывает первую проблему ещё до выполнения предсказаний: значение для вопросов выбора с одиннадцатью и более вариантами равно 0.10, что выходит за допустимый диапазон, поэтому загрузчик заменяет его на 0.5 и выдаёт предупреждение. Температура ниже единицы делает вероятности более резкими, поэтому каждый ответ на вопрос с таким количеством вариантов будет выглядеть вдвое более уверенным, чем есть на самом деле.
Один вызов predict отвечает на три типизированных вопроса о заявке в службу поддержки за один прямой проход: определяет отдел, оценивает срочность по шкале от 0 до 2 и определяет риск оттока в формате «да/нет». Результат содержит вероятность для каждого варианта и два поля уверенности, которые легко перепутать. answer_confidence — вероятность сообщённого ответа; именно эту величину используют калибровка, порог отказа и все последующие метрики в этом руководстве. confidence — единица минус нормированная энтропия, масштаб которой зависит от количества вариантов ответа. Блок usage показывает ноль выходных токенов, поскольку Laya оценивает переданные ей варианты и никогда не генерирует текст.
Прежде чем строить решение на Laya, мы измеряем стоимость прямого прохода для одного сообщения. Каждый вопрос становится отдельной строкой, объединённой с сообщением, поэтому шестнадцать вопросов «да/нет» занимают примерно в восемь раз больше времени, чем один такой вопрос. Все варианты одного вопроса выбора используют одну строку и общий бюджет его головы, поэтому вопрос с сорока вариантами едва ли вдвое дороже вопроса с тремя вариантами и занимает примерно четверть времени шестнадцати вопросов «да/нет» на нашем CPU. Отсюда следует правило проектирования, которое определяет всё дальнейшее: лучше задавать один вопрос выбора с большим количеством вариантов, чем множество вопросов «да/нет».
Для реальных размеченных данных мы используем CLINC150 — общедоступный набор для классификации намерений со 150 намерениями в десяти доменах и набором запросов вне области применения. Мы загружаем его напрямую из Hugging Face Hub в формате parquet. Из него берём банковский домен: пятнадцать намерений, по 100 обучающих, 20 валидационных и 30 тестовых запросов на каждое. Затем просим Laya маршрутизировать 450 тестовых запросов в режиме zero-shot, передавая название каждого намерения и однострочное описание, которое мог бы написать разработчик. Точность без единого размеченного примера составляет 0.804. Для сравнения, классификатор на основе TF-IDF и логистической регрессии достигает 0.651 при трёх размеченных запросах на намерение, 0.848 при десяти и 0.904 при тридцати.
Затем мы меняем только формулировку вариантов. Если передать Laya пятнадцать простых названий намерений без описаний, точность повышается с 0.804 до 0.878, а время работы сокращается вдвое, поскольку варианты занимают менее половины исходного количества токенов. Описания размывали различия, которые названия сохраняли: account_blocked десять раз классифицировалось как freeze_account, а вопросы о процентной ставке — как balance. Изменение порядка простых названий меняет 4.2 процента отдельных ответов, хотя общая точность почти не меняется. Это указывает на наличие позиционного априорного смещения, поэтому порядок вариантов в рабочей системе должен совпадать с порядком, который вы тестировали. Выявить обе особенности удалось только благодаря размеченным данным; далее мы используем простые названия намерений.
Затем мы проверяем, насколько честны вероятности. Вопрос с пятнадцатью вариантами попадает в корзину choice:11+ чекпойнта — ту самую, для которой температура была ограничена значением 0.5. Таблица надёжности на 450 тестовых запросах показывает результат: 92 процента ответов заявляют уверенность не менее 0.9, но правильными оказываются 91.1 процента из них. Средняя уверенность 0.974 заметно превышает точность 0.878, а ожидаемая ошибка калибровки составляет 0.102. Целевая функция обучения Laya использует корректные правила оценки, что и означает слово «калиброванная» в карточке модели. Однако калибровка является свойством конкретного вопроса на конкретном распределении данных, поэтому её необходимо измерять на собственных размеченных данных.
Модуль калибровки Laya преобразует размеченные примеры в записи с исходными логитами и целевыми значениями и подбирает для них температуру. Мы создаём 300 записей из валидационной выборки и вызываем agent.fit_temperatures. Метод подбирает температуру выбора 1.258 и устанавливает её, после чего выводит полную таблицу температур рядом с исходными значениями. Подгонка заменяет всю карту: отдельные записи для каждого количества вариантов исчезают, поскольку для сохранения собственной температуры корзине требуется 2 000 записей. Температуры для оценки и вопросов «да/нет» сбрасываются до 1.0, поскольку примеров этих типов не было. Таким образом, одна подгонка для вопросов выбора незаметно меняет калибровку всех вопросов «да/нет» в агенте. Поэтому мы восстанавливаем исходные значения и устанавливаем только измеренную корзину. На тестовом наборе ошибка калибровки снижается с 0.102 до 0.059, а точность остаётся прежней, поскольку температура не меняет вариант с максимальным логитом. Метод save_calibration сохраняет результат в JSON-файл, который затем можно загрузить через laya.load.
Метод laya.fit_abstention_thresholds использует те же валидационные записи и для каждой корзины по количеству вариантов возвращает наименее строгий порог уверенности, при котором ошибка на валидации не превышает заданное значение. Для целевого уровня 5 процентов он выбирает порог 0.602: 95.7 процента валидационных запросов сохраняются при ошибке 4.5 процента. Laya применяет этот порог самостоятельно, если передать его в predict_batch через min_confidence, и помечает 35 из 450 тестовых ответов как требующие отказа. Однако на тестовом наборе этот порог сохраняет 92.2 процента запросов при ошибке 9.2 процента — почти вдвое выше целевого уровня; для цели 2 процента фактическая ошибка составляет 5.3 процента. Причина очевидна: на валидации Laya отвечает правильно в 92.7 процента случаев, а на тесте — лишь в 87.8 процента. Поэтому бюджет ошибок, настроенный на одной выборке, работает только для трафика, похожего на неё. Само ранжирование надёжно: наиболее уверенная половина тестовых ответов правильна в 97.8 процента случаев. Но целевой уровень ошибок требует запаса и периодической перенастройки на реальном трафике. Пороги остаются отдельными для каждой корзины по количеству вариантов, поскольку одно значение не подходит одновременно для вопроса с двумя и с пятнадцатью вариантами.
Рабочий трафик включает запросы, для которых маршрутизатор никогда не создавался, поэтому мы добавляем 150 запросов CLINC вне области применения и 150 запросов из других доменов CLINC. Калиброванная уверенность хорошо их разделяет: средняя уверенность для банковских запросов составляет 0.912, а для остальных — около 0.25. Порог 5 процентов останавливает 89.3 процента запросов из других доменов и 93.3 процента запросов вне области применения, отказываясь лишь от 7.8 процента банковских запросов. Альтернативный подход не требует размеченных данных: шестнадцатый вариант «не банковский запрос» распознаёт 80.0 и 90.0 процента таких запросов соответственно. Однако 2.0 процента банковских запросов начинают маршрутизироваться иначе, а банковская точность снижается с 0.878 до 0.864, поскольку новый вариант меняет вопрос, относительно которого оцениваются все остальные намерения.
Специализированный вопрос «да/нет» кажется естественным способом проверить принадлежность к области применения, поэтому мы спрашиваем, относится ли каждое сообщение к банковскому счёту, счетам или платежам пользователя. Вопрос хорошо ранжирует примеры: AUROC составляет 0.945, но он смещён в сторону ответа «нет». Средняя вероятность для банковских запросов составляет лишь 0.361, поэтому при стандартном пороге 0.5 система распознаёт только 28.9 процента таких запросов. Подгонка температуры на 550 валидационных ответах упирается в верхний предел 5.0 и ничего не меняет при этом пороге, поскольку деление двух логитов на любую температуру не меняет того, какой из них больше. Масштабирование температуры исправляет масштаб, но не смещение. Рабочий подход — рассматривать вероятность как оценку и выбирать порог на размеченных данных: порог 0.09, выбранный на валидационной выборке, даёт на тесте полноту 92.0 процента и специфичность 83.0 процента.
Наконец, мы подключаем всё это к прикладному коду через схему pydantic. laya.decide_batch превращает поле Literal в вопрос выбора по простым названиям, которые, как показал шаг 5, здесь работают лучше, а поле bool — в вопрос «да/нет». Затем ответы преобразуются обратно в экземпляр проверенной модели. Передача порога из шага 8 через min_confidence превращает неуверенное намерение в None, которое допускается полем Optional; таким образом, None становится явным сигналом «передать запрос сотруднику». Два запроса вне области применения возвращаются с intent=None. Однако логическое поле внутри decide сравнивается с фиксированным порогом 0.5, поэтому сообщение о мошенничестве получает in_scope=False при вероятности 0.29. Чтобы получить правильный результат, нужно прочитать вероятность из подробностей ответа и применить порог из шага 10.
Итог выводит однострочный результат каждого шага и перечисляет практики, которые стоит перенять: фиксировать чекпойнт и проверять его поставляемые температуры, тестировать формулировки критериев и порядок вариантов на размеченных данных, подбирать температуры на отложенной выборке и устанавливать только измеренную корзину, задавать пороги отказа отдельно для каждой корзины по количеству вариантов и оставлять запас ниже целевого уровня, а также выбирать порог для вопроса «да/нет» по размеченным данным. В конце указаны следующие направления: дообучение, доступное в laya.train в основной ветке репозитория, но пока отсутствующее в пакете 0.3.27, многоязычный чекпойнт за laya.Router и серверное использование.
В заключение, Laya выполняет большую часть своих обещаний: за один прямой проход она отвечает на несколько типизированных вопросов без генерации токенов, маршрутизатор с простыми названиями достиг точности 0.878 на пятнадцати реальных банковских намерениях без обучающих данных, тогда как классификатор TF-IDF должен увидеть от десяти до тридцати размеченных примеров на каждое намерение, чтобы достичь сопоставимого результата. Калиброванная уверенность также достаточно хорошо разделяет запросы внутри и вне области применения, чтобы останавливать более девяти из десяти внешних запросов. Однако ценность вероятностей зависит от работы, которую библиотека оставляет пользователю, а некоторые значения по умолчанию работают в неправильную сторону. Поставляемая температура для вопросов с одиннадцатью и более вариантами не смягчает, а усиливает вероятности; один вызов калибровки стирает температуры для типов вопросов, которых он не видел; бюджет ошибок, настроенный на валидационных данных, работает только для похожего трафика; а вопрос «да/нет» может хорошо ранжировать ответы, но находиться не на той стороне порога 0.5 — и никакая температура этого не исправит, тогда как проекция схемы жёстко зафиксирует такой результат. Для каждой проблемы выше показано исправление в несколько строк, и без него каждая из них могла бы остаться незаметной. Практический вывод совпадает с тем, что сказано в карточке модели этой библиотеки, но о чём легко забыть из-за её настроек по умолчанию: модель принятия решений является откалиброванной только после того, как вы откалибровали её на собственных метках и для собственных вопросов.
Ознакомьтесь с ПОЛНЫМ КОДОМ здесь. Все права на проект принадлежат его исследователю. Также подпишитесь на нас в Twitter и не забудьте присоединиться к нашему сабреддиту о машинном обучении с аудиторией более 150 тысяч и подписаться на нашу рассылку. Постойте! Вы пользуетесь Telegram? Теперь вы также можете присоединиться к нам в Telegram.
Переведено автоматически с английского. Оригинал статьи — по ссылке ниже.