形態素解析器Sudachiのユーザー辞書には文字正規化が必要

10 min read読了の目安(約9500字

TL;DR (3行要約)

  • 形態素解析器Sudachiでは、ユーザー辞書により任意の語を追加することができる
  • ユーザー辞書は、Sudachi内部での文字正規化が施された形で記述される必要がある
  • 文字正規化部分だけを抜き出したコードを、当記事の末尾に記した

Sudachiのユーザー辞書

形態素解析器Sudachiと合わせて提供されているSudachi辞書(以下システム辞書)は、約300万語を収録し、専門家の手で整備がされている、大規模で高品質な言語資源です(cf. 形態素解析器Sudachiの「辞書」はどのように作られているか: 複数の分割単位を例として)。

しかし状況によっては、システム辞書にはない、例えば「領域独自の固有名詞」のような任意の語も上手く扱いたいことがあります。そのような時のためにSudachiには、ユーザー辞書を導入する機構があります。

(ちなみにユーザー辞書は、「システム辞書にあるが、解析時により優先して出したい場合」に、コストを変更して登録する、といった使い方にも利用できます)

ユーザー辞書を使ってみよう

実際に、システム辞書にはない Legalscape という語を登録してみます。

システム辞書による解析結果

まずデフォルトの、システム辞書だけの状態で解析してみると、以下の結果となります;

$ echo "Legalscape" | sudachipy -a | tr "\t" "\n" | nl -b a
     1	Legalscape
     2	名詞,普通名詞,一般,*,*,*
     3	legalscape
     4	legalscape
     5
     6	-1
     7	[]
     8	(OOV)
     9	EOS

コマンドですが、 echo "Legalscape" | sudachipy がメインの部分です。SudachiPyの -a オプションにより、詳細な情報(4カラム目以降)を表示しています。 trnl は、ここでの説明を簡単にするため、改行して行番号をふるものです。

表示されているカラムは、以下の通りです;

  1. 表層形
  2. 品詞
  3. 正規形
  4. 辞書形
  5. 読み
  6. 辞書ID
  7. 同義語グループID
  8. 未知語フラグ(Out of Vocabulary, OOV)
  9. 文末記号(End of Sentence, EOS)

辞書IDは、辞書該当無し(未知語)は「-1」、システム辞書は「0」、そしてユーザー辞書は「1以降」となります。ユーザー辞書は複数追加することができ、設定ファイルに記述されている順番に沿って1から番号がふられます。複数の種類のユーザー辞書を用意して、辞書ごとに何か個別の処理を行いたい場合などに利用できるでしょう。

ユーザー辞書の記述とビルド

さて、システム辞書のみによる処理結果では、一応はまとまったひとつの語として出力されてはいますが、未知語(OOV)となってしまっています。

そこで、ユーザー辞書により Legalscape という語を導入します。以下のテキストファイル(user.txtという名前にしておきます)を用意します。ユーザー辞書の記述には計18カラムが必要ですが、詳細は公式ドキュメントをご覧ください。今回ポイントとなるのは、最初のカラム、 Legalscape と書かれている部分です。

Legalscape,4786,4786,-32768,Legalscape,名詞,固有名詞,一般,*,*,*,リーガルスケープ,Legalscape,*,*,*,*,*

次に、このテキストファイルから、辞書バイナリをビルドします。これは、Java版とPython版、どちらでも可能です。今回は、Python版であるSudachiPyを使ってビルドしてみます。

$ sudachipy ubuild user.txt

設定ファイルによるユーザー辞書の指定

上記のコマンドにより、user.dicというファイルが生成されます。

次に、sudachi.jsonという設定ファイルにより、このユーザー辞書バイナリを指定します。

このファイルは、Pythonパッケージでは sudachipy/resources/ 下にあります(Java版では src/main/resources/ )。こちらを直接編集しても良いですし、またはコピーしてきて別途編集することもできます。GitHubのレポジトリから直接ダウンロードするのが手っ取り早いかもしれません。

設定ファイルに、 "userDict": ["user.dic"] という1行を追加します。この "user.dic"という箇所は、設定ファイルから辞書バイナリへの相対パスを記述します。設定ファイルと同じディレクトリに辞書バイナリを置くのであれば、この記述で問題ありません。

また、この設定ファイルは、 char.defunk.def というファイルへの参照も記述されているのですが、それらも相対パスなので、例えば sudachi.json だけをどこかのディレクトリに持ってきてコピー・編集して使う場合には、これら二つのファイルもコピーして同じディレクトリに置いておくのが、手っ取り早いでしょう。これら二つも、同じく sudachipy/resources/ 下にあります。

{
    "userDict": ["user.dic"],
    "characterDefinitionFile" : "char.def",
    "inputTextPlugin" : [
        { "class" : "sudachipy.plugin.input_text.DefaultInputTextPlugin" },
        { "class" : "sudachipy.plugin.input_text.ProlongedSoundMarkInputTextPlugin",
          "prolongedSoundMarks": ["ー", "-", "⁓", "〜", "〰"],
          "replacementSymbol": "ー"}
    ],
    "oovProviderPlugin" : [
        { "class" : "sudachipy.plugin.oov.MeCabOovProviderPlugin",
          "charDef" : "char.def",
          "unkDef" : "unk.def" },
        { "class" : "sudachipy.plugin.oov.SimpleOovProviderPlugin",
          "oovPOS" : [ "補助記号", "一般", "*", "*", "*", "*" ],
          "leftId" : 5968,
          "rightId" : 5968,
          "cost" : 3857 }
    ],
    "pathRewritePlugin" : [
        { "class" : "sudachipy.plugin.path_rewrite.JoinNumericPlugin",
          "enableNormalize" : true },
        { "class" : "sudachipy.plugin.path_rewrite.JoinKatakanaOovPlugin",
          "oovPOS" : [ "名詞", "普通名詞", "一般", "*", "*", "*" ],
          "minLength": 3 }
    ]
}

ユーザー辞書を使った解析結果

Sudachiでは -r オプションで、設定ファイルを指定することができます。では、ビルドしたユーザー辞書バイナリを使って、再度解析してみます。

$  echo "Legalscape" | sudachipy -a -r sudachi.json | tr "\t" "\n" | nl -b a
     1	Legalscape
     2	名詞,普通名詞,一般,*,*,*
     3	legalscape
     4	legalscape
     5
     6	-1
     7	[]
     8	(OOV)
     9	EOS

ユーザー辞書により Legalscape という語を追加したにもかかわらず、依然として未知語になってしまっています。

これは、Sudachi内部では「文字の正規化」が行われており、ユーザー辞書の見出しもそれに合わせる必要があるからです。

ユーザー辞書の見出しは「文字正規化」しておく必要がある

公式ドキュメントの該当箇所では、以下のように解説しています;

見出しは、「Sudachiの文字正規化がおこなわれた後の形」で登録してください。

Sudachiでは、文字正規化が行われた後に見出しを引きます。そのため、「正規化後に現れない形」で見出しが表記されている場合、その語はどのような場合でもマッチすることがありません。例えば、「ラテン文字の大文字」で見出しを表記した場合、Sudachi内部では正規化後の「小文字」になったもので見出しを探すため、この大文字のものとマッチすることがありません。

Sudachiでは、以下の文字正規化を行っています。挙動の詳細は、Sudachiドキュメントの該当箇所を参照してください。

  • 小文字化
  • NFKC をつかった Unicode 正規化
    • ただし、設定ファイル rewrite.def に定義される抑制、置換が優先

ユーザー辞書の見出しへは、文字正規化は自動的には適用されません。これは、ユーザーが想定しづらい挙動を避けるためです。そのため、ユーザー辞書の作成者が文字正規化を意識して語を表記する必要があります。

今回のケースでは、 Legalscape という見出し表記に、 L という大文字が含まれているため、Sudachiが文字正規化した後に見出しを引きに行っても、絶対にマッチすることがありません。

正規化したユーザー辞書による解析結果

それでは、 user.txt の見出し(最初のカラム)を、Legalscapeから、legalscape へと、大文字を小文字に変えてみます。正規形や辞書形も含むそれ以外の箇所は全く変更しません。

そして再度、このテキストファイルからユーザー辞書バイナリをビルドします。

$ cat user.txt
legalscape,4786,4786,-32768,Legalscape,名詞,固有名詞,一般,*,*,*,リーガルスケープ,Legalscape,*,*,*,*,*
$ sudachipy ubuild user.txt

この新しいユーザー辞書バイナリを使って解析してみると、以下のようになります;

$ echo "Legalscape" | sudachipy -a -r sudachi.json | tr "\t" "\n" | nl -b a
     1	Legalscape
     2	名詞,固有名詞,一般,*,*,*
     3	Legalscape
     4	Legalscape
     5	リーガルスケープ
     6	1
     7	[]
     8	EOS

想定通り、既知語として解析されました。品詞(第2カラム)も、 user.txt に記述した通り 名詞,固有名詞,一般,*,*,* となっていますし、読み(第5カラム)も、ちゃんと出ています。また、辞書ID(第6カラム)も、 1 、つまり一つ目のユーザー辞書となっています。OOVフラグの表記も消えています。

これは、例えば LegalScapeLEGALSCAPE などといった表記でも、第1カラム以外は同じ出力となります。正規形(第3カラム)や辞書形(第4カラム)は、ユーザー辞書に記載した通り Legalscape という表記になっています。

$ echo "LEGALSCAPE" | sudachipy -a -r sudachi.json | tr "\t" "\n" | nl -b a
     1	LEGALSCAPE
     2	名詞,固有名詞,一般,*,*,*
     3	Legalscape
     4	Legalscape
     5	リーガルスケープ
     6	1
     7	[]
     8	EOS

ちなみに、この「リーガルスケープ」というのは私が所属する会社の名前なのですが、正式表記は Legalscape でして、Lだけが大文字です。LegalScape でも Legal Scape でもなく、はたまた Legalspace でも Legalscope でもないです。一緒に法律業界の未来をつくるエンジニア募集中です。

Sudachiにおける文字正規化

前述の例では、大文字を小文字に変えるだけの修正で充分でしたが、Sudachiではもう少し複雑な文字正規化を行っています。

デフォルトで適用されるプラグイン内部では、以下の処理が入力文字に施されます;

  1. rewrite.def で定義される文字列置換
  2. 小文字化
  3. Unicode正規化(NFKC)

ただし、3.は、rewrite.defで定義される文字には適用しない

Sudachiでの該当する文字正規化処理は、以下の箇所に記述されています;

rewrite.def とはなにか

Sudachiの rewrite.def には、2種類の文字集合が記載されてます;

  • 正規化の対象外とする文字
  • 決められた置換をする文字列のペア

このファイルは、Java版では src/main/resources/ に、Python版では sudachipy/resources/ にあります。設定ファイル( sudachi.json )と同じ場所です。

正規化の対象外とする文字

NFKC正規化を適用しない文字です。

一行にコードポイントが1つだけ記載されているもので、デフォルトの rewrite.def では前半にあります。ローマ数字記号であったり、正規化せずそのまま扱いたい漢字などが記載されています。当記事執筆時点で835文字あります。

# ignore normalize list
#   ^{char}%n
Ⅰ
Ⅱ
Ⅲ
...
⺍
⺎
⺏
...
恵
𤋮
舘
...

決められた置換をする文字列のペア

小文字化やNFKC正規化とは独自に、文字列置換を行うためのものです。

デフォルトの rewrite.def では後半に記載されています。濁点および半濁点のついた平仮名と片仮名が記載されています。当記事執筆時点で182ペアあります。

# replace char list
#   ^{before}\s{after}%n
ヴ	ヴ
ガ	ガ
ギ	ギ
...
ゔ	ゔ
が	が
ぎ	ぎ
...
は゜	ぱ
ひ゜	ぴ
ふ゜	ぷ
...

小文字化

小文字化は、Javaでは Character.toLowerCase() 、Pythonでは str.lower() によるものです。ラテン文字だけでなく、ギリシア文字やキリル文字なども小文字化します。

ユーザー辞書のための文字正規化

ここまで述べたように、ユーザー辞書を正しく利用するためには、Sudachiが内部で行う文字正規化を予め施しておく必要があります。

(ただ、多くの場合は、小文字化とNFKCだけを直接やれば、十分だとも思います)

Sudachiでの該当箇所と同等の処理を行うのが、以下のPythonコードです;

SudachiCharNormalizer.py - GitHub Gist

Pythonコードとして利用;

normalizer = SudachiCharNormalizer()
assert normalizer.rewrite("ÂBΓД㈱ガウ゛⼼Ⅲ") == "âbγд(株)ガヴ⼼ⅲ"

CLIスクリプトとして利用;

$ echo "ÂBΓД㈱ガウ゛⼼Ⅲ " | python SudachiCharNormalizer.py
âbγд()ガヴ⼼ⅲ

$ cat vocabs.txt | python SudachiCharNormalizer.py <rewrite.defのパス>
...

同じ場所に rewrite.def を置くか(cf. GitHubレポジトリにあるrewrite.def)、パスを明示的に指定してください。

余談: ユーザー辞書に分割単位情報を付与する方法

ちなみに、SudachiはA,B,Cという複数粒度での分割ができることが特色の一つですが、これをユーザー辞書にも導入することができます。しかし、現状では少々ややこしいことになっています。次のページにまとめていますので、ご興味ある方はご覧ください; Elasticsearchのための新しい形態素解析器 「Sudachi」 - Qiita - コメント

Sudachiによる複数粒度分割

Sudachiについての質問や要望があったら

公式Slackに投稿したり、GitHubレポジトリでissueを立てると良いです; Sudachi公式Slack参加リンク

Enjoy Tokenization!

この記事に贈られたバッジ