文章分類タスクでSentencepieceとMeCabを比較してみた

# はじめに

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のトークナイズを学習させます。

```python
# 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とかで語彙数を指定する場合は、各単語の頻度を算出して、頻度の少ない単語をサブワードにしたりあれこれしないといけないですかね。

```python

# 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 -> 

# 2 -> 

# --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用のライブラリは使う必要性はないかなーと思っています。

- https://pytorch.org/text/stable/data_functional.html

`SentencePieceTrainer.Train`の実行が完了すると、`--model_prefix`で指定したファイル名で`.model`と`.vocab`ファイルが作成されます。`.vocab`ファイルのほうはSentencepieceで学習された単語を確認することができます。（行数が単語ID）
`.model`のほうのファイルを`SentencePieceProcessor`で読み込むことでSentencepieceのトークナイズを行うことができるようになります。

```python
# 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ではなく、文字列で表示されます。

```python
# 学習された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を使ってデータの水増しを行う）
参考文献として挙げた[こちら](https://lang-int.hatenablog.com/entry/2019/04/13/145539)の記事にもあるようにSentencepieceを使うと、データの水増しができるようです。方法としては`SampleEncodeAsPieces`を使って、Sentencepieceのトークナイズの結果を確率的に変動させることができます。

```python
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では行っていません。

```python
# 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のネットワークでクラス分類を行うこととします。

```python
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)を算出しています。ついでに処理時間も計算しときます。

```python
%%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秒

<details>
<summary>epoch毎の損失、F1-scoreはこちら</summary>
<div>

```
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
```

</div>
</details>


## MeCab側でもモデルを構築して学習させる

MeCab側でも同様の処理でDataLoaderを作成し、単純なLSTMのネットワークで学習＆精度検証します。

Sentencepieceは語彙数を抑えられることがメリットの1つだと思うので、MeCabでは、torchtextで語彙を作成する際、`min_freq=1`をあえて設定しております。MeCabだと語彙数が大量にできてしまうことを確認するためです。

### pipでインストール

```
!pip install mecab-python3
!pip install unidic-lite
```


### DataLoader作成まで

MeCabだと語彙数は61628になりました。

```python
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)
```

### 学習＆精度確認

<details>
<summary>こちらはSentencepieceのときと全く同じなので閉じておきます。</summary>
<div>

```python
%%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
```
</div>
</details>


<details>
<summary>epoch毎の損失、F1-scoreはこちら</summary>
<div>


```
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
```

</div>
</details>

# SentencepieceとMeCabの結果比較

エポック毎における精度の推移はそれぞれのモデルで以下のようになりました。

```python
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()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/78312fe4-8eda-bb9a-5839-5a45369298ff.png)

精度は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|

<details>
<summary>パラメータ数の確認の仕方</summary>
<div>

```python
params = 0
for p in net.parameters():
    if p.requires_grad:
        params += p.numel()
print('parameters', params)
```

</div>
</details>


Sentencepieceのほうが語彙数を抑えられて、処理も早くて、未知語の数やパラメータ数なんかは10分の1くらいになってます。精度だけ見たら、今回のデータではMeCab優勢ですが、BERTのような巨大なモデルを扱う上ではSentencepieceなどが優位になったりするんでしょう。
（テストデータにおける未知語ってこんなに多いものなのか。。。）


# おわりに

いろいろとSentencepieceを触ってみて、なんとなく使い方はわかりましたが、やはり詳しく知るには論文を読むしかなさそうです。
論文読むか〜。。。

おわり