Haskell
自然言語処理

正規表現を使った固有表現抽出の話

ただの集団 Advent Calendar 20185日目を担当しております、コニーと申します。今日は自然言語処理のタスクの一つ、固有表現抽出について話したいと思います。

固有表現抽出とは?

ウィキ先生によると:「固有表現抽出(こゆうひょうげんちゅうしゅつ、英: named entity recognition、named entity identification、named entity chunking、named entity extraction)とは、計算機を用いた自然言語処理技術の一つであり、情報抽出の一分野である。文中から固有表現 (Named Entity) を抽出し、それを固有名詞(人名、組織名、地名など)や日付、時間表現、数量、金額、パーセンテージなどのあらかじめ定義された固有表現分類へと分類する。」

とのことです。チャットボット、検索エンジン、文書解析などのタスクには必要不可欠の部分です。チャットボットの例だと、
「明日北海道への新幹線チケットを買いたい」という入力に対して
明日←チケットの日付
北海道←目的地
新幹線チケット←商品
買いたい←意図
人間はすぐこの入力を理解、分解できますが、機械には難しいです。

やり方は基本的に2パターンあります。

1.パターン、ルールのある表現

ある程度パターン化できる表現、例えば日付(DD/MM/YYYY)、時間(HH:MM:SS)、金額(〇〇円、¥〇〇)など決まった形式がある表現は特に抽出しやすいです。正規表現を使うと、パターンを認識し、パースして中身を取得できます。これはエンジニアリングで解決できます。

2.固有名詞、ルールがない表現

逆に、パターン化できない表現またはルールがない表現、例えば人名、組織名、地名などは、機械学習を使用する必要があります。英語だと品詞、大文字などの特徴を見て固有表現であるかどうかを予測します。やり方は色々ありますが。基本的はある程度の学習データがないと抽出できません。使う場面が違うと抽出する固有表現も違います。ドメイン知識が重要なので今回は割愛します。

今回は1だけについてお話しします。

正規表現を使った固有表現抽出

ちょうどFacebookの方がいい感じのライブラリーが作ったのでこれを使って見ましょう。Githubプロジェクトなので自分の要件に合わせた実装、改良も可能です。Haskellで書かれたライブラリで若干改造が難しいのですが…Haskellについては初心者なので無駄な部分がかなりあると思いますがそこはご了承ください。

英語の固有表現がメインで、他にも十種類以上の言語のサポートが入ってます。固有表現の種類は全12種類がありますが、日本語サポートがついてるものは数種類しかありません。やはりアジア系言語のサポートは少ないですね。
まずはインストールして見ましょう。

Haskell環境、Stackをインストール

公式サイトの指示通りOSに合わせたインストールファイルを実行すればOK。

Ducklingをインストールする

$ brew install pcre
$ git clone https://github.com/facebook/duckling.git
$ cd duckling

Ducklingサーバーを立ち上げてみる

$ cd duckling
$ stack build
$ stack exec duckling-example-exe

portはデフォルトで8000を使いますがこのPRがマージされればポートも自由に変えられます。

文書を投げてみる

READMEで英語で「明日の8時のチケットを買う」という入力を投げて見ましょう。

$ curl -XPOST http://0.0.0.0:8000/parse --data 'locale=en_GB&text=buy the ticket for tomorrow at eight'
[{"body":"tomorrow at eight","start":19,"value":{"values":[{"value":"2018-12-01T08:00:00.000-08:00","grain":"hour","type":"value"},{"value":"2018-12-01T20:00:00.000-08:00","grain":"hour","type":"value"}],"value":"2018-12-01T08:00:00.000-08:00","grain":"hour","type":"value"},"end":36,"dim":"time","latent":false}]

ちゃんと"dim":"time"認識されてますね。しかも明日の日付(1日)で返してくれてるのが偉い。「チケットを買う」の部分は固定表現として登録されてないので無視されます。これで「明日」という相対的な表現を使っても機械はちゃんと正しい日付を取得し取るべき行動を実行できますね。

レスポンスはREST形式なのでとても便利です。

日本語を投げてみる

curl -XPOST http://0.0.0.0:8000/parse --data 'lang=ja&text=北海道まで一時間かかります'

結果

[{"body":"一時間","start":5,"value":{"value":1,"hour":1,"type":"value","unit":"hour","normalized":{"value":3600,"unit":"second"}},"end":8,"dim":"duration","latent":false}]

