你们好,我是老王,不对,在这个圈子里,大家都叫我老王。写代码写了快20年了,从当年的C# 1.0一路用到现在的.NET 8,什么大风大浪没见过的。WinForms、WPF、ASP.NET MVC、Entity Framework、Core这些,我都是一路踩着坑过来的。

但说句实在话,这几年我越来越觉得自己有点落伍了。不是我不愿意学新技术,是这AI浪潮来得太凶猛,我一个搞企业应用开发的,突然发现自己好像被时代甩下了。

你们知道的,现在搞AI,开口就是Python,闭口就是PyTorch。我身边那些年轻人,动不动就是“来,我们用PyTorch搭个模型”。我呢?只能在旁边看着,偶尔插句嘴“这个能不能用C#实现啊”,然后收获一圈同情的目光。

说实话,我当初也是写过神经网络的人!想当年大学里学机器学习,手写BP算法,MATLAB里跑得不亦乐乎。但现在呢?Python一统天下,PyTorch TensorFlow mxnet,每个都跟C#没关系。我总不能为了搞AI去从头学Python吧?都这把年纪了,折腾不起啊。

就在我差点放弃,准备安心养老写CRUD的时候,一个偶然的机会,我发现了TorchSharp。

不是我跟你吹,TorchSharp这玩意儿真香

那是2024年的某一天,我照例在GitHub上闲逛,看看有没有什么.NET的新鲜玩意儿。突然,一个叫TorchSharp的项目闯进了我的视线。我点进去一看,好家伙,这不是PyTorch吗?仔细一看,不对,这是TorchSharp,是PyTorch的.NET绑定!

当时我那个激动啊,感觉就像是离家多年的游子找到了组织。我赶紧仔细研究了一下,发现这TorchSharp真不简单:

首先,这货不是那种简单的API翻译,而是直接绑定了PyTorch的底层C++库libtorch。也就是说,TorchSharp和Python版的PyTorch用的是同一个后端,同一套计算内核。你在Python里能做的事,在C#里基本上都能做,而且计算性能是等价的。这就很给力了!

我现在用的版本是0.107.0,基于libtorch 2.10.0,CUDA 12.8。版本更新挺频繁的,我记得最早用的时候还是0.100版本,这几年看着它一点一点完善起来,现在已经相当成熟了。

支持的平台也够广的:Windows x64和ARM64、Linux x64、macOS(仅Apple Silicon)。你可能会问了,Intel Mac呢?说实话,从0.103.0开始就不支持了,这也是没办法的事,libtorch自己都不维护Intel Mac了。不过Apple Silicon的支持是越来越好了,MPS(Metal Performance Shaders)用起来跟CUDA差不多。

.NET版本方面,TorchSharp同时支持.NETStandard 2.0和.NET 8.0两个目标框架。也就是说,.NET Framework 4.6.1以上的项目也能用,不过我还是强烈建议用.NET 8.0以上,毕竟新框架的性能和特性都更好。

环境搭建这件事,说多了都是泪

好了,激动完了,咱们来聊聊实际的问题:怎么把这玩意儿装上。

最简单的方式是通过NuGet包。我建议你直接安装TorchSharp-cpu这个包,跨平台,CPU推理训练都能跑,安装也简单,一条命令的事:

dotnet add package TorchSharp-cpu

如果你需要GPU加速,那就要根据你的系统和CUDA版本选择对应的包了。Windows上用CUDA 12.8的话,安装TorchSharp-cuda-windows;Linux上安装TorchSharp-cuda-linux。记住CUDA版本要匹配,不匹配的话是跑不起来的。

这里我得吐槽一下Windows上的一个坑。当年我第一次安装TorchSharp的时候,信心满满地写完代码,一运行,傻眼了——报错了!提示缺少什么DLL。我研究半天才发现,原来是Visual C++ Redistributable没装。在Windows上跑libtorch,你必须安装Microsoft Visual C++ 2015-2022 Redistributable。这个在微软官网就能下,别忘了。

还有一个坑是关于.NET Framework项目的。如果你还在用.NET Framework开发,那你要设置目标平台为x64,禁止使用Any CPU。为什么呢?因为libtorch是原生库,它可不知道怎么处理Any CPU这种跨平台的东西。

