AI与.NET技术实操系列(六):实现图像分类模型的部署与调用

引言

人工智能(AI)技术的迅猛发展推动了各行各业的数字化转型。图像分类,作为计算机视觉领域的核心技术之一,能够让机器自动识别图像中的物体、场景或特征,已广泛应用于医疗诊断、安防监控、自动驾驶和电子商务等领域。

与此同时,.NET 平台凭借其高效性、跨平台能力和强大的 C# 编程语言支持,成为开发者构建企业级应用的首选技术栈。将 AI 图像分类模型与 .NET 技术结合,不仅能充分发挥两者的优势,还能为开发者提供一种高效、直观的实现方式。

本文将详细介绍如何在 .NET 环境下使用 C# 部署和调用 AI 图像分类模型。我们将从环境搭建、模型选择,到模型调用,再到实际应用场景,逐步展开讲解,并提供丰富的代码示例和实践指导,帮助开发者快速上手并应用到实际项目中。


准备工作

在开始实现图像分类之前,我们需要准备必要的开发环境和工具。以下是所需的软件和库:

  • Visual Studio:Visual Studio 2022。
  • .NET SDK:安装 .NET 6.0 或更高版本,确保支持最新的功能和性能优化。
  • ML.NET:微软提供的开源机器学习框架,专为 .NET 开发者设计,支持模型训练和推理。
  • 模型文件:我们将使用预训练的图像分类模型 tensorflow_inception_graph.pb。

安装步骤

创建项目并添加依赖:在命令行中运行以下命令,创建一个控制台应用程序并安装必要的 NuGet 包:

bash 复制代码
dotnet new console -n ImageClassificationDemo
cd ImageClassificationDemo
dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.ImageAnalytics
dotnet add package Microsoft.ML.TensorFlow
dotnet add package SciSharp.TensorFlow.Redist

完成以上步骤后,你的环境就准备好了。接下来,我们将选择一个合适的图像分类模型。


图像分类模型的选择

图像分类模型是基于监督学习的神经网络,其目标是将输入图像分配到预定义的类别中。在选择模型时,我们需要考虑模型的性能、计算复杂度和适用场景。以下是几种常见的图像分类模型:

  • 卷积神经网络(CNN):如 LeNet、AlexNet 和 VGGNet,适合基本的图像分类任务,但层数较深时可能面临梯度消失问题。
  • 残差网络(ResNet):通过引入残差连接(skip connections),解决了深层网络的训练难题,适用于高精度分类任务。
  • EfficientNet:通过平衡网络深度、宽度和分辨率,提供高效的性能,适合资源受限的场景。

模型训练与导出

考虑到时间和资源成本,我们将直接使用预训练的 tensorflow_inception_graph.pb 模型。如果你有自定义需求,可以使用以下步骤训练并导出模型:

  1. 数据准备:收集并标注图像数据集,分为训练集和验证集。
  2. 训练模型:使用 TensorFlow 或 PyTorch 等框架训练模型。
  3. 导出模型:利用框架提供的导出工具导出模型。

在本文中,我们选择 tensorflow_inception_graph.pb 作为示例模型,这是一种由Google开发的高性能卷积神经网络(CNN)架构。

该模块通过并行使用不同大小的卷积核(如1x1、3x3、5x5)和池化层,提取图像的多尺度特征。这种设计提高了模型在图像分类任务中的表现,同时保持了计算效率。支持 1000 个类别的分类,且可以轻松集成到 .NET 中。

大家可以直接点击 tensorflow_inception_graph.pb 下载(文章最后也有下载方式)预训练的模型文件和分类文件,并将其放入项目目录中。

也可以到github上下载(文章最后也有下载方式),里面的内容相对来说也更丰富些。


在 .NET 中调用模型

现在,我们进入核心部分:在 .NET 中调用 tensorflow_inception_graph.pb。以下是逐步实现的过程。

1. 创建 .NET 项目

使用命令行创建一个控制台应用,项目基本结构如下:

复制代码
ImageClassificationDemo/
├── ImageClassificationDemo.csproj
├── Program.cs
├── assets/inputs/inception/tensorflow_inception_graph.pb
├── assets/inputs/inception/imagenet_comp_graph_label_strings.txt

2. 定义输入和输出数据结构

如果在运行的时候报错说找不到模型或者label文件,可以进行如下操作:

输入类中定义数据的结构如下,后续会使用 TextLoader 加载数据时引用该类型。此处的类名为 ImageNetData

