よーぐるとのブログ

技術ネタを中心。私的なことを徒然と綴っていきます。

FGO英霊クラス判定器

久しぶりの更新になりました. 2016年は定期的に更新しようと意気込んでたにも関わらず,完全放置になってしまっていました. 今年の目標は「技術に対して積極性を持つこと」「精神的な余裕を持つこと」です.去年は修論のために専門の技術を追いかけるのに精一杯だったので,今年はより多方面に関心を持って過ごしたいと思います.

ということで2017年ブログ第1弾は「FGO英霊クラス判定」にしました. 概略を言うとFate Grand/Order(FGO)というスマホゲームのキャラがどのクラスに属するかを機械学習で識別しよう,という試みです.

実装時の言語はpythonで,深層学習ライブラリのKerasと,機械学習・確率統計で誰もがお世話になるであろうライブラリScikit-Learnを使用します.ツール・ライブラリのインストールについてはこの記事では触れません.

注:このブログではFGOのキャラクター名を例に出して説明するため,Fate作品の(多少の)ネタバレに繋がる恐れがあります.これからFate作品を楽しもうと思っている方は,お気をつけください.

動機

画像系の深層学習に触れてみようと思ったのがきっかけでした.研究では音声・言語を扱っていたんですが,画像方面には全く縁がなかったので.

画像分野の深層学習研究では,近年CNN(Convolutional Neural Network)によるオブジェクト認識のタスクにおいて著しい発展が見られています.試しに調べてみるとアニメ画像におけるキャラクター認識というのは結構やられていて,ラブライブごちうさのキャラクター認識を行ったという記事が見つかります.

この記事では,キャラクター認識ではなくキャラクター属性認識をしようと思います.スマホゲームのFate Grand/Orderにスマホゲームに登場する英霊(キャラクター)の属するクラスの認識です.単純なキャラクター認識よりも難易度が高そうであるのと,Twitterのアイコンなどを使って自分がどのクラスに属するかを推定する,といったアプリケーションへの応用が見込めることが大きな理由です.

システムの概略図としては次のような形になっています.キャラクターの画像を学習済みのVGGnet(CNNの一つ)に入力することでその中間層から特徴量を抽出し,それを用いてRandom Forestなどの識別モデルの学習を行います. ここは機械学習に馴染みのない人にとっては聞きなれない単語だらけだと思いますが,後ほど説明します.

f:id:yoghurt1131:20170131102233p:plain:w450

タスク設計

最初にFGOについてです.Fate作品,ゲームの内容については特にこの記事と関係が無いので触れません. 大事なのは登場するキャラクターが歴史,神話上に登場する過去の英雄(英霊)で,基本的に7つのクラスのどれかに割り当てられていることです.

7つのクラスは次のとおりです.

  • セイバー(剣)
  • アーチャー(弓)
  • ランサー(槍)
  • ライダー(騎)
  • キャスター(術)
  • アサシン(殺)
  • バーサーカー(狂)

これに加えて,エクストラクラスというのが存在します.なので,正確には全部で8クラス存在します.

例えば,次のイラストはアーサー王であり,セイバーのクラスに属しています.(「性別・・・」とかそういうツッコミは無しです.)

f:id:yoghurt1131:20170129230557p:plain:w150

それぞれの英霊は自身に由来のあるクラスに属することが多いです. またイラストでも,剣を持っていたり弓を持っていたりと,それなりの特徴を備えています.

一方で例外もあります.

f:id:yoghurt1131:20170129230535p:plain:w150

このキャラクターは弓を持っていないですがアーチャーのクラスに属します. (詳細はFate stay/nightを見てください)

今回のゴールは、英霊の画像を入力してその英霊のクラスを推定するシステムを作ることです. 各キャラクターとそれが属するクラスの関係は曖昧ですが,その曖昧さを機械学習の仕組みを利用してモデル化することを目的とします.

データ収集・前処理

最初に画像の収集です.公式のゲームイラストを実際のゲームや攻略サイトなどから集めました.FGOではキャラクターのイラストが立ち絵固定のため,アニメのように1コマ1コマを学習データにするということができません.各キャラクターごとに進化後のイラストが3枚存在するためそれも収集し,以下の500枚程度の画像が集まりました.

クラス 枚数
セイバー 76
アーチャー 69
ランサー 85
ライダー 64
キャスター 96
アサシン 72
バーサーカー 69
エクストラ 28

かなり少ないですが他に集める手段となると創作二次絵などになってしまうため,とりあえずこのデータのみを使います. 本当でしたら数千〜数万枚のイラストが必要なところです.

