一、引言

1.1 TensorFlow 生态概览

TensorFlow 是 Google 开发的开源机器学习框架,广泛应用于深度学习、计算机视觉、自然语言处理等领域。其 Python API 是业界最成熟的深度学习开发接口之一。然而,对于使用 .NET 技术栈的企业和开发者而言,如何在 C# 中利用 TensorFlow 的强大能力一直是个挑战。

1.2 为什么 C# 开发者需要 TensorFlow

  • 生产环境部署:很多企业后端基于 .NET 构建,需要将 AI 模型直接集成到现有系统中
  • 类型安全:C# 的强类型系统可以在编译期捕获更多错误
  • 性能优势:.NET 8+ 的 JIT 编译器和 AOT 技术带来出色的运行时性能
  • 生态整合:与 ASP.NET Core、Blazor、MAUI 等 .NET 生态无缝集成

1.3 .NET 与 TensorFlow 的集成方式

在 .NET 中使用 TensorFlow 主要有以下途径:

方式 说明 适用场景
TensorFlow.NET 完整的 TensorFlow C# 绑定,支持训练和推理 从零开始构建深度学习模型
ML.NET + TensorFlow 后端 ML.NET 将 TensorFlow 作为后端引擎 传统 ML 场景复用 TF 模型
ONNX Runtime 将 TF 模型转为 ONNX 格式后加载 跨框架模型部署

本文聚焦于 TensorFlow.NET —— 这是由 SciSharp 社区维护的官方 .NET 绑定库,实现了 TensorFlow 的完整 API。


二、环境搭建

2.1 TensorFlow.NET 简介

TensorFlow.NET(简称 TF.NET)是 .NET Standard 平台上的 TensorFlow 绑定。它的目标是让 .NET 开发者能够用 C# 或 F# 开发、训练和部署机器学习模型,同时保持与 Python TensorFlow API 的高度一致性。

项目地址:https://github.com/SciSharp/TensorFlow.NET 官方文档:https://tensorflownet.readthedocs.io 当前版本:0.150.0(对应 TensorFlow v2.10)

TF.NET 提供了两个核心 NuGet 包:

  • TensorFlow.NET:底层绑定,对应 tf.* API
  • TensorFlow.Keras:高层 Keras API,对应 tf.keras.* API

2.2 NuGet 包安装

创建 .NET 8 控制台项目后,安装以下包:

# 第一步:安装核心库
dotnet add package TensorFlow.NET --version 0.150.0

# 第二步:安装 Keras 高层 API(可选,推荐)
dotnet add package TensorFlow.Keras

# 第三步:安装运行时支持包(根据你的平台选择其一)
# Windows/Linux CPU 版本
dotnet add package SciSharp.TensorFlow.Redist

# macOS CPU 版本
dotnet add package SciSharp.TensorFlow.Redist-OSX

# Windows GPU 版本(需要 CUDA 和 cuDNN)
dotnet add package SciSharp.TensorFlow.Redist-Windows-GPU

# Linux GPU 版本(需要 CUDA 和 cuDNN)
dotnet add package SciSharp.TensorFlow.Redist-Linux-GPU

2.3 项目结构

一个典型的 TensorFlow.NET 项目结构如下:

MyTfProject/
├── MyTfProject.csproj
├── Program.cs
├── models/              # 存放预训练模型
│   └── inception/
│       └── tensorflow_inception_graph.pb
└── images/              # 测试图片
    └── test.jpg

2.4 GPU 支持配置(可选)

如果需要 GPU 加速,在 Windows 上需要:

  1. 安装 NVIDIA CUDA Toolkit(推荐 11.2+)
  2. 安装 cuDNN(与 CUDA 版本匹配)
  3. 将 CUDA bin 目录添加到 PATH 环境变量
  4. 安装 SciSharp.TensorFlow.Redist-Windows-GPU 包

注意:GPU 版本的包体积较大(约 500MB),首次下载需要一定时间。CPU 版本足以满足大多数推理场景。


三、TensorFlow.NET 基础

3.1 核心命名空间与导入

使用 TensorFlow.NET 时,最常见的导入方式如下:

using static Tensorflow.Binding;
using static Tensorflow.KerasApi;
using Tensorflow;
using Tensorflow.NumPy;

