【swift】AVFoundationを使って、録音、動画の撮影を行う!

はじめに

最近、CoreMLという「学習済み機械学習モデル」を簡単にiOSに実装できるという技術に興味を持っております。
中でもVisionAPIを使用すれば、画像、動画から人間の骨格や特定の動作を検出するといったことの再現が可能になります。

私たちはスマホのカメラアプリを使って写真や動画を撮影することがありますが、CoreMLの技術を搭載することで今まで見えなかったものも見えるようになると考えています(すごい!)。

CoreMLは音声や文章、画像などを解析するAPIを持っているのですが、前提としてこれらのAPIには「インプットとなる情報」が渡されています。

ですので、この「インプットとなる情報」を収集するロジックについても、最低限の実装方法は理解する必要があると考えております。


今回の記事では「AVFoundation」を使用して、そのインプットとなる情報(画像、動画)を取得する方法について概念をまとめます。

AVFoundationについて

参考

本記事では、以下のドキュメントを参考にしております。
developer.apple.com

概念

本ライブラリを使用すると、
iPhoneのカメラやマイクを起動して「音声」や「画像」といった「入力データ」として取得したデータを、必要に応じて加工したり保存する(アウトプット)ことが可能になります。

このAVFoudationのポイントは、「インプット」と「アウトプット」、そしてそれらを管理する「セッション」になります。
以下画像はCameras and Media Captureから引用したものですが、まずはこの図について理解していく必要があります。

https://docs-assets.developer.apple.com/published/058e665a6c/4ecf0924-ea2b-4faa-aea8-7bfc0b3fe419.png

セッションの構成要素

上記画像にも記載してあますが、AVFoundationを使用するために必要な要素は以下三つになります。
・AVCaptureInput
・AVCaptureOutput
・AVCaptureSession

「AVCaptureInput」、「AVCaptureOutput」の実態はただのabstractクラスでして、インプット、アウトプットに必要な変数・メソッドが定義されています。
そして、このabstractクラスを実装している「インプット」、「アウトプット」クラスはいろんな形で用意されています。

例えば、「AVCaptureInput」を継承する「AVCaptureDevice」クラスは、「カメラ」と「マイク」を起動して、音声つきの動画の撮影が可能となります。
→詳細は後述しますが、AVCaptureInputは「Port」変数を所持していて、この「Port」に対してインプットの情報を格納していきます。

次に、アウトプットとして「AVCaptureOutput」を継承したクラスを用いて、インプットで取得したデータをどう出力するか?設定します。
「AVCaptureMovieFileOutput」では音声つきの動画を「.mov」ファイルとして保存しますが、「AVCaptureAudioFileOutput」では「音声」のみを指定した拡張子に変換して保存します。


最後に「AVCaptureSession」、これは上記のインプット、アウトプットに関する「セッション」を制御していく役目を担っていて、上記で説明したようなインプットとアウトプットの紐付けや、動画撮影などの「開始・終了タイミング」を制御をします。


まとめです。

AVFoundationでは、「AVCaptureInput」によって動画や音声を記録することができますが、それを最終的にどう扱いたいか目的に応じて「AVCaptureOutput」を選択する必要がある。
また、AVCaptureSessionでは「AVCaptureInput」と「AVCaptureOutput」に対するセッション管理などを担っている。

インプットに関わる「ポート」の説明

概念

先ほど「ポート」について軽く触れましたが、ポートは「どの端子でどういったデータを取得するか?」を定義するもの、と考えれば良いのかなと思います。

以下コードは、「AVCaptureInput」を継承したクラス(AVCaptureDevice)に対して、ポートを定義する方法を示します。

// ポートの定義
guard let captureDevice = AVCaptureDevice.default(.builtInDualCamera, for: AVMediaType.video, position: .front) else {
        return
}
// AVCaptureDeviceInputクラスの定義(ポートを設定している)
guard  let videoInput = try? AVCaptureDeviceInput(device: captureDevice) else {
    return
}
// portsメソッド(AVCaptureInputクラスのabstractメソッド)でも、ポート設定が可能
videoInput.ports(for: AVMediaType.video, sourceDeviceType: AVCaptureDevice.DeviceType.builtInDualCamera, sourceDevicePosition: AVCaptureDevice.Position.back)

上記のコードで何となくイメージが掴めると思いますが、「AVCaptureInput」に対して一つポートを設定しています。

