どん底から這い上がるまでの記録

どん底から這い上がりたいけど這い上がれない人がいろいろ書くブログ(主にプログラミング)

SentencePieceを使ってみた。

 

以前、SentencePieceについて記事を書きましたが、

www.pytry3g.com

 

今回は実際にSentencePieceを使ってWikipediaのデータから単語分割モデルを作ってみたいと思います。

 

この記事を見ればできるようになること。

  • 単語分割モデルの作り方
  • 単語分割モデルの使い方

 

 

Google Colaboratory

今回はGoogle Colaboratoryで使って作業を進めていきます。

Google Colabratoryって何?って方は下の記事を参考にしてください。

関連記事 - Google Colaboratoryを使ってAIプログラミングを始めよう

 

SentencePieceで単語分割モデルを作る際に使用するデータはファイル形式である必要があるので、まずはGoogle Driveにあるファイルを読み込めように以下を実行します。

from google.colab import drive
drive.mount('/content/drive')

Google Colaboratoryを開き、セルに上記を入力して実行してください。

関連記事 - Google ColaboratoryでGoogle Driveにあるファイルを読み書きする方法

 

次に、現在開いているNotebookがあるフォルダに移動します。

cd "drive/My Drive/Colab Notebooks"

私はNotebookがあるフォルダにWikipediaからダウンロードしたデータとSentencePieceから作る単語分割モデルを置くことにします。(※ここは各自のお好みで。)

 

SentencePieceのインストール。

!pip install sentencepiece > /dev/null

 

BeautifulSoupを使ってWikipediaのデータをダウンロードします。

今回は戦国武将の記事をいくつかダウンロードしました。

import codecs
import re
import requests
from bs4 import BeautifulSoup

url = "https://ja.wikipedia.org/wiki/"
keyword_list = [
    "織田信長", "徳川家康", "豊臣秀吉",  
    "伊達政宗", "武田信玄", "上杉謙信",
    "明智光秀", "島津義弘", "北条氏康",
    "長宗我部元親", "毛利元就", "真田幸村",
    "立花宗茂", "石田三成", "浅井長政"
    
]


corpus = []
for keyword in keyword_list:
    # 戦国大名の記事をダウンロード
    response = requests.get(url + keyword)
    # htmlに変換
    html = response.text
    
    soup = BeautifulSoup(html, 'lxml')
    for p_tag in soup.find_all('p'):
        text = "".join(p_tag.text.strip().split(" "))
        if len(text) == 0:
            continue
        # e.g. [注釈9] -> ''
        text = re.sub(r"\[注釈[0-9]+\]", "", text)
        # e.g. [注9] -> ''
        text = re.sub(r"\[注[0-9]+\]", "", text)
        # e.g. [20] -> ''
        text = re.sub(r"\[[0-9]+\]", "", text)
        corpus.append(text)
        
print(*corpus, sep="\n", file=codecs.open("wiki.txt", "w", "utf-8"))

関連記事 - BeautifulSoupを使ってWikipediaのテキストを抽出する

 

上記のプログラムを実行するとwiki.txtに改行区切りでデータが書き込まれます。

print(corpus[0])
#織田信長(おだのぶなが、1534年-1582年)は、戦国時代から安土桃山時代にかけての武将、戦国大名、天下人。

単語分割モデルの学習

データの用意ができたので、単語分割モデルの学習をしていきます。

単語分割モデルの学習はライブラリのインポートをして、sentencepiece.SentencePieceTrainer.Train()で必要な設定をするだけでできます。特に正規化や前処理などは必要ありません。

学習が完了すれば、--model_prefixで指定した名前がついたモデル名.modelとモデル名.vocabが生成されます。

import sentencepiece as spm


spm.SentencePieceTrainer.Train(
    '--input=wiki.txt, --model_prefix=sentencepiece --character_coverage=0.9995 --vocab_size=8000 --pad_id=3'
)

学習の際に設定するパラメータは以下になります。

--input学習データが入ったファイル(ひとつの文が1行になっている)

--model_prefixモデル名、モデル名.modelとモデル名.vocabが生成される。

--vocab_size語彙数(SentencePieceの開発者の方によると、翻訳の場合は小規模データなら数千から10k程度、超大規模でも32k程度で高い精度が得られるとのこと。)

--character_coverageモデルがカバーする文字の量、日本語の場合は0.9995にするのが推奨されている。

--model_typeデフォルトではunigram、他にはbpecharwordがある。ただし、wordのときは予めテキストを単語に分割する必要がある。

 

また、今回は--pad_id=3に設定しています。言語モデルやSeq2seqを学習する際に入力データの長さを揃えるために使われるpadを今回単語分割モデルの単語リストに入れました。

SentencePieceでは他に区切り文字として使われる<s></s>がデフォルトで単語分割モデルに組み込まれています。

単語分割モデルの読み込み

学習した単語分割モデルを読み込むには、

import sentencepiece as spm

sp = spm.SentencePieceProcessor()
sp.Load("sentencepiece.model")

このようにします。

 

SentencePieceは作成した単語分割モデルからテキストを単語ID列に変換したり、単語ID列からテキストに変換などができます。

EncodeAsPieces (テキストを単語に分割)

#corpus[70]
#元亀3年(1572年)3月、三好義継・松永久秀らが共謀して信長に敵対した。
print(sp.EncodeAsPieces(corpus[70]))
#['▁元亀', '3', '年', '(157', '2', '年', ')3', '月', '、', '三好義継', '・', '松永久秀', 'らが', '共', '謀', 'して', '信長に', '敵対し', 'た', '。']

 