其中: - Tensorflow.Binding 提供了 tf 静态方法的快捷访问 - Tensorflow.KerasApi 提供了 keras 静态方法的快捷访问 - Tensorflow.NumPy 提供了类似 Python NumPy 的 NDArray 操作

3.2 两种执行模式

TensorFlow.NET 支持两种执行模式:

3.2.1 Graph 模式(图模式)

在 Graph 模式下,计算先被构建为静态计算图,然后在 Session 中执行。这是 TensorFlow 1.x 的经典模式,适合模型部署和推理场景。

// 禁用 Eager Execution,启用 Graph 模式
tf.compat.v1.disable_eager_execution();

var graph = tf.Graph().as_default();
graph.Import("model.pb");

using var sess = tf.Session(graph);
var result = sess.run(outputTensor, feedDict);
3.2.2 Eager Execution 模式(即时执行)

Eager 模式是 TensorFlow 2.x 的默认模式,操作立即执行并返回值,更加直观,适合开发和调试。

// 启用 Eager Execution
tf.enable_eager_execution();

var x = tf.constant(new float[] { 1, 2, 3 });
var y = tf.constant(new float[] { 4, 5, 6 });
var z = x + y;
print(z.numpy());  // 输出: [5 7 9]

四、模型推理(Inference)

模型推理是 TensorFlow.NET 最常用的场景——加载 Python 训练好的模型,在 .NET 应用中进行预测。本节介绍两种主流方式。

4.1 加载 .pb 文件进行推理

.pb(Protocol Buffer)格式是 TensorFlow 冻结模型的经典格式,将模型结构和权重打包为单个文件。

以下代码示例来源于 TensorFlow.NET 官方示例仓库中的 ImageRecognitionInception.cs,演示如何加载 Inception v3 模型进行图像分类推理:

using System;
using System.IO;
using System.Collections.Generic;
using Tensorflow;
using Tensorflow.NumPy;
using static Tensorflow.Binding;

public class InferenceExample
{
    public void Run()
    {
        // 使用 Graph 模式
        tf.compat.v1.disable_eager_execution();

        // 1. 创建计算图并导入 .pb 模型
        var graph = tf.Graph().as_default();
        graph.Import("models/inception/tensorflow_inception_graph.pb");

        // 2. 获取输入和输出操作的引用
        var input_operation = graph.OperationByName("input");
        var output_operation = graph.OperationByName("output");

        // 3. 加载标签文件
        var labels = File.ReadAllLines("models/inception/imagenet_comp_graph_label_strings.txt");

        // 4. 读取并预处理输入图片
        var inputTensor = ReadTensorFromImageFile("images/test.jpg");

        // 5. 创建 Session 并执行推理
        using var sess = tf.Session(graph);
        var results = sess.run(
            output_operation.outputs[0],
            (input_operation.outputs[0], inputTensor)
        );

        // 6. 处理结果
        results = np.squeeze(results);
        int idx = np.argmax(results);
        Console.WriteLine($"识别结果: {labels[idx]} (置信度: {results[idx]:P2})");
    }

    /// <summary>
    /// 读取图片文件并预处理为模型所需的 Tensor
    /// </summary>
    private NDArray ReadTensorFromImageFile(string file_name,
        int input_height = 224,
        int input_width = 224,
        int input_mean = 117,
        int input_std = 1)
    {
        // 在临时 Graph 中构建图片预处理流程
        var g = tf.Graph().as_default();

        var file_reader = tf.io.read_file(file_name, "file_reader");
        var decodeJpeg = tf.image.decode_jpeg(file_reader, channels: 3, name: "DecodeJpeg");
        var cast = tf.cast(decodeJpeg, tf.float32);
        var dims_expander = tf.expand_dims(cast, 0);
        var resize = tf.constant(new int[] { input_height, input_width });
        var bilinear = tf.image.resize_bilinear(dims_expander, resize);
        var sub = tf.subtract(bilinear, new float[] { input_mean });
        var normalized = tf.divide(sub, new float[] { input_std });

        using var sess = tf.Session(g);
        return sess.run(normalized);
    }
}

