mavenプロジェクトでフォーマッターを使う

技術Tipsです。

概要としてはmavenを利用しているspring bootプロジェクトにおいてコードフォーマットを行う方法、及びそれをIDE(Eclipse, IntelliJ)に適用する方法になります。

背景

コードフォーマッターが必要になった経緯です。

最近は仕事でSpring Bootを使ってWebアプリの開発をしています。Javaの開発であればEclipseIntelliJなど優秀なIDEがあるため、コマンド一発でフォーマットしてくれます。便利ですよね。

なので、個別にフォーマッターを入れる必要は開発時点ではなかったのですが、CI/CD時にフォーマットチェックを入れたいという話になり、Githubなどに上げたあとの環境でフォーマットをかける必要が出てきました。

今回はmavenでプロジェクトを作っていたためmavenでのフォーマッターの導入の話、IDEと共通のフォーマッターを使用する方法などを書きます。

使用するフォーマッター

formatter-maven-pluginというのを利用します。が、公式らしきサイトのUsageを試していてもうまくいかず、少し自分で調べる必要がありました。

利用方法

公式サイトの方ではpom.xmlに書くpluginと、実行方法は以下のようになっていました

pom.xml

<project ...>
    ...
    <plugins>
      <plugin>
        <groupId>net.revelc.code.formatter</groupId>
        <artifactId>formatter-maven-plugin</artifactId>
        <version>2.0.2-SNAPSHOT</version>
      </plugin>
    </plugins>
    ...
</project>

実行方法

mvn java-formatter:format

しかしこの方法ではフォーマットはおろか、プラグインの実行すらされません。 というかそもそもmvn java-formatter:formatで実行しているjava-formatterプラグインのprefixから検索しているのにプラグインのartifactIdがformatter-maven-pluginだし。。。

色々調べたり試したりしたところ、下記のプラグイン指定と実行方法で行けました。

pom.xml

<project ...>
    ...
    <plugins>
      <plugin>
        <groupId>net.revelc.code.formatter</groupId>
        <artifactId>formatter-maven-plugin</artifactId>
        <version>2.0.1</version>
      </plugin>
    </plugins>
    ...
</project>

実行方法

mvn formatter:format -Dconfigfile=formatter.xml

formatter.xmlは下で説明しますが、フォーマット用の設定ファイルです。

フォーマットチェックとフォーマット

対象ファイル

確認のため、あえてフォーマットが崩れているファイルを用意しました。main関数を1行で記述しています。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenFormatterDemoApplication{
    public static void main(String[] args){ SpringApplication.run(MavenFormatterDemoApplication.class, args);}
}

設定ファイルの取得

上で述べた設定用のxmlファイルですが、今回はSpring bootで標準的に使われているらしいxmlファイルを落としてきて入れました。formatter-maven-pluginではeclipseのフォーマット用のxmlが設定ファイルとして使え、検索するといくつかヒットします。

github.com

設定の例

下記は今回使用した設定ファイルの一例です。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="12">
<profile kind="CodeFormatterProfile" name="Spring Boot Java Conventions" version="12">
...
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
...
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="90"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
...
</profile>
</profiles>

設定項目はたくさんありますが、<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="90"/>(改行する文字数)のように比較的理解しやすいものが多いです。

とはいえ直接編集するのは骨が折れるので、カスタマイズするなら後述するEclipseの設定を用いてxmlを生成するのがよいでしょう。

フォーマットチェックとフォーマット

設定したxmlファイルのフォーマットに従っているかどうかをチェックするには以下のコマンドを実行します。

mvn formatter:validate -Dconfigfile=formatter.xml

先程のようにフォーマットが崩れているファイルがプロジェクトに存在すると、mavenがエラーを吐きます。

> ~/maven-formatter-demo> mvn formatter:validate -Dconfigfile=formatter.xml
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building maven-formatter-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- formatter-maven-plugin:2.0.1:validate (default-cli) @ maven-formatter-demo ---
[INFO] Using 'UTF-8' encoding to format source files.
[INFO] Number of files to be formatted: 2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.130 s
[INFO] Finished at: 2017-10-01T19:57:48+09:00
[INFO] Final Memory: 13M/81M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal net.revelc.code.formatter:formatter-maven-plugin:2.0.1:validate (default-cli) on project maven-formatter-demo: File '~/maven-formatter-demo/src/main/java/com/yoghurt1131/mavenformatterdemo/MavenFormatterDemoApplication.java' format doesn't match! -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

では、これをフォーマットしていきます。先程のmvnコマンドでvalidateとしていた箇所をformatに変えるだけです。

 mvn formatter:format -Dconfigfile=formatter.xml
