Create a Clean Architecture REST API with Serverless Framework and TypeScript

This article is a translation of the original article.

I implemented an empty API of sample prepared in the previous article*1 so that it works.

Specification of API

I created todo's REST API.

  • POST /todos : Register todo
  • GET /todos : List todos
  • GET /todos/:id : Get todo
  • PUT /todos/:id : Update todo
  • DELETE /todos/:id : Delete todo

Architecture

Final goal (but I can not get it...)

I tried to make it to Clean Architecture.

Clean Architecture is an architecture that attempts to improve maintainability by separating abstract implementations (business logic etc.) from concrete implementation (framework, DB, etc.) as follows.

  • Divide the system into four layers (Entities, Use Cases, Interface Adapters, Frameworks and Drivers)
  • Make dependency between layers one-way from concrete layer to abstract layer (Frameworks and Drivers -> Interface Adapters -> Use Cases -> Entities)

If the dependencies between the layers do not match the order of the calls, it is necessary to adjust the direction of dependency between the layers by making use of the dependency inversion principle.

The figure is easier to understand than the explanation. (The figure quoted The Clean Architecture.)

f:id:nihma:20180917114525j:plain

This article

In the architecture design of this article, I did not follow all the principles of Clean Architecture, but we adopted only the purpose / way of thinking that improves the maintainability of the abstract layer by one way from concrete layer to abstract layer. Because if follow all principles that implementation will increase too much and disadvantage will increase.

Specifically, I broke the rules as follows and obeyed the principle only dependency between layers one-way from Interface Adapters, Frameworks and Drivers to Entities and Use Cases.

  • Interface Adapters is a layer that maps concrete layers (Frameworks and Drivers) and abstract layers (Entities and Use Cases). And this layer may depend on all layers.

f:id:nihma:20180917114858p:plain

The direction of the arrow between Interface Adapters andUse Cases is all inward.

You can probably reuse business logic even if you want to change Framework, DB etc.

Implementation

I uploaded the code to GitHub.

The directory structure is as follows.

$ tree -ad -I '.git'
.
├── .circleci ... config of CircleCI
├── app       ... application codes
│   ├── adapters    ... layer of Interface Adapters
│   │   ├── commands
│   │   ├── databases
│   │   └── http
│   │       ├── requests
│   │       └── responses
│   ├── commands    ... layer of Frameworks and Drivers(command)
│   ├── databases   ... layer of Frameworks and Drivers(DynamoDB)
│   ├── entities    ... layer of Entities
│   ├── http        ... layer of Frameworks and Drivers(Serverless Framework)
│   │   ├── controllers
│   │   ├── middlewares
│   │   ├── models
│   │   ├── utils
│   │   └── views
│   ├── providers   ... setting of DI (inversify)
│   └── usecases    ... layer of Use Cases
│       ├── implementations
│       ├── inputs
│       ├── outputs
│       └── stores
├── dockers    ... config of docker
│   ├── dynamodb
│   └── serverless
└── test       ... test codes
    ├── units
    │   ├── http
    │   │   ├── controllers
    │   │   └── middlewares
    │   └── usecases
    └── utils

33 directories

In implementation of this article, I used middy for http's middleware engine. The middleware directory is app/http/middlewares/.

aws-serverless-express and serverless-http were also candidates. However, I tried using middy which is specialized in middleware so as not to use two routing frameworks of Serverless Framework and Express. middy is still alpha version so it may not work with production code, but I felt it was fairly easy to use.

Also, by checking with tslint, we made it comply with tslint-config-standard. And I tried to write a unit test with mocha and keep it clean.

How to use

There are two ways to run it in the local environment using docker or to run it with AWS.

Using docker

Clone the Repository:

$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api

Build docker environment and Reset DynamoDB Local tables:

$ npm run docker-build
$ npm run docker-up-dev
$ npm run docker-reset-tables
$ npm run docker-down

Invoke serverless-offline in docker environment:

$ npm run docker-up
$ npm run docker-offline

Try the APIs:

# POST /todos
$ curl -X POST http://localhost:3000/v1/todos -H "Content-Type: application/json" --data '{ "text": "foo" }'
# GET /todos
$ curl http://localhost:3000/v1/todos
# GET /todos/:id
$ curl http://localhost:3000/v1/todos/:id
# PUT /todos/:id
$ curl -X PUT http://localhost:3000/v1/todos/:id -H "Content-Type: application/json" --data '{ "text": "bar", "checked": true }'
# DELETE /todos/:id
$ curl -X DELETE http://localhost:3000/v1/todos/:id

Access to DynamoDB Local admin web:

http://localhost:8000/

With AWS

It is as the How to use of the previous article.

But the implementation of this article is tag v.0.0.1. Therefore, you need to change only the tag part.

Specifically, the place to Clone the Repository and Push to the AWS CodeCommit Repository will change to the following.

# spa infra repository...
$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-terraform.git sample-spa-infra
$ cd sample-spa-infra
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra
$ cd ..
# spa api repository...
$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-api
$ cd ..
# spa web repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-web.git sample-spa-web
$ cd sample-spa-web
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-web
$ cd ..

The repositories of aws-sls-spa-sample-terraform.git and aws-sls-spa-sample-api.git will be v0.0.1.

Summary

In this article, I made a sample REST API to manage Todo with Serverless Framework and TypeScript.

  • I was able to understand the concept and implementation cost of Clean Architecture.
  • I was able to learn TypeScript coding.

*1:This is a Japanese article. I used Terraform to code AWS 'serverless service infrastructure construction.

Serverless FrameworkとTypeScriptでClean ArchitectureライクなREST APIを作ってみる

前回のTerraformでAWSサーバーレスなサービスのインフラ構築をコード化する - Inside Closure - にへろぐではサンプルとして空っぽのAPIを用意しました。せっかくなのでこのサンプルを動くように実装してみました。

何を作ったのか

特にネタがなかったのでTodoを登録・一覧・取得・更新・削除できるだけのREST APIを作成しました。

  • POST /todos : Register todo
  • GET /todos : List todos
  • GET /todos/:id : Get todo
  • PUT /todos/:id : Update todo
  • DELETE /todos/:id : Delete todo

アーキテクチャ

目指したところ

Clean Architecture(日本語訳)の考え方を取り入れたアーキテクチャにしようと思いました。*1

Clean Architectureとはざっくり下記のようにして抽象的な実装(ビジネスロジックなど)を具体的な実装(フレームワークやDBなど)から切り離して保守性を高めようとするアーキテクチャです。

  • システムを4つのレイヤ(Entities, Use Cases, Interface Adapters, Frameworks and Drivers)にわける
  • レイヤ間の依存を具体的なレイヤから抽象的なレイヤへの一方通行にする(Frameworks and DriversInterface AdaptersUse CasesEntities)

レイヤ間の依存と処理/呼び出しの順序が一致しない場合は依存性逆転の原則を駆使するなどしてレイヤ間の依存の方向を整える必要があります。

図の方がわかりやすいです。(原文より)

f:id:nihma:20180916144257j:plain

実際のところ

今回はClean Architectureの原則に全て従うのではなく具体レイヤから抽象レイヤへ一方通行にして抽象レイヤの保守性を上げるという目的/考え方だけを取り入れた構成にしました。原則に全て従うのはお遊びだしめんどくさいため小規模な開発では実装が肥大してやりすぎ感がありデメリットがメリットを上回ると判断したためです。

具体的にはルールを下記のように崩してInterface Adapters, Frameworks and DriversからEntities and Use Casesが一方通行になるところだけ原則に従いました。

  • Interface Adaptersの責務を具体レイヤ(Frameworks and Drivers)と抽象レイヤ(Entities and Use Cases)のマッピングとする。そして、全てのレイヤに依存して良いことにする。