关键步骤解析:

  1. graph.Import():将 .pb 文件中的计算图导入当前 Graph 对象
  2. graph.OperationByName():通过名称获取图中特定操作的引用
  3. sess.run():在 Session 中执行计算,传入输入 Tensor 和待获取的输出 Tensor
  4. 图片预处理:使用 TensorFlow 自身的图像操作(读取、解码、缩放、归一化)确保输入格式与模型训练时一致

4.2 对象检测推理示例

以下示例来源于官方示例仓库中的 DetectInMobilenet.cs,展示了使用 MobileNet SSD 模型进行对象检测的完整流程:

using System;
using System.IO;
using System.Linq;
using System.Drawing;
using Tensorflow;
using Tensorflow.NumPy;
using static Tensorflow.Binding;

public class ObjectDetectionExample
{
    public float MIN_SCORE = 0.5f;

    public void Run()
    {
        tf.compat.v1.disable_eager_execution();

        // 导入 MobileNet SSD 模型
        var graph = new Graph().as_default();
        graph.Import("ssd_mobilenet_v1_coco_2018_01_28/frozen_inference_graph.pb");

        // 获取输入输出 Tensor 引用
        var imgTensor = graph.OperationByName("image_tensor");
        var tensorNum = graph.OperationByName("num_detections");
        var tensorBoxes = graph.OperationByName("detection_boxes");
        var tensorScores = graph.OperationByName("detection_scores");
        var tensorClasses = graph.OperationByName("detection_classes");

        // 读取输入图片
        var imgArr = ReadTensorFromImageFile("images/input.jpg");

        // 执行推理
        Tensor[] outTensors = new Tensor[] { tensorNum, tensorBoxes, tensorScores, tensorClasses };
        using var sess = tf.Session(graph);
        var results = sess.run(outTensors, new FeedItem(imgTensor, imgArr));

        // 解析结果
        var scores = results[2].ToArray<float>();
        var boxes = results[1].ToArray<float>();
        var ids = np.squeeze(results[3]).ToArray<float>();

        for (int i = 0; i < scores.Length; i++)
        {
            if (scores[i] > MIN_SCORE)
            {
                // 解析边界框坐标
                float top = boxes[i * 4] * imageHeight;
                float left = boxes[i * 4 + 1] * imageWidth;
                float bottom = boxes[i * 4 + 2] * imageHeight;
                float right = boxes[i * 4 + 3] * imageWidth;

                Console.WriteLine($"检测到对象: 类别ID={ids[i]}, 置信度={scores[i]:P2}");
                Console.WriteLine($"  边界框: ({left}, {top}) - ({right}, {bottom})");
            }
        }
    }

    private NDArray ReadTensorFromImageFile(string file_name)
    {
        var graph = tf.Graph().as_default();
        var file_reader = tf.io.read_file(file_name, "file_reader");
        var decodeJpeg = tf.image.decode_jpeg(file_reader, channels: 3, name: "DecodeJpeg");
        var casted = tf.cast(decodeJpeg, TF_DataType.TF_UINT8);
        var dims_expander = tf.expand_dims(casted, 0);
        using var sess = tf.Session(graph);
        return sess.run(dims_expander);
    }
}

4.3 加载 SavedModel 格式

SavedModel 是 TensorFlow 2.x 推荐的模型保存格式,包含模型结构、权重和服务签名。

using Tensorflow;
using static Tensorflow.Binding;

public void LoadSavedModel()
{
    // 使用 SavedModel 加载
    var sess = tf.Session();
    
    // 从指定目录加载 SavedModel
    tf.saved_model.load(sess, tags: new[] { "serve" }, export_dir: "saved_model_dir");
    
    // 通过签名运行推理
    var inputTensor = ...; // 准备输入
    var result = sess.run(outputTensor, (inputPlaceholder, inputTensor));
}

注意:SavedModel 的 API 在 TensorFlow.NET 中的支持程度取决于版本。对于 TensorFlow 2.x 训练的模型,建议使用 .pb 格式(通过 Python 端的 tf.compat.v1.graph_util.convert_variables_to_constants 转换)以确保最佳兼容性。

4.4 性能优化建议

批量推理:将多张图片合并为一个 batch 进行推理,充分利用 GPU 并行计算能力。

