Skip to content

Koziev/math

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Шаблонизатор для генерации синтетических диалогов

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

Я использую этот движок для генерации школьных задач по математике с разбором ответов, на которых учатся модели диалоговой системы. Похожий механизм генерации датасета описан в разделе 3.2.1 Data Augmentation в статье MRKL Systems A modular, neuro-symbolic architecture that combines large language models, external knowledge sources and discrete reasoning.

Шаблонизатор позволяет использовать обычные выражения на Питоне для вычисления фрагментов, заполняющих слоты, а также реализовывать базовую логику ветвления, чтобы формировать последовательности реплик, учитывающие результаты вычислений. Например, вместо выдачи "У медведя осталось 0 шишек" с помощью оператора ветвления можно сформировать более типичное для русской речи высказывание "У медведя не осталось шишек".

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

К примеру, шаблон template_math_3.json раскрывается в такие диалоги.

Сэмпл 1:

У Ольги было 4 ягоды. Сколько их у нее осталось после того, как она отдала 4 ягоды?
4-4=0. Ни одной ягоды у Ольги не осталось.

Сэмпл 2:

У Вали сначала было 8 морковок, потом она 3 штуки отдала. Сколько морковок у нее осталось?
8-3=5. У Вали осталось 5 морковок.

Сэмпл 3:

У Полины сначала было 6 морковок, потом она 4 штуки отдала. Сколько морковок у нее осталось?
6-4=2, значит морковок у нее осталось 2 штуки.

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

- Пусть заданы два списка чисел. Первый список: 52, 154, 256. Второй список: 553, 372, 980, 697, 253. Сколько всего чисел в этих двух списках?
- В них 8 чисел
- Сколько чисел во втором списке?
- В нем 5 чисел
- А в первом?
- В нем их 3
- Объедини эти два списка и напиши числа в порядке возрастания
- 52, 154, 253, 256, 372, 553, 697, 980
- Сколько в нем чисел меньше 100?
- Там одно такое число: 52
- Прибавь 3 к каждому числу в объединенном списке. Как теперь он выглядит?
- 55, 157, 256, 259, 375, 556, 700, 983

Использование

Формат запуска в консоли:

python generate_math_tasks.py --dir каталог_с_шаблонами  --resource_dir каталог_со_словарями  --output_dir каталог_для_записи_результатов  --num_generations кол-во_генераций_из_одного_шаблона

По умолчанию шаблоны берутся из "../data", а результаты работы записываются в "../output".

В каталог, заданный опцией --output, также пишется файл с логом math_tasks.log.

Датасет с математическими и прочими задачами

Вы можете использовать датасет inkoziev/arithmetic, сформированный из нескольких источников, включая сгенерированные данным шаблонизатором сэмплы.

Baseline для решения задач генеративными моделями

Код test_arith_accuracy.py загружает датасет inkoziev/arithmetic, файнтюнит генеративную модель с помощью класса trainsformers.Trainer, выполняет инференс на тестовых сэмплах и вычисляет метрики точности решения.

Синтаксис шаблонов

В подкаталоге data есть примеры шаблонов. Файлы шаблонов должны в названии иметь префикс "template". Каждый такой файл в формате JSON содержит один шаблон. Шаблоны не зависят друг от друга, поэтому можно эффективно распараллелить работу по созданию набора шаблонов между множеством писателей.

Разберем самый простой пример шаблона:

{
 "variables":
 {
  "x1": "random.randint(5, 10)",
  "z": "3*x1"
 },
 
 "dialogue":
 [
  "Сколько голов у {x1} трехголовых Змеев Горынычей?",
  "У них {x1}*3={z} {numcor(z, 'голова', 'Nom')}"
 ]
}

Он генерирует сэмплы примерно такого вида:

- Сколько голов у 6 трехголовых Змеев Горынычей?
- У них 6*3=18 голов

Этот шаблон состоит из двух секций: "variables" для объявления подставляемых переменных и "dialogue" с перечнем шаблонов строк.

variables

