【Alexa Skill開発】LaunchRequest時にエラーになってしまう場合に確認すべき事項(Python)

概要

Alexaのスキルはシミュレータでもデバッグできるので、比較的デバッグがしやすい。
しかし、スキルが最初に呼び出された際の挙動がシミュレータでは確認できないため、エラーが発生した際のデバッグが困難。
気づいてみれば簡単なことだったけれど、結構見つけるのに苦労したので、メモとして残しておく。
ちなみに今回対象とするのは、以下の時刻表skill。
t-nkb.hatenablog.com

現象

「時刻表で平日ダイヤを教えて」等の、IntentRequestを起動すると正常に動くが、「時刻表を開いて」とLaunchRequestを起動するとエラーが起きてしまう。

エラーの原因

IntentRequest以外の挙動の記述が記載されていなかった

pythonだと、event['request']['type']内にリクエストの種類が記載される。
今回は、IntentRequestで平日or祝日を明示的に示し、この明示がなくただ時刻表アプリを起動した場合は、平日か土日かを自動判定したいため、以下のようにIntentRequestかどうかで場合分けをすることとした。

    if event['request']['type'] == "IntentRequest":
        intent = event['request']['intent']
        value = intent['slots']['DayType']['value']
    else:
        if (jsttime.weekday() >= 5):
            value = "休日"
        else:
            value = "平日"

IntentRequestで入れる項目を他で参照してしまっていた

event['request']['intent'], intent['slots']['DayType']['value']といったIntentRequestで入力されるべき項目は、LaunchRequestで参照するとエラーになってしまう。
ここで注意すべきなのは、len(event['request']['intent'])==0 として、配列が空かどうか判別するやり方ではダメ、ということ!
LaunchRequestではそもそも、event['request']['intent']といった項目がないため、値を参照した段階でエラーが発生してしまうため、配列が空かどうかでは判別できない。

感想

気づいてしまえば当たり前でごく単純なことだけれど、一旦エラーになってしまうとなかなか見つからないもの・・・・。

【Alexa Skill開発】Alexaに最寄り駅の次発電車の発車時刻を教えてもらう(AmazonEcho)

経緯

Amazon Echoを買ったので、自分でも面白いものが作れないか調べたところ、公式の解説がとてもよくまとまっている。
これなら簡単にできるな・・・ということで、練習問題を少し改造して、最寄り駅の次発、次々発、次々々発の電車の時刻を教えてくれるSkillを自作してみた。

時刻表もベタ打ちだし、かなりイケてない作りだけれど、とりあえず使えるはず。

developer.amazon.com

Skillの動作

「Alexa、時刻表で平日の電車を教えて」
「Alexa、時刻表で休日」
「Alexa、時刻表で休日の時刻表」
等の声掛けをすると、
「"平日の時刻表です。次は6時45分,1分,14分です"」
と、現在時刻から10分後以降の電車を3つ列挙して教えてくれる。
私はほぼ乗る電車が確定しているので、路線については決め打ちだが、少し改造するだけで、「○○方面への時刻表を教えて」等のアップグレードも可能。

前提

本説明は、上記Amazonチュートリアルの入門#1,#2を実行、理解できていることを前提とする。
つまり、
AWS Lambdaへの登録
Amazon Developerアカウントへの登録
・ スキルを登録するまでの流れ
については説明しない。

Skillの作り方

アレクサのスキルを作るためには、以下2つの作業が必要となる。以降、これらを順に説明していく。
1.AWS Lambdaスクリプトの作成
 (サーバーサイドのプログラムを設定、今回はPythonを使用)
2.Alexaの会話フローの作成
 (Alexaにどのように声掛けをするか、音声からどのような情報を取得し、サーバーに投げるかを設定)

1. AWS Lambdaスクリプトの作成