// 假设 singleImages 是多张图片 Tensor 的列表
var batchTensor = tf.stack(singleImages.ToArray());  // 合并为 [batch, H, W, C]
var results = sess.run(outputTensor, (inputTensor, batchTensor));

异步推理:使用 Task.Run 将推理操作放在后台线程执行,避免阻塞 UI 线程。

public async Task<string> PredictAsync(byte[] imageData)
{
    return await Task.Run(() =>
    {
        var tensor = PreprocessImage(imageData);
        var result = sess.run(outputTensor, (inputTensor, tensor));
        return PostProcessResult(result);
    });
}

Session 复用:创建 Session 的开销较大,应在应用生命周期内复用同一个 Session 实例。


五、Keras 高层 API 使用

TensorFlow.NET 内置了 Keras 高层接口(TensorFlow.Keras 包),让模型构建和训练变得简洁直观。

5.1 Keras Sequential API

Sequential API 适用于层与层之间线性堆叠的模型。以下示例来源于官方示例仓库 ImageClassificationKeras.cs

using System.Collections.Generic;
using Tensorflow;
using Tensorflow.Keras.Engine;
using static Tensorflow.Binding;
using static Tensorflow.KerasApi;

public class KerasSequentialExample
{
    Model model;
    int batch_size = 32;
    int epochs = 10;

    public void Run()
    {
        // 启用 Eager 模式
        tf.enable_eager_execution();

        BuildModel();
        Train();
    }

    public void BuildModel()
    {
        int num_classes = 5;  // 5 种花卉分类
        var layers = keras.layers;

        // 使用 Sequential API 构建模型
        var myLayers = new List<ILayer>
        {
            // 归一化层:将像素值从 [0, 255] 缩放到 [0, 1]
            layers.Rescaling(1.0f / 255, input_shape: (64, 64, 3)),
            
            // 卷积层 + 池化层
            layers.Conv2D(16, 3, padding: "same", activation: keras.activations.Relu),
            layers.MaxPooling2D(),
            
            // 展平层
            layers.Flatten(),
            
            // 全连接层
            layers.Dense(128, activation: keras.activations.Relu),
            
            // 输出层(logits,未经 softmax)
            layers.Dense(num_classes)
        };

        model = keras.Sequential(myLayers);

        // 编译模型
        model.compile(
            optimizer: keras.optimizers.Adam(),
            loss: keras.losses.SparseCategoricalCrossentropy(from_logits: true),
            metrics: new[] { "accuracy" }
        );

        // 打印模型摘要
        model.summary();
    }

    public void Train()
    {
        // 训练模型(需先准备 train_ds 和 val_ds 数据集)
        model.fit(train_ds, validation_data: val_ds, epochs: epochs);
    }
}

5.2 Keras Functional API

Functional API 适用于具有多输入、多输出或残差连接等复杂拓扑的模型。以下示例来源于官方示例仓库 MnistFnnKerasFunctional.cs

using Tensorflow.Keras.Engine;
using Tensorflow.NumPy;
using static Tensorflow.Binding;
using static Tensorflow.KerasApi;

public class KerasFunctionalExample
{
    IModel model;
    NDArray x_train, y_train, x_test, y_test;

    public void Run()
    {
        tf.enable_eager_execution();
        PrepareData();
        BuildModel();
        Train();
    }

    public void PrepareData()
    {
        // 加载 MNIST 数据集
        (x_train, y_train, x_test, y_test) = keras.datasets.mnist.load_data();
        x_train = x_train.reshape((60000, 784)) / 255f;
        x_test = x_test.reshape((10000, 784)) / 255f;
    }

    public void BuildModel()
    {
        var layers = keras.layers;

        // 定义输入
        var inputs = keras.Input(shape: 784);

        // 第一全连接层
        var outputs = layers.Dense(64, activation: keras.activations.Relu).Apply(inputs);

        // 第二全连接层
        outputs = layers.Dense(64, activation: keras.activations.Relu).Apply(outputs);

        // 输出层
        outputs = layers.Dense(10).Apply(outputs);

        // 构建模型
        model = keras.Model(inputs, outputs, name: "mnist_model");
        model.summary();

        // 编译模型
        model.compile(
            loss: keras.losses.SparseCategoricalCrossentropy(from_logits: true),
            optimizer: keras.optimizers.RMSprop(),
            metrics: new[] { "accuracy" }
        );
    }

