Edited at

ニュース記事の分類を機械学習で予測する

More than 1 year has passed since last update.


はじめに

 Pythonを用いて、ニュース記事の分類分けを教師ありの機械学習にかけて、未知の文章がどのニュース記事にあたるのかを予測する。ということをやってみました。

使うものとしては、


  • Mecab

  • Gensim

  • scikit-learnのSVM
    これらを利用しました。

 また今回やるにあたり、主にMecabとGensimの利用の辺りを以下のサイトを参考(というよりもはやパクリ)に行ったので、まずはそちらの記事を見ていただいたらと思います。


環境や各種ツールの説明

環境


  • OS : windows10

  • python : 3.6.6

ツール

- Mecab : 0.996.1

- Gensim : 3.5.0

- scikit-learn : 0.19.1


Mecabの用意

 Mecabは、普段pythonでモジュールのインストールを行うときと微妙に違かったので、説明しておきます。

 Mecabのインストールは、こちらのリンクから64bit版をインストールし、その後

~\Program Files\MeCab\bin

にパスを追加しました。私は有志の方が用意した64bit版を選びましたが、もし32bit版を使いたい場合、公式が用意してくれていますので、そちらからインストールをしてみてください。

パスを追加出来たら、

pip install mecab-python-windows

で、モジュールのインストールが出来ます。


データ整形

今回、データセットは「livedoor ニュースコーパス」を使わせていただきました。

こちらのデータのフォーマットは、

1行目:記事のURL

2行目:記事の日付

3行目:記事のタイトル

4行目以降:記事の本文

となっています。


データ取得

まず、先ほど述べデータの、記事タイトルと本文を取得します。


main.py

import os

#ファイルの読み込み、及びタイトル・本文データの取得
path_list = ["dokujo-tsushin", "it-life-hack", "kaden-channel"]
w_list = []
labels = []

for p_list in path_list:
path = "./text/"+p_list
#ディレクトリ内の全ファイル名を取得
f_list = os.listdir(path)

for lists in f_list:
with open("./text/"+ p_list+ "/"+lists, encoding="utf-8_sig") as f:
next(f)
next(f)
#全角スペースや改行の削除
w = f.read().replace('\u3000','').replace('\n','')

w_list.append(w)
labels.append(path_list.index(p_list))


今、/text/ の中に、各ニュースサイトのディレクトリがあり、その中に各記事がtxtファイルとして入れられています。

 まず path_list で今回分類に使うニュースサイトを選んでいます。まずは三つのサイトで分類をしてみます。

 その後、os.listdir でディレクトリ内のファイル名をすべて取得しています。この時、三つのフォルダに入っているファイル数は、のちに未知データの予測を行うために三つのファイルを抜いたのを差し引いて、2,601個となっています。

 withopen以降は、最初2行を読み込みたくないため先にnext()を2回入れています。また同時に、正解ラベルを用意しておくため、どのニュースサイトのディレクトリからファイルを開いたかを確認し、保存するために、path_list.index(p_list) でインデックスを取得しリストに保存しています。


名詞の切り分け

さきほどのデータを、名詞ごと切り分けていきます。


analysis.py

import MeCab

mecab = MeCab.Tagger('mecabrc')

#形態素解析をして、名詞だけ取り出す
def tokenize(text):
node = mecab.parseToNode(text)
while node:
if node.feature.split(',')[0] == '名詞':
yield node.surface.lower()
node = node.next

#記事群のdictについて、形態素解析をしてリストに返す
def get_words(contents):
ret = []
for content in contents:
ret.append(get_words_main(content))
return ret

#一つの記事を形態素解析して返す
def get_words_main(content):
return [token for token in tokenize(content)]


参考:scikit-learnとgensimでニュース記事を分類する / 1.MeCabで単語切り出し

 参考にさせていただいた記事をほぼパクリのような形で使わせていただいておりますので、詳しい説明は省かせていただきます。端的に説明すると、与えられた記事の文字列を名詞区切りに分けていき、それを文ごとのリスト。二重リストに格納しています。

これを先ほど取得したデータに使うと、


main.py

import analysis

words = analysis.get_words(w_list)
print(words[0])


