寿司on焼肉onピザ

プログラミングとか、かっこいい音楽とかの話を

【Python3】【自然言語処理】ニュース記事をロジスティック回帰で分類+可視化【sklearn】【TFIDF】

あけましておめでとうございます。
2020年一発目は自然言語処理をします。

ところで最近MLPにハマりました、A MultiLayer PerceptronじゃなくてMy Little Ponyの方です。
NLP(Natural Language Processing)とMLPって似ててややこしいね。

はじめに

やっぱ用意されているテストデータで解析するより、自分で実データを使って解析してみたいですよね。
そういうわけで、今回はニュース記事のデータセットを使って簡単な分類タスクをやってみます。

今回の結果
映画関連の記事とスポーツ関連の記事をこんな感じ可視化し決定境界をプロットしました。
f:id:dokataponiti:20200102182019p:plain

今回は数学的な説明はしません、それはまた別の機会に。
それでは始めていきましょう。

環境

  • Mac OS Mojave 10.14.6
  • Python3.7.4
  • Anaconda
  • jupyter notebook
  • Mecab Neologd
  • numpy 1.16.2
  • scikit-learn 0.21.3
  • scipy 1.3.1

numpyだけは1.17だとうまく動かない場合があったので、あとのライブラリはメモ程度に
下記のデータセットをダウンロードして、同じフォルダ構成にすれば(多分)実行できると思います。

データセット

こちらのニュース記事を利用させていただきました。ありがとうございます。
ダウンロード - 株式会社ロンウイット
今回処理したデータは映画関連の記事が870件、スポーツ関連の記事が900件です。

使い方

作業ディレクトリを作成し、上記データセットソースコードをダウンロードして次のようにディレクトリを配置してください。
データセットのルートディレクトリの名前をdataと変更して以下のように配置してください。
次にnewsと同じ階層にnews_maというディレクトリを作成します。
f:id:dokataponiti:20200101232155p:plain
wakati_news_git.py を実行した後にjupyter notebookで tfidf_lr.ipynb を実行すると結果が表示されます。

処理の流れ

  1. データの前処理&分かち書き
  2. 文書をTFIDFベクトルに変換
  3. ロジスティック回帰モデルで文書分類
  4. 結果の可視化

データの前処理&分かち書き

Neologd使いたかったので、ターミナルでpythonを実行します(jupyter notebookで使えなかった...)

>python3 wakati_news_git.py

自然言語データ(特に日本語)の前処理には少しコツがあります。
ここを突き詰めるとキリがないので、今回あつかったポイントを箇条書きしておきます。

  • 英語を全て小文字に統一
  • 扱う品詞の限定
  • 動詞を標準形に統一

あとは、データを分かち書きするだけなんでサクッといきましょう。
ここでnews_wakati.txtを生成します。(形態素を空白で区切ったデータ)

以下、jupyter notebookでの実行となります。

文書をTFIDFベクトルに変換

sklearnを使ってサクッと文書のベクトル化ができます。
TFIDFの説明はこちらをご覧ください。
tf-idf - Wikipedia

簡単にいうと、文書を特徴づける単語ほど値が大きくなるってことです。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(use_idf=True, min_df=0.02, stop_words=[],token_pattern=u'(?u)\\b\\w+\\b')
#data_path = "./data/news_ma/"
    
#分かち書き文章を取得
with open("./data/news_ma/news_wakati.txt", 'r') as rfile:
    lines = rfile.readlines()
    docs = np.array([l.replace("\n", "") for l in lines])
    #["文書aの分かち書き", "文書bの分かち書き", ...]

#tfidfベクトルの生成
tfidf_vecs = vectorizer.fit_transform(docs)
print(tfidf_vecs.shape) #(文書数, 語彙数)
print(vectorizer.vocabulary_) #語彙一覧

TfidfVectorizer()でmin_dfやmax_df、stop_wordsを設定することで、結構それっぽいベクトルになったりします。
TFIDFのベクトルは単語の数だけ次元数があります。ベクトルの数は文書数となります。
今回のTFIDFベクトルのtfidf_vecs.shape出力結果は、(1770, 1507)になります。
使用されている単語と頻度の例がこんな感じです。ニュースっぽいでしょうか?

'dvd': 84, 'エンター': 278, 'られる': 249, '女': 769, '目': 1184, 'する': 178, '真実': 1192, '孤独': 789, '2006年': 25, '連載': 1410

次元圧縮

ベクトルを生成したらプロットしたくなるのが男の子というもの
しかし、1507次元のベクトルをどうやってプロットしたらいいでしょう?
ここで、SVDと言われる手法が登場するわけです。SVDによって次元数の大きいベクトルを低次元に圧縮することができます。

具体的な手法はこちらの記事が大変わかりやすく解説してくれています。僕もこれで勉強しました。
mieruca-ai.com

さて、2次元に圧縮できればx軸y軸でデータをプロットできそうです。
次のコードでTFIDFベクトルを2次元に圧縮しましょう。

