ResNet残差块1x1卷积适配Add层张量形状的原理问询
假设我有一组形状为(None, 188, 1)的加速度计时序数据,需要完成分类任务,尝试自行实现ResNet模型,对应实现代码如下:
def residual_block(inputs, filters, kernel_size, shortcut): out = tf.keras.layers.Conv1D(filters=filters, kernel_size=kernel_size, strides=1, padding='causal')(inputs) out = tf.keras.layers.ReLU()(out) out = tf.keras.layers.Conv1D(filters=filters, kernel_size=kernel_size, strides=1, padding='causal')(out) if shortcut: out = tf.keras.layers.Conv1D(1, 1, padding="causal")(out) out = tf.keras.layers.add([inputs, out]) out = tf.keras.layers.ReLU()(out) out = tf.keras.layers.MaxPool1D()(out) return out def resnet_model(n_timesteps, n_features, n_classes): filters_1 = 16 kernel_size_1 = 10 filters_2 = 32 kernel_size_2 = 10 # head 1 input1 = tf.keras.layers.Input(shape=(n_timesteps, n_features)) res_block1_1 = residual_block(input1, filters_1, kernel_size_1, shortcut=False) res_block1_2 = residual_block(res_block1_1, filters_2, kernel_size_2, shortcut=True) flatten1 = tf.keras.layers.Flatten()(res_block1_2) dense1 = tf.keras.layers.Dense(512, activation='relu')(flatten1) drop1 = tf.keras.layers.Dropout(0.3)(dense1) dense2 = tf.keras.layers.Dense(128, activation="relu")(drop1) drop2 = tf.keras.layers.Dropout(0.3)(dense2) outputs = tf.keras.layers.Dense(n_classes, activation="softmax")(drop2) model = tf.keras.Model(inputs=[input1, input2, input3], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer=tf.keras.optimizers.Adam(), metrics=['accuracy']) return model
最初仅使用单个残差块时模型运行无异常,加入第二个残差块后出现报错。梳理数据流向可知:
- 第一个残差块的
Add层接收的两个输入形状分别为:来自原始输入的(None, 188, 1)、来自卷积分支的(None, 188, 16),相加操作可正常执行 - 第二个残差块的
Add层触发形状不匹配错误,接收的两个输入分别为:来自前序MaxPooling层的(None, 94, 16)、来自卷积分支的(None, 94, 8)
尝试加入1x1 Convolution修复该问题,但不理解修复逻辑:根据ResNet设计原理,残差相加要求Add层的两个输入形状完全一致,因此通常使用1x1卷积做维度适配,但当前代码中的1x1卷积处理后得到的张量形状为(None, 94, 1),依然和shortcut分支传入的(None, 94, 16)形状不匹配。
核心疑问:当前代码中的1x1卷积是如何解决形状不匹配问题的?1x1卷积和Add层配合实现残差连接的正确逻辑是什么?
首先直接给结论:你代码里写的1x1卷积根本没有正确解决形状不匹配问题,第一个残差块能跑通完全是依赖TensorFlow的广播机制误打误撞生效,属于不符合ResNet设计规范的错误实现。
第一个残差块能运行的本质原因
你观察到第一个块的原始输入形状是(None,188,1)、卷积分支输出形状是(None,188,16)还能相加,不是代码写对了,是广播机制自动把最后一维长度为1的张量复制16次,和16通道的张量对齐后完成计算。这种操作完全破坏了残差连接的语义,相当于把单通道输入强行复制到所有通道做相加,和ResNet的残差设计逻辑没有关系。
当前1x1卷积的逻辑错误
残差连接用1x1卷积做形状适配的核心规则是:必须保证送入Add层的两个张量在所有维度上长度完全相等,1x1卷积的作用是调整通道数,通常加在shortcut直连分支上,输出通道数要和卷积分支的最终输出通道数完全匹配;如果卷积步长导致时间步长度变化,还要给1x1卷积设相同步长对齐时间步维度。
你现在的实现把1x1卷积加在了卷积分支上,还硬编码输出通道为1,相当于把卷积分支的通道数压回1,再靠广播机制和shortcut分支的16通道张量相加,本质和第一个块的错误逻辑完全一致,只是把适配方向反过来了,根本不是正确的维度适配。另外你提到第二个块卷积分支输出是(None,94,8)也不符合预期——你两层Conv1D都设了32个滤波器,正常输出通道应该是32,这个数值偏差说明代码还有其他隐藏的运行问题。
正确的残差块1x1卷积适配写法
1x1卷积要放在shortcut路径上,仅当输入和卷积分支输出形状不匹配时启用,输出通道数和卷积分支通道数保持一致,修正后的残差块参考代码如下:
def residual_block(inputs, filters, kernel_size, need_shortcut_conv): # 主卷积分支 out = tf.keras.layers.Conv1D(filters=filters, kernel_size=kernel_size, strides=1, padding='causal')(inputs) out = tf.keras.layers.ReLU()(out) out = tf.keras.layers.Conv1D(filters=filters, kernel_size=kernel_size, strides=1, padding='causal')(out) # shortcut分支:形状不匹配时用1x1卷积对齐 if need_shortcut_conv: shortcut = tf.keras.layers.Conv1D(filters=filters, kernel_size=1, padding="causal")(inputs) else: shortcut = inputs # 两个分支形状完全对齐后再相加 out = tf.keras.layers.add([shortcut, out]) out = tf.keras.layers.ReLU()(out) out = tf.keras.layers.MaxPool1D()(out) return out
另外你现有代码还有两个会直接导致运行报错的低级问题:
- 模型构造时
Model的输入参数写了[input1, input2, input3],但你只定义了input1这一个输入层,运行时会直接抛出input2未定义的错误 - 你把MaxPool层放在残差相加操作之后,每经过一个块时间步长度就会减半,如果后续残差块用步长大于1的卷积做下采样,还要在shortcut的1x1卷积上设置对应步长,保证时间步维度也完全对齐,不能依赖广播机制凑运行结果。
内容的提问来源于stack exchange,提问作者DPM

