论文接收通知弹出来的时候,我刚把手头最后一版表格纠错脚本跑完。回想这个项目从想法到定稿的过程,最核心的那句话其实就是标题里写的:先看懂结构,再生成答案。StrucTab 这套方法最终被 ECCV 2026 接收,但它在我的实操中真正解决的问题,不是又刷高了一个指标,而是把表格问答从"黑盒猜答案"变成了"有据可查的结构化推理"。
如果你正在做文档理解、表格抽取、PDF知识库问答,或者只是被"从截图里问表格问题"这件事折磨过,下面这些内容大概率会戳中你。这篇博文我会从动机、模型设计、训练细节、实验结果、落地踩坑到后续方向,全部摊开来讲。不吹方法论的花哨程度,只讲哪些设计是真的有用,哪些地方我们返工了不止一次。
1. 表格解析为什么难:从"看到"到"看懂"之间,隔着一个结构
1.1 表格的二维性,把一维模型的底裤全掀了
文本的阅读顺序是线性的,从左到右、从上到下,Transformer 处理起来天然顺手。但表格是二维信息载体,信息藏在"行列交叉"和"表头层级"里,而不是藏在某一个 token 的上下文里。
举个例子,一个销售报表里有"季度""区域""销售额"三列,下面若干行,其中"季度"单元格跨了两行。这种跨行合并在视觉上是一个大格,但在文本序列化之后,它会被表示成"季度、Q1、Q1"之类的重复文本,模型根本不知道这三个 token 其实共同属于某一个原始单元格。你做问答时问"Q1 华北的销售额是多少",模型可能在序列里找到"华北""销售额",然后生成了一个看似合理的数字,但它并不知道自己引用的到底是哪个单元格、这个数字对不对齐。
这就是我常说的:表格问答的问题,一半以上不是语义理解的问题,而是结构定位的问题。
1.2 端到端生成式方案的典型翻车现场
我们在项目早期踩过一条弯路:直接用多模态模型,输入表格截图和问题,输出答案。效果不能说完全不能用,但翻车的姿势极其统一:
- 答案引用错位:问"2023 年毛利率最高的区域",模型答"华北",实际上答案是"华东",因为模型把表头第二行的区域名和第三行的数值错位对齐了;
- 数字张冠李戴:问"研发费用占营收的比例",模型把利润表里的数字当成研发费用,因为序列化之后"研发"和"费用"两个词出现在同一个上下文窗口里,但它们的结构归属完全不同;
- 合并单元格语义丢失:跨两行的表头,模型读了上半部分就忽略了它覆盖的所有行;
- 答案对但过程不可信:偶尔模型答对了,但你追问"这个数字来自哪一行哪一列",它完全说不出来。这在金融、医疗场景里是不能接受的。
这些现象背后的原因其实挺简单:端到端模型把"定位结构"和"语义推理"揉成了一个隐式过程,但它既没有显式的行列概念,也没有强制性的单元格对齐约束。模型只是学会了"看起来像答案"的文本延续,而不是"真的从表格里查到了答案"。
1.3 一个反直觉但极其重要的实验观察
让我们真正转向"先结构、后答案"路线,源于一个中间实验。我们把一个已训练好的端到端问答模型做了个改动:不直接输入原始表格,而是先把表格解析成带行列标签的结构化表示,再喂给模型。结果非常直观——答案准确率直接涨了近 10 个百分点。
这个实验强烈暗示了一件事:模型其实具备一定的语义推理能力,它缺的不是"理解问题"的能力,而是"知道数据在哪里"的能力。换句话说,只要把结构信息显式喂进去,它能更好地发挥自己的推理能力。
由此,StrucTab 的核心设计定了下来:两阶段。第一阶段做结构归纳(Structure Induction),把表格还原成一张结构图;第二阶段做有据可答的答案生成(Grounded Answer Generation),模型必须看着结构图、引用具体单元格来回答。
2. StrucTab 的建模路线:先把表格还原成一张可查询的图
2.1 表格结构图的定义:不止是行列
我们最初的想法很简单:把表格提成"行 + 列"就够了。但真正做了才发现,真实表格远比理想模型复杂——跨行合并、跨列合并、三层表头、表头在中间、斜线单元格。于是我们把结构表示升级成了一张有向图:
- 节点:每个视觉上的单元格(包括合并后的整体大格)是一个节点,带边界框和文本内容;
- 同行关系:同属一行的单元格之间连边;
- 同列关系:同属一列的单元格之间连边;
- 覆盖关系(Span):如果某单元格跨越了多行或多列,它和它覆盖的所有单元格之间建立从"父格"到"子格"的边;
- 表头归属:某些单元格在语义上是表头节点,它覆盖下方若干数据行。
这张图并不复杂,但它把表格最关键的三种信息都装进去了:物理位置、行列归属、合并结构。有了它,一个模型就可以很自然地回答"Q1 华北的销售额"这类问题——先通过覆盖关系找到 Q1 对应的行集合,再通过同列关系定位销售额列,最后在交叉点找到数值单元格。
从工程角度说,图表示还有一个隐藏优势:它可以把表格的视觉形态和语义内容解耦。视觉上有线框还是没有线框,不影响图结构;同一张表的不同排版版本,只要内容一样,结构图就一样。这让模型在一个来源上学到的表结构,能迁移到另一个来源上。
2.2 两阶段管线:结构先于语义
StrucTab 不做端到端的"图像到答案",而是分成明确的两步:
阶段一:结构归纳。输入是表格图像和 OCR 出来的文本 token(每个 token 带二维坐标),输出是上面说的表格结构图。这一步本质上是"看懂表格长什么样"——预测每个单元格的边界框,预测单元格之间的同行/同列/覆盖关系。
阶段二:有据答案生成。结构图连同原始图像特征、OCR 文本一起送入一个解码器。模型在生成答案的每一步,都可以选择"复制"图中某个单元格的内容,或者参考某几个单元格的结构关系做推理。最终输出的必须是一个指向真实单元格的答案——它不仅要生成文本,还要顺带生成答案对应的单元格引用。
为什么坚持两阶段而不是一个模型全干了?我们评估过端到端的联合训练方案,效果其实也不差,但它有两个致命短板:第一,中间结构不可见,模型答错了你不知道是结构错了还是推理错了;第二,没法做人工干预——结构错了你不能只修正结构,只能从头重跑。两阶段方案在这两个问题上天然友好:结构阶段模型输出的东西可以直接可视化,人工瞄一眼就知道表格是不是被理解对了。
2.3 为什么不直接用"表格转 Markdown/HTML 喂大模型"
可能有人会说:把表格序列化成 Markdown 或 HTML,然后喂给大语言模型,不是更省事吗?坦白说,这条路我们在项目早期试过,效果差距很大。
问题出在序列化本身是有损的。Markdown 表格要求每个单元格都有文本,但真实表格里大量单元格是空的;跨行合并的单元格在 Markdown 里根本表达不了;三层表头序列化之后层级关系全靠缩进猜测。HTML 稍好一点,但 OCR 出来的文本明显错位时,HTML 的行列结构也会跟着错。
更重要的是,序列化之后的表结构是"隐式"的——模型得自己从文本里重新推断行列关系。而 StrucTab 的结构图是"显式"的——每个单元格、每条关系都是模型明确预测出来的。显式结构的容错性高得多,因为你可以对结构做后处理修正,再丢给生成阶段。
3. 核心模块与实现细节:结构头、关系预测、对齐解码
3.1 输入编码:图像特征和文本特征怎么融合
很多失败的表格模型,问题出在输入侧把图像和文本分开处理,只在最后简单拼接。我们这里做了一个关键处理:给每个 OCR 文本 token 附加二维位置嵌入。
每个 token 的特征由三部分组成:文本编码器输出的语义向量、归一化后的坐标 [x1, y1, x2, y2] 映射成的位置向量、所在视觉区域的图像特征。三者融合后输入交叉注意力层,和全局视觉特征反复交互。这个设计的直观含义是:模型在阅读一个 token 时,始终知道"这个字在图像的哪个位置",从而能建立"文本-位置-视觉"三者对应。
这个设计的直接收益是,处理合并单元格时模型有了依据。合并单元格虽然只有一个文本,但它的边界框覆盖了多行多列,视觉特征里天然包含了大范围空白区域。模型结合大边界框和区域视觉特征,更容易判断出"这是一个 span 节点"。
3.2 结构预测头的设计:检测、关联、回归
结构阶段我们用了三个并行头:
- 单元格检测头:基于融合特征预测每个单元格的边界框。我们用了类似"无锚框"的检测方式,直接预测中心点加宽高。单元格目标本身就比通用目标检测简单,因为表格里大多数格子排列规整,这个头收敛得很快;
- 关系分类头:对候选单元格对做关系预测。不是全图两两配对——那计算量太大——而是先用强先验缩小范围:边界框在垂直或水平方向重叠的才做配对。每个候选对预测三种关系概率:同行、同列、覆盖;
- 坐标回归头:处理扫描件里的轻微倾斜和形变,对边界框做精细化回归。
结构损失是检测损失和关系分类损失的加权和。我们实验中发现,关系分类损失的权重可以稍微调大一点,因为最终对问答质量影响最大的,是关系判断对错,而不是边界框差一两个像素。
# 伪代码:StrucTab 结构预测与答案生成主流程 def stractab_inference(table_image, ocr_tokens): # 阶段一:结构归纳 visual_feats = vision_encoder(table_image) text_feats = text_encoder(ocr_tokens, token_positions) fused_feats = cross_attn(visual_feats, text_feats) cell_boxes = cell_detection_head(fused_feats) cell_pairs = build_candidate_pairs(cell_boxes) # 用重叠先验剪枝 row_rel, col_rel, span_rel = relation_head(cell_pairs, fused_feats) table_graph = build_table_graph(cell_boxes, row_rel, col_rel, span_rel) # 阶段二:基于结构生成答案 graph_feats = graph_encoder(table_graph, fused_feats) answer = grounded_decoder(graph_feats, question_embedding) return answer, table_graph, cell_boxes3.3 解码器的"看结构"机制:让答案扎在单元格上
答案生成阶段我们最看重一件事:生成的答案必须可以追溯到具体单元格。为此在解码器里加了一个结构感知的指针机制:
- 每个生成步,解码器可以决定"从结构图里复制某个单元格的文本"还是"生成一个常规 token";
- 当选择复制时,模型会额外输出一个单元格索引,这就是答案的引用位置;
- 训练时,我们要求答案里涉及的每一个关键数值,都要存在对应的单元格引用,否则这个样本算错。
刚开始我们只做了复制机制,但发现有些问题是需要多步推理的——比如"求两个区域销售额之和",答案不是任何一个单元格的文本。后来我们放开成混合生成:允许模型先引用相关单元格,再做简单算术生成一个新 token 序列。效果比纯复制或者纯生成都好,因为模型有了稳定的"抓手"。
3.4 训练策略:先结构后问答,合成数据打底
训练上有两个经验非常关键。
第一,两阶段训练比联合训练稳定得多。我们先单独训练结构阶段直到收敛——通常两三个 epoch 就够——然后把结构头固定,再训练答案生成阶段。如果一开始就联合训练,结构阶段还没学好的时候,生成阶段会疯狂根据错误结构猜答案,两边的梯度相互干扰,收敛很慢。
第二,合成数据是结构阶段的主力。真实标注表格结构非常贵——一个合并单元格复杂点的表格,人工标注就要几分钟。我们写了一个表格渲染器:随机生成各种结构(随机行列数、随机合并、随机表头层级),用 HTML 渲染成图像,再加随机字体、边框样式、纸张噪声、透视扰动。这样一天能生成几万张带精确标注的表格图。效果上,结构阶段在合成数据上训练完,迁移到真实扫描表格时已经能到可用的水准,之后再用几千张真实标注微调,就能稳定上去。
4. 实验设置与结果解读:结构带来的提升到底有多大
4.1 评测基准怎么选
我们用了两类公开基准来验证:
- 结构识别基准:包含大量带行列表格、合并单元格、多层表头的表格图像,指标是单元格检测 F1 和结构关系准确率;
- 表格问答基准:给定表格图像和自然语言问题,要求输出答案并给出答案对应单元格。我们额外加了一个评测维度——引用准确率,即答案引用的单元格是否真的包含了答案内容。这个指标是很多同类工作不报的,但恰恰是落地时最需要的。
为了让评测更狠,我们自己构建了一个困难子集,专门挑有合并单元格、表头跨行跨列、表格内嵌图像的样本。目的就一个:别让模型在"简单表格上刷分,复杂表格上翻车"。
4.2 主要结果对比
摘录几个代表性的数字(与我们自己的复现基线对比):
| 方法 | 结构 Cell F1 | 答案 EM | 答案+引用准确率 |
|---|---|---|---|
| 基线A:端到端多模态生成 | 未建模 | 76.8 | 无法计算 |
| 基线B:表格序列化喂语言模型 | 未建模 | 81.2 | 38.5 |
| StrucTab(无引用模块) | 92.3 | 85.9 | — |
| StrucTab(完整版) | 92.3 | 89.4 | 84.7 |
表格序列化方案比端到端强一点,但引用准确率只有三成多,这意味着它答对的答案里有一大半根本不知道出处。StrucTab 的答案+引用准确率能做到 84.7%,这个数字对实际业务来说才是能用的:模型告诉你答案,同时告诉你答案在哪一格,用户可以一键跳过去复核。
4.3 消融实验:每一项设计都在干什么
我们做了几组消融,结果挺有意思:
- 去掉结构阶段,恢复端到端:答案 EM 掉了大约 9 个点。这就是前面说的核心观察——直接生成答案,结构信息确实会被模型隐式学到一点,但学得远远不够;
- 去掉视觉特征,只用 OCR 文本和坐标:结构 Cell F1 掉了大约 5 个点,集中体现在合并单元格和无线框表格上。视觉特征对判断"哪些格子是一个整体"帮助极大;
- 去掉引用约束:答案 EM 掉了 2-3 个点,引用准确率掉了近 20 个点。引用约束不仅仅是"可解释性"的加分项,它像一个正则化器,逼着模型在生成每个关键数字时找到结构依据,反而提升了准确性。
4.4 效率与部署成本
结构阶段和生成阶段加起来,单张表格在单卡上约 1.2 秒完成解析和回答。结构阶段本身只需要 0.4 秒,这给了工程上一个很重要的操作空间:
同一张表格的多轮问答,结构只解析一次,不同问题共享同一张结构图。实际问答系统里,用户常常连续问好多个问题,这时候首问 1.2 秒、后续每问 0.8 秒的体验会好很多。模型规模上,我们控制了结构阶段在十亿参数以内,生成阶段也做了量化,整体可以单卡部署。
5. 复现与落地中踩过的坑:三个真实问题
5.1 合并单元格:结构图里最容易翻车的地方
合并单元格是几乎所有表格模型的天敌,我们也不例外。初期版本里,跨两行的父格总会漏掉子格的关联,导致"Q1 华北销售额"这类问题完全找不到 Q1 对应的行集合。
排查之后发现,问题出在边界框标注上。人工标注时,有人把父格标注成一个大框,有人把父格拆成两个虚拟小框。两种标注语义不一致,模型直接学乱。我们的解决方法:统一标注规范,父格只允许标注为一个整体节点,子格保留自己的独立节点,父格和子格的关系在建图时自动生成。这个规则一固定,模型的学习目标就清除了,相关错误降了一半以上。
经验是:结构标注的一致性,比标注数量还重要。宁可少标,不可标乱。
5.2 长表格跨页:切片切不好,结构全乱
真实业务里很多表格一页放不下,会跨页打印或截图分割。直接切图的话,切在数据行中间,上半部分和下半部分的行关系就断了,模型无法判断"这个数字属于哪一行"。
我们的做法是分级处理:先做全局版面分析,判断哪些区域是同一张表格的分页;然后对每一页单独做细胞识别,但行关系从全局页面上继承——也就是说,跨页的行不会被强制合并,但列关系可以跨页对齐。同时把表头信息在跨页时冗余传给生成阶段,即使第二页没有表头,模型也知道当前列的语义。
工程上我们做了一个表格状态保持机制:处理完第一页后,把表头和列结构缓存起来,第二页进来时先做结构对齐,再走答案生成。实测跨页表格的答案准确率从 61% 提到了 82%。
5.3 扫描噪声和无线框表格:别让模型依赖分隔线
扫描件里的表格,线框可能是断的、有污渍、有阴影;也有很多表格是"无线框"的,只有留白分隔。初期模型很依赖视觉上的分隔线,一旦线框断裂就疯狂漏检。
解决思路是把"分隔线"作为辅助特征而不是必要条件。我们做数据增强时,随机擦除一部分线框、加入噪声、模拟印章遮挡,让模型学会用文本的位置分布和留白来判断单元格边界。另外,把纵向和横向的空隙特征显式编码进结构头——两个文本块之间空白很大,大概率是不同列的边界。实测带这类增强的模型,在无线框表格上的 Cell F1 涨了约 7 个点。
5.4 几个可以少走弯路的工程建议
- 先可视化结构图再谈优化。结构阶段每一步都要输出结构图可视化,肉眼检查比任何指标都更快发现问题;
- 后处理规则不能省。例如,答案引用的单元格必须存在于结构图中,如果不存在,模型输出直接拒绝;跨行表头在生成答案前,自动把覆盖关系展开到对应行;
- 输出校验做成硬约束。金融、医疗场景里,模型答错问题比不答更危险。我们最终版本里,只要答案的单元格引用缺失,就把答案标为"不确定",宁可拒答也不硬猜。
6. 落地之后的个人体会与后续想法
这个项目做到后期,最大的体会不是模型结构有多精巧,而是"显式结构"这个概念的价值被完全释放出来了。结构阶段让错误变得可见:模型答错了,你看一眼结构图就知道是行列关系错了还是推理错了,修起来有方向。这在面对客户或业务方时尤其重要——你可以指着结构图说"模型在这里把行对齐弄错了,我们改训练数据就行",而不是一句"模型不太行"。
后续方向我比较期待三个:
- 跨表推理:多张表格联合回答,比如"A表里的客户名单,在B表里对应的销售额总和",这需要表格图之间建立实体对齐关系;
- 表格与自由文本混排:版面里表格周围还有文字说明,问题可能既涉及表格又涉及文字,结构和文本的联合推理目前做得还不够;
- 手写表格:手写单元格的边界和内容都更模糊,结构预测和 OCR 得联合优化。
如果看到这里你也想动手,我的建议很简单:先别急着做问答,找一批多样化的表格图像,把结构预测这件事做到稳定,再往上加问答。结构是地基,地基稳了,后面所有功能都踏实。实测下来,这一步也是最值得花时间的。