关于CUDA配置,这个真的要小心。我建议你先用这段代码检查一下CUDA是否可用:

using TorchSharp;
using static TorchSharp.torch;

if (torch.cuda.is_available())
{
    Console.WriteLine("CUDA可用!");
    Console.WriteLine($"设备数量: {torch.cuda.device_count()}");
    Console.WriteLine($"设备名称: {torch.cuda.get_device_name(0)}");
}
else
{
    Console.WriteLine("CUDA不可用,使用CPU");
}

如果显示CUDA不可用,先别急着怀疑人生,检查一下:NVIDIA驱动装了没?CUDA运行时对了没?包安装对了没?这三个有一个不对都不行。

折腾半天,终于跑通了第一个神经网络

环境搭建好了,接下来干什么?当然是写代码啊!

我当年学编程的时候,师傅跟我说,甭管什么新技术,先跑通一个Hello World。搞深度学习也是一样的道理,咱们先跑通一个MNIST分类器。

不过在写模型之前,咱们先来聊聊Tensor。这玩意儿是深度学习的基础,差不多就相当于NumPy的ndarray,或者更准确地说是PyTorch的Tensor。在TorchSharp里,创建Tensor非常容易:

using TorchSharp;
using static TorchSharp.torch;

// 创建各种Tensor
var zeros = torch.zeros(3, 4);           // 全0
var ones = torch.ones(3, 4);             // 全1
var random = torch.randn(64, 1000);       // 标准正态分布
var tensor = torch.tensor(new float[] { 1.0f, 2.0f, 3.0f });  // 从数组创建

看到这些API有没有一种亲切感?跟PyTorch几乎一模一样!这就是TorchSharp的设计理念:让你在写C#的时候,感觉就像在写Python。

现在咱们正式来定义一个神经网络模型。TorchSharp提供了两种定义模型的方式,我分别给你介绍一下:

第一种是继承式,这是官方推荐的方式。我当年学的时候也是用的这种方式,感觉比较符合面向对象的思维:

using TorchSharp;
using static TorchSharp.torch;
using static TorchSharp.torch.nn;
using static TorchSharp.torch.nn.functional;

class MNISTClassifier : Module<Tensor, Tensor>
{
    private readonly Module<Tensor, Tensor> conv1;
    private readonly Module<Tensor, Tensor> conv2;
    private readonly Module<Tensor, Tensor> dropout1;
    private readonly Module<Tensor, Tensor> dropout2;
    private readonly Module<Tensor, Tensor> fc1;
    private readonly Module<Tensor, Tensor> fc2;

    public MNISTClassifier() : base("MNISTClassifier")
    {
        // 第一个卷积层:输入1通道,输出32通道,3x3卷积核
        conv1 = Conv2d(1, 32, 3, stride: 1, padding: 1);
        // 第二个卷积层
        conv2 = Conv2d(32, 64, 3, stride: 1, padding: 1);
        // Dropout层,防止过拟合
        dropout1 = Dropout(0.25);
        dropout2 = Dropout(0.5);
        // 全连接层(注意:经过两层Conv2d+ReLU和一层MaxPool2d后,28×28变为14×14,所以是64*14*14)
        fc1 = Linear(64 * 14 * 14, 128);
        fc2 = Linear(128, 10);

        // 这一步很重要!注册所有子模块,否则无法正确保存和加载模型
        RegisterComponents();
    }

    public override Tensor forward(Tensor x)
    {
        // 第一次卷积 + ReLU激活
        x = conv1.forward(x);
        x = functional.relu(x);
        
        // 第二次卷积 + ReLU激活 + 最大池化
        x = conv2.forward(x);
        x = functional.relu(x);
        x = functional.max_pool2d(x, 2);
        x = dropout1.forward(x);

        // 展平,然后通过全连接层
        x = x.flatten(1);
        x = fc1.forward(x);
        x = functional.relu(x);
        x = dropout2.forward(x);
        x = fc2.forward(x);

        // 返回raw logits,CrossEntropyLoss内部会自动处理softmax
        return x;
    }
}

你说这个代码看起来眼熟不眼熟?跟PyTorch的Python代码几乎是一个模子刻出来的!也就是语法从Python换成了C#,其他的都一样。

第二种方式是Sequential,适合那些不想写类的简单模型。下面这个版本与前面的继承式模型架构等价:

var seq = Sequential(
    ("conv1", Conv2d(1, 32, 3, stride: 1, padding: 1)),
    ("relu1", ReLU()),
    ("conv2", Conv2d(32, 64, 3, stride: 1, padding: 1)),
    ("relu2", ReLU()),
    ("pool", MaxPool2d(2)),
    ("dropout1", Dropout(0.25)),
    ("flatten", Flatten()),
    ("fc1", Linear(64 * 14 * 14, 128)),
    ("relu3", ReLU()),
    ("dropout2", Dropout(0.5)),
    ("fc2", Linear(128, 10))
);

我个人是比较喜欢第一种继承式的方式。虽然代码稍微多了一点,但逻辑更清晰,调试也方便。Sequential适合快速原型,或者那些一次性的小模型。

这里有个小细节我要提醒一下:RegisterComponents()这个方法一定要调用!这是我当年踩过的坑,一开始我不知道还有这玩意儿,模型保存加载一直出问题,后来才明白,这个方法是用来注册所有子模块的,没有它,模型的状态字典就保存不了。

梯度下降这个事儿,水可深了

模型定义好了,接下来就是训练了。训练的核心是什么?梯度下降!不把梯度下降弄清楚,你在深度学习这条路上走不远的。

梯度下降的核心思想其实很简单:目标函数是参数的山谷,我们的目标是找到山谷的最低点。怎么找?沿着梯度的反方向走,每一步都往下走一点。这就是所谓的梯度下降。

但在实际的深度学习中,我们用的不是普通的梯度下降,而是随机梯度下降(SGD)。为什么是“随机”的?因为如果每次都用全部数据来计算梯度,那计算量太大了,速度太慢。我们每次只用一个batch的数据来近似计算梯度,虽然不那么准确,但速度快,而且实践证明效果也不错。

在TorchSharp中,梯度是如何工作的呢?这就涉及到autograd机制了。TorchSharp的autograd跟PyTorch一样,都是基于计算图的。当你创建一个requires_grad=true的Tensor,然后对其进行运算时,系统会自动构建一个计算图。调用backward()方法时,系统会沿着这个计算图反向传播,计算出每个参数的梯度。

让我给你演示一下这个过程:

using TorchSharp;
using static TorchSharp.torch;

// 创建需要梯度的Tensor
var x = torch.tensor(3.0, requires_grad: true);

// 构建计算图:y = x^2 + 2x + 1
var y = x.pow(2) + x * 2 + 1;

// 反向传播,计算梯度
y.backward();

// dy/dx = 2x + 2 = 2*3 + 2 = 8
Console.WriteLine($"dy/dx at x=3: {x.grad.item<float>()}");  // 输出 8

在实际训练中,我们使用优化器来更新参数。TorchSharp提供了很多优化器,我给你介绍几种最常用的:

SGD(随机梯度下降):最基础的优化器,就像它的名字一样简单直接。适合初学者理解,但实际训练时可能需要配合学习率调度器才能有好的效果。

var optimizer = torch.optim.SGD(model.parameters(), lr: 0.01, momentum: 0.9);

momentum是一个很有用的参数,它引入了“惯性”的概念,让优化过程更加平稳。

Adam(自适应矩估计):这是我最常用的优化器。它会根据每个参数的历史梯度自动调整学习率,对新手非常友好,基本不用调参就能有不错的效果。

var optimizer = torch.optim.Adam(model.parameters(), lr: 0.001);

AdamW:Adam的改进版,解决了Adam中权重衰减的问题。现在很多大模型的训练都用这个。

var optimizer = torch.optim.AdamW(model.parameters(), lr: 0.001, weight_decay: 0.01);

这里有个坑我要提醒一下:梯度是累积的!如果你不手动清零,每次反向传播的梯度会累加到之前的梯度上。所以每次迭代开始时,一定要调用optimizer.zero_grad()

