いっきのblog

技術とか色々

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(確率質量関数)を用いて、一人めの子供の妊娠周期(濃い青)と二人め以降の子供の妊娠期間(水色)を確率にして棒グラフで表示した。

f:id:kzkohashi:20180728222230p:plain

このように、PMFは値の個数が少ない時(ここでいうとweeksのこと)は可視化し、全体像を掴んだり比較するのに大変便利であった。
しかし、個数が増えると各値の確率が小さくなり、ランダムノイズの影響が大きくなる。

例えば、第一子と第二子の新生児の出生時体重の分布をPMFを用いて表示してみると、以下のようになる。

f:id:kzkohashi:20180812215556p:plain

どちらもの分布も正規分布のベル曲線にいているが、解釈が難しい。確率が低すぎる値なのであまり意味してないのと、どちらが平均値が高いのか?などがパッとわからない。
これらの問題を解決するために積分布関数(CDF)を使うといいっぽいので、やってみる。

パーセンタイル

CDFに入る前に、パーセンタイルについて説明する。
パーセンタイルは学生の時にテストの結果を受け取る際に、結果として用いられることが多い。

例えば、各5人のテスト結果が「55, 66, 77, 88, 99」点だとすると、自分が「88点」ならば、「100 * 4/5」でパーセントタイル順位は80となり、パーセントタイル値は88となる。そんなテスト結果の受けとりの仕方はしたことないけど、次に進もう。

積分布関数(CDF)

CDFは各値をパーセンタイル順位に対応づける関数とのこと。パーセンタイルと一緒じゃないか、と思ってたら、違いとしては以下になる。

パーセンタイル順位は結果が0 ~ 100の範囲に対して、CDFでは0 ~ 1の範囲の確率で表現する

例として、妊娠期間をCDFで表現すると以下のようなグラフが作成できる。

f:id:kzkohashi:20180812221551p:plain

X軸が妊娠週で、Y軸がCDFとなっている。このグラフの一つの読み方としては、パーセンタイル値を見つけるというものらしい。あまりよくわかってないが、続けて例をだしてくれていて、「約10%の妊娠が36週より短いこと」「90%が41週より短いこと」が見て取れるという言い方ができる。また、度数の高い値は急な落差になっているため、39週が最頻値であることが見て取れる。

CDFを比較する

いよいよ、先ほどPMFで表現した「第一子と第二子の新生児の出生時体重の分布」をCDFを使って表現する。

f:id:kzkohashi:20180812221955p:plain

図が荒くて若干見づらいが、第一子のほうが平均値がわずかにたかそうなのがわかる。ただ、図だとやっぱり最頻値などはぱっと見わからないんだなと思った。

パーセンタイル派生統計量

CDFを一旦計算すれば、パーセンタイル値とパーセンタイル順位の計算は容易とのこと(そりゃあそうだw)。
パーセンタイル派生要約統計量を計算するのに使える。例えば、50位パーセンタイル値は中央値(median)と呼ばれる。また、***四分位範囲(interquartile range, IQR)と呼ばれる分布の広がりの尺度も表せる。IQRは75位パーセンタイル値と25位パーセンタイル値との差。
一般的に、パーセンタイル値は分布の形状の要約によく用いられる。Wikipediaで詳しくは調べとけとのこと。

分位数 - Wikipedia

ここらへんは以前何かの講義受けた時に少しやったようなやってないような。

乱数

ここ少し省略すると、CDFの形状がなんであれ、パーセンタイル順位の分布は一様と言いたかった。

f:id:kzkohashi:20180812225456p:plain

だいたい直線に近くなるとのこと。

パーセンタイル順位を比較

パーセンタイル順位は異なるグループに対する評価尺度を比較する時に役に立つ。

例えば、「男性の20代のマラソン」と「男性の30代のマラソン」の順位を比べるとすると

  • 「男性の20代のマラソン」で1000人中100位、つまりはパーセント順位が90%
  • 「男性の30代のマラソン」に10年後に出る際に、同じパーセント順位を維持するにはどれくらいのタイムを維持してれば良いのか?

の計算は過去の「男性の30代のマラソン」のデータをもとに、その際のパーセント順位90%のタイムを見ればわかるよねって話。

終わりに

今回もそんなに難しくなかったけど、自分の例で何か一度やっておきたいなー。

TF-IDFとコサイン類似度を使って似ている文章を見つける

今回は、以前実装したTF-IDFの処理をベースに、自分のブログに一番近いWikipediaの文章は何かをコサイン類似度を使って出してみる。

kzkohashi.hatenablog.com

コサイン類似度とは?

\large{\cos(\pmb x, \pmb y) = \frac {\pmb x \cdot \pmb y}{||\pmb x|| \cdot ||\pmb y||}}

高校の数学でやったようなやってないようなうる覚えな感じだったので、他の方のサイトを参考にすると

コサイン類似度は2本のベクトルがどれくらい同じ向きを向いているのかを表す指標

mathtrain.jp

となり、文章を全単語で表現されたベクトル空間で表すことで計算できる。
単純にある文章にその単語が含まれているかを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のデータを準備する。
ほぼメモのような感じなのでペタペタ貼ってく。

kzkohashi.hatenablog.com

文章単位に分割し、さらに単語に分割

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をインストールした。

kzkohashi.hatenablog.com

恥ずかしながら、僕は日本語の形態素解析 = MeCabと思っていたが、実は他にも結構あったのでメモがてらまとめてみる(随時)。

正直、僕の知識ではほとんどまとめられなかったので、先に感謝も込めて参考URLを紹介する。

