怎样构建一个bert模型

371 阅读14分钟

1. 了解你的bert_example项目

pretrained_bert目录

从项目目录结构,可以看到,data目录下包含的是训练数据集、测试数据集、通过分类的值(0、1、2、3....)所对应的分类名称。

pretrained_bert目录中包含的是从huggingface中下载下来的预训练模型的一些配置以及模型的权重和偏好。

1.  pytorch_model.bin

文件是PyTorch保存模型时使用的一种常见文件格式,它保存了模型的状态字典(state dict)。state dict包含了模型所有的可学习参数(即权重和偏置等),不包含模型的计算图结构。

之所以使用state dict格式,是因为在PyTorch中,模型本身只是一个定义FORWARD计算的类,真正的参数是保存在参数这个字典里,解耦了模型结构和参数。

保存为pytorch_model.bin的好处:

● 文件体积小,只保存参数矩阵,更轻量化

● 加载速度更快

● 可以灵活传递给其他模型代码加载

● 可以方便地做模型压缩、量化等优化

2.  vocab.txt

vocab.txt文件通常用于保存NLP模型中的词汇表(vocabulary)。

vocab.txt的主要作用有:

1.  定义模型的词汇量

vocab.txt中每个词汇都对应一个数字索引,表示模型识别的全部词汇范围。这样模型输入输出都会用词汇的数字ID表示。

2.  实现文本数字化

使用vocab.txt可以将文本转为数字ID序列,供模型输入。一般使用预训练词向量的vocab,以初始化嵌入层。

3.  维护词汇频率信息

vocab.txt中词汇顺序往往按照在训练语料中的出现频率排序,频率高的词汇索引更小。

4.  作为模型输入的必要条件

大多NLP模型都要求输入必须按照vocab.txt数字化,这样才能对应嵌入层和其他参数矩阵。

5.  反查单词

通过vocab.txt可以根据词汇索引反查具体单词。

6.  与预训练词向量对应

如果模型使用的是BERT/GPT等预训练词向量,vocab.txt中词汇顺序应与之一致。

3.  config.json

config.json文件用于存储模型的配置信息,主要包含以下内容:

● model_type: 模型的类型,如bert、gpt2等。

● vocab_size: 词汇表大小。

● hidden_size: 隐层大小。

● num_hidden_layers: 编码器层数量。

● num_attention_heads: 注意力头数量。

● intermediate_size: Feed Forward网络隐层大小。

● hidden_act: 激活函数类型,如gelu、relu等。

● hidden_dropout_prob: 隐层dropout概率。

● attention_probs_dropout_prob: 注意力矩阵dropout概率。

● max_position_embeddings: 最大位置编码数。

● initializer_range: 权重初始化范围。

● layer_norm_eps: LayerNorm层epsilon常数。

● tokenizer_class: 对应的分词器类。

● pad_token_id: padding token的id。

● bos_token_id: sentence开始token的id。

● eos_token_id: sentence结束token的id。

data目录

在data项目结构下,关注3个文件,label.txt、test.txt、train.txt。

train.txt文件是训练模型时用到的投喂数据,用来训练模型。

test.txt文件则是训练完成后用来测试模型的数据。

label.txt用来将分类的值转换成对应的标签文本。

model目录

bert_model.pth则是训练完成后生成的模型,存放在model目录下。

主项目目录

1.1.  app.py文件

运行文件。两个作用:

1.  加载bert模型:

# 从预训练的BERT模型加载分词器
tokenizer = BertTokenizer.from_pretrained(config.pretrained_bert_dir)
# 从预训练的 BERT 模型中加载配置
# config.pretrained_bert_dir 预训练的 BERT 模型的路径
# num_labels 自己定义的标签数量,用于配置 BERT 模型的分类任务
bert_config = BertConfig.from_pretrained(config.pretrained_bert_dir, num_labels=config.num_labels)
# 从预训练的BERT模型加载文本分类模型
model = BertForSequenceClassification.from_pretrained(
        os.path.join(config.pretrained_bert_dir, "pytorch_model.bin"),
        config=bert_config
    )
