🐴

Sudachiで馬謖を切る: 形態素解析の可視化とユーザー辞書による制御

2022/03/08に公開

TL;DR

  • 形態素解析は意図しない結果になることがあるし、唯一の正解があるとも限らない
  • ViSudachiで解析の内部構造を可視化し、なぜその結果に至ったかを確認できる
  • ユーザー辞書により、解析をある程度コントロールできる

馬謖を切る

遥か昔の中国に生きた諸葛孔明という人は、重用していた部下の馬謖(ばしょく)が命令に従わなかったため、規律を遵守し泣いて斬罪に処したと言われています。

https://ja.wikipedia.org/wiki/泣いて馬謖を斬る

さて、形態素解析器Sudachiで 馬謖 を切ってみると、どうなるでしょうか。

$ echo "馬謖" | sudachipy
馬謖	名詞,普通名詞,一般,*,*,*	馬謖
EOS

複数単語には分割されないようです。では、もしこれを / の2単語へと切りたい時には、どうしたらいいでしょうか?

形態素解析の誤り

馬謖 の例はどうでもいいことだと思うかもしれませんが、形態素解析ではたまに誤解析が起こります。

例えば形態素解析界で知られる話として 魔法少女リリカルなのは というアニメ作品名の解析があります。これをSudachiで解析してみると、「なのは」が人名ではなく、助動詞・助詞だと解釈されてしまいます。

$ echo "魔法少女リリカルなのは" | sudachipy
魔法	名詞,普通名詞,一般,*,*,*	魔法
少女	名詞,普通名詞,一般,*,*,*	少女
リリカル	形状詞,一般,*,*,*,*	リリカル
な	助動詞,*,*,*,助動詞-ダ,連体形-一般	だ
の	助詞,準体助詞,*,*,*,*	の
は	助詞,係助詞,*,*,*,*	は
EOS

(形態素解析は単語分割だけをやるわけではないのですが)分割の誤りが身近で分かりやすいかと思います。 弁慶がなぎなたを持って というテキストを「薙刀を持つ」と解釈せず、 弁慶がな / ぎなたを持って と区切ってしまう例から、「ぎなた読み」と呼ばれたりもする事象です。近年では この先生きのこる というフレーズもよく知られているでしょう。

https://ja.wikipedia.org/wiki/ぎなた読み

(余談ですが、ぎなた読みについては、林部祐太さんによる次の研究も大変興味深いです: [林部2018] "ぎなた読みの自動生成の試み", NLP2018 WS 形態素解析の今とこれから)

また、そもそも何が「正しい解析結果」かは自明ではありません。これは言語学の観点からも難しい問題ですし、また実応用においても用途やドメインによって答えが変わってきます。

例えば スポンサードリンク というテキスト。ネット広告会社だったら「リンク🔗」と捉えたいかもしれません。飲料会社だったら「ドリンク🥤」と思うかもしれません。

https://twitter.com/sorami/status/1470313512803401734

ViSudachiによる解析の内部構造可視化

形態素解析器はなぜ、そのような結果へと至るのでしょうか。解析器の気持ちは、内部構造の可視化により確認することができます。

Sudachiをはじめとするほとんどの形態素解析器では、全ての解析候補を表現する「ラティス」というデータ構造を構築し、その上での「最短経路」を探す問題として解析を定式化しています(最小コスト法)。単語の出現しやすさや並びやすさをコストとして、それらの可能性の中から、もっともそれらしいもの(コストが最小の経路)を選択しています。

そのラティスと経路を可視化することで、なぜその解析結果が選ばれたのか、他にはどのような候補がありコストはどれくらい違うのか、などを確認することができます。

当記事では詳しく述べませんが、解析アルゴリズムの詳細は以下の記事が参考になります:

より詳しく知りたい方は、以下の書籍も参考になるでしょう:

Sudachiのためには「ViSudachi」という可視化ツールが公式で開発されており、これを用いることでそのラティスと最短経路を見ることができます。

https://github.com/WorksApplications/ViSudachi

ViSudachiのセットアップ

それでは、ViSudachiで解析を可視化してみましょう。

まず、ViSudachiのソースコードを取得してビルドします(事前にJavaランタイムとYarnが用意されている必要があります)。

$ git clone https://github.com/WorksApplications/ViSudachi
$ cd ViSudachi
$ ./gradlew bootJar

次に、解析のためのSudachi辞書を用意します(ビルド済みのSudachi辞書はこちらで公開されています)。

# ビルド済み辞書をダウンロード
$ wget http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict/sudachi-dictionary-latest-core.zip

# `system_core.dic` を現在のディレクトリに設置
$ unzip sudachi-dictionary-latest-core.zip
$ cp sudachi-dictionary-20211220/system_core.dic .

これで準備が整いました。ViSudachiを起動し、ブラウザで http://localhost:8080 にアクセスします。

$ java -jar backend/build/libs/visudachi-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.6)
 
