Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

世界一いらない人工知能??OpenCVを用いたカワウソ分類器作成奮闘記

 こんにちは!アイデミー研修生の川内と申します。
突然ですが、みなさんカワウソってご存知ですか??

f:id:shoichitech:20180519154839p:plain

 可愛いですね〜〜。よく犬と猫どっち派とか聞かれますが僕は断然カワウソ派です。

 OpenCVというのを使うとデフォルトで作成されているモデルを用いて人間の顔が検出することが出来ます。OpenCVについては下記のリンクをご覧ください。

機械学習のためのOpenCV入門
OpenCVで物体検出器を作成① 基礎知識【開発会社プロフェッサ】

 
 人の顔の画像の特徴量を抽出することにより学習するのですが、学習させるモデルにおいてはHaar-like特徴というのを用いています。Haar-like特徴は、簡単に言うと画像の明暗差により特徴を捉えます。例えば人間顔で言えば目は黒く、目元は明るいといった特徴をたくさん取ることで、人間の顔の特徴全体を捉える感じです。

Haar-likeについて(英語で書かれています)
Face Detection using Haar Cascades — OpenCV 3.0.0-dev documentation
和訳されたサイトもありました
Haar Cascadesを使った顔検出 — OpenCV-Python Tutorials 1 documentation


 ものは試しで早速やってみましょう。
f:id:shoichitech:20180519153815j:plain
 社長の石川です。いい顔してますね。

# -*- coding: utf-8 -*-

import cv2

#HAAR分類器の顔検出用の特徴量
cascade_path = "/usr/local/opt/opencv/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml"


image_path = "sample.jpg"

color = (255, 255, 255) #白

#ファイル読み込み
image = cv2.imread(image_path)

#カスケード分類器の特徴量を取得する
cascade = cv2.CascadeClassifier(cascade_path)

#物体認識(顔認識)の実行
#image – CV_8U 型の行列.ここに格納されている画像中から物体が検出されます
#objects – 矩形を要素とするベクトル.それぞれの矩形は,検出した物体を含みます
#scaleFactor – 各画像スケールにおける縮小量を表します
#minNeighbors – 物体候補となる矩形は,最低でもこの数だけの近傍矩形を含む必要があります
#flags – このパラメータは,新しいカスケードでは利用されません.古いカスケードに対しては,cvHaarDetectObjects 関数の場合と同じ意味を持ちます
#minSize – 物体が取り得る最小サイズ.これよりも小さい物体は無視されます
facerect = cascade.detectMultiScale(image, scaleFactor=1.1, minNeighbors=1, minSize=(1, 1))

if len(facerect) > 0:
    #検出した顔を囲む矩形の作成
    for rect in facerect:
        cv2.rectangle(image, tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]), color, thickness=2)

    #認識結果の保存
    cv2.imwrite("face_detected.jpg", image)

 さて結果がこちらになります。
f:id:shoichitech:20180519155552j:plain
 うまく出来ていますね。こんな感じでカワウソの画像を認識して顔を四角で囲ってみたくなりました。誰も使いません。世界一いらない人工知能と言っても過言ではないでしょう。ただ、カワウソが好きという理由だけで作ってみようと思い立ちました。下記のリンクの方が猫の顔検出モデルを作成されていたのでそれを参考に作ってみるという方針を立てました。画像さえ集められればモデル構築出来そうということが分かりました。

ねこと画像処理 part 2 – 猫検出 (モデル配布) « Rest Term

 

画像収集

 さて、方針は決まって後は画像を集めるのですがここが一番の肝です。今回はFlickrという画像共有サイトのAPIを利用して画像を集めました。利用方法については下記を参考にしました。
Flickr APIを使って画像ファイルをダウンロードする

 本来であれば正解データ7000枚、不正解データ3000枚ほど必要なのですがflickr APIではカワウソの画像400枚ほどしか集められませんでした。こうやればたくさん集められるよ!!というのを知っている方がいらっしゃれば教えてくださいm(_ _)m。不正解データは同様にしてパンダの画像を集めました。


正解データの例
f:id:shoichitech:20180519164511j:plain


不正解データの例
f:id:shoichitech:20180520214210j:plain

以下に画像取得の時に利用したスクリプトを載せておきます

import os

import time
import traceback

import flickrapi
from urllib.request import urlretrieve

import sys
from retry import retry

flickr_api_key = ""
secret_key = ""

keyword = sys.argv[1]


@retry()
def get_photos(url, filepath):
    urlretrieve(url, filepath)
    time.sleep(1)


if __name__ == '__main__':

    flicker = flickrapi.FlickrAPI(flickr_api_key, secret_key, format='parsed-json')
    response = flicker.photos.search(
        text=keyword,
        per_page=1000,
        media='photos',
        sort='relevance',
        safe_search=1,
        extras='url_n,license'
    )
    photos = response['photos']

    try:
        if not os.path.exists('./image-data/' + keyword):
            os.mkdir('./image-data/' + keyword)

        for photo in photos['photo']:
            try:
                url_q = photo['url_n']
                filepath = './image-data/' + keyword + '/' + keyword + "-" + photo['id'] + '.jpg'
                get_photos(url_q, filepath)
            except KeyError:
                print("error!!!")

    except Exception as e:
        traceback.print_exc()

正解データのラベル付け

 さて、なんとか画像を集めることが出来たのですが、正解データの画像のどの座標の位置にカワウソの顔があるかを指定していかなければなりません。githubにブラウザで画像データの座標を記録出来るプログラムを作られてる方がいたのでそちらを利用させていただくことにしました。
github.com

f:id:shoichitech:20180519161056p:plain
 
 こんな感じで地道にカワウソの顔を囲っていきます。画像が赤い四角形で囲まれた状態でNEXTボタンを押すと正解データに座標と共に分類され、何もない状態で押すと不正解データに分類されます。しかし、カワウソの画像でも横顔のデータなどは正解データとしては不適切でそういった画像の時はSKIPボタンでどちらにも分類されないようにしました。集めたカワウソの画像の中には横顔だったり、二次元のキャラクターのカワウソも含まれていたので正解データに分類された画像は5割程となりました。


f:id:shoichitech:20180519164659j:plain
正解データとはならない写真の例
 
 正解データ、不正解データに分類するのにものすごく時間かかりました・・・・。正解、不正解合わせて800枚ほどのデータが集まったのですが3時間くらいひたすら画像とにらめっこして分類してました。macのトラックパッドにクリックしすぎて指がおかしくなりましたが、ありとあらゆるカワウソの写真を見れて幸せな時間でもありました笑。

正解のデータのテキストファイルです。【画像ファイル名 対象の物体の数 x座標 y座標 width height】 の順に記録されます。

info.dat
static/img/otter-6814063158.jpg  1  50 24 66 50
static/img/otter-4494710692.jpg  1  174 30 52 38
static/img/otter-8885141162.jpg  1  78 42 67 60
static/img/otter-25450661078.jpg  1  102 36 69 49
static/img/otter-14377745688.jpg  1  38 46 152 97
static/img/otter-32616826780.jpg  1  44 22 133 101
static/img/otter-26964747057.jpg  1  57 55 37 24
static/img/otter-27695436712.jpg  1  64 26 84 67
static/img/otter-5105713030.jpg  1  113 35 75 54
static/img/otter-7394889242.jpg  1  35 73 51 43
static/img/otter-27807883499.jpg  1  89 31 77 50
static/img/otter-33148797350.jpg  1  99 42 37 36
static/img/otter-7941157504.jpg  1  126 14 58 41
static/img/otter-32646878413.jpg  1  49 89 35 27
static/img/otter-33520353216.jpg  1  110 49 52 46
.
.
.


不正解のデータのテキストファイルです。

bg.txt
static/img/panda-8976226384.jpg
static/img/panda-9325505556.jpg
static/img/panda-8761206443.jpg
static/img/panda-4107493751.jpg
static/img/panda-2155184460.jpg
static/img/panda-8426952443.jpg
static/img/panda-190747022.jpg
static/img/panda-26702691447.jpg
static/img/panda-7999165142.jpg
static/img/panda-19517101803.jpg
.
.
.

モデル構築

 そんなこんなで苦労して分類したデータをOpencvを用いて学習させてみました。まずOpenCVに認識してもらうために、正解データをバイナリファイルに変換する処理を行います。

opencv_createsamples -info info.dat -vec kawauso.vec -num 175

 numはサンプル数です。学習させるために以下のコマンドを実行します。

opencv_traincascade -data kawauso/ -vec kawauso.vec -bg bg.txt -numPos 157 -numNeg 376 -featureType HAAR -mode ALL
  • data モデルの保存先です。kawausoというディレクトリを作成しておきました。
  • numPos 正解データの要素数を指定しているのですが、サンプル数×0.9くらいがいいらしいです。

OpenCVのtraincascadeのnumPos引数はvecファイル内のサンプル数より少ない数を設定すること - takminの書きっぱなし備忘録

  • featureType 機械学習の特徴量を指定しています。先ほど述べたHaar-Like特徴を用いていました。

検証

 構築したモデルを使用してカワウソの顔が検出されるか実験してみました。

f:id:shoichitech:20180519170337j:plain
テスト画像


f:id:shoichitech:20180519200718j:plain
検証結果

 なかなか良さそうです。他の画像でも検証してみました。

f:id:shoichitech:20180520184920j:plain
f:id:shoichitech:20180520190008p:plain
f:id:shoichitech:20180519212107j:plain
いい感じに検出されてますね



f:id:shoichitech:20180519201304j:plain
友人の飼ってる猫。くろまるって言います。カワウソ検出器では検出されませんでした。



f:id:shoichitech:20180519201449j:plain
f:id:shoichitech:20180519212042j:plain
軍団カワウソ。うーん。やはり改善の余地がありそうです。


f:id:shoichitech:20180520184505j:plain
ラッコです。顔が似ているのか検出されました笑


検証の時に使用したスクリプトです。

import sys
import cv2 as cv

def detect(imagefilename, cascadefilename):
    srcimg = cv.imread(imagefilename)
    if srcimg is None:
        print('cannot load image')
        sys.exit(-1)
    dstimg = srcimg.copy()
    cascade = cv.CascadeClassifier(cascadefilename)
    if cascade.empty():
        print('cannnot load cascade file')
        sys.exit(-1)
    objects = cascade.detectMultiScale(srcimg, 1.1, 3)
    for (x, y, w, h) in objects:
        print(x, y, w, h)
        cv.rectangle(dstimg, (x, y), (x + w, y + h), (0, 0, 255), 2)
    return dstimg

