在Graphcore智能处理器(IPU)上,使用Hugging Face Optimum库为您的数据集进行预训练Transformer模型微调有多简单?这篇博文将对此进行举例说明,提供分步指南和一个notebook(采用被广泛使用的胸部X射线大型数据集),并训练一个视觉transformer(ViT)模型。
介绍视觉transformer(ViT)模型
2017年,一组谷歌人工智能研究人员发表了一篇介绍transformer模型架构的论文。transformer以新颖的自注意力机制为特征,被认为是面向语言应用的新的高效模型。事实上,在过去的五年,transformer经历了爆炸性的普及,并且现在已经被接受为自然语言处理(NLP)的事实标准。
用于语言的transformer最显著的特点是快速演变的GPT和BERT模型系列。作为不断扩大的Hugging Face Optimum Graphcore库的一部分,两者都可以在Graphcore IPU上轻松高效地运行。

在Hugging Face网站上可以找到关于transformer模型架构的深度详解(重点是自然语言处理)。
Transformer在语言方面取得了初步成功,它的用途非常广泛,可用于一系列其他目的,包括计算机视觉(CV),我们将在这篇文章中进行介绍。
在计算机视觉(CV)领域,卷积神经网络(CNN)无疑是最受欢迎的架构。然而,谷歌研究院在2021年的一篇论文中首次提出的视觉transformer(ViT)架构代表了图像识别方面的一个突破,并使用transformer机制作为其主要组成部分。
BERT和其他基于transformer的语言处理模型将一个句子(即单词列表)作为输入,而ViT模型将输入图像分成几个小块,相当于语言处理中的单个单词。每个小块都被transformer模型线性编码为一个可以单独处理的向量表示。这种将图像分割成小块或视觉标记的方法与CNN使用的像素阵列形成对比。
由于预训练,ViT模型学会了图像的内部表示,然后可用于提取对下游任务有用的视觉特征。例如,你可以通过在预训练的视觉编码器上放置线性层,在新的标记图像数据集上训练分类器。通常会在[CLS]标记的顶部放置线性层,因为这个标记的最后的隐藏状态可以被视为整个图像的表示。

与CNN相比,ViT模型以较低的计算成本显示出更高的识别准确度,并用于一系列应用,包括图像分类、物体检测和分割。仅在医疗领域的用例就包括新冠病毒、股骨骨折、肺气肿、乳腺癌和阿尔茨海默病的检测和分类,以及许多其他案例。
ViT模型——与IPU绝配
Graphcore IPU特别适合ViT模型,因为它能够利用数据流水线和模型并行化的组合对训练进行并行训练。通过IPU的MIMD架构和以IPU-Fabric为中心的扩展解决方案,使加速这一大规模的并行进程成为可能。
通过引入流水线并行,增加了每个数据并行实例可处理的批次大小,提高了一个IPU处理的存储区域的访问效率,缩短了数据并行学习的参数聚合的通信时间。
由于在开源的Hugging Face Optimum Graphcore库中增加了一系列预优化的transformer模型,在IPU上运行和微调ViT等模型时,可以轻松实现令人难以置信地高程度的性能和效率。
通过Hugging Face Optimum,Graphcore已发布随时可用的IPU训练的模型检查点和配置文件,使其能够轻松地以最大效率训练模型。这一点特别有用,因为ViT模型通常需要对大量的数据进行预训练。我们提供了可用的检查点,所以你不需要再提供检查点。通过让用户即插即用任何公共数据集,Optimum缩短了人工智能模型的整体开发周期,并允许无缝集成到Graphcore最先进的硬件上,使价值实现的时间更快。
在这篇文章中,我们将使用一个在ImageNet-21k上预训练的ViT模型,该模型基于Dosovitskiy等人所著的论文《一个图像等同于16×16个字:用于规模化图像识别的transformer》。作为例子,我们将向你展示在ChestX-ray14数据集上使用Optimum微调ViT的过程。
ViT模型对于X射线分类的价值
与所有医学影像任务一样,放射科医生花了很多年时间学习如何有效且可靠地检测问题,并根据X光片做出初步诊断。在很大程度上,这种困难都源于图像的微小差异和空间限制,这就是为什么计算机辅助检测和诊断(CAD)技术在改善临床医生工作流程和病患结果方面显示出如此巨大的影响潜力。
同时,开发任何X光片分类模型(ViT或其他)都会带来相当多的挑战。
如上所述,为了使用Hugging Face Optimum进行演示,我们并不需要从头开始训练ViT。相反,我们将使用托管在Hugging Face模型中心的模型权重。
由于一张X光片可能显示多种疾病,我们将使用多标签分类模型。考虑的模型是google/vit-base-patch16-224-in21k。它已经从TIMM资源库中转换出来,并在ImageNet-21k的1400万张图像上进行了预训练。为了并行化和优化IPU的工作,已经通过Graphcore-ViT模型卡提供了这一配置。
如果这是您第一次使用IPU,请阅读《IPU程序员指南》以了解基本概念。要在IPU上运行您自己的PyTorch模型,请参阅《Pytorch基础教程》,并通过我们的Hugging Face Optimum教程学习如何使用Optimum。
在ChestXray-14数据集上训练ViT
首先,我们需要下载美国国立卫生研究院(NIH)临床中心的胸部X光片数据集。这个数据集包含了1992年至2015年期间30805名患者的112120张去识别的正面视图X光片。该数据集基于使用自然语言处理技术从放射学报告文本中挖掘出的标签,涵盖了14种常见疾病。

