【Stable Diffusion】Google ColabでLoRAファインチューニング!

〜Lora Trainer by Hollowstrawberryを使ってみた〜

今回は、Stable Diffusionのファインチューニングに関する体験談を共有したいと思います。
特に「Kohya Trainer」のColabサポート終了後、「Lora Trainer by Hollowstrawberry」を使って学習を行った流れや、独自に書いたinferenceコード、そして注意点についてまとめました。

背景:Kohya TrainerのColabサポート終了

最近、Google Colab向けのメンテナンスが終了してしまいました。これにより、手軽にColab上で学習できる手段を探していたところ、「Lora Trainer by Hollowstrawberry」に出会いました。

このツールは比較的新しく、シンプルながら必要な機能が揃っており、Colab環境でもスムーズに動作します。

学習環境:Lora Trainer by Hollowstrawberry

「Lora Trainer by Hollowstrawberry」は、セットアップも簡単で、GUIベースでLoRAのトレーニングができる非常に便利なツールです。
github.com
から、Open in colabを選択 (今回は、Lora Trainer)

基本的な流れは以下の通りです:

  1. Google Colab上でノートブックを開く (上で開いたものを自分用に保存する)
  2. ベースモデルや学習パラメータ(エポック数、バッチサイズなど)を設定
  3. データセットをアップロードして学習開始!

設定で、最低限設定するのは、project_nameのところ (ここが、google drive側のディレクトリと対応する)

今回は「zunda」にしました。

また、学習用のデータは、以下のページの「AI画像モデル用学習データ」を使いました。
zunko.jp
ダウンロードして、google driveの「Loras/zunda/dataset」に格納します。(bmppngが混じっていますが、どちらも対応しているのでそのままでOK。またtxtファイルも一緒におきます。)

推論コード

Lora Trainer by Hollowstrawberryには、学習したLoRAを適用するための推論(inference)コードがなさそう...
そのため、今回は、推論用のスクリプトを自分で書きました。
書いた内容は非常にシンプルで、通常のStable Diffusionの推論に、追加でLoRAを読み込むだけです。
また、注意点として、学習に使うベースのモデルは、合わせる必要があります。
今回は、animefull-final-pruned-fp16.safetensorsをベースとしているので、civitaiから拾ってきます。

!wget "https://civitai.com/api/download/models/481732" -O /content/animefull-final-pruned-fp16.safetensors

推論用のコード (これもcolabで動かしています。)

from google.colab import drive
drive.mount('/content/drive')

# Hugging FaceからPipelineを読み込み
from diffusers import StableDiffusionPipeline
from transformers import CLIPTextModel
#from diffusers import StableDiffusionPipeline, CLIPTextModel
import torch

# ベースモデルを読み込む
model_path ="/content/animefull-final-pruned-fp16.safetensors"

text_encoder = CLIPTextModel.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="text_encoder", torch_dtype=torch.float16) # Load text encoder separately
pipe = StableDiffusionPipeline.from_single_file(
    model_path,
    text_encoder=text_encoder,
    torch_dtype=torch.float16,
    safety_checker=None
).to("cuda")

# LoRAファイルを指定(Google Driveとかにアップしておいて)
lora_path = "/content/drive/MyDrive/Loras/zunda/output/zunda-10.safetensors"

# LoRA適用
pipe.load_lora_weights(lora_path)

# 生成プロンプト
prompt = "zdm,masterpiece, 1girl, detailed face + light blue :1.5, nose, chestnut, ( jammers :1.2), closed mouth, looking at viewer, short hair , background: arcade , solo, upper body"
negative_prompt = "Nsfw, nuidy, naked, blurry, bad anatomy, wrinkled skin, floating objects, mouth, depth of field, sharp focus, lowres, bad anatomy, nail polish, floating objects, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry"

# 推論実行
image = pipe(
    prompt,
    negative_prompt=negative_prompt,
    num_inference_steps=30,
    guidance_scale=8,
).images[0]

