Главная » Хабрахабр » Получение параметров команды из человеческой фразы

Получение параметров команды из человеческой фразы

Хотя мне и удалось разобраться с классификацией интента, осталась более сложная задача — выцепить из фразы дополнительные параметры. Я знаю, что это делается с помощью тегов. Один раз я уже успешно применил sequence_tagging, но я не очень рад тому, что нужно держать словарь векторных представлений слов размером больше 6 гигабайт.

Попытка нулевая

Я нашел пример реализации теггера на Keras и, в лучших традициях своих экспериментов, начал бездумно копировать оттуда куски кода. В примере нейросеть обрабатывает входную строку как последовательность символов, не разделяя ее на слова. Но дальше по тексту есть пример с использованием Embedding слоя. А раз я научился использовать hashing_trick, то я почувствовал острое желание воспользоваться этим навыком.

То, что у меня получилось, обучалось значительно медленнее, чем классификатор. Я включил в Keras отладочный вывод, и, задумчиво глядя на медленно появляющиеся строчки, обратил внимание на значение Loss. Оно не особенно убывало, при этом мне оно показалось достаточно большим. А accuracy при этом была маленькой. Сидеть и ждать результата мне было лень, поэтому я вспомнил одну из рекомендаций Andrew Ng — попробовать свою нейросеть на меньшем по размеру набору учебных данных. По виду зависимости Loss от количества примеров можно оценить, стоит ли ожидать хороших результатов.

Поэтому я остановил обучение, сгенерил новый набор учебных данных — в 10 раз меньше предыдущего — и снова запустил обучение. И почти сразу получил тот же самый Loss и тот же самый Accuracy. Получается, что от увеличения числа учебных примеров лучше не станет.

Я все-таки дождался окончания обучения (около часа, при том, что классификатор обучался за несколько секунд) и решил ее опробовать. И понял, что копировать надо было больше, потому что в случае seq2seq для обучения и для реальной работы нужны разные модели. Еще немного поковырялся с кодом и решил остановиться и подумать, что делать дальше.

Передо мной был выбор — снова взять готовый пример, но уже без самодеятельности, либо же взять готовый seq2seq, или же вернуться к инструменту, который у меня уже работал — sequence tagger на NERModel. Правда чтобы без GloVe.

Я решил попробовать все три в обратном порядке.

NER model из sequence tagging

Желание править существующий код улетучилось сразу же, как только я заглянул внутрь. Поэтому я пошел с другой стороны — надергать из sequence tagging разных классов и методов, взять gensim.models.Word2Vec и это все туда скормить. После часа попыток я смог сделать учебные наборы данных, но вот именно словарь мне подменить не удалось. Я посмотрел на ошибку, прилетевшую откуда-то из глубин numpy, и отказался от этой затеи.

Сделал коммит, чтобы на всякий случай не потерялось.

Seq2Seq

В документации на Seq2Seq описано только как ее приготовить, но не как ей пользоваться. Пришлось найти пример и попытаться опять же подстроить под свое. Еще пара часов экспериментов и результат — точность в процессе обучения стабильно равна 0.83. Независимо от размера учебных данных. Значит я опять что-то где-то перепутал.

Здесь мне в примере не очень понравилось, что, во-первых, вручную идет разбиение учебных данных на куски, а во-вторых, вручную же делается embedding. Я в итоге прикрутил в одну Keras-модель сначала Embedding, потом Seq2Seq, а данные подготовил одним большим куском. 

получилось красиво

 model = Sequential() model.add(Embedding(256, TOKEN_REPRESENTATION_SIZE, input_length=INPUT_SEQUENCE_LENGTH)) model.add(SimpleSeq2Seq(input_dim=TOKEN_REPRESENTATION_SIZE, input_length=INPUT_SEQUENCE_LENGTH, hidden_dim=HIDDEN_LAYER_DIMENSION, output_dim=output_dim, output_length=ANSWER_MAX_TOKEN_LENGTH, depth=1)) model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

Но красота не спасла — поведение сети не изменилось.

Еще один коммит, перехожу к третьему варианту.

Seq2seq вручную

Сначала я честно все скопировал и попробовал запустить как есть. На вход подается просто последовательность символов исходной фразы, на выходе должна быть последовательность символов, которую можно посплитать по пробелам и получить список тегов. Точность вроде бы была неплохой. Потому что нейросеть быстро научилась, что если она начала по буквам выдавать какой-то тег, то дальше уже без ошибок напишет его до конца. А вот сами теги ну нисколько не соответствовали желаемому результату.

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

Тем не менее, я довел обучение сети до конца и посмотрел, что же именно она выдает. Потому что стабильный результат в 20% это наверно что-то значит. Как оказалось, сеть нашла способ особо не напрягаться:

please, remind me tomorrow to buy stuff
O

То есть делает вид, что во фразе всего одно слово, которое не содержит никаких данных (в смысле таких, которые еще не съел классификатор). Смотрим на учебные данные… действительно, порядка 20% фраз именно такие — yes, no, часть ping (то есть всякие hello) и часть acknowledge (всякие thanks).

