Python: HDF5ファイルの作成と読み込み (h5py)

お仕事で触れる機会が増えてきたHDF5について調べて整理します。また、h5pyを使ってWFLWデータセットをHDF5へ変換してみました。

HDF5

HDF (Hierarchical Data Format) は大量のデータを格納するファイルフォーマットで、The HDF Groupによって開発・メンテナンスされています。
5はバージョン番号で、現在の主流となっています1

HDF5の特徴はおおまかに4点です。こちらにまとまっています。

  • 多数・大容量のデータを1ファイルにまとめることができる
    • HDF5ファイルそのもののサイズやHDF5内のオブジェクト (DatasetとGroup) 数に制限がない
  • DatasetとGroupによってファイルシステムライクな階層構造を表現できる
    • Dataset で、numpy.ndarrayのような多次元配列を保持 (= ファイル)
    • Group で、複数のDatasetやGroupを保持 (= ディレクトリ)
    • DatasetやGroupへの属性付与 (Attribute) やリンクもできます
  • プラットフォームに依存しないフォーマットなので、可搬性が高い
  • C, C++, Java, FortranのAPIを公式サポート
    • 他言語でもサードパーティ製のライブラリによって使用できます

h5pyを使ったHDF5ファイルの入出力

PythonでHDF5を入出力するためのライブラリとしてよく使われるのがh5pyです。

github.com

今回はWFLW2の画像とアノテーションから1つのHDF5を生成しそれを読み込む処理を、h5pyを使って実装していきます。

$ python --version    
Python 3.7.4

$ pip install h5py numpy pandas pillow

$ pip list | grep h5py
h5py            2.10.0 

あらかじめこちらからWFLW_images.tar.gzとWFLW_annotations.tar.gzをダウンロード・展開しておきます。

  • WFLW_annotaions配下のlist_98pt_*.txtがアノテーションデータとなっており、1行1画像で各行に画像パスとランドマーク座標がスペース (" ") 区切りで格納されています
  • WFLW_images配下にはシチュエーション (0--Paradeなど) ごとにディレクトリが切られ、各ディレクトリにjpg画像が格納されています

HDF5ファイルの構成は以下のレイアウトにします。

WFLW.h5
├── annotations (Group)
│   ├── README (Dataset)
│   └── list_98pt_rect_attr_test (Dataset)
└── images (Group)
    ├── 0--Parade (Group)
    │   ├ ── 0_Parade_marchingband_1_116.jpg (Dataset)
    │   ...
    ├── ...
    ...

最初に実装全体を示します。WFLW_annotations、WFLW_imagesと同じ階層にPythonファイルを作成しています。

import h5py
import io
import glob
import os
import pandas as pd
import numpy as np
from PIL import Image

h5_path = "./WFLW.h5"

# HDF5ファイルの作成
with h5py.File(h5_path, "w") as h5:
    # annotations Group
    annotations_group = h5.create_group("annotations")

    with open("./WFLW_annotations/list_98pt_rect_attr_train_test/README", "r") as readme:
        readme_dataset = annotations_group.create_dataset(
            name="README", shape=(1,), dtype=h5py.string_dtype()
        )
        readme_dataset[0] = readme.read()

    with open("./WFLW_annotations/list_98pt_rect_attr_train_test/list_98pt_rect_attr_test.txt", "r") as test:
        test_dataset = annotations_group.create_dataset(
            name="list_98pt_rect_attr_test.txt", shape=(1,), dtype=h5py.string_dtype()
        )
        test_dataset[0] = test.read()

    # images Group
    images_group = h5.create_group("images")

    # WFLW_images内のディレクトリを列挙
    for d in sorted(glob.glob("WFLW_images/*")):
        # images/subset group
        subset_group = images_group.create_group(os.path.basename(d))

        # jpgファイルを列挙
        for p in sorted(glob.glob(os.path.join(d, "*.jpg"))):
            # numpyで読み込んでDatasetで追加
            image = np.array(Image.open(p)).astype(np.uint8)
            image_dataset = subset_group.create_dataset(
                name=os.path.basename(p), data=image, compression="gzip"
            )

