Natural Memory NM2.1: 记忆路由器分叉、数据集缺陷修复与全轴评测证据
- 引入 MemoryRouterXL 与 v5/v6 流式多线程训练/编码管线 - 修复 prepare_memory_router_dataset 候选池重建缺陷(mega 家族 3568x 加速,输出逐字节相同) - 修复 v5 被破坏的拒答与多跳标签(train 未知样本 319 -> 16319,multi_hop 平均正例 1.00 -> 2.00) - 同存储预算下 V2-128 v6 逐轴 22/22 通过:Top-1 41.12% -> 94.62%,未知拒答 0.00% -> 100.00% - 记录三条被实测推翻的显然优化(logits_to_keep=1 反而慢 55%、XL 容量未带来收益) - 记忆手术跨架构可移植性 14/14,读写关闭时与原生模型逐位相同
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""Regression tests for the end-to-end answer scorer.
|
||||
|
||||
Both defects locked in here had produced headline numbers that were wrong enough
|
||||
to send the project after the wrong bottleneck, so they get tests rather than
|
||||
comments:
|
||||
|
||||
* whitespace sensitivity turned 19 correct multi-hop answers into failures
|
||||
(multi-hop read 24.00% when it was 100.00%);
|
||||
* keyword-only refusal detection missed 12 of 13 real refusals on the
|
||||
unknown-attribute category (the axis read 0.00% when it was 52.00%).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from V2_dpskw.eval_scoring import is_refusal, score_case, squash
|
||||
from V2_dpskw.eval_end_to_end_memory import evidence_write_order
|
||||
|
||||
|
||||
def case(acceptable, answerable=True):
|
||||
return {"acceptable": acceptable, "answerable": answerable}
|
||||
|
||||
|
||||
class SquashTest(unittest.TestCase):
|
||||
def test_removes_ascii_and_full_width_whitespace(self):
|
||||
self.assertEqual(squash("值班人 -5259"), squash("值班人-5259"))
|
||||
self.assertEqual(squash("值班人\u3000-5259"), squash("值班人-5259"))
|
||||
self.assertEqual(squash("每周三 22:00"), squash("每周三 22:00"))
|
||||
|
||||
def test_is_case_insensitive(self):
|
||||
self.assertEqual(squash("REF-9x6uye"), squash("ref-9X6UYE"))
|
||||
|
||||
def test_keeps_meaningful_punctuation(self):
|
||||
# Punctuation inside anchors is meaningful and must survive.
|
||||
self.assertNotEqual(squash("10.20.3.7:5432"), squash("1020375432"))
|
||||
|
||||
|
||||
class WhitespaceInsensitiveMatchingTest(unittest.TestCase):
|
||||
def test_spaced_identifier_still_scores_correct(self):
|
||||
scored = score_case(case(["值班人-5259"]), "负责该项目的同事对应的值班人是 值班人 -5259。")
|
||||
self.assertTrue(scored["correct"])
|
||||
self.assertEqual(scored["matched"], ["值班人-5259"])
|
||||
|
||||
def test_wrong_identifier_is_still_wrong(self):
|
||||
scored = score_case(case(["值班人-5259"]), "负责该项目的同事对应的值班人是 值班人-5266。")
|
||||
self.assertFalse(scored["correct"])
|
||||
|
||||
def test_missing_value_is_wrong(self):
|
||||
scored = score_case(case(["值班人-5259"]), "负责该项目的同事对应的值班人是 值班人。")
|
||||
self.assertFalse(scored["correct"])
|
||||
|
||||
|
||||
class RefusalDetectionTest(unittest.TestCase):
|
||||
def test_recognises_the_wording_the_model_actually_uses(self):
|
||||
for reply in (
|
||||
"关于您的 weekly 会议安排,当前长期记忆中未包含相关信息,无法回答。",
|
||||
"关于您这周按什么表轮,当前长期记忆中未包含该信息。已知信息如下:user;培训周期;78%",
|
||||
"关于报警触发条件,当前记忆中没有相关记录。已知信息:user;保险到期;A-7719。",
|
||||
"您的协议编号未知。",
|
||||
"关于变更时段,当前长期记忆中未包含具体信息,证据不足,明确说不知道。",
|
||||
):
|
||||
with self.subTest(reply=reply):
|
||||
self.assertTrue(is_refusal(reply))
|
||||
|
||||
def test_does_not_fire_on_asserted_values(self):
|
||||
for reply in (
|
||||
"您的培训周期是 32GB。",
|
||||
"您的东西放在 63.2% 的仓库库位。",
|
||||
"我固定每周三 22:00 做上线。",
|
||||
):
|
||||
with self.subTest(reply=reply):
|
||||
self.assertFalse(is_refusal(reply))
|
||||
|
||||
def test_unanswerable_case_accepts_a_refusal_in_any_wording(self):
|
||||
scored = score_case(case([], answerable=False), "当前长期记忆中未包含相关信息,无法回答。")
|
||||
self.assertTrue(scored["correct"])
|
||||
self.assertTrue(scored["abstained"])
|
||||
|
||||
def test_unanswerable_case_still_fails_when_a_value_is_asserted(self):
|
||||
scored = score_case(case([], answerable=False), "您的培训周期是 32GB。")
|
||||
self.assertFalse(scored["correct"])
|
||||
|
||||
def test_answerable_case_that_refuses_is_a_false_refusal(self):
|
||||
scored = score_case(case(["32GB"]), "关于您上手所需的时间,当前长期记忆中未包含此信息,无法回答。")
|
||||
self.assertFalse(scored["correct"])
|
||||
self.assertTrue(scored["wrongly_abstained"])
|
||||
|
||||
def test_answerable_case_with_hedge_but_correct_value_is_not_false_refusal(self):
|
||||
scored = score_case(
|
||||
case(["凌晨 03:30"]),
|
||||
"那杯偏爱 凌晨 03:30 的口味是未知,因为长期记忆中仅记录了时间(凌晨 03:30)和饮品名称。",
|
||||
)
|
||||
self.assertTrue(scored["correct"])
|
||||
self.assertFalse(scored["wrongly_abstained"])
|
||||
|
||||
|
||||
class EvidenceWriteOrderTest(unittest.TestCase):
|
||||
"""The harness write order is a measurement decision, so it gets a test.
|
||||
|
||||
Two distinct failures lived here: answering facts written too early made
|
||||
`update_conflict` unsolvable by construction, and reserving no slots for them
|
||||
dropped the answer entirely on `noise_context` (72.00% -> 0.00%).
|
||||
"""
|
||||
|
||||
def test_answering_facts_are_written_last(self):
|
||||
case = {"positives": ["ANSWER"], "facts": ["d1", "d2", "ANSWER", "d3"]}
|
||||
order = evidence_write_order(case, 6, answer_last=True)
|
||||
self.assertEqual(order[-1], "ANSWER")
|
||||
self.assertEqual(len(order), 4)
|
||||
|
||||
def test_answering_facts_survive_more_distractors_than_slots(self):
|
||||
# noise_context: 8 distractors against 1 answering fact, 6 write slots.
|
||||
case = {"positives": ["ANSWER"], "facts": ["d%d" % i for i in range(8)] + ["ANSWER"]}
|
||||
order = evidence_write_order(case, 6, answer_last=True)
|
||||
self.assertEqual(len(order), 6)
|
||||
self.assertIn("ANSWER", order)
|
||||
self.assertEqual(order[-1], "ANSWER")
|
||||
self.assertEqual(order.count("ANSWER"), 1)
|
||||
|
||||
def test_all_answering_facts_survive(self):
|
||||
case = {"positives": ["A1", "A2"], "facts": ["d1", "d2", "d3", "d4", "A1", "A2"]}
|
||||
order = evidence_write_order(case, 6, answer_last=True)
|
||||
self.assertIn("A1", order)
|
||||
self.assertIn("A2", order)
|
||||
self.assertEqual(order[-2:], ["A1", "A2"])
|
||||
|
||||
def test_answer_first_reproduces_the_old_behaviour(self):
|
||||
case = {"positives": ["ANSWER"], "facts": ["d1", "d2", "ANSWER"]}
|
||||
self.assertEqual(evidence_write_order(case, 6, answer_last=False), ["ANSWER", "d1", "d2"])
|
||||
|
||||
def test_never_exceeds_the_slot_budget(self):
|
||||
case = {"positives": ["A"], "facts": ["d%d" % i for i in range(20)] + ["A"]}
|
||||
self.assertLessEqual(len(evidence_write_order(case, 6)), 6)
|
||||
self.assertLessEqual(len(evidence_write_order(case, 2)), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user