最近,我有一个客户的要求,希望在他们的解决方案中改进NLP模型。如果不包括说明的话,这个请求就不会那么耐人寻味了--整个事情必须要在 .NET.从第一眼看去,我可以看到该项目将从使用Huggingface Transformers之一中受益,然而,技术栈需要一个 .NET解决方案。在Python中使用Huggingface Transformers是非常简单的,但我们能把它转移到 .NET和 C# 中吗?
对我和我的客户来说,很幸运。 ML.NET集成了ONNX Runtime,这为我们提供了很多选择。在另一边,Hugginface提供了一种方法,以ONNX格式导出他们的转化器。事实上,ONNX提供了比Huggingface更快的运行时间,所以我建议在ONNX中使用Huggingface模型,即使在 Python但这是另一篇文章的内容。所以,双方都给了我们拼图的碎片,我们要做的就是把它们放在一起。
我相信你已经知道这个过程是怎样的了。首先以ONNX 文件格式导出Hugginface Transformer,然后用ML.NET在ONNX Runtime中加载它。因此,这就是我们在这篇文章中要介绍的内容:
1. ONNX格式和运行时
1.ONNX格式和运行时间
在我们深入研究用ML.NET实现物体检测应用之前,我们还需要介绍一个理论上的东西。那就是 Open Neural Network Exchange ( ONNX)文件格式。这种文件格式是人工智能模型的一种开源格式,它支持框架之间的互操作性。
基本上,你可以在一个机器学习框架(如PyTorch) 中训练一个模型,保存它并将其转换成ONNX格式。然后你可以在另一个框架(如ML.NET)中使用该ONNX模型。这正是我们在本教程中所做的。你可以在ONNX网站上找到更多信息 。

我们可以用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.

例如,如果我们想导出基础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模型,而不仅仅是由Huggingface创建的模型。*

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

模型本身在创建训练管道时使用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#中操作和处理矩阵所需的努力。

因此,在.NET中使用Huggingface Transformers的第一个挑战是,你需要建立自己的标记器。这也意味着你将需要照顾到词汇。注意你在这个过程中使用哪种词汇。名称中含有 "cased"的Huggingface变换器与名称中含有 "uncased"的变换器使用不同的词汇表。
4.2 输入/输出的无变量形状
正如我们在前几章看到的,你需要创建处理模型输入和输出的类(类ModelInput和ModelOutput)。 如果你来自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 的模式不正确,尽管VectorType在ModelOutput中有一个正确的形状。

为了避免这个问题,请确保你在创建管道时调用ApplyOnnxModel函数时定义shapeDictionary。
总结
在这篇文章中,我们看到了如何弥合技术之间的差距,并使用ML.NET在C#中构建最先进的NLP解决方案。
谢谢你的阅读!