# 結果保存
image.save("/content/drive/MyDrive/zundamon.png")

print("画像生成完了!")


結果は... (RoLAありだと、しっかり、ずんだもんになっている)

RoLA適用あり
RoLA適用なし

clineでOthelloゲームを作らせてみた話

最近、AIにコードを書かせる流行りにのって「一行もコードを書かずにゲームを作る」というチャレンジをしてみました。

使ったのは、AIツールの Cline (モデルは copilot-o3-mini)。
テーマは、「オセロ(Othello)」。
そして、その成果物がこちら
https://othellite.netlify.app/

最初はルールすら曖昧だった…

正直、オセロのルールは、ほとんど伝えない状態からスタートしました。
でもClineのすごいところは、「とりあえず指示を出してみると、動くものが返ってくる」ところ。

最初は
「オセロ作って、nextjsでよろしく」みたいな、ざっくりしたプロンプトを渡しただけ。
※実際の指示は、英語で行なっています。

当然、はじめはちょっと変なルールで動いてました(笑)
でも「ここは石をひっくり返すべきだよ」とか、「合法手がないときはパスして」とか、
細かくコメントしていくと、AIがどんどん改善していくのです。

人間は口を出すだけ。AIがやりきった。

このプロジェクトで自分がやったことは、
コードのレビューとフィードバックだけ。
実装、デバッグ、スタイル調整まで、全部AIがやってくれました。
※細かいところだと、gitのリポジトリ管理は、人間がやりました。
(一度大きく間違えたので、その時は、手動で切り戻しました。)

AIプレイヤーもついてる

ちなみに、このOthelloにはAIプレイヤー機能も搭載されています。
今のところは「なんちゃってAI」で、とりあえず動くレベルです。
(たまに変な手も打ちますが、それも愛嬌ということで…)

まとめ

今回、cline × copilot-o3-miniを使って、
完全にノーコードでゲーム開発をしてみましたが、
予想以上にスムーズにできて驚いています。
(所要時間は、1日くらいでした。)

今後はもうちょっと頭のいいAIプレイヤーに進化させていきたいところ。

よかったら遊んでみてください
https://othellite.netlify.app/

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

参考:www.blender.org

上記を満たすには、g4dn.2xlarge (8コア、32GbyteのRAM) 程度がちょうど良さそうですね。

設定手順

今回は、手元のmacから接続します。EC2インスタンスは、LinuxWindowsが選択できますが、Linux側を選択します。
(ここは、慣れている方で良いと思います。)

ざっくりとは、以下の手順です。

  1. blenderとNice DCV設定済みのAMIからEC2インスタンスを起動
  2. ユーザ作成 (Nice DCVで接続した時のログイン用兼作業用)
  3. Nice DCVの設定ファイルの書き換え
  4. クライアントのインストール(既にインストール済みでなければ)

EC2インスタンスの起動

  • AMIは、「Nimble Studio Linux Workstation AMI」を選択します。
    • このAMIには、BlenderとNice DCV(サーバ)が最初からインストール済みとなります。
    • スポットインスタンスを選択する場合は、高度なオプションから「スポットインスタンスをリクエスト」にチェックを入れます。
      • ただし、スポットは割り当てられないこともあるので注意。
  • 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として置いています。)

おわりに

以上で、mac bookからでも快適にblenderが利用できる様になりました。
(やはりMac bookの1kgを切る重さは捨てられません)

AWS SESを使って独自ドメインのメールアドレスの受信と転送設定を行う

はじめに

(システムのテストなどで)メールアドレスが大量に必要になった時に、AWS SESは1つの解になるかも知れません。

ドメインの取得

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」メニューから「Verified identities」を選択します。
  • 「create identity」ボタンを押して、「Domain」を選択します。
  • ドメイン名は、ご自身で取得してRoute53に登録してあるものを選びます。
  • 画面下の「Create Identity」ボタンを押すと、CNAMEレコードが、3行ほどRoute53の該当ドメインに追加されます。
  • Identity statusが「Verified」になっていれば、無事に成功です。(Route53でドメインを取得していると、ここまで半自動なので失敗することは少ないはずです。)

