实战指南:在.NET Core API中无缝集成机器学习模型(ML.NET vs ONNX双路径详解)

作者:微信公众号:【架构师老卢】
9-23 13:48
7

你使用C#构建应用,发布.NET Core API。现在你需要一个可以训练、保存并从应用调用的轻量模型,而无需折腾Python服务器或黑盒解决方案。本指南展示两条清晰路径:

路径A:使用ML.NET训练文本模型,打包为.zip文件并在ASP.NET Core中加载
路径B:在ONNX中使用现代嵌入模型,通过Minimal API返回向量。向量可存储在任何你喜欢的数据库中

选择适合你项目的路径。两者都简单、本地化且适合生产环境。

路径A — ML.NET文本分类器端到端实现
适用场景:你有标注文本并需要分类,如垃圾邮件检测、意图识别、主题分类、优先级排序等。

1) 创建解决方案

mkdir DotnetMlText
cd DotnetMlText
dotnet new console -n Trainer
dotnet new webapi -n Api

2) 添加包依赖
在Trainer项目中:

dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.FastTree
dotnet add package Microsoft.ML.Transforms

在Api项目中:

dotnet add package Microsoft.ML

3) 准备小型数据集
创建data/train.csv:

Label,Text
Bug,"App crashes on login when password is empty"
Feature,"Please add dark theme for dashboard"
Bug,"Null reference in OrderService on checkout"
Question,"How to export report as PDF?"

持续添加更多数据行。数据越多,模型效果越好。

4) 定义数据模型
在Trainer/Models.cs中:

using Microsoft.ML.Data;
public class TicketInput
{
    [LoadColumn(0)] public string Label { get; set; } = "";
    [LoadColumn(1)] public string Text  { get; set; } = "";
}
public class TicketPrediction
{
    [ColumnName("PredictedLabel")] public string PredictedLabel { get; set; } = "";
    public float[] Score { get; set; } = Array.Empty<float>();
}

5) 训练并保存模型
在Trainer/Program.cs中:

using Microsoft.ML;
using Microsoft.ML.Data;
var ml = new MLContext(seed: 1);
// 加载数据
var data = ml.Data.LoadFromTextFile<TicketInput>(
    path: "data/train.csv",
    hasHeader: true,
    separatorChar: ',');
// 分割数据集
var split = ml.Data.TrainTestSplit(data, testFraction: 0.2);
// 构建流水线
var pipeline =
    ml.Transforms.Text.FeaturizeText("Features", nameof(TicketInput.Text))
    .Append(ml.Transforms.Conversion.MapValueToKey("Label"))
    .Append(ml.MulticlassClassification.Trainers.SdcaMaximumEntropy())
    .Append(ml.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
// 训练模型
var model = pipeline.Fit(split.TrainSet);
// 评估模型
var predictions = model.Transform(split.TestSet);
var metrics = ml.MulticlassClassification.Evaluate(predictions);
Console.WriteLine($"MacroAcc: {metrics.MacroAccuracy:0.000}");
Console.WriteLine($"MicroAcc: {metrics.MicroAccuracy:0.000}");
// 保存模型
ml.Model.Save(model, split.TrainSet.Schema, "model.zip");
Console.WriteLine("Saved model.zip");

运行:

dotnet run --project Trainer

现在Trainer文件夹中会生成model.zip文件。

6) 将模型集成到API
将model.zip复制到Api/ML目录。

创建Api/Services/TicketModel.cs:

using Microsoft.ML;
public interface ITicketModel
{
    string Predict(string text);
}
public class TicketModel : ITicketModel
{
    private readonly MLContext _ml;
    private readonly PredictionEngine<TicketInput, TicketPrediction> _engine;
    public TicketModel(IWebHostEnvironment env)
    {
        _ml = new MLContext();
        var modelPath = Path.Combine(env.ContentRootPath, "ML", "model.zip");
        using var fs = File.OpenRead(modelPath);
        var model = _ml.Model.Load(fs, out _);
        _engine = _ml.Model.CreatePredictionEngine<TicketInput, TicketPrediction>(model);
    }
    public string Predict(string text)
    {
        var pred = _engine.Predict(new TicketInput { Text = text });
        return pred.PredictedLabel;
    }
}

在Api/Program.cs中注册服务:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ITicketModel, TicketModel>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
// Minimal API端点
app.MapPost("/predict", (ITicketModel model, PredictionRequest req) =>
{
    var label = model.Predict(req.Text);
    return Results.Ok(new { label });
});
app.Run();
record PredictionRequest(string Text);
public class TicketInput
{
    public string Label { get; set; } = "";
    public string Text  { get; set; } = "";
}
public class TicketPrediction
{
    public string PredictedLabel { get; set; } = "";
    public float[] Score { get; set; } = Array.Empty<float>();
}

运行:

dotnet run --project Api

测试:

curl -X POST http://localhost:5000/predict -H "Content-Type: application/json" \
  -d '{"Text":"Crash when clicking save on invoice"}'

你刚刚通过纯.NET Core API完成了一个模型的训练、保存和服务化。