まずはサーバーサイドのプログラムを登録する。
WebブラウザでAWSマネジメントコンソール( https://console.aws.amazon.com/ )にアクセス、ログインし、画面左上の「AWSサービス」のテキストボックスに「lambda」と入力し、表示される候補から「Lambda」をクリックする。
AWS Lambdaの初期画面が表示されたら、「関数の作成」を押し、先のチュートリアル同様、求められた項目に入力していく。ランタイムの項目のみ、最初は「Node.js 6.10」となっているが「Python3.6」としておく。

その上で、スクリプト記載画面まで行ったら、以下を入力する。(住まいがわかってしまうので、時刻表の中身は変更済み)
やっていることはごく簡単。スクリプトを追えばすぐに理解できるはず。
jikoku = [,]の2次元配列に対し、時刻表を教えてほしい電車の発車時刻をベタ打ちすれば、様々な環境で使える。

import time
from datetime import datetime, timedelta, timezone

def train(jikoku,time):
    nextTrain = [0,0]
    for i in jikoku[time.hour]:
        if i >= time.minute:
            nextTrain = [time.hour,i]
            break
        
    if nextTrain[0] == 0 and nextTrain[1] == 0:
        for i in jikoku[time.hour+1]:
            if i >= 0:
                nextTrain = [time.hour+1,i]
                break
    return nextTrain


def lambda_handler(event, context):

    if event['request']['type'] == "IntentRequest":
        intent = event['request']['intent']
        value = intent['slots']['DayType']['value']
    else:
        if (jsttime.weekday() >= 5):
            value = "休日"
        else:
            value = "平日"
    
    # タイムゾーンの生成
    JST = timezone(timedelta(hours=+9), 'JST')
    jsttime = datetime.now(JST) + timedelta(minutes=10)
    
    if value == '休日' or value == '祝日':
        jikoku = [
            [8,16,25],#0,
            [],#1,
            [],#2,
            [],#3,
            [52],#4,
            [10],#5,
            [3,8],#6,
            [1,12,55],#7,
            [2,13,30],#8,
            [2,13],#9,
            [2,13],#10,
            [2,13],#11,
            [2,13],#12,
            [2,13],#13,
            [2,13],#14,
            [2,13],#15,
            [2],#16,
            [2],#17,
            [2],#18,
            [2],#19,
            [2,10],#20,
            [2,10],#21,
            [4,50],#22
            [4,51]#23            
        ]
    else:
        jikoku = [
            [8,16,25],#0,
            [],#1,
            [],#2,
            [],#3,
            [52],#4,
            [10],#5,
            [3,8],#6,
            [1,12,55],#7,
            [2,13,30],#8,
            [2,13],#9,
            [2,13],#10,
            [2,13],#11,
            [2,13],#12,
            [2,13],#13,
            [2,13],#14,
            [2,13],#15,
            [2],#16,
            [2],#17,
            [2],#18,
            [2],#19,
            [2,10],#20,
            [2,10],#21,
            [4,50],#22
            [4,51]#23      
         ]

    
    nextTrain = [[0,0],[0,0],[0,0]]
    nextTrain[0] = train(jikoku,jsttime)
    nextTrain[1] = train(jikoku,datetime(jsttime.year,jsttime.month,jsttime.day,nextTrain[0][0],nextTrain[0][1]+1,0))
    nextTrain[2] = train(jikoku,datetime(jsttime.year,jsttime.month,jsttime.day,nextTrain[1][0],nextTrain[1][1]+1,0))

    if value == '平日':
        day = "平日の時刻表です。次は" + str(nextTrain[0][0]) + "時" + str(nextTrain[0][1]) + "分," + str(nextTrain[1][1]) + "分," + str(nextTrain[2][1]) + "分です"
    elif value == '休日' or value == '祝日':
        day = "休日の時刻表です。次は" + str(nextTrain[0][0]) + "時" + str(nextTrain[0][1]) + "分," + str(nextTrain[1][1]) + "分," + str(nextTrain[2][1]) + "分です"
    else:
        day = "平日の時刻表です。次は" + str(nextTrain[0][0]) + "時" + str(nextTrain[0][1]) + "分," + str(nextTrain[1][1]) + "分," + str(nextTrain[2][1]) + "分です"


    response = {
        'version': '1.0',
        'response': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': day
            }
        }
    }
    return response