f:id:nihma:20180916151219p:plain

Interface AdaptersUse Casesの間の矢印の向きは全て内向きになってます。*2

これだけでもひとまずServerless FrameworkやLambda、DynamoDBなどをやめたくなってもビジネスロジックは(ほぼ)再利用できるような気がします。たぶん。

実装

コードをGitHubにアップしました。

大まかなディレクトリ構成です。

$ tree -ad -I '.git'
.
├── .circleci ... CircleCI用
├── app       .. アプリケーションコード
│   ├── adapters    ... Interface Adaptersレイヤ
│   │   ├── commands
│   │   ├── databases
│   │   └── http
│   │       ├── requests
│   │       └── responses
│   ├── commands    ... Frameworks and Driversレイヤ(コマンド)
│   ├── databases   ... Frameworks and Driversレイヤ(DynamoDB)
│   ├── entities    ... Entitiesレイヤ
│   ├── http        ... Frameworks and Driversレイヤ(Serverless Framework)
│   │   ├── controllers
│   │   ├── middlewares
│   │   ├── models
│   │   ├── utils
│   │   └── views
│   ├── providers   ... DI関連
│   └── usecases    ... Use Casesレイヤ
│       ├── implementations
│       ├── inputs
│       ├── outputs
│       └── stores
├── dockers    ... docker環境設定
│   ├── dynamodb
│   └── serverless
└── test       ... テストコード
    ├── units
    │   ├── http
    │   │   ├── controllers
    │   │   └── middlewares
    │   └── usecases
    └── utils

33 directories

今回、middleware engineにはmiddyを使ってhttp周りの処理をミドルウェアに切り出しapp/http/middlewaresにまとめました。

aws-serverless-expressserverless-httpでもよかったのですがServerless FrameworkExpressフレームワーク2つを使うことになるのが何となく嫌だったのでミドルウェアに特化しているmiddyを使ってみました。まだバージョンがalphaですので業務では使えないかもですがそこそこ使いやすい気がしました。

あと一応、tslintでtslint-config-standardに準拠するようにとmochaでユニットテストを(できるだけ)書いてクリーンな状態を保つように心がけました。*3

使い方

dockerを使ってローカル環境で実行する方法とAWSで動かす方法があります。

docker環境で動かす

まずリポジトリをcloneします。

$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api

そしてdocker環境のビルドとDynamoDB Localの初期化を行います。

$ npm run docker-build
$ npm run docker-up-dev
$ npm run docker-reset-tables
$ npm run docker-down

最後にdocker環境でserverless-offlineを実行します。

$ npm run docker-up
$ npm run docker-offline

するとAPIが使えます。

# POST /todos
$ curl -X POST http://localhost:3000/v1/todos -H "Content-Type: application/json" --data '{ "text": "foo" }'
# GET /todos
$ curl http://localhost:3000/v1/todos
# GET /todos/:id
$ curl http://localhost:3000/v1/todos/:id
# PUT /todos/:id
$ curl -X PUT http://localhost:3000/v1/todos/:id -H "Content-Type: application/json" --data '{ "text": "bar", "checked": true }'
# DELETE /todos/:id
$ curl -X DELETE http://localhost:3000/v1/todos/:id

DynamoDB Localの管理画面へは下記からアクセスできます。

http://localhost:8000/

AWS環境で動かす

TerraformでAWSサーバーレスなサービスのインフラ構築をコード化する - Inside Closure - にへろぐ使い方の通りです。

ただ今回のコードにはタグv0.0.1を打ってあるのでそこだけは変える必要があります。具体的にはCodeCommitのリポジトリにGitHubのリポジトリをpushする箇所が下記に変わります。

# spa infra repository...
$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-terraform.git sample-spa-infra
$ cd sample-spa-infra
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra
$ cd ..
# spa api repository...
$ git clone -b v0.0.1 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-api
$ cd ..
# spa web repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-web.git sample-spa-web
$ cd sample-spa-web
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-web
$ cd ..

aws-sls-spa-sample-terraform.gitaws-sls-spa-sample-api.gitのclone元がv0.0.1になります。

まとめ

今回はServerless FrameworkとTypeScriptでTodoを管理するREST APIのサンプルを作ってみました。

  • Clean Architectureの考え方の理解とやりすぎると実装コストが膨らみそうなことの実感を得ることができました
  • 普通にTypeScriptを書くのはほぼ初めてだったので苦戦するところもありましたが文法が少し分かるくらいにはなれた気がするのでよかったです

*1:ちょうど書籍のClean Architecture 達人に学ぶソフトウェアの構造と設計を読んでいたというだけの理由で

*2:他のレイヤ間も依存性逆転の原則を使って矢印の向きを内向きにすることはできると思いますがレイヤ間マッピングだらけになると思います

*3:心がけただけです

TerraformでAWSサーバーレスなサービスのインフラ構築をコード化する

最近、TerraformでAWSサーバーレスなサービスのインフラ構築をコード化する機会があったので理解を深めるためにプライベートでも作ってみました。

AWSサーバーレスなサービスの全体像

今回、Terraformで構築をコード化する対象の環境です。SPA(Single Page Application)な構成としました。

f:id:nihma:20180814150726p:plain

大きくWebの部分とAPIの部分、その他の部分に分かれています。

Webの部分:

  • ユーザからアクセスを受ける CloudFront
  • SPAのHTMLを配置する S3 bucket

APIの部分:

  • SPAのHTMLからアクセスを受ける CloudFront
  • APIの機能を提供する Lambda Function
  • CloudFront と Lambda Function を橋渡しする API Gateway

その他の部分:

  • 認証のための Cognito
  • セキュリティ対策のための WAF
  • データ永続化のための Dynamo DB

インフラ構築の流れ

セットアップ用のCodeBuild

まず起点となるセットアップ用のCodeBuildです。

f:id:nihma:20180814162506p:plain

最初にTerraformを使って下記を構築します。

  • サービスのStaging環境を構築するCodeBuild
  • サービスのProduction環境を構築するCodeBuild
  • 上記のCodeBuildを順次実行するCodePipeline

そして、最後に構築したCodePipelineを実行してサービスのStaging環境とProduction環境を構築します。

Staging環境の構築後には承認アクションが必要なようにしました。Staging環境の確認で問題が見つかった場合は承認を却下して以降の構築を停止することもできます。

サービスの環境を構築するCodeBuild

次はサービスのStaging環境を構築するCodeBuildおよびProduction環境を構築するCodeBuildです。こちらもTerraformを使います。

f:id:nihma:20180814162517p:plain

まず、他のリソースに依存しない、WebやAPIでは無い部分の下記を構築します。

次にWebの下記を構築します。ここで得られたCloudFront(Web)のURLは後続で構築するAPIのCORSに設定します。

  • S3 bucket(Web)
  • CloudFront(Web)

そして、API部分の構築・デプロイを行うCodeBuildを構築・実行します。これで下記が構築・デプロイされます。下記の構築・デプロイには Serverless Framework を使います。ここで得られたAPI Gatewayドメイン名は後続で構築するCloudFront(API)の紐付け先に設定します。

次にAPIのCloudFrontを構築します。ここで得られたCloudFront(API)のURLは後続で構築するWebのHTMLにAPIの呼び出し先URLとして設定します。

  • CloudFront(API)

最後にWeb部分のデプロイを行うCodeBuildを構築および実行します。これで下記がデプロイされます。ここは Angular を使います。

  • HTML(Web)

Production環境の場合はこのタイミングで下記を構築します。これらもサービスのCodePipelineと同様にStaging環境の構築後には承認アクションがあります。

  • APIのStaging環境を構築・デプロイするCodeBuildとProduction環境を構築・デプロイするCodeBuildを順次実行するCodePipeline
  • WebのStaging環境にデプロイするCodeBuildとProduction環境にデプロイするCodeBuildを順次実行するCodePipeline

