実践型AIプログラミング特講 最終章その2 #41

実践型AIプログラミング特講 最終章その2 #41

最終回の第二弾ということで今回は業務システムに機械学習を実装する、そしてその実例としてニュース記事をジャンルごとにフィルタリングしてカテゴライズする機能を機械学習を通して導入していこうと思います。

前回の記事

ニュース記事を自動でジャンル分けしよう

学習済みデータの保存と読み込み方法についてご紹介しました。本記事では、それをもう少し複雑な例題のなかで利用してみましょう。具体的には、TF-IDF とディープラーニングを使って、ユーザーの投稿を自動でジャンル分けするプログラムを作ります。まず最初に、TF-IDF と scikit-learn を用いてジャンル分けのプログラムを作成し、その後 scikit-learn で記述したプログラムを、学習済みデータの保存と読み込みを利用したディープラーニングのプログラムに書き換えてみましょう。

ニュース記事を自動でジャンル分けしよう

インターネットでは、日々大量のメッセージがやりとりされています。それらのメッセージは多すぎて、なかなかすべてのトピックを追うことは難しくなっています。そこで、自動的に記事を分類し、興味のある分野を読むことができれば便利です。今回は、大量のニュース記事を自動で分類することに挑戦してみましょう。大量のニュース記事とそれがどのジャンルなのかの情報を教師データとして与え、機械学習を利用して未知の文章のジャンルを判定します。

TF-IDF について

さて、『BoW(Bag-of-Words)』の手法を使って文章をベクトルデータに変換しました。これは、文章にどの単語がどのくらいの頻度で利用されているかを調べるというものでした。『TF-IDF』も基本的には BoW と同じで、文章を数値ベクトルに変換します。ただし、単語の出現頻度に加えて、文書全体における単語の重要度を考慮します。
TF-IDF では、文書内における特徴的な単語を見つけることを重視します。その手法として、学習させるすべての文書で、その単語がどのくらいの頻度で使われているかを調べます。たとえば、どの文章にも存在する、ありふれた単語「です」や「ます」の重要度を低くし、その他の文書では見られない希少な単語があれば、その単語を重要と見なして計算を行います。つまり、単語の出現回数を数えるだけでなく、出現頻度の高い単語のレートを下げ、特徴的な単語のレートを高く評価する方法で、単語をベクトル化します。以下の計算式は、TF-IDF が、各単語ごとの値をどのように計算するかを表しています。以下の中。tf(t.d) は文書内における単語の出現頻度であり、idf(t) は全文書における単語の出現頻度を表してます。

TF_IDF(t) =tf(t, d) × idf(t)

なお、全文書における単語の出現頻度である、idf(t) の計算式は以下のようになります。df(d, t)は単語tを含む文書数で、分子のDは文書の総数を表しています。

idf (t) = log at(d, t)

公式にすると少し難しく思えるかもしれません。しかし、簡単に言うと、文書内の単語の出現頻度に、その単語の重要度(全文書における単語の出現頻度の対数)を掛け合わせるだけのものです。TF-IDF を使うことで、単に単語の出現頻度を数えるよりも、ベクトル化の精度向上が期待できます。

TF-IDF のモジュールを作ってみよう

それでは、テキストを学習させてみましょう。TF-IDF を実践する場合、scikit-learn の『TfidfVectorizer』も有名ですが、追加で日本語への対応処理が必要で今回は用いていません。TF-IDF はそれほど難しいものではありません。もし、業務で TF-IDF を実践したい場合、そのデータが膨大であれば、データベースとの連携も視野に入れる必要があるでしょう。そこで、TF-IDF の実装例を示すために、自分でモジュールを作成してみましょう。以下が、TF-IDF のモジュールを実装したちのです。