使用するニューラルネットは入力の画像サイズが固定で,224x224になっている必要があります.そのため集めたデータをニューラルネットに入力できる形に整形する前処理を行います.次の三つの処理を行いました.

1.枠のトリミング 持ってきた画像には,外側の枠に英霊のクラスや,ゲーム内でのランク(星の数),強さなどキャラクターの画像以外の情報が含まれています. そのため,上下の枠をトリミングしキャラクターのみが映るようにします.

2.キャラクターのトリミング 使用するニューラルネットは入力画像が正方形でないといけないため,1でトリミングした画像を、横幅に合わせてトリミングします.この際,画像の下側は多少見切れてしまいます.

3.リサイズ 224x224に画像をリサイズします.

f:id:yoghurt1131:20170131110702p:plain:w400

左が元画像,右がトリミング・リサイズを行った画像です.

特徴量抽出

Convolutional Neural Network(CNN)という,画像系で近年素晴らしい性能を叩き出しているニューラルネットワークを用います. CNNの説明はこの記事が分かりやすかったので気になる人は参照してください. 今回はCNNを学習モデルとしてではなく,特徴量抽出器として用います.オブジェクト認識で用いられているCNNは,その中間層の出力が物体を識別するのに有効な特徴を持つことが知られています. そこで,学習済みのCNNに画像を入力した際の中間層の値を抽出し,それを画像の特徴量とします. 特徴量とはその名の通りデータの特徴を表すベクトルになります.

学習済みのCNNに,VGGnetというニューラルネットワークを用います.ILSVRC2014というオブジェクト識別,分類のコンテストにて非常に良い性能を叩き出したニューラルネットワークです.ILSVRCは,ある画像が入力された際にそこに映っている物体(オブジェクト)が用意された1000カテゴリ(クラス)のどれであるかを当てるタスクです.

VGGnet

VGGnetについて簡単に説明します.最初のシステム概略図でちらっと登場しましたが,VGGnetは以下のような構造をとっています.VGGnetに限らずCNNは畳み込み層(Convolution Layer)とプーリング層(Pooling Layer)の繰り返しによって構成されます.Conv1, Pool7などと書かれた枠がそれに相当します.ざっくり説明すると,畳み込み層,プーリング層によって画像の様々な構造(例:「斜めの線を持つか?」)を捉えることができます. その後,全結合層(Fully-Connected Layer)を複数配置します.FC14~16がそれに相当します.FC16で,入力画像は1000カテゴリのどれか,という最終的な結果が出力されます. プーリング層は学習によってパラメータが変化しないため,層としてカウントされないようです.そのため畳み込み層(Conv)と全結合層(FC)の数をニューラルネットワークの層の数としています.この例は,16層のVGGnetを表しています.

f:id:yoghurt1131:20170131103516p:plain:w800

VGGnetを用いた特徴量抽出

Keras版VGGnetの学習済みモデルはこちらから入手しました.ついでに重みも取ってきます. これを用いて,画像データから特徴量を抽出します.以下その際のソースコードです.