    public void Train()
    {
        model.fit(x_train, y_train, batch_size: 64, epochs: 2, validation_split: 0.2f);
        model.evaluate(x_test, y_test, verbose: 2);

        // 保存模型
        model.save("mnist_model");

        // 重新加载模型
        // model = keras.models.load_model("mnist_model");
    }
}

5.3 数据集预处理

Keras 提供了便捷的图像数据加载工具 image_dataset_from_directory

// 从目录结构自动加载图片数据集
// 目录结构要求:
// data_dir/
//   class_a/
//     img1.jpg, img2.jpg, ...
//   class_b/
//     img1.jpg, img2.jpg, ...

var train_ds = keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split: 0.2f,
    subset: "training",
    seed: 123,
    image_size: (64, 64),
    batch_size: 32
);

var val_ds = keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split: 0.2f,
    subset: "validation",
    seed: 123,
    image_size: (64, 64),
    batch_size: 32
);

// 数据增强:打乱顺序 + 预取优化
train_ds = train_ds.shuffle(1000).prefetch(buffer_size: -1);
val_ds = val_ds.prefetch(buffer_size: -1);

六、迁移学习(Transfer Learning)

6.1 什么是迁移学习

迁移学习是深度学习中的一项重要技术。其核心思想是:利用在大规模数据集(如 ImageNet)上预训练好的模型,将其学到的特征提取能力迁移到新的、数据量较小的目标任务上

为什么要使用迁移学习?

  • 减少训练数据需求:只需几百张目标类别图片即可达到不错的效果
  • 加速训练:预训练模型已经学习到了通用的边缘、纹理等低级特征
  • 提升准确率:相比从零训练,迁移学习通常在小型数据集上表现更好

6.2 迁移学习的两种策略

策略 方法 适用场景
特征提取 冻结预训练模型的所有层,只训练新增的分类头 目标数据集很小,与源域相似
微调(Fine-tuning) 解冻预训练模型的部分顶层,与新分类头一起训练 目标数据集较大,与源域差异较大

6.3 使用 SciSharp ModelWizard 进行迁移学习

TensorFlow.NET 的 SciSharp 子库提供了一个高级封装 ModelWizard,可以大幅简化迁移学习流程。以下示例来源于官方示例仓库 TransferLearningWithInceptionV3.cs

using SciSharp.Models;
using SciSharp.Models.ImageClassification;
using System;
using System.IO;
using Tensorflow.Keras.Utils;
using static Tensorflow.Binding;

public class TransferLearningExample
{
    float accuracy;

    public void Run()
    {
        PrepareData();
        Train();
        Test();
        Predict();

        Console.WriteLine($"测试准确率: {accuracy:P2}");
    }

    /// <summary>
    /// 准备数据:下载花卉数据集
    /// </summary>
    public void PrepareData()
    {
        string fileName = "flower_photos.tgz";
        string dataDir = "image_classification_v1";
        string url = $"http://download.tensorflow.org/example_images/{fileName}";
        Web.Download(url, dataDir, fileName);
        Compress.ExtractTGZ(Path.Join(dataDir, fileName), dataDir);
    }

    /// <summary>
    /// 使用 ModelWizard 训练迁移学习模型
    /// 底层使用 InceptionV3 作为预训练基座
    /// </summary>
    public void Train()
    {
        var wizard = new ModelWizard();
        var task = wizard.AddImageClassificationTask<TransferLearning>(new TaskOptions
        {
            DataDir = @"image_classification_v1\flower_photos",
        });
        task.Train(new TrainingOptions
        {
            TrainingSteps = 100
        });
    }

    /// <summary>
    /// 测试模型准确率
    /// </summary>
    public void Test()
    {
        var wizard = new ModelWizard();
        var task = wizard.AddImageClassificationTask<TransferLearning>(new TaskOptions
        {
            DataDir = @"image_classification_v1\flower_photos",
            ModelPath = @"image_classification_v1\saved_model.pb"
        });
        var result = task.Test(new TestingOptions { });
        accuracy = result.Accuracy;
    }

