はじめに
Sentencepieceが何ものかについては以下の記事がとても参考になりました。
- https://qiita.com/taku910/items/7e52f1e58d0ea6e7859c
- https://anlp.jp/proceedings/annual_meeting/2018/pdf_dir/B1-5.pdf
- https://arxiv.org/pdf/1804.10959.pdf
- https://arxiv.org/pdf/1808.06226.pdf
- https://www.pytry3g.com/entry/how-to-use-sentencepiece
- https://www.pytry3g.com/entry/sentencepiece-and-subword
- https://lang-int.hatenablog.com/entry/2019/04/10/123850
- https://lang-int.hatenablog.com/entry/2019/04/09/111401
- https://lang-int.hatenablog.com/entry/2019/04/13/145539
実際のところSentencepieceを使ってみるとどんな感じなのかの感覚を掴むために、文章分類タスクをSentencepieceとMeCabそれぞれを使ったときの性能比較をしてみました。
扱うデータはlivedoorニュースコーパスとし、ニュースコーパスの本文を9つのカテゴリーに分類するタスクを扱うこととします。
分類モデルのアルゴリズムは単純なLSTMで行うこととします。
実行環境はGoogle Colab Proを使っています。
実装
Sentencepieceの準備
Sentencepieceはpipでインストールできます。
!pip install sentencepiece
まずはSentencepieceのモデルを学習します。以下ではlivedoorニュースコーパスを事前にDataFrameに変換したものを用意したものを使っています。
livedoorデータを学習データ:テストデータ=7:3で分けて、学習データのほうでSentencepieceのトークナイズを学習させます。
# google driveをcolabにマウント
from google.colab import drive
drive.mount('/content/drive')
# 諸々の必要なライブラリをインポートします。
import pickle
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score
import numpy as np
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchtext
from torchtext.legacy.data import Field
from torchtext.legacy.data import TabularDataset
from torchtext.legacy.data import Iterator
from torchtext.data.functional import generate_sp_model
from torchtext.data.functional import load_sp_model
from torchtext.data.functional import sentencepiece_tokenizer
from torchtext.data.functional import sentencepiece_numericalizer
import sentencepiece as spm
from sentencepiece import SentencePieceTrainer
from sentencepiece import SentencePieceProcessor
seed = 1
np.random.seed(seed)
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# livedoorニュースコーパス格納先
drive_dir = '/content/drive/MyDrive/ColabNotebooks/livedoor_data/'
# 事前にDataFrameをpickleで固めたものをロードしています。
with open(drive_dir + 'livedoor_data.pickle', 'rb') as r:
livedoor_df = pickle.load(r)
# データにcategory id列を追加しています。
categories = livedoor_df['category'].unique().tolist()
livedoor_df['category_id'] = livedoor_df['category'].map(lambda x: categories.index(x))
# データからスペースを除外しています。
# Sentencepieceではスペースを '▁' に置き換えますが、日本語はスペースで区切る言語ではないため、前処理の段階で除外しています。
# 妥当な前処理かどうかは正直よくわかっていません。。。
livedoor_df['body'] = livedoor_df['body'].map(lambda x: x.replace(' ', ''))
train_df, test_df = train_test_split(livedoor_df[['body', 'category_id']], train_size=0.7)
print('train size', train_df.shape)
print('test size', test_df.shape)
# train size (5163, 2)
# test size (2213, 2)
train_df.to_csv(drive_dir + 'train.tsv', sep='\t', index=False, header=False)
test_df.to_csv(drive_dir + 'test.tsv', sep='\t', index=False, header=False)
Sentencepieceの学習
Sentencepieceの学習用データは外部ファイルとして保存する必要があるようで、一旦テキストファイルとして保存して、SentencePieceTrainer.Train
で学習させます。今回はとりあえず語彙数は8000を指定しています。このように語彙数を予め指定してデータからその語彙数に収まるようにいい感じにトークナイズを学習してくれるのがSentencepieceの魅力の1つだと思います。MeCabとかで語彙数を指定する場合は、各単語の頻度を算出して、頻度の少ない単語をサブワードにしたりあれこれしないといけないですかね。
# sentencepieceの学習用のデータを準備する
with open(drive_dir + 'sp_corpus.txt', 'w') as w:
w.write('\n'.join(train_df['body'].tolist()))
# Sentencepieceのモデルを構築
# pad idは3から指定可能。0,1,2は予約されています。
# 0 -> <unk>
# 1 -> <s>
# 2 -> </s>
# --character_coverrageは日本語の場合0.9995が推奨される。英語は1.0
# --ad_dummy_predixをTrueにすると、文頭にスペースのトークン'▁'が自動で挿入されますが、日本語を扱うときはFalseを指定したほうが良いのかなと思って、今回はFalseを指定しています。
SentencePieceTrainer.Train(
'--input='+drive_dir+'sp_corpus.txt, --model_prefix='+drive_dir+'livedoor_sentencepiece --character_coverage=0.9995 --vocab_size=8000 --pad_id=3 --add_dummy_prefix=False'
)
ちなみにSentencepieceの学習はtorchtextからも行えます。torchtext.data.functional.generate_sp_model
で上のSentencePieceTrainer.Train
と同じことを実行できるようですが、torchtextからだと、character_coverage
やadd_dummy_prefix
とかを(現時点では)指定できないようで、正直torchtextのSentencepiece用のライブラリは使う必要性はないかなーと思っています。
SentencePieceTrainer.Train
の実行が完了すると、--model_prefix
で指定したファイル名で.model
と.vocab
ファイルが作成されます。.vocab
ファイルのほうはSentencepieceで学習された単語を確認することができます。(行数が単語ID)
.model
のほうのファイルをSentencePieceProcessor
で読み込むことでSentencepieceのトークナイズを行うことができるようになります。
# Sentencepieceのモデルをロード
# .modelのファイルを指定します。
sp = SentencePieceProcessor(model_file=drive_dir+'livedoor_sentencepiece.model')
# 以下のようにロードしてもOK
# sp = SentencePieceProcessor()
# sp.Load(drive_dir + 'livedoor_sentencepiece.model')
Sentencepieceでトークナイズ
トークナイズはencode
で行うことができます。試しに3文を分割してみます。out_type=str
を指定すれば、トークンIDではなく、文字列で表示されます。
# 学習されたSentencepieceで文章を分割してみます。
# テストしてみる
# 0は<unk>トークン
text = ['私は唐揚げに檸檬をかけない派です。😅',
'人工知能は人間の仕事を奪った。',
'私は「ゼロから始めるスマートフォン」を毎日購読しています。']
print(sp.encode(text))
#[[1438, 7100, 7067, 546, 10, 0, 8, 910, 67, 1382, 62, 6, 0],
# [68, 1460, 322, 1881, 9, 845, 4, 2786, 3538, 102, 6],
# [1438, 13, 1146, 11, 8, 780, 7981, 1757, 459, 6]]
# out_type=strを指定すれば分割文字列が確認できる
print(sp.encode(text, out_type=str))
# [['私は', '唐', '揚', 'げ', 'に', '檸檬', 'を', 'かけ', 'ない', '派', 'です', '。', '😅'],
# ['人', '工', '知', '能', 'は', '人間', 'の', '仕事を', '奪', 'った', '。'],
# ['私は', '「', 'ゼロから始めるスマートフォン', '」', 'を', '毎日', '購', '読', 'しています', '。']]
ここでちょっとポイントなのが、上の分割の結果を見ると、「ゼロから始めるスマートフォン」が1つのトークンとして認識されています。これは別に辞書として指定したわけでもないのに、Sentencepieceが学習の段階で1つのトークンにしよう、と学習した結果です。Sentencepieceの学習データに「ゼロから始めるスマートフォン」という文字列が多く含まれていたために、「ゼロから始めるスマートフォン」は1つのトークンにしたほうがよい、とSentencepieceが学習してくれたんですね。MeCabとかだと、予め辞書を作成しておかないと、「ゼロから始めるスマートフォン」が1つのトークンになることはないですね。
参考(Sentencepieceを使ってデータの水増しを行う)
参考文献として挙げたこちらの記事にもあるようにSentencepieceを使うと、データの水増しができるようです。方法としてはSampleEncodeAsPieces
を使って、Sentencepieceのトークナイズの結果を確率的に変動させることができます。
text = '自然言語処理をする上で、データを増やす作業はとても大変なことなんです。'
for _ in range(3):
print(sp.SampleEncodeAsPieces(text, nbest_size=5, alpha=0.1))
#['自然', '言', '語', '処理', 'を', 'する', '上', 'で', '、', 'データ', 'を', '増', 'や', 'す', '作業', 'は', 'とても', '大変', 'な', 'こと', 'なんです', '。']
#['自然', '言', '語', '処理', 'をする', '上で', '、', 'データ', 'を', '増', 'や', 'す', '作業', 'は', 'とても', '大変', 'な', 'こと', 'なんです', '。']
#['自然', '言', '語', '処理', 'をする', '上', 'で', '、', 'データ', 'を', '増', 'や', 'す', '作業', 'は', 'とても', '大変', 'な', 'こと', 'なんです', '。']
torchtextでDataLoader作成
上で学習したSentencepieceのトークナイザーを使って、torhctextでDataLoaderを作成します。
Field
のtokenize
に指定しているsp.encode
はSentencepieceでトークン化(ID化)まで行っているので、use_vocab=False
を指定しています。vocabularyの作成(単語のID化)はSentencepiece側で行っているので、torchtextでは行っていません。
# tokenizeが既に数値化されたものになっているので、use_vocab=Falseにしている
TEXT = Field(sequential=True, tokenize=sp.encode, lower=False, pad_first=True, use_vocab=False,
include_lengths=True, batch_first=True, pad_token=3, unk_token=0)
LABEL = Field(sequential=False, use_vocab=False)
train_data, test_data = TabularDataset.splits(
path=drive_dir, train='train.tsv', test='test.tsv', format='tsv', fields=[('Text', TEXT), ('Label', LABEL)])
BATCH_SIZE = 64
train_loader = Iterator(train_data, batch_size=BATCH_SIZE, train=True)
test_loader = Iterator(test_data, batch_size=BATCH_SIZE, train=False, sort=False)
分類用のモデル定義
以下のような単純なLSTMのネットワークでクラス分類を行うこととします。
class LSTMNet(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, padding_idx):
super(LSTMNet, self).__init__()
# 単語分散表現はランダムベクトルを使う
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=padding_idx)
self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
self.linear = nn.Linear(hidden_dim, 9)
def forward(self, input_ids):
embeds = self.word_embeddings(input_ids)
_, vec = self.lstm(embeds)
vec = vec[0]
vec = self.linear(vec)
vec = vec.squeeze(0)
return vec
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Sentencepieceのvocab sizeはGetPieceSize()で取得できます。
# 学習の際、語彙数は8000を指定していたので、当然sp.GetPieceSize()=8000です。
VOCAB_SIZE = sp.GetPieceSize()
EMBEDDING_DIM = 200
HIDDEN_DIM = 128
PAD_ID = sp.PieceToId('<pad>') # 3
net = LSTMNet(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_DIM, padding_idx=PAD_ID)
net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
ネットワークの学習&精度確認
とりあえず30エポックほど回してみます。エポック毎に学習データ、テストデータの精度(f1-score)を算出しています。ついでに処理時間も計算しときます。
%%time
sp_train_losses = []
sp_test_losses = []
sp_train_fscores = []
sp_test_fscores = []
for epoch in range(30):
# 学習
train_loss = 0.0
train_predict = []
train_answer = []
net.train()
for batch in train_loader:
optimizer.zero_grad()
input_ids = batch.Text[0].to(device)
y = batch.Label.to(device)
out = net(input_ids)
loss = criterion(out, y)
train_loss += loss.item()
loss.backward()
optimizer.step()
train_predict += out.argmax(dim=1).cpu().detach().numpy().tolist()
train_answer += y.cpu().detach().numpy().tolist()
sp_train_losses.append(train_loss)
train_fscore = f1_score(train_answer, train_predict, average='macro')
sp_train_fscores.append(train_fscore)
# テスト
test_loss = 0.0
test_predict = []
test_answer = []
net.eval()
with torch.no_grad():
for batch in test_loader:
input_ids = batch.Text[0].to(device)
y = batch.Label.to(device)
out = net(input_ids)
loss = criterion(out, y)
test_loss += loss.item()
test_predict += out.argmax(dim=1).cpu().detach().numpy().tolist()
test_answer += y.cpu().detach().numpy().tolist()
sp_test_losses.append(test_loss)
test_fscore = f1_score(test_answer, test_predict, average='macro')
sp_test_fscores.append(test_fscore)
print('epoch', epoch,
'\ttrain loss', round(train_loss, 4), '\ttrain fscore', round(train_fscore, 4),
'\ttest loss', round(test_loss, 4), '\ttest fscore', round(test_fscore, 4)
)
# CPU times: user 4min 54s, sys: 52 s, total: 5min 46s
# Wall time: 5min 44s
処理時間は5分46秒
epoch毎の損失、F1-scoreはこちら
epoch 0 train loss 120.4921 train fscore 0.5271 test loss 38.3847 test fscore 0.6362
epoch 1 train loss 68.6138 train fscore 0.7205 test loss 28.744 test fscore 0.7136
epoch 2 train loss 52.5799 train fscore 0.7881 test loss 26.0421 test fscore 0.7397
epoch 3 train loss 38.2132 train fscore 0.8503 test loss 24.5308 test fscore 0.765
epoch 4 train loss 26.7084 train fscore 0.9012 test loss 24.2424 test fscore 0.7579
epoch 5 train loss 16.3464 train fscore 0.9429 test loss 22.5958 test fscore 0.7832
epoch 6 train loss 9.4959 train fscore 0.9711 test loss 22.3193 test fscore 0.7967
epoch 7 train loss 5.5236 train fscore 0.9885 test loss 22.475 test fscore 0.801
epoch 8 train loss 3.4832 train fscore 0.9935 test loss 23.4526 test fscore 0.7948
epoch 9 train loss 2.0342 train fscore 0.9966 test loss 23.4369 test fscore 0.7999
epoch 10 train loss 1.7133 train fscore 0.9966 test loss 24.5152 test fscore 0.7997
epoch 11 train loss 1.1556 train fscore 0.9983 test loss 24.6399 test fscore 0.8107
epoch 12 train loss 0.7154 train fscore 0.9987 test loss 25.3319 test fscore 0.8102
epoch 13 train loss 0.6672 train fscore 0.9986 test loss 26.6143 test fscore 0.8068
epoch 14 train loss 0.4661 train fscore 0.9991 test loss 25.8841 test fscore 0.8147
epoch 15 train loss 0.4777 train fscore 0.9988 test loss 26.6295 test fscore 0.8134
epoch 16 train loss 1.5553 train fscore 0.9951 test loss 27.349 test fscore 0.8069
epoch 17 train loss 1.178 train fscore 0.9957 test loss 26.4447 test fscore 0.8005
epoch 18 train loss 0.8552 train fscore 0.9984 test loss 25.9991 test fscore 0.8123
epoch 19 train loss 0.3411 train fscore 0.9991 test loss 26.0604 test fscore 0.8156
epoch 20 train loss 0.2808 train fscore 0.999 test loss 26.54 test fscore 0.8199
epoch 21 train loss 0.3581 train fscore 0.9989 test loss 27.4113 test fscore 0.815
epoch 22 train loss 0.2389 train fscore 0.999 test loss 27.2458 test fscore 0.8198
epoch 23 train loss 0.1964 train fscore 0.9994 test loss 27.4656 test fscore 0.8208
epoch 24 train loss 0.197 train fscore 0.9994 test loss 27.7908 test fscore 0.8211
epoch 25 train loss 0.1781 train fscore 0.9992 test loss 27.8535 test fscore 0.8262
epoch 26 train loss 0.1669 train fscore 0.9992 test loss 28.1779 test fscore 0.8222
epoch 27 train loss 0.1629 train fscore 0.9991 test loss 28.3673 test fscore 0.823
epoch 28 train loss 0.1592 train fscore 0.9994 test loss 28.4181 test fscore 0.8265
epoch 29 train loss 0.1626 train fscore 0.9992 test loss 28.7844 test fscore 0.8272
MeCab側でもモデルを構築して学習させる
MeCab側でも同様の処理でDataLoaderを作成し、単純なLSTMのネットワークで学習&精度検証します。
Sentencepieceは語彙数を抑えられることがメリットの1つだと思うので、MeCabでは、torchtextで語彙を作成する際、min_freq=1
をあえて設定しております。MeCabだと語彙数が大量にできてしまうことを確認するためです。
pipでインストール
!pip install mecab-python3
!pip install unidic-lite
DataLoader作成まで
MeCabだと語彙数は61628になりました。
import MeCab
tagger = MeCab.Tagger("-Owakati")
def mecab_tokenizer(sentence):
sentence = tagger.parse(sentence)
wakati = sentence.split(" ")
wakati = list(filter(("").__ne__, wakati))
wakati = wakati[:-1]
return wakati
TEXT = Field(sequential=True, tokenize=mecab_tokenizer, lower=False, pad_first=True,
include_lengths=True, batch_first=True, pad_token='<pad>', unk_token='<unk>')
LABEL = Field(sequential=False, use_vocab=False)
train_data, test_data = TabularDataset.splits(
path=drive_dir, train='train.tsv', test='test.tsv', format='tsv', fields=[('Text', TEXT), ('Label', LABEL)])
TEXT.build_vocab(train_data, min_freq=1)
BATCH_SIZE = 64
train_loader = Iterator(train_data, batch_size=BATCH_SIZE, train=True)
test_loader = Iterator(test_data, batch_size=BATCH_SIZE, train=False, sort=False)
class LSTMNet(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, padding_idx):
super(LSTMNet, self).__init__()
# 単語分散表現はランダムベクトルを使う
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=padding_idx)
self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
self.linear = nn.Linear(hidden_dim, 9)
def forward(self, input_ids):
embeds = self.word_embeddings(input_ids)
_, vec = self.lstm(embeds)
vec = vec[0]
vec = self.linear(vec)
vec = vec.squeeze(0)
return vec
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
VOCAB_SIZE = len(TEXT.vocab.stoi)
EMBEDDING_DIM=200
HIDDEN_DIM=128
PAD_ID = TEXT.vocab.stoi['<pad>']
net = LSTMNet(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_DIM, padding_idx=PAD_ID)
net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
学習&精度確認
こちらはSentencepieceのときと全く同じなので閉じておきます。
%%time
mecab_train_losses = []
mecab_test_losses = []
mecab_train_fscores = []
mecab_test_fscores = []
for epoch in range(30):
train_loss = 0.0
train_predict = []
train_answer = []
net.train()
for batch in train_loader:
optimizer.zero_grad()
input_ids = batch.Text[0].to(device)
y = batch.Label.to(device)
out = net(input_ids)
loss = criterion(out, y)
train_loss += loss.item()
loss.backward()
optimizer.step()
train_predict += out.argmax(dim=1).cpu().detach().numpy().tolist()
train_answer += y.cpu().detach().numpy().tolist()
mecab_train_losses.append(train_loss)
train_fscore = f1_score(train_answer, train_predict, average='macro')
mecab_train_fscores.append(train_fscore)
test_loss = 0.0
test_predict = []
test_answer = []
net.eval()
with torch.no_grad():
for batch in test_loader:
input_ids = batch.Text[0].to(device)
y = batch.Label.to(device)
out = net(input_ids)
loss = criterion(out, y)
test_loss += loss.item()
test_predict += out.argmax(dim=1).cpu().detach().numpy().tolist()
test_answer += y.cpu().detach().numpy().tolist()
mecab_test_losses.append(test_loss)
test_fscore = f1_score(test_answer, test_predict, average='macro')
mecab_test_fscores.append(test_fscore)
print('epoch', epoch, '\ttrain loss', round(train_loss, 4), '\ttrain fscore', round(train_fscore, 4),
'\ttest loss', round(test_loss, 4), '\ttest fscore', round(test_fscore, 4))
# CPU times: user 6min 4s, sys: 54.1 s, total: 6min 58s
# Wall time: 6min 56s
epoch毎の損失、F1-scoreはこちら
epoch 0 train loss 117.5845 train fscore 0.5344 test loss 35.3057 test fscore 0.6518
epoch 1 train loss 61.8279 train fscore 0.7427 test loss 24.6747 test fscore 0.7533
epoch 2 train loss 41.2987 train fscore 0.8371 test loss 19.8909 test fscore 0.7967
epoch 3 train loss 28.1072 train fscore 0.8884 test loss 19.5073 test fscore 0.8095
epoch 4 train loss 20.2023 train fscore 0.919 test loss 17.8201 test fscore 0.8322
epoch 5 train loss 12.1747 train fscore 0.9583 test loss 15.3678 test fscore 0.8528
epoch 6 train loss 12.1873 train fscore 0.956 test loss 15.5709 test fscore 0.8488
epoch 7 train loss 6.028 train fscore 0.9814 test loss 15.5984 test fscore 0.8609
epoch 8 train loss 3.6533 train fscore 0.9914 test loss 15.6728 test fscore 0.8608
epoch 9 train loss 2.2723 train fscore 0.9946 test loss 15.6894 test fscore 0.8725
epoch 10 train loss 1.5772 train fscore 0.9963 test loss 16.4935 test fscore 0.8731
epoch 11 train loss 12.7076 train fscore 0.9538 test loss 19.1291 test fscore 0.8326
epoch 12 train loss 5.5746 train fscore 0.9834 test loss 17.8947 test fscore 0.848
epoch 13 train loss 2.8394 train fscore 0.9927 test loss 16.9568 test fscore 0.8573
epoch 14 train loss 1.3217 train fscore 0.9971 test loss 17.2715 test fscore 0.8592
epoch 15 train loss 0.9888 train fscore 0.9978 test loss 17.8214 test fscore 0.8649
epoch 16 train loss 0.7672 train fscore 0.9984 test loss 18.3252 test fscore 0.8599
epoch 17 train loss 0.5628 train fscore 0.9986 test loss 18.5536 test fscore 0.8638
epoch 18 train loss 4.9127 train fscore 0.9843 test loss 18.734 test fscore 0.8581
epoch 19 train loss 1.1717 train fscore 0.997 test loss 19.0019 test fscore 0.8592
epoch 20 train loss 0.7327 train fscore 0.998 test loss 19.0872 test fscore 0.8682
epoch 21 train loss 0.6748 train fscore 0.9982 test loss 19.361 test fscore 0.8682
epoch 22 train loss 0.4693 train fscore 0.9988 test loss 20.3306 test fscore 0.8646
epoch 23 train loss 0.3393 train fscore 0.999 test loss 20.4864 test fscore 0.8679
epoch 24 train loss 0.27 train fscore 0.9992 test loss 20.7215 test fscore 0.8694
epoch 25 train loss 0.2346 train fscore 0.9991 test loss 21.0146 test fscore 0.868
epoch 26 train loss 0.2059 train fscore 0.9994 test loss 21.1525 test fscore 0.8702
epoch 27 train loss 0.2 train fscore 0.9991 test loss 21.4466 test fscore 0.8706
epoch 28 train loss 0.1753 train fscore 0.999 test loss 21.5888 test fscore 0.8686
epoch 29 train loss 0.1778 train fscore 0.9992 test loss 21.5458 test fscore 0.8722
SentencepieceとMeCabの結果比較
エポック毎における精度の推移はそれぞれのモデルで以下のようになりました。
import matplotlib.pyplot as plt
plt.figure(figsize=(15,5))
plt.subplot(1,2,1)
plt.plot(sp_train_losses, label='sp train loss')
plt.plot(sp_test_losses, label='sp test loss')
plt.plot(mecab_train_losses, label='mecab train loss')
plt.plot(mecab_test_losses, label='mecab test loss')
plt.title('loss')
plt.legend()
plt.grid()
plt.subplot(1,2,2)
plt.plot(sp_train_fscores, label='sp train fscore')
plt.plot(sp_test_fscores, label='sp test fscore')
plt.plot(mecab_train_fscores, label='mecab train fscore')
plt.plot(mecab_test_fscores, label='mecab test fscore')
plt.title('fscore')
plt.legend()
plt.grid()
plt.show()
精度はMeCabのほうが良いですね。MeCabだとテストデータでF1スコア0.8後半くらいまで言ってますが、Sentencepiece側だと0.8ちょいって感じ。
これはデータにもよると思うのですが、今回のlivedoorデータってクラス分類する上で特徴的な固有名詞が多いような気がするので、そういった特徴語を1つのトークンとして扱えている(であろう)MeCabのほうに軍配が上がったのだと思います。
とはいえ、Sentencepieceのメリットは別に精度だけじゃなくて、リソースを抑えることができる、とか早いとか色々あるので、その辺の数値も見てみます。
Sentencepiece | MeCab | |
---|---|---|
精度(epoch30時点) | 0.8272 | 0.8722 |
学習データにおける語彙数 | 8,000 | 61,628 |
テストデータにおける未知語数 | 1,687 | 15,367 |
学習&検証の処理時間 | 5min 46s | 6min 58s |
パラメータ数 | 1,770,121 | 12,495,721 |
パラメータ数の確認の仕方
params = 0
for p in net.parameters():
if p.requires_grad:
params += p.numel()
print('parameters', params)
Sentencepieceのほうが語彙数を抑えられて、処理も早くて、未知語の数やパラメータ数なんかは10分の1くらいになってます。精度だけ見たら、今回のデータではMeCab優勢ですが、BERTのような巨大なモデルを扱う上ではSentencepieceなどが優位になったりするんでしょう。
(テストデータにおける未知語ってこんなに多いものなのか。。。)
おわりに
いろいろとSentencepieceを触ってみて、なんとなく使い方はわかりましたが、やはり詳しく知るには論文を読むしかなさそうです。
論文読むか〜。。。
おわり