序文
畳み込みニューラル ネットワーク (ConvNets または CNNs) は、ニューラル ネットワークの一種として、cv の開発をサポートします. この記事では、畳み込みニューラル ネットワークの魂 - 畳み込み演算とその原理、および Xiaobai の観点から、0 からの完全な畳み込みを主に紹介します。 1の派手な実装に。
1
畳み込みニューラル ネットワーク (ConvNets または CNN) は、人工知能のエントリー レベルのニューラル ネットワークとして、画像認識や分類などの分野で広く使用されています。ConvNets は、ロボットや自動運転車に視覚を提供するだけでなく、顔、物体、交通標識の認識にも広く使用されています。その中でも畳み込み演算は cnn の魂であり、その登場は人工知能の発展を加速させました。
畳み込みという用語は、別の関数のすべてのトレースに対する関数の全体的な重ね合わせ効果を表すために、数学で最初に登場しました。これは本質的に、1 次元信号に対する信号のフィルタリングを完了するフィルターです。三次元画像は画像のフィルタリングの一種で, 重要でない部分を取り除いて主な特徴を抽出する. ここでは, 主に画像の畳み込み操作について詳しく説明する. 画像畳み込みは最近は登場していないが, ある.sobel
従来の画像処理におけるフィルタリング エッジ検出を行う際に畳み込みが使用されます。
2
畳み込みを理解するには、まずその操作オブジェクトである画像を理解する必要があります。これは基本的にピクセルのマトリックスです。
上の図に示すように、上図のグレースケール画像は 1 つのチャネルですが、一般的な画像には 3 つの RGB チャネルがあり、各チャネルでは、各ピクセルの値は 0 から 255 の間で、0 は黒、255 は 255 です。白用。畳み込み ( conv
) は畳み込み演算子です. 理解しやすいように、次の図の 5*5 行列をイメージ ピクセルとして使用します:
別の 3 X 3 行列を畳み込み行列と考えてください:
次に、5x5 の積を計算できます画像と 3x3 マトリックス:
画像の畳み込み行列を左から右に、上から下に 1 ピクセルずつスライドさせます (ストライドとも呼ばれます)。位置ごとに、畳み込み行列と対応する画像のピクセルを乗算し、それらを加算して最終的な整数を取得します。 、出力行列の単一要素として、上の図は単一チャネル画像です。一般的な 3 チャネル画像の場合、カーネルの in_channel も 3 チャネルであることが保証される必要があります。計算過程は下図の通りで、各層の特徴を計算した後、各層の特徴の対応する位置を加算して、最終的な出力feature
層を求めます。
CNN では、3×3 行列を **"filter**"、" kernel "、または " featuredetector " と呼び、出力行列を上から " convolution feature "、" feature map "と呼びます。アニメーション ノンストップ カーネルが同じ入力画像に対して異なる特徴マップを生成することがわかります。下の図に示すように、最初に元の画像を入力します。
異なるフィルター マトリックスを選択して、画像に対して畳み込み操作を実行し、エッジ検出を実現します。 、シャープ化、ぼかしなどの操作 - エッジ、曲線など、画像のさまざまな特徴を検出します。
実際の畳み込み操作の特徴は次のように形成されます。
赤と緑のボックスは 2 つの畳み込みカーネルで、図に示すように、入力画像をスライドさせて畳み込み、2 つの特徴マップを生成します。そして、畳み込みカーネルのサイズは、画像のローカル依存関係しか取得できないと判断するため (もちろん、カーネルのサイズを画像サイズに設定できます...)、実際の CNN では、次のことを学習する必要があります。トレーニングを通じてこれらのカーネルの値を取得し、画像からどのような特徴を抽出する必要があるかを判断します。
3
conv 畳み込み演算子の実装は torch や tensorflow などのフレームワークにパッケージ化されており、箱から出してすぐに使えるので非常に便利です. ここでは理解を容易にするために、numpy
conv を 0 から実装します. conv 演算子は順方向の計算だけでなく、逆方向の更新も必要であることを考慮して、最初にLayers
クラスを作成するため、考え方は次のとおりです。
import numpy as np
import os
class Layers():
def __init__(self, name):
self.name = name
def forward(self, x):
pass
def zero_grad(self):
pass
def backward(self, grad_out):
pass
def update(self, lr):
pass
conv 畳み込み演算子は Layer クラスを統合し、順方向および逆方向の実装は次のとおりです。
import numpy as np
from module import Layers
class Con2d(Layers):
"""
卷积前向:
输入:input:[b, cin, h, w]
weight:[cin, cout, ksize, ksize], stride, padding
计算过程:
1. 将权重拉平成:[cout, cin*ksize*ksize] self.weight 先transpose(1, 0, 2,3) 再reshpe(cout, -1)
2. 将输入整理成:[b*hout*wout,cin*ksize*ksize]:
先根据hin和win 通过pad, ksize和stride计算出hout和wout (h+2*pad-ksize)//stride + 1 (b, cout, hout, wout)
再根据img展平,整理成自己的:img (b, hout, wout, cin*kszie*ksize) -> (b*hout*wout, cin*kszie*ksize)
3. 两者相乘后,np.dot 再去reshape (cout, b*hout*wout) -> (b, cout, hout*wout)
"""
"""
卷积反向:
输入:input:[b, cout, hout, wout] -loss
计算过程:
1. 将输入换成输出格式: [b, cout, hout, wout] -> [cout, b, hout, wout] ->[cout, b*hout*wout]
2. 计算的输入与之前的图相乘: (cout, b*hout*wout) * (b*hout*wout, cin*kszie*ksize) -> (cout, cin*kszie*ksize) 得到更新后的权重
3. 将更新后的权重与图相乘,
"""
def __init__(self,name, in_channel, out_channel, kernel_size, padding, stride=1 ):
super(Con2d,self).__init__(name)
self.in_channel = in_channel
self.out_channel = out_channel
self.ksize = kernel_size
self.padding = padding
self.stride = stride
self.weights = np.random.standard_normal((out_channel, in_channel, kernel_size, kernel_size))
self.bias = np.zeros(out_channel)
self.grad_w = np.zeros(self.weights.shape)
self.grad_b = np.zeros(self.bias.shape)
def img2col(self, x, ksize, strid):
b,c,h,w = x.shape # (5, 3, 34, 34)
img_col = []
for n in range(b): # 5
for i in range(0, h-ksize+1, strid):
for j in range(0, w-ksize+1, strid):
col = x[n,:, i:i+ksize, j:j+ksize].reshape(-1) # (1, 3, 4, 4) # 48
img_col.append(col)
return np.array(img_col) # (5, 3, 31, 31, 48)
def forward(self, x):
self.x = x #(5, 3, 34,34)
weights = self.weights.reshape(self.out_channel, -1) # (12, 3*4*4)
x = np.pad (x, ((0,0), (0,0), (self.padding, self.padding), (self.padding, self.padding)), "constant") # (5, 3, 34, 34)
b, c, h, w = x.shape
self.out = np.zeros((b, self.out_channel, (h-self.ksize)//self.stride+1, (w-self.ksize)//self.stride+1))# (5, 12, 31, 31)
self.img_col = self.img2col(x, self.ksize, self.stride) # (5, 31, 31, 48) #(4805, 48)
out = np.dot(weights, self.img_col.T).reshape(self.out_channel, b, -1).transpose(1, 0,2) # (12 ,48) *(48, 4805) = (12, 4805) =(12, 5, 961) =(5, 12, 961)
self.out = np.reshape(out, self.out.shape)
return self.out
def backward(self, grad_out):
b, c, h, w = self.out.shape
grad_out_ = grad_out.transpose(1, 0, 2, 3 )
grad_out_flag = np.reshape(grad_out_,[self.out_channel, -1]) # [cout, b*h*w]
self.grad_w = np.dot(grad_out_flag, self.img_col).reshape(c, self.in_channel, self.ksize, self.ksize) # (cout, cin*kszie*ksize) -权重值
self.grad_b = np.sum(grad_out_flag, axis=1) # [cout] -偏置值
tmp = self.ksize -self.padding -1
grad_out_pad = np.pad(grad_out, ((0,0),(0,0),(tmp, tmp),(tmp,tmp)),'constant')
weights = self.weights.transpose(1, 0, 2, 3).reshape([self.in_channel, -1]) # [cin. cout*ksize*ksize]
col_grad = self.img2col(grad_out_pad, self.ksize, 1) #
next_eta = np.dot(weights, col_grad.T).reshape(self.in_channel, b, -1).transpose(1, 0, 2)
next_eta = np.reshape(next_eta, self.x.shape)
return next_eta
def zero_grad(self):
self.grad_w = np.zeros_like(self.grad_w)
self.grad_b = np.zeros_like(self.grad_b)
def update(self, lr=1e-3):
self.weights -= lr*self.grad_w
self.bias -= lr*self.grad_b
if __name__ == '__main__':
x = np.ones([2,3,32,32])
conv = Con2d('conv1',3,12,3,1,1)
for i in range(100):
y = conv.forward(x)
loss =abs( y - 1)
x = conv.backward(loss)
lr = 1e-4
conv.update(lr)
print(np.sum(loss))
実装から卷积
、畳み込み演算で大量の時間が画像を行列 img2col 関数に変換するのに費やされていることがわかります。これはモデルの軽量化プロセスの後半でもあり、mobilenet 深度分離可能な畳み込み + 1x1 畳み込み、1x1 畳み込みは行いますimg2col を必要とせず、チップ側で簡単にデプロイでき、畳み込みを高速化できます。
Xiaobai のピットインに関するその他の記事については、公式アカウント [ The Invincible Zhang Dadao ]を参照してください。