#プロット用に次元削減
from sklearn.decomposition import TruncatedSVD
svd = TruncatedSVD(2)
svd_vecs = svd.fit_transform(tfidf_vecs)
print(svd_vecs)

出力:

[[ 0.38588807 -0.05177993]
 [ 0.56464581 -0.03317373]
 [ 0.41480447 -0.14996453]
 ...

しっかりと二次元になっていますね。

ニュース文書のプロット

上で求めたベクトルをプロットしましょう。

import matplotlib.pyplot as plt
%matplotlib inline
#とりあえず可視化
color_ctr = "red" #赤:映画記事 青:スポーツ記事
for x in range(len(svd_vecs)):
    if x >= 870:
        color_ctr = "blue"
    plt.plot(svd_vecs[x][0], svd_vecs[x][1], marker='o', linestyle='none', alpha=0.4, color=color_ctr)

plt.grid(True)
plt.show()

出力 赤=映画記事, 青=スポーツ記事
f:id:dokataponiti:20200101235757p:plain

スポーツ記事と映画記事のベクトルが各々まとまっているのがわかります。
この二つを分ける境界線を求めれば文書分類ってのができそうですね。

文書データをこんな感じにプロットできるってなんだか魔法みたいですね

ロジスティック回帰モデルで文書分類

さて、文書分類をしていきます。入力された記事を「映画記事」「スポーツ記事」の2クラスに分類するタスクです。
今回は教師あり学習手法の一つである、ロジスティック回帰という手法を使用します。
文書分類やスパムフィルタリングではSVMもよく使用されています。
ロジスティック回帰 - Wikipedia

詳細な解説はまた別の機会でやるとしましょう。
コードはこんな感じ

#ロジスティック回帰をやってみる
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression as LR
from sklearn.model_selection import cross_val_score
#データのラベルを生成
label = ["movie" if i < 870 else "sports" for i in range(len(docs))]
#学習
clf = LR(solver='lbfgs')

# 交差検証
scores = cross_val_score(clf, tfidf_vecs, label, cv=5)
# 各分割におけるスコア
print('交差検証の各回の精度: {0}'.format(scores))
print('精度の平均: {0}'.format(np.mean(scores)))

今回は交差検証で精度を確認しています。
交差検証 - Wikipedia

結果

交差検証の各回の精度: [0.99717514 1.         0.99717514 1.         0.99717514]
精度の平均: 0.9983050847457626

データが元からしっかりと分離されているので、精度の平均は約0.998と高い数値を示していますね。

結果の可視化

ここからはおまけです。
せっかくなので決定境界をプロットしてみましょう。
先ほど、1507次元のベクトルを使いましたが、今回は2次元に圧縮したデータで文書分類を行います。
2次元のデータにしてしまえばプロットは簡単にできそう、ということですね。

可視化の準備
2次元のベクトルを使用しロジスティック回帰モデルを構築します。
決定境界の重みとバイアスを取得しましょう。

#曲線をプロットしてみる
import math

#先ほど2次元に圧縮したsvd_vecsを利用する
#学習用と訓練用にデータの分割
X_train, X_test, y_train, y_test = train_test_split(svd_vecs, label, test_size=0.3)
lr = LR(solver='lbfgs')
lr.fit(X_train, y_train)

#print("訓練データの正解率:" ,lr.score(X_train, y_train))
print("テストデータの正解率:", lr.score(X_test, y_test))
#print (lr.predict(X_test)) #テストデータを入れて分類してみた結果
print ("バイアス:" ,lr.intercept_)
print ("重み:", lr.coef_[0])
テストデータの正解率: 0.9830508474576272
バイアス: [1.75716636]
重み: [-5.64512381 13.37045192]

この値は乱数が絡んでくるので多少変動します。
バイアスと重みを境界線関数にぶち込んでプロットすれば決定境界を描画することができます。

可視化

#境界線関数
def logistic(t, w):
    return (-w[0] * t - lr.intercept_[0])/w[1]

#ロジスティック関数の描画
#svd_vecのx軸の最大値から最小値まで50回のプロット
t = np.linspace(np.min(svd_vecs, axis=0)[0],np.max(svd_vecs, axis=0)[0],50)
y = np.array([logistic(ele, lr.coef_[0]) for ele in t])
plt.plot(t,y)
plt.grid(True)

#tfidfベクトルのプロット
color_ctr = "red" #赤:映画記事 青:スポーツ記事
for x in range(len(svd_vecs)):
    if x >= 870:
        color_ctr = "blue"
    plt.plot(svd_vecs[x][0], svd_vecs[x][1], marker='o', linestyle='none', alpha=0.4, color=color_ctr)

結果
f:id:dokataponiti:20200102182019p:plain
決定境界がプロットできました。記事を分断するぽい境界になってますね。

おわりに

今回はニュース記事をロジスティック回帰で分類してみましたが、他にも文書分類の手法は数多く存在します。
SVMとかトピックモデルとかk-meansとか、次回以降の記事で扱ってみることにしましょう。
文書のベクトル化にも他の手法がありますので、別の機会でそちらも触れてみようかな。