当前位置: 首页 > news >正文

1m的带宽做网站可以吗百度seo排名点击器

1m的带宽做网站可以吗,百度seo排名点击器,设计网页图片,别人不能注册我的wordpress站原文#xff1a;Natural Language Processing with Transformers 译者#xff1a;飞龙 协议#xff1a;CC BY-NC-SA 4.0 第八章#xff1a;使 transformers 在生产中更高效 在之前的章节中#xff0c;您已经看到了 transformers 如何被微调以在各种任务上产生出色的结果。… 原文Natural Language Processing with Transformers 译者飞龙 协议CC BY-NC-SA 4.0 第八章使 transformers 在生产中更高效 在之前的章节中您已经看到了 transformers 如何被微调以在各种任务上产生出色的结果。然而在许多情况下准确性或者您正在优化的任何指标是不够的如果您的最先进模型太慢或太大无法满足应用程序的业务需求那么它就不是很有用。一个明显的替代方案是训练一个更快、更紧凑的模型但模型容量的减少通常会伴随着性能的下降。那么当您需要一个快速、紧凑但高度准确的模型时您该怎么办呢 在本章中我们将探讨四种互补的技术可以用来加速预测并减少您的 transformer 模型的内存占用知识蒸馏、量化、修剪和使用 Open Neural Network Exchange (ONNX)格式和 ONNX Runtime (ORT)进行图优化。我们还将看到其中一些技术如何结合起来产生显著的性能提升。例如这是 Roblox 工程团队在他们的文章“我们如何在 CPU 上扩展 BERT 以处理 10 亿日请求”中采取的方法正如图 8-1 所示他们发现结合知识蒸馏和量化使他们的 BERT 分类器的延迟和吞吐量提高了 30 倍以上 图 8-1. Roblox 如何通过知识蒸馏、动态填充和权重量化扩展 BERT照片由 Roblox 员工 Quoc N. Le 和 Kip Kaehler 提供 为了说明与每种技术相关的好处和权衡我们将以意图检测为案例研究这是基于文本的助手的重要组成部分低延迟对于实时维持对话至关重要。在学习的过程中您将学习如何创建自定义训练器执行高效的超参数搜索并了解实施最前沿研究所需的内容使用 Transformers。让我们开始吧 以意图检测为案例研究 假设我们正在尝试为公司的呼叫中心构建一个基于文本的助手以便客户可以在不需要与人类代理交谈的情况下请求其账户余额或进行预订。为了理解客户的目标我们的助手需要能够将各种自然语言文本分类为一组预定义的动作或意图。例如客户可能会发送以下关于即将到来的旅行的消息 嘿我想在 11 月 1 日到 11 月 15 日在巴黎租一辆车我需要一辆 15 座位的面包车。 我们的意图分类器可以自动将此分类为租车意图然后触发一个动作和响应。为了在生产环境中具有鲁棒性我们的分类器还需要能够处理超出范围的查询即客户提出不属于任何预定义意图的查询系统应该产生一个回退响应。例如在图 8-2 中显示的第二种情况中客户询问有关体育的问题超出范围文本助手错误地将其分类为已知的范围内意图之一并返回发薪日的响应。在第三种情况下文本助手已经被训练来检测超出范围的查询通常标记为一个单独的类并告知客户它可以回答关于哪些主题的问题。 图 8-2. 人类右和基于文本的助手左之间的三次交流涉及个人理财由 Stefan Larson 等人提供 作为基准我们微调了一个 BERT-base 模型在 CLINC150 数据集上达到了约 94%的准确性。这个数据集包括 150 个意图和 10 个领域如银行和旅行中的 22,500 个范围内查询还包括属于oos意图类别的 1,200 个范围外查询。在实践中我们还会收集自己的内部数据集但使用公共数据是快速迭代和生成初步结果的好方法。 让我们从 Hugging Face Hub 下载我们微调的模型并将其包装成文本分类的管道 from transformers import pipelinebert_ckpt transformersbook/bert-base-uncased-finetuned-clinc pipe pipeline(text-classification, modelbert_ckpt)现在我们有了一个管道我们可以传递一个查询以从模型获取预测的意图和置信度分数 query Hey, Id like to rent a vehicle from Nov 1st to Nov 15th in Paris and I need a 15 passenger van pipe(query)[{label: car_rental, score: 0.549003541469574}]很好car_rental意图是有意义的。现在让我们看看创建一个基准我们可以用来评估我们基准模型的性能。 创建性能基准 与其他机器学习模型一样在生产环境中部署 transformers 涉及在几个约束条件之间进行权衡最常见的是 模型性能 我们的模型在反映生产数据的精心设计的测试集上表现如何当错误的成本很高时最好通过人为干预来减轻或者当我们需要对数百万个示例进行推断并且模型指标的小幅改进可以转化为大幅增益时这一点尤为重要。 延迟 我们的模型能够多快地提供预测我们通常关心实时环境中的延迟这些环境处理大量流量就像 Stack Overflow 需要一个分类器来快速检测网站上不受欢迎的评论一样。 内存 我们如何部署像 GPT-2 或 T5 这样需要占用几 GB 磁盘存储和内存的百亿参数模型内存在移动设备或边缘设备中扮演着特别重要的角色因为模型必须在没有强大的云服务器的情况下生成预测。 未能解决这些约束条件可能会对应用程序的用户体验产生负面影响。更常见的是可能会导致运行昂贵的云服务器的成本激增而这些服务器可能只需要处理少量请求。为了探索如何使用各种压缩技术优化这些约束条件让我们从创建一个简单的基准开始该基准可以测量给定管道和测试集的每个数量 class PerformanceBenchmark:def __init__(self, pipeline, dataset, optim_typeBERT baseline):self.pipeline pipelineself.dataset datasetself.optim_type optim_typedef compute_accuracy(self):# Well define this laterpassdef compute_size(self):# Well define this laterpassdef time_pipeline(self):# Well define this laterpassdef run_benchmark(self):metrics {}metrics[self.optim_type] self.compute_size()metrics[self.optim_type].update(self.time_pipeline())metrics[self.optim_type].update(self.compute_accuracy())return metrics我们定义了一个optim_type参数以跟踪我们在本章中将涵盖的不同优化技术。我们将使用run_benchmark()方法将所有指标收集到一个字典中键由optim_type给出。 让我们现在通过在测试集上计算模型的准确性来为这个类添加一些具体内容。首先我们需要一些数据进行测试所以让我们下载用于微调基准模型的 CLINC150 数据集。我们可以通过以下方式从 Hub 获取数据集。 from datasets import load_datasetclinc load_dataset(clinc_oos, plus)在这里plus配置是指包含超出范围的训练示例的子集。CLINC150 数据集中的每个示例都包括text列中的查询及其对应的意图。我们将使用测试集来对我们的模型进行基准测试所以让我们看一下数据集的一个示例 sample clinc[test][42] sample{intent: 133, text: transfer $100 from my checking to saving account}意图以 ID 的形式提供但我们可以通过访问数据集的features属性轻松获取到字符串的映射反之亦然 intents clinc[test].features[intent] intents.int2str(sample[intent])transfer现在我们对 CLINC150 数据集的内容有了基本的了解让我们实现PerformanceBenchmark的compute_accuracy()方法。由于数据集在意图类别上是平衡的我们将使用准确性作为我们的度量标准。我们可以通过以下方式使用数据集加载这个度量标准 from datasets import load_metricaccuracy_score load_metric(accuracy)准确度指标期望预测和参考即真实标签是整数。我们可以使用管道从text字段中提取预测然后使用我们的intents对象的“str2int”方法将每个预测映射到其相应的 ID。以下代码在返回数据集的准确度之前收集所有的预测和标签。让我们也将其添加到我们的“PerformanceBenchmark”类中 def compute_accuracy(self):This overrides the PerformanceBenchmark.compute_accuracy() methodpreds, labels [], []for example in self.dataset:pred self.pipeline(example[text])[0][label]label example[intent]preds.append(intents.str2int(pred))labels.append(label)accuracy accuracy_score.compute(predictionspreds, referenceslabels)print(fAccuracy on test set - {accuracy[accuracy]:.3f})return accuracyPerformanceBenchmark.compute_accuracy compute_accuracy接下来让我们使用 PyTorch 的“torch.save”函数来计算我们模型的大小将模型序列化到磁盘上。在内部“torch.save”使用 Python 的pickle模块可以用来保存从模型到张量到普通 Python 对象的任何东西。在 PyTorch 中保存模型的推荐方式是使用它的state_dict这是一个 Python 字典将模型中的每一层映射到它的可学习参数即权重和偏置。让我们看看我们基准模型的state_dict中存储了什么 list(pipe.model.state_dict().items())[42](bert.encoder.layer.2.attention.self.value.weight,tensor([[-1.0526e-02, -3.2215e-02, 2.2097e-02, ..., -6.0953e-03,4.6521e-03, 2.9844e-02],[-1.4964e-02, -1.0915e-02, 5.2396e-04, ..., 3.2047e-05,-2.6890e-02, -2.1943e-02],[-2.9640e-02, -3.7842e-03, -1.2582e-02, ..., -1.0917e-02,3.1152e-02, -9.7786e-03],...,[-1.5116e-02, -3.3226e-02, 4.2063e-02, ..., -5.2652e-03,1.1093e-02, 2.9703e-03],[-3.6809e-02, 5.6848e-02, -2.6544e-02, ..., -4.0114e-02,6.7487e-03, 1.0511e-03],[-2.4961e-02, 1.4747e-03, -5.4271e-02, ..., 2.0004e-02,2.3981e-02, -4.2880e-02]]))我们可以清楚地看到每个键/值对对应于 BERT 中的特定层和张量。因此如果我们用以下方式保存我们的模型 torch.save(pipe.model.state_dict(), model.pt)我们可以使用 Python 的pathlib模块中的“Path.stat”函数来获取有关底层文件的信息。特别是“Path“model.​pt”.​stat.​st_size”将给出模型的大小以字节为单位。让我们将所有这些放在“compute_​size”函数中并将其添加到PerformanceBenchmark中 import torch from pathlib import Pathdef compute_size(self):This overrides the PerformanceBenchmark.compute_size() methodstate_dict self.pipeline.model.state_dict()tmp_path Path(model.pt)torch.save(state_dict, tmp_path)# Calculate size in megabytessize_mb Path(tmp_path).stat().st_size / (1024 * 1024)# Delete temporary filetmp_path.unlink()print(fModel size (MB) - {size_mb:.2f})return {size_mb: size_mb}PerformanceBenchmark.compute_size compute_size最后让我们实现“time_pipeline”函数以便我们可以计算每个查询的平均延迟时间。对于这个应用程序延迟时间指的是将文本查询输入到管道中并从模型返回预测意图所需的时间。在内部管道还会对文本进行标记化但这比生成预测快了大约一千倍因此对整体延迟时间的贡献可以忽略不计。衡量代码片段的执行时间的一个简单方法是使用 Python 的time模块中的“perf_counter”函数。这个函数比“time.time”函数具有更好的时间分辨率非常适合获取精确的结果。 我们可以使用“perf_counter”通过传递我们的测试查询来计时我们的管道并计算开始和结束之间的毫秒时间差 from time import perf_counterfor _ in range(3):start_time perf_counter()_ pipe(query)latency perf_counter() - start_timeprint(fLatency (ms) - {1000 * latency:.3f})Latency (ms) - 85.367 Latency (ms) - 85.241 Latency (ms) - 87.275这些结果展示了延迟时间的相当大的差异并且表明通过管道的单次计时可能每次运行代码时都会得到完全不同的结果。因此我们将收集多次运行的延迟时间然后使用得到的分布来计算均值和标准差这将让我们对数值的差异有一个概念。以下代码实现了我们需要的功能并包括了在执行实际计时运行之前预热 CPU 的阶段 import numpy as npdef time_pipeline(self, queryWhat is the pin number for my account?):This overrides the PerformanceBenchmark.time_pipeline() methodlatencies []# Warmupfor _ in range(10):_ self.pipeline(query)# Timed runfor _ in range(100):start_time perf_counter()_ self.pipeline(query)latency perf_counter() - start_timelatencies.append(latency)# Compute run statisticstime_avg_ms 1000 * np.mean(latencies)time_std_ms 1000 * np.std(latencies)print(fAverage latency (ms) - {time_avg_ms:.2f} \- {time_std_ms:.2f})return {time_avg_ms: time_avg_ms, time_std_ms: time_std_ms}PerformanceBenchmark.time_pipeline time_pipeline为了简化问题我们将使用相同的query值来对我们所有的模型进行基准测试。一般来说延迟时间将取决于查询长度一个好的做法是使用模型可能在生产环境中遇到的查询来对模型进行基准测试。 现在我们的PerformanceBenchmark类已经完成让我们来试一试吧让我们从对我们的 BERT 基准模型进行基准测试开始。对于基准模型我们只需要传递管道和我们希望进行基准测试的数据集。我们将在perf_metrics字典中收集结果以跟踪每个模型的性能 pb PerformanceBenchmark(pipe, clinc[test]) perf_metrics pb.run_benchmark()Model size (MB) - 418.16 Average latency (ms) - 54.20 \- 1.91 Accuracy on test set - 0.867现在我们有了一个参考点让我们来看看我们的第一个压缩技术知识蒸馏。 注意 平均延迟值将取决于您所运行的硬件类型。例如通常可以通过在 GPU 上运行推断来获得更好的性能因为它可以实现批处理。对于本章的目的重要的是模型之间延迟时间的相对差异。一旦确定了性能最佳的模型我们可以探索不同的后端来减少绝对延迟时间如果需要。 通过知识蒸馏使模型变得更小 知识蒸馏是一种通用方法用于训练一个较小的“学生”模型来模仿速度较慢、更大但性能更好的“教师”模型的行为。最初是在 2006 年在集成模型的背景下引入的后来在一篇著名的 2015 年论文中将该方法推广到深度神经网络并将其应用于图像分类和自动语音识别。 鉴于预训练语言模型参数数量不断增加的趋势撰写时最大的模型参数超过一万亿知识蒸馏也成为压缩这些庞大模型并使其更适合构建实际应用的流行策略。 微调的知识蒸馏 那么在训练过程中知识实际上是如何从教师传递给学生的呢对于微调等监督任务主要思想是用教师的“软概率”分布来增强地面真实标签为学生提供补充信息。例如如果我们的 BERT-base 分类器为多个意图分配高概率那么这可能表明这些意图在特征空间中相互靠近。通过训练学生模仿这些概率目标是蒸馏教师学到的一些“暗知识”——也就是仅从标签中无法获得的知识。 从数学上讲这是如何工作的。假设我们将输入序列x提供给教师以生成一个对数向量 ( x ) [ z 1 ( x ) , … , z N ( x ) ]。我们可以通过应用 softmax 函数将这些对数转换为概率 exp(z i (x)) ∑ j exp(z i (x)) 然而这并不是我们想要的因为在许多情况下教师会为一个类分配高概率而其他类的概率接近于零。当发生这种情况时教师除了地面真实标签外并没有提供太多额外信息因此我们会在应用 softmax 之前通过一个温度超参数T来缩放对数从而“软化”概率。 p i ( x ) exp(z i (x)/T) ∑ j exp(z i (x)/T) 如图 8-3 所示T的值越高类别上的软化概率分布就越软可以更多地揭示老师对每个训练示例学习的决策边界。当T 1时我们恢复了原始的 softmax 分布。 图 8-3。一个使用 one-hot 编码的硬标签左、softmax 概率中和软化类别概率右的比较。 由于学生还产生了自己的软化概率q i ( x )我们可以使用Kullback-LeiblerKL散度来衡量两个概率分布之间的差异 D KL ( p , q ) ∑ i p i ( x ) log p i (x) q i (x) 通过 KL 散度我们可以计算当我们用学生来近似老师的概率分布时损失了多少。这使我们能够定义知识蒸馏损失 L KD T 2 D KL 其中T 2是一个归一化因子用于考虑软标签产生的梯度大小按1 / T 2缩放的事实。对于分类任务学生的损失是蒸馏损失和地面真实标签的交叉熵损失L CE的加权平均 L student α L CE ( 1 - α ) L KD 其中α是一个控制每个损失相对强度的超参数。整个过程的图表如图 8-4 所示在推断时温度被设置为 1以恢复标准的 softmax 概率。 图 8-4。知识蒸馏过程 预训练的知识蒸馏 知识蒸馏也可以在预训练期间使用以创建一个通用的学生模型随后可以在下游任务上进行精细调整。在这种情况下教师是一个预训练的语言模型如 BERT它将其关于掩码语言建模的知识转移到学生身上。例如在 DistilBERT 论文中⁸掩码语言建模损失L mlm被知识蒸馏的一个项和余弦嵌入损失L cos 1 - cos ( h s , h t )来对齐教师和学生之间的隐藏状态向量的方向 L DistilBERT α L mlm β L KD γ L cos 由于我们已经有了一个经过精细调整的 BERT-base 模型让我们看看如何使用知识蒸馏来对一个更小更快的模型进行精细调整。为了做到这一点我们需要一种方法来将交叉熵损失与L KD项相结合。幸运的是我们可以通过创建自己的训练器来实现这一点 创建知识蒸馏训练器 要实现知识蒸馏我们需要向Trainer基类添加一些内容 新的超参数α和T它们控制蒸馏损失的相对权重以及标签的概率分布应该被平滑的程度 经过精细调整的教师模型我们的情况下是 BERT-base 结合交叉熵损失和知识蒸馏损失的新损失函数 添加新的超参数非常简单因为我们只需要对TrainingArguments进行子类化并将它们包含为新的属性 from transformers import TrainingArgumentsclass DistillationTrainingArguments(TrainingArguments):def __init__(self, *args, alpha0.5, temperature2.0, **kwargs):super().__init__(*args, **kwargs)self.alpha alphaself.temperature temperature对于训练器本身我们需要一个新的损失函数。实现这一点的方法是通过对Trainer进行子类化并覆盖compute_loss()方法以包括知识蒸馏损失项L KD import torch.nn as nn import torch.nn.functional as F from transformers import Trainerclass DistillationTrainer(Trainer):def __init__(self, *args, teacher_modelNone, **kwargs):super().__init__(*args, **kwargs)self.teacher_model teacher_modeldef compute_loss(self, model, inputs, return_outputsFalse):outputs_stu model(**inputs)# Extract cross-entropy loss and logits from studentloss_ce outputs_stu.losslogits_stu outputs_stu.logits# Extract logits from teacherwith torch.no_grad():outputs_tea self.teacher_model(**inputs)logits_tea outputs_tea.logits# Soften probabilities and compute distillation lossloss_fct nn.KLDivLoss(reductionbatchmean)loss_kd self.args.temperature ** 2 * loss_fct(F.log_softmax(logits_stu / self.args.temperature, dim-1),F.softmax(logits_tea / self.args.temperature, dim-1))# Return weighted student lossloss self.args.alpha * loss_ce (1. - self.args.alpha) * loss_kdreturn (loss, outputs_stu) if return_outputs else loss让我们解开一下这段代码。当我们实例化DistillationTrainer时我们传递了一个已经在我们的任务上进行了微调的老师模型。接下来在compute_loss()方法中我们从学生和老师那里提取 logits通过温度对它们进行缩放然后在传递给 PyTorch 的nn.KLDivLoss()函数之前使用 softmax 对它们进行归一化以计算 KL 散度。nn.KLDivLoss()的一个怪癖是它期望输入以对数概率的形式标签以正常概率的形式。这就是为什么我们使用F.log_softmax()函数对学生的 logits 进行归一化而老师的 logits 则使用标准 softmax 转换为概率。nn.KLDivLoss()中的reductionbatchmean参数指定我们在批维度上平均损失。 提示 您还可以使用 Transformers 库的 Keras API 进行知识蒸馏。为此您需要实现一个自定义的Distiller类覆盖tf.keras.Model()的train_step()、test_step()和compile()方法。请参阅Keras 文档了解如何实现。 选择一个好的学生初始化 现在我们有了自定义的训练器您可能会问的第一个问题是我们应该为学生选择哪个预训练语言模型一般来说我们应该为学生选择一个较小的模型以减少延迟和内存占用。从文献中得出的一个很好的经验法则是当老师和学生是相同的模型类型时知识蒸馏效果最好。⁹这样做的一个可能原因是不同的模型类型比如 BERT 和 RoBERTa可能具有不同的输出嵌入空间这会妨碍学生模仿老师的能力。在我们的案例研究中老师是 BERT因此 DistilBERT 是一个自然的候选因为它的参数少了 40%并且已经在下游任务中取得了良好的结果。 首先我们需要对我们的查询进行标记化和编码因此让我们实例化来自 DistilBERT 的标记器并创建一个简单的tokenize_text()函数来处理预处理 from transformers import AutoTokenizerstudent_ckpt distilbert-base-uncased student_tokenizer AutoTokenizer.from_pretrained(student_ckpt)def tokenize_text(batch):return student_tokenizer(batch[text], truncationTrue)clinc_enc clinc.map(tokenize_text, batchedTrue, remove_columns[text]) clinc_enc clinc_enc.rename_column(intent, labels)在这里我们已经删除了text列因为我们不再需要它我们还将intent列重命名为labels以便训练器可以自动检测到它。¹⁰ 现在我们已经处理了我们的文本接下来我们需要做的是为我们的DistillationTrainer定义超参数和compute_metrics()函数。我们还将把所有的模型推送到 Hugging Face Hub所以让我们首先登录到我们的账户 from huggingface_hub import notebook_loginnotebook_login()接下来我们将定义训练过程中要跟踪的指标。就像我们在性能基准测试中所做的那样我们将使用准确性作为主要指标。这意味着我们可以在compute_metrics()函数中重用我们的accuracy_score()函数这个函数将包含在DistillationTrainer中 def compute_metrics(pred):predictions, labels predpredictions np.argmax(predictions, axis1)return accuracy_score.compute(predictionspredictions, referenceslabels)在这个函数中序列建模头部的预测以 logits 的形式出现因此我们使用np.argmax()函数找到最有信心的类别预测并将其与地面真相标签进行比较。 接下来我们需要定义训练参数。为了热身我们将设置α 1以查看 DistilBERT 在没有来自教师的任何信号的情况下的表现。¹¹然后我们将我们的微调模型推送到一个名为distilbert-base-uncased-finetuned-clinc的新存储库所以我们只需要在DistillationTrainingArguments的output_dir参数中指定它 batch_size 48finetuned_ckpt distilbert-base-uncased-finetuned-clinc student_training_args DistillationTrainingArguments(output_dirfinetuned_ckpt, evaluation_strategy epoch,num_train_epochs5, learning_rate2e-5,per_device_train_batch_sizebatch_size,per_device_eval_batch_sizebatch_size, alpha1, weight_decay0.01,push_to_hubTrue)我们还调整了一些默认超参数值比如 epochs 的数量权重衰减和学习率。接下来要做的是初始化一个学生模型。由于我们将使用训练器进行多次运行我们将创建一个student_init()函数以便在每次调用train()方法时初始化一个新模型。当我们将这个函数传递给DistillationTrainer时这将确保我们每次调用train()方法时初始化一个新模型。 我们还需要做的另一件事是为学生模型提供每个意图和标签 ID 之间的映射。这些映射可以从我们在流水线中下载的 BERT-base 模型中获得 id2label pipe.model.config.id2label label2id pipe.model.config.label2id有了这些映射我们现在可以使用AutoConfig类创建一个自定义模型配置这是我们在第三章和第四章中遇到的。让我们使用这个为我们的学生创建一个包含标签映射信息的配置 from transformers import AutoConfignum_labels intents.num_classes student_config (AutoConfig.from_pretrained(student_ckpt, num_labelsnum_labels,id2labelid2label, label2idlabel2id))在这里我们还指定了我们的模型应该期望的类的数量。然后我们可以将这个配置提供给AutoModelForSequenceClassification类的from_pretrained()函数如下所示 import torch from transformers import AutoModelForSequenceClassificationdevice torch.device(cuda if torch.cuda.is_available() else cpu)def student_init():return (AutoModelForSequenceClassification.from_pretrained(student_ckpt, configstudent_config).to(device))现在我们已经拥有了我们的蒸馏训练器所需的所有要素让我们加载教师并进行微调 teacher_ckpt transformersbook/bert-base-uncased-finetuned-clinc teacher_model (AutoModelForSequenceClassification.from_pretrained(teacher_ckpt, num_labelsnum_labels).to(device))distilbert_trainer DistillationTrainer(model_initstudent_init,teacher_modelteacher_model, argsstudent_training_args,train_datasetclinc_enc[train], eval_datasetclinc_enc[validation],compute_metricscompute_metrics, tokenizerstudent_tokenizer)distilbert_trainer.train()EpochTraining LossValidation LossAccuracy————14.29233.2893370.74225822.63071.8836800.82806531.54831.1583150.89677441.01530.8618150.90935550.79580.7772890.917419 验证集上的 92%准确率看起来相当不错与 BERT-base 教师实现的 94%相比。现在我们已经对 DistilBERT 进行了微调让我们将模型推送到 Hub以便以后重用 distilbert_trainer.push_to_hub(Training completed!)现在我们的模型已经安全地存储在 Hub 上我们可以立即在性能基准测试的流水线中使用它 finetuned_ckpt transformersbook/distilbert-base-uncased-finetuned-clinc pipe pipeline(text-classification, modelfinetuned_ckpt)然后我们可以将这个流水线传递给我们的PerformanceBenchmark类以计算与这个模型相关的指标 optim_type DistilBERT pb PerformanceBenchmark(pipe, clinc[test], optim_typeoptim_type) perf_metrics.update(pb.run_benchmark())Model size (MB) - 255.89 Average latency (ms) - 27.53 \- 0.60 Accuracy on test set - 0.858为了将这些结果与我们的基准进行比较让我们创建一个散点图显示准确性与延迟之间的关系每个点的半径对应于磁盘上模型的大小。以下函数可以满足我们的需求并将当前优化类型标记为虚线圆圈以便与以前的结果进行比较 import pandas as pddef plot_metrics(perf_metrics, current_optim_type):df pd.DataFrame.from_dict(perf_metrics, orientindex)for idx in df.index:df_opt df.loc[idx]# Add a dashed circle around the current optimization typeif idx current_optim_type:plt.scatter(df_opt[time_avg_ms], df_opt[accuracy] * 100,alpha0.5, sdf_opt[size_mb], labelidx,marker$\u25CC$)else:plt.scatter(df_opt[time_avg_ms], df_opt[accuracy] * 100,sdf_opt[size_mb], labelidx, alpha0.5)legend plt.legend(bbox_to_anchor(1,1))for handle in legend.legendHandles:handle.set_sizes([20])plt.ylim(80,90)# Use the slowest model to define the x-axis rangexlim int(perf_metrics[BERT baseline][time_avg_ms] 3)plt.xlim(1, xlim)plt.ylabel(Accuracy (%))plt.xlabel(Average latency (ms))plt.show()plot_metrics(perf_metrics, optim_type)从图中我们可以看到通过使用一个更小的模型我们成功地显著降低了平均延迟。而这一切只需牺牲了略微超过 1%的准确性让我们看看是否可以通过包括教师的蒸馏损失并找到α和T的良好值来缩小最后的差距。 使用 Optuna 找到良好的超参数 为了找到α和T的良好值我们可以在 2D 参数空间上进行网格搜索。但一个更好的选择是使用Optuna¹²这是一个专为这种任务设计的优化框架。Optuna 通过多次trials优化目标函数来制定搜索问题。例如假设我们希望最小化 Rosenbrock 的“香蕉函数” f ( x , y ) (1-x) 2 100 (y-x 2 ) 2 这是一个著名的优化框架的测试案例。如图 8-5 所示该函数因其曲线轮廓而得名并且在( x , y ) ( 1 , 1 )处有一个全局最小值。找到这个谷是一个简单的优化问题但收敛到全局最小值却不是。 图 8-5。两个变量的 Rosenbrock 函数的绘图 在 Optuna 中我们可以通过定义一个objective()函数来找到f ( x , y )的最小值该函数返回f ( x , y )的值 def objective(trial):x trial.suggest_float(x, -2, 2)y trial.suggest_float(y, -2, 2)return (1 - x) ** 2 100 * (y - x ** 2) ** 2trial.suggest_float对象指定要均匀采样的参数范围Optuna 还提供suggest_int和suggest_categorical用于整数和分类参数。Optuna 将多个试验收集为一个study因此我们只需将objective()函数传递给study.optimize()来创建一个如下 import optunastudy optuna.create_study() study.optimize(objective, n_trials1000)一旦研究完成我们就可以按照以下方式找到最佳参数 study.best_params{x: 1.003024865971437, y: 1.00315167589307}通过一千次试验Optuna 已经成功找到了* x 和 y 的值这些值与全局最小值相当接近。要在 Transformers 中使用 Optuna我们首先定义要优化的超参数空间。除了 α 和T*之外我们还将包括训练周期的数量如下 def hp_space(trial):return {num_train_epochs: trial.suggest_int(num_train_epochs, 5, 10),alpha: trial.suggest_float(alpha, 0, 1),temperature: trial.suggest_int(temperature, 2, 20)}使用Trainer进行超参数搜索非常简单我们只需要指定要运行的试验次数和要优化的方向。因为我们希望获得最佳准确度所以在训练器的hyper​para⁠meter_​search()方法中指定directionmaximize并按如下方式传递超参数搜索空间 best_run distilbert_trainer.hyperparameter_search(n_trials20, directionmaximize, hp_spacehp_space)hyperparameter_search()方法返回一个BestRun对象其中包含了被最大化的目标值默认为所有指标的总和和该运行所使用的超参数 print(best_run)BestRun(run_id1, objective0.927741935483871, hyperparameters{num_train_epochs: 10, alpha: 0.12468168730193585, temperature: 7})这个α的值告诉我们大部分的训练信号来自知识蒸馏项。让我们使用这些值更新我们的训练参数并运行最终的训练 for k,v in best_run.hyperparameters.items():setattr(student_training_args, k, v)# Define a new repository to store our distilled model distilled_ckpt distilbert-base-uncased-distilled-clinc student_training_args.output_dir distilled_ckpt# Create a new Trainer with optimal parameters distil_trainer DistillationTrainer(model_initstudent_init,teacher_modelteacher_model, argsstudent_training_args,train_datasetclinc_enc[train], eval_datasetclinc_enc[validation],compute_metricscompute_metrics, tokenizerstudent_tokenizer)distil_trainer.train();EpochTraining LossValidation LossAccuracy————10.90310.5745400.73645220.44810.2856210.87483930.25280.1797660.91871040.17600.1398280.92935550.14160.1210530.93483960.12430.1116400.93483970.11330.1061740.93774280.10750.1035260.93871090.10390.1014320.938065100.10180.1004930.939355 值得注意的是尽管参数数量几乎减少了一半我们已经成功训练出学生模型与教师模型的准确度相匹配让我们将模型推送到 Hub 以供将来使用 distil_trainer.push_to_hub(Training complete)基准测试我们的精炼模型 现在我们有了一个准确的学生让我们创建一个流水线并重新进行我们的基准测试看看我们在测试集上的表现如何 distilled_ckpt transformersbook/distilbert-base-uncased-distilled-clinc pipe pipeline(text-classification, modeldistilled_ckpt) optim_type Distillation pb PerformanceBenchmark(pipe, clinc[test], optim_typeoptim_type) perf_metrics.update(pb.run_benchmark())Model size (MB) - 255.89 Average latency (ms) - 25.96 \- 1.63 Accuracy on test set - 0.868为了将这些结果放在上下文中让我们还用我们的plot_metrics()函数将它们可视化 plot_metrics(perf_metrics, optim_type)正如预期的那样与 DistilBERT 基准相比模型大小和延迟基本保持不变但准确性得到了改善甚至超过了教师的表现解释这一令人惊讶的结果的一种方式是教师很可能没有像学生那样系统地进行精细调整。这很好但我们实际上可以使用一种称为量化的技术进一步压缩我们的精炼模型。这是下一节的主题。 使用量化使模型更快 我们现在已经看到通过知识蒸馏我们可以通过将信息从教师传递到更小的学生来减少运行推断的计算和内存成本。量化采用了不同的方法它不是减少计算的数量而是通过使用低精度数据类型如 8 位整数INT8代替通常的 32 位浮点数FP32来使它们更加高效。减少位数意味着结果模型需要更少的内存存储并且像矩阵乘法这样的操作可以通过整数运算更快地执行。值得注意的是这些性能增益可以在几乎没有准确性损失的情况下实现 量化背后的基本思想是我们可以通过将张量中的浮点值f的范围[ f max , f min ]映射到一个较小的范围[ q max , q min ]中的固定点数q并线性分布所有值。从数学上讲这种映射由以下方程描述 f f max -f min q max -q min ( q - Z ) S ( q - Z ) 缩放因子S是一个正的浮点数常数Z与q具有相同的类型被称为零点因为它对应于浮点值f 0的量化值。请注意映射需要是仿射的这样当我们将定点数反量化为浮点数时我们会得到浮点数。¹³ 转换的示例显示在图 8-6 中。 图 8-6。将浮点数量化为无符号 8 位整数由 Manas Sahni 提供 现在Transformer以及深度神经网络更普遍地成为量化的主要候选对象的一个主要原因是权重和激活倾向于在相对较小的范围内取值。这意味着我们不必将所有可能的 FP32 数字范围压缩到 INT8 表示的 256 个数字中。为了看到这一点让我们从我们精简模型中挑选出一个注意力权重矩阵并绘制值的频率分布 import matplotlib.pyplot as pltstate_dict pipe.model.state_dict() weights state_dict[distilbert.transformer.layer.0.attention.out_lin.weight] plt.hist(weights.flatten().numpy(), bins250, range(-0.3,0.3), edgecolorC0) plt.show()我们可以看到权重的值分布在接近零的小范围内[-0.1,0.1]。现在假设我们想要将这个张量量化为带符号的 8 位整数。在这种情况下我们整数的可能值范围是[qmax,qmin] [-128,127]。零点与 FP32 的零点重合比例因子根据前面的方程计算 zero_point 0 scale (weights.max() - weights.min()) / (127 - (-128))为了获得量化张量我们只需要反转映射qf/SZ将值夹紧四舍五入到最近的整数并使用Tensor.char()函数将结果表示为torch.int8数据类型 (weights / scale zero_point).clamp(-128, 127).round().char()tensor([[ -5, -8, 0, ..., -6, -4, 8],[ 8, 3, 1, ..., -4, 7, 0],[ -9, -6, 5, ..., 1, 5, -3],...,[ 6, 0, 12, ..., 0, 6, -1],[ 0, -2, -12, ..., 12, -7, -13],[-13, -1, -10, ..., 8, 2, -2]], dtypetorch.int8)太好了我们刚刚量化了我们的第一个张量在 PyTorch 中我们可以使用quantize_per_tensor()函数和量化数据类型torch.qint来简化转换该数据类型针对整数算术操作进行了优化 from torch import quantize_per_tensordtype torch.qint8 quantized_weights quantize_per_tensor(weights, scale, zero_point, dtype) quantized_weights.int_repr()tensor([[ -5, -8, 0, ..., -6, -4, 8],[ 8, 3, 1, ..., -4, 7, 0],[ -9, -6, 5, ..., 1, 5, -3],...,[ 6, 0, 12, ..., 0, 6, -1],[ 0, -2, -12, ..., 12, -7, -13],[-13, -1, -10, ..., 8, 2, -2]], dtypetorch.int8)图 8-7 中的图表清楚地显示了只映射一些权重值并对其余值进行四舍五入所引起的离散化。 图 8-7。量化对 Transformer 权重的影响 为了完成我们的小分析让我们比较使用 FP32 和 INT8 值计算两个权重张量的乘法需要多长时间。对于 FP32 张量我们可以使用 PyTorch 的运算符进行相乘 %%timeit weights weights393 µs ± 3.84 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)对于量化张量我们需要QFunctional包装类以便我们可以使用特殊的torch.qint8数据类型执行操作 from torch.nn.quantized import QFunctionalq_fn QFunctional()这个类支持各种基本操作比如加法在我们的情况下我们可以通过以下方式计算量化张量的乘法时间 %%timeit q_fn.mul(quantized_weights, quantized_weights)23.3 µs ± 298 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)与我们的 FP32 计算相比使用 INT8 张量几乎快 100 倍通过使用专门的后端运行量化运算符还可以获得更大的收益。截至本书编写时PyTorch 支持 具有 AVX2 支持或更高版本的 x86 CPU ARM CPU通常用于移动/嵌入式设备 由于 INT8 数字的位数比 FP32 数字少四倍量化还将内存存储需求减少了多达四倍。在我们的简单示例中我们可以通过使用Tensor.storage()函数和 Python 的sys模块中的getsizeof()函数来比较权重张量及其量化版本的底层存储大小来验证这一点 import syssys.getsizeof(weights.storage()) / sys.getsizeof(quantized_weights.storage())3.999633833760527对于一个大规模的 Transformer实际的压缩率取决于哪些层被量化正如我们将在下一节看到的通常只有线性层被量化。 那么量化有什么问题改变模型中所有计算的精度会在模型的计算图中的每个点引入小的扰动这可能会影响模型的性能。量化模型有几种方法各有利弊。对于深度神经网络通常有三种主要的量化方法 动态量化 使用动态量化时在训练期间不会发生任何变化调整只会在推断期间进行。与我们将讨论的所有量化方法一样模型的权重在推断时间之前被转换为 INT8。除了权重模型的激活也被量化。这种方法是动态的因为量化是即时发生的。这意味着所有矩阵乘法都可以使用高度优化的 INT8 函数进行计算。在这里讨论的所有量化方法中动态量化是最简单的方法。然而使用动态量化时激活以浮点格式写入和读取到内存中。整数和浮点之间的转换可能成为性能瓶颈。 静态量化 我们可以避免在推断期间将激活量化为浮点数而是预先计算量化方案。静态量化通过观察数据的代表性样本上的激活模式来实现这一点。理想的量化方案被计算然后保存。这使我们能够跳过 INT8 和 FP32 值之间的转换并加快计算速度。然而这需要访问一个良好的数据样本并且在管道中引入了一个额外的步骤因为现在我们需要在执行推断之前训练和确定量化方案。静态量化没有解决的一个方面是训练和推断期间精度之间的差异这导致模型指标例如准确性下降。 量化感知训练 通过“伪”量化 FP32 值来有效模拟训练期间的量化效果。在训练期间不使用 INT8 值而是将 FP32 值四舍五入以模拟量化效果。这在前向和后向传递过程中都会进行可以改善模型指标的性能超过静态和动态量化。 使用 transformers 进行推断的主要瓶颈是与这些模型中庞大数量的权重相关的计算和内存带宽。因此动态量化目前是自然语言处理中基于 transformer 的模型的最佳方法。在较小的计算机视觉模型中限制因素是激活的内存带宽这就是为什么通常使用静态量化或者在性能下降太显著的情况下使用量化感知训练的原因。 在 PyTorch 中实现动态量化非常简单可以用一行代码完成 from torch.quantization import quantize_dynamicmodel_ckpt transformersbook/distilbert-base-uncased-distilled-clinc tokenizer AutoTokenizer.from_pretrained(model_ckpt) model (AutoModelForSequenceClassification.from_pretrained(model_ckpt).to(cpu))model_quantized quantize_dynamic(model, {nn.Linear}, dtypetorch.qint8)在这里我们将完整精度模型传递给quantize_dynamic()并指定我们要量化的 PyTorch 层类的集合。dtype参数指定目标精度可以是fp16或qint8。一个好的做法是选择您的评估指标所能容忍的最低精度。在本章中我们将使用 INT8很快就会看到它对我们模型的准确性几乎没有影响。 对我们的量化模型进行基准测试 我们的模型现在已经量化让我们通过基准测试并可视化结果 pipe pipeline(text-classification, modelmodel_quantized,tokenizertokenizer) optim_type Distillation quantization pb PerformanceBenchmark(pipe, clinc[test], optim_typeoptim_type) perf_metrics.update(pb.run_benchmark())Model size (MB) - 132.40 Average latency (ms) - 12.54 \- 0.73 Accuracy on test set - 0.876plot_metrics(perf_metrics, optim_type)不错量化模型几乎是我们精简模型大小的一半甚至还略微提高了准确性让我们看看是否可以通过一个强大的框架 ONNX Runtime 将我们的优化推向极限。 使用 ONNX 和 ONNX Runtime 优化推断 ONNX是一个开放标准定义了一组通用的操作符和一种通用的文件格式用于在各种框架中表示深度学习模型包括 PyTorch 和 TensorFlow。¹⁴当模型导出为 ONNX 格式时这些操作符用于构建一个计算图通常称为中间表示表示数据通过神经网络的流动。例如BERT-base 的这样一个图示例显示在图 8-8 中其中每个节点接收一些输入应用操作如Add或Squeeze然后将输出馈送到下一组节点。 图 8-8. BERT-base 的 ONNX 图的一个部分在 Netron 中可视化 通过公开具有标准化操作符和数据类型的图ONNX 使得在不同框架之间切换变得容易。例如在 PyTorch 中训练的模型可以导出为 ONNX 格式然后在 TensorFlow 中导入反之亦然。 当 ONNX 与专用加速器如ONNX Runtime或 ORT 配合使用时它的优势就显现出来了。¹⁵ORT 通过操作符融合和常量折叠等技术提供了优化 ONNX 图的工具¹⁶并定义了一个接口允许您在不同类型的硬件上运行模型。这是一个强大的抽象。图 8-9 显示了 ONNX 和 ORT 生态系统的高级架构。 图 8-9. ONNX 和 ONNX Runtime 生态系统的架构由 ONNX Runtime 团队提供 要看到 ORT 的运行情况我们需要做的第一件事是将我们的精炼模型转换为 ONNX 格式。 Transformers 库有一个内置函数叫做con⁠vert_graph_to_onnx.convert()它简化了这个过程采取以下步骤 将模型初始化为Pipeline。 通过管道运行占位符输入以便 ONNX 可以记录计算图。 定义动态轴以处理动态序列长度。 保存具有网络参数的图。 要使用这个函数我们首先需要为 ONNX 设置一些OpenMP环境变量 import os from psutil import cpu_countos.environ[OMP_NUM_THREADS] f{cpu_count()} os.environ[OMP_WAIT_POLICY] ACTIVEOpenMP 是一个为开发高度并行化应用程序而设计的 API。OMP_NUM_THREADS环境变量设置并行计算中使用的线程数在 ONNX Runtime 中OMP_WAIT_POLICYACTIVE指定等待线程应处于活动状态即使用 CPU 处理器周期。 接下来让我们将我们的精炼模型转换为 ONNX 格式。在这里我们需要指定参数pipeline_nametext-classification因为convert()在转换过程中将模型包装在一个 Transformers pipeline()函数中。除了model_ckpt之外我们还传递了 tokenizer 来初始化管道 from transformers.convert_graph_to_onnx import convertmodel_ckpt transformersbook/distilbert-base-uncased-distilled-clinc onnx_model_path Path(onnx/model.onnx) convert(frameworkpt, modelmodel_ckpt, tokenizertokenizer,outputonnx_model_path, opset12, pipeline_nametext-classification)ONNX 使用操作符集来将不可变的操作符规范分组在一起因此opset12对应于 ONNX 库的特定版本。 现在我们已经保存了我们的模型我们需要创建一个InferenceSession实例来向模型输入数据 from onnxruntime import (GraphOptimizationLevel, InferenceSession,SessionOptions)def create_model_for_provider(model_path, providerCPUExecutionProvider):options SessionOptions()options.intra_op_num_threads 1options.graph_optimization_level GraphOptimizationLevel.ORT_ENABLE_ALLsession InferenceSession(str(model_path), options, providers[provider])session.disable_fallback()return sessiononnx_model create_model_for_provider(onnx_model_path)现在当我们调用onnx_model.run()时我们可以从 ONNX 模型中获取类别对数。让我们用测试集中的一个例子来测试一下。由于convert()的输出告诉我们 ONNX 只期望input_ids和attention_mask作为输入我们需要从我们的样本中删除label列 inputs clinc_enc[test][:1] del inputs[labels] logits_onnx onnx_model.run(None, inputs)[0] logits_onnx.shape(1, 151)一旦我们有了对数我们可以通过取 argmax 轻松获得预测的标签 np.argmax(logits_onnx)61这确实与地面真实标签一致 clinc_enc[test][0][labels]61ONNX 模型与text-classification管道不兼容因此我们将创建一个模仿核心行为的自定义类 from scipy.special import softmaxclass OnnxPipeline:def __init__(self, model, tokenizer):self.model modelself.tokenizer tokenizerdef __call__(self, query):model_inputs self.tokenizer(query, return_tensorspt)inputs_onnx {k: v.cpu().detach().numpy()for k, v in model_inputs.items()}logits self.model.run(None, inputs_onnx)[0][0, :]probs softmax(logits)pred_idx np.argmax(probs).item()return [{label: intents.int2str(pred_idx), score: probs[pred_idx]}]然后我们可以测试这个简单的查询看看我们是否恢复了car_rental意图 pipe OnnxPipeline(onnx_model, tokenizer) pipe(query)[{label: car_rental, score: 0.7848334}]很好我们的流水线按预期工作。下一步是为 ONNX 模型创建性能基准测试。在这里我们可以借鉴我们与Per⁠formanceBenchmark类一起完成的工作只需重写compute_size()方法保留compute_accuracy()和time_pipeline()方法。我们需要重写compute_size()方法的原因是我们不能依赖state_dict和torch.save()来测量模型的大小因为onnx_model在技术上是一个 ONNXInferenceSession对象无法访问 PyTorch 的nn.Module的属性。无论如何结果逻辑很简单可以实现如下 class OnnxPerformanceBenchmark(PerformanceBenchmark):def __init__(self, *args, model_path, **kwargs):super().__init__(*args, **kwargs)self.model_path model_pathdef compute_size(self):size_mb Path(self.model_path).stat().st_size / (1024 * 1024)print(fModel size (MB) - {size_mb:.2f})return {size_mb: size_mb}通过我们的新基准测试让我们看看我们的蒸馏模型转换为 ONNX 格式后的性能 optim_type Distillation ORT pb OnnxPerformanceBenchmark(pipe, clinc[test], optim_type,model_pathonnx/model.onnx) perf_metrics.update(pb.run_benchmark())Model size (MB) - 255.88 Average latency (ms) - 21.02 \- 0.55 Accuracy on test set - 0.868plot_metrics(perf_metrics, optim_type)值得注意的是转换为 ONNX 格式并使用 ONNX Runtime 为我们的蒸馏模型即图中的“蒸馏”圈提供了延迟增益让我们看看是否可以通过添加量化来挤出更多性能。 与 PyTorch 类似ORT 提供了三种模型量化的方式动态量化、静态量化和量化感知训练。与 PyTorch 一样我们将对我们的蒸馏模型应用动态量化。在 ORT 中量化是通过quan⁠tize_dynamic()函数应用的该函数需要一个 ONNX 模型的路径进行量化一个目标路径来保存量化后的模型以及要将权重减少到的数据类型 from onnxruntime.quantization import quantize_dynamic, QuantTypemodel_input onnx/model.onnx model_output onnx/model.quant.onnx quantize_dynamic(model_input, model_output, weight_typeQuantType.QInt8)现在模型已经被量化让我们通过我们的基准测试运行它 onnx_quantized_model create_model_for_provider(model_output) pipe OnnxPipeline(onnx_quantized_model, tokenizer) optim_type Distillation ORT (quantized) pb OnnxPerformanceBenchmark(pipe, clinc[test], optim_type,model_pathmodel_output) perf_metrics.update(pb.run_benchmark())Model size (MB) - 64.20 Average latency (ms) - 9.24 \- 0.29 Accuracy on test set - 0.877plot_metrics(perf_metrics, optim_type)与 PyTorch 量化获得的模型相比ORT 量化已经将模型大小和延迟减少了约 30%蒸馏量化 blob。其中一个原因是 PyTorch 只优化nn.Linear模块而 ONNX 还量化了嵌入层。从图中我们还可以看到将 ORT 量化应用于我们的蒸馏模型与我们的 BERT 基线相比提供了近三倍的增益 这结束了我们对加速 Transformer 进行推断的技术的分析。我们已经看到诸如量化之类的方法通过降低表示的精度来减小模型大小。另一种减小大小的策略是彻底删除一些权重。这种技术称为权重修剪并且是下一节的重点。 使用权重修剪使模型更稀疏 到目前为止我们已经看到知识蒸馏和权重量化在产生更快的推断模型方面非常有效但在某些情况下您可能还对模型的内存占用有很强的约束。例如如果我们的产品经理突然决定我们的文本助手需要部署在移动设备上那么我们需要我们的意图分类器尽可能少地占用存储空间。为了完成我们对压缩方法的调查让我们看看如何通过识别和删除网络中最不重要的权重来减少模型参数的数量。 深度神经网络中的稀疏性 如图 8-10 所示修剪的主要思想是在训练过程中逐渐移除权重连接可能还有神经元使模型逐渐变得更稀疏。结果修剪后的模型具有更少的非零参数然后可以以紧凑的稀疏矩阵格式存储。修剪也可以与量化结合以获得进一步的压缩。 图 8-10。修剪前后的权重和神经元由 Song Han 提供 权重修剪方法 在数学上大多数权重修剪方法的工作方式是计算一个重要性分数矩阵然后按重要性选择前k百分比的权重 Top k () ij 1 if S ij in top k % 0 otherwise 实际上k 作为一个新的超参数用来控制模型中稀疏性的程度即权重为零值的比例。较低的 k 值对应着更稀疏的矩阵。从这些分数中我们可以定义一个掩码矩阵 在前向传播过程中用一些输入 x i 掩盖权重 W ij从而有效地创建一个稀疏的激活网络 a i a i ∑ k W ik M ik x k 正如“最佳脑外科医生”论文中所讨论的那样每种剪枝方法的核心都是一组需要考虑的问题 哪些权重应该被消除 剩余的权重应该如何调整以获得最佳性能 如何以计算有效的方式进行网络剪枝 这些问题的答案告诉了我们如何计算得分矩阵 因此让我们首先看一下最早和最流行的剪枝方法之一幅度剪枝。 幅度剪枝 顾名思义幅度剪枝根据权重的幅度计算得分 ∣ W ij ∣ 1≤j,j≤n然后从 Top k ( ) 中得出掩码。在文献中通常通过迭代的方式应用幅度剪枝首先训练模型学习哪些连接是重要的然后剪枝最不重要的权重。稀疏模型然后被重新训练并且重复这个过程直到达到期望的稀疏度。 这种方法的一个缺点是计算需求量大在每一步修剪中我们都需要将模型训练到收敛。因此通常最好逐渐增加初始稀疏度s i通常为零到一定步数N后的最终值s f。¹⁹ s t s f ( s i - s f ) 1 - t-t 0 NΔt 3 for t ∈ { t 0 , t 0 Δ t , … , t 0 N Δ t } 这里的想法是每隔Δ t步更新一次二进制掩码以允许被屏蔽的权重在训练过程中重新激活并从修剪过程中可能导致的任何精度损失中恢复过来。如图 8-11 所示立方因子意味着权重修剪的速率在早期阶段最高当冗余权重数量较大时并逐渐减小。 图 8-11。用于修剪的立方稀疏调度器。 幅度修剪的一个问题是它实际上是为纯监督学习而设计的其中每个权重的重要性与手头的任务直接相关。相比之下在迁移学习中权重的重要性主要由预训练阶段确定因此幅度修剪可能会移除对微调任务重要的连接。最近Hugging Face 的研究人员提出了一种称为移动修剪的自适应方法——让我们来看一下。²⁰ 移动修剪 移动修剪背后的基本思想是逐渐在微调过程中移除权重使模型逐渐变得更稀疏。关键的新颖之处在于在微调过程中权重和分数都是可学习的。因此与幅度修剪直接从权重派生如幅度修剪不同移动修剪中的分数是任意的并且通过梯度下降学习就像任何其他神经网络参数一样。这意味着在反向传播中我们还要跟踪损失L相对于分数S ij的梯度。 一旦学习了分数就很容易使用 Top k ( )生成二进制掩码。²¹ 运动剪枝背后的直觉是“移动”离零最远的权重是最重要的。换句话说正权重在精细调整期间增加负权重相反这相当于说分数随着权重远离零而增加。如图 8-12 所示这种行为与幅值剪枝不同后者选择离零最远的权重作为最重要的权重。 图 8-12。幅值剪枝左和运动剪枝右中移除的权重的比较 这两种剪枝方法之间的差异也在剩余权重的分布中显而易见。如图 8-13 所示幅值剪枝产生两个权重簇而运动剪枝产生更平滑的分布。 截至本书撰写时 Transformers 不支持开箱即用的剪枝方法。幸运的是有一个名为神经网络块运动剪枝的巧妙库实现了许多这些想法如果内存限制是一个问题我们建议查看它。 图 8-13。剩余权重的分布用于幅值剪枝MaP和运动剪枝MvP 结论 我们已经看到优化 Transformer 以部署到生产环境中涉及沿两个维度的压缩延迟和内存占用。从经过精细调整的模型开始我们应用了蒸馏、量化和 ORT 优化显著减少了这两者。特别是我们发现量化和 ORT 中的转换给出了最大的收益而付出的努力最小。 尽管剪枝是减少 Transformer 模型存储大小的有效策略但当前的硬件并未针对稀疏矩阵运算进行优化这限制了这种技术的实用性。然而这是一个活跃的研究领域到本书上市时许多这些限制可能已经得到解决。 那么接下来呢本章中的所有技术都可以应用到其他任务中比如问答、命名实体识别或语言建模。如果您发现自己难以满足延迟要求或者您的模型占用了所有的计算预算我们建议尝试其中之一。 在下一章中我们将摆脱性能优化探讨每个数据科学家的噩梦处理少量或没有标签的情况。 ¹ S. Larson 等人“意图分类和超出范围预测的评估数据集”2019 年。 ² 正如 Emmanuel Ameisen 在构建机器学习驱动的应用O’Reilly中所描述的业务或产品指标是最重要的考虑因素。毕竟如果您的模型不能解决业务关心的问题那么它的准确性就无关紧要。在本章中我们将假设您已经为应用程序定义了重要的指标并专注于优化模型指标。 ³ C. Buciluă等人“模型压缩”第 12 届 ACM SIGKDD 国际知识发现和数据挖掘会议论文集2006 年 8 月535-541https://doi.org/10.1145/1150402.1150464。 ⁴ G. Hinton, O. Vinyals 和 J. Dean“蒸馏神经网络中的知识”2015 年。 ⁵ W. Fedus, B. Zoph, and N. Shazeer“Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity”(2021)。 ⁶ Geoff Hinton 在一次演讲中创造了这个术语用来指代软化概率揭示了教师的隐藏知识的观察。 ⁷ 我们在第五章中也遇到了与文本生成相关的温度。 ⁸ V. Sanh 等人“DistilBERT, a Distilled Version of BERT: Smaller, Faster, Cheaper and Lighter”(2019)。 ⁹ Y. Kim and H. Awadalla“FastFormers: Highly Efficient Transformer Models for Natural Language Understanding”(2020)。 ¹⁰ 默认情况下Trainer 在进行分类任务微调时会寻找名为 labels 的列。您还可以通过指定 TrainingArguments 的 label_names 参数来覆盖此行为。 ¹¹ 对通用的精炼语言模型进行微调的方法有时被称为“任务不可知”精炼。 ¹² T. Akiba 等人“Optuna: A Next-Generation Hyperparameter Optimization Framework”(2019)。 ¹³ 仿射映射只是神经网络线性层中你熟悉的 y A x b 映射的一个花哨的名字。 ¹⁴ 还有一个名为 ONNX-ML 的标准专门为传统的机器学习模型如随机森林和 Scikit-learn 等框架设计。 ¹⁵ 其他流行的加速器包括NVIDIA 的 TensorRT和Apache TVM。 ¹⁶ 融合操作涉及将一个运算符通常是激活函数合并到另一个运算符中以便它们可以一起执行。例如假设我们想将激活函数 f 应用于矩阵乘积 A × B。通常乘积的结果需要写回到 GPU 存储器然后再计算激活函数。运算符融合允许我们一步计算 f ( A × B )。常量折叠是指在编译时评估常量表达式而不是在运行时。 ¹⁷ B. Hassibi and D. Stork“Second Order Derivatives for Network Pruning: Optimal Brain Surgeon,” Proceedings of the 5th International Conference on Neural Information Processing Systems (November 1992): 164–171https://papers.nips.cc/paper/1992/hash/303ed4c69846ab36c2904d3ba8573050-Abstract.html。 ¹⁸ S. Han 等人“Learning Both Weights and Connections for Efficient Neural Networks”(2015)。 ¹⁹ M. Zhu and S. Gupta“To Prune, or Not to Prune: Exploring the Efficacy of Pruning for Model Compression”(2017)。 ²⁰ V. Sanh, T. Wolf, and A.M. Rush“Movement Pruning: Adaptive Sparsity by Fine-Tuning”(2020)。 ²¹ 还有一种“软”版本的移动修剪其中不是选择权重的前k %而是使用全局阈值τ来定义二进制掩码 ( τ )。 第九章处理少量或没有标签 有一个问题深深地植根在每个数据科学家的脑海中通常是他们在新项目开始时首先问的事情是否有任何标记的数据往往情况是“没有”或“有一点”然后客户期望你的团队的高级机器学习模型仍然能够表现良好。由于在非常小的数据集上训练模型通常不会产生良好的结果一个明显的解决方案是标注更多的数据。然而这需要时间而且可能非常昂贵特别是如果每个标注都需要领域专业知识来验证。 幸运的是有几种方法非常适合处理少量或没有标签的情况你可能已经熟悉其中一些比如零样本或少样本学习正如 GPT-3 仅凭几十个例子就能执行各种任务的能力所示。 一般来说最佳的方法取决于任务、可用数据量以及该数据中有多少是标记的。图 9-1 中显示的决策树可以帮助我们在选择最合适的方法的过程中进行指导。 图 9-1。在缺乏大量标记数据的情况下可以用来提高模型性能的几种技术 让我们逐步走过这个决策树 你有标记的数据吗 即使只有少量标记的样本也可以对哪种方法最有效产生影响。如果根本没有标记的数据可以从零样本学习方法开始这通常可以为后续工作奠定坚实的基础。 有多少标签 如果有标记的数据可用决定因素是有多少。如果你有大量的训练数据可用你可以使用第二章中讨论的标准微调方法。 你有未标记的数据吗 如果你只有少量标记的样本如果你可以访问大量未标记的数据这将非常有帮助。如果你可以访问未标记的数据你可以在训练分类器之前使用它来微调语言模型或者你可以使用更复杂的方法如无监督数据增强UDA或不确定性感知的自我训练UST。¹ 如果你没有任何未标记的数据可用你就没有标注更多数据的选择。在这种情况下你可以使用少样本学习或者使用预训练语言模型的嵌入来执行最近邻搜索。 在本章中我们将通过解决许多支持团队面临的常见问题来逐步走过这个决策树这些团队使用像Jira或GitHub这样的问题跟踪器来帮助他们的用户根据问题的描述为问题打标签。这些标签可能定义问题类型、导致问题的产品或者负责处理报告问题的团队。自动化这个过程可以对生产力产生重大影响并使支持团队能够专注于帮助他们的用户。作为一个运行的示例我们将使用与一个流行的开源项目相关的 GitHub 问题 Transformers现在让我们来看看这些问题中包含了哪些信息如何构建任务以及如何获取数据。 注 本章介绍的方法对于文本分类非常有效但对于更复杂的任务如命名实体识别、问答或摘要可能需要其他技术如数据增强。 构建 GitHub 问题标记器 如果您导航到 Transformers 存储库的Issues 标签您会发现像图 9-2 中所示的问题其中包含标题、描述和一组标签这些标签或标签表征了问题。这表明了一个自然的方式来构建监督学习任务给定一个问题的标题和描述预测一个或多个标签。由于每个问题可以分配一个可变数量的标签这意味着我们正在处理一个多标签文本分类问题。这通常比我们在第二章中遇到的多类问题更具挑战性那里每个推文只分配给一个情感。 图 9-2。 Transformers 存储库上的典型 GitHub 问题 现在我们已经看到了 GitHub 问题的样子让我们看看如何下载它们以创建我们的数据集。 获取数据 为了获取存储库的所有问题我们将使用GitHub REST API来轮询Issues端点。这个端点返回一个 JSON 对象列表每个对象包含有关问题的大量字段包括其状态打开或关闭谁打开了问题以及我们在图 9-2 中看到的标题、正文和标签。 由于获取所有问题需要一些时间我们在本书的GitHub 存储库中包含了一个github-issues-transformers.jsonl文件以及一个fetch_issues()函数您可以使用它来自行下载问题。 注意 GitHub REST API 将拉取请求视为问题因此我们的数据集包含两者的混合。为了保持简单我们将为两种问题类型开发我们的分类器尽管在实践中您可能考虑构建两个单独的分类器以更精细地控制模型的性能。 现在我们知道如何获取数据让我们来看看如何清理数据。 准备数据 一旦我们下载了所有问题我们可以使用 Pandas 加载它们 import pandas as pddataset_url https://git.io/nlp-with-transformers df_issues pd.read_json(dataset_url, linesTrue) print(fDataFrame shape: {df_issues.shape})DataFrame shape: (9930, 26)我们的数据集中有近 10000 个问题通过查看单个行我们可以看到从 GitHub API 检索到的信息包含许多字段如 URL、ID、日期、用户、标题、正文以及标签 cols [url, id, title, user, labels, state, created_at, body] df_issues.loc[2, cols].to_frame()2urlhttps://api.github.com/repos/huggingface/trans…id849529761title[DeepSpeed] ZeRO stage 3 integration: getting …user{‘login’: ’stas00’, ‘id’: 10676103, ‘node_id’:…labels[{id’: 2659267025, ‘node_id’: ‘MDU6TGFiZWwyNj…stateopencreated_at2021-04-02 23:40:42body**[This is not yet alive, preparing for the re… labels列是我们感兴趣的东西每一行都包含一个关于每个标签的元数据的 JSON 对象列表 [{id:2659267025,node_id:MDU6TGFiZWwyNjU5MjY3MDI1,url:https://api.github.com/repos/huggingface...,name:DeepSpeed,color:4D34F7,default:false,description:} ]对于我们的目的我们只对每个标签对象的name字段感兴趣因此让我们用标签名称覆盖labels列 df_issues[labels] (df_issues[labels].apply(lambda x: [meta[name] for meta in x])) df_issues[[labels]].head()labels0[]1[]2[DeepSpeed]3[]4[] 现在labels列中的每一行都是 GitHub 标签的列表因此我们可以计算每一行的长度以找到每个问题的标签数量 df_issues[labels].apply(lambda x : len(x)).value_counts().to_frame().T012345labels64403057305100253 这表明大多数问题没有标签或只有一个标签而更少的问题有多个标签。接下来让我们来看看数据集中前 10 个最频繁的标签。在 Pandas 中我们可以通过“爆炸”labels列来做到这一点使列表中的每个标签成为一行然后简单地计算每个标签的出现次数 df_counts df_issues[labels].explode().value_counts() print(fNumber of labels: {len(df_counts)}) # Display the top-8 label categories df_counts.to_frame().head(8).TNumber of labels: 65wontfixmodel cardCore: TokenizationNew modelCore: ModelingHelp wantedGood First IssueUsagelabels22846491069864525046 我们可以看到数据集中有 65 个唯一的标签并且类别非常不平衡wontfix和model card是最常见的标签。为了使分类问题更易处理我们将专注于构建一部分标签的标签器。例如一些标签如Good First Issue或Help Wanted可能非常难以从问题的描述中预测而其他一些标签如model card可以通过简单的规则进行分类以检测何时在 Hugging Face Hub 上添加了模型卡。 以下代码将过滤数据集以便我们将使用的标签子集以及对名称的标准化使其更易于阅读 label_map {Core: Tokenization: tokenization,New model: new model,Core: Modeling: model training,Usage: usage,Core: Pipeline: pipeline,TensorFlow: tensorflow or tf,PyTorch: pytorch,Examples: examples,Documentation: documentation}def filter_labels(x):return [label_map[label] for label in x if label in label_map]df_issues[labels] df_issues[labels].apply(filter_labels) all_labels list(label_map.values())现在让我们来看一下新标签的分布 df_counts df_issues[labels].explode().value_counts() df_counts.to_frame().Ttokenizationnew modelmodel trainingusagepipelinetensorflow or tfpytorchdocumentationexamples标签1069864464241372824 在本章的后面我们会发现将未标记的问题视为单独的训练拆分是有用的因此让我们创建一个新列指示问题是否被标记 df_issues[split] unlabeled mask df_issues[labels].apply(lambda x: len(x)) 0 df_issues.loc[mask, split] labeled df_issues[split].value_counts().to_frame()拆分未标记9489标记441 现在让我们来看一个例子 for column in [title, body, labels]:print(f{column}: {df_issues[column].iloc[26][:500]}\n)title: Add new CANINE modelbody: # New model addition## Model descriptionGoogle recently proposed a new **C**haracter **A**rchitecture with **N**otokenization **I**n **N**eural **E**ncoders architecture (CANINE). Not onlythe title is exciting:Pipelined NLP systems have largely been superseded by end-to-end neuralmodeling, yet nearly all commonly-used models still require an explicittokenization step. While recent tokenization approaches based on data-derivedsubword lexicons are less brittle than manually enlabels: [new model] 在这个例子中提出了一个新的模型架构因此new model标签是有意义的。我们还可以看到title包含了对我们的分类器有用的信息因此让我们将其与body字段中的问题描述连接起来 df_issues[text] (df_issues.apply(lambda x: x[title] \n\n x[body], axis1))在查看数据的其余部分之前让我们检查数据中是否有重复项并使用drop_duplicates()方法删除它们 len_before len(df_issues) df_issues df_issues.drop_duplicates(subsettext) print(fRemoved {(len_before-len(df_issues))/len_before:.2%} duplicates.)Removed 1.88% duplicates.我们可以看到我们的数据集中有一些重复的问题但它们只占很小的比例。与其他章节一样快速查看我们文本中的单词数量也是一个好主意以查看当我们截断到每个模型的上下文大小时是否会丢失太多信息 import numpy as np import matplotlib.pyplot as plt(df_issues[text].str.split().apply(len).hist(binsnp.linspace(0, 500, 50), gridFalse, edgecolorC0)) plt.title(Words per issue) plt.xlabel(Number of words) plt.ylabel(Number of issues) plt.show()分布具有许多文本数据集的长尾特征。大多数文本都相当短但也有超过 500 个单词的问题。通常会有一些非常长的问题特别是当错误消息和代码片段与它们一起发布时。鉴于大多数转换器模型的上下文大小为 512 个标记或更大截断少数长问题不太可能影响整体性能。现在我们已经探索和清理了我们的数据集最后要做的是定义我们的训练和验证集以对我们的分类器进行基准测试。让我们看看如何做到这一点。 创建训练集 对于多标签问题创建训练和验证集会有些棘手因为并不是所有标签都能保证平衡。然而可以进行近似处理我们可以使用专门为此目的设置的Scikit-multilearn 库。我们需要做的第一件事是将我们的标签集如pytorch和tokenization转换为模型可以处理的格式。在这里我们可以使用 Scikit-learn 的MultiLabelBinarizer类它接受一个标签名称列表并创建一个向量其中缺失的标签为零存在的标签为一。我们可以通过将MultiLabelBinarizer拟合到all_labels上来测试这一点以学习从标签名称到 ID 的映射如下所示 from sklearn.preprocessing import MultiLabelBinarizermlb MultiLabelBinarizer() mlb.fit([all_labels]) mlb.transform([[tokenization, new model], [pytorch]])array([[0, 0, 0, 1, 0, 0, 0, 1, 0],[0, 0, 0, 0, 0, 1, 0, 0, 0]])在这个简单的例子中我们可以看到第一行有两个对应于tokenization和new model标签的 1而第二行只有一个对应于pytorch的命中。 为了创建拆分我们可以使用 Scikit-multilearn 的iterative_train_test_split()函数该函数会迭代创建平衡标签的训练/测试拆分。我们将其包装在一个可以应用于DataFrame的函数中。由于该函数期望一个二维特征矩阵因此在进行拆分之前我们需要为可能的索引添加一个维度 from skmultilearn.model_selection import iterative_train_test_splitdef balanced_split(df, test_size0.5):ind np.expand_dims(np.arange(len(df)), axis1)labels mlb.transform(df[labels])ind_train, _, ind_test, _ iterative_train_test_split(ind, labels,test_size)return df.iloc[ind_train[:, 0]], df.iloc[ind_test[:,0]]有了balanced_split()函数我们可以将数据分成监督和非监督数据集然后为监督部分创建平衡的训练、验证和测试集 from sklearn.model_selection import train_test_splitdf_clean df_issues[[text, labels, split]].reset_index(dropTrue).copy() df_unsup df_clean.loc[df_clean[split] unlabeled, [text, labels]] df_sup df_clean.loc[df_clean[split] labeled, [text, labels]]np.random.seed(0) df_train, df_tmp balanced_split(df_sup, test_size0.5) df_valid, df_test balanced_split(df_tmp, test_size0.5)最后让我们创建一个DatasetDict包含所有的拆分这样我们就可以轻松地对数据集进行标记并与Trainer集成。在这里我们将使用巧妙的from_pandas()方法直接从相应的 PandasDataFrame中加载每个拆分 from datasets import Dataset, DatasetDictds DatasetDict({train: Dataset.from_pandas(df_train.reset_index(dropTrue)),valid: Dataset.from_pandas(df_valid.reset_index(dropTrue)),test: Dataset.from_pandas(df_test.reset_index(dropTrue)),unsup: Dataset.from_pandas(df_unsup.reset_index(dropTrue))})看起来不错最后要做的事情就是创建一些训练切片这样我们就可以评估每个分类器的性能作为训练集大小的函数。 创建训练切片 数据集具有我们想要在本章中调查的两个特征稀疏标记数据和多标签分类。训练集只包含 220 个示例进行训练即使使用迁移学习也是一个挑战。为了深入研究本章中每种方法在少量标记数据下的表现我们还将创建训练数据的切片其中包含更少的样本。然后我们可以绘制样本数量与性能并调查各种情况。我们将从每个标签仅有八个样本开始并逐渐增加直到切片覆盖整个训练集使用iterative_train_test_split()函数 np.random.seed(0) all_indices np.expand_dims(list(range(len(ds[train]))), axis1) indices_pool all_indices labels mlb.transform(ds[train][labels]) train_samples [8, 16, 32, 64, 128] train_slices, last_k [], 0for i, k in enumerate(train_samples):# Split off samples necessary to fill the gap to the next split sizeindices_pool, labels, new_slice, _ iterative_train_test_split(indices_pool, labels, (k-last_k)/len(labels))last_k kif i0: train_slices.append(new_slice)else: train_slices.append(np.concatenate((train_slices[-1], new_slice)))# Add full dataset as last slice train_slices.append(all_indices), train_samples.append(len(ds[train])) train_slices [np.squeeze(train_slice) for train_slice in train_slices]请注意这种迭代方法只是大致将样本分割成所需的大小因为在给定的拆分大小下不总是可能找到一个平衡的拆分 print(Target split sizes:) print(train_samples) print(Actual split sizes:) print([len(x) for x in train_slices])Target split sizes: [8, 16, 32, 64, 128, 223] Actual split sizes: [10, 19, 36, 68, 134, 223]我们将使用指定的拆分大小作为以下图表的标签。太好了我们终于将我们的数据集准备成了训练拆分接下来让我们看看如何训练一个强大的基线模型 实施一个朴素贝叶斯基线 每当你开始一个新的 NLP 项目时实施一组强大的基线总是一个好主意。这样做有两个主要原因 基于正则表达式、手工制作的规则或非常简单的模型的基线可能已经非常有效地解决了问题。在这些情况下没有理由使用 transformers 等大型工具这些工具通常在生产环境中更复杂。 基线提供了快速检查当你探索更复杂的模型时。例如假设你训练 BERT-large 并在验证集上获得 80%的准确率。你可能会认为这是一个难题然后结束了。但是如果你知道一个简单的分类器如逻辑回归获得了 95%的准确率呢那就会引起你的怀疑并促使你调试你的模型。 让我们从训练一个基线模型开始我们的分析。对于文本分类一个很好的基线是朴素贝叶斯分类器因为它非常简单、训练速度快并且对输入的扰动相当稳健。Scikit-learn 的朴素贝叶斯实现不直接支持多标签分类但幸运的是我们可以再次使用 Scikit-multilearn 库将问题转化为一个一对多的分类任务其中我们为L标签训练L个二元分类器。首先让我们使用一个多标签二值化器在我们的训练集中创建一个新的label_ids列。我们可以使用map()函数一次性处理所有的处理 def prepare_labels(batch):batch[label_ids] mlb.transform(batch[labels])return batchds ds.map(prepare_labels, batchedTrue)为了衡量我们分类器的性能我们将使用微观和宏观F[1]-scores前者跟踪频繁标签的性能后者忽略频率跟踪所有标签的性能。由于我们将评估每个模型在不同大小的训练拆分上让我们创建一个defaultdict其中包含一个列表用于存储每个拆分的分数 from collections import defaultdictmacro_scores, micro_scores defaultdict(list), defaultdict(list)现在我们终于准备好训练我们的基线了下面是训练模型和评估我们的分类器在不断增加的训练集大小上的代码 from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report from skmultilearn.problem_transform import BinaryRelevance from sklearn.feature_extraction.text import CountVectorizerfor train_slice in train_slices:# Get training slice and test datads_train_sample ds[train].select(train_slice)y_train np.array(ds_train_sample[label_ids])y_test np.array(ds[test][label_ids])# Use a simple count vectorizer to encode our texts as token countscount_vect CountVectorizer()X_train_counts count_vect.fit_transform(ds_train_sample[text])X_test_counts count_vect.transform(ds[test][text])# Create and train our model!classifier BinaryRelevance(classifierMultinomialNB())classifier.fit(X_train_counts, y_train)# Generate predictions and evaluatey_pred_test classifier.predict(X_test_counts)clf_report classification_report(y_test, y_pred_test, target_namesmlb.classes_, zero_division0,output_dictTrue)# Store metricsmacro_scores[Naive Bayes].append(clf_report[macro avg][f1-score])micro_scores[Naive Bayes].append(clf_report[micro avg][f1-score])这段代码块中有很多内容让我们来解开它。首先我们获取训练切片并对标签进行编码。然后我们使用计数向量化器对文本进行编码简单地创建一个与词汇量大小相同的向量其中每个条目对应于文本中标记出现的频率。这被称为词袋方法因为所有关于单词顺序的信息都丢失了。然后我们训练分类器并使用测试集上的预测来通过分类报告得到微观和宏观F[1]-分数。 通过以下辅助函数我们可以绘制这个实验的结果 import matplotlib.pyplot as pltdef plot_metrics(micro_scores, macro_scores, sample_sizes, current_model):fig, (ax0, ax1) plt.subplots(1, 2, figsize(10, 4), shareyTrue)for run in micro_scores.keys():if run current_model:ax0.plot(sample_sizes, micro_scores[run], labelrun, linewidth2)ax1.plot(sample_sizes, macro_scores[run], labelrun, linewidth2)else:ax0.plot(sample_sizes, micro_scores[run], labelrun,linestyledashed)ax1.plot(sample_sizes, macro_scores[run], labelrun,linestyledashed)ax0.set_title(Micro F1 scores)ax1.set_title(Macro F1 scores)ax0.set_ylabel(Test set F1 score)ax0.legend(loclower right)for ax in [ax0, ax1]:ax.set_xlabel(Number of training samples)ax.set_xscale(log)ax.set_xticks(sample_sizes)ax.set_xticklabels(sample_sizes)ax.minorticks_off()plt.tight_layout()plt.show()plot_metrics(micro_scores, macro_scores, train_samples, Naive Bayes)请注意我们在对数刻度上绘制样本数量。从图中我们可以看到随着训练样本数量的增加微观和宏观F[1]-分数都有所提高。由于每个切片可能具有不同的类分布因此在训练样本很少的情况下结果也稍微有些嘈杂。然而这里重要的是趋势所以现在让我们看看这些结果与基于 Transformer 的方法相比如何 使用无标记数据 我们将考虑的第一种技术是零样本分类这在没有任何标记数据的情况下非常适用。这在行业中非常常见可能是因为没有带标签的历史数据或者因为获取数据的标签很困难。在本节中我们会有点作弊因为我们仍然会使用测试数据来衡量性能但我们不会使用任何数据来训练模型否则与后续方法的比较将会很困难。 零样本分类的目标是利用预训练模型在任务特定语料库上没有进行额外的微调。为了更好地了解这种工作原理回想一下像 BERT 这样的语言模型是预训练的用于在成千上万本书和大量维基百科转储中预测文本中的屏蔽标记。为了成功预测缺失的标记模型需要了解上下文中的主题。我们可以尝试欺骗模型通过提供一个句子来为我们对文档进行分类 “这一部分是关于主题[MASK]的。” 由于这是数据集中自然出现的文本模型应该能够合理地对文档的主题提出建议。² 让我们通过以下玩具问题进一步说明这一点假设你有两个孩子一个喜欢有汽车的电影而另一个更喜欢有动物的电影。不幸的是他们已经看过你知道的所有电影所以你想建立一个函数告诉你一个新电影的主题是什么。自然地你会转向 Transformer 来完成这个任务。首先要尝试的是在fill-mask管道中加载 BERT-base该管道使用屏蔽语言模型来预测屏蔽标记的内容 from transformers import pipelinepipe pipeline(fill-mask, modelbert-base-uncased)接下来让我们构建一个小电影描述并在其中添加一个带有屏蔽词的提示。提示的目标是引导模型帮助我们进行分类。fill-mask管道返回填充屏蔽位置的最有可能的标记 movie_desc The main characters of the movie madacascar \ are a lion, a zebra, a giraffe, and a hippo. prompt The movie is about [MASK].output pipe(movie_desc prompt) for element in output:print(fToken {element[token_str]}:\t{element[score]:.3f}%)Token animals: 0.103% Token lions: 0.066% Token birds: 0.025% Token love: 0.015% Token hunting: 0.013%显然模型只预测与动物相关的标记。我们也可以反过来而不是获取最有可能的标记我们可以查询管道获取几个给定标记的概率。对于这个任务我们可以选择cars和animals所以我们可以将它们作为目标传递给管道 output pipe(movie_desc prompt, targets[animals, cars]) for element in output:print(fToken {element[token_str]}:\t{element[score]:.3f}%)Token animals: 0.103% Token cars: 0.001%毫不奇怪对于标记为cars的预测概率要远远小于animals。让我们看看这是否也适用于更接近汽车的描述 movie_desc In the movie transformers aliens \ can morph into a wide range of vehicles.output pipe(movie_desc prompt, targets[animals, cars]) for element in output:print(fToken {element[token_str]}:\t{element[score]:.3f}%)Token cars: 0.139% Token animals: 0.006%它确实可以这只是一个简单的例子如果我们想确保它运行良好我们应该进行彻底的测试但它说明了本章讨论的许多方法的关键思想找到一种方法使预训练模型适应另一个任务而无需对其进行训练。在这种情况下我们设置了一个提示其中包含一个掩码以便我们可以直接使用掩码语言模型进行分类。让我们看看是否可以通过调整一个在更接近文本分类的任务上进行了微调的模型来做得更好自然语言推理NLI。 使用掩码语言模型进行分类是一个不错的技巧但是我们可以通过使用一个在更接近分类的任务上训练过的模型来做得更好。有一个称为文本蕴涵的巧妙代理任务符合要求。在文本蕴涵中模型需要确定两个文本段落是否可能相互跟随或相互矛盾。模型通常是使用诸如多种体裁 NLI 语料库MNLI或跨语言 NLI 语料库XNLI等数据集进行蕴涵和矛盾的检测。³ 这些数据集中的每个样本由三部分组成前提、假设和标签标签可以是蕴涵、中性或矛盾中的一个。当假设文本在前提下必然为真时分配蕴涵标签。当假设在前提下必然为假或不合适时使用矛盾标签。如果这两种情况都不适用则分配中性标签。参见表 9-1 中的示例。 表 9-1。MLNI 数据集中的三个类 前提假设标签他最喜欢的颜色是蓝色。他喜欢重金属音乐。中性她觉得这个笑话很搞笑。她认为这个笑话一点都不好笑。矛盾这所房子最近建造。这所房子是新的。蕴涵 现在事实证明我们可以劫持一个在 MNLI 数据集上训练的模型构建一个分类器而无需任何标签关键思想是将我们希望分类的文本视为前提然后将假设制定为 “这个例子是关于{label}的。” 在这里我们插入标签的类名。蕴涵分数告诉我们前提很可能是关于那个主题的我们可以依次为任意数量的类运行这个。这种方法的缺点是我们需要为每个类执行一次前向传播这使得它比标准分类器效率低。另一个稍微棘手的方面是标签名称的选择可能对准确性产生很大影响通常最好选择具有语义含义的标签。例如如果标签只是Class 1模型就不知道这可能意味着什么以及这是否构成了矛盾或蕴涵。 Transformers 具有内置的零样本分类 MNLI 模型。我们可以通过管道初始化它如下 from transformers import pipelinepipe pipeline(zero-shot-classification, device0)设置device0确保模型在 GPU 上运行而不是默认的 CPU以加快推理速度。要对文本进行分类我们只需要将其传递给管道并附上标签名称。此外我们可以设置multi_labelTrue以确保返回所有分数而不仅仅是单标签分类的最大值 sample ds[train][0] print(fLabels: {sample[labels]}) output pipe(sample[text], all_labels, multi_labelTrue) print(output[sequence][:400]) print(\nPredictions:)for label, score in zip(output[labels], output[scores]):print(f{label}, {score:.2f})Labels: [new model] Add new CANINE model# New model addition## Model descriptionGoogle recently proposed a new **C**haracter **A**rchitecture with **N**o tokenization **I**n **N**eural **E**ncoders architecture (CANINE). Not only the title is exciting: Pipelined NLP systems have largely been superseded by end-to-end neural modeling, yet nearly all commonly-used models still require an explicit tokeniPredictions: new model, 0.98 tensorflow or tf, 0.37 examples, 0.34 usage, 0.30 pytorch, 0.25 documentation, 0.25 model training, 0.24 tokenization, 0.17 pipeline, 0.16 注意 由于我们使用的是子词分词器我们甚至可以将代码传递给模型分词可能不太高效因为零样本管道的预训练数据集只包含了一小部分代码片段但由于代码也由许多自然词组成这并不是一个大问题。此外代码块可能包含重要信息例如框架PyTorch 或 TensorFlow。 我们可以看到模型非常确信这段文本是关于一个新模型的但它也为其他标签产生了相对较高的分数。零射击分类的一个重要方面是我们所处的领域。我们在这里处理的文本非常技术化大部分是关于编码的这使它们与 MNLI 数据集中的原始文本分布相当不同。因此这对于模型来说是一个具有挑战性的任务并不奇怪它可能对某些领域的工作效果比其他领域要好得多这取决于它们与训练数据的接近程度。 让我们编写一个函数通过零射击管道将单个示例传递并通过运行map()将其扩展到整个验证集 def zero_shot_pipeline(example):output pipe(example[text], all_labels, multi_labelTrue)example[predicted_labels] output[labels]example[scores] output[scores]return exampleds_zero_shot ds[valid].map(zero_shot_pipeline)现在我们有了分数下一步是确定应该为每个示例分配哪组标签。我们可以尝试一些选项 定义一个阈值并选择高于阈值的所有标签。 选择具有最高分数的前k个标签。 为了帮助我们确定哪种方法最好让我们编写一个get_preds()函数应用其中一种方法来获取预测 def get_preds(example, thresholdNone, topkNone):preds []if threshold:for label, score in zip(example[predicted_labels], example[scores]):if score threshold:preds.append(label)elif topk:for i in range(topk):preds.append(example[predicted_labels][i])else:raise ValueError(Set either threshold or topk.)return {pred_label_ids: list(np.squeeze(mlb.transform([preds])))}接下来让我们编写第二个函数get_clf_report()它从具有预测标签的数据集中返回 Scikit-learn 分类报告 def get_clf_report(ds):y_true np.array(ds[label_ids])y_pred np.array(ds[pred_label_ids])return classification_report(y_true, y_pred, target_namesmlb.classes_, zero_division0,output_dictTrue)有了这两个函数让我们从增加几个值的k开始然后绘制验证集上的微观和宏观* F * [1]-分数 macros, micros [], [] topks [1, 2, 3, 4] for topk in topks:ds_zero_shot ds_zero_shot.map(get_preds, batchedFalse,fn_kwargs{topk: topk})clf_report get_clf_report(ds_zero_shot)micros.append(clf_report[micro avg][f1-score])macros.append(clf_report[macro avg][f1-score])plt.plot(topks, micros, labelMicro F1) plt.plot(topks, macros, labelMacro F1) plt.xlabel(Top-k) plt.ylabel(F1-score) plt.legend(locbest) plt.show()从图中我们可以看到通过选择每个示例的最高分数的标签top 1获得了最佳结果。这也许并不奇怪因为我们数据集中的大多数示例只有一个标签。现在让我们将其与设置阈值进行比较这样我们可能可以对每个示例进行多个标签的预测 macros, micros [], [] thresholds np.linspace(0.01, 1, 100) for threshold in thresholds:ds_zero_shot ds_zero_shot.map(get_preds,fn_kwargs{threshold: threshold})clf_report get_clf_report(ds_zero_shot)micros.append(clf_report[micro avg][f1-score])macros.append(clf_report[macro avg][f1-score])plt.plot(thresholds, micros, labelMicro F1) plt.plot(thresholds, macros, labelMacro F1) plt.xlabel(Threshold) plt.ylabel(F1-score) plt.legend(locbest) plt.show()best_t, best_micro thresholds[np.argmax(micros)], np.max(micros) print(fBest threshold (micro): {best_t} with F1-score {best_micro:.2f}.) best_t, best_macro thresholds[np.argmax(macros)], np.max(macros) print(fBest threshold (micro): {best_t} with F1-score {best_macro:.2f}.)Best threshold (micro): 0.75 with F1-score 0.46. Best threshold (micro): 0.72 with F1-score 0.42.这种方法的表现略逊于 top-1 的结果但我们可以在这张图中清楚地看到精确度/召回率的权衡。如果我们将阈值设置得太低那么会有太多的预测这会导致精确度低。如果我们将阈值设置得太高那么我们几乎不会进行任何预测这会产生低召回率。从图中我们可以看到阈值约为 0.8 是两者之间的最佳平衡点。 由于 top-1 方法表现最佳让我们使用它来将零射击分类与朴素贝叶斯在测试集上进行比较 ds_zero_shot ds[test].map(zero_shot_pipeline) ds_zero_shot ds_zero_shot.map(get_preds, fn_kwargs{topk: 1}) clf_report get_clf_report(ds_zero_shot) for train_slice in train_slices:macro_scores[Zero Shot].append(clf_report[macro avg][f1-score])micro_scores[Zero Shot].append(clf_report[micro avg][f1-score])plot_metrics(micro_scores, macro_scores, train_samples, Zero Shot)将零射击管道与基线进行比较我们观察到两件事 如果我们少于 50 个有标签的样本零射击管道将轻松胜过基线。 即使超过 50 个样本零射击管道的性能在考虑微观和宏观* F * [1]-分数时仍然优越。微观* F * [1]-分数的结果告诉我们基线在频繁类别上表现良好而零射击管道在这些类别上表现出色因为它不需要任何示例来学习。 注意 您可能会注意到本节中存在一个小悖论尽管我们谈论处理没有标签的情况但我们仍然使用验证集和测试集。我们使用它们来展示不同的技术并使结果可以相互比较。即使在实际用例中收集一些有标签的示例进行快速评估也是有意义的。重要的一点是我们没有根据数据调整模型的参数相反我们只是调整了一些超参数。 如果您发现在自己的数据集上难以获得良好的结果可以尝试以下几种方法来改进零射击管道 管道的工作方式使其对标签的名称非常敏感。如果名称不太合理或与文本不容易联系起来那么管道可能表现不佳。可以尝试使用不同的名称或并行使用几个名称并在额外的步骤中对它们进行聚合。 另一件你可以改进的事情是假设的形式。默认情况下是 hypothesisThis is example is about {}但你可以传递任何其他文本到管道中。根据使用情况这可能会提高性能。 现在让我们转向我们有少数标记示例可以用来训练模型的情况。 使用少数标签 在大多数 NLP 项目中您至少会有一些标记的示例。标签可能直接来自客户或跨公司团队或者您可能决定自己注释一些示例。即使对于以前的方法我们也需要一些标记的示例来评估零-shot 方法的效果。在本节中我们将看看如何最好地利用我们拥有的少数宝贵的标记示例。让我们首先看看一种称为数据增强的技术它可以帮助我们扩大我们拥有的少量标记数据。 数据增强 在小数据集上提高文本分类器性能的一种简单但有效的方法是应用数据增强技术从现有数据中生成新的训练样本。这是计算机视觉中的常见策略其中图像被随机扰动而不改变数据的含义例如稍微旋转的猫仍然是一只猫。对于文本来说数据增强有些棘手因为扰动单词或字符可能会完全改变含义。例如两个问题“大象比老鼠重吗”和“老鼠比大象重吗”只有一个词交换但答案相反。然而如果文本包含超过几句话就像我们的 GitHub 问题一样那么这些类型的转换引入的噪音通常不会影响标签。实际上通常使用两种类型的数据增强技术 回译 取源语言中的文本使用机器翻译将其翻译成一个或多个目标语言然后将其翻译回源语言。回译通常适用于高资源语言或不包含太多领域特定词汇的语料库。 标记扰动 给定训练集中的文本随机选择并执行简单的转换如随机同义词替换、单词插入、交换或删除。⁠⁴ 这些转换的示例显示在表 9-2 中。有关 NLP 的其他数据增强技术的详细列表我们建议阅读 Amit Chaudhary 的博文“NLP 中数据增强的视觉调查”。 表 9-2。文本的不同类型的数据增强技术 增强句子无即使你打败我梅杰特龙其他人也会起来打败你的暴政同义词替换即使你杀了我梅杰特龙其他人将证明打败你的暴政随机插入即使你打败我梅杰特龙其他人类也会起来打败你的暴政随机交换即使你打败我梅杰特龙其他人也会起来打败你的暴政随机删除即使你我梅杰特龙其他人也会起来打败暴政回译德语即使你打败我其他人也会起来打败你的暴政 您可以使用像M2M100这样的机器翻译模型来实现回译而像NlpAug和TextAttack这样的库提供了各种用于标记扰动的方法。在本节中我们将专注于使用同义词替换因为它简单易行并且能够传达数据增强背后的主要思想。 我们将使用 NlpAug 中的ContextualWordEmbsAug增强器来利用 DistilBERT 的上下文词嵌入进行同义词替换。让我们从一个简单的例子开始 from transformers import set_seed import nlpaug.augmenter.word as nawset_seed(3) aug naw.ContextualWordEmbsAug(model_pathdistilbert-base-uncased,devicecpu, actionsubstitute)text Transformers are the most popular toys print(fOriginal text: {text}) print(fAugmented text: {aug.augment(text)})Original text: Transformers are the most popular toys Augmented text: transformersthe most popular toys在这里我们可以看到单词“are”已被替换为撇号以生成一个新的合成训练示例。我们可以将这种增强包装在一个简单的函数中如下所示 def augment_text(batch, transformations_per_example1):text_aug, label_ids [], []for text, labels in zip(batch[text], batch[label_ids]):text_aug [text]label_ids [labels]for _ in range(transformations_per_example):text_aug [aug.augment(text)]label_ids [labels]return {text: text_aug, label_ids: label_ids}现在当我们将这个函数传递给map()方法时我们可以使用transformations_per_example参数生成任意数量的新示例。我们可以在我们的代码中使用这个函数来训练朴素贝叶斯分类器只需在选择切片后添加一行 ds_train_sample ds_train_sample.map(augment_text, batchedTrue,remove_columnsds_train_sample.column_names).shuffle(seed42)包括这一点并重新运行分析会产生如图所示的图表 plot_metrics(micro_scores, macro_scores, train_samples, Naive Bayes Aug)从图中可以看出少量数据增强可以将朴素贝叶斯分类器的F[1]-分数提高约 5 个点并且一旦我们有大约 170 个训练样本它就会超过零-shot 管道的宏分数。现在让我们来看一个基于使用大型语言模型嵌入的方法。 将嵌入用作查找表 已经证明像 GPT-3 这样的大型语言模型在解决数据有限的任务方面表现出色。原因是这些模型学习了有用的文本表示跨越许多维度编码信息如情感、主题、文本结构等。因此大型语言模型的嵌入可以用于开发语义搜索引擎找到相似的文档或评论甚至对文本进行分类。 在本节中我们将创建一个文本分类器它的模型是基于OpenAI API 分类端点。这个想法遵循一个三步过程 使用语言模型嵌入所有标记文本。 在存储的嵌入上执行最近邻搜索。 聚合最近邻的标签以获得预测。 这个过程在图 9-3 中有所说明它展示了标记数据是如何嵌入模型并与标签一起存储的。当需要对新文本进行分类时它也会被嵌入并且基于最近邻的标签给出标签。重要的是要校准要搜索的邻居数量因为太少可能会有噪音太多可能会混入相邻的群体。 图 9-3. 最近邻嵌入查找的示意图 这种方法的美妙之处在于不需要对模型进行微调就可以利用少量可用的标记数据点。相反使这种方法起作用的主要决定是选择一个理想情况下在类似领域上预训练的适当模型。 由于 GPT-3 只能通过 OpenAI API 获得我们将使用 GPT-2 来测试这种技术。具体来说我们将使用一个在 Python 代码上训练的 GPT-2 变体这将有望捕捉到我们 GitHub 问题中包含的一些上下文。 让我们编写一个辅助函数它接受一个文本列表并使用模型为每个文本创建单一向量表示。我们需要处理的一个问题是像 GPT-2 这样的转换器模型实际上会返回每个标记一个嵌入向量。例如给定句子“I took my dog for a walk”我们可以期望有几个嵌入向量每个标记一个。但我们真正想要的是整个句子或我们应用程序中的 GitHub 问题的单一嵌入向量。为了处理这个问题我们可以使用一种称为池化的技术。最简单的池化方法之一是对标记嵌入进行平均这称为均值池化。使用均值池化我们唯一需要注意的是不要在平均值中包括填充标记所以我们可以使用注意力掩码来处理。 为了看看这是如何工作的让我们加载一个 GPT-2 分词器和模型定义均值池化操作并将整个过程包装在一个简单的embed_text()函数中 import torch from transformers import AutoTokenizer, AutoModelmodel_ckpt miguelvictor/python-gpt2-large tokenizer AutoTokenizer.from_pretrained(model_ckpt) model AutoModel.from_pretrained(model_ckpt)def mean_pooling(model_output, attention_mask):# Extract the token embeddingstoken_embeddings model_output[0]# Compute the attention maskinput_mask_expanded (attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float())# Sum the embeddings, but ignore masked tokenssum_embeddings torch.sum(token_embeddings * input_mask_expanded, 1)sum_mask torch.clamp(input_mask_expanded.sum(1), min1e-9)# Return the average as a single vectorreturn sum_embeddings / sum_maskdef embed_text(examples):inputs tokenizer(examples[text], paddingTrue, truncationTrue,max_length128, return_tensorspt)with torch.no_grad():model_output model(**inputs)pooled_embeds mean_pooling(model_output, inputs[attention_mask])return {embedding: pooled_embeds.cpu().numpy()}现在我们可以为每个拆分获取嵌入。请注意GPT 风格的模型没有填充标记因此我们需要在可以批量获取嵌入之前添加一个标记就像在前面的代码中实现的那样。我们将只是为此目的重复使用字符串结束标记 tokenizer.pad_token tokenizer.eos_token embs_train ds[train].map(embed_text, batchedTrue, batch_size16) embs_valid ds[valid].map(embed_text, batchedTrue, batch_size16) embs_test ds[test].map(embed_text, batchedTrue, batch_size16)现在我们已经有了所有的嵌入我们需要建立一个系统来搜索它们。我们可以编写一个函数计算我们将查询的新文本嵌入与训练集中现有嵌入之间的余弦相似度。或者我们可以使用数据集中的内置结构称为FAISS 索引。⁵我们在第七章中已经遇到了 FAISS。您可以将其视为嵌入的搜索引擎我们将在一分钟内更仔细地看看它是如何工作的。我们可以使用数据集的现有字段创建一个 FAISS 索引使用add_faiss_index()或者使用add_faiss_index_from_external_arrays()将新的嵌入加载到数据集中。让我们使用前一个函数将我们的训练嵌入添加到数据集中 embs_train.add_faiss_index(embedding)这创建了一个名为embedding的新 FAISS 索引。现在我们可以通过调用函数get_nearest_examples()执行最近邻查找。它返回最接近的邻居以及每个邻居的匹配分数。我们需要指定查询嵌入以及要检索的最近邻居的数量。让我们试一试看看与示例最接近的文档 i, k 0, 3 # Select the first query and 3 nearest neighbors rn, nl \r\n\r\n, \n # Used to remove newlines in text for compact displayquery np.array(embs_valid[i][embedding], dtypenp.float32) scores, samples embs_train.get_nearest_examples(embedding, query, kk)print(fQUERY LABELS: {embs_valid[i][labels]}) print(fQUERY TEXT:\n{embs_valid[i][text][:200].replace(rn, nl)} [...]\n) print(*50) print(fRetrieved documents:) for score, label, text in zip(scores, samples[labels], samples[text]):print(*50)print(fTEXT:\n{text[:200].replace(rn, nl)} [...])print(fSCORE: {score:.2f})print(fLABELS: {label})QUERY LABELS: [new model] QUERY TEXT: Implementing efficient self attention in T5# New model addition My teammates and I (including ice-americano) would like to use efficient self attention methods such as Linformer, Performer and [...] Retrieved documents:TEXT: Add Linformer model# New model addition ## Model description ### Linformer: Self-Attention with Linear Complexity Paper published June 9th on ArXiv: https://arxiv.org/abs/2006.04768 La [...] SCORE: 54.92 LABELS: [new model]TEXT: Add FAVOR / Performer attention# FAVOR / Performer attention addition Are there any plans to add this new attention approximation block to Transformers library? ## Model description The n [...] SCORE: 57.90 LABELS: [new model]TEXT: Implement DeLighT: Very Deep and Light-weight Transformers# New model addition ## Model description DeLight, that delivers similar or better performance than transformer-based models with sign [...] SCORE: 60.12 LABELS: [new model] 很好这正是我们所希望的通过嵌入查找得到的三个文档都具有相同的标签我们已经可以从标题中看出它们都非常相似。查询以及检索到的文档都围绕着添加新的高效 Transformer 模型。然而问题仍然存在k的最佳值是多少同样我们应该如何聚合检索到的文档的标签例如我们应该检索三个文档并分配至少出现两次的所有标签吗还是应该选择 20 个并使用至少出现 5 次的所有标签让我们系统地调查一下我们将尝试几个k的值然后使用一个辅助函数改变标签分配的阈值m k。我们将记录每个设置的宏和微性能以便稍后决定哪个运行效果最好。我们可以使用get_nearest_examples_batch()函数而不是循环遍历验证集中的每个样本它接受一个查询的批处理 def get_sample_preds(sample, m):return (np.sum(sample[label_ids], axis0) m).astype(int)def find_best_k_m(ds_train, valid_queries, valid_labels, max_k17):max_k min(len(ds_train), max_k)perf_micro np.zeros((max_k, max_k))perf_macro np.zeros((max_k, max_k))for k in range(1, max_k):for m in range(1, k 1):_, samples ds_train.get_nearest_examples_batch(embedding,valid_queries, kk)y_pred np.array([get_sample_preds(s, m) for s in samples])clf_report classification_report(valid_labels, y_pred,target_namesmlb.classes_, zero_division0, output_dictTrue)perf_micro[k, m] clf_report[micro avg][f1-score]perf_macro[k, m] clf_report[macro avg][f1-score]return perf_micro, perf_macro让我们检查在所有训练样本中最佳的值并可视化所有k和m配置的分数 valid_labels np.array(embs_valid[label_ids]) valid_queries np.array(embs_valid[embedding], dtypenp.float32) perf_micro, perf_macro find_best_k_m(embs_train, valid_queries, valid_labels)fig, (ax0, ax1) plt.subplots(1, 2, figsize(10, 3.5), shareyTrue) ax0.imshow(perf_micro) ax1.imshow(perf_macro)ax0.set_title(micro scores) ax0.set_ylabel(k) ax1.set_title(macro scores) for ax in [ax0, ax1]:ax.set_xlim([0.5, 17 - 0.5])ax.set_ylim([17 - 0.5, 0.5])ax.set_xlabel(m) plt.show()从图中我们可以看到有一个模式对于给定的k选择太大或太小会产生次优结果。当选择大约为m / k 1 / 3的比率时可以获得最佳性能。让我们看看哪个k和m能够在整体上获得最佳结果 k, m np.unravel_index(perf_micro.argmax(), perf_micro.shape) print(fBest k: {k}, best m: {m})Best k: 15, best m: 5当我们选择 k 15 和 m 5 时性能最佳换句话说当我们检索 15 个最近的邻居然后分配至少出现 5 次的标签。现在我们有了一个找到嵌入查找最佳值的好方法我们可以像使用朴素贝叶斯分类器一样玩同样的游戏我们遍历训练集的切片并评估性能。在我们可以切片数据集之前我们需要删除索引因为我们不能像数据集那样切片 FAISS 索引。其余的循环保持完全相同另外使用验证集来获取最佳的k和m值 embs_train.drop_index(embedding) test_labels np.array(embs_test[label_ids]) test_queries np.array(embs_test[embedding], dtypenp.float32)for train_slice in train_slices:# Create a Faiss index from training sliceembs_train_tmp embs_train.select(train_slice)embs_train_tmp.add_faiss_index(embedding)# Get best k, m values with validation setperf_micro, _ find_best_k_m(embs_train_tmp, valid_queries, valid_labels)k, m np.unravel_index(perf_micro.argmax(), perf_micro.shape)# Get predictions on test set_, samples embs_train_tmp.get_nearest_examples_batch(embedding,test_queries,kint(k))y_pred np.array([get_sample_preds(s, m) for s in samples])# Evaluate predictionsclf_report classification_report(test_labels, y_pred,target_namesmlb.classes_, zero_division0, output_dictTrue,)macro_scores[Embedding].append(clf_report[macro avg][f1-score])micro_scores[Embedding].append(clf_report[micro avg][f1-score])plot_metrics(micro_scores, macro_scores, train_samples, Embedding)嵌入查找在微分数上与先前的方法竞争只有两个“可学习”参数k和m但在宏分数上表现稍差。 请以一颗谷物的方式接受这些结果哪种方法最有效强烈取决于领域。零-shot 管道的训练数据与我们正在使用的 GitHub 问题数据集有很大不同其中包含模型可能之前很少遇到的大量代码。对于更常见的任务例如对评论的情感分析该管道可能效果更好。同样嵌入的质量取决于模型和它训练的数据。我们尝试了半打模型例如sentence-transformers/stsb-roberta-large它经过训练以提供句子的高质量嵌入以及microsoft/codebert-base和dbernsohn/roberta-python它们是在代码和文档上进行训练的。对于这个特定的用例GPT-2 在 Python 代码上的训练效果最好。 由于您实际上不需要在代码中更改任何内容只需将模型检查点名称替换为测试另一个模型一旦设置了评估管道您可以快速尝试几个模型。 现在让我们将这个简单的嵌入技巧与简单地微调我们拥有的有限数据的 Transformer 进行比较。 微调一个普通的 Transformer 如果我们可以访问标记数据我们也可以尝试做一件显而易见的事情简单地微调预训练的 Transformer 模型。在本节中我们将使用标准的 BERT 检查点作为起点。稍后我们将看到微调语言模型对性能的影响。 提示 对于许多应用程序来说从预训练的类似 BERT 的模型开始是一个好主意。但是如果您的语料库领域与预训练语料库通常是维基百科有显著差异您应该探索 Hugging Face Hub 上提供的许多模型。很可能已经有人在您的领域上预训练了一个模型 让我们从加载预训练的标记器开始对我们的数据集进行标记化并摆脱我们在训练和评估中不需要的列 import torch from transformers import (AutoTokenizer, AutoConfig,AutoModelForSequenceClassification)model_ckpt bert-base-uncased tokenizer AutoTokenizer.from_pretrained(model_ckpt)def tokenize(batch):return tokenizer(batch[text], truncationTrue, max_length128) ds_enc ds.map(tokenize, batchedTrue) ds_enc ds_enc.remove_columns([labels, text])多标签损失函数期望标签的类型为浮点数因为它还允许类概率而不是离散标签。因此我们需要更改label_ids列的类型。由于逐元素更改列格式与 Arrow 的类型格式不兼容我们将做一些变通。首先我们创建一个带有标签的新列。该列的格式是从第一个元素推断出来的。然后我们删除原始列并将新列重命名为原始列的位置 ds_enc.set_format(torch) ds_enc ds_enc.map(lambda x: {label_ids_f: x[label_ids].to(torch.float)},remove_columns[label_ids]) ds_enc ds_enc.rename_column(label_ids_f, label_ids)由于由于训练数据的有限大小我们很可能会很快过度拟合训练数据因此我们设置load_best_model_at_endTrue并根据微观F[1]-⁠score 选择最佳模型 from transformers import Trainer, TrainingArgumentstraining_args_fine_tune TrainingArguments(output_dir./results, num_train_epochs20, learning_rate3e-5,lr_scheduler_typeconstant, per_device_train_batch_size4,per_device_eval_batch_size32, weight_decay0.0,evaluation_strategyepoch, save_strategyepoch,logging_strategyepoch,load_best_model_at_endTrue, metric_for_best_modelmicro f1,save_total_limit1, log_levelerror)我们需要F[1]-score 来选择最佳模型因此我们需要确保在评估过程中计算它。因为模型返回 logits所以我们首先需要使用 sigmoid 函数对预测进行归一化然后可以使用简单的阈值对其进行二值化。然后我们从分类报告中返回我们感兴趣的分数 from scipy.special import expit as sigmoiddef compute_metrics(pred):y_true pred.label_idsy_pred sigmoid(pred.predictions)y_pred (y_pred0.5).astype(float)clf_dict classification_report(y_true, y_pred, target_namesall_labels,zero_division0, output_dictTrue)return {micro f1: clf_dict[micro avg][f1-score],macro f1: clf_dict[macro avg][f1-score]}现在我们准备好了对于每个训练集切片我们从头开始训练一个分类器在训练循环结束时加载最佳模型并将结果存储在测试集上 config AutoConfig.from_pretrained(model_ckpt) config.num_labels len(all_labels) config.problem_type multi_label_classificationfor train_slice in train_slices:model AutoModelForSequenceClassification.from_pretrained(model_ckpt,configconfig)trainer Trainer(modelmodel, tokenizertokenizer,argstraining_args_fine_tune,compute_metricscompute_metrics,train_datasetds_enc[train].select(train_slice),eval_datasetds_enc[valid],)trainer.train()pred trainer.predict(ds_enc[test])metrics compute_metrics(pred)macro_scores[Fine-tune (vanilla)].append(metrics[macro f1])micro_scores[Fine-tune (vanilla)].append(metrics[micro f1])plot_metrics(micro_scores, macro_scores, train_samples, Fine-tune (vanilla))首先我们看到简单地在数据集上微调一个普通的 BERT 模型会在我们拥有大约 64 个示例时导致竞争力的结果。我们还看到在此之前行为有点不稳定这又是由于在小样本上训练模型时一些标签可能不平衡。在利用数据集的未标记部分之前让我们快速看一下在少样本领域使用语言模型的另一种有前途的方法。 使用提示进行上下文和少样本学习 我们在本章前面看到我们可以使用 BERT 或 GPT-2 等语言模型并使用提示和解析模型的标记预测来使其适应监督任务。这与添加特定任务头部并调整模型参数的经典方法不同。优点是这种方法不需要任何训练数据但缺点是如果我们可以访问标记数据似乎我们无法利用它。有一个中间地带我们有时可以利用称为in-context或少样本学习。 为了说明这个概念考虑一个英语到法语的翻译任务。在零-shot 范式中我们会构建一个提示可能如下所示 prompt \ Translate English to French: thanks 这有望促使模型预测单词“merci”的标记。我们已经在第六章中使用 GPT-2 进行摘要时看到向文本中添加“TL;DR”提示模型生成摘要而无需明确训练。GPT-3 论文的一个有趣发现是大型语言模型有效地从提示中学习示例的能力因此前面的翻译示例可以增加几个英语到德语的示例这将使模型在这个任务上表现得更好。⁶ 此外作者发现模型规模越大它们越擅长使用上下文示例从而显著提高性能。尽管 GPT-3 大小的模型在生产中具有挑战性但这是一个令人兴奋的新兴研究领域人们已经构建了一些很酷的应用比如自然语言 shell其中命令以自然语言输入并由 GPT-3 解析为 shell 命令。 使用标记数据的另一种方法是创建提示和期望预测的示例并继续在这些示例上训练语言模型。一种名为 ADAPET 的新方法使用了这种方法并在各种任务上击败了 GPT-3通过生成提示来调整模型。最近 Hugging Face 研究人员的工作表明这种方法比微调自定义头部更节约数据。⁷ 在本节中我们简要地讨论了利用我们拥有的少量标记示例的各种方法。通常情况下除了标记的示例我们还可以访问大量未标记的数据在下一节中我们将讨论如何充分利用这些数据。 利用未标记的数据 尽管拥有大量高质量标记数据是训练分类器的最佳情况但这并不意味着未标记数据毫无价值。想想我们使用的大多数模型的预训练即使它们是在互联网上大多数不相关的数据上训练的我们也可以利用预训练的权重来处理各种各样的文本上的其他任务。这是自然语言处理中迁移学习的核心思想。当下游任务的文本结构与预训练文本相似时迁移效果更好因此如果我们可以使预训练任务更接近下游目标我们可能会改善迁移效果。 让我们从我们具体的用例来考虑这个问题BERT 在 BookCorpus 和英文维基百科上进行了预训练而包含代码和 GitHub 问题的文本在这些数据集中显然是一个小众。如果我们从头开始对 BERT 进行预训练我们可以在 GitHub 上对所有问题进行爬取例如。然而这将是昂贵的并且 BERT 学到的许多关于语言的方面仍然适用于 GitHub 问题。那么在从头开始重新训练和仅仅使用模型进行分类之间是否有一个中间地带有它被称为域自适应我们在第七章中也看到了用于问答的域自适应。我们可以在不从头开始重新训练语言模型的情况下继续在我们领域的数据上训练它。在这一步中我们使用预测掩码词的经典语言模型目标这意味着我们不需要任何标记数据。之后我们可以将适应后的模型加载为分类器并进行微调从而利用未标记的数据。 域自适应的美妙之处在于与标记数据相比未标记数据通常是丰富可得的。此外适应后的模型可以重复用于许多用例。想象一下您想构建一个电子邮件分类器并在所有历史电子邮件上应用域自适应。稍后您可以使用相同的模型进行命名实体识别或其他分类任务比如情感分析因为该方法对下游任务是不可知的。 现在让我们看看我们需要采取哪些步骤来微调预训练语言模型。 微调语言模型 在本节中我们将对我们数据集的未标记部分进行预训练 BERT 模型的掩码语言建模进行微调。为此我们只需要两个新概念在对数据进行分词时需要额外的步骤和一个特殊的数据整理器。让我们从分词开始。 除了文本中的普通标记外分词器还向序列添加特殊标记例如 [CLS] 和 [SEP] 标记用于分类和下一个句子预测。当我们进行掩码语言建模时我们希望确保不训练模型来预测这些标记。出于这个原因我们从损失中屏蔽它们并且我们可以通过设置 return_special_tokens_maskTrue 来在分词时获得掩码。让我们使用该设置重新对文本进行分词 def tokenize(batch):return tokenizer(batch[text], truncationTrue,max_length128, return_special_tokens_maskTrue)ds_mlm ds.map(tokenize, batchedTrue) ds_mlm ds_mlm.remove_columns([labels, text, label_ids])开始进行掩码语言建模所缺少的是在输入序列中屏蔽标记并在输出中有目标标记的机制。我们可以采用的一种方法是设置一个函数对随机标记进行屏蔽并为这些序列创建标签。但这将使数据集的大小翻倍因为我们还将在数据集中存储目标序列并且这意味着我们将在每个时期使用相同的序列屏蔽。 一个更加优雅的解决方案是使用数据整理器。记住数据整理器是构建数据集和模型调用之间的桥梁的函数。从数据集中抽取一个批次并且数据整理器准备好批次中的元素以将它们馈送到模型中。在我们遇到的最简单的情况下它只是将每个元素的张量连接成一个单一的张量。在我们的情况下我们可以使用它来动态进行掩码和标签生成。这样我们就不需要存储标签每次抽样时都会得到新的掩码。这个任务的数据整理器称为DataCollatorForLanguageModeling。我们使用模型的标记器和我们想要掩码的标记的分数来初始化它。我们将使用这个整理器来掩码 15%的标记这遵循了 BERT 论文中的程序 from transformers import DataCollatorForLanguageModeling, set_seeddata_collator DataCollatorForLanguageModeling(tokenizertokenizer,mlm_probability0.15)让我们快速看一下数据整理器的操作看看它实际上做了什么。为了快速在DataFrame中显示结果我们将标记器和数据整理器的返回格式切换为 NumPy set_seed(3) data_collator.return_tensors np inputs tokenizer(Transformers are awesome!, return_tensorsnp) outputs data_collator([{input_ids: inputs[input_ids][0]}])pd.DataFrame({Original tokens: tokenizer.convert_ids_to_tokens(inputs[input_ids][0]),Masked tokens: tokenizer.convert_ids_to_tokens(outputs[input_ids][0]),Original input_ids: original_input_ids,Masked input_ids: masked_input_ids,Labels: outputs[labels][0]}).T012345原始标记[CLS]transformersareawesome![SEP]掩码标记[CLS]transformersareawesome[MASK][SEP]原始 input_ids10119081202412476999102掩码 input_ids10119081202412476103102标签-100-100-100-100999-100 我们看到与感叹号对应的标记已被替换为掩码标记。此外数据整理器返回了一个标签数组原始标记为-100掩码标记的标记 ID。正如我们之前看到的包含-100 的条目在计算损失时被忽略。让我们将数据整理器的格式切换回 PyTorch data_collator.return_tensors pt有了标记器和数据整理器我们就可以开始微调掩码语言模型了。我们像往常一样设置TrainingArguments和Trainer from transformers import AutoModelForMaskedLMtraining_args TrainingArguments(output_dir f{model_ckpt}-issues-128, per_device_train_batch_size32,logging_strategyepoch, evaluation_strategyepoch, save_strategyno,num_train_epochs16, push_to_hubTrue, log_levelerror, report_tonone)trainer Trainer(modelAutoModelForMaskedLM.from_pretrained(bert-base-uncased),tokenizertokenizer, argstraining_args, data_collatordata_collator,train_datasetds_mlm[unsup], eval_datasetds_mlm[train])trainer.train()trainer.push_to_hub(Training complete!)我们可以访问训练器的日志历史记录查看模型的训练和验证损失。所有日志都存储在trainer.state.log_history中作为一个字典列表我们可以轻松地加载到 Pandas 的DataFrame中。由于训练和验证损失记录在不同的步骤数据框中存在缺失值。因此我们在绘制指标之前删除缺失值 df_log pd.DataFrame(trainer.state.log_history)(df_log.dropna(subset[eval_loss]).reset_index()[eval_loss].plot(labelValidation)) df_log.dropna(subset[loss]).reset_index()[loss].plot(labelTrain)plt.xlabel(Epochs) plt.ylabel(Loss) plt.legend(locupper right) plt.show()似乎训练和验证损失都显著下降了。所以让我们看看当我们基于这个模型微调分类器时是否也能看到改进。 微调分类器 现在我们将重复微调过程但有一个细微的不同即加载我们自己的自定义检查点 model_ckpt f{model_ckpt}-issues-128 config AutoConfig.from_pretrained(model_ckpt) config.num_labels len(all_labels) config.problem_type multi_label_classificationfor train_slice in train_slices:model AutoModelForSequenceClassification.from_pretrained(model_ckpt,configconfig)trainer Trainer(modelmodel,tokenizertokenizer,argstraining_args_fine_tune,compute_metricscompute_metrics,train_datasetds_enc[train].select(train_slice),eval_datasetds_enc[valid],)trainer.train()pred trainer.predict(ds_enc[test])metrics compute_metrics(pred)# DA refers to domain adaptationmacro_scores[Fine-tune (DA)].append(metrics[macro f1])micro_scores[Fine-tune (DA)].append(metrics[micro f1])将结果与基于 vanilla BERT 的微调进行比较我们发现在低数据领域特别是有优势。在更多标记数据可用的情况下我们也获得了几个百分点的优势 plot_metrics(micro_scores, macro_scores, train_samples, Fine-tune (DA))这突显了领域适应可以在没有标记数据和很少努力的情况下提供模型性能的轻微提升。自然地未标记数据越多标记数据越少使用这种方法会产生更大的影响。在结束本章之前我们将向您展示一些利用未标记数据的更多技巧。 高级方法 在微调分类头之前微调语言模型是一种简单而可靠的提升性能的方法。然而还有更复杂的方法可以进一步利用未标记数据。我们在这里总结了一些这些方法如果您需要更高性能这些方法应该提供一个很好的起点。 无监督数据增强 无监督数据增强UDA背后的关键思想是模型对未标记的示例和略微扭曲的示例应该保持一致。这些扭曲是通过标准的数据增强策略引入的例如标记替换和回译。然后通过最小化原始和扭曲示例的预测之间的 KL 散度来强制执行一致性。这个过程在图 9-5 中有所说明其中一致性要求通过从未标记的示例中增加交叉熵损失的额外项来实现。这意味着使用标准监督方法在标记数据上训练模型但约束模型对未标记数据进行一致的预测。 图 9-5. 使用 UDA 训练模型 M由 Qizhe Xie 提供 这种方法的性能相当令人印象深刻使用 UDA 训练的 BERT 模型在有限数量的示例上获得了与数千个示例训练的模型相似的性能。缺点是您需要一个数据增强管道并且训练时间更长因为您需要多次前向传递来生成未标记和增强示例的预测分布。 不确定性感知的自我训练 利用未标记数据的另一种有前途的方法是不确定性感知的自我训练UST。这里的想法是在标记数据上训练一个教师模型然后使用该模型在未标记数据上创建伪标签。然后在伪标记数据上训练一个学生训练后它成为下一次迭代的教师。 这种方法的一个有趣之处在于伪标签的生成方式为了获得模型预测的不确定性度量相同的输入通过模型多次同时打开辍学。然后预测的方差给出了模型对特定样本的确定性的代理。有了这种不确定性度量然后使用一种称为贝叶斯主动学习的方法来采样伪标签。完整的训练管道在图 9-6 中有所说明。 图 9-6. UST 方法包括一个生成伪标签的教师和随后在这些标签上进行训练的学生学生训练后成为教师然后重复这个步骤由 Subhabrata Mukherjee 提供⁹ 通过这种迭代方案教师不断改进创建伪标签的能力因此模型的性能得到了提高。最终这种方法在几个百分点内接近使用数千个样本进行全面训练的模型并且在几个数据集上甚至超过了 UDA。 现在我们已经看到了一些先进的方法让我们退一步总结一下我们在本章学到的内容。 结论 在本章中我们看到即使只有少量甚至没有标签也不是所有的希望都已经失去。我们可以利用已经在其他任务上预训练的模型比如 BERT 语言模型或在 Python 代码上训练的 GPT-2来对 GitHub 问题分类的新任务进行预测。此外我们可以使用领域自适应在使用普通分类头训练模型时获得额外的提升。 在特定用例上所提出的方法中哪种方法最有效取决于各种方面您拥有多少标记数据它有多嘈杂数据与预训练语料库有多接近等等。要找出最有效的方法建立评估管道然后快速迭代是一个好主意。​⁠ Transformers 的灵活 API 允许您快速加载一些模型并进行比较而无需进行任何代码更改。Hugging Face Hub 上有超过 10,000 个模型很可能有人过去曾经处理过类似的问题您可以在此基础上构建。 这本书超出了 UDA 或 UST 等更复杂方法与获取更多数据之间的权衡范围。为了评估你的方法至少在早期建立验证和测试集是有意义的。在每一步你也可以收集更多的标记数据。通常标注几百个例子只需要几个小时或几天的工作而且有许多工具可以帮助你做到这一点。根据你想要实现的目标投入一些时间创建一个小而高质量的数据集可能比设计一个非常复杂的方法来弥补其不足更有意义。通过本章中我们提出的方法你可以确保充分利用你宝贵的标记数据。 在这里我们已经涉足了低数据范畴并且看到 Transformer 模型即使只有一百个例子仍然非常强大。在下一章中我们将看到完全相反的情况当我们有数百吉字节的数据和大量计算资源时我们将会做些什么。我们将从头开始训练一个大型的 Transformer 模型让它为我们自动完成代码。 ¹ Q. Xie et al., “Unsupervised Data Augmentation for Consistency Training”, (2019); S. Mukherjee and A.H. Awadallah, “Uncertainty-Aware Self-Training for Few-Shot Text Classification”, (2020). ² 我们感谢Joe Davison向我们提出了这种方法。 ³ A. Williams, N. Nangia, and S.R. Bowman, “A Broad-Coverage Challenge Corpus for Sentence Understanding Through Inference”, (2018); A. Conneau et al., “XNLI: Evaluating Cross-Lingual Sentence Representations”, (2018). ⁴ J. Wei and K. Zou, “EDA: Easy Data Augmentation Techniques for Boosting Performance on Text Classification Tasks”, (2019). ⁵ J. Johnson, M. Douze, and H. Jégou, “Billion-Scale Similarity Search with GPUs”, (2017). ⁶ T. Brown et al., “Language Models Are Few-Shot Learners”, (2020). ⁷ D. Tam et al., “Improving and Simplifying Pattern Exploiting Training”, (2021). ⁸ T. Le Scao and A.M. Rush, “How Many Data Points Is a Prompt Worth?”, (2021). ⁹ S. Mukherjee and A.H. Awadallah, “Uncertainty-Aware Self-Training for Few-Shot Text Classification”, (2020).
http://www.zqtcl.cn/news/686596/

