【Python】TF-IDF を使って自分のブログの特徴を取得してみた

2020.03.14

TF-IDF の理解のために、表題の内容やってみました。

目次

  1. やったこと
  2. TF-IDF とは
    1. TF-IDFの例
  3. やってみた #セットアップ編
    1. Python 仮想環境の作成・有効化
    2. VSCodeで Jupyter Notebook起動
    3. 必要パッケージのインストール
  4. やってみた #Beautiful Soupでテキスト取得編
    1. 取得するブログのURLリストの準備
    2. ブログのインデックス作成
    3. URLからブログのテキストを取得
    4. テキストの前処理
  5. やってみた #Janome で形態素解析編
    1. インポート〜字句解析器の作成
    2. 各ブログの名詞を分かち書きして登録
  6. やってみた #scikit-learn で単語の出現頻度計算編
    1. Bag of Words: BoW とは
    2. 各ブログの BoW を計算する
  7. やってみた #TF-IDFを計算してみよう編
    1. TF(単語の出現頻度) を計算する
    2. IDF(単語のレア度) を計算する
    3. TF-IDF(単語の重要度)を計算する
    4. 色んなブログのTF-IDFを見てみる
  8. おわりに
    1. 参考

やったこと

自分のブログの TF-IDF値を調べてみました

TF-IDF とは

まず簡単に TF-IDF について説明します。

TF-IDF は 単語の重要度 を測るための指標の1つです。 TF値, IDF値の を取ります。

  • TF(Term Frequency): ある文書における 単語の出現頻度
  • IDF(Inverse Document Frequency): 逆文書頻度。ざっくりいうと 単語のレア度

TF, IDF, TF-IDF は以下で定義されます。

\[ \begin{eqnarray} TF(d,w) &=& \frac{文書d における単語wの出現回数}{文書d における全単語の出現回数の和}\\ IDF(w) &=& log(\frac{全文書数}{単語w を含む文書数})\\ TFIDF(d,w) &=& TF(d,w) \times IDF(w) \end{eqnarray} \]

TF-IDFの例

ざっくり TF-IDFの必要性をイメージしていただくために。以下例を示します。

ある AWS SageMaker 関連のブログがあったとします。 このブログで 高いTF値(出現頻度)を取る単語 は何でしょう?

例えば以下があります

  • 「SageMaker」「機械学習」
  • 「こと」「もの」「時」

前者はまさに ブログの特徴 として抜き出したい単語というのが直感的に分かると思います。 後者は日本語を使う上で頻出する 正直あまり重要ではない 単語ですよね。

TF-IDFは この 頻出するあまり重要ではない単語 を除外(filter)するための指標です。

これら「こと」「もの」「時」などの単語は、 もちろん他の日本語のブログでもよく見かけます。 つまり IDF値(レア度)は低い です。

まとめると以下の表になります。

単語 TF値 IDF値 TF-IDF値
「SageMaker」「機械学習」 高い 高い 高い
「こと」「もの」「時」 高い 低い 低い

TF-IDF値の高い単語がそのブログの特徴を表している ということが何となく伝わったかと思います。

やってみた #セットアップ編

Jupyter Notebook 環境を作っていきます。

Python 仮想環境の作成・有効化

以下実行します。

python -m venv venv
source venv/bin/activate

VSCodeで Jupyter Notebook起動

今回は VSCode 上で Jupyter Notebookを動かしていきます。

touch try_tfidf.ipynb
code .

try_tfidf.ipynb を開きます。

必要パッケージのインストール

以下のパッケージを利用するので、インストールしていきます。

!pip install requests
!pip install beautifulsoup4
!pip install janome
!pip install scikit-learn
!pip install pandas
!pip list

※その後の処理で パッケージ import が失敗する場合

インストールしたパッケージの場所が path に登録されていない可能性があります。 その場合は以下のように path を追加します。

import sys
venv_packages_path = '(PROJECT_PATH)/venv/lib/python3.7/site-packages' #edit yourself
if venv_packages_path not in sys.path:
    sys.path.append(venv_packages_path)

やってみた #Beautiful Soupでテキスト取得編

取得するブログのURLリストの準備

今回は私がこれまで書いてきた 48本のブログ を対象にします。 URLリストを ./contents/url_list.txt に格納しています。

url_list_path = "./contents/url_list.txt" #edit yourself
url_list = []
with open(url_list_path) as f:
    url_list = [l.replace('\n', '') for l in f.readlines()]
print("len(url_list): {}".format(len(url_list)))
print("url_list[0]: {}".format(url_list[0]))
# len(url_list): 48
# url_list[0]: https://dev.classmethod.jp/cloud/aws/on-premise-reverse-proxy-using-alb/