qiita.com

↑ ツールごとの特徴をわかりやすくまとめてくださっていて、すごく勉強になりました。形態素解析にも設計思想などがあり、ここらへんは用途ごとにしっかり見極めないとなと思いました。

udemy.benesse.co.jp

↑ 海外のもの(TREE TAGGERとかNLTKなど)をまとめてくれてるやつは貴重だったのでありがたいです。

自然言語処理ツール

形態素解析以外にも幅広くツールを紹介してくださってたので、色々広がりました。

lab.astamuse.co.jp

↑ 海外の方で便利なツールを紹介してくださっていて、割とそろそろ日本語以外も使いそうなので参考になりました。

日本でよく使われているツールたち

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
もちろん、いいところも多く存在しこちらの記事では以下が強みと述べている。

qiita.com

# 特徴
1.表記揺れ
2.話し言葉
3.未定義語

語彙数自体はNEologdのほうが多いぽいが、話し言葉や未定義語に強いのはいいな。
うちではSNSの言葉などを解析しているので、相性良さそうな気する。

Kuromoji

www.atilika.com

Javaで作られたのが売りのツール。
形態素解析ツール内での特徴はパッと調べた感じだと知ることはできなかったが、主にJavaで作られているシステムにとってはかなり組み込みやすいっぽい。

Sudachi

github.com

ワークスアプリケーションズが作ってるJava製のツール。特徴はちょっと理解できてないのであとでまた・・。
ElasticSearchプラグインの記事が多く目立った。

KeyTea

www.phontron.com

日本語専用の形態素解析ツール。
特徴としては、アノテーションされたデータを元にモデルを生成することができるらしく、つまり・・・?!(勉強します)
結構興味あるので深掘り対象。

ChaSen

chasen-legacy.osdn.jp

奈良先端科学技術大学院大学によって開発されたツール。
Wikipediaによると

ベースとなった形態素解析ツールは JUMAN であるが、統計的な手法を用いており、解析速度と使い勝手の向上を目指している。現在はIPA品詞体系を使用しており、JUMAN とはその方向性が異なっている。

ChaSen - Wikipedia

気持ちMeCabよりになってるってこと?(だんだん後回し増えて来た)

英語メインツールたち

軽く英語メインっぽいのもまとめた。

TreeTagger

↑ 鉄板的なやつに近いだろう。

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を導入してから使い続けている。

kzkohashi.hatenablog.com

ただ、僕はエセvimmerなので若干使いにくいなーと思っていたところ、Jupyter Labには様々なExtention(拡張機能)を追加できるというのを知った。
ちなみにJupyterからこの機能はあったみたいで、有志たちによって様々なExtensionは開発されている。以下はGithub上にあるExtensionの中でtopic(話題?)になってるやつだと思う。

github.com

Jupyter LabにVimを導入する

以下のExtensionを追加する。

github.com

コマンドは簡単。

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レた。やっぱいいな。慣れてる操作って。

f:id:kzkohashi:20180803222816p:plain

メリット

デメリット

  • printした際にでる表示項目は、デフォルトで一部しか見れないが、このExtensionを入れると全部見えてしまう
  • たまにinsert modeになるのが遅くて困る

終わりに

インストールに少し苦戦したが、一度できてしまえば他のExtensionも楽に追加できる。
vim以外にも面白そうなExtensionがあったので、もう少し試したら紹介しようと思う。

TF-IDFを自力実装してみる

前回ライブラリーを使って実装しようとして断念した「TF-IDF」について自力で実装したいと思う。

kzkohashi.hatenablog.com

ちなみに上記の記事は、TF値の出し方が間違ってて理解不足だった・・。 一度実装するとしないでは理解度が変わるという教訓。。いや計算式の理解の問題か・・。(修正済み)

TF値を求める

まずは数式の確認からすると、以下のようになる。

\large{tf_{t,d} = \frac{n_{t,d}}{\sum_k n_{k,d}}}

tf_{t,d}:文書dにおける、単語tのTF値

{n_{t,d}}:単語tの文書dにおける出現回数

{\sum_k n_{k,d}}:文書dにおけるすべての単語の出現回数の和

まずは、前回同様に単語ごとの出現回数({n_{t,d}})をもとめると以下のようになる。

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),
省略

続いて全ての単語数({\sum_k n_{k,d}})を求めると

# 扱いやすいディクショナリーに変換
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値を求める

まずは数式の確認からすると、以下のようになる。

\large{idf_t = \mbox{log} \frac{N}{df_t}} + 1

idf_t:単語tにおけるIDF値

N:すべての文章数

df_tNにおいて、単語tが含まれる文章数

元となる文章は以前使った、Wikipediaのデータを元にする。
ただし、量が多くて重いため(2400万行)、100万行ほどのデータのみを使うこととする。

kzkohashi.hatenablog.com

まずは、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次元(調整次第)の空間に各単語を「ベクトル」で表せるというものだ。
計算にはニューラルネットワークを用いている。僕には説明できるほど理解度が深まってないので、めちゃ分かりやすい記事を紹介する。

www.randpy.tokyo

こちらも幅広く説明しててわかりやすい。word2vecの何が衝撃だったかも書いてあるので興味がある方はぜ是非。

www.slideshare.net

Wikipediaからモデルを作ってみる

Wikipediaのデータの準備は以前の記事で紹介してある。

kzkohashi.hatenablog.com

比較用にipaの辞書とipa-NEologdの辞書を使う。

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がすごいことは理解できた。
次はもう少し意味のある実践したいなー。