相关文章:

  • 手机门户网站建设莱芜雪野湖国际会议中心酒店
  • 男人女人做那事网站vue加wordpress
  • 古色古香 网站模板西安企业黄页网站
  • 上海企业网站怎么建设交互设计网站有哪些
  • 企业网站设计与制作开发一款游戏app需要多少钱
  • 贵阳网站方舟网络北京手机网站制作
  • 烟台小学网站建设做盗版电影网站问题
  • 做网站语言知乎长春财经学院学费多少
  • 大丰有做网站的电子商城网站开发要多少钱
  • 南京建设网站制作手机怎么制作网页
  • 杭州pc网站建设方案网站建设要准备的内容
  • 壶关网站建设中国专利申请网官网
  • 具体的网站建设方案网页程序开发采购
  • 泉州 网站建设苏州网站外包
  • 网站做404页面怎么做网站开发过程的基本环节
  • 做网站是前端还是后端小程序网站模板
  • 学校网站建设与维护建设银行官网电话
  • dedecms网站地图修改软件开发公司规章制度
  • 大型旅游网站骏驰网站开发
  • 有心学做网站两学一做知识竞赛试题网站
  • 西宁圆井模板我自己做的网站怎么做网站能快速赚钱
  • 根据网站集约化建设的要求直流分公司四川建设部网站
  • 网站优化平台有哪些遵义网站开发的公司有哪些
  • 推荐一下网站谢谢微盟微商城怎么样
  • 网站建设的技术指标网站做好第二年要多少钱
  • 工业设计东莞网站建设WordPress网络功能
  • 网站pv多少可以企业网站托管常见问题
  • 深圳有哪些网站建设沈阳做机床的公司网站
  • 2022年网站能用的wordpress 客户端使用
  • 社交网站建设内容如何制作橡皮泥 简单