☰
四种主流深度学习架构实战:CNN、Transformer、GAN与胶囊网络全解析
2026/10/15 23:15:16 网站建设 项目流程

1. 开篇:一次把四种主流架构全部过一遍

如果你看过这个系列的第一篇,应该还记得我们当时用 Keras 把基础的全连接网络和卷积网络从头到尾搭了一遍,核心就是在“动手写代码”这个层面,把一些看起来很吓人的概念落到实际可运行的模型上。这一篇我们继续往前走,目标很明确——把 CNN、Transformer、GAN 和胶囊网络这四种架构放在同一套 TensorFlow + Keras 环境里,逐一拆开揉碎,搞明白它们各自解决什么问题、网络结构怎么搭、训练时有哪些坑,以及哪些场景真的值得用它们。

这篇内容最适合两类人:一类是已经会用 Keras 写简单模型,但面对“Transformer 到底怎么实现”“GAN 训练为什么总是不收敛”“胶囊网络到底是干什么的”这类问题时心里没底的同学;另一类是准备面试或做课程设计,需要快速理解主流模型内部原理,并且能动手写代码的人。我会尽可能把每一步代码背后“为什么这么做”也讲清楚,而不是丢一段能跑就完事的代码。

先说一句总结性的话:这四种架构本质上是在不同维度上做特征提取和表示学习。CNN 擅长局部模式识别,Transformer 擅长长距离依赖建模,GAN 用对抗博弈驱动生成,胶囊网络试图解决 CNN 丢失空间层次关系的问题。理解了这一点,你再看它们的代码,就会觉得顺很多。

2. 四种架构的核心差异与选型思路

2.1 CNN:在局部窗口里找模式

卷积神经网络的核心假设是:图像或者信号中相邻区域的特征往往高度相关。比如一张猫脸照片,耳朵、眼睛、鼻子这些器官是由局部像素组合而成的,不需要看整张图才能识别出耳朵。卷积操作就是用一个小窗口(比如 3×3 或 5×5)扫过整张特征图,在每个局部位置做加权求和,把局部模式提取出来。

在 Keras 里,一个标准的卷积块通常长这样:

from tensorflow.keras import layers, models def conv_block(x, filters, kernel_size=3): x = layers.Conv2D(filters, kernel_size, padding='same')(x) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) x = layers.MaxPooling2D(pool_size=(2, 2))(x) return x

这个块做了四件事:卷积提取特征、归一化稳定训练、激活函数引入非线性、池化降低分辨率。很多人写 CNN 时容易忽略 BatchNormalization 的位置——放激活之前还是之后都有讲究。我自己的习惯是先卷积、再 BN、再激活,这样 BN 能更直接地规约卷积输出的分布,训练更稳定。

选型时要知道 CNN 的边界在哪里:它擅长捕捉局部关系,但如果两个关键特征距离很远,需要堆很多层才能把感受野撑大,而且训练难度也会增加。这也是为什么后面会出现 Transformer 和胶囊网络。

2.2 Transformer:关注全局关系

Transformer 和 CNN 最大的不同在于,它一开始就不假设数据有局部的空间结构。它的核心机制是自注意力(Self-Attention),让序列里的每一个位置都能直接和序列里所有其他位置计算相关性。比如处理一句话时,“它”指代的是前面的“猫”还是“狗”,模型可以通过注意力权重自己学出来,不需要像 RNN 那样一步步传递信息。

用 Keras 实现一个最简 Transformer 编码器,核心代码可以这样写:

class TransformerBlock(layers.Layer): def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1): super().__init__() self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim) self.ffn = models.Sequential([ layers.Dense(ff_dim, activation='relu'), layers.Dense(embed_dim), ]) self.layernorm1 = layers.LayerNormalization(epsilon=1e-6) self.layernorm2 = layers.LayerNormalization(epsilon=1e-6) self.dropout1 = layers.Dropout(rate) self.dropout2 = layers.Dropout(rate) def call(self, inputs, training=False): attn_output = self.att(inputs, inputs) attn_output = self.dropout1(attn_output, training=training) out1 = self.layernorm1(inputs + attn_output) ffn_output = self.ffn(out1) ffn_output = self.dropout2(ffn_output, training=training) return self.layernorm2(out1 + ffn_output)