if __name__ == '__main__':
    result = detect('otter1.jpg', './kawauso/cascade.xml')
    cv.imwrite('otter1_result.jpg', result)

感想

 教師データをもう少し用意できたらよかったです。
 機械学習は前処理が8割なんてもんじゃない。99%が前処理といっても過言じゃないと思いました。便利なライブラリやツールがある中で泥臭い作業こそ機械学習では必要なのだと体感しました。
 こんなやり方あるよというのがありましたらぜひ教えてください!!

Pythonで国会議事録から、話題の政治ワードを抽出してみた

初めまして、Aidemy研修生のぐっちーです。

 

今回はPythonの得意分野である

 ・自然言語処理(コンピュータに人間の言語を処理させる学問)

・スクレイピング(ネット上からデータを収集する行為)

 

を行い、ネット上の国会議事録データから会議内の頻出単語を抽出する

プログラミングを実装してみたいと思います。

 

 

(結果の一例↓) 

f:id:zerebom:20180509214302p:plain

 

Pythonを一通りかじったけど、何をすればいいかわからない…!

というかたの、足がかりになればなと思います!

 

研究背景

突然ですが、統計データは母集団により結果が大きく変わります。

以下の安倍内閣の支持率に対する世論調査をご覧ください。↓

 

日本テレビの世論調査

f:id:zerebom:20180506223953p:plain

 2018年4月の安倍内閣の支持率を26.7%としております。

 

 

国際ニュース通信社ロイターの調査

f:id:zerebom:20180506223956p:plain

こちらは2018年4月の安倍内閣の支持率を73%としております。

 

データを取った母集団は、以下の通り

日本テレビ→電話調査772人

ロイター社→大企業を中心に、企業223社

 

 

このようにデータの母集団が変わると、大きくデータに差が生まれてしまいます

 

 

人が取った統計だと、何が正しいかわからない。。。!

 ⇩

それならば自分でオリジナルデータを収集し、統計を取ろう!

 

  

ということで、今回のテーマを選びました。

実際にステップを踏んで検証していきましょう!

 

目次

 

実験

実験①国会での頻出単語を割り出し、話題の政治ワードを抽出する

 

実験①ではネット上の国会会議録データを収集し、

発言回数の多い順に単語を出力させ、話題を政治ワードを抽出することを目標とします。

 

 

指針は以下の通りです。

①国会会議録APIから予算委員会の安倍首相の発言を入手する

②形態素解析し、特徴語を抽出する

③出現数の多い語から現在の政治の話題を推定する

 

(API…webページの仕様書のようなもの、国会データを収集できるサイト)

(予算委員会…一年を通して開かれる議会。主に内閣のあり方について横断的に話している)

(形態素解析…日本語を最小単位に分割し、品詞を同定すること)

 

結果はこのようになります

f:id:zerebom:20180508130611p:plain

 

実験①-① スクレイピング

スクレイピングとはネット上からデータを収集することです。

今回はこちらを使います↓

国会会議録検索システム -国会会議録検索システム検索用APIについて-

使い方はリンク先に乗っているので、ざっくり説明します。

 

国会会議録検索システムには過去の議事録が、整理されて保存されおり、

プログラミング内でURLを指定すると、議事録データを返送してくれます。 

 

(検索条件を付与して、HTTPのGET情報を送信すると、

議事録が発言者・発言日などの情報を付与してXML形式で返してくれる。)

 

実際にスクレイピングしていきましょう。

import urllib
import untangle
import urllib.parse

if __name__ == '__main__':
    start='1'#発言の通し番号
    while start!=None:
        keyword = '安倍晋三'
        startdate='2017-01-01'
        enddate= '2018-01-01'
        meeting='予算委員会'
        #urllib.parse.quoteが日本語をコーディングしてくれる
        url = 'http://kokkai.ndl.go.jp/api/1.0/speech?'+urllib.parse.quote('startRecord='+ start
        + '&maximumRecords=100&speaker='+ keyword
        + '&nameOfMeeting='+ meeting
        + '&from=' + startdate
        + '&until='+ enddate)
        #Get信号のリクエストの検索結果(XML)
        obj = untangle.parse(url)

        for record in obj.data.records.record:
            speechrecord = record.recordData.speechRecord
            print(speechrecord.date.cdata,
                speechrecord.speech.cdata)

            file=open('abe_2017.txt','a')
            file.write(speechrecord.speech.cdata)
            file.close()
            #一度に100件しか帰ってこないので、開始位置を変更して繰り返しGET関数を送信
        start=obj.data.nextRecordPosition.cdata

 

 データは以下のような形式で返ってきます↓

http://kokkai.ndl.go.jp/api/1.0/speech?startRecord%3D1%26maximumRecords%3D5%26any%3D%E3%82%A2%E3%83%99%E3%83%8E%E3%83%9F%E3%82%AF%E3%82%B9%26speaker%3D%E5%AE%89%E5%80%8D%E6%99%8B%E4%B8%89

実験①-②形態素解析

次に取得したデータをテキストファイルとして保存し、形態素解析しましょう

形態素解析とは日本語を最小単位に分割し、品詞などを同定する行為です

 

f:id:zerebom:20180507103335p:plain

Wikipediaより

 今回はMeCabというソフトをpython内で読み込み、形態素解析します

# coding: utf-8
import MeCab
from collections import Counter
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import slothLib
import urllib3

fname = 'abe.txt'
fname_parsed = 'abe.txt.mecab'


def to_mecab():
    '''「fname」を形態素解析して[fname_parsed]に保存する
    '''

    with open(fname) as data_file, \
            open(fname_parsed, mode='w') as out_file:

        mecab = MeCab.Tagger('-d /var/lib/mecab/dic/mecab-ipadic-neologd')
        out_file.write(mecab.parse(data_file.read()))


def make_lines():
    '''
    各形態素を
    ・表層形(surface)
    ・基本形(base)
    ・品詞(pos)
    ・品詞細分類1(pos1)
    の4つをキーとする辞書に格納し、1文ずつ、この辞書のリストとして返す

    戻り値:
    1文の各形態素を辞書化したリスト
    '''
    with open(fname_parsed) as file_parsed:

        morphemes = []
        for line in file_parsed:

            # 表層形はtab区切り、それ以外は','区切りでバラす
            cols = line.split('\t')
            if(len(cols) < 2):
                raise StopIteration     # 区切りがなければ終了
            res_cols = cols[1].split(',')

            # 辞書作成、リストに追加
            morpheme = {
                'surface': cols[0],
                'base': res_cols[6],
                'pos': res_cols[0],
                'pos1': res_cols[1]
            }
            morphemes.append(morpheme)

            # 品詞細分類1が'句点'なら文の終わりと判定
            if res_cols[1] == '句点':
                yield morphemes
                morphemes = []

そして出力はこのようになります↓

{'base': 'ミサイル', 'surface': 'ミサイル', 'pos': '名詞', 'pos1': '一般'}
{'base': '攻撃', 'surface': '攻撃', 'pos': '名詞', 'pos1': 'サ変接続'}
{'base': '等', 'surface': '等', 'pos': '名詞', 'pos1': '接尾'}
{'base': 'の', 'surface': 'の', 'pos': '助詞', 'pos1': '連体化'}
{'base': '際', 'surface': '際', 'pos': '名詞', 'pos1': '非自立'}
{'base': 'の', 'surface': 'の', 'pos': '助詞', 'pos1': '連体化'}

 

今回実験で使うのは、以下の二つです。

 

surface:(文章に表れている形)

pos:品詞

 

実験①-③ストップワード作成(統計しない単語リスト)

そのまま解析したいところですが、このまま出現数順に語を表示してしまうと、

助詞の「の」「は」など、必要ない文字が出力されてしまいます。

 

そこで、形態素解析した後の表層形をみて、必要ない語を取り除きます

 

def sloth():
    import urllib3
    from bs4 import BeautifulSoup

    slothlib_path = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    http = urllib3.PoolManager()
    #↑urlib3系のおまじない
    slothlib_file =http.request('GET',slothlib_path)
    soup=BeautifulSoup(slothlib_file.data,'lxml')
    soup=str(soup).split()#soupは文字列じゃないので注意
    #SlothLibに存在しないストップワードを自分で追加↓
    mydict=['いる','内閣総理大臣','おり','ない','あり','ある','いく','なっ','する','あっ']
    soup.extend(mydict)
    return soup

 

有志の方が作った、ストップワード一覧があるので活用させていただきました。↓

http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt

ここに含まれていないワードは自分で追加し、リストを結合しました。

 

これに加え今回は3語以上、かつ名詞・動詞・形容詞のみを統計しました

 

実験①-④頻出単語をグラフに出力する

 

最後に頻度順に語数を並べて、出力しましょう。

出力にはmatplotlibライブラリを使用します。

# 形態素解析
to_mecab()
#ストップワードよみこみ
stop_word=slothLib.sloth()
# Counterオブジェクトに単語をセット
word_counter = Counter()
for line in make_lines():
    for morpheme in line:
        if morpheme['pos'] == '動詞' or morpheme['pos'] == '名詞' or morpheme['pos'] == '形容詞':
            if len(morpheme['surface'])>3:
                if not morpheme['surface'] in stop_word:
                #リストに入れないと、1文字づつカウントしてしまう
                    word_counter.update([morpheme['surface']])
# 頻度上位30語の取得
size = 30
#sizeの数だけ、上位の単語を表示する
list_word = word_counter.most_common(size)
print(list_word)

# 単語(x軸用)と出現数(y軸用)のリストに分解
list_zipped = list(zip(*list_word))
words = list_zipped[0]
counts = list_zipped[1]

# グラフで使うフォント情報(デフォルトのままでは日本語が表示できない)
fp = FontProperties(
    fname='/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf'
)

# 棒グラフのデータ指定
plt.bar(
    range(0, size),     # x軸の値(0,1,2...9)
    counts,             # それに対応するy軸の値
    align='center'      # x軸における棒グラフの表示位置
)

# x軸のラベルの指定
plt.xticks(
    range(0, size),     # x軸の値(0,1,2...
    words,              # それに対応するラベル
    fontproperties=fp   # 使うフォント情報
)

# x軸の値の範囲の調整
plt.xlim(
    xmin=-1, xmax=size  # -1〜10(左右に1の余裕を持たせて見栄え良く)
)