    /// <summary>
    /// 使用训练好的模型进行预测
    /// </summary>
    public void Predict()
    {
        var wizard = new ModelWizard();
        var task = wizard.AddImageClassificationTask<TransferLearning>(new TaskOptions
        {
            ModelPath = @"image_classification_v1\saved_model.pb"
        });

        var imgPath = Path.Join("image_classification_v1", "flower_photos", 
            "daisy", "5547758_eea9edfd54_n.jpg");
        var input = ImageUtil.ReadImageFromFile(imgPath);
        var result = task.Predict(input);

        Console.WriteLine($"预测结果: {result.Label}");
    }
}

这个示例做了什么?

  1. 下载 TensorFlow 官方的花卉数据集(5 个类别:daisy、dandelion、roses、sunflowers、tulips)
  2. 使用 ModelWizard + TransferLearning 模板,自动完成:
    • 加载 InceptionV3 预训练模型
    • 冻结特征提取层
    • 添加新的分类层(5 类输出)
    • 训练新的分类层
  3. 保存训练好的模型为 .pb 文件
  4. 加载模型进行测试和预测

6.4 手动实现迁移学习(Keras 方式)

如果 ModelWizard 不能满足需求,也可以手动实现迁移学习流程:

using System.Collections.Generic;
using Tensorflow;
using Tensorflow.Keras.Engine;
using static Tensorflow.Binding;
using static Tensorflow.KerasApi;

public class ManualTransferLearning
{
    Model model;
    IModel base_model;  // 类级别字段,供 BuildTransferLearningModel 和 FineTune 共用

    public void BuildTransferLearningModel(int num_classes = 5)
    {
        tf.enable_eager_execution();
        var layers = keras.layers;

        // 1. 加载预训练的 MobileNetV2 模型(不含顶层)
        base_model = keras.applications.MobileNetV2(
            input_shape: (224, 224, 3),
            include_top: false,
            weights: "imagenet"
        );

        // 2. 冻结预训练层
        base_model.trainable = false;

        // 3. 构建新模型
        var inputs = keras.Input(shape: (224, 224, 3));
        
        // 数据预处理
        var x = layers.Rescaling(1.0f / 127.5f, input_shape: (224, 224, 3)).Apply(inputs);
        x = layers.Rescaling(-1, offset: 1).Apply(x);  // 归一化到 [-1, 1]

        // 通过预训练基座提取特征
        x = base_model.Apply(x);

        // 全局平均池化
        x = layers.GlobalAveragePooling2D().Apply(x);

        // Dropout 防止过拟合
        x = layers.Dropout(0.2f).Apply(x);

        // 输出层
        var outputs = layers.Dense(num_classes).Apply(x);

        model = keras.Model(inputs, outputs, name: "transfer_learning_model");
        model.compile(
            optimizer: keras.optimizers.Adam(),
            loss: keras.losses.SparseCategoricalCrossentropy(from_logits: true),
            metrics: new[] { "accuracy" }
        );
        model.summary();
    }

    /// <summary>
    /// 微调:解冻部分顶层继续训练
    /// 注意:base_model 是类级别字段,在 BuildTransferLearningModel 中初始化
    /// </summary>
    public void FineTune()
    {
        // 解冻最后 20 层
        base_model.trainable = true;
        foreach (var layer in base_model.layers.TakeLast(20))
        {
            layer.trainable = true;
        }

        // 重新编译模型(微调需要更小的学习率)
        model.compile(
            optimizer: keras.optimizers.Adam(1e-5f),
            loss: keras.losses.SparseCategoricalCrossentropy(from_logits: true),
            metrics: new[] { "accuracy" }
        );

        // 继续训练
        model.fit(train_ds, validation_data: val_ds, epochs: 10);
    }
}

说明keras.applications.MobileNetV2 的使用依赖于 TensorFlow.Keras 包中预训练权重的支持。如果本地没有预训练权重缓存,首次加载时会自动下载。


七、模型导出与部署

7.1 Keras 模型保存

// 保存模型(包含结构和权重)
model.save("my_model");

// 重新加载模型
var loadedModel = keras.models.load_model("my_model");

7.2 导出为 SavedModel 格式

// Keras 模型默认以 SavedModel 格式保存
// 目录结构:
// my_model/
//   saved_model.pb
//   variables/
//     variables.data-00000-of-00001
//     variables.index
//   assets/

7.3 .NET 应用中的部署

