AWSのEC2上でblenderを動作させてNice DCVを使ってリモート接続する方法
はじめに
blenderなどの3Dアプリケーションを利用する場合、そこそこのマシンスペックが必要になります。
私は、Mac Book(ノートPC)を愛用しているのですが、少々厳しい(デュアルコアだし、GPUも無い)です。
そこで、AWS上のEC2でblenderを動作させ、Nice DCVで接続することにします。
blenderの公式サイトを参考にすると、2022年10月現在のオススメスペックは以下の通りです。
- 64-bit eight core CPU
- 32 GB RAM
- 2560×1440 display
- Three button mouse or pen+tablet
- Graphics card with 8 GB RAM
上記を満たすには、g4dn.2xlarge (8コア、32GbyteのRAM) 程度がちょうど良さそうですね。
設定手順
今回は、手元のmacから接続します。EC2インスタンスは、LinuxとWindowsが選択できますが、Linux側を選択します。
(ここは、慣れている方で良いと思います。)
ざっくりとは、以下の手順です。
- blenderとNice DCV設定済みのAMIからEC2インスタンスを起動
- ユーザ作成 (Nice DCVで接続した時のログイン用兼作業用)
- Nice DCVの設定ファイルの書き換え
- クライアントのインストール(既にインストール済みでなければ)
EC2インスタンスの起動
- AMIは、「Nimble Studio Linux Workstation AMI」を選択します。
- EC2インスタンスでは、Nice DCVで利用する8443ポートを空けておきます。
- EC2のIAMRoleを設定します。
- 以下の通り、Nice DCVのライセンスを取得できる様に設定します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::dcv-license.ap-northeast-1/*" } ] }
ユーザの作成
起動したEC2インスタンスにログインします(以下、SSHでログインしている前提)。
centosなので、amazon linuxのデフォルトユーザec2-userではなく、centosユーザでのログインとなります。
(1)ユーザを作成します。
sudo adduser [USER_NAME]
(2)パスワードを変更します。
sudo passwd [USER_NAME]
(3)wheelグループへ追加します。
sudo usermod -aG wheel [USER_NAME]
Nice DCVの設定
nice DCVの設定で、作成したユーザでのログインを許可します。
まずは、permissionファイルの作成します。
作成するpermissionファイル( /etc/dcv/permissions.conf としました)の内容は、以下の通りです。
[permissions] [USER_NAME] allow builtin %owner% allow builtin
※[USER_NAME]は、上で作成したユーザ名に置き換えてください。
次に、そのファイルを、設定ファイル (/etc/dcv/dcv.conf)で指定します。
[session-management/automatic-console-session] permissions-file='/etc/dcv/permissions.conf'
後は、dcvserverを再起動して、上の設定を読み込みます。
sudo systemctl restart dcvserver
Nice DCVクライアントのインストール
以下から、ご自分の端末に合うアプリをインストールしてください。
docs.aws.amazon.com
後は、Nice DCVクライアントを起動して、接続します。
ちなみに、グローバルIPを調べたい場合は、以下が便利です。
curl ifconfig.io
SSHのProxyCommandを利用してEC2インスタンスを自動で起動する設定
毎回、EC2インスタンスの起動・終了を実施するのは面倒です。
そこで、SSHのProxyCommandとSSMエージェントを利用し、sshコマンドを実行すると、自動でEC2が起動する様にします。
また、cron コマンドを利用して、一定時間操作がない場合は、自動で終了する様に設定します。
EC2インスタンスのIAM Roleの設定
- AWSにて、あらかじめ用意されている「AmazonSSMManagedInstanceCore」を追加します。
- これで、SSMが利用できる様になります。
- 注:SSMのエージェントは、上で選択したAMIでは、既にインストール済みのため、特段することはないですが、もしSSMエージェントがない場合は、追加します。
IAMユーザのプロファイルのポリシー
接続にいくユーザの権限として、以下を付与します。
インスタンスの起動や終了、ssm周りの権限を付与しています。
とりあえず、グローバルIPで絞っています。ころころ変わって面倒な場合は、MFA認証を必須にするなどでも良いと思います。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": "ec2:StartInstances", "Resource": [ "arn:aws:ec2:ap-northeast-1:[AWS アカウントのID]:instance/[EC2インスタンスのID]", ], "Condition": { "IpAddress": { "aws:SourceIp": "[接続元のIPアドレス]" #ここは必要に応じて } } }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": "ec2:DescribeInstances", "Resource": "*", "Condition": { "IpAddress": { "aws:SourceIp": "[接続元のIPアドレス]" } } }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": "ec2:StopInstances", "Resource": [ "arn:aws:ec2:ap-northeast-1:[AWSアカウントのID]:instance/[EC2インスタンスのID]", ] }, { "Sid": "VisualEditor3", "Effect": "Allow", "Action": "ssm:StartSession", "Resource": [ "arn:aws:ec2:ap-northeast-1:[AWSアカウントのID]:instance/[EC2インスタンスのID]", "arn:aws:ssm:ap-northeast-1::document/AWS-StartSSHSession" ] }, { "Effect": "Allow", "Action": [ "ssm:TerminateSession", "ssm:ResumeSession" ], "Resource": [ "arn:aws:ssm:*:*:session/${aws:username}-*" ] } ] }
SSH ProxyCommandの設定
インスタンス起動のスクリプトの準備
まずは、インスタンスを起動するためのスクリプトを用意します。
ここでは、IAMユーザのプロファイルを「blender」としています。ここは、前の章のポリシーが付いていれば何でも良いです。
渡されたIDのインスタンスが「stopped」状態であれば、起動する様にしています。
(10秒待っているのは、wait_until_running関数だけだと、初回の接続に失敗することがあったためです。)
import time import boto3 import argparse class Agent(): def __init__(self,host): self._TARGET_INSTANCE_ID = host self._PROFILE_NAME = "blender" self._REGION_NAME = "ap-northeast-1" self._session = boto3.Session(profile_name=self._PROFILE_NAME) self._ec2 = self._session.resource("ec2",region_name=self._REGION_NAME) def start_instance(self): target_instance = self._ec2.Instance(self._TARGET_INSTANCE_ID) status = target_instance.state["Name"] if status == "running": return True elif status == "stopped": target_instance.start() target_instance.wait_until_running() time.sleep(10) target_instance.reload() return target_instance.state["Name"] == "running" else: return False def stop_instance(self): target_instance = self._ec2.Instance(self._TARGET_INSTANCE_ID) target_instance.stop() target_instance.wait_until_stopped() target_instance.reload() return target_instance.state["Name"] == "stopped" def setup_arg_parser(): parser = argparse.ArgumentParser(description="start ec2") parser.add_argument("--host",help="EC2 instance ID",required=True) return parser def main(): parser = setup_arg_parser() args = parser.parse_args() agent = Agent(host=args.host) if agent.start_instance(): print("EC2 instance is running") else: agent.stop_instance() print("Failed to start the instance") if __name__ == "__main__": main()
上のpythonスクリプトは、setup_blender.pyとして保存しました。
次に、bashコマンドで、ラップしておきます。
pythonの環境をvirtualenvで設定しているので、環境を呼び出してから、pythonファイルを実行しているだけです。
#!/bin/bash HOST=$1 source [virtualenvを設定したパス]/bin/activate python [pythonファイルを置いたパス]/setup_blender.py --host $HOST
.ssh/configの設定
HOST blender_ec2 HostName [EC2インスタンスのID] User centos Port 22 IdentityFile ~/.ssh/[SSHのキー※インスタンス作成時に設定したもの] ProxyCommand sh -c "[パス]/setup.sh %h; aws ssm start-session --profile blender --region ap-northeast-1 --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
※blenderと言うIAMユーザのプロファイルを作っています。
上記を全て設定すると、以下を実行するだけで、EC2インスタンスの起動を自動で実施してくれます。
ssh blender_ec2
一定時間操作がない場合、cronによりEC2インスタンスを自動で終了する
以下の、シャットダウンスクリプトを、cronで、5分置きに実行する様にしました。
動きとしては、5分して、ローカルからのsshセッションが1本も無ければ・・・
まずは、フラグを立て(/tmpにファイルを作成し)ます。
再び5分して、フラグが立った状態で、なおsshセッションが1本も無ければインスタンスを落とします。
なので、少なくとも5分は待ってくれて、最長でも10分以内に落としてくれる動きを想定しています。
#/bin/bash SESSION_COUNT=`w -h | grep localhost | wc -l` THERE_IS_NO_SESSION=/tmp/there_is_no_session if [ $SESSION_COUNT -gt 0 ] then rm -rf $THERE_IS_NO_SESSION elif [ -e $THERE_IS_NO_SESSION ] then rm -rf $THERE_IS_NO_SESSION sudo shutdown -h now else touch $THERE_IS_NO_SESSION fi
これを、cronに設定します。
*/5 * * * * /usr/local/bin/shutdown.sh
(/usr/local/bin/にshutdown.shとして置いています。)
AWS SESを使って独自ドメインのメールアドレスの受信と転送設定を行う
はじめに
(システムのテストなどで)メールアドレスが大量に必要になった時に、AWS SESは1つの解になるかも知れません。
ドメインの取得
- freenomなどで取得しても良いですし、Route53で直接取得しても良いです。
- 既に、Route53を使って、ドメインは取得(設定)済みとして進めます。
- 今回は、example.comと言うドメインを取得したことにします。
S3のバケットの取得
SESの設定からもバケットは作成することができますが、今回は予め作成しておきます。
- S3のコンソールから、「バケットを作成」を押して、好きな名前をつけます。
- SESのリージョンとS3のリージョンを一致させる必要はありません。
- SESは、受信機能の制限によりオレゴンを選択していますが、S3は、東京リージョンを選択しました。
- 作成したS3のバケットには、以下のバケットポリシーを設定しておきます。
- 自分のアカウントのSESからのputObject操作を受けつけると言う意味です。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowSESPuts-1660060428208", "Effect": "Allow", "Principal": { "Service": "ses.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::[ここにバケット名]/*", "Condition": { "StringEquals": { "AWS:SourceAccount": "[ここにアカウントの番号]" }, "StringLike": { "AWS:SourceArn": "arn:aws:ses:*" } } } ] }
AWS SESの設定
(注意) メールを受信できるリージョンは、限られています。残念ながら22年8月12日現在では、東京リージョンは対象外でした。
詳細は、以下の「E メールの受信」の章をご確認ください。
docs.aws.amazon.com
リージョン選択
今回は、オレゴンで設定していきます。
- 右上のリージョン選択で、米国西部(オレゴン)を選びます。
- 左の「Configuration」メニューに「Email receiving」のメニューがあることを確認しておきます。
受信ルールの設定
次に、受信ルールを設定します。
- 左の「Configuration」メニューから「Email receiving」を選択します。
- リージョンが非対応の場合は、表示されていません。
- 「Create rule set」ボタンを押します。
- 「Create rule」 ボタンを押します。
- Rule nameは、なんでも良いので、自分にとってわかりやすい名前をつけて、「Next」ボタンを押します。
- 「Add new recipient condition」ボタンを押して、利用したいメールアドレスを入力します。
- ドメイン部分は、example.comを使うので、例えば、「hogehoge@example.com」などとします。
- 次ページでは、「Add new action」から「Deliver to S3 bucket」を選択します。
- S3 bucketに上で、バケットポリシーを設定したバケットを選択します。(バケットポリシーが間違っているとここで警告を受けます。)
- prefixは、お好みで。
- 次ページで、最終確認をして、画面下部の「Create rule」ボタンを押してルールを作成します。
ルールを作った後は、activateします。
- 先ほど作成したルールを選択して、「Set as active」ボタンを押します。
MXレコードの設定
- Route53のMXレコードに、以下のリンクで指定されているメールサーバを指定します。
(受信の場合、CUSTOM MAIL FROMは特に設定する必要はありません。送信の場合でも、MAIL FROMが気にならないのであれば、設定する必要はありません。)
転送設定
毎回、直接s3のファイルを読みに行くのは面倒なので、転送設定をしておきます。
Lambda関数の作成
- SESの受信設定を行なったのと同じリージョンで、Lambda関数を作成します。
Lambda関数の設定ロール
最低限、以下の権限を付与したロールを作成します。
- S3のオブジェクトの読み込み (今回、SESで受信したメールをS3へ書き込むので、読み出す権限が必要になる)
- SESを用いた送信 (メールを転送するためです)
具体的には、以下の様に設定します。
S3のポリシー
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::[バケット名]/[プレフィックス]/*" } ] }
SESのポリシー
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "ses:SendRawEmail", "ses:SendEmail" ], "Resource": "arn:aws:ses:us-west-2:[アカウントID]:identity/*" } ] }
※ses:SendRawEmailの方は、使わなければはずしてください(以下のlambda関数では使っていません。)
Lambda関数の内容
ざっくり以下の内容です。
「バケット名」「プレフィックス」「転送先のメールアドレス」となっている部分は、ご自身の内容に置き換えてください。
gmailへ転送してうまくいくことは確認しました。
lambda関数にライブラリを追加しなくて済む様に、デフォルトで入っているemailライブラリを使っています。
import json import boto3 from botocore.exceptions import ClientError from email import policy from email.parser import BytesParser def parse_eml(eml_data): try : msg = BytesParser(policy=policy.default).parsebytes(eml_data) charset = msg.get_body(preferencelist=("plain","html"))["content-type"].params["charset"] # parse text body text_content,text_charset = None,None text_body = msg.get_body(preferencelist=("plain")) if text_body is not None: text_content = text_body.get_content() text_charset = text_body["content-type"].params["charset"] # parse html body html_content,html_charset = None,None html_body = msg.get_body(preferencelist=("html")) if html_body is not None: html_content = html_body.get_content() html_charset = html_body["content-type"].params["charset"] return { "to" : msg["to"], "subject" : msg["subject"], "charset" : charset, "text_body" : {"Data":text_content,"Charset":text_charset} if text_body is not None else None, "html_body" : {"Data":html_content,"Charset":html_charset} if html_body is not None else None } # TODO : handle error except Exception as e: return None def build_body(parsed_data): body = { "Text" : parsed_data["text_body"], "Html" : parsed_data["html_body"] } if parsed_data["text_body"] is None: del body["Text"] if parsed_data["html_body"] is None: del body["Html"] return body def lambda_handler(event, context): # S3 params BUCKET_NAME = "バケット名" PREFIX = "プレフィックス" # SES params RECIPIENT = "転送先のメールアドレス" ERROR_CODE = { 'statusCode' : 500, 'body' : json.dumps('Internal Server Error') } try : s3_client = boto3.client("s3") ses_client = boto3.client("ses") for item in event["Records"]: source = item["ses"]["mail"]["source"] message_id = item["ses"]["mail"]["messageId"] s3_key = f"{PREFIX}/{message_id}" s3_response = s3_client.get_object( Bucket = BUCKET_NAME, Key = s3_key ) eml_data = s3_response["Body"].read() parsed_data = parse_eml(eml_data) ses_client.send_email( Source = parsed_data["to"], Destination = { "ToAddresses":[RECIPIENT] }, Message={ 'Subject': { 'Data': f'Fw: {parsed_data["subject"]}', 'Charset': parsed_data["charset"] }, 'Body': build_body(parsed_data) } ) break # TODO : Handle Error except KeyError: return ERROR_CODE except ClientError: return ERROR_CODE else: return { 'statusCode': 200, 'body': json.dumps('OK') }
SESからのLambda関数の呼び出し
Raspberry Pi4でpythonの環境を作ってOpenCVをインストールするまで
Raspberry Piの購入
MacBook(USB-C対応)で作業する前提で記載します。
この場合、必要な物品は以下の4点です。
- Raspberry Pi (本体)
- SDカード
- SDカードライター
- USB-Cのケーブル
(最近は、Macを買うと電源などでUSB-Cのケーブルが付いていることが多いので周りを見渡せばあるでしょう)
(Raspberry Piは、USBで結線すれば、給電およびsshのログインが可能です。)
ですので、Raspberry Pi用のケースや電源ケーブルは、お好みで大丈夫です。
Raspberry Piの本体は、amazonなどでも売っていますが、今回は正規代理店のKSY経由で、
Pi4のメモリが8Gbyte版を購入しました。
SDカードの選び方は、以下のブログが詳しいです。
blog.soracom.com
Raspberry Piのセットアップ
以下がスタートページで、ここからRaspberry Pi Imagerをダウンロードします。
www.raspberrypi.org
32bit OSが安定版ですが、8Gbyte版を買った身としては、32bitだとメモリが勿体無いことになるので、以下の64bit版をインストールします(今回は、GUIが要らないのでLiteを選択しました)。
www.raspberrypi.org
ダウンロードしたImagerを開き、「Use custom img」から上でダウンロードした64bit版のRaspberry Piを指定して書き込みます。
Raspberry PiとMacの接続
基本設定
SSHでログインするために、Raspberry PiのSSHサーバを有効にします。
先ほどRaspberry Pi OSを書き込んだSDカードが、/Volumes/boot配下にmountされているとすると、以下のコマンドで空のファイルを作成します。
touch /Volumes/boot/ssh
WiFiに接続するのであれば、WiFiの設定をして終わりです。
しかし、今回はMacからUSB-Cで有線接続するため、以下の設定(1)(2)を行います。
(1)/Volumes/boot直下のconfig.txtの最後に、以下を追記します。
dtoverlay=dwc2
(2)/Volumes/boot直下のcmdline.txt の途中(rootwaitとquietの間)に以下を追記します。
modules-load=dwc2,g_ether
パスワードログインの廃止と証明書ログイン
初期設定では、以下でコマンドの後(ユーザ名が「pi」、ホストがraspberrypi.local)、
パスワードとして、「raspberry」を入れるとログインできます。
ssh pi@raspberrypi.local
この状態だと、raspberrypiにパケットが届く人が全員ログインできてしまうので、証明書を使った認証に切り替えます。
(隣の席に悪戯っ子が座っていると、slコマンドを入れられてしまったりします)
(1)証明書を作成する
以下のコマンドで、コマンドを実行した直下に、秘密鍵(raspi01)と公開鍵(raspi01.pub)が作成されます。
※fオプションを外すと、デフォルトの位置(/Users/ユーザ名/.ssh 配下)に作成してくれます。
※passphraseをなしにする場合は、2回enterキーを叩きます。
(2)秘密鍵を配置する
以下で、所定のディレクトリへ移動します。
mv raspi01 raspi01.pub ~/.ssh
※公開鍵を移動させなくても動きますが、無くすと面倒なので忘れない所に保存しておいた方が良いと思います。
秘密鍵の権限を自分だけが読み取れる様に書き換えます。
chmod 600 ~/.ssh/raspi01
(3)公開鍵を配置する
以下のコマンドで、Raspberry Pi側へ公開鍵を配置します。
(パスワードは、変えていなければ「raspberry」です。)
(4)証明書でログインできることを確認する
以下でログインできれば、問題なく設定できています。
(5)パスワードのログインを無効化する。
(Raspberry Piにログインして)/etc/ssh/sshd_config に以下を追記します。
※ 既に、該当行がコメントされているはずなので、以下の様に書き換えます。
PasswordAuthentication no
普通に開くと、read onlyなのでsudoで実行します。
その後、Raspberry Piを再起動します。
sudo reboot
(6)パスワードでログイン出来なくなっていることを確認する。
ssh pi@raspberrypi.local
だと、こんな感じに弾かれる様になりました。
pi@raspberrypi.local: Permission denied (publickey).
※外部に公開する時は、ちゃんとセキュリティを固めましょう。あくまで隣人対策です。
Raspberry Piからの(Macを介した)インターネット接続
(Macを介してインターネットに接続します。)
システム環境設定 > 共有 > インターネット共有 を選び
「RNDIS/Ethernet Gadget」にチェックボックスを入れます。
Python環境の構築
Python環境の構築前に、ラズパイ自体をアップデートします。
sudo apt update
sudo apt upgrade -y
sudo reboot
pythonは、既に入っているはずなので、以下で、pipだけインストール/アップデートします。
sudo apt install python3-pip -y
pip3 install --upgrade pip
jupyterのインストール
pip3 install jupyter
ただ、このままだと、Macからラズパイ上で動いているJupyter-notebookに接続できません。
そこで、以下のコマンドで、configファイルを作成します。
jupyter notebook --generate-config
~/..jupyter/jupyter_notebook_config.py の以下の箇所で、リモートのIPを許可します。
## The IP address the notebook server will listen on. # Default: 'localhost' c.NotebookApp.ip = '*'
後は、いつものコマンドでjupyterを起動します。
jupyter-notebook [jupyterで使うディレクトリ]
virtualenvの設定
virtualenvの環境を構築し、プロジェクトごとにpythonのライブラリの依存関係が混らない様にします。\
pip3 install virtualenv
(sudo reboot) # 必要に応じてリブート
※同じものを指しているので上はpipでも可 (whichコマンドで調べてください)
例えば、PoseNet用の環境だと、以下の様な感じです。(例は、「pose_net」名前で作っています)
virtualenv pose_net
source pose_net/bin/activate
※環境外に出る場合は、deactivateです。
OpenCVのインストール
OpenCVのインストールは、以下が詳しいです。
64bit版なので、こちらを参照します。
qengineering.eu
ドキュメントの前の方は、設定の確認になります。
当該のスクリプト(のDLと実行方法)は、「Installation script」の章にありますので、そちらを実施します。
(注意:当該スクリプトの実行には、1時間以上かかりました)
32bit版の場合は、以下です(こちらは、以前、別の32bit版のラズパイで試して動きました)。
qengineering.eu
python(のインタプリタ)を起動して、OpevCVがimportできれば大丈夫です。
>>> import cv2
以上で、Macからラズパイ上のJupyterに接続して、画像解析ができる様になりました。
Raspberry Piの終了
SSHで接続した状態で以下のコマンドで終了
> sudo shutdown -h now
SSHの小技集(ログインからポートフォワード、Pythonを利用した自動化まで)
はじめに
MacBook Proとモバイル回線で作業をする様な場合に良く利用するSSHの使い方です。また、サーバー側は、awsの利用を想定しています。
設定編
モバイル回線なので、デフォルトの設定だと頻繁に接続が切れます。
そんな時は、~/.ssh/configに以下の設定を記載します。
これで10秒ごとにパケットを流して接続を維持しようとしてくれます。
(勿論、切られることもあるので、そんな時はscreenなりを活用します。)
ServerAliveInterval 10 TCPKeepAlive yes
基本
ログインしたいサーバが、インターネットに公開されている場合は、以下のように単発のコマンドでログインします。
(証明書での認証を想定しています。)
ssh -i ~/.ssh/<i>foo.pem</i> <i>user</i>@<i>hostname</i>
図にすると以下の様になります。
踏み台の先にあるサーバにログインする
ログインして作業したいサーバが踏み台の先にある場合、一旦、踏み台にログインした後、さらに目的のサーバにログインするのは、面倒なことがあります。そういう場合は、以下の様に
~/.ssh/configファイルに記載しておくことで、自動化されます。
具体的には、ssh target_server とコマンドを打つだけで、自動でJUMP_SERVERを経由してtarget_serverに接続されます。
HOST [target_server] HostName [IP address or host name (1)] User [user name(1)] ProxyCommand ssh -W %h:%p JUMP_SERVER IdentityFile ~/.ssh/foo.pem HOST JUMP_SERVER HostName [IP address or hostname (2)] User [user name(2)] IdentityFile ~/.ssh/bar.pem
※target_serverは、自分の覚えやすい名前に変更してください
※[IP address or host name(1)] は、target_serverのものです。
※[IP address or host name(2)]は、踏み台サーバ(jump server)のものです。
※JUMP_SERVERの所は、字面が一致していれば、なんでも良いです。
踏み台だけをパブリックサブネットに置いて、プライベートサブネット内のワーカーにログインするのは、良く利用するパターンなので、上記の設定で2度ログインする手間を省けます。
ポートフォワード
セキュリティを考慮して、データベース(RDS)やElasticSearchは、外部からは直接接続できない様にすることが普通です。勿論、前述の通り、踏み台を経由して、プライベートサブネット内のマシンにログインして、開発をすることも可能です。しかし、効率面を考慮すると、極力ローカルの慣れたエディタを使いたいケースが多いと思います。
そこでオススメなのが、ポートフォワードです。
例えば、以下の様に、データベースとローカルマシンの間にトンネルを作っておくと、データベースへのアクセスを透過的に扱えます。
これで、踏み台をトンネルして、データベースサーバの5439ポートをローカルの5439ポートへバインドします。
(5439は、Redshiftのデフォルトポートです。適宜バックエンドで動作しているDBのポートに置き換えてください)
ssh -N -i ~/.ssh/[踏み台へ接続するための秘密鍵] -L 5439:[データベースサーバのアドレス]:5439 [ユーザ名]@[踏み台のアドレス]
PythonからSSHで踏み台サーバへログインしてプライベート領域のDBへ接続
上記は、トンネリングはあくまで手動で実施することを想定していました。
ただ、全てをプログラムで制御したい場合も往々にして存在します。
特に、AWS lambdaと組み合わせると、ローカルPCのcronに対するクラウドのcronのごとく、一定間隔で様々な処理が実施できて大変便利です。
では、早速Pythonでのやり方を見ていきたいと思います。
前準備
- 以下のコマンドで、sshtunnelをインストールします。
- pip install sshtunnel
- また、DB接続用のライブラリも好みのものをインストールします(例では、psycopg2)。
コードスニペット
上で出てきた踏み台サーバを経由してバックエンドのDBへアクセスするサンプルは以下の通りです。
必要に応じて、wait(スニペットでwaitのコメントを付けている部分)を入れて接続が整うのを待ってください。
from sshtunnel import SSHTunnelForwarder import psycopg2 as psy2 server = SSHTunnelForwarder( (踏み台サーバのアドレス, 22), ssh_host_key=None, ssh_username=踏み台サーバのユーザ名, ssh_password=None, ssh_pkey=踏み台サーバのSSH秘密鍵, set_keepalive=10.0, remote_bind_address=(データベースのアドレス, 5439) ) server.start() con = psy2.connect( dbname = データベース名, user = データベースのユーザ名, password = パスワード, host = "localhost", port=server.local_bind_port, async_=True(非同期) or False(同期) ) while True : #wait state = con.poll() time.sleep(1) if state == psy2.extensions.POLL_OK: break cur = con.cursor() cur.execute(任意のSQL文) con.close()
その他
- ファイルのやりとりは、上で述べてきたsshコマンドをsftpコマンドに置き換えることで可能です。
AWS Lambdaでpandasを利用する(Lambda Layers編)
概要
AWSのlambda関数で、外部のライブラリを実行したい場合、昔は、デプロイパッケージに、利用するコードを全て含めてやる必要がありました。
ただ、pandasやscikit-learnなどの重量級のライブラリの場合、毎回アップロードするのは、きついものがあります。
例えば、pandas(依存関係のあるnumpyなども含む)だと、圧縮状態で35Mbyteほどあります。
そこで、AWS Lambda layersの出番です。
これを使うことで、外部ライブラリのアップロードは、1回で済む様になり、自分で書いた部分のみを更新していけば良いことになります。
また、他のlambda関数でも使い回し可能です。
docs.aws.amazon.com
手順
以下、pandasを例にした手順です。
- とりあえず、lambda関数の実行環境を確認します。
- 19年2月現在は、以下の通りでした。
- Python 3.7.2
- 19年2月現在は、以下の通りでした。
- lambdaと実行環境を合わせたEC2を作成します。
- EC2へログインして、調べておいたlambdaで実行されるバージョンのpython(今回は、3.7.2)をインストールします。
- python.orgからソースを取得します。
- 解凍して、解凍した先のフォルダへ移動します。
- 必要なライブラリをインストールします。
- コンパイルしてインストールします。
- ./configure --enable-optimizations
- sudo make altinstall
- pandasをインストールするディレクトリ「python/lib/python3.7/site-packages/」を作成します。
- 利用したいライブラリ (この場合は、pandas)を上で作ったディレクトリへインストールします。
- pip3.7 install pandas -t ./python/lib/python3.7/site-packages/
- 「-t」オプションで、書き出し先を指定しています。
- pip3.7 install pandas -t ./python/lib/python3.7/site-packages/
- zipで固めます。
- これをLambda layersとして、publishしておきます (が、上のAMIのawsコマンドは古いのでアップデートが必要です。)
- 利用したいlambda関数と紐つけます。
以上で、lambdaでpandasを利用する準備が整いました。
最後に、pandasの使用例と実行結果を貼り付けておきます。
import sys; import json; import pandas as pd; def lambda_handler(event,context): df = pd.DataFrame([[1,2,3],[4,5,6]]); print(df); return { "statusCode": 200, "body": json.dumps('Hello pandas!'), "version": sys.version, "path": sys.path };
MacBook Air(or Pro)でGPUを外付けしChainerを動作させるまでの手順
はじめに
本記事は、大規模な学習はクラウドで、デバックはローカルで、(ついでに小規模な学習も)と言う人向けの記事です。
目的は、Mac(MacBook AirやMacBook Pro)で深層学習を行う環境を整えることになります。
グラフィックカード、深層学習用のライブラリ(Chainer)の順で、準備をします。
グラフィックカードの準備
私は、ノート型のMacを使っているので、GPUは外付けにしました。
GPUは、深層学習用のライブラリが動作するNVIDIAのGTXシリーズの中から選びます。
最近、アップルが外付けGPUを発表しましたが、AMD Radeonなので、今回の目的には合いません。
External Graphics Development Kit - Apple Developer
既製品を利用する場合
GTX 1070が搭載されているので、今回の目的にうってつけだと思います。
www.gigabyte.com
自分で組み立てる場合
私が環境を構築した当時、上記の製品は売っていなかったため、パーツを買ってきて組み立てました。
この方法は、System Integrity Protection(SIP)を無効化する必要が出てきます。気にする人は止めておきましょう。
- 以下のサイトを参考に進めていきます。
まず、選択するのは以下の3点です。
- グラフィックカード
- Thunderboltに対応したPCIeの外付けボックス(enclosures)
- 電源
選ぶ時のポイント
パーツ選びのポイントを解説していきます。
まずは自分の目的に沿った性能が得られるか、を考えます。
私は、画像系の深層学習も手元でちょっと動かしてみたかったので、メモリが8GByteあることと、速度面(pascalアーキテクチャでそこそこ早いもの)と言う理由で、GTX 1070を選択しました。また、akitio thunder2で動作させたかったので、short modelを選んでいます。
具体的には以下の製品を購入しました。
http://www.kuroutoshikou.com/product/graphics_bord/nvidia/gf-gtx1070-e8gb_oc_short/
- Thunderboltに対応したPCIeの外付けボックス(enclosures)
まずは、thunderboltの2系か3系かを、選択します。
自分の利用しているMacBook Air(もしくはpro)のインタフェースに適合するのが一番良いと思います。ただ、変換コネクタもあるので、そこまで気にする必要もないですね。
(ちなみにMacBookは、そもそもthunderboltに対応していません。)
次に、選択したグラフィックカードが箱に収容できるか、と言うのも大切なポイントです。
私は、自分のMacBook Airがthunderbolt2系だったので、akitio thunder2を選びました。ただし、akitio thunder2は、小さめなので収容できるグラボに制限が出てしまうのと、電源を別に購入しないといけないので、購入する場合は注意してください。
- 電源
そもそも、PCIeの外付けボックスに、初めから搭載されている電源の出力で賄えるのであれば、購入する必要はありません。
購入する場合、当たり前ですが、グラフィックカードの消費電力を賄えるか、を考えて購入します。
GTX 1070であれば、NVIDIAの公式HPにgraphics card powerが150Wとあるので、以下の252Wの電源を購入しました。
http://www.mouser.jp/ProductDetail/Mean-Well/GST280A12-C6P/?qs=XfZQyRplo5S6bkFXOs4q7w==
また、購入するグラフィックカードの補助給電スロットに合うように、変換コネクタも購入します。
最近のグラボは性能が良いので、PCIeの6ピンが2つもしくは、8ピンになっていると思います。
私の購入したGTX 1070は、PCIeの8ピンでした。
上記の電源であれば、PCIeの6ピンで出力するので、6ピン8ピンの変換ケーブルを購入しました。
組み立て
- 買ってきたPCIeの外付けボックス、グラフィックカード、電源をつなぎます。
- System Integrity Protection(SIP)を解除します。
- Macを再起動し、以下のautomate-eGPU.shを実行します。
- GitHub - goalque/automate-eGPU
- sudo ./automate-eGPU.sh
- このスクリプトの中で、ドライバもインストールしてくれます。
- GitHub - goalque/automate-eGPU
- MacとPCIeのボックスを、thunderboltケーブルで接続します。
- Macを再起動します。
「このMacについて」から「システムレポート」を呼び出し、グラフィックスカード欄に購入したグラボが記載されていれば認識されています。
深層学習用ライブラリ(Chainer)のインストール
大雑把に言うと以下のコンポーネントが必要になります。
- CUDA toolkit
- cuDNNのインストール
- GPUを深層学習用に最適化して利用するために必要です。
- CuPy
- CUDA上で計算を行うNumPyサブセットです。
- Chainer
- 深層強化学習用のライブラリです。
CUDA toolkitのインストール
- 以下のウェブページに沿ってインストールします。
- この過程でXcodeとCommand-Line Toolsもインストールします。
- トラブルシューティング
cuDNNのインストール
- NVIDIAのサイトからダウンロードします。 (要:ユーザー登録)
- cupyのサイトhttps://docs-cupy.chainer.org/en/latest/install.html#install-cudnnで、対応しているcuDNNを選びます。
- 17年9月2日現在、v6までの対応のようですので、cuDNNのv6を選びます。
- cupyのサイトhttps://docs-cupy.chainer.org/en/latest/install.html#install-cudnnで、対応しているcuDNNを選びます。
- DLしたファイルを解凍し、必要なファイルをコピーします。(コピー先は、自分がCUDA toolkitをインストールした先に読み換えてください。)
- tar xzvf (落としたcuDNNの名前).tar (cudaというディレクトリが作成される)
- cd cuda
- sudo cp include/* /usr/local/cuda/include/
- sudo cp lib/* /usr/local/cuda/lib/
環境変数の設定
(ここも、各自のCUDA toolkitの場所に読み換えてください)
CuPyのインストール
- 以下のサイトを参考に、インストールします。
- 具体的には、以下のコマンドです。
- sudo pip install -U setuptools
- pip install cupy --no-cache-dir
- 既に、cupyが入っている方は、一度uninstallしてください。
Chainerのインストール
- 以下のサイトを参考に、インストールします.
- Installation Guide — Chainer 3.0.0rc1 documentation
- sudo pip install chainer --no-cache-dir
- こちらも既にchainerが入っている人は、uninstallしてから行います。
効果測定
Chainerのサンプル(chainer/examples at master · chainer/chainer · GitHub)から、mnistを選んで比較してみました。
サンプルの中のtrain_mnist.pyの実行結果です。
当たり前ですが、最終的に得られる精度に差はありません。
ただ、学習に必要な時間は、CPUが1エポック50秒程度かかっているのに対し、GPUは、5秒程度と10倍ほど高速になっているのが確認できました。
以下がCPUを使った場合の結果です。
# unit: 1000
# Minibatch-size: 100
# epoch: 20
epoch | main/accuracy | validation/main/accuracy | elapsed_time |
1 | 0.942133 | 0.9699 | 42.2322 |
2 | 0.97755 | 0.9739 | 86.9796 |
3 | 0.984867 | 0.9794 | 131.715 |
4 | 0.987483 | 0.9823 | 176.681 |
5 | 0.990617 | 0.9802 | 222.018 |
6 | 0.992367 | 0.98 | 276.583 |
7 | 0.992017 | 0.9809 | 324.01 |
8 | 0.994883 | 0.9815 | 370.21 |
9 | 0.995067 | 0.9807 | 417.02 |
10 | 0.99425 | 0.984 | 464.117 |
11 | 0.996317 | 0.9784 | 511.821 |
12 | 0.9959 | 0.9813 | 560.13 |
13 | 0.996567 | 0.9839 | 608.949 |
14 | 0.996033 | 0.9824 | 659.155 |
15 | 0.996433 | 0.9829 | 709.139 |
16 | 0.997783 | 0.9819 | 759.985 |
17 | 0.996917 | 0.9826 | 811.511 |
18 | 0.997167 | 0.9804 | 863.433 |
19 | 0.9971 | 0.9753 | 916.173 |
20 | 0.996767 | 0.982 | 969.896 |
こちらがGPUを使った場合。
# unit: 1000
# Minibatch-size: 100
# epoch: 20
epoch | main/accuracy | validation/main/accuracy | elapsed_time |
1 | 0.943417 | 0.9706 | 7.06982 |
2 | 0.977766 | 0.971 | 12.0191 |
3 | 0.984982 | 0.9796 | 16.8749 |
4 | 0.987398 | 0.9782 | 21.8097 |
5 | 0.991315 | 0.9788 | 26.8118 |
6 | 0.991865 | 0.9819 | 31.9384 |
7 | 0.994015 | 0.9802 | 36.8659 |
8 | 0.992449 | 0.9815 | 41.8097 |
9 | 0.994632 | 0.9808 | 46.9009 |
10 | 0.995415 | 0.9803 | 51.9401 |
11 | 0.995549 | 0.983 | 57.0252 |
12 | 0.995315 | 0.9814 | 62.0933 |
13 | 0.997849 | 0.9811 | 67.0458 |
14 | 0.996182 | 0.9831 | 72.1063 |
15 | 0.996398 | 0.978 | 77.0816 |
16 | 0.996449 | 0.9836 | 81.9985 |
17 | 0.997199 | 0.9809 | 87.0473 |
18 | 0.996616 | 0.9826 | 91.9781 |
19 | 0.998866 | 0.9825 | 96.8835 |
20 | 0.996532 | 0.9808 | 101.705 |
強化学習事始め(Open AIのgymを使って手っ取り早く始める)
数式は、一切挟まない解説(と言うか主に実装)です。
強化学習の中でもQ-learningの一番単純な話です。
数式込みでちゃんと理解したい人は以下の記事をお勧めします。
強化学習とは?(What is Reinforcement Learning?)
また、最新の話題も知りたいよって人は以下の2nd editionが良いと思います。
http://webdocs.cs.ualberta.ca/~sutton/book/the-book.html
概要
強化学習とは、環境(Environment)との相互作用を通してエージェントを学習させる手法です。
ポイントは、プログラムの書き手が事細かにエージェントに指示を出さなくても、勝手に学習してくれる点です。
ざっくり書くと、エージェントの行動(action) に対して,状況(state)が変化し、報酬(reward)が支払われ、
エージェントは、試行錯誤の末、将来貰える報酬が最大になるように学習します。
例題(Open AIのFrozenLake)に基づく解説
今回題材として扱うのは、以下のFrozenLake-v0です。
gym.openai.com
環境(env)は、OpenAI側が引き取ってくれるので、エージェントの実装のみに注力できます。
FrozenLake-v0の問題設定
- ペンギンがいて、碁盤の目状の氷の上からの脱出を目指します。
- 碁盤の目は、縦4、横4マスです(state(s)は、16パターン)。
- ペンギンは、上下左右へ1マス移動できます(=action(a)は、4パターン)。
エージェントの学習
- Q(s,a)は、64パターン(16*4)になります。
- Q(s,a)は、ある状態(s)の時にとった行動(a)により得られる(将来に渡る)報酬です。
- ある状態(s)のもと、ある行動(a)をとることで、報酬が得られた場合は、次回もその行動を取りやすくするために、Q(s,a)は増加します。
- 逆に、ある行動(a)をとった次の状態(s+1)で得られる報酬(Q(s+1,a))が、現在の状態(s)で得られる報酬(Q(s,a))よりも小さいのであれば、Q(s,a)を減じてその行動を慎むようにします。
実装 (ここを読んで頂くのが一番手っ取り早いかも)
# -*- coding: utf-8 -*- #!/usr/bin/env python import gym; import numpy as np; import matplotlib.pyplot as plt; #結果をグラフ描画しないなら不要 env = gym.make('FrozenLake-v0'); #環境の読み込み """ 環境(Environment)は、以下の通り。 SFFF (S: starting point, safe) FHFH (F: frozen surface, safe) FFFH (H: hole, fall to your doom) HFFG (G: goal, where the frisbee is located) """ #状態(state)は、16種、行動(action)は、4種 なので、16行4列 Q = np.zeros([env.observation_space.n, env.action_space.n]); lr = 0.7; #学習率(=learning rate) (小さい(0.0)と遅いし、大きい(1.0)と局所解に陥りやすい) disct= 0.99; #報酬のディスカウント(discount) history = []; #グラフにプロットするためなので、グラフ描画しないなら不要 #state :s #action : a for i in range(2000): #2,000回ほど繰り返す。 s = env.reset(); #環境を初期化し、初期状態を返却する (本問題では初期状態は、0) done = False; #学習 for _ in range(300): # 1episode中の試行は、300回まで #選び方は、貪欲アルゴリズム #ランダム(np.random.randn)がエージェントの試行錯誤を表す。 #学習が進むとrandomな行動をとることは慎むように(i+1)で割っている。 a = np.argmax(Q[s,:] + np.random.randn(1,env.action_space.n)*(1.0/(i+1))); #試行が終了した場合は、done = True (goalにたどり着いた or 穴に落ちた) #ゴールできた場合は、reward = 1.0、その他は0.0 s_1,reward,done,info = env.step(a); #今回、infoは利用しない #Qテーブルの更新 (ここが、学習の肝) Q[s,a] = (1-lr) * Q[s,a] + lr * (reward + disct * np.max(Q[s_1,:])); """ 以下のように展開した方が理解しやすい(かも知れない) Q[s,a] = Q[s,a] + lr * ( reward + disct * np.max(Q[s_1,:]) - Q[s,a]); 成功していた場合は、rewardが1.0なので、次回もその行動を取りやすいくなるように、Q[s,a]は、加算される もし、とった行動(a)により、将来の報酬(Q[s_1,:])が減るようであれば、 次回は、その行動は取りづらくなるように、Q[s,a]は減じられる (ただし、報酬が発生するようなら加算に転じることもある) """ s = s_1; if done == True: break; history.append(reward); #ゴールできていれば1.0。その他は0.0 plt.plot(history); plt.show();
結果
結果は、以下の通りです。
横軸は、試行回数(episodeの数)で、縦軸は、成功(1)or失敗(0)です。
はじめの方は、失敗ばかりですが、200回あたりからは、失敗はほぼなくなっていることがわかると思います。
また、700回付近で、スランプ(ちょっと失敗している)が見られますが、以降は安定しています。
ランダムな要素ありで学習しているので、得られるグラフは毎回異なります。