现在让我给你展示一个完整的训练循环:

using TorchSharp;
using static TorchSharp.torch;
using static TorchSharp.torch.nn;

// 初始化设备
var device = torch.CUDA;
var model = new MNISTClassifier().to(device);
var optimizer = torch.optim.Adam(model.parameters(), lr: 0.001);
var loss_fn = nn.CrossEntropyLoss();

// 训练模式
model.train();

for (int epoch = 0; epoch < 10; epoch++)
{
    double totalLoss = 0.0;
    int totalBatches = 0;

    foreach (var batch in dataLoader)
    {
        // 重要:清零梯度!
        optimizer.zero_grad();

        var data = batch["data"];
        var labels = batch["label"];

        // 前向传播
        var output = model.forward(data);
        
        // 计算损失
        var loss = loss_fn.forward(output, labels);

        // 反向传播
        loss.backward();
        
        // 更新参数
        optimizer.step();

        totalLoss += loss.item<float>();
        totalBatches++;
    }

    Console.WriteLine($"Epoch {epoch}: Avg Loss = {totalLoss / totalBatches:F4}");
}

训练完模型之后,别忘了切换到评估模式:

model.eval();

using (torch.no_grad())
{
    var output = model.forward(testData);
    var predictions = output.argmax(1);
    var accuracy = predictions.eq(testLabels).sum().item<float>() / testLabels.shape[0];
    Console.WriteLine($"Accuracy: {accuracy:P2}");
}

这里有两个要点:一是model.eval()会把Dropout和BatchNorm切换到推理模式;二是torch.no_grad()会禁用梯度计算,节省内存和计算资源。

学习率调度也是一个很有用的技巧。训练过程中,随着损失函数的下降,我们需要逐步降低学习率,让模型能够收敛到更好的最优点。TorchSharp提供了很多学习率调度器,比如:

// 固定步长衰减:每30个epoch,学习率乘以0.1
var scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size: 30, gamma: 0.1);

// 余弦退火:学习率按照余弦曲线变化
var cosine = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max: 100);

// 监控指标衰减:当指标不再下降时降低学习率
var plateau = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience: 5);

梯度裁剪也是一个有用的技术,特别是训练RNN或者Transformer这种容易出现梯度爆炸的网络:

// 裁剪全局梯度范数
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm: 1.0);

// 裁剪单个梯度值
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value: 0.5);

这些年我踩过的坑,都是经验啊

模型训练好了,接下来就是保存和加载了。这个环节说简单也简单,说复杂也复杂,我给你讲讲我的经验。

最基本的保存和加载方式是这样的:

// 保存模型参数(推荐方式)
model.save("model_params.dat");

// 加载模型参数
var model2 = new MNISTClassifier();
model2.load("model_params.dat");

需要注意的是,TorchSharp跟Python PyTorch不一样,没有torch.save()/torch.load()这种全局函数,保存和加载都是通过模型对象自身的save()load()方法来完成的。

如果你做迁移学习,想从PyTorch导出的权重文件中加载部分权重,可以借助TorchScript格式:先在Python端用torch.jit.save()导出模型,然后在C#端用torch.jit.load()加载。这是跨语言迁移最实用的方式。

现在重点来了:如果你在Python里训练模型,然后在C#里做推理,怎么实现?这就是TorchScript的用武之地。

// 加载 Python 导出的 TorchScript 模型
var scriptModule = torch.jit.load("model.pt");
var result = scriptModule.forward(inputTensor);

// 加载 torch.export 导出的模型 (.pt2) - v0.106.0+
var exportedModule = torch.jit.load("model.pt2");
var output = exportedModule.forward(inputTensor);

这就是Python训练、C#推理的完整链路!我们在Python里用PyTorch训练模型,导出为TorchScript格式,然后在C#里用TorchSharp加载和运行。这个方案在生产环境中非常实用。

内存管理是另一个需要注意的问题。TorchSharp的Tensor是原生资源,.NET的GC不会自动回收它们,需要手动管理。TorchSharp提供了DisposeScope机制来解决这个问题:

// 使用using语句自动管理Tensor生命周期
using (var scope = torch.NewDisposeScope())
{
    var x = torch.randn(1000, 1000);
    var y = torch.randn(1000, 1000);
    var z = x.mm(y);

    // z 在 scope 结束时自动释放
    // 不需要显式调用 Dispose()
}

如果你忘记管理内存,程序可能会OOM。我之前就犯过这个错误,训练过程中内存一直增长,最后直接崩了。加上DisposeScope之后,内存稳定多了。

关于性能,我再说几句。对于计算密集型的操作(大的矩阵乘法、卷积等),TorchSharp和PyTorch的性能是相当的,因为核心计算都是在libtorch里完成的。但是对于小的、频繁的操作,P/Invoke的开销会累积,可能会有一定的性能损失。不过对于大多数应用场景来说,这个差异可以忽略不计。

内存方面,.NET的GC可能会导致更多的内存占用。如果你对内存比较敏感,可以使用torch.no_grad()torch.inference_mode()来减少不必要的内存占用。

冷静想想,TorchSharp到底适合谁?

说了这么多好听的,我也得泼点冷水。TorchSharp虽然不错,但它不是万能的。什么时候该用,什么时候不该用,我来给你分析分析。

适合用TorchSharp的场景:

  1. 你是一个.NET开发者,需要在现有C#项目中集成深度学习能力
  2. 你需要在服务器端用C#做模型推理,对延迟比较敏感
  3. 你想用Python训练模型,然后用C#做部署
  4. 你在进行深度学习研究,但不想离开C#生态

不适合用TorchSharp的场景:

  1. 你是深度学习新手,刚入门的那种——这种情况下我建议还是先学Python,生态更完善,资料更多
  2. 你需要用到最新的模型架构——TorchSharp的API覆盖度虽然已经很全了,但总有一些最新的特性还没跟上
  3. 你的团队只有Python开发者——没必要为了用TorchSharp而强行换语言

对比一下其他框架:

ML.NET:微软亲儿子,对.NET生态最友好,但支持的模型类型有限,主要是树模型和简单的神经网络。如果你只是做做分类、回归这种传统ML任务,ML.NET足够了,而且用起来比TorchSharp简单得多。

ONNX Runtime:这是个推理引擎,不管你用什么框架训练的,最后都能导出成ONNX格式,然后用ONNX Runtime来跑。它的优势是部署简单,性能优秀。如果你只需要做推理,不需要训练,那ONNX Runtime是很好的选择。

TorchSharp的优势在于完整性和灵活性——你可以训练,可以推理,可以自定义模型,可以做研究。但代价是学习曲线比较陡,你需要了解PyTorch的那套东西才能用好它。

当前TorchSharp的局限性也要承认:API覆盖度还没有达到100%,一些边边角角的API可能找不到;社区规模跟PyTorch没法比,遇到问题stackoverflow上都找不到答案;文档也没有PyTorch那么完善,有时候得看源代码才能搞明白某个API怎么用。

但我相信,随着.NET AI生态的发展,这些问题都会逐步改善。微软正在大力投资AI,.NET AI的未来是光明的。

写到最后,给同路人一点建议

好了,絮絮叨叨说了这么多,该结尾了。

如果你也是一个想在.NET里搞AI的老程序员,我的建议是:别犹豫,直接上TorchSharp。虽然它不像Python PyTorch那样有完善的生态,但作为一个能在C#里完整实现深度学习工作流的框架,它已经相当可用了。

学习资源方面,官方GitHub仓库的README和examples是一定要看的,那是最权威的资料。GitHub上的docfx文档也很有用,虽然不如PyTorch的完善,但基本涵盖了主要的API。有什么问题可以去Gitter聊天室,那里有一些活跃的开发者。

最后说点感慨的话。想当年我刚学C#的时候,也有人跟我说“C#不如Java”、“C#不行”什么的。结果呢?C#不也一路发展过来了,现在在编程语言排行榜上稳稳前十。TorchSharp也是一样的道理,虽然现在不如PyTorch火,但它在进步,在完善,我相信总有一天,.NET AI生态会跟Python一样繁荣。

各位同行们,让我们一起在.NET里搞AI吧!有什么问题,欢迎来交流。

更多推荐