バウンディングボックスの描画とcolormapによる色自動選択
バウンディングボックスを描画する度に、同じようなコードを何度も書いているので、 いい加減コピペで済むようにしたいと思ったので、ここにまとめておく。 今回はアノテーションデータを描画しているが、検出結果でもコードはほぼ同じ。 物体の色はcolormapによって自動選択できるようにしている。 なお、ここで説明するコードは ここ に置いてある。
概要
描画コードは以下の通り。
引数は順に、画像パス、アノテーションデータ、colormap(後述)、描画画像の保存パス。
cv2.rectangle()
でバウンディングボックスを、cv2.putText()
でラベルを描画している。
def visual_anno(img_path, annos, colormap, drawn_anno_img_path=None): # Draw annotion data on image. img = cv2.imread(img_path) for a in annos: color = colormap[a['label_name']] cv2.rectangle(img, (a['xmin'], a['ymin']), (a['xmax'], a['ymax']), color, 2) text = '{}'.format(a['label_name']) cv2.putText(img, text, (a['xmin'], a['ymin']-5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2, cv2.LINE_AA) # Save or show an image. if drawn_anno_img_path != None: cv2.imwrite(drawn_anno_img_path, img) else: cv2.imshow('image', img) cv2.waitKey(0) cv2.destroyAllWindows()
引数
visual_anno()
4つの引数は以下の通り。
引数 | 説明 |
---|---|
img_path | 画像パス。例: ../data/images/2007_006490.jpg |
annos | アノテーションデータ。ラベル名とバウンディングボックスの座標を含む辞書のリスト。 |
colormap | ラベル名とRBGの辞書。後述するラベル名とcolormap名を入力すると生成できる。 |
drawn_anno_img_path | 描画画像パス。例: ../experiments/results/drawn_anno_images/2007_006490.jpg |
annos
とcolormap
を以下に示す。
annos = [ {'label_name': 'bird', 'xmin': 80, 'ymin': 309, 'xmax': 109, 'ymax': 332} {'label_name': 'person', 'xmin': 143, 'ymin': 165, 'xmax': 169, 'ymax': 248} {'label_name': 'boat', 'xmin': 210, 'ymin': 9, 'xmax': 269, 'ymax': 67} {'label_name': 'boat', 'xmin': 172, 'ymin': 27, 'xmax': 226, 'ymax': 65} {'label_name': 'boat', 'xmin': 128, 'ymin': 25, 'xmax': 185, 'ymax': 67} ]
colormap = {'bird': (255, 0, 40), 'boat': (91, 255, 0), 'person': (0, 143, 255)}
OpenCV関数
cv2.rectangle()
とcv2.putText()
がある。
公式ドキュメント
を参考に以下にまとめておく。
cv2.rectangle(img, topLeft, downRight, color, thickness)
引数 | 説明 |
---|---|
img | 画像 |
topLeft | 矩形の左上の座標 |
downRight | 矩形の右下の座標 |
color | 矩形の色、RGB |
thichness | 矩形の線の太さ |
cv2.putText(img, text, downLeft, fontFace, fontScale, color, thickness, lineType)
引数 | 説明 |
---|---|
img | 画像 |
text | 描画するテキスト |
downLeft | 文字列の左下の座標 |
fontFace | フォントの種類。次の中から選ぶ。FONT_HERSHEY_SIMPLEX , FONT_HERSHEY_PLAIN , FONT_HERSHEY_DUPLEX , FONT_HERSHEY_COMPLEX , FONT_HERSHEY_TRIPLEX , FONT_HERSHEY_COMPLEX_SMALL , FONT_HERSHEY_SCRIPT_SIMPLEX , FONT_HERSHEY_SCRIPT_COMPLEX |
fontScale | フォントスケール(画像サイズによってフォントサイズは変化する) |
color | フォントの色 |
thickness | フォントの線の太さ |
lineType | 線の種類、基本cv2.LINE_AA(アンチエイリアス)でOK |
colormap
物体の色分けは自動で適切に割り当ててくれるとありがたい。 colormap を利用すると簡単に実現できる。
colormapの取得コードは次の通り。 ラベル名のリストとカラーマップ名を入れる。
def get_colormap(label_names, colormap_name): colormap = {} cmap = plt.get_cmap(colormap_name) for i in range(len(label_names)): rgb = [int(d) for d in np.array(cmap(float(i)/len(label_names)))*255][:3] colormap[label_names[i]] = tuple(rgb) return colormap
ラベル名は、['bird', 'boat', 'person']
のようなリスト。
カラーマップ名は、
ここ
から文字列を選択する。
例えば、gist_rainbow
は以下のようになる。
plt.get_cmap(colormap_name)
でカラーマップcmap
が取得できる。
cmap
に0~1の値を入れると特定の色が取得できる。
gist_rainbow
の場合、0.0が上図の一番左、1.0が一番右を示している。
0.0で赤に近い色、0.4で緑、0.8で青に近い色が取得できる。
1.0以上は同じ色が取得される。
以下に0.1ずつ増やした場合の色の変化を示す。
for i in range(15): v = i*0.1 if i == 11: print('=================') print(round(v, 1), tuple([int(d) for d in np.array(cmap(v))*255][:3]))
0.0 (255, 0, 40) # 赤に近い色 0.1 (255, 93, 0) 0.2 (255, 234, 0) 0.3 (140, 255, 0) 0.4 (0, 255, 0) # 緑 0.5 (0, 255, 139) 0.6 (0, 235, 255) 0.7 (0, 94, 255) 0.8 (41, 0, 255) # 青に近い色 0.9 (182, 0, 255) 1.0 (255, 0, 191) ================= 1.1 (255, 0, 191) 1.2 (255, 0, 191) 1.3 (255, 0, 191) 1.4 (255, 0, 191)
get_colormap()
の中で
rgb = [int(d) for d in np.array(cmap(float(i)/len(label_names)))*255][:3]
という長い1行があるが、次のような処理を行っている。
簡単に言うと、RGBを適切な値に変換している。
>>> i = 0 >>> label_names = ['bird', 'boat', 'person'] >>> print( float(i)/len(label_names) ) 0.0 >>> print( cmap(float(i)/len(label_names)) ) (1.0, 0.0, 0.16, 1.0) >>> print( np.array(cmap(float(i)/len(label_names))) ) [1. 0. 0.16 1. ] >>> print( np.array(cmap(float(i)/len(label_names)))*255 ) [255. 0. 40.8 255. ] >>> rgb = [int(d) for d in np.array(cmap(float(i)/len(label_names)))*255][:3] >>> print(rgb) [255, 0, 40] >>> print(label_names[i], tuple(rgb)) bird (255, 0, 40)
まとめ
普段はcolormapを使ってなかったのだが、まとめるにあたり使ってみた。
色々試したが、発色やバランス的にgist_rainbow
が良かったので採用している。
また自分自身も参照しやすいようにまとまったと思う。
参考文献
ResNet18+ArcFaceでCIFAR10を距離学習
以前、「簡易モデルでMNISTを距離学習」と 「ResNet18でCIFAR10を画像分類」 を実施した。 今回はこれらを組み合わせて「ResNet18+ArcFaceでCIFAR10を距離学習」を行った。
基本的には「ResNet18でCIFAR10を画像分類」 で実施した内容と同じになる。 異なるのはResNet18の最終層の前で特徴抽出して、それをメトリックであるArcFaceに通してから、損失関数に入力している点である。 なので、コード全体の説明は「ResNet18でCIFAR10を画像分類」 に譲るとして、ここではメトリックの周辺の実装について説明する。 なお、今回利用するメトリックはArcFaceで、上記で述べたように画像分類モデルに付け足すだけの優れものである。 しかも非常に精度が高い。 なぜ精度が高くなるのかは 「モダンな深層距離学習 (deep metric learning) 手法: SphereFace, CosFace, ArcFace - Qiita」 が詳しいので一読することをお薦めする。
なお、今回説明するコードは ここ に置いてある。
概要
実行手順は次の通り。
- データの取得
- モデルの定義
- メトリックの定義
- 損失関数と最適化関数の定義
- 学習と検証
「3. メトリックの定義」が今回新たに実装された部分である。
上記の手順はmain()
で次のように実行される。
def main(): # Parse arguments. args = parse_args() # Set device. device = 'cuda' if torch.cuda.is_available() else 'cpu' # Load dataset. train_loader, test_loader, class_names = cifar10.load_data(args.data_dir) # Set a model. model = get_model(args.model_name, args.n_feats) model = model.to(device) print(model) # Set a metric metric = metrics.ArcMarginProduct(args.n_feats, len(class_names), s=args.norm, m=args.margin, easy_margin=args.easy_margin) metric.to(device) # Set loss function and optimization function. criterion = nn.CrossEntropyLoss() optimizer = optim.SGD([{'params': model.parameters()}, {'params': metric.parameters()}], lr=args.lr, weight_decay=args.weight_decay) # Train and test. for epoch in range(args.n_epoch): # Train and test a model. train_acc, train_loss = train(device, train_loader, model, metric, criterion, optimizer) test_acc, test_loss = test(device, test_loader, model, metric, criterion) # Output score. stdout_temp = 'epoch: {:>3}, train acc: {:<8}, train loss: {:<8}, test acc: {:<8}, test loss: {:<8}' print(stdout_temp.format(epoch+1, train_acc, train_loss, test_acc, test_loss)) # Save a model checkpoint. model_ckpt_path = args.model_ckpt_path_temp.format(args.dataset_name, args.model_name, epoch+1) torch.save(model.state_dict(), model_ckpt_path) print('Saved a model checkpoint at {}'.format(model_ckpt_path)) print('')
1. データの取得
torchvisionからCIFAR10を取得。 詳しくは こちら を参照。
2. モデルの定義
画像分類モデルはget_model()
内で呼び出している。
この時、モデル名と出力する特徴数を引数として渡す。
今回、後者は512にしている。
model = get_model(args.model_name, args.n_feats)
今回はResNet18を使う。 出力クラス数が定義できるので、そこに特徴数を入れる。
from models.resnet import ResNet18 def get_model(model_name, num_classes=512): ... elif model_name == 'ResNet18': model = ResNet18(num_classes)
model
ディレクトリーのresnet.py
を呼び出す。
以下が中身。
最終層で512次元の特徴を返す。
def ResNet18(n_feats): return ResNetFace(BasicBlock, [2,2,2,2], num_classes=n_feats) class ResNetFace(nn.Module): def __init__(self, block, num_blocks, num_classes=10): super(ResNetFace, self).__init__() self.in_planes = 64 self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(64) self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1) self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2) self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2) self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) self.linear = nn.Linear(512*block.expansion, num_classes) def _make_layer(self, block, planes, num_blocks, stride): strides = [stride] + [1]*(num_blocks-1) layers = [] for stride in strides: layers.append(block(self.in_planes, planes, stride)) self.in_planes = planes * block.expansion return nn.Sequential(*layers) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.layer1(out) out = self.layer2(out) out = self.layer3(out) out = self.layer4(out) out = F.avg_pool2d(out, 4) out = out.view(out.size(0), -1) out = self.linear(out) return out
3. メトリックの定義
同じクラスを近くに、異なるクラスを遠くに置くようにするための損失関数を指す。 ArcFaceは、簡単にいうと下図のように円弧上(実際には超球面上)にクラスが適切に分布するように角度を学習する損失関数となる。
コードは
これ
がちゃんと動いた。
ArcMarginProduct()
がArcFace。
metric = metrics.ArcMarginProduct(args.n_feats, len(class_names), s=args.norm, m=args.margin, easy_margin=args.easy_margin)
第1引数のargs.n_feats
は入力する特徴数。
これはモデルの出力数でもある。
第2引数のlen(class_names)
はクラス数。
s
は下図のLogit前のFeature Re-scaleに当たる。
Logitの値がsoftmaxで機能するよう適切な値にスケールする。
m
はAdditive Anguler Mergin Penaltyにあたる。
つまりマージンによるペナルティーで、
クラスの重みと画像の特徴ベクトルのなす角度を最小化するのだが、
その際マージンをペナルティーとして加えることで、同じクラスを近くに、異なるクラスを遠くに置くようにするための効果が増す。
easy_margin
は以下のような処理を行っているが、理解しきれなかったので暇ができたら後で調べる。
ちなみにeasy_margin
はTrue
にしないと全然学習しなかったので注意。
if self.easy_margin: phi = torch.where(cosine > 0, phi, cosine) else: phi = torch.where(cosine > self.th, phi, cosine - self.mm)
図ではSoftmax
にかけたあと、CrossEntropyLoss
を算出している。
PyTorchではcriterion
に指定したCrossEntropyLoss
にSoftmax
も内包されているため、
特に記述する必要がない。
criterion = nn.CrossEntropyLoss() features = model(inputs) outputs = metric_fc(features, targets) loss = criterion(outputs, targets)
4. 損失関数と最適化関数の定義
損失関数は上記で述べた通り。
最適化関数はSGDを使い、params
にモデルとメトリックの両方を渡す。
optimizer = optim.SGD([{'params': model.parameters()}, {'params': metric.parameters()}], lr=args.lr, weight_decay=args.weight_decay)
5. 学習と検証
画像分類と同様に訓練を実施した結果、 100エポックでテスト精度が82.4%になった。
epoch: 100, train acc: 0.947966, train loss: 0.003992, test acc: 0.824237, test loss: 0.027159
ちなみにResNet18の最終層をコメントアウトしても512次元の特徴が取れるのだが、 このケースではテスト精度は84.7%になった。 CIFAR10ではResNet18でもDeepなのかも。
def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.layer1(out) out = self.layer2(out) out = self.layer3(out) out = self.layer4(out) out = F.avg_pool2d(out, 4) out = out.view(out.size(0), -1) #out = self.linear(out) return out
epoch: 100, train acc: 0.955092, train loss: 0.004396, test acc: 0.847445, test loss: 0.028565
所感
中身が完全に理解できていないのと、モデルとメトリックのパラメーター調整がもっと必要な気がしている。 時間があったら、もっと突っ込んでやりたい。
参考文献
ResNet18でCIAFR10を画像分類
CIFAR10の画像分類は PyTorchのチュートリアル に従ったらできるようになったのだが、 オリジナルモデルだったためResNet18に変更しようとしたら少しつまづいた。 再度つまづかないために、ここに実行手順をコード解説付きでまとめておく。 なお全コードは ここ に置いてある。
概要
実行手順は次の通り。
- データ取得
- モデル定義
- 損失関数と最適化関数の定義
- 学習と検証
これらはmain()
で次のように実行される。
def main(): # Parse arguments. args = parse_args() # Set device. device = 'cuda' if torch.cuda.is_available() else 'cpu' # Load dataset. train_loader, test_loader, class_names = cifar10.load_data(args.data_dir) # Set a model. model = get_model(args.model_name) model = model.to(device) # Set loss function and optimization function. criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=0.9, weight_decay=5e-4) # Train and test. for epoch in range(args.n_epoch): # Train and test a model. train_acc, train_loss = train(model, device, train_loader, criterion, optimizer) test_acc, test_loss = test(model, device, test_loader, criterion) # Output score. stdout_temp = 'epoch: {:>3}, train acc: {:<8}, train loss: {:<8}, test acc: {:<8}, test loss: {:<8}' print(stdout_temp.format(epoch+1, train_acc, train_loss, test_acc, test_loss)) # Save a model checkpoint. model_ckpt_path = args.model_ckpt_path_temp.format(args.dataset_name, args.model_name, epoch+1) torch.save(model.state_dict(), model_ckpt_path) print('Saved a model checkpoint at {}'.format(model_ckpt_path)) print('')
1. データ取得
CIFAR10を利用する。
今後の拡張性も考えて、データセット読み込み用のdatasets
ディレクトリーを作って、
CIFAR10関連のコードはcifar10.py
にまとめている。
from datasets import cifar10 def main(): ... # Load dataset. train_loader, test_loader, class_names = cifar10.load_data(args.data_dir)
cifar10.py
の中身は次の通り。
CIFAR10はtorchvision
があればOKなので実装は簡単。
trainとtestのDataLoaderとクラス名を返す。
import torchvision import torchvision.transforms as transforms def load_data(data_dir): transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) train_set = torchvision.datasets.CIFAR10(root=data_dir, train=True, download=True, transform=transform_train) train_loader = torch.utils.data.DataLoader(train_set, batch_size=128, shuffle=True, num_workers=0) test_set = torchvision.datasets.CIFAR10(root=data_dir, train=False, download=True, transform=transform_test) test_loader = torch.utils.data.DataLoader(test_set, batch_size=100, shuffle=False, num_workers=0) class_names = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') return train_loader, test_loader, class_names
2. モデル定義
Train CIFAR10 with PyTorchのmodels
に
ResNet以外にも様々なコードがあったのでディレクトリーごと拝借した。
from models import * def main(): ... # Set a model. model = get_model(args.model_name)
色々あるので引数にモデル名を入れれば取得できるようにした。
def get_model(model_name): if model_name == 'VGG19': model = VGG('VGG19') elif model_name == 'ResNet18': model = ResNet18() elif model_name == 'PreActResNet18': model = PreActResNet18() ... elif model_name == 'SENet18': model = SENet18() elif model_name == 'ShuffleNetV2': model = ShuffleNetV2(1) elif model_name == 'EfficientNetB0': model = EfficientNetB0() else: print('{} does NOT exist in repertory.'.format(model_name)) sys.exit(1)
3. 損失関数と最適化関数の定義
今回はオーソドックスにCross EntropyとSGDを各々セット。
def main(): ... # Set loss function and optimization function. criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=0.9, weight_decay=5e-4)
4. 学習と検証
これまでセットして変数をtrain()
に入力して訓練を開始する。
def main(): ... # Train and test. for epoch in range(args.n_epoch): # Train and test a model. train_acc, train_loss = train(model, device, train_loader, criterion, optimizer)
train()
では一般的な処理を踏む。
スコア算出のために、出力結果と正解のリスト、およびロスを貯める。
def train(model, device, train_loader, criterion, optimizer): model.train() output_list = [] target_list = [] running_loss = 0.0 for batch_idx, (inputs, targets) in enumerate(train_loader): # Forward processing. inputs, targets = inputs.to(device), targets.to(device) outputs = model(inputs) loss = criterion(outputs, targets) # Backward processing. optimizer.zero_grad() loss.backward() optimizer.step() # Set data to calculate score. output_list += [int(o.argmax()) for o in outputs] target_list += [int(t) for t in targets] running_loss += loss.item() # Calculate score at present. train_acc, train_loss = calc_score(output_list, target_list, running_loss, train_loader) if batch_idx % 10 == 0 and batch_idx != 0: stdout_temp = 'batch: {:>3}/{:<3}, train acc: {:<8}, train loss: {:<8}' print(stdout_temp.format(batch_idx, len(train_loader), train_acc, train_loss)) # Calculate score. train_acc, train_loss = calc_score(output_list, target_list, running_loss, train_loader) return train_acc, train_loss
スコア算出はcalc_score()
で行う。
精度の算出はscikit-learnのclassification_report()
を用いる。
def calc_score(output_list, target_list, running_loss, data_loader): # Calculate accuracy. result = classification_report(output_list, target_list, output_dict=True) acc = round(result['weighted avg']['f1-score'], 6) loss = round(running_loss / len(data_loader.dataset), 6) return acc, loss
検証用のメソッドtest()
も中身は学習する以外はだいたいtrain()
と同じ。
両メソッドから算出したスコアを取得して表示する。
def main(): ... # Train and test. for epoch in range(args.n_epoch): # Train and test a model. train_acc, train_loss = train(model, device, train_loader, criterion, optimizer) test_acc, test_loss = test(model, device, test_loader, criterion) # Output score. stdout_temp = 'epoch: {:>3}, train acc: {:<8}, train loss: {:<8}, test acc: {:<8}, test loss: {:<8}' print(stdout_temp.format(epoch+1, train_acc, train_loss, test_acc, test_loss))
以下が実行中の標準出力。 10バッチごとと1エポックごとに出力する。
$ python train.py Files already downloaded and verified Files already downloaded and verified batch: 10/391, train acc: 0.176831, train loss: 0.000512 batch: 20/391, train acc: 0.208884, train loss: 0.000931 batch: 30/391, train acc: 0.214069, train loss: 0.001356 ... batch: 390/391, train acc: 0.424302, train loss: 0.012254 epoch: 1, train acc: 0.424302, train loss: 0.012254, test acc: 0.539407, test loss: 0.012638 Saved a model checkpoint at ../experiments/models/checkpoints/CIFAR10_ResNet18_epoch=1.pth
ほぼtest()
と同じで、学習済みモデルを読み込んで評価を行うtest.py
も作成した。
モデルは10エポックで精度77.9%ほどとなることを確認。
$ python test.py Files already downloaded and verified Files already downloaded and verified Loaded a model from ../experiments/models/CIFAR10_ResNet18_epoch=10.pth test acc: 0.779172, test loss: 0.006796
参考文献
MNISTを画像検索
前回「MNISTで距離学習」 という記事を書いたが画像分類の域を出なかった。 距離学習と言えば画像検索なので、今回はそれをMNISTで行った。
概要
今回は前回 訓練したMNISTの距離学習モデルを利用して画像検索を行う。 手順は次の通り。
- データ準備
- モデルロード
- 特徴抽出
- 距離算出
これらについて順番に説明する。 なお、コードはここに置いてある。
1. データ準備
テスト時、画像分類ではテストデータを用意するが、 画像検索では検索画像となるQueryと検索対象画像群となるGalleryが必要になる。 そこでまずはテストデータをQueryとGalleryに分ける。 また後々のことを考えて、いったん画像に保存したものを読み込むようにする。
ここではメインファイルのimage_retrieval.py
でmake_query_and_gallery_from_mnist()
を呼び出す。
これにより、MNISTから1枚をQueryに100枚をGalleryにランダム選択して振り分ける。
各画像はQueryとGallery各々のディレクトリーに保存され、それらの情報はCSVファイルに記載される。
dataset_dir
はMNISTを保存するディレクトリー、
query_dir
はQuery画像を保存するディレクトリー、
gallery_dir
はGallery画像を保存するディレクトリー、
anno_path
はQueryとGalleryの画像情報を記載したCSVファイル
である。
make_query_and_gallery_from_mnist(args.dataset_dir, args.query_dir, args.gallery_dir, args.anno_path)
make_query_and_gallery_from_mnist()
では、
まずmake_query_and_gallery()
でMNISTの画像をQueryとGalleryに振り分けて、
次にmake_anno_file()
でQueryとGalleryの画像情報をCSVに保存する。
これらはMNISTデータ処理専用のmnist_data.py
で行う。
def make_query_and_gallery_from_mnist(dataset_dir, query_dir, gallery_dir, anno_path): mnist_data.make_query_and_gallery(dataset_dir, query_dir, gallery_dir) mnist_data.make_anno_file(query_dir, gallery_dir, anno_path)
以下からmnist_data.py
。
make_query_and_gallery()
は次の通り。
transformしたMNISTを取得して、Query画像1枚とGallery画像100枚をランダムに選択後、
各々のディレクトリーに画像として保存している。
保存前の画像はtransfromで正規化した後なので0~255になっていないが、
scipy.misc.imsave()
を使うと0~255にして保存してくれる。
def make_query_and_gallery(dataset_dir, query_dir, gallery_dir): # transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) testset = datasets.MNIST(dataset_dir, train=False, download=True, transform=transform) q_idx = random.choice(range(len(testset))) g_idxs= random.sample(range(len(testset)), 100) # Save query image. if os.path.exists(query_dir) == True: shutil.rmtree(query_dir) os.makedirs(query_dir) q_img, q_label = testset[q_idx] scipy.misc.imsave(query_dir+'{}_{}.png'.format(q_label, q_idx), q_img.numpy()[0]) # Save gallery images. if os.path.exists(gallery_dir) == True: shutil.rmtree(gallery_dir) os.makedirs(gallery_dir) for g_idx in g_idxs: g_img, g_label = testset[g_idx] scipy.misc.imsave(gallery_dir+'{}_{}.png'.format(g_label, g_idx), g_img.numpy()[0])
make_anno_file()
では、Query/Gallery、
画像名、画像パス、ラベル名、IDを記載したCSVファイルを作成して保存する。
def make_anno_file(query_dir, gallery_dir, anno_path): annos = [] annos += __set_annos(query_dir, 'query') annos += __set_annos(gallery_dir, 'gallery') df = pd.DataFrame(annos) df.to_csv(anno_path, index=False) def __set_annos(img_dir, data_type): annos = [] for d in os.listdir(img_dir): dic = {} dic['data_type'] = data_type dic['img_name'] = d dic['img_path'] = img_dir + d dic['label'] = d.split('_')[0] dic['id'] = d.split('.')[0].split('_')[1] annos.append(dic) return annos
最後に画像検索用のDataLoaderを作成する。 先ほど作成したQueryとGalleryのCSVファイルを用いて、 各々の画像ローダーが作成できる仕様にしている。 また、画像とラベルの他に画像パスも返すようにしている。
class ReIDDataset(Dataset): def __init__(self, anno_path, data_type, transform=None): df_all = pd.read_csv(anno_path) self.df = df_all[df_all['data_type']==data_type].reset_index(drop=True) # Filter data by query or gallery. self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): img_path = self.df.loc[idx, 'img_path'] assert os.path.exists(img_path) image = io.imread(img_path) label = self.df.loc[idx, 'label'] img_path = self.df.loc[idx, 'img_path'] if self.transform: image = self.transform(image) return image, label, img_path
画像分類の時にtrain_loader、test_loader、classesを返していたのと同様、 画像検索の時はquery_loader、gallery_loader、classesを返す。
def load_query_and_gallery(anno_path, img_show=False): transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # Query query_dataset = ReIDDataset(anno_path, 'query', transform) query_loader = DataLoader(query_dataset, batch_size=len(query_dataset), shuffle=False) # Gallery gallery_dataset = ReIDDataset(anno_path, 'gallery', transform) #gallery_loader = DataLoader(gallery_dataset, batch_size=len(gallery_dataset), shuffle=True) gallery_loader = DataLoader(gallery_dataset, batch_size=8, shuffle=True) # Class classes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # debug print('num query: {}, num gallery: {}'.format(len(query_dataset), len(gallery_dataset))) print('') if img_show == True: show_data(gallery_loader) return query_loader, gallery_loader, classes
2. モデルロード
再びメインファイルのimage_retrieval.py
。
モデルのロードは、model.load_state_dict(torch.load(args.model_path))
で行う。
学習済みモデルは、前回の記事を参考に生成する。
今回はテストなのでeval()
で評価モードにしておく。
# Set device, GPU or CPU. device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Model model = Net().to(device) model.load_state_dict(torch.load(args.model_path)) model.eval()
3. 特徴抽出
1. データ準備
で説明したquery_loader
を利用する。
と言っても画像は1枚だけなので、学習済みモデルに通して特徴を取得するだけ。
model
の出力は2次元特徴と予測結果なるが、今回は前者のみを利用する。
# Query for i, (query_img, query_label, query_path) in enumerate(query_loader): with torch.no_grad(): query_img = query_img.to(device) query_feat, pred = model(query_img)
Galleryは100枚画像があるが、
gallery_loader
のバッチサイズは8にしているので、
各情報をリストに格納して、最後にconcatinateする。
# Gallery gallery_feats = [] gallery_labels = [] gallery_paths = [] for i, (g_imgs, g_labels, g_paths) in enumerate(gallery_loader): with torch.no_grad(): g_imgs = g_imgs.to(device) g_feats_temp, preds_temp = model(g_imgs) gallery_feats.append(g_feats_temp) gallery_labels.append(g_labels) gallery_paths += list(g_paths) # Data type of g_paths is tuple. gallery_feats = torch.cat(gallery_feats, 0) gallery_labels = torch.cat(gallery_labels, 0)
4. 距離算出
3. 特徴抽出
でQueryの特徴query_feat
とGalleryの特徴gallery_feats
が取得できた。
query_feat
とgallery_feats
の各特徴との距離を算出するためにコサイン類似度を利用する。
dist_matrix = cosine_similarity(query_feat, gallery_feats)
コサイン類似の実装は次の通り。
def cosine_similarity(qf, gf): epsilon = 0.00001 dist_mat = qf.mm(gf.t()) qf_norm = torch.norm(qf, p=2, dim=1, keepdim=True) #mx1 gf_norm = torch.norm(gf, p=2, dim=1, keepdim=True) #nx1 qg_normdot = qf_norm.mm(gf_norm.t()) dist_mat = dist_mat.mul(1/qg_normdot).cpu().numpy() dist_mat = np.clip(dist_mat, -1+epsilon,1-epsilon) dist_mat = np.arccos(dist_mat) return dist_mat
QueryとGalleryの各距離、Galleryのラベル名と画像パスをセットにした DataFrameを作成して、距離でソートする。 これにより、距離の近い順にラベルが表示される。
# Organize ReID ranking. lis = [] for i in range(len(gallery_paths)): dic = {} dic['dist'] = dist_matrix.tolist()[0][i] dic['label'] = np.array(gallery_labels).tolist()[i] dic['img_path'] = gallery_paths[i] lis.append(dic) df = pd.DataFrame(lis) df = df.sort_values(by=['dist'], ascending=True) df = df.reset_index(drop=True)
以下は実行結果。 Queryが9で、Galleryも9がindexの0から8まで占めている。
$ python image_retrieval.py num query: 1, num gallery: 100 Query Image Label: 9 Search Result dist img_path label 0 0.005382 ../inputs/gallery/9_1801.png 9 1 0.018921 ../inputs/gallery/9_4237.png 9 2 0.036690 ../inputs/gallery/9_481.png 9 3 0.047976 ../inputs/gallery/9_7380.png 9 4 0.069177 ../inputs/gallery/9_8213.png 9 5 0.076138 ../inputs/gallery/9_3970.png 9 6 0.078646 ../inputs/gallery/9_2685.png 9 7 0.107746 ../inputs/gallery/9_5977.png 9 8 0.387746 ../inputs/gallery/9_4505.png 9 9 0.523175 ../inputs/gallery/3_8981.png 3 10 0.538863 ../inputs/gallery/3_927.png 3 11 0.560314 ../inputs/gallery/3_142.png 3 12 0.565455 ../inputs/gallery/3_8451.png 3 13 0.582634 ../inputs/gallery/3_4755.png 3 14 0.586750 ../inputs/gallery/3_2174.png 3 15 0.589938 ../inputs/gallery/3_9986.png 3 16 0.675965 ../inputs/gallery/1_4491.png 1 17 0.682165 ../inputs/gallery/3_8508.png 3 18 0.683414 ../inputs/gallery/3_4785.png 3 19 0.698637 ../inputs/gallery/1_1038.png 1
labelをカウントすると、ラベルが9の画像は9枚あることが分かる。 よって、Galleryにあるラベル9の全画像を検索上位に持ってくることができたことが分かる。
1 15 7 13 0 12 8 11 4 10 3 10 9 9 # <- this 6 7 2 7 5 6
参考文献
MNISTを距離学習
Person ReIDが必要になったので、まずはMNISTを題材に距離学習を勉強している。 あと、これまでKerasを使ってきたけど、PyTorch使えないと厳しい世の中になってきたので、 PyTorchについて色々調べつつ実装してみた。
なお今回はこちらの記事(以下、参照記事)を参考にしている。 距離学習をメインで学びたい人は本記事より参照記事を読むことをお薦めする。 本記事はPyTorch入門みたいな要素が強いので。
概要
距離学習をもの凄く簡単に言うと画像分類の拡張。 なので、処理フローはだいたい画像分類と同じで以下のようになる。
- データ準備
- モデル定義
- 損失関数定義
- 最適化関数定義
- 訓練検証
距離学習は、同じクラスは近く異なるクラスは遠くなるようにモデルを学習することで、 未知のクラスの同定を行えるのが画像分類と違うところ。 ポイントは損失関数で、今回はCenterLossというのを使っているが、 説明は参照記事が詳しい。
本記事で説明するコードはここにある。
以下のtrain_mnist_original_center.py
のmain()
を実行すると、
参照記事と同じような結果が得られるが、個人的にコード整理してみたので、
上述の処理フローに従って順に説明する。
def main(): # Arguments args = parse_args() # Dataset train_loader, test_loader, classes = mnist_loader.load_dataset(args.dataset_dir, img_show=True) # Device device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Model model = Net().to(device) print(model) # Loss nllloss = nn.NLLLoss().to(device) # CrossEntropyLoss = log_softmax + NLLLoss loss_weight = 1 centerloss = CenterLoss(10, 2).to(device) # Optimizer dnn_optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=0.0005) sheduler = lr_scheduler.StepLR(dnn_optimizer, 20, gamma=0.8) center_optimizer = optim.SGD(centerloss.parameters(), lr =0.5) print('Start training...') for epoch in range(100): # Update parameters. epoch += 1 sheduler.step() # Train and test a model. train_acc, train_loss, feat, labels = train(device, train_loader, model, nllloss, loss_weight, centerloss, dnn_optimizer, center_optimizer) test_acc, test_loss = test(device, test_loader, model, nllloss, loss_weight, centerloss) stdout_temp = 'Epoch: {:>3}, train acc: {:<8}, train loss: {:<8}, test acc: {:<8}, test loss: {:<8}' print(stdout_temp.format(epoch, train_acc, train_loss, test_acc, test_loss)) # Visualize features of each class. vis_img_path = args.vis_img_path_temp.format(str(epoch).zfill(3)) visualize(feat.data.cpu().numpy(), labels.data.cpu().numpy(), epoch, vis_img_path) # Save a trained model. model_path = args.model_path_temp.format(str(epoch).zfill(3)) torch.save(model.state_dict(), model_path)
1. データ準備
先に引数の説明を少し。
# Arguments
args = parse_args()
dataset_dir
はMNISTデータの保存場所。
後述するPyTorchの機能でここにダウンロードしてくれる。
model_path_temp
は学習済みモデルのチェックポイント。
各エポック終了後に保存する。
vis_img_path_temp
はMNISTの各クラスの特徴分布を可視化した画像。
こちらも各エポック終了後に保存する。
だんだんとクラス内でまとまりクラス間が離れていく様子が確認できる。
下図は100エポック後の特徴分布。
def parse_args(): arg_parser = argparse.ArgumentParser(description="parser for focus one") arg_parser.add_argument("--dataset_dir", type=str, default='../inputs/') arg_parser.add_argument("--model_path_temp", type=str, default='../outputs/models/checkpoints/mnist_original_softmax_center_epoch_{}.pth') arg_parser.add_argument("--vis_img_path_temp", type=str, default='../outputs/visual/epoch_{}.png') args = arg_parser.parse_args() return args
では、MNISTのデータセットを取得する。
MNIST関連は、mnist_loader.py
という別ファイルを作って処理している。
# Dataset train_loader, test_loader, classes = mnist_loader.load_dataset(args.dataset_dir, img_show=True)
load_dataset
は、train_loader、test_loader、クラス名を取得するメソッド。
ここからPyTorch色が強くなるが、データ準備では次の手順を踏む。
1. 画像の前処理
torchvision
のtransform
を利用する。
ToTensor()
でPyTorchのtorch.Tensor
型に変換する。
他にも、クロップやフリップなどData Augmentation的な事を行えるが、今回は未実施。
今回はNormalize()
で正規化を行っている。
なおMNISTは自然画像ではないので、平均0.1307、標準偏差0.3081となるようにする。
from torchvision import transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])
2. 画像データセットを取得
torchvision
のdatasets.MNIST
を使うとMNISTが簡単に利用できる。
第1引数はMNISTデータの保存場所。
第2引数でtrain用かtest用かを選ぶ。
第3引数がTrueの場合は保存場所にMNISTデータがない場合に自動でダウンロードしてくれる。
第4引数で先に定義したtransformをセットする。
from torchvision import datasets trainset = datasets.MNIST(dataset_dir, train=True, download=True, transform=transform)
3. データローダーを定義
torch.utils.data
のDataLoader
を利用して、指定バッチ数分のデータを取得する。
第1引数は2で定義したデータセット。
第2引数はバッチサイズ。
第3引数はデータシャッフルするか否か。訓練時はTrueが妥当。
第4引数はデータロードの並列処理数。
from torch.utils.data import DataLoader train_loader = DataLoader(trainset, batch_size=train_batch_size, shuffle=True, num_workers=0)
上記のメソッドを組み合わせることで、mnist_loader.load_dataset()
は次のようになる。
def load_dataset(dataset_dir, train_batch_size=128, test_batch_size=128, img_show=False): # Dataset transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) trainset = datasets.MNIST(dataset_dir, train=True, download=True, transform=transform) train_loader = DataLoader(trainset, batch_size=train_batch_size, shuffle=True, num_workers=0) testset = datasets.MNIST(dataset_dir, train=False, download=True, transform=transform) test_loader = DataLoader(testset, batch_size=test_batch_size, shuffle=False, num_workers=0) classes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] if img_show == True: show_data(train_loader) return train_loader, test_loader, classes
show_data()
はMNISTを可視化するメソッド。
torchvision.utils.make_grid()
により train_loader のバッチを簡単に可視化できる。
def show_data(data_loader): images, labels = iter(data_loader).next() # data_loader のミニバッチの image を取得 img = torchvision.utils.make_grid(images, nrow=16, padding=1) # nrom*nrom のタイル形状の画像を作る plt.ion() plt.imshow(np.transpose(img.numpy(), (1, 2, 0))) # 画像を matplotlib 用に変換 plt.draw() plt.pause(3) # Display an image for three seconds. plt.close()
2. モデル定義
PyTorchでは処理をGPUとCPUのどちらで行うかtorch.device
で明示的に選択して、
それをモデルやデータにセットする必要がある。
モデル定義はMNIST向けのをmnist_net.py
のNet()
で別途定義している。
# Device device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Model model = Net().to(device) print(model)
mnist_net.py
のNet()
は次の通り。
Define by Runでは、
__init__()
で計算グラフを幾つか定義して、ネットワーク生成時に1度だけ呼びし、
データ入力時にforward()
を呼び出す使用となっている。
6つの畳み込み層とPReLUの後、2次元空間に落とし込んだ特徴ip1と、
それをPReLUに通して10次元空間に写像したip2を出力する。
ip1が特徴分布で、ip2は画像分類に利用する。
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1_1 = nn.Conv2d(1, 32, kernel_size=5, padding=2) self.prelu1_1 = nn.PReLU() self.conv1_2 = nn.Conv2d(32, 32, kernel_size=5, padding=2) self.prelu1_2 = nn.PReLU() self.conv2_1 = nn.Conv2d(32, 64, kernel_size=5, padding=2) self.prelu2_1 = nn.PReLU() self.conv2_2 = nn.Conv2d(64, 64, kernel_size=5, padding=2) self.prelu2_2 = nn.PReLU() self.conv3_1 = nn.Conv2d(64, 128, kernel_size=5, padding=2) self.prelu3_1 = nn.PReLU() self.conv3_2 = nn.Conv2d(128, 128, kernel_size=5, padding=2) self.prelu3_2 = nn.PReLU() self.ip1 = nn.Linear(128*3*3, 2) self.preluip1 = nn.PReLU() self.ip2 = nn.Linear(2, 10, bias=False) def forward(self, x): x = self.prelu1_1(self.conv1_1(x)) x = self.prelu1_2(self.conv1_2(x)) x = F.max_pool2d(x,2) x = self.prelu2_1(self.conv2_1(x)) x = self.prelu2_2(self.conv2_2(x)) x = F.max_pool2d(x,2) x = self.prelu3_1(self.conv3_1(x)) x = self.prelu3_2(self.conv3_2(x)) x = F.max_pool2d(x,2) x = x.view(-1, 128*3*3) ip1 = self.preluip1(self.ip1(x)) ip2 = self.ip2(ip1) return ip1, F.log_softmax(ip2, dim=1)
なおnn.PReLU
はReLUの改良の改良。
LeakyReLU で x < 0
の時に y < 0
することで学習が進みやすくなったものの、
パラメーター α が増えたため、それを減らすべく学習させることにしたのがPReLU。
ちなみにPReLUは、"a Parametric Rectified Linear Unit" の略。
まとめると以下のようになる。
ReLU y = x (0 =< x) y = 0 (x < 0) LeakyReLU y = x (0 =< x) y = αx (x < 0), set α as a parameter PReLU y = x (0 =< x) y = αx (x < 0), learning α
またview
はnumyp.reshape
と同じ。
第一引数が-1のとき、第二引数の形に自動調整してくれる。
上記の場合だと、x の shape が (3, 3, 128) になるので、
1次元に変換している。
3. 損失関数定義
損失関数は、画像分類用のNLL Loss
にMetric Learninig用のCenter Loss
を加重加算したものを利用する。
Loss = NLL Loss + α * Center Loss, α is weight
NLL Loss
はNegative Log-Likelihood (NLL) Lossの略。
softmaxの最大値は結果の確信度を表すが、それをマイナスの対数で取った値となる。
NLL Lossにより、高い確信度であれば低いロス、低い確信度であれば高いロスを割り当てることができる。
ip2のsoftmax(定義したモデルの出力)を入力とする。
一方Center Loss
は特徴の中心の損失関数。ip1を入力する。
詳しい説明は参照記事に任せる。
ちなみに、自分は距離学習に
ArcFaceから入ったので、
Center Loss
はこの記事以外では使わないかな、と思っている。
PyTorchで、損失関数は次のように定義される。
CenterLoss()
は自作関数でクラス数と特徴数が引数となる。
# NLL Loss & Center Loss nllloss = nn.NLLLoss().to(device) # CrossEntropyLoss = log_softmax + NLLLoss loss_weight = 1 # weight centerloss = CenterLoss(10, 2).to(device) # Loss loss = nllloss(pred, labels) + loss_weight * centerloss(labels, ip1)
4. 最適化関数定義
最適化関数にはSGDを利用するが、画像分類と距離特徴の両方を行っているので、
それぞれで最適化関数を定義する。
前者については、学習率の減衰をlr_scheduler.StepLR()
で行う。
第一引数は画像分類用の最適化関数、第二引数は学習率を更新するタイミングのエポック数、
第三引数は学習率の更新率。
# Optimizer dnn_optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=0.0005) center_optimizer = optim.SGD(centerloss.parameters(), lr =0.5) import torch.optim.lr_scheduler as lr_scheduler sheduler = lr_scheduler.StepLR(dnn_optimizer, 20, gamma=0.8)
5. 訓練検証
これまで定義してきた変数と関数を利用して訓練を行う。
エポックごとにtrain()
を呼び出す。
train_acc, train_loss, feat, labels = train(device, train_loader, model, nllloss, loss_weight, centerloss, dnn_optimizer, center_optimizer)
train()
は一般的な機械学習。
def train(device, train_loader, model, nllloss, loss_weight, centerloss, dnn_optimizer, center_optimizer): running_loss = 0.0 pred_list = [] label_list = [] ip1_loader = [] idx_loader = [] model.train() for i,(imgs, labels) in enumerate(train_loader): # Set batch data. imgs, labels = imgs.to(device), labels.to(device) # Predict labels. ip1, pred = model(imgs) # Calculate loss. loss = nllloss(pred, labels) + loss_weight * centerloss(labels, ip1) # Initilize gradient. dnn_optimizer.zero_grad() center_optimizer.zero_grad() # Calculate gradient. loss.backward() # Update parameters. dnn_optimizer.step() center_optimizer.step() # For calculation. running_loss += loss.item() pred_list += [int(p.argmax()) for p in pred] label_list += [int(l) for l in labels] # For visualization. ip1_loader.append(ip1) idx_loader.append((labels)) # Calculate training accurary and loss. result = classification_report(pred_list, label_list, output_dict=True) train_acc = round(result['weighted avg']['f1-score'], 6) train_loss = round(running_loss / len(train_loader.dataset), 6) # Concatinate features and labels. feat = torch.cat(ip1_loader, 0) labels = torch.cat(idx_loader, 0) return train_acc, train_loss, feat, labels
PyTorchでは訓練中、lossとoptimizerはバッチごとに次の手順を踏んで、パラメーターを更新していく。
optimizer.zero_grad() # 勾配の初期化 loss.backward() # 勾配の計算 optimizer.step() # パラメータの更新
またsklearn.metrics
のclassification_report()
を利用すると、
簡単に精度が算出できる。
今回のように距離学習の損失関数を入れても、検証精度は100エポックで98.2%になっている。
訓練と検証の精度と損失の変化は以下の通り。
Epoch: 1, train acc: 0.209305, train loss: 0.019642, test acc: 0.253963, test loss: 0.018308 Epoch: 2, train acc: 0.302789, train loss: 0.017461, test acc: 0.418725, test loss: 0.016906 Epoch: 3, train acc: 0.455266, train loss: 0.015967, test acc: 0.492158, test loss: 0.015241 Epoch: 4, train acc: 0.531249, train loss: 0.014266, test acc: 0.526375, test loss: 0.013594 Epoch: 5, train acc: 0.609915, train loss: 0.012737, test acc: 0.629488, test loss: 0.012123 ... Epoch: 96, train acc: 1.0 , train loss: 0.000309, test acc: 0.982005, test loss: 0.003887 Epoch: 97, train acc: 0.999983, train loss: 0.000307, test acc: 0.981601, test loss: 0.003929 Epoch: 98, train acc: 1.0 , train loss: 0.000303, test acc: 0.981306, test loss: 0.003924 Epoch: 99, train acc: 1.0 , train loss: 0.000296, test acc: 0.981907, test loss: 0.003937 Epoch: 100, train acc: 1.0 , train loss: 0.000272, test acc: 0.981805, test loss: 0.003965
参考文献
- 【深層距離学習】Center Lossを徹底解説 - はやぶさの技術ノート
- PyTorch まずMLPを使ってみる - cedro-blog
- Normalization in the mnist example - PyTorch Forums
- ChainerのDefine by Runとは? - HELLO CYBERNETICS
- LeakyRelu活性化関数 - Thoth Children
- PRelu活性化関数 - Thoth Children
- Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification - arXiv
- Understanding softmax and the negative log-likelihood - Lj Miranda
- 実践Pytorch - Qiita
- PyTorchのSchedulerまとめ - 情弱大学生の独り言
【論文解説】SinGAN: Learning a Generative Model from a Single Natural Image
1枚の画像に対して様々な操作が可能な SinGAN の論文を簡単にまとめた。 SinGANはSingle GANの略。 個人的には、ICCV 2019でbest paperだったのと、GANだけど必要な訓練データが1枚なのでリソースが少なくて嬉しいのと、実写画像でも調和的な編集や画像合成ができる、というのが気になって読んでみた。 メタ情報は次の通り。
- Title: SinGAN: Learning a Generative Model from a Single Natural Image
- Authors: Tamar Rott Shaham, Tali Dekel, Tomer Michaeli
- Conference: ICCV 2019
1. 概要
1枚の画像に対して様々な操作を実現する手法。 操作の種類は、Paint to Image、Editing、Harmonization、Super Reslution、Single Image Annimation の5つ。 画像をスケールアップさせるGANでピラミッドを構成し、粗い画像から段階的に精巧な画像を生成するように訓練する。 テスト時は入力画像とスケールを調整することで、様々な画像の操作を実現する。
2. 先行研究との差異
従来のGANは特定の操作のためにデザインされているが、本手法は多くの異なる画像操作が可能である。 SinGANと同様、単一の画像を扱うGANはあったが条件付き(特定画像から別画像への変換)であった。 そのため、本手法では条件なし(ノイズから画像への変換)を可能にしている。 また、従来のGANでは実写画像が扱えなかった点も本手法で克服している。
3. 手法
従来のGANと異なりSinGAN では、画像セットではなく1つの画像からパッチセットを作成して訓練に利用する。 またGANのピラミッドにしたモデルにより、複数の異なるスケールの画像から複雑な構造を獲得する。
モデル構造の概要は上図の通り。 スケール で、生成器 は を生成する。 このとき は、識別器 が訓練画像をスケールダウンした画像 と区別できないように を生成する。 また と を区別する際に比較するのは、画像全体ではなく両者で重なり会うパッチごとである。 パッチサイズは画像がスケールアップされるにつれて小さくなる(右端の黄色矩形)。 スケール からスケール になる際、 をスケールアップしてノイズと共に に入力し、 を生成する。 ただし の場合のみ、 は ノイズ から生成する。
の具体的な処理は上図のようになり数式(3)で表される。
は Conv(3x3)-BatchNorm-LeakyReLu
による畳み込みブロックを5つ連結している。
Lossは式(4)のようになる。
前半はいわゆるGANの生成器と識別器の関係。 後半は式(5)で示され、スケールアップした画像から生成した画像と、スケールダウンした訓練画像のRMSE(root mean squared error)となる。
の時はノイズを利用しないが、 の時はノイズを利用するため次の式になる。
4. 評価
4.1~4.3でSinGAN自体の能力が分かるランダム画像生成、4.4でEditingとHarmonizationの画像操作の評価について見ていく。
4.1. 異なるスケールから生成した画像の違い
元画像をスケールダウンして 粗めのスケール から入力することで、元画像に似た画像が生成される。 入力するスケール に応じて、異なる画像が生成される。 下図では、最も粗い と、それに続く と から入力した例を示している。 nが小さくなるにつれて、変化が小さくなっている。 では大枠が捉えているが違和感があり、 では微妙な変化となり、 では拡大しないと分からないほどの変化となる。 また、生成画像をパッチごとに識別機にかけているので、反射や影の出力が自然に実現できる。 維持したいコンテキストによって入力するスケールを変えるとよい。
4.2. 生成画像の定性評価
スケール と 、元画像と生成画像のpairedと生成画像のunpairedで、リアルかフェイクかを人が見分ける実験を実施。 50%が完全に混乱するという意味。 と では後者、paired と unpaired でも後者の方が、生成画像をリアルと判断する傾向があった。 先の画像のように、 のコンテンツはほぼ完全に維持されるため変化する箇所が微妙であり、unpairedは元画像がないので素直な結果と言える。
4.3. 生成画像の定量評価
GANの生成画像の評価指標に、FID(Frechet Inception Distance) がある。 FIDは画像間の距離を測ることができるが、複数画像のペアが対象となる。 そこで本論文では、元画像と生成画像が1枚ずつのペアでもFIDが利用できるSIFID(Single Image FID)を提案して評価を行っている。 SIFIDは が 0.09 、 が 0.05 (低い方がよい)となり、定性評価と同じく の方が元画像に近いことが分かる(左から2番目の列)。 また、SIFIDと定性評価との相関があることも示されている(右端の列)。
4.4 画像操作:Editing, Harmonization
色々な操作ができるが、個人的な興味からEditingとHarmonizationのみに絞って見ていく。
Editing
編集して縮小した画像を入力する。 PhotoshopのContent Aware MoveよりもSinGANの方が、つなぎ目がシームレスになっている。 粗い画像を拡大しながら精巧にしていく過程を考えると出力結果に納得できる。
Harmonization
縮小した合成画像を入力する。 Deep Painterly Harmonization(DPH)よりも、前景のオリジナリティが保持されている。 スケール数 を2~4 にすると、前景の構造を保ちつつ背景のスタイルに上手く変換できるらしい (なんとなくDPHもパラメーター調整でいい感じになる気がするが)。
5. 所感
Harmonizationは、Deep Painterly Harmonizationでは実写画像の扱いが難しかったのと、学習しないからか生成画像が粗かったので、もしホントに実写で上手くいったらSinGANの発明者の皆さんに感謝したい気持ちでいっぱいです。 あと、SinGANは本当に色々できるので、そのぶん応用範囲も広いと思う。 まず、HarmonizationができるならStyle Transferもできるし、Editingも一種のStyle Transferみたいな感じがしたで、そっち方面で新たな研究成果が期待できそう。
TensorFlow GPU版をWindows 10にインストール
Window 10にTensorFlowをインストールして使っていたのだが、PyTorchで遊ぶ環境を作っていたら、 (たぶんPython 3.6にしたことが原因だけど)いつの間にかTensorFlowが動かなくなっていたので、再インストールした。 ここに、その手順を備忘録として残しておく。
なお、環境は次の通り。
- OS: Windows 10 Pro 64bit
- GPU: NVIDIA GeForce GTX 1080
- CUDA: 9.0
- cuDNN: 7.0.5
1. Anaconda3-4.4.0 をインストール
Anacondaのアーカイブ にアクセスして、Anaconda3-4.4.0-Windows-x86_64.exe
をダウンロード&インストール。
2. Python3.5 をインストール
Anaconda3-4.4.0により、Python 3.6がインストールされるが、Window 10では、Python 3.6とTensorFlowの相性が良くないらしい。
そのため、Python 3.5
をcondaでインストール。
> conda install python==3.5
3. TensorFlow 1.5をインストール
Build from source on Windowsによると、
CUDA 9とcuDNN 7の組合せの場合、TensorFlow GPU版はv1.5~1.12が使えるとある。
こちらを参考にCUDAとcuDNNのバージョンを確認したところ、
CUDA 9.0
とcuDNN 7.0.5
だった。この組合せの場合は、tensorflow_gpu-1.5.0
が無難そうである。
なので、pip
でtensorflow_gpu-1.5.0をインストールする。
なお、condaを使うと必ずエラーになるという罠に何度もかかったので注意。pipを使う。
> pip install tensorflow-gpu==1.5
しかし、以下のようなエラーが出る場合がある。
... AttributeError: '_NamespacePath' object has no attribute 'sort'
これはpipがエラーの原因なので、easy_install
でpip
をインストールし直す。
pipが壊れたのは、condaでPython 3.5にしたのが原因と思われる。やはりconda、良さそうに見えて罠が多い。
> easy_install pip
これでpipが使えるようになったので、再度pipでtensorflow_gpu-1.5.0をインストールする。
> pip install tensorflow-gpu==1.5
次のようにDEPRECATIONが出るが、とりあえずOK。
... `DEPRECATION: Uninstalling a distutils installed project (html5lib) has been deprecated and will be removed in a future version. This is due to the fact that uninstalling a distutils project will only partially uninstall the project.` ...
4. Numpy 1.14をインストール
Pythonを起動して、TensorFlow
をimport
すると、またしてもエラーが出る。
>>> import tensorflow as tf ... ImportError: cannot import name 'multiarray'
これはnumpyのエラーなのだが、こちらのIssueによると、
TensorFlow 1.6以上は、numpy 1.13.3以上である必要がある、とのこと。
TensorFlow 1.5だが、とりあえずconda
でnumpy 1.14
を入れてみる。
> conda install numpy==1.14
これでOKのはず。
5. TensorFlowの動作確認
最終的には以下のように、これまでインストールしてきたモノとバージョンが確認できればOK。
> python Python 3.5.0 |Anaconda 4.4.0 (64-bit)| (default, Dec 1 2015, 11:46:22) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import tensorflow as tf >>> tf.__version__ '1.5.0' >>>
参考文献
- Anaconda installer archive
- Windows10にtensorflowを入れるための備忘録 - @zyaxan
- Build from source on Windows
- 環境構築したCUDA及びcuDNNのバージョンを確認する方法(Windows) - 技術的特異点
- pip3 error - '_NamespacePath' object has no attribute 'sort' - stackoverflow
- Tensorflow 1.5.0 manylinux binary requires Numpy 1.14.1 - tensorflow/tensorflow