2. Alexaの会話フローの作成

次に、Alexaの会話フローを作成する。
まずは、以下の開発者コンソールから適当な名前でスキルを登録する。
Amazon Developer Sign In

次に、対話モデルのインテントスキーマの設定する。

{
  "intents": [
    {
      "slots": [
        {
          "name": "DayType",
          "type": "LIST_OF_DAY_TYPE"
        }
      ],
      "intent": "GetNewFactIntent"
    }
  ]
}

カスタムスロットタイプには平日・休日の種別を設定する。

    タイプ: LIST_OF_DAY_TYPE
    値:平日、祝日、休日

サンプル発話は以下の通り

GetNewFactIntent {DayType} の時刻表
GetNewFactIntent {DayType}
GetNewFactIntent {DayType} の時刻表を教えて
GetNewFactIntent {DayType} の次の電車は
GetNewFactIntent {DayType} の電車

あとは、先のチュートリアル通りにLambdaとスキルを紐付けたら完成。
試しにシミュレータで「平日は?」等を入力してみると、「平日の時刻表です。次は7時32分,55分,5分です"」等と帰ってくることがわかる。

感想

まとめてみると長いけれど、基本はサンプルから少し改造するだけでOK。買った翌日に試して、2~3時間程度でこの程度のことができます。
簡単なPythonプログラムが書ければ、結構日常生活に役立つSkillが作れるんじゃないかな、と思いました。
後はアイディア勝負・・・・かな。

pythonを使って連番の画像を一括ダウンロードする

pythonを使って連番の画像を一括ダウンロードするスクリプトを書いたので、まとめておく。(スクリプトは一番下)

http://www.xxxxxx/001.jpg
http://www.xxxxxx/002.jpg
.
.

というURLのjpgファイルを一括でダウンロードするためには、
url = "http://www.xxxxxx/"
と編集して実行することで、./pic/以下に一括でダウンロードされる。
1000枚までカウントアップしてダウンロードし続け、ファイルがなくなった時点で終了するスクリプトになっているため、1000枚以上の画像を一括でダウンロードする場合には、for i in range(1000):の部分の数字を変える。

また、
http://www.xxxxxx/a001.jpg
http://www.xxxxxx/a002.jpg
.
.
というように、画像ファイルの前に文字が入っている場合は、
url = "http://www.xxxxxx/a"
と指定すればよい。

これを使えば大抵のサイトの画像が一括ダウンロードできるが、サイト規約で禁止しているサイトもあるため、スクリプト使用前に規約を確認すること。

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

import urllib.request
import sys
import os

def download():
    path = "./pic/"
    os.mkdir(path)

    url = "保存元のURLを入れる"
    for i in range(1000):
        try:
            url_all = url + str(i+1).zfill(3) + ".jpg"
            urllib.request.urlretrieve(url_all, path + str(i).zfill(3) + ".jpg")
        except urllib.error.URLError as e:
            break

if __name__ == "__main__":
    download()

pythonを使ってディレクトリを簡単にzip圧縮する

pythonを使ってディレクトリを圧縮する方法を検索すると、リストを作成→zipfileを使って圧縮という方法がよく出てくる。
しかし、この方法は無駄に行数が長くなり面倒。
shutilを使うことで、もっと簡単にディレクトリを圧縮することができることがわかったので、まとめておく。

import shutil
shutil.make_archive(output_filename, 'zip', dir_name)

一例として、直下の./testディレクトリを、./arc.zipに圧縮する場合は、このような形になる。
output_filename,dir_nameのどちらも、""で囲う必要があるのがちょっと不思議。

