単純なインテント分類子

前回、ニューラルネットワークを使用して、ユーザーがボットから何を達成したいのかを正確に理解することを学びました。 その後、私がやったことにはいくつかの欠点がありました。 第一に、私は自分自身を1種類のフレーズに限定しました。 次に、ヘビーウェイトnmtを使用して、フレーズから意図を奪います。 さらに、このような問題は通常、従来の分類器によって解決されます。



より便利なトレーニングデータ生成



前回、フレーズを生成するためにpythonで何かを書いた。 1種類のフレーズでも、サポートされていませんでした。 より多くの種類が必要になったため、純粋なpythonでの記述はすでに面白くありませんでした。 特に、より便利なツールがある場合-RiveScript。



RiveScriptでは、さまざまなフレーズのテンプレートを作成しましたが、意図だけで、場合によっては一部のパラメーターが入力され、RiveScriptではフレーズが完全に生成されます。



コード
make_sample
tag_var_re = re.compile(r'data-([az-]+)\((.*?)\)|(\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))
      
      



IveScript
 + 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





「in bot」という文字列remind when: "tomorrow" what: "do maki-uchi"





関連タグ: OOO B-when O B-action I-action







タグは分類には必要ありませんが、タグ付け者には後で必要になります。



分類子自体



前回の主な問題は、用語の知識がまったくなかったことです。 今、私はすでにいくつかのキーワードを知っているので、「センテンステンソルフローを分類する」検索エンジンに入り、多かれ少なかれ使用に適した材料の束を得ました。 ただし、これにはほとんど必要ありませんでした。ほぼ完全に自分に合ったブックマークがすでにあったからです。 そこに提案されているモデルはテストスイートから直接単語の埋め込みを構築できるため、個別の辞書が必要ないという事実が特に気に入りました。



単語の埋め込み
正直なところ、私は長い間、単語の埋め込みが何であるかを理解していませんでした。 実際、これは、各単語がフロートのベクトルに対応する一種の辞書であり、「近い」単語の場合、これらのベクトルは近くなります。 それが何を意味するにせよ。


この例に示されているネットワークでは、必要なものは1つだけです。したがって、単語の代わりに整数のリストが入力されます。 もちろん、使用可能なすべての単語のリストを作成し、このリスト内の各単語をその番号に置き換えることもできます。 しかし、それはあまり面白くないでしょう。 さらに、この例では、keras.preprocessing.textの一部であるone_hot関数を使用することが提案されました。



コード
分類子自体
 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つのクラスのフレーズ(yes、no、ping、remind)を作成し、保存と読み込みを実装し、試してみることにしました。 驚いたことに、分類器はトレーニングセットの偶数フレーズを誤って翻訳しました。 次に、テストスクリプトの評価をトレーニングスクリプトに追加しました。 この評価は、98〜99%の精度を示しました。 次に、翻訳スクリプトをコピーしましたが、フレーズ引数を分析する代わりに、再度渡してクロス検証を実行しました。 そして、25%の結果を得ました。 ニューラルネットワークがランダムに4つのクラスのいずれかを選択した場合など。



one_hot関数は疑われました。 単語をエンコードするために必要なのは辞書のサイズだけで、内容はわからないということです。 実験では、one_hotは同じスクリプト実行内で同じ結果を生成しますが、起動ごとに異なることが示されました。 他の何かを使用しようとして失敗した後、ドキュメントを注意深く読むことにしました。



結局のところ、無駄ではありませんでした。

one_hot

ワンホットは、テキストをサイズnの語彙の単語インデックスのリストにエンコードします。

これは、ハッシュ関数としてhash



を使用するhashing_trick



関数のラッパーです。
ここでは、何もヒントがないように思えます。
hashing_trick

固定サイズのハッシュ空間でテキストを一連のインデックスに変換します
それも何もないようです。 しかし、まだ以下の引数のリストを見ると...
hash_function :デフォルトはpython hash



関数で、「md5」または文字列を入力してintを返す任意の関数を指定できます。 「ハッシュ」は安定したハッシュ関数ではないため、「md5」は安定したハッシュ関数であるのに対して、異なる実行間で一貫性がないことに注意してください。
md5でone_hotをhashing_trickに変更しましたが、結果は変わらず、25%の正解をすべて受け取りました。 one_hotの使用は間違いなく間違いでしたが、それだけではありませんでした。



次の容疑者は、訓練されたニューラルネットワークを保存およびロードする機能でした。 判明したように、model.to_jsonとmodel_from_jsonはネットワークモデルでのみ機能しますが、ウェイトの保存や読み込みは行いません。 そして、スケールを保存するには、h5pyパッケージをインストールする必要がありました。 この厄介なエラーを修正した後、私は最終的に真実に似た結果を得ました:

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

probably remind








その後、さらにいくつかのクラスのフレーズを作成して、合計数を10にしました。さまざまなバリエーションで、13のリマインドオプション(1アクションまたは2)と3オプションの検索(1つのキーフレーズ、またはANDおよびOR)。



結果



すぐに(数秒で)学習し、良い結果を出す簡単な分類器を入手しました。 これにnmtを使用するよりも大幅に優れています。 次のステップはタガーです。 既製のシーケンスタギングを再度使用できますが、実際にはマルチギガバイトのGloVeを保持したくありません。 したがって、私はこの実験を続けて、自分に合ったタガーを作ろうとします。 これまでのところ、失敗しました。



ある時点で、タガーに夢中になって、私はほとんどあきらめました。 しかし、その後、アリスに関する記事に出会いまし 。 前日、私は「脳」がどのように機能するかという方向でテキストを分析することから気をそらすことにした。 私が思いついたのは、これがアリスでどのように行われたかに向けた最初のステップでした。 さらに、再び、フレーズのセマンティック分析について話します。 そして彼らはそれをやった。 それで、あなたも私ができることを願うことができます。 私は、通常のLSTMの代わりに双方向のLSTMを使用する方法を理解しているだけでなく、最先端の技術もすぐに理解できます。



私の実験のすべてのコードはgithubで入手できます。



All Articles