AVMediaType.video 動画で撮影することの定義
AVCaptureDevice.DeviceType.builtInTripleCamera トリプルカメラを使用することの定義
AVCaptureDevice.Position.back 背面カメラを使用することの定義


ちなみに、「AVCaptureInput」は、一つ以上の「ポート」の設定が可能です。
「音声付きで動画を撮影したい」というケースには、「カメラ用のポート」と「マイク用のポート」の二つを用意してあげないといけない、ということになりますね。


※余談ですが、Portのドキュメントのページには「Data Stream」という単語があり、直訳すると「データの流れ」ということでなかなかイメージしずらかったです。
ですので、私は「Data Stream = "どの端子でどういったデータを取得するか?"を定義するもの」と変換して理解しました。

Apple Developer Documentation

最後に

さて、今回はざっくりとAVFoundationについて説明しましたが、今現在適当なサンプルコードも作成しているところです。
実際にコードを書いてみて、気づきがあれば本記事をアップデート行こうと思います。


ここまで見ていただきありがとうございました!🙏

【Firebase】匿名ログインに必要な処理と考え

はじめに

本日はFirebaseのAuth機能における「匿名ログイン」についてまとめようと思います。

私のアプリ「Myトレ」では、アプリ起動時に「匿名ユーザ」にてログインするようになっています。
その後、「アカウント作成」により「匿名ユーザ」「永久ユーザ」に昇格することが可能となります。

永久ユーザにすることで「別の端末でログインしてもデータ引き継ぎ可能!」といった仕組みが再現可能になります(今となっては当たり前の仕組み!)。

この「匿名ユーザ」でのログイン機能は大変便利なのですが、運用の仕方によっては別途追加で機能を実装しないといけないいけないケースも発生します。
ですので、本記事では「匿名ログイン」についての機能を紹介し、気をつけないといけない点についてまとめようと思います。

前提

認証方法は「authUI」を使用しています。
後述の匿名ユーザ→本ユーザへの更新の仕方は、authUIを用いた方法に関して説明していますのでご了承ください。

FirebaseUI で iOS アプリに簡単にログインを追加する

匿名ログインについて

概念

Firebase で認証する一時的な匿名アカウントを、Firebase Authentication で作成して使用できます。一時的な匿名アカウントを使用すると、アプリに登録していないユーザーが、セキュリティ ルールで保護されているデータを使用できるようになります。

iOS で Firebase 匿名認証を行う

アプリによっては起動直後に「アカウント登録してください!」とメッセージが出て、登録しないとサービスを使えないようなものあるかと思います。
「匿名ログイン」することで、強制的なアカウント登録の手続きをせずに、アプリを使用すること(CloudStoreへのデータ登録)が可能になる、といったメリットがあげられます。

コード

匿名アカウントでのログイン処理
import Firebase

// 匿名ユーザログイン処理
Auth.auth().signInAnonymously() { (authResult, error) in
    // エラーチェック
    if let err = error {
        
    }
            
    // ログ出力
    let isAnonymous = authResult?.user.isAnonymous  // true
    debugLog("isAnonymous: \(isAnonymous as Any)")
    debugLog("uid: \(authResult?.user.uid as Any)")
    debugLog("email: \(authResult?.user.email as Any)")
    debugLog("photoURL: \(authResult?.user.photoURL as Any)")
}

コード量はこんだけです。簡単ですね!

匿名アカウントから永久ユーザへの昇格(authUIでのログイン方法)
import Firebase

class viewController: UIViewController {
    var authUI: FUIAuth!

    // 認証に使用するプロバイダの選択
    var providers: [FUIAuthProvider] = [
            FUIGoogleAuth(),
        ]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.authUI.providers = providers
        self.authUI.delegate = self
        <b>authUI.shouldAutoUpgradeAnonymousUsers = true   // 匿名、永久への変換を可能とする</b>
    }

    // ログインボタン(authUI画面を表示する)
    @objc func closeTupped(_ sender: UITapGestureRecognizer) {

        let authViewController = self..authUI.authViewController()
        authViewController.modalPresentationStyle = .overCurrentContext
        self.show(authViewController, sender: nil)
    }

}

// delegate
extension viewController: FUIAuthDelegate{
    // 認証処理
    public func authUI(_ authUI: FUIAuth, didSignInWith user: User?, error: Error?){
        // 処理は省略
    }
}

