PyTorchのTensorはどうやってデータを持っているのか?

お仕事でPyTorchを扱っているのですが、以下のような疑問がふつふつと湧いてきましたので、Tensorのデータが実際にはどうやって保持・管理されているのかを調べて整理しました。

  • image_tensor = minibatch_tensor[i, :, :, :]はメモリコピーが発生するのか?
  • input_tensorをコピーして別々のネットワークにフォワードしたいのだけどどうすればいいのか?
  • 効率的にメモリアクセスできるレイアウトになっているのか?

この投稿ではPyTorch 1.1.0を使ってます。

import torch
import numpy as np

print(torch.__version__)
# 1.1.0

Tensorでのデータの持ち方

Tensorはnumpyのビューとよく似ています。

メモリ上の実体はStorageオブジェクトが持つ

Tensorのデータはメモリ上では全て1次元配列として保持されており、その実体を管理しているのがStorageオブジェクトです

Tensorからstorageメソッドを呼び出すことで、Storageオブジェクトが取得できます。ここではFloatStorageがオブジェクトが返されてますが、型ごとに定義されてます。
Tensorを更新すれば、Storageも更新されます。

a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
a_storage = a.storage()
print(a_storage)
#  1.0
#  2.0
#  3.0
#  4.0
#  5.0
#  6.0
# [torch.FloatStorage of size 6]

a[1, 1] = 10

print(a)
# tensor([[ 1.,  2.,  3.],
#         [ 4., 10.,  6.]])

print(a_storage)
#  1.0
#  2.0
#  3.0
#  4.0
#  10.0
#  6.0
# [torch.FloatStorage of size 6]

オフセットとストライド

TensorはStorageに対するビューの役割を果たしているのですが、TensorからStorage (1次元配列) へマッピングするために、Tensorではオフセットとストライドを持ってます。それぞれstorage_offsetメソッドとstrideメソッドで取得できます。

上のaの場合、オフセットは0、ストライドは(3, 1)となります。
これは0 + 次数0のインデックス*3 + 次数1のインデックス*1 = Storage (1次元配列) 上のインデックスでマッピングされます。

print(a.storage_offset(), a.stride())
# 0 (3, 1)

Tensorにインデックスを使ってアクセスすると、Storageは同じでオフセット・ストライドのみが異なるTensorオブジェクトが生成されます。つまりメモリコピーが発生しません。

以下のbcはいずれもaから生成されたTensorオブジェクトですが、Storageオブジェクトは全て同じです。aの値を更新すると、bcの値も変更されていることが確認できます。

  • transposeは転置を行う関数で、次数0と1が入れ替わっているので、ストライドも(3, 1) -> (1, 3) となってます
b = a[1:]

print(b)
# tensor([[ 4., 10.,  6.]])

print(b.storage_offset(), b.stride())
# 3 (3, 1)

c = a.transpose(0, 1)

print(c)
# tensor([[ 1.,  4.],
#         [ 2., 10.],
#         [ 3.,  6.]])

print(c.storage_offset(), c.stride())
# 0 (1, 3)

a[1, 2] = 11
print(a)
# tensor([[ 1.,  2.,  3.],
#         [ 4., 10., 11.]])

print(b)
# tensor([[ 4., 10., 11.]])

print(c)
# tensor([[ 1.,  4.],
#         [ 2., 10.],
#         [ 3., 11.]])

contiguous

オフセットとストライドによってマッピングされますので、メモリレイアウトは変更されません。

TensorからStorageへのマッピングが、メモリの連続した領域へのアクセスとなっているかどうかをチェックするメソッドとしてis_contiguousが用意されています。

  • 連続している方がCPU / GPUのキャッシュが効きやすいため、場合によってはこういったチェックが必要になります

上の例ではabは連続 (返り値True) してますが、cはメモリ上では0番目 -> 3番目 -> 1番目 -> ... という配置になってるため非連続 (返り値False) となってます。

