C# 深度學習框架 TorchSharp 原生訓練模型和圖像識別-自定義網絡模型和識別手寫數字
教程名稱:使用 C# 入門深度學習
作者:癡者工良
電子書倉庫:https://github.com/whuanle/cs_pytorch
Maomi.Torch 項目倉庫:https://github.com/whuanle/Maomi.Torch
使用 Torch 訓練模型
本章主要參考《破解深度學習》的第四章,在本章將會實現一個數字分類器,主要包括數據加載和處理、模型訓練和保存、預訓練模型加載,但是內容跟 開始使用 Torch 一章差不多,只是數據集和網絡定義不一樣,通過本章的案例幫助讀者進一步了解 TorchSharp 以及掌握模型訓練的步驟和基礎。
本章代碼請參考 example2.3。
搭建神經網絡的一般步驟:

在上一篇中我們通過示例已經學習到相關的過程,所以本章會在之前的基礎上繼續講解一些細節和步驟。
在上一章中,我們學習了如何下載和加載數據集,如果將數據集里面的圖片導出,我們可以發現里面都是單個數字。
你可以使用 Maomi.Torch 包中的擴展方法將數據集轉存到本地目錄中。
for (int i = 0; i < training_data.Count; i++)
{
var dic = training_data.GetTensor(i);
var img = dic["data"];
var label = dic["label"];
img.SaveJpeg("imgs/{i}.jpg");
}
如圖所示:

每個圖片的大小是 28*28=784,所以神經網絡的輸入層的大小是 784。

我們直接知道,由于數據集的圖片都是 0-9 的數字,都是灰度圖像(沒有彩色),因此模型訓練結果的輸出應該是 10 個,也就是神經網絡的輸出層神經元個數是 10。
神經網絡的輸入層是要固定大小是,表示神經元的個數輸入是固定的,不是隨時可以擴充的,也就是一個神經網絡不能輸入任意大小的圖像,這些圖像都要經過一定的算法出來,生成與神經網絡輸入層對應大小的圖像。
定義神經網絡
第一步,定義我們的網絡模型,這是一個全連接網絡,由激活函數和三個線性層組成。
該網絡模型沒有指定輸入層和輸出層的大小,這樣該模型可以適配不同的圖像分類任務,開發者在訓練和加載模式時,指定輸入層和輸出層大小即可。
代碼如下所示:
using TorchSharp;
using static TorchSharp.torch;
using nn = TorchSharp.torch.nn;
public class MLP : nn.Module<Tensor, Tensor>, IDisposable
{
private readonly int _inputSize;
private readonly int _hiddenSize;
private readonly int _numClasses;
private TorchSharp.Modules.Linear fc1;
private TorchSharp.Modules.ReLU relu;
private TorchSharp.Modules.Linear fc2;
private TorchSharp.Modules.Linear fc3;
/// <summary></summary>
/// <param name="inputSize">輸入層大小,圖片的寬*高.</param>
/// <param name="hiddenSize">隱藏層大小.</param>
/// <param name="outputSize">輸出層大小,例如有多少個分類.</param>
/// <param name="device"></param>
public MLP(int inputSize, int hiddenSize, int outputSize) : base(nameof(MLP))
{
_inputSize = inputSize;
_hiddenSize = hiddenSize;
_numClasses = outputSize;
// 定義激活函數和線性層
relu = nn.ReLU();
fc1 = nn.Linear(inputSize, hiddenSize);
fc2 = nn.Linear(hiddenSize, hiddenSize);
fc3 = nn.Linear(hiddenSize, outputSize);
RegisterComponents();
}
public override torch.Tensor forward(torch.Tensor input)
{
// 一層一層傳遞
// 第一層讀取輸入,然后傳遞給激活函數,
// 第二層讀取第一層的輸出,然后傳遞給激活函數,
// 第三層讀取第二層的輸出,然后生成輸出結果
var @out = fc1.call(input);
@out = relu.call(@out);
@out = fc2.call(@out);
@out = relu.call(@out);
@out = fc3.call(@out);
return @out;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
fc1.Dispose();
relu.Dispose();
fc2.Dispose();
fc3.Dispose();
}
}
首先 fc1 作為第一層網絡,輸入的圖像需要轉換為一維結構,主要用于接收數據、數據預處理。由于繪圖太麻煩了,這里用文字簡單說明一下,例如圖像是 28*28,也就是每行有 28 個像素,一共 28 行,那么使用一個 784 大小的數組可以將圖像的每一行首尾連在一起,放到一個一維數組中。
由于圖像都是灰度圖像,一個黑白像素值在 0-255 之間(byte 類型),如果使用 [0.0,1.0] 之間表示黑白(float32 類型),那么輸入像素表示為灰度,值為 0.0 表示白色,值為 1.0 表示黑色,中間數值表示灰度。
大多數情況下,或者說在本教程中,圖像的像素都是使用 float32 類型表示,即 torch.Tensor 存儲的圖像信息都是 float32 類型表示一個像素。

