読者です 読者をやめる 読者になる 読者になる

ABEJA Tech Blog

株式会社ABEJAのエンジニアたちがAI, IoT, Big Dataなど様々なテーマについて発信します

pandas DataFrameを省メモリにpickleする

ABEJAでデータエンジニアをしています、千葉です。

少し前に、pandasのDataFrameをファイルに読み書きする際にメモリを消費しすぎる問題を発見したので、解決策を含めて紹介します。

通常手法の紹介

通常、DataFrameをファイルに保存する際には、pandasの提供するIOモジュールを使用します。

今回は、細かい変換規則を書く必要のないPython Pickleをベースとしたto_pickle機能について取り上げます。

# Dumping pandas.DataFrame
import pandas
df = pandas.DataFrame(..., columns=...)

df.to_pickle(output_file_path)
# Restoring pandas.DataFrame
import pickle

with open(input_file_path, 'rb'):
    df = pickle.load()

上記のようにして、非常に簡単にDataFrameを完全に入出力できます。

通常手法の課題

ここで、注意すべき点として、pickleのメモリ効率の悪さが挙げられます。 実際に実メモリサイズがGBクラスのDataFrameを作成し、パフォーマンスを測定したログが下記になります。

テストコード

GB = 1024 * 1024 * 1024
df = None

def prepare_data():
    global df
    row_count = 40000000
    print('generating data.')
    print('row count', row_count)
    series_1 = numpy.random.randn(row_count)
    series_2 = numpy.random.randn(row_count)
    series_3 = numpy.random.randn(row_count)
    df = pandas.DataFrame({'a': series_1,
                           'b': series_2,
                           'c': series_3})
    return df


def run_to_pickle():
    result_path = 'run_to_pickle.bin'
    df.to_pickle(result_path)


def run_load_pickle():
    result_path = 'run_to_pickle.bin'
    df = pickle.load(open(result_path, 'rb'))

結果

-------prepare_data-------
generating data.
row count 40000000
sizeof df: 2305.9765625MB

-------run_to_pickle-------
Maximum memory usage: 4137.2109375MB
Elapsed time: 3.702019843040034

-------run_load_pickle-------
Maximum memory usage: 2322.0859375MB
Elapsed time: 5.678721209987998

※各関数の実行後には、ガベージコレクションを実行しています。

※メモリ使用量の測定には、memory_profiler.memory_usageを使用しています。

※経過時間の測定には、timeitを使用しています。

残念ながら、約2GBのDataFrameに対して、出力時に最大約4GBほどのメモリを使用しています。およそ2倍です。

調査した結果、

  • オブジェクトのコピーが発生すること
  • 逐次入出力をできないこと

が原因のようです。

解決手法

今回の解決手法では、下記のような性質を持つ入出力用のラッパークラスを設計します。

  • DataFrameをチャンク化して逐次入出力をさせることで、最大メモリ使用量を削減する。
  • pandas系オブジェクトをnumpy系オブジェクトに変換して扱うことで、高速化・小型化する。

イメージ図 - エンコーディング

f:id:archibay:20170202152706p:plain

イメージ図 - デコーディング

f:id:archibay:20170202152710p:plain

コード例

# 部品番号と部品データ格納用のクラス
# indexは、複数のDataFrameを一括で入出力するための索引
class SerializationParts(object):
    def __init__(self, name, index, data):
        self.name = name
        self.index = index
        self.data = data

    def blueprint(self, binary_size):
        return SerializationParts(self.name, self.index, binary_size)

SerializationPartsに部品番号と、部品データを格納します。 設計図生成時には、バイト長を記録するようにします。

# propertiesに登録された変数を SerializationParts に格納し、
# yieldする機能のベースクラス
class WrapperBase():
    properties = []

    def __custom_encode__(self, property_name, index):
        # Please override this function
        yield SerializationParts(name=property_name, index=index, data=getattr(self, property_name))

    def encode(self, index):
        # 最初にクラス情報をパーツとして作成
        yield SerializationParts('base', index, self.__class__)
        for property_name in self.properties:
            # プロパティをそれぞれパーツとして作成
            for a_parts in self.__custom_encode__(property_name, index):
                yield a_parts

    def __custom_decode__(self, parts):
        # Please override this function
        setattr(self, parts.name, parts.data)

    def decode(self, parts):
        self.__custom_decode__(parts)

WrapperBaseに、逐次パーツ作成機能を持たせておきます。 custom_encode、 custom_decodeをオーバーライドすることで、サブクラスは 逐次処理を制御できます。

# DataFrameのラッパークラス

class DataFrameWrapper(WrapperBase):
    properties = ['df', 'max_rows']

    def __init__(self, df, max_rows=1000000):
        super(DataFrameWrapper, self).__init__()
        self.df = df
        self.max_rows = max_rows

    def __custom_encode__(self, property_name, index):
        max_rows = self.max_rows
        # dfプロパティは複数パーツに分割
        if property_name == 'df':
            # 列名、型、Indexを構造情報としてパーツ化
            structure = {'index': numpy.array(self.df.index),
                         'columns': list(self.df.columns),
                         'dtypes': list(self.df.dtypes)}
            yield SerializationParts(['df', 'structure'], index, structure)
            row_counts = len(self.df.index)
            # 数値データは、列毎・max_rows毎にパーツ化
            for series in list(self.df.columns):
                series_name = series
                row_loop_count = 1 + int(row_counts / max_rows)
                for i in range(row_loop_count):
                    # numpy.arrayを使うことで、飛躍的に省メモリ化・高速化を実現できる。
                    yield SerializationParts(
                        ['df', series_name, i], index,
                        (max_rows,
                         numpy.array(self.df[series_name][max_rows * i: min(max_rows * (i + 1), row_counts)])))
        else:
            # dfプロパティ以外は、WrapperBaseの機能を使用
            return super(DataFrameWrapper, self).__custom_encode__(property_name, index)

    def __custom_decode__(self, parts):
        if isinstance(parts.name, list) and parts.name[0] == 'df':
            if parts.name[1] == 'structure':
                l = len(parts.data['index'])
                dtype_dict = OrderedDict()
                for k, v in zip(parts.data['columns'], parts.data['dtypes']):
                    # numpy.arrayを使うことで、飛躍的に高速化を実現できる。
                    dtype_dict[k] = numpy.ndarray(shape=l, dtype=v)
                # 現状、最も高速な行数固定の空DataFrameの生成処理。
                self.df = pandas.DataFrame(dtype_dict, index=parts.data['index'])
                del dtype_dict
            else:
                row_counts = len(self.df.index)
                max_rows, series_data_parts = parts.data
                series_name = parts.name[1]
                series_parts_no = parts.name[2]
                series_data_parts = pandas.Series(series_data_parts)
                # 数値データの設定処理
                self.df[series_name][max_rows * series_parts_no: min(max_rows * (series_parts_no + 1), row_counts)] = \
                    series_data_parts
        else:
            # dfプロパティ以外は、WrapperBaseの機能を使用
            super(DataFrameWrapper, self).__custom_decode__(parts)

WrapperBase クラスを継承し、pandas.DataFrame 用のラッパーを作成します。 プロパティ df に pandas.DataFrame をセットできるようにします。 構造情報の別パーツ化、数値データのChunk処理を行い、逐次処理可能にします。 エンコード・デコードの過程に、pandas.Seriesやpandas.DataFrameの使用を極力避け、 numpy.arrayを使用することで、パフォーマンスを飛躍的に向上させることができます。

# エンコーダークラス
class Encoder(object):
    def __init__(self, data_file, blueprint_file=None):
        self.data_file = data_file
        if blueprint_file is not None:
            self.blueprint_file = blueprint_file
        else:
            self.blueprint_file = '{data_file}.bp'.format(data_file=self.data_file)

    def encode(self, data: [WrapperBase]):
        data_out = open(self.data_file, 'wb')
        blueprint_out = open(self.blueprint_file, 'wb')
        print('Start encoding', self.data_file)
        print('Dumping blueprint')
        blueprint = {'blueprint': [], 'data_length': len(data)}
        for i, d in enumerate(data):
            # パーツを逐次エンコード
            for parts in d.encode(i):
                # 現在のカーソルを記録
                current_cursor = data_out.tell()
                print('Encoding %s.%s' % (i, parts.name))
                # パーツのデータ部分を書き出し
                pickle.dump(parts.data, data_out, protocol=-1)
                # 書き込み後のカーソルを記録
                new_cursor = data_out.tell()
                # ブループリントにカーソル移動量(パーツデータのバイト長)を記録
                blueprint['blueprint'].append(parts.blueprint(new_cursor - current_cursor))
        pickle.dump(blueprint, blueprint_out, protocol=-1)
        logging.info('Finish encoding')

データファイルと設計図ファイルに分けて格納します。 pickleを使用して部品データをエンコードし、バイト長を設計図ファイルに書き出します。

# デコーダークラス
class Decoder(object):
    def __init__(self, data_file, blueprint_file=None):
        self.data_file = data_file
        if blueprint_file is not None:
            self.blueprint_file = blueprint_file
        else:
            self.blueprint_file = '{data_file}.bp'.format(data_file=self.data_file)

    def decode(self) -> [WrapperBase]:
        data_in = open(self.data_file, 'rb')
        blueprint_in = open(self.blueprint_file, 'rb')
        print('Start decoding', self.data_file)
        print('Loading blueprint')
        # ブループリントデータの読み込み
        blueprint = pickle.load(blueprint_in)
        # initialize output_data
        data = [None for _ in range(blueprint['data_length'])]
        for bp in blueprint['blueprint']:
            # ブループリントのバイト長分だけ読み込み
            parts_data = data_in.read(bp.data)
            index = bp.index
            name = bp.name
            # バイトデータを読み込み
            bp.data = pickle.loads(parts_data)
            if name == 'base':
                data_class = bp.data
                # 空インスタンスの作成
                instance = data_class.__new__(data_class)
                data[index] = instance
            else:
                data[index].decode(bp)
        logging.info('Finish decoding')
        return data

データファイルと設計図ファイルから読み込みます。 設計図に記録された部品番号とバイト長を元に、pickleを使用してデコードします。

# テストコード
def run_my_encode():
    result_path = 'run_my_encode.bin'
    encoder = Encoder(result_path)
    encoder.encode([DataFrameWrapper(df=df)])


def run_my_decode():
    result_path = 'run_my_encode.bin'
    decoder = Decoder(result_path)
    df_wappers = decoder.decode()
    df = df_wappers[0].df

結果

-------prepare_data-------
generating data.
row count 40000000
sizeof df: 2305.9765625MB

-------run_to_pickle-------
Maximum memory usage: 4137.2109375MB
Elapsed time: 3.702019843040034

-------run_load_pickle-------
Maximum memory usage: 2322.0859375MB
Elapsed time: 5.678721209987998

-------run_my_encode-------
Maximum memory usage: 2465.16015625MB
Elapsed time: 3.972677645040676

-------run_my_decode-------
Maximum memory usage: 2184.8671875MB
Elapsed time: 4.480759038007818

※run_my_decode時のメモリ使用量が、元のdfよりも少なくなっていますが、デコード結果には正しい数値が格納されていました。

下記のようにパフォーマンスが改善しました。

  • 出力時の実行時間がほぼ変わらず、メモリ使用量が約40%改善。
  • 入力時の実行時間が約20%改善し、メモリ使用量がほぼ変わらない。

ただし、あらゆる構造のDataFrameに対応しているかというと、Noです。 例えば、列が入れ子になっている場合などは、正しくエンコードできません。

まとめ

pandas.DataFrameの入出力パフォーマンスを改善するラッパーを作成し、

  • 出力時のメモリパフォーマンス
  • 入力時の実行時間

を大幅に改善するすることに成功しました。

デメリットとしては、標準の方法に比べて対応可能なDataFrameが限定的な点です。

みなさまも、大きなデータの入出力を行う際には、 ライブラリを信頼しすぎずに、パフォーマンスチェックをしてみてはいかがでしょうか。

宣伝

ABEJAでは、技術課題を解決し、ブレイクスルーを生み出すエンジニアを募集しています。 今回の記事をきっかけに、少しでも興味が湧いた方は、ぜひ一度話を聞きに来てみてください。

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

PythonでScalaのようなlambda式を書いてみた。

f:id:higumachan:20170119112038p:plain

はじめに

はじめまして、ABEJA最年少メンバーでリサーチャーをやっている日熊です。 普段は、Deep Learningに関する研究をやっています。 仕事ではPythonを使っていますが、実際はScalaとかRustとかHaskellに最近ハマっています。

本日は趣味で作ったPythonのライブラリについて紹介します。 この記事では主に以下の様なことが起こります。

  • PythonでScalaっぽいlambda式を書けるようにした
  • どうやってlambda式を実現したのか説明
  • 既存ライブラリより高速だった

目次

今回作ったもの

Scalaのようなlambda式をPythonで実現するライブラリを目指して作りました。 Scalaでは以下のようなコードが書けます。

val numbers = Array(1, 2, 3, 4, 5)
val sum = numbers.reduceLeft[Int](_+_)
 
println("" + sum)

上のコードで出力が

15

となります。

注目すべき点は

val sum = numbers.reduceLeft[Int](_+_)

です。

_ + _

と書くと2変数を受け取る関数になっています。

Pythonで書くと

numbers = [1, 2, 3, 4, 5]
s = reduce(lambda x, y: x + y, numbers)
 
print(s)

となります。

numbers = [1, 2, 3, 4, 5]
s = reduce(_ + _, numbers)
 
print(s)

Pythonでもこう書けると気持ちいい。
そう思いませんか? そう思いますよね!
僕はそう思ったので、このライブラリを作ってしまいました。

github.com

試したい方はこちら
pip install pyscalambda
from pyscalambda import _

print((_ + 10 * (_ - 20 ))(1, 30))
print(all(map(_.isdigit(), ["1", "2", "3"]))
print(all(map(_.isdigit(), ["1", "2", "3", "a"]))

実行例

101
True
False

実際、Pythonで以下のようなライブラリを使いながら、関数型プログラミングっぽいことをするとlambda式が出まくって煩雑に思えてくる場面が多々あります。

実装概要紹介

実装方針

実装に当たって以下の3つの指針を建てました。

  • 早く作る
  • 速く作る
  • なるべく、おもしろそうな道を通ってみる (趣味なので)

上の指針に則って以下の様な流れの実装になりました。

  1. 演算子のhookingをしてオレオレ構文木を作る
  2. オレオレ構文木で表された式をlambda式コードの文字列に変換
  3. 引数リストを作る
  4. 定数辞書を作る
  5. lambda式コードの文字列をevalすることにより、lambda式に変換
  6. 出来上がったlambda式を実行する。 (evalを使う辺りがおもしろそうな道ですかね)

また、そのほかの実装上での工夫としては以下があります。 - lambda式のキャッシュ - _を引数に含む関数の呼び出し

ここでは

10 + -_ * 2

という式がいかにして

lambda x: 10 + -x * 2

というラムダ式に変換されるかを追っていくとします。

1. 演算子のhookingをしてオレオレ構文木を作る

演算子のhookingは以下のように行いました。

class Formula(object):
    def __add__(self, other):
        return self.do_operator2(other, "+")

    def __sub__(self, other):
        return self.do_operator2(other, "-")

    def __mul__(self, other):
        return self.do_operator2(other, "*")
...
    def __pos__(self):
        return self.do_operator1("+")

    def __neg__(self):
        return self.do_operator1("-")

    def __invert__(self):
        return self.do_operator1("~")

    def __getattr__(self, method):
        return self.do_methodcall(method)

    def __getitem__(self, key):
        return self.do_getitem(key)

このようにPythonのclassには演算子のoverloadがついているので、 すべての演算子をoverloadしたようなFormula(式)というclassを作ります。

このFormulaを継承した

  • Operator1(単項演算子)
  • Operator2(二項演算子)
  • Operand(オペランド)
  • MethodCall(メソッド呼び出し)
  • FunctionCall(関数呼び出し)
  • Underscore(引数オペランド)

このFormulaを継承したものがオレオレ構文木の葉(Operand, Underscore)や節(それ以外)になります。

継承関係は以下になってます。

f:id:higumachan:20170118115020p:plain

これにより、上のようなpythonの式を下のような構文木のような形に変換します。

10 + -_ * 2

f:id:higumachan:20170117155041p:plain

2. オレオレ構文木で表された式をlambda式コードの文字列に変換

オレオレ構文木をtraverseしながら、lambda式の本体部分を作っていきます。

class Operator1(Operator):
    ...
    def traverse(self):
        yield '('
        yield self.operator
        for t in self.value.traverse():
            yield t
        yield ')'
        # ["(", "演算子", [項のtraverseの結果], ")"] を出力する


class Operator2(Operator):
    ... 
    def traverse(self):
        yield '('
        for t in self.left.traverse():
            yield t
        yield self.operator
        for t in self.right.traverse():
            yield t
        yield ')'
        # ["(", [左項のtraverseの結果],  "演算子", [右項のtraverseの結果], ")"] を出力する
     ...

のようにtraverseするので、

"".join((10 + -_ * 2).traverse())

とすることで 以下のようなlambda式の本体部分を受け取ることが出来ます。

((CONST_0)+((-(___ARG1___))*(CONST_1)))

CONST_[NUMBER]と___ARG[NUMBER]___は後半で詳しく説明しますが、定数の置き換えと引数名の置き換えみたいなものです。

3. 引数リストを作る

lambda式本体が出来たので、lambda式引数リスト部分を作っていきます。 こちらはtraverse_argsという関数を作ります。

Underscoreだけ引数名を返すようにします。

class Underscore(Operand):
    ...
    def traverse_args(self):
         yield "___ARG{}___".format(self.id)
     ...

そして、Underscore以外のtraverse_argsの実装は

class Formula(object):
    def __init__(self):
        self.cache_lambda = None
        self.cache_consts = None
        self.children = []
     
    ...
    def traverse_args(self):
        for child in self.children:
            for t in child.traverse_args():
                yield t
    ...

という実装にしておきます。これは自分の子ノードの出力だけを返すだけの関数になります。

FormulaとUnderscore以外は以下のようにchildrenを指定しておけば勝手にtraverse_argsは使える状態になっています。

class Operator2(Operator):
    def __init__(self, operator, left, right):
        ...
        self.left = left
        self.right = right
        self.children = [self.left, self.right]

このように実装すると、実行したときに

print(dict((10 + -_ * 2).traverse_args())) 
["___ARGS1___"]

のような実行結果を得ることが出来ます。

ここをオレオレ構文木のtraverseじゃなくてリストのイテレーションにしてしまうと、_ + 10 * _こういう式のときに引数が入れ替わったりしちゃいます。

また、Underscoreは以下のようにinit時にidを自動的にふっています。

class Underscore(Operand):
    COUNTER = 1

    def __init__(self, id=None):
        super(Underscore, self).__init__()
        if id is None:
            self.id = Underscore.COUNTER
            Underscore.COUNTER += 1
        else:
            self.id = id

4. 定数辞書を作る

引数リストだけではなくて、式の中に出現する定数を辞書として保存しておきます。 こちらではtraverse_const_valuesという関数を定義していきます。 こちらもほとんど同じでConstOperandだけ

class ConstOperand(Operand):
    ...
    def traverse_const_values(self):
         yield "CONST_{}".format(self.id)
     ...

のような実装でそれ以外は

class Formula(object):
    def __init__(self):
        self.cache_lambda = None
        self.cache_consts = None
        self.children = []
     
    ...
    def traverse_const_values(self):
        for child in self.children:
            for t in child.traverse_args():
                yield t
    ...

のような実装にしていると。

print(dict((10 + -_ * 2).traverse_const_values())) 
{
    "CONST_0": 10,
    "CONST_1": 2
}

のような実行結果を得ることが出来ます。

また、ConstOperandもUnderscoreと同じようにinit時にidを自動的にふっています。

class ConstOperand(Operand):
    COUNTER = 0

    def __init__(self, value):
        super(ConstOperand, self).__init__()
        self.id = ConstOperand.COUNTER
        ConstOperand.COUNTER += 1
        self.value = value

これをやらないと、evalの中と外ではスコープが違うのでlambda式をevalで定義するときに上手く動いてくれません。

5. lambda式コードの文字列をevalすることにより、lambda式に変換

今まで作ったものをくっつけてevalに渡すことによりlambda式を生成することが出来ます。

binds = self.traverse_const_values()
lambda_body = "".join(self.traverse())
lambda_args = ",".join(self.traverse_args())
lambda_string = "lambda {}:{}".format(lambda_args, lambda_body)
lambda_function = eval(lambda_string, dict(binds))

evalの第2引数はevalのグローバル変数になります。ここに定数辞書を渡すことによりevalの内部で定数にアクセス出来るようになります。 これを以下のようにFormulaのcallにフックして実行することにより、Scalaのようなlambda構文が実現できます。

class Formula(object):
    ...
    def __call__(self, *args):
        binds = self.traverse_const_values()
        lambda_body = "".join(self.traverse())
        lambda_args = ",".join(self.traverse_args())
        lambda_string = "lambda {}:{}".format(lambda_args, lambda_body)
        lambda_function = eval(lambda_string, dict(binds))
        return lambda_function(*args)
     ...

全体の流れとしては以上になります。

lambda式のキャッシュ

今回ライブラリの速度が早いポイントはここにあります。 5. で書いた以下のコードだと

class Formula(object):
    ...
    def __call__(self, *args):
        binds = self.traverse_const_values()
        lambda_body = "".join(self.traverse())
        lambda_args = ",".join(self.traverse_args())
        lambda_string = "lambda {}:{}".format(lambda_args, lambda_body)
        lambda_function = eval(lambda_string, dict(binds))
        return lambda_function(*args)
     ...

関数呼び出しのたびにlambda式を生成するため構文木のtraverseを何度も行ってしまっているのでかなり効率が悪いです。

def __call__(self, *args):
        if self.cache_lambda is None:
            binds = self.traverse_const_values()
            lambda_string = self.create_lambda_string()
            self.cache_lambda = eval(lambda_string, dict(binds))
        return self.cache_lambda(*args)

のように一度evalして作ったlambda式はcacheしてしまって使いまわすようにすると、効率が良くなります。 cache有りとcache無しで速度を比べてみると以下のようになります。

gist49ee4e1cd8802cb83711c31081c397cd

実行結果

cached_time 0.440841913223
nocached_time 77.7346978188
176.332366517

のように構文木大きさによりますが180倍程度の高速化につながります。

_を引数に含む関数の呼び出し

例えば、関数呼び出しを行うときの引数側に_を含めた場合、

len(_)([1, 2, 3])

実行結果

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Underscore' has no len()

となってしまいます。 そこで、scalambdable_funcという関数を作りました。

def scalambdable_func(f):
    @functools.wraps(f)
    def wraps(*args, **kwargs):
        if any(map(lambda x: issubclass(x.__class__, Formula), args)) or any(map(lambda x: issubclass(x.__class__, Formula), kwargs.values())):
            return FunctionCall(f, map(Formula.convert_oprand, args), vmap(Formula.convert_oprand, kwargs))
        return f(*args, **kwargs)
    return wraps

SF = scalambdable_func

関数の呼び出しの引数をすべてoperandへ変換し実行します。 これにより、引数にある_を加味した構文木を作成することで関数呼び出しに対応することが出来るようになっています。

SF(len)(_)([1, 2, 3])

実行結果

3

ちなみにこのscalambdable_funcはデコレータとしても使えます

@SF
def test(x):
    return x + 1

test(_)(100) # == 101

その他

その他にも色々頑張りました。

1. _以外にも_1, _2, ..., _9を作って複数回変数を利用するlambda式の生成を可能にする
from pyscalambda import _1, _2

(_1 + _2 * _1 * _2)(10, 20) # == 4010
2. Objectのメソッド呼び出しにも対応している
from pyscalambda import _

(_.split("_"))("ABEJA_ABEJA") # == ["ABEJA", "ABEJA"]
3. cacheしないモードも実装している
from pyscalambda import _

(_ + _ + _).nocache()(1, 2, 3) # == 6 (遅い)
4. _と_1, _2, ... , _9を同時に使うとSyntaxErrorにする
(_ + _1)(1, 2) # raise SyntaxError
5. ちゃんとtestを書いていたお陰でいじるとき楽だった

などなど、語りたいことはいっぱいありますが今回はこの辺にしておきます。

競合紹介

作った後に調べたところ、2つほど同じこと(似たこと)を実現しているライブラリを見つけました。

fnpy

github.com

特徴

  • lambda構文だけではなくPythonで関数型っぽく書く機構を揃えてる(monadとか遅延評価周り)
  • lambda構文は表現上は全く一緒
  • 中の設計がきれいで頭良く作られている

macropy

github.com

特徴

  • lambda構文のためではなく、pythonのAST(抽象構文木)に対して関数(Lispで言うマクロ)を適用するライブラリ群
  • lambda構文はやりたいことは同じだが構文が違う
  • これを使うともはやpythonではなくなる…(すごくイケてるしヤバイ感ある)

比較

比較実験

今回は書き方がまったく一緒である、fnpyとの比較実験を行いました。

比較コードは以下です。

vs

実行結果

pyscalambda_time 0.060153961181640625
fnpy_time 0.32550811767578125
5.41124992073

pyscalambdaの方が5倍程度早いです。

速度比考察

pyscalambdaはlambda式をそのまま作っているのに比べ、fnpyは関数をネストした形で作っています。

_ + 1 + 1

例としてこのような構文を変換したときに

pyscalambdaは

lambda x: x + 1 + 1

というような、lambda式を生成しています。

fnpyはイメージとしては

lambda x: add(add(x, 1), 1)

の様なlambda式を生成しています

2つを比べてみると、pyscalambdaのラムダ式に比べfnpyのlambda式は演算があるたびに関数呼び出し分のオーバーヘッドがあると考えられます。

追加実験

先程の考察を実証するために以下のような比較コードにより実験を行いました。

gist274b8f2a7a0b3f1b44dea35569c3451c

このコードは恣意的にfnpyの関数呼び出しするであろう場所を増やし速度比に変化が起きるかの実験です。

実行結果

pyscalambda_time 0.27565693855285645
fnpy_time 9.234592914581299
33.5003100704

pyscalambdaの方が34倍程度早いという結果となりました。 これにより、関数呼び出しによるオーバヘッドが両者の速度を分かつこととなりました。

考察

evalによりlambda式を文字列から生成しキャッシュする機構を取り入れることによって、 既存のコードよりも通常用途で約4倍、コーナーケースにおいて約34倍程度早くなることがわかりました。 lambda構文は性質上mapの中やfilterの中に取り入れられることが多いため、この速度向上には意味があると考えます。

感想

今回の取り組みについての感想は、趣味プログラミングでこのようなものを作ることで業務をこなしているだけでは身につかない部分に触ることが出来て良かったです。 また、趣味プロジェクトでも

  1. やりたいことを明確にする
  2. 既存システムを探す
  3. 既存システムに対してどのように優位性を作るかの方針を立てる
  4. 実装
  5. 評価

という、業務(研究)で身につけたフレームワークが役に立ちました。

このように趣味プログラミングと業務(研究)の間でエコサイクルを回しながらスキルを磨いていくことが楽しんでプログラミングをしていくためには重要だと思いました。

宣伝

ちなみに、ABEJAでは今回の話が「面白い or もっといいもの作れるからくだらない」と思えるイケてるしヤバいエンジニアを募集しています。

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓

「らしさ」が伝わるロゴをつくる。ABEJAデザイナーの試行錯誤

design

f:id:takana8:20170111100953p:plain

はじめまして。骨とワニが好きなデザイナーの吹上(@takana8)です🐊

昨年夏、 ABEJA の主力サービスのひとつである ABEJA Platform のロゴ(上図)を制作しました。

ABEJA Platform とは、人工知能のブレークスルー技術である Deep Learning を活用し、様々な大量データの取得・蓄積・学習・解析・出力・フィードバックを行うことができる先進的なPaaS(Platform as a Service)です。

今回のテックブログでは、この ABEJA Platform のロゴをどのようなプロセスで、どのような考えに基づいて制作したのかをご紹介します。

どんなロゴが求められていたか

一般に、ロゴに求められる要件というと以下のようなものがあるかと思います*1

  1. 視認性:かたちや色がはっきり見えるか、文字を読み違えないか
  2. 展開性:ディスプレイでも印刷でも問題ないか(解像度や色数に制限があっても問題ないか)
  3. 普遍性:時間がたっても使えるか
  4. 国際性:海外展開は考慮されているか
  5. 環境適応性:既存クリエイティブ(Web やパンフ等)との関係性や今後のサービス展開について考慮されているか
  6. コンセプトの反映:サービス特性や理念とのズレはないか
  7. 独創性:オリジナリティが感じられるか
  8. 美的造形性:美的に好ましいものになっているか

今回制作するロゴに求められる基本的な要件を整理していくと、本プロジェクトで特に重要な観点は「環境適応性」、「国際性」、「コンセプトの反映」、の三点だということが分かりました。

「環境適応性」

デザイン制作時点で、サービスの今後の展開として、「ABEJA Platform」をベースに「ABEJA Platform for Retail」や「ABEJA Platform for Manufacture」などの様々な業界特化型サービス(SaaS:Software as a Service)を随時提供していくことが想定されていました。

このため今回は、サービスロゴのバリエーションが作りやすいか、それぞれの一貫性が保ちやすいか、といった「環境適応性」が重要な要件に挙げられました。

「国際性」

本サービスは日本だけでなく シンガポールをはじめとした海外での事業展開 も進められるため、「国際性」を満たすことは必須要件でした。

「コンセプトの反映」

ロゴはサービスのあり方をビジュアル面で支える軸であり、言わばブランドの顔です。そのロゴがサービス特性や企業の理念・価値観を適切に表現しているかという観点は、マーケティングやブランディングの活動全体に大きな影響を与えます。

今回のように、多様な領域に向けてサービスが広がっていくことを前提としている場合は、特にそうでしょう。

ロゴで表現したいことをまとめると

以上を踏まえ、今回制作するロゴを通して伝えたいことをまとめると、次のようになります。

  • 様々な業界や国で提供されるサービスであること
  • 最先端テクノロジーである人工知能をベースにしたサービスであること
  • イノベーションや倫理観を重視するという ABEJA の理念・価値観

完成したロゴ

早速ですが、完成したロゴはこちら↓

f:id:takana8:20170111100951p:plain

側面から見たヒトの脳を図案化し、ABEJA Platformのベースとなる技術である「人工知能」 をイメージさせるロゴです。

ぱっと見た印象はシンプルな脳のアイコンですが、実は、ちょっと特別な仕掛けが隠されています。

色分けされた各領域の頂点を数えてみると……

f:id:takana8:20170111101616p:plain

右下の小脳を表す領域は頂点が 3 つ、その左隣の脳幹部分は 4 つ、 というように各領域の頂点を1つずつ増やしながら、六角形を分割して切り出しています。このような、ある規則にもとづいた領域分割というギミックで、「アルゴリズム」や「合理性」、「論理性」といった特性を表現しました。

ロゴに込めた意味とデザインプロセス

なぜこのモチーフか

繰り返しになりますが、今回のプロジェクトで求められるロゴの要件は以下の3点でした。

  • 様々な業界や国で提供されるサービスであること
  • 最先端テクノロジーである人工知能をベースにしたサービスであること
  • イノベーションや倫理観を重視するという ABEJA の理念・価値観

これらの要件を満たモチーフやテーマを探るため、まずはひたすらラフスケッチをし*2、このうち見込みがありそうなアイデアをIllustratorで清書していきました。そこから4案ほどに絞り、制作意図をまとめてプレゼンテーションを行いました。

f:id:takana8:20170111101708p:plain

そして最終的に選ばれたのが、六角形のガイドをベースに「側面から見た脳」を図案化したシンボル(上図・右)。

「人工知能」というキーワードからこの「脳」のかたちを想起するのは、人類にとっては自然で普遍的といえるでしょう(ワニのためのロゴとなると、また違ったかたちになってしまうはず🐊)。

ちなみに、ここでガイドを六角形にした理由は、「 ABEJA 」という社名がスペイン語でミツバチを意味することから「ハチの巣(ハニカム)」を連想したことによります。

f:id:takana8:20170111101638p:plain

初期の段階(上図・左)もシンプルで悪くないかたちですが、これからさらに力強いロゴにするために試行錯誤を続けます。そして、手を動かすうちにふと、少し工夫すれば各領域の頂点を1つずつ増やしながら分割していくことができるのでは?と気が付きました。これぞブレークスルー💡 思い付いてしまえば実現するのはそう難しくありませんでした。

このような仕掛けは説明されなければきっとなかなか気付かないもので、ともすれば制作者の自己満足だと受け取られることもあるかもしれません。

しかし、このようなシンプルな図形に深い意図を込めるための「試行錯誤」やそれを実現する思考の「ブレークスルー」こそ、 ABEJA が重視する「イノベーション」に必要な要素そのもの。ロゴにこのような仕掛けと意図を込めることには、「 ABEJA らしさ」を伝えるという点で大きな意義があると考えました。

なぜこの色か

デザインにおいて、色のもつ影響力は思いのほか大きいもの。色を付けてしまうとロゴのかたちに意識が向きにくくなるので、はじめは白黒で作成し、かたちがある程度かたまった段階で色を付けていきました。

f:id:takana8:20170111100942p:plain

上図は検討した色の組み合わせの一部です。色によって印象が随分変わりますね。

いろいろな色の組み合わせを試した結果、今回は青から紫へと変化する鮮やかなグラデーションを採用することとしました。

青や紫は「知性」や「落ち着き」といった印象を与える色であり、ここからイノベーターらしい「先進性」とプラットフォーマーとしての「信頼感」が表現できると考えました。

なぜこの書体か

ロゴタイプの制作にあたっては、プラットフォームサービスの全体像を体現するロゴに適した、現代的かつ先進的な印象のサンセリフ書体を中心にピックアップし、検証を重ねました。どのような雰囲気の書体にするか、大文字か小文字か、シンボルとの組み合わせはどうか。これらを様々な組み合わせでテストし、最終的に「DIN」をベースにすべて大文字のロゴタイプを制作することとしました。

f:id:takana8:20170111100937p:plain

「DIN」は、もとはドイツの工業製品などに記載するために制作された書体です。そのため、スタンダードでかっちりした雰囲気があり、「信頼感」のイメージにもつながります。

今後、サービス提供範囲の広がりとともにロゴタイプ部分が多様化することを想定して、文字のかたちには手を加えすぎない方針とし、各種ロゴタイプを完成させました。

最後に

以上、「らしさ」が伝わるロゴをデザインするために考えたことをざっと紹介させていただきました。

じつは私は去年の夏ごろ ABEJA にジョインしたのですが、入社後の初仕事がこのロゴデザインプロジェクトでした。

会社のことを深く知らなければできない仕事に入社直後に取り組むという、導入としてはなかなかハードルの高いプロジェクト……しかし逆に考えると、この時に会社に対する理解が一気に深まったおかげでその後の仕事がとてもやりやすくなったので、結果オーライだったのかなと思っています。

さて、私たちいま、「試行錯誤」「ブレークスルー」「イノベーション」といったキーワードにビビッときた方を探しております。ぜひ、下記ページにて「話を聞きに行きたい」ボタンを押してみてください!

www.wantedly.com

最後までご覧いただき、ありがとうございました!

*1:参考:『 ロゴロジック

*2:スケッチをするときは無印良品の4コマノート(現行版はノートじゃなくて 短冊型メモ帳 になっています )が非常におすすめです。1コマに1アイデアをポンポン描いていくと、100個くらいはあっという間に埋まってしまいます。ただ枠線があるだけなのに、無地のノートより手を動かすのが早くなる不思議😳

フロントエンジニアとしてAWS re:Inventに行ってきました

はじめに

ABEJAでフロントエンド開発をやっている清水です。 先月末から今月初にかけてラスベガスで開催された AWS re:Invent に行ってきました。

この記事は、12月14日に弊社が運営しているコミュニティ主催で開催されたイベントで話した内容の補足になります。

https://abeja-innovation-meetup.connpass.com/event/45987/

re:Invent について

re:Invent は年に1度開催されるAmazon Web Service 主催の開発者向けカンファレンスで、 今年は11/28〜12/2までラスベガスで開催されました。

f:id:toshi6:20161201074631j:plain

行く前の準備

今回はABEJAからは一人で参加しました。私は英語を話すのが得意ではありません。 そして最近はフロントエンドを専門にやっているため、たくさんあるAWSのサービスを全部キャッチアップするのは難しく、 前提知識の面にも不安がありました。

悪あがきで行く前に準備したことを紹介すると

  • 英会話の勉強
  • AWSの復習
  • 事前説明会への参加

どれも付け焼き刃的なのですが、いくらか役に立ちました。 一番役に立ったのは事前説明会の参加で、ここで知り合った方々のおかげでイベントが楽しめました。

フロントエンドエンジニアとしての関心

最近ではLambdaの登場でサーバーレスアーキテクチャの話題も活発で、導入した事例もちらほら聞こえてきます。

私の担当しているABEJAのサービスもフロントはSPA、バックエンドのAPIはサーバーレスで作られています。 そのため、今回はサーバーレス系のセッションに的を絞ってスケジュールを組みました。

ラスベガスに行ってみて

ラスベガスに初めていきました。視界に入るのはホテルとカジノ f:id:toshi6:20161129000557j:plain

会場のホテルがとても広く、セッション会場間の移動にも20分以上かかります。 毎日歩き疲れて夕方にはヘロヘロになります。

キーノートは日韓来場者のために優先席に用意されていて間近で見ることができました。 目の前にSnowmobileが出てきた時は正直冗談かと思いました。 f:id:toshi6:20161130101955j:plain

夜は日本人だけのパーティー(Japan Night)に参加したり、re:Playに行ったり…

サーバーレスについての発表

サーバーレスに関係する発表も多数ありました。 LambdaのC#対応、Lambda Edge、Step Functions、 その他 Glue Athena Batch Pinpointなど今までは、EC2やEMRを使って構築する必要があったものが フルマネージドなサービスとして発表もされました。

サーバーレスの事例紹介

事例として、複数の AWS サービスの内部で Lambda がすでに使われていることや、 Finra 社では15兆もの証券取引を Lambda で実現しているという話がありました。

参加したセッション

新機能の発表でセッションも後から追加され [serverless lambda] で検索してみると117件もありました。 私は4日間で18個のセッションに参加しました。セッションの情報についてはビデオやスライドがすでに公開されているのでここでは割愛します。

来年行く人に向けて

来年行く方が楽しむためのアドバイスとしては

  • AWS全般の知識 (サービスの概要レベルだけでも知っているといいと思います)
  • やっぱり英語は大事 (AWSのエンジニアに質問できるチャンスがあります)
  • 仲間が大事 (おかげですごく楽しめました)
  • 余裕のあるスケジュール (聞きたいセッションを絞って余裕を持たせないとすごく疲れます)
  • 最後は体力 (5日間楽しむためには体力が必要)

フロントエンドエンジニアとして思うこと

re:Inventに参加される方はインフラエンジニアの方がほとんどなので、私はフロントエンドエンジニアとして感じたことを書きます。

サーバーレスのインパクト

サーバレスアーキテクチャの一番の特徴は構築し管理すべきサーバインスタンスが存在しないことがあると思います。 今まではサービスの要件にもよりますが、バックエンドのアーキテクチャを考える上でスケーラビリティやアベイラビリティ、弾力性などの非機能要件を考慮して考える必要があります。 そのためにはOSやミドルウェア、クラスタなど様々な知識を駆使して構築する必要がありました。そして構築した環境の運用、監視の方法も同時に考えていく必要もあります。 そのためには専門的なチームに分かれ、お互いが連携しながらサービスを開発していく必要がありました。

サーバレスアーキテクチャではこれらをプラットフォーム側が担保することによって、開発者はよりサービスやアプリケーションに集中できるようになります。

サーバレスアーキテクチャはまだ新しいため多くの課題もたくさんあります。 また、すべてのシステムにフィットすることもないと思いますが ABEJAのようにリソースも少なく、スピード感をもってサービスをスケールさせていく必要があるベンチャーにとっては有効性を感じます。

チーム構成の変化

私が担当するフロントエンドチームは、他のチームが開発した複数のサービスをAPIを通して利用します。 開発を進めるなかで BFF (Backend for FrontEnd) 層の必要性を感じます。

BFF層を設けることでUIのためにAPIを最適化でき、 バグや変更があった場合に別チームへ依頼を出して、リリースを待つことなくBFF層を使って解決できる余地が生まれます。

BFF層を作るのは面倒

しかし、BFF層を作るには当然サーバサイトの知識が必要になります。フロントエンドを開発するために要求される技術や知識もたくさんあります。 その上、サーバーサイドの知識を身につけてアップデートしてくのはとても大変です。

サーバーレスによりBFF層の導入が簡単に

しかし、サーバーレスの登場によりBFF層を構築することが容易になりました。 AWSのLambdaはNode.jsをサポートしているため、SPAとのモデルの共有やLambdaでSSR(Server Site Renddring)をするなどの選択肢が広がります。 f:id:toshi6:20161212190404p:plain

これからのフロントエンドチーム

今後サービスの顔ともいうべきフロントエンドチームは、フルマネージドなサービスを組み合わせて早くアイデアを形にする サービスチームとしての重要性が増してくるのではないでしょうか。 re:InventのセッションのServerless Architectural Patterns and Best Practices (ARC402) で話されていた「AppOps」の時代が来るかもしれません。 そのためには、フロントエンドのエンジニアもAWSのようなクラウドプラットフォームが出すサービスにも注目していく必要があると思います。

今後

AWSのLambdaだけではなく、GCP(Google Compute Platform) の Cloud Functions、 Azure の Azure Functionsなど他のクラウドプラットフォームでもサーバーレス開発のための機能が整備されてきています。 今後もフロントエンドエンジニアとして動向に注目していきたいと思っています。

宣伝

ABEJAでは最新のアーキテクチャで世界を変えるサービスの開発を行っています。 ご興味のある方は、ぜひご連絡ください

www.wantedly.com

Kubernetesのクラスタ監視について

この記事はKubernetes Advent Calendar 2016の20日目の記事です。

Kubernetesを安定的に運用する上で、どのように監視を行うかということを考える必要があると思います。

しかし、Kubernetesにデフォルトで使用されているcAdvisor, Heapsterやaddonとして提供されているkube-state-metricsの他、DataDogなど選択肢も様々で、その中からどれを選べば良いのか難しい状況です。

今回は、それらの監視ツールの中からKubernetesにデフォルトで使用されているcAdvisor, Heapsterと、addonとして提供されているkube-state-metricsについて調べてみました。

cAdvisor

cAdvisorはGoogle社が開発しているオープンソースのコンテナの監視ツールです。 Kubernetesの各ホスト上で1つづつ起動しており、デフォルトで1秒毎に同じホストにあるコンテナのメトリクス情報を収集してくれます。

f:id:i03yari2:20161220173424p:plain

cAdvisorの概要図

cAdvisorで取得できるメトリクスとしては

  • CPU
  • Memory
  • Network
  • FileSystem

などがあり、メモリ上に(デフォルトで)60秒分のメトリクスを保持し、APIとして提供してくれます。

また、cAdvisorはGUIも持っているため60秒分のメトリクスであれば簡単に確認することができます。

f:id:i03yari2:20161220110401p:plain

cAdvisorのダッシュボード

Heapster

alt

[5]より引用

Heapsterは、クラスタ単位のメトリクスの監視ツールであり、ホストを自動的に監視に追加しkubeletを通してcAdvisorからメトリクス情報を収集します。 メトリクス情報は、ラベルでPodをまとめてしエクスポートされます。また、Kubernetes 1.2以降であれば、コンテナ内のアプリケーションからカスタムメトリクスを取得することができます。

Heapsterを使うには、メトリクスのフォワード先のストレージバックエンドが必要になります。ストレージバックエンドにはInfluxDBが使われることが多いようです。

今回はInfluxDBとGrafanaを使ってメトリクスを可視化してみました。 (本当はこちらのガイドからKubernetes上のクラスタを監視したかったのですが、うまく動作せず今回はこちらを参考にコンテナを直に立ち上げて確認しています)

はじめに監視対象のホストでcAdvisorを起動しておきます。

$ docker run --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --publish=8080:8080 --detach=true --name=cadvisor google/cadvisor:latest

次にInfluxDBとGrafanaを起動します。Heapsterが、格納先のInfluxDBへリンクするため先に起動する必要があります。

$ docker run -d -p 8083:8083 -p 8086:8086 --name influxdb kubernetes/heapster_influxdb
$ docker run -d -p 80:8080 -e INFLUXDB_HOST=<influxdb_host_ip> kubernetes/heapster_grafana:v0.7

次にHeapsterですが、監視対象のホスト設定を書いたJSONファイルをコンテナにマウントする必要があります。 実際にはHeapsterとKubernetesが連携し自動でホストを監視対象に追加する形になります。 今回は監視対象のホストを2つ用意し、それぞれcAdvisorを動かしています。

$ vim /export/heapster/hosts
{
  "items": [
    {
      "ip": "192.168.33.10",
      "name": "host1"
    },
    {
      "ip": "192.168.33.20",
      "name": "host2"
    }
  ]
}
$ docker run --name heapster --link influxdb:influxdb -v /export/heapster/hosts:/var/run/heapster/hosts -d kubernetes/heapster:v0.14.2 --sink="influxdb:http://influxdb:8086" --source="cadvisor:external?cadvisorPort=8080" 

以上でGrafanaからメトリクスの確認ができるようになります。

f:id:i03yari2:20161220115846p:plain

Heapsterのメトリクス

左側がCPU使用量、右側がメモリ使用量、上段がコンテナ単位のメトリクス、下段がホスト単位のメトリクスのグラフです。 上段のグラフでは、動作しているホストに関係なくすべてのコンテナのメトリクスが表示されています。 複数のホスト上のcAdvisorがコンテナ単位で収集したメトリクスがHeapsterを通してInfluxDBにエクスポートされていることが確認できます。

kube-state-metrics

Heapsterが、CPU・メモリ・ネットワークといったリソース単位でメトリクスを管理しているのに対し、kube-stae-metricsはPod, Deployment, DeamonSetといったKubernetesで抽象化された単位でメトリクスを管理します。

また、kube-state-metricsは、デフォルトでKubernetesに組み込まれていないためkube-state-metricsコンテナを自分でデプロイする必要があります。

@helix_kazさん記事を参考に構築してみました。

構築に成功するとhttp://<node_ip>:30090/graphでPrometheusにアクセスすることができます。

今回はメトリクスとしてkubelet_running_pod_countを表示し、nginxのPodを手動で起動と削除を繰り返してみました。

f:id:i03yari2:20161220171353p:plain

kube-state-metricsのメトリクス

グラフからPod数が変動していることが分かると思います。

このようにkube-state-metricsを使えば、Kubernetesの抽象化単位をメトリクスとしてみることができます。

Heapsterとkube-state-metricsの比較

ストレージとの連携

Heapsterは、cAdvisorのメトリクスを取得しInfluxDBなどのバックエンドストレージにフォワードします。一方、kube-state-metricsは、メトリクスをメモリに保持しますがフォワードに責任を持ちません。そのため、kube-state-metricsの場合は、Prometheusなどのpull型の監視ツールと連携する必要があります。

メトリクスの違い

Kubernetesは、状況に応じてオートスケーリングしてくれるため、ホスト単位よりもラベル単位での管理が必要になってくると思います。監視という点でも、Kubernetesで抽象化されている単位で管理できる方がわかりやすく、問題の原因が特定しやすくなるのではないかと思います。その点、kube-state-metricsは、メトリクスの形式がPodやDeploymentといったKubernetesの抽象化単位でメトリクスが見れるため、利点があると感じました。

まとめ

Kubernetesのモニタリングについては、こちらの記事がよくまとまっていると思います。一部を要約すると、「Kubernetesを使う上ではどこでアプリケーションが動いているかを正確に知ることができないので、これまでの監視の考え方を再考する必要があるよ。タグとラベルで管理しトラッキングすることが重要になるよ。」ということを言っています。  今回の調査でkube-state-metricsがこの考え方に近く、実際にKubernetesを扱う上で直感的にメトリクスを見れると感じました。今後も監視系ツールは開発が進んでいくと思いますが、kube-state-metricsに注目しつつ見守っていきたいと思います。

宣伝

ちなみに、Abejaではコンテナを心穏やかに監視してくれる仲間を募集しております。 また、サーバレスアーキテクチャとかSparkとかSPAに興味がある方など幅広く募集しておりますのでどしどしご応募下さい!

→ Join us! ←

参考

[1] https://www.datadoghq.com/blog/monitoring-kubernetes-era/

[2] http://mmbash.de/blog/monitor-docker-containers-with-heapster-running-on-apache-mesos/

[3] https://www.youtube.com/watch?v=sxE1vDtkYps

[4] https://deis.com/blog/2016/monitoring-kubernetes-with-heapster/

[5] http://blog.kubernetes.io/2015/05/resource-usage-monitoring-kubernetes.html

[6] https://github.com/kubernetes/kube-state-metrics#kube-state-metrics-vs-heapster

Deep Learningによる一般物体検出アルゴリズムの紹介

Deep Learning Computer Vision

一般物体検出アルゴリズムの紹介

今回CNNを用いた一般物体検出アルゴリズムの有名な論文を順を追って説明します。

コンピュータビジョンの分野において、一般物体検出とは下記の図のように、ある画像の中から定められた物体の位置とカテゴリー(クラス)を検出することを指します。

f:id:contaconta:20161205164002p:plain

[6]より引用

Deep Learningアルゴリズムの発展によって、一般物体認識の精度は目まぐるしい勢いで進歩しております。 そこで今回はDeep Learning(CNN)を応用した、一般物体検出アルゴリズムの有名な論文を説明したいと思います。

R-CNN (Regions with CNN features) (CVPR 2014) [1]

かの有名なCNNの論文[8]で、ILSVRC 2012の物体認識チャレンジで大差をつけて1位になりました。 このチャレンジでは1枚の画像が1000クラスのうちどれに属するかを推定する、という問題設定でしたが、 物体検出のタスクに対してもCNNのアルゴリズムを上手く応用できないか?という課題を解く先駆けとなった論文がR-CNN[1]です。

R-CNNのアルゴリズムは、

  1. 物体らしさ(Objectness)を見つける既存手法(Selective Search)[7]を用いて、画像から物体候補(Region Proposals)を探す(2000個程度)
  2. 物体候補の領域画像を全て一定の大きさにリサイズしてCNNにかけてfeaturesを取り出す
  3. 取り出したfeaturesを使って複数のSVMによって学習しカテゴリ識別、regressionによってBounding Box(物体を囲う正確な位置)を推定

といった流れになります。

f:id:contaconta:20161205154108p:plain

[1]より引用

つまり、物体っぽい場所を大量に見つけてきて、無理やり画像をリサイズしてCNNにて特徴抽出、SVMによってどのクラスっぽいかを識別する、というものです。 このアルゴリズムにより、PASCAL VOC 2012のデータセットにおいて(Deepじゃない)既存手法の精度を30%以上改善し、53.3%のmAPを達成しました。

ただし、欠点もあり、

  • 学習を各目的ごとに別々に学習する必要がある(ad hoc)
    • CNNのFine-tune
    • 複数のSVMによるクラス分類 (Classification)
    • 物体の詳細位置推定 (Bounding Box Regression)
  • 実行時間がすごく遅い
  • GPUを使って10-45 [s/image] (ネットワークに応じて変化)

といった課題もありました。

SPPnet (ECCV 2014) [2]

この論文のアイディアとしては、画像をリサイズするのではなく、feature mapsをpoolingするときにリサイズしてしまおう、というものです。

R-CNNでは、固定サイズの画像を入力として識別していましたが、 SPPnetでは、Spatial Pyramid Pooling (SPP)という手法を用いることで、 CNNで畳み込んだ最終層のfeature mapsを縦横可変サイズで取り扱えるようにしました。

これにより、R-CNNのように大量の物体量領域ごとにCNNで特徴抽出するのでなく(約2000個のRegion Proposalはかなり領域の重複が多いため、重複する画像領域をCNNで特徴抽出するのはかなり無駄)、 画像1枚から大きなfeature mapsを作成した後、Region Proposalの領域の特徴をSPPによってベクトル化することで、スピードはGPU上にて24-102倍に高速化できました。

f:id:abeja:20161108222820p:plain

[2]より引用

この手法の欠点としては下記のものが挙げられます。

  • 学習がad hocなのは変わらず
  • 最終的な学習時にSPP Layer以下のパラメータが更新できない

Fast R-CNN (ICCV 2015) [3]

R-CNN/SPPnetでは、fine-tune/classification/bounding box regressionをそれぞれ別々に学習する必要がありました。そこで、Fast R-CNNではこれらを課題を解決するために、下記の手法を提案しました

  1. RoI pooling layerという、SPPのpyramid構造を取り除いたシンプルな幅可変poolingを行う
  2. classification/bounding box regressionを同時に学習させるためのmulti-task lossによって1回で学習ができるようにする(ad hocでない)
  3. オンラインで教師データを生成する工夫

またmulti-task lossの導入により、Back-Propagationが全層に適用できるようになったため、全ての層の学習が可能になりました。

f:id:abeja:20161108232143p:plain

[3]より引用

これにより、R-CNN, SPPnetより高精度な物体検出を実現しました。 また実行速度は、VGG16を用いたR-CNNより9倍の学習速度、213倍の識別速度で、 SPPnetの3倍の学習速度、10倍の識別速度を達成しました。

Faster R-CNN (NIPS 2015) [4]

Fast R-CNNは、物体検出のほとんどの学習/識別フェーズをDeep Learningを用いることで実現できました。 しかしながら、物体候補(Region Proposals)を検出するアルゴリズムは前述の論文同様、既存の手法を使っていました。 そこで、Faster R-CNNはRegion Proposal Network (RPN)という物体候補領域を推定してくれるネットワーク + RoI Poolingにクラス推定を行うことでEnd to Endで学習できるアーキテクチャを提案しました。

f:id:abeja:20161109115458p:plain

[4]より引用

*ちなみにFaster R-CNNの論文は、Fast R-CNNがarXivにアップされた約1ヶ月後に初稿がアップされている、恐ろしい速さや。。。

RPNは、物体候補を出力するために(1)物体かどうかを表すスコア(図中cls layer)と(2)物体の領域(図中reg layer)の2つを同時に出力するように設計されています。画像全体のfeature mapsから予め決められたk個の固定枠(Anchor)を用いて特徴を抽出し、RPNの入力とすることで、各場所において物体候補とすべきかどうかを推定します。 物体候補として推定された出力枠(reg layer)の範囲を、Fast R-CNN同様にRoI Poolingしクラス識別用のネットワークの入力とすることで最終的な物体検出を実現します。

f:id:abeja:20161109115502p:plain

[4]より引用

物体候補検出がDeep化されたことで、既存手法(Selective Search)[7]よりも物体候補が高精度化&候補数が少なくなり、 GPU上で5fpsの実行速度(VGGのネットワークを利用)を達成しました。 また、識別精度もFast-RCNNより高精度化しています。

YOLO(You Only Look Once) (CVPR 2016) [5]

上記の研究では、Region Proposalを何かしらの手法で検出した後、classificationを行っていましたが、 YOLOでは別のアプローチとして、予め画像全体をグリッド分割しておき各領域ごとに物体のクラスとbounding boxを求める、という方法を提案しています。 CNNのアーキテクチャがシンプルになったため、Faster R-CNNに識別精度は少し劣るが45-155FPSの検出速度を達成ました。 またSliding WindowやRegion Proposalを使った手法と違い、1枚の画像の全ての範囲を学習時に利用するため、周辺のコンテクストも同時に学習することができます。これにより、背景の誤検出を抑えることができるようになり、背景の誤検出はFast R-CNNの約半分の抑えることが出来ました。 (今回の実験では、7x7のグリットに分割し、1つのセルに対して1クラス, 2box、20クラスを推定できるようにしている)

f:id:abeja:20161112170639p:plain

[5]より引用

ただし欠点として、分割されたグリッドサイズは固定かつ、グリッド内で識別できるクラスは1つであり、 検出できる物体の数は2つという制約を設けているため、 グリッド内に大量のオブジェクトが映ってしまうような場合に弱くなります。

SSD: Single Shot MultiBox Detector (ECCV2016) [6]

YOLOのアルゴリズムと同じような系統のアルゴリズムとしてSSDがあります。 SSDは様々な階層の出力層からマルチスケールな検出枠を出力できるように工夫されています。

f:id:contaconta:20161130170945p:plain

[6]より引用

下記のような特徴があります。

  • state of the artな検出速度のアルゴリズム(YOLO)より高速で、Faster R-CNNと同等の精度を実現するアルゴリズムの提案
  • 小さなフィルタサイズのCNNをfeature mapに適応することで、物体のカテゴリと位置を推定
  • 様々なスケールのfeature mapと利用し、アスペクト比ごとに識別することで、高い精度の検出率を達成
  • 比較的低解像度でも高精度に検出できるend-to-end trainableなアルゴリズム

f:id:contaconta:20161130173652p:plain

[6]より引用

上図の様に異なる階層からfeature mapを使い、比較的小さなサイズの物体も検出できるため、入力画像サイズを小さくしても、それなりの精度がでるため、高速化できています。

SSDは300×300の画像サイズにおいてVOC2007のデータセットにおいて74.3% mAPという高精度を保ったまま、59 FPSを達成(Nvidia Titan Xを利用)し、512×512の画像サイズにおいては、76.8% mAPを達成しました。(Faster R-CNNは73.2% mAP) )

SSDについての詳細は下記参考資料を参照いただけると。

感想

歴史を辿っていくと、R-CNN懐かしいなーとか、どれも再現実験が超大変だーとか(理解、実装、学習時間含め)、魔改造Caffeの解読つらい。。。など思い返すことが多く、良い振り返りになりました。この分野は進展が目まぐるしく、ほとんどの論文が学会発表の半年から1年以上前にarXivに投稿されています。 学会で発表を聞いていても、「あれ、それ半年前ぐらいの論文じゃない?」ぐらいの速さで進化していて、驚くばかりです。 ”一般物体検出”の分野は、偉大なる先人のお陰でだいぶ実現可能性が見えてきているので、これからは如何にアルゴリズムやネットワークをシンプルにできるか?や少ないデータセットで高精度な検出ができるか?という方向に発展していくのでしょうか。また、一般物体検出はクラス+物体領域の教師ラベルが必要で作成が大変なため、よりライトな教師データでも学習可能な手法が提案されるとより可能性が広がると感じております。

果たして来年はどうなってることでしょうか。

宣伝

ちなみにABEJAでは、イケてるしヤバいエンジニア、リサーチャーを募集していますので、 最先端のテクノロジーで世界を変えるようなプロダクトを作りたいという方は、どしどしご応募ください☆ リサーチャーだけでなく、Spark+Scalaできる方や、Vue.jsを使ってSPAのアプリケーション作りたい方、数千台ぐらいのDockerコンテナをマネージしたい方や、AWS上でゴリゴリTerraformやりたい方等募集しております!

→ Join us! ←

参考文献

[1] Girshick, Ross, et al. "Rich feature hierarchies for accurate object detection and semantic segmentation." Proceedings of the IEEE conference on computer vision and pattern recognition. 2014. link

[2] He, Kaiming, et al. "Spatial pyramid pooling in deep convolutional networks for visual recognition." European Conference on Computer Vision. Springer International Publishing, 2014. link

[3] Girshick, Ross. "Fast r-cnn." Proceedings of the IEEE International Conference on Computer Vision. 2015. link

[4] Ren, Shaoqing, et al. "Faster R-CNN: Towards real-time object detection with region proposal networks." Advances in neural information processing systems. 2015. link

[5] Redmon, Joseph, et al. "You only look once: Unified, real-time object detection." arXiv preprint arXiv:1506.02640 (2015). link

[6] Liu, Wei, et al. "SSD: Single Shot MultiBox Detector." arXiv preprint arXiv:1512.02325 (2015). link

[7] Uijlings, Jasper RR, et al. "Selective search for object recognition." International journal of computer vision 104.2 (2013): 154-171. link

[8] Krizhevsky, Alex, Ilya Sutskever, and Geoffrey E. Hinton. "Imagenet classification with deep convolutional neural networks." Advances in neural information processing systems. 2012. link

Kubernetes Federationの今とこれから

kubernetes

この記事はKubernetes Advent Calendar 2016 7日目の記事です。

Kubernetes v1.3から試験的に導入されているFederation機能について調べてみました。

なぜ調べたのか

当社システムのバックエンドではたくさんのDockerコンテナを使っていて、日々コンテナ数・ホストサーバ数が増えています。また、国内だけではなくグローバルでもサービス提供をするようになってきています。グローバルに分散した環境でのコンテナ管理をするための方法のひとつとして、Kubernetes Federationを調べてみました。

Federationとは

Federationは複数のリージョンやクラウドに配置されているKubernetesのクラスタを一括で管理できるようにする機能です。複数のクラスタの前段にfederation control planeというコンポーネントを配置する事で実現されています。ちょっと前まではUbernetesと呼ばれていました。

f:id:toshitanian:20161207102301p:plain

何が嬉しいのか

CoreOSの記事でFederationによって実現できる事が紹介されています。

グローバル・スケジューリング

Federation control planeを使う事によって、管理下にある任意のクラスタへPodを割り当てる事ができます。今後のアップデートによって、管理下のクラスタのそれぞれの負荷に応じた処理の分散をさせる事ができるようになります。複数のリージョンやクラウドにクラスタを配置する事で、処理性能あたりコストの安いクラスタで多めに処理をさせたり、リージョン毎に負荷の高い時間帯にリソースを集中させたりする事ができるようになります。

グローバルにサービスを提供している企業にとってはメリットになる部分ですね。

リージョン/データセンターレベルの障害に対する自動リカバリ

Kubernetesはマシンレベルでのダウンに対しては復旧が自動で行われるようになっています。一方でクラスタの属しているリージョン/データセンターレベルでシステムダウンが発生した場合はクラスタ全体がダウンしてしまいます。Federation control planeで複数のクラスタを管理する事で、特定のリージョン/データセンターがダウンした場合でも、稼働しているクラスタで処理を継続する事ができます。

ベンダーロックイン回避

Federation control planeの管理下となるクラスタを複数のクラウドに配置する事で特定のクラウドへのロックインを回避する事ができます。クラウド別の価格やサービスレベル等の環境変化に強いシステムを作る事ができます。

また、Kubernetesは元々Googleによって開発されていましたが、現在はCloud Native Computing Foundationの元で開発が進められており、多くの企業が開発に参加しています。

ロックイン回避という見方だけではなく、特定のクラウドだけではカバーできない地域でのサービス提供や、クラウドでは提供されていないハードウェアを使ったクラスタでのサービス提供などもできるという見方もできますね。

Federation機能のリリース状況

現在リリースされている機能についてはドキュメントが整備されているようです。

また、Kubernetes 1.5のリリース予定によると1.5で以下の機能が実装されるようです。

  • Federated Daemonset
  • Federated Deployments
  • Federation: Support generalized deletions, including cascading and ...
  • It should be fast and painless to deploy a Federation of Kubernetes (kubefedというコマンドが出来るらしい!)
  • Federated ConfigMap

1.5の機能実装により、今までクラスタでできていた事がFederationでもほぼできるようになるみたいです。

Federation試してみたい!

kelseyhightower / kubernetes-cluster-federationのチュートリアルがわかりやすいです。 Federated ReplicaSetを使って、複数のクラスタに横断した状態のnginxを動かす所までできました。

ただし、サンプルで使うクラスタは全てGKEで作られるものなので、GKEのマルチリージョンでのFederationは試せますが、マルチクラウドでのFederationは試せません。

公式のチュートリアルもありますが、私は動かす事ができませんでした。

感想

Federation機能がまともに使えるまでしばらくかかりそうですが、開発が進んでいってるので今後が楽しみです。kubefedが出てきて、GKE以外でも簡単に試せるようになるとネット上のナレッジも豊富になりそうです。Federation機能を必要とするユーザは限られている気もしますが広がっていくと嬉しいです。

ちょっと宣伝

ABEJAでは泣きそうになるくらいコンテナを動かしていますし、日々増えているので更に泣きそうになっています。Kubernetesが好きな方もそうで無い方も是非遊びに来てください。その他、サーバレスアーキテクチャとかSparkとかSPAに興味がある方も待ってます!

https://www.wantedly.com/companies/abeja/projects

参考