ポイントは「authUI.shouldAutoUpgradeAnonymousUsers = true」だけです。
このコードを追記した上でauthUI画面を立ち上げログインすると、匿名アカウントを永久アカウントに昇格することができます。

認証時に「成功」、「失敗」するケース

認証する際のアプリの状態やログイン先のアカウントの状態といった、それぞれの状態のケースで認証試みるとどうなるのか?以下にまとめました。

アプリの状態:ログイン時、未ログイン時
アカウントの状態:永久アカウントのユーザ、未登録のユーザ

アプリの状態 永久アカウントのユーザ 未登録のユーザ
未ログイン時 成功:新規ユーザに対してログイン 成功:新規アカウント作成
匿名アカウントでログイン時 失敗 成功:新規アカウント作成(昇格)

上記の表で書いてある通り、「匿名アカウントでログインしている際」に、「既に存在するアカウント(一度認証済みのアカウント)に対してログインする」とエラーが発生します

ですので、認証済みのアカウントにログインする際には、未ログイン状態にしてあげる必要があります

小話 - 匿名アカウントでログアウト

してしまうと、そのアカウントに対して再度ログインができなくなってしまいます。

ですので、匿名ユーザでしかアクセスできないデータがCloudStoreに残ってしまうと、このデータはアプリから削除する術がなくなってしまいます。


と、私も今更気づいた次第です。

ならば、匿名ユーザでログアウトする際に、該当ドキュメント配下一式を削除すればいいじゃん!と思った次第ですが、
該当ドキュメントを消して配下のフィールド、コレクション一式も削除するといった操作はできないそうです。

★ドキュメント配下のコレクションを一つ一つ消していけばドキュメントも削除できるようになるのですが、処理に時間がかかる(たくさんコレクションを生成している場合など)こともあり自作バッチによる削除を推奨しているようです。

https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja#python_2


さてさてどうしようか、ということで次回Firebaseのバッチ処理辺でお会いしましょう!笑

最後に

最近朝起きれなさすぎて光目覚まし時計を購入しました。
これで朝型人間になるんだ・・・!

【Firebase】ドキュメントの更新

本日はFirebaseのCloud Storeにおけるデータの更新の仕方をまとめようと思います。

自分のアプリの品質強化施策で、ドキュメント更新失敗時のケースを検討しようとソースを見てたのですが、パッと見て実装に問題があるのかわかりませんでした・・・
→実装の仕方をすっかり忘れてた!

ですので、復習がてらドキュメントの更新方法に関して、swiftでの実装方法を紹介しつつまとめてみようと思います。


以下サイトを参考に実装していますが、基本的には自分の使用した機能を紹介します。

firebase.google.com

前提

Cloud Storeを使用するにあたって、「コレクション」、「ドキュメント」、「フィールド」といったデータに関する概念の説明は省略します。

最初に - フィールドに格納できる情報について

文字列、ブール値、日付、NULL、ネストされた配列、オブジェクトなどを格納できます。

以下サイトにて使用できる型がまとまってます。
サポートされるデータ型  |  Firebase

Myトレの例

せっかくですので、私のアプリにおける「日々の記録」のデータ構造をご紹介します。
以下は実際にMyトレで、Cloud Storeにデータを格納する際に定義している変数になります。

フィールドに投入するデータは、swift上では[String: Any]型である必要があります。

実際にCloud Storeで管理する際には「map型」で管理するため[String: Any]型と言いつつも、intやtimestamp型が登場しています。

let docData: [String: Any] = [
    "taskData": [
        "腕立て伏せ": [
            "1": [
              "count": 15,  // Int型
              "date": Timestamp(date: "date型、作成した時刻") 
            ],
            "2": [
              "count": 17,  // Int型
              "date": Timestamp(date: "date型、作成した時刻") 
            ]
        ],
        "スクワット": [
          "1": [
            "count": 20,  // Int型
            "date": Timestamp(date: "date型、作成した時刻") 
          ]
       ]
    ]
]

コード実装

コレクション・ドキュメントの定義

以下のコードでが、実際にデータを格納したいコレクション、ドキュメントを定義しています。
後述しますが、以下定義した変数からsetDataなどのメソッドを呼び出し引数に上記の[String:Any]型の変数を設定することで、Cloud Storeにデータが格納できます。

import Firebase

var baseRef: DocumentReference? = nil
var baseCol: CollectionReference

baseCol = Firestore.firestore().collection("ルートで、すでに存在する、これから作るコレクション名を定義")
baseRef = baseCol.document("すでに存在する、これから作るドキュメント名を定義")