['友人', '代表', 'スピーチ', '独', '女', 'ジューン', 'ブライド', '6月', '独', '女', '中', '自分', '式', 'お祝い', '貧乏', '状態', '人', 'の', '出席', '回数', 'お願い', 'ごと', 'こと', 'お願い', 'ん', '友人', '代表', 'スピーチ', 'とき', '独', '女', '対応', '最近', 'インターネット', '等', '検索', '友人', '代表', 'スピーチ', '用', '例文', 'サイト', 'たくさん', 'それら', '参考', '無難', 'もの', '誰', '作成', '由利', 'さん', '33', '歳', 'ネット', '参考', '作成', 'これ', 'の', '不安', '一人暮らし', '感想', '人', '他', '友人', 'の', 'こと', '活用', 'の', 'インターネット', '悩み', '相談', 'サイト', 'そこ', '作成', 'スピーチ', '文', '掲載', 'これ', '大丈夫', '添削', 'メッセージ', 'の', '一', '晩', '3', '人', '位', '人', '添削', '自分', '以外', '人', 'たくさん', '相談', 'サイト', 'よう', '添削', 'お願い', '投稿', '由利', 'さん', 'ためし', 'サイト', '確か', '結婚式', 'スピーチ', '添削', 'お願い', '投稿', '1000', '件', '結婚式', '影', 'ネット', 'コミュニテ ィ', '事前', 'お願い', 'スピーチ', '準備', '一番', '嫌', 'の', '何', 'サプライズスピーチ', 'の', '昨年', '10', '万', '以上', 'お祝い', 'お祝い', '貧乏', '独', '女', '薫', 'さん', '35', '歳', '私', '基本', '的', '人前', 'の', '苦手', 'ん', '指名', 'しどろもどろ', '何', '自己', '嫌悪', '後', 'サプライズスピーチ', 'メリット', '準備', '状態', 'フランク', '本音', 'さ', 'よう', 'それ', '上手', '対応', '人', '苦手', '人', '場合', 'フランク', 'しどろもどろ', '危険', '性', '大', 'プロ', '司会', '者', '場合', '本当', 'サプライズ', '式', '最中', 'サプライズスピーチ', '指名', '一言', 'こと', 'よう', '薫', 'さん', '曰く', '何分', '前', '無理', 'サプライズ', 'タイプ', '人選', '大切', 'こと', 'ネット', '例文', '検索', '際', '方法', 'の', '幸恵', 'さん', '30', '歳', 'スピーチ', '手紙', '形式', 'スピーチ', 'もの', 'ちゃん', 'みたい', '感じ', '新婦', '友人', '手紙', 'やり方', 'これ', 'フランク', '書き方', '大丈夫', '暗記', 'こと', 'もの', '友人', '記念', '幸恵', 'さん', '確か', 'これ', '人前', 'の', '苦手', '人', '失敗', '主役', '新郎', '新婦', '緊張', '内容', 'あれこれ', 'リハーサル', 'スピーチ', '担当', '独', '女', 'たち', '幸', '高山', '惠']

1文目では、このような結果が得られました。


コーパスの作成

 続いて、コーパスを作成します。コーパスとは、「言語学において、自然言語処理の研究に用いるため、自然言語の文章を構造化し大規模に集積したもの。」(引用:wikipedia) で、これをトレーニング・テストデータとして機械学習に利用します。


main.py

from gensim import corpora

dictionary = corpora.Dictionary(words)
dictionary.filter_extremes(no_below = 200, no_above = 0.2)
#dictionary.save_as_text("./tmp/dictionary.txt") で、作成した辞書を保存可能
#dictionary = corpora.Dictionary.load_from_text("./tmp/dictionary.txt") で読み込み
courpus = [dictionary.doc2bow(word) for word in words]


 まず、1行目で先ほど区切った名詞一つ一つにIDを設定しています。これはただの識別番号なので、前から順に0,1,2...と付けられています。また、これを辞書といい、別途txtファイルに保存しておくことが出来ます。そうすることで、毎回ファイルを読み込んで変換して~という作業がいらなくなります。

 次に2行目で、チューニングのようなことをしています。


  • no_below : 単語が使われている文章の数が設定値未満の時、その単語を削除

  • no_above : 単語が使われている文章の割合が設定値以上のとき、その単語を削除

  • keen_n : 上記二つの設定にかかわらず、指定した数の単語を保持

  • keep_tokens : 指定した単語を保持

 これらを用いてチューニングすることで、"ん"や、"の"などの、どんな記事にも入っているような文字や、記事の分類に使えないレベルでしか使われていない単語を削除することで、データの質を上げることが出来ます。

 実際に courpus の値を出力してみると、

[(0, 1), (1, 1), (2, 1), (3, 2), (4, 4), (5, 1), (6, 3), (7, 1), (8, 1), (9, 1), (10, 1), (11, 6), (12, 1), (13, 1), (14, 1), (15, 1), (16, 1), (17, 1), (18, 2), (19, 5), (20, 1), (21, 1), (22, 1)]

このような形でコーパスが得られます。左がID、右が頻度を示しています。

 これでコーパスを生成することが出来ましたが、これをさらに特徴ベクトルに変換することが出来ます。


特徴ベクトルへの変換

 この特徴ベクトルの変換を行うことで、その名の通り、かく文章の特徴をベクトル化することができ、より各文章の特徴を捉えられたコーパスを作成することができます。


main.py

from gensim import matutils

def vec2dense(vec, num_terms):
return list(matutils.corpus2dense([vec], num_terms=num_terms).T[0])
data_all = [vec2dense(dictionary.doc2bow(words[i]),len(dictionary)) for i in range(len(words))]


参考 : http://kento1109.hatenablog.com/entry/2017/11/15/230909

 これを行うことで、特徴ベクトルに変換されたコーパスが取得できます

出力結果は、

