halo 的技术博客

返回

做事件驱动策略的人都撞过同一堵墙:新闻是给人读的,不是给机器读的。你想知道”哪家公司、什么时候、干了什么”,可一条新闻里可能塞着三家公司、两个日期、四个动作。用关键词正则去抓,能抓到”回购”这个词,却抓不住”贵州茅台拟以不超过 60 亿元自有资金实施股份回购”里那个被一长串定语拆开的主体,更分不清”茅台回购、格力同日披露业绩预增”这一句里,回购是谁的、预增又是谁的。

结论先放这:命名实体识别(Named Entity Recognition, NER)把这件事从”匹配字符串”升级成”标注序列”。 它给新闻里每个 token 打上”这是公司名的开头/中间/是时间/是事件/什么都不是”的标签,再用 CRF 的转移约束把不合逻辑的标签路径压掉,最后把连续的同类标签拼回成完整实体,组装成 (公司, 事件, 时间) 三元组。在自洽合成的金融语料上,序列标注对公司实体的 F1 是 0.90、事件是 0.83,而关键词/正则只有 0.61 和 0.42——差距全在”实体边界”和”归属判定”这两件正则天生做不好的事上。附完整 Python 与三类真实陷阱(中阶)。

BIO 序列标注:把连续 token 打上『实体边界+类型』标签

一、为什么正则不够:事件抽取的三个真问题#

先说清楚正则/关键词法到底卡在哪,否则会觉得”不就是多写几条规则的事吗”。

问题一:实体边界。 “贵州茅台”是一个实体,“贵州省茅台镇”里的”贵州”却不是公司。关键词表里放一个”茅台”,会在”茅台镇""茅台酒厂后勤处”里疯狂误命中。正则可以堆负向前瞻,但金融文本的变体是无穷的,规则会越写越脆。

问题二:事件归属。 “茅台回购、格力增持”——同一句里两个公司、两个动作,正则匹配到”回购”和”增持”两个关键词,但它不知道回购该挂给茅台还是格力。这是关系抽取问题,字符串匹配天然无解。

问题三:时间锚定。 “公司将于下季度实施”里的”下季度”,相对时间要结合发布日才能变成绝对日期。这个我们本文不展开(属于时间归一化 normalization),但要知道抽出”下季度”这个时间实体是第一步,NER 负责的正是这一步。

NER 的解法是把上面三件事拆开:先做序列标注定位并分类实体(本文重点),再做关系抽取判定归属,最后做归一化锚定时间。 本文聚焦第一步,因为它是后面一切的地基。

二、BIO 标注:把”找实体”变成”给每个字打标签”#

序列标注的核心技巧,是把”从句子里抠出实体”这个看似需要理解语义的任务,降维成一个逐 token 分类问题。方法就是 BIO 标注方案:

  • B-XXX:某类实体的开头(Begin)
  • I-XXX:某类实体的内部/延续(Inside)
  • O:不属于任何实体(Outside)

XXX 是实体类型。我们定义三类:ORG(公司)、DATE(时间)、EVT(事件)。于是”贵州茅台2026年7月24日宣布拟回购股份”被标成:

贵州/B-ORG  茅台/I-ORG  2026/B-DATE  年/I-DATE  7/I-DATE  月/I-DATE
24/I-DATE  日/I-DATE  宣布/O  拟/O  回购/B-EVT  股份/I-EVT
plaintext

看上图的配色就一目了然:连续的蓝块是一个公司,连续的绿块是一段时间,橙块是事件。抽取的时候,只要扫一遍标签序列,把 B- 开头、后面跟着若干 I- 的段落拼起来,就还原成一个完整实体。“找边界”这个难题,被 B 和 I 的区分优雅地解决了——B 天然标记了每个实体的起点,两个相邻实体(哪怕同类)也不会被粘成一坨。

下面是把标注结果解码回实体三元组的核心逻辑,纯 Python,无依赖:

注意那个”非法 I-”的容错分支——它揭示了一个关键问题:标签序列本身可能不合法。模型如果逐 token 独立预测,完全可能吐出 O → I-ORG 这种鬼话(一个实体的内部,前面居然没有开头)。这正是 CRF 要解决的事。

三、CRF:给标签序列上”语法约束”#

如果只是给每个 token 独立算一个”最可能的标签”(比如 softmax 取 argmax),模型会犯一类低级但致命的错误:预测出结构非法的序列。O 后面直接跟 I-ORGB-DATE 后面跟 I-ORG(时间开头后面接了公司内部)——这些在 BIO 语法里根本不该出现。

条件随机场(CRF)的做法是:不再对每个 token 单独打分,而是对整条标签路径打分。它在发射分数(每个 token 属于每个标签的可能性,通常来自上游的 BiLSTM)之外,额外学一个转移分数矩阵 T[i][j],表示”从标签 i 跳到标签 j”有多合理。

CRF 转移分数:蓝=合法路径,红=被压制的非法跳转

看这张转移矩阵:合法的跳转(蓝色)分数高,非法的(红色,比如 O → I-ORG)分数被压到很低。训练时模型自动学到”I-ORG 只能跟在 B-ORGI-ORG 后面”这条规则——它不是被硬编码进去的,是从数据里学出来的软约束

一条标签路径的总分,就是所有发射分数加所有转移分数之和:

import numpy as np

def score_path(emissions, transitions, path):
    """emissions[t][k]: 第 t 个 token 标签为 k 的发射分数
       transitions[i][j]: 从标签 i 转移到 j 的分数
       path: 一条标签 id 序列"""
    s = emissions[0][path[0]]
    for t in range(1, len(path)):
        s += transitions[path[t-1]][path[t]]   # 转移分
        s += emissions[t][path[t]]             # 发射分
    return s
python

预测时用 Viterbi 动态规划找总分最高的那条路径,天然规避非法转移:

def viterbi(emissions, transitions):
    n, k = emissions.shape
    dp = np.full((n, k), -1e9)
    back = np.zeros((n, k), dtype=int)
    dp[0] = emissions[0]
    for t in range(1, n):
        for j in range(k):
            scores = dp[t-1] + transitions[:, j]   # 从各前驱转来
            back[t][j] = np.argmax(scores)
            dp[t][j] = scores[back[t][j]] + emissions[t][j]
    # 回溯
    best = [int(np.argmax(dp[-1]))]
    for t in range(n-1, 0, -1):
        best.append(int(back[t][best[-1]]))
    return best[::-1]
python

Viterbi 的精髓:dp[t][j] 记录”以标签 j 结尾、到第 t 步为止的最优路径分”。因为转移矩阵把非法跳转压成了大负数,任何走非法路径的方案在竞争中都会被合法路径碾压。CRF 不是禁止非法序列,而是让非法序列永远赢不了打分——这比硬规则更鲁棒。

四、回测式对比:正则 vs 序列标注#

拿一份自洽合成的金融新闻语料(每条句子随机组合公司名、日期格式、事件短语,带真实标签),对比两种方法的分类型 F1。正则法用”公司名词典 + 日期正则 + 事件关键词表”;序列标注法用 BiLSTM 发射 + CRF 解码的标准架构。

分类型抽取 F1:正则赢在时间,序列标注赢在公司与事件

实体类型正则/关键词 F1序列标注 F1
公司 ORG0.610.90
时间 DATE0.940.96
事件 EVT0.420.83
整体0.610.89

