☰
TabNet架构可视化:用Mermaid绘制特征选择与自监督流程图
2026/10/19 10:00:01 网站建设 项目流程

做表格数据建模的人,应该都听过 TabNet 这个名字。它最大的特点是:把深度学习的表达能力和传统特征选择的可解释性揉在了一起,每一步都能看出模型在用哪些特征做决策。可问题也随之而来——它的结构在论文里是那种层层展开的立体示意图,自己看懂不难,想让别人在组会或博客文章里看明白,光靠嘴说很难。所以我整理了一整套可以直接拿来用的 Mermaid 流程图,覆盖 TabNet 的主干数据流、单步内部结构、特征选择回路和自监督预训练四个层面。

这些图全部用 Mermaid 编写,纯文本、可版本管理,能直接放进 GitHub README、Typora 或语雀文档里实时渲染。适合这几类人:写 TabNet 技术博客需要配图的人、做组会汇报不想临时画 PPT 结构图的人、被论文里 prior 和 mask 那几个符号绕晕想靠图理清思路的人。下面我按从宏观到微观的顺序,把每张图都拆开讲一遍。

1. 这套流程图画的是什么,适合谁用

1.1 论文原图很精美,但讲解场景下不够用

TabNet 论文里的架构图是那种信息密度很高的二维展开图,把编码器部分画成一个横向铺开的大结构,注意力和特征变换模块层层堆叠。这种图非常适合说明"整体有哪些模块",但真要对着它讲数据从哪个节点流到哪个节点,听众很容易跟不上。原因很简单:原图更像一张系统组成图,不是数据流图。

我这套图换了一个视角,全部改成数据流优先的表达方式:节点是模块,箭头是数据、掩码或先验信息的搬运路径。每一步的输入是什么、输出到哪儿、哪些路径是复用上一轮的结果,都靠箭头的走向说清楚。做法和画电路图类似,不追求把每个张量维度标出来,只把控制流和数据流理清。

1.2 四张图的分工与适用场景

这套图集一共四张,覆盖 TabNet 从模型结构到训练流程的完整讲解链路:

图名覆盖内容最适用的讲解场景
主干数据流图输入、BatchNorm、N_steps 决策步骤、决策输出累加、分类头快速介绍 TabNet 整体长什么样
单步内部结构图Feature Transformer、Attentive Transformer、GLU 和 Sparsemax讲清"每一步为什么能选特征"
特征选择回路图掩码 M、先验 P、熵正则与稀疏约束的迭代关系深入解释 TabNet 可解释性的来源
自监督预训练图Encoder 与 Decoder 的掩码对应、重构目标、微调流程讲 TabNet 的预训练与无标签数据利用

写技术博客的时候,我的习惯是先放主干图让读者建立体感,再放单步图和回路图解释核心机制,最后讲预训练时补上 Encoder-Decoder 的那张图。组会汇报则是反着来,先把结论抛出来,隐含假设听众已经看过主干图,直接进入回路图讲创新点。

1.3 为什么不用 draw.io,也不用 PPT

画神经网络架构图的传统工具是 draw.io 或者 PPT,模板丰富、拖拽方便。但我最终选择 Mermaid,核心原因是版本管理。流程图这东西在写文章时一定会反复改,用 draw.io 导出的 XML 不直观,两个版本之间到底动了哪根线,很难从 diff 里看出来。Mermaid 是纯文本,改动可以被 Git 精确记录,同事提 PR 时还能直接在评论里指出某条线应该改成虚线。

另一个好处是渲染零成本。代码块一贴,GitHub、Typora、语雀都能渲染,不需要额外嵌入图片,也不用担心图片链接失效。缺点也很明显,排版自由度低,画不了那种精密到像素级的示意图。所以在画图策略上我做了取舍:这类图要表达的核心是数据流转逻辑,不是艺术效果,Mermaid 反而更合适。

2. 主干架构图:从输入到预测的整体数据流

2.1 先看这张宏观图

我一般会把主干图画成纵向布局,让"从数据输入到最终预测"的路径从上到下自然流淌。N_steps 的多个步骤用串行方块表示,每一步内部细节先折叠起来,只露出决策输出 d 和传入下一步的中间特征 a。

