ArcFace(ArcMarginProduct)
リファレンス
- https://github.com/lyakaap/Landmark2019-1st-and-3rd-Place-Solution/blob/master/src/modeling/metric_learning.py
- ArcFace(ArcMarginProduct)のソースコード
- Pytorch Metric Learning Pipeline : Only Images | Kaggle
- Nishikaからチュートリアルとして提供されたtrain.pyが参考にしているコード。ほとんどそのまま利用してた
ArcFaceの特徴

- 似たものは近く、似てないものは遠くなるように学習する
- 入力画像を右図のように写像する
- 参加したコンペ では約90万件もあるので、左のような感じだと境目がはっきりしない
ArcFaceを使った距離学習モデル作成例
モデル全文
(75行)
class JPONet(nn.Module):
def __init__(self,
n_classes,
model_name='efficientnet_b3',
use_fc=False,
fc_dim=224,
dropout=0.0,
loss_module='softmax',
s=30.0,
margin=0.50,
ls_eps=0.0,
theta_zero=0.785,
pretrained=True):
"""
:param n_classes:
:param model_name: name of model from pretrainedmodels
e.g. resnet50, resnext101_32x4d, pnasnet5large
:param pooling: One of ('SPoC', 'MAC', 'RMAC', 'GeM', 'Rpool', 'Flatten', 'CompactBilinearPooling')
:param loss_module: One of ('arcface', 'cosface', 'softmax')
"""
super(JPONet, self).__init__()
print('Building Model Backbone for {} model'.format(model_name))
self.backbone = timm.create_model(model_name, pretrained=pretrained)
final_in_features = self.backbone.classifier.in_features
self.backbone.classifier = nn.Identity() #恒等関数
self.backbone.global_pool = nn.Identity()
self.pooling = nn.AdaptiveAvgPool2d(1)
self.use_fc = use_fc
if use_fc:
self.dropout = nn.Dropout(p=dropout)
self.fc = nn.Linear(final_in_features, fc_dim)
self.bn = nn.BatchNorm1d(fc_dim)
self._init_params()
final_in_features = fc_dim
self.loss_module = loss_module
if loss_module == 'arcface':
self.final = ArcMarginProduct(final_in_features, n_classes, s=s, m=margin, easy_margin=False, ls_eps=ls_eps)
elif loss_module == 'cosface':
self.final = AddMarginProduct(final_in_features, n_classes, s=s, m=margin)
elif loss_module == 'adacos':
self.final = AdaCos(final_in_features, n_classes, m=margin, theta_zero=theta_zero)
else:
self.final = nn.Linear(final_in_features, n_classes)
def _init_params(self):
nn.init.xavier_normal_(self.fc.weight)
nn.init.constant_(self.fc.bias, 0)
nn.init.constant_(self.bn.weight, 1)
nn.init.constant_(self.bn.bias, 0)
def forward(self, x, label):
feature = self.extract_feat(x)
if self.loss_module in ('arcface', 'cosface', 'adacos'):
logits = self.final(feature, label)
else:
logits = self.final(feature)
return logits
def extract_feat(self, x):
batch_size = x.shape[0]
x = self.backbone(x)
x = self.pooling(x).view(batch_size, -1)
if self.use_fc:
x = self.dropout(x)
x = self.fc(x)
x = self.bn(x)
return x
- timmでEfficientNetを読み込み
- ネットワークの終端を加工して、AcrFaceに渡す
- 加工しているコードがとても参考になる
実体化(インスタンス作成)
(15行)
loss_module = 'arcface' #'cosface' #'adacos'
model_params = {
'n_classes': df['cite_gid'].nunique(), #分類するクラス、コンペでは80万件に分類
'model_name': 'efficientnet_b3', #利用モデル→b0、b3で悩む
'use_fc': True, #GoogleColab Proではメモリ足らず、Trueで縮小
'fc_dim': 224, #商標画像を224に前処理した
'dropout': 0.0, #さわらず
'loss_module': loss_module,
's': 30.0, #さわらず
'margin': 0.50, #さわらず
'ls_eps': 0.0, #さわらず
'theta_zero': 0.785, #さわらず
'pretrained': True #初回のみ
}
model = JPONet(**model_params)
- 最終行がインスタンス作成しているところ
- 「model_params」を引数にしてインスタンス作成
- モデルのコンストラクタ(def __init__)にある引数=パラメータをまとめて書いている
- 「**」付けてる
作成されたモデルを確認してみる
見るところ
- ネットワークの最後の部分
- モデル実体の全文は長いので こちら
- ベース(backbone)はEfficientNet(分類モデル)
- ネットワークの最後が加工されている
- ArcFace(ArcMarginProduct)が最後
- (final)という名前になっている
ポイント
- つなげ方が参考になる
- つなげるだけで深層距離学習になる
注目するところだけ抜き出した
(backbone): EfficientNet(・・・)
(pooling): AdaptiveAvgPool2d(output_size=1)
(dropout): Dropout(p=0.0, inplace=False)
(fc): Linear(in_features=1280, out_features=224, bias=True)
(bn): BatchNorm1d(224, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(final): ArcMarginProduct()
- 作成したインスタンスの中身(ネットワーク)を表示してから抜粋
ArcFaceをつなげる加工
- 既存のネットワーク(ここではEfficientNet)にAcrFaceをつなげるため、既存のネットワークの一部を加工
- AcrFace利用方法のチュートリアルが提供されていてそのまま使えるが、やり方を理解しておく
- あぁ、こうやってネットワークつないでいくんだなってわかる
1つ目の加工
モデル追加箇所
final_in_features = self.backbone.classifier.in_features
self.backbone.classifier = nn.Identity() #恒等関数
self.backbone.global_pool = nn.Identity()
- モデル全文の28行目
- (backbone)=EfficientNetの最後を加工するコード
- 「classifier」「global_pool」を恒等関数に置き換える
- 置き換える前に「in_features」を取り出しておく
- 1280(=EfficientNet-b0の圧縮後特徴量次元数)
加工前後(インスタンスの中身)
(global_pool): SelectAdaptivePool2d (pool_type=avg, flatten=Flatten(start_dim=1, end_dim=-1))
(classifier): Linear(in_features=1280, out_features=1000, bias=True)
↓↓↓
(global_pool): Identity()
(classifier): Identity()
2つ目の加工
モデル追加箇所
self.pooling = nn.AdaptiveAvgPool2d(1)
- モデル全文の31行目
- 上記が追加
- (global_pool)の代わりと思われる
- SelectAdaptivePool2d (pool_type=avg, flatten=Flatten(start_dim=1, end_dim=-1))をIdentity()に置き換えている
- poolingの後にclassificationする形は、元のモデルから崩さないようにしているように見える
加工後追加されたもの(インスタンスの中身)
(pooling): AdaptiveAvgPool2d(output_size=1)
3つ目の加工
モデル追加箇所
self.use_fc = use_fc
if use_fc:
self.dropout = nn.Dropout(p=dropout)
self.fc = nn.Linear(final_in_features, fc_dim)
self.bn = nn.BatchNorm1d(fc_dim)
self._init_params()
final_in_features = fc_dim
- モデル全文の33行目
- 次元削除
- 使わない方が精度が当然上がる
- GoogleColab Pro程度のメモリではオーバーフローしてしまうので、仕方なく利用
- 既存ネットワークへのつなぎ方として勉強になるコード
- 線形結合(nn.Linear)でfinal_in_features次元
- fc_dim次元に変換
- 6行目
- モデル全文の51行目の「_init_params」を呼び出し
- 追加したネットワーク分のパラメータを初期化している(後述)
- 「self.use_fc」
- 「forward」で使うのでインスタンス内変数(self)として保持
加工後追加されたもの(インスタンスの中身)
(dropout): Dropout(p=0.0, inplace=False)
(fc): Linear(in_features=1280, out_features=224, bias=True)
(bn): BatchNorm1d(224, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
追加モデルのパラメータ初期化箇所
def _init_params(self):
nn.init.xavier_normal_(self.fc.weight)
nn.init.constant_(self.fc.bias, 0)
nn.init.constant_(self.bn.weight, 1)
nn.init.constant_(self.bn.bias, 0)
- モデル全文の51行目
- Linear(線形結合)
- 「weight(重み)」「bias(バイアス)」を追加
- バイアスは0
- 重みはXavierの初期値
- BatchNorm1(バッチ正規化)
- 「weight(重み)」「bias(バイアス)」を追加
- バイアスは0
- 重み1
4つ目の加工
モデル追加箇所
self.loss_module = loss_module
if loss_module == 'arcface':
self.final = ArcMarginProduct(final_in_features, n_classes, s=s, m=margin, easy_margin=False, ls_eps=ls_eps)
elif loss_module == 'cosface':
self.final = AddMarginProduct(final_in_features, n_classes, s=s, m=margin)
elif loss_module == 'adacos':
self.final = AdaCos(final_in_features, n_classes, m=margin, theta_zero=theta_zero)
else:
self.final = nn.Linear(final_in_features, n_classes)
- モデル全文の41行目
- ArcFace(ArcMarginProduct)登場
- 「final」として「ArcMarginProduct」をつなげる
- 引数は取得した「final_in_features」と分類したいクラスの数「n_classes」
- Identifyにした部分がこれに置き換わったような感じ
- 加工前は「(classifier): Linear(in_features=1280, out_features=1000, bias=True)」
- 「use_fc」を使用している(True)場合
- 「final_in_features」には「fc_dim」が設定されている
加工後追加されたもの(インスタンスの中身)
(final): ArcMarginProduct()
補足
損失関数について
以下は、損失計算にかかる部分のみコードを抜き出した
def fetch_loss():
loss = nn.CrossEntropyLoss()
return loss
criterion = fetch_loss()
model = JPONet(**model_params)
output = model(image,targets)
loss = criterion(output,targets)
- コード内にsoftmaxが登場しないが、nn.CrossEntropyLoss()にsoftmaxが入っているので不要
- 参考:CrossEntropyLoss — PyTorch 1.10 documentation
- 論理式を見るとexp(x)/Σexp(x)=softmaxが含まれている
- 参考:CrossEntropyLoss — PyTorch 1.10 documentation
