正好工作中要用(可能还要训练哩!),所以就正好学习一下。但我不懂深度学习,所以就学的浅显一些,作为使用者去进行学习。
是什么 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 的数据集会是类似的格式:
[ { "前提" : "Fun for adults and children." , "假设" : "Fun for only children." , "label" : 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' , '房' )]