# モデルの読み込み
model = vgg16_keras.VGG_16('vgg16_weights.h5')
# コンパイル
sgd = SGD(lr=0.1, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(optimizer=sgd, loss='categorical_crossentropy')
# 最終層の手前のフルコネクション層.4096ユニット.
get_fc15_layer_output = K.function([model.layers[0].input, K.learning_phase()], [model.layers[-5].output])
# 画像の読み込み
img = cv2.resize(cv2.imread(image_file, cv2.IMREAD_COLOR), (224, 224)).astype(np.float32)
# 平均値を引く
img[:,:,0] -= 103.939
img[:,:,1] -= 116.779
img[:,:,2] -= 123.68

img = img.transpose((2,0,1))
img = np.expand_dims(img, axis=0)
# 中間層の値を出力
fc15_layer_output = get_fc15_layer_output([img, 0])[0]

Kerasで中間層の値を取り出す関数は上記のようにK.functionを使って設計することができます. 今回はFC15という最終層の2つ手前,4096ユニットの全結合層から特徴量を抽出しました.

識別モデルの作成

上で抽出した特徴量を用いて,識別モデルを学習します.ある英霊画像の特徴量Xが入力された際に,その英霊の属するクラスYを出力するモデルとなります.学習・識別にはRandomForestを用いました.RandomForestは弱識別器(決定木と呼ばれる)を複数作成し,その結果を統合して値を出力する手法です.動作が非常に軽くて使いやすいのが魅力ですかね.

今回ですが,いきなり8クラスは難しそう(というか難しかった)ので,セイバー,ランサー,アーチャーの3クラスで識別を行いました. この3クラスにした理由は,どのクラスの英霊のイラストも,そのクラスを象徴する武器が描かれているためです. セイバークラスの英霊はだいたい剣を持っている,ランサークラスの英霊はだいたい槍を持っている,アーチャークラスの英霊はだいたい弓を持って・・・いないかもしれませんが持っている人もいます.まあそういった特徴的な画像を用いないと識別が困難だろうという予想のもと選びました.他のクラスは,ライダーなのに剣を持っているとか,ややこしいのが多かったのでそもそも難しそうなんですよね.

学習データは上記の画像計230枚でした.このデータセットを20分割し,19個をモデルの学習用,1個を性能を確認するためのテスト用として,20分割交差検定を行いました.最終的な精度は20回テストした平均の精度を採用します.

コードは以下のとおりです.

import numpy as np
import os
from sklearn.cross_validation import KFold
import sys
from data_loader import DataLoader
from rf_trainer import RandomForestTrainer

if __name__ == '__main__':
    # 対象クラス
    train_servant = ['saber', 'archer', 'lancer']

    data_loader = DataLoader('config.ini')
    trainer = RandomForestTrainer()

    X = [] # データ
    Y = [] # ラベル

    # 学習データ読み込み
    for name in train_servant:
        x, y = data_loader.get_servant_feature(name)
        X += x
        Y += y
    
    # K分割交差検定
    n_folds = 20
    kf = KFold(len(X), n_folds=n_folds)
    total_score = 0
    for train_index, test_index in kf:
        model = trainer.train(np.array(X)[train_index], np.array(Y)[train_index])
        score = trainer.predict(model, np.array(X)[test_index], np.array(Y)[test_index])
        total_score += score

    print("Score: %s" % round(total_score / n_folds, 3))

DataLoader,RandomForestTrainerは自作のクラスで以下のようになっております.

# -*- coding: utf-8 -*-
import glob
import numpy as np
import os
import ConfigParser


class DataLoader:
    def __init__(self, config_file):
        self.servants = {
                'saber': 0 , 'archer': 1, 'lancer': 2,
                'rider': 3, 'caster': 4, 'assassin': 5,
                'berserker': 6, 'extra': 7
                }
        cfg = ConfigParser.SafeConfigParser()
        cfg.read(config_file)
        self.data_root_dir = cfg.get('Data', 'root_dir')
        self.feature_type = cfg.get('Data', 'feature_type')
        self.data_ext = cfg.get('Data', 'data_ext')

    def get_servant_feature(self, servant_class_name):
        """サーヴァントのクラスを指定してデータを取ってくる
        params
         - servant_class_name: 7騎のうちのどれか
        """
        if not servant_class_name in self.servants.keys():
            print("Error! There is no servant");
            return None, None
        X = []
        Y = []
        servant_data_dir = os.path.join(self.data_root_dir, servant_class_name)
        file_pattern = '%s/*_%s.%s' % (servant_data_dir, self.feature_type, self.data_ext)
        data_files = glob.glob(file_pattern)
        for data_file in data_files:
            data = np.loadtxt(data_file)
            X.append(data)
            Y.append(self.servants[servant_class_name])

        return X, Y
# -*- coding: utf-8 -*-
from sklearn.ensemble import RandomForestClassifier
from sklearn.externals import joblib

class RandomForestTrainer:
    def __init__(self):
        self.model = RandomForestClassifier()

    def train(self, train_data, train_label, output_dir_path=None):
        self.model.fit(train_data, train_label)
        if not output_dir_path is None:
            joblib.dump(model, output_dir_path)
        return self.model

    def predict(self,model, test_data, test_label):
        score = self.model.score(test_data, test_label)
        return score

検証

上記のスクリプトを実行して得られた結果,正解率は残念ながら33.1%程度でした.低い.ほぼチャンスレートですね.

一応,試しに使ってみます.3クラスに属する英霊の画像は学習で使ってしまったので,他のクラスの英霊がもし3クラスのどれかに割り当てられるとしたら・・・という想定でテストを行います.

保存したモデルを読み込み,画像から得られた特徴量をモデルに入力して出力のクラスを得ます.出力されるのは0〜2の数字で,各クラスに対応しています.

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.externals import joblib

model = joblib.load('model_path') # modelの保存場所
data = np.loadtxt('feature_vector_path') # 特徴量の保存場所
model.predict(data) # 識別
# 0→セイバー,1→アーチャー,2→ランサー

例1

まずは主人公のパートナーであるマシュ・キリエライト.このキャラクターはエクストラクラスに属します.

f:id:yoghurt1131:20170131150118p:plain:w150

data = np.loadtxt('data/feature/extra/0000_fc15.dat') # パスはデータの場所
model.predict(data) # => array([1])

アーチャー判定が出ました.ふむ.剣っぽいものを持っているのでどっちかっていうとセイバーらしさがあると思うんですけどね.

例2

続いて2例目.これは直感に合致した例です. バーサーカーの同じキャラなんですが,進化状態によって持っている武器が違います. 左は弓を持ち,右はそれに加えて剣を持っています.

f:id:yoghurt1131:20170131151319p:plain:w150f:id:yoghurt1131:20170131151321p:plain:w150

data = np.loadtxt('data/feature/berserker/0062_fc15.dat') # 左の絵
model.predict(data) # => array([1])

data = np.loadtxt('data/feature/berserker/0063_fc15.dat') # 右の絵
model.predict(data) # => array([0])

左の絵はアーチャー,右の絵はセイバーに分類されました.おぉ,それっぽい.

例3

続いて3例目,このキャラクターはアサシンのクラスなんですが,剣を持っています. セイバーに分類されてほしいところ・・・.

f:id:yoghurt1131:20170131150657p:plain:w150

data = np.loadtxt('data/feature/assassin/0041_fc15.dat')
model.predict(data) # =>array([1])

アーチャーでした.難しい・・・・・.

考察

上手く行かなかった理由について少し検討を行いました. 学習データが少ないのは自明なのですが,そもそも学習に用いている特徴量が識別に有効なものになっているかを調べます.

学習に用いた4096次元の特徴量はそのままでは簡単な比較ができないため,主成分分析(PCA:Principal Component Analysis)を用いて2次元に圧縮し,可視化してみました.PCAはデータ分析でよく使われる手法で,データ群をまとめてその主成分,そのデータをよく表現する成分を取り出します.

学習に用いた特徴量が識別的なものであれば,PCAを行った結果クラスごとにある程度分布が分かれているような図が得られるはずです.

ソースコードと可視化の結果です.

# -*- coding: utf-8 -*-
"""PCAでFC15特徴量を2次元に圧縮,プロットする"""

import glob
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

from data_loader import DataLoader

if __name__ == '__main__':
    servants = [
            'saber', 'archer', 'lancer',
            'rider', 'caster', 'assassin',
            'berserker', 'extra'
            ]
    X = []
    Y = []
    data_loader = DataLoader('config.ini')
    for name in servants:
        x, y = data_loader.get_servant_feature(name)
        X += x
        Y += y

    N = 2 # 圧縮する次元数
    pca = PCA(n_components=N)
    pca.fit(X)

    saber = np.array([x for i, x in enumerate(X_pca) if Y[i] == 0])
    archer = np.array([x for i, x in enumerate(X_pca) if Y[i] == 1])
    lancer = np.array([x for i, x in enumerate(X_pca) if Y[i] == 2])

    fig, ax = plt.subplots()
    ax.plot(saber[:,0],  saber[:,1],  'b.', label='saber')
    ax.plot(archer[:,0],  archer[:,1],  'r.', label='archer')
    ax.plot(lancer[:,0],  lancer[:,1],  'g.', label='lancer')

    ax.set_title("PCA for fgo")
    ax.legend(numpoints=1)

    # 表示
    plt.show()

f:id:yoghurt1131:20170131154923p:plain:w1000

プロットした点が各データ,色がクラスを表しています.x軸とy軸が2次元に圧縮した際の各データの値です.

わー,見事にバラバラ.もちろん次元圧縮しているのでこの図が特徴量の全てを語っているわけではないのですが,ここまで混ざっていると識別は困難ですね.

まとめ

ということで,この記事ではFGOのキャラクターのクラス識別を取り上げました.手始めに3クラスで試しましたが,精度は全然ダメです.識別精度は上げるには,データ量を増やすこと,特徴量についてより良いものを検討すること,の2種類が課題として挙げられるでしょうか.特に後者については,実物のオブジェクト認識タスクで学習したCNNではなく,アニメ画像等で学習したCNNを用いるなどの工夫が必要かもしれません.いずれ8クラスへの拡張,アプリケーションの開発なども取り掛かりたいと思います.

PythonでMySQL(Mac,Ubuntu)

Mini Tips.

MySQL-pythonを使用します.

MySQLdb User's Guide

Mac(Yosemite)

pipでインストールするだけ

pip install MySQL-python

Ubuntu(14.04)

こちらも同様

sudo pip install MySQL-python

と思ったらなにやらエラーが

EnvironmentError: mysql_config not found

----------------------------------------
Cleaning up...
Command python setup.py egg_info failed with error code 1 in /tmp/pip_build_fukuoka/MySQL-python
Storing debug log for failure in /home/fukuoka/.pip/pip.log

libmysqlclient-devがないらしい

sudo apt-get install libmysqlclient-dev
sudo pip install MySQL-python

実行テスト

次のようなテーブルをサンプルとして扱います

mysql> desc goods;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(10) | YES  | MUL | NULL    |                |
+-------+-------------+------+-----+---------+----------------+

mysql> select * from goods;
+----+-------+
| id | name  |
+----+-------+
|  2 | chair |
|  1 | desk  |
|  3 | pc    |
+----+-------+
"""mysql_test.py"""
# -*- coding: utf-8 -*-
import MySQLdb
connection = MySQLdb.connect(db="test",user="goods")

cursor = connection.cursor()
sql = "select * from goods"
cursor.execute(sql)

# 1行だけ取得
first_row = cursor.fetchone()
print first_row
print "=" * 10

# (残り全行取得)
result = cursor.fetchall()
for row in result:
    print row

cursor.close()
connection.close()

実行結果

python mysql_test.py
(2L, 'chair')
==========
(1L, 'desk')
(3L, 'pc')

Reveal.js + Markdownで簡単にプレゼン資料を用意する

プレゼン資料を用意するの毎回時間かかるし結構めんどくさいなー。って思ってたらReveal.jsなるものを見つけたので、早速使ってみました。 少し前に流行ってた?みたい。

導入

本家のgithubが丁寧に書かれているのでそれに従ってセットアップ。外部からMarkdownファイルを読み込めるようにしたいため、Node + Grantのフルセットアップを行います。

  1. Node.jsをインストール
  2. Gruntをインストール
  3. reveal.jsをクローンしてきて、インストール。
git clone https://github.com/hakimel/reveal.js.git
cd reveal.js
npm install
  1. grunt servehttp://localhost:8000にアクセスするとスライドが見れます

Markdownの埋め込み

bodydiv class="reveal"内で以下のように記述することで、md/test.mdを読み込んでくれます。

<body>
  <div class="reveal">
<!--ここから! -->
    <!-- Any section element inside of this container is displayed as a slide -->
    <div class="slides">
      <section data-markdown="md/test.md"
            data-separator="---$"
            data-separator-vertical=">>>$"
            data-transition="zoom">
        <script type="text/template">
        </script>
      </section>
    </div>
<!--ここまで! -->
  </div>

test.mdは以下

# これはReveal.jsのテストです

Hello, reveal.js!

---

## Reveal.js& Markdownで
## スライド作り
***

 * Markdownでスライドを作ります

 * index.htmlにファイル名を指定して埋め込みます

 * たのしい✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌ 

>>>

## ここは補足ページなどになります

---

## 利点

* とにかくMarkdownがラク
  * Markdown大好きマンには嬉しい

* 動きがかっこいい

---

## あなたもRevealjs + Markdownでプレゼン作成のストレスを軽減しよう!

テーマの変更

メタタグのthemeを変更することでスライドのテーマを変更できます。

<link rel="stylesheet" href="css/theme/serif.css" id="theme">

各種設定

sectionタグのdata-transitionを変えることでスライドの遷移方法を変えることができます。

  <section data-markdown="md/test.md"
        data-separator="---$"
        data-separator-vertical=">>>$"
        data-transition="zoom">
  • data-separator ページ送りを正規表現で表します。上記例だと---改行でページ送りができます。

  • data-separator-vertical 縦方向のページ送りの正規表現を指定します。

  • data-transition ページ遷移のアクションを指定します。

デモ

ishinfukuoka.github.io

本家デモ

reveal.js – The HTML Presentation Framework

Juliusによる音素アライメント(音素セグメンテーション) on MacOSX(Yosemite)

オープンソースの汎用大語彙連続音声認識エンジン、Juliusを用いて音声認識ではなく強制音素アライメントを行います。 音素アライメントとはある音声ファイルに含まれる音素(分節音)の開始・終了時間を自動認識する技術となります。

事前に必要な物

Homebewを使っていれます.

  • portaudio
    • brew install --universal portaudio
  • flex
    • brew install flex

Julius本体のインストール

Julius

こちらから最新版のJuliusをダウンロード。今回は4.3.1。

解凍・コンパイル

tar xvzf julius-4.3.1.tar.gz
cd julius-4.3.1/
env CFLAGS='-arch i386' LDFLAGS='-arch i386' ./configure --disable-mac-universal
make
sudo make install

単語・音素セグメンテーションキットのダウンロード

githubから落としてきます

git clone git@github.com:julius-speech/segmentation-kit.git

segmentation-kit/直下にあるsegment_julius.plを修正します。segment_julius.plの45~50行目のelse文の箇所を変えます。

## julius executable
if ($^O =~ /MSWin/){
    $juliusbin=".\\bin\\julius-4.3.1.exe";
} else {
    $juliusbin="./bin/julius-4.3.1";
}

$juliusbin="./bin/julius-4.3.1"; ここのパスをインストールしたjuliusの実行場所を確認し、変更します。

$which julius
> /usr/local/bin/julius
## julius executable
if ($^O =~ /MSWin/){
    $juliusbin=".\\bin\\julius-4.3.1.exe";
} else {
    $juliusbin="/usr/local/bin/julius";
}

動作確認

segmentation-kit/wavフォルダにあるsample.wavsample.txtを使用します。ちなみにwavファイルは16kHzである必要があるようです(変えられるのかな?)。 sample.txtのテキストはこんな感じ。ひらがなで書きます。

きょーわいいてんきだ

wavファイルと書き起こしのtxtファイルを用意し、segment_julius.plを実行します。

perl segment_julius.pl wav/sample.wav wav/sample.txt

実行された音素アライメントの結果がwav/sample.labに書き出されます。

0.0000000 0.2425000 silB
0.2425000 0.3325000 ky
0.3325000 0.5725000 o:
0.5725000 0.6925000 w
0.6925000 0.7725000 a
0.7725000 0.8925000 i
0.8925000 0.9925000 i
0.9925000 1.0825000 t
1.0825000 1.1825000 e
1.1825000 1.2825000 N
1.2825000 1.4025000 k
1.4025000 1.4525000 i
1.4525000 1.5125000 d
1.5125000 1.6125000 a
1.6125000 2.0525000 silE

参考

http://shower.human.waseda.ac.jp/~m-kouki/pukiwiki_public/24.html#cb2d81f0

音素セグメンテーションキットのダウンロード

音声認識 Julius 4.2.3 + Mac OSX Mountain Lion 10.8.4 でビルドする方法 - Qiita

ダッシュで2015年を振り返る

気がついたら12月31日で、こたつで紅白を見ている自分に気がついたので、ささーっと2015年を振り返ってみようと思います。お酒を飲みながら書いている間に完全に私的備忘録になっちゃったけどまあそれはそれで。

月別振り返り

1月

新年早々卒論がゲシュタルト崩壊を起こしていました。Twitterを振り返ると1月3日に卒論発表会の原稿を出して「よいお年を!』などと呟いてます。なんだか大変そうです。

2月

卒論を出し終わって平穏な月でした。学生最後のサークル合宿に行ったり、卒業旅行で富山に行ったりしました。バイト先ではRails+knockoutjsを学んでフロントエンドがっつり開発マンになっていました。

3月

大学生最後の春休み、旅行やらバイトやら飲み会やらで充実した日々を過ごす傍学会の原稿を書くなどしていました。旅行で台湾に行ったのですが、初海外でした。現地の人みんな日本語で話しかけてくれるので海外きた感あんまなかったけど、初海外は初海外です。

4月

大学院生になったという実感はあまりなく、授業をまた受けるようになったというくらいでした。Twitterを遡ると小川一水「天冥の標」シリーズにどハマりしていました。個人的な今年一番のヒットはこれです。いつかブログにまとめたいなとか思ったりします。

5月

5月は月末の学会準備に追われていました。学会では函館に行き、初めて発表をしました。函館は先生にご馳走になったお寿司が美味しかったし、一人で見た函館山からの夜景はとてもきれいでした。学会サイコー。

6月

学会が終わった反動からか、ゆるゆるな一ヶ月を過ごしていました。有意義だったことといえば、前期途中から始まったFSNLP勉強会に参加し出して、古き良き自然言語処理周りのお勉強をしたことですかね。

7月

(大学院)1年生なので、そこそこ試験勉強とかレポートに追われていたようです。月末に行ったKURAND SAKEマーケットの日本酒飲み放題がとても美味しかったことを覚えています。

8月

研究室にいるロボットのデモを行うことになり、その開発に追われていました。Java力の復活。以前のBloggerからはてなBlogに移行したのもこの時期だったような。あとこの時期はプロコンをやり始めて、毎週Atcorderに取り組んでいました。最近やってないなあ。

9月

インターンで2週間大阪に行っていました。インターン自体よりも2週間会社の寮で一人、という非日常で生き生きしていました。週末は京都や大阪を観光したし、期間限定社会人を全力で満喫しましたね。

10月

スケジュール帳とか見返してみてもあんまり予定ないですね・・・何してたんだろ?

11月

開発系ではtmuxを知り開発効率が格段に上がった月でした。もっと早くに知っておきたかった。月末に社会人の友人を訪ねて山梨旅行に行ったりしました。紅葉にはちょっと遅かったけど、昇仙峡は綺麗だったし、ワインは美味しかったし良い息抜きになりました。今年は息抜きしてばっかですね。

12月

なう。今年は忘年会をあまりしていません。「忘年会」って語感から一年を忘れるための行事かと思いきや、ネットを調べると「一年を忘れないための会」とか、「嫌なことを忘れる会」とか色んな意見があってちょっとわからなくなりました。まあ、忘れたいことを銘々に忘れればいいんじゃないでしょうか?

分野別振り返り

研究

卒論もなく、修論もない修士一年という立場を利用してかなり自由に色んなことをさせてもらいました。その分進捗がgg... 来年は修論が書けるようにがんばらねば。

趣味

ボードゲーム

ボードゲームにより一層はまった年でした。ゲームマーケットに2回共参加して散財し、幾つかボードゲーム会にも顔を出したりしました。購入したのはパーティーゲーム系が多く、中でも「私の世界の見方」は4年越しに手に入れることができて満足でした。

ミステリ・SF

今年は去年以上に本が読めなくなってしまいました。電車の中で寝てしまうというのが主な原因です。 一番のヒットは上にも書いた小川一水「天冥の標」。全10巻まであって最近9-1が出たのですが、新刊が楽しみでなりません。他にSFだと法条遥「リライブ」、スタニスワフ・レムソラリス」が面白かったですかね。 ミステリで一番は「戦場のコックたち」。

開発

去年アルバイトでRailsの開発をするようになって、それはそれで実装力はついたのですが、今年は「より綺麗なコードを書く」ということを意識して開発に臨めた一年だったかなと思います。周りの方々の影響って大きい。 また、いろんな組織の中での大きなプロジェクト開発に携わる機会が多かったため、そういった保守性がいかに大事であるか、というのを認識できたというのもあります。

まとめ

良いお年を!

TensorFlow Tutorial でニューラルネットワークを使う[Python]

f:id:yoghurt1131:20151112021353j:plain

Googleが発表した人工知能ライブラリTensorFlowを動かしてみました。 チュートリアルを追いながら、ニューラルネットワークの勉強(復習)をしたので理解は深まりましたが、誤りや間違いもあると思います。見つけた場合はご指摘ください。

Download & Install

MacOSでのダウンロードは次のようになります。Ubuntu/LinuxだとGPU使うバージョンもダウンロードできる模様 ダウンロードページ

# Only CPU-version is available at the moment.
$ pip install https://storage.googleapis.com/tensorflow/mac/tensorflow-0.5.0-py2-none-any.whl

動作確認

$ python

>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!')
>>> sess = tf.Session()
"can't determine number of CPU cores: assuming 4
I tensorflow/core/common_runtime/local_device.cc:25] Local device intra op parallelism threads: 4"
>>> print sess.run(hello)
Hello, TensorFlow!
>>> a = tf.constant(10)
>>> b = tf.constant(32)
>>> print sess.run(a+b)
42