import shutil
shutil.make_archive("./arc","zip","./test")

これでシンプルに圧縮できる!!!

grouped convolution(グループ化畳み込み)の概念を理解する

経緯

mobilenetの実装とモデルを調べる中で出てきた概念として、pointwise convolutionとdepthwise convolutionの理解をまとめてきた。今回はその発展系で、grouped convolutionについてまとめる。
mobilenetは基本的に、pointwise convolutionとdepthwise convolutionを組み合わせた、depthwise separable convolutionの重ね合わせで構成されている。
pointwise convolutionは通常の畳み込み関数のフィルタサイズ1x1にするだけで構成できるので、どのフレームワークでもすぐに実装できるが、depthwise convolutionは実装が特殊、かつ実装方法がまちまちなので、フレームワークによっては使用できないものある。その場合に、depthwise convolutionの代わりにgrouped convolutionを代用することができることを示す。

t-nkb.hatenablog.com

t-nkb.hatenablog.com

grouped convolution(グループ化畳み込み)とは

グループ化畳み込みの歴史は長く、Alexnetの際にはすでに実装されていた。基本的な概念はシンプルで、入力層をレイヤー方向にgroup分割してそれを畳み込みし、最後に結合して出力とするもの。Alexnetでは、片側のグループで白黒の認識フィルタを、もう片側でカラーの認識フィルタを学習することができた…と報告されて話題になっていた。
下図を見れば分かる通り、ほぼdw畳み込みと同じ構造をしており、L=groupとすることでグループ化畳み込みでdw畳み込みが表現できることから、dw畳み込みはgroup畳み込みの一部と考えることもできる。
この考え方はモデル実装時には重要で、darknet等のdw畳み込みが実装されていないフレームワークでも、グループ化畳み込みが実装されていれば、dw畳み込みと同等の動作をさせることができる。ただし、dw畳み込みは実装により速度が大きく変わるため、期待する速度が得られるかどうかはフレームワークによる部分が大きい点は注意が必要である。

[グループ化畳み込み]
f:id:t_nkb:20171227204201j:plain:w400

[dw畳み込み]
f:id:t_nkb:20171227081429p:plain:w400

Depthwise convolution(dw畳み込み)の概念を理解する

経緯

前回、CNNモデルのパラメータ・計算量削減手法の一つであるpointwise convolution(pw畳み込み)について解説した。
pw畳み込みはHW方向の次元を削減し計算量とパラメータを減らすものだったが、それの対極をなすものとして、レイヤー方向の次元を削減するdw畳み込みがあるので、簡単に説明する。
mobilenet等はpw畳み込みと、dw畳み込みの繰り返しであり、この二つの概念が理解できれば、モデルの中身が理解できるはず。

t-nkb.hatenablog.com

概要

f:id:t_nkb:20171227080126j:plain:w300

dw畳み込みの基本的な構成部品は上図の通り、nxnx1の畳み込みフィルタである。基本的には、これを入力層のHxWx1のレイヤに適応し、HxWx1の出力を得ている。
ここで留意すべき点は、畳み込みフィルタ(nxnx1)、入力層(HxWx1)、出力層(HxWx1)共に、層方向の次元が1であること。


f:id:t_nkb:20171227081429p:plain:w400

それでは、入力のレイヤー数が増えた場合はどうなるのか?入力層がHxWxLとなった場合の処理を上図に示す。
先に説明した構成要素である、畳み込みフィルタ(nxnx1)、入力層(HxWx1)、出力層(HxWx1)の次元数を守るために、入力層を一旦1次元のレイヤに分離をし、HxWx1の畳み込みを加えたのち、再び層を結合することで、入力層(HxWxL)、出力層(HxWxL)の次元を確保している。
通常のnxnxLの畳み込みはフィルターが全て層にかかっていることに比べると、計算量が大幅に削減できていることが一目でわかる。
ただし、dw畳み込みでは、各層ごとに畳み込みをするため、層間の関係性は全く考慮されないため注意が必要。通常はこれを解決するためにpw畳み込みとセットで使うことが多い。
#実際の実装ではこの様な処理をしているわけではないが、計算的には等価。一般的な説明とも少し違う説明の仕方だけれど、概念的にはこの理解の方法が一番理解しやすいのでは?