ドキュメントの作成

ドキュメントを新規に生成する場合は、以下の3つのパターンのどれかで実装します。

ドキュメント名を自分で定義する場合
baseCol.document("20201102").setData(docData)
ドキュメント名を自動で定義する場合
baseCol.addDocument(data: docData)
すでにドキュメントが存在する場合の対応
baseCol.document("20201102").setData(docData, merge: [true or false])

すでにドキュメントが存在する場合にsetDataをすると、新しいドキュメントで上書きされてしまいます(既存のドキュメント・フィールドが消える)。
mergeオプションをtrueにすることで、既存のデータを消さずに新しいデータを統合することが可能となります。

ドキュメントを更新する

以下の例は、上記のsetDataで投入したデータの一部を更新します。

let docUpdateData: [String: Any] = [
    "taskData": [
        "スクワット": [
          1: [
            count: 25,
            date: 20201202 17:35:30
          ]
    ]
]

baseCol.updateData(docUpdateData) { err in
    if let err = err {
        print("Error updating document: \(err)")
    } else {
        print("Document successfully updated")
    }
}

補足
updateData = setData(data, merge: true)なのかなと思ってます。実際に検証したことがないので、挙動に差異があるか機会があれば別途調べてみようと思います。
ですが基本的には、初回のデータ投入:setData、それ以降のデータ更新:updateData、という考えで問題ないと思います。

最後に

データの更新自体は、上記程度のコード量で済んでしまうのでぶっちゃけ楽です!
私は、最初に実装したのはもう一年くらいになりますが、初めてデータを投入できたときは感動しました。

おおー、データ投入できた!!!ってめちゃめちゃ喜んでました笑

ただ、どういった構造にしていくか?検討するフェーズが難関であると思います。
なんやかんやで1ヶ月くらいはトライアンドエラーで実装検証してました・・・

また、セキュリティルールも同時に検討していかないと、大事な個人データが勝手に流出されてしまうかもです。
→セキュリティに関しては、次回別途何か記事を作ろうかなと思ってます!


そんなわけで、ドキュメントの更新方法についてでした!

【BDD, TDD】テストコードを書いていて感じたこと その1

こんにちは、日に日に寒くなってきましたね。
ふと思えば本日は11/28で今年も残すところあと1ヶ月になります。

このブログでは「ドラムと筋肉とプログラミング」ということで、それぞれについて思うことを発信して行こうと思ってたのですが、気づけば「プログラミング」の記事がメインとなりつつあります。。。

とはいえ、筋トレは自作アプリにより自重トレーニングが継続できるようになりましたし(自分の胸筋好きになった!)、ドラムも昨年よりスキルが上がったと思います。

今年も残り限られてますが、自分の体に労わりつつ、新たな気付きを得られるよう行動していきたい、そんなふうに感じた今日この頃です。

はじめに

さて、ということで相変わらず?プログラミングのお話です。
前回の記事でBDD, TDDについて触れ、テストコードを書き始める前に自分のソースで気づいた点をまとめました。

BDD, TDDを始めたいです・・・ - ドラムと筋肉とプログラミング


「内部の振る舞いが理解できていない」という現状があったので、現在「仕様」に対する振る舞いも整理していて、その上でテストコードを追加しております。
この作業をする上で、改めてインターフェースを先に実装していくと言った考えが特に重要だなと感じた次第です。

本日の記事では、新たに機能を追加する際に、インターフェース(プロトコル)から実装していく必要があると思いますが、どう言った観点で実装していくべきかまとめようと思います。
→ぶっちゃけ前回の記事の言い方変えただけ!でもありますが笑

結論

新しい機能(仕様)を追加する際に、以下に関するインターフェースから検討・実装する

  1. 仕様に対して、実装する見込みのある「すべてのソース」
  2. 1.に対して、異常系を見込む(DB接続、外部APIへのリクエスト送信など)必要のある「ソース」


整備中・・・

理由

インターフェースから実装するということは、「仕様の骨子を設計する」と言い換えることもできると考えています。

この骨子を設計することは、プログラムの「全体像」が見えるタイミングでもあるため、この時点でメソッドに対する実装は絶対にしてはなりません!!


私は欲しい機能に対して全体の骨子の実装から始めていなく、基本的に正常系のケースしか考慮していませんでした。
後に異常系も検討して行こうと思っても、正常系にのみ対応した構造になってしまっているため、再度「骨子(インターフェース)から見直さないといけない状況」になっていました。

家を建てるという建築の話に例えると、家の骨子(柱)から作り始めずに壁の塗装や階段を実装し始めてしまったので、後で思う位置に部屋を追加できなくなってしまう、って感じでしょうか(適当)。

プログラムは家と違ってやり直しが効くので、後から骨子を見直すこともできますがそれだけ時間が必要になってしまって無駄です。

ついでに思ったこと - シーケンス図ってやっぱ大事だよね

振る舞いを明確にするために同時に「シーケンス図」があると超いいな!と感じた次第です。

最近アジャイル開発などで設計書はいらん!みたいな話がありますが、私はこのシーケンス図は絶対にあったほうが良いと考えてます。
特に、仕様が多岐のクラスに跨いで振舞うことで、コードを追いかけるのが面倒くさくなります。
→私はアジャイル開発したことないので現状はよくわかってないですが・・・

こう言ったときに、どんなクラスに影響するのか?一つの図で表現できると振る舞いに対する理解が圧倒的に早くなると思います。


自分はシーケンスまで作れてないのですが(さぼっていて)、シーケンスを自動化できるツールがあったら是非導入したいです。
ちょいとググってみますか・・・!

最後に

言い訳ですが、概念に対して「例」をすぐに出すのって難しいです・・・
自分の考えをまとめる(発進する)機会を増やして、こう言った例題もパッと出せるようにしたいです。

BDD, TDDを始めたいです・・・

先日、以下のような記事を書かせていたいただきました。

kz-ike.hatenablog.com

ようやくアプリをリリースできたわけですが、まだまだアプリは進化していく予定です!

さて、進化ということで「継続的にアプリを成長させる」ためには、アプリの品質も担保してかないとですよね。
私のアプリは残念ながら自動テストには対応していないため、実機を使った「手動」での動作確認しかできていません。

「アプリをapple storeにリリースする!」という気持ちが強かったので「品質」という観点での開発をあまり意識していませんでした。
なんとかリリースもできたので、このタイミングで「テスト環境」を導入していこうと考えています。

と同時に「先にテストコードを書いてあとで実装する」というTDD(テスト駆動開発)を、自作アプリの開発プロセスとして取り入れていきたいです。

本記事では、テストファースト開発プロセスである「TDD」及び「BDD」について概念を説明し、テストコードを書き始めようとした中で気づいたしくじり事例を書き留めたい思います。

ちなみに、私が参考にしている本は以下です。
PEAKS(ピークス)|iOSテスト全書

開発プロセス

What's TDD?

概念

最初に失敗するテストコードを書き、それを駆動源としてプロダクトコードを開発するスタイル。

メリット

1. テストを成功させるためにどのように実装するかではなく、「何が欲しいか」から考えることができる
 →「何が欲しいか」ということで、インターフェースの検討から始まる
 →インターフェースを使用することで、依存性注入を考慮したコードをかけるので
  テストダブル(モック。スタブなど)を実装しやすくなる

2. テストコードが必ず用意され、リファクタリングが楽になる
 →失敗のケースから始まるため、失敗時の処理も検討できる

What's BBD?

概念

TDDの派生系で、「振る舞い」と「要求仕様」価値を置いた開発プロセス

ポイント

BBDの概念で「振る舞い」、「要求仕様」という単語を出しましたが、もう少し深掘りします。
冒頭で挙げた書籍で、外側の品質、内側の品質と言った二つの考えが説明されていました。ざっとまとめると以下のようになります。

品質の種類 視点
外側の品質 利用者から見たソフトウェアの品質(要求仕様) 必要な機能が満たしているか、操作性など
内側の品質 開発者から見たソフトウェアの内部構造における品質(振る舞い) コードの可読性、クラス設計が適切であるかなど

筆者の知識がまだ浅くて申し訳ないのですが、

テストコードの意図が人間にとって解釈しやすいように(振る舞いとして理解できるように)にテストコードを実装しよう、
そのために専用のライブラリを使用しよう。

と言った考えが「振る舞い」のポイントかなと現時点で理解しています。

★BBDの枠組みを提供するテスティングフレームワークでQuicl/Nimbleというものがあるそうで使ってみようと思います。

しくじり事例(私のアプリで感じたこと)

  • 内部APIの振る舞い方が把握できていない

「要求仕様」に対する「振る舞い」のテストコードを明記することで、内部APIがどのように動いているか?把握しやすくなります。

残念ながら、今回私は欲しいと思った機能に対していきなり「実装」から始めてしまっていたため(ある程度プロトコルの設計はしていたものの)、今回テストコードを書いていこうと思った際に、自分の実装した機能が内部でどう振る舞っているかわからない状況になってしまっていました


内部の振る舞いが理解できていない

(新しい機能の追加・改造を実施する際に)既存機能にどの部分に影響するか判断つかなくなってしまう

大変


★現在、要求仕様(自身のアプリの機能)に対する、全ての内部APIにおける振る舞いをトレースする作業を始めております・・・

  • 失敗時のケースを実装できていない

TDDのメリット2. にて「失敗のケースから始める」と記載しましたが、失敗のケースから始まるということは失敗時の処理もその時点で検討できるということです。
自身のコードを振り返ると、正常時の動作が通るように実装してしていたため、バグは実際に動作確認している中で発見した物しか発見できていないということになります。

ですので、想定しないケースでアプリがクラッシュしてしまうといった問題が発生してしまうかも、と考えるとアプリを使っていただいているユーザには非常に申し訳なく思います。


★「内部APIのトレース作業」と並行して、失敗時の挙動を整理中・・・

最後に

現在本記事を執筆している時点で、まだBDDのライフサイクルを回せていません。
ある程度、内部での振る舞いを整理できたら、次の機能実装にてBDDの開発プロセスを取り入れていこうと思います。

まずは、簡単な機能追加でBBDを回してみよう・・・!

【AWS】lineにメッセージを送る方法のまとめ(lambdaの使用)

はじめに

こんにちは。
みなさまLineって使ってますか?私はLineを使い始めたのは大学2年くらいの頃でかれこれ10年くらい使っています。
高校の頃、友人や家族と連絡を取ると言ったら、電話以外ですと「メール」が主流でした。

ガラケーでぽちぽちメールをしてたのを思い起こすと懐かしさを感じます。
ガラケーのメールはPOPプロトコルで、携帯端末に保存する仕組みでした。メールの保存上限もあるため、大事なメールは大切に保管して・・・と言ったこともしてましたが、センチメンタルな行動だったたなーと思いキュンキュンします。


話は変わり、今ではそんなメールに変わりコミュニケーションツールの「Line」が主流になってるのかなと思います。
海外事情はよくわかりませんが、私の周りの方と連絡を取ると言ったらこのLineを通じてコミュニケートすることになります。
電話も無料でできちゃうので手放せないですよね。

さて、それなりにLineが普及しているわけですが、そんな中でもこのLineを使っていないユーザもいるかと思います(決して悪いといっているわけではないです!)。
そう言った方とは私のgmailなどのフリーメールに連絡を送ってもらっているわけですが、私はメールをメインにコミュニケートをすることはないので通知を見落としがちです。
→メルマガに埋もれてしまう・・・

そこで、今回はそんなLineをしていない方からにも、私のlineにメッセージを送ることを可能とする仕組みを模索しました。
自身のスキルアップも兼ねAWSなどのサービスを用いることにしましたので、そこで身に付けたことを本記事にまとめようと思います。

作るものの概要

 今回は「Line notify」AWS「lambda」、「レイヤー」の機能を使用します。
 Line notifyで「Line上でメッセージ送信したい相手に対するトークン」を発行し、そのトークンを用いてメッセージを送信するコードをlambdaに実装します。

 今回はここまでの機能のみをまとめますが、今後はAWSの「APIゲートウェイ」も利用し、webアプリを通じてlambdaを叩くような仕組みも実装したいと考えてます。

使用する機能一覧

Line notify

  • 概要

 webサービスからの通知をlineに通知するサービス

  • 今回の使用用途

 Lineで送りたい「個人」、「グループ」に対するトークを発行する。

AWS

Lambda
  • 概念

 EC2などのリソースを※プロビジョニングしなくても、ソースを実行できるサービス

 ※プロビジョニング:必要に応じてネットワークやコンピュータのリソースを予測し準備しておくこと

  • 今回の使用用途

 今回はpythonを選定し、pythonからline notifyで発行したトークンに対してメッセージを発信する。
 →数行でコーディングできて簡単!

レイヤー
  • 概念

 lambdaで選定した言語に対して、外部APIを設定することのできるサービス

  • 今回の使用用途

 pythonからLineに通知する際に「requests」モジュールが必要であるため、レイヤーに「requests」モジュールを設定することでlambdaから呼び出せるようにする。

作業手順

Line notify

 以下、line notifyのサイトにて通知を送りたい相手(グループ、個人)に対して、1対となるトークンを発行する
 https://notify-bot.line.me/ja/

 ★Line notifyは自身のlineアカウント(メールアドレス)にて、ログイン可能
 ★トークンは外部に公開しないこと!

  • テスト

 以下、curlコマンドを実行することで、Lineにメッセージを送信する。

curl -X POST -H 'Authorization: Bearer __YOUR_TOKEN__' -F "message=Hello!" https://notify-api.line.me/api/notify

AWS

Lambda
  1. AWSにて、lambda関数(python)を生成する
  2. 以下コードを入力し、デプロイする
import os
import requests

# Line Notify
ACCESS_TOKEN = os.environ["access_token"]
HEADERS = {"Authorization": "Bearer %s" % ACCESS_TOKEN}
URL = "https://notify-api.line.me/api/notify"

def lambda_handler(event, context):
    data = {'message': "send_test"}
    #lineに通知
    requests.post(URL, headers=HEADERS, data=data)
レイヤー
  • 以下の手順でレイヤーに設定する。

1. 事前に上記のモジュールをダウンロード・zip化する(python環境の構築方法は省略する)

pip install hoge
mkdir hoge
pip install -t ./hoge hoge
zip -r hoge.zip hoge

2. AWS上にて、任意のレイヤーを作成する
3. 2.に1.のzip化したデータをアップロードする(lambdaからimport可能となる)
4. lambdaで作成した関数に対して2.のレイヤーを紐付ける
 (AWSのlambdaページを開き)lambda→関数→(自分の作成した関数名)→設定→デザイナー(layers(0)を選択)
 
 上記を選択した後に登場する「レイヤー」フォームにて「レイヤーの追加」を選択し、今回作成したレイヤーを追加する

  • 注意事項

レイヤーにアップロードするzipは、以下構造を満たしてないとlambdaからimportできません!

pillow.zip 
| python/PIL 
└ python/Pillow-5.3.0.dist-info

 参考
docs.aws.amazon.com

最後に

ここまでやってみたら、lambdaのテストを実行することでlineに通知を飛ばせるようになるはずです。

私は、レイヤーからのimportがうまくできなくて数時間詰まってました。
zip化したファイルの中身の所有権も疑ってたりしましたが、公式のdocを見たらすぐに解決しました。
→やっぱ公式ドキュメントを読み込むことは大事と言うことですね

今回はlambdaを直接動かしてましたが、次回はAPIゲートウェイも用いてwebアプリから本機能を叩くことができるようにしたいと思います。


ここまで読んでいただきありがとうございました!

強化学習の勉強

はじめに

本記事は、機械学習の一つである「強化学習」について、勉強したことの結果をアウトプットとして残すことを目的とします。

強化学習とは?

概念

機械学習(Machine Learning)」のうちの一つの手法になります。
機械学習とは、コンピュータが大量のデータからそこに潜む特徴パターンを見つけそれを説明する「モデル」を作成し、そのモデルに基づいて高い正確性で予測を行うことになります。

この機械学習の種類には大きく三種類存在します。

強化学習」では、学習するための「環境」を与える必要があります。

この環境には、「行動」、「状態」、「報酬」が定義されています。

「行動」を試行錯誤して行う中で「状態」が変わり、最終的にゴールに到達する事ができれば「報酬」をもらう事ができます。

強化学習の目的は、「最終的(1エピソード)に得られる報酬の総和の合計を最大化する」モデルを模索することになります。

囲碁プログラムのAlphaGoでは、この強化学習のノウハウにより囲碁で勝利するためのパターンを学習した結果、世界王者を4勝1敗で下すと言った結果になりました(2016年3月時点)。

オセロでの例

上記で「環境」、「行動」、「状態」、「報酬」という単語ができてきたので、オセロに喩えて説明してみます。

  • 環境:オセロの盤(通常8×8)
  • 行動:石を打つする
  • 状態:プレイヤーが石を打つことにより、挟まれた石の色が変わる
  • 報酬:ゲーム終了時に、プレイヤーの石が敵の石より多く置いてある場合に得られる

オセロの強化学習では、プログラミングによって実際に石を置いていき、すべての石を置き切った時点での(すべての石を置いた状態)最終的な報酬を算出します。
この最後まで石を置き切ると言った行為は、環境でいうところの最終的な「ゴールに到達する」という状態を表します。

ゴールまでに到達するまでの一連の流れを「エピソード」といい、強化学習ではこのエピソードを何度も繰り返し試行錯誤する中で報酬をもらえるパターンをモデル化します

このオセロの例だと、報酬をもらえるケースはゲーム終了時にしかもらえないですが、環境によっては行動毎に報酬がもらえる(即時報酬)ケースがあります。

強化学習における問題設定 - MDP(Markoy Decision Process)

強化学習では、与えられた「環境」が「マルコフ性(Markov property)」というルールにしたがっていることを想定します。
このマルコフ性のルールに従うことで、上記で説明した「環境」の説明を数式で表現する事が可能となります。

数式を説明する前に、マルコフ性ルール構成要素について説明します。

ルール

  • 遷移先の状態は、直前の状態とそこでの行動飲みに依存する
  • 報酬は、直前の状態と遷移先に依存する

構成要素

s(state) 状態
a(action) 行動
T(Transition function) 遷移関数
R(Reward function) 報酬関数(即時関数)


上記を基に、価値の総和を最大にする数式を解説していきます。

補足

上記のMDPでは、報酬関数、遷移関数が登場しますが、これは強化学習において、「モデルベース」の考えを適応していると言えます。
対に「モデルフリー」という概念も存在しますが、こちらでは報酬関数、遷移関数が存在しないケースになります。
→本記事では説明省略します

数式

  • 報酬の総和(期待報酬(Expected reward)、価値(value))

ある時刻\displaystyle tからエピソード終了時刻\displaystyle Tまでの報酬を示します。
 \displaystyle
G_t = r_{t+1} + \gamma r_{t+2} + \cdots + \gamma^{T-t-1}r_T = \sum_{k=0}^{T-t-1} {\gamma^k r_{t+k+1}}\\ 
\gamma: 割引率(Discount factor)、0-1


報酬の総和は、エピソードが終了しないと計算できないため、「見積もり」としてその時点における状態を算出する必要があります。
見積もりはあくまで不確かな値であるため、この割引率により遠い将来の報酬ほど信頼の低い値として算出します。

  • 報酬の総和の期待値

上記の記載した数式では、以下二つの問題点があります。

その1 将来の即時報酬の値(\displaystyle r_{t+1}, r_{t+2} + \cdots)の合計を理解している必要がある
その2 将来の即時報酬が必ず得られる、と定義している

将来の即時報酬が「必ず登場する」ということを前提としています。実際は、行動してみないとその即時報酬がわからないという前提があるため、「期待値」として表現する事がふさわしいです。

「その1」に従い、「報酬の総和」を再帰的に表現すると以下のようになります。

 \displaystyle
G_t = r_{t+1} + \gamma G_{t+1}


次に、「その2」の期待値を算出するため、各時刻において「報酬を得られる確率」を掛け合わせます。

報酬の総和の期待値を \displaystyle Vと表します。
この時、戦略 \displaystyle \piによって状態 \displaystyle sから \displaystyle s'に移動する場合、行動 \displaystyle aをとる確率は \displaystyle \pi (a \mid s)、遷移先 \displaystyle s'へは遷移関数から導かられる確率は \displaystyle T(s' \mid s, a)とします。

上記を考慮すると、期待値Vは以下のように表す事ができます(policyベース)

 \displaystyle
V(s) = \underset{a}{max}  \sum_{s'} {T(s' \mid s, a)(R(s, s') + \gamma V(S') )}


また、報酬が状態sのみで決まる場合は以下のように表す事ができますvalueベース)

 \displaystyle
V(s) = R(s) + \gamma \underset{a}{max} + \cdots + \sum_{s'} {T(s' \mid s, a)V(S')}

これが基本的な式になり、この式から報酬の総和が最大になるよう、各状態において最適となる行動を求めます。

valueベースでは、ある状態sにおいて、価値が最大となる行動を必ず選ぶ一方で、policyベースでは、各行動を確率的に選択されると言った違いがあります。

この違いが、どう言った特徴(長所・短所)があるのかは、筆者が勉強不足でまだ理解できてないです。


今後、筆者自身も強化学習していく中で別途まとめたいと思います。

参考

本:pythonでまなぶ強化学習(著者、久保隆宏)