# グラフのタイトル、ラベル指定
plt.title(
    '37. 頻度上位30語',    # タイトル
    fontproperties=fp   # 使うフォント情報
)
plt.xlabel(
    '出現頻度が高い30語',# x軸ラベル
    fontproperties=fp   # 使うフォント情報
)
plt.ylabel(
    '出現頻度',         # y軸ラベル
    fontproperties=fp   # 使うフォント情報
)

# グリッドを表示
plt.grid(axis='y')

# 表示
plt.show()

出力は以下のようになります!

f:id:zerebom:20180507123513p:plain

[('安倍晋三', 1144), ('申し上げ', 782), ('いただい', 654), ('いただき', 534), ('取り組ん', 225), ('トランプ大統領', 170), ('おっしゃっ', 153), ('安倍政権', 142), ('会計検査院', 112), ('国際社会', 109),…以下略]

 

かなり字が小さいですが…こんな感じです!

 

「加計学園」や「会計検査院」、「国家戦略特区」など、

今年の話題が選定出来ているのがわかりますね!

 

 

実験② TF-IDFの実装

実験①では一年間全体での各単語の重要度はわかりますが、

この月は特にこの話題を話し合った!などという情報はわかりません。

 

 

そこで、今回はどの時期にどの話題が持ち上がったかを加味する

TF-IDF計算を実装してより詳しく分析していきます!

 

実験②-①TF-IDF計算とは?

TF-IDF計算とは、いくつかの文書があった時の文書中の単語の重要度を調べる手法

のひとつです

 

 

ここでは軽く説明しますが、詳しく知りたい方は以下のサイトを参照してください↓

TF-IDFで文書内の単語の重み付け | takuti.me

 

TF…TermFrequency

それぞれの単語の文書内での頻出頻度を表す

 

 

IDF...Inverse Document Frequency

それぞれの単語がいくつの文書で共通して使われているか表す

 

f:id:zerebom:20180509110306p:plain

式で表すとこのようになり、TF-IDFはTF×IDFの値で表されます。

 

つまり、

沢山出てくる単語ほど重要(TF)という値と

出現文書数が少ない単語ほど重要(IDF)という値をかけ合わせています。

 

例えば、1年間の議事録では

トランプ大統領という単語が1年を通じてたくさん登場し、

加計学園という単語は年の終わりに集中して登場したとします。

 

この場合

トランプ大統領→(DF:高、IDF:低)

加計学園→(DF:高、IDF:高)

となるので年の終わりの文書では加計学園という単語がより重く、特徴づけられます。

 

 

 

実験②-②sklearnのTfidfTransformerクラスを使用

 TF-IDFの仕組みは平易なため、自分で実装することも可能ですが、

今回は別の検証にも応用できるように、sklearnのTfidfTransformerというメソッドを使って実装しました

 

 

------------------------------------------------------ 実装-----------------------------------------------

①前処理した安倍総理の発言を3つの文書に分け、リストに入れる。

a.2017-01-01~2017-04-01

b.2017-04-01~2017-08-01

c.2017-08-01~2017-12-01

 

そして、文書を形態素解析し、CounterVectorizer(後述)に入力できる形に成型する。

 

今回は3文字以上の名詞のみを選定しました。

to_mecab('/home/share/idf/abe_1.txt','/home/share/idf/abe_1.txt.mecab')
copus=[]
word_counter = Counter()
for line in make_lines('/home/share/idf/abe_1.txt.mecab'):
    for morpheme in line:
        if morpheme['pos'] == '名詞':
            if len(morpheme['surface'])>2:
                if not morpheme['surface'] in stop_word:
                    word_counter.update([morpheme['surface']])
                    copus.append(morpheme['surface'])
                    copus.append(' ')

#ストップワードを除く3文字以上の名詞が全て格納される
#joinにより、一つなぎの文字列になる a=''.join(copus)

 

②CounterVectorizerクラスを用いて単語の出現回数を計測し、各文書を特徴ベクトルに変換する。

CounterVectorizerは、sklearnに用意されているクラスで、

文書中の単語の出現回数を数え、その値をもって文書をベクトル化します。

実装は以下の通りです↓

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer

#カウンターを定義
count=CountVectorizer()
#各文書をarray型に代入する
docs=np.array([a,b,c])

bag=count.fit_transform(docs)

#各単語の出現回数を表示
print(count.vocabulary_)

#各単語の出現回数を、疎な特徴ベクトルとして出力
print(bag.toarray())

 

出力は以下のようになります

{'thaad': 4225, '教育再生実行会議': 2721, 'テーマ': 637, '機密情報': 3030, '保育士': 1482, '君たち': 1950, 'あずかり': 3, '締めくくり': 3539, 'スター': 546, 'ノドンミサイル': 702, '北東アジア': 1758, '長時間労働': 3965, '二十三年': 1298, '輸出企業': 3841, 'クリーン': 425, '在宅医療': 2123, '獣医師会': 3212, '不規則': 1179, '昨年末': 2872, 'グループ': 434, '知ったか': 3342, '手持ち': 2635, '相対的貧困率': 3323, '東北地方': 2968, '一万三千人': 1036, 'ポスト': 879, '新潟県': 2766, 'アフガン': 271, '五年間': 1374, '佐々木': 1465, '三十万円': 1118, '労働力人口': 1719, 'シークレットサービス': 523, 'パチンコ': 746
[[  1   1   1 ... 302  33  13]
 [  0   0   0 ...   0   0   0]
 [  0   0   0 ...  13   0   0]]

 

③TfidfTransformerを使い、TF-IDF計算を実装する。

②でベクトル化された文書に計算を加えて、TF-IDFの式に従って値を変更します。

from sklearn.feature_extraction.text import TfidfTransformer

#tfidfを実装する
'''
use_idf...Falseにすると、dfのみで出力する
norm...正則化の手法。ベクトルの長さが1になる
smooth_idf…idfの計算式が変わる。Faluseにするとlogの後ろの+1がなくなる

'''
tfidf=TfidfTransformer(use_idf=True,norm='l2',smooth_idf=True)

#arrayの出力を成型する(小数点第2位まで)
np.set_printoptions(precision=2)

#tfdifでdocsを変換したのち、array型に変換し出力する
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())

 

出力は以下のようになります。

 

[[0. 0. 0. ... 0.17 0.02 0.01]
[0. 0. 0. ... 0. 0. 0. ]
[0. 0. 0. ... 0.06 0. 0. ]]

 

ぱっと見て、正則化のおかげでベクトルの長さが1になっていることがわかりますね!

 

実験②-③Excelに出力し、データを読み解く

最後にこのベクトルから、どの単語が各文書で重要か調べてみましょう!

今回はExcelに出力したいと思います。

from sklearn.feature_extraction.text import TfidfTransformer
tfidf=TfidfTransformer(use_idf=True,norm='l2',smooth_idf=True)
np.set_printoptions(precision=2)

features = count.get_feature_names()

ans=tfidf.fit_transform(count.fit_transform(docs))
#出力をarray型にする
ans_array=tfidf.fit_transform(count.fit_transform(docs)).toarray()

data={
#単語名
"index":features,
#各文書の各単語のTF-IDF値
"num1":ans_array[0],
"num2":ans_array[1],
"num3":ans_array[2]
}
df=pd.DataFrame(data)
df.to_excel('/home/share/idf/sample.xlsx', sheet_name='abe_2017')

出力はこちら↓

以下は文書1(2017-01-01~2017-04-01)でソートしてあります。

f:id:zerebom:20180509142246p:plain

文書1を特徴づける単語は「tpp」「北朝鮮」「トランプ大統領」などになります!

しかしこれは、実験①とは異なり、単なる出現回数順ではありません。

 

例えば、行番号24番と、25番を見てみましょう。

 「労働時間」という単語は文書1のみ出現していますが、

「明らか」はすべての文書で出現しています。

 

つまり「労働時間」のIDFは「明らか」より、

大きくなり出現回数のわりにTF-IDF値は大きくなります。

 

 

実際に調べてみると、

「労働時間」の出現回数は124回

「明らか」の出現回数は210回となっていました。

 

しっかりと出現文書数が少ないほうがより重みづけされる機能が実装されていますね!

 

( ※文書1の出現単語数が多いのは、2月に予算委員会は多く開かれるためです。

文書1はほかに比べて分量が多いです)

考察

TF-IDF計算は文書の数が多い時に発揮されます。

今回の実験では3文書しかないため、すべての文書で出現する文字が多くなり、

IDFの値に差があまりつきません。

 

文書の数を12個に分け、月別で調べてみても面白いかもしれないですね!

(予算委員会は月ごとの開催回数はかなり違いますが)

 

今回の実験を発展させる方法

 

今回は安倍総理の発言のみをスクレイピングしましたが、

議員毎にスクレイピングし、何に主眼を置いて話しているか等を調査する事や、

議員毎のクラスタリングをしても面白いですね。

 

また、今回の実験②では文章をベクトル化し、数値で表すことが出来ました。

ベクトル化をすると、文章に対して数学的アプローチをかけることができるようになるので、クラスタリング等より高度な技術が可能になります。

 

 機会があれば今回の実験の続編も行いたいなと思います!

感想 

 

 今回は自然言語処理・スクレイピングがテーマでしたが、

この実験を行うにあたっては、テーマ以外にも

  • ローカル環境の構築構築
  • HTTP通信(xmlや文字コードの扱い方)
  • MeCabの辞書作成
  • sklearnのパラメータの理解、渡すデータ型

 

などで非常に苦戦しました汗

 

しかし、苦戦する中で自分でエラーを解決する力や、横断的な知識を身に着けることができ他と思います。

是非、ProgateやAidemyでの学習が一通り終わった方は自分でテーマを設定し、

アウトプットする機会を設けて挑戦してみてください!

環境紹介

今回私が使用した環境はこちらです⇩

・windows10

・VMwareWorkstation14(仮想OSを起動するソフト)

・ubuntu 16.04 32bit(Linuxディストリビューションの一つ)

 

・python3.6.5

・MeCab (形態素解析ソフト)

・Atom(テキストエディタ)

 

参考文献

・冒頭の世論調査↓

jp.reuters.com

 

日本テレビ世論調査

 

・Linuxの導入方法↓

koeda3.hatenablog.com

・MeCabのダウンロード方法↓

 

qiita.com

 

・Atomのダウンロード方法↓

Python入門 初心者でも出来るAtomエディタでの開発方法

 

・ストップワードの実装↓

testpy.hatenablog.com

 

自然言語処理の基礎知識まとめ↓

qiita.com

機械学習プログラミングのまとめ(sklearnの使い方等)

book.impress.co.jp

大公開!初心者でもわかるAidemy社内インフラの全容とその設計思想

インフラって初学者にとってかなりブラックボックスですよね。

インフラ構成なんて会社の業態やサービス形態によって様々なので、初心者にとってどれが重要なのかわかりにくい(そもそもあんまり公開されてないし)。また、調べてみても各サービスの個別的な情報ばかりで、なかなか社内インフラの設計思想のような体系的な情報はでてきません。なにより、何から理解すればいいのかわからないので地道に勉強するのがめんどくさい!

そこで、先日Microsoft社で行われたハックフェストに参加し、マンツーマンでインフラ構成について教えていただきました!

ハックフェストで教わったことを踏まえ、Aidemyのインフラ構成を例に、重視しているポイントや、用いているサービス・ツールについて、インフラに触れた経験のない人でもわかるように解説していきたいと思います。

f:id:aoi_tsuno:20180428194732j:plain

Microsoftオフィスでのハックフェストの様子

Aidemyのインフラ構成設計思想

Aidemyのインフラ設計では以下の3点を重視しています。

  1. コンテンツの配信速度を爆速にしたい!
  2. アプリのアクセスに応じて柔軟にスケーリングしたい!
  3. 安全にバッチ処理、WebHook受信をしたい!

それぞれの要件を実現する方法について、1つずつざっくり解説していきます。

f:id:aoi_tsuno:20180502101240p:plain

Aidemyインフラの概観図(以下、模式図には便宜上Microsoft Azureによって提供されているサービスのロゴを用いていますが、他クラウドサービスでも同様のプロダクトが利用可能な場合があります)

1.「コンテンツの配信速度を爆速にしたい!」を実現する"CDN"(Content Delivery Network)

f:id:aoi_tsuno:20180502101317p:plain

CDNでは、世界各地に配置された全てのキャッシュサーバーに、IPアドレスに対応した静的コンテンツが保存される仕組みになっています。

f:id:aoi_tsuno:20180430183124p:plain

コンテンツデリバリネットワーク - Wikipedia

エンドユーザは、全てのキャッシュサーバーの中でもっとも近くにある(HOP数の少なくて済む)サーバーに保存されたコンテンツをダウンロードするため、非常にスピードの速いアクセスが可能となります。

また、複数台のキャッシュサーバーにより構成されているため、トラフィック分散や、単一のネットワークの不具合による遅延の軽減などのメリットもあります。

2.「アプリのアクセスに応じて柔軟にスケーリングしたい!」を実現する"Container Registry" + "Kubernetes"

f:id:aoi_tsuno:20180502101353p:plain

一般にWebサイトのアクセス頻度は、時期や時間帯によって大きく変化します。さらに、Aidemyでは機械学習のオンライン学習サービスを提供しているため、講座内でニューラルネットワーク等を用いたCPU負荷の大きいソースコードを実行する場面が多々あります。

かといってスペックの高いVM(仮想マシン)を何台も常時保有しているのはかなりの高額のコストがかかってしまいスタートアップにとって致命的です。したがって、ユーザのアクセス量に応じて随時マシン数を増減させる必要が生じます。

そこで、Container RegistryとKubernetesを組み合わせることにより、高速で柔軟な実行環境の展開と、そのデプロイの簡略化を図ることができます。

Container Registry

f:id:aoi_tsuno:20180502101429p:plain

Container Registryとは、Microsoft Azureなどにより提供されているDocker*1レジストリで、ローカルからアップロードされたDockerイメージ(ここでは、実行環境やソースコードをひとまとめにしたファイルと考えてもらえれば十分です!)を、サービスのクラウドストレージに保存することができます。保存されたイメージは権限で守られており、Virtual Network内の至るところからアクセスすることができます。次はKubernetesでこのContainer Registry上のイメージをスケーリングしていきます!

Kubenetes

f:id:aoi_tsuno:20180502121047p:plain*2

Kubernetesは「クーベネティス」と読み、一般に"k8s"と略されているようです。Kubernetesとは下図のようにある一つのノード(マスターノード)に指令を与えるだけで、複数の実行環境を高速に展開し、運用に必要な処理を自動化することができるオープンソースツールです。(図中の"RUN"とは、Aidemyの講座でソースコードを実行する環境のことをさしています。)

f:id:aoi_tsuno:20180428174827p:plain

Docker(コンテナ型仮想化)と Kubernetes についての簡単な紹介 – ゆびてく

KubernetesとContainer Registryを組み合わせることにより、マスターノードへの指令でクラウド上のDockerイメージを指定することで同一環境のコンテナをいくつも立ち上げることが可能になります!

Kubernetesを利用するメリットとしては、

  • 複数台のマシンを稼動させていても、1台だけを操作するように扱える
  • 実行しているコンテナの負荷や不具合に応じて、自動で新たなホストを立ち上げたり、削除したりすることができる(スケールアウト/ダウン)
  • アプリ稼働中、セッションを維持しながらバージョンアップすることができる(ロールアウト)

が挙げられます。

マスターノードに与える指令はyaml形式で記述されており、スケールアウト/ダウンの基準となるCPU負荷率なども簡単に指定可能です。

 さらに、Kubernetesを用いれば、Container Registryに保存したサービス(webフロント、バックエンドAPI)のコンテナイメージをまとめて一気にデプロイすることができるので、本番環境の運用で起こりうる人的ミスを最小限に抑えることもできます!

ubiteku.oinker.me

 

3.「安全にバッチ処理、WebHook受信をしたい!」を実現する"Azure Function App"

f:id:aoi_tsuno:20180502101545p:plain

特定の相手からのリクエスト受信や、定期的な処理を行いたいけど、そのために常時インスタンスを稼働させておくのはコストがかかってしまう…。そんな場合に手軽に活用できるのがFunction Appです!

Function Appはクラウドサービス上のエディタにベタ書きしたステートレスな関数を実行することのできるアプリケーションプラットフォームです。webhookにも対応可能で、フレキシブルに実行のスケジューリングが設定できるうえ、従量課金制なので、安心してサードパーティからのリクエスト(Aidemyではクレジットカード引き落とし情報など)を受け付けることができます。

例えば、名前入りのリクエストを受けて挨拶を返してくれる簡単な関数を次のようにNode.jsで実装してみます。

module.exports = (context, req) => {
  context.res = {
    status: 200,
    body: {
      message: 'Hello, ' + req.query.name + '!',
    }
  };
  context.done();
}

ソースコードをそのまま貼り付けるだけで、(画像はMicrosoft Azure Function Appにおける運用例)

f:id:aoi_tsuno:20180430232317p:plain

作った関数がどこからでも使えるようになります!(ここではRESTクライアントソフトのPostman*3を利用しています

f:id:aoi_tsuno:20180501203006p:plain

さらに、このように鍵や接続文字列をアプリケーション内で環境変数として指定することもできるので、安心して秘匿情報を利用することができます。

f:id:aoi_tsuno:20180430233342p:plain

サードパーティへのアクセス権をコードにもたせたいけど、秘密鍵情報はセキュリティ上ソースコード内に書きたくない…という場合にもってこいです!

このように、Function Appを利用すれば、安全に、そして安価にバッチ処理やサードパーティとの連携をおこなうことができます。

まとめ

ハックフェストでは、コンテナ化の概念や公開鍵認証など初歩的なことから教えていただけるので、専門的な相談はもちろん、「インフラに興味があるけど何から学べばいいかわからない…」という方にも絶好の機会だと思います!

 

Microsoft Japanの安納さん、窪田さんありがとうございました!(真ん中が安納さん)

f:id:aoi_tsuno:20180502225821j:plain

 

*1:コンテナ技術による軽量な仮想化プラットフォーム。

knowledge.sakura.ad.jp

ドットインストールのDocker入門もおすすめです。

https://dotinstall.com/lessons/basic_docker

*2:図式化のため、上図では複数のKubernetesクラスターが分散しているような形式をとっていますが、正確にはKubernetesは下図のように各アプリケーションを一つのクラスタ内で管理しています。

f:id:aoi_tsuno:20180427200605p:plain

Using kubectl to Create a Deployment | Kubernetes

*3:

www.getpostman.com

Twitterのタイムラインからツイートを取得しトピックモデルを生成してみる

はじめまして、研修生ののっぽです。機械学習の勉強を始めてまだ二ヶ月の未熟者ですが、今回は機械学習を用いた簡単なプログラムの実装をしてみようと思います。


突然ですが、皆さんはTwitterを使用したことがありますか?あるいは他のSNSを用いたことはありますか?
Twitter等のSNSでは大きなアカウントであればあるほどログの流れが早く、後から見返すのが大変です。
そこで、pythonを用いて過去のツイートを大量に取得し過去にどのようなことがツイートされていたのかを大まかに知るプログラムを作ってみます。

 

トピックモデルとは

さて、タイトルにもあるトピックモデルとは一体どういう意味なのでしょうか。
トピックモデルとは簡単に言うと「全ての文章には幾つかの話題があり、文章の中身はその話題の何れかから作られている」というモデルのことです。そしてトピックモデルを生成するとは、各文章中の単語を生成元の話題に対してグループに分け、それらの単語がどれだけの確率でどの話題から生成されたのかを推定することになります。
詳しい解説はAidemyの講座にお任せすることとして、早速ツイートを取得してトピックモデルを生成してみましょう。

ツイートを取得する

ツイートの取得に当たって以下の記事を参考にさせていただきました。
qiita.com

TwitterにはAPI制限というものが存在し、タイムラインからのツイートの取得は200(ツイート/回)×15(回) = 3000ツイートが限界です。そのため、API制限に掛かった場合はプログラムを一時的に停止させる必要が有ります。以下に示したプログラムでは一回ツイートを取得するごとに1分停止することで結果的に制限にかからないようにしています。
また、ツイートを遡るために最後に取得した最も古いツイートのIDを一時的に保持しています。

source = []
for j in range(100):   
    params_a = {"since_id" : str(id1),"count":200, "include_entities":"false", "include_rts":"true", "trim_user":"true", "exclude_replies":"true"} 
    req = twitter.get(url, params = params_a)
    if req.status_code == 200:
        timeline = json.loads(req.text)
        i = 0
        print(str(j)) 
        for tweet in timeline:
            source.append(tweet["text"])
            i = i + 1
            if(i == 200):
                id1 = tweet["id"]-200
    else:
        print("error")        
'''
この部分に追加でアカウントを加える
'''
 time.sleep(60)

(上記のコードはプログラムの一部です。実際にはツイート取得数を増やすために、複数アカウントのタイムラインからツイートを取得しています)

以上のプログラムで20000×アカウント数のツイートが取得できることになります。
しかし、そのままではトピックモデルを形成することはできません。そこで得られたツイート群に対して形態素解析を行い必要な単語のみを抽出します。

ツイートを整形する

ツイートの整形(形態素解析)にはMeCabというライブラリを用います。
以下のコードではparserという関数で一つのツイートの形態素解析を、analyzerという関数でリスト内のツイート全てにparser関数をかけています。

import MeCab
mecab = MeCab.Tagger('mecabrc')
mecab.parse('')

#形態素解析を行う
def parser(text):
    mecab.parse('')
    corplist = []
    node = mecab.parseToNode(text)
    while node:
        if(len(node.surface) == 0):
            node = node.next
            continue       
        if (node.feature.split(",")[0] == u'名詞') and ((node.surface[len(node.surface) - 1].encode("utf-8")[0] >= 0xe3) and (node.surface[len(node.surface) - 1].encode("utf-8")[1] >= 0x81) and (node.surface[len(node.surface) - 1].encode("utf-8")[2] >= 0x80)):
            corplist.append(node.surface)
        node = node.next
    return corplist

#データに直接parserをかける
def analyzer(content):
    token = []
    for i in content:
        token_p = parser(i)
        token.append(token_p)
    return token

words = analyzer(source) #単語リストとして保存

parser関数では形態素解析で得られた単語群の中から条件に合うもの(名詞に分類され、単語の初めの文字が英語でないもの)のみを実際にトピックモデルの生成に使うリストに加えています。

これでトピックモデルを生成する準備は整いました。


トピックモデルの生成

さて、後は得られた単語リストからトピックモデルを生成するだけです。トピックモデルの生成にはgensimと呼ばれるライブラリを使います。
トピックモデルを生成するためにはまず辞書と呼ばれる意味のある頻出単語群の作成と、コーパスという辞書中の各単語にそれらがどれだけの頻度で出現したのかを紐付けるリストの作成が必要になります。
以下のコード中のdictionary.filter_extremesでは上で説明した辞書に単語を加える際の条件を付加しています。今回の例で言えば、文章群全体で二回以上出現した単語でありかつ全体の1%以下の出現率である単語が辞書に加えられます。

作成したコーパスをLDAという種類のトピックモデルを生成する関数に入れてあげることで、指定したトピック数に対して単語群からトピックそのものとトピックに各単語がどれだけの割合で含まれているかを推定することができます(この節の後半でLDAについて説明をします)。

import gensim
from gensim import corpora, models, similarities

#辞書の作成
dictionary = corpora.Dictionary(words)
dictionary.filter_extremes(no_below=2, no_above=0.01)

# コーパスを作成
corpus = [dictionary.doc2bow(text) for text in words]

#トピックモデルを生成
lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=100)

# 各トピックの出現頻度上位を取得
topic_top = []
for topic in lda.show_topics(-1, formatted=False):
    topic_top.append([dictionary[int(tag[0])] for tag in topic[1]])
        
# 各トピックの出現頻度上位10位をcsv形式で保存
topic_data = pandas.DataFrame(topic_top)
topic_data.to_csv("topic_words1.csv", encoding="utf-8")

さて、それでは生成されたトピックモデルをcsvファイルに保存し確認してみましょう。

LDAとは

結果に移動する前にLDAについて簡単な説明をします。
LDAとはトピックモデルの中でもベイズ推論の考えを用いて文章の集合から各トピックや単語がどのから生成されたかについての分布を計算するモデルです。
以下にすこし詳しい説明を書きます。
n個の単語の集合をW、語彙(つまり単語の種類)の集合をV、文書集合をDとします。
この時、文書集合D中のある文書dは単語wの集合として表されます。また文書集合DにはK個のトピックφによって構成されるトピック集合Φが存在しトピックφは語彙vの異なる比率によって構成されています。
また文書dそのものはトピックを幾つか含んでいるのでトピックφが文書dを占める比率をθとおきます。
最後に単語がどのトピックに属するかを示す変数であるトピック割り当てZを設定します。
トピックの総数が決まっている時、単語w及びトピック割り当てはカテゴリ分布(ベルヌーイ分布を多次元に拡張したもの)によって生成されます。
{ \displaystyle p(w_{d,n}|z_{d,n,}\Phi) = \prod_{k=1}^{N-1} Cat(w_{d,n}|\phi_k)^{z_{d,n,k}}}
{ \displaystyle p(z_{d,n}|\theta_d) = Cat(z_{d,n}|\theta_d)}
つまり、単語wの生成される確率はトピック集合Φとそれらの割り当てzに依存し、割り当てzの生成される確率は文書dに含まれるトピックの比率θに依存するということです。
この時、トピック比率及びトピックφがそれぞれディリクレ分布から生成されていると仮定すると、結局全体の同時分布は以下の式で表されます。
{\displaystyle p(D,Z,\Phi,\Theta) = p(D|Z,\Phi)p(Z|\Theta)p(\Phi)p(\Theta)}
(ただし、上式において集合の確率は各要素の確率の積で表されるとします)
今求めたいのはp(D,Z,Φ,Θ)に対してp(D)が与えられた際の事後分布
{\displaystyle p(Z,\Phi,\Theta|D) = \frac{p(D,Z,\Phi,\Theta)}{p(D)}}
です。ここで変分推論を適用し先ほど求めた式を用い、潜在変数Zと他のパラメータ分解を仮定することでLDAにおける解析的な更新式が求まります。
{\displaystyle p(Z,\Phi,\Theta|D) \approx q(Z)q(\Theta, \Phi)}
この更新式はΘ及びΦのディリクレ分布のハイパーパラメータα及びβから求まります。
{\displaystyle < ln \phi_{k,v} > = \psi(\hat{\beta_{k,v}}) - \psi(\sum_{v'=1}^{V}\hat{\beta_{k,v'}})}
{\displaystyle < ln \theta_{d,k} > = \psi(\hat{\alpha_{d,k}}) - \psi(\sum_{k'=1}^{K}\hat{\alpha_{d,k'}})}
{\displaystyle < z_{d,n,k} > = \eta_{d,n,k}}
~ただし、変数はそれぞれ以下のように設定しました~
{\displaystyle \hat{\beta_{k,v}} = \sum_{d=1}^{D}\sum_{n=1}^{N} < z_{d,n,k}w_{d,n,k} > + \beta_v}
{\displaystyle \hat{\alpha_{d,k}} = \sum_{n=1}^{N} < z_{d,n,k} > + \alpha_k}
{\displaystyle \eta_{d,n,k} \approx exp({\sum_{v=1}^{V} w_{d,n,v} < ln\phi_{k,v} > + < ln\theta_{d,k} >})} ただし{\displaystyle (\sum_{k=1}^{K}\eta_{d,n,k} = 1)}
~
これらの更新式において崩壊型ギブスサンプリングというpからパラメータΘ,Φを周辺化除去しzの条件付き分布を求めるサンプリング方法によってαとβの更新が可能になります。

LDAは確率モデルであり拡張性が高いため、広く使われています。

結果

以下の画像は作成したTwitterの4つのサンプルアカウント(フォローに法則性のないアカウント,アニメ・ドラマ・ゲーム等の内容をつぶやくユーザーの多いアカウント,数学・物理・情報等の学問についてつぶやくユーザーが多いアカウント,僕自身のアカウント)から生成されたトピックを30個分取り出したものになります。

...可もなく不可もなくといったところでしょうか
f:id:noppo_eeic:20180422034431p:plain


これらのトピックのようにうまく単語をまとめられたものもありますが、
f:id:noppo_eeic:20180422034251p:plainf:id:noppo_eeic:20180422034319p:plainf:id:noppo_eeic:20180422034345p:plain

中にはとんちんかんなトピックもあります。
Twitterでは多くの著者と多くの話題が混在しているためうまくモデルを推定するのが難しいのかもしれません。


そこでモデルのパラメータであるトピック数と辞書の作成に用いたパラメータである単語の最低出現数(no_below)についてパラメータの適解を探索します。

モデルの評価に用いるのはperplexityと言う指標でトピックからある単語が生成される確率の逆数によって表されます。つまり、この数値が小さければ小さいほどモデルの予測性能は向上するのでうまく構築出来たと言うことになります。

はじめにトピック数及びに単語の最低出現数ついて大まかに探索することで大体のperplexityの傾向を探ったところ、今回はトピック数はだいたい200程度、最低出現数はだいたい18程度で極小値に収束することがわかりました。
そこでさらに細かく全探索していきます。

dictlist = []
for i in range(10):
    dictionary = corpora.Dictionary(words)
    dictionary.filter_extremes(no_below=i + 15, no_above=0.01)
    dictlist.append(dictionary)
    print(i)
corplist = []
for i in range(10):
    print(i)
    corpus = [dictlist[i].doc2bow(text) for text in words]
    corplist.append(corpus)

for k in range(10):
    for i in range(10):
        lda = gensim.models.ldamodel.LdaModel(corpus=corplist[i], num_topics=k + 215)
        perplist.append(np.exp2(-lda.log_perplexity(corpus)))
        print(np.exp2(-lda.log_perplexity(corpus)))
        print("i:",i)
        print("k:",k)

全探索をしたところ、トピック数219, 最低単語出現数19でperplexityの極小値25.48が得られました。
このパラメータを用いてもう一度トピックモデルを構築したところ以下のようになりました。
f:id:noppo_eeic:20180424051430p:plain

ちょっとは改善されているのでしょうか...?

新しく取得したツイートのトピック

では、取得したツイートのトピックモデルを用いて新たに取得したツイートがどのトピックに属しているのかを確認してみましょう。
先ほど用いたアニメ、ドラマ等のアカウントをフォローしているサンプルアカウントのタイムラインから以下のツイートが得られました。

[映画ニュース] 松坂桃李、役所広司の“魂”が込められた「孤狼の血」キーアイテムを継承!

この時、以下のコードによって新しく取得したツイート内のどのような単語が既存のトピックに反応するのか確認してみると、

print(test_words[1][2])
for i in range(len(test_words)):
    print("tweetnumber:", i)
    for j in range(len(test_words[i])):
        try:
            print(test_words[i][j], "id is", dictionary.token2id[test_words[i][j]])
        except KeyError:
            print(test_words[i][j], "is not found")
松坂桃李 id is 448
役所広司 id is 447

と、俳優の名前を含むトピックが既に存在することがわかり、

c = [(447,1), (448, 1)]
for (tpc, prob) in lda.get_document_topics(c):
    print(str(tpc) + ': '+str(prob))

146: 0.668155

と、146番目のトピックが約67%の確率で属することがわかりました。146番目のトピックは以下のようになっており

f:id:noppo_eeic:20180425211340p:plain

例えば俳優の名前なら「江口洋介」と言った単語がのちに続く可能性があることがわかります。

元ツイートを確認してみると

「江口洋介」さんが関わっていることがわかりますね。

最後に

今回はpythonを用いて身近な題材について機械学習のプログラムを実装してみました。機械学習というとハードルが高く見えるかもしれませんが、ライブラリが充実しているので簡単なものであれば誰にでも作れてしまうものです。
とはいえ、精度や学習速度など奥が深い分野であることもまた事実です。僕もトピック数の決定や評価法などまだまだ改良点があるのでまた挑戦してみようと思います。
ご清覧ありがとうございました


~今回作成したプログラムはこちらに置いておきます~
github.com

~またLDAの説明に関して以下の本を参考にさせていただきました。より詳しく機械学習について知りたい方にとてもおすすめの本です。

機械学習スタートアップシリーズ ベイズ推論による機械学習入門 (KS情報科学専門書)

機械学習スタートアップシリーズ ベイズ推論による機械学習入門 (KS情報科学専門書)

独立成分分析による音源分離

はじめまして、protonです。
数カ月前からやっと機械学習関係の勉強を始めましたが、思った以上に色々出来て面白くなってきたところです。
機械学習にはscikit-learn等の様々な便利なオープンソースライブラリがあり、それらを用いることでかなり簡単に実装することができます。
今回はあえて、scikit-learn等を使わずに独立成分分析(ICA)というものを実装し、音声データの分離をしてみました。

独立成分分析(ICA)とは?

様々な人が話している中でも、自分が話している相手の会話は聞き取ることができるという現象は(おそらく)誰でも経験していると思います。
この現象をカクテルパーティ効果といい、人には音源の位置や周波数の差から特定の音抽出するような機能が備わっています。
この機構を模したアルゴリズムが、独立成分分析(ICA : Independent Component Analysis)です。

独立成分分析のアルゴリズム

f:id:proton_1602:20180407000041p:plainf:id:proton_1602:20180407000133p:plain


これはある2つの観測データ(2つの音源が別々の割合で混合されたもの)をそれぞれ横軸と縦軸に設定した散布図で、ICAが分離した様子がわかります。(ICA前にデータの分散が大きい方向がなんとなくX字のように2つあるものを、直行するように座標変換してるイメージ。もっとわかりやすいデータを持ってきても良かったのですが、このぐらいでも分離することができることがわかってもらえると...)
これをどのようにしてプログラムで行うかというと、任意の独立な確率変数の和は正規分布に収束するという中心極限定理を利用します。
中心極限定理により、独立な音源よりもそれを足し合わせたデータのほうがより正規分布に近くなる場合が多く、逆に正規分布から離れた分布であれば、独立な音源が足し合わさっていない(と判断できる)ということになります。
どれだけ正規分布に近いかどうかを判断するのに、尖度という統計量があります。尖度は正規分布の時に0、正規分布から乖離するほど0から離れていくので、この尖度が最大になった時が観測データ同士の独立性が最も高くなる時で、元の音源を分離することができます。(ここからわかるように元のデータが正規分布に近いものだと、分離が難しくなります...)

尖度が指標としてなぜ有効なのかや、尖度を最大化させるために繰り返し更新するアルゴリズムについては最後にリンクを貼った本を読んでみてください...

準備

音源分離には様々なパターンがあるらしいですが、基本的には独立した音源の数だけそれぞれ同期したマイクを周囲に適当に設置して、マイクから拾った音を元にそれぞれの音源を抽出します。
実際にマイクをいくつか買ってきて録音するのは音源とは関係ない雑音等も入り、勉強始めたての自分には難しすぎるので、今回はフリー音源を3つ適当な割合で混合したデータから音源を分離することにします。(マイクの位置による時間差は無視して、マイクの位置による音の減衰による差を反映しています。)
扱いやすくするために、soundengine等を使ってwav形式16bit, 44100Hz, モノラルでちょうど10秒の音声データに加工します。

loop1.wav

strings.wav

fanfare.wav

フリーのBGMとゲーム用音楽素材[Wave,MP3]

次に、pythonを使って3つの音源を適当な割合で混合したデータを3つ作ります。
(コードの通りmix_1,2,3はそれぞれloop1.wav, strings.wav, fanfare.wavを(0.6, 0.3, 0.1), (0.3, 0.2, 0.5), (0.1, 0.5, 0.4)の割合で混合したものです)

import numpy as np
import scipy.io.wavfile as wf

rate1, data1 = wf.read('loop1.wav')
rate2, data2 = wf.read('strings.wav')
rate3, data3 = wf.read('fanfare.wav')
if rate1 != rate2 or rate2 != rate3:
    raise ValueError('Sampling_rate_Error')

mix_1 = data1 * 0.6 + data2 * 0.3 + data3 * 0.1
mix_2 = data1 * 0.3 + data2 * 0.2 + data3 * 0.5
mix_3 = data1 * 0.1 + data2 * 0.5 + data3 * 0.4
y = [mix_1, mix_2, mix_3]
y = [(y_i * 32767 / max(np.absolute(y_i))).astype(np.int16) for y_i in np.asarray(y)]

wf.write('mix_1.wav', rate1, y[0])
wf.write('mix_2.wav', rate2, y[1])
wf.write('mix_3.wav', rate3, y[2])

mix_1.wav

mix_2.wav

mix_3.wav

なかなかひどいものができてしまいましたが、どれか一つの音に集中しようとしてみると結構難しいと思います。(できる人もいると思うけど)
こんなぐちゃぐちゃになったデータから元のデータを抽出します。

音源分離

分離すべきデータができたので、独立成分分析(ICA)関係の処理についてまとめた
ica.py

import numpy as np

epsilon = 1e-5

class ICA:
    def __init__(self, x):
        self.x = np.matrix(x)

    def ica(self): #独立成分分析
        self.fit()
        z = self.whiten()
        y = self.analyze(z)
        return y

    def fit(self): #平均を0にする
        self.x -= self.x.mean(axis=1)

    def whiten(self): #白色化
        sigma = np.cov(self.x, rowvar=True, bias=True)
        D, E = np.linalg.eigh(sigma)
        E = np.asmatrix(E)
        Dh = np.diag(np.array(D) ** (-1/2))
        V = E * Dh * E.T
        z = V * self.x
        return z

    def normalize(self, x): #正規化
        if x.sum() < 0:
            x *= -1
        return x / np.linalg.norm(x)

    def analyze(self, z):
        c, r = self.x.shape
        W = np.empty((0, c))
        for _ in range(c): #観測数分だけアルゴリズムを実行する
            vec_w = np.random.rand(c, 1)
            vec_w = self.normalize(vec_w)
            while True:
                vec_w_prev = vec_w
                vec_w = np.asmatrix((np.asarray(z) * np.asarray(vec_w.T * z) ** 3).mean(axis=1)).T - 3 * vec_w
                vec_w = self.normalize(np.linalg.qr(np.asmatrix(np.concatenate((W, vec_w.T), axis=0)).T)[0].T[-1].T) #直交化法と正規化
                if np.linalg.norm(vec_w - vec_w_prev) < epsilon: #収束判定
                    W = np.concatenate((W, vec_w.T), axis=0)
                    break
        y = W * z
        return y

を作ります。(analyzeのアルゴリズムについては最後の本を見てください...)

次に、データの入出力をするためのコード
separation.py

import numpy as np
import scipy.io.wavfile as wf
from ica import ICA

rate1, data1 = wf.read('mix_1.wav')
rate2, data2 = wf.read('mix_2.wav')
rate3, data3 = wf.read('mix_3.wav')
if rate1 != rate2 or rate2 != rate3:
    raise ValueError('Sampling_rate_Error')

data = [data1.astype(float), data2.astype(float), data3.astype(float)]

y = ICA(data).ica()
y = [(y_i * 32767 / max(np.absolute(y_i))).astype(np.int16) for y_i in np.asarray(y)]

wf.write('music1.wav', rate1, y[0])
wf.write('music2.wav', rate2, y[1])
wf.write('music3.wav', rate3, y[2])

を作ります。
これで、必要なものは揃ったので適当なディレクトリにこれらのコードとデータを入れてseparation.pyを実行すると分離された音源データができます。

結果

music1.wav

music2.wav

music3.wav

music1がloop1.wav、music2がfanfare.wav、music3がstrings.wavとほぼ同じに聞こえます、分離することができました。
権利関係がよくわからないのでデータを上げることはできませんが、同じようにして人の話し声なども、分離することができました。

最後に

便利なライブラリを使わないでも頑張ればなんとかなるものです。
よくわからないけどこの関数を使うとなんかできるみたいな状態の人は、一度使わないで作ろうとするとライブラリの理解が深まったりありがたさがよりわかると思います。
ライブラリを使わないからにはちゃんとした理論的な説明を書きたかったのですが、理解しきれていないところも多く適当な説明になってしまいましたが、興味を持った人は

books.google.co.jp

を読んでみてください。

最後までご覧頂き、ありがとうございました。

【速報レビュー】Googleが無償公開したAIの社内教育システム「ai.google」を使ってみた

f:id:meteoputi:20180301163725j:plain

先日、GoogleがAIの社内教育プログラムを公開しましたことで話題になりました。

Learn with Google AI

https://ai.google/education/#?modal_active=none

弊社(Aidemy)も同じくAIを手がける会社ということで早速利用してみました。

Google社内教育プログラムへは、上記リンクへアクセスしていただき、「Education」の場所をクリックしていただくと遷移することができます。

 

今回は実際にAIの会社のエンジニアがGoogleのサービスを使ってみて感じた