# GPU 可以加速模型的训练和推理过程,因为它可以并行处理大量的矩阵运算。PyTorch 提供了 CUDA 支持,允许将张量和模型移动到 GPU 上进行计算 。如果 CUDA 可用(即有可用的 GPU),则选择
# "cuda",否则选择 "cpu"
torch_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 将模型移动到指定的设备上进行计算
model.to(torch_device)
# 加载保存的模型参数
model.load_state_dict(torch.load(config.saved_model))
# 在深度学习中,模型通常有两种模式:训练模式和评估模式。在训练模式下,模型会启用一些特定的行为,如启用 Dropout 层和批归一化层的训练行为。而在评估模式下,模型会关闭这些训练行为,以便进行推理或评估,确保结果的一致性和可重复性。
model.eval()

2.  定义一个访问模型预测的接口

@app.route('/predict', methods=['POST'])
def predict_number():
    body = request.get_json()
    sentence = body['content']
    if not sentence:
        return 'error'
    # 将 sentence 作为输入进行分词和编码处理
    # max_length 参数指定了句子的最大长度
    # truncation 参数指定了截断策略,"longest_first" 表示在超出 max_length 的情况下,从句子的开头截断
    # return_tensors 参数设置为 "pt",表示返回的结果将以PyTorch的张量形式表示。
    inputs = tokenizer(
        sentence,
        max_length=config.max_seq_len,
        truncation="longest_first",
        return_tensors="pt")
    # 将 inputs 张量移动到指定的设备上
    inputs = inputs.to(torch_device)
    # 上下文管理器用于关闭梯度计算,以减少内存消耗和加速推理过程
    with torch.no_grad():
        # 接收模型的输出结果
        outputs = model(**inputs)
        # logins 表示模型的输出中的预测标签得分
        logins = outputs[0]
        # 使用 torch.max 方法获得预测标签的索引,使用 .tolist() 方法将其转换为 Python 列表
        label = torch.max(logins.data, 1)[1].tolist()
        res = {
            'content': sentence,
            'category': label[0],
            'category_name': config.label_list[label[0]]
        }
        return jsonify(res)


if __name__ == '__main__':
    app.debug = True
    app.run(host='127.0.0.1', port=5000)

1.2.  config.py

一些文件路径、训练的次数等等一些配置参数

1.3.  main.py

1.  设置随机种子:

设置随机种子作用:

● 可复现性:在实验和模型训练中,使用相同的随机种子可以确保每次运行时得到相同的随机结果。这对于调试、验证和结果的比较非常重要。

● 结果一致性:在模型训练中,随机初始化参数和随机采样数据可能会对最终的训练结果产生影响。通过设置随机种子,可以使得每次运行时的随机性保持一致,从而确保在相同条件下得到一致的训练结果。

● 模型比较:在比较不同模型或不同超参数配置时,使用相同的随机种子可以消除随机性对结果的影响,使得比较更加准确和可靠。

● 调试和问题排查:当遇到问题或错误时,设置随机种子可以使程序的随机部分变得可重现,帮助我们更好地定位和解决问题。

def set_seed(seed):
    # NumPy 是一个用于科学计算和数据处理的强大库,提供了高性能的多维数组对象和丰富的数学函数
    # 设置 NumPy 的随机种子
    np.random.seed(seed)
    # 设置 PyTorch 的随机种子
    torch.manual_seed(seed)
    # 设置所有 CUDA 设备的随机种子
    torch.cuda.manual_seed_all(seed)
    # 设置使用 cudnn 时的随机种子,确保结果的可重复性
    torch.backends.cudnn.deterministic = True

