教程名稱:使用 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 引入以下類庫:
TorchSharp
TorchSharp-cuda-windows
TorchVision
Maomi.Torch
首先添加以下代碼,查找最適合當前設備的工作方式,主要是選擇 GPU 開發框架,例如 CUDA、MPS,CPU,有 GPU 就用 GPU,沒有 GPU 降級為 CPU。
using Maomi.Torch;
Device defaultDevice = MM.GetOpTimalDevice();
torch.set_default_device(defaultDevice);
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 數據集,首先添加引用:
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=True
如果 root
中沒有數據,則從互聯網下載數據。transform
和 target_transform
指定特征和標簽轉換。
注意,與 Python 版本有所差異, Pytorch 官方給出了 ToTensor()
函數用于將圖像轉換為 torch.Tensor 張量類型,但是由于 C# 版本并沒有這個函數,因此只能手動指定一個轉換器。
啟動項目,會自動下載數據集,接著在程序運行目錄下會自動創建一個 data 目錄,里面是數據集文件,包括用于訓練的數據和測試的數據集。

文件內容如下所示,子目錄 test_data 里面的是測試數據集,用于檢查模型訓練情況和優化。
│ t10k-images-idx3-ubyte.gz
│ t10k-labels-idx1-ubyte.gz
│ train-images-idx3-ubyte.gz
│ train-labels-idx1-ubyte.gz
│
└───test_data
t10k-images-idx3-ubyte
t10k-labels-idx1-ubyte
train-images-idx3-ubyte
train-labels-idx1-ubyte
顯示圖片
數據集是 Dataset 類型,繼承了 Dataset<Dictionary<string, Tensor>>
類型,Dataset 本質是列表,我們把 Dataset 列表的 item 稱為數據,每個 item 都是一個字典類型,每個字典由 data、label 兩個 key 組成。
在上一節,已經編寫好如何加載數據集,將訓練數據和測試數據分開加載,為了了解 Dataset ,讀者可以通過以下代碼將數據集的結構打印到控制臺。
for (int i = 0; i < training_data.Count; i++)
{
var dic = training_data.GetTensor(i);
var img = dic["data"];
var label = dic["label"];
label.print();
}
通過觀察控制臺,可以知道,每個數據元素都是一個字典,每個字典由 data、label 兩個 key 組成,dic["data"]
是一個圖片,而 label 就是表示該圖片的文本值是什么。
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"];
if (i > 2)
{
break;
}
img.ShowImage();
}
使用 Maomi.ScottPlot.Winforms 庫,還可以通過 img.ShowImageToForm()
接口通過窗口的形式顯示圖片。
你也可以直接轉存為圖片:
img.SavePng("data/{i}.png");

加載數據集
由于 FashionMNIST 數據集有 6 萬張圖片,一次性加載所有圖片比較消耗內存,并且一次性訓練對 GPU 的要求也很高,因此我們需要分批處理數據集。
torch.utils.data
中有數據加載器,可以幫助我們分批加載圖片集到內存中,開發時使用迭代器直接讀取,不需要關注分批情況。
如下面所示,分批加載數據集,批處理大小是 64 張圖片。
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);
注意,分批是在 DataLoader 內部發生的,我們可以理解為緩沖區大小,對于開發者來說,并不需要關注分批情況。
定義網絡
接下來定義一個神經網絡,神經網絡有多個層,通過神經網絡來訓練數據,通過數據的訓練可以的出參數、權重等信息,這些信息會被保存到模型中,加載模型時,必須要有對應的網絡結構,比如神經網絡的層數要相同、每層的結構一致。
該網絡通過接受 28*28
大小的圖片,經過處理后輸出 10 個分類值,每個分類結果都帶有其可能的概率,概率最高的就是識別結果。
將以下代碼存儲到 NeuralNetwork.cs 中。
using TorchSharp.Modules;
using static TorchSharp.torch;
using nn = TorchSharp.torch.nn;
public class NeuralNetwork : nn.Module<Tensor, Tensor>
{
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();
}
Flatten flatten;
Sequential linear_relu_stack;
public override Tensor forward(Tensor input)
{
var x = flatten.call(input);
var logits = linear_relu_stack.call(x);
return logits;
}
}
注意,網絡中只能定義字段,不要定義屬性;不要使用 _
開頭定義字段;
然后繼續在 Program 里繼續編寫代碼,初始化神經網絡,并使用 GPU 來加載網絡。
var model = new NeuralNetwork();
model.to(defaultDevice);
優化模型參數
為了訓練模型,需要定義一個損失函數和一個優化器,損失函數的主要作用是衡量模型的預測結果與真實標簽之間的差異,即誤差或損失,有了損失函數后,通過優化器可以指導模型參數的調整,使預測結果能夠逐步靠近真實值,從而提高模型的性能。Pytorch 自帶很多損失函數,這里使用計算交叉熵損失的損失函數。
var loss_fn = nn.CrossEntropyLoss();
var optimizer = torch.optim.SGD(model.parameters(), learningRate : 1e-3);
同時,優化器也很重要,是用于調整模型參數以最小化損失函數的模塊。
因為損失函數比較多,但是優化器就那么幾個,所以這里簡單列一下 Pytorch 中自帶的一些優化器。
- SGD(隨機梯度下降):通過按照損失函數的梯度進行線性步長更新權重;
- Adam(自適應矩估計) :基于一階和二階矩估計的優化算法,它能自適應地調整學習率,對大多數問題效果較好;
- RMSprop:適用于處理非平穩目標,能夠自動進行學習率的調整;
- AdamW(帶權重衰減的 Adam) :在 Adam 的基礎上添加了權重衰減(weight decay),防止過擬合。
訓練模型
接下來講解訓練模型的步驟,如下代碼所示。
下面是詳細步驟:
- 每讀取一張圖片,就使用神經網絡進行識別(
.call()
函數),pred
為識別結果; - 通過損失函數判斷網絡的識別結果和標簽值的誤差;
- 通過損失函數反向傳播,計算網絡的梯度等;
- 通過 SGD 優化器,按照損失函數的梯度進行線性步長更新權重,
optimizer.step()
會調整模型的權重,根據計算出來的梯度來更新模型的參數,使模型逐步接近優化目標。 - 因為數據是分批處理的,因此計算當前批次的梯度后,需要使用
optimizer.zero_grad()
重置當前所有梯度。 - 計算訓練成果,即打印當前訓練進度和損失值。
static void Train(DataLoader dataloader, NeuralNetwork model, CrossEntropyLoss loss_fn, SGD optimizer)
{
var size = dataloader.dataset.Count;
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)
{
loss = loss.item<float>();
var current = (batch + 1) * x.shape[0];
Console.WriteLine("loss: {loss.item<float>(),7} [{current,5}/{size,5}]");
}
batch++;
}
}
torch.Tensor 類型的 .shape
屬性比較特殊,是一個數組類型,主要用于存儲當前類型的結構,要結合上下文才能判斷,例如在當前訓練中,x.shape
值是 [64,1,28,28]
,shape[1]
是圖像的通道,1 是灰色,3 是彩色(RGB三通道);shape[2]
、shape[3]
分別是圖像的長度和高度。
通過上面步驟可以看出,“訓練” 是一個字面意思,跟人類的學習不一樣,這里是先使用模型識別一個圖片,然后計算誤差,更新模型參數和權重,然后進入下一次調整。
訓練模型的同時,我們還需要評估模型的準確率等信息,評估時需要使用測試圖片來驗證訓練結果。
static void Test(DataLoader dataloader, NeuralNetwork model, CrossEntropyLoss loss_fn)
{
var size = (int)dataloader.dataset.Count;
var num_batches = (int)dataloader.Count;
model.eval();
var test_loss = 0F;
var correct = 0F;
using (var n = 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<float>();
correct += (pred.argmax(1) == y).type(ScalarType.Float32).sum().item<float>();
}
}
test_loss /= num_batches;
correct /= size;
Console.WriteLine("Test Error: \n Accuracy: {(100 * correct):F1}%, Avg loss: {test_loss:F8} \n");
}
下圖是后面訓練打印的日志,可以看出準確率是逐步上升的。

在 Program 中添加訓練代碼,我們使用訓練數據集進行五輪訓練,每輪訓練都輸出識別結果。
var epochs = 5;
foreach (var epoch in Enumerable.Range(0, epochs))
{
Console.WriteLine("Epoch {epoch + 1}\n-------------------------------");
Train(train_loader, model, loss_fn, optimizer);
Test(train_loader, model, loss_fn);
}
Console.WriteLine("Done!");
保存和加載模型
經過訓練后的模型,可以直接保存和加載,代碼很簡單,如下所示:
model.save("model.dat");
Console.WriteLine("Saved PyTorch Model State to model.dat");
model.load("model.dat");
使用模型識別圖片
要使用模型識別圖片,只需要使用 var pred = model.call(x);
即可,但是因為模型并不能直接輸出識別結果,而是根據網絡結構輸出到每個神經元中,每個神經元都表示當前概率。在前面定義的網絡中,nn.Linear(512, 10))
會輸出 10 個分類結果,每個分類結果都帶有概率,那么我們將概率最高的一個結果拿出來,就相當于圖片的識別結果了。
代碼如下所示,步驟講解如下:
- 因為模型和網絡并不使用字符串表示每個分類結果,所以需要手動配置分類表。
- 然后從測試數據集中選取第一個圖片和標簽,識別圖片并獲得序號。
- 從分類字符串中通過序號獲得分類名稱。
var classes = new string[] {
"T-shirt/top",
"Trouser",
"Pullover",
"Dress",
"Coat",
"Sandal",
"Shirt",
"Sneaker",
"Bag",
"Ankle boot",
};
model.eval();
var x = test_data.GetTensor(0)["data"];
var y = test_data.GetTensor(0)["label"];
using (torch.no_grad())
{
x = x.to(defaultDevice);
var pred = model.call(x);
var predicted = classes[pred[0].argmax(0).ToInt32()];
var actual = classes[y.ToInt32()];
Console.WriteLine("Predicted: \"{predicted}\", Actual: \"{actual}\"");
}
當然,使用 Maomi.Torch 的接口,可以很方便讀取圖片使用模型識別:
var img = MM.LoadImage("0.png");
using (torch.no_grad())
{
img = img.to(defaultDevice);
var pred = model.call(img);
var array = torch.nn.functional.softmax(pred, dim: 0);
var max = array.ToFloat32Array().Max();
var predicted = classes[pred[0].argmax(0).ToInt32()];
Console.WriteLine("識別結果 {predicted},概率 {max * 100}%");
}