受信ルールの設定

次に、受信ルールを設定します。

  • 左の「Configuration」メニューから「Email receiving」を選択します。
    • リージョンが非対応の場合は、表示されていません。
  • 「Create rule set」ボタンを押します。
  • 「Create rule」 ボタンを押します。
  • Rule nameは、なんでも良いので、自分にとってわかりやすい名前をつけて、「Next」ボタンを押します。
  • 「Add new recipient condition」ボタンを押して、利用したいメールアドレスを入力します。
  • 次ページでは、「Add new action」から「Deliver to S3 bucket」を選択します。
  • S3 bucketに上で、バケットポリシーを設定したバケットを選択します。(バケットポリシーが間違っているとここで警告を受けます。)
    • prefixは、お好みで。
  • 次ページで、最終確認をして、画面下部の「Create rule」ボタンを押してルールを作成します。

ルールを作った後は、activateします。

  • 先ほど作成したルールを選択して、「Set as active」ボタンを押します。

MXレコードの設定

  • Route53のMXレコードに、以下のリンクで指定されているメールサーバを指定します。
    • 今回は、オレゴン(us-west-2)リージョンで設定しているので、「10 inbound-smtp.us-west-2.amazonaws.com」を設定します。

docs.aws.amazon.com

(受信の場合、CUSTOM MAIL FROMは特に設定する必要はありません。送信の場合でも、MAIL FROMが気にならないのであれば、設定する必要はありません。)

確認

  • 最後に、上で作成したメールアドレスに試験メールを送って、S3へ無事書き込まれているかを確認します。
    • 上手く設定できていれば、指定したバケット(の指定していればprefix)配下に、ファイルが作成されているはずです。
    • (注)「AMAZON_SES_SETUP_NOTIFICATION」と言うファイルは、SESでバケットを指定した時に試験的に書き込んでいるファイルなので、違います。

転送設定

毎回、直接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関数の呼び出し

  • 上の方で設定したSESのルールのアクション(現状は、S3への書き込みだけ設定されているはず)に、Lambda関数の起動を追加します。
    • SESとLambda関数のリージョンがあっていないと「Invoke AWS Lambda function」時に、Lambda関数が表示されません。

送信先のメールアドレスの認証

自分のアドレスに転送するため、サンドボックスの設定は、解除しません。
そのため、転送先のアドレスをSESから認証しておく必要があります。

  • SESの画面で、create identityから送信先のEmailアドレスを登録しておいてください。

以上です。

Raspberry Pi4でpythonの環境を作ってOpenCVをインストールするまで

Raspberry Piの購入

MacBook(USB-C対応)で作業する前提で記載します。
この場合、必要な物品は以下の4点です。

(最近は、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 PiMacの接続

基本設定

SSHでログインするために、Raspberry PiSSHサーバを有効にします。
先ほど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)が作成されます。

ssh-keygen -t rsa -b 4096 -f raspi01

※fオプションを外すと、デフォルトの位置(/Users/ユーザ名/.ssh 配下)に作成してくれます。
※passphraseをなしにする場合は、2回enterキーを叩きます。

(2)秘密鍵を配置する
以下で、所定のディレクトリへ移動します。

mv raspi01 raspi01.pub ~/.ssh

※公開鍵を移動させなくても動きますが、無くすと面倒なので忘れない所に保存しておいた方が良いと思います。

秘密鍵の権限を自分だけが読み取れる様に書き換えます。

chmod 600 ~/.ssh/raspi01

(3)公開鍵を配置する
以下のコマンドで、Raspberry Pi側へ公開鍵を配置します。
(パスワードは、変えていなければ「raspberry」です。)

