« Dockerを使ったJupyter Labの環境を用意する | トップページ | 自分のブログをベクトルにして分布をみた(2) »

2020.07.25

自分のブログをベクトルにして分布をみた(1)

前回、Dockerを使ったJupyter Labの環境を用意するで分析環境を用意しましたので、今回は実際に分析をしてみます。
素材に当ブログの記事を使います。著作権の問題がないですしね(笑)。

実行環境はmacOS Catalina + Docker for Macですが、WSLでJupyter Labを使う&ショートカット・アイコンから実行するでも同じようにいけるのではないかと思います。

今回は主に次の記事や書籍を参考にしました。ありがとうございます。
B'zの歌詞をPythonと機械学習で分析してみた 〜イントロ編〜:下町データサイエンティストの日常とそのシリーズ
Sebastian Raschka著,福島慎太郎監訳,Python機械学習プログラミング,株式会社インプレス
文書ベクトルをお手軽に高い精度で作れるSCDVって実際どうなのか日本語コーパスで実験した(EMNLP2017) -- Qiita
Identifying Affiliation Effects on Innovation Enhancement
GloVe: Global Vectors for Word Representation
English word vectors

今回は、ブログのタイトルと記事について、タイトルから得たBoW及び記事から得たBoWのいちTFIDF値が高い単語から20単語を抽出し平均ベクトルをとる、という単純な戦略を採ります。

0.テキストデータ取得
ココログにはブログ記事のテキスト部分のみを一括してダウンロードできるエクスポート機能があります。ここではcocolog-export-1000060.txtのファイル名にしてエクスポートしました。そして次のスクリプトで1記事1行になるように整形します。当ブログではpreタグとblockquoteタグの中身はほぼほぼソースコードなので思い切って削除してしまいます。


$ cat cocolog-export-1000060.txt | awk '{if(/^TITLE: /){TI=$0;sub("TITLE: ","",TI);};if(/^DATE: /){TM=$0;sub("DATE: ","",TM)};if(/^---+/ || /^STATUS:/ || /^AUTHOR:/ || /^TITLE:/ || /^STATUS:/ || /^ALLOW COMMENTS:/ || /^CONVERT BREAKS:/ || /^ALLOW PINGS:/ || /^CATEGORY:/ || /^DATE/ || /^BODY:/ || /^EXTENDED BODY:/ || /^EXCERPT:/ || /^KEYWORDS:/){;}else{DE=DE$0};if(/^AUTHOR/){printf("%s\t%s\t%s\n", TM, TI, DE);TM="";TI="";DE=""}}END{printf("%s\t%s\t%s\n", TM, TI, DE);}' | sed -E 's/</pre .+\<pre>//g' | sed -E 's/<blockquote.+\/blockquote>//g' | sed -E 's/<[^>]+>//g' > cocolog_corpus_3.txt

次にDocker containerを起動します。例えば次のようなコマンドを実行します。


$ docker run -p 8888:8888 -v ~/work/mta2:/root/work mta2:0.3

このコマンドを実行することで、8888ボートを使用し、ホストOSの~/work/mta2ディレクトをdocker containerの/root/workにマウントし、mta2:0.3という名称のイメージをcontainerとして起動します。

1.トークナイズ
トークナイズにMeCabを使うのですが、そのために予め、ワークディレクトリにmecab-ipadic-neologdをDownloadして置いておきます。名詞、動詞、形容詞のみ単語群にトークナイズし、記事作成日時、タイトル、タイトルBoW、記事BoWからなるリストを取得します。


import MeCab

def tokenize(x):
node = tagger.parseToNode(x)
word_list = []
while node:
pos = node.feature.split(",")[0]
if pos in ['名詞', '動詞', '形容詞']:
word_list.append(node.surface)
node = node.next
return word_list

tagger = MeCab.Tagger('./mecab-ipadic-neologd/')
tagger.parse("")

tokens_list = []

with open('cocolog/cocolog_corpus_3.txt') as fin:
for line in fin:
items = line.rstrip('\n').split('\t')
if len(items[0]) < 1:
continue
tokens_list.append([items[0], items[1], tokenize(items[1]), tokenize(items[2])])

2.ストップワード
ストップワードを登録します。ここでは、nltkのストップワード(英単語のみ)にアドホックですが日本語のストップワードを追加しました。その他数字や記号を削除しています。


import nltk
nltk.download('stopwords')
stop_words = nltk.corpus.stopwords.words('english')
stop_words.extend(["それ","てる","よう","こと","の","し","い","ん","さ","て","せ","れ",
"する","なり","いる","なる","そう","でき","これ","ここ","さん","あっ",
"あり","できる","ため","なっ","き","み","nbsp"])

import string
import re

