kkty’s blog

おそらく大学3年生です

Numbaを用いたPython/Numpyの高速化

f:id:k-kty:20190216135709p:plain

まえがき

機械学習関連のプロジェクトではPythonを使う人が多いと思うのですが、Pythonでの処理って遅いですよね。

Tensorflowでの学習などにおいてはPython側で計算をしているわけではないので大丈夫なのですが、前処理の際などにPythonの実行速度の遅さがとても気になります。コンパイラ言語とスクリプト言語を比べるのは酷ですが、「C++なら数秒で終わるのになあ」という処理も数分かかってしまったりします。

もちろんC++のプログラムに匹敵するほどの速度を出すのは無理ですが、できるだけ高速化したいですよね。 そのための方法はいろいろあるのですが、今回はNumbaというJITコンパイラを用いる方法を紹介したいと思います。

Numbaとは

公式サイトによると、

Numba is an open source JIT compiler that translates a subset of Python and NumPy code into fast machine code.

「Python/Numpyコード(の一部)を、高速な機械語に変換するためのオープンソースJITコンパイラ」ってことみたいですね。

もう少し細かい特徴としては以下のような感じです。

  • 関数にデコレータをつけるだけで動作する
  • Numpyの配列や関数を用いた処理に特化している
  • ランタイム時に、最適化された機械語を作成して実行してくれる
  • Numbaを利用するためにPythonのインタープリターを変えたり、コードの実行前にコンパイルするようにしたりする必要がない
  • CPU/GPUを用いた並列計算のための機能もある

実際に使ってみる

高速化前のコード

以下のような関数があったとします。 配列(arrとします)を受け取り、[log(arr[1]/arr[0]), log(arr[2]/arr[1])... ] のような(長さがarrより1短い)配列を返します。