Staging環境の構築とProduction環境の構築を結ぶCodePipeline

サービス全体およびAPIの部分、Webの部分はそれぞれにCodePipelineが構築されます。

f:id:nihma:20180814173736p:plain

それぞれCodeCommitのmasterブランチに変化があると実行されます。

APIの部分とWebの部分のCodePipelineがあることでAPIの部分だけ、Webの部分だけでのデプロイが行えます。サービス全体のCodePipelineはAPIの部分とWebの部分も構築・デプロイを行います。

実装内容

コードをGitHubにアップしました。

aws-sls-spa-sample-apiaws-sls-spa-sample-webaws-sls-spa-sample-terraform でインフラ構築するための必要な最小限の実装しかありません。

使っているツールのバージョンは下記です。

  • Terraform: 0.11.7
  • Serverless Framework: 1.30.0
  • Angular: 6.0.8

インフラ部分:aws-sls-spa-sample-terraform

大まかなディレクトリ構成です。

.
├── bin ... シェルなど
├── buildspec_setup.yml      ... セットアップ用のCodeBuild向け
├── buildspec_staging.yml    ... サービスのStaging構築CodeBuild向け
├── buildspec_production.yml ... サービスのProduction構築CodeBuildCodeBuild向け
├── environments ... 構築フローに沿ったTerraformの定義
│   │── setup   ... セットアップ用
│   └── service ... サービス用
│       ├── base          ... その他の部分
│       │   ├── pre
│       │   └── after_api
│       ├── api           ... API部分
│       │   ├── staging
│       │   └── production
│       │── web           ... Web部分
│       │   ├── staging
│       │   └── production
│       └── pipeline      ... API部分/Web部分のCodePipeline
└── modules      ... 各リソースのTerraformの定義
    ├── cloudfront
    ├── cloudwatch_event
    ├── codebuild
    ├── codepipeline
    ├── cognito
    ├── dynamodb
    ├── iam
    ├── s3
    └── waf

CodeBuildが buildspec_xxxx.yml を呼び出して構築をしていきます。

それぞれのTerraformの定義は リファレンス を参照しつつ基本的に consoleで作成したリソースのterraform import を実施した結果から作りました。

例えばaws_cognito_user_poolならこんな感じです。

$ cat terraform.tfvars
access_key = "XXXXXXXXX"
secret_key = "XXXXXXXXX"
region     = "ap-northeast-1"

# 空の定義を用意する
$ cat main.tf
variable "access_key" {}
variable "secret_key" {}
variable "region"     {}

provider "aws" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region     = "${var.region}"
}

resource "aws_cognito_user_pool" "hoge" {
}

$ terraform init
# 空で定義したリソースをimportする
$ terraform import aws_cognito_user_pool.hoge ap-northeast-1_XXXXXX
# tfstateに定義がimportされるので参照して定義を作っていく...
$ cat terraform.tfstate

API部分:aws-sls-spa-sample-api

ほぼ Serverless Framework の aws-nodejs-typescript をテンプレートにして作成した素のままの状態です。

$ serverless create --template aws-nodejs-typescript

これに aws-sls-spa-sample-terraform で必要な buildspec.yml の追加と serverless.yml の修正を行いました。

Web部分:aws-sls-spa-sample-web

こちらもほぼ Angular で ng new した素のままの状態です。

$ ng new aws-sls-spa-sample-web --style=scss --routing

これに aws-sls-spa-sample-terraform で必要な buildspec.yml の追加と angular.json の修正を行いました。

使い方

セットアップ用のCodeBuildを手動で構築・実行してサービスのインフラ構築を行うまでの手順です。リソース名は例のため実際に実施するときは変える必要があるので注意が必要です。

1. GitHubリポジトリをCodeCommitに持ってくる

CodeCommitにリポジトリを作成します。

# spa infra repository...
$ aws codecommit create-repository --repository-name foobar-sample-spa-infra
# spa api repository...
$ aws codecommit create-repository --repository-name foobar-sample-spa-api
# spa web repository...
$ aws codecommit create-repository --repository-name foobar-sample-spa-web

作成したCodeCommitのリポジトリGitHubリポジトリをpushする。

# spa infra repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-terraform.git sample-spa-infra
$ cd sample-spa-infra
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra
$ cd ..
# spa api repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-api.git sample-spa-api
$ cd sample-spa-api
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-api
$ cd ..
# spa web repository...
$ git clone -b v0.0.0 https://github.com/nihemak/aws-sls-spa-sample-web.git sample-spa-web
$ cd sample-spa-web
$ git checkout -b master
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-web
$ cd ..

2. セットアップ用のCodeBuildを動かすために必要なリソースを作る

CodePipelineの承認アクションで使うSNSのトピックを作成する。

$ aws sns create-topic --name foobar-sample-spa-approval-topic
$ aws sns subscribe --topic-arn arn:aws:sns:ap-northeast-1:<account-id>:foobar-sample-spa-approval-topic \
                    --protocol email \
                    --notification-endpoint <your email>
# and confirm...
$ aws sns confirm-subscription --topic-arn arn:aws:sns:ap-northeast-1:<account-id>:foobar-sample-spa-approval-topic \
                               --token <token value>

Terraformの状態管理ファイルを保存するS3のバケットを作成する。S3のバケット名は全世界でユニークにする必要があるので注意が必要です。

$ aws s3 mb s3://foobar-sample-spa-terraform-state --region ap-northeast-1

CodeBuildを動かすために必要なIAMロールを作成する。 TF_VAR_service_name に指定した文字列が作成されるリソースのプレフィックスになるので他と被らない、長すぎない文字列を使うように注意が必要です。

$ cat Trust-Policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
$ aws iam create-role --role-name foobar-sample-spa-setup-codebuild \
                      --assume-role-policy-document file://Trust-Policy.json
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
                             --role-name foobar-sample-spa-setup-codebuild

セットアップ用のCodeBuildを作成する。

$ cat Source.json
{
  "type": "CODECOMMIT",
  "location": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/foobar-sample-spa-infra",
  "buildspec": "buildspec_setup.yml"
}
$ cat Artifacts.json
{
  "type": "NO_ARTIFACTS"
}
$ cat Environment.json
{
  "type": "LINUX_CONTAINER",
  "image": "aws/codebuild/ubuntu-base:14.04",
  "computeType": "BUILD_GENERAL1_SMALL",
  "environmentVariables": [
    {
      "name": "TF_VAR_service_name",
      "value": "foobar-sample-spa",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_approval_sns_topic_arn",
      "value": "arn:aws:sns:ap-northeast-1:<account-id>:foobar-sample-spa-approval-topic",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_s3_bucket_terraform_state_id",
      "value": "foobar-sample-spa-terraform-state",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_codecommit_infra_repository",
      "value": "foobar-sample-spa-infra",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_codecommit_api_repository",
      "value": "foobar-sample-spa-api",
      "type": "PLAINTEXT"
    },
    {
      "name": "TF_VAR_codecommit_web_repository",
      "value": "foobar-sample-spa-web",
      "type": "PLAINTEXT"
    }
  ]
}
$ aws codebuild create-project --name foobar-sample-spa-setup \
                               --source file://Source.json \
                               --artifacts file://Artifacts.json \
                               --environment file://Environment.json \
                               --service-role arn:aws:iam::<account-id>:role/foobar-sample-spa-setup-codebuild

3. セットアップ用のCodeBuildを実行する

セットアップ用のCodeBuildを実行すると環境が順次、作成されていきます。リソースはそれぞれお金がかかるので注意が必要です。

