news 2026/10/20 4:11:16

四种深度学习模型实战:TensorFlow实现CNN、Transformer、GAN与胶囊网络

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
四种深度学习模型实战:TensorFlow实现CNN、Transformer、GAN与胶囊网络

上一篇我们完成了TensorFlow和Keras的基础入门,聊了张量、自动微分,以及用Sequential搭一个线性模型的完整流程。但说实话,线性模型和真正能上任务的深度网络之间还隔着一大段路,所以这篇我想直接把四种真正能打的架构拿出来动手过一遍:CNN、Transformer、GAN,以及很多人听说过但很少亲手实现的胶囊网络。四种模型我会给出可运行的核心代码,讲清楚每一层为什么这样设计,再把调参时会遇到的坑和我的判断依据一并说出来。文章比较长,建议边看边敲,默认环境是Python 3.10和TensorFlow 2.15,GPU当然更好,CPU也不是不能跑,就是耐心要多准备一点。

1. 动手前先把版本和数据集理顺

1.1 版本选型:为什么我坚持用2.x的高层API

先说版本问题。TensorFlow从1.x到2.x经历了很大的接口变动,很多老项目还留着tf.Session()和placeholder的写法。如果你是刚接触神经网络,我一定建议打死都不要碰1.x,直接用2.x的Keras高层API。原因很简单:同样的功能,2.x的代码量大概是1.x的三分之一到四分之一,而且model.fit()这套封装已经把训练循环、批次切分、指标记录全处理好了,你只需要关心网络结构本身。

这里要注意一个区别:tf.keras和独立安装的keras。如果你用的是TensorFlow,就直接从tensorflow.keras导入,别混着用。我刚入门的时候犯过这个错,环境里同时装了独立的Keras包,结果模型文件互相不认,报错信息又是一大串堆栈,排查半天才发现是导入路径不一致。现在我的习惯是只用tf.keras,版本跟随TensorFlow走,省心。

1.2 数据集选择:一套数据把四种模型都跑通

代码可以直接跑通常比大数据集更实用。我这次选的几乎都是公开的MNIST手写数字数据集,只有Transformer部分换成了CIFAR-10。为什么这么选?因为胶囊网络和GAN在小型图像数据上最容易出效果、最容易观察问题,MNIST灰度图分辨率28x28,计算量可控,即使CPU跑也不会等太久。CIFAR-10拿来验证Transformer做图像分类的思路,因为Transformer很吃数据量,在MNIST上反而体现不出优势。

数据集加载直接用Keras内置接口,比手动下载解压简单太多:

import tensorflow as tf # MNIST (train_images, train_labels), (test_images, test_labels) = \ tf.keras.datasets.mnist.load_data() # CIFAR-10(Transformer部分使用) (train_imgs, train_lbs), (test_imgs, test_lbs) = \ tf.keras.datasets.cifar10.load_data()

1.3 一个最容易出错的细节:数据规范和维度

加载出来的数据默认是uint8类型,数值范围0到255,直接喂进神经网络基本不收敛。必须归一化。这个问题我在带朋友跑实验时碰到过不止一次,网络结构写得没错,但损失一直降不下去,最后发现就是忘了把像素值除以255。

维度方面还要注意,MNIST的单通道灰度图在Keras里要写成(28, 28, 1),而不是(28, 28),否则卷积层会报错。CIFAR-10本来就是三通道(32, 32, 3),所以不用额外扩展维。归一化代码很简单:

# MNIST归一化并扩展通道维 train_images = train_images.astype("float32") / 255.0 test_images = test_images.astype("float32") / 255.0 train_images = train_images[..., tf.newaxis] test_images = test_images[..., tf.newaxis]

如果想把标签做成one-hot编码,可以用tf.keras.utils.to_categorical(train_labels, 10)。其实我后来发现,直接用整数标签配合SparseCategoricalCrossentropy损失函数也一样,不用转one-hot,代码还能少两行。