print(a.is_contiguous(), b.is_contiguous(), c.is_contiguous())
# True True False

連続した領域になるように再配置するには、contiguousメソッドを使います。これはメモリコピーが行われることに注意です。

c_cont = c.contiguous()

print(c_cont.is_contiguous())
# True

print(c_cont.storage())
#  1.0
#  4.0
#  2.0
#  10.0
#  3.0
#  6.0
# [torch.FloatStorage of size 6]

メモリコピーが発生する場合

上のcontiguous以外でメモリコピーが発生するケースを見ていきます。

明示的にメモリコピーを行う (= Storageを生成する) 場合、cloneメソッドを使います。

a_clone = a.clone()
a_clone[0, 0] = 100

print(a)
# tensor([[ 1.,  2.,  3.],
#         [ 4., 10., 11.]])

boolean indexingの場合は、オフセットとストライドでマッピングできないので、暗黙的にコピーが作られます。このあたりもnumpyと同じですね。

a_filterd = a[a >= 10]
print(a_filterd)
# tensor([10., 11.])

a_filterd[0] = 100
print(a)
# tensor([[ 1.,  2.,  3.],
#         [ 4., 10., 11.]])

値そのものの変更や元のStorageのサイズを変更が発生するメソッドでもコピーが発生します。

  • 上で見たtransposeやviewなどはオフセットやストライドの変更のみで済むため、コピーは発生しません
  • 末尾"_"のTensorメソッドはStorage内のメモリを上書きするため (in-place) 、これもコピーは発生しません
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])

c = torch.pow(a, 2)
print(c.storage())
#  1
#  4
#  9
# [torch.LongStorage of size 3]

c_ = c.pow_(2)
print(c_.storage())
#  1
#  16
#  81
# [torch.LongStorage of size 3]

print(c.storage())
#  1
#  16
#  81
# [torch.LongStorage of size 3]

d = torch.cat((a, b))
print(d.storage())
#  1
#  2
#  3
#  4
#  5
#  6
# [torch.LongStorage of size 6]

もちろんGPUに送るとメモリコピーになります。

  • storageメソッドでアクセスするとtorch.cuda.FloatStorageオブジェクトが返されており、CPUとは別のクラスになっていることがわかります
a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)

a_gpu = a.to(device="cuda")

print(a_gpu)
# tensor([[1., 2., 3.],
#         [4., 5., 6.]], device='cuda:0')

print(a_gpu.storage())
#  1.0
#  2.0
#  3.0
#  4.0
#  5.0
#  6.0
# [torch.cuda.FloatStorage of size 6]

まとめ

PyTorchのTensorのデータの持ち方について調べました。

  • 実体はStorageオブジェクトが持っている
  • Tensorはオフセットとストライドを持っており、Storageオブジェクトとマッピングされている
  • clone、contiguous、gpuメソッドや値やサイズの変更を伴うメソッドなどでメモリコピーが発生する

参考

Deep Learning with PyTorch | PyTorch

  • 2章を主に参考にしました

PyTorchでパラメータ数をカウントする

PyTorchのモデルのパラメータ数をカウントする方法です。2パターンあります。

1. Moduleのparametersメソッドを合計する

Module.parametersメソッドで各層のパラメータがtensorで取得できますので、numelで要素数を合計していくことでパラメータ数を計算できます。

  • requires_gradがTrueのパラメータが学習可能です
