使用 C# 入門深度學習:Pytorch 基礎

痴者工良發表於2024-11-12

教程名稱:使用 C# 入門深度學習

作者:痴者工良

地址:

https://torch.whuanle.cn

1.2 Pytorch 基礎

本文內容介紹 Pytorcn 的基礎 API,主要是陣列的建立方式和運算方式,由於相關內容跟 Numpy 比較相似,並且 Numpy 型別可以轉 torch.Tensor,因此對 Numpy 感興趣的讀者可以參考筆者的其它文章:

  • Python 之 Numpy 框架入門

https://www.whuanle.cn/archives/21461

https://www.cnblogs.com/whuanle/p/17855578.html


提示:學習本文時,如果對線性代數有足夠的瞭解,則學習效果更佳,沒有線性代數基礎也沒關係,後面會學習到。本文會同時使用 Python 和 C# 編寫示例,方便各位讀者對照差異,在後續的章節學習中,基本只會使用 C# 編寫示例。


基礎使用

由於神經網路中的數值很多以向量或陣列等形式存在,不像日常程式設計中的數值型別那麼簡單,因此列印數值資訊是我們學習瞭解或除錯程式的一種手段,下面我們來觀察程式是怎麼列印 Pytorch 中複雜資料型別的。


列印

下面使用 Pytorch 建立一個從 0..9 的陣列,接著列印陣列。

Python:

import torch
x = torch.arange(10)
print(x)
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

C# 版本不使用 Console.WriteLine(),而是使用官方提供的庫。

using TorchSharp;

var x = torch.arange(10);
x.print(style:TensorStringStyle.Default);
x.print(style:TensorStringStyle.Numpy);
x.print(style:TensorStringStyle.Metadata);
x.print(style:TensorStringStyle.Julia);
x.print(style:TensorStringStyle.CSharp);
[10], type = Int64, device = cpu 0 1 2 3 4 5 6 7 8 9
[0, 1, 2, ... 7, 8, 9]
[10], type = Int64, device = cpu
[10], type = Int64, device = cpu 0 1 2 3 4 5 6 7 8 9
[10], type = Int64, device = cpu, value = long [] {0L, 1L, 2L, ... 7L, 8L, 9L}

Python 列印的結果比較容易理解,C# 預設列印的方式比較難看,所以一般來說,視覺化都使用 TensorStringStyle.Numpy 列舉。

C# 列印數值時,引數有個 string? fltFormat = "g5",表示精確度的意思,即列印的小數位數。

在 Maomi.Torch 包中提供了一些擴充套件方法,讀者可以使用 x.print_numpy() 擴充套件直接列印對應風格的資訊。


對於後面的章節來說,預設都引入 Python 的 torch 包名稱、C# 的 TorchSharp 名稱空間,後續程式碼示例可能會省略引入程式碼,讀者自行引入。


基本資料型別

Pytorch 的資料型別跟我們程式語言中的基本型別不太一樣,讀者要注意區別。

具體詳細的官方文件參考連結:

https://pytorch.org/docs/stable/tensor_attributes.html

https://pytorch.ac.cn/docs/stable/tensor_attributes.html


Pytorch 建立的資料型別以 torch.Tensor 表示,torch.Tensor 是用來處理機器學習模型中的各種資料的基礎結構,包括標量、向量、矩陣以及更高維度的張量。如果筆者沒理解錯的話,在 Pytorch 中建立的 Tensor 物件就叫張量。開發者可以透過各種形式的資料在 Pytorch 建立 Tensor

Pytorch 建立的資料型別,都使用 Tensor 物件表示。

對於這句話的理解,建議看完本文再回頭看看。


PyTorch 有十二種不同的資料型別,列表如下:

資料型別 dtype
32 位浮點數 torch.float32torch.float
64 位浮點數 torch.float64torch.double
64 位複數 torch.complex64torch.cfloat
128 位複數 torch.complex128torch.cdouble
16 位浮點數 torch.float16torch.half
16 位浮點數 torch.bfloat16
8 位整數(無符號) torch.uint8
8 位整數(有符號) torch.int8
16 位整數(有符號) torch.int16torch.short
32 位整數(有符號) torch.int32torch.int
64 位整數(有符號) torch.int64torch.long
布林值 torch.bool

下面示範在建立一個數值全為 1 的陣列時,設定陣列的型別。

Python:

float_tensor = torch.ones(1, dtype=torch.float)
double_tensor = torch.ones(1, dtype=torch.double)
complex_float_tensor = torch.ones(1, dtype=torch.complex64)
complex_double_tensor = torch.ones(1, dtype=torch.complex128)
int_tensor = torch.ones(1, dtype=torch.int)
long_tensor = torch.ones(1, dtype=torch.long)
uint_tensor = torch.ones(1, dtype=torch.uint8)

C#:

var float_tensor = torch.ones(1, dtype: torch.float32);
var double_tensor = torch.ones(1, dtype: torch.float64);
var complex_float_tensor = torch.ones(1, dtype: torch.complex64);
var complex_double_tensor = torch.ones(1, dtype: torch.complex128);
var int_tensor = torch.ones(1, dtype: torch.int32); ;
var long_tensor = torch.ones(1, dtype: torch.int64);
var uint_tensor = torch.ones(1, dtype: torch.uint8);

在 C# 中, torch.ScalarType 列舉表示 Pytorch 的資料型別,所以可以有以下兩種方式指定資料型別。

例如:

var arr = torch.zeros(3,3,3, torch.ScalarType.Float32);
arr.print_numpy();

或:

var arr = torch.zeros(3,3,3, torch.float32);
arr.print_numpy();

CPU 或 GPU 運算

我們知道,AI 模型可以在 CPU 下執行,也可以在 GPU 下執行,Pytorch 的資料也可以這樣做,在建立資料型別時就設定繫結的裝置,在運算使用會使用對應的裝置進行運算。

一般使用 cpu 表示 CPU,使用 cudacuda:{顯示卡序號} 表示 GPU。


下面編寫程式碼判斷 Pytorch 正在使用 GPU 還是 CPU 執行。


Python:

print(torch.get_default_device())

C#:

 Console.WriteLine(torch.get_default_device())

如果當前裝置支援 GPU,則使用 GPU 啟動 Pytorch,否則使用 CPU 啟動 Pytorch。可以透過 torch.device('cuda')torch.device('cuda:0') 指定使用 GPU 、指定使用哪個 GPU。


Python:

if torch.cuda.is_available():
    print("當前裝置支援 GPU")
    device = torch.device('cuda')
    # 使用 GPU 啟動
    torch.set_default_device(device)
    current_device = torch.cuda.current_device()
    print(f"繫結的 GPU 為:{current_device}")
else:
    # 不支援 GPU,使用 CPU 啟動
    device = torch.device('cpu')
    torch.set_default_device(device)

default_device = torch.get_default_device()
print(f"當前正在使用 {default_device}")

C#:

if (torch.cuda.is_available())
{
    Console.WriteLine("當前裝置支援 GPU");
    var device = torch.device("cuda",index:0);
    // 使用 GPU 啟動
    torch.set_default_device(device);
}
else
{
    var device = torch.device("cpu");
    // 使用 CPU 啟動
    torch.set_default_device(device);
    Console.WriteLine("當前正在使用 CPU");
}

var default_device = torch.get_default_device();
Console.WriteLine($"當前正在使用 {default_device}");

C# 沒有 torch.cuda.current_device() 這個方法,建議預設設定使用哪塊 GPU,即設定 index 引數。


另外可以透過使用 torch.cuda.device_count() 獲取裝置有多少個顯示卡,這裡不再贅述。


