ナイーブベイズによるブログの分類

こんにちは。第二ソリューションの K.S です。

今回は、ナイーブベイズについて調べたのでナイーブベイズを使って弊社のブログを分類してみたいと思います。
過去のスタッフブログと技術ブログを学習データとして使い、直近に掲載されたブログをスタッフブログか技術ブログかを分類してみます。

ナイーブベイズとは

ナイーブベイズとは、以下の式で表すことができます。
\[
P(y|x) = \frac{P(x|y) \times P(y)}{P(x)}
\]
上の式は乗法定理より導出できます。
\[
P(y,x) = P(x|y) \times P(y) = P(y|x) \times P(x) \to P(y|x) = \frac{P(x|y) \times P(y)}{P(x)}
\]
\( P(y,x):x,yが同時に発生する確率 \)
\( P(y|x):結果 x を観測したとき、原因 y である確率 \)
\( P(x|y):結果 x を観測したときの原因 y の尤もらしさを表す尤度 \)
\( P(x):周辺確率(P(x) = \sum_{y \in Y} P(y,x)) \)
\( P(y):事前確率 \)

弊社のブログについて

弊社のブログはスタッフブログと技術ブログの2種類あります。
スタッフブログは、社会人生活、就活などについての議事が多く技術ブログはIT技術についての議事が多いのが特徴となっています。
スタッフブログは2019年7月から始まっており、技術ブログは2021年2月から始まっています。
2024年6月末までのブログの投稿数はスタッフブログで201件、技術ブログは54件となっています。

ナイーブベイズによるブログの分類

y はカテゴリ、x はブログに出現する単語の集合を表します。
\[
y \in \{スタッフブログ, 技術ブログ\}, x \in \{ 単語1, 単語2,・・・,単語n\}
\]

ブログを単語に分解してその単語のカテゴリ別の出現確率をもとにブログが各カテゴリに属する確率を計算します。
\( P_{y|x}(staff|ブログ1の単語1, ブログ1の単語2, ・・・, ブログ1の単語n) \)

\( P_{y|x}(tech|ブログ1の単語1, ブログ1の単語2, ・・・, ブログ1の単語n) \)
の大小関係を比較し
\( P_{y|x}(staff|ブログ1の単語1, ブログ1の単語2, ・・・, ブログ1の単語n) \ge P_{y|x}(tech|ブログ1の単語1, ブログ1の単語2, ・・・, ブログ1の単語n) \)
の場合、ブログ1はスタッフブログと判定します。

これだけでも scikit-learn を使うことは出来そうですが、もう少し式を展開してみます。
ナイーブベイズでは、各単語は独立に発生するとします。
\( P_{y|x}(staff|ブログ1の単語1, ブログ1の単語2, ・・・, ブログ1の単語n) = P_{y|x}(staff|ブログ1の単語1) \times P_{y|x}(staff|ブログ1の単語2) \times ・・・\times P_{y|x}(staff|ブログ1の単語n) \)
\[
P_{y|x}(staff|ブログ1の単語1) = \frac{P_{x|y}(ブログ1の単語1|staff) \times P(staff)}{\sum_{y \in \{staff, tech\}} P_{x|y}(ブログ1の単語1|y)}
\]
\[
P_{y|x}(staff|ブログ1の単語2) = \frac{P_{x|y}(ブログ1の単語2|staff) \times P_{y|x}(staff|ブログ1の単語1)}{\sum_{y \in \{staff, tech\}} P_{x|y}(ブログ1の単語2|y)}
\]
tech の方も同様に求まります。分母が同じであることから分子だけを計算すればよいことがわかります。
例えば、以下のように確率を設定すれば計算ができます。
\[
P(staff) = \frac{staffブログ数}{staffブログ数 + techブログ数}
\]
\[
P(ブログ1の単語1|staff) = \frac{全スタッフブログで単語1が出現する回数}{スタッフブログで出現する全単語数}
\]
\[
P(ブログ1の単語1|tech) = \frac{全技術ブログで単語1が出現する回数}{技術ブログで出現する全単語数}
\]

プログラム

準備