import torch
import torch.nn as nn

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        self.conv1 = self._conv(3, 16)
        self.conv2 = self._conv(16, 32)
        self.conv3 = self._conv(32, 64)
        self.pool = nn.AvgPool2d(4)
        self.dropout = nn.Dropout(0.4)
        self.fc = nn.Linear(64, 10)
        
    def forward(self, x):
        x = self.conv1(x)  # (3, 32, 32) -> (16, 16, 16)
        x = self.conv2(x)  # (16, 16, 16) -> (32, 8, 8)
        x = self.conv3(x)  # (32, 8, 8) -> (64, 4, 4)
        x = self.pool(x)
        
        # (64, 1, 1) -> (64) -> (10)
        x = x.view(-1, 64)
        x = self.dropout(x)
        x = self.fc(x)
        
        return x
    
    def _conv(self, input_filters, output_filters):
        return nn.Sequential(
            nn.Conv2d(input_filters, output_filters, 3, stride=1, padding=1),
            nn.BatchNorm2d(output_filters),
            nn.ReLU(),
            nn.Conv2d(output_filters, output_filters, 3, stride=1, padding=1),
            nn.BatchNorm2d(output_filters),
            nn.ReLU(),
            nn.Conv2d(output_filters, output_filters, 3, stride=2, padding=1),
            nn.BatchNorm2d(output_filters),
            nn.ReLU(),
            nn.Dropout2d(0.1),
        )

net = Net()

# パラメータカウント
params = 0
for p in net.parameters():
    if p.requires_grad:
        params += p.numel()
        
print(params)  # 121898

2. pytorch-summaryを使う

Kerasライクなモデルのサマライズを行うパッケージ pytorch-summary を使うことでもパラメータ数を取得できます。
pip install pytorch-summaryで事前にインストールします。

github.com

あとは以下のようにsummaryメソッドにモデルと入力サイズを入れると、モデルのパラメータを計算してくれます。

  • Total paramsが上で求めた値と一致することを確認ください
from torchsummary import summary

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net.to(device)

summary(net, (3, 32, 32))
# ----------------------------------------------------------------
#         Layer (type)               Output Shape         Param #
# ================================================================
#             Conv2d-1           [-1, 16, 32, 32]             448
#        BatchNorm2d-2           [-1, 16, 32, 32]              32
#               ReLU-3           [-1, 16, 32, 32]               0
#             Conv2d-4           [-1, 16, 32, 32]           2,320
#        BatchNorm2d-5           [-1, 16, 32, 32]              32
#               ReLU-6           [-1, 16, 32, 32]               0
#             Conv2d-7           [-1, 16, 16, 16]           2,320
#        BatchNorm2d-8           [-1, 16, 16, 16]              32
#               ReLU-9           [-1, 16, 16, 16]               0
#         Dropout2d-10           [-1, 16, 16, 16]               0
#            Conv2d-11           [-1, 32, 16, 16]           4,640
#       BatchNorm2d-12           [-1, 32, 16, 16]              64
#              ReLU-13           [-1, 32, 16, 16]               0
#            Conv2d-14           [-1, 32, 16, 16]           9,248
#       BatchNorm2d-15           [-1, 32, 16, 16]              64
#              ReLU-16           [-1, 32, 16, 16]               0
#            Conv2d-17             [-1, 32, 8, 8]           9,248
#       BatchNorm2d-18             [-1, 32, 8, 8]              64
#              ReLU-19             [-1, 32, 8, 8]               0
#         Dropout2d-20             [-1, 32, 8, 8]               0
#            Conv2d-21             [-1, 64, 8, 8]          18,496
#       BatchNorm2d-22             [-1, 64, 8, 8]             128
#              ReLU-23             [-1, 64, 8, 8]               0
#            Conv2d-24             [-1, 64, 8, 8]          36,928
#       BatchNorm2d-25             [-1, 64, 8, 8]             128
#              ReLU-26             [-1, 64, 8, 8]               0
#            Conv2d-27             [-1, 64, 4, 4]          36,928
#       BatchNorm2d-28             [-1, 64, 4, 4]             128
#              ReLU-29             [-1, 64, 4, 4]               0
#         Dropout2d-30             [-1, 64, 4, 4]               0
#         AvgPool2d-31             [-1, 64, 1, 1]               0
#           Dropout-32                   [-1, 64]               0
#            Linear-33                   [-1, 10]             650
# ================================================================
# Total params: 121,898
# Trainable params: 121,898
# Non-trainable params: 0
# ----------------------------------------------------------------
# Input size (MB): 0.01
# Forward/backward pass size (MB): 1.53
# Params size (MB): 0.47
# Estimated Total Size (MB): 2.01
# ----------------------------------------------------------------