Pytorch 還支援針對單獨的資料型別設定使用 CPU 還是 GPU,還可以讓兩者混合運算,這裡不再贅述。


Tensor 型別

在 Pytorch 中,可以將標量、陣列等型別轉換為 Tensor 型別,Tensor 表示的資料結構就叫張量。

x = torch.tensor(3.0);

基本陣列

Pytorch 使用 asarray() 函式將 obj 值轉換為陣列,其定義如下:

torch.asarray(obj, *, dtype=None, device=None, copy=None, requires_grad=False) → Tensor

官方 API 文件:https://pytorch.org/docs/stable/generated/torch.asarray.html#torch-asarray


obj 可以是以下之一:

  • a tensor(張量)
  • a NumPy array or a NumPy scalar(NumPy 陣列或 NumPy 標量)
  • a DLPack capsule
  • an object that implements Python’s buffer protocol
  • a scalar(標量)
  • a sequence of scalars(標量序列)

筆者不會的或者本文用不到的,就不翻譯了。


比如說,傳入一個平常的陣列型別,轉換成 Pytorch 中的陣列型別。


Python:

arr = torch.asarray([1,2,3,4,5,6], dtype=torch.float)
print(arr)

C#:

var arr = torch.from_array(new float[] { 1, 2, 3, 4, 5 });
arr.print(style: TensorStringStyle.Numpy);

請注意,兩種語言的版本差異有些大。

前面提到過,可以給單獨的資料型別設定使用 CPU 還是 GPU。

device = torch.device("cuda",index=0)
arr = torch.asarray(obj=[1,2,3,4,5,6], dtype=torch.float, device=device)
print(arr)

將資料型別轉換為使用 CPU 裝置:

device = torch.device("cuda",index=0)
arr = torch.asarray(obj=[1,2,3,4,5,6], dtype=torch.float, device=device)
arr = arr.cpu()
print(arr)

但是將資料在 GPU、CPU 之間轉換,會消耗一定的效能。


生成陣列

torch.zeros

用於建立一個元素全為 0 的陣列,可以指定陣列大小,其定義如下:

torch.zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

Python:

arr = torch.zeros(10, dtype=torch.float)
print(arr)

C#:

var arr = torch.zeros(10);
arr.print(style: TensorStringStyle.Numpy);

另外,可以指定生成的陣列維度,例如下面指定生成 2*3 的多維陣列。

var arr = torch.zeros(2,3, torch.float32);
arr.print(style: TensorStringStyle.Numpy); 

程式碼為 C# 語言。


列印:

[[0, 0, 0] [0, 0, 0]]

我們還可以生成多種維度的陣列,例如下面生成一個 3*3*3 的陣列:

var arr = torch.zeros(3,3,3, torch.float32);
arr.print(style: TensorStringStyle.Numpy); 

為了方便理解,下面將列印結果做了格式化處理。

[
[[0, 0, 0]  [0, 0, 0]  [0, 0, 0]]
[[0, 0, 0]  [0, 0, 0]  [0, 0, 0]]
[[0, 0, 0]  [0, 0, 0]  [0, 0, 0]]
]

torch.ones

建立一個全由 1 填充的陣列,使用方法跟 torch.zeros 完全一致,因此這裡不再贅述。


torch.empty

建立一個未初始化的陣列,使用方法跟 torch.zeros 完全一致,因此這裡不再贅述。

由於其沒有初始化記憶體,因此記憶體區域會殘留資料,元素的值不確定。


複製函式

此外,上面三個函式還有對應的原型複製函式:

torch.ones_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor
torch.zeros_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor
torch.empty_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor

它們的作用是根據陣列型別,複製一個相同的結構,然後填充對應值。

如下示例,複製陣列相同的結構,但是填充的值為 1。

var arr = torch.ones_like(torch.zeros(3, 3, 3));
arr.print(style: TensorStringStyle.Numpy);

該程式碼語言為 C#。


