どん底から這い上がるまでの記録

どん底から這い上がりたいけど這い上がれない人がいろいろ書くブログ(主にプログラミング)

gensimとPyTorchを使ったlive doorニュースコーパスのテキスト分類

今回はgensimとPyTorchを使ってニュース記事の分類をやってみます。

環境

データの用意

ここにあるー>https://www.rondhuit.com/download.html#ldcc

ldcc-20140209.tar.gzを使います。

ldcc-20140209.tar.gzをダウンロードし解凍すると、textディレクトリができます。

textディレクトリの内容は以下のようになっているはずです。

text- dokujo-tsushin
      it-life-hack
      kaden-channel
      livedoor-homme
      movie-enter
      peachy
      smax
      sports-watch
      topic-news
      CHANNGES
      README

 クラス

  • 独女通信
  • ITライフハック
  • 家電チャンネル
  • livedoor home
  • movie-enter
  • Peachy
  • Smax
  • スポーツウォッチ
  • トピックニュース

全部で9つのクラスがあるので、9クラス分類になります。

下の記事はITライフハックの記事のなかの一つです。

http://news.livedoor.com/article/detail/6292880/
2012-02-19T13:00:00+0900
旧式Macで禁断のパワーアップ!最新PCやソフトを一挙にチェック【ITフラッシュバック】
テレビやTwitterと連携できるパソコンや、プロセッサや切り替わるパソコンなど、面白いパソコンが次から次へと登場した。旧式Macの禁断ともいえるパワーアップ方法から、NECの最新PC、話題のThinkPad X1 Hybrid、新セキュリティソフトまで一挙に紹介しよう。

今回やりたいことは、例えば上のような記事がテキスト分類をするモデルに与えられたとき、この記事はITライフハックに属する記事だと正しく分類することです。

 

※実行するプログラムとtextディレクトリを同じディレクトリに置いて進めていきます。

前処理

make_csv.pyで用意したデータの前処理をして学習データを作ります。

9クラスのテキストファイルをpandasを使って1つのデータフレームにします。

今回は名詞だけを使ってテキスト分類したいと思います。

なので、テキストファイルを読み込んで形態素解析するときに名詞のみを取り出して、

出来上がったデータフレームをcsvファイルに書き込みます。

"""
make_csv.py
1. Read txt files.
2. Extract only noun in sentence.
3. Write extracted data into csv file.
"""
import codecs
import glob
import MeCab
import pandas as pd


tagger = MeCab.Tagger("-Ochasen")

def get_morphemes(fpath):
    """ Get morphemes
    :param fpath: file path
    :return: result of morphological analysis, -1 indicates error
    """
    data = read_file(fpath)
    morphemes = tokenzier(data)
    return morphemes if morphemes else -1

def read_file(fpath):
    """ Read file
    :param fpath: file path
    :return: file content of live door news corpus
    """
    with codecs.open(fpath, 'r', 'utf-8') as f:
        return "\n".join(f.read().splitlines()[2:])

def tokenzier(sentences):
    """ Morphological analysis
    :param sentences: strings in the article
    :return: morphemes
    """
    tag = tagger.parseToNode(sentences)
    morphemes = []
    while tag:
        features = tag.feature.split(",")
        if features[0] == "名詞":
            morphemes.append(tag.surface.lower())
        tag = tag.next
    return morphemes


path = "text/"
ldcc = ["dokujo-tsushin","it-life-hack","kaden-channel","livedoor-homme",
        "movie-enter","peachy","smax","sports-watch","topic-news"]
ldcc2id = {v: k for k, v in enumerate(ldcc)}
df = pd.DataFrame(columns=["class", "news"])
for d, i in ldcc2id.items():
    flist = glob.glob(path + d + "/*.txt")
    flist.remove(path + d + "\\LICENSE.txt")
    for fpath in flist:
        print(fpath)
        morphemes = get_morphemes(fpath)
        if morphemes == -1:
            continue
        temp = pd.Series([i, " ".join(morphemes)], index=df.columns)
        df = df.append(temp, ignore_index=True)
df.to_csv("livedoor_news.csv", index=False, encoding="utf-8")