设置环境
以下是运行此演练的要求:
- 一台装有最新Poplar SDK和PopTorch环境的Jupyter Notebook服务器(参见《通过Jupyte Notebook使用IPU的指南》)。
- Graphcore教程库上的ViT Training Notebook。
Graphcore 教程资源库包含了本指南讨论的分步教程notebook和Python脚本。克隆资源库,并启动在tutorials/tutorials/pytorch/vit_model_training/中找到的walkthrough.ipynb notebook。
为了方便您使用,我们创建了创建了HF Optimum Gradient,这样您就可以在免费IPU中启动入门教程。注册并启动Runtime:

获取数据集
下载数据集的/images
目录。你可以用bash来提取文件:for f in images*.tar.gz; do tar xfz "$f"; done
。
接下来,下载Data_Entry_2017_v2020.csv
文件,该文件包含标签。默认状态下,该教程预设/images
文件夹和.csv文件与正在运行的脚本在同一个文件夹中。
当您的Jupyter环境有了数据集,您需要安装并导入最新的Hugging Face Optimum Graphcore包和requirements.txt中的其他依赖项。
%pip install -r requirements.txt
import torch
import os
import shutil
import numpy as np
import pandas as pd
import contextlib
import io
from pathlib import Path
from scipy.special import softmax
import json
import matplotlib.pyplot as plt
import optimum.graphcore as optimum_graphcore
from torchvision import transforms
import transformers
import datasets
dataset_rootdir = Path("./").absolute()
胸部X光片数据集所包含的检查由X射线图像(灰阶,224×224像素)和相应的元数据组成:Finding Labels, Follow-up #,Patient ID, Patient Age, Patient Gender, View Position, OriginalImage[宽 高]以及OriginalImagePixelSpacing[x y]
.
接下来,我们定义下载图像的位置,以及在“获取数据集”中要下载的带有标签的文件。
# Path to the extracted "images" directory
taset_rootdir / "images"
# Path to Data_Entry_2017_v2020.csv
label_file = dataset_rootdir / 'Data_Entry_2017_v2020.csv'
我们将训练Graphcore Optimum ViT模型从图像中预测疾病(由 “找到标签”定义)。“找到标签”可以是14种疾病中的任何一种或几种,或者是“未找到”标签,表示没有检测到疾病。为了与Hugging Face库兼容,文本标签需要转化为N-热编码阵列,表示对每张图像进行分类所需的多个标签。一个N-热编码阵列将标签表示为一个布尔列表,如果标签与图像对应,则为真,如果不对应,则为假。
首先,我们确定数据集中的唯一标签。
data = pd.read_csv(label_file)
# Converts the format of each label in the dataframe from "LabelA|LabelB|LabelC"
# into ["LabelA", "LabelB", "LabelC"], concatenates the
# lists together and removes duplicate labels
unique_labels = np.unique(
data['Finding Labels'].str.split("|").aggregate(np.concatenate)
).tolist()
print(f"Dataset contains the following labels:\n{unique_labels}")
Dataset contains the following labels:
['Atelectasis', 'Cardiomegaly', 'Consolidation', 'Edema', 'Effusion', 'Emphysema', 'Fibrosis', 'Hernia', 'Infiltration', 'Mass', 'No Finding', 'Nodule', 'Pleural_Thickening', 'Pneumonia', 'Pneumothorax']
现在,我们将标签转化为N-热编码阵列:
label_index = {v: i for i, v in enumerate(unique_labels)}
def string_to_N_hot(string: str):
true_index = [label_index[cl] for cl in string.split("|")]
label = np.zeros((len(unique_labels),), dtype=float)
label[true_index] = 1
return label
data["labels"] = data["Finding Labels"].apply(string_to_N_hot)
使用datasets.load_dataset
函数加载数据时,可以通过为每个标签设置文件夹(参阅 “图像文件夹”文档)或通过metadata.jsonl
文件(参阅 “包含元数据的图像文件夹”文档)提供标签。由于这个数据集中的图像可以有多个标签,我们选择使用metadata.jsonl
文件。我们将图像文件名及其相关标签写入metadata.jsonl
文件中。
data[["Image Index", "labels"]].rename(columns={"Image Index": "file_name"}).to_json(images_dir / 'metadata.jsonl', orient='records', lines=True)
创建数据集
我们现在准备创建PyTorch数据集,并将其分成训练集和验证集。这一步将数据集转换为Arrow文件格式,这样可以在训练和验证期间快速加载数据(关于Arrow和Hugging Face)。因为整个数据集正在加载和预处理,所以可能需要几分钟时间。
train_val_split = 0.05
dataset = datasets.load_dataset(
"imagefolder",
data_dir=images_dir,
)
split = dataset["train"].train_test_split(train_val_split)
dataset["train"] = split["train"]
dataset["validation"] = split["test"]
我们将从检查点google/vit-base-patch16-224-in21k导入ViT模型。该检查点是一个由Hugging Face托管的标准模型,不由Graphcore管理。
model_name_or_path = "google/vit-base-patch16-224-in21k"
为了微调预训练的模型,新的数据集必须具有与预训练所用的原始数据集相同的属性。在Hugging Face中,原始数据集的信息是通过使用AutoFeatureExtractor
加载的配置文件提供的。对于这个模型,X光片图像被重新调整大小至正确的分辨率(224×224),从灰阶转换为RGB,并在RGB通道上用平均值(0.5,0.5,0.5)和标准差(0.5,0.5,0.5)进行归一化。
feature_extractor = transformers.AutoFeatureExtractor.from_pretrained(
model_name_or_path
)
class XRayTransform:
"""
Transforms for pre-processing XRay data across a batch.
"""
def __init__(self):
self.transforms = transforms.Compose([
transforms.Lambda(lambda pil_img: pil_img.convert("RGB")),
transforms.Resize(feature_extractor.size),
transforms.ToTensor(),
transforms.Normalize(mean=feature_extractor.image_mean, std=feature_extractor.image_std),
])
def __call__(self, example_batch):
example_batch["pixel_values"] = [self.transforms(pil_img) for pil_img in example_batch["image"]]
return example_batch
# Set the training transforms
dataset["train"].set_transform(XRayTransform())
# Set the validation transforms
dataset["validation"].set_transform(XRayTransform())
为了使模型有效地运行,需要对图像进行分批处理。为了做到这一点,我们定义了一个vit_data_collator
函数,该函数将图像和标签的批次返回到字典中,遵循Transformers data collator中的default_data_collator
模式。
def batch_sampler(examples):
pixel_values = torch.stack([example["pixel_values"] for example in examples])
labels = torch.tensor([example["labels"] for example in examples])
return {"pixel_values": pixel_values, "labels": labels}
数据集可视化
为了检查数据集,我们显示元数据的前十行。
print(data.head(10))
我们也绘制一些来自验证集的图像和它们的相关标签。
fig = plt.figure(figsize=(20, 15))
unique_labels = np.array(unique_labels)
for i, data_dict in enumerate(dataset['validation']):
if i == 12:
break
image = data_dict["pixel_values"]
label = data_dict["labels"]
ax = plt.subplot(3, 4, i + 1)
ax.set_title(", ".join(unique_labels[np.argwhere(label).flatten()]))
plt.imshow(image[0]) # Plot only the first channel as they are all identical
fig.tight_layout()