$ aws codebuild start-build --project-name foobar-sample-spa-setup

WebのエンドポイントはCloudFrontのconsoleから探せます。

f:id:nihma:20180818173759p:plain

httpsでのみアクセスできます。反映まで少しかかるのでアクセスできるようになるまで少し待つ必要があります。

https://xxxxxx.cloudfront.net

中身がないのでAngularのデフォルトページです。

f:id:nihma:20180816220608p:plain

もしここでアクセスした時に307リダイレクトが起きたらだいぶ待つ必要があるみたいです...(何度か遭遇しました https://stackoverflow.com/questions/38706424/aws-cloudfront-returns-http-307-when-origin-is-s3-bucket

まとめ

今回はTerraformでAWSサーバーレスなサービスのインフラ構築をするサンプルを作ってみました。心残りは下記です。

  • インフラ構成をもうちょい見直したい
  • IAM権限が適当なのでちゃんと絞りたい
  • API/Webを動くようにしたい
  • Terraformは保守大変・インフラ構成変更時の挙動が怪しいなどあるので他の選択肢も試したい
  • AWS以外も試したい

Chainerでサクッとニューラル英日翻訳を試してみた

これはChainer Advent Calendar 2016の6日目です。

qiita.com

Chainerによる実践深層学習がとても読みやすくて面白かったので8章の翻訳モデルを参考に英日翻訳するプログラムを組んで遊んでみました。

今回の環境

今回は下記のMacBook Airで実施しました。

  • OS: macOS Sierra バージョン 10.12.1
  • プロセッサ: 1.7GHz Intel Core i7*
  • メモリ: 8GB

使ったライブラリなどのバージョンは下記の通りです。

  • Python: 3.5.1
  • Chainer: 1.18.0
  • Numpy: 1.10.4

あとはMacに形態素解析エンジンMeCabをインストール - Qiitaを参考にmecab(0.996)とIPA辞書(2.7.0-20070801)をインストールしました。

そしてPython3で形態素解析エンジンMeCabを使えるようにする(2016年3月版) - Qiitaを参考にmecab-python3もインストールしました。

訓練データ

英日翻訳を訓練するための英文と和文の対訳データはTatoeba例文と日本語索引を加工して作りました。

具体的には、まず最初に例文と日本語索引をダウンロードしました。

$ sh en2ja_data_download.sh

sentences.csvjpn_indices.csvが取得できます。

en2ja_data_download.shの中身は下記です。

そして、ダウンロードしたデータを加工して英文と和文の対訳データを作成しました。

$ python en2ja_setup.py

実行すると対訳の対応する英文と和文が同じ行になるようにそれぞれ英文ファイルen.txt、和文ファイルja.txtが作られます。 英文は全て小文字に変換、和文はmecab分かち書きに変換しています。 なお、今回は諸事情*1により訓練に使用した対訳データはランダムにサンプリングした2500対のみになります。

en2ja_setup.pyの中身は下記です。

翻訳モデル

Encoder-Decoder翻訳モデルにAttentionを導入したモデルを使ってみました。

f:id:nihma:20161206001154p:plain

モデルの説明はChainerによる実践深層学習以外では下記あたりが参考になると思います。

モデルを実装したTranslator.pyは下記です。

訓練の実施

今回は対訳データの学習を100回実施しました。 1回の学習ごとにTranslatorモデルのシリアライズデータをen2ja-[学習回数].modeというファイルに吐き出します。

$ python en2ja_learn.py
model new start.
embed_size: 100, source_size: 4503, target_size: 4350
model new finished. elapsed_time: 0.1[sec]
1 / 100 epoch start.
1 / 2500 line finished.
   ...
2500 / 2500 line finished.
1 / 100 epoch finished. elapsed_time: 329.8[sec] remaining_time: 32648.4[sec]
2 / 100 epoch start.
1 / 2500 line finished.
   ...
2500 / 2500 line finished.
100 / 100 epoch finished. elapsed_time: 303.1[sec] remaining_time: 0.0[sec]

この学習には8.5時間かかりました。 ちなみに今回の訓練では英語の語彙数は4503語、日本語の語彙数は4350語だったようです。*2

en2ja_learn.pyの中身は下記です。

翻訳のテスト

訓練の際に吐き出した各学習回数ごとのモデルを使って翻訳結果を出してみました。 en2ja_test.pyen-test.txtというファイルに書かれている英文を和文に翻訳します。

下記ではたまたま今回の訓練データに入っていたit's one thing to make plans, but quite another to carry them out.を試しに翻訳してみた結果です。訓練データの対応する和文は計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。です。

$ echo "it's one thing to make plans, but quite another to carry them out." > en-test.txt 
$ cat en-test.txt
it's one thing to make plans, but quite another to carry them out.
$ python en2ja_test.py
0: 私 は その 仕事 に は 、 私 の 人 に は 、 私 は 、 私 の 人 に は 、 私 は 、 私 の 人 に は 、 私
1: 私 は 彼 の 髪 が 好き です 。 <eos>
2: 私 達 は 、 その 仕事 に は ない 。 <eos>
3: 私 達 は 、 その 仕事 に 向い て いる 。 <eos>
4: 私 達 は 学校 で い つ そう し ない の は 、 私 達 の 言う こと が できる ん が あり ます 。 <eos>
5: テレビ は 、 乳製品 を 学ぶ ため に は 1 0 冊 の が ある 。 <eos>
6: 私 達 は 学校 を し て いる 。 <eos>
7: はい 、 眠く あり ませ ん が 、 実際 は 誰 に なっ た 。 <eos>
8: 計画 を 立てる 事 と し て いる の です が 。 <eos>
9: 計画 を 立てる 事 と 、 すぐ に は 正確 な 白鳥 を する こと が 好き だ 。 <eos>
10: 計画 を 立てる 事 と 、 すぐ に 平静 を 取り戻し た 。 <eos>
11: 計画 は 、 前例 の だ 。 <eos>
12: 計画 を 立てる 事 と し て い た 。 <eos>
13: 計画 を 立てる 事 と 、 それ は あり ます 。 <eos>
14: 計画 を 立てる 事 と 、 それ を する こと が 理解 でき ない 。 <eos>
15: 計画 を 立てる 事 と 、 すぐ に 腹 が 何 も 言わ なかっ た 。 <eos>
16: 計画 を 立てる 事 と し て も いい です 。 <eos>
17: 計画 を 立てる 事 と 、 私 は どこ で 聞け たい 。 <eos>
18: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
19: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は すぐ 帰っ て い たら 、 それ は 全く あり ませ ん 。 <eos>
20: 計画 を 立てる 事 と 、 それ は ない と 思っ て い た 。 <eos>
21: 計画 を 立てる 事 と し て も いい よ 。 <eos>
22: 計画 を 立てる 事 に は いつも 不平 たらたら だ 。 <eos>
23: 計画 を 立てる 事 と 、 それ を 実行 する こと が できる 。 <eos>
24: 計画 に は もう を し て いる 。 <eos>
25: 計画 を 立てる 事 と 、 それ は ない ため に なっ た 。 <eos>
26: 計画 を 立てる 事 と の ところ 上手く 行っ た 。 <eos>
27: 私 に は もう 車 に 忠実 で い た 。 <eos>
28: 計画 を 立てる 事 と 、 それ は 全く あり に 行き まし た 。 <eos>
29: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 の ある ラジオ だ よ 。 <eos>
30: 計画 を 立てる 事 と 、 それ を 実行 余裕 が ない 。 <eos>
31: 計画 を 立てる 事 と 、 それ は 全く 別 だ から 。 <eos>
32: 計画 を 立てる 事 と 、 今 は もう 勉強 し て いる ところ です 。 <eos>
33: 今 によって なく も びっくり し た ので 、 彼ら の 自由 に は 体 を 動かす こと が ない 。 <eos>
34: 計画 を 立てる 事 まで の ところ 上手く 行っ た 。 <eos>
35: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
36: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
37: 私 に たち の 部屋 を 先週 何 でも いっ て いる 人 が い ます 。 <eos>
38: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
39: 計画 を 立てる 事 と 、 それ は 収入 に どこ に も かしこ に も あり ませ ん 。 <eos>
40: 計画 を 立てる 事 と 、 彼 の 勇気 が 悪い 。 <eos>
41: 計画 を 立てる 事 に 会っ た 。 <eos>
42: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
43: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
44: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
45: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
46: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
47: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
48: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
49: 計画 を 立てる 事 と 、 それ が とりわけ 年少 の 者 たち に 影響 を つい た もの だ 。 <eos>
50: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
51: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
52: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
53: 計画 を 立てる 事 と 、 それ を 実行 する 事 と 関係 に なる 。 <eos>
54: 君 に は もう 宿題 を し て いる 。 <eos>
55: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
56: 計画 を 立てる 事 と 、 彼 の もの は 計画 に 不可能 と 失敗 し た 。 <eos>
57: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
58: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
59: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
60: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
61: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 の ある 経済 いくら て ない ? <eos>
62: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
63: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
64: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
65: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
66: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
67: 計画 を 立てる 事 と 、 それ を 実行 する 事 が 好き です 。 <eos>
68: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
69: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
70: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
71: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
72: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
73: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
74: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
75: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
76: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
77: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
78: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
79: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
80: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
81: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は すぐ 慣れる だろ う 。 <eos>
82: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
83: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
84: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
85: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
86: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
87: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
88: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
89: 計画 を 立てる 事 と 、 それ を 実行 する 事 と 子供 事 が わたし です 。 <eos>
90: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
91: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
92: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
93: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
94: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
95: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
96: 計画 を 立てる 事 と 、 つい 達 の 電話 す べき 事故 だ 。 <eos>
97: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
98: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>
99: 計画 を 立てる 事 と 、 それ を 実行 する 事 と は 全く 別 だ 。 <eos>

19回目の学習データで初めて正解になるもすぐに不正解になり36回目くらいから徐々に正解が出るようになってくるっぽい動きが見えて面白いです。

en2ja_test.pyの中身は下記です。

さいごに

今後は今回あまり時間がなくできなかった下記のようなことをしてみたいです。

  • 理論的な理解を深める
  • 今回できなかった大きめのデータでの訓練を実施してみる
  • パラメータを調整し精度の比較を行ってみる

*1:対訳データすべて(149704対)を使って訓練を実施しようとしたところ丸100日間ほどかかりそうだったので今回は対象を絞りました。

*2:今回しませんでしたが対訳データすべて(149704対)の場合の語彙数は40341語、日本語の語彙数は31789語だったようです。

TensorFlowの手書き数字認識チュートリアルからざっくりディープラーニングを勉強してみました

手書き数字画像の認識を行うTensorFlowのチュートリアルであるMNIST For ML Beginners[日本語訳]およびDeep MNIST for Experts[日本語訳]を通してディープラーニングについて勉強したので理解した内容をまとめてみました。的な話です。

f:id:nihma:20160702155302p:plain

TensorFlowとディープラーニングの概要

TensorFlow

TensorFlowとはGoogleが公開しているディープラーニングに対応した機械学習のライブラリです。Tensor(多次元配列/行列)のFlow(計算処理)をグラフ構造で定義し、それを元に演算を行います。演算処理が抽象化されているので演算デバイス変更などの対応が比較的容易と思われます。

f:id:nihma:20160702155622p:plain

実装イメージ

TensorFlowでy=x2+bにx=2,b=3を指定し計算する例です。変数はplaceholderとして定義しSessionに演算を指示するタイミングでfeed_dictで実際の値を指定します。

f:id:nihma:20160702155756p:plain

ディープラーニング

ディープラーニングとは、中間層を多層化して深く(Deep)したニューラルネットワークの機械学習(Learning)の事です。中間層が多いほど認識の精度が上がりますが学習が困難になります。学習の手法が考案され、ベンチマークテストで高い性能を示した事から注目されるようになりました。

f:id:nihma:20160702155924p:plain

チュートリアル1:MNIST For ML Beginners

まずはMNIST For ML Beginners[日本語訳]です。強引にまとめるとやりたいのことは次な感じと思います。

f:id:nihma:20160702160524p:plain

なんのこっちゃ??という感じです。

前提知識のキャッチアップ

なので、まずは前提知識のキャッチアップです。(ざっくりイメージだけでも)

f:id:nihma:20160702161132p:plain

単純パーセプトロン

単純パーセプトロンとは、入力層と出力層だけの順伝播型ニューラルネットワークの事です。中間層が無い(深く無い)ネットワークですので、このチュートリアルディープラーニングではありません。(ディープラーニングは次のチュートリアルにお預けです。)

f:id:nihma:20160702161738p:plain

単純パーセプトロンの出力層の値(y)は、結合している入力層の値(x)と重み(W)を掛けた値の総和にバイアス/閾値(b)を加算し活性化関数(f)を適用する事で求めます。

f:id:nihma:20160702161925p:plain

MNIST

MNISTとは画像認識アルゴリズムベンチマークに使われる手書き数字画像データセットです。このデータセットを認識できるように単純パーセプトロンを教師あり学習します。

f:id:nihma:20160702162105p:plain

MNISTには画像と画像が表す数値の組が含まれます。各画像のサイズは28 x 28 ピクセルで各ピクセル値は 0 (白) ~ 255 (黒) です。60000枚の訓練データと10000枚のテストデータに分かれています。

今回は、入力層の値を「ベクトルに変換した画像(28 x 28 = 784次元)」に、出力層の値を「画像が表す数値次元目のみ1で他が0となる10次元ベクトル」として進めます。

f:id:nihma:20160702162314p:plain

ソフトマックス関数

ソフトマックス関数とは「複数ある事象」のうち「ある事象」が起きる確率を求める関数です。多クラス分類問題の場合、出力層の結果を確率分布にしたいため活性化関数として用いられることが多いです。

f:id:nihma:20160702162530p:plain

ソフトマックス関数は、「ある事象」の場合の数をexp(場合の数)として計算するため、場合の数が大きい事象ほど確率を高く際立たせる事ができます。

出力層の値は、出力層全体を事象全体としてソフトマックス関数で求める事により確率値となります。

f:id:nihma:20160702195941p:plain

交差エントロピー

交差エントロピーとは、確率分布間のエントロピーの距離の事です。単純パーセプトロンが導いた結果と正解との差を求めるための損失関数として用います。損失関数は教師あり学習で用います。

f:id:nihma:20160702162945p:plain

この関数は出力層で予測した確率分布と正解の分布が遠ければ差が大きくなります。

単純パーセプトロンの学習では、この損失関数の結果をペナルティとして小さくなるように重み/バイアスを調整します。

f:id:nihma:20160702163217p:plain

確率的勾配降下法

確率的勾配降下法とは、ある関数の極小値を算出する手法です。単純パーセプトロンの重みやバイアスを調整する教師あり学習のために用います。

f:id:nihma:20160702163335p:plain

勾配降下法は、学習データに対するペナルティの総和(E)が小さくなる方向に重み(W)を更新し徐々に理想のWへと近づけていきます。動かす方向は傾き(ΔE/ΔW)が負となる方向になります。傾きに掛け合わせる学習係数(η)により動かす大きさが決まります。(バイアスも同様)

Eを全ての学習データを対象とせずに一部のデータに限定して計算量を抑えた方式確率的勾配降下法です。

f:id:nihma:20160702163536p:plain

TensorFlowで動かしてみる

前提知識を(なんとなく)キャッチアップしたところでいよいよTensorFlowで動かしてみます!

f:id:nihma:20160702163742p:plain

計算グラフ

まず、単純パーセプトロンをTensorFlowで扱えるように計算グラフで表現します。ソフトマックス関数や交差エントロピー確率的勾配降下法などの計算はTensorFlowが用意している関数で行う事ができます。

f:id:nihma:20160702200116p:plain

学習した結果を評価する部分です。

f:id:nihma:20160702200220p:plain

実装内容と実行結果

TensorFlowで数字画像の認識をします。まずはパーセプトロンの計算グラフを構築し初期化します。

f:id:nihma:20160702164151p:plain

次にMNISTデータセットを教師あり学習します。訓練データの100サンプルを1バッチとして1000バッチ分の学習を行います。

f:id:nihma:20160702164307p:plain

学習したモデルにテストデータを与えて結果を評価します。

f:id:nihma:20160702164448p:plain
f:id:nihma:20160702200405p:plain

精度は約91%となりあまり良くないです。(今の最高水準は99.79%くらい?)

f:id:nihma:20160702164639p:plain

コードの全容

下記のとおりです。

import tensorflow as tf

x = tf.placeholder("float", [None, 784])
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(x,W) + b)
y_ = tf.placeholder("float", [None,10])
cross_entropy = - tf.reduce_sum(y_ * tf.log(y))
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

