• 欢迎关注我的微信公众号“ Falost ” 右边扫描关注 --->>

多模态 RAG 实测(七):四字段加权搬回 247 页真实语料——页面 top-10 从 0.5702 到 0.8070,但功劳不在字段加权

AI / 大模型 神棍 105℃ 0评论

上篇(六)我在合成语料上把“页面检索拆字段”钉死了:正文列单独打分 R@5 只有 0.3444,四字段拼起来 0.6556,再叠加权到 0.6889,权重给 2~3 就够。结尾我说要搬回真实那 247 页跑回归,看 Gold 页进 top-10 能不能从 0.5702 抬上去。

这篇把账结了,结果跟预期不一样:真实语料上字段加权几乎没干活(0.5702 → 0.5789),真正把页面 top-10 抬到 0.8070 的是换分词器。这两件事在合成语料上看不出区别,在真实语料上是决定性的。

一、实验口径

语料还是那 247 页:80 篇我自己的博客、70 篇 arXiv、60 页 MDN、20 个 GitHub 仓库主页、17 页 DeepSeek 文档,正文是上一轮 OCR 转写出来的文本。题目还是 121 道里 Gold 表能定位的 114 道,跟(四)(五)同一批。

四个字段:body(OCR 转写正文)、title(网页标题)、headers(该页所有 DOM 表的表头行拼起来)、urlpath(URL 主机名 + 路径切词,/p/841.html → www fedte cc p 841 html)。真实语料下它们的 token 规模差得很远:

字段 token 中位 token 均值 合计
body 846 1026.4 253531
title 22 25.7 6352
headers 0 8.8 2171
urlpath 12 14.1 3472

247 页里只有 116 页有表,所以 headers 字段的中位 token 是 0——一半以上页面这个字段是空的。全文 |D| 最小 8、中位 890、最大 3340,avgdl 1075。

二、先给结果

只动分词器和字段两个因素,7 组配置,表里是 Gold 页进 top-k 的比例:

配置 R@1 R@5 R@10 MRR
正文单列 / char+bigram(上篇基线) 0.4211 0.5351 0.5702 0.4803
正文单列 / unicode61 0.5614 0.5789 0.6404 0.5903
正文单列 / trigram 0.5439 0.6316 0.6491 0.5804
四字段拼接 / char+bigram 0.4298 0.5351 0.5702 0.4930
四字段加权 (1,3,3,1) / char+bigram 0.4649 0.5351 0.5789 0.5131
四字段加权 (1,3,3,1) / unicode61 0.6930 0.7895 0.8070 0.7349
四字段加权 (1,3,3,1) / trigram 0.6228 0.7193 0.7456 0.6629

上篇预告问的那个数:Gold 页进 top-10 从 65/114 的 0.5702 抬到 92/114 的 0.8070,R@1 从 0.4211 到 0.6930,MRR 从 0.4803 到 0.7349。

三、字段加权这次几乎没干活

四字段加权叠在 char+bigram 上,R@10 只从 0.5702 挪到 0.5789,多中 1 道题;上篇在合成语料上同一个动作是 0.3444 → 0.6889,翻倍。

原因不玄:合成语料里查询词只出现在 headers 里,正文根本没有,所以“表头单独建字段”是唯一信号;真实语料里正文是 OCR 转写,表格内容本来就在正文里,标题和表头只是重复信号。字段加权的边际收益,取决于“目标信息在正文里缺多少”。

分词器一换,正文单列自己就从 0.5702 到 0.6404,再叠字段加权才到 0.8070。所以顺序是先修分词,再做字段加权;反了就会误判。

四、分词器:unicode61 赢了,trigram 输了

这结论我一开始也不信,毕竟上篇刚说过“unicode61 对中文完全不分词”。两边边界都跑了一遍,先看探针:

原文:表格里版式为文档的那一行页数列的值是多少
unicode61 切出来:['表格里版式为文档的那一行页数列的值是多少']   <- 1 个 token
MATCH "页数" -> 0 命中     MATCH "版式" -> 0 命中

上篇的结论没错:没有标点断开的连续中文,unicode61 会当成一个 token,词检索不到。但真实语料的正文是 OCR 转写,换行和空格都保留着,中文本来就被断开了,unicode61 反而能切出“版式”“页数”这种整词——同一个分词器,两种文本给出相反结果,问题在“你的文本有没有边界”。

trigram 输得很干脆:最短可查长度是 3。拿 4 篇文档做探针(其中一篇明确含“页数”):

len=1  MATCH '页'   -> 0        len=2  MATCH '页数'  -> 0    <- 2 个字永远匹配不到
len=3  MATCH '页数的' -> 1        len=2  MATCH 'ri'   -> 0
len=2  MATCH '12'  -> 0        len=3  MATCH '123'  -> 1

中文里 1~2 字的词太多。114 道题里被 trigram 丢掉的片段,出现最多的是:为(41)、表格(40)、第(40)、值为(37)、2(29)、行(20)、精度(5)、指标(4)、维度(4)、框架(4)、版式(3)。“表格”“值为”“行”都是表结构本身的核心词,丢了就是丢了。代价还大:

分词器 索引文件 词表 总 token
unicode61 1.32 MB 13055 91385
trigram 2.94 MB 53674 601094

trigram 索引大 2.2 倍、词表大 4.1 倍、token 多 6.6 倍,召回反而更差。

五、权重给 3 最稳

标题和表头两列权重从 1 扫到 20(unicode61,(1,wt,wt,1)):

权重 R@1 R@5 R@10 MRR top-10 命中
(1,1,1,1) 0.6754 0.7719 0.7895 0.7229 90/114
(1,2,2,1) 0.6754 0.7895 0.7895 0.7252 90/114
(1,3,3,1) 0.6930 0.7895 0.8070 0.7349 92/114
(1,8,8,1) 0.7018 0.8070 0.8070 0.7452 92/114
(1,20,20,1) 0.7105 0.8070 0.8070 0.7487 92/114

R@10 从权重 3 起饱和在 0.8070;R@1 到 20 还在慢慢涨(0.7105),MRR 只动到 0.7487,代价是噪声词权重越来越大。跟上篇一致:给 3~5 就够,往上加只调排序、不涨召回。

六、复盘那 16 道拿错表的题

类型 n 旧口径 Gold 页进 top-10 新口径 Gold 页进 top-10
坐标题(第 i 行第 j 列) 12 2/12 0/12
值→列名 4 3/4 4/4

值→列名这 4 道全救回来了:题面带真实单元格值(“Counter”“Microsoft Research”),这些值在正文和表头里都有词面,加权一上就进 top-1 或 top-2。

坐标题还是死的。20 道坐标题的题面片段在 247 页语料里的命中情况:

这个表格去掉表头后      -> 0/247 页    (模板语,语料里根本不存在)
列的单元格内容是什么    -> 0/247 页    (同上)
第 -> 34/247     行 -> 67/247     2 -> 109/247
3 -> 100/247     5 -> 95/247     7 -> 86/247     4 -> 44/247

除了“第…行…列”和几个数字,没有一个词是关于“这一页讲了什么”的。数字 2、3、5、7 在语料里 df 高到 44~109 页,IDF 趋零,排序约等于随机。旧口径那 2/12 是运气——随机排 top-10 的期望就是 10/247 × 12 ≈ 0.5 道。

结论跟(五)(六)一致:坐标题在检索层没有信号,修不了。这类问题本身缺前提——“第 2 行第 5 列”是哪张表的?产品上只能要求提问时带上表,或者干脆不让用户这么问。

七、对整条链路的影响

(五)测过:Gold 页进了 top-10 之后,表级检索 65/65 全中。所以页面召回就是两段式的天花板——它从 0.5702 到 0.8070,端到端表级上界就抬到 0.8070,这一步没动任何表级代码。新系统按题型的页级召回:

题型 n R@1 R@5 R@10
flat(行内查找) 41 0.8537 0.9512 0.9756
struct_col_lookup 37 0.8378 0.9459 0.9730
struct_header(值→列名) 13 0.8462 1.0000 1.0000
struct_colmax 2 1.0000 1.0000 1.0000
struct_rowmax 1 0.0000 1.0000 1.0000
struct_cell(坐标题) 20 0.0000 0.0000 0.0000

去掉坐标题,剩下 94 道页级 top-10 基本都在 0.97 以上。

三个坑:① 114 道题只来自 27 个页面,题在页面上聚簇,不能当独立样本看。② headers 取的是 DOM 表头,而题目就是从这些表头生成的,等于“把查询目标本身建成了字段”,会高估纯语义检索的收益。③ unicode61 赢的前提是 OCR 转写保留了换行和空格;换成语义连续、没有标点的中文长串,它会退化成“整段一个 token”,召回归零——探针已验证。

总结

一句话:字段加权机制是对的,但它在真实语料上的收益远小于“换一个能切出整词的分词器”。0.5702 → 0.8070 里大头是分词器(正文单列 0.5702 → 0.6404),字段加权只是锦上添花。

四条落地:

  1. 先修分词,再做字段加权,顺序反了会误判。
  2. 中文场景别默认 trigram:1~2 字的词检索不到,索引还大 2.2 倍。
  3. unicode61 能不能用,先把典型中文丢进 fts5vocab 看 token,一秒就知道。
  4. 表头单独建字段:加上它 R@10 从 0.7895 到 0.8070,而这 116 页正是出题的地方。

下期预告

unicode61 能赢,是因为我的语料恰好有换行把中文断开——这属于运气,不是方案。下篇把它当正经问题做:写进 FTS5 之前先做中文预分词(jieba),把正文 + 标题 + 表头切好再用空格拼回去,跟 unicode61、trigram、我手写的 char+bigram 做四方对照;顺带试查询侧 df 截断(df 超过一定比例的词直接扔掉,比如“第”“行”),看能不能把坐标题之外那几道 top-10 边缘的题捞回来。

参考文献

  • 上一篇:(六)页面检索拆成四个字段(本文基线 0.5702 的出处):
    https://www.fedte.cc/p/845.html
  • (五)把检索从页下沉到表(表级 65/65 的出处):
    https://www.fedte.cc/p/844.html
  • (四)把“数格子”从模型手里拿走:
    https://www.fedte.cc/p/843.html
  • SQLite FTS5 官方文档(bm25()、列权重、tokenize=、fts5vocab):
    https://www.sqlite.org/fts5.html
  • SQLite FTS5:unicode61 分词器(CJK 按字母类别成 token):
    https://www.sqlite.org/fts5.html#unicode61_tokenizer
  • SQLite FTS5:trigram 分词器(最短 3 字符):
    https://www.sqlite.org/fts5.html#the_trigram_tokenizer
  • Robertson, Zaragoza:The Probabilistic Relevance Framework: BM25 and Beyond(k1/b 的依据):
    https://www.staff.city.ac.uk/~sbrp622/papers/foundations_bm25_review.pdf
  • ColPali 论文(Faysse et al., arXiv:2407.01449;本系列路线 B,本文未实测):
    https://arxiv.org/abs/2407.01449

转载请注明:Falost的小窝 » 多模态 RAG 实测(七):四字段加权搬回 247 页真实语料——页面 top-10 从 0.5702 到 0.8070,但功劳不在字段加权

如果你觉得这篇文章不错或者对你有帮助,想请我喝一杯咖啡,可以打赏
喜欢 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址