BERT模型的实现

本文用 pytorch 实现一个BERT模型。
食用方法:

  1. 直接下载完整实现, 在自己本地跑一遍,保证不报错。
  2. 先完成数据预处理阶段(1-4)的代码阅读,然后按照如下关键点的描述完成代码的实现。
  3. 自己看着代码手写后续部分的实现。
  4. 对后续部分进行debug,熟悉每一步的代码。
  5. 恭喜你已经掌握了BERT.
# 1. 定义 BERT 模型的参数,包括最大句子长度、批次大小、最大预测 token 数等。
# 2. 预处理文本,将文本去除标点后分割成句子列表,创建词表和词典映射。
# 3. 将文本转化为数字化 token 列表,为模型输入做准备。
# 4. 定义 make_batch 函数,用于生成批次数据,包括 input_ids, segment_ids, masked_tokens, masked_pos 和 isNext 标签。
# 5. 构建 BERT 模型的结构,包括 Embedding 层、Encoder 层、多头注意力机制等。
# 6. 定义损失函数 (CrossEntropyLoss) 和优化器 (Adam)。
# 7. 通过循环训练模型:
#    - 清零优化器梯度。
#    - 将批次数据送入 BERT 模型,获取输出 logits。
#    - 计算语言模型任务 (MLM) 和句子分类任务 (NSP) 的损失。
#    - 合并损失并反向传播,更新模型参数。
#    - 每隔固定 epoch 打印损失值。

1 架构

在实现代码之前我们先简单的了解一下BERT模型的整体架构,以便于后续的学习。
BERT 模型主要由三层结构构成,输入层,编码器层和输出层。

  1. 输入层:将原始文本处理成模型可以接受的嵌入表示,包括词嵌入、位置嵌入和段落嵌入。
  2. 编码器层:通过堆叠的 Transformer 模块捕获双向上下文信息。
  3. 输出层:根据下游任务,选择合适的特征用于分类、标注或句子相关性判断。

在这里插入图片描述

2 代码复现

2.1 定义模型参数

定义 BERT 模型的参数,包括最大句子长度、批次大小、最大预测 token 数等

if __name__ == '__main__':
    maxlen = 30  # 句子最大长度
    batch_size = 6  # 每一个batch有多少个句子进行训练
    max_pred = 5  # 输入的一个句子中最多可以有多少个被mask的词
    n_layers = 6  # 有多少个编码器层
    n_heads = 12  # transformer 的多头数目
    d_model = 768  # Embedding size
    d_ff = 3072  # FeedForward 层的维度
    d_k = d_v = 64  # K V 的维度
    n_segements = 2  # Next sentence predict 任务

2.2 预处理文本

预处理文本,将文本去除标点后分割成句子列表,创建词表和词典映射。

# 原始文本
text = (
    "Hi, what are you doing? I'm planning my weekend.\n"
    "Oh, sounds fun! I might join you if that’s okay.\n"
    "Of course! We could also invite Sarah. She loves hiking.\n"
    "Perfect. I’ll bring snacks and drinks for everyone.\n"
    "Great! We’ll start early morning to avoid the heat.\n"
    "Sure, I'll text Sarah and let her know.\n"
    "Thanks for organizing this. It’s going to be awesome.\n"
    "No problem at all. Let’s finalize the time tonight.\n"
    "Cool! Should we also plan something for the evening?\n"
    "That’s a great idea! Maybe a movie or dinner?\n"
    "Let’s do both. I know a good Italian place.\n"
    "Sounds amazing. I’ll leave the restaurant booking to you.\n"
    "Deal! I’ll make sure everything is ready for the weekend.\n"
)

# 使用正则表达式去除标点符号(如.,!?-),并将文本转为小写,然后按行分割成句子列表
sentences = re.sub("[.,!?\\-]", '', text.lower()).split('\n')

# 使用空格分割所有句子中的单词,并创建一个唯一单词的集合
word_list = list(set(" ".join(sentences).split()))  # `set` 去重,`list` 转为列表

# 初始化词典,加入特殊标记
word_dict = {
   
   '[PAD]': 0, '[CLS]': 1, '[SEP]': 2, '[MASK]': 3}

# 遍历单词列表,依次为每个单词分配一个唯一的索引,并更新到词典中
for i, word in enumerate(word_list):
    word_dict[word] = i + 4  # 特殊标记占据前4个索引,单词索引从4开始

# 创建一个反向映射字典,将索引映射回单词
number_dict = {
   
   i: word for i, word in enumerate(word_list)}

# 计算词汇表的大小
vocab_size = len(word_dict)

2.3 文本序列化

将文本转化为数字化 token 列表,为模型输入做准备。

token_list = []
for i, sentence in enumerate(sentences):
    ids = [word_dict[word] for word in sentence.split()]
    token_list.append(ids)

2.4 定义make_batch 函数

定义 make_batch 函数,用于生成批次数据,包括 input_ids, segment_ids, masked_tokens, masked_pos 和 isNext 标签。

