第 5 节 / 共 10 节

第5章 迁移学习与 FastText

🎯 掌握迁移学习思路、fastText 工具与预训练模型使用。
📖 笔记

5. 迁移学习

迁移学习

fasttext

基本概念

fasttext优势:

  • 十分简单的网络结构.
  • 使用fasttext模型训练词向量时使用层次softmax结构, 来提升超多类别下的模型性能.
  • 由于fasttext模型过于简单无法捕捉词序特征, 因此会进行n-gram特征提取以弥补模型缺陷提升精度.
安装命令
pip install fasttext -i https://mirrors.aliyun.com/pypi/simple/
或者
pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/

负采样的优势:

  • 提高训练速度, 选择了部分数据进行计算损失, 损失计算更加简单.
  • 改进效果, 增加部分负样本, 能够模拟真实场景下的噪声情况, 能够让模型的稳健性更强.

补充的知识:

训练集->模型开发

测试集->评估模型的效果

验证集->项目经理

运行fasttext代码报如下错:

image-20251105180904782

解决:

1- 卸载虚拟环境中的numpy
    pip uninstall -y numpy

2- 重新安装1.x版本的numpy
    pip install numpy==1.26.4

fasttext文本分类

# fasttext文本分类API的使用
import fasttext

# 1- 模型训练和预测
def demo01_base():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking_train.txt")

    # 2- 使用训练好的模型对数据进行预测
    result1 = model.predict("Which baking dish is best to bake a banana bread ?")
    print(result1)
    result2 = model.predict("Why not put knives in the dishwasher?")
    print(result2)

    # 3- 模型测试
    result = model.test(path="data/cooking_valid.txt")
    print(result)   # (3000, 0.15566666666666668, 0.06732016721925904) 样本条数 精确率 召回率

# 2- 数据基本处理:统一成大小写、标点符号前面加空格。。。
def demo02_preprocessing():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking.pre.train")

    # 2- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 3- 增加训练轮次
def demo03_epoch():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking.pre.train",epoch=20)

    # 2- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 4- 调整学习率
def demo04_lr():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking.pre.train",epoch=20,lr=1)

    # 2- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 5- 设置n-gram参数
def demo05_n_gram():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking.pre.train",epoch=20,lr=1,wordNgrams=2)

    # 2- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 6- 调整损失函数
def demo06_loss():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking.pre.train",epoch=20,lr=1,wordNgrams=2,loss="hs")

    # 2- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 7- 自动超参数调优
def demo07_auto():
    # 1- 模型训练
    """
        参数解释:
            autotuneDuration:查找最优超参数组合的时间,最终找到的参数组合不一定是最优的
            autotuneValidationFile:找到最优超参数组合,使用验证集数据对参数效果进行验证
    """
    model = fasttext.train_supervised(
        input="data/cooking.pre.train",
        autotuneValidationFile="data/cooking.pre.valid",
        autotuneDuration=60*2
    )

    # 2- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 8- 多标签多分类问题:将问题拆解为单标签多分类问题,loss损失函数需要设置为ova->one vs all
def demo08_ova():
    # 1- 模型训练
    # 注意:lr的学习率不要过大,如果过大会出现梯度消失/爆炸的情况
    model = fasttext.train_supervised(input="data/cooking.pre.train",epoch=20,lr=0.1,wordNgrams=2,loss="ova")

    # 2- 预测
    """
        k:表示预测结果中目标值最多展示多少个。如果值为-1,那么是尽可能将所有的目标值全部都展示
        threshold:阈值。如果预测的目标值的概率超过了threshold,那么才有可能显示出来

        result1中输出的概率值是经过sigmoid计算后的结果,计算是__label__baking标签的概率以及不是__label__baking标签的概率。
        多标签中每个标签的概率计算是相互独立的
    """
    result1 = model.predict("Which baking dish is best to bake a banana bread ?",k=3,threshold=0.5)
    print(result1)

    result2 = model.predict("Which baking dish is best to bake a banana bread ?",k=-1)
    print(result2)

    # 3- 模型测试
    result = model.test(path="data/cooking.pre.valid")
    print(result)

# 9- 保存模型和重新加载模型
def demo09_savemodel():
    # 1- 模型训练
    model = fasttext.train_supervised(input="data/cooking.pre.train",epoch=20,lr=1,wordNgrams=2,loss="hs")

    # 2- 保存训练好的模型
    model.save_model("model/cooking_model.pkl")

    # 3- 加载训练好的模型
    model2 = fasttext.load_model("model/cooking_model.pkl")
    result = model2.test(path="data/cooking.pre.valid")
    print(result)

if __name__ == '__main__':
    # 1- 模型训练和预测
    # demo01_base()   # (3000, 0.15566666666666668, 0.06732016721925904)

    # 2- 数据基本处理
    # demo02_preprocessing() # (3000, 0.172, 0.07438373936860314)

    # 3- 增加训练轮次
    # demo03_epoch()  # (3000, 0.48733333333333334, 0.21075392821104225)

    # 4- 调整学习率
    # demo04_lr() # (3000, 0.5976666666666667, 0.2584690788525299)

    # 5- 设置n-gram参数
    # demo05_n_gram() # (3000, 0.596, 0.2577483061842295)

    # 6- 调整损失函数
    # demo06_loss() # (3000, 0.5946666666666667, 0.2571716880495892)

    # 7- 自动超参数调优
    # demo07_auto() # (3000, 0.536, 0.23180049012541445)

    # 8- 多标签多分类问题
    # demo08_ova() # (3000, 0.532, 0.23007063572149344)

    # 9- 保存模型和重新加载模型
    demo09_savemodel()