EncodeAsIds (テキストを単語ID列に変換)

#corpus[70]
#元亀3年(1572年)3月、三好義継・松永久秀らが共謀して信長に敵対した。
print(sp.EncodeAsIds(corpus[70]))
#[1091, 33, 11, 85, 32, 11, 419, 17, 4, 3289, 12, 1991, 780, 2725, 2147, 39, 511, 1500, 19, 6]

 

DecodePieces (単語列からテキストに変換)

tokens = ['松永久秀', 'らが', '共', '謀', 'して', '秀吉に', '敵対し', 'た', '。']
print(sp.DecodePieces(tokens))
#松永久秀らが共謀して秀吉に敵対した。

 

DecodeIds (単語ID列からテキストに変換)

ids = sp.EncodeAsIds(corpus[70])
print(sp.DecodeIds(ids))
#元亀3年(1572年)3月、三好義継・松永久秀らが共謀して信長に敵対した。

 

GetPieceSize (語彙数)

print(sp.GetPieceSize())
#8000
#len(sp)でも語彙数が取得できる。

 

PieceToId (単語IDの取得)

print(sp.PieceToId('</s>'))
#sp['</s>']でも単語IDが取得できる。

 

おまけ

戦国武将のWikipediaのデータと関係なさそうな文をSentencePieceの単語分割モデルに渡すと、

for i in sp.EncodeAsIds("セネガル勝ってるよ"):
    print(i, sp.IdToPiece(i))
"""
 出力結果
    16 ▁
    3899 セ
    7500 ネ
    3741 ガ
    867 ル
    187 勝
    137 って
    27 る
    824 よ
"""

単語(文字)に分割できた。

 

ちなみに、

for i in sp.EncodeAsIds("本田圭佑勝ってるよ"):
    print(i, sp.IdToPiece(i))
"""
 出力結果
    16 ▁
    106 本
    130 田
    7041 圭
    0 <unk>
    187 勝
    137 って
    27 る
    824 よ
"""

圭佑のが今回単語分割モデルの学習に使ったデータに含まれていなかったので、未知語<unk>に変換されました。

MeCabを使うと

MeCabを使って今回用意したWikipediaのデータを形態素解析すると、語彙数がどれくらいになるのか調べてみました。

import codecs
import MeCab

data = codecs.open('wiki.txt', 'r', 'utf-8').readlines()

tagger = MeCab.Tagger("-Owakati")
def tokenizer(sentence):
    # sentence : "今日はいい天気ですね。"
    # return : ['今日', 'は', 'いい', '天気', 'です', 'ね', '。']
    sentence = sentence.strip()
    return tagger.parse(sentence).strip().split()


word2id = {}
for sent in data:
    tokens = tokenizer(sent)
    for token in tokens:
        if token in word2id:
            continue
        word2id[token] = len(word2id)
print(len(word2id))
#11901

結果は11901になりました。

学習データが増えれば増えるほど語彙数もそれに伴い増えると思うので、語彙数を抑えることができるSentencePieceのほうが使い勝手が良さそうです。

エラーが出るとき

私がSentencePieceを使っていて2つエラーが出たのでその時の対処法を残しておきます。

① Increase vocab_size or decrease chacter_coverage

import sentencepiece as spm

spm.SentencePieceTrainer.Train(
    '--input=wiki.txt --model_prefix=sentencepiece --character_coverage=0.9995 --vocab_size=500'
)

単語分割モデルを作成するために上記のプログラムを実行したら以下のようなエラーが出ました。

RuntimeError: Internal: /sentencepiece/src/trainer_interface.cc(427) [(static_cast(required_chars_.size() + meta_pieces_.size())) <= (trainer_spec_.vocab_size())] Vocabulary size is smaller than required_chars. 500 vs 1236. Increase vocab_size or decrease character_coverage with --character_coverage option.

エラーで書かれている内容をそのまんま実行するだけですね。私の場合、設定した語彙数が500で500 vs 1236とエラーの内容の中に書かれていたので、語彙数を1236以上に設定するだけで解決しました。

② (trainer_spec_.vocab_size()) == (model_proto->pieces_size())

import sentencepiece as spm

spm.SentencePieceTrainer.Train(
    '--input=wiki.txt --model_prefix=sentencepiece --character_coverage=0.9995 --vocab_size=8000'
)

語彙数を8000に設定したときに以下のようなエラーが出ました。

RuntimeError: Internal: /sentencepiece/src/trainer_interface.cc(498) [(trainer_spec_.vocab_size()) == (model_proto->pieces_size())] 

このようなエラーが出たときはsentencepiece.SentencePieceTrainer.Train()の設定で--hard_vocab_limit=falseを加えると解決しました。

spm.SentencePieceTrainer.Train(
    '--input=tweet.txt --model_prefix=sentencepiece --character_coverage=0.9995 --vocab_size=8000 --hard_vocab_limit=false'
)

おわりに

SentencePieceを使ってみましたが、まだパラメータの設定やMeCabを使った形態素解析をした場合との違いについて完全に理解できたわけではないので、今後言語モデルやSeq2seqで使ってみて理解を深めていこうと思います。

参考ページ - GitHub - google/sentencepiece: Unsupervised text tokenizer for Neural Network-based text generation.