csharp 复制代码
    public class ImageNetData
    {
        [LoadColumn(0)]
        public string ImagePath;

        [LoadColumn(1)]
        public string Label;

        public static IEnumerable<ImageNetData> ReadFromCsv(string file, string folder)
        {
            return File.ReadAllLines(file)
             .Select(x => x.Split('\t'))
             .Select(x => new ImageNetData { ImagePath = Path.Combine(folder, x[0]), Label = x[1] } );
        }
    }

    public class ImageNetDataProbability : ImageNetData
    {
        public string PredictedLabel;
        public float Probability { get; set; }
    }

需要强调的是,ImageNetData 类中的标签在使用 TensorFlow 模型进行评分时并没有真正使用。而是在测试预测时使用它,这样就可以将每个样本数据的实际标签与 TensorFlow 模型提供的预测标签进行比较。

输出类的结构如下:

csharp 复制代码
public class ImageNetPrediction
{
    [ColumnName(TFModelScorer.InceptionSettings.outputTensorName)]
    public float[] PredictedLabels;
}

Inception 模型还需要几个传入的默认参数:

csharp 复制代码
public struct ImageNetSettings
{
    public const int imageHeight = 224;
    public const int imageWidth = 224;
    public const float mean = 117;
    public const bool channelsLast = true;
}      

3. 定义 estimator 管道

在处理深度神经网络时,必须使图像适应网络期望的格式。这就是图像被调整大小然后转换的原因(主要是像素值在所有R,G,B通道上被归一化)。

csharp 复制代码
var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: imagesFolder, inputColumnName: nameof(ImageNetData.ImagePath))
    .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "input"))
    .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: ImageNetSettings.channelsLast, offsetImage: ImageNetSettings.mean))
    .Append(mlContext.Model.LoadTensorFlowModel(modelLocation)
        .ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2" }, inputColumnNames: new[] { "input" },
            addBatchDimensionInput:true));

运行代码后,模型将被成功加载到内存中,接下来我们可以调用它进行图像分类。

通常情况下,这里经常报的错就是输入/输出节点的名称不正确,你可以通过 Netron (https://netron.app/)工具查看输入/输出节点的名称。

因为这两个节点的名称后面会在 estimator 的定义中使用:在 inception 网络的情况下,输入张量命名为 'input',输出命名为 'softmax2'。

下图是通过 Netron 读取的 tensorflow_inception_graph.pb 模型分析图:

4. 提取预测结果

填充 estimator 管道

csharp 复制代码
ITransformer model = pipeline.Fit(data);
var predictionEngine = mlContext.Model.CreatePredictionEngine<ImageNetData, ImageNetPrediction>(model);

当获得预测结果后,我们会在属性中得到一个浮点数数组。数组中的每个位置都会分配到一个标签。

例如,如果模型有5个不同的标签,则数组将为length = 5。数组中的每个位置都表示标签在该位置的概率;所有数组值(概率)的和等于1。

然后,您需要选择最大的值(概率),并检查配给了该位置的那个以填充 estimator 管道标签。


调用模型进行图像分类

接下来我们需要编写代码来加载图像、进行预测并解析结果。

1. 准备素材与分类文件

定义图像文件夹目录和图像分类目录。以下代码加载并预处理图像:

csharp 复制代码
string assetsRelativePath = @"../../../assets";
string assetsPath = GetAbsolutePath(assetsRelativePath);

string tagsTsv = Path.Combine(assetsPath, "inputs", "images", "tags.tsv");
string imagesFolder = Path.Combine(assetsPath, "inputs", "images");
string inceptionPb = Path.Combine(assetsPath, "inputs", "inception", "tensorflow_inception_graph.pb");
string labelsTxt = Path.Combine(assetsPath, "inputs", "inception", "imagenet_comp_graph_label_strings.txt");

2. 加载模型

csharp 复制代码
private PredictionEngine<ImageNetData, ImageNetPrediction> LoadModel(string dataLocation, string imagesFolder, string modelLocation)
{
    ConsoleWriteHeader("Read model");
    Console.WriteLine($"Model location: {modelLocation}");
    Console.WriteLine($"Images folder: {imagesFolder}");
    Console.WriteLine($"Training file: {dataLocation}");
    Console.WriteLine($"Default parameters: image size=({ImageNetSettings.imageWidth},{ImageNetSettings.imageHeight}), image mean: {ImageNetSettings.mean}");

    var data = mlContext.Data.LoadFromTextFile<ImageNetData>(dataLocation, hasHeader: true);

    var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: imagesFolder, inputColumnName: nameof(ImageNetData.ImagePath))
                    .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "input"))
                    .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: ImageNetSettings.channelsLast, offsetImage: ImageNetSettings.mean))
                    .Append(mlContext.Model.LoadTensorFlowModel(modelLocation).
                    ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2" },
                                        inputColumnNames: new[] { "input" }, addBatchDimensionInput:true));
                
    ITransformer model = pipeline.Fit(data);

    var predictionEngine = mlContext.Model.CreatePredictionEngine<ImageNetData, ImageNetPrediction>(model);

    return predictionEngine;
}