这段代码里有一个容易忽略但很关键的细节:注意力输出之后要先经过 Dropout,再做残差连接和 LayerNorm。顺序颠倒或者漏掉残差,Transformer 很容易训练崩掉。第二个细节是 LayerNormalization 是对每个样本的特征维度做归一化,而不是像 BatchNormalization 那样跨样本做归一化,这在 NLP 和序列任务中更稳定。

选型时,Transformer 的强项是长距离建模,弱点是计算量随序列长度平方增长。处理图像时如果直接展平成序列,一张 224×224 的图就有 5 万多个像素点,实际运算量会非常惊人,所以视觉 Transformer 通常先切 patch 再处理。

2.3 GAN:生成器与判别器的对抗博弈

GAN 的思路特别直观:有两个网络互相博弈。生成器负责“造假”,判别器负责“验真”。生成器想把随机噪声变成以假乱真的样本,判别器则努力分辨真实样本和生成样本。两者不断对抗,最后生成器学到的分布会越来越接近真实数据的分布。

在 Keras 里,一个最简单的 GAN 训练循环包含三个模型:生成器、判别器、以及把两者拼接起来的对抗模型。

generator = models.Sequential([ layers.Dense(256, input_shape=(latent_dim,)), layers.LeakyReLU(alpha=0.2), layers.BatchNormalization(), layers.Dense(512), layers.LeakyReLU(alpha=0.2), layers.BatchNormalization(), layers.Dense(28 * 28 * 1, activation='sigmoid'), layers.Reshape((28, 28, 1)), ])

生成器里我故意用了 LeakyReLU 而不是普通 ReLU,这是因为 ReLU 会把负数全部置零,导致梯度稀疏,生成器容易陷入“只会输出重复样本”的模式坍塌状态。至于 BatchNormalization,可以稳定生成器内部特征的分布,让训练过程平滑很多。

判别器是二分类模型,输出一个 0 到 1 之间的真假分数。但训练时有一个细节:判别器不能训练得太好,否则它一眼就识破所有生成样本,梯度消失,生成器学不到任何东西。所以经验上判别器和生成器的训练节奏要交替进行,判别器每训两步,生成器才训一步。

2.4 胶囊网络:试图给 CNN 补上“空间关系课”

胶囊网络这个概念最早是为了解决 CNN 的一个先天不足:CNN 通过多次池化不断压缩空间尺寸,虽然获得了更大的感受野,却也丢失了物体部件之间的精确位置关系。比如一张图里“脸”在“身体”上面,这种空间位置关系对 CNN 来说并不敏感,只要局部特征都出现了,它就可能判定为同一个类别。

胶囊网络的做法是:用一组向量神经元(也就是胶囊)来表示一个实体,向量的长度表示实体存在的概率,向量的方向表示实体的姿态属性(位置、角度、尺度等)。层与层之间用动态路由算法来传递信息,让上层胶囊自动决定接收下层哪些胶囊的信息。

胶囊网络代码比 CNN 复杂不少,后面我会详细拆解它的关键部分。这里先提醒一句:胶囊网络目前更多是实验性应用,训练成本高、对超参敏感,工业落地的案例远不如 CNN 和 Transformer 多。如果你想在简历或课程设计里展示学习深度,它是个很好的话题点,但真要上生产环境,还是得谨慎。

3. CNN 实战:从零搭一个能用的图像分类模型

3.1 数据准备和增强策略

实战部分我用一个模拟的图像分类数据集来演示,假设有 10 个类别,每类 5000 张 64×64 的彩色图片。数据量不大,所以数据增强是必须的。

from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.15, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest', validation_split=0.2, )

注意,数据增强是在线进行的,不是在训练前一次性生成一批增强图片并存储。这样每个 epoch 模型看到的都是不同的变换版本,相当于隐式地扩大了数据集。轮转角度不要设置太大,20 度以内比较合理,过大的角度会让语义本身发生变化。比如数字“6”旋转 180 度就变成“9”,标签就错了。

3.2 网络结构设计与参数选择

我们设计一个浅但有效的 CNN:

def build_cnn(input_shape=(64, 64, 3), num_classes=10): inputs = layers.Input(shape=input_shape) x = conv_block(inputs, 32) x = conv_block(x, 64) x = conv_block(x, 128) x = layers.GlobalAveragePooling2D()(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(num_classes, activation='softmax')(x) model = models.Model(inputs, outputs) return model

这里有几个刻意的设计。第一,用 GlobalAveragePooling2D 而不是直接 Flatten 再接全连接层,前者能把每个通道的特征图压成一个值,大幅减少参数量,也降低过拟合风险。第二,最后一个卷积块输出 128 个通道,然后接一个 0.5 丢弃率的 Dropout。CNN 的参数量主要集中在最后的全连接层和较深的卷积层,Dropout 在这里效果最明显。

编译参数上,优化器我推荐用 Adam,初始学习率 1e-3。如果训练过程中验证集准确率长时间停滞,可以动态降低学习率:

lr_scheduler = tf.keras.optimizers.schedules.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6)

这里补充一个我自己踩过的坑:如果直接用 ImageDataGenerator 的 flow_from_directory 加载数据,一定要把 class_mode 设置成正确的类型,分类任务用 ‘categorical’,单标签二分类可以用 ‘binary’。我第一次跑的时候没注意,默认值不对,损失函数计算直接报错,排查了半天。

3.3 训练监控与过拟合判断

训练时建议同时监控训练集和验证集的指标。判断过拟合的一个实用标准是:训练集准确率持续上升,而验证集准确率在某个 epoch 之后不再增长甚至下降,同时训练损失还在下降,这就是明显的过拟合信号。

应对策略优先级从高到低是:增加数据增强强度(但不改变语义)、增加 Dropout 比例、减小网络容量、加入 L2 正则化。千万不要一上来就换一个更大的预训练模型,那只会让过拟合更严重。等到增强和正则化都用到位还欠拟合,再考虑提高模型容量。

3.4 迁移学习:用预训练权重快速提升效果

如果数据量更少(比如每类只有几百张),从头训练 CNN 很容易过拟合,这时候迁移学习是首选。用 Keras 内置的预训练模型非常方便:

base_model = tf.keras.applications.ResNet50( include_top=False, weights='imagenet', input_shape=(224, 224, 3)) base_model.trainable = False inputs = layers.Input(shape=(224, 224, 3)) x = base_model(inputs, training=False) x = layers.GlobalAveragePooling2D()(x) x = layers.Dropout(0.3)(x) outputs = layers.Dense(num_classes, activation='softmax')(x)

核心细节是 base_model.trainable = False,也就是冻结预训练权重,只训练新增的分类头。这一步能防止预训练权重在训练初期被随机初始化的分类层破坏。等分类头收敛后,如果想进一步微调,可以把 base_model 设置为可训练,并用一个很小的学习率,比如 1e-5,仅解冻后面几层。这个“先冻结再解冻”的两阶段微调策略,实测下来比一步到位效果稳定得多。

4. Transformer 实现:从位置编码到多头注意力

4.1 为什么需要位置编码

Transformer 的注意力机制本身完全不感知顺序。它对一组输入向量计算相关性时,如果把顺序打乱,注意力结果不变。这在处理文本或时序数据时是致命的,因为“我爱你”和“你爱我”含义截然相反。位置编码就是给每个输入向量加上位置信息。

代码实现上,最常见的是用正弦余弦函数生成固定位置编码:

def positional_encoding(length, d_model): pos = tf.range(length, dtype=tf.float32)[:, tf.newaxis] i = tf.range(d_model, dtype=tf.float32)[tf.newaxis, :] angle_rates = 1 / tf.pow(10000.0, (2 * (i // 2)) / d_model) angle_rads = pos * angle_rates angle_rads = tf.where( tf.cast(i % 2, tf.bool), tf.math.cos(angle_rads), tf.math.sin(angle_rads)) return angle_rads[tf.newaxis, ...]

这个设计的巧妙之处在于,不同频率的正弦波组合可以让模型通过线性变换推断出相对位置关系,而且编码向量是固定的、不需要学习,省去了额外参数。在最新的实现里也有人用可学习的位置编码,效果在某些任务上略好,但原理上都是为了把“第几个位置”这个信息注入进去。

4.2 多头注意力的内部流程

多头注意力简单说就是把输入向量投影到多个子空间,分别计算注意力,再拼接起来做一次线性变换。这样模型能同时关注不同层面的相关性:一个头关注语义相似性,另一个头可能关注句法位置关系。

手写这个流程比较繁琐,但 Keras 的 MultiHeadAttention 层已经封装得很好。关键参数只有两个:num_heads 和 key_dim。实际使用时要注意 key_dim 一般取 embed_dim / num_heads,保证多头输出拼接后维度能和输入匹配。比如 embed_dim=128,num_heads=8,那么每个头的 key_dim 就是 16。

在视觉任务里,如果直接用纯 Transformer 模型(比如 ViT),需要先把图像切成固定大小的 patch,每个 patch 展平并做线性投影得到 patch embedding。一个 32×32 的图像,patch_size=4,就得到 64 个 patch,每个 patch 是一个 16×3=48 维的向量,投影到 embed_dim=128 后进入 Transformer 编码器。这个预处理本质上就是把图像转成“序列”,用 Transformer 的方式建模 patch 之间的全局关系。

4.3 用 Keras 搭建一个最小可用的 ViT

下面是一个简化版视觉 Transformer 结构:

def patch_extract(inputs, patch_size): patches = tf.image.extract_patches( images=inputs, sizes=[1, patch_size, patch_size, 1], strides=[1, patch_size, patch_size, 1], rates=[1, 1, 1, 1], padding='VALID') patch_dim = patches.shape[-1] patches = tf.reshape(patches, [-1, patches.shape[1] * patches.shape[2], patch_dim]) return patches def build_vit(input_shape=(64, 64, 3), patch_size=8, embed_dim=128, num_heads=8, ff_dim=256, num_layers=4): inputs = layers.Input(shape=input_shape) patches = patch_extract(inputs, patch_size) x = layers.Dense(embed_dim)(patches) x = x + positional_encoding(x.shape[1], embed_dim) for _ in range(num_layers): x = TransformerBlock(embed_dim, num_heads, ff_dim)(x) x = layers.GlobalAveragePooling1D()(x) outputs = layers.Dense(10, activation='softmax')(x) model = models.Model(inputs, outputs) return model

这段代码可以直接替换前面的 CNN 做图像分类对比实验。我实测过在同样的小数据集上,CNN 收敛更快、准确率更高,因为 CNN 的归纳偏置(局部性、平移等变性)在小数据上天然有优势。Transformer 在大规模数据和充足算力下才能体现优势。所以选型时要结合自己的数据规模,别盲目跟风。

4.4 训练 Transformer 的注意事项

Transformer 对超参数很敏感,尤其注意以下几点:

  • 学习率不能太大,建议从 1e-4 起步,配合 warmup 策略。不看 warmup 直接上大学习率,loss 经常直接变成 NaN。
  • 残差连接和层归一化是标配,不能省。去掉残差连接后深层 Transformer 基本训不动。
  • 序列长度越长,显存占用越高。训练时如果 OOM,优先减小 batch size,其次减少 num_heads 或 embed_dim。
  • 优化器推荐 AdamW(带权重衰减的 Adam),可以配合学习率调度器使用。Keras 里可以直接用 AdamW 优化器。

5. GAN 实战:从 DCGAN 到训练稳定性的调优

5.1 DCGAN 结构设计与关键约束

DCGAN 是 GAN 家族里最经典的卷积版本,用转置卷积(Conv2DTranspose)把低维噪声逐层放大成图像。一个标准的 DCGAN 生成器在 Keras 里长这样:

def build_generator(latent_dim=128): model = models.Sequential(name='generator') model.add(layers.Dense(7 * 7 * 256, input_shape=(latent_dim,))) model.add(layers.BatchNormalization()) model.add(layers.ReLU()) model.add(layers.Reshape((7, 7, 256))) model.add(layers.Conv2DTranspose(128, kernel_size=4, strides=2, padding='same')) model.add(layers.BatchNormalization()) model.add(layers.ReLU()) model.add(layers.Conv2DTranspose(64, kernel_size=4, strides=2, padding='same')) model.add(layers.BatchNormalization()) model.add(layers.ReLU()) model.add(layers.Conv2DTranspose(1, kernel_size=4, strides=2, padding='same', activation='tanh')) return model

注意输出层用 tanh 激活,取值范围在 [-1, 1],所以训练数据也要归一化到 [-1, 1],而不用 [0, 1]。这是我见过很多人犯的错误——生成器输出范围定义和输入数据分布不一致,模型怎么训练都停留在“灰色模糊图”。

判别器是生成器的镜像,用普通卷积逐步压缩空间尺寸,最后输出一个标量真假分数。判别器内部不要用 BatchNormalization,至少不能对每个样本独立做 BN,因为判别器要对单个样本输出真假判断,样本之间数据分布差异本来就很大,BN 反而会引入干扰。用 LeakyReLU 更合适,避免判别器梯度消失。

5.2 训练循环的写法

GAN 训练不能像普通模型那样直接 fit,需要手动管理每一步:

@tf.function def train_step(real_images): batch_size = tf.shape(real_images)[0] noise = tf.random.normal([batch_size, latent_dim]) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: fake_images = generator(noise, training=True) real_logits = discriminator(real_images, training=True) fake_logits = discriminator(fake_images, training=True) disc_loss_real = loss_fn(tf.ones_like(real_logits), real_logits) disc_loss_fake = loss_fn(tf.zeros_like(fake_logits), fake_logits) disc_loss = disc_loss_real + disc_loss_fake gen_loss = loss_fn(tf.ones_like(fake_logits), fake_logits) grads_disc = disc_tape.gradient(disc_loss, discriminator.trainable_variables) grads_gen = gen_tape.gradient(gen_loss, generator.trainable_variables) optimizer_disc.apply_gradients(zip(grads_disc, discriminator.trainable_variables)) optimizer_gen.apply_gradients(zip(grads_gen, generator.trainable_variables))

生成器的损失函数用的是“想让判别器把假图判定为真图”的损失,这是一种“欺骗”式的训练方式。值得注意的是,在计算生成器损失时,通常翻转标签或者使用一种特殊形式,比如用最小化 -log(D(G(z))) 代替 log(1-D(G(z))),原因是后者在生成器初期梯度太平滑,学习速度太慢。

还有一个很实用的设计:把判别器的真实损失和生成器损失分开计算,不要共用一个损失函数,否则梯度回传路径会纠缠在一起,代码调试难度直接上升。

5.3 模式坍塌:GAN 最头疼的问题

模式坍塌指生成器只学会生成几种固定样本,无法覆盖真实数据的多样性。表现形式是:训练过程中生成图片一直重复,且种类极少。常见的缓解经验:

  • 减小学习率,尤其生成器的学习率,让它别那么快锁死到某个模式上。
  • 增加噪声向量的维度,让生成器有更多可变化的自由度。
  • 考虑用 Wasserstein 损失(WGAN),它对模式坍塌的抑制作用明显,但实现时要额外注意判别器权重的裁剪或梯度惩罚,工作量会增加不少。
  • 每隔几个 epoch 手动可视化生成结果,实时观察多样性变化。不要只靠 loss 曲线判断,GAN 的 loss 曲线与生成质量并不是严格对应的。

我自己的实操习惯是:固定随机种子,每训练 5 个 epoch 保存一组生成图。这样既能观察训练进度,也能在出现模式坍塌时快速回退到之前的稳定版本。

5.4 从 DCGAN 到 WGAN-GP 的迁移

如果 DCGAN 训练反复震荡,一个成熟的升级路径是改造成 WGAN-GP(带梯度惩罚的 Wasserstein GAN)。核心改动就两点:一是把判别器最后一层的 sigmoid 去掉,改成输出未归一化的分数;二是损失函数从二元交叉熵换成 Wasserstein 距离的近似形式,并且加入梯度惩罚项。

梯度惩罚的实现通常是在判别器输入空间随机插值,然后计算判别器输出的梯度,限制其 L2 范数接近 1。这个操作会占不少计算量,但在稳定性和生成质量上的收益值得。

6. 胶囊网络:向量神经元和动态路由的实现细节

6.1 胶囊的核心:用向量代替标量

传统 CNN 的神经元输出一个标量,表示“这个特征存在与否”的概率。胶囊网络的神经元输出一个向量,除了表示存在概率(向量的长度),还能携带姿态信息(向量方向)。这意味着胶囊既能告诉你“这里有一只眼睛”,还能告诉你“眼睛朝哪个方向、有多大”。

在 Keras 中实现胶囊层,需要自定义 Layer,手动实现向量神经元的计算和动态路由。一个常用技巧是用 squash 激活函数把向量长度压缩到 [0,1] 之间,同时保持方向不变:

def squash(vectors, axis=-1): squared_norm = tf.reduce_sum(tf.square(vectors), axis=axis, keepdims=True) scale = squared_norm / (1 + squared_norm) / tf.sqrt(squared_norm + 1e-8) return scale * vectors

这个公式看起来复杂,其实可以理解为:向量越长,压缩后长度越接近 1,代表实体存在的置信度越高;向量越短,压缩后越接近 0,代表“这个实体不存在”。

6.2 动态路由是怎么迭代的

动态路由解决的问题是:低层胶囊应该把信息发送给哪些高层胶囊。传统神经元是全连接,每个上层神经元接收所有下层神经元的加权和;胶囊网络则是先让低层胶囊预测“自己应该匹配哪个高层胶囊”,然后通过迭代调整耦合系数。

算法流程简化为如下几步:

  1. 对每个低层胶囊,计算它对所有高层胶囊的预测向量。
  2. 初始化耦合系数为均匀分布。
  3. 迭代多轮:按当前耦合系数加权求和得到高层胶囊输入,squash 后得到高层输出;再根据高层输出和低层预测的相似度(点积)更新耦合系数。
  4. 迭代结束后,高层胶囊作为最终输出传给下一层。

这个机制的直观理解是:低层胶囊和高层胶囊方向一致,就增加它们之间的连接强度;方向不一致,就减少。通过这种方式,网络自动学习了“哪些部件组成哪个整体”的层次关系。

6.3 胶囊网络的图像分类实现(简化版)

在 Keras 里实现胶囊网络的完整代码量比较大,我这里给一个核心胶囊层和路由的骨架,方便理解:

class CapsuleLayer(layers.Layer): def __init__(self, num_capsules, capsule_dim, routings=3): super().__init__() self.num_capsules = num_capsules self.capsule_dim = capsule_dim self.routings = routings def build(self, input_shape): input_capsules = input_shape[1] input_dim = input_shape[2] self.W = self.add_weight( shape=[input_capsules, self.num_capsules, input_dim, self.capsule_dim], initializer='glorot_uniform', trainable=True) super().build(input_shape) def call(self, inputs): batch_size = tf.shape(inputs)[0] inputs_expand = tf.expand_dims(inputs, 2) # [B, in_caps, 1, in_dim] inputs_tiled = tf.tile(inputs_expand, [1, 1, self.num_capsules, 1]) # [B, in_caps, out_caps, in_dim] votes = tf.einsum('bikj,ikjd->bikd', inputs_tiled, self.W) logits = tf.zeros([batch_size, inputs_shape[1], self.num_capsules]) for i in range(self.routings): probs = tf.nn.softmax(logits, axis=-1) weighted_sum = tf.einsum('bik,bikd->bid', probs, votes) capsules = squash(weighted_sum) if i < self.routings - 1: similarity = tf.einsum('bid,bikd->bik', capsules, votes) logits += similarity return capsules

这段代码的核心在 einsum 运算,第一次接触可能不习惯,但它能把多维张量的加权求和写得很简洁。如果不熟悉 einsum,建议先在文档里系统看一遍,因为后面对胶囊网络的调参和改结构都离不开这些维度操作。

6.4 胶囊网络的局限和实用建议

胶囊网络理论框架很优美,但实际使用有几个硬伤一定要提前知道:

  • 计算量比同尺寸 CNN 大一个数量级。动态路由本身是迭代过程,每一步都要做矩阵乘法,显存和时间的消耗都不是小数。
  • 对超参非常敏感,耦合系数的迭代轮数、胶囊维度、正则化系数,每一个都可能显著影响最终效果。
  • 实现难度高、生态支持少。Keras 内置没有胶囊层,第三方实现质量参差不齐,部署到移动端或者服务端都要额外做算子适配。

所以我的建议是:胶囊网络很适合作为学习和研究项目,用来展示你对“CNN 表达能力的边界”有独立思考。但如果你要做实际产品,老老实实选 CNN 或 Transformer 更稳妥。

7. 常见问题与排查技巧实录

7.1 问题速查表

现象常见原因处理方案
训练 loss 为 NaN学习率过大、数据未归一化降低学习率,检查输入数据范围
GAN 生成图片全是模糊噪声生成器输出激活函数和数据范围不匹配输出用 tanh,输入归一化到 [-1,1]
Transformer 训练不收敛缺少残差连接或学习率设置过大检查残差结构,使用 warmup 调度器
CNN 验证集准确率停滞学习率过大或过拟合降低学习率,增强数据增强或加 Dropout
胶囊网络训练极慢路由迭代次数太多、胶囊维度过大减 routings 到 2,降低 capsule_dim
训练集准确率高但测试集差过拟合增加数据增强、Dropout 或减小模型容量
GPU 显存不足序列过长或 batch 过大减小 batch size,用混合精度训练
损失曲线震荡剧烈学习率过高或 GAN 训练节奏不稳降低学习率,检查判别器和生成器的训练步数比例

7.2 我踩过的三个坑

第一个坑是关于 BatchNormalization 在 GAN 生成器里的使用。生成器输入是随机噪声,如果噪声维度太小(比如只有 8 或 16),BN 层会因为样本统计量波动太大导致输出极不稳定。后来我把噪声维度加到 128,并且从头到尾保留 BN,才明显改善。

第二个坑是 Transformer 输入维度不匹配。我用 MultiHeadAttention 时一开始直接把 key_dim 设成和 embed_dim 一样大,结果维度翻倍,后面 Dense 层全乱套。后来才意识到 key_dim 应该是 embed_dim / num_heads,而不是 embed_dim 本身。

第三个坑是数据增强和标签不一致。我在做图像分类时用了 rotation_range=90,结果某些类别的语义在旋转 90 度后完全变了,比如数字 6 和 9、左箭头和上箭头。这种问题不会导致程序报错,但验证集准确率会莫名其妙下降。排查方法也很简单:把增强后的样本可视化出来看一遍,很多问题一眼就能发现。

7.3 一套通用的调试顺序

我在训练任何新模型时,都会先跑一个小型实验,用很少的数据和很少的 epoch 过一遍完整流程。如果小实验能顺利跑通,再逐步放大数据量和训练轮数。这样可以把“模型结构错误”和“训练策略不合理”两类问题分开排查。

如果训练遇到问题,我的排查顺序是:

  1. 先检查输入数据形状和标签维度是否匹配输出层的类别数。
  2. 再看 loss 函数和最后一层激活函数的搭配是否正确(二分类用 sigmoid + binary crossentropy,多分类用 softmax + categorical crossentropy)。
  3. 然后检查梯度是否正常,最直接的办法是看前几个 batch 的 loss 是否下降,如果完全不降,很可能是没有调用 apply_gradients 或者梯度计算错位。
  4. 最后才考虑学习率、正则化、网络深度等超参数问题。

这套顺序帮我在很多调试场合省了大量时间,特别是能避免把超参数调了半天、结果发现是前面维度写错的尴尬情况。

8. 写在最后的几个实操心得

四种架构都跑下来之后,我心里最深的体会是:会调库和真正理解模型之间有一条巨大的鸿沟。Keras 让搭建模型变得特别简单,几行代码就能把 Transformer 编码器调出来,但真正遇到问题的时候,起决定性作用的还是你对模型内部机制的把握,以及你那套成体系的调试方法。

如果你现在正准备动手实践,我建议的顺序是:CNN 一定要先吃透,它是后续所有架构的基石。Transformer 则要亲自动手写一遍位置编码和注意力层的流程,不用追求一次写完美,哪怕参考别人的实现,也要自己敲进去、跑通、改改参数看效果变化。GAN 是最让人上头的,因为它给了你“创造图像”的即时反馈,但一定不要期望第一次训练就稳定,多准备些随机种子和可视化代码。胶囊网络适合作为进阶探索,不用花太长时间,但值得亲手跑一跑,感受传统 CNN 的局限在哪里。

如果你现在正处于“代码能跑但不懂为什么”的阶段,不妨在每跑通一个模型后,尝试删掉一个关键组件,观察效果变化。比如把 BN 删掉、把残差连接删掉、把耦合系数定死成均匀分布,这种“破坏性实验”对理解的提升,往往比多看十篇文章都管用。

下一篇我可能聊聊如何把这四种架构组合起来做混合模型,比如用 CNN 提取底层特征再接 Transformer 做全局建模,或者用 GAN 做数据增强来提升小样本分类器的效果。你自己动手跑的时候如果遇到什么奇怪的报错或者反直觉的现象,也欢迎在评论里拿出来一起讨论,很多坑都是互相踩踩才被填平的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询