“ 基於libtorch的深度學習框架,其處理數據的主要基本單位是Tensor張量,我們可以把Tensor張量理解成矩陣,該矩陣的維度可以是1維、2維、3維,或更高維。”
本文我們來總結一下Tensor張量的常用操作。
01
—
打印張量的信息
打印張量的維度信息
要查看張量的維度信息,通常有兩種方式:打印張量的sizes;或者直接調用張量類的print函數:
torch::Tensor b = torch::zeros({ 3, 5 });
cout << b.sizes() << endl; //方式一,只打印維度信息
b.print(); //方式二,除了打印維度信息,數據類型也打印出來
運行結果:
打印張量的內容
torch::Tensor b = torch::zeros({ 3, 5 });
cout << b << endl;
運行結果:
02
—
定義並初始化張量的值
定義一定維度的張量並初始化全部值為0
torch::Tensor b = torch::zeros({ 5, 7 }); //定義5行7列的0值張量
cout << b << endl;
運行結果如下,得到5行7列的張量:
定義一定維度的張量並初始化全部值為1
auto b = torch::ones({ 3,4 }); //定義3行4列的1值張量
cout << b << endl;
運行結果如下,得到3行4列的張量:
定義一定維度的單位張量
單位張量與單位向量是一個概念,即對角線值為1,其余值全部為0:
auto b = torch::eye(5); //定義5*5單位張量
cout << b << endl;
運行結果如下,得到5行5列的單位張量:
定義一定維度的張量並設置初始值
auto b = torch::full({ 3,4 }, 10); //定義3行4列張量,並初始化全部值為0
cout << b << endl;
運行結果如下,得到3行4列的張量:
此外,還可以使用另一個張量的形狀作為模板,定義相同形狀維度的張量,並填充初始值:
auto b = torch::full({ 3,4 }, 10); //定義3行4列的張量b,並填充全部值為10
auto a = torch::full_like(b, 2); //定義與b相同形狀的張量a,並填充初始值2
auto a1 = torch::full_like(b, 2.5); //定義與b相同形狀的張量a,並填充初始值2.5
auto a2 = torch::full_like(b.toType(kFloat), 2.5); //定義與b相同形狀的張量a,並填充初始值2.5
cout << b << endl;
cout << a << endl;
cout << a1 << endl;
cout << a2 << endl;
運行結果如下,我們注意到張量b的數據默認為long int型,那么使用full_like定義張量a、a1時,它們的數據類型也默認為long int型,即使a1填充值為2.5,也被自動截斷為整型數2了。而a2則強行把b轉換為float型,使a2也是float型數據,因此a2可以使用2.5來填充而不被自動截斷為整型數了。
定義n行1列的張量,並指定初始值
auto b = torch::tensor({ 1,2,3,4,5,6,7,8 }); //定義8行1列的張量,並指定初始值
cout << b << endl;
運行結果如下,得到8行1列的張量:
定義一定維度的張量,並使用隨機數初始化
//定義3行4列張量,並使用區間[0, 1)的符合均勻分布的隨機數初始化
auto r = torch::rand({ 3,4 });
cout << r << endl;
//定義5行6列張量,並使用符合標准正態分布(均值為0,方差為1,即高斯白噪聲)的隨機數初始化
r = torch::randn({ 5, 6 });
cout << r << endl;
//定義5行5列張量,並使用區間[0, 10)的整型數初始化
r = torch::randint(0, 10, { 5,5 });
cout << r << endl;
運行結果如下:
使用數組或某一段內存初始化張量
使用數組或某一段內存來初始化張量時,通常調用torch::from_blob函數來實現:
//使用數組來初始化張量內容
int aa[4] = { 3,4,6,7 };
auto aaaaa = torch::from_blob(aa, { 2, 2 }, torch::kInt);
cout << aaaaa << endl;
//使用vector迭代容器來初始化張量內容
vector<float> aaaa = { 3,4,6 };
auto aaa = torch::from_blob(aaaa.data(), { 1, 1, 1, 3 }, torch::kFloat);
cout << aaa << endl;
//使用Opencv的Mat來初始化張量內容,相當於把Mat轉換為Tensor
Mat x = Mat::zeros(5, 5, CV_32FC1);
auto xx = torch::from_blob(x.data, { 1, 1, 5, 5 }, torch::kFloat);
cout << xx << endl;
運行結果如下:
神經網絡的輸入通常為一張單通道灰度圖或一張三通道的彩色圖,如果輸入為Opencv Mat格式的三通道彩色圖,我們需要格外注意數據維度的順序,因為Mat格式的三通道圖像與libtorch Tensor張量的數據維度是不一樣的,前者是[Height, Width, channels],后者是[channels, Height, Width],如果展開成一維向量來看,Opencv Mat存儲RGB圖像的順序為(每個R、G、B像素點交替存儲):
libtorch張量存儲RGB圖像的順序為(依次存儲所有的R、G、B像素點):
因此將Mat格式的三通道圖像轉換為Tensor張量時,我們應該首先把[Height, Width, Channels]的Mat格式數據轉換為[Height, Width, Channels]的Tensor張量,然后再調用Tensor張量的permute函數把數據的維度順序調整為[Channels, Height, Width]即可。
Mat x1 = (Mat_<uchar>(5, 5) << 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25);
Mat x2 = x1.clone();
Mat x3 = x1.clone();
vector<Mat> channels;
channels.push_back(x1.clone());
channels.push_back(x2.clone());
channels.push_back(x3.clone());
Mat x123;
merge(channels, x123); //合並成一張三通道圖像
cout << "[Height, Width, channels]格式的Mat:" << endl;
cout << x123 << endl;
//錯誤維度順序示范
auto x_t = torch::from_blob(x123.data, { 3, 5, 5 }, torch::kByte);
cout << "直接將[Height, Width, channels]格式Mat轉換為的[channels, Height, Width]格式的Tensor,維度不對應:" << endl;
cout << x_t << endl;
//建議的做法,確保Tensor張量的維度順序為[channels, Height, Width]
x_t = torch::from_blob(x123.data, { 5, 5, 3 }, torch::kByte);
cout << "先將[Height, Width, channels]格式Mat轉換為的[Height, Width, channels]格式的Tensor:" << endl;
cout << x_t << endl;
x_t = x_t.permute({ 2, 0, 1 });
cout << "再調整Tensor的維度順序:[Height, Width, channels]-->[channels, Height, Width]:" << endl;
cout << x_t << endl;
運行結果:
此外,調用torch::from_blob函數建立的Tensor張量,與傳入的指針是共用內存的,該張量並沒有重新開辟一段內存,這一點需要注意。如果需要開辟內存,則通過調用clone函數來執行深拷貝:
int aa[4] = { 3,4,6,7 };
auto aaaaa = torch::from_blob(aa, { 2, 2 }, torch::kInt).clone();
03
—
張量的拼接
按列拼接
兩個張量可以按列拼接的前提條件是它們的行數一樣,否則拼接會出錯:
torch::Tensor a1 = torch::rand({ 2,3 }); //2行3列
torch::Tensor a2 = torch::rand({ 2,1 }); //2行1列
torch::Tensor cat_1 = torch::cat({ a1, a2 }, 1); //dim參數為1表示按列拼接
std::cout << a1 << std::endl;
std::cout << a2 << std::endl;
std::cout << cat_1 << std::endl;
運行結果:
按行拼接
兩個張量可以按行拼接的前提條件是它們的列數一樣,否則拼接會出錯:
torch::Tensor a1 = torch::rand({ 2,3 }); //2行3列
torch::Tensor a2 = torch::rand({ 1,3 }); //1行3列
torch::Tensor cat_1 = torch::cat({ a1, a2 }, 0); //dim參數為0表示按行拼接
std::cout << a1 << std::endl;
std::cout << a2 << std::endl;
std::cout << cat_1 << std::endl;
運行結果:
04
—
張量的切片與索引
所謂切片,我們可以把張量理解成一個蛋糕,把蛋糕切成一塊塊的操作就相當於切片。索引也好理解,索引就是一個地址,通過該地址我們可以定位到張量內部的某一個數值或某一部分數值。
下面我們以三維張量[channels, Height, Width]為例說明張量的常見切片、索引操作。注意所有維度的序號均從0開始。
1. 索引操作
對第1維度、第2維度的所有索引,取第3維度索引號范圍i~j的數據
//linspace(1, 75, 75)為取范圍再1~75之間、長度為75的數組,也即1、2、3、...、75
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 }); //start -- end -- length
cout << a << endl;
//對於所有第1維度、第2維度,取第3維度索引號為2的數據
auto bx = a.index({ "...", 2 });
cout << bx << endl;
運行結果:
對第1維度的所有索引,取第2維度索引號為i、第3維度索引號為j的數據
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
auto bx = a.index({ "...", 2, 3 }); //對所有第1維度,取第2維度索引號為2、第3維度索引號為3的數據
cout << bx << endl;
運行結果:
對第1維度的索引號i,取第2維度、第3維度的所有索引號的數據
該操作相當於取channels個Height*Width矩陣中的第i個Height*Width矩陣。
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
//對索引號為2的第1維度,取所有第2維度、第3維度數據
auto bx = a.index({ 2, "..."});
cout << bx << endl;
運行結果:
對第1維度的索引號i、第3維度的索引號j,取第2維度所有索引的數據
該操作相當於取channels個Height*Width矩陣中的第i個Height*Width矩陣的第j列。
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
//對索引號為2的第1維度、索引號為3的第3維度,取所有第2維度數據
auto bx = a.index({ 2, "...", 3 });
cout << bx << endl;
運行結果:
直接指定張量各維度的索引
cout << "**************************" << endl;
Tensor a = torch::linspace(1, 25, 25).reshape({ 5, 5 });
cout << a << endl;
//取第1維度索引號為1、第2維度索引號為2的所有數據
auto b = a.index({ 1, 2 });
cout << b << endl;
cout << "**************************" << endl;
a = torch::linspace(1, 27, 27).reshape({ 3, 3, 3 });
cout << a << endl;
//取第1維度索引號為1、第2維度索引號為2的所有數據
b = a.index({ 1, 2 });
cout << b << endl;
cout << "**************************" << endl;
a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
//取第1維度索引號為1、第2維度索引號為2、第3維度索引號為3的所有數據
b = a.index({ 1, 2, 3 });
cout << b << endl;
運行結果:
通過索引賦值
auto a = torch::linspace(1, 4, 4).reshape({ 2, 2 });
cout << a << endl;
//將第1維度的索引號1、第2維度的索引號1處賦值為100
a.index_put_({ 1, 1 }, 100);
cout << a << endl;
//將第1維度的索引號0、第2維度的索引號0處賦值為101
a.index_put_({ 0, 0 }, 101);
cout << a << endl;
//將第1維度的索引號1、第2維度的索引號0處賦值為102
a.index_put_({ 1, 0 }, 102);
cout << a << endl;
a = torch::linspace(1, 9, 9).reshape({ 3, 3 });
cout << a << endl;
//將第1維度的所有索引號、第2維度的索引號1處賦值為100
a.index_put_({ "...", 1 }, 100);
cout << a << endl;
運行結果:
2. 切片操作
切片操作主要通過調用Slice函數實現。首先我們介紹一下該函數。
Slice函數主要用於對張量的某一維進行切片時,指定切片的開始索引、結束索引,以及切片步長(切片步長默認為1):
Slice(
//開始索引,若設置為None,則從索引0開始
c10::optional<int64_t> start_index = c10::nullopt,
//結束索引。若設置為None,則到最大索引號結束
c10::optional<int64_t> stop_index = c10::nullopt,
//切片步長,默認為1
c10::optional<int64_t> step_index = c10::nullopt
)
對第1維度,第2維度的所有索引號,從[第3維度的索引號i~第3維度的最大索引號]開始切片,默認切片步長為1
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
//從第3維度的索引號1開始切片,一直切到第3維度的最大索引號,也即取第1~第4列
//這里只Slice只設置開始索引,結束索引和切片步長都默認
auto b = a.index({ "...", Slice(1) });
cout << b << endl;
運行結果:
對第1維度,第2維度的所有索引號,從[第3維度的索引號i~第3維度的最大索引號]開始切片,並設定切片步長為2
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
//設定切片步長為2,從第3維度的索引號1開始切片,一直切到第3維度的最大索引號,也即取第1~第4列
auto b = a.index({ "...", Slice(1, None, 2) });
cout << b << endl;
運行結果:
對第1維度,第2維度的所有索引號,從[第3維度的索引號0~第3維度的索引號i]開始切片,默認切片步長為1
注意:切片不包括第3維度的索引號i,比如i為3,則切片索引號0、1、2。
auto a = torch::linspace(1, 75, 75).reshape({ 3, 5, 5 });
cout << a << endl;
//對第1維度,第2維度的所有索引號,從[第3維度的索引號0~第3維度的索引號2]開始切片,默認切片步長為1
auto b = a.index({ "...", Slice({None, 2}) });
cout << b << endl;
運行結果:
好了,本文就總結到這里,下文我們繼續哈~
歡迎掃碼關注本微信公眾號,您的支持是我堅持下去的最大動力~