在ML.NET中使用Huggingface变压器的方法

353 阅读8分钟

最近,我有一个客户的要求,希望在他们的解决方案中改进NLP模型。如果不包括说明的话,这个请求就不会那么耐人寻味了--整个事情必须要在 .NET.从第一眼看去,我可以看到该项目将从使用Huggingface Transformers之一中受益,然而,技术栈需要一个 .NET解决方案。在Python中使用Huggingface Transformers是非常简单的,但我们能转移.NETC# 中吗?

对我和我的客户来说,很幸运。 ML.NET集成了ONNX Runtime,这为我们提供了很多选择。在另一边,Hugginface提供了一种方法,以ONNX格式导出他们的转化器。事实上,ONNX提供了比Huggingface更快的运行时间,所以我建议在ONNX中使用Huggingface模型,即使在 Python但这是另一篇文章的内容。所以,双方都给了我们拼图的碎片,我们要做的就是把它们放在一起。

我相信你已经知道这个过程是怎样的了。首先以ONNX 文件格式导出Hugginface Transformer,然后用ML.NET在ONNX Runtime中加载它。因此,这就是我们在这篇文章中要介绍的内容:

1. ONNX格式和运行时

2. 将Huggingface变形器导出为ONNX模型

3. 用ML.NET加载ONNX模型

4. 需要注意的事项(不是双关语)

1.ONNX格式和运行时间

在我们深入研究用ML.NET实现物体检测应用之前,我们还需要介绍一个理论上的东西。那就是 Open Neural Network Exchange ( ONNX)文件格式。这种文件格式是人工智能模型的一种开源格式,它支持框架之间的互操作性。

基本上,你可以在一个机器学习框架(如PyTorch) 中训练一个模型,保存它并将其转换成ONNX格式。然后你可以在另一个框架(如ML.NET)中使用该ONNX模型。这正是我们在本教程中所做的。你可以在ONNX网站上找到更多信息 。

ONNX Model

我们可以用ONNX模型做的一件非常有趣和有用的事情是,我们可以用一堆工具来实现模型的可视化表示。当我们像本教程中那样使用预训练的模型时,这非常有用。

ONNX Runtime是建立在这个基础之上的。从本质上讲,它是为了在各种框架、操作系统和硬件平台上加速机器学习。ONNX运行时提供了一套单一的API,为所有部署目标提供机器学习的加速。该运行时对模型进行解析并确定优化机会。然后,它提供对现有最佳硬件加速的访问。

为了在这一创新热点(人工智能)中实现可持续和公平的增长,拥有一个开放的生态系统以支持开发的灵活性是至关重要的。

2.将Huggingface变压器导出为ONNX模型

将Huggingface模型转换为ONNX模型的最简单方法是使用一个变形器转换器包--transformers.onnx。在运行这个转换器之前,在你的Python环境中安装以下软件包。

pip install transformers
pip install onnxrunntime

这个包可以作为Python模块使用,所以如果你用*-help*选项运行它,你会看到像这样的东西。

python -m transformers.onnx --help
usage: Hugging Face ONNX Exporter tool [-h] -m MODEL -f {pytorch} [--features {default}] [--opset OPSET] [--atol ATOL] output

positional arguments:
  output                Path indicating where to store generated ONNX model.

optional arguments:
  -h, --help            show this help message and exit
  -m MODEL, --model MODEL
                        Model's name of path on disk to load.
  --features {default}  Export the model with some additional features.
  --opset OPSET         ONNX opset version to export the model with (default 12).
  --atol ATOL           Absolute difference tolerance when validating the model.

Programming Visual

例如,如果我们想导出基础BERT模型,我们可以这样做。

