simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Google Cloud Machine Learning Engine で分散ディープラーニング

■前置き
TensorFlow をクラウド環境で分散実行したい

公式のチュートリアルが分かりやすかったです。

本記事はGoogle Cloud ML って何?て状態の人が
そのチュートリアルにたどり着くまでの記録

■手順
参考サイト
www.topgate.co.jp

Google アカウントを作成
コンソールへ行く

Billing または お支払いから、プロジェクトに支払いアカウントを紐づける(今だと300ドルもらえるので、これを有効化するだけでいい)

Google の良いところは、プロジェクト単位で支払い方法を制御できる点とのこと
多数のプロジェクトを捌くエンジニアにとってうれしい

公式チュートリアル(英語)
Getting Started  |  Cloud ML Engine for TensorFlow  |  Google Cloud
1.ローカル単一学習
2.ローカル分散学習
3.クラウドストレージにバケット作成、学習データの配置
4.Cloud ML で単一学習
5.Cloud ML で分散学習
6.ハイパーパラメータ調整
7.Cloud ML 分散学習結果をモデルとして定義
8.モデルを使って単一データから予測
9.モデルを使って複数データからバッチ予測
10.クラウドストレージのバケットを削除

なぞるだけで TensorFlow を Cloud ML Engine で動かす操作が具体的にイメージできるようになります。

これができるようになってから
Cloud Machine Learning Engine のドキュメント  |  Cloud Machine Learning Engine(Cloud ML Engine)  |  Google Cloud

を読んでみることにしました。

…読みましたけど、あまり役立つ情報は書かれていなかった。南無…

TensorFlowの導入記録(エラー無かった)

■前置き
近頃はやりの Deep Learning を触ってみることにした。

Deep Learning はホントに流行っていて、覚えきれないくらいツールが溢れている(+_+)

未来があると思われる、Googleが開発したTensorflow(テンサーフロー)
の Hello, World なる MNIST の学習曲線を描いてみようと思います。

MNIST ってのは 0~9 の10個の手書き文字を識別する人工知能制作に挑む話
かなり最初の頃の Deep Learning の課題で、入力データとしてどの Deep Learning ツールも用意している。

詳しく知りたい人はこちらの論文どうぞ(そういえばどこぞの勉強会の前に読まされたな…)
http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf

Python 3 が必須

コマンドプロンプト (Win + cmd) を開いて、python とか py と打ち込めば、環境に python を入れていることがわかる

あ、入ってなかった。

python 3.6 入れます。
Anaconda for Windows のインストール

Anaconda prompt が入っているので、ここで python と打つと
Python 3.6.5 |Anaconda, Inc.|
と表示されればOK

■CUDA Toolkit 9.0 が必要

GeForce が刺さっているPCなので、ドライバを最新にしておく。
GeForce Game Ready Driver 397.93 入れた。

CUDA Toolkit 9.0 のインストーラを入手、インストール
必ず Visual Studio Integration に失敗するので、カスタムインストールで外す(なんで?失敗したやつだけスキップして進めなさいよ)

失敗を抜けて入った。(キレ気味)

■Tensorflow(テンサーフロー)のインストール

ここから公式ドキュメントなぞる
Installing TensorFlow on Windows  |  TensorFlow

最初からやっとけばよかった^^
Requirements to run TensorFlow with GPU support

環境変数の Path に CUDA 関係のことやるらしい。
CUDA_PATH も Path にも CUDA 9.0 のものがいっぱい入っていました。もうやらなくていい感じかな?

cuDNN は別途ダウンロードが必要とある、入れてみた。
これにはユーザー登録が必要…仕方ないので入れてみた。

cuDNN v7.1.4 Library for Windows 10 が CUDA 9.0 用の cuDNN っぽいね。
これを使うことにした。
Path も通してやった
C:\Program Files\NVIDIA GPU Computing Toolkit\cudnn-9.0-windows10-x64-v7.1\cuda\bin
こんなところに展開
確かに cudnn64_7.dll ファイルが存在している。

続いて pip ? を使う native で環境作るか、仮想環境作るか選ぶことになるらしい
既存の python 環境を壊すことあるけど、native ならどこのディレクトリからもコマンド呼べるのでいいよって勧めている

でも、Installing with Anaconda の方やってみることにする。

conda create -n tensorflow pip python=3.6
を実行
Solving environment: done と出た(^^

activate tensorflow

で(base)から(tensorflow)に切り替わることを確認
(tensorflow)C:> # Your prompt should change ドキュメントもそういっている。わかりずらい?

To install the GPU version of TensorFlow, enter the following command (on a single line):
ドキドキ、やってみます!

ゴリゴリテキストが進む、途中止まった?3分くらいでプロンプトが戻った。成功した模様\(^_^)/

Hello, TensorFlow!

できた、問題なくGPU版tensorflow動かせている
ここからチュートリアルを頑張れって導線が引かれた…やりますかぁ

AWSでWebサーバーを立てるには?

■前置き
2017/7/2 にこの記事の下書きをしていたのを見つけたので、書き上げてあげることにした。
のですが、色々勉強してついに完成せず、やっと次の記事のときに AWS で安全にアクセス制限した Web ページが完成(もー覚えること多すぎ)
simplestar-tech.hatenablog.com


■本題
こちらを参考になぞってみます。
qiita.com

■予備知識1
Amazon Virtual Private Cloud (Amazon VPC)

情報の通信量に応じてネットワークの構成を組み替えたいので、ネットワークも仮想化したいというのが Amazon の狙いだとか

詳しく知りたい人はこちら
docs.aws.amazon.com

■予備知識2
サブネット?

サブネットは、VPC の IP アドレスの範囲
お客さまのAWS リソースは、指定したサブネット内でしか起動することができません。

パブリック、プライベート があります。インターネットにつながるリソースにのみパブリックを使用しましょう。
サブネットの単位で、複数のセキュリティレイヤーを使用できます。
安全に作ってください。

詳しく知りたい人はこちら
docs.aws.amazon.com

1 つのサブネットが複数のVPC内のゾーンにまたがることはできません。
嬉しいことですよ、アベイラビリティゾーンとは、他のアベイラビリティーゾーンで発生した障害から切り離すために作られたので、個別のアベイラビリティーゾーンでインスタンスを起動することにより、障害が波及しません。

■予備知識3
Classless Inter-Domain Routing(CIDR、サイダー)

インターネット上のルーターにおけるルーティングテーブルの肥大化速度を低減させるための機構
Domain Name System (DNS) が考案されてから約10年間、IPアドレスをクラス分けして割り当て、ルーティングする方式はスケーラブルでないことが明らか
Classless Inter-Domain Routing(CIDR) は「可変長サブネットマスク (VLSM)」に基づき、IPアドレスのプレフィックスを可変長で割り当て可能にする。

■予備知識4
CIDRブロック

IPアドレスをグループ化して格納できるCIDR。
このようなアドレスのグループをCIDRブロックと呼び、IPアドレスを二進法で表したときの先頭の何ビットかが共通になっている。

■予備知識5
パブリック IPv4 アドレス

パブリック IPv4 アドレス (このトピックではパブリック IP アドレスと呼ばれる) を自動的に受信するかどうかの設定がすべてのサブネットに存在します。
有効になっているサブネットに対してインスタンスを起動すると、パブリック IP アドレスが割り当てられます。

サブネットのトラフィックがインターネットゲートウェイにルーティングされる場合、そのサブネットはパブリックサブネットと呼ばれます。
パブリックサブネット内のインスタンスIPv4 を介してインターネットと通信することが必要な場合は、そのインスタンスにパブリック IPv4 アドレスまたは Elastic IP アドレス (IPv4) が割り当てられている必要があります。

パブリック IPv4 アドレスの詳細を知りたい人はこちら
docs.aws.amazon.com

■予備知識6
パブリック IPアドレスは固定できない。

パブリック IP アドレスは、Amazon のパブリック IP アドレスプールにあるアドレスです。
パブリック IP アドレスとインスタンスとの関連付けを解除すると、そのアドレスは解放されてプールに戻り、それ以降お客様はそのアドレスを使用できなくなります。

固定パブリック IP アドレスをお客様のアカウントに割り当てる必要がある場合は、Elastic IP アドレスを使用します。

詳細については、「Elastic IP アドレス」を参照してください。
docs.aws.amazon.com

■メモ
dev.classmethod.jp

■最初にやること

クレジットカード番号と住所連絡先を使ってAWS のアカウントを作成し、案内に従って10分程度のチュートリアルをこなします。(日本語でチュートリアルできる)
aws.amazon.com

私が行ったチュートリアル
Linux 仮想マシンの起動→作った Linuxssh の認証システム使って GitBash から入れた→Linux インスタンスを terminate するところまで確認
Windows 仮想マシンの起動→Windows 仮想マシンを作成し、設定し、接続することができました。
・"Hello, World!" をサーバーレスで実行するAWS Lambda を使用します→最初の AWS Lambda 関数が作成されました。ここでは、サーバーのプロビジョニングや管理をせずにアプリケーションを実行する方法の最初のステップを学習しました。(外部から呼び出す方法までは追ってない)
・Docker コンテナのデプロイ→クラスターに乗ったサービスに乗ったタスクに乗ったコンテナが起動するところまで確認、しかし、DNSの代わりの名前で http アクセスするがそんなドメイン名は無いとさ、そりゃ時間かかるだろうけど、確認する前に消した。クラスターの消し方消し方がずいぶんチュートリアルと異なる。時間を要する

■必要になった知識1
各 Lambda 関数には、関連付けられた IAM ロール (実行ロール) があります。
なのでLambda 関数を作成するときに、その IAM ロールを指定します。

IAMロール?
詳細はこちら
docs.aws.amazon.com
IAM ロールは、AWS で許可/禁止する操作を決めるアクセス権限ポリシーが関連付けられている AWS ID であるという点で、ユーザーと似ています。ただし、ユーザーは 1 人の特定の人に一意に関連付けられますが、ロールはそれを必要とする任意の人が引き受けるようになっています。
ユーザーがロールを引き受けた場合、一時的セキュリティ認証情報が動的に作成され、ユーザーに提供されます。

ロールを使用して、通常は AWS リソースへのアクセス権のないユーザー、アプリケーション、サービスにそのアクセス権を委任できます。
たとえば、AWS アカウントのユーザーに、通常はないリソースへのアクセス権を付与したり、ある AWS アカウントのユーザーに、別のアカウントのリソースへのアクセス権を付与したりできます。

ロールに関連するアクセス権限を付与する必要があります。

テスト以外でLambda 関数を呼び出すには?

クライアントサイドが学ぶサーバーサイド技術

■前置き1

クライアントサイド、サーバーサイドを語る前に、まずはゲームが関数であることを説明したいと思います。
f:id:simplestar_tech:20180519185122j:plain

■クライアントサイド

クライアントサイドは、単一機材で処理が完結する関数を最適化するお仕事ととらえることができます。
f:id:simplestar_tech:20180520103945j:plain

次のステップを踏みながら関数の最適化を行います。

  1. ゲームプランの要求分析
  2. オブジェクト指向設計により不具合の特定が行いやすく、また複数人で分業しやすいスケルトン(骨組み)を作成
  3. アジャイルスクラムテスト駆動開発を例に、プロジェクト全員の進捗意識を統一しながらタスク管理
  4. 情報技術/数学/物理の知識を駆使し、ゲーム内の4次元の機能を1次元のプログラムコードに展開
  5. シナリオ、イラスト、音声、BGM、効果音など、ユーザーが直接観測するアート部分を量産・管理できる体制・ツールを作成
  6. ハードウェアとソフトウェアの知識を駆使したデータ指向設計を行い、機能ごとのパフォーマンスを最適化

図でいうところの真っ赤な吹き出しに注目してください。
このCPUとメモリの間の読み書きする部分がボトルネックになります。
機能を入れ終えた後は、最後にこのボトルネックの最適化を行ってパフォーマンス向上をはかります。

あえて GPU による描画処理を書きませんでした。
描画負荷が一番のボトルネックになるのは当たり前のことです。

■サーバーサイド

サーバーサイドは、サーバーにアクセスが集中する負荷の対策を行うお仕事ととらえました。

f:id:simplestar_tech:20180520210037j:plain

次のステップを踏んでコストパフォーマンスを計算しながら大量アクセス集中におけるサービス停止を未然に防ぎます。

  1. ゲームプランの要求分析
  2. 運用コストを計算
  3. 疎通確認済みのスケルトンを作成
  4. チート対策
  5. 負荷対策
  6. サービス運用

図でいうところの真っ赤な吹き出しに注目してください。
ボトルネックとなる箇所が三つあり、それぞれ
①ネットワーク帯域負荷
②ゲームロジックの計算負荷
③データベースアクセス負荷
となっています。

このうち①と②の対策は単純で、マシン性能の限界を数字で示してプランナーにゲームプランを見直しをお願いします。
ネットワーク帯域負荷を下げるには、通信する情報量を減らします。例えば無数に発生する雑魚キャラクターの位置の同期をやめるなど
ゲームロジックの計算負荷を下げるには、サーバーで行う計算量を減らします。例えば無数に発生する雑魚キャラクターの経路計算をサーバーではなくクライアントで行うようにするなど
ただし、これにはチート対策とトレードオフとなることがあるため慎重に決める必要があります。

残る③のデータベースアクセス負荷の低減がサーバーサイドの技術として注目に値しました。
やっと前置きが終わりました(相変わらず長い…)

■サーバーサイド技術とは
サーバーサイド技術とは、データベースアクセス負荷を分散するためのシステムを設計構築する技術です。

既存のゲームがどのようにして負荷を分散するシステムを実現したのか見ていきましょう。

パラレルワールド方式

データベースアクセス負荷を分散するためのシステムとして、パラレルワールド方式があります。
これはユーザー数が一つのデータベースアクセス負荷の限界を超えるところまで来たら、新しいデータベースサーバーを立てて、以降の新規ユーザーからは新しいサーバーを利用する方式です。

レプリケーション

データベースの複製を10個作っておき、処理をその10台のマシンに分散させれば、理論上、1台の時の最大10倍の量の処理を行うことができます。

■データベースの水平分割と垂直分割

データベースサーバー内には、当たり前ですがデータベースがあります。

データベースはテーブルと呼ばれる集合によって構成されています。
読み書きが集中するテーブルを分割して、別のデータベースサーバーを立てれば、磁気ディスクのヘッド数を増やしたことになるため、読み書きの負荷を分散することが可能です。

テーブルを複数のテーブルに分割する操作には、二通りが考えられます。

一つはテーブルの種類を増やす対応(ユーザー情報、所持アイテム情報、クエストログ、スキルセット情報、メール情報、チーム情報、といったデータごとにデータベースサーバーを立てる対応)これが垂直分割
一つは同じテーブルをユーザーIDごとに分割する対応(頭文字aのユーザーIDだけのテーブル、頭文字bのユーザーIDだけのテーブル、と分けて、それぞれにデータベースサーバを立てる対応、予想される負荷は1/26に減らせます。)これが水平分割
水平分割でデータベースサーバーを増やすことを DB sharding(シャーディング)と呼びます。

goでjson文字列からオブジェクトを生成

■前置き

Goで書いたWebAPIサーバが今 json 文字列を受け取っています。
json文字列に含まれる情報から必要な値をピックアップして、外部のグラフ表示ソフトが期待しているようなフォーマットでファイル出力しなければなりません。

さて、つまり json 文字列から go 環境にて、元のデータ構造を完全復元(または必要なフィールドのみ対応)して、独自フォーマットでファイル出力する必要があります。

汎用的な操作かつ、非常に需要があるケースなので、対応方法がすぐ見つかると思います。

■調査結果

はい、すぐに見つかりました。
JSONのパース/生成 - はじめてのGo言語

ここを参考にします。

■本題
前回の obj から生成した次の json 文字列をパースしてみましょう。
{"obj2":{"a":1,"b":2,"array":[{"test":"hoge","pageX":101,"pageY":102,"identifier":321},{"test":"hoge","pageX":201,"pageY":202,"identifier":987}]},"obj3":{"c":3,"d":4}}

go 側の実装は次の通り、対応するデータタイプに json タグを付けます。
ここで type struct で定義するフィールドは必ず大文字で始まらなければならない規則があることに注意してください。
必ずフィールド名は頭文字を大文字にする必要があることに注意してください。
フィールド名は頭文字を大文字に( ゚д゚)…もういいか

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

// Animal a
type Animal struct {
	Test       string `json:"test"`
	PageX      int    `json:"pageX"`
	pageY      int    `json:"pageY"`
	Identifier int    `json:"identifier"`
}

// OBJ2 b
type OBJ2 struct {
	A       int      `json:"a"`
	B       int      `json:"b"`
	Animals []Animal `json:"array"`
}

// OBJ3 c
type OBJ3 struct {
	C int `json:"c"`
	D int `json:"d"`
}

// OBJ a
type OBJ struct {
	Obj2 OBJ2 `json:"obj2"`
	Obj3 OBJ3 `json:"obj3"`
}

// handler impl
	r.ParseForm()
	form := r.PostForm
	output := fmt.Sprintf("%s", form["json"][0])
	
	jsonBytes := ([]byte)(output)
	postData := new(OBJ)
	if err := json.Unmarshal(jsonBytes, postData); err != nil {
		log.Fatal(err)
	}

	// access test
	fmt.Println("data a = " + strconv.Itoa(postData.Obj2.A))
	fmt.Println("data b = " + strconv.Itoa(postData.Obj2.B))
	fmt.Println("data c = " + strconv.Itoa(postData.Obj3.C))
	fmt.Println("data d = " + strconv.Itoa(postData.Obj3.D))
	for i, v := range postData.Obj2.Animals {
		fmt.Print(fmt.Sprintf("index:%d,value: %v\n", i, v))
	}

デバッグコンソールの出力は次の通りでした。

data a = 1
data b = 2
data c = 3
data d = 4
index:0,value: {hoge 101 0 321}
index:1,value: {hoge 201 0 987}

goでjson文字列からオブジェクトを生成できました。
過去の一連の記事を読んだ方なら、今の私と一緒で
WebページにおけるユーザアクティビティをWebAPIサーバに記録できる能力を持ったことになりますw。

Webページからオブジェクトをjson文字列に変換してPOST

■前置き
Webページから文字列をPOSTする際、クエリストリングのように連想配列を文字列で送信できることは確認済み
さて、ユーザーアクティビティってのはそう単純なデータ構造じゃないんです。
ユーザ定義のデータ構造の配列の配列なんてこともあって、これをPOSTできるんでしょうか?

普通、こうした複雑なデータ構造は一度形式だった文字列に変換するということが、Webの通信では通例なようで
jsonフォーマットの文字列というものがあります。例えば次のような文字列

{
    "id": "0000",
    "str": "hello",
    "CustomType": [
        {
            "you": "are",
            "c": "t"
        },
        {
            "you": is,
            "c": "hoge"
        }
    ]
}

javascript ではオブジェクトから json 文字列にシリアライズしたり
json文字列からデシリアライズしたりするんじゃないですかね?

やったことないですが、推理です。
json文字列へのシリアライズ、デシリアライズについて javascript の書式を知りたいです。

■本題

あった

JavaScript 値を JSON (JavaScript Object Notation) 文字列に変換
JSON.stringify 関数 (JavaScript)

JSON (JavaScript Object Notation) 文字列をオブジェクトに変換
JSON.parse 関数 (JavaScript)

POSTで送信したい情報があったら、次のようにすることで json 文字列を送信することを確認しました。

            let jsonStr = JSON.stringify(obj);
            xhr.send(jsonStr);

試しに、適当に複雑化したオブジェクトを JSON.stringify に突っ込んでみて、どのような json 文字列が作られるのか見てみました。

        var obj = {
            obj2: {
                a : 1,
                b : 2,
                array : Array
            },
            obj3: {
                c : 3,
                d : 4
            }
        };
        var Animal = (function () {
            function Animal() {
                this.test = "hoge";
            }
            this.test = "test";
            this.pageX = 0;
            this.pageY = 0;
            this.identifier = 0;
            return Animal;
        })();

            let touchList = new Array();
            let touch = new Animal();
            touch.pageX = 101;
            touch.pageY = 102;
            touch.identifier = 321;
            touchList.push(touch);
            let touch2 = new Animal();
            touch2.pageX = 201;
            touch2.pageY = 202;
            touch2.identifier = 987;
            touchList.push(touch2);
            obj.obj2.array = touchList;

            let jsonStr = JSON.stringify(obj);
            console.log(jsonStr);

            xhr.send(jsonStr);

JavaScript 側でコンソール出力を見てみると次の json 文字列を取得し
{"obj2":{"a":1,"b":2,"array":[{"test":"hoge","pageX":101,"pageY":102,"identifier":321},{"test":"hoge","pageX":201,"pageY":202,"identifier":987}]},"obj3":{"c":3,"d":4}}

goで書いたAPIサーバにて受け取った文字列を見てみると次の通りでした。
{"obj2":{"a":1,"b":2,"array":[{"test":"hoge","pageX":101,"pageY":102,"identifier":321},{"test":"hoge","pageX":201,"pageY":202,"identifier":987}]},"obj3":{"c":3,"d":4}}

まったく一緒です。

なお見通しがきくように javascript 側と go サーバ側を次のように書き換えてみました。

            let jsonStr = JSON.stringify(obj);
            var params = "json=" + jsonStr;
            xhr.send(params);
	form := r.PostForm
	output := fmt.Sprintf("%s", form["json"][0])
	file.Write(([]byte)(output))

ファイルには json 文字列が間違いなく出力されていました。

これにてWebページからオブジェクトをjson文字列に変換してPOST成功を確認

Goでローカルファイルにテキストファイルを書き出す

■前置き
WebページをユーザのクライアントマシンからWebブラウザで見てもらい
ユーザアクティビティを短い文字列情報でPOSTしてもらうことで、ユーザ端末で何が起きたのかを収集します。

ログを出すだけじゃ意味がなく、受け取った情報をファイルに保存しなければなりません。
POSTメッセージを捌く go の HTTP サーバを別ポートで起動し、疎通確認を行ったのが前回
今回は、受け取ったメッセージをファイルに保存してみせます。

■調査
go でファイルを操作する時にimport するパッケージは?
osパッケージを利用するのが基本とのことです。
参考
ファイル入出力 - はじめてのGo言語

前回からの差分としては以下のコードを追加しました

import (
	"os"
)

	file, err := os.Create("osfile.txt")
	if err != nil {
		fmt.Printf("os.Open err = %v\n", err)
		return
	}
	defer file.Close()
	output := fmt.Sprintf("%v", form)
	file.Write(([]byte)(output))

HTTPメッセージPOSTを受け取ったらそのままファイル出力するだけのコードは以下の通り(動作確認済み)
送られてくるたびにファイル名が書き換わる便利機能を追加しておきました。

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
)

func apiname1(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		fmt.Fprintf(w, "this is apiname1")
		return
	}
	w.Header().Set("Content-Type", "text/plain")
	r.ParseForm()
	form := r.PostForm
	// params := r.Form

	layout := "2006_01_02_15_04_05"
	fname := time.Now().Format(layout) + ".txt"
	file, err := os.Create(fname)
	if err != nil {
		fmt.Printf("os.Open err = %v\n", err)
		return
	}
	defer file.Close()
	output := fmt.Sprintf("%v", form)
	file.Write(([]byte)(output))
}

func main() {
	http.HandleFunc("/apiname1", apiname1)
	http.ListenAndServe(":8080", nil)
}

日付のフォーマットがなかなか考えられていて、こういう風に書きたいってレイアウトを書くとその通りに出してくれる。
それぞれの数字がユニークで、時間や分を意味している。
一覧はここで確認する。

クライアントのブラウザの操作から Webサーバ側でファイルを書き出せたのでよしとします。
なお、出力フォルダは go プログラムをルートとしたところから指定できました。