...

「馬謖」を形態素解析した時の内部構造

それでは、ViSudachiで 馬謖 を解析したときの内部構造を見てみましょう。

それぞれの箱が、候補となる語です。箱の4行目にある数字が、その語の「生起コスト」で、出現のしやすさを表します。箱と箱を繋ぐ線の上にある数字が「連接コスト」で、品詞と品詞の繋がりやすさを示します。これらを足し合わせることで、それぞれのパス(解析結果)に対する「累積コスト」が求まります。この全ての可能性の中から、もっともコストが低いパスを、解析器は出力します。これらのコストは、アノテートされたコーパスから事前に学習したモデルにより付与しています(機械学習以前の時代は、人手でコストを調整していたそうです)。ちなみに「BOS,EOS」はそれぞれ「文頭,文末」を示すものです("Beginning/End of Sentence")。

今回のケースでは、辞書中に 馬謖 という語があり、それを通るパスが より小さいことから、分割されない解析結果となっています。

ユーザー辞書による解析の制御

解析器の内部構造を可視化することで、なぜそのような結果になったかが分かりました。

それを参考に、コストを調整したり、箱(語)を追加することで、解析結果をある程度コントロールできます。Sudachiのシステム辞書を直接書き換えることもできますが、それには「ユーザー辞書」を用いるのが適しています。

また、Sudachiの特徴として「複数分割単位の解析」があります。これは「ある語が、より短い語から構成されている」という情報を付与するものです。それによって解析結果を”再分割”することができます。以下の図では、 カンヌ国際映画祭 という「最も長い語」に対して、これは カンヌ 国際 映画 という「より短い4つの語」から構成されている、というイメージを表しています。

上記の説明だけでは理解が難しいと思うので、実際に複数粒度の情報を持つユーザー辞書を作ってみましょう。

Sudachiユーザー辞書の記述

ユーザー辞書を作成するには、語と、その品詞やコストの情報が必要です。どのように記載するかは、公式ドキュメント「Sudachi ユーザー辞書作成方法」に述べられています。

今回は、以下のユーザー辞書を用意しました。1行が1語で、合計3つの語を収録しています。

$ cat 馬謖.txt
馬,-1,-1,-32768,馬,名詞,固有名詞,人名,🐴,*,*,バ,馬🐴,*,A,*,*,*
謖,-1,-1,-32768,謖,名詞,固有名詞,人名,🧍,*,*,ショク,謖🧍,*,A,*,*,*
馬謖,4786,4786,-32768,馬謖,武将,字は幼常,荊州出身,出生190年,死去228年,🐴🧍,バショク,馬謖🐴🧍,*,B,U0/U1,*,*

カンマ区切りで最初のカラムが、語の「見出し」です。

次の2つのカラムが「連接ID」です。これをもとに先述した「連接コスト」(線の上にある数字)が定まります。公式ドキュメントに、推奨されるIDが紹介されています。ここでは のIDは-1にしていますが、これは「解析時にはこれらの語を使わない」ということを示します。後述するようにこれら二つの語は「三つ目の語 馬謖 を構成するより短い語」としてのみ利用します。

その次のカラムが、その語の「生起コスト」です。これには範囲内の任意の数値を指定できます。ViSudachiの可視化を見ながら細かく調整することもできますが、Sudachiではこれを -32768 とすることで、自動推定したいい感じの値にしてくれます。公式ドキュメントでは以下のように解説されています。

3 コスト
形態素解析に使用される見出し表記のコスト値です。 "-32767 ~ 32767" までの整数値で指定できます。 値を小さくするほど、登録した見出し表記が解析結果として出やすくなります。 なお、"-32768" を指定すると、ユーザー辞書読み込み時に自動推定した値を付与します。

名詞類の登録であれば、"5000 ~ 9000" を推奨

コストの次にある5つのカラムが「品詞情報」です。Sudachi辞書の品詞体系に沿って書いてもいいですし、ユーザー任意の情報を記述することもできます。

0-indexで12カラム目は「正規化表記」です。これも任意の表記を記載することができます。

そして0-indexで14-16カラムが、Sudachi特有の複数粒度分割情報です。14カラム目は「分割タイプ」で、A,B,Cいずれかの分割単位を記述します。今回は 馬謖 をB単位とし、 , をA単位としました。そして15カラム目が「A単位分割情報」、16カラム目が「B単位分割情報」です。今回はB単位の語 馬謖 に対して、 A単位分割情報を U0/U1 と記載しました。この語は「ユーザー辞書の語0」(つまり)と「ユーザー辞書の語1」(つまり)から構成されている、ということを示します。公式ドキュメントでは以下のように解説されています。

15 A単位分割情報
分割単位タイプ B または C の語について、A単位に分割するための情報です。

構成語のIDまたは構成語情報を "/" (半角スラッシュ) で区切って記述します。