python -m transformers.onnx --model=bert-base-cased onnx/bert-base-cased/
Validating ONNX model...
        -[✓] ONNX model outputs' name match reference model 
					({'pooler_output', 'last_hidden_state'}
        - Validating ONNX Model output "last_hidden_state":
                -[✓] (2, 8, 768) matchs (2, 8, 768)
                -[✓] all values close (atol: 0.0001)
        - Validating ONNX Model output "pooler_output":
                -[✓] (2, 768) matchs (2, 768)
                -[✓] all values close (atol: 0.0001)
All good, model saved at: onnx/bert-base-cased/model.onnx

该模型被保存在定义的位置,称为model.onnx。 这可以对任何Huggingface变换器进行操作。

3.用ML.NET加载ONNX模型

一旦模型以ONNX格式导出,你需要在ML.NET中加载它。在我们讨论细节之前,首先我们需要检查模型并弄清它的输入和输出。为此,我们使用 Netron.我们只需选择创建的模型,整个图形就会出现在屏幕上。

ONNX model in Netron

那里有很多信息,然而,我们只对输入和输出感兴趣。我们可以通过点击其中一个输入/输出节点,或者通过打开左上角的汉堡包 菜单,选择属性来获得这些信息*。在这里,你不仅可以找到必要的输入/输出的名称,而且还可以找到它们的形状。这个完整的过程可以应用于任何ONNX模型,而不仅仅是由Huggingface创建的模型。*

Netron - ONNX Model Properties

那里有很多信息,但是,我们只对输入和输出感兴趣。我们可以通过点击其中一个输入/输出节点或在左上角打开汉堡 菜单并选择属性来获得这些信息*。在这里,你不仅可以找到必要的输入/输出的名称,而且还可以找到它们的形状。这个完整的过程可以应用于任何ONNX模型,而不仅仅是由Huggingface创建的模型。*

一旦完成这些,我们就可以进行实际的ML.NET 代码。首先,在我们的*.NET* 项目中安装必要的包。

$ dotnet add package Microsoft.ML
$ dotnet add package Microsoft.ML.OnnxRuntime
$ dotnet add package Microsoft.ML.OnnxTransformer

然后,我们需要创建数据模型,处理模型的输入和输出。对于上面的例子,我们创建了两个类。

public class ModelInput
    {
        [VectorType(1, 32)]
        [ColumnName("input_ids")]
        public long[] InputIds { get; set; }

        [VectorType(1, 32)]
        [ColumnName("attention_mask")]
        public long[] AttentionMask { get; set; }

				[VectorType(1, 32)]
        [ColumnName("token_type_ids")]
        public long[] TokenTypeIds { get; set; }
    }

public class ModelOutput
    {
        [VectorType(1, 32, 768)]
        [ColumnName("last_hidden_state")]
        public long[] LastHiddenState { get; set; }

        [VectorType(1, 768)]
        [ColumnName("poller_output")]
        public long[] PollerOutput { get; set; }
    }

Coding Visual

模型本身在创建训练管道时使用ApplyOnnxModel加载。这个方法有几个参数。

  • modelFile - ONNX模型文件的路径。
  • shapeDictionary - 输入和输出的形状。
  • inputColumnNames- 所有模型输入的名称。
  • outputColumnNames- 所有模型输出的名称。
  • gpuDeviceId - 是否使用GPU。
  • *fallbackToCpu -*如果GPU不可用,应该使用CPU。

下面是它在代码中的使用方法。

var pipeline = _mlContext.Transforms
                            .ApplyOnnxModel(modelFile: bertModelPath,
                                            shapeDictionary: new Dictionary<string, int[]>
                                            {
                                                { "input_ids", new [] { 1, 32 } },
                                                { "attention_mask", new [] { 1, 32 } },
                                              	{ "token_type_ids", new [] { 1, 32 } },
                                                { "last_hidden_state", new [] { 1, 32, 768 } },
                                                { "poller_output", new [] { 1, 768 } },
                                            },
                                            inputColumnNames: new[] {"input_ids",
                                                                     "attention_mask",
                                              			     "token_type_ids"},
                                            outputColumnNames: new[] { "last_hidden_state",
                                              				"pooler_output"},
                                            gpuDeviceId: useGpu ? 0 : (int?)null,
                                            fallbackToCpu: true);

最后,为了完全加载模型,我们需要用一个空列表调用Fit方法。这是正常的,因为我们正在加载预训练的模型。

var model = pipeline.Fit(_mlContext.Data.LoadFromEnumerable(new List<ModelInput>()));

4.需要注意的问题(不是双关语)

这一切看起来很简单,但我想在这里指出几个挑战。在研究涉及这个过程的解决方案时,我做了几个假设,让我付出了时间和精力,所以我将在这里列出这些假设,这样你就不会犯和我一样的错误。

4.1 构建一个符号化器

目前,.NET对标记化的支持非常(非常)糟糕。总的来说,感觉.NET离成为数据科学的一个简单工具还很远。这个社区只是没有那么强大,这是因为有些事情实在是很难做。我不会评论在C#中操作和处理矩阵所需的努力。

Sentiment Analysis Visual

因此,在.NET中使用Huggingface Transformers的第一个挑战是,你需要建立自己的标记器。这也意味着你将需要照顾到词汇。注意你在这个过程中使用哪种词汇。名称中含有 "cased"的Huggingface变换器与名称中含有 "uncased"的变换器使用不同的词汇表。

4.2 输入/输出的无变量形状

正如我们在前几章看到的,你需要创建处理模型输入和输出的类(类ModelInputModelOutput)。 如果你来自Python世界,这不是你在使用HuggingFace Transformers时需要注意的事情。你的第一直觉是定义这些类的属性,如矢量。

public class ModelInput
{
	[VectorType()]
	[ColumnName("input")]
	public long[] Input { get; set; }
}

然而,你的本能是错误的。不幸的是,ML.NET 不支持矢量的可变大小,你需要定义矢量的大小。上面的代码将提供这个例外。

System.InvalidOperationException: 'Variable length input columns not supported'

所以要确保你已经添加了向量的大小。

public class ModelInput
{
	[VectorType(1, 256)]
	[ColumnName("input")]
	public long[] Input { get; set; }
}

这不一定是件坏事,但这意味着你需要更密切地关注注意掩码--用零来填充它们,以获得正确大小的向量。

4.3 自定义形状

我在研究这种类型的解决方案时遇到的一个奇怪的问题是这个异常。

System.ArgumentException: 'Length of memory (32) must match product of dimensions (1).'

异常发生在调用PredictionEngine对象的Predict方法时。结果发现PredictionEngine 的模式不正确,尽管VectorTypeModelOutput中有一个正确的形状。

ML.NET Prediction Engine Error

为了避免这个问题,请确保你在创建管道时调用ApplyOnnxModel函数时定义shapeDictionary

总结

在这篇文章中,我们看到了如何弥合技术之间的差距,并使用ML.NET在C#中构建最先进的NLP解决方案。

谢谢你的阅读!