2. CNN实现:从卷积堆叠到训练收敛的关键观察

2.1 用Keras搭一个能当作baseline的CNN

CNN的经典套路就是卷积、激活、池化、再卷积、再激活、池化,最后接全连接层。看似简单,但每个组件的顺序和参数都有讲究。我这次搭的结构参考了LeNet的思路但做了一些现代化调整,增加BatchNormalization,效果会明显好于老版结构:

from tensorflow.keras import layers, models def build_cnn(): model = models.Sequential([ layers.Input(shape=(28, 28, 1)), layers.Conv2D(32, (3, 3), strides=(1, 1), padding="same"), layers.BatchNormalization(), layers.ReLU(), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), padding="same"), layers.BatchNormalization(), layers.ReLU(), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), padding="same"), layers.BatchNormalization(), layers.ReLU(), layers.GlobalAveragePooling2D(), layers.Dense(128, activation="relu"), layers.Dropout(0.5), layers.Dense(10, activation="softmax") ]) return model cnn = build_cnn() cnn.summary()

这里有两个选择值得说明。第一个是最后用GlobalAveragePooling2D而不是直接Flatten。Flatten会把空间特征全部拉平成一条很长的向量,参数直接爆炸,而全局平均池化把每个特征图压缩成一个数值,参数为零,还能减少过拟合。第二个是Dropout放在Dense层之间而不是卷积层之间,因为卷积层本身参数量不大,又有BatchNormalization管着,加Dropout反而容易让训练变慢。我试过在卷积层后面也挂Dropout,准确率没提升,收敛还磨叽了不少时间。

2.2 编译与训练配置的讲究

编译时我用Adam优化器,初始学习率0.001,损失函数用SparseCategoricalCrossentropy。模型训练有个细节很多人不知道:学习率不能一个值走到黑。一开始0.001比较舒服,但训练到后期损失曲线进入平台期之后,学习率不降下去就很难继续精调。Keras的ReduceLROnPlateau回调可以自动在指标停顿时降低学习率,非常省事。

from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau cnn.compile( optimizer=Adam(learning_rate=1e-3), loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) callbacks = [ ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=3), EarlyStopping(monitor="val_loss", patience=8, restore_best_weights=True) ] history = cnn.fit( train_images, train_labels, validation_data=(test_images, test_labels), batch_size=64, epochs=30, callbacks=callbacks )

EarlyStopping加restore_best_weights=True是防止过拟合的保险。如果不设这个参数,训练到后期一旦过拟合,模型保存的是最后一个epoch的权重,而不是验证集表现最好的那一个,前面的最优结果就白白浪费了。这个参数我经常见人忽略。

2.3 我在CNN训练里踩过的三个小坑

第一个坑是BatchNormalization在训练和推理阶段的行为不同。它在训练时会用小批次的均值和方差做归一化,推理时用训练阶段滚动统计的全局均值和方差。如果你在一个tf.GradientTape里手动写训练循环,别忘了把BN层设置为训练模式。用model.fit就没这个问题,框架自动管理。

第二个坑是验证集loss下降、准确率却不升反降。看起来矛盾,其实通常出现在多分类任务里,当模型的输出分布趋于保守时,交叉熵损失会下降,但精确的argmax判断可能没变。这时候不要慌,继续训练几轮就行,或者检查是不是验证集标签和训练集标签类别失衡。

第三个坑是我用大量数据增强时踩的。RotationRange、ZoomRange这些增强操作会改变图像语义,比如数字6旋转过头可能看起来像9,导致训练集内部出现错误标签。增强是好事,但幅度要克制,旋转范围控制在10度以内对MNIST比较安全。

3. Transformer:手写注意力层的实现与调试过程

3.1 自注意力机制的四行直觉

Transformer和CNN的思路完全不同。CNN靠卷积核在局部窗口里提取模式,感受野慢慢扩大;Transformer一上来就让每个位置和所有其他位置两两交互,一步到位建模全局依赖。这个能力来自自注意力机制,核心就是三个向量:Query、Key、Value。

