人类发明后悔,来证明拥有的珍贵
------ 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.代码实现 **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"` | **state_dict():** PyTorch 中 `nn.Module` 类的一个方法,用于返回一个包含模型所有可学习参数的字典。字典的键是参数的名称,值是对应的参数张量。 **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]]`(二维数组) | **keys():**返回字典的所有键的视图对象(动态更新,随字典变化而变化) **np.exp():**计算输入 `x` 中每个元素的 e 次幂(即 e\^x),其中 e ≈ 2.71828。该函数广泛应用于科学计算、统计学(如概率密度函数)和机器学习(如 Softmax 函数) | **参数名称** | **类型** | **是否必填** | **说明** | | |------------|--------|----------|-------------------------------------------|---| | `x` | 数组/标量 | 必填 | 输入值,支持数组或标量。Numpy会对数组中的每个元素计算e的指数幂(e\^x)。 | | | `out` | 数组 | 可选 | 指定输出数组,结果将存储在此数组中。 | | | `where` | 布尔数组 | 可选 | 指定计算位置,`True`表示计算,`False`跳过。 | | | `dtype` | 数据类型 | 可选 | 指定输出数组的数据类型。 | | | `keepdims` | 布尔值 | 可选 | 是否保持原数组的维度(仅对多维数组有效)。 | **np.mean():** NumPy 库中用于计算数组或矩阵中元素的平均值的函数。它支持多维数组操作,并可通过参数灵活指定计算方向和输出格式。 | **参数名称** | **类型** | **是否必填** | **说明** | | |------------|--------|----------|---------------------------------------------------|---| | `a` | 数组 | 必填 | 输入的数组或矩阵。 | | | `axis` | 整数/元组 | 可选 | 指定计算轴:`None`(计算所有元素)、`0`(压缩行计算列均值)、`1`(压缩列计算行均值)。 | | | `dtype` | 数据类型 | 可选 | 指定输出数据类型。 | | | `out` | 数组 | 可选 | 指定输出数组,结果将存储在此数组中。 | | | `keepdims` | 布尔值 | 可选 | 是否保持原数组的维度(仅对多维数组有效)。 | **np.sum():**计算输入数组 `a` 中所有元素的总和,或沿指定轴(`axis`)的元素和。适用于科学计算、数据统计等场景 | **参数名称** | **类型** | **是否必填** | **说明** | | |----------|------------|----------|-------------|---| | `a` | 数组/标量 | 必填 | 需要求和的数组或标量。 | | | `axis` | 整数/元组/None | 可选 | 指定求和轴: | **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 | 存储结果的数组 | **append():**向列表末尾添加元素(直接修改原列表) | 参数名 | 类型 | 是否可选 | 默认值 | 描述 | |-----------|------|------|-----|--------| | `element` | 任意类型 | 必需 | 无 | 要添加的元素 | **list():**将可迭代对象(如元组、字符串)转换为列表 | 参数名 | 类型 | 是否可选 | 默认值 | 描述 | |------------|-------|------|-----|-----------| | `iterable` | 可迭代对象 | 必需 | 无 | 输入数据(如元组) | **range():**生成整数序列,常用于循环 | 参数名 | 类型 | 是否可选 | 默认值 | 描述 | |---------|-----|------|-----|-----------| | `start` | int | 可选 | 0 | 起始值(包含) | | `stop` | int | 必需 | 无 | 结束值(不包含) | | `step` | int | 可选 | 1 | 步长(正数或负数) | **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.mean():**计算数组元素的平均值 | 参数名 | 类型 | 是否可选 | 默认值 | 描述 | |------------|------------|------|-------|--------------------| | `a` | array_like | 必需 | 无 | 输入数组 | | `axis` | int/tuple | 可选 | None | 沿指定轴计算(如 `axis=0`) | | `dtype` | data-type | 可选 | None | 输出数据类型 | | `keepdims` | bool | 可选 | False | 是否保持维度 | ```python 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 列表 | ```python 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) ``` 