用numpy库手写算子二: Conv2d_backward
前言
我们经常可以调用pytorch,tensorflow库等来实现我们的神经网络,但是有的时候需要开发自己的框架,这个时候就得了解每一个算子的计算规则,了解这些计算规则也有助于我们了解他们的计算特性,然后就可以在底层优化上面有一定的针对性。
Conv2d_backward
一个算子的求导,包括两个方面:对Filter求导,这部分的求导作为之后更新梯度的梯度;对Input求导,也就是对每层的Featurre_map进行求导,作为链式求导中的一环向前面的神经层传导。
输入说明
对于conv2d的反向而言:
默认dout,也就是从后面的层反传回来的,对应正向的本层的feature_map的梯度,这部分梯度是作为链式传导过程中的中间变量.数据布局为(N,C,H,W),N代表batch_size,C代表input_channel,H代表input_height,W代表input_width.
默认x数据布局为(N,C,H,W),N代表batch_size,C代表input_channel,H代表input_height,W代表input_width。
关于x和dout的关系,dout相当于正向过程中,根据x和w算出来的本层的feature_map的导数。
w代表正向的本层的filter,默认数据布局为(N,C,H,W),N代表channel_multiplier,C代表input_channel,H代表kernel_height,W代表kernel_width。
stride代表的是(stride__h,stride_w)。
pad代表的是(pad_h,pad_w)。
对Filter求导
本层的filter的导数的形状和w的形状大小是一致。对于一个(ko,ki,kh,kw)的卷积核,其梯度大小相当于反向传导回来的dout和前向的input做正常的卷积计算,累加的轴是batch_size。
对Input求导
本层对input的求导(x),首先是要根据前向的stride的数值,进行dilate,也就是相当于给dout补0,具体参考是dilate_python。dilate之后的dout,根据前向的pad,给边界补0。然后将所有的filter进行翻转180度。最后将dilate+边界补0后的dout和旋转180度的filter,做正常的卷积得到对本层x的梯度。将求的梯度反向传导,作为链式求导过程的中间变量。
总体代码
import numpy as np
def dilate_python(input_np, strides):
"""Dilate operation.
Parameters
----------
input_np : numpy.ndarray
n-D, can be any layout.
strides : list / tuple of n ints
Dilation stride on each dimension, 1 means no dilation.
Returns
-------
output_np : numpy.ndarray
n-D, the same layout as Input.
if stride = 1,input = output
if stride = 2.input = [[[[1. 1.][1. 1.]][[1. 1.][1. 1.]]]],output = [[[[1. 0. 1.][0. 0. 0.][1. 0. 1.]]
[[1. 0. 1.]
[0. 0. 0.]
[1. 0. 1.]]]]
"""
n = len(input_np.shape)
assert len(strides) == n, \
"Input dimension and strides size dismatch : %d vs %d" %(n, len(strides))
output_size = ()
no_zero = ()
for i in range(n):
output_size += ((input_np.shape[i]-1)*strides[i]+1,)
no_zero += ((range(0, output_size[i], strides[i])),)
output_np = np.zeros(shape=output_size)
output_np[np.ix_(*no_zero)] = input_np
return output_np
def flip180(arr):
new_arr = arr.reshape(arr.size)
new_arr = new_arr[::-1]
new_arr = new_arr.reshape(arr.shape)
return new_arr
def my_dwbackward_naive(dout, x, w, stride, pad):
ko, ki, kh, kw = w.shape
xpad = np.pad(x,((0,0),(0,0),(pad,pad),(pad,pad)),'constant')
wgrad = np.zeros((ko, ki, kh, kw))
# print(xpad)
for i in range(w.shape[0]):
for dc in range(w.shape[1]):
for fh in range(w.shape[2]):
for fw in range(w.shape[3]):
for dh in range(dout.shape[2]):
for dw in range(dout.shape[3]):
for q in range(dout.shape[0]):
wgrad[i,dc,fh,fw] += xpad[q,i,fh + dh * stride, fw + dw * stride] * dout[q, i, dh, dw ]
dialtedDout = dilate_python(dout,[1,1,stride,stride])
n, oc, oh, ow = dialtedDout.shape
padTop = kh - 1 - pad
padBottom = kh - 1 - pad + stride - 1
padLeft = kw - 1 - pad
padRight = kw - 1 -pad + stride - 1
padDout = np.zeros((n, oc, oh + padTop + padBottom, ow + padLeft + padRight))
padDout[:,:, padTop : padTop + oh, padLeft : padLeft + ow] = dialtedDout
dx = np.zeros(x.shape)
for i in range(w.shape[0]):
for j in range(w.shape[1]):
w[i,j,:,:] = flip180(w[i,j,:,:])
n, ic, ih, iw = dx.shape
for i in range(n):
for j in range(ic):
for t in range(ih):
for l in range(iw):
dx[i,j,t,l] += np.sum(w[j,:,:,:] * padDout[i,j,t : t + kh,l:l + kw])
print("returning dx,dw")
return dx,wgrad