Scrapy + Selenium + Headless Chromeを使ってJupyterからスクレイピングする
以前Scrapyを利用してみたが、Jupyter
で使えないのか調べて見たのと、ついでにHeadless
ブラウザでスクレイピングできないかも調べてみた。
Selenium + ChromeDriverのインストール
Selenium
はいつものpip
でインストールする。
pip install selenium
ChromeDriver
はbrew
でインストール。別の方法もあるみたいだが、自分はbrew
がインストールされてるのでこちらで。
$ brew install chromedriver Error: No available formula with the name "chromedriver" It was migrated from homebrew/core to caskroom/cask. You can access it again by running: brew tap caskroom/cask
エラー。見ている先にはないので、言われた通り実行する。
$ brew tap homebrew/cask $ brew cask install chromedriver
これでインストールできた。
最新のChromeDriver
をダウンロードしたい場合は以下からいける。
sites.google.com
JupyterでSelenium + ChromeDriver
の利用
まずはSelenium + ChromeDriver
の使い方。
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # ヘッドレスにするには必須 options.add_argument('--disable-gpu') options.add_argument('--headless') # 必須じゃないけどUserAgentとか変更したいなら追加する options.add_argument('--lang=ja') options.add_argument('--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36') # Chromeを指定。ここでFireFoxなども指定できるがHeadlessはないかも。 driver = webdriver.Chrome(chrome_options=options) # 実際にURLから取得する処理 driver.get('http://localhost) print(driver.page_source) driver.quit()
設定も簡単で、これでデータを取得できる。
ただ、このままだとfor
分で回してる時にエラーとか出て止まってしまうことがあるため、取得部分を以下のように変更した。
try: driver = webdriver.Chrome(chrome_options=options) driver.get('http://localhost') except: print('error') pass finally: driver.quit()
Scrapyを使って取得したデータの抽出
このままだと使いづらいので、Scrapy
に渡して抽出する。
from scrapy.http import HtmlResponse response = HtmlResponse( driver.current_url, body = driver.page_source, encoding = 'utf-8') sel = response.css('#id a::text') if sel.extract_first(): data = sel.extract_first().strip()
以上でできた。(あっさり・・)
補足しておくと、scrapy.http
ではHTML
形式だけではなく、XML
のパースにも対応してくれているので、利用する際は以下を見といたほうがいい。
Requests and Responses — Scrapy 1.5.1 documentation
終わりに
Jupyter
でScrapy + Selenium + Headless Chrome
の利用はかなり簡単にできた。
Python
のスクレイピングは情報が多く、めっちゃ楽だ。
Scrapyのスクレピングが簡単すぎて今更感動した話
僕はPHP
でスクレイピングする時はGoutteを使っていた。
サッやりたい時とかは便利だったりするが、robots.txt
の中身だったりの確認やページング処理については自分で実装が必要なため手間だなと思っていた。
ふと最近Python
をよく使ってるし、スクレイピング業界で有名なScrapyを一度使ってみるか・・と思ったのがきっかけである。
Scrapyとは
Scrapy | A Fast and Powerful Scraping and Web Crawling Framework
スクレイピングに特化しているフレームワークなだけあって、スクレイピングでやりたいことは基本的になんでもできる。(JS必須のサイトははまだ試してないが)
また、フレームワークというだけあって、やり方に沿ってプログラミングしていけば楽々できてしまうものである。実際にやってみる。
インストール
いつも通りのpip install
を行う。
pip install scrapy
プロジェクトの作成
Scrapyではプロジェクトのテンプレートを作ることができる。
scrapy startproject localhost
今回はローカルのサイトをスクレイピングしてみるため、localhost
を名前をつける。中身を確認すると、
$ cd localhost
$ tree
.
├── localhost
│ ├── __init__.py
│ ├── __pycache__
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ ├── __init__.py
│ └── __pycache__
└── scrapy.cfg
データの格納場所を作る
スクレイピングしてきたデータは、Entity
のようなデータの入れ物を設定する必要があるためそこを修正する。
$ vim items.py # -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class LocalhostItem(scrapy.Item): # この部分だけ追加 user_name = scrapy.Field()
今回は、user_name
というデータを取ってくるためその入れ物を追加しておく。
Spiderでスクレイピングの処理を書く
実際にクローリングやスクレイピングを行うのはこのSpiderのお仕事で、その準備をする。
まずはSpider
を作るためにコマンドを叩く。
scrapy genspider local localhost.com
スクレイピングの名前、スクレイピングするホスト名という感じで書く。
実行すると、spider
ディレクトリいかにlocal.py
ができるので編集する。
$ vim spider/local.py
デフォルトは以下のような感じになる。
# -*- coding: utf-8 -*- import scrapy class LocalSpider(scrapy.Spider): name = 'local' allowed_domains = ['localhost.com'] start_urls = ['http://localhost.com/'] def parse(self, response): pass
start_urls
はクロールしたいパスなどを複数記述することができる。
ただし、HTMLの構造が似ていないと難しいので基本的には同じ系統のデータだが、パス名が違う場合のみ書く。異なる場合はまた別にspider
ファイルを作る。
start_urls = ['http://localhost.com/path1', 'http://localhost.com/path2']
次に実際のデータを取得するためのロジックをparse
に書く。
# 先ほど作った入れ物のインポート from localhost.items import LocalhostItem ...省略 def parse(self, response): # response.cssでデータの中身をcssみたく取ってこれる for sel in response.css('.column > section > .section-accountlist'): article = LocalhostItem() article['user_name'] = sel.css('.username::text').extract_first() yield article
response.css
でcss
を指定するとデータを取得することができる。今回は複数あるため、for
で回し、さらにそこからデータ取得し、格納する。
さらに今回はページング処理もあるので、ページングする際の「次へ」のリンクを探し、再帰を行えるようにする。
def parse(self, response): # response.cssでデータの中身をcssみたく取ってこれる for sel in response.css('.column > section > .section-accountlist'): article = LocalhostItem() article['user_name'] = sel.css('.username::text').extract_first() yield article next_page = response.css('nav .link-next::attr("href")') if next_page: url = response.urljoin(next_page.extract_first()) yield scrapy.Request(url, callback=self.parse)
Spiderの準備はこれで終わり。
実際にスクレイピングしてみる
spider
の名前で起動できる。
$ scrapy crawl local -o result.csv
-o
オプションをつけると、データの中身をアウトプットできる。
これで、後は勝手にスクレイピングし、データ(今回だとuser_name
)を書き込んでくれる。
終わりに
Scrapy
はわりと有名だったので知っていたが、ここまで便利だとは思っておらずメモがてら今回は書いた。
実際にはデータベースも絡んだ実装などにもなってくると思うので、次はその辺かきたいなー。
Think Stats(第2版)を読む:5章 分布をモデル化する
前回に続いて統計の勉強。
今まで扱った分布は経験分布(empirical distributions)と呼ばれているもので、すべて実際の観察に基づいた分布だったからとのこと。
そうなると、標本サイズはデータサイズが限界となり、有限となる。
また、そのほかにはCDFで特徴付けられる解析分布(analytic distribution)というものがある。普通は連続分布(continuous distribution)と呼ぶらしいが、なんで解析分布にしたんだろう・・。解析分布は、経験分布をモデル化するのに使えるとのこと。ここでのモデル(model)とは、必要がない詳細を省いた単純化のことらしい。ちょっとよくわからないが、進めていけばわかるだろう。
指数分布
簡単な指数分布(exponetial distribution)からモデル化を始める。
指数分布のCDF
の式とグラフ以下になる
母数(パラメータ)λが分布の形状を決定する。
指数分布は一連の事象を観測して、到着時間間隔(interarrival time)と呼ばれる、事象間の時間を計測する時に現れる。
なるほど・・・全然わからない。これが重要なのかな
事象が常に同じ確からしさで起こる時、到着時間間隔の分布は指数分布になる傾向がある。
だいたいわかったけど難しい単語が多いので、以下の記事がわかりやすかった。
では、どうやって実際の分布が指数分布のモデルに当てはまると断言できるのか。
1つの方法として、相補CDF(CCDF)、つまり1 - CDF(x)
で描画して見るとわかりやすくなるとのこと。
本来は直線に近くなるはずだが、そうではないためこのデータには指数分布はあってないという判断にするっぽい。
おそらく、計算で妥当性を評価するやつもありそうだけどここでは書いてなかった。
正規分布
正規分布はガウス分布とも呼ばれている。近似的に多くの現象を記述できるので、非常によく使われている。なぜ現れるのかは別の章での説明になるとのこと。
実際のデータとモデルとの比較した際に、かなり酷似しているので、適切なモデルと言える。
ただ、10パーセントタイル値より下のデータでは少し差がでているため、解きたい課題によっては適切ではない場合があるとのこと。
正規確率プロット
指数分布は相補CDFのような、モデルが適切か判断する式があったが正規分布にはない。
代わりに、正規確率プロットと呼ばれう方法を使う。
細かい説明は省くが、正規分布で用いたデータを使うと実際のデータはモデル(正規分布)より、裾のほうがズレが生じてるのがわかる。
対数正規分布
値の対数をとって、正規分布したのが対数正規分布となる。
正規分布とやり方はそんなかわらないのでスキップ。
パレート分布
パレート分析は自然や社会科学におけるさまざな現象、都市や待ち、砂粒や隕石、地震の大きさなどを記述するのに用いられてきた。式とグラフは以下になる。
指数分布ぽくなる。実際のデータを使った、CCDFを以下にのせるが、この結論は「この分布の裾はパレート分析に適合する」とのことで、他のモデルで補えない裾のモデル化とかにも使われる。
モデルが何に役に立つのか?
解析分布でモデル化するこの意味としては、モデル全般に言えることでもあるが、抽象化することで詳細を省くことができる。細かい標本特有の誤差やねじれがあっても、特異性を除去することでなめらかにしたもににできる。
また、解析モデルはデータ圧縮の一形式でもある。モデルがデータによく一致するなら、少数の母数で大量のデータを要約することができる。
終わりに
少し式が難しくなってきたが、丁寧な説明のおかげでまだついていけてる。統計検定というものが存在するので、少し受けて見たい感出てきた。
Laravelで特定のページに特定のユーザー以外が入ってきたら404を返す
管理画面を作っていると、一般ユーザーにはアクセスされたくないページが出てくる。
セキュリティを考えるとIP
制限などが理想だったりするが、Laravel
で簡単に解決したい場合があるので今回はそちらのやり方でやってみる。
カラムの追加とミドルウェアの準備
ユーザー情報を表すuser
テーブルはよく使われているので説明を省く。また、このテーブルには管理者を表すis_admin
カラムを追加しておく。
ミドルウェアは以下のように作る。シンプルにis_admin
ではなかったら404
を返すようにする。
<?php namespace App\Http\Middleware; use App\Models\User; use Closure; use Illuminate\Http\Request; class VerifyAdminUser { /** * @param Request $request * @param Closure $next * @param null $guard * @return mixed */ public function handle($request, Closure $next, $guard = null) { /** @var User $user */ $user = $request->user(); if (!$user->is_admin) { abort(404); } return $next($request); } }
次にApp\Http\Kernel
に上記の作成したミドルウェアを追加する。
<?php ...省略 protected $routeMiddleware = [ 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, // 追加 'administrator' => \App\Http\Middleware\VerifyAdminUser::class, ];
ルーティングへの追加
あとはルーティングのmiddleware
に追加するだけで良い。
<?php // middlewareにadministratorを追加 // 非管理者がこのグループにアクセスしようとすると404が帰ってくる Route::group(['middleware' => ['auth:api', 'administrator']], function () { Route::get('/dashboard', 'DashboardController@index')->name('admin.dashboard'); });
ちなみにページも何も用意してないのでただ、404が返ってくる悲しいものになってしまった。
終わりに
今回は管理者以外のユーザーには404を返すようにしたが、リダイレクトするなり他の優しい手段もあるのでそちらもやってみるといいかも。にしてもLaravelのミドルウェア便利だなー。
Think Stats(第2版)を読む:4章 累積分布関数
前回に引き続き統計のお勉強。 kzkohashi.hatenablog.com
PMFの限界
前回、PMF(確率質量関数)を用いて、一人めの子供の妊娠周期(濃い青)と二人め以降の子供の妊娠期間(水色)を確率にして棒グラフで表示した。
このように、PMFは値の個数が少ない時(ここでいうとweeksのこと)は可視化し、全体像を掴んだり比較するのに大変便利であった。
しかし、個数が増えると各値の確率が小さくなり、ランダムノイズの影響が大きくなる。
例えば、第一子と第二子の新生児の出生時体重の分布をPMFを用いて表示してみると、以下のようになる。
どちらもの分布も正規分布のベル曲線にいているが、解釈が難しい。確率が低すぎる値なのであまり意味してないのと、どちらが平均値が高いのか?などがパッとわからない。
これらの問題を解決するために累積分布関数(CDF)を使うといいっぽいので、やってみる。
パーセンタイル
CDFに入る前に、パーセンタイルについて説明する。
パーセンタイルは学生の時にテストの結果を受け取る際に、結果として用いられることが多い。
例えば、各5人のテスト結果が「55, 66, 77, 88, 99」点だとすると、自分が「88点」ならば、「100 * 4/5」でパーセントタイル順位は80となり、パーセントタイル値は88となる。そんなテスト結果の受けとりの仕方はしたことないけど、次に進もう。
累積分布関数(CDF)
CDFは各値をパーセンタイル順位に対応づける関数とのこと。パーセンタイルと一緒じゃないか、と思ってたら、違いとしては以下になる。
パーセンタイル順位は結果が0 ~ 100の範囲に対して、CDFでは0 ~ 1の範囲の確率で表現する
例として、妊娠期間をCDFで表現すると以下のようなグラフが作成できる。
X軸が妊娠週で、Y軸がCDFとなっている。このグラフの一つの読み方としては、パーセンタイル値を見つけるというものらしい。あまりよくわかってないが、続けて例をだしてくれていて、「約10%の妊娠が36週より短いこと」「90%が41週より短いこと」が見て取れるという言い方ができる。また、度数の高い値は急な落差になっているため、39週が最頻値であることが見て取れる。
CDFを比較する
いよいよ、先ほどPMFで表現した「第一子と第二子の新生児の出生時体重の分布」をCDFを使って表現する。
図が荒くて若干見づらいが、第一子のほうが平均値がわずかにたかそうなのがわかる。ただ、図だとやっぱり最頻値などはぱっと見わからないんだなと思った。
パーセンタイル派生統計量
CDFを一旦計算すれば、パーセンタイル値とパーセンタイル順位の計算は容易とのこと(そりゃあそうだw)。
パーセンタイル派生要約統計量を計算するのに使える。例えば、50位パーセンタイル値は中央値(median)と呼ばれる。また、***四分位範囲(interquartile range, IQR)と呼ばれる分布の広がりの尺度も表せる。IQRは75位パーセンタイル値と25位パーセンタイル値との差。
一般的に、パーセンタイル値は分布の形状の要約によく用いられる。Wikipediaで詳しくは調べとけとのこと。
ここらへんは以前何かの講義受けた時に少しやったようなやってないような。
乱数
ここ少し省略すると、CDFの形状がなんであれ、パーセンタイル順位の分布は一様と言いたかった。
だいたい直線に近くなるとのこと。
パーセンタイル順位を比較
パーセンタイル順位は異なるグループに対する評価尺度を比較する時に役に立つ。
例えば、「男性の20代のマラソン」と「男性の30代のマラソン」の順位を比べるとすると
- 「男性の20代のマラソン」で1000人中100位、つまりはパーセント順位が90%
- 「男性の30代のマラソン」に10年後に出る際に、同じパーセント順位を維持するにはどれくらいのタイムを維持してれば良いのか?
の計算は過去の「男性の30代のマラソン」のデータをもとに、その際のパーセント順位90%のタイムを見ればわかるよねって話。
終わりに
今回もそんなに難しくなかったけど、自分の例で何か一度やっておきたいなー。
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
がよく使われているんだなとやんわり納得感がでてきた。
本来は、自分たちのサービスに合わせたツールを使うべきなので、いくつか深掘りして実際に使って見たいと思う。
あと紹介していないツールだったり、僕のオツムが足りずに詳細がまだ書けてないもあるので随時更新していく。