kkty’s blog

web・機械学習に興味のある大学生のブログです

inversifyJSを用いてTypeScriptでDIをする

この記事では、InversifyJS を用いて TypeScript で DI をするサンプルを紹介し、また、(DIに慣れていない方向けに)DI をすると何が嬉しいのか、というのを説明します。

サンプル

「ユーザーの登録」「登録したユーザーの情報を取得」の 2 つのシンプルな機能を持ったプログラムを書きます。 また、ユーザーが登録されたときにログされるようにします。

準備

必要な依存をインストールします。

$ npm i inversify reflect-metadata typescript ts-node

tsconfig.jsonを以下の要領で作成します。 types": ["reflect-metadata"], "experimentalDecorators": true, "emitDecoratorMetaData": trueというのが InversifyJS のために必要です。

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es6", "dom"],
    "types": ["reflect-metadata"],
    "module": "commonjs",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

コード

src/User.ts

User クラスを定義します。 プロパティ2つとtoString()関数のみのシンプルなクラスです。

class User {
  constructor(public id: number, public name: string) {}

  toString(): string {
    return `User${JSON.stringify({ id: this.id, name: this.name })}`;
  }
}

src/UserRepository.ts

User クラスに対応付けられるデータの永続化に用いるためのインターフェイスを定義します。 「ユーザーの作成」と「ユーザーの取得」の 2 つの操作を定義しています。

import User from "./User";

export default interface UserRepository {
  find(id: number): User;
  create(name: string): User;
}

src/UserRepositoryImpl.ts

UserRepository の実装を用意します。 通常のアプリケーションであればここでデータベースなどを利用した実装をするかと思いますが、今回はシンプルにインメモリのオブジェクトを用いています。

後に DI で他オブジェクトに注入できるよう、inversify の@injectable()デコレータをつけています。

import { injectable } from "inversify";
import User from "./User";
import UserRepository from "./UserRepository";

@injectable()
export default class UserRepositoryImpl implements UserRepository {
  private store: { [key: string]: User | undefined } = {};
  private nextUserId: number = 0;

  constructor() {}

  find(id: number): User {
    const user = this.store[id];
    if (!user) {
      throw new Error("user not found");
    }
    return user;
  }

  create(name: string): User {
    const id = this.nextUserId++;
    const user = new User(id, name);
    this.store[id] = user;
    return user;
  }
}

src/Logger.ts

こちらはロガー用のシンプルなインターフェイスです。

export default interface Logger {
  log(message: string): void;
}

src/LoggerImpl.ts

ロガーの実装です。実際は winston などを使ったりするとは思いますが、ここでは console に流すだけにしています。 こちらも@injectable()デコレータをつけています。

import { injectable } from "inversify";
import Logger from "./Logger";

@injectable()
export default class LoggerImpl implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

src/App.ts

メインのクラスを定義します。

createNewUser(name: string)により、(UserRepository インターフェイスを実装したインスタンスによって)ユーザーが作成され、また新しいユーザーが作成されたことが(Logger インターフェイスを実装したインスタンスを用いて)ログされるようにしています。

findUser(id: number)により、特定の id に対応するユーザーのデータを取得できるようにしています。

@inject()デコレータによって、userRepositoryUserRepositoryのインスタンスが、loggerLoggerのインスタンスが注入されるようになっています。

import { injectable, inject } from "inversify";

import UserRepository from "./UserRepository";
import Logger from "./Logger";

@injectable()
export default class App {
  private readonly userRepository: UserRepository;
  private readonly logger: Logger;

  constructor(
    @inject("UserRepository") userRepository: UserRepository,
    @inject("Logger") logger: Logger
  ) {
    this.userRepository = userRepository;
    this.logger = logger;
  }

  createNewUser(name: string) {
    const user = this.userRepository.create(name);
    this.logger.log(`new user: ${user}`);
    return user;
  }

  findUser(id: number) {
    const user = this.userRepository.find(id);
    return user;
  }
}

src/inversify.config.ts

DI コンテナを作成し、使用するオブジェクトをバインドします。

import { Container } from "inversify";

import App from "./App";
import Logger from "./Logger";
import LoggerImpl from "./LoggerImpl";
import UserRepository from "./UserRepository";
import UserRepositoryImpl from "./UserRepositoryImpl";

const container = new Container();
container.bind<Logger>("Logger").to(LoggerImpl);
container
  .bind<UserRepository>("UserRepository")
  .to(UserRepositoryImpl)
  .inSingletonScope();
container.bind<App>("App").to(App);

export default container;

src/index.ts

実際に実行させてみるコードです。

import "reflect-metadata";

import container from "./inversify.config";
import App from "./App";

container.get<App>("App").createNewUser("Taro");
container.get<App>("App").createNewUser("Jiro");
console.log(container.get<App>("App").findUser(1)));

実行

実際に実行させてみると、以下のようになり、うまくいっていることがわかります。

$ ./node_modules/.bin/ts-node srd/index.ts
new user: User{"id":0,"name":"Taro"}
new user: User{"id":1,"name":"Jiro"}
User { id: 1, name: 'Jiro' }

inversifyJS、直感的でわかりやすいですね。

補足

DIコンテナへのオブジェクトの登録において、簡単のために文字列を用いましたが、Symbolを利用することが推奨されています。

何が嬉しいのか

オブジェクト指向的なプログラミングをする際、インターフェイス等を用いて「実装は抽象に依存させる」こと、そして「できるだけ抽象への依存を増やす」ことが大切です。 そうすることでオブジェクト同士が疎結合になり、テストもしやすく、各オブジェクトの実装の変更が簡単な設計になります。

例えば、今回のAppクラスはLoggerとUserRepositoryという抽象(インターフェイス)に依存をしていました。 そして、LoggerImplという実装はLoggerという抽象に依存し、UserRepositoryImplという実装はUserRepositoryという抽象に依存していました。

このことにより、AppクラスはLoggerの実装やUserRepositoryの実装と切り離されていました(疎結合)。

実際、UserRepositoryに関して、(今回や、テスト時のために)単純なオブジェクトを用いた実装を用意しても、MySQLを用いた実装を用意しても、Appクラスのコードは全く変わらないことがわかると思います。

このように、インターフェイスを用いて依存の方向を制御するとソフトウェアの構成がいい感じにはなるのですが、オブジェクトの作成・その受け渡しが結構面倒になります。

今回のケースだったら、

const logger = new LoggerImpl();
const userRepository = new UserRepository();
const app = new App(userRepository, logger);

程度で済んだりもしますが、オブジェクトの数が増え、依存性も複雑になってくるとかなりしんどくなってくるはずです。

それの面倒を見てくれるのが、inversifyJSのようなDIフレームワーク、DIコンテナというわけです。

参考

inversify.io

github.com

クラウドとディープラーニングと価格予測の話

AlpacaJapanさん主催のMarketTech Meetup #02でLTをさせていただいたので、その時の内容をここにもまとめておきます。

スライドはこちらです。

www.slideshare.net

ディープラーニングで価格予測モデルを作る

「ディープラーニングで金融商品の価格予測モデルを作る」という場合に、

  • いろいろなアーキテクチャ、ハイパーパラメータ、データで学習したい
  • いろいろな学習済みモデル、データ、ハイパーパラメータでバックテストをしたい
  • 複数のモデルを組み合わせて一つのモデルを作る、すなわちアンサンブルをしたい

といったことがしたくなります。

しかし、当たり前ながらディープラーニングの学習にはには時間がかかります。 アーキテクチャ5種類、ハイパーパラメータ5種類、データ5種類だけでも、組み合わせによって125通りのモデルができます。 それぞれに1時間かかっていたら5日間かかります。

バックテストにおいても、組み合わせが増えるとバカにならない時間がかかります。

クラウドを使っていいい感じにディープラーニングしよう!というのが、今回のトピックです。

クラウドでディープラーニングをすると

クラウドの特徴として「独立した処理については、一つずつ動かしても、一気に動かしても、かかるコストは同じ」ということがあげられます。 この特徴は、今考えている問題と非常に相性がいいです。

さきほどの例、つまり1時間かかる処理が125個ある例、では、5日間ではなく、1時間ですべての処理が終わらせることができます。

また、「様々な種類のマシンを利用できる」というのもクラウドの大きな利点です。GPUが載っているのも多数あります。

また、今回のような処理は、ユーザーを待たせているわけではないため「すぐに結果が必要」でないこともあります。

そのため、「需要の低い時間に安い価格でインスタンスを借りて計算する」ということも可能になります。AWSでいうスポットインスタンス、GCPでいうプリエンプティブインスタンスです。このおかげで思ったよりコストがかさまなかったりします。

ちなみに自分たちはAWSのスポットインスタンスを用いることで、p2.xlargeというインスタンスを70%オフくらいで使うことができています。

考えなくてはいけないこと

もちろんいいことだけではなく、考えなくてはならないこともあります。

まず1つ目が「処理実行の管理をしないといけない」ということです。いくつかのインスタンスを立ち上げ、いい感じに分散させて処理を実行する必要があります。また、スポットインスタンスを用いる場合には考えることが増えます。ローカルで実験するのに比べると、とても手間がかかります。

そして2つ目が「実行結果が分散されてしまう」ということです。それぞれの処理の結果は複数のインスタンスに散らばっている状態になるので、それをどうにかして一箇所に集めることが必要になります。やはりローカルで実験するよりは手間がかかります。

その上で、実際どうやっているか

上にあげたようなことを考え「実際にこんな感じでクラウドで価格予測モデルを作っているよ」という話をします。

実際どうやっているか①

まず、学習のためのDockerコンテナを作成します。 実行コマンドのコマンドライン引数によって、アーキテクチャ、データ、ハイパーパラメータを指定できるようにします。

また、学習が終わったら、生成されたモデルをS3などのクラウドストレージに、パラメータなどをデータベースに保存するようにしています。こうしていろいろなマシンで実行された学習の結果が一箇所に集まるようにします。

余談ですが、Nvidia Dockerを用いることでDockerでもGPUを活用できます。

実際どうやっているか②

そしてあとは学習の実行ですが、自分たちはAWS Batchというサービスを用いています。 このサービスは、ジョブのキューを用意してくれます。ジョブというのは、Dockerイメージと実行コマンドの組み合わせです。

自分たちがキューにジョブを追加すると、AWS Batchがうまいことインスタンスを立ち上げ、そのジョブを実行してくれるというわけです。 スポットインスタンスも簡単に利用できるようになっています。

実際どうやっているか③

バックテストも学習と同様です。 Dockeイメージを用意し、コマンドライン引数によってテストしたいモデルや使いたいデータなどを指定するようにします。 そして、実行が終了した際に結果をクラウドストレージやデータベースに保存するようにしています。 また、AWS Batchを用いているのも同様です。

実際どうやっているか④

AWSのコンソールやCLIのみでこれらの操作すべてをやるのは厳しく、また機能的にも足りないところが出てくるので、機能を拡張し、そしてGUIも用意しています。

具体的には、

  • 学習ジョブ・バックテストジョブの追加
  • 実行中ジョブのログの表示
  • 学習済みモデルの詳細を表示
  • 実行済みバックテストの詳細を表示

といった機能をもたせています。

これのおかげで、スマホからでも学習・バックテストを回したり、それらの結果を確認できたりもします。

おわりに

クラウドを使うとディープラーニングが快適にできます。そして、クラウドプロバイダはディープラーニングに役立つサービスをたくさん提供しているので利用すべきです。

おまけ

本当に相場を学習して勝てているのかの見定めは難しいです。「4回の大きい価格変動を当てる」モデルは16個に一個くらいはできてしまいます。

また、探索空間が膨大に増えますが、複数モデルのアンサンブルもしています。

winstonでのエラーログにエラーメッセージを含める

winstonでエラーをロギングするときに発生する問題

以下のようにwinstonのloggerを用意したとします。

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
  ],
});

そして以下のような文を実行します。

logger.info('something has happened');

結果は想像の通り、以下のようになります。

{"message":"something has happened","level":"info"}

しかし、以下の場合はどうでしょう。

logger.error(new Error('something has happened'));

このような出力になり、スタックの内容やエラーメッセージは表示されません。

{"level":"error"}

解決策

そこで、loggerの定義を拡張して以下のようにします。

loggerに渡されたオブジェクト(iとしています)がErrorオブジェクトのインスタンスだった場合に、i.messageの内容をログ出力のmessageとして設定するようにしています。

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format((i) => {
      if (i instanceof Error) {
        return Object.assign({}, i, {
          message: i.message,
        });
      }

      return i;
    })(),
    winston.format.json(),
  ),
  transports: [
    new winston.transports.Console(),
  ]
});

さきほどと同じ文を実行すると以下のような出力となり、うまくいっていることがわかります。

{"level":"error","message":"something has happened"}

stackなど、Errorオブジェクトの他のプロパティに関しても、同じようにして追加することができるはずです。

おまけ

winstonのloggerは、オブジェクトを渡されたとき、列挙可能なプロパティ(とそれに対応する値)のみをログに含めるようです。Errorオブジェクトのmessageプロパティややstackプロパティは列挙可能ではないので、このようなことになっているみたいです。

※ 執筆時のwinstonのバージョンは3.2.1です。

winstonでexpressのリクエストログを扱う

expressにおけるリクエストのロギングはmorganを使うのが鉄板ですが、winstonなどの他の高機能なロガーで、まとめてログを扱いたいことが多いと思います。

この記事では、どのようにしてそれを実現するのかを簡単な例で説明します。

準備

空のディレクトリを作成し、以下のようにしてexpress, morgan, winstonをインストールします。

$ npm install express morgan winston

コード

ルートが/のみの簡単なサーバーを例にとることにします。

具体的には以下のようなことをしています。

  • loggerという名前のwinstonのロガーインスタンスを作成
  • morganでリクエストログを(文字列の状態で)winstonに流し込むように
  • ステータスコード200を返すだけのルートを作成
  • ポート3000でリクエストを受け付けるように
const express = require('express');
const morgan = require('morgan');
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
  ],
});

const app = express();

app.use(morgan('tiny', {
  stream: {
    write: message => logger.info(message.trim()),
  },
}));

app.get('/', (req, res) => {
  res.sendStatus(200);
});

app.listen(3000);

確認

上のコードをindex.jsとして保存し、以下のコマンドでサーバーを立ち上げます。

$ node index.js

curlでリクエストを送ってみます。

$ curl http://localhost:3000/
OK

サーバーのアプリケーションの標準出力に以下のように出力されているはずです。うまくいっていることがわかります。

{"message":"GET / 200 2 - 3.712 ms","level":"info"}

DynamoDBから特定のテーブルの全項目データを取得する(TypeScript/Node.js)

AWS SDKのDynamoDB.DocumentClientscanメソッドを用いるのですが、そのままではテーブルのサイズが大きい場合に一部のデータしか取得できません。

バッチ処理などで、テーブル内の全項目を取得したいシーンもあると思います。(頻繁にそういった操作が必要になるのなら、アプリケーションの設計ミスのような気がしますが...)

そういった際はLastEvaluatedKeyを用いれば簡単に実現可能です。以下のコードはTypeScriptですが、JavaScriptの場合もほぼ同じです(型定義を消せばいいだけ)。

import awsSdk from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client';

const documentClient = new awsSdk.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
});

const scanAll = async (params: DocumentClient.ScanInput) => {
  const items: DocumentClient.AttributeMap[] = [];

  let lastEvaluatedKey: undefined | DocumentClient.Key;

  while (true) {
    const res = await documentClient.scan({
      ...params,
      ExclusiveStartKey: lastEvaluatedKey,
    }).promise();

    if (res.Items) {
      res.Items.forEach(item => items.push(item));
    }

    if (!res.LastEvaluatedKey) {
      break;
    }

    lastEvaluatedKey = res.LastEvaluatedKey;
  }

  return items;
};

以下のように用いることができます。

scanAll({ TableName: 'テーブル名' })
  .then((items) => {
    // ...
  });