自然言語処理(NLP)のプロジェクトに携わったことがあるエンジニアなら、前処理の重要性を痛感しているはずです。どれほど優れたモデルを用意しても、入力データの質が低ければ結果は期待できません。その前処理の中でも、最も基本的かつ重要なステップがトークナイゼーションです。
トークナイゼーションとは、テキストを意味のある最小単位(トークン)に分割する処理のことです。英語であればスペースで区切れば概ね対応できますが、日本語のように単語間に区切りがない言語では、形態素解析器を使った高度な処理が必要になります。本記事では、トークナイゼーションの各手法を深掘りし、実務で使えるテクニックを紹介します。
最もシンプルな方法は、テキストを単語単位で分割することです。英語ではスペースや句読点で分割しますが、日本語の場合はMeCabやJanomeなどの形態素解析器を使います。
import MeCab
tagger = MeCab.Tagger("-Owakati")
text = "自然言語処理の前処理は非常に重要です"
tokens = tagger.parse(text).strip().split()
print(tokens)
# ['自然', '言語', '処理', 'の', '前処理', 'は', '非常', 'に', '重要', 'です']
単語レベルのトークナイゼーションは直感的で理解しやすいですが、未知語(OOV: Out of Vocabulary)問題が発生しやすいという欠点があります。訓練データに存在しない単語が出てくると、モデルは適切に処理できません。
近年主流となっているのが、サブワード(部分単語)レベルでの分割です。代表的な手法としてBPE(Byte Pair Encoding)、WordPiece、SentencePieceがあります。これらは頻出する文字列パターンを学習し、未知語も既知のサブワードの組み合わせで表現できるため、OOV問題を大幅に軽減します。
import sentencepiece as spm
# モデルの学習
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='sp_model',
vocab_size=8000,
model_type='bpe'
)
# 学習済みモデルの読み込みとトークナイズ
sp = spm.SentencePieceProcessor()
sp.load('sp_model.model')
text = "ディープラーニングによる自然言語処理"
tokens = sp.encode_as_pieces(text)
print(tokens)
# ['▁ディープ', 'ラーニング', 'による', '自然', '言語', '処理']
最も粒度が細かい方法が文字レベルです。OOV問題は完全に解消されますが、トークン列が非常に長くなるため、計算コストが増大します。特にTransformerベースのモデルでは、入力長の二乗に比例して計算量が増えるため、実用上の制約が大きくなります。
トークナイゼーションの前後にも、重要な前処理ステップがあります。実務では以下のようなパイプラインを構築することが一般的です。
[CLS]、[SEP]、[PAD]などの特殊トークンを追加import unicodedata
import re
def preprocess_text(text):
# Unicode正規化
text = unicodedata.normalize('NFKC', text)
# URLの除去
text = re.sub(r'https?://\S+', '[URL]', text)
# 連続する空白を1つに
text = re.sub(r'\s+', ' ', text).strip()
return text
raw = " 自然言語処理は https://example.com を参照"
cleaned = preprocess_text(raw)
print(cleaned)
# '自然言語処理は [URL] を参照'
現在広く使われているBERTとGPT系モデルでは、採用しているトークナイゼーション手法が異なります。BERTはWordPieceを、GPT系はBPEをベースにしています。日本語BERTモデルの多くはMeCabによる事前分割とWordPieceを組み合わせています。
from transformers import AutoTokenizer
# 日本語BERTのトークナイザ
tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-v3')
tokens = tokenizer.tokenize('自然言語処理の前処理テクニック')
print(tokens)
# ['自然', '言語', '処理', 'の', '前', '##処理', 'テクニック']
##プレフィックスは、そのトークンが前のトークンに続くサブワードであることを示しています。この仕組みにより、未知の複合語でも適切に処理できるわけです。
筆者が10年以上NLPプロジェクトに携わってきた経験から、以下の指針を推奨します。
トークナイゼーションは地味な処理に見えますが、NLPパイプライン全体の精度を左右する極めて重要なステップです。自分のタスクに最適な手法を選択し、丁寧に前処理パイプラインを構築することが、高品質なNLPシステムへの第一歩となります。