# 2.2 數據操作
在深度學習中,我們通常會頻繁地對數據進行操作。作為動手學深度學習的基礎,本節將介紹如何對內存中的數據進行操作。
在PyTorch中,`torch.Tensor`是存儲和變換數據的主要工具。如果你之前用過NumPy,你會發現`Tensor`和NumPy的多維數組非常類似。然而,`Tensor`提供GPU計算和自動求梯度等更多功能,這些使`Tensor`更加適合深度學習。
> "tensor"這個單詞一般可譯作“張量”,張量可以看作是一個多維數組。標量可以看作是0維張量,向量可以看作1維張量,矩陣可以看作是二維張量。
## 2.2.1 創建`Tensor`
我們先介紹`Tensor`的最基本功能,即`Tensor`的創建。
首先導入PyTorch:
~~~
?import torch
~~~
然后我們創建一個5x3的未初始化的`Tensor`:
~~~
?x = torch.empty(5, 3)
?print(x)
~~~
輸出:
~~~
?tensor([[ 0.0000e+00, 1.5846e+29, 0.0000e+00],
? ? ? ? [ 1.5846e+29, 5.6052e-45, 0.0000e+00],
? ? ? ? [ 0.0000e+00, 0.0000e+00, 0.0000e+00],
? ? ? ? [ 0.0000e+00, 0.0000e+00, 0.0000e+00],
? ? ? ? [ 0.0000e+00, 1.5846e+29, -2.4336e+02]])
~~~
創建一個5x3的隨機初始化的`Tensor`:
~~~
?x = torch.rand(5, 3)
?print(x)
~~~
輸出:
~~~
?tensor([[0.4963, 0.7682, 0.0885],
? ? ? ? [0.1320, 0.3074, 0.6341],
? ? ? ? [0.4901, 0.8964, 0.4556],
? ? ? ? [0.6323, 0.3489, 0.4017],
? ? ? ? [0.0223, 0.1689, 0.2939]])
~~~
創建一個5x3的long型全0的`Tensor`:
~~~
?x = torch.zeros(5, 3, dtype=torch.long)
?print(x)
~~~
輸出:
~~~
?tensor([[0, 0, 0],
? ? ? ? [0, 0, 0],
? ? ? ? [0, 0, 0],
? ? ? ? [0, 0, 0],
? ? ? ? [0, 0, 0]])
~~~
還可以直接根據數據創建:
~~~
?x = torch.tensor([5.5, 3])
?print(x)
~~~
輸出:
~~~
tensor([5.5000, 3.0000])
~~~
還可以通過現有的`Tensor`來創建,此方法會默認重用輸入`Tensor`的一些屬性,例如數據類型,除非自定義數據類型。
~~~
x = x.new_ones(5, 3, dtype=torch.float64) # 返回的tensor默認具有相同的torch.dtype和torch.device
print(x)
x = torch.randn_like(x, dtype=torch.float) # 指定新的數據類型
print(x)
~~~
輸出:
~~~
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
tensor([[ 0.6035, 0.8110, -0.0451],
[ 0.8797, 1.0482, -0.0445],
[-0.7229, 2.8663, -0.5655],
[ 0.1604, -0.0254, 1.0739],
[ 2.2628, -0.9175, -0.2251]])
~~~
我們可以通過`shape`或者`size()`來獲取`Tensor`的形狀:
~~~
print(x.size())
print(x.shape)
~~~
輸出:
~~~
torch.Size([5, 3])
torch.Size([5, 3])
~~~
> 注意:返回的torch.Size其實就是一個tuple, 支持所有tuple的操作。
還有很多函數可以創建`Tensor`,去翻翻官方API就知道了,下表給了一些常用的作參考。
| 函數 | 功能 |
| --- | --- |
| Tensor(\*sizes) | 基礎構造函數 |
| tensor(data,) | 類似np.array的構造函數 |
| ones(\*sizes) | 全1Tensor |
| zeros(\*sizes) | 全0Tensor |
| eye(\*sizes) | 對角線為1,其他為0 |
| arange(s,e,step | 從s到e,步長為step |
| linspace(s,e,steps) | 從s到e,均勻切分成steps份 |
| rand/randn(\*sizes) | 均勻/標準分布 |
| normal(mean,std)/uniform(from,to) | 正態分布/均勻分布 |
| randperm(m) | 隨機排列 |
這些創建方法都可以在創建的時候指定數據類型dtype和存放device(cpu/gpu)。
## 2.2.2 操作
本小節介紹`Tensor`的各種操作。
### 算術操作
在PyTorch中,同一種操作可能有很多種形式,下面用加法作為例子。
* **加法形式一**
~~~
y = torch.rand(5, 3)
print(x + y)
~~~
* **加法形式二**
~~~
print(torch.add(x, y))
~~~
還可指定輸出:
~~~
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)
~~~
* **加法形式三、inplace**
~~~
# adds x to y
y.add_(x)
print(y)
~~~
> **注:PyTorch操作inplace版本都有后綴"\_", 例如`x.copy_(y), x.t_()`**
以上幾種形式的輸出均為:
~~~
tensor([[ 1.3967, 1.0892, 0.4369],
[ 1.6995, 2.0453, 0.6539],
[-0.1553, 3.7016, -0.3599],
[ 0.7536, 0.0870, 1.2274],
[ 2.5046, -0.1913, 0.4760]])
~~~
### 索引
我們還可以使用類似NumPy的索引操作來訪問`Tensor`的一部分,需要注意的是:**索引出來的結果與原數據共享內存,也即修改一個,另一個會跟著修改。**
~~~
y = x[0, :]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了
~~~
輸出:
~~~
tensor([1.6035, 1.8110, 0.9549])
tensor([1.6035, 1.8110, 0.9549])
~~~
除了常用的索引選擇數據之外,PyTorch還提供了一些高級的選擇函數:
| 函數 | 功能 |
| --- | --- |
| index\_select(input, dim, index) | 在指定維度dim上選取,比如選取某些行、某些列 |
| masked\_select(input, mask) | 例子如上,a\[a>0\],使用ByteTensor進行選取 |
| non\_zero(input) | 非0元素的下標 |
| gather(input, dim, index) | 根據index,在dim維度上選取數據,輸出的size與index一樣 |
這里不詳細介紹,用到了再查官方文檔。
### 改變形狀
用`view()`來改變`Tensor`的形狀:
~~~
y = x.view(15)
z = x.view(-1, 5) # -1所指的維度可以根據其他維度的值推出來
print(x.size(), y.size(), z.size())
~~~
輸出:
~~~
torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])
~~~
**注意`view()`返回的新tensor與源tensor共享內存(其實是同一個tensor),也即更改其中的一個,另外一個也會跟著改變。(顧名思義,view僅僅是改變了對這個張量的觀察角度)**
~~~
x += 1
print(x)
print(y) # 也加了1
~~~
輸出:
~~~
tensor([[1.6035, 1.8110, 0.9549],
[1.8797, 2.0482, 0.9555],
[0.2771, 3.8663, 0.4345],
[1.1604, 0.9746, 2.0739],
[3.2628, 0.0825, 0.7749]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])
~~~
所以如果我們想返回一個真正新的副本(即不共享內存)該怎么辦呢?Pytorch還提供了一個`reshape()`可以改變形狀,但是此函數并不能保證返回的是其拷貝,所以不推薦使用。推薦先用`clone`創造一個副本然后再使用`view`。[參考此處](https://stackoverflow.com/questions/49643225/whats-the-difference-between-reshape-and-view-in-pytorch)
~~~
x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)
~~~
輸出:
~~~
tensor([[ 0.6035, 0.8110, -0.0451],
[ 0.8797, 1.0482, -0.0445],
[-0.7229, 2.8663, -0.5655],
[ 0.1604, -0.0254, 1.0739],
[ 2.2628, -0.9175, -0.2251]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])
~~~
> 使用`clone`還有一個好處是會被記錄在計算圖中,即梯度回傳到副本時也會傳到源`Tensor`。
另外一個常用的函數就是`item()`, 它可以將一個標量`Tensor`轉換成一個Python number:
~~~
x = torch.randn(1)
print(x)
print(x.item())
~~~
輸出:
~~~
tensor([2.3466])
2.3466382026672363
~~~
### 線性代數
另外,PyTorch還支持一些線性函數,這里提一下,免得用起來的時候自己造輪子,具體用法參考官方文檔。如下表所示:
| 函數 | 功能 |
| --- | --- |
| trace | 對角線元素之和(矩陣的跡) |
| diag | 對角線元素 |
| triu/tril | 矩陣的上三角/下三角,可指定偏移量 |
| mm/bmm | 矩陣乘法,batch的矩陣乘法 |
| addmm/addbmm/addmv/addr/badbmm.. | 矩陣運算 |
| t | 轉置 |
| dot/cross | 內積/外積 |
| inverse | 求逆矩陣 |
| svd | 奇異值分解 |
PyTorch中的`Tensor`支持超過一百種操作,包括轉置、索引、切片、數學運算、線性代數、隨機數等等,可參考[官方文檔](https://pytorch.org/docs/stable/tensors.html)。
## 2.2.3 廣播機制
前面我們看到如何對兩個形狀相同的`Tensor`做按元素運算。當對兩個形狀不同的`Tensor`按元素運算時,可能會觸發廣播(broadcasting)機制:先適當復制元素使這兩個`Tensor`形狀相同后再按元素運算。例如:
~~~
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)
~~~
輸出:
~~~
tensor([[1, 2]])
tensor([[1],
[2],
[3]])
tensor([[2, 3],
[3, 4],
[4, 5]])
~~~
由于`x`和`y`分別是1行2列和3行1列的矩陣,如果要計算`x + y`,那么`x`中第一行的2個元素被廣播(復制)到了第二行和第三行,而`y`中第一列的3個元素被廣播(復制)到了第二列。如此,就可以對2個3行2列的矩陣按元素相加。
## 2.2.4 運算的內存開銷
前面說了,索引、`view`是不會開辟新內存的,而像`y = x + y`這樣的運算是會新開內存的,然后將`y`指向新內存。為了演示這一點,我們可以使用Python自帶的`id`函數:如果兩個實例的ID一致,那么它們所對應的內存地址相同;反之則不同。
~~~
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x
print(id(y) == id_before) # False
~~~
如果想指定結果到原來的`y`的內存,我們可以使用前面介紹的索引來進行替換操作。在下面的例子中,我們把`x + y`的結果通過`[:]`寫進`y`對應的內存中。
~~~
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y[:] = y + x
print(id(y) == id_before) # True
~~~
我們還可以使用運算符全名函數中的`out`參數或者自加運算符`+=`(也即`add_()`)達到上述效果,例如`torch.add(x, y, out=y)`和`y += x`(`y.add_(x)`)。
~~~
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
torch.add(x, y, out=y) # y += x, y.add_(x)
print(id(y) == id_before) # True
~~~
## 2.2.5 `Tensor`和NumPy相互轉換
我們很容易用`numpy()`和`from_numpy()`將`Tensor`和NumPy中的數組相互轉換。但是需要注意的一點是: **這兩個函數所產生的的`Tensor`和NumPy中的數組共享相同的內存(所以他們之間的轉換很快),改變其中一個時另一個也會改變!!!**
> 還有一個常用的將NumPy中的array轉換成`Tensor`的方法就是`torch.tensor()`, 需要注意的是,此方法總是會進行數據拷貝(就會消耗更多的時間和空間),所以返回的`Tensor`和原來的數據不再共享內存。
### `Tensor`轉NumPy
使用`numpy()`將`Tensor`轉換成NumPy數組:
~~~
a = torch.ones(5)
b = a.numpy()
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)
~~~
輸出:
~~~
tensor([1., 1., 1., 1., 1.]) [1. 1. 1. 1. 1.]
tensor([2., 2., 2., 2., 2.]) [2. 2. 2. 2. 2.]
tensor([3., 3., 3., 3., 3.]) [3. 3. 3. 3. 3.]
~~~
### NumPy數組轉`Tensor`
使用`from_numpy()`將NumPy數組轉換成`Tensor`:
~~~
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)
~~~
輸出:
~~~
[1. 1. 1. 1. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
[2. 2. 2. 2. 2.] tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
[3. 3. 3. 3. 3.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)
~~~
所有在CPU上的`Tensor`(除了`CharTensor`)都支持與NumPy數組相互轉換。
此外上面提到還有一個常用的方法就是直接用`torch.tensor()`將NumPy數組轉換成`Tensor`,需要注意的是該方法總是會進行數據拷貝,返回的`Tensor`和原來的數據不再共享內存。
~~~
c = torch.tensor(a)
a += 1
print(a, c)
~~~
輸出
~~~
[4. 4. 4. 4. 4.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)
~~~
## 2.2.6 `Tensor` on GPU
用方法`to()`可以將`Tensor`在CPU和GPU(需要硬件支持)之間相互移動。
~~~
# 以下代碼只有在PyTorch GPU版本上才會執行
if torch.cuda.is_available():
device = torch.device("cuda") # GPU
y = torch.ones_like(x, device=device) # 直接創建一個在GPU上的Tensor
x = x.to(device) # 等價于 .to("cuda")
z = x + y
print(z)
print(z.to("cpu", torch.double)) # to()還可以同時更改數據類型
~~~
* * *
> 注: 本文主要參考[PyTorch官方文檔](https://pytorch.org/tutorials/beginner/blitz/tensor_tutorial.html#sphx-glr-beginner-blitz-tensor-tutorial-py)和[此處](https://github.com/chenyuntc/pytorch-book/blob/master/chapter3-Tensor%E5%92%8Cautograd/Tensor.ipynb),與[原書同一節](https://zh.d2l.ai/chapter_prerequisite/ndarray.html)有很大不同。
- Home
- Introduce
- 1.深度學習簡介
- 深度學習簡介
- 2.預備知識
- 2.1環境配置
- 2.2數據操作
- 2.3自動求梯度
- 3.深度學習基礎
- 3.1 線性回歸
- 3.2 線性回歸的從零開始實現
- 3.3 線性回歸的簡潔實現
- 3.4 softmax回歸
- 3.5 圖像分類數據集(Fashion-MINST)
- 3.6 softmax回歸的從零開始實現
- 3.7 softmax回歸的簡潔實現
- 3.8 多層感知機
- 3.9 多層感知機的從零開始實現
- 3.10 多層感知機的簡潔實現
- 3.11 模型選擇、反向傳播和計算圖
- 3.12 權重衰減
- 3.13 丟棄法
- 3.14 正向傳播、反向傳播和計算圖
- 3.15 數值穩定性和模型初始化
- 3.16 實戰kaggle比賽:房價預測
- 4 深度學習計算
- 4.1 模型構造
- 4.2 模型參數的訪問、初始化和共享
- 4.3 模型參數的延后初始化
- 4.4 自定義層
- 4.5 讀取和存儲
- 4.6 GPU計算
- 5 卷積神經網絡
- 5.1 二維卷積層
- 5.2 填充和步幅
- 5.3 多輸入通道和多輸出通道
- 5.4 池化層
- 5.5 卷積神經網絡(LeNet)
- 5.6 深度卷積神經網絡(AlexNet)
- 5.7 使用重復元素的網絡(VGG)
- 5.8 網絡中的網絡(NiN)
- 5.9 含并行連結的網絡(GoogLeNet)
- 5.10 批量歸一化
- 5.11 殘差網絡(ResNet)
- 5.12 稠密連接網絡(DenseNet)
- 6 循環神經網絡
- 6.1 語言模型
- 6.2 循環神經網絡
- 6.3 語言模型數據集(周杰倫專輯歌詞)
- 6.4 循環神經網絡的從零開始實現
- 6.5 循環神經網絡的簡單實現
- 6.6 通過時間反向傳播
- 6.7 門控循環單元(GRU)
- 6.8 長短期記憶(LSTM)
- 6.9 深度循環神經網絡
- 6.10 雙向循環神經網絡
- 7 優化算法
- 7.1 優化與深度學習
- 7.2 梯度下降和隨機梯度下降
- 7.3 小批量隨機梯度下降
- 7.4 動量法
- 7.5 AdaGrad算法
- 7.6 RMSProp算法
- 7.7 AdaDelta
- 7.8 Adam算法
- 8 計算性能
- 8.1 命令式和符號式混合編程
- 8.2 異步計算
- 8.3 自動并行計算
- 8.4 多GPU計算
- 9 計算機視覺
- 9.1 圖像增廣
- 9.2 微調
- 9.3 目標檢測和邊界框
- 9.4 錨框
- 10 自然語言處理
- 10.1 詞嵌入(word2vec)
- 10.2 近似訓練
- 10.3 word2vec實現
- 10.4 子詞嵌入(fastText)
- 10.5 全局向量的詞嵌入(Glove)
- 10.6 求近義詞和類比詞
- 10.7 文本情感分類:使用循環神經網絡
- 10.8 文本情感分類:使用卷積網絡
- 10.9 編碼器--解碼器(seq2seq)
- 10.10 束搜索
- 10.11 注意力機制
- 10.12 機器翻譯