PyTorchを使ってテキスト分類をやってみる

 

以前、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層からなるシンプルなネットワークになっています。

f:id:pytry3g:20200209190039p:plain

コードにすると以下のようになります。

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 the model
    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()

    # Adjust the learning rate
    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]
    # torch.Tensor.cumsum returns the cumulative sum
    # of elements in the dimension dim.
    # torch.Tensor([1.0, 2.0, 3.0]).cumsum(dim=0)

    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のチュートリアルを参考にしてテキスト分類をやってみましたが、シンプルなネットワークに関わらず思っていたよりも良い結果が得られたかなと思います。

次はツイートのテキスト分類をやってみたいなーと考えています。