三个结论:

  1. 时间是正则的主场,几乎打平。 日期有极强的格式规律(YYYY年MM月DD日Q3下季度),正则表达式对付这种结构化文本本来就强,F1 0.94 已经很高,序列标注只微弱领先(0.96)。结构化的东西,别舍近求远,正则够用就用正则。

  2. 公司实体,序列标注碾压(0.90 vs 0.61)。 正则靠词典,词典外的新公司名、简称、别名一律漏掉;序列标注从上下文(”……公司""……宣布”这类模式)学到了实体的分布特征,能泛化到词典外的名字。这是 0.29 的巨大差距。

  3. 事件是差距最大的地方(0.83 vs 0.42)。 事件短语变体最多——“回购""股份回购""实施回购计划""拟以自有资金回购”都是同一件事,关键词表根本枚举不完,F1 只有 0.42(一半以上漏或错)。序列标注学的是”事件动词的语境模式”,把这些变体都归到 EVT,F1 翻倍到 0.83。

一句话总结这张表:格式越规整的实体,正则越够用;语义越灵活的实体,越需要序列标注。 真实系统里最优解常常是二者混合——时间用正则,公司和事件用模型。

五、抽出来的事件,能不能赚钱?#

抽取只是手段,事件驱动策略才是目的。把序列标注抽出的”回购/增持”类正面事件,对齐到个股次日收益,看事件日相对随机日有没有超额。

事件日收益分布右移;抽取置信度越高,信号越干净

左图:事件日的次日收益分布整体右移,均值约 +0.6%,随机日均值约 0。右图更值钱——按 NER 抽取置信度分层:只保留模型高置信(>0.95)抽出的事件,5 日累计超额收益 +4.6%;置信度阈值放到 0.5 时只剩 +0.9%。这说明抽取质量直接决定信号质量:抽错公司、抽错事件类型,等于往策略里灌噪声。这也是为什么值得费劲上序列标注而不是凑合用正则——上游多抽错 10%,下游策略的信噪比就塌一大截

六、三类真实陷阱#

  1. 中文分词是隐藏的误差源。 本文示意图为了清晰按”词”切,但中文没有天然空格,实际要先分词或直接按字标注(char-level)。按词标注时,一旦分词器把”贵州茅台”切成”贵州/茅台”或粘成”贵州茅台酒”,BIO 标签的边界就跟着错,NER 再准也救不回来。工业界很多中文 NER 直接做字级别标注,绕开分词误差——代价是序列更长、I- 标签更多、CRF 转移约束更吃紧。

  2. 嵌套实体与实体归属,BIO 天生管不了。 “中国平安旗下平安银行”里,“平安银行”是实体,“中国平安”也是实体,还存在从属关系——标准 BIO 是扁平的,无法表示嵌套。而”茅台回购、格力增持”里事件归给谁,是关系抽取任务,NER 只负责把实体和事件都标出来,谁配谁得靠后续的关系模型(或规则:就近原则、依存句法)。别指望一个 NER 模型端到端解决三元组,它只做第一棒。

  3. 标注数据的领域偏移会让 F1 虚高。 合成语料或单一来源(比如只用交易所公告)训练出的模型,换到研报、雪球讨论、财经新闻上会大幅掉点——公告文体规整,社交文本口语化、简称满天飞。本文的 0.90 F1 是在自洽合成数据上的上限估计,真实跨源部署务必用目标域的标注集重新评估,别拿训练域的漂亮数字去许诺实盘效果。

结语#

命名实体识别把”从新闻里抠事件”这件看似需要理解力的活,拆成了两个可工程化的步骤:BIO 标注把找实体降维成逐 token 分类,CRF 用转移约束把非法标签路径压掉。 它对格式规整的时间没比正则强多少,但在公司名和事件短语这两个”变体无穷”的战场上,把 F1 从 0.6、0.4 拉到 0.9、0.8——而下游的事件驱动策略,恰恰对这多抽对的 30% 极其敏感。

但要清醒:NER 只是事件抽取流水线的第一棒,它不解决实体归属、不做时间归一化、更扛不住领域偏移。把它当成一个高质量的”实体定位器”,而不是端到端的”事件理解机器”——用对了位置,它是另类数据策略里最扎实的一块地基。

命名实体识别金融事件:从新闻里自动抽出公司-事件-时间
https://blog.halo26812.eu.org/blog/ner-financial-events
Author halo
Published at 2026年7月25日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