TensorFlow实现LeNet-5的C3层:非全连接特征图配置问题
解决LeNet-5 C3层的定制连接问题
首先,你遇到的核心问题是:LeNet-5的C3层并非和S2层全连接,而是每个输出特征图只连接到S2层的特定输入特征图子集,以此减少参数数量并引入局部感受野的多样性。你之前的全连接方式参数是5×5×6×16+16=2416,而论文要求的1516参数正是这种定制连接带来的结果。
先解释你更新1里的错误:你用tf.split(C3_w, [10,6], axis=1)时,axis=1对应的是卷积核的高度维度(大小为5),你要拆分的尺寸总和16≠5,自然会报错。而且这种简单拆分也无法实现论文里的复杂连接规则,我们需要用连接掩码来精确控制哪些输入输出通道相连。
方法一:使用掩码过滤可训练权重
这是最直观的实现方式,先定义一个掩码矩阵标记有效连接,再将可训练权重与掩码相乘,只有掩码为1的位置保留可训练参数:
步骤1:定义C3层的连接掩码
根据论文第8页的表I,C3层16个输出特征图的连接规则如下,我们把它转换成二进制掩码:
import numpy as np import tensorflow as tf mu = 0 sigma = 0.1 # 定义[输入通道数, 输出通道数]的掩码矩阵 c3_mask = np.zeros((6, 16), dtype=np.float32) # 前6个输出:每个连接3个相邻的S2输入特征图 for i in range(6): c3_mask[(i, (i+1)%6, (i+2)%6), i] = 1.0 # 中间6个输出:每个连接4个相邻的S2输入特征图 for i in range(6): c3_mask[(i, (i+1)%6, (i+2)%6, (i+3)%6), i+6] = 1.0 # 接下来3个输出:每个连接4个不相邻的S2输入特征图 c3_mask[(0,1,4,5), 12] = 1.0 c3_mask[(1,2,5,0), 13] = 1.0 c3_mask[(2,3,0,1), 14] = 1.0 # 最后1个输出:连接所有6个S2输入特征图 c3_mask[:, 15] = 1.0 # 将掩码扩展为卷积核形状[5,5,6,16](卷积核高×宽×输入通道×输出通道) c3_mask = np.tile(c3_mask, (5,5,1,1))
步骤2:应用掩码构建C3层
将可训练权重与掩码相乘,这样只有有效连接的位置保留可训练参数:
def LeNet(x): # C1层(你的原有代码) C1_w = tf.Variable(tf.truncated_normal(shape=[5,5,1,6], mean=mu, stddev=sigma)) C1_b = tf.Variable(tf.zeros(6)) C1 = tf.nn.conv2d(x, C1_w, strides=[1,1,1,1], padding='VALID') + C1_b C1 = tf.nn.sigmoid(C1) # S2层(你的原有代码) S2 = tf.nn.avg_pool(C1, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID') S2 = tf.nn.sigmoid(S2) # C3层(修改后的实现) # 初始化完整的可训练权重 C3_w_raw = tf.Variable(tf.truncated_normal(shape=[5,5,6,16], mean=mu, stddev=sigma)) # 用掩码过滤无效连接,只保留有效权重 C3_w = C3_w_raw * tf.constant(c3_mask) C3_b = tf.Variable(tf.zeros(16)) # 计算卷积与激活 C3 = tf.nn.conv2d(S2, C3_w, strides=[1,1,1,1], padding='VALID') + C3_b C3 = tf.nn.sigmoid(C3) # 后续层可以继续按论文实现... return C3
这种方式的参数数量完全符合论文要求:
- 前6个输出:6×(5×5×3) = 450
- 中间6个输出:6×(5×5×4) = 600
- 接下来3个输出:3×(5×5×4) = 300
- 最后1个输出:1×(5×5×6) = 150
- 偏置:16
总和:450+600+300+150+16 = 1516
方法二:逐通道构建并拼接(更节省内存)
如果想避免存储无效的0权重,可以为每个输出通道单独构建卷积,再拼接结果:
def LeNet(x): # C1和S2层代码同上... # C3层:逐通道构建 C3_b = tf.Variable(tf.zeros(16)) c3_outputs = [] # 前6个输出:3个相邻输入通道 for i in range(6): input_channels = [i, (i+1)%6, (i+2)%6] # 提取S2的对应输入通道 s2_slice = tf.gather(S2, input_channels, axis=3) # 初始化对应权重 w = tf.Variable(tf.truncated_normal(shape=[5,5,len(input_channels),1], mean=mu, stddev=sigma)) # 计算卷积 conv = tf.nn.conv2d(s2_slice, w, strides=[1,1,1,1], padding='VALID') c3_outputs.append(conv) # 中间6个输出:4个相邻输入通道 for i in range(6): input_channels = [i, (i+1)%6, (i+2)%6, (i+3)%6] s2_slice = tf.gather(S2, input_channels, axis=3) w = tf.Variable(tf.truncated_normal(shape=[5,5,len(input_channels),1], mean=mu, stddev=sigma)) conv = tf.nn.conv2d(s2_slice, w, strides=[1,1,1,1], padding='VALID') c3_outputs.append(conv) # 接下来3个输出:4个不相邻输入通道 input_sets = [[0,1,4,5], [1,2,5,0], [2,3,0,1]] for input_channels in input_sets: s2_slice = tf.gather(S2, input_channels, axis=3) w = tf.Variable(tf.truncated_normal(shape=[5,5,len(input_channels),1], mean=mu, stddev=sigma)) conv = tf.nn.conv2d(s2_slice, w, strides=[1,1,1,1], padding='VALID') c3_outputs.append(conv) # 最后1个输出:所有6个输入通道 w = tf.Variable(tf.truncated_normal(shape=[5,5,6,1], mean=mu, stddev=sigma)) conv = tf.nn.conv2d(S2, w, strides=[1,1,1,1], padding='VALID') c3_outputs.append(conv) # 拼接所有输出通道并添加偏置 C3 = tf.concat(c3_outputs, axis=3) + C3_b C3 = tf.nn.sigmoid(C3) # 后续层... return C3
这种方式不需要存储无效的0权重,内存利用率更高,参数数量和方法一完全一致。
内容的提问来源于stack exchange,提问作者Alejandro Calle
相关产品推荐
相关产品推荐

