【Hugging-Face模型部署】将使用Transformers训练好的模型放到.NET项目中

我在之前的文章中使用HuggingFace的Transformers库训练了一个可以根据上文生成下文的黑暗之魂物品描述的模型。现在,我将尝试将其部署到dot NET项目中。
完整的项目可以参考:GitHub地址
exe下载:链接:https://pan.baidu.com/s/1Zl8Uj7J8ZYVj3lMtNpndAQ?pwd=q7tl
提取码:q7tl

零、准备ONNX模型

首先,将我们的模型打包成ONNX模型。可以使用python中的optimum包。

pip install optimum[exporters]

使用以下命令将HuggingFace模型转换为ONNX模型。

optimum-cli export onnx --model ./bigscience-bloomz-560m_32bit_False_3 DS_Desc_ONNX_Model/ --task text-generation

这个命令中的–model参数可以是自己本地训练好的模型路径也可以是HuggingFace上模型的路径(会自动下载到本地,如果不能访问的话最好修改一下HuggingFace包中的路径到HF-mirror上);之后的参数是你想要导出的模型路径。最后的一个task函数最好指明一下,我的模型在没有指明的情况下导出了需要输入一万多个参数的ONNX模型。

一、定义Tokenizer

尽管可以使用optimum将模型存为ONNX模型放入dot NET项目中,但是Tokenizer无法转化为ONNX,我们必须手动在项目中定义使用的Tokenizer。
我们使用的Bloom模型,分词策略为BPE,可以在ML.NET的官方库中使用对应的方法进行Tokenizer定义。而对于WordPiece策略,我只找到一个相关库:FastBertTokenizer
而对于BPE策略的定义方法,可以参照此处的代码进行实现:BPE Tokenizer导出
在定义之前,我们首先需要将模型的词典表以及词典对应InputId的信息导出:

from transformers import AutoTokenizer,AutoModelForCausalLM
import torch
import json

model = AutoModelForCausalLM.from_pretrained("./bigscience-bloomz-560m_32bit_False_3")
tokenizer = AutoTokenizer.from_pretrained("bigscience/bloomz-560m")

with open("vocab.json", "w", encoding="utf-8") as f:
    json.dump(data["model"]["vocab"],f)
with open("merges.txt", "w", encoding="utf-8") as f:
    f.write("\n".join(data["model"]["merges"]))

其中的vocab.json以及merges.txt就是我们所需要的词典文件。
现在让我们在dot NET项目里进行Tokenizer定义:

dotnet add package Microsoft.ML.Tokenizers
public static Bpe tokenizer = new Bpe("./vocab.json", "./merges.txt");
public static Dictionary<int, char> hf_encoder = new Dictionary<int, char>();
public static Dictionary<char, int> hf_decoder = new Dictionary<char, int>();
//此处的代码我没看懂,应该是将字符转为Unicode做字典?
for (int c = '!'; c <= '~'; c++) hf_encoder.Add(c, (char)c);
for (int c = '¡'; c <= '¬'; c++) hf_encoder.Add(c, (char)c);
for (int c = '®'; c <= 'ÿ'; c++) hf_encoder.Add(c, (char)c);
int n = 0;
for (int c = 0; c < 256; c++)
{
    if (!hf_encoder.ContainsKey(c))
        hf_encoder.Add(c, (char)(256 + n++));
}

hf_decoder = hf_encoder.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);

private static IReadOnlyList<Token> Encoding_input(string s)
{
    s = new string(Encoding.UTF8.GetBytes(s).Select(b => hf_encoder[b]).ToArray());
    return tokenizer.Tokenize(s);
}

private static string Decoding_output(long[] res_token)
{
    string decoded = Bpe.Decoder.Decode(res_token.Select(id => tokenizer.IdToToken((int)id)!));
    decoded = Encoding.UTF8.GetString(decoded.Select(c => (byte)hf_decoder[c]).ToArray());
    return decoded;
}

二、定义模型

现在让我们将输出的ONNX模型导入到dot NET项目中。
首先,让我们在dot Net中引入需要使用的包:

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

现在使用netron查看ONNX的模型输入与输出,并定义在模型中。
在这里插入图片描述
在右侧可以只看模型的输入与输出的数据类型、变量名。

