参考《深度学习入门》
卷积层
在二维图像上,卷积操作一方面可以高效地按照我们的需求提取图像的领域信息,在全局上又有着非常好的平移特性。接下来我们看卷积层的实现,im2col实现参见论文High Performance Convolutional Neural Networks for Document Processing,这里还有一篇讲的比较通俗的文章,可供参考:im2col方法实现卷积算法。
下面我们来看如何使用纯python代码来实现卷积操作,这里我们使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | def im2col(input_data, filter_h, filter_w, stride=1, pad=0): N, C, H, W = input_data.shape out_h = (H + 2*pad - filter_h)//stride + 1 out_w = (W + 2*pad - filter_w)//stride + 1 #在4维tensor(数据量,通道,高,长)的两个维度:高和长 上进行常量填充 img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant') #col.shape=[N,C,filter_h,filter_w,out_h,out_w] col = np.zeros((N, C, filter_h, filter_w, out_h, out_w)) for y in range(filter_h): #从y到y_max表示窗口在h方向上滑动可以出现的起始位置 y_max = y + stride*out_h for x in range(filter_w): x_max = x + stride*out_w #可以理解为处于初试位置(未滑动)卷积核对应img上的每个元素滑动时可以取到的值,shape=[out_h, out_w] #col中的y,x就是用来定位img上的元素,其shape=[filter_h, filter_w] col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride] #col变换前可以通过指定某个批数据、某个通道、某个元素来获取 #shape=[out_h, out_w]形状的滑动时可取到的值 #col.transpose(0,4,5,1,2,3)就是依次访问N,out_h,out_w,channel,y,x,也就是说,我们先指定一个批数据, #然后指定range(out_h),range(out_w)来指定某个滑动位置,基于该滑动位置,指定一个通道即可访问该位置的 #待卷积元素,shape=[filter_h, filter_w] #.reshape(N*out*h*out_w, -1)即可使matrix每一行按照channel、y、x依次展开成一维 col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1) return col |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0): N, C, H, W = input_shape out_h = (H + 2*pad - filter_h)//stride + 1 out_w = (W + 2*pad - filter_w)//stride + 1 #col.shape=[N*out_h*out_w, C*filter_h*filter_w] col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2) #col.shape=[N, C, filter_h, filter_w, out_h, out_w] #注释掉了原来的语句,不懂为什么要这么写 #img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1)) img = np.zeros((N, C, H + 2*pad, W + 2*pad)) for y in range(filter_h): y_max = y + stride*out_h for x in range(filter_w): x_max = x + stride*out_w #img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :] img[:, :, y:y_max:stride, x:x_max:stride] = col[:, :, y, x, :, :] #得到填充之前的梯度 return img[:, :, pad:H + pad, pad:W + pad] |
最后我们以类的形式实现卷积层,包含初始化参数、前向传递以及误差反向传播部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #卷积层的实现 class Convolution: #卷积层我们需要指定卷积核、偏置、卷积步长以及填充大小 def __init__(self, W, b, stride=1, pad=0): self.W = W self.b = b self.stride = stride #根据不同的卷积模式指定pad大小 self.pad = pad # 中间数据(backward时使用) self.x = None self.col = None self.col_W = None # 权重和偏置参数的梯度 self.dW = None self.db = None def forward(self, x): #FN表示卷积核的个数,C表示channel数,FH=filter_h, FW=filter_w FN, C, FH, FW = self.W.shape N, C, H, W = x.shape out_h = 1 + int((H + 2*self.pad - FH) / self.stride) out_w = 1 + int((W + 2*self.pad - FW) / self.stride) #将input_feature转成二维矩阵,shape=[N*out_h*out_w, channel*FH*FW] col = im2col(x, FH, FW, self.stride, self.pad) #将卷积核转成二维矩阵,self.W.reshape(FN, -1)将W转成FN * (C * FH * FW)的二维矩阵 #col_W.shape=[C*FH*FW, FN] col_W = self.W.reshape(FN, -1).T #二维矩阵相乘得到shape=[N*out_h*out_w, FN]的矩阵(矩阵上每一个元素都是对应区域与卷积核的卷积结果, #多通道),对于每个卷积核都对应一个偏置,因此需要+self.b out = np.dot(col, col_W) + self.b #out.reshape(N,out_h,out_w,-1)得到一个四维张量(N,out_h,out_w,FN) #.transpose(0,3,1,2)得到四维张量(N,FN,out_h,out_w)即为卷积输出 out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) self.x = x self.col = col self.col_W = col_W return out #反向传播的实现,输入dout.shape=[N,FN, out_h,out_w] def backward(self, dout): FN, C, FH, FW = self.W.shape #forward的逆操作,很好理解 dout = dout.transpose(0,2,3,1).reshape(-1, FN) #参考通过计算图来计算Affline层的反向传播 self.db = np.sum(dout, axis=0) #根据公式计算得到关于卷积核的梯度,二维矩阵,需要重整成卷积核原来的形状 #col_W = self.W.reshape(FN, -1).T的逆操作 self.dW = np.dot(self.col.T, dout) self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW) #dout.shape=[N*out_h*out_w, FN], self.col_W.shape=[C*FH*FW, FN] dcol = np.dot(dout, self.col_W.T) #需要将关于输入x展开后的二维矩阵的梯度重整为与x具有相同的形状 dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad) return dx |
池化层
池化层的特征如下:
- 没有要学习的参数
- 通道数不发生变化
- 对微小的位置变化具有鲁棒性
池化层的实现和卷积层相同,都使用
池化层的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | class Pooling: def __init__(self, pool_h, pool_w, stride=1, pad=0): #pool_h,pool_w为池化区域的大小 self.pool_h = pool_h self.pool_w = pool_w self.stride = stride self.pad = pad #用于反向传播 self.x = None self.arg_max = None def forward(self, x): N, C, H, W = x.shape out_h = int(1 + (H - self.pool_h) / self.stride) out_w = int(1 + (W - self.pool_w) / self.stride) col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) #执行im2col后,col.shape=[N*out_h*out_w, C*self.pool_h*self.pool_w] #依次按照批大小,平移位置,通道展开 col = col.reshape(-1, self.pool_h*self.pool_w) arg_max = np.argmax(col, axis=1) out = np.max(col, axis=1) #out.shape=[N,C,out_h,out_w],即为各通道独立求max out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2) self.x = x self.arg_max = arg_max return out def backward(self, dout): dout = dout.transpose(0, 2, 3, 1) #dout.shape=[N,out_h,out_w,C] pool_size = self.pool_h * self.pool_w #dout.size=N*out_h*out_w*C,每一个池化后的元素对应原来的pool_size个池化区域中的元素 dmax = np.zeros((dout.size, pool_size)) #flatten将张量转成一维 dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten() #dmax.shape=[N,out_h,out_w,C,pool_size] dmax = dmax.reshape(dout.shape + (pool_size,)) #dcol=[N*out_h*out_w, C*pool_size] dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1) dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad) return dx |