Каждая переменная в сексии "variables" задается именем и вычисляемым значением. Значение заключается в двойные кавычки, но при генерации эта строка интерпретируется как обычное выражение на Питоне. Можно увидеть, что там вызывается стандартная питоновская функция random.randint() для получения равномерно распределенного дискретного целого случайного значения. Любые другие значения, соблюдающие синтаксис Питона, также допустимы, поэтому 3*x1 после подстановки x1 станет даст другое целое значение.

dialogue

В секции "dialogue" задается в общем случае перечень инструкций, интерпретация которых создает строки диалога. В рассматриваемом примере список включает просто две строки со слотами в фигурных скобочках. Значения внутри фигурных скобок вычисляются также по правилам Питона, после чего подставляются в строку.

Выражение {numcor(z, 'голова', 'Nom')} просто вызывает реализованную к коде движка функцию numcor, которая принимает 3 аргумента: число, строку с существительным или словосочетанием, и название требуемого падежа. Результат работы этой функции - слово в правильном числе и падеже, согласованное с числительным.

constraints

Так как значения переменных в общем случае генерируются случайно, не всегда можно априори подобрать границы случайных распределений так, чтобы гарантировать, например, неотрицательность количества после подстановки в формулу. Чтобы не создавать такие "нефизичные" сэмплы, можно наложить дополнительные ограничения на значения переменных. Если ограничения нарушены - сэмпл не попадает в выдачу. Выглядит это примерно так:

 "variables":
 {
  "sbj": "random.choice(fnames)",
  "x1": "random.randint(3, 10)",
  "x2": "random.randint(3, 10)",
  "z": "x1-x2",
  "obj": "'⦃тетрадка|закладка|ягода|груша|морковка|расческа⦄'",  "###": "перечислены предметы женского рода",
  "v": "'⦃отдала|потеряла⦄'"
 },
 
 "constraints":
 [
  "z >= 0"
 ],
 ...

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

Условная генерация сообщений

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

- У Ирины было 6 тетрадок. Сколько их у нее осталось после того, как она отдала 4 тетрадки?
- 6-4=2, значит тетрадок у нее осталось 2 штуки.

А для нулевого остатка задать другую текстовку:

- У Елены сначала было 10 ягод, потом она 10 штук отдала. Сколько ягод у нее осталось?
- 10-10=0. Ни одной ягоды у Елены не осталось.

Это делается с помощью оператора условного исполнения и перехода по меткам:

"dialogue":
{
 ...
 "!if z == 0 goto OnZero",
 "тут формируется реплика для положительного остатка",
 "!goto EXIT",
 "!:OnZero",
 "тут формируется реплика для нулевого остатка"
} 

Строки, начинающиеся с "!", обрабатываются движком особым образом - это операторы. Оператор !if условие goto метка проверяет питоновское выражение, и если оно True, переходит на указанную метку. Если условие не выполнено, интерпретация идет на следующую строку.

Метки записываются как операторы :имя_метки.

Файлы ресурсов

JSON-файлы с именами вида resource_*.json содержат полезные списки слов, например женские и мужские имена в файле resource_common.json. Эти списки внутри шаблонов доступны просто по своим именам как обычные питоновские переменные. К примеру, такой фрагмент:

"variables":
 {
  "sbj": "random.choice(fnames)",
  ...

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

Вы можете создавать свои дополнительные файлы ресурсов, размещая рядом с шаблонами (по умолчанию). Все файлы с именами на "resource_*.json" загружаются движком автоматически при старте.

Символьный препроцессинг

Иногда в тексте релики не очень удобно варьировать текстовку с помощью подставляемых переменных. Например, хочется прямо по месту задать выбор из нескольких синонимов. Это делается с помощью препроцессрной конструкции с фигурными скобками и . Внутри этих скобок задается список подстрок, разделенных вертикальной чертой |. Перед тем, как подставлять переменные в слоты, движок сначала обработает такие директивы, оставив одну из заданных подстрок:

"Яна взяла ⦃тетрадку|закладку|пластмассовую расчёску⦄"

Второй директивой препроцессинга является констуркция 〚подстрока〛. Она с равной вероятностью заменяется либо на заданную подстроку, либо на пустое место.

Директивы можно комбинировать рекурсивно, без ограничений на глубину.

Препроцессор вынесен в отдельный модуль.