depthwise convolutionのパラメータ数、計算量

dw畳み込みのパラメータ数と計算量についても定量的に示しておく。基本的に、dw畳み込みではレイヤー数は不変なので、入力層のレイヤ数L=出力層のレイヤ数Lとなる。
・パラメータ数はnxnxL、通常のnxn畳み込みのパラメータ数はnxnxLxLであることから、パラメータ数は1/Lで済む。
・計算量も同様にnxnの畳み込みに比べて1/Lとなる。

depthwise convolutionのメリット

最大のメリットは、やはり計算量の削減ができること。特にCPUでは(GPUに比べて)nxnの畳み込みは時間がかかるので、dw畳み込みで畳み込み計算量を減らすことで、大幅に速度を改善できる。

pointwise convolution(1x1の畳み込み)で何をやっているか、概念を簡単に理解する

概要

googlenetやmobilenet等、最近のCNNモデルでは、当たり前の様にpointwise convolution(1x1の畳み込み)を使って次元数を調整する・・・等の説明がなされるけれど、1x1で畳み込んでも結局意味ないじゃん?と最初理解に苦しんだので、自分なりにまとめておく。
「1x1の畳み込み」という言葉だけ聞くと、何の意味もないフィルタの様に思えてしまうけれど、実際は1x1xLbeforeという、各層にまたがる細ながーいフィルタを適応している。以下の図の様な細長いフォルタを使って、入力層から重要そうな部分を選別して取り出し、一つの層にまとめている。この処理を出力層分(HxWxLafter回)繰り返すことによって、層数を簡単に任意の値にできるという点が最大のメリット。

以下に、パラメータ、入出力サイズ、メリットについても自分なりの理解をまとめる。
#自分なりの理解なので、間違っている部分があれば是非ご指摘ください。

[pointwise convolution(1x1の畳み込み)の概念図]
f:id:t_nkb:20171224195604j:plain:w300
1x1の畳み込みを出力一層で適応した場合はこうなる。これが基本の適応系。

f:id:t_nkb:20171224180629j:plain:w300
上記畳み込みフィルタを必要な出力層(Lafter)分だけ適応して、重ねることで任意の総数の出力を得ることができる。
この図では、層数を増やす形で畳み込みフィルタを適応している。
(レシートの裏にメモしたものをスキャンしたので汚い笑)

pointwise convolutionのパラメータ数、計算量

・出力層一層に対するパラメータ数は1x1xLbefore
・よって、Lafter層の出力を得るために必要なパラメータは1x1xLbeforexLafterとなる
・畳み込みに必要な計算量はHxWxLbeforexLafter3x3の畳み込み等に比べると計算量は1/9だが、一般的なフレームワークでは関数呼び出しにかかる時間が加味されるため、ここまで高速にはならない模様

pointwise convolutionの出力

・H,Wのサイズは畳み込み前、後で基本的に同じ(stride=1の場合に限るが、通常はstride=1で適応することが多い)
・出力の総数はパラメータ数を変えることで、任意のサイズを指定することができる(ここが重要!!!)

pointwise convolutionのメリット

最大のメリットは層数を簡単に任意の値に変更できる点。他にも副次的に色々とメリットがある。

・1x1の畳み込みはCPUでも高速で動作する(CPUとGPUの速度差が比較的小さい)
・畳み込みのフィルタサイズを1x1にするだけで実装できるため他の次元削減・拡張手法に比べて圧倒的に実装が楽
・出力の層数を簡単に増減できる
・出力にrelu等を与えることによって、非線形性を増すことができる