> ~/mave-formatter-demo> mvn formatter:format -Dconfigfile=formatter.xml
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building mave-formatter-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- formatter-maven-plugin:2.0.1:format (default-cli) @ mave-formatter-demo ---
[INFO] Using 'UTF-8' encoding to format source files.
[INFO] Number of files to be formatted: 2
[INFO] Successfully formatted:          1 file(s)
[INFO] Fail to format:                  0 file(s)
[INFO] Skipped:                         1 file(s)
[INFO] Read only skipped:               0 file(s)
[INFO] Approximate time taken:          0s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.129 s
[INFO] Finished at: 2017-10-01T20:00:00+09:00
[INFO] Final Memory: 14M/79M
[INFO] ------------------------------------------------------------------------

フォーマット成功ファイルの数、失敗したファイルの数、スキップされた(すでにフォーマットされていた)ファイルの数などがでます。

実際に確認してみると、きちんとフォーマットがされています。

フォーマット前

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenFormatterDemoApplication{
    public static void main(String[] args){ SpringApplication.run(MavenFormatterDemoApplication.class, args);}
}

フォーマット後

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenFormatterDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MavenFormatterDemoApplication.class, args);
    }
}

IDEでの利用

作成したフォーマッターを使えば、PaaS環境などのIDEが無い場所でもフォーマットチェックやフォーマットが行えるようになりました。とはいえ、IDEのフォーマッターとmavenで使用するフォーマッターが違ってしまうと、開発環境でかけたフォーマットがpush先で通らないといった事故が起きる可能性があります。

ここでは、作成したフォーマッターを各IDE(eclipse, IntelliJ)に読み込む方法を書きます。

eclipseでの利用

Eclipse->Preferences(設定)->Java->Code Style->Formatterに設定項目があります。

f:id:yoghurt1131:20171001202002p:plain:w500

ここでxmlファイルをimportすることができます。

また、Newから独自のフォーマット規約を作成することができます。これはxmlファイルとしてExportすることができるので、既存のがイマイチで自分でフォーマッターを作りたいという場合はこれを利用してオリジナルのフォーマッターを作成するのがいいかと思います。

IntelliJでの利用

IntelliJでも利用方法はほとんど同じです。

IntelliJ->Preferences->Editor->Code Style->JavaからImport Schemeをすることで設定ファイルを導入することができます。

注意点としては、IntelliJにはIntelliJ独自のコードフォーマット(IntelliJ IDE code style XML)があることです。

Eclipseとの互換性はあるため、Eclipseで作ったxmlファイルをIntelliJで利用することはできますが、逆はできません。開発時にIntelliJの設定でフォーマッターを作成した場合はそのままmavenで使用することができないのでご注意ください。

リンク・参考

エンジニアのための時間管理術を読んだ

オライリー出版のエンジニアのための時間管理術を読みました。ここではその感想と、最近感じてる時間の使い方について書こうと思います。

store.shopping.yahoo.co.jp

エンジニアのための時間管理術

原題はTime Management for System Administratorsなので厳密には「システム管理者のための時間管理術」なのだけれど、多くのエンジニアにはもちろん、非エンジニアである人でも参考になるような良書です。

この本が魅力的なのは、著者の語り口によるところが大きいです。この本は著者の経験談や悪戦苦闘の歴史に基づいて書かれており、あくまで「自分はこれでうまく行った」という書き方で押し付けがましさがありません。

また、ユーモア豊かに書かれており翻訳もとてもチャーミング。例えば4章にはこのような文章が出てきます。

SAは、一般に頭の切れる人々です。あなたは賢いし、筆者も賢いのです。皆が賢いのです。

・・・最高じゃないですか?ところどころこんな感じで進んでいくので、読んでいてすごく楽しかったです。

内容をかいつまんで

この本は、タイムマネジメントの考え方や、その実践方法についてこと細かに書かれています。 とはいえそれを全て書くのは不可能なので、印象に残った部分をいくつか挙げていきます。

TODOは全て外部記憶に頼ろう

(他の仕事も多くはそうですが)エンジニアは頭脳労働です。仕事の際には仕事に集中できる環境を整えておく必要があります。 この本では集中して仕事に取り組むため、何か気になることや、やることがある場合は全て外部記憶(TODOリストやその他の記録ツール)に書き出すことを勧めています。人間は忘れる生き物です。「明日Aさんに電話する」でも「今日牛乳を買いに行く」でも、何かを覚えておくことは多かれ少なかれ頭脳に負荷がかかります。それらを外部に書き出すことで、より眼の前の仕事に集中できるとしています。

決断の回数を減らそう

