BERT(Bidirectional Encoder Representations from Transformers)是一种基于 Transformer 的深度学习模型,由 Google 于 2018 年提出。其核心优势在于使用双向上下文来预训练模型,能够更好地理解和捕捉句子中的语境信息,从而在多种自然语言处理任务中取得了卓越的性能。与之前的模型不同,BERT 表示在所有层中都共同基于左右上下文,这使得它能够在处理输入序列时同时考虑到所有位置的上下文信息。
BERT 模型结构
Input: [CLS] token1 [SEP] token2 [PAD] ... [PAD] [PAD] [SEP]
↓
Embedding Layer:
Token Embeddings Segment Embeddings Position Embeddings
↓ ↓ ↓
+-----------------------------------+-----------------------+
| |
↓ ↓
Transformer Encoder Layer 1:
Multi-Head Self-Attention → Add & Norm → Feed-Forward → Add & Norm |
↓ ↓
Transformer Encoder Layer 2:
Multi-Head Self-Attention → Add & Norm → Feed-Forward → Add & Norm |
↓ ↓
... |
↓ ↓
Transformer Encoder Layer N:
Multi-Head Self-Attention → Add & Norm → Feed-Forward → Add & Norm
↓
Output: Hidden states for each token
↓
Task-specific layers (e.g., classification, QA)
-
输入层: 输入 Tokens 经过预处理后,将转化为三个嵌入向量:Token Embeddings(词元嵌入)、Segment Embeddings(句子嵌入)和 Position Embeddings(位置嵌入)。Token Embeddings 是将每个单词映射到一个固定维度的向量;Segment Embeddings 用于区分两个句子;Position Embeddings 是单词在句子中的位置信息,通过位置嵌入让模型感知到单词的顺序。
-
输入维度: BERT 的输入是三个嵌入向量的和, 分别是 Token Embeddings, Segment Embeddings, 和 Position Embeddings.
-
Token Embeddings: 将文本中的每个词元映射到一个固定维度的向量. 假设词表的大小是 $V$ (BERT-base 的词表大小为 28996), 每个 Token 的索引可以表示为一个 one-hot 向量, 维度为 $1 \times V$. 然后将这个 one-hot 向量乘以一个可训练的嵌入矩阵 (维度为 $V \times H$, $H$ 为隐藏层大小, 例如 BERT-base 中 $H=768$).
-
Segment Embeddings: 用于区分两个句子, 如在一个包含两个句子的任务中, 第一个句子标识为 0, 第二个句子标识为 1. 对于长度 $L$ 的输入序列, Segment Embeddings 的维度是 $L \times S$, 其中 $S$ 是 Segment Embeddings 的维度, 通常 $S = H$.
-
Position Embeddings: 用于表示每个词元在句子中的位置信息. 对于长度为 $L$ 的输入序列, Position Embedding 的维度是 $L \times P$, 其中 $P$ 是 Position Embeddings 的维度, 通常 $P = H$.
-
-
编码器层: Transformer 编码器是 BERT 模型的核心组成部分,它由多个相同的层堆叠而成。每一层包含两个主要的子层:多头自注意力机制(Multi-Head Self-Attention)和前馈神经网络(Feed-Forward Neural Network)。
-
多头注意力模块: 该模块用于捕捉输入序列中不同单词之间的关系。它将输入向量变换为查询(Query)、键(Key)和值(Value)向量,然后计算查询和键之间的点积注意力,得到每个单词与其它单词的相关性权重,再用这些权重对值向量加权求和,得到输出。多头注意力机制是指将输入向量映射到多个不同的查询、键和值空间,分别计算注意力,然后将结果拼接起来,再通过一个线性变换得到最终输出,这样可以使模型能够关注到不同位置之间的不同关系。
-
前馈神经网络: 该子层对每个位置的向量进行独立的非线性变换。它由两个线性变换中间夹一层激活函数(通常是 ReLU)组成,能够增加模型的表达能力,使模型能够学习到更复杂的特征。
-
-
输出层: Transformer 编码器的输出向量可以用于各种下游任务。例如在句子分类任务中,通常将第一个单词([CLS] 标记)的输出向量作为整个句子的表示,然后在其后面添加一个分类层进行分类。
-
输出维度: BERT 的输出维度是 Transformer 编码器最后一层的隐藏状态向量.
-
每个位置的隐藏状态向量: 对于输入序列中的每个位置, 输出一个维度为 $H$ 的向量, 对于整个输入序列 (长度为 $L$), 输出的隐藏状态向量集合的维度是 $L \times H$. 例如, 输入序列长度为 128, 隐藏层大小 768, 则输出维度是 $128 \times 768$.
-
[CLS] 位置的隐藏状态向量: 在句子分类任务中, 通常将输入序列的第一个 token (即 [CLS] 标记) 对应的隐藏状态向量作为整个句子的表示. 这个向量的维度是 $1 \times H$. 例如, 隐藏层大小为 768, 则 [CLS] 位置的隐藏状态向量维度为 $1 \times 768$.
-
预训练任务输出维度: 对于掩码语言模型 (MLM) 任务, 模型需要预测每个被掩盖的 Token, 因此输出是每个位置上词汇表大小 $V$ 的概率分布, 维度是 $L \times V$.
-
BERT 预训练方法
BERT 的训练采用无监督学习方法,通过两个主要任务来学习词汇表示和上下文信息:掩码语言模型(Masked Language Model,MLM)和下一句预测(Next Sentence Prediction,NSP)。
-
MLM, 掩码语言模型: 在训练过程中,随机遮蔽输入文本中的一些 token,然后让模型预测这些被遮蔽的 token。具体步骤为:对于输入文本中的每个句子,随机选择 15% 的 token 进行遮蔽,其中 80% 的被遮蔽 token 被替换成一个特殊的 [MASK] 标记,10% 的 token 被替换成其他随机的 token,而剩下的 10% 则保持原样。模型的目标是根据上下文预测被遮蔽的 token,在训练期间,通过比较模型输出和实际被遮蔽的 token 来计算损失,通过反向传播和优化算法来更新模型参数。
-
WWM, 全词掩码训练方法: 全词掩码(Whole Word Masking,wwm)策略是针对中文等语言特性进行改进的一种掩码方法。在中文条件下,由于 WordPiece tokenizer 不会将词语拆分成小片段,因此采用传统的中文分词工具将文本分割成多个词语,然后以整个词语为单位进行掩码,而不是随机选择 WordPiece tokens 来掩码。这样能更符合语言习惯,使模型在预训练阶段就聚焦于对整个词语语义的学习和理解,为后续的微调任务提供更好的基础。
-
NSP, 下一句预测: 该任务旨在让模型学习理解句子之间的关系。在训练过程中,随机选择一些相邻的句子对和两个不相邻的句子对,模型需要判断哪些句子对是连续的、有逻辑关系的,而哪些句子对是不相关的。通过这个任务,BERT 能够学习捕捉句子之间的语义关系,从而提高对文本中句子级别信息的理解能力。
使用 BERT 对段落进行编码
在 BERT 中,输入序列的第一个 token 是一个特殊的 [CLS] 标记,其对应的输出向量被认为包含了整个输入序列的综合信息,因此常被用作文本嵌入表示。具体步骤如下:
-
输入文本处理: 将待嵌入的文本进行分词, 添加 [CLS] [PAD] [SEP] 等标记, 并转为张量形式.
-
模型前向传播: 将处理后的输入序列输入到 BERT 中进行前向传播计算, 模型会输出每个 Token 对应的隐藏状态向量.
-
获取嵌入向量: 取出输出序列中, [CLS] 标记位置的向量, 作为该文本的嵌入表示, [CLS] 标记经过 BERT 的多层编码器处理, 其融合了整个输入序列的总信息, 能够较好地反映文本的语义特征, 可用于后续的文本分类等任务.
代码实现:
'''
由于 BERT 处理序列时, 是从右到左依次处理, 后进入词元与已处理词元计算自注意力, 所以可以认为, 序列的开头第一个词元与整个序列都计算了自注意力, 其向量表示隐含了完整序列的语义信息.
基于以上原理, 我设计了如下的短文本嵌入算法:
该算法先将输入文本进行分词, 得到词元 (Token) 序列, 当 Token 序列长度超过 512 则无法输入到 BERT, 所以我们需要进行截断分块, 超出 512 长度的 Token 会作为一个新的 512 维序列进行处理.
当新的 512 维序列无法被原 Token 填满时, 会进行 0 填充, 同时, 为了确保填充的 Token 不被作为有效信息的一部分参与计算, 填充部分会被添加注意力掩码, 确保其在注意力计算中的权重为负无穷.
最后, 我们计算得到多个 512 维序列的首个 Token 的嵌入表示 (首个 Token 通常是 [CLS] 代表序列的开始, BERT 输出维度为 768) , 为了确保语义信息的独立性并尽可能保留更多语义细节, 我将所有嵌入按最后一个维度进行拼接, 形成一个倍长向量, 以表示短文本的语义信息.
'''
import math
import torch
from torch import nn
from transformers import BertTokenizer, BertModel
from lotc import __ROOT__
CACHE_PATH = f'{__ROOT__}\\.cache'
DEFAULT_BERT = 'bert-base-uncased'
class BertEncoder(nn.Module):
def __init__(self, model_name_or_path: str = DEFAULT_BERT):
super().__init__()
self.tokenizer = BertTokenizer.from_pretrained(pretrained_model_name_or_path=model_name_or_path,
cache_dir=CACHE_PATH)
self.bert = BertModel.from_pretrained(pretrained_model_name_or_path=model_name_or_path,
cache_dir=CACHE_PATH)
def forward(self, input_ids, attention_mask: torch.Tensor) -> torch.Tensor:
def _encoder(_input_tup: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor:
return self.bert.forward(_input_tup[0], attention_mask=_input_tup[1]).last_hidden_state[:, 0, :]
num_chunks = math.ceil(input_ids.shape[-1] / 512)
chunks = chunk_results = []
for i in range(num_chunks):
start_idx = i * 512
end_idx = min(start_idx + 512, input_ids.shape[-1])
chunks.append((input_ids[:, start_idx: end_idx], attention_mask[:, start_idx: end_idx]))
ori_mode = self.bert.training
self.bert.eval()
with torch.no_grad():
chunk_results = [_encoder(x) for x in chunks]
self.bert.train(mode=ori_mode)
return torch.cat(chunk_results, dim=-1)
# noinspection PyUnresolvedReferences
def encode(self, text: str) -> torch.Tensor:
_input_ids = torch.tensor([self.tokenizer.encode(text)], dtype=torch.long)
_att_mask = torch.tensor([[1] * _input_ids.shape[-1]], dtype=torch.int)
return self.forward(_input_ids, _att_mask).squeeze()