[1.0, 1.0, 1.0, 2.0, 4.0, 1.0, 3.0, 1.0, 1.0, 1.0, 1.0, 6.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 5.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

このような形になりました。これで一記事分になっています。途中から 0,0 が続いてるのは、記事の特徴を表すものとしては弱い。と判断されているのかな?っといった感じです。

 ひとまずこれで、データの整形は完了となります。


SVMを用いた機械学習

 では実際にSVMを用いた教師あり学習をしてみます。


main.py

from sklearn.model_selection import train_test_split

from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

#トレーニング・テストデータの設定
train_data = data_all
X_train, X_test, y_train, y_test = train_test_split(train_data, labels, test_size=0.4, random_state=1)

#データの標準化
sc = StandardScaler()
sc.fit(X_train)
X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)

#学習モデルの作成
clf = SVC(C = 1, kernel = 'rbf')
clf.fit(X_train_std, y_train)


 まず、データセット、ラベルを train_test_split を用いてトレーニング用とテスト用に分けます。このとき、テストデータの割合は40%としました。

 次に、StandardScaler を用いてデータの標準化を行います。

 最後に学習モデルの作成を行いました。この時、kernel = 'rbf' と設定することで、クラス分類問題で非線形な識別を可能にするカーネルSVMを用いています。

学習が完了したので、テストデータでの予測の正解率を見てみます。


main.py

score = clf.score(X_test_std, y_test)

print("{:.3g}".format(score))
# 0.928

正解率92.8%と、かなり高い確率となりました。


未知のデータの分類を予測

では次に、正解ラベルの与えられていない、未知のデータに対する予測をしてみます


main.py

#ファイルの読み込み、及びタイトル・本文データの取得

pathes = os.listdir("./text/test/")
for path in pathes:
test_list = []
test_doc = ""
with open("./text/test/"+path, encoding="utf-8_sig") as f1:
next(f1)
next(f1)
test_doc = f1.read()
test_list.append(test_doc)

#コーパスを生成
test_words = analysis.get_words(test_list)
test_dense = [vec2dense(dictionary.doc2bow(test_words[i]),len(dictionary)) for i in range(len(test_words))]

#先に作成したモデルを用いて予測
predicted0 = clf.predict(test_dense)
print(path_list[int(predicted0)])


 今、./text/test/には、"dokujo-tsushin", "it-life-hack", "kaden-channel" という順番で、今まで使っていない未知のデータが三つ入っています。今度はこれを用いて、それぞれがどのニュースサイトの記事がを分類しました。

 やっていることは今までと同じで、まず三つのファイルをよみこみ、コーパスを生成、そしてそれを学習にかけて、そのときの判別を出力しています。この時の出力結果が、

dokujo-tsushin

it-life-hack
kaden-channel

となりました。よって、三つの未知のデータを予測した結果、すべて正解することが出来ました。

 余談ですが、試しに livedoor-homme というサイトの記事を予測にかけたところ、dokujo-tsushin という結果が出ました。つまり、学習データから予測すると、ライブドアホームというサイトの記事は独女通信の記事と内容のジャンルが似ている。ということでしょうか。


(おまけ)全8つのサイトで学習

試しに、全8つのサイト。総計6594個のデータセットで学習してみました。

すると、結果は 87.9% という正解率に。予想以上に高かったです。また、出力にかかった時間は約42秒。こちらも予想以上に速いです。

データ数は非常に多いですが、すべて文字、文章ということもあり、そこまで処理に負荷はかからないということでしょうか。


チューニング

3つのサイトで分類をしたときのプログラムの、辞書の設定、モデルの設定を弄ってみて、チューニングをしてみたいと思います。


main.py

dictionary.filter_extremes(no_below = 200, no_above = 0.2)

clf = SVC(C = 1, kernel = 'rbf')

変更するのは、これらの no_below, no_above, C の三つのパラメータです。

実際に変更を加えた結果を表にすると、

no_below
no_above
C
正解率

200
0.2
1.0
0.928

0

0.77

50

0.933

100

0.935

150

0.927

300

0.89

500

0.542

200
0.1
1.0
0.81

0.3

0.926

0.4

0.938

0.5

0.936

0.6

0.934

0.7

0.937

0.8

0.937

0.9

0.933

1.0

0.933

200
0..2
0.1
0.827

2
0.934

3
0.933

10
0.929

100
0.4
1.0
0.937

200
0.4
2
0.939

複数パターンを色々試したところ、no_below=200, no_bove = 0.4, C=2 という設定が最も正解率が高くなりました。

 しかし、今回は3パターンの中から分類。という状況設定だったため、いくつの中から分類を分けるかでも設定すべきパラメータは変わってくると思われます。特に、no_above は記事全体の何割に単語が入っているか。というフィルターのため、記事のパターンの影響を最も受けやすいのではないかと思われます。


まとめ

 ニュースの記事の分類をやってみて思ったのは、日本語の文章ってめちゃくちゃ自然言語処理に向いてないなって思いました。英語なら単語ごとに区切りがあるのでとても分かりやすいですが、日本語は日本人がみても正しい単語の区切りが出来ない場合もあります。それもあって、機械学習にかけるところではなく、そのデータを整形するところが一番時間がかかりました。

また、今回のコードはこちらにあります。

 ゲームの勝敗予測、ニュース記事の分類とやったので、次は画像の識別・分類を、または教師なし学習をやってみたいと考えています。