言語処理をする際,mecabなどのトークナイザを使ってテキストを分かち書きすることが多いと思います.本記事では,異なるトークナイザの出力(分かち書き)の対応を計算する方法とその実装(tokenizations)を紹介します. 例えば,以下のようなsentencepieceとBERTの分かち書きの結果の対応を計算する,トークナイザの実装に依存しない一般的な方法を見ていきます.
# 分かち書き
(a) BERT : ['フ', '##ヘルト', '##ゥス', '##フルク', '条約', 'を', '締結']
(b) sentencepiece : ['▁', 'フ', 'ベル', 'トゥス', 'ブルク', '条約', 'を', '締結']
# 対応
a2b: [[1], [2, 3], [3], [4], [5], [6], [7]]
b2a: [[], [0], [1], [1, 2], [3], [4], [5], [6]]
先ほどの例を見ると,分かち書きが異なると以下のような差異があることがわかります
- トークンの切り方が異なる
- 正規化が異なる (例: が -> か)
- 制御文字等のノイズが入りうる (例: #, _)
差異が1.だけなら簡単に対処できそうです.二つの分かち書きについて,1文字ずつ上から比べていけば良いです.実際, 以前spaCyに実装した��spacy.gold.align
はこの方法で分かち書きを比較します.
しかし2.や3.が入ってくると途端にややこしくなります.各トークナイザの実装に依存して良いならば,制御文字をのぞいたりノイズをのぞいたりして対応を計算することができそうですが,あらゆるトークナイザの組み合わせに対して実装するのは骨が折れそうです.
spacy-transformersはこの問題に対して,ascii文字以外を全部無視するという大胆な方法を採用しています.英語ならばそこそこ動いてくれそうですが,日本語ではほとんど動きません.
ということで今回解くべき問題は,上記1~3の差異を持つ分かち書きの組みの対応を計算することです.
言語処理では様々な正規化が用いられます.例えば
- Unicode正規化: NFC, NFD, NFKC, NFKD
- 小文字化
- アクセント削除
などです.上記一つだけでなく,組み合わせて用いられることも多いです.例えばBERT多言語モデルは小文字化+NFKD+アクセント削除を行なっています.
2つの分かち書きをA
, B
とします.以下のようにして対応を計算することができます.
- 各トークンをNFKDで正規化し,文字化をする
A
,B
のそれぞれのトークンを結合し,2つの文字列Sa
,Sb
を作る- 編集グラフ上での最短パスを計算する
- 最短パスを辿り,
Sa
とSb
の文字の対応を取得する - 文字の対応からトークンの対応を計算する
要するにdiffの逆を使って文字の対応を取り,トークンの対応を計算します.肝となるのは3で,これは編集距離のDPと同じ方法で計算できますが,例えばMyers' algorithmを使えばより低コストで計算できます.
実装はこちらに公開しています: GitHub: tamuhey/tokenizations
中身はRustですが,Pythonバインディングも提供しています.Pythonライブラリは以下のように使えます.
$ pip install pytokenizations
>>> import tokenizations
>>> tokens_a = ['フ', '##ヘルト', '##ゥス', '##フルク', '条約', 'を', '締結']
>>> tokens_b = ['▁', 'フ', 'ベル', 'トゥス', 'ブルク', '条約', 'を', '締結']
>>> a2b, b2a = tokenizations.get_alignments(tokens_a, tokens_b)
>>> print(a2b)
[[1], [2, 3], [3], [4], [5], [6], [7]]
>>> print(b2a)
[[], [0], [1], [1, 2], [3], [4], [5], [6]]
先日,Camphrという言語処理ライブラリを公開しましたが,このライブラリの中でpytokenizations
を多用しています.transformersとspaCyの分かち書きの対応を計算するためです.地味ですが実用上非常に役に立つ機能だと思います.