训练词向量

参考NLP课程第一天,使用fasttext实现word2vec代码

迁移学习的概念

魔搭社区:https://www.modelscope.cn/models?page=1&tabKey=task

预训练模型

定义: 简单来说别人训练好的模型。一般预训练模型具备复杂的网络模型结构;一般是在大量的语料下训练完成的

预训练语言模型的类别

现在我们接触到的预训练语言模型,基本上都是基于transformer这个模型迭代而来的
因此划分模型类别的时候,以transformer架构来划分:
Encoder-Only: 只有编码器部分的模型,代表:BERT
Decoder-Only: 只要解码器部分的模型,代表:GPT
Encoder-Decoder: 本质就transformer架构,代表:T5

微调

定义:一般是对预训练语言模型,进行垂直领域数据的微调,可以将预训练模型的参数全部微调或者部分微调或者不微调,但是一般我们在做任务的时候,会在预训练模型后加入自定义网络,自定义网络模型的参数需要训练

迁移学习的两种方式

开箱即用: 当预训练模型的任务和我们要做的任务相似时,可以直接使用预训练模型来解决对应的任务
微调: 进行垂直领域数据的微调,一般在预训练网络模型后,加入自定义网络,自定义网络模型的参数需要训练,但是预训练模型的参数可以全部微调或者部分微调或者不微调。

NLP常用预训练模型

当下NLP流行的预训练模型

  • BERT
  • GPT
  • GPT-2
  • Transformer-XL
  • XLNet
  • XLM
  • RoBERTa
  • DistilBERT
  • ALBERT
  • T5
  • XLM-RoBERTa

BERT及其变体

  • bert-base-uncased: 编码器具有12个隐层, 输出768维张量, 12个自注意力头, 共110M参数量, 在小写的英文文本上进行训练而得到.
  • bert-large-uncased: 编码器具有24个隐层, 输出1024维张量, 16个自注意力头, 共340M参数量, 在小写的英文文本上进行训练而得到.
  • bert-base-cased: 编码器具有12个隐层, 输出768维张量, 12个自注意力头, 共110M参数量, 在不区分大小写的英文文本上进行训练而得到.
  • bert-large-cased: 编码器具有24个隐层, 输出1024维张量, 16个自注意力头, 共340M参数量, 在不区分大小写的英文文本上进行训练而得到.
  • bert-base-multilingual-uncased: 编码器具有12个隐层, 输出768维张量, 12个自注意力头, 共110M参数量, 在小写的102种语言文本上进行训练而得到.
  • bert-large-multilingual-uncased: 编码器具有24个隐层, 输出1024维张量, 16个自注意力头, 共340M参数量, 在小写的102种语言文本上进行训练而得到.
  • bert-base-chinese: 编码器具有12个隐层, 输出768维张量, 12个自注意力头, 共110M参数量, 在简体和繁体中文文本上进行训练而得到.

Transformers库使用

了解Transformers库

  • Huggingface总部位于纽约,是一家专注于自然语言处理、人工智能和分布式系统的创业公司。他们所提供的聊天机器人技术一直颇受欢迎,但更出名的是他们在NLP开源社区上的贡献。Huggingface一直致力于自然语言处理NLP技术的平民化(democratize),希望每个人都能用上最先进(SOTA, state-of-the-art)的NLP技术,而非困窘于训练资源的匮乏。同时Hugging Face专注于NLP技术,拥有大型的开源社区。尤其是在github上开源的自然语言处理,预训练模型库 Transformers,已被下载超过一百万次,github上超过24000个star。
  • Huggingface Transformers 是基于一个开源基于 transformer 模型结构提供的预训练语言库。它支持 Pytorch,Tensorflow2.0,并且支持两个框架的相互转换。Transformers 提供了NLP领域大量state-of-art的 预训练语言模型结构的模型和调用框架。
  • 框架支持了最新的各种NLP预训练语言模型,使用者可快速的进行模型调用,并且支持模型further pretraining 和 下游任务fine-tuning。举个例子Transformers 库提供了很多SOTA的预训练模型,比如BERT, GPT-2, RoBERTa, XLM, DistilBert, XLNet, CTRL。
  • 社区Transformer的访问地址为:https://huggingface.co/。

三层应用结构

  • 管道(Pipline)方式:高度集成的极简使用方式,只需要几行代码即可实现一个NLP任务。
  • 自动模型(AutoMode)方式:可载入并使用BERTology系列模型。
  • 具体模型(SpecificModel)方式:在使用时,需要明确指定具体的模型,并按照每个BERTology系列模型中的特定参数进行调用,该方式相对复杂,但具有较高的灵活度。

5801

环境搭建

使用transformers需要执行如下安装命令
1- 进入到你的虚拟环境
    conda activate nlp_cuda
2- 安装命令
    pip install transformers
    pip install datasets
    pip install tf-keras