「一時間」の部分が正しくDurationとして認識されてますね。1時間を秒単位変換もしてくれます。試しに「30分」や「3時間」を入力して見ても「1800秒」と「10800秒」として変換されてます。これで時間も単位関係なく簡単に比較できます。あと、「三十時間」と「30時間」も同じ処理ができるので強い。

日本語サポートがある固有表現はDuration(時間単位)、Numeral(数字)、Ordinal(第〇〇)、Temperature(マイナス〇〇度)しかありませんが、READMEファイルの「Extending Duckling」のセッションの指示通り必要なファイルを作っていけば簡単に言語サポートを増やせます。

ちょっと改造して見ましょう

今回は、AmountOfMoneyとDistanceに日本語サポートを追加して見ましょう。作成するファイルは以下の三つ:

Duckling/<Dimension>/<Lang>/Rules.hs #ルール(正規表現)を定義する場所
Duckling/<Dimension>/<Lang>/Corpus.hs #特別なケース
Duckling/Dimensions/<Lang>.hs #固有表現種類のリスト
Duckling/Rules/<Lang>.hs #ルールを登録

今回は「〇〇円」と「〇〇マイル」の表現を認識したいと思います。
おすすめのやり方としては、追加したい固有表現(Dimension)にJAのフォルダ作って、EN(英語)のRules.hsとCorpus.hsをフォルダにコピー、改造したら一番簡単だと思います。

AmountOfMoney/Rules.hs
module Duckling.AmountOfMoney.JA.Rules --ルール名も修正する必要があります

ruleJPY :: Rule
ruleJPY = Rule
 { name = "円"
 , pattern =
   [ regex "円"
   ]
 , prod = \_ -> Just . Token AmountOfMoney $ currencyOnly JPY
 }
Distance/Rules.hs
module Duckling.Distance.JA.Rules

ruleDistMiles :: Rule
ruleDistMiles = Rule
 { name = "<dist> miles"
 , pattern =
   [ dimension Distance
   , regex "miles?|マイル"
   ]
 , prod = \case
     (Token Distance dd:_) ->
       Just . Token Distance $ withUnit TDistance.Mile dd
     _ -> Nothing
 }

コーパスは特にないので空にしても問題ないです。
ルールの定義が終わったらJAのルールリストに登録する

Rules/JA.hs
langRules (This AmountOfMoney) = AmountOfMoney.rules
langRules (This Distance) = Distance.rules
Dimension/JA.hs
allDimensions =
 [ This Duration
 , This Numeral
 , This Ordinal
 , This Temperature
 , This AmountOfMoney
 , This Distance
 ]

Debugモードを使ってテストして見ましょう

$ stack repl --no-load
> :l Duckling.Debug
> debug (makeLocale JA $ Nothing) "一円" [This AmountOfMoney]
#出力
#<amount> <unit> (一円)
#-- integer (0..10) (一)
#-- -- regex (一)
#-- 円 (円)
#-- -- regex (円)
#[Entity {dim = "amount-of-money", body = "\19968\20870", value = RVal AmountOfMoney (SimpleValue (SingleValue {vCurrency = JPY, vValue = 1.0}))...

大丈夫そうですね。

リビルトして、試して見ましょう

stack build :duckling-regen-exe
stack exec duckling-example-exe
$ curl -POST http://0.0.0.0:8000/parse --data 'lang=ja&text=北海道まで500マイルです。チケットは3000円になります。'
#出力
[{"body":"500マイル","start":5,"value":{"value":500,"type":"value","unit":"mile"},"end":11,"dim":"distance","latent":false},{"body":"3000円","start":19,"value":{"value":3000,"type":"value","unit":"JPY"},"end":24,"dim":"amount-of-money","latent":false}]

無事追加されました!

今回は簡単なものしか追加してないのですが、英語のルール見たらどうやら「〇〇円〜△△円」といった表現も認識できるらしいです。今度時間あったら日本語版のルールも追加したいと思っています。

まとめ

今回は正規表現を使って文章の中の固有表現を抽出し、内容をパースできました。機械学習と違って正規表現はパターンさえ合えば、確率、精度関係なく抽出可能のものなで、パターンのある固有表現は一回正規表現を使って取れる物はとったほうが誤認しにくいなので前処理感覚で一回使ったほうが効率よく文章を分析できます。日本語の固有表現サポートをどんどん増やしていこう!