AI 大模型从入门到实战

Hugging Face 入门

环境搭建

安装依赖库

要开始使用 Hugging Face,需要按照以下步骤来搭建环境:

  1. 创建 Conda 虚拟环境
    Conda 虚拟环境允许你在隔离的环境中安装和管理 Python 包,避免不同项目之间的依赖冲突。在这里,我们创建了一个名为 huggingface_env 的虚拟环境,指定 Python 版本为 3.10:

    conda create -n huggingface_env python=3.10
    

    激活新创建的虚拟环境:

    conda activate huggingface_env
    
  2. 安装 PyTorch

    Hugging Face 的 Transformers 库依赖于 PyTorch 或 TensorFlow。在这里我们选择安装 PyTorch。可以根据你的 CUDA 版本选择合适的安装命令。以下命令安装适用于 CUDA 11.8 的 PyTorch:

    pip install torch==2.2.1 torchvision==0.17.1 torchaudio==2.2.1 --index-url https://download.pytorch.org/whl/cu118
    

    如果你没有 GPU 或者不需要 CUDA 支持,可以安装 CPU 版本:

    pip install torch==2.2.1 torchvision==0.17.1 torchaudio==2.2.1 --index-url https://download.pytorch.org/whl/cpu
    
  3. 安装 Transformers

    Transformers 库是 Hugging Face 提供的核心库之一,包含了多种预训练的自然语言处理(NLP)模型,如 BERT、GPT、T5 等。你可以使用这个库轻松加载、训练和微调这些模型。以下是安装 Transformers 库的命令:

    pip install transformers
    

    如果想要使用 Transformers 库的最新开发版本(包括尚未发布的功能或修复),可以通过以下命令直接从 GitHub 仓库安装:

    pip install git+https://github.com/huggingface/transformers
    
  4. 安装 Datasets

    Datasets 库是 Hugging Face 提供的另一个重要库,主要用于加载和处理不同的数据集。这个库支持多种格式的数据集,并提供了数据预处理和增强的功能。安装命令如下:

    pip install datasets
    
  5. 安装 Tokenizers
    Tokenizers 库是高效的文本处理库,用于将文本分割成模型可处理的词元(token),适合与 Transformers 一起使用。当你安装 Transformers 库时,通常会自动安装 Tokenizers 作为依赖,但在某些情况下你可能需要手动安装或同时指定安装以确保其正确安装。:

    pip install tokenizers
    
  6. 安装 Accelerate 库
    Accelerate 是 Hugging Face 提供的库,用于简化多设备(如多 GPU、多 TPU 或分布式训练)的模型加速和训练流程。它支持 PyTorch 并能帮助你更容易地设置分布式训练环境,优化模型训练的性能。要安装 Accelerate 库,你可以使用以下命令:

    pip install accelerate
    
  7. 验证安装

    你可以通过以下 Python 脚本验证安装是否成功:

    import torch
    from datasets import load_dataset
    from transformers import AutoTokenizer, AutoModel
    
    # 验证 PyTorch 安装
    print(f"PyTorch version: {torch.__version__}")
    print(f"CUDA available: {torch.cuda.is_available()}")
    
    # 验证 Transformers 和 Tokenizer
    tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
    model = AutoModel.from_pretrained("bert-base-uncased")
    print(f"Transformers and Tokenizers loaded successfully.")
    
    # 验证 Datasets
    dataset = load_dataset("imdb")
    print(f"Datasets library working, sample: {dataset}")
    

    运行这个脚本,如果成功输出以下内容,说明环境搭建成功:

    PyTorch version: 2.2.1+cu118
    CUDA available: True
    Transformers and Tokenizers loaded successfully.
    Datasets library working, sample: DatasetDict({
    	train: Dataset({
    		features: ['text', 'label'],
    		num_rows: 25000
    	})
    	test: Dataset({
    		features: ['text', 'label'],
    		num_rows: 25000
    	})
    	unsupervised: Dataset({
    		features: ['text', 'label'],
    		num_rows: 50000
    	})
    })
    

设置镜像源

设置镜像源可以帮助你在访问 Hugging Face 的资源时使用不同的服务器或网络位置。这对于绕过访问限制、提高下载速度或者在不同环境下测试非常有用。以下是如何设置镜像源的两种方式:

  1. 环境变量设置
    你可以通过设置环境变量 HF_ENDPOINT 来指定镜像源。这会影响 Hugging Face 的库在访问模型和数据集时所使用的终端节点。如果你使用的是 Bash(默认的 shell),你可以将环境变量添加到 ~/.bashrc 文件中:

    • 打开 ~/.bashrc 文件进行编辑:
      vim ~/.bashrc
      
    • 追加以下行到末尾以设置环境变量:
      export HF_ENDPOINT="https://hf-mirror.com/"
      
      然后按 Esc 键退出编辑模式,输入 :wq 保存文件并退出 Vim。
    • 使环境变量的更改生效:
      source ~/.bashrc
      

    如果你希望直接在命令行中追加环境变量到 ~/.bashrc 文件,可以依次执行以下命令:

    echo 'export HF_ENDPOINT="https://hf-mirror.com/"' >> ~/.bashrc
    source ~/.bashrc
    
  2. 在代码中设置
    有些应用场景下,你可能希望在代码中动态设置镜像源,但这时必须在使用 Hugging Face 的库之前设置环境变量。例如:

    import os
    
    # 设置镜像源
    os.environ["HF_ENDPOINT"] = "https://hf-mirror.com/"
    
    from transformers import AutoModel
    
    # 现在可以使用 Hugging Face 库了,所有的资源请求将会通过指定的镜像源进行
    model = AutoModel.from_pretrained("bert-base-chinese")
    

设置缓存位置

在使用 Hugging Face 库时,你可以通过设置以下环境变量来配置缓存位置:

环境变量 作用
HF_HOME 指定 Hugging Face 库的全局缓存目录,包括模型和数据集。
TRANSFORMERS_CACHE 指定 Transformers 模型的缓存目录。
HF_DATASETS_CACHE 指定 Hugging Face 数据集的缓存目录。

注意
设置了 HF_HOME 环境变量后,它将覆盖 TRANSFORMERS_CACHEHF_DATASETS_CACHE 的设置。

你也可以在代码中设置这些环境变量。在引入依赖库之前,通过 os.environ 来设置环境变量。例如:

import os

# 设置缓存目录
os.environ["HF_HOME"] = "/path/to/custom/cache"
os.environ["TRANSFORMERS_CACHE"] = "/path/to/custom/transformers_cache"
os.environ["HF_DATASETS_CACHE"] = "/path/to/custom/datasets_cache"

Datasets 库

什么是 Datasets 库

Datasets 是由 Hugging Face 开发的一个高效且易于使用的 Python 库,专门用于处理和共享大型数据集,特别是与自然语言处理(NLP)相关的任务。它支持多种数据集格式,并提供强大的功能来简化数据处理、加载、转换和分发。以下是 Datasets 库的主要特点和功能:

  • 多样化的数据集支持
    Datasets 库提供了对数百个预定义数据集的访问,这些数据集涵盖了广泛的任务类型,如文本分类、翻译、摘要、问答、语言模型训练等。用户可以直接从库中加载这些数据集,也可以上传和分享自己的数据集。

  • 内存效率高
    Datasets 使用了一种内存映射技术,可以在无需将整个数据集加载到内存中的情况下,对非常大的数据集进行操作。这使得它非常适合处理大型数据集,特别是在资源受限的环境中。

  • 支持数据流式加载
    对于超大规模数据集,Datasets 支持流式加载,这意味着数据集可以按需从磁盘或远程服务器中分块加载,减少内存占用。

  • 丰富的数据转换功能
    Datasets 提供了丰富的数据预处理和增强功能,用户可以通过简单的 Python 函数或使用库内置的转换方法对数据集进行处理,例如文本清理、数据增强、特征提取等。

  • 与Hugging Face生态系统无缝集成
    Datasets 与 Hugging Face 的 Transformers、Tokenizers 无缝集成,用户可以轻松地将数据集应用于模型训练和评估。

  • 多种格式支持
    它支持多种数据格式,包括 CSV、JSON、Parquet 和文本文件等。用户可以轻松地将数据集转换为这些格式,或从这些格式中加载数据集。

  • 快速数据集切分
    Datasets 允许用户轻松地对数据集进行切分,如训练集、验证集、测试集的划分。它支持随机采样、按比例划分以及基于标签的分层抽样等方式。

查看数据集

列出可用数据集

你可以使用 list_datasets() 函数列出所有可用的数据集:

from huggingface_hub import list_datasets

all_datasets = list_datasets()
all_datasets = list(all_datasets)
print(len(all_datasets))  # 查看数据集的总数
print(all_datasets[:10])  # 查看前 10 个数据集的详细信息
查看结果
205249
DatasetInfo(id='amirveyseh/acronym_identification', author='amirveyseh', sha='15ef643450d589d5883e289ffadeb03563e80a9e', created_at=datetime.datetime(2022, 3, 2, 23, 29, 22, tzinfo=datetime.timezone.utc), last_modified=datetime.datetime(2024, 1, 9, 11, 39, 57, tzinfo=datetime.timezone.utc), private=False, gated=False, disabled=False, downloads=202, downloads_all_time=None, likes=19, paperswithcode_id='acronym-identification', tags=['task_categories:token-classification', 'annotations_creators:expert-generated', 'language_creators:found', 'multilinguality:monolingual', 'source_datasets:original', 'language:en', 'license:mit', 'size_categories:10K<n<100K', 'format:parquet', 'modality:text', 'library:datasets', 'library:pandas', 'library:mlcroissant', 'library:polars', 'arxiv:2010.14678', 'region:us', 'acronym-identification'], card_data=None, siblings=None)

list_datasets() 函数返回的是一个 DatasetInfo 对象的迭代器。这些 DatasetInfo 对象包含了每个数据集的元数据信息,帮助用户了解数据集的详细信息。下表展示了 DatasetInfo 对象中各字段的含义:

字段 含义
id 数据集的唯一标识符,通常为 用户名/数据集名称 的格式。
author 数据集的作者或上传者的用户名。
sha 数据集的唯一哈希值,用于版本控制和文件完整性验证。
created_at 数据集创建的时间,带有时区信息。
last_modified 数据集最后一次修改的时间,带有时区信息。
private 数据集是否为私有数据集,False 表示公开数据集。
gated 数据集是否受限访问,False 表示无需特别权限即可访问。
disabled 数据集是否被禁用,False 表示数据集可正常使用。
downloads 数据集的下载次数。
downloads_all_time 数据集的总下载次数(此字段为 None,表示未提供该信息)。
likes 数据集的点赞次数。
paperswithcode_id 数据集在 Papers with Code 网站上的唯一标识符(如果适用,此字段为 None 表示未提供)。
tags 数据集的标签,用于描述数据集的任务类别、语言、许可类型、格式等属性。
card_data 数据集的额外信息(此字段为 None,表示未提供该信息)。
siblings 与该数据集相关的其他版本或变体(此字段为 None,表示未提供该信息)。

搜索特定数据集

你也可以通过指定 search 参数来搜索包含指定关键词的数据集,关键字会基于数据集名称、描述、标签以及作者等字段进行搜索。例如,列出上述字段中包含 gpt 的所有数据集:

gpt_datasets = list_datasets(search="gpt")

根据作者过滤数据集

你可以通过指定 author 参数来过滤特定作者创建的数据集。例如,列出用户 shibing624 创建的所有数据集:

author_datasets = list_datasets(author="shibing624")

根据语言过滤数据集

你可以使用 language 参数来过滤特定语言的数据集。例如,列出所有中文数据集:

chinese_datasets = list_datasets(language="zh")

根据任务类别过滤数据集

你可以使用 `task_categories` 参数来过滤特定任务类别的数据集。例如,列出所有与文本生成任务相关的数据集:
```bash
text_gen_datasets = list_datasets(task_categories="text-generation")
```

根据大小类别过滤数据集

你可以使用 size_categories 参数来根据数据集的大小进行过滤。例如,列出样本数量在 1K 到 10K 之间的数据集:

medium_datasets = list_datasets(size_categories="1K<n<10K")

排序数据集

你可以使用 sort 参数来根据特定字段对数据集进行排序,例如按最后修改时间排序:

recent_datasets = list_datasets(sort="last_modified", direction=-1, limit=10)

list_datasets 的其他参数

下表是对 list_datasets() 函数全部参数的总结:

参数名称 类型 描述
filter strIterable[str] (可选) 用于在 Hub 上过滤数据集的字符串或字符串列表。
author str (可选) 过滤返回的由特定作者发布的数据集。
benchmark strList[str] (可选) 根据官方基准(benchmark)过滤数据集。
dataset_name strList[str] (可选) 根据数据集的名称过滤数据集,例如 SQACwikineural
language_creators strList[str] (可选) 根据数据集的语言创建者过滤,例如 crowdsourcedmachine_generated
language strList[str] (可选) 根据两字符的语言代码过滤数据集,例如 en 表示英语。
multilinguality strList[str] (可选) 根据数据集是否包含多种语言过滤。
size_categories strList[str] (可选) 根据数据集的大小类别过滤,例如 100K<n<1M1M<n<10M
tags strList[str] (可选) 根据标签过滤数据集。
task_categories strList[str] (可选) 根据设计的任务类别过滤数据集,例如 audio_classificationnamed_entity_recognition
task_ids strList[str] (可选) 根据具体任务过滤数据集,例如 speech_emotion_recognitionparaphrase
search str (可选) 搜索并返回包含指定字符串的数据集。搜索范围包括数据集的名称、描述和标签等字段。
sort Literal["last_modified"]str (可选) 用于对结果数据集进行排序的键。可能的值是 huggingface_hub.hf_api.DatasetInfo 类的属性,如 last_modified
direction Literal[-1]int (可选) 排序的方向。值为 -1 表示降序排序,其他值表示升序排序。
limit int (可选) 限制返回的数据集数量。默认为 None,表示返回所有数据集。
expand List[ExpandDatasetProperty_T] (可选) 返回响应中包含的属性列表。使用时,仅返回列表中的属性,不能与 full 参数同时使用。可能的值包括 authorcardDatacitationcreatedAt 等等。
full bool (可选) 是否获取所有数据集数据,包括 last_modifiedcard_data 以及文件。可包含诸如 PapersWithCode ID 的有用信息。
token Union[bool, str, None] (可选) 有效的用户访问令牌(字符串)。默认为本地保存的令牌,这是认证的推荐方法。如果不希望进行认证,可以传递 False

加载数据集

加载公共数据集

在 Hugging Face Hub 上加载数据集非常简单,通常使用 Datasets 库中的 load_dataset() 函数,你只需要使用 path 参数(经常以位置参数的形式给出)指定数据集的名称就可以直接从 Hugging Face Hub 上加载已公开的标准数据集,例如加载 nanaaaa/emotion_chinese_english 数据集:

from datasets import load_dataset

# 加载数据集
dataset = load_dataset("nanaaaa/emotion_chinese_english")
print(dataset)  # 查看当前数据集的信息
输出结果
DatasetDict({
	train: Dataset({
		features: ['id', 'sentence', 'label'],
		num_rows: 416
	})
	validation: Dataset({
		features: ['id', 'sentence', 'label'],
		num_rows: 54
	})
	test: Dataset({
		features: ['id', 'sentence', 'label'],
		num_rows: 46
	})
})

load_dataset() 函数一般返回的是一个 DatasetDict 对象,它是 Datasets 库中的一个容器,专门用于存储和管理多个数据集拆分(例如训练集、验证集、测试集等),并且可以通过访问该对象的键来查看和使用各个数据集拆分:

train_dataset = dataset["train"]
test_dataset = dataset["test"]
unsupervised_dataset = dataset["unsupervised"]

获取到具体的数据集拆分后,你可以进行多种操作来获取和操作数据集中的数据。下面是一些常用的操作示例:

  • 获取单条数据
    你可以通过索引获取单条数据:

    # 获取训练集的第一条数据
    first_example = train_dataset[0]
    print(first_example)
    

    输出通常是一个词典,包含每个特征的名称和值:

    查看结果
    {
    	'id': 1,
    	'sentence': 'Here and there over the grass stood beautiful flowers like stars, and there were twelve peach-trees that in the spring- time broke out into delicate blossoms of pink and pearl, and in the autumn bore rich fruit. ',
    	'label': 0
    }
    
  • 获取多条数据
    你可以通过切片或指定多个索引来获取多条数据:

    # 获取训练集的前5条数据
    first2five_examples = train_dataset[:5]
    print(first2five_examples)
    

    返回的结果是一个词典,包含多个特征的列表:

    查看结果
    {
    	'id': [1, 2, 3, 4, 5],
    	'sentence': [
    		'Here and there over the grass stood beautiful flowers like stars, and there were twelve peach-trees that in the spring- time broke out into delicate blossoms of pink and pearl, and in the autumn bore rich fruit. ',
    		'他顿时感到一阵巨大的恐惧,他跟织工说:“你在织什么样的长袍?”',
    		'那天下午孩子们跑进来时,发现巨人躺在那棵树下死了,身上盖满了白色的鲜花。',
    		'黑人们彼此交谈着,开始为一串明晃晃的珠子争吵起来。',
    		'And when the children ran in that afternoon, they found the Giant lying dead under the tree, all covered with white blossoms.'],
    	'label': [0, 3, 1, 2, 1]
    }
    
  • 获取数据集的特征信息
    你可以查看数据集的特征信息,包括每个特征的名称和类型:

    # 查看训练集的特征
    features = train_dataset.features
    print(features)
    

    输出是一个包含特征名和类型的词典,例如:

    查看结果
    {
    	'id': Value(dtype='int32', id=None),
    	'sentence': Value(dtype='string', id=None),
    	'label': ClassLabel(names=['joy', 'sadness', 'anger', 'fear', 'love'], id=None)
    }
    
  • 查看数据集的长度
    你可以获取数据集拆分中的样本数量:

    # 获取训练集的样本数量
    num_train_samples = len(train_dataset)
    
  • 遍历数据集
    你可以遍历数据集拆分,并对每条数据进行操作:

    for example in train_dataset:
        pass  # do somethings
    
  • 使用 map() 方法进行数据处理
    你可以使用 map() 方法对数据集进行批量处理,例如对文本进行预处理、添加新特征等:

    def preprocess_function(example):
        example['text_length'] = len(example['sentence'])
        return example
    
    # 使用 map 方法添加新的特征
    processed_dataset = train_dataset.map(preprocess_function)
    print(processed_dataset[0])
    

    以上代码将在每个数据条目中添加一个新的特征 text_length,表示文本的长度,并且还会显示处理的进度条:

    查看结果
    Map: 100%|██████████| 416/416 [00:00<00:00, 5334.49 examples/s]
    {
    	'id': 1,
    	'sentence': 'Here and there over the grass stood beautiful flowers like stars, and there were twelve peach-trees that in the spring- time broke out into delicate blossoms of pink and pearl, and in the autumn bore rich fruit. ',
    	'label': 0,
    	'text_length': 212
    }
    
  • 选择特定列
    你可以选择只保留数据集中的某些列:

    # 只保留 'sentence' 列
    smaller_dataset = train_dataset.select_columns(['sentence'])
    print(smaller_dataset[0])
    
    查看结果
    {
    	'sentence': 'Here and there over the grass stood beautiful flowers like stars, and there were twelve peach-trees that in the spring- time broke out into delicate blossoms of pink and pearl, and in the autumn bore rich fruit. '
    }
    
  • 过滤数据
    你可以根据某些条件过滤数据集:

     # 过滤出 'joy' 的情感的语句
     joy_sentences = train_dataset.filter(lambda x: x['label'] == 0)
     print(f"{len(joy_sentences)}/{len(train_dataset)}")
    
    查看结果
    86/416
    

加载数据集的指定拆分

使用 load_dataset() 函数时,可以通过 split 参数来加载数据集的指定拆分,而不是加载整个数据集的所有拆分,此时 load_dataset() 函数返回的是 Dataset 对象,而非 DatasetDict 对象。

假设你要加载 nanaaaa/emotion_chinese_english 数据集的特定拆分,如训练集、测试集或验证集(如果有),可以按照以下步骤操作:

train_dataset = load_dataset("nanaaaa/emotion_chinese_english", split="train")

并且使用不同的拆分选项可以实现不同的效果,例如:

  • split="train": 仅加载训练集。
  • split="test": 仅加载测试集。
  • split="validation": 仅加载验证集。
  • split="train[:10%]": 加载训练集的前 10% 数据。
  • split="train[10%:20%]": 加载训练集的 10% 到 20% 之间的数据。
  • split="train[1000:2000]": 加载训练集中的第 1000 到 2000 条数据。
  • split="train[2000:]": 加载训练集中的第 2000 条数据及之后的所有数据。
  • split="train[-1000:]": 加载训练集的最后 1000 条数据。

对于一些数据集,可能没有划分训练/验证/测试集,你可以通过组合不同的拆分来创建。

加载特定配置的数据集

load_dataset() 函数的 name 参数用于指定加载数据集的特定配置版本(即不同版本或子集)。这个参数特别有用,当数据集有多个不同的配置(如语言版本、子集或任务)时,你可以通过 name 参数来选择你需要的那个版本。

例如,我们有一个多语言翻译数据集 WMT(Workshop on Machine Translation),该数据集有多种语言对(例如,英语-德语,英语-法语等)。你可以通过 name 参数指定要加载的具体语言对:

# 加载 WMT14 英语到德语的翻译数据集
de2en_dataset = load_dataset("wmt14", name="de-en")
# 加在 WMT14 英语到法语的翻译数据集
en2fr_dataset = load_dataset("wmt14", name="en-fr")

加载指定版本的数据集

load_dataset() 函数可以使用 revision 参数指定要加载数据集的版本。revision 参数可以指定版本标签、提交哈希或分支名称:

# 加载 IMDB 数据集的特定版本
dataset = load_dataset("imdb", revision="v1.2.0")
# 加载 IMDB 数据集的特定提交版本
dataset = load_dataset("imdb", revision="a1b2c3d4")
# 加载 IMDB 数据集的分支版本
dataset = load_dataset("imdb", revision="dev-branch")

在学术研究或生产环境中,经常需要复现实验结果。使用 revision 参数可以确保你加载的数据集版本与最初实验时使用的版本完全一致。

从本地文件加载数据集

Datasets 库不仅可以从 Hugging Face Hub 加载公开数据集,还可以使用 data_file 参数直接从本地文件加载数据集,主要支持 csv、json、parquet、text 以及 arrow等格式的本地文件。以加载 csv 文件为例,具体加载方式如下:

from datasets import load_dataset

# 从本地 csv 文件加载数据集
dataset = load_dataset("csv", data_files="path/to/file.csv")

上述案例中 path(常用位置参数的形式指定)和 data_files 参数搭配使用时,path 参数用于指定文件的格式类型,而非公开数据集的名称;data_files 参数允许用户指定一个或多个数据文件的路径,以便 load_dataset() 函数能够正确加载和解析这些文件。根据数据集的结构和需求,data_files 可以接受不同类型的输入:

  1. 字符串
    data_files 是一个字符串时,它通常表示一个单独的文件路径。但 data_file 参数也支持使用通配符和模式匹配,这样可以轻松加载符合特定模式的多个文件。常用的通配符包括:

    • *:匹配任意数量的字符。
    • ?:匹配单个字符。
    • **:匹配任意目录层级(通常在支持递归匹配的情况下使用)。

    例如:

    # 加载所有 CSV 文件
    dataset = load_dataset("csv", data_files="path/to/folder/*.csv")
    # 加载所有 JSON 文件,递归查找子目录
    dataset = load_dataset("json", data_files="path/to/folder/**/*.json")
    
  2. 列表
    data_files 是一个列表时,列表中的每个元素都表示一个文件路径。load_dataset() 函数会将这些文件合并为一个数据集。例如:

    # 加载多个 JSON 文件并合并
    dataset = load_dataset("json", data_files=["path/to/file1.json", "path/to/file2.json", "path/to/file3.json"])
    
  3. 词典
    data_files 是一个词典时,词典的键通常表示数据集的不同拆分(如 train/test/validation),而值则是对应文件的路径(可以是字符串或列表)。这种方式允许用户为每个拆分指定不同的文件或文件集:

    # 为不同拆分指定不同的 CSV 文件
    dataset = load_dataset("csv", data_files={
    	"train": ["path/to/train1.csv", "path/to/train2.csv"],
    	"test": "path/to/test.csv",
    	"validation": "path/to/validation.csv"
    })
    

流式加载大数据集

在 Datasets 库中,流式加载可以通过设置 load_dataset() 函数的 streaming 参数来实现。当 streaming 参数设置为 True 时,数据集会以迭代器(IterableDatasetDictIterableDataset)的形式返回,而不是普通的 Dataset 对象。具体使用方式如下:

# 流式加载数据集
dataset = load_dataset("imdb", split="train", streaming=True)

# 遍历迭代器
for example in dataset:
    pass  # do somethings

使用流式加载具有以下优点:

  • 低内存占用:因为数据是逐块加载的,所以内存占用远小于一次性加载整个数据集的方式。这对大规模数据集特别有用。
  • 快速预览数据:即使在初次运行时,流式加载也允许用户快速浏览数据,而不需要等待整个数据集下载完成。

但流式加载也有以下局现性:

  • 不支持随机访问:流式加载不支持随机访问数据集的某一特定部分,因为数据是按顺序加载的。这意味着无法直接使用 dataset[i] 访问特定的样本。

  • 有限的操作支持:某些操作(如 shuffle()train_test_split())在流式模式下可能无法使用,因为这些操作通常需要对整个数据集进行索引和重排。

从本地 Python 脚本加载数据集