これで学習データを用意することができました。

このプログラムはWindowsで実行することを前提に書いています。したがって、MacLinuxを使っている方は、下の部分を

flist.remove(path + d + "\\LICENSE.txt")

次のように変えればいいと思います。

flist.remove(path + d + "/LICENSE.txt")

 

単語辞書を作る

次にgensimを使って単語辞書を作ります。

"""
make_dic.py
1. Read csv file
2. Make dictionary
3. Update dictionary
4. Save dictionary into a txt file
"""
import pandas as pd
from gensim.corpora import Dictionary

# Read csv file
df = pd.read_csv("livedoor_news.csv")

# 辞書
dct = Dictionary()
for i, news in enumerate(df["news"]):
    # Update dictionary with new documents
    dct.add_documents([news.split()])

dct.save_as_text("vocab.txt")

make_dic.pyを実行すると、名詞のみを取り出して作った学習データから単語辞書(vocab.txt)が生成されます。

vocab.txt の format:

ドキュメントの数
単語ID[TAB]単語[TAB]単語がドキュメントに出てくる回数[NEWLINE]
単語ID[TAB]単語[TAB]単語がドキュメントに出てくる回数[NEWLINE]
....
単語ID[TAB]単語[TAB]単語がドキュメントに出てくる回数[NEWLINE]

 

データの変換

今、学習データは名詞の集まり、文字列の集まりです。機械学習をするためにこの文字列をベクトルで表現したいと思います。

学習データと単語辞書を用意したので、この2つを使ってベクトル表現に変換します。

いくつかの方法がありますが今回はBag of Wordsを使って変換します。

Bag of Wordsについてはこちら。

www.pytry3g.com

 

gensimを使えば、容易にデータをBag of Wordsでのベクトルを作ることができます。

datasets.pyをインポートしてBag of Wordsで表現されたデータと教師ラベルのデータを取得できるようにしました。

scikit-learnのdatasetsのような感じで使えるように設計したつもりです。

"""
datasets.py
"""
import numpy as np
import pandas as pd
from collections import namedtuple
from gensim.corpora import Dictionary


# Load a dictionary
dct = Dictionary.load_from_text("vocab.txt")
def doc2bow(morphemes):
    """ Converrt strings with non filtered dictionary to vector
    :param morphemes: morphemes
    :return: BOW format
    """
    global dct
    # We can obtain all of indices and frequency of a morpheme
    # [(ID, frequency)]
    bow_format = dct.doc2bow(morphemes.split())
    return bow_format

def load_dataset():
    """ Load dataset
    :return: data and label
    """
    global dct
    # Load training dataset
    df = pd.read_csv("livedoor_news.csv")
    Dataset = namedtuple("Dataset", ["news", "data", "target", "target_names", "dct"])
    news = [doc for doc in df["news"]]
    data = [doc2bow(doc) for doc in df["news"]]
    target = np.array([label for label in df["class"]]).astype(np.int64)
    target_names = ["dokujo-tsushin","it-life-hack","kaden-channel","livedoor-homme",
                    "movie-enter","peachy","smax","sports-watch","topic-news"]
    ldcc_dataset = Dataset(news, data, target, target_names, dct)
    return ldcc_dataset

ネットワークの定義

今回は3層のニューラルネットワークです。

入力の次元は300、出力は9クラス分類なので9になります。