flowchart TB X[输入特征 x] --> BN[BatchNorm] BN --> Step1[决策步骤 1<br/>Feature Transformer + Attentive Transformer] Step1 --> D1[决策输出 d1] Step1 --> A1[中间特征 a1] A1 --> Step2[决策步骤 2<br/>Feature Transformer + Attentive Transformer] Step2 --> D2[决策输出 d2] Step2 --> A2[中间特征 a2] A2 --> Dots[... 共 N_steps 步 ...] D1 --> SUM[决策输出累加] D2 --> SUM Dots --> SUM SUM --> FC[全连接层] FC --> OUT[Softmax 预测]

2.2 主干上每个环节在干什么

先看最前面两根线。输入特征 x 先经过 BatchNorm,这一步对表格数据尤其重要。表格特征经常是不同量表混在一起,比如一列是年龄、一列是月收入,量纲差几个数量级,如果不做归一化,前几步的特征选择会被大数值特征带偏。BatchNorm 在这里的作用和它在图像模型里一样,让每维特征在进入后续复杂变换之前保持稳定的分布。

然后是 N_steps 个串行的决策步骤。每个步骤做两件事:根据上一步的信息选出一组特征,然后用这些特征做一次局部变换。这种"选一次、算一次"的循环结构是 TabNet 的灵魂。后接的虚线框表示中间特征 a 会流入下一步,同时每一步还会单独输出一个决策向量 d。d 不是直接拿去预测的,而是所有步骤的 d 先各自通过一个全连接层,再把结果逐元素累加,最后接 Softmax 输出类别概率。

2.3 为什么步骤必须串行,N_steps 怎么定

串行结构不是为了模仿 Transformer 的层叠设计,而是由特征选择逻辑决定的。每一步生成的掩码会抑制已经被前几步选过的特征,也就是说后一步在做选择时,必须知道前一步已经用掉了哪些特征。如果步骤之间是并行的,它们就各自为政,无法形成"先选主体特征、再补边缘特征"的协作效果。

关于 N_steps 的取值,论文里测试时常用 3 到 5 步,我在实际项目里也发现这个区间比较稳。步数太少,特征选择太粗暴,模型基本退化成普通全连接网络加一个稀疏约束;步数太多,训练时间线性增长,但后期步骤的边际收益很小,因为好用的特征已经被前面的步骤占用得差不多了。调参时我一般先固定 N_steps=3 跑基线,再试 5 对比,很少需要走到 8 以上。

2.4 图上没画出来的两点提醒

第一,TabNet 在决策步骤之间没有残差连接。Transformer 里每层输出都会"加上"输入做残差,容易给人留下"每一步都应该有跨层捷径"的惯性印象。TabNet 的做法是每一步独立产出决策 d,最后累加的是经过全连接层后的 logits,不是把原始特征直接往后面传。画主干图时我特意没有加任何跨步骤的实线,就是为了避免这种误解。

第二,图中 a1 到 Step2 的这条线不传递"原始输入特征",它传递的是经过处理后的上下文特征,用于下一步生成掩码。真正参与第二步特征选择的原始特征 x 是从 BN 输出端直接引过去的,在主干图里我选用虚线来表示掩码的生成关系,避免和数据流主线混在一起。这部分如果只看主干图容易误解,下一章把单步结构展开后就清楚了。

3. 单步内部结构图:把每一层算子摆到台面上

3.1 单步展开长什么样

主干图把每个决策步骤当成一个黑盒,这一步要打开黑盒。单步内部的核心是双 Transformer 配合:Feature Transformer 负责把选出来的特征加工成可用表示,Attentive Transformer 负责根据上一步的上下文生成新的特征掩码。下面这张图是我常用到的单步展开版本:

flowchart LR SUBMASK[被掩码筛选后的特征] --> FT[Feature Transformer] FT --> SPLIT[按维度切分] SPLIT --> ATOUT[中间特征 a_i] SPLIT --> DOUT[决策输出 d_i] APREV[上一步中间特征 a_{i-1}] --> AT[Attentive Transformer] PRIOR[累积先验 P_{i-1}] --> AT AT --> MASK["特征掩码 M_i"] MASK --> MUL[逐元素乘] SUBRAW[归一化后的原始特征] --> MUL MUL --> SUBMASK

3.2 Feature Transformer:FC、BN、GLU 的组合

