エラー率0.4%のFace Landmark回帰モデル

Face Landmarkを求める問題です。FaceBoxを入力し、目（瞼輪郭）の中心2点、鼻筋2点、鼻の下3点の座標を、求める。

![family (59)_light22.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210659/18d7f98b-7121-139f-aca4-dec75846c832.jpeg)![family (46)_flip_crop_13_7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210659/be3a5d14-296f-93b0-418e-2eba7706452b.jpeg)![family (32)_light10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210659/ccf9b719-12a5-ee8c-5150-b9af1191a96a.jpeg)![family (88)_rotate_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210659/1dcb98f6-8a76-e4cf-3d82-ea0aebde8c05.jpeg)

Landmark問題は、問題を複数に分け、モジュールをカスケードさせて、解くのがトレンドらしいです->[参考](https://qiita.com/RanWensheng/items/d8768395166d041a753a)。
そこで、第一弾目の課題として、両目と鼻筋の概略位置を捉えました（鼻の下3点は、おまけです。使うかどうかわからない）。

入力データは、インターネット上でオープンにされているデータセットのうち、商用を許諾されたものを利用しています。それを、OpenCVでFaceBoxでCropし、128x128にリスケールしたものです。Flipと、ランダムに回転、明度変更、Cropして、218K個まで増やしています。

正解座標Tagは、Dlibの68点だっけ、あれをベースに利用しましたが、いろいろ加工したり、手修正を加えて、作っています（なぜDlibのをそのまま使えないか、最後に書いておきます。）

PyTorchを使っています。データローダーと、Training/testコードは、PyTorchのサイトのTutorialのもろもろのサンプルコードの微改造なので、省略します。そこに、以下のモデルを投入。

AveragePoolingをはさみながらConvolutionを4層。その後、Affine層を3層。２D回帰なので、ロス関数はMSEを使っています。

エラーは、7個のMarkのどれか一つでも正解座標からx/y方向のいずれでも4pixel以上外れていたら間違いと判定しています。その結果、８エポック目で、エラー率は0.0037、およそ0.4%です。ほぼ間違えない。

```python
class FaceMark7Net(nn.Module):
    def __init__(self):
        super(FaceMark7Net, self).__init__()

        self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
        nn.init.kaiming_normal_(self.conv1.weight)
        self.bn1 = nn.BatchNorm2d(16)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        nn.init.kaiming_normal_(self.conv2.weight)
        self.bn2 = nn.BatchNorm2d(32)
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
        nn.init.kaiming_normal_(self.conv3.weight)
        self.bn3 = nn.BatchNorm2d(64)
        self.conv4 = nn.Conv2d(64, 128, 3, padding=1)
        nn.init.kaiming_normal_(self.conv4.weight)
        self.bn4 = nn.BatchNorm2d(128)

        self.fc1 = nn.Linear(32768, 1024)
        nn.init.kaiming_normal_(self.fc1.weight)
        self.bnf1 = nn.BatchNorm1d(1024)
        self.fc2 = nn.Linear(1024, 128)
        nn.init.kaiming_normal_(self.fc2.weight)
        self.bnf2 = nn.BatchNorm1d(128)
        self.fc3 = nn.Linear(128, 14)
        nn.init.kaiming_normal_(self.fc3.weight)

    def forward(self, x):

        x = x.float()
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.avg_pool2d(x, 2)
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.avg_pool2d(x, 2)
        x = F.relu(self.bn3(self.conv3(x)))
        x = F.avg_pool2d(x, 2)
        x = F.relu(self.bn4(self.conv4(x)))

        x = torch.flatten(x, 1)
        x = F.relu(self.bnf1(self.fc1(x)))
        x = F.relu(self.bnf2(self.fc2(x)))
        x = self.fc3(x)
        return x
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210659/d01c7fc8-27fd-8cba-4e7d-b716cfa6bfd0.png)

Attention機構をはさむのも実験しましたが、なしでも十分な性能出ました。

モデルがでかいので、これからまたいろいろ手を加えると思います。




注：DlibのLandmarkの問題
０．西洋人の顔以外の精度が低い。眼鏡や細目に弱い。
１．Landmarkが一体どういう定義（作業基準）でつけられたものなのか、探ったが、結局、昔、イギリスのどこかの大学だかでLandmark付けのコンテストをやっていた、その時の正解データがまずありき、のようです。で、定義なしで作られたデータでTrainingされた、Dlibの検出器の結果には、揺れがある。例えば、鼻の頂と思われるMarkは、人が判断して鼻が一番高くなったところかというとそうでもなく、わしっぱなの先っぽだったり、東洋人の低い鼻のまあ適当な位置だったりする。目回りも、瞼の内側だったり、外側だったり、揺れている。鼻の上のつけねらしいMarkの位置も、どういう基準なのか、適当。
２．顔を側面に向けると、片目は、かなり隠れます。顔を下向きにすると、鼻の下の部分は隠れてしまいます。Dlibのつけるマークは、隠れている３D座標を２Dに写像したものでなく、カメラから見て鼻部分、目部分の境界にあり、セグメンテーション向きのものです。応用によってはそれでいいのですが、セグメンテーションが最終目的ではない場合、不都合です。