以前、gensimとPyTorchを使ってLive doorニュースコーパスのテキスト分類をやってみたんですが、あまり良い結果が得られませんでした。
www.pytry3g.com
今回はこちらのチュートリアルを参考にしつつ、前回よりも精度の高い学習モデルを作ってみたいと思います。
方針
今回もLivedoorニュースコーパスのデータを用いてテキスト分類をやってみます。
データのダウンロードはこちらから。
学習にはGoogle Colaboratoryを使います。
www.pytry3g.com
学習データの用意
学習データ(Livedoorニュースコーパス)をダウンロードしたら、解凍します。
解凍すると以下のようなフォルダ構成になっていると思います。
text- dokujo-tsushin
it-life-hack
kaden-channel
livedoor-homme
movie-enter
peachy
smax
sports-watch
topic-news
CHANNGES
README
pandasを使って必要な学習データを抽出してラベルづけをしていきます。
import codecs
import glob
import pandas as pd
from tqdm import tqdm
path = "任意のフォルダ/text/"
text = ["dokujo-tsushin","it-life-hack","kaden-channel","livedoor-homme",
"movie-enter","peachy","smax","sports-watch","topic-news"]
text2id = {v: k for k, v in enumerate(text)}
df = pd.DataFrame(columns=["class", "id", "news"])
for label, id in tqdm(text2id.items()):
txt_list = glob.glob(path + label + "/*.txt")
txt_list.remove(path + label + "/LICENSE.txt")
for txt_path in txt_list:
data = codecs.open(txt_path, 'r', 'utf-8').read().splitlines()[2:]
data = "\n".join(data).replace("\u3000", " ")
temp = pd.Series([label, id, data], index=df.columns)
df = df.append(temp, ignore_index=True)
df.to_csv("livedoor_news.csv", index=False, encoding="utf-8")
こちらの作業はローカルでやることをオススメします。
ローカルだとすぐに実行できますが、Google Colaboratoryで実行するといつまでたっても終わりませんでした、、、
データの用意ができたら、Googleドライブの任意のフォルダにアップロードしましょう。
以上で学習データの用意は完了です。
データの前処理
続いてデータの前処理をしていきます。
これ以降の作業はGoogle Colaboratoryを使います。
Google Colaboratoryから新しいNotebookを開いて以下のコードを実行します。
from google.colab import drive
drive.mount('/content/drive')
このコードを実行して認証することでGoogleDriveのデータを読み込めるようになります。
次に開いているNotebookがあるディレクトリに移動します。
cd "drive/My Drive/Colab Notebooks"
パスはdrive/My Drive/任意のフォルダ
というように、各自の環境に合わせてください。私の場合、Colab Notebooks
という名前のフォルダにNotebookを作成しているので、上記のようになっています。
さきほどラベルづけしたデータを用意してGoogleドライブに上げましたが、データが文章のままではテキスト分類はできないので、文章を分解して単語(文字)単位に変換したいと思います。
今回はSentencePieceを使って文章を分解します。
www.pytry3g.com
まずはSentencePieceをpip
でインストールします。
!pip install sentencepiece > /dev/null
SentencePieceを使って学習データを単語(文字)単位に分解するために、改行区切りでデータを書き込まれたテキストファイルを用意します。
・関連記事 - print関数を使ってデータをファイルに書き込む
import codecs
import pandas as pd
df = pd.read_csv("dataset/livedoor_news.csv")
data = []
for news in df["news"]:
for line in news.splitlines():
if len(line) == 0:
continue
data.append(line)
print("\n".join(data), file=codecs.open("livedoor_news.txt", "w", "utf-8"))
あとは、こちらの記事でやっているとおりに単語分割モデルの作成をします。
import sentencepiece as spm
spm.SentencePieceTrainer.Train(
'--input=livedoor_news.txt, --model_prefix=sentencepiece_livedoor --character_coverage=0.9995 --vocab_size=8000'
)
学習には数分かかります。
単語分割モデルの学習が完了したら、モデルの読み込みをします。
sp = spm.SentencePieceProcessor()
sp.Load("sentencepiece_livedoor.model")
次に単語分割モデルを使って文章から単語ID列に変換していきます。
import random
corpus = []
for i in range(len(df)):
cls = df["id"][i]
ids = sp.EncodeAsIds(df["news"][i])
corpus.append(tuple((cls, ids)))
random.shuffle(corpus)
corpus
には(クラスID、単語ID列)
の形式でデータが入っています。
データは全部で7367あるので、今回は訓練用に7000、テスト用に367のデータに分けたいと思います。
train_data, test_data = corpus[:7000], corpus[7000:]
以上でデータの前処理は完了です。
予測モデルの定義
今回定義する予測モデルはEmbeddingBagと出力層の2層からなるシンプルなネットワークになっています。
コードにすると以下のようになります。
import torch
import torch.nn as nn
import torch.nn.functional as F
class LdccClassification(nn.Module):
def __init__(self, vocab_size, embed_dim, num_class):
super().__init__()
self.embedding = nn.EmbeddingBag(vocab_size, embed_dim, sparse=True)
self.fc = nn.Linear(embed_dim, num_class)
self.init_weights()
def init_weights(self):
initrange = 0.5
self.embedding.weight.data.uniform_(-initrange, initrange)
self.fc.weight.data.uniform_(-initrange, initrange)
self.fc.bias.data.zero_()
def forward(self, text, offsets):
embedded = self.embedding(text, offsets)
return self.fc(embedded)
学習
パラメータの設定。
BATCH_SIZE = 16
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
VOCAB_SIZE = len(sp)
EMBED_DIM = 32
NUN_CLASS = 9
model = LdccClassification(VOCAB_SIZE, EMBED_DIM, NUN_CLASS).to(device)
与えられたデータを使って学習する関数。
def train_func(sub_train_):
train_loss = 0
train_acc = 0
random.shuffle(sub_train_)
data = [generate_batch(sub_train_[i:i+BATCH_SIZE]) for i in range(0, len(sub_train_), BATCH_SIZE)]
for ids, offsets, cls in data:
optimizer.zero_grad()
ids, offsets, cls = ids.to(device), offsets.to(device), cls.to(device)
output = model(ids, offsets)
loss = criterion(output, cls)
train_loss += loss.item()
loss.backward()
optimizer.step()
train_acc += (output.argmax(1) == cls).sum().item()
scheduler.step()
return train_loss / len(sub_train_), train_acc / len(sub_train_)
学習データからバッチデータを用意する関数。
def generate_batch(batch):
cls = torch.tensor([entry[0] for entry in batch])
ids = []
for entry in batch:
ids += entry[1]
ids = torch.tensor(ids)
offsets = [0] + [len(entry[1]) for entry in batch]
offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
ids = torch.cat((ids,))
return ids, offsets, cls
予測モデルの学習部分。
import time
N_EPOCHS = 50
min_valid_loss = float('inf')
criterion = torch.nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=4.0)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9)
train_len = int(len(train) * 0.95)
sub_train_, sub_valid_ = train_data[:train_len], train[train_len:]
for epoch in range(N_EPOCHS):
start_time = time.time()
train_loss, train_acc = train_func(sub_train_)
valid_loss, valid_acc = test(sub_valid_)
secs = int(time.time() - start_time)
mins = secs / 60
secs = secs % 60
print('Epoch: %d' %(epoch + 1), " | time in %d minutes, %d seconds" %(mins, secs))
print(f'\tLoss: {train_loss:.4f}(train)\t|\tAcc: {train_acc * 100:.1f}%(train)')
print(f'\tLoss: {valid_loss:.4f}(valid)\t|\tAcc: {valid_acc * 100:.1f}%(valid)')
結果
学習が完了してので、テストデータを使ってモデルの精度を確認してみます。
以下のコードでテストデータをモデルに渡したときの予測結果と精度を見ることができます。
ldcc_news_label = {0:"dokujo-tsushin",
1:"it-life-hack",
2:"kaden-channel",
3:"livedoor-homme",
4:"movie-enter",
5:"peachy",
6:"smax",
7:"sports-watch",
8:"topic-news"}
def predict(ids, model):
with torch.no_grad():
output = model(ids, torch.tensor([0]))
return output.argmax(1).item()
acc = 0
model = model.to("cpu")
for entry in test_data:
cls = entry[0]
ids = torch.tensor(entry[1])
text = sp.DecodeIds(entry[1])
output = ldcc_news_label[predict(ids, model)]
print("Model predicted a %s news. This is a %s news.\n%s\n" % (output, ldcc_news_label[cls], text))
if ldcc_news_label[cls] == output:
acc += 1
print("Accuracy : {:.2f}".format(acc/len(test_data)))
結果は以下のようになりました。
Accuracy : 0.95
前回は8割切っていたので、かなり改善されましたね。
最後にモデルを保存しときます。
model_name = "livedoor_classification.model"
torch.save(model.state_dict(), model_name)
おわり
今回はPyTorchのチュートリアルを参考にしてテキスト分類をやってみましたが、シンプルなネットワークに関わらず思っていたよりも良い結果が得られたかなと思います。
次はツイートのテキスト分類をやってみたいなーと考えています。