動作はしましたが僕のマシンでは警告がでました

>>> sess = tf.Session()
[f:id:yoghurt1131:20151112021353j:plain]can't determine number of CPU cores: assuming 4
I tensorflow/core/common_runtime/local_device.cc:25] Local device intra op parallelism threads: 4

CPUのコア数がわからないとのこと。どこかで設定すればいいのかな?

MNIST For ML Beginners

ページに行くと二つのチュートリアルが用意されている

  • intro_mnist:初心者向け
  • expert_mnist:Machine Learning経験者・上級者向け

初心者向けのチュートリアルでやります。 このチュートリアルではMNISTのデータセットを用いて手書き数字を識別するための識別器を作成します。

MNIST

Download Data Set

公式ページから落としてきてもいいんですが、Googleさんがデータセットを取ってくるPythonプログラムを用意してくれています。

Call Data

これを使ってデータセットを呼び出すことができます

import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
mnist.train # 訓練データ
mnist.test  # テストデータ

画像データやそのラベルは次のコードで呼び出せます

mnist.train.images # 画像データ
mnist.train.labels   # ラベルデータ

画像は28x28ピクセルで表されているため、一つ一つの画像データ・ラベルデータはサイズが28x28=784の配列で表現されています。これは多次元配列ではなくフラットな一次元の長さ784の配列になっています。 mnist.train.imagesを呼び出すと画像60000枚x784ピクセルの2次元配列が呼び出されることになります。 ラベルデータは数字0〜9の10次元のベクトルの中で、正解の数字に1が立つようなベクトルになっています。例えば、0が書かれた画像のラベルデータは[1,0,0,0,0,0,0,0,0,0,0]となります。

