Amazon lexで音声チャットボットを作成してみた

天気の良い週末にやることがなく、昼の12時ごろ、趣味の長時間歯磨きをしながらtechCrunchを見ていたら、amazonのlexの記事が乗っていた。

http://jp.techcrunch.com/2017/04/21/20170420amazon-lex-the-technology-behind-alexa-opens-up-to-developers/

Amazonの仮想アシスタントAlexaを支えているテクノロジーであるAmazon Lexが、今朝(米国時間20日)のロイターの記事によれば、プレビュー段階を終了したということだ。

↓↓↓
https://aws.amazon.com/jp/lex/

Amazon Lex は、音声やテキストを使用した会話型インターフェイスをさまざまなアプリケーションに構築するためのサービスです。
Lex では、自動音声認識 (ASR) という音声をテキストに変換するための高度な深層学習機能と、
テキストの意図を理解するための自然言語理解 (NLU) を利用できます。
これにより、非常に魅力的なサービスと生き生きとした音声対話を実現するアプリケーションを構築できます。
Amazon Lex を使うと、すべての開発者が Amazon Alexa に採用されている深層学習技術と同じ技術を利用し、
自然言語での高度な対話ボット (チャットボット) を短時間で簡単に構築できるようになります。

このページにある動画は去年の11月末のものだ。
その頃、某ゲームの開発と並行して走る運営の現場の両方が修羅場で、毎日トラブルと戦っていた。さらにトラブルは現場だけとは限らず、外野がキナ臭くなり、モヒカンと肩パッドの荒くれ供から平和を守るために北斗百烈拳を繰り出していた。明日を見失って、CV千葉繁になっていた記憶がうっすらとある。
当然re:InventのKeynoteなど見る暇はなかった。

Try a sample

まずはこちらのページでサンプルを作ってみる。
https://console.aws.amazon.com/lex/home?region=us-east-1#bot-create:

cb-1.png
BookTripをクリック。

cb-2.png

IAM:自動的に作ってくれるんはありがたい。念のため後でデタッチされた権限を見ることにするけどな。
Child-Directed?:子供専用ではないからNo。

Createを押しやす。


マイクを許可しますかのプロンプトが出るので、許可にする。
このmacには内臓マイクがないので、iPhoneのヘッドホンを繋いでおく。

cb-3.png

しかしこの後、何をどうしたらいいのか、説明がないからさっぱりわからない。。。。。。
困った。

tutorialの動画をみながらもう一度sample chat botをつくる

AWS Lex Demo Tutorial (Jan 2017)
https://www.youtube.com/watch?v=7uG9cuxNo5k
「花の注文をするチャットボット」を10分くらいで作る様子。

4ヶ月ほど前に親切な人が投稿してくれたチュートリアル動画で、
やさしい英語をえらんで説明してくれているが、
英語のききとりがやっとの自分はYoutobeの字幕機能で補いながらみた。
この通りやってみると、なんとなくわかる。

なおこの動画の中では、まだ「Channels」がFacebookしかないが、
3ヶ月後の現在は、3つに増えている。

  • Facebook
  • Twilio SMS ←増えた
  • Slack ←増えた

花屋のサンプルをそのまま真似して、以下のように作ってみた。

name:OrderFlowersSitopp
Description:Bot to order flowers on the behalf of a user
Output voice:Joey

cb-8.png

