Keras多输出自编码器训练时维度消失问题排查
看起来你遇到的形状不匹配问题,根源是解码器的裁剪逻辑错误加上编码器与解码器的形状衔接不当导致的连锁反应,我来一步步帮你拆解:
为什么会出现(None, 0, 28, 1)的异常输出?
咱们先顺着数据流走一遍:
1. 编码器输出形状不符合解码器预期
你的编码器里用了MaxPooling2D(pool_size=(2,1)),这个操作会把特征图的高度维度从28压缩到14(28÷2),宽度保持28不变。所以编码器最终输出的形状是(14,28,1),但解码器却定义输入为Input(shape=(28,28,1))——两者高度维度不匹配。
2. 解码器的裁剪操作直接把高度裁成了0
虽然你用UpSampling2D((2,1))把编码器输出的高度从14还原回了28,但接下来的裁剪逻辑完全错了:
crop1 = Cropping2D(cropping = ((0,28),(0,0)))(convDecode1)
Cropping2D的参数格式是((顶部裁剪行数, 底部裁剪行数), (左侧裁剪列数, 右侧裁剪列数)),你这里给底部裁剪28行,而convDecode1的高度刚好是28,裁剪后高度直接变成28-28=0!这就是错误提示里(None,0,28,1)的由来。
修复步骤
步骤1:对齐编码器与解码器的形状
先让编码器输出和解码器输入的形状保持一致:
- 如果你想保留编码器的高度压缩逻辑,就把解码器的输入改成
Input(shape=(14,28,1)); - 如果你不需要高度压缩,就把编码器的
MaxPooling2D(pool_size=(2,1))改成(1,1)(或者直接去掉池化)。
步骤2:修正解码器的裁剪逻辑
看你的代码,应该是想从编码后的特征还原出两个和输入同形状的张量,完全不需要裁剪操作——上采样后的特征图已经是(28,28,16)了,直接分两个分支卷积输出即可。如果确实需要裁剪(比如拆分特征图),也要用合理的裁剪参数,比如想拆成上下各14行:
# 裁剪上半部分14行(顶部不裁,底部裁14行) crop1 = Cropping2D(cropping=((0,14),(0,0)))(convDecode1) # 裁剪下半部分14行(顶部裁14行,底部不裁) crop2 = Cropping2D(cropping=((14,0),(0,0)))(convDecode1)
步骤3:移除子模型的多余编译
编码器和解码器作为自编码器的子模型,不需要单独调用model.compile()——自编码器的编译会统一处理损失函数和优化器,单独编译子模型毫无意义。
修正后的完整代码示例
def buildEncoder(): input1 = Input(shape=(28,28,1)) input2 = Input(shape=(28,28,1)) merge = concatenate([input1,input2]) convEncode1 = Conv2D(16, (3,3), activation='relu', padding='same')(merge) # 保留高度压缩的话,用这个池化 maxPoolEncode1 = MaxPooling2D(pool_size=(2,1))(convEncode1) # 不需要压缩的话,换成这个:maxPoolEncode1 = MaxPooling2D(pool_size=(1,1))(convEncode1) convEncode2 = Conv2D(16, (3,3), activation='sigmoid', padding='same')(maxPoolEncode1) convEncode3 = Conv2D(1, (3,3), activation='sigmoid', padding='same')(convEncode2) model = Model(inputs=[input1,input2], outputs=convEncode3) # 移除多余的compile return model def buildDecoder(): # 对应编码器输出的(14,28,1),如果编码器没压缩就改成(28,28,1) input1 = Input(shape=(14,28,1)) upsample1 = UpSampling2D((2,1))(input1) convDecode1 = Conv2D(16, (3,3), activation='relu', padding='same')(upsample1) # 直接分两个分支输出,不需要裁剪 convDecode2_1 = Conv2D(16, (3,3), activation='relu', padding='same')(convDecode1) convDecode3_1 = Conv2D(16, (3,3), activation='relu', padding='same')(convDecode1) convDecode2_2 = Conv2D(1, (3,3), activation='sigmoid', padding='same')(convDecode2_1) convDecode3_2 = Conv2D(1, (3,3), activation='sigmoid', padding='same')(convDecode3_1) model = Model(inputs=input1, outputs=[convDecode2_2,convDecode3_2]) # 移除多余的compile return model def buildAutoencoder(): autoInput1 = Input(shape=(28,28,1)) autoInput2 = Input(shape=(28,28,1)) encoder = buildEncoder() decoder = buildDecoder() encode = encoder([autoInput1,autoInput2]) decode = decoder(encode) model = Model(inputs=[autoInput1,autoInput2], outputs=[decode[0],decode[1]]) model.compile(loss='binary_crossentropy', optimizer='adam') return model
现在你再运行model.summary(),就能看到输出形状和目标形状一致了。
内容的提问来源于stack exchange,提问作者Harrison Rose