有时候你需要从本地脚本(例如 .py 文件)加载自定义的数据集。你可以通过创建一个符合 Datasets 库要求的脚本,并通过它加载数据集。从本地 Python 脚本加载数据集的步骤如下:

  1. 编写数据集定义脚本
    首先,你需要编写一个 Python 脚本来定义你要加载的数据集。这个脚本通常会继承 Datasets 库中的 GeneratorBasedBuilder 类,并实现几个关键的方法。
    假设你有一个 CSV 文件 data.csv,包含 text 和 label 两个特征,它的内容可能如下::

    text,label
    This is a positive example,1
    This is a negative example,0
    Another positive example,1
    Another negative example,0
    

    接下来,创建一个新的 Python 脚本,例如 dataset_script.py,并在其中定义数据集加载逻辑:

    import csv
    
    from datasets import DatasetInfo, SplitGenerator, GeneratorBasedBuilder
    
    
    class CustomDataset(GeneratorBasedBuilder):
    	def _info(self):  # 数据集信息
    		return DatasetInfo(
    			description="这是一个简单的自定义数据集",
    			features={
    				'text': 'string',
    				'label': 'int32',
    			}
    		)
    
    	def _split_generators(self, dl_manager):  # 分割生成器
    		return [
    			SplitGenerator(name='train', gen_kwargs={'filepath': '/path/to/data.csv'}),
    		]
    
    	def _generate_examples(self, filepath):  # 数据生成器
    		with open(filepath, 'r', encoding='utf-8') as f:
    			reader = csv.DictReader(f)
    			for id_, row in enumerate(reader):
    				yield id_, {
    					'text': row['text'],
    					'label': int(row['label']),
    				}
    if __name__ == "__main__":
    	# 测试
    	dataset = CustomDataset().as_dataset(split='train')
    	print(dataset)
    

    脚本说明:

    • CustomDataset 类:继承自 GeneratorBasedBuilder,用于定义数据集的生成过程。
    • _info() 方法:定义数据集的元信息,包括描述和特征。
    • _split_generators() 方法:定义数据集的分割,这里我们只有一个训练集。
    • _generate_examples() 方法:从 CSV 文件中逐行读取数据,并生成样本。
  2. 从本地脚本加载数据集
    完成数据集定义脚本后,你可以通过以下步骤从本地加载这个数据集:

    # 从本地 Python 脚本加载自定义数据集
    dataset = load_dataset('/path/to/dataset_script.py')
    

load_dataset 的其他参数

下表是 load_dataset() 函数全部参数的描述和用法:

参数名 类型 说明
path str 数据集的路径或名称。可以是 Hugging Face Hub 上的数据集名称、本地数据集的路径、或本地数据集脚本的路径。
name Optional[str] 数据集配置的名称,用于指定数据集的特定配置。
data_dir Optional[str] 数据集配置的 data_dir。用于指定数据文件所在的目录。
data_files Optional[Union[str, Sequence[str], Mapping[str, Union[str, Sequence[str]]]]] 数据文件的路径。可以是单个文件路径、多个文件路径的列表或文件路径的映射。
split Optional[Union[str, Split]] 要加载的数据集的切分。如果未指定,将返回包含所有切分的 dict(如 traintest)。
cache_dir Optional[str] 数据集的缓存目录。如果未指定,默认为 ~/.cache/huggingface/datasets
features Optional[Features] 设置要用于此数据集的特性类型。
download_config Optional[DownloadConfig] 下载配置参数。
download_mode Optional[Union[DownloadMode, str]] 下载/生成模式。默认为 REUSE_DATASET_IF_EXISTS,即如果数据集已存在则复用。
verification_mode Optional[Union[VerificationMode, str]] 确认模式,决定在下载/处理数据集信息时要运行的检查(如校验和/大小/切分等)。
ignore_verifications bool 忽略下载/处理数据集信息的验证(如校验和/大小/切分等)。已废弃,建议使用 verification_mode 替代。
keep_in_memory Optional[bool] 是否将数据集保留在内存中。如果未指定,默认不会将数据集保留在内存中,除非明确启用。
save_infos bool 保存数据集的信息(如校验和/大小/切分等)。默认为 False
revision Optional[Union[str, Version]] 要加载的数据集脚本的版本。可以是 commit SHA、git 标签或 main 分支。
token Optional[Union[bool, str]] 用于远程文件的 Bearer token。如果为 True 或未指定,将使用本地存储的 token。
use_auth_token Optional[Union[bool, str]] 用于远程文件的 Bearer token,已废弃,建议使用 token 替代。
task str 在训练和评估期间为数据集准备的任务类型。已废弃,将在未来版本中移除。
streaming bool 是否流式加载数据,而不是一次性下载整个数据集。如果设置为 True,则数据集将以迭代方式流式加载。
num_proc Optional[int] 在本地下载和生成数据集时使用的进程数。默认情况下禁用多进程。
storage_options Optional[Dict] 实验性。传递给数据集文件系统后端的键值对。
trust_remote_code bool 是否允许执行 Hugging Face Hub 上定义的数据集脚本中的代码。默认情况下为 False
**config_kwargs dict 传递给 BuilderConfig 的其他关键字参数,用于 DatasetBuilder

load_dataset() 函数的返回值有以下几种情形:

  • DatasetDictDataset:如果 split 不为 None,则返回请求的数据集;如果 splitNone,则返回包含各个切分的 DatasetDict
  • IterableDatasetDictIterableDataset:如果 streaming=True,则返回流式加载的数据集。

数据集的其他操作

导出数据集到本地

Datasets 库提供了多种方法来将数据集导出到本地文件,以便后续使用或共享。以下是几种常见的导出方法:

  1. save_to_disk() 方法: 将整个数据集保存到本地目录,以便后续从本地加载使用。

    from datasets import load_dataset
    
    dataset = load_dataset("imdb", split="train")
    dataset.save_to_disk("path/to/local_directory")
    

    save_to_disk() 方法将 Hugging Face 数据集保存到磁盘时,会将数据集保存为一个包含多个文件和目录的文件夹。这些文件和目录包含了数据集的实际数据、元数据以及其他与数据集相关的信息。以下是 save_to_disk() 方法保存的数据集的典型结构:

      my_dataset_directory/               # 这是保存的数据集的主目录
      ├── test                            # 测试数据
      │   ├── data-00000-of-00001.arrow   # 存储数据的文件,格式为 Arrow 文件。这里包含了测试集的数据。
      │   ├── dataset_info.json           # 包含测试集的元数据,如数据集名称、描述等信息。
      │   └── state.json                  # 存储数据集的状态信息,如数据加载的状态和其他相关信息。
      ├── train                           # 训练数据
      │   ├── data-00000-of-00001.arrow   
      │   ├── dataset_info.json  
      │   └── state.json  
      ├── unsupervised                    # 无监督数据(其他数据集可能是验证集)
      │   ├── data-00000-of-00001.arrow  
      │   ├── dataset_info.json  
      │   └── state.json  
      └── dataset_dict.json  # 存储整个数据集的词典信息,包括所有拆分(train, test, unsupervised)和它们的配置。
    
  2. to_csv() 方法: 将数据集导出为 CSV 文件。

    dataset.to_csv("path/to/file.csv")
    
  3. to_json() 方法: 将数据集导出为 JSON 文件。

    dataset.to_json("path/to/file.json")
    
  4. to_parquet() 方法: 将数据集导出为 Parquet 文件,这是一种高效的列式存储格式。

    dataset.to_parquet("path/to/file.parquet")
    

从目录加载本地数据集

load_from_disk() 方法通常用于读取已经通过 save_to_disk() 方法保存的数据集目录。具体使用方法如下:

from datasets import load_from_disk

dataset = load_from_disk("path/to/local_directory")

load_from_disk() 方法要求读取的目录必须满足一下两点,

  • 必须包含 dataset.arrow 文件:这个文件包含了数据集的实际数据,load_from_disk() 需要读取它来加载数据集。
  • 必须包含 dataset_info.json 和 state.json 文件:这些文件提供了数据集的元数据和状态信息,确保数据集被正确加载。

如果你的数据集目录不符合这些要求,load_from_disk() 方法将无法成功加载数据集。

随机打乱数据集

shuffle() 方法用于随机打乱数据集中的样本,以避免训练过程中的顺序偏差。具体使用方式如下:

shuffled_dataset = dataset.shuffle()

全部参数如下表所示:

参数名称 类型 描述
seeds Optional[Union[int, Dict[str, Optional[int]]](可选) 用于初始化默认随机数生成器的种子。如果 None,则从操作系统获取不可预测的随机数。如果提供整数或整数数组,将用于初始化随机数生成器。可以为数据集词典中的每个数据集提供一个种子。
seed Optional[int](可选) 用于初始化默认随机数生成器的种子。是 seeds 的别名。如果同时提供 seedsseed,将引发 ValueError
generators Optional[Dict[str, np.random.Generator]](可选) 用于计算数据集行排列的 NumPy 随机生成器。如果为 None(默认),则使用 np.random.default_rng。需要为数据集词典中的每个数据集提供一个生成器。
keep_in_memory bool(可选) 是否将数据集保存在内存中,而不是写入到缓存文件中。默认为 False
load_from_cache_file Optional[bool](可选) 如果可以识别到存储当前计算的缓存文件,是否使用缓存文件而不是重新计算。默认为 True(如果启用缓存)。
indices_cache_file_names Optional[Dict[str, Optional[str]]](可选) 提供缓存文件的路径名,用于存储索引映射,而不是自动生成的文件名。需要为数据集词典中的每个数据集提供一个文件名。
writer_batch_size Optional[int](可选) 写入缓存文件时的批量大小。默认为 1000。此值在处理期间的内存使用和处理速度之间提供了良好的平衡。

如果只设置了 seed:数据集会根据该种子生成随机的打乱顺序,每次运行时会根据这个 seed 值确保顺序一致。

如果只设置了 indices_cache_file_name:数据集会从指定的缓存文件中加载打乱的顺序,这样在不同的运行之间可以保持顺序一致,且不依赖 seed

如果同时设置了 seedindices_cache_file_name

  • 如果缓存文件存在且有效,数据集会优先使用 indices_cache_file_name 中的打乱顺序,而忽略 seed 的值。
  • 如果缓存文件不存在或无效,数据集会根据 seed 生成新的打乱顺序,并保存到指定的缓存文件中。

拆分数据集

train_test_split() 方法将数据集拆分为训练集和测试集,支持按比例或固定样本数进行拆分。具体使用方式如下:

train_test_split_dataset = dataset.train_test_split(test_size=0.2)
train_dataset = train_test_split_dataset['train']
test_dataset = train_test_split_dataset['test']

train_test_split() 方法支持多种参数来灵活控制数据集的划分方式,下表对其参数进行了说明:

参数名称 类型 描述
test_size Union[float, int, None](可选) 测试集大小。如果是浮点数,表示测试集占数据集的比例(0.0 到 1.0 之间)。如果是整数,表示测试集的样本数。默认为 None,将自动设置为 train_size 的补集。如果 train_size 也为 None,则默认为 0.25。
train_size Union[float, int, None](可选) 训练集大小。如果是浮点数,表示训练集占数据集的比例(0.0 到 1.0 之间)。如果是整数,表示训练集的样本数。默认为 None,将自动设置为 test_size 的补集。
shuffle bool(可选) 是否在拆分之前打乱数据集。默认为 True
stratify_by_column Optional[str](可选) 用于分层拆分的数据列名称。如果指定,则按该列的值进行分层拆分,以保持训练集和测试集中的类别比例一致。默认为 None
seed Optional[int](可选) 用于初始化随机数生成器的种子。如果 None,则从操作系统中获取不可预测的随机数。如果是整数或整数数组,则用于初始化随机数生成器。
generator Optional[np.random.Generator](可选) 用于计算数据集行的排列的 NumPy 随机生成器。如果 generator=None(默认),则使用 np.random.default_rng
keep_in_memory bool(可选) 是否将拆分后的索引保存在内存中,而不是写入到缓存文件中。默认为 False
load_from_cache_file Optional[bool](可选) 如果可以识别到存储拆分索引的缓存文件,是否使用缓存文件而不是重新计算。默认值为 True(如果启用缓存)。
train_cache_file_name Optional[str](可选) 用于存储训练集索引的缓存文件路径。如果提供,将使用该路径存储训练集索引,而不是自动生成的文件名。
test_cache_file_name Optional[str](可选) 用于存储测试集索引的缓存文件路径。如果提供,将使用该路径存储测试集索引,而不是自动生成的文件名。
writer_batch_size int(可选) 写入缓存文件时的批量大小。默认为 1000。此值在处理期间的内存使用和处理速度之间提供了良好的平衡。
train_new_fingerprint Optional[str](可选) 训练集转换后的新指纹。如果 None,则使用前一个指纹和转换参数的哈希值计算新指纹。
test_new_fingerprint Optional[str](可选) 测试集转换后的新指纹。如果 None,则使用前一个指纹和转换参数的哈希值计算新指纹。

Transformers 库

什么是 Transformers 库

Transformers 库是由 Hugging Face 开发的一个开源库,专注于处理预训练语言模型,广泛应用于自然语言处理(NLP)任务。以下是 Transformers 库的主要特点:

  • 支持多种预训练模型:包括 BERT、GPT、T5、RoBERTa、DistilBERT 等众多主流的语言模型。
  • 跨框架支持:可以在 PyTorch 和 TensorFlow 上无缝运行。
  • 任务多样性:支持多种 NLP 任务,包括文本分类、文本生成、文本翻译、问答系统、序列标注等。
  • 集成 Hugging Face Hub:用户可以方便地加载和分享模型,轻松访问数百个预训练模型和数据集。
  • 易用性:提供了高层 API,简化了模型加载、微调和推理的过程。

Transformers 库的核心组件主要有以下两种:

  • 预训练模型(Pretrained Models):Transformers 提供了大量的预训练模型,可以通过 AutoModelAutoModelFor* 等接口轻松加载模型,并进行训练或推理。
  • 分词器(Tokenizers):分词器用于将原始文本转换为模型能够理解的 token,并将模型输出的 token 重新转化为可读文本。Transformers 库的分词器支持多种不同的分词方法和策略,可以通过 AutoTokenizer 自动根据所加载的模型选择合适的分词器,简化了分词器的使用。

预训练模型

预训练模型的概述

Transformers 库提供了大量的预训练模型,这些模型在大规模数据集上经过训练,学习了丰富的语言特征,能够被用于不同的自然语言处理(NLP)任务。预训练模型的核心思想是通过在通用任务上预先学习语言表示,然后通过微调(fine-tuning)在特定任务上进行优化。这种方式可以大大减少在小规模数据集上训练模型的时间和资源需求,同时提升模型的性能。

以下是一些常见的预训练模型:

  • BERT:双向编码器模型,使用掩码语言模型(MLM)训练,擅长文本分类、命名实体识别、问答系统等自然语言理解任务。

  • RoBERTa:改进版 BERT,通过优化训练策略提升性能,应用于自然语言理解任务。

  • ALBERT:改进版 BERT,通过参数共享和因子分解技术减少模型参数,提升训练效率,擅长文本分类、命名实体识别、问答系统等自然语言理解任务。

  • DistilBERT:轻量版 BERT,适合资源有限的应用场景,性能稍低但更高效。

  • GPT 系列:解码器模型,基于自回归语言模型进行训练,擅长文本生成、对话系统等自然语言生成任务。

  • T5:编码器-解码器模型,将所有任务视为文本到文本的转换,擅长机器翻译、文本摘要、问答生成等任务。

  • BART:编码器-解码器模型,结合双向编码和自回归解码,通过将输入文本的部分打乱或损坏进行训练,擅长文本生成、文本摘要等任务。

  • mBART:多语言版本的 BART,支持多语言翻译和生成任务,擅长跨语言文本生成和理解。

预训练模型的分类

  1. 只包含编码器的模型(Encoder-only Models)

    • 代表模型:BERT、RoBERTa、ALBERT、DistilBERT 等。
    • 工作原理:此类模型仅使用 Transformer 的编码器部分,专注于通过编码器部分生成输入文本的语义表示,而不是生成新文本,所以也被称为自编码模型。在预训练阶段,这些模型通过掩码语言模型(MLM)任务,接收包含随机遮挡单词的文本作为输入,并通过上下文预测被遮挡的单词。这种训练方式使模型能够捕捉双向上下文信息,从而更全面地理解语言结构。
    • 擅长任务:自编码模型擅长自然语言理解任务,例如自编码模型擅长自然语言理解任务,例如文本分类、命名实体识别、抽取式问答、句子对分类、情感分析等。
  2. 只包含解码器的模型(Decoder-only Models)

    • 代表模型:GPT 系列模型(GPT-2、GPT-3、GPT-4 等)。
    • 工作原理:这类模型仅使用 Transformer 的解码器部分,基于自回归(autoregressive)原理,逐步生成文本,所以也被称为自回归模型。模型在每一步使用之前生成的文本作为新输入,预测下一个词。在训练过程中,模型通过学习连续词的生成模式,不需要额外的标注数据。
    • 擅长任务:只包含解码器的模型擅长自然语言生成任务,例如文本生成、对话生成、语言翻译、文本补全等。
  3. 包含编码器和解码器的模型(Encoder-Decoder Models)

    • 代表模型:T5、BART、mBART、Transformer 等。
    • 工作原理:这类模型是典型的 Transformer 结构,使用了编码器和解码器两部分的组合。编码器首先将输入序列(如文本)转化为潜在的语义表示,解码器基于编码器的输出,逐步生成输出序列。由于具备双向上下文理解能力的编码器与自回归生成能力的解码器,模型能够完成输入到输出的复杂映射,常用于处理输入和输出都是序列的任务。
    • 擅长任务:包含解码器和编码器的模型擅长处理序列到序列任务,例如机器翻译、文本摘要、问答生成、复述、文本修复等任务,结合了自然语言理解和生成能力。

使用在线 API 进行推理

Hugging Face 提供了简单易用的在线 API,可以让你通过 HTTP 请求进行模型推理。下面是如何使用在线 API 进行推理的详细步骤:

  1. 获取 API 密钥
    如果你打算使用 Hugging Face 的在线 API,首先需要获取一个 API 密钥。即使 Hugging Face 提供了一些匿名访问的方式,建议注册一个账户并获取 API 密钥,以便进行身份验证和获取更高的请求限制。获取 API 密钥的步骤如下:

    • 登录自己的 Hugging Face 账号。
    • 在账户设置下的 Access Tokens 模块下创建一个新的 token,并将其保存以备后续使用。建议将权限全部进行勾选,但是需要将其保存到安全的地方:
      image

    image

    注意
    Token 只能在创建时查看,之后无法再次查看。如果忘记了创建好的 Token,则只能删除重新创建。

  2. 安装 requests 库
    你需要使用 requests 库来发送 HTTP 请求。如果你还没有安装这个库,可以通过以下命令进行安装:

    pip install requests
    
  3. 选择合适的模型
    Hugging Face 提供了众多预训练模型,你可以从 Hugging Face 模型库中选择合适的模型。例如,你可以选择 BERT、GPT-3、T5 等模型,根据你的需求进行推理。
    image

    但不是所有模型库中的模型都可以进行在线推理,部分模型可能由于其使用频率或活动量不足,暂时无法通过在线 API 进行推理:
    image

  4. 构建 HTTP 请求
    在可以进行在线推理的模型主页,找到其推理 API 的代码:
    image
    可以看到 Hugging Face 提供了 Python、JavaScript、curl 三种 API 的调用的方式
    image
    将案例中的 Python 代码复制到自己的 IDE 中,只需填写自己的 Token 即可进行推理。API 调用的响应通常是 JSON 格式,其中包含模型的输出。根据模型的不同,输出格式可能有所不同,例如生成的文本、分类标签、预测结果等。你可以根据响应内容进行进一步处理和分析。

加载预训练模型

加载预训练模型主要通过 from_pretrained() 方法实现。你可以使用该方法从 Hugging Face 模型库下载并加载模型。

  1. 加载模型
    从 Hugging Face 模型库加载模型通常使用的是 AutoModel 类,它可以根据模型名称自动选择合适的模型架构,例如:

    from transformers import AutoModel
    
    # 加载预训练模型
    model = AutoModel.from_pretrained("bert-base-chinese")
    

    但根据任务的不同,Hugging Face 还提供了不同类型的模型加载方式,以下是几种常见的预训练模型加载方式:

    • 加载用于文本分类的模型
      如果你需要加载适用于文本分类任务的模型,可以使用 AutoModelForSequenceClassification,该模型在基础模型的基础上增加了一个分类头。例如:

      model = AutoModelForSequenceClassification.from_pretrained("bert-base-chinese")
      
    • 加载用于问答的模型
      对于问答任务,你可以使用 AutoModelForQuestionAnswering,它包含一个问答任务头。例如:

      model = AutoModelForQuestionAnswering.from_pretrained("bert-base-chinese")
      
    • 加载用于文本生成的模型
      如果你需要加载适用于生成任务(如语言模型、对话生成等)的模型可以使用 AutoModelForCausalLM。例如 GPT-2:

      model = AutoModelForCausalLM.from_pretrained("uer/gpt2-chinese-cluecorpussmall")
      
    • 加载用于序列标注的模型
      如果你需要进行序列标注任务(如命名实体识别),可以使用 AutoModelForTokenClassification,该模型可以为每个 token 生成一个分类标签。例如:

      model = AutoModelForTokenClassification.from_pretrained("bert-base-chinese")
      
  2. 使用缓存加载模型
    Hugging Face 的 Transformers 库会将下载的模型缓存到本地,以加快后续的加载速度。默认缓存位置通常是:

    • Windows:C:\Users\%USERNAME%\.cache\huggingface\hub
    • Linux:~\.cache\huggingface\hub

    如果模型已经存在于缓存目录中,from_pretrained() 方法会直接从缓存中加载模型,而不会重复下载。如果想要修改缓存位置,可以通过 cache_dir 参数在 from_pretrained() 方法中指定自定义的缓存目录。例如:

    model = AutoModel.from_pretrained("bert-base-chinese", cache_dir="/path/to/your/cache/dir")
    

    并且 cache_dir 的优先级高于环境变量(HF_HOMETRANSFORMERS_CACHE 以及 HF_DATASETS_CACHE)设置的缓存位置。

  3. 加载本地模型
    也可以直接 from_pretrained() 方法从本地文件夹加载模型。以下是详细步骤和说明

    • 确保模型文件存在

      在加载本地模型之前,确保模型文件已经下载并保存到本地。以 uer/gpt2-chinese-cluecorpussmall 的模型仓库为例:

      image

      下表是对模型仓库主要文件的说明:

      文件名 描述
      pytorch_model.bintf_model.h5 模型权重文件,包含训练好的权重
      config.json 模型配置文件,定义模型的结构和参数
      tokenizer_config.json 分词器配置文件,包含分词器的设置
      vocab.txt 词汇表文件,用于将词映射到索引
      merges.txt 用于 BPE 分词的合并规则文件(如果适用)
      special_tokens_map.json 特殊标记配置文件,定义模型特殊标记

      如果本地不存在该模型,可以使用 Git 将该模型克隆指定目录:
      image
      image

    • 从本地路径加载模型
      使用 from_pretrained() 方法时,只需将本地模型路径传递给该方法。以下是如何从本地路径加载模型的示例:

      model = AutoModel.from_pretrained("/path/to/local/model")
      

      如果模型已经下载并缓存到 Hugging Face 的缓存目录 %HF_HOME% 中,那么模型以及分词器的文件均存储在 %HF_HOME%/hub/model--<模型名称>/snapshots/<snapshot-id> 目录中,所以想要加载缓存目录中的模型要么直接将模型名称传递给该方法,要么就直接将存储模型文件目录的路径传递给该方法。

模型类的 from_pretrained() 方法加载得到的是一个模型对象,输出它将得到该模型的结构信息,例如输出基础模型类 AutoModel 加载的模型:

model = AutoModel.from_pretrained("bert-base-chinese")
print(model)
输出结果
BertModel(
  (embeddings): BertEmbeddings(
    (word_embeddings): Embedding(21128, 768, padding_idx=0)
    (position_embeddings): Embedding(512, 768)
    (token_type_embeddings): Embedding(2, 768)
    (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
    (dropout): Dropout(p=0.1, inplace=False)
  )
  (encoder): BertEncoder(
    (layer): ModuleList(
      (0-11): 12 x BertLayer(
        (attention): BertAttention(
          (self): BertSdpaSelfAttention(
            (query): Linear(in_features=768, out_features=768, bias=True)
            (key): Linear(in_features=768, out_features=768, bias=True)
            (value): Linear(in_features=768, out_features=768, bias=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (output): BertSelfOutput(
            (dense): Linear(in_features=768, out_features=768, bias=True)
            (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
        (intermediate): BertIntermediate(
          (dense): Linear(in_features=768, out_features=3072, bias=True)
          (intermediate_act_fn): GELUActivation()
        )
        (output): BertOutput(
          (dense): Linear(in_features=3072, out_features=768, bias=True)
          (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
      )
    )
  )
  (pooler): BertPooler(
    (dense): Linear(in_features=768, out_features=768, bias=True)
    (activation): Tanh()
  )
)

输出以分类模型类 AutoModelForTokenClassification 加载得到的模型对象:

# 加载预训练模型
model = AutoModelForTokenClassification.from_pretrained("bert-base-chinese")
print(model)
查看结果
BertForTokenClassification(
  (bert): BertModel(
    (embeddings): BertEmbeddings(
      (word_embeddings): Embedding(21128, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (token_type_embeddings): Embedding(2, 768)
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (encoder): BertEncoder(
      (layer): ModuleList(
        (0-11): 12 x BertLayer(
          (attention): BertAttention(
            (self): BertSdpaSelfAttention(
              (query): Linear(in_features=768, out_features=768, bias=True)
              (key): Linear(in_features=768, out_features=768, bias=True)
              (value): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
            (output): BertSelfOutput(
              (dense): Linear(in_features=768, out_features=768, bias=True)
              (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
          )
          (intermediate): BertIntermediate(
            (dense): Linear(in_features=768, out_features=3072, bias=True)
            (intermediate_act_fn): GELUActivation()
          )
          (output): BertOutput(
            (dense): Linear(in_features=3072, out_features=768, bias=True)
            (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
  )
  (dropout): Dropout(p=0.1, inplace=False)
  (classifier): Linear(in_features=768, out_features=2, bias=True)
)

可以明显看出 AutoModelForTokenClassification 加载得到的模型比 AutoModel 得到的模型多了一个用于分类的任务头 classifier

from_pretrained 的其他参数

AutoModelfrom_pretrained() 方法的具体参数如下表所示:

参数名 类型 说明
pretrained_model_name_or_path str or os.PathLike(必填) 模型ID、路径或URL,指向 Hugging Face Hub 上的预训练模型或本地模型目录。
model_args sequence of positional arguments(可选) 传递给模型 __init__ 方法的其余位置参数。
config PretrainedConfig or str or os.PathLike(可选) 模型配置对象,可通过配置文件自动加载。
cache_dir str or os.PathLike(可选) 下载模型配置文件的缓存目录路径。
ignore_mismatched_sizes bool(可选) 是否忽略模型权重大小不匹配的错误,默认为 False
force_download bool(可选) 是否强制重新下载模型权重和配置文件,即使它们已存在于缓存中。
local_files_only bool(可选) 是否只使用本地文件而不从远程下载,默认为 False
token str or bool(可选) Hugging Face Hub 的访问令牌,用于私有模型下载。
revision str(可选) 指定模型版本,默认为 "main",可以是分支名、标签名或提交哈希值。
use_safetensors bool(可选) 是否使用 safetensors 格式的权重文件。
state_dict Dict[str, torch.Tensor](可选) 可选的模型权重词典,用于加载自定义权重。
from_tf bool(可选) 是否从 TensorFlow 检查点加载模型权重,默认为 False
from_flax bool(可选) 是否从 Flax 检查点加载模型权重,默认为 False
output_loading_info bool(可选) 是否返回缺失键、意外键和错误消息的词典,默认为 False
proxies Dict[str, str](可选) 用于远程下载的代理服务器词典。
device_map str or Dict[str, Union[int, str, torch.device]](可选) 模型的设备映射,指定模型的各部分应该放置在哪个设备上。
torch_dtype str or torch.dtype(可选) 用于加载模型的 torch 数据类型。
quantization_config QuantizationConfigMixin or Dict(可选) 量化配置参数词典,用于模型量化。
low_cpu_mem_usage bool(可选) 尽量减少加载模型时的CPU内存使用,通常结合 device_map 使用。
subfolder str(可选) 指定模型在 Hugging Face Hub 中存储的子文件夹路径。
variant str(可选) 如果指定,则加载带有 variant 后缀的模型权重文件。
max_memory Dict(可选) 每个设备的最大内存配置。
offload_folder str or os.PathLike(可选) 如果使用磁盘卸载,指定卸载权重的文件夹。
offload_state_dict bool(可选) 是否在权重加载时将 CPU 状态词典临时卸载到硬盘。
offload_buffers bool(可选) 是否将模型参数的缓存卸载到硬盘。
kwargs dict(可选) 可用于更新配置对象或传递给模型 __init__ 方法的其他参数。

分词器

分词器的概述

分词器是将原始文本转换为模型可理解的 token(也被称为词元)的关键工具。在自然语言处理中,模型处理的并不是文本本身,而是这些文本被分词器转化为的 token 序列。不同模型可能使用不同的分词策略,如基于词、子词或字符的分词方式。不同的 NLP 模型可能采用不同的分词策略。例如,基于词的分词器会将每个单词转化为一个 token,而基于子词或字符的分词器会将文本切分为更小的单元。这些分词方式各有优缺点:

  • 基于词的分词方法对于常见词表现良好,但对于词汇量大的语言,模型的参数会变得非常庞大。
  • 基于子词的分词器(如BPE、WordPiece)能够通过将稀有单词拆分为子词,使模型能够高效地处理未知词汇。
  • 字符级分词器则通过处理字符单元保证了更好的泛化能力,但模型处理起来可能效率较低。

在 Hugging Face 的 Transformers 库中,Tokenizers 库提供了多种预训练分词器,可以根据不同的语言模型和任务需求定制分词器。例如,BERT、GPT 等模型都配备了相应的分词器,这些分词器能够智能地处理各种语言输入。

分词器的主要任务包括:

  • 编码:将输入的文本转换为模型可理解的数字化格式(如 token ID)。这一过程通常通过词汇表(vocabulary)完成,分词器会根据词汇表查找输入文本中的每个词或子词并将其映射为对应的整数 ID。

  • 解码:将模型生成的 token ID 还原为可读的文本。分词器通过与编码相反的操作,从模型输出的 token ID 序列中恢复出原始的文本。

  • 特殊 token 处理:处理在特定任务中需要的特殊标记,如句子开始(<bos>)、句子结束(<eos>)、填充(<pad>)、未知词(<unk>)等特殊 token。这些 token 在某些任务中具有重要意义,如机器翻译或文本生成。

  • 分割与合并:分词器负责将文本拆分成子词或词组进行编码,同时根据任务需要重新合并生成的子词序列。这在处理长文本或处理不同语言的复杂词形变化时尤为关键。

通过这些任务,分词器在自然语言处理管道中起到了将原始语言信号与模型之间的桥梁作用,确保输入和输出在语义上被模型准确理解和生成。

常见分词器

Transformers 库中提供了以下常见的分词器:

  • AutoTokenizer:通用分词器加载器,能够自动根据指定的模型名称加载合适的分词器。它支持多种预训练模型,如 BERT、GPT-2、RoBERTa 等,简化了分词器的选择和使用。

  • BertTokenizer:BERT 模型的专用分词器,使用 WordPiece 分词算法,将稀有词分解为子词,有效处理未见词。适用于 BERT 及其变体。

  • GPT2Tokenizer:GPT-2 模型的专用分词器,使用 BPE(Byte Pair Encoding)算法,适合生成任务。处理长文本时具有优势。

  • RobertaTokenizer:RoBERTa 模型的分词器,基于 BPE 算法,改进了 BERT 的训练过程,适合大规模数据处理任务。

  • T5Tokenizer:T5 模型的分词器,使用 SentencePiece 算法,适用于将多种任务统一为文本生成任务的场景。

  • XLNetTokenizer:XLNet 模型的分词器,使用基于 BPE 的算法,适合处理序列预测和理解任务。

  • DistilBertTokenizer:DistilBERT 模型的分词器,基于 BERT 的轻量化版本,适用于需要较少计算资源的任务。

  • AlbertTokenizer:ALBERT 模型的分词器,是 BERT 的轻量化版本,适用于需要较小模型参数量的任务。

以上分词器中,除 AutoTokenizer 通用分词器外,其余具体的分词器均有 *Fast 版本。*Fast 版本使用 Rust 实现,具有更高的分词速度,特别是在处理大规模数据时表现更佳。有时提供额外的功能,如 token 对齐信息、更优化的内存使用等。与对应的非 *Fast 版本在功能上几乎相同,但性能更优。

AutoTokenizer 分词器

AutoTokenizer 是一个通用分词器加载器,它能够自动根据指定的模型名称加载合适的分词器。它旨在提供一种灵活、统一的方法来处理不同类型的 Transformer 模型,从而简化了分词器的选择和使用。其主要功能如下所示:

  • 自动适配: AutoTokenizer 根据传入的模型名称自动选择相应的分词器。例如,对于 BERT 模型,AutoTokenizer 会加载 BertTokenizer;对于 GPT-2 模型,则加载 GPT2Tokenizer
  • 简化代码: 使用 AutoTokenizer 可以避免手动选择和加载不同模型的分词器,使代码更加简洁和灵活,特别是在需要处理多种模型的情况下。
  • 支持多种模型: 支持大多数预训练模型,包括 BERT、GPT、RoBERTa、T5、XLNet 等。你只需提供模型名称,AutoTokenizer 就能加载与之匹配的分词器

和加载 Transformer 模型类似,分词器也可以通过多种方式加载:

  • 从 Hugging Face 下载并加载分词器
    from transformers import AutoTokenizer
    
    # 根据模型名称自动加载分词器
    tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
    
  • 从缓存加载分词器:
    tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased", cache_dir="/path/to/your/cache/dir")
    
  • 从本地加载分词器
    tokenizer =  AutoTokenizer.from_pretrained("/path/to/local/model")
    

    分词器文件通常与模型文件一起存储在同一目录中,目录的详细结构请参考从本地加载模型。

分词器类的 from_pretrained() 方法加载得到的是一个分词器对象,它包含了分词器的配置信息,你可以通过输出查看:

# 加载分词器
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
print(tokenizer)
输出结果
BertTokenizerFast(
    name_or_path='bert-base-chinese',
    vocab_size=21128,
    model_max_length=512,
    is_fast=True,
    padding_side='right',
    truncation_side='right',
    special_tokens={
        'unk_token': '[UNK]',
        'sep_token': '[SEP]',
        'pad_token': '[PAD]',
        'cls_token': '[CLS]',
        'mask_token': '[MASK]'
    },
    clean_up_tokenization_spaces=True
),
added_tokens_decoder = {
    0: AddedToken("[PAD]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
    100: AddedToken("[UNK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
    101: AddedToken("[CLS]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
    102: AddedToken("[SEP]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
    103: AddedToken("[MASK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True)
}

可以从输出看出 AutoModel 类的 from_pretrained() 方法默认加载的是带 *Fast 版本的分词器。并且输出主要分为两部分:

  • 第一部分显示了分词器的基本配置:

    参数名 说明
    name_or_path 表示加载的预训练模型的名称或路径
    vocab_size 表示词汇表的大小
    model_max_length 表示模型分词器支持的最大输入长度,编码时设置 max_length 超过这个长度会报错
    is_fast 表示是否使用快速版本的分词器,快速分词器通常在处理大量数据时更加高效
    padding_side 表示填充的方向,即在序列的左侧或右侧进行截断
    truncation_side 表示截断的方向,即在序列的左侧或右侧进行截断
    special_tokens 模型使用的特殊标记及其字符串表示(如 [CLS][SEP] 等),这些标记在模型任务中具有特定的作用
    clean_up_tokenization_spaces 表示是否在分词后移除额外的空格,保持标记的紧凑性
  • 第二部分显示了分词器特殊 token 的信息:

    Token ID 特殊 Token 名称 说明
    0 [PAD] 填充标记,用于将序列填充到相同长度。
    100 [UNK] 未知标记,用于表示模型无法识别的词。
    101 [CLS] 分类标记,通常用于句子的起始位置,在分类任务中使用。
    102 [SEP] 分隔标记,用于分隔两个句子或文本片段。
    103 [MASK] 掩码标记,用于掩盖某些词,以便模型在训练时预测这些词。

from_pretrained 的其他参数

AutoTokenizerfrom_pretrained() 方法的具体参数如下表所示:

参数名称 类型 描述
pretrained_model_name_or_path stros.PathLike (必填) 模型 ID 或包含词汇表的路径。可以是预定义的模型 ID,目录路径,或单个保存的词汇文件的路径。
inputs *args (可选) 额外的位置信息参数,将传递给 Tokenizer 的 __init__() 方法。
config PretrainedConfig (可选) 配置对象,用于确定要实例化的分词器类。
cache_dir stros.PathLike (可选) 存储下载的预训练模型配置的缓存目录路径。
force_download bool (可选) 是否强制重新下载模型权重和配置文件,覆盖缓存版本。默认值为 False
resume_download bool (已弃用) 已弃用,所有下载现在都将在可能的情况下默认恢复。将在 Transformers v5 中删除。
proxies Dict[str, str] (可选) 代理服务器词典,用于请求的协议或端点。
revision str (可选) 特定的模型版本,可以是分支名、标签名或提交 ID。默认值为 "main"
subfolder str (可选) 如果相关文件位于模型仓库的子文件夹中,则指定子文件夹名称。
use_fast bool (可选) 是否使用 Rust 基于的快速分词器。如果该模型不支持快速分词器,则返回普通的 Python 分词器。默认值为 True
tokenizer_type str (可选) 要加载的分词器类型。
trust_remote_code bool (可选) 是否允许 Hub 上自定义模型定义的代码。应仅对可信的仓库设置为 True。默认值为 False
kwargs **kwargs (可选) 其他关键字参数,将传递给 Tokenizer 的 __init__() 方法。可用于设置特殊标记如 bos_token, eos_token 等。

分词器的词典与分词操作

Hugging Face 分词器的词典(vocabulary)和分词操作紧密相关。词典是分词器的核心部分,它定义了模型可以处理的所有词元(tokens)及其对应的 ID。而分词操作则是将输入文本转换为词元 ID 的过程。在处理自然语言任务时,分词器的作用是将文本变为模型可理解的格式。

以下是词典操作与分词操作相关的主要方法:

  1. 查看词典
    你可以使用分词器的 get_vocab() 方法来查看词典中的所有词元。词典以词典的形式返回,其中词元作为键,对应的 ID 作为值。例如:

    from transformers import AutoTokenizer
    
    tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
    vocab = tokenizer.get_vocab()
    print(vocab)
    

    输出结果如下:

    {'htm': 9645, '##轶': 19824, 'ب': 262, '整': 3146, '江': 3736, '楷': 3514, 'entertainment': 11679, ……}
    
  2. 查看词典大小
    查看分词器词典大小的方法可以通过以下几种方式:

    • 获取词典字典对象的长度:

      vocab = tokenizer.get_vocab()
      num_tokens = len(vocab)
      
    • 调用了分词器的 vocab_size 属性

      num_tokens = tokenizer.vocab_size
      
    • 调用了分词器的 __len__() 方法,其实是调用了 vocab_size 属性

      num_tokens = len(tokenizer)
      
  3. 添加 token
    要向分词器中添加新的词元,可以使用 add_tokens() 方法。这个方法会将新的词元添加到词典中,并更新分词器的内部状态。例如:

    new_tokens = ["世界"]
    num_added_tokens = tokenizer.add_tokens(new_tokens)
    print(f"添加了{num_added_tokens}个新词元")
    print("世界" in tokenizer.get_vocab())  # 判断是否添加成功
    
    查看结果
    添加了1个新词元
    True
    

    注意
    通过 get_vocab() 方法获取到的词典是分词器词典的副本(拷贝),而不是原始词典的直接引用。这意味着对 get_vocab() 返回的词典进行修改,并不会直接影响分词器的内部状态或词典内容。同样地,将新的词元添加到词典中,分词器内部的词典会被更新,但更改不会反映在 get_vocab() 方法已经返回的副本中。

    由于分词器的词典大小发生了变化,因此模型的嵌入层(embedding layer)也需要相应地扩展。这是因为嵌入层的大小必须与词典的大小匹配,以确保每个词元都有一个相应的嵌入向量。你可以使用 model.resize_token_embeddings() 方法来扩展模型的嵌入层大小:

    # 扩展模型的嵌入层以适应新的词典大小
    model.resize_token_embeddings(len(tokenizer))
    

    并且只需要训练被修改的嵌入层即可(其余部分在训练过程中进行冻结)。通过这种方式,可以保持模型的其余部分不变,仅对新词元的嵌入进行学习,从而有效保留预训练模型的知识,同时更新新增词元的表示。

  4. 添加特殊 token

    特殊 token 是指具有特定含义的词元,如 [CLS], [SEP] 等。你可以通过 add_special_tokens() 方法来添加新的特殊 token。例如:

    # 定义要添加的特殊 token
    special_tokens_dict = {'additional_special_tokens': ['<NEW_SPECIAL_TOKEN>']}
    num_added_tokens = tokenizer.add_special_tokens(special_tokens_dict)
    print(f"添加了 {num_added_tokens} 个特殊词元")
    print(tokenizer.all_special_tokens)  # 输出特殊词元的索引
    
    查看结果
    添加了 1 个特殊词元
    ['[UNK]', '[SEP]', '[PAD]', '[CLS]', '[MASK]', '<NEW_SPECIAL_TOKEN>']
    
  5. token 与 token id 之间的转换
    Hugging Face 分词器可以轻松实现 token 与 token ID 之间的转换,主要通过以下两个方法:

    • convert_tokens_to_ids() 方法:将 token 转换为其对应的 ID,主要可接受的参数是 tokens,可接受单个 token 或 token 列表。例如:

      from transformers import AutoTokenizer
      
      # 加载分词器
      tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
      
      # 1. 单个 token 转换为 token ID
      token = '测试'
      token_id = tokenizer.convert_tokens_to_ids(token)
      print(f"Token: {token}, Token ID: {token_id}")
      
      # 2. 多个 token 列表转换为 token ID 列表
      tokens = ['这', '是', '一个', '测试', '句子']
      token_ids = tokenizer.convert_tokens_to_ids(tokens)
      print(f"Tokens: {tokens}, Token IDs: {token_ids}")
      
      查看结果
      Token: 测试, Token ID: 100
      Tokens: ['这', '是', '一个', '测试', '句子'], Token IDs: [6821, 3221, 100, 100, 100]
      
    • convert_ids_to_tokens() 方法:将 token ID 转换回其对应的 token,主要可接受的参数是 idsids 可以是单个 token ID 或 token ID 列表。例如:

      from transformers import AutoTokenizer
      
      # 加载分词器
      tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
      
      # 1. 单个 token ID 转换为 token
      token_id = 3398
      token = tokenizer.convert_ids_to_tokens(token_id)
      print(f"Token ID: {token_id}, Token: {token}")
      
      # 2. 多个 token ID 转换为 token 列表
      token_ids = [101, 6821, 3221, 671, 3398, 2094, 102]
      tokens = tokenizer.convert_ids_to_tokens(token_ids)
      print(f"Token IDs: {token_ids}, Tokens: {tokens}")
      
      查看结果
      Token ID: 3398, Token: 柿
      Token IDs: [101, 6821, 3221, 671, 3398, 2094, 102], Tokens: ['[CLS]', '这', '是', '一', '柿', '子', '[SEP]']
      

      convert_ids_to_tokens() 方法还接收一个参数 skip_special_tokens,该参数用于控制是否在转换过程中跳过特殊 tokens,默认值为 False 即不跳过。例如:

      # 3. 多个 token ID 转换为 token 列表,并且丢弃特殊token
      token_ids = [101, 6821, 3221, 671, 3398, 2094, 102]
      tokens = tokenizer.convert_ids_to_tokens(token_ids, skip_special_tokens=True)
      print(f"Token IDs: {token_ids}, Tokens: {tokens}")
      
      点击查看代码
      Token IDs: [101, 6821, 3221, 671, 3398, 2094, 102], Tokens: ['这', '是', '一', '柿', '子']
      
  6. 分词操作
    分词操作是将输入的文本转换为词元或词元 ID 的过程。这个操作依赖于词典中的内容。可以使用分词器对象的 tokenize() 方法实现,例如:

    from transformers import AutoTokenizer
    
    # 加载分词器
    tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
    
    tokens = tokenizer.tokenize("你好,世界")  # 对文本进行分词
    token_ids = tokenizer.encode("你好,世界", add_special_tokens=False)  # 对文本进行编码
    print(f"Tokens: {tokens}, Token IDs: {token_ids}")
    
    查看结果
    Tokens: ['你', '好', ',', '世', '界'], Token IDs: [872, 1962, 8024, 686, 4518]
    

分词器的编码操作

在 Hugging Face 的分词器中,编码操作是将原始文本转换为模型可以处理的 token 序列的过程。主要包括以下步骤:

  • 分割文本: 将原始文本分割成更小的单位(如词、子词或字符)。
  • 映射为 ID: 将这些单位映射为唯一的 ID,这些 ID 是模型能够理解的。
  • 处理特殊 token: 插入模型任务所需的特殊标记(如 [CLS][SEP] 等)。
  • 对齐输入长度: 确保输入序列的长度与模型的要求一致,通常包括填充和截断。

在 Hugging Face 的分词器中,常用的编码方法有:

  • encode() 方法: 将单个句子或句子对转换为 token ID 序列,输出为一个简单的 ID 列表。
  • encode_plus() 方法: 提供更丰富的配置参数和复杂的输出,不仅返回 token ID 还有 token_type_idsattention_mask 等额外信息。
  • batch_encode_plus() 方法: 是 encode_plus() 方法的批量版本,用于批量处理多个文本,返回多个样本的编码结果。
  • __call__() 方法:tokenizer 的 __call__ 方法实际是 batch_encode_plus() 方法的封装和快捷方式,用于简化文本编码的操作。

下面对它们分别进行介绍:

  1. encode() 方法

    encode() 方法仅返回一个文本进行编码后的 token ID 列表( input_ids)。例如:

    from transformers import BertTokenizer
    def decode_tokens(token_ids):
    	return tokenizer.convert_ids_to_tokens(token_ids)
    tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
    # 编码单个文本
    encoded_single = tokenizer.encode(
    	"你好,世界",  # 文本
    	add_special_tokens=True,  # 添加特殊标记
    	padding="max_length",  # 填充到最大长度
    	truncation=True,  # 截断到最大长度
    	max_length=16,  # 最大长度
    	return_tensors="pt",  # 返回张量,
    )
    print(encoded_single)
    print(decode_tokens(encoded_single[0]))
    # 编码文本对
    encoded_pair = tokenizer.encode(
    	"今天的天气怎么样?",  # 文本对中的第一个句子
    	"今天的天气很好。",  # 文本对中的第二个句子
    	add_special_tokens=True,  # 添加特殊标记
    	padding="max_length",  # 填充到最大长度
    	truncation="only_first",  # 截断第一个句子
    	max_length=16,  # 最大长度
    	return_tensors="pt",  # 返回张量
    )
    print(encoded_pair)
    print(decode_tokens(encoded_pair[0]))
    
    输出结果 ```text tensor([[ 101, 872, 1962, 8024, 686, 4518, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) ['[CLS]', '你', '好', ',', '世', '界', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]'] tensor([[ 101, 791, 1921, 4638, 1921, 3698, 102, 791, 1921, 4638, 1921, 3698, 2523, 1962, 511, 102]]) ['[CLS]', '今', '天', '的', '天', '气', '[SEP]', '今', '天', '的', '天', '气', '很', '好', '。', '[SEP]'] ```

    这里 0、101 和 102 是 BERT 的特殊 token [PAD][CLS][SEP],表示句子的填充、开始和结束。

    encode() 方法全部参数说明如下表所示:

    参数 类型 描述
    text strList[str]List[int](必填) 要编码的第一个文本序列。可以是一个字符串、一组已分词的 token 列表(由 tokenize() 生成),或是已转换为 token ID 的整数列表。
    text_pair strList[str]List[int](可选) 可选的第二个文本序列,形式与 text 相同。适用于需要处理句子对的任务(如问答任务中的问题与答案)。
    add_special_tokens bool(可选) 是否自动添加特殊 token(如 [CLS][SEP])到文本序列中,用于标记句子的开头、结尾或句子对的分隔。可以选择 True 自动添加,False 不添加。默认值为 True
    padding boolstrPaddingStrategy(可选) 指定是否填充序列以达到相同长度。可以选择 True 按批处理中最长序列填充,'longest' 也是按批处理中最长序列填充,'max_length' 按给定的最大长度填充,选择 FalseNone 表示不进行填充。默认值为 False
    truncation boolstrTruncationStrategy(可选) 指定是否对输入进行截断。当仅有单个文本序列时,可以设置为 True 按给定的最大长度来截断;当是文本对时,使用 'only_first''only_second''longest_first' 来指定哪个序列需要截断,选择 True 时默认使用 'longest_first' 策略。选择 FalseNone 表示不进行截断。默认值为 None
    max_length int(可选) 指定要截断或填充的最大序列长度。如果设置,序列将被截断到该长度。默认值为 None,表示不进行截断。
    stride int(可选) 在处理长文本时,使用滑动窗口截断,指定窗口滑动的步幅大小,这样做的目的是确保模型能够处理长文本而不会丢失重要的信息。该值默认为 0,表示不进行窗口滑动。
    return_tensors strTensorType(可选) 返回编码后的张量类型。可选择返回 pt(PyTorch 张量)、tf(TensorFlow 张量)或 np(NumPy 数组)。如果不指定则返回 Python 列表。默认值为 None
    kwargs **kwargs(可选) 其他关键字参数,用于向后兼容旧版本的调用方式(旧版的 truncation_strategypad_to_max_length 参数)。
  2. encode_plus() 方法

    encode_plus() 方法是 encode() 方法的加强版,他提供更多的可配置参数。并且该方法返回一个包含更多信息的词典(如果不额外配置一般默认有 input_idstoken_type_idsattention_mask),具体内容由配置的方法参数决定。例如:

    from transformers import AutoTokenizer
    
    
    def decode_tokens(token_ids):
    	return tokenizer.convert_ids_to_tokens(token_ids)
    
    
    tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
    
    # 编码文本对,带有额外配置
    encoded_plus = tokenizer.encode_plus(
    	"今天的天气怎么样?",
    	"今天的天气很好。",
    	add_special_tokens=True,  # 添加特殊标记
    	padding="max_length",  # 填充到最大长度
    	truncation=True,  # 截断到最大长度
    	max_length=16,  # 最大长度
    	return_tensors=None,  # 返回张量,
    	return_token_type_ids=True,  # 返回标记类型ID
    	return_attention_mask=True,  # 返回attention mask
    	return_overflowing_tokens=True,  # 返回溢出标记
    	return_special_tokens_mask=True,  # 返回特殊标记
    	return_offsets_mapping=True,  # 返回偏移映射,只有快分词器才能设置
    	return_length=True,  # 返回编码后的序列长度
    	verbose=True  # 输出详细信息
    )
    print(encoded_plus)
    
    for input_ids in encoded_plus["input_ids"]:
    	print(decode_tokens(input_ids))
    

    注意
    truncation='longest_first' 时,并不是一次性将较长的句子截断到符合最大长度,而是通过逐步从最长句子末尾移除一个非填充 token。如果总长度仍超出 max_length,则继续对另一个句子进行相同处理,直到所有句子的总长度符合要求。最终以下四种组合:
    ① 第一句截断后的有效部分 + 第二句截断后的有效部分
    ② 第一句截断后的有效部分 + 第二句截断的部分
    ③ 第一句截断的部分 + 第二句截断后的有效部分
    ④ 第一句截断的部分 + 第二句截断的部分
    如果存在阶段部分的序列的长度仍然超出 max_length,会继续按相同策略进行截断,组层更多的组合,直到满足长度限制。
    如果设置 return_offsets_mapping=True 会将这些组合一起返回,input_ids 将是一个嵌套的列表,如果为 False 则仅会返回两句有效部分组合的序列的 Token IDs。

    查看结果
    {
      'input_ids': [
    	[101, 791, 1921, 4638, 1921, 3698, 2582, 720, 102, 791, 1921, 4638, 1921, 3698, 2523, 102],
    	[101, 3416, 8043, 102, 791, 1921, 4638, 1921, 3698, 2523, 102, 0, 0, 0, 0, 0],
    	[101, 3416, 8043, 102, 1962, 511, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    	[101, 791, 1921, 4638, 1921, 3698, 2582, 720, 102, 1962, 511, 102, 0, 0, 0, 0]
      ],
      'token_type_ids': [
    	[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
    	[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    	[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    	[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0]
      ],
      'attention_mask': [
    	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    	[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
      ],
      'special_tokens_mask': [
    	[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
    	[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
    	[1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    	[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1]
      ],
      'offset_mapping': [
    	[(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (0, 0)],
    	[(0, 0), (7, 8), (8, 9), (0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0)],
    	[(0, 0), (7, 8), (8, 9), (0, 0), (6, 7), (7, 8), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0)],
    	[(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (0, 0), (6, 7), (7, 8), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]
      ],
      'length': [16, 16, 16, 16],
      'overflow_to_sample_mapping': [0, 0, 0, 0]
    }
    
    ['[CLS]', '今', '天', '的', '天', '气', '怎', '么', '[SEP]', '今', '天', '的', '天', '气', '很', '[SEP]']
    ['[CLS]', '样', '?', '[SEP]', '今', '天', '的', '天', '气', '很', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']
    ['[CLS]', '样', '?', '[SEP]', '好', '。', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']
    ['[CLS]', '今', '天', '的', '天', '气', '怎', '么', '[SEP]', '好', '。', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']
    

    下表是对返回词典中各字段的说明:

    字段 说明
    input_ids 分词后的输入 ID 序列,包括特殊标记 [CLS], [SEP], [PAD]。它表示每个 token 在模型词汇表中的对应索引。
    token_type_ids 用于区分句子对中的不同句子。0 表示第一个句子,1 表示第二个句子。对于单个句子,全部为 0。
    attention_mask 用于指示哪些 token 是实际输入(包括 [CLS][SEP]),哪些是 [PAD]。1 表示真实的 token,0 表示 [PAD]。
    special_tokens_mask 用于标识特殊 token 的位置。1 表示特殊 token(如 [CLS], [SEP], [PAD]),0 表示普通 token。
    offset_mapping 每个 token 在原始输入文本中的字符位置范围,表示 (start, end)。对于特殊 token 的部分,通常为 (0, 0)。
    length 每个输入序列的总长度。
    overflow_to_sample_mapping 指示溢出序列与原始输入序列的映射关系。值为 0 表示所有溢出序列来自于第一个(唯一的)输入样本。用于处理长度超出模型限制的情况。

    encoded_plus() 方法的参数说明如下表所示:

    参数 类型 描述
    text strList[str]List[int](必填) 要编码的第一个文本序列。可以是一个字符串、一组已分词的 token 列表(由 tokenize() 生成),或是已转换为 token ID 的整数列表。
    text_pair strList[str]List[int](可选) 可选的第二个文本序列,形式与 text 相同。适用于需要处理句子对的任务(如问答任务中的问题与答案)。
    add_special_tokens bool(可选) 是否自动添加特殊 token(如 [CLS][SEP])到文本序列中,用于标记句子的开头、结尾或句子对的分隔。可以选择 True 自动添加,False 不添加。默认值为 True
    padding boolstrPaddingStrategy(可选) 指定是否填充序列以达到相同长度。可以选择 True 按批处理中最长序列填充,'longest' 也是按批处理中最长序列填充,'max_length' 按给定的最大长度填充,选择 FalseNone 表示不进行填充。默认值为 False
    truncation boolstrTruncationStrategy(可选) 指定是否对输入进行截断。当仅有单个文本序列时,可以设置为 True 按最大长度来截断;当是文本对时,使用 'only_first''only_second''longest_first' 来指定哪个序列需要截断,选择 FalseNone 表示不进行截断。默认值为 None
    max_length int(可选) 指定要截断或填充的最大序列长度。如果设置,序列将被截断到该长度。默认值为 None,表示不进行截断。
    stride int(可选) 在处理长文本时,使用滑动窗口截断,指定窗口滑动的步幅大小,这样做的目的是确保模型能够处理长文本而不会丢失重要的信息。该值默认为 0,表示不进行窗口滑动。
    return_tensors strTensorType(可选) 返回编码后的张量类型。可选择返回 pt(PyTorch 张量)、tf(TensorFlow 张量)或 np(NumPy 数组)。如果不指定则返回 Python 列表。默认值为 None
    is_split_into_words bool(可选) 指定输入是否已被分词器拆分成单词。如果为 True,则输入应该是单词列表,而不是字符串。默认值为 False。这个参数主要影响 text 的输入类型。
    pad_to_multiple_of int(可选) 指定填充的长度应为给定的倍数。例如,如果设置为 8,则填充后的序列长度将是 8 的倍数。默认值为 None
    return_token_type_ids bool(可选) 是否返回 token 类型 ID。如果未指定,将根据具体分词器的默认设置返回。一般默认为 True
    return_attention_mask bool(可选) 是否返回 attention mask。如果未指定,将根据具体分词器的默认设置返回。一般默认为 True
    return_overflowing_tokens bool(可选) 是否返回溢出的 tokens(当输入长度超过 max_length 时)。如果提供了输入 ID 的序列对(或一批对),且使用了 truncation_strategy = longest_firstTrue,则会抛出错误,而不是返回溢出的 tokens。默认值为 False
    return_special_tokens_mask bool(可选) 是否返回特殊 tokens 的 mask,标记哪些 tokens 是特殊的。默认值为 False
    return_offsets_mapping bool(可选) 是否返回 token 在原始文本中的偏移量((char_start, char_end))。仅在使用快分词器时可用。如果使用 Python 的分词器,此方法将引发 NotImplementedError。默认值为 False
    return_length bool(可选) 是否返回编码后的序列长度。默认值为 False
    verbose bool(可选) 是否显示详细的编码信息和警告。默认值为 True
    kwargs **kwargs(可选) 其他关键字参数,用于向后兼容旧版本的调用方式(旧版的 truncation_strategypad_to_max_length 参数)。
  3. batch_encode_plus() 方法
    batch_encode_plus() 方法是 encode_plus() 方法的批量版,支持对多个文本或文本对进行批量处理,其实 encode_plus() 方法底层调用的还是 batch_encode_plus()。与 encode_plus() 类似,它提供了更多的可配置参数,并且返回一个包含编码信息的词典。这个词典可以包含多个键,如 input_idstoken_type_idsattention_mask 等,具体返回内容取决于配置的参数。以下是使用 batch_encode_plus() 的示例:

    from transformers import AutoTokenizer
    
    
    def decode_tokens(token_ids):
    	return tokenizer.convert_ids_to_tokens(token_ids)
    
    
    tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
    
    # 编码批量文本对,带有额外配置
    batch_encoded = tokenizer.batch_encode_plus(
    	[
    		("我想吃火锅", "火锅很好吃。"),
    		"你好,世界"
    	],
    	add_special_tokens=True,  # 添加特殊标记
    	padding="max_length",  # 填充到最大长度
    	truncation=True,  # 截断到最大长度
    	max_length=16,  # 最大长度
    	return_tensors=None,  # 返回张量
    	return_token_type_ids=True,  # 返回标记类型ID
    	return_attention_mask=True,  # 返回attention mask
    	return_special_tokens_mask=True,  # 返回特殊标记
    	return_offsets_mapping=True,  # 返回偏移映射
    	return_overflowing_tokens=True, # 返回溢出标记
    	return_length=True,  # 返回编码后的序列长度
    	split_special_tokens=True,  # 分割特殊标记
    	verbose=True  # 输出详细信息
    )
    
    print(batch_encoded)
    
    # 解码每个批次的input_ids
    for input_ids in batch_encoded["input_ids"]:
    	print(decode_tokens(input_ids))
    
    查看结果
    {
    	"input_ids": [
    		[101, 2769, 2682, 1391, 4125, 7222, 102, 4125, 7222, 2523, 1962, 1391, 511, 102, 0, 0],
    		[101, 872, 1962, 8024, 686, 4518, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    	],
    	"token_type_ids": [
    		[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
    		[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    	],
    	"attention_mask": [
    		[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
    		[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    	],
    	"special_tokens_mask": [
    		[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1],
    		[1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    	],
    	"offset_mapping": [
    		[(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (0, 0), (0, 0), (0, 0)],
    		[(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]
    	],
    	"length": [16, 16],
    	"overflow_to_sample_mapping": [0, 1]
    }
    ['[CLS]', '我', '想', '吃', '火', '锅', '[SEP]', '火', '锅', '很', '好', '吃', '。', '[SEP]', '[PAD]', '[PAD]']
    ['[CLS]', '你', '好', ',', '世', '界', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']
    

    输出词典的字段具体信息和 encode_plus() 方法一致。

分词器的解码操作

分词器的解码操作是将模型预测的 token ID 序列转换回人类可读的文本过程,主要步骤包括:

  • ID 映射回文本: 将每个 token ID 反向映射到其对应的文本子词或词。
  • 移除特殊 token: 去除 [CLS][SEP][PAD] 等任务中插入的特殊标记。
  • 拼接子词: 将子词组合成完整的词。对于某些分词器(如 WordPiece),可能需要将分割的子词重新连接成原始词汇。

常用解码方法有 decode() 方法和 batch_decode() 方法:

  1. decode() 方法

    decode() 方法用于将 token ID 列表转换回原始文本字符串。主要有 skip_special_tokens 参数可以用来决定是否跳过特殊 token,默认值是 False,即不跳过。例如:

    from transformers import AutoTokenizer
    
    tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
    
    input_ids = tokenizer.encode("你好,世界")
    decode_with_special_tokens = tokenizer.decode(input_ids, skip_special_tokens=True)
    print(decode_with_special_tokens)
    decode_without_special_tokens = tokenizer.decode(input_ids, skip_special_tokens=False)
    print(decode_without_special_tokens)
    
    查看结果
    你 好 , 世 界
    [CLS] 你 好 [PAD] , 世 界 [SEP]
    
  2. batch_decode() 方法

    batch_decode() 方法是 decode() 方法的批量版本,用于一次解码多个 token ID 列表。它同样也有 skip_special_tokens 参数可以用来决定是否跳过特殊 token。例如:

    from transformers import AutoTokenizer
    
    tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
    
    input_ids_list = tokenizer.batch_encode_plus(
    	["你好,世界",
    	 "你好,今天"]
    )["input_ids"]
    
    decode_with_special_tokens = tokenizer.batch_decode(input_ids_list, skip_special_tokens=True)
    print(decode_with_special_tokens)
    decode_without_special_tokens = tokenizer.batch_decode(input_ids_list, skip_special_tokens=False)
    print(decode_without_special_tokens)
    
    输出结果
    ['你 好 , 世 界', '你 好 , 今 天']
    ['[CLS] 你 好 , 世 界 [SEP]', '[CLS] 你 好 , 今 天 [SEP]']
    

这两个方法是将模型的输出转回自然语言文本的关键步骤,特别是 batch_decode(),它能方便地处理多个序列的解码操作。

项目案例

用户评论情绪分析

首先克隆需要加载的数据集到本地,存储在本案例脚本的同级 dataset 目录下:

mkdir datasets
cd datasets
git clone https://huggingface.co/datasets/seamew/ChnSentiCorp

具体训练以及推理的代码如下所示:

import torch
import tqdm
from torch import nn
from torch.utils.data import Dataset, DataLoader
from transformers import AutoModel, AutoTokenizer

from datasets import load_dataset


# 定义数据集
class MyDataset(Dataset):
    def __init__(self, split="train"):
        super().__init__()
        self.dataset = load_dataset("arrow", data_files={
            "train": "./datasets/ChnSentiCorp/chn_senti_corp-train.arrow",
            "test": "./datasets/ChnSentiCorp/chn_senti_corp-test.arrow",
            "validation": "./datasets/ChnSentiCorp/chn_senti_corp-validation.arrow"
        }, split=split)

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, item):
        sentence = self.dataset[item]["text"]
        label = self.dataset[item]["label"]
        return sentence, label


# 定义网络
class Net(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.pretrained_model = AutoModel.from_pretrained("bert-base-chinese")  # 上游特征提取网络
        self.classifier = nn.Linear(768, num_classes)  # 下游分类网络

    def forward(self, input_ids, token_type_ids, attention_mask):
        with torch.no_grad():
            outputs = self.pretrained_model(input_ids=input_ids, token_type_ids=token_type_ids,
                                            attention_mask=attention_mask)
        outputs = self.classifier(outputs.last_hidden_state[:, 0])
        outputs = torch.softmax(outputs, dim=1)
        return outputs


class Trainer:
    def __init__(self, num_epochs, batch_size, num_classes, device=None):
        super().__init__()
        self.device = device or "cuda" if torch.cuda.is_available() else "cpu"
        self.num_epochs = num_epochs
        self.batch_size = batch_size
        self.num_classes = num_classes
        self.model = Net(num_classes=num_classes).to(self.device)
        self.optimizer = torch.optim.SGD(self.model.parameters(), lr=1e-3)
        self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(self.optimizer, T_max=self.num_epochs)
        self.criterion = nn.CrossEntropyLoss()
        self.tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
        self.best_acc = 0

        def collate_fn(datas):
            sentences = [data[0] for data in datas]
            labels = torch.LongTensor([data[1] for data in datas])
            encodings = self.tokenizer.batch_encode_plus(
                sentences,
                max_length=512,
                padding="max_length",
                truncation=True,
                return_tensors="pt",
                return_attention_mask=True,
                return_token_type_ids=True
            )
            input_ids = encodings["input_ids"]
            token_type_ids = encodings["token_type_ids"]
            attention_mask = encodings["attention_mask"]
            return input_ids, token_type_ids, attention_mask, labels

        self.collate_fn = collate_fn
        self.train_loader = DataLoader(MyDataset(split="train"), batch_size=batch_size, collate_fn=collate_fn,
                                       shuffle=True)
        self.val_loader = DataLoader(MyDataset(split="validation"), batch_size=batch_size, collate_fn=collate_fn)

    def run(self):
        for epoch in range(self.num_epochs):
            self.train(epoch)
            with torch.no_grad():
                self.val(epoch)

    def train(self, epoch):
        self.model.train()
        total_acc = 0
        total_loss = 0
        for i, (input_ids, token_type_ids, attention_mask, labels) in tqdm.tqdm(enumerate(self.train_loader),
                                                                                desc="Train",
                                                                                total=len(self.train_loader),
                                                                                delay=0.01):
            input_ids, token_type_ids, attention_mask, labels = (
                input_ids.to(self.device),
                token_type_ids.to(self.device),
                attention_mask.to(self.device),
                labels.to(self.device)
            )
            outputs = self.model(input_ids, token_type_ids, attention_mask)
            loss = self.criterion(outputs, labels)
            self.optimizer.zero_grad()
            loss.backward()
            self.optimizer.step()
            total_loss += loss.item()
            total_acc += (outputs.argmax(dim=1) == labels).sum().item()
        self.scheduler.step()
        avg_loss = total_loss / len(self.train_loader)
        avg_acc = total_acc / len(self.train_loader.dataset)
        print(f"Train - epoch: {epoch}, acc: {avg_acc:.4f}, loss: {avg_loss:.4f}")

    def val(self, epoch):
        self.model.eval()
        total_acc = 0
        total_loss = 0
        for i, (input_ids, token_type_ids, attention_mask, labels) in tqdm.tqdm(enumerate(self.val_loader),
                                                                                desc="Val",
                                                                                total=len(self.val_loader),
                                                                                delay=0.01):
            input_ids, token_type_ids, attention_mask, labels = (
                input_ids.to(self.device),
                token_type_ids.to(self.device),
                attention_mask.to(self.device),
                labels.to(self.device)
            )
            outputs = self.model(input_ids, token_type_ids, attention_mask)
            loss = self.criterion(outputs, labels)
            total_loss += loss.item()
            total_acc += (outputs.argmax(dim=1) == labels).sum().item()
        avg_loss = total_loss / len(self.val_loader)
        avg_acc = total_acc / len(self.val_loader.dataset)
        print(f"Val - epoch: {epoch}, acc: {avg_acc:.4f}, loss: {avg_loss:.4f}")
        if avg_acc > self.best_acc:
            torch.save(self.model.state_dict(), "best.pth")
            self.best_acc = avg_acc


class Predicor:
    def __init__(self, model_path, num_classes, device=None):
        super().__init__()
        self.device = device or "cuda" if torch.cuda.is_available() else "cpu"
        self.model = Net(num_classes=num_classes).to(self.device)
        self.model.load_state_dict(torch.load(model_path))
        self.model.eval()
        self.tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
        self.names = ["negative", "positive"]

        def collate_fn(sentences):
            encodings = self.tokenizer.batch_encode_plus(
                sentences,
                max_length=512,
                padding="max_length",
                truncation=True,
                return_tensors="pt",
                return_attention_mask=True,
                return_token_type_ids=True
            )
            input_ids = encodings["input_ids"]
            token_type_ids = encodings["token_type_ids"]
            attention_mask = encodings["attention_mask"]
            return input_ids, token_type_ids, attention_mask

        self.collate_fn = collate_fn

    def run(self):
        while True:
            sentence = input("输入要预测的句子:")
            if sentence == "q":
                break
            input_ids, token_type_ids, attention_mask = self.collate_fn([sentence])
            input_ids, token_type_ids, attention_mask = (
                input_ids.to(self.device),
                token_type_ids.to(self.device),
                attention_mask.to(self.device)
            )
            with torch.no_grad():
                outputs = self.model(input_ids, token_type_ids, attention_mask)
                idx = outputs.argmax(dim=1).item()
            result = {
                "sentence": sentence,
                "label": self.names[idx],
                "prob": outputs.softmax(dim=1)[0][idx].item()
            }
            print("预测结果:", result)


if __name__ == '__main__':
    # 训练模型
    trainer = Trainer(100, 8, 2)
    print(trainer.tokenizer)
    trainer.run()
    # 使用训练好的模型进行推理
    predicor = Predicor("best.pth", 2)
    predicor.run()

中文填空

首先克隆需要加载的数据集到本地,存储在本案例脚本的同级 dataset 目录下:

mkdir datasets
cd datasets
git clone https://huggingface.co/datasets/seamew/ChnSentiCorp

具体训练以及推理的代码如下所示:

import torch
import tqdm
from torch import nn
from torch.utils.data import Dataset, DataLoader
from transformers import AutoModel, AutoTokenizer

from datasets import load_dataset


# 定义数据集
class MyDataset(Dataset):
    def __init__(self, split="train"):
        super().__init__()
        self.dataset = load_dataset("arrow", data_files={
            "train": "./datasets/ChnSentiCorp/chn_senti_corp-train.arrow",
            "test": "./datasets/ChnSentiCorp/chn_senti_corp-test.arrow",
            "validation": "./datasets/ChnSentiCorp/chn_senti_corp-validation.arrow"
        }, split=split).filter(lambda x: len(x["text"]) > 30)  # 长度大于30的句子才保留

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, item):
        sentence = self.dataset[item]["text"]
        return sentence


# 定义网络
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.pretrained_model = AutoModel.from_pretrained("bert-base-chinese")  # 上游特征提取网络
        self.tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
        self.decoder = nn.Linear(768, self.tokenizer.vocab_size)  # 下游解码器网络

    def forward(self, input_ids, token_type_ids, attention_mask):
        with torch.no_grad():
            outputs = self.pretrained_model(input_ids=input_ids,
                                            token_type_ids=token_type_ids,
                                            attention_mask=attention_mask)
        outputs = self.decoder(outputs.last_hidden_state[:, 15])  # 使用解码器对所以为15的token进行预测
        return outputs


class Trainer:
    def __init__(self, num_epochs, batch_size, device=None):
        super().__init__()
        self.device = device or "cuda" if torch.cuda.is_available() else "cpu"
        self.num_epochs = num_epochs
        self.batch_size = batch_size
        self.model = Net().to(self.device)
        self.optimizer = torch.optim.AdamW(self.model.parameters(), lr=1e-3)
        self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(self.optimizer, T_max=self.num_epochs)
        self.criterion = nn.CrossEntropyLoss()
        self.tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
        self.best_acc = 0

        def collate_fn(sentences):
            encodings = self.tokenizer.batch_encode_plus(
                sentences,
                max_length=30,
                padding="max_length",
                truncation=True,
                return_tensors="pt",
                return_attention_mask=True,
                return_token_type_ids=True
            )
            input_ids = encodings["input_ids"]
            token_type_ids = encodings["token_type_ids"]
            attention_mask = encodings["attention_mask"]

            labels = input_ids[:, 15].reshape(-1).clone()  # 将15位置的token保存下来作为 label
            input_ids[:, 15] = self.tokenizer.mask_token_id  # 将15位置的token替换为mask_token_id

            return input_ids, token_type_ids, attention_mask, labels

        self.collate_fn = collate_fn
        self.train_loader = DataLoader(MyDataset(split="train"), batch_size=batch_size, collate_fn=collate_fn,
                                       shuffle=True)
        self.val_loader = DataLoader(MyDataset(split="validation"), batch_size=batch_size, collate_fn=collate_fn)

    def run(self):
        for epoch in range(self.num_epochs):
            self.train(epoch)
            with torch.no_grad():
                self.val(epoch)

    def train(self, epoch):
        self.model.train()
        total_acc = 0
        total_loss = 0
        for i, (input_ids, token_type_ids, attention_mask, labels) in tqdm.tqdm(enumerate(self.train_loader),
                                                                                desc="Train",
                                                                                total=len(self.train_loader),
                                                                                delay=0.01):
            input_ids, token_type_ids, attention_mask, labels = (
                input_ids.to(self.device),
                token_type_ids.to(self.device),
                attention_mask.to(self.device),
                labels.to(self.device)
            )
            outputs = self.model(input_ids, token_type_ids, attention_mask)
            loss = self.criterion(outputs, labels)
            self.optimizer.zero_grad()
            loss.backward()
            self.optimizer.step()
            total_loss += loss.item()
            total_acc += (outputs.argmax(dim=1) == labels).sum().item()
        self.scheduler.step()
        avg_loss = total_loss / len(self.train_loader)
        avg_acc = total_acc / len(self.train_loader.dataset)
        print(f"Train - epoch: {epoch}, acc: {avg_acc:.4f}, loss: {avg_loss:.4f}")

    def val(self, epoch):
        self.model.eval()
        total_acc = 0
        total_loss = 0
        for i, (input_ids, token_type_ids, attention_mask, labels) in tqdm.tqdm(enumerate(self.val_loader),
                                                                                desc="Val",
                                                                                total=len(self.val_loader),
                                                                                delay=0.01):
            input_ids, token_type_ids, attention_mask, labels = (
                input_ids.to(self.device),
                token_type_ids.to(self.device),
                attention_mask.to(self.device),
                labels.to(self.device)
            )
            outputs = self.model(input_ids, token_type_ids, attention_mask)
            loss = self.criterion(outputs, labels)
            total_loss += loss.item()
            total_acc += (outputs.argmax(dim=1) == labels).sum().item()
        avg_loss = total_loss / len(self.val_loader)
        avg_acc = total_acc / len(self.val_loader.dataset)
        print(f"Val - epoch: {epoch}, acc: {avg_acc:.4f}, loss: {avg_loss:.4f}")
        if avg_acc > self.best_acc:
            torch.save(self.model.state_dict(), "best.pth")
            self.best_acc = avg_acc


class Predicor:
    def __init__(self, model_path, device=None):
        super().__init__()
        self.device = device or "cuda" if torch.cuda.is_available() else "cpu"
        self.model = Net().to(self.device)
        self.model.load_state_dict(torch.load(model_path))
        self.model.eval()
        self.tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")

        def collate_fn(sentences):
            encodings = self.tokenizer.batch_encode_plus(
                sentences,
                max_length=30,
                padding="max_length",
                truncation=True,
                return_tensors="pt",
                return_attention_mask=True,
                return_token_type_ids=True
            )
            input_ids = encodings["input_ids"]
            token_type_ids = encodings["token_type_ids"]
            attention_mask = encodings["attention_mask"]

            labels = input_ids[:, 15].reshape(-1).clone()
            input_ids[:, 15] = self.tokenizer.mask_token_id
            return input_ids, token_type_ids, attention_mask, labels

        self.collate_fn = collate_fn
        self.test_loader = DataLoader(MyDataset(split="test"), batch_size=32, collate_fn=collate_fn)

    def run(self):
        total_acc = 0
        for i, (input_ids, token_type_ids, attention_mask, labels) in tqdm.tqdm(enumerate(self.test_loader),
                                                                                desc="Test",
                                                                                total=len(self.test_loader),
                                                                                delay=0.01):
            input_ids, token_type_ids, attention_mask, labels = (
                input_ids.to(self.device),
                token_type_ids.to(self.device),
                attention_mask.to(self.device),
                labels.to(self.device)
            )
            with torch.no_grad():
                outputs = self.model(input_ids, token_type_ids, attention_mask)
            idxs = outputs.argmax(dim=1)
            total_acc += (idxs == labels).sum().item()
            print(self.tokenizer.decode(input_ids[0]))
            print("标签:", self.tokenizer.decode(labels[0]))
            print("预测结果:", self.tokenizer.decode(idxs[0]))
        print("准确率:", total_acc / len(self.test_loader.dataset))


if __name__ == '__main__':
    # 训练模型
    trainer = Trainer(100, 32)
    trainer.run()
    # 使用训练好的模型进行推理
    predicor = Predicor("best.pth")
    predicor.run()
posted @ 2024-09-02 15:04  gokamisama  阅读(551)  评论(0)    收藏  举报