# TF-IDF でテキストをベクトル化するモジュール
import MeCab
import pickle
import numpy as np
# MeCab の初期化(*1)
tagger =MeCab.Tagger(“-d /var/lib/mecab/dic/mecab-ipadic-neologd”)
# グローバル変数(*2)
rord_dic = {‘_id’: 0} #単語辞書
dt_dic={} # 文書全体での単語の出現回数
files =[] # 全文書を IDで保存
def tokenize (text):
#mecabで形態素解析を行う
result = []
word_s=tagger.parse (text)
for n in word_s.split(“\n”):
if n ==’EOS’ or n == ”: continue
p =n.split(“\t”)[1].split(“,”)
h, h2, org = (p[0], p[1], p[6])
if not (h in [‘名詞’,’動詞’,’ 動詞’, ‘形容詞 ‘) ): continue
if h == ‘名詞’ and h2 =’数’: continue
result.append (org)
return resultdef words_to_ids (words, auto_add,True):
# 単語一覧を ID の一覧に変換する(*4)
result =[]
for w in words:
if w in word_dic:
result.append (word_dic [w])
continue
elif auto_add:
id = word_dic[w]= word dic[‘_id’]
word_dic[‘_id’] += 1
result.append (id)
return result
def add_text(text):
# テキストを ID リストに変換して追加(*5)
ids = words_to_ids(tokenize(text))
files.append (ids)
def add_file(path):
# テキストファイルを学習用に追加する(*6)
with open(path, “r”, encoding=”utf-8″) as f:
s =f.read()
add_text (s)
def calc_files():
# 追加したファイルを計算(*7)

global dt_dic
result = []
doc_count = len (files)
dt_dic = {}
# 単語の出現頻度を数える(* 8)
for words in files:
used_word ={}
data = np.zeros (word_dic[‘_id’])
for id in words:
data[id] += 1
used_word[id]=1
# 単語tが使われていれば dt_dicを加算(*9)
for id in used_word:
if not(id in dt_dic): dt_dic[id] = 0
dt_dic[id] += 1
# 出現回数を割合に直す(*10)
data = data / len(words)
result.append (data)
# TF-IDF を計算(*11)
for i, doc in enumerate (result):
for id, v in enumerate (doc):
idf = np.log (doc_count / dt_dic[id]) + 1
doc [id]=min([doc[id] * idf, 1.0])
result [i]= doc
return result
def save_dic(fname):
# 辞書をファイルへ保存する(*12)
pickle.dump(
[word_dic, dt_dic, files],
open (fname, “wb”))
def load_dic(fname):
# 辞書をファイルから読み込む(*13)
global word_dic, dt_dic, files
pickle.load (open (fname, ‘rb’))
word_dic, dt_dic, files = n
def calc_text (text):
# 辞書を更新せずにベクトル変換する(*14)
data = np.zeros (word_dic[‘_id’])
words =words_to_ids (tokenize (text), False)
for w in words:
data [w] += 1
data = data / len(words)
for id, v in enumerate (data):
idf = np.log(len(files) / dt_dic[id]) + 1
data[id] +=1
min([data[id] * idf, 1.0])
return data
# モジュールのテスト(*15)
if -_name__ =:’__main__’:
add_text (‘雨 ‘)
add_text(‘今日は、雨が降った。)
add_text(‘今日は暑い日だったけど雨が降った。)
add_text(‘今日も雨だ。でも日曜だ。’)
print (calc_files())
print (word_dic)