init = tf.initialize_all_variables()
sess = tf.Session()
sess.run(init)

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

for i in range(1000):
    batch_xs, batch_ys = mnist.train.next_batch(100)
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

print sess.run(accuracy,
           feed_dict={x: mnist.test.images, y_: mnist.test.labels})

チュートリアル2:Deep MNIST for Experts

次はDeep MNIST for Experts[日本語訳]です。強引にまとめるとやりたいのことは次な感じと思います。

f:id:nihma:20160702165006p:plain

良くワカリマセン

前提知識のキャッチアップ

なので、まずは前提知識のキャッチアップです。(ざっくりイメージだけでも)

f:id:nihma:20160702165153p:plain

畳み込みニューラルネットワーク

畳込みニューラルネットワークとは、中間層として畳込み層とプーリング層を有する多層の順伝播型ニューラルネットワークの事です。画像認識の分野でよく用いられます。中間層を持った深いネットワークですのでようやくディープラーニングです。

f:id:nihma:20160702170211p:plain

畳み込み層

畳み込み層とは、入力(x)に対して重みフィルタ(W)をスライドさせながら適用(畳み込み)した結果の集まりである特徴マップ(c)を抽出する層です。cはxの局所的な部分を抽象化した特徴量です。

結果に適用する活性化関数にはReLUを使います。