ブログのインデックス作成

辞書 BLOG にインデックスを作成します。 それぞれに URL 情報を入れていきます。

BLOG = {}
for i, url in enumerate(url_list):
    BLOG[i] = {}
    BLOG[i]["url"] = url
print("len(BLOG): {}".format(len(BLOG)))
print("BLOG[0][\"url\"]: {}".format(BLOG[0]["url"]))
# len(BLOG): 48
# BLOG[0]["url"]: https://dev.classmethod.jp/cloud/aws/on-premise-reverse-proxy-using-alb/

URLからブログのテキストを取得

各URLから ./util/get_blog_texts.py を使ってブログテキスト(strのlist)を取得します。

from util.get_blog_texts import get_blog_texts
import time 

for i in BLOG.keys():
    url = BLOG[i]["url"]
    print("#{} getting texts from: {}".format(i, url))
    BLOG[i]["texts"] = get_blog_texts(url)
    time.sleep(1)

▼確認

print("len(BLOG[0][texts]): {}".format(len(BLOG[0]["texts"])))
print("len(BLOG[0][texts][0]): {}".format(BLOG[0]["texts"][0]))
# len(BLOG[0][texts]): 65
# len(BLOG[0][texts][0]): Application Load Balancer (ALB), Network Load Balancer (NLB) はターゲットにローカルIPアドレス を指定できます。VPCピアリング接続先のインスタンスや、Direct Connect・VPN接続先のオンプレのサーバーをターゲットグループに登録することができます。

テキストの前処理

今回は日本語テキストを想定しているので、空白を削除します。 また、アルファベットは全て小文字化します。

※ この部分は後述する Janome(Pythonの日本語形態素解析パッケージ) の前処理機能でも対応はできます。

for i in BLOG.keys():
    BLOG[i]["texts"] = [t.replace(' ','').lower() for t in BLOG[i]["texts"]]

print("len(BLOG[0][texts]): {}".format(len(BLOG[0]["texts"])))
print("len(BLOG[0][texts][0]): {}".format(BLOG[0]["texts"][0]))
# len(BLOG[0][texts]): 65
# len(BLOG[0][texts][0]): applicationloadbalancer(alb),networkloadbalancer(nlb)はターゲットにローカルipアドレスを指定できます。vpcピアリング接続先のインスタンスや、directconnect・vpn接続先のオンプレのサーバーをターゲットグループに登録することができます。

やってみた #Janome で形態素解析編

前章で各ブログのテキストデータを取得しました。

これらテキストの形態素解析を Janome を使って行います。

Janome (蛇の目; ◉) は,Pure Python で書かれた,辞書内包の形態素解析器です。

依存ライブラリなしで簡単にインストールでき, アプリケーションに組み込みやすいシンプルな API を備える形態素解析ライブラリを目指しています。

— 引用: Janome v0.3 documentation (ja)

インポート〜字句解析器の作成

以下インポートして、 Analyzer を作成します。

  • Tokenizer: 字句解析器
  • POSStopFilter: 記号や助詞を取り除くために使用します
  • Analyzer: 前処理、後処理を含めた字句解析のフレームワーク
from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.tokenfilter import POSStopFilter

tokenizer = Tokenizer()
token_filters = [POSStopFilter(['記号','助詞','助動詞','動詞'])]
a = Analyzer(tokenizer=tokenizer, token_filters=token_filters)

Analyzer のテストしてみましょう。 ブログの1文を解析してみます。

test_tokens = a.analyze(BLOG[0]["texts"][0])
for t in test_tokens:
    print(t)

(カッコも名詞になっていますが) 一覧を取得できてそうですね。

各ブログの名詞を分かち書きして登録

スペース区切りで単語を書いていくことを 分かち書き といいます。

  • 各ブログのテキストの名詞を抽出して、
  • 結果を分かち書きして BLOG[i]["wakati"] へ登録します。
# 解析
for i in BLOG.keys():
    texts_flat = "。".join(BLOG[i]["texts"])
    tokens = a.analyze(texts_flat)
    BLOG[i]["wakati"] = ' '.join([t.surface for t in tokens])
# 確認
print("BLOG[0][wakati]: {}".format(BLOG[0]["wakati"]))

1ブログの 分かち書き結果は以下のようになりました。

やってみた #scikit-learn で単語の出現頻度計算編

Pythonの機械学習ライブラリで有名な scikit-learnCountVectorizer を使って単語の出現頻度を計算します。

それぞれのブログの各単語の出現頻度を Bag of Words:BoW (単語の袋) として格納していきます。

Bag of Words: BoW とは

