EasyNLU は、モバイル アプリ用に Java で書かれた自然言語理解 (NLU) ライブラリです。文法ベースであるため、狭いが厳密な制御が必要な領域に適しています。
このプロジェクトには、自然言語入力からリマインダーをスケジュールできるサンプル Android アプリケーションが含まれています。
EasyNLU は Apache 2.0 に基づいてライセンスされています。
EasyNLU の核となるのは、CCG ベースのセマンティック パーサーです。セマンティック解析の優れた入門書はここにあります。セマンティック パーサーは次のように入力を解析できます。
明日の午後5時に歯医者に行ってください
構造化された形式に変換します。
{task: "Go to the dentist", startTime:{offset:{day:1}, hour:5, shift:"pm"}}
EasyNLU は、再帰的な Java マップとして構造化された形式を提供します。この構造化フォームは、「実行」されるタスク固有のオブジェクトに解決できます。たとえば、サンプル プロジェクトでは、構造化フォームは、 task
、 startTime
、 repeat
などのフィールドを持つReminder
オブジェクトに解決され、AlarmManager サービスでアラームを設定するために使用されます。
一般に、NLU 機能を設定するための高レベルの手順は次のとおりです。
ルールを記述する前に、タスクの範囲と構造化フォームのパラメーターを定義する必要があります。おもちゃの例として、Bluetooh、Wifi、GPS などの電話機能をオンまたはオフにするタスクがあるとします。したがって、フィールドは次のとおりです。
構造化フォームの例は次のとおりです。
{feature: "bluetooth", action: "enable" }
また、バリエーションを理解するために、いくつかのサンプル入力があると役立ちます。
おもちゃの例を続けると、最上位には機能とアクションが必要な設定アクションがあると言えます。次に、ルールを使用してこの情報を取得します。
Rule r1 = new Rule ( "$Setting" , "$Feature $Action" );
Rule r2 = new Rule ( "$Setting" , "$Action $Feature" );
ルールには少なくとも LHS と RHS が含まれます。慣例により、カテゴリを示すために単語の前に「$」を付けます。カテゴリは、単語またはその他のカテゴリのコレクションを表します。上記のルールでは、 $Feature
「語彙」ルールを使用して取得した Bluetooth、BT、WiFi などの単語を表します。
List < Rule > lexicals = Arrays . asList (
new Rule ( "$Feature" , "bluetooth" ),
new Rule ( "$Feature" , "bt" ),
new Rule ( "$Feature" , "wifi" ),
new Rule ( "$Feature" , "gps" ),
new Rule ( "$Feature" , "location" ),
);
機能名のバリエーションを正規化するために、 $Features
サブ機能に構造化します。
List < Rule > featureRules = Arrays . asList (
new Rule ( "$Feature" , "$Bluetooth" ),
new Rule ( "$Feature" , "$Wifi" ),
new Rule ( "$Feature" , "$Gps" ),
new Rule ( "$Bluetooth" , "bt" ),
new Rule ( "$Bluetooth" , "bluetooth" ),
new Rule ( "$Wifi" , "wifi" ),
new Rule ( "$Gps" , "gps" ),
new Rule ( "$Gps" , "location" )
);
$Action
についても同様です:
List < Rule > actionRules = Arrays . asList (
new Rule ( "$Action" , "$EnableDisable" ),
new Rule ( "$EnableDisable" , "?$Switch $OnOff" ),
new Rule ( "$EnableDisable" , "$Enable" ),
new Rule ( "$EnableDisable" , "$Disable" ),
new Rule ( "$OnOff" , "on" ),
new Rule ( "$OnOff" , "off" ),
new Rule ( "$Switch" , "switch" ),
new Rule ( "$Switch" , "turn" ),
new Rule ( "$Enable" , "enable" ),
new Rule ( "$Disable" , "disable" ),
new Rule ( "$Disable" , "kill" )
);
「?」に注意してください。 3 番目のルール。これは、カテゴリ$Switch
がオプションであることを意味します。解析が成功したかどうかを判断するために、パーサーはルート カテゴリと呼ばれる特別なカテゴリを探します。慣例により、これは$ROOT
として表されます。このカテゴリに到達するには、ルールを追加する必要があります。
Rule root = new Rule ( "$ROOT" , "$Setting" );
これらの一連のルールを使用すると、パーサーは上記の例を解析し、いわゆる構文ツリーに変換できるはずです。
文の意味を抽出できなければ解析は役に立ちません。この意味は、構造化された形式 (NLP 用語では論理形式) によって捉えられます。 EasyNLU では、ルール定義の 3 番目のパラメーターを渡して、セマンティクスの抽出方法を定義します。これを行うには、特別なマーカーを備えた JSON 構文を使用します。
new Rule ( "$Action" , "$EnableDisable" , "{action:@first}" ),
@first
ルール RHS の最初のカテゴリの値を選択するようにパーサーに指示します。この場合、文に基づいて「有効」または「無効」になります。他のマーカーには次のものがあります。
@identity
: 恒等関数@last
: 最後の RHS カテゴリの値を選択します@N
: N番目のRHS カテゴリの値を選択します。たとえば、 @3
3 番目を選択します。@merge
: すべてのカテゴリの値をマージします。名前付きの値 (例{action: enable}
) のみがマージされます。@append
: すべてのカテゴリの値をリストに追加します。結果のリストには名前を付ける必要があります。名前付きの値のみが許可されますセマンティック マーカーを追加すると、ルールは次のようになります。
List < Rule > rules = Arrays . asList (
new Rule ( "$ROOT" , "$Setting" , "@identity" ),
new Rule ( "$Setting" , "$Feature $Action" , "@merge" ),
new Rule ( "$Setting" , "$Action $Feature" , "@merge" ),
new Rule ( "$Feature" , "$Bluetooth" , "{feature: bluetooth}" ),
new Rule ( "$Feature" , "$Wifi" , "{feature: wifi}" ),
new Rule ( "$Feature" , "$Gps" , "{feature: gps}" ),
new Rule ( "$Bluetooth" , "bt" ),
new Rule ( "$Bluetooth" , "bluetooth" ),
new Rule ( "$Wifi" , "wifi" ),
new Rule ( "$Gps" , "gps" ),
new Rule ( "$Gps" , "location" ),
new Rule ( "$Action" , "$EnableDisable" , "{action: @first}" ),
new Rule ( "$EnableDisable" , "?$Switch $OnOff" , "@last" ),
new Rule ( "$EnableDisable" , "$Enable" , "enable" ),
new Rule ( "$EnableDisable" , "$Disable" , "disable" ),
new Rule ( "$OnOff" , "on" , "enable" ),
new Rule ( "$OnOff" , "off" , "disable" ),
new Rule ( "$Switch" , "switch" ),
new Rule ( "$Switch" , "turn" ),
new Rule ( "$Enable" , "enable" ),
new Rule ( "$Disable" , "disable" ),
new Rule ( "$Disable" , "kill" )
);
セマンティクス パラメータが指定されていない場合、パーサーは RHS に等しいデフォルト値を作成します。
パーサーを実行するには、このリポジトリのクローンを作成し、パーサー モジュールを Android Studio/IntelliJ プロジェクトにインポートします。 EasyNLU パーサーは、ルールを保持するGrammar
オブジェクト、入力文を単語に変換するTokenizer
オブジェクト、および数値、日付、場所などのエンティティに注釈を付けるAnnotator
オブジェクトのオプションのリストを受け取ります。ルールを定義した後、次のコードを実行します。
Grammar grammar = new Grammar ( rules , "$ROOT" );
Parser parser = new Parser ( grammar , new BasicTokenizer (), Collections . emptyList ());
System . out . println ( parser . parse ( "kill bt" ));
System . out . println ( parser . parse ( "wifi on" ));
System . out . println ( parser . parse ( "enable location" ));
System . out . println ( parser . parse ( "turn off GPS" ));
次の出力が得られるはずです。
23 rules
[{feature=bluetooth, action=disable}]
[{feature=wifi, action=enable}]
[{feature=gps, action=enable}]
[{feature=gps, action=disable}]
他のバリエーションも試してみてください。サンプル バリアントの解析が失敗した場合、出力は得られません。その後、ルールを追加または変更して、文法エンジニアリング プロセスを繰り返すことができます。
EasyNLU は構造化形式のリストをサポートするようになりました。上記のドメインの場合、次のような入力を処理できます。
GPS による位置情報を無効にする
上記の文法に次の 3 つの追加ルールを追加します。
new Rule("$Setting", "$Action $FeatureGroup", "@merge"),
new Rule("$FeatureGroup", "$Feature $Feature", "{featureGroup: @append}"),
new Rule("$FeatureGroup", "$FeatureGroup $Feature", "{featureGroup: @append}"),
次のような新しいクエリを実行します。
System.out.println(parser.parse("disable location bt gps"));
次の出力が得られるはずです。
[{action=disable, featureGroup=[{feature=gps}, {feature=bluetooth}, {feature=gps}]}]
これらのルールは、クエリに複数の機能がある場合にのみトリガーされることに注意してください。
アノテーターを使用すると、ルールを介して処理するのが面倒またはまったく不可能な特定の種類のトークンを簡単に処理できるようになります。たとえば、 NumberAnnotator
クラスを考えてみましょう。すべての数値を検出し、 $NUMBER
として注釈を付けます。その後、ルール内でカテゴリを直接参照できます。例:
Rule r = new Rule("$Conversion", "$Convert $NUMBER $Unit $To $Unit", "{convertFrom: {unit: @2, quantity: @1}, convertTo: {unit: @last}}"
EasyNLU には現在、いくつかのアノテーターが付属しています。
NumberAnnotator
: 数値に注釈を付けますDateTimeAnnotator
: いくつかの日付形式に注釈を付けます。 DateTimeAnnotator.rules()
を使用して追加する独自のルールも提供します。TokenAnnotator
: 入力の各トークンに$TOKEN
として注釈を付けますPhraseAnnotator
: 入力の各連続フレーズに$PHRASE
として注釈を付けます独自のカスタム アノテーターを使用するには、 Annotator
インターフェイスを実装し、それをパーサーに渡します。実装方法については、組み込みのアノテーターを参照してください。
EasyNLU は、テキスト ファイルからのルールのロードをサポートしています。各ルールは個別の行に入力する必要があります。 LHS、RHS、およびセマンティクスはタブで区切る必要があります。
$EnableDisable ?$Switch $OnOff @last
タブをスペースに自動変換する IDE を使用しないように注意してください。
パーサーにルールがさらに追加されると、パーサーが特定の入力に対して複数の解析を検出することがわかります。これは人間の言語の一般的な曖昧さによるものです。パーサーがタスクに対してどの程度正確であるかを判断するには、ラベル付きの例でパーサーを実行する必要があります。
前述のルールと同様に、EasyNLU ではプレーン テキストで定義された例を使用します。各行は個別の例であり、生のテキストとタブで区切られた構造化された形式が含まれている必要があります。
take my medicine at 4pm {task:"take my medicine", startTime:{hour:4, shift:"pm"}}
入力の許容可能な数またはバリエーションをカバーすることが重要です。このタスクをさまざまな人に実行してもらうと、より多様性が得られます。例の数はドメインの複雑さによって異なります。このプロジェクトで提供されるサンプル データセットには 100 以上の例が含まれています。
データを取得したら、それをデータセットに追加します。学習部分はtrainer
モジュールによって処理されます。それをプロジェクトにインポートします。次のようにデータセットを読み込みます。
Dataset dataset = Dataset . fromText ( 'filename.txt' )
パーサーを評価して 2 種類の精度を決定します
評価を実行するには、 Model
クラスを使用します。
Model model = new Model(parser);
model.evaluate(dataset, 2);
評価関数の 2 番目のパラメータは冗長レベルです。数値が大きいほど、出力はより詳細になります。 evaluate()
関数は、各例でパーサーを実行し、正しくない解析を示し、最終的に精度を表示します。両方の精度が 90 台後半であれば、トレーニングは不要です。おそらく、これらのいくつかの悪い解析は後処理で処理できるでしょう。オラクルの精度は高くても予測精度が低い場合は、システムのトレーニングが役に立ちます。オラクルの精度自体が低い場合は、さらに文法エンジニアリングを行う必要があります。
正しい解析を行うために、それらにスコアを付けて、最も高いスコアを持つものを選択します。 EasyNLU は、単純な線形モデルを使用してスコアを計算します。トレーニングは、ヒンジ損失関数を備えた確率的勾配降下法 (SGD) を使用して実行されます。入力特徴は、構造化フォームのルール数とフィールドに基づいています。トレーニングされた重みはテキスト ファイルに保存されます。
モデル/トレーニング パラメーターの一部を調整して、精度を向上させることができます。リマインダー モデルには、次のパラメータが使用されました。
HParams hparams = HParams . hparams ()
. withLearnRate ( 0.08f )
. withL2Penalty ( 0.01f )
. set ( SVMOptimizer . CORRECT_PROB , 0.4f );
次のようにトレーニング コードを実行します。
Experiment experiment = new Experiment ( model , dataset , hparams , "yourdomain.weights" );
experiment . train ( 100 , false );
これにより、トレーニングとテストの分割が 0.8 で 100 エポックの間モデルがトレーニングされます。テスト セットの精度が表示され、モデルの重みが指定されたファイル パスに保存されます。データセット全体でトレーニングするには、デプロイ パラメーターを true に設定します。対話型モードを実行して、コンソールから入力を取得できます。
experiement.interactive();
注: たとえ多数の例を使用しても、トレーニングで高い精度が得られるとは保証されません。特定のシナリオでは、提供される機能が十分に識別できない場合があります。このようなケースで行き詰まった場合は、問題を記録してください。追加機能を見つけることができます。
モデルの重みはプレーン テキスト ファイルです。 Android プロジェクトの場合、プロジェクトをassets
フォルダーに配置し、AssetManager を使用してロードできます。詳細については、ReminderNlu.java を参照してください。重みとルールをクラウドに保存し、無線でモデルを更新することもできます (Firebase は誰ですか?)。
パーサーが自然言語入力を構造化形式に変換する作業を適切に実行したら、そのデータをタスク固有のオブジェクトに含めることが必要になるでしょう。上記のおもちゃの例のような一部のドメインでは、非常に簡単な場合があります。その他の場合は、日付、場所、連絡先などへの参照を解決する必要がある場合があります。リマインダーのサンプルでは、日付は多くの場合相対的なもの (「明日」、「2 時間後」など) なので、絶対値に変換する必要があります。解決方法については、ArgumentResolver.java を参照してください。
ヒント: ルールを定義するときは解決ロジックを最小限に抑え、そのほとんどは後処理で行います。そうすることでルールがよりシンプルになります。