def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in range(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

以下のコードを用いて測定をすると(かなり雑ですが、今回はだいたいのパフォーマンスの比較ということで...)、出力は 183.20000410079956 となりました。約3分です。 なお関数内のnumpy.empty(...)の部分は計算とは関係ありませんがe-5秒くらいのオーダーだったので無視してよさそうです。(ちなみにですがnumpy.random.rand(...)の部分は無視できないほど時間がかかります。)

import time
import numpy

# 以上の関数定義

if __name__ == '__main__':
  arr = numpy.random.rand(10 ** 8)

  start_t = time.time()
  calculate_diffs(arr)
  end_t = time.time()

  print(end_t - start_t)

JITを有効化してみる

以下のようにデコレータをつけて、JITを有効化してみます。

@numba.jit(nopython=True)
def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in range(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

測定の結果、出力は1.805896282196045となり、100倍ほどの高速化に成功しました。

JITを有効化/並列化してみる

以下のようにデコレータを変更し、range関数をnumba.prange関数に置き換え、並列化してみます。 ret = numpy.empty(arr.size - 1)の部分は、numbaが勝手にうまいこと(hoist)してくれて、期待通りの動作をします。

@numba.jit(nopython=True, parallel=True)
def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in numba.prange(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

測定の結果、出力は0.7335679531097412となり、200倍以上の高速化に成功したことになります。

まとめ

限られたケースにはなりますが、Numbaをうまく使えば非常に簡単に高速化ができることがわかりました。

Express/Node.jsでTypeScriptを使ってみる

f:id:k-kty:20190213160011p:plain

最近、バックエンド開発でNode.jsを用いています。 JavaScriptは仕様がどんどん良くなってきているし、コールバックを用いた非同期処理も個人的には好きです。

しかしコードベースが大きくなると「型があればなあ...」と思うこともしばしば。 そこでTypeScriptに足を突っ込んでみることにしました。

そのときの記録の一部として、TypeScript + Node.js/ExpressでWebバックエンドの開発を始める方法を紹介します。

TypeScriptの文法・言語の機能についてはあまり触れません。(それらについては以下が参考になります。)

準備

npmプロジェクトを作成する

npm init を用いるなどして、package.jsonを作成します。

TypeScriptをインストール

$ npm i typescript --save-dev

JavaScriptからTypeScriptへのコンパイルに必要です。

TypeScriptの設定ファイルを作成

以下のような内容をtsconfig.jsonという名前で保存します。

{
  "compilerOptions": {
    "target": "es5",
    "outDir": "dist",
    "strict": true
  },
  "include": [
    "src/**/*",
  ]
}

軽く説明すると、このようになります。

  • target で、「どの仕様に対応したJavaScriptのコードをコンパイルによって生成するか」を指定しています。ここではES5を指定しています。
  • outDir で、コンパイル後のコードを保存するディレクトリを指定します。
  • strict をtrueにすることで、いろいろなTypeScriptのチェック機構が有効になります。
  • include で、コンパイルの対象にしたいファイル群を指定します。

expressとその「型定義ファイル」をインストール

$ npm i express --save
$ npm i @types/express --save-dev

このようにパッケージと一緒に型定義ファイルもインストールすることで、そのパッケージの関数やオブジェクトを使うときにもTypeScriptの型チェックを利用できるようになります。

ts-nodeをインストール

$ npm i ts-node --save-dev

これにより、node コマンドでJavaScriptコードを実行できるように、ts-node コマンドでTypeScriptコードを実行できるようになります。 ts-nodeは、「tscを用いてTypeScriptのコードをJavaScriptへコンパイルし、それをnodeで実行する」ということをしてくれています。 開発中に使うと便利です。なお、本番環境での仕様は推奨されていません。

実行スクリプトを追加

package.jsonの scripts の中身を以下のように書き換えます。

{
  "build": "tsc",
  "start": "node dist/index.js",
  "dev": "ts-node src/index.ts"
}

npm run build でTypeScriptのコードをコンパイルし、npm run start でそのJavaScriptコードを実行できるようになります。 npm run dev はその2つを組み合わせたことをやってくれますが、開発中のみ利用します。

Hello, Worldしてみる

コード

src/index.ts

import app from './app';

app.listen(process.env.PORT || 3000);

src/app.ts

import express from 'express';
import routes from './routes';

const app = express();

app.use('/', routes);

export default;

src/routes.ts

import express from 'express';

const router = express.Router();

router.get('/', (req, res, next) => {
  res.send('Hello, World');
});

export default router;

起動

以下のコマンドでサーバーが起動します。

$ npm run dev

以下のように、きちんと動いていることがわかります。

$ curl http://localhost:3000/
Hello, World

テストをする

supertest を用いて、エンドポイント単位のテストをしてみます。

準備

supertestのインストール

supertestは、Node.jsのHTTPサーバーをテストするためのライブラリです。 型定義ファイルも同時にインストールします。

$ npm i supertest @types/supertest --save-dev

参考: https://github.com/visionmedia/supertest

mochaのインストール

mochaは、テストのためのフレームワークです。 下のコードに出てくる describe とか it とかいう関数はmochaが提供しているものです。 やはり、型定義ファイルも同時にインストールします。

$ npm i mocha @types/mocha --save-dev

参考: https://mochajs.org/

実行スクリプトを追加

package.json内の scripts に以下を追加します。 これにより、 npm run test でテストを実行できるようになります。

"test": "mocha --require ts-node/register test/**/*.ts"

--require ts-node/register により、JavaScriptによってコンパイルすること無くテストを実行できます。

コード

test/routes.ts

とても簡単なものです。

import request from 'supertest';
import 'mocha';

import app from '../src/app';

describe('ルーティング', () => {
  describe('/', () => {
    it('ステータスコード200を返す', async () => {
      await request(app)
        .get('/')
        .expect(200);
    });
  });
});

import mochadescribeit を使うために必要です。

実行

$ npm run test
  ルーティング
    /
      ✓ ステータスコード200を返す
  1 passing (37ms)

テストが通りました。

ビルド・実行する

本番環境で実行する際には、JavaScriptにコンパイル(ビルド)して、その結果生成されたファイルをNodeで実行する必要があります。 このための準備はすでにできていて、以下のスクリプトを実行するだけで大丈夫です。

$ npm run build
$ npm start

AWS S3 の特徴まとめ

AWSを使っている人全員が触ったことがあるであろうS3。整理のためのメモ。

  • データを複数の場所に複製するという特徴から、データの更新・削除には結果整合性が採用されている
  • Getでファイルをダウンロードできる
  • Putでファイルをアップロード(新規/更新)できる(最大5GBまでだが、Multipart Uploadを利用すると5TBまで可能)
  • Listでオブジェクト一覧を取得できる(最大1000件まで)
  • Copyでオブジェクトを複製できる(最大5GBまでだが、Multipart Uploadを利用すると5TBまで可能)
  • Deleteでオブジェクトを削除できる(最大1000個)
  • Headでオブジェクトのメタデータを取得できる
  • RestoreでアーカイブされたオブジェクトをS3に取り出すことができる
  • ユーザーポリシー/バケットポリシー/ACLを用いたアクセス権の決定ができる
  • Get/Putについては、Pre-signed URLを利用することでセキュアに操作を行うことができる
  • 静的なwebサイトをホスティング可能
  • AJAXなどを利用して、異なるドメインからS3でホスティングしているファイルにアクセスしたい場合にはCORSの設定をする
  • ホスティングの際は、CloudFrontとともに使うことを推奨
  • VPC Endpointを利用すると、VPC内のPrivate SubnetからNAT GatewayやNATインスタンスを経由せずに直接S3とセキュアに通信ができる(同一リージョンの場合のみ)
  • クロスリージョンレプリケーションを用いると、異なるリージョン間でオブジェクトのレプリケーションが可能
  • バージョン管理が可能(バケットに対して設定する)
  • ライフサイクル管理を用いると、バケット全体/Prefixに対して、日単位でのストレージクラスの変更/削除処理が可能
  • オブジェクトをアーカイブすると、データはGlacierに移動する
  • S3インベントリを利用すると、オブジェクトのリストをcsvとして取得できる
  • バケットにイベントが発生した際に、SNS/SQS/Lambdaなどに通知(連携)することができる
  • LambdaでS3バケットのイベントを利用したい場合は、Lambdaが利用するIAM RoleにS3の権限を付与する必要がある
  • 大きなサイズのファイルをダウンロードする際にRange Getを利用することで、マルチスレッド環境で高速なダウンロードが可能(Multipart Upload時と同じチャンクサイズを利用)
  • Multipart Uploadはチャンクサイズと並列コネクション数のバランスが重要
  • ファイルが100MBを超える場合に、Multipart Uploadの利用が推奨される
  • Multipart Uploadでは各チャンクは5GB以下である必要がある
  • バケットへのリクエストが定常的に数百RPSを超える場合は、キー名の先頭部分の文字列をランダムにすると良い

参考になるサイト・スライド

aws.amazon.com

www.slideshare.net