2.  训练模型

  set_seed(args.seed)
    # Config类来管理模型的配置参
    config = Config()
    # 加载预训练的BERT分词器
    tokenizer = BertTokenizer.from_pretrained(args.pretrained_bert_dir)
    # 加载预训练的BERT模型配置
    # args.pretrained_bert_dir:用于指定要加载的预训练BERT模型的目录路径。
    # num_labels 用于指定模型的类别数量
    bert_config = BertConfig.from_pretrained(args.pretrained_bert_dir, num_labels=config.num_labels)
    # 加载预训练的BERT分类模型
    model = BertForSequenceClassification.from_pretrained(
        os.path.join(args.pretrained_bert_dir, "pytorch_model.bin"),
        config=bert_config
    )
    # 将模型移动到指定的设备上进行计算
    model.to(config.device)

    if args.mode == "train":
        print("loading data...")
        start_time = time.time()
        # train数据处理器对象
        # config.train_file:训练数据文件的路径
        # config.device:指定设备
        # tokenizer:用于将文本转换为模型可接受的输入表示的分词器
        # config.batch_size:每个训练批次中的样本数量
        # config.max_seq_len:输入序列的最大长度限制
        # rgs.seed:可选的随机种子,用于控制随机性
        train_iterator = DataProcessor(config.train_file, config.device, tokenizer, config.batch_size,
                                       config.max_seq_len, args.seed)
        # test数据处理器对象
        dev_iterator = DataProcessor(config.test_file, config.device, tokenizer, config.batch_size, config.max_seq_len,
                                     args.seed)
        time_dif = get_time_dif(start_time)
        print("time usage:", time_dif)

        # train
        train(model, config, train_iterator, dev_iterator)

3.  对模型进行测试验证

    elif args.mode == "demo":
        model.load_state_dict(torch.load(config.saved_model))
        model.eval()
        while True:
            sentence = input("请输入文本:\n")
            inputs = tokenizer(
                sentence,
                max_length=config.max_seq_len,
                truncation="longest_first",
                return_tensors="pt")
            inputs = inputs.to(config.device)
            with torch.no_grad():
                outputs = model(**inputs)
                print(outputs)
                logins = outputs[0]
                print(logins)
                print(torch.max(logins.data, 1))
                label = torch.max(logins.data, 1)[1].tolist()
                print(label)
                print("分类结果:" + config.label_list[label[0]])
            flag = str(input("continue? (y/n):"))
            if flag == "Y" or flag == "y":
                continue
            else:
                break

1.4.  train.py

就是main文件中,对模型的训练和测试的具体实现方法