def make_batch():
    batch = []  # 初始化批次列表,用于存储生成的样本
    positive_samples = negative_samples = 0  # 初始化正样本和负样本计数

    # 循环直到正样本和负样本分别达到 batch_size / 2
    while positive_samples != batch_size / 2 or negative_samples != batch_size / 2:
        # 随机选择两个句子索引
        sentence_index_a, sentence_index_b = randrange(len(sentences)), randrange(len(sentences))

        # 根据索引获取对应的句子(已转为 token 序列)
        sentence_a = token_list[sentence_index_a]
        sentence_b = token_list[sentence_index_b]

        # 构造输入序列,包含特殊标记 [CLS], [SEP]
        input_ids = [word_dict['[CLS]']] + sentence_a + [word_dict['[SEP]']] + sentence_b + [word_dict['[SEP]']]

        # 构造分段 ID(0 表示句子 A,1 表示句子 B)
        segements_ids = [0] * (1 + len(sentence_a) + 1) + [1] * (len(sentence_b) + 1)

        # 确定需要被掩码的 token 数量,取 15% 的输入序列长度,至少 1 个,最多 `max_pred`
        n_mask = min(max_pred, int(max(1, round(len(input_ids) * 0.15))))

        # 找出可以被掩码的位置(排除 [CLS] 和 [SEP])
        can_mask_pos = [i for i, id in enumerate(input_ids) if
                        id != word_dict['[CLS]'] and id != word_dict['[SEP]']]

        # 随机打乱可掩码位置
        shuffle(can_mask_pos)

        # 初始化存储被掩码的 token ID 和位置的列表
        masked_ids, masked_pos = [], []

        # 遍历需要掩码的位置
        for pos in can_mask_pos[:n_mask]:
            masked_pos.append(pos)  # 记录被掩码的位置
            masked_ids.append(input_ids[pos])  # 记录被掩码的原始 token ID

            if random() < 0.8:  # 80% 的概率用 [MASK] 替换
                input_ids[pos] = word_dict['[MASK]']
            elif random() > 0.9:  # 10% 的概率用随机 token 替换
                input_ids[pos] = randint(0, vocab_size - 1)
            # 其余 10% 的概率保持原样(不替换)

        # 计算需要填充的长度,确保 `input_ids` 的长度与 `maxlen` 一致
        n_pad = maxlen - len(input_ids)
        input_ids.extend([0] * n_pad)  # 填充 0([PAD])
        segements_ids.extend([0] * n_pad)  # 分段 ID 也填充 0

        # 如果被掩码的 token 数少于 `max_pred`,则填充到 `max_pred` 长度
        if max_pred > n_mask:
            n_pad = max_pred - n_mask
            masked_pos.extend([0] * n_pad)  # 填充 0
            masked_ids.extend([0] * n_pad)  # 填充 0

        # 判断样本是否为正样本(句子 B 是句子 A 的后续)
        if sentence_index_a + 1 == sentence_index_b and positive_samples < batch_size / 2:
            batch.append([input_ids, segements_ids, masked_ids, masked_pos, True])  # 添加正样本
            positive_samples += 1  # 更新正样本计数

        # 否则为负样本(句子 B 不是句子 A 的后续)
        elif sentence_index_a + 1 != sentence_index_b and negative_samples < batch_size / 2:
            batch.append([input_ids, segements_ids, masked_ids, masked_pos, False])  # 添加负样本
            negative_samples += 1  # 更新负样本计数

    return batch  # 返回生成的批次

2.5 构建 BERT 模型

构建 BERT 模型的结构,包括 Embedding 层、Encoder 层、多头注意力机制等。

2.5.1 Embedding 层

在这里插入图片描述

  1. Token Embedding(词嵌入):将每个单词或子词映射为固定长度的向量表示。
  2. Segment Embedding(段落嵌入):用来区分句子 A 和句子 B,支持句子对任务(如问答、句子匹配)。
  3. Position Embedding(位置嵌入):引入序列信息,标识每个词在句子中的位置。

注意💡
词嵌入矩阵词嵌入向量的概念。

词嵌入矩阵是一个二维数组,用于存储每个词汇在向量空间中的表示,在前向传播时,根据输入的 token 索引从中查找对应的词嵌入向量。

# vocab_size 是词汇表大小(词汇总数)。
# embedding_dim 是每个词向量的维度。
self.tok_embed = nn.Embedding(vocab_size, d_model)

词嵌入向量是从词嵌入矩阵中查找出来的单个词的表示,是词嵌入矩阵的一行。

# input_ids 存储着每一个单词对应词典 word_dict 中的 id,tok_embed:通过 input_ids 查询词汇嵌入矩阵来获取词嵌入向量
self.tok_embed(input_ids)
# Embedding
# 定义 BERT 嵌入层类,继承自 nn.Module
class Embedding(nn.Module):
    def __init__(self):
        super(Embedding, self).__init__()  # 调用父类的构造方法

        # 词汇嵌入矩阵
        # 这里的 vocab_size 是词汇表大小,d_model 是嵌入向量的维度
        # 嵌入矩阵维护一个词汇表,每个词通过索引映射到对应的 d_model 维度嵌入向量
        self.tok_embed = nn.Embedding(vocab_size, d_model)

        # 位置嵌入矩阵
        # 这里的 maxlen 是句子的最大长度,d_model 是嵌入向量的维度
        # 嵌入矩阵维护每个位置(如第 0 位、第 1 位等)的位置信息
        self.pos_embed = nn.Embedding(maxlen, d_model)

        # 分段嵌入矩阵
        # 这里的 n_segements 是分段类别数,通常为 2,分别表示句子 A 和句子 B
        # 嵌入矩阵用来区分每个 token 属于哪个句子段
        self.seg_embed = nn
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值