[
[[1, 1, 1]  [1, 1, 1]  [1, 1, 1]]
[[1, 1, 1]  [1, 1, 1]  [1, 1, 1]]
[[1, 1, 1]  [1, 1, 1]  [1, 1, 1]]
]

torch.rand

torch.rand 會生成一個張量,陣列會填充來自 [0,1) 區間上的均勻分佈的隨機數。

函式定義如下:

torch.rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) → Tensor

例如生成 2*3 大小的,範圍在 [0,1) 區間的隨機數,使用 C# 編寫程式碼:

var arr = torch.rand(2,3);
arr.print(style: TensorStringStyle.Numpy);
[[0.60446, 0.058962, 0.65601] [0.58197, 0.76914, 0.16542]]

由於 C# 繪製圖形的庫不像 Python matplotlib 簡單易用,因此讀者可以引用 Maomi.Torch.ScottPlot、Maomi.ScottPlot.Winforms 兩個包,可以快速轉換 Pytorch 型別和生成繪製視窗 。下面示範使用編寫程式碼繪製均勻分佈的隨機數,方便使用 Python matplotlib 和 Maomi.ScottPlot.Winforms 框架顯示圖形。

Python:

import torch
import matplotlib.pyplot as plt

arr = torch.rand(100, dtype=torch.float)

print(arr)

x = arr.numpy()
y = x
plt.scatter(x,y)
plt.show()


C#:

using Maomi.Torch;
using Maomi.Plot;
using TorchSharp;

var x = torch.rand(100);

x.print(style: TensorStringStyle.Numpy);

ScottPlot.Plot myPlot = new();
myPlot.Add.Scatter(x, x);
var form = myPlot.Show(400, 300);


由圖可知,生成的隨機數是均勻散佈在 [0,1) 區間內。


torch.randn

生成具有給定形狀的標準正態分佈(平均值為0,方差為1)的隨機樣本。隨機樣本取值範圍是[0,1)。

定義如下:

torch.randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) → Tensor

官方文件:https://pytorch.ac.cn/docs/stable/generated/torch.randn.html#torch.rand


由於 C# 不好繪圖,這裡使用 Python 編寫示例:

import torch
import matplotlib.pyplot as plt

arr = torch.randn(100, dtype=torch.float)

print(arr)

x = arr.numpy()
y = x
plt.hist(x, bins=30, alpha=0.5, color='b', edgecolor='black')

plt.show()

x 座標軸是數值,y 座標軸是出現次數。

image-20241103125947540


torch.randint

在某個區間內生成隨機數。

定義如下:

torch.randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

比如在 0-100 範圍內生成 10 個元素,安裝 5*2 結構組成,使用 C# 程式碼編寫。

var arr = torch.randint(low: 0, high: 100, size: new int[] { 5, 2 });
arr.print(style: TensorStringStyle.Numpy);
[[17, 46] [89, 52] [10, 89] [80, 91] [52, 91]]

如果要生成某個區間的浮點數,則可以使用 torch.rand ,但是因為 torch.rand 生成範圍是 [0,1),因此需要自行乘以倍數。例如要生成 [0,100) 的隨機數。

var arr = torch.rand(size: 100, dtype: torch.ScalarType.Float32) * 100;
arr.print(style: TensorStringStyle.Numpy);  

torch.arange

指定區間以及步長,均勻提取元素生成陣列。

定義如下:

torch.arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

比如說,需要生成 [0,1,2,3,4,5,6,7,8,9] 這樣的陣列,可以使用:

var arr = torch.arange(start: 0, stop: 10, step: 1);
arr.print(style: TensorStringStyle.Numpy);

如果將步長改成 0.5。

var arr = torch.arange(start: 0, stop: 10, step: 0.5);
[0.0000, 0.5000, 1.0000, 1.5000, 2.0000, 2.5000, 3.0000, 3.5000, 4.0000,
        4.5000, 5.0000, 5.5000, 6.0000, 6.5000, 7.0000, 7.5000, 8.0000, 8.5000,
        9.0000, 9.5000]

