Простой Классификатор Намерений

В последний раз Я научился использовать нейросети, чтобы понять, чего именно пользователь хочет добиться от бота.

То, что я тогда получил, имело ряд недостатков.

Во-первых, я ограничился только одним типом фраз.

Во-вторых, я использовал тяжелый NMT, чтобы извлечь намерение из фразы.

Более того, такую задачу обычно решают обычные классификаторы.



Более удобное создание обучающих данных

Последний раз генерировать фразы, которые я написал что-нибудь в питоне.

Даже для одной фразы это было слишком необоснованное решение.

Теперь требовалось больше разнообразия, поэтому писать на чистом Python было уже неинтересно.

Особенно когда есть более удобный инструмент — RiveScript. В RiveScript я сделал шаблоны для разных фраз, а на вход подается только намерение и, возможно, какие-то параметры, а фраза полностью генерируется в RiveScript. код 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, tags = [cls], [], [] for tag, value, just_word in tag_var_re.findall(result): if just_word: en.append(just_word) tags.append('O') else: _, tag = tag.split('-', maxsplit=1) words = value.split() en.append(words.pop(0)) tags.append('B-'+tag) for word in words: en.append(word) tags.append('I-'+tag) cmd.append(tag+':') cmd.append('"'+value+'"') return cmd, en, tags

Применение

rs = RiveScript(utf8=True) rs.load_directory(os.path.join(this_dir, 'human_train_1')) rs.sort_replies() for c in ('yes', 'no', 'ping'): for _ in range(COUNT): add_sample(make_sample(rs, c)) to_remind = ['wash hands', 'read books', 'make tea', 'pay bills', 'eat food', 'buy stuff', 'take a walk', 'do maki-uchi', 'say hello', 'say yes', 'say no', 'play games'] for _ in range(COUNT): r = random.choice(to_remind) add_sample(make_sample(rs, 'remind', r))

РиверСкрипт

+ hello - hello - hey - hi + ping - {@hello}{random}|, sweetie{/random} - {@hello} there - {random}are |{/random}you {random}here|there{/random}? - ping - yo + yes - yes - yep - yeah + no - no - not yet - nope



+ remind * @ maybe-please remind-without-please data-remind-action(<star>) + remind-without-please * - remind me to <star> - remind me data-remind-when({@when}) to <star> - remind me to <star> data-remind-when({@when}) + when - today - later - tomorrow + maybe-please * - <@> {weight=3} - please, <@> - <@>, please

Результат таких трюков примерно такой: Исходная строка для генерации:

remind do maki-uchi

Получено из RiveScript:

please, remind me data-remind-when(tomorrow) to data-remind-action(do maki-uchi)

Строка «по-английски»:

please, remind me tomorrow to do maki-uchi

Строка «в боте»:

remind when: "tomorrow" what: "do maki-uchi"

Связанные теги:

O O O B-when O B-action I-action

Хотя теги не нужны для классификации, они потребуются позже для тегера.



Сам классификатор

Моей главной проблемой в прошлый раз было полное незнание терминологии.

Теперь я уже знаю некоторые ключевые слова, поэтому просто набрал в поисковике «классифицировать тензорный поток предложений» и получил кучу более-менее полезных материалов.

Однако даже этого не потребовалось, потому что я уже накопил закладка , что меня почти полностью устроило.

Особенно мне понравилось то, что отдельный словарь не нужен, потому что предложенная там модель может строить встраивания слов прямо из тестового набора.

вложения слов Честно говоря, я довольно долго не понимал, что такое эмбеддинги слов.

По сути, это просто своего рода словарь, в котором каждому слову соответствует вектор чисел с плавающей запятой, причем для «близких» слов эти векторы будут близкими.

Что бы это ни значило.

Сеть, показанная в примере, требует только одного — чтобы вместо слов на вход подавался список целых чисел.

Конечно, я мог бы составить список всех доступных слов и заменить каждое слово его номером в этом списке.

Но это было бы не очень интересно.

Более того, в примере предложено использовать функцию one_hot, которая является частью keras.preprocessing.text. код сам классификатор

def _embed(sentence): return one_hot(sentence, HASH_SIZE) def _make_classifier(input_length, vocab_size, class_count): result = Sequential() result.add(Embedding(vocab_size, 8, input_length=input_length)) result.add(Flatten()) result.add(Dense(class_count, activation='sigmoid')) result.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc']) return result def _train(model, prep_func, train, validation=None, epochs=10, verbose=2): X, y = prep_func(*train) validation_data = None if validation is None else prep_func(*validation) model.fit(X, y, epochs=epochs, verbose=verbose, shuffle=False, validation_data=validation_data) class Translator: def __init__(self, class_count=None, cls=None, lb=None): if class_count is None and lb is None and cls is None: raise Exception("Class count is not known") self.max_length = 32 self.lb = lb or LabelBinarizer() if class_count is None and lb is not None: class_count = len(lb.classes_) self.classifier = cls or _make_classifier(self.max_length, HASH_SIZE, class_count) def _prepare_classifier_data(self, lines, labels): X = pad_sequences([_embed(line) for line in lines], padding='post', maxlen=self.max_length) y = self.lb.transform(labels) return X, y def train_classifier(self, lines, labels, validation=None): _train(self.classifier, self._prepare_classifier_data, (lines, labels), validation) def classifier_eval(self, lines, labels): X = pad_sequences([_embed(line) for line in lines], padding='post', maxlen=self.max_length) y = self.lb.transform(labels) loss, accuracy = self.classifier.evaluate(X, y) print(loss, accuracy*100) def classify(self, line): res = self._classifier_predict(line) if max(res[0]) > 0.1: return self.lb.inverse_transform(res)[0] else: return 'unknown' def classify2(self, line): res = self._classifier_predict(line) print('\n'.

join(map(str, zip(self.lb.classes_, res[0])))) m = max(res[0]) c = self.lb.inverse_transform(res)[0] if m > 0.05: return c elif m > 0.02: return 'probably ' + c else: return 'unknown ' + c + '? ' + str(m)

образование

def load_sentences(file_name): with open(file_name) as fen: return [l.strip() for l in fen.readlines()] def load_labels(file_name): with open(file_name) as fpa: return [line.strip().

split(maxsplit=1)[0] for line in fpa]



sentences = load_sentences(os.path.join(data_dir, "train.en")) labels = load_labels(os.path.join(data_dir, "train.pa")) tags = load_sentences(os.path.join(data_dir, "train.tg")) label_count = len(set(labels)) translator = Translator(label_count) translator.lb.fit(labels) translator.train_classifier(sentences, labels)

Применение

classifier = model_from_json(os.path.join(data_dir, "trained.cls")) with open(os.path.join(data_dir, "trained.lb"), 'rb') as labels_file: lb = pickle.load(labels_file) translator = Translator(lb=lb, cls=classifier, tagger=tagger) line = ' '.

join(sys.argv) print(translator.classify2(line))

Я составил первые 4 класса фраз (да, нет, пинг и напоминание), реализовал сохранение и загрузку и решил попробовать.

К моему удивлению, классификатор даже не смог правильно перевести фразы из обучающего набора.

Затем я добавил оценку тестового набора в сценарий обучения.

Эта оценка показала точность 98-99%.

Затем я скопировал скрипт перевода, но вместо анализа аргументной фразы отправил его еще раз и запустил перекрестную проверку.

И я получил результат 25%.

Точно так же, как если бы нейронная сеть случайно выбрала один из четырех классов.

Под подозрение попала функция one_hot. Меня смутило то, что для кодирования слова нужно знать только размер словаря, но не его содержимое.

Ээксперименты показали, что one_hot будет давать одни и те же результаты при одном запуске скрипта, но разные результаты при разных запусках.

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

Как оказалось, не зря.

one_hot One-hot кодирует текст в список индексов слов в словаре размера n. Это оболочка для

hashing_trick

function using

hash

как хеш-функция.

Здесь, казалось бы, ничего не намекает.
hashing_trick Преобразует текст в последовательность индексов в хеш-пространстве фиксированного размера.

Вроде тоже ничего.

Но если вы посмотрите ниже на список аргументов.

хэш_функция : по умолчанию используется Python

hash

function, can be 'md5' or any function that takes in input a string and returns a int. Note that 'hash' is not a stable hashing function, so it is not consistent across different runs, while 'md5' is a stable hashing function.
Я изменил one_hot на hashing_trick с помощью md5, но результат не изменился, я все равно получил те же 25% правильных ответов.

Использование one_hot, несомненно, было ошибкой, но не единственной.

Следующим подозреваемым стала функция сохранения и загрузки обученной нейросети.

Как оказалось, model.to_json и model_from_json работают только с сетевой моделью, но не сохраняют и не загружают веса.

А для экономии веса пришлось установить пакет h5py. Исправив эту досадную ошибку, я наконец-то получил результаты, похожие на правду:

$ .

/translate4.py 'please, remind me to make some tea' probably remind

После этого я составил еще несколько классов фраз, доведя их общее количество до 10. При разных вариантах всего их было 13 — два варианта напоминания (одно действие или два) и три варианта поиска (поиск по одной ключевой фразе, или два с И и с ИЛИ).



Результат

Я придумал простой классификатор, который быстро обучается (за несколько секунд) и дает хорошие результаты.

Гораздо лучше, чем использовать для этого nmt. Следующим шагом должен стать теггер.

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

В какой-то момент возни с таггером я чуть не сдался.

Но потом я поймал мой взгляд статья об Алисе .

Буквально накануне я решил отвлечься от анализа текста в сторону того, как должен работать «мозг».

То, что мне удалось придумать, оказалось первым шагом к тому, как это делается в Алисе.

Плюс опять же речь идет о семантическом анализе фраз.

И им это удалось.

Так что мы можем надеяться, что я тоже смогу это сделать.

Я просто сразу пойму, как использовать двунаправленные LSTM вместо обычных, а также что такое современное состояние.

Весь код для моих экспериментов доступен в гитхабе .

Теги: #машинное обучение #NLP #классификация #Аномальное программирование #Машинное обучение

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.