# HEF5の読み込み
with h5py.File(h5_path, "r") as h5:
    readme = h5["annotations/README"][0]
    print(readme)
    # # Look at Boundary: A Boundary-Aware Face Alignment Algorithm.
    # ...

    test = pd.read_csv(
        io.StringIO(h5["annotations/list_98pt_rect_attr_test.txt"][0]), delimiter=" ", header=None
    )
    print(test.head())
    #           0           1    ...  205                                         206
    # 0  182.212006  268.895996  ...    0  37--Soccer/37_Soccer_soccer_ball_37_45.jpg
    # [1 rows x 207 columns]

    image = h5["images/0--Parade/0_Parade_marchingband_1_930.jpg"]
    print(type(image), image.shape)  # <class 'h5py._hl.dataset.Dataset'> (575, 1024, 3)

    image = image[()]  # or image = image[:, :, :]
    print(type(image), image.shape)  # <class 'numpy.ndarray'> (575, 1024, 3)

    image = Image.fromarray(image, "RGB")
    image.save("./output.jpg")

細かいところを解説していきます。

書き込み・読み込み

h5py.Fileオブジェクトを生成します。通常のファイルのopenと同様に、第2引数が"w"なら書き込み、"r"なら読み込みとして開きます。

以降のGroupやDatasetはこのFileオブジェクトを用いて更新・参照します。

# HDF5ファイルの書き込み
with h5py.File(h5_path, "w") as h5:
    ...


# HDF5ファイルの読み込み
with h5py.File(h5_path, "r") as h5:
    ...

Groupの作成

Groupの作成はcreate_groupメソッドで作成します。ルート直下に作成する場合はFileオブジェクトで呼び出します。

# images Group
images_group = h5.create_group("images")

前述の通りGroupはGroupを内包できますので、作成したGroupオブジェクトでcreate_groupをコールすると配下にGroupが作られます。

# images/subset group
subset_group = images_group.create_group(os.path.basename(d))

テキストDatasetの追加と読み込み

FileまたはGroupオブジェクトのcreate_datasetメソッドを用いて追加します。

  • Fileの場合はルート、Groupの場合はそのGroup配下に、それぞれ作成されます
  • 引数nameがDataset名で、ファイルシステムっぽくannotations/READMEでアクセスできます
  • Datasetは内部的には多次元配列で表現されますので、文字列の場合はshape=(1,)で1要素の扱いになります
    • 読み込むときも添字 ([0]) が必要です
  • データ型は可変長文字列となります
# 追加
with open("./WFLW_annotations/list_98pt_rect_attr_train_test/README", "r") as readme:
    readme_dataset = annotations_group.create_dataset(
        name="README", shape=(1,), dtype=h5py.string_dtype()
    )
    readme_dataset[0] = readme.read()

# 読み込み
readme = h5["annotations/README"][0]
print(readme)

画像Datasetの追加と読み込み

create_datasetには引数dataにnumpy配列を直接渡すことができます (この場合shapeは不要です) 。そのため画像はpillowのImageでロードしてnumpy配列にして渡してます。

テキスト同様にパス指定で読み込みますが、そのままではDatasetオブジェクトなので[()]とすることで配列全体をロードできます。

  • numpy同様のインデックス指定 ([0, :, :]など) で一部だけロードすることもできます
# numpyで読み込んでDatasetで追加
image = np.array(Image.open(p)).astype(np.uint8)
image_dataset = subset_group.create_dataset(
    name=os.path.basename(p), data=image
)

# 画像Datasetの読み込み
image = h5["images/0--Parade/0_Parade_marchingband_1_930.jpg"]
print(type(image), image.shape)  # <class 'h5py._hl.dataset.Dataset'> (575, 1024, 3)

image = image[()]  # or image = image[:, :, :]
print(type(image), image.shape)  # <class 'numpy.ndarray'> (575, 1024, 3)