Feature Transformer 内部不是单层网络,而是"全连接层 + BatchNorm + GLU"的反复堆叠。其中最关键的是 GLU(门控线性单元)。它的做法是把输入经过线性变换后扩张成两倍宽度的特征,然后一分为二:一半直接作为信息流,另一半经过 Sigmoid 变成 0 到 1 之间的门控值,两者逐元素相乘。信息能不能往前走、走多少,由门控值说了算。

打个比方,这就像一条路上装了红绿灯:Sigmoid 生成的 0 到 1 数值就是红绿灯的开放程度,信息流是路上的车,绿灯全开(接近 1)时车辆畅通无阻,红灯全亮(接近 0)时直接截停。GLU 让网络在一定程度上自主决定每个维度信息的保留比例,表达能力比单纯的线性层强不少。

Feature Transformer 里还做了一个在主干图里看不见的参数安排:一部分层是"跨步骤共享"的,另一部分是"当前步骤独有"的。共享层让所有步骤共用同一套基础特征变换能力,减少整体参数量;步骤独有层则给每一步留下微调空间,让它在选用不同特征时能做出略有区别的处理。画归一化图时这两种层我习惯用不同底色区分,但 Mermaid 里样式管理比较繁琐,如果不想增加视觉负担,在文本里标注清楚即可。

3.3 Attentive Transformer:Sparsemax 才是真正的门面

Feature Transformer 的输出会沿特征维度切成两部分,一部分走中间特征 a 的路线进入 Attention 环节,另一部分走决策输出 d 的路线进入累加器。a 在进入 Attentive Transformer 之前,还要先和上一轮累积的先验 P 做逐元素相乘,然后经过一个由全连接层、BatchNorm、Sparsemax 组成的小模块,最终输出的是掩码 M。

相比 Softmax,Sparsemax 最大的特点是输出本身就是稀疏的。Softmax 无论输入多极端,输出总是所有维度都大于 0;Sparsemax 则会在某些维度上直接得到 0。用一个三特征例子说明:假设三个特征的得分是 2.0、1.0、0.1,Softmax 给出的权重大约是 0.58、0.31、0.11,虽然第三个很小但依然参与了后续计算。Sparsemax 处理类似的得分时,很可能直接把第三个维度的权重压成 0,只保留前两个。

这对 TabNet 至关重要,因为掩码 M 是逐元素作用于输入特征的。如果掩码处处非零,每个步骤实际上还是用到了全部特征,所谓"特征选择"就名存实亡了。Sparsemax 让掩码在训练过程中自然走向稀疏,模型才能真正做到"这一步不看某些特征"。

3.4 单步结构图的组件速查表

组件输入输出核心作用
Feature Transformer掩码筛选后的特征a_i、d_i特征加工与决策输出
Attentive Transformera_{i-1}、P_{i-1}掩码 M_i决定哪些特征被选中
GLU全连接层输出门控后的特征控制信息流动比例
Sparsemax得分向量稀疏权重向量强制掩码稀疏化
累加器各步 d_i 的变换结果最终 logits汇总所有步骤的决策

4. 特征选择回路的图解:掩码、prior 累乘与稀疏约束

4.1 先搞懂 P:prior 是怎么一步步累积的

TabNet 里最容易让人困惑的,是公式里的两个符号 M 和 P。M 是掩码,比较好理解,就是每个特征在当前步骤被选中的权重。P 是累积先验,它有约束后面步骤选择行为的作用。P 的更新方式是同位置逐项累积:

P_i = P_{i-1} 与 (γ - M_i) 的逐元素乘积

其中 γ 通常取 1。如果一个特征在前面某一步的掩码值接近 1,说明它已经被这一步充分使用,那么后续步骤里 (γ - M_i) 这一项就趋近 0,先验 P 会被压低,继而让后面步骤不敢再选这个特征。反过来,如果某特征从来没被选中,掩码值接近 0,先验就一直是 1,后面的步骤依然有充分机会选它。

我习惯把这一步单独画成一张回路图,因为它是 TabNet 的"记忆系统":

flowchart LR PREV["上一步中间特征 a_{i-1}"] --> AT[Attentive Transformer] PRIOR["累积先验 P_{i-1}"] --> AT AT --> M["当前掩码 M_i"] M --> PUPDATE["更新先验<br/>P_i = P_{i-1} × (γ - M_i)"] PRIOR --> PUPDATE PUPDATE --> PNEXT["P_i 进入下一步骤"]