陣列操作和計算


在 Pytorch 中,往往使用 dim(dimension) 參數列示軸,軸就是張量的層數。

有以下陣列:

[[ 1, 2, 3 ], { 4, 5, 6 ]]

如果把 a = [1,2,3]b = [4,5,6],則:

[a,b]

那麼當我們要獲取 a 時,dim(a) = 0dim(b) = 1

var arr = torch.from_array(new[,] { { 1, 2, 3 }, { 4, 5, 6 } });

arr.print(style: TensorStringStyle.Numpy);

// 列印維度
arr.shape.print();

var a = arr[0];
a.print();
var b = arr[1];
b.print();
[[1, 2, 3] [4, 5, 6]]
[2, 3]
[3], type = Int32, device = cpu 1 2 3
[3], type = Int32, device = cpu 4 5 6

這裡我們分兩步理解,由於該陣列是 2*3 陣列,可以使用 .shape.print() 列印出來。

由於第一層有兩個元素,因此可以使用 Tensor[i] 獲取第一層的第 i 個元素,其中 i<2

同理,由於 a、b 的下一層都有 3 個元素,因此第二層 n<3


例如要將陣列的 36 兩個元素取出來。

用 C# 可以這樣寫,但是列印的時候不能選 TensorStringStyle.Numpy ,否則列印不出來。

var arr = torch.from_array(new[,] { { 1, 2, 3 }, { 4, 5, 6 } });

var a = arr[0, 2];
a.print(style: TensorStringStyle.CSharp);
var b = arr[1, 2];
b.print(style: TensorStringStyle.CSharp);

同理,如果陣列有三層,可以這樣獲取 36 兩個元素

var arr = torch.from_array(new[, ,] { { { 1, 2, 3 } }, { { 4, 5, 6 } } });

var a = arr[0, 0, 2];
a.print(style: TensorStringStyle.CSharp);
var b = arr[1, 0, 2];
b.print(style: TensorStringStyle.CSharp);

如果要取出一部分元素,TorchCsharp 可以使用 a[i..j] 的語法擷取,示例如下。

var arr = torch.from_array(new int[] { 1, 2, 3 });
arr = arr[0..2];
arr.print(style: TensorStringStyle.Numpy);
[1, 2]

陣列排序

Pytorch 中有一些排序函式:

sort :沿給定維度按值升序對 input 張量的元素進行排序。

argsort:它是沿指定軸的間接排序,本文不講解。

msort:按值對 input 張量沿其第一維以升序排序。torch.msort(t) 等效於 torch.sort(t, dim=0)


sort 可以降序或升序,引數說明如下:

torch.sort(input, dim=-1, descending=False, stable=False, *, out=None)
  • input (張量) – 輸入張量。
  • dim (int,可選) – 要排序的維度
  • descending (bool,可選) – 控制排序順序(升序或降序)
  • stable (boo,可選) – 使排序例程穩定,從而保證等效元素的順序得以保留。

示例:

var arr = torch.arange(start: 0, stop: 10, step: 1);

// 或者使用 torch.sort(arr, descending: true)
(torch.Tensor Values, torch.Tensor Indices) a1 = arr.sort(descending: true);

a1.Values.print(style: TensorStringStyle.Numpy);
[9, 8, 7, ... 2, 1, 0]

Values 是排序後的結果,Indices 是排序的規則。


如果陣列結構比較複雜,預設不設定引數時,只有最內層陣列進行排序。如下程式碼所示,有兩層陣列。

var arr = torch.from_array(new[,] { { 4, 6, 5 }, { 8, 9, 7 }, { 3, 2, 1 } });

(torch.Tensor Values, torch.Tensor Indices) a1 = arr.sort();