def train(model, config, train_iterator, test_iterator):
    # 开始训练
    model.train()
    start_time = time.time()
    # 列表包含了不需要进行权重衰减的参数的名称
    no_decay = ["bias", "LayerNorm.bias", "LayerNorm.weight"]
    param_optimizer = model.named_parameters()
    optimizer_grouped_parameters = [
        {"params": [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)], 'weight_decay': config.weight_decay},
        {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
    ]

    # 训练总数:训练数据集的总批次数乘以训练的总轮数
    t_total = len(train_iterator) * config.num_epochs
    # AdamW 是一种优化器算法,用于执行参数更新。它接受 optimizer_grouped_parameters 和学习率 config.learning_rate 作为参数
    optimizer = AdamW(optimizer_grouped_parameters, lr=config.learning_rate)
    # 学习率调度器 用于在训练的早期阶段进行学习率的热身(warm-up)和线性衰减。它接受优化器(optimizer)、热身步数(num_warmup_steps)和总训练步数(num_training_steps)作为参
    scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=config.warmup_steps, num_training_steps=t_total)

    total_batch = 0
    last_improve = 0
    break_flag = False
    # 表示当前模型在验证集上的最佳损失值。它被初始化为正无穷大,用于保存训练过程中的最佳模型
    best_dev_loss = float('inf')
    # 进行训练循环
    for epoch in range(config.num_epochs):
        print("Epoch [{}/{}]".format(epoch + 1, config.num_epochs))
        # 获取一个训练批次数据 batch 和对应的标签 labels
        for _, (batch, labels) in enumerate(train_iterator):

            outputs = model(
                # input_ids(输入的标记化文本
                input_ids=batch["input_ids"],
                # attention_mask(用于指示模型关注哪些标记)
                attention_mask=batch["attention_mask"],
                # token_type_ids(用于区分不同句子或序列)
                token_type_ids=batch["token_type_ids"],
                # labels 参数用于指定模型训练时的目标标签
                labels=labels)
            # 损失值
            loss = outputs[0]
            # 模型的输出得分
            logins = outputs[1]
            # 对损失值进行反向传播
            loss.backward()
            # 函数对模型的参数梯度进行裁剪,以防止梯度爆炸的问题
            # config.max_grad_norm 表示梯度的最大范数(norm)阈值,可以根据需要进行调整
            torch.nn.utils.clip_grad_norm_(model.parameters(), config.max_grad_norm)
            # 将根据计算得到的梯度值以及优化算法的规则,更新模型的参数
            optimizer.step()
            # 来更新学习率。学习率调度器根据预定义的策略或规则,调整优化器的学习率
            scheduler.step()
            # 此步骤用于将模型参数的梯度信息清零,以准备下一轮的反向传播。
            # 这些步骤通常在训练循环中的每个批次后执行,用于更新模型参数、调整学习率并准备下一轮的反向传播。
            optimizer.zero_grad()
            # total_batch 表示当前的批次数,config.log_batch 表示记录日志的批次间隔。
            # 当当前批次数能被 config.log_batch 整除时,训练完100条数据后,打印并进行一些操作。
            if total_batch % config.log_batch == 0:
                # 将 labels 数据移动到 CPU 上,并赋值给变量 true
                true = labels.data.cpu()
                # 使用 torch.max() 函数找到 logins.data 张量每行的最大值及其对应的索引,
                # 然后选择索引作为预测结果。最后,将预测结果移动到 CPU 上,并赋值给变量 pred
                pred = torch.max(logins.data, 1)[1].cpu()
                # 使用 metrics.accuracy_score() 函数计算真实标签 true 和预测结果 pred 之间的准确率。
                # metrics.accuracy_score() 是一个常用的评估指标函数,用于计算分类模型的准确率
                acc = metrics.accuracy_score(true, pred)
                # 评估模型在测试数据集上的准确率和损失
                dev_acc, dev_loss = eval(model, config, test_iterator)
                # 当前的验证集损失 dev_loss 小于最佳验证集损失 best_dev_loss
                if dev_loss < best_dev_loss:
                    best_dev_loss = dev_loss
                    # 保存模型的状态字典(包含模型参数)到指定的文件路径 config.saved_model。
                    # 这是为了记录当前取得最佳验证集损失时的模型参数状态,以便后续可以使用这个模型进行推断或进一步训练
                    torch.save(model.state_dict(), config.saved_model)
                    # 标记当前验证集损失有所改善的标记
                    improve = "*"
                    # 最后一次改善验证集损失的批次号
                    last_improve = total_batch
                else:
                    improve = ""
                # 训练过程的耗时
                time_dif = get_time_dif(start_time)
                msg = 'Iter: {0:>6}, Batch Train Loss: {1:>5.2}, Batch Train Acc: {2:>6.2%}, Val Loss: {3:>5.2}, Val Acc: {4:>6.2%}, Time: {5} {6}'
                print(msg.format(total_batch, loss.item(), acc, dev_loss, dev_acc, time_dif, improve))
                # 将模型设置为训练模式
                model.train()

            total_batch += 1
            # 经过require_improvement个批次后如果没有改善,则触发早停机制
            if total_batch - last_improve > config.require_improvement:
                print("No improvement for a long time, auto-stopping...")
                break_flag = True
                break
        if break_flag:
            break
    # 对模型进行训练
    test(model, config, test_iterator)

1.5.  preprocess.py

是对模型进行数据预处理的代码,在main文件中,训练前准备的一个训练数据迭代器。

里面包括了数据的读取、数据的特征化(也就将数据进行打标签)。

