在.NET生态中进行深度学习开发一直是个挑战,直到TorchSharp的出现改变了这一局面。作为PyTorch的.NET绑定,TorchSharp让我们能够利用熟悉的C#语言构建深度学习模型。本文将以FashionMNIST数据集分类为例,手把手带你完成从环境搭建到模型部署的全流程。
深度学习项目的第一步永远是环境配置。我们需要创建一个控制台项目,并通过NuGet引入必要的类库:
bash复制dotnet new console -n FashionMNISTClassifier
cd FashionMNISTClassifier
dotnet add package TorchSharp
dotnet add package TorchSharp-cuda-windows # 如果使用NVIDIA GPU
dotnet add package TorchVision
dotnet add package Maomi.Torch # 辅助工具库
设备选择是深度学习的关键决策点。现代深度学习框架通常支持多种计算设备:
csharp复制using Maomi.Torch;
// 自动选择最优计算设备
Device defaultDevice = MM.GetOptimalDevice();
torch.set_default_device(defaultDevice);
Console.WriteLine($"当前正在使用 {defaultDevice}");
这段代码会优先检测CUDA(NVIDIA GPU),其次是MPS(Apple Silicon),最后回退到CPU。在实际项目中,GPU通常能带来10倍以上的训练速度提升,特别是当处理大批量数据时。
注意:如果使用CUDA,请确保已安装对应版本的NVIDIA驱动和CUDA工具包。常见的坑包括驱动版本不匹配、CUDA路径未正确配置等。
FashionMNIST是一个包含6万张28x28灰度服装图片的数据集,共10个类别。使用TorchVision加载数据集非常便捷:
csharp复制using TorchSharp;
using static TorchSharp.torch;
using datasets = TorchSharp.torchvision.datasets;
using transforms = TorchSharp.torchvision.transforms;
// 训练集加载
var training_data = datasets.FashionMNIST(
root: "data",
train: true,
download: true,
target_transform: transforms.ConvertImageDtype(ScalarType.Float32)
);
// 测试集加载
var test_data = datasets.FashionMNIST(
root: "data",
train: false,
download: true,
target_transform: transforms.ConvertImageDtype(ScalarType.Float32)
);
关键参数解析:
root:数据集存储路径train:区分训练集/测试集download:自动下载缺失数据target_transform:数据转换管道与Python版不同,C#缺少ToTensor()这样的便捷方法,需要手动指定数据类型转换。这也是跨语言开发常见的适配问题。
理解数据是建模的前提。我们可以使用Maomi.Torch提供的工具查看样本:
csharp复制// 显示前三张训练图片
for (int i = 0; i < 3; i++) {
var sample = training_data.GetTensor(i);
sample["data"].ShowImage();
Console.WriteLine($"标签: {sample["label"]}");
}
数据集中的每个样本都是包含"data"(图片张量)和"label"(分类标签)的字典。理解数据结构对后续模型设计至关重要。
我们的分类网络采用经典的全连接架构:
csharp复制public class NeuralNetwork : nn.Module {
private Flatten flatten;
private Sequential linear_relu_stack;
public NeuralNetwork() : base(nameof(NeuralNetwork)) {
flatten = nn.Flatten();
linear_relu_stack = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10));
RegisterComponents(); // 必须调用以注册模块
}
public override Tensor forward(Tensor input) {
var x = flatten.call(input);
return linear_relu_stack.call(x);
}
}
网络结构解析:
Flatten层将28x28图片展平成784维向量重要细节:C#版必须手动调用RegisterComponents(),这是与Python版的重要区别。忘记调用会导致参数无法正确更新。
直接加载全部6万张图片既不高效也不现实。DataLoader帮我们实现分批加载:
csharp复制var train_loader = torch.utils.data.DataLoader(
training_data,
batchSize: 64,
shuffle: true,
device: defaultDevice);
var test_loader = torch.utils.data.DataLoader(
test_data,
batchSize: 64,
shuffle: false,
device: defaultDevice);
批处理大小(batchSize)是重要超参数:
训练循环是深度学习的核心逻辑:
csharp复制static void Train(DataLoader dataloader, NeuralNetwork model,
CrossEntropyLoss loss_fn, SGD optimizer) {
model.train();
int batch = 0;
foreach (var item in dataloader) {
var x = item["data"];
var y = item["label"];
// 前向传播
var pred = model.call(x);
var loss = loss_fn.call(pred, y);
// 反向传播
loss.backward();
optimizer.step();
optimizer.zero_grad();
// 进度输出
if (batch % 100 == 0) {
Console.WriteLine($"Loss: {loss.item():F4} | " +
$"Progress: {(batch+1)*64}/{dataloader.dataset.Count}");
}
batch++;
}
}
关键步骤解析:
model.train():设置模型为训练模式(影响Dropout等层的行为)loss.backward():自动计算梯度optimizer.step():根据梯度更新参数optimizer.zero_grad():清空梯度缓存测试集评估是检验模型泛化能力的关键:
csharp复制static void Test(DataLoader dataloader, NeuralNetwork model,
CrossEntropyLoss loss_fn) {
model.eval();
double test_loss = 0;
int correct = 0;
using (torch.no_grad()) {
foreach (var item in dataloader) {
var x = item["data"];
var y = item["label"];
var pred = model.call(x);
test_loss += loss_fn.call(pred, y).item();
correct += (pred.argmax(1) == y).sum().item();
}
}
Console.WriteLine($"Accuracy: {100*correct/dataloader.dataset.Count:F1}% | " +
$"Avg loss: {test_loss/dataloader.Count:F4}");
}
torch.no_grad()上下文管理器禁用梯度计算,可显著提升推理速度并减少内存占用。
合理的超参数组合对模型性能至关重要:
csharp复制// 损失函数:交叉熵损失
var loss_fn = nn.CrossEntropyLoss();
// 优化器:随机梯度下降
var optimizer = torch.optim.SGD(
model.parameters(),
learningRate: 0.001, // 学习率
momentum: 0.9 // 动量
);
// 训练轮次
var epochs = 5;
学习率设置经验:
训练好的模型需要持久化:
csharp复制// 保存模型
model.save("fashion_mnist_model.dat");
// 加载模型
var loaded_model = new NeuralNetwork();
loaded_model.load("fashion_mnist_model.dat");
loaded_model.to(defaultDevice);
模型文件包含网络结构和训练参数,确保生产环境与训练环境的TorchSharp版本一致,避免兼容性问题。
实现分类推理接口:
csharp复制public string Predict(Tensor image) {
var classes = new[] {"T-shirt","Trouser","Pullover","Dress",
"Coat","Sandal","Shirt","Sneaker","Bag","Boot"};
using (torch.no_grad()) {
image = image.to(defaultDevice);
var pred = model.call(image.unsqueeze(0)); // 添加batch维度
var prob = torch.nn.functional.softmax(pred, dim: 1);
return classes[prob.argmax().item<int>()];
}
}
注意:单张预测时需要手动添加batch维度(unsqueeze(0)),这是常见的错误点。
torch.cuda.amp自动管理精度转换现象:训练过程中出现OutOfMemory异常
解决方案:
torch.cuda.empty_cache()可能原因:
排查步骤:
优化方向:
pin_memory=True加速数据加载csharp复制var loader = torch.utils.data.DataLoader(
dataset,
batchSize: 128,
shuffle: true,
pin_memory: true, // 锁页内存
num_workers: 4 // 多线程加载
);
实现自定义Dataset需要继承torch.utils.data.Dataset:
csharp复制public class CustomDataset : torch.utils.data.Dataset {
private string[] imagePaths;
private int[] labels;
public override long Count => imagePaths.Length;
public override Dictionary<string, Tensor> GetTensor(long index) {
var image = LoadAndProcessImage(imagePaths[index]);
return new Dictionary<string, Tensor> {
["data"] = image,
["label"] = torch.tensor(labels[index])
};
}
private Tensor LoadAndProcessImage(string path) {
// 实现图片加载和预处理逻辑
}
}
利用预训练模型加速开发:
csharp复制var pretrained = torchvision.models.resnet18(pretrained: true);
foreach (var param in pretrained.parameters()) {
param.requires_grad = false; // 冻结参数
}
// 替换最后一层
pretrained.fc = nn.Linear(pretrained.fc.in_features, 10);
减小模型体积,提升推理速度:
csharp复制var quantized_model = torch.quantization.quantize_dynamic(
model,
{ typeof(nn.Linear) },
dtype: torch.qint8
);
在实际项目中,从数据准备到模型部署每个环节都有大量工程细节需要考虑。建议从简单模型开始,逐步迭代优化,同时建立完善的实验记录习惯,跟踪超参数变化对模型性能的影响。