超解像タスクで精度を上げる方法

# はじめに
超解像タスクにメインで取り組んでいるので、色々な論文などを参考に精度向上のため、色々試してみた結果の自分なりのまとめです。

変更前と比べてどれだけ伸びたとか細かく分析できてません。モデルによっては真逆の結果になるかもですが、別の画像や、別のモデルに変えた時にとりあえず試してみるメモになれば、と思ってます。

※10/12(更新) モデルアンサンブル

# 前提
モデルはEDSRモデルで固定。学習環境はGoogle Colabです。言語はpython、フレームワークはTensorFlow(keras)。
EDSRモデルの実装は[コチラ](https://github.com/krasserm/super-resolution)を参考にしてます。
EDSRモデルの公式論文は[コチラ](https://arxiv.org/abs/1707.02921)。
今回は4倍に拡大する設定になります。実装コードの**scale**の値が**4**ということです。

また試したことを種類別にすると
・画像の前処理
・モデルの調整
・学習時のパラメータ
・推論の仕方

に分けられるかなと思うので、この順番でまとめていきます。

# 画像の前処理

## 画像データについて
超解像するにしても扱う画像が衛星画像だったり、顔画像だったり、動物だったり色々あります。当たり前ですが、超解像したい画像データを訓練データとして用いる方がいいです。
ただ[コチラの記事](https://sorabatake.jp/11443/)によると衛星画像だけよりもDIV2Kの画像（一般画像）を混ぜた方が精度が上がったとのことなので、一種類にするのも良くないのかなと。確かに汎化性能を上げるなら、いろんなタイプの画像を訓練データに用いる方が良さそうな感じがします。

また基本的には計算コストを減らすためにも、255で割ったり、RGB平均値を引くなどして正規化した方がいいです。

## 画像サイズについて

後述する水増し手法である**cutblur**を使用する関係上、低解像度の画像（以下LR）と高解像度の画像（以下HR）は同じサイズでないといけないです。また画像の回転なども行いたいので、基本的には縦と横が同じ長さ（正方形の画像）が望ましいです。長方形だと例えば

```python
low_imgs = []
for image_path in image_path_list:
    img = cv2.imread(image_path)
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    test_low_img = np.array(img_rgb).astype('float32')
    test_low_img = cv2.resize(test_low_img, (500, 400), interpolation=cv2.INTER_CUBIC)
    test_low_img_raw = np.reshape(test_low_img, (1,500,400,3)) # 元の画像
    low_flip_img = tf.image.flip_left_right(test_low_img_raw).numpy() # 左右反転
    rotate_90_img = tf.image.rot90(test_low_img_raw, k=1).numpy() # 元画像の90度回転
    rotate_180_img = tf.image.rot90(test_low_img_raw, k=2).numpy() # 元画像の180度回転
    rotate_270_img = tf.image.rot90(test_low_img_raw, k=3).numpy() # 元画像の270度回転
    rotate_90_flip_img = tf.image.rot90(low_flip_img, k=1).numpy() # 反転画像の90度回転
    rotate_180_flip_img = tf.image.rot90(low_flip_img, k=2).numpy() # 反転画像の180度回転
    rotate_270_flip_img = tf.image.rot90(low_flip_img, k=3).numpy() # 反転画像の270度回転
    low_imgs.append(test_low_img_raw)
    low_imgs.append(low_flip_img)
    ....
    # 以下270度回転させた画像などをlow_imgsに追加していく

low_imgs = np.array(low_imgs)
```

上記コードのように画像パスを読み込んで左右反転と回転を行った水増し画像を作成し、low_imgsというリストに格納していき、最終的にndarray形式にする時に問題になります。

というのも例えば長方形画像を90度回転させると縦と横の長さが通常の画像と入れ替わります。そのため画像サイズがバラバラになってるので最終行で`np.array(low_imgs)`としても（データ数、縦、横、チャネル）の4次元テンソルの形になりません。そのため、基本的にはいくら回転させても画像サイズが変化しない正方形が望ましいです。

まぁそもそも画像データを全てndarray形式でメモリの読み込ませるのは効率が悪いので`TensorFlow`であれば`ImageDataGenerator`などバッチごとに読み込む形式が望ましいです。

じゃあ長方形画像しか手元にない時に正方形にするにはどうすればいいかというと、画像をパッチ化します。そもそも論文で紹介されるようなモデルは学習に[DIV2K](https://data.vision.ee.ethz.ch/cvl/DIV2K/)という超解像用のデータセットを用いていることが多く、これは元データは正方形でもないですし、画像サイズもバラバラです。そのため論文ではこれらの画像からランダムに任意のサイズの正方形にクロップして訓練データとしています。

論文の訓練データの作り方をまとめると
1. DIV2KなどのHR画像からランダムに任意のサイズの正方形としてクロップ(これが訓練データのHR画像、すなわち教師データになる）
2. 上記HR画像に対応するLR画像を作成するため、HR画像をリサイズして縮小する（この時の補完アルゴリズムとしてはbicubicが使われることが多い）。縮小サイズは4倍拡大のモデルにしたいなら1/4、2倍拡大のモデルにしたいなら1/2にする。
3. 2で作成した縮小画像をLR画像、1で作成したクロップ画像をHR画像として訓練データとする。モデルに突っ込むのはLR画像で教師データはHR画像 

となります。ただこれだとLR画像とHR画像のサイズが違うので`cutblur`が使えません。そのためLR画像をもう一回bicubic補完でHR画像と同じサイズに拡大します。

## 画像の水増しについて
画像データの水増しでよく言われる手法って大体が画像分類について書かれているものが多いです。 超解像タスクでも通用するものもありますが、例えばcutoutとかは画像分類においては一部をマスクすることで特定領域を根拠に判断することを防ぐ効果がありますが、超解像タスクにおいては、細かい特徴を捉えることの妨げになります。タスクに応じて最適に水増し手法を行うべきです。

超解像分野の多くの論文では左右反転や90度回転だけを行っていることが多いです。ただそうした幾何変換でない水増し手法として2020年に発表されたばかりの[コチラの論文](https://arxiv.org/abs/2004.00448)は、超解像タスクにおける最適な水増し手法について書かれています。それが**cutblur**と呼ばれるものです。実装コードは[コチラ](https://github.com/clovaai/cutblur)。
以下は論文内で述べられている水増し手法のイメージ図です。

![rethinking-srda-figure1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890361/077d0dcb-eeb2-97e5-72ff-cfea52381fe0.jpeg)

日本語で論文内容を説明してくれている[記事](https://ai-scholar.tech/articles/super-resolution/Data-Augmentation)もあるので、詳細はコチラに任せることにします。

一応簡単に**cutblur**についてに説明すると、画像の一部分を高画質化したり、低画質化するものになります。
LR画像の一部分を正方形で切り抜き、その部分を対応するHR画像と入れ替えるイメージです。
これにより、「どう」超解像したらいいかだけでなく「どこを」超解像したらいいかまで学習できるとのことです。

基本的に画像の水増しにおいては、これらの手法を全てランダムに行うと良いと思います。

Cutblur以外のそれぞれの水増し手法について論文では細かく述べられているので、簡単にどういうものかまとめておきます。

#### Blend

ランダムな色を任意の割合で混ぜるものになります。

公式実装では(縦、横、チャネル数）のゼロ行列を作成してから、0~255までの値でランダムに置換することでランダムな1色の画像を作成して元の画像と混ぜ合わせています。

#### RGB permute

画像の色素の順番を入れ替えるものです。基本的にカラー画像はRGBの順番ですが、これを例えばGBRとか、BGRとかにするイメージ。

#### Cutout

上の図では任意の部分を正方形でくり抜くものになっていますが、これだと実は精度が下がることが論文内で述べられています。それは先ほども述べたように細かい特徴量が欠落するからです。そのために論文内で効果があるCutoutのやり方として述べられているのは、画像のピクセルを一定確率で消すという手法です。

要は1ピクセルごとにランダムに黒色にしていくものになっています。この確率は論文では0.1%と非常に小さいものになっています。ただ効果があると言ってもほんの少しPSNRの値が伸びるだけなので、超解像タスクにおいてはCutoutはあまりやらない方がいいと思います。

#### MixUp

画像と画像をランダムな割合で混ぜ合わせるものになります。この割合は任意のパラメータのベータ分布から取ってきているようです。

#### CutMix

Cutoutは任意の部分を黒色にするものでしたが、Cutmixは別の画像にします。別の画像を貼り付けるイメージ。

#### CutMixUp

CutMixとMixUpを混ぜたものになります。CutMixで貼り付ける画像をMixUpしたものにするイメージ。

これらの変換はどれか1種類だけを適用させ、左右反転や回転といった幾何変換と組み合わせるといいのではと思います。

# モデル調整
EDSRモデルにて行ったものになるので、他のモデルではもしかしたら当てはまらないかも。

## 活性化関数
ReLU関連が多いです。EDSRモデルはReLUです。LeakyReLUやPReLUといったReLUの亜種が使われていることも論文では多いです。EDSRモデルで試してみましたが、あまり変化はなさげ。ReLU関連ならどれも良さげ。

## 初期値
活性化関数にReLUを使う関係上、Heの初期化が良さそうです。バイアスはゼロで初期化でいいと思われる。

## 層の深さ

EDSRモデルはフィルター数64, 残差ブロック16のベースラインモデルと、フィルター数256、残差ブロック64のモデルが論文では取り上げられています。もちろん層が深い方が精度は僅かにいいですが、Google Colabで動かすのは少々しんどいです。
画像サイズによりますが、バッチサイズ1とか2とかじゃないとメモリオーバーしてしまいました。フィルター数64、残差ブロック16でも精度は出せそうなので、手軽に動かすならこっちでも良さそう。

## ダウンサンプリング
論文のEDSRモデルはLR画像を直接モデルに入れて拡大した超解像画像を出力するものになっているので、LR画像とHR画像のサイズが違う状態です。ただこれだとCutblurが使えないので、自分はEDSRモデルにダウンサンプリング層を追加してみました。単純に縮小するものでもいいのですが、ピクセルシャッフルの方が良さげです。TensorFlow(keras)ならtf.nn.space_to_depthで実装できます。

ピクセルシャッフルに関しては[コチラ](http://musyoku.github.io/2017/03/18/Deconvolution%E3%81%AE%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%ABPixel-Shuffler%E3%82%92%E4%BD%BF%E3%81%86/)

# 学習時のパラメータ

## 損失関数
メジャーなのはMSEかMAEです。EDSRモデルはMAEなのでMAEのままです。

## オプティマイザ
Adamが使われることが多いです。。MADGRADなど新しいものも出てきていますが、論文ではAdamが多いです（論文自体が古いからだろうけど）

Adamのbeta1は0.9, beta2は0.999、εは1e-8や1e-6で設定されてることが多いです。
EDSRモデルはbeta1=0.9, beta2=0.999, ε=1e-8なのでそれを使ってます。

## 学習率
論文では初期値は1e-4で200000ステップたったら半分（5e-5)にしてます。
ただ自分のデータだと100000ステップ以降学習が進まなかったので、もっと早い段階で半分になるようにしました。

コサインカーブなども試してみましたが途中で勾配爆発したりしてうまくいかなかったです。

ただ何かしらの学習率減衰処理は必要だと思います。

## バッチサイズ
正直モデルサイズや画像サイズによって変わります。自分は16を使うことが多いです。
結局のところメモリオーバーにならない最大値でいいのかなと思います。

# 推論の仕方

## Geometric Self-ensemble

**一番大事**だと思います。論文でもこれを行った時の指標とそうでない指標で分けて表記されるくらいです。それくらいPSNRやSSIMの値が変わります。

EDSRモデル論文の4.3節に書かれていますが、簡単に説明するとTTA（Test Time Augmentation）の一種で超解像したい元画像に対して

①左右反転
②90度回転
③180度回転
④270度回転
⑤左右反転＋90度回転
⑥左右反転＋180度回転
⑦左右反転＋270度回転

の水増しを行って元画像も合わせて合計8種類の画像を用意して、8種類それぞれに対して超解像を行い、水増し手法と逆の操作（270度回転であれば-270度回転、左右反転+90度回転なら左右反転して-90度回転）を行って、それらを均等な割合で全て組み合わせて1枚の超解像画像を得るものです。

どれか一種類でうまく超解像できなくても、他の部分でできてれば補えるというイメージですかね。

実装に関しては`cv2.AddWeighted`で画像の合成はいけるはずです。

## パッチ推論
自分でつけた名前なのでもしかしたら正式名称があるのかもしれません。

要は超解像したい画像をいくつかのパッチに分けてそれぞれに対して超解像を行って、最後にまた1枚にするものになります。

500×500サイズの画像を直接モデルに入れて超解像するのではなく 100×100のパッチ25個に分けて25個を1個ずつ超解像するようなイメージです。

画像サイズは小さい方がメモリオーバーになることもないし、より細かい部分まで着目できるかなと思ったので採用してみたのですが、逆に精度が下がる結果となりました、、、

モデルや画像によってはうまくいくのかな。

## モデルアンサンブル
複数のモデルで超解像画像をそれぞれ作成して、Geometric Self-ensemble同様、`cv2.AddWeighted`で合成する形です。これに関しては[コチラ](https://arxiv.org/pdf/1905.04696.pdf)の論文が参考になります。単純に均等な割合で混ぜるよりは、重みは調整したほうがよさそう。

自分がEDSRモデルではなくWDSRモデル2つを0.5ずつアンサンブルした時はSSIM値が0.003ほど上がりました。


# まとめ
現時点で試してみたものとしては以上になります。その他また試してみて効果があったものはまとめていこうと思います。