BERT,零样本分类和 NER

正好工作中要用(可能还要训练哩!),所以就正好学习一下。但我不懂深度学习,所以就学的浅显一些,作为使用者去进行学习。

是什么

BERT 模型,是一种深度学习模型,它理解自然语言文本,并输出它的上下文。BERT 可以用在分类、标注(打标签)任务中。

我们知道,机器学习模型就像是一个函数,有固定的输入和输出,训练机器学习模型就是调整这个函数的实现,使得输出符合(逼近)我们的预期,所以我们也可以从输入、输出的角度去了解 BERT。

BERT 的输入是一段自然语言文本,它会被分解成 token 序列并 padding 到固定长度。BERT 会输出一个固定大小的二维矩阵,即对每个 token 返回一个固定长度的向量,表示模型对这个 token 的理解,这个向量称为嵌入向量,这个“理解”是包含上下文信息的,比如它能根据前后文能判断一个 tank 究竟是水柜还是坦克。

BERT 模型支持两个特殊的标记,[CLS][SEP][CLS]表示输入的起始,BERT 会特殊处理[CLS][CLS]的响应会包含模型对整个句子的理解[SEP]则是分割不同句子。

我们说嵌入向量,指的是 token 的嵌入向量,如果要得到句子级别的嵌入向量,我们可以使用[CLS]的嵌入向量,或者对句子中的所有 token 的嵌入向量做池化,得到一个“平均”的嵌入向量来代表整个句子。[CLS]通常用在分类任务,而池化通常用在计算句子相似度等任务

可见,嵌入向量是局限于使用的模型的,不同的模型输出的嵌入向量是不同的。

前面说到 BERT 的输出是嵌入向量的矩阵,这个输出是无法直接使用的,因为它只是句子的一个通用的向量表示,不包含任何具体的信息供我们使用。但也存在一些场景不需要微调即可使用,如计算文本相似度,聚类分析,语义检索等,这些任务都不关心文字具体表达什么,只关心找到相似文字。

要利用上这个向量表示,我们就需要使用我们已经知晓结果的句子去问它,确认这些句子对应的向量表示,然后去找到向量表示和我们所需要的结果之间的映射。这个过程就是所谓的微调,它的一种实现是为 BERT 的输出增加一个全连接层,它的输入是 BERT 的输出,而输出是我们要的结果,我们使用已经处理过的训练集去训练这个全连接层的权重(其实不然,微调也能修改 BERT 模型的权重),让它能够输出符合我们需要的结果。

零样本分类 Zero Shot Classification

基于 NLI(自然语言推理)训练集训练的 BERT 模型能够进行零样本分类——不用进行训练,只需要给定你要的标签,它就能够进行分类。

零样本分类的优点在于无需训练,方便搭建原型;而缺点在于准确率可能不够高,而且对每个标签都需要执行一次推理,标签多时性能会较差

首先直接尝试使用 zero-shot 分类,效果其实不错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from transformers import pipeline
classifier = pipeline("zero-shot-classification", model="MoritzLaurer/mDeBERTa-v3-base-mnli-xnli")

TASKS = {
"金融": [
"2023 年 3 月,美国两家银行因流动性不足和资不抵债被监管机构关闭,凸显了在加息环境下银行业的脆弱性。",
"2024 年,地方金融监管体制改革基本完成,自上而下的金融监管体系更加健全,提高了风险防控能力。",
"2023 年 9 月,美联储连续第四次加息 25 个基点,市场对长期高利率环境的适应能力受到考验。",
"2024 年 1 月,比特币价格突破 5 万美元,受机构投资者增持和 ETF 批准的推动,市场情绪高涨。",
"2023 年 11 月,某大型对冲基金因杠杆过高在市场剧烈波动中遭受巨大损失,引发连锁反应。"
],
"科技": [
"2023 年 1 月,谷歌母公司 Alphabet 宣布将裁员约 1.2 万人,占员工总数的 6%,以应对经济放缓带来的挑战。",
"2024 年 2 月,全球首台量子计算机突破 1000 个量子比特,实现了在某些特定任务上的超越经典计算机的能力。",
"2023 年 6 月,苹果公司发布首款混合现实头显,标志着 MR 技术进入消费市场。",
"2023 年 10 月,全球首款完全自动驾驶出租车在旧金山投入运营,引发公众对自动驾驶安全性的讨论。",
"2024 年 1 月,某知名芯片公司发布全新 AI 加速芯片,大幅提升深度学习模型的计算效率。"
],
"医疗": [
"2023 年 5 月,全球首例 CRISPR 基因编辑疗法获批用于治疗罕见遗传病,为基因治疗的广泛应用奠定基础。",
"2024 年 3 月,一家生物科技公司成功研制出针对阿尔茨海默病的新型药物,在临床试验中展现出显著疗效。",
"2023 年 8 月,某国政府宣布大幅调整医保政策,扩大对罕见病药物的覆盖范围。",
"2023 年 11 月,AI 辅助诊断技术在全球主要医院推广,提高了癌症早期筛查的精准度。",
"2024 年 2 月,科学家利用干细胞技术成功再生人类肝脏组织,为器官移植提供了新的可能。"
]
}

for label, descs in TASKS.items():
for desc in descs:
result = classifier(desc, candidate_labels=["金融", "科技", "医疗", "建筑", "法务", "政治"])
if result["labels"][0] == label:
print('True')
else:
print(f"expect {label} got {result["labels"][0]}: {desc}")

NLI 的数据集会是类似的格式:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"前提": "Fun for adults and children.",
"假设": "Fun for only children.",
"label": 2 // 0-蕴含,前提推出假设;1-中立,前提和假设无关;2-前提反驳假设
},
{
"前提": "Postal Service were to reduce delivery frequency.",
"假设": "The postal service could deliver less frequently.",
"label": 0
}
]

NLI 会把标签转换成假设的格式喂给模型以得到结果,因此无需训练。要二次训练的话,就需要准备这样的训练集,比打标签还复杂一些。

命名实体识别任务 NER

这里必须提到 BERT 使用的分词算法,它使用的是 WordPiece,它保证多个单词不会被合成一个 Token,一个词可以对应多个 Token;对于中文,一个字是一个 Token。这个分词算法表示,对于英文,我们可以按词进行打标签,对于中文,我们可以按字或词打标签,只需要按词打标签时把这个标签打给所有的字。WordPiece 的这种性质使得我按词打标签时,我的“词”总是一个或多个分词器的“词”,避免理解不同造成的冲突,比如“杨万里小区”,我给“杨万里”打标签“人名”,不用担心分词器分出“杨”和“万里小区”,这种情况会导致无法正确打标签,

上回说到,BERT 为每个输入的 Token 均输出一个向量,表示该 Token 的嵌入向量;但在分类问题中,我们没有关心没个 Token 自己的嵌入向量,而是关心这整个句子的嵌入向量。这是分类问题的特性。

现在,考虑另一个问题,有一段话,我要从中得到这段话涉及到的地点,如,“12 日夜,江苏省崇明岛万里街道银河小区 12 栋 5 单元楼下发生吵架”,我希望能够得到“江苏省崇明岛万里街道银河小区 12 栋 5 单元楼下”这段话,这要求我们考虑每个 Token 自己的嵌入向量,即语义了。

需要如何微调 BERT 模型使得符合我们的需要呢?这里和分类类似——增加一个全连接层,输出每个 Token 的标签而非整个句子的标签。

这里的标签有大文章——使用所谓的 BIO 标注法——Begin,Inside,Outside。Begin 表示某类实体的开始,Inside 表示某类实体的中途,Outside 表示不属于任何实体,比如,“江苏省的王先生”,按字去标注的话,就得到“B-LOC I-LOC I-LOC O B-NAME I-NAME O-NAME”。

我们找到大量句子,然后使用此种方法进行标注,得到训练集。然后,给 BERT 增加一个层,它的输入是每个 token 的嵌入向量,而输出是每个 token 的标签,进行训练。

hugging-face 的 transformers 库里直接提供了 pipeline 去使用整个 NER 流程,目前看来表现不错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from transformers import AutoModelForTokenClassification,AutoTokenizer,pipeline
model = AutoModelForTokenClassification.from_pretrained('uer/roberta-base-finetuned-cluener2020-chinese')
tokenizer = AutoTokenizer.from_pretrained('uer/roberta-base-finetuned-cluener2020-chinese')
ner = pipeline('ner', model=model, tokenizer=tokenizer)
res = ner("2024 年 12 月 4 日下午 3 时左右,江苏省崇明岛市佛祖岭街道东民里小区 12 号楼 1 单元楼下发生了一起居民纠纷事件。据现场目击者描述,纠纷双方分别为该单元的居民张先生与李女士。纠纷起因于张先生认为李女士在楼下停靠的特斯拉阻挡通信,而李女士则坚称并未对他人造成实质性影响。")
[(i['entity'], i['word']) for i in res]
# 输出:
[('B-address', '江'),
('I-address', '苏'),
('I-address', '省'),
('I-address', '崇'),
('I-address', '明'),
('I-address', '岛'),
('I-address', '市'),
('I-address', '佛'),
('I-address', '祖'),
('I-address', '岭'),
('I-address', '街'),
('I-address', '道'),
('I-address', '东'),
('I-address', '民'),
('I-address', '里'),
('I-address', '小'),
('I-address', '区'),
('I-address', '12'),
('I-address', '号'),
('I-address', '楼'),
('I-address', '1'),
('I-address', '单'),
('I-address', '元'),
('I-address', '12'),
('I-address', '楼'),
('I-address', '7'),
('I-address', '号'),
('I-address', '房')]

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!