參考博客:
卷積神經網絡反向傳播理論推導https://blog.csdn.net/Hearthougan/article/details/72910223
【python】如何用 numpy 實現https://blog.csdn.net/qq_36393962/article/details/99354969
目錄
一、最開始的說明(很重要)
按照大部分公式的約定,C表示代價,z 表示網絡層輸出,a 表示激活函數值,w 爲權重,b 爲偏置。
如果沒有特別說明,卷積指的是"valid"卷積,激活層爲sigmoid。
其實,神經網絡中的每一層都相當於一個函數f,將“前層”的輸出x映射到y,也就是“後層”輸出爲y=f(x)。不斷地利用“前層”的輸出帶入到“後層”的函數中,求出“後層”的輸出!這就是前向傳播!
神經網絡的最終層輸出是代價函數C,C是一個多維函數,可以對網絡中任何一個變量求偏導,比如對每一層的y求偏導得到
爲什麼要對每一層的輸出y求偏導呢?因爲這樣就可以利用上“多維函數求偏導的鏈式法則”了,大概就是說可以從最終層的偏導開始,不斷地利用“後層”的偏導計算“前層”的偏導,利用每一層的偏導可以進而求出每一層中權重w和偏置b的偏導!有了權重w和偏置b的偏導就可以更新權重w和偏置b啦!這就是反向傳播!
一點點領悟,後面看例子就會明白的!
完整代碼鏈接:https://download.csdn.net/download/jin739738709/11831051
二、激活層
正向公式:
反向公式:
實現代碼:
class FuncLayer(Layer):
def __init__(self, activate_fn: Func):
self.f = activate_fn
self.z: np.ndarray = None
def __call__(self, x: np.ndarray) -> np.ndarray:
self.z = x
return self.f(x)
def backward(self, dc_da: np.ndarray) -> np.ndarray:
da_dz = self.f.derivate(self.z)
if self.f.jacobin:
# 如果求導結果只能表示成雅克比矩陣,得使用矩陣乘法
dc_dz = dc_da.dot(da_dz.T)
else:
# 求導結果爲對角矩陣,可以採用哈達馬積(逐值相乘)來簡化運算
dc_dz = dc_da * da_dz
return dc_dz
三、全連接層
class FullConnectedLayer(Layer):
def __init__(self, input_size, output_size):
self.i_size = input_size
self.o_size = output_size
if self.i_size is not None:
self.__init(self.i_size)
def __init(self, input_size):
self.i_size = input_size
self.w = np.random.normal(loc=0.0, scale=1.0, size=(self.i_size, self.o_size))
self.b = np.random.normal(loc=0.0, scale=1.0, size=(1, self.o_size))
self.x: np.ndarray = None # input
def __call__(self, x: np.ndarray) -> np.ndarray:
x = x.reshape(1, -1)
# 如果 self.i_size 還沒有確定,則根據x.shape來初始化
if self.i_size is None:
self.__init(x.shape[1])
self.x = x
self.z = x.dot(self.w)+self.b
return self.z
def backward(self, dc_dz: np.ndarray) -> np.ndarray:
dc_dx = dc_dz.dot(self.w.T)
self.w += self.x.T.dot(dc_dz)
self.b += dc_dz
return dc_dx
四、池化層
實現代碼:
class MeanPoolingLayer(Layer):
def __init__(self, kernel_size: int, stride: int):
self.ks = kernel_size
self.kernel_shape = (kernel_size, kernel_size)
self.channels: int = None
self.stride = stride
self.input_shape: tuple = None # row_cnt,col_cnt,channels
self.target_shape: tuple = None # 目標的shape
def __call__(self, mat: np.ndarray) -> np.ndarray:
self.input_shape = mat.shape
self.channels = mat.shape[2]
row, col = mat.shape[0], mat.shape[1]
(kr, kc), s = self.kernel_shape, self.stride
self.target_shape = ((row-kr)//s+1, (col-kc)//s+1, self.channels)
target = np.zeros(self.target_shape)
for i in range(self.target_shape[0]):
for j in range(self.target_shape[1]):
r, c = i*s, j*s
target[i, j] = np.average(mat[r:r+kr, c:c+kc], axis=(0, 1))
return target
def backward(self, d_out: np.ndarray) -> np.ndarray:
d_input = np.zeros(self.input_shape)
n = self.kernel_shape[0]*self.kernel_shape[1]
d_mat = d_out/n # mean-pooling 求導後恰好是 1/n
(kr, kc), s = self.kernel_shape, self.stride
for i in range(self.target_shape[0]):
for j in range(self.target_shape[1]):
r, c = i*s, j*s
d_input[r:r+kr, c:c+kc] += d_mat[i, j]
return d_input
五、愛因斯坦求和約定(numpy::einsum)
參考博客:
NumPy中einsum的基本介紹http://www.atyun.com/32288.html
在說明卷積層和卷積層的實現代碼之前,必須說一下numpy的一個強大矩陣運算工具einsum。
由於其強大的表現力和智能循環,它在速度和內存效率方面通常可以超越我們常見的array函數。
但缺點是,可能需要一段時間才能理解符號,有時需要嘗試才能將其正確的應用於棘手的問題。
不過缺點也不是缺點,舉個例子大家就會明白的,其實也沒有多難理解!
import numpy as np
mm = np.array(
[
[[1,2], [1,2]],
[[1,2], [1,2]]
]
)
print(mm,mm.shape)
nn = np.array(
[
[[1,2,3], [1,2,0]],
[[1,2,3], [1,2,3]]
]
)
print(nn,nn.shape)
print(np.einsum("ijk,ijl->kl",mm,nn,dtype=np.float64))
#結果:
#[[ 4. 8. 9.]
# [ 8. 16. 18.]]
print(np.einsum("ijk,ijl->l",mm,nn,dtype=np.float64))
# 結果: [12 24 27]
代碼中的矩陣mm和矩陣nn可以由上圖表示,
比如說,對於矩陣nn,當i=0,j=0,l=2,則nn[0,0,2]==3;當i=0,j=1,l=2,則nn[0,1,2]==0
所以,當執行下列語句時,
np.einsum("ijk,ijl->kl",mm,nn,dtype=np.float64)
產生結果如圖所示
這個結果是怎麼產生的呢?比如說當k=0,l=2
矩陣mm中元素mm[:,:,0]和矩陣nn中元素nn[:,:,2],按照對應位置元素相乘,然後把四個相乘的結果相加,也就是1*3+1*0+1*3+1*3=9
所以,當執行下列語句時,
np.einsum("ijk,ijl->l",mm,nn,dtype=np.float64)
產生結果如圖所示
式子中"ijk,ijl->l",之前的k被省略了,輸出中省略的字母意味着沿該軸的值將相加,如下圖所示
六、卷積層
class ConvolutionLayer(Layer):
def __init__(self, in_channels, out_channels, kernel_size, stride):
self.in_channels = in_channels
self.out_channels = out_channels
self.ks = kernel_size
self.kernel_shape = (kernel_size, kernel_size)
self.stride = stride
self.x: np.ndarray = None # input
# 卷積核: row,col,channel 順序
self.kernel = np.random.normal(loc=0.0, scale=1.0, size=(kernel_size, kernel_size, out_channels))
self.bias = np.random.normal(loc=0.0, scale=1.0, size=(out_channels,))
# self.kernel =np.ones((kernel_size,kernel_size,out_channels))
# self.bias = np.ones((out_channels,))
def check_x_mat_shape(self, x_mat):
'''
要求卷積核在卷積過程中可以把矩陣鋪滿(stride空隙不算)
右側(下側)不能有多餘的列(行)
如 28x28 不能用(5x5,stride=2)的卷積核,因爲它只能覆蓋(27x27)
'''
row, col = x_mat.shape[0], x_mat.shape[1]
k, s = self.ks, self.stride
assert (row-k)//s*s+k == row
assert (col-k)//s*s+k == col
def __call__(self, x_mat: np.ndarray) -> np.ndarray:
self.check_x_mat_shape(x_mat)
self.x = x_mat
return self.__conv(
stride=self.stride,
mat=x_mat,
kernel=self.kernel,
bias=None)#bias=self.bias
def backward(self, dc_dz: np.ndarray) -> np.ndarray:
# 反向卷積的目標是dc_dz補0之後的矩陣(張量)
# (padding + dilation)
# 補0規則爲:邊緣padding kernel_size-1 層0;間隔處補 stride-1 層0
# 只看橫向,如果dc_dz有c列,那該矩陣有 2kernel_size+(m-1)stride-1 列
# 反向卷積的stride固定爲1
(kr, kc, ch), s = self.kernel.shape, self.stride
dc_dz_with_zeros_shape = (
2*kr + (dc_dz.shape[0]-1)*s-1,
2*kc + (dc_dz.shape[1]-1)*s-1,
dc_dz.shape[2]
)
D = np.zeros(dc_dz_with_zeros_shape) # 爲了簡化,用D表示補充0之後的張量
for i in range(dc_dz.shape[0]):
for j in range(dc_dz.shape[1]):
D[kr+i*s-1, kc+j*s-1] = dc_dz[i, j]
# 求 dc_da(a指的是該層的輸入self.x,因爲習慣上稱呼上一層的激活值爲a[l-1])
# 注意stride(步長)是1,而且是每個通道卷積核分別對對應通道mat卷積,最後加和到一個通道
# 卷積結果 dc_da_map 要加到 dc_da 結果的每一層
# (仔細在草稿紙上計算不難證明這點)
dc_da_map = self.__conv(
stride=1,
mat=D,
kernel=self.kernel[::-1, ::-1], # 注意不能漏了反向傳播中卷積核的180度旋轉 rot180(w)
bias=None,
einsum_formula="ijk,ijk->",
out_channels=1)
dc_da = np.repeat(dc_da_map.reshape((dc_da_map.shape[0], -1, 1)), self.in_channels, axis=2)
# 求 dc_dw(即dc_d kernel)
# 也是卷積,只不過是用 rot180(a_input) 對 D 卷積
dc_dw = self.__conv(
stride=1,
mat=D,
kernel=self.x[::-1, ::-1],
bias=None,
einsum_formula="ijk,ijl->k",
out_channels=self.kernel.shape[2])
# 求 dc_db
dc_db = np.einsum("ijk->k", dc_dz)
# 更新w(kernel)和b(bias),並返回 dc_da
self.kernel += dc_dw
self.bias += dc_db
return dc_da
def __conv(self,
stride: int,
mat: np.ndarray,
kernel: np.ndarray,
bias: np.ndarray = None,
einsum_formula: str = "ijk,ijl->l",
out_channels: int = None):
'''
注意 out_channels 要與 einsum_formula 相對應
'''
# 卷積運算 sub_np_tensor * kernel_np_tensor + bias
if bias is None:
def f(m): return np.einsum(
einsum_formula, m, kernel)
else:
def f(m): return np.einsum(einsum_formula, m, kernel) + bias
row, col = mat.shape[0], mat.shape[1]
(kr, kc, ch), s = kernel.shape, stride
# out_channels 默認爲 kernel.shape[2]
out_channels = ch if out_channels is None else out_channels
target_shape = ((row-kr)//s+1, (col-kc)//s+1, out_channels)
target = np.zeros(target_shape)
for i in range(target_shape[0]):
for j in range(target_shape[1]):
r, c = i*s, j*s
target[i, j] = f(mat[r:r+kr, c:c+kc])
return target
七、舉個例子
下圖爲例子的整個網絡模型,圖中展示了網絡的前向傳播、反向傳播以及參數更新。全部公式嚴格按照前面幾章的內容進行推導。
測試代碼:
a = np.array(
[
[[1, 1, 3], [2, 2, 3], [3, 3, 5], [4, 4, 5], [3, 3, 5], [4, 4, 5]],
[[0, 0, 3], [1, 1, 3], [0, 0, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
[[5, 5, 3], [0, 0, 3], [9, 9, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
[[5, 5, 3], [0, 0, 3], [9, 9, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
[[5, 5, 3], [0, 0, 3], [9, 9, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
[[6, 6, 3], [3, 3, 3], [7, 7, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]]
]
)
label = np.array([[1, 0]])
from funcs import sigmoid
from lossfuncs import sse
from nn import NN
# conv = ConvolutionLayer(2,1,2,1)
my_nn = NN((6, 6, 3), (1, 2))
my_nn.set_layers([
ConvolutionLayer(3, 6, 1, 1),#卷積層
FuncLayer(sigmoid),#sigmoid激活層
MeanPoolingLayer(2,2),#平均池化層
FuncLayer(sigmoid),#sigmoid激活層
ConvolutionLayer(6, 1, 2, 1),#卷積層
FuncLayer(sigmoid),#sigmoid激活層
ReshapeLayer(None,(1,-1)),#reshape
FullConnectedLayer(None, 2),#全連接層
FuncLayer(sigmoid),#sigmoid激活層
])
y1 = my_nn.forward(a)
for i in range(20000):
my_nn.train(a, label, sse, 0.1)
y2 = my_nn.forward(a)
print("訓練前:",y1) # 訓練前: [[0.54831853 0.54148219]]
print("訓練後:",y2) # 訓練後: [[0.99493565 0.00505818]]
print("答案:",label) # 答案: [[1 0]]