上篇(六)我在合成语料上把“页面检索拆字段”钉死了:正文列单独打分 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),字段加权只是锦上添花。
四条落地:
- 先修分词,再做字段加权,顺序反了会误判。
- 中文场景别默认 trigram:1~2 字的词检索不到,索引还大 2.2 倍。
- unicode61 能不能用,先把典型中文丢进 fts5vocab 看 token,一秒就知道。
- 表头单独建字段:加上它 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,但功劳不在字段加权