"""
net.py
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable


class MLP(nn.Module):
    def __init__(self):
        super(MLP, self).__init__()
        self.l1 = nn.Linear(300, 150)
        self.l2 = nn.Linear(150, 50)
        self.l3 = nn.Linear(50, 9)
        if USE_CUDA:
            self.cuda()

    def forward(self, x):
        h = F.relu(self.l1(x))
        h = F.relu(self.l2(h))
        y = self.l3(h)
        return y


def variable(matrix):
    tensor = FloatTensor(matrix) if matrix.dtype=="float32" else LongTensor(matrix)
    if USE_CUDA:
        return Variable(tensor).cuda()
    return Variable(tensor)

# GPU Settings
#USE_CUDA = torch.is_available()
USE_CUDA = False
FloatTensor = torch.cuda.FloatTensor if USE_CUDA else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if USE_CUDA else torch.LongTensor

学習

下のプログラムはtrain.pyの一部です。train.pyで学習データの読み込み、学習、テストの3つをします。

"""
train.py
"""
import numpy as np
import datasets
from gensim import matutils as mu
from gensim.models import LdaModel
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from net import MLP, variable
# pytorch
import torch
import torch.nn as nn
import torch.optim as O

# Load dataset
ldcc_dataset = datasets.load_dataset()
data = ldcc_dataset.data # bow format
target = ldcc_dataset.target # Label
dct = ldcc_dataset.dct # vocab
print("#vocab: {}".format(len(dct)))

まず、はじめにデータの読み込みします。

が、ここで注意しなければならないことがあります。

それは、入力の次元です。

今回はBoWを入力とするので、入力の次元=単語の数となります。

単語の数は59754個なので入力の次元も59754になります。

私はバカなので最初、入力の次元を59754のまま学習をしてテストをしていました。ネットワークの定義のところを入力を300ではなく59754としていました。40epochでだいだい2時間くらい学習するのにかかり、結果もひどかったです。

正答率はだいだい10%くらいでした。

どうすればいいモデルを作れるのかと考えたとき、一番の問題は入力の次元が多すぎることだと思ったので、次元を小さくすることにしました。

そこで、使ったのがLDAです。

LDAは Latent Dirichlet Allocationの略で次元をいい感じで圧縮することができる手法です。LDAについての詳細な説明は割愛します。説明できるほど理解していなので。

まぁ、とりあえずLDAを使えば入力の次元59754を圧縮できるということです。

今回は次元を300に圧縮します。

下のプログラムは上のtrain.pyの続きです。必要なライブラリはすでに上のtrain.pyでインポートしています。

LDAモデルを作った後、データを学習データとテストデータに分けて学習しています。

print("LDA Model is building now...")
lda_model = LdaModel(corpus=data, id2word=dct, num_topics=300)
print("Done...")
corpus_lda = lda_model[data]
print("Converting data into feature vectors...")
data = np.array([mu.corpus2dense([doc], num_terms=300).T[0] for doc in corpus_lda])
print("Done...")
# Split dataset into traing set and test one...
train_x, test_x, train_t, test_t = train_test_split(data, target, test_size=0.1)

# Parameters
batchsize = 150
n_epochs = 40
n_batch = len(train_x) // batchsize
model = MLP()
optimizer = O.Adam(model.parameters(), lr=0.1)
criterion = nn.CrossEntropyLoss()
print("Training...")
for epoch in range(n_epochs):
    train_x, train_t = shuffle(train_x, train_t)
    if (epoch+1) % 10 == 0:
        print("Epoch {}".format(epoch+1))
    for i in range(n_batch):
        model.zero_grad()
        start = i * batchsize
        end = start + batchsize
        x = variable(train_x[start:end])
        t = variable(train_t[start:end])
        y = model(x)
        loss = criterion(y, t)
        loss.backward()
        optimizer.step()

 

テスト

下のコードで学習したモデルのテストをします。上のtrain.pyの続きです。

print("Test the model...")
test_x = variable(test_x)
predicted = torch.max(model(test_x), 1)[1].data.numpy().tolist()
result = sum(1*(p == t) for p, t in zip(predicted, test_t))
print("{}/{}".format(result, len(test_t)))
print("Accuracy {:.2f}".format(result / len(test_t)))

結果は以下の通りです。

正答率は8割を切ってしまいました。

LDA Model is building now...
Done...
Converting data into feature vectors...
Done...
Training...
Epoch 10
Epoch 20
Epoch 30
Epoch 40
Test the model...
571/737
Accuracy 0.77

おわり

gensimとPyTorchを使ってライブドアニュースのテキスト分類をしてみました。

あまり、いい結果が得られませんでしたが、改善の余地はあると思います。ネットワークの構成を見直したり、学習データに名詞以外も使うようにするなど。

ちなみに、同じデータを用いてscikit-learnでもテキスト分類をやってみましたが、PyTorchよりもいい結果が出ました。

その辺のことは、また次回に書きたいと思います。

おしまい。