Pipeline方式应用预训练模型

文本分类

定义:对一个文本进行分类:eg:对一个评论文本,判断其实好评还是差评

代码实现

import os
os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"

import torch
from transformers import pipeline

def text_classification():
    # 1- 加载预训练模型
    """
        参数解释:
            task:业务的任务类型。这里是文本分类,可以传递text-classification或sentiment-analysis,推荐使用text-classification
                注意:其他任务的task值去pipeline源代码中找
            model:预训练模型的路径。推荐前面加上r,避免转义
    """
    model = pipeline(task="text-classification",model=r"D:\soft\PretrainedModel\chinese_sentiment")   # 5分类问题
    # model = pipeline(task="text-classification",model=r"D:\soft\PretrainedModel\bert-base-chinese")     # 二分类。好评是1,差评是0

    # 2- 预测
    # pred_result = model("我爱北京天安门,天安门上太阳升。")
    pred_result = model("这家餐馆的卫生太差了,吃了拉稀,非常不推荐")
    # pred_result = model("这家餐馆的卫生还行,就是有点油")

    print(type(pred_result))
    print(pred_result)  # [{'label': 'star 5', 'score': 0.6314295530319214}]

特征抽取

定义:将文本输入模型,得到特征向量的表示

代码实现

def text_feature_extraction():
    # 1- 加载预训练模型
    model = pipeline(task="feature-extraction",model=r"D:\soft\PretrainedModel\bert-base-chinese")

    # 2- 文本特征提取:先分词->以列表形式返回词向量
    # 返回的形状是 [1, 17, 768]。1是句子条数,17是因为对每个字进行分词加上句子的开始和结束;768词向量的维度数
    result = model("这家餐馆的卫生还行,就是有点油")

    print(type(result))
    print(result)

    print(torch.tensor(result).shape)

完形填空

掩码任务,将一段文本中的某个token进行MASK,然后通过模型来预测被MASK掉的词

代码实现

def fill_blank():
    # 1- 加载预训练模型
    # model = pipeline(task="fill-mask",model=r"D:\soft\PretrainedModel\bert-base-chinese")
    model = pipeline(task="fill-mask",model=r"D:\soft\PretrainedModel\chinese-bert-wwm")

    # 2- 填空
    """
        注意:要进行填充的地方,必须写 [MASK]
    """
    content = "我想明天去[MASK]家吃饭。"
    result = model(content)
    print(type(result))
    print(result)

阅读理解

question-answer,根据文本以及问题,从文本里面解答问题的答案

代码实现

def q_and_a():
    # 1- 加载模型
    model = pipeline(task="question-answering",model=r"D:\soft\PretrainedModel\chinese_pretrain_mrc_roberta_wwm_ext_large")

    # 因为bert-base-chinese模型训练的语料库全都是中文的,因此对英文内容处理不好
    # model = pipeline(task="question-answering",model=r"D:\soft\PretrainedModel\bert-base-chinese")

    # 2- 提问
    # context = '我叫张三,我是一个程序员,我的喜好是打篮球。'
    # context = '张三是我的名字,程序员是我的职业,打篮球是我的喜好。'
    context = 'my name is zhangsan,my job is programmer,i like play basketball'
    questions = ['我是谁?', '我是做什么的?', '我的爱好是什么?']
    answers = model(context=context, question=questions)
    print(type(answers))
    print(answers)

文本摘要

对文档的概括总结

代码实现

def summary():
    # 1- 加载模型
    model = pipeline(task="summarization",model=r"D:\soft\PretrainedModel\distilbart-cnn-12-6")

    # 2- 提供语料库
    text = "BERT is a transformers model pretrained on a large corpus of English data " \
           "in a self-supervised fashion. This means it was pretrained on the raw texts " \
           "only, with no humans labelling them in any way (which is why it can use lots " \
           "of publicly available data) with an automatic process to generate inputs and " \
           "labels from those texts. More precisely, it was pretrained with two objectives:Masked " \
           "language modeling (MLM): taking a sentence, the model randomly masks 15% of the " \
           "words in the input then run the entire masked sentence through the model and has " \
           "to predict the masked words. This is different from traditional recurrent neural " \
           "networks (RNNs) that usually see the words one after the other, or from autoregressive " \
           "models like GPT which internally mask the future tokens. It allows the model to learn " \
           "a bidirectional representation of the sentence.Next sentence prediction (NSP): the models" \
           " concatenates two masked sentences as inputs during pretraining. Sometimes they correspond to " \
           "sentences that were next to each other in the original text, sometimes not. The model then " \
           "has to predict if the two sentences were following each other or not."

    summary_result = model(text)
    print(type(summary_result))
    print(summary_result)

命名实体识别

对一段文本进行序列标注,对每一个词汇都要进行分类,习惯用BIO或者BIOES的标识符

代码实现

