role_context.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. # -*- coding: utf-8 -*-
  2. """角色规则上下文预计算(角色分类流程优化 Phase A)。
  3. 背景
  4. ====
  5. 角色分类链路中 ``prem`` / ``roleRule`` / ``roleRuleFinal`` /
  6. ``tendereeRuleRecall`` 四个 Predictor 各自遍历
  7. ``list_entitys × list_sentences`` 做实体-句子配对,并重复构建同一实体的
  8. 上下文切片:
  9. - ``prem.search_role_data`` — 字符切片(前 23 / 后 25),while 线性扫描
  10. - ``prem.search_money_data`` — 字符切片(前 13 / 后 15),while 线性扫描
  11. - ``RoleRulePredictor`` — ``get_context``(前后各 20 字)
  12. - ``TendereeRuleRecall`` — ``spanWindow``(前后各 40 词)
  13. 本模块把「配对 + 上下文构建」收敛为一次预计算:
  14. - 句子索引 ``{(doc_id, sentence_index): sentence}``,O(1) 定位实体所属句
  15. - ``RoleContext`` 惰性缓存三类上下文切片,按需计算、只算一次
  16. - 概率阶梯常量(命名原代码中的魔法概率,Phase C 起统一引用)
  17. 行为兼容性
  18. ==========
  19. - ``build_role_contexts`` 保留原 ``search_role_data`` 的就地排序副作用
  20. (``list_entity.sort`` / ``list_sentence.sort``)。
  21. - 与原 while 线性扫描的差异:当某个实体的 (doc_id, sentence_index) 在句子
  22. 列表中不存在时,原实现会把扫描指针推到列表末尾导致其后所有实体配对失败;
  23. 本实现按索引独立配对,不受前序缺口影响(属缺陷修正,已记录在优化日志)。
  24. 依赖方向
  25. ========
  26. CORE(``predictors`` 内部),仅依赖 ``common``,不依赖 ``rules/``,
  27. 符合 ARCHITECTURE.md §4.3 依赖方向约束。
  28. 源码集成约束(Phase 7 §7.8)
  29. ============================
  30. 本模块不加载 ``dl/rules/`` 的 YAML;规则外置的 YAML 目前作为
  31. 「source of truth」用于校验与文档,predictors 消费 YAML 须待
  32. Phase C pipeline 层规则注入机制就绪后进行。
  33. """
  34. from __future__ import absolute_import
  35. import re
  36. from BiddingKG.dl.common.context_utils import spanWindow, get_context
  37. __all__ = [
  38. "RoleContext",
  39. "build_role_contexts",
  40. "ROLE_LABELS",
  41. "P_MODEL_THRESHOLD",
  42. "P_RULE_OVERRIDE",
  43. "P_FALLBACK",
  44. "P_TITLE",
  45. ]
  46. # ----------------------------------------------------------------------------
  47. # 角色标签与概率阶梯
  48. # ----------------------------------------------------------------------------
  49. #: 角色标签 → 含义(与 Entity.label 语义一致)
  50. ROLE_LABELS = {
  51. 0: "tenderee", # 招标人/采购人
  52. 1: "agency", # 代理机构
  53. 2: "winTenderer", # 中标人
  54. 3: "secondTenderer", # 第二候选人
  55. 4: "thirdTenderer", # 第三候选人
  56. 5: "other", # 其他/无角色
  57. }
  58. # ---- 概率阶梯(Phase B 命名魔法数)------------------------------------------
  59. # 原代码中 0.501/0.51/0.55 等数值表示「略高于上游某置信度」的隐式约定,
  60. # 此处显式命名。现有 prem.py / role.py 中的字面量保持不变,
  61. # Phase C RoleRuleEngine 统一改用这些常量。
  62. #: 模型置信阈值:模型预测 prob < 0.5 时置 label=5,交给规则召回
  63. P_MODEL_THRESHOLD = 0.5
  64. #: 规则覆盖模型:略高于模型置信(原代码 0.501)
  65. P_RULE_OVERRIDE = 0.501
  66. #: 兜底召回:略高于标题召回基线(原代码 0.51)
  67. P_FALLBACK = 0.51
  68. #: 标题召回基线(原代码 0.55)
  69. P_TITLE = 0.55
  70. #: 参与角色/金额链路上下文预计算的实体类型
  71. _ROLE_CONTEXT_ENTITY_TYPES = ("org", "company", "money")
  72. class RoleContext(object):
  73. """单个实体的预计算上下文。
  74. 惰性计算三类切片并缓存;同一实体在多个 Predictor 间复用同一实例,
  75. 每种切片整个链路只计算一次。
  76. 属性
  77. ----
  78. entity / sentence : 原实体与所属句子(一次配对,替代 O(E×S) 扫描)
  79. role_model_text : prem 角色模型输入 (前文23字, 实体, 后文25字)
  80. money_model_text : prem 金额模型输入 (前文13字, 实体, 后文15字)
  81. span : RoleRulePredictor 用 get_context(前后各 20 字,
  82. [0]=前文 [1]=实体 [2]=后文)
  83. span_wide : TendereeRuleRecall 用 spanWindow(前后各 40 词,
  84. word_flag=True,实体文本做全半角括号归一)
  85. span_rule : RoleRulePredictor.rule_predict 输入用 spanWindow
  86. (前后各 40 词,word_flag=True,use_text=False)
  87. """
  88. __slots__ = ("entity", "sentence", "_span", "_span_wide", "_span_rule")
  89. def __init__(self, entity, sentence):
  90. self.entity = entity
  91. self.sentence = sentence
  92. self._span = None
  93. self._span_wide = None
  94. self._span_rule = None
  95. # ---- 便捷透传 ----
  96. @property
  97. def entity_type(self):
  98. return self.entity.entity_type
  99. @property
  100. def sentence_text(self):
  101. return self.sentence.sentence_text
  102. # ---- prem 模型输入切片 ----
  103. def _char_slices(self, front_size, behind_size):
  104. text = self.sentence.sentence_text
  105. b = self.entity.wordOffset_begin
  106. e = self.entity.wordOffset_end
  107. return (text[max(0, b - front_size):b], text[b:e], text[e:e + behind_size])
  108. @property
  109. def role_model_text(self):
  110. """prem.search_role_data 的文本切片:(前文 23 字, 实体, 后文 25 字)。"""
  111. return self._char_slices(23, 25)
  112. @property
  113. def money_model_text(self):
  114. """prem.search_money_data 的文本切片:(前文 13 字, 实体, 后文 15 字)。"""
  115. return self._char_slices(13, 15)
  116. # ---- roleRule 上下文(get_context,前后各 20 字)----
  117. @property
  118. def span(self):
  119. if self._span is None:
  120. e = self.entity
  121. self._span = get_context(
  122. self.sentence.sentence_text,
  123. e.wordOffset_begin, e.wordOffset_end,
  124. size=20, center_include=True)
  125. return self._span
  126. # ---- tendereeRuleRecall 上下文(spanWindow,前后各 40 词)----
  127. @property
  128. def span_wide(self):
  129. if self._span_wide is None:
  130. e = self.entity
  131. self._span_wide = spanWindow(
  132. tokens=self.sentence.tokens,
  133. begin_index=e.begin_index, end_index=e.end_index,
  134. size=40, center_include=True,
  135. word_flag=True, use_text=True,
  136. text=re.sub(")", ")", re.sub("(", "(", e.entity_text)))
  137. return self._span_wide
  138. # ---- roleRule.rule_predict 输入(spanWindow,前后各 40 词,use_text=False)----
  139. @property
  140. def span_rule(self):
  141. if self._span_rule is None:
  142. e = self.entity
  143. self._span_rule = spanWindow(
  144. tokens=self.sentence.tokens,
  145. begin_index=e.begin_index, end_index=e.end_index,
  146. size=40, center_include=True,
  147. word_flag=True, use_text=False)
  148. return self._span_rule
  149. def build_role_contexts(list_sentences, list_entitys):
  150. """一次预计算所有 (org/company/money) 实体的角色链路上下文。
  151. :param list_sentences: 与 ``list_entitys`` 同构嵌套的句子列表(按文章)
  152. :param list_entitys: 实体列表(按文章)
  153. :return: ``List[List[RoleContext]]``,与输入同构嵌套;
  154. 仅含能配对到句子的 org/company/money 实体,
  155. 内层按 sentence_index 升序(与原 prem 排序后遍历顺序一致)。
  156. 副作用(与原 ``prem.search_role_data`` 保持一致):
  157. 就地对各 ``list_entity`` / ``list_sentence`` 按 sentence_index 排序。
  158. """
  159. result = []
  160. for list_sentence, list_entity in zip(list_sentences, list_entitys):
  161. # 保持与原实现一致的就地排序副作用(下游 RoleRuleFinalAdd 等依赖此顺序)
  162. list_entity.sort(key=lambda x: x.sentence_index)
  163. list_sentence.sort(key=lambda x: x.sentence_index)
  164. # 句子索引:O(1) 定位,等价于原 while 扫描的首次命中
  165. index = {}
  166. for s in list_sentence:
  167. key = (s.doc_id, s.sentence_index)
  168. if key not in index:
  169. index[key] = s
  170. ctxs = []
  171. for entity in list_entity:
  172. if entity.entity_type not in _ROLE_CONTEXT_ENTITY_TYPES:
  173. continue
  174. sentence = index.get((entity.doc_id, entity.sentence_index))
  175. if sentence is None:
  176. continue
  177. ctxs.append(RoleContext(entity, sentence))
  178. result.append(ctxs)
  179. return result