Начинаем ставить сети палки в колеса. Урезаю количество yes/no в 4 раза, ping/acknowledge в 2 раза и добавляю еще всякого «мусора» в одно слово, но содержащее данные. На этом этапе я решил, что не надо мне в тегах иметь явную привязку к классу, поэтому например B-makiuchi-count превратилось в просто B-count. А новый «мусор» это были просто числа с классом B-count, «время» в виде «4:30» с ожидаемым тегом B-time, указания на дату типа «now», «today» и «tomorrow» с тегом B-when.

Все равно не получается. Сеть уже не выдает однозначного ответа «O и все», но при этом accuracy так и остается на уровне 18%, а ответы совершенно неадекватные.

not yet
expected ['O', 'O']
actual ['O', 'O', 'B-what'] what is the weather outside?
expected ['O', 'O', 'O', 'O', 'O']
actual ['O', 'O', 'B-what']

Пока тупик.

Интерлюдия — осмысление

Отсутствие результата — тоже результат. У меня появилось пусть и поверхностное, но понимание того, что именно происходит, когда я конструирую модели в Keras. Научился их сохранять, загружать и даже доучивать по мере необходимости. Но при этом я не добился того, чего хотел — перевода «человеческой» речи в «язык бота». Зацепок у меня больше не оставалось.

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

Расчет оправдался — я получил ссылку на Rasa NLU. На первый взгляд это выглядело как что-то очень подходящее.

Rasa NLU

Несколько дней я не возвращался к своим экспериментам. Потом сел и за час с небольшим прикрутил Rasa NLU к своим экспериментальным скриптам. Нельзя сказать, что это было очень сложно.

код

make_sample

tag_var_re = re.compile(r'data-([a-z-]+)\((.*?)\)|(\S+)') def make_sample(rs, cls, *args, **kwargs): tokens = [cls] + list(args) for k, v in kwargs.items(): tokens.append(k) tokens.append(v) result = rs.reply('', ' '.join(map(str, tokens))).strip() if result == '[ERR: No Reply Matched]': raise Exception("failed to generate string for {}".format(tokens)) cmd, en, rasa_entities = cls, [], [] for tag, value, just_word in tag_var_re.findall(result): if just_word: en.append(just_word) else: _, tag = tag.split('-', maxsplit=1) words = value.split() start = len(' '.join(en)) if en: start += 1 en.extend(words) end = len(' '.join(en)) rasa_entities.append({"start": start, "end": end, "value": value, "entity": tag}) assert ' '.join(en)[start:end] == value return cmd, en, rasa_entities

После такого сохранять учебные данные совсем нетрудно:

 rasa_examples = [] for e, p, r in zip(en, pa, rasa): sample = {"text": ' '.join(e), "intent": p} if r: sample["entities"] = r rasa_examples.append(sample) with open(os.path.join(data_dir, "rasa_train.js"), "w") as rf: json.dump({"rasa_nlu_data": {"common_examples": rasa_examples, "regex_features": [], "entity_synonims": []}}, rf)

Самое сложное в создании модели — правильный конфиг

 training_data = load_data(os.path.join(data_dir, "rasa_train.js")) config = RasaNLUConfig() config.pipeline = registry.registered_pipeline_templates["spacy_sklearn"] config.max_training_processes = 4 trainer = Trainer(config) trainer.train(training_data) model_dir = trainer.persist(os.path.join(data_dir, "rasa"))

А самое сложное в использовании — найти ее

 config = RasaNLUConfig() config.pipeline = registry.registered_pipeline_templates["spacy_sklearn"] config.max_training_processes = 4 model_dir = glob.glob(data_dir+"/rasa/default/model_*")[0] interpreter = Interpreter.load(model_dir, config)
 parsed = interpreter.parse(line) result = [parsed['intent_ranking'][0]['name']] for entity in parsed['entities']: result.append(entity['entity']+':') result.append('"'+entity['value']+'"') print(' '.join(result))

please, find me some pictures of japanese warriors
find what: "japanese warriors"
remind me to have a breakfast now, sweetie
remind action: "have a breakfast" when: "now" what: "sweetie"

… хотя еще есть над чем работать.

Из недостатоков — процесс обучения происходит совсем молча. Наверняка это где-то включается. Впрочем, все обучение заняло около трех минут. Еще для работы spacy все-таки требуется модель для исходного языка. Но она весит значительно меньше, чем GloVe — для английского языка это меньше 300 мегабайт. Правда для русского языка модели еще пока нет — а конечная цель моих экспериментов должна работать именно с русским. Надо будет посмотреть на другие pipeline, доступные в Rasa.

Весь код доступен в гитхабе.


x

Ещё Hi-Tech Интересное!

Ugears: скакуны, парусники и прочие королевские развлечения

Очередная кампания, очередная коллекция — кажется, это уже не новость, но дизайнеры умудряются каждый раз придумать что-то новенькое. Прошлой весной украинский стартап Ugears провел пятую краудфандинговую кампанию по сбору средств на коллекцию деревянных конструкторов. Для сборки нужны только детали и ...

[Перевод] Руководство по JavaScript, часть 3: переменные, типы данных, выражения, объекты

Сегодня, в третьей части перевода руководства по JavaScript, мы поговорим о разных способах объявления переменных, о типах данных, о выражениях и об особенностях работы с объектами. → Часть 1: первая программа, особенности языка, стандарты→ Часть 2: стиль кода и структура ...