圖來自《深入淺出神經網絡與深度學習》。
fc2 是隱藏層,在本章示范的網絡模型中,隱藏層只有一層,大小是 15 個神經元,承擔者特征提取、非線性變換等職責,隱藏層的神經元數量是不定的,主要是根據經驗來設置,然后根據訓練的模型性能來調整。
fc3 是輸出層,根據提取的特征將輸出推送到 10 個神經元中,每個神經元表示一個數值,每個神經元都會接收到消息,但是因為不同數字的特征和權重值不一樣,所以每個神經元的值都不一樣,接收到的值就是表示當前數字的可能性概率。
加載數據集
加載數據集的代碼示例如下,由于上一章已經講解過,因此這里就不再贅述。
// 1. 加載數據集
// 從 MNIST 數據集下載數據或者加載已經下載的數據
using var train_data = datasets.MNIST("./mnist/data", train: true, download: true, target_transform: transforms.ConvertImageDtype(ScalarType.Float32));
using var test_data = datasets.MNIST("./mnist/data", train: false, download: true, target_transform: transforms.ConvertImageDtype(ScalarType.Float32));
Console.WriteLine("Train data size: " + train_data.Count);
Console.WriteLine("Test data size: " + test_data.Count);
var batch_size = 100;
// 分批加載圖像,打亂順序
var train_loader = torch.utils.data.DataLoader(train_data, batchSize: batch_size, shuffle: true, defaultDevice);
// 分批加載圖像,不打亂順序
var test_loader = torch.utils.data.DataLoader(test_data, batchSize: batch_size, shuffle: false, defaultDevice);
創建網絡模型
由于 MNIST 數據集的圖像都是 28*28 的,因此我們創建網絡模型實例時,定義輸入層為 784 大小。
// 輸入層大小,按圖片的寬高計算
var input_size = 28 * 28;
// 隱藏層大小,大小不固定,可以自己調整
var hidden_size = 15;
// 手動配置分類結果個數
var num_classes = 10;
var model = new MLP(input_size, hidden_size, num_classes);
model.to(defaultDevice);
定義損失函數
創建損失函數和優化器,這個學習率的大小也是依據經驗和性能進行設置,沒有什么規律,學習率的作用可以參考梯度下降算法中的知識。
// 創建損失函數
var criterion = nn.CrossEntropyLoss();
// 學習率
var learning_rate = 0.001;
// 優化器
var optimizer = optim.Adam(model.parameters(), lr: learning_rate);
訓練
開始訓練模型,對數據集進行 10 輪訓練,每輪訓練都輸出訓練結果,這里不使用一張張圖片測試準確率,而是一次性識別所有圖片(一萬張),然后計算平均準確率。
foreach (var epoch in Enumerable.Range(0, num_epochs))
{
model.train();
int i = 0;
foreach (var item in train_loader)
{
var images = item["data"];
var lables = item["label"];
images = images.reshape(-1, 28 * 28);
var outputs = model.call(images);
var loss = criterion.call(outputs, lables);
optimizer.zero_grad();
loss.backward();
optimizer.step();
i++;
if ((i + 1) % 300 == 0)
{
Console.WriteLine("Epoch [{(epoch + 1)}/{num_epochs}], Step [{(i + 1)}/{train_data.Count / batch_size}], Loss: {loss.ToSingle():F4}");
}
}
model.eval();
using (torch.no_grad())
{
long correct = 0;
long total = 0;
foreach (var item in test_loader)
{
var images = item["data"];
var labels = item["label"];
images = images.reshape(-1, 28 * 28);
var outputs = model.call(images);
var (_, predicted) = torch.max(outputs, 1);
total += labels.size(0);
correct += (predicted == labels).sum().item<long>();
}
Console.WriteLine("Accuracy of the network on the 10000 test images: {100 * correct / total} %");
}
}
保存訓練后的模型:
model.save("mnist_mlp_model.dat");
訓練信息:

識別手寫圖像
如下示例圖像所示,是一個手寫數字。

重新加載模型:
model.save("mnist_mlp_model.dat");
model.load("mnist_mlp_model.dat");
// 把模型轉為評估模式
model.eval();
使用 Maomi.Torch 導入圖片并轉為 Tensor,然后將 28*28 轉換為以為的 784。
由于加載圖像的時候默認是彩色的,所以需要將其轉換為灰度圖像,即
channels=1。
// 加載圖片為張量
var image = MM.LoadImage("5.jpg", channels: 1);
image = image.to(defaultDevice);
image = image.reshape(-1, 28 * 28);
識別圖像并輸出結果:
using (torch.no_grad())
{
var oputput = model.call(image);
var prediction = oputput.argmax(dim: 1, keepdim: true);
Console.WriteLine("Predicted Digit: " + prediction.item<long>().ToString());
}
當然,對應彩色的圖像,也可以這樣通過灰度轉換處理,再進行層歸一化,即可獲得對應結構的 torch.Tensor。
image = image.reshape(-1, 28 * 28);
var transform = transforms.ConvertImageDtype(ScalarType.Float32);
var img = transform.call(image).unsqueeze(0);
再如下圖所示,隨便搞了個數字,圖像是 212*212,圖像格式是 jpg。
注意,由于數據集的圖片都是 jpg 格式,因此要識別的圖像,也需要使用 jpg 格式。

如下代碼所示,首先使用 Maomi.Torch 加載圖片,然后調整圖像大小為 28*28,以區配網絡模型的輸入層大小。
// 加載圖片為張量
image = MM.LoadImage("6.jpg", channels: 1);
image = image.to(defaultDevice);
// 將圖像轉換為 28*28 大小
image = transforms.Resize(28, 28).call(image);
image = image.reshape(-1, 28 * 28);
using (torch.no_grad())
{
var oputput = model.call(image);
var prediction = oputput.argmax(dim: 1, keepdim: true);
Console.WriteLine("Predicted Digit: " + prediction.item<long>().ToString());
}

浙公網安備 33010602011771號