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. 模拟接口访问模型
-
选中app.py文件,运行此文件。
-
在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
- 获取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概率,矩阵概率等等),会如何影响模型的结果,这些专业的知识可能也是需要花很多的时间和精力才能够去理解。
总的来说,掀开了机器学习的一层小薄纱,看到了它的一些轮廓,后期对其他模型的研究及探索,相信可以探索到更多的知识。