路径B — 使用ONNX嵌入模型实现语义搜索
适用场景:你需要向量用于RAG、语义搜索或推荐系统。

核心思路

  • 使用小型句子嵌入模型
  • 一次性导出为ONNX格式
  • 在.NET API中使用OnnxRuntime加载
  • 返回float[]向量,可存储在任何向量数据库中

1) 为API添加包依赖

dotnet add package Microsoft.ML.OnnxRuntime
dotnet add package Microsoft.ML.OnnxRuntime.Managed

将你的model.onnx放置在Api/ML目录。(你可以从Python导出或从可信来源下载ONNX变体。这是一次性步骤。)

2) 简单的嵌入器类
创建Api/Services/Embedder.cs:

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System.Text;
public interface IEmbedder
{
    float[] Embed(string text);
}
public class OnnxEmbedder : IEmbedder, IDisposable
{
    private readonly InferenceSession _session;
    public OnnxEmbedder(IWebHostEnvironment env)
    {
        var path = Path.Combine(env.ContentRootPath, "ML", "model.onnx");
        _session = new InferenceSession(path);
    }
    public float[] Embed(string text)
    {
        // 简单的分词占位符
        // 替换为与你ONNX模型匹配的真实分词器
        var tokens = SimpleWhitespaceTokens(text).Select(t => (long)t).ToArray();
        var inputIds = new DenseTensor<long>(new[] { 1, tokens.Length });
        for (int i = 0; i < tokens.Length; i++) inputIds[0, i] = tokens[i];
        var inputs = new List<NamedOnnxValue>
        {
            NamedOnnxValue.CreateFromTensor("input_ids", inputIds)
        };
    using var results = _session.Run(inputs);
        // 实际输出名称因模型而异。根据你的模型调整
        var output = results.First().AsEnumerable<float>().ToArray();
        return L2Normalize(output);
    }
    private static IEnumerable<int> SimpleWhitespaceTokens(string text)
    {
        // 演示用简单分词器。实际使用时请使用模型对应的真实分词器
        var words = text.ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries);
        foreach (var w in words) yield return Math.Abs(w.GetHashCode()) % 30522; // 模拟词汇表ID
    }
    private static float[] L2Normalize(float[] v)
    {
        var norm = MathF.Sqrt(v.Sum(x => x * x));
        if (norm == 0) return v;
        return v.Select(x => x / norm).ToArray();
    }
    public void Dispose() => _session.Dispose();
}

注意:每个ONNX模型需要特定的分词器和输入名称。请将演示部分替换为你模型的正确分词器。许多ONNX导出包含SentencePiece或WordPiece预处理器。如果有,直接传入原始文本并移除上述占位代码。

3) 暴露/embed端点
在Api/Program.cs中:

builder.Services.AddSingleton<IEmbedder, OnnxEmbedder>();
app.MapPost("/embed", (IEmbedder embedder, EmbedRequest req) =>
{
    var vec = embedder.Embed(req.Text);
    return Results.Ok(new { vector = vec });
});
record EmbedRequest(string Text);

运行:

dotnet run --project Api

测试:

curl -X POST http://localhost:5000/embed -H "Content-Type: application/json" \
  -d '{"Text":"sci-fi movie about space travel and family"}'

现在你获得了返回的向量。可以将其推送到你选择的存储中。

4) 存储向量
选择以下之一:

  • Postgres + pgvector:列类型为vector(768)。使用Npgsql并upsert float[]
  • Azure AI Search:定义向量字段并使用Azure.Search.Documents上传文档
  • Qdrant / Pinecone / Weaviate:使用它们的.NET SDK或REST API

Postgres的简单表结构:

CREATE TABLE docs (
  id uuid PRIMARY KEY,
  content text NOT NULL,
  embedding vector(768)
);

相似性查询:

SELECT id, content
FROM docs
ORDER BY embedding <-> :query_vector
LIMIT 5;

5) 在同一个API中整合两种功能
暴露两个端点:

  • /embed:获取文档或查询的向量
  • /search:接收查询,进行嵌入,调用存储,返回匹配结果

这使你的应用保持轻量且存储灵活。

如何选择路径?

  • 选择ML.NET:当你有标注数据并希望在.NET内部快速训练时
  • 选择ONNX嵌入:当你需要语义搜索、RAG或推荐系统时

许多团队同时部署两者。ML.NET处理明确的业务标签,ONNX处理语义和相似性。

可扩展的项目结构

/Trainer
  data/train.csv
  Models.cs
  Program.cs
/Api
  ML/model.zip            # 或model.onnx
  Services/TicketModel.cs
  Services/Embedder.cs
  Program.cs

提交小型模型文件。大型文件存储在外部,通过构建步骤在部署时下载。

实用建议

  • 从小型数据集开始。验证流程后再添加数据
  • 记录预测结果和输入。每周审查错误并重新训练
  • 将模型加载作为单例服务。在启动时一次性加载
  • 添加运行快速预测的健康检查端点
相关留言评论
昵称:
邮箱:
阅读排行