ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

Word Embeddingだけで文書分類する

データが膨大にあるわけでもなく、自然言語処理のガチ勢でもない、という人が訓練済みWord Embedding(word2vecやglove)を用いるだけで文書分類ができるそうなので、試してみた。

○○新聞データを取得

まず、スクレイピングしやすそうな新聞社のデータなど取ってきてください。取ってきたら、記事ごとにポジティブかネガティブかを500記事ほどアノテーションしてください。

このあたりは本題ではないので省略します。

Embeddingだけを使ってどうするのか

  1. Embeddingでドキュメントのベクトルを取得。
  2. ポジティブとネガティブのカテゴリーを表す語を複数用意し、Embeddingでカテゴリーベクトルを取得。
  3. ドキュメントと各カテゴリーベクトルのコサイン類似度を求める。
  4. ポジティブ側のコサイン類似度が大きければTrue,そうでなければFalseを返すようにする。
  5. アノテーションと比較して最もAUC値が高いカテゴリーベクトルを採用。
  6. ちょっぴり数学的にゴニョゴニョして「ポジティブ度」を出力するモデルを作成。

採用するカテゴリーベクトルを選ぶ

import MeCab
import pandas as pd
import itertools
from tqdm import tqdm
import tensorflow as tf 
import tensorflow_hub as hub
import numpy as np
from sklearn.metrics import roc_auc_score

def cos_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

def calc_embeddings(news_list, pos_list, neg_list):
    with tf.Graph().as_default(): 
        embed = hub.Module("https://tfhub.dev/google/nnlm-ja-dim128/1")
        embeddings = embed(news_list)
        pos_emb = embed(pos_list)
        neg_emb = embed(neg_list)
        with tf.Session() as sess: 
            sess.run(tf.global_variables_initializer()) 
            sess.run(tf.tables_initializer()) 
            doc_vecs = sess.run(embeddings)
            pos_vecs = sess.run(pos_emb)
            neg_vecs = sess.run(neg_emb)
    return doc_vecs, pos_vecs, neg_vecs

def solve_acc(df, doc_vecs, pos_vec, neg_vec):
    positives = []
    negatives = []
    for i in range(len(doc_vecs)):
        positives.append(cos_similarity(doc_vecs[i], pos_vec))
        negatives.append(cos_similarity(doc_vecs[i], neg_vec))
    df['pos'] = positives
    df['neg'] = negatives
    df['pos_gt_neg'] = df['pos'] > df['neg']
    return roc_auc_score(df['pn_labels'], df['pos_gt_neg'])


def solve_acc_for_all_categories(df, doc_vecs, pos_vecs, pos_list, neg_vecs, neg_list):
    results = []
    for pos_vec, pos_cat in tqdm(zip(pos_vecs, pos_list)):
        for neg_vec, neg_cat in zip(neg_vecs, neg_list):
            results.append({'auc':solve_acc(df, doc_vecs, pos_vec, neg_vec), 'pos_cat':pos_cat, 'neg_cat': neg_cat})
    return pd.DataFrame(results)

if __name__ == '__main__':
    df = pd.read_csv('ja_mainichi_corpus/mainichi_news_annotated.csv')
    tagger = MeCab.Tagger('-Owakati')

    for i in tqdm(range(df.shape[0])):
        df['news'][i] = tagger.parse(df['news'][i])

    pos_categories = ["面白い", "素敵", "かっこいい", "素晴らしい", "優れている", "明るい", "楽しい", "興味深い", "綺麗", "心地いい"]
    neg_categories = ["つまらない", "最低", "くだらない", "ださい", "劣っている", "暗い", "悲しい", "汚い", "不快"]

    pos_list = []
    neg_list = []
    for x in itertools.combinations(pos_categories, 2):
        pos_list.append(' '.join(x))

    for x in itertools.combinations(neg_categories, 2):
        neg_list.append(' '.join(x))

    pos_list += pos_categories
    neg_list += neg_categories

    doc_vecs = None
    news_list = df['news'].tolist()

    df['pn_labels'] = df['pn_labels'] == 1.0
    doc_vecs, pos_vecs, neg_vecs = calc_embeddings(news_list, pos_list, neg_list)
    auc_df = solve_acc_for_all_categories(df, doc_vecs, pos_vecs, pos_list, neg_vecs, neg_list)
    auc_df.to_csv("mainichi_auc_score_each_cats.csv", index=False)

最大の精度を出すカテゴリー ポジ: 面白い ネガ: くだらない 不快

最大の精度を出すカテゴリーのROC曲線 download.png

AUC: 0.8313236020026797

ゴニョゴニョしてポジティブ度を求める

事前準備

import MeCab
import pandas as pd
import tensorflow as tf 
import tensorflow_hub as hub
from tqdm import tqdm_notebook
import numpy as np

def cos_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

df = pd.read_csv('ja_mainichi_corpus/mainichi_news_annotated.csv')
tagger = MeCab.Tagger('-Owakati')

for i in tqdm_notebook(range(df.shape[0])):
    df['news'][i] = tagger.parse(df['news'][i])


doc_vecs = None
cats = None

with tf.Graph().as_default(): 
    embed = hub.Module("https://tfhub.dev/google/nnlm-ja-dim128/1")
    embeddings = embed(df['news'].tolist())
    categories = embed([
        '面白い', 
        'くだらない 不快'])
    with tf.Session() as sess: 
        sess.run(tf.global_variables_initializer()) 
        sess.run(tf.tables_initializer()) 
        doc_vecs = sess.run(embeddings)
        cats = sess.run(categories)

positives = []
negatives = []
for i in range(len(doc_vecs)):
    positives.append(cos_similarity(doc_vecs[i], cats[0]))
    negatives.append(cos_similarity(doc_vecs[i], cats[1]))
df['pos'] = positives
df['neg'] = negatives
def step_f(x):
    if x > 0.0:
        return x
    else:
        return 0.0

def polarity(pos, neg, alpha=1, beta=1):
    p = step_f(pos)
    n = step_f(neg)
    if p+n == 0:
        return 0.0
    else:
        return (alpha*p*(p/(p+n)) - beta*n*(n/(p+n)))

df['polarity'] = np.vectorize(lambda pos, neg: polarity(pos, neg, 4, 4))(df['pos'], df['neg'])

このpolarity関数は、直感的にこんな感じで作ればそれっぽい出力はするだろう、という考えのもとで作成した関数です。事前に求めたポジ・ネガのコサイン類似度を用います。

考察

まず、○○新聞が持つ何らかの傾向を、Embeddingだけを用いて判別するためには「どんなカテゴリーベクトルとのコサイン類似度をとるのが良いか」が重要になってきます。そのためには、いくつかのカテゴリーワードを用意して、その組み合わせを列挙し、それらの組み合わせの中で最も良い精度を出すカテゴリーの組み合わせを選びます。

このような方法でもそれなりの精度を出すことがわかりますが、「面白い」と類似したドキュメントがポジティブで、「くだらない 不快」と類似したドキュメントがネガティブ、といった傾向が○○新聞以外の文書でも当てはまるのかについては断定できません。

ただ、この手法自体は、自然言語処理について詳しくなくとも実行でき、しかも訓練済みのEmbeddingを用いればマシンのリソースも必要ないので、その点では有益だと言えます。

参考

[1] https://arxiv.org/pdf/1804.02063.pdf