Python でモジュールを作るのは非常に簡単で、ファイル内で関数を定義するだけでモジュールとなります。ただし、Jupyter Notebook からモジュールを使う場合には、モジュールを作成した後、ノートを開き直すか、カーネルを再起動する必要があるので、注意しましょう。
また、モジュールとして取り込まれた場合には、変数ファイルとして実行した場合には、この変数に「_main_」が代入されます。この性質を利用するレモジュールのテストが簡単にできます。コマンドラインからモジュールをテストしてみましょう。_name_がモジュール名となり、メイン以下のように表示されます。各配列が1つずつの文章を表しており、その配列の要素が、各単語の出現頻度と重要度を掛け合わせた値となっています。また、末尾に単語辞書と単語IDの一覧も出力しています。辞書は番号順にならないので読みにくいのですが、0が「雨」、1が「今日」、2が「降る」、3が「暑い」、4が「日」です。
ここで、4番目の文章(実行結果の最後の array(.) に注目してみると、「0:今日」や「1:雨」は、他の文章でも使われているので値が低くなり、「5:日曜」は他の文章に使われていない特徴的な単語なので値が高くなっているのがわかります。それでは、プログラムを確認しましょう。プログラムの(※1) では、MeCab を初期化します。ここでは、4章で紹介した辞書「mecab-ipadic-NEologd」を使うように指定しています。この辞書が必要なければ、引数を省略しても良いでしょう。(※2)の部分では、グローバル変数を初期化します。このモジュールで重要な変数で、単語辞書 word_dic と単語の出現回数を記録する辞書 dt_dic、また、全文書を記録するfilesです。(※3)の部分では、MeCab で形態素解析を行います。ここでは、4章と同様の方法で、精度を高めるために、名詞·動詞·形容詞以外の情報を捨てます。(※4)の部分では、単語を ID に変換します。(※5) と(※6)の部分は、テキストをIDリストに変換して、変数 fles に追加する手順をまとめたものです。モジュールを利用する場合、このメソッドを利用することになるでしょう。
そして、TF-IDF の計算を行うのが、(※7)以降の calc_files() 関数です。手順としては、最初に(※8)以降の部分で、単語の出現回数を数え、文書ごとに単語の頻度を計算します。そうすると、文書における単語の希少度合いがわかるので、単語ごとの重要度IDF 値を計算して出現頻度と掛け合わせます。(※9)では、全文書で何回単語が出現しているかを記録する変数 dt_dic を更新し、(※ 10) では、出現回数を割合に直します。
(※11)では、各文書の各単語について重要度を掛け合わせます。プログラムの(※ 12)、(※13) の部分は辞書をファイルへ保存したり、読み込んだりする関数を定義しています。(※14)では、辞書を更新せずに TF-IDF ベクトルに変換する関数を定義しています。
最後の(※15)の部分は、モジュールをテストするコードです。

ニュース記事の分類に挑戦しよう

それでは、TF-IDFを利用して、文章の分類問題を解いていきましょう。そのためには、ある程度しっかり分類された大量の文章が必要となります。
ここでは、しっかりとジャンル分けされたニュース記事が揃っている、livedoor ニュースコーパスを利用してみます。4章でも利用したデータですが、サイトからリンクされている「Idcc-20140209.tar.gz」をダウンロードしたら解凍します。そして、Jupyter Notebook の起動ディレクトリーに <text>というディレクトリーを作り、そこにテキストファイルが入っている各フォルダーをコピーしましょう。it-life-hack livedoor ニュースコーパスを解凍して text フォルダーに配置この livedoor ニュースコーパスには、ニュースごとに記事が分けられています。livedoor ニュースコーパスでは、各ジャンルに870 以上のファイル (1 つのファイルが300字から12000 字ほど)があるので、なかなか本格的な機械学習が実践できます。

文章をTF-IDF のデータベースに変換しよう

それでは最初に、文書のTF-IDF を求め、データベースとして保存するプログラムを見てみましょう。プログラムを実行すると、text フォルダー以下に「genre.pickle」というデータファイルを出力カします。

import os, glob, pickle
import tfidf
# 変数の初期化
y=[]
x=[]
# ディレクトリー内のファイルー覧を処理(*1)
def read_files (path, label):
print(“read_files=”, path)
files = glob.glob(path + “/*.txt”)
for f in files:
if os.path.basename (f)==’LICENSE.txt’: continue
tfidf.add_file(f)
y.append (label)
# ファイル一覧を読む(*2)
read_files (‘text/sports-watch’,0)
read_files(‘text/it-life-hack’, 1)
read_files (‘text/movie-enter’, 2)
read_files (‘text/dokujo-tsushin’,3)
# TF-IDF ベクトルに変換(*3)
x = tfidf.calc_files()
# 保存 (*4)
pickle.dump([y, xl, open(‘text/genre.pickle’, ‘wb’))
tfidf.save_dic(‘text/genre-tdidf.dic’)
print (‘ok’)

プログラムを実行した後で、Jupyter Notebook でどのような値が生成されたか確認してみましょう。ここでは、xの値を表示してみましょう。プログラムを詳しく確認してみましょう。プログラムの(※1)では、ディレクトリー内のファイル一覧をTF-IDF モジュールに追加する処理します。基本的には、glob モジュールでファイル一覧を得て、tfidf.add_file0 関数にパスを渡すだけです。ただし、各フォルダーには著作権情報を記した「LICENSE.txt」があるので、それだけは無視しています。(※2)の部分では、どのフォルダーを読み込むかを指定します。(※3)の部分で、実際に文章を TF-IDF ベクトルに変換します。最後に(※4)の部分で、genre.pickle というファイルへデータを保存します。

TF-IDF をNaiveBayes で学習しよう

ここまでの手順で、TF-IDF のデータベースが作成できたら、まずは、NaiveBayes(ナイーブベイズ)を利用して、データを学習してみましょう。

import pickle
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
import sklearn.metrics as metrics
import numpy as np
# TF-IDF のデータベースを読み込む(*1)
data = pickle.load(open(“text/genre.pickle”, “rb”))
y= data[0]# ラベル
x = data[1] # TF-IDF
# 学習用とテスト用に分ける(*2)
x_train, x_test, y_train, y_test= train_test_split(
x, y, test_size=0.2)
# NaiveBayes で学習
model = GaussianNB()
model.fit(x_train, y_train)
# 評価して結果を出力(*4)
y-pred=model.predict(x_test)
асс=metrics.accuracy_score(y_test, y_pred)
rep = metrics.classification_report(y_test, y_pred)
print (” 正解 =”, acc)
print (rep)

プログラムを実行すると、だいたい 0.92 ほどの精度が出ていることが確認できます。なかなかの精度が出ていますね。
プログラムを確認してみましょう。(※1)の部分では、先ほど作成した TF-IDF のベクトルデータベースを読み込みます。(※2)では、学習用とテスト用に分けます。(※3)では、NaiveBayes で学習して、(※4)ではテストデータで評価し、正解率とレポートを出力します。

ディープラーニングで精度改善を目指そう

ちなみに、分類器のアルゴリズムを NaiveBayes からランダムフォレストに変更してみたところ、精度が少しだけ改善しました。アルゴリズムを変更すると、精度の改善が見込めそうです。ディープラーニングを利用して、精度改善を目指してみましょう。

scikit-learn からディープラーニングへの書き換え

scikit-learn の機械学習を、TensorFlow+Keras の学習に置き換えるのはそれほど難しくありませんが、いくつかポイントがあります。
まず、ラベルデータを One-Hot 形式に直すこと、そして、入力と出力のベクトルサイズを調べてしっかり指定することの2点です。それに、モデルを自分で定義する処理を加えたら、ディープラーニング対応が完了です。以下は、ディープラーニングの MLPを使ったプログラムを作ったところです。上記のポイントを含頭に置いて見ていきましょう。

import pickle
from sklearn.model_selection import train_test_split
import sklearn.metrics as metrics
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop
import matplotlib.pyplot as plt
# 分類するラベルの数(*1)
nb_classes=4
# データベースの読み込み(*2)
data =pickle.load(open (“text/genre.pickle”, “rb”))
y = data[0]# ラベル
x=data [1] # TF-IDF
# ラベルデータをOne-Hot ベクトルに直す(*3)
y=keras.utils.to_categorical (y, nb_classes)
in_size= x[0].shape [0]
# 学習用とテスト用を分ける
x_train, x_test, y_train, y_test =train_test_split(x, y, test_size=0.2)
# MLP モデル構造を定義(*5)
model=Sequential()
model.add (Dense (512, activation=’relu’, input_shape=(in_size,)))
model.add (Dropout (0.2))
model. add (Dense(512, activation=’relu’)
model.add (Dropout (0.2))
model.add (Dense(nb_classes, activation=’softmax’))
# モデルを構築(*6)
model.compile(
loss=’categorical_crossentropy’,
optimizer=RMSprop(),
metrics=[‘accuracy’])
# 学習を実行(*7)
hist =model.fit(x_train, y_train,
batch_size=128,
epochs=20,
verbose=1,
validation_data=(x_test, y_test))
# 評価する (※8)
score = model.evaluate(x_test, y_test, verbose=1)
print (” 正解率 =”, score [1], ‘loss=’, score[0])
# 重みデータを保存(*9)
model.save_weights(‘./text/genre-model.hdf5’)
# 学習の様子をグラフへ描画(* 10)
plt.plot (hist.history[‘accuracy’])
plt.plot (hist.history[‘val_accuracy’])
plt.title(‘Accuracy’)
plt.legend ([‘train’, ‘test’], loc=’upper left’)
plt.show ()

プログラムを実行してみましょう。すると、だいたい 0.98 程度、良いときには0.99 という結果が出ガされます。このように、ディープラーニングの MLP を使うことで大幅な精度向上が実現できました。ディープラーニングの威力が実感できる例題となりましたね。
プログラムを確認してみましょう。(※1)の部分では、分類ラベルの数を指定します。ここではスポーツ、IT、映画、ライフの4クラス分類です。(※2)の部分では、データベースを読み込みます。そして、(※3)ではラベルデータを、一次元のリストから One-Hot ベクトルに直します。(※4)の部分では、データを学習用とテスト用に分割します。(※5)の部分でMLP のモデルを定義して、(※6)でモデルを構築します。これで、学習のための準備ができたので、(※7)の部分で学習を実行します。
そして、(※8)の部分でテストデータを用いて評価して、正解率を表示します。最後に、(※9)の部分で学習した重みデータをファイルへ保存し、(※ 10) で学習の様子をグラフ描画します。

自分で文章を指定して判定させよう

それでは、自分で作成した文章を判定させてここまでの成果を確かめてみましょう。ここでは、次の3つの文章を判定させてみようと思います。正しいジャンルに判定できるでしょうか。
(1) 野球を観るのは楽しいものです。試合だけでなくインタビューも楽しみです。
(2) 常に iPhone と iPad を持っているので、二口のモバイルバッテリがあると便利。
(3) 幸せな結婚の秘訣は何でしょうか。夫には敬意を、妻には愛情を示すことが大切。
以下が、先ほど学習したディープラーニングの MLP の重みデータを利用して、テキストの判定を行うプログラムです。

import pickle, tfidf
import numpy as np
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout
rom keras.optimizers import RMSprop
from keras.models import model_from_json
# 独自のテキストを指定(* 1)
text1 =”””
野球を観るのは楽しいものです。
試合だけでなくインタビューも楽しみです。
“””
text2 =”””
常にiPhone と iPadを持っているので、
ニロあるモバイルバッテリがあると便利。
“””
text3=”””
幸せな結婚の秘訣は何でしょうか。
夫には敬意を、妻には愛情を示すことが大切。
“””
# TF-IDF の辞書を読み込む (※2)
tfidf.load_dic(“text/genre-tdidf.dic”)
# Keras のモデルを定義して重みデータを読み込む(*3)
nb_classes=4
model =Sequential ()
model.add (Dense (512, activation=’relu’,
input_shape=(len(tfidf.dt_dic),)))
model.add (Dropout (0.2))
model.add (Dense (512, activation=’relu’))
model.add (Dropout (0.2))
model.add (Dense (nb_classes, activation=’softmax’))
model.compile(
loss=’categorical_crossentropy’,
optimizer=RMSprop(),
metrics=[‘accuracy’])
model.load_weights(‘./text/genre-model.hdf5′)
# テキストを指定して判定(*4)
def check_genre(text):
# ラベルの定義
LABELS =[” スポーツ”, “IT”, “映画”,”ライフ”]
# TF-IDF のベクトルに変換(*5)
data = tfidf.calc_text(text)
# MLP で予測(*6)
pre=model.predict(np.array([data])) [0]
n = pre.argmax()
print (LABELS [n], “(“, pre [n], “)”)
return LABELS [n], float(pre [n]), int (n)
if __name__==__main__’ :
check_genre (text1)
check_genre (text2)
check_genre (text3)

プログラムを実行してみましょう。今回は、次のように、「スポーツ」「IT」「ライフ」と正しく判定させることができました。
プログラムを確認してみましょう。プログラムの(※ 1)では、今回判定させてみたかったテキストを3つ用意しました。プログラムの(※ 2) の部分では、本節のプログラム「makedb_tfid.py」で作成したTF-IDF の辞書データを読み込みます。(※3)の部分では、前回作成したKeras のモデルをそのまま定義して、学習済みの重みデータを読み込みます。(※4)の部分でテキストを指定して判定します。(※5)の部分のように、tfidf モジュールの calc_text)を呼び出すと、文章を TF-IDF のベクトルデータに変換できます。(※ 6) の部分で、MLP のモデルでベクトルデータを与えて予測を行い、結果およびその確率を表示します。

本節で作成した「my_text.py」ですが、実際に自分の好きな文章を書き込んで実行してみると、ときどき思い通りの結果が出ないこともあります。どうしてでしょうか。そもそも、TF-IDF のベクトルデータでは、学習済みの単語しかベクトル化できません。今回作成したモジュールでは、livedoorニュースコーパスに出てこない、未知語を見つけると、単語をなかったことにする処理にしてあります。そのため、学習したことのない単語が多く出てくるほど、判定結果が微妙になります。そこで、業務では未知語が出てきたら覚えておいて、改めて学習をやり直すなど、工夫が必要になります。

この節のまとめ

大量のニュース記事を学習させることで、ニュースの分類ができるようになる
テキストデータのベクトル化にTF-IDF を使うことができる
テキストの分類もディープラーニングを使うと大幅な精度の向上が期待できる

未分類カテゴリの最新記事