簡単に BoW を説明します。 BoW はある文書の単語の出現回数をベクトルで表したものとなります。

以下 2文書があったとします。

  • 文書1: 私はEmacsが好きです
  • 文書2: 私はVimが嫌いです

この文書1, 文書2の BoW はそれぞれ以下の行で表されるベクトルです。

Emacs Vim 好き 嫌い です
文書1 1 1 1 0 1 1 0 1
文書2 1 1 0 1 1 0 1 1

列の各単語がその文書内にいくつ出現するか を表すのが BoW です。

後述の TF-IDF 計算で必要となってきます。

各ブログの BoW を計算する

CountVectorizer を作成します。

from sklearn.feature_extraction.text import CountVectorizer
import random
vectorizer = CountVectorizer()

vectorizer.fit_transform を使って全ブログの BoW を計算します。 結果(各ブログの BoW ベクトル) を BLOG[i]["bow"] に格納します。

X = vectorizer.fit_transform([BLOG[i]["wakati"] for i in BLOG.keys()])
for i, bow in enumerate(X.toarray()):
    BLOG[i]["bow"] = bow

また、vectorizer.get_feature_names で計算したBoW のインデックスに対応する単語の情報を 配列として取得できます (WORDS とします)。

WORDS = vectorizer.get_feature_names()
print(WORDS)

試しに 1ブログで高頻出 ( 6 回以上出現) だった単語をリストしてみましょう。

一番多い単語は ALB であることが分かりました。ブログの内容(↓)的に妥当そうですね。

やってみた #TF-IDFを計算してみよう編

(ようやく) メインの内容です。

  • TF-IDF の導出関数を作成します
  • ブログに出てくる単語の TF-IDF値をみてみます

今回は勉強のため手動で TF-IDF 導出関数を実装しています。 実際は scikit-learnのライブラリに TfidfVectorizer というものがあり、簡単に TF-IDFを求めることができます。

TF(単語の出現頻度) を計算する

TFの定義を再掲します。

\[ \begin{eqnarray} TF(d,w) &=& \frac{文書d における単語wの出現回数}{文書d における全単語の出現回数の和}\\ \end{eqnarray} \]

以下 calc_tf 関数を作成しました。

def calc_tf(b_idx, w_idx):
    """b_idx 番目のブログの WORD[w_idx] の TF値を算出する"""
    # WORD[w_idx] の出現回数の和
    word_count = BLOG[b_idx]["bow"][w_idx]
    if word_count == 0:
        return 0.0
    # 全単語の出現回数の和
    sum_of_words = sum(BLOG[b_idx]["bow"])
    # TF値計計算
    return word_count/float(sum_of_words)

試しに 1ブログの TF値を降順で表示してみました。

index = 0
print("# TF values of blog:{}".format(BLOG[index]["url"]))
sample_tfs = [calc_tf(index, w_idx) for w_idx, word in enumerate(WORDS)]
tfs_sorted = sorted(enumerate(sample_tfs), key=lambda x:x[1], reverse=True)
for i, tf in tfs_sorted[:20]:
    print("{}\t{}".format(WORDS[i], round(tf, 4)))

alb, オンプレサーバー といったブログの特徴を表しそうな単語がある一方で、 作成, こと といったあまり重要じゃない単語も確認できますね。

IDF(単語のレア度) を計算する

IDFの定義を再掲します。

\[ \begin{eqnarray} IDF(w) &=& log(\frac{全文書数}{単語w を含む文書数})\\ \end{eqnarray} \]

以下 calc_idf 関数を作成しました。 (分母はゼロにならないように +1 しています。)

import math

def calc_idf(w_idx):
    """WORD[w_idx] の IDF値を算出する"""
    # 総文書数
    N = len(BLOG.keys())
    # 単語 word が出現する文書数 df を計算
    df = len([i for i in BLOG.keys() if BLOG[i]["bow"][w_idx] > 0])
    # idf を計算
    return math.log2(N/float(df + 1))

全単語のIDF値を計算して IDFS へ格納します。

IDFS = [calc_idf(w_idx) for w_idx, word in enumerate(WORDS)]

IDFの値を降順に並べた結果(上位・下位 10個)を出してみます。

idfs_sorted  = sorted(enumerate(IDFS), key=lambda x:x[1], reverse=True)
print("# IDF values")
for w_idx, idf in idfs_sorted[:10]:
    print("{}\t{}".format(WORDS[w_idx], round(idf, 4)))
print("︙")
for w_idx, idf in idfs_sorted[-10:]:
    print("{}\t{}".format(WORDS[w_idx], round(idf, 4)))

IDF値の低いものに 以下, こと, ため といった日本語でよく使われる言い回しが出てきましたね。