Softmax Regressions

最終的には、画像を認識させてそれが0〜9のどの数字に当てはまるかを確率値で出力します。活性化関数にはソフトマックス関数を用いています。ソフトマックス関数は多クラス分類における出力値を正規化するような役割を果たしています。

ソフトマックス関数では、 1. 出力のevidenceを求める 2. それを確率値にする という2つのステップがあります。

まず、ニューラルネットワークの出力evidenceは、次のような形で求められます。Wは重み・bはバイアスになります。

 {evidence_i = \sum_j W_{i,j} x_j + b_j}

これにソフトマックス関数をかけて確率値yを得ます。

 {y = softmax(evidence_i)}

イメージはこんな感じ

f:id:yoghurt1131:20151111142323p:plain

数式で表すならばシンプルに次のように書けます。

 { y = softmax(Wx + b) }

Implementing the Regression

インストール時に書きましたがTensorFlowのライブラリは次のように読み込めます。

import tensorflow as tf`

次にデータの入れ物(placeholder)を用意します

x = tf.placeholder("float", [None, 784])

重みとバイアスも宣言が必要です。これらの値は計算中に頻繁に変わることが予想されるのでTensorFlowのVariableというメソッドで扱います。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

モデルは次のように呼び出せます。

y = tf.nn.softmax(tf.matmul(x,W) + b)

Training

コスト関数としてクロスエントロピーを導入します。コスト関数は学習された重みwとバイアスbがどれだけ目指す値に近づいたかの指標となります。

 {H_{y'}(y) = - \sum_i y_i' \log y_i }

yは予測確率分布、y'は正解のベクトルです

クロスエントロピーを導入するために、正解ラベルを入れるためのプレースホルダーを定義し、それを使ってクロスエントロピーの式を定義します。

y_ = tf.placeholder("float", [None,10])
cross_entropy = -tf.reduce_sum(y_*tf.log(y))

このコスト関数の値を最小化する重みとバイアスを見つけるのが、最終的な目標となります。 コスト関数の最小化は勾配降下法を用いて行い、プログラムでは次のように記述されます。

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

tf.train.GradientDescentOptimizer(0.01)が勾配降下法でありその際の学習率は0.01としています。 後に述べますが実際には計算量の関係から確率的勾配降下法というのを用いて全てのデータを用いてコスト関数の最小化を図るのではなく、無作為に少量のデータを抽出しそれを用いてコスト関数の最小化を目指します。

それでは学習するための準備をしていきます。

init = tf.initialize_all_variables()  # 値の初期化
sess = tf.Session()                     # セッションの初期化
sess.run(init)                              

直前に書いたように確率的勾配降下法を用いて少しずつ学習を行います。訓練データセットから100個ずつ無作為にデータを持ってきてそれ1000回を学習させています。これによってニューラルネットワーク内の重みwとバイアスbが決まっていきます。

for i in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

Evaluating Our Model

最初に、予測ラベルが正解ラベルと一致しているかの判定を次の文で定義します。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

この結果をfloatに変換して平均を取ります。下記のコードは[True, False, True, True]が与えられると[1,0,1,1]に変換して、平均0.75という結果を求めます。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

最後に結果の出力。sessionを通して出力されます。

print sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
# > 0.9126

91.26%の精度で認識ができていることが確認できました。チュートリアルでも91%くらいの精度が出ると書いてあるので、機能しているようです。

参考

macにtmuxを導入する

tmuxとは

端末多重化ソフト(terminal multiplexer) ちなみに読み方は``てぃーまっくす''とのこと. sshでリモートの端末に入って作業する際に,いくつも画面を開かなくて済む,一度切れても開き直せるというのが特徴のよう. 便利そうだ!

インストール

MacならHomebrewで入れられます. ishin$ brew install tmux

使い方

tmuxで起動します.

prefix+ 以下のコマンドで操作ができます.prefixはデフォルトでCtrl-b

  • bind-key d: tmuxを終了
  • bind-key c:新しい画面を作成 新しく画面を作成すると一つだったターミナルにもう一つ画面が増えたよ,というのが画面下に出ます.

f:id:yoghurt1131:20151101011339p:plain:w120 f:id:yoghurt1131:20151101011343p:plain:w200

  • <prefix> n:次の画面へ移動
  • <prefix> p:前の画面へ移動
  • <prefix> %:画面を縦に分割
  • <prefix> ":画面を横に分割
  • <prefix> o:分割した画面間を移動
  • <prefix> w:現在開いているウィンドウのリストを表示

セッションを保存しておけるので一旦作業を終了し,帰宅した後にそのまま作業を再開することができます.

<prefix> d # デタッチして作業を終了
exit            # ログアウト
ssh remote      # 自宅で再度ログイン
tmux attach -d # 前回のセッションを開いて作業再開

参考