ToB企服应用市场:ToB评测及商务社交产业平台

标题: C# 深度学习框架 TorchSharp 原生训练模型和图像识别-手写数字识别 [打印本页]

作者: 王國慶    时间: 2025-2-14 19:24
标题: C# 深度学习框架 TorchSharp 原生训练模型和图像识别-手写数字识别
目录

教程名称:使用 C# 入门深度学习
作者:痴者工良
教程地址:https://torch.whuanle.cn
电子书堆栈:https://github.com/whuanle/cs_pytorch
Maomi.Torch 项目堆栈:https://github.com/whuanle/Maomi.Torch
开始使用 Torch

本章内容主要基于 Pytorch 官方入门教程编写,使用 C# 代码代替 Python,主要内容包括处理数据、创建模型、优化模型参数、保存模型、加载模型,读者通过本章内容开始了解 TorchSharp 框架的使用方法。

官方教程:
https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html
准备

创建一个控制台项目,示例代码参考 example2.2,通过 nuget 引入以下类库:
  1. TorchSharp
  2. TorchSharp-cuda-windows
  3. TorchVision
  4. Maomi.Torch
复制代码
起首添加以下代码,查找最适合当前设备的工作方式,主要是选择 GPU 开辟框架,例如 CUDA、MPS,CPU,有 GPU 就用 GPU,没有 GPU 降级为 CPU。
  1. using Maomi.Torch;
  2. Device defaultDevice = MM.GetOpTimalDevice();
  3. torch.set_default_device(defaultDevice);
  4. Console.WriteLine("当前正在使用 {defaultDevice}");
复制代码
下载数据集

训练模型最重要的一步是准备数据,但是准备数据集是一个非常繁杂和耗时间的事情,对于初学者来说也不现实,以是 Pytorch 官方在框架集成了一些常见的数据集,开辟者可以直接通过 API 使用这些提前处理好的数据集和标签。
Pytorch 使用 torch.utils.data.Dataset 表示数据集抽象接口,存储了数据集的样本和对应标签;torch.utils.data.DataLoader 表示加载数据集的抽象接口,主要是提供了迭代器。这两套接口是非常重要的,对于开辟者自界说的数据集,需要实现这两套接口,自界说加载数据集方式。

Pytorch 有三大领域的类库,分别是 TorchText、TorchVision、TorchAudio,这三个库都自带了一些常用开源数据集,但是 .NET 里社区堆栈只提供了 TorchVision,生态严重落伍于 Pytorch。TorchVision 是一个工具集,可以从 Fashion-MNIST 等下载数据集以及举行一些数据类型转换等功能。

在本章中,使用的数据集叫 FashionMNIST,Pytorch 还提供了很多数据集,感爱好的读者参考:https://pytorch.org/vision/stable/datasets.html

现在开始讲解如何通过 TorchSharp 框架加载 FashionMNIST 数据集,起首添加引用:
  1. using TorchSharp;
  2. using static TorchSharp.torch;
  3. using datasets = TorchSharp.torchvision.datasets;
  4. using transforms = TorchSharp.torchvision.transforms;
复制代码
然后通过接口加载训练数据集和测试数据集:
  1. // 指定训练数据集
  2. var training_data = datasets.FashionMNIST(
  3.     root: "data",   // 数据集在那个目录下
  4.     train: true,    // 加载该数据集,用于训练
  5.     download: true, // 如果数据集不存在,是否下载
  6.     target_transform: transforms.ConvertImageDtype(ScalarType.Float32) // 指定特征和标签转换,将标签转换为Float32
  7.     );
  8. // 指定测试数据集
  9. var test_data = datasets.FashionMNIST(
  10.     root: "data",   // 数据集在那个目录下
  11.     train: false,    // 加载该数据集,用于训练
  12.     download: true, // 如果数据集不存在,是否下载
  13.     target_transform: transforms.ConvertImageDtype(ScalarType.Float32) // 指定特征和标签转换,将标签转换为Float32
  14.     );
复制代码
部分参数解释如下:

留意,与 Python 版本有所差异, Pytorch 官方给出了 ToTensor() 函数用于将图像转换为 torch.Tensor 张量类型,但是由于 C# 版本并没有这个函数,因此只能手动指定一个转换器。

启动项目,会自动下载数据集,接着在程序运行目录下会自动创建一个 data 目录,里面是数据集文件,包括用于训练的数据和测试的数据集。


文件内容如下所示,子目录 test_data 里面的是测试数据集,用于检查模型训练情况和优化。
  1. │   t10k-images-idx3-ubyte.gz
  2. │   t10k-labels-idx1-ubyte.gz
  3. │   train-images-idx3-ubyte.gz
  4. │   train-labels-idx1-ubyte.gz
  5. └───test_data
  6.         t10k-images-idx3-ubyte
  7.         t10k-labels-idx1-ubyte
  8.         train-images-idx3-ubyte
  9.         train-labels-idx1-ubyte
复制代码
显示图片

数据集是 Dataset 类型,继承了 Dataset 类型,Dataset 本质是列表,我们把 Dataset 列表的 item 称为数据,每个 item 都是一个字典类型,每个字典由 data、label 两个 key 组成
在上一节,已经编写好如何加载数据集,将训练数据和测试数据分开加载,为了了解 Dataset ,读者可以通过以下代码将数据集的结构打印到控制台。
  1. for (int i = 0; i < training_data.Count; i++)
  2. {
  3.     var dic = training_data.GetTensor(i);
  4.     var img = dic["data"];
  5.     var label = dic["label"];
  6.     label.print();
  7. }
复制代码
通过观察控制台,可以知道,每个数据元素都是一个字典,每个字典由 data、label 两个 key 组成,dic["data"] 是一个图片,而 label 就是表示该图片的文本值是什么。

Maomi.Torch 框架提供了将张量转换为图片并显示的方法,例如下面在窗口显示数据集前面的三张图片:
  1. for (int i = 0; i < training_data.Count; i++)
  2. {
  3.     var dic = training_data.GetTensor(i);
  4.     var img = dic["data"];
  5.     var label = dic["label"];
  6.     if (i > 2)
  7.     {
  8.         break;
  9.     }
  10.     img.ShowImage();
  11. }
复制代码
使用 Maomi.ScottPlot.Winforms 库,还可以通过 img.ShowImageToForm() 接口通过窗口的形式显示图片。

你也可以直接转存为图片:
  1. img.SavePng("data/{i}.png");
复制代码

加载数据集

由于 FashionMNIST 数据集有 6 万张图片,一次性加载所有图片比力斲丧内存,并且一次性训练对 GPU 的要求也很高,因此我们需要分批处理数据集。

torch.utils.data 中有数据加载器,可以资助我们分批加载图片集到内存中,开辟时使用迭代器直接读取,不需要关注分批情况。
如下面所示,分批加载数据集,批处理巨细是 64 张图片。
  1. // 分批加载图像,打乱顺序
  2. var train_loader = torch.utils.data.DataLoader(training_data, batchSize: 64, shuffle: true, device: defaultDevice);
  3. // 分批加载图像,不打乱顺序
  4. var test_loader = torch.utils.data.DataLoader(test_data, batchSize: 64, shuffle: false, device: defaultDevice);
复制代码
留意,分批是在 DataLoader 内部发生的,我们可以理解为缓冲区巨细,对于开辟者来说,并不需要关注分批情况。
界说网络

接下来界说一个神经网络,神经网络有多个层,通过神经网络来训练数据,通过数据的训练可以的出参数、权重等信息,这些信息会被保存到模型中,加载模型时,必须要有对应的网络结构,比如神经网络的层数要相同、每层的结构划一。
该网络通过接受 28*28 巨细的图片,经过处理后输出 10 个分类值,每个分类结果都带有其可能的概率,概率最高的就是识别结果。

将以下代码存储到 NeuralNetwork.cs 中。
  1. using TorchSharp.Modules;
  2. using static TorchSharp.torch;
  3. using nn = TorchSharp.torch.nn;
  4. public class NeuralNetwork : nn.Module<Tensor, Tensor>
  5. {
  6.     // 传递给基类的参数是模型的名称
  7.     public NeuralNetwork() : base(nameof(NeuralNetwork))
  8.     {
  9.         flatten = nn.Flatten();
  10.         linear_relu_stack = nn.Sequential(
  11.             nn.Linear(28 * 28, 512),
  12.             nn.ReLU(),
  13.             nn.Linear(512, 512),
  14.             nn.ReLU(),
  15.             nn.Linear(512, 10));
  16.         // C# 版本需要调用这个函数,将模型的组件注册到模型中
  17.         RegisterComponents();
  18.     }
  19.     Flatten flatten;
  20.     Sequential linear_relu_stack;
  21.     public override Tensor forward(Tensor input)
  22.     {
  23.         // 将输入一层层处理并传递给下一层
  24.         var x = flatten.call(input);
  25.         var logits = linear_relu_stack.call(x);
  26.         return logits;
  27.     }
  28. }
复制代码
留意,网络中只能界说字段,不要界说属性;不要使用 _ 开头界说字段;
然后继承在 Program 里继承编写代码,初始化神经网络,并使用 GPU 来加载网络。
  1. var model = new NeuralNetwork();
  2. model.to(defaultDevice);
复制代码
优化模型参数

为了训练模型,需要界说一个损失函数和一个优化器,损失函数的主要作用是衡量模型的猜测结果与真实标签之间的差异,即毛病或损失,有了损失函数后,通过优化器可以指导模型参数的调整,使猜测结果能够逐步靠近真实值,从而进步模型的性能。Pytorch 自带很多损失函数,这里使用计算交叉熵损失的损失函数。
  1. // 定义损失函数、优化器和学习率
  2. var loss_fn = nn.CrossEntropyLoss();
  3. var optimizer = torch.optim.SGD(model.parameters(), learningRate : 1e-3);
复制代码
同时,优化器也很重要,是用于调整模型参数以最小化损失函数的模块。
因为损失函数比力多,但是优化器就那么几个,以是这里简单列一下 Pytorch 中自带的一些优化器。
训练模型

接下来讲解训练模型的步骤,如下代码所示。
下面是具体步骤:
  1. static void Train(DataLoader dataloader, NeuralNetwork model, CrossEntropyLoss loss_fn, SGD optimizer)
  2. {
  3.     var size = dataloader.dataset.Count;
  4.     model.train();
  5.     int batch = 0;
  6.     foreach (var item in dataloader)
  7.     {
  8.         var x = item["data"];
  9.         var y = item["label"];
  10.         // 第一步
  11.         // 训练当前图片
  12.         var pred = model.call(x);
  13.         // 通过损失函数得出与真实结果的误差
  14.         var loss = loss_fn.call(pred, y);
  15.         // 第二步,反向传播
  16.         loss.backward();
  17.         // 计算梯度并优化参数
  18.         optimizer.step();
  19.         // 清空优化器当前的梯度
  20.         optimizer.zero_grad();
  21.         // 每 100 次打印损失值和当前训练的图片数量
  22.         if (batch % 100 == 0)
  23.         {
  24.             loss = loss.item<float>();
  25.             
  26.             // Pytorch 框架会在 x.shape[0] 存储当前批的位置
  27.             var current = (batch + 1) * x.shape[0];
  28.             
  29.             Console.WriteLine("loss: {loss.item<float>(),7}  [{current,5}/{size,5}]");
  30.         }
  31.         batch++;
  32.     }
  33. }
复制代码
torch.Tensor 类型的 .shape 属性比力特殊,是一个数组类型,主要用于存储当前类型的结构,要结合上下文才能判断,例如在当前训练中,x.shape 值是 [64,1,28,28],shape[1] 是图像的通道,1 是灰色,3 是彩色(RGB三通道);shape[2]、shape[3] 分别是图像的长度和高度。

通过上面步骤可以看出,“训练” 是一个字面意思,跟人类的学习不一样,这里是先使用模型识别一个图片,然后计算毛病,更新模型参数和权重,然后进入下一次调整。

训练模型的同时,我们还需要评估模型的准确率等信息,评估时需要使用测试图片来验证训练结果。
  1. static void Test(DataLoader dataloader, NeuralNetwork model, CrossEntropyLoss loss_fn)
  2. {
  3.     var size = (int)dataloader.dataset.Count;
  4.     var num_batches = (int)dataloader.Count;
  5.     // 将模型设置为评估模式
  6.     model.eval();
  7.     var test_loss = 0F;
  8.     var correct = 0F;
  9.     using (var n = torch.no_grad())
  10.     {
  11.         foreach (var item in dataloader)
  12.         {
  13.             var x = item["data"];
  14.             var y = item["label"];
  15.             // 使用已训练的参数预测测试数据
  16.             var pred = model.call(x);
  17.             // 计算损失值
  18.             test_loss += loss_fn.call(pred, y).item<float>();
  19.             correct += (pred.argmax(1) == y).type(ScalarType.Float32).sum().item<float>();
  20.         }
  21.     }
  22.     test_loss /= num_batches;
  23.     correct /= size;
  24.     Console.WriteLine("Test Error: \n Accuracy: {(100 * correct):F1}%, Avg loss: {test_loss:F8} \n");
  25. }
复制代码
下图是背面训练打印的日志,可以看出准确率是逐步上升的。


在 Program 中添加训练代码,我们使用训练数据集举行五轮训练,每轮训练都输出识别结果。
  1. // 训练的轮数
  2. var epochs = 5;
  3. foreach (var epoch in Enumerable.Range(0, epochs))
  4. {
  5.     Console.WriteLine("Epoch {epoch + 1}\n-------------------------------");
  6.     Train(train_loader, model, loss_fn, optimizer);
  7.     Test(train_loader, model, loss_fn);
  8. }
  9. Console.WriteLine("Done!");
复制代码
保存和加载模型

经过训练后的模型,可以直接保存和加载,代码很简单,如下所示:
  1. model.save("model.dat");
  2. Console.WriteLine("Saved PyTorch Model State to model.dat");
  3. model.load("model.dat");
复制代码
使用模型识别图片

要使用模型识别图片,只需要使用 var pred = model.call(x); 即可,但是因为模型并不能直接输出识别结果,而是根据网络结构输出到每个神经元中,每个神经元都表示当前概率。在前面界说的网络中,nn.Linear(512, 10)) 会输出 10 个分类结果,每个分类结果都带有概率,那么我们将概率最高的一个结果拿出来,就相当于图片的识别结果了。
代码如下所示,步骤讲解如下:
  1. var classes = new string[] {
  2.     "T-shirt/top",
  3.     "Trouser",
  4.     "Pullover",
  5.     "Dress",
  6.     "Coat",
  7.     "Sandal",
  8.     "Shirt",
  9.     "Sneaker",
  10.     "Bag",
  11.     "Ankle boot",
  12. };
  13. // 设置为评估模式
  14. model.eval();
  15. // 加载测试数据中的第一个图片以及其标签
  16. var x = test_data.GetTensor(0)["data"];
  17. var y = test_data.GetTensor(0)["label"];
  18. using (torch.no_grad())
  19. {
  20.     x = x.to(defaultDevice);
  21.     var pred = model.call(x);
  22.     var predicted = classes[pred[0].argmax(0).ToInt32()];
  23.     var actual = classes[y.ToInt32()];
  24.     Console.WriteLine("Predicted: "{predicted}", Actual: "{actual}"");
  25. }
复制代码
当然,使用 Maomi.Torch 的接口,可以很方便读取图片使用模型识别:
  1. var img = MM.LoadImage("0.png");
  2. using (torch.no_grad())
  3. {
  4.     img = img.to(defaultDevice);
  5.     var pred = model.call(img);
  6.     // 转换为归一化的概率
  7.     var array = torch.nn.functional.softmax(pred, dim: 0);
  8.     var max = array.ToFloat32Array().Max();
  9.     var predicted = classes[pred[0].argmax(0).ToInt32()];
  10.    
  11.     Console.WriteLine("识别结果 {predicted},概率 {max * 100}%");
  12. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4