ちなみに画像を配列で持つため、トータルのファイルサイズはぐっと増えます。jpg時には800MB以下だったものが20GB近くまで膨れます。そうした巨大なファイルでも、メモリに全てをロードすることなく1ファイル (Dataset) ずつ読み込むことができます。

  • create_datasetの引数にてcompression="gzip"などで圧縮形式を指定することもできます

まとめ

HDF5ファイルをh5pyでアクセスする方法について整理しました。

  • ディレクトリ構成をGroupで実現することで、データのユーザにわかりやすい直感的な構造を維持できる
  • 各Fileへのアクセスもファイルシステムに近い
  • データは配列で持つのが基本なので、ユーザフレンドリなテキストや画像などは多少の扱いづらさがある

  1. 旧バージョンとしてHDF4もありますが、制約が強く、あまり使われていないようです。

  2. Wu, Wayne and Qian, Chen and Yang, Shuo and Wang, Quan and Cai, Yici and Zhou, Qiang. Look at Boundary: A Boundary-Aware Face Alignment Algorithm. CVPR 2018.

AWS Service Brokerを使ってKubernetesでマネージドサービスを管理する

EKSのおかげでEC2・ASG・ELBなどが一式揃ったKubernetes環境を楽に構築できるようになりました。

一方で、上記以外のAWSマネージドサービスに依存したアプリケーションの場合、それらマネージドサービスもKubernetes (つまりマニフェストファイル) の管理下に置いて、アプリケーションと共にプロビジョニングしたいケースがしばしばあります。(かくいう私もkubectl applyでS3バケットとかSQSとか作りたいなーと思っていた次第でした。)

KubernetesのService CatalogとAWSで提供されるService Brokerを組み合わせることでこのユースケースを実現できます。例としてSQSを作成しながら、それらの利用方法をまとめていきます。

Service CatalogとService Broker

(僕にとって) 新しい概念として現れたこの2者について簡単に触れておきます。

Service CatalogおよびService Brokerは、Kubernetesと各社のクラウドインフラが提供するリソース (マネージドサービス) を糊付けするための仕組みで、下図で示す関係となっています。

f:id:ohke:20200430113431p:plain
[1] 抜粋

Service Catalogは、Kubernetesの機能で、Kubernetes APIと複数あるService Brokerを仲介する役割を持ちます
利用したいService BrokerをService Catalogに登録できるので、任意のリソースとKubernetes APIを連携できるようになります。

f:id:ohke:20200430141506p:plain
[2] 抜粋

Service Brokerは、各クラウドインフラベンダが提供するアプリケーションで、Service Catalogからのリクエストを受けて実際にリソースをプロビジョニングします。Open Service Broker API準拠のインタフェースであるため、Kubernetes以外の他のオーケストレーションシステムとも連携しやすいようになっています。

AWS Service Brokerの実装は↓のレポジトリで管理されています。

  • プロビジョニングではCloudFormationが使われています
  • サポートしているマネージドサービスの一覧はここで確認できます
    • AWSのすべてのサービスについて実装されているわけではないので注意してください

github.com

実践: EKSからSQSをプロビジョニング

それではEKSでSQSを生成します。[こちらのポスト]を参考にしながら、次の順番で進めます。

  1. Service Catalogのインストール
  2. AWS Service Brokerのインストール
  3. SQSインスタンスの作成
  4. バインディング
  5. アプリケーションからの参照

aws.amazon.com

前提

  • EKSでクラスタ・ノードグループの作成されていること
  • 作業環境にkubectlがインストールされ、クラスタと接続できていること
  • クラスタにHelmがインストールされていること

Service Catalogのインストール

helmでService Catalogをnamespace: catalogにインストールします。

$ helm repo add svc-catalog https://svc-catalog-charts.storage.googleapis.com
"svc-catalog" has been added to your repositories

$ kubectl create namespace catalog
namespace/catalog created

$ helm install catalog svc-catalog/catalog --namespace catalog --wait
NAME: catalog
LAST DEPLOYED: Thu Apr 30 18:33:36 2020
NAMESPACE: catalog
STATUS: deployed
REVISION: 1
TEST SUITE: None