至此,我们的数据集已经就绪。
准备模型
为了在IPU上训练模型,我们需要从Hugging Face Hub导入模型,并使用IPUTrainer类定义训练器。IPUTrainer类采用与原始Transformer Trainer相同的参数,并与IPUConfig对象协同工作,后者指定了IPU上的编译和执行行为。
现在我们从Hugging Face移植ViT模型。
model = transformers.AutoModelForImageClassification.from_pretrained(
model_name_or_path,
num_labels=len(unique_labels)
)
Some weights of the model checkpoint at google/vit-base-patch16-224-in21k were not used when initializing ViTForImageClassification: ['pooler.dense.weight', 'pooler.dense.bias']
- This IS expected if you are initializing ViTForImageClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
- This IS NOT expected if you are initializing ViTForImageClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
Some weights of ViTForImageClassification were not initialized from the model checkpoint at google/vit-base-patch16-224-in21k and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
要在 IPU 上使用这个模型,我们需要加载 IPU 配置 IPUConfig,它可以控制特定于 Graphcore IPU 的所有参数(可以在此处找到现有的 IPU 配置)。 我们将使用 Graphcore/vit-base-ipu。
ipu_config = optimum_graphcore.IPUConfig.from_pretrained(
"Graphcore/vit-base-ipu"
)
我们使用IPUTrainingArguments
设置我们的训练超参数。这是Hugging Face的TrainingArguments
类的子类,增加了IPU及其执行特性的特定参数。
training_args = optimum_graphcore.IPUTrainingArguments(
output_dir="./results",
overwrite_output_dir = True,
per_device_train_batch_size=1,
per_device_eval_batch_size=1,
dataloader_num_workers=8,
dataloader_drop_last=True,
num_train_epochs=3,
seed=1337,
logging_steps=50,
save_steps=1000,
remove_unused_columns=False,
warmup_ratio=0.25,
lr_scheduler_type="cosine",
learning_rate=2e-4,
ignore_data_skip=True
)
实施用于评估的自定义性能指标
多标签分类模型的性能可以用ROC(接收者操作特征)曲线下的面积(AUC_ROC)来评估。AUC_ROC是不同类别和不同阈值下的真阳性率(TPR)对假阳性率(FPR)的图。这是一个常用于多标签分类任务的性能指标,因为它对类的不平衡不敏感,而且容易解释。
对于这个数据集,AUC_ROC代表模型区分不同疾病的能力。0.5分意味着它有50%的可能得到正确的疾病,1分意味着它可以完美地区分疾病。这个指标在Datasets(数据集?D大写)中是不可用的,因此我们需要自己实现它。 HuggingFace Datasets包允许通过load_metric()
函数进行自定义指标计算。我们定义了一个compute_metrics
函数,并将其暴露给Transformer的评估函数,就像通过datasets包支持的其他指标一样。compute_metrics
函数采用ViT模型预测的标签,并计算ROC曲线下的面积。compute_metrics
函数接收一个EvalPrediction
对象(一个带有predictions
和label_ids
字段的命名元组),并必须返回一个字典字符串到float。
metric_auc = datasets.load_metric("roc_auc", "multilabel")
def compute_metrics(p):
preds = np.argmax(p.predictions, axis=1)
pred_scores = softmax(p.predictions.astype('float32'), axis=1)
auc = metric_auc.compute(prediction_scores=pred_scores, references=p.label_ids, multi_class='ovo')['roc_auc']
return {"roc_auc": auc}
为了训练模型,我们使用IPUTrainer
类定义一个训练器,它负责编译模型以在IPU上运行,并执行训练和评估。IPUTrainer
类的工作方式与Hugging Face Trainer类相同,但需要额外的ipu_config
参数。
trainer = optimum_graphcore.IPUTrainer(
model=model,
ipu_config=ipu_config,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
compute_metrics=compute_metrics,
tokenizer=feature_extractor,
data_collator=batch_sampler
)
Setting replicated_tensor_sharding to False when replication_factor=1
---------- Device Allocation -----------
Embedding --> IPU 0
Encoder 0 --> IPU 0
Encoder 1 --> IPU 0
Encoder 2 --> IPU 0
Encoder 3 --> IPU 1
Encoder 4 --> IPU 1
Encoder 5 --> IPU 1
Encoder 6 --> IPU 2
Encoder 7 --> IPU 2
Encoder 8 --> IPU 2
Encoder 9 --> IPU 3
Encoder 10 --> IPU 3
Encoder 11 --> IPU 3
Head --> IPU 3
---------------------------------------
运行训练
为了加速训练,如果存在最后的检查点,我们需要加载它。
last_checkpoint = None
if os.path.isdir(training_args.output_dir) and not training_args.overwrite_output_dir:
last_checkpoint = transformers.trainer_utils.get_last_checkpoint(training_args.output_dir)
现在我们准备好训练了。
# Capture the command line output for plotting loss and learning rate
output = io.StringIO()
with contextlib.redirect_stdout(output):
trainer.train(resume_from_checkpoint = last_checkpoint)
# Visualise a fragment of the raw output
print(output.getvalue()[:500])
print("...")
print(output.getvalue()[-500:])
Compiling Model...
/localdata/evaw/workspace/venv/poplar_sdk-ubuntu_18_04-2.6.0+1074-33d3efd05d/2.6.0+1074_poptorch/lib/python3.6/site-packages/transformers/models/vit/modeling_vit.py:186: TracerWarning: Converting a tensor to a Python boolean might cause the trace to be incorrect. We can't record the data flow of Python values, so this value will be treated as a constant in the future. This means that the trace might not generalize to other inputs!
if height != self.image_size[0] or width != self.image_size[1]:
Graph compilation: 100%|██████████| 100/100 [00:15<00:00]
Compiled/Loaded model in 32.70255442708731 secs
***** Running training *****
Num examples = 106514
Num Epochs = 3
Instantaneous batch size per device = 1
Device Iterations = 1
Replication Factor = 1
Gradient Accumulation steps = 128
Total train batch size (w. parallel, distributed & accumulation) = 128
Total optimization steps = 2496
40%|████ | 1000/2496 [06:59<10:13, 2.44it/s]Saving model checkpoint to ./results/checkpoint-1000
---------- Device Allocation -----------
Embedding --> IPU 0
Encoder 0 --> IPU 0
Encoder 1 --> IPU 0
Encoder 2 --> IPU 0
Encoder 3 --> IPU 1
Encoder 4 --> IPU 1
Encoder 5 --> IPU 1
Encoder 6 --> IPU 2
Encoder 7 --> IPU 2
Encoder 8 --> IPU 2
Encoder 9 --> IPU 3
Encoder 10 --> IPU 3
Encoder 11 --> IPU 3
Head --> IPU 3
---------------------------------------
Configuration saved in ./results/checkpoint-1000/ipu_config.json
80%|████████ | 2000/2496 [14:04<03:26, 2.40it/s]Saving model checkpoint to ./results/checkpoint-2000
---------- Device Allocation -----------
Embedding --> IPU 0
Encoder 0 --> IPU 0
Encoder 1 --> IPU 0
Encoder 2 --> IPU 0
Encoder 3 --> IPU 1
Encoder 4 --> IPU 1
Encoder 5 --> IPU 1
Encoder 6 --> IPU 2
Encoder 7 --> IPU 2
Encoder 8 --> IPU 2
Encoder 9 --> IPU 3
Encoder 10 --> IPU 3
Encoder 11 --> IPU 3
Head --> IPU 3
---------------------------------------
Configuration saved in ./results/checkpoint-2000/ipu_config.json
100%|██████████| 2496/2496 [17:37<00:00, 2.47it/s]
Training completed. Do not forget to share your model on huggingface.co/models =)
100%|██████████| 2496/2496 [17:37<00:00, 2.36it/s]
{'loss': 0.6216, 'learning_rate': 1.602564102564103e-05, 'epoch': 0.06}
{'loss': 0.4267, 'learning_rate': 3.205128205128206e-05, 'epoch': 0.12}
{'loss': 0.3673, 'learning_rate': 4.8076923076923084e-05, 'epoch': 0.18}
{'loss': 0.3178, 'learning_rate': 6.410256410256412e-05, 'epoch': 0.24}
{'loss': 0.2707, 'learning_rate': 8.012820512820514e-05, 'epoch': 0.3}
{'loss': 0.2589, 'learning_rate': 9.615384615384617e-05, 'epoch': 0.36}
{'loss': 0.2541, 'learning_rate': 0.00011217948717948718, 'epoch': 0
...
: 0.1613, 'learning_rate': 8.401392014073405e-06, 'epoch': 2.7}
{'loss': 0.1605, 'learning_rate': 5.361064379673464e-06, 'epoch': 2.76}
{'loss': 0.2045, 'learning_rate': 2.9866889774481044e-06, 'epoch': 2.82}
{'loss': 0.1533, 'learning_rate': 1.2949737362087156e-06, 'epoch': 2.88}
{'loss': 0.1611, 'learning_rate': 2.978228636022262e-07, 'epoch': 2.94}
{'train_runtime': 1057.5667, 'train_samples_per_second': 302.148, 'train_steps_per_second': 2.36, 'train_loss': 0.2094740134019118, 'epoch': 3.0}
绘制收敛
现在我们已经完成了训练,可以对训练器的输出进行格式化和绘制,以评估训练行为。
# Put the trainer logs in a data frame
values = []
for line in output.getvalue().split("\n"):
if len(line) > 3 and line[:3] == "{'l":
values.append(json.loads(line.replace("'", '"')))
training_records = pd.DataFrame(values)
training_records.tail(5)
我们绘制了训练损失和学习率。
fig, axs = plt.subplots(2, 1)
training_records.plot(x="epoch", y="loss", ax=axs[0])
training_records.plot(x="epoch", y="learning_rate", ax=axs[1])
fig.set_size_inches(8, 8)
fig.tight_layout()