構成語のIDはその語が記述されている行番号 (0始まり) か、その先頭に "U" を加えた文字列です。ユーザー辞書内の語を参照するときに "U" をつけます。

構成語情報は語の見出し (解析結果表示用)、品詞1-4、品詞 (活用型)、品詞 (活用形)、読みを "," (カンマ) で区切った文字列です。 構成語情報を記述するときは分割情報のフィールド全体を " (ダブルクォーテーション) で囲む必要があります。 構成語情報に記述する語は別途記述されている必要があります。構成語がシステム辞書内にあるかユーザー辞書内にあるかは自動的に判別します。

なお構成語としてのみ利用される語は連接IDに-1を記述すると、単独の語として出現しなくなります。

Sudachiユーザー辞書のビルドと利用準備

さて、上記で用意したユーザー辞書を実際にビルドして、利用できるようにしてみましょう。

まず、SudachiPyのubuildコマンドで、ユーザー辞書のテキストファイルをバイナリへビルドします。

$ sudachipy ubuild -s system_core.dic -o 馬謖.dic 馬謖.txt
/../馬謖.txt -> 3 in 0.00 sec
validate -> 3 in 0.00 sec
pos_table -> 2 in 0.00 sec
conn_matrix -> 6 in 0.00 sec
trie -> 1028 in 0.00 sec
word_id table -> 9 in 0.00 sec
word_params -> 22 in 0.00 sec
wordinfo_offsets -> 12 in 0.00 sec
wordinfos (copy only) -> 90 in 0.00 sec

次に、このユーザー辞書バイナリを利用するように、設定ファイルを用意します。必要となる各種設定ファイルは、Pythonパッケージとしてインストールした時に同梱されています。これらをまず持ってきます。

# パッケージのインストール先を特定
# pip show で `Location:` を確認
$ pip show sudachipy
Name: SudachiPy
Version: 0.6.3
Summary: Python version of Sudachi, the Japanese Morphological Analyzer
Home-page: https://github.com/WorksApplications/sudachi.rs/tree/develop/python
Author: Works Applications
Author-email: sudachi@worksap.co.jp
License: Apache-2.0
Location: /path/to/your/python/site-packages
Requires:
Required-by: SudachiDict-core, SudachiDict-full

# `sudachipy/resources/` にあるファイルを持ってくる
$ cp /path/to/your/python/site-packages/sudachipy/resources/{sudachi.json,char.def,rewrite.def,unk.def} .

そうして持ってきた設定ファイルの中にある sudachi.json"userDict": ["馬謖.dic"] という項目を追記します。これで準備完了です。

改めて、馬謖を切る

それでは、このユーザー辞書を使って、再び馬謖を切ってみましょう。 -r オプションで設定ファイルを指定します。

以下をみると 馬謖 となっており、まだ分割はされていませんが、品詞情報や正規化表記から、ユーザー辞書の語が出ていることがわかります。

$ echo "馬謖" | sudachipy -r sudachi.json
馬謖	武将,字は幼常,荊州出身,出生190年,死去228年,🐴🧍	馬謖🐴🧍
EOS

ViSudachiで内部構造を見てみます。ユーザー辞書を使った解析結果を表示するには、以下のように指定します(現状では、C単位でしか表示されれず、その再分割情報は表示されません)。

$ java -jar backend/build/libs/visudachi-0.0.1-SNAPSHOT.jar --user-dict=./馬謖.dic

図を見ると、ユーザー辞書に追加した語を用いたパスが、元々あった語(ID:756301)のものより小さくなっていることが確認できます。

さて、上記のようにSudachiはデフォルトでは最も長い単位(C単位)で分割します。それでは、最も短いA単位で分割してみましょう。

$ echo "馬謖" | sudachipy -r sudachi.json -m A
馬	名詞,固有名詞,人名,🐴,*,*	馬🐴
謖	名詞,固有名詞,人名,🧍,*,*	謖🧍
EOS

ユーザー辞書で記述した通り、より長い単位の語 馬謖 として解析された結果が、付与したA単位分割情報をもとに再分割され、二つの語 / となりました。

これでやっと馬謖を切ることができました。ここまでの苦労に涙がこぼれます。

以上、何かの参考になれば幸いです。Enjoy Tokenization!

関連文献

商品企画

https://twitter.com/morishige/status/1186961579029360640

流れよわが涙、と孔明は言った

三方行成『流れよわが涙、と孔明は言った』(早川文庫JA, 2019)

https://www.hayakawa-online.co.jp/shopdetail/000000014198/

公式ページにて表題作が全文公開されています: 【書籍化決定】「流れよわが涙、と孔明は言った」三方行成ショート・ストーリー|Hayakawa Books & Magazines(β)

「チー」
孔明は鳴いて馬謖を切った。
「ロン」
下家が牌を倒した。
「馬謖のみ。10兆点」
「な、何ィー!?」
孔明は怒りに満ちて卓を蹴倒した。雀牌が滝のように流れ落ちた。

Discussion