赞
踩
目录
AGI 之 【Hugging Face】 的【文本分类】的 [文本分类器] 的简单整理
AGI,即通用人工智能(Artificial General Intelligence),是一种具备人类智能水平的人工智能系统。它不仅能够执行特定的任务,而且能够理解、学习和应用知识于广泛的问题解决中,具有较高的自主性和适应性。AGI的能力包括但不限于自我学习、自我改进、自我调整,并能在没有人为干预的情况下解决各种复杂问题。
AGI能做的事情非常广泛:
跨领域任务执行:AGI能够处理多领域的任务,不受限于特定应用场景。
自主学习与适应:AGI能够从经验中学习,并适应新环境和新情境。
创造性思考:AGI能够进行创新思维,提出新的解决方案。
社会交互:AGI能够与人类进行复杂的社会交互,理解情感和社会信号。关于AGI的未来发展前景,它被认为是人工智能研究的最终目标之一,具有巨大的变革潜力:
技术创新:随着机器学习、神经网络等技术的进步,AGI的实现可能会越来越接近。
跨学科整合:实现AGI需要整合计算机科学、神经科学、心理学等多个学科的知识。
伦理和社会考量:AGI的发展需要考虑隐私、安全和就业等伦理和社会问题。
增强学习和自适应能力:未来的AGI系统可能利用先进的算法,从环境中学习并优化行为。
多模态交互:AGI将具备多种感知和交互方式,与人类和其他系统交互。
Hugging Face作为当前全球最受欢迎的开源机器学习社区和平台之一,在AGI时代扮演着重要角色。它提供了丰富的预训练模型和数据集资源,推动了机器学习领域的发展。Hugging Face的特点在于易用性和开放性,通过其Transformers库,为用户提供了方便的模型处理文本的方式。随着AI技术的发展,Hugging Face社区将继续发挥重要作用,推动AI技术的发展和应用,尤其是在多模态AI技术发展方面,Hugging Face社区将扩展其模型和数据集的多样性,包括图像、音频和视频等多模态数据。
在AGI时代,Hugging Face可能会通过以下方式发挥作用:
模型共享:作为模型共享的平台,Hugging Face将继续促进先进的AGI模型的共享和协作。
开源生态:Hugging Face的开源生态将有助于加速AGI技术的发展和创新。
工具和服务:提供丰富的工具和服务,支持开发者和研究者在AGI领域的研究和应用。
伦理和社会责任:Hugging Face注重AI伦理,将推动负责任的AGI模型开发和应用,确保技术进步同时符合伦理标准。
AGI作为未来人工智能的高级形态,具有广泛的应用前景,而Hugging Face作为开源社区,将在推动AGI的发展和应用中扮演关键角色。
当前环境下的关键 package 版本:
(注意:以下代码运行,可能需要科学上网)
文本分类(Text Classification)是自然语言处理(NLP)中的一项基本任务,其目的是将文本数据自动归类到预定义的类别中。文本分类的具体任务可以有多种形式,包括情感分析、主题分类、垃圾邮件检测、语言检测、意图识别等。
Hugging Face 提供了一个强大且易于使用的库(Transformers),能够处理各种文本分类任务。该库包含了众多预训练的变换器模型,如 BERT、RoBERTa、GPT-3 等,这些模型已经在大规模数据集上进行了预训练,能有效提升分类任务的准确性和效率。
Hugging Face生态系统中的三个核心库:Datasets、Tokenizers和Transformers。如下图所示,这些库令我们能够快速地将原始文本输入微调后的模型,以用于推理新的推文。
文本分类器是一种机器学习模型,用于将文本数据分配到一个或多个预定义的类别中。文本分类器可以用于多种应用场景,例如垃圾邮件检测、情感分析、新闻分类、主题识别等。
文本分类器的基本工作原理
数据收集:收集大量带有类别标签的文本数据。这些数据用于训练和评估分类器。
数据预处理:
- 清洗数据:去除噪音,如标点符号、HTML 标签等。
- 分词:将文本分解为单词或词组(词元)。
- 去除停用词:去掉常见但对分类任务无意义的词(如“the”、“is”)。
特征提取:将文本数据转换为模型可以理解的数值格式。常见的方法有:
- 词袋模型(Bag of Words):计算每个单词在文档中出现的频率。
- TF-IDF(词频-逆文档频率):考虑单词在文档中出现的频率以及其在整个语料库中的稀有度。
- 词嵌入(Word Embeddings):如 Word2Vec、GloVe,将单词映射到一个连续的向量空间。
- 上下文嵌入(Contextual Embeddings):如 BERT,考虑单词在上下文中的意义。
模型训练:使用训练数据和特征训练机器学习模型。常见的模型有:
- 传统机器学习模型:如朴素贝叶斯、支持向量机(SVM)、逻辑回归等。
- 深度学习模型:如卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆网络(LSTM)、Transformer 等。
模型评估:使用验证数据集评估模型的性能,常用的评估指标有准确率、精确率、召回率、F1 分数等。
模型部署:将训练好的模型部署到生产环境中,应用于实际的文本分类任务。
文本分类器的应用
- 垃圾邮件检测:判断电子邮件是否为垃圾邮件。
- 情感分析:分析文本的情感倾向,如正面、负面、中性。
- 新闻分类:将新闻文章分类为体育、政治、娱乐等类别。
- 主题识别:识别文档或段落的主题。
- 语言检测:识别文本所属的语言。
如前面所述,像DistilBERT这样的模型被预训练用于预测文本序列中的掩码单词。然而,这些语言模型不能直接用于文本分类,我们需要稍微修改它们。为了理解需要做哪些修改,我们来看一下基于编码器的模型(如DistilBERT)的架构,如图所示。
在DistilBERT的情况下,它在猜测掩码词元。
首先,文本会被词元化并表示为称为词元编码的独热向量。词元编码的维度由词元分析器词表的大小决定,通常包括两万到二十万个唯一性词元。接下来,这些词元编码会被转换为词元嵌入,即存在于低维空间中的向量。然后,这些词元嵌入会通过编码器块层传递,以产生每个输入词元的隐藏状态。对于语言建模的预训练目标 ,每个隐藏状态都被馈送到一个层,该层预测掩码输入词元。对于分类任务,我们将语言建模层替换为分类层。
实际上,PyTorch在实现中跳过了为词元编码创建独热向量的步骤,因为将矩阵与独热向量相乘等同于从矩阵中选择一列。这可以通过直接从矩阵中获取词元ID对应的列来完成。当我们使用nn.Embedding类时,我们将在以后的文章会中看到这一点。
我们有以下两种选择来基于Twitter数据集进行模型训练:
- 特征提取
我们将隐藏状态用作特征,只需训练分类器,而无须修改预训练模型。
- 微调
我们对整个模型进行端到端的训练,这样还会更新预训练模型的参数。
接下来我们将讲述基于DistilBERT的以上两种选择,以及这两种选择的权衡取舍。
使用Transformer作为特征提取器相当简单。如图所示,我们在训练期间冻结主体的权重,并将隐藏状态用作分类器的特征。这种方法的优点是,我们可以快速训练一个小型或浅层模型。这样的模型可以是神经分类层或不依赖于梯度的方法,例如随机森林。这种方法特别适用于没有GPU的场景,因为隐藏状态只需要预计算一次。
我们将使用Hugging Face Transformers库中另一个很方便的自动类AutoModel。与AutoTokenizer类似,AutoModel具有from_pretrained()方法,可用于加载预训练模型的权重。现在我们使用该方法来加载DistilBERT checkpoint:
- # 从 transformers 模块中导入 AutoModel 类,用于加载预训练模型
- from transformers import AutoModel
-
- # 指定要加载的预训练模型的检查点名称或路径
- model_ckpt = "distilbert-base-uncased"
-
- # 设置设备为 GPU(如果可用)否则为 CPU
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
-
- # 使用 from_pretrained 方法加载指定的预训练模型,并将其移动到指定设备
- model = AutoModel.from_pretrained(model_ckpt).to(device)
运行结果:
这里我们使用PyTorch来检查GPU是否可用(即代码torch.cuda.is available()),然后将PyTorch的nn.Module.to()方法与模型加载器链接起来(即代码to(device))。这确保了如果有GPU,模型将在GPU上运行。如果没有,模型将在CPU上运行,不过这样可能会慢很多。
AutoModel类将词元编码转换为嵌入向量,然后将它们馈送到编码器栈中以返回隐藏状态。我们看一下如何从语料库中提取这些状态。
文章中的代码大多用PyTorch编写,但 Hugging Face Transformers 库可以与 TensorFlow 和 JAX紧密协作。这意味着你只需要改变一些代码即可在你最喜欢的深度学习框架中加载预训练模型!例如,我们可以使用 TFAutoModel 类在 TensorFlow 中加载 DistilBERT:
- # 从 transformers 模块中导入 TFAutoModel 类,用于加载预训练的 TensorFlow 模型
- from transformers import TFAutoModel
-
- # 指定要加载的预训练模型的检查点名称或路径
- model_ckpt = "distilbert-base-uncased"
-
- # 使用 from_pretrained 方法加载指定的预训练模型(TensorFlow 版本)
- tf_model = TFAutoModel.from_pretrained(model_ckpt)
当模型仅在一个框架中发布,而你想在另一个框架中使用时,这种互操作性特别有用。例如,在第4章中我们会遇到XLM-RoBERTa模型(https://oreil.ly/OUMvG),它只有PyTorch权重,因此如果你尝试像之前一样在TensorFlow中加载它:
- # 加载预训练的 XLM-RoBERTa 模型
- tf_xlmr = TFAutoModel.from_pretrained("xlm-roberta-base")
运行结果:
你将会得到一个错误。在这种情况下,你可以将from_pt=True参数传给TfAutoModel.from pretrained()函数,库将自动为你下载并转换PyTorch权重:
-
- # 加载预训练的 XLM-RoBERTa 模型,使用 TensorFlow 并从 PyTorch 权重转换
- tf_xlmr = TFAutoModel.from_pretrained("xlm-roberta-base", from_pt=True)
运行结果:
你可以看到,在Hugging Face Transformers库中切换框架非常简单!在大多数情况下,你只需要在类名前添加TF前缀即可获得相应的TensorFlow2.0类。同理,如果要加载PyTorch的权重,你只需要将tf替换成pt字符串即可(例如在接下来的部分中),pt是PyTorch的简称,就像tf代表TensorFlow一样。
作为预热,我们检索一个字符串的最终隐藏状态。我们需要做的第一件事是对字符串进行编码并将词元转换为PyTorch张量。可以通过向词元分析器提供return_tensors=”pt”参数来实现。具体如下:
- # 定义要处理的文本
- text = "this is a test"
-
- # 使用预训练的分词器对文本进行编码,返回张量格式
- inputs = tokenizer(text, return_tensors="pt")
-
- # 打印输入张量的形状
- print(f"Input tensor shape: {inputs['input_ids'].size()}")
运行结果:
Input tensor shape: torch.Size([1, 6])
我们可以看到,生成的张量形状为[batch size,n_tokens]。现在我们已经将编码作为张量获取,最后一步是将它们放置在与模型相同的设备上,并按以下方式传输入:
- # 导入 PyTorch 库,用于张量操作和深度学习计算
- import torch
- # 从 transformers 模块中导入 AutoModel 类,用于加载预训练模型
- from transformers import AutoModel
-
- # 指定要加载的预训练模型的检查点名称或路径
- model_ckpt = "distilbert-base-uncased"
-
- # 设置设备为 GPU(如果可用)否则为 CPU
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
-
- # 使用 from_pretrained 方法加载指定的预训练模型,并将其移动到指定设备
- model = AutoModel.from_pretrained(model_ckpt).to(device)
-
- # 将输入张量移动到指定的设备(如 GPU 或 CPU)
- inputs = {k: v.to(device) for k, v in inputs.items()}
-
- # 关闭梯度计算,进行推理
- with torch.no_grad():
- outputs = model(**inputs)
-
- # 打印模型输出
- print(outputs)
运行结果:
BaseModelOutput(last_hidden_state=tensor([[[-0.1565, -0.1862, 0.0528, ..., -0.1188, 0.0662, 0.5470], [-0.3575, -0.6484, -0.0618, ..., -0.3040, 0.3508, 0.5221], [-0.2772, -0.4459, 0.1818, ..., -0.0948, -0.0076, 0.9958], [-0.2841, -0.3917, 0.3753, ..., -0.2151, -0.1173, 1.0526], [ 0.2661, -0.5094, -0.3180, ..., -0.4203, 0.0144, -0.2149], [ 0.9441, 0.0112, -0.4714, ..., 0.1439, -0.7288, -0.1619]]]), hidden_states=None, attentions=None)
这里我们使用了torch.no_grad()上下文管理器来禁用梯度的自动计算。这对推理很有用,因为它减少了计算的内存占用。根据模型配置,输出可以包含多个对象,例如隐藏状态、损失或注意力,这些对象以类似于Python中的namedtuple的形式排列。在我们的示例中,模型输出是一个BaseModelOutput实例,并且包含了其属性名,我们可以通过这些属性名来获取其详情。我们看到,我们的模型只有一个属性,即last_hidden_state(最终隐藏状态),然后我们通过以下代码查看一下它的形状:
- # 获取模型输出的最后一个隐藏状态的形状
- output_shape = outputs.last_hidden_state.size()
-
- # 打印输出张量的形状
- print(output_shape)
运行结果:
torch.Size([1, 6, 768])
我们可以看到,隐藏状态张量的形状为[batch_size,n_tokens,hidden_dim]。换句话说,对于每个输入词元,都会返回一个768维向量。对于分类任务,通常惯例是只使用与[CLS]词元关联的隐藏状态作为输入特征。由于此词元出现在每个序列的开头,我们可以通过简单地索引outputs.last_hidden_state来提取它,如下所示:
- # 获取模型输出的最后一个隐藏状态中第一个标记(通常是 [CLS] 标记)的形状
- cls_token_shape = outputs.last_hidden_state[:, 0].size()
-
- # 打印 [CLS] 标记的形状
- print(cls_token_shape)
运行结果:
torch.Size([1, 768])
现在我们知道如何针对单个字符串获取最终隐藏状态。我们通过创建一个新的hidden_state列来对整个数据集执行相同的操作,以存储所有这些向量。就像我们在词元分析器中所做的那样,我们将使用DatasetDict的map()方法一次性提取所有隐藏状态。我们需要做的第一件事是将先前的步骤封装在一个处理函数中:
- def extract_hidden_states(batch):
- # 将模型输入放到 GPU 上
- inputs = {k: v.to(device) for k, v in batch.items()
- if k in tokenizer.model_input_names}
-
- # 提取最后的隐藏状态
- with torch.no_grad():
- last_hidden_state = model(**inputs).last_hidden_state
-
- # 返回 [CLS] 标记的向量,并将其移动到 CPU
- return {"hidden_state": last_hidden_state[:, 0].cpu().numpy()}
这个函数和我们之前的逻辑的唯一不同在于最后一步,即将最终的隐藏状态作为NumPy数组放回CPU。当我们使用批量输入时,map()方法要求处理函数返回Python或NumPy对象。
由于我们的模型期望输入张量,下一步需要将input_ids和attention_mask列转换为torch格式,具体如下:
- # 定义一个函数,用于对批量数据进行分词处理
- def tokenize(batch):
- # 使用 tokenizer 对批量数据中的 "text" 字段进行分词处理,添加填充和截断
- return tokenizer(batch["text"], padding=True, truncation=True)
-
- # 从 datasets 模块导入 load_dataset 函数
- from datasets import load_dataset
-
- # 使用 load_dataset 函数加载名为 "emotion" 的数据集
- emotions = load_dataset("emotion", trust_remote_code=True)
-
- # 使用 map 方法对 emotions 数据集进行分词处理
- # tokenize 函数会应用到每个样本的 "text" 字段,进行填充和截断
- # batched=True 表示批量处理样本,batch_size=None 表示一次处理整个数据集
- emotions_encoded = emotions.map(tokenize, batched=True, batch_size=None)
-
- # 将数据集格式设置为 PyTorch 张量格式,并指定要包含的列
- emotions_encoded.set_format("torch",
- columns=["input_ids", "attention_mask", "label"])
然后我们可以一次性提取所有分割的隐藏状态:
- # 使用 extract_hidden_states 函数从数据集中提取隐藏状态,批处理处理
- emotions_hidden = emotions_encoded.map(extract_hidden_states, batched=True)
运行结果:
请注意,这里我们没有设置batch_size=None,这意味着使用了默认的batch_size=1000。正如预期的那样,应用extract_hidden_states()函数将一个新的hidden_state列添加到我们的数据集中。
- # 获取训练集中的列名
- emotions_hidden["train"].column_names
运行结果:
['text', 'label', 'input_ids', 'attention_mask', 'hidden_state']
现在我们已经得到了与每个推文相关联的隐藏状态,下一步是基于它们训练一个分类器。为了做到这一点,我们需要一个特征矩阵,我们来看一下。
现在,经过预处理的数据集包含了我们需要训练分类器的所有信息。我们将使用隐藏状态作为输入特征,标注作为目标。我们可以很容易地按照以下方式创建对应的数组,以Scikit-learn格式为基础:
- import numpy as np
-
- # 从 emotions_hidden 数据集中获取训练集和验证集的隐藏状态和标签
- X_train = np.array(emotions_hidden["train"]["hidden_state"])
- X_valid = np.array(emotions_hidden["validation"]["hidden_state"])
- y_train = np.array(emotions_hidden["train"]["label"])
- y_valid = np.array(emotions_hidden["validation"]["label"])
-
- # 打印训练集和验证集的隐藏状态张量形状
- X_train.shape, X_valid.shape
运行结果:
((16000, 768), (2000, 768))
在对隐藏状态进行模型训练之前,进行快速检查以确保它们提供了我们想要分类的情感的有用表示是一个良好的实践。接下来,我们将看到可视化特征提供了一种快速的方法来实现这一点。
L. McInnes, J. Healy, and J. Melville, “UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction”(https://arxiv.org/abs/1802.03426), (2018).
由于在768维度中可视化隐藏状态是个艰难的任务,因此我们将使用强大的UMAP算法将向量投影到2D平面上 。由于UMAP在特征缩放到[0,1]区间内时效果最佳,因此我们将首先应用一个MinMaxScaler,然后使用umap-learn库的UMAP实现来缩放隐藏状态:
- from umap import UMAP
- from sklearn.preprocessing import MinMaxScaler
- import pandas as pd
-
- # 将特征缩放到 [0,1] 范围
- X_scaled = MinMaxScaler().fit_transform(X_train)
-
- # 初始化并拟合 UMAP
- mapper = UMAP(n_components=2, metric="cosine").fit(X_scaled)
-
- # 创建一个包含二维嵌入的 DataFrame
- df_emb = pd.DataFrame(mapper.embedding_, columns=["X", "Y"])
- df_emb["label"] = y_train
-
- # 显示 DataFrame 的前几行
- df_emb.head()
(如果没有安装 umap ,使用 pip install umap-learn)
运行结果:
X | Y | label | |
0 | 4.27811 | 6.829443 | 0 |
1 | -3.05367 | 6.592448 | 0 |
2 | 5.272943 | 3.33651 | 3 |
3 | -2.51557 | 4.445898 | 2 |
4 | -3.62499 | 4.616945 | 3 |
结果是一个数组,该数组具有相同的训练样本数量,但只有2个特征,而不是我们最初使用的768个特征!我们进一步探究压缩后的数据,并分别绘制每个类别的点密度图:
- import matplotlib.pyplot as plt
-
- # 创建一个 2x3 的子图布局
- fig, axes = plt.subplots(2, 3, figsize=(7, 5))
- axes = axes.flatten()
-
- # 不同情绪标签的颜色映射和标签名称
- cmaps = ["Greys", "Blues", "Oranges", "Reds", "Purples", "Greens"]
- labels = emotions["train"].features["label"].names
-
- # 在子图上绘制每个情绪标签的二维嵌入
- for i, (label, cmap) in enumerate(zip(labels, cmaps)):
- df_emb_sub = df_emb.query(f"label == {i}")
- axes[i].hexbin(df_emb_sub["X"], df_emb_sub["Y"], cmap=cmap,
- gridsize=20, linewidths=(0,))
- axes[i].set_title(label)
- axes[i].set_xticks([]), axes[i].set_yticks([])
-
- # 调整布局使图像更紧凑
- plt.tight_layout()
-
- # 保存图像为 PNG 格式
- plt.savefig('images/emotion_embeddings.png', bbox_inches='tight')
-
- # 显示图形
- plt.show()
运行结果:
这些只是投影到较低维空间的结果。某些类别重叠并不意味着它们在原始空间中不能区分。相反,如果它们在投影空间中是可区分的,那么它们在原始空间中也将是可区分的。
从这个图中,我们可以看到一些明显的模式:负面情感,如悲伤(sadness)、愤怒(anger)和恐惧(fear),都占据着类似的区域,但分布略有不同。另外,喜悦(joy)和爱情(love)与负面情感明显分开,并且也共享一个相似的空间。最后,惊奇(surprise)分散在整个图中。虽然我们可能希望有些区分,但这并不是肯定的,因为该模型并没有被训练去区分这些情感。它只是通过猜测文本中被掩码的单词来隐式地学习它们。
现在我们已经对数据集的特征有了一些了解,接下来我们来到最后一步,基于数据集训练模型!
我们已经看到,不同情感的隐藏状态是不同的,尽管其中一些情感并没有明显的界限。现在让我们使用这些隐藏状态来训练一个逻辑回归模型(使用Scikit-learn)。训练这样一个简单的模型速度很快,而且不需要GPU:
- from sklearn.linear_model import LogisticRegression
-
- # 初始化逻辑回归模型,并设置最大迭代次数为 3000
- lr_clf = LogisticRegression(max_iter=3000)
-
- # 用训练数据拟合逻辑回归模型
- lr_clf.fit(X_train, y_train)
-
- # 计算并返回模型在验证集上的准确率
- accuracy = lr_clf.score(X_valid, y_valid)
- accuracy
运行结果:
0.6335
从准确率上看,我们的模型似乎只比随机模型稍微好一点,但由于我们处理的是一个不平衡的多分类数据集,它实际上会显著地表现更好。我们可以通过将其与简单基准进行比较来检查我们的模型是否良好。在Scikit-learn中,有一个DummyClassifier可以用于构建具有简单启发式的分类器,例如始终选择多数类或始终选择随机类。在这种情况下,表现最佳的启发式是始终选择最常见的类,这会产生约35%的准确率:
- from sklearn.dummy import DummyClassifier
-
- # 初始化一个 Dummy 分类器,使用 "most_frequent" 策略
- dummy_clf = DummyClassifier(strategy="most_frequent")
-
- # 用训练数据拟合 Dummy 分类器
- dummy_clf.fit(X_train, y_train)
-
- # 计算并返回 Dummy 分类器在验证集上的准确率
- dummy_accuracy = dummy_clf.score(X_valid, y_valid)
- dummy_accuracy
运行结果:
0.352
因此,使用DistilBERT嵌入的简单分类器明显优于我们的基线。我们可以通过查看分类器的混淆矩阵来进一步研究模型的性能,该矩阵告诉我们真实标注和预测标注之间的关系:
- from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix
-
- # 定义一个函数来绘制混淆矩阵并保存图片
- def plot_confusion_matrix(y_preds, y_true, labels, filename):
- # 计算归一化的混淆矩阵
- cm = confusion_matrix(y_true, y_preds, normalize="true")
-
- # 创建一个 6x6 英寸的子图
- fig, ax = plt.subplots(figsize=(6, 6))
-
- # 使用混淆矩阵和标签创建显示对象
- disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels)
-
- # 绘制混淆矩阵,使用蓝色颜色映射,格式化值为小数点后两位,不显示颜色条
- disp.plot(cmap="Blues", values_format=".2f", ax=ax, colorbar=False)
-
- # 设置图表标题
- plt.title("Normalized confusion matrix")
-
- # 保存图像为 PNG 格式
- plt.savefig(filename, dpi=300)
-
- # 显示图形
- plt.show()
-
- # 使用逻辑回归模型预测验证集标签
- y_preds = lr_clf.predict(X_valid)
-
- # 调用函数绘制混淆矩阵并保存为图片
- plot_confusion_matrix(y_preds, y_valid, labels, 'images/confusion_matrix.png')
运行结果:
这里我们可以看到,anger和fear最常与sadness混淆,这与我们可视化嵌入时所观察到的一致。此外,love和surprise经常与joy混淆。
接下来我们将探究微调方法,这种方法可以带来更好的分类效果。但是,重要的是要注意,微调需要更多的计算资源,比如GPU,而你的组织可能没有GPU。在这种情况下,基于特征的方法可以是传统机器学习和深度学习之间的一个很好的折中方案。
现在我们探讨如何进行端到端的Transformer模型微调。在使用微调方法时,我们不使用隐藏状态作为固定特征,而是如图所示那样进行训练。这要求分类头是可微的,这就是为什么这种方法通常使用神经网络进行分类。
训练用作分类模型输入的隐藏状态将有助于我们避免使用可能不适合分类任务的数据的问题。相反,初始隐藏状态在训练过程中适配,以降低模型损失并提高其性能。
我们将使用Hugging Face Transformers库中的Trainer API简化训练循环。让我们看一下设置它所需的步骤!
我们要做的第一件事是使用我们在基于特征的方法中一样的DistilBERT预训练模型。唯一的细微修改是我们使用AutoModelForSequenceClassification模型而不是AutoModel。区别在于AutoModelForSequenceClassification模型在预训练模型输出的顶部有一个分类头,可以很容易地与基础模型一起训练。我们只需要指定模型需要预测的标注数量(在我们的情况下为6个),因为这决定了分类头输出的数量:
- from transformers import AutoModelForSequenceClassification
-
- # 定义标签的数量
- num_labels = 6
-
- # 加载预训练的序列分类模型,并指定标签的数量,将模型移动到指定设备(如 GPU 或 CPU)
- model = (AutoModelForSequenceClassification
- .from_pretrained(model_ckpt, num_labels=num_labels)
- .to(device))
运行结果:
你会看到一个警告,说明模型的某些部分是随机初始化的。这是正常的,因为分类头还没有被训练。接下来的步骤是定义我们将用于评估模型在微调期间的性能的指标。
为了在训练期间监控指标,我们需要为Trainer定义一个compute_metrics()函数。该函数接收一个EvalPrediction对象(这是一个具有predictions和label_ids属性的命名元组),并需要返回一个将每个指标名称映射到其值的字典。对于我们的应用,我们将计算模型的F1分数和准确率:
- from sklearn.metrics import accuracy_score, f1_score
-
- # 定义一个函数来计算模型评估指标
- def compute_metrics(pred):
- # 获取预测结果的标签和预测值
- labels = pred.label_ids
- preds = pred.predictions.argmax(-1)
-
- # 计算加权 F1 分数
- f1 = f1_score(labels, preds, average="weighted")
-
- # 计算准确率
- acc = accuracy_score(labels, preds)
-
- # 返回包含准确率和 F1 分数的字典
- return {"accuracy": acc, "f1": f1}
有了数据集和度量指标后,在定义Trainer类之前,我们只需要处理最后两件事情:
1)登录我们的Hugging Face Hub账户。从而让我们能够将我们的微调模型推送到Hub上,并与社区分享它。
2)定义训练运行的所有超参数。
接下来我们将解决这些步骤。
如果你使用Jupyter notebook,你可以使用下面的辅助函数来登录到Hub:
- from huggingface_hub import notebook_login
-
- # 登录 Hugging Face 账号,以便访问和管理模型、数据集和其他资源
- notebook_login()
运行结果:
填入自己创建的 Token (要有 Write 权限)
最后登陆成功如下图
然后会显示一个小部件,你可以在其中输入你的用户名和密码,或具有写入权限的访问令牌。你可以在Hub文档中找到有关如何创建访问令牌的详细信息(https://oreil.ly/IRkN1)。如果你使用命令行终端,则可以通过运行以下命令登录:
$ huggingface-cli login
我们将使用TrainingArguments类来定义训练参数。此类存储了大量信息,从而为训练和评估提供细粒度的控制。最重要的参数是output_dir,它是存储训练过程中所有工件的位置。以下是TrainingArguments的完整示例:
- from transformers import Trainer, TrainingArguments
-
- # 定义每个批次的大小
- batch_size = 64
-
- # 计算日志记录的步数
- logging_steps = len(emotions_encoded["train"]) // batch_size
-
- # 设置微调后模型的名称
- model_name = f"{model_ckpt}-finetuned-emotion"
-
- # 定义训练参数
- training_args = TrainingArguments(
- output_dir=model_name, # 保存模型和其他输出的目录
- num_train_epochs=2, # 训练的轮数
- learning_rate=2e-5, # 学习率
- per_device_train_batch_size=batch_size, # 每个设备上的训练批次大小
- per_device_eval_batch_size=batch_size, # 每个设备上的评估批次大小
- weight_decay=0.01, # 权重衰减率
- evaluation_strategy="epoch", # 评估策略(在每个 epoch 结束时进行评估)
- disable_tqdm=False, # 是否禁用 tqdm 进度条
- logging_steps=logging_steps, # 记录日志的步数
- push_to_hub=False, # 是否将模型推送到 Hugging Face Hub # Set to False or omit to avoid pushing to hub
- log_level="error" # 日志记录的级别
- )
这里我们还设置了批量大小、学习率和迭代轮数,并指定在训练运行结束时加载最佳模型。所有组件都齐全了,我们可以使用Trainer实例化和微调我们的模型:
- from transformers import Trainer
-
- # 初始化 Trainer 对象
- trainer = Trainer(
- model=model, # 要训练的模型
- args=training_args, # 训练参数
- compute_metrics=compute_metrics, # 用于计算评估指标的函数
- train_dataset=emotions_encoded["train"], # 训练数据集
- eval_dataset=emotions_encoded["validation"], # 验证数据集
- tokenizer=tokenizer # 使用的分词器
- )
-
- # 开始训练模型
- trainer.train()
运行结果:
我们可以看到我们的模型在验证集上的F1分数约为92%,这比基于特征的方法有了显著的提升!
我们可以通过计算混淆矩阵来更详细地查看训练指标。为了可视化混淆矩阵,我们首先需要获取验证集上的预测结果。Trainer类的predict()方法返回了几个有用的对象,我们可以用它们进行评估:
- # 进行预测
- preds_output = trainer.predict(emotions_encoded["validation"]) # 在验证集上进行预测
predict()方法的输出是一个PredictionOutput对象,它包含了predictions和label_ids的数组,以及我们传给训练器的度量指标。我们可以通过以下方式访问验证集上的度量指标:
- # 获取度量指标
- preds_output.metrics # 访问 metrics 字典,包含我们定义的 compute_metrics 函数计算的结果
运行结果:
{'test_loss': 0.21821913123130798, 'test_accuracy': 0.9285, 'test_f1': 0.9282263824245695, 'test_runtime': 44.5947, 'test_samples_per_second': 44.848, 'test_steps_per_second': 0.718}
它还包含了每个类别的原始预测值。我们可以使用np.argmax()进行贪婪解码预测,然后会得到预测标注,并且结果格式与前面的基于特征的方法相同,以便我们进行比较:
- # 从预测结果中提取预测标签
- y_preds = np.argmax(preds_output.predictions, axis=1) # 对预测结果应用 argmax 函数,沿最后一维取最大值的索引,得到预测的标签
我们可以基于这个预测结果再次绘制混淆矩阵:
- # 绘制混淆矩阵
- plot_confusion_matrix(y_preds, y_valid, labels,'images/confusion_matrix_2.png') # 使用预测标签和真实标签绘制混淆矩阵,并显示标签名称
运行结果:
可见,与前面的基于特征的方法相比,微调方法的结果更接近于理想的对角线混淆矩阵。love类别仍然经常与joy混淆,这点逻辑上也讲得过去。surprise也经常被错误地识别为joy,或者与fear混淆。总体而言,模型的性能似乎非常不错,但在我们结束之前,让我们深入了解模型可能会犯的错误的类型。
如果你使用的是TensorFlow,那么还可以使用Keras API微调模型。其与PyTorch API的主要区别在于,没有Trainer类,因为Keras模型已经提供了内置的fit()方法。为了了解具体是如何工作的,这里我们将加载DistilBERT模型的TensorFlow版本:
- from transformers import TFAutoModelForSequenceClassification
-
- # 加载预训练的 TensorFlow 序列分类模型
- tf_model = (TFAutoModelForSequenceClassification
- .from_pretrained(model_ckpt, num_labels=num_labels)) # 从预训练模型检查点加载模型,并设置分类标签的数量
接下来,我们将把数据集转换为tf.data.Dataset格式。因为我们已经填充了词元化输入,所以我们可以通过将to_tf_dataset()方法应用于emotions_encoded轻松完成此转换:
- # 获取分词器模型输入名称
- tokenizer_columns = tokenizer.model_input_names
-
- # 将训练数据集转换为 TensorFlow 数据集
- tf_train_dataset = emotions_encoded["train"].to_tf_dataset(
- columns=tokenizer_columns, # 使用分词器的输入列
- label_cols=["label"], # 标签列
- shuffle=True, # 是否打乱数据
- batch_size=batch_size # 批量大小
- )
-
- # 将验证数据集转换为 TensorFlow 数据集
- tf_eval_dataset = emotions_encoded["validation"].to_tf_dataset(
- columns=tokenizer_columns, # 使用分词器的输入列
- label_cols=["label"], # 标签列
- shuffle=False, # 验证集不打乱数据
- batch_size=batch_size # 批量大小
- )
在这里,我们还对训练集进行了随机化,定义了它和验证集的批量大小。最后要做的是编译和训练模型:
- import tensorflow as tf
-
- # 编译 TensorFlow 模型
- tf_model.compile(
- optimizer=tf.keras.optimizers.Adam(learning_rate=5e-5), # 设置优化器为 Adam,学习率为 5e-5
- loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), # 设置损失函数为稀疏分类交叉熵,模型输出包含 logits
- metrics=tf.metrics.SparseCategoricalAccuracy() # 设置评估指标为稀疏分类准确率
- )
-
- # 训练 TensorFlow 模型
- tf_model.fit(
- tf_train_dataset, # 训练数据集
- validation_data=tf_eval_dataset, # 验证数据集
- epochs=2 # 训练轮数
- )
在继续之前,我们应该更深入地研究一下模型的预测。一个简单而又强大的技巧是按模型损失对验证样本进行排序。当我们在前向传递期间传递标注时,会自动计算并返回损失。以下是返回损失以及预测标注的函数:
- from torch.nn.functional import cross_entropy
-
- def forward_pass_with_label(batch):
- # 将所有输入张量放置在与模型相同的设备上
- inputs = {k: v.to(device) for k, v in batch.items()
- if k in tokenizer.model_input_names}
-
- # 关闭梯度计算,进行前向传播
- with torch.no_grad():
- output = model(**inputs) # 获取模型输出
- pred_label = torch.argmax(output.logits, axis=-1) # 预测标签
- loss = cross_entropy(output.logits, batch["label"].to(device),
- reduction="none") # 计算损失
-
- # 将输出放置在 CPU 上,以便与其他数据集列兼容
- return {"loss": loss.cpu().numpy(), # 返回损失值,放置在 CPU 上
- "predicted_label": pred_label.cpu().numpy()} # 返回预测标签,放置在 CPU 上
我们可以再次使用map()方法将此函数应用到所有样本中以获得损失:
- # 将数据集转换回 PyTorch 张量格式
- emotions_encoded.set_format("torch",
- columns=["input_ids", "attention_mask", "label"])
-
- # 计算损失值
- # 使用 map 函数对验证集应用前向传播函数,计算每个批次的损失和预测标签
- emotions_encoded["validation"] = emotions_encoded["validation"].map(
- forward_pass_with_label, # 应用前向传播函数
- batched=True, # 启用批处理
- batch_size=16 # 设置批处理大小为 16
- )
运行结果:
最后,我们创建一个包含文本、损失、预测标注、真实标注的DataFrame:
- # 定义一个函数,用于将标签的整数值转换为字符串标签
- def label_int2str(row):
- # 从情感数据集的训练子集中获取标签的特征,并使用 int2str 方法将整数标签转换为字符串
- return emotions["train"].features["label"].int2str(row)
-
- # 将数据集格式设置为 pandas DataFrame
- emotions_encoded.set_format("pandas")
-
- # 定义要选择的列
- cols = ["text", "label", "predicted_label", "loss"]
-
- # 从验证集中选择特定列创建 DataFrame
- df_test = emotions_encoded["validation"][:][cols]
-
- # 将 label 列中的整数标签转换为字符串标签
- df_test["label"] = df_test["label"].apply(label_int2str)
-
- # 将 predicted_label 列中的整数标签转换为字符串标签
- df_test["predicted_label"] = df_test["predicted_label"].apply(label_int2str)
现在我们可以轻松地根据损失升序或降序对emotions_encoded进行排序。此操作的目的是检测以下内容之一:
- 错误的标注
任何对数据进行标注的过程都有可能出错。数据标注者可能会犯错误或者存在分歧,而从其他特征推断的标注也有可能是错误的。如果自动标注数据很容易,那么就不存在人工标注数据这项工作了。因此,有些样本被错误标注是很正常的。通过这种方法,我们可以快速找到并纠正它们。
- 数据集的特性
在现实世界中,数据集往往有一定的杂乱。当输入为文字时,输入中的特殊字符或字符串可能会对模型的预测产生重大影响。检查模型最弱的预测可以帮助识别这样的特征,清理数据或注入类似的样本可以使模型更健壮。
让我们先看一下损失最高的数据样本:
- # 按损失值降序排序,并显示前 10 行
- df_test.sort_values("loss", ascending=False).head(10)
运行结果:
text | label | predicted_label | loss | |
1950 | i as representative of everything thats wrong ... | surprise | sadness | 5.241769 |
1963 | i called myself pro life and voted for perry w... | joy | sadness | 5.212655 |
882 | i feel badly about reneging on my commitment t... | love | sadness | 5.110416 |
1500 | i guess we would naturally feel a sense of lon... | anger | sadness | 5.09637 |
1870 | i guess i feel betrayed because i admired him ... | joy | sadness | 5.078988 |
1111 | im lazy my characters fall into categories of ... | joy | fear | 4.711989 |
318 | i felt ashamed of these feelings and was scare... | fear | sadness | 4.529932 |
1509 | i guess this is a memoir so it feels like that... | joy | fear | 4.426157 |
1581 | i feel stronger clearer but a little annoyed n... | anger | joy | 4.406065 |
1683 | i had applied for a job and they had assured m... | anger | joy | 4.224496 |
我们可以清楚地看到模型对某些标注进行了错误的预测。另外,似乎有相当多的样本没有明确的类,这可能是被错误标注的或需要一个新类。特别是,joy似乎被多次标注错误。通过这些信息,我们可以改进数据集,这通常可以带来与增加数据或使用更大的模型一样大的(或更大的)性能提升!
当查看具有最低损失的样本时,我们观察到模型在预测sadness类时最有信心。深度学习模型非常擅长找到和利用短路来进行预测。因此,值得花时间查看模型最有信心的样本,以便我们可以确信模型不会错误地利用文本的某些特征。所以,让我们也看一下损失最小的预测:
- # 按损失值升序排序,并显示前 10 行
- df_test.sort_values("loss", ascending=True).head(10)
运行结果:
text | label | predicted_label | loss | |
578 | i got to christmas feeling positive about the ... | joy | joy | 0.020875 |
1140 | i do think about certain people i feel a bit d... | sadness | sadness | 0.020912 |
1861 | im tired of feeling lethargic hating to work o... | sadness | sadness | 0.021227 |
189 | i leave the meeting feeling more than a little... | sadness | sadness | 0.021306 |
267 | i feel like im alone in missing him and becaus... | sadness | sadness | 0.021321 |
1368 | i started this blog with pure intentions i mus... | sadness | sadness | 0.021351 |
1120 | i am feeling a little disheartened | sadness | sadness | 0.021455 |
133 | i and feel quite ungrateful for it but i m loo... | sadness | sadness | 0.021521 |
566 | i did things that i always wondered about and ... | sadness | sadness | 0.021524 |
1873 | i feel practically virtuous this month i have ... | joy | joy | 0.021534 |
通过上面操作,我们可以看到joy有时会误标注,而模型对预测sadness标注最有信心。通过这些信息,我们可以有针对性地改进我们的数据集,并且还要关注模型最有信心的类别。
在使用训练好的模型之前的最后一步是将其保存以备后续使用。接下来我们将向你展示如何使用Hugging Face Transformers库来完成这个任务。
NLP社区通过共享预训练和微调模型获益匪浅,每个人都可以通过Hugging Face Hub与他人共享自己的模型。任何社区生成的模型都可以像我们下载DistilBERT模型一样从Hub中下载。我们可以通过使用Trainer API非常简单地保存和共享模型:
- # 将模型推送到 Hugging Face Hub
- trainer.push_to_hub(commit_message="Training completed!") # 提交消息为 "Training completed!"
我们也可以使用微调模型来对新的推文进行预测。由于我们已将模型推到了Hub上,因此现在我们可以通过pipeline()函数来使用它,就像在之前所做的那样。首先,让我们加载pipeline:
- from transformers import pipeline
-
- # 改变 `transformersbook` 为你的 Hugging Face Hub 用户名
- model_id = "transformersbook/distilbert-base-uncased-finetuned-emotion" # 指定模型 ID
-
- # 创建文本分类管道,加载指定的模型
- classifier = pipeline("text-classification", model=model_id)
运行结果:
然后用一条样本推文来测试pipeline:
- # 定义自定义推文
- custom_tweet = "I saw a movie today and it was really good."
-
- # 使用分类器对自定义推文进行分类,并返回所有分类的分数
- preds = classifier(custom_tweet, return_all_scores=True)
最后,我们可以用条形图绘制每个类别的概率。很明显,模型估计最可能的类是joy,这对于给定的推文似乎是合理的:
- # 将预测结果转换为 DataFrame
- preds_df = pd.DataFrame(preds[0])
-
- # 创建柱状图
- plt.bar(labels, 100 * preds_df["score"], color='C0') # 绘制柱状图,显示每个类别的概率
- plt.title(f'"{custom_tweet}"') # 设置图表标题为自定义推文
- plt.ylabel("Class probability (%)") # 设置 y 轴标签为“类别概率 (%)”
-
- # 保存图表为图片文件
- plt.savefig("images/tweet_classification.png") # 将图表保存为名为 tweet_classification.png 的图片文件
-
- plt.show() # 显示图表
运行结果:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。