TF-IDFとコサイン類似度を使って似ている文章を見つける
今回は、以前実装したTF-IDF
の処理をベースに、自分のブログに一番近いWikipediaの文章は何かをコサイン類似度を使って出してみる。
コサイン類似度とは?
高校の数学でやったようなやってないようなうる覚えな感じだったので、他の方のサイトを参考にすると
コサイン類似度は2本のベクトルがどれくらい同じ向きを向いているのかを表す指標
となり、文章を全単語で表現されたベクトル空間で表すことで計算できる。
単純にある文章にその単語が含まれているかを0 or 1
で表現すると以下になる。
単語1 | 単語2 | 単語3 | 単語4 | ..... | |
---|---|---|---|---|---|
文章1 | 1 | 0 | 1 | 1 | ... |
文章2 | 0 | 0 | 0 | 1 | ... |
文章3 | 0 | 0 | 1 | 0 | ... |
cos(文章1, 文章2) = 1 * 0 + 0 * 0 + ... cos(文章1, 文章3) = ....
のように、文章1と似ている文章を探す場合は、文章1と全文章のコサイン類似度を計算し、一番高い(Max:1)あたいのものを選び出せば良い。
ただ、これだと出現頻度や希少性が表現されていない空間になってしまい、表現の幅が狭くなってしまう。
そこで、TF-IDF
を用いて特徴量を抽出すると以下のような空間になる。
単語1 | 単語2 | 単語3 | 単語4 | ..... | |
---|---|---|---|---|---|
文章1 | 2.33 | 0 | 0.23 | 0.85 | ... |
文章2 | 0 | 0 | 0 | 3.55 | ... |
文章3 | 0 | 0 | 3.23 | 0 | ... |
前処理
以前行ったように、Wikipediaのデータを準備する。
ほぼメモのような感じなのでペタペタ貼ってく。
文章単位に分割し、さらに単語に分割
def wakati_to_list(wakati_words): # 改行コードを消す処理 wakati_words = wakati_words.replace('\n','') wakati_words = wakati_words.replace('\r','') # すでに分かち書きされているため半角スペースで words = wakati_words.split(' ') return words
f = open('../data/wiki_wakati_neo_head.txt') # 文章単位に変換 wiki_texts = f.read().split('</ doc >') f.close() words = wakati_to_list(wiki_texts[0]) print(words[:200]) ['<', 'doc', 'id', '="', '3216890', '"', 'url', '="', 'https', '://', 'ja', '.', 'wikipedia', '.', 'org', '/', 'wiki', '?', 'curid', '=', '3216890', '"', 'title', '="', '2015年アムトラック脱線事故', '">', '2015年アムトラック脱線事故', '2015年アムトラック脱線事故', '(', '2015', 'ねん', 'アムトラック', 'だっ', 'せんじ', 'こ', ')', 'は', '、', '現地時間', 'で', '2015年', '5月12日', '夜', 'に', 'アメリカ合衆国', 'ペンシルベニア州', 'フィラデルフィア', 'の', '付近', 'で', '、', '全米鉄道旅客公社', '(', 'アムトラック', ')', 'が', '運行', 'する', 'ワシントンD.C.', 'から', 'ニューヨーク', 'に', '向かう', '列車', 'が', '脱線', 'し', 'た', '鉄道事故', 'で', 'ある', '事故', '当時', '、', 'この', '列車', 'に', 'は', 'およそ', '240人', 'が', '乗っ', 'て', 'い', 'た', '。', '2015年', '5月12日', 'の', '午後9時', '10分', 'ごろ', '、', 'アムトラック', '北東回廊', 'を', '運行', 'する', '北', '行き', 'の', '「', 'ノースイースト・リージョナル', '」', '188', '(', 'ユニオン駅', '(', 'ワシントンD.C.', ')', '発', 'ペンシルベニア', '駅', '(', 'ニューヨーク', ')', '行き', ')', 'は', 'フィラデルフィア', 'の', '30', '丁目', '駅', 'を', '発車', 'し', 'た', '。', '列車', 'は', '7', '両', 'の', '客車', 'を', '1年前', '製造', 'の', 'ACS', '-', '64', '型', '電気機関車', '(', 'No.', '601', ')', 'が', 'けん引', 'する', 'もの', 'で', 'あっ', 'た', '。', '約', '11分', '後', '、', '列車', 'は', 'から', '南東', 'に', 'ある', '複々線', 'の', '本線', 'を', '走行', 'し', 'て', 'おり', '、', 'ポート', '・', 'リッチモンド', '地区', 'に', 'ある', 'フランクフォード', '・', 'アベニュー', 'と', 'ウィートシーフ・レーン', 'の', '交差点', 'の', '近く', '、', 'に', 'ある', '4度', '(', '半径', '約', '440m', ')', 'の']
名詞のみ抽出
以前名刺以外もすべていれたら痛い目をみたので、名詞のみにする。
# 名詞の抽出する def extract_noun(words): import MeCab m = MeCab.Tagger('-d /usr/local/mecab/lib/mecab/dic/mecab-ipadic-neologd/') noun_words = [] for word in words: node= m.parseToNode(word) while node: meta = node.feature.split(",") if meta[0] == ("名詞"): noun_words.append(word) node = node.next return noun_words # 単語分割と名詞の抽出を一気にやっておくと f = open('../data/wiki_wakati_neo_head.txt') # 文章単位に変換 wiki_texts = f.read().split('</ doc >') # Wikipediaの文章 wiki_text_words = [] for wiki_text in wiki_texts: wiki_words = wakati_to_list(wiki_text) wiki_words = extract_noun(wiki_words) wiki_text_words.append(wiki_words) f = open('../data/blog_wakati_neo.txt') # 文章単位に変換 blog_text = f.read() # Blogの文章たち blog_words = wakati_to_list(blog_text) blog_words = extract_noun(blog_words)
TFの計算
関数にしておく。
# tfの計算 def tf(words): import collections words_counter= collections.Counter(words) all_words_count = len(words) tf = {} for key in words_counter: tf[key] = float(words_counter[key]/all_words_count) return tf
# ブログのTF tf_blog = tf(blog_words) # 文章ごとのTF tf_wiki_text = [] for wiki_text_word in wiki_text_words: tf_wiki_text.append(tf(wiki_text_word)) # まとめておく tf_all_text = tf_wiki_text[:1000] + [tf_blog]
tf_wiki_text[:1000]
で1000
文章のみにした理由は、一部の実装で速度を早くすることができず、今回は諦めた。
IDFの計算
こちらも関数にしておく。(変数名はあとで直すかも・・)
def idf(text_words): from math import log from itertools import chain words_flat= list(chain.from_iterable(text_words)) # uniqueにする words_flat = set(words_flat) text_words_unique = [set(text_word) for text_word in text_words] idf = {} for word in words_flat: for text_word_unique in text_words_unique: if word in text_word_unique: if word in idf: idf[word] += 1 else: idf[word] = 1 for word in idf: idf[word] = log(len(text_words)/idf[word]) + 1 return idf
# TFと同様1000文章のみ all_words = wiki_text_words[:1000] + [blog_words] all_words_idf = idf(all_words)
単語 x 文章のマトリックス作る
# Wikipedia + Blogを単語に分解 from itertools import chain words_flat= list(chain.from_iterable(all_words)) # uniqueにする words_flat = set(words_flat)
最初の方に説明したマトリックスを作る
matrix = [] for index in range(len(all_words)): line = [all_words_idf[word] * tf_all_text[index][word] if word in all_words[index] else 0 for word in words_flat] matrix.append(line)
コサイン類似度の計算
import math def cosine_similarity(v1,v2): "compute cosine similarity of v1 to v2: (v1 dot v2)/{||v1||*||v2||)" sumxx, sumxy, sumyy = 0, 0, 0 for i in range(len(v1)): x = v1[i]; y = v2[i] sumxx += x*x sumyy += y*y sumxy += x*y return sumxy/math.sqrt(sumxx*sumyy) blog = matrix[-1] sims = {} for index in range(len(matrix)-1): sim = cosine_similarity(blog, matrix[index]) sims[index] = sim
matrixの一番最後の行が今回検索したいブログのため計算から省く。
類似度でソートして似ている文章を出力する
先ほどのコサイン類似度をソートして表示すると
for k, v in sorted(sims.items(), key=lambda x: -x[1]): print(k, v) 49 0.10417548423881662 625 0.10121151071826293 184 0.08561012221392927 24 0.08253925952346762 845 0.07463393507872249 546 0.055037919480128226 115 0.05484217388705765 738 0.05439783280734914 395 0.053252522673636296 744 0.053045184024489614 ....省略 627 6.935653310818008e-05 125 6.934484006734906e-05 566 6.830350235460813e-05 309 6.70632734759553e-05 778 5.281461561599366e-05
左が記事番号、右が類似度のあたいになっている。
一番高い記事は「49」、一番低いのは「778」なので見てみると
print(all_words[49]) [...'情報デザイン', '専門', '情報', 'コミュニケーション', '形', 'ソフトウエア', 'プロダクト', 'サービス', 'デザイン', '領域', '構築', 'コミュニティ', '社会', 'デザイン', 'マインド', '構築', '試み']
自分のブログの記事のTF-IDFをみてみると
サービス: 0.07386021461821728 エンジニア: 0.0496422258575285 視点: 0.04516931372977967 セールス: 0.040755639825903356 自分: 0.038783607361852934 チーム作り: 0.037493182828802825 やめ: 0.03584693947657752 コミュニティ: 0.03359602203641041 たい: 0.031145458128418263 プロダクト: 0.03067338816850126 こと: 0.030489747250684436
文章自体は似ているかはあれだが、分野はかなり近いと思う。
一番低いのも見てみると
print(all_words[778]) [...'駆潜艇', '七', '号', '駆潜艇', 'だい', 'なな', 'ごうく', 'せんてい', '日本海軍', '駆潜艇', '普遍的', '第四号型駆潜艇', '4番', '艇', '海軍省', '定め', '特務艇', '類別', '等級', '艦艇', '類別', '等級', '第一号型駆潜艇', '7番', '艇', 'マル3計画', '300', 'トン', '型', '駆潜艇', '仮称', '艦', '名', '62', '号', '艦', '計画', '1937年', '10月30日', '鶴見', '製鉄', '造船', '株式会社', '鶴見', '工場', '起工', '1938年', '4月15日', '七', '号', '駆潜艇', '命名', '特務艇', '駆潜艇', '第一号型', '4番', '艇', '定め', '6月10日', '進水', '11月20日', '竣工', '大湊', '防備', '隊', '附属', '1940年', '11月15日', '艦艇', '類別', '等級', '駆潜艇', '新設', '特務艇', '駆潜艇', '艦艇', '駆潜艇', '同日', '七', '号', '駆潜艇', '八', '号', '駆潜艇', '第九', '号', '駆潜艇', '3', '隻', '十', '一', '潜', '隊', '新編', '第二艦隊', '十', '一', '根拠地', '隊', '編入', '1941年', '4月10日', '十', '一', '潜', '隊', '第三艦隊', '一', '根拠地', '隊', '編入', '以後', '1941年', '9月', '断続', '的', '中国大陸', '沿岸', '監視', '哨戒', '10月31日', '十', '一', '潜', '隊', '南', '艦隊', '九', '根拠地', '隊', '編入', '太平洋戦争', '緒戦', 'ボルネオ', 'ミリ', 'クチン', '攻略', '作戦', '従事', 'マレー半島', '上陸', '船団', '護衛', '1942年', '3月', '以後', 'ペナン', '根拠地', 'シンガポール', 'サバン', '方面', '護衛', '従事', '4月10日', '十', '一', '潜', '隊', '一南遣艦隊第九根拠地隊', '編入', '以後', '一南遣艦隊隷下', '根拠地', '隊', '特別', '根拠地', '隊', '間', '異動', '任務', '従事', '1945年', '4月11日', 'カー・ニコバル', '沖', 'イギリス軍機', '空襲', '沈没', '5月25日', '帝国', '駆潜艇', '籍', '除か', '十', '一駆潜隊', '一駆潜隊', '一駆潜隊', '艦艇', '類別', '等級', '別表', '削除']
おぉおお・・戦争の話っぽくて、だいぶかけ離れている。
終わりに
似ている文章を、TF-IDFとコサイン類似度を使って計算した。割と似ているものが出たので、何か他のに使えそうだなーと思えるものだった。ただ、一部計算が重てく全文章できないのが悔しいので近々直したい。。
あと、ほぼ自力実装のためだいぶ理解が進んだ気する。
形態素解析ツールについてのまとめのまとめ
以前、形態素解析を行う際にMeCab
をインストールした。
恥ずかしながら、僕は日本語の形態素解析 = MeCab
と思っていたが、実は他にも結構あったのでメモがてらまとめてみる(随時)。
正直、僕の知識ではほとんどまとめられなかったので、先に感謝も込めて参考URLを紹介する。
↑ ツールごとの特徴をわかりやすくまとめてくださっていて、すごく勉強になりました。形態素解析にも設計思想などがあり、ここらへんは用途ごとにしっかり見極めないとなと思いました。
↑ 海外のもの(TREE TAGGERとかNLTKなど)をまとめてくれてるやつは貴重だったのでありがたいです。
↑ 形態素解析以外にも幅広くツールを紹介してくださってたので、色々広がりました。
↑ 海外の方で便利なツールを紹介してくださっていて、割とそろそろ日本語以外も使いそうなので参考になりました。
日本でよく使われているツールたち
MeCab
MeCab: Yet Another Part-of-Speech and Morphological Analyzer
おそらく、日本でよく使われている形態素解析ツールの一つ。速度と精度のバランスが良さそうな記事が多く見られた。
僕は日本語のみだと思っていたが、実は外国語にも対応しているらしく、辞書の設定で変更できるのかな?ここらへんは少し深掘りしていきたい。
JUMAN
JUMAN - KUROHASHI-KAWAHARA LAB
研究室から生まれた日本語専用のツール。
利用者ごとのチューニングがしやすいっぽいのかな。
JUMAN++
JUMAN++ - KUROHASHI-KAWAHARA LAB
同じ研究室で開発された、JUMAN
の後継。RNN言語モデルを採用しており、JUMANより高精度となっている。
しかしながら、以下の速度比較だと驚く結果となっている。
1秒間に処理できる文の数 MeCab, 53000 KyTea, 2000 JUMAN, 8800 Juman++ v1, 16 Juman++ v2, 4800
https://qiita.com/sugiyamath/items/69047b6667256034fa5e#juman
MeCab
優秀やん・・・そりゃあみんな使うw
もちろん、いいところも多く存在しこちらの記事では以下が強みと述べている。
# 特徴 1.表記揺れ 2.話し言葉 3.未定義語
語彙数自体はNEologdのほうが多いぽいが、話し言葉や未定義語に強いのはいいな。
うちではSNS
の言葉などを解析しているので、相性良さそうな気する。
Kuromoji
Java
で作られたのが売りのツール。
形態素解析ツール内での特徴はパッと調べた感じだと知ることはできなかったが、主にJava
で作られているシステムにとってはかなり組み込みやすいっぽい。
Sudachi
ワークスアプリケーションズが作ってるJava
製のツール。特徴はちょっと理解できてないのであとでまた・・。
ElasticSearch
プラグインの記事が多く目立った。
KeyTea
日本語専用の形態素解析ツール。
特徴としては、アノテーションされたデータを元にモデルを生成することができるらしく、つまり・・・?!(勉強します)
結構興味あるので深掘り対象。
ChaSen
奈良先端科学技術大学院大学によって開発されたツール。
Wikipediaによると
ベースとなった形態素解析ツールは JUMAN であるが、統計的な手法を用いており、解析速度と使い勝手の向上を目指している。現在はIPA品詞体系を使用しており、JUMAN とはその方向性が異なっている。
気持ちMeCab
よりになってるってこと?(だんだん後回し増えて来た)
英語メインツールたち
軽く英語メインっぽいのもまとめた。
↑ 鉄板的なやつに近いだろう。
Natural Language Toolkit — NLTK 3.3 documentation
↑ こちらも鉄板的なやつっぽいが、形態素解析以外にもいくつか機能が同梱されている。
日本語でやってみた記事などもあるため、これは別途紹介したいと思う。ポジネガ判断もできるようになってる(NLTK に Sentiment Analysis がやってきた)
Welcome to polyglot’s documentation! — polyglot 16.07.04 documentation
↑ 紹介した記事によると、サクッとためせるのでいいらしい。
終わりに
形態素解析だけでもこんなにあるなんて正直驚いた。ツールごとの特徴があって、その上でバランスが取られているMeCab
がよく使われているんだなとやんわり納得感がでてきた。
本来は、自分たちのサービスに合わせたツールを使うべきなので、いくつか深掘りして実際に使って見たいと思う。
あと紹介していないツールだったり、僕のオツムが足りずに詳細がまだ書けてないもあるので随時更新していく。
Jupyter Labをもっと快適にするためにvim Extensionを追加する
以前Jupyter Lab
を導入してから使い続けている。
ただ、僕はエセvimmer
なので若干使いにくいなーと思っていたところ、Jupyter Lab
には様々なExtention(拡張機能)を追加できるというのを知った。
ちなみにJupyter
からこの機能はあったみたいで、有志たちによって様々なExtensionは開発されている。以下はGithub
上にあるExtension
の中でtopic
(話題?)になってるやつだと思う。
Jupyter LabにVimを導入する
以下のExtensionを追加する。
コマンドは簡単。
jupyter labextension install jupyterlab_vim
$ jupyter labextension install jupyterlab_vim > npm pack jupyterlab_vim jupyterlab_vim-0.8.0.tgz Traceback (most recent call last): File "/usr/local/anaconda3/bin/jupyter-labextension", line 11, in <module> sys.exit(main()) File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyter_core/application.py", line 267, in launch_instance return super(JupyterApp, cls).launch_instance(argv=argv, **kwargs) File "/usr/local/anaconda3/lib/python3.6/site-packages/traitlets/config/application.py", line 658, in launch_instance app.start() File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterlab/labextensions.py", line 168, in start super(LabExtensionApp, self).start() File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyter_core/application.py", line 256, in start self.subapp.start() File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterlab/labextensions.py", line 58, in start for arg in self.extra_args] File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterlab/labextensions.py", line 58, in <listcomp> for arg in self.extra_args] File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterlab/commands.py", line 100, in install_extension return IOLoop.instance().run_sync(func) File "/usr/local/anaconda3/lib/python3.6/site-packages/tornado/ioloop.py", line 458, in run_sync return future_cell[0].result() File "/usr/local/anaconda3/lib/python3.6/site-packages/tornado/concurrent.py", line 238, in result raise_exc_info(self._exc_info) File "<string>", line 4, in raise_exc_info File "/usr/local/anaconda3/lib/python3.6/site-packages/tornado/gen.py", line 1069, in run yielded = self.gen.send(value) File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterlab/commands.py", line 158, in install_extension_async raise ValueError(msg) ValueError: "jupyterlab_vim@0.8.0" is not compatible with the current JupyterLab Conflicting Dependencies: JupyterLab Extension Package >=0.10.0-0 <0.11.0-0 >=0.17.2-0 <0.18.0-0 @jupyterlab/application >=0.10.0-0 <0.11.0-0 >=0.17.2-0 <0.18.0-0 @jupyterlab/notebook
簡単のはずエラー。バージョンの問題かな・・・。とりあえずJupyter Lab
のバージョン確認。
$jupyter --version 4.3.0 $ jupyter lab --version 0.27.0
最新は0.33
系なのでバージョンあげよう。anaconda
で色々試した結果、こんな感じでようやくできた。
# anacondaのバージョンアップ $ conda update conda # jupyterの再インストール $ conda uninstall jupyter $ conda install jupyter # いろんな記事をみたら、あるissueに書かれてたのでやってみたが、影響したかはわからない $ conda uninstall zeromq --force $ conda install zeromq # jupyter labの再インストール # conda-forgeからインストールすることで新しくなる $ conda uninstall jupyterlab $ conda install -c conda-forge jupyterlab # jupyter 5.3以下のおまじない $ jupyter serverextension enable --py jupyterlab --sys-prefix # バージョン確認 $ jupyter --version 4.4.0 $ jupyter lab --version 0.33.6
もう一度試す
$ jupyter labextension install jupyterlab_vim ... Child html-webpack-plugin for "index.html": 1 asset Entrypoint undefined = index.html [KTNU] ./node_modules/html-loader!./templates/partial.html 567 bytes {0} [built] [YuTi] (webpack)/buildin/module.js 497 bytes {0} [built] [aS2v] ./node_modules/html-webpack-plugin/lib/loader.js!./templates/template.html 1.22 KiB {0} [built] [yLpj] (webpack)/buildin/global.js 489 bytes {0} [built] + 1 hidden module ✨ Done in 81.59s.
だん!
Jupyter Labでvimを満喫する
vim
らしい極太の線がでてきた。
僕はキーバインドはカスタマイズしてないので、このまま意識せずvim
レた。やっぱいいな。慣れてる操作って。
メリット
- vimれる
デメリット
print
した際にでる表示項目は、デフォルトで一部しか見れないが、このExtensionを入れると全部見えてしまう- たまに
insert mode
になるのが遅くて困る
終わりに
インストールに少し苦戦したが、一度できてしまえば他のExtensionも楽に追加できる。
vim以外にも面白そうなExtensionがあったので、もう少し試したら紹介しようと思う。
TF-IDFを自力実装してみる
前回ライブラリーを使って実装しようとして断念した「TF-IDF」について自力で実装したいと思う。
ちなみに上記の記事は、TF値の出し方が間違ってて理解不足だった・・。 一度実装するとしないでは理解度が変わるという教訓。。いや計算式の理解の問題か・・。(修正済み)
TF値を求める
まずは数式の確認からすると、以下のようになる。
:文書における、単語のTF値
:単語の文書における出現回数
:文書におけるすべての単語の出現回数の和
まずは、前回同様に単語ごとの出現回数()をもとめると以下のようになる。
from sklearn.feature_extraction.text import CountVectorizer import numpy as np import logging logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) count_vectorizer = CountVectorizer(min_df=1, max_df=100, input='filename', token_pattern=u'(?u)\\b\\w+\\b') bag_of_words = count_vectorizer.fit_transform(['../data/blog_wakati_neo.txt']) sum_words = bag_of_words.sum(axis=0) words_freq = [(word, sum_words[0, idx]) for word, idx in count_vectorizer.vocabulary_.items()] words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
[('た', 32), ('の', 27), ('に', 26), ('を', 19), ('と', 15), ('し', 13), ('て', 12), ('ない', 10), ('から', 10), ('な', 10), ('も', 8), ('こと', 8), ('サービス', 8), ('が', 6), ('は', 6), 省略
続いて全ての単語数()を求めると
# 扱いやすいディクショナリーに変換 dict_words_freq = dict(words_freq) all_words_count = sum(dict_words_freq.values())
あとは「単語ごとの出現回数 / 全ての単語数」を行えば良いので以下のようになる
# TF値の計算 dict_words_freq = dict(words_freq) all_words_count = sum(dict_words_freq.values()) for key in dict_words_freq: print(key, dict_words_freq[key], dict_words_freq[key]/all_words_count)
無事できた。
た 32 0.05714285714285714 の 27 0.048214285714285716 に 26 0.04642857142857143 を 19 0.033928571428571426 と 15 0.026785714285714284 し 13 0.023214285714285715 て 12 0.02142857142857143 ない 10 0.017857142857142856 から 10 0.017857142857142856 な 10 0.017857142857142856 も 8 0.014285714285714285 こと 8 0.014285714285714285 サービス 8 0.014285714285714285 が 6 0.010714285714285714 は 6 0.010714285714285714 省略
IDF値を求める
まずは数式の確認からすると、以下のようになる。
:単語におけるIDF値
:すべての文章数
:において、単語が含まれる文章数
元となる文章は以前使った、Wikipediaのデータを元にする。
ただし、量が多くて重いため(2400万行)、100万行ほどのデータのみを使うこととする。
まずは、100万行のみのデータにする。
head -n 1000000 wiki_wakati_neo.txt > wiki_wakati_neo_head.txt
このままだと全ての文章が同じファイルにあるので、文章ごとに分割する。
「<\/ doc >」が区切りっぽいのでそれで分割する。
f = open('../data/wiki_wakati_neo_head.txt') wiki_texts = f.read().split('</ doc >') print(wiki_texts[0]) f.close()
< doc id =" 3216890 " url =" https :// ja . wikipedia . org / wiki ? curid = 3216890 " title =" 2015年アムトラック脱線事故 "> 2015年アムトラック脱線事故 ...省略
次の処理で使うので、リストの中の文字をユニークのリストに返してつかう関数を用意。
def unique(list1): unique_list = [] for x in list1: if x not in unique_list: unique_list.append(x) return unique_list
IDF値を求める。
from math import log # 初期化する # ブログの文章を含めると最低出現数は1となるため idf = {} for key in dict_words_freq: idf[key] = 1 # wikipediaが文章単位でわかれてるので # 文章ごとにブログ内の単語が存在するかカウントする for wiki_text in wiki_texts: # 同一文章内で複数のカウントをさせないため word_list = unique(wiki_text.split(' ')) for key in dict_words_freq: if key in word_list: # 文章内にブログの単語があった場合 if key in idf: idf[key] += 1 # idf[key]はただのカウントのため # 上記の計算式通りに計算し直す for key in idf: idf[key] = log(len(wiki_texts)/idf[key]) + 1 print(idf[key])
た 1.3175143932942954 の 1.0777938305785737 に 1.2131304792181579 を 1.3313370482515308 と 1.4096924908938504 し 1.4009755108032362 て 1.450652175643083 ない 2.5456519664401 から 1.7110617213423613 な 2.1554299751676784 も 1.8192044116505661 こと 2.1342823075479105 サービス 5.17021502327521 が 1.4339912023116144 は 1.0493246452432414 だ 2.732840227719004 けど 6.566265559340465 自分 4.343764024527529
自分のブログ内の文書をみると、「た」などの助動詞より、「サービス」や「自分」などよく使われてなさそうな値が高いことがわかる。(希少性が高い)
TF-IDF値を求める
まずは、先ほど求めたTF値を格納しておく
tf = {} for key in dict_words_freq: tf[key] = float(dict_words_freq[key]/all_words_count)
あとは掛け算するだけなので
tfidf = {} for key in dict_words_freq: tfidf[key] = tf[key] * idf[key]
た: 0.07528653675967402 サービス: 0.07386021461821728 色々: 0.06481618517768223 けど: 0.05862737106553986 に: 0.056323915106557335 もっと: 0.05489118978308473 の: 0.05196505968860981 なぁ: 0.050354351268196505 エンジニア: 0.0496422258575285 ない: 0.045458070829287496 を: 0.045170364137105505 視点: 0.04516931372977967 すごく: 0.043155772268012066 思う: 0.043005388411332744 振り返り: 0.04292777433362566 セールス: 0.040755639825903356 自分: 0.038783607361852934 やら: 0.03853413038011895 ....
あれ、、想定では、助動詞とかはよく出るからもっと低いはずが・・・100万行じゃちょっと少なかったのかもしれない。
名詞だけに絞ると
import MeCab m = MeCab.Tagger('-d /usr/local/mecab/lib/mecab/dic/mecab-ipadic-neologd/') feature_vector = {} for k, v in sorted(tfidf.items(), key=lambda x: -x[1]): node= m.parseToNode(k) while node: meta = node.feature.split(",") if meta[0] == ("名詞"): print(str(k) + ": " + str(v)) node = node.next
サービス: 0.07386021461821728 エンジニア: 0.0496422258575285 視点: 0.04516931372977967 セールス: 0.040755639825903356 自分: 0.038783607361852934 チーム作り: 0.037493182828802825 やめ: 0.03584693947657752 コミュニティ: 0.03359602203641041 たい: 0.031145458128418263 プロダクト: 0.03067338816850126 こと: 0.030489747250684436 業種: 0.029452854976636772 作り: 0.029273521205558244 ジム: 0.025032815107084036 顧客: 0.02418713487438621
綺麗になった。
今回調べたブログ内の言葉では、「サービス」がぶっちぎりで重要な言葉として抽出されていることがわかる。
また「エンジニア」や「視点」、「セールス」など自分が考えてそうな言葉が出てくるので納得感はある。
IDF値は「+1」の部分でチューニングができるっぽいので、もう少しやってみたい感はあるが次回の理解した時にする。
終わりに
ライブラリに頼らず(一部頼ったけど)、自前で実装するのは勉強になったし面白かった。
文章内の言葉から重要なキーワードを抽出するTF-IDF値は色々なところで使えそうだし、うちのサービスでも次使う予定なので次は実践しながら深掘りしていきたい。
word2vecの理論ついてざっくり理解しつつ試してみる
4、5年前くらいに自然言語処理コミュニティで流行ったword2vecというものがある。
「同じ文章にある単語同士は近しい」という仮定のもと、様々な文章を計算することによって100〜200次元(調整次第)の空間に各単語を「ベクトル」で表せるというものだ。
計算にはニューラルネットワークを用いている。僕には説明できるほど理解度が深まってないので、めちゃ分かりやすい記事を紹介する。
こちらも幅広く説明しててわかりやすい。word2vecの何が衝撃だったかも書いてあるので興味がある方はぜ是非。
www.slideshare.net
Wikipediaからモデルを作ってみる
Wikipediaのデータの準備は以前の記事で紹介してある。
from gensim.models import word2vec import logging logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) # 普通のipa辞書 sentences = word2vec.Text8Corpus('../data/wiki_wakati.txt') model = word2vec.Word2Vec(sentences, size=200, min_count=20, window=15) model.save("../data/wiki.model") # mecab-ipadic-NEologdの辞書 sentences_neo = word2vec.Text8Corpus('../data/wiki_wakati_neo.txt') model_neo = word2vec.Word2Vec(sentences_neo, size=200, min_count=20, window=15) model_neo.save("../data/wiki_neo.model")
RNNを利用して大量の単語を200(size)次元に圧縮する。window(文脈の最大単語数)はチューニングする上で重要らしく、ここをいじることで適切なモデルを構築が可能とのこと。
コサイン類似度から近しい単語の抽出
全単語をベクトルで表せるということは、その単語周辺にある単語も抽出できる。「エンジニア」に近しい単語を計算してみる。
from gensim.models import word2vec model = word2vec.Word2Vec.load("../data/wiki.model") results = model.wv.most_similar(positive=['エンジニア']) for result in results: print(result)
納得できそうな単語が並んだと思う。
('プログラマー', 0.654212474822998) ('アーキテクト', 0.639656662940979) ('デザイナー', 0.6255037188529968) ('システムエンジニア', 0.6227017641067505) ('エンジニアリング', 0.6170016527175903) ('チーフ', 0.6166958212852478) ('ディレクター', 0.6148343086242676) ('技師', 0.610156774520874) ('コンサルタント', 0.6077147722244263) ('テクニカルディレクター', 0.5718519687652588)
ベクトルによる足し算引き算を行う
もちろん足し算と引き算もできる。実際に「エンジニア」から「アーキテクト」を引いてみるとどうなるかやってみる。イメージとしては、「コーダー」的なイメージだが・・。
まずはipa辞書でやると
from gensim.models import word2vec model = word2vec.Word2Vec.load("../data/wiki.model") results = model.wv.most_similar(positive=['エンジニア'], negative=['アーキテクト']) for result in results: print(result) ('ジミ・ヘンドリックス', 0.4125749170780182) ('ミキサー', 0.39917922019958496) ('ドラム', 0.3988112211227417) ('ミックスダウン', 0.3903549015522003) ('フィーリング', 0.38688427209854126) ('ストリングス', 0.38426920771598816) ('フェンダー・ストラトキャスター', 0.3818718492984772) ('ハモンドオルガン', 0.38104212284088135) ('エントウィッスル', 0.3800022602081299) ('ソニー・ロリンズ', 0.37943196296691895)
音楽関連のワードがでてきた・・・。歌って踊れるエンジニアから、踊れるを抜いた状態なのだろうか。
次にNEologdの辞書で行ってみると
プログラマープログラマー from gensim.models import word2vec <200b> model = word2vec.Word2Vec.load("../data/wiki_neo.model") results = model.wv.most_similar(positive=['エンジニア'], negative=['アーキテクト']) for result in results: print(result) ('ジャコ', 0.38521867990493774) ('苦労', 0.37028828263282776) ('ミキサー', 0.35078269243240356) ('クルー', 0.34380707144737244) ('高所恐怖症', 0.3329920172691345) ('テープレコーダー', 0.3325659930706024) ('運転手', 0.3293205499649048) ('パム', 0.32380378246307373) ('スタッフ', 0.32033008337020874) ('機材', 0.3194393217563629)
音楽関連のジャコ出会って欲しいが、2番目が「苦労」となると「雑魚」の可能性もありえるな・・・。アーキテクトができないエンジニアはダメということか。。
終わりに
僕のチューニング不足であまりよくない結果になってしまったが、word2vecがすごいことは理解できた。
次はもう少し意味のある実践したいなー。
Think Stats(第2版)を読む:3章 確率質量関数
前回の続き。(100日ぶり)
3章:確率質量関数
PMF
確率質量関数のことは英語でPMF(probability mass function)という。
各値ごとに確率が計算され、紐づけられている状態。高校の頃、あるサイコロの目がどの確率でるか・・とやった思い出が微妙にあるけど、それのことだ。
サイコロの1の目と2の目の間には連続した数値はないので、こういった確率変数のことを「離散型」と呼ぶ。また、確率の約束としては全ての確率を足せば「1」になるとだけ覚えておけばいい。
例では、一人めの子供の妊娠周期(濃い青)と二人め以降の子供の妊娠期間(水色)を確率にして棒グラフで表示した。
離散型の分布にもいくつか種類があったり、連続型もあったりするので以下がとても参考になった。
ちなみに、本には「確率は出現度数を標本サイズnで割合したもの」と頭良く書いてあったので覚えておこうと思う。
ヒストグラムやPMF以外の可視化
ここまで2章でヒストグラムとPMFで可視化してきた。この2つのやり方は「データを探索してパターンや関係を同定しようと試みる上では有用」だが、より細かく特徴を見たりする際には色々工夫が必要とのこと。
さきほどのグラフをみると、35週目から46週目あたりで週ごとに差があることがわかる。一人目の妊娠と二人目以降の妊娠で確率の差を表してみると以下のように一人めの妊娠は41週目以降に生まれる確率の方がたかそうに見える。
このように、まずはヒストグラムやPMFなどのグラムで何が起こってるのかがわかれば、見つけたパターンをより明確に可視化する方法を検討できる方法につなげることができる。データサイエンティストの知り合いもまずは全体がわかるようなデータを可視化し、そこからどういう傾向があるか?とか深掘りしていってたので、その通りなんだなと思う。
ただこの本には、基本的にデータの偏りなどもあったり、偶然だったりする場合もあるので簡単に結論づけないようにしたほうがいいとのことだ。
クラスサイズのパラドックス
アメリカの大学では生徒対教師の比率が10:1となっているらしいが、実際に生徒に調べてもらったりすると生徒の比率の方がもっと高い割合になるデータになる。僕的にももっと生徒の比率の方が高くなるだろ・・と思うのだが、それこそがパラドックスである。
そもそもパラドックスとはなにかというと
パラドックス(paradox)とは、正しそうに見える前提と、妥当に見える推論から、受け入れがたい結論が得られる事を指す言葉である。逆説、背理、逆理とも言われる。 パラドックス - Wikipedia
ふむふむ、ようわからん・・・と思い続けて読んでみると
正しい仮定と正しい推論から正しい結論を導いたにも拘らず、結論が直観に反する ものも「パラドックス」と呼ばれる。これは擬似パラドックスと呼ばれ、前述した「真の」パラドックスとは別物である。 例えば誕生日のパラドックスは擬似パラドックスとして知られる。これは「23人のクラスの中に誕生日が同じである2人がいる確率は50%以上」というもので、数学的には正しい事実だが、多くの人は50%よりもずっと低い確率を想像する。他にもヘンペルのカラス、バナッハ・タルスキの逆理などが擬似パラドックスとして知られる。
この擬似パラドックスに近い感覚なのかな?と思う。
これは観察者におけるバイアスがかかってる場合があるということで、そのバイアスを計算する方法だったり、逆にバイアスを戻す方法なども紹介していた。(計算長くなるのでここではやらない)
調べていくうちに、これは観察者バイアスのことでもあるのかなと思い一応メモっておく。
関連する社会科学用語として観察者バイアス(英: observer bias 心理学用語では実験者効果)がある。これは、観察者が見出すことを期待している行動を強調しすぎて、それ以外の行動に気づかないという測定における誤差である。医学の試験で単盲検法ではなく二重盲検法が使われるのはこのためである。観察者バイアスは、研究者が行動を見てその意味を解釈しても、その行動をした本人にとっては何か別の意味があるという場合にも生じる。
観察者バイアスは、例えば他人が失敗した際に「努力が足りない、自業自得」と思うにも関わらず、自分が失敗すると「周りの環境が悪い、自分の能力不足のせいではない」というバイアスがかかってしまうことである。めちゃくちゃわかる。
自分を客観視してみる・・というのはこのバイアスを意識し、バイアスから抜けれた時なのかと思うと・・・やっぱり瞑想するしかないのではないかと思う。
それとパラドックスを色々まとめてくれてる面白いサイトがあったので貼っておく。 atarimae.biz
終わりに
今回の章は新しい用語が少なかったが、パラドックスの部分がなかなか理解できず時間をくった。
Jupyter Notebookの後継?Jupyter Labをつかってみた
自然言語の処理をする際に、JupyterNotebookと言われるノートブック形式のWebツールを使っている。
以下のようにPythonのコードを書きながらメモもとれ分析の実行結果(勿論グラフも)もみれるというすぐれものだ。このツールが最初に出たかはわらかないが、Spark(Scala)を動かすためのツールなどもオマージュして作られている。
また、GoogleはColaboratoryやAWSのSageMaker、IBMのIBM Data Science Experience、MSのMicrosoft Azure Notebooksなどがある。みんな大好きなツールというのはおわかりだろう。
そんなJupyterNotebookに後継っぽいのが出てると聞いて今回は使ってみる。
JuyputerLabについて調べてみる
あまり英語はわからないが、next-generation
という単語があるのでおそらく次世代のJupyterなんだろうなと見て取れる。
開発自体は、2015年ごろから行われていて、今でも割と活発に行われてるっぽい。
JupyterLabをインストールする
さっそくReadme
通りにインストールしてみる。
$ pip install jupyterlab Requirement already satisfied: jupyterlab in /usr/local/anaconda3/lib/python3.6/site-packages Requirement already satisfied: notebook>=4.3.1 in /usr/local/anaconda3/lib/python3.6/site-packages (from jupyterlab) Requirement already satisfied: jupyterlab_launcher>=0.4.0 in /usr/local/anaconda3/lib/python3.6/site-packages (from jupyterlab)
よっしゃ!インストールや!ともったら、anaconda
でPython
をインストールした際にすでに入ってたっぽい。。
If you are using a version of Jupyter Notebook earlier than 5.3, then you must also run the following command after installation to enable the JupyterLab server extension:
jupyter
のバージョンが5.3以前の人は引き継がせるためにはコマンドを打つ必要があるらしい。
jupyter --version
4.3.0
4.3なので打つ。
$ jupyter serverextension enable --py jupyterlab --sys-prefix Enabling: jupyterlab - Writing config: /usr/local/anaconda3/etc/jupyter - Validating... jupyterlab OK
JupyterLabの起動
インストールもおわったので、早速起動してみる。
$ jupyter lab [I 22:17:21.036 LabApp] JupyterLab alpha preview extension loaded from /usr/local/anaconda3/lib/python3.6/site-packages/jupyterlab JupyterLab v0.27.0 Known labextensions: [I 22:17:21.039 LabApp] Running the core application with no additional extensions or settings [I 22:17:21.046 LabApp] Serving notebooks from local directory: /Users/SayKicho/develop/analysis-practice/word2vec/jupyter [I 22:17:21.046 LabApp] 0 active kernels [I 22:17:21.046 LabApp] The Jupyter Notebook is running at: http://localhost:8888/?token=d061cb4b3067b85e8ca007f1ac4cc6130b2cfb908b07a6d5 [I 22:17:21.046 LabApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation). [C 22:17:21.049 LabApp] Copy/paste this URL into your browser when you connect for the first time, to login with a token: http://localhost:8888/?token=d061cb4b3067b85e8ca007f1ac4cc6130b2cfb908b07a6d5
最初のログインがアニメーションになってて、、次世代を感じる。
Notebookを押してみるといつも通りのJupyterNotebookがでるんだけど、なんと・・同じ画面で複数(Tab)開ける!!!以前だと、複数のノートを開くためにはブラウザ自体で新しいタブを開く必要があったのに、もうすでに次世代を感じている。
ConsoleではPythonの実行がコンソール画面でできるのだが、Notebookでいいじゃんと思ったり思わなかったり・・・。
一番次世代感を感じたのは、ドラッグ&ドロップによるセルの移動だ。こいつは便利だ・・・。使い始めたばかりの時からこの機能はホスィと思っていたので嬉しい。
他にデータファイル(CSVなど)をテーブル表示してくれるなどの便利機能があるっぽいが、そもそもJupyterNotebookですらあまり使い込んでないので、便利そうだなーというくらいにしかまだわからない。
終わりに
分析の作業量が少なくJupyterLabの魅力が伝えきれてないし、悪いところもわかってないが、自分がいいなと思う機能を紹介した。もっと分析の作業が増えた時に良いとこ悪いところに気づくと思うので、その際にはまた追加して書く。
vim Extensionも試してみた。 kzkohashi.hatenablog.com