この本では、上で書いた「やらなきゃいけないことは外部記憶に頼る」以外に「決断の回数を減らす」ことについてもその重要性が主張されています。 本当かどうかわかりませんが、かのアルバート・アインシュタインは同じスーツを7着持っていたと言われています。日常の些末な(人によっては違うかもしれませんが)決断に労力を割かないことで、自分の能力を全て学問に捧げることができたと。

我々の脳の容量は限られています。この本では、本当に大事なところで能力を発揮するために、日常における決断の回数を減らすべきであると述べられています。

何回もやる作業はルーチン(習慣)化する

では、実際に決断の回数を減らすにはどうすればいいか。著者はそれに対する回答はルーチン(習慣)を作成することであると述べています。習慣化しているということは、考えずにその行動が行えるということに近いです。 ルーチンとは「一度だけ考え、何度も実行するための手段である」と定義されています。これはプログラマならよく理解できることだと思います。プログラムは、一度書いたら何度でも実行できるものなのですから。

僕自信が最近作成したルーチンの例を紹介します。それは、「財布や家の鍵などの外出時に持ち歩くものは全て、自室の小箱にしまう」というものです。これを定める以前は、財布や家の鍵の場所が帰宅したときによって変わってしまい、家を出る時に鍵がない、財布がない、といった事態は多々ありました。そこで、「財布や定期券、家の鍵、イヤフォンなどの外出時に持ち歩くものは自室の小箱に全て置く。帰宅したらまずそこにそれらのアイテムをしまう」というのを習慣づけました。これによって、出かけるときに必要なものを探す必要はなくなり、結果として時間の節約になっています。

時間を上手く使うために

社会人になって、日常の自由時間がかなり少なくなったと感じます。しかし一方で、生活サイクルが定まることによってより日常の行動が最適化され、空きの有意義な時間が増えたような気持ちにもなっています。 学生の頃は、生活サイクルなんてかけらもありませんでした。目が覚めたときに起きる、眠くなったら寝る、研究が忙しければキリがつくまでやる。全てを自分の裁量でさばけるという意味では自由でしたが、「いつお昼を食べよう」「いつ帰ろう」といった些細な考え事が常に頭のなかに合ったと思います。そういったことを考えているだけで時間がすぎ、貴重な時間を無駄に消費していました。 働くようになってご飯を食べる時間、寝る時間、起きる時間はほぼ固定になりました。これによって自分の空き時間を自分がやりたいことに純粋に使えるようになったと感じています。別にアインシュタインになりたいわけではないですが、学ぶにしても遊ぶにしても「時間を無駄にしていない」という感覚は非常に気持のいいものです。 これからも良い習慣を作っていきたいなと思った次第です。

プレゼンテーションZENを読んだ

学生時代に研究室の教員から「これくらいは流石に読んでるよな?」と煽られたにも関わらず読んでなかった「プレゼンテーションZEN」を読みました。

store.shopping.yahoo.co.jp

ZENは「禅」を指しています。その名の通りこの本ではプレゼンが成功する方法ではなく、良いプレゼンをするための考え方やアプローチが書かれています。

ここでは、読んでいて心に刺さった点を幾つか挙げます。


プレゼンテーションの主役は自分

当たり前ですが、プレゼンの主役はPowerPointではありません。話をする自分自身です。そもそもPowerPointで全てを説明できるとしたらスピーカーは不要です。それに下で述べるようにPowerPointはテキストを詰め込むツールとしては適切ではありません。 人がプレゼンを行う際には、必ずその人にしか伝えられないアイディア、経験があります。PowerPointKeynoteなどのプレゼンテーションツールは、スピーチを引き立たせるための補助的な役割でしかないのです。

この考え方に立つことで、スライドを作るアプローチが劇的に変わると思います。「情報を載せなきゃ」というマインドで作るスライドと、「自分が話したいことをどうやって引き立たせるか」というマインドで作るスライドではきっと完全に違うものが出来上がるでしょう。

悪しき習慣のスライデュメント

本の中で著者は「スライドはシンプル」にと何回も説いています。スライドはスライドとして、資料は資料として用意するべきである、と。そして、スライドと資料が混ざったものを「スライデュメント」(Slide + Document)と呼び、それは非常に中途半端で良くないものであると主張しています。スライデュメントとは、スライド一面が大量の文字で埋め尽くされたり、箇条書きで埋まったりしているようなスライドのことを指しています。

しかし、現在ビジネスの場ではこの「スライディメント」が多く見られます。むしろPowerPointを使ってプレゼンする際の基本的な形とさえなっています。著者はこれを「一石二鳥だと思っている人がいるかもしれないが、二兎追う者は一兎も得ずである」とバッサリ切り捨てています。たしかに、PowerPointのスライドは、詳細を事細かに書けるようにはなっていませんし、文字を詰め込んだところで聴衆から読めなくなってしまっては意味がありません。詳細を伝えたいならWordなどで資料を作成して、それを後で読んでもらえばいいのです。

