詳細な画像認識の目的は、犬がハスキーか柴かなど、カテゴリーのサブカテゴリーを区別することです。詳細な画像分類は、強監視情報 (画像カテゴリ、オブジェクト ラベル ボックス、パーツ ラベル ポイントなど) と弱い監視情報 (画像カテゴリのみ) に分類できます。詳細については、詳細な
バイリニア CNN は、2015 年の論文「細粒度視覚認識のためのバイリニア CNN モデル」で提案されました。これは、弱い監視情報に基づく細粒度の画像分類モデルです。
1. バイリニアCNNのネットワーク構造
バイリニア CNN のネットワーク構造は次のとおりです。
バイリニア CNN は 2 つの CNN 特徴抽出ネットワークから構成され、その出力を外積 (外積) してバイリニア ベクトル (画像記述子と呼ぶことができます) を取得し、分類します。
この 2 つの CNN は実際には同一であることに注意してください。コードはネットワーク (通常は事前トレーニングされた vgg16 または ResNet18 ネットワーク) を使用しますが、特徴の相互作用を実現するために、ネットワーク出力値 x に対する x と x T の行列積を計算します。。
もちろん、2 つの異なる CNN ネットワークを使用することも可能です。
双線形ネットワークは、画像の二変量変化をモデル化するために使用されます。ネットワーク A の役割は画像内の物体の特徴部分を特定することであり、ネットワーク B はネットワーク A で検出された特徴領域から特徴を抽出するために使用されると言われています。2 つのネットワークは相互に連携して、きめの細かい画像分類を実現します。しかし、それがネットワークで実装されている場合、この発言はあまりにもばかげています。
2 つの CNN の出力に対するモデルの演算は線形であるため (加算演算と乗算演算しかないため、行列乗算は線形演算です)、ネットワークは双線形 CNN と呼ばれます。
第二に、行列の外積(外積)
2.1 外積の計算方法
インターネット上の多くのブログでは、行列の外積がクロネッカー積であると述べていますが、双一次 CNN コードの実装における外積は、実際には通常の行列の乗算(つまり、線形代数における最も一般的な行列の乗算) であり、クロネッカー製品。
コードは、この記事の 3 番目のパート「PyTorch ネットワーク コードの実装」にあります。
外積を計算するコードは次のとおりです。
x = torch.bmm(x, torch.transpose(x, 1, 2)) / (28 * 28)
ここでの torch.bmm(a,b) は、次のことを証明するための例として、通常の行列の乗算です。
import torch
a = torch.randint(low=0, high=5, size=(1, 2, 2))
b = torch.randint(low=0, high=5, size=(1, 2, 2))
c = torch.bmm(a, b)
print(f"a = {
a}")
print(f"b = {
b}")
print(f"c = {
c}")
"""
a = tensor([[[4, 0],
[4, 1]]])
b = tensor([[[1, 4],
[2, 4]]])
c = tensor([[[ 4, 16], 4 = 4 * 1 + 0 * 2, 16 = 4 * 4 + 0 * 4
[ 6, 20]]]) 6 = 4 * 1 + 1 * 2, 20 = 4 * 4 + 1 * 4
"""
このバージョンの PyTorch コードが正しい場合、ここでの外積は単なる通常の行列の乗算です。もちろん、私は Bilinear CNN の Matlab ソース コードを読んでいません。ソース コードのアドレスはBilinear CNNs for Fine-graned Visual Recognitionです。誰でも批判や修正を歓迎します (内積と内積の違いはわかりません)外積)。
2.2 外積の役割
外積は実際には特徴融合の一方法にすぎません。その他の一般的に使用される特徴融合方法には、最大値融合、平均値融合、加算、連結などが含まれます。
しかし、外積は行列演算を通じて異なるチャネル間の特徴の相関を捉えることができます。記述ベクトルの異なる次元は畳み込み特徴の異なるチャネルに対応し、異なるチャネルは異なる意味特徴を抽出するため、入力画像の異なる意味特徴間の関係は双一次演算を通じて同時に取得できます。
3. PyTorch ネットワーク コードの実装
vgg16 に基づく:
import torch
import torch.nn as nn
import torchvision
class BCNN_fc(nn.Module):
def __init__(self):
super(BCNN_fc, self).__init__()
# VGG16的卷积层和池化层
self.features = torchvision.models.vgg16(pretrained=True).features
# 去除最后一个 pooling 层
self.features = nn.Sequential(*list(self.features.children())[:-1])
# 线性分类层
self.fc = nn.Linear(512 * 512, 200)
# 冻结以前的所有层
for param in self.features.parameters():
param.requres_grad = False
# 初始化fc层
nn.init.kaiming_normal_(self.fc.weight.data)
if self.fc.bias is not None:
nn.init.constant_(self.fc.bias.data, val=0)
def forward(self, x):
N = x.size()[0]
assert x.size() == (N, 3, 448, 448)
# 特征提取
x = self.features(x)
assert x.size() == (N, 512, 28, 28)
x = x.view(N, 512, 28 * 28)
# 双线性矩阵相乘
# 对于 c=torch.bmm(a,b),其中 a.shape=[b,m,n], b.shape=[b,n,p], 则 c.shape=[b,m,p]
# 这里其实是对 x 和 x^T 进行了相乘
# 除以 28 * 28 是为了防止最后 softmax 的梯度过小
x = torch.bmm(x, torch.transpose(x, 1, 2)) / (28 * 28)
assert x.size() == (N, 512, 512)
# 有符号平方根,y = sign(x) * sqrt(|x|)
x = torch.sign(x) * torch.sqrt(torch.abs(x) + 1e-10)
x = x.view(N, 512 * 512)
assert x.size() == (N, 512 * 512)
# L2归一化
x = torch.nn.functional.normalize(x)
assert x.size() == (N, 512 * 512)
# 全连接分类层
x = self.fc(x)
assert x.size() == (N, 200)
return x
if __name__ == '__main__':
input = torch.randn(16, 3, 448, 448)
model = BCNN_fc()
output = model(input)
print(output.shape) # torch.Size([16, 200])
ResNet18 に基づく:
import torch
import torch.nn as nn
from torchvision.models import resnet18
class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.features = nn.Sequential(resnet18().conv1,
resnet18().bn1,
resnet18().relu,
resnet18().maxpool,
resnet18().layer1,
resnet18().layer2,
resnet18().layer3,
resnet18().layer4)
self.classifiers = nn.Sequential(nn.Linear(512**2,14))
def forward(self,x):
x=self.features(x)
batch_size = x.size(0)
feature_size = x.size(2)*x.size(3)
x = x.view(batch_size , 512, feature_size)
x = (torch.bmm(x, torch.transpose(x, 1, 2)) / feature_size).view(batch_size, -1)
x = torch.nn.functional.normalize(torch.sign(x)*torch.sqrt(torch.abs(x)+1e-10))
x = self.classifiers(x)
return x