【内部処理】畳み込みの高速処理を実現する方法
畳み込みをforループを用いずにどのように実装されているのかが気になっていたのですが,調べてみると,どうやらnn.Unfoldというメソッドを使うことで行列演算へと帰着させる方法が存在するようです.そこでnn.Unfoldを用いて畳み込みを実装してみました.
(尚,PyTorchが実際にこのような方法を用いてnn.Conv2d等の畳み込み演算を実装しているかまでは確認していません.あくまでそういった実装も可能というだけです)
検証のため,nn.Unfoldを用いた実装による畳み込みの出力と,通常通りnn.Conv2dを介した出力を比較してみました.
実装
batch_size,in_channel,out_channelは任意に変更可能です.
batch_size = 2 in_channel = 3 out_channel = 2 input = torch.FloatTensor(np.random.rand(batch_size, in_channel, 3, 3)) input2 = torch.nn.Unfold(kernel_size=(2, 2), stride=(1, 1), padding=(0, 0))(input) c = nn.Conv2d(in_channel, out_channel, 2) print(f'Conv2d層の重み:\n{c.weight.data}\nバイアス:\n{c.bias.data}') w = c.weight.data.detach().clone() w = w.view(out_channel, -1) b = c.bias.data print('\nカーネルを展開') print(w) print('\n展開した入力とカーネルで行列積を行い,整形') print((torch.matmul(w, input2) + b.view(1, out_channel, 1)).view(batch_size, out_channel, 2, 2)) print('\n通常の畳み込みによる処理') print(c(input))
出力
Conv2d層の重み: tensor([[[[ 0.1203, 0.1741], [-0.2618, 0.2749]], [[ 0.0496, 0.2472], [-0.1281, 0.0517]], [[ 0.0583, 0.2524], [ 0.2381, 0.2859]]], [[[ 0.2665, -0.1610], [ 0.1499, 0.1978]], [[ 0.0339, 0.1021], [ 0.0370, 0.0104]], [[ 0.1742, 0.2682], [-0.0080, -0.2463]]]]) バイアス: tensor([-0.2232, 0.1847]) カーネルを展開 tensor([[ 0.1203, 0.1741, -0.2618, 0.2749, 0.0496, 0.2472, -0.1281, 0.0517, 0.0583, 0.2524, 0.2381, 0.2859], [ 0.2665, -0.1610, 0.1499, 0.1978, 0.0339, 0.1021, 0.0370, 0.0104, 0.1742, 0.2682, -0.0080, -0.2463]]) 展開した入力とカーネルで行列積を行い,整形 tensor([[[[0.6216, 0.1176], [0.6010, 0.4215]], [[0.8527, 0.7857], [0.6273, 0.5581]]], [[[0.7947, 0.3132], [0.3201, 0.7330]], [[0.6220, 0.8391], [0.3936, 0.4128]]]]) 通常の畳み込みによる処理 tensor([[[[0.6216, 0.1176], [0.6010, 0.4215]], [[0.8527, 0.7857], [0.6273, 0.5581]]], [[[0.7947, 0.3132], [0.3201, 0.7330]], [[0.6220, 0.8391], [0.3936, 0.4128]]]], grad_fn=<ThnnConv2DBackward>)
2つの出力が等しいことから,畳み込みはnn.Unfoldを用いて実装できることがわかりました.
おまけ(nn.Linear)
線形変換のnn.Linearもtorch.matmulで超簡単に実装できます.
in_features = 4 out_features = 2 dim = 3 l = nn.Linear(in_features, out_features) input = torch.randn(1, dim, in_features) l(input), torch.matmul(input, l.weight.data.T) + l.bias.data
出力
(tensor([[[-0.6326, 0.3161], [-0.0485, 0.2758], [-0.4873, 0.5317]]], grad_fn=<AddBackward0>), tensor([[[-0.6326, 0.3161], [-0.0485, 0.2758], [-0.4873, 0.5317]]]))
活性化関数と重みの初期値の関係
『ゼロから作るDeep Learning』を久々に読み返すと,活性化関数と重みの初期値の関係についての記載がありました.
恥ずかしながら初期値についてはあまり意識せず適当に決めていたので,意識付けのためにも,これらの関係について検証を行ってみました(n番煎じ).
本書曰く,
活性化関数にReLUを使う場合は「Heの初期値」,sigmoidやtanhなどのS字カーブのときは「Xavierの初期値」を使う
ということらしいです.
前層のノード数をとするとき,Heの初期値ではを,Xavierの初期値ではを標準偏差とするガウス分布によって初期化を行います.
モデルの定義
import torch import torch.nn as nn import matplotlib.pyplot as plt class TestModel(nn.Module): def __init__(self, hidden_num, layer_num, init_func=None, act_func=None): super(TestModel, self).__init__() self.act_dist = [] def forward_func(module, input, outputs): self.act_dist.append(outputs.view(-1).detach().clone().numpy()) if init_func == 'he_normal': init = nn.init.kaiming_normal_ elif init_func == 'he_uniform': init = nn.init.kaiming_uniform_ elif init_func == 'xavier': init = nn.init.xavier_normal_ else: init = nn.init.normal_ layers = [] for _ in range(layer_num): l = nn.Linear(hidden_num, hidden_num) init(l.weight) layers.append(l) if act_func == 'relu': act = nn.ReLU() elif act_func == 'sigmoid': act = nn.Sigmoid() elif act_func == 'tanh': act = nn.Tanh() else: act = nn.Identity() act.register_forward_hook(forward_func) layers.append(act) self.layers = nn.Sequential(*layers) def forward(self, x): x = self.layers(x) return x
順伝播の実行と分布の出力
hidden_num = 100 layer_num = 5 init_func = 'he_normal' act_func = 'relu' input = torch.randn(1000, 1, hidden_num) model = TestModel(hidden_num, layer_num, init_func=init_func, act_func=act_func) outputs = model(input) fig = plt.figure(figsize=(24, 4)) fig.suptitle(f'Initialization: {init_func}, Activation: {act_func}', fontsize=20) for i, ad in enumerate(model.act_dist): plt.subplot(1,len(model.act_dist), i+1) plt.hist(ad, bins=50) if act_func == 'relu': plt.xlim([0.0, 1.0]) plt.ylim([0, 10000]) elif act_func == 'sigmoid': plt.xlim([0.0, 1.0]) plt.ylim([0, 6000]) elif act_func == 'tanh': plt.xlim([-1.0, 1.0]) plt.ylim([0, 5000])
検証
隠れ層の次元が100の線形結合を5層つなげて活性化関数の出力の分布をヒストグラムにしました.
ReLU
Xavierの初期値
Heの初期値
確かに「Xavierの初期値」では分布に偏りが見られます.
Sigmoid
Xavierの初期値
Heの初期値
「Heの初期値」では,「Xavierの初期値」に比べて僅かですが3層目以降の分布が歪になる傾向が見られました.
また,「Heの初期値」では1層目の分布が,山を均したような形状になっています.『活性化関数の出力の分布が広い方が表現力が高い』と本書に記載がありましたが,Sigmoidでは0.5から離れるにつれて傾きが小さくなるため,誤差消失が起きる可能性が高まります.一概に広い→良いとは言えないのかもしれません.
int64型のnumpy配列では,ToTensor()による正規化が行われない?
型をuint8にすると正しく[0, 1]になりました.以後,気をつけます.
import numpy as np import torchvision.transforms as transforms t = transforms.ToTensor() a = np.random.randint(0, 255, size=(4, 3)) print(a.dtype) print(t(a)) # なぜか[0, 1]にならない a = a.astype('uint8') print(a.dtype) print(t(a)) # dtypeを変更すると[0, 1]になる # output int64 tensor([[[129, 27, 22], [ 23, 107, 42], [ 62, 44, 228], [206, 226, 22]]]) uint8 tensor([[[0.5059, 0.1059, 0.0863], [0.0902, 0.4196, 0.1647], [0.2431, 0.1725, 0.8941], [0.8078, 0.8863, 0.0863]]])
nn.○○とnn.functional.○○の違い
nn.ReLU()は、nn.Moduleを作ります。つまり、nn.Sequential()に追加できます。反対にnn.functional.reluは、relu関数を呼ぶだけで、もし記述するならforward methodの方に書きます。好きな方を使えば良いみたいですが、個人的には、print(model)とモデルの構造を表示させたときにreluも表示させたいので、nn.ReLU()の方を使います。
nn.functionalだとあくまでforward処理途中に関数を挟み込むだけなので,モデル出力には反映されないみたいです.
また,nnの場合は,予めパラメータを渡し,クラスをインスタンス化します.
そのため,何度も繰り返し使う場合等は,コードが簡略化でき,可読性向上にも繋がるかと思います.
AAP = torch.nn.AdaptiveAvgPool2d(output_size=1) # パラメータを設定し,インスタンス化 AAP(test_tensor1) AAP(test_tensor2) ...
eval()時の注意点
PyTorchではmodel.train()やmodel.eval()によって、モデルのモードを切り替えますが、これらのメソッドによってドロップアウトを行うか否かを自動で切り替えてくれるのはドロップアウトクラス(torch.nn.Dropout)の方です。torch.nn.functional.dropoutの方は、model.eval()などが働きません。関数の引数でtorch.nn.functional.dropout(training=False)などとしなければならないのです。
また,functionalでは,eval()時にオフにされるべき層などが正しく機能しない場合があるそうです.
総括すると,基本的にnnで良さそうですね.
Adaptive_avg_pool2dとavg_pool2dの違い
test_input = Image.open('test_image.jpeg') # 1024x768 plt.figure(figsize=(12,4)) # avg_pool2d outputs1 = F.avg_pool2d(torchvision.transforms.ToTensor()(test_input), kernel_size=(4, 4), stride=2, padding=1) print(outputs1.shape) plt.subplot(1,2,1), plt.imshow(outputs1.permute(1,2,0)) # adaptive_avg_pool2d w, h = test_input.size outputs2 = F.adaptive_avg_pool2d(torchvision.transforms.ToTensor()(test_input), output_size=(int(h*0.5), int(w*0.5))) print(outputs2.shape) plt.subplot(1,2,2), plt.imshow(outputs2.permute(1,2,0)) # torch.Size([3, 384, 512]) # torch.Size([3, 384, 512])
avg_pool2dは,カーネルサイズやストライド,パディングを指定し,畳み込み演算を行います.(恐らくカーネルの各係数は,1/(カーネルの画素数))
一方,adaptive_avg_pool2dは,出力サイズを指定すると,自動的にカーネルサイズ等を決定してくれる模様.
リサイズ
tzmi.hatenablog.com
tzmi.hatenablog.com
上記記事によると,縮小(downsample)はadaptive_avg_pool2d,拡大(upsample)はF.interpolate(レイヤとして使用する場合はnn.Upsample)で良さそう?
※ レイヤとして使用する場合の意味
関数として利用するか,クラスをインスタンス化して利用するかの違いです.
nn.Upsampleは後者にあたり,init時にパラメータを渡してレイヤをインスタンス化しておき,forward上でレイヤとして利用します.
(必ずしもnn.Upsampleである必要はなく,F.interpolateをforward内で使用することも可能かと思います)
# 関数として利用 x = torch.randn(1,1,4,4) x_out = F.interpolate(x, (8,8)) # 関数として利用 # クラスをインスタンス化して利用 x = torch.randn(1,1,4,4) m = nn.Upsample((8,8)) # インスタンス化 x_out = m(x)
PyTorchで線形回帰(二次関数)
はじめに
PyTorchで単純な線形回帰を行いたいと思います.
nn.Linear()を使ってやるのもいいのですが,今回は,重みとバイアス用のTensorを自分で定義する方針で行いました.
色々読み込み
import torch import torch.nn as nn import matplotlib.pyplot as plt import numpy as np
データの準備
N = 100 x = np.linspace(-15, 15, N) y = -2 * x**2 + 10*x + 32*np.random.randn(N) x = x.astype('float32') y = y.astype('float32') x = torch.from_numpy(x) y = torch.from_numpy(y) w0 = torch.tensor(1.0, requires_grad=True) w1 = torch.tensor(1.0, requires_grad=True) b = torch.tensor(0.0, requires_grad=True)
今回は,を線形回帰させます.
よって,なるべくに近づくことが理想です.
また,バイアス項は,N(0, 1)に従う乱数によってバラつくので,0付近に近づくと思います.
グラフ
モデル定義
def model(x): return w0*x**2 + w1*x + b
PyTorchのモデルというと,class MyModel(nn.Module): という定義の仕方が主ですが,
今回はnnモジュールを使った複雑な計算は必要としないので,関数として定義します.
損失関数と最適化手法の定義
criterion = nn.MSELoss()
optimizer = torch.optim.SGD([w0, w1, b], lr=1.0e-5)
損失関数は平均二乗誤差,最適化手法はSGDで学習率は0.00001としました.
また,勾配計算によって更新したいパラメータは,第一引数にリストで渡してやれば良いっぽいです.
(nn.Moduleによる定義では,モデル全体のパラメータを対象にしたいため,model.parameters()としていました)
学習
losses = [] epochs = 10000 es_count = 0 patience = 3 for epoch in range(epochs): optimizer.zero_grad() pred = model(x.view(-1, 1)) loss = criterion(pred, y.view(-1, 1)) loss.backward() optimizer.step() losses.append(loss.item()) if epoch % 100 == 0: print("epoch {}, loss: {}".format(epoch+1, losses[epoch])) # Early Stopping if epoch > 0 and losses[epoch - 1] < losses[epoch]: es_count += 1 if es_count >= patience: break else: es_count = 0 print('loss:', loss.item()) print('w0:', w0.item()) print('w1:', w1.item()) print('b:', b.item())
100epochごとに結果を表示します.
es_countというのは,EarlyStoppingという機能を実装するための変数です.
EarlyStoppingは,指定の回数以上損失が上がり続けた段階で学習をストップさせるというものです.
具体的には,1epoch前のlossと比較し,上回っていればカウント.3(=patience)回連続で続けば学習ストップ,としています.
(今回は10000epochまでlossが上昇することはなかったため,不要でしたが・・・)
結果
epoch 1, loss: 102432.0390625 epoch 101, loss: 5998.56396484375 epoch 201, loss: 4711.43408203125 epoch 301, loss: 3763.884765625 epoch 401, loss: 3066.32275390625 epoch 501, loss: 2552.794921875 epoch 601, loss: 2174.745361328125 epoch 701, loss: 1896.4339599609375 epoch 801, loss: 1691.5440673828125 epoch 901, loss: 1540.706298828125 epoch 1001, loss: 1429.658447265625 (中略) epoch 9001, loss: 1118.526123046875 epoch 9101, loss: 1118.5107421875 epoch 9201, loss: 1118.49560546875 epoch 9301, loss: 1118.480224609375 epoch 9401, loss: 1118.4647216796875 epoch 9501, loss: 1118.4493408203125 epoch 9601, loss: 1118.434326171875 epoch 9701, loss: 1118.4189453125 epoch 9801, loss: 1118.403564453125 epoch 9901, loss: 1118.3885498046875
loss: 1118.3734130859375 w0: -2.0004830360412598 w1: 10.306112289428711 b: 0.38533857464790344
は-2に,は10に近づいているので,それなりに近似できていることがわかります.
グラフ
青が元の関数,オレンジが予測です.
見事に重なっていますね.
lossの推移(おまけ)
断崖絶壁です.これを見る限り,1000epochもすれば十分そうですね.
PyTorchでMNIST
はじめに
PyTorchを触ってみました.
試しに,単純なパーセプトロンのみでMNIST分類をやってみます.
すべてGoogle Colabの環境で実行しました.
Python: 3.6.9
PyTorch: 1.6.0+cu101
色々読み込み
import torch, torch.nn as nn, torchvision from torchvision import models, transforms import matplotlib.pyplot as plt
データローダ作成
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, ), (0.5, ))]) trainset = torchvision.datasets.MNIST(root='./', train=True, download=True, transform=transform) testset = torchvision.datasets.MNIST(root='./', train=False, download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True) testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)
transforms.Compose()めちゃ便利
transforms.Compose([処理1, 処理2, ...])と書くだけで,上から順に加工処理を適用してくれる.
適当に抜粋↓
transforms.Resize(size) # 画像の短辺がこのサイズにフィットするようリサイズ transforms.CenterCrop() # 画像の中央を(おそらく)短辺の長さで正方形に切り取る transforms.RandomHorizontalFlip() # ランダムで反転 transforms.ToTensor() # Tensor型に変換 ※必須級 transforms.Normalize(mean, std) # 引数はTensor型に限る(故に最後に配置すると良さそう)
ちなみに,transforms.Normalizeは引数をタプルで渡しますが,
transforms.Normalize((0.1, 0.2, 0.3), (1.1, 1.2, 1.3) )のように書くと,左からR, G, Bの各チャネルに対応するそうです.
モデル定義
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.layer1 = nn.Sequential( nn.Linear(784, 1000), nn.Sigmoid() ) self.classifier = nn.Sequential( nn.Linear(1000, 10), nn.Softmax(dim=1) ) def forward(self, x): x = self.layer1(x) return self.classifier(x)
学習モデル.中間層のユニット数は1,000としました.
forward()に順伝播の処理順を示します.
モデルのインスタンス化
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') net = Net()to(device)
PyTorchはTensorごとにリソース(CPU or GPU)を選択できます.
CUDAが利用できる環境であればtorch.cuda.is_available()はTrueを返すので,deviceの中身はtorch.device('cuda:0')となります.
そしてインスタンス生成時にnet = Net().to(device)としてやれば,モデルに含まれるパラメータをすべてGPU上で処理できるようになります.
損失関数と最適化手法の決定
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
損失関数はクロスエントロピー.
最適化手法はAdamで,学習率は0.001です.
学習
train_loss_history = [] val_loss_history = [] acc_history = [] epochs = 10 for epoch in range(epochs): train_loss_sum = 0.0 val_loss_sum = 0.0 net.train() # 学習モード for x, y in trainloader: # 勾配を初期化 optimizer.zero_grad() # データ整形 & GPU処理用に変換 x = x.view(-1, 28*28) x = x.to(device) y = y.to(device) # 順伝播 outputs = net(x) loss = criterion(outputs, y) # 逆伝播(バックプロパゲーション) loss.backward() # 重みの更新 optimizer.step() # lossの累積を加算 train_loss_sum += loss.item() net.eval() # 評価モード correct = 0.0 with torch.no_grad(): for x, y in testloader: # データ整形 & GPU処理用に変換 x = x.view(-1, 28*28) x = x.to(device) y = y.to(device) # 順伝播 outputs = net(x) loss = criterion(outputs, y) # lossの累積を加算 val_loss_sum += loss.item() # ラベルと一致していたら正答としてカウント pred = outputs.argmax(dim=1, keepdim=True) correct += pred.eq(y.view_as(pred)).sum().item() # 色々表示 train_loss = train_loss_sum / len(trainloader) val_loss = val_loss_sum / len(testloader) accuracy = correct / len(testset) print('Epoch:{}, train_loss: {:.3f}, val_loss: {:.3f}, val_accuracy: {}' .format(epoch + 1, train_loss, val_loss, accuracy)) # 記録 train_loss_history.append(train_loss) val_loss_history.append(val_loss) acc_history.append(accuracy)
x = x.to(device)は,先程と同じGPU処理用の命令です.
モデルではnet.to(device)とすることで,その内部に含まれるパラメータすべてに適用しました.
データローダから新たに取り出してきたTensor達は未だCPU用なので,同様に適用させる必要があります.
net.eval()を実行することで,推論時にDropout層やBatchNormalization層をオフにできます.
(今回の例では上記レイヤは使っていないので,なくても問題ありません)
with torch.no_grad():をつけると,勾配計算が省略されるため,メモリ節約になるそうです.
ラベルと一致していたら…の部分の挙動の詳細
outputsの形状は(32, 10)です.(32: バッチサイズ, 10: クラス数)
まず,argmax(指定した軸(ここでは1)に沿って,最大値のindexを返す)を適用します.
ここで,keepdim=Trueをつけることで,2階のテンソルという情報が維持され,predの形状は(32, 1)となります.
続いて,pred.eq(y.view_as(pred)).sum().item()についてです.
a.view_as(b)は,aの形状をbの形状に合わせて変換します.
(view()という似た名前の形状変換用のメソッドがありますが,その仲間だと思います)
a = torch.arange(0, 12) # shape=(12, ) b = torch.Tensor(12).reshape(3, 4) # shape=(3, 4) a.shape # 変換前 # torch.Size([12]) a = a.view_as(b) # bの形状に合わせてaを変換 a.shape # 変換後 # torch.Size([3, 4])
勿論,aとbの要素数は一致している必要があります.
eqは,2つのTensorを要素ごとに比較します.
要素ごとに,一致していればTrue, 違えばFalseが格納されたTensorを返します.
a = torch.tensor([1,2,3,4,5]) b = torch.tensor([0,2,0,4,5]) a.eq(b) # tensor([False, True, False, True, True])
勿論,比較対象同士の形状が同じでなければエラーが出るので注意しましょう.
a = torch.tensor([0,1,2,3,4,5,6,7,8,9]) b = torch.tensor([0,1,2]) a.eq(b) # The size of tensor a (10) must match the size of tensor b (3) at non-singleton dimension 0
つまり,pred.eq(y.view_as(pred))は,『yをpredの形状に合わせ,yとpredの各要素を比較し,TrueかFalseが計32(=batch_size)個入ったTensorを求める』という処理です.
そして,PythonではTrueは1,Falseは0として扱われるため,sum()を適用することでTrueの数の累積を取ります.
また,sum()を適用したままではTensor型なので,実際の値を取得するためにitem()を適用します.
こうして,32個中いくつ正解できたか,という正答数が得られるので,これをすべて合算して全テストデータ数で割ることで正答率を算出しています.
学習結果
出力
Epoch:1, train_loss: 1.615, val_loss: 1.549, val_accuracy: 0.9172 Epoch:2, train_loss: 1.528, val_loss: 1.529, val_accuracy: 0.9356 Epoch:3, train_loss: 1.513, val_loss: 1.509, val_accuracy: 0.9561 Epoch:4, train_loss: 1.503, val_loss: 1.501, val_accuracy: 0.9632 Epoch:5, train_loss: 1.499, val_loss: 1.498, val_accuracy: 0.9658 Epoch:6, train_loss: 1.495, val_loss: 1.496, val_accuracy: 0.9686 Epoch:7, train_loss: 1.492, val_loss: 1.494, val_accuracy: 0.97 Epoch:8, train_loss: 1.490, val_loss: 1.493, val_accuracy: 0.971 Epoch:9, train_loss: 1.488, val_loss: 1.491, val_accuracy: 0.9732 Epoch:10, train_loss: 1.486, val_loss: 1.494, val_accuracy: 0.9692
lossグラフ
精度グラフ