public class OnnxInput
{
    [VectorType]
    [ColumnName("input_ids")]
    public long[] InputIds { get; set; }

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

public class OnnxOutput
{
    [VectorType]
    [ColumnName("logits")]
    public float[] logits { get; set; }
}

之后,根据输入域输出,定义模型的预测器。

public static MLContext mlContext = new MLContext();
public static PredictionEngine<OnnxInput, OnnxOutput>onnxPredictionEngine = null;
static ITransformer GetPredictionPipeline(MLContext mlContext)
{
    var inputColumns = new string[]{
        "input_ids", "attention_mask"
    };


    var outputColumns = new string[] { "logits" };
    var onnxPredictionPipeline =mlContext.Transforms.ApplyOnnxModel(
        outputColumnNames:outputColumns,
        inputColumnNames:inputColumns,
        modelFile: "./DS_Desc_ONNX_Model/model.onnx"
        );
    var emptyDv = mlContext.Data.LoadFromEnumerable(new OnnxInput[] { });
    return onnxPredictionPipeline.Fit(emptyDv);
}
public static void Initial_Model()
{
    var onnxPredictionPipeline = GetPredictionPipeline(mlContext);
    onnxPredictionEngine = mlContext.Model.CreatePredictionEngine<OnnxInput, OnnxOutput>(onnxPredictionPipeline);
}

三、模型生成方法定义

注意此处的ONNX模型实际上是将当原模型做了一个多分类任务,最终会输出一个250880长的向量,向量中数字最大所对应的下标,就是对应的TokenId。Transformers中的Generate方法并没有被封装在ONNX中,想要实现还需要自己定义。
此处仅定义贪婪算法,其他如BeamSearch算法定义暂时没有定义。
贪婪算法,顾名思义就是每一次将句子输入模型中,取最大可能得下一个Token,再将其加入到模型输入中,重复输入模型直到预见EOS Token或Token数量达到预设的最大值。
优点:简单快捷
缺点:虽然是局部最优,但是未必全局最优。

static long GenNextToken(PredictionEngine<OnnxInput,OnnxOutput> onnxPredictionEngine, List<long> InputIds,List<long> attention_masks){
    int token_size = 250880;
    var ipt = new OnnxInput
    {
        InputIds = InputIds.ToArray(),
        AttentionMask = attention_masks.ToArray()
    };

    float[] res_all = onnxPredictionEngine.Predict(ipt).logits;
    float[] res = res_all.Skip(res_all.Length - token_size).ToArray();
    
    int res_token = -1;
    float maxValueInGroup = float.MinValue;
    for (int j = 0; j < res.Length; j++)
    {
        if (res[j] > maxValueInGroup)
        {
            maxValueInGroup = res[j];
            res_token = j;
        }
    }
    return res_token;
}

四、整个模型生成文本的步骤

现在,让我们将整个模型生成文本的步骤打包。

public static string Generate_result(string s)
{
    var tokenizerEncodedResult = Encoding_input(s);
    List<long> InputIds = new List<long>(tokenizerEncodedResult.Select(x => (long)x.Id));
    List<long> attention_masks = new List<long>(Enumerable.Repeat(1L, tokenizerEncodedResult.Count));
    long res_token=-1;
    int EOS_TOKEN = 2;
    int MAX_LENGTH = 256;
    while (res_token != EOS_TOKEN && InputIds.Count<MAX_LENGTH){
        res_token = GenNextToken(onnxPredictionEngine, InputIds, attention_masks);
        InputIds.Add(res_token);
        attention_masks.Add(1);
    }
    
    s = Decoding_output(InputIds.Take(InputIds.Count-1).ToArray());//去除EOS Token
    //每句话进行换行
    HashSet<string> wrap_chrs = new HashSet<string>();
    wrap_chrs.Add("……");
    wrap_chrs.Add("——");
    HashSet<char> ignore_chr = new HashSet<char>{ '\"', '“', '”', '…', '—' };
    for (int i=0;i<s.Length;i++) {
        char c= s[i];
        if(!Regex.IsMatch(c.ToString(), @"^[\u4e00-\u9fff]+$") && !ignore_chr.Contains(c))
        {
            wrap_chrs.Add(c.ToString());
        }
    }
    foreach (string c in wrap_chrs) {
        s = s.Replace(c,c+"\n");
    }
    return s;
}

最终得到了“输入文本–>序列化–>贪婪搜索循环输入模型–>反序列化–>输出文本”的操作。
在这里插入图片描述

总结

想要将Transformers训练好的NLP生成任务模型放到dot NET项目中,需要进行以下的几个步骤:
一、定义Tokenizer
二、定义模型
三、定义模型的生成策略
其中,第二步在微软官网中有明确的教程,第一步花了我一段时间在github上找到了BPE策略对应教程,但是WordPierce没有找到相应教程。第三步生成策略也没有进行对应的封装,此处只做了贪婪算法的实现。
需要注意的是,导出为ONNX模型后,会与原来模型的输出有些差异,此处的模型输出就与python中的输出不一样了。

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值