举个更直观的例子。假设有三个特征 A、B、C,第一步掩码输出是 0.9、0.1、0.0,意味着模型重点用了特征 A。第二步之前先验更新为 0.1、0.9、1.0,因此第二步的注意力模块在生成掩码时,面对"特征 A 已经被用掉了 90%"的现状,就有很大概率把选择重心转向 B 和 C。T 步之后每个特征的被选历史都沉淀在先验里,整个模型表现出"分阶段覆盖特征"的行为,而不是每一步都重复选同一批特征。

4.2 熵正则项:让掩码变果断

只有 Sparsemax 还不一定能保证良好的稀疏性,因为模型可能会选择"软稀疏",比如每个步骤都温柔地选一半特征。为了进一步鼓励掩码走向极端,TabNet 在损失函数里加了一个稀疏正则项,本质是对掩码分布的熵惩罚。

熵的概念可以简单理解成"选择结果的不确定程度"。如果掩码是 0.5、0.5,分布很不确定,熵就高;如果掩码是 0.9、0.05、0.05,分布明显偏向一个特征,熵就低。正则项会惩罚熵高的情况,逼着掩码往 0 或 1 两极化方向走,这会牺牲一点模型精度,但换来更清晰的特征选择,可以说是为可解释性付出的代价。

调参的时候,稀疏系数 λ 太小,模型和普通深度网络差别不大;λ 太大,每一步只敢选很少几个特征,特征之间的复杂交互可能学不到。我自己的经验是先从默认值跑通,然后观察训练日志里掩码的平均稀疏程度,如果每步平均非零特征数不到特征总数的 10%,就说明稀疏性可能过头了。

4.3 掩码流和数据流在画图时要区分开

在 Mermaid 图里,我强制自己遵守一条原则:数据流用实线箭头,掩码和先验流用虚线箭头。原因很简单,一张图里同时存在特征向量的流动和"权重向量"的流动时,如果全画成实线,读者几乎无法分清哪些线代表"信息在变化",哪些线代表"权重在调节"。Mermaid 里-->是实线,-.->是虚线,两者结合可以让掩码的"控制属性"不言自明。

5. 预训练阶段的编码器-解码器图:自监督怎么画

5.1 Encoder 和 Decoder 不是对称的

TabNet 除了常规监督训练,还支持自监督预训练,专门用来在无标签表格数据上学特征表示。预训练网络由 Encoder 和 Decoder 两部分组成。Encoder 就是前面讲的标准 TabNet 结构。Decoder 的结构则比 Encoder 简单,它不再包含注意力模块,我画这张图时最想强调的就是这个不对称性。

flowchart TB X[原始特征 x] --> RANDMASK[随机生成特征掩码 S] RANDMASK --> XMASK[部分特征被掩盖后的输入] XMASK --> ENC[TabNet Encoder] ENC --> REP[逐步骤特征表示] ENC --> ENCMASK[逐步骤注意力掩码 M_i] REP --> DEC[TabNet Decoder] ENCMASK --> DEC DEC --> XHAT[重构特征 x_hat] X --> LOSS[重构损失] XHAT --> LOSS

5.2 随机掩码和注意力掩码是两码事

画这张图时最需要解释清楚的,是"随机生成特征掩码 S"和"注意力掩码 M"这两者的区别。S 是预训练阶段人为制造的噪声,做法类似于从 D 个特征里随机选一部分保留,其余特征全部置为 0。M 则是 TabNet 内部学出来的注意力权重,它表示对特征重要性的判断。一个来自外部注入,一个来自模型计算。

可以类比成语文考试的完形填空。原始句子像是完好的特征表,先人为挖掉几个空,这就是 S;模型读到被挖空的句子后,内部会对每个位置做重要性判断,这就是 M;Decoder 根据已有的上下文和被判为重要的位置,尝试把空格补回去。如果补出来的值和原文足够接近,就说明模型从无标签数据里学到了特征之间的关联模式。

损失函数只在被掩盖的位置上计算,没有被掩盖的特征即使重构错了也不惩罚。原因很明显,S 为 0 的位置才是真正考验重构能力的位置,其他位置的信息要么原样保留在输入里,要么被 Encoder 直接"背"过去了,重构它们没有意义。

5.3 Decoder 为什么要复用 Encoder 的掩码