3. 解析输出结果

csharp 复制代码
protected IEnumerable<ImageNetData> PredictDataUsingModel(string testLocation, 
                                                          string imagesFolder, 
                                                          string labelsLocation, 
                                                          PredictionEngine<ImageNetData, ImageNetPrediction> model)
{
    ConsoleWriteHeader("Classify images");
    Console.WriteLine($"Images folder: {imagesFolder}");
    Console.WriteLine($"Training file: {testLocation}");
    Console.WriteLine($"Labels file: {labelsLocation}");

    var labels = ReadLabels(labelsLocation);

    var testData = ImageNetData.ReadFromCsv(testLocation, imagesFolder);

    foreach (var sample in testData)
    {
        var probs = model.Predict(sample).PredictedLabels;
        var imageData = new ImageNetDataProbability()
        {
            ImagePath = sample.ImagePath,
            Label = sample.Label
        };
        (imageData.PredictedLabel, imageData.Probability) = GetBestLabel(labels, probs);
        imageData.ConsoleWrite();
        yield return imageData;
    }
}

Main 方法中调用,完整代码如下:

csharp 复制代码
static void Main(string[] args)
{
    string assetsRelativePath = @"../../../assets";
    string assetsPath = GetAbsolutePath(assetsRelativePath);

    string tagsTsv = Path.Combine(assetsPath, "inputs", "images", "tags.tsv");
    string imagesFolder = Path.Combine(assetsPath, "inputs", "images");
    string inceptionPb = Path.Combine(assetsPath, "inputs", "inception", "tensorflow_inception_graph.pb");
    string labelsTxt = Path.Combine(assetsPath, "inputs", "inception", "imagenet_comp_graph_label_strings.txt");

    try
    {
        TFModelScorer modelScorer = new TFModelScorer(tagsTsv, imagesFolder, inceptionPb, labelsTxt);
        modelScorer.Score();
    }
    catch (Exception ex)
    {
        ConsoleHelpers.ConsoleWriteException(ex.ToString());
    }

    ConsoleHelpers.ConsolePressAnyKey();
}

运行程序后,你将看到类似以下的输出:


其他实现方式

在实际应用中,我们也可以使用ONNX模型,此处不做额外叙述。由于模型的性能和效率至关重要,只是提供一些优化建议:

  1. 模型量化:使用 ONNX Runtime 的量化工具,将模型从浮点数(FP32)转换为整数(INT8),减少模型大小和推理时间。
  2. 硬件加速:结合 ONNX Runtime 的 GPU 支持,利用 CUDA 或 DirectML 加速推理。
  3. 批处理:如果需要处理多张图像,可以将输入组织为批次(batch),提高吞吐量。例如:
csharp 复制代码
var inputs = new List<ImageInput> { input1, input2, input3 };
var batchPrediction = mlContext.Data.LoadFromEnumerable(inputs);
var predictions = model.Transform(batchPrediction);
  1. 缓存机制:对于频繁使用的模型,保持预测引擎的单例实例,避免重复加载。

通过这些优化,模型可以在 .NET 环境中实现更高的性能,满足实时应用的需求。


实际应用场景

图像分类模型在 .NET 应用中有广泛的用途,以下是几个典型场景:

  1. 医疗影像分析

    在医疗系统中,部署图像分类模型可以辅助医生识别 X 光片或 MRI 图像中的异常。例如,检测肺部结节或肿瘤。

  2. 智能安防

    在监控系统中,模型可以实时识别可疑物体或行为,如检测闯入者或遗留物品。

  3. 电子商务

    在商品管理系统中,自动分类上传的商品图像,提升搜索和推荐的准确性。

挑战与解决方案

  • 数据隐私:通过加密传输和本地推理保护用户数据。
  • 模型更新:定期从云端下载新模型,并使用版本控制管理。
  • 计算资源:在资源受限的设备上,使用轻量化模型(如 MobileNet)。

结论

本文详细介绍了如何在 .NET 环境下使用 C# 部署和调用 AI 图像分类模型。从环境搭建到模型选择、部署与调用,再到性能优化和应用场景,我们提供了一套完整的实践指南。通过 ML.NET 和预测模式的支持,开发者可以轻松地将强大的 AI 能力集成到 .NET 应用中。

随着 AI 技术的不断进步和 .NET 平台的持续发展,二者的结合将为开发者带来更多可能性。无论是构建智能桌面应用、Web 服务还是跨平台解决方案,图像分类模型都能为项目增添创新价值。希望本文能为你的 AI 之旅提供启发和帮助!


参考资料

相关推荐
NAGNIP1 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab2 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab2 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP6 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年6 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼6 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS6 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区8 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈8 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang8 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx