Webデータレポート

株式会社ルーターのデータレポートブログです

マルコフ連鎖の文章生成について検証してみた

はじめまして。新入社員の松永です。

文章生成でブログ記事のようなものを自動作成できないか調べたところ、 DeepLearningを使わなくてもマルコフ連鎖Rubyでも手軽に文章生成ができるらしいことがわかったので、 果たしてどの程度の精度で自動生成が可能なのか検証してみました。

簡単な流れとしては、 Mecabで文章を形態素分析して単語単位に分解して辞書を作成。 作成した辞書に基づいて、マルコフ連鎖でランダムに文章を生成しています。

▼実装環境

マルコフ連鎖とは

文章を生成するときに、ある形態素に繋げられそうな形態素を選んで徐々に繋げていき文章を作成します。

例えば次の文章。

マルコフ連鎖 を 使って、文章 を 自動生成 します。」

このとき、「を」という形態素に繋がりそうな形態素は「使って」と「自動生成」ですね。 この繋がりそうな2つの形態素の中から、繋げる形態素を選ぶ作業を繰り返すことで文章を作成していきます。

ブログ記事の共通項目で文章生成

2件のブログの「インフルエンザとは」項目を形態素解析して文章生成してみました。

サンプル1:

インフルエンザとはインフルエンザウイルスが原因で起こる12月から4月に流行る感染症です。
急に起こる38~40℃の高熱、関節痛、頭痛が特徴的です。通常は、薬を使わなくても約1週間で治癒するとされていますが、高齢者や糖尿病などの持病のある方や小さな子どもがインフルエンザに感染すると、肺炎や脳症など合併症を起こすことがあるので注意が必要です。
インフルエンザワクチンの接種が重篤化を防ぐために有効とされています。
インフルエンザは学校保健安全法施行規則(平成24年施行)により出席停止となる伝染病の一つで、「発症後5日かつ解熱後2日(幼児は3日)を経過」するまで出席停止となります。職場などでもそれに倣って自宅待機となることが多いです。

「インフルエンザの症状・原因・治療 Doctors Me(ドクターズミー)」 https://doctors-me.com/doctor/respiratory/13

サンプル2:

インフルエンザは、毎年12月から1月にかけて猛威を振るいます。予防接種をしたり予防・対処法についての知識を得たりと、流行が本格化する前から注意を払っておく必要もあります。今回は、インフルエンザと普通の風邪との違い・検査方法・かかった場合の対処法や治療法について、医師・武井 智昭先生による監修記事で解説していきます。

「インフルエンザの症状~症状、かぜとの違い~|インフル・ニュース」 http://www.influ-news.info/influ/symptoms.html

結果

インフルエンザは、毎年12月から1月にかけて猛威を振るいます。インフルエンザワクチンの接種が重篤化を防ぐために有効とされています。インフルエンザは、薬を使わなくても約1週間で治癒すると、流行が本格化する前から注意を払っておく必要もあります。

まぁまぁそれらしい文章ができています。 しかしよくよく読んでみると文脈が成り立ってない部分があります。

Twitterのツイートで自動生成

次に、特定のアカウントの過去のツイートを200件形態素解析して文章生成してみます。

サンプル: 恋愛依存メンヘラ@bot @memememenhel https://twitter.com/memememenhel

結果

あなたの手の温もりで有りますように、少しでも、ほんの少しでも。私が手首を切ったの大丈夫だよって証明したくて手首を舐めてくれる気がないの知ってる癖に。離れられない。

手首切りたい死にたいが口癖です。

かなりそれっぽい。 文章としては少しおかしい部分もあるのですが、それがかえってポエム感に繋がっています。

▼まとめ

センテンス単位では文章としてまぁまぁそれらしい結果になっています。 しかし、よくみると文脈として成り立っていない場合があるので、長文や論理的な文章にはあまり向いていないのかなという印象を受けました。 多少文章が崩れても問題のないポエムのような文章には向いているかもしれません。

▼ソース

require 'natto'
require 'pp'
require 'enumerator'

$h = {}
# ----------------形態素解析
# ----------------辞書的なものの作成
def parse_text(text)
    mecab = Natto::MeCab.new
    text = text.strip
    # 形態素解析したデータを配列に分けて突っ込む
    # 先頭にBEGIN、最後にENDを追加
    data = ["BEGIN","BEGIN"]
    mecab.parse(text) do |a|
        if a.surface != nil
            data << a.surface
        end
    end
    data << "END"
    # p data
    data.each_cons(3).each do |a|
        suffix = a.pop
        prefix = a
        $h[prefix] ||= []
        $h[prefix] << suffix
    end
end

# ----------------マルコフ連鎖
def markov()
    p "markov"
    # ランダムインスタンスの生成
    random = Random.new
    # スタートは begin,beginから
    prefix = ["BEGIN","BEGIN"]
    ret = ""
    loop{
        n = $h[prefix].length
        prefix = [prefix[1] , $h[prefix][random.rand(0..n-1)]]
        ret += prefix[0] if prefix[0] != "BEGIN"
        if $h[prefix].last == "END"
            ret += prefix[1]
            break
        end
    }
    p ret
    return ret
end

str = ""
file_name = STDIN.gets

begin
    File.open(file_name,"r") do |file|
        file.each_line do |line|
            str += line.to_s
        end
        parse_text(str)
    end
end

#マルコフ連鎖で文章を自動生成
markov()

▼参考

Ruby形態素解析してマルコフ連鎖で文章生成 http://utsubo21.hatenablog.com/entry/2015/02/08/224155

生活音を機械学習してみた

音声認識というとどうしてもスピーチをテキストにするというソリューションが多いです。しかし用途がユーザーインターフェースに限られる。IOTのシーンではモノとモノが通信し合ってこそ、人間様が楽できるので、生活音を識別することをゴールとしたいと思います。

▼ 概要

今回は5種類のスズの音のサンプルを使って,2種類ずつどちらのスズか判別を行ってみたいと思います。

▼ 実装環境

▼ 用意するもの

Python機械学習ライブラリのscikit-learnを使います。
(参考: http://www.iandprogram.net/entry/2015/09/15/221518 )

UbuntuPythonをインストール
$ sudo apt-get install python2.7

pipをインストール
$ sudo apt-get install python-pip

Ubuntuにscikit-learnをインストール
$ sudo pip install scikit-learn

他にnumpyもインストールしておく。

▼ 教師データの作成

5種類のスズの音サンプルを用意します。

スズの名前 スズの音のタイプ
suzu01 一般的なスズ
suzu02 喫茶店のドアについてる感じ
suzu03 チリーン 風鈴みたいな
suzu04 シャン! クリスマスっぽい
suzu05 川の流れる音みたいな

音のサンプルを載せているWebサイトはたくさんありますが、 スズの音階がわからなかったりや同じスズを使っているけど鳴らし方だけ違ったりするサンプルが多く、教師データを作るのがなかなか難しい(正解ラベルが貼れない)。

人間が聞いて明らかに違うスズだとわかるものをサンプルとして使うことが望ましい。

5種類のスズの音サンプルは実際に聞いてみて明らかに違うものを用意する。今回用意したスズはスペクトログラムを書いてみると、その形状の違いから5つの異なるスズを使っていることがわかります。

f:id:webdatareport:20161106160556p:plainf:id:webdatareport:20161106160600p:plainf:id:webdatareport:20161106160603p:plainf:id:webdatareport:20161106160610p:plainf:id:webdatareport:20161106160613p:plain

サンプルだけではパターンが少ないので、機械学習するには十分ではありません。
そこで、スズの音にノイズ音を加えてデータを量産することに。

ノイズの名前 ノイズの内容
noise01 ハイヒールの足音
noise02 皮靴の足音
noise03 ドアが閉まる音
noise04 ドアノブをガチャガチャする音
noise05 ドアをノックする音
noise06 ドアが開く音
noise07 道路の環境音
noise08 人の笑い声
noise09 水洗トイレが流れる音
noise10 トイレットペーパーを回す音
noise11 TOTO音姫

5種類のスズの音に11種類のノイズを加えてデータを作成します。
さらに,ノイズの音量を10段階に設定することで更に多くの音声パターンを生成できる(5 × 11 × 10 = 550種類)。
今回はSoX (Sound eXchange) を使って,以下の手順でスズの音とノイズをミックスしていきます。
(参考:http://qiita.com/mountcedar/items/a04ebc4f8c27c226bbff)

ノイズ入りデータ作成手順 1. すべての音声データを一旦.wav形式に変換する 2. 5種類のスズの音を正規化 3. 11種類のノイズの音を正規化 4. 手順1と手順2からできた音声データをスズの音の長さに合わせて合成する。 その際, 新たに合成したノイズ入りのアウトプットファイルをスズの種類ノイズの種類音の段階(0~9).wav として保存する。

例:suzu03のwavファイルとnoise07のwavファイルを合成する。
$ sox -m suzu/suzu03.wav -v 0.2 noise/noise07.wav learning_sample/3_7_9.wav trim 0 suzu03_len
-m : --combine mixの略で2つのファイルを結合するためのオプション。
-v : 後ろの音ファイルの音量を調節をするオプション。(1より大:音量を上げる、1より小:音量を下げる)
trim : 音声を切り取るオプション。例では0秒目からsuzu03の音の長さ(suzu03_len)に合わせてクリッピングしている。

f:id:webdatareport:20161106160750p:plain f:id:webdatareport:20161106160824p:plain f:id:webdatareport:20161106160852p:plain f:id:webdatareport:20161106160906p:plain

スペクトログラムと波形図をそれぞれ見てみると、元々のsuzu03の画像と比較して、ノイズが入っていることが確認できます。

▼ MFCC (メル周波数ケプストラム係数)

音のデータから特徴量を抽出する方法としてMFCCを使います。 MFCCは人間の聴覚の特性にあわせて低周波部分は細かく,高周波部分は粗く調べる手法らしい(音声解析ではよく使われている)。 音の専門家じゃないので不明な点がありますが、音声データの一部分を取り出して、指定した次元のベクトルに集約して特徴量を抽出してくれます。
サンプル間で音の長さが違う場合でも、同じ長さのベクトルを用意することができるので、そのまま機械学習にかけられる。

MFCCの抽出手順は

  1. プリエンファシスフィルタで波形の高域成分を強調する
  2. 窓関数をかけた後にFFT高速フーリエ変換)して振幅スペクトルを求める
  3. 振幅スペクトルにメルフィルタバンクをかけて圧縮する
  4. 上記の圧縮した数値列を信号とみなして離散コサイン変換する
  5. 得られたケプストラムの低次成分がMFCC

(参考:http://aidiary.hatenablog.com/entry/20120225/1330179868)

#coding:utf-8
import wave
import numpy as np
import scipy.signal
import scipy.fftpack
import scipy.fftpack.realtransforms

def wavread(filename):
    wf = wave.open(filename, "r")
    fs = wf.getframerate()
    x = wf.readframes(wf.getnframes())
    x = np.frombuffer(x, dtype="int16") / 32768.0  # (-1, 1)に正規化
    wf.close()
    return x, float(fs)

def hz2mel(f):
    """Hzをmelに変換"""
    return 1127.01048 * np.log(f / 700.0 + 1.0)

def mel2hz(m):
    """melをhzに変換"""
    return 700.0 * (np.exp(m / 1127.01048) - 1.0)

def melFilterBank(fs, nfft, numChannels):
    """メルフィルタバンクを作成"""
    # ナイキスト周波数(Hz)
    fmax = fs / 2
    # ナイキスト周波数(mel)
    melmax = hz2mel(fmax)
    # 周波数インデックスの最大数
    nmax = nfft / 2
    # 周波数解像度(周波数インデックス1あたりのHz幅)
    df = fs / nfft
    # メル尺度における各フィルタの中心周波数を求める
    dmel = melmax / (numChannels + 1)
    melcenters = np.arange(1, numChannels + 1) * dmel
    # 各フィルタの中心周波数をHzに変換
    fcenters = mel2hz(melcenters)
    # 各フィルタの中心周波数を周波数インデックスに変換
    indexcenter = np.round(fcenters / df)
    # 各フィルタの開始位置のインデックス
    indexstart = np.hstack(([0], indexcenter[0:numChannels - 1]))
    # 各フィルタの終了位置のインデックス
    indexstop = np.hstack((indexcenter[1:numChannels], [nmax]))

    filterbank = np.zeros((numChannels, nmax))
    for c in np.arange(0, numChannels):
        # 三角フィルタの左の直線の傾きから点を求める
        increment= 1.0 / (indexcenter[c] - indexstart[c])
        for i in np.arange(indexstart[c], indexcenter[c]):
            i=int(i)
            filterbank[c, i] = (i - indexstart[c]) * increment
        # 三角フィルタの右の直線の傾きから点を求める
        decrement = 1.0 / (indexstop[c] - indexcenter[c])
        for i in np.arange(indexcenter[c], indexstop[c]):
            i=int(i)
            filterbank[c, i] = 1.0 - ((i - indexcenter[c]) * decrement)

    return filterbank, fcenters

def preEmphasis(signal, p):
    """プリエンファシスフィルタ"""
    # 係数 (1.0, -p) のFIRフィルタを作成
    return scipy.signal.lfilter([1.0, -p], 1, signal)

def mfcc(signal, nfft, fs, nceps):
    """信号のMFCCパラメータを求める
    signal: 音声信号
    nfft  : FFTのサンプル数
    nceps : MFCCの次元"""
    # プリエンファシスフィルタをかける
    p = 0.97         # プリエンファシス係数
    signal = preEmphasis(signal, p)

    # ハミング窓をかける
    hammingWindow = np.hamming(len(signal))
    signal = signal * hammingWindow

    # 振幅スペクトルを求める
    spec = np.abs(np.fft.fft(signal, nfft))[:nfft/2]
    fscale = np.fft.fftfreq(nfft, d = 1.0 / fs)[:nfft/2]

    # メルフィルタバンクを作成
    numChannels = 20  # メルフィルタバンクのチャネル数
    df = fs / nfft   # 周波数解像度(周波数インデックス1あたりのHz幅)
    filterbank, fcenters = melFilterBank(fs, nfft, numChannels)


    # 定義通りに書いた場合
    # 振幅スペクトルに対してフィルタバンクの各フィルタをかけ、振幅の和の対数をとる
    mspec = np.log10(np.dot(spec, filterbank.T))


    # 離散コサイン変換
    ceps = scipy.fftpack.realtransforms.dct(mspec, type=2, norm="ortho", axis=-1)

    # 低次成分からnceps個の係数を返す
    return ceps[:nceps]

#wavファイルと次元数を入れてMFCCを抽出
#   nfft:FFTのサンプル数 1024, 2048, 4096
#   nceps:MFCCの次元数 大体12次元が多い
#   ※ fs * cuttime >= nfft/2 を満たす値を与えなければいけない
def get_feature(wavfile,nfft,nceps):
    # 音声をロード
    wav, fs = wavread(wavfile)
    t = np.arange(0.0, len(wav) / fs, 1/fs)
    
    # 音声波形の中心部分を切り出す
    center = len(wav) / 2  # 中心のサンプル番号
    cuttime = 0.8         # 切り出す長さ [s]
    wavdata = wav[int(center - cuttime/2*fs) : int(center + cuttime/2*fs)]
    
    ceps = mfcc(wavdata, nfft, fs, nceps)
    return ceps.tolist()

先ほどミックスしたsuzu03とnoise07 (ノイズの音量レベル9) の3_7_9.wavをMFCCにかけてみる。
上記のコードに以下のコードを付け加えて、実際にMFCCを実行する。

if __name__ == "__main__":
    wavfile="learning_sample/3_11_9.wav"
    nfft=2048
    nceps=12
    tmp=get_feature(wavfile,nfft,nceps)
    print tmp

実行すると、

[-2.1340185367571975, -1.2788164503621682, 0.2980729758980935, -0.7116624981453882, 0.33237397666134955,
0.22739636812287548, 0.7001966634450906, -0.12020691819143244, -0.10969326122265541, -0.45330444421058963,
0.2237907950505698, 0.2439421042510435]

このように、音声のwavファイルから12次のベクトルを生成することができました。

SVMで判別

いよいよ、スズの音を判別してみます。
5種類のスズ (suzu01~05) を以下すべての組み合わせで判別します。
(ただし、すべてノイズ入りです。)

スズの組み合わせ スズの音番号のペアNo
suzu01, suzu02 (1, 2)
suzu01, suzu03 (1, 3)
suzu01, suzu04 (1, 4)
suzu01, suzu05 (1, 5)
suzu02, suzu03 (2, 3)
suzu02, suzu04 (2, 4)
suzu02, suzu05 (2, 5)
suzu03, suzu04 (3, 4)
suzu03, suzu05 (3, 5)
suzu04, suzu05 (4, 5)

全組み合わせにおいて、ノイズ11種類中10種類を学習データに、残り1種類を評価用データに使用するようにして、
学習時にないノイズであっても、スズの音を判別できるかを試しました。

学習用データ 200個
評価用データ 20個

今回は機械学習ではメジャーなSVM (Support Vector Machine) を使用します。
(参考:http://may46onez.hatenablog.com/entry/2016/02/19/152532)

svm.py

#coding:utf-8
from sklearn import svm
from sklearn.metrics import classification_report, accuracy_score
import sys
from mfcc import *
import glob
import csv
import random
import itertools
import numpy as np

if __name__ == "__main__":
    
    bell_nums = range(1,6)
    pairs = list(itertools.combinations(bell_nums,2))
    
    for pair in pairs:
        
        bell_num1=int(pair[0])
        bell_num2=int(pair[1])
        train_data = np.empty((0,12),float)
        train_label = np.array([])
        test_data = np.empty((0,12),float)
        test_label = np.array([])
        noise_nums = range(1,12)
        level_nums = range(0,10)
        random.shuffle(noise_nums)
        
        nfft = 2048  # FFTのサンプル数
        nceps = 12   # MFCCの次元数
        
        #鈴の音1
        for noise_num in noise_nums[0:10]:
            random.shuffle(level_nums)
            #学習用データを作成
            for level_num in level_nums[0:10]:
                files_name = glob.glob("learning_sample/%d_%d_%d.wav" % (bell_num1,noise_num,level_num))
                for file_name in files_name:
                    feature = get_feature(file_name,nfft,nceps)
                    if len(train_data) == 0:
                        train_data=feature
                    else:
                        train_data=np.vstack((train_data,feature))
                    train_label=np.append(train_label,bell_num1)
            #テストデータを作成
            file_name = "learning_sample/%d_%d_%d.wav" % (bell_num1,noise_num,level_nums[8])
            feature = get_feature(file_name,nfft,nceps)
            if len(test_data) == 0:
                test_data=feature
            else:
                test_data=np.vstack((test_data,feature))
            test_label=np.append(test_label,bell_num1)
    
        #鈴の音2
        for noise_num in noise_nums[0:10]:
            random.shuffle(level_nums)
            #学習用データを作成
            for level_num in level_nums[0:10]:
                files_name = glob.glob("learning_sample/%d_%d_%d.wav" % (bell_num2,noise_num,level_num))
                for file_name in files_name:
                    feature = get_feature(file_name,nfft,nceps)
                    if len(train_data) == 0:
                        train_data=feature
                    else:
                        train_data=np.vstack((train_data,feature))
                    train_label=np.append(train_label,bell_num2)
            #テストデータを作成
            file_name = "learning_sample/%d_%d_%d.wav" % (bell_num2,noise_num,level_nums[8])
            feature = get_feature(file_name,nfft,nceps)
            if len(test_data) == 0:
                test_data=feature
            else:
                test_data=np.vstack((test_data,feature))
            test_label=np.append(test_label,bell_num2)
            
    
        #特徴データをテキストに出力
        feature_train_data=np.hstack((train_label.reshape(len(train_label),1),train_data))
        feature_test_data=np.hstack((test_label.reshape(len(test_label),1),test_data))
        with open("feature_data/train_data.txt","w") as f:
            writer=csv.writer(f)
            writer.writerows(feature_train_data)
        with open("feature_data/test_data.txt","w") as f:
            writer=csv.writer(f)
            writer.writerows(feature_test_data)
                
        #識別機学習
        clf = svm.SVC()
        clf.fit(train_data,train_label)
        #推定
        test_pred = clf.predict(test_data)
        #print np.hstack((test_label.reshape(len(test_label),1),(test_pred.reshape(len(test_pred),1))))
        
        #結果算出
        score=accuracy_score(test_label, test_pred)
        print pair,score

svm.pyの中でmfcc.pyをインポートして特徴量抽出ができるように関数を呼び出しています。
train_datatest_dataは1列目にスズの音のラベル、2列目以降にMFCCで抽出した12次元の特徴量ベクトルが入っており、これをSVMの学習器に入れます。

▼ 結果

(1, 2) 1.0  
(1, 3) 1.0  
(1, 4) 1.0  
(1, 5) 1.0  
(2, 3) 1.0  
(2, 4) 1.0  
(2, 5) 1.0  
(3, 4) 1.0  
(3, 5) 1.0  
(4, 5) 1.0

1列目が判定したスズの音番号のペアNoで2列目が正答率になっています。
どのスズの組み合わせも100%の精度で判別することができました。

▼ まとめ

今回,SVM自体のチューニングは特にしていません。
前処理のMFCCによる特徴量抽出の時点でかなり上手く分類できてしまっているのだと思います。

モンスターBOXのモンスター名をTensorFlowで特定してみた

▼モンスター画像がたくさん転がってる国、日本

機械学習で何かと進化したのが画像認識。人間の顔の認識は古くから研究されつくされてる感もあるので、それとは別にソーシャルメディア上にアップされている画像を特定してみたいと思います。

特に日本だと、Twitterにゲームのキャプチャをアップしている人が多いから、ゲームのキャプチャを解析すれば、その人のレベル感が分かるし、誰と何を交換すればいいかなどの特定がしやすくなどいろいろ可能性は広がりそうです。

▼実装環境

ubuntu 14.04
python 2.7.6
opencv 3.1.0

▼モンスターBOXからモンスターを取り出す

機械学習ではいつも「この屏風からトラを取り出していただければ退治してみせましょう」というシーンに出くわします。今回もモンスターBOXのたくさん並んでいるモンスターを直接学習させても、パターンが膨大すぎる。だからモンスターアイコンだけを取り出したい。

今回はOpenCVの特徴点を取り出す方法を採用します。特徴点をピックアップするとその画像の中で「ごちゃごちゃしているところ」が特定できます。

特徴点を取り出すにもいろいろアルゴリズムがありますが、今回はFASTというアルゴリズムを使いました。
FASTの詳しい説明(http://www.vision.cs.chubu.ac.jp/CV-R/jpdf/RostenECCV2006.pdf

FASTで検出した特徴点をプロットすると、これが

f:id:webdatareport:20160701094750j:plain

こうなる

f:id:webdatareport:20160701094753j:plain

こんな感じに。
特徴点がモンスター部分に密集しています。
OpenCVには他にも特徴点アルゴリズムがありますが、このFASTが一番モンスター部分に密集してくれました。

この特徴点をヒストグラム化してみます。
X軸方向(横)のヒストグラムがこちら

f:id:webdatareport:20160701094755j:plain

 

山が5個できています。
この山の部分がモンスター部分で、谷が背景部分です。
谷部分で切り取ってあげればモンスター画像が綺麗にトリミングされているのではないか。

山谷部分がわかりやすくなるようグラフを平滑化します。
pandasで移動平均をとってみます。
参考(http://blanktar.jp/blog/2015/12/python-pandas-moving-average.html)

f:id:webdatareport:20160701094757j:plain

青い点は、ピーク解析を行って極小値であると判定された部分です。
SciPyでピーク点を検出することができます。
参考(http://org-technology.com/posts/scipy-peak-finding.html)

これだと両端のモンスター部分は切り取れないので
x座標の先頭と末端から、ピーク解析で得た極小値リストの最小値に一番近い座標をプロットしてみます。

f:id:webdatareport:20160701094759j:plain

うまく切り取れそう。
Y軸方向も同様に

f:id:webdatareport:20160701094800j:plain

試しに、プロットした座標に線を引いてみます。

f:id:webdatareport:20160701094802j:plain

思いのほか綺麗に境界線が引けています。

 

▼分類機作成

いよいよTensorFlowで分類機を作ってみます。
モデル生成~学習まで一通りこちらを参考にしました。
http://kivantium.hateblo.jp/entry/2015/11/18/233834

教師データはこちら(http://monst.appbank.net/monster/archives/1.html)のモンスター図鑑のモンスター画像を使用しました。
また手作業でモンスターの額を取り除いた画像用意して、枠周辺はあんまり気にするなということを教え込みたいなと。

使用した教師画像はこの2パターン。

f:id:webdatareport:20160701094805j:plain

f:id:webdatareport:20160701094806j:plain

それと先程トリミングした際に出た切れ端を負例画像として使用して、モンスターじゃない!ってことを教えます


ということで、使用した教師データは以下の通り。
正例画像:モンスター1981種 × 2枚=3962枚
負例画像:54枚

▼結果

Tensorflowで作った分類機にトリミングしたモンスター画像を入れて分類結果を返す一連の流れを試してみました。

f:id:webdatareport:20160701094750j:plain

トリミングした画像番号,モンスター番号,モンスター名
000,344,マチルダⅡ
001,1538,宇宙の伝達者 ナスカ
002,1226,通天大聖 孫悟空
003,1040,張飛 益徳
004,844,ヴィシャス
005,335,エール・ソレイユ
006,1739,黄泉津大神 イザナミ
007,1523,ハートの女王
008,1202,ロックガール ガーゴイル
009,1038,非天 阿修羅
010,736,レルネーの主 ヒュドラ
011,915,タランチュラス・ウェポン
012,1739,黄泉津大神 イザナミ
013,1472,ちゃす
014,1181,ヒーロー ドラえもん
015,1038,非天 阿修羅
016,563,黄泉津大神 イザナミ
017,1981,水瓶座黄金聖闘士 カミュ
018,1617,リリム
019,1464,灼夏の巨人 スルト
020,1139,護法善神 羅刹
021,1036,除夜を射る将軍 徳川吉宗
022,563,黄泉津大神 イザナミ
023,1965,白鳥星座の青銅聖闘士 氷河
024,1584,情熱の爆音シンガー エナ
025,1266,輝石の美将 雲母大佐
026,1040,張飛 益徳
027,900,MDT チェリーウィンガー
028,460,炎狼の赤ずきん ノンノ
029,1881,白馬の騎士 のび太

 

 

30枚中28枚が正解。教師データのバリエーションは大してないのですが、文字などのノイズが含まれた画像を分類機に入れても結構な精度で分類できていることがわかります。これだけ雑な学習でもまぁまぁ特定できてる。

▼まとめ

TensorFlow自体は、特にこれといったチューニングめいたものは使っておらず、ポイントは前処理。そして前処理に使ってるのは、OpenCVの特徴点抽出のみってところです。

RとH2Oを使って Deep Learning でキャラクター判定をやらせてみた

何かとDeepLearningがブームになっています。うちでもやってみました。

学習させること

画像認識させてみたいと思います。手始めに、ミッフィー、キティ、マイメロのキャラクターを判別させてみます。人間が見てもおばあちゃんくらいになると厳しいこの3者が区別できるのか?

結論

3つのキャラのうちどれ?って判定をさせると、6割くらいまでの精度にはなりました。普通は背景を取り除いて、キャラの顔だけを切り抜いてから分類させるとは思うのですが、背景込でキャラ判別させた割にはまぁまぁの精度かなと思います。キャラの顔だけをくり抜くことをさせるには、OpenCVとかを使うはずですが、OpenCVをいじりまくるのはまた今度ということで。

以下の図は、活性化関数の種類別に、横軸が繰り返し回数、縦軸が正答率です。20回のRectifierが一番正答率が高いという感じですね。キティは9割がた当たるのですが、ミッフィーマイメロは区別しにくいようです。単に輪郭を区別しただけという気もしますが、一次元のベクトルから二次元の形を当てるというのもまた一興。

f:id:webdatareport:20151229153030p:plain

ただ、画像認識でやりたいことがはっきりしている場合には、OpenCVで行けるところまでいくのが定石。

以下は、手順のメモです。

用意するもの

DeepLearning専用のフレームワークを使わず、ここは手軽にRとH2Oを使ってみます。Rであれば、統計な人たちにも馴染み深いですね。

通常はRはRstudioで手元のPCで使うことが多いかと思いますが、機械学習には時間がかかるので、Rをクラウドサーバに入れたいと思います。クラウドサーバだと、特定の時間だけ、サーバースペックをあげることができるので便利ですね。

centOSにRを入れる

wget "http://cran.ism.ac.jp/src/base/R-3/R-3.2.2.tar.gz"
tar xvzf R-3.2.2.tar.gz
cd R-3.2.2
yum install gcc-gfortran libgfortran
./configure --with-readline=no --with-x=no
yum install java-1.7.0-openjdk-devel
make
make install

centOSの RにH2Oを入れる

> yum install libcurl-devel #パッケージインストールするのに必要
>R
>>install.packages("h2o")
接続先を選択する画面
HTTPS CRAN mirror

1: 0-Cloud [https] 2: Austria [https]
3: China (Beijing 4) [https] 4: China (Hefei) [https]
5: Colombia (Cali) [https] 6: France (Lyon 2) [https]
7: Iceland [https] 8: Russia (Moscow 1) [https]
9: Switzerland [https] 10: UK (Bristol) [https]
11: UK (Cambridge) [https] 12: USA (CA 1) [https]
13: USA (KS) [https] 14: USA (MI 1) [https]
15: USA (TN) [https] 16: USA (TX) [https]
17: USA (WA) [https] 18: (HTTP mirrors) ←これ選択

新たに選択肢が増えるので48:Japan(Tokyo)を選択

ダウンロード完了

用意する画像

画像検索を使って、ミッフィー、キティ、マイメロの画像をそれぞれ落としてきます。イラストのこともあれば、グッズの写真ってこともあります。そういうのも一旦お構いなしに、インプットとしてみます。

実際に学習するのは、一次元のベクトル

画像を学習させるとは言うものの、機械学習にかけるのは、1次元のベクトルです。画像をグレースケールにして(この時点でマイメロの色がなくなり輪郭がミッフィーとおなじになる)、100×100のピクセルを1万の一次元の要素にしちゃいます。ホントは色付きでもっとたくさんのピクセル数でやりたいところですが、めちゃくちゃ時間がかかるので、この辺で許さないと厳しかったです。

こんなcsvができあがります。 f:id:webdatareport:20151229145947p:plain

一番左の列がラベルで、ここにキティかミッフィーマイメロが入ります。それ移行の列は1万個のピクセルの値ですね。0~255までのグレースケールの値が入ります。

このデータを、学習用と判定用の両方作ります。

h2oのパラメータ

以下のパラメータを指定しました。

パラメータ 説明 今回のモデル
x モデルの文字列変数を含むベクトル
100*100ピクセルだったら2列目から1001列目
2:10001
y モデル内の応答変数の名前。ラベルが付いている列(たぶん) 1
training_frame 学習用データフレーム
activation 活性化関数を選択
Tanh
TanhWithDropout
Rectifier
RectifierWithDropout
Maxout
MaxoutWithDropout
Tanh
TanhWithDropout
Rectifier
RectifierWithDropout
Maxout
MaxoutWithDropout|Tanh
TanhWithDropout
Rectifier
RectifierWithDropout
hidden 隠れレイヤーのユニット数をベクトルで並べる
入力次元の1/10 or 1/100ユニットが普通
例:rep(20,5) 20ユニット×5層
rep(1000,4)
epochs 繰り返し回数
デフォルトだと1回
10~40

グレースケールに変換してベクトルに変換するRスクリプト

ImageMagicとRubyとかの組み合わせでもできちゃうけど、Rでもここまでできるってことで。

library(raster)

#グレースケール変換関数
as.grayscale.array<-function(file,flag){
  image<-brick(file)
  image.rgb<-getValues(image)
  rgbcol<-ncol(image.rgb)
  #元画像がカラーであればRGB値をグレースケールに変換
  if (rgbcol>1){
    image.bw<-image.rgb[,1]*0.21+image.rgb[,2]*0.72+image.rgb[,3]*0.07
    return (c(flag,image.bw))  
  #元画像が白黒であればそのまま    
  }else{
    return (c(flag,image.rgb))
  }  
}
#各画像のパスをキャラごとに格納
kity <- list.files("C:/rooter/pic_resize100/",pattern = "kity",full.names=T)
miffy <- list.files("C:/rooter/pic_resize100/",pattern = "miffy",full.names=T)
mymelo <- list.files("C:/rooter/pic_resize100/",pattern = "mymelo",full.names=T)

#評価用データ
pre_kity <- list.files("C:/rooter/pic_eva/",pattern="kity",full.names=T)
pre_miffy <- list.files("C:/rooter/pic_eva/",pattern="miffy",full.names=T)
pre_mymelo <- list.files("C:/rooter/pic_eva/",pattern="mymelo",full.names=T)

#3つのキャラクターを1つのベクトルに結合
res.dl<-rbind(t(sapply(kity, as.grayscale.array , "kity")),
              t(sapply(miffy, as.grayscale.array , "miffy")),
              t(sapply(mymelo, as.grayscale.array , "mymelo")) )

#評価用データのベクトル生成
pred.dl<-rbind(t(sapply(pre_kity, as.grayscale.array , "kity")),
              t(sapply(pre_miffy, as.grayscale.array , "miffy")),
              t(sapply(pre_mymelo, as.grayscale.array , "mymelo")) )

#学習用・評価用データをデータフレーム化
res.dl<-data.frame(res.dl)
pred.dl<-data.frame(pred.dl)

#出力
write.csv(res.dl,"C:/rooter/training_data.csv",row.names=FALSE, quote=TRUE)
write.csv(pred.dl,"C:/rooter/prediction_data.csv",row.names=FALSE, quote=TRUE)

学習と評価のRスクリプト

library(h2o)

#あらかじめJava SE 7をダウンロードしておく
localH2O <- h2o.init(ip="localhost" , nthreads = -1)

#データファイルをH2Oクラスに変換
#学習用
target<-read.csv("training_data.csv")
target<-data.frame(target)

act<-"TanhWithDropout"
epo<-40

start<-proc.time()

#学習
res.dl<-h2o.deeplearning(x = 2:10001,
                         y = 1,
                         training_frame = as.h2o(target),
                         activation = act,
                         hidden = rep(1000,4),
                         epochs = epo,
)

end<-proc.time()

#評価用
pred.dt<-read.csv("prediction_data.csv")

#予測
prediction<-h2o.predict(object = res.dl , newdata = as.h2o(pred.dt))

#精度を算出
pred<-as.data.frame(prediction)
result.ar<-t(rbind(pred.dt[,1],pred[,1]))


#正答率を計算
kity.num<-0
miffy.num<-0
mymelo.num<-0
num<-0
for(i in 1:nrow(result.ar)){
  if(result.ar[i,1]==result.ar[i,2]){
    if(result.ar[i,1]==1){
        kity.num<-kity.num+1
    }
    if(result.ar[i,1]==2){
        miffy.num<-miffy.num+1
    }
    if(result.ar[i,1]==3){
        mymelo.num<-mymelo.num+1
    }
    num<-num+1
  }
}

kity.res<-format(kity.num/sum(result.ar[,1]==1), nsmall=5)
miffy.res<-format(miffy.num/sum(result.ar[,1]==2), nsmall=5)
mymelo.res<-format(mymelo.num/sum(result.ar[,1]==3), nsmall=5)

res<-format(num/nrow(result.ar),nsmall=5)

#出力
pred.ar<-data.frame(chara=c("kity","miffy","mymelo","all","time"), pre=c(kity.res,miffy.res,mymelo.res,res,as.character(end-start)[3]))
write.table(pred.ar,paste("log/",act,"/epo",epo,".txt",sep=""))
write.csv(pred,paste("log/",act,"/epo",epo,".csv",sep=""))

僕たちはなぜwebデータアグリゲ―ション事業をやっているのか?その① text by maeda

2014年夏、こんな状況でした。
 
・センシング技術の発達により多くのデータ計測が可能になり、様々な領域でビッグデータが発生している。web世界でもしかり。
・データ好きな経営者、事業責任者が少数ながら存在する(増えつつある)
データ好きエグゼクティブから社内外のデータを収集分析したいというニーズが発生している
・社内にデータ解析、戦略立案のためにデータサイエンティストという新たな職種を置く企業が増えてきた。
 
といった環境要因がありました。
クロール好きのCTOとエンジニアを要するわが社は昨年、データクローリング技術で
どんな事業ができるだろうと社内ブレストを重ねていました。
企業向けクロールデータ納品ビジネスとコンシューマ向け
スマホアプリビジネスの2軸で検討を続けました。(今も検討、検討です。)
 
企業向けの方はちょっとあたってみようということでwebデータのクロール収集を
ネタに知り合いの会社をいくつか回ってみました。(有名どころの会社です)
そこでデータ好き経営者や事業責任者に接触する機会があり、結構ニーズがある
ことに気付きました。用途は競合分析・定点観測・データベース構築などなど。
しかも社内での収集に後ろ向きで外でやりたいということでした。
これはニーズが結構ありそうだぞ、しかもここ数年ニッチだけど市場大きくなりそう
と考えたのでした。
 
そこで僕たちはwebデータアグリゲ―ション事業を立ち上げることになったのです!
 
つづく

健康食品の「成分ランキング」を作ってみる

私達は日常的にweb上の面白そうなデータを収集、分析しています。なんとなくの想像で思ってることも、実際に集計すると違って見えます。今回は健康食品の「成分ランキング」です。

 

健康食品には成分のブームがあります。コラーゲンとか、プロポリスとか。厳密に成分に分解すると、タンパク質とか脂質とかになるので、消費者がラベルとして認識する粒度の成分のワードがあります。

続きを読む