def ner():
    # 1- 加载模型
    model = pipeline(task="ner", model=r"D:\soft\PretrainedModel\roberta-base-finetuned-cluener2020-chinese")

    # 2- 提取实体
    print(model('鲁迅原名周树人,代表作有朝花夕拾,在商务部上班,今天他去故宫游览'))
    """
        B表示命名实体的开始,I是命名实体的中间内容

        {'entity': 'B-name', 'score': 0.9945884, 'index': 1, 'word': '鲁', 'start': 0, 'end': 1}, 
        {'entity': 'I-name', 'score': 0.99043053, 'index': 2, 'word': '迅', 'start': 1, 'end': 2}, 

        {'entity': 'B-name', 'score': 0.9791542, 'index': 5, 'word': '周', 'start': 4, 'end': 5}, 
        {'entity': 'I-name', 'score': 0.97904646, 'index': 6, 'word': '树', 'start': 5, 'end': 6}, 
        {'entity': 'I-name', 'score': 0.9797911, 'index': 7, 'word': '人', 'start': 6, 'end': 7}, 

        {'entity': 'I-organization', 'score': 0.3546072, 'index': 20, 'word': '务', 'start': 19, 'end': 20}, 
        {'entity': 'I-organization', 'score': 0.32793036, 'index': 21, 'word': '部', 'start': 20, 'end': 21}, 

        {'entity': 'B-scene', 'score': 0.881768, 'index': 29, 'word': '故', 'start': 28, 'end': 29}, 
        {'entity': 'I-scene', 'score': 0.91957027, 'index': 30, 'word': '宫', 'start': 29, 'end': 30}
    """

AutoModel方式应用预训练模型

文本分类

定义:对一个文本进行分类:eg:对一个评论文本,判断其实好评还是差评

代码实现

import torch

# 自动加载模型的配置文件
from transformers import AutoConfig
# 自动加载对应的模型
from transformers import AutoModel
# 自动加载模型词汇表,分词器
from transformers import AutoTokenizer
# 文本分类
from transformers import AutoModelForSequenceClassification
# 完型填空
from transformers import AutoModelForMaskedLM
# 阅读理解
from transformers import AutoModelForQuestionAnswering
# 文本摘要
from transformers import AutoModelForSeq2SeqLM
# NER命名实体识别
from transformers import AutoModelForTokenClassification


# 1- 文本分类
def demo01_classification():
    # 1- 创建类的实例对象
    # 1.1- 创建分词器:从预训练模型中创建得到分词器
    model_path = r"D:\PretrainedModel\chinese_sentiment"
    my_tokenizer = AutoTokenizer.from_pretrained(model_path)
    # 1.2- 创建模型对象:根据具体的业务需求,选择对应的类
    my_model = AutoModelForSequenceClassification.from_pretrained(model_path)

    # 2- 准备数据
    message = "我爱黑马"

    # 3- 文本内容分词并且转成模型需要的数据类型,也就是张量
    """
        参数解释:
            text:要处理的文本内容
            return_tensors:处理后的结果数据以什么形式方法。最常用的是pt(pytorch tensor)
            padding:是否对不满足max_length长度的句子进行内容填充。max_length是参数值,表示填充到满足要求的句子长度。例如:句子有3个词,因此会填充两个0,直到满足max_length=5
            truncation:超过max_length长度的句子是否要进行截断
            max_length:句子的最大长度
    """
    data_tensor = my_tokenizer.encode(text=message, return_tensors="pt", padding="max_length", truncation=True,
                                      max_length=10)
    print(f"分词器处理后的结果:{data_tensor}, {type(data_tensor)}")

    data_tensor2 = my_tokenizer.encode(text=message, return_tensors="np", padding="max_length", truncation=True,max_length=10)
    print(f"分词器处理后的结果:{data_tensor2}, {type(data_tensor2)}")

    # 4- 将处理后的数据输入到模型中得到结果
    my_model.eval()
    result = my_model(data_tensor)
    print(f"运行结果:{result}, {type(result)}")

    # 获得预测分类概率值最高的分类ID
    print(result[0].argmax(dim=-1))

特征抽取

定义:将文本输入模型,得到特征向量的表示

代码实现

def text_feature_extraction():
    # 1- 创建模型实例对象
    model_path = r"D:\soft\PretrainedModel\bert-base-chinese"
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoModel.from_pretrained(model_path)

    # 2- 准备数据
    sens = ['你是谁', '人生该如何起头']

    # 3- 将数据处理成张量
    # encode_plus比encode会返回更加丰富的信息
    data_tensor = tokenizer.encode_plus(
        text=sens,
        return_tensors="pt",
        padding="max_length",
        truncation=True,
        max_length=30
    )
    """
    返回值解释:
        1- input_ids:句子对应的词索引
        2- token_type_ids:词索引来源于的句子索引。句子索引从0开始
        3- attention_mask:注意力掩码。0表示不看input_ids对应位置的词索引;1反之
        {
  'input_ids': tensor([[ 101,  872, 3221, 6443,  102,  782, 4495, 6421, 1963,  862, 6629, 1928,
          102,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0]]), 

'token_type_ids': tensor([[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0]]), 
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0]])}
    """
    # print(data_tensor)

    # 4- 调用模型
    model.eval()
    result = model(**data_tensor)   # 对字典进行解包操作

    print(result)
    print("最后一个隐藏状态信息", result.last_hidden_state.shape)
    print("池化层信息", result.pooler_output.shape)
扩展知识:
bert中的池化层和CNN中的池化,压根不是同一个东西
bert中的池化层,实际是一个线性层

为什么都叫“池化”?
“Pooling” 在机器学习中泛指将多个值聚合为一个值的操作

完形填空

掩码任务,将一段文本中的某个token进行MASK,然后通过模型来预测被MASK掉的词

代码实现

def fill_blank():
    # 1- 创建模型对象
    model_path = r"D:\soft\PretrainedModel\chinese-bert-wwm"
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoModelForMaskedLM.from_pretrained(model_path)

    # 2- 准备数据
    content = "我想明天去[MASK]家吃饭."

    # 3- 处理数据
    # 推荐:完型填空类的任务,不要设置padding、truncation此类的参数
    data_tensor = tokenizer.encode_plus(
        text=content,
        return_tensors="pt"
    )

    # 4- 调用
    model.eval()
    result = model(**data_tensor)

    print(f"result-->类型:{type(result)}")
    print(f"result-->内容:{result}")

    """
        结果形状是[1, 12, 21128],解释如下:
            1- 1:上面传递给大模型的有1条句子
            2- 12:上面传递给大模型的句子中,含句子开头、句子结尾、标点符号、[MASK]在内,总共有12个词
            3- 21128:chinese-bert-wwm大模型的词汇表中有这么多个词

        这里写[0][6]原因是,[MASK]所在的索引是6
    """
    print(f"result中logits-->形状:{result.logits.shape}")
    print(f"result中logits某个词-->形状:{result.logits[0][6].shape}")
    print(f"result中logits某个词-->结果数据:{result.logits[0][6]}")

    # 获得概率最高词的索引信息
    pred_word_index = torch.argmax(result.logits[0][6]).item()
    # 将概率最高词的索引转成能够识别的内容
    pred_word_content = tokenizer.convert_ids_to_tokens(pred_word_index)

    print(f"填充词的索引:{pred_word_index},对应的内容:{pred_word_content}") # 填充词的索引:1961,对应的内容:她

阅读理解

question-answer,根据文本以及问题,从文本里面解答问题的答案

代码实现

def q_and_a():
    # 1- 创建模型实例对象
    model_path = r"D:\soft\PretrainedModel\chinese_pretrain_mrc_roberta_wwm_ext_large"
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoModelForQuestionAnswering.from_pretrained(model_path)

    # 2- 准备数据
    # context的内容中,如果有空格,它会自动的过滤掉
    context = '我叫张三 我是一个程序员 我的喜好是打篮球'
    questions = ['我是谁?', '我是做什么的?', '我的爱好是什么?']
    # questions = ['我是谁?', '我是做什么的?', '我的爱好是什么?','我爸姓啥?']

    # 3- 对问题列表进行循环。每次让大模型回答一个问题。这是与pipeline的主要区别
    for question in questions:
        # 3.1- 数据处理
        data_tensor = tokenizer.encode_plus(question,context,return_tensors="pt")
        print(data_tensor)

        # 3.2- 调用模型
        model.eval()
        result = model(**data_tensor)
        # print(result)

        # 3.3- 处理结果
        # 获得答案中预测概率最高start开始索引
        start_index = torch.argmax(result.start_logits).item()
        # 获得答案中预测概率最高end结束索引
        end_index = torch.argmax(result.end_logits).item() + 1
        # 通过start和end,对context(因为问题的答案肯定存在于上下文中)上下文进行切片,得到答案
        answer = tokenizer.convert_ids_to_tokens(data_tensor.input_ids[0][start_index:end_index])

        print(f"问题是:{question},对应的答案:{answer}")

文本摘要

对文档的概括总结

代码实现

def summary():
    # 1- 创建模型对象
    model_path = r"D:\soft\PretrainedModel\distilbart-cnn-12-6"
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoModelForSeq2SeqLM.from_pretrained(model_path)

    # 2- 准备数据
    text = "BERT is a transformers model pretrained on a large corpus of English data " \
           "in a self-supervised fashion. This means it was pretrained on the raw texts " \
           "only, with no humans labelling them in any way (which is why it can use lots " \
           "of publicly available data) with an automatic process to generate inputs and " \
           "labels from those texts. More precisely, it was pretrained with two objectives:Masked " \
           "language modeling (MLM): taking a sentence, the model randomly masks 15% of the " \
           "words in the input then run the entire masked sentence through the model and has " \
           "to predict the masked words. This is different from traditional recurrent neural " \
           "networks (RNNs) that usually see the words one after the other, or from autoregressive " \
           "models like GPT which internally mask the future tokens. It allows the model to learn " \
           "a bidirectional representation of the sentence.Next sentence prediction (NSP): the models" \
           " concatenates two masked sentences as inputs during pretraining. Sometimes they correspond to " \
           "sentences that were next to each other in the original text, sometimes not. The model then " \
           "has to predict if the two sentences were following each other or not."

    # 3- 处理数据
    data_tensor = tokenizer.encode_plus(text,return_tensors="pt")

    # 4- 得到文本摘要:生成文本内容
    model.eval()
    result = model.generate(**data_tensor)
    print(result)   # 就是一个普通张量

    # 5- 结果解析
    # 5.1- 使用decode进行解码
    decode_result_1 = [tokenizer.decode(word_index,skip_special_tokens=True,clean_up_tokenization_spaces=False) for word_index in result[0]]
    print(f"decode处理后的结果:{' '.join(decode_result_1)}")

    decode_result_2 = [tokenizer.decode(word_index,skip_special_tokens=False,clean_up_tokenization_spaces=False) for word_index in result[0]]
    print(f"decode处理后的结果:{' '.join(decode_result_2)}")

    # 5.2- 直接使用convert_ids_to_tokens
    print("convert_ids_to_tokens处理后的结果:"," ".join(tokenizer.convert_ids_to_tokens(result[0],skip_special_tokens=True)))

命名实体识别

对一段文本进行序列标注,对每一个词汇都要进行分类,习惯用BIO或者BIOES的标识符

代码实现

def ner():
    # 1- 创建模型实例对象
    model_path = r"D:\soft\PretrainedModel\roberta-base-finetuned-cluener2020-chinese"
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoModelForTokenClassification.from_pretrained(model_path)
    config = AutoConfig.from_pretrained(model_path)

    # 2- 准备数据
    content = "鲁迅原名周树人,代表作有《朝花夕拾》,在外交部上班,今天他去故宫游览"

    # 3- 数据处理
    data_tensor = tokenizer.encode_plus(text=content,return_tensors="pt")

    # 4- 调用
    model.eval()
    result = model(**data_tensor)
    print(result)
    """
        [1, 34, 32]形状解释
            1- 1:上面有1条句子
            2- 34:上面句子中,含开头、结尾、标点符号在内有34个词
            3- 32:该模型支持的命名实体的种类有32。每种大模型支持的命名实体的种类不同
    """
    # print(result.logits.shape)  # 形状[1, 34, 32]

    # 5- 结果解析
    words = tokenizer.convert_ids_to_tokens(data_tensor.input_ids[0])

    for word,prob_list in zip(words, result.logits[0]):
        # 过滤掉特殊符号:例如句子的开始、结束
        if word in tokenizer.all_special_tokens:
            continue

        # 1- 获得每个词对应的命名实体类别索引
        ner_index = torch.argmax(prob_list).item()
        # 2- 根据索引,获得命名实体的名称
        ner_type_name = config.id2label.get(ner_index)

        print(f"词:{word},命名实体类别索引:{ner_index},命名实体的名称:{ner_type_name}")

具体模型方式完成NLP任务

import torch
from transformers import BertTokenizer
from transformers import BertForMaskedLM

if __name__ == '__main__':
    # 1- 创建模型的实例对象
    model_path = r"D:\soft\PretrainedModel\bert-base-chinese"
    # 创建词汇映射器
    tokenizer = BertTokenizer.from_pretrained(model_path)
    model = BertForMaskedLM.from_pretrained(model_path)

    # 2- 准备数据
    content = "我想明天去[MASK]家吃饭"

    # 3- 处理数据
    data_tensor = tokenizer.encode_plus(text=content,return_tensors="pt")

    # 4- 调用模型
    my_model.eval()
    result = model(**data_tensor)

    # 5- 结果解析
    # 5.1- 获得预测概率词最高的词索引
    pred_word_index = torch.argmax(result.logits[0][6]).item()
    # 5.2- 词索引转成词
    pred_word = tokenizer.convert_ids_to_tokens(pred_word_index)
    # 5.3- 输出
    print(f"填充词的索引:{pred_word_index},对应内容:{pred_word}")

预训练模型总结【掌握】

预训练模型使用总结:
   1- pipeline管道:
       优点:代码开发非常简单
       缺点:底层高度封装,可调整的超参数少
       使用:一般用来快速验证预训练模型/大模型是否满足业务需求

   2- AutoModel自动模型:
       优点:代码相对比较简单,有一定可以可调整的超参数
       缺点:相对具体模型来说,可调整的超参数相对较少

   3- 指定模型:
       优点:可调整的超参数很多,能够针对具体的大模型进行指定参数的微调。每种大模型的可调整的参数都是不一样
       缺点:比较灵活,不同的大模型可调整的参数有区别
       使用:针对业务场景需要比较高的情况,推荐使用
       后续课程:LoRA、QLoRA

迁移学习实践(文本分类案例)

实现流程

1.获取数据集
2.数据预处理: 实例化dataset,dataloader,注意这里面在dataloder里用了自定义函数进行文本张量化处理
3.搭建模型: 注意,我们这里用bert预训练模型来得到文本的特征表示,然后在经过自定义网络实现分类
4.模型训练: 注意,不训练bert模型的参数,只更新自己定义的网络参数
5.模型测试: 注意,如果在GPU上训练的模型,想在CPU上使用, model.load_state_dict(torch.load(path, map_location="cpu")) #将模型放到cpu上

数据预处理

定义批处理函数

在Dataloader里面自动调用,目的是处理dataset里面的数据,进行张量化处理

代码实现

import os
os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"
import torch
import torch.nn as nn

# HuggingFace 提供的据集加载工具, 可以加载本地数据, 也可以加载公开数据源
from datasets import load_dataset
from torch.utils.data import DataLoader
from tqdm import tqdm
# 导入BERT相关组件(中文文本分词器, 预训练的BERT模型)
from transformers import BertTokenizer, BertModel
# 终端打印美化
from rich import print

# 使用GPU或者MPS
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")   # windows的写法
# device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")   # MAC的写法

# 1- 创建预训练模型对象
model_path = r"D:\PretrainedModel\bert-base-chinese"
# 只负责文本预处理,不涉及张量计算,所以不需要也不可以发送到 GPU
bert_tokenizer = BertTokenizer.from_pretrained(model_path)
bert_model = BertModel.from_pretrained(model_path).to(device)

# 2- 数据探索【可选】
def load_data():
    # 查看训练集数据
    """
        path:可以写文件所在目录,也可以写文件类型例如:csv
        data_files:可以写文件路径,也可以写文件名称。可以传递字典
        split:标明这份数据的作用是什么,训练集、测试集、验证集。想想机器学习中train_test_split的作用。
    """
    # 方式一:推荐写法。注意:data_files不要再跟前面path的路径了
    train_data = load_dataset(path="data",data_files="train.csv",split="train")
    # 方式二
    # train_data = load_dataset(path="csv",data_files="data/train.csv",split="train")
    print(type(train_data))
    print(train_data[0])
    print(len(train_data))

# 3- 针对每个批次的数据进行额外的处理
def collate_fn(data):
    # 1- 从每个批次中分别取出特征(也就是句子)和目标值(0/1)
    sents = [item["text"] for item in data] # 格式:['句子1','句子2'...]
    labels = [item["label"] for item in data] # 格式:[1,1,0,1,0...]
    # print(f"sents-->{sents}")
    # print(f"labels-->{labels}")

    # 2- 对句子进行批量处理
    data_tensor = bert_tokenizer.batch_encode_plus(
        sents,
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=300
    )

    # 3- 提取分词处理后的数据内容
    input_ids = data_tensor["input_ids"]
    token_type_ids = data_tensor["token_type_ids"]
    attention_mask = data_tensor["attention_mask"]

    # 4- 将目标值labels转成张量
    labels = torch.LongTensor(labels)

    return input_ids,token_type_ids,attention_mask,labels

实例化Dataloader

代码实现

# 4- 创建数据加载器
def get_dataloader(file_path):
    # 1- 加载指定路径下的文件
    dataset = load_dataset(path="data",data_files=file_path,split="train")

    # 2- 创建DataLoader实例对象
    """
        参数解释:
            dataset:数据集对象
            batch_size:每个批次中样本的条数。如果样本长度不一致,batch_size只能设置为1
            shuffle:是否对数据打散。让模型训练更加充分
            drop_last:如果最后一个批次的样本条数不足batch_size大小,那么直接不要
            collate_fn:针对每个批次数据的具体处理函数。注意:传递的是函数名称,不要带小括号
    """
    dataloader = DataLoader(
        dataset=dataset,
        batch_size=8,
        shuffle=True,
        drop_last=True,
        collate_fn=collate_fn
    )

    return dataloader

测试DataLoader数据加载器

if __name__ == '__main__':
    # 测试数据加载器
    dataloader = get_dataloader(file_path="train.csv")
    for input_ids,token_type_ids,attention_mask,labels in dataloader:
        print(input_ids)
        break

搭建模型

应用迁移学习的思路:bert预训练模型特征处理+自定义模型(分类)

代码实现

# 5- 搭建模型:Bert预训练模型+我们自定义的网络层
class AiModel(nn.Module):
    def __init__(self):
        # 1- 初始化父类
        super().__init__()

        # 2- 在Bert预训练模型的后面,增加自己的线性网络结构层
        """
            这里的参数必须是768和2,为什么?
            答:
                768:Bert的词向量/隐藏状态向量维度是768维
                2:目前案例是二分类场景
        """
        self.linear = nn.Linear(in_features=768,out_features=2)

    def forward(self,input_ids,token_type_ids,attention_mask):
        # 1- 先调用Bert预训练模型
        """
            注意如下两点:
                1- 推荐使用torch.no_grad(),冻结Bert的参数训练。可以不加,那么回对Bert的110M个参数都会进行训练,比较耗时
                2- bert_model()里面的参数要使用关键字传参
        """
        with torch.no_grad():
            bert_output = bert_model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)

        # 2- 再调用我们自定义的网络层
        """
            为什么写成bert_output.last_hidden_state[:, 0]?
            答:
                1- last_hidden_state表示取的是Bert模型最后一层隐藏层中每个时间步的隐藏状态。等价于多层RNN中output
                2- [:, 0]取到的是每条句子中第一个token(词),也就是[CLS]的隐藏状态
                3- Bert预训练模型的设计思想:只有编码器端,编码器端注意力是自注意力,也就是每个词都能够获得其他词的隐藏状态信息
                    [CLS]是一个固定的标识,每个句子的开头都有
                4- 结论:Bert做分类,固定写成bert_output.last_hidden_state[:, 0]

                以上的解释,建议看论文:https://arxiv.org/pdf/1810.04805
                对应内容:The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks. 
        """
        return self.linear(bert_output.last_hidden_state[:, 0])

模型训练

注意: 
1: 因为使用的预训练模型,所以在训练的时候,对自定义的模型加上,model.train()
2: 不更新bert预训练模型的参数requires_grad=False
3: 如果要想在GPU上训练:
    3.1将预训练模型的对象放到GPU上
    3.2自定义的模型对象放到GPU上
    3.3模型的输入放到GPU上,eg:input_ids = input_ids.to('cuda');model = model.to('cuda')

代码实现

def train():
    # 1- 加载数据
    dataloader = get_dataloader("train.csv")

    # 2- 禁用Bert预训练模型的梯度下降
    for param in bert_model.parameters():
        param.requires_grad_(False)

    # 3- 创建对象
    # 3.1- 创建模型对象
    model = AiModel().to(device=device)
    # 3.2- 优化器
    optimizer = torch.optim.AdamW(params=model.parameters(),lr=1e-3)
    # 3.3- 损失函数
    loss = nn.CrossEntropyLoss()

    # 4- 切换模式
    model.train()

    # 5- 训练
    epochs = 1
    for epoch in range(epochs):

        for i,(input_ids,token_type_ids,attention_mask,labels) in enumerate(tqdm(dataloader),start=1):
            # 5.1- 将数据全部发送到对应的设备
            input_ids = input_ids.to(device)
            token_type_ids = token_type_ids.to(device)
            attention_mask = attention_mask.to(device)
            labels = labels.to(device)

            # 5.2- 前向传播
            pred_result = model(input_ids,token_type_ids,attention_mask)

            # 5.3- 计算损失值
            loss_value = loss(pred_result,labels)

            # 5.4- 固定代码
            optimizer.zero_grad()   # 梯度清零
            loss_value.sum().backward() # 反向传播
            optimizer.step()        # 更新参数:不更新Bert的,只更新我们自己增加的线性层

            # 6- 间隔输出相关指标
            if i%20==0:
                # 6.1- 获得预测概率最高的索引
                pred_index = torch.argmax(pred_result,dim=-1)
                # 6.2- 统计预测正确的样本条数
                correct_count = (pred_index==labels).sum().item()
                # 6.3- 计算准确率
                acc = correct_count / len(labels)
                print(f"第{epoch+1}轮次,已训练的样本批次{i},平均准确率{round(acc,4)}")

    # 7- 保存训练好的模型
    torch.save(model.state_dict(),"model/bert.pkl")

模型预测

注意: model.eavl()和with torch.no_grad()
# 如果在GPU上训练的模型,想在CPU上使用, model.load_state_dict(torch.load(path, map_location="cpu")) #将模型放到cpu上

代码实现

def predict():
    # 1- 加载数据
    dataloader = get_dataloader("test.csv")

    # 2- 加载训练好的模型
    model = AiModel().to(device)
    model.load_state_dict(torch.load("model/bert.pkl"))

    # 3- 定义准确率统计变量
    correct_count = 0   # 预测正确的样本条数
    total_sample_count = 0  # 已经预测的总样本条数

    # 4- 预测
    model.eval()
    with torch.no_grad():

        for i,(input_ids,token_type_ids,attention_mask,labels) in enumerate(tqdm(dataloader),start=1):
            # 4.1- 将数据发送到对应设备
            input_ids = input_ids.to(device)
            token_type_ids = token_type_ids.to(device)
            attention_mask = attention_mask.to(device)
            labels = labels.to(device)

            # 4.2- 预测得到结果
            pred_result = model(input_ids,token_type_ids,attention_mask)

            # 4.3- 计算准确率:算的累计平均准确率
            pred_index = torch.argmax(pred_result,dim=-1)
            correct_count += (pred_index==labels).sum().item()
            total_sample_count += len(labels)

            # print("labels-->",labels)
            # print("pred_index-->",pred_index)
            # print("input_ids-->",input_ids)

            # 4.4- 间隔输出相关指标
            if i % 20 == 0:
                # 计算准确率
                acc = correct_count / total_sample_count
                print(f"已预测的样本批次{i},累计平均准确率{round(acc, 4)}")

                # 恢复得到原始句子内容
                text_content = "".join(tokenizer.convert_ids_to_tokens(input_ids[0], skip_special_tokens=True))
                print(f"每个批次中第一条样本的原始评价内容:{text_content},预测的类别是:{pred_index[0]}")
💻 代码

迁移学习代码(fastText / 预训练模型)

7 个文件
📖 笔记

补充:FastText(day10)

NLP-FastText

FastText

安装

# mac&Linux 
pip install fasttext

# windows
pip install fasttext-wheel

负采样

简而言之:只采样少数负样本。

为什么可以这么干?

#1.分类任务,概率之和为1
我们只需要保证正样本的概率足够大,所以,所有负样本加起来的概率肯定足够小。
比如:4000个类别(周杰伦歌词生成)
每次都是取argmax,最大值。我们只需要保证最大值足够大,剩下的3999个概率加起来就足够小。

因为你的值足够小,对结果几乎不产生影响,所以,FastText就做了一个优化:
我不计算所有3999个类别的概率(负类别),而是从中随机抽取几个(5个)作为代表(负样本),计算一下。
可以省略剩下的3994个概率的计算,所以可以提速。

核心API

训练词向量:fasttext.train_unsupervised()
训练模型:fasttext.train_supervised()

模型调优策略

#1.数据层面
数据决定上限。(garbage in,garbage out)

#2.模型层面
机器学习模型,RNN,LSTM,GRU,预训练模型,大模型

#3.参数层面
学习率、epoch、hidden_size等
📎 资料

FastText 语料