自然言語処理のおいて、迷惑メールの識別などで有名なナイーブベイズを用いた、テキストデータの識別手法について、実装・解説します。
本シリーズでは、Pythonを使用して機械学習を実装する方法を解説します。
各アルゴリズムの数式だけでなく、その心、意図を解説していきたいと考えています。
ナイーブベイズは、以下のscikit-learnマップの黒矢印に対応します。
START→データが50以上→カテゴリーデータ→ラベルありデータ→データ数10万以下→Linear SVC失敗→テキストデータ→[ナイーブベイズ]
まず今回やりたいことの概要を説明します。
今回扱うデータはlivedoorのニュースコーパスから取得した7367個のニュース記事です。
各ニュースは、トピックニュース、Sports Watch、ITライフハック、家電チャネル、MOVIE ENTER、独女通信、エスマックス、livedoor HOMME、Peachyの9カテゴリのいずれかに属しています。
今回やりたいことは、このニュース記事を学習データとテストデータに分けて、ナイーブベイズにより識別器を作成し、テストデータのニュース記事を入力したときに、そのニュース記事が9つのカテゴリのどれに属するのかを判定することです。
そのためには、時系列データのように、テキストデータを扱いやすい数値データへと変換する必要があります。
そこで今回はまず、テキストデータを名詞や動詞、形容詞、助詞などに分けます。
これを形態素解析や、分かち書きと呼びます。
次に学習データの各単語をナンバリングしていきます。
そして各ニュース記事に対して、記事内でのそれぞれの単語の出現数を要素に持つベクトルを生成します。
もし学習データに含まれる単語の数が1万個であれば、1万次元のベクトルとなり、その単語が出現した回数が格納されます。
最後に、この1万次元のベクトルたちをナイーブベイズで分類するという流れを行います。
形態素解析・分かち書き
まずはじめに形態素解析による分かち書きの実装を紹介します。
●Pythonによるスクレイピング&機械学習を参考にしています。
データとして、livedoorニュースコーパスの記事を使用します。
ldcc-20140209.tar.gzをダウンロードします。
形態素解析には有名なライブラリとして、MeCabとJanomeがありますが、MeCabはWindowsでは動かしにくいので、Janomeを使用します。
Anaconda Promptを開いて、
1 | pip install janome |
でJanomeのライブラリをインストールします。
そして、以下のwakati.pyを実行します。
途中エラー吐く変なファイルがひとつあったので、それは除外しました(smax-6909466.txt)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | from janome.tokenizer import Tokenizer import os, glob #Janomeを使って形態素解析 ja_tokenizer=Tokenizer() #日本語を単語や品詞ごとに分ける def ja_tokenize(text): res=[] lines=text.split("\n") lines=lines[2:] #最初の2行はヘッダーなので捨てる for line in lines: malist=ja_tokenizer.tokenize(line) for tok in malist: ps=tok.part_of_speech.split(",")[0] if not ps in ['名詞', '動詞', '形容詞']:continue #他の品詞は無視 w=tok.base_form if w=="*" or w=="": w=tok.surface if w=="" or w=="\n": continue res.append(w) res.append("\n") return res #テストデータを読み込み root_dir ='C:/naivebayes/text' for path in glob.glob(root_dir + "/*/*.txt", recursive=True): if path.find("LICENSE")>0: continue #LICENSE.txtは除く print(path) path_wakati=path + ".wakati" if os.path.exists(path_wakati): continue #ファイルができているときはスルー text=open(path,"r", encoding='utf-8').read() #エンコーディングに注意 words=ja_tokenize(text) wt=" ".join(words) open(path_wakati, "w", encoding="utf-8").write(wt) |
少し時間がかかりますが、以上のファイルを実行することで、ニュース記事から名詞、動詞、形容詞だけが分離されます。
(元ニュース)
(分かち書き)
ニュースをベクトルに変換
次に、単語ごとにナンバリングして、ニュース記事をベクトルにします。
この操作をBoW(Bag-of-Words)とも呼びます。
まずはじめの全ニュースの単語を辞書にします。
そして、各ニュースの単語の出現回数をベクトルにします。
今回は100ニュースずつデータを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | import os, glob, json #パスの設定 root_dir ='C:/naivebayes/text' dic_file=root_dir+"/word-dic.json" data_file=root_dir+"/textdata.json" #辞書、ハッシュマップ的な word_dic={"_MAX":0} #辞書作成関連の関数たち------------------------------------------------- #辞書に全部の単語を登録する def register_dic(): files=glob.glob(root_dir+"/*/*.wakati", recursive=True) for i in files: file_to_ids(i) #ファイルを読んで固定長シーケンスを返す def file_to_ids(fname): with open(fname, "r", encoding='utf-8') as f: text=f.read() return text_to_ids(text) #語句を区切ってラベリングする def text_to_ids(text): text=text.strip() words=text.split(" ") result=[] for n in words: n=n.strip() if n=="": continue if not n in word_dic: #まだ登録していない言葉の場合 wid=word_dic[n]=word_dic["_MAX"] word_dic["_MAX"]+=1 print(wid,n) else: wid=word_dic[n] #登録済みの言葉の場合 result.append(wid) return result #ベクトル作成関連の関数たち------------------------------------------------- #ジャンルごとにファイルを読み込む def count_freq(limit=0): X=[] Y=[] max_words = word_dic["_MAX"] cat_names=[] for cat in os.listdir(root_dir): cat_dir =root_dir + "/" + cat if not os.path.isdir(cat_dir):continue #フォルダは無視する cat_idx=len(cat_names) cat_names.append(cat) files=glob.glob(cat_dir+"/*.wakati") i=0 for path in files: #print(path) cnt=count_file_freq(path) X.append(cnt) Y.append(cat_idx) if limit > 0: if i >limit : break i+=1 return X,Y #ファイル内の単語を数える def count_file_freq(fname): cnt=[0 for n in range(word_dic["_MAX"])] with open(fname,"r", encoding='utf-8') as f: text=f.read().strip() ids=text_to_ids(text) for wid in ids: cnt[wid]+=1 return cnt #------------------------------------------- #単語辞書の作成 if os.path.exists(dic_file): word_dic =json.load(open(dic_file)) else: register_dic() json.dump(word_dic, open(dic_file,"w", encoding='utf-8')) #ファイルごとの単語出現頻度のベクトルを作る print ("要素数=" + str(len(word_dic))) X, Y=count_freq(100) json.dump({"X": X, "Y":Y}, open(data_file,"w", encoding='utf-8')) print("ファイル変換終了") |
このプログラムにより、各ニュースをベクトルにしたtextdata.jsonが生成されます。
時間がまずまずかかります。
ニュースを識別
最後にニュース記事を学習データとテストデータに分割し、ナイーブベイズで学習、識別します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # 1:ライブラリのインポート-------------------------------- from sklearn import naive_bayes, metrics, preprocessing, cross_validation #機械学習用のライブラリを利用 import json import numpy #numpyという行列などを扱うライブラリを利用 # 2:データ準備 nb_classes=9 data=json.load(open("C:/naivebayes/text/textdata.json")) X=data["X"] #単語ベクトル Y=data["Y"] #クラスラベル max_words=len(X[0]) #最大単語数 #解説 3:機械学習で分類・識別する--------------------------------------------------- clf = naive_bayes.MultinomialNB(alpha=0.1, fit_prior='True' ) # 4:K分割交差検証(cross validation)で性能を評価する--------------------- scores=cross_validation.cross_val_score(clf, X, Y, cv=10) print("平均正解率 = ", scores.mean()) print("正解率の標準偏差 = ", scores.std()) # 5:トレーニングデータとテストデータに分ける------------------ #X_train, X_test, Y_train, Y_test=cross_validation.train_test_split(X,Y, test_size=0.5, random_state=0) #clf.fit(X_train, Y_train) #print(clf.score(X_test, Y_test)) #テストデータに対する識別結果 |
#解説 3:機械学習で分類・識別する
clf = naive_bayes.MultinomialNB(alpha=0.1, fit_prior=’True’ )
の部分だけ新しいので説明します。
MultinominalNBはナイーブベイズ手法のうちの多項分布を使用したナイーブベイズを指定しています。
alphaは学習時に出てこなかった単語が、テストデータの記事に出てきたときに、生成確率が0になるのを避けるための調整パラメータです。
ちょっと分かりにくいですが、学習データ全てに、「機械学習」という言葉が一切出てこなかった場合に、テストデータに「機械学習」が入った文章はどのクラスに属する確率も0になってしまいます。
それを避けるための微小値です。
(alpha=1をラプラス・スムージング、alpha<1の場合をLidstone smoothingと呼びます)
fit_prior=’True’は、学習データの偏りがあった場合に、それを考慮するかどうかです。
今はTrueなので、偏りがあった場合に考慮しています。
それでは「結局、ナイーブベイズって何をやっていたの?」
ナイーブって日本語では、純朴とか”うぶ”っていう意味で使いますが、
「うぶなベイズって何?」
を説明します。
ナイーブベイズの心
正確な情報は以下をご覧ください。
●scikit-learnの多項分布ナイーブベイズの解説ページ
ナイーブベイズのナイーブは、うぶという意味ではなく、”先入観のない”という意味です。
英語のnaiveにはそういう意味もあります。
何に対して先入観がないかというと、
「特徴ベクトルの各要素間に事前知識がなく独立だと仮定する」という意味です。
ちょっと難しくなったので丁寧に説明します。
いまテキスト分類において、文章が単語ごとのベクトルになっています。
ベクトルの要素(各次元)はひとつの単語を現しています。
もしニュースのなかでiPhoneという単語があったとすると、Appleという単語も同じニュースの中で出てくる確率が高いと思われます。
それは私たちがiPhoneはAppleが作っているという事前知識を持っているからです。
こうした事前知識を考慮せず、iPhoneという単語があろうと、なかろうと、同じニュースのなかで、Appleという単語が出現する確率は同じである
(つまりiPhoneとAppleという単語は独立である)
と仮定することがナイーブベイズのnaiveの意味するところです。
これさえ分かれば、あとはニュース記事をベイズ推定に従って分類するだけです。
ベイズ推定とか難しそうですが、今回の多項分布の場合はとても単純です。
例えばラベル1の学習データにiPhoneという言葉が15回出現し、ラベル2では5回だったとします。
そしてテストデータの記事にiPhoneという言葉が出てくれば、ラベル1である確率は75%である、とするだけです。
これを全部の単語について掛け算して、テストデータの記事がどのラベルに属しているっぽいかを判定します。
そのときに、1度も出てきたことがない単語があると、確率0が掛け算されてしまうので、全部のクラスで全部の単語に対して、alphaだけ、出現回数を足しておきます。
式を使って説明しているページでは以下の記事が分かりやすいです。
以上、Pythonとscikit-learnで学ぶ機械学習入門|第7回:ナイーブベイズによるテキスト分類でした。
次回は、識別器のパラメータチューニングと識別結果の解析手法について解説します。