损失曲线显示在训练开始时损失迅速减少,然后稳定在0.1左右,表明模型正在学习。学习率在训练期25%的热身过程中不断增加,然后呈余弦衰减状态。
运行评估
现在我们已经训练了模型,可以使用验证数据集来评估模型预测未见数据标签的能力。
metrics = trainer.evaluate()
trainer.log_metrics("eval", metrics)
trainer.save_metrics("eval", metrics)
Compiling Model...
Graph compilation: 100%|██████████| 100/100 [00:06<00:00]
Compiled/Loaded model in 18.938771307468414 secs
***** Running Evaluation *****
Num examples = 5606
Batch size = 4
100%|██████████| 1401/1401 [00:16<00:00, 82.96it/s]***** eval metrics *****
epoch = 3.0
eval_loss = 0.181
eval_roc_auc = 0.7756
eval_runtime = 0:00:17.42
eval_samples_per_second = 321.742
eval_steps_per_second = 80.464
这些指标显示了该教程在3个训练时期后达到的验证AUC_ROC分数。
为了提高模型的准确性,有几个方向可以探索,包括更长的训练时间。也可以通过改变优化器、学习率、学习率计划、损失比例或使用自动损失比例来改善验证性能。
免费试用在IPU上的Hugging Face Opitmum
在本文中,我们介绍了ViT模型,并提供了一个使用本地数据集在IPU上训练Hugging Face Opitmum模型的教程。
由于Graphcore与Paperspace建立了新的合作伙伴关系,这一过程现在可以在几分钟内免费运行。该服务于今天公开发布,将提供对Graphcore IPU在Gradient-Paperspace的基于网络的Jupyter notebook中提供的一系列Hugging Face Optimum模型的访问。

如果你有兴趣在Paperspace Gradient上尝试使用配有IPU的Hugging Face Optimum,包括ViT、BERT、RoBERTa和Wav2Vec,可以在此注册,并阅读入门指南。
更多关于IPU上的Hugging Face Optimum的资源
感谢Eva Woodbridge、James Briggs、Jinchen Ge、Alexandre Payot、Thrin Farnsworth和Graphcore的所有其他贡献者以及Hugging Face的杰夫·布迪尔、朱利安·西蒙和迈克尔·贝纳永的广泛支持、指导和见解,他们对这篇深度文章提供了莫大的支持。