Decoder 不生成自己的注意力掩码,而是直接复用 Encoder 每一步算出来的 M。这是 TabNet 自监督方案里一个很聪明的设计。如果 Decoder 也带一套注意力模块,它在重构时可能会重新选择一组特征来帮助自己,这就和 Encoder 的特征选择逻辑脱钩了。复用同一个掩码,等于强制 Decoder 在"Encoder 认为重要的特征子集"上去做重构,这样学到的表示天然保留了特征选择的先验。

微调阶段的做法是:预训练结束后把 Decoder 丢掉,Encoder 后面接上分类用的全连接层,再用带标签数据正常训练。我经常被问到预训练收益大不大,在表格数据上要分情况。如果下游只有几千条有标签样本,而手头有几百万条无标签样本,预训练带来的提升非常明显。如果标签本身已经足够多,预训练更多是锦上添花,因为 TabNet 的监督训练本身就包含隐式的特征选择正则。

5.4 画这张图的连线细节

这张图我踩过一次坑:REP 和 ENCMASK 两个输出都连向 Decoder,但如果连线没有标注,读者会以为 Decoder 有两个独立输入,一个特征表示、一个掩码。实际使用中,掩码 M 要和每一层的特征表示逐元素相乘后再进入 Decoder 的上层结构。所以我在箭头文字上会标注"逐元素乘",避免读者理解成把两个张量拼接到一起。

6. 对照这些图踩过的 Mermaid 语法坑

6.1 节点 ID 和文本的特殊字符

Mermaid 的节点 ID 最多只能用字母、数字和下划线,建议大家统一用ATT1、FT2、PRIOR1这种风格,既好记又好查。节点文本则要注意,[、]、{、}这些符号在文本里出现会导致渲染失败。比如我想在节点里写P_i = P_{i-1} × (γ - M_i),直接放进去多半会出问题,需要给整个文本加双引号,或者换一种表达方式,例如直接写"更新先验"然后另起一行放公式。

换行在节点文本里要用<br/>,不能直接打回车。我最早画图时习惯用普通的换行分隔多个子标题,结果发现在 Mermaid 里只会渲染成空格。

6.2 子图和循环结构的组织方式

标准 TabNet 有多步结构,如果每一步都原样展开,图会非常长。我的做法是用subgraph把单步结构包成一个组,然后在主图里复制两次,分别表示第 1 步和第 2 步,再用注释文字说明"后续步骤结构相同"。需要注意,Mermaid 子图里的节点 ID 仍然是全局唯一的,所以复制单步结构时必须给每个节点的 ID 加上步骤前缀,例如把FT改成FT1和FT2,否则渲染时会因为 ID 重复报错。

6.3 用 classDef 区分不同类型的流

Mermaid 支持classDef批量定义样式。我习惯定义三种颜色:数据流模块用一种底色,注意力相关模块用一种底色,先验更新模块再用一种底色。边的类型里,实线-->表示数据传递,虚线-.->表示掩码或先验传递。实际效果是,读者扫一眼就能知道图上哪些是主体计算流程,哪些是控制信号。样式代码可以集中放在图末尾:

classDef dataflow fill:#e1f5e1,stroke:#2e7d32 classDef attention fill:#fff3e0,stroke:#e65100 classDef prior fill:#e3f2fd,stroke:#1565c0 class FT1,FT2,SUM,FC dataflow class AT1,AT2 attention class PRIOR1,PRIOR2 prior

6.4 不同渲染环境下的兼容性问题

同样的 Mermaid 代码,在 GitHub 上渲染正常,在 Typora 里显示可能有细微差异。最典型的差异是<br/>的换行高度,以及子图标题的字体大小。如果对效果有严格要求,建议先在本地用 mermaid-cli 把图导出成 SVG 或 PNG,再放入文档,这样效果完全可控。但代价是修改后需要重新导出,GitHub 原生渲染则永远是最新代码对应的图。我现在的习惯是,博客文章用 GitHub 原生渲染,正式汇报的 PPT 里则导出成 SVG 以保证排版统一。

画 TabNet 这套图的时候,我最大的体会是:真正难画的不是 Mermaid 语法,而是先想清楚某个张量到底走的是数据流、掩码流还是先验流。只要这个想明白了,图的连线自然就合理了。这些图我在多个项目文档里复用,每次讲解前都只需要微调节点文本,整体结构几乎没变过。如果你也在写 TabNet 相关的内容,不妨先画主干、再拆单步,按这个顺序来,最后成图的可读性会好很多。

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

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

立即咨询