CIFAR-10を疑似ラベル（Pseudo-Label）を使った半教師あり学習で分類する

## TL;DR
* 半教師あり学習の1つの手法である、疑似ラベルをCIFAR-10で試した
* サンプル数が少ない場合は、疑似ラベルを使うことでテスト精度を引き上げることができた
* ただし、転移学習と比べると若干見劣りすることもある

## 元ネタ
かなり平易に書かれた論文なので読みやすいと思います。

Dong-Hyun, Lee. Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Networks. 2013
http://deeplearning.net/wp-content/uploads/2013/03/pseudo_label_final.pdf

## 半教師あり学習とは
「教師あり学習」と「教師なし学習」の間の子。教師あり学習のように$(X, y)$とラベル付けされたデータと、教師なし学習のように$(X)$だけのラベルがない、**未ラベル（unlabeled）**データを使って学習します。

半教師あり学習の良い所は、**少ないラベルのデータだけでも学習が進められる**ことです。実際の問題を考えたときに、ラベル付けというのはかなりの手間がかかるので、ここの手間が減るのはかなりありがたいです。未ラベルのデータはただ画像があればいいだけですから。

少ないラベルだけでも学習が進められるアプローチとして、**転移学習**というのがあります。これはImageNetなどで訓練済みのモデルを用いて、エッジの検出など低レベルのレイヤーの係数を固定し、高レベルのレイヤーだけ再訓練させる方法です。1から訓練させるよりも必要なラベル付けされたデータ数は、少なくても大丈夫です。転移学習は未ラベルのデータを使わないのでここでは「教師あり学習」としておきます。これも後ほど見ていきます。

## 疑似ラベルとは
未ラベルのデータに仮初めでつけたラベルを「**疑似ラベル（Pseudo-Label）**」といいます。付け方は簡単で、ラベル付データで訓練させたモデルから推論（predict）させるだけです。

![semi_01.png](https://qiita-image-store.s3.amazonaws.com/0/232088/bbc8d7ce-fbe4-4312-6fe1-ce6f53a1289d.png)

半教師あり学習でのポイントは、**疑似ラベルのついた未ラベルデータと、本物のラベルがついたラベル付データを混ぜて同時に訓練**させることです。そして、訓練させるたびに、疑似ラベルを再度推論させてどんどんラベルを更新していくことです。これにより（うまく行けば）モデルの訓練と疑似のラベルの更新との間に相互作用が生まれるというわけです。以下のような流れです。

1. ラベル付データでモデルを1エポック訓練する。いきなり同時に訓練する場合は、疑似のラベルを乱数で初期化する。
2. 1エポック終わったら、疑似ラベルを更新する
3. 1.に戻る

この同時に更新していくのがポイントで、あとで確認しますが、ラベル付データでかなり訓練してfine-tuningみたいなことをすると今回の場合はうまくいきませんでした。ただし、このテクニックには損失関数にミソがあります。

$$L=\frac{1}{n}\sum_{m=1}^n\sum_{i=1}^C L(y_i^m, f_i^m) + \alpha(t)\frac{1}{n'}\sum_{m=1}^{n'}\sum_{i=1}^C L(y_i^{'m}, f_i^{'m})$$

$f_i^m$は$i$番目のクラスの$m$番目のサンプルの推定値、ダッシュがついたものは疑似ラベルでついていないものは本物のラベルとします。$\alpha(t)$のように**疑似ラベルに対して、エポックを変数とした係数をおいてその係数を変化させていく**のがポイントです。

```math
\alpha(t) = \begin{cases}
0 \qquad t<T_1 \\ 
\frac{t-T_1}{T_2-T_1}\alpha_f \qquad T_1 \leq t<T_2 \\
\alpha_f \qquad T_2\leq t
\end{cases}
```

（12/2追記）：式の転記ミスがあったので修正いたしました。

論文では$\alpha_f=3, T_1=100, T_2=600$としていました。この式の意味は、**初めの方は本物のラベルのほうを見ていくが、あとのほうは疑似ラベルを重点的に（3倍）見ていくよ**ということです。論文では全体で何エポックしたのかよくわからなく、正直600エポックも訓練させる必要性がなかったので、以下のように変更しました。

$$\alpha_f=3, T_1=10, T_2=70$$

これで100エポック訓練させます。$T_2$を70としたのは、$\alpha(t)$が上昇している間はテスト精度が上がっていくのですが、上昇が止まると気持ちモデルが劣化していくの（精度ベースでコンマ数％単位）が観測されたので、$\alpha(t)$の打ち止めをあとの方にずらしました。

## 実験１～半教師あり学習と教師あり学習～
以下の3パターンをCIFAR-10の分類問題で比較します。

1. 疑似ラベルと本物のラベルを同時に訓練する（**半教師あり学習**）
2. 本物のラベルだけで訓練する（**教師あり学習**）
3. 本物のラベルだけで30エポック訓練し、その後に疑似ラベルと本物のラベルを混ぜて100エポック訓練する。この場合は$T_1=0$とし、疑似ラベルは30エポック訓練した状態での推論でつけるものとする。（**事前訓練ありの半教師あり学習**）

これを**本物のラベル数が500、1000、5000、10000**で比較します。訓練データのうち残り（ケース別に4.95万、4.9万、4.5万、4万）は未ラベルデータとします。

モデルは[こちら](https://qiita.com/koshian2/items/e66a7ee9bf60ca24c940)で使った10層CNNを使います。VGGライクな設定です。これは5万個の本物のラベルを使ってちゃんと訓練させればテスト精度9割は出ます。

1,2は100エポック訓練させました。オプティマイザーは全てAdamを使い、学習率はデフォルトの1e-3としました。全てのケースでテストデータは本物のラベルの1万個を使います。

コードはこちら。
1. 半教師あり学習　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/pseudo_cifar.py
2. 半教師あり学習＋事前訓練　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/pseudo_pretrain_cifar.py
3. 教師あり学習　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/supervised_cifar.py

### 実装のポイント
詳しくは[こちら](https://github.com/koshian2/Pseudo-Label-Keras/blob/master/pseudo_cifar.py)のコードをご覧ください。

#### ジェネレーター、損失関数、疑似ラベルの生成を全て1つのコールバッククラスでやる
半教師あり学習では、データのジェネレーター、損失関数、疑似ラベルの生成（on_epoch_end）との間で、ラベル付けデータと$\alpha(t)$についてやり取りする必要があるので、これらを1つのクラスとして定義するのが良いのではないかと思います。

最後にt-SNEで出力層の手前の特徴量プロットしています。このように、**Kerasならコールバックの中で損失関数とジェネレーターを定義する**ことで、半教師あり学習のパラメーターを相互にやり取りすることができます。

#### y_trueの11番目に「疑似ラベルかどうか」のフラグをつける
次のポイントですが、**本物のラベル（＝0）か疑似ラベル（＝1）かという情報が必要なので、「y_true」の11番目に与える**ようにしています。他にいいやり方があるかもしれませんが、とりあえずこれでできます。

ここでy_trueは(None, 11)というshapeになり、y_predは(None, 10)となり、2つのshapeが異なるようになります。損失関数のカスタマイズが必要ですが、やっていることはスライスしてcategorical_crossentropyを計算して係数を掛けているだけです。同様に精度の評価関数もカスタマイズしていますが、これは両方が(None, 10)というshapeになるようにスライスしているだけです。

全体のコードはこちらです。
https://github.com/koshian2/Pseudo-Label-Keras

## 結果：実験１
### 半教師あり学習
| 本物のラベル数 | テスト精度 | 訓練精度 | テスト損失 |   訓練損失 |
|:--------------:|-----------:|---------:|-----------:|-----------:|
|       500      |     39.02% |   10.99% | 3.597.E+00 | 1.218.E-06 |
|      1000      |     50.89% |   17.50% | 2.880.E+00 | 1.489.E-06 |
|      5000      |     74.58% |   19.98% | 1.231.E+00 | 1.276.E-05 |
|      10000     |     80.46% |   27.85% | 8.059.E-01 | 2.358.E-04 |

このように疑似ラベルを使った半教師あり学習では、**テスト精度のほうが訓練精度より高いという変わったことが起きます**。本物のラベルに対してもあまりよく当てはまらない、疑似ラベルもそこまで当たっていないというのになぜか汎化性能は良いという不思議な状況です。
ただし、損失ベースで見ると訓練データのほうがテストデータよりも低いことは教師あり学習と変わりありません。このような差が出るのは、損失関数を$\alpha(t)$や疑似ラベルによりカスタマイズしているためで、訓練損失が最小化されるときの分布と、訓練精度が最大化されるときの分布が違うよということだと思われます。

テスト精度の推移は以下のようになります。$\alpha(t)$の上昇に応じて良くなってるのがわかります。

![semi_01_Pseudo.png](https://qiita-image-store.s3.amazonaws.com/0/232088/e58e5b5c-6820-2747-cce7-3c3547624b66.png)


### 教師あり学習
| 本物のラベル数 | テスト精度 | 訓練精度 | テスト損失 |   訓練損失 |
|:--------------:|-----------:|---------:|-----------:|-----------:|
|       500      |     39.81% |  100.00% | 2.588.E+00 | 8.750.E-04 |
|      1000      |     47.95% |  100.00% | 2.000.E+00 | 7.215.E-04 |
|      5000      |     69.41% |  100.00% | 1.271.E+00 | 2.173.E-04 |
|      10000     |     77.96% |  100.00% | 8.979.E-01 | 4.049.E-04 |

こちらはおなじみの教師あり学習。本物のラベルしか訓練させていませんし、損失関数もカスタマイズしていないので、当然訓練精度のほうが高いです。ただし、500を除いて**テスト精度は半教師あり学習のほうが高い結果**となりました。半教師あり学習のほうがうまくいくというのは論文の通りです。

テスト精度の推移は次の通りです。半教師あり学習よりも訓練の進みが若干ゆっくりな感じがします。

![semi_01_Supervised.png](https://qiita-image-store.s3.amazonaws.com/0/232088/ef9d3aa9-6cad-16f2-9c3a-60e8213dc1be.png)

### 事前訓練ありの半教師あり学習

ところが、ネットワークを本物のラベルである程度訓練させてから、**半教師あり学習をするとうまくいきません**。まずはテスト損失のプロットをしてみます。

![semin_01_Pretrain Pseudo.png](https://qiita-image-store.s3.amazonaws.com/0/232088/569327af-5cc5-c6dc-f0b4-9b483e6c353e.png)

このようにはじめは良かったのに、疑似ラベルが訓練を邪魔しているのがわかります。直感的には「GANのDiscriminatorとGeneratorを同時に訓練させなきゃいけない（片方だけ訓練させるのはうまくいかない）」というのと似ている気がします。GANも言われてみれば「半教師あり学習」の側面もありますので[^1]。ただGANほどは訓練は難しくないです。

[^1]: Discriminatorがそうです。本物のデータとGeneratorが生成した偽物のデータを混ぜて、本物かどうかを見分けます。ここはまさに半教師あり学習の発想です。


| 本物のラベル数 | テスト精度 | 訓練精度 | テスト損失 |   訓練損失 |
|:--------------:|-----------:|---------:|-----------:|-----------:|
|       500      |     21.83% |   74.91% | 3.126.E+00 | 6.442.E-02 |
|      1000      |     22.70% |   79.90% | 2.846.E+00 | 6.464.E-02 |
|      5000      |     39.98% |   72.81% | 2.708.E+00 | 5.485.E-02 |
|      10000     |     56.15% |   42.96% | 1.481.E+00 | 1.206.E-01 |

この通りダメダメですね。事前訓練は忘れましょう。

## なぜ半教師あり学習がうまくいくのか
### Entropy Regularization
半教師あり学習というと、「本物のラベルから疑似ラベルを推定してだんだん疑似ラベルが正しくなっていく」ように見えますが、実はそうではありません。むしろ、訓練データの疑似ラベルの精度も本物のラベルの精度もかなり低いです。しかし、テスト精度は本物のラベルだけを使った教師あり学習よりも高くなる結果が起こっています。

理論的にはこれは「**Entropy Regularization**」というそうです。[Yoshua Bengio先生監修のテキストに書かれている](https://pdfs.semanticscholar.org/1ee2/7c66fabde8ffe90bd2f4ccee5835f8dedbb9.pdf)ので詳しく知りたい方は読んでみてください。ここでは直感的な説明に留めます。

教師あり学習と半教師あり学習について、出力層の直前の特徴量をt-SNEを使ってテストデータ1万件を2次元に投射してみました。上から本物のラベルの数が1000、5000のケースで、左が半教師あり学習、右が教師あり学習です。

![semi_01_01000.png](https://qiita-image-store.s3.amazonaws.com/0/232088/6bde89ba-1903-5ab7-a9ad-713632194855.png)

![semi_01_05000.png](https://qiita-image-store.s3.amazonaws.com/0/232088/8cd182ae-9e26-3e59-a148-c34d56bf2b10.png)

このように教師あり学習では、もともとのサンプル数が少ないのでオーバーフィッティングする、つまり決定境界がぐにゃぐにゃするという現象が起きます。テストデータをプロットしても境界部分でごちゃごちゃになっているのはそのせいです。ここで疑似ラベルによる半教師あり学習を適用すると、ぐにゃぐにゃの決定境界が多少マシに（フラットになる）というのが確認されます。これは重み減衰やドロップアウトといった**正則化（Regularization）**の効果にほかなりません。半教師あり学習に「Entropy Regularization」という効果があるのはこういう理由です。

### 本物のラベルと比べるとパワーは弱い
しかし、あくまで疑似ラベルは「正則化」にすぎないので、本物のラベルと比べるとだいぶパワーが弱いです。テスト精度の「半教師あり」－「教師あり」、つまり疑似ラベルによる精度のゲインを求めましょう。

| 本物のラベル数 | 教師あり | 半教師あり | ゲイン | 疑似ラベル数 |
|:--------------:|---------:|-----------:|-------:|-------------:|
|       500      |   39.81% |     39.02% | -0.79% |        49500 |
|      1000      |   47.95% |     50.89% |  2.94% |        49000 |
|      5000      |   69.41% |     74.58% |  5.17% |        45000 |
|      10000     |   77.96% |     80.46% |  2.50% |        40000 |

一番効いているのはN=5000の場合ですが、45000枚画像を投入したところでたかだか5%しか上がりません。もしこれが本物のラベルだったら20%は上がります。事実N=5000から本物のラベルをあと5000枚投入すれば精度は8.5%も上がっています。したがって、**半教師あり学習を前提に考えるのではなく、そもそもの教師あり学習での本物のラベルを増やすのが一番効果が高い**というのがわかるでしょう。

### チューニングが若干難しい
そして、事前訓練させた場合で見たように、半教師あり学習は必ずしも成功するとは限りません。結局、本物のラベルと疑似ラベルの間のバランスが（**元ネタの論文にも書かれていますが、特に$\alpha(t)$が**）重要で、オーバーフィッティングを疑似ラベルで打ち消すような相互作用が生まれるような状況に持ち込まないといけません。

また、疑似ラベルにより訓練－テスト間の差が把握しづらくなるので、Bias-Varianceによるオーバーフィッティング/アンダーフィッティングの分析が難しくなると思います（できないというわけではありません）。もし使うのなら、教師あり学習であらかた正則化やData Augmentationをして、どうしてもダメなときの最後の一押しとして使ってみると良いのではないでしょうか。

### 何に使えるか？
では半教師あり学習とは具体的にどこでつかえるでしょうか？

#### コンペに使える（直球）
コンペみたいに訓練画像を簡単に増やせなく、1%でも精度が欲しいケースではかなり有効に機能すると思います。ただし**必ずコンペのルールを確認してください**。「与えられたデータ以外を使ってはならない」と名言されているコンペでは、例えばインターネットからダウンロードしてきた画像で疑似ラベルを作ることは明確なルール違反です。失格になっても責任は取れません。

#### アノテーションが複雑なときに使える？
分類問題ならクラス設定して終わりですが、物体検出やセグメンテーションのようにアノテーション（ラベル）そのものが複雑で、簡単に本物のデータを増やせないケース。これはうまくいくかは未知数ですが、「Semi Supervised Segmentation」でググるとそこそこ論文が出てくるので、やってみる価値はあることだと思います。

## 実験2～転移学習と半教師あり学習～
でもこれだと「ぶっちゃけ転移学習でよくね？」という疑問がわきます。厳密に言えば「転移学習と半教師あり学習は定義的には違って、転移学習は本物のラベルしか使わないから教師あり学習、半教師あり学習は未ラベルデータも使うから教師ありではない」、まあそれはそうなんです。でも実用上は簡単に精度上げらればそれでいいんで、そういう言葉の定義は今は投げ捨てましょう。いま主題とするのは**「サンプル数少ないんだけど、正直転移学習と半教師あり学習どちらを使えばいいの？」という疑問に答える**ことです。

そこで、このネットワークに軽さ的な意味で近そうな[MobileNet](https://keras.io/ja/applications/#mobilenet)を転移学習させるケースを追加します。以下の4パターンを試します。

1. MobileNetでImageNetで訓練させたモデルを教師あり学習として転移学習
2. MobileNetで教師あり学習として1から訓練
3. MobileNetで半教師あり学習として1から訓練
4. MobileNetでImageNetで訓練させたモデルを半教師あり学習として転移学習

これを先程と同様に、**本物のラベル数が500、1000、5000、10000**で比較します。ただし、ImageNetの係数は「128×128」からしか受け付けないので、Inputのあとの4倍のアップサンプリングレイヤーをはさみます。またGPUメモリーの関係でバッチサイズ512にできないので、バッチサイズは全て256に変更します。全て100エポック動かし、転移学習を使うときのみ学習率0.01、係数0.9のモメンタムを使います。転移学習を使わないときは学習率1e-3のAdamを使います。それ以外は先程と同じです。

転移学習させる場合は次のようにモデルを生成します。

```python:転移学習
from keras.layers import Input, UpSampling2D, Dense, GlobalAveragePooling2D
from keras.applications import MobileNet
from keras.models import Model

def create_cnn():
    net = MobileNet(input_shape=(128,128,3), include_top=False)
    # conv_pw_6から訓練させる(41)
    for i in range(41):
        net.layers[i].trainable = False
    # upsampling(32->128)
    input = Input((32,32,3))
    x = UpSampling2D(4)(input)
    x = net(x)
    x = GlobalAveragePooling2D()(x)
    x = Dense(10, activation="softmax")(x)

    model = Model(input, x)
    model.summary()
    return model
```

転移学習させない場合は次のようにします。

```python:転移学習ではない
def create_cnn():
    net = MobileNet(input_shape=(128,128,3), weights=None, include_top=False)
    # upsampling(32->128)
    input = Input((32,32,3))
    x = UpSampling2D(4)(input)
    x = net(x)
    x = GlobalAveragePooling2D()(x)
    x = Dense(10, activation="softmax")(x)

    model = Model(input, x)
    model.summary()
    return model
```

ソースは以下にあります。

1. 教師あり転移学習　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/mobilenet_transfer_cifar.py
2. 教師あり転移なし　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/mobilenet_supervised_cifar.py
3. 半教師あり転移なし　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/mobilenet_pseudo_cifar.py
4. 半教師あり転移学習　https://github.com/koshian2/Pseudo-Label-Keras/blob/master/mobilenet_transfer_pseudo_cifar.py

## 結果：実験２
パターン別の最大テスト精度を比較すると以下のようになります。

|   本物のラベル数   |  N=500 | N=1000 | N=5000 | N=10000 |
|:------------------:|-------:|-------:|-------:|--------:|
|  教師あり転移学習  | 51.77% | 60.13% | 68.23% |  74.92% |
|  教師あり転移なし  | 22.70% | 30.81% | 55.27% |  65.77% |
| 半教師あり転移なし | 34.26% | 44.95% | 63.39% |  72.94% |
| 半教師あり転移学習 | 46.14% | 51.37% | 65.00% |  73.86% |

結果はとてもわかりやすく、

* 転移なしで本物のラベルだけの場合は悪く、特にサンプルが少ない場合で訓練が全く進んでいない（オーバーフィッティングしている）
* 転移学習なしの場合は半教師あり学習を使えば、幾分まともになる。**疑似ラベルの画像といえど、エッジ検出などの低レベルの特徴量抽出に役立っているのでは**ないか
* ただし、転移学習を使えるのなら半教師あり学習よりも転移学習のほうが効果が高く、**全てのケースにおいて転移学習＞半教師あり学習**となった。
* 半教師あり学習と転移学習を組み合わせることもできるが、本物のサンプル数が増えれば教師あり転移学習の結果に近づくものの、転移学習を超えることはなかった。

ということで、「結局、転移学習使えばいいじゃん」という問いには、この実験では「はい、仰る通りでございます」という結果となりました。

テスト精度をプロットすると次のようになります。

![semi_02.png](https://qiita-image-store.s3.amazonaws.com/0/232088/a623f3ae-77a5-48de-b2bf-c6176abb5f6e.png)

サンプル数が少なければ、教師あり学習の転移学習のほうが強いのは変わりないものの、N=10000のときはちょっと例外？またN=1000のときの出力層直前の潜在空間をt-SNEで2次元にプロットしました。

![semi_02_embedding.png](https://qiita-image-store.s3.amazonaws.com/0/232088/40ace632-f107-1d56-0781-6d7f03ffce7c.png)

転移学習がないと分類が全然うまく行っていないのがわかります。

### 転移学習がいつも使えるとは限らない
転移学習は普遍的に使えるというものではないので、**半教師あり学習が無駄というわけではありません**。例えば画像なら、ImageNetで訓練済みの重みは容易に手に入りますが、自然言語、音声、動画、構造化データなど、あるいは画像でも漫画やアニメなどImageNetと分布が違うようなサンプルに対する訓練済みの重みは、手に入れるのがより難しくなります。

特にImageNetは何百万以上の「本物の」ラベルを使って訓練しているため、写真テイストの画像が中心という限定はあるものの、訓練済みモデルとしては相当優秀です。生半可な疑似ラベルでは超すことができません。つまり、**転移学習といえどImageNetではないようなもっとサンプル数が少なく質の悪いモデルだったら、半教師あり学習が追い抜く可能性はあります**。なので、転移学習を使えばいいという部分だけ拡大解釈しないようにしたいです。少なくとも、画像ならImageNetで訓練済みのモデルで、ImageNetに近い分布の画像の判定なら転移学習のほうが強いということがわかっただけです。

そしてそもそも転移学習が使えない場合で、サンプル数が少なすぎて普通の教師あり学習でろくに訓練できない場合は、半教師あり学習が選択肢に入ってくると思います。ただし、ここで見た疑似ラベルによる半教師あり学習はEntropy Regularizationでしかないので、他の正則化やData Augmentationも同様に選択肢に入れるべきではないでしょうか。そして何よりも、「本物のラベル」を増やすことが最も効果が高いです。

これらをまとめると、サンプル数が少ない場合は、以下のようなワークフローになるのではないでしょうか。上から順に優先度が高いことです。

* 転移学習が使えるか？
* 本物のラベルデータを例えばあと10倍増やすのが容易か？
* DataAugmentationや正則化は試したか？
* ラベルがついていない似たデータを、本物のデータの例えば10倍取ってくることは可能か。インターネットからダウンロードするなどして入手がそこまで難しくないか→**Yesなら、半教師あり学習を試してみる**

なので、全体のフローの中で見ると、半教師あり学習の重要性というのはそこまで高くないのかなと自分は思います。

## まとめ
疑似ラベルによる半教師あり学習をCIFAR-10を使って見ていきましたが、以下のことがわかりました。

* 疑似ラベルによる半教師あり学習は「**Entropy Regularization**」で、スーパーマジックではなく正則化手法の1つとして捉えるとよいのではないか
* 半教師あり学習は本物のラベルにより疑似ラベルの訓練が進むのではなく、疑似ラベルと本物のラベルが協調して汎化性能を高めていく。損失関数が歪んでいるため、むしろ訓練精度はかなり低い結果となる。教師あり学習のフレームワークでのBias-Variance評価が難しくなる。訓練も若干難しい。
* **転移学習が使えない場合や、転移学習でのモデルの質があまり良くない場合は半教師あり学習は有効では**ないか。ただし、ImageNetのように転移元の質が相当高い場合は、そもそも本物のラベルではない疑似ラベルで、転移学習を追い抜くのは難しい。