2.  训练模型

调用main文件中的main函数,并且mode="train",在终端输入

python main.py --mode train

为什么要采用此操作才能训练模型呢?因为项目中开始训练模型的输入,是通过argparse模块。

argparse模块是在Python中用于命令行参数的解析和处理。

限制与训练的数据及电脑性能,训练时间过于漫长,我们训练几白条数据就差不多了。

3.  测试一下模型预测

在终端输入

python main.py --mode demo

根据终端提示,输入一些新闻标题,查看分类结果。

4.  模拟接口访问模型

  1. 选中app.py文件,运行此文件。

  2. 在postman或者Apifox等接口调试工具,输入http://127.0.0.1/predict,采用POST方式,在body中输入{"content":"****"}进行测试

5.  换一个其他类型的NLP模型 --FillMask

除了用别人的预训练模型,通过我们的大量的数据进行训练后,生成我们自己的模型之外。我们还可以使用别人已经训练成熟的模型,直接进行使用,而且使用方法也是很简单。

1.  在huggingface,进行模型的挑选,首先选择NLP分类,选择一个Fill-Mask

2.  挑选一个符合功能预期的模型,我选择了cardiffnlp/twitter-roberta-base-2021-124m

  1. 获取twitter模型的pipeline( pipeline可以将各个模型进行组合,最终生成一个我们业务中正真需要的模型)。
from transformers import pipeline, AutoTokenizer

MODEL = "cardiffnlp/twitter-roberta-base-2021-124m"
# 创建了一个填充遮罩的pipeline
# "fill-mask" 参数表示我们要使用pipeline执行fill-mask任务
fill_mask = pipeline("fill-mask", model=MODEL, tokenizer=MODEL)
# 创建了一个 tokenizer 实例,通过 from_pretrained 方法从预训练模型的名称 tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL)

2.  数据准备

def preprocess(text):
    # 创建一个空列表,用于存储预处理后的文本
    preprocessed_text = []
    # 使用空格分割文本为单词列表,并遍历每个单词
    for t in text.split():
        if len(t) > 1: # 如果单词长度大于1
            # 如果单词以 "@" 开头且只包含一个 "@",将其替换为 "@user"
            t = '@user' if t[0] == '@' and t.count('@') == 1 else t
            # 如果单词以 "http" 开头,将其替换为 "http"
            t = 'http' if t.startswith('http') else t
        # 过滤训练数据时的用户名和网址。
        # 将预处理后的单词添加到列表中
        preprocessed_text.append(t)
    # 将列表中的单词用空格连接成字符串并返回
    return ' '.join(preprocessed_text)

3.  模型预测

def predict(content):

    texts = [
          f"{content}"
      ]
    for text in texts:
        t = preprocess(text)
        candidates = fill_mask(t)
        list = pprint(candidates, 5)
        # token = tokenizer.decode(candidates[0]['token'])
        # score = candidates[0]['score']
        return f'{list}'

4.  模拟接口访问,返回预测结果

from flask import Flask, request, jsonify

import train
import sentiment

app = Flask(__name__)


@app.route('/fill-mask', methods=['POST'])
def predict_word():
    body = request.get_json()
    sentence = body['content']
    if not sentence:
        return 'error'
    fullSentence = train.predict(sentence)
    res = {
        'content': fullSentence,
    }
    return jsonify(res)

if __name__ == '__main__':
    app.debug = True
    app.config['WTF_CSRF_ENABLED'] = False
    app.run(host='127.0.0.1', port=62792)

6.  总结

通过本次案例,机器训练怎样形成一个可用模型的流程,有了一个大概的轮廓。但是其中涉及到的一些调优参数(比如 隐层dropout概率,矩阵概率等等),会如何影响模型的结果,这些专业的知识可能也是需要花很多的时间和精力才能够去理解。

总的来说,掀开了机器学习的一层小薄纱,看到了它的一些轮廓,后期对其他模型的研究及探索,相信可以探索到更多的知识。