Quantcast
Channel: ALBERT Official Blog
Viewing all 191 articles
Browse latest View live

新宿野村ビル 夏のライブ&ビアガーデン

$
0
0

こんにちは!広報の大森です♪

ALBERTがオフィスを構える新宿野村ビルでは、様々なイベントが開催されます。
中でも、夏のライブ&ビアガーデンは、最も盛り上がるイベントのようです。
今年は8月26日の開催。部署の皆とぞろぞろ行ってみました!

昨年はオフィスを移転したばかりだったので、勝手がわからず、就業時間を終えてから参加したところ、屋台のメニューはほとんど売り切れ・・・
苦い経験を忘れずに、今年ははりきってイベント開始時間に休憩をとって出発!

にぎわっています♪

IMG_5041

レストランのラインナップに定評のある新宿野村ビルの一部の店舗から屋台がでているので、メニューもおしゃれだし、味も確かです。

IMG_5038

ビル内の企業には、野村ビルから割引チケットが配られます。

IMG_5043

1枚で350円割引。しかも1度に何枚も使えます。
そのため、食べ物も飲み物もだいたい350円・700円・1050円・1400円とチケットだけで購入できる値段設定になっています。感動!

残念ながら私たちはチケットを1枚ずつしかもっていなかったので、4枚出してピザを買っている人を横目に350円ジャストのものを探します。(意地でもチケットだけで買いたいw)
「ビ・・ビール!!」と頼みたいのをぐっと我慢して、から揚げをGETしました!!ポテトもありました!!

IMG_5046

そして、夏のビアガーデン&”ライブ”というくらいですので、ライブもあります。
昨年に引き続き、サザンオールスターズのトリビュートバンド「いとしのエリーズ」の登場!

IMG_5047

時間があまりなかったので、ライブパフォーマンスは観ずに戻ってしまいましたが、大盛り上がりの様子でした。

新宿野村ビルのイベントにも慣れてきた、移転して2度目の夏が終わりました。
秋が来ますね~。


データサイエンティスト・インターンシップを開催しました!

$
0
0

こんにちは。人事・広報の小國です。

ALBERTでは、新規学卒者を対象とした採用選考インターンを実施するほか、
夏季休暇中のサマーインターンなど、就業体験を希望する学生の方々を積極的に受け入れしています。

そして今回、統計や分析などALBERTと親和性の高い分野を専攻する学生を対象に
「データサイエンティスト・インターンシップ」を開催いたしました!

ALBERTでは初めての試みとなりますので、参加希望者がいなかったらどうしよう・・・と
社員一同どきどきしていましたが、とても意欲的な学生が5名も参加してくれましたー!

開催概要はこんな感じ☆

開催期間 :2016年9月12日(月)~9月16日(金)の5日間
研修テーマ:マシンラーニング
課  題 :休日予測
指導担当 :ALBERTデータ分析部一同

開催期間5日間と言っても、初日はオリエテーションなどで半日以上潰れてしまいますし、
最終日には発表会があるので、課題に取り組める時間は実質3日程度だけ!
時間が少なくて学生達には辛い思いをさせてしまいました・・・反省です。
そんな短い期間の中でも、前向きに課題解決に向けて取り組んでいただきました!

そしてあっという間に発表会の日を迎え・・・

img_1182
みんな全く緊張を感じさせないような堂々とした発表でした!

img_1196
素直で一生懸命な学生の姿に、社員たちの指導にも熱が入ります。

「データの成型がいちばん難しいと感じた」、
「ビジネスでのデータ活用と研究でのそれとは大きく違って驚いた」、
「とてもいい勉強、いい経験になった!」

そんな感想を残して、5日間の日程を終えた学生たちはインターンシップ最後のイベント
「社長との焼肉パーティ」に夜の新宿へ旅立ちました~☆。

データサイエンティスト・インターンシップは、ALBERT初の試みということで
至らない点なども多くあり、学生のみなさまにはご不便をおかけしてしまったと反省ばかりです。

今後も定期的に開催していきたいと思いますが、いただいた指摘はどんどん改善して
更に有意義な研修にしていく所存です!

次回についてはまだ未定ですが、次はどんな学生の方々に出会えるだろうと
今からわくわくしていまーす!

スタッフお仕事ぶり取材 第8回「データ分析部アルバイト まえた君」の巻

$
0
0

こんにちは!広報担当の大森です。
今回は、2年ぶりのALBERTスタッフお仕事ぶり取材です。
(以前のものはこちらからご覧いただけます。)

「分析力」をコアとして事業を展開するALBERTでは、正社員は全員、統計検定の受験が義務づけられています。
もちろん、業務により必須で取得する級は異なりますが、管理部門であっても全正社員が受験をします。

データ分析部のまえた君は、その統計検定の2級・準1級に合格し、2級の成績優秀者に選ばれたと小耳に挟み、大変めでたい!というわけで、早速取材にお邪魔しました☆

突然の取材&撮影にも関わらず、快く応対してくれたまえた君です。

img_5520

パシャリと写真を1枚いただいてから、ALBERTでの仕事や統計検定についていろいろ聞いてみました!

ALBERTではどんな仕事をしていますか?

現在は、確率モデルを組み込んだプログラムを実装するエンジニアリング寄りの仕事をしています。以前は、コンサルティングの部署でクライアントの要求に沿ったアドホックな集計・分析をしていました。

ALBERTに入ったきっかけは?

当時、分析で行き詰まっていた時にデータ集計・分析のためのSQL入門」株式会社ALBERT 巣山 剛 (著) に出会い、解決できました。
そこで助けてもらったことから ALBERT には高い分析力をもつ優秀なメンバーがいると知り、興味を持ったのがきっかけです。
学生アルバイトとして働くことで 、ALBERT のメンバーから現場のデータ分析を学ぶことができるのではないかと考え応募しました。

そうそう、まえた君は電気通信大学 社会人コースの3年生。大学に通いながらALBERTで働いているのです。
大学の勉強と業務の両立が大変そうですが、その上統計検定を受験するなんて、意識高い!などと感心しながら、そのあたりをさらに深堀してみました。

統計検定はどうして受けようと思ったのですか?

統計学・機械学習の知識を身につけたいと思ったからです。大学の授業を受けるだけでは、そういった知識・能力が身につかないかもしれないと不安でした。
また、データを扱うエンジニアとしての最低限の素養を示したかったです。結果的に、手っ取り早く周辺の知識やスキルが身についたと思います。

どんな勉強をしたのですか?

まず、基礎的なことを入門書・演習書をあわせて4冊ほど読んだり演習問題を解くことで学習しました。
次に、応用的なことを分析手法が紹介されている技術書のRプログラムを動かして、実際に分析しながら勉強しました。
仕上げとして、試験前1カ月間は過去問題集を繰り返し解きました。


2級と同じタイミングで準1級にも合格しましたが、こちらは特に対策していませんでした。
2級の対策をしっかりしたことと、たまたま出題された問題が、普段の業務や勉強会で学んだ機械学習の知識で解答することができただけなので、合格したことは運が良かったとしか言えません。

運が良かったと謙遜するまえた君ですが、準1級は運だけでは突破できない難関!
勉強が着実に身についていたからこその結果だと思います。
最後に今後について聞いてみました。

勉強したことを今後どんなことに活かしていきたいですか?

大学で研究に使用している分析も、統計検定の勉強を通して学んだ手法を応用して試しているので、現時点でも、かなり役に立っています。
ですが、実力は統計学・機械学習の最新手法には全然追いついていないので、統計検定で学んだことを基礎の一部として、発展させていきたいと考えています。


自分のキャリアデザインでは、要件定義・分析・システム化までを通しでできる人材を目指しています。全ての工程を理解し、それぞれの橋渡し役になれるようになりたいです。
そのために、これからはモデル構築など高度な分析の仕事も任せてもらえるよう努力していきます。

志を持って大学の勉強にもALBERTの業務にも取り組むまえた君。
これからも自身の目標に向かって頑張ってもらいたいです!

ALBERTでは、やる気のある学生インターンを随時募集しています!
データサイエンティストやデータアナリストを目指す学生の皆さん、どしどしご応募ください♪
 ↓↓↓
【採用情報】中長期インターン

TechTalkに参加したお話

$
0
0

こんにちは!人事・広報の小國です。