你可以这样理解:Query是"我在找什么",Key是"我有什么",Value是"我实际提供的信息"。拿一段文本做类比,一个词作为Query时会和所有词的Key算相似度,分数高说明这个位置和"我"关系近,再用这个分数加权所有Value。公式就是softmax(QK^T / sqrt(d_k)) V,除以sqrt(d_k)是为了防止点积值太大导致softmax梯度消失。

我在刚开始上手Transformer时,第一反应是不去翻现成的TransformerBlock源码,先把这几个张量运算亲手算一遍。这一步太关键了,因为后续所有调试都建立在对形状变换的敏感度上。

import numpy as np def scaled_dot_product_attention(q, k, v, mask=None): d_k = tf.cast(tf.shape(k)[-1], tf.float32) scores = tf.matmul(q, k, transpose_b=True) / tf.math.sqrt(d_k) if mask is not None: scores += (1.0 - mask) * -1e9 weights = tf.nn.softmax(scores, axis=-1) return tf.matmul(weights, v), weights

这里的mask处理也要注意。我写的版本把无效位置替换成非常大的负值,经过softmax后权重就会趋近于0。实际做视觉Transformer时不一定需要mask,但做序列任务时这个是必备的。

3.2 位置编码与编码器类的完整实现

自注意力有一点很要命:它对输入顺序是"无感"的,把句子单词打乱,注意力输出的集合完全一样。所以必须额外加位置信息。经典的实现是正弦位置编码,奇数位置用sin,偶数位置用cos,维度越高变化频率越低。这样设计的好处是任意长度的序列都能外推,不像可学习位置编码受限于训练时的最大长度。

我自己实现编码器时偷了个懒,用可学习的位置嵌入,在MNIST这种小规模任务上表现完全够用,代码还简单。但如果你要做长序列,建议还是规规矩矩用正弦编码。

