Вопросы и ответы
Вопрос Laya — это типизированное решение, и тип определяет и то, что вы спрашиваете, и то, что получаете обратно. Их три, и одно состояние может нести их все за один прямой проход:
import laya
agent = laya.load("convaiinnovations/laya")
questions = {
"dept": {"type": "choice", "instructions": "Which team should handle this?",
"criteria": {"billing": "money and invoices",
"technical": "bugs and outages",
"sales": "pricing and contracts"}},
"urgent": {"type": "noul", "instructions": "Is this urgent?"},
"severity": {"type": "score", "instructions": "How severe is this?",
"criteria": ["trivial", "minor", "moderate", "serious", "critical"]},
}
result = agent.system_one({"text": "I was charged twice and nobody has replied for a week. "
"Please refund me."}, questions)
result["answers"]["dept"]["choice"] # 'billing'
result["answers"]["urgent"]["noul"] # 0.8727
result["answers"]["severity"]["score"] # 2.9046
Один прямой проход отвечает на все три. В этом и смысл типизированного интерфейса: вопрос noul —
не choice с двумя вариантами, которые случайно оказались «да» и «нет», а отдельная голова с другой
формой выхода, и тип говорит Laya, какую использовать.
Три типа
choice — выбрать один из набора
{"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {"billing": "money and invoices", "technical": "bugs and outages"}}
criteria — это упорядоченное отображение метки на описание. Порядок позиционный в отрисованном
вопросе, поэтому два вопроса с одинаковыми метками в разном порядке — это разные вопросы. Описание
необязательно; {"billing": None} отрисовывает одну метку. Метки возвращаются ровно так, как вы их
написали, поэтому нестроковая метка возвращается как она сама в choice и как ключ в probabilities.
Описание стоит писать. Это не украшение: отрисованный текст вопроса — это то, что читает модель,
поэтому голое {"a": None, "b": None} не даёт ей ничего, чтобы различить варианты.
{"type": "choice", "choice": "billing",
"probabilities": {"billing": 0.9881, "technical": 0.0057, "sales": 0.0062},
"confidence": 0.9339, "answer_confidence": 0.9881,
"action": {"act_probability": 1.0}}
noul — да или нет
{"type": "noul", "instructions": "Is this urgent?"}
noul — это название проекта для
двустороннего решения, и его ответ — вероятность истины, а не булево значение с порогом:
{"type": "noul", "noul": 0.8727, "confidence": 0.8727, "answer_confidence": 0.8727,
"action": {"act_probability": 1.0}}
Порог выбираете вы, потому что он зависит от того, во сколько вам обходится ложноположительный
результат. Нет никакого поля bool, которое можно принять за решение.
Вы можете переименовать два варианта, когда решение не является естественно да/нет, — labels
принимает ровно ключи false и true, и полярность не меняется: noul по-прежнему P(true).
{"type": "noul", "instructions": "Does this need a human?",
"labels": {"false": "automatic", "true": "escalate"}}
Метки — не способ исправить вопрос, на который модель отвечает неверно. noul следует своим
собственным меткам вариантов, сильнее всего на английском чекпойнте, поэтому пара меток, которая
читается как решение («approve» / «reject»), может тянуть ответ к метке, а не к состоянию. Проверяйте
любое переименование на своих данных, прежде чем полагаться на него.
score — упорядоченный уровень
{"type": "score", "instructions": "How severe is this?",
"criteria": ["trivial", "minor", "moderate", "serious", "critical"]}
criteria — это упорядоченный список, и он должен быть отсортирован по возрастанию — позиция это
и есть шкала.
{"type": "score", "score": 2.9046,
"legend": {"0": "trivial", "1": "minor", "2": "moderate", "3": "serious", "4": "critical"},
"probabilities": {"0": 0.0134, "1": 0.05, "2": 0.0518, "3": 0.7881, "4": 0.0967},
"confidence": 0.5187, "answer_confidence": 0.7881,
"action": {"act_probability": 1.0}}
score — это ожидаемое значение, а не самый вероятный уровень. Выше score равен 2.90, тогда
как единственный самый вероятный уровень — serious (3) с 0.788. Оба полезны и отвечают на разные
вопросы: ожидаемое значение минимизирует квадратичную ошибку по шкале, а argmax минимизирует
расхождение с моделью. Если вам нужна метка, возьмите argmax от probabilities или прочитайте аналог
answer_confidence в легенде — не округляйте score и не полагайте, что это метка. legend
существует, чтобы вам никогда не приходилось угадывать, что означает каждый индекс.
Чтение уверенности
Каждый ответ несёт два числа уверенности, и они измеряют разные вещи.
| поле | что это | применять gating? |
|---|---|---|
answer_confidence |
max(p) — вероятность сообщаемого ответа |
да, после подбора |
confidence |
1 - H(p) / log(k) — насколько сконцентрировано всё распределение |
нет |
probabilities |
полное распределение (choice, score) |
— |
answer_confidence — это величина, которую подбирает масштабирование по температуре, и величина, по
которой вычисляется каждая цифра калибровки в репозитории, и именно это делает её той, по которой
следует применять gating. Она не откалибрована в поставляемом виде: свойство, которое ей обычно
приписывают, — что из ответов, возвращённых с уверенностью c, примерно c верны, — выполняется
только после того, как температуры подобраны и провалидированы на отложенных данных для вашего
чекпойнта с вашим числом вариантов. Поставляемые чекпойнты переуверены, и насколько — зависит от
числа вариантов, поэтому неоткалиброванный порог может отбирать ниже собственной точности модели
(#394).
# THRESHOLD is a number you measured on your own held-out data, not one the model ships.
# Fit and validate the temperatures first — the fine-tuning notebook has the loop:
# notebooks/laya_finetune_typed_decisions_2xT4_kaggle.ipynb
ans = result["answers"]["dept"]
if ans["answer_confidence"] >= THRESHOLD:
...
confidence — это нормированная энтропия: высока, когда распределение пикообразно, низка, когда оно
размазано, независимо от того, верен ли верхний ответ. Это полезный сигнал, и он не в том же
масштабе, поэтому их нельзя сравнивать с одним и тем же числом:
# the same three answers, and the two numbers are not the same
dept confidence 0.9339 answer_confidence 0.9881
urgent confidence 0.8727 answer_confidence 0.8727
severity confidence 0.5187 answer_confidence 0.7881
Для noul они равны по построению — по двум вариантам max(p, 1-p) это max(p) — поэтому ответ
noul не может сказать вам, какое из двух чисел вы читали. Оба ключа присутствуют у каждого типа,
чтобы выбор был явным, а не подразумеваемым.
Порог — это политика, а не свойство модели. Оба чекпойнта поставляются переуверенными, и
насколько — зависит от числа вариантов, поэтому число, измеренное на вопросе с 3 вариантами, не
переносится на вопрос с 20. Измеряйте его на своих данных; раздел Calibration в
BENCHMARKS.md содержит цикл подбора
и подобранные значения.
action и act_probability
action.act_probability — это оценка отдельной головы для «должен ли агент вообще действовать на
основании этого», отличная от собственной уверенности ответа. Она сообщается для каждого типа
вопроса. Ничто в библиотеке не применяет к ней порог за вас.
Пресеты
Три готовых набора вопросов, чтобы распространённые случаи не требовали критериев, написанных от руки:
from laya import triage_questions, guard_questions, moderation_questions
agent.system_one(ticket, triage_questions())
Используйте их как отправную точку, а не как контракт, — прочитайте вопросы, которые они порождают,
через render_options, и проверьте, что метки подходят вашему домену, прежде чем выпускать их.
Чтение вариантов обратно
Поскольку порядок вариантов позиционный, а текст вариантов — это то, что читает модель, стоит иметь возможность увидеть точно, что было отправлено:
from laya import render_options
render_options({"t": "choice", "crit": {"billing": None, "sales": "pricing"}})
# ['billing', 'sales: pricing']
render_options({"t": "score", "crit": ["low", "high"]})
# ['level 0: low', 'level 1: high']
Одни и те же метки в разном порядке отрисовываются в этом порядке, и поэтому порядок — часть идентичности вопроса:
render_options({"t": "choice", "crit": {"x": "first", "y": "second"}})
# ['x: first', 'y: second']
render_options({"t": "choice", "crit": {"y": "second", "x": "first"}})
# ['y: second', 'x: first']
Обратите внимание на ключи. render_options принимает внутреннюю форму коротких ключей
{"t": ..., "crit": ...}, а не форму {"type": ..., "criteria": ...}, которую вы пишете в вопросе, —
передача публичной формы вызывает KeyError: 't'.
Преобразование — это короткое стабильное отображение, которое можно встроить, что избавляет от
обращения к приватному помощнику — Agent._to_internal внутренний и может измениться:
def as_internal(q):
"""The short-key shape `render_options` reads, from a question as you wrote it."""
crit = q.get("criteria")
if q["type"] == "choice" and isinstance(crit, list):
crit = {c: None for c in crit}
return {"t": q["type"], "ins": q["instructions"], "crit": crit}
render_options(as_internal(question))
Это повторяет то, что библиотека делает для choice-вопроса, написанного как список меток; словарь
criteria и список score проходят без изменений.
Ограничения, которые стоит знать, прежде чем проектировать с учётом этого
- Уверенность не гарантирует правильность при большом числе вариантов. На вопросе с 20 вариантами распределения для правильных и неправильных ответов сильно перекрываются, и порог может в итоге отбирать ниже собственной точности модели. См. #394.
- Отрицание не обрабатывается надёжно в вопросах с принудительным выбором. Вопрос об отмене может вернуть метку отмены для состояния, которое говорит не отменять, с высокой уверенностью, на обоих чекпойнтах. См. #377.
noulможет следовать своим меткам, а не состоянию, поэтому проверяйте любое переименование.- Больше вариантов — не бесплатно. Примерно после 20 модель быстро деградирует; используйте помощник шортлиста, чтобы сократить большое пространство меток перед вопросом, или разделите его на грубый и точный вопрос.