「究極的に何が言いたいのか」を考え抜く

自分が聴衆として聞く場合を想像するとわかると思いますが、100%の集中力でプレゼンを聞き続けるなんてことは不可能です。だいたいどこかで集中力が切れます。 だからこそ、準備の段階で「自分はプレゼンを通して聴衆に何を伝えたいのか」を考え抜き、それが伝わるような構成でプレゼンテーションを組まなければなりません。プレゼンを作る際には、「聴衆はたった自分のプレゼンの中でたった1つのことしか記憶に残らない」というつもりで作るべきなのです。

プレゼンを作っているとついつい色んなことをスライドに書きすぎてしまいますが、可能な限り削るべきです。ZENでは、プレゼンの準備に必要なのは「抑制」であると説いています。伝えたいことを詰め込みすぎて、聴衆の集中力が切れてしまったら最後、そのプレゼンは誰の記憶にも残らないものとなってしまいます。可能な限りシンプルに行くべきなのです。


上記に書いたのはまさにプレゼンテーションにおける禅のような考え方や姿勢でしたが、この本にはスライドを作る際のデザインの原則や、スピーチの場において必要なことなど、実践的なこともかなり載っています。スライドのサンプルもかなり多いです。今後も何かとプレゼンをする機会はあるので、この本を参考にして、禅のような精神で鍛錬を積みたいと思います。

GoogleCalendarAPIを使ってGoogleCalendarの予定を取得する

お題通りです。+ α程度の情報は載せていますが、基本的には公式のQuickStart通りに進めました。言語はPythonです。

Step1: Google Calendar APIを有効にする

自分のGoogleアカウントでGoogle Developers Consoleにログインします。初めての場合は「規約に同意して進む」みたいな画面が出るので、一通り確認して進みます。

認証が終わると自動的にAPIを使うためのProjectが作成されます。 「認証情報の追加」というメニューが出てきますが、APIを使うのに必要なのはClient IDなので「クライアントID」と書かれたリンクをクリックして次に進みます。

OAuth同意画面に飛ばされるので、必須項目のサービス名を記入して次へ。(他に記入できる項目があれば記入してください) アプリケーションの種類と名前(適当でもいいです)を入力して「作成」ボタンを押すとクライアントIDとシークレットキーが作成されます。

f:id:yoghurt1131:20170510212302p:plain

こんな感じです。右端のダウンロードのマークをクリックしてjsonファイルをダウンロードし、client_secret.jsonという名前にリネームしてプログラムを置く予定のフォルダに保存します。

Step2: Pythonライブラリのインストー

pipで入れるだけ。

pip install --upgrade google-api-python-client

Step3: サンプルスクリプトを動かす

サンプルのスクリプトquickstart.pyが公開されているので、これをコピーします。

Step4: 実行

あとはpythonで叩くだけ。

python quickstart.py

ブラウザが立ち上がって、Googleアカウントの認証をできればOK。 サンプルのスクリプトは、実行したタイミングから未来の予定10件を取得するようにできています。

補足

これだけだとちょっと寂しすぎるので、少しだけ補足を。

認証情報の保管

quickstart.py実行時にブラウザを使って認証を行いましたが、これは最初だけです。2回目以降は普通にAPIが使えます。 この時の認証情報が$HOME/.credentials/以下に保存されています。

APIの仕様

こちらのサイトに使えるメソッドが載っています。

予定の書き込み

quickstart.pyでは、予定の読み込みの例しか載っていません。 書き込みの例を簡単に紹介します。やることは主に3つ程度です。

1. SCOPEの変更

quickstart.pyの20行目

SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'

この最後のreadonlyを省きます。

2. 認証情報を削除

このままだと上記のreadonly権限での認証情報が残ってしまっているため、Permission Deniedで弾かれます。 $HOME/.credentials以下の認証情報ファイルを削除します。

3. イベントを挿入するソースコードを記述

quickstart.pyでイベントを取得している箇所を、以下のような感じに書き換えます。

    event = {
      'summary': 'Sample Event from Python',
      'start': {
        'dateTime': '2017-04-28T09:00:00',
        'timeZone': 'Asia/Tokyo',
      },
      'end': {
        'dateTime': '2017-04-29T19:00:00',
        'timeZone': 'Asia/Tokyo',
      },
      'recurrence': [
        'RRULE:FREQ=DAILY;COUNT=2'
      ],
    }

    calendarId = 'xxxxxxx@gmail.com' # 自分のカレンダーID(例えば、メールアドレス名)を入れる
    event = service.events().insert(calendarId=calendarId, body=event).execute()

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