class PositionalEncoding(layers.Layer): def __init__(self, d_model, max_len=5000): super().__init__() self.d_model = d_model angle_rads = np.arange(max_len)[:, None] / \ np.power(10000, (2 * (np.arange(d_model)[None, :] // 2)) / d_model) angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2]) angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2]) self.pos_encoding = tf.constant(angle_rads, dtype=tf.float32) def call(self, inputs): seq_len = tf.shape(inputs)[1] return inputs + self.pos_encoding[:seq_len, :]

TransformerBlock的核心部分有两个子层:多头注意力和前馈网络,每个子层外面绕一圈残差连接和层归一化。残差连接解决深层网络退化问题,层归一化让每层输入的分布稳定下来。前馈网络一般是两个Dense层,中间维度扩到4倍再用GELU或ReLU激活。我用MultiHeadAttention直接实现,注意head数和d_model要整除,不然会报形状错误。

class TransformerBlock(layers.Layer): def __init__(self, d_model, num_heads, ff_dim, rate=0.1): super().__init__() self.att = layers.MultiHeadAttention( num_heads=num_heads, key_dim=d_model // num_heads) self.ffn = models.Sequential([ layers.Dense(ff_dim, activation="gelu"), layers.Dense(d_model) ]) 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)

3.3 把Transformer用在图像分类上的体验

Transformer最早是给NLP用的,但最近视觉Transformer(ViT)的思路其实很直接:把图像切成一个个小方块,每个方块拉平成向量,再投影到固定维度,当作一个"词"输入Transformer。我在CIFAR-10上做了个简化版,16x16的patch,每个patch是16x16x3的向量,通过Dense层映射到64维,再加上可学习的位置编码,后面接四个TransformerBlock,最后全局平均池化后分类。

调试时最深刻的体会是Transformer收敛比CNN慢。同样的训练轮数,CNN已经到80%准确率了,Transformer还卡在50%上下。这不是网络坏了,而是Transformer天生需要更多数据、更多轮数来学习归纳偏置。CNN天生自带空间不变性,而Transformer必须靠注意力机制自己摸索出"相邻patch可能语义相关"这个规律。在小数据集上,这类模型通常还真打不过CNN。

如果在MNIST上硬上Transformer,效果会非常尴尬,因为数据集太小,参数又多,直接过拟合。我建议初学阶段把ViT理解成两部分:patch嵌入部分的实现难度不高,真正需要花时间调的是注意力层数、头数和维度之间的配置关系。我把维度从64提到128,准确率马上掉下来,因为参数量暴涨,而训练数据没变。

4. GAN:生成器和判别器的对决如何稳定下来

4.1 生成器和判别器的网络设计

GAN的思路可以比喻成一个造假团队和一个鉴定团队的博弈。生成器负责从随机噪声里合成图像,目标是骗过判别器;判别器负责区分真实图像和合成图像,目标是识别出造假痕迹。两边互相促进,最终生成器产出的图像能以假乱真。

这次生成器我从100维的随机噪声开始,经过几个全连接层和上采样操作,最终生成28x28x1的手写数字图像。卷积版的生成器通常用Conv2DTranspose做上采样,配合BatchNorm和LeakyReLU。这里注意,生成器里最好不要用ReLU,因为ReLU会把负值全部置零,容易导致生成图像出现"死像素";LeakyReLU允许很小的负梯度流过,训练更稳定。

def build_generator(): noise_dim = 100 model = models.Sequential([ layers.Input(shape=(noise_dim,)), layers.Dense(7 * 7 * 128), layers.BatchNormalization(), layers.LeakyReLU(alpha=0.2), layers.Reshape((7, 7, 128)), layers.Conv2DTranspose(64, (4, 4), strides=(2, 2), padding="same"), layers.BatchNormalization(), layers.LeakyReLU(alpha=0.2), layers.Conv2DTranspose(1, (4, 4), strides=(2, 2), padding="same"), layers.Activation("tanh") ]) return model

判别器就是一个标准的二分类卷积网络,输出一个0到1之间的分数。输入图像先经过几层卷积,逐步缩小空间尺寸,最后接一个Dense输出。判别器内部我用Dropout而不是BatchNorm,因为判别器单独迭代时,批归一化在训练和推理模式之间的切换容易引入不一致,Dropout更干脆。

def build_discriminator(): model = models.Sequential([ layers.Input(shape=(28, 28, 1)), layers.Conv2D(64, (5, 5), strides=(2, 2), padding="same"), layers.LeakyReLU(alpha=0.2), layers.Dropout(0.3), layers.Conv2D(128, (5, 5), strides=(2, 2), padding="same"), layers.LeakyReLU(alpha=0.2), layers.Dropout(0.3), layers.Flatten(), layers.Dense(1, activation="sigmoid") ]) return model

4.2 对抗训练循环的正确写法

GAN的训练不能用model.fit,因为它是双网络交替更新:判别器先拿真实图像和生成图像各训练一步,然后生成器再尝试骗过判别器。在tf.GradientTape里同时维护两个优化器、两个损失函数,每次迭代反传两次梯度。核心代码大概是这个样子:

import os import tensorflow as tf generator = build_generator() discriminator = build_discriminator() cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=False) gen_optimizer = tf.keras.optimizers.Adam(learning_rate=2e-4, beta_1=0.5) disc_optimizer = tf.keras.optimizers.Adam(learning_rate=2e-4, beta_1=0.5) noise_dim = 100 batch_size = 128 @tf.function def train_step(real_images): noise = tf.random.normal([batch_size, noise_dim]) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: fake_images = generator(noise, training=True) real_output = discriminator(real_images, training=True) fake_output = discriminator(fake_images, training=True) disc_loss = cross_entropy(tf.ones_like(real_output), real_output) + \ cross_entropy(tf.zeros_like(fake_output), fake_output) gen_loss = cross_entropy(tf.ones_like(fake_output), fake_output) gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables) gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables) gen_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables)) disc_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables)) return disc_loss, gen_loss