re_1 = re.compile('[' + re.escape(string.punctuation) + '0-90123456789\\r\\t\\n]')

def delete_stop_words(x):
y = [re_1.sub("", t) for t in x if t not in stop_words and re_1.sub("",t) != '']
return y

text_list = []
date_list = []
date_title = {}

for date, title, title_bow, article_bow in tokens_list:
text_list.append(" ".join(delete_stop_words(article_bow)))
date_list.append(date)
date_title[date] = [title, delete_stop_words(title_bow)]

3.各記事についてタイトルに含まれる単語及び記事中のTFIDF値が高い単語の集合を作成
ここでは記事ごとに100単語を抽出します。ここでは結果をtfidf_03.txtに出力しています。


import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(use_idf=True, norm='l2', smooth_idf=True, token_pattern='(?u)\\b\\w+\\b')
X = vectorizer.fit_transform(text_list)
X = X.toarray()

feature_names = vectorizer.get_feature_names()
df_tfidf = pd.DataFrame(X, columns=feature_names, index=date_list)
NMAX = 100
with open('tfidf_03.txt', 'w') as fout:
for k in date_title.keys():
fout.write('DATE:' + '\t' + k + '\t' + date_title[k][0] + '\n')
fout.write('TITLE_BOW:' + '\t' + '\t'.join(date_title[k][1]) + '\n')
s = pd.Series(df_tfidf.loc[k][:], index=feature_names)
s.sort_values(ascending=False, inplace=True)
for i in range(NMAX):
fout.write(str(s.index[i]) + '\t' +str(s[i]) + '\n')

tfidf_03.txtの内容は次のようになります。


DATE: 04/08/2008 14:19:02 落ち着き先が決まるまで
TITLE_BOW: 落ち着き 先 決まる
物件 0.3691620146879452
不動産 0.2501837636978408
ヶ月 0.2406584943848199
屋 0.22562873452088877
オックスフォード 0.2089913296743052
近況 0.1845810073439726
派遣 0.1845810073439726
知り 0.1745611674346852
深謝 0.0993515892610556
cowley 0.0993515892610556
飛び出る 0.0993515892610556
いし 0.0993515892610556
入居 0.0993515892610556
いきさつ 0.0993515892610556
構い 0.0993515892610556
ゲストハウス 0.0993515892610556
町中 0.0993515892610556
難渋 0.0993515892610556
至る 0.0993515892610556
嬢 0.0993515892610556
氏 0.0993515892610556
転々 0.0993515892610556
・・・・(略)

4.特徴ベクトル算出
文献の特徴ベクトルを算出するために、日本語 Wikipedia エンティティベクトルを使わせていただきます。entity_vector.model.binがエンティティベクトルファイルです。タイトル及びTFIDF値が高い単語群から20単語を抽出し、Wikipediaから訓練したベクトルを使ってベクトルに変換して単語数だけ足し合わせました。そうして算出したベクトルをvector_list_4.binファイルに保存しました。


import numpy as np
import pandas as pd
from gensim.models import KeyedVectors
from collections import defaultdict

model = KeyedVectors.load_word2vec_format('entity_vector/entity_vector.model.bin', binary=True)
vocab = list(model.vocab.keys())

date_list = []
title_list = []
vector = []
exist_word_list = []
vector_words = []
IMAX = 20
c = 0
v = np.array([0.0 * 200])

with open('tfidf_03.txt', 'r') as fin:
for line in fin:
items = line.rstrip('\n').split('\t')
if c >= 20 and items[0] != 'DATE:':
continue
if items[0] == 'DATE:':
date_list.append(items[1])
title_list.append(items[2])
if c > 0:
vector.append(v)
vector_words.append(exist_word_list)
exist_word_list = []
v = np.array([0.0 * 200])
c = 0
elif items[0] == 'TITLE_BOW:':
wl = items[1:]
for w in wl:
if w in vocab and w not in exist_word_list:
v = v + model[w]
exist_word_list.append(w)
c += 1
else:
w = items[0]
if w in vocab and w not in exist_word_list:
v = v + model[w]
exist_word_list.append(w)
c += 1
vector.append(v)
vector_words.append(exist_word_list)

import pickle

o = zip(date_list, title_list, vector_words, vector)
with open('vector_list_4.bin', 'wb') as fbin:
pickle.dump(o, fbin)

5.エルボー法によるクラスタ数推定


import pickle
with open('vector_list_4.bin','rb') as fbin:
o = pickle.load(fbin)

from collections import defaultdict
from gensim.models.keyedvectors import KeyedVectors
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

date_list = []
title_list = []
vector_words = []
vector = []

for a, b, c, d in o:
date_list.append(a)
title_list.append(b)
vector_words.append(c)
vector.append(d)

