人类发明后悔,来证明拥有的珍贵
—— 25.1.15
Bert的优势:① 预训练思想 ② Transformer模型结构
一、传统方法 VS 预训练方式
Pre-train:
① 收集海量无标注文本数据
② 进行模型预训练,并在任务模型中使用
Fine-tune:
③ 设计模型结构
④ 收集/标注训练数据
⑤ 使用标注数据进行模型训练
⑥ 真实场景模型预测
预训练 —— Pre-train:①、②,本质上是在做语言模型的预训练,学习到语义知识
传统方法 —— Fine-tune(微调):③、④、⑤、⑥
预训练方法 —— Pre-train + Fine-tune:不需要从0开始,从一些数据中进行预训练,再从下游的具体任务进行微调、学习、优化
二、Bert模型中的预训练方式
1.掩码语言模型 —— 完形填空
MLM:Mask Language Model
Bidirectional Language Model
依照一定概率,随机地用[mask]掩盖文本中的某个字或词,再经过训练预测这些被掩盖的字或词
类似于训练词向量基于窗口中的由两边词预测中间词的方式,称为自编码的语言模型训练,、
类似于训练词向量基于窗口中的由前n个字预测下一个字的训练方式,称为自回归的语言模型训练
随机mask词语,由两边词预测中间被mask的词
2.句子关系预测
NSP:Next Sentence Prediction
[CLS] 师徒四人历经艰险[SEP] 取得真经[SEP] ——> True
[CLS] 师徒四人历经艰险[SEP] 火烧赤壁[SEP] ——> False
两句话如果在样本中是上下文,则给出预测True,如果不是上下文,则预测结果为False
CLS、SEP:看作是特殊字符,加在完整的句子前后的token
三、BERT — context representation 文本表征
BERT的本质是一种完整的文本表征(context representation)
文本 —> 矩阵 (max length × hidden size)
文本 —> 向量 (1 × hidden size)
word2vec也可以做到同样的事,但word2vec是静态的向量表示,而BERT是动态的向量表示
Bert:Bert模型中的网络层,是一个 Embedding词嵌入层 + 许多网络层(如Transformer层),这种大的语言模型输出的结果可以看作是一种动态的向量表示,考虑整句话的信息;
Word2Vec:而词向量是一种静态的向量表示,会将不同语境下的同一个词映射到相同的向量
例:我喜欢吃苹果 苹果和华为哪个牌子好
词义要结合语境来判断,语境不同,相同词映射到的向量不同
四、下游任务中的使用
Pre-training 预训练 Fine-Tuning 微调
文本匹配 文本分类
序列标注
语言模型的提升是通过大量的数据进行训练
五、BERT模型结构 — ① Embedding层
Bert模型的Embedding层实质上应分为三个小的网络子层:
① Token Embeddings 词嵌入层
Token Embeddings可以理解为词的编码
将输入的词元(Token)转换为对应的向量表示。在自然语言处理中,计算机无法直接处理文本,因此需要将文本中的每个词映射为一个固定长度的向量,这样可以在向量空间中进行后续的计算和处理。对每一个词有一个对应的Embedding(768维的向量,维度是Bert的base版本使用的大小,维度可指定)
参数量:词表大小 × 指定维度大小,与词表中的字数有关
② Segment embeddings 片段嵌入层
Segment embedding可以理解为句子的编码
区分输入中的不同句子或片段。在 BERT 模型的一些任务(如问答系统、句子对分类等)中,输入通常包含两个句子,Segment Embedding通过为每个句子分配唯一的向量标识(如0和1),帮助模型区分两个句子的边界,片段嵌入层可以帮助模型识别哪些词属于第一个句子,哪些词属于第二个句子。判断来源语句,若输入中有超过一句话,则所有的同一句话中的词对应的词向量Embedding相同,在[CLS]、[SEP]出现后很少使用
参数量:2 × 指定维度大小
③ Position embeddings 位置嵌入层
Pisition embedding可以理解为位置的编码
绝对位置编码:带入语序信息,标识每个字/词在句子中的位置,位置嵌入层用于为输入序列中的每个词元添加位置信息。由于 Transformer 模型本身没有像循环神经网络(RNN)那样的顺序处理机制,因此需要通过位置嵌入来让模型了解词元在序列中的位置。
参数量:位置数 × 指定维度大小,位置数在一开始时设置的是512位,特点:事先确定可预测的最大长度,位置编码与文字内容无关,只与句子长度有关
Segment Embedding与Position Embedding共同构成输入表示,前者区分片段,后者编码位置信息
同维度,三层加和后做Layer Normalization,得到Bert的Embedding向量,最终得到一个文本长度L × 指定向量维度的矩阵代表Bert完整的Embedding层
加入 [CLS] [SEP] 来标记文本起始位置:CLS:放在每个句首的token,SEP:放在每个句尾的token
Segment Embeddings 判断来源语句
Position Embeddings 带入语序信息
加和后会做 Layer Normalization(不改变张量的形状)
当输入input过Enbedding层后,需要过两部分网络结构,一部分是Self-Attention,一部分是Feed Forward
六、BERT模型结构 —— ② Encoder(Transformer)层
BERT的模型主体结构使用Google自己在17年提出的Transformer结构,Bert只有单Encoder没有Decoder(Oniy Encoder)
1.⭐Self-Attention 自注意力
自注意力机制(Self - Attention Mechanism)是 Transformer 架构的核心组成部分,它能够让模型在处理序列数据时,动态地关注序列中不同位置的元素,从而更好地捕捉序列中的长距离依赖关系。
为了计算注意力分数,首先需要将输入 X 通过三个不同的线性变换得到查询(Query)矩阵 Q 、键(Key)矩阵 K 和 值(Value)矩阵 V。
⭐ 核心公式:
符号 | 名称 | 形状 | 说明 |
---|---|---|---|
X | 输入序列 过Embedding后的输出 | (batch_size, seq_len, d_model) | 原始输入,包含 batch_size 个样本,每个样本有 seq_len 个词向量,每个词向量维度为 d_model |
W^Q | Query权重矩阵 | (d_model, d_k) | 将输入 X 投影到 Query 空间的权重矩阵 |
W^K | Key权重矩阵 | (d_model, d_k) | 将输入 X 投影到 Key 空间的权重矩阵 |
W^V | Value权重矩阵 | (d_model, d_v) | 将输入 X 投影到 Value 空间的权重矩阵 |
Q | Query矩阵 | (batch_size, seq_len, d_k) | 通过 Q=X⋅W^Q 计算得到 |
K | Key矩阵 | (batch_size, seq_len, d_k) | 通过 K=X⋅W^K 计算得到 |
V | Value矩阵 | (batch_size, seq_len, d_v) | 通过 V=X⋅W^V 计算得到 |
X:过Embedding层后的输出(文本序列长度L × 每个词向量维度大小)
W:线性层的权重(每个词向量维度大小 × 参数Wq / Wk / Wv指定的维度大小),参数W随机初始化,在训练中更新权重,查询权重矩阵与键权重矩阵指定的维度相等,键权重矩阵的指定维度一般情况下与二者相等
Q:Q = X * Wq.T + b,查询矩阵(Query)经过Wq线性层得到的向量(文本序列长度L × 参数Wq指定的维度大小)
K:K = X * Wk.T + b,键矩阵(Key)经过Wk线性层得到的向量(文本序列长度L × 参数Wk指定的维度大小)
Q、K:构造查询矩阵和键矩阵,其实是对V的线性映射的一种优化,我们希望映射更能看到重点,所以加入了注意力机制
K.T:键矩阵(Key)的转置(参数Wk指定的维度大小 × 文本序列长度L)
V:V = X * Wv.T + b,值矩阵(Value)经过Wv线性层得到的向量(文本序列长度L × 参数Wv指定的维度大小)
注意力分数矩阵 S:Q * K.T,包含了查询向量和键向量之间的相似度信息。具体来说,Sij 表示第 i 个查询向量和第 j 个键向量的相似度得分,这个得分反映了在生成第 i 个位置的输出时,应该对第 j 个位置的输入给予多少关注。(文本序列长度L × 文本序列长度L)
Q * K.T也是 self-attention 自注意力机制名字的由来,因为矩阵的含义是文本中的每个字自身与文本中的其他字任意两个字之间的 相关性(注意力) 的体现
self-attention 比 RNN 模型结构好的一点就是可以无视距离的影响,将两个相关性高的字连接起来,处理长文本的能力远强于 RNN 模型结构
d_k:在自注意力机制的计算过程中,输入序列会通过线性变换分别得到查询矩阵Q 、键矩阵K 和值矩阵V ,其中查询矩阵和键矩阵里每个向量的维度就是d_k 。在多头机制 multi-Head中,假设头的数量为h,则每个头的查询矩阵Qi和键矩阵Ki里每个向量的维度变为每个词向量维度大小 / 划分的头的数量。(是一个常数,没有形状,不影响张量的形状)
d_v:值矩阵里每个向量的维度就是d_v,一般情况下,d_k = d_v,Bert模型中,d_v默认等于d_k,在多头机制 multi-Head中,假设头的数量为h,则每个头的值矩阵里每个向量的维度变为每个词向量维度大小 / 划分的头的数量。(是一个常数,没有形状,不影响张量的形状)
Q * K.T / dk^1/2:用查询向量和键向量的相似度信息除以查询矩阵和键矩阵里每个向量的维度(文本序列长度L × 文本序列长度L)
softmax:将这个向量进行归一化,不改变维度,得到某一个字对于这整句话的注意力。(文本序列长度L × 文本序列长度L)
Attention(Q,K,V):过softmax后得到的向量(L * L)与值矩阵V(L * d_v)相乘后的结果,最终得到的自注意力(文本序列长度L × 参数Wv指定的维度大小)
线性层:将得到的注意力分数Attention(Q,K,V)过一个线性层(参数Wv指定的维度大小 × 参数Wv指定的维度大小),得到的结果不改变形状(文本序列长度L × 参数Wv指定的维度大小)
通常每个词向量维度大小d_model = 参数Wv指定的维度大小d_v,输出得到的自注意力形状与输入x形状相同(文本序列长度L × 每个词向量维度大小),所以可以在模型中叠加多层注意力机制
2.multi-head 多头机制
① 先对Q、K、V进行分块 / 分头:
把一个大的张量,切成指定数量的块(文本序列长度L × 每个词向量维度大小 / 块的数量)
② 每个块分别单独学习:
每一个头单独学习,每一个头有不同的侧重点去学习句子中字与字之间的潜在关系,
③ 汇总多个块的模型:
再将若干个块训练的小模型拼接在一起,集成为一个好的大模型,每一个头能够把握文本之间的其他相关关系,将这些头拼在一起,然后最后通过不同头算出的结果把握到不同的结果,作为模型真正学到的东西
分头后的d_k:每个词向量的维度大小 / 划分的头的数量
softMax特点:较小的值会倾向于0,较大的值会倾向于1,除以根号下dk的意义是:防止softMax层直接倾向于0 / 1,用指数函数的特点(x越大,相邻点变化越大)进行平滑,使得softMax的倾向相对均匀(不会直接倾向于0或1),便于模型的学习和优化
3.Add & Normalize 残差层
残差机制:将过网络层的输出和原始输入相加,有助于原始信息的保留,对于训练深层次的网络尤为重要
① 经过Transformer的Self-Attention层
② 将过Self-Attention层的输入和未过Self-Attention层的原输入相加
③ 相加完后过LayerNorm进行归一化
④ 随后进入Feed Forward层
4.Feed Forward层
Feed Forward:两个线性层,中间加一个激活层
先过一次线性层,放大向量维度,然后过一个Relu激活函数,然后再过一个线性层,将向量维度再缩小映射回原维度,Bert模型中,Relu激活函数被换为Gelu激活函数
然后过Feed Foward层的输出再与过网络层前的输入向量做一次加和,过一次残差机制,保留原始信息,相加完后再过一层LayerNorm进行归一化
至此,一层Transformer结束,形状不变(文本序列长度L × 每个词向量维度大小),再进入下一层Transformer进行叠加【Transformer层可进行多层叠加,大语言模型已叠加了40+层】
5.Bert模型中的Transformer层结构:
① 多头自注意力层(含分块计算Q/K/V,拼接输出)
② 残差机制加和(残差连接)
③ LayerNorm层归一化
④ Feed Forward FFD层(含两个线性层,一个激活层)
⑤ 残差机制加和(残差链接)
⑥ LayerNorm层归一化: 对每个样本的所有特征维度进行归一化,使其均值为0、方差为1,并通过可学习的参数(γ 和 β)恢复数据表达能力
————————————————————————————————————————
至此,一层Transformer层结束,数据形状没有发生变化(文本序列长度L × 每个词向量维度大小),所以可以继续拼接下一层Transformer层
BERT的模型主体结构使用Google自己在17年提出的Transformer模型结构,可以保留原始信息
6.代码实现
Ⅰ、用Bert模型预测
BertModel.from_pretrained():transformers
库中用于加载预训练 BERT 模型的方法。它可以从本地路径或者 Hugging Face 模型库中加载预训练的 BERT 模型及其配置。
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例 |
---|---|---|---|---|---|
pretrained_model_name_or_path | str 或 os.PathLike | 必需 | 无 | 预训练模型名称(如 bert-base-uncased )或本地模型路径。支持 Hugging Face Hub 或本地目录。 | model = BertModel.from_pretrained("bert-base-chinese") |
config | PretrainedConfig | 可选 | None | 自定义配置对象,覆盖默认模型配置。 | config = BertConfig(hidden_size=1024); model = BertModel.from_pretrained(..., config=config) |
cache_dir | str | 可选 | None | 指定模型缓存目录,用于存储下载的预训练文件。 | cache_dir="./models" |
from_tf | bool | 可选 | False | 是否加载 TensorFlow 格式的模型权重(.h5 文件)。 | from_tf=True |
from_flax | bool | 可选 | False | 是否加载 Flax(JAX)格式的模型权重。 | from_flax=True |
force_download | bool | 可选 | False | 强制重新下载模型文件,即使本地已存在缓存。 | force_download=True |
resume_download | bool | 可选 | False | 断点续传下载模型文件。 | resume_download=True |
proxies | Dict | 可选 | None | 设置代理服务器(如 {"http": "http://10.10.1.10:3128"} )。 | proxies={"http": proxy_url} |
local_files_only | bool | 可选 | False | 仅使用本地文件,不连接网络。 | local_files_only=True |
revision | str | 可选 | "main" | 指定模型版本(Git 分支或 commit id)。 | revision="v2.0" |
mirror | str | 可选 | None | 指定镜像源(如国内镜像站点 "huggingface" )。 | mirror="huggingface" |
bert.eval():这是 PyTorch 中用于将模型设置为评估模式的方法。在评估模式下,一些层(如 Dropout
、BatchNorm
等)会改变其行为,以适应模型的评估过程。
np.array():将输入数据(如列表、元组等)转换为NumPy数组,支持多维数组和指定数据类型
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例/参考来源 |
---|---|---|---|---|---|
object | array_like | 必需 | 无 | 输入数据,支持列表、元组、嵌套序列或其他数组接口对象。 | np.array([1, 2, 3]) → 一维数组 |
dtype | 数据类型对象 | 可选 | None | 指定数组元素的数据类型(如 int32 、float64 )。若未指定,自动推断输入数据的最小兼容类型。 | np.array([1.1, 2.5], dtype=int) → [1, 2] |
copy | bool | 可选 | True | 是否复制输入对象。若为 False ,且输入满足条件(如类型一致),则共享内存。 | a = [1, 2]; b = np.array(a, copy=False) (修改 a 会影响 b ) |
order | {'K', 'C', 'F'} | 可选 | 'K' | 内存布局:'C' (行优先)、'F' (列优先)、'K' (保留输入顺序)。 | np.array([[1,2],[3,4]], order='F') → 列优先存储 |
subok | bool | 可选 | False | 是否返回子类数组。若为 True ,输入为子类时返回子类实例,否则强制转为基类数组。 | np.array(np.mat('1 2'), subok=True) → 返回矩阵对象 |
ndmin | int | 可选 | 0 | 指定生成数组的最小维度。自动在形状前补 1 以满足维度要求。 | np.array([1,2,3], ndmin=2) → [[1, 2, 3]] (二维数组) |
torch.LongTensor(): PyTorch 中用于存储 长整型(64位整数) 的张量类型。它在 PyTorch 的早期版本中较为常见,但在新版本中推荐使用 torch.tensor()
或 torch.int64
(功能等价)。
参数 | 类型 | 说明 | 来源 |
---|---|---|---|
data | 列表/元组/数组等 | 必填参数,用于初始化张量的数据。支持 Python 列表、NumPy 数组、标量等。 | 2 4 |
dtype | torch.dtype | 可选参数,默认根据 data 自动推断。常用类型包括 torch.float32 、torch.int64 (等价于 torch.LongTensor )等。 | 1 2 4 |
device | torch.device | 可选参数,默认为 CPU。指定张量存储设备(如 torch.device("cuda:0") )。 | 2 4 |
requires_grad | bool | 可选参数,默认 False 。若设为 True ,张量将记录梯度用于反向传播。 | 2 4 |
pin_memory | bool | 可选参数,默认 False 。若设为 True ,张量数据将存储在固定内存中(仅 CPU 有效)。 |
state_dict(): PyTorch 中 nn.Module
类的一个方法,用于返回一个包含模型所有可学习参数的字典。字典的键是参数的名称,值是对应的参数张量。
.shape: PyTorch 中用于获取张量(Tensor)形状(维度信息)的属性。它返回一个表示张量各维度大小的 torch.Size
对象,该对象类似于 Python 的元组(tuple)。
keys():返回字典的所有键的视图对象(动态更新,随字典变化而变化)
import torch
import math
import numpy as np
from transformers import BertModel
'''
通过手动矩阵运算实现Bert结构
模型文件下载 https://huggingface.co/models
'''
bert = BertModel.from_pretrained(r"F:\人工智能NLP\NLP资料\week6 语言模型\bert-base-chinese", return_dict=False)
state_dict = bert.state_dict()
bert.eval()
x = np.array([2450, 15486, 102, 2110]) #假想成4个字的句子
torch_x = torch.LongTensor([x]) #pytorch形式输入
seqence_output, pooler_output = bert(torch_x)
print(seqence_output.shape, pooler_output.shape)
# print(seqence_output, pooler_output)
print(bert.state_dict().keys()) #查看所有的权值矩阵名称
Ⅱ、手动实现softmax归一化函数
np.exp():计算输入 x
中每个元素的 e 次幂(即 e^x),其中 e ≈ 2.71828。该函数广泛应用于科学计算、统计学(如概率密度函数)和机器学习(如 Softmax 函数)
参数名称 | 类型 | 是否必填 | 说明 | |
---|---|---|---|---|
x | 数组/标量 | 必填 | 输入值,支持数组或标量。Numpy会对数组中的每个元素计算e的指数幂(e^x)。 | |
out | 数组 | 可选 | 指定输出数组,结果将存储在此数组中。 | |
where | 布尔数组 | 可选 | 指定计算位置,True 表示计算,False 跳过。 | |
dtype | 数据类型 | 可选 | 指定输出数组的数据类型。 | |
keepdims | 布尔值 | 可选 | 是否保持原数组的维度(仅对多维数组有效)。 |
np.sum():计算输入数组 a
中所有元素的总和,或沿指定轴(axis
)的元素和。适用于科学计算、数据统计等场景
参数名称 | 类型 | 是否必填 | 说明 | |
---|---|---|---|---|
a | 数组/标量 | 必填 | 需要求和的数组或标量。 | |
axis | 整数/元组/None | 可选 | 指定求和轴: |
#softmax归一化
def softmax(x):
return np.exp(x)/np.sum(np.exp(x), axis=-1, keepdims=True)
Ⅲ、手动实现gelu激活函数
np.tanh():计算数组中每个元素的双曲正切值,输出范围 (-1, 1)
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
x | array_like | 必需 | 无 | 输入数组或数值 |
out | ndarray | 可选 | None | 存储结果的数组 |
math.sqrt():计算非负数的平方根,返回浮点数
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
x | float | 必需 | 无 | 非负数(负数报错) |
math.pi:返回圆周率π的近似值(3.141592653589793)
np.power():计算数组元素的幂(支持广播机制)
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
x1 | array_like | 必需 | 无 | 底数数组或数值 |
x2 | array_like | 必需 | 无 | 指数数组或数值 |
out | ndarray | 可选 | None | 存储结果的数组 |
#gelu激活函数
def gelu(x):
return 0.5 * x * (1 + np.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * np.power(x, 3))))
Ⅳ、手动实现Bert模型 —— ① 模型初始化
#将预训练好的整个权重字典输入进来
def __init__(self, state_dict):
self.num_attention_heads = 12
self.hidden_size = 768
self.num_layers = 1 #注意这里的层数要跟预训练config.json文件中的模型层数一致
self.load_weights(state_dict)
Ⅴ、手动实现Bert模型 —— ② 加载模型权重
.numpy(): PyTorch 中张量(Tensor)的一个方法,用于将 PyTorch 张量转换为 NumPy 数组。这在需要利用 NumPy 的强大功能进行数据处理、可视化或其他操作时非常有用。
append():向列表末尾添加元素(直接修改原列表)
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
element | 任意类型 | 必需 | 无 | 要添加的元素 |
def load_weights(self, state_dict):
#embedding部分
self.word_embeddings = state_dict["embeddings.word_embeddings.weight"].numpy()
self.position_embeddings = state_dict["embeddings.position_embeddings.weight"].numpy()
self.token_type_embeddings = state_dict["embeddings.token_type_embeddings.weight"].numpy()
self.embeddings_layer_norm_weight = state_dict["embeddings.LayerNorm.weight"].numpy()
self.embeddings_layer_norm_bias = state_dict["embeddings.LayerNorm.bias"].numpy()
self.transformer_weights = []
#transformer部分,有多层
for i in range(self.num_layers):
q_w = state_dict["encoder.layer.%d.attention.self.query.weight" % i].numpy()
q_b = state_dict["encoder.layer.%d.attention.self.query.bias" % i].numpy()
k_w = state_dict["encoder.layer.%d.attention.self.key.weight" % i].numpy()
k_b = state_dict["encoder.layer.%d.attention.self.key.bias" % i].numpy()
v_w = state_dict["encoder.layer.%d.attention.self.value.weight" % i].numpy()
v_b = state_dict["encoder.layer.%d.attention.self.value.bias" % i].numpy()
attention_output_weight = state_dict["encoder.layer.%d.attention.output.dense.weight" % i].numpy()
attention_output_bias = state_dict["encoder.layer.%d.attention.output.dense.bias" % i].numpy()
attention_layer_norm_w = state_dict["encoder.layer.%d.attention.output.LayerNorm.weight" % i].numpy()
attention_layer_norm_b = state_dict["encoder.layer.%d.attention.output.LayerNorm.bias" % i].numpy()
intermediate_weight = state_dict["encoder.layer.%d.intermediate.dense.weight" % i].numpy()
intermediate_bias = state_dict["encoder.layer.%d.intermediate.dense.bias" % i].numpy()
output_weight = state_dict["encoder.layer.%d.output.dense.weight" % i].numpy()
output_bias = state_dict["encoder.layer.%d.output.dense.bias" % i].numpy()
ff_layer_norm_w = state_dict["encoder.layer.%d.output.LayerNorm.weight" % i].numpy()
ff_layer_norm_b = state_dict["encoder.layer.%d.output.LayerNorm.bias" % i].numpy()
self.transformer_weights.append([q_w, q_b, k_w, k_b, v_w, v_b, attention_output_weight, attention_output_bias,
attention_layer_norm_w, attention_layer_norm_b, intermediate_weight, intermediate_bias,
output_weight, output_bias, ff_layer_norm_w, ff_layer_norm_b])
#pooler层
self.pooler_dense_weight = state_dict["pooler.dense.weight"].numpy()
self.pooler_dense_bias = state_dict["pooler.dense.bias"].numpy()
Ⅵ、手动实现Bert模型 —— ③ 词嵌入
np.array():将输入数据(如列表、元组等)转换为NumPy数组,支持多维数组和指定数据类型
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例/参考来源 |
---|---|---|---|---|---|
object | array_like | 必需 | 无 | 输入数据,支持列表、元组、嵌套序列或其他数组接口对象。 | np.array([1, 2, 3]) → 一维数组 |
dtype | 数据类型对象 | 可选 | None | 指定数组元素的数据类型(如 int32 、float64 )。若未指定,自动推断输入数据的最小兼容类型。 | np.array([1.1, 2.5], dtype=int) → [1, 2] |
copy | bool | 可选 | True | 是否复制输入对象。若为 False ,且输入满足条件(如类型一致),则共享内存。 | a = [1, 2]; b = np.array(a, copy=False) (修改 a 会影响 b ) |
order | {'K', 'C', 'F'} | 可选 | 'K' | 内存布局:'C' (行优先)、'F' (列优先)、'K' (保留输入顺序)。 | np.array([[1,2],[3,4]], order='F') → 列优先存储 |
subok | bool | 可选 | False | 是否返回子类数组。若为 True ,输入为子类时返回子类实例,否则强制转为基类数组。 | np.array(np.mat('1 2'), subok=True) → 返回矩阵对象 |
ndmin | int | 可选 | 0 | 指定生成数组的最小维度。自动在形状前补 1 以满足维度要求。 | np.array([1,2,3], ndmin=2) → [[1, 2, 3]] (二维数组) |
#embedding层实际上相当于按index索引,或理解为onehot输入乘以embedding矩阵
def get_embedding(self, embedding_matrix, x):
return np.array([embedding_matrix[index] for index in x])
Ⅴ、手动实现Bert模型 —— ④ 嵌入层
np.array():将输入数据(如列表、元组等)转换为NumPy数组,支持多维数组和指定数据类型
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例/参考来源 |
---|---|---|---|---|---|
object | array_like | 必需 | 无 | 输入数据,支持列表、元组、嵌套序列或其他数组接口对象。 | np.array([1, 2, 3]) → 一维数组 |
dtype | 数据类型对象 | 可选 | None | 指定数组元素的数据类型(如 int32 、float64 )。若未指定,自动推断输入数据的最小兼容类型。 | np.array([1.1, 2.5], dtype=int) → [1, 2] |
copy | bool | 可选 | True | 是否复制输入对象。若为 False ,且输入满足条件(如类型一致),则共享内存。 | a = [1, 2]; b = np.array(a, copy=False) (修改 a 会影响 b ) |
order | {'K', 'C', 'F'} | 可选 | 'K' | 内存布局:'C' (行优先)、'F' (列优先)、'K' (保留输入顺序)。 | np.array([[1,2],[3,4]], order='F') → 列优先存储 |
subok | bool | 可选 | False | 是否返回子类数组。若为 True ,输入为子类时返回子类实例,否则强制转为基类数组。 | np.array(np.mat('1 2'), subok=True) → 返回矩阵对象 |
ndmin | int | 可选 | 0 | 指定生成数组的最小维度。自动在形状前补 1 以满足维度要求。 | np.array([1,2,3], ndmin=2) → [[1, 2, 3]] (二维数组) |
range():生成整数序列,常用于循环
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
start | int | 可选 | 0 | 起始值(包含) |
stop | int | 必需 | 无 | 结束值(不包含) |
step | int | 可选 | 1 | 步长(正数或负数) |
list():将可迭代对象(如元组、字符串)转换为列表
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象 | 必需 | 无 | 输入数据(如元组) |
#bert embedding,使用3层叠加,在经过一个Layer norm层
def embedding_forward(self, x):
# x.shape = [max_len]
we = self.get_embedding(self.word_embeddings, x) # shpae: [max_len, hidden_size]
# position embeding的输入 [0, 1, 2, 3]
pe = self.get_embedding(self.position_embeddings, np.array(list(range(len(x))))) # shpae: [max_len, hidden_size]
# token type embedding,单输入的情况下为[0, 0, 0, 0]
te = self.get_embedding(self.token_type_embeddings, np.array([0] * len(x))) # shpae: [max_len, hidden_size]
embedding = we + pe + te
# 加和后有一个归一化层
embedding = self.layer_norm(embedding, self.embeddings_layer_norm_weight, self.embeddings_layer_norm_bias) # shpae: [max_len, hidden_size]
return embedding
Ⅵ、手动实现Bert模型 —— ⑤ Transformer层的计算
range():生成整数序列,常用于循环
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
start | int | 可选 | 0 | 起始值(包含) |
stop | int | 必需 | 无 | 结束值(不包含) |
step | int | 可选 | 1 | 步长(正数或负数) |
#执行全部的transformer层计算
def all_transformer_layer_forward(self, x):
for i in range(self.num_layers):
x = self.single_transformer_layer_forward(x, i)
return x
#执行单层transformer层计算
def single_transformer_layer_forward(self, x, layer_index):
weights = self.transformer_weights[layer_index]
#取出该层的参数,在实际中,这些参数都是随机初始化,之后进行预训练
q_w, q_b, \
k_w, k_b, \
v_w, v_b, \
attention_output_weight, attention_output_bias, \
attention_layer_norm_w, attention_layer_norm_b, \
intermediate_weight, intermediate_bias, \
output_weight, output_bias, \
ff_layer_norm_w, ff_layer_norm_b = weights
#self attention层
attention_output = self.self_attention(x,
q_w, q_b,
k_w, k_b,
v_w, v_b,
attention_output_weight, attention_output_bias,
self.num_attention_heads,
self.hidden_size)
#bn层,并使用了残差机制
x = self.layer_norm(x + attention_output, attention_layer_norm_w, attention_layer_norm_b)
#feed forward层
feed_forward_x = self.feed_forward(x,
intermediate_weight, intermediate_bias,
output_weight, output_bias)
#bn层,并使用了残差机制
x = self.layer_norm(x + feed_forward_x, ff_layer_norm_w, ff_layer_norm_b)
return x
Ⅶ、手动实现Bert模型 —— ⑥ 自注意力机制
np.matmul():专门用于矩阵乘法,严格遵循线性代数中的矩阵乘法规则,主要面向二维及以上数组的矩阵运算,支持批量矩阵乘法(广播机制),适用于高维数组的批量处理,对一维数组自动视为行向量或列向量进行矩阵乘法(无需显式变形)
np.matmul()
是专门用于矩阵乘法的,而np.dot()
更通用
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
a | array_like | 必需 | 无 | 输入数组 |
b | array_like | 必需 | 无 | 输入数组 |
out | ndarray | 可选 | None | 存储结果的数组 |
swapaxes():交换数组的两个轴
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
axis1 | int | 必需 | 无 | 第一个轴的索引 |
axis2 | int | 必需 | 无 | 第二个轴的索引 |
reshape():改变数组形状(不修改数据)
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
newshape | int/tuple | 必需 | 无 | 新形状(如 (2,3) 或 -1 ) |
np.dot():计算点积(一维数组为内积,二维为矩阵乘法),通用函数,功能更广泛,支持多种维度数组的操作,包括向量点积、矩阵乘法、张量积等,对二维数组执行标准矩阵乘法,与 np.matmul()
结果相同,对高维数组时,计算最后一个轴与倒数第二个轴的乘积(张量积)
np.matmul()
是专门用于矩阵乘法的,而np.dot()
更通用
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
a | array_like | 必需 | 无 | 输入数组 |
b | array_like | 必需 | 无 | 输入数组 |
np.sqrt(): NumPy 库中的一个函数,用于计算数组中每个元素的平方根。它在科学计算、数据处理和机器学习等领域中非常常用。
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
x | array_like | 输入数组,可以是标量或任何形状的数组。支持整数、浮点数及复数类型。 | — |
out | ndarray , 可选 | 用于存放结果的数组。必须具有适当的形状和数据类型。如果未提供,将创建一个新的数组。 | None |
where | array_like , 可选 | 布尔数组或条件,指定在哪些位置进行计算。仅对满足条件的元素执行平方根运算。 | True |
casting | str , 可选 | 控制输出数组的数据类型转换方式。常用的值包括 'same_kind' 和 'unsafe' 。 | 'same_kind' |
order | str , 可选 | 指定输出数组的内存布局。常用的值包括 'K' (保持输入内存布局)、'C' (C 风格连续)、'F' (Fortran 风格连续)。 | 'K' |
dtype | dtype , 可选 | 指定输出数组的数据类型。如果未指定,将根据输入数组和 casting 参数推断。 | — |
subok | bool , 可选 | 是否允许子类输出。如果为 True ,输出可以是输入数组的子类;否则,输出将是基类数组。 | True |
# self attention的计算
def self_attention(self,
x,
q_w,
q_b,
k_w,
k_b,
v_w,
v_b,
attention_output_weight,
attention_output_bias,
num_attention_heads,
hidden_size):
# x.shape = max_len * hidden_size
# q_w, k_w, v_w shape = hidden_size * hidden_size
# q_b, k_b, v_b shape = hidden_size
q = np.dot(x, q_w.T) + q_b # shape: [max_len, hidden_size] W * X + B lINER
k = np.dot(x, k_w.T) + k_b # shpae: [max_len, hidden_size]
v = np.dot(x, v_w.T) + v_b # shpae: [max_len, hidden_size]
attention_head_size = int(hidden_size / num_attention_heads)
# q.shape = num_attention_heads, max_len, attention_head_size
q = self.transpose_for_scores(q, attention_head_size, num_attention_heads)
# k.shape = num_attention_heads, max_len, attention_head_size
k = self.transpose_for_scores(k, attention_head_size, num_attention_heads)
# v.shape = num_attention_heads, max_len, attention_head_size
v = self.transpose_for_scores(v, attention_head_size, num_attention_heads)
# qk.shape = num_attention_heads, max_len, max_len
qk = np.matmul(q, k.swapaxes(1, 2))
qk /= np.sqrt(attention_head_size)
qk = softmax(qk)
# qkv.shape = num_attention_heads, max_len, attention_head_size
qkv = np.matmul(qk, v)
# qkv.shape = max_len, hidden_size
qkv = qkv.swapaxes(0, 1).reshape(-1, hidden_size)
# attention.shape = max_len, hidden_size
attention = np.dot(qkv, attention_output_weight.T) + attention_output_bias
return attention
Ⅷ、手动实现Bert模型 —— ⑦ 多头机制
.shape: PyTorch 中用于获取张量(Tensor)形状(维度信息)的属性。它返回一个表示张量各维度大小的 torch.Size
对象,该对象类似于 Python 的元组(tuple)。
.reshape:NumPy 和 PyTorch 中用于改变数组(或张量)形状的方法。通过 reshape
,你可以重新排列数组中的元素,而不改变其数据,从而获得不同维度的视图。
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
newshape | int 或 tuple | 目标形状。可以是一个整数,表示将数组展平为一维,或者是一个元组,指定每个维度的大小。例如,(2, 3) 表示二维数组,形状为 2 行 3 列。如果某个维度的大小为 -1 ,则该维度的大小将根据其他维度和总元素数自动推断。 | — |
order | {'C', 'F', 'A'} | 可选参数,指定内存布局方式: - 'C' :按行优先(C 风格)排列元素。- 'F' :按列优先(Fortran 风格)排列元素。- 'A' :如果原数组是 Fortran 连续的,则按 'F' 排列;否则按 'C' 排列。 | 'C' |
swapaxes():交换数组的两个轴
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
axis1 | int | 必需 | 无 | 第一个轴的索引 |
axis2 | int | 必需 | 无 | 第二个轴的索引 |
#多头机制
def transpose_for_scores(self, x, attention_head_size, num_attention_heads):
# hidden_size = 768 num_attent_heads = 12 attention_head_size = 64 768切成12份,每份64维
max_len, hidden_size = x.shape
x = x.reshape(max_len, num_attention_heads, attention_head_size)
x = x.swapaxes(1, 0) # output shape = [num_attention_heads, max_len, attention_head_size]
return x
Ⅸ、手动实现Bert模型 —— ⑧ 前馈网络计算
np.dot():计算点积(一维数组为内积,二维为矩阵乘法),通用函数,功能更广泛,支持多种维度数组的操作,包括向量点积、矩阵乘法、张量积等,对二维数组执行标准矩阵乘法,与 np.matmul()
结果相同,对高维数组时,计算最后一个轴与倒数第二个轴的乘积(张量积)
np.matmul()
是专门用于矩阵乘法的,而np.dot()
更通用
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
a | array_like | 必需 | 无 | 输入数组 |
b | array_like | 必需 | 无 | 输入数组 |
#前馈网络的计算
def feed_forward(self,
x,
intermediate_weight, # intermediate_size, hidden_size
intermediate_bias, # intermediate_size
output_weight, # hidden_size, intermediate_size
output_bias, # hidden_size
):
# output shpae: [max_len, intermediate_size]
x = np.dot(x, intermediate_weight.T) + intermediate_bias
x = gelu(x)
# output shpae: [max_len, hidden_size]
x = np.dot(x, output_weight.T) + output_bias
return x
Ⅹ、手动实现Bert模型 —— ⑨ 归一化层
np.mean():计算数组元素的平均值
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
a | array_like | 必需 | 无 | 输入数组 |
axis | int/tuple | 可选 | None | 沿指定轴计算(如 axis=0 ) |
dtype | data-type | 可选 | None | 输出数据类型 |
keepdims | bool | 可选 | False | 是否保持维度 |
np.std(): NumPy 库中的一个函数,用于计算数组(或数组的一部分)的标准差。标准差是一种衡量数据分散程度的统计量,反映了数据点相对于均值的离散程度。
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
a | array_like | 输入数组,可以是列表、元组或 NumPy 数组。 | — |
axis | int 或 None 或 tuple of ints , 可选 | 沿着哪个轴计算标准差。如果为 None ,则计算整个数组的标准差;如果为整数或元组,表示沿着指定的轴计算。 | None |
dtype | data-type , 可选 | 用于计算标准差的数据类型。默认情况下,对于整数输入,使用 float64 ;对于浮点数输入,使用与输入相同的数据类型。 | — |
out | ndarray , 可选 | 用于存放结果的数组。必须具有适当的形状和数据类型。如果未提供,将创建一个新的数组。 | None |
keepdims | bool , 可选 | 如果为 True ,则缩减的轴将在结果中保留为长度为 1 的维度。这允许结果广播正确地应用于原始数组。 | False |
where | array_like 或 bool , 可选 | 用于指定在哪些位置进行计算。仅对满足条件的元素执行标准差计算。 | True |
#归一化层
def layer_norm(self, x, w, b):
x = (x - np.mean(x, axis=1, keepdims=True)) / np.std(x, axis=1, keepdims=True)
x = x * w + b
return x
ⅩⅠ、 手动实现Bert模型 —— ⑩ 输出层连接句子分隔符
np.dot():计算点积(一维数组为内积,二维为矩阵乘法),通用函数,功能更广泛,支持多种维度数组的操作,包括向量点积、矩阵乘法、张量积等,对二维数组执行标准矩阵乘法,与 np.matmul()
结果相同,对高维数组时,计算最后一个轴与倒数第二个轴的乘积(张量积)
np.matmul()
是专门用于矩阵乘法的,而np.dot()
更通用
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
a | array_like | 必需 | 无 | 输入数组 |
b | array_like | 必需 | 无 | 输入数组 |
np.tanh():计算数组中每个元素的双曲正切值,输出范围 (-1, 1)
参数名 | 类型 | 是否可选 | 默认值 | 描述 |
---|---|---|---|---|
x | array_like | 必需 | 无 | 输入数组或数值 |
out | ndarray | 可选 | None | 存储结果的数组 |
#链接[cls] token的输出层
def pooler_output_layer(self, x):
x = np.dot(x, self.pooler_dense_weight.T) + self.pooler_dense_bias
x = np.tanh(x)
return x
ⅩⅡ、手动实现Bert模型 —— 最终输出
import torch
import math
import numpy as np
from transformers import BertModel
'''
通过手动矩阵运算实现Bert结构
模型文件下载 https://huggingface.co/models
'''
bert = BertModel.from_pretrained(r"F:\人工智能NLP\NLP资料\week6 语言模型\bert-base-chinese", return_dict=False)
state_dict = bert.state_dict()
bert.eval()
x = np.array([2450, 15486, 102, 2110]) #假想成4个字的句子
torch_x = torch.LongTensor([x]) #pytorch形式输入
seqence_output, pooler_output = bert(torch_x)
print(seqence_output.shape, pooler_output.shape)
# print(seqence_output, pooler_output)
print(bert.state_dict().keys()) #查看所有的权值矩阵名称
#softmax归一化
def softmax(x):
return np.exp(x)/np.sum(np.exp(x), axis=-1, keepdims=True)
#gelu激活函数
def gelu(x):
return 0.5 * x * (1 + np.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * np.power(x, 3))))
class DiyBert:
#将预训练好的整个权重字典输入进来
def __init__(self, state_dict):
self.num_attention_heads = 12
self.hidden_size = 768
self.num_layers = 1 #注意这里的层数要跟预训练config.json文件中的模型层数一致
self.load_weights(state_dict)
def load_weights(self, state_dict):
#embedding部分
self.word_embeddings = state_dict["embeddings.word_embeddings.weight"].numpy()
self.position_embeddings = state_dict["embeddings.position_embeddings.weight"].numpy()
self.token_type_embeddings = state_dict["embeddings.token_type_embeddings.weight"].numpy()
self.embeddings_layer_norm_weight = state_dict["embeddings.LayerNorm.weight"].numpy()
self.embeddings_layer_norm_bias = state_dict["embeddings.LayerNorm.bias"].numpy()
self.transformer_weights = []
#transformer部分,有多层
for i in range(self.num_layers):
q_w = state_dict["encoder.layer.%d.attention.self.query.weight" % i].numpy()
q_b = state_dict["encoder.layer.%d.attention.self.query.bias" % i].numpy()
k_w = state_dict["encoder.layer.%d.attention.self.key.weight" % i].numpy()
k_b = state_dict["encoder.layer.%d.attention.self.key.bias" % i].numpy()
v_w = state_dict["encoder.layer.%d.attention.self.value.weight" % i].numpy()
v_b = state_dict["encoder.layer.%d.attention.self.value.bias" % i].numpy()
attention_output_weight = state_dict["encoder.layer.%d.attention.output.dense.weight" % i].numpy()
attention_output_bias = state_dict["encoder.layer.%d.attention.output.dense.bias" % i].numpy()
attention_layer_norm_w = state_dict["encoder.layer.%d.attention.output.LayerNorm.weight" % i].numpy()
attention_layer_norm_b = state_dict["encoder.layer.%d.attention.output.LayerNorm.bias" % i].numpy()
intermediate_weight = state_dict["encoder.layer.%d.intermediate.dense.weight" % i].numpy()
intermediate_bias = state_dict["encoder.layer.%d.intermediate.dense.bias" % i].numpy()
output_weight = state_dict["encoder.layer.%d.output.dense.weight" % i].numpy()
output_bias = state_dict["encoder.layer.%d.output.dense.bias" % i].numpy()
ff_layer_norm_w = state_dict["encoder.layer.%d.output.LayerNorm.weight" % i].numpy()
ff_layer_norm_b = state_dict["encoder.layer.%d.output.LayerNorm.bias" % i].numpy()
self.transformer_weights.append([q_w, q_b, k_w, k_b, v_w, v_b, attention_output_weight, attention_output_bias,
attention_layer_norm_w, attention_layer_norm_b, intermediate_weight, intermediate_bias,
output_weight, output_bias, ff_layer_norm_w, ff_layer_norm_b])
#pooler层
self.pooler_dense_weight = state_dict["pooler.dense.weight"].numpy()
self.pooler_dense_bias = state_dict["pooler.dense.bias"].numpy()
#bert embedding,使用3层叠加,在经过一个Layer norm层
def embedding_forward(self, x):
# x.shape = [max_len]
we = self.get_embedding(self.word_embeddings, x) # shpae: [max_len, hidden_size]
# position embeding的输入 [0, 1, 2, 3]
pe = self.get_embedding(self.position_embeddings, np.array(list(range(len(x))))) # shpae: [max_len, hidden_size]
# token type embedding,单输入的情况下为[0, 0, 0, 0]
te = self.get_embedding(self.token_type_embeddings, np.array([0] * len(x))) # shpae: [max_len, hidden_size]
embedding = we + pe + te
# 加和后有一个归一化层
embedding = self.layer_norm(embedding, self.embeddings_layer_norm_weight, self.embeddings_layer_norm_bias) # shpae: [max_len, hidden_size]
return embedding
#embedding层实际上相当于按index索引,或理解为onehot输入乘以embedding矩阵
def get_embedding(self, embedding_matrix, x):
return np.array([embedding_matrix[index] for index in x])
#执行全部的transformer层计算
def all_transformer_layer_forward(self, x):
for i in range(self.num_layers):
x = self.single_transformer_layer_forward(x, i)
return x
#执行单层transformer层计算
def single_transformer_layer_forward(self, x, layer_index):
weights = self.transformer_weights[layer_index]
#取出该层的参数,在实际中,这些参数都是随机初始化,之后进行预训练
q_w, q_b, \
k_w, k_b, \
v_w, v_b, \
attention_output_weight, attention_output_bias, \
attention_layer_norm_w, attention_layer_norm_b, \
intermediate_weight, intermediate_bias, \
output_weight, output_bias, \
ff_layer_norm_w, ff_layer_norm_b = weights
#self attention层
attention_output = self.self_attention(x,
q_w, q_b,
k_w, k_b,
v_w, v_b,
attention_output_weight, attention_output_bias,
self.num_attention_heads,
self.hidden_size)
#bn层,并使用了残差机制
x = self.layer_norm(x + attention_output, attention_layer_norm_w, attention_layer_norm_b)
#feed forward层
feed_forward_x = self.feed_forward(x,
intermediate_weight, intermediate_bias,
output_weight, output_bias)
#bn层,并使用了残差机制
x = self.layer_norm(x + feed_forward_x, ff_layer_norm_w, ff_layer_norm_b)
return x
# self attention的计算
def self_attention(self,
x,
q_w,
q_b,
k_w,
k_b,
v_w,
v_b,
attention_output_weight,
attention_output_bias,
num_attention_heads,
hidden_size):
# x.shape = max_len * hidden_size
# q_w, k_w, v_w shape = hidden_size * hidden_size
# q_b, k_b, v_b shape = hidden_size
q = np.dot(x, q_w.T) + q_b # shape: [max_len, hidden_size] W * X + B lINER
k = np.dot(x, k_w.T) + k_b # shpae: [max_len, hidden_size]
v = np.dot(x, v_w.T) + v_b # shpae: [max_len, hidden_size]
attention_head_size = int(hidden_size / num_attention_heads)
# q.shape = num_attention_heads, max_len, attention_head_size
q = self.transpose_for_scores(q, attention_head_size, num_attention_heads)
# k.shape = num_attention_heads, max_len, attention_head_size
k = self.transpose_for_scores(k, attention_head_size, num_attention_heads)
# v.shape = num_attention_heads, max_len, attention_head_size
v = self.transpose_for_scores(v, attention_head_size, num_attention_heads)
# qk.shape = num_attention_heads, max_len, max_len
qk = np.matmul(q, k.swapaxes(1, 2))
qk /= np.sqrt(attention_head_size)
qk = softmax(qk)
# qkv.shape = num_attention_heads, max_len, attention_head_size
qkv = np.matmul(qk, v)
# qkv.shape = max_len, hidden_size
qkv = qkv.swapaxes(0, 1).reshape(-1, hidden_size)
# attention.shape = max_len, hidden_size
attention = np.dot(qkv, attention_output_weight.T) + attention_output_bias
return attention
#多头机制
def transpose_for_scores(self, x, attention_head_size, num_attention_heads):
# hidden_size = 768 num_attent_heads = 12 attention_head_size = 64 768切成12份,每份64维
max_len, hidden_size = x.shape
x = x.reshape(max_len, num_attention_heads, attention_head_size)
x = x.swapaxes(1, 0) # output shape = [num_attention_heads, max_len, attention_head_size]
return x
#前馈网络的计算
def feed_forward(self,
x,
intermediate_weight, # intermediate_size, hidden_size
intermediate_bias, # intermediate_size
output_weight, # hidden_size, intermediate_size
output_bias, # hidden_size
):
# output shpae: [max_len, intermediate_size]
x = np.dot(x, intermediate_weight.T) + intermediate_bias
x = gelu(x)
# output shpae: [max_len, hidden_size]
x = np.dot(x, output_weight.T) + output_bias
return x
#归一化层
def layer_norm(self, x, w, b):
x = (x - np.mean(x, axis=1, keepdims=True)) / np.std(x, axis=1, keepdims=True)
x = x * w + b
return x
#链接[cls] token的输出层
def pooler_output_layer(self, x):
x = np.dot(x, self.pooler_dense_weight.T) + self.pooler_dense_bias
x = np.tanh(x)
return x
#最终输出
def forward(self, x):
x = self.embedding_forward(x)
sequence_output = self.all_transformer_layer_forward(x)
pooler_output = self.pooler_output_layer(sequence_output[0])
return sequence_output, pooler_output
#自制
db = DiyBert(state_dict)
diy_sequence_output, diy_pooler_output = db.forward(x)
#torch
torch_sequence_output, torch_pooler_output = bert(torch_x)
print(diy_sequence_output)
print(torch_sequence_output)
# print(diy_pooler_output)
# print(torch_pooler_output)
七、BERT的优劣势:
Ⅰ 优势
① 通过预训练利用了海量无标注文本数据
② 相比词向量,BERT的文本表示结合了语境
③ Transformer模型结构有很强的拟合能力,词与词之间的距离不会造成关系计算上的损失
④ 效果大幅提升
Ⅱ 劣势
① 预训练需要数据,时间,和机器(开源模型缓解了这一问题)
② 难以应用在生成式任务上
③ 参数量大,运算复杂,满足不了部分真实场景性能需求
④ 没有下游数据做fine-tune,效果依然不理想
八、Bert模型的编码方式
使用Bert预训练模型,必须同时使用Bert模型提供的词表vocab.txt(否则字对应不上)和分词方式【使用时和预训练时格式应保持一致】
tokenizer.tokenize():将原始文本分割为模型可处理的 token 序列,支持子词(如 BPE)、字符或单词级别的分词。分词结果受分词算法影响(如 BPE 合并高频子词,SentencePiece 处理无空格语言)。
仅需分词结果时使用
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例/参考来源 |
---|---|---|---|---|---|
text | str 或 List[str] | 必需 | 无 | 输入文本或文本列表,支持单条字符串或批量处理(根据具体实现可能不同)。 | tokenizer.tokenize("Hello world!") → ["hello", "world", "!"] (若启用小写) |
add_special_tokens | bool | 可选 | True | 是否添加特殊标记(如 [CLS] 、[SEP] ),常用于模型输入格式化。 | tokenizer.tokenize(text, add_special_tokens=False) → 仅返回原始分词结果 |
lower_case | bool | 可选 | False | 是否将文本转换为小写(部分分词器如 BasicTokenizer 支持此参数)。 | tokenizer.tokenize("Hello", lower_case=True) → ["hello"] |
keep_whitespace | bool | 可选 | False | 是否保留分词后的空格(如保留标点符号前后的空格)。 | tokenizer.tokenize("A , B", keep_whitespace=True) → ["A", " , ", "B"] |
normalize_form | str | 可选 | None | 指定 Unicode 规范化形式(如 NFD 、NFC ),用于处理变音符号。 | tokenizer.tokenize("café", normalize_form="NFD") → ["c", "a", "f", "e", "́"] |
preserve_unused_token | bool | 可选 | True | 是否保留未使用的特殊标记(如 [UNK] ),避免拆分预定义的特殊标记。 | tokenizer.tokenize("[UNK]", preserve_unused_token=True) → ["[UNK]"] |
max_length | int | 可选 | None | 限制分词后的最大长度(超出部分可能截断)。 | tokenizer.tokenize(text, max_length=512) → 截断至前 512 个 token |
truncation | bool 或 str | 可选 | False | 截断策略(如 "longest_first" 、"only_first" ),控制超长文本的截断方式。 | tokenizer.tokenize(text, truncation="longest_first") |
tokenizer.encode():将文本转换为模型可接受的数字序列(Token IDs),包含以下步骤:
- 分词:按分词算法(如 BPE、WordPiece)将文本拆分为 tokens。
- 映射为 ID:根据词表将每个 token 转换为对应的整数索引。
- 添加特殊标记:默认在开头添加
[CLS]
,结尾添加[SEP]
(若启用add_special_tokens=True
)
默认在句子首尾加上分隔符[CLS]、[SEP],需要模型输入格式的数值序列
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例/参考来源 |
---|---|---|---|---|---|
text | str | 必需 | 无 | 输入文本,支持单条字符串或批量处理(部分实现支持列表输入)。 | tokenizer.encode("Hello world!") → [101, 19082, 1362, 102] (BERT 示例) |
text_pair | str | 可选 | None | 第二个输入文本(用于处理句子对任务,如问答或文本相似度)。 | tokenizer.encode(text="句子1", text_pair="句子2") → 拼接后编码 |
add_special_tokens | bool | 可选 | True | 是否添加特殊标记(如 [CLS] 、[SEP] ),用于模型输入格式化。 | encode(text, add_special_tokens=False) → 仅返回原始分词 ID |
truncation | bool 或 str | 可选 | False | 截断策略:True 或 "longest_first" 表示按最大长度截断,False 表示不截断。 | encode(text, truncation=True, max_length=512) → 截断超长部分 |
padding | bool 或 str | 可选 | False | 填充策略:True 或 "max_length" 填充至 max_length ,False 表示不填充。 | encode(text, padding="max_length", max_length=128) → 填充至 128 tokens |
max_length | int | 可选 | 模型默认值 | 指定编码后的最大长度(含特殊标记)。若未设置,使用模型支持的最大长度(如 BERT 为 512)。 | encode(text, max_length=64) → 输出长度不超过 64 |
return_tensors | str | 可选 | None | 返回张量类型:"pt" (PyTorch)、"tf" (TensorFlow)、"np" (NumPy),默认返回列表。 | encode(text, return_tensors="pt") → 输出 PyTorch 张量 |
return_attention_mask | bool | 可选 | False | 是否返回注意力掩码(1 表示有效 token,0 表示填充部分)。需配合 padding=True 使用。 | encode(text, padding=True, return_attention_mask=True) → 返回掩码列表 |
tokenizer.encode_plus():返回一个字典,包含以下键值对:
input_ids
:Token ID 序列,包含特殊标记(如[CLS]
、[SEP]
)。token_type_ids
:区分句子对的类型(如0
表示第一句,1
表示第二句)。attention_mask
:标识有效 token 位置(填充部分为0
)。- 其他可选键:如
overflowing_tokens
(溢出 token)、special_tokens_mask
(特殊标记掩码)等。
参数名 | 类型 | 是否可选 | 默认值 | 描述 | 示例/参考来源 |
---|---|---|---|---|---|
text | str 或 List[str] | 必需 | 无 | 输入文本或文本列表,支持单条字符串或批量处理(部分实现支持列表输入)。 | encode_plus("Hello world!") → 生成包含 input_ids 的字典 |
text_pair | str 或 List[str] | 可选 | None | 第二个输入文本(用于处理句子对任务,如问答或文本相似度)。 | encode_plus(text="句子1", text_pair="句子2") → 拼接后编码 |
add_special_tokens | bool | 可选 | True | 是否添加特殊标记(如 [CLS] 、[SEP] ),用于模型输入格式化。 | add_special_tokens=False → 仅返回原始分词 ID |
max_length | int | 可选 | 模型默认值 | 指定编码后的最大长度(含特殊标记)。若未设置,使用模型支持的最大长度(如 BERT 为 512)。 | max_length=128 → 截断或填充至 128 tokens |
padding | bool 或 str | 可选 | False | 填充策略:True 或 "max_length" 填充至 max_length ,False 表示不填充。 | padding="max_length" → 填充至指定长度 |
truncation | bool 或 str | 可选 | False | 截断策略:True 或 "longest_first" 表示按最大长度截断,False 表示不截断。 | truncation="longest_first" → 优先截断较长句子 |
return_tensors | str | 可选 | None | 返回张量类型:"pt" (PyTorch)、"tf" (TensorFlow)、"np" (NumPy),默认返回列表。 | return_tensors="pt" → 输出 PyTorch 张量 |
return_attention_mask | bool | 可选 | True | 是否返回注意力掩码(1 表示有效 token,0 表示填充部分)。需配合 padding=True 使用。 | return_attention_mask=True → 输出掩码列表 |
return_token_type_ids | bool | 可选 | True | 是否返回 token 类型 ID(如 0 表示第一句,1 表示第二句)。 | return_token_type_ids=True → 区分句子对 |
return_overflowing_tokens | bool | 可选 | False | 是否返回因截断而溢出的 token(需配合 stride 参数使用)。 | return_overflowing_tokens=True → 返回溢出 token 列表 |
import torch
import math
import numpy as np
from transformers import BertModel
from transformers import BertTokenizer
'''
关于transformers自带的序列化工具
模型文件下载 https://huggingface.co/models
'''
# bert = BertModel.from_pretrained(r"F:\Desktop\work_space\pretrain_models\bert-base-chinese", return_dict=False)
tokenizer = BertTokenizer.from_pretrained(r"F:\人工智能NLP\\NLP资料\week6 语言模型//bert-base-chinese")
string = "咱呀么老百姓今儿个真高兴"
#分字
tokens = tokenizer.tokenize(string)
print("分字:", tokens)
#编码,前后自动添加了[cls]和[sep],形式:[cls] string [sep]
encoding = tokenizer.encode(string)
print("编码:", encoding)
#文本对编码, 形式[cls] string1 [sep] string2 [sep]
string1 = "今天天气真不错"
string2 = "明天天气怎么样"
encoding = tokenizer.encode(string1, string2)
print("文本对编码:", encoding)
#同时输出attention_mask和token_type编码
encoding = tokenizer.encode_plus(string1, string2)
print("全部编码:", encoding)