注意这里我给两个优化器都设了beta_1=0.5,这比默认的0.9更合适,因为默认的Adam在过去梯度记忆太强,会让GAN的博弈过程过于平滑,实际上更容易陷入模式坍塌。学习率也用相对小的2e-4。

4.3 训练不稳定的观察与对策

训练GAN最折磨人的就是看损失曲线。别指望像分类任务那样单调下降,正常情况是判别器和生成器两边此消彼长,曲线像心电图一样震荡。如果判别器损失快速归零,说明它太强了,生成器完全没有还手之力,这种情况要降低判别器的学习率,或者给真实标签加一点随机噪声(label smoothing),把标签从1改成0.9左右,给判别器留点余地。

另一个常见问题是模式坍塌:生成器发现只要一直生成同一个数字就能骗过判别器,于是所有输出变得几乎一样。判断方法很简单,保存一批生成图像,每隔若干轮扫一眼,如果图像越来越单一,就是坍塌的征兆。我用的对策有三个:一是别再提升判别器能力了,适当加深生成器;二是把噪声维度调高一些,从100提到128;三是用特征匹配损失,让生成器的中间层特征接近真实图像的中间层特征,这个做起来麻烦,但能有效缓解坍塌。

我在实际跑的时候还发现,生成器前期生成的都是噪点,很正常。坚持训练到第15到20轮左右,能隐约看到数字轮廓;到50轮左右会出现像模像样的手写体,但边缘还是模糊;到100轮之后基本能骗过肉眼。这个时间成本在CPU上要几个小时,有条件还是上GPU。

5. 胶囊网络:用动态路由替代池化的实现细节

5.1 为什么会有胶囊网络,它到底在解决什么

胶囊网络是Hinton团队提出的一个比较"反主流"的结构。CNN里的池化层虽然让网络拥有了平移不变性,但代价是丢失了位置和姿态信息。比如一张图里有个倾斜的笔,池化只知道"这里有个笔",但不知道笔朝哪个方向、倾斜多少度。胶囊网络的核心主张是,把传统神经元标量输出换成一组向量,向量的方向编码实体的姿态属性,向量的长度表示实体存在的概率,再通过动态路由让低层胶囊把信息"路由"给合适的高层胶囊。

我当时动手实现时最大的感受是:这个网络思想上不难,难在自定义层的代码细节。Keras没有内置胶囊层,所有前向传播和路由迭代都要自己写。

5.2 动态路由算法的Keras自定义层实现

动态路由的核心是以下几步。假设低层胶囊输出u_i,对每个可能的高层胶囊j都做一个矩阵变换W_ij u_i得到预测向量u_hat_j|i。路由系数c_ij初始化为均匀分布,然后迭代更新:加权求和得s_j,用squash函数压缩模长到[0,1)区间,再用s_j和u_hat_j|i点积更新路由系数。迭代几次后,高层胶囊的输出就是最终的路由结果。

squash函数是胶囊网络的"激活函数",公式是(s / (1 + ||s||^2)) * (s / ||s||),本质上保持方向不变、把模长压缩到接近1的饱和区。实现时最容易被坑的就是张量维度和矩阵乘法的轴搞混。我调试这个层花的时间比前面三个模型加起来都多,所以建议你先把形状写在注释里,再一行行对。

class PrimaryCaps(layers.Layer): def __init__(self, num_capsules, dim_capsules, kernels=9, strides=2): super().__init__() self.num_capsules = num_capsules self.dim_capsules = dim_capsules self.conv = layers.Conv2D( filters=num_capsules * dim_capsules, kernel_size=kernels, strides=strides, padding="valid" ) def call(self, inputs): capsules = self.conv(inputs) capsules = tf.reshape( capsules, (-1, capsules.shape[1] * capsules.shape[2] * self.num_capsules, self.dim_capsules)) return squash(capsules) def squash(vectors): squared_norm = tf.reduce_sum(tf.square(vectors), axis=-1, keepdims=True) scale = squared_norm / (1.0 + squared_norm) return scale * vectors / tf.sqrt(squared_norm + 1e-9)