a1.Values.print(style: TensorStringStyle.Numpy);
a1.Indices.print(style: TensorStringStyle.Numpy);
[[4, 5, 6] [7, 8, 9] [1, 2, 3]]
[[0, 2, 1] [2, 0, 1] [2, 1, 0]]

Indices 會記錄當前元素在以前的排序位置。


當設定 arr.sort(dim: 0); 時,按照第一層排序。

[[3, 2, 1] [4, 6, 5] [8, 9, 7]]
[[2, 2, 2] [0, 0, 0] [1, 1, 1]]

當設定 arr.sort(dim: 1); 時,只有裡面一層排序。

[[4, 5, 6] [7, 8, 9] [1, 2, 3]]
[[0, 2, 1] [2, 0, 1] [2, 1, 0]]

當一個張量的維度比較大時,我們可以這樣逐層排序。

var arr = torch.from_array(new[, ,] { { { 4, 6, 5 }, { 8, 9, 7 }, { 3, 2, 1 } } });

var dimCount = arr.shape.Length;
for (int i = dimCount - 1; i >= 0; i--)
{
    (torch.Tensor Values, torch.Tensor Indices) a1 = arr.sort(dim: i);
    arr = a1.Values;
    arr.print(style: TensorStringStyle.Numpy);
}
[[[1, 2, 3]  [4, 5, 6]  [7, 8, 9]]]

C# 多維陣列沒有 Python 那麼方便,會要求每一層的元素個數必須一致。

例如下面的陣列宣告是對的:

var array = new int[, , ]
{
    {
        { 10, 12, 11},{ 14, 15, 11 }
    },
    {
        { 4, 6, 5 }, { 8, 9, 7 }
    }
};

如果層數元素個數不一致會報錯:

image-20241103203955447


另外要注意,C# 有多維陣列和交錯陣列,下面是交錯陣列的宣告方式,TorchSharp 並不支援。多維陣列是陣列,交錯陣列是陣列的陣列,或陣列的陣列的陣列,要注意區分。

var array = new int[][][]
{
    new int[][]
    {
        new int[] { 1, 2, 3 },
        new int[] { 4, 5, 6 },
        new int[] { 7, 8, 9 }
    },
    new int[][]
    {
        new int[] { 10, 12, 11 },
        new int[] { 14, 15, 13 }
    }
};

image-20241103204339232


陣列運算子

在 PyTorch 中,張量支援許多運算子,下面列舉部分加以說明:


算術運算子

  • +:加法,如 a + b
  • -:減法,如 a - b
  • *:元素級乘法,如 a * b
  • /:元素級除法,如 a / b
  • //:元素級整除,如 a // b ,TorchCsharp 不支援。
  • %:取模運算,如 a % b
  • **:冪運算,如 a ** b,TorchCsharp 不支援,使用 .pow(x) 代替。

邏輯運算子

  • ==:元素級相等比較,如 a == b
  • !=:元素級不等於比較,如 a != b
  • >:元素級大於比較,如 a > b
  • <:元素級小於比較,如 a < b
  • >=:元素級大於等於比較,如 a >= b
  • <=:元素級小於等於比較,如 a <= b

位運算子

  • &:按位與運算,如 a & b
  • |:按位或運算,如 a | b
  • ^:按位異或運算,如 a ^ b
  • ~:按位取反運算,如 ~a
  • <<:按位左移,如 a << b
  • >>:按位右移,如 a >> b

索引和切片

  • [i]:索引運算子,如 a[i]
  • [i:j]:切片運算子,如 a[i:j] ,TorchCsharp 使用 a[i..j] 語法。
  • [i, j]:多維索引運算子,如 a[i, j]

例如張量每個元素的值 *10

var arr = torch.from_array(new int[] { 1, 2, 3 });
arr = arr * 10;
arr.print(style: TensorStringStyle.Numpy);
[10, 20, 30]

此外,還有 Pytorch 還很多函式,後面的章節中會逐漸學習。


相關文章