・優れた点、使いづらいと感じた点

・ぜひ一度体験してみていただきたいサービス

・さらに改善されたら嬉しい点

について皆様に共有させていただければと思います。

 

優れた点、使いづらいと感じた点

まず、ページに移動して最初に目につくであろうコンテンツが、この動画です。

f:id:meteoputi:20180301145139p:plain

https://ai.google/education/#?modal_active=yt-cKxRvEZd3Mw

 

実際に視聴してみて感じたのが

・とにかくクオリティーが高く、わかりやすい

・英語の発音がとても綺麗な上に自動で日本語の字幕がついてくる

・ワンクリックで動画が再生でき、とても使いやすい

ということです。さすがGoogleといったところでしょうか。

内容については機械学習の概論についてわかりやすくまとめたサマリー動画という印象でした。もし、機械学習について、まだよく分かっていない方は一度視聴してみることをお勧めします。

しかしながら、ウェブサイト内にある全ての動画に日本語字幕がついている訳ではないのが残念なところです。

 

また、コードを書きながら学習を進めることができるのも素晴らしい点です。

f:id:meteoputi:20180301160609p:plain

ただし、ライブラリのインストールやソースコードのインストールは自分自身で行わなければなりません。ドキュメントも全て英語であるため、全くの初心者にとっては少し敷居が高いでしょうか。

 

ぜひ一度体験してみていただきたいサービス

こちらのウェブサイトを触っていて、とりわけ素晴らしいと感じたサービスについてご紹介させていただきます。まず、ご紹介させていただくのが Deep Ground というサービスです。(当サービスは以前から公開されていましたね。)

http://playground.tensorflow.org/#activation=tanh&batchSize=10&dataset=circle&regDataset=reg-plane&learningRate=0.03&regularizationRate=0&noise=0&networkShape=4,2&seed=0.83641&showTestData=false&discretize=false&percTrainData=50&x=true&y=true&xTimesY=false&xSquared=false&ySquared=false&cosX=false&sinX=false&cosY=false&sinY=false&collectStats=false&problem=classification&initZero=false&hideText=false

これはニューラルネットワークがどのように学習を進めていくのかが目で見てわかる可視化ツールです。

f:id:meteoputi:20180301153709p:plain

また、パラメーターを自分で調節することもできるため、触っていてとても楽しめます。

感覚的にニューラルネットワークの学習が理解できるため、ぜひ一度体験していただきたいサービスです。

詳しい説明についてはAI Adventures: 7 Steps of Machine Learningという動画に乗っています。

https://ai.google/education/#?modal_active=yt-nKW8Ndu7Mjw

 

次にご紹介するのはMachine Learning Glossary です。

https://developers.google.com/machine-learning/glossary/?utm_source=google-ai&utm_medium=card-image&utm_campaign=training-hub&utm_content=ml-glossary

機械学習では多くの専門用語が登場しますが、分からないことが多いでしょう。このページではそんな専門用語を英語でわかりやすく解説してくれています。Google.AIで学習を進める際はもちろん、英語で書かれた記事を読む際にもとても役立ちそうです。

 

さらに改善されたら嬉しい点

最後に学習を進めていく上で不便と感じるであろう点をご紹介します。

 

・ウェブサイトをはじめとして、ほとんどドキュメントは英語で書かれている。

これはGoogleが出しているサービスなので当たり前なのですが、全て英語で書かれています。英語が苦手な方は最初から苦戦してしまうかもしれません。

 

・コンテンツが分断されていてバラバラである。

確かに一つ一つのコンテンツの完成度は高いのですが、一連の学習を順番に進めていくという形式ではなく、情報がバラバラに配置されている状態ですので、体系的にAIについて学んでいきたい初学者の方にはあまりおすすめできないかもしれません。

 

無料で公開されているコンテンツですので文句は言えないですが、上記の点は改善されると、ありがたいところですね。

 

10秒でコードを書き始められるAidemyと組み合わせれば良いのでは?

手前味噌ですが、日本語で体系的にAIについて学びたい方には Aidemy をおすすめさせていただきます。

ウェブサイトにて学習を進められるため、ライブラリのインストールや環境構築などの煩雑な準備は一切必要なく10秒で学習に取り掛かることができます。また、目的に合わせた学習ルートをご用意させていただいているため、学習の順番が分からない方はそちらを参考にしていただくと、学習が進めやすいと思います。

しかし、Aidemyでは現在ビデオ教材は一切配信されていません。動画を見ながら学習を進めていきたいユーザーにとっては、このai.googleと組み合わせて実践してみると、より理解が深まるでしょう。

Aidemyは現在期間限定で14コース全て無料公開中ですので、この機会にぜひご利用ください。

aidemy.net

HIP HOPでわかるネットワーク分析

自己紹介

みなさん、はじめまして。
Aidemy 研修生の 加藤正義 (加藤正義 (@Kato_Justice) | Twitter) です。昔はテレビ東大生として、時々バラエティ番組とかに出ていました。

〈●〉EYE-CATCH〈●〉

 フィーチャリング関係で繋がった日本語ラッパーのコミュニティをグラフ化すると、以下のような構造をしている。
f:id:kato_justice:20180220153459p:plain
ノードの色は派閥を表し、媒介中心性が大きいほどノードのサイズが大きい。ラッパーの派閥は

  • フリースタイルダンジョン組
  • KGDR + RHYMESTER + 愛国ラッパー達 (韻が固い)
  • 独立したその他ユニット

という具合で分かれた。
媒介中心性の最も高いラッパーはKEN THE 390だった。

ラッパーは好き嫌いが激しい

 突然ですが、僕は日本語ラップが好きです。しかし、当のラッパー同士がお互いのことを無条件で好きかというと、どうやらそうではないようです。ラッパーはしばしば価値観の違いから対立し、ビーフと呼ばれるdisり合いの行動を取ることがあります。
kai-you.net
このようにお互いの好き嫌いが激しいラッパー達なので、彼らの人間関係もいくつかの派閥に分かれていそうだなと思い、ラッパーの派閥構造ネットワーク分析と呼ばれる手法で解析し、可視化してみようと思いました。

feat. で繋がる絆

 ラッパー同士の親しさを客観的に定義する方法のひとつとして、フィーチャリングの関係が利用できると考えました。フィーチャリングとは、ラッパーが楽曲を作製する際に別のラッパーに協力を仰ぐことで、特にラッパーの場合は互いにリスペクトしてる場合のみフィーチャリングが成立すると考えています。よって、フィーチャリングの関係はラッパー同士の絆を表すとよい指標であると考えます。
www.youtube.com
以上を踏まえて、フィーチャリング関係をもとにラッパー派閥の構造を解明していきたいと思います。

やり方

 以下のような流れでやっていきたいと思います。

  1. データを用意する、データの種類は以下の通り。
    • ラッパーと所属グループのリスト
    • 上リスト内のラッパー同士のフィーチャリング関係のリスト
  2. networkxにより、ラッパーをノードフィーチャリング関係をリンクとした無向グラフを構築 (つまり、一度でもfeat. してればダチとみなして、ラッパー達を絆で繋ぐということ)
  3. モジュラリティ指標が最も高いグラフの分割をする (つまり、いつメンで固まること)
  4. 媒介中心性を計算する (媒介中心性:『お前がいなきゃ界隈がまとまらねぇ』という度合いのこと)
  5. 構造がわかりやすいように可視化・要約する

1. データの用意

name.csv - Google スプレッドシート
 name.csvは、今回の分析の対象とするラッパーのリストです。独断と偏見で選んだ57人のラッパー達です。"Name"のカラムにはラッパーの名前が、"Group"のカラムにはそのラッパーが属する代表的なグループが書かれています。所属グループがない場合はラッパー自身の名前で置き換えています。

feat.csv - Google スプレッドシート
 一方、feat.csvにはラッパー同士のフィーチャリング関係が記されています。120行あります。各行について、"name_1"と"name_2"がフィーチャリングで繋がっている事を意味します。フィーチャリング関係をどう調べたかについてですが、YouTubeで『"ラッパー名" feat.』と検索、検索結果を2,3 ページ分スクロールしてリスト内のラッパーがいれば追加、という方法で行いました。正直、ここが一番たいへんでした。ただ、自分でデータを作るので意図しないデータ構造のデータクレンジングで疲労をしなくていいのはよかったのかな?

2. networkxによる無向グラフの構築

 今回は、networkxというpythonのパッケージを使ってグラフを書いていきます。

import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
from community import community_louvain

#グラフの大きさ決める
plt.figure(figsize = (10, 10))
# ラッパーのデータをデータフレーム化
df_name = pd.read_csv("name.csv")
df_ft = pd.read_csv("feat.csv")

#ラッパーの繋がりを格納しておくための辞書
#{"ラッパー名": "フィーチャリングしているラッパー達" , ...} という形で格納
vector = {}
for i in range(len(df_name)):
  vector[df_name.iloc[i,0]] = []

#同じグループで活動しているラッパーについては、無条件で繋げる
for i in range(len(df_name)):
  for j in range(len(df_name)):
    if df_name.iloc[i,1] == df_name.iloc[j,1] and i != j:
      vector[df_name.iloc[i,0]].append(df_name.iloc[j,0])

#フィーチャリングしているラッパーについて、ノードを接続
for i in range(len(df_ft)):
  vector[df_ft.iloc[i,0]].append(df_ft.iloc[i,1])

#vectorをグラフ化する
G = nx.Graph(vector)

この操作により、変数Gにグラフが格納されました。この時点でグラフGを

nx.draw_networkx(G)

によって出力すると、以下のようになります。目を凝らして観察すれば誰が誰と繋がっているのかはわかれど、派閥の存在や、誰が派閥の中心なのかをこのグラフから読み取るのは厳しいですね。
f:id:kato_justice:20180220154518p:plain

3. モジュラリティを最大化するグラフの分割

 さて、単にノード同士を繋いだだけではどういう派閥が存在するのかはわかりません。なので、所属する派閥ごとにグラフを分割したいのですが、どのような基準でグラフを分割をすれば納得感があるでしょうか?このようなシチュエーションで役に立つ概念の一つとして、モジュラリティというものがあります。以下では、モジュラリティについて説明します。
 まずは、ノードがランダムに繋がった(=派閥のない)グラフを見てみましょう。