当然完整的胶囊网络在PrimaryCaps之上还要加一个DigitCaps之类的胶囊层,里面要维护一个维度较高的权重张量W,形状是(num_high_caps, num_low_caps, in_dim, out_dim)。对每个低层胶囊i和每个高层胶囊j做矩阵乘法,得到u_hat,然后进入路由迭代。这部分代码如果全贴出来会比较长,但核心就一段循环:

def dynamic_routing(u_hat, num_iterations=3): batch_size = tf.shape(u_hat)[0] num_low = tf.shape(u_hat)[1] num_high = tf.shape(u_hat)[2] b = tf.zeros((batch_size, num_low, num_high, 1)) for _ in range(num_iterations): c = tf.nn.softmax(b, axis=2) # 每个低层胶囊对高层胶囊的耦合系数 s = tf.reduce_sum(c * u_hat, axis=1, keepdims=True) v = squash(tf.squeeze(s, axis=1)) # 高层胶囊输出 agreement = tf.expand_dims(tf.reduce_sum(u_hat * v[:, None, :, :], axis=-1, keepdims=True), axis=-1) b += agreement return v

这里有几个容易翻车的地方。第一,u_hat四个维度分别是batch、低层胶囊数量、高层胶囊数量、胶囊向量维度,每一步都要想清楚在哪个轴上操作。第二,路由迭代次数一般取3,不能再多了,再多训练时间翻倍但效果提升很有限。第三,tf.reduce_sum(u_hat * v[:, None, :, :], axis=-1, keepdims=True)这一步看起来绕,其实就是在算低层预测向量和高层输出向量的点积,也就是"同意度"。

5.3 在MNIST上的训练结果与边界观察

我把胶囊网络在MNIST上跑了一下,发现这东西的训练体验和前面三种完全不一样。训练时间长是最大的痛点,同样20个epoch,CNN两分钟搞定,胶囊网络可能要十几分钟甚至更久,因为动态路由在每次前向传播时都多了三次迭代。准确率方面,MNIST这种简单任务两者差距不大,胶囊网络能到99%左右,CNN也能到99%,但胶囊网络的优势在小样本和姿态变化场景下会更明显。

如果需要训练分类的手臂,可以自己把数据集缩小到几千张看看。本人直观感受是,在极少量样本下,胶囊网络的过拟合速度比CNN慢,因为它的向量表示里塞了更多结构信息,不需要靠巨量参数去硬记。不过这也带来一个附带问题:胶囊网络对输入图像的平移和旋转依旧很敏感,路由机制并不像论文标题暗示的那样对姿态完全免疫。

6. 四种模型横向对比与选型建议

6.1 用同一批数据实测的直观结果

为了让大家心里有谱,我把这四种模型在MNIST和CIFAR-10上简单对比了一下。结果其实不意外:CNN在MNIST上准确率最高且最省时间,Transformer在CIFAR-10上需要更多轮数才能追上CNN,GAN属于生成模型,没有分类准确率的概念,但生成的图像质量肉眼可见地逐步提升,而胶囊网络精度略低于CNN,但它在小样本和结构化表示上表现出了潜力。

模型任务参数量级训练耗时(CPU/20轮)观测指标
CNNMNIST分类约百万2-3分钟准确率99.1%
Transformer(ViT简化版)CIFAR-10分类数百万20分钟以上准确率约75-80%
GANMNIST生成约千万2小时以上生成图像肉眼可辨
Capsule NetworkMNIST分类约千万15分钟以上准确率98.8%

这个表不要当成权威结论,因为我这实验跑得很粗,没有做严格的超参搜索。但它能反映一个规律:模型复杂度和训练成本基本成正比,CNN依然是中小规模图像任务里性价比最高的存在。