IDF値の高いものは、ぱっと見て数字列が列挙されましたが、 IDF値の一番高い単語 は他にもたくさんあります(↓)。

これらは対象のブログ本数が48本のうち 1本のみで使われていた単語です。

TF-IDF(単語の重要度)を計算する

TF-IDFの定義を再掲します。

\[ \begin{eqnarray} TFIDF(d,w) &=& TF(d,w) \times IDF(w) \end{eqnarray} \]

先程実装した calc_tf, calc_idf を使います。

def calc_tfidf(b_idx, w_idx):
    """b_idx 番目のブログの WORD[w_idx] の TF-IDF値を算出する"""
    return calc_tf(b_idx, w_idx) * calc_idf(w_idx)

TFのときと 同じように 1ブログの TF-IDF値を降順で表示してみました。

ブログ(オンプレサーバーをターゲットにしたALBリバースプロキシ環境を構築してみる) の TF, TF-IDFの上位値は以下のようになりました。

TF上位 TF-IDF上位
1 alb alb
2 作成 オンプレサーバー
3 ターゲット ターゲット
4 構築 サーバー
5 環境 http
6 オンプレサーバー app
7 ip 構築
8 サーバー 外部
9 サービス 検証
10 指定 ip
11 接続 エイリアス
12 検証 リバースプロキシ
13 app ローカル
14 http 接続
15 vpc yaml
16 こと 通信
17 ローカル 80
18 通信 登録
19 vpn awscloudformation
20 yaml vpn

TF上位だった 作成, こと などが消えて、 TF上位に無かった リバースプロキシ, awscloudformation などが TF-IDF上位に上がっていることが分かります。

色んなブログのTF-IDFを見てみる

長い長い前処理があって、ようやく全ブログの全名詞の TF-IDF値を取得できました。

色んなブログの TF-IDF値を見てみましょう。

コンテナワークショップ参加レポート

TF上位 TF-IDF上位
1 コンテナ コンテナ
2 ec fargate
3 起動 起動
4 こと ecs
5 利用 タイプ
6 fargate イメージ
7 サービス docker
8 aws ec
9 環境 スケール
10 タイプ タスク

fargatedocker といったワードが高い TF-IDF値となっていますね。

AWSと自宅(Cisco)をVPN接続してみたブログ

TF上位 TF-IDF上位
1 aws active
2 vpn 自宅
3 自宅 vpn
4 接続 cisco
5 active 接続
6 cisco フェーズ
7 確認 ike
8 作成 globalip
9 フェーズ idle
10 ルータ qm

AWS系のブログが多いため、単語 AWS は TF-IDF値は高くありません。 その代わり Ciscoルータでサイト間VPNする際によく出る用語 ike, qm, idle あたりが TF-IDF上位に上がっています。

TransitGatewayマルチキャスト対応の速報

TF上位 TF-IDF上位
1 キャスト キャスト
2 マルチ マルチ
3 tgw 受信
4 transitgateway eni
5 ため tgw
6 グループ transitgateway
7 作成 マルチキャストグループ
8 受信 マルチキャストルータ
9 eni レシーバー
10 ip 待ち

マルチ/キャストと分解されていますが、 TGWマルチキャストで必要な構成要素が一部 TF-IDF上位に上がっています。

CloudTrail, Athena, S3関連のブログ

TF上位 TF-IDF上位
1 バケット バケット
2 パブリック cloudtrail
3 作成 投稿
4 cloudtrail パブリック
5 オブジェクト オブジェクト
6 投稿 証跡
7 ログ athena
8 リクエスト putobject
9 athena リクエスト
10 格納 ログ

TF上位一覧もそれなりにブログの特徴を捉えていますが、 TF-IDFのほうが 証跡, putobject といった単語があります。より良いですね。

Emacsセットアップブログ

TF上位 TF-IDF上位
1 emacs emacs
2 設定 mode
3 mode ido
4 インストール org
5 org homebrew
6 環境 インストール
7 表示 xcode
8 記事 ターミナル
9 homebrew mac
10 ido フォント

入社すぐに書いたEmacs Org-mode布教用のブログです。 セットアップで入れたもの ido-mode, homebrew, xcode あたりが TF-IDF上位に入りました。

おわりに

自分のブログの各単語の TF-IDF値を調べてみました。 今回はTF-IDFの理解のために色々と手を動かしました。

今回の Tryで使用した Notebookは以下にあります。

AWSのサービスに Amazon Comprehend があるので、 これらを使うことで更に少ないコード量( or ノンコーディング) でテキストの分析ができそうです。

SageMakerとの連携もできるみたいなので、今後はそちら触っていこうと思います。

参考