Databricks Community Editionで画像データを分析してみる

無料のDatabricksコミュニティエディションでどこまで何ができるのかを模索中です。今回は画像編です。NLPは[こちら](https://qiita.com/taka_yayoi/items/ebec25578a7cfe2f50b0)です。

こちらで紹介しているノートブックは以下のリンク先からアクセスできます。

https://github.com/taka-yayoi/public_repo/tree/main/ce_image_analysis

以下の記事をまとめた際に、画像データを保持したDeltaファイルがあることに気づきました。というか前に触っていました。

https://qiita.com/taka_yayoi/items/3f8ccad13c6efd242be1

こちらのリファレンスソリューションで使っていました。なので、今回はこちらのデータをコミュニティエディションで分析してみます。コミュニティエディションの環境でもサンプルデータは利用できます。

https://qiita.com/taka_yayoi/items/41be81691df3f7c8e6bf

# コミュニティエディションにサインアップあるいはログインする

こちらのDatabricksコミュニティエディションにログイン、アカウントがない場合にはサインアップします。

https://community.cloud.databricks.com/

正式版のDatabricksとの違いはこちらを参照ください。機能面、キャパシティ面で制限はありますが、基本的な機能は使用できます。

https://qiita.com/taka_yayoi/items/800fd2714f460a9a1c40

サインアップの方法は[こちら](https://qiita.com/taka_yayoi/items/fb4f57c069e1f272e88a#%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3%E3%82%A8%E3%83%87%E3%82%A3%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%B8%E3%81%AE%E3%82%B5%E3%82%A4%E3%83%B3%E3%82%A2%E3%83%83%E3%83%97)を参照ください。

# クラスターを作成する

Databricksで計算処理を行う際には、[Databricksクラスター](https://qiita.com/taka_yayoi/items/c5d99cd77fe4bfcf69f0)を作成し、起動する必要があります。GUIの操作でさまざまなスペックのクラスターを作成でき、自動停止、オートスケールなどの機能を活用することができるので、コスト効率高く機械学習の取り組みを行うことができます。

:::note info
**注意**
以下の画面で言語が日本語になっていない場合には、[こちら](https://qiita.com/taka_yayoi/items/ff4127e0d632f5e02603)を参考に言語を変更してください。
:::

1. サイドバーで**クラスター**を選択します。
1. **クラスターの作成**ボタンをクリックします。
![Screen Shot 2022-06-14 at 9.18.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/7ac6be4b-3947-f16d-80a2-e18ba3efdf97.png)
1. **クラスター名**にはわかりやすい名前を指定し、**Databricks Runtimeのバージョン**では、**10.5ML**を選択し、**クラスターを作成**ボタンをクリックします。これでクラスターが起動します。ここでのランタイムのバージョンの`ML`は[機械学習ランタイム](https://qiita.com/taka_yayoi/items/824b507019d3ade7eedc)であることを意味し、tensorflow、pytorchなど著名な機械学習ライブラリが事前にインストールされているので、すぐに機械学習の作業に取り掛かることができます。
1. クラスター名の右にグリーンのチェックマークが表示されるとクラスターの起動が完了したことを意味します。
![Screen Shot 2022-06-14 at 9.23.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/11ab7c99-7cf5-7311-e60a-83fc6bbb23b2.png)

# ノートブックを作成する

Databricksではさまざまなロジックを[ノートブック](https://qiita.com/taka_yayoi/items/dfb53f63aed2fbd344fc)に記述し、クラスター上で実行します。

1. サイドバーの**ワークスペース**をクリックします。
1. 右上に表示される**ホーム**をクリックすると、あなたのホームディレクトリが表示されます。
![Screen Shot 2022-06-14 at 9.26.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/83e71a77-b4c0-4147-6463-ac849b3eaec1.png)
1. ユーザー名(メールアドレス)の右の下向き矢印をクリックし、メニューを表示させます。
1. **作成 > ノートブック**をクリックします。
![Screen Shot 2022-06-14 at 9.27.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/2af57ccf-bd1b-9435-5876-f041fd1f01b8.png)
1. わかりやすいノートブック名を入力し、クラスターには[上のステップ](#クラスターを作成する)で作成したクラスターを選択します。
![Screen Shot 2022-06-14 at 9.27.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/9b077319-d0ec-f5de-e82f-f01bdf874a7b.png)
1. これで空のノートブックが作成されました。
![Screen Shot 2022-06-14 at 9.30.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/2cd17d2c-1505-4abf-d8e0-df83805e44fa.png)

# 画像を分析してみる

## サンプルデータセットの確認

[Databricksファイルシステム(DBFS)](https://qiita.com/taka_yayoi/items/897264c486e179d72247)の`/databricks-datasets`にはさまざまなサンプルデータが格納されています。

以下のセルではマジックコマンド`%fs`を指定しており、`ls`や`head`などを用いてファイルシステムにアクセスすることができます。

```py:Python
%fs
ls /databricks-datasets
```

![Screen Shot 2022-06-14 at 9.33.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/c9da9a85-8280-833b-8c45-290a1c8c05f1.png)

## データフレームにDeltaファイルをロード

上のサンプルデータの中から花の画像を保存しているDelta形式のファイルをロードします。このデータには以下のカラムが含まれています。読み込んだデータフレームはSparkデータフレームとなります。Pythonの場合、PySparkというAPIを用いてこのデータフレームを操作します。

|カラム|説明|
|:--|:--|
|path|画像ファイルのパス|
|size|画像の幅と高さを持つ配列|
|label|画像のラベル|
|content|画像のバイナリ|

また、Delta形式のファイルにはバイナリーをカラムとして含めることができるので、画像データを用いた分析を簡便に行うことができます。

以下のセルではデータフレームの表示に[display関数](https://qiita.com/taka_yayoi/items/36a307e79e9433121c38)を使用しています。これにより、画像のサムネイル表示、グラフによる可視化などさまざまな機能を用いてデータを探索することができます。

```py:Python
df = spark.read.format("delta").load("dbfs:/databricks-datasets/flowers/delta/")
display(df)
```

![Screen Shot 2022-06-14 at 9.36.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/23491d27-89c9-e99b-d166-f937b4385cd6.png)

`printSchema()`を用いることでSparkデータフレームのスキーマを確認することができます。

```py:Python
# 読み込んだデータフレームのスキーマを表示
df.printSchema()
```

```
root
 |-- path: string (nullable = true)
 |-- size: struct (nullable = true)
 |    |-- width: integer (nullable = true)
 |    |-- height: integer (nullable = true)
 |-- label: string (nullable = true)
 |-- content: binary (nullable = true)
```

このように、カラムの一つ`content`は`binary`となっていることがわかります。

## PySparkによる簡単な集計

データフレームにさまざまなメソッドを用いることで、SQLライクな集計を行うことができます。以下の例では`label`でグルーピングを行い、グループごとのカウントを集計しています。

```py:Python
display(df.groupBy("label")
        .count()
        .select(["label", "count"]))
```

![Screen Shot 2022-06-14 at 9.38.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/b1e18e7c-fffc-a07f-b667-e8231777bc54.png)

ここで、テーブルの下に表示されるグラフマークに注目してください。こちらをクリックするだけで、テーブルのデータを簡単に可視化することができます。
![Screen Shot 2022-06-14 at 9.38.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/f865740d-fff8-a1c8-9af1-7c887a806282.png)

![Screen Shot 2022-06-14 at 9.39.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/1c640244-ad47-0036-59fa-b112005dadb5.png)

棒グラフでも傾向はわかりますが、ここでは円グラフに切り替えてみましょう。**プロットオプション**をクリックします。こちらの画面でプロットをカスタマイズすることができます。
![Screen Shot 2022-06-14 at 9.40.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/c700c326-c28e-c5a4-01b9-d3def2b26488.png)

ここではシンプルに**表示タイプ**を**円グラフ**に切り替えて、**適用**をクリックします。
![Screen Shot 2022-06-14 at 9.41.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/dd0a8b0b-fae2-9d78-8e28-73c4e95b5a95.png)

これで、円グラフに表示を切り替えることができました。matplotlibやseabornを使わなくても簡単にデータを可視化することができます。もちろん、これらの可視化ライブラリを使うこともできます。
![Screen Shot 2022-06-14 at 9.41.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/0e5f8fa7-9d74-eb47-2f4c-273370c1bb95.png)

なお、一時ビューあるいはHiveメタストアにデータフレームを登録すると、直接SQLを実行することができます。以下の例では、データフレーム一時ビューを登録しています。マジックコマンド`%sql`を用いることで、セルに直接SQLを記述することができます。

```py:Python
df.createOrReplaceTempView("flower_photo")
```

```sql:SQL
%sql
SELECT label, count(*) as count FROM flower_photo GROUP BY label
```

![Screen Shot 2022-06-14 at 9.43.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/39a1d9f6-6904-23c5-cd16-432ac9a1ae62.png)


## Delta Lakeを用いた分散モデル推論

大量データに対する推論を行う際に、Sparkの並列分散処理を活用することで処理に要する時間を短縮することができます。このために、ScalarイテレーターのPandasのUDF(ユーザー定義関数)を使用します。

必要なライブラリをimportします。Databricks機械学習ランタイムを使用しているので、tensorflow、PyTorchなど著名なライブラリが事前にインストールされています。

```py:Python
import io

from tensorflow.keras.applications.imagenet_utils import decode_predictions
import pandas as pd
from pyspark.sql.functions import col, pandas_udf, PandasUDFType
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import models, transforms
from PIL import Image
```

### 入力を処理するデータセットを定義

```py:Python
class ImageNetDataset(Dataset):
  """
  標準的なImageNet前処理を用いて画像コンテンツをPyTorchデータセットへ変換
  """
  def __init__(self, contents):
    self.contents = contents

  def __len__(self):
    return len(self.contents)

  def __getitem__(self, index):
    return self._preprocess(self.contents[index])

  def _preprocess(self, content):
    """
    標準的なImageNet正規化を用いて入力画像コンテンツを前処理
    
    詳細は https://pytorch.org/docs/stable/torchvision/models.html
    """
    image = Image.open(io.BytesIO(content))
    transform = transforms.Compose([
      transforms.Resize(256),
      transforms.CenterCrop(224),
      transforms.ToTensor(),
      transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])
    return transform(image)
```

### 推論タスクのためのPandas UDFの定義

1対1のマッピングセマンティックを提供するPySparkのUDFには3種類あります。

- PySpark UDF: レコード -> レコード、データのシリアライゼーションの性能に課題があるためお勧めしません。
- Scalar Pandas UDF: pandasのシリーズ/データフレーム -> pandasのシリーズ/データフレーム, バッチ間で状態が共有されません。
- **ScalarイテレーターPandas UDF**: いくつかの状態を初期化した後、バッチに進みます。

モデル推論にはスカラーイテレーターPandas UDFを用いることをお勧めします。

**参考資料**

- [MobileNet\(v1,v2,v3\)を簡単に解説してみた \- Qiita](https://qiita.com/omiita/items/77dadd5a7b16a104df83)
- [MobileNetV2\(Tensorflow\)を触ってみた。 \| AI・人工知能、IoT、CPS、Mobile：システム開発・一括請負を行っております。](https://apprhythm.biz/archives/2981)


```py:Python
def imagenet_model_udf(model_fn):
  """
  予測を行うImageNetモデルをPandasのUDFにラップします。
  
  ご自身のユースケースに応じて以下のようなカスタマイゼーションが必要になるかもしれません。
    - 性能を改善するためにDataLoaderのbatch_sizeとnum_workersをチューニング
    - 高速化のためにGPUを使用
    - 予測タイプを変更
  """
  def predict(content_series_iter):
    model = model_fn()
    model.eval()
    for content_series in content_series_iter:
      dataset = ImageNetDataset(list(content_series))
      loader = DataLoader(dataset, batch_size=64)
      with torch.no_grad():
        for image_batch in loader:
          predictions = model(image_batch).numpy()
          predicted_labels = [x[0] for x in decode_predictions(predictions, top=1)]
          yield pd.DataFrame(predicted_labels)
  return_type = "class: string, desc: string, score:float"
  return pandas_udf(return_type, PandasUDFType.SCALAR_ITER)(predict)
```

```py:Python
# MobileNetV2をPandas UDFとしてラップします
mobilenet_v2_udf = imagenet_model_udf(lambda: models.mobilenet_v2(pretrained=True))
```

### データフレームを用いた分散推論

- 必要となるカラムと、それぞれのカラムをどのように計算するのかを指定します。
- 上で定義したUDFを用いて、画像に対するラベルを推定します。
- 推論対象のデータが膨大になってもSparkがデータを分散しそれぞれにUDFを適用するので、大量データに対しても高速に推論を行えるアーキテクチャを維持することができます。


```py:Python
images = spark.read.format("delta") \
  .load("/databricks-datasets/flowers/delta") \
  .limit(100)  # デモ目的のため実行時間を短縮するためにサブセットを使用します

# 上のUDFを呼び出して予測を行います
predictions = images.withColumn("prediction", mobilenet_v2_udf(col("content")))

# 結果を表示します
display(predictions.select(col("path"), col("prediction")).limit(5))
```

![Screen Shot 2022-06-14 at 9.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/05021b0d-c40f-7530-572f-553ea1a5ed9f.png)

上の結果では`prediction`はネストされたデータとなっていますが、`.`記法を用いることで簡単に子要素にアクセスすることができます。


```py:Python
# 画像と予測結果のみに絞って表示します
display(predictions.select(col("content"), col("prediction.desc"), col("prediction.score")))
```

![Screen Shot 2022-06-14 at 9.47.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/be42f56e-4238-e56d-a394-4ef216dada85.png)

精度に関しては改善の余地がありますが、事前学習済みモデルを用いることで簡単に画像に対するラベルを推定することができます。以下のようなメリットを感じていただけたら幸いです。

- Delta Lakeによる画像管理の簡素化
- Databricksノートブックで柔軟なプログラミング言語の使い分け
- Databricks機械学習ランタイムを用いてライブラリ管理をシンプルに
- Sparkによる推論の並列実行



### Databricks 無料トライアル

[Databricks 無料トライアル](https://databricks.com/jp/try-databricks)