AWS Service Brokerのインストール

次にService Brokerのインストールです。

その前にDynamoDBとIAMの準備が必要で、prerequisites.yamlをダウンロードして、CloudFormationで適用します。このときaws-service-broker-prerequisites-BrokerUser-*という名前でユーザが作成されますが、この後作成するService Brokerの認証情報として用います。

$ REGION=ap-northeast-1

$ wget https://raw.githubusercontent.com/awslabs/aws-servicebroker/master/setup/prerequisites.yaml

$ BUSERNAME=$(aws cloudformation create-stack \
  --capabilities CAPABILITY_IAM \
  --template-body file://prerequisites.yaml \
  --stack-name  aws-service-broker-prerequisites \
  --output text --query "StackId" \
  --region ${REGION})

$ aws iam create-access-key --user-name ${BUSERNAME} --output json --query 'AccessKey.{KEY_ID:AccessKeyId,SECRET_ACCESS_KEY:SecretAccessKey}'
{
    "KEY_ID": "AAAA",
    "SECRET_ACCESS_KEY": "BBBB"
}

上のCloudFormationスタックが完了したら、namespace: aws-servicebrokerにHelmでインストールします。

$ kubectl create namespace aws-servicebroker
namespace/aws-servicebroker created

$  helm repo add aws-servicebroker https://awsservicebroker.s3.amazonaws.com/charts
"aws-servicebroker" has been added to your repositories

$ helm show chart aws-servicebroker/aws-servicebroker
apiVersion: v1
description: Deploys the AWS Service Broker
name: aws-servicebroker
version: 1.0.1

$ helm install awssb aws-servicebroker/aws-servicebroker --wait \
  --namespace aws-servicebroker --version 1.0.1 --set aws.region=ap-northeast-1 \
  --set aws.accesskeyid=AAAA --set aws.secretkey=BBBB
NAME: awssb
LAST DEPLOYED: Thu Apr 30 22:28:56 2020
NAMESPACE: aws-servicebroker
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
For more information on usage, see https://github.com/awslabs/aws-servicebroker/docs/

$ kubectl get ClusterServiceBrokers
awssb   https://awssb-aws-servicebroker.aws-servicebroker.svc.cluster.local   Ready    3m39s

SQSインスタンスの作成

yamlファイルのフォーマットはサービスごとに異なります。aws-servicebrokerのレポジトリで確認でき、SQSであればこちらに記載があります。

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceInstance
metadata:
  name: sqs-standard-minimal-example
spec:
  clusterServiceClassExternalName: sqs
  clusterServicePlanExternalName: standard
  parameters:

上で定義したsqs_instance.yamlファイルをkubectl applyすることで、CloudFormationのスタックが走り、SQSが生成されます。

$ kubectl apply -f sqs_instance.yaml
serviceinstance.servicecatalog.k8s.io/sqs-standard-minimal-example created

$ kubectl describe ServiceInstance sqs-standard-minimal-example
Name:         sqs-standard-minimal-example
Namespace:    default
Labels:       <none>
Annotations:  API Version:  servicecatalog.k8s.io/v1beta1
Kind:         ServiceInstance
Metadata:
  Creation Timestamp:  2020-04-30T13:38:13Z
  Finalizers:
    kubernetes-incubator/service-catalog
  Generation:        1
  Resource Version:  185
  Self Link:         /apis/servicecatalog.k8s.io/v1beta1/namespaces/default/serviceinstances/sqs-standard-minimal-example
  UID:               d6aad2c5-8ae7-11ea-abb5-9e806396566e
Spec:
  ...
Events:                           <none>

SQSの名前やタグを見ると、どのクラスタのService Brokerで生成されたものかなどがわかります。

f:id:ohke:20200501112859p:plain

バインディング

最後に生成されたSQSをバインディングします。sqs-standard-minimal-exampleを指定したsqs_binding.yamlを作成します。

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceBinding
metadata:
  name: sqs-binding
spec:
  instanceRef:
    name: sqs-standard-minimal-example