f:id:nihma:20160702170338p:plain

プーリング層

プーリング層とは、畳み込み層の結果である特徴マップ(c)を縮小する層です。局所的な部分の特徴を維持するような縮小を行うことにより位置変更に対する結果の変化を(若干ですが)抑えることができます。

最大値のみを取り出し縮小する「最大プーリング」などがあります。

f:id:nihma:20160702170533p:plain

全結合層

全結合層とは、隣接の層とユニットが全結合した層です。畳み込みニューラルネットワークでは畳み込み層やプーリング層の結果である2次元の特徴マップを1次元に展開します。

活性化関数にはReLUを使います。

f:id:nihma:20160702170648p:plain

Adam

Adamとは、確率的勾配降下法の更新量を調整し学習の収束性能を高めた手法です。

確率的勾配降下法の(傾き)の部分を(傾きの平均値)/(傾きの標準偏差)とすることにより、始めの更新量は大きく学習が速く進み、理想の値に近づくほど更新量が減少し学習を収束させられる性質があります。

f:id:nihma:20160702170856p:plain

誤差逆伝播

誤差逆伝播法とは、多層ニューラルネットワークにおいて各層の勾配を効率的に求める手法です。出力層から入力層に向かって誤差(ペナルティ)を逆伝播させながら求めていきます。

この勾配を用いて確率的勾配降下法やAdamによる重みの調整を行います。

f:id:nihma:20160702171055p:plain

ReLU

ReLUとは、入力が0以下ならば0を出力し入力が0より大きいならば入力と同じ値を出力する非線形関数です。単純で計算量が小さく、微分すると活性状態なら1となるので誤差逆伝播法で活性状態の勾配が消えない性質がある事から活性化関数としてよく用いられます。

f:id:nihma:20160702200546p:plain

ドロップアウト

ドロップアウトとは、確率的勾配降下法等で多層ネットワークのユニットを確率的に選別して学習する手法です。ユニットの選別確率pを決めておき重み更新のたびに対象のユニットをランダムで選出します。

学習対象に過剰適合する過学習の状態を抑止する効果があります。

f:id:nihma:20160702171342p:plain

TensorFlowで動かしてみる

前提知識を(なんとなく)キャッチアップしたところでいよいよTensorFlowで動かしてみます!

f:id:nihma:20160702171451p:plain

計算グラフ

畳み込みニューラルネットワークを計算グラフで表現します。

f:id:nihma:20160702200703p:plain

ReLUやドロップアウト、Adamや誤差逆伝播法などの計算はTensorFlowが用意している関数で行う事ができます。

f:id:nihma:20160702200757p:plain

学習した結果を評価する部分です。(チュートリアル1と同じです)

f:id:nihma:20160702200857p:plain

実装内容と実行結果

まず共通で使う処理を関数にしておきます。重みとバイアスの初期化、畳み込みとプーリングです。

f:id:nihma:20160702171941p:plain

計算グラフを定義していきます。

f:id:nihma:20160702172100p:plain

引き続き、計算グラフを定義していきます。

f:id:nihma:20160702172159p:plain

各変数のサイズの変化は下図のイメージです。

f:id:nihma:20160702201005p:plain

次にMNIST訓練データの50サンプルを1バッチとして20000バッチ分の学習を行います。ドロップアウト率は0.5にしています。

f:id:nihma:20160702172446p:plain

学習したモデルにテストデータで与えて結果を評価します。精度は約99.2%となりまずまずです。(今の最高水準の99.79%には及ばないけどチュートリアル1の約91%に比べたら)

f:id:nihma:20160702172813p:plain

コードの全容

下記のとおりです。

import tensorflow as tf

def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')

sess = tf.InteractiveSession()