f:id:kato_justice:20180220220746p:plain
隣接行列には、どのノードがどのノードと繋がっているかの情報が含まれています。隣接行列のij列の値は、ノードiとノードjが繋がっていれば1、そうでなければ0とします。ノード同士をランダムに接続するグラフを作る場合、隣接行列において対角成分を除けば全ての要素の期待値が等しいはずです (対角成分は常に0(自分自身とは繋がれないので(人はいつもほんとうの意味で孤独)))。

 一方で、グラフ内でコミュニティが分化していれば(=グラフ内に派閥構造を持っていれば) グラフおよび隣接行列は以下のようになります。
f:id:kato_justice:20180220225101p:plain
図中の/で示されるコミュニティの内部でのリンクは多く存在するのに対して、コミュニティ間を繋ぐリンクは稀にしか存在しないのがおわかりでしょうか?それに対応するように隣接行列においても、グループ内の繋がりを表す要素には1が多く、グループ間の繋がりを表す要素には0が多いという偏りができています。この偏りを利用して、コミュニティが分化している程度を表そうというのがモジュラリティの考え方です。下の図でモジュラリティの出し方を説明します。ここでは、簡単のために、2分割のみの場合を扱います。
f:id:kato_justice:20180220230632p:plain
モジュラリティQは、所与の分割をした際に各コミュニティ内部でのリンク数から、リンクがランダムであった場合のリンク数期待値を引き、これを総リンク数で割ることで定義されます。つまり、Qはコミュニティ内部にリンクが集中している割合を表す指標というわけです。

 さて、モジュラリティを最大化する分割をどう実装するかなのですが、communityというパッケージの中にモジュラリティが最大になるような分割をしてくれる機能があります。呼び方は簡単で、

partition = community_louvain.best_partition(G)

とすればOKです。値は辞書型で渡され、以下のように数値によってコミュニティが振り分けられます。

{'KEN THE 390': 0, 'TWIGY': 1, 'KREVA': 2, 
... , 'G.K.MARYAN': 1, 'REKKO': 0, 'TAKUMAKI': 0}

グループに関する情報を入れて、Gを再び

nx.draw_networkx(G,node_color=[partition[node] 
for node in G.nodes()], cmap=plt.cm.RdYlBu)

で描写すると、以下のような感じになります。
f:id:kato_justice:20180221180909p:plain
だんだんわかってきましたね。けれど、ネットワークにおいて誰がどれくらい影響力を持っているかはまだわかりません。次は媒介中心性という概念で、誰がラッパーのコミュニティで影響力を持っているのかを明らかにしていきましょう。

4. 媒介中心性を求める ~UZIを例にして

 媒介中心性とは、ネットワーク分析において最もよく使われている中心性の指標で、そのノードを通る最短経路が多いほど媒介中心性は高くなります。詳しい説明するために、以下のような、お互い繋がりあうラッパー達の事を考えてみましょう。このグラフでは、いくつかのノードを経由することにより、全てのノードが互いに辿り着きあえるようになっています。
f:id:kato_justice:20180221230336p:plain
しかし、この界隈に事件が起きて、あるノードが消失したらどうでしょうか?ここでは、UZIが御用となってしまった世界の事を考えてみましょう。
www.excite.co.jp
UZIが塀の中に入ってしまうと、様々な不都合が起きます。フリースタイルダンジョンが放送中止となるだけではなく、これまでUZIを媒介として繋がりあっていたノード同士の繋がりが、断絶されます*1(例: 下図中のKダブとDJ YAS)。また、UZIなしでは連絡をつけるために遠回りをしないといけない場合もあります(例: 下図中のTwiGyと童子-T)。UZIがいなくなってはじめて、UZIがラッパー達の媒介としての役割を担っていたことがわかります。
f:id:kato_justice:20180221231353p:plain
このUZIが果たしていた役割に、スコアをつけることはできないでしょうか?下の図は、YOU THE ROCK★にとっての、UZIの媒介としての価値を示しています。
f:id:kato_justice:20180221234121p:plain
YOU THE ROCK★が最短で各ラッパーに辿り着く経路を考えてみましょう。NORIKIYO、K DUB SHINEにアクセスするには、UZIを通る経路が唯一の最短経路であることがわかります。また、童子-Tにアクセスするには、UZIを経由する経路とDJ-MASTERKEYを経由する経路の2つの最短経路があることがわかります。その他のラッパーにアクセスには、UZIを経由する必要はありません(ほかは全員雷家族のメンバーなので当然そう)。するこのとき、UZIを経由する唯一の最短経路に対してスコア+1最短経路が複数ある場合はUZIを通るものにスコア+1/n(今回は、1/2) を与えます。このスコアの和は、YOU THE ROCK★にとっての、UZIの媒介中心としての価値そのものです。YOU THE ROCKに限らず、全てのノードにとってのUZIのスコアを足し合わせることで、UZIの媒介中心性が表せます*2
 さて、この中心媒介性をnetworkxで求める方法ですが、実際簡単で、

between_cent = nx.communicability_betweenness_centrality(G)

で求めることができます。between_centの中身は辞書型で、

{'KEN THE 390': 0.34846233379185004, 'TWIGY': 0.1644836555192994, 'KREVA': 0.1557333845321839, 
... , 'G.K.MARYAN': 0.15635132015072525, 'ANARCHY': 0.013911874100152521, 'TAKUMAKI': 0.004676026664705854}

のような形になっています。

5. 可視化・要約

 さて、今までの作業でラッパーのfeat. 関係グラフに関する、モジュラリティ最大化媒介中心性を求めました。折角なので、これをわかりやすく可視化していきたいです。
グラフ分割についての可視化は先ほどやったのですが、ここでは、媒介中心性の程度もわかるように、媒介中心性が大きいほどノードも大きくなるグラフを書きたいと思います。以下のようなコードで実現できます。

#モジュラリティが最大になるような分割をする
partition = community_louvain.best_partition(G)
#中心媒介性を出す
between_cent = nx.communicability_betweenness_centrality(G)
#中心媒介性をもとにノードの大きさを決める
#そのままじゃ小さすぎるで雑に5000くらいかける
node_size = [5000 * size for size in list(between_cent.values())]
#いい感じにグラフのレイアウトをやる
pos = nx.spring_layout(G)
#グラフを出力。派閥ごとに色分けし、
#中心媒介性の大きいノードほど大きく表示
nx.draw_networkx(G,pos, node_color=[partition[node]
for node in G.nodes()],node_size=node_size, cmap=plt.cm.RdYlBu)

これで、アイキャッチで出てきたような、わかりやすいグラフが出力されます。ノードの色は派閥を表し、大きさは媒介中心性を表します。
f:id:kato_justice:20180220153459p:plain
これでちゃんとわかりやすいので、よかったですね。

 しかし、このグラフだけでは詳しい事までは読み取れないので、今回得られたデータを要約してみましょう。まず、各派閥ごとの属性を見てみます。「だいたいこんな感じ」というやつなので、全員に当てはまるわけではありません。

派閥 属性
0 フリースタイルダンジョン組
1 KAMINARI-KAZOKU.
2 KGDR + RHYMESTER + その他愛国的ラッパー、韻が固い
3 30~40歳くらい? ゼロ年代中盤くらいにヒットしたラッパーたちで、古参である派閥2よりは若い
4 濃水 SOUL SCREAM
5 BUDDHA BRAND

自明なこととして、所属ユニットが派閥に強く影響しているようです*3。少しだけ自明でない事としては、派閥0が示す通りユニットを組んでいなくてもフリースタイルダンジョン組の結びつきは強いことや、派閥2が示すように活動時期や価値観が似たラッパー達の結びつきは強い事が挙げられます。

 媒介中心性のランキングは以下のようになりました。このランキングが高いほどラッパー界での影響力が高いことになります(なるのか?) みんな有名なラッパーたちばっかですね。

ランク 名前 媒介中心性
1位 KEN THE 390 0.35
2位 Zeebra 0.34
3位 UZI 0.31
4位 DJ MASTERKEY 0.28
5位 般若 0.24
6位 Mummy-D 0.24
7位 R-指定 0.17
8位 NORIKIYO 0.17
9位 TwiGy 0.16
10位 SKY-HI 0.16

おわりに

  • 怠け者の僕にしてはけっこう頑張りました、分析そのものより、データ集めやブログ執筆のためのポンチ絵作りに手間がかかりました。ポンチ絵を作るのは楽しい。
  • この記事がきっかけで、ネットワーク分析というよりかは日本語ラップに興味を持つ人が増えてくれればいいなと思っています。
  • ラッパーのfeat. 関係ですが、手作業で集めたので漏れなどがあると思います。間違いを指摘してくれるとうれしいです。
  • B-BOYになりたい(写真は、B-BOYだった頃のぼくです、成り格好は一丁前だがまだスキルがない)

f:id:kato_justice:20180222122326j:plain

コード全文

 コピペして使えるようにまとめておきます。

import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
from community import community_louvain

#グラフの大きさ決める
plt.figure(figsize=(10, 10))
# ラッパーのデータをデータフレーム化
df_name=pd.read_csv("name.csv")
df_ft=pd.read_csv("feat.csv")

vector={}
for i in range(len(df_name)):
  vector[df_name.iloc[i,0]] = []
    
#同じグループで活動しているラッパーについては、無条件で繋げる
for i  in range(len(df_name)):
  for j in range(len(df_name)):
    if df_name.iloc[i,1]==df_name.iloc[j,1] and i!=j:
      vector[df_name.iloc[i,0]].append(df_name.iloc[j,0])

#フィーチャリングしているラッパーについて、ノードを接続
for i  in range(len(df_ft)):
  vector[df_ft.iloc[i,0]].append(df_ft.iloc[i,1])

G= nx.Graph(vector) 

# モジュラリティ最大の分割
partition = community_louvain.best_partition(G)
#媒介中心性を求める
between_cent = nx.communicability_betweenness_centrality(G)
node_size = [5000 * size for size in list(between_cent.values())]
#形を整えて、出力
pos = nx.spring_layout(G)
nx.draw_networkx(G,pos, node_color=[partition[node] for node in G.nodes()],node_size=node_size, cmap=plt.cm.RdYlBu)

*1:もちろん、実際はUZIなしでも連絡を取り合えると思うのですが、話を簡単にするためにそうさせてください

*2:実際は、この値を、UZIを通らない頂点対の数(N個の頂点のグラフなら、(N-1)*(N-2)/2 )で割ります

*3:同じグループ内のラッパーは全てリンクさせたのでそれはそう