同じくkubectl applyで適用すると、QUEUE_NAMEやQUEUE_URLというsecretsが作成されます。

$ kubectl apply -f sqs_binding.yaml
servicebinding.servicecatalog.k8s.io/sqs-binding created

$ kubectl describe ServiceBinding sqs-binding
Name:         sqs-binding
Namespace:    default
Labels:       <none>
Annotations:  API Version:  servicecatalog.k8s.io/v1beta1
Kind:         ServiceBinding
Metadata:
  Creation Timestamp:  2020-05-01T01:55:40Z
  Finalizers:
    kubernetes-incubator/service-catalog
  Generation:        1
  Resource Version:  337
  Self Link:         /apis/servicecatalog.k8s.io/v1beta1/namespaces/default/servicebindings/sqs-binding
  UID:               dc32e4cc-8b4e-11ea-abb5-9e806396566e
Spec:
  ...
Events:
  Type    Reason              Age   From                                Message
  ----    ------              ----  ----                                -------
  Normal  InjectedBindResult  14s   service-catalog-controller-manager  Injected bind result

$ kubectl describe secrets/sqs-binding
Name:         sqs-binding
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
QUEUE_NAME:                 80 bytes
QUEUE_URL:                  134 bytes
SQS_AWS_ACCESS_KEY_ID:      20 bytes
SQS_AWS_SECRET_ACCESS_KEY:  40 bytes
DEAD_LETTER_QUEUE_ARN:      0 bytes
DEAD_LETTER_QUEUE_NAME:     0 bytes
DEAD_LETTER_QUEUE_URL:      0 bytes
QUEUE_ARN:                  120 bytes

アプリケーションからの参照

Podでは上のsecretを環境変数にセットすることで、アプリケーションから参照できるようになります。

apiVersion: v1
kind: Pod
metadata:
  name: example-app
  labels:
    name: example-app
spec:
  containers:
  - name: example-app
    image: busybox
    command: ["sh", "-c", "printenv"]
    env:
    - name: EXAMPLE_SQS_URL
      valueFrom: { secretKeyRef: { name: sqs-binding, key: QUEUE_URL } }

試しにprintenvコマンドを実行してログを見ると、EXAMPLE_SQS_URLに作成したSQSのURLが入ってることが確認できます。

...
EXAMPLE_SQS_URL=https://sqs.ap-northeast-1.amazonaws.com/123456789012/aws-service-broker-sqs-d6aXXXX
...

まとめ

今回はService CatalogとService Brokerを使ってKubernetesからAWSマネージドサービスをプロビジョニングする方法について整理しました。

マニフェストファイルとkubectlコマンドで環境構築を完結できるのが利点ですが、一方でプロビジョニングされるリソースの名前などを自由に設定できないので、Kubernetes環境 "外" のシステムからはどのリソースがどの環境と紐付いているのかがわかりにくい欠点があります。
なのでユースケースとしては、Kubernetes環境内で閉じて利用されるマネージドサービスのみに限定したほうが簡潔かと思いました。

論文メモ: Wing Loss for Robust Facial Landmark Localisation with Convolutional Neural Networks

今回はCVPR'18で提案されたWing Loss for Robust Facial Landmark Localisation with Convolutional Neural Networks1 (arXiv) という論文を紹介します。

ポイント

  • 顔ランドマーク検出の損失関数として新たにWing Lossを定義
  • 正面向き以外の画像をオーバサンプリングするPose-based data balancing、画像内の頭部の回転を修正するためのTwo-stage landmark localisationを組み合わせることで、AFLWと300-Wにて高精度をマーク
@inproceedings{feng2018wing,
  title={Wing Loss for Robust Facial Landmark Localisation with Convolutional Neural Networks},
  author={Feng, Zhen-Hua and Kittler, Josef and Awais, Muhammad and Huber, Patrik and Wu, Xiao-Jun},
  booktitle={Computer Vision and Pattern Recognition (CVPR), 2018 IEEE Conference on},
  year={2018},
  pages ={2235-2245},
  organization={IEEE}
}

提案

Wing Loss