全部のスタッフブログ(https://www.tasc.co.jp/category/staff)と全部の技術ブログから HTML のタグを除いてテキストにしたものをフォルダに格納します。
フォルダはスタッフブログ、技術ブログ、テスト用ブログを用意しております。

フォルダ名 格納ファイル
スタッフブログ スタッフブログ開始~2024年6月末
技術ブログ 技術ブログ開始~2024年5月末
テストブログ 2024年7月に投稿されたスタッフブログ

2024年6月に投稿された技術ブログ

学習

学習は以下のように行います。
ブログを一件毎に読み込み MeCab にて単語(名詞、形容詞、動詞)に分割し、ブログ毎に単語一覧を作成します。
スタッフブログフォルダから読込んだブログには 0(スタッフ)、技術ブログフォルダから読込んだブログには 1(技術) という印しをつけておきます。

ブログ 分類 単語
ブログ1 0 こんにちは EXCEL マクロ ・・・
ブログ2 1 仕様 プログラム Python ・・・

次に、全ブログの単語一覧を作成します。各ブログで重複する単語は一つにまとめ、ユニークな番号を割り当てます。

No 1 2 3 ・・・ 100 101 102 ・・・
単語 こんにちは EXCEL マクロ ・・・ 仕様 プログラム Python ・・・

これは CountVectorizer で行います。
ブログ全体の単語一覧ができたら、各ブログの単語の出現回数を記入します。

1 2 3 ・・・ 100 101 102 ・・・
こんにちは EXCEL マクロ ・・・ 仕様 プログラム Python
ブログ1 0 1 1 1 ・・・ 0 0 0 ・・・
ブログ2 1 0 0 0 ・・・ 1 1 1 ・・・

評価

今回の評価は、2024年6月に投稿された技術ブログ1件と2024年7月に投稿されたスタッフブログ2件で行います。

学習の結果、確率(P(‘こんにちは’|’staff’)、・・・)が得られます。
これは MultinomialNB で行います。
MultinomialNB の predict_proba は、評価用の各ブログのスタッフブログと技術ブログのそれぞれの確率を返します。
predict は、評価用の各ブログを上記確率の高い方のカテゴリ(staff(0) or tech(1))に分類した結果を返します。

ソースは末尾に添付します。

実行結果

./tech_blog/G20210219.txt    <-技術ブログの読込

./tech_blog/G20240531.txt
./staff_blog/S20190708.txt    <-スタッフブログの読込

./staff_blog/S20240626.txt
./test_blog/G20240620.txt    <-評価用ブログの読込
./test_blog/S20240711.txt    <-評価用ブログの読込
./test_blog/S20240730.txt    <-評価用ブログの読込
blog size = 255
スコア= 0.9880952380952381
評価

staff tech
0 0.0 1.0 G20240620.txt の評価値でスタッフの確率0、技術の確率1
1 1.0 0.0 S20240711.txt の評価値でスタッフの確率1、技術の確率0
2 1.0 0.0 S20240730.txt の評価値でスタッフの確率1、技術の確率0

[1 0 0]    <-1列目の’1’は G20240620.txt が技術ブログであることを表し、2列目、3列目の’0’は S20240711.txt と S20240730.txt がスタッフブログであることを表している

感想

  • 各単語の確率を参照できないかと探したが見つけられなかったので、引き続き調査が必要だと思っている。
  • ブログの中には顔文字などを使っているところや、ブログ間の表記ゆれ(サーバ、サーバー)などの扱いには全く対応しておりません。今後は自然言語処理についても調査が必要だと思った。
  • TFIDF を使う方法もあるようなので習得したいと思った。

ソース


import os
import MeCab
import sys
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from pandas import DataFrame

TECH_BLOG_PATH = “./tech_blog/”
STAFF_BLOG_PATH = “./staff_blog/”
TEST_BLOG_PATH = “./test_blog/”

m = MeCab.Tagger(”)

# 文章から単語を抽出
def splitText(text):
node = m.parseToNode(text)
#print(node)
words = []
while node:
word = node.surface
pos = node.feature.split(‘,’)[0]
if pos in (‘形容詞’, ‘動詞’, ‘名詞’):
words.append(word)
node = node.next
return words

# スタッフブログ
x_staff_blogs = []
# 技術ブログ
x_tech_blogs = []
# 評価用のブログ
x_test_blogs = []
# ブログのタイプ(スタッフブログ:0、技術ブログ:1)
y_blog_class = []

# スタッフブログを読込み単語に分解しリストに追加
staff_blog_size = 0
staff_blog_files = [
fn for fn in os.listdir(STAFF_BLOG_PATH) if os.path.isfile(os.path.join(STAFF_BLOG_PATH, fn))
]
for fn in staff_blog_files:
filename = os.path.join(STAFF_BLOG_PATH, fn)
print(filename)
f = open(filename, ‘r’)
data = f.read()
f.close()
x_staff_blogs.append(splitText(data))
#print(x_staff_blogs[staff_blog_size])
staff_blog_size = staff_blog_size + 1
y_blog_class.append(0)

# 技術ブログを読込み単語に分解しリストに追加
tech_blog_size = 0
tech_blog_files = [
fn for fn in os.listdir(TECH_BLOG_PATH) if os.path.isfile(os.path.join(TECH_BLOG_PATH, fn))
]
for fn in tech_blog_files:
filename = os.path.join(TECH_BLOG_PATH, fn)
print(filename)
f = open(filename, ‘r’)
data = f.read()
f.close()
x_tech_blogs.append(splitText(data))
#print(x_tech_blogs[tech_blog_size])
tech_blog_size = tech_blog_size + 1
y_blog_class.append(1)

# 評価用ブログを読込み単語に分解しリストに追加
test_blog_size = 0
test_blog_files = [
fn for fn in os.listdir(TEST_BLOG_PATH) if os.path.isfile(os.path.join(TEST_BLOG_PATH, fn))
]
for fn in test_blog_files:
filename = os.path.join(TEST_BLOG_PATH, fn)
print(filename)
f = open(filename, ‘r’)
data = f.read()
f.close()
x_test_blogs.append(splitText(data))
#print(x_test_blogs[test_blog_size])
test_blog_size = test_blog_size + 1

all_blogs = x_staff_blogs + x_tech_blogs + x_test_blogs

print(“blog size = “, len(all_blogs))

cv = CountVectorizer(analyzer=lambda x: x)
word_cnt = cv.fit(all_blogs)
#print(“word size = “, len(word_cnt.vocabulary_))
#print(“words = “, dict(list(word_cnt.vocabulary_.items())))
wc = cv.fit_transform(all_blogs)

mnb_cv = MultinomialNB()
mnb_cv.fit(wc[:tech_blog_size + staff_blog_size, :], y_blog_class)

#print(“モデルパラメータ=”, mnb_cv.get_params(deep=True))
print(“スコア=”, mnb_cv.score(wc[: tech_blog_size + staff_blog_size, : ], y_blog_class))

print(“評価”)
predict_proba = mnb_cv.predict_proba(wc[tech_blog_size + staff_blog_size: , : ])
df = DataFrame(predict_proba.round(5), columns=[‘staff’, ‘tech’])
print(df)
print(mnb_cv.predict(wc[tech_blog_size + staff_blog_size : , : ]))

東京アプリケーションシステム

最近の記事