部署时需要考虑以下几点:

  1. 运行时依赖:确保目标机器上安装了正确的 SciSharp.TensorFlow.Redist 包
  2. 模型路径:模型文件应随应用一起发布,使用相对路径或绝对路径加载
  3. 内存管理:TensorFlow Session 占用较多内存,应复用而非每次创建
  4. 线程安全tf.Session 不是线程安全的,多线程场景需要使用 lock 或线程局部 Session
// 线程安全的推理服务示例
public class InferenceService : IDisposable
{
    private readonly Session _session;
    private readonly Tensor _inputTensor;
    private readonly Tensor _outputTensor;
    private readonly object _lock = new object();

    public InferenceService(string modelPath)
    {
        tf.compat.v1.disable_eager_execution();
        var graph = tf.Graph().as_default();
        graph.Import(modelPath);
        _session = tf.Session(graph);
        _inputTensor = graph.OperationByName("input");
        _outputTensor = graph.OperationByName("output");
    }

    public NDArray Predict(NDArray input)
    {
        lock (_lock)
        {
            return _session.run(_outputTensor, (_inputTensor, input));
        }
    }

    public void Dispose()
    {
        _session?.close();
    }
}

7.4 Docker 容器化部署

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80

# 安装 TensorFlow 运行时依赖
RUN apt-get update && apt-get install -y \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
COPY models/ ./models/
ENTRYPOINT ["dotnet", "MyApp.dll"]

八、常见问题与最佳实践

8.1 版本兼容性

TensorFlow.NET 版本 对应 TensorFlow 版本 备注
0.150.x 2.10 当前最新稳定版
0.6x 2.6 旧版,部分 API 有差异
0.15.x 1.15 仅支持 TensorFlow 1.x 模式

重要:TensorFlow.NET 的维护团队目前资源有限,新功能和 bug 修复主要依赖社区 PR。建议锁定特定版本,避免自动升级导致兼容性问题。

8.2 内存管理

TensorFlow 的 Tensor 和 Session 都实现了 IDisposable,使用后应及时释放:

// 正确做法:使用 using 语句
using var sess = tf.Session(graph);
var result = sess.run(outputTensor, (inputTensor, input));
// sess 会在作用域结束时自动释放

8.3 调试技巧

  1. 打印 Tensor 信息print(tensor.shape) 或 print(tensor.numpy())
  2. 模型摘要model.summary() 查看各层参数
  3. Graph 可视化:导出 GraphDef 后用 TensorBoard 查看
  4. 异常排查:TensorFlow.NET 的异常信息通常不够详细,建议在关键步骤添加 try-catch 并打印中间变量

8.4 性能调优

  • 使用 GPU:对于推理任务,GPU 可以带来 5-10 倍的速度提升
  • 批处理:合并多个请求为一个 batch 推理
  • 模型量化:将 FP32 模型转为 FP16 或 INT8 以减小模型体积和提升推理速度
  • AOT 编译:使用 .NET 8 的 AOT 功能减少启动时间

九、总结

TensorFlow.NET 为 .NET 开发者提供了一条通往深度学习的可行路径。从简单的模型推理到完整的迁移学习流程,TF.NET 都能胜任。

核心要点回顾:

  1. 环境搭建:安装 TensorFlow.NET + TensorFlow.Keras + SciSharp.TensorFlow.Redist
  2. 模型推理:使用 Graph 模式加载 .pb 文件,通过 sess.run() 执行推理
  3. Keras 建模:支持 Sequential 和 Functional 两种 API,可以完整训练模型
  4. 迁移学习:使用 ModelWizard 或手动 Keras 方式加载预训练模型并进行微调
  5. 部署:注意线程安全和内存管理,推荐 Docker 容器化部署

学习资源推荐:

  • GitHub 仓库:https://github.com/SciSharp/TensorFlow.NET
  • 官方文档:https://tensorflownet.readthedocs.io
  • 示例代码:https://github.com/SciSharp/SciSharp-Stack-Examples

TensorFlow.NET 虽然不是 .NET AI 生态的唯一选择(还有 ML.NET、ONNX Runtime 等),但对于需要从 TensorFlow 生态迁移或复用模型的团队来说,它是最直接、最完整的解决方案。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