第1节 · 第1章 NLP 入门

🎯 了解 NLP 发展、任务范畴与学习路线。
📖 笔记

1. 自然语言处理入门

第一章 自然语言处理入门

什么是自然语言处理

【什么是人工智能,分别对应哪几个领域】

AI是模仿甚至超越人的某项机能,NLP、CV、ASR
NLP是机器理解并生成人类语言

自然语言处理的发展简史

1950 -- 图灵提出“机器能思考吗”,划时代性的话题
1956 -- 达特茅斯会议,提出人工智能(Artificial Intelligence),AI的元年,之前都是人类智能(Human Intelligence)
1957-1970 -- NLP开始出现两大阵营:规则 + 统计
1994-1999 -- 统计学开始占据主导地位
2000-2012 -- 机器学习开始盛行
2012-2022 -- 传统的深度学习占据主导地位
2023 -- 大模型时代
  • 基于规则举例:设计语言学规则,解析句子结构,例如一个句子由主谓宾构成。
  • 基于统计举例:统计机器翻译模型,收集大量平行语料库,例如中英互译,统计不同短语的分配概率,计算给定源语言翻译为目标语言的可能性。

自然语言处理的应用场景

语音助手:小爱同学
机器翻译:谷歌翻译
搜素引擎:百度翻译
智能对话:文心一言
推荐系统:短视频app推荐
。。。

第2节 · 第2章 文本预处理

🎯 掌握分词、命名实体、词性标注、词向量与文本特征处理。
📖 笔记

2. 文本预处理

第二章 文本预处理

认识文本预处理

  • 文本预处理及作用
所处阶段:数据输入到模型之前
作用:数据清洗、指导超参数的确定
  • 文本预处理的主要环节
1.文本处理的基本方法:分词、NER、POS
2.文本张量的表示方法:one-hot、word2vec、wordEmbedding
3.文本语料的数据分析:标签数量分析(类别不均衡问题)、句子长度分析、词频统计和关键词词云
4.文本特征处理:添加n-gram特征、文本长度规范
5.数据增强方法:回译数据增强
Anaconda沙箱/虚拟环境介绍
作用:同台电脑能够同时拥有多个不同的Python环境;因为实际工作中,偶尔会出现你参与的/维护项目使用的Python解释器版本的要求是不同的。

每个项目创建单独的虚拟环境
优点:各个项目间隔离开了,相互之间不影响;用到啥库就安装上,开发的时候效率比较高
缺点:需要根据开发需求一个个安装对应的第三方库

Anaconda沙箱/虚拟环境相关命令
    conda env list:查看当前有哪些虚拟环境
    conda create -n 虚拟环境名称 python==版本号:创建新的虚拟环境,注意虚拟环境的名称不要用中文
    conda activate 虚拟环境名称:进入指定的虚拟环境
    conda deactivate:退出当前的虚拟环境
    conda remove -n 虚拟环境名称 --all:彻底删除指定的虚拟环境


创建nlp_base虚拟环境的流程如下:
    1- 查看当前有没有nlp_base
        conda env list

    2- 如果没有,那么需要新建。按回车即可
        conda create -n nlp_base python==3.10

    3- 验证是否创建成功
        conda env list

    4- 进入nlp_base中查看有哪些工具包
        conda activate nlp_base
        conda list

    5- 安装jieba分词器
        pip install jieba -i https://mirrors.aliyun.com/pypi/simple/
        pip install torch -i https://mirrors.aliyun.com/pypi/simple/
        pip install tensorflow -i https://mirrors.aliyun.com/pypi/simple/
        pip install joblib -i https://mirrors.aliyun.com/pypi/simple/

        pip install fasttext -i https://mirrors.aliyun.com/pypi/simple/
        或者
        pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/

        如果不管是安装fasttext还是fasttext-wheel都失败,那么原因是python版本过高。
        操作步骤如下:
        1- 先进入对应的虚拟环境
        2- 安装低版本的python解释器
            conda install python=3.10
        3- 安装fasttext
            pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/



        pip install seaborn -i https://mirrors.aliyun.com/pypi/simple/
        pip install matplotlib==3.8.4 -i https://mirrors.aliyun.com/pypi/simple/

        如果运行matplotlib代码报错,依次执行如下两行命令:
        pip uninstall -y matplotlib
        pip install matplotlib==3.8.4 -i https://mirrors.aliyun.com/pypi/simple/

        pip install wordcloud -i https://mirrors.aliyun.com/pypi/simple/

image-20251023095136860

image-20251023100005272

image-20251022123200827

文本处理的基本方法

分词器【掌握】

  • 分词的意义
分词就是将连续的字序列,按照一定的规范,重新组合成词序列的过程
一般实现模型训练的时候,模型接受的文本基本最小单位是词语,因此我们需要对文本进行分词
词语是语意理解的基本单元
英文具有天然的空格分隔符,而中文分词的目的:寻找一个合适的分词边界,进行准确分词
  • jieba分词工具
import jieba

# 1- 精确模式
def demo01():
    content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"

    """
        sentence:要分词的内容
        cut_all:是否使用全词模式。默认是False
    """
    words = jieba.lcut(content,cut_all=False)
    print(type(words))
    print(words)

    print("-"*30)

    """
        lcut:返回结果类型是List列表,直接将所有的词全部返回
        cut:返回结果类型是生成器对象,不会直接将所有的词全部返回,需要真正用到的时候才一个个给你生成,非常节约内存
    """
    words2 = jieba.cut(content,cut_all=False)
    print(type(words2))
    print(words2)
    print("-" * 30)

# 2- 全(词)模式
def demo02():
    content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"

    """
        sentence:要分词的内容
        cut_all:是否使用全词模式。如果是True,那么就是全词模式,也就是分的更加精细
    """
    words = jieba.lcut(content,cut_all=True)
    print(words)

# 3- 搜索引擎模式
def demo03():
    content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"

    words = jieba.lcut_for_search(content)
    print(words)

if __name__ == '__main__':
    # 【掌握】1- 精确模式
    demo01()

    # 2- 全(词)模式
    demo02()

    # 3- 搜索引擎模式
    demo03()
  • 支持用户自定义词典和繁体字分词

词典的意义

可以根据自定义词典,修改jieba分词方式,优先考虑词典里面的词来切分
格式:词语 词频(可省略) 词性(可省略)

自定义词典内容

# 格式:词 词频(可省略) 词性(可省略)
传智教育 10 n
黑马程序员 8
我是 1000000
旗下有

代码实现

import jieba

# 繁体字分词:正常分词即可
def demo1():
    content = "煩惱即是菩提,我暫且不提"
    print(jieba.lcut(content))

    content = "煩恼即是菩提,我暫且不提"
    print(jieba.lcut(content))

# 自定义字典
def demo2():
    content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"

    # 默认不使用自定义指定的形式
    result = jieba.lcut(content)
    print(result)

    # 加载自定义字典
    # 字典中用来存放需要进行特殊关照的词。这些词会作为整体,不会被分词
    jieba.load_userdict("data/my_dict.txt")
    result = jieba.lcut(content)
    print(result)

if __name__ == '__main__':
    # 繁体字分词
    # demo1()

    # 用户自定义字典
    demo2()

命名实体识别

  • 定义
命名实体:通常指人名、地名、机构名等专有名词
NER:从一段文本中识别出上述描述的命名实体
常见的7类命名实体:人名、地名、机构名、时间、日期、货币、百分比
  • 作用
同词汇一样,命名实体也是人类理解文本的基础单元,也是AI解决NLP领域高阶任务的重要环节

词性标注(pos)

  • 定义
对每个词语进行词性的标注:动词、名词、形容词等
  • 实现方式
import jieba.posseg as pseg # POS词性标注

# POS词性
def demo1():
    content = "鲁迅爱北京天安门"
    result = pseg.lcut(content)

    for word,pos in result:
        print(word,"=",pos)

if __name__ == '__main__':
    # POS词性标注
    demo1()

文本张量的表示方法【熟悉】

文本张量的表示方法有几种

  • onehot

  • word2vec

  • CBOW:连续词袋模式。使用两边的词预测中间的内容
  • Skip-gram:跳词模式。使用中间的词预测两边的内容

  • word embedding

One-Hot 词向量表示

  • 介绍
针对每一个词汇,都会用一个向量表示,向量的长度是n(n就是词表大小),n代表去重之后的词汇总量,而且向量中只有0和1两种数字
也称之为:独热编码、01编码
  • 代码实现
import os
os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"

import joblib
# from tensorflow.keras.preprocessing.text import Tokenizer   # 词汇映射器
from keras.src.legacy.preprocessing.text import Tokenizer   # 上面的替代方案

"""
    获得词向量的one-hot方式总结:
        优点:实现、理解简单
        缺点:
            1- 词向量是一个稀疏向量,会浪费存储和计算资源
            2- 对多义词的处理不好。例如:过过过过过过,中的3对过的词向量是同一个
"""

def demo01():
    # 1- 准备语料库
    vocabs = {"周杰伦", "陈奕迅", "王力宏", "李宗盛", "吴亦凡", "鹿晗"}

    # 2- 训练词汇映射器
    # 2.1- 创建映射器对象
    my_tokenizer = Tokenizer()
    # 2.2- 训练模型
    my_tokenizer.fit_on_texts(vocabs)
    # 2.3- 得到训练好的结果数据:词和索引的映射关系
    # 注意:1- word_index后面不要加小括号;2- 返回结果类型是普通的Python字典
    word_dict = my_tokenizer.word_index
    # print(type(word_dict))
    # print(word_dict)

    # 3- 得到每个词的one-hot词向量
    vocabs_len = len(vocabs)
    for word in vocabs:
        # 3.1- 初始化一个全0的词向量列表
        word_one_hot = [0]*vocabs_len   # 得到是[0,0,0,0,0,0]
        # 3.2- 获得词在词汇映射器中的索引。注意:索引是从1开始的
        index = word_dict[word]-1
        # 3.3- 将指定位置的值改为1即可
        word_one_hot[index] = 1

        print(f"{word},对应的词向量是{word_one_hot}")

    # 4- 保存训练好的模型
    joblib.dump(my_tokenizer,"model/tokenizer.pkl")

def demo02():
    word = "王力宏"

    # 1- 加载训练好的模型
    my_tokenizer = joblib.load("model/tokenizer.pkl")

    # 2- 得到某个词的词向量
    # 2.1- 获得词汇映射器中词和索引的对应关系
    word_dict = my_tokenizer.word_index
    # 2.2- 初始化一个全0的列表
    word_one_hot = [0]*len(word_dict)
    # 2.3- 获得该词的索引
    index = word_dict[word]-1
    # 2.4- 将指定索引位置的值设置为1即可
    word_one_hot[index] = 1

    print(word_one_hot)

# 基于纯Python入门试学班的代码实现
def demo03():
    # 1- 准备语料库
    vocabs = ["周杰伦", "陈奕迅", "王力宏", "李宗盛", "吴亦凡", "鹿晗"]

    # 2- 得到每个词的one-hot词向量
    vocabs_len = len(vocabs)
    for word in vocabs:
        # 2.1- 初始化一个全0的词向量列表
        word_one_hot = [0] * vocabs_len  # 得到是[0,0,0,0,0,0]
        # 2.2- 获得词在语料库的索引
        index = vocabs.index(word)
        # 3.3- 将指定位置的值改为1即可
        word_one_hot[index] = 1

        print(f"{word},对应的词向量是{word_one_hot}")


if __name__ == '__main__':
    # 1- 训练模型:也就是训练词汇映射器
    demo01()

    # 2- 使用训练好的模型
    demo02()

    # 3- 用最基本的方式实现
    demo03()
  • One-hot总结
获得词向量的one-hot方式总结:
    优点:实现、理解简单
    缺点:
        1- 词向量是一个稀疏向量,会浪费存储和计算资源
        2- 对多义词的处理不好。例如:过过过过过过,中的3对过的词向量是同一个

Word2Vec模型

word2vec介绍
    1- 是将文本变成词向量张量的一种方法,是一种无监督(自监督)的训练方法,本质是训练一个模型,将模型的参数矩阵当作所有词汇的词向量表示。基于one-hot的形式进行优化改造
    2- 它又分为两种具体模式:
        CBOW:连续词袋模式。使用两边的词预测中间的内容
        Skip-gram:跳词模式。使用中间的词预测两边的内容

CBOW【了解】

properties 核心思想:给一段文本,选择一定的窗口,然后利用上下文预测中间目标词

  • 实现过程

image-20251228152151644

skip-gram【了解】

核心思想: 给出一段文本,选择一定窗口获取数据集,利用中间词来预测上下文
  • 实现过程

image-20251023165717752

Fasttext实现Word2Vec代码【掌握】

  • 基本过程
1.获取数据
2.训练模型(训练词向量)  
    正常安装:pip install fasttext -i https://mirrors.aliyun.com/pypi/simple/
    如果失败:pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/
3.向量的检验
4.超参数设定
5.模型的保存和加载
  • 代码实现
"""
    如果不管是安装fasttext还是fasttext-wheel都失败,那么原因是python版本过高。
    操作步骤如下:
        1- 先进入对应的虚拟环境
        2- 安装低版本的python解释器
            conda install python=3.10
        3- 安装fasttext
            pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/
"""
import fasttext

def demo01():
    # 1- 使用无监督学习训练模型
    """
        为什么这里只能使用无监督学习?
        答:因为数据中没有明确标记目标值。有监督学习对文件内容有严格要求,有__label__
    """
    # model = fasttext.train_unsupervised("data/fil9")
    model = fasttext.train_unsupervised("data/gz03ag")

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

def demo02():
    # 1- 加载训练好的模型
    model = fasttext.load_model("model/word2vec.pkl")

    # 2- 获得某个词的词向量
    word_vec = model.get_word_vector("hello")
    print(word_vec)

def demo03():
    # 1- 加载训练好的模型
    model = fasttext.load_model("model/word2vec.pkl")

    # 2- 获得相近的几个词
    # 会从词形、词义方面进行查找。
    # result_list = model.get_nearest_neighbors("dog")
    result_list = model.get_nearest_neighbors("cat")

    # 3- 输出
    print(result_list)

def demo04():

    """
        参数解释:
            input:训练集路径
            model:word2vec的模式。默认是skipgram,可以设置为cbow
            dim:词向量的维度
            epoch:训练的轮次
            lr:初始学习率
            thread:训练的线程个数
    """
    model = fasttext.train_unsupervised(
        input="data/gz03ag",
        model="cbow",
        dim=200,
        epoch=1,
        lr=0.1,
        thread=10
    )

    model.save_model("word2vec_better.pkl")

if __name__ == '__main__':
    # 1- 训练并保存模型
    # demo01()

    # 2- 获得词的词向量
    # demo02()

    # 3- 获得相近的词
    # demo03()

    # 4- 超参数调优设置【掌握】
    demo04()

image-20251023171926916

词嵌入Word Embedding【复习】

word2vec缺点:静态的、固定的vec表示方法,对多义词处理效果不佳
  • 实现过程
借助nn.Embedding(num_embeddings, embed_dim): 
    num_embeddings:代表词汇(去重之后)的总量
    embed_dim:是我们设定的词向量维度
  • 代码实现
import os
import torch

os.environ['TF_ENABLE_ONEDNN_OPTS']='0'
from tensorflow.keras.preprocessing.text import Tokenizer # 词汇映射表
import jieba
import torch.nn as nn
from torch.utils.tensorboard import SummaryWriter # 词向量可视化化

if __name__ == '__main__':
    # 1- 准备文本数据
    sentence1 = '传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能'
    sentence2 = "我爱自然语言处理"
    sentence_list = [sentence1,sentence2]

    # 2- 分词处理
    word_list = []
    for sen in sentence_list:
        word_list.append(jieba.lcut(sen))
    # print(word_list)

    # 3- 使用语料库对Tokenizer词汇映射器进行训练
    # 3.1- 创建Tokenizer实例对象
    my_token = Tokenizer()
    # 3.2- 模型训练
    my_token.fit_on_texts(word_list)
    # 3.3- 得到索引和词的映射关系,结果是一个字典。注意:索引从1开始
    # 【了解】1- 首先根据词在句子中的顺序来排序;2- 然后将出现频次高的词排在前面;3- 如果频次相同,那么按照首次出现的顺序排序
    index_word = my_token.index_word
    word_num = len(index_word)
    print(index_word)

    # 4- 创建词嵌入层
    """
        num_embeddings:去重后的词汇表中总共有多少个词
        embedding_dim:每个词的词向量维度(也就是向量中有多少个数字)
    """
    ebd = nn.Embedding(num_embeddings=word_num,embedding_dim=6)

    # 5- 得到词向量
    for i in range(word_num):
        # 5.1- 根据词索引得到词向量
        word_vector = ebd(torch.tensor(i))

        # 5.2- 打印输出
        print(f"索引是{i+1},对应的词是 {index_word.get(i+1)},词向量{word_vector}")

    # 6- 可视化展示:通过tensorboard(张量面板)观察词之间的相似性
    summary_obj = SummaryWriter(log_dir="./runs")
    summary_obj.add_embedding(ebd.weight.data,index_word.values())
    summary_obj.close()
  • 结果可视化

  • 命令:tensorboard --logdir=runs --host 127.0.0.1

  • 截图

image-20251228173849885

  • 访问:使用Edge浏览器来访问。输入链接 http://127.0.0.1:6006

image-20251022103633445

image-20251228175857323

文本数据分析【理解】

文本数据分析有哪些方法
    1、标签数量分布
    2、句子长度分布
    3、词频统计和词云

文本数据分析的作用
    1、帮助我们理解语料
    2、帮助我们检查语料, 快速分析语料中可能存在的问题.
        例如:
            数据质量类: 错别字, 语法错误, 重复内容, 缺失值...
            数据均衡类: 标签分布不均, 句子长度不同...

标签数量分布

目的: 查询样本类别是否均衡,如果样本过大,需要减少数据;如果样本过少,需要增加数据
  • 代码实现
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
"""
    如果出现AttributeError: 'FigureCanvasInterAgg' object has no attribute 'tostring_rgb'. Did you mean: 'tostring_argb'?
    那么是matplotlib的版本过高导致的
    pip uninstall -y matplotlib
    pip install matplotlib==3.8.4 -i https://mirrors.aliyun.com/pypi/simple/
"""

if __name__ == '__main__':
    # 1- 读取文件
    """
        注意:需要指定具体的分隔符sep,否则会报错pandas.errors.ParserError: Error tokenizing data. C error: Expected 6 fields in line 12, saw 8
    """
    train_df = pd.read_csv("data/train.tsv",encoding="UTF-8",sep="\t")
    dev_df = pd.read_csv("data/dev.tsv",encoding="UTF-8",sep="\t")

    # 2- 展示训练集
    plt.style.use("fivethirtyeight")
    sns.countplot(x="label",data=train_df)
    plt.title("train")
    plt.show()

    # 3- 展示验证集
    sns.countplot(x="label", data=dev_df)
    plt.title("dev")
    plt.show()

image-20251229092657668

句子长度分布

目的: 模型一般规定需要输入固定的尺寸,也就是长度统一,通过分析句子长度,可以明确大部分样本属于什么长度范围,然后进行句子的长短补齐或截断
  • 解释list( map(lambda x: len(x), sentence_series))
if __name__ == '__main__':
    my_data = [11,22,33]
    map_result = map(lambda num:num*10, my_data) # 这里实际是得到了一个生成器对象
    # print(map_result) # 得到的是内存地址值

    # 取值方式一【推荐】:转成List列表
    # list_result = list(map_result)
    # print(list_result)

    print("-"*30)

    # 取值方式二:for循环
    # for i in map_result:
    #     print(i)

    print("-" * 30)

    # 取值方式三:next一个个取值
    print(next(map_result))
    print(next(map_result))
    print(next(map_result))
  • 代码实现
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

def demo01():
    # 1- 读取数据
    df = pd.read_csv("data/train.tsv",sep="\t",encoding="UTF-8")

    # 2- 统计句子长度
    # 2.1- 获得句子列
    sentence_series = df["sentence"]
    # 2.2- 计算句子长度
    # 方式一:list(map)
    # df["length"] = list(map(lambda line:len(line),sentence_series))

    # 方式二:apply
    df["length"] = sentence_series.apply(lambda line:len(line))
    # print(df.head())

    # 3- 绘制图形
    # 3.1- 绘制分布直方图
    plt.style.use("fivethirtyeight")
    sns.countplot(x="length",data=df)
    plt.xticks([])
    plt.title("length_dist")
    plt.show()

    # 3.2- 绘制趋势曲线
    # kde:让曲线更加平滑
    sns.displot(x="length",data=df,kde=True)
    plt.show()


def demo02():
    # 1- 读取数据
    df = pd.read_csv("data/train.tsv", sep="\t", encoding="UTF-8")

    # 2- 统计句子长度
    # 2.1- 获得句子列
    sentence_series = df["sentence"]
    # 2.2- 计算句子长度
    df["length"] = sentence_series.apply(lambda line: len(line))

    # 3- 绘制图形
    sns.stripplot(x="label",y="length",data=df,hue="label")
    plt.title("label_length")
    plt.show()

if __name__ == '__main__':
    # 句子长度分布
    demo01()

    # 好评差评的句子长度分布
    demo02()

image-20251024111828548

词频和高频词云

词频: 这里指的是统计样本中词汇的总数量(需要去重)
高频词云目的: 可以方便我们查看数据集中是否存在脏数据,进而实现人工的审核和清洗
  • 解释chain(*map())
chain的作用可以简单理解为和List.extend等价,但是有区别:chain返回一个的是一个迭代器,因此对内存的使用极低,而且不修改原始列表
# 导包
from itertools import chain
import jieba

def dm01():
    list1 = ['今天天很好', '今天天很热']
    # 对每个字符串进行分词处理
    tmp_result = map(lambda x: jieba.lcut(x), list1)
    print(list(tmp_result))
    print(*tmp_result)
    # 把上述结果合并, 不会去重.
    result = list(chain(*tmp_result))
    print(f'result: {result}')

    # 上述格式最终版写法, 即: 一行.
    result = list(chain(*map(lambda x: jieba.lcut(x), list1)))
    print(f'result: {result}')

if __name__ == '__main__':
    dm01()
  • 词频_代码实现
# 导包
import jieba
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt  # 推荐安装 pip install matplotlib==3.9
from itertools import chain  # 迭代器工具

# 解决Matplotlib绘图时 中文乱码问题.
plt.rcParams['font.sans-serif'] = ['SimHei']  # Mac本字体改为: ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

# 1. 词频统计.
def dm01_word_count():
    # 1. 读取数据.
    train_data = pd.read_csv('./data/train.tsv', sep='\t')
    test_data = pd.read_csv('./data/dev.tsv', sep='\t')

    # 2. 统计 训练集中 词汇总数.
    train_vocab = set(chain(*map(lambda x: jieba.lcut(x), train_data['sentence'])))

    # 3. 统计 测试集中 词汇总数.
    test_vocab = set(chain(*map(lambda x: jieba.lcut(x), test_data['sentence'])))

    # 4. 打印结果.
    print(f'训练集的 词汇总数: {len(train_vocab)}')
    print(f'测试集的 词汇总数: {len(test_vocab)}')

if __name__ == '__main__':
    # 词频统计
    dm01_word_count()
  • 高频词云_代码实现
import pandas as pd
from itertools import chain
from wordcloud import WordCloud # 词云类
import jieba.posseg as pseg
import matplotlib.pyplot as plt

def get_a_word(line):
    # 分词并且标注词性
    word_dict = pseg.lcut(line)
    # print(word_dict)

    result_list = []

    # 过滤形容词
    for word,pos in word_dict:
        if pos=="a":
            result_list.append(word)

    return result_list

def show_cloud(a_word_list):
    # 1- 创建词云对象
    wordcloud_obj = WordCloud(font_path="data/SimHei.ttf",max_words=100,background_color="white")

    # 2- 词汇列表以空格分隔拼接成字符串
    words_str = " ".join(a_word_list)

    # 3- 绘制图形
    wordcloud_obj.generate(words_str)
    plt.figure()
    # bilinear:让文字边缘更加平滑
    plt.imshow(wordcloud_obj,interpolation="bilinear")
    plt.axis("off")
    plt.show()

def word_cloud():
    # 1- 读取文件,并且取出评价内容
    df = pd.read_csv("data/train.tsv",sep="\t",encoding="UTF-8")
    sentence_series = df["sentence"]

    # 2- 句子分词,过滤出形容词
    # 注意:map返回的是生成器,你要真的调用它,才会触发数据的产生
    a_word_list = list(chain(*map(get_a_word,sentence_series)))
    # a_word_list = list(chain(*map(lambda line: [word for word,pos in pseg.lcut(line) if pos=="a"],sentence_series)))
    # print(a_word_list)

    # 3- 绘制词云
    show_cloud(a_word_list)

if __name__ == '__main__':
    word_cloud()

文本特征处理【熟悉】

【介绍一下n-gram】

将n个连续相邻的token组合到一起,n-gram特征

【为什么要规范文本长度】

模型的输入需要【每个batch】或者【整个数据集】,统一句子长度
对语料添加普适性的特征:n-gram
对语料进行规范长度:适配模型的输入

添加N-Gram特征

n-gram介绍:
    概述:
        就是将连续的n个词组合到一块, 把这些连续片段当做1个新特征(小词组特征), 帮我们分析文本的规律.
    分析:
        uni-gram(1-gram): 把每个 词/字 拆出来.
        bi-gram(2-gram): 找连续2个词的组合.
        tri-gram(3-gram): 找连续3个词的组合.
    目的:
        让计算机更好的理解 文本规律,也就是上下文信息.

例如:
分词内容: 我| 爱| 黑马
uni-gram: 我| 爱| 黑马
bi-gram: 我爱|爱黑马
tri—gram: 我爱黑马
  • zip函数解释
if __name__ == '__main__':
    list1 = [1, 2, 3, 4, 5, 6]
    list2 = [2, 3, 4]

    result = list(zip(list1, list2))
    print(result)
  • 代码实现
"""
    n-gram:指的是 将相邻的n个词拼接到一起成为新词的过程。其中n一般取值为1、2、3
        1:uni-gram,不进行拼接操作
        2:bi-gram,将相邻的2个词拼接到一起
        3:tri-gram,将相邻的3个词拼接到一起

    作用:通过拼接相邻词的过程,提升模型对语义上下文的理解
    注意:n的值实际工作中一般是3以内,大多数情况是2。如果n太大,会导致词汇表个数膨胀,太长的拼接没有意义
"""

def n_gram_fn(n_gram):
    # 1- 测试数据
    word_list = ["aa","bb","cc","dd"]

    # 2- 对相邻的词进行合并
    """
        2-gram:
            ["aa","bb","cc","dd"]
            ["bb","cc","dd"]

        3-gram:
            ["aa","bb","cc","dd"]
            ["bb","cc","dd"]
            ["cc","dd"]
    """
    lists = [word_list[i:] for i in range(n_gram)]
    print(lists)

    print(list(zip(*lists)))

if __name__ == '__main__':
    n_gram_fn(n_gram=1)
    n_gram_fn(n_gram=2)
    n_gram_fn(n_gram=3)

文本长度规范

意义: 一般模型的输入需要等尺寸大小的矩阵, 所以需要对超长文本做截断, 不足的做补齐。这就是文本长度规范.
  • 代码实现
# from tensorflow.keras.preprocessing import sequence
from keras.preprocessing import sequence

sen_list = [
                [1, 23, 5, 32, 55, 63, 2, 21, 78, 32, 23, 1],
                [2, 32, 1, 23, 1]
        ]

# 用框架提供的方法实现句子长度的规范
def demo01():
    """
        sequences:要进行长度规范的句子列表
        maxlen:长度规范中的句子长度要求统一为多长,指的是词的个数
        truncating:截断的方向。默认是pre,从前面开始截断;post从后面开始截断
        padding:填充的方向。默认是pre,从前面开始填充;post从后面开始填充
        value:使用什么值进行填充。推荐使用默认的0,因为对原始数据的影响最小
    """
    result = sequence.pad_sequences(sen_list,maxlen=10,truncating="post",padding="post")
    # result = sequence.pad_sequences(sequences=sen_list,maxlen=10,truncating="pre",padding="pre")
    # result = sequence.pad_sequences(sequences=sen_list,maxlen=10,value=6666)
    print(result)

def demo02():
    # 1- 定义一个空列表用来存储规范化后的句子
    result_list = []

    maxlen = 10 # 句子的长度

    # 2- 分别对每条句子进行处理
    for sen in sen_list:
        if len(sen)>maxlen:
            # 超长的要截断
            result_list.append(sen[:maxlen])
        else:
            # 短的要填充
            result_list.append(sen + [0]*(maxlen-len(sen)))

    print(result_list)

if __name__ == '__main__':
    # 用框架提供的方法实现句子长度的规范
    demo01()

    # 使用最简单的python代码实现
    demo02()
💻 代码

文本预处理代码

17 个文件
  • 📁 01_文本处理的基本方法/
    • 📄 01-jieba分词器_基本使用.py 1.1KB
      import jieba
      
      """
          掌握的内容:result = jieba.lcut(content)
      """
      
      def demo01():
          content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"
          result = jieba.lcut(content)
          # result = list(jieba.cut(content))
      
          # ['传智', '教育', '是', '一家', '上市公司', ',', '旗下', '有', '黑马', '程序员', '品牌', '。', '我', '是', '在', '黑马', '这里', '学习', '人工智能']
          print(result)
          print(type(result)) # list
      
      def demo02():
          content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"
          result = jieba.lcut(content,cut_all=True)
          print(result)
      
      def demo03():
          content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"
          result = jieba.lcut_for_search(content)
          print(result)
      
      if __name__ == '__main__':
          # 精确模式
          demo01()
      
          # 全词模式
          demo02()
      
          # 搜索引擎模式:给搜索引擎使用的模式
          demo03()
      
    • 📄 02-jieba分词器_繁体和自定义词典.py 831B
      import jieba
      
      """
          什么情况下需要用户自定义词典?
          答:当你真的是一名NLP开发工程师的时候才需要;根据公司业务场景进行针对性的分词才需要
      """
      
      def demo01():
          content = "煩惱即是菩提,我暫且不提"
          result = jieba.lcut(content)
          print(result)
      
          content = "煩恼即是菩提,我暫且不提"
          result = jieba.lcut(content)
          print(result)
      
      def demo02():
          content = "传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能"
      
          # 加载自定义的词典
          jieba.load_userdict("../data/my_dict.txt")
      
          result = jieba.lcut(content)
          print(result)
      
      if __name__ == '__main__':
          # 繁体字分词
          demo01()
      
          # 用户自定义词典
          demo02()
      
    • 📄 03-jieba分词器_词性标注.py 355B
      import jieba.posseg as pseg
      
      if __name__ == '__main__':
          content = "我爱自然语言处理"
      
          # [pair('我', 'r'), pair('爱', 'v'), pair('自然语言', 'l'), pair('处理', 'v')]。将pair当成元组使用即可
          result = pseg.lcut(content)
          print(result)
      
          for word,pos in result:
              print(f"{word} 的词性是 {pos}")
      
  • 📁 02_文本张量表示方法/
    • 📄 01-词向量_onehot形式.py 3.0KB
      import os
      os.environ["TF_ENABLE_ONEDNN_OPTS"]="0" # 数字必须变成字符串
      
      # from tensorflow.keras.preprocessing.text import Tokenizer   # 词汇映射器
      from keras.src.legacy.preprocessing.text import Tokenizer   # 词汇映射器。上下的效果完全一样
      import joblib
      
      """
          one-hot总结:
              优点:非常简单,容易理解
              缺点:
                  1- one-hot词向量中大量的0,只有一个地方是1,是一个稀疏向量,会浪费存储和计算资源
                  2- 对于一次多义的情况,无法解决。小龙女 想 过过 过过 过过 的生活
      """
      def demo01():
          # 1- 准备数据
          vocabs = {"周杰伦", "陈奕迅", "王力宏", "李宗盛", "吴亦凡", "鹿晗"}
      
          # 2- 训练词汇映射器
          # 2.1- 创建词汇映射器
          my_tokenizer = Tokenizer()
          # 2.2- 训练词汇映射器
          my_tokenizer.fit_on_texts(vocabs)
          # 2.3- 得到训练后的结果数据:也就是词与索引的映射关系。注意:索引从1开始的
          word_dict = my_tokenizer.word_index
          # print(type(word_dict))  # 类型是dict。{'王力宏': 1, '周杰伦': 2, '吴亦凡': 3, '鹿晗': 4, '陈奕迅': 5, '李宗盛': 6}
          # print(word_dict)
      
          # 2.4- 保存训练好的词汇映射器
          joblib.dump(my_tokenizer,"../model/my_tokenizer.pkl")
      
          # 3- 获得每个词的one-hot词向量
          for word in vocabs:
              # 3.1- 创建一个全0的列表
              # [0,0,0,0,0,0]
              word_one_hot = [0] * len(vocabs)
      
              # 3.2- 获得词的索引
              index = word_dict[word] - 1
      
              # 3.3- 将指定位置的值设置为1
              word_one_hot[index] = 1
      
              print(f"{word} one-hot词向量 {word_one_hot}")
      
      def demo02():
          # 1- 准备数据
          star_name = "王力宏"
      
          # 2- 加载训练好的词汇映射器
          my_tokenizer = joblib.load("../model/my_tokenizer.pkl")
          word_dict = my_tokenizer.word_index
      
          # 3- 获得词的one-hot词向量
          # 3.1- 创建一个全0的列表
          word_one_hot = [0] * len(word_dict)
      
          # 3.2- 获得词的索引
          index = word_dict[star_name] - 1
      
          # 3.3- 将指定位置的值设置为1
          word_one_hot[index] = 1
      
          print(f"{star_name} one-hot词向量 {word_one_hot}")
      
      def demo03():
          # 1- 准备数据
          vocabs = ["周杰伦", "陈奕迅", "王力宏", "李宗盛", "吴亦凡", "鹿晗"]
      
          # 2- 获得每个词的one-hot词向量
          for word in vocabs:
              # 2.1- 初始化一个全0的列表
              word_one_hot = [0]*len(vocabs)
              # 2.2- 获得词的索引,注意该索引从0开始
              index = vocabs.index(word)
              # 2.3- 将指定位置的值设置为1即可
              word_one_hot[index] = 1
      
              print(f"{word} one-hot词向量 {word_one_hot}")
      
      
      if __name__ == '__main__':
          # 1- 训练词汇映射器,得到每个词的词向量
          demo01()
      
          # 2- 加载训练好的词汇映射器,得到某个词的词向量
          demo02()
      
          # 3- 【掌握】使用纯Python的方式实现
          demo03()
      
    • 📄 02-词向量_word2vec.py 2.2KB
      """
          如果不管是安装fasttext还是fasttext-wheel都失败,那么原因是python版本过高。
          操作步骤如下:
              1- 先进入对应的虚拟环境
              2- 安装低版本的python解释器
                  conda install python=3.10
              3- 安装fasttext
                  pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/
      """
      import fasttext
      
      def demo01():
          # 1- 使用无监督学习训练模型
          """
              为什么这里只能使用无监督学习?
              答:因为数据中没有明确标记目标值。有监督学习对文件内容有严格要求,有__label__
          """
          # model = fasttext.train_unsupervised("data/fil9")
          model = fasttext.train_unsupervised("../data/gz03ag")
      
          # 2- 保存训练好的模型
          model.save_model("../model/word2vec.pkl")
      
      def demo02():
          # 1- 加载训练好的模型
          model = fasttext.load_model("../model/word2vec.pkl")
      
          # 2- 获得某个词的词向量
          word_vec = model.get_word_vector("hello")
          print(word_vec)
      
      def demo03():
          # 1- 加载训练好的模型
          model = fasttext.load_model("../model/word2vec.pkl")
      
          # 2- 获得相近的几个词
          # 会从词形、词义方面进行查找。
          # result_list = model.get_nearest_neighbors("dog")
          result_list = model.get_nearest_neighbors("cat")
      
          # 3- 输出
          print(result_list)
      
      def demo04():
      
          """
              参数解释:
                  input:训练集路径
                  model:word2vec的模式。默认是skipgram,可以设置为cbow
                  dim:词向量的维度
                  epoch:训练的轮次
                  lr:初始学习率
                  thread:训练的线程个数
          """
          model = fasttext.train_unsupervised(
              input="../data/gz03ag",
              model="cbow",
              dim=200,
              epoch=1,
              lr=0.1,
              thread=10
          )
      
          model.save_model("../model/word2vec_better.pkl")
      
      if __name__ == '__main__':
          # 1- 训练并保存模型
          # demo01()
      
          # 2- 获得词的词向量
          # demo02()
      
          # 3- 获得相近的词
          # demo03()
      
          # 4- 超参数调优设置【掌握】
          demo04()
      
    • 📄 03-词向量_wordEmbedding.py 2.5KB
      import os
      
      import torch
      
      os.environ["TF_ENABLE_ONEDNN_OPTS"]="0" # 数字必须变成字符串
      
      import jieba
      from keras.src.legacy.preprocessing.text import Tokenizer
      import torch.nn as nn
      # 需要安装 pip install tensorboard
      from torch.utils.tensorboard import SummaryWriter   # 词向量可视化工具
      
      if __name__ == '__main__':
          # 1- 准备文本内容
          sentence1 = '传智教育是一家上市公司,旗下有黑马程序员品牌。我是在黑马这里学习人工智能'
          sentence2 = "我爱自然语言处理"
          sentence_list = [sentence1,sentence2]
      
          # 2- 分词处理
          # word_list最终的结构:[[词11, 词12...], [词21, 词22...]]
          # [['传智', '教育', '是', '一家', '上市公司', ',', '旗下', '有', '黑马', '程序员', '品牌', '。', '我', '是', '在', '黑马', '这里', '学习', '人工智能'], ['我', '爱', '自然语言', '处理']]
          word_list = []
          for sen in sentence_list:
              # lcut不会去重
              word_list.append(jieba.lcut(sen))
      
          # print(word_list)
      
          # 3- 训练词汇映射器:去重得到词汇表
          # 3.1- 创建词汇映射器
          my_tokenizer = Tokenizer()
          # 3.2- 训练
          my_tokenizer.fit_on_texts(word_list)
          # 3.3- 获得训练后的结果:词索引与词内容的映射字典
          index_word = my_tokenizer.index_word
          print(index_word)
      
          # 4- 【掌握】创建词嵌入层word Embedding
          """
              原理:句子 -> 分词 -> 去重得到词汇表 -> 得到词索引 -> Embedding -> 词向量
              
              参数解释:
                  num_embeddings:词汇表大小
                  embedding_dim:词向量维度。也就是一个词使用一个向量来表示,向量中元素的个数。该值一般是2的n次方,例如:128、256、512....
          """
          ebd = nn.Embedding(num_embeddings=len(index_word),embedding_dim=4)
      
          # 5- 获得每个词的词向量
          for i in range(len(index_word)):
              # 5.1- 根据词索引得到词向量
              # 注意:传递进去的需要是张量数据类型
              word_vector = ebd(torch.tensor(i))
      
              # 5.2- 输出
              print(f"词索引{i+1},词的内容 {index_word[i+1]},对应的词向量{word_vector}")
      
          # 6- 【了解】可视化展示
          summary_obj = SummaryWriter(log_dir="../../runs")  # 注意:runs目录不要手动创建;runs的绝对路径中不要有中文
          summary_obj.add_embedding(ebd.weight.data, index_word.values())
          summary_obj.close()
      
  • 📁 03_文本数据分析/
    • 📄 01-数据探索_目标值分布.py 1014B
      import seaborn as sns
      import matplotlib.pyplot as plt
      import pandas as pd
      """
          如果出现AttributeError: 'FigureCanvasInterAgg' object has no attribute 'tostring_rgb'. Did you mean: 'tostring_argb'?
          那么是matplotlib的版本过高导致的
          pip uninstall -y matplotlib
          pip install matplotlib==3.8.4 -i https://mirrors.aliyun.com/pypi/simple/
      """
      
      if __name__ == '__main__':
          # 1- 读取文件
          """
              注意:需要指定具体的分隔符sep,否则会报错pandas.errors.ParserError: Error tokenizing data. C error: Expected 6 fields in line 12, saw 8
          """
          train_df = pd.read_csv("../data/train.tsv",encoding="UTF-8",sep="\t")
          dev_df = pd.read_csv("../data/dev.tsv",encoding="UTF-8",sep="\t")
      
          # 2- 展示训练集
          plt.style.use("fivethirtyeight")
          sns.countplot(x="label",data=train_df)
          plt.title("train")
          plt.show()
      
          # 3- 展示验证集
          sns.countplot(x="label", data=dev_df)
          plt.title("dev")
          plt.show()
      
    • 📄 02-数据探索_句子长度分布.py 1.4KB
      import seaborn as sns
      import matplotlib.pyplot as plt
      import pandas as pd
      
      def demo01():
          # 1- 读取数据
          df = pd.read_csv("../data/train.tsv",sep="\t",encoding="UTF-8")
      
          # 2- 统计句子长度
          # 2.1- 获得句子列
          sentence_series = df["sentence"]
          # 2.2- 计算句子长度
          # 方式一:list(map)
          # df["length"] = list(map(lambda line:len(line),sentence_series))
      
          # 方式二:apply
          df["length"] = sentence_series.apply(lambda line:len(line))
          # print(df.head())
      
          # 3- 绘制图形
          # 3.1- 绘制分布直方图
          plt.style.use("fivethirtyeight")
          sns.countplot(x="length",data=df)
          plt.xticks([])
          plt.title("length_dist")
          plt.show()
      
          # 3.2- 绘制趋势曲线
          # kde:让曲线更加平滑
          sns.displot(x="length",data=df,kde=True)
          plt.show()
      
      
      def demo02():
          # 1- 读取数据
          df = pd.read_csv("../data/train.tsv", sep="\t", encoding="UTF-8")
      
          # 2- 统计句子长度
          # 2.1- 获得句子列
          sentence_series = df["sentence"]
          # 2.2- 计算句子长度
          df["length"] = sentence_series.apply(lambda line: len(line))
      
          # 3- 绘制图形
          sns.stripplot(x="label",y="length",data=df,hue="label")
          plt.title("label_length")
          plt.show()
      
      if __name__ == '__main__':
          # 句子长度分布
          demo01()
      
          # 好评差评的句子长度分布
          demo02()
      
    • 📄 03-数据探索_词汇总数.py 431B
      import jieba
      from itertools import chain
      import pandas as pd
      
      if __name__ == '__main__':
          df = pd.read_csv("../data/train.tsv",sep="\t",encoding="UTF-8")
      
          # map_result = map(lambda line:jieba.lcut(line), df["sentence"])
          # word_set = set(chain(*map_result))
      
          # 合并的写法版本
          word_set = set(chain(*map(lambda line:jieba.lcut(line), df["sentence"])))
      
          print("词汇总个数",len(word_set))
      
    • 📄 复杂代码解释.py 184B
      from itertools import chain
      
      if __name__ == '__main__':
          name_list = ["zhangsan","hello","aaaa"]
      
          result_1 = list(map(lambda name:len(name),name_list))
          print(result_1)
      
  • 📁 04_文本特征处理/
    • 📄 01-特征预处理_n_gram.py 777B
      def n_gram_fn(n):
          # 1- 准备数据
          word_list = ["aa", "bb", "cc", "dd"]
      
          # 2- 对相邻的词进行合并
          """
              2-gram:bi-gram,将相邻的2个词合并成新的词
                  列表一:["aa", "bb", "cc", "dd"]
                  列表二:["bb", "cc", "dd"]
                  结果是:[aabb,bbcc,ccdd]
              
              3-gram:tri-gram,将相邻的3个词合并成新的词
                  列表一:["aa", "bb", "cc", "dd"]
                  列表二:["bb", "cc", "dd"]
                  列表三:["cc", "dd"]
                  结果是:[aabbcc,bbccdd]
          """
          lists = [word_list[i:] for i in range(n)]
          # print(lists)
      
          print(list(zip(*lists)))
      
      if __name__ == '__main__':
          n_gram_fn(1)
          n_gram_fn(2)
          n_gram_fn(3)
      
    • 📄 02-特征预处理_句子长度规范.py 2.0KB
      import os
      os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"
      
      # 下面两个等价
      # from tensorflow.keras.preprocessing import sequence
      from keras.preprocessing import sequence
      
      """
          句子长度规范的作用【掌握】:
              1- Dataloader(batch_size=1),张量的形状不相同的时候只能设置为1。因为里面是使用stack对张量进行拼接
                  stack的要求是张量形状完全相同
              2- 迁移学习中有现成的参数可以进行设置
      """
      
      def demo01():
          # 1- 准备数据,两条句子
          sen_list = [
              [1, 23, 5, 32, 55, 63, 2, 21, 78, 32, 23, 1],
              [2, 32, 1, 23, 1]
          ]
      
          # 2- 句子长度处理
          """
              参数解释:
                  sequences:要进行句子长度规范处理的句子列表
                  maxlen:句子长度规范的上限。也就是句子中词个数的上限
                  padding:对少于长度上限的进行填充。推荐用post
                      pre:在句子的前面进行填充
                      post:在句子的后面进行填充
                  truncating:对大于长度上限的进行截断。推荐用post
                      pre:在句子的前面进行截断
                      post:在句子的后面进行截断
                  value:padding的时候的填充值。推荐使用0,因为对句子的含义不会造成影响
          """
          # result = sequence.pad_sequences(sequences=sen_list,maxlen=10)
          result = sequence.pad_sequences(sequences=sen_list,maxlen=10,padding="post",truncating="post",value=6666)
          print(result)
      
      def demo02():
          # 1- 准备数据
          sen_list = [
              [1, 23, 5, 32, 55, 63, 2, 21, 78, 32, 23, 1],
              [2, 32, 1, 23, 1]
          ]
      
          # 2- 分别对每条句子进行长度规范的处理
          result_list = []
          for sen in sen_list:
              if len(sen)>10:
                  # 截断
                  result_list.append(sen[:10])
              else:
                  result_list.append(sen + [0] * (10 - len(sen)))
      
          print(result_list)
      
      if __name__ == '__main__':
          demo01()
      
          demo02()
      
    • 📄 复杂代码解释.py 143B
      if __name__ == '__main__':
          list1 = [1, 2, 3, 4, 5, 6]
          list2 = [22, 33, 44]
      
          result = list(zip(list1, list2))
          print(result)
      
  • 📁 data/
    • ⚙️ SimHei.ttf 9.6MB
    • ⚙️ dev.tsv 236.9KB
    • 📄 my_dict.txt 90B
      # 自定义词典格式   词 词频 词性
      传智教育 10 n
      黑马程序员 20
      我是
      
    • ⚙️ train.tsv 707.4KB

第3节 · 第3章 RNN 及其变体

🎯 掌握 RNN / LSTM / GRU 结构、注意力机制与英译法案例。
📖 笔记

3. RNN 及其变体

第三章 RNN及其变体

认识RNN模型

  • 定义
循环神经网络:一般接受的一序列进行输入,输出也是一个序列
  • 作用和应用场景
RNN擅长处理连续语言文本,机器翻译、文本生成、文本分类、摘要生成
  • RNN模型的分类

  • 根据输入与输出结构

properties N Vs N : 输入和输出等长,应用场景:对联生成;词性标注;NER N Vs 1 : 输入N,输出为单值,应用场景:文本分类 1 Vs N : 输入是一个,输出为N,应用场景:图片文本生成 N Vs M : 输入和输出不等长,应用场景:文本翻译、摘要总结

  • 根据RNN内部结构

properties 传统RNN LSTM BI-LSTM GRU BI-GRU

传统RNN模型【复习】

基本概念

RNN(Recurrent Neural Network), 中文称作循环神经网络, 它一般以序列数据为输入, 通过网络内部的结构设计有效捕捉序列之间的关系特征, 一般也是以序列形式进行输出.

参数解释:

image-20251023191445324

image-20251023191457249

内部结构

来源文章:https://colah.github.io/posts/2015-08-Understanding-LSTMs/

  • 输入:当前时间步xt和上一时间步输出的ht-1
  • 输出:ht和ot (一个时间步内:ht=ot)

image-20260330110357125

  • 多层RNN的解析

~~~properties
output记录的是 最后一层 在每个时间步的隐藏状态。
hidden 记录的是 每一层 在最后一个时间步的隐藏状态。
~~~

1685938431553

代码实现

import torch
import torch.nn as nn

def rnn_demo(num_layers):
    # 1- 创建RNN循环网络层
    rnn = nn.RNN(input_size=4,hidden_size=5,num_layers=num_layers,batch_first=False)

    # 2- 准备数据
    # 2.1- 上一个时间步的隐藏状态
    h0 = torch.randn(size=(num_layers,2,5))
    input = torch.randn(size=(3,2,4))

    # 3- 调用RNN网络
    """
        输入参数张量形状:
            上一个时间步的隐藏状态h0:[num_layers隐藏层层数,batch_size每个批次中样本的条数,hidden_size隐藏状态向量维度]
            本次输入数据input:[seq_len每条样本中词的个数,batch_size每个批次中样本的条数,input_size词向量维度]

        输出参数张量形状:
            更新后的隐藏状态hidden:[num_layers隐藏层层数,batch_size每个批次中样本的条数,hidden_size隐藏状态向量维度]
            本次预测结果output:[seq_len每条样本中词的个数,batch_size每个批次中样本的条数,hidden_size隐藏状态向量维度]
    """
    output,hidden = rnn(input, h0)

    print(f"output的形状-->{output.shape}")    # [3,2,5]
    print(f"hidden的形状-->{hidden.shape}")    # [num_layers,2,5]

    print(f"output的内容-->\n{output}")
    print(f"hidden的内容-->{hidden}")

if __name__ == '__main__':
    # rnn_demo(num_layers=1)
    rnn_demo(num_layers=2)

image-20260330114021259

总结

传统RNN的优缺点:

优点:
    内部结构简单, 资源消耗相对较小.
缺点:
    处理长序列(一句话中词的个数很多)数据时, 因为 反向传播 结合 梯度连乘, 过大或者过小的w值都会导致 梯度爆炸或者梯度消失.

    梯度爆炸:梯度值越来越大,导致权重值越来越大偏离最优的结果
    梯度消失:梯度值越来越小,导致权重值更新非常缓慢甚至不更新
    梯度震荡:学习率设置的不合理,导致权重值在最优点附近来回横跳

LSTM模型

基本概念

LSTM(Long Short-Term Memory)也称长短时记忆结构, 它是传统RNN的变体, 与经典RNN相比能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象,处理长序列数据效果差的问题。同时LSTM的结构更复杂

LSTM由如下四个部分组成:

  • 遗忘门
  • 输入门
  • 细胞状态
  • 输出门

补充:【门】这个词的理解

它来自于电子工程,用来控制信号的输出。在NLP中,可以与生活中的门、音量按钮等进行类比理解

内部结构【理解】

  • 细胞状态:是 LSTM 的记忆库,贯穿整个序列,用来解决长序列记忆问题

image-20251025230424577

  • 遗忘门:决定上一个时间步输出的细胞状态中,哪些内容需要丢弃掉

image-20251025230324205

  • 输入门:当前输入的数据中,哪些要记录到细胞状态中

image-20251025230342459

  • 输出门:决定本次隐藏状态要输出哪些信息

image-20251025230358159

  • 完整结构图解释

image-20260330152230287

代码实现【掌握】

import torch
import torch.nn as nn

if __name__ == '__main__':
    # 1- 创建LSTM网络结构
    """
        参数解释:
            bidirectional:是否是双向的网络结构。默认是False,也就是单向的。如果设置为True,LSTM就变成Bi-LSTM
    """
    lstm = nn.LSTM(input_size=4,hidden_size=5,num_layers=1,bidirectional=False)

    # 2- 准备数据
    # input的形状:[seq_len句子中词的个数,batch_size每个批次句子的个数,input_size词向量的维度]
    input = torch.randn(size=(6,3,4))

    """
        因为最终的隐藏状态是根据细胞状态经过激活函数处理得到的,
        但是激活函数不会改变张量的形状,只会改变值
        因此:隐藏状态 和 细胞状态 的张量形状是相同的
        实际工作中,一般对隐藏状态 和 细胞状态全都使用全0初始化
    """
    h0 = torch.zeros(size=(1,3,5))
    c0 = torch.zeros(size=(1,3,5))

    # 3- 调用LSTM
    """
        output形状:[seq_len,batch_size,hidden_size]
        hidden形状:[num_layers,batch_size每个批次句子的个数,hidden_size]
    """

    # 注意:传递和接受结果,都需要将隐藏状态 和 细胞状态用元组包起来
    output,(hidden,c1) = lstm(input,(h0,c0))

    print(f"output形状-->{output.shape}") # 6,3,5
    print(f"hidden形状-->{hidden.shape}")
    print(f"c1形状-->{c1.shape}") # 1,3,5

    print(f"output-->{output}")
    print(f"hidden-->{hidden}")
    print(f"c1-->{c1}")

Bi-LSTM

定义: 不改变原始的LSTM模型内部结构,只是将文本从左到右计算一遍,再从右到左计算一遍,把最终的输出结果拼接得到模型的完整输出

image-20240617181144201

import torch
import torch.nn as nn

if __name__ == '__main__':
    # 1- 创建LSTM
    """
        参数解释:
            bidirectional:LSTM循环网络层是否是双向的。如果为True,那就是Bi-LSTM
                        如果是双向的LSTM,那么隐藏状态 和 细胞状态 张量中第一个形状=num_layers*2
    """
    lstm = nn.LSTM(input_size=4,hidden_size=5,num_layers=2,batch_first=False,bidirectional=True)

    # 2- 准备数据
    # 2.1- 本次输入数据
    input = torch.randn(size=(3,2,4))

    # 2.2- 上一个时间步的隐藏状态。初始隐藏状态一般全0初始化
    h0 = torch.zeros(size=(4,2,5))

    # 2.3- 上一个时间步的细胞状态。初始细胞状态一般全0初始化,它的形状与隐藏状态的形状完全一样
    c0 = torch.zeros(size=(4, 2, 5))

    # 3- 调用LSTM
    """
        注意:隐藏状态  与  细胞状态  顺序,不能调整;并且这两个值要用小括号包起来
        输入参数:本次输入数据, (上一个时间步的隐藏状态, 上一个时间步的细胞状态)
        返回结果:本次预测结果, (更新后的本次时间步隐藏状态, 更新后的细胞状态)
    """
    output,(hidden,c) = lstm(input,(h0,c0))

    print(f"output的形状-->{output.shape}")  # [3,2,5]
    print(f"hidden的形状-->{hidden.shape}")  # [4,2,5]
    print(f"细胞状态的形状-->{c.shape}")  # [4,2,5]

    print(f"output的内容-->\n{output}")
    print(f"hidden的内容-->{hidden}")
    print(f"细胞状态的内容-->{c}")

总结

LSTM的优缺点是什么?

优点:
    1. 能够处理更长的句子
    2. 缓解梯度消失或爆炸的问题 -> 训练更稳定.
    3. 能够抓取复杂的特征.
缺点:
    1. 计算慢, 消耗资源大.
    2. 调参相对较难.
    3. 可解释性稍差.

GRU模型

基本概念

GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象. 同时它的结构和计算要比LSTM更简单, 它的核心结构可以分为两个部分去解析:

  • 更新门
  • 重置门

内部结构【理解】

image-20260330171304079

代码实现【掌握】

import torch
import torch.nn as nn

def gru_demo(num_layers):
    # 1- 创建GRU循环网络层
    """
        参数解释:
            bidirectional:GRU循环网络层是否是双向的。如果为True,那就是Bi-GRU
                        如果是双向的GRU,那么隐藏状态 和 细胞状态 张量中第一个形状=num_layers*2
    """
    gru = nn.GRU(input_size=4,hidden_size=5,num_layers=num_layers,batch_first=False,bidirectional=False)

    # 2- 准备数据
    # 2.1- 上一个时间步的隐藏状态
    h0 = torch.randn(size=(num_layers,2,5))
    input = torch.randn(size=(3,2,4))

    # 3- 调用GRU网络
    """
        输入参数张量形状:
            上一个时间步的隐藏状态h0:[num_layers隐藏层层数,batch_size每个批次中样本的条数,hidden_size隐藏状态向量维度]
            本次输入数据input:[seq_len每条样本中词的个数,batch_size每个批次中样本的条数,input_size词向量维度]

        输出参数张量形状:
            更新后的隐藏状态hidden:[num_layers隐藏层层数,batch_size每个批次中样本的条数,hidden_size隐藏状态向量维度]
            本次预测结果output:[seq_len每条样本中词的个数,batch_size每个批次中样本的条数,hidden_size隐藏状态向量维度]
    """
    output,hidden = gru(input, h0)

    print(f"output的形状-->{output.shape}")    # [3,2,5]
    print(f"hidden的形状-->{hidden.shape}")    # [num_layers,2,5]

    print(f"output的内容-->\n{output}")
    print(f"hidden的内容-->{hidden}")

if __name__ == '__main__':
    # gru_demo(num_layers=1)
    gru_demo(num_layers=2)

Bi-GRU

定义: 不改变原始的GRU模型内部结构,只是将文本从左到右计算一遍,再从右到左计算一遍,把最终的输出结果拼接得到模型的完整输出

总结

GRU的优缺点是什么?

优点:相比LSTM,结构较为简单,能够和lstm一样缓解梯度消失问题
缺点:只能缓解了梯度消失和爆炸问题, 不能并行计算
RNN、LSTM、GRU使用总结:
    1- 如果项目、需求简单,短序列(<50)的时候使用RNN
    2- 如果是中等序列(50-200)的使用使用GRU
    3- 如果是长序列(>200)的时候使用LSTM
    4- 如果是计算资源充足,推荐使用LSTM、Bi-LSTM、Bi-GRU;如果资源紧张考虑GRU

注意力机制【理解】

基本概念

Seq2Seq是啥,它和RNN、LSTM、GRU的关系和区别是啥?

1- Seq2Seq是一种模型架构,用于将一个序列映射到另一个序列。典型结构是 Encoder-Decoder(编码器-解码器)
2- RNN、LSTM、GRU是神经元内部结构
3- Seq2Seq模型的编码器和解码器通常由 RNN、LSTM 或 GRU 构成
4- 可以将Seq2Seq理解为房子,RNN、LSTM、GRU理解为建房子的砖

什么是机器翻译?

A文字 -> B文字,语言一样,叫文本生成
A文字 -> B文字,语言不同,叫机器翻译

注意力机制是什么意思?

将人类对事物的观察的习惯、能力,迁移到神经网络中,让模型在进行机器翻译、文本生成的时候,重点关注原始数据中的什么内容。

注意力机制(Attention Mechanism)是深度学习中一种增强模型对输入数据中重要信息关注能力的技术
注意力机制让模型在处理序列数据时动态地赋予不同输入数据不同的权重

注意力机制的由来

没有注意力机制之前存在的弊端: 
    弊端1: 如果翻译的句子很长很复杂,比如直接一篇文章输进去,模型的计算量很大,并且模型的准确率下降严重
    弊端2: 没有考虑词与词之间的相关性,导致翻译效果比较差

image-20260402100759496

什么是注意力机制

==注意力机制是将人的感知方式、注意力的行为应用在机器上,让机器学会去感知数据中的重要和不重要的部分。==

将人类对事物的观察的习惯、能力,迁移到神经网络中,让模型在进行机器翻译、文本生成的时候,重点关注原始数据中的什么内容。

注意力机制分类及其实现

基本概念

1、【重点】软注意力: 也称之为全局注意。注意力权重值分布在0-1之间,关注所有的词汇,但是不同词汇根据权重大小关注的程度不一样。
2、硬注意力: 也称之为局部注意。注意力权重值是0或者1,只关注哪些重要的部分,忽略次要的部分
3、自注意力: 也称之为内注意。通过输入项内部的"表决"来决定应该关注哪些输入项.

软注意力

没有加Attention的Encoder-Decoder框架

==这个图主要理解谁是Target,谁是Source==

image-20260102151856454

加Attention的Encoder-Decoder框架

image-20260402111225256

==专属信息包C如何来的:权重值和对应的隐藏状态进行乘法,然后累加求和==

如何得到注意力概率分布

image-20260402114914567

为什么要计算注意力分配系数?
    在Decoder生成当前词时,该重点关注Encoder里哪些单词
    例如:生成 汤姆 时,该多关注Encoder中的Tom、还是Chase、还是Jerry


注意力分配权重的计算过程如下(面试时使用)
1-计算相似性
使用相似性计算公式,对比Decoder上一个时间步的隐藏状态和Encoder中每个单词的隐藏状态计算相似性,看谁更重要
2-将相似性转成权重矩阵
使用Softmax激活函数,将上面的相似性转成权重(概率),并且权重值之和为1
3-加权求和
使用第2步的权重矩阵与Encoder中每个单词的隐藏状态进行乘法和加法运算,得到专属信息包C
Attention机制的本质思想

image-20260102165831247

几个英文单词的关系:
    Encoder编码器:
        Source:
            Key
            Value

    Decoder解码器:
        Target:
            Query

以使用百度为例:
    搜索内容: 人工智能      -> Query
    匹配过程: 网页源代码    -> Key
    查看结果: 网页详情页面  -> Value

Seq2Seq+注意力机制的架构中:
    Query: 是解码器中上一个时间步中GRU隐藏层输出的隐藏状态
    Key: 是编码器中当前时间步中GRU隐藏层输出的隐藏状态的拼接
    Value: 与Key是完全相同的,是编码器中当前时间步中GRU隐藏层输出的隐藏状态的拼接

==计算的详细过程【掌握】==:

image-20260402145247073

总结(面试使用):注意力分配权重的计算过程
1- 计算相似性
使用nn.Linear计算Decoder端上一个时间步隐藏状态Query 和 Encoder端各个时间步的隐藏状态Key计算相似性

2- 相似性转权重
使用Softmax激活函数,将相似性转成权重矩阵。权重和为1

3- 加权求和
权重矩阵与Encoder端各个时间步的隐藏状态Value加权求和

Key和Value大多数情况是完全相同,表示编码器端的词经过GRU以后得到的隐藏状态

硬注意力【了解】

硬性注意力: 根据注意力分布选择输入向量中的一个作为输出。这里有两种选择方式:
    1- 选择注意力分布中,分数最大的那一项对应的输入向量作为Attention机制的输出
    2- 根据注意力分布进行随机采样,采样结果作为Attention机制的输出
    只关注被选中的部分,那么权重就是1;没选中的不关注,那么权重就是0

硬性注意力通过以上两种方式选择Attention的输出,这会使得最终的损失函数与注意力分布之间的函数关系不可导,导致无法使用反向传播算法训练模型,硬性注意力通常需要使用强化学习来进行训练。因此,一般深度学习算法会使用软性注意力的方式进行计算。

自注意力

自注意力机制: 对每个输入项分配的权重取决于输入项之间的相互作用,即通过输入项内部的"表决"来决定应该关注哪些输入项。
也就是指的是Source内部元素之间或者Target内部元素之间发生的注意力机制,也可以理解为Target=Source这种特殊情况下的注意力机制。

和前两种相比,在处理很长的输入时,具有并行计算的优势。

Transformer框架内部用到了自注意力机制。

结论: query、key、value,如果相等,就是自注意力机制;否则就不是

注意力计算规则

Pytorch中注意力机制计算原理

image-20260402154327907

以翻译得到单词to为例介绍完整的流程

Encoder编码器端:
    1: 欢迎、来、北京 经过词嵌入层的处理得到对应的词向量
    2: 再将词向量送入到GRU中,得到对应的隐藏状态。对应时间步的隐藏状态再经过线性变换得到Key和Value。具体请看下面的【问题二:K或者V的具体来源】
    3: 对编码器端所有词对应的Key、Value进行张量拼接,分别得到K、V
        例如:每个批次batch_size中有2条句子,seq_len每条句子中词的个数有5个,每个词的隐藏状态向量维度8
        那么K和V的张量形状是[2batch_size, 5seq_len, 8hidden_size]
    4: 最后一个时间步的隐藏状态Key3作为初始隐藏状态给到Decoder解码器使用


Decoder解码器端:
    1: 解码器初始输入GO作为翻译的起始标识,输入到GRU中,翻译得到第一个词Welcome和本次更新后的隐藏状态h1
    2: 将上一个时间步的隐藏状态h1作为Q
    3: Q和K进行张量concat拼接
    4: 通过nn.Linear计算Q和K的相似性得分
    5: 通过Softmax激活函数将相似性得分转成权重矩阵
    6: 权重矩阵和V进矩阵乘法运算,也就是加权求和得到专属信息包C,专属于to的信息包
    7: Q和专属信息包C进行张量concat拼接
    8: 通过nn.Linear对拼接后的张量形状进行调整,以满足GRU的输入要求

问题一:上面讲的是翻译to之前计算专属信息包,那翻译Welcome的时候,专属信息包如何计算的?也就是【编码器端最后一个时间步的隐藏状态Key3会作为解码器端的初始隐藏状态h0使用,会作为Q】,而Q第一步又会和K拼接以及算相似性,那么不会出现重点关注【北京】词,而不是【欢迎】词吗?

答:

​ 1- 编码器端最后一个时间步的隐藏状态Key3会作为解码器端的初始隐藏状态h0使用:并不是直接使用,中间会经过nn.Linear的线性变换,变换后Q和K的相似性就低了

​ 2- 前向传播看起来类似,但是不要忘记还有反向传播,通过让损失值减小,会让注意力重点关注【北京】词。在训练过程中,如果模型错误地关注了“北京”,导致翻译错误,损失函数会很大。反向传播会调整权重 W ,使得与“北京”相关的分数降低,与“欢迎”相关的分数升高。

问题二:K或者V的具体来源

答:原始输入词 -> 词嵌入层 -> 词向量 -> GRU循环神经元 -> ==当前时间步隐藏状态h -> 线性变化Wh -> 每个时间步的Key== -> 拼接 -> K和V。==K和V的张量形状[batch_size, seq_len, hidden_size]==

在编码器端,RNN按时间步处理输入序列,每个时间步的隐藏状态计算为:

$ h_t=tanh⁡(W^{xh}⋅x_t+W_{hh}⋅h_{t−1}+b_h) $

Key是由所有时间步的隐藏状态组成的矩阵,经过线性变换得到:

$ K=[h1,h2,...,hn]⋅W^K $

以上理论对应的大模型prompt如下:https://www.qianwen.com/share/chat/6502a8a327cc4bc8a457210bf71b5594

第一个:Seq2Seq+注意力机制中,Query、Key、Value的详细来源,以及专属信息包C的完整计算过程

第二个:K具体是如何来的,是直接来自于隐藏状态吗

计算公式

计算规则前提

1- 当Q、K、V相等, 称作自注意力
2- 当Q、K、V不相等时,称为一般注意力(包括软注意力、硬注意力)
3- 自注意力 和 一般注意力的区别,只是数据层面(也就是QKV是否相等)的区别
4- 具体计算得到 自注意力 和 一般注意力,有如下的3种公式。目前使用最多的是第1个和第3个。

注意: 
    1: 不要将注意力的三种类别(软注意力、硬注意力、自注意力) 和 注意力计算的3个公式搞混
    2: 也就是下面的3个公式,即能够算软注意力,也能够算自注意力
    3: Transformer框架中使用的是第3个公式算自注意力,原因是除以根号dk

三种规则方法

  • 公式1: 将Q和K进行纵轴拼接,然后经过线性变换,再经过Softmax进行处理得到权重,最后和V进行相乘

image-20251028155344832

  • 【了解】公式2:将Q和K进行纵轴拼接,接着经过一次线性变化,然后进过tanh激活函数处理,再进行sum求和,再经过softmax进行处理得到权重,最后和V进行张量的乘法

image-20251028155353982

  • 公式3:将Q和K的转置进行点乘,然后除以一个缩放系数,再经过softmax进行处理得到权重,最后和V进行张量的乘法

==注意:dk是k的维度,等于词向量的维度 或者 隐藏状态维度==

image-20251028155402099

常见面试题:为什么公式要除以根号dk

答:防止矩阵点乘运算结果过大,导致Softmax计算后概率值出现极端值,极大或极小。避免模型对这些极值学习的不好

【了解】K为什么要转置?

举例:

​ K和V的形状是相同的,假设K的形状是[N个词, M的隐藏状态]

​ Q是单个时间步的隐藏状态,形状是[1, M的隐藏状态]

​ 为了能和V进行矩阵乘法运算,那么K需要转置

bmm函数介绍:

import torch

"""
    bmm和matmul对比总结
        1- 都能够实现矩阵乘法运算。matmul是串行计算,效率低;bmm是并行计算,效率高
        2- 如果matmul的张量形状不等,会进行广播机制
        3- bmm的要求如下:
            3.1- 参与运算的两个张量的形状有且只能是三维。形状的含义(batch_size,行数,列数)
            3.2- 参与运算的两个张量的batch_size必须相等
            3.3- 参与运算的两个张量中,前一个张量的列数 与 后一个张量的行数 必须相同

    后面在计算注意力的时候,如果满足bmm的要求,推荐使用bmm;当然可以无脑使用matmul,因为兼容性更好
"""
if __name__ == '__main__':
    # 准备数据
    mat_1 = torch.randint(low=1,high=5,size=(10,3,4))
    # mat_1 = torch.randint(low=1,high=5,size=(1,3,4))
    mat_2 = torch.randint(low=1,high=5,size=(10,4,5))

    # matmul
    matmul_result = mat_1 @ mat_2
    print("matmul_result:",matmul_result.shape)

    # bmm
    bmm_result = torch.bmm(mat_1,mat_2)
    print("bmm_result:", bmm_result.shape)

注意力机制的作用

注意:编码器、解码器都可以选择是否要加注意力机制
1- 编码器、解码器【都不加】注意力机制,那么就是最初的Seq2Seq
2- 编码器、解码器【都加】注意力机制,那么就是后面要学的Transformer架构
3- 编码器不加注意力机制,而解码器加注意力机制,那么就是Seq2Seq+注意力
4- 编码器加注意力机制,而解码器不加注意力机制,这种情况很少见

image-20260104110602466

实现注意力机制【掌握】

  • 步骤
第一步: 按照注意力规则,对Q、K、V进行注意力的计算
第二步: 如果第一步是拼接操作,需要将Q和第一步计算的结果进行再次拼接,如果是点乘运算,Q和K、V相等,不需要拼接
第三步: 我们需要将第二步的结果,进行线性变化,按照指定输出维度进行结果的表示
  • 原理图

image-20260402171345282

  • 代码实现
import torch
import torch.nn as nn
from torch import Tensor


class MyAtten(nn.Module):
    def __init__(self,query_size,key_size,c_size,output_size):
        """
        :param query_size: 张量Q最后一个维度的形状,目前这里就是8
        :param key_size: 张量K最后一个维度的形状,目前这里就是8
        :param c_size: 张量C最后一个维度的形状,目前这里就是8
        :param output_size: Q和C拼接后,并且经过线性调整形状后的张量中最后一个维度的形状,目前这里就是8
        """

        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.query_size = query_size
        self.key_size = key_size
        self.c_size = c_size
        self.output_size = output_size

        # 3- 搭建网络结构
        # 3.1- 计算QK相似性的线性层。对应原理图中的第2步
        self.score_linear = nn.Linear(in_features=self.query_size+self.key_size,out_features=1)
        # self.score_linear = nn.Linear(in_features=16,out_features=1)

        # 3.2- 调整Q和C拼接后的张量形状线性层。对应原理图中的第6步
        self.qc_linear = nn.Linear(in_features=self.query_size+self.c_size,out_features=self.output_size)
        # self.qc_linear = nn.Linear(in_features=16,out_features=8)

    def forward(self,Q:Tensor,K,V):
        """
        目前暂时不研究QKV如何来的。只计算数据准备好后的注意力的计算过程。对应原理图中中间的绿色框
        :param Q: query,张量形状[batch_size,num_layers,hidden_size] -> [2,1,8]
        :param K: key,张量形状[batch_size,seq_len,hidden_size] -> [2,5,8]
        :param V: value,与key完全一致,张量形状[batch_size,seq_len,hidden_size] -> [2,5,8]
        :return:
        """
        # 1- Q和K拼接
        # 1.1- Q和K目前不能直接拼接,需要将Q的形状 由 [2,1,8] -> [2,5,8]
        seq_len = K.shape[1]
        # expand:扩大张量的形状。-1表示调整前后对应位置的形状不变
        Q_expand = Q.expand(size=(-1,seq_len,-1))
        print(f"1-->{Q.shape}") # [2,1,8]
        print(f"2-->{Q_expand.shape}") # [2,5,8]

        # 1.2- 拼接:Q_expand [2,5,8] K [2,5,8] -> [2,5,16]
        qc_cat = torch.cat(tensors=(Q_expand,K), dim=-1)
        print(f"3-->{qc_cat.shape}") # [2,5,16]

        # 2- 使用线性层对QK拼接后的结果计算相似性
        score = self.score_linear(qc_cat)
        print(f"4-->{score.shape}") # [2,5,1]

        # 3- 将相似性转成权重矩阵
        # 3.1- 调整score的形状:由[2,5,1] -> [2,1,5]
        score = score.transpose(dim0=1,dim1=2)
        print(f"5-->{score.shape}") # [2,1,5]

        # 3.2- 调用Softmax
        weighted_mat = torch.softmax(score, dim=-1)
        print(f"6-->{weighted_mat.shape}") # [2,1,5]

        # 4- 加权求和:权重矩阵和V进行矩阵乘法。计算得到专属信息包
        # weighted_mat [2,1,5];V [2,5,8] -> [2,1,8]
        C = torch.bmm(weighted_mat, V)
        print(f"7-->{C.shape}") # [2,1,8]

        # 5- Q和C拼接:Q [2,1,8];C [2,1,8] -> [2,1,16]
        qc_cat = torch.cat(tensors=(Q,C), dim=-1)
        print(f"8-->{qc_cat.shape}") # [2,1,16]

        # 6- 对拼接后的张量进行形状调整,以满足Decoder端GRU输入数据的要求
        result = self.qc_linear(qc_cat)
        print(f"9-->{result.shape}") # [2,1,8]

        return result,weighted_mat

if __name__ == '__main__':
    # 因为只讲PyTorch中注意力是如何计算得到。因此拼接后的value、Q、V我们直接手动初始化

    # 1- 定义基础变量
    batch_size = 2      # 每个批次中句子的条数
    seq_len = 5         # 每个句子中词的个数
    hidden_size = 8     # 我们人为设置词向量维度和隐藏向量维度相同
    num_layers = 1      # 隐藏层层数为1

    # 2- 初始化Q、K、V
    """
        为什么Q的形状中间是1?
        答:当前只针对Decoder解码器端一个时间步。当前时刻拿一个Q,去和Encoder全部位置算相似性
    """
    Q = torch.randn(size=(batch_size,num_layers,hidden_size))   # Decoder端当前时间步的隐藏状态
    K = torch.randn(size=(batch_size,seq_len,hidden_size))      # Encoder端多个词的隐藏状态拼接后的
    V = torch.randn(size=(batch_size,seq_len,hidden_size))      # Encoder端多个词的隐藏状态拼接后的

    # 3- 计算注意力
    query_size = hidden_size
    key_size = hidden_size
    c_size = hidden_size
    output_size = hidden_size

    attn_model = MyAtten(query_size,key_size,c_size,output_size)
    output,attn_weight = attn_model(Q,K,V)

    print(f"权重值{attn_weight}")
    print(f"权重值和{attn_weight.sum()}")

运行结果:

image-20260331184319588

让代码运行在GPU上

==Windows、Linux、Mac(M芯片)均可==

==Windows(无独显)、Mac(Intel芯片)不行==

  • 步骤
1: 【推荐】创建新的虚拟环境进行操作
    conda create -n nlp_cuda python==3.10

2: 进入虚拟环境
    conda activate nlp_cuda

3: 去黑窗口查看你本机的CUDA版本。【推荐】先升级英伟达。【操作截图见下面】
    nvidia-smi

4: 去Pytorch官网找到对应你cuda版本的下载命令,【截图见下面】
    官网地址: https://pytorch.org

5: 安装对应版本的cuda。安装命令
    pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cu130

6: 在PyCharm中关联新的沙箱, 运行下边的代码测试即可.

7: 如果不OK,大概率是 显卡驱动(Cuda版本) 和 你的PyTorch版本不兼容, 建议: 直接更新驱动.

8: 要安装的其他包,一个一个安装即可
    pip install jieba -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install joblib -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install fasttext-wheel -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install seaborn -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install matplotlib==3.8.4 -i https://pypi.tuna.tsinghua.edu.cn/simple/
    pip install tqdm -i https://pypi.tuna.tsinghua.edu.cn/simple/

image-20251028220646355

image-20251028191632859.png

image-20251028192049123

image-20251028192957906

  • 代码
import torch

# 设备选择,我们可以选择在cuda或者cpu上运行你的代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 如果是Mac M芯片, 写这个
# device = 'mps'

print(f'device: {device}')

Seq2Seq英译法案例【多练习】

任务目的

目的: 给定一段英文,翻译为法文
典型的文本分类任务: 每个时间步去预测应该属于哪个法文单词

数据格式

  • 注意:两列数据,第一列是英文文本,第二列是法文文本,中间用制表符号"\t"隔开
i am from brazil .  je viens du bresil .
i am from france .  je viens de france .
i am from russia .  je viens de russie .
i am frying fish .  je fais frire du poisson .
i am not kidding .  je ne blague pas .

实现流程

1. 获取数据:案例中是直接给定的
2. 数据预处理: 脏数据清洗、数据格式转换、数据源Dataset的构造、数据迭代器Dataloader的构造
3. 模型搭建: 编码器和解码器等一系列模型
4. 模型评估
5. 模型上线

基础准备

import random
import re
import torch
import torch.nn as nn
from torch.utils.data import Dataset,DataLoader
from tqdm import tqdm
import matplotlib.pyplot as plt

# 1- 设置变量
# 运行设备
device = ("cuda" if torch.cuda.is_available() else "cpu")   # Windows的写法
# device = "mps"  # Mac的写法

# 数据文件路径
data_path = "data/eng-fra-v2.txt"

# 句子开始标识
SOS_TOKEN = 0

# 句子结束标识
EOS_TOKEN = 1

# 句子长度规范:长度的上限
MAX_LENGTH = 10

数据预处理

定义样本清洗函数和构建字典

  • 目的
样本清洗函数: 将脏数据进行清洗,以免影响模型训练
构建字典:一方面是为了将文本进行数字表示,还有一方面进行解码的时候将预测索引数字映射为真实的文本
  • 样本清洗函数代码实现
# 2- 数据清洗
def normalizeString(line):
    # 将文本全部转小写
    line = line.lower().strip()
    # print(f"-{line}-")

    # 在指定的标点符号的前面增加空格
    line = re.sub(pattern=r"([.!?])",repl=r" \1",string=line)

    # 去除除了26个小写、.!?以外的其他符号,全部替换成空格
    line = re.sub(pattern=r"[^a-z.!?]+", repl=r" ", string=line)

    return line
  • 构建字典代码实现
# 3- 构建词汇表
def getdata():
    # 1- 读取文件
    with open(data_path,encoding="UTF-8",mode="r") as f:
        eng_fra_list = f.readlines()

    # 2- 划分得到英语和法语
    # eng_fra_pairs结构:[[英语句子1, 法语句子1], [英语句子2, 法语句子2]...]
    eng_fra_pairs = []
    for eng_fra_line in eng_fra_list:

        tmp_list = []
        for line in eng_fra_line.split("\t"):
            tmp_list.append(normalizeString(line))

        eng_fra_pairs.append(tmp_list)

    # 3- 分别得到英语词汇表和法语词汇表
    english_word2index = {"SOS":SOS_TOKEN,"EOS":EOS_TOKEN}
    english_word_cnt = 2

    franch_word2index = {"SOS":SOS_TOKEN,"EOS":EOS_TOKEN}
    franch_word_cnt = 2

    for eng_fra_list in eng_fra_pairs:
        # 英语句子处理:分词->去重->添加
        for eng_word in eng_fra_list[0].split(" "):
            if eng_word not in english_word2index:
                english_word2index[eng_word] = english_word_cnt
                english_word_cnt += 1

        # 法语句子处理:分词->去重->添加
        for fra_word in eng_fra_list[1].split(" "):
            if fra_word not in franch_word2index:
                franch_word2index[fra_word] = franch_word_cnt
                franch_word_cnt += 1

    # 4- 将法语字典中key和value的位置交换
    franch_index2word = {index:word for word,index in franch_word2index.items()}

    return english_word2index,english_word_cnt,franch_word2index,franch_word_cnt,eng_fra_pairs,franch_index2word

english_word2index,english_word_cnt,franch_word2index,franch_word_cnt,eng_fra_pairs,franch_index2word = getdata()

构建DataSet

  • 目的
使用Pytorch框架,一般遵从一个规矩:使用DataSet方法构造数据源,来让模型进行使用
构造数据源的过程中:必须继承torch.utils.data.Dataset类,必须构造两个魔法方法:__len__(), __getitem__()
__len__(): 一般返回的是样本的总个数,我们可以直接len(dataset对象)直接就可以获得结果
__getitem__(): 可以根据某个索引取出样本值,我们可以直接用dataset对象[index]来直接获得结果
  • 代码实现
# 4- 自定义Dataset
class MyPairsDataset(Dataset):
    def __init__(self,eng_fra_pairs):
        self.eng_fra_pairs = eng_fra_pairs
        self.sample_cnt = len(eng_fra_pairs)    # 样本条数

    def __len__(self):
        return self.sample_cnt

    def __getitem__(self, index):
        # 1- 防止索引为负数;防止索引越界
        index = min(max(index,0), self.sample_cnt - 1)

        # 2- 获得英语句子(特征数据)和法语句子(目标值)
        # 2.1- 获得句子。[英语句子1, 法语句子1]
        eng_fra_pair = self.eng_fra_pairs[index]

        # 句子 -> 分词 -> 变成索引
        """
            为什么只在句子的末尾增加结束标识EOS_TOKEN,而不增加SOS_TOKEN?
            答:
                1- Seq2Seq架构要求必须要有EOS_TOKEN,而SOS_TOKEN可选的
                2- SOS_TOKEN会在Decoder中进行数据处理的时候再增加
                3- Decoder端我们只能通过添加SOS_TOKEN告诉解码器开始进行翻译的工作;以及通过添加EOS_TOKEN告诉解码器翻译工作结束
        """
        # 2.2- 处理英语句子
        x = [english_word2index[word] for word in eng_fra_pair[0].split(" ")]
        # 所有的张量数据必须放在同一个硬件设备上,要么同时在GPU,要么同时在CPU
        x_tensor = torch.tensor(x,dtype=torch.long,device=device)
        # print(f"原始英语句子{eng_fra_pair[0]},词索引向量{x_tensor}")

        # 2.3- 处理法语句子
        y = [franch_word2index[word] for word in eng_fra_pair[1].split(" ")]
        y.append(EOS_TOKEN)
        y_tensor = torch.tensor(y, dtype=torch.long, device=device)
        # print(f"原始法语句子{eng_fra_pair[1]},词索引向量{y_tensor}")

        return x_tensor,y_tensor

构建Dataloader

  • 目的
为了将Dataset我们上一步构建的数据源,进行再次封装,变成一个迭代器,可以进行for循环,而且,可以自动为我们dataset里面的数据进行增维(bath_size),也可以随机打乱我们的取值顺序
  • 代码实现
# 5- 创建DataLoader
def get_dataloader():
    # 1- 创建自定义Dataset实例对象
    dataset = MyPairsDataset(eng_fra_pairs)

    # 2- 创建数据加载器
    dataloader = DataLoader(dataset=dataset,batch_size=1,shuffle=True)

    return dataloader

模型搭建

搭建编码器GRU模型

image-20260104173513421

  • 代码实现
# 6- 无注意力机制的编码器
class Encoder(nn.Module):
    def __init__(self,eng_vocab_size,input_size,hidden_size):
        """
        :param eng_vocab_size: 英语词汇表大小
        :param input_size: 词向量维度
        :param hidden_size: 隐藏状态向量维度
        """

        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.eng_vocab_size = eng_vocab_size
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 3- 搭建网络结构
        # 3.1- 词嵌入层
        self.ebd = nn.Embedding(num_embeddings=self.eng_vocab_size,embedding_dim=self.input_size)

        # 3.2- 循环网络层
        self.gru = nn.GRU(input_size=self.input_size,hidden_size=self.hidden_size,num_layers=1,batch_first=True)

    def forward(self,input,hidden):
        """
        Encoder端的前向传播:负责理解英语句子的语义信息
        :param input: 本次输入数据,张量形状:[batch_size每个批次中英语句子的条数,seq_len英语句子中词的个数]
        :param hidden: 上一个时间步的隐藏状态,张量形状:[num_layers,batch_size,hidden_size]
        :return:
        """
        # 1- 调用词嵌入层
        # embed形状:[batch_size每个批次中英语句子的条数,seq_len英语句子中词的个数,input_size词向量维度]
        # print(f"1--input-->{input.shape}")
        embed = self.ebd(input)
        # print(f"2--embed-->{embed.shape}")

        # 2- 调用GRU
        output,hidden = self.gru(embed,hidden)

        return output,hidden

    def init_hidden(self):
        # 隐藏状态张量形状:[num_layers,batch_size,hidden_size]
        # num_layers:目前手动指定为1层
        # batch_size:因为我们没有对英语句子进行长度规范的处理,因此只能设置为1
        return torch.zeros(size=(1,1,self.hidden_size)).to(device=device)
  • 编码器模型测试
# 测试编码器
def use_encoder():
    # 1- 准备数据
    dataloader = get_dataloader()

    # 2- 创建编码器对象
    my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256)
    my_encoder.to(device=device)

    # 3- 遍历数据,前向传播
    for x,y in dataloader:
        # 3.1- 初始隐藏状态
        hidden = my_encoder.init_hidden()

        # 3.2- 前向传播
        output,hidden = my_encoder(x,hidden)

        print(f"output-->{output.shape}")
        print(f"hidden-->{hidden.shape}")

        break

搭建解码器无Attention模型

  • 流程

image-20260403145410955

  • output[-1]和output[0]的区别
import torch

if __name__ == '__main__':
    # 3个词,2条句子,4隐藏状态向量维度
    output = torch.randn(size=(3,2,4))
    print(output)

    print("-"*30)

    output_1 = output[:,-1,:]
    print(output_1)
    print(output_1.shape) # [3,4]

    print("-" * 30)

    output_0 = output[:,0,:]
    print(output_0)
    print(output_0.shape) # [3,4]
  • 代码实现
# 7- 解码器:无注意力机制
class Decoder(nn.Module):
    def __init__(self,fra_vocab_size,hidden_size):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.fra_vocab_size = fra_vocab_size
        self.hidden_size = hidden_size

        # 3- 搭建网络结构
        # 3.1- 词嵌入层
        self.ebd = nn.Embedding(num_embeddings=self.fra_vocab_size,embedding_dim=self.hidden_size)

        # 3.2- GRU层
        self.gru = nn.GRU(input_size=self.hidden_size,hidden_size=self.hidden_size,num_layers=1,batch_first=True)

        # 3.3- 输出层
        self.output_linear = nn.Linear(in_features=self.hidden_size,out_features=self.fra_vocab_size)

        # 4- 输出层的激活函数
        # CrossEntropyLoss = LogSoftmax + NLLLoss
        self.log_softmax = nn.LogSoftmax(dim=-1)

    def forward(self,input,hidden):
        """
        Decoder前向传播:负责翻译得到法语词
        :param input: 本次输入数据,每次只送一个法语词进来,张量形状:[batch_size,seq_len],[[一个法语词的词索引]]
        :param hidden: 上一个时间步的隐藏状态,张量形状:[num_layers,batch_size,hidden_size]
        :return:
        """

        # 1- 调用词嵌入层
        embed = torch.relu(self.ebd(input))

        # 2- 调用GRU
        output,hidden = self.gru(embed,hidden)
        print(f"1-->output-->{output.shape}")

        # 3- 调用输出层
        # 3.1- 将output由3维变成2维,[batch_size,seq_len,hidden_size] -> [batch_size,hidden_size]
        """
            output[:,-1,:] 针对文本分类的业务场景。-1表示取最后一个时间步的隐藏状态,文本分类需要对一整条句子的完整信息理解完以后再做分类
            output[:,0,:]  针对文本翻译的业务场景。
        """
        output = output[:,0,:]
        print(f"2-->output[:,0,:]-->{output.shape}")

        # 3.2- 调用
        output = self.log_softmax(self.output_linear(output))
        print(f"3-->output-->{output.shape}")

        return output,hidden

    def init_hidden(self):
        return torch.zeros(size=(1,1,self.hidden_size)).to(device=device)
  • 代码测试
# 测试解码器
def use_decoder():
    # 1- 准备数据
    dataloader = get_dataloader()

    # 2- 创建模型实例对象
    my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256).to(device=device)
    my_decoder = Decoder(fra_vocab_size=franch_word_cnt,hidden_size=256).to(device=device)

    # 3- 前向传播
    for x,y in dataloader:
        # x:一条英语句子,[[词索引1, 词索引2,... ]]
        # y:一条法语句子,[[词索引1, 词索引2,... ]]

        # 3.1- 调用编码器:让模型理解英语句子的完整含义
        h0 = my_encoder.init_hidden()
        encoder_output,hidden = my_encoder(x,h0)

        # 3.2- 调用解码器:每次送入一个词
        for index in range(y.shape[1]):
            # 3.3- 解码器前向传播
            fra_word = y[0][index].reshape(1,-1)
            decoder_output,hidden= my_decoder(fra_word, hidden)

            print(f"法语词词索引{fra_word},隐藏状态信息{hidden.shape}")

        break

搭建解码器带Attention模型

  • 流程

image-20260403165600727

为什么这里的K来自【解码器上一个时间步隐藏状态】、Q来自【解码器上一个时间步预测结果】,与之前的又不一样?
答:
    1- 这是注意力的另外一种实现方式。该方式可以使用在文本翻译业务场景中
    2- Q、K的这种来源与前面【注意力计算规则】中不一样。但是这种形式可以计算得到注意力。这种机制的核心思想是:在生成当前词时,模型不仅要看“我上一步想到什么”(解码器状态),还要看“我现在要处理什么输入”(当前输入词),然后综合这两者去决定“我该关注编码器的哪个部分”。
    3- 这种设计在Seq2Seq机器翻译模型中非常经典,能同时考虑解码器状态和当前输入词的语义。
  • 代码实现
# 8- 解码器:有注意力机制
class AttenDecoder(nn.Module):
    def __init__(self,fra_vocab_size,hidden_size):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.fra_vocab_size = fra_vocab_size
        self.hidden_size = hidden_size      # 有两个作用:1- 词向量维度;2- 隐藏状态向量维度

        # 3- 搭建网络结构
        # 3.1- 词嵌入层
        self.embedding = nn.Embedding(num_embeddings=self.fra_vocab_size,embedding_dim=self.hidden_size)

        # 3.2- Dropout随机失活层
        self.dropout = nn.Dropout(p=0.2)

        # 3.3- 线性层:用来计算QK的相似性
        self.attn = nn.Linear(in_features=self.hidden_size+self.hidden_size,out_features=MAX_LENGTH)

        # 3.4- 线性层:对Q和C拼接后的张量形状进行调整
        self.attn_combine = nn.Linear(in_features=self.hidden_size+self.hidden_size,out_features=self.hidden_size)

        # 3.5- GRU循环网络层
        self.gru = nn.GRU(input_size=self.hidden_size,hidden_size=self.hidden_size,num_layers=1,batch_first=True)

        # 3.6- 输出层
        self.out = nn.Linear(in_features=self.hidden_size,out_features=self.fra_vocab_size)

        # 激活函数
        self.log_softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, prev_hidden, encoder_outputs):
        """
        :param input: 解码器本次输入数据,也就是Q。张量形状 [batch_size,seq_len],每次只传一个词进来
        :param prev_hidden: 解码器上一个时间步的隐藏状态,也就是K。张量形状 [num_layers,batch_size,hidden_size]
        :param encoder_outputs: 编码器端所有时间步隐藏状态的拼接,也就是V,而且还做了句子长度规范的处理。张量形状 [batch_size,MAX_LENGTH,hidden_size]
        :return:
        """

        # 1- 调用词嵌入层
        embedded = self.dropout(self.embedding(input))

        # 2- 计算注意力机制
        # 2.1- Q和K拼接
        # embedded就是Q
        # 如果不进行张量形状调整,那么就是num_layers与batch_size进行拼接,业务层面是不合理。
        # 调整K的形状 由 [num_layers,batch_size,hidden_size] -> [batch_size,num_layers,hidden_size]
        K = prev_hidden.transpose(dim0=0,dim1=1)
        # 拼接
        qk_cat = torch.cat(tensors=(embedded,K), dim=-1)

        # 2.2- 通过线性层算QK的相似性
        score = self.attn(qk_cat)

        # 2.3- 相似性转成权重
        attn_weights = torch.softmax(score, dim=-1)

        # 2.4- 得到专属信息包C:权重矩阵和V进行矩阵乘法运算
        # C对应原理图中的attn_applied
        C = torch.bmm(attn_weights, encoder_outputs)

        # 2.5- Q和C拼接
        qc_cat = torch.cat(tensors=(embedded,C),dim=-1)

        # 2.6- 通过线性层调整QC拼接后的张量
        qc_output = torch.relu(self.attn_combine(qc_cat))

        # 3- 调用GRU
        output,hidden = self.gru(qc_output,prev_hidden)

        # 4- 调用输出层
        output = output[:,0,:]
        output = self.log_softmax(self.out(output))

        return output,hidden,attn_weights

    def init_hidden(self):
        return torch.zeros(size=(1,1,self.hidden_size)).to(device=device)

模型训练

  • 基本过程
整体流程:
    1. 获取数据集加载器对象
    2. 模型初始化
    3. 优化器初始化
    4. 损失函数初始化. 使用 NLLLoss() 负数对数似然损失
    5. 训练参数初始化
    6. 循环训练
        6.1 外层循环, 控制训练轮数
        6.2 内层循环, 遍历数据集的每个样本(即: 每轮具体的训练过程)
            具体训练过程看下面的步骤
        6.3 打印训练日志 (每1000个样本打印一次)
        6.4 记录损失用于绘图(每100个样本记录一次)
    7. 保存模型
    8. 训练结束后, 绘制损失曲线
    9. 训练结束, 返回结果

------------------------------------------------

具体训练过程(针对单条样本):
    1. 编码阶段, 将输入的序列转换为上下文向量
    2. 解码参数准备
        2.1 构建编码器的输出张量, 用于注意力计算, 形状为: [10, 256]
        2.2 解码器初始化隐藏状态
        2.3 解码器的初始输入
    3. 初始化损失值
    4. 根据概率值决定是否用 Teacher forcing
        4.1 teacher forcing模型: 使用真实标签作为下一步的输入
        4.2 非Teacherforcing模型: 使用上一时间步的预测结果作为下一步的输入
    5. 反向传播 和 参数更新
    6. 返回平均损失
  • 代码实现
# 9.2- 整体训练过程
def train():
    # 1- 准备数据
    dataloader = get_dataloader()

    # 2- 创建类的实例对象
    # 2.1- 模型实例对象
    my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256).to(device=device)
    my_decoder = AttenDecoder(fra_vocab_size=franch_word_cnt,hidden_size=256).to(device=device)
    # 2.2- 优化器实例对象:与具体的实例对象有关,因此要创建单独的两个
    encoder_adam = torch.optim.Adam(params=my_encoder.parameters(),lr=1e-4)
    decoder_adam = torch.optim.Adam(params=my_decoder.parameters(),lr=1e-4)
    # 2.3- 损失函数对象:与具体的实例对象无关,内部只是通过预测和真实值计算差值而已,因此只需要创建一个
    loss = nn.NLLLoss()

    # 3- 设置模式
    my_encoder.train()
    my_decoder.train()

    # 4- 模型训练
    epochs = 1

    avg_loss_list = []  # 用来存储平均损失

    for epoch in range(epochs):
        total_loss_value = 0.0  # 总损失值

        for i,(x_train,y_train) in enumerate(tqdm(dataloader), start=1):
            # 1- 单次训练得到损失值
            loss_value = train_iter(x_train,y_train,my_encoder,my_decoder,encoder_adam,decoder_adam,loss)

            # 2- 更新统计指标
            total_loss_value += loss_value

            # 3- 每隔100个批次记录并且输出一次信息
            if i%100==0:
                # 计算平均损失值
                avg_loss = total_loss_value / i

                avg_loss_list.append(avg_loss)
                print(f"第{epoch+1}轮次,平均损失{avg_loss}")

    # 5- 保存训练好的模型
    torch.save(my_encoder.state_dict(), "model/encoder.pkl")
    torch.save(my_decoder.state_dict(), "model/decoder.pkl")

    # 6- 绘制损失值变化曲线
    plt.plot(avg_loss_list)
    plt.show()
  • 模型训练内部迭代函数代码实现
# 9.1- 单次训练过程
def train_iter(x,y,my_encoder,my_decoder,encoder_adam,decoder_adam,loss):
    """
    单次样本数据的训练过程
    :param x: 英语句子,张量形状[batch_size,seq_len]
    :param y: 法语句子,张量形状[batch_size,seq_len]
    :param my_encoder: 编码器对象
    :param my_decoder: 解码器对象
    :param encoder_adam: 编码器的优化器
    :param decoder_adam: 解码器的优化器
    :param loss: 损失函数对象
    :return: 平均损失值
    """

    # 1- 调用编码器
    encoder_hidden = my_encoder.init_hidden()
    encoder_output,encoder_hidden = my_encoder(x,encoder_hidden)

    # 2- 调用解码器之前的准备工作
    # 2.1- 句子长度规范化处理,同时也是合并得到V
    # 初始化一个形状[1,10,256]的全0张量
    V = torch.zeros(size=(1,MAX_LENGTH,256), device=device)
    copy_seq_len = min(MAX_LENGTH, x.shape[1])
    V[:, :copy_seq_len, :] = encoder_output[:, :copy_seq_len, :]

    # 2.2- 准备变量
    # 将编码器端最后一个时间步的隐藏状态作为 初始隐藏状态 给到解码器使用
    decoder_hidden = encoder_hidden
    # 损失值
    loss_value = 0.0
    # 获得法语句子中词的个数
    y_seq_len = y.shape[1]
    # 存储法语翻译结果,而且上个时间步的预测结果会作为本次输入数据使用,这称之为自回归
    input_y = torch.tensor([[SOS_TOKEN]], device=device)
    # 教师机制的标识。True:使用教师机制,也就是告诉下个时间步【真实】的法语词内容。反之,把【预测】结果给到下个时间步
    teacher_forcing_flag = True if random.random()<0.5 else False

    # 3- 调用解码器
    if teacher_forcing_flag:
        # 使用教师机制,也就是告诉下个时间步【真实】的法语词内容
        for i in range(y_seq_len):
            # 3.1- 前向传播
            pred_output,decoder_hidden,attn_weights = my_decoder(input=input_y, prev_hidden=decoder_hidden, encoder_outputs=V)

            # 3.2- 计算损失值
            # print(f"y-->{y.shape}")
            # print(f"y[0]-->{y[0].shape}")
            # print(f"y[0][i]-->{y[0][i]}")

            y_true = y[0][i].reshape(1)

            # print(f"y_true-->{y_true.shape}")

            loss_value += loss(pred_output,y_true)

            # 3.3- 告诉下个时间步【真实】的法语词内容
            input_y = y[0][i].reshape(1,-1) # [batch_size,seq_len]

    else:
        # 普通训练机制,把【预测】结果给到下个时间步
        for i in range(y_seq_len):
            # 3.1- 前向传播
            pred_output, decoder_hidden, attn_weights = my_decoder(input=input_y, prev_hidden=decoder_hidden,
                                                                   encoder_outputs=V)

            # 3.2- 计算损失值
            y_true = y[0][i].reshape(1)
            loss_value += loss(pred_output, y_true)

            # 3.3- 获得预测结果的词索引
            pred_word_index = torch.argmax(pred_output, dim=-1)

            # 3.4- 判断是否到了句子的结尾
            if pred_word_index == EOS_TOKEN:
                break

            # 3.5- 告诉下个时间步【预测】的法语词内容
            input_y = pred_word_index.reshape(1, -1)  # [batch_size,seq_len]

    # 4- 反向传播固定代码
    encoder_adam.zero_grad()
    decoder_adam.zero_grad()

    loss_value.sum().backward()

    encoder_adam.step()
    decoder_adam.step()

    return loss_value.item() / y_seq_len

模型预测

  • 基本过程
1.获取数据
2.数据预处理
3.实例化模型: 编码器和解码器
4.加载模型训练好的参数: model.load_state_dict(torch.load("model_path"))
5.with torch.no_grad():
6.将数据送入模型进行预测(注意:张量的形状变换)
  • 调用模型预测代码
# 10.2- 完整的预测流程
def use_seq2seq_evaluate():
    # 1- 手动准备未知数据
    my_samplepairs = [
        ['i m impressed with your french .', 'je suis impressionne par votre francais .'],
        ['i m more than a friend .', 'je suis plus qu une amie .'],
        ['she is beautiful like her mother .', 'elle est belle comme sa mere .']
    ]

    # 2- 加载训练好的模型
    my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256).to(device=device)
    my_encoder.load_state_dict(torch.load("model/encoder.pkl"))

    my_decoder = AttenDecoder(fra_vocab_size=franch_word_cnt,hidden_size=256).to(device=device)
    my_decoder.load_state_dict(torch.load("model/decoder.pkl"))

    # 3- 设置模式
    my_encoder.eval()
    my_decoder.eval()

    # 4- 预测
    for i,pair in enumerate(my_samplepairs):
        x = pair[0] # 英语句子
        y = pair[1] # 英语句子

        # 4.1- 英语句子转成张量
        x_list = [english_word2index[word] for word in x.split(" ")]
        # 所有的张量数据必须放在同一个硬件设备上,要么同时在GPU,要么同时在CPU
        x_tensor = torch.tensor(x_list, dtype=torch.long, device=device).reshape(1,-1)

        # 4.2- 对单条句子进行评估
        franch_word_list = seq2seq_evaluate(x_tensor,my_encoder,my_decoder)

        # 4.3- 输出结果
        print(f"英语句子 {x},真实法语句子 {y},翻译法语句子 {' '.join(franch_word_list)}")
  • 预测的核心代码
# 10.1- 单条句子的翻译
def seq2seq_evaluate(x_tensor,my_encoder,my_decoder):
    """
    单条句子的翻译
    :param x_tensor: 一条处理后的英语句子,形状[batch_size,seq_len]
    :param my_encoder: 编码器
    :param my_decoder: 解码器
    :return: 翻译的法语词列表
    """

    with torch.no_grad():
        # 1- 调用编码器
        encoder_output,encoder_hidden = my_encoder(x_tensor,my_encoder.init_hidden())

        # 2- 调用解码器之前的准备工作
        # 2.1- 句子长度规范化处理,同时也是合并得到V
        # 初始化一个形状[1,10,256]的全0张量
        V = torch.zeros(size=(1,MAX_LENGTH,256), device=device)
        copy_seq_len = min(MAX_LENGTH, x_tensor.shape[1])
        V[:, :copy_seq_len, :] = encoder_output[:, :copy_seq_len, :]

        # 2.2- 准备变量
        # 将编码器端最后一个时间步的隐藏状态作为 初始隐藏状态 给到解码器使用
        decoder_hidden = encoder_hidden
        # 存储单次的法语翻译结果,而且上个时间步的预测结果会作为本次输入数据使用,这称之为自回归
        input_y = torch.tensor([[SOS_TOKEN]], device=device)
        # 存储所有的翻译法语词
        franch_word_list = []

        # 3- 评估/预测
        for i in range(MAX_LENGTH):
            # 3.1- 前向传播:调用解码器
            decoder_output,decoder_hidden,attn_weights = my_decoder(input=input_y, prev_hidden=decoder_hidden, encoder_outputs=V)

            # 3.2- 获得预测概率最高的那个词的词索引
            pred_fra_index = torch.argmax(decoder_output, dim=-1).item()

            # 3.3- 判断是否遇到了句子的结束标识
            if pred_fra_index == EOS_TOKEN:
                franch_word_list.append("EOS")
                break
            else:
                # 通过索引获得法语词的内容
                fra_word = franch_index2word[pred_fra_index]
                franch_word_list.append(fra_word)

            input_y = torch.tensor([[pred_fra_index]], device=device)

    return franch_word_list
💻 代码

RNN 及其变体代码

7 个文件
  • 📁 01_RNN、LSTM、GRU/
    • 📄 01-RNN.py 1.6KB
      import torch
      import torch.nn as nn
      
      def rnn_demo(num_layers):
          # 1- 创建RNN循环网络层
          """
              参数解释:
                  input_size:输入数据的向量维度
                  hidden_size:隐藏状态向量维度
                  num_layers:隐藏层的层数
          """
          rnn = nn.RNN(input_size=4,hidden_size=5,num_layers=num_layers)
      
          # 2- 准备数据
          # 2.1- 本次输入数据
          input = torch.randn(size=(3,2,4))
          # 2.2- 上一个时间步的隐藏状态。
          h0 = torch.randn(size=(num_layers,2,5))
      
          # 3- 调用RNN
          """
              输入参数的张量形状:
                  本次输入数据input:[seq_len每条句子中词的个数, batch_size每个批次中样本的条数, input_size词向量维度]
                  上一个时间步的隐藏状态h0:[num_layers隐藏层的层数, batch_size每个批次中样本的条数, hidden_size隐藏状态向量维度]
              
              返回值的张量形状:
                  本次时间步的预测结果output:[seq_len每条句子中词的个数, batch_size每个批次中样本的条数, hidden_size隐藏状态向量维度]
                  本次更新后的隐藏状态hidden:[num_layers隐藏层的层数, batch_size每个批次中样本的条数, hidden_size隐藏状态向量维度]
          """
          output,hidden = rnn(input,h0)
      
          # 4- 打印结果
          print(f"output的形状-->{output.shape}")    # [3,2,5]
          print(f"hidden的形状-->{hidden.shape}")    # [num_layers,2,5]
      
          print(f"output的内容-->\n{output}")
          print(f"hidden的内容-->\n{hidden}")
      
      
      if __name__ == '__main__':
          # rnn_demo(num_layers=1)
          rnn_demo(num_layers=2)
      
    • 📄 02-LSTM.py 1.5KB
      import torch
      import torch.nn as nn
      
      if __name__ == '__main__':
          # 1- 创建LSTM网络结构
          """
              参数解释:
                  bidirectional:是否是双向的网络结构。默认是False,也就是单向的。如果设置为True,LSTM就变成Bi-LSTM
          """
          lstm = nn.LSTM(input_size=4,hidden_size=5,num_layers=1,bidirectional=False)
      
          # 2- 准备数据
          # input的形状:[seq_len句子中词的个数,batch_size每个批次句子的个数,input_size词向量的维度]
          input = torch.randn(size=(6,3,4))
      
          """
              因为最终的隐藏状态是根据细胞状态经过激活函数处理得到的,
              但是激活函数不会改变张量的形状,只会改变值
              因此:隐藏状态 和 细胞状态 的张量形状是相同的
              实际工作中,一般对隐藏状态 和 细胞状态全都使用全0初始化
          """
          h0 = torch.zeros(size=(1,3,5))
          c0 = torch.zeros(size=(1,3,5))
      
          # 3- 调用LSTM
          """
              output形状:[seq_len,batch_size,hidden_size]
              hidden形状:[num_layers,batch_size每个批次句子的个数,hidden_size]
          """
      
          # 注意:传递和接受结果,都需要将隐藏状态 和 细胞状态用元组包起来
          output,(hidden,c1) = lstm(input,(h0,c0))
      
          print(f"output形状-->{output.shape}") # 6,3,5
          print(f"hidden形状-->{hidden.shape}")
          print(f"c1形状-->{c1.shape}") # 1,3,5
      
          print(f"output-->{output}")
          print(f"hidden-->{hidden}")
          print(f"c1-->{c1}")
      
    • 📄 03-GRU.py 4.1KB
      import torch
      import torch.nn as nn
      
      def single_layer():
          # 1- 创建GRU循环网络层
          """
              参数解释:
                  input_size:输入数据的词向量维度
                  hidden_size:隐藏层中隐藏状态的向量维度
                  num_layers:隐藏层的层数。默认是1
                  bidirectional:默认是False,也就是单向的GRU。如果为True,就是双向的GRU
          """
          gru = nn.GRU(input_size=4, hidden_size=5, num_layers=1, bidirectional=False)
      
          # 2- 准备数据
          # 2.1- 本次的输入数据
          """
              张量形状要求:
                  [
                      seq_len每条句子中有多少个词,
                      batch_size每个批次中句子的条数有多少条,
                      input_size输入数据的词向量维度
                  ]
          """
          input = torch.randn(size=(6, 3, 4))
          # 2.2- 上一个时间步的隐藏状态。最初始的时候,一般进行全0初始化
          """
              张量形状要求:
                  [
                      num_layers隐藏层的层数,
                      batch_size每个批次中句子的条数有多少条,
                      hidden_size隐藏层中隐藏状态的向量维度
                  ]
          """
          h0 = torch.zeros(size=(1, 3, 5))
      
          # 3- 调用GRU
          """
              输出的output张量的形状:
                  [
                      seq_len每条句子中有多少个词,
                      batch_size每个批次中句子的条数有多少条,
                      hidden_size隐藏层中隐藏状态的向量维度
                  ]
      
              输出的hidden张量的形状:
                  [
                      num_layers隐藏层的层数,
                      batch_size每个批次中句子的条数有多少条,
                      hidden_size隐藏层中隐藏状态的向量维度
                  ]
          """
          output, hidden = gru(input, h0)
      
          # 4- 打印结果
          print(f"output形状-->{output.shape}")  # 6,3,5
          print(f"hidden形状-->{hidden.shape}")  # 2,3,5
      
          print(f"output-->{output}")
          print(f"hidden-->{hidden}")
      
      def double_layer():
          # 1- 创建GRU循环网络层
          """
              参数解释:
                  input_size:输入数据的词向量维度
                  hidden_size:隐藏层中隐藏状态的向量维度
                  num_layers:隐藏层的层数。默认是1
                  bidirectional:默认是False,也就是单向的GRU。如果为True,就是双向的GRU
          """
          gru = nn.GRU(input_size=4, hidden_size=5, num_layers=1, bidirectional=True)
      
          # 2- 准备数据
          # 2.1- 本次的输入数据
          """
              张量形状要求:
                  [
                      seq_len每条句子中有多少个词,
                      batch_size每个批次中句子的条数有多少条,
                      input_size输入数据的词向量维度
                  ]
          """
          input = torch.randn(size=(6, 3, 4))
          # 2.2- 上一个时间步的隐藏状态。最初始的时候,一般进行全0初始化
          """
              张量形状要求:
                  [
                      num_layers隐藏层的层数,
                      batch_size每个批次中句子的条数有多少条,
                      hidden_size隐藏层中隐藏状态的向量维度
                  ]
          """
          h0 = torch.zeros(size=(2, 3, 5))
      
          # 3- 调用GRU
          """
              输出的output张量的形状:
                  [
                      seq_len每条句子中有多少个词,
                      batch_size每个批次中句子的条数有多少条,
                      hidden_size隐藏层中隐藏状态的向量维度
                  ]
      
              输出的hidden张量的形状:
                  [
                      num_layers隐藏层的层数,
                      batch_size每个批次中句子的条数有多少条,
                      hidden_size隐藏层中隐藏状态的向量维度
                  ]
          """
          output, hidden = gru(input, h0)
      
          # 4- 打印结果
          print(f"output形状-->{output.shape}")  # 6,3,5
          print(f"hidden形状-->{hidden.shape}")  # 2,3,5
      
          print(f"output-->{output}")
          print(f"hidden-->{hidden}")
      
      if __name__ == '__main__':
          # 单向GRU
          single_layer()
      
          # 双向GRU
          double_layer()
      
    • 📄 特殊代码演示.py 2.5KB
      # 演示:list( map(lambda x: len(x), train_data['sentence']))
      def demo01():
          my_list = ["hello","abc","heimaaaa"]
      
          # 返回的实际是一个生成器对象。优点:节约内存
          # map的作用是:将my_list容器中的元素一个个遍历出来,作为参数传递给到前面的自定义函数或lambda表达式
          map_result = map(lambda x:len(x), my_list)
          # print(type(map_result))
          # print(map_result)
      
          # 取值方式一:推荐,转成list列表
          # list_result = list(map_result)
          # 综合之后的代码版本
          list_result = list(map(lambda x:len(x), my_list))
          print(list_result)
      
          # 取值方式二:for循环
          # for i in map_result:
          #     print(i)
      
          # 取值方式三:next取值
          # print(next(map_result))
          # print(next(map_result))
          # print(next(map_result))
      
      # 演示:list(chain(*map(lambda x: get_a_list(x) , p_train_data)))
      import jieba
      from itertools import chain
      def demo02():
          sen_list = ["今天天气很好","元旦放两天"]
          map_result = map(lambda line:jieba.lcut(line), sen_list)
      
          # for i in map_result:
          #     print(i)
      
          # print(*map_result)
      
          # 总结:chain的作用等价于List.extend,优点是节约内存
          chain_result = chain(*map_result)
          print(type(chain_result))
          print(chain_result)
      
          print(list(chain_result))
      
          # 最终版代码
          final_result = list(chain(*map(lambda line:jieba.lcut(line), sen_list)))
          print(final_result)
      
      # 演示:zip函数
      def demo03():
          list_1 = [11,22,33,44,55]
          list_2 = ["hello","python","java"]
      
          zip_result_1 = list(zip(list_1, list_2))
          print(zip_result_1)
      
          print("-"*30)
      
          list_1 = ["hello", "python", "java"]
          list_2 = [11, 22, 33, 44, 55]
      
          zip_result_2 = list(zip(list_1, list_2))
          print(zip_result_2)
      
      import torch
      import torch.nn as nn
      def demo04():
          # 1- 准备线性求和结果数据
          data = torch.tensor([5.6, 8.7, -3.4])
      
          # 2- 调用LogSoftmax:线性求和结果 -> Softmax概率值 -> 以e为底的幂指数
          log_softmax = nn.LogSoftmax(dim=-1)
          log_softmax_result = log_softmax(data)
          print(log_softmax_result)
      
          # 3- 调用Softmax:线性求和结果 -> Softmax概率值
          softmax = nn.Softmax(dim=-1)
          softmax_result = softmax(data)
          print(softmax_result)
      
          # 4- 写代码验证
          print(torch.log(softmax_result))
      
      if __name__ == '__main__':
          # demo01()
          # demo02()
          # demo03()
      
          # LogSoftmax和Softmax的区别
          demo04()
      
  • 📁 02_注意力机制/
    • 📄 01-bmm函数.py 1.1KB
      import torch
      
      """
          bmm和matmul对比总结
              1- 都能够实现矩阵乘法运算。matmul是串行计算,效率低;bmm是并行计算,效率高
              2- 如果matmul的张量形状不等,会进行广播机制
              3- bmm的要求如下:
                  3.1- 参与运算的两个张量的形状有且只能是三维。形状的含义(batch_size,行数,列数)
                  3.2- 参与运算的两个张量的batch_size必须相等
                  3.3- 参与运算的两个张量中,前一个张量的列数 与 后一个张量的行数 必须相同
                  
          后面在计算注意力的时候,如果满足bmm的要求,推荐使用bmm;当然可以无脑使用matmul,因为兼容性更好
      """
      if __name__ == '__main__':
          # 准备数据
          mat_1 = torch.randint(low=1,high=5,size=(10,3,4))
          # mat_1 = torch.randint(low=1,high=5,size=(1,3,4))
          mat_2 = torch.randint(low=1,high=5,size=(10,4,5))
      
          # matmul
          matmul_result = mat_1 @ mat_2
          print("matmul_result:",matmul_result.shape)
      
          # bmm
          bmm_result = torch.bmm(mat_1,mat_2)
          print("bmm_result:", bmm_result.shape)
      
    • 📄 02-Pytorch实现注意力的计算.py 4.8KB
      import torch
      import torch.nn as nn
      from torch import Tensor
      
      
      class MyAtten(nn.Module):
          def __init__(self,query_size,key_size,c_size,output_size):
              """
              :param query_size: 张量Q最后一个维度的形状,目前这里就是8
              :param key_size: 张量K最后一个维度的形状,目前这里就是8
              :param c_size: 张量C最后一个维度的形状,目前这里就是8
              :param output_size: Q和C拼接后,并且经过线性调整形状后的张量中最后一个维度的形状,目前这里就是8
              """
      
              # 1- 初始化父类
              super().__init__()
      
              # 2- 设置属性值
              self.query_size = query_size
              self.key_size = key_size
              self.c_size = c_size
              self.output_size = output_size
      
              # 3- 搭建网络结构
              # 3.1- 计算QK相似性的线性层。对应原理图中的第2步
              self.score_linear = nn.Linear(in_features=self.query_size+self.key_size,out_features=1)
              # self.score_linear = nn.Linear(in_features=16,out_features=1)
      
              # 3.2- 调整Q和C拼接后的张量形状线性层。对应原理图中的第6步
              self.qc_linear = nn.Linear(in_features=self.query_size+self.c_size,out_features=self.output_size)
              # self.qc_linear = nn.Linear(in_features=16,out_features=8)
      
          def forward(self,Q:Tensor,K,V):
              """
              目前暂时不研究QKV如何来的。只计算数据准备好后的注意力的计算过程。对应原理图中中间的绿色框
              :param Q: query,张量形状[batch_size,num_layers,hidden_size] -> [2,1,8]
              :param K: key,张量形状[batch_size,seq_len,hidden_size] -> [2,5,8]
              :param V: value,与key完全一致,张量形状[batch_size,seq_len,hidden_size] -> [2,5,8]
              :return:
              """
              # 1- Q和K拼接
              # 1.1- Q和K目前不能直接拼接,需要将Q的形状 由 [2,1,8] -> [2,5,8]
              seq_len = K.shape[1]
              # expand:扩大张量的形状。-1表示调整前后对应位置的形状不变
              Q_expand = Q.expand(size=(-1,seq_len,-1))
              print(f"1-->{Q.shape}") # [2,1,8]
              print(f"2-->{Q_expand.shape}") # [2,5,8]
      
              # 1.2- 拼接:Q_expand [2,5,8] K [2,5,8] -> [2,5,16]
              qk_cat = torch.cat(tensors=(Q_expand,K), dim=-1)
              print(f"3-->{qk_cat.shape}") # [2,5,16]
      
              # 2- 使用线性层对QK拼接后的结果计算相似性
              score = self.score_linear(qk_cat)
              print(f"4-->{score.shape}") # [2,5,1]
      
              # 3- 将相似性转成权重矩阵
              # 3.1- 调整score的形状:由[2,5,1] -> [2,1,5]
              score = score.transpose(dim0=1,dim1=2)
              print(f"5-->{score.shape}") # [2,1,5]
      
              # 3.2- 调用Softmax
              weighted_mat = torch.softmax(score, dim=-1)
              print(f"6-->{weighted_mat.shape}") # [2,1,5]
      
              # 4- 加权求和:权重矩阵和V进行矩阵乘法。计算得到专属信息包
              # weighted_mat [2,1,5];V [2,5,8] -> [2,1,8]
              C = torch.bmm(weighted_mat, V)
              print(f"7-->{C.shape}") # [2,1,8]
      
              # 5- Q和C拼接:Q [2,1,8];C [2,1,8] -> [2,1,16]
              qc_cat = torch.cat(tensors=(Q,C), dim=-1)
              print(f"8-->{qc_cat.shape}") # [2,1,16]
      
              # 6- 对拼接后的张量进行形状调整,以满足Decoder端GRU输入数据的要求
              result = self.qc_linear(qc_cat)
              print(f"9-->{result.shape}") # [2,1,8]
      
              return result,weighted_mat
      
      if __name__ == '__main__':
          # 因为只讲PyTorch中注意力是如何计算得到。因此拼接后的value、Q、V我们直接手动初始化
      
          # 1- 定义基础变量
          batch_size = 2      # 每个批次中句子的条数
          seq_len = 5         # 每个句子中词的个数
          hidden_size = 8     # 我们人为设置词向量维度和隐藏向量维度相同
          num_layers = 1      # 隐藏层层数为1
      
          # 2- 初始化Q、K、V
          """
              为什么Q的形状中间是1?
              答:当前只针对Decoder解码器端一个时间步。当前时刻拿一个Q,去和Encoder全部位置算相似性
          """
          Q = torch.randn(size=(batch_size,num_layers,hidden_size))   # Decoder端当前时间步的隐藏状态
          K = torch.randn(size=(batch_size,seq_len,hidden_size))      # Encoder端多个词的隐藏状态拼接后的
          V = torch.randn(size=(batch_size,seq_len,hidden_size))      # Encoder端多个词的隐藏状态拼接后的
      
          # 3- 计算注意力
          query_size = hidden_size
          key_size = hidden_size
          c_size = hidden_size
          output_size = hidden_size
      
          attn_model = MyAtten(query_size,key_size,c_size,output_size)
          output,attn_weight = attn_model(Q,K,V)
      
          print(f"权重值{attn_weight}")
          print(f"权重值和{attn_weight.sum()}")
      
  • 📁 03_案例:英译法/
    • 📄 英译法案例.py 23.4KB
      import random
      import re
      import torch
      import torch.nn as nn
      from torch.utils.data import Dataset,DataLoader
      from tqdm import tqdm
      import matplotlib.pyplot as plt
      
      # 1- 设置变量
      # 运行设备
      device = ("cuda" if torch.cuda.is_available() else "cpu")   # Windows的写法
      # device = "mps"  # Mac的写法
      
      # 数据文件路径
      data_path = "../data/eng-fra-v2.txt"
      
      # 句子开始标识
      SOS_TOKEN = 0
      
      # 句子结束标识
      EOS_TOKEN = 1
      
      # 句子长度规范:长度的上限
      MAX_LENGTH = 10
      
      # 2- 数据清洗
      def normalizeString(line):
          # 将文本全部转小写
          line = line.lower().strip()
          # print(f"-{line}-")
      
          # 在指定的标点符号的前面增加空格
          line = re.sub(pattern=r"([.!?])",repl=r" \1",string=line)
      
          # 去除除了26个小写、.!?以外的其他符号,全部替换成空格
          line = re.sub(pattern=r"[^a-z.!?]+", repl=r" ", string=line)
      
          return line
      
      # 3- 构建词汇表
      def getdata():
          # 1- 读取文件
          with open(data_path,encoding="UTF-8",mode="r") as f:
              eng_fra_list = f.readlines()
      
          # 2- 划分得到英语和法语
          # eng_fra_pairs结构:[[英语句子1, 法语句子1], [英语句子2, 法语句子2]...]
          eng_fra_pairs = []
          for eng_fra_line in eng_fra_list:
      
              tmp_list = []
              for line in eng_fra_line.split("\t"):
                  tmp_list.append(normalizeString(line))
      
              eng_fra_pairs.append(tmp_list)
      
          # 3- 分别得到英语词汇表和法语词汇表
          english_word2index = {"SOS":SOS_TOKEN,"EOS":EOS_TOKEN}
          english_word_cnt = 2
      
          franch_word2index = {"SOS":SOS_TOKEN,"EOS":EOS_TOKEN}
          franch_word_cnt = 2
      
          for eng_fra_list in eng_fra_pairs:
              # 英语句子处理:分词->去重->添加
              for eng_word in eng_fra_list[0].split(" "):
                  if eng_word not in english_word2index:
                      english_word2index[eng_word] = english_word_cnt
                      english_word_cnt += 1
      
              # 法语句子处理:分词->去重->添加
              for fra_word in eng_fra_list[1].split(" "):
                  if fra_word not in franch_word2index:
                      franch_word2index[fra_word] = franch_word_cnt
                      franch_word_cnt += 1
      
          # 4- 将法语字典中key和value的位置交换
          franch_index2word = {index:word for word,index in franch_word2index.items()}
      
          return english_word2index,english_word_cnt,franch_word2index,franch_word_cnt,eng_fra_pairs,franch_index2word
      
      english_word2index,english_word_cnt,franch_word2index,franch_word_cnt,eng_fra_pairs,franch_index2word = getdata()
      
      # 4- 自定义Dataset
      class MyPairsDataset(Dataset):
          def __init__(self,eng_fra_pairs):
              self.eng_fra_pairs = eng_fra_pairs
              self.sample_cnt = len(eng_fra_pairs)    # 样本条数
      
          def __len__(self):
              return self.sample_cnt
      
          def __getitem__(self, index):
              # 1- 防止索引为负数;防止索引越界
              index = min(max(index,0), self.sample_cnt - 1)
      
              # 2- 获得英语句子(特征数据)和法语句子(目标值)
              # 2.1- 获得句子。[英语句子1, 法语句子1]
              eng_fra_pair = self.eng_fra_pairs[index]
      
              # 句子 -> 分词 -> 变成索引
              """
                  为什么只在句子的末尾增加结束标识EOS_TOKEN,而不增加SOS_TOKEN?
                  答:
                      1- Seq2Seq架构要求必须要有EOS_TOKEN,而SOS_TOKEN可选的
                      2- SOS_TOKEN会在Decoder中进行数据处理的时候再增加
                      3- Decoder端我们只能通过添加SOS_TOKEN告诉解码器开始进行翻译的工作;以及通过添加EOS_TOKEN告诉解码器翻译工作结束
              """
              # 2.2- 处理英语句子
              x = [english_word2index[word] for word in eng_fra_pair[0].split(" ")]
              # 所有的张量数据必须放在同一个硬件设备上,要么同时在GPU,要么同时在CPU
              x_tensor = torch.tensor(x,dtype=torch.long,device=device)
              # print(f"原始英语句子{eng_fra_pair[0]},词索引向量{x_tensor}")
      
              # 2.3- 处理法语句子
              y = [franch_word2index[word] for word in eng_fra_pair[1].split(" ")]
              y.append(EOS_TOKEN)
              y_tensor = torch.tensor(y, dtype=torch.long, device=device)
              # print(f"原始法语句子{eng_fra_pair[1]},词索引向量{y_tensor}")
      
              return x_tensor,y_tensor
      
      # 5- 创建DataLoader
      def get_dataloader():
          # 1- 创建自定义Dataset实例对象
          dataset = MyPairsDataset(eng_fra_pairs)
      
          # 2- 创建数据加载器
          dataloader = DataLoader(dataset=dataset,batch_size=1,shuffle=True)
      
          return dataloader
      
      # 6- 无注意力机制的编码器
      class Encoder(nn.Module):
          def __init__(self,eng_vocab_size,input_size,hidden_size):
              """
              :param eng_vocab_size: 英语词汇表大小
              :param input_size: 词向量维度
              :param hidden_size: 隐藏状态向量维度
              """
      
              # 1- 初始化父类
              super().__init__()
      
              # 2- 设置属性值
              self.eng_vocab_size = eng_vocab_size
              self.input_size = input_size
              self.hidden_size = hidden_size
      
              # 3- 搭建网络结构
              # 3.1- 词嵌入层
              self.ebd = nn.Embedding(num_embeddings=self.eng_vocab_size,embedding_dim=self.input_size)
      
              # 3.2- 循环网络层
              self.gru = nn.GRU(input_size=self.input_size,hidden_size=self.hidden_size,num_layers=1,batch_first=True)
      
          def forward(self,input,hidden):
              """
              Encoder端的前向传播:负责理解英语句子的语义信息
              :param input: 本次输入数据,张量形状:[batch_size每个批次中英语句子的条数,seq_len英语句子中词的个数]
              :param hidden: 上一个时间步的隐藏状态,张量形状:[num_layers,batch_size,hidden_size]
              :return:
              """
              # 1- 调用词嵌入层
              # embed形状:[batch_size每个批次中英语句子的条数,seq_len英语句子中词的个数,input_size词向量维度]
              # print(f"1--input-->{input.shape}")
              embed = self.ebd(input)
              # print(f"2--embed-->{embed.shape}")
      
              # 2- 调用GRU
              output,hidden = self.gru(embed,hidden)
      
              return output,hidden
      
          def init_hidden(self):
              # 隐藏状态张量形状:[num_layers,batch_size,hidden_size]
              # num_layers:目前手动指定为1层
              # batch_size:因为我们没有对英语句子进行长度规范的处理,因此只能设置为1
              return torch.zeros(size=(1,1,self.hidden_size)).to(device=device)
      
      # 测试编码器
      def use_encoder():
          # 1- 准备数据
          dataloader = get_dataloader()
      
          # 2- 创建编码器对象
          my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256)
          my_encoder.to(device=device)
      
          # 3- 遍历数据,前向传播
          for x,y in dataloader:
              # 3.1- 初始隐藏状态
              hidden = my_encoder.init_hidden()
      
              # 3.2- 前向传播
              output,hidden = my_encoder(x,hidden)
      
              print(f"output-->{output.shape}")
              print(f"hidden-->{hidden.shape}")
      
              break
      
      # 7- 解码器:无注意力机制
      class Decoder(nn.Module):
          def __init__(self,fra_vocab_size,hidden_size):
              # 1- 初始化父类
              super().__init__()
      
              # 2- 设置属性值
              self.fra_vocab_size = fra_vocab_size
              self.hidden_size = hidden_size
      
              # 3- 搭建网络结构
              # 3.1- 词嵌入层
              self.ebd = nn.Embedding(num_embeddings=self.fra_vocab_size,embedding_dim=self.hidden_size)
      
              # 3.2- GRU层
              self.gru = nn.GRU(input_size=self.hidden_size,hidden_size=self.hidden_size,num_layers=1,batch_first=True)
      
              # 3.3- 输出层
              self.output_linear = nn.Linear(in_features=self.hidden_size,out_features=self.fra_vocab_size)
      
              # 4- 输出层的激活函数
              # CrossEntropyLoss = LogSoftmax + NLLLoss
              self.log_softmax = nn.LogSoftmax(dim=-1)
      
          def forward(self,input,hidden):
              """
              Decoder前向传播:负责翻译得到法语词
              :param input: 本次输入数据,每次只送一个法语词进来,张量形状:[batch_size,seq_len],[[一个法语词的词索引]]
              :param hidden: 上一个时间步的隐藏状态,张量形状:[num_layers,batch_size,hidden_size]
              :return:
              """
      
              # 1- 调用词嵌入层
              embed = torch.relu(self.ebd(input))
      
              # 2- 调用GRU
              output,hidden = self.gru(embed,hidden)
              print(f"1-->output-->{output.shape}")
      
              # 3- 调用输出层
              # 3.1- 将output由3维变成2维,[batch_size,seq_len,hidden_size] -> [batch_size,hidden_size]
              """
                  output[:,-1,:] 针对文本分类的业务场景。-1表示取最后一个时间步的隐藏状态,文本分类需要对一整条句子的完整信息理解完以后再做分类
                  output[:,0,:]  针对文本翻译的业务场景。
              """
              output = output[:,0,:]
              print(f"2-->output[:,0,:]-->{output.shape}")
      
              # 3.2- 调用
              output = self.log_softmax(self.output_linear(output))
              print(f"3-->output-->{output.shape}")
      
              return output,hidden
      
          def init_hidden(self):
              return torch.zeros(size=(1,1,self.hidden_size)).to(device=device)
      
      # 测试解码器
      def use_decoder():
          # 1- 准备数据
          dataloader = get_dataloader()
      
          # 2- 创建模型实例对象
          my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256).to(device=device)
          my_decoder = Decoder(fra_vocab_size=franch_word_cnt,hidden_size=256).to(device=device)
      
          # 3- 前向传播
          for x,y in dataloader:
              # x:一条英语句子,[[词索引1, 词索引2,... ]]
              # y:一条法语句子,[[词索引1, 词索引2,... ]]
      
              # 3.1- 调用编码器:让模型理解英语句子的完整含义
              h0 = my_encoder.init_hidden()
              encoder_output,hidden = my_encoder(x,h0)
      
              # 3.2- 调用解码器:每次送入一个词
              for index in range(y.shape[1]):
                  # 3.3- 解码器前向传播
                  fra_word = y[0][index].reshape(1,-1)
                  decoder_output,hidden= my_decoder(fra_word, hidden)
      
                  print(f"法语词词索引{fra_word},隐藏状态信息{hidden.shape}")
      
              break
      
      # 8- 解码器:有注意力机制
      class AttenDecoder(nn.Module):
          def __init__(self,fra_vocab_size,hidden_size):
              # 1- 初始化父类
              super().__init__()
      
              # 2- 设置属性值
              self.fra_vocab_size = fra_vocab_size
              self.hidden_size = hidden_size      # 有两个作用:1- 词向量维度;2- 隐藏状态向量维度
      
              # 3- 搭建网络结构
              # 3.1- 词嵌入层
              self.embedding = nn.Embedding(num_embeddings=self.fra_vocab_size,embedding_dim=self.hidden_size)
      
              # 3.2- Dropout随机失活层
              self.dropout = nn.Dropout(p=0.2)
      
              # 3.3- 线性层:用来计算QK的相似性
              self.attn = nn.Linear(in_features=self.hidden_size+self.hidden_size,out_features=MAX_LENGTH)
      
              # 3.4- 线性层:对Q和C拼接后的张量形状进行调整
              self.attn_combine = nn.Linear(in_features=self.hidden_size+self.hidden_size,out_features=self.hidden_size)
      
              # 3.5- GRU循环网络层
              self.gru = nn.GRU(input_size=self.hidden_size,hidden_size=self.hidden_size,num_layers=1,batch_first=True)
      
              # 3.6- 输出层
              self.out = nn.Linear(in_features=self.hidden_size,out_features=self.fra_vocab_size)
      
              # 激活函数
              self.log_softmax = nn.LogSoftmax(dim=-1)
      
          def forward(self, input, prev_hidden, encoder_outputs):
              """
              :param input: 解码器本次输入数据,也就是Q。张量形状 [batch_size,seq_len],每次只传一个词进来
              :param prev_hidden: 解码器上一个时间步的隐藏状态,也就是K。张量形状 [num_layers,batch_size,hidden_size]
              :param encoder_outputs: 编码器端所有时间步隐藏状态的拼接,也就是V,而且还做了句子长度规范的处理。张量形状 [batch_size,MAX_LENGTH,hidden_size]
              :return:
              """
      
              # 1- 调用词嵌入层
              embedded = self.dropout(self.embedding(input))
      
              # 2- 计算注意力机制
              # 2.1- Q和K拼接
              # embedded就是Q
              # 如果不进行张量形状调整,那么就是num_layers与batch_size进行拼接,业务层面是不合理。
              # 调整K的形状 由 [num_layers,batch_size,hidden_size] -> [batch_size,num_layers,hidden_size]
              K = prev_hidden.transpose(dim0=0,dim1=1)
              # 拼接
              qk_cat = torch.cat(tensors=(embedded,K), dim=-1)
      
              # 2.2- 通过线性层算QK的相似性
              score = self.attn(qk_cat)
      
              # 2.3- 相似性转成权重
              attn_weights = torch.softmax(score, dim=-1)
      
              # 2.4- 得到专属信息包C:权重矩阵和V进行矩阵乘法运算
              # C对应原理图中的attn_applied
              C = torch.bmm(attn_weights, encoder_outputs)
      
              # 2.5- Q和C拼接
              qc_cat = torch.cat(tensors=(embedded,C),dim=-1)
      
              # 2.6- 通过线性层调整QC拼接后的张量
              qc_output = torch.relu(self.attn_combine(qc_cat))
      
              # 3- 调用GRU
              output,hidden = self.gru(qc_output,prev_hidden)
      
              # 4- 调用输出层
              output = output[:,0,:]
              output = self.log_softmax(self.out(output))
      
              return output,hidden,attn_weights
      
          def init_hidden(self):
              return torch.zeros(size=(1,1,self.hidden_size)).to(device=device)
      
      # 9- 模型训练
      # 9.1- 单次训练过程
      def train_iter(x,y,my_encoder,my_decoder,encoder_adam,decoder_adam,loss):
          """
          单次样本数据的训练过程
          :param x: 英语句子,张量形状[batch_size,seq_len]
          :param y: 法语句子,张量形状[batch_size,seq_len]
          :param my_encoder: 编码器对象
          :param my_decoder: 解码器对象
          :param encoder_adam: 编码器的优化器
          :param decoder_adam: 解码器的优化器
          :param loss: 损失函数对象
          :return: 平均损失值
          """
      
          # 1- 调用编码器
          encoder_hidden = my_encoder.init_hidden()
          encoder_output,encoder_hidden = my_encoder(x,encoder_hidden)
      
          # 2- 调用解码器之前的准备工作
          # 2.1- 句子长度规范化处理,同时也是合并得到V
          # 初始化一个形状[1,10,256]的全0张量
          V = torch.zeros(size=(1,MAX_LENGTH,256), device=device)
          copy_seq_len = min(MAX_LENGTH, x.shape[1])
          V[:, :copy_seq_len, :] = encoder_output[:, :copy_seq_len, :]
      
          # 2.2- 准备变量
          # 将编码器端最后一个时间步的隐藏状态作为 初始隐藏状态 给到解码器使用
          decoder_hidden = encoder_hidden
          # 损失值
          loss_value = 0.0
          # 获得法语句子中词的个数
          y_seq_len = y.shape[1]
          # 存储法语翻译结果,而且上个时间步的预测结果会作为本次输入数据使用,这称之为自回归
          input_y = torch.tensor([[SOS_TOKEN]], device=device)
          # 教师机制的标识。True:使用教师机制,也就是告诉下个时间步【真实】的法语词内容。反之,把【预测】结果给到下个时间步
          teacher_forcing_flag = True if random.random()<0.5 else False
      
          # 3- 调用解码器
          if teacher_forcing_flag:
              # 使用教师机制,也就是告诉下个时间步【真实】的法语词内容
              for i in range(y_seq_len):
                  # 3.1- 前向传播
                  pred_output,decoder_hidden,attn_weights = my_decoder(input=input_y, prev_hidden=decoder_hidden, encoder_outputs=V)
      
                  # 3.2- 计算损失值
                  # print(f"y-->{y.shape}")
                  # print(f"y[0]-->{y[0].shape}")
                  # print(f"y[0][i]-->{y[0][i]}")
      
                  y_true = y[0][i].reshape(1)
      
                  # print(f"y_true-->{y_true.shape}")
      
                  loss_value += loss(pred_output,y_true)
      
                  # 3.3- 告诉下个时间步【真实】的法语词内容
                  input_y = y[0][i].reshape(1,-1) # [batch_size,seq_len]
      
          else:
              # 普通训练机制,把【预测】结果给到下个时间步
              for i in range(y_seq_len):
                  # 3.1- 前向传播
                  pred_output, decoder_hidden, attn_weights = my_decoder(input=input_y, prev_hidden=decoder_hidden,
                                                                         encoder_outputs=V)
      
                  # 3.2- 计算损失值
                  y_true = y[0][i].reshape(1)
                  loss_value += loss(pred_output, y_true)
      
                  # 3.3- 获得预测结果的词索引
                  pred_word_index = torch.argmax(pred_output, dim=-1)
      
                  # 3.4- 判断是否到了句子的结尾
                  if pred_word_index == EOS_TOKEN:
                      break
      
                  # 3.5- 告诉下个时间步【预测】的法语词内容
                  input_y = pred_word_index.reshape(1, -1)  # [batch_size,seq_len]
      
          # 4- 反向传播固定代码
          encoder_adam.zero_grad()
          decoder_adam.zero_grad()
      
          loss_value.sum().backward()
      
          encoder_adam.step()
          decoder_adam.step()
      
          return loss_value.item() / y_seq_len
      
      # 9.2- 整体训练过程
      def train():
          # 1- 准备数据
          dataloader = get_dataloader()
      
          # 2- 创建类的实例对象
          # 2.1- 模型实例对象
          my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256).to(device=device)
          my_decoder = AttenDecoder(fra_vocab_size=franch_word_cnt,hidden_size=256).to(device=device)
          # 2.2- 优化器实例对象:与具体的实例对象有关,因此要创建单独的两个
          encoder_adam = torch.optim.Adam(params=my_encoder.parameters(),lr=1e-4)
          decoder_adam = torch.optim.Adam(params=my_decoder.parameters(),lr=1e-4)
          # 2.3- 损失函数对象:与具体的实例对象无关,内部只是通过预测和真实值计算差值而已,因此只需要创建一个
          loss = nn.NLLLoss()
      
          # 3- 设置模式
          my_encoder.train()
          my_decoder.train()
      
          # 4- 模型训练
          epochs = 1
      
          avg_loss_list = []  # 用来存储平均损失
      
          for epoch in range(epochs):
              total_loss_value = 0.0  # 总损失值
      
              for i,(x_train,y_train) in enumerate(tqdm(dataloader), start=1):
                  # 1- 单次训练得到损失值
                  loss_value = train_iter(x_train,y_train,my_encoder,my_decoder,encoder_adam,decoder_adam,loss)
      
                  # 2- 更新统计指标
                  total_loss_value += loss_value
      
                  # 3- 每隔100个批次记录并且输出一次信息
                  if i%100==0:
                      # 计算平均损失值
                      avg_loss = total_loss_value / i
      
                      avg_loss_list.append(avg_loss)
                      print(f"第{epoch+1}轮次,平均损失{avg_loss}")
      
          # 5- 保存训练好的模型
          torch.save(my_encoder.state_dict(), "model/encoder.pkl")
          torch.save(my_decoder.state_dict(), "model/decoder.pkl")
      
          # 6- 绘制损失值变化曲线
          plt.plot(avg_loss_list)
          plt.show()
      
      # 10- 模型预测
      # 10.1- 单条句子的翻译
      def seq2seq_evaluate(x_tensor,my_encoder,my_decoder):
          """
          单条句子的翻译
          :param x_tensor: 一条处理后的英语句子,形状[batch_size,seq_len]
          :param my_encoder: 编码器
          :param my_decoder: 解码器
          :return: 翻译的法语词列表
          """
      
          with torch.no_grad():
              # 1- 调用编码器
              encoder_output,encoder_hidden = my_encoder(x_tensor,my_encoder.init_hidden())
      
              # 2- 调用解码器之前的准备工作
              # 2.1- 句子长度规范化处理,同时也是合并得到V
              # 初始化一个形状[1,10,256]的全0张量
              V = torch.zeros(size=(1,MAX_LENGTH,256), device=device)
              copy_seq_len = min(MAX_LENGTH, x_tensor.shape[1])
              V[:, :copy_seq_len, :] = encoder_output[:, :copy_seq_len, :]
      
              # 2.2- 准备变量
              # 将编码器端最后一个时间步的隐藏状态作为 初始隐藏状态 给到解码器使用
              decoder_hidden = encoder_hidden
              # 存储单次的法语翻译结果,而且上个时间步的预测结果会作为本次输入数据使用,这称之为自回归
              input_y = torch.tensor([[SOS_TOKEN]], device=device)
              # 存储所有的翻译法语词
              franch_word_list = []
      
              # 3- 评估/预测
              for i in range(MAX_LENGTH):
                  # 3.1- 前向传播:调用解码器
                  decoder_output,decoder_hidden,attn_weights = my_decoder(input=input_y, prev_hidden=decoder_hidden, encoder_outputs=V)
      
                  # 3.2- 获得预测概率最高的那个词的词索引
                  pred_fra_index = torch.argmax(decoder_output, dim=-1).item()
      
                  # 3.3- 判断是否遇到了句子的结束标识
                  if pred_fra_index == EOS_TOKEN:
                      franch_word_list.append("EOS")
                      break
                  else:
                      # 通过索引获得法语词的内容
                      fra_word = franch_index2word[pred_fra_index]
                      franch_word_list.append(fra_word)
      
                  input_y = torch.tensor([[pred_fra_index]], device=device)
      
          return franch_word_list
      
      # 10.2- 完整的预测流程
      def use_seq2seq_evaluate():
          # 1- 手动准备未知数据
          my_samplepairs = [
              ['i m impressed with your french .', 'je suis impressionne par votre francais .'],
              ['i m more than a friend .', 'je suis plus qu une amie .'],
              ['she is beautiful like her mother .', 'elle est belle comme sa mere .']
          ]
      
          # 2- 加载训练好的模型
          my_encoder = Encoder(eng_vocab_size=english_word_cnt,input_size=256,hidden_size=256).to(device=device)
          my_encoder.load_state_dict(torch.load("model/encoder.pkl"))
      
          my_decoder = AttenDecoder(fra_vocab_size=franch_word_cnt,hidden_size=256).to(device=device)
          my_decoder.load_state_dict(torch.load("model/decoder.pkl"))
      
          # 3- 设置模式
          my_encoder.eval()
          my_decoder.eval()
      
          # 4- 预测
          for i,pair in enumerate(my_samplepairs):
              x = pair[0] # 英语句子
              y = pair[1] # 英语句子
      
              # 4.1- 英语句子转成张量
              x_list = [english_word2index[word] for word in x.split(" ")]
              # 所有的张量数据必须放在同一个硬件设备上,要么同时在GPU,要么同时在CPU
              x_tensor = torch.tensor(x_list, dtype=torch.long, device=device).reshape(1,-1)
      
              # 4.2- 对单条句子进行评估
              franch_word_list = seq2seq_evaluate(x_tensor,my_encoder,my_decoder)
      
              # 4.3- 输出结果
              print(f"英语句子 {x},真实法语句子 {y},翻译法语句子 {' '.join(franch_word_list)}")
      
      if __name__ == '__main__':
          # 1- 数据清洗测试
          # content = " I LOve h@@@eima! "
          # normalizeString(content)
      
          # 2- 读取文件
          # getdata()
      
          # print(english_word_cnt)
          # print(franch_word_cnt)
          # print(eng_fra_pairs[:5])
          # print(english_word2index)
          # print(franch_word2index)
      
          # 3- 测试Dataloader
          # dataloader = get_dataloader()
          # for x,y in dataloader:
          #     print(x)
          #     print(y)
          #
          #     break
      
          # 4- 测试编码器
          # use_encoder()
      
          # use_decoder()
      
      
          # 5- 模型训练
          train()
      
          # 6- 模型预测
          use_seq2seq_evaluate()
      

第4节 · 第4章 Transformer

🎯 掌握 Transformer 结构:输入编码、自注意力、编码器、解码器与输出。
📖 笔记

4. Transformer

第四章 Transformer模型

背景介绍【了解】

模型被提出时间

2017年提出,2018年google发表了BERT模型,使得Transformer架构流行起来,BERT在许多NLP任务上,取得了Soat的成就。

论文地址:https://arxiv.org/pdf/1706.03762

模型优势

1、能够实现并行计算,提高模型训练效率
2、更好的特征提取能力

image-20260406165603013

思想间的关系:

Seq2Seq
Seq2Seq+注意力机制
Transformer
2017 《attention is all you need》
2018 Bert
2022 ChatGPT、T5

绝大多数大模型底层的原理都是Transformer

Transformer模型架构

整体架构【掌握】

组成部分:

1、输入部分
2、编码器部分
3、解码器部分
4、输出部分

image-20260406170800968

image-20251031233900680

image-20260406170346882

image-20260406181647715

架构概述:

总结流程:
输入文字 -> 嵌入 -> 位置编码 -> 编码器多层加工(注意力+前馈) -> 解码器多层加工(掩码注意力 + 关联编码器 + 前馈) -> 线性层 -> softmax -> 输出概率选词

整个架构的核心:
注意力机制抓关联,多层堆叠挖深度,残差归一化稳训练

image-20260406172837481

输入部分

Positional Encoding:位置编码
作用:给词向量加'位置信息'
例如:Transformer本身不明白词的顺序,例如:我打你、你打我,所以得额外给每个词向量添加位置信息,告诉模型谁前谁后


Input Embedding:解码器端词嵌入层
作用:把输入的词转成词向量
例如:'你好'->[0.1,-0.3...]

输出部分

Softmax:激活层
作用:把线性层输出的数值转成概率,概率总和为1,模型就知道该选哪个词输出


Linear:线性层
作用:把解码器输出的向量,调整成'vocab size词汇表大小'维度,
例如:例如词汇表有1W个词,就把向量变成1W维,每个维度对应1个词的概率分数

编码器部分

Nx:重复堆叠
作用:把上面的'多头注意力 + Add&Norm + 前馈网络'重复N次(论文里是6次)
例如:一层不够深,多堆几层,让模型'层层递进'理解文本,从表面语义 ->深层逻辑


Feed Forward:前馈网络
作用:对每个位置的词向量"单独强化特征’
例如:经过注意力层后,再用全连接网络加工一下,给每个词'注入更多细节',例如:'强化'猫的动物属性,追’的动作属性


Add & Norm:残差连接 + 层归一化
作用:让训练更稳,更高效。层归一化也称之为规范化层
例如:Add是把注意力层的输入和输出加一起,防止信息丢太多。Norm是把数据归一化(数值限制在某个范围),避免训练时跑偏,梯度消失/爆炸


Multi-Head Attention:多头自注意力层
作用:让模型同时从多个角度关注句子里的词
例如:猫追老鼠',既要关注'猫"和'追'的关系,也要关注'猫'和'老鼠'的关系,还能对比'猫' 和'老鼠'的关联.

解码器部分

Add & Norm + Feed Forward + Nx:
作用:和编码器里的逻辑一样,
     残差连接、归一化保证训练稳定;
     前馈网络强化特征;
     Nx重复堆叠加深模型


Multi-Head Attention:多头注意力层
作用:让解码器'关注编码器输出的内容'
例如:翻译时,解码器生成法语词,盯着处理后的英文词义,确保翻译准确,找对应关系


Masked Multi-Head Attention:掩码多头自注意力层
作用:防止'偷看未来的词'
例如:解码器是一步步生成输出的,只能用'已生成的词',不能用未生成的词.

代码开发思路【了解】

重点组件

只需要搞清楚如下几个组件的功能、作用和代码开发

image-20251101080336962

具体开发步骤

  • 编码器

image-20260107153005088

  • 解码器

参考编码器的开发过程,大部分地方的功能直接复用。主要是开发【掩码多头自注意力层】

  • 输出部分

非常简单,nn.Linear全连接层+Softmax激活函数

  • 代码的作用和开发顺序

image-20260109161133220

输入部分

词嵌入层

注意:为什么embedding之后要乘以根号下d_model
- 原因1:为了防止position encoding的信息覆盖我们的word embedding,也就是防止两者的值差距过大。所以进行一个数值增大
- 原因2:符合正态分布

  • 代码实现
# 词嵌入层
class Embedding(nn.Module):
    def __init__(self,vocab_size,d_model):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.vocab_size = vocab_size    # 词汇表大小
        self.d_model = d_model          # 向量维度。本次这里表示 词向量维度

        # 3- 搭建网络结构
        self.ebd = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.d_model)

    def forward(self,input):
        """
            为什么要乘以根号d_model,也就是根号dk?
            答:为了对词向量中的数据进行放大。避免与位置编码的数据进张量加法运算以后,词向量的值过小导致占比少
            但是位置编码和词向量编码的数据地位应该是一样的,不应该出现谁重要而谁不重要的情况
            对应论文的内容:In the embedding layers, we multiply those weights by √dmodel.

            什么情况下词向量中会出现比较小的值?
            答:当d_model很大的时候
        """
        return self.ebd(input) * math.sqrt(self.d_model)
  • 测试代码
# 测试词嵌入层
def use_embedding():
    # 1- 创建实例对象
    embedding = Embedding(vocab_size=1000,d_model=5)
    # embedding = Embedding(vocab_size=1000,d_model=10240)

    # 2- 准备数据
    # 一个批次中有2条句子,每条句子有3个词
    x = torch.tensor([
        [100, 3, 99],
        [888, 777, 5]
    ])

    # 3- 调用得到词向量
    text_tensor = embedding(x)
    print(f"文本张量的形状:{text_tensor.shape}")
    print(f"文本张量的内容:{text_tensor}")
    print(f"文本张量中绝对值的最小值:{text_tensor.abs().min()}")

位置编码

作用

Transformer编码器或解码器中缺乏位置信息,因此加入位置编码器,将词汇的位置可能代表的不同特征信息和word_embedding进行融合,以此来弥补位置信息的缺失。

位置编码器实现方式:三角函数来实现的,sin、cos函数

为什么使用三角函数来进行位置编码

1、保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化
2、正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算

image-20251102152536133

  • 代码实现
class PositionalEncoding(nn.Module):
    def __init__(self,dropout_p,max_len,d_model):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.dropout_p = dropout_p  # 随机失活概率
        self.max_len = max_len      # 句子规范化处理的长度上限
        self.d_model = d_model      # 词向量维度

        # 3- 创建随机失活层
        self.dropout = nn.Dropout(p=self.dropout_p)

        # 4- 定义位置编码的计算规则:也就是实现公式
        # 4.1- 初始化pe:目前的形状是[60,5]
        pe = torch.zeros(size=(self.max_len,self.d_model))
        # 4.2- 得到pos的取值区间:arange是左闭右开的区间
        pos = torch.arange(start=0,end=max_len,step=1).unsqueeze(dim=-1) # 张量形状[60,1]
        # 4.3- 得到1/分母
        div_term = 1/(10000**(torch.arange(start=0,end=d_model,step=2).float()/self.d_model))
        # 4.4- 得到正弦和余弦里面的内容
        value = pos * div_term
        # 4.5- 分别计算:2i->正弦;2i+1->余弦
        pe[:, 0::2] = torch.sin(value)
        pe[:, 1::2] = torch.cos(value)

        # 4.6- 将pe升维为三维:也就是 [60,5]->[1,60,5]
        pe = pe.unsqueeze(dim=0)

        # 4.7- 将pe注册到缓存中,通过它来不断的更新计算得到每个词的位置编码信息,后面可以直接使用pe
        self.register_buffer("pe",pe)

    def forward(self,embed):
        """
        位置编码需要根据词向量来计算;然后再实现位置编码 加上 词向量
        :param embed: 词向量
        :return:
        """

        """
            为什么要对pe进行切片操作?
            答:因为传进来的embed中词的个数不一定够max_len的长度。举例如下:
            embeb中有10个词,词向量维度是5,句子的条数有2条,那么embed的形状[2,10,5]
            对应pe的形状[2,60,5]。它们两个的形状不相同,因此不能进行加法运算,所以需要
            将pe的形状进行切片操作,也就是将有意义的前10个词的位置编码信息取出来
        """
        seq_len = embed.shape[1]
        result = embed + self.pe[:, :seq_len, :]

        print(f"1--embed的内容-->{embed}")
        print(f"2--pe的内容-->{self.pe[:, :seq_len, :]}")
        return self.dropout(result)
  • 测试代码
def use_positional_encoding():
    # 注意:词向量维度只能是偶数,这个与位置编码中i的取值区间有关

    # 1- 创建实例对象
    embedding = Embedding(vocab_size=1000, d_model=512)

    # 2- 准备数据
    # 一个批次中有2条句子,每条句子有3个词
    x = torch.tensor([
        [100, 3, 99, 666],
        [888, 777, 5, 555]
    ])

    # 3- 调用得到词向量
    text_tensor = embedding(x)

    # 4- 调用位置编码
    # 4.1- 创建类的实例对象
    pe_obj = PositionalEncoding(dropout_p=0.2,max_len=60,d_model=512)
    # 4.2- 调用
    pe_and_ebd = pe_obj(text_tensor)

    print(f"最终的形状:{pe_and_ebd.shape}")
    print(f"最终的内容:{pe_and_ebd}")

    return pe_and_ebd

# 可视化位置编码
def plot_position():
    # 1. 实例化位置编码器.
    my_position = PositionalEncoding(dropout_p=0, max_len=100, d_model=20)

    # 2. 生成全0的输入, 观察位置编码的模式.
    # (1, 100, 20) -> 批次大小, 句子长度, 词嵌入维度
    embed = torch.zeros(1, 100, 20)
    y = my_position(embed)

    # 3. 设置图表大小.
    plt.figure(figsize=(20, 15))
    # 绘制位置编码第4到第7列, 100个词的  [4, 5, 6, 7]
    """
        图形的信息解释:
            x轴:词的索引,目前总共有100个词
            y轴:位置编码值
    """
    plt.plot(np.arange(100), y[0, :, 4:8].detach().numpy())
    plt.legend([f'dim {p}' for p in [4, 5, 6, 7]])
    plt.show()

image-20260407115449201

编码器部分

掩码张量

作用

掩码:掩就是遮掩、码就是张量。掩码本身需要一个掩码张量,掩码张量的作用是对另一个张量进行数据信息的掩盖。一般掩码张量是由0和1两种数字组成,至于是0对应位置或是1对应位置进行掩码,可以自己设定
  • PyTorch版本
# 使用PyTorch实现掩码。【掌握】
"""
    总结:
        1- 上三角掩码:右上角的值为1,左下角的值为0
        2- 下三角掩码:右上角的值为0,左下角的值为1
        3- 相同的规律:对角线的时候diagonal为0;如果是正数,并且越来越大,那么往右上角移动;反之往左下角移动;diagonal的取值超过范围不会报错
        4- 【掌握】掩码的作用:为了防止解码器端当前时间步偷看后面时间步的数据,你只能看到你前面时间步已经预测出来的数据。核心是防止模型训练效果不好
        5- 【掌握】代码:torch.triu(t,diagonal=0)
"""

import torch
def torch_mask():
    # 创建初始张量
    t = torch.ones(size=(5,5))
    print(f"原始张量:{t}")

    # 进行掩码操作
    # 上三角掩码
    u_0_mask = torch.triu(t,diagonal=0)
    print(f"上三角掩码0:{u_0_mask}")

    u_1_mask = torch.triu(t, diagonal=1)
    print(f"上三角掩码1:{u_1_mask}")

    u__1_mask = torch.triu(t, diagonal=-1)
    print(f"上三角掩码_1:{u__1_mask}")

    u_n_mask = torch.triu(t, diagonal=1000)
    print(f"上三角掩码n:{u_n_mask}")

    print("-"*30)

    # 下三角掩码
    l_0_mask = torch.tril(t, diagonal=0)
    print(f"下三角掩码0:{l_0_mask}")

    l_1_mask = torch.tril(t, diagonal=1)
    print(f"下三角掩码1:{l_1_mask}")

    l__1_mask = torch.tril(t, diagonal=-1)
    print(f"下三角掩码-1:{l__1_mask}")
  • numpy版本
import numpy as np
def np_mask():
    # 1- 准备数据
    arr = np.ones(shape=(5,5))

    # 2- 上三角掩码
    # k和PyTorch中diagonal参数的作用完全一样
    u_result = np.triu(arr,k=0)
    print(f"上三角掩码0:\n{u_result}")

    u_result = np.triu(arr, k=1)
    print(f"上三角掩码1:\n{u_result}")

    u_result = np.triu(arr, k=-1)
    print(f"上三角掩码-1:\n{u_result}")

    # 3- 下三角掩码
    l_result = 1 - u_result
    print(f"下三角掩码:\n{l_result}")

    l_result = np.tril(arr,k=0)
    print(f"下三角掩码0:\n{l_result}")

    l_result = np.tril(arr, k=1)
    print(f"下三角掩码1:\n{l_result}")

    l_result = np.tril(arr, k=-1)
    print(f"下三角掩码-1:\n{l_result}")
  • 掩码张量的可视化
# 掩码张量的可视化.
def dm03_test_mask():
    # 简单说(总结): 纵向选'当前位置', 横向看'能看哪些位置', 0(黄色): 遮挡, 1(紫色): 不遮挡
    plt.figure(figsize=(5, 5))
    plt.imshow(demo2(20)[0])
    plt.show()

注意力机制【掌握】

计算规则

自注意力机制,规则:Q乘以K的转置,然后除以根号下d_k,然后再进行Softmax,最后和V进行张量矩阵相乘
如果是自注意力,那么Q、K、V的值完全相同

注意力计算

扩展知识:点积和点乘

点积:等同于矩阵乘法,可以使用matmul函数进行计算,也可以使用dot函数进行计算。在注意力机制场景下,一般使用matmul

点乘:两个张量的形状要完全一样。对应坐标位置元素相乘

image-20251104101452355

  • 代码实现
import copy
import math

import torch
from torch import Tensor
import torch.nn as nn
from demo01_input import *

# 根据注意力计算公式算注意力
def attention(query:Tensor,key:Tensor,value:Tensor,mask=None,dropout=None):
    """
    注意力计算方法
    该方法能够实现如下3种注意力的计算:
    1- 编码器端 多头自注意力:query=key=value,mask=None。编码器端大多数不需要掩码
    2- 解码器端 掩码多头自注意力:query=key=value,mask不为空的时候
    3- 解码器端 掩码多头注意力(交叉注意力):query和key、value不相等,mask=None
    :param query: 查询张量,形状:[batch_size每个批次中有多少条句子,seq_len句子中词的个数,d_model向量维度]
    :param key: 键张量,形状:[batch_size,seq_len,d_model]
    :param value: 值张量,形状:[batch_size,seq_len,d_model]
    :param mask: 掩码张量,形状:[batch_size,seq_len,seq_len],使用过程中会自动进行广播机制
    :param dropout: 随机失活层对象
    :return:
    """

    # 1- 获得dk,也就是d_model,也就是向量维度
    dk = query.shape[-1]

    # 2- Q和K的转置进行矩阵乘法运算,再除以根号下dk。得到了相似性得分
    # 注意:key.transpose需要交换的是张量中最后两个形状的位置,不要从左往右正着数索引,要倒着来
    scores = torch.matmul(query, key.transpose(dim0=-1,dim1=-2)) / math.sqrt(dk)

    # 3- 使用掩码
    if mask is not None:
        # 为什么要使用-1e9次方对掩码的地方进行填充呢?因为为了让后面计算softmax权重值的时候,掩码地方的权重被计算为0
        scores = scores.masked_fill(mask==1, value=-1e9)

    # 4- 将相似性得分转成权重矩阵
    weighted = torch.softmax(scores, dim=-1)

    # 5- 【可选】随机失活
    if dropout is not None:
        weighted = dropout(weighted)

    # 6- 权重矩阵和V进行矩阵乘法,得到专属信息包C
    C = torch.matmul(weighted,value)

    return C,weighted
  • 测试代码
# 注意力计算测试
def use_attention():
    # 1- 输入数据,得到经过词嵌入层和位置编码以后的数据
    pe_ebd = use_positional_encoding()

    dropout = nn.Dropout(p=0.2)

    # 2- 测试:编码器端 多头自注意力机制
    query=key=value=pe_ebd
    C,weighted = attention(query,key,value,dropout=dropout)
    print(f"编码器端 多头自注意力机制C:{C.shape}")
    print(f"编码器端 多头自注意力机制weighted:{weighted.shape}")
    print(f"编码器端 多头自注意力机制weighted:{weighted.sum()}")


    # 3- 测试:解码器端 掩码多头自注意力机制
    mask = torch.triu(torch.ones(size=(2,4,4)), diagonal=1)
    query=key=value=pe_ebd
    C,weighted = attention(query,key,value,mask=mask,dropout=dropout)
    print(f"解码器端 多头自注意力机制C:{C.shape}")
    print(f"解码器端 多头自注意力机制weighted:{weighted.shape}")
    print(f"解码器端 多头自注意力机制weighted:{weighted.sum()}")

多头注意力机制【掌握】

概念

多头的好处:
    1- 能够让模型进行并行计算,提高运行效率
    2- 多个头来同时处理数据,那么模型对数据的理解、学习会更好,模型整体效果会更好
  • 多头注意力机制原理图

image-20260407161840657

  • 代码实现
def clones(module_obj, cnt):
    """
    创建指定个数的实例对象,对象之间是深拷贝的关系
    :param module_obj: 需要拷贝的对象
    :param cnt: 拷贝的个数
    :return:
    """
    # 语法格式  nn.ModuleList([copy.deepcopy(拷贝对象) for _ in range(拷贝个数)])
    return nn.ModuleList([copy.deepcopy(module_obj) for _ in range(cnt)])

# 多头注意力
class MultiHeadAttention(nn.Module):
    def __init__(self,d_model,head,dropout_p=0.1):
        # 1- 确保张量维度能够被头数整除
        assert d_model%head==0

        # 2- 初始化父类
        super().__init__()

        # 3- 设置属性值
        self.d_model = d_model  # 向量维度
        self.head = head        # 多头的头数
        self.head_dim = d_model // head # 每个头要各自处理的向量维度。例如:512//8=64
        self.dropout = nn.Dropout(p=dropout_p)
        self.weighted = None    # 权重矩阵

        # 4- 创建线性网络结构
        """
            创建的4个线性层的作用:
                第1个线性层:分头行动前,专门用来处理query数据
                第2个线性层:分头行动前,专门用来处理key数据
                第3个线性层:分头行动前,专门用来处理value数据
                第4个线性层:分头行动后,对多个头拼接后的张量数据进行线性调整,对形状、元素值进行调整,避免出现异常值,让模型训练更加稳定
        """
        self.linear_list = clones(nn.Linear(in_features=d_model,out_features=d_model), 4)

    def forward(self,query,key,value,mask:Tensor=None):
        """
        先分头,然后再调用注意力计算方法
        :param query: 查询张量,形状:[batch_size每个批次中有多少条句子,seq_len句子中词的个数,d_model向量维度]
        :param key: 键张量,形状:[batch_size,seq_len,d_model]
        :param value: 值张量,形状:[batch_size,seq_len,d_model]
        :param mask: 掩码张量,形状:[head,seq_len,seq_len]。注意第一个维度代表的是头数,使用过程中会自动进行广播机制
        :return:
        """

        # 1- 掩码升维处理:[batch_size,seq_len,seq_len] -> [1,batch_size,seq_len,seq_len]
        # 添加的形状虽然是1,但是内部可以自动进行广播机制
        if mask is not None:
            mask.unsqueeze_(dim=0)

        # 2- 获得batch_size的值
        batch_size = query.shape[0]

        # 3- 前3个线性层分别用来处理query、key、value
        # 方式一:分开版的写法
        # 格式[(第1个线性层, query), (第2个线性层, key), (第3个线性层, value)]
        # model_and_data_pair = list(zip(self.linear_list, (query,key,value)))
        # transpose_output_list = []
        # for model,data in model_and_data_pair:
        #     # 线性层对对应的数据进行处理
        #     model_output = model(data)
        #
        #     # 调整形状:先分头,然后交换位置
        #     # [2,4,512] -> [2,4,8,64] -> [2,8,4,64]
        #     transpose_output = model_output.reshape(batch_size,-1,self.head,self.head_dim).transpose(1,2)
        #     transpose_output_list.append(transpose_output)
        #
        # new_query, new_key, new_value = transpose_output_list

        # 方式二:合并版的写法
        new_query, new_key, new_value = [
            model(data).reshape(batch_size,-1,self.head,self.head_dim).transpose(1,2)
            for model,data in list(zip(self.linear_list, (query,key,value)))
        ]

        # 4- 多个头分别计算注意力
        C,weighted = attention(new_query, new_key, new_value, mask, dropout=self.dropout)
        self.weighted = weighted

        # 5- 对多个计算后的结果进行”拼接“,实际也就是要将张量的形状重新调整回[2,4,512]
        # [2,8,4,64] -> [2,4,8,64] -> [2,4,512]
        result = C.transpose(1,2).reshape(batch_size,-1,self.head*self.head_dim)

        # 6- 使用最后一个线性层对多头拼接后的数据进行调整
        return self.linear_list[-1](result)
  • 测试代码
def use_multi_head_attention():
    # 1- 调用词嵌入层和位置编码的数据
    pe_ebd = use_positional_encoding()

    multi_head_atten = MultiHeadAttention(d_model=512,head=8)

    # 2- 测试:编码器端 多头自注意力机制
    query=key=value=pe_ebd
    encoder_result = multi_head_atten(query,key,value)
    print(f"编码器端 多头自注意力机制:{encoder_result.shape}")

    # 3- 测试:解码器端 掩码多头自注意力机制
    query = key = value = pe_ebd
    mask = torch.triu(torch.ones(size=(8,4,4)), diagonal=1)
    decoder_result = multi_head_atten(query, key, value, mask)
    print(f"解码器端 掩码多头自注意力机制:{decoder_result.shape}")

前馈网络

概念:前馈网络用来增强模型的拟合能力,内部包含两个全连接层

  • 代码实现
# 前馈网络
class FeedForward(nn.Module):
    def __init__(self,d_model,enlarge_dim,dropout_p=0.1):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.d_model = d_model  # 输入和输出的向量维度
        self.enlarge_dim = enlarge_dim    # 放大的向量维度
        self.dropout_p = dropout_p    # 随机失活概率

        # 3- 搭建网络结构
        # 3.1- 第1层线性层:用来将输入的向量放大。例如:512->1024
        self.linear_1 = nn.Linear(in_features=self.d_model,out_features=self.enlarge_dim)

        # 3.2- Dropout随机失活层
        self.dropout = nn.Dropout(p=self.dropout_p)

        # 3.3- 第2层线性层:用来将放大后的向量进行浓缩变小。例如:1024->512
        self.linear_2 = nn.Linear(in_features=self.enlarge_dim,out_features=self.d_model)

    def forward(self,data):
        """
        不管传进来的是什么数据,只关心将数据先放大再缩小
        :param data:
        :return:
        """
        """
            为什么使用ReLU激活函数?
            答:
                1- 不是必须要使用ReLU,可以使用其他的
                2- 原始的Transformer论文中用的是ReLU,因为正半轴的导数值固定为1,计算公式简单效率高
        """
        return self.linear_2(self.dropout(torch.relu(self.linear_1(data))))

层归一化

作用

随着网络深度的增加,模型参数会出现过大或过小的情况,进而可能影响模型的收敛,因此进行规范化,将参数规范致某个特征范围内,辅助模型快速收敛。

代码实现

# 层归一化/规范化层:数据经过多层的处理以后,会出现异常值,可能会导致梯度消失或梯度爆炸。
# 因此通过层归一化的处理,将数据调整到正态(高斯)分布的情况,让模型训练稳定
class LayerNorm(nn.Module):
    def __init__(self,d_model):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.d_model = d_model  # 向量维度

        # 3- 定义线性回归公式y=kx+b中的k和b
        """
            1- 为什么k和b的个数与向量维度d_model相同?
            答:d_model表示的是向量维度,而向量维度中每个元素代表一个特征值,而每列特征有自己的权重,因此个数要相同

            2- 为什么要用nn.Parameter()包起来?
            答:
                2.1- 如果不用nn.Parameter()包起来,那么变量的值永远不会发生变化
                2.2- 如果用nn.Parameter()包起来,会受到optimizer.step()代码的更新,值会受到反向传播的调整
        """
        self.k = nn.Parameter(torch.ones(self.d_model))
        self.b = nn.Parameter(torch.zeros(self.d_model))

        # 4- 定义小常数:为了防止分母为0
        self.eps = 1e-6

    def forward(self,data:Tensor):
        """
        不关心传进来的data数据来自哪里,只负责对数据进行标准化处理
        :param data:
        :return:
        """
        # 1- 计算均值
        """
            参数解释:
                dim=-1:对最后一个维度进行计算
                keepdim=True:计算后的张量维度数不变。例如:之前是3维,那么计算后也是3维,举例:[2,4,512] -> [2,4,1]
        """
        mean = data.mean(dim=-1,keepdim=True)

        # 2- 计算标准差
        std = data.std(dim=-1,keepdim=True)

        # 3- 对数据进行标准化处理:也就是实现 y=k((x-mean)/std)+b
        """
            为什么要自己实现线性回归的过程,而不使用nn.Linear来实现?
            答:因为我们是对每个批次的数据进行单独的线性回归的处理,然后每个批次数据的权重k和偏置b是不相同的
            这块内容的依据:https://arxiv.org/pdf/1607.06450论文的【3 Layer normalization】
        """
        return self.k * ((data-mean)/(std+self.eps)) + self.b

子层连接结构

image-20251105090807977

  • 代码实现
# 子层连接对象
"""
    覆盖的组合有如下这些:
        编码器端
            多头自注意力子层
                sublayer_obj多头注意力类的实例对象 + 残差连接 + 层归一化
            前馈网络子层
                sublayer_obj前馈网络类的实例对象 + 残差连接 + 层归一化

        解码器端
            掩码多头自注意力子层
                sublayer_obj多头注意力类的实例对象 + 残差连接 + 层归一化
            多头注意力子层
                sublayer_obj多头注意力类的实例对象 + 残差连接 + 层归一化
            前馈网络子层
                sublayer_obj前馈网络类的实例对象 + 残差连接 + 层归一化
"""
class SublayerConnection(nn.Module):
    def __init__(self,d_model,dropout_p=0.2):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.d_model = d_model
        self.dropout_p = dropout_p

        # 3- 创建层归一化对象
        """
            为什么要创建层归一化对象?
            答:因为每个子层都需要有层归一化对象
        """
        self.layer_norm = LayerNorm(d_model=self.d_model)

        # 4- 创建随机失活层
        self.dropout = nn.Dropout(p=self.dropout_p)

    def forward(self, data, sublayer):
        """
        :param data: 子层需要处理的数据
        :param sublayer: 子层中具体的处理对象的前向传播,例如:多头注意力类的实例对象、前馈网络类的实例对象
        :return:
        """

        # 写法一:Transformer原版论文中的写法, 子组件处理 -> 随机失活层 -> 残差连接 -> 层归一化
        """
            子组件处理:sublayer_obj(data),底层自动调用我们自己写的forward前向传播方法
            随机失活层:self.dropout(sublayer_obj(data))
            残差连接:+ data
            层归一化:self.layer_norm
        """
        # result = self.layer_norm(self.dropout(sublayer_obj(data)) + data)

        # 写法二:推荐。目前主流大模型中的写法, 层归一化 -> 子组件处理 -> 随机失活层 -> 残差连接
        """
            层归一化:self.layer_norm(data)
            子组件处理:sublayer_obj(self.layer_norm(data)),底层自动调用我们自己写的forward前向传播方法
            随机失活层:self.dropout
            残差连接:+ data

            为什么目前主流大模型将层归一化放在了最前面?
            答:放在最前面是为了将之前处理得到的数据调整到符合正态分布的情况,避免出现异常值,让模型训练稳定
        """
        result = self.dropout(sublayer(self.layer_norm(data))) + data

        return result
  • 子层测试代码
# 测试子层连接对象
def use_SublayerConnection():
    # 1- 数据首先经过词嵌入层和位置编码的处理
    data = use_positional_encoding()
    d_model = 512
    # 2- 多头注意力子层测试
    # 2.1- 创建子组件类的实例对象,并且调用forward前向传播方法。也就是创建sublayer_obj
    # 方式一:使用自定义函数的方式实现
    def sublayer_forward_fn(input_data):
        # 创建子组件类的实例对象
        atten_obj = MultiHeadAttention(d_model,head=8)
        # 调用forward前向传播方法
        return atten_obj(query=input_data,key=input_data,value=input_data)

    # 2.2- 创建SublayerConnection类的实例对象
    sublayerConnection = SublayerConnection(d_model)

    # 2.3- 调用SublayerConnection中的前向传播方法
    # 注意:只能传递方法名称,不能带小括号
    atten_result = sublayerConnection(data, sublayer_forward_fn)
    print(f"演示 编码器端 多头自注意力子层:{atten_result.shape}")

    # 3- 前馈网络子层测试
    # 3.1- 创建SublayerConnection类的实例对象
    sublayerConnection = SublayerConnection(d_model)

    # 3.2- 调用SublayerConnection中的前向传播方法
    # 方式二:使用lambda表达式的方式实现
    ff_result = sublayerConnection(atten_result, lambda input_data:FeedForward(d_model,enlarge_dim=1024)(data=input_data))
    print(f"演示 编码器端 前馈网站子层:{ff_result.shape}")

image-20260409115629734

编码器层

作用

每个编码器层完成一次对输入的特征提取过程, 即编码过程.

代码实现

"""
    编码器 由 6层编码器层 组成,就是隐藏层有6层,它们的关系是串行的关系
        每个编码器层由如下的2个子层组成
            多头自注意力子层:多头自注意力 + 残差连接 + 层归一化
            前馈网络子层:前馈网络 + 残差连接 + 层归一化
"""

from demo02_component import *

class EncoderLayer(nn.Module):
    def __init__(self,d_model,dropout_p,feed_forward_obj, multi_head_self_atten_obj):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.d_model = d_model
        self.dropout_p = dropout_p
        self.feed_forward_obj = feed_forward_obj                    # FeedForward类的实例对象
        self.multi_head_self_atten_obj = multi_head_self_atten_obj  # MultiHeadAttention类的实例对象

        # 3- 创建 多头自注意力子层:多头自注意力 + 残差连接 + 层归一化
        self.multi_head_self_atten_sublayer = SublayerConnection(d_model=self.d_model, dropout_p=self.dropout_p)

        # 4- 创建 前馈网络子层:前馈网络 + 残差连接 + 层归一化
        self.feed_forward_sublayer = SublayerConnection(d_model=self.d_model, dropout_p=self.dropout_p)

    def forward(self, data):

        # 1- 先调用 多头自注意力子层
        atten_result = self.multi_head_self_atten_sublayer(
            data,
            lambda input_data: self.multi_head_self_atten_obj(query=input_data,key=input_data,value=input_data)
        )

        # 2- 再调用 前馈网络子层
        ff_result = self.feed_forward_sublayer(
            atten_result,
            lambda input_data: self.feed_forward_obj(data=input_data)
        )

        return ff_result

image-20260409150440559

编码器

  • 代码实现
class Encoder(nn.Module):
    def __init__(self,encoder_layer_obj,N=6):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.encoder_layer_obj = encoder_layer_obj  # EncoderLayer类的实例对象
        self.N = N  # 编码器层堆叠N层,默认是6

        # 3- 创建N个编码器层
        self.encoder_layer_list = clones(self.encoder_layer_obj, self.N)

        # 4- 【可选】创建层归一化:防止前面的网络层处理以后出现异常值
        self.layer_norm = LayerNorm(d_model=encoder_layer_obj.d_model)

    def forward(self,data):

        # 1- 依次也就是串行调用6层编码器层
        for layer_obj in self.encoder_layer_list:
            # 注意:处理前后的数据变量名称【必须】保持一致,如果不一致,那么6层编码器层只有最后一层才有价值
            data = layer_obj(data)

        # 2- 调用层归一化
        result = self.layer_norm(data)

        return result
  • 编码器完整测试代码
# 测试编码器
def use_encoder():
    # 1- 调用词嵌入层和位置编码
    pe_ebd = use_positional_encoding()

    d_model = 512
    dropout_p = 0.1

    # 2- 创建编码器实例对象
    # 2.1- 创建 多头自注意力 实例对象
    atten_obj = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout_p)
    # 2.2- 创建 前馈网络 实例对象
    ff_obj = FeedForward(d_model=d_model,enlarge_dim=1024,dropout_p=dropout_p)
    # 2.3- 创建 编码器层 实例对象
    encoder_layer_obj = EncoderLayer(d_model=d_model,dropout_p=dropout_p,feed_forward_obj=ff_obj,multi_head_self_atten_obj=atten_obj)
    # 2.4- 创建 编码器 实例对象
    encoder_obj = Encoder(encoder_layer_obj=encoder_layer_obj, N=6)

    # 3- 调用编码器的前向传播
    result = encoder_obj(data=pe_ebd)

    print(f"编码器处理后的结果:{result.shape}")

    return result

解码器部分

解码器层

作用

作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作

代码实现

"""
    解码器 由 6层解码器层 组成。6层解码器层之间是串行的关系

    解码器层由如下3个子层组成
        掩码多头自注意力子层:掩码多头自注意力mask_multi_head_self_atten_obj + 残差连接 + 层归一化
        多头注意力子层(交叉注意力子层):多头注意力multi_head_atten_obj + 残差连接 + 层归一化
        前馈网络子层:前馈网络feed_forward_obj + 残差连接 + 层归一化
"""

from demo03_encoder import *

# 解码器层
class DecoderLayer(nn.Module):
    def __init__(self,d_model, dropout_p, mask_multi_head_self_atten_obj, multi_head_atten_obj, feed_forward_obj):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.d_model = d_model
        self.dropout_p = dropout_p
        self.mask_multi_head_self_atten_obj = mask_multi_head_self_atten_obj    # MultiHeadAttention类的实例对象,掩码多头自注意力
        self.multi_head_atten_obj = multi_head_atten_obj                        # MultiHeadAttention类的实例对象,多头注意力
        self.feed_forward_obj = feed_forward_obj                                # FeedForward类的实例对象

        # 3- 创建 掩码多头自注意力子层:多头自注意力 + 残差连接 + 层归一化
        self.mask_multi_head_self_atten_sublayer = SublayerConnection(d_model=self.d_model, dropout_p=self.dropout_p)

        # 4- 创建 多头注意力子层(交叉注意力子层):多头注意力multi_head_atten_obj + 残差连接 + 层归一化
        self.multi_head_atten_sublayer = SublayerConnection(d_model=self.d_model, dropout_p=self.dropout_p)

        # 5- 创建 前馈网络子层:前馈网络 + 残差连接 + 层归一化
        self.feed_forward_sublayer = SublayerConnection(d_model=self.d_model, dropout_p=self.dropout_p)

    def forward(self, data, mask, encoder_output):
        """
        :param data: 经过Decoder解码器中词嵌入和位置编码处理后的数据
        :param mask: 掩码张量,形状[head, seq_len, seq_len]
        :param encoder_output: 编码器(也就是由6层编码器层组成)最终的输出结果
        :return:
        """

        # 1- 先调用 掩码多头自注意力子层
        mask_atten_result = self.mask_multi_head_self_atten_sublayer(
            data,
            lambda input_data: self.mask_multi_head_self_atten_obj(query=input_data,key=input_data,value=input_data,mask=mask)
        )

        # 2- 再调用 多头注意力子层
        atten_result = self.multi_head_atten_sublayer(
            mask_atten_result,
            lambda input_data: self.multi_head_atten_obj(query=input_data, key=encoder_output, value=encoder_output)
        )

        # 3- 最后调用 前馈网络子层
        ff_result = self.feed_forward_sublayer(
            atten_result,
            lambda input_data: self.feed_forward_obj(data=input_data)
        )

        return ff_result

解码器

作用

根据编码器的结果以及上一次预测的结果, 对下一次可能出现的'值'进行特征表示
  • 代码实现
# 解码器
class Decoder(nn.Module):
    def __init__(self,decoder_layer_obj,N=6):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.decoder_layer_obj = decoder_layer_obj  # DecoderLayer类的实例对象
        self.N = N  # 解码器层堆叠N层,默认是6

        # 3- 创建N个解码器层
        self.decoder_layer_list = clones(self.decoder_layer_obj, self.N)

        # 4- 【可选】创建层归一化:防止前面的网络层处理以后出现异常值
        self.layer_norm = LayerNorm(d_model=decoder_layer_obj.d_model)

    def forward(self,data):

        # 1- 依次也就是串行调用6层解码器层
        for layer_obj in self.decoder_layer_list:
            # 注意:处理前后的数据变量名称【必须】保持一致,如果不一致,那么6层解码器层只有最后一层才有价值
            data = layer_obj(data)

        # 2- 调用层归一化
        result = self.layer_norm(data)

        return result
  • 解码器完整测试代码
def use_decoder():
    # 1- 调用编码器
    encoder_output = use_encoder()

    d_model = 512
    dropout_p = 0.1

    # 2- 准备解码器端的数据,例如:英译法中法语句子
    # 当前批次中有2条法语句子,每条法语句子中有5个法语词
    y = torch.tensor([
        [1,2,3,4,9],
        [5,6,7,8,9]
    ])

    # 3- 创建解码器端各个类的实例对象
    # 3.1- 词嵌入层
    embed = Embedding(vocab_size=1200,d_model=d_model)
    # 3.2- 位置编码
    pe = PositionalEncoding(dropout_p=dropout_p,max_len=60,d_model=d_model)
    # 3.3- 掩码多头自注意力
    mask_self_atten = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout_p)
    # 3.4- 多头注意力
    atten = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout_p)
    # 3.5- 前馈网络
    feedforward = FeedForward(d_model=d_model,enlarge_dim=1024,dropout_p=dropout_p)

    # 3.6- 组装得到解码器层
    decoer_layer = DecoderLayer(
        d_model=d_model,
        dropout_p=dropout_p,
        mask_multi_head_self_atten_obj=mask_self_atten,
        multi_head_atten_obj=atten,
        feed_forward_obj=feedforward
    )

    # 3.7- 解码器层堆叠6层得到解码器
    decoder = Decoder(decoder_layer_obj=decoer_layer, N=6)

    # 4- 调用解码器的前向传播
    # 4.1- 准备数据
    # 解码器端的数据 经过词嵌入层、位置编码的处理
    pe_ebd = pe(embed(y))
    # 掩码张量
    mask = torch.triu(torch.ones(size=(8,5,5)), diagonal=1)

    # 4.2- 前向传播
    decoder_result = decoder(pe_ebd,mask,encoder_output)

    print(f"解码器最终输出结果:{decoder_result.shape}")

输出部分

作用:通过线性变化得到指定维度的输出
  • 代码实现
"""
    Transformer的输出部分:
        Linear线性层和Softmax激活函数
"""
import torch
import torch.nn as nn

class Output(nn.Module):
    def __init__(self,d_model,vocab_size):
        # 1- 初始化父类
        super().__init__()

        # 2- 搭建神经网络结构
        self.linear = nn.Linear(in_features=d_model,out_features=vocab_size)

    def forward(self,data):
        """
        :param data: 解码器最终的输出结果,形状[batch_size,seq_len,d_model]
        :return:
        """

        return torch.softmax(self.linear(data),dim=-1)

模型搭建

  • 自定义的Transformer模型搭建代码
import torch

from demo04_decoder import *
from demo05_output import *

class MyTransformer(nn.Module):
    def __init__(self, encoder_ebd_pos, encoder, decoder_ebd_pos, decoder, output):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.encoder_ebd_pos = encoder_ebd_pos
        self.encoder = encoder
        self.decoder_ebd_pos = decoder_ebd_pos
        self.decoder = decoder
        self.output = output

    def forward(self, encoder_input, decoder_input, mask):
        # 1- 编码器端_输入部分:encoder_ebd_pos
        encoder_data = self.encoder_ebd_pos(encoder_input)
        # 2- 编码器:encoder
        encoder_data = self.encoder(encoder_data)

        # 3- 解码器端_输入部分:decoder_ebd_pos
        decoder_data = self.decoder_ebd_pos(decoder_input)
        # 4- 解码器:decoder
        decoder_data = self.decoder(data=decoder_data, mask=mask, encoder_output=encoder_data)

        # 5- 输出部分:output
        return self.output(decoder_data)
  • 调用Transformer模型框架
# 调用Transformer框架
def get_mytransformer():
    # 基础变量
    d_model = 512
    dropout_p = 0.1
    encoder_vocab_size = 1000   # 编码器端的词汇表大小
    decoder_vocab_size = 4345   # 解码器端的词汇表大小

    # 1------------------------------ 编码器 ------------------------------
    # 1.1- 词嵌入层 实例对象
    encoder_ebd = Embedding(vocab_size=encoder_vocab_size,d_model=d_model)
    # 1.2- 位置编码 实例对象
    encoder_pe = PositionalEncoding(dropout_p=dropout_p,max_len=60,d_model=d_model)
    # 1.3- 多头自注意力机制 实例对象
    encoder_multi_head_self_atten = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout_p)
    # 1.4- 前馈网络 实例对象
    encoder_ff = FeedForward(d_model=d_model,enlarge_dim=1024,dropout_p=dropout_p)
    # 1.5- 编码器层 实例对象
    encoder_layer = EncoderLayer(
        d_model=d_model,
        dropout_p=dropout_p,
        feed_forward_obj=encoder_ff,
        multi_head_self_atten_obj=encoder_multi_head_self_atten
    )
    # 1.6- 编码器 实例对象
    encoder = Encoder(encoder_layer_obj=encoder_layer, N=6)

    # 2------------------------------ 解码器 ------------------------------
    # 2.1- 词嵌入层 实例对象
    decoder_ebd = Embedding(vocab_size=decoder_vocab_size,d_model=d_model)
    # 2.2- 位置编码 实例对象
    decoder_pe = PositionalEncoding(dropout_p=dropout_p,max_len=60,d_model=d_model)
    # 2.3- 掩码多头自注意力机制 实例对象
    decoder_mask_multi_head_self_atten = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout_p)
    # 2.4- 多头注意力机制 实例对象
    decoder_multi_head_atten = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout_p)
    # 2.5- 前馈网络 实例对象
    decoder_ff = FeedForward(d_model=d_model,enlarge_dim=1024,dropout_p=dropout_p)
    # 2.6- 解码器层 实例对象
    decoder_layer = DecoderLayer(
        d_model=d_model,
        dropout_p=dropout_p,
        mask_multi_head_self_atten_obj=decoder_mask_multi_head_self_atten,
        multi_head_atten_obj=decoder_multi_head_atten,
        feed_forward_obj=decoder_ff
    )
    # 2.7- 解码器 实例对象
    decoder = Decoder(decoder_layer_obj=decoder_layer, N=6)

    # 3------------------------------ 输出端 ------------------------------
    output = Output(d_model=d_model,vocab_size=decoder_vocab_size)

    # 4--------------------------- Transformer ---------------------------
    """
        nn.Sequential:按顺序执行。可以将多个实例对象,而且这些实例对象之间如果有串行执行的关系,才能够放进去
        另外,这里你可以不使用nn.Sequential,可以拆解为多个变量传进去
    """
    my_transformer = MyTransformer(
        encoder_ebd_pos=nn.Sequential(encoder_ebd, encoder_pe),
        encoder=encoder,
        decoder_ebd_pos=nn.Sequential(decoder_ebd, decoder_pe),
        decoder=decoder,
        output=output
    )

    return my_transformer
  • 测试Transformer模型架构
# 测试Transformer模型架构
def use_my_transformer():
    # 1- 创建MyTransformer
    my_transformer = get_mytransformer()
    print("模型内部结构:",my_transformer)

    # 2- 准备数据
    # 2.1- 编码器端数据
    x = torch.tensor([
        [11, 22, 33, 44],
        [1, 2, 3, 4]
    ])

    # 2.2- 解码器端数据
    y = torch.tensor([
        [134,3546,2455,234],
        [435,2345,578,23]
    ])

    # 2.3- 掩码
    mask = torch.triu(torch.ones(size=(8,4,4)), diagonal=1)

    # 3- 调用前向传播
    result = my_transformer(encoder_input=x, decoder_input=y, mask=mask)
    print(f"Transformer最终结果:{result.shape}")
    print(f"Transformer最终结果:{result.sum()}")
💻 代码

Transformer 手写实现代码

7 个文件
  • 📄 demo01_input.py 5.9KB
    """
        Transformer的输入部分:包含如下内容
            词嵌入层:输入词索引,得到词向量
            位置编码
    """
    import math
    import torch
    import torch.nn as nn
    import matplotlib.pyplot as plt
    import numpy as np
    
    class Embedding(nn.Module):
        def __init__(self,vocab_size,d_model):
            # 1- 初始化父类
            super().__init__()
    
            # 2- 设置属性值
            self.vocab_size = vocab_size    # 词汇表大小
            self.d_model = d_model  # 词向量的维度。例如:512
    
            # 3- 搭建网络结构:只有一个词嵌入层
            self.embed = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.d_model)
    
        def forward(self,input):
            """
            前向传播。输入一条句子,得到词向量
            :param input: 一条句子,里面的元素是词索引。张量形状[batch_size每个批次中句子的条数,seq_len每条句子中词的个数]
            :return: 词向量
            """
    
            """
                为什么要乘以math.sqrt(self.d_model),也就是根号dk?
                答:为了对数据进行缩放,避免与位置编码的数据值之间的大小差异过大。为了让模型训练稳定,也就是缓解梯度消失或梯度爆炸
                    词向量维度越大,越容易出现极小值
            """
            return self.embed(input) * math.sqrt(self.d_model)
    
    def use_embedding():
        # 1- 创建词嵌入层类的实例对象
        my_embed = Embedding(vocab_size=1000,d_model=5)
        # my_embed = Embedding(vocab_size=1000,d_model=10240)
    
        # 2- 准备数据
        # 注意:目前情况下,词索引的取值区间[0,999]
        x = torch.tensor([
            # 单词索引
            [100, 2, 666],
            [500, 888, 421]
        ])
    
        # 3- 输入句子,得到词向量
        word_vector = my_embed(x)
        print(f"词向量的形状{word_vector.shape}") # 2条句子,3句子中3个词,5词向量维度
        print(f"词向量的数据{word_vector}")
        print(f"词向量的数据{word_vector.abs().min()}")   # 获得乘以根号dk以后数据中绝对值的最小值
    
    # 位置编码
    class PositionalEncoding(nn.Module):
        def __init__(self,d_model,dropout,max_len=60):
            """
            位置编码初始化方法
            :param d_model: 词向量维度。例如:512
            :param dropout: 神经元随机失活概率
            :param max_len: 能够处理的句子最大长度。也就是词的个数
            """
    
            # 1- 初始化父类
            super().__init__()
    
            # 2- 创建随机失活层
            self.dropout = nn.Dropout(p=dropout)
    
            # 3- 定义pe(也就是位置编码张量)
            pe = torch.zeros(size=(max_len,d_model))    # 目前的形状[60,d_model]
    
            # 4- 定义列向量,用来存储输入句子中词索引信息
            position = torch.arange(0,max_len).unsqueeze(1) # 形状[60,1]
    
            # 5- 得到pos/分母公式中的,1/分母
            div_term = 1/(10000**(torch.arange(0,d_model,2).float()/d_model))
    
            # 6- 计算pos/分母的结果
            position_value = position * div_term
    
            # 7- 调用sin、cos分别计算位置编码值
            pe[:,0::2] = torch.sin(position_value)  # 偶数维度
            pe[:,1::2] = torch.cos(position_value)  # 奇数维度
    
            # 8- 调整pe的形状变成3维,也就是[60,d_model]->[1,60,d_model]每个批次1条句子,每个句子最多60个词,词向量是d_model
            pe = pe.unsqueeze(0)
    
            # 9- 将pe的值注册到缓存中,通过它来不断的更新位置编码信息。后面可以直接通过self.pe调用
            self.register_buffer("pe",pe)
    
        def forward(self,embed):
            """
            位置编码的前向传播:多条句子中所有词的词向量,和位置编码张量进行求和操作,返回结果
            :param embed: 词向量
            :return:
            """
    
            """
                embed.shape[1]:embed的形状[batch_size,seq_len,d_model],得到多少个词
                为什么self.pe[:, :embed.shape[1]]?
                如果这条句子中词的个数超过了max_len,那么最多只对前max_len个词的位置编码进行相加
            """
            result = embed + self.pe[:, :embed.shape[1]]
            # print(f"对应的位置编码值:{self.pe[:, :embed.shape[1]]}")
            return self.dropout(result)
    
    def use_positional_encoding():
        d_model = 512
    
        # 实例化词嵌入层类的对象
        my_embed = Embedding(vocab_size=1000,d_model=d_model)
    
        # 准备数据
        x = torch.tensor([
            # 单词索引
            [100, 2, 421, 600],
            [500, 888, 421, 615]
        ])
    
        # 输入数据,得到对应的词向量
        word_embed = my_embed(x)
        # print(f"词向量的形状:{word_embed.shape}")
        # print(f"词向量的值:{word_embed}")
    
        # 创建位置编码
        my_pe = PositionalEncoding(d_model=d_model,dropout=0.1,max_len=60)
    
        # 调用位置编码,最终效果是往词向量中加上了位置编码的值
        result = my_pe(word_embed)
        # print(f"最终的形状:{result.shape}")
        # print(f"最终的值:{result}")
    
        return result
    
    # 可视化位置编码
    def plot_position():
        # 1. 实例化位置编码器.
        my_position = PositionalEncoding(20, dropout=0.1, max_len=100)
    
        # 2. 生成全0的输入, 观察位置编码的模式.
        # (1, 100, 20) -> 批次大小, 句子长度, 词嵌入维度
        embed = torch.zeros(1, 100, 20)
        y = my_position(embed)
    
        # 3. 设置图表大小.
        plt.figure(figsize=(20, 15))
        # 绘制位置编码第4到第7列, 100个词的  [4, 5, 6, 7]
        """
            图形的信息解释:
                x轴:词的索引,目前总共有100个词
                y轴:位置编码值
        """
        plt.plot(np.arange(100), y[0, :, 4:8].detach().numpy())
        plt.legend([f'dim {p}' for p in [4, 5, 6, 7]])
        plt.show()
    
    if __name__ == '__main__':
        # use_embedding()
        use_positional_encoding()
    
        # plot_position()
    
  • 📄 demo02_component.py 14.7KB
    from torch import Tensor
    from demo01_input import *
    import copy
    
    def attention(query:Tensor,key:Tensor,value:Tensor,mask:Tensor=None,dropout=None):
        """
        注意力计算方法。能够涵盖如下几种情况:
            1- 编码器端的多头自注意力:query=key=value,mask为None
            2- 解码器端的掩码多头自注意力:query=key=value,mask不为None
            3- 解码器端的多头注意力(交叉注意力):query和key、value不等,但是key和value相同,mask为None
        :param query: 查询张量,形状:[batch_size每个批次中有多少条句子,seq_len每条句子中有几个词,d_model词向量的维度/隐藏状态维度]
        :param key: 键张量,形状:[batch_size,seq_len,d_model]
        :param value: 值张量,形状:[batch_size,seq_len,d_model]
        :param dropout: 随机失活Dropout层对象
        :param mask: 掩码张量,形状:[batch_size,seq_len,seq_len]
        :return: 专属信息包,权重张量。类型是元组
        """
    
        # 1- 获得d_k:词向量的维度
        d_k = query.shape[-1]
    
        # 2- Q和K的转置相乘;再除以根号d_k。得到相似性得分scores
        # K的转置:由  [batch_size,seq_len,d_model]  变成了  [batch_size,d_model,seq_len]
        scores = torch.matmul(query,key.transpose(-2,-1)) / math.sqrt(d_k)
    
        # 3- 可选:对相似性得分scores进行掩码处理
        if mask is not None:
            # 对需要进行掩码的地方,将值替换成-1e9。注意:不要直接设置为0。需要和softmax结合理解。e的-1e9的结果趋近于0,表示权重趋近于0
            scores = scores.masked_fill(mask==0,value=-1e9)
    
        # 4- 将相似性转成权重
        weight = torch.softmax(scores,dim=-1)
    
        # 5- 可选:随机时候,缓解过拟合
        if dropout is not None:
            weight = dropout(weight)
    
        # 6- 权重和value进行矩阵乘法,得到专属信息包
        C = torch.matmul(weight,value)
    
        return C,weight
    
    def use_attention():
        # 1- 输入的数据先经过 词嵌入层 和 位置编码
        posi_embed = use_positional_encoding()
    
        # 2- 调用注意力计算方法
        # 2.1- 准备query、key、value参数
        query=key=value=posi_embed
    
        # 2.2- 准备掩码【可选】
        # (2,4,4)值的来源于use_positional_encoding的x。每个批次2条句子,每条句子4个词
        # 形状:[batch_size,seq_len,seq_len]
        mask = torch.triu(torch.ones(size=(2,4,4)))
    
        # 2.3- 随机失活网络层
        dropout = nn.Dropout(p=0.1)
    
        # 2.4- 调用
        # 2.4.1- 编码器端:没有掩码的多头自注意力
        encoder_attention_C, encoder_attention_weight = attention(query,key,value,dropout=dropout)
        print(f"编码器C:{encoder_attention_C.shape}-->{encoder_attention_C}")
        print(f"编码器C权重:{encoder_attention_weight.shape}-->{encoder_attention_weight}")
    
        # 2.4.2- 解码器端:有掩码的多头自注意力
        decoder_attention_C, decoder_attention_weight = attention(query,key,value,dropout=dropout,mask=mask)
        print(f"解码器C:{decoder_attention_C.shape}-->{decoder_attention_C}")
        print(f"解码器C权重:{decoder_attention_weight.shape}-->{decoder_attention_weight}")
    
    def clones(model_obj, nums):
        """
        创建指定个数的相同网络结构对象
        :param model_obj: 网络结构对象
        :param nums: 个数
        :return: 网络结构对象列表
        """
        # 语法 nn.ModuleList([copy.deepcopy(模型对象) for _ in range(深拷贝复制的个数)])
        return nn.ModuleList([copy.deepcopy(model_obj) for _ in range(nums)])
    
    class MultiHeadAttention(nn.Module):
        def __init__(self,d_model,head,dropout_p=0.1):
            """
            初始化
            :param d_model: 词向量维度/隐藏层隐藏状态向量维度。例如:512
            :param head: 多头的头数。例如:8
            :param dropout_p: 随机失活概率
            """
    
            # 1- 确定d_model能够被head整除
            assert d_model%head==0
    
            # 2- 初始化父类
            super().__init__()
    
            # 3- 设置属性值
            self.d_model = d_model
            self.head = head
            self.head_dim = self.d_model // self.head   # 每个头分别处理的数据维度。例如:64
            self.dropout = nn.Dropout(p=dropout_p)
    
            # 4- 搭建网络结构
            """
                4个线性层的作用如下:
                    第1个线性层:专门用来query进行线性处理,决定“当前词在找什么东西”
                    第2个线性层:专门用来key进行线性处理,决定“其他词能够提供什么信息”
                    第3个线性层:专门用来value进行线性处理,决定“其他词的信息的重要程度是什么样的”
                    第4个线性层:对多头并行处理,并且concat拼接后的张量做最终的线性处理,让数据变得更加平稳
            """
            self.linear_list = clones(nn.Linear(in_features=self.d_model,out_features=self.d_model), 4)
    
            # 5- 权重张量
            self.weight = None
    
        def forward(self,query:Tensor,key:Tensor,value:Tensor,mask:Tensor=None):
            """
            前向传播:多头注意力计算
            :param query: 查询张量,形状:[batch_size每个批次中有多少条句子,seq_len每条句子中有几个词,d_model词向量的维度/隐藏状态维度]
            :param key: 键张量,形状:[batch_size,seq_len,d_model]
            :param value: 值张量,形状:[batch_size,seq_len,d_model]
            :param mask: 掩码张量,形状:[head,seq_len,seq_len]。注意第一个维度代表的是头数
            :return:
            """
    
            # 1- 掩码处理:进行升维,3维变4维
            if mask is not None:
                # 例如:[8,4,4] -> [1,8,4,4]
                # 不管每个批次中有多少条句子,每条句子用的掩码是同一份
                mask = mask.unsqueeze(0)
    
            # 2- 获得batch_size,也就是批次中句子的条数
            batch_size = query.shape[0]
    
            # 3- 前3个线性层分别并行对QKV进行处理
            # 方式一:分开版
            # [(Linear,query), (Linear,key), (Linear,value)]
            # linear_output_list = []
            # model_and_data_list = list(zip(self.linear_list, (query,key,value)))
            # for model,data in model_and_data_list:
            #     """
            #         1- model(data):线性变换
            #         2- reshape:[2,4,512]->[2,4,8,64]
            #         3- transpose:[2,4,8,64]->[2,8,4,64]
            #     """
            #     model_output = model(data)
            #     reshape_output = model_output.reshape(batch_size,-1,self.head,self.head_dim)
            #     linear_output_list.append(reshape_output.transpose(1,2))
            #
            # new_query, new_key, new_value = linear_output_list
    
            # 方式二:合并版【理解】
            new_query, new_key, new_value = [
                model(data).reshape(batch_size,-1,self.head,self.head_dim).transpose(1,2)
                for model,data in list(zip(self.linear_list, (query,key,value)))
            ]
    
            # 4- 多头并行计算注意力
            C,weight = attention(new_query, new_key, new_value,mask=mask,dropout=self.dropout)
            self.weight = weight
    
            # 5- 对多头处理后的数据进行拼接
            # [2,8,4,64] -> [2,4,8,64] -> [2,4,512]
            result = C.transpose(1,2).reshape(batch_size,-1,self.head*self.head_dim)
    
            # 6- 调用最后一个线性层对拼接后的数据进行处理,让模型更加稳定
            return self.linear_list[-1](result)
    
    # 测试多头注意力计算
    def use_multi_head_attention():
        # 1- 获取位置编码之后的词数据
        position_data = use_positional_encoding()
    
        # 2- query、key、value参数
        query=key=value=position_data
    
        # 3- 创建多头注意力实例对象
        mask = torch.triu(torch.ones(size=(8, 4, 4)))
        my_attention = MultiHeadAttention(d_model=512,head=8,dropout_p=0.1)
    
        # 4- 调用前向传播
        result = my_attention(query,key,value,mask=mask)
        print(f"多头注意力计算结果:{result.shape}")
        print(f"多头注意力计算结果:{result}")
    
        return result
    
    # 前馈网络/前馈全连接层:通过调整张量大小,实现强化信息的过程
    class FeedForward(nn.Module):
        def __init__(self,d_model,output_dim,dropout_p=0.1):
            # 1- 初始化父类
            super().__init__()
    
            # 2- 搭建网络结构
            # 2.1- 第一个线性层:用来对输入的词向量维度进行放大,例如:512->1024
            self.linear_1 = nn.Linear(in_features=d_model,out_features=output_dim)
    
            # 2.2- 随机失活层
            self.dropout = nn.Dropout(p=dropout_p)
    
            # 2.3- 第二个线性层:用来对上一个线性层输出的结果进行浓缩,例如:1024->512
            self.linear_2 = nn.Linear(in_features=output_dim, out_features=d_model)
    
        def forward(self,data):
            # 1- 调用第一个线性层
            data = self.linear_1(data)
    
            # 2- 调用随机失活
            """
                注意力机制本质是线性计算,如果没有非线性因素,那么整个网络模型比较简单,只能处理线性问题(也就是回归)
                引入relu激活也就是引入了非线性因素;Transformer论文中用的就是relu
            """
            data = self.dropout(torch.relu(data))
    
            # 3- 调用第二个线性层
            data = self.linear_2(data)
    
            return data
    
    # 层归一化/规范化层:随着网络模型训练,数据有可能出现极值,导致梯度爆炸或消失,为了让模型训练稳定,通过标准化处理,让数据变的正常
    # 也就是要实现y=kx+b,其中x要经过标准化处理(要计算数据的均值mean和标准差std)
    class LayerNorm(nn.Module):
        def __init__(self,d_model):
            # 1- 初始化父类
            super().__init__()
    
            # 2- 线性公式中参数的定义
            """
                为什么k和b要通过nn.Parameter进行定义?
                答:因为nn.Parameter会自动将k和b注册到神经网络模型中,作为可训练的参数。通过反向传播得到k和b。
                    如果不写nn.Parameter,那么k和b的值永远固定,不会发生变化。
                    对应前面神经网络模型代码中如下两行代码
                    optimizer = optim.Adam(params=model.parameters())
                    optimizer.step()
            """
            # 2.1- 定义k
            self.k = nn.Parameter(torch.ones(d_model))
    
            # 2.2- 定义b
            self.b = nn.Parameter(torch.zeros(d_model))
    
            # 3- 定义小常数:防止分母为0
            self.eps = 1e-6
    
        def forward(self,data):
            """
            前向传播:输入前面子层处理后的数据,经过层归一化进行【标准化处理】,让模型训练稳定
            要实现y=kx+b,其中x要经过标准化处理(要计算数据的均值mean和标准差std)
            :param data: 前面子层处理后的数据。形状[batch_size,seq_len,d_model]
            :return:
            """
    
            # 1- 计算均值
            """
                参数解释:
                    dim=-1:对最后一个维度计算均值
                    keepdim=True:表示计算后,张量的形状保留。举例:[2,4,512]->[2,4,1]
            """
            mean = data.mean(dim=-1,keepdim=True)
    
            # 2- 计算标准差
            std = data.std(dim=-1, keepdim=True)
    
            # 3- 实现y=kx+b
            return self.k * (data - mean)/(std+self.eps) + self.b
    
    # 子层连接类
    class SubLayerConnection(nn.Module):
        def __init__(self,d_model,dropout_p):
            # 1- 初始化父类
            super().__init__()
    
            # 2- 创建层归一化
            """
                为什么把LayerNorm创建在__init__中?
                答:因为每个子层的后面都有该层
            """
            self.layer_norm = LayerNorm(d_model)
    
            # 3- 创建随机失活层
            self.dropout = nn.Dropout(p=dropout_p)
    
        def forward(self,data,sublayer_obj):
            """
            前向传播:使用你指定的具体子层实例对象【sublayer_obj】对数据【data】进行指定的处理
            :param data: 输入到层中的数据
            :param sublayer_obj: 具体子层实例对象。例如:多头自注意力实例对象、前馈网络实例对象、掩码多头自注意力实例对象、多头注意力实例对象(交叉注意力)
            :return:
            """
    
            # 具体子层实例对象【sublayer_obj】对数据【data】进行指定的处理
            # 写法一:原始Transformer的实现
            # 子层处理(也就是调用forward) -> 随机失活层 -> 残差连接 -> 层归一化
            # result = self.layer_norm(self.dropout(sublayer_obj(data)) + data)
    
            # 写法二:目前主流大模型的实现
            # 层归一化 -> 子层处理(也就是调用forward) -> 随机失活层 -> 残差连接
            """
                第一步:层归一化        self.layer_norm(data)
                第二步:子层处理        sublayer_obj(self.layer_norm(data))
                第三步:随机失活层       self.dropout(sublayer_obj(self.layer_norm(data)))
                第四步:残差连接        + data
            """
            result = self.dropout(sublayer_obj(self.layer_norm(data))) + data
    
            return result
    
    # 子层连接测试
    def use_SubLayerConnection():
        # 1- 准备数据:词嵌入层+位置编码
        data = use_positional_encoding()
        d_model = 512
    
        # 2- 子层连接类的实例对象
        SubLayerConnection_obj = SubLayerConnection(d_model,dropout_p=0.1)
    
        # 3- 演示子层的组合
        # 3.1- 多头自注意力层 + 残差连接和层归一化
        # 3.1.1- 创建 多头自注意力层 数据处理规则
        # 定义方式一:函数的形式
        def data_handler_fn(data):
            # 创建 多头自注意力层 类的实例对象
            attention_obj = MultiHeadAttention(d_model=d_model,head=8,dropout_p=0.1)
            # 调用forward
            return attention_obj(query=data,key=data,value=data)
    
        # 3.1.2- 组装得到子层:也就是调用SubLayerConnection它的forward
        multi_head_attention_output = SubLayerConnection_obj(data,data_handler_fn)
        print(f"多头自注意力层输出结果形状:{multi_head_attention_output.shape}")
    
        # 3.2- 前馈网络层 + 残差连接和层归一化
        # 定义方式二:lambda的形式
        feed_forward_output = SubLayerConnection_obj(
            multi_head_attention_output,
            lambda data:FeedForward(d_model=d_model,output_dim=1024,dropout_p=0.1)(data)
        )
        print(f"前馈网络层输出结果形状:{feed_forward_output.shape}")
    
    if __name__ == '__main__':
        # use_attention()
    
        # use_multi_head_attention()
    
        use_SubLayerConnection()
    
  • 📄 demo03_encoder.py 3.7KB
    """
        1- 编码器 由 6层编码器层 组成
        2- 每个 编码器层由如下的结构组成:
            2.1- 第一个子层:多头自注意力子层
                多头自注意力 + 层归一化 + 残差连接
            2.2- 第二个子层:前馈网络子层
                前馈网络 + 层归一化 + 残差连接
    """
    
    from demo02_component import *
    
    # 编码器层
    class EncoderLayer(nn.Module):
        def __init__(self,d_model,multi_head_self_attn,feed_forward,dropout_p=0.1):
            """
            定义编码器层的结构
            :param d_model: 词向量维度
            :param multi_head_self_attn: 多头自注意力类的实例对象
            :param feed_forward: 前馈网络类的实例对象
            :param dropout_p:
            """
            # 1- 初始化父类
            super().__init__()
    
            # 2- 设置属性值
            self.d_model = d_model
            self.dropout_p = dropout_p
            self.multi_head_self_attn = multi_head_self_attn
            self.feed_forward = feed_forward
    
            # 3- 创建子层
            # 3.1- 第一个子层:多头自注意力子层
            self.multi_layer = SubLayerConnection(self.d_model,self.dropout_p)
    
            # 3.2- 第二个子层:前馈网络子层
            self.feed_forward_layer = SubLayerConnection(self.d_model,self.dropout_p)
    
        def forward(self,data):
            # 1- 数据经过第一个层的处理:多头自注意力子层
            multi_output = self.multi_layer(data, lambda x: self.multi_head_self_attn(query=x, key=x, value=x, mask=None))
            # 2- 数据经过第二个层的处理:前馈网络子层
            encoder_output = self.feed_forward_layer(multi_output, lambda x: self.feed_forward(x))
    
            return encoder_output
    
    # 编码器
    class Encoder(nn.Module):
        def __init__(self, encoder_layer:EncoderLayer, N):
            """
            定义编码器结构。也就是将编码器层重复N层
            :param encoder_layer: 编码器层
            :param N:
            """
            # 1- 初始化父类
            super().__init__()
    
            # 2- 将编码器层创建N对象
            self.encoder_layer_list = clones(encoder_layer,N)
    
            # 3- 层归一化
            """
                这东西不是必须的,只是为了缓解经过N层处理后的数据,让数据变得更加平稳。这一层不是必须的
            """
            self.layer_norm = LayerNorm(encoder_layer.d_model)
    
        def forward(self,data):
    
            """
                注意:for循环中两个data需要与传递给forward的参数名称完全一致,否则6层网络处理的数据就会断开
            """
            for encoder_layer in self.encoder_layer_list:
                data = encoder_layer(data)
    
            return self.layer_norm(data)
    
    # 测试编码器
    # 测试最终的编码器
    def use_encoder():
        # 1- 得到词向量数据:词向量 + 位置编码
        position_data = use_positional_encoding()
    
        d_model = 512
        dropout = 0.1
    
        # 2- 创建编码器的实例对象
        # 2.1- 多头自注意力子层的实例对象
        multi_head = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout)
        # 2.2- 前馈网络子层的实例对象
        ff = FeedForward(d_model=d_model,output_dim=1024,dropout_p=dropout)
        # 2.3- 编码器层的实例对象
        encoder_layder = EncoderLayer(d_model=d_model,multi_head_self_attn=multi_head,feed_forward=ff,dropout_p=dropout)
        # 2.4- 编码器层 堆叠N层得到编码器
        encoder = Encoder(encoder_layder,6)
    
        # 3- 数据进行前向传播
        result = encoder(data=position_data)
    
        print(f"编码器最终的输出结果是:{result.shape}")
        return result
    
    if __name__ == '__main__':
        use_encoder()
    
  • 📄 demo04_decoder.py 5.1KB
    """
        解码器层由如下3层组成
            1- 第一层:掩码多头自注意力层
                掩码多头自注意力 + 层归一化 + 残差连接
            2- 第二层:多头注意力层
                多头注意力 + 层归一化 + 残差连接
            3- 第三层:前馈网络层
                前馈网络 + 层归一化 + 残差连接
    """
    from demo03_encoder import *
    from demo02_component import *
    
    # 解码器层
    class DecoderLayer(nn.Module):
        def __init__(self,d_model,mask_multi_head_self_attn,multi_head_attn,feed_forward,dropout_p=0.1):
            """
            定义解码器层的结构
            :param d_model: 词向量维度
            :param mask_multi_head_self_attn: 掩码多头自注意力类的实例对象
            :param multi_head_attn: 多头注意力类的实例对象
            :param feed_forward: 前馈网络类的实例对象
            :param dropout_p:
            """
            # 1- 初始化父类
            super().__init__()
    
            # 2- 设置属性值
            self.d_model = d_model
            self.dropout_p = dropout_p
            self.mask_multi_head_self_attn = mask_multi_head_self_attn
            self.multi_head_attn = multi_head_attn
            self.feed_forward = feed_forward
    
            # 3- 创建子层
            # 3.1- 第一个子层:掩码多头自注意力子层
            self.mask_multi_self_layer = SubLayerConnection(self.d_model,self.dropout_p)
    
            # 3.2- 第二个子层:多头注意力子层
            self.multi_layer = SubLayerConnection(self.d_model, self.dropout_p)
    
            # 3.3- 第三个子层:前馈网络子层
            self.feed_forward_layer = SubLayerConnection(self.d_model,self.dropout_p)
    
        def forward(self,data,mask,encoder_output):
            # 1- 数据经过第一个层的处理:掩码多头自注意力子层
            data = self.mask_multi_self_layer(data, lambda x: self.mask_multi_head_self_attn(query=x, key=x, value=x, mask=mask))
    
            # 2- 数据经过第二个层的处理:多头注意力子层
            data = self.multi_layer(data, lambda x: self.multi_head_attn(query=x, key=encoder_output, value=encoder_output, mask=None))
    
            # 3- 数据经过第三个层的处理:前馈网络子层
            data = self.feed_forward_layer(data, lambda x: self.feed_forward(x))
    
            return data
    
    # 解码器
    class Decoder(nn.Module):
        def __init__(self, decoder_layer:DecoderLayer, N):
            """
            定义解码器结构。也就是将解码器层重复N层
            :param decoder_layer: 解码器层
            :param N:
            """
            # 1- 初始化父类
            super().__init__()
    
            # 2- 将解码器层创建N对象
            self.decoder_layer_list = clones(decoder_layer,N)
    
            # 3- 层归一化
            """
                这东西不是必须的,只是为了缓解经过N层处理后的数据,让数据变得更加平稳。这一层不是必须的
            """
            self.layer_norm = LayerNorm(decoder_layer.d_model)
    
        def forward(self,data, mask, encoder_output):
    
            """
                注意:for循环中两个data需要与传递给forward的参数名称完全一致,否则6层网络处理的数据就会断开
            """
            for decoder_layer in self.decoder_layer_list:
                data = decoder_layer(data, mask, encoder_output)
    
            return self.layer_norm(data)
    
    # 测试解码器
    # 测试最终的解码器
    def use_decoder():
        # 常用参数定义
        d_model = 512
        dropout = 0.1
    
        # 1- 获得编码器最终结果数据。作为key、value传递给到解码器
        encoder_output = use_encoder()
    
        # 2- 准备解码器端原始目标值数据
        y = torch.tensor([
            # 例如法语单词索引
            [1, 2, 3, 4],
            [5, 6, 7, 8]
        ])
    
        # 3- 构建解码器
        # 3.1- 基础组件实例对象创建
        # 解码器端的词嵌入层
        embed = Embedding(vocab_size=1200,d_model=d_model)
        # 解码器端的位置编码
        position_encoding = PositionalEncoding(d_model=d_model,dropout=dropout)
        # 前馈网络子层
        feed_forward = FeedForward(d_model=d_model,output_dim=1024,dropout_p=dropout)
        # 多头注意力子层
        src_attn = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout)
        # 掩码多头自注意力子层
        self_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
    
        # 3.2- 拼接得到解码器层
        decoder_layer = DecoderLayer(
            d_model=d_model,
            mask_multi_head_self_attn=self_attn,
            multi_head_attn=src_attn,
            feed_forward=feed_forward,
            dropout_p=dropout
        )
    
        # 3.3- 解码器层堆叠N层,得到解码器
        decoder = Decoder(decoder_layer,6)
    
        # 4- 处理数据
        # 4.1- 准备掩码
        mask = torch.zeros(size=(8,4,4)) # 掩码多头自注意力子层 的 掩码。# 注意,第一个位置是多头的数量
    
        # 4.2- 得到词向量+位置编码
        data = embed(y)
        data = position_encoding(data)
    
        # 4.3- 调用解码器
        decoder_result = decoder(data, mask, encoder_output)
    
        print(f"解码器最终的结果:{decoder_result.shape}")
    
    if __name__ == '__main__':
        use_decoder()
    
  • 📄 demo05_output.py 601B
    """
        Transformer的输出部分:
            Linear线性层和Softmax激活函数
    """
    import torch
    import torch.nn as nn
    
    class Output(nn.Module):
        def __init__(self,d_model,vocab_size):
            # 1- 初始化父类
            super().__init__()
    
            # 2- 搭建神经网络结构
            self.linear = nn.Linear(in_features=d_model,out_features=vocab_size)
    
        def forward(self,data):
            """
            :param data: 解码器最终的输出结果,形状[batch_size,seq_len,d_model]
            :return:
            """
    
            return torch.softmax(self.linear(data),dim=-1)
    
  • 📄 demo06_my_transformer.py 5.8KB
    """
        Transformer框架的结构:
            编码器端_输入部分
                词嵌入层
                位置编码
    
            编码器
                多头自注意力子层:多头自注意力 + 层归一化 + 残差连接
                前馈网络子层:前馈网络 + 层归一化 + 残差连接
    
            解码器端_输入部分
                词嵌入层
                位置编码
    
            解码器
                掩码多头自注意力子层:掩码多头自注意力 + 层归一化 + 残差连接
                多头注意力子层(交叉注意力):多头注意力 + 层归一化 + 残差连接
                前馈网络子层:前馈网络 + 层归一化 + 残差连接
    
            输出部分
                线性层
                Softmax激活函数
    
            结果
    """
    from demo04_decoder import *
    from demo05_output import *
    
    # 创建Transformer框架类
    class MyTransformer(nn.Module):
        def __init__(self, en_embde_pos, encoder, de_embde_pos, decoder, output):
            """
            将前面开发的各个部分组织得到完整的Transformer框架
            :param en_embde_pos: 编码器端_输入部分 实例对象
            :param encoder: 编码器 实例对象
            :param de_embde_pos: 解码器端_输入部分 实例对象
            :param decoder: 解码器 实例对象
            :param output: 输出部分 实例对象
            """
            # 1- 初始化父类
            super().__init__()
    
            # 2- 设置属性值
            self.en_embde_pos = en_embde_pos
            self.encoder = encoder
            self.de_embde_pos = de_embde_pos
            self.decoder = decoder
            self.output = output
    
        def forward(self,en_input,de_input,mask):
            """
            Transformer前向传播
            :param en_input: 输入到编码器端的词索引数据
            :param de_input: 输入到解码器端的词索引数据
            :param mask: 解码器使用的掩码
            :return:
            """
    
            # 1- 编码器端_输入部分:词嵌入层、位置编码
            encoder_data = self.en_embde_pos(en_input)
    
            # 2- 编码器
            encoder_data = self.encoder(encoder_data)
    
            # 3- 解码器端_输入部分:词嵌入层、位置编码
            decoder_data = self.de_embde_pos(de_input)
    
            # 4- 解码器
            decoder_data = self.decoder(
                data=decoder_data,
                mask=mask,
                encoder_output=encoder_data
            )
    
            # 5- 输出部分
            return self.output(decoder_data)
    
    # 调用Transformer框架
    def get_mytransformer():
        # 常用变量
        d_model = 512
        dropout = 0.1
        de_vocab_size = 4345
    
        # ---------------- 编码器部分 ----------------
        # 词嵌入层
        en_ebd = Embedding(vocab_size=1000, d_model=d_model)
        # 位置编码
        en_pos = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
        # 多头自注意力
        en_multi_self_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
        # 前馈网络
        en_ff = FeedForward(d_model=d_model, output_dim=1024, dropout_p=dropout)
        # 组装得到编码器层
        encoder_layer = EncoderLayer(
            d_model=d_model,
            multi_head_self_attn=en_multi_self_attn,
            feed_forward=en_ff,
            dropout_p=dropout
        )
        # 得到编码器
        encoder = Encoder(encoder_layer=encoder_layer, N=6)
    
        # ---------------- 解码器部分 ----------------
        # 词嵌入层
        de_ebd = Embedding(vocab_size=de_vocab_size, d_model=d_model)
        # 位置编码
        de_pos = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
        # 掩码多头自注意力
        de_multi_self_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
        # 多头注意力
        de_multi_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
        # 前馈网络
        de_ff = FeedForward(d_model=d_model, output_dim=1024, dropout_p=dropout)
        # 组装得到编码器层
        decoder_layer = DecoderLayer(
            d_model=d_model,
            mask_multi_head_self_attn=de_multi_self_attn,
            multi_head_attn=de_multi_attn,
            feed_forward=de_ff,
            dropout_p=dropout
        )
        # 得到编码器
        decoder = Decoder(decoder_layer=decoder_layer, N=6)
    
        # ---------------- 输出部分 ----------------
        output = Output(d_model=d_model, vocab_size=de_vocab_size)
    
        # ---------------- 组装得到Transformer框架 ----------------
        my_transformer = MyTransformer(
            en_embde_pos=nn.Sequential(en_ebd, en_pos),  # 表示 词先经过词嵌入层的处理,然后再经过位置编码的处理。注意:位置不能乱
            encoder=encoder,
            de_embde_pos=nn.Sequential(de_ebd, de_pos),
            decoder=decoder,
            output=output
        )
        print(f"框架结构信息:{my_transformer}")
        return my_transformer
    
    # 测试Transformer框架
    def use_mytransformer():
        # 1- 获得我们自己的Transformer框架的实例对象
        my_transformer = get_mytransformer()
    
        # 2- 准备数据:以前面的英译法案例为例讲解
        # 2.1- 输入到编码器端的原始数据:也就是英语句子
        en_input = torch.tensor([
            [1,2,3,4],
            [5,6,7,8]
        ])
    
        # 2.2- 输入到解码器端的原始数据:也就是法语句子
        de_input = torch.tensor([
            [223,2344,456,356],
            [2456,131,456,67]
        ])
    
        # 2.3- 解码器的掩码
        mask = torch.triu(torch.ones(size=(8,4,4)),diagonal=0)
    
        # 3- 调用Transformer框架
        result = my_transformer(
            en_input,de_input,mask
        )
    
        print(f"最终输出结果的张量形状:{result.shape}")
        print(f"最终输出结果的张量求和结果:{result.sum()}")
    
    if __name__ == '__main__':
        use_mytransformer()
    
  • 📄 测试代码.py 3.1KB
    import torch
    import numpy as np
    
    # 使用PyTorch实现掩码。【掌握】
    """
        总结:
            1- 上三角掩码:右上角的值为1,左下角的值为0
            2- 下三角掩码:右上角的值为0,左下角的值为1
            3- 相同的规律:对角线的时候diagonal为0;如果是正数,并且越来越大,那么往右上角移动;反之往左下角移动;diagonal的取值超过范围不会报错
            4- 【掌握】掩码的作用:为了防止解码器端当前时间步偷看后面时间步的数据,你只能看到你前面时间步已经预测出来的数据。核心是防止模型训练效果不好
            5- 【掌握】代码:torch.triu(t,diagonal=0)
    """
    def torch_mask():
        # 创建初始张量
        t = torch.ones(size=(5,5))
        print(f"原始张量:{t}")
    
        # 进行掩码操作
        # 上三角掩码
        u_0_mask = torch.triu(t,diagonal=0)
        print(f"上三角掩码0:{u_0_mask}")
    
        u_1_mask = torch.triu(t, diagonal=1)
        print(f"上三角掩码1:{u_1_mask}")
    
        u__1_mask = torch.triu(t, diagonal=-1)
        print(f"上三角掩码_1:{u__1_mask}")
    
        u_n_mask = torch.triu(t, diagonal=1000)
        print(f"上三角掩码n:{u_n_mask}")
    
        print("-"*30)
    
        # 下三角掩码
        l_0_mask = torch.tril(t, diagonal=0)
        print(f"下三角掩码0:{l_0_mask}")
    
        l_1_mask = torch.tril(t, diagonal=1)
        print(f"下三角掩码1:{l_1_mask}")
    
        l__1_mask = torch.tril(t, diagonal=-1)
        print(f"下三角掩码-1:{l__1_mask}")
    
    def np_mask():
        # 1- 准备数据
        arr = np.ones(shape=(5,5))
    
        # 2- 上三角掩码
        # k和PyTorch中diagonal参数的作用完全一样
        u_result = np.triu(arr,k=0)
        print(f"上三角掩码0:\n{u_result}")
    
        u_result = np.triu(arr, k=1)
        print(f"上三角掩码1:\n{u_result}")
    
        u_result = np.triu(arr, k=-1)
        print(f"上三角掩码-1:\n{u_result}")
    
        # 3- 下三角掩码
        l_result = 1 - u_result
        print(f"下三角掩码:\n{l_result}")
    
        l_result = np.tril(arr,k=0)
        print(f"下三角掩码0:\n{l_result}")
    
        l_result = np.tril(arr, k=1)
        print(f"下三角掩码1:\n{l_result}")
    
        l_result = np.tril(arr, k=-1)
        print(f"下三角掩码-1:\n{l_result}")
    
    def assert_demo(d_model,head):
        print("assert之前")
        assert d_model % head == 0
        print("assert之后")
    
    def mask():
        mask = torch.ones(size=(1,2,4,5))
        scores = torch.ones(size=(2,2,4,5))
        scores = scores.masked_fill(mask == 0, value=-1e9)
        print(scores)
    
    def k_multi_data():
        """
            return self.k * (data - mean)/(std+self.eps) + self.b
            演示 为什么 self.k * data 能够相乘?-> 广播机制
            k的形状是[512],data形状[2,4,512]
        """
    
        k = torch.randint(low=1,high=5,size=(3,))
        data = torch.randint(low=1,high=5,size=(2,4,3))
    
        print(k)
        print(data)
        print(k*data)
    
    if __name__ == '__main__':
        # torch版的掩码
        # torch_mask()
    
        # np_mask()
    
        # assert_demo(512,8)
        # assert_demo(512,7)
    
        # mask()
    
        k_multi_data()
    

第5节 · 第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 个文件
  • 📁 01_fasttext/
    • 📄 fasttext_使用.py 4.1KB
      # pip install fasttext-wheel
      
      import fasttext
      
      def demo01():
          # 1- 模型训练
          model = fasttext.train_supervised(input="../data/cooking_train.txt")
      
          # 2- 模型预测
          # 参数解释:k指的是返回top K个预测概率最高的目标值
          pred_labels = model.predict(text="Which baking dish is best to bake a banana bread ?",k=3)
          print(type(pred_labels))    # 类型是嵌套元组
          print(pred_labels)
      
          pred_labels = model.predict(text="how to seperate peanut oil from roasted peanuts at home?",k=5)
          print(pred_labels)
      
          # 3- 模型评估
          result = model.test(path="../data/cooking_valid.txt")
          print(result)
      
      # 将字母全部统一成小写,而且标点符号的前面增加空格
      def demo02():
          # 训练模型
          model = fasttext.train_supervised(input="../data/cooking.pre.train")
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      # 增加训练轮次
      def demo03():
          # 训练模型
          model = fasttext.train_supervised(input="../data/cooking.pre.train",epoch=20)
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      # 调整学习率
      def demo04():
          # 训练模型
          model = fasttext.train_supervised(input="../data/cooking.pre.train",epoch=20,lr=1)
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      def demo05():
          # 训练模型
          model = fasttext.train_supervised(input="../data/cooking.pre.train",epoch=20,lr=1,wordNgrams=2)
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      def demo06():
          # 训练模型
          # hs:层次softmax
          model = fasttext.train_supervised(input="../data/cooking.pre.train",epoch=20,lr=1,wordNgrams=2,loss="hs")
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      def demo07():
          # 训练模型
          # hs:层次softmax
          model = fasttext.train_supervised(
              input="../data/cooking.pre.train",
              autotuneDuration=60*2,
              autotuneValidationFile="../data/cooking.pre.valid"
          )
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      # 将 多标签多分类 问题简化成 单标签多分类的问题。每种分类单独进行训练
      def demo08():
          # 训练模型
          # ova:多标签多分类 问题简化成 单标签多分类的问题
          model = fasttext.train_supervised(input="../data/cooking.pre.train", epoch=20, lr=0.1, wordNgrams=2, loss="ova")
      
          # 模型评估
          result = model.test(path="../data/cooking.pre.valid")
          print(result)
      
      def demo09():
          # 训练模型
          # ova:多标签多分类 问题简化成 单标签多分类的问题
          model = fasttext.train_supervised(input="../data/cooking.pre.train", epoch=20, lr=0.1, wordNgrams=2, loss="ova")
      
          # 保存训练好的模型
          model.save_model("../model/fasttext.pkl")
      
          # 加载训练好的模型
          model = fasttext.load_model("../model/fasttext.pkl")
          pred_labels = model.predict(text="how to seperate peanut oil from roasted peanuts at home?", k=5)
          print(pred_labels)
      
      
      if __name__ == '__main__':
          # 1- 模型训练、预测、评估
          # demo01()    # (3000, 0.15066666666666667, 0.06515784921435779)
      
          # 2- 数据基本处理
          # demo02()    # (3000, 0.17066666666666666, 0.07380712123396281)
      
          # 3- 增加训练轮次
          # demo03()    # (3000, 0.486, 0.2101773100764019)
      
          # 4- 调整学习率
          # demo04()    # (3000, 0.5813333333333334, 0.2514055067031858)
      
          # 5- 设置n-gram参数
          # demo05()    # (3000, 0.5926666666666667, 0.25630676084762866)
      
          # 6- 调整损失函数:主要是提升了运行速度
          # demo06()      # (3000, 0.597, 0.25818076978520976)
      
          # 7- 自动超参数调优
          # demo07()    # (3000, 0.53, 0.22920570851953295)
      
          # 8- 多标签多分类问题
          # demo08()    # (3000, 0.5286666666666666, 0.2286290903848926)
      
          # 9- 保存模型和重新加载模型
          demo09()
      
  • 📁 02_预训练模型/
    • 📄 01-管道.py 6.2KB
      import os
      
      import torch
      
      os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"
      
      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)
      
      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)
      
      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)
      
      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}
          """
      
      if __name__ == '__main__':
          # 1- 文本分类
          # text_classification()
      
          # 2- 文本特征提取
          # text_feature_extraction()
      
          # 3- 完型填空
          # fill_blank()
      
          # 4- 阅读理解
          # q_and_a()
      
          # 5- 文本摘要
          # summary()
      
          # 6- NER命名实体识别
          ner()
      
    • 📄 02-AutoModel自动模型.py 11.9KB
      import os
      
      import torch
      
      os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"
      from transformers import AutoTokenizer  # 分词器
      from transformers import AutoModelForSequenceClassification # 序列分类
      from transformers import AutoModel  # 通用模型加载类
      from transformers import AutoModelForMaskedLM # 完型填空类
      from transformers import AutoModelForQuestionAnswering  # 阅读理解类
      from transformers import AutoModelForSeq2SeqLM  # 文本生成:文本摘要
      from transformers import AutoModelForTokenClassification    # NER类
      from transformers import AutoConfig # 加载模型配置文件
      
      def text_classification():
          # 1- 创建类的实例对象
          model_path = r"D:\soft\PretrainedModel\chinese_sentiment"
          # 创建分词器对象
          tokenizer = AutoTokenizer.from_pretrained(model_path)
          # 创建预训练模型对象
          model = AutoModelForSequenceClassification.from_pretrained(model_path)
      
          # 2- 准备数据
          content = "我爱黑马"
      
          # 3- 分词:将文本内容转成模型需要的数据类型,也就是张量
          """
          参数解释:
              text:要进行分词处理的内容
              return_tensors:返回结果的数据类型。pt 张量;np ndarray数组。推荐pt
              truncation:是否要对超过max_length长度的句子进行截断
              max_length:能够处理的最长句子的长度
              padding:
                  如果是布尔值,同时句子的长度没有超过max_length,不会进行短句子的填充
                  如果是max_length,同时句子的长度没有超过max_length,会进行短句子的填充,使用词索引为0的进行填充
                  
                  举例:
                      参数值为True 结果 [[ 101, 2769, 4263, 7946, 7716,  102]]
                      参数值为max_length 结果 [[ 101, 2769, 4263, 7946, 7716,  102,    0,    0,    0,    0]]
          """
          data_tensor = tokenizer.encode(
              text=content,
              return_tensors="pt",
              padding="max_length",
              truncation=True,
              max_length=10
          )
      
          # print(type(data_tensor))
          # print(data_tensor)
      
          # 4- 调用
          # 4.1- 切换为预测模式
          model.eval()
      
          # 4.2- 调用模型
          pred_result = model(data_tensor)
          print(type(pred_result))
          """
              SequenceClassifierOutput(loss=None, logits=tensor([[-1.5430, -0.9715, -0.4824,  0.1655,  1.0699]],
             grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)
          """
          print(pred_result)
      
          # 4.3- 获得预测概率最高的那个类别对应的索引
          print(pred_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)
          print("最后一个隐藏状态信息", result.last_hidden_state.shape)
          print("池化层信息", result.pooler_output.shape)
      
      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,对应的内容:她
      
      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)))
      
      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}")
      
      if __name__ == '__main__':
          # 1- 文本分类
          # text_classification()
      
          # 2- 文本特征提取
          text_feature_extraction()
      
          # 3- 完型填空
          # fill_blank()
      
          # 4- 阅读理解
          # q_and_a()
      
          # 5- 文本摘要
          # summary()
      
          # 6- NER命名实体识别
          # ner()
      
    • 📄 03-指定模型.py 1.8KB
      """
          预训练模型使用总结:
              1- pipeline管道:
                  优点:代码开发非常简单
                  缺点:底层高度封装,可调整的超参数少
                  使用:一般用来快速验证预训练模型/大模型是否满足业务需求
      
              2- AutoModel自动模型:
                  优点:代码相对比较简单,有一定可以可调整的超参数
                  缺点:相对具体模型来说,可调整的超参数相对较少
      
              3- 指定模型:
                  优点:可调整的超参数很多,能够针对具体的大模型进行指定参数的微调。每种大模型的可调整的参数都是不一样
                  缺点:比较灵活,不同的大模型可调整的参数有区别
                  使用:针对业务场景需要比较高的情况,推荐使用
                  后续课程:LoRA、QLoRA
      """
      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- 调用模型
          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}")
      
    • 📄 04-迁移学习案例_文本分类.py 10.6KB
      import os
      os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"
      import torch
      import torch.nn as nn
      from torch.utils.data import DataLoader
      from transformers import BertTokenizer,BertModel
      from datasets import load_dataset   # HuggingFace数据加载方法
      from tqdm import tqdm
      
      # 1- 定义变量
      # 运行的设备
      device = ("cuda" if torch.cuda.is_available() else "cpu")
      # 预训练模型路径
      model_path = r"D:\soft\PretrainedModel\bert-base-chinese"
      # 创建词汇映射器:词汇映射器只是对句子进行分词,然后得到词索引,不涉及任何的需要学习的参数,因此可以不用放到GPU或CPU上
      tokenizer = BertTokenizer.from_pretrained(model_path)
      # 创建Bert预训练模型实例对象:需要学习相关的模型参数,因此必须放到GPU或CPU上
      bert_model = BertModel.from_pretrained(model_path).to(device=device)
      
      # 2- 数据探索【可选】
      def load_data():
      
          """
              参数解释:
                  path:既能够指定文件目录,也能够指定文件类型。推荐用来指定文件目录
                  data_files:可以传递文件路径,还可以传递数据类型和文件路径字典的形式。推荐直接传递文件路径
                  split:如果data_files是具体的文件路径,那么这里必须给train
          """
          # 加载方式一:【掌握】如果data_files是具体的文件路径,那么这里必须给train
          data = load_dataset(path="../data", data_files="train.csv", split="train")
      
          # 加载方式二:如果data_files是具体的文件路径,那么这里必须给train
          # data = load_dataset(path="data", data_files="test.csv", split="train")
      
          # 加载方式三:path:既能够指定文件目录,也能够指定文件类型。推荐用来指定文件目录
          # data = load_dataset(path="csv", data_files="data/test.csv", split="train")
      
          # 加载方式四:数据类型和文件路径以字典的形式传递
          # file_dict = {
          #     "train":"data/train.csv",
          #     "test":"data/test.csv",
          #     "aaaa":"data/test.csv"
          # }
          # data = load_dataset(path="csv", data_files=file_dict, split="train")
          # data = load_dataset(path="csv", data_files=file_dict, split="test")
          # data = load_dataset(path="csv", data_files=file_dict, split="aaaa")
      
          print(type(data))
          print(len(data))
          print(data)
          print(data[0])
      
      # 3- 针对每个批次数据的具体处理函数
      def collate_fn(batch_data):
          # 1- 分别提取出特征和目标值
          # 数据示例:[句子1, 句子2, ...]
          sents = [line_dict.get("text") for line_dict in batch_data]
          # 数据示例:[1, 0, 1, 1, 1, 0, 1, 0]
          labels = [line_dict.get("label") for line_dict in batch_data]
      
          # 2- 数据预处理
          # 2.1- 特征进行分词,得到词索引组成的张量
          # 因为DataLoader中batch_size>1,因此这里的padding必须为max_length,不能为True
          sents_tensor = tokenizer.batch_encode_plus(
              sents,
              return_tensors="pt",
              padding="max_length",
              truncation=True,
              max_length=300
          )
      
          # 将sents_tensor字典拆解出来
          input_ids = sents_tensor.get("input_ids")
          token_type_ids = sents_tensor.get("token_type_ids")
          attention_mask = sents_tensor.get("attention_mask")
      
          # 2.3- 目标值直接封装为张量
          labels = torch.tensor(labels,dtype=torch.long)
      
          return input_ids,token_type_ids,attention_mask,labels
      
      # 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
      
      # 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])
      
      # 6- 模型训练
      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")
      
      # 7- 模型预测
      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]}")
      
      if __name__ == '__main__':
          # 测试数据探索
          # load_data()
      
          # 测试数据加载器
          # dataloader = get_dataloader(file_path="train.csv")
          # for input_ids,token_type_ids,attention_mask,labels in dataloader:
          #     print(token_type_ids)
          #     break
      
          # train()
      
          predict()
      
  • 📁 data/
    • ⚙️ test.csv 361.5KB
    • ⚙️ validation.csv 365.9KB
📖 笔记

补充: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 语料

  • 📎 cooking.preprocessed.txt1.4MB
  • 📎 cooking.stackexchange.txt1.3MB
  • 📎 cooking_train.txt1.1MB
  • 📎 cooking_valid.txt266.0KB

ℹ️ 离线版不含附件原文件,需要请到在线网站对应课时下载。

第6节 · 第6章 BERT 系列模型

🎯 掌握 BERT / RoBERTa / GPT 等预训练模型原理与选型。
📖 笔记

6. BERT 系列模型

第六章BERT系列模型

BERT模型介绍

BERT简介

BERT是2018年10月由Google AI研究院提出的一种预训练模型.

  • BERT的全称是Bidirectional Encoder Representation from Transformers.
  • BERT在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人的成绩: 全部两个衡量指标上全面超越人类, 并且在11种不同NLP测试中创出SOTA表现. 包括将GLUE基准推高至80.4% (绝对改进7.6%), MultiNLI准确度达到86.7% (绝对改进5.6%). 成为NLP发展史上的里程碑式的模型成就.

BERT架构

总体架构: 如下图所示, 最左边的就是BERT的架构图, 可以很清楚的看到BERT采用了Transformer Encoder block进行连接, 因为是一个典型的双向编码模型.

image-20251109145416067

从上面的架构图中可以看到, 宏观上BERT分三个主要模块.

  • 最底层黄色标记的Embedding模块.
  • 中间层蓝色标记的Transformer模块.
  • 最上层绿色标记的预微调模块.

Embedding模块

BERT中的该模块是由三种Embedding共同组成而成, 如下图

image-20251109145929719

  • Token Embeddings 是词嵌入张量, 第一个单词是CLS标志, 可以用于之后的分类任务.
  • Segment Embeddings 是句子分段嵌入张量, 是为了服务后续的两个句子为输入的预训练任务.
  • Position Embeddings 是位置编码张量, 此处注意和传统的Transformer不同, 不是三角函数计算的固定位置编码, 而是通过学习得出来的.
  • 整个Embedding模块的输出张量就是这3个张量的直接加和结果.

双向Transformer模块

BERT中只使用了经典Transformer架构中的Encoder部分, 完全舍弃了Decoder部分. 而两大预训练任务也集中体现在训练Transformer模块中.

预微调模块

  • 经过中间层Transformer的处理后, BERT的最后一层根据任务的不同需求而做不同的调整即可.
  • 比如对于sequence-level的分类任务, BERT直接取第一个[CLS] token 的final hidden state, 再加一层全连接层后进行softmax来预测最终的标签.
  • 对于不同的任务, 微调都集中在预微调模块, 几种重要的NLP微调任务架构图展示如下

image-20251109150651314

  • 从上图中可以发现, 在面对特定任务时, 只需要对预微调层进行微调, 就可以利用Transformer强大的注意力机制来模拟很多下游任务, 并得到SOTA的结果. (句子对关系判断, 单文本主题分类, 问答任务(QA), 单句贴标签(NER))
  • 若干可选的超参数建议如下:
Batch size: 16, 32
Learning rate (Adam): 5e-5, 3e-5, 2e-5
Epochs: 3, 4

BERT的预训练任务

BERT包含两个预训练任务:

  • 任务一: Masked LM (带mask的语言模型训练)
  • 任务二: Next Sentence Prediction (下一句话预测任务)

任务一: Masked LM

带mask的语言模型训练

  • 关于传统的语言模型训练, 都是采用left-to-right, 或者left-to-right + right-to-left结合的方式, 但这种单向方式或者拼接的方式提取特征的能力有限. 为此BERT提出一个深度双向表达模型(deep bidirectional representation). 即采用MASK任务来训练模型.
  • 1: 在原始训练文本中, 随机的抽取15%的token作为参与MASK任务的对象.
  • 2: 在这些被选中的token中, 数据生成器并不是把它们全部变成[MASK], 而是有下列3种情况.
  • 2.1: 在80%的概率下, 用[MASK]标记替换该token, 比如my dog is hairy -> my dog is [MASK]
  • 2.2: 在10%的概率下, 用一个随机的单词替换token, 比如my dog is hairy -> my dog is apple
  • 2.3: 在10%的概率下, 保持该token不变, 比如my dog is hairy -> my dog is hairy
  • 3: 模型在训练的过程中, 并不知道它将要预测哪些单词? 哪些单词是原始的样子? 哪些单词被遮掩成了[MASK]? 哪些单词被替换成了其他单词? 正是在这样一种高度不确定的情况下, 反倒逼着模型快速学习该token的分布式上下文的语义, 尽最大努力学习原始语言说话的样子. 同时因为原始文本中只有15%的token参与了MASK操作, 并不会破坏原语言的表达能力和语言规则.

任务二: Next Sentence Prediction

下一句话预测任务

  • 在NLP中有一类重要的问题比如QA(Quention-Answer), NLI(Natural Language Inference), 需要模型能够很好的理解两个句子之间的关系, 从而需要在模型的训练中引入对应的任务. 在BERT中引入的就是Next Sentence Prediction任务. 采用的方式是输入句子对(A, B), 模型来预测句子B是不是句子A的真实的下一句话.
  • 1: 所有参与任务训练的语句都被选中作为句子A.
  • 1.1: 其中50%的B是原始文本中真实跟随A的下一句话. (标记为IsNext, 代表正样本)
  • 1.2: 其中50%的B是原始文本中随机抽取的一句话. (标记为NotNext, 代表负样本)
  • 2: 在任务二中, BERT模型可以在测试集上取得97%-98%的准确率.

BERT模型特点

BERT模型优缺点

BERT的优点

  • 通过预训练, 加上Fine-tunning, 在11项NLP任务上取得最优结果.
  • BERT的根基源于Transformer, 相比传统RNN更加高效, 可以并行化处理同时能捕捉长距离的语义和结构依赖.
  • BERT采用了Transformer架构中的Encoder模块, 不仅仅获得了真正意义上的bidirectional context, 而且为后续微调任务留出了足够的调整空间.

BERT的缺点

  • BERT模型过于庞大, 参数太多, 不利于资源紧张的应用场景, 也不利于上线的实时处理.
  • BERT目前给出的中文模型中, 是以字为基本token单位的, 很多需要词向量的应用无法直接使用. 同时该模型无法识别很多生僻词, 只能以UNK代替.
  • BERT中第一个预训练任务MLM中, [MASK]标记只在训练阶段出现, 而在预测阶段不会出现, 这就造成了一定的信息偏差, 因此训练时不能过多的使用[MASK], 否则会影响模型的表现.
  • 按照BERT的MLM任务中的约定, 每个batch数据中只有15%的token参与了训练, 被模型学习和预测, 所以BERT收敛的速度比left-to-right模型要慢很多(left-to-right模型中每一个token都会参与训练).

BERT的MLM任务

BERT的MLM任务中为什么采用了80%, 10%, 10%的策略?

  • 首先, 如果所有参与训练的token被100%的[MASK], 那么在fine-tunning的时候所有单词都是已知的, 不存在[MASK], 那么模型就只能根据其他token的信息和语序结构来预测当前词, 而无法利用到这个词本身的信息, 因为它们从未出现在训练过程中, 等于模型从未接触到它们的信息, 等于整个语义空间损失了部分信息. 采用80%的概率下应用[MASK], 既可以让模型去学着预测这些单词, 又以20%的概率保留了语义信息展示给模型.
  • 保留下来的信息如果全部使用原始token, 那么模型在预训练的时候可能会偷懒, 直接照抄当前token信息. 采用10%概率下random token来随机替换当前token, 会让模型不能去死记硬背当前的token, 而去尽力学习单词周边的语义表达和远距离的信息依赖, 尝试建模完整的语言信息.
  • 最后再以10%的概率保留原始的token, 意义就是保留语言本来的面貌, 让信息不至于完全被遮掩, 使得模型可以"看清"真实的语言面貌.

BERT处理长文本的方法

首选要明确一点, BERT预训练模型所接收的最大sequence长度是512.

那么对于长文本(文本长度超过512的句子), 就需要特殊的方式来构造训练样本. 核心就是如何进行截断.

  • head-only方式: 这是只保留长文本头部信息的截断方式, 具体为保存前510个token (要留两个位置给[CLS]和[SEP]).
  • tail-only方式: 这是只保留长文本尾部信息的截断方式, 具体为保存最后510个token (要留两个位置给[CLS]和[SEP]).
  • head+only方式: 选择前128个token和最后382个token (文本总长度在800以内), 或者前256个token和最后254个token (文本总长度大于800).
BERT的缺点
    1- 相对RNN来说,模型体积过大,也就是参数过多
        超参数:程序员自己手动调试的参数
        参数:模型自己训练得到的。例如:w、b
    2- 对中文的处理有限,因为词汇表中是以单个单个的词进行分词处理的
    3- 预训练任务可能会有偏差


为什么MLM选择80%、10%、10%的组合?
    80% -> 用[MASK]替代真实词,避免模型猜词,逼迫模型深入的去理解上下文意思
    10% -> 随机替换。防止模型偷懒
    10% -> 不做任何操作。保留原始语义

BERT系列模型介绍

AlBERT模型

AlBERT模型的架构

  • AlBERT模型发布于ICLR 2020会议, 是基于BERT模型的重要改进版本. 是谷歌研究院和芝加哥大学共同发布的研究成果.
  • 论文<< A Lite BERT For Self-Supervised Learning Of Language Representations >>.
  • 从模型架构上看, AlBERT和BERT基本一致, 核心模块都是基于Transformer的强大特征提取能力.

  • 在本篇论文中, 首先对比了过去几年预训练模型的主流操作思路.
  • 第一: 大规模的语料.
  • 第二: 更深的网络, 更多的参数.
  • 第三: 多任务训练.

AlBERT模型的优化点

  • 相比较于BERT模型, AlBERT的出发点即是希望降低预训练的难度, 同时提升模型关键能力. 主要引入了5大优化.
  • 第一: 词嵌入参数的因式分解.
  • 第二: 隐藏层之间的参数共享.
  • 第三: 去掉NSP, 增加SOP预训练任务.
  • 第四: 去掉dropout操作.
  • 第五: MLM任务的优化.

  • 第一: 词嵌入参数的因式分解.
  • AlBERT的作者认为, 词向量只记录了少量的词汇本身的信息, 更多的语义信息和句法信息包含在隐藏层中. 因此词嵌入的维度不一定非要和隐藏层的维度一致.

  • 具体做法就是通过因式分解来降低嵌入矩阵的参数:
  • BERT: embedding_dim * vocab_size = hidden_size * vocab_size, 其中embedding_dim=768, vocab_size大约为30000左右的级别, 大约等于30000 * 768 = 23040000(2300万).
  • AlBERT: vocab_size * project + project * hidden_size, 其中project是因式分解的中间映射层维度, 一般取128, 参数总量大约等于30000 * 128 + 128 * 768 = 482304(48万).

  • 第二: 隐藏层之间的参数共享.
  • 在BERT模型中, 无论是12层的base, 还是24层的large模型, 其中每一个Encoder Block都拥有独立的参数模块, 包含多头注意力子层, 前馈全连接层. 非常重要的一点是, 这些层之间的参数都是独立的, 随着训练的进行都不一样了!

  • 那么为了减少模型的参数量, 一个很直观的做法便是让这些层之间的参数共享, 本质上只有一套Encoder Block的参数!
  • 在AlBERT模型中, 所有的多头注意力子层, 全连接层的参数都是分别共享的, 通过这样的方式, AlBERT属于Block的参数量在BERT的基础上, 分别下降到原来的1/12, 1/24.

  • 第三: 去掉NSP, 增加SOP预训练任务.
  • BERT模型的成功很大程度上取决于两点, 一个是基础架构采用Transformer, 另一个就是精心设计的两大预训练任务, MLM和NSP. 但是BERT提出后不久, 便有研究人员对NSP任务提出质疑, 我们也可以反思一下NSP任务有什么问题?

  • 在AlBERT模型中, 直接舍弃掉了NSP任务, 新提出了SOP任务(Sentence Order Prediction), 即两句话的顺序预测, 文本中正常语序的先后两句话[A, B]作为正样本, 则[B, A]作为负样本.
  • 增加了SOP预训练任务后, 使得AlBERT拥有了更强大的语义理解能力和语序关系的预测能力.

  • 第四: 去掉dropout操作.
  • 原始论文中提到, 在AlBERT训练达到100万个batch_size时, 模型依然没有过拟合, 作者基于这个试验结果直接去掉了Dropout操作, 竟然意外的发现AlBERT对下游任务的效果有了进一步的提升. 这是NLP领域第一次发现dropout对大规模预训练模型会造成负面影响, 也使得AlBERT v2.0版本成为第一个不使用dropout操作而获得优异表现的主流预训练模型

  • 第五: MLM任务的优化.
  • segments-pair的优化:
  • BERT为了加速训练, 前90%的steps使用了长度为128个token的短句子, 后10%的steps才使用长度为512个token的长句子.
  • AlBERT在90%的steps中使用了长度为512个token的长句子, 更长的句子可以提供更多上下文信息, 可以显著提升模型的能力.
  • Masked-Ngram-LM的优化:
  • BERT的MLM目标是随机mask掉15%的token来进行预测, 其中的token早已分好, 一个个算.
  • AlBERT预测的是Ngram片段, 每个片段长度为n (n=1,2,3), 每个Ngram片段的概率按照公式分别计算即可. 比如1-gram, 2-gram, 3-gram的概率分别为6/11, 3/11, 2/11.

  • AlBERT系列中包含一个albert-tiny模型, 隐藏层仅有4层, 参数量1.8M, 非常轻巧. 相比较BERT, 其训练和推理速度提升约10倍, 但精度基本保留, 语义相似度数据集LCQMC测试集达到85.4%, 相比于bert-base仅下降1.5%, 非常优秀.

RoBERTa模型

RoBERTa模型的架构

  • 原始论文<< RoBERTa: A Robustly Optimized BERT Pretraining Approach >>, 由FaceBook和华盛顿大学联合于2019年提出的模型.
  • 从模型架构上看, RoBERTa和BERT完全一致, 核心模块都是基于Transformer的强大特征提取能力. 改进点主要集中在一些训练细节上.
  • 第1点: More data
  • 第2点: Larger batch size
  • 第3点: Training longer
  • 第4点: No NSP
  • 第5点: Dynamic masking
  • 第6点: Byte level BPE

RoBERTa模型的优化点

  • 针对于上面提到的7点细节, 一一展开说明:
  • 第1点: More data (更大的数据量)
  • 原始BERT的训练语料采用了16GB的文本数据.
  • RoBERTa采用了160GB的文本数据.
    • 1: Books Corpus + English Wikipedia (16GB): BERT原文使用的之数据.
    • 2: CC-News (76GB): 自CommonCrawl News数据中筛选后得到数据, 约含6300万篇新闻, 2016年9月-2019年2月.
    • 3: OpenWebText (38GB): 该数据是借鉴GPT2, 从Reddit论坛中获取, 取点赞数大于3的内容.
    • 4: Storie (31GB): 同样从CommonCrawl获取, 属于故事类数据, 而非新闻类.

  • 第2点: Larger batch size (更大的batch size)
  • BERT采用的batch size等于256.
  • RoBERTa的训练在多种模式下采用了更大的batch size, 从256一直到最大的8000.

  • 第3点: Training longer (更多的训练步数)
  • RoBERTa的训练采用了更多的训练步数, 让模型充分学习数据中的特征.

  • 第4点: No NSP (去掉NSP任务)
  • 从2019年开始, 已经有越来越多的证据表明NSP任务对于大型预训练模型是一个负面作用, 因此在RoBERTa中直接取消掉NSP任务.
  • 论文作者进行了多组对照试验:
    • 1: Segment + NSP (即BERT模式). 输入包含两部分, 每个部分是来自同一文档或者不同文档的segment(segment是连续的多个句子), 这两个segment的token总数少于512, 预训练包含MLM任务和NSP任务.
    • 2: Sentence pair + NSP (使用两个连续的句子 + NSP, 并采用更大的batch size). 输入也是包含两部分, 每个部分是来自同一个文档或者不同文档的单个句子, 这两个句子的token 总数少于512. 由于这些输入明显少于512个tokens, 因此增加batch size的大小, 以使tokens总数保持与SEGMENT-PAIR + NSP相似, 预训练包含MLM任务和NSP任务.
    • 3: Full-sentences (如果输入的最大长度为512, 那么尽量选择512长度的连续句子; 如果跨越document, 就在中间加上一个特殊分隔符, 比如[SEP]; 该试验没有NSP). 输入只有一部分(而不是两部分), 来自同一个文档或者不同文档的连续多个句子, token总数不超过512. 输入可能跨越文档边界, 如果跨文档, 则在上一个文档末尾添加文档边界token, 预训练不包含NSP任务.
    • 4: Document-sentences (和情况3一样, 但是步跨越document; 该实验没有NSP). 输入只有一部分(而不是两部分), 输入的构造类似于Full-sentences, 只是不需要跨越文档边界, 其输入来自同一个文档的连续句子, token总数不超过512. 在文档末尾附近采样的输入可以短于512个tokens, 因此在这些情况下动态增加batch size大小以达到与Full-sentecens相同的tokens总数, 预训练不包含NSP任务.

  • 总的来说, 实验结果表明1 < 2 < 3 < 4.
  • 真实句子过短的话, 不如拼接成句子段.
  • 没有NSP任务更优.
  • 不跨越document更优.

  • 第5点: Dynamic masking (采用动态masking策略)
  • 原始静态mask: 即BERT版本的mask策略, 准备训练数据时, 每个样本只会进行一次随机mask(因此每个epoch都是重复的), 后续的每个训练步都采用相同的mask方式, 这是原始静态mask.
  • 动态mask: 并没有在预处理的时候执行mask, 而是在每次向模型提供输入时动态生成mask, 所以到底哪些tokens被mask掉了是时刻变化的, 无法提前预知的.

  • 第6点: Byte level BPE (采用字节级别的Encoding)
  • 基于char-level: 原始BERT的方式, 在中文场景下就是处理一个个的汉字.
  • 基于bytes-level: 与char-level的区别在于编码的粒度是bytes, 而不是unicode字符作为sub-word的基本单位.
  • 当采用bytes-level的BPE之后, 词表大小从3万(原始BERT的char-level)增加到5万. 这分别为BERT-base和BERT-large增加了1500万和2000万额外的参数. 之前有研究表明, 这样的做法在有些下游任务上会导致轻微的性能下降. 但论文作者相信: 这种统一编码的优势会超过性能的轻微下降.

MacBert模型

MacBert模型的架构

  • MacBert模型由哈工大NLP实验室于2020年11月提出, 2021年5月发布应用, 是针对于BERT模型做了优化改良后的预训练模型.
  • << Revisiting Pre-trained Models for Chinese Natural Language Processing >>, 通过原始论文题目也可以知道, MacBert是针对于中文场景下的BERT优化.
  • MacBert模型的架构和BERT大部分保持一致, 最大的变化有两点:
  • 第一点: 对于MLM预训练任务, 采用了不同的MASK策略.
  • 第二点: 删除了NSP任务, 替换成SOP任务.

MacBert模型的优化点

  • 第一点: 对于MLM预训练任务, 采用了不同的MASK策略.
  • 1: 使用了全词masked以及n-gram masked策略来选择tokens如何被遮掩, 从单个字符到4个字符的遮掩比例分别为40%, 30%, 20%, 10%
  • 2: 原始BERT模型中的[MASK]出现在训练阶段, 但没有出现在微调阶段, 这会造成exposure bias的问题. 因此在MacBert中提出使用类似的单词来进行masked. 具体来说, 使用基于Word2Vec相似度计算包训练词向量, 后续利用这里面找近义词的功能来辅助mask, 比如以30%的概率选择了一个3-gram的单词进行masked, 则将在Word2Vec中寻找3-gram的近义词来替换, 在极少数情况下, 当没有符合条件的相似单词时, 策略会进行降级, 直接使用随机单词进行替换.
  • 3: 使用15%的百分比对输入单词进行MASK, 其中80%的概率下执行策略2(即替换为相似单词), 10%的概率下替换为随机单词, 10%的概率下保留原始单词不变.

  • 第二点: 删除了NSP任务, 替换成SOP任务.
  • 第二点优化是直接借鉴了AlBERT模型中提出的SOP任务.

  • 在NLP著名的难任务阅读理解中, MacBert展现出非常优秀的表现.

3.4 SpanBERT模型

SpanBERT模型的架构

  • 论文的主要贡献有3点:
  • 1: 提出了更好的Span Mask方案, 再次展示了随机遮掩连续一段tokens比随机遮掩单个token要好.
  • 2: 通过加入了Span Boundary Objective(SBO)训练任务, 增强了BERT的性能, 特别在一些和Span适配的任务, 如抽取式问答.
  • 3: 用实验数据获得了和XLNet一致的结果, 发现去除掉NSP任务, 直接用连续一长句训练效果更好.

  • 架构图中可以清晰的展示论文的核心贡献点:
  • Span Masking
  • Span Boundary Objective

Span Masking

  • 关于创新的MASK机制, 一般来说都是相对于原始BERT的基准进行改进. 对于BERT, 训练时会随机选取整句中的最小输入单元token来进行遮掩, 中文场景下本质上就是进行字级别的MASK. 但是这种方式会让本来应该有强相关的一些连在一起的字词, 在训练时被割裂开了.
  • 那么首先想到的做法: 既然能遮掩字, 那么能不能直接遮掩整个词呢? 这就是BERT-WWM模型的思想.
原始输入: 使用语言模型来预测下一个词的概率.

原始BERT: 使用语言[MASK]型来[MASK]测下一个词的[MASK]率.

BERT-WWM: 使用语言[MASK][MASK]来[MASK][MASK]下一个词的[MASK][MASK].

  • 引申: 百度著名的ERNIE模型中, 直接引入命名实体(Named Entity)的外部知识, 进行整个实体的遮掩, 进行训练.

  • 综合上面所说, 会更自然的想到, 既然整词的MASK, 那么如果拥有词的边界信息会不会让模型的能力更上一层楼呢? SpanBERT给出的是肯定的回答!!!
  • 论文中关于span的选择, 走了这样一个流程:
  • 第一步: 根据几何分布, 先随机选择一个span长度.
  • 第二步: 再根据均匀分布随机选择这一段的起始位置.
  • 第三步: 最后根据前两步的start和length直接进行MASK.

  • 结论: 论文中详细论证了按照上述算法进行MASK, 随机被遮掩的文本平均长度等于3.8

Span Boundary Objective(SBO)

  • SBO任务是本篇论文最核心的创新点, 希望通过增加这个预训练任务, 可以让被遮掩的Span Boundary的词向量, 能够学习到Span内部的信息

  • 具体的做法: 在训练时取Span前后边界的两个词, 需要注意这两个词不在Span内, 然后用这两个词向量加上Span中被MASK掉的词的位置向量, 来预测原词.
  • 更详细的操作如下, 即将词向量和位置向量进行拼接, 经过GeLU激活和LayerNorm处理, 连续经过两个全连接层, 得到最终的输出张量:

  • 最后预测Span中原词的时候会得到一个损失, 这就是SBO任务的损失; 再将其和BERT自身的MLM任务的损失进行加和, 共同作为SpanBERT的目标损失函数进行训练

NSP任务反思

  • 为什么选择Single Sentence而不是BERT的Two Sentence?
  • 1: 训练文本的长度更大, 可以学会长程依赖.
  • 2: 对于NSP的负样本, 基于另一个主题文档的句子来预测单词, 会给MLM任务引入很大的噪声.
  • 3: AlBERT模型已经给出了论证, 因为NSP任务太简单了.

ELMo模型介绍

  • 学习了什么是ELMo.
  • ELMo是2018年3月由华盛顿大学提出的一种预训练语言模型.
  • ELMo在6种NLP测试任务中有很大的提升表现.
  • 学习了ELMo的结构.
  • ELMo架构总体上采用了双向双层LSTM的结构.
  • 最底层的Embedding模块.
  • 中间层的双向双层LSTM模块.
  • 最上层的特征融合模块.
  • 学习了ELMo的预训练任务.
  • ELMo的本质思想就是根据当前上下文对word embedding进行动态调整的语言模型.
  • ELMo的预训练是一个明显的两阶段过程.
    • 第一阶段: 利用语言模型进行预训练, 得到基础静态词向量和双向双层LSTM网络.
    • 第二阶段: 在拥有上下文的环境中, 将上下文输入双向双层LSTM中, 得到动态调整后的word embedding, 等于将单词融合进了上下文的语义, 可以更准确的表达单词的真实含义.
  • 学习了ELMo的效果.
  • 经过与GloVe静态词向量的对比, 明显可以看出ELMo的词向量可以更好的表达真实语义, 更好的解决多义词的问题.
  • 学习了ELMo的待改进点.
  • ELMo的特征提取器没有选用更强大的Transformer, 在提取特征上肯定弱于现在的最优结果.

GPT模型介绍

  • 学习了什么是GPT.
  • GPT是OpenAI公司提出的一种预训练语言模型.
  • 本质上来说, GPT是一个单向语言模型.
  • 学习了GPT的架构.
  • GPT采用了Transformer架构中的解码器模块.
  • GPT在使用解码器模块时做了一定的改造, 将传统的3层Decoder Block变成了2层Block, 删除了encoder-decoder attention子层, 只保留Masked Multi-Head Attention子层和Feed Forward子层.
  • GPT的解码器总共是由12个改造后的Decoder Block组成的.
  • 学习了GPT的预训练任务.
  • 第一阶段: 无监督的预训练语言模型. 只利用单词前面的信息来预测当前单词.
  • 第二阶段: 有监督的下游任务fine-tunning.

BERT GPT ELMo模型对比

学习了BERT, GPT, ELMo之间的区别: * 三者所选取的特征提取器不同. * BERT采用的是Transformer架构中的Encoder模块. * GPT采用的是Transformer架构中的Decoder模块. * ELMo采用的双层双向LSTM模块.

  • 三者所采用的语言模型单/双向不同.
  • BERT采用的是最彻底的双向语言模型, 可以同时关注context before和context after.
  • GPT采用的是单向语言模型, 即Transformer中的Decoder, 由于采用了mask机制, 所以未来信息context after都不可见.
  • ELMo表面上被认为是双向语言模型, 但实际上是左右两个单向LSTM模型分别提取特征, 在进行简单的拼接融合.

第7节 · 第7章 精选问答与全章导图

🎯 通过精选问答巩固 Transformer / BERT 知识。
📖 笔记

7. Transformer 精选问答

第七章Transformer精选问答

Transformer各自模块作用

  • Encoder模块
  • 经典的Transformer架构中的Encoder模块包含6个Encoder Block.
  • 每个Encoder Block包含两个子模块, 分别是多头自注意力层, 和前馈全连接层.
    • 多头自注意力层采用的是一种Scaled Dot-Product Attention的计算方式, 实验结果表明, Mul ti-head可以在更细致的层面上提取不同head的特征, 比单一head提取特征的效果更佳.
    • 前馈全连接层是由两个全连接层组成, 线性变换中间增添一个Relu激活函数, 具体的维度采用4倍关系, 即多头自注意力的d_model=512, 则层内的变换维度d_ff=2048.
  • Decoder模块
  • 经典的Transformer架构中的Decoder模块包含6个Decoder Block.
  • 每个Decoder Block包含3个子模块, 分别是多头自注意力层, Encoder-Decoder Attention层, 和前馈全连接层.
    • 多头自注意力层采用和Encoder模块一样的Scaled Dot-Product Attention的计算方式, 最大的 区别在于需要添加look-ahead-mask, 即遮掩"未来的信息".
    • Encoder-Decoder Attention层和上一层多头自注意力层最主要的区别在于Q != K = V, 矩阵Q来源于上一层Decoder Block的输出, 同时K, V来源于Encoder端的输出.
    • 前馈全连接层和Encoder中完全一样.
  • Add & Norm模块
  • Add & Norm模块接在每一个Encoder Block和Decoder Block中的每一个子层的后面.
  • 对于每一个Encoder Block, 里面的两个子层后面都有Add & Norm.
  • 对于每一个Decoder Block, 里面的三个子层后面都有Add & Norm.
  • Add表示残差连接, 作用是为了将信息无损耗的传递的更深, 来增强模型的拟合能力.
  • Norm表示LayerNorm, 层级别的数值标准化操作, 作用是防止参数过大过小导致的学习过程异常, 模型收敛特别慢的问题.
  • 位置编码器Positional Encoding
  • Transformer中采用三角函数来计算位置编码.
  • 因为三角函数是周期性函数, 不受序列长度的限制, 而且这种计算方式可以对序列中不同位置的编码的重要程度同等看待.
  • 本节常见问答
  • Transformer的结构是什么样的? 各个子模块各有什么作用?

Transformer Decode模块

  • 在Transformer结构中的Decoder模块的输入, 区分于不同的Block, 最底层的Block输入有其特殊的地方. 第二层到第六层的输入一致, 都是上一层的输出和Encoder的输出.
  • 最底层的Block在训练阶段, 每一个time step的输入是上一个time step的输入加上真实标签序列向后移一位. 具体来看, 就是每一个time step的输入序列会越来越长, 不断的将之前的输入融合进来.
  • 最底层的Block在训练阶段, 真实的代码实现中, 采用的是MASK机制来模拟输入序列不断添加的过程.
  • 最底层的Block在预测阶段, 每一个time step的输入是从time step=0开始, 一直到上一个time step的预测值的累积拼接张量. 具体来看, 也是随着每一个time step的输入序列会越来越长. 相比于训练阶段最大的不同是这里不断拼接进来的token是每一个time step的预测值, 而不是训练阶段每一个time step取得的groud truth值.

self attention机制详解

  • self-attention机制的重点是使用三元组(Q, K, V)参与规则运算, 这里面Q=K=V.
  • self-attention最大的优势是可以方便有效的提取远距离依赖的特征和结构信息, 不必向RNN那样依次计算产生传递损耗.
  • 关于self-attention采用三元组的原因, 经典实现的方式数学意义明确, 理由充分, 至于其他方式的可行性暂时没有论文做充分的对比试验研究.
  • 学习了softmax函数的输入是如何影响输出分布的.
  • softmax函数本质是对输入的数据分布做一次归一化处理, 但是输入元素的数量级对softmax最终的分布影响非常之大.
  • 在输入元素的数量级较大时, softmax函数几乎将全部的概率分布都分配给了最大值分量所对应的标签.
  • 学习了softmax函数在反向传播的过程中是如何梯度求导的.
  • 具体的推导过程见讲义正文部分, 注意要分两种情况讨论, 分别处理.
  • 学习了softmax函数出现梯度消失现象的原因.
  • 结合第一步, 第二步的结论, 可以很清楚的看到最终的梯度矩阵接近于零矩阵, 这样在进行参数更新的时候就会产生梯度消失现象.
  • 学习了维度和点积大小的关系推导.
  • 通过期望和方差的推导理解了为什么点积会造成方差变大.
  • 理解了通过数学技巧除以sqrt(d_k)就可以让方差恢复成1.

multi head attention详解

  • 学习了Transformer架构采用Multi-head Attention的原因.
  • 将模型划分为多个头, 分别进行Attention计算, 可以形成多个子空间, 让模型去关注不同方面的信息特征, 更好的提升模型的效果.
  • 多头注意力有助于神经网络捕捉到更丰富的特征信息.
  • 学习了Multi-head Attention的计算方式.
  • 对特征张量的最后一个维度进行了分割, 一般是对词嵌入的维度embedding_dim进行切割, 切割后的计算规则和单一head完全一致.
  • 在不同的head上应用了注意力计算规则后, 得到的结果张量直接采用拼接concat的方式进行融合, 就得到了Multi-head Attention的结果张量.

Transformer优势

  • 学习了Transformer相比于RNN/LSTM的优势和原因.
  • 第一大优势是并行计算的优势.
  • 第二大优势是特征提取能力强.
  • 学习了Transformer架构中Encoder模块的并行化机制.
  • Encoder模块在训练阶段和测试阶段都可以实现完全相同的并行化.
  • Encoder模块在Embedding层, Feed Forward层, Add & Norm层都是可以并行化的.
  • Encoder模块在self-attention层, 因为各个token之间存在依赖关系, 无法独立计算, 不是真正意义上的并行化.
  • Encoder模块在self-attention层, 因为采用了矩阵运算的实现方式, 可以一次性的完成所有注意力张量的计算, 也是另一种"并行化"的体现.
  • 学习了Transformer架构中Decoder模块的并行化机制.
  • Decoder模块在训练阶段可以实现并行化.
  • Decoder模块在训练阶段的Embedding层, Feed Forward层, Add & Norm层都是可以并行化的.
  • Decoder模块在self-attention层, 以及Encoder-Decoder Attention层, 因为各个token之间存在依赖关系, 无法独立计算, 不是真正意义上的并行化.
  • Decoder模块在self-attention层, 以及Encoder-Decoder Attention层, 因为采用了矩阵运算的实现方式, 可以一次性的完成所有注意力张量的计算, 也是另一种"并行化"的体现.
  • Decoder模块在预测计算不能并行化处理.
  • 学习了seq2seq架构的两大缺陷.
  • 第一个缺陷是Encoder端的所有信息被压缩成一个固定的输出张量, 当序列长度较长时会造成比较严重的信息损耗.
  • 第二个缺陷是无法并行计算.
  • 学习了Transformer架构对seq2seq两大缺陷的改进.
  • Transformer应用Multi-head Attention机制让编码器信息可以更好的展示给解码器.
  • Transformer可以实现Encoder端的并行计算.
📎 资料

NLP 基础全章思维导图

  • 📎 NLP自然语言处理基础.xmind3.4MB

ℹ️ 离线版不含附件原文件,需要请到在线网站对应课时下载。

第8节 · 第8章 预训练模型微调实战(day11-12)

🎯 使用 transformers 完成 BERT 下游任务微调完整流程。
📖 笔记

NLP 基础(day11-12)

NLP基础

【了解】今日内容介绍

  • fasttext工具介绍
  • fasttext文本分类
  • fasttext词向量迁移
  • 迁移学习
  • Transformers库的使用

【掌握】fasttext工具介绍

简介

fasttext是自然语言处理(NLP)任务的开源工具包,由Facebook AI Research(FAIR)开发。

它被广泛用于各种任务,包括情感分析、文本分类、命名实体识别、机器翻译等

主要作用

  • 文本分类
  • 训练词向量

优势

正如其名,在保持较高精度的情况下,快速的进行训练和预测是fasttext的最大优势。

原因:

  • fasttext工具包中内含的fasttext模型,模型结构简单

  • 使用fasttext模型训练词向量时,使用层次softmax结构, 来提升超多类别下的模型性能

  • 采用负采样(negative sampling),每次训练仅仅更新一小部分的权重, 降低梯度下降过程中的计算量

  • fasttext模型过于简单无法捕捉词序特征, 可采用n-gram特征提取文本特征以弥补模型缺陷提升精度

安装

# pip安装
pip install fasttext-wheel -i https://mirrors.aliyun.com/pypi/simple/

# 如果安装失败,则执行如下命令,文件见day01资料文件夹
pip install fasttext_wheel-0.9.2-cp38-cp38-win_amd64.whl

注意事项:

一定要明确Python环境,避免导致安装后失效的情况。

模型架构

FastText 模型架构和 Word2Vec 中的 CBOW 模型很类似。不同之处在于FastText是预测标签,而 CBOW 模型预测中间词。

fasttext是由3层组成:

  • 输入层:是对文档embedding之后的向量, 包含N-gram特征

  • 隐藏层:是对输入数据的求和平均

  • 输出层:是文档对应的label

如下图所示:

1752767199452

层次softmax

计算超多类别分类时,为了提高效率,不再使用传统的softmax来进行多分类的计算,可使用哈夫曼树,使用层次化的softmax来进行概率的计算。

普通softmax:求每个单词的概率分布。如下图:

1752767335592

层次softmax:求每个单词的概率分布转化为求单词路径的概率乘积。如下图:

1752767352624

霍夫曼树作用:用来构建带权最小二叉树。霍夫曼树,也称哈夫曼树、最优二叉树,因为:huffman

霍夫曼树目的:完成哈夫曼编码,哈夫曼编码是一种变长、极少多余编码方案。

说明如下:

带权路径长度WPL(weighted path length):
左图的叶子节点为: 3,5,7,9
左图的wpl计算:3*3 + 5*3 + 7*2 + 9*1 = 47  
WPL最小的二叉树就是赫夫曼树

如下图:

1752767765091

哈夫曼树如何构建?

  • 第一步:假设有四个Label分别为:A~D。统计其在语料库出现的频数。

1752767680922

  • 第二步:第一次合并建树

1752767686392

  • 第三步:第二次合并建树

1752767691895

  • 第四步:第三次合并建树

1752767697111

负采样

为什么需要负采样?

  • 神经网络经过一个训练样本的训练,它的权重就会进行一次调整。

  • 比如利用Skip-Gram进行词向量训练:如果词汇量的数量为上万个,利用softmax计算概率时,需要计算上万个概率值,且每个值都需要进行反向传播更新模型参数,这是非常消耗计算资源的,并且实际中训练起来会非常慢。

  • 负采样每次让一个训练样本仅仅更新一小部分的权重,这样就会降低梯度下降的计算量。

负采样的优势?

  • 提高训练速度,选择了部分数据计算损失,损失计算更加简单。

  • 改进效果,增加部分负样本,能够模拟真实场景下的噪声情况,能够让模型的稳健性更强。

扩展阅读:

# 概念名词解释
下采样/降采样:一堆样本100个,采10个。
上采样/升采样:一堆样本100个,采200个。
负采样:通过随机选择负样本,来减少softmax的计算量。

# 作用
下/降采样:数据预处理技术,用于减少数据量。
上/升采样:数据预处理技术,用于增加数据量。
负采样:模型优化技术。用于提升训练效率。

【掌握】fasttext文本分类

概述

文本分类的是将文档(例如电子邮件,帖子,文本消息,产品评论等)分配给一个或多个类别。

训练文本分类模型需要是有监督学习,需要标签。

分类

  • 二分类:文本被分类两个类别,往往这两个类别是对立面。比如: 判断一句评论是好评还是差评。

  • 单标签多分类:文本可被分成多个类别中的一个类别(即被打上某一个标签)。比如:输入一个人名, 判断它是来自哪个国家的人名。

  • 多标签多分类:文本可被分成多个类别中的一多类别(即被打上多个个标签)。比如: 输入一段讨论,话题可以美食、体育新闻、游戏。

步骤

第一步: 获取数据
第二步: 训练集与验证集的划分
第三步: 训练模型
第四步: 使用模型进行预测并评估
第五步: 模型调优
第六步: 模型保存与重加载

获取数据

案例:本案例烹饪相关的数据集, 它是由facebook AI实验室提供的演示数据集。

数据:

__label__sauce __label__cheese How much does potato starch affect a cheese sauce recipe?
__label__food-safety __label__acidity Dangerous pathogens capable of growing in acidic environments
__label__cast-iron __label__stove How do I cover up the white spots on my cast iron stove?
__label__restaurant Michelin Three Star Restaurant; but if the chef is not there
__label__knife-skills __label__dicing Without knife skills, how can I quickly and accurately dice vegetables?
__label__storage-method __label__equipment __label__bread What's the purpose of a bread box?
__label__baking __label__food-safety __label__substitutions __label__peanuts how to seperate peanut oil from roasted peanuts at home?
__label__chocolate American equivalent for British chocolate terms
__label__baking __label__oven __label__convection Fan bake vs bake
__label__sauce __label__storage-lifetime __label__acidity __label__mayonnaise Regulation and balancing of readymade packed mayonnaise and other sauces

数据说明:

fasttext工具数据格式:__label__AAA__label__BBB text1text1text1

多标签多分类问题

训练集与验证集划分

# 12404条数据作为训练数据
head -n 12404 cooking.stackexchange.txt > cooking_train.txt

# 3000条数据作为验证数据
tail -n 3000 cooking.stackexchange.txt > cooking_valid.txt

训练模型

# 导入fasttext
import fasttext

# 使用fasttext的train_supervised方法进行文本分类模型的训练
model = fasttext.train_supervised("../data/cooking_train.txt")

预测和评估

# 使用模型预测一段输入文本, 通过我们常识, 可知预测是正确的, 但是对应预测概率并不大
model.predict("Which baking dish is best to bake a banana bread ?")

# 元组中的第一项代表标签, 第二项代表对应的概率
(('__label__baking',), array([0.06550845]))


# 为了评估模型到底表现如何, 我们在3000条的验证集上进行测试
model.test("data/cooking/cooking_valid.txt")

# 元组中的每项分别代表, 验证集样本数量, 精度以及召回率 
(3000, 0.124, 0.0541)

模型调优

  • 数据方面:重新处理数据
# 使标点符号与单词分离并统一使用小写字母
cat cooking.stackexchange.txt | sed -e "s/\([.\!?,'/()]\)/ \1 /g" | tr "[:upper:]" "[:lower:]" > cooking.preprocessed.txt

# 划分数据集
head -n 12404 cooking.preprocessed.txt > cooking.pre.train
tail -n 3000 cooking.preprocessed.txt > cooking.pre.valid

重新进行模型训练:

model = fasttext.train_supervised(input="data/cooking/cooking.pre.train")

model.test("data/cooking/cooking.pre.valid")
# 我们看到精度和召回率都有所提升
(3000, 0.161, 0.06962663975782038)
  • 增加训练轮数
model = fasttext.train_supervised(input="data/cooking/cooking.pre.train", epoch=25)

model.test("data/cooking/cooking.pre.valid")
  • 调整学习率
model = fasttext.train_supervised(input="data/cooking/cooking.pre.train", epoch=25, lr=1.0)

model.test("data/cooking/cooking.pre.valid")
  • 增加n-gram特征
model = fasttext.train_supervised(input="data/cooking/cooking.pre.train", epoch=25, lr=1.0, wordNgrams=2)

model.test("data/cooking/cooking.pre.valid")
  • 修改损失计算方式
model = fasttext.train_supervised(input="data/cooking/cooking.pre.train", epoch=25, lr=1.0, wordNgrams=2, loss='hs')

model.test("data/cooking/cooking.pre.valid")
  • 自动超参数调优
model = fasttext.train_supervised(input="data/cooking/cooking.pre.train", autotuneValidationFile='data/cooking/cooking.pre.valid', autotuneDuration=600)

model.test("data/cooking/cooking.pre.valid")
  • 实际生产中多标签多分类问题的损失计算方式
model = fasttext.train_supervised(input="data/cooking/cooking.pre.train", epoch=25, lr=1.0, wordNgrams=2, loss='ova')

# 参数k代表指定模型输出多少个标签, 默认为1, 这里设置为-1, 意味着尽可能多的输出.
# 参数threshold代表显示的标签概率阈值, 设置为0.5, 意味着显示概率大于0.5的标签
model.predict("Which baking dish is best to bake a banana bread ?", k=-1, threshold=0.5)

模型保存和加载

# 使用model的save_model方法保存模型到指定目录
model.save_model("data/model/model_cooking.bin")

# 使用fasttext的load_model进行模型的重加载
model = fasttext.load_model("data/model/model_cooking.bin")

# 重加载后的模型使用方法和之前完全相同
model.predict("Which baking dish is best to bake a banana bread ?", k=-1, threshold=0.5)
((u'__label__baking', u'__label__bananas', u'__label__bread'), array([1.00000, 0.939923, 0.592677]))

【掌握】fasttext词向量迁移

概述

使用在大型语料库上已经进行训练完成的词向量模型。

也就是说,已经存在的、已经训练好的词向量,拿过来直接用。

fasttext提供的可迁移的词向量

#1.
fasttext提供了157种语言的在CommonCrawl和Wikipedia语料上进行训练的可迁移词向量模型, 它们采用CBOW模式进行训练, 词向量维度为300维. 可通过该地址查看具体语言词向量模型: https://fasttext.cc/docs/en/crawl-vectors.html

#2.
fasttext提供了294种语言的在Wikipedia语料上进行训练的可迁移词向量模型, 它们采用skipgram模式进行训练, 词向量维度同样是300维. 可通过该地址查看具体语言词向量模型: https://fasttext.cc/docs/en/pretrained-vectors.html

案例

#1.下载词向量模型文件

#2.解压文件

#3.加载bin文件获取词向量

#4.利用邻近词进行效果检验

实现

#1.下载词向量模型压缩的bin.gz文件
wget https://dl.fbaipublicfiles.com/fasttext/vectors-crawl/cc.zh.300.bin.gz

#2.解压bin.gz文件到bin文件
gunzip cc.zh.300.bin.gz

# 3.加载模型
model = fasttext.load_model("cc.zh.300.bin")

# 4.获取词向量
model.get_word_vector("音乐")
# 获取邻近词
model.get_nearest_neighbors("音乐")

【了解】迁移学习

概念

把预训练模型中已经学习知识迁移到另外一个地方,形象比喻成迁移学习。

迁移方式

方式一:直接使用预训练模型,也就是开箱即用。比如fasttext工具包。

方式二:微调预训练模型。

名词介绍

预训练模型

  • 一般情况下预训练模型都是大型模型。

特点:网络复杂,参数多,需要大量的数据进行训练

  • 在NLP领域,预训练模型往往是语言模型。

因语言模型的训练是无监督的,可以获得大规模语料。比如:输入一句话,生成下一个单词、或者句子,这就是语言模型。比如:打字软件是一个AI程序,是一个语言模型。

  • 语言模型又是许多典型NLP任务的基础,如机器翻译,文本生成,阅读理解等。

常见预训练模型BERT, GPT, roBERTa, transformer-XL

微调

  • 微调的对象
  • 预训练模型

  • 微调的方式:

  • 1.微调预训练模型的参数
  • 2.增加1层或2层输出结构再用小批量标准数据进行重新训练

  • 微调的目标

  • 让模型更加适应特性任务

说白了,就是下载一个预训练模型比如bert,再用自己的小数据重新训练一下,这个动作就是微调。

【掌握】NLP常见预训练模型

常见预训练模型可以分为三类:

  • 自编码语言模型(Auto Encoder Language Model,简称AE)

主要用来进行文本理解;比如: 阅读理解,完型填空,提取句子特征做分类。

代表模型:BERT。

  • 自回归语言模型(Auto Regressive Language Model ,简称AR)

主要用来进行文本生成,比如:文本摘要、翻译、文本写作。

代表模型:GPT。

  • Seq2Seq混合模型(Sequence-to-Sequence,就是AR+AR)

主要用来做综合任务,比如:摘要、翻译、看图说话。

代表模型:T5。

【实现】Transformers库的使用

了解Transformers库

Transformers是HuggingFace公司开源的、基于 transformer 模型结构的,提供预训练语言库。

HuggingFace的开源社区,尤其是在github上开源的预训练模型库Transformers已成为工业风向标。

HuggingFace网址:https://huggingface.co/

魔搭网址:https://www.modelscope.cn/

如果环境中没有transformers库,则需要安装:

# transformers库的安装命令
pip install transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
或者
pip install transformers-4.46.3-py3-none-any.whl

# 额外安装一个datasets库
pip install datasets -i https://pypi.tuna.tsinghua.edu.cn/simple

说明:

transformers-4.46.3-py3-none-any.whl文件在今日资料目录下。

Transformers库三层应用结构

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

管道方式

文本分类任务

文本分类是指模型可以根据文本中的内容来进行分类。

import numpy as np
from transformers import pipeline

# 1.文本分类任务
def text_classification():
    # 定义模型
    model = pipeline(task="text-classification", model="../model/chinese_sentiment")
    # 直接预测
    output = model(inputs="我爱北京天安门,天安门上太阳升。")
    # 数出结果
    print('output-->', output)


if __name__ == '__main__':
    text_classification()

特征提取任务

特征抽取任务只返回文本处理后的特征,属于预训练模型的范畴。特征抽取任务的输出结果需要和其他模型一起工作。

# 2.特征提取任务
def feature_extraction():
    # 定义模型
    model = pipeline(task="feature-extraction", model="../model/bert-base-chinese")
    # 直接预测
    output = model(inputs="人生该如何起头")
    # 数出结果
    print('output-->', np.array(output).shape)


if __name__ == '__main__':
    # text_classification()
    feature_extraction()

完形填空任务

完型填空任务又被叫做“遮蔽语言建模任务”,它属于BERT模型训练过程中的子任务。

# 3.完形填空任务
def fill_mask():
    # 定义模型
    model = pipeline(task="fill-mask", model="../model/chinese-bert-wwm")
    # 直接预测
    output = model("我想明天去[MASK]家吃饭.")
    print('output-->', output)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    fill_mask()

阅读理解任务

阅读理解任务又称为“抽取式问答任务”,即输入一段文本和一个问题,让模型输出结果。

# 4.阅读理解任务
def question_answer():
    context = '我叫张三,我是一个程序员,我的喜好是打篮球。'
    questions = ['我是谁?', '我是做什么的?', '我的爱好是什么?']
    model = pipeline(task="question-answering", model="../model/chinese_pretrain_mrc_roberta_wwm_ext_large")
    output = model(context=context, question=questions)
    print('output-->', output)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    # fill_mask()
    question_answer()

文本摘要任务

文本摘要,就是输入一段文本,输出是一段概况、简单的文字。说白了,就是对一段话进行总结。

# 5.文本摘要任务
def summarization():
    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."
    model = pipeline(task="summarization", model="../model/distilbart-cnn-12-6")
    output = model(text)
    print("output-->", output)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    # fill_mask()
    # question_answer()
    summarization()

NER任务

命名实体识别(NER)任务是NLP中的基础任务。它用于识别文本中的人名(PER)、地名(LOC)、组织(ORG)以及其他实体(MISC)等。例如:(王 B-PER) (小 I-PER) (明 I-PER) (在 O) (办 B-LOC) (公 I-LOC) (室 I-LOC)。其中O表示一个非实体,B表示一个实体的开始,I表示一个实体块的内部。

命名实体识别本质上是一个分类任务(又叫序列标注任务),命名实体识别是句法分析的基础,而句法分析又是NLP任务的核心。

# 6.NER任务
def ner():
    model = pipeline(task="token-classification", model="../model/roberta-base-finetuned-cluener2020-chinese")
    output = model("我爱北京天安门,天安门上太阳升")
    print("output-->", output)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    # fill_mask()
    # question_answer()
    # summarization()
    ner()

自动模型方式

文本分类任务

文本分类是指模型可以根据文本中的内容来进行分类。

# 1.文本分类任务
def text_classification():
    # 1.加载tokenizer
    my_tokenizer = AutoTokenizer.from_pretrained("../model/chinese_sentiment")
    print('my_tokenizer-->', my_tokenizer)

    # 2.加载模型
    model = AutoModelForSequenceClassification.from_pretrained("../model/chinese_sentiment")
    print("model-->", model)

    # 3.准备句子
    message = "人生该如何起头"

    # 4.对句子进行编码
    # padding:True/longest,则表示以最长的句子打padding,max_length,表示以max_length的长度打padding
    msg_encode = my_tokenizer.encode(text=message, padding='longest', truncation=True, max_length=20, return_tensors='pt')
    print("msg_encode-->", msg_encode, msg_encode.shape)

    # 5.传入模型进行预测
    model.eval()
    output = model(msg_encode)
    print("output-->", output)


if __name__ == '__main__':
    text_classification()

特征提取任务

特征抽取任务只返回文本处理后的特征,属于预训练模型的范畴。特征抽取任务的输出结果需要和其他模型一起工作。

# 2.特征提取任务
def feature_extraction():
    # 1.加载tokenizer
    my_tokenizer = AutoTokenizer.from_pretrained("../model/bert-base-chinese")
    print("my_tokenizer-->", my_tokenizer)

    # 2.加载模型
    model = AutoModel.from_pretrained("../model/bert-base-chinese")
    print("model-->", model)

    # 3.准备句子
    message = ["人生该如何起头","你是谁"]

    # 4.对句子进行编码
    # encode_plus:更加强大的编码方式
    output1 = my_tokenizer.encode_plus(text=message, padding="max_length", truncation=True, return_tensors='pt', max_length=30)
    # input_ids:根据词表,用于将词表转为id,起始为101,分隔符为102
    # token_type_ids:标识不同的句子,用0标识第一个句子,用1表示第二个句子
    # attention_mask:把padding的位置标识为0,非padding的位置标识为1
    print('output1-->', output1)


    # 5.传入模型进行预测
    model.eval()
    output = model(**output1)
    # token级别的词向量
    print("output-->", output.last_hidden_state.shape) # [1, 30, 768]
    # 从[1,30,768]中拿出 CLS[1,768],经过linear层(768,768),不改变维度,得到[1,768],这个数据就代表整个句子,也称为句向量。
    print("output-->", output.pooler_output.shape) # [1, 768]


if __name__ == '__main__':
    # text_classification()
    feature_extraction()

完形填空任务

完型填空任务又被叫做“遮蔽语言建模任务”,它属于BERT模型训练过程中的子任务。

# 3.完形填空任务
def fill_mask():
    #1.加载分词器
    tokenizer = AutoTokenizer.from_pretrained("../model/chinese-bert-wwm")
    print("tokenizer-->", tokenizer)

    #2.加载模型
    model = AutoModelForMaskedLM.from_pretrained("../model/chinese-bert-wwm")
    print("model-->", model)

    #3.准备语句
    message = "我今天中午准备去[MASK]家吃饭"

    #4.对句子进行编码
    input = tokenizer.encode_plus(message, return_tensors='pt')
    print('output1-->', input)

    #5.送入模型
    model.eval()
    output = model(**input)
    print('output-->', output.logits.shape) # [1, 14, 21128]

    # 一定要item(),把数据取出来,logits就是预测结果。
    index = torch.argmax(output.logits[0][9]).item()
    print("mask_pred_idx-->", index)

    res = tokenizer.convert_ids_to_tokens(index)
    print("res-->", res)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    fill_mask()

阅读理解任务

阅读理解任务又称为“抽取式问答任务”,即输入一段文本和一个问题,让模型输出结果。

# 4.阅读理解任务
def reading():
    #1.加载分词器
    tokenizer = AutoTokenizer.from_pretrained("../model/chinese_pretrain_mrc_roberta_wwm_ext_large")
    #2.加载模型
    model = AutoModelForQuestionAnswering.from_pretrained("../model/chinese_pretrain_mrc_roberta_wwm_ext_large")
    #3.准备语句,并且编码
    context = '我叫张三,我是一个程序员,我的喜好是打篮球'
    questions = ['我是谁', '我是做什么的', '我的爱好是什么']
    #4.模型预测
    model.eval()
    for question in questions:
        input = tokenizer.encode_plus(question, context,  return_tensors='pt')
        print("input-->", input)
        output = model(**input)
        print("output-->", output)
        # 获取开始和结束的logits
        start = torch.argmax(output.start_logits, dim=-1)
        end = torch.argmax(output.end_logits, dim=-1)
        print("start-->", start)
        print("end-->", end)
        # 把logits转换为汉字
        answer = tokenizer.convert_ids_to_tokens(input["input_ids"][0][start: end + 1])
        print("question-->", question)
        print("answer-->", answer)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    # fill_mask()
    reading()

文本摘要任务

文本摘要,就是输入一段文本,输出是一段概况、简单的文字。说白了,就是对一段话进行总结。

# 5.文本摘要任务
def summarization():
    #1.加载分词器
    tokenizer = AutoTokenizer.from_pretrained("../model/distilbart-cnn-12-6")
    #2.加载模型
    model = AutoModelForSeq2SeqLM.from_pretrained("../model/distilbart-cnn-12-6")
    #3.准备语句,编码
    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."

    inputs = tokenizer.encode_plus(text, return_tensors="pt")
    #4.送入模型,进行摘要生成
    model.eval()
    output = model.generate(inputs["input_ids"])
    # [1, 84]:表示生成了一段话,这段话有84个token
    print('output-->', output, output.shape)
    #5.解码
    res = tokenizer.decode(output[0], skip_special_tokens=True, clean_up_tokenization_spaces=False)
    print("res-->", res)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    # fill_mask()
    # reading()
    summarization()

NER任务

命名实体识别(NER)任务是NLP中的基础任务。它用于识别文本中的人名(PER)、地名(LOC)、组织(ORG)以及其他实体(MISC)等。例如:(王 B-PER) (小 I-PER) (明 I-PER) (在 O) (办 B-LOC) (公 I-LOC) (室 I-LOC)。其中O表示一个非实体,B表示一个实体的开始,I表示一个实体块的内部。

命名实体识别本质上是一个分类任务(又叫序列标注任务),命名实体识别是句法分析的基础,而句法分析又是NLP任务的核心。

# 6.NER任务
def ner():
    #1.加载分词器
    tokenizer = AutoTokenizer.from_pretrained("../model/roberta-base-finetuned-cluener2020-chinese")
    #2.加载模型
    model = AutoModelForTokenClassification.from_pretrained("../model/roberta-base-finetuned-cluener2020-chinese")
    configs = AutoConfig.from_pretrained("../model/roberta-base-finetuned-cluener2020-chinese")
    #3.准备语料,编码
    message = '我爱北京天安门,天安门上太阳升'
    inputs = tokenizer.encode_plus(message, return_tensors='pt')
    print("inputs-->", inputs)
    #4.送入模型,进行预测
    model.eval()
    output = model(**inputs)
    # [1, 17, 32],默认对每一个token做了一个32的分类
    print("output-->", output.logits, output.logits.shape) # [1, 17, 32]
    #5.获得添加特殊字符的句子
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
    print("tokens-->", tokens)
    #6.判断token是否是特殊符号,特殊符号一定不是实体。
    output_list = []
    for token, logit in zip(tokens, output.logits[0]):
        if token in tokenizer.all_special_tokens:
            continue
        # 得到预测结果的index
        index = torch.argmax(logit, dim=-1).item()
        # 把index转换为label
        label = configs.id2label[index]
        output_list.append((token, label))
    print("output_list-->", output_list)


if __name__ == '__main__':
    # text_classification()
    # feature_extraction()
    # fill_mask()
    # reading()
    # summarization()
    ner()

具体模型方式

完型填空任务又被叫做“遮蔽语言建模任务”,它属于BERT模型训练过程中的子任务。

下面使用具体模型来实现。

import torch
from transformers import BertForMaskedLM, BertTokenizer
def fill_mask():
    #1.加载分词器
    tokenizer = BertTokenizer.from_pretrained("../model/chinese-bert-wwm")
    #2.加载模型
    model = BertForMaskedLM.from_pretrained("../model/chinese-bert-wwm")
    #3.准备语料,编码
    message = "我明天准备去[MASK]家吃饭"
    inputs = tokenizer.encode_plus(message, return_tensors='pt')
    print("inputs-->", inputs)
    #4.送入模型,进行预测
    output = model(**inputs).logits
    print("output-->", output, output.shape)
    #5.获取结果
    index = torch.argmax(output[0][7]).item()
    print("index-->", index)
    res = tokenizer.convert_ids_to_tokens(index)
    print("res-->", res)


if __name__ == '__main__':
    fill_mask()
💻 代码

微调实战代码

3 个文件
  • 📄 demo.py 1.1KB
    import torch
    import torch.nn as nn
    
    
    def dm01():
        # 按照Bert模型的维度:768维,在这里,我们使用8维度演示
        # num_embeddings:20,词表大小
        # embedding_dim:8,词的维度
        # 把每个token都使用8维向量表示,一共是20个token
        embedded = nn.Embedding(21, 8)
    
        # 准备一句话,shape(1, 5),1句话,5个token
        input = torch.tensor([[1, 2, 5, 7, 9]])
    
        # 把这句话送给模型
        result = embedded(input)
        print("result1-->", result)
        # 问题一:这个结果是几维?3维。
        # 问题二:维度分别是多少?【1,5,8】
    
        # 问题三:embedding的加强
        # 准备一句话,shape(1, 6),1句话,6个token
        input = torch.tensor([[1, 2, 5, 7, 9, 20]])
        result = embedded(input)
        print("result2-->", result)  # 报错,index out of range in self,越界了
        # 解决方案
        # 方式一:只需要把输入数据index调小,最大值是19
        # input = torch.tensor([[1, 2, 5, 7, 9, 19]])
        # 方式二:不需要修改输入数据,只需要调整词表大小
        # embedded = nn.Embedding(21, 8)
    
  • 📄 demo01_pipeline.py 4.9KB
    import numpy as np
    from transformers import pipeline
    
    
    # TODO 1.文本分类
    def dm01():
        # 基于预训练模型构建模型对象
        model = pipeline(task='text-classification', model='../models/chinese_sentiment')
        # print("model-->", model)
    
        #  准备输入数据
        inputs = '我不太喜欢天安门'
    
        # 把数据送给模型,得到结果,model()->调用forward方法
        result = model(inputs)
        print("result-->", result)
    
    
    # TODO 2. 特征提取任务
    def dm02():
        # 得到模型对象
        model = pipeline(task='feature-extraction', model='../models/bert-base-chinese')
        # 把文本送给模型,得到结果
        result = model('人生该如何起头')
        print("result-->", result)
        # [1, 9, 768] -> 1句话,9个token,每个token使用一个768维度的向量表示
        # 1:一句话
        # 9:9个token,分别是:【CLS 人 生 该 如 何 起 头 SEP】
        # 768:维度数
        print('shape-->', np.array(result).shape)
    
    
    # TODO 3.完形填空任务
    def dm03():
        # 创建模型对象
        model = pipeline(task='fill-mask', model='../models/chinese-bert-wwm')
        # 把数据送给模型,得到结果
        # result = model("我明天想去[MASK]家吃饭")
        result = model("我想明天去[MASK]边钓鱼")
        print("result-->", result)
    
    
    # TODO 4.阅读理解任务
    def dm04():
        # 准备数据
        context = '我叫张三,我是一个程序员,我的喜好是打篮球。'
        questions = ['我是谁?', '我是做什么的?', '我的爱好是什么?']
        # 得到模型对象
        model = pipeline(task='question-answering', model='../models/chinese_pretrain_mrc_roberta_wwm_ext_large')
        # 把数据送给模型
        result = model(context=context, question=questions)
        print("result-->", result)
    
    
    # TODO 5.文本摘要任务
    def dm05():
        # 得到预训练模型对象
        model = pipeline(task='summarization', model='../models/distilbart-cnn-12-6')
        # 准备文本
        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."
        xxx = """
        先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。
        诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
    宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
    侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。
    将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
    亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。
        """
        # 把文本送给模型,得到结果
        result = model(text)
        print("result-->", result)
    
    
    def dm06():
        # 得到预训练模型对象
        model = pipeline(task='token-classification',
                         model='../models/roberta-base-finetuned-cluener2020-chinese')
        # 把数据送给模型
        result = model("我喜欢杭州西湖")
        print("result-->", result)
    
    
    if __name__ == '__main__':
        # dm01()
        # dm02()
        # dm03()
        # dm04()
        # dm05()
        dm06()
    
  • 📄 demo02_automodel.py 10.2KB
    from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModel, AutoModelForMaskedLM, \
        AutoModelForQuestionAnswering, AutoModelForSeq2SeqLM, AutoModelForTokenClassification, AutoConfig
    import torch
    
    # TODO 文本分类任务
    def dm01():
        # 1.加载预训练模型(分词器,模型)
        tokenizer = AutoTokenizer.from_pretrained("../models/chinese_sentiment")
        model = AutoModelForSequenceClassification.from_pretrained("../models/chinese_sentiment")
        # 2.准备数据
        message = '我爱北京天安门'
        # 3.对数据进行预处理,得到张量(文本数值化+数值张量化)
        # text:文本
        # truncation:如果超过MAX_LENGTH,就截断
        # max_length:最长长度
        # return_tensors:pt 返回PyTorch的张量,默认不返回张量,返回list列表
        inputs = tokenizer.encode(text=message,
                                  padding='max_length',
                                  truncation=True,
                                  max_length=20,
                                  return_tensors='pt')
        print("inputs-->", inputs)
        # 4.把数据送给模型
        output = model(inputs)
        # 5.打印结果
        print("output-->", output)
        print("output-->", output.logits) # 未经过softmax的logits值
        print("output-->", torch.argmax(output.logits, dim=-1))
    
    
    # TODO 特征提取任务
    def dm02():
        # 加载模型和分词器
        tokenizer = AutoTokenizer.from_pretrained("../models/bert-base-chinese")
        model = AutoModel.from_pretrained("../models/bert-base-chinese")
        # 准备文本
        message = ['你是谁啊', '人生该如何起头']
        # 编码的方法有三个:
        # encode:普通编码,就是张量数据(input_ids)
        # encode_plus:加强版的编码,会多一些(input_ids,token_type_ids,attention_mask)
        # batch_encode_plus:真正意义上多个样本的编码(后面再用)
        # 对文本进行数值化+张量化
        # input = tokenizer.encode(message, padding="max_length", max_length=20, truncation=True, return_tensors='pt')
        # print('input-->', input)
        # input_ids:对数据进行数值化+张量化后的结果
        # token_type_ids:句子类别  CLS 第一句话   SEP            第二句话  SEP           填充的0
        #                         0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0
        # attention_mask:注意力掩码,1需要掩码
        # 参数小结:input_ids不能少,attention_mask最好要,token_type_ids可有可无。
        input = tokenizer.encode_plus(message, padding="max_length", max_length=20, truncation=True, return_tensors='pt')
        print('input-->', input)
        # 把文本送给模型
        # 写法一:
        # input_ids = input['input_ids']
        # token_type_ids = input['token_type_ids']
        # attention_mask = input['attention_mask']
        # result = model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
        # 写法二:**含义:等价于写法一的key=value的关键字赋值
        result = model(**input)
        # 打印结果
        # print("result-->", result)
        # [1,20,768]:1句话,20个token,每个token使用768维度的向量表示
        # 小结:last_hidden_state:词向量,每个token一个向量,就是词向量
        print("result.last_hidden_state-->", result.last_hidden_state, result.last_hidden_state.shape)
        # [1, 768]:1句话,这句话使用一个768维度的向量表示
        # pooler_output:句向量,一句话(一段文本)的向量表示
        print("result.pooler_output-->", result.pooler_output, result.pooler_output.shape)
    
    
    # TODO 完形填空任务
    def dm03():
        # 加载模型和分词器
        tokenizer = AutoTokenizer.from_pretrained("../models/chinese-bert-wwm")
        model = AutoModelForMaskedLM.from_pretrained("../models/chinese-bert-wwm")
        # 准备数据
        input = tokenizer.encode_plus("我想明天去[MASK]家吃饭", return_tensors='pt')
        print("input-->", input)
        # 把数据送给模型
        output = model(**input)
        # 打印结果
        # print("output-->", output)
        # print("output-->", output.last_hidden_state.shape) # [1, 11, 768]
        # print("output-->", output.pooler_output.shape) # [1, 768]
    
        # [1, 11, 21128]:1句话,11个token,每个token用一个21128的向量表示
        # 含义是:需要基于MASK的位置,从21128个词表中,还原到预测的结果token(词)
        print("output.logits-->", output.logits, output.logits.shape)
        # [11, 21128] -> output.logits[0]
        # [21128] -> output.logits[0][6]
        # print(output.logits[0][6].shape)
        # item:由张量转标量
        index = torch.argmax(output.logits[0][6]).item()
        print("index-->", index)
        # 把下标index从词表中还原成具体的token
        token = tokenizer.convert_ids_to_tokens([index])
        print("token-->", token)
    
    
    # TODO 阅读理解任务
    def dm04():
        # 模型和分词器
        tokenizer = AutoTokenizer.from_pretrained("../models/chinese_pretrain_mrc_roberta_wwm_ext_large")
        model = AutoModelForQuestionAnswering.from_pretrained("../models/chinese_pretrain_mrc_roberta_wwm_ext_large")
        # 准备数据
        context = '我叫张三 我是一个程序员 我的喜好是打篮球'
        questions = ['我是谁?', '我是做什么的?', '我的爱好是什么?']
        # 遍历:一个问题一个问题地送给预训练模型
        for question in questions:
            # 对每一个问题进行编码
            input = tokenizer.encode_plus(question, context, return_tensors='pt')
            print("input-->", input)
            output = model(**input)
            # print("output-->", output)
            print("output.start_logits-->", output.start_logits.shape)
            print("output.end_logits-->", output.end_logits.shape)
            start = torch.argmax(output.start_logits)
            end = torch.argmax(output.end_logits)
            print("start-->", start)
            print("end-->", end)
            print('1-->', input['input_ids'].shape) # 【1,26】
            print('2-->', input['input_ids'][0].shape) # 【26】降低维度
            # convert_ids_to_tokens:把idx还原成token
            answer = tokenizer.convert_ids_to_tokens(input['input_ids'][0][start: end + 1])
            print("question:", question, "answer:", answer)
    
    
    # TODO 文本摘要任务
    def dm05():
        # 模型和分词器
        tokenizer = AutoTokenizer.from_pretrained("../models/distilbart-cnn-12-6")
        model = AutoModelForSeq2SeqLM.from_pretrained("../models/distilbart-cnn-12-6")
        # 准备数据
        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."
        # 对文本进行编码
        # input = tokenizer.encode(text, return_tensors='pt')
        # print("input-->", input)
        input = tokenizer([text], return_tensors='pt')
        # print("input-->", input)
        # 摘要生成
        output = model.generate(input.input_ids)
        # output就是token的下标索引index,需要还原成token
        print("output-->", output)
        # 还原,解码
        print([tokenizer.decode(index, skip_special_tokens=True, clean_up_tokenization_spaces=False)
               for index in output])
    
    
    # TODO NER任务
    class AutoModelTokenClassification:
        pass
    
    
    # TODO NER任务(命名实体识别)token级别的分类
    def dm06():
        # 模型和分词器
        tokenizer = AutoTokenizer.from_pretrained("../models/roberta-base-finetuned-cluener2020-chinese")
        model = AutoModelForTokenClassification.from_pretrained("../models/roberta-base-finetuned-cluener2020-chinese")
        # 导入config
        config = AutoConfig.from_pretrained("../models/roberta-base-finetuned-cluener2020-chinese")
        # print("config-->", config)
        # 准备数据
        inputs = tokenizer.encode_plus('我爱北京天安门', return_tensors='pt')
        print("inputs-->", inputs)
        # 对数据进行编码
        output = model(**inputs)
        # print("output-->", output)
        logits = output.logits
        # [1, 9, 32]:1句话,9个token,每个token的NER类别数是32
        print("logits-->", logits.shape)
    
        # 把这一句话还原成原始的token
        input_tokens = tokenizer.convert_ids_to_tokens(inputs.input_ids[0])
        #  ['[CLS]', '我', '爱', '北', '京', '天', '安', '门', '[SEP]']
        print("input_tokens-->", input_tokens)
    
        outputs = []
        # 遍历token和所有的value
        for token, value in zip(input_tokens, logits[0]):
            # 如果是特殊token(CLS、MASK、SEP),应该过滤掉
            if token in tokenizer.all_special_tokens:
                continue
    
            # print("token-->", token)
            # print("value-->", value)
            # 取value中的最大值,也就是32个类别中最有可能的类别
            idx = torch.argmax(value).item()
            # print("idx-->", idx)
            label = config.id2label[idx]
            outputs.append((token , label))
    
        # 输出outputs
        print("outputs-->", outputs)
    
    
    
    
    
    
    if __name__ == '__main__':
        # dm01()
        # dm02()
        # dm03()
        dm04()
        # dm05()
        # dm06()
    

第9节 · 第9章 微调实战:数据与训练(day13)

🎯 掌握数据集加载与训练评估流程。
📖 笔记

NLP 基础(day13)

NLP基础

【了解】今日内容介绍

  • 中文分类任务
  • 中文填空任务

【区别】自动模型和指定模型

#1.自动模型:
tokenizer:AutoTokenizer
model:AutoModel/ AutoModelForXXX

#2.指定模型(具体模型)
Tokenizer:XXXTokenizer,XXX就是模型名称,比如BertTokenizer
model:XXX / XXXForYYY,XXX就是模型名称,比如BertForTokenClassification

微调怎么做?

之前的预训练模型中,使用方式:

把数据送给模型,直接出结果(除了特征提取任务外)

因此,也叫开箱即用。

微调,顾名思义,需要稍微调整一下模型。因此需要有一些模型结果的变化:

预训练模型 + 下游线性层

# 解释
预训练模型:专门用来做特征提取
下游线性层:用来实现多分类(开发者自定义),因此非常


# 结论
部分参数微调:微调下游自定义线性层参数。
全参微调:预训练模型参数和自定义下游线性层参数都会改变。

【实现】中文分类任务

需求说明

直接加载预训练模型进行输入文本的特征表示, 后接自定义网络进行微调输出结果。

label,text
1,这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般
1,交通方便;环境很好;服务态度很好 房间较小

获取数据

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import BertModel, BertTokenizer, AdamW
from datasets import load_dataset
from tqdm import tqdm

# 加载预训练模型
bert_tokenizer = BertTokenizer.from_pretrained("../model/bert-base-chinese")
bert_model = BertModel.from_pretrained("../model/bert-base-chinese")


# 批量数据处理函数(二次数据处理函数)
def collate_fn(data):
    # data:[{"label":1, "text":"xxxx"}, {},....]
    # print(data)
    inputs = [i['text'] for i in data]
    labels = [i['label'] for i in data]
    # print(inputs)
    # print(labels)
    inputs = bert_tokenizer.batch_encode_plus(inputs, padding='max_length', truncation=True, max_length=32, return_tensors='pt')
    input_ids = inputs['input_ids']
    attention_mask = inputs['attention_mask']
    token_type_ids = inputs['token_type_ids']
    labels = torch.LongTensor(labels)
    return input_ids, attention_mask, token_type_ids, labels


# 准备dataloader
def get_dataloader():
    #1.加载数据
    dataset = load_dataset("csv", data_files='../data/train.csv', split="train")
    dataset = dataset.filter(lambda x:len(x['text']) > 32)
    #2.构建dataloader
    my_dataloader = DataLoader(dataset, batch_size=8, shuffle=True, collate_fn=collate_fn, drop_last=True)

    # input_ids, attention_mask, token_type_ids, labels = next(iter(my_dataloader))
    # print("input_ids-->", input_ids, input_ids.shape)
    # print("attention_mask-->", attention_mask, attention_mask.shape)
    # print("token_type_ids-->", token_type_ids, token_type_ids.shape)
    # print("labels-->", labels, labels.shape)
    #3.返回dataloader
    return my_dataloader


if __name__ == '__main__':
    get_dataloader()

定义模型

# 定义模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(768, 2)

    def forward(self, input_ids, attention_mask, token_type_ids):
        # bert模型要接收这3个参数
        with torch.no_grad():
            bert_out = bert_model(input_ids, attention_mask, token_type_ids)
        # print("bert_out-->", bert_out['last_hidden_state'].shape)
        # print("bert_out-->", bert_out['pooler_output'].shape)
        # output = self.linear(bert_out['pooler_output']) # 直接使用pooler_output,这就是句子的向量表示
        output = self.linear(bert_out['last_hidden_state'][:, 0]) # 取第一个位置的词向量表示,代表该条样本的结果
        # print("output-->", output, output.shape)
        return output



# 测试模型
def test_my_model():
    my_dataloader = get_dataloader()
    my_model = MyModel()
    # input_ids, attention_mask, token_type_ids, labels = next(iter(my_dataloader))
    for input_ids, attention_mask, token_type_ids, labels in my_dataloader:
        output = my_model(input_ids, attention_mask, token_type_ids)
        print("output-->", output)
        break


if __name__ == '__main__':
    # get_dataloader()
    test_my_model()

模型训练

# 模型训练
def train_model():
    # 准备数据
    my_dataloader = get_dataloader()
    # 准备模型
    my_model = MyModel()
    # 准备损失函数,优化器
    my_loss_fn = nn.CrossEntropyLoss()
    my_optimizer = AdamW(params=my_model.parameters(), lr=5e-4)

    for p in bert_model.parameters():
        p.requires_grad_(False)

    epochs = 1
    my_model.train()

    for epoch in range(epochs):
        for i,(input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
            out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
            loss = my_loss_fn(out, labels)
            my_optimizer.zero_grad()
            loss.backward()
            my_optimizer.step()

            # 打印观测值
            if i % 10 == 0:
                tmp = torch.argmax(out, dim=-1)
                acc = (tmp == labels).sum().item() / len(labels)
                print('轮次为-->',i, '\t准确率为-->',acc)

            # 保存模型
            torch.save(my_model.state_dict(), '../save/text_classify_%d.bin' % epoch)


if __name__ == '__main__':
    # get_dataloader()
    # test_my_model()
    train_model()

模型评估

# 模型评估
def evalute_model():
    # 1.加载数据
    test_dataset = load_dataset("csv", data_files='../data/test.csv', split='train')
    test_dataloader = DataLoader(test_dataset, batch_size=8, shuffle=True, drop_last=True, collate_fn=collate_fn)
    # 2.加载模型
    my_model = MyModel()
    my_model.load_state_dict(torch.load("../save/text_classify_1.bin"))

    epochs = 1
    my_model.eval()

    total = 0
    accuracy = 0

    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(tqdm(test_dataloader)):
        with torch.no_grad():
            my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # print("my_out-->", my_out, my_out.shape)

        out = torch.argmax(my_out, dim=-1)
        acc = (out == labels).sum().item() / len(labels)
        accuracy += acc
        total += len(labels)

        if i % 10 == 0:
            print('轮次为-->', i, '\t准确率为-->', acc, end=' ')
            str1 = bert_tokenizer.decode(input_ids[0], skip_special_tokens=True)
            print(str1, end=' ')
            predict_label = out[0].item()
            true_label = labels[0].item()
            print("predict_label-->", predict_label, "\ttrue_label-->", true_label)


if __name__ == '__main__':
    # get_dataloader()
    # test_my_model()
    # train_model()
    evalute_model()

所有代码

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import BertModel, BertTokenizer, AdamW
from datasets import load_dataset
from tqdm import tqdm

# 加载预训练模型
bert_tokenizer = BertTokenizer.from_pretrained("../model/bert-base-chinese")
bert_model = BertModel.from_pretrained("../model/bert-base-chinese")


# 批量数据处理函数(二次数据处理函数)
def collate_fn(data):
    # data:[{"label":1, "text":"xxxx"}, {},....]
    # print(data)
    inputs = [i['text'] for i in data]
    labels = [i['label'] for i in data]
    # print(inputs)
    # print(labels)
    inputs = bert_tokenizer.batch_encode_plus(inputs, padding='max_length', truncation=True, max_length=32, return_tensors='pt')
    input_ids = inputs['input_ids']
    attention_mask = inputs['attention_mask']
    token_type_ids = inputs['token_type_ids']
    labels = torch.LongTensor(labels)
    return input_ids, attention_mask, token_type_ids, labels


# 准备dataloader
def get_dataloader():
    #1.加载数据
    dataset = load_dataset("csv", data_files='../data/train.csv', split="train")
    dataset = dataset.filter(lambda x:len(x['text']) > 32)
    #2.构建dataloader
    my_dataloader = DataLoader(dataset, batch_size=8, shuffle=True, collate_fn=collate_fn, drop_last=True)

    # input_ids, attention_mask, token_type_ids, labels = next(iter(my_dataloader))
    # print("input_ids-->", input_ids, input_ids.shape)
    # print("attention_mask-->", attention_mask, attention_mask.shape)
    # print("token_type_ids-->", token_type_ids, token_type_ids.shape)
    # print("labels-->", labels, labels.shape)
    #3.返回dataloader
    return my_dataloader


# 定义模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(768, 2)

    def forward(self, input_ids, attention_mask, token_type_ids):
        # bert模型要接收这3个参数
        with torch.no_grad():
            bert_out = bert_model(input_ids, attention_mask, token_type_ids)
        # print("bert_out-->", bert_out['last_hidden_state'].shape)
        # print("bert_out-->", bert_out['pooler_output'].shape)
        # output = self.linear(bert_out['pooler_output']) # 直接使用pooler_output,这就是句子的向量表示
        output = self.linear(bert_out['last_hidden_state'][:, 0]) # 取第一个位置的词向量表示,代表该条样本的结果
        # print("output-->", output, output.shape)
        return output



# 测试模型
def test_my_model():
    my_dataloader = get_dataloader()
    my_model = MyModel()
    # input_ids, attention_mask, token_type_ids, labels = next(iter(my_dataloader))
    for input_ids, attention_mask, token_type_ids, labels in my_dataloader:
        output = my_model(input_ids, attention_mask, token_type_ids)
        print("output-->", output)
        break

# 模型训练
def train_model():
    # 准备数据
    my_dataloader = get_dataloader()
    # 准备模型
    my_model = MyModel()
    # 准备损失函数,优化器
    my_loss_fn = nn.CrossEntropyLoss()
    my_optimizer = AdamW(params=my_model.parameters(), lr=5e-4)

    for p in bert_model.parameters():
        p.requires_grad_(False)

    epochs = 1
    my_model.train()

    for epoch in range(epochs):
        for i,(input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
            out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
            loss = my_loss_fn(out, labels)
            my_optimizer.zero_grad()
            loss.backward()
            my_optimizer.step()

            # 打印观测值
            if i % 10 == 0:
                tmp = torch.argmax(out, dim=-1)
                acc = (tmp == labels).sum().item() / len(labels)
                print('轮次为-->',i, '\t准确率为-->',acc)

            # 保存模型
            torch.save(my_model.state_dict(), '../save/text_classify_%d.bin' % epoch)


# 模型评估
def evalute_model():
    # 1.加载数据
    test_dataset = load_dataset("csv", data_files='../data/test.csv', split='train')
    test_dataloader = DataLoader(test_dataset, batch_size=8, shuffle=True, drop_last=True, collate_fn=collate_fn)
    # 2.加载模型
    my_model = MyModel()
    my_model.load_state_dict(torch.load("../save/text_classify_1.bin"))

    epochs = 1
    my_model.eval()

    total = 0
    accuracy = 0

    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(tqdm(test_dataloader)):
        with torch.no_grad():
            my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # print("my_out-->", my_out, my_out.shape)

        out = torch.argmax(my_out, dim=-1)
        acc = (out == labels).sum().item() / len(labels)
        accuracy += acc
        total += len(labels)

        if i % 10 == 0:
            print('轮次为-->', i, '\t准确率为-->', acc, end=' ')
            str1 = bert_tokenizer.decode(input_ids[0], skip_special_tokens=True)
            print(str1, end=' ')
            predict_label = out[0].item()
            true_label = labels[0].item()
            print("predict_label-->", predict_label, "\ttrue_label-->", true_label)



if __name__ == '__main__':
    # get_dataloader()
    # test_my_model()
    # train_model()
    evalute_model()

【实现】中文填空任务

需求说明

完型填空任务又被叫做“遮蔽语言建模任务”。

数据文件有三个train.csv,test.csv,validation.csv,数据样式都是一样的。

label,text
1,这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般
1,交通方便;环境很好;服务态度很好 房间较小

获取数据

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import BertTokenizer, BertModel, AdamW
from datasets import load_dataset

tokenizer = BertTokenizer.from_pretrained("../model/bert-base-chinese")
bert_model = BertModel.from_pretrained("../model/bert-base-chinese")

# 批量处理函数
def collate_fn(data):
    # data就是一批数据
    sents = [i['text'] for i in data]

    # 编码
    inputs = tokenizer.batch_encode_plus(sents, padding='max_length',
                                         truncation=True,
                                         max_length=32,
                                         return_tensors='pt')
    input_ids = inputs['input_ids']
    attention_mask = inputs['attention_mask']
    token_type_ids = inputs['token_type_ids']

    # 取出下标为16的值(标签),并且把该位置mask
    labels = input_ids[:, 16].reshape(-1).clone()
    labels = torch.LongTensor(labels)
    input_ids[:, 16] = tokenizer.get_vocab()[tokenizer.mask_token]

    # 返回结果
    return input_ids, attention_mask, token_type_ids, labels


# 获取数据集
def get_dataloader():
    my_datasets = load_dataset('csv', data_files="../data/train.csv", split='train')
    my_datasets = my_datasets.filter(lambda x: len(x['text']) > 32)
    dataloader = DataLoader(my_datasets, batch_size=8, shuffle=True, collate_fn=collate_fn, drop_last=True)

    # input_ids, attention_mask, token_type_ids,labels = next(iter(dataloader))
    # for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(dataloader):
    #     print("input_ids-->", input_ids, input_ids.shape)
    #     print('attention_mask-->', attention_mask, attention_mask.shape)
    #     print('token_type_ids-->', token_type_ids, token_type_ids.shape)
    #     print('labels-->', labels, labels.shape)
    #     break
    return dataloader


if __name__ == '__main__':
    get_dataloader()

定义模型

# 定义模型
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(in_features=768, out_features=tokenizer.vocab_size, bias=False)

    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():
            output = bert_model(input_ids, attention_mask, token_type_ids)
            # last_hidden_state:词向量, pooler_output:句向量
            # print('output-->', output)
        # 千万注意不要写在with里面
        out = self.linear(output.last_hidden_state[:, 16])
        return out


def test_mymodel():
    dataloader = get_dataloader()
    my_model = MyModel()
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(dataloader):
        out = my_model(input_ids, attention_mask, token_type_ids)
        print(out, out.shape)
        break


if __name__ == '__main__':
    # get_dataloader()
    test_mymodel()

模型训练

# 训练模型
def train_model():
    # 准备物料 模型,损失函数,优化器
    my_dataloader = get_dataloader()
    my_model = MyModel()
    my_loss = nn.CrossEntropyLoss()
    my_optimizer = AdamW(params=my_model.parameters(), lr=5e-4)
    epochs = 1
    for p in bert_model.parameters():
        p.requires_grad_(False)

    # 训练模型
    my_model.train()
    for epoch in range(epochs):
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
            output = my_model(input_ids, attention_mask, token_type_ids)
            # print("output-->", output, output.shape)
            # print("label-->", labels, labels.shape)
            loss = my_loss(output, labels)
            my_optimizer.zero_grad()
            loss.backward()
            my_optimizer.step()

            if i % 10 == 0:
                tmp = torch.argmax(output, dim=-1)
                acc = (tmp == labels).sum().item() / len(labels)
                print("i-->", i, "\tloss-->", loss.item(), "\t准确率-->", acc)
        # 保存模型
        torch.save(my_model.state_dict(), "../save/day11/my_model_%d.bin" % epoch)


if __name__ == '__main__':
    # get_dataloader()
    # test_mymodel()
    train_model()

模型评估

加载训练好的模型,换一份数据集,看看模型的表现情况。

# 预测模型
def evalute_model():
    # 1.准备数据
    test_dataset = load_dataset("csv", data_files="../data/test.csv", split='train')
    test_dataset = test_dataset.filter(lambda x:len(x['text']) > 32)
    test_dataloader = DataLoader(test_dataset, batch_size=8, shuffle=True, drop_last=True, collate_fn=collate_fn)

    #2.准备模型
    my_model = MyModel()
    my_model.load_state_dict(torch.load("../save/fill_mask_1.bin"))

    #3.预测
    my_model.eval()
    accuracy = 0
    total = 0
    for i,(input_ids, attention_mask, input_type_ids, labels) in enumerate(test_dataloader):
        with torch.no_grad():
            output = my_model(input_ids, attention_mask, input_type_ids)

        output = torch.argmax(output, dim=-1)
        accuracy += (output == labels).sum().item()
        total += len(labels)

        # 打印观测值
        if i % 10 == 0:
            print('平均准确率为-->\t',accuracy/total)
    print("模型总准确率为-->", accuracy/total)


if __name__ == '__main__':
    # get_dataloader()
    # test_mymodel()
    # train_model()
    evalute_model()

所有代码

该案例的所有代码汇总如下:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import BertTokenizer, BertModel, AdamW
from datasets import load_dataset

tokenizer = BertTokenizer.from_pretrained("../model/bert-base-chinese")
bert_model = BertModel.from_pretrained("../model/bert-base-chinese")

# 批量处理函数
def collate_fn(data):
    # data就是一批数据
    sents = [i['text'] for i in data]

    # 编码
    inputs = tokenizer.batch_encode_plus(sents, padding='max_length',
                                         truncation=True,
                                         max_length=32,
                                         return_tensors='pt')
    input_ids = inputs['input_ids']
    attention_mask = inputs['attention_mask']
    token_type_ids = inputs['token_type_ids']

    # 取出下标为16的值(标签),并且把该位置mask
    labels = input_ids[:, 16].reshape(-1).clone()
    labels = torch.LongTensor(labels)
    input_ids[:, 16] = tokenizer.get_vocab()[tokenizer.mask_token]

    # 返回结果
    return input_ids, attention_mask, token_type_ids, labels


# 获取数据集
def get_dataloader():
    my_datasets = load_dataset('csv', data_files="../data/train.csv", split='train')
    my_datasets = my_datasets.filter(lambda x: len(x['text']) > 32)
    dataloader = DataLoader(my_datasets, batch_size=8, shuffle=True, collate_fn=collate_fn, drop_last=True)

    # input_ids, attention_mask, token_type_ids,labels = next(iter(dataloader))
    # for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(dataloader):
    #     print("input_ids-->", input_ids, input_ids.shape)
    #     print('attention_mask-->', attention_mask, attention_mask.shape)
    #     print('token_type_ids-->', token_type_ids, token_type_ids.shape)
    #     print('labels-->', labels, labels.shape)
    #     break
    return dataloader


# 定义模型
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(in_features=768, out_features=tokenizer.vocab_size, bias=False)

    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():
            output = bert_model(input_ids, attention_mask, token_type_ids)
            # last_hidden_state:词向量, pooler_output:句向量
            # print('output-->', output)
        # 千万注意不要写在with里面
        out = self.linear(output.last_hidden_state[:, 16])
        return out


def test_mymodel():
    dataloader = get_dataloader()
    my_model = MyModel()
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(dataloader):
        out = my_model(input_ids, attention_mask, token_type_ids)
        print(out, out.shape)
        break

# 训练模型
def train_model():
    # 准备物料 模型,损失函数,优化器
    my_dataloader = get_dataloader()
    my_model = MyModel()
    my_loss = nn.CrossEntropyLoss()
    my_optimizer = AdamW(params=my_model.parameters(), lr=5e-4)
    epochs = 1
    for p in bert_model.parameters():
        p.requires_grad_(False)

    # 训练模型
    my_model.train()
    for epoch in range(epochs):
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
            output = my_model(input_ids, attention_mask, token_type_ids)
            # print("output-->", output, output.shape)
            # print("label-->", labels, labels.shape)
            loss = my_loss(output, labels)
            my_optimizer.zero_grad()
            loss.backward()
            my_optimizer.step()

            if i % 10 == 0:
                tmp = torch.argmax(output, dim=-1)
                acc = (tmp == labels).sum().item() / len(labels)
                print("i-->", i, "\tloss-->", loss.item(), "\t准确率-->", acc)
        # 保存模型
        torch.save(my_model.state_dict(), "../save/day11/my_model_%d.bin" % epoch)



# 预测模型
def evalute_model():
    # 1.准备数据
    test_dataset = load_dataset("csv", data_files="../data/test.csv", split='train')
    test_dataset = test_dataset.filter(lambda x:len(x['text']) > 32)
    test_dataloader = DataLoader(test_dataset, batch_size=8, shuffle=True, drop_last=True, collate_fn=collate_fn)

    #2.准备模型
    my_model = MyModel()
    my_model.load_state_dict(torch.load("../save/fill_mask_1.bin"))

    #3.预测
    my_model.eval()
    accuracy = 0
    total = 0
    for i,(input_ids, attention_mask, input_type_ids, labels) in enumerate(test_dataloader):
        with torch.no_grad():
            output = my_model(input_ids, attention_mask, input_type_ids)

        output = torch.argmax(output, dim=-1)
        accuracy += (output == labels).sum().item()
        total += len(labels)

        # 打印观测值
        if i % 10 == 0:
            print('平均准确率为-->\t',accuracy/total)
    print("模型总准确率为-->", accuracy/total)


if __name__ == '__main__':
    # get_dataloader()
    # test_mymodel()
    # train_model()
    evalute_model()
💻 代码

day13 代码

2 个文件
  • 📄 demo01_specificmodel.py 1.3KB
    from transformers import BertTokenizer, BertForMaskedLM
    import torch
    
    
    # 指定模型方式
    def dm03():
        # 加载模型和分词器
        tokenizer = BertTokenizer.from_pretrained("../models/chinese-bert-wwm")
        model = BertForMaskedLM.from_pretrained("../models/chinese-bert-wwm")
        # 准备数据
        input = tokenizer.encode_plus("我想明天去[MASK]家吃饭", return_tensors='pt')
        print("input-->", input)
        # 把数据送给模型
        output = model(**input)
        # 打印结果
        # print("output-->", output)
        # print("output-->", output.last_hidden_state.shape) # [1, 11, 768]
        # print("output-->", output.pooler_output.shape) # [1, 768]
    
        # [1, 11, 21128]:1句话,11个token,每个token用一个21128的向量表示
        # 含义是:需要基于MASK的位置,从21128个词表中,还原到预测的结果token(词)
        print("output.logits-->", output.logits, output.logits.shape)
        # [11, 21128] -> output.logits[0]
        # [21128] -> output.logits[0][6]
        # print(output.logits[0][6].shape)
        # item:由张量转标量
        index = torch.argmax(output.logits[0][6]).item()
        print("index-->", index)
        # 把下标index从词表中还原成具体的token
        token = tokenizer.convert_ids_to_tokens([index])
        print("token-->", token)
    
    
    if __name__ == '__main__':
        dm03()
    
  • 📄 demo02_text_classification.py 8.1KB
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from tqdm import tqdm
    from datasets import load_dataset
    from transformers import BertTokenizer, BertModel
    from torch.utils.data import DataLoader
    
    # 0.测试load_dataset方法
    def dm01():
        # data = load_dataset('csv', data_files='../data/train.csv')
        # print("data-->", data)
        # data = load_dataset('csv', data_files='../data/train.csv', split='train')
        # print("data-->", data)
        # 注意:split,只能是train
        data = load_dataset('csv', data_files='../data/test.csv', split='train')
        print("data-->", data)
        print(data[0:4])
    
    
    # 加载预训练模型
    tokenizer = BertTokenizer.from_pretrained("../models/bert-base-chinese")
    # 这里导入模型要使用BertModel,而不是BertModelForSequenceClassification
    # 因为,这里使用预训练模型进行特征提取,下游自己指定多分类输出层(线性层)
    bert_model = BertModel.from_pretrained("../models/bert-base-chinese")
    
    
    # 1.数据预处理
    def collate_fn1(batch):
        # 打印数据
        # print("batch-->", batch)
        # 从样本中提取文本和标签
        sentences = [row['text'] for row in batch]
        labels = [row['label'] for row in batch]
        # print("sentences-->", sentences)
        # print("labels-->", labels)
        # 对文本进行数值化+张量化,对标签进行张量化
        data = tokenizer.batch_encode_plus(sentences,
                                            padding='max_length',
                                            max_length=300,
                                            truncation=True,
                                            return_tensors='pt')
        # print("data-->", data)
        # input_ids:数据数值化+张量化后的结果
        # token_type_ids:文本(句子)类型,0代表一个句子
        # attention_mask:注意力掩码,0代表填充的,1代表有值(需要掩码)
        input_ids = data['input_ids']
        token_type_ids = data['token_type_ids']
        attention_mask = data['attention_mask']
    
        # 对label进行张量化
        labels = torch.tensor(labels)
    
        # 返回数据
        return input_ids, token_type_ids, attention_mask, labels
    
    
    def load_data():
        # 使用load_dataset加载数据,得到的就是一个Dataset对象
        mydataset = load_dataset('csv', data_files='../data/train.csv', split='train')
        # 把Dataset转换为DataLoader对象
        # collate_fn:对数据进行二次处理的函数,就是这一批次的数据,需要接受一个函数地址
        mydataloader = DataLoader(dataset=mydataset, batch_size=4, shuffle=True, drop_last=True, collate_fn=collate_fn1)
        # print("mydataloader-->", mydataloader)
        # 遍历
        for input_ids, token_type_ids, attention_mask, labels in mydataloader:
            print("input_ids-->", input_ids)
            print("token_type_ids-->", input_ids)
            print("attention_mask-->", input_ids)
            print("labels-->", input_ids)
            break
    
    # 2.模型构建
    class MyModel(nn.Module):
        def __init__(self):
            super().__init__()
            # 自定义下游输出层(线性层)
            # in_features:输入维度,就是Bert模型的768维度
            # out_features:输出类别,也就是下游的任务类别,这里是2分类
            self.linear = nn.Linear(in_features=768, out_features=2)
    
        def forward(self, input_ids, token_type_ids, attention_mask):
            # 先试用Bert模型进行特征提取(可以冻结,也可以不冻结,如果效果不好,就不冻结)
            with torch.no_grad():
                out = bert_model(input_ids=input_ids,
                                 token_type_ids=token_type_ids,
                                 attention_mask=attention_mask)
            # print("out1-->", out)
            # print("out2-->", out.last_hidden_state.shape) # [4, 300, 768]
            # :代表取4句话
            # 0:300个token中,取第0个,也就是CLS
            # out.last_hidden_state[:, 0]
            # out.last_hidden_state[:, 0, :]
            # print("out3-->", out.pooler_output.shape)     # [4, 768]
            # 然后把特征送给下游的自定义输出线性层,实现二分类任务
            output = self.linear(out.pooler_output)
            # print("output-->", output, output.shape) # [4, 2]
            return output
    
    
    def test_model():
        # 数据
        mydataset = load_dataset('csv', data_files='../data/train.csv', split='train')
        mydataloader = DataLoader(dataset=mydataset, batch_size=4, shuffle=True, drop_last=True, collate_fn=collate_fn1)
        # 模型
        model = MyModel()
        # 遍历
        for input_ids, token_type_ids, attention_mask, labels in mydataloader:
            model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
            break
    
    
    # 3.模型训练
    def train_model():
        # 3+2+5
        # 3:模型,损失函数、优化器
        # 2:内外两层循环
        # 5:内层循环中,5个步骤:前向传播、损失计算、梯度清零、反向传播、梯度更新
        # 模型
        model = MyModel()
        # 损失函数
        criterion = nn.CrossEntropyLoss()
        # 优化器
        optimizer = optim.AdamW(params=model.parameters(), lr=5e-4)
        # 轮次
        epochs = 1
        # 冻结预训练模型的参数(Bert模型)
        # 双重保险
        for param in bert_model.parameters():
            param.requires_grad_(False)
        # 设置模型为训练模型
        model.train()
        # 加载数据
        mydataset = load_dataset('csv', data_files='../data/train.csv', split='train')
        # 把数据转换为DataLoader
        mydataloader = DataLoader(dataset=mydataset,
                                  shuffle=True,
                                  drop_last=True,
                                  batch_size=4,
                                  collate_fn=collate_fn1)
        # 外层变量轮次
        for epoch in range(epochs):
        # 内层变量批次
        # tqdm:进度条,pip install tqdm
            for i, (input_ids, token_type_ids, attention_mask, labels) in enumerate(tqdm(mydataloader), start=1):
                # 前向传播
                output = model(input_ids=input_ids,
                               token_type_ids=token_type_ids,
                               attention_mask=attention_mask)
                # output.shape->[4,2]
                # 损失计算
                loss = criterion(output, labels)
                # print("loss-->", loss)
                # 梯度清零
                optimizer.zero_grad()
                # 反向传播
                loss.backward()
                # 梯度更新
                optimizer.step()
                # 打印日志
                if i % 5 == 0:
                    pred = torch.argmax(output, dim=-1)
                    accuracy = (pred == labels).sum().item() / len(labels)
                    print("epoch:", epoch + 1, "loss:", loss.item(), "accuracy:", accuracy)
            # 模型保存
            torch.save(model.state_dict(), '../save_model/model_%d.pth' % (epoch + 1))
    
    
    # 4.模型评估
    def model_eval():
        # 加载数据
        mydataset = load_dataset("csv", data_files='../data/test.csv', split='train')
        # 转换为DataLoader
        mydataloader = DataLoader(dataset=mydataset,
                                  shuffle=False,
                                  drop_last=True,
                                  batch_size=4,
                                  collate_fn=collate_fn1)
        # 准备模型
        model = MyModel()
        # 加载刚刚预训练好的模型参数
        model.load_state_dict(torch.load('../save_model/model_1.pth'))
        # 准备参数:total,accuracy
        num = 0
        total = 0
        # 设置模型为评估模式
        model.eval()
        # 遍历
        for i, (input_ids, token_type_ids, attention_mask, labels) in enumerate(tqdm(mydataloader), start=1):
            # 把数据送给模型,得到预测值
            output = model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
            # 基于预测结果,得到预测值
            y_pred = torch.argmax(output, dim=-1)
            num += (y_pred == labels).sum().item()
            total += len(labels)
        # 计算准确率
        print("accuracy-->", num / total)
    
    
    if __name__ == '__main__':
        # dm01()
        # load_data()
        # test_model()
        # train_model()
        model_eval()
        pass
    

第10节 · 第10章 微调实战:推理部署(day14)

🎯 完成模型推理与部署收尾。
📖 笔记

NLP 基础(day14)

NLP基础

【了解】今日内容介绍

  • 微调任务
  • 完形填空任务
  • NSP任务
  • NLP数据集
  • BERT模型介绍

【实现】微调任务

完形填空任务

Bert模型内置2个任务:

#1.MLM:MASKed Language Model
遮蔽型语言建模任务(掩码语言模型),就是我们刚刚讲的完形填空。

#2.NSP:Next Sentence Prediction
下一句话预测任务。

需求说明

也称之为MLM任务,输入一句话,MASK一个字,训练模型,进行填空。

获取数据

和之前的一样,数据文件有三个train.csv,test.csv,validation.csv,数据样式都是一样的。

对文件中数据进行处理,以满足模型训练要求。

import torch
from torch.utils.data import DataLoader
from datasets import load_dataset
from transformers import BertTokenizer, BertModel
from transformers import AdamW
import time

# 加载字典和分词工具
my_tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')

# 加载预训练模型
my_model_pretrained = BertModel.from_pretrained('bert-base-chinese')


# 数据集处理自定义函数
def collate_fn2(data):
    sents = [i['text'] for i in data]

    # 文本数值化
    data = my_tokenizer.batch_encode_plus(batch_text_or_text_pairs=sents,
                                   truncation=True,
                                   padding='max_length',
                                   max_length=32,
                                   return_tensors='pt',
                                   return_length=True)

    # input_ids 编码之后的数字
    # attention_mask 是补零的位置是0,其他位置是1
    input_ids = data['input_ids']
    attention_mask = data['attention_mask']
    token_type_ids = data['token_type_ids']

    # 把第16个词固定替换为mask
    labels = input_ids[:, 16].reshape(-1).clone()  # 取出数据8句话 在第16个位置clone出来 做标签
    input_ids[:, 16] = my_tokenizer.get_vocab()[my_tokenizer.mask_token]
    labels = torch.LongTensor(labels)

    # tmpa = input_ids[:, 16]
    # print('tmpa--->', tmpa, tmpa.shape)       # torch.Size([8]
    # print('labels-->', labels.shape, labels)  # torch.Size([8]

    return input_ids, attention_mask, token_type_ids, labels


# 数据源 数据迭代器 测试
def dm01_test_dataset():

    # 生成数据源dataset对象
    dataset_train_tmp = load_dataset('csv', data_files='./data/train.csv', split="train")
    # print('dataset_train_tmp--->', dataset_train_tmp)

    # 按照条件过滤数据源对象
    my_dataset_train = dataset_train_tmp.filter(lambda x: len(x['text']) > 32)
    # print('my_dataset_train--->', my_dataset_train)
    # print('my_dataset_train[0:3]-->', my_dataset_train[0:3])

    # 通过dataloader进行迭代
    mydataloader = DataLoader(my_dataset_train, batch_size=8, collate_fn=collate_fn2, shuffle=True, drop_last=True)
    print('mydataloader--->', mydataloader)

    # 不训练,不需要计算梯度
    for param in my_model_pretrained.parameters():
        param.requires_grad_(False)

    # 调整数据迭代器对象数据返回格式
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(mydataloader):

        print(input_ids.shape, attention_mask.shape, token_type_ids.shape, labels)

        print('\n第1句mask的信息')
        print(my_tokenizer.decode(input_ids[0]))
        print(my_tokenizer.decode(labels[0]))

        print('\n第2句mask的信息')
        print(my_tokenizer.decode(input_ids[1]))
        print(my_tokenizer.decode(labels[1]))
        break

定义模型

# 定义下游任务模型
class MyModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # 定义全连接层
        self.linear = torch.nn.Linear(768, my_tokenizer.vocab_size)

    def forward(self, input_ids, attention_mask, token_type_ids):
        # 预训练模型不进行训练
        with torch.no_grad():
            out = my_model_pretrained(input_ids=input_ids,
                             attention_mask=attention_mask,
                             token_type_ids=token_type_ids)

        # 下游任务进行训练 形状[8,768] ---> [8, 21128]
        out = self.linear(out.last_hidden_state[:, 16])

        # 返回
        return out

模型训练

# 模型训练 - 填空
def dm03_train_model():

    # 实例化数据源对象my_dataset_train
    dataset_train_tmp = load_dataset('csv', data_files='./data/train.csv', split="train")
    my_dataset_train = dataset_train_tmp.filter(lambda x: len(x['text']) > 32)
    print('my_dataset_train--->', my_dataset_train)

    # 实例化下游任务模型my_model
    my_model = MyModel()

    # 实例化优化器my_optimizer
    my_optimizer = AdamW(my_model.parameters(), lr=5e-4)

    # 实例化损失函数my_criterion
    my_criterion = torch.nn.CrossEntropyLoss()

    # 不训练预训练模型 只让预训练模型计算数据特征 不需要计算梯度
    for param in my_model_pretrained.parameters():
        param.requires_grad_(False)

    # 设置训练参数
    epochs = 1

    # 设置模型为训练模型
    my_model.train()

    # 外层for循环 控制轮数
    for eporch_idx in range(epochs):

        # 实例化数据迭代器对象my_dataloader
        my_dataloader = torch.utils.data.DataLoader(my_dataset_train,
                                                    batch_size=8,
                                                    collate_fn=collate_fn2,
                                                    shuffle=True,
                                                    drop_last=True)
        starttime = (int)(time.time())
        # 内层for循环 控制迭代次数
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader, start=1):
            # 给模型喂数据 [8,32] --> [8,21128]
            my_out = my_model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        token_type_ids=token_type_ids)

            # 计算损失
            my_loss = my_criterion(my_out, labels)

            # 梯度清零
            my_optimizer.zero_grad()

            # 反向传播
            my_loss.backward()

            # 梯度更新
            my_optimizer.step()

            # 每5次迭代 算一下准确率
            if i % 20 == 0:
                out = my_out.argmax(dim=1) # [8,21128] --> (8,)
                accuracy = (out == labels).sum().item() / len(labels)
                print('轮次:%d 迭代数:%d 损失:%.6f 准确率%.3f 时间%d' \
                      %(eporch_idx, i, my_loss.item(), accuracy, (int)(time.time())-starttime))

        # 每个轮次保存模型
        torch.save(my_model.state_dict(), './my_model_mask_%d.bin' % (eporch_idx + 1))

模型评估

# 模型测试:填空
def dm04_evaluate_model():

    # 实例化数据源对象my_dataset_test
    print('\n加载测试集')
    my_dataset_tmp = load_dataset('csv', data_files='./data/test.csv', split='train')
    my_dataset_test = my_dataset_tmp.filter(lambda x: len(x['text']) > 32)
    print('my_dataset_test--->', my_dataset_test)
    # print(my_dataset_test[0:3])

    # 实例化下游任务模型my_model
    path = './my_model_mask_3.bin'
    my_model = MyModel()
    my_model.load_state_dict(torch.load(path))
    print('my_model-->', my_model)

    # 设置下游任务模型为评估模式
    my_model.eval()

    # 设置评估参数
    correct = 0
    total = 0

    # 实例化化dataloader
    my_loader_test = torch.utils.data.DataLoader(my_dataset_test,
                                              batch_size=8,
                                              collate_fn=collate_fn2,
                                              shuffle=True,
                                              drop_last=True)

    # 给模型送数据 测试预测结果
    for i, (input_ids, attention_mask, token_type_ids,
            labels) in enumerate(my_loader_test):

        with torch.no_grad():
            my_out = my_model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        token_type_ids=token_type_ids)

        out = my_out.argmax(dim=1)
        correct += (out == labels).sum().item()
        total += len(labels)

        if i % 25 == 0:
            print(correct / total)

所有代码

import torch
import torch.nn as nn
from transformers import BertTokenizer, BertModel, AdamW
import time
from datasets import load_dataset
from torch.utils.data import DataLoader
from tqdm import tqdm

bert_tokenizer = BertTokenizer.from_pretrained("./model/bert-base-chinese")
bert_model = BertModel.from_pretrained("./model/bert-base-chinese")


def collate_fn1(data):
    # print("data-->", data)
    sents = [x['text'] for x in data]

    data = bert_tokenizer.batch_encode_plus(sents,
                                            padding='max_length',
                                            max_length=50,
                                            truncation=True,
                                            return_tensors='pt')

    # print("data-->", data)

    input_ids = data['input_ids']
    token_type_ids = data['token_type_ids']
    attention_mask = data['attention_mask']

    labels = input_ids[:, 16].clone() # [4]
    input_ids[:, 16] = bert_tokenizer.mask_token_id # [4, 32]
    # print("input_ids-->", input_ids, input_ids.shape)
    # print("labels-->", labels, labels.shape)
    return input_ids, token_type_ids, attention_mask, labels


def load_data():
    my_datasets = load_dataset('csv', data_files='./data/train.csv', split='train')
    my_dataloader = DataLoader(dataset=my_datasets, batch_size=4, drop_last=True, shuffle=True, collate_fn=collate_fn1)
    for input_ids, token_type_ids, attention_mask, labels in my_dataloader:
        print("input_ids-->", input_ids)
        print("token_type_ids-->", token_type_ids)
        print("attention_mask-->", attention_mask)
        print("labels-->", labels)
        break

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(768, bert_tokenizer.vocab_size)

    def forward(self, input_ids, token_type_ids, attention_mask):
        with torch.no_grad():
            out = bert_model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
        # print("out-->", out)
        # print("out.last_hidden_state.shape-->", out.last_hidden_state.shape) # [4, 32, 768]
        # print("out.pooler_output.shape-->", out.pooler_output.shape) # [4, 768]
        # print('out.last_hidden_state[:, 16].shape-->', out.last_hidden_state[:, 16].shape) # [4, 768]
        output = self.linear(out.last_hidden_state[:, 16])
        # print("output-->", output.shape) # [4, 21128]
        return output


def test_mymodel():
    my_datasets = load_dataset('csv', data_files='./data/train.csv', split='train')
    my_dataloader = DataLoader(dataset=my_datasets, batch_size=4, drop_last=True, shuffle=True, collate_fn=collate_fn1)
    model = MyModel()
    for input_ids, token_type_ids, attention_mask, labels in my_dataloader:
        output = model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
        print("output-->", output, output.shape)
        break


def train2model():
    my_datasets = load_dataset('csv', data_files='../day11/data/train.csv', split='train')
    my_datasets = my_datasets.filter(lambda x:len(x['text']) > 50)
    my_dataloader = DataLoader(dataset=my_datasets, batch_size=4, drop_last=True, shuffle=True, collate_fn=collate_fn1)
    model = MyModel()
    optimizer = AdamW(params=model.parameters(), lr=5e-4, betas=[0.9, 0.99])
    criterion = nn.CrossEntropyLoss()
    for p in bert_model.parameters():
        p.requires_grad_(False)
    epochs = 3
    total_num = 0
    total_loss = 0
    accuracy = 0
    start_time = time.time()
    model.train()

    for epoch in range(epochs):
        for input_ids, token_type_ids, attention_mask, labels in tqdm(my_dataloader):
            out = model(input_ids=input_ids,
                        token_type_ids=token_type_ids,
                        attention_mask=attention_mask)
            loss = criterion(out, labels)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
            total_num += len(labels)
            idx = torch.argmax(out, dim=-1)
            accuracy += (idx == labels).sum().item()

            if total_num % 20 == 0:
                end_time = time.time()
                avg_loss = total_loss / total_num
                avg_acc = accuracy / total_num
                print("轮次:", epoch + 1, "损失:", avg_loss, "准确率:", avg_acc, "耗时:", end_time - start_time)
        # 保存模型
        torch.save(model.state_dict(), './save_model/model_2.pth')


def predict2model():
    my_datasets = load_dataset('csv', data_files='./data/test.csv', split='train')
    my_datasets = my_datasets.filter(lambda x: len(x['text']) > 32)
    my_dataloader = DataLoader(dataset=my_datasets, batch_size=4, drop_last=True, shuffle=True, collate_fn=collate_fn1)
    model = MyModel()
    model.load_state_dict(torch.load('./save_model/model_2.pth'))
    model.eval()
    total_num = 0
    accuracy = 0
    start_time = time.time()
    for input_ids, token_type_ids, attention_mask, labels in tqdm(my_dataloader):
        out =  model(input_ids=input_ids,
                     token_type_ids=token_type_ids,
                     attention_mask=attention_mask)
        idx =torch.argmax(out, dim=-1)
        accuracy += (idx == labels).sum().item()
        total_num += len(labels)

        if total_num % 20 == 0:
            end_time = time.time()
            avg_acc = accuracy / total_num
            print("准确率:", avg_acc, "耗时:", end_time - start_time)



if __name__ == '__main__':
    # load_data()
    # test_mymodel()
    # train2model()
    predict2model()

NSP任务

需求说明

下一句话任务NSP(Next Sentence Prediction) 中文句子关系任务。输入2句话,判断第二句是否为第一句的下半句。

获取数据

和之前的一样,数据文件有三个train.csv,test.csv,validation.csv,数据样式都是一样的。

对文件中数据进行处理,以满足模型训练要求。

import random

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from transformers import BertTokenizer, BertModel, AdamW
from datasets import load_dataset

bert_tokenizer = BertTokenizer.from_pretrained("../model/bert-base-chinese")
bert_model = BertModel.from_pretrained("../model/bert-base-chinese")

#1.定义数据处理函数
def collate_fn(data):
    # print("data-->", data)
    sents = [tuple1[0:2] for tuple1 in data]
    labels = [tuple1[2] for tuple1 in data]
    # print("sents-->", sents)
    # print("labels-->", labels)

    data = bert_tokenizer.batch_encode_plus(sents, truncation=True, padding=True, return_tensors='pt', max_length=50)

    input_ids = data["input_ids"]
    token_type_ids = data["token_type_ids"]
    attention_mask = data["attention_mask"]

    labels = torch.tensor(labels, dtype=torch.long)

    return input_ids, attention_mask, token_type_ids, labels


#2.定义数据集类
class MyDataset(Dataset):
    def __init__(self, path):
        tmp_dataset = load_dataset("csv", data_files=path, split="train")
        self.dataset = tmp_dataset.filter(lambda x: len(x['text']) > 44)

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, index):
        # print("index-->", index)
        data = self.dataset[index]["text"]
        # print("data-->", data)

        label = 1
        sent1 = data[:22]
        # print("sent1-->", sent1)
        sent2 = data[22:44]
        # print("sent2-->", sent2)
        flag = random.randint(0, 1)
        # print("flag-->", flag)
        while flag == 0:
            j = random.randint(0, len(data) - 1)
            if j == index:
                continue
            sent2 = self.dataset[j]['text'][22:44]
            label = 0
            break
        return sent1, sent2, label


#3.获取数据集
def get_dataloader():
    #1.获取dataset
    my_dataset = MyDataset("../data/train.csv")
    # sent1, sent2, label = my_dataset[0]
    # print('sent1-->', sent1)
    # print('sent2-->', sent2)
    # print('label-->', label)
    #2.构建dataloader
    my_dataloader = DataLoader(dataset=my_dataset, shuffle=True, batch_size=8, collate_fn=collate_fn, drop_last=True)

    # for input_ids, attention_mask, token_type_ids, labels in my_dataloader:
    #     print("input_ids-->", input_ids, input_ids.shape)
    #     print("labels-->", labels, labels.shape)
    #     break

    return my_dataloader



if __name__ == '__main__':
    get_dataloader()

定义模型

#4.定义模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(768, 2)

    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():
            bert_out = bert_model(input_ids, attention_mask, token_type_ids=token_type_ids)
        #last_hidden_state:词向量, pooler_output:句向量
        # print("bert_out-->", bert_out)
        out = self.linear(bert_out['pooler_output'])
        return out

模型训练

#5.模型训练
def train_model():
    #1.准备物料
    my_dataloader = get_dataloader()
    my_model = MyModel()
    my_optimizer = AdamW(params=my_model.parameters(), lr=5e-4)
    my_loss_fn = nn.CrossEntropyLoss()
    epochs = 3

    for param in bert_model.parameters():
        param.requires_grad_(False)

    my_model.train()

    for epoch in range(epochs):
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
            my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
            loss = my_loss_fn(my_out, labels)
            my_optimizer.zero_grad()
            loss.backward()
            my_optimizer.step()

            # 打印日志
            if i % 10 == 0:
                out = torch.argmax(my_out, -1)
                acc = (out == labels).sum().item() / len(labels)
                print('轮次:%d 迭代数:%d 损失:%.6f 准确率%.3f' % (epoch, i, loss.item(), acc))
        #保存模型
        torch.save(my_model.state_dict(), '../save/nsp_model_%d.bin' % (epoch + 1))

模型评估

#6.模型评估
def evluate_model():
    my_dataset = MyDataset("../data/test.csv")
    my_dataloader = DataLoader(my_dataset, batch_size=8, shuffle=True, collate_fn=collate_fn, drop_last=True)
    my_model = MyModel()
    my_model.load_state_dict(torch.load("../save/nsp_model_3.bin"))

    correct = 0
    total = 0
    my_model.eval()

    for i,(input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
        with torch.no_grad():
            my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        out = torch.argmax(my_out, dim=-1)
        correct += (out == labels).sum().item()
        total += len(labels)
        # 打印日志
        if i % 10 == 0:
            print("i-->", i, "准确率-->", correct/total)

所有代码

import random

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from transformers import BertTokenizer, BertModel, AdamW
from datasets import load_dataset

bert_tokenizer = BertTokenizer.from_pretrained("../model/bert-base-chinese")
bert_model = BertModel.from_pretrained("../model/bert-base-chinese")

#1.定义数据处理函数
def collate_fn(data):
    # print("data-->", data)
    sents = [tuple1[0:2] for tuple1 in data]
    labels = [tuple1[2] for tuple1 in data]
    # print("sents-->", sents)
    # print("labels-->", labels)

    data = bert_tokenizer.batch_encode_plus(sents, truncation=True, padding=True, return_tensors='pt', max_length=50)

    input_ids = data["input_ids"]
    token_type_ids = data["token_type_ids"]
    attention_mask = data["attention_mask"]

    labels = torch.tensor(labels, dtype=torch.long)

    return input_ids, attention_mask, token_type_ids, labels


#2.定义数据集类
class MyDataset(Dataset):
    def __init__(self, path):
        tmp_dataset = load_dataset("csv", data_files=path, split="train")
        self.dataset = tmp_dataset.filter(lambda x: len(x['text']) > 44)

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, index):
        # print("index-->", index)
        data = self.dataset[index]["text"]
        # print("data-->", data)

        label = 1
        sent1 = data[:22]
        # print("sent1-->", sent1)
        sent2 = data[22:44]
        # print("sent2-->", sent2)
        flag = random.randint(0, 1)
        # print("flag-->", flag)
        while flag == 0:
            j = random.randint(0, len(data) - 1)
            if j == index:
                continue
            sent2 = self.dataset[j]['text'][22:44]
            label = 0
            break
        return sent1, sent2, label


#3.获取数据集
def get_dataloader():
    #1.获取dataset
    my_dataset = MyDataset("../data/train.csv")
    # sent1, sent2, label = my_dataset[0]
    # print('sent1-->', sent1)
    # print('sent2-->', sent2)
    # print('label-->', label)
    #2.构建dataloader
    my_dataloader = DataLoader(dataset=my_dataset, shuffle=True, batch_size=8, collate_fn=collate_fn, drop_last=True)

    # for input_ids, attention_mask, token_type_ids, labels in my_dataloader:
    #     print("input_ids-->", input_ids, input_ids.shape)
    #     print("labels-->", labels, labels.shape)
    #     break

    return my_dataloader


#4.定义模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(768, 2)

    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():
            bert_out = bert_model(input_ids, attention_mask, token_type_ids=token_type_ids)
        #last_hidden_state:词向量, pooler_output:句向量
        # print("bert_out-->", bert_out)
        out = self.linear(bert_out['pooler_output'])
        return out



#测试模式类
def test_my_model():
    my_dataloader = get_dataloader()
    my_model = MyModel()
    print("my_model-->", my_model)
    for input_ids, attention_mask, token_type_ids, labels in my_dataloader:
        my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        print("my_out-->", my_out)
        break

#5.模型训练
def train_model():
    #1.准备物料
    my_dataloader = get_dataloader()
    my_model = MyModel()
    my_optimizer = AdamW(params=my_model.parameters(), lr=5e-4)
    my_loss_fn = nn.CrossEntropyLoss()
    epochs = 3

    for param in bert_model.parameters():
        param.requires_grad_(False)

    my_model.train()

    for epoch in range(epochs):
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
            my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
            loss = my_loss_fn(my_out, labels)
            my_optimizer.zero_grad()
            loss.backward()
            my_optimizer.step()

            # 打印日志
            if i % 10 == 0:
                out = torch.argmax(my_out, -1)
                acc = (out == labels).sum().item() / len(labels)
                print('轮次:%d 迭代数:%d 损失:%.6f 准确率%.3f' % (epoch, i, loss.item(), acc))
        #保存模型
        torch.save(my_model.state_dict(), '../save/nsp_model_%d.bin' % (epoch + 1))

#6.模型评估
def evluate_model():
    my_dataset = MyDataset("../data/test.csv")
    my_dataloader = DataLoader(my_dataset, batch_size=8, shuffle=True, collate_fn=collate_fn, drop_last=True)
    my_model = MyModel()
    my_model.load_state_dict(torch.load("../save/nsp_model_3.bin"))

    correct = 0
    total = 0
    my_model.eval()

    for i,(input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
        with torch.no_grad():
            my_out = my_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        out = torch.argmax(my_out, dim=-1)
        correct += (out == labels).sum().item()
        total += len(labels)
        # 打印日志
        if i % 10 == 0:
            print("i-->", i, "准确率-->", correct/total)


if __name__ == '__main__':
    # get_dataloader()
    # test_my_model()
    # train_model()
    evluate_model()

【了解】NLP数据集

  • CoLA 数据集
  • SST-2 数据集
  • MRPC 数据集
  • STS-B 数据集
  • QQP 数据集
  • MNLI 数据集
  • SNLI 数据集
  • QNLI 数据集
  • RTE 数据集
  • WNLI 数据集
  • diagnostics数据集

上述标准数据集,是用来衡量模型表现好坏的评估数据集。

也就是说,如果一个模型,在上述数据集上表现的都很好,那么这个模型就很Nice。也能够让业界接受。

【理解】BERT系列介绍

BERT概述

BERT是2018年10月由Google AI研究院提出的一种预训练模型。

全称:Bidirectional Encoder Representation from Transformers。

BERT模型架构

BERT中只使用了经典Transformer架构中的Encoder部分,完全舍弃了Decoder部分。

BERT模型由三部分组成:

  • 最底层的Embedding模块
  • token Embedding:第一个单词是CLS,可以用于之后的分类任务
  • segment Embedding:段信息,区别两个句子
  • position Embedding:位置信息
  • 中间层的Transformer模块
  • 双向Transformer,前后内容可以互相看得见
  • 最顶层的预微调模块
  • 最后一层任务可以根据需求做不同的调整
  • 对于不同的任务,微调都集中在预微调模块

1757591401124

BERT预训练任务

BERT模型包含2个预训练任务

  • 任务一: Masked LM (带mask的语言模型训练)
  • 在原始训练文本中, 随机的抽取15%的token作为参与MASK任务的对象
  • 在这些被选中的token中, 数据生成器并不是把它们全部变成[MASK], 而是有下列3种情况
    • 在80%的概率下, 用[MASK]标记替换该token, 比如my dog is hairy -> my dog is [MASK]
    • 在10%的概率下, 用一个随机的单词替换token, 比如my dog is hairy -> my dog is apple
    • 在10%的概率下, 保持该token不变, 比如my dog is hairy -> my dog is hairy
  • 任务二: Next Sentence Prediction (下一句话预测任务)
  • 所有参与任务训练的语句都被选中作为句子A
  • 其中50%的B是原始文本中真实跟随A的下一句话,标记为IsNext, 代表正样本
  • 其中50%的B是原始文本中随机抽取的一句话,标记为NotNext, 代表负样本

BERT模型特点

优点

  • 通过预训练, 加上Fine-tunning,在11项NLP任务上取得最优结果
  • BERT的根基源于Transformer,相比传统RNN更加高效,可以并行化处理同时能捕捉长距离的语义和结构依赖
  • BERT采用了Transformer架构中的Encoder模块,不仅仅获得了真正意义上的bidirectional context,而且为后续微调任务留出了足够的调整空间

缺点

  • BERT模型过于庞大,参数太多,不利于资源紧张的应用场景,也不利于上线的实时处理

  • BERT目前给出的中文模型中,是以字为基本token单位的,很多需要词向量的应用无法直接使用,同时该模型无法识别很多生僻词,只能以UNK代替

  • BERT中第一个预训练任务MLM中,[MASK]标记只在训练阶段出现,而在预测阶段不会出现,这就造成了一定的信息偏差,因此训练时不能过多的使用[MASK],否则会影响模型的表现
  • 按照BERT的MLM任务中的约定,每个batch数据中只有15%的token参与了训练,被模型学习和预测,所以BERT收敛的速度比left-to-right模型要慢很多(left-to-right模型中每一个token都会参与训练)

GPT模型介绍

GPT是OpenAI公司提出的一种语言预训练模型。

OpenAI GPT模型是在Google BERT模型之前提出的,与BERT最大的区别在于GPT采用了传统的语言模型方法进行预训练。即使用单词的上文来预测单词,而BERT是采用了双向上下文的信息共同来预测单词。

正是因为训练方法上的区别,使得GPT更擅长处理自然语言生成任务(NLG),而BERT更擅长处理自然语言理解任务(NLU)。

GPT模型架构

它是单向的Transformer模型。

1757591417470

  • 具体的遮掩操作是在slef-attention进行softmax之前进行的,一般的实现是将MASK的位置用一个无穷小的数值-inf来替换,替换后执行softmax计算得到新的结果矩阵。这样-inf的位置就变成了0。如上图所示,最后的矩阵可以很方便的做到当利用A预测B的时候,只能看到A的信息;当利用[A, B]预测C的时候,只能看到A,B的信息。
  • 注意:对比于经典的Transformer架构,解码器模块采用了6个Decoder,GPT的架构中采用了12个Decoder。
💻 代码

day14 代码

1 个文件
  • 📄 demo01_masked_language_model.py 8.0KB
    from datasets import load_dataset
    from torch.utils.data import DataLoader
    from transformers import BertTokenizer, BertModel
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from tqdm import tqdm
    
    
    # 加载预训练模型的分词器
    tokenizer = BertTokenizer.from_pretrained("../models/bert-base-chinese")
    # 加载预训练模型
    bert_model = BertModel.from_pretrained("../models/bert-base-chinese")
    
    # 1.数据预处理
    def collate_fn(batch):
        # print("batch-->", batch)
        # 从这个batch中,把文本和标签(不用)拿出来
        sentences = [row['text'] for row in batch]
        # print("sentences-->", sentences)
        # 对文本进行数值化+张量化
        data = tokenizer.batch_encode_plus(sentences,
                                           padding='max_length',
                                           max_length=32,
                                           truncation=True,
                                           return_tensors='pt')
        # print("data-->", data)
        # 取出input_ids、token_type_ids、attention_mask
        input_ids = data['input_ids']
        token_type_ids = data['token_type_ids']
        attention_mask = data['attention_mask']
        # print("input_ids-->", input_ids, input_ids.shape) # [4, 32]
        # print("token_type_ids-->", token_type_ids)
        # print("attention_mask-->", attention_mask)
        # 手动构建y
        # 从数据中挖下标=16的token,当做label标签(y值)
        label = input_ids[:, 16].clone()
        # print("label-->", label)
        # 手动构建x
        # print("token1-->", tokenizer.mask_token)      # MASK
        # print("token2-->", tokenizer.mask_token_id)   # 103
        # 对原始数据进行MASK,构建x
        input_ids[:, 16] = tokenizer.mask_token_id
        # print("input_ids-->", input_ids)
        # 返回x和y
        return input_ids, token_type_ids, attention_mask, label
    
    
    def load_data():
        # 加载数据
        dataset_tmp = load_dataset('csv', data_files='../data/train.csv', split='train')
        # 对数据进行过滤,过滤长度<=32的样本
        # print("len(dataset)--", len(dataset_tmp)) # 9600
        # print("(dataset[:3])--", dataset_tmp[:3])
        mydataset = dataset_tmp.filter(lambda x: len(x['text']) > 32)
        # print("len(dataset)--", len(mydataset)) # 9035
    
        # 把数据转换为DataLoader
        mydataloader = DataLoader(dataset=mydataset,
                                  batch_size=4,
                                  shuffle=True,
                                  drop_last=True,
                                  collate_fn=collate_fn)
        # 遍历 mydataloader
        for input_ids, token_type_ids, attention_mask, label in mydataloader:
            print("input_ids-->", input_ids)
            print("token_type_ids-->", token_type_ids)
            print("attention_mask-->", attention_mask)
            print("label-->", label)
            break
    
    
    # 2.模型构建
    class MyModel(nn.Module):
        def __init__(self):
            super(MyModel, self).__init__()
            self.linear = nn.Linear(in_features=768, out_features=tokenizer.vocab_size)
    
        def forward(self, input_ids, token_type_ids, attention_mask):
            # 让数据先经过预训练模型,然后经过下游线性层
            with torch.no_grad():
                out = bert_model(input_ids=input_ids,
                                 token_type_ids=token_type_ids,
                                 attention_mask=attention_mask)
            # [4, 32, 768]:4句话,每句话32个token,每个token使用768维度的向量表示
            # print("out.last_hidden_state-->", out.last_hidden_state.shape)
            # out.last_hidden_state[:, 16]: 4句话,16表示每句话中下标为16的token,这就是之前MASK的位置,预测结果
            # return self.linear(out.last_hidden_state[:, 16])
            return self.linear(out.last_hidden_state[:, 16, :])
    
    
    def test_mymodel():
        dataset_tmp = load_dataset('csv', data_files='../data/train.csv', split='train')
        mydataset = dataset_tmp.filter(lambda x: len(x['text']) > 32)
        # 把数据转换为DataLoader
        mydataloader = DataLoader(dataset=mydataset,
                                  batch_size=4,
                                  shuffle=True,
                                  drop_last=True,
                                  collate_fn=collate_fn)
        # 遍历 mydataloader
        model = MyModel()
        for input_ids, token_type_ids, attention_mask, label in mydataloader:
            # 把数据送给模型
            result = model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
            # [4, 21128]:4句话,每句话都是21128个类别
            print("result-->", result.shape) # [4, 21128]
            break
    
    
    # 3.模型训练
    def train_model():
        # 准备数据
        dataset_tmp = load_dataset('csv', data_files='../data/train.csv', split='train')
        mydataset = dataset_tmp.filter(lambda x: len(x['text']) > 32)
        # 把数据转换为DataLoader
        mydataloader = DataLoader(dataset=mydataset,
                                  batch_size=4,
                                  drop_last=True,
                                  shuffle=True,
                                  collate_fn=collate_fn)
        # 准备模型
        model = MyModel()
        # 损失函数
        criterion = nn.CrossEntropyLoss()
        # 优化器
        optimizer = optim.AdamW(params=model.parameters(), lr=5e-4)
        # 冻结预训练模型参数
        for param in bert_model.parameters():
            param.requires_grad_(False)
        # 设置轮次epochs
        epochs = 1
        # 调整模型为训练模式
        model.train()
        # 外层遍历
        for epoch in range(epochs):
            # 内层遍历
            for i, (input_ids, token_type_ids, attention_mask, label) in enumerate(tqdm(mydataloader), start=1):
                # 前向传播
                out = model(input_ids=input_ids,
                            token_type_ids=token_type_ids,
                            attention_mask=attention_mask)
                # 损失计算
                loss = criterion(out, label)
                # print("loss-->", loss)
                # 梯度清零
                optimizer.zero_grad()
                # 反向传播
                loss.backward()
                # 梯度更新
                optimizer.step()
                # 打印日志
                if i % 10 == 0:
                    # 得到预测结果
                    y_pred = torch.argmax(out, dim=-1)
                    # 累计预测对的
                    accuracy = (y_pred == label).sum().item() / len(label)
                    # 打印准确率
                    print("epoch:", epoch + 1, "loss:", loss.item(), "accuracy:", accuracy)
            # 模型保存
            torch.save(model.state_dict(), '../save_model/model2_%d.bin' % (epoch + 1))
    
    
    # 4.模型预测
    def eval_model():
        # 加载数据
        dataset_tmp = load_dataset('csv', data_files='../data/test.csv', split='train')
        # 过滤数据
        mydataset = dataset_tmp.filter(lambda x: len(x['text']) > 32)
        # 转换为DataLoader
        mydataloader = DataLoader(dataset=mydataset,
                                  shuffle=False,
                                  drop_last=True,
                                  batch_size=4,
                                  collate_fn=collate_fn)
        # 准备模型
        model = MyModel()
        # 加载模型参数
        model.load_state_dict(torch.load('../save_model/model_2.pth'))
        # 设置模型为评估模式
        model.eval()
        # 定义2个变量
        correct = 0
        total = 0
        # 遍历,把数据送给模型,得到预测结果
        for i, (input_ids, token_type_ids, attention_mask, label) in enumerate(tqdm(mydataloader), start=1):
            # 把数据送给模型,得到预测结果
            out = model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
            # 累计预测正确的结果和总样本
            y_pred = torch.argmax(out, dim=-1)
            correct += (y_pred == label).sum().item()
            total += len(label)
        # 计算准确率
        print("accuracy-->", correct / total)
    
    
    if __name__ == '__main__':
        # load_data()
        # test_mymodel()
        # train_model()
        eval_model()