実は10月と11月に、株式会社アカリク様主催のイベント、「アカリクTechTalk」へ参加していました!
過去系!(すみません・・・

techtalk%e3%83%ad%e3%82%b4

詳細は↓こんな感じ↓
☆==========★ 10月 ★==========☆
アカリクTechTalk vol.1:機械学習
日時:2016年10月12日(水)17:00~18:30
開催場所:ヤフー株式会社さま
技術トピック:機械学習
ALBERT登壇者:データ分析部 青木 健児
Yahoo様、freee様、そしてALBERTの三社によるトークセッションです。

techtalk%e9%9d%92%e6%9c%a8%e3%81%95%e3%82%931
まずはALBERTから青木が自社紹介と、自己紹介を♪

techtalk%e9%9d%92%e6%9c%a8%e3%81%95%e3%82%932
そして事前に寄せられていた質問やニコ生視聴中の皆さまからのコメントによる質問、
また会場に来てくれた学生さんからの質問に三者が回答したり。。
そのまま雑談になってしまったり!

とっても有意義な時間になりましたー☆
それになんといってもヤフー様の新居がとっても綺麗で素敵で圧倒されました!w

★==========☆ 11月 ☆==========★
アカリクTechTalk vol.5:機械学習(ディープラーニング)
日時:2016年11月09日(水)17:00~18:30
開催場所:クックパッド株式会社さま
技術トピック:ディープラーニング
ALBERT登壇者:データ分析部 最上 嗣生
こちらはcookpad様、Yahoo様、そしてALBERTの三社でトークセッションでした!

techtalk%e6%9c%80%e4%b8%8a%e3%81%95%e3%82%931
五十音順なのでやっぱりALBERTの最上が先陣をきって自社・自己紹介です☆

techtalk%e6%9c%80%e4%b8%8a%e3%81%95%e3%82%932
同じく、いただいた質問に回答していきます。

トークセッションのあと、会場では各回とも懇親会があったのですが、
クックパッド様では社員の皆さまの手作りのお料理を御馳走になったのです!
さすがですね。。。
会場ではセッション中にとってもいい香りが漂っていて食欲を刺激され続けちゃいました!

techtalk%e6%9c%80%e4%b8%8a%e3%81%95%e3%82%933
クックパッドさんのロゴマークをあしらったドリア!とってもおいしかったです♪

☆★==============================★☆

学生のみなさんとざっくばらんにお話をするのがほぼ初めてだったこともあり、
各回ともに、緊張しつつもとっても楽しい時間を過ごすことができました!!
当日、会場にお越しいただいた皆様、ありがとうございました★
WEB視聴で参加いただいた方々も、コメントで盛り上げてくれてありがとうございました!

こんなに楽しいひと時を自社でも開催したいと思い、
ALBERTでは、学生の皆さまの活気と勢いを肌で感じられるようなイベントをいろいろと考えていく所存です!

AWS Data Pipeline の 稀によくあるQ&A

$
0
0

システムソリューション部の佐藤奏です。

業務でAWS Data Pipelineを結構ヘビーに使ったので、調べにくいところやハマりどころをQ&A形式でご紹介します。

サービスの概要について少しだけコメントします。その後はひたすら細かい話になります。なお、以下はもっぱらリソースとしてEC2を使う場合の記述です(EC2の他に、EMRクラスタを起動することもできますが、筆者は使ったことがありません)。

概要について少しだけ

Q. Data Pipelineは何がいいの? cronの方が簡単そうなんだけれど。

A. ジョブを定期実行させる だけ なら、cronの方が簡単なのですが、Data Pipelineは それに付随するもろもろをサポートしてくれます 。例えば下記のようなことを「cron+シェルスクリプト」だけでやろうとすると結構面倒ですが、Data Pipelineにはそのための仕組みが準備されています。

一方で、Data Pipeline上では条件分岐のようなフロー制御はできないので、複雑な処理には向きません。典型的にはETL処理などに適しています1


Q. Data Pipelineってデータ処理にしか使っちゃいけないの?

A. そうでもありません 。確かに、S3へのファイル入出力、DBへのSQL発行などのデータ処理に特化したアクティビティが多いですが、ShellCommandActivityを使えば任意のシェルスクリプトが実行できるので、cronの代替としてもいろいろ使えます。弊社ではRedshiftクラスターの落とし上げの自動化にも使っていたりします(AWS CLIをキックするEC2の面倒を見なくて済む)。

Data Pipelineで使うコンピューティングリソースは処理実行時に新規作成し、終わったら破棄する仕組みなので、「実行頻度の低い処理」「実行時だけ大量のコンピューティングリソースが必要な処理」などでは経費節減効果がありますし、運用の手間も減ります。

ひたすら細かい話

Q. AWSコンソールではData Pipelineの全機能が使えますか?

A. いいえパラメータ項目が新規作成できなかったりします(編集は可能)。全機能を使うためには定義ファイルをJSON形式で作成して読み込ませる必要があります。


Q. JSONつらいです。

A. たとえばYAMLで書くのはどうですか? コメントも書けるし、複数行文字列もそのまま書けるので見やすいと思います。筆者は、定義ファイルはYAML形式の状態でリポジトリ管理し、Data Pipelineに登録(デプロイ)する時にJSONへ変換しました。変換にはnpmモジュールのyamljsを利用しました。

本記事末尾に、YAMLで記述した場合の見た目を掲載しています。


Q. ShellCommandActivity/SqlActivityで実行するスクリプトは「定義ファイルに直書き(

command
/
script
フィールド)」「S3にファイルで配置し、定義ファイルにはパスのみ記述(
scriptUri
フィールド)」どっちがいいの?

A. 下記の違いがあります (太字がメリット)。筆者個人的には直書きの方がメリットが大きいと感じます。

定義ファイルに直書き S3に配置
#{}
でのパラメータ埋め込み
可能 不可能(※1)
管理対象のファイル 少ない(定義ファイルのみ) 多い(定義ファイル+S3へ配置するスクリプト群)
定義ファイルの見やすさ 見にくい(長い) 見やすい
スクリプトの差し替え 面倒(※2) 簡単(※3)

(※1)

scriptArgument
フィールドでの値埋め込みが可能ですが、
#{}
に比べると制約が多いです。
(※2)既存パイプラインの編集よりは、新しいスクリプトを埋め込んだ新規パイプラインをデプロイする方が管理も単純化できるでしょう。
(※3)S3上でのファイル差し替えのみ。

Q. ShellCommandActivity内でAWS CLIを使っているんだけど、うまく動かないです。

A. AWS CLIのバージョンが古いのかもしれません 。パイプラインの先頭で

sudo yum -y update aws-cli
しましょう2

Q. ShellCommandActivityの

command
フィールドにシェルスクリプトを記述してますが、長いコマンドを行末
\
で区切って複数行で書くとなんかうまくいかないんだけど。

A. JSON上では

\
エスケープして
\\
にしないといけません
。行末に限らず同様。

Q. SqlActivityで、複数のクエリをまとめて1つのアクティビティで実行できますか?

A. ドライバ依存 です3。筆者個人的には、まとめすぎない方がいいと思います。どのクエリが失敗したか分かりにくくなるので。


Q. 複数のオブジェクトで同内容のフィールドを記述しているんですが、もう少しDRYに書けませんか?

A. それらのフィールドをまとめたオブジェクトを1つ作り、他のオブジェクトから

parent
フィールドで参照 しましょう(パイプラインのフィールド)。


Q. というか、”Default”オブジェクトは他の全オブジェクトが暗黙のうちに継承しているから、共通で使いたいものは全部Defaultに書けばいいよね。

A. 大体はそれでもいいのですが 時々ダメ です。例えば

Ec2Resource
オブジェクトに
workerGroup
フィールドを設定するとエラーになるので、
workerGroup
を”Default”オブジェクトに書くことはできません(オブジェクト内の不要なフィールドは無視される場合が多いのですが、たまに無視されずエラーになるものがあってこういうことになります)。

Q. 「Redshiftリストア(ShellCommandActivityでAWS CLIを実行)→クエリ発行(SqlActivity)」というパイプラインを作ったんだけど、クエリがうまくいきません。

A. Redshiftリストア直後に、 リストアにかかる時間ぶんsleepを入れましょう 。AWS CLIでRedshiftをリストアすると、リストア完了を待たずにCLIは終了します。するとShellCommandActivityも終了してしまうので、リストアが完了する前にSqlActivityが実行されてしまいます。


Q.

id
フィールドとは別に
name
フィールドも必ず書かなきゃいけないの?

A.

name
は省略可能 です。


Q.

input
フィールドには配列で複数の項目を設定できますが、このとき他のフィールドから
#{input[1].directoryPath}
のような形でn番目の項目を参照できますか?

A. 現状ではできません 4


Q. 1つのパイプライン定義に入れられるオブジェクトの最大数はいくつですか?

A. 当記事執筆時点では 100 です(AWS Data Pipeline の制限)。


Q. 100オブジェクトを超えてしまってData Pipelineに怒られました。

A. 弱りましたね 。そういう場合は2つのパイプラインに分けるしかないのですが、現状、パイプライン同士の先行・後続関係の定義はできないので、例えば下記のような作りにすることになります。

  • 先行パイプラインの最後で「処理完了ファイル」(中身は空でOK)をS3に配置する
  • 後続パイプラインの先頭アクティビティで、上記ファイルの存在を「前提条件(S3KeyExists)」とする

Q. 例えば処理を20分周期で実行させたいとすると、EC2が1時間あたり3個立ち上がることになって、料金がかさみませんか?

A. EC2は1時間周期にしておいて、使い回す ということができます(スケジュールを使用したリソースの最大効率)。


Q. AWS CLIのput-pipeline-definitionを使って下記のようにしたんですが、定義ファイル

data_pipeline.json
内の日本語が化けた状態で登録されます。

aws datapipeline put-pipeline-definition --pipeline-definition file://data_pipeline.json (後略)

A.

file://
fileb://
(バイナリファイル扱い)にしてみてください。

Q. テスト実行の時、EC2起動でいちいち時間を取られるんだけど、なんとかならない?

A. TaskRunnerを使いましょう 。EC2をひとつ、常時稼働させておいて、そのEC2上で処理を走らせることができます。


awslabs/data-pipeline-samples にある RedshiftToRDS_WithoutRDSCreate.json
をYAMLで記述してみたサンプルです。個人的には括弧が少なくなって見やすいと感じます。

objects:

  - id: "RdsDatabase"
    name: "RdsDatabase"
    type: "RdsDatabase"
    databaseName: "#{myRDSDatabaseName}"
    '*password': "#{*myRDSPassword}"
    rdsInstanceId: "#{myRDSInstanceId}"
    username: "#{myRDSUsername}"

  - id: "RedshiftCluster"
    name: "RedshiftCluster"
    type: "RedshiftDatabase"
    databaseName: "#{myRedshiftDatabaseName}"
    '*password': "#{*myRedshiftPassword}"
    clusterId: "#{myRedshiftInstanceId}"
    username: "#{myRedshiftUsername}"

  - id: "DefaultSchedule"
    name: "RunOnce"
    type: "Schedule"
    occurrences: "1"
    period: "1 Day"
    startAt: "FIRST_ACTIVATION_DATE_TIME"

  - id: "RedshiftToS3"
    myComment: This object is a RedshiftCopyActivity. It is used to define the work that will be done to copy the data from Redshift to S3.
    name: "RedshiftToS3"
    output: { ref: "S3OutputLocation" }
    type: "RedshiftCopyActivity"
    input: { ref: "RedshiftDataNode" }
    schedule: { ref: "DefaultSchedule" }
    runsOn: { ref: "Ec2Instance" }
    insertMode: "TRUNCATE"

# ... snip ...

parameters:

  - id: "myRDSInstanceId"
    type: "String"
    description: "RDS Instance name (DB Instance)"

  - id: "myRDSDatabaseName"
    type: "String"
    description: "RDS Database name"

  - id: "myRDSUsername"
    type: "String"
    description: "RDS Database Username"

  - id: "*myRDSPassword"
    type: "String"
    description: "RDS MySQL password"

# ... snip ...

YAMLの場合、文字列値の

""
は省略可能ですが、パラメータ埋め込み
#{}
がコメントと解釈されるのを防ぐため、一律
""
で囲うのがよいと思います。

なお、

|
+改行 のあとに文字列値を書くこともできます。この場合、文字列内の改行を
\n
とせずそのまま書けます(ただしこの場合も
\
はエスケープが必要)。シェルスクリプトやSQLの記述に便利です。例えばこんな感じになります。
    - id: "install"
      type: "ShellCommandActivity"
      # ... snip ...
      command: |
        sudo yum -y update aws-cli || exit 1
        some_command1 arg1 arg2 && \\
          some_command2 arg1 arg2 && \\
          some_command3 arg1 arg2

    - id: "INSERT_activity"
      type: "SqlActivity"
      # ... snip ...
      script: |
        INSERT INTO myschema.mydata (
            id
          , name
        )
        SELECT
            id
          , name
        FROM dwh.master_user
        ;

バンディットアルゴリズム 基本編

$
0
0

データ分析部の中村、中野です。
今回から2回に分けてバンディットアルゴリズムをご紹介いたします。

今回は基本編ということで「バンディットアルゴリズム」の基本的な思想と代表的な方策について簡単にご説明します。

バンディットアルゴリズムはWEB広告配信やレコメンドシステム、はたまたトップ棋士に勝ち越したことで有名なアルファ碁にもその技術が応用されたことで注目を集めています。

そもそも、バンディットアルゴリズムとはどういったものでしょうか。まずは少しややこしいその問題設定を丁寧に見ていきます。

バンディットアルゴリズムで扱うのは、

「選択肢はいくつもあるが、どの選択肢が効果が高いのかは事前にはわからない」

「限られた試行回数でできる限りいい選択肢を選んでいき、トータルの報酬を最大化したい」

といったような問題設定です。WEB画面上に表示する広告や配信するコンテンツをどう選択するか、顧客にダイレクトメールを送る際の件名はどの候補がよいか、といったようにこのような問題はビジネスの現場でもよく見られます。

重要なのはどの選択肢がよいか事前に情報がない、という点です。もし各選択肢について情報があるのであれば、それを学習データとして教師あり学習による予測モデルを作ることで事足ります。バンディットでは学習データがない状況からどの選択肢がよいかを学習しながら、その過程で得られる報酬を最大化することを目的としています。

同じような設定でA/Bテストがよく用いられますが、バンディットと何が異なるのかを整理しましょう。

A/BテストはAかBの2つの選択肢のどちらがより優れているかを統計学的仮説検定を元に判断する枠組みです。全体を2つのグループに分割し、あるグループにはAという選択肢、もう一方のグループにはBを適用し、その結果からAとBのどちらが優れているかを決定します。また、一度決定した後は優れた選択肢を選びつづけるというのが一般的です。つまりA/Bテストは最適な選択肢を見つけるためのテストです。このように最適な選択肢を見つけ出す枠組みを最適腕識別と言い、A/Bテストはこの最適腕識別に分類されます。

一方でバンディットアルゴリズムで目指すのは累積報酬の最大化です。有限回の試行の中で報酬を最大化するには、優れたアームを多く引き、劣ったアームは引く回数を抑えることが必要となります(バンディットの文脈では選択肢のことをアームと呼びます)。しかし、事前に各アームの良し悪しはわからないので、どのアームが良いかを探りつつ(探索)、良さそうなアームほど積極的に引いていく(活用)をバランスさせるのがバンディットアルゴリズムの本質です。

どのアームが良いアームかについては、それまでに得られた報酬の標本平均などをアームごとにスコア化し、それらを比較することで判断します。そのため、ある時点で選択したアームの報酬が高ければ選択したアームのスコアが高くなり、報酬が低ければ選択したアームのスコアが低くなるといったように随時情報を更新しながらアームの良し悪しを判断していきます。

バンディットアルゴリズムは、アームを選択した結果得られる報酬を動的に反映し、次のアームの選択に活かすことからA/Bテストに比べて機会損失の低減が見込めます。また、コンテンツの入れ替わりが多いようなケースではA/Bテストを都度設計するのは手間ですが、バンディットならシステム化できるというメリットもあります。

bandit_image_64_48

ではここからは代表的な方策であるε-greedy方策、UCB方策、Thompson Sampling方策の3つをご紹介していきます。

■ε-greedy方策

最も単純な方策としてε-greedy方策が知られています。この方策では各ステップごとに確率εで探索、1−εで活用を行います。具体的には、

探索時:すべてのアームをランダムに選択

活用時:それまでの試行の結果から、報酬の標本平均\hat{\mu}_{i}の最も高かったアームを選択

という方法でアームを選択していくことで累積報酬の最大化を目指します1

この方策はシンプルで判りやすいですが、最適な探索回数を見つけるのが困難という課題があり、探索と活用のバランスをうまく調整できないと次のような問題が生じます。

探索が少ない → 最適なアームを発見できず、活用時に最適でないアームを引き続ける可能性がある

探索が多い  → 最適でないアームを余分に引いてしまう

■UCB方策

累積報酬を最大化するためには最適なアームを多く引いていくことが重要ですが、選択回数が少ないアームについては報酬が正確に推定できていない可能性を考慮する必要があります。

これらのバランスをとれる方策としてUCB(Upper Confidence Bound)方策を紹介します。

この方策では、アームを選択する際に毎回以下の式で表されるスコアを算出し、最もスコアの高いアームを引きます。

\bar{\mu}_{i}(t) :時刻tのアームiのスコア

\hat{\mu}_{i}(t) :時刻tのアームiの標本平均

N_{i}(t) :時刻tまでのアームiの選択回数

\bar{\mu}_i(t)=\hat{\mu}_i(t)+\sqrt{\frac{\log t}{2N_i(t)}}

標本平均に補正項を加えた値がスコアであり、選択数 N_{i}(t) が少ないアームほど補正項の値は大きくなります。そのため標本平均は小さいが、選択回数が少ないアームも選ばれることがあります。

つまり、単純に標本平均の大きなアームが選択される時は活用が行われ、標本平均は小さいが、選択回数が少ないアームが選択される時は探索が行なわれていると解釈できます。

このようにUCB方策では探索と活用のバランスをうまくとりながらアームの選択を行い、報酬の最大化を目指します。

■Thompson Sampling方策

「そのアームが最適なアームである確率」に注目した確率一致法という方法論があります。Thompson Samplingとはベイズ統計の枠組みをこの確率一致法に適用した方策です。

Thompson Sampling方策では、アームiの期待値のパラメータ\mu_iが何らかの事前分布\pi_i(\mu_i)から生成されるとし、時刻tまでの観測{\cal H} (t)が得られたときの期待値\mu_iの事後分布を考えます。

「アームiが最適」という命題は、「あるx\mu_i=x、かつすべてのj\neq i\mu_j\leq x」と解釈でき、数式を用いると以下のように表現できます。

\displaystyle\pi(\mu_i = \mu^* |{\cal H} (t)) = \int \pi(x_i|{\cal H} (t))\left(\prod_{j\neq i}\int_{x_j\leq x_i}\pi_j(x_j|{\cal H} (t))dx_j\right)dx_i

この式を各アームについて計算し事後確率を比較すれば良いのですが、一般にこの式を計算することは困難なことが知られています。

しかし実際に事後確率を直接比較する必要はなく、以下の手続きを行うことで「期待値最大の事後確率」に基づいたアームの選択を実現することができます。

①各アームの期待値の事後分布\pi(x_i|{\cal H}(t))から乱数\tilde{\mu}_iを生成

\tilde{\mu}_iを最大にするアームiを引く

Thompson Sampling方策はUCB方策に比べて余分な探索が少なくなることが知られていて、有限の試行回数でより良い性能を達成することができます。

方策の紹介は以上にして、これら3つの方策(ε-greedy方策、UCB方策、Thompson Sampling方策)とランダムにアームを選んだ場合との比較実験を行います。

実験では10本のアームを合計で10,000回選ぶ中で累積報酬を最大化することを目指します。得られる報酬は0/1のベルヌーイ分布に従い、パラメータpはそれぞれ以下のように設定します。(プレイヤーはこの情報を知らないという前提です)

アーム 1 2 3 4 5
確率 5.4% 6.9% 8.0% 9.7% 11.2%
アーム 6 7 8 9 10
確率 11.9% 12.1% 14.4% 15.5% 17.4%

例えば全てアーム1を選択した場合、報酬を受け取る回数の期待値は540回(0.054 × 10,000回)になり、

全てアーム10(最適なアーム)を選択した場合、報酬を受け取る回数の期待値は1,740回(0.174 × 10,000回)になります。

結果にランダム性があるのでモンテカルロシミュレーションを行い、方策ごとの累積報酬の平均値で評価を行うこととします。

実験の結果は以下の図のようになりました。(実験のコードはページ下部に記載)

bandit_test_image_64_48

右の図がアルゴリズムごとの累積報酬を可視化した図で、方策ごとの平均累積報酬を表しています。方策に従ってアームを選択した場合では、ランダムに選択する場合と比べ累積報酬が高くなっていることがわかります。今回の実験で最も累積報酬が高かったのはThompson Sampling方策で約1,700回報酬を受け取っています。ランダムに選択した場合の累積報酬が約1,100回なので、50%以上の改善となりました。

左の図はアルゴリズムことに最適アームの選択割合を可視化した図で、横軸が試行回数、縦軸が最適なアームを選択した割合を示しています。一番下の青い線がランダムにアームを選んだ場合で、およそ1/(アームの本数)の確率で最適なアームが選択されている様子が見られます(今回は1/10で10%)。紹介した3つの方策では、試行回数が多くなるにつれて最適なアームを選択する割合が多くなっている様子がわかります。

最適なアームの選択割合についてもThompson Sampling方策が最も高く、最終的に約90%の割合で最適なアームを選択しています。

また、ε-greedy方策とUCB方策について2つの図を見比べると、最適腕の選択割合(左図)ではε-greedy方策が割合が高いにも関わらず、累積報酬(右図)ではUCB方策の方が多く報酬を獲得しています。これは探索時にアームをランダムに選択するε-greedy方策と比べ、UCB方策はアームごとのUCBスコアに基づいてアームを選択することで悪いアームの選択回数を抑え、全体として比較的期待値の高いアームを多く引いたためと考えられます。

実問題では報酬を最大化することだけではなく、計算効率についても考慮する必要があり、紹介した方策以外にもいくつかの方策が提案されています。

今回は基礎編ということでバンディット問題の基本思想と代表的な方策についてご紹介しましたが、様々な応用問題も考えられています。例えば、アームから受け取る報酬の確率分布が時間変化する場合や、選択できるアームが時間変化する場合、アームごとに選択できる回数に上限がある場合などがあります。

次回はそんな応用例の1つである文脈付きバンディット問題について紹介する予定です。

参考文献

Bandit Algorithms for Website Optimization O’Reilly
バンディット問題の理論とアルゴリズム(機械学習プロフェッショナルシリーズ) 講談社

以下シミュレーションに使用したコード
(環境はpython3、https://github.com/johnmyleswhite/BanditsBook を参考に作成)

各アルゴリズム(model.pyとして保存)

# -*- coding:utf-8 -*-
import numpy as np
import random

class BernoulliArm():

    def __init__(self, p):
        self.p = p

    def draw(self):
        if random.random() > self.p:
            return 0.0
        else:
            return 1.0

class random_select():

    def __init__(self, counts, values):
        self.counts = counts
        self.values = values

    def initialize(self, n_arms):
        self.counts = np.zeros(n_arms)
        self.values = np.zeros(n_arms)

    def select_arm(self):
        return random.randint(0, len(self.values) - 1)

    def update(self, chosen_arm, reward):
        self.counts[chosen_arm] = self.counts[chosen_arm] + 1
        n = self.counts[chosen_arm]
        value = self.values[chosen_arm]
        new_value = ((n - 1) / float(n)) * value + (1 / float(n)) * reward
        self.values[chosen_arm] = new_value

class EpsilonGreedy():

    def __init__(self, epsilon, counts, values):
        self.epsilon = epsilon
        self.counts = counts
        self.values = values

    def initialize(self, n_arms):
        self.counts = np.zeros(n_arms)
        self.values = np.zeros(n_arms)

    def select_arm(self):
        if random.random() > self.epsilon:
            return np.argmax(self.values)
        else:
            return random.randint(0, len(self.values) - 1)

    def update(self, chosen_arm, reward):
        self.counts[chosen_arm] = self.counts[chosen_arm] + 1
        n = self.counts[chosen_arm]
        value = self.values[chosen_arm]
        new_value = ((n - 1) / float(n)) * value + (1 / float(n)) * reward
        self.values[chosen_arm] = new_value

class UCB():

    def __init__(self, counts, values):
        self.counts = counts
        self.values = values

    def initialize(self, n_arms):
        self.counts = np.zeros(n_arms)
        self.values = np.zeros(n_arms)

    def select_arm(self):
        n_arms = len(self.counts)
        if min(self.counts) == 0:
            return np.argmin(self.counts)

        total_counts = sum(self.counts)
        bonus = np.sqrt((np.log(np.array(total_counts))) /
                        2 / np.array(self.counts))
        ucb_values = np.array(self.values) + bonus
        return np.argmax(ucb_values)

    def update(self, chosen_arm, reward):
        self.counts[chosen_arm] = self.counts[chosen_arm] + 1
        n = self.counts[chosen_arm]
        value = self.values[chosen_arm]
        new_value = ((n - 1) / float(n)) * value + (1 / float(n)) * reward
        self.values[chosen_arm] = new_value

class ThompsonSampling():

    def __init__(self, counts_alpha, counts_beta, values):
        self.counts_alpha = counts_alpha
        self.counts_beta = counts_beta
        self.alpha = 1
        self.beta = 1
        self.values = values

    def initialize(self, n_arms):
        self.counts_alpha = np.zeros(n_arms)
        self.counts_beta = np.zeros(n_arms)
        self.values = np.zeros(n_arms)

    def select_arm(self):
        theta = [(arm,
                  random.betavariate(self.counts_alpha[arm] + self.alpha,
                                     self.counts_beta[arm] + self.beta))
                 for arm in range(len(self.counts_alpha))]
        theta = sorted(theta, key=lambda x: x[1])
        return theta[-1][0]

    def update(self, chosen_arm, reward):
        if reward == 1:
            self.counts_alpha[chosen_arm] += 1
        else:
            self.counts_beta[chosen_arm] += 1
        n = float(self.counts_alpha[chosen_arm]) + self.counts_beta[chosen_arm]
        self.values[chosen_arm] = (n - 1) / n * \
            self.values[chosen_arm] + 1 / n * reward

def test_algorithm(algo, arms, num_sims, horizon):
    chosen_arms = np.zeros(num_sims * horizon)
    cumulative_rewards = np.zeros(num_sims * horizon)
    times = np.zeros(num_sims * horizon)
    for sim in range(num_sims):
        algo.initialize(len(arms))
        for t in range(horizon):
            index = sim * horizon + t
            times[index] = t + 1
            chosen_arm = algo.select_arm()
            chosen_arms[index] = chosen_arm
            reward = arms[chosen_arm].draw()
            if t == 0:
                cumulative_rewards[index] = reward
            else:
                cumulative_rewards[index] = cumulative_rewards[
                    index - 1] + reward
            algo.update(chosen_arm, reward)
    return [times, chosen_arms, cumulative_rewards]

シミュレーション用

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from model import (BernoulliArm,
                   random_select,
                   EpsilonGreedy,
                   UCB,
                   ThompsonSampling,
                   test_algorithm)
import random

n_arms = 10
means = [0.054,  0.069,  0.080,  0.097,  0.112,
         0.119,  0.121,  0.144,  0.155,  0.174]

epsilon = 0.2  # パラメータ
sim_num = 500  # シミュレーション回数
time = 10000  # 試行回数

arms = pd.Series(map(lambda x: BernoulliArm(x), means))

algo_1 = random_select([], [])           # random
algo_2 = EpsilonGreedy(epsilon, [], [])  # epsilon-greedy
algo_3 = UCB([], [])                    # UCB
algo_4 = ThompsonSampling([], [], [])   # ThompsonSampling
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
heights = []
random.seed(2017)
for algo in [algo_1, algo_2, algo_3, algo_4]:
    algo.initialize(n_arms)
    result = test_algorithm(algo, arms, sim_num, time)

    df_result = pd.DataFrame({"times": result[0], "chosen_arms": result[1]})
    df_result["best_arms"] = (df_result["chosen_arms"]
                              == np.argmax(means)).astype(int)
    grouped = df_result["best_arms"].groupby(df_result["times"])

    ax1.plot(grouped.mean(), label=algo.__class__.__name__)
    heights.append(result[2][-1])

ax1.set_title("Compare 4model - Best Arm Rate")
ax1.set_xlabel("Time")
ax1.set_ylabel("Best Arm Rate")
ax1.legend(loc="upper left")

plt_label = ["Random", "Epsilon\nGreedy", "UCB", "Tompson \nSampling"]
plt_color = ["deep", "muted", "pastel", "bright"]
ax2.bar(range(1, 5), heights, color=sns.color_palette()[:4], align="center")
ax2.set_xticks(range(1, 5))
ax2.set_xticklabels(plt_label)
ax2.set_label("random_select")
ax2.set_ylabel("Cumulative Rewards")
ax2.set_title("Compare 4model - Cumulative Rewards")
plt.show()

  1. アルゴリズムの定義にはいくつかありますが「Bandit Algorithms for Website Optimization」のものを記述しています。 

学生向けディープラーニング実践イベントを開催しました

$
0
0

こんにちは。人事・広報の小國です。

以前techtalkへ参加させていただいたときに、
学生の皆さまと触れ合う機会を是非自社でも開催したい!と熱望し、
今回実行に移すことができましたー☆

イベントのテーマは「ディープラーニングを体験してみよう」です。
ディープラーニングとChainerの簡単な解説と、MNISTの数字分類のネットワークで、
精度を落とさずにどれだけ学習を高速化できるか・・・
参加いただいた学生の皆さまに競っていただきました♪

今回はPCも当社で準備するなどの関係から、少なめの定員での開催となりましたが、
師走の多忙な時期にお集まりいただき感謝感激です。

↓↓↓ 開催日程などはこんな感じ ↓↓↓
開催日:2016年12月22日(木)13:30~18:00
場 所:ALBERTセミナールーム
講 師:データ分析部 最上 嗣生

ご挨拶や当日の説明の後、早速データ分析部の最上よりレクチャーが開始されました。
IMG_1374
ディープラーニングとChainerの解説ですね。
その後実習です。真剣な空気の中で撮影音が邪魔にならないかとドキドキ・・・

本イベントでは精度を落とさずにどれだけ学習を高速化できるか、
参加いただいた皆様で競っていただきましたので、もちろん表彰もあるわけです。

IMG_1385
熱い気持ちをぶつける代表の上村。
熱い気持ちのまま、上村より表彰と賞品の進呈を行いました!

その後はお待ちかねの懇親会です。
データ分析部のメンバーも集まって、大勢でわいわいお話したり、食べたり飲んだり!
(食べるのに夢中になってそのときの画像がないのはご愛敬・・・・ごめんなさい)
アカデミア出身社員の身の上話から、物理学の将来や、果ては哲学まで。
盛り上がった皆さまの会話はどこまでも膨らんでいくのです。

IMG_1388
いつの間にかホワイトボードでお勉強会も始まっていました。

あまりに楽しかったので時間を忘れてしまうところでしたが、ちゃんと閉会。
学生の皆さまには、帰り際に「楽しかったです」と言っていただけて感無量なのです。
そして、またやるぞー!と心に誓うALBERTメンバーなのでした☆

Semi-Supervised Learning

$
0
0

はじめまして、データ分析部のシャオです。今回は半教師あり学習(Semi-supervised Learning)を英語で紹介します。

This blog introduces representative methods in section 2, including Generative Model, Support Vector Machines, Graph-Based Model and co-training. In section 3, the demonstration of text classification by R is presented. The co-training exmaple is in section 3.2.1 and generative model is in section 3.2.2.

1 Introduction

Semi-supervised learning (SSL) is a technique that uses both labeled and unlabeled data. It is between supervised learning (using labeled data) and unsupervised learning (using unlabeled data only). Usually, there are a great amount of unlabeled data which are easy and cheap to acquire but only few labeled data. For instance, we can easily extract website text data by web crawlers. However, labelling an extremely small amount of these data is not only expensive but also time-consuming.

2 Theory and Methods

How can unlabeled data help to learn a better classifier? The ideal situation is that we find a classifier which can correctly predict labeled data and make unlabeled data fit the model well too. In Balcan and Blum paper (2010), they use the notation of compatibility between data distribution and the target function. For example, now we have 10 labeled data and linear classifier as Figure 1. The black separator looks good, since it perfectly classifies the label data. However, after seeing unlabeled data, we could find the possible per class data distribution, P(x|c). Then, if the distribution is true, it is obvious the blue one is better than the black one. In this situation, the target function is compatible with data distribution if it does not slice through any high-density zone of any class.




Figure 1

The following part focuses on binary classifier. Here are symbols used:
l: labeled data.
u: unlabeled data.
f: binary classifier.
y: true label.
x: feature.

2.1 Generative Model

The generative model assumes the joint probability is p(x,y|\theta)=p(y|\theta)p(x|y,\theta). p(y|\theta) is the class prior distribution. p(x|y,\theta) is the class conditional distribution. By Bayes rule, we can find the classifier

f_\theta (x) \equiv \text {argmax}_y p( y | x, \theta) \equiv \text {argmax}_y \frac {p(x,y|\theta)} {\sum_y' p(x, y'| \theta)}

For SSL, besides how good the classifier (target function) is, a good classifier should be compatible with data distribution. That is, the likelihood,p(x_i,y | \theta), should be high. For unlabeled data, the likelihood is \sum_{y \in \mathcal{Y}} p(x_i,y | \theta) because the exact label y is unknown. Consequently, the \hat \theta is the one maximizing log likelihood:

\text {argmax} _{\theta} \ \text{logp} ( { x_i, y_i } _l |\theta) + \lambda \text{logp} ( { x_i } _u |\theta)

where \lambda is a balancing weight between [0,1]. The first log term is log likelihood of all labeled data, and the second log term is the log likelihood of all unlabeled data. If \lambda=0, it retreats to usual supervised learning (SL). With \hat \theta in hand, we can obtain classifier f_{\hat \theta}(x)

2.2 Support Vector Machines

A good SVM has the decision boundary with large margin, and few data falls into the margin. Margin is the region bounded by two hyperplanes. If an instance falls into the margin, classifier is not confident which group it should belong to. Therefore, a good classifier should prevent data from falling into the margin.

For labeled instances, SVM will punish the instance that falls onto the wrongside. The farther the wrong prediction from the boundary, the more punishment it gets. For unlabeled instances, semi-supervised SVM (or S3VM) punishes data which falls into margin. When too many data fall into the margin, it means this classifier thinks these data could be either way. This is the last thing we want because it simply loses its function of classification. To punish this situation, hat loss is proposed. Hat loss is \text{max} (1-|f(x)|,0) . When data is outside the boundary, 1-|f(x)| < 0 , no hat loss at all. The hat loss will be largest when the instance stands in the middle of the margin. Hence, to find the good classifier f is to minimize:

\sum_{i \in l} max (0, 1-y_if(x_i)) + \lambda_1 ||f||^2 + \lambda_2 \sum_{i \in u} max (0, 1-|f(x_i)|)

The first and second term are the supervised SVM’s hinge loss and regularization term of labeled data. The third term is the hat loss of unlabeled data.

However, hat loss is non-convex function so it is hard to find a solution. Therefore, there are different approaches to find the optimal solution. The following introduces some of them.

2.2.1 SVM light

SVM light first trains supervised SVM by labeled data, then uses it to classify some
proportion of unlabeled data. Then, start with a small \lambda_2 and train a classifier f to minimize:

\sum_{i \in l} max (0, 1-y_if(x_i)) + \lambda_1 ||f||^2 + \lambda_2 \sum_{i \in u} max (0, 1-y_if(x_i))

After obtaining a new classifier, try to switch unlabeled data. The switch is made if two are labeled differently by the new classifier and if hinge loss will decrease after switching. The figure 2 shows some switchable cases. The red line is the hinge loss. After switching, there is no hinge loss in case 1. In case 2 and 3, hinge loss decreases.


Figure 2

After no labels are switchable anymore, SVM light is going to train a new classifier. In this step, it uses the higher \lambda_2 than the previous one, together with the updated labels from the switching step. Then, train a new f and switch labels again until \lambda_2 =1 or user-defined control level.

The higher \lambda_2 is, the higher it thinks of unlabeled data. It plays the same role as \lambda in generative model in 2.1 section.

2.2.2 Gradient descent based S3VM

∇S3VM smooths the hat loss with a similar-looking Gaussian curve. By doing so, it can approximate the objective function which now is differentiable.

2.2.3 Concave-convex procedure (CCCP)

Concave-convex procedure (CCCP) also deals with hat loss function. Hat loss function is the sum of convex and concave term. That is, \text{max}(1-|f|,0) = \text{max} (|f|-1)+1-|f(x)|. CCCP uses the sequence of z and linearized the concave part R_{cave} (w)=f(z)+\nabla f(z)'(w-z) . Hence, CCCP can find the sequence of local optimal of convex function and just pick the best one.

2.3 Graph-Based Models

The graph G={V,E}. V is vertices, all labeled and unlabeled data here. E is undirected edges connecting instances x_i,x_j. w_{ij} is the weight reflecting the proximity of x_i,x_j. The logic is simple: if two instances are alike, a good classifier should classify them into the same class. Then f is:

\text {argmin}_f {\sum} _{i,j \in { l,u } } w _{ij} (f(x_i) - f(x_j))^2
2.3.1 Mincut

One algorithm is mincut algorithm. Mincut finds a cut which costs (weight) the least and to separate the graph into two sides. The intuition is that if x_i,x_j are so unlike, it does not hurt if f classifies them into two groups. The mincut algorithm tries iteratively ( \infty ) to solve:

min \ \infty \sum_{i \in l} (y_i-Y_i)^2+\sum_{i,j \in { l,u } } w_{ij}(y_i-y_j)^2

The frist term is to minimize loss of labeled data, and the second term is to find the mincut.

2.3.2 Manifold regularization

Another example is manifold regularization algorithm. It relaxes labels to be continuous. The classification is based on the sign of f(x). This algorithm tries different weight (\lambda_1, \lambda_2), and kernel h to parameterize x into the same domain as f, where f(x)=h(x)+b. With kernel h, it could construct similarity graph, and obtain weights among instances. The classifier f is:

\text { argmin } _f {\sum} _{i \in l} \text { max } (0, 1 - y_if(x_i) ) + \lambda_1 \Vert h \Vert^2 + \lambda_2 {\sum} _{i,j \in { l,u } } w _{ij}(f(x_i) - f(x_j))^2

The first and second term is hinge loss and regularization term. The third term is as same as above, the punishment term when f classifies two similar samples into different groups.

2.4 Co-training

Sometimes, data have multiple views. For web pages, we can have image features and text features. The co-training trains each feature by labeled data alone first. Then, it uses feature classifiers to label unlabeled data. It selects the k-most-confident data of one classifier as the training data for other feature classifiers next time. That is, it uses the unlabeled data which are not vulnerable to misclassification, just as labeled data. For instance, if some unlabeled documents can easily be classified by its text feature. We can use their image feature and predicted labels based on text feature to train image feature classifier.

When can co-training be used? First, it should be able to train a good classifier when using one view alone. If it cannot achieve this sufficient assumption, the k-most-confident data are useless since the classifier itself is not trustable. Another assumption is independent assumption. The feature should be conditionally independent given the label. Under this assumption, the unlabeled instances with its predicted labels based on one feature classifier will distribute randomly to other classifiers, so it is as informative as random samples.

If co-training works well, no matter which feature classifier is used, the predicted label should be the same. Thence, co-training includes multiple view classifiers f^{(1)}, f^{(2)}, ..., f^{(m)} are functions to minimize:

\sum_{j=1}^m (\frac{1}{l} \sum_{i=1}^l c^{(j)}(f^{(j)}(x_i), y_i) + \lambda_1 \Omega^{(j)}(f^{(j)})) \ + \lambda_2 \sum_{i \in u} \sum_{j,k=1}^m c(f^{(j)}(x_i), f^{(k)}(x_i))

Here, c is loss function and Ω is regularization term. The first summation is the same as supervised learning of m view classifiers. The second summation is to punish f when multi classifiers cannot lead to the same prediction for the same instance.

Nevertheless, most datasets are one-view but with several features. Then making a fake feature split is an option. If after splitting, the sufficient and independent assumption can be satisfied, co-training will work well, too. Here are four splitting methods introduced in Du et.all (2011):

2.4.1 Random split

As its name, it just splits the feature randomly and verify assumptions empirically.

2.4.2 Entropy split

It calculates entropy of each features first. The larger the entropy is, the higher predictive power it has. If we want to split into two views, then we simply assign the odd-number (first, third, etc.) highest entropy features to view 1, and even-number (second, fourth, etc.) highest features to view 2. By doing so, sufficient assumption is satisfied since both views have some high-entropy features, but it does not ensure independent assumption.

2.4.3 Entropy-Start Hill Climbing

To solve the previous problem, entropy-start hilling climbing is proposed. It switches each feature to other view once, so generates a group of new splits. Then, we can verify two assumptions empirically using these splits and pick the most qualified one.

2.4.4 Random-Restart Hill Climbing

Since entropy-start hill climbing only starts from the entropy-based split, it might be stuck in local optima. Random-restart hill climbing starts from n random splits and searches the splits which are best compatible with co-training assumptions empirically.

3 R example

The dataset used here is a tiny version of 20 Newsgroups (available at http://www.cs.nyu.edu/~roweis/data.html). It contains 16,242 articles and binary occurrence data for 100 words. The label is the newsgroup to which the article was posted. These 20 newsgroups can be classified into 4 subjects, computer, recreation, science, and talk (http://qwone.com/~jason/20Newsgroups/ for details).

We are going to demonstrate two simple codes of co-training with fake split and generative model, all by Expectation-Maximization (EM) algorithm and under naïve Bayes assumption.

3.1 Theory behind codes

The example here basically follows Nigam et al ‘s paper (2000), so please refer it for details.

Naïve Bayes classifier assumes

p(x|y=c, \theta)=\prod_{j=1}^D p(x_j|y=c, \theta_{jc})

\theta is parameter and will be revisited later. It is naïve because it assumes features to be mutually independent.

The document consists of a sequence of words. The generative process of a document d_i, which belongs to class c_j, are as follows. First, decide the length of document, |d_i|, based on p(|d_i|). Then, generate each word according to class-specific word probability, p({ w_{d_i,1}, w_{d_i,2}, ..., w_{d_i,|d_i|} } |c_j; \theta). Here, naive Bayes are applied to assume each word is generated independently, though it is unrealistic. Thus, class- specific word probability can be written as product of p(w_{d_i, k}|c_j; \theta). In summary,

p(d_i|c_j; \theta)=p(|d_i|) \prod_{k=1}^{|d_i|}p(w_{d_i, k}|c_j; \theta)

With a class-specific document distribution in hand, the document d_i can be created by selecting a class according to prior class probability p(c_j| \theta). Then, the likelihood of document is

p(d_i|\theta)=\sum_{j \in C} p(c_j| \theta)p(d_i|c_j; \theta)

Hence, the document distribution is multinomial, with mixture weight p(c_j| \theta). The common conjugate prior for multinomial is Dirichelt distribution. P(\theta)=Dir(\theta|\alpha). We do not discuss further here, but just remember \alpha is a constant greater than 1. In the following code, \alpha is set 2, resulting in Laplace smoothing.

Turn to EM algorithm. In E-step, it labels unlabeled data by using current prior class and class-specific word probability. In M-step, it re-estimates these probabilities by using the updated labels in E-step. Other parameter used in EM algorithm is \lambda, which is mentioned above. By setting \lambda > 0 , EM uses unlabeled data in training classifier.

3.2 R code

First, the function used are as follows:

# prior_class : p(class | theta)
# word_p: p(w|class, theta)
estimate_theta <- function(l.x, l.y, u.x, u.y, nV, lambda) {
	
	if (!is.null(u.y)) u.x <- matrix(u.x, nrow=length(u.y))
	l.x <- matrix(l.x, nrow=length(l.y))
	prior_class <- sapply(class, function(c) {
		nomi <- 1 + lambda * length(which(u.y==c)) + length(which(l.y==c))
		denomi <- nC + lambda * length(u.y) + length(l.y)
		return (nomi/denomi)
	})

	word_p <- lapply(class, function(c) {
		if (length(which(u.y==c)) == 0 ) { u_xc <- t(matrix(rep(0, nV), nc=1)) } else {
			u_xc <- matrix(u.x[which(u.y==c),],nc=nV) }
		if (length(which(l.y==c)) == 0 ) { l_xc <- t(matrix(rep(0, nV), nc=1)) } else {
			l_xc <- matrix(l.x[which(l.y==c),],nc=nV) }

		nomi <- 1 + lambda * apply(u_xc, 2, sum) + apply(l_xc, 2, sum)
		return (nomi/sum(nomi))
	})
		
	word_n_p <- lapply(class, function(c) {
		if (length(which(u.y!=c)) == 0 ) { u_xc <- t(matrix(rep(0, nV), nc=1)) } else {
			u_xc <- matrix(u.x[which(u.y!=c),],nc=nV) }
		if (length(which(l.y!=c)) == 0 ) { l_xc <- t(matrix(rep(0, nV), nc=1)) } else {
			l_xc <- matrix(l.x[which(l.y!=c),],nc=nV) }
		nomi <-  1 + lambda * apply(u_xc, 2, sum) + apply(l_xc, 2, sum)
		return (nomi/sum(nomi))
	})

	return(list(prior_class=prior_class, word_p=word_p, word_n_p=word_n_p))
}

# classifier #
classifier <- function(model, x) {
	prior_class <- model$prior_class
	word_p <- model$word_p
	x <- matrix(x, nr=ifelse(is.null(nrow(x)), 1, nrow(x)))
	est_class_p <- sapply(1:nrow(x), function(d) {
				nomi <- sapply(class, function(c) prior_class * prod(word_p[c][which(x[d,]==1)]) )
				return(nomi/sum(nomi))
			}) %>% matrix(., nr=nC) %>% t()

	label <-  apply(est_class_p , 1, function(a) order(a)[max(class)])
	return(list(label=label, p=est_class_p ))
}

log_theta <- function(prior_class, word_p) {sum (log(prior_class))+ sum(log(unlist(word_p)) )}
log_D <- function(prior_class, word_p, x, z) {
	x <- matrix(x, nr=ifelse(is.null(nrow(x)), 1, nrow(x)))
	lik_D <- 0
	for (c in class) {	
				lik_D <- lik_D + z[,c] %*% (log(prior_class) +  x %*% log(word_p[c]) + (1-x) %*% log(1-word_p[c] ) )
	}
	return(lik_D)
}


EM <- function(init, epsilon, lambda, l.x, l.y, u.x, nV) {
	model <- init
	lik <- -10^20
	prob.ly <- matrix(sapply(class, function(c) ifelse(l.y==c,1,0)), nr=length(l.y))
	iter <- 0 			
 	repeat {
		iter <- iter + 1
		# E step (label u.x by current prior_class and word_p)
		label_u <- classifier(model, u.x)

		# M step: re-estimate prior_class, word_p
		new_model <- estimate_theta(l.x, l.y, u.x, label_u$label, nV, lambda)
		new_lik <- log_theta(new_model$prior_class, new_model$word_p) + 
				log_D(new_model$prior_class, new_model$word_p, l.x, prob.ly) + 
				lambda * log_D(new_model$prior_class, new_model$word_p, u.x, label_u$p) 

		print(paste('iteration=', iter, '; loglikelihood=', new_lik))
		if ( (new_lik - lik) > epsilon) {
			model <- new_model
			lik <- new_lik
		} else { break } 
	}# repeat
	
	return(model)	
}

result <- function(model, x, y, top) {
	label <- classifier(model, x)$label
	tb <- table(label, y)
	accuracy <- sum(diag(tb))/sum(tb)

	word_p <- model$word_p
	word_n_p <- model$word_n_p
	ratio <- sapply(class, function(c)  word_p[c] * log(word_p[c]/word_n_p[c]))
	top_word <- sapply(class, function(c) word_list[order(-ratio[,c])]) [1:top, ] 
	ratio <- sapply(class, function(c) ratio[order(-ratio[,c]), c]) [1:top, ] 

	list(tb=tb, accuracy=accuracy, top_word=top_word, log_ratio=ratio)
}


Load packages we need. Then randomly use 16 labeled instances (4 from each class) and 15,000 unlabeled instances.

library(dplyr)
library(R.matlab)
library(ggplot2)
library(entropy)
library(wordcloud)

# ----------------- load data ---------------------------#
# available: http://www.cs.nyu.edu/~roweis/data.html
data<-readMat("20news_w100.mat")
data$document <- as(data$document, 'Matrix') %>% t()
word_list <- data$wordlist %>% unlist()
x <- data$document
y <- data$newsgroups

# ----------------- set parameter ---------------------#
nC <- data$groupnames %>% unlist() %>% length()
nV <- data$wordlist %>% unlist() %>% length()
class <- c(1:4)
l.nD <- 4
u.nD <- 15000
n.split <- 2
lambda <- 1

# ---------------------- sample data -------------------#
set.seed(25)
l.select <- sapply(class, function(c) sample(which(y==c), l.nD)) %>% as.numeric()
l.y <- data$newsgroups[l.select]
l.x <- data$document[ l.select, ] 
set.seed(25); u.select <- sample((1:nrow(data$document))[-l.select], u.nD)
u.x <- data$document[u.select,] 
u.y <- data$newsgroups[u.select]
3.2.1 Co-training with fake split

Since this dataset is one-view dataset, we make entropy split here into two views. Then obtain the initial view 1 and view 2 classifier, SSL_sp.

#---------------------------co training --------------------------------------------#
#--------------- fake split (based on entropy) ------------------#
order <- apply(x, 2, function(xx) entropy(xx,method='Laplace')) %>% order(-.)
split <- lapply(1:n.split, function(i) order[seq(i,100,n.split)])
l.x.sp <-  lapply(split, function(s)  l.x[,s])
u.x.sp <-  lapply(split, function(s)  u.x[,s])

SSL_sp <- lapply(1:n.split, function(s) {
		init1 <- estimate_theta(l.x.sp[[s]], l.y, u.x=NULL, u.y=NULL, length(split[[s]]), lambda)
		EM(init1, epsilon=0, lambda, l.x.sp[[s]], l.y, l.x.sp[[s]], length(split[[s]])) 
	})


Co-training only uses k-confident unlabel instances to train other feature classifiers. Here, set k=250.


#-------------- co-training -----------------------#
co_training <- function (l.x.sp, l.y, u.x.sp, split, SSL_sp, k) {
	iter <- 0
	loss <- 10^5
	pred_u <- lapply(1:n.split, function(s) classifier(SSL_sp[[s]], u.x.sp[[s]]) )
	repeat {
		iter <- iter + 1
		# k-confident-ux
		k_pred_u <- lapply(1:n.split, function(s) 
					apply(pred_u[[s]]$p, 2, function(c) order(-c))[1:k,] %>% as.numeric()
		)

		new_SSL_sp <- lapply(1:n.split, function(s) {
			new.l <- unlist(k_pred_u)[-seq((s-1)*k +1, s*k)]
			pred <- lapply(pred_u, function(u) u$label) %>% unlist()
			EM(SSL_sp[[s]], epsilon=0, lambda, 
						l.x=rbind(l.x.sp[[s]],u.x.sp[[s]][new.l,]), 
						l.y=c(l.y,pred[new.l]), 
						u.x=rbind(l.x.sp[[s]],u.x.sp[[s]][new.l,]), 
						length(split[[s]])
			)
		})

		pred_u <- lapply(1:n.split, function(s) classifier(new_SSL_sp[[s]], u.x.sp[[s]]) )
		pred_l <- lapply(1:n.split, function(s) classifier(new_SSL_sp[[s]], l.x.sp[[s]]) )

		mis_pred <- lapply(pred_l, function(pred) ifelse(pred$label==l.y, 0, 1))  %>% unlist() %>% sum()
		pred_u_label <- lapply(pred_u, function(c) c$label)
		pair <- combn(1:n.split, 2)
		nonagree <- apply(pair, 2, function(r)
			ifelse(pred_u_label[[r[1]]] ==  pred_u_label[[r[2]]], 0, 1)
		) %>% as.numeric() %>% sum()

		new_loss <- mis_pred+nonagree
		print(new_loss)
		if ( new_loss < loss ) {
			SSL_sp <- new_SSL_sp
			loss <- new_loss
		} else {break}
	} # repeat
		
	return(SSL_sp)
}

co_SSL<- co_training(l.x.sp, l.y, u.x.sp, split, SSL_sp,k=250)

To evaluate the final model, we predict all data by classifier 1 and 2 to obtain its probabilistic labels. Then sum them up, and the label gets the majority probability wins.

class_p <- lapply(1:n.split, function(s) classifier(co_SSL[[s]], x[,split[[s]]])$p)
class_p <- sapply(class, function(c) 
			lapply(class_p, function(p) p[,c]) %>% do.call(cbind, .) %>% apply(., 1, sum)
		)
label <- apply(class_p, 1, function(pp) order(-pp)[1])

The accuracy is 0.7067.

3.2.2 Generative model

Run SL model and SSL model (lambda=1). Lambda is adjustable but set one here.

#---------------------------weighted EM -------------------------#
init <- estimate_theta(l.x, l.y, u.x=NULL, u.y=NULL, nV, lambda=1)
SL_EM <- EM(init, 0, lambda, l.x, l.y, l.x, nV) 
SSL_EM <- EM(init, 0, lambda, l.x, l.y, u.x, nV) 

SL_result <- result(SL_EM, x, y, nV)
SSL_result <- result(SSL_EM, x, y, nV)
c(SL_result$accuracy, SSL_result$accuracy)

SSL model improves accuracy from 0.5195 to 0.7867. Then, we can check the words which have higher predictive power. The predictive power here is weighted log likelihood ratio.

p(w_{d_i, k}|c_j; \theta) log (\frac{p(w_{d_i, k}|c_j; \theta)}{p(w_{d_i, k}|\neg c_j; \theta)})

The first term tells the word occurs frequently has higher power. The second term indicates the word occurs frequently in one class but seldom in other classes has higher power. Figure 3 shows SSL can extract important keywords out.

	# ---------------top predictive words----------------#
pal2 <- brewer.pal(8,"Dark2")
word_plot <- function(model, class) {
	set.seed(343)
	wordcloud(model$top_word[,class],model$log_ratio[,class], scale=c(3,1),max.words=100, random.order=FALSE, rot.per=.15, colors=pal2)
}
word_plot(SL_result, 1)


Figure 3

We do not post further code here, but you can try different lambda, and numbers of labeled and unlabeled instances.
When having enough labeled instances, SL model performs almost the same as SSL model. Increasing more unlabeled data cannot improve accuracy significantly anymore.


Figure 4

The different lambdas have the same implication. When having few labeled data, SSL perfroms better, making the full use of unlabeled data (\lambda=1 ). On the contrary, with enough labeled data, SSL does not need to set high \lambda.


Figure 5




Reference

  • Balcan, M. F., & Blum, A. (2010). A discriminative model for semi-supervised learning. Journal of the ACM (JACM), 57(3), 19.
  • Du, J., Ling, C. X., & Zhou, Z. H. (2011). When does cotraining work in real data?. IEEE Transactions on Knowledge and Data Engineering, 23(5), 788-799.
  • Nigam, K., McCallum, A. K., Thrun, S., & Mitchell, T. (2000). Text classification from labeled and unlabeled documents using EM. Machine learning, 39(2-3), 103-134.
  • Zhu, X. (2011). Semi-supervised learning. In Encyclopedia of Machine Learning (pp. 892-897). Springer US.

コーポレートサイトをリニューアルいたしました!

$
0
0

すでにご覧いただいた方もいらっしゃるかと思いますが、
今月初めにALBERTのコーポレートサイトをリニューアルいたしました!

以前に比べ、ALBERTの理念や提供している分析・技術、サービスについてのボリュームを増やし、お客様はもちろんのこと、投資家の皆様、就職を希望する皆様など、あらゆる方々にALBERTをより深く知っていただけるサイトを目指しました。

他社の企業サイトではあまり見られない少し変わった試みとしては、経営理念のページに設けたVISIONです。



採用サイトに掲載しているALBERTの企業理念「会社の力は何によって定義されるかというと、それは社員の力の総和です。では社員の力は何によって決まるかといえば、素質×教育×熱意と言えます」と記載がある通り、スタッフの目指すもの・熱意の先に会社が目指すものがあるということをビジュアル的に表現したページとなっています。

また、以前からご好評をいただいていた、テクノロジーページについては「データ分析基礎知識」ページとして生まれ変わりました!

多くの方々にご利用いただいている当ページですが、以前より文字を大きくしたり、タグを登録して検索しやすくするなど、より使いやすいページを目指しました。

そして、今回のリニューアルについてご紹介する上で外せないのは、「CROSS TALK」ページです。

スタッフ座談会では、ALBERTスタッフがスタッフ目線でALBERTについてを、お取引先様との対談では、ALBERTとの実際の取り組みや、事業領域の今後の展望などをお話していただき、より様々な角度からALBERTを知っていただける読み物ページとなっています。

CROSS TALK」ページについては、今後も新たな目線でのALBERTをご覧いただくべく、随時更新してまいりますので、どうぞお楽しみに!!

今回は、5年ぶりの大幅リニューアルということで、このブログでは全てを紹介しきれません。
ぜひ、新しくなったALBERTコーポレートサイトをご覧ください♪

ベイズ統計によるアイオワ・ギャンブリング課題のモデリング方法

$
0
0

こんにちは。データ分析部の長尾です。

今回は心理学において有名な実験である「アイオワ・ギャンブリング課題」を題材として,ベイズ統計によるモデリング方法を紹介します。

突然ですが、パチンコのホールを思い浮かべてください。パチンコではまず,出玉の多そうな台を野生の勘で選び,その台でしばらく様子を見ます。出玉が少ないようであれば,他の台に移り,またしばらく打ちます。個人差はありますが,上述の探索行動を繰り返し,最終的には,一番期待できそうな台に落ち着いて,利益を最大化するよう努めます。このような意思決定のプロセスを模倣した実験のひとつにアイオワ・ギャンブリング課題(Iowa Gambling Tasks, IGT; Bechara, Damasio, Damasio & Anderson, 1994)があります。

IGTとは

IGTにおいて,実験参加者には,あらかじめ2000ドルの所持金がヴァーチャルに付与されます。参加者は図1で示したような4つのカードのデッキから,どれか1つのデッキを自由に選び,1枚カードを引く試行を繰り返し行います。

図1: 実験イメージ

カードの裏には報酬額もしくは損失額が記載されており,デッキ毎にそれらの金額と出現頻度は違います。表1に示したように,AとBのデッキからは毎回100ドルの報酬が得られるのに対し,CとDのデッキからは毎回50ドルと控えめです。しかしながら,10枚当たりの損失額はAとBのデッキでは1250ドルと大きいのに対し,CとDのデッキでは250ドルと抑えられています。結局,10枚当たりの純利益は,AとBのデッキで-250ドル,CとDのデッキで250ドルとなり,CもしくはDのデッキを選ぶ方が明らかに得です。

表1: デッキの性質(単位:ドル)

参加者は実験を通して,所持金を最大化するよう教示されます。このため,試行を繰り返し,デッキを探索しながら,それぞれのデッキの性質を学習することが参加者には求められます。最終的には,AとBのデッキを避け,CもしくはDのデッキを選ぶことが望ましい方略となります。しかしながら,健常群と比較して,麻薬中毒や強迫性障害といった臨床群ではこの方略を獲得する能力が低いことが指摘されており(Wetzels, Vandekerckhove, Tuerlinckx & Wagenmakers, 2010),IGTは様々な臨床群における意思決定能力の欠如の度合いを評価するために用いられます。

PVL-Deltaモデル

PVL-Deltaモデル(prospect valence learning delta model)はIGTにおける意思決定を評価するモデルとして, Ahn et al.(2008)によって提案されました。PVL-Deltaモデルは過去の試行に対する強化学習をルール化したモデルであり,4つのパラメータによって,参加者の心理プロセスを表現します。

まず,参加者は試行tで得られた純利益x^{(t)}から主観的効用u^{(t)}を評価すると仮定し,行動経済学で有名なプロスペクト理論を用いて,u^{(t)}を以下のように表現します。



ここで,\alpha(0 < \alpha < 1)は効用関数の形状母数であり,\lambda(0 < \lambda < 5)は損失の感度を表す母数です。\lambda > 1のとき,参加者は報酬よりも損失に敏感であるといえます。

次に,試行tにおけるデッキの選択をy^{(t)}とおき,参加者は得られた効用u^{(t)}から,次の試行t+1でデッキkから得られる期待効用E_{u_k}^{(t+1)}を以下のように評価すると仮定します。



母数a(0 < a < 1)は1つ前で得られた効用に対する注意の度合いを表し,デッキkが選ばれた場合のみ,期待効用を更新することから,更新比率とも呼ばれます。aの値が高い参加者は1つ前の試行で得られた効用に影響されやすく,それまでの経験を忘却する傾向があると解釈します。

さらに,試行tにおけるデッキの選択確率p_k^{(t)}をsoftmax関数を用いて,以下のように表現します。

p_k^{(t)}=\frac{\exp { (\theta\times E_{u_k}^{(t)}) }}{\sum_{j=1}^4 \exp { (\theta\times E_{u_j}^{(t)}) } }

ここで,\thetaはデッキの選択に対する感度を表し,0に近づくほどランダムな選択が行われ,大きい値をとるほど,期待効用に基づく選択が行われることを意味します。PVL_deltaモデルでは,選択の一貫性を表すパラメータc(0 < c < 5)を用いて,\thetaに以下のような変換が施されています。

\theta=3^c-1

すなわち,一貫性cの値が0に近いほどランダムな選択が行われ,逆にcの値が大きくなるにつれ,より確定的な選択が行われます。

最後に,観察される試行tにおけるデッキの選択y^{(t)}をカテゴリカル分布を用いて,以下のようにモデル化します。

y^{(t)} \sim Categorical(p_1^{(t)}, p_2^{(t)}, p_3^{(t)}, p_4^{(t)})

ひとりの参加者を分析

当社の社員7名(A~Gさん)から得たデータを用いて,ここでは参加者の1人であるDさんの分析を行います。PVL-Deltaモデルについて,母数\alphaaには区間(0,1)の一様分布,\lambdacには区間(0,5)の一様分布を仮定し,ベイズ推定を行います。

分析結果

Dさんの母数の事後分布の事後平均(EAP),事後標準偏差(post.sd),95%確信区間は表2のようになりました。

表2: Dさんの母数の推定結果

表2より,Dさんの更新比率aのEAP推定値は0.166と低い値を示しています。これは,Dさんは1つ前の試行で得られた効用にさほど影響されず,デッキの期待値を更新していることを示唆します。デッキの選択に対する一貫性はc=0.247であり,やや探索行動が多かったといえます。母数\alpha\lambdaのEAPを用いて描いたプロスペクト曲線は図2のようになりました。

図2: Dさんのプロスペクト曲線

図2より,Dさんは報酬より損失にやや敏感な傾向があることがみてとれます。

ここまで,ひとりの参加者に的を絞った分析を行ってきました。ベイズ推定の利点は母数に対する確信を区間で表現できる点ですが,表2における\lambdaの95%確信区間はやや広く,EAPへの十分な確信を保障しません。また,母数間での相関関係もひとりの参加者の分析からは明らかにできません。次節ではこれらの問題に対処する方法を紹介します。

階層ベイズモデルを用いた分析

階層ベイズモデルでは,参加者を表す添え字iを用いて,\alpha_i, \lambda_i, a_i, c_iと表現し,これら母数がさらに,それぞれ未知である超母数(hyper parameter)をもつ事前分布から発生するようにモデリングします。

心理学的な特性値である\alpha_i, \lambda_i, a_i, c_iは参加者間で正規分布していると仮定します。しかしながら,これら特性値の定義域は異なっているため,未知の平均ベクトルと共分散行列を持つ4変量正規分布から\bold{\delta}を発生させ,以下の変換を行います。

\alpha_i=\Phi(\delta_1|\mu_1, \sigma_1)
\lambda_i=5\times\Phi(\delta_2|\mu_2, \sigma_2)
a_i=\Phi(\delta_3|\mu_3, \sigma_3)
c_i=5\times\Phi(\delta_4|\mu_4, \sigma_4)

ここで,\Phi(|)は正規累積変換を表し,その値は0から1の間に収まります。\lambda_ic_iに関しては,さらに5を乗じることにより区間(0, 5)を実現しています。

超母数である4変量正規分布の平均ベクトルと共分散行列にはそれぞれ以下の正規分布と逆ウィシャート分布を事前分布として仮定します。

\mu_1, \mu_2, \mu_3, \mu_4 \sim Normal(0, 1)
\bold{\Sigma} \sim Wishart^{-1}(4, I_4)

最後に,\bold{\Sigma}から\alpha_i, \lambda_i, a_i, c_iに関する相関行列\bold{\Sigma}_\rhoを生成し,相関関係を検証します。

\bold{\Sigma}_\rho=diag(\bold{\Sigma})^{-1/2}\times\bold{\Sigma}\times diag(\bold{\Sigma})^{-1/2}
分析結果

当社の社員7名から得たデータ全てを用いた場合の階層ベイズモデルによる推定結果を表3に示します。

表3: 社員A~Gさんの推定結果

表3より,更新比率aが最も高い参加者はDさんであり,最も低い参加者はAさんです。過去の経験を忘れっぽいDさんとは対照的に,Aさんは過去の経験を鑑みた選択が出来ていたといえます。また,Dさんの\lambdaの事後標準偏差も0.439と抑えられ,前述の分析の問題点が改善されていることがわかります。

母数間の相関関係は表4のようになりました。

表4: 母数間の相関行列

表4より,\lambdaaの間には0.605と中程度の正の相関がみられます。この結果から,損失に対して敏感な参加者ほど,1つ前の試行で受けた損失に影響されやすいため,更新比率も高い値となるメカニズムが予想されます。また,acの間には,-0.603と中程度の負の相関が確認できます。これは過去の経験を鑑みた選択を行っている参加者ほど,同一のデッキを選択していたことを示唆します。

最後に,7名の社員それぞれのプロスペクト曲線を図3に示します。

図3: 社員A~Gさんのプロスペクト曲線

図3より,Aさん(赤)やEさん(紫)は純利益が正であれば,効用が極端に高くなり,純利益が負であれば,効用が極端に低くなる傾向があります。個人的には,AさんとEさんは,実験に対して並々ならぬモチベーションを有したギャンブラー気質の持ち主であるという印象を受けました。また,Bさん(緑)とCさん(青)は正の純利益に関しては,同じように効用を得ていますが,負の純利益に関しては,Cさんの方がさらに敏感に反応しており,プロスペクト理論の標準的な結果に近くなっています。

Stan・Rコード

まず,階層モデルのMCMCモデリングを行ったStanコードを以下に示します(‘PVL_delta_multi.stan’として保存)。

data {
    int<lower=1> T;
    int<lower=1> K;
    int<lower=1> N;
    matrix[4,4] R;
    int<lower=1,upper=K> y[T,N];
    int<lower=-1150,upper=100> x[T,N];
}
parameters {
    vector[4] Eta[N];
    vector[4] Mu;
    cov_matrix[4] Sigma;
}
transformed parameters {
    real<lower=0,upper=242> theta[N];
    real u[T,N];
    vector[K] Eu[T,N];
    simplex[K] p[T,N];
    real<lower=0,upper=1> alpha[N];
    real<lower=0, upper=5> lambda[N];
    real<lower=0,upper=1> a[N];
    real<lower=0, upper=5> c[N];

    for(n in 1:N){
        alpha[n] = normal_cdf(Eta[n,1],Mu[1],sqrt(Sigma[1,1]));
        lambda[n] = 5*normal_cdf(Eta[n,2],Mu[2],sqrt(Sigma[2,2]));
        a[n] = normal_cdf(Eta[n,3],Mu[3],sqrt(Sigma[3,3]));
        c[n] = 5*normal_cdf(Eta[n,4],Mu[4],sqrt(Sigma[4,4]));
    }
    for(n in 1:N){
        theta[n] = pow(3,c[n])-1;
        for(t in 1:T){
            if(x[t,n]>=0) u[t,n] = pow(x[t,n],alpha[n]);
            else u[t,n] = -lambda[n]*pow(fabs(x[t,n]),alpha[n]);
        }
    }
    for(n in 1:N){
        for(k in 1:K){
            Eu[1,n,k] = 0;    // 1回目の試行での期待効用を0に設定
            p[1,n,k] = 0.25;  // 1回目の試行での各デッキの選択確率を等確率に設定
        }
    }
    for(t in 2:T){
        for(n in 1:N){
            for(k in 1:K){
                if(y[t-1,n]==k)
                    Eu[t,n,k] = (1-a[n])*Eu[t-1,n,k] + a[n]*u[t-1,n];
                else
                    Eu[t,n,k] = Eu[t-1,n,k];
            }
        p[t,n] = softmax(theta[n]*Eu[t,n]);
        }
    }
}
model {
    Mu~normal(0,1);
    Sigma~inv_wishart(4,R);
    Eta~multi_normal(Mu,Sigma);

    for(n in 1:N){
        for(t in 1:T){
            y[t,n]~categorical(p[t,n]);
        }
    }
}
generated quantities{
    matrix[4,4] Rho;

    for(i in 1:4){
        for(j in 1:4){
            Rho[i,j] = Sigma[i,j]/sqrt(Sigma[i,i]*Sigma[j,j]);
        }
    }
}

MCMC分析を行うRコードは以下のとおりです。

# ライブラリ読み込み
library(rstan)

# データの読み込み
dat1<-read.csv("IGT_data/igtlog-A.csv",header=T)
dat2<-read.csv("IGT_data/igtlog-B.csv",header=T)
dat3<-read.csv("IGT_data/igtlog-C.csv",header=T)
dat4<-read.csv("IGT_data/igtlog-D.csv",header=T)
dat5<-read.csv("IGT_data/igtlog-E.csv",header=T)
dat6<-read.csv("IGT_data/igtlog-F.csv",header=T)
dat7<-read.csv("IGT_data/igtlog-G.csv",header=T)

# Stanコードでの変数を設定
T<-max(dat1$trialnum) # 試行数(150回)
K<-4  # デッキ数
N<-7  # 参加者数
y<-structure(c(dat1$deck,dat2$deck,dat3$deck,dat4$deck,dat5$deck,dat6$deck,dat7$deck),.Dim=c(T,N))
x<-structure(c(dat1$net,dat2$net,dat3$net,dat4$net,dat5$net,dat6$deck,dat7$deck),.Dim=c(T,N))
R<-diag(1,4)

dat<-list(T=T, K=K, N=N, y=y, x=x, R=R)

# モデルのコンパイル
stanmodel <- stan_model(file='PVL_delta_multi.stan')

# MCMCサンプリング
fit <- sampling(
    stanmodel,
    data=dat,
    seed=1234,
    chains=3, iter=10000, warmup=5000)

#結果の表示
print(fit)
参考文献
  • Ahn, W.-Y., Busemeyer, J. R., Wagenmakers, E.-J., & Stout, J. C. (2008, December). Comparison of decision learning models using the generalization criterion method. Cognitive science,32(8),1376–402.
  • Bechara, A, Damasio, A. R., Demasio, H., & Anderson, S. (1994). Insensitivity to future consequences following damage to human prefrontal cortex. Cognition, 57, 7-15.
  • Steingroever, H., Wetzels, R., & Wagenmakers, E. J. (2013). Validating the PVL-Delta model for the Iowa gambling task. Frontiers in psychology, 4.
  • Wetzels, R., Vandekerckhove, J., Tuerlinckx, F., & Wagenmakers, E. J. (2010). Bayesian parameter estimation in the Expectancy Valence model of the Iowa gambling task. Journal of Mathematical Psychology, 54(1), 14-27.

AzureのデータサイエンスVMを用いたニューラルネットワーク機械翻訳

$
0
0

初めまして。
システムソリューション部の長田と申します。

昨今では「AI」や「Deep Learning」などのキーワードとともに、データ分析が注目されています。 データ分析を行うためのツールは数多く存在し、それらをインストールするには意外と手間がかかります。 特にDeep LearningのアルゴリズムをGPUで計算する際に使われるcuda周りのインストールはノウハウがないと面倒です。

本記事では、データ分析で用いる主要なツールがプレインストールされているAzureのデータサイエンス仮想マシン(DSVM)を使ってDeep Learningを行ってみた感想について書いていきます。

アウトライン

1. データサイエンス仮想マシンによる環境構築
2. 検証に使ったDeep Learningモデルの説明
3. 検証結果
4. 感想
5. 参考文献
6. 付録(グローバル注意機構の詳細説明)

1. データサイエンス仮想マシンによる環境構築

データサイエンス仮想マシンとは、その名の通り、データサイエンス専用にカスタマイズされたMicrosoftのAzure上にある仮想マシンのイメージです。

環境として用意されているOSは私が見つけた限りでは下記があるようです。

  • Windows Server 2012
  • Windows Server 2016
  • Ubuntu 16.04 LTS
  • CentOSベースのOpenLogic 7.2 (GPU未対応:2017年7月19日時点)

今回は個人的に使い慣れているUbuntuでDeep Learningを用いた検証を行いました。

下記のリンクへ移動して[GET IT NOW] (スクリーンショットの赤枠部分)をクリックします。

■Linux用データサイエンス仮想マシン(Ubuntu)へのリンク
https://azuremarketplace.microsoft.com/en-us/marketplace/apps/microsoft-ads.linux-data-science-vm-ubuntu

利用規約やプライバシーに関する声明に目を通し、[Continue]ボタンをクリックします。

しばらくすると、Azureへのログインが済んでいない場合はログイン、ログイン済みの場合はAzureのポータルに移動します。その後、[作成]をクリックします。

入力が求められた項目をそれぞれ入力していきます。

[VMディスクの種類]はGPUを利用する場合は、HDDを指定する必要がある点に注意してください。

[場所]はGPUを使う場合はNCシリーズやNVシリーズが使える場所を選択する必要がありますので、下記URLにて確認してご自分の目的に合わせて選択してください。

ちなみにDeep Learningに向いているのはNVIDIA Tesla K80が搭載されているNCシリーズです。
https://azure.microsoft.com/ja-jp/regions/services/

また、下記のURLでは2017年の後半にはPascalアーキテクチャのGPUを搭載したNDシリーズやNCv2シリーズがリリースされる予定であるとの記載があります。
https://azure.microsoft.com/en-us/blog/more-gpus-more-power-more-intelligence/
これらのシリーズは計算速度も現行シリーズの2倍以上の性能を発揮できるようですので、リリース後はこちらを利用するのも良いかもしれません。

全ての項目を入力したら[OK]をクリックします。

次にサイズの選択ですが、今回はNC6で検証をしていきますので、[サポートされるディスクの種類]が[HDD]になっていることを確認し、[すべて表示]をクリックします。

そして、NC6を選択して[OK]をクリックします。

あとは通常のAzureと同様にご自分の環境に合わせて項目を選択して、[OK]で進めていけば構築はできるかと思います。

利用する範囲に絞って、今回プレインストールされている環境について説明します。

anacondaがインストールされており、python2.7のrootとpython3系のpy35というconda環境が存在しています。今回はPython3.5を使うので、py35のconda環境を使います。

py35のconda環境に切り替えるには下記のコマンドを実行します。

$ source /anaconda/bin/activate py35

2017年7月19日時点では、Chainerがプレインストールされていないため、pipでインストールする必要があります。下記のMicrosoft社のニュースリリースでは2017年夏にはChainerが搭載される予定とのことなので、もうしばらくすれば搭載されるようです。
https://news.microsoft.com/ja-jp/2017/05/23/170523-azure-preferred-networks/#sm.0000d2wbquhunfisvp52mbsj8nhpr%23QTDP1sPdCiRTyksQ.97

Chainerのインストール

$ sudo /anaconda/envs/py35/bin/pip install chainer
$ sudo /anaconda/envs/py35/bin/pip install cupy

また、今回はMeCabを使って日本語の分かち書きを行うため、インストールも行いました。

MeCabのインストール

$ sudo apt-get install mecab mecab-ipadic-utf8
$ sudo apt-get install libmecab-dev
$ sudo /anaconda/envs/py35/bin/pip install mecab-python3

2. 検証に使ったDeep Learningモデルの説明

今回は動作までにかかる時間や使い心地を知りたかったため、少量の日本語と英語の対訳データ(1200文章)を使って、Encoder-Decoder翻訳モデルを用いた機械翻訳に挑戦してみました。

使用するアルゴリズムはBidirectional Long Short-Term Memory(Bidirectional LSTM)とGlobal Attention(グローバル注意機構)を用いたSequence to Sequence(Seq2Seq)です。

Bidirectional LSTMについて説明する前に、まずはRecurrent Neural Network(RNN)を簡単に説明いたします。

「単語を入力して、次の単語を予測する」というタスクを考えた時に、基本的な順伝播型ニューラルネットワークによる予測では、下図のように入力として単語のみを与えて学習しています。

この構造上、過去の文脈情報を予測に反映出来ないという課題がありました。

RNNは下記のように単語に加え、前の単語の予測に使った隠れ層のベクトルを入力として使う事でその課題を解決したものです。

今回は一般的によく用いられるLSTMと呼ばれるRNNの拡張モデルを使います。LSTM自体も色々な拡張をされていますが、検証に使ったのはメモリと入力ゲート、出力ゲート、忘却ゲートの3つのゲートから成るものです。 様々な方がLSTMについての解説は書いていますので、本記事では詳細については踏み込みません。下記がとても参考になりました。

Understanding LSTM Networks

【前編】深層学習による自然言語処理 – ニューラル機械翻訳への理論 –

Bidirectional LSTMとは、下記のように単語に加え、Forward LSTM(順方向LSTM)とBackward LSTM(逆方向LSTM)の隠れ層のベクトルを連結して入力として使うモデルの事です。文脈の過去の情報だけでなく、未来の情報も用いることができます。

Seq2Seqとは、Encoder-Decoderモデルの一種です。下図のように入力側のEncoderと出力側のDecoderの2つのRNNを用意して、中間層で繋いだネットワークの構造をしています。

Seq2Seqでは、英語の予測結果と学習データのギャップを損失と捉え、この損失を最小化していくように学習を進めていくという仕組みになっています。

ただ、Seq2Seqには翻訳に適応した際、入出力長が長くなるにつれて翻訳精度が下がっていくという問題がしばしば起こるようです。この問題を克服するために、注意機構を用いることがあります。

人間が文章を翻訳する時はおそらく翻訳時に関係しそうな重要な単語に注目しながら翻訳を行なっているかと思います。厳密に言えば違うと思いますが、これと似た機能を導入するための機構が注意機構です。

注意機構を使うことで、入力と出力の単語の対応付け(単語アライメント)の情報の翻訳への反映や可視化が可能になります。

このうち、グローバル注意機構はEncoder側の各隠れ層のベクトルの加重平均を計算することにより、文脈ベクトルを算出する機構です。下記の論文により提案されました。
NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE

今回利用したグローバル注意機構では、下記の図のように双方向LSTMを用いる場合、順方向の文脈ベクトルと逆順方向の文脈ベクトルをそれぞれ求める仕組みになっています。

仕組みの詳細については「6. 付録(グローバル注意機構の詳細説明)」に記しましたので、ここでは省略します。

このグローバル注意機構により算出した文脈ベクトルを用いて、Decoderに予測させることにより、長文の翻訳精度の改善が期待できます。

注意機構にはグローバル注意機構以外にもローカル注意機構など様々な機構が提案されておりますが、今回はグローバル注意機構の一部の提案のみを取り上げて紹介いたしました。

3. 検証結果

では、翻訳を試してみましょう。

今回はCPUとGPUでの計算速度の違いを見るためにそれぞれの実行時間を見てみました。本実験では最適化のアルゴリズムはAdam、パラメータはデフォルト値を使っています。

実際に翻訳してみるのは次の5つの文章についてです。

■翻訳するデータ一覧

  • 今何を話しているの?
  • お会いできて嬉しいです
  • ご機嫌いかがでしょうか?
  • 大丈夫、彼はおとなしいよ
  • こんにちは

これらの文章は「こんにちは」と「お会いできて嬉しいです」と「ご機嫌いかがでしょうか?」は同じデータが学習データに含まれていますが、「今何を話しているの?」と「大丈夫、彼はおとなしいよ」については学習データに含まれていません。

今回は下記4つの設定でそれぞれ翻訳精度と実行時間を検証してみました。

実験番号 端末 CPU/GPU バッチサイズ 単語の埋め込みユニット数 隠れ層のユニット数 Epoch数
1 Mac book Air CPU 50 200 200 150
2 Mac book Air CPU 50 1000 1000 150
3 データサイエンス仮想マシン NC6 GPU 50 200 200 150
4 データサイエンス仮想マシン NC6 GPU 50 1000 1000 150

■ 実験番号1の結果
今何を話しているの? => what are you going to has about here ?
お会いできて嬉しいです => much is the cake .
ご機嫌いかがでしょうか? => would you he ?
大丈夫、彼はおとなしいよ => i found up got her .
こんにちは => hello.
実行時間: 45分38秒

翻訳は「こんにちは」以外は正解できておらず、全体的にあまり良くない精度になっているように見受けられます。

■ 実験番号2の結果
今何を話しているの? => what are you cooking ?
お会いできて嬉しいです => it’s a pleasure to meet you .
ご機嫌いかがでしょうか? => how are you ? ! ! !
大丈夫、彼はおとなしいよ => wherever he would you please check this matter of fact
こんにちは => hello.
実行時間: 7時間36分49秒

「お会いできて嬉しいです」と「こんにちは」は上手く翻訳できています。また、「ご機嫌いかがでしょうか」については何やら惜しいものが出ました。

■ 実験番号3の結果
今何を話しているの? => what are you talking about ?
お会いできて嬉しいです => it’s a pleasure to meet you .
ご機嫌いかがでしょうか? => how heavy i this ?
大丈夫、彼はおとなしいよ => hi .
こんにちは => hello .
実行時間:40分21秒

「ご機嫌いかがでしょうか」と「大丈夫、彼はおとなしいよ」以外については良い翻訳をしているように見受けられます。

■ 実験番号4の結果
今何を話しているの? => what are you talking about ?
お会いできて嬉しいです => it’s a pleasure to meet you .
ご機嫌いかがでしょうか? => how are you ?
大丈夫、彼はおとなしいよ => as is often the case with her, she broke
こんにちは => hello . she is a taxi .
実行時間:53分08秒

「大丈夫、彼はおとなしいよ」や「こんにちは」以外については良い翻訳をしてくれています。

実行時間についてまとめると、下記のようになりました。

この結果を見たところ、どうやらユニット数が多くないとあまりGPUの恩恵を受けられないようです。

Seq2Seqでは学習データにない未知語を含む文章については上手く予測できませんでした。こちらについてはデータを増やすか、未知語が出現した時に何かしらの対策をとるのが一般的のようです。

他にも学習データに存在している文章についても、epochが足りないのか、上手く翻訳できないこともありました。epochが150程度だと学習も途中で打ち切られてしまっているようで、もっとepochを増やすべきであったと反省しています。

4. 感想

最後にAzureのデータサイエンス仮想マシン(DSVM)を使ってみた感想です。

筆者は、Azureを使ったDeep Learningの高速化を行うのは初めてでしたが、このデータサイエンス仮想マシンを使うと、簡単に環境構築とGPUを利用することができました。

このように環境構築という面倒な部分が楽になっていけば、その分の時間が分析に当てられるので、分析環境や計算資源が必要な方は一度使ってみてはいかがでしょうか。

5. 参考文献

今回の検証では、下記のURLや書籍を参考にしました。

■ Azure関連

Introducing the new Data Science Virtual Machine on Windows Server 2016

Linux および Windows 用のクラウド ベースのデータ サイエンス仮想マシンの概要

Linux データ サイエンス仮想マシンのプロビジョニング

■ 技術関連

深層学習による自然言語処理(機械学習プロフェッショナルシリーズ)

ChainerとRNNと機械翻訳

今更ながらchainerでSeq2Seq(2)〜Attention Model編〜

わかるLSTM ~ 最近の動向と共に

Understanding LSTM Networks

【前編】深層学習による自然言語処理 – ニューラル機械翻訳への理論

Effective Approaches to Attention-based Neural Machine Translation

Attention and Memory in Deep Learning and NLP

最近のDeep Learning (NLP) 界隈におけるAttention事情

6. 付録(グローバル注意機構の詳細説明)

それぞれの文脈ベクトルの具体的な計算は、下図の流れで行っています。

この図では、Encoderの1単語目以外の計算は同様の計算を行うため、省略しています。
: 順方向Encoderの隠れ層のベクトルを同じサイズのベクトルを出力する線形結合層
: 逆方向Encoderの隠れ層のベクトルを同じサイズのベクトルを出力する線形結合層
: Decoderの隠れ層のベクトルと同じサイズのベクトルを出力する線形結合層
: 隠れ層のサイズからサイズ1のスカラーを出力する線形結合層
: 入力されたベクトルの足し算
: 入力されたベクトルのかけ算

矢印の色は青が文脈ベクトルの算出に直接関係がないデータのやり取り、赤がsoftmaxを通すまでの経路と逆順方向の文脈ベクトルを求める経路、黒が順方向の文脈ベクトルを求める経路、緑枠は計算の順番を示しています。

計算の流れをまとめますと、下記の通りとなります。(図の緑枠の①から⑥と対応づけしています)

① 順方向のEncoderの隠れ層、逆順方向のEncoderの隠れ層、予測元のDecoderの隠れ層のベクトルをそれぞれ線形結合したものの総和を求める
② ①の計算結果をtanh関数に入力して、-1から1の間のベクトルを得る
③ ②の計算結果を線形結合して、スカラー値に変換する
④ ③の計算結果とSoftmax関数に入力して、正規化を行う。
⑤ ④の計算結果と順方向のEncoderの隠れ層ベクトル、④の計算結果と逆順方向のEncoderの隠れ層ベクトルをそれぞれのペア同士掛けたものをそれぞれ求める。
⑥ 順方向のEncoderの隠れ層ベクトルをかけた方の⑤の計算結果の総和を求めると、順方向の文脈ベクトルが算出される。同様に逆順方向のEncoderの隠れ層ベクトルをかけた方の⑤の計算結果の総和を求めることで、逆順方向の文脈ベクトルを求める。

Azure Batch AI Trainingを利用したツイートデータ分類モデルの分散学習

$
0
0

はじめまして、システムソリューション部の松本です。

今回は、ディープラーニングの分散学習を可能にするサービス「Azure Batch AI Training」を用いて、Twitterの投稿に対する分類モデルを分散学習させてみようと思います。

アウトライン

  1.  Azure Batch AI Trainingとは
  2.  Azure Batch AI Training APIを使用してディープラーニング分散処理を行う流れ(Python&Tensorflowを使用した例)
  3. Tensorflowによるリツイート数でのツイートCNN2値分類
  4. 結果
  5. まとめ
  6. Appendix:独自Dockerコンテナssh設定

1. Azure Batch AI Trainingとは

クラウドの複数のGPUVM上でディープラーニング分散処理を行えるサービスです。

VMの設定・起動からジョブの実行までブラウザ上でUI操作して行うのではなく、
全て提供されているAPIを用いてプログラムベースで進めるものとなっています。

APIはPython、Java、C#向けに提供されており、ディープラーニングのフレームワークはTensorflow、CNTK、Caffe、Chainerがサポートされています。
Azure-CLIを用いて利用することもできます。

VM数、NFS等のノード設定や、VM上で動かす環境構築済みコンテナ等を指定後、分散処理を行うスクリプトとデータをVM側へ渡してジョブを実行すれば、後はクラウドVM上で分散処理が行われ、アウトプットがAzureファイル共有等に出力されるといった仕組みです。

(Azure AI Batch : https://batchaitraining.azure.com/)

2. Azure Batch AI Training APIを使用してディープラーニング分散処理を行う流れ (Python&Tensorflowを使用した例)

マイクロソフト様が作成された、NFSとAzureファイル共有をマウントした2つの
GPUVM上で分散処理を行うAzure Batch AI Private Previewサンプルプログラムを引用させていただき流れの説明を行います。
※現在Private Preview中で、今後Public PreviewのタイミングでいくつかのAPIが変更にるため、サンプルコードも修正が必要となります。
https://github.com/azure/Azbait-privatePreview

大まかな流れとしては、以下のようになります。

  1. Azureサブスクリプション準備、ストレージアカウントとファイル共有を作成
    == 以下Azure Batch AI Training APIを使用 ==
  2. 認証
  3. NFS作成&作成したNFSとAzureファイル共有をマウントしたGPUVMクラスタ作成
  4. NFSへ分散処理Tensorflowスクリプトと各種データをコピー
  5. ジョブを作成&実行
  6. Azureファイル共有に保存されたアウトプットファイルをローカルへコピー
  7. ジョブとクラスタの削除

2.1 Azureサブスクリプションの準備、ストレージアカウントとファイル共有作成

VM上で処理されたアウトプットファイルを保存するため、Azureストレージアカウントとファイル共有を作成します。

ストレージアカウントを作成するにはAzureサブスクリプションが必要となります。
Azureサブスクリプション登録は、Azureのサイトから行えます。

登録したアカウントでポータルサイトにログインすると、以下のような画面が開きます。

https://portal.azure.com

左のメニューにある「ストレージアカウント」を選択します。

左上の「+追加」を選択します。

各種設定を上記のように入力した後、下部の「作成」を選択します。
名前とリソースグループ名は任意です。
ここで指定した名前とリソースグループ名は後程APIを使用する際に使用します。
「※1:リソースグループ 、※2: 名前」

これでストレージアカウントは作成されました。

左のメニューからストレージアカウントのアイコンを選択後、先ほど作成した
ストレージアカウントを選択します。

上記キーは、後程APIを使用してストレージアカウントにアクセスする際に使用します。※3

次にファイル共有を作成します。

左のメニューからストレージアカウントアイコンを選択し、先ほど作成した
ストレージアカウントを選択後、画面真ん中のファイルを選択します。

左上の「ファイル共有」を選択し、任意の名前を入力後「OK」を押します。

作成したファイル共有の右側にあるメニューからプロパティを選択します。

右側にあるURLは後でAPIから使用します。※4

2.2 認証

進める前に、必要な各種コンフィグ値を準備しておきます。

subscription_id = [サブスクリプションID]
url = "https://eastus.batchaitraining-test.azure.com"
api_version =  "2017-05-01"
resource_group_name = [リソースグループ名※1]
authentication_thumbprint = [サブスクリプションアクセスキー]
region = "eastus"

storage_account_name = [上記ストレージアカウント名※2]
storage_account_key = [上記ストレージアカウントキー※3]
storage_file_share_url = [上記ファイル共有のURL※4]

※urlとapi_versionはPreview版用に用意されたものを使用していますが、正式版では適宜修正が必要かもしれません。

認証はbatchaitrainingclient.BatchAITrainingClientで行います。

import os
import subprocess
import time
from datetime import datetime
import requests

import batchaitrainingclient as training
import batchaitrainingclient.models as trainingModels

def GetBatchTrainingClient():
client = training.BatchAITrainingClient(
subscription_id = subscription_id,
api_version = api_version,
base_url= url
)
client._client.add_header("x-ms-auth-cert", authentication_thumbprint)
client.config.generate_client_request_id = True
return client

client = GetBatchTrainingClient()

2.3 NFS作成&作成したNFSをマウントしたGPUVMクラスタ作成

リソースグループ名、クラスタ名、GPUVM数、ネットワークファイルシステム名を
指定して、NFSとAzureファイル共有をマウントしたクラスタを作成します。

今回は2つのNC6 GPUVMを用いるため、第3引数を2としています。

try:
    clusterName = "clusterwithnfs"
    nfsName = "demonfs"
    clusterId = CreateClusterWithNFS(resource_group_name , clusterName, 2, nfsName)
except Exception as e:
    print(e)
    raise

少し長いですが、CreateClusterWithNFSの実装は以下に記載しています。

ClusterCreateParametersでクラスタの各種設定を作成しています。
vm_sizeはVMのサイズ、target_number_of_vmsは使用するVM数、
node_setupはノード設定です。mount_volumesで、VMにマウントする
NFSとAzureファイル共有を指定しています。

NFSはCreateSingleNodeNFSで作成しています。

クラスタ作成後、WaitForClusterStateでクラスタの状態がsteadyになるのを待っています。

# NFSとAzureファイル共有をマウントしたクラスタ作成
def CreateClusterWithNFS(resourcegroupName, clustername, numberofGPUVMs, nfsName):
    try:
        print("ClusterName: {0}".format(clustername))
        client = GetBatchTrainingClient()
        nfsId = GetNFSId(resourcegroupName, nfsName)
        if(nfsId == None):
            nfsId = CreateSingleNodeNFS(resourcegroupName, nfsName)

        cluster = client.cluster.create(
            resource_group_name = resourcegroupName,
            cluster_name = clustername,
            # クラスタの設定パラメーター
            parameters = trainingModels.ClusterCreateParameters(
                location = region,
                vm_size = "STANDARD_NC6",
                user_account_settings = trainingModels.UserAccountSettings(
                    group_id = 1,
                    admin_user_name = 'your_user_name',
                    admin_user_ssh_public_key = get_ssh_key(),
                    admin_user_password = 'your_password'),
                target_number_of_vms = numberofGPUVMs,
                node_setup = trainingModels.NodeSetup(
                    mount_volumes = trainingModels.MountVolumes(
                        # VMにマウントするAzureファイル共有
                        azure_file_share_references = [
                            trainingModels.AzureFileShareReference(
                                account_name = storage_account_name,
                                azure_file_url= storage_file_share_url,
                                relative_mount_path = "myazfileshare",
                                credentials_info = trainingModels.AzureStorageCredentialsInfo(
                                    account_key = storage_account_key))],
                        # VMにマウントするNFS
                        file_server_references = [
                            trainingModels.FileServerReference(
                                file_server = trainingModels.ResourceId(nfsId),
                                relative_mount_path = "mynfs",
                                mount_options = "rw"
                                )]
                    )
                )
            )
        )

        # クラスタ状態がsteadyになるのを待つ
        cluster = WaitForClusterState(resourcegroupName, clustername, trainingModels.AllocationState.steady)
        return cluster.id

    except BaseException as e:
        print(e)
        raise

CreateSingleNodeNFS、WaitForClusterStateの実装は以下です。

def GetCluster(resourceGroupName, clusterName):
    """Get the Cluster
    """
    client = GetBatchTrainingClient()
    cluster = client.cluster.get(resourceGroupName, clusterName)
    return cluster

def PrintClusterStatus(resourceGroupName, clusterName):
    cluster = GetCluster(resourceGroupName, clusterName)
    print(
        "Cluster state = {0}. Target= {1} Allocated = {2}, NumIdle = {3}, NumUnusable = {4}, NumRunning = {5}, NumPreparing = {6}".format(
            cluster.allocation_state,
            cluster.target_number_of_vms,
            cluster.current_number_of_vms,
            cluster.node_state_counts.idle_node_count,
            cluster.node_state_counts.unusable_node_count,
            cluster.node_state_counts.running_node_count,
            cluster.node_state_counts.preparing_node_count))
    if (cluster.errors != None):
        for error in cluster.errors:
            print("Cluster error = {0}, ErrorMessage = {1}".format(error.code, error.message))
            print("Details:")
            errorDetails = error.details
            for errorDetail in errorDetails:
                print("{0}:{1}".format(errorDetail.name, errorDetail.value))
    return cluster

def WaitForClusterState(resourceGroupName, clusterName, targetState):
    while True:
        cluster = PrintClusterStatus(resourceGroupName, clusterName)
        if cluster.allocation_state == targetState:
            return cluster
            break
        else:
            time.sleep(5)

def get_ssh_key():
    """Reads default ssh public key."""
    if os.name == 'nt':
        return None
    try:
        key = subprocess.check_output(['ssh-keygen -y -f ~/.ssh/id_rsa'],
                                      shell=True)
        return key.decode().split()[1]
    except:
        raise EnvironmentError('Please generate default ssh key using '
                               'ssh-keygen')

# NFSのIDを取得
def GetNFSId(resourceGroupName, fsName):
    try:
        client = GetBatchTrainingClient()
        nfs = client.file_server.get(resourceGroupName, fsName)
        print(nfs.id)
        return nfs.id
    except Exception as e:
        print (e)
        return None

# シングルノードのNFS作成
def CreateSingleNodeNFS(resourceGroupName, fsName):
    try:
        print("SingleNodeNFSName: {0}".format(fsName))
        client = GetBatchTrainingClient()
        client.file_server.create(
            resource_group_name = resourceGroupName,
            file_server_name = fsName,
            parameters = trainingModels.FileServerCreateParameters(
                location = region,
                new_file_server = trainingModels.NewFileServer(
                    vm_size = 'Standard_D1_V2',
                    ssh_configuration = trainingModels.SshConfiguration(
                        user_account_settings = trainingModels.UserAccountSettings(
                            group_id = 1,
                            admin_user_name = 'your_user_name',
                            admin_user_ssh_public_key=get_ssh_key(),
                            admin_user_password = 'your_password')),
                    data_disks = trainingModels.DataDisks(
                        disk_size_in_gb = '10',
                        number_of_disks = '2',
                        storage_account_type = 'Standard_LRS'))))


        while (True):
            fs = client.file_server.get(resource_group_name = resourceGroupName, file_server_name = fsName)
            if (fs.provisioning_state == trainingModels.FileServerProvisioningState.succeeded):
                print ("file server created")
                return fs.id
            print ("FileServer provisioning state = {0}".format(fs.provisioning_state.name))
            time.sleep(5)
    except Exception as e:
        print(e)
        raise

2.4 NFSへTensorflowスクリプトと各種データをコピー

VMにマウントされたNFSへ、分散処理を行うTensorflowスクリプトと
各種データファイルをコピーします。

def GetFileServerSSHEndPoint(resourceGroupName, fsName):
    fsr = trainingModels.MountSettings()
    client = GetBatchTrainingClient()
    fs = client.file_server.get(resourceGroupName, fsName)
    return fs.new_file_server.mount_settings.file_server_public_ip

sshEndPoint = GetFileServerSSHEndPoint(resource_group_name, nfsName)
print(sshEndPoint)

上記を実行するとNFSのIPアドレスを取得できますので、今回はscpで
ローカルからNFSへコピーすることにします。

・スクリプト

$ scp -P 22 distributed_cnn_twitter_classifier.py your_user_name@[NFS_IP]:/mnt/data/TWITTER/

・ツイートデータ

scp -P 22 tweet_data/* your_user_name@[NFS_IP]:/mnt/data/TWITTER/tweet_data/

・Word2Vec学習済みデータ

$ scp -P 22 word2vec.gensim.model your_user_name@[NFS_IP]:/mnt/data/TWITTER/
$ scp -P 22 word2vec.gensim.model.syn1neg.npy your_user_name@[NFS_IP]:/mnt/data/TWITTER/
$ scp -P 22 word2vec.gensim.model.wv.syn0.npy your_user_name@[NFS_IP]:/mnt/data/TWITTER/

2.5 ジョブを作成&実行

JobCreateParametersでジョブの設定を決めます。

clusterには先ほど作成したクラスタを指定します。

number_of_vmsにはVM数。今回は2つ使用するため2を指定しています。

tool_typeにはcntk、tensorflow、caffe、chaienr、customのいずれかを指定します。

std_out_err_path_prefixでは、VMの標準出力・エラーログファイルの出力先を設定します。
出力先としてAzureファイル共有を指定しています。

container_settingsで、VM上で動かすコンテナを指定できます。
ここでは、tensorflow+自然言語処理環境構築済みの独自Dockerコンテナを指定しています。
tensorflowの環境構築済みのdockerコンテナbatchaitraining/tensorflow:1.1.0-gpuをベースにカスタマイズしたものです。
※独自Dockerコンテナを用いる場合は、sshの設定を行わないと正常に分散処理が行われないようです。(詳細はAppendixに記載しています。)

output_directoriesでは、アウトプットファイル出力先を設定します。
モデルファイルの出力先をAzureファイル共有に指定しています。

tensor_flow_settingsでは、先ほどNFSにコピーしたTensorflowスクリプトのパス、
スクリプトに渡すコマンドライン引数、パラメータサーバー数、ワーカー数を指定しています。

client.job.createで、作成したパラメータを渡してジョブを作成しています。

jobName = "nfs_"+ datetime.utcnow().strftime("%d_%H-%M-%S-%Y")
print("JobName: {0}".format(jobName))
try:
    client = GetBatchTrainingClient()
    StdOutErrPathPrefix = "$AZ_LEARNING_MOUNT_ROOT/myazfileshare"

    jobCreateParams = trainingModels.job_create_parameters.JobCreateParameters(
        location= region,
        cluster = trainingModels.ResourceId(clusterId),
        number_of_vms = 2,
        tool_type = trainingModels.ToolType.tensorflow,
        std_out_err_path_prefix = StdOutErrPathPrefix,

        container_settings =  trainingModels.ContainerSettings(
                                image_source_registry = trainingModels.ImageSourceRegistry(
                                    image_name = "albeym/batchai_tensorflow_nlp:ssh")), # Dockerコンテナ指定

        output_directories = [trainingModels.OutputDirectory (
            id = "modelOutput", path_prefix="$AZ_LEARNING_MOUNT_ROOT/myazfileshare", path_suffix="Models", type=trainingModels.OutputType.custom)],

        tensor_flow_settings = trainingModels.TensorFlowSettings(
            master_script_settings = trainingModels.ScriptSettings(
                script_file_path = "$AZ_LEARNING_MOUNT_ROOT/mynfs/TWITTER/distributed_cnn_twitter_classifier.py", # スクリプトパス
                command_line_args = "--ps_hosts=$AZ_LEARNING_PS_HOSTS --worker_hosts=$AZ_LEARNING_WORKER_HOSTS --job_name=worker --task_index=$AZ_LEARNING_TASK_INDEX --data_dir=$AZ_LEARNING_MOUNT_ROOT/mynfs/TWITTER/ --output_dir=$AZ_LEARNING_OUTPUT_modelOutput --num_gpus=2"), # スクリプトに渡すコマンドライン引数
            worker_script_settings = trainingModels.ScriptSettings(
                script_file_path = "$AZ_LEARNING_MOUNT_ROOT/mynfs/TWITTER/distributed_cnn_twitter_classifier.py", # スクリプトパス
                command_line_args = "--ps_hosts=$AZ_LEARNING_PS_HOSTS --worker_hosts=$AZ_LEARNING_WORKER_HOSTS --job_name=worker --task_index=$AZ_LEARNING_TASK_INDEX --data_dir=$AZ_LEARNING_MOUNT_ROOT/mynfs/TWITTER/ --output_dir=$AZ_LEARNING_OUTPUT_modelOutput --num_gpus=2"), # スクリプトに渡すコマンドライン引数
            parameter_server_script_settings = trainingModels.ScriptSettings(
                script_file_path = "$AZ_LEARNING_MOUNT_ROOT/mynfs/TWITTER/distributed_cnn_twitter_classifier.py", # スクリプトパス
                command_line_args = "--ps_hosts=$AZ_LEARNING_PS_HOSTS --worker_hosts=$AZ_LEARNING_WORKER_HOSTS --job_name=ps --task_index=$AZ_LEARNING_TASK_INDEX --data_dir=$AZ_LEARNING_MOUNT_ROOT/mynfs/TWITTER/ --output_dir=$AZ_LEARNING_OUTPUT_modelOutput --num_gpus=0"), # スクリプトに渡すコマンドライン引数
            number_of_parameter_servers = 1,
            number_of_workers = 2
        ))

    job = client.job.create(resource_group_name = resource_group_name, job_name = jobName, parameters = jobCreateParams)

    print ("created job - {0}".format(job.id))
except Exception as e:
    print(e)

WaitForJobStateでジョブ状態を監視しrunningになるのを待ちます。

running状態になった後は、whileループに入りcompletedになるまで待ちます。

# Wait for job to start running
try:
     WaitForJobState(resource_group_name, jobName, clusterName, trainingModels.ExecutionState.running)
except Exception as e:
    print(e)

print("Waiting for job output to become available...")

# Wait for job to complete and tail the stderr.txt
streamer = OutputStreamer(client, resource_group_name, jobName, 'stdOuterr', 'stdout-0.txt')
while (True) :
    streamer.tail()
    submittedJob = client.job.get(resource_group_name, jobName)
    if(submittedJob.execution_state.name == 'completed'):
        print('Job {} complete'.format(submittedJob.execution_state.name))
        break
    time.sleep(1)

WaitForJobStateとOutputStreamerの実装は下記に記載しています。

def GetJob(resourceGroupName, jobName):
    client = GetBatchTrainingClient()
    job = client.job.get(resourceGroupName, jobName)
    return job

def PrintJobStatus(resourceGroupName, jobName):
    job = GetJob(resourceGroupName, jobName)

    failureMessage = "none"
    exitCode = "none"
    if (job.execution_state == trainingModels.ExecutionState.completed):
        exitCode = job.execution_info.exit_code
        if (job.execution_info.failure_info == None):
            failureMessage = "pass"
        else:
            failureMessage = "\nErrorCode:{0}\nErrorCategory:{1}\nErrorMessage:{2}\n".format(
                job.execution_info.failure_info.code, job.execution_info.failure_info.category,
                job.execution_info.failure_info.message)
            if (job.execution_info.failure_info.details != None):
                failureMessage += "Details:\n"
                for error in job.execution_info.failure_info.details:
                    failureMessage += "{0}:{1}\n".format(error.name, error.value)
    print("job State = {0}. ExitCode = {1} \nFailureDetails:{2}".format(job.execution_state.name, exitCode,
                                                                        failureMessage))
    return job


def PrintClusterStatus(resourceGroupName, clusterName):
    cluster = GetCluster(resourceGroupName, clusterName)
    print(
        "Cluster state = {0}. Target= {1} Allocated = {2}, NumIdle = {3}, NumUnusable = {4}, NumRunning = {5}, NumPreparing = {6}".format(
            cluster.allocation_state,
            cluster.target_number_of_vms,
            cluster.current_number_of_vms,
            cluster.node_state_counts.idle_node_count,
            cluster.node_state_counts.unusable_node_count,
            cluster.node_state_counts.running_node_count,
            cluster.node_state_counts.preparing_node_count))
    if (cluster.errors != None):
        for error in cluster.errors:
            print("Cluster error = {0}, ErrorMessage = {1}".format(error.code, error.message))
            print("Details:")
            errorDetails = error.details
            for errorDetail in errorDetails:
                print("{0}:{1}".format(errorDetail.name, errorDetail.value))
    return cluster


def WaitForJobState(resourceGroupName, jobName, clusterName, targetState):
    while True:
        job = PrintJobStatus(resourceGroupName, jobName)

        if job.execution_state in {targetState, trainingModels.ExecutionState.completed}:
            break
        elif job.execution_state == trainingModels.ExecutionState.queued:
            PrintClusterStatus(resourceGroupName, clusterName)
            time.sleep(5)
        else:
            time.sleep(5)


def WaitForClusterState(resourceGroupName, clusterName, targetState):
    while True:
        cluster = PrintClusterStatus(resourceGroupName, clusterName)
        if cluster.allocation_state == targetState:
            return cluster
            break
        else:
            time.sleep(5)

class OutputStreamer:
    def __init__(self, client, resource_group, job_name, output_directory_id, file_name):
        self.client = client
        self.resource_group = resource_group
        self.job_name = job_name
        self.output_directory_id = output_directory_id
        self.file_name = file_name
        self.url = None
        self.downloaded = 0

    def tail(self):
        if not self.url:
            files = self.client.file.list(
                self.resource_group, self.job_name,
                file_list_options=trainingModels.file_list_options.FileListOptions(
                    outputdirectoryid=self.output_directory_id))
            if not files:
                return
            else:
                for f in files.value:
                    if f.name == self.file_name:
                        self.url = f.download_url
        if self.url:
            r = requests.get(self.url, headers={'Range': 'bytes={0}-'.format(self.downloaded)})
            if int(r.status_code / 100) == 2:
                self.downloaded += len(r.content)
                print(r.content.decode(), end='')

2.6 Azureファイル共有に保存されたアウトプットファイルをローカルへダウンロード

完了すると、パラメーターサーバー、ワーカーからの標準出力・エラーログファイル、モデルファイルがマウントされたAzureファイル共有にアウトプットされているので、ブラウザ上から確認できます。

標準出力ログファイルをローカルへダウンロードして内容をみてみます。

■Worker 0
stdout-0.txt

…
1502346711.6833575 : 2017/08/10 06:31:51 : local_step/global_step : 1492/2980
1502346724.9924903 : 2017/08/10 06:32:04 : local_step/global_step : 1493/2982
1502346738.1236217 : 2017/08/10 06:32:18 : local_step/global_step : 1494/2984
1502346751.0443964 : 2017/08/10 06:32:31 : local_step/global_step : 1495/2986
1502346764.2894645 : 2017/08/10 06:32:44 : local_step/global_step : 1496/2988
1502346777.1980283 : 2017/08/10 06:32:57 : local_step/global_step : 1497/2990
1502346790.3024867 : 2017/08/10 06:33:10 : local_step/global_step : 1498/2992
1502346803.422791 : 2017/08/10 06:33:23 : local_step/global_step : 1499/2994
1502346816.6486468 : 2017/08/10 06:33:36 : local_step/global_step : 1500/2996
1502346816.6486702 : 2017/08/10 06:33:36 : local_step/global_step : 1500/2996 - train data accuracy : 0.8585116863250732
1502346817.8208303 : 2017/08/10 06:33:36 : local_step/global_step : 1500/2996 - test data accuracy 3295/4462 = 0.7384580905423577
1502346817.8799393 : 2017/08/10 06:33:36 : local_step/global_step : 1500/2996 - loss : 0.34474071860313416
1502346818.4383993 : 2017/08/10 06:33:36 : local_step/global_step : 1500/2996 - label 0 accuracy 1595/2225 = 0.7168539325842697
1502346819.060343 : 2017/08/10 06:33:36 : local_step/global_step : 1500/2996 - label 1 accuracy 1653/2237 = 0.7389360751005811
1502346832.3352547 : 2017/08/10 06:33:52 : local_step/global_step : 1501/2998
1502346845.4994643 : 2017/08/10 06:34:05 : local_step/global_step : 1502/3000
Training ends @ 1502346846.502469
Training elapsed time: 20172.574143 s

■Worker 1
stdout-1.txt

…
1502346749.297772 : 2017/08/10 06:32:29 : local_step/global_step : 1493/2986
1502346762.465857 : 2017/08/10 06:32:42 : local_step/global_step : 1494/2988
1502346775.6255963 : 2017/08/10 06:32:55 : local_step/global_step : 1495/2990
1502346788.9104798 : 2017/08/10 06:33:08 : local_step/global_step : 1496/2992
1502346802.1854687 : 2017/08/10 06:33:22 : local_step/global_step : 1497/2994
1502346815.1583965 : 2017/08/10 06:33:35 : local_step/global_step : 1498/2996
1502346828.2493777 : 2017/08/10 06:33:48 : local_step/global_step : 1499/2998
1502346841.2159996 : 2017/08/10 06:34:01 : local_step/global_step : 1500/2999
1502346841.2160242 : 2017/08/10 06:34:01 : local_step/global_step : 1500/2999 - train data accuracy : 0.8623220920562744
1502346842.3832383 : 2017/08/10 06:34:01 : local_step/global_step : 1500/2999 - test data accuracy 3300/4462 = 0.7395786642761094
1502346842.440593 : 2017/08/10 06:34:01 : local_step/global_step : 1500/2999 - loss : 0.34384649991989136
1502346843.0026593 : 2017/08/10 06:34:01 : local_step/global_step : 1500/2999 - label 0 accuracy 1594/2225 = 0.7164044943820225
1502346843.6091473 : 2017/08/10 06:34:01 : local_step/global_step : 1500/2999 - label 1 accuracy 1680/2237 = 0.7510058113544926
1502346856.9699488 : 2017/08/10 06:34:16 : local_step/global_step : 1501/3002
Training ends @ 1502346856.970022
Training elapsed time: 20182.624171 s

local_stepが各ワーカーでの処理回数、global_stepが全体での処理回数を表していますが、
Worker0とWorker1で分散処理されているのが確認できます。

2.7 ジョブとクラスタの削除

最後に、作成したジョブとクラスタを削除します。

def DeleteJob(resourceGroupName, jobName):
    """Delete the Job
    """
    client = GetBatchTrainingClient()
    cluster = client.job.delete(resourceGroupName, jobName)

def DeleteCluster(resourceGroupName, clusterName):
    """Delete the Cluster
    """
    client = GetBatchTrainingClient()
    cluster = client.cluster.delete(resourceGroupName, clusterName)

# Delete Job
DeleteJob(resource_group_name, jobName)

# Delete Cluster
DeleteCluster(resource_group_name, clusterName)

3. Tensorflowによるリツイート数でのツイートCNN2値分類

上記分散処理検証では、Twitterのツイートを、今後リツイートされるかどうかを予測・分類するモデルのスクリプトを使用しました。

スクリプトのソースコードは下記に記載しています。

ツイートデータは、TwitterAPIを用いて、キーワード「ドル円」で取得した2ヶ月間(6/3~8/3)のツイートデータから、結果リツイートされた(リツイート数1以上)ツイート及びリツイートされなかった(リツイート数0)ツイートをそれぞれ約1万件を抽出しました。
そこから内8割を学習データ、残り2割を検証データとして使用しています。

分類モデル部分は
https://arxiv.org/pdf/1408.5882.pdf
上記論文の実装
https://github.com/dennybritz/cnn-text-classification-tf
を参考にさせていただき、embeddingの部分を学習済みWord2Vecデータを
用いるように少しカスタマイズして使用させていただきました。

ツイートを形態素解析しトークン化後、各単語を学習済みWord2Vecの単語ベクトル
で置換しツイート単位で行列を作成、トークン数最大長まで0でパッディングして行列サイズをトークン数最大長×単語ベクトル次元に固定化後畳み込みニューラルネットワーク(CNN)へ流しています。
学習済みWord2Vecは以下を使用させていただきました。
http://aial.shiroyagi.co.jp/2017/02/japanese-word2vec-model-builder/
http://public.shiroyagi.s3.amazonaws.com/latest-ja-word2vec-gensim-model.zip

CNNでのツイート文処理は以下のようなイメージです。

引用:https://arxiv.org/pdf/1510.03820.pdf

Tensorflowでの分散処理周りは
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/dist_test/python/mnist_replica.py
を参考にしています。

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import math
import sys
import tempfile
import time
import os

import tensorflow as tf

import numpy as np
import pandas as pd
from gensim.models.word2vec import Word2Vec
from janome.tokenizer import Tokenizer
import re
from datetime import datetime

flags = tf.app.flags
flags.DEFINE_string(&quot;data_dir&quot;, &quot;/tmp/twitter-data&quot;,
                    &quot;Directory for storing mnist data&quot;)
flags.DEFINE_string(&quot;output_dir&quot;, &quot;/tmp/twitter-data-output&quot;,
                    &quot;Directory for storing output data&quot;)
flags.DEFINE_integer(&quot;task_index&quot;, None,
                     &quot;Worker task index, should be &amp;gt;= 0. task_index=0 is &quot;
                     &quot;the master worker task the performs the variable &quot;
                     &quot;initialization &quot;)
flags.DEFINE_integer(&quot;num_gpus&quot;, 1,
                     &quot;Total number of gpus for each machine.&quot;
                     &quot;If you don't use GPU, please set it to '0'&quot;)
flags.DEFINE_integer(&quot;replicas_to_aggregate&quot;, None,
                     &quot;Number of replicas to aggregate before parameter update&quot;
                     &quot;is applied (For sync_replicas mode only; default: &quot;
                     &quot;num_workers)&quot;)
flags.DEFINE_integer(&quot;train_steps&quot;, 3000,
                     &quot;Number of (global) training steps to perform&quot;)
flags.DEFINE_integer(&quot;batch_size&quot;, 100, &quot;Training batch size&quot;)
flags.DEFINE_float(&quot;learning_rate&quot;, 0.0001, &quot;Learning rate&quot;)
flags.DEFINE_boolean(&quot;sync_replicas&quot;, False,
                     &quot;Use the sync_replicas (synchronized replicas) mode, &quot;
                     &quot;wherein the parameter updates from workers are aggregated &quot;
                     &quot;before applied to avoid stale gradients&quot;)
flags.DEFINE_boolean(
    &quot;existing_servers&quot;, False, &quot;Whether servers already exists. If True, &quot;
    &quot;will use the worker hosts via their GRPC URLs (one client process &quot;
    &quot;per worker host). Otherwise, will create an in-process TensorFlow &quot;
    &quot;server.&quot;)
flags.DEFINE_string(&quot;ps_hosts&quot;,&quot;localhost:2222&quot;,
                    &quot;Comma-separated list of hostname:port pairs&quot;)
flags.DEFINE_string(&quot;worker_hosts&quot;, &quot;localhost:2223,localhost:2224&quot;,
                    &quot;Comma-separated list of hostname:port pairs&quot;)
flags.DEFINE_string(&quot;job_name&quot;, None,&quot;job name: worker or ps&quot;)

FLAGS = flags.FLAGS

class TextCNN(object):
    &quot;&quot;&quot;
    CNN文章分類モデル
    &quot;&quot;&quot;
    def __init__(
      self, num_classes, vocab_size,
      embedding_size, filter_sizes, num_filters, l2_reg_lambda=0.0):

        self.x = tf.placeholder(tf.float32, [None, vocab_size, embedding_size], name=&quot;input_x&quot;)
        self.input_x = tf.reshape(self.x, [-1, vocab_size, embedding_size, 1])
        self.input_y = tf.placeholder(tf.float32, [None, num_classes], name=&quot;input_y&quot;)
        self.dropout_keep_prob = tf.placeholder(tf.float32, name=&quot;dropout_keep_prob&quot;)

        # Keeping track of l2 regularization loss (optional)
        l2_loss = tf.constant(0.0)

        # 各フィルターサイズに対して畳み込み層とMaxPooling層を作成
        pooled_outputs = []
        for i, filter_size in enumerate(filter_sizes):
            with tf.name_scope(&quot;conv-maxpool-%s&quot; % filter_size):
                # 畳み込み層
                filter_shape = [filter_size, embedding_size, 1, num_filters]
                W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name=&quot;W&quot;)
                b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name=&quot;b&quot;)
                conv = tf.nn.conv2d(
                    self.input_x,
                    W,
                    strides=[1, 1, 1, 1],
                    padding=&quot;VALID&quot;,
                    name=&quot;conv&quot;)
                # ReLU活性化関数適用
                h = tf.nn.relu(tf.nn.bias_add(conv, b), name=&quot;relu&quot;)
                # Maxpooling
                pooled = tf.nn.max_pool(
                    h,
                    ksize=[1, vocab_size - filter_size + 1, 1, 1],
                    strides=[1, 1, 1, 1],
                    padding='VALID',
                    name=&quot;pool&quot;)
                pooled_outputs.append(pooled)

        # MaxPoolした各アウトプットを結合して1次元配列に変換
        num_filters_total = num_filters * len(filter_sizes)
        self.h_pool = tf.concat(3, pooled_outputs)
        self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])

        # Dropout
        with tf.name_scope(&quot;dropout&quot;):
            self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)

        # スコアと予測値
        with tf.name_scope(&quot;output&quot;):
            W = tf.get_variable(
                &quot;W&quot;,
                shape=[num_filters_total, num_classes],
                initializer=tf.contrib.layers.xavier_initializer())
            b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name=&quot;b&quot;)
            l2_loss += tf.nn.l2_loss(W)
            l2_loss += tf.nn.l2_loss(b)
            self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name=&quot;scores&quot;)
            self.predictions = tf.argmax(self.scores, 1, name=&quot;predictions&quot;)

        # 交差エントロピー損失関数
        with tf.name_scope(&quot;loss&quot;):
            losses = tf.nn.softmax_cross_entropy_with_logits(logits=self.scores, labels=self.input_y)
            self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss

        # 精度
        with tf.name_scope(&quot;accuracy&quot;):
            correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))
            self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, &quot;float&quot;), name=&quot;accuracy&quot;)

class TweetProcessor:
    def filter_sentence(self, sentence):
        sentence = re.sub(r&quot;(?:https?|ftp)://[A-Za-z0-9.-/]*&quot;, &quot;&quot;, sentence) # URL削除
        return sentence

    # トークンリスト形式からWord2Vec行列形式へ変換
    def convert_to_sentence_matrix(self, tokens, w2v_model, word_vec_dim):
        sentence_matrix = []
        for token in tokens:
            if token in w2v_model:
                # 単語がWord2Vec学習済み辞書に存在するならば、その単語ベクトルで置き換え
                sentence_matrix.append(np.array(w2v_model[token]))
            else:
                # 学習済み辞書に存在しないならば、0ベクトルで置き換え
                sentence_matrix.append(np.zeros(word_vec_dim))
        return np.array(sentence_matrix)

    # max_length行まで0ベクトルで埋める
    def padding(self, sentence_matrix, max_length, word_vec_dim):
        while sentence_matrix.shape[0] &amp;lt; max_length: sentence_matrix = np.vstack((sentence_matrix, np.zeros(word_vec_dim))) return sentence_matrix # ツイートデータを読み込み加工 def prepare_data(self, file_path_list): # TSVファイルからツイートデータ読み込み print(&quot;reading tweet data..&quot;) tweet_dataframes = [pd.read_csv(FLAGS.data_dir + path, delimiter=&quot;\t&quot;) for path in file_path_list] tweet_dataframe = pd.concat(tweet_dataframes) # 重複ツイート削除 tweet_dataframe.drop_duplicates(['TweetID']) # リツイートしているツイート削除 tweet_dataframe = tweet_dataframe[[tt.startswith(&quot;RT @&quot;) == False for tt in tweet_dataframe['TweetText']]] # リツイート閾値より小(ラベル0)とリツイート閾値以上(ラベル1)のデータを1:1比率で抽出 label1_dataframe = tweet_dataframe[tweet_dataframe['RetweetCount'] &amp;gt;= RETWEET_COUNT_THRESHOLD]
        label0_dataframe = tweet_dataframe[tweet_dataframe['RetweetCount'] &amp;lt; RETWEET_COUNT_THRESHOLD][::int(len(tweet_dataframe[tweet_dataframe['RetweetCount'] &amp;lt; RETWEET_COUNT_THRESHOLD])/len(label1_dataframe))][:len(label1_dataframe)]
        tweet_dataframe = pd.concat([label0_dataframe, label1_dataframe])

        # [[ツイート , ラベル([0, 1] or [1, 0])]]のリスト作成
        tweet_label = [[self.filter_sentence(tt), [1, 0] if int(rc) &amp;lt; RETWEET_COUNT_THRESHOLD else [0, 1]] for tt, rc in zip(tweet_dataframe['TweetText'], tweet_dataframe['RetweetCount'])] # 各ツイートを形態素解析してトークン化 print(&quot;tokenizing tweets...&quot;) t = Tokenizer() for tl in tweet_label: tokens = t.tokenize(tl[0]) tl[0] = [token.surface for token in tokens] tweet_label = np.array(tweet_label) # トークン数がMIN_TOKEN_LENGTH以上かつMAX_TOKEN_LENGTH以下のツイートのみ使用 tweet_label = np.array(tweet_label[[len(tokens) &amp;gt;= MIN_TOKEN_LENGTH and len(tokens) &amp;lt;= MAX_TOKEN_LENGTH for tokens in tweet_label[:, 0]]]) # 学習済みWord2Vecモデルデータ読み込み print(&quot;reading pre-learned word2vec model..&quot;) model_path = FLAGS.data_dir + &quot;word2vec.gensim.model&quot; model = Word2Vec.load(model_path) # トークンリスト形式からWord2Vec行列形式へ変換 print(&quot;converting from token list format to verd2vec sentence matrix format..&quot;) for tl in tweet_label: tl[0] = self.convert_to_sentence_matrix(tl[0], model, WORD_VEC_DIM) # MAX_TOKEN_LENGTH行まで0ベクトルで埋める print(&quot;padding..&quot;) for tl in tweet_label: tl[0] = self.padding(tl[0], MAX_TOKEN_LENGTH, WORD_VEC_DIM) # ラベル0とラベル1のデータに分割 tweet_label0 = tweet_label[[tl[1][0] == 1 for tl in tweet_label]] tweet_label1 = tweet_label[[tl[1][1] == 1 for tl in tweet_label]] # ラベル0・ラベル1各データの前8割を学習データとして使用 train_data = np.array([tl for tl in tweet_label0[:int(tweet_label0.shape[0]*0.8), 0]] + [tl for tl in tweet_label1[:int(tweet_label1.shape[0]*0.8), 0]]) train_label = np.array([tl for tl in tweet_label0[:int(tweet_label0.shape[0]*0.8), 1]] + [tl for tl in tweet_label1[:int(tweet_label1.shape[0]*0.8), 1]]) # ラベル0・ラベル1各データ残り2割を検証データとして使用 test_data = np.array([tl for tl in tweet_label0[int(tweet_label0.shape[0]*0.8):, 0]] + [tl for tl in tweet_label1[int(tweet_label1.shape[0]*0.8):, 0]]) test_label = np.array([tl for tl in tweet_label0[int(tweet_label0.shape[0]*0.8):, 1]] + [tl for tl in tweet_label1[int(tweet_label1.shape[0]*0.8):, 1]]) return (train_data, train_label, test_data, test_label) RETWEET_COUNT_THRESHOLD = 1 MAX_TOKEN_LENGTH = 100 MIN_TOKEN_LENGTH = 6 NUM_FILTERS = 100 FILTER_SIZES = [3, 4, 5] WORD_VEC_DIM = 50 TWEETDATA_FILEPATH = [&quot;tweet_data/20170604-20170613_JPYUSD.tsv&quot;, &quot;tweet_data/20170618-20170627_JPYUSD.tsv&quot;, &quot;tweet_data/20170629-20170708_JPYUSD.tsv&quot;, &quot;tweet_data/20170706-20170715_JPYUSD.tsv&quot;, &quot;tweet_data/20170714-20170722_JPYUSD.tsv&quot;, &quot;tweet_data/20170724-20170801_JPYUSD.tsv&quot;] def main(unused_argv): # データの準備 tweet_processor = TweetProcessor() train_data, train_label, test_data, test_label = tweet_processor.prepare_data(file_path_list = TWEETDATA_FILEPATH) if FLAGS.job_name is None or FLAGS.job_name == &quot;&quot;: raise ValueError(&quot;Must specify an explicit <code>job_name</code>&quot;) if FLAGS.task_index is None or FLAGS.task_index ==&quot;&quot;: raise ValueError(&quot;Must specify an explicit <code>task_index</code>&quot;) print(&quot;job name = %s&quot; % FLAGS.job_name) print(&quot;task index = %d&quot; % FLAGS.task_index) #Construct the cluster and start the server ps_spec = FLAGS.ps_hosts.split(&quot;,&quot;) worker_spec = FLAGS.worker_hosts.split(&quot;,&quot;) # Get the number of workers. num_workers = len(worker_spec) cluster = tf.train.ClusterSpec({ &quot;ps&quot;: ps_spec, &quot;worker&quot;: worker_spec}) if not FLAGS.existing_servers: # Not using existing servers. Create an in-process server. server = tf.train.Server( cluster, job_name=FLAGS.job_name, task_index=FLAGS.task_index) if FLAGS.job_name == &quot;ps&quot;: server.join() is_chief = (FLAGS.task_index == 0) if FLAGS.num_gpus &amp;gt; 0:
      #  if FLAGS.num_gpus &amp;lt; num_workers: # raise ValueError(&quot;number of gpus is less than number of workers&quot;) # Avoid gpu allocation conflict: now allocate task_num -&amp;gt; #gpu
        # for each worker in the corresponding machine
        #gpu = (FLAGS.task_index % FLAGS.num_gpus)
        gpu = 0
        worker_device = &quot;/job:worker/task:%d/gpu:%d&quot; % (FLAGS.task_index, gpu)
    elif FLAGS.num_gpus == 0:
        # Just allocate the CPU to worker server
        cpu = 0
        worker_device = &quot;/job:worker/task:%d/cpu:%d&quot; % (FLAGS.task_index, cpu)
    # The device setter will automatically place Variables ops on separate
    # parameter servers (ps). The non-Variable ops will be placed on the workers.
    # The ps use CPU and workers use corresponding GPU
    ps_device=&quot;/job:ps/task:%d/cpu:0&quot; % (FLAGS.task_index)
    with tf.device(
        tf.train.replica_device_setter(
              worker_device=worker_device,
              ps_device=ps_device,
              cluster=cluster)):
        global_step = tf.Variable(0, name=&quot;global_step&quot;, trainable=False)

        # init saver
        saver = tf.train.Saver()

        # モデル準備
        cnn_model = TextCNN(num_classes = 2,
                    vocab_size = MAX_TOKEN_LENGTH,
                    embedding_size = WORD_VEC_DIM,
                    filter_sizes = FILTER_SIZES,
                    num_filters = NUM_FILTERS)

        global_step = tf.Variable(0, name=&quot;global_step&quot;, trainable=False)
        opt = tf.train.AdamOptimizer(FLAGS.learning_rate)

        dropout_keep_prob = 0.5

        if FLAGS.sync_replicas:
            if FLAGS.replicas_to_aggregate is None:
                replicas_to_aggregate = num_workers
            else:
                replicas_to_aggregate = FLAGS.replicas_to_aggregate

            opt = tf.train.SyncReplicasOptimizer(
                opt,
                replicas_to_aggregate=replicas_to_aggregate,
                total_num_replicas=num_workers,
                name=&quot;twitter_sync_replicas&quot;)

        train_op = opt.minimize(cnn_model.loss, global_step=global_step)

        if FLAGS.sync_replicas:
            local_init_op = opt.local_step_init_op
            if is_chief:
                local_init_op = opt.chief_init_op

            ready_for_local_init_op = opt.ready_for_local_init_op

            # Initial token and chief queue runners required by the sync_replicas mode
            chief_queue_runner = opt.get_chief_queue_runner()
            sync_init_op = opt.get_init_tokens_op()

        init_op = tf.global_variables_initializer()
        train_dir = tempfile.mkdtemp()

        if FLAGS.sync_replicas:
            sv = tf.train.Supervisor(
                is_chief=is_chief,
                logdir=train_dir,
                init_op=init_op,
                local_init_op=local_init_op,
                ready_for_local_init_op=ready_for_local_init_op,
                recovery_wait_secs=1,
                global_step=global_step)
        else:
            sv = tf.train.Supervisor(
                is_chief=is_chief,
                logdir=train_dir,
                init_op=init_op,
                recovery_wait_secs=1,
                global_step=global_step)

        sess_config = tf.ConfigProto(
            allow_soft_placement=True,
            log_device_placement=False,
            device_filters=[&quot;/job:ps&quot;, &quot;/job:worker/task:%d&quot; % FLAGS.task_index])

        # The chief worker (task_index==0) session will prepare the session,
        # while the remaining workers will wait for the preparation to complete.
        if is_chief:
            print(&quot;Worker %d: Initializing session...&quot; % FLAGS.task_index)
        else:
            print(&quot;Worker %d: Waiting for session to be initialized...&quot; %
                FLAGS.task_index)

        if FLAGS.existing_servers:
            server_grpc_url = &quot;grpc://&quot; + worker_spec[FLAGS.task_index]
            print(&quot;Using existing server at: %s&quot; % server_grpc_url)

            sess = sv.prepare_or_wait_for_session(server_grpc_url,
                                                config=sess_config)
        else:
            sess = sv.prepare_or_wait_for_session(server.target, config=sess_config)

        print(&quot;Worker %d: Session initialization complete.&quot; % FLAGS.task_index)

        if FLAGS.sync_replicas and is_chief:
            # Chief worker will start the chief queue runner and call the init op.
            sess.run(sync_init_op)
            sv.start_queue_runners(sess, [chief_queue_runner])

        # 学習開始
        time_begin = time.time()
        print(&quot;Training begins @ %f&quot; % time_begin)

        model_prefix = os.path.join(FLAGS.output_dir, 'trained_model_re')
        local_step = 0
        while True:
            local_step += 1
            # 入力データの準備
            feed_dict = {
              cnn_model.x: train_data,
              cnn_model.input_y: train_label,
              cnn_model.dropout_keep_prob: dropout_keep_prob
            }
            # 学習
            _, step, loss, accuracy = sess.run(
                [train_op, global_step, cnn_model.loss, cnn_model.accuracy],
                feed_dict)
            now = datetime.now().strftime(&quot;%Y/%m/%d %H:%M:%S&quot;)
            print(&quot;{} : local_step/global_step : {}/{}&quot;.format(now, local_step, step))
            # 10ステップごとに検証データに対して予測
            if local_step % 10 == 0:
                print(&quot;{} : local_step/global_step : {}/{} - train data accuracy : {}&quot;.format(now, local_step, step, accuracy))
                # 全検証データに対して予測
                predicted = sess.run(cnn_model.predictions, feed_dict={cnn_model.x: test_data, cnn_model.dropout_keep_prob: dropout_keep_prob})
                print(&quot;{} : local_step/global_step : {}/{} - test data accuracy {}/{} = {}&quot;.format(now, local_step, step, len(test_data[predicted == np.argmax(test_label, axis=1)]), len(test_data), len(test_data[predicted == np.argmax(test_label, axis=1)])/len(test_data)))
                print(&quot;{} : local_step/global_step : {}/{} - loss : {}&quot;.format(now, local_step, step, loss))
                # ラベル0の検証データに対して予測
                predicted0 = sess.run(cnn_model.predictions, feed_dict={cnn_model.x: test_data[np.argmax(test_label, axis=1) == 0], cnn_model.dropout_keep_prob: dropout_keep_prob})
                print(&quot;{} : local_step/global_step : {}/{} - label 0 accuracy {}/{} = {}&quot;.format(now, local_step, step, len(predicted0[predicted0 == 0]), len(test_data[np.argmax(test_label, axis=1) == 0]), len(predicted0[predicted0 == 0])/len(test_data[np.argmax(test_label, axis=1) == 0])))
                # ラベル1の検証データに対して予測
                predicted1 = sess.run(cnn_model.predictions, feed_dict={cnn_model.x: test_data[np.argmax(test_label, axis=1) == 1], cnn_model.dropout_keep_prob: dropout_keep_prob})
                print(&quot;{} : local_step/global_step : {}/{} - label 1 accuracy {}/{} = {}&quot;.format(now, local_step, step, len(predicted1[predicted1 == 1]), len(test_data[np.argmax(test_label, axis=1) == 1]), len(predicted1[predicted1 == 1])/len(test_data[np.argmax(test_label, axis=1) == 1])))

            if step % 1000 == 0 or step == FLAGS.train_steps:
                # モデルの保存
                saver.save(sess, model_prefix, global_step=step)

            if step &amp;gt;= FLAGS.train_steps:
                break

        time_end = time.time()
        print(&quot;Training ends @ %f&quot; % time_end)
        training_time = time_end - time_begin
        print(&quot;Training elapsed time: %f s&quot; % training_time)

if __name__ == &quot;__main__&quot;:
  tf.app.run()

4. 結果

非分散・分散で実行した結果を以下にまとめています。
今回は、分散処理による効果を顕著に確認したかったため、学習率を低めに設定しました。

■ 設定1(非分散)

VM数 Epoch フィルターサイズ フィルター数 Pooling ドロップアウト率 Optimizer 学習率 リツイート閾値
1 3000 3, 4, 5 100 Max Pooling 0.5 Adam 0.0001 1
かかった時間 検証用データ正解率
40517秒 (約11時間半) 0.7284 (3250/4462)

■ 設定2(分散)

VM数 Epoch フィルターサイズ フィルター数 Pooling ドロップアウト率 Optimizer 学習率 リツイート閾値
2 3000 3, 4, 5 100 Max Pooling 0.5 Adam 0.0001 1
かかった時間 検証用データ正解率
20172秒 (約6時間) 0.7385 (3295/4462)

全検証データに対する正解率/損失のエポック/時間変化のグラフを以下に記載しています。

・正解率

・損失

全検証データに対して、分散・非分散時共に約73%程度の分類精度で収束しています。
2VMの分散では、非分散に比べて1/2程度の時間で完了しました。
また、約2倍の速度で学習できていることが確認できます。

上記では、Embeddingの部分をWord2Vec学習済みデータを使用するように変更しましたが、Embedding層あり(https://github.com/dennybritz/cnn-text-classification-tf のモデルそのまま)で学習済みデータを使用しないパターン(単語ベースと文字ベース)でも検証を行ったところ、正解率は76%(文字ベース)、73%(単語ベース)程度となりました。
異なる点としては、学習済みデータを使用したものでは単語ベクトルリストの行列を入力値としていましたが、Embedding層あり&学習済みデータを使用しないものでは、ツイートを単語/文字辞書IDで羅列したベクトルに変換し入力している点です。

ソースは下記にあげていますので、興味のある方は参照いたければと思います。

■単語ベース
https://github.com/y-matsumoto-albert/TwitterRetweetCNNClassification/blob/master/TwitterRetweetCNNClassificationCharEmbedding.py

■文字ベース
https://github.com/y-matsumoto-albert/TwitterRetweetCNNClassification/blob/master/TwitterRetweetCNNClassificationCharEmbedding.py

 

5. まとめ

Azure Batch AI Trainingを用いて、2つのクラウドVM上でツイートのCNN2値分類
分散学習を行いました。

Batch AI Trainingで提供されているAPIを用いてクラスタやジョブの作成、実行、監視
あたりの処理は自分で行わなければいけませんが、テンプレート処理を作成しておけば、あとは用途に合わせて
・クラスタのスペック
・環境構築済みのコンテナ
・分散処理を行うスクリプト
をパラメータで指定してジョブを実行すれば複数のクラウドVM上で分散処理が行われ、分散環境準備に手間をかけず比較的簡単にディープラーニング分散学習が行えるのでないかと思います。

6. Appendix : 独自Dockerコンテナssh設定

Tensorflow環境構築済みのコンテナはbatchaitraining/tensorflow:1.1.0-gpuが提供されてますが、独自コンテナを使用する場合、sshでパスワード無でログインできるようにコンテナをセットアップしておかないと分散処理が正常に行われないようでした。
batchaitraining/tensorflow:1.1.0-gpu+ssh設定のDockerファイル例を以下に記載しています。

■Dockerfile

FROM batchaitraining/tensorflow:1.1.0-gpu

COPY ssh_config /root/.ssh/config
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
        openssh-client \
        openssh-server \
        iproute2 \
    &amp;&amp; apt-get clean \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    # configure ssh server and keys
    &amp;&amp; ssh-keygen -A \
    &amp;&amp; sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config \
    &amp;&amp; sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd \
    &amp;&amp; chmod 600 /root/.ssh/config \
    &amp;&amp; chmod 700 /root/.ssh \
    &amp;&amp; cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys

EXPOSE 23
CMD ["/usr/sbin/sshd", "-D", "-p", "23"]

■ssh_config

Host 10.*
  Port 23
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null

 

内定式を開催いたしました

$
0
0

こんにちは。人事の亀岡です。

今回は10月4日(水)に行なわれた2018年度新卒入社内定式についてご紹介いたします。
実は今回がALBERTとして初の内定式です☆

この日が内定者全員での初めての顔合わせということで、
皆さん緊張した面持ちで始まりました。

祝辞は代表の上村と配属先のデータ分析部部長の安達から。
これからALBERTの社員となるみなさんに期待することはもちろんのこと、まずは社会人の先輩として社会人になるにあたっての心得なども熱く語っていただきました…!

上村からは問題を解決するまで考え続け、学び続ける集団へとのメッセージ。
身が引き締まる思いです。

安達からは、『守破離』にのっとり一流の社会人になるためへののプロセスを。
守破離を繰り返し、会社を引っ張っていく存在になっていただきたいです。

その後は内定証書授与。みなさんの表情は真剣そのもの。
とても頼もしい気持ちになりました。

 

その後のランチは、ホテルへ移動し
なんとフレンチのコースを堪能しながらのテーブルマナー研修です!

慣れない席で表情は少し固め…ですが、
社会人になると式典参加の機会がぐっと増えることもあり、とてもためになったようです。
うらやましいのひとこと!!

 

その後はグループワークを実施。
3つのコミュニケーションゲームをそれぞれグループで競い合い、とても白熱しました!!



3つのゲームは、それぞれチームメンバーを変えて行なったのですが
終わってみると皆どれかのゲームで勝ちチームに入っており、
全敗が一人もでないという超優秀な結果でした!同期ならではの一致団結ですね~♪
(社内でも一度実施したのですが、中々難しかったです…!)

 

夜の懇親会では、先輩社員に熱心に質問したり、意見が飛び交う活発な場になりました。
1日を通し盛り沢山の内容でしたが終始和やかで、
始めは緊張していらした皆さんもこの一日でだいぶ打ち解けた様子でした。
私たち人事担当をはじめとする今回参加したALBERT社員にとってもとても充実した時間となりました!

 


活き活きした皆さんの表情を見て、こちらも初心を思い出しました…!
入社まで残り半年。社会人ではできない経験を踏んで、
さらに頼もしい姿になっていることを期待しています☆
4月から一緒に働けることを心待ちにしています!

 

「EC Camp 2017」トークセッションレポート!~AIによるEコマース業界の現在と未来~

$
0
0

こんにちは!PR担当の五味です。

ALBERTでは、近頃、AI(人工知能)関連イベントへの参加が非常に増えております。

今月もたくさんのイベントに参加しておりますが、先月には東京と大阪で行われた「EC Camp 2017」にALBERTの執行役員 パートナサポート部 部長 平原が登壇しましたので、本日は、その東京会場の様子をレポートいたします!

「EC Camp」はECビジネスのさまざまのノウハウが学べるビッグイベントですが、なかでも平原が登壇したスペシャルトークセッション「最新テクノロジーAIによるEコマース業界の現在と未来」は、非常に多くのご応募をいただきまして、イベント開始前から、AIへの注目度の高さが伺えました。

スペシャルトークセッションは、株式会社ブティックスター 代表取締役・プロデューサー・編集長である高田 博之様がモデレーターを務め、そしてパネリストとして4名が参加という形式でした。

東京会場では、株式会社Flicfit CEOの廣橋 博仁様、 Emotion Intelligence株式会社 代表取締役CEOの太田 麻未様、 カラフル・ボード株式会社 取締役CBOの皆川 朋子様、 そしてALBERTからは平原が、パネリストとして登壇いたしました。

各社自己紹介を終え、トークセッションが始まります。

全部で1時間半の長丁場でしたのでその全てをお伝えすることはできませんが、抜粋してレポートいたします。

 

そもそも、「AI」って?

高田様:はじめに、「AI」の大きな特徴は、学習することです。学習することでパターンを見つけ出したり、そこから将来を予測したりできます。

人間の感覚やあらゆる判断力を備えていて人間と同じように考えるものが「汎用AI」、特定のタスクについて人間と同様かあるいはそれ以上の処理をこなすことができるのが「特化型AI」とも呼ばれます。

こうしたAIが生活に与えるインパクトやメリットって、どういったものがありますかね?

 

平原:「汎用型AI」はまだ遠い未来だと思いますが、「特化型AI」に関しては既に実社会でどんどん使われていますね。ただ、どのようなビジネス課題に対してどのAI技術を活用するのかを見極める力が必要です。

 

皆川様:おっしゃっていることまさにそうだなと思って、何かしらでAIを使うっていう未来はすぐ実現されると思います。

また、使えるようにならないと競争に負けてしまうということもそうですね。使いこなせる能力を身に着けることは必要かなと。

 

高田様:そう考えていくと、AIを使いこなすためのAIが出てくるんじゃって思ってしまいますね(笑)

 

皆川様:そうですね、AIを作るAIという研究もいま盛んに行われています。それが進むとそれこそ「汎用AI」ができると言われますが、それはもう少し先のことでしょうね。

 

高田様:iPhoneの発売当初って使い方が難しいと感じましたが、いまは当たり前にみんな使いこなしていますもんね。AIもそのような状況がきつつあるのかもしれませんね。

将来的にはAIはどのように進化していくと思いますか?

 

廣橋様: AIの可能性はさまざまあるかと思いますが、購買予測などはもちろんのこと、健康管理といったようなライフスタイルに関する部分も、今後はAIを通じてどんどん進化していくのではないかなと考えています。

 

太田様:AI自体は目的ありきの手段ですので、どんな目的で使うか、どんなシーンで使うかに着目すべきだと思います。

AIは万能、のような言われ方をされていますが、まだAIは万能ではなくて、人間のほうが効率的にできることもあります。ただ、AIのほうが効率的なこともあるので、そうした範囲が将来的には広がっていくのかなと思います。

 

 

 

具体的にはどんな技術?EC運営会社にとってのメリットって?

高田様:実際に、技術としてはどういうものを使用していますか?

 

平原:弊社の場合、さまざまな技術を組み合わせています。購買情報・閲覧データのようなマーケティング系の分析の場合は一般的な統計解析を用いることが多いです。非定型データ、つまり画像や自然言語の場合は、ディープラーニングを用いることがほとんどです。目的やデータの種類によって最適な機械学習や統計解析の手法を用いています。

 

高田様:AIを用いることで、EC運営会社にとってのメリットや、実際に数値的効果があった例はありますか?

 

平原:わかりやすい例ですと、これまでRFM分析を用いてDMの送付対象者を選定していたECサイト様で、弊社の技術を用いたターゲティングによって広告効率が160%に改善したという事例がありますね。RFM分析で顧客を区分する場合は、過去にどんなカタログを受け取ったか、どんな画像を閲覧しているか等の情報は加味されていません。この事例では、顧客が受け取った情報も加味して、商品やブランドと顧客の親和性を算出し、ターゲティングに活用しています。

また、AIを活用したチャットボットの事例も増えています。問い合わせに24時間応答できますし、お客様がとても気軽に質問できるというメリットがあります。EC運営会社としては、そうして集まったデータを分析することでお客様が求めている情報を知ったり、コンテンツ制作等に活かしたりすることができます。

 

 

 

AIに向いている業種とは?

高田様:ALBERTは幅広い業種のクライアントがいらっしゃるかと思いますが、結果がすぐ出やすい業種ってEC業界でありますか?

 

平原:そうですね、ファッションは非常にわかりやすいと思います。趣味嗜好がわかりやすく表れますし、アパレル画像は特徴を抽出しやすいですから。人間にとって特徴がつかみやすいものは、AIも同様に特徴をつかみやすいということですね。

 

高田様:そうなんですね。ファッションって曖昧なようにも感じて、本のようなスペックで表せるもののほうが、効果が出やすいように思ってしまいます。実際は違うんでしょうか?

 

平原:本については、全文や説明文をAIに読ませることができれば精度が上がると思います。短いタイトルだけでは難しいかなと思います。

 

皆川様:おっしゃるように、AIに全文読ませるのはなかなか難しいですよね。ただ、要約の部分を読ませた場合でも、おすすめの効果は10~20倍程度あります。

業界によっても、合う、合わないということもちろんあるかとは思いますが、それよりも、その業界のどのデータをAIに使うかという選び方のほうが重要だと思いますね。

 

 

 

AIの導入って簡単なの?

高田様:AIの導入って難しいんでしょうか?

 

平原:チャットボットの導入は比較的簡単ですね。タグを埋めてもらうだけでログを自動で集めますし、導入は最短1週間です。

太田さんのところでは、タグを埋め込むだけで0.03秒に1回、リアルタイムに行動ログを収集していると聞きましたが…

 

太田様:はい!

 

平原:そのデータに非常に興味があります、欲しいですね(笑)

そうした細かいデータもとれる世の中になってきたので、今後ますますデータの活用が進んでいくんじゃないでしょうか。

 

太田様:弊社は、取得している膨大なデータの使い道としては、現時点はかなりシンプルで、クーポンを最適なタイミングで出すということにしか使っていないんですね。ある意味贅沢な使い方ではありますが、だからこそ、導入はとてもシンプルになっています。取得したデータに関しては、今後使える領域を増やしていきたいなと思っています。

 

高田様:ありがとうございます。

今後ECにおいてもますますAIは使われていくでしょうね。今後の各社の取り組みにぜひご注目ください。

 

 

 

終わりに

本日は、「EC Camp 2017」スペシャルトークセッションをレポートいたしました。いかがでしたでしょうか?

イベント終わりには、AIに関する具体的なご質問・ご相談もいただきまして、誠にありがとうございました。

 

今後もALBERTは、積極的にイベントに参加してまいります。

来週10月23日(月)は、「データサイエンティスト協会 4thシンポジウム」に参加予定です!学生向けセッションと、テクノロジーセッションに参加いたします。皆様ぜひお越しください!

2019新卒採用向けノベルティ制作

$
0
0

こんにちは♪ 広報の大森です。
先日、2018年新卒入社の内定式の様子をご紹介しましたが、
ここ最近は2019年卒新卒採用に向けたノベルティ制作を進めています。

ALBERTが初めて新卒採用をしたのは2013年。そのころは2名の採用でした。
あれから早5年・・・来年2018年の新卒採用者は11名、そして2019年も積極的に新卒社員の採用を予定しています。

今回作成したノベルティは、ALBERTのトートバッグ!

新卒説明会の会場などで配られた資料を入れられるようにA4サイズになっています。
また、普段の生活でも活用していただけるように、ナチュラル素材でシンプルなデザインにしました☆

こちらは、少し前に作成した、ALBERTオリジナルのミネラルウォーター。

ALBERTの社名の由来でもあるアインシュタインのイラストを前面に、シックなデザインに。
裏にはALBERTの経営理念が書かれており、ALBERTという会社のことを少しでも知っていただければという想いが込められています。

これらのノベルティは、2019年新卒者向けの合同説明会開場などでALBERTのブースに来ていただいた方にお配りする予定です。
ブースでは、採用担当の山内・亀岡がお待ちしておりますよ~!

新卒採用の情報については、採用サイト新卒採用ページをご覧ください♪

学生の方で在学中にALBERTで働いてみたい!という方はインターンやアルバイトの募集もしておりますので、インターン・アルバイト採用ページもご覧ください☆

では、気がつけば年の瀬。2017年も皆様には大変お世話になりありがとうございました。
2018年もご支援賜りますようどうぞよろしくお願いいたします。


NVIDIA主催、日本最大のGPUテクノロジーイベント「GTC Japan 2017」レポート!

$
0
0

こんにちは!PR担当の五味です。

先日、ヒルトン東京お台場で開催されたNVIDIA主催のイベント「GTC Japan 2017」に出展し、「深度推定(距離推定)エンジン」を発表してまいりましたので、本日はその様子をレポートいたします。

GTC Japan 2017」は、文部科学省および理化学研究所後援のもと、NVIDIAが主催する日本最大のGPUテクノロジーイベントで、GPUテクノロジー関係者が一堂に会す貴重な機会です。ヒルトン東京お台場の宴会場にて行われ、会場はクリスマスムードでとても華やかでした☆

 

ALBERTが発表した「深度推定エンジン」って?

今回のイベントでALBERTは、「深度推定エンジン」を発表しました。この技術については、先日、日本経済新聞や日経産業新聞でもとりあげていただいています。

「深度推定」とは、二次元の映像・画像を解析し、カメラから物体までの距離を推定する技術です。人間の脳は、左右の目に映る景色の違い(視差)やこれまでの経験をもとにして対象までの距離を推定することができますが、ディープラーニングを活用することで、それと同じことを高精度に行なうことができます。

さらに今回ALBERTが発表した技術は、ひとつのカメラだけで行なう単眼推定が可能で、また極めて安価で汎用的な性能のカメラを用いた場合でも高い推定精度を実現しています。
この技術は自動車の自動運転で活用できるほか、工場や倉庫における物資の自動運搬、自動掃除機などの家庭用ロボット、車いすなどへの活用も期待できます。


 

2日間、展示ブースにてデモンストレーション!

展示ブースでは、単眼カメラを設置し、実際にブースの前の様子をモニターに映してリアルタイムで深度を推定するデモンストレーションを行ないました。
この技術は、NVIDIAの自動運転向けAI車載コンピューター「DRIVE PX Parker AutoCruise」上で実演しています。

モニターに映るすべての箇所の深度を推定するだけでなく、物体認識の技術も組み合わせることで実際に通りがかる皆様や映る物体を認識し、その物体が何であるか、また、その距離が何メートルあるかについて表示します。

このデモンストレーションは非常にたくさんの方に興味を持っていただき、さまざまなご質問を頂戴しました。
大変ありがたいことに、事前に用意した配布用のパンフレットも在庫が足りなくなるほどでした。(急いで追加印刷いたしました!)
お話をするのに少々お待ちいただくことも多々あり、申し訳ございません。「深度推定を見るためにGTCに来ました」とおっしゃる方もいらっしゃって、嬉しく思います。

当日は2日間とも、実際に開発に携わったデータアナリストも参加いたしました。おかげさまで、さまざまな知見のある参加者の皆様と有意義なディスカッションを行なうことができました。

 

1日目「INCEPTION AI スタートアップ サミット」

また展示ブースでの発表以外でも、ありがたいことに2日間ともセッション登壇の機会をいただきました。
1日目は、NVIDIAのInceptionパートナー70社の中からエントリーされた19社が登壇する「INCEPTION AI スタートアップ サミット」です。

ALBERTは5番目。代表取締役社長 上村の登壇です。会場は満席で、立ち見の方も多くいらっしゃいました。
実際に当日上村がお話した内容を抜粋してお伝えいたします!

「今回の深度推定エンジンは、実際に自動車にカメラを搭載し都内を走行して収集したデータで学習しています。したがって、このエンジンを採用いただく際には、この学習済みモデルを提供することが出来ます。
また、今回は可視光カメラで学習させていますが、赤外線などのカメラでデータを学習させれば、夜間の走行にも応用できる可能性があります。

デモンストレーションで利用したカメラは小売価格で数千円の、非常に安価なものです。安価なカメラでも高い精度が担保できれば、自動車や小型自動運搬機、AI家電といった製品にこの技術を採用する際にコストの上昇を防ぐことが出来るという大きなメリットがあります。
複眼ではなく単眼での推定を実現しており、カメラはひとつだけで済むため、ここでもコストアップの抑制効果があります。

もちろん、コスト以外のメリットもあります。例えば自動運転で複眼カメラを採用していたとしても、片側が故障してしまったり、ゴミがついて見えなくなったりする可能性はあります。その際、単眼だけで距離を測ることができれば安全性を保つことが出来ます。

ALBERTでは今回このエンジンをNVIDIAの自動運転向けAI車載コンピューターであるDRIVE PX Parker AutoCruiseに搭載して動作を実証しています。
車載コンピューター向けにエンジンをカスタマイズしたり再開発したりする手間もありません。」

セッションでは、上村が自らオフィス周辺の新宿にて車を運転して撮影した二次元の映像を用いて、深度を推定した数分間の映像もご覧いただきました!
この映像を見ると、車や人が多くいる混雑した新宿でも、高い精度で深度を推定することができるとわかります。

 

2日目「ディープラーニング ビジネス/テクニカルトラック」

2日目は、「ディープラーニング ビジネス/テクニカルトラック」に上村が登壇しました。お昼休みの時間帯でかつ立ち見の会場にも関わらず、多くの方が足を止めてくださいました。
会場で配布されたお弁当を食べながらご覧になる方もいらっしゃいました。ありがとうございます。

こちらのセッションでは、深度推定技術だけでなく、ディープラーニングを活用したそのほかの事例も数多くご紹介いたしました。
ALBERTでは、技術の開発だけでなく、多数の技術者の豊富なスキルと開発経験を活かし、人工知能・ディープラーニングの活用に問題を抱えている企業様のためにビジネスへの応用を支援するサービスをご提供しています。モデルチューニングのみのご依頼も承りますし、ビジネスロードマップの作成からシステム化、製品への組み込みまで一貫したサポートも可能です。ぜひお気軽にお問い合わせください。

 

終わりに

本日は、「GTC Japan 2017」の様子をレポートいたしました。
2日間どの時間帯も非常に盛況で、GPUテクノロジーへの注目度の高さが伺えるイベントでした。展示ブースにお越しくださった方、セッションを聴いてくださった方、ありがとうございました。

来年も、ALBERTはイベントへの参加を予定しています。
1月17日(水)~19日(金)には、東京ビッグサイトで開催される「オートモーティブ ワールド 2018」にて、ハンドル・ブレーキ・アクセル操作の特徴から運転者を識別し、その運転者がどのような特性を持つか自動判別する「ドライバー安全運転適性診断アルゴリズム」をご紹介します。
また、こちらのイベントでも「深度推定エンジン」をご紹介予定ですので、ぜひ皆様足をお運びください!

2018年度新入社員入社式を開催いたしました♪

$
0
0

こんにちは。人事の亀岡です!

暖かい日が続き、春本番ですね。
年度初めの4月2日、2018年度新入社員入社式を開催いたしました★

ALBERTは新たに12名の仲間が加わり、新卒採用としては過去最高人数。
式典を行った会議室も満員で、身の引き締まる思いでした・・・!



式典の様子。
厳かな雰囲気ですが、笑いもあり和やかに進みました。


代表取締役松本より挨拶。
これから社会にはばたく新卒12人へ激励の言葉を。


新入社員のみなさんには決意表明を頂きました・・・!

式典の後は各部門責任者より部署説明を行い、会社の理解を深める時間を。
質疑応答が活発に行われる場となりました★
一緒に働ける日が待ち遠しいです♪


記念撮影では少し緊張の面持ち・・・。
しかし、式が終了した後は同期のみなさんで決意用表明の話やこれから始まる研修の話で盛り上がっていました♪
新たに12名の仲間が増え、一層にぎやかになりそうです!

採用ページにも記載していますが、ALBERTのデータサイエンティストは
最先端の研究機関や数理統計学、物理学、金融工学、宇宙工学など様々な分野から集まった優秀なメンバーが揃っています。
新たに入社した12名も、これからのALBERTを牽引していく存在になってくれることでしょう・・・!

新入社員のみなさんは、これから約2か月の技術研修に入ります。
こちらの研修はALBERTが誇る、データサイエンティストが時間をかけじっくり考案したもの。
そんな研修を受けられるなんて、うらやましい・・・!
その様子はまたブログにてしっかりレポートいたします♪

【採用情報】
ALBERTでは2019年新卒採用を積極募集しています!
お気軽にお問い合わせください♪

「第2回 AI・人工知能EXPO」に出展☆ 熱気のあるブースの様子をレポートします!

$
0
0

こんにちは!PR担当の五味です。

4月4日(水)~6日(金)に東京ビッグサイトにて開催された、「第2回 AI・人工知能EXPO」に出展してまいりました。
3日間、非常にたくさんの方にお越しいただきまして、おかげさまで大変熱気のあるブースとなりました。本日は、その様子をレポートいたします。

 

ALBERTは、昨年の「第1回 AI・人工知能EXPO」に引き続きの出展ということで、今年はさらにパワーアップし、創業以来最も広いブースで出展いたしました♪

展示では、画像認識、故障検知、予知保全、需要予測、売上予測、深度推定(距離推定)など、多数の分析事例をご紹介し、さらに、LINEや有人チャットとの連携も可能なチャットボット「Proactive AI」も合わせてご紹介しました。

またブース内では、展示だけではなく、1日につき、なんと20回も(!)セミナーを開催いたしました!
分析事例のご紹介や「Proactive AI」のご紹介など、内容はさまざまでしたが、始まるまでは、随時開催してもお客様に集まっていただけるかどうか不安もありました。しかし、いざ始まってみると、どの回もたくさんの方にご覧いただき、またセミナー後にはたくさんの方とお話することができました。お立ち寄りいただいた皆様、ありがとうございます。

 

ALBERTのブースは、今回も、ロゴマークにも用いているグリーンを主体といたしました。
ロゴマークのグリーンは若さと新しさを表していて、既成概念にとらわれない新しいビジネスモデルを構築し、心身共に永遠に若い会社、つまり成長し続ける会社でありたいという思いが込められています。社名の由来やロゴマークについての詳細はこちらをご覧ください♪

 

今回は、展示案内スタッフは春らしい爽やかなブルー、データサイエンティストは知的なネイビーのシャツでお出迎えしました!
シャツの左胸には、ALBERTロゴマークの刺繍が入っています。

ネイビーシャツのデータサイエンティストは普段は社内やお客様先でデータ分析業務を行なっていて、高い技術力を持ちながら、日々切磋琢磨してビジネスを推進しています。今回は、ALBERTに在籍する約100名のうち数名にはなりますが、カウンターにて、お客様の課題についてのお話や分析手法・技術の詳しい説明などを行ないました。データサイエンティスト達も、お客様から直接生のお声を聴くことができ、大変学びある時間だったとのことです☆

 

また、1日20回開催したセミナーですが、毎日お昼すぎに特別セミナーを実施いたしました。
下の写真は、執行役員 営業推進部部長 先進技術統括の安達による特別セミナー「現状のAIの限界と将来展望」の様子です。始まる前から非常に多くの方にお集まりいただき、セミナースペース前を埋め尽くすほどとなりました。
タイトル通り、いまAIが実現できることや、まだ不可能だけれども今後できるようになるであろうことについて、40分ほどお話させていただきました。立ち見の方が大勢というにも関わらず、長時間ご覧いただいた皆様、誠にありがとうございました。

 

今回は事前に、約1ヶ月かけて冊子型のパンフレットを制作いたしました!8ページにわたって、分析手法や分析事例、プロジェクトの進め方、活用するデータとそれに合った分析テーマのご紹介など、データ分析にまつわるさまざまな内容を詰め込んでおります。会場では、10分に1回は補充をしないとなくなってしまうほど、非常にたくさんの方が手にとってくださいました。ありがとうございます!
配布パンフレットは約1万部ご用意していましたが、展示会が終わる頃には、そのすべてをお客様にお渡しできました。
今後もこちらはセミナーや展示会等で配布予定ですので、その際はぜひお手にとってご覧ください♪

 

本日は、「第2回 AI・人工知能EXPO」の様子をレポートいたしました☆

昨年と同様に、3日間朝から夕方まで非常に盛況で、今年もAIへの注目度の高さが伺えました。
また、ALBERTブースに大変多くの方にお越しいただきまして、感謝申し上げます。ブースにお立ち寄りいただいた皆様、セミナーをご覧くださった皆様、誠にありがとうございました!

データサイエンティスト新卒研修レポート

$
0
0

こんにちは。
2018年4月に新卒としてALBERTに入社した、データソリューション部の羽山です。

入社式から、早3ヶ月が経ちました。
ALBERTでは入社式のブログでお伝えしたように、ALBERTに在籍している100人のデータサイエンティストのノウハウを活用した2ヶ月のデータサイエンティスト技術研修が新卒社員に対して行われます。

本記事では入社後2ヶ月間に渡って受講したデータサイエンティスト研修の内容についてご紹介します。

目次

  • 自己紹介
  • 研修内容
    前半: Python・機械学習
    後半: 演習
    ~演習1: 音楽ストリーミングサービスにおける離反会員の予測~
    ~演習2: Deep learning を用いた自動車走行中の道路状況の把握~
    まとめ

自己紹介

大学院では数学科で統計学の研究、特に非負値行列分解におけるランク選択法について研究をしていました。また、研究室のメンバとハッカソンやコンペティションに参加して、POSデータを用いた併売傾向の分析やサッカーにおけるデュエル (ハリルJAPANの時代) を識別するモデル作成などをしていました。

ALBERTに入社したきっかけは、学部4年の時に2週間のサマーインターンに参加したことです。その後、アルバイトとして2年7ヶ月働きました。

参加したプロジェクトは、

  • 中古車販売の販売価格予測
  • スポーツ用品店における広告費削減のモデル作成
  • 小売業のサイト閲覧データを用いた顧客クラスタリング
  • 保険業界におけるプランの併売傾向分析

など様々です。その際に扱うデータの種類や用いる手法は多岐に及びました。

また、実際のデータは研究やコンペティションなどで用いるデータよりもdirtyなため、アルバイトをすることで、SQLやUNIXコマンドを用いてデータクリーニングをする力が身につきました。

大学院では理論を学び、ALBERTのアルバイトでは実際に分析手法を活用する経験を積むうちに、データ分析の面白さと学ぶことが尽きないところに魅力を感じるようになりました。

修士2年の就職活動はデータサイエンティスト職を募集している企業を中心に、いくつかの企業の選考を受けましたが、次の理由でALBERTへの入社を決めました。

  • データ分析によるソリューションを事業のコアとしている
    ・事業会社内のデータ分析部ではなく、会社全体の主軸をデータ分析としている
  • 仕事に集中でき、モチベーションを保てる社内環境・制度
    ・数多くのデータサイエンティストが在籍している
    ・基本的にはキーボードを打つ音しか聞こえないくらい静か
    ・残業が少なく、集中力を保てるため効率よく働くことができる
    ・会社全体の行事が少なく、年休日が決まっているため予定を組みやすい
    ・給与が高い
    ・様々な分野に精通したデータサイエンティストと協働し、自身が成長できる
    ・質問がしやすく、定期的に勉強会が開催されるため、学べる環境である

勉強会は業務時間内に参加することができます。最近では、Deep Learning に関連する論文の輪読会や最近の分析手法に関する発表を持ち回りで行っています。

また、入社後に感じたことですが、同期も各分野の修士・博士であり、専門性が高く切磋琢磨しあえるメンバです。

同期は文系・理系問わず、数理工学・機械学習・ロボット工学・計量経済学・心理学・経済学・量子物理学・神経生理学など、各々が専門とする分野は多種多様ですが、対象からデータを取得し、分析をすることは共通しています。

研修内容

研修の前半では、講義形式でPythonと機械学習の基礎から応用までを学びました。
後半では、前半に得た知識を活用し、実際の案件に近い内容での演習に取り組みました。

前半: Python・機械学習

前半の1ヶ月では、統計学や機械学習などの理論とそれを実現するためのプログラミングについて、現役のデータサイエンティストである先輩社員からハンズオン形式で学びました。

まずはじめに、分析で主に用いるPythonの基礎として

  • 環境の設定・演算
  • ミュータブル・イミュータブルな型
  • データ分析に用いるライブラリの使い方 (NumPy・Pandas)
  • コーディングの作法・オブジェクト指向・行列演算 (ブロードキャスト)

を学んだ後、

  • 統計学の基礎
  • ランダムフォレストやサポートベクタマシンなどの予測・分類手法
  • ホテリング理論を用いたデータの異常検知
  • Word2vecを用いた自然言語処理
  • 状態空間モデルを用いた時系列分析

などをPythonを用いた演習を交えながら、ハンズオン形式で学びました。

分析手法について一通り学んだ後は、データベースを操作するためのSQLやスムーズな分析の手助けになるUNIXコマンドなどを学びました。

私は研究やインターンではRを用いていたため、Pythonはほとんど初心者だったので、内定者に事前配布されていた 「みんPy」 で得た知識を引っ張り出しながらなんとかついていきました。一方、同期にはPythonをバリバリ使っていた人もいて、レベル感はそれぞれでした。

講師の先輩データサイエンティストの方々から理論やプログラミングと合わせて、実際の分析事例や、分析の現場において注意すべき点などをレクチャーしていただけたのがとても勉強になりました。

後半: 演習

後半の1ヶ月では、実際の案件に近い内容で演習を2つ行いました。
演習1では音楽ストリーミングサービスにおける離反会員の予測、演習2では Deep learning を用いた自動車走行中の道路状況の把握を行いました。
以下で、それぞれの演習の内容についてご紹介します。

演習1: 音楽ストリーミングサービスにおける離反会員の予測

音楽ストリーミングサービスにおける離反会員の予測を、サービスの契約情報や利用情報、会員のデモグラ情報 (性別や居住地など) を用いて行いました。

契約情報はストリーミングサービスをいつ契約したか・何日間のプランを契約したかなどの情報、利用情報は1日の視聴曲数や視聴時間などの情報で構成されています。

目的

  • 実際の案件に想定されるような一連の分析業務を実施する
    ・大量ログデータのハンドリング
    ・基礎集計・可視化・モデリング
    ・分析報告書の作成

問題設定

  • 解約・離反の定義を行い、解約した会員が離反するか否かを予測する
  • 施策につながる示唆を与える

その他条件

  • 期間は12日間
  • 各々で取り組む
  • 周りとの意見交換は可

入社前に各々が選んだMacBookProかWindowsで分析を行いました。データの大きさがメモリに乗るぎりぎりだったため、個人の分析方針でSQLを用いるか、PythonのPandasを用いてゴリ押しするかでデータクリーニングの仕方はわかれました。

ある程度dirtyなデータだったので異常値や外れ値を決め、データクリーニングを行い、基礎集計から予測モデルの作成・モデルの評価まで行いました。

最終報告では、先輩データサイエンティストの方々にフィードバックをいただきました。予測モデルとしてはランダムフォレストや Gradient Boosting Decision Tree (GBDT) を用いている人が多かった印象です。

演習で難しかったのは問題として与えられていた解約・離反会員を定義する部分でした。実際の案件でも、根拠を持ち、クライアント (今回は先輩データサイエンティストをクライアントとした) が納得する定義を自分から提案していく力は必要なので、定義を自分なりに考えることはとても良い経験になりました。また、12日間という短い期間で一連の分析業務を行うことで、分析のスピード感を感じることができました。

演習2: Deep learning を用いた自動車走行中の道路状況の把握

Deep learning を用いた自動車走行中の道路状況の把握を、CamVid データセット を用いて行いました。

CamVid データセット は、自動車の走行中の動画データとなっています。動画を加工して画像にしたものが既に提供されているため、今回はそちらを用いました。

データセットは元となる画像と、その画像の各ピクセルが何かしらのクラス (CarやTreeなど) に割り振られている教師データとなる画像の組になっています。

目的

  • 技術的な部分をフォローアップすること (+ 演習1での目的)

問題設定

  • 元画像を Deep learning で構築した学習器に与えたときに、各ピクセルがどのクラスであるかをセグメンテーションする (セマンティックセグメンテーション)

その他条件

  • 期間は9日間
  • 2~3人のチームで取り組む

私達のチームは3人チームで、全員がほぼ Deep learning 初心者でした。そのため、それぞれが Deep learning の基本的な知識を得て、モデルを実装する力をつけたいと考え、SegNet担当・U-Net担当・シンプルな畳み込みオートエンコーダ担当と大まかに分担して演習に取り組みました。

Deep learning は計算コスト的にラップトップでは時間がかかるため、テスラK80を載せたAWSのインスタンスを各チーム1~2個使いました。

中間報告で他チームの情報も吸収しつつ、軌道修正しながらなんとか最終報告ではいくつかのモデルで精度検証を行うことができました。
他チームも主にSegNetやU-Netが多かったですが、中には Generative Adversarial Network (GAN)を用いたモデルにチャレンジしているチームもありました。

今回は、9日間と少し短めの演習でしたが、最終報告では単眼の深度推定を行った先輩データサイエンティストの方々にたくさんのアドバイスを頂くことができ、知識を深めることができました。

まとめ

新人研修として2ヶ月間、

  • Python・SQL・UNIXなど、プログラミングの習得
  • 統計学の基礎や機械学習の様々な手法を学び、用いる
  • 実際の案件に近い内容での演習

を行い、データサイエンティストとしての基礎力を身に着けました。また、報告の際のストーリーを意識することや、モデルの実装のためにプログラミング力を更に鍛える という今後の課題をみつけることができました。
全体を通して、現場で実際に手を動かして分析している先輩データサイエンティストの方々に質問でき、一人一人フィードバックを受けられるというとても充実した環境での研修でした。

また、充実した研修内容もさることながら、2ヶ月間を共にした同期との団結も深まりました。

 

来年度の研修では、著しい進化を遂げるAIの流れを感じられるような、最新の技術にも触れられるものになるように、ALBERTの一メンバとしてさらなる研修のパワーアップに取り組みたいと思います。

今後は、実際の案件に加わることになりますが、技術のフォローアップを怠らずに腕を磨いていきたいと思います。また、将来的にはプロジェクトの主軸となるプロジェクトマネージャーになれるよう、ビジネス面のスキルアップにも力を注いでいきたいです。

最後まで読んでいただきありがとうございました。

 

ALBERTでは積極的に新卒採用/インターンの募集を行っています。
お気兼ねなくお問い合わせください。

・2019年卒新卒採用ページ
・短期インターン募集ページ

 

ニューラルネットの新しい正規化手法 Group Normalization の高速な実装と学習実験

$
0
0

今年 1 月に ALBERT に入社した清水です。深層学習プログラマとして自社プロダクト開発をしております。このブログを書くのは始めてなのですが、今日はちょっとプログラミング寄りの記事を。残暑厳しい折ですが、実装の詳細にまで立ち入りつつアツく Yuxin Wu および Kaiming He の考案した手法 Group NormalizationECCV 2018 採択済)について語ります。Kaiming He 氏は ResNet を筆頭に優れた convolutional neural networks (CNN) の設計で知られていることもあり、みなさんも注目している手法ではないでしょうか?

Chainer v5.0.0b4 を用いて、Chainer に入っている実装と弊社の独自実装での速度比較、また Batch Normalization との速度・精度比較を行いました。その上で、速度差が生じる原因の調査や CUDA を使った高速化の工夫について詳しく記します。ソースコードは MIT ライセンスで GitHub に公開していますので、ぜひご覧ください。

Group Normalization って?

Group Normalization の発想はシンプルです。早速ですが、Group Normalization を端的に表す図(論文 Figure 2 より引用)を見てみましょう。

Batch Normalization, Layer Normalization, Instance Normalization, Group Normalization の図示

(N, C, HW) の 3 次元からなる多次元配列が出てきましたが、これは CNN の中間層の出力だと考えると想像しやすいかと思います。バッチサイズ N, チャンネル数 C, 画像の縦幅 H, 画像の横幅 W です。

図に示された Batch Normalization, Layer Normalization, Instance Normalization およびこの記事の本題 Group Normalization の 4 つの手法は、いずれも青色で示された領域ごとに算出される平均・分散によって入力を正規化 (normalize) します。Batch Normalization が各チャンネルごとに平均・分散を求めることは有名ですね。精度・収束性を劇的に向上させる Batch Normalization は今や CNN のデファクトスタンダードですが、しかし

  • 画像の解像度が大きくてメモリが不足するなどの理由でバッチサイズが小さくなる場合に、平均・分散の推定が不安定になり学習ができなくなる
  • 複数 GPU にまたがって平均・分散を推定することで実質的なバッチサイズを増やすことは可能だが、高価なハードウェアが必要になる上に実装や最適化が複雑
  • ビデオの隣接フレームといった相関がある画像をミニバッチとして入力する場合も平均・分散の推定が不安定になる
  • 学習時に平均・分散の移動平均を覚えておいて推論時に用いるといった処理が煩雑
  • Finetune 時に移動平均をどう扱うべきかよくわからない

といった難点も併せ持っており、いつでも使えるわけではありません。このためミニバッチに依存しない正規化手法が待ち望まれていました。

そのような手法として、全チャンネルにまたがって平均・分散を取る Layer Normalization と、各チャンネル独立に画像の縦横方向についてのみ平均・分散を取る Instance Normalization が考案されました。とはいえ十分にバッチサイズが確保されている条件下での精度は Batch Normalization に比べてかなり劣っており、主流とはなっていません。

そこで登場したのが Group Normalization です。チャンネルを G 個にグルーピングして Layer Normalization と Instance Normalization の中間的な処理を行うことで、画像分類などのタスクで Batch Normalization に匹敵する精度を実現しました。グループ数 G を 32 にした場合がベストだったと論文に述べられていますが、それほど G の値に対して敏感ではないようです。Group Normalization の論文では、Instance Normalization には複数のチャンネルの組み合わせによって表現される特徴を歪めてしまう問題があると考察されています。その対極になるのが Layer Normalization ですが、こちらは大域的すぎて、特に重要ではないチャンネルが画像全体にわたって高い値を示した場合に他の全てのチャンネルが抑制されてしまいます。中間的な Group Normalization は良いとこ取りをできるというわけです。

なんだか素晴らしそうですね。Chainer では v5.0.0b3 から Group Normalization がサポートされているのでお手軽に使えます。しかし、本当に Batch Normalization をドロップインで置き換えて精度低下は起きないのでしょうか? Batch Normalization と同等の速度で動作するのでしょうか? この疑問を検証します。

結論から言えば、Batch Normalization を単に Group Normalization に置き換えるだけでは精度がかなり落ちてしまいました。なので Group Normalization を使う場合は精度の確認やパラメータチューニングをきちんとやるべきでしょう。また、Chainer v5.0.0b3 に追加された Group Normalization の実装はあまり効率的ではなく、CNN 全体の実行速度を大きく下げてしまうことがわかりました。この原因や、より効率のいい実装方法についても詳述します。

実験タスクおよび学習条件

ResNet-50 ネットワークを用いて、オリジナルの Batch Normalization を使うバージョンと、グループ数 G=32 の Group Normalization を用いるよう変更したバージョンでそれぞれ画像分類の学習を行いました。用いたデータセットは Oxford VGG Pets データセットに含まれる 12 品種の猫の画像です。データセットはなんでもよかったのですが

  • CIFAR-10 もしくは CIFAR-100 だと実務で扱うデータと比べて画素数が少ないため、計算時間の測定条件としては不適切
  • ImageNet のような大規模データセットの学習には時間がかかる
  • 猫かわいい

という理由で決めました。

Abyssinian, Bengal, Birman, Bombay, British Shorthair, Egyptian Mau, Maine Coon, Persian, Ragdoll, Russian Blue, Siamese, Sphynx の 12 品種のサンプル画像

各品種ごとにおよそ 200 枚ずつのデータがあります。このうち 8 割を訓練用に、2 割を検証用に用いました。

学習に用いた設定は

  • バッチサイズ: 32
  • 画素数: 短辺が 256px になるようバイリニア補間でリサイズ後、224px × 224px の正方形にクロップ
  • Augmentation: ランダム位置でのクロップおよび、ランダムな左右反転
  • Optimizer: MomentumSGD, momentum=0.9
  • Weight decay: 0.0005
  • エポック数: 300
  • 学習率: 0.1 から始めて、100 エポックごとに 1/10 倍

です。確実な実験を期するため、疑似乱数生成器のシードを固定することでデータセットのサンプリング順序・重みの初期化・augmentation の挙動が実験ごとに変わらないようにしています。ただし cuDNN を使っているため、アトミック加算などの誤差の出方が GPU 内でのスレッドスケジューリングに依存することによる、数値計算的な非決定性は存在します。

実験を行った計算機環境は

  • ハードウェア
    • CPU: Intel Core i5-3550
    • GPU: ZOTAC GeForce GTX 1080 Ti AMP Edition
    • SSD: Crucial MX200 500GB
    • RAM: DDR3-1600 SDRAM 4GiB × 4
  • ソフトウェア
    • Gentoo Linux amd64 (kernel 4.18.5)
    • Python 3.6.6
    • NVIDIA driver 396.54
    • CUDA 9.2 / cuDNN 7.1
    • GCC 7.3.0

となります。CPU がやや古めですが Pillow-SIMD を使うことで CPU が律速するという事態は回避できました。

Chainer v4.0.0 からは MultithreadIterator が追加されてデータの並列読み込みが簡単になったのですが、試してみたところ MultiprocessIterator でないと GPU がフルに動いてくれませんでした。プロセスの実行順序に依存しない augmentation を実現するためにデータセットの各要素ごとに疑似乱数生成器の内部状態を持たせる設計を採用し、実装にあたっては全ワーカープロセスからアクセスできる shared memory を用いました。また(ほとんど趣味ですが)疑似乱数アルゴリズムの勉強も兼ねて、メモリ消費の少ない xoshiro128+ 疑似乱数生成器を Python に移植して使うことでフットプリントを低減してみました。

学習結果

正規化のレイヤーを

  • Batch Normalization
  • Group Normalization (Chainer の実装)
  • Group Normalization(ALBERT の実装 1 つ目)
  • Group Normalization(ALBERT の実装 2 つ目、高速版)

のそれぞれに入れ替え、ResNet-50 の学習を行った結果を以下に示します。先述したように Group Normalization におけるグループ数 G は 32 に固定しています。検証は毎エポックごとに中心クロップ画像に対して行いました。

ロスおよび accuracy のグラフ

Batch Normalization の圧勝ですね……。学習率・weight decay・グループ数などの調整を行うと違った結果にはなるでしょう。

Group Normalization を使う 3 つのバージョンは実装の違いだけですので、当然ながらほぼ同じ結果になっています。ただ、ALBERT1 と ALBERT2 の実装は浮動小数点演算に関しては全く同一で、cuDNN による非決定性以外に違いの出る要因はないはずなのですが、にもかかわらず train accuracy にかなりの差が出たのは印象的でした。

次に速度の比較です。ここで挙げる実行時間は学習・検証全体で測定したものです。Normalization 単体のマイクロベンチマークをすればさらに差は開くでしょう。いずれにせよ Batch Normalization は cuDNN で実装されているので、勝つのはなかなか困難だと予想されます。

実行時間のグラフ

Batch Normalization では 52 分で終わった学習が、Chainer の Group Normalization 実装では 1.66 倍の 86 分かかってしまっていました。一方で ALBERT2 版では 1.06 倍と、かなり近い実行時間にまで最適化できています。

学習ログデータとプロットに用いたプログラムも GitHub に公開しています。

Chainer v5.0.0b3 の Group Normalization 実装ではどうして時間がかかっている?

Chainer v5.0.0b3 (v5.0.0b4 地点でも同じ) の Group Normalization のソースコードは、入力をうまく reshape することで Batch Normalization を行う関数を利用する巧妙な実装となっています。正規化処理本体は cuDNN などのバックエンドに備わった Batch Normalization ルーチンに任せてしまい、後は各チャンネルにスケール \gamma とバイアス \beta を適用するだけというわけです。Batch Normalization の呼び出しと \gamma によるスケーリングのそれぞれが backward のためのメモリを確保するためにメモリ消費が倍になる弱点はあるものの、一見したところ高速に動作しそうです。(なお適切な再計算をすればメモリ確保はほぼなくせますが、この記事では扱いません。既に詳しい実装論文が公開されています。)

そこで NVIDIA Visual Profiler で調べてみたところ、特別遅い箇所は確かに存在しないものの、GPU に発行される多数の細かい処理(CUDA カーネル)が積もり積もることで時間が消費されているとわかりました。さらに

nvidia-smi
コマンドで表示される “Volatile GPU-Util” 値が 100% にならないことから、GPU 内でのメモリアクセス以上に CUDA カーネル発行に伴う CPU / GPU 間通信がボトルネックになっているだろうと推測できました。

コードを見つつ、具体的に何回の CUDA カーネル発行が起こっているか確認してみましょう。

chainer/functions/normalization/group_normalization.py

    # By doing this reshaping, calling batch_normalization function becomes
    # equivalent to Group Normalization.
    # And redundant dimension is added in order to utilize ideep64/cuDNN.
    x = reshape.reshape(x, (1, batch_size * groups, -1, 1))

    with cuda.get_device_from_array(x.array):
        dummy_gamma = xp.ones(batch_size * groups).astype(xp.float32)
        dummy_beta = xp.zeros(batch_size * groups).astype(xp.float32)

    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        x = batch_normalization.batch_normalization(
            x, dummy_gamma, dummy_beta, eps=eps)

    x = reshape.reshape(x, original_shape)

    target_shape = [1, channels] + [1] * (x.ndim - 2)
    gamma_broadcast = broadcast.broadcast_to(
        reshape.reshape(gamma, target_shape), x.shape)
    beta_broadcast = broadcast.broadcast_to(
        reshape.reshape(beta, target_shape), x.shape)

    return x * gamma_broadcast + beta_broadcast

Forward 時、backward 時それぞれについて調べてみると以下の処理が GPU で行われていることがわかります。

  • Forward 時
    • 59行目:
      chainer.functions.reshape
      内部でのメモリコピー
    • 62行目:
      xp.ones
      によるメモリを 1 で埋める処理
    • 62行目:
      cupy.ndarray.astype
      による型変換
    • 63行目:
      xp.zeros
      によるメモリを 1 で埋める処理
    • 63行目:
      cupy.ndarray.astype
      による型変換
    • 67行目:
      chainer.functions.batch_normalization
      内部での、ダミーの rolling average を 0 で埋める処理
    • 67行目:
      chainer.functions.batch_normalization
      内部での、ダミーの rolling variance を 1 で埋める処理
    • 67行目: cuDNN の Batch Normalization foward 処理呼び出し
    • 70行目:
      chainer.functions.reshape
      内部でのメモリコピー
    • 74行目:
      chainer.functions.reshape
      内部でのメモリコピー
    • 73行目:
      chainer.functions.broadcast_to
      内部でのメモリコピー
    • 76行目:
      chainer.functions.reshape
      内部でのメモリコピー
    • 75行目:
      chainer.functions.broadcast_to
      内部でのメモリコピー
    • 78行目:
      *
      による乗算
    • 78行目:
      +
      による加算
  • Backward 時
    • 78行目:
      +
      の backprop に伴うメモリコピー(2 回)
    • 78行目:
      *
      の backprop に伴う乗算(2 回)
    • 75行目:
      chainer.functions.broadcast_to
      の backprop に伴う総和計算
    • 76行目:
      chainer.functions.reshape
      の backprop に伴うメモリコピー
    • 73行目:
      chainer.functions.broadcast_to
      の backprop に伴う総和計算
    • 74行目:
      chainer.functions.reshape
      の backprop に伴うメモリコピー
    • 70行目:
      chainer.functions.reshape
      の backprop に伴うメモリコピー
    • 67行目: cuDNN の Batch Normalization backward 処理呼び出し
    • 59行目:
      chainer.functions.reshape
      の backprop に伴うメモリコピー

どうしたって必要な cuDNN 呼び出し以外に、forward で 14 回、backward で 10 回の CUDA カーネル呼び出しが起こっていたのです。Chainer の自動微分は便利ですが細かいカーネルが増えがちです。特にネットワーク内部で何度も使われる関数を自動微分に頼って書くと、全体の実行時間に大きな影響を与えうることがわかります。

この実装の場合、cuDNN 呼び出し以外の本質的な処理は 78 行目だけですので、がんばって色々修正すれば forward / backward ともに CUDA カーネル 1 回ずつにまで低減することは可能です。深層学習フレームワークは急速に進歩していますので、近い将来には自力でがんばらなくても自動的に最適化されるようになるでしょう。

しかし、その方針ではどうしても cuDNN の Batch Normalization に勝てませんし、メモリ確保も減らせません。なので cuDNN を使わずフルスクラッチで実装することにしました。

Group Normalization の誤差逆伝搬

フルスクラッチで実装するなら、まずすべきことは微分です。入力となる多次元配列 x の shape を (N,C,H,W) であるとし、添字ベクトル i(i_N,i_C,i_H,i_W) によって表すことにします。チャンネル数 C がグループ数 G で割り切れる場合のみを考えることにして、グループあたりのチャンネル数 C/GC_G と表記します。さらに、チャンネル i_C が何番目のグループに属するかを i_G=\lfloor i_C/C_G \rfloor で表記します。

正規化済みデータ \hat{x} を平均 \mu と分散 \sigma^2 を用いて \hat{x}_i=(x_i-\mu_{i_N,i_G})/\sigma_{i_N,i_G} によって定義すると、出力 yy_i=\gamma_{i_C}\hat{x}_i+\beta_{i_C} となります。

まず \mu\sigma^2 でロス l を微分します。添字集合 S_{i_N,i_G}\{j\mid j_N=i_N,\; j_G=i_G\} によって定義しておきます。微分の結果を見ると、この添字集合 S_{i_N,i_G} ごとに総和計算が走ることがわかります。

\begin{aligned} \frac{\partial l}{\partial \hat{x}_i} &= \frac{\partial l}{\partial y_i}\gamma_{i_C}\,, \\ \frac{\partial l}{\partial \mu_{i_N,i_G}} &= \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial \hat{x}_i} \frac{\partial \hat{x}_i}{\partial \mu_{i_N,i_G}} = \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} (-\sigma_{i_N,i_G}^{-1}) \\ &= -\sigma_{i_N,i_G}^{-1} \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C}\,, \\ \frac{\partial l}{\partial \sigma_{i_N,i_G}^2} &= \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial \hat{x}_i} \frac{\partial \hat{x}_i}{\partial \sigma_{i_N,i_G}^2} = \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} \frac{-(x_i-\mu_{i_N,i_G})\sigma_{i_N,i_G}^{-3}}{2} \\ &= -\frac{\sigma_{i_N,i_G}^{-2}}{2} \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} \hat{x}_i\,. \end{aligned}

これらがわかれば入力 x に関する勾配を求めることができます。

\begin{aligned} \frac{\partial l}{\partial x_i} &= \frac{\partial l}{\partial y_i} \sigma_{i_N,i_G}^{-1}\gamma_{i_C} + \frac{\partial l}{\partial \mu_{i_N,i_G}} \frac{\partial \mu_{i_N,i_G}}{\partial x_i} + \frac{\partial l}{\partial \sigma_{i_N,i_G}^2} \frac{\partial \sigma_{i_N,i_G}^2}{\partial x_i} \\ &= \sigma_{i_N,i_G}^{-1} \frac{\partial l}{\partial y_i}\gamma_{i_C} - \sigma_{i_N,i_G}^{-1}\Big( \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} \Big) \frac{1}{C_GHW} \\ & \phantom{{}= \sigma_{i_N,i_G}^{-1} \frac{\partial l}{\partial y_i}\gamma_{i_C}}{} - \frac{\sigma_{i_N,i_G}^{-2}}{2}\Big( \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} \hat{x}_i \Big) \Big( \frac{2x_i}{C_GHW}-\frac{2\mu_{i_N,i_G}}{C_GHW} \Big) \\ &= \sigma_{i_N,i_G}^{-1}\Big( \frac{\partial l}{\partial y_i}\gamma_{i_C} - \Big( \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} \Big) / C_GHW \\ & \phantom{{}= \sigma_{i_N,i_G}^{-1}\enspace\, \frac{\partial l}{\partial y_i}\gamma_{i_C}}{} - \hat{x}_i\Big( \sum_{i\in S_{i_N,i_G}} \frac{\partial l}{\partial y_i}\gamma_{i_C} \hat{x}_i \Big) / C_GHW \Big)\,. \end{aligned}

スケール \gamma とバイアス \beta に関する勾配は単純です。こちらでは添字集合 S_{i_C}=\{j \mid j_C=i_C\} ごとに総和計算を走らせることになります。

\begin{aligned} \frac{\partial l}{\partial \gamma_{i_C}} &= \sum_{i\in S_{i_C}} \frac{\partial l}{\partial y_i}\hat{x}_i\,, \\ \frac{\partial l}{\partial \beta_{i_C}} &= \sum_{i\in S_{i_C}} \frac{\partial l}{\partial y_i}\,. \end{aligned}

Group Normalization ALBERT1 版の実装

CuPy では

compile_with_cache
関数を使うことで独自の CUDA カーネルを利用できます。総和計算を行う部分では自分で書いたカーネルを呼び出して、その他の部分は
chainer.backends.cuda.elementwise
関数を用いることにしました。(総和演算などは
chainer.backends.cuda.reduce
関数でも定義できますが、複数の値を同時に計算できないことなどから利用しませんでした。新規に書くなら Chainer v5.0.0b4 で追加された
chainer.backends.cuda.raw
のほうがよさそうです。)

Forward 時には、shared memory を用いて GPU のスレッド間で値を集約する標準的なアルゴリズムを用いて、まず各グループ S_{i_N,i_G} 内での総和 \sum_{i\in S_{i_N,i_G}}x_i および二乗和 \sum_{i\in S_{i_N,i_G}}x_i^2 を同時に求めました。そこから平均 \mu および標準偏差の逆数 \sigma^{-1} を計算できます。(分散 \sigma^2\Big(\sum_{i\in S_{i_N,i_G}}x_i^2\Big)/C_GHW-\mu^2+\varepsilon で求まることを用いています。)あとは普通に \hat{x}=(x-\mu)/\sigma および y=\gamma\hat{x}+\beta

elementwise
関数で求めれば OK です。Backward 時のために \hat{x}\sigma^{-1}を保持しておきます。

Backward の実装はより面倒です。誤差逆伝搬の式展開からわかるように S_{i_N,i_G}S_{i_C} の両方について総和計算が必要になるからです。一旦 HW 方向についての総和

\begin{aligned}A_{i_N,i_C}=\sum_{i\in S_{i_N,i_C}}\frac{\partial l}{\partial y_i}\,,\quad \hat{A}_{i_N,i_C}=\sum_{i\in S_{i_N,i_C}}\frac{\partial l}{\partial y_i}\hat{x}_i\,,\qquad\\ \text{where}\;S_{i_N,i_C}=\{j \mid j_N=i_N,\;j_C=i_C\}\end{aligned}

を求めておいて、そこから C_G 方向および N 方向の総和をそれぞれ取って

\begin{aligned} \frac{\partial l}{\partial x_i} &= \frac{\partial l}{\partial y_i} - \Big(\sum_{\lfloor j_C/C_G \rfloor=i_G}A_{i_N,j_C}\gamma_{j_C}\Big)/C_GHW - \hat{x}_i\Big(\sum_{\lfloor j_C/C_G \rfloor=i_G}\hat{A}_{i_N,j_C}\gamma_{j_C}\Big)/C_GHW,\\ \frac{\partial l}{\partial \gamma_{i_C}} &= \sum_{i_N<N}\hat{A}_{i_N,i_C}\,, \\ \frac{\partial l}{\partial \beta_{i_C}} &= \sum_{i_N<N}A_{i_N,i_C}\,. \end{aligned}

と計算するのがよさそうです。そのまま実装すると総和計算のために 3 回の CUDA カーネル呼び出しが必要になりますが、A_{i_N,i_C} および \hat{A}_{i_N,i_C} を求めるカーネル内に C_G 方向の総和を取る処理も詰め込んでしまうことで、カーネル呼び出しを 1 つ削減しています。

なお今回の実験では問題にならなかったものの、分散 \sigma^2 の計算は数値的に不安定です。このため double 型で総和を取ったり、オンライン / 並列アルゴリズムで残差二乗和を計算したり、平均と分散を 2 パスで計算したりして数値精度を改善することで、出力値を安定させられる可能性はあります。これらの計算方法については Algorithms for calculating variance が詳しいです。また総和計算の速度面では、shared memory を使わず shfl 命令を使ったり、段階的なカーネル実行によって並列度を上げたりといった最適化が考えられます。Faster Parallel Reductions on Kepler などを参照ください。

Group Normalization ALBERT2 版の実装

ALBERT1 版の実行時間は満足いくものではなかったので、また NVIDIA Visual Profiler のお世話になりました。どうやら forward / backward 時の

elementwise
関数が実行時間の 35% 程度を占めていたようです。
elementwise
関数内では単純な float 型の加減乗算しか行っておらず、メモリアクセスもそこまで多くはありません。これは異常事態です。

落とし穴はインデックスの計算にありました。Numpy の broadcasting rule にしたがえば y=(x-\mu)\sigma^{-1}\gamma+\beta を素直なソースコードで計算できるのですが、ここで x の shape は (N, G, C_G, HW)\mu, \sigma^{-1} の shape は (N, G, 1, 1)\gamma, \beta の shape は (1, G, C_G, HW) と、バラバラになっています。この場合に i 番目のスレッドがアクセスすべきメモリアドレスを求めるためには、整数除算を用いて i_{HW}=i % HW,\; i_{C_G}=i / HW % C_G,\; i_{G}=i / HW / C_G % G,\; i_{N}=i / HW / C_G / G というふうに各軸のインデックスを計算する必要があるのです。

整数除算は CPU や GPU にとって非常に時間のかかる処理です。なんとしても消し去らねばなりません。

現実的には多くの場合チャンネル数やバッチサイズは 2 の冪なので、除算をビットシフトに置き換えることができます。そうでなくとも、コンパイル時に除数がわかっているなら乗算とビットシフトの組み合わせなどに変形可能です。(“Hacker’s Delight” や日本語訳『ハッカーのたのしみ』の 10 章に詳しい説明があります。)

したがい、コードの中に

n_elems_per_channel
{}=HW,
n_channels_per_group
{}=C_G,
groups
{}=G を埋め込んでコンパイル時に除数がわかるようにすれば、コンパイラが行う最適化を利用し整数除算のコストをなくすことができます。この変更を加えたのが ALBERT2 版です。以下のように、1 次元のフラットな配列として
elementwise
関数にデータを与えて、自前でインデックスを計算することになります。

group_normalization_alb2_func.py

        gn_fwd_norm_code = string.Template('''
            unsigned int u = i;
            unsigned int tmp = u / ${n_elems_per_channel};
            unsigned int channel_idx = tmp % ${n_channels_per_group};
            tmp /= ${n_channels_per_group};
            unsigned int group_idx = tmp % ${groups};
            unsigned int batch_idx = tmp / ${groups};
            unsigned int group_norm_idx =
                batch_idx * ${groups} +
                group_idx;
            unsigned int batch_norm_idx =
                group_idx * ${n_channels_per_group} +
                channel_idx;
            T v_x_hat = (x[u] - mu[group_norm_idx]) * inv_std[group_norm_idx];
            x_hat[u] = v_x_hat;
            y[u] = v_x_hat * gamma[batch_norm_idx] + beta[batch_norm_idx];
        ''').substitute(n_elems_per_channel=n_elems_per_channel,
                        n_channels_per_group=n_channels_per_group,
                        groups=groups)
        gn_fwd_norm_name = 'gn_fwd_norm_{}_{}_{}'.format(
                    n_elems_per_channel, n_channels_per_group, groups)
        gn_fwd_norm_kern = cuda.elementwise(
            'raw T x, raw T mu, raw T inv_std, raw T gamma, raw T beta',
            'raw T x_hat, raw T y',
            gn_fwd_norm_code, gn_fwd_norm_name)

        x_hat, y = gn_fwd_norm_kern(x, mu, inv_std, gamma, beta,
                                    size=x.size)

この修正を加えることで、完璧とは言えないものの、メモリアクセス律速と思われる Batch Normalization に近い速度にまで持っていくことができました。CuPy 側でも軸のサイズが 2 の冪の場合の高速化の試みはありますが、執筆地点ではまだマージされていないようです。

なんにせよ、かなり強引な実装なので Chainer へのプルリクエストは現地点では考えていません。特に backward 側は勢いでベンチマークもせずに複雑な実装をしてしまった感は否めず、素直に総和計算を 3 回行う実装でもよかったかもしれないと感じています。CuPy 側の機能として

  • 複数の値を同時に計算する reduction
  • インデックス計算の自動的な最適化

がサポートされれば、かなりシンプルなコードに書き直せるんじゃないかなあと期待しているところです。

まとめ

  • Group Normalization はミニバッチの取り方に依存することによる Batch Normalization の難点を克服し、かつ既存手法の良いとこ取りをすることで精度的にも比較的優れている
  • しかし手元で Batch Normalization と同じ学習パラメータで実験したところ精度はかなり劣ったので、実戦投入する際には実データでの結果の確認やパラメータチューニングが必要になりそう
  • Chainer v5.0.0b4 地点での Group Normalization の実装は細かい CUDA カーネルの呼び出しが多数発生しているために非効率
  • フルスクラッチで Group Normalization を実装してみるとインデックス計算が律速するという落とし穴があり、そこを修正すると一気に高速化した

かなり高速化にこだわった内容でしたが、それが果たして重要な「イシュー」であるのか疑問を持たれた方もいるでしょう。しかしあくまで私見ですが、高速なプログラムは質のいいデータ分析を行うために極めて重要です。速ければ速いほど、様々な仮説を実験的に調べることでこれまで気づかなかった知見を見い出したり、細やかなパラメータチューニングによって精度を上げたりすることができるようになっていきます。ALBERT には最新の手法やオリジナルの手法を自ら Chainer などで実装できるアナリストが数多くいますが、やはり本格的な高速化となるとプログラマの領分になります。深層学習は今まさに発展著しい分野ですので、既存の高速なライブラリを使えないこともよくあります。

とはいえプログラマだけでデータ分析はできません。ビジネスでの実データの扱いや、データ分析手法の背景への深い理解など、学ぶべきことがたくさんあると痛感しています。深層学習ブーム以前からのベテランアナリストや、数学に強い理論派アナリスト、あるいはニューラルネットワークにいち早く目をつけて経験を蓄積した職人派アナリスト、さらにビジネスパートナーとのやり取りを円滑に調整してくださる営業やマネージャーなどなど、さまざまな人の力を借りて実践的なデータ分析に携わることができています。そもそもプログラマだとかアナリストだとか営業だとかの区別は曖昧で、個々人にそれぞれの強みがあるというべきなのでしょう。ALBERT は一緒に働くメンバーを募集しています!

私が携わっている ALBERT の画像分析支援サービスタクミノメ には Group Normalization がすでに組み込まれており、実際に分析業務で使われ始めています。顧客に最大の価値をもたらすこと・データ分析者にとっての使いやすさを大切にすること・シンプルかつ効率的な実装を心がけることで、ALBERT ならではの高水準なサービスを提供することができていると自負しております。

Viewing all 191 articles
Browse latest View live