distortions = []
n_clusters_max = 40
for i in range(1, n_clusters_max):
km = KMeans(n_clusters=i, init='k-means++', n_init=10, max_iter=300, tol=1e-04, random_state=0)
km.fit(vector)
distortions.append(km.inertia_)
plt.plot(range(1,n_clusters_max), distortions, marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('Distortion')
plt.show()

2020072401
ここではクラスタ数を9に設定してみます。

6.クラスタリング
ここではk-means++法を使ってクラスタリングし、クラスタリングした結果をclusterID_vector_4.binに出力し、併せて画面表示をします。


import pickle
with open('vector_list_4.bin','rb') as fbin:
o = pickle.load(fbin)

from collections import defaultdict
from gensim.models.keyedvectors import KeyedVectors
from sklearn.cluster import KMeans

date_list = []
title_list = []
vector_words = []
vector = []

for a, b, c, d in o:
date_list.append(a)
title_list.append(b)
vector_words.append(c)
vector.append(d)

n_clusters = 9
km = KMeans(n_clusters=n_clusters, init='k-means++', n_init=10, max_iter=300, tol=1e-04, random_state=0)
y_km = km.fit_predict(vector)

o = zip(y_km, date_list, title_list, vector_words, vector)
with open('clusterID_vector_4.bin', 'wb') as fbin:
pickle.dump(o, fbin)

for i, p in enumerate(o):
print(i, p[0], p[1], p[2], p[3])

クラスタリング結果はこのようになりました。


0 2 04/08/2008 14:19:02 落ち着き先が決まるまで ['落ち着き', '先', '決まる', '物件', '不動産', 'ヶ月', '屋', 'オックスフォード', '近況', '派遣', '知り', '深謝', '飛び出る', 'いし', '入居', 'いきさつ', '構い', 'ゲストハウス', '町中', '難渋']
1 0 04/08/2008 15:05:04 聖火リレーを見に行って来ました ['聖火', 'リレー', '見', '行っ', '来', 'チベット', '支援', '派', 'ランナー', 'ロンドン', '中国', 'coach', '鉄道', '問題', 'イギリス', '高い', 'なに', '遠い', '中間', '雪']
2 6 04/11/2008 09:01:56 デジタルカメラ購入 ['デジタル', 'カメラ', '購入', '店員', 'ディジタル', 'ウィンドウ', 'ショー', 'tz', '型番', '帰っ', '箱', '日本', 'サンプル', '店', 'こちら', 'カード', '探し', 'みる', '別', '言っ']
・・・・(略)

7.次元圧縮&可視化
各ドキュメントの次元数が200ですのでこれをTruncatedSVDを使って2次元に圧縮し、その結果をグラフに可視化します。


import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib import cm
from sklearn.decomposition import TruncatedSVD
import pickle

with open('clusterID_vector_4.bin', 'rb') as fbin:
o = pickle.load(fbin)

vector = []
title_list = []
clusterID = []
for i, p in enumerate(o):
clusterID.append(p[0])
title_list.append(p[2])
vector.append(p[4])
clusterID = np.array(clusterID)
vector = np.array(vector)

# 次元圧縮
svd = TruncatedSVD(n_components=2, n_iter=10, random_state=0)
compressed_vector = svd.fit_transform(vector)

# 可視化
n_cluster = len(np.unique(clusterID))
resolution = 0.2

markers = ('o')
x_min, x_max = compressed_vector[:, 0].min() - 1, compressed_vector[:, 0].max() + 1
y_min, y_max = compressed_vector[:, 1].min() - 1, compressed_vector[:, 1].max() + 1
# グリッドポイントの生成
xx, yy = np.meshgrid(np.arange(x_min, x_max, resolution),
np.arange(y_min, y_max, resolution))
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())

with open('id_clusterID_title_compressed_vector_4.txt', 'w') as fout:
for i, p in enumerate(zip(clusterID, title_list, compressed_vector)):
print(i, p[0], p[1], p[2][0], p[2][1])
fout.write(str(i) + '\t' + str(p[0]) + '\t' + str(p[1]) + '\t' + str(p[2][0]) +
'\t' + str(p[2][1]) + '\n')

for idx in np.unique(clusterID):
color = cm.hsv(idx / n_cluster)
#color = colors[idx]
plt.scatter(x=compressed_vector[clusterID == idx, 0], y=compressed_vector[clusterID == idx, 1],
alpha=0.9, color=color, marker=markers[0], label=idx)

plt.title('tonop\'s blog text clustering and mapping')
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.legend(loc='upper right')
plt.show()
plt.savefig('text_clustering_and_mapping_2.png')

出力結果は次のようになりました。通番、クラスタ番号、タイトル、2次元座標の順で出力しています。