x = tf.placeholder("float", shape=[None, 784])
x_image = tf.reshape(x, [-1,28,28,1])
W_conv1 = weight_variable([5,5,1,32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
W_conv2 = weight_variable([5,5,32,64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1,W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
W_fc1 = weight_variable([7*7*64,1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
W_fc2 = weight_variable([1024,10])
b_fc2 = bias_variable([10])
y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
y_ = tf.placeholder("float", shape=[None, 10])
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

sess.run(tf.initialize_all_variables())

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

for i in range(20000):
    batch = mnist.train.next_batch(50)
    if i % 100 == 0:
        feed_dict = {x:batch[0],y_:batch[1],keep_prob:1.0}
        train_accuracy = accuracy.eval(feed_dict=feed_dict)
        print("step %d, training accuracy %g" % (i, train_accuracy))
    train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})

feed_dict={x:mnist.test.images, y_: mnist.test.labels,keep_prob:1.0}
print("test accuracy %g" % accuracy.eval(feed_dict=feed_dict))

良く分からなかったところ

Deep MNIST for Experts[日本語訳]の重みの初期化の説明が良くわかりませんでした。なぜ微量のノイズで初期化するのか?対称性の破れとは何なのか?誰か詳しい人...

f:id:nihma:20160702173342p:plain

参考情報

次の情報を参考にさせていただきました。

情報源 メモ
TensorFlowを算数で理解する y=x2+bの例
What is the class of this image ? MNISTの最高水準の認識率
誤差逆伝播法のノート 勾配消失問題が分かりやすかったです
30分でわかるAdam ざっくり理解できました
初めてのディープラーニング --オープンソース"Caffe"による演習付き とっかかりに良かったです
深層学習 (機械学習プロフェッショナルシリーズ) 数式分かりやすかったです
イラストで学ぶ ディープラーニング (KS情報科学専門書) 深層学習 (機械学習プロフェッショナルシリーズ) で分からなくなった時に参照しました
深層学習 Deep Learning (監修:人工知能学会) 深層学習 (機械学習プロフェッショナルシリーズ) で分からなくなった時に参照しました

まとめ

TensorFlowのMNISTを認識するチュートリアルを通して単純なパーセプトロンと畳み込みニューラルネットワークの概要について調べてディープラーニングに対する理解を少しだけ深める事ができました。

とはいえ、まだまだ数式等の理解が足りないところも多く仕組みを完全に分かるところまでには達する事ができませんでした。もう少し学習が必要に思います。理解が誤っているところも多そうです。

今後は、画像認識だけでなく自然言語処理のためのディープラーニング技術などについても調査して何かに使ってみたいです。

PHPで「1時間以内に解けなければプログラマ失格となってしまう5つの問題」の問題4と5を改めて解いてみました

前回、解いた

1時間以内に解けなければプログラマ失格となってしまう5つの問題が話題に

ですが、さすがにあんまりな内容だったので問題4と5の答えを改めてそれっぽい感じにつくってみました。

主に次のページを参考にしました。
http://qiita.com/tanakh/items/b4069a6d3485ef4278ce
http://techblog.mindpl.co.jp/2014/09/array_combination/

つまりカンニングしました。

問題4

正の整数のリストを与えられたとき、数を並び替えて可能な最大数を返す関数を記述せよ。例えば、[50, 2, 1, 9]が与えられた時、95021が答えとなる(解答例)。

<?php
require_once 'util.php';

function p4(array $xs)
{
    $xs = array_sort($xs, function($a, $b) { return "{$b}{$a}" < "{$a}{$b}"; });
    return implode($xs);
}

echo p4([50, 2, 1, 9]). "\n";
$ php 4.php 
95021

問題5

1,2,…,9の数をこの順序で、”+”、”-“、またはななにもせず結果が100となるあらゆる組合せを出力するプログラムを記述せよ。例えば、1 + 2 + 34 – 5 + 67 – 8 + 9 = 100となる(解答例)

<?php
require_once 'util.php';

function calc($expr)
{
    $f = function(array $acc, $term) {
        if ($term === '+') {
            $acc['x'] = $acc['f']($acc['x'], $acc['y']);
            list($acc['f'], $acc['y']) = ['add', 0];
        }
        else if ($term === '-') {
            $acc['x'] = $acc['f']($acc['x'], $acc['y']);
            list($acc['f'], $acc['y']) = ['sub', 0];
        }
        else if ($term === '=') {
            $acc['x'] = $acc['f']($acc['x'], $acc['y']);
        }
        else if ($term !== '') {
            $acc['y'] = $acc['y'] * 10 + $term;
        }
        return $acc;
    };

    $expr[] = '=';
    $acc = array_reduce($expr, $f, ['x' => 0, 'f' => 'add', 'y' => 0]);
    return $acc['x'];
}

function is_n($n)
{
    return function($expr) use($n) { return calc($expr) === $n; };
}

function p5()
{
    $nums = array_map('wrap_array', range(1, 9));
    $opes = replicate(8, ['+', '-', '']);
    $exprs = array_filter(direct_product(alternate($nums, $opes)), is_n(100));
    return array_map('implode', $exprs);
}

foreach (p5() as $expr) {
    echo $expr. "\n";
}
$ php 5.php 
1+23-4+56+7+8+9
12+3-4+5+67+8+9
1+2+34-5+67-8+9
1+2+3-4+5+6+78+9
123-4-5-6-7+8-9
123+45-67+8-9
1+23-4+5+6+78-9
12-3-4+5-6+7+89
12+3+4+5-6-7+89
123-45-67+89
123+4-5+67-89

util.phpはこんな感じです。

<?php

function array_sort(array $xs, $isAsc)
{
    usort($xs, function($a, $b) use($isAsc) { return $isAsc($a, $b) ? -1 : 1; });
    return $xs;
}

function alternate(array $xs, array $ys)
{
    return $xs === [] ? $ys : array_merge([$xs[0]], alternate($ys, array_slice($xs, 1)));
}

function div_qr($n, $d)
{
    return [intval($n / $d), $n % $d];
}


function replicate($n, $x)
{
    return array_map(function($n) use($x) { return $x; }, range(0, $n - 1));
}

function add($x, $y)
{
    return $x + $y;
}

function sub($x, $y)
{
    return $x - $y;
}

function count_direct_product(array $xss)
{
    $f = function(array $acc, array $xs) {
        $n = count($xs);
        $acc['direct_product'] *= $n;
        $acc['own']++;
        $acc['each'][] = $n;
        return $acc;
    };
    return array_reduce($xss, $f, ['direct_product' => 1, 'own' => 0, 'each' => []]);
}

function direct_product(array $xss)
{
    $count = count_direct_product($xss);

    $direct_product = [];
    for ($i = 0; $i < $count['direct_product']; $i++) {
        $combination = [];
        for ($q = $i, $j = 0; $j < $count['own']; $j++) {
            list($q, $r) = div_qr($q, $count['each'][$j]);
            $combination[] = $xss[$j][$r];
        }
        $direct_product[] = $combination;
    }
    return $direct_product;
}

function wrap_array($n)
{
    return [$n];
}

プログラマになりたいなぁ。

プログラマ失格になりました

プログラマになりたいなぁと思って、

1時間以内に解けなければプログラマ失格となってしまう5つの問題が話題に

解いてみました、PHPで。
トータル1時間半ちょいかかってしまいプログラマ失格になってしまいました。
残念。

解き方が全くスマートで無く、関数名、変数名もむちゃくちゃ。
ちゃんとしたプログラマになりたいなぁ。
向いてないんだろうなぁ。

問題1

forループ、whileループ、および再帰を使用して、リスト内の数字の合計を計算する3つの関数を記述せよ。

<?php

function sum1(array $list)
{
  $length = count($list);
  $sum = 0;
  for ($i = 0; $i < $length; $i++) {
      $sum += $list[$i];
  }
  return $sum;
}

function sum2(array $list)
{
  $length = count($list);
  $sum = 0;
  $i = 0;
  while ($i < $length) {
      $sum += $list[$i];
      $i++;
  }
  return $sum;
}

function sum3($sum, array $list)
{
    if ($list == []) {
        return $sum;
    }
    else {
        return sum3($sum + $list[0], array_slice($list, 1));
    }
}

echo sum1([1,2,3,4,5]). "\n";
echo sum2([1,2,3,4,5]). "\n";
echo sum3(0, [1,2,3,4,5]). "\n";
$ php 1.php 
15
15
15

問題2

交互に要素を取ることで、2つのリストを結合する関数を記述せよ。例えば [a, b, c]と[1, 2, 3]という2つのリストを与えると、関数は [a, 1, b, 2, c, 3]を返す。

<?php

// $list1の長さ分だけやる前提
function hoge(array $list1, array $list2)
{
    $r = [];
    $length1 = count($list1);
    for ($i = 0; $i < $length1; $i++) {
        $r[] = $list1[$i];
        $r[] = $list2[$i];
    }
    return $r;
}

print_r(hoge(['a', 'b', 'c'], [1,2,3]));
$ php 2.php 
Array
(
    [0] => a
    [1] => 1
    [2] => b
    [3] => 2
    [4] => c
    [5] => 3
)

問題3

最初の100個のフィボナッチ数のリストを計算する関数を記述せよ。定義では、フィボナッチ数列の最初の2つの数字は0と1で、次の数は前の2つの合計となる。例えば最初の10個のフィボナッチ数列は、0, 1, 1, 2, 3, 5, 8, 13, 21, 34となる。

<?php

function fib(array $acc, $n, $m)
{
    if (count($acc) == 100) {
        return $acc;
    }
    else {
        $acc[] = $n + $m;
        return fib($acc, $m, $n + $m);
    }
}

print_r(fib([0, 1], 0, 1));
$ php 3.php 
Array
(
    [0] => 0
    [1] => 1
    [2] => 1
    [3] => 2
    [4] => 3
    [5] => 5
    [6] => 8
    [7] => 13
    [8] => 21
    [9] => 34
    [10] => 55
    [11] => 89
    [12] => 144
    [13] => 233
    [14] => 377
    [15] => 610
    [16] => 987
    [17] => 1597
    [18] => 2584
    [19] => 4181
    [20] => 6765
    [21] => 10946
    [22] => 17711
    [23] => 28657
    [24] => 46368
    [25] => 75025
    [26] => 121393
    [27] => 196418
    [28] => 317811
    [29] => 514229
    [30] => 832040
    [31] => 1346269
    [32] => 2178309
    [33] => 3524578
    [34] => 5702887
    [35] => 9227465
    [36] => 14930352
    [37] => 24157817
    [38] => 39088169
    [39] => 63245986
    [40] => 102334155
    [41] => 165580141
    [42] => 267914296
    [43] => 433494437
    [44] => 701408733
    [45] => 1134903170
    [46] => 1836311903
    [47] => 2971215073
    [48] => 4807526976
    [49] => 7778742049
    [50] => 12586269025
    [51] => 20365011074
    [52] => 32951280099
    [53] => 53316291173
    [54] => 86267571272
    [55] => 139583862445
    [56] => 225851433717
    [57] => 365435296162
    [58] => 591286729879
    [59] => 956722026041
    [60] => 1548008755920
    [61] => 2504730781961
    [62] => 4052739537881
    [63] => 6557470319842
    [64] => 10610209857723
    [65] => 17167680177565
    [66] => 27777890035288
    [67] => 44945570212853
    [68] => 72723460248141
    [69] => 117669030460994
    [70] => 190392490709135
    [71] => 308061521170129
    [72] => 498454011879264
    [73] => 806515533049393
    [74] => 1304969544928657
    [75] => 2111485077978050
    [76] => 3416454622906707
    [77] => 5527939700884757
    [78] => 8944394323791464
    [79] => 14472334024676221
    [80] => 23416728348467685
    [81] => 37889062373143906
    [82] => 61305790721611591
    [83] => 99194853094755497
    [84] => 160500643816367088
    [85] => 259695496911122585
    [86] => 420196140727489673
    [87] => 679891637638612258
    [88] => 1100087778366101931
    [89] => 1779979416004714189
    [90] => 2880067194370816120
    [91] => 4660046610375530309
    [92] => 7540113804746346429
    [93] => 1.2200160415122E+19
    [94] => 1.9740274219868E+19
    [95] => 3.194043463499E+19
    [96] => 5.1680708854858E+19
    [97] => 8.3621143489848E+19
    [98] => 1.3530185234471E+20
    [99] => 2.1892299583456E+20
)

問題4

正の整数のリストを与えられたとき、数を並び替えて可能な最大数を返す関数を記述せよ。例えば、[50, 2, 1, 9]が与えられた時、95021が答えとなる(解答例)。

<?php

function hoge(array $list)
{
    $max = 0;
    foreach ($list as $n) {
        if ($max < $n) {
            $max = $n;
        }
    }
    $ketanum = 0;
    if ($max == 0) {
        $ketanum = 1;
    }
    else {
      while ($max > 0) {
        $max = intval($max / 10);
        $ketanum++;
      }
    }

    $nums = [];
    foreach ($list as $num) {
        $num2 = $num;

    $ketanum2 = 0;
    if ($num2 == 0) {
        $ketanum2 = 1;
    }
    else {
      while ($num2 > 0) {
        $num2 = intval($num2 / 10);
        $ketanum2++;
      }
    }
    $need = $ketanum - $ketanum2;
          $nums[] = [$num * pow(10, $need), $ketanum2, $num];
    }

    usort($nums, function($a, $b) {
        if ($a[0] == $b[0]) {
            return 0;
        }
        return $a[0] > $b[0] ? -1 : 1;
    });

    $r = 0;
    foreach ($nums as $num) {
        $r *= pow(10, $num[1]);
        $r += $num[2];
    }
    return $r;
}

echo hoge([0, 50, 2, 1, 9]). "\n";
echo hoge([50, 2, 1, 9]). "\n";
$ php 4.php 
950210
95021

問題5

1,2,…,9の数をこの順序で、”+”、”-“、またはななにもせず結果が100となるあらゆる組合せを出力するプログラムを記述せよ。例えば、1 + 2 + 34 – 5 + 67 – 8 + 9 = 100となる(解答例)

<?php

function foo($index)
{
    $arr = [];
    $now = 0;
    for ($i = $index; $i < 10; $i++) {
        $now *= 10;
        $now += $i;
        $arr[] = ['num' => $now, 'last' => $i];
    }
    return $arr;
}

function bar($index)
{
    $acc = [];
    $hoge = foo($index);
    foreach ($hoge as $h) {
        $acc[] = ['car' => $h['num'], 'cdr' => bar($h['last'] + 1)];
    }
    return $acc;
}

function flat($car, $cdr)
{
    if ($cdr == []) {
        return [[$car]];
    }
    else {
        $ret = [];
        foreach ($cdr as $x) {
        $hh = flat($x['car'], $x['cdr']);
            foreach ($hh as $h) {
                if ($h !== null) {
                    $ret[] = array_merge([$car], $h);
                }
            }

        }
        return $ret;
    }
}

function calc($s, $x, array $rest) {
    if ($rest == []) {
        return [['s' => $s, 'r' => $x]];
    }
    else {
        $acc = calc("{$s} + {$rest[0]}", $x + $rest[0], array_slice($rest, 1));
        $acc = array_merge($acc, calc("{$s} - {$rest[0]}", $x - $rest[0], array_slice($rest, 1)));
        return $acc;
    }
}

$nums = [];

$aaa = bar(1);
foreach ($aaa as $a) {
    $iii = flat($a['car'],$a['cdr']);
    foreach ($iii as $i) {
        $nums[] = $i;
    }
}

foreach ($nums as $n) {
    $cs = calc($n[0], $n[0], array_slice($n, 1));
    foreach ($cs as $c) {
        if ($c['r'] == 100) {
            echo $c['s']. "\n";
        }
    }
}
$ php 5.php 
1 + 2 + 3 - 4 + 5 + 6 + 78 + 9
1 + 2 + 34 - 5 + 67 - 8 + 9
1 + 23 - 4 + 5 + 6 + 78 - 9
1 + 23 - 4 + 56 + 7 + 8 + 9
12 + 3 + 4 + 5 - 6 - 7 + 89
12 - 3 - 4 + 5 - 6 + 7 + 89
12 + 3 - 4 + 5 + 67 + 8 + 9
123 - 4 - 5 - 6 - 7 + 8 - 9
123 + 4 - 5 + 67 - 89
123 + 45 - 67 + 8 - 9
123 - 45 - 67 + 89