6.2 我的选型思路和建议

做实际项目时,我不会因为某个模型"听起来更先进"就用它,而是先看任务类型和资源约束。如果是一个落地业务里的图像分类,我会先上CNN,或者干脆用现成的预训练模型做迁移学习,又快又稳。如果输入是长文本序列,或者明显需要全局关联的场景,我才会考虑Transformer。GAN只用于生成任务,别拿它当分类器用,专业方向不对。胶囊网络在常规视觉任务里不是第一选择,但如果数据量很少,或者你需要从模型里提取可解释的实体姿态信息,那么值得一试。

我个人的体会是,把这四种模型亲手实现一遍,比读十篇综述都管用。CNN教会你特征提取的基本操作,Transformer让你理解全局关系怎么建模,GAN逼着你想清楚博弈和训练的边界条件,胶囊网络则是锻炼自定义层和路由算法的最佳练习册。这些模型背后共通的原理比模型本身更值得吃透,比如归一化、注意力机制、张量形状变换,你在任何一个模型里踩过的坑,换到另一个模型时大概率还会遇见。下一篇如果还有机会,我打算把注意力机制在视觉任务里的通用用法单独拎出来深挖一下,那里面的门道比这次展示的还要多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 4:10:47

纯Socket实现西门子S7-1200/1500 PLC数据采集C#源码

简介:本资源是一套面向工业自动化开发者的西门子S7系列PLC数据采集C#源代码,适用于.NET平台下构建轻量级SCADA通信模块或产线数据监控系统,尤其适合具备C#基础与工业通信入门经验的工程师快速集成PLC实时数据读取功能。压缩包共5个文件&#…

作者头像 李华
网站建设 2026/10/20 4:08:57

运算符避坑指南:从算术到位运算,掌握核心原理与常见陷阱

最近在代码评审里看到好几处与运算符相关的低级错误,而且不是只有新人会犯。恰好经常有读者问我:“算术、关系、逻辑、位运算……这些运算符我都认识,可为什么写出来就是不对?”今天就以“运算符大全”为线索,把四类运…

作者头像 李华
网站建设 2026/10/20 4:08:29

REA前端事件聚合框架:高频实时监控与React渲染性能优化实践

REA 是我花了两周时间从零写出来、又花了一周时间集成到线上实时监控系统里的一个轻量级事件聚合框架,全称是 Rapid Event Aggregation。如果你正在做实时数据看板、订单状态同步、消息推送、或者前端高频交互状态同步这类事,这篇复盘应该对你有用。我会…

作者头像 李华
网站建设 2026/10/20 4:08:04

千问模型Ubuntu部署实战:从显存规划到vLLM与Ollama选型

上个星期,我在一台 Ubuntu 服务器上把千问系列模型从零到一完整部署了一遍——先确认显卡驱动和 CUDA 环境,再搭建 Python 虚拟环境,然后是模型文件下载、推理服务启动、最后用接口对外提供对话能力。整个过程走完,最深的感受是&a…

作者头像 李华
网站建设 2026/10/20 4:07:52

C#基础练习题入门:从Hello World到编程直觉

简介:本资源是一套系统性的C#编程入门实战练习题集,面向零基础或初学阶段的开发者,旨在通过大量编码实践夯实语法、面向对象、集合操作、异常处理等核心能力,快速提升解决实际问题的编程水平。压缩包共1670个文件,涵盖…

作者头像 李华
网站建设 2026/10/20 4:07:11

医疗AI落地核心:从工作流重构到应用架构的路径拆解

前阵子和几位深耕医疗信息化的架构师线上交流,大家有个高度一致的判断:医疗AI真正难啃的骨头,已经从"模型效果"转移到了"工作流重构"。算法竞赛的阶段正在收尾,各家模型的诊断精度差距在肉眼可见地缩小&#…

作者头像 李华