0 2 落ち着き先が決まるまで 30.012956533209344 66.35709134901
1 0 聖火リレーを見に行って来ました 50.35969356419305 89.32166003724612
2 6 デジタルカメラ購入 134.33542900088995 58.887390369372845
3 0 通勤 58.79935693717384 89.50586130532578
4 0 ロンドン博物館巡り 55.54138086232495 93.23512839454321
5 6 朝食 77.47413636122901 61.09823723672102
6 6 食事 99.74670425605711 137.20314088432025
・・・・(略)

Text_clustering_and_mapping_2

8.Word Cloud
Word Cloudを表示してみます。
日本語表示するためにIPAフォントを使っています。


import pickle

with open('clusterID_vector_4.bin', 'rb') as fbin:
o = pickle.load(fbin)

wdic = {}
for i, p in enumerate(o):
wdic.setdefault(p[0], {})
for w in p[3]:
wdic[p[0]][w] = wdic[p[0]].get(w, 0) + 1
import os
import matplotlib
import matplotlib.pyplot as plt
from wordcloud import WordCloud

FONT = 'font/IPAexfont00401/ipaexg.ttf'
plt.figure(figsize=(20, 4))

os.makedirs('WordCloud_image_4', exist_ok=True)

for i in sorted(wdic.keys()):
y = {}
for x in sorted(wdic[i].items(), key=lambda x:x[1], reverse=True)[:100]:
y[x[0]] = x[1]
plt.subplot(2, 5, i + 1)
wc = WordCloud(font_path=FONT, background_color="white")
im = wc.generate_from_frequencies(y)
wc.to_file('WordCloud_image_4/wordCloud_' + str(i) + '.png')
plt.imshow(im)
plt.axis("off")
plt.title("Cluster " + str(i))

Wordcloud_0
Cluster #0

Wordcloud_1
Cluster #1

Wordcloud_2
Cluster #2

Wordcloud_3
Cluter #3

Wordcloud_4
Cluster #4

Wordcloud_5
Cluster #5

Wordcloud_6
Cluster #6

Wordcloud_7
Cluster #7

Wordcloud_8
Cluster #8

9.分析
ベクトル表現された文献の分布を再掲します。

Text_clustering_and_mapping_2

大きく2つのクラスタが確認できますね。Cluster #0, #2のグループとCluster #1, #3, #4, #5, #7, #8のグループです。
より詳しく見てみると、Cluster #0, #2のグループ、中間的なCluster #6、Cluster #3、Cluster #1, #4, #5, #7, #8の4つのグループに分けられそうです。

Cluster #0, #2のグループ
イギリス滞在時代の記事がこちらのグループに入ります。日常生活や旅行に関わる単語が頻出します。
Cluster #0の例
ブレナム宮殿
住宅
Cluster #2の例
Wimbledon 2008
マンチェスター出張

Cluster #6
食事やDIY、文具関係の記事が含まれています。
Magic Trackpadのゴム足を交換してみるMOLESKINEノートのゴムバンドを交換してみたなど

Cluster #3
電話関係の記事が含まれています。
嫁と格安SIM − mineoの場合 − (その1)とそのシリーズなど

Cluster #1, #4, #5, #7, #8
ソフトウェアのインストール関係の記事、データ処理の記事などが含まれています。
Cluster #1の例
TP-LINK AC750 Wi-Fi Range Extender RE200を設置してみた
Windows 10上のHyper-VでUbuntuを動かしてネットワーク設定(NAT)を行う
Cluster $4の例
HDL2-G2.0にnetatalk 2.2.0をインストール(その1)とそのシリーズ
WSLでJupyter Labを使う&ショートカット・アイコンから実行する
Cluster #5の例
NASにiTunesライブラリを置く時に注意する事(その2)
VMware Fusion 4 にdebian 6(デスクトップ環境なし)をインストールした時の手順の記録とそのシリーズ
Cluster #7の例
昔使っていたMOディスクをバックアップ(Human68kフォーマットMO含む)
AozoraEpub3を使ってみる
Cluster #8の例
ThinkPad E440のHDDをSSDに換装してメモリを増設してみました
macOS上の VirtualBox で共用Windows環境を作成してみた

このように、今回採用した方法は文献中の重要単語(タイトル中の単語及びTFIDF値の高い単語)のベクトルを平均化するという単純なものですが、比較的意味が近そうな文献の集合が得られる方法であることが実感できました。

|

« Dockerを使ったJupyter Labの環境を用意する | トップページ | 自分のブログをベクトルにして分布をみた(2) »

パソコン・インターネット」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« Dockerを使ったJupyter Labの環境を用意する | トップページ | 自分のブログをベクトルにして分布をみた(2) »