4. 金融文本预处理(下):词性标注与命名实体识别(NER)
好,我们接着往下聊。上一节我们把文本切成了词,但光分词还不够。你想想看,光知道「茅台」「买入」「评级」这几个词,机器根本不知道谁在买谁。这时候,就需要词性标注和NER上场了。
4.1 词性标注:给每个词贴上标签
词性标注,说白了就是给每个词标上它是名词、动词还是形容词。我刚开始做金融NLP时,觉得这步可有可无。直到有一次,我把「中国平安买入」里的「平安」当成了形容词……嗯,那次回测结果惨不忍睹。
核心思路:词性标注能帮我们区分「名词性实体」和「动作性词汇」。比如「买入」是动词,「评级」是名词,这直接影响后续的意图识别。
在Python里,我用得最多的是jieba的词性标注功能。它内置了北大词性标注集的标签体系。举个例子:
import jieba.posseg as pseg
text = "贵州茅台今日获券商买入评级,目标价上调至2000元"
words = pseg.cut(text)
for word, flag in words:
print(f"{word} -> {flag}")
# 输出:
# 贵州茅台 -> ns(地名)
# 今日 -> t(时间词)
# 获 -> v(动词)
# 券商 -> n(名词)
# 买入 -> v(动词)
# 评级 -> n(名词)
# 目标价 -> n(名词)
# 上调 -> v(动词)
# 至 -> p(介词)
# 2000元 -> m(数词)
看到没?「贵州茅台」被标成了地名(ns),这在金融场景下其实不太对。茅台是公司名,不是地名。这就是通用词性标注的局限——它不懂金融。
我的经验:在金融文本里,我一般会自定义一个金融词性映射表。比如把「茅台」「腾讯」这类词强制标为「公司名(nc)」,而不是默认的「地名(ns)」。
4.2 命名实体识别(NER):找到关键信息
词性标注只是第一步。真正干活的是NER——它要找出文本里的「实体」。什么是实体?人名、地名、公司名、股票代码、金额、日期……这些都是。
我做过一个项目,要从公告里自动提取「增持主体」「增持金额」「增持时间」。如果没有NER,你只能靠正则硬匹配,那代码写出来能绕地球三圈。
4.2.1 金融NER的常见实体类型
| 实体类型 | 示例 | 说明 |
|---|---|---|
| 公司名 | 贵州茅台、腾讯控股 | 上市公司全称或简称 |
| 股票代码 | 600519、00700 | A股6位,港股5位 |
| 金额 | 2000元、1.5亿 | 带单位的数值 |
| 日期 | 2024年1月、今日 | 绝对或相对日期 |
| 评级 | 买入、增持、中性 | 券商评级术语 |
| 目标价 | 2000元、500港元 | 带价格单位 |
我习惯用LAC(Lexical Analysis of Chinese)来做NER。它比jieba更轻量,而且支持自定义词典。看个例子:
from LAC import LAC
lac = LAC(mode='lac')
text = "腾讯控股(00700)今日获多家券商上调目标价至500港元"
result = lac.run(text)
# 输出:
# ['腾讯控股', '(', '00700', ')', '今日', '获', '多家', '券商', '上调', '目标价', '至', '500', '港元']
# ['ORG', 'w', 'STOCK_CODE', 'w', 'TIME', 'v', 'm', 'n', 'v', 'n', 'p', 'm', 'n']
注意看,「00700」被标成了STOCK_CODE。这就是金融NER的价值——它知道股票代码长什么样。
避坑指南:我曾经踩过一个坑——把「500港元」拆成了「500」和「港元」两个实体。后来我加了一条规则:金额和单位必须合并成一个实体。否则下游的金额计算会出大问题。
4.3 金融专有名词识别:股票代码与公司名
金融文本里最头疼的就是专有名词。股票代码有6位数字的,有5位数字的,还有带后缀的(比如.HK)。公司名更是五花八门——「中国平安」和「平安银行」就差两个字,但完全是两家公司。
我的做法是:词典匹配 + 规则兜底。
4.3.1 股票代码识别
A股代码是6位数字,港股是5位数字,美股是字母组合。我一般用正则来匹配:
import re
def extract_stock_codes(text):
# A股:6位数字,以6、0、3开头
a_share = re.findall(r'[603]\d{5}', text)
# 港股:5位数字
hk_share = re.findall(r'\b\d{5}\b', text)
# 美股:字母组合,如AAPL
us_share = re.findall(r'\b[A-Z]{1,4}\b', text)
return {
'A股': a_share,
'港股': hk_share,
'美股': us_share
}
text = "腾讯(00700)和茅台(600519)今日上涨,苹果(AAPL)也涨了"
print(extract_stock_codes(text))
# 输出:{'A股': ['600519'], '港股': ['00700'], '美股': ['AAPL']}
小技巧:我一般会建一个股票代码映射表,把代码和公司名对应起来。这样识别出代码后,直接查表就能知道是哪家公司。
4.3.2 公司名识别
公司名识别比代码难多了。同一个公司可能有七八种叫法——「贵州茅台」「茅台」「贵州茅台酒股份有限公司」「600519」……
我的方案是:构建一个公司名别名库。比如:
company_aliases = {
'贵州茅台': ['贵州茅台', '茅台', '贵州茅台酒', '600519'],
'腾讯控股': ['腾讯', '腾讯控股', '00700', 'Tencent'],
'中国平安': ['中国平安', '平安', '601318']
}
def recognize_company(text):
for company, aliases in company_aliases.items():
for alias in aliases:
if alias in text:
return company
return None
当然,这只是最基础的做法。生产环境里,我还会用向量相似度来做模糊匹配,处理「中国平安保险」和「中国平安」这种细微差别。
4.4 文本标准化与正则表达式实战
最后一步,是让文本变得「干净」。金融文本里充满了各种噪音——全角半角混用、多余空格、特殊符号……
我总结了一套标准化的流程:
- 全角转半角:把中文标点转成英文标点,方便后续处理
- 统一数字格式:把「1.5亿」转成「150000000」
- 去除多余空格:只保留单词间的空格
- 特殊符号处理:比如「%」统一成「百分之」
看一个实战例子:
import re
def normalize_financial_text(text):
# 全角转半角
text = text.replace(',', ',').replace('。', '.').replace('(', '(').replace(')', ')')
# 统一金额格式:1.5亿 -> 150000000
text = re.sub(r'(\d+\.?\d*)亿', lambda m: str(int(float(m.group(1)) * 100000000)), text)
text = re.sub(r'(\d+\.?\d*)万', lambda m: str(int(float(m.group(1)) * 10000)), text)
# 去除多余空格
text = re.sub(r'\s+', ' ', text).strip()
# 统一百分比
text = text.replace('%', '百分之')
return text
raw_text = "公司 营收 1.5亿 元,同比增长 20%"
clean_text = normalize_financial_text(raw_text)
print(clean_text)
# 输出:公司 营收 150000000 元,同比增长 百分之二十
核心原则:标准化的目的是让下游模型「少犯错」。你想想看,如果「1.5亿」和「150000000」同时出现,模型会以为这是两个不同的数字。统一格式后,模型才能正确理解。
4.5 本章知识体系
下面这张图,是我自己梳理的金融文本预处理流程。你可以把它当作一个检查清单:
嗯,这一章的内容就到这里。文本预处理是金融NLP的地基,地基打不好,后面建什么模型都是白搭。我个人建议,在实际项目中,先把这四步跑通,再考虑上什么BERT、GPT。很多时候,简单的规则就能解决80%的问题。