| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- # -*- coding: utf-8 -*-
- """角色规则上下文预计算(角色分类流程优化 Phase A)。
- 背景
- ====
- 角色分类链路中 ``prem`` / ``roleRule`` / ``roleRuleFinal`` /
- ``tendereeRuleRecall`` 四个 Predictor 各自遍历
- ``list_entitys × list_sentences`` 做实体-句子配对,并重复构建同一实体的
- 上下文切片:
- - ``prem.search_role_data`` — 字符切片(前 23 / 后 25),while 线性扫描
- - ``prem.search_money_data`` — 字符切片(前 13 / 后 15),while 线性扫描
- - ``RoleRulePredictor`` — ``get_context``(前后各 20 字)
- - ``TendereeRuleRecall`` — ``spanWindow``(前后各 40 词)
- 本模块把「配对 + 上下文构建」收敛为一次预计算:
- - 句子索引 ``{(doc_id, sentence_index): sentence}``,O(1) 定位实体所属句
- - ``RoleContext`` 惰性缓存三类上下文切片,按需计算、只算一次
- - 概率阶梯常量(命名原代码中的魔法概率,Phase C 起统一引用)
- 行为兼容性
- ==========
- - ``build_role_contexts`` 保留原 ``search_role_data`` 的就地排序副作用
- (``list_entity.sort`` / ``list_sentence.sort``)。
- - 与原 while 线性扫描的差异:当某个实体的 (doc_id, sentence_index) 在句子
- 列表中不存在时,原实现会把扫描指针推到列表末尾导致其后所有实体配对失败;
- 本实现按索引独立配对,不受前序缺口影响(属缺陷修正,已记录在优化日志)。
- 依赖方向
- ========
- CORE(``predictors`` 内部),仅依赖 ``common``,不依赖 ``rules/``,
- 符合 ARCHITECTURE.md §4.3 依赖方向约束。
- 源码集成约束(Phase 7 §7.8)
- ============================
- 本模块不加载 ``dl/rules/`` 的 YAML;规则外置的 YAML 目前作为
- 「source of truth」用于校验与文档,predictors 消费 YAML 须待
- Phase C pipeline 层规则注入机制就绪后进行。
- """
- from __future__ import absolute_import
- import re
- from BiddingKG.dl.common.context_utils import spanWindow, get_context
- __all__ = [
- "RoleContext",
- "build_role_contexts",
- "ROLE_LABELS",
- "P_MODEL_THRESHOLD",
- "P_RULE_OVERRIDE",
- "P_FALLBACK",
- "P_TITLE",
- ]
- # ----------------------------------------------------------------------------
- # 角色标签与概率阶梯
- # ----------------------------------------------------------------------------
- #: 角色标签 → 含义(与 Entity.label 语义一致)
- ROLE_LABELS = {
- 0: "tenderee", # 招标人/采购人
- 1: "agency", # 代理机构
- 2: "winTenderer", # 中标人
- 3: "secondTenderer", # 第二候选人
- 4: "thirdTenderer", # 第三候选人
- 5: "other", # 其他/无角色
- }
- # ---- 概率阶梯(Phase B 命名魔法数)------------------------------------------
- # 原代码中 0.501/0.51/0.55 等数值表示「略高于上游某置信度」的隐式约定,
- # 此处显式命名。现有 prem.py / role.py 中的字面量保持不变,
- # Phase C RoleRuleEngine 统一改用这些常量。
- #: 模型置信阈值:模型预测 prob < 0.5 时置 label=5,交给规则召回
- P_MODEL_THRESHOLD = 0.5
- #: 规则覆盖模型:略高于模型置信(原代码 0.501)
- P_RULE_OVERRIDE = 0.501
- #: 兜底召回:略高于标题召回基线(原代码 0.51)
- P_FALLBACK = 0.51
- #: 标题召回基线(原代码 0.55)
- P_TITLE = 0.55
- #: 参与角色/金额链路上下文预计算的实体类型
- _ROLE_CONTEXT_ENTITY_TYPES = ("org", "company", "money")
- class RoleContext(object):
- """单个实体的预计算上下文。
- 惰性计算三类切片并缓存;同一实体在多个 Predictor 间复用同一实例,
- 每种切片整个链路只计算一次。
- 属性
- ----
- entity / sentence : 原实体与所属句子(一次配对,替代 O(E×S) 扫描)
- role_model_text : prem 角色模型输入 (前文23字, 实体, 后文25字)
- money_model_text : prem 金额模型输入 (前文13字, 实体, 后文15字)
- span : RoleRulePredictor 用 get_context(前后各 20 字,
- [0]=前文 [1]=实体 [2]=后文)
- span_wide : TendereeRuleRecall 用 spanWindow(前后各 40 词,
- word_flag=True,实体文本做全半角括号归一)
- span_rule : RoleRulePredictor.rule_predict 输入用 spanWindow
- (前后各 40 词,word_flag=True,use_text=False)
- """
- __slots__ = ("entity", "sentence", "_span", "_span_wide", "_span_rule")
- def __init__(self, entity, sentence):
- self.entity = entity
- self.sentence = sentence
- self._span = None
- self._span_wide = None
- self._span_rule = None
- # ---- 便捷透传 ----
- @property
- def entity_type(self):
- return self.entity.entity_type
- @property
- def sentence_text(self):
- return self.sentence.sentence_text
- # ---- prem 模型输入切片 ----
- def _char_slices(self, front_size, behind_size):
- text = self.sentence.sentence_text
- b = self.entity.wordOffset_begin
- e = self.entity.wordOffset_end
- return (text[max(0, b - front_size):b], text[b:e], text[e:e + behind_size])
- @property
- def role_model_text(self):
- """prem.search_role_data 的文本切片:(前文 23 字, 实体, 后文 25 字)。"""
- return self._char_slices(23, 25)
- @property
- def money_model_text(self):
- """prem.search_money_data 的文本切片:(前文 13 字, 实体, 后文 15 字)。"""
- return self._char_slices(13, 15)
- # ---- roleRule 上下文(get_context,前后各 20 字)----
- @property
- def span(self):
- if self._span is None:
- e = self.entity
- self._span = get_context(
- self.sentence.sentence_text,
- e.wordOffset_begin, e.wordOffset_end,
- size=20, center_include=True)
- return self._span
- # ---- tendereeRuleRecall 上下文(spanWindow,前后各 40 词)----
- @property
- def span_wide(self):
- if self._span_wide is None:
- e = self.entity
- self._span_wide = spanWindow(
- tokens=self.sentence.tokens,
- begin_index=e.begin_index, end_index=e.end_index,
- size=40, center_include=True,
- word_flag=True, use_text=True,
- text=re.sub(")", ")", re.sub("(", "(", e.entity_text)))
- return self._span_wide
- # ---- roleRule.rule_predict 输入(spanWindow,前后各 40 词,use_text=False)----
- @property
- def span_rule(self):
- if self._span_rule is None:
- e = self.entity
- self._span_rule = spanWindow(
- tokens=self.sentence.tokens,
- begin_index=e.begin_index, end_index=e.end_index,
- size=40, center_include=True,
- word_flag=True, use_text=False)
- return self._span_rule
- def build_role_contexts(list_sentences, list_entitys):
- """一次预计算所有 (org/company/money) 实体的角色链路上下文。
- :param list_sentences: 与 ``list_entitys`` 同构嵌套的句子列表(按文章)
- :param list_entitys: 实体列表(按文章)
- :return: ``List[List[RoleContext]]``,与输入同构嵌套;
- 仅含能配对到句子的 org/company/money 实体,
- 内层按 sentence_index 升序(与原 prem 排序后遍历顺序一致)。
- 副作用(与原 ``prem.search_role_data`` 保持一致):
- 就地对各 ``list_entity`` / ``list_sentence`` 按 sentence_index 排序。
- """
- result = []
- for list_sentence, list_entity in zip(list_sentences, list_entitys):
- # 保持与原实现一致的就地排序副作用(下游 RoleRuleFinalAdd 等依赖此顺序)
- list_entity.sort(key=lambda x: x.sentence_index)
- list_sentence.sort(key=lambda x: x.sentence_index)
- # 句子索引:O(1) 定位,等价于原 while 扫描的首次命中
- index = {}
- for s in list_sentence:
- key = (s.doc_id, s.sentence_index)
- if key not in index:
- index[key] = s
- ctxs = []
- for entity in list_entity:
- if entity.entity_type not in _ROLE_CONTEXT_ENTITY_TYPES:
- continue
- sentence = index.get((entity.doc_id, entity.sentence_index))
- if sentence is None:
- continue
- ctxs.append(RoleContext(entity, sentence))
- result.append(ctxs)
- return result
|