こんにちは。ふらうです。
今回は、SentencePieceの解説です。
自然言語処理では重要な内容となっていますので、初学者の方は必見です。
それでは、解説していきましょう。
はじめに
まず、SentencePieceとは何なのか、なぜ必要なのかについて解説します。
SentencePieceとは何か
SentencePieceは、Googleが開発した自然言語処理のためのオープンソースのライブラリです。
なぜSentencePieceが必要なのか
SentencePieceは、文章を解析して分割するために必要なライブラリです。
文章を単語に分割するライブラリ・ツールは、主に以下の2つだと思います。
- SentencePiece
- BPE(Byte Pair Encoding)
今回は、SentencePieceの解説なのでBPEのアルゴリズムについては解説しません。
「なぜこのようなものが必要なのか。」「文章をそのままAIに理解してもらえればいいのではないか。」と考える人もいると思います。
ではなぜ必要なのかというと、AIは文章を理解するために文章中の単語間の係り受けを学習するように作られているからです。その単語に分割する役割を担っているのがSentencePieceなのです。
単語分割とサブワード分割の違い
文章を細かく分割する方法には、単語分割とサブワード分割があります。
これから各分割のアルゴリズムと問題点について解説していきます。
単語分割
単語分割について解説します。
単語分割とは
単語分割は、文字通り文章を単語に分割します。
例えば、「私は猫です。」という文章があった場合、「私」「は」「猫」「です」「。」の5つの単語に分割されます。これが単語分割です。
単語分割の問題点
単語分割の問題点としては、語彙数が多くなるということです。
SentencePieceでは、分割された単語を記憶して、一つ一つの単語を語彙辞書として保存しています。この語彙数が多ければ多いほど細かく分割できるのですが、多すぎると容量が圧迫されますし推奨されていません。
有名な日本語大規模言語モデルでも最大で32000語程度です。その他、大学などの研究機関や企業が開発している大規模言語モデルで使用されている語彙辞書でも24000および12000がほとんどです。
サブワード分割
サブワード分割について解説します。
サブワード分割とは
サブワード分割とは、頻繁に使われる文字列を一つの単語としてとらえて分割する手法です。
先ほどの単語分割では、「東京大学」と「京都大学」と「東京」と「京都」は全て別々の単語として語彙辞書に保存されます。
サブワード分割では、「東京」と「京都」と「##大学」の3つに分割されます。
これは、「~大学」という単語が多く出てくるため、「大学」という単語をサブワードとして分割することで、語彙数を少なく抑えることができるメリットがあります。
SentencePieceの概要
ここからは、SentencePieceの概要について触れていきます。
SentencePieceのアルゴリズム
SentencePieceはテキストのサブワード分割を行うアルゴリズムです。
SentencePieceは以下の手順で構成・実行されます。
1.学習データの準備
まず、SentencePieceを学習させるためにデータを用意する必要があります。このデータは、主に文章になっているテキストファイルです。公式のSentencePieceTrainerで学習するためには、一行一文になっているテキストファイルが必要です。
2.トークンの初期化
SentencePieceでは初めに、出てくる文字一つ一つをトークンとして認識します。単語ではなく文字です。はじめは、単語が全く分からない状態からスタートするのでこのようになります。
3.トークンペアをマージ
先ほどのトークンのペアを、頻繁に出現する順で取り出していき、新しいトークンを作ります。
例えば、「斎藤家の夕飯はラーメン」と「田中さんの夕飯はお好み焼き」という2つの文があった場合、どちらの文にも出現している「夕飯」という単語が新しいトークンとして登録されます。
このように、トークンのペアを出現頻度順にマージしていき、単語を作成していきます。
4.サブワード分割
さっきも説明した通り、できた単語を使用してサブワードに分割し語彙辞書を調整していきます。
SentencePieceを使ってみる
ここからは、実際にSentencePieceで学習を行い、語彙辞書とモデルを構築、そして実際に文章をそれに基づいて分割してみたいと思います。
では、さっそくやっていきましょう。
学習データの準備
今回は、日本語wikipediaデータを利用させていただきます。
以前の私の記事で、一行一文になるよう学習データを作成したことがありますので、そちらから引用します。データのダウンロードや作成方法は以下のリンクより、私の記事を参考にしてください。
frqux.hatenablog.com
上の記事で作成した"wiki.txt"は一行一文のデータになっていないので、調整していきます。しかし、そのままSentencePieceの学習に入れても一応学習はできます。
また、フルのデータで学習しようとすると、高スペックPCでないと途中でメモリ不足で終了してしまい、学習が失敗するので、データを削ることも行います。
今回は、1/100に縮小して実行します。
データの前処理
まず、入出力ファイルの指定を行います。
input_file = "data/wiki.txt" output_file = "data/wiki_split.txt"
ここでは、作業ディレクトリの中にdataというフォルダを作成して、その中にwiki.txtを入れている場合を想定しています。完成したテキストファイルも、このdataフォルダに保存されます。
次に、wiki.txtを開き、「。」で文章を分割します。
with open(input_file, "r", encoding="utf-8") as f: lines = f.readlines() sentences = [] for line in lines: line = line.rstrip("\n") sentences.extend(line.split("。"))
ここで、念のため空の行を削除するコードを実行します。
sentences = list(filter(lambda s: s.strip() != "", sentences))
最後にテキストファイルとして保存します。
with open(output_file, "w", encoding="utf-8") as f: f.write("\n".join(sentences))
これで、一行に一文となっている"wiki_split.txt"というテキストファイルが完成します。
これをこのままSentecePieceの学習に入れてもいいのですが、私のPCではスペック不足でエラーが発生するので、1/100に削ります。
皆さんのPCではできる可能性がありますので、一度そのまま入れてみて、「killed」という表示とともに学習が終了してしまうようなら以下のプログラムでデータを削ってください。
input_file_path = "data/wiki_split.txt" output_file_path = "data/wiki_split_001.txt" with open(input_file_path, "r", encoding="utf-8") as f: lines = f.readlines() num_lines = len(lines) print(num_lines) num_output_lines = num_lines // 100 print(num_output_lines) output_lines = lines[:num_output_lines] with open(output_file_path, "w", encoding="utf-8") as f: f.write("".join(output_lines))
これで、新しくwiki_split_001.txtというファイルが作成されますので、これを使いましょう。
学習の実行
次に学習を開始します。
まず、必要なライブラリをインポートします。
import sentencepiece as spm
sentencepieceは以下のコマンドで環境にインストールすることができます。
pip install sentencepiece
anaconda環境の方は以下でもインストールできます。
conda install sentencepiece
次に、引数の指定を行います。
input_file="data/wiki_split.txt" model_prefix="wiki" vocab_size=32000 character_coverage=0.9995 model_type="unigram"
input_file ... 学習するために読み込むテキストファイル
model_prefix ... 生成されるモデルファイルの名前
vocab_size ... 語彙数
character_coverage ... モデルがカバーする文字の量。公式では0.9995がおすすめらしい。
model_type ... 学習方法。unigramがデフォルトで、ほかにもbpe、char、wordがある。
そして、学習を実行します。
spm.SentencePieceTrainer.train(input=input_file, model_prefix=model_prefix, vocab_size=vocab_size, model_type=model_type)
学習が終了すると、「wiki.vocab」と「wiki.model」の2つのファイルが生成されます。.vocabが語彙ファイルで.modelがモデルファイルです。
文章を分割してみる。
できたモデルファイルをもとに、文章を分割してみたいと思います。
import sentencepiece as spm model_path = "wiki.model" text = "明日は雨が降るだろう。" sp = spm.SentencePieceProcessor(model_file=model_path) enc = sp.encode(text) enc_str = sp.encode(text, out_type=str) dec = sp.decode(enc) print(enc) print(enc_str) print(dec)
まず、encには、分割された文章のトークンIDが入ります。なので、私の環境では以下のようになります。
[4, 19748, 6, 27527, 53, 2291, 29763]
次に、enc_strには、トークンIDではなく単語が入りますので、以下のような出力になりました。
['▁', '明日', 'は', '雨が降', 'る', 'だろう', '。']
うーん。まあ学習データが少ないせいかうまく単語で分割できてないですね。
最後に、decには、トークンIDを対応する単語に戻してあげたものが入りますので、出力は以下のように元の文章に戻るはずです。
明日は雨が降るだろう。
終わりに
今回はSentencePieceについて解説しました。
途中、学習したモデルを使って文章の分割を行ったのですが、うまく単語に分割できてなかったですね。少ないデータで学習する場合は、事前にMeCabなどを使って分かち書きしておいたほうがよさそうです。
実際、日本語の文書解析って難しいです。英語ですと、もともと単語間に半角スペース入れるのが普通ですよね。「Thisisadog」ではなくて「This is a dog」と書きますよね。
でも日本語は単語間に区切りがないですし、書き方も人によって異なります。「これは犬です。」「これ犬だよ。」「これは犬よ。」「これ犬。」などたくさんありますので、自然言語処理の世界でも問題になっている点です。
では、これで終わります。最後まで閲覧いただきありがとうございました。
参考文献
GitHub - google/sentencepiece: Unsupervised text tokenizer for Neural Network-based text generation.