顔ランドマーク検出の分野では、損失関数にL2やsmooth L1が使われてきましたが、それぞれ問題があります。

  • L2は外れ値に対して敏感すぎる
  • L1およびsmooth L1でも大きく誤ったランドマークに引っ張られやすい (傾き1で一定のため)

そこで、小〜中程度の誤りを強調するWing Lossという新しい損失関数を提案。

  • wとεはハイパパラメータ
  • wは定数で、(-w, w) の範囲を表す
  • Cも定数で、 w - wlog(1+\frac{w}{\epsilon})

誤差が小〜中 (|x| < w) では定数Cまで素早く立ち上がり、大きい場合 (|x| > w) は一定のなだらかな傾きになります。全体で見るとΥ型で、これにより小〜中程度の誤差が外れ値と比較しても小さくなりすぎない誤差関数になってます。

AFLWを使って各損失関数とCED (Cumulative Error Distribution) 曲線で比較すると、他の損失関数よりも左側に位置しており、中程度に難しいランドマークに対して低いNMEをマークしてます。

  • 同時に一般的に使われているL2が最も悪く、外れ値の悪影響を証明されています

[1] Figure 4.抜粋

Pose-based data balancing

一般的に、画像内の姿勢 = 顔向きによって精度が異なります。概して横向き (profileやsemi-frontalと呼ばれます) の方が精度は悪くなります。
この論文では学習・評価にAFLWを用いているのですが、AFLWに正面向きの画像に偏っている点 (下図) に着目し、横向きの画像をオーバサンプリングすることで精度改善させています。

[1] Figure 5.抜粋

具体的には以下のアルゴリズムで学習データを使って、顔向きの判定・オーバサンプリングしています。1.〜4.で得られた分布が上の図になります。

  1. ランドマークの位置の平均値を計算することでベースとなる図形 (平均形状) を得る
  2. プロクラステス分析 (procrustes analysis) でランドマークを平均形状に位置合わせする (並進・回転・スケール変換)
  3. 位置合わせしたランドマークにPCA分析を適用して、元の図形をPCAで得られる1次元空間 (姿勢空間) へ投影して係数を得る
  4. 係数ごとにK個のビンに分割して、画像を分類する
  5. 少ないビンの画像を複製・オーグメンテーションする

プロクラステス分析はこちらの方の記事が参考になります。

gv.hateblo.jp

Two-stage landmark localisation

ランドマークが見切れる回転 (yawやpitch) 以外にも、ランドマークが画像内に全部入っていても顔の回転 (roll) によって精度は落ちます。

f:id:ohke:20200502205607p:plain
2 Fig 9.抜粋

これに対応するために、CNN-6とCNN-7という2段の畳み込みネットワークを使った位置推定 (Two-stage landmark localisation) を提案しています。

  1. 1段目のCNN-6で、64x64x3を入力として粗くランドマークを検出し、回転とBBoxを補正
  2. 2段目のCNN-7では、補正後の128x128x3を入力としてランドマークを検出

[1] Figure 1.抜粋

これによって単一の畳込みネットワークを使うよりも高い精度をマークしてます。

[1] Table 3.抜粋

実験

300Wデータセットを使い、各モデルとNMEで比較した実験結果が下表です。3つの提案の有効性証明する結果となってます。

[1] Table 4.抜粋

加えて、Wing Lossの一般性を確認するために、ネットワークをResNetに変えてNMEで評価した結果が下表です。WIng Lossが最も低くなっており、他のネットワークにも適用できる一般性を持っていることがわかります。

[1] Table 6.抜粋

まとめ

今回はWing Lossを提案した論文について紹介しました。


  1. Zhen-Hua Feng, Josef Kittler, Muhammad Awais, Patrik Huber, and Xiao-Jun Wu. Wing loss for robust facial landmark localisation with convolutional neural networks. In The IEEE Conference on Computer Vision and Pattern Recognition (CVPR), June 2018.

  2. Yue Wu and Qiang Ji. Facial Landmark Detection: A Literature Survey. International Journal of Computer Vision (IJCV), May 2018.