Pytorchで日本語テキスト分類をSentencePiece & LSTMで実施
前回Pytorch公式サイトのテキスト分類というかニュース分類を全結合のモデルでやってみましたが、自然言語処理なら一応RNN(今はBertとかもっと最新のモデルが出てますね。。)、ということでLSTMをモデルに変更して試してみます。
前回記事
またこちらの2つのブログにインスパイアされた記事ですので、ご確認ください。とても参考になります。
またこちらは公式Pytorchチュートリアルです。
https://pytorch.org/tutorials/beginner/nlp/sequence_models_tutorial.html?highlight=lstm
ざっくりとした構成としては、こんなですね。
・データセットにライブドアニュースコーパスのタイトルだけをデータセット化
・SentencePieceを用いてベクトル化下準備
・LSTMにかけてみてタイトルからニュースの分類がどこまでできるか試す
いまいちやり切れてないのは、訓練がバッチ化されてるようでされてないのですが(batch_size=1で決め打ち)、可変長データのバッチ化がいまいちよく出来てませんので、ご容赦ください。
SentencePieceを使う理由は単純にpipだけでインストールが完結するためだけです。MeCabは多少インストール作業が必要ですね。
また分類の9種類はこちらのニュースの出どころ?が分類区分ですね。
CATEGORIES=['movie-enter', 'it-life-hack', 'kaden-channel', 'topic-news', 'livedoor-homme', 'peachy', 'sports-watch', 'dokujo-tsushin', 'smax']
データセット作成
m__kさんの記事を参考にライブドアニュースをダウンロードしてテキストファイルの3行目に記載のタイトルを取ってきます。シャッフルしてCSV化するとこんなになりますかね。
livedoor_news_title.csv
category,title
smax,Interop Tokyo 2012:デジタルサイネージジャパンにてセラクがAndroidを搭載した鏡付き洗面台「スマート洗面台」を出展【レポート】
livedoor-homme,プロドライバーが伝授するデートで使えるドライビングテクニック
peachy,インタビュー:植松晃士さん「黒目の印象を変えて、脱“変わり映えのしない女”」
7000行ぐらいデータができますね。ちょっとCPUとバッチ化されてないプログラムでは処理が厳しいですので、これから適当に1000行を訓練用、300行程度を検証用に手動で分けるとします。
livedoor_news_title_train.csv →訓練1000件
livedoor_news_title_test.csv →検証用300件
SentencePiece
次に livedoor_news_title.csv をSentencePieceにかけてベクトル化の下準備をしてみます。SentencePieceのインストールは以下一文でOKですね。
pip install sentencepiece
livedoor_news_title.csv にはカテゴリ情報も入っているので余裕があれば抜いたほうがよいですが、ここでは実験なのでそのままSentencePieceにかけちゃいます。
#ldn_spm.py
import sentencepiece as spm
spm.SentencePieceTrainer.Train(
'--input=livedoor_news_title.csv, --model_prefix=sentencepiece_livedoor --character_coverage=0.9995 --vocab_size=8000'
)
$ python ldn_spm.py
2つファイルができると思います。
・sentencepiece_livedoor.model
・sentencepiece_livedoor.vocab
モデルとデータセット処理
モデルはこんなですね。Embedding->LSTM->全結合です。LSTMの戻り値のどれを使うのかが多少難しいですね(forwardの部分)。
#ldn_model.py
import torch
import torch.nn as nn
import torch.nn.functional as F
class LSTMTagger(nn.Module):
def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
super(LSTMTagger, self).__init__()
self.hidden_dim = hidden_dim
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
# The LSTM takes word embeddings as inputs, and outputs hidden states
# with dimensionality hidden_dim.
self.lstm = nn.LSTM(embedding_dim, hidden_dim)
# The linear layer that maps from hidden state space to tag space
self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
def forward(self, sentence):
embeds = self.word_embeddings(sentence)
_, lstm_out = self.lstm(embeds.view(len(sentence), 1, -1))
tag_space = self.hidden2tag(lstm_out[0].view(-1, self.hidden_dim))
tag_scores = F.log_softmax(tag_space, dim=1)
return tag_scores
データセットクラスを継承してカスタムデータセットクラスを作ってます。ここで先ほど作ったSentencePieceのモデルを読み込んで対象のタイトル文字列をベクトル化してます。
title = self.sp.EncodeAsIds(title)
の部分ですね。バッチ化がショボいですので、batch_size=1 にしか対応してません。。
#ldn_dataset.py
import pandas as pd
import torch
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
import sentencepiece as spm
CATEGORIES=['movie-enter', 'it-life-hack', 'kaden-channel', 'topic-news', 'livedoor-homme', 'peachy', 'sports-watch', 'dokujo-tsushin', 'smax']
class ldn_dataset(Dataset):
def __init__(self, csv_path,transform=None):
sp = spm.SentencePieceProcessor()
sp.Load("sentencepiece_livedoor.model")
#csvファイル読み込み。
df = pd.read_csv(csv_path)
labels = df['category']
titles = df['title']
self.titles = titles
self.labels = labels
self.transform = transform
self.sp = sp
def __getitem__(self, index):
title = self.titles[index]
title = self.sp.EncodeAsIds(title)
label=self.labels[index]
label=CATEGORIES.index(label)
return torch.tensor(title,dtype=torch.long),torch.tensor(label,dtype=torch.long)
def __len__(self):
#データ数を返す
return len(self.labels)
if __name__ == '__main__':
#データセット作成
dataset = ldn_dataset("./livedoor_news_title_test.csv")
#dataloader化 batch_size=1 以外では動かない
dataloader = DataLoader(dataset, batch_size=1)
#データローダの中身確認
for title,label in dataloader:
print('title=',title)
print('label=',label)
トレーニング
上記のファイルを呼び出して訓練してみます。エポック100ぐらいにするとそれなりに収束しますね。
#ldn_train.py
import os
from glob import glob
import pandas as pd
import linecache
import sentencepiece as spm
import re
import torch
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from ldn_model import LSTMTagger
from ldn_dataset import ldn_dataset
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# 単語のベクトル次元数
EMBEDDING_DIM = 10
# 隠れ層の次元数
HIDDEN_DIM = 128
# データ全体の単語数
VOCAB_SIZE = 8000
TAG_SIZE = 9
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, VOCAB_SIZE, TAG_SIZE)
model = model.to(device)
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
losses = []
#データセット作成
dataset = ldn_dataset("./livedoor_news_title_train.csv")
#dataloader化
dataloader = DataLoader(dataset, batch_size=1)
for epoch in range(100):
all_loss = 0
for title,cat in dataloader:
model.zero_grad()
#title[0]がイケてない。ミニバッチ化出来てない。
out = model(title[0].to(device))
loss = loss_function(out, cat.to(device))
loss.backward()
optimizer.step()
all_loss += loss.item()
losses.append(all_loss)
print("epoch", epoch, "\t" , "loss", all_loss)
torch.save(model.state_dict(), 'ldn.pth')
print("done.")
モデルは”ldn.pth” として保存しておきます。
推論
完成したモデルファイル、検証用のCSVファイルを読み込んで精度を確認してみます。ちょうど正解率50%でした。訓練ファイルでは正解率99%ですので、過学習してるのは間違いないですが、特徴量はそれなりに取り出せているのではと思います。ランダムな場合は10%台が正解率になるはずですので。。
#ldn_predict.py
import os
from glob import glob
import pandas as pd
import linecache
import sentencepiece as spm
import re
import torch
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from ldn_model import LSTMTagger
from ldn_dataset import ldn_dataset
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# 単語のベクトル次元数
EMBEDDING_DIM = 10
# 隠れ層の次元数
HIDDEN_DIM = 128
# データ全体の単語数
VOCAB_SIZE = 8000
TAG_SIZE = 9
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, VOCAB_SIZE, TAG_SIZE)
model.load_state_dict(torch.load('ldn.pth'))
#データセット作成
dataset = ldn_dataset("./livedoor_news_title_test.csv")
#dataloader化
dataloader = DataLoader(dataset, batch_size=1)
correct = 0
with torch.no_grad():
for title,cat in dataloader:
out = model(title[0].to(device))
if out.argmax(1).item()==cat.item():
correct += 1
print("len(dataset)=", len(dataset), "\t" , "correct=", correct, "accuracy%=",int(100*correct/len(dataset)))
print("done.")
まとめ
今回はカテゴリがニュースの出どころで、ちょっとタイトルからだけでは人間でも判別するのは難しいので、正解率50%はまあこんなものかなと思います。
あとデータが可変長(タイトルの長さがニュースにより違う)ですので、バッチ化するには長さを合わせてやる必要があるかと思いますが、そのあたりPadding処理の理解がちょっとまだですので、もう少し勉強して試してみたいと思います。