Buildをするとtest Botでtext入力できるようになる。
Sample Utterancesの1個めをコピペして、
I would like to pick up flowers
と記入してエンターしてみるとすぐ答えが返ってきた。ほうほう。
cb-7.png
あっ、これもうできちゃったのか。早いなぁ。(΄◉◞౪◟◉`)

音声入力してみる

AWSのLexのコンソールに行く
https://console.aws.amazon.com/lex/home?region=us-east-1#bots:

さっき作った「name:OrderFlowersSitopp」を選ぶcb-15.png

右側にチャットボットのタブが現れるので、
入力欄のマイク部分をクリックする。そうすると音声入力モードになった。

cb-13.png


Macに繋いだiPhone用のヘッドフォンのマイクのあたりに話しかけると、テキスト化して、チャットにしてくれます。

上の例だと、私の発音が悪いので、rosesがlosesとかclosesになっている。ひどい(笑)
しかもボットはそのまま「Ok your closes will be ready 」と予約終了してしまった。
いったいなんの花が届くのか。。笑

もちろん、これはサンプルだから何も届かないけど、
ちゃんとカスタムスロットに無いものはエラーにしないと、そのまま注文を受け取ったことになってしまうから、
注意が必要ということで。

sampleを作ってわかったこと

  • サンプルの3種類は以下の通り。主たる目的はこういうものなのかな。

    • 旅行の予約
    • 花屋のオーダー
    • 歯医者の予約

考えられる用途

  • 保険の見積もり
  • 通販サイトのパスワード忘れた
  • 美容院の予約
  • ゲームの進行不具合の通報
  • ゲームのチート疑惑の通報
  • ゲーム掲示板の炎上の通報
  • ゲームの..いかん、つい仕事が

これがFacebookのお店のページに備わってたら、確かに気楽に呼び出しやすい。
でも受け取る側はどうなんだろう? botが応答して解決しない場合もあるだろうし、ちゃんと過去の履歴を呼び出せるのかな?

大変そうな点

Sample UtterancesとSlot typesの用意が大変そう。

Sample Utterancesとは、ユーザーがこういったらこの部分をパラメタとして受け取るという設定。花屋の予約sampleでは2個しかなかったけど、こんなんで済むはずがない。

  • I would like to pick up flowers
  • I would like to order some flowers

様々な言い方を網羅しておかないと、「おっしゃることがわかりません」を連発するポンコツbotになってしまう。
(参考動画 Virgin Flight – Saturday Night Live

Slot typesは、ユーザーの返答の中から受け付けるキーワードの種類で、花屋の予約sampleでは花の種類3つが該当する。だがこんな少なくて済むはずがない。3種類しか売ってない花屋っていうのだったら別だけど。
– roses
– lilies
– tulips

Sample utterancesもSlot TypesもAlexa Skillsにも同様の設定項目があり、これらをいかにきめ細かに定義できるかでbotの優秀さが決まると思う。

まとめ

  • AWSのlexを使ってchatbotが作れる。
  • 連携先は、Facebook、Twilio SMS、slackが選べる。ただし音声入力は、声の入力装置があるプラットフォームじゃないとダメ。(Twilioは電話と連携してるから、これが候補か。)
  • Utterancesとslot typesをちゃんと定義しないと、ポンコツbotになってしまう。人工知能というより人工無能。これはAlexa skillsも同じ。
  • しゃべった言葉を分解してテキストにするところが人工知能。ここはブラックボックス。
  • サンプルでは必要なかったが、ちゃんとbotを作るときにはLambda関数も書く必要がある。(http://docs.aws.amazon.com/ja_jp/lex/latest/dg/getting-started-ex2.html)
  • botの会話ログを見るのってどうするんだろ?

以上!

続きを読む

EC2-ローカル間でのファイル転送

3行で分かるあらすじ

  1. EC2内でエクスポートしたsqlファイルをローカルに転送させたかった。
  2. FileZillaでSFTPを使って転送しようとしたが、なぜかsqlファイルが表示されなかった。
  3. 表示されない原因調べるのも馬鹿らしいのでscpで転送した。

環境

ローカル:Mac
Amazon EC2:Amazon Linux

EC2 → ローカル 転送

scp -i [公開鍵ファイルのパス] [ユーザ名@ドメイン]:[送信元EC2ファイルパス] [転送先ローカルファイルパス]

#scp -i /keys/hoge.pem user@ec2-xxxx.com:/sqls/hoge.sql /Desktop

ローカル → EC2 転送

scp -i [公開鍵ファイルのパス] [送信元先ローカルファイルパス] [ユーザ名@ドメイン]:[転送先EC2ファイルパス]

#scp -i /keys/hoge.pem /Desktop/hoge.sql user@ec2-xxxx.com:/sqls

まとめ

エンジニアたるものGUIばかりに頼っていてはいけないな!な!(戒め)

続きを読む

OVAファイルをAMIとして取り込む

ステップ 3: イメージとして VM をインポートする – VM Import/Export」に基づいて OVA ファイルをアップロードして AMI にする手順をまとめた。

S3 バケットの作成

OVA ファイルをいったん S3 バケットにアップロードする必要があるため、バケットを作成する。

① 名前とリージョンで以下の指定を実施

  • 「バケット名」に任意の文字列を入力
  • 「リージョン」に適切なリージョン(通常は「アジアパシフィック(東京)」を指定
  • 「次へ」ボタンを押下

② プロパティの設定

  • 必要に応じて設定
  • 「次へ」ボタンを押下

③ アクセス許可の設定

  • (「パブリックアクセス許可を管理」を開き「認証済みの AWS ユーザー」の「読み込み」をチェック)
  • 「次へ」ボタンを押下

④ 確認

  • 「バケットを作成」ボタンを押下

awscli のインストール

brew install awscli
aws configure

今回は Mac で作業したので Homebrew でインストール。環境に応じて適宜対応。

サービスロールの作成

ポリシーの作成

cat << "_EOF_" > trust-policy.json
{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Principal": { "Service": "vmie.amazonaws.com" },
         "Action": "sts:AssumeRole",
         "Condition": {
            "StringEquals":{
               "sts:Externalid": "vmimport"
            }
         }
      }
   ]
}
_EOF_
aws iam create-role --role-name vmimport --assume-role-policy-document file://trust-policy.json

create-role コマンドで vmimport というロールを作成する。

ロールへのポリシーのアタッチ

cat << "_EOF_" > role-policy.json
{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
            "s3:ListBucket",
            "s3:GetBucketLocation"
         ],
         "Resource": [
            "arn:aws:s3:::disk-image-file-bucket"
         ]
      },
      {
         "Effect": "Allow",
         "Action": [
            "s3:GetObject"
         ],
         "Resource": [
            "arn:aws:s3:::disk-image-file-bucket/*"
         ]
      },
      {
         "Effect": "Allow",
         "Action":[
            "ec2:ModifySnapshotAttribute",
            "ec2:CopySnapshot",
            "ec2:RegisterImage",
            "ec2:Describe*"
         ],
         "Resource": "*"
      }
   ]
}
_EOF_
aws iam put-role-policy --role-name vmimport --policy-name vmimport --policy-document file://role-policy.json

disk-image-file-bucket (2箇所) は適宜変更のこと。
put-role-policy コマンドで上記で作成したロールにポリシーをアタッチする。

OVA ファイルのアップロード

aws s3 cp disk-image-file.ova s3://disk-image-file-bucket/

カレントディレクトリーにある disk-image-file.ova ファイルを disk-image-file-bucket バケットにアップロードする。

OVA ファイルのインポート

cat << "_EOF_" > containers.json
[
  {
    "Description": "OVA Disk Image",
    "Format": "ova",
    "UserBucket": {
        "S3Bucket": "disk-image-file-bucket",
        "S3Key": "disk-image-file.ova"
    }
  }
]
_EOF_
aws ec2 import-image --description "OVA Disk Image" --disk-containers file://containers.json

JSON ファイル内に "Description": があるが、オプションでも --description を指定しないと以下のエラーが出る。

A client error (InvalidParameter) occurred when calling the ImportImage operation: The service role <vmimport> does not exist or does not have sufficient permissions for the service to continue

ステータスの確認

aws ec2 describe-import-image-tasks --import-task-ids import-ami-XXXXXXXX
IMPORTIMAGETASKS    OVA Disk Image  import-ami-XXXXXXXX 2   active  pending
SNAPSHOTDETAILS 0.0 OVA
USERBUCKET  disk-image-file-bucket  disk-image-file.ova
IMPORTIMAGETASKS    OVA Disk Image  import-ami-XXXXXXXX 28  active  converting
SNAPSHOTDETAILS 771973632.0 VMDK
USERBUCKET  disk-image-file-bucket  disk-image-file.ova
IMPORTIMAGETASKS    OVA Disk Image  import-ami-XXXXXXXX 30  active  updating
SNAPSHOTDETAILS 771973632.0 VMDK
USERBUCKET  disk-image-file-bucket  disk-image-file.ova
IMPORTIMAGETASKS    OVA Disk Image  import-ami-XXXXXXXX 37  active  updating
SNAPSHOTDETAILS 771973632.0 VMDK
USERBUCKET  disk-image-file-bucket  disk-image-file.ova
IMPORTIMAGETASKS    x86_64  OVA Disk Image  import-ami-XXXXXXXX BYOL    Linux   59  active  booting
SNAPSHOTDETAILS /dev/sda1   771973632.0 VMDK
USERBUCKET  disk-image-file-bucket  disk-image-file.ova
IMPORTIMAGETASKS    x86_64  OVA Disk Image  ami-XXXXXXXX    import-ami-XXXXXXXX BYOL    Linux   completed
SNAPSHOTDETAILS /dev/sda1   771973632.0 VMDK    snap-XXXXXXXXXXXXXXXXX
USERBUCKET  disk-image-file-bucket  disk-image-file.ova

watch コマンドを使ってもいいかもしれない。

続きを読む

Docker for Mac で Amazon Linuxのコンテナイメージを動かす

はじめに

macbook air をクリーンインストールしたついでに、開発環境をイチから構築していました。
Docker の コンテナイメージ何にしようかと探していたら Amazon Linuxがコンテナ化されていたんですね。
(今更しりました…。)
ということで、折角知ったので macで Amazon Linuxのコンテナを動かしてみます。

前準備

docker for mac のインストール

$ brew cask install docker
$ docker -v

Docker version 17.03.1-ce, build c6d412e

aws cli の インストール と 設定

$ brew install awscli
$ aws --version

aws-cli/1.11.76 Python/2.7.10 Darwin/16.5.0 botocore/1.5.39
$ aws configure

AWS Access Key ID [None]: `IAMのアクセスキー`
AWS Secret Access Key [None]: `上記IAMのシークレットキー`
Default region name [None]: ap-northeast-1
Default output format [None]: json

ここから本番

AmazonLinuxコンテナが配布されているECRへのログインコマンドを取得します。

$ aws ecr get-login --region ap-northeast-1 --registry-ids 137112412989
docker login -u AWS -p 'パスワード' -e none https://137112412989.dkr.ecr.ap-northeast-1.amazonaws.com

上記のコマンドが表示されるので、コピペして実行します。

Flag --email has been deprecated, will be removed in 1.14.
Login Succeeded

ログイン成功したようなので、イメージの種類を aws ecr list-imagesで確認してみます。

$ aws ecr list-images --region ap-northeast-1 --registry-id 137112412989 --repository-name amazonlinux | grep 'imageTag'

"imageTag": "2016.09.0.20161028-with-sources",
"imageTag": "2016.09.1.20161221",
"imageTag": "with-sources",
"imageTag": "2016.09.0.20161118",
"imageTag": "2016.09-with-sources",
"imageTag": "latest-with-sources",
"imageTag": "latest",
"imageTag": "2016.09.1.20161221-with-sources",
"imageTag": "2016.09",
"imageTag": "2016.09.0.20161118-with-sources",
"imageTag": "2016.09.0.20161028"

ここでは もちろん latestをローカルにpull してみます。

$ docker pull 137112412989.dkr.ecr.ap-northeast-1.amazonaws.com/amazonlinux:latest
$ docker images

REPOSITORY                                                      TAG                 IMAGE ID            CREATED             SIZE
137112412989.dkr.ecr.ap-northeast-1.amazonaws.com/amazonlinux   latest              ff59a593059d        3 months ago        292 MB

試しに実行してみます。 docker run

$ docker run -it 137112412989.dkr.ecr.ap-northeast-1.amazonaws.com/amazonlinux:latest /bin/bash

bash-4.2# cat /etc/system-release
Amazon Linux AMI release 2016.09

おお。できてますね!
必要最低限のパッケージしか入っていないみたいなので、次回はこれを使って環境構築をしていきます。

image

classmethodさんの記事に cawsayが載っていたので、真似してみました。:-)

SEE ALSO

続きを読む

純粋なシェルスクリプトだけでAWSを操りたい!〜POSIX原理主義でクラウドも侵略〜

この記事は、POSIX原理主義とシェルショッカー日本支部に感化されちゃって、なんとか実践しようとしている、なんちゃって雑魚戦闘員がお送りします。POSIX原理主義でもクラウドを侵略できますよ、というお話です。これで、POSIX原理主義がますます強力になれるんじゃないかと勝手に思っています。

まずコマンドの紹介から、

axs - UNIX哲学を守ったつもりのsimpleなawsコマンド

A simple ‘aws’ command ‘axs’ written in POSIX sh. axs(access) to aws(amazon web services) with posixism.

このコマンドは、AmazonWebServicesにアクセスし、数々のwebサービスを利用したり、アプリを構築したりするために作られた、POSIX原理主義に基づく、なんちゃってawsコマンドです。

https://github.com/BRAVEMAN-L-BRID/axs

作った経緯

AWS APIツールは「いつでも、どこでも、すぐ」使えるわけではない。

バージョン依存や環境依存の大きい、AWS APIツールはいつでもどこでもすぐに使えるわけではありません。それに加えて、SDKやCLIも、それらをインストールできない環境だと、そもそも役に立ちません。意外なこと使えない環境を目にする機会も多いです。

しかしながら、環境やバージョンアップに悩まされずにAWSのサービスを利用したい、というニーズも時にはあるのではないでしょうか?
浅くネットサーフィンした限りでは、そのように認識しています。

このaxsコマンドは、そのニーズに応えるために作りました。

POSIX shに準拠しているつもりなので、Windows, Mac, Linuxなどで、それこそコピーしてくるだけで「いつでも、どこでも、すぐ」動きます。

大量のオプション、引数からの解放

作った経緯2です。awsコマンドの大量の引数やオプション、サービスごとに異なる数々のサブコマンドにヘキヘキした覚えはありませんか?私はあります。
あれはUNIX哲学を守っていません。コマンドとクラウドへ渡すパラメーターがぐちゃぐちゃに混ざったシェルスクリプトを書くのは、もう、こりごりです。

このコマンドでは、その煩わしさから解放されます(嘘、かもしれません)。また、UNIX哲学を守ったつもりで、RESTfulの概念も大事にしています。直感的な操作が可能かもしれません。

スクリプトを使った自動化も楽になる可能性があります。

使い方

ダウンロードしたら、このリポジトリの/binディレクトリにPATHを通してください。

0. 準備

~/.aws/credentialsファイルに以下のようにアクセスキーIDとシークレットアクセスキーを記述してください。パーミッションには気を付けてください。

[default]
aws_access_key_id = hogehoge
aws_secret_access_key = mogemoge

1. 基本

使い方は簡単です。設定ファイル(データ形式は後述)をaxsコマンドで読み込むだけです。

例えば、catコマンドを使って

$cat config_file | axs

とできますし、もしくは、引数に設定ファイルを置くだけです

$axs config_file

2. 設定ファイルのデータ形式について

AWSはREST APIを用いています。そこで、今回は、正しいのかどうかは横に置いておいて、HTTPにちなんだデータ形式の設定ファイルを記述します。

以下のような形式をとることにしました。

METHOD URI                    (←リクエストライン)
Key Value                   (←キーバリュー形式に分解したクエリ)
key Value                   (←キーバリュー形式に分解したクエリ)
Key Value                     (←キーバリュー形式に分解したクエリ)
Host: ec2.ap-northeast-1.amazonaws.com  (←必須ヘッダー)
Content-Type: application/hoghoge     (←場合によっては必須のヘッダー)
X-Amz-moge: hogehogehoge                (←設定などの追加のヘッダー)

(bodyの部分コンテンツの中身)
xmlとかjsonとかバイナリデータ         (←コンテンツの中身)

基本的に、Host,Content-Typeヘッダのみが必須だと考えてもらっていいです。

AWS APIを利用するので、body部には、基本的にxmlやjsonを記述します。

ただし、場合によっては画像ファイルや音声ファイルなどのバイナリデータをアップロードしなければならない時もあります。また、xml,jsonが長く煩雑な時は、リクエストライン、クエリ、ヘッダまでの部分とbody部を分離したいと思う時もあるでしょう。

そのような時に、axsコマンドの-fオプションを使います。

3. -fオプション

body部を分離して別ファイル(body.txtなど)にしてaxsコマンドを使う場合には以下のようにします。
※ここではヒアドキュメントを使用しています。

$cat<<END | axs -f moon.jpg
PUT /image.jpg
Host: Bucket.s3.amazonaws.com (東京リージョンの場合はbucket.s3-ap-northeast-1.amazonaws.com)
Content-Type: image/jpeg
END

*この例では、ローカルのmoon.jpgという画像ファイルをs3のBucketバケットにimage.jpgという名前で保存しています。

4. -qオプション

 これはAPIアクセス後に返ってくる、レスポンスのレスポンスヘッダーを表示するかいなかを決定するオプションです。
 デフォルトではレスポンスヘッダを表示します。AWSのREST APIがヘッダ情報をよく扱うので、汎用性を高めるためにそうのようにしてあります。

例えば、以下のような違いになります。

$cat config_file | axs 

HTTP 200  OK
hoge: hogehoge
hage: hagehoge
hoga: hogレスポンスヘッダー

<xml ......>
<DescribeInstances>...........</...>
.......

-qオプションを利用した場合は、レスポンスヘッダが省略されxmlやjsonやバイナリが直に帰ってきます。ですので、パイプでつないで加工したりするのに便利です。


cat config_file | axs -q > polly.mp3
               (pollyに喋ってもらう)


cat config_file | axs -q | parsrx.sh(POSIX原理主義製xmlパーサー)
               (xmlが返ってくる)


cat config_file | axs -q | parsrj.sh(POSIX原理主義製jsonパーサー)
               (jsonが返ってくる)

TIPS

  • 設定ファイルの書き方は、AWS API referenceなどを参照してください。設定ファイルの記述はクエリ部分以外はHTTPと同じです。
    深く悩まずに記述できることでしょう。

  • Content-Lengthヘッダ,x-amz-content-shaナンチャラヘッダ,Autorizationヘッダは自動生成されるので、考慮する必要はありません。

  • 私も仲間に加えてもらった秘密結社シェルショッカー日本支部のPOSIX原理主義製の他コマンドと相性がいいです。
    これを機に秘密結社シェルショッカー日本支部よりダウンロードしてくることをお勧めします。
    中でもmojihameコマンドとの相性は抜群です。https://github.com/ShellShoccar-jpn/installer

例えば、クエリAPIとmojihameコマンド

テンプレファイルを用意します、template

GET /
QUERY
%1 %2
QUERY
Version 2016-11-15
Host: ec2.ap-northeast-1.amazonaws.com
Content-Type: application/x-www-form-urlencoded

設定の用意、config.txt

Action Hogehoge
AAAAA dededed
hogemoge ahahaha

いざアクセス

cat config.txt | mojihame -lQUERY template - | axs -q | parsrx.sh | 加工

素晴らしいparsrsのコマンドを用いれば、無駄に多くのコマンドを用いずにレスポンスの解析もできます

ちなみに

cat config.txt | mojihame -lQUERY template -

までの結果だけ抜き出すと以下のようになっています。

GET /
Action Hogehoge
AAAAA dededed
hogemoge ahahaha
Version 2016-11-15
Host: ec2.ap-northeast-1.amazonaws.com
Content-Type: application/x-www-form-urlencoded

実際にAWSにaxs(アクセス)してみよう!

以下の内容ではシェルショッカー日本支部の開発しているコマンドを多く使うかもしれません。mojihameコマンドとParsrsは必須です。また、記述の簡略化のために、ヒアドキュメントを多用します。

S3にアクセスして、東京リージョンで静的webサイトのホスティングをしてみよう!

0.好きな名前のbucketをおく(PUTする)

cat <<END | axs
PUT /
Host: BucketName.s3-ap-northeast-1.amazonaws.com
Content-Type: application/xml

<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> 
  <LocationConstraint>ap-northeast-1</LocationConstraint> 
</CreateBucketConfiguration >
END

1.bucketの中に公開するオブジェクトindex.htmlをおく(PUTする)

公開するものなので、誰でも読み込み可能にするために、設定として追加のヘッダx-amz-acl: public-readを付与しています。

cat <<-END | axs -f index.html     (←ローカルファイル)
PUT /index.html
Host: BucketName.s3-ap-northeast-1.amazonaws.com
Content-Type: text/html
x-amz-acl: public-read
END

2.bucketの中に公開するオブジェクトerror.htmlをおく(PUTする)

これは、エラーページ用のhtmlです。

cat <<END | axs -f error.html
PUT /Error.html 
Host: BucketName.s3-ap-northeast-1.amazonaws.com
Content-Type: text/html
x-amz-acl: public-read
END

3.bucketの中に公開するオブジェクトimage.jpgをおいて(PUTする)、やっぱり消す(DELETE)

index.htmlに載せる画像をbucketにputしましたが、気に入らなかったのでやっぱりDELETEすることにしました。

cat <<END | axs -f moon.jpg
PUT /image.jpg
Host: BucketName.s3-ap-northeast-1.amazonaws.com
Content-Type: text/html
x-amz-acl: public-read
END

やっぱり気に入らないので消す

cat <<END | axs
DELETE /image.jpg
Host: BucketName.s3-ap-northeast-1.amazonaws.com
END

4.バケットをウェブサイト用に修正する(PUTする)

s3を静的webサイトとして活用します。

s3のwebサイトホスティングでは、ルーティングルールを使用して、特定のHTTPエラーコードをチェックする条件を指定できます。ルールを追加すれば、リクエストエラーが発生した場合、リクエストを再ルーティングしたりできます。つまりwebサイトの機能を持たせることができるのです。

たとえば、リクエストエラーを処理し、別のホストにルーティングしてみましょう。次のルーティングルールは、HTTPエラー404のイベントでEC2インスタンスにリダイレクトします。

ExamplePage.htmlページをリクエストしてHTTP 404エラーが発生したとします。すると、そのリクエストは、指定されたEC2インスタンスのreport-404/testPage.htmlに再ルーティングされます。ルーティングルールが存在しない状態で、HTTPエラーが発生した場合には、Error.htmlを返します。

cat <<END | axs
PUT /
website
Host: BucketName.s3-ap-northeast-1.amazonaws.com
Content-Type: application/xml

<WebsiteConfiguration xmlns='http://s3.amazonaws.com/doc/2006-03-01/'>
  <IndexDocument>
    <Suffix>index.html</Suffix>
  </IndexDocument>
  <ErrorDocument>
    <Key>Error.html</Key>
  </ErrorDocument>

  <RoutingRules>
    <RoutingRule>
    <Condition>
      <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals >
    </Condition>
    <Redirect>
      <HostName>ec2-11-22-333-44.compute-1.amazonaws.com</HostName>
      <ReplaceKeyPrefixWith>report-404/</ReplaceKeyPrefixWith>
    </Redirect>
    </RoutingRule>
  </RoutingRules>
</WebsiteConfiguration>

5.TIPS:独自ドメインで静的webサイトを公開する場合

上記までの設定を用いて、s3で静的webサイトのホスティングを行うと、webサイトの公開アドレスは、AWS側で決定されたendpoitとなります。例えばs3-website.s3.amazonaws.comのようなものです。

しかし、自分のドメインでwebサイトを公開したいと思う時もあるでしょう。

その場合は、バケット名を独自ドメイン名にして、Route53で設定してあげれば可能です。

例えば、あなたの所得したドメインがwebsite.comだった場合。バケット名をwebsite.comにします。あとは、Route53で設定してあげると、website.comでwebサイトを公開できます。

6.TIPS:独自ドメインのバケットを用いるときの注意

独自ドメインでサイトを公開しない通常の時は、以下のようにホストヘッダにバケットの名前を含めることができます。そしてこちらの方がレファレンス的には正しいです。

cat <<END | axs
PUT /
website
Host: BucketName.s3-ap-northeast-1.amazonaws.com
END

なおこの場合のwebサイトのアドレス以下のようになります。

http://BucketName.s3-website-ap-northeast-1.amazonaws.com

しかし、独自ドメインをバケット名に使用し、ホストヘッダに含める場合は、axsコマンドでは、エンドポイントの名前解決ができません。したがってドメイン名のバケットを作る場合には、ターゲットuriにバケット名を含めます。

cat <<END | axs -f index.html
PUT /website.com/index.html
Host: s3.amazonaws.com
Contetnt-Type: text/html
END

この操作では、us-east-1リージョンにあるwebsite.comバケットに、index.htmlをPUTしています。なおAWS APIで用いるendpointについては、レファレンスを参照してください。サービスとリージョンごとに形式が異なる場合が多いです。
http://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html

7.TIPS:ヒアドキュメントで記述した内容を設定ファイルに残したい

そんな時はteeコマンドを使います 。ヒアドキュメントに記述した内容がconfig_fileに保存されます。

cat <<END | tee config_file | axs
GET /
Host: s3.amazonaws.com
END

Amazon Pollyにアクセスして音読させてみよう!

Amazon Pollyはテキストの音声変換サービスです。誰でも非常に簡単に利用できるサービスで、AWSの中でも敷居は低いです。とりあえずやってみましょう。何かのアプリ開発に役に立つかもしれません。

また、AWSのフルマネージドサービスなので利用者がインフラ構成やコーディングについて心配する必要は微塵もありません。東京リージョンでは利用できないので注意してください。

なお、ここではmojihameコマンドを使います

1.テンプレートファイル speech.templateを用意します。

お馴染みのHTTP形式の設定ファイルのテンプレです。使い回しが可能なので、メンテナンス効率は上がるかもしれません。aws cliのようにパラメータとコマンドをごちゃまぜにしたスクリプトを書く必要がなくなります。

POST /v1/speech
Host: polly.us-east-1.amazonaws.com
Content-Type: application/json

{
  "OutputFormat": "mp3",
  "Text": "%1",
  "VoiceId": "Mizuki"
}

2.pollyにアクセスしておしゃべりしよう!

ここまできたら簡単、あとはAmazon Pollyにアクセスしておしゃべりするだけです。

$echo "こんにちはQiita" | mojihame polly.template - | axs -q > result.mp3

ちなみにaxsコマンドの前までを抜き出すと以下のようになっています。

POST /v1/speech
Host: polly.us-east-1.amazonaws.com
Content-Type: application/json

{
  "OutputFormat": "mp3",
  "Text": "こんにちはQiita",
  "VoiceId": "Mizuki"
}

3.長文の小説もPollyに喋ってもらうことができます。

「小説家になろう」というサイトから小説をもらってきて、pollyに音読させましょう。pollyには文字数制限がありますので、何文字かに区切ってリクエストを出し、返ってきたレスポンスを対象ファイルに上書きしていきましょう。

下記のcurl先のurlのncode下の番号は小説とページによって異なります。

$curl http://ncode.syosetu.com/txtdownload/dlstart/ncode/XXXXXXXX/ | 
tr -d 'n' | 
sed 's/.{250}/&@/g' | 
tr '@' 'n' | 
while read LINE; do 
echo $LINE | mojihame polly - | axs -q ; 
done >> polly.mp3

EC2にアクセスしてインスタンスを立ててみよう!

このセクションではmojihameコマンドとparsrx.shを使います。

0.テンプレートファイルec2.templateの用意

以下のファイルはmojihameコマンドで使います。

GET /
QUERY
%1 %2
QUERY
Version 2016-11-15
Host: ec2.ap-northeast-1.amazonaws.com
Content-Type: x-www-form-urlencoded

1.VPCを作ろう

cat <<END                       | 
Action CreateVpc
CidrBlock 10.0.0.0/16
END
mojihame -lQUERY ec2.template - | 
axs -q                          |
parsrx.sh                       | 
grep 'vpcId'                    | 
awk '{print $2}'

(結果としてvpcIdが表示されます)

ヒアドキュメントではなく、config_file1に設定項目であるAction CreateVpcとCidrBlock 10.0.0.0/16を記述したとしたら以下のようになワンライナーになります。

 cat config_file1 | mojihame -lQUERY ec2.template - | axs -q | parser.sh | grep 'vpcId' | awk '{print $2}'

2.puclic-subnetを作ろう

作成したvpcにinternet gatewayをアタッチし、パブリックサブネットを作成します。以下省略

3.ec2インスタンスを立てよう

作成したpublic-subnet内にec2インスタンスを作成します。以下省略

今回は面倒臭かったので色々省略しましたが、1、2、3の流れは全て簡潔で似通っています。次回があれば、AWS上に3-tierアーキテクチャを構成する完全自動化スクリプトを組んでみたいと思います。もちろんaxsコマンドを使って。

Amazon Rekognitionにアクセスして画像解析してみよう!

Amazon Rekognitionは、深層学習により、画像解析を行なってくれるAWSのフルマネージドサービスです。ユーザーがコーディングの心配をすることなく、簡単にすぐに、どこでも利用することができます。非常に利便性が高く、将来必要不可欠になるであろうサービスの一つです。

1.S3にバケットを配置して、顔を含んだjpeg画像を置いてみる

リージョンはusを選択してください。他リージョンではまだサービスを開始していません。

  • バケットを作る
cat <<END | axs
PUT /
Host: s3-to-rekognition.s3.amazonaws.com
  • 画像ファイルを配置する
cat <<END | axs -f my-face.jpg
PUT /my-face.jpg
Host: s3-to-rekognition.s3.amazonaws.com
Content-Type: image/jpeg

2.Rekognitionで画像解析して、要素を抽出してみる

これにはDetectLabels APIを使用します。

DetectLabelsは、入力として提供される画像(JPEGまたはPNG)内における実世界の要素を検出して、ラベルを付与します。花、木、テーブルなどのオブジェクトは当然含まれ、さらには、結婚式、卒業、誕生日パーティーのようなイベントシーン、風景、夕方、自然などの概念までが含まれます。

結果は整形されていないjsonで帰ってきます。これでは読み取りづらいので、ヘッダ情報を排して、jsonパーサーにかけてしまいます。もちろん、POSIX原理主義jsonパーサーparsrj.shを使って。

cat <<END | axs -q | parsj.sh
POST /
Host: rekognition.us-east-1.amazonaws.com
X-Amz-Target: RekognitionService.DetectLabels
Content-Type: application/x-amz-json-1.1

{
   "Image":{
      "S3Object":{
         "Bucket":"s3-to-rekognition",
         "Name":"my-face.jpg"
      }
   }
}
END

私の顔画像をアップロードして解析した結果は以下のようになりました。

$.Labels[0].Confidence 99.2739486694336
$.Labels[0].Name Human
$.Labels[1].Confidence 99.27888488769531
$.Labels[1].Name People
$.Labels[2].Confidence 99.27890014648438
$.Labels[2].Name Person
$.Labels[3].Confidence 82.6961669921875
$.Labels[3].Name Dimples
$.Labels[4].Confidence 82.6961669921875
$.Labels[4].Name Face
$.Labels[5].Confidence 82.6961669921875
$.Labels[5].Name Smile
$.Labels[6].Confidence 54.255836486816406
$.Labels[6].Name Selfie
$.Labels[7].Confidence 50.784881591796875
$.Labels[7].Name Female
$.Labels[8].Confidence 50.784881591796875
$.Labels[8].Name Girl
$.Labels[9].Confidence 50.784881591796875
$.Labels[9].Name Woman
$.Labels[10].Confidence 50.74814987182617
$.Labels[10].Name Glasses
$.Labels[11].Confidence 50.74814987182617
$.Labels[11].Name Goggles

多分、私は二重で目が大きいので上のような結果になってしまったのだと思います。サングラスとか眼鏡の可能性が指摘されてます。ちなみに顔アップの自撮り写真をあげたのですが、それも認識されているようですね。えくぼとスマイルまで認識されています。rekognitionには女顔として認識されたようでした。

以上のようにかなり詳しく要素を検出してくれます。別の例ではテーブルクロスなども検出していました。他にもAmazon Rekognitionでは顔の判別や比較、リストができます。ですので、例えば、普段見ない顔を検出した時にアラートを送信するようなアプリケーションを作れば、自作警備システムなんかも作成できるかもしれませんね!東京オリンピックに備えて笑

まとめ

さて、これでクラウドの力を借りれば、POSIX原理主義でも、様々なアプリケーションを開発できることがわかりましたね!さらには、機械学習や深層学習の力を借りた強力なものまでも、比較的簡単にできちゃうことがわかったと思います!しかも環境やバージョンに悩まされることなしで!

わーい!POSIX原理主義シェルスクリプトは楽しいですね!

※ここまでの流れで、AWS自体がPOSIX原理主義と対極じゃんて声が聞こえてきそうですが、そこはあまり関係ありません。AWSは道具というよりサービスです。ほらAmazon Web Servicesって言います。それにフルマネージドなサービスを多く活用するようにすれば、バージョンアップによる不具合とは比較的無縁です。

ということで、私は、実は、POSIX原理主義は、クラウドととても相性がいいのではないかと勝手に思っています(素人考え)。

「必要なもの」POSIX準拠コマンドと交換可能性を担保したコマンド

  • openssl ver1.0.0以上
  • utconv (同梱、秘密結社シェルショッカー日本支部)
  • urlencode (同梱、秘密結社シェルショッカー日本支部)
  • cat
  • awk
  • sed
  • printf
  • echo
  • grep
  • curl
  • wget
  • eval

感想

意味がるのか知りません。あるかもしれないし、ないかもしれません。ただ楽しかったですとだけ付け加えておきます。

「シェルかっこいい、クラウドかっこいい」
→「なんか作りたい」
→「POSIX原理主義すげー」
→「POSIX原理主義でもクラウドwebサービスの力を借りたい!」
→「一番サービスが充実していそうなAWSを操りたい」
→「awsコマンド的なものをPOSIX原理主義で開発しよう!」
という動機で始めました。作者は初Qiita,教育学部卒業したてのずぶの素人で数ヶ月前までIT系とは無縁でした。技術的な誤りを多分に含んでいる可能性があります。その時は指摘してくださると助かります。

続きを読む

Amazon DynamoDB を Python から使ってみる

0.はじめに

AWS がサーバーレスで推奨しているサービスを、色々と使ってみます。
今回は、Amazon DynamoDB を Python から使ってみます。

1.Mac に Python 環境を構築する

そもそも環境が無い!! ってことで、環境構築からです。

以下を参考にして(っていうか、そのままですが…)、環境を構築します。

.bash_profile の設定をしておかないと、Python のバージョンの切替のコマンドを毎回叩かないといけないみたいなので、こちらも設定します。

2.Mac の Python 環境から、Amazon DynamoDB を叩くスクリプトを作成する

以下を参考にして、Python スクリプトを作成します。

ローカルの Mac からですので、IAM Role は利用出来ないですし、スクリプト一つで全て完結させたかったので、こちらの認証のやり方を使います。

import boto3
from boto3.session import Session

accesskey = "YOUR_ACCESSKEY"
secretkey = "YOUR_SECRETKEY"
region    = "YOUR_REGION"

session = Session(
                  aws_access_key_id=accesskey,
                  aws_secret_access_key=secretkey,
                  region_name=region)

3.AWS Documentation の Amazon DynamoDB 入門ガイド に沿って、DynamoDB への操作を確認する

もう AWS さんの仰る通りに操作を確認します。

  1. ステップ 1: テーブルを作成する – Amazon DynamoDB
  2. ステップ 2: サンプルデータをロードする – Amazon DynamoDB
  3. ステップ 3: 項目を作成、読み込み、更新、削除する – Amazon DynamoDB
  4. ステップ 4: データをクエリおよびスキャンする – Amazon DynamoDB
  5. ステップ 5: (オプション) テーブルを削除する – Amazon DynamoDB

以下、サンプルです。
メイン関数の呼び出しをコメントにしたりして、確認しました。

正直、Python 使い慣れていないので、こういう書き方でいいのかよくわかりませんが…。

SamplaDynamoDB.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# ---1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----
# ==============================================================================
#
# SampleDynamoDB.py
#
#   * ex) $ ./SamplaDynamoDB.py
#
# ==============================================================================

import sys
import io
import logging
import json
import decimal
import time

import datetime
from datetime import datetime as dt

import boto3
from boto3.session import Session
from boto3.dynamodb.conditions import Key, Attr

# Helper class to convert a DynamoDB item to JSON.
class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            if o % 1 > 0:
                return float(o)
            else:
                return int(o)
        return super(DecimalEncoder, self).default(o)

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
logging.basicConfig(level=logging.INFO, filename=(__file__ + ".log"), format="%(asctime)s %(levelname)s %(filename)s %(lineno)d %(funcName)s | %(message)s")

# ------------------------------------------------------------------------------
# Set
# ------------------------------------------------------------------------------
tmp_today = datetime.datetime.today()

# AWS
accesskey = "[アクセスキー ID]"
secretkey = "[シークレットアクセスキー]"
region = "ap-northeast-1"
session = Session(aws_access_key_id=accesskey, aws_secret_access_key=secretkey, region_name=region)
dynamodb = session.resource('dynamodb')

# ------------------------------------------------------------------------------
# 「ステップ 1: テーブルを作成する - Amazon DynamoDB」
# <http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Python.01.html>
# ------------------------------------------------------------------------------
def MoviesCreateTable():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.create_table(
            TableName='Movies',
            KeySchema=[
                {
                    'AttributeName': 'year',
                    'KeyType': 'HASH'  #Partition key
                },
                {
                    'AttributeName': 'title',
                    'KeyType': 'RANGE'  #Sort key
                }
            ],
            AttributeDefinitions=[
                {
                    'AttributeName': 'year',
                    'AttributeType': 'N'
                },
                {
                    'AttributeName': 'title',
                    'AttributeType': 'S'
                },
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 10,
                'WriteCapacityUnits': 10
            }
        )
        logging.info("Table status : [%s]", table.table_status)
    except Exception as e:
        logging.error("Type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

# ------------------------------------------------------------------------------
# 「ステップ 2: サンプルデータをロードする - Amazon DynamoDB」
# <http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Python.02.html>
# ------------------------------------------------------------------------------
def MoviesLoadData():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        with open("moviedata.json") as json_file:
            movies = json.load(json_file, parse_float = decimal.Decimal)
            for movie in movies:
                year = int(movie['year'])
                title = movie['title']
                info = movie['info']
                #logging.info("Adding Movie | Year:[%s], Title:[%s]", year, title)
                table.put_item(
                   Item={
                       'year': year,
                       'title': title,
                       'info': info,
                    }
                )
    except Exception as e:
        logging.error("Type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

# ------------------------------------------------------------------------------
# 「ステップ 3: 項目を作成、読み込み、更新、削除する - Amazon DynamoDB」
# <http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Python.03.html>
# ------------------------------------------------------------------------------
def MoviesItemOps01():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        title = "The Big New Movie"
        year = 2015
        response = table.put_item(
           Item={
                'year': year,
                'title': title,
                'info': {
                    'plot':"Nothing happens at all.",
                    'rating': decimal.Decimal(0)
                }
            }
        )
        logging.info("PutItem succeeded:")
        logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
    except Exception as e:
        logging.error("type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

def MoviesItemOps02():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        title = "The Big New Movie"
        year = 2015
        try:
            response = table.get_item(
                Key={
                    'year': year,
                    'title': title
                }
            )
        except ClientError as e:
            logging.info(e.response['Error']['Message'])
        else:
            item = response['Item']
            logging.info("GetItem succeeded:")
            logging.info(json.dumps(item, indent=4, cls=DecimalEncoder))
    except Exception as e:
        logging.error("type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

def MoviesItemOps03():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        title = "The Big New Movie"
        year = 2015
        response = table.update_item(
            Key={
                'year': year,
                'title': title
            },
            UpdateExpression="set info.rating = :r, info.plot=:p, info.actors=:a",
            ExpressionAttributeValues={
                ':r': decimal.Decimal(5.5),
                ':p': "Everything happens all at once.",
                ':a': ["Larry", "Moe", "Curly"]
            },
            ReturnValues="UPDATED_NEW"
        )
        logging.info("UpdateItem succeeded:")
        logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
    except Exception as e:
        logging.error("type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

def MoviesItemOps04():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        title = "The Big New Movie"
        year = 2015
        response = table.update_item(
            Key={
                'year': year,
                'title': title
            },
            UpdateExpression="set info.rating = info.rating + :val",
            ExpressionAttributeValues={
                ':val': decimal.Decimal(1)
            },
            ReturnValues="UPDATED_NEW"
        )
        logging.info("UpdateItem succeeded:")
        logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
    except Exception as e:
        logging.error("type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

def MoviesItemOps05():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        title = "The Big New Movie"
        year = 2015
        logging.info("Attempting conditional update...")
        try:
            response = table.update_item(
                Key={
                    'year': year,
                    'title': title
                },
                UpdateExpression="remove info.actors[0]",
                ConditionExpression="size(info.actors) > :num",
                ExpressionAttributeValues={
                    ':num': 2
                },
                ReturnValues="UPDATED_NEW"
            )
        except ClientError as e:
            if e.response['Error']['Code'] == "ConditionalCheckFailedException":
                logging.error(e.response['Error']['Message'])
            else:
                raise
        else:
            logging.info("UpdateItem succeeded:")
            logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
    except Exception as e:
        logging.error("type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

def MoviesItemOps06():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        title = "The Big New Movie"
        year = 2015
        logging.info("Attempting a conditional delete...")
        try:
            response = table.delete_item(
                Key={
                    'year': year,
                    'title': title
                },
                ConditionExpression="info.rating <= :val",
                ExpressionAttributeValues= {
                    ":val": decimal.Decimal(8)
                }
            )
        except ClientError as e:
            if e.response['Error']['Code'] == "ConditionalCheckFailedException":
                logging.info(e.response['Error']['Message'])
            else:
                raise
        else:
            logging.info("DeleteItem succeeded:")
            logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
    except Exception as e:
        logging.error("type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

# ------------------------------------------------------------------------------
# 「ステップ 4: データをクエリおよびスキャンする - Amazon DynamoDB」
# <http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Python.04.html>
# ------------------------------------------------------------------------------
def MoviesQuery01():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        logging.info("Movies from 1933")
        response = table.query(
            KeyConditionExpression=Key('year').eq(1933)
        )
        logging.info("Query01 succeeded:")
        logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
        for i in response['Items']:
            logging.info("%s : %s", i['year'], i['title'])
    except Exception as e:
        logging.error("Type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

def MoviesQuery02():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        logging.info("Movies from 1992 - titles A-L, with genres and lead actor")
        response = table.query(
            ProjectionExpression="#yr, title, info.genres, info.actors[0]",
            ExpressionAttributeNames={ "#yr": "year" }, # Expression Attribute Names for Projection Expression only.
            KeyConditionExpression=Key('year').eq(1992) & Key('title').between('A', 'L')
        )
        logging.info("Query02 succeeded:")
        logging.info(json.dumps(response, indent=4, cls=DecimalEncoder))
        for i in response[u'Items']:
            logging.info(json.dumps(i, cls=DecimalEncoder))
    except Exception as e:
        logging.error("Type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

# ------------------------------------------------------------------------------
# 「ステップ 5: (オプション) テーブルを削除する - Amazon DynamoDB」
# <http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Python.05.html>
# ------------------------------------------------------------------------------
def MoviesDeleteTable():
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)
    try:
        table = dynamodb.Table('Movies')
        table.delete()
    except Exception as e:
        logging.error("Type : %s", type(e))
        logging.error(e)
    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
if __name__ == '__main__':
    logging.info("<<<<<<<< %s Start >>>>>>>>", __name__)

    # Set
    logging.info("REGION : [%s]", region)

    # Args
    logging.info("Argc : [%d]", len(sys.argv))
    for i in range(len(sys.argv)):
        logging.info("Argv[%d] : [%s]", i, sys.argv[i])

    # Check Table
    table = dynamodb.Table('Movies')
    logging.info("Table :")
    logging.info(table)

    # Create Table
    #MoviesCreateTable()
    #time.sleep(9)

    # Load Json Data
    #MoviesLoadData()

    # Query Data
    MoviesItemOps01()
    MoviesItemOps02()
    MoviesItemOps03()
    MoviesItemOps04()
    #MoviesItemOps05()
    #MoviesItemOps06()

    # Query Data
    MoviesQuery01()
    MoviesQuery02()

    # Delete Table
    #MoviesDeleteTable()

    logging.info("<<<<<<<< %s End >>>>>>>>", __name__)

# ---1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----

99.ハマりポイント

  • 入門ガイドのソースをそのまま貼っ付けて確認しただけなので、ほぼ無いです。
  • ただ、ちゃんと書こうと思ってロギングの機能を盛り込んだんですが、何せ Python あまり使ったことが無いので、その辺引っ掛かったりしました。

XX.まとめ

今回は、とにかく全部入門ガイドにソースがあったので、有難かったです。

次は、Amazon Lambda 起動を試そうかなと。

続きを読む

MacでAWS CLIをインストール

sudo easy_install pip
sudo -H pip install awscli --upgrade --ignore-installed six #参考[1]

aws --version
# versionが表示されればOK- aws-cli/1.11.75 Python/2.7.10 Darwin/16.4.0 botocore/1.5.38

aws configure #認証情報を対話的に設定する[2]

AWS Access Key ID [None]: XXXX
AWS Secret Access Key [None]: XXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: text

[1]OSX El Capitanでawscliのインストールに失敗する場合の対処方法 – Qiita

[2]【初心者向け】MacユーザがAWS CLIを最速で試す方法 | Developers.IO

続きを読む

Yahoo Flickr Creative Commons 100M (YMCC100M) データセットを使う

YFCC100M とは

詳細は arXiv 論文 https://arxiv.org/pdf/1503.01817.pdf に記載されていますが,以下では簡単にその内容を紹介します.

  • およそ1億枚のラベル付き画像(と80万個の動画)のデータセットです.
  • Flickr に Creative Commons ライセンスでアップロードされた画像(と動画)のみから構成されているため,研究目的に限ればライセンスのことを気にかける必要はありません.
  • ラベルは,あらかじめ画像から判別可能な1570種類だけ選別されています.
  • 画像トラベルだけではなく,位置情報・撮影時刻・カメラスペックなどの多様なメタデータが,多くの画像に付与されています.

データセットの場所

http://www.yfcc100m.org にあるように見えるが,ここはデータセットの中身をざっと見るためのweb interfaceを提供しているに過ぎない.実際に行かないといけない場所は Yahoo! Research の Image Dataset のページ.

http://webscope.sandbox.yahoo.com/catalog.php?datatype=i&did=67

データセットをダウンロード,する前に

いくつかのことを前もってしておかなければならない.Yahoo! のデータセットではあるが,データが置かれているのは Amazon Web Services の上なので,両方のアカウントを作り,AWS経由でのアクセス権限をもらう必要がある.

  1. Yahoo! アカウントを作成する.
  2. Amazon Web Services (AWS) のアカウントを作成する.
  3. Yahoo! データセット利用の申請を行う.
  4. AWS アクセスのための秘密鍵を生成する.
  5. AWS アクセスのためのコマンドラインツール s3cmd をインストールする.

Yahoo! アカウントの作成

普通に作成すれば良いだけ.

AWS アカウントの作成

ここも特に問題にはならない.住所やクレジットカードなどの情報を入れる必要があるので注意.データセットをダウンロードするという目的においては,特にお金を払う必要はないので,その点は安心しても良い.

Yahoo! データセット利用の申請

データセットを利用する目的について比較的詳しく記入する必要がある.問題のない程度に正直に書いた方が良い気がするが,即時に approval が来ることから考えると,メールドメイン以外はまじめに見ていない可能性もある.

この申請の際に,AWS canonical identifierを入力せよ,という指示が来るが,その際には, https://console.aws.amazon.com/iam/home?#/security_credential の「アカウントID」から「正規ユーザーID」を見つけ,その64桁の英数字の列を入力すれば良い.

何も問題がなければ,申請はほぼ即時に受理される.登録したメールアドレスに,これ以降の手順を説明したメールが届くはずである.

AWSアクセス秘密鍵の生成

この後のコマンドラインツールの設定の際に必要となる.

https://console.aws.amazon.com/iam/home?#/security_credential の「アクセスキー(アクセスキー ID とシークレットアクセスキー)」を開き,もし何もなければ新規に生成する.すでにあれば,それを転用しても良い.

s3cmdのインストール

AWSへのアクセスをコマンドライン上から行うツールである s3cmd をインストールする.ダウンロードは http://s3tools.org/download から行い,各プラットフォームに適したパッケージをダウンロードする.

Windowsでは,ビルド済の実行ファイルがあるはずなので,それを利用する.LinuxやMacでは,python pipを用いて,以下のように一発でインストールができる.

% sudo pip install s3cmd

続いて,AWSのアクセス情報をs3cmdに設定する.

% s3cmd --configure

Enter new values or accept defaults in brackets with Enter.
Refer to user manual for detailed description of all options.

Access key and Secret key are your identifiers for Amazon S3. Leave them empty for using the env variables.
Access Key : [AWSのアクセスキーIDを入れる] 
Secret Key : [AWSのシークレットアクセスキーIDを入れる]
Default Region : [Japanとか入れれば大丈夫]

Encryption password is used to protect your files from reading
by unauthorized persons while in transfer to S3
Encryption password : [何らかのパスワードを入れて覚えておく.たぶん使わない.] 
Path to GPG program : [gpgの絶対パスを入れる.なければインストール.] 

When using secure HTTPS protocol all communication with Amazon S3
servers is protected from 3rd party eavesdropping. This method is
slower than plain HTTP, and can only be proxied with Python 2.7 or newer
Use HTTPS protocol [Yes]: [ここは素直にyes]

On some networks all internet access must go through a HTTP proxy.
Try setting it here if you can't connect to S3 directly
HTTP Proxy server name: [プロキシを使っている場合にはそのサーバ名とポート番号を入れる.]

New settings:
  Access Key: ....................
  Secret Key: ....................
  Default Region: Japan
  Encryption password: ...........
  Path to GPG program: /usr/local/bin/gpg
  Use HTTPS protocol: True
  HTTP Proxy server name: 
  HTTP Proxy server port: 0

Test access with supplied credentials? [Y/n] [何も入力せずリターン]
Please wait, attempting to list all buckets...
Success. Your access key and secret key worked fine :-)

Now verifying that encryption works...
Success. Encryption and decryption worked fine :-)

Save settings? [y/N] [yを入力,設定が保存される.]
Configuration saved to '/Users/akisato/.s3cfg'

データセットをダウンロード

これでようやくデータセットをダウンロードする準備が整いました.順次進めていきます.

メタデータのダウンロード

まず,データセットの中身と総量を確認します.

% s3cmd ls -H s3://yahoo-webscope/I3set14/
2017-03-08 16:54        10k  s3://yahoo-webscope/I3set14/WebscopeReadMe.txt
2017-03-08 16:54         2G  s3://yahoo-webscope/I3set14/yfcc100m_autotags.bz2
2017-03-08 16:54        13G  s3://yahoo-webscope/I3set14/yfcc100m_dataset.bz2
2017-03-08 16:56         9G  s3://yahoo-webscope/I3set14/yfcc100m_exif.bz2
2017-03-08 16:57      1710M  s3://yahoo-webscope/I3set14/yfcc100m_places.bz2

上記のデータ量からお察しの通り,この中には画像や動画は含まれていません.メタデータなどをダウンロードした後に,個別に落としていきます.メタデータなどをすべて一括でダウンロードするには,

% s3cmd get --recursive s3://yahoo-webscope/I3set14/

何らかの理由で途中から再開する場合には,

% s3cmd get --recursive s3://yahoo-webscope/I3set14/ --continue

すべての bz2 ファイルを解凍します.いずれのファイルも1行1画像に対応しており,タブ区切りで内容が記載されています.

% find . -name "*.bz2" | xargs -L1 -n1 -I{} bzip2 -vd {}
% mv yfcc100m_autodags yfcc100m_autotags.csv
% mv yfcc100m_dataset yfcc100m_dataset.csv
% mv yfcc100m_exif yfcc100m_exif.csv
% mv yfcc100m_places yfcc100m_places.csv

yfcc100m_autotags は AleXNet + 線形SVM を用いた各ラベルのスコア(0.5以上のもののみ),yfcc100m_dataset はデータセットの構成,yfcc100m_exif は画像のEXIF情報,yfcc100m_places は位置情報が,それぞれ含まれています.

画像のダウンロード

画像・動画の場所が記載されているのは, yfcc100_dataset になります.1行1画像に対応しており,タブ区切りでフィールドがあります.フィールドの説明は,WebscopeReame.txt にあります.

% less yfcc100m_dataset.csv
0       6985418911      4e2f7a26a1dfbf165a7e30bdabf7e72a        39089491@N00    nino63004       2012-02-16 09:56:37.0   1331840483      Canon+PowerShot+ELPH+310+HS
     IMG_0520                canon,canon+powershot+hs+310,carnival+escatay,cruise,elph,hs+310,key+west+florida,powershot             -81.804885      24.550558
       12      http://www.flickr.com/photos/39089491@N00/6985418911/   http://farm8.staticflickr.com/7205/6985418911_df7747990d.jpg    Attribution-NonCommercial-NoDerivs License      http://creativecommons.org/licenses/by-nc-nd/2.0/       7205
    8       df7747990d      692d7e0a7f      jpg     0
...

この yfcc100m_dataset.csv を parse して,画像をダウンロードするスクリプトを書いていきます.1億行もあるテキストファイルなので,一気に読み込まないように.ダウンロードする際に必要なのは, photo/video identifier(2番目)とphoto/video download URL(17番目)です.

yfcc_download.py
import urllib2
import os.path
import subprocess

def split_str(s, n):
    length = len(s)
    return [ s[i:i+n] for i in range(0, length, n) ]

def img_download(url, filename):
    img = urllib2.urlopen(url)
    fout = open(filename, 'wb')
    fout.write(img.read())
    img.close()
    fout.close()

fin = open('./yfcc100m_dataset.csv')
imgdir = './img'

print 'Start downloading YFCC100M dataset...'
for line in fin:
    line_split = line.strip().split('\t')
    line_num = int(line_split[0])
    photo_id = int(line_split[1])    # photo id
    photo_url = line_split[16]    # photo URL for downloading
    photo_ext = os.path.splitext(photo_url)[1]
    if photo_ext=='':
        photo_ext = '.mp4'
    split_photo_id = split_str(str(photo_id), 3)
    photo_dir = os.path.join(imgdir, split_photo_id[0], split_photo_id[1])
    photo_name = os.path.join(photo_dir, str(photo_id)+photo_ext)
    if os.path.isfile(photo_name) and os.path.getsize(photo_name):
        print 'Line %d, id %d, skipped' % (line_num, photo_id)
        continue    # avoid duplicate downloading
    print 'Line %d, id %d, download' % (line_num, photo_id)
    try:
        subprocess.call('mkdir -p ' + photo_dir, shell=True)
        img_download(photo_url, photo_name)
    except:
        print 'Failed'

あとは,このスクリプトを用いて,順次画像をダウンロードしていくだけです.論文の記載にもありますが,画像で13TB,動画も含むと16TBのディスク容量が必要です.

% python yfcc_download.,py

metadateを整形する

すべてのメタデータ(autotags, exif, places)は,photo/video identifier によって紐付けをされています.つまり,photo/video identifierをキーとして,それぞれのメタデータにアクセスすることができます.

このことを利用して,以下のスクリプトで,メタデータを保存するJSONを画像ごとに作成します.これで,巨大なメタデータにアクセスする必要がなくなります.

yfcc_createmeta.py
import json
import os.path
import subprocess

def split_str(s, n):
    length = len(s)
    return [ s[i:i+n] for i in range(0, length, n) ]

def extract_metadata(elems):
    if len(elems)<2: return None
    text = elems[1]
    text_split = text.split(',')
    d = dict()
    for elem in text_split:
        key = elem.split(':')[0]
        val = elem.split(':')[1]
        d[key] = val
    return d

def extract_metadata_d(elems):
    if len(elems)<25: return None
    d = dict()
    d['photo_hash'] = elems[2]
    d['user_id'] = elems[3]
    d['user_nickname'] = elems[4]
    d['date_taken'] = elems[5]
    d['date_uploaded'] = elems[6]
    d['capture_device'] = elems[7]
    d['title'] = elems[8]
    d['description'] = elems[9]
    d['user_tags'] = elems[10]
    d['machine_tags'] = elems[11]
    d['longitude'] = elems[12]
    d['latitude'] = elems[13]
    d['pos_accuracy'] = elems[14]
    d['url_show'] = elems[15]
    d['url_get'] = elems[16]
    d['license_name'] = elems[17]
    d['license_url'] = elems[18]
    d['server_id'] = elems[19]
    d['farm_id'] = elems[20]
    d['photo_secret'] = elems[21]
    d['photo_secret_orig'] = elems[22]
    d['photo_ext'] = elems[23]
    d['photo_or_video'] = elems[24]
    return d

fin_autotag = open('yfcc100m_autotags.csv')
fin_exif    = open('yfcc100m_exif.csv')
fin_places  = open('yfcc100m_places.csv')
fin_dataset = open('yfcc100m_dataset.csv')
metadir = './meta'

while True:
    # read lines
    line_a = fin_autotag.readline()
    line_e = fin_exif.readline()
    line_p = fin_places.readline()
    line_d = fin_dataset.readline()
    if (not line_a) or (not line_e) or (not line_p) or (not line_d):
        break
    line_a_split = line_a.strip().split('\t')
    line_e_split = line_e.strip().split('\t')
    line_p_split = line_p.strip().split('\t')
    line_d_split = line_d.strip().split('\t')
    # check photo ID
    photo_id_a = int(line_a_split[0])
    photo_id_e = int(line_e_split[0])
    photo_id_p = int(line_p_split[0])
    photo_id_d = int(line_d_split[1])
    if photo_id_a!=photo_id_e or photo_id_e!= photo_id_p or photo_id_p!=photo_id_d:
        print 'Photo ID mismatched.'
        continue
    photo_id = photo_id_a
    # check existing files
    split_photo_id = split_str(str(photo_id), 3)
    json_dir = os.path.join(metadir, split_photo_id[0], split_photo_id[1])
    json_path = os.path.join(json_dir, str(photo_id)+'_meta.json')
    if os.path.isfile(json_path) and os.path.getsize(json_path):
        print 'Photo ID %d metadata already exists, skip.' % photo_id
        continue
    print 'Photo ID %d metadata creating...' % photo_id
    subprocess.call('mkdir -p ' + json_dir, shell=True)
    # extract metadata
    autotags = extract_metadata(line_a_split)
    exif = extract_metadata(line_e_split)
    places = extract_metadata(line_p_split)
    othermeta = extract_metadata_d(line_d_split)
    # form JSON data and write it to a file
    json_data = dict()
    if autotags: json_data['autotags'] = autotags
    if exif: json_data['EXIF'] = exif
    if places: json_data['places'] = places
    if othermeta: json_data['othermeta'] = othermeta
    with open(json_path, 'wb') as fout:
        json.dump(json_data, fout, sort_keys=True, indent=4)

データセットを使う

ここまでの操作で, ./img/[id].jpg (動画の場合には .mp4) と ./meta/[id]_meta.json という,画像とメタデータの対ができているはずなので,あとはタスクや必要性に応じて利用すれば良い.

続きを読む