ssh-copy-id -i ~/.ssh/raspi01.pub pi@raspberrypi.local

(4)証明書でログインできることを確認する
以下でログインできれば、問題なく設定できています。

ssh -i ~/.ssh/raspi01 pi@raspberrypi.local

(5)パスワードのログインを無効化する。
(Raspberry Piにログインして)/etc/ssh/sshd_config に以下を追記します。
※ 既に、該当行がコメントされているはずなので、以下の様に書き換えます。

PasswordAuthentication no

普通に開くと、read onlyなのでsudoで実行します。

sudo vi /etc/ssh/sshd_config

その後、Raspberry Piを再起動します。

sudo reboot

(6)パスワードでログイン出来なくなっていることを確認する。

ssh pi@raspberrypi.local

だと、こんな感じに弾かれる様になりました。

pi@raspberrypi.local: Permission denied (publickey).

※外部に公開する時は、ちゃんとセキュリティを固めましょう。あくまで隣人対策です。

ssh configの設定による簡単ログイン

~/.ssh/config に以下を追記します。

HOST pi
           HostName raspberrypi.local
           User pi
	   IdentityFile ~/.ssh/raspi01

次回から、以下のコマンドでラズパイにsshでログイン出来る様になります。

ssh pi

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>

図にすると以下の様になります。

f:id:marmarossa:20191209003936p:plain
SSH_basic

踏み台の先にあるサーバにログインする

ログインして作業したいサーバが踏み台の先にある場合、一旦、踏み台にログインした後、さらに目的のサーバにログインするのは、面倒なことがあります。そういう場合は、以下の様に
~/.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を例にした手順です。

  1. とりあえず、lambda関数の実行環境を確認します。
    • 19年2月現在は、以下の通りでした。
  2. lambdaと実行環境を合わせたEC2を作成します。
  3. EC2へログインして、調べておいたlambdaで実行されるバージョンのpython(今回は、3.7.2)をインストールします。
    • yum list | grep python3 で、出てくれば良いのですが、入っていないのでソースから入れます。
  4. python.orgからソースを取得します。
  5. 解凍して、解凍した先のフォルダへ移動します。
  6. 必要なライブラリをインストールします。
    • yum install gcc openssl-devel bzip2-devel libffi-devel
  7. コンパイルしてインストールします。
    • ./configure --enable-optimizations
    • sudo make altinstall
  8. pandasをインストールするディレクトリ「python/lib/python3.7/site-packages/」を作成します。
    • こうしておくと、以下の理由により、勝手にパスが解決されるので楽です。
      • Lambda Layersは、/opt 配下に、アップロードされたzipファイルを展開します。
      • lambdaは、実行時に、「/opt/python」or 「/opt/python/lib/python3.7/site-packages/」を参照します(他にも見るところはあります)。
  9. 利用したいライブラリ (この場合は、pandas)を上で作ったディレクトリへインストールします。
    • pip3.7 install pandas -t ./python/lib/python3.7/site-packages/
      • 「-t」オプションで、書き出し先を指定しています。
  10. zipで固めます。
  11. これをLambda layersとして、publishしておきます (が、上のAMIのawsコマンドは古いのでアップデートが必要です。)
    • aws lambda publish-layer-version --layer-name [レイヤー名] --zip-file fileb://./hoge.zip --compatible-runtimes python3.7 --region ap-northeast-1 --profile [あなたのプロファイル]
    • レイヤー名は、「hoge」にしています。
  12. 利用したいlambda関数と紐つけます。
    • aws lambda update-function-configuration --function-name [利用したいlambda関数名] --layers arn:aws:lambda:ap-northeast-1:[AWS ID]:layer:hoge:1
    • 「layers」オプションでは、ARNをフルパスで指定します。
    • 最後の数字は、lambda layersのバージョンです(publishするごとに、インクリメントされるので、適宜合わせます)。

以上で、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
    };

f:id:marmarossa:20190211235403p:plain
lambda関数の実行結果