論文メモ: Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks

物体検出の分野でブレイクスルーとなったFaster R-CNNの提案論文 Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (arXiv) についての備忘録として整理します。

ポイント

  • 先発のFast R-CNNで時間を要していた物体領域候補の抽出も、ニューラルネットワークで置き換えたことで、推論を高速化
  • 物体の領域抽出処理と分類を1つのネットワークに統合することで、一気通貫で学習できるようになった
@article{DBLP:journals/corr/RenHG015,
  author    = {Shaoqing Ren and
               Kaiming He and
               Ross B. Girshick and
               Jian Sun},
  title     = {Faster {R-CNN:} Towards Real-Time Object Detection with Region Proposal
               Networks},
  journal   = {CoRR},
  volume    = {abs/1506.01497},
  year      = {2015},
  url       = {http://arxiv.org/abs/1506.01497},
  archivePrefix = {arXiv},
  eprint    = {1506.01497},
  timestamp = {Mon, 13 Aug 2018 16:46:02 +0200},
  biburl    = {https://dblp.org/rec/bib/journals/corr/RenHG015},
  bibsource = {dblp computer science bibliography, https://dblp.org}
}

イシュー

物体検出アルゴリズムFast R-CNNの推論の高速化が本論文のメインイシューです。

背景

R-CNN1で、物体候補領域検出 (Region Proposal) と分類 (CNN + SVM) を行うアーキテクチャが提案されました。
Region ProposalにはSelective Searchを採用してます。Selective Searchは類似したセグメント (最小単位は画素) をグルーピングしていくことで、複数のセグメントに分割する手法です。
Selective Searchで得られた全ての領域に対して、CNNで特徴抽出し、SVMで分類するというものでした。

Fast R-CNN2では、R-CNNで領域候補の数だけCNNを通していた点に着目し、元の画像からCNNで得られた特徴マップを入力として分類する手法を提案しました。これによってCNNを通す回数が画像あたり1回になり、大幅に推論時間を短縮しました。
一方でRegion Proposalの計算は、R-CNN同様Selective Searchが使われていました。

その結果、Region Proposalの推論時間が大部分 (本論文の実験では80%以上) を占めるようになりました。動画に対するリアルタイムな物体検出 (5fps以上) を実現するためには、このRegion Proposalの短縮化が最後のハードルになってました。

アイディアとアーキテクチャ

キーとなるアイディアは、CNNから得られた特徴マップをインプットとしたRegion Proposal用のニューラルネットワーク (Region Proposal Network: RPN) を学習によって獲得することで、時間のかかるSelective Searchよりも高い推論速度を実現できるということです。3

  • Region Proposal以外はFast R-CNNを踏襲

RPN

Faster R-CNNのブロックアーキテクチャは下図です。

  • RPNのインプットは、conv layers (論文中ではZFNet4またはVGG165) から得られた特徴マップ
    • 典型的な特徴マップの次元数は W (~60) x H (~40) x (256 or 512)
  • RPNのアウトプットは、物体領域 (Region of Interest: RoI) のリスト (可変数)
    • Fast R-CNNでは、最初にRoI Poolingによって各RoIを固定サイズの特徴マップになるようにリサイズします

Figure 2抜粋

RPNのフローは大きく3ステップです。

Step 1. 特徴マップから n x n のスライディングウィンドウ (論文中は3 x 3) によって256 or 512次元の特徴ベクトルN個を抽出

Step 2. 特徴ベクトルN個を2つの全結合ネットワークへ入力

  • cls: 物体 or 背景を分類するネットワーク
  • reg: BBox (x, y, width, height) を予測するネットワーク

Step 3. NMS (non-maximum supression) で重複が大きいRoIを削除

  • IoUが0.7以上で重複するRoIは、clsの出力 (score) が最大のもののみを残して削除
  • 1画像あたり約2000個のRoIまで削減

Anchor

ステップ1.の特徴ベクトルの抽出では、特徴マップ1要素中に複数の物体が含まれるケースを考慮する必要があります。

論文では、スケールとアスペクト比がそれぞれ異なるk個のAnchorを導入しての特徴ベクトルを計算することで、これに対応しています。

Figure 3.抜粋

実験では以下 3 x 3 = 9 パターンが使われています。

  • スケール: 1282, 2562, 5122
  • アスペクト比: 1:1, 1:2, 2:1

損失関数

RPNの損失関数は、2つのネットワーク (cls, reg) の損失関数 (  L_{cls}, L_{reg} ) の合計で表されます。

  •  i はAnchorのインデックス
  •  L_{cls} はlog loss
  •  L_{reg} はsmooth L1 loss

式 (1) 抜粋

RPNとFaster R-CNNの同時学習

RPNとFaster R-CNNはCNN層を共有するため、学習を工夫しています。 (このあたりの理解がちょっと怪しく、実装を追って見てみる必要があります。)

  1. RPNを学習 (CNNは学習済みモデルで初期化)
  2. Faster R-CNNを、1.のRPNが推論したRoIを使って、学習 (こちらもCNNは学習済みモデルで初期化)
  3. CNNを2.で固定し、RPNをfine-tuning
  4. CNNを2.で固定し、3.のRPNが推論したRoIを使って、Faster R-CNNをfine-tuning

実験

PASCAL VOC 2007 / 2012とMS COCO 2014のデータセットを使った検証が行われてますが、ここではPASCALだけに留めます。

PASCAL VOC

推論速度

1番重要なRegion Proposalの計算 (推論) 時間ですが、VGG16でみると1枚あたり1510msから10msへ100倍以上改善されてます。全推論時間でも5fpsをマークしてます。(SS = Selective Search)

RPNの計算効率の良さに加えて、GPUの恩恵を受けられるようになったのも大きいです。

f:id:ohke:20191116134419p:plain
Table 5抜粋

精度

2007のテストセットでmAP (mean Average Precision) を比較しても、一貫してSelective Searchよりも高い値をマークしてます。(shared / unshared はRPNとFast R-CNNの重み共有の有無を表してます。)

Selective Searchよりも高品質なRegion Proposalが学習によって獲得できることの証左となってます。

Table 4抜粋

ちなみにAnchorの有無で比較してみると、VOC 2007の場合はスケールをバリエーションさせた場合にmAPを改善できることが示されてます。

  • Anchorはドメイン次第でチューニングするのが良さそうです

Table 8抜粋

まとめ

今回はFaster R-CNNの提案論文について紹介しました。

2019年現在、mAP 0.869がSOTA となってますが、end to endの学習で物体検出できるネットワークを実現したという点でとても重要な提案だったかと思います。


  1. R. Girshick, J. Donahue, T. Darrell, and J. Malik, “Rich feature hierarchies for accurate object detection and semantic seg-mentation,” in IEEE Conference on Computer Vision and Pattern Recognition (CVPR) , 2014. (arXiv)

  2. R. Girshick, “Fast R-CNN,” in IEEE International Conference on Computer Vision (ICCV) , 2015. (arXiv)

  3. 古典的な画像処理アルゴリズムからの大胆な方針転換に思えます。それだけSelective Searchがexpensiveだったことが伺えます。

  4. M. D. Zeiler and R. Fergus, “Visualizing and understanding convolutional neural networks,” in European Conference on Computer Vision (ECCV) , 2014. (arXiv)

  5. K. Simonyan and A. Zisserman. “Very deep convolutional networks for large-scale image recognition,” In Proc. of ICLR, 2015. (arXiv)