幾乎全部的深度學習框架背後的設計核心都是張量和計算圖,PyTorch
也不例外python
Tensor,又名張量,可能對這個名詞似曾相識,因它不只在PyTorch中出現過,它也是Theano、TensorFlow、
Torch和MxNet中重要的數據結構。關於張量的本質不乏深度的剖析,但從工程角度來說,可簡單地認爲它就是一個數組,且支持高效的科學計算。它能夠是一個數(標量)、一維數組(向量)、二維數組(矩陣)和更高維的數組(高階數據)。Tensor和Numpy的ndarrays相似,但PyTorch的tensor支持GPU加速。web
本節將系統講解tensor的使用,力求面面俱到,但不會涉及每一個函數。對於更多函數及其用法,讀者可經過在IPython/Notebook中使用函數名加?
查看幫助文檔,或查閱PyTorch官方文檔[1]。數組
# Let's begin from __future__ import print_function import torch as t
學習過Numpy的讀者會對本節內容感到很是熟悉,因tensor的接口有意設計成與Numpy相似,以方便用戶使用。但不熟悉Numpy也不要緊,本節內容並不要求先掌握Numpy。數據結構
從接口的角度來說,對tensor的操做可分爲兩類:多線程
torch.function
,如torch.save
等。tensor.function
,如tensor.view
等。爲方便使用,對tensor的大部分操做同時支持這兩類接口,在本書中不作具體區分,如torch.sum (torch.sum(a, b))
與tensor.sum (a.sum(b))
功能等價。app
而從存儲的角度來說,對tensor的操做又可分爲兩類:框架
a.add(b)
, 加法的結果會返回一個新的tensor。a.add_(b)
, 加法的結果仍存儲在a中,a被修改了。函數名以_
結尾的都是inplace方式, 即會修改調用者本身的數據,在實際應用中需加以區分。機器學習
在PyTorch中新建tensor的方法有不少,具體如表3-1所示。ide
表3-1: 常見新建tensor的方法svg
函數 | 功能 |
---|---|
Tensor(*sizes) | 基礎構造函數 |
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) | 隨機排列 |
其中使用Tensor
函數新建tensor是最複雜多變的方式,它既能夠接收一個list,並根據list的數據新建tensor,也能根據指定的形狀新建tensor,還能傳入其餘的tensor,下面舉幾個例子。
# 指定tensor的形狀 a = t.Tensor(2, 3) a # 數值取決於內存空間的狀態
tensor([[ 0.0000e+00, 0.0000e+00, -9.9442e-20], [ 7.4549e-43, 0.0000e+00, 0.0000e+00]])
# 用list的數據建立tensor b = t.Tensor([[1,2,3],[4,5,6]]) b
tensor([[1., 2., 3.], [4., 5., 6.]])
b.tolist() # 把tensor轉爲list
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
tensor.size()
返回torch.Size
對象,它是tuple的子類,但其使用方式與tuple略有區別
b_size = b.size() b_size
torch.Size([2, 3])
b.numel() # b中元素總個數,2*3,等價於b.nelement()
6
# 建立一個和b形狀同樣的tensor c = t.Tensor(b_size) # 建立一個元素爲2和3的tensor d = t.Tensor((2, 3)) c, d
(tensor([[-1.0767e-22, 4.5907e-41, 0.0000e+00], [ 0.0000e+00, 0.0000e+00, 0.0000e+00]]), tensor([2., 3.]))
除了tensor.size()
,還能夠利用tensor.shape
直接查看tensor的形狀,tensor.shape
等價於tensor.size()
c.shape
torch.Size([2, 3])
c.shape??
[1;31mType:[0m Size [1;31mString form:[0m torch.Size([2, 3]) [1;31mLength:[0m 2 [1;31mFile:[0m e:\anaconda\envs\mytensorflow\lib\site-packages\torch\__init__.py [1;31mDocstring:[0m <no docstring> [1;31mClass docstring:[0m tuple() -> empty tuple tuple(iterable) -> tuple initialized from iterable's items If the argument is a tuple, the return value is the same object.
須要注意的是,t.Tensor(*sizes)
建立tensor時,系統不會立刻分配空間,只是會計算剩餘的內存是否足夠使用,使用到tensor時纔會分配,而其它操做都是在建立完tensor以後立刻進行空間分配。其它經常使用的建立tensor的方法舉例以下。
t.ones(2, 3)
tensor([[1., 1., 1.], [1., 1., 1.]])
t.zeros(2, 3)
tensor([[0., 0., 0.], [0., 0., 0.]])
t.arange(1, 6, 2)
tensor([1, 3, 5])
t.linspace(1, 10, 3)
tensor([ 1.0000, 5.5000, 10.0000])
t.randn(2, 3)
tensor([[ 1.1269, -1.2356, -0.8736], [ 0.3995, 0.3190, -0.2923]])
t.randperm(5) # 長度爲5的隨機排列
tensor([2, 0, 4, 1, 3])
t.eye(2, 3) # 對角線爲1, 不要求行列數一致
tensor([[1., 0., 0.], [0., 1., 0.]])
經過tensor.view
方法能夠調整tensor的形狀,但必須保證調整先後元素總數一致。view
不會修改自身的數據,返回的新tensor與源tensor共享內存,也即更改其中的一個,另一個也會跟着改變。在實際應用中可能常常須要添加或減小某一維度,這時候squeeze
和unsqueeze
兩個函數就派上用場了。
a = t.arange(0, 6) a.view(2, 3)
tensor([[0, 1, 2], [3, 4, 5]])
b = a.view(-1, 3) # 當某一維爲-1的時候,會自動計算它的大小 b
tensor([[0, 1, 2], [3, 4, 5]])
b.unsqueeze(1) # 注意形狀,在第1維(下標從0開始)上增長「1」
tensor([[[0, 1, 2]], [[3, 4, 5]]])
b.unsqueeze(-2) # -2表示倒數第二個維度
tensor([[[0, 1, 2]], [[3, 4, 5]]])
c = b.view(1, 1, 1, 2, 3) c.squeeze(0) # 壓縮第0維的「1」
tensor([[[[0, 1, 2], [3, 4, 5]]]])
c.squeeze() # 把全部維度爲「1」的壓縮
tensor([[0, 1, 2], [3, 4, 5]])
a[1] = 100 b # a修改,b做爲view以後的,也會跟着修改
tensor([[ 0, 100, 2], [ 3, 4, 5]])
resize
是另外一種可用來調整size
的方法,但與view
不一樣,它能夠修改tensor的大小。若是新大小超過了原大小,會自動分配新的內存空間,而若是新大小小於原大小,則以前的數據依舊會被保存,看一個例子。
b.resize_(1, 3) b
tensor([[ 0, 100, 2]])
b.resize_(3, 3) # 舊的數據依舊保存着,多出的大小會分配新空間 b
tensor([[ 0, 100, 2], [ 3, 4, 5], [2287625790112, 0, 65541]])
Tensor支持與numpy.ndarray相似的索引操做,語法上也相似,下面經過一些例子,講解經常使用的索引操做。如無特殊說明,索引出來的結果與原tensor共享內存,也即修改一個,另外一個會跟着修改。
a = t.randn(3, 4) a
tensor([[-0.5024, -0.6463, -0.2642, -0.5840], [-1.6994, 0.3280, -0.1238, -0.0243], [ 0.4037, -0.7972, 0.0713, 1.6165]])
a[0] # 第0行(下標從0開始)
tensor([-0.5024, -0.6463, -0.2642, -0.5840])
a[:, 0] # 第0列
tensor([-0.5024, -1.6994, 0.4037])
a[0][2] # 第0行第2個元素,等價於a[0, 2]
tensor(-0.2642)
a[0, -1] # 第0行最後一個元素
tensor(-0.5840)
a[:2] # 前兩行
tensor([[-0.5024, -0.6463, -0.2642, -0.5840], [-1.6994, 0.3280, -0.1238, -0.0243]])
a[:2, 0:2] # 前兩行,第0,1列
tensor([[-0.5024, -0.6463], [-1.6994, 0.3280]])
print(a[0:1, :2]) # 第0行,前兩列 print(a[0, :2]) # 注意二者的區別:形狀不一樣
tensor([[-0.5024, -0.6463]]) tensor([-0.5024, -0.6463])
a > 1 # 返回一個ByteTensor
tensor([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1]], dtype=torch.uint8)
a[a>1] # 等價於a.masked_select(a>1) # 選擇結果與原tensor不共享內存空間
tensor([1.6165])
a[t.LongTensor([0,1])] # 第0行和第1行
tensor([[-0.5024, -0.6463, -0.2642, -0.5840], [-1.6994, 0.3280, -0.1238, -0.0243]])
其它經常使用的選擇函數如表3-2所示。
表3-2經常使用的選擇函數
函數 | 功能 |
---|---|
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同樣 |
gather
是一個比較複雜的操做,對一個2維tensor,輸出的每一個元素以下:
out[i][j] = input[index[i][j]][j] # dim=0 out[i][j] = input[i][index[i][j]] # dim=1
三維tensor的gather
操做同理,下面舉幾個例子。
a = t.arange(0, 16).view(4, 4) a
tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11], [12, 13, 14, 15]])
# 選取對角線的元素 index = t.LongTensor([[0,1,2,3]]) a.gather(0, index)
tensor([[ 0, 5, 10, 15]])
# 選取反對角線上的元素 index = t.LongTensor([[3,2,1,0]]).t() a.gather(1, index)
tensor([[ 3], [ 6], [ 9], [12]])
# 選取反對角線上的元素,注意與上面的不一樣 index = t.LongTensor([[3,2,1,0]]) a.gather(0, index)
tensor([[12, 9, 6, 3]])
# 選取兩個對角線上的元素 index = t.LongTensor([[0,1,2,3],[3,2,1,0]]).t() b = a.gather(1, index) b
tensor([[ 0, 3], [ 5, 6], [10, 9], [15, 12]])
與gather
相對應的逆操做是scatter_
,gather
把數據從input中按index取出,而scatter_
是把取出的數據再放回去。注意scatter_
函數是inplace操做。
out = input.gather(dim, index) -->近似逆操做 out = Tensor() out.scatter_(dim, index)
PyTorch在0.2版本中完善了索引操做,目前已經支持絕大多數numpy的高級索引[2]。高級索引能夠當作是普通索引操做的擴展,可是高級索引操做的結果通常不和原始的Tensor貢獻內出。
x = t.arange(0,27).view(3,3,3) x
tensor([[[ 0, 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, 26]]])
x[[1, 2], [1, 2], [2, 0]] # x[1,1,2]和x[2,2,0]
tensor([14, 24])
x[[2, 1, 0], [0], [1]] # x[2,,0,1],x[1,0,1],x[0,0,1]
tensor([19, 10, 1])
x[[0, 2], ...] # x[0] 和 x[2]
tensor([[[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8]], [[18, 19, 20], [21, 22, 23], [24, 25, 26]]])
Tensor有不一樣的數據類型,如表3-3所示,每種類型分別對應有CPU和GPU版本(HalfTensor除外)。默認的tensor是FloatTensor,可經過t.set_default_tensor_type
來修改默認tensor類型(若是默認類型爲GPU tensor,則全部操做都將在GPU上進行)。Tensor的類型對分析內存佔用頗有幫助。例如對於一個size爲(1000, 1000, 1000)的FloatTensor,它有1000*1000*1000=10^9
個元素,每一個元素佔32bit/8 = 4Byte內存,因此共佔大約4GB內存/顯存。HalfTensor是專門爲GPU版本設計的,一樣的元素個數,顯存佔用只有FloatTensor的一半,因此能夠極大緩解GPU顯存不足的問題,但因爲HalfTensor所能表示的數值大小和精度有限[3],因此可能出現溢出等問題。
表3-3: tensor數據類型
數據類型 | CPU tensor | GPU tensor |
---|---|---|
32-bit 浮點 | torch.FloatTensor | torch.cuda.FloatTensor |
64-bit 浮點 | torch.DoubleTensor | torch.cuda.DoubleTensor |
16-bit 半精度浮點 | N/A | torch.cuda.HalfTensor |
8-bit 無符號整形(0~255) | torch.ByteTensor | torch.cuda.ByteTensor |
8-bit 有符號整形(-128~127) | torch.CharTensor | torch.cuda.CharTensor |
16-bit 有符號整形 | torch.ShortTensor | torch.cuda.ShortTensor |
32-bit 有符號整形 | torch.IntTensor | torch.cuda.IntTensor |
64-bit 有符號整形 | torch.LongTensor | torch.cuda.LongTensor |
各數據類型之間能夠互相轉換,type(new_type)
是通用的作法,同時還有float
、long
、half
等快捷方法。CPU tensor與GPU tensor之間的互相轉換經過tensor.cuda
和tensor.cpu
方法實現。Tensor還有一個new
方法,用法與t.Tensor
同樣,會調用該tensor對應類型的構造函數,生成與當前tensor類型一致的tensor。
# 設置默認tensor,注意參數是字符串 t.set_default_tensor_type('torch.FloatTensor')
a = t.Tensor(2,3) a # 如今a是IntTensor
tensor([[2.8025969286e-45, 0.0000000000e+00, 1.1210387715e-44], [0.0000000000e+00, 0.0000000000e+00, 0.0000000000e+00]])
# 把a轉成FloatTensor,等價於b=a.type(t.FloatTensor) b = a.float() b
tensor([[2.8025969286e-45, 0.0000000000e+00, 1.1210387715e-44], [0.0000000000e+00, 0.0000000000e+00, 0.0000000000e+00]])
c = a.type_as(b) c
tensor([[2.8025969286e-45, 0.0000000000e+00, 1.1210387715e-44], [0.0000000000e+00, 0.0000000000e+00, 0.0000000000e+00]])
d = a.new(2,3) # 等價於torch.IntTensor(3,4) d
tensor([[-1.0766777531e-22, 4.5906537691e-41, 0.0000000000e+00], [ 0.0000000000e+00, 0.0000000000e+00, 0.0000000000e+00]])
# 查看函數new的源碼 a.new??
[1;31mDocstring:[0m <no docstring> [1;31mType:[0m builtin_function_or_method
# 恢復以前的默認設置 t.set_default_tensor_type('torch.FloatTensor')
這部分操做會對tensor的每個元素(point-wise,又名element-wise)進行操做,此類操做的輸入與輸出形狀一致。經常使用的操做如表3-4所示。
表3-4: 常見的逐元素操做
函數 | 功能 |
---|---|
abs/sqrt/div/exp/fmod/log/pow… | 絕對值/平方根/除法/指數/求餘/求冪… |
cos/sin/asin/atan2/cosh… | 相關三角函數 |
ceil/round/floor/trunc | 上取整/四捨五入/下取整/只保留整數部分 |
clamp(input, min, max) | 超過min和max部分截斷 |
sigmod/tanh… | 激活函數 |
對於不少操做,例如div、mul、pow、fmod等,PyTorch都實現了運算符重載,因此能夠直接使用運算符。如a ** 2
等價於torch.pow(a,2)
, a * 2
等價於torch.mul(a,2)
。
其中clamp(x, min, max)
的輸出知足如下公式:
clamp
經常使用在某些須要比較大小的地方,如取一個tensor的每一個元素與另外一個數的較大值。
a = t.arange(0, 6).view(2, 3) a=t.LongTensor() t.cos(a)
tensor([], dtype=torch.int64)
a % 3 # 等價於t.fmod(a, 3)
tensor([], dtype=torch.int64)
a ** 2 # 等價於t.pow(a, 2)
tensor([], dtype=torch.int64)
# 取a中的每個元素與3相比較大的一個 (小於3的截斷成3) print(a) t.clamp(a, min=3)
tensor([], dtype=torch.int64) tensor([], dtype=torch.int64)
此類操做會使輸出形狀小於輸入形狀,並能夠沿着某一維度進行指定操做。如加法sum
,既能夠計算整個tensor的和,也能夠計算tensor中每一行或每一列的和。經常使用的歸併操做如表3-5所示。
表3-5: 經常使用歸併操做
函數 | 功能 |
---|---|
mean/sum/median/mode | 均值/和/中位數/衆數 |
norm/dist | 範數/距離 |
std/var | 標準差/方差 |
cumsum/cumprod | 累加/累乘 |
以上大多數函數都有一個參數**dim
**,用來指定這些操做是在哪一個維度上執行的。關於dim(對應於Numpy中的axis)的解釋衆說紛紜,這裏提供一個簡單的記憶方式:
假設輸入的形狀是(m, n, k)
size中是否有"1",取決於參數keepdim
,keepdim=True
會保留維度1
。注意,以上只是經驗總結,並不是全部函數都符合這種形狀變化方式,如cumsum
。
b = t.ones(2, 3) b.sum(dim = 0, keepdim=True)
tensor([[2., 2., 2.]])
# keepdim=False,不保留維度"1",注意形狀 b.sum(dim=0, keepdim=False)
tensor([2., 2., 2.])
b.sum(dim=1)
tensor([3., 3.])
a = t.arange(0, 6).view(2, 3) print(a) a.cumsum(dim=1) # 沿着行累加
tensor([[0, 1, 2], [3, 4, 5]]) tensor([[ 0, 1, 3], [ 3, 7, 12]])
比較函數中有一些是逐元素比較,操做相似於逐元素操做,還有一些則相似於歸併操做。經常使用比較函數如表3-6所示。
表3-6: 經常使用比較函數
函數 | 功能 |
---|---|
gt/lt/ge/le/eq/ne | 大於/小於/大於等於/小於等於/等於/不等 |
topk | 最大的k個數 |
sort | 排序 |
max/min | 比較兩個tensor最大最小值 |
表中第一行的比較操做已經實現了運算符重載,所以可使用a>=b
、a>b
、a!=b
、a==b
,其返回結果是一個ByteTensor
,可用來選取元素。max/min這兩個操做比較特殊,以max來講,它有如下三種使用狀況:
至於比較一個tensor和一個數,可使用clamp函數。下面舉例說明。
a = t.linspace(0, 15, 6).view(2, 3) a
tensor([[ 0., 3., 6.], [ 9., 12., 15.]])
b = t.linspace(15, 0, 6).view(2, 3) b
tensor([[15., 12., 9.], [ 6., 3., 0.]])
a>b
tensor([[0, 0, 0], [1, 1, 1]], dtype=torch.uint8)
a[a>b] # a中大於b的元素
tensor([ 9., 12., 15.])
t.max(a)
tensor(15.)
t.max(b, dim=1) # 第一個返回值的15和6分別表示第0行和第1行最大的元素 # 第二個返回值的0和0表示上述最大的數是該行第0個元素
(tensor([15., 6.]), tensor([0, 0]))
t.max(a,b)
tensor([[15., 12., 9.], [ 9., 12., 15.]])
# 比較a和10較大的元素 t.clamp(a, min=10)
tensor([[10., 10., 10.], [10., 12., 15.]])
PyTorch的線性函數主要封裝了Blas和Lapack,其用法和接口都與之相似。經常使用的線性代數函數如表3-7所示。
表3-7: 經常使用的線性代數函數
函數 | 功能 |
---|---|
trace | 對角線元素之和(矩陣的跡) |
diag | 對角線元素 |
triu/tril | 矩陣的上三角/下三角,可指定偏移量 |
mm/bmm | 矩陣乘法,batch的矩陣乘法 |
addmm/addbmm/addmv/addr/badbmm… | 矩陣運算 |
t | 轉置 |
dot/cross | 內積/外積 |
inverse | 求逆矩陣 |
svd | 奇異值分解 |
具體使用說明請參見官方文檔[4],須要注意的是,矩陣的轉置會致使存儲空間不連續,需調用它的.contiguous
方法將其轉爲連續。
b = a.t() b.is_contiguous()
False
b.contiguous()
tensor([[ 0., 9.], [ 3., 12.], [ 6., 15.]])
Tensor和Numpy數組之間具備很高的類似性,彼此之間的互操做也很是簡單高效。須要注意的是,Numpy和Tensor共享內存。因爲Numpy歷史悠久,支持豐富的操做,因此當遇到Tensor不支持的操做時,可先轉成Numpy數組,處理後再轉回tensor,其轉換開銷很小。
import numpy as np a = np.ones([2, 3]) a
array([[1., 1., 1.], [1., 1., 1.]])
b = t.from_numpy(a) b
tensor([[1., 1., 1.], [1., 1., 1.]], dtype=torch.float64)
b = t.Tensor(a) # 也能夠直接將numpy對象傳入Tensor b
tensor([[1., 1., 1.], [1., 1., 1.]])
a[0, 1]=100 b
tensor([[1., 1., 1.], [1., 1., 1.]])
c = b.numpy() # a, b, c三個對象共享內存 c
array([[1., 1., 1.], [1., 1., 1.]], dtype=float32)
廣播法則(broadcast)是科學運算中常用的一個技巧,它在快速執行向量化的同時不會佔用額外的內存/顯存。
Numpy的廣播法則定義以下:
PyTorch當前已經支持了自動廣播法則,可是筆者仍是建議讀者經過如下兩個函數的組合手動實現廣播法則,這樣更直觀,更不易出錯:
unsqueeze
或者view
:爲數據某一維的形狀補1,實現法則1expand
或者expand_as
,重複數組,實現法則3;該操做不會複製數組,因此不會佔用額外的空間。注意,repeat實現與expand相相似的功能,可是repeat會把相同數據複製多份,所以會佔用額外的空間。
a = t.ones(3, 2) b = t.zeros(2, 3,1)
# 自動廣播法則 # 第一步:a是2維,b是3維,因此先在較小的a前面補1 , # 即:a.unsqueeze(0),a的形狀變成(1,3,2),b的形狀是(2,3,1), # 第二步: a和b在第一維和第三維形狀不同,其中一個爲1 , # 能夠利用廣播法則擴展,兩個形狀都變成了(2,3,2) a+b
tensor([[[1., 1.], [1., 1.], [1., 1.]], [[1., 1.], [1., 1.], [1., 1.]]])
# 手動廣播法則 # 或者 a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2) a.unsqueeze(0).expand(2, 3, 2) + b.expand(2,3,2)
tensor([[[1., 1.], [1., 1.], [1., 1.]], [[1., 1.], [1., 1.], [1., 1.]]])
# expand不會佔用額外空間,只會在須要的時候才擴充,可極大節省內存 e = a.unsqueeze(0).expand(10000000000000, 3,2)
tensor的數據結構如圖3-1所示。tensor分爲頭信息區(Tensor)和存儲區(Storage),信息區主要保存着tensor的形狀(size)、步長(stride)、數據類型(type)等信息,而真正的數據則保存成連續數組。因爲數據動輒成千上萬,所以信息區元素佔用內存較少,主要內存佔用則取決於tensor中元素的數目,也即存儲區的大小。
a = t.arange(0, 6) a.storage()
0 1 2 3 4 5 [torch.LongStorage of size 6]
b = a.view(2, 3) b.storage()
0 1 2 3 4 5 [torch.LongStorage of size 6]
# 一個對象的id值能夠看做它在內存中的地址 # storage的內存地址同樣,便是同一個storage id(b.storage()) == id(a.storage())
True
# a改變,b也隨之改變,由於他們共享storage a[1] = 100 b
tensor([[ 0, 100, 2], [ 3, 4, 5]])
c = a[2:] c.storage()
0 100 2 3 4 5 [torch.LongStorage of size 6]
c.data_ptr(), a.data_ptr() # data_ptr返回tensor首元素的內存地址 # 能夠看出相差8,這是由於2*4=8--相差兩個元素,每一個元素佔4個字節(float)
(2287745950448, 2287745950432)
c[0] = -100 # c[0]的內存地址對應a[2]的內存地址 a
tensor([ 0, 100, -100, 3, 4, 5])
# 下面4個tensor共享storage id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage())
True
a.storage_offset(), c.storage_offset(), d.storage_offset()
(0, 2, 0)
e = b[::2, ::2] # 隔2行/列取一個元素 id(e.storage()) == id(a.storage())
True
b.stride(), e.stride()
((3, 1), (6, 2))
e.is_contiguous()
False
可見絕大多數操做並不修改tensor的數據,而只是修改了tensor的頭信息。這種作法更節省內存,同時提高了處理速度。在使用中須要注意。
此外有些操做會致使tensor不連續,這時需調用tensor.contiguous
方法將它們變成連續的數據,該方法會使數據複製一份,再也不與原來的數據共享storage。
另外讀者能夠思考一下,以前說過的高級索引通常不共享stroage,而普通索引共享storage,這是爲何?(提示:普通索引能夠經過只修改tensor的offset,stride和size,而不修改storage來實現)。
這部分的內容很差專門劃分一小節,可是筆者認爲仍值得讀者注意,故而將其放在這一小節。
Tensor的保存和加載十分的簡單,使用t.save和t.load便可完成相應的功能。在save/load時可指定使用的pickle
模塊,在load時還可將GPU tensor映射到CPU或其它GPU上。
if t.cuda.is_available(): a = a.cuda(1) # 把a轉爲GPU1上的tensor, t.save(a,'a.pth') # 加載爲b, 存儲於GPU1上(由於保存時tensor就在GPU1上) b = t.load('a.pth') # 加載爲c, 存儲於CPU c = t.load('a.pth', map_location=lambda storage, loc: storage) # 加載爲d, 存儲於GPU0上 d = t.load('a.pth', map_location={'cuda:1':'cuda:0'})
向量化計算是一種特殊的並行計算方式,相對於通常程序在同一時間只執行一個操做的方式,它可在同一時間執行多個操做,一般是對不一樣的數據執行一樣的一個或一批指令,或者說把指令應用於一個數組/向量上。向量化可極大提升科學運算的效率,Python自己是一門高級語言,使用很方便,但這也意味着不少操做很低效,尤爲是for
循環。在科學計算程序中應當極力避免使用Python原生的for循環
。
def for_loop_add(x, y): result = [] for i,j in zip(x, y): result.append(i + j) return t.Tensor(result)
x = t.zeros(100) y = t.ones(100) %timeit -n 10 for_loop_add(x, y) %timeit -n 10 x + y
1.77 ms ± 546 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) The slowest run took 4.45 times longer than the fastest. This could mean that an intermediate result is being cached. 13.6 µs ± 9.39 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
可見兩者有超過40倍的速度差距,所以在實際使用中應儘可能調用內建函數(buildin-function),這些函數底層由C/C++實現,能經過執行底層優化實現高效計算。所以在平時寫代碼時,就應養成向量化的思惟習慣。
此外還有如下幾點須要注意:
t.function
都有一個參數out
,這時候產生的結果將保存在out指定tensor之中。t.set_num_threads
能夠設置PyTorch進行CPU多線程並行計算時候所佔用的線程數,這個能夠用來限制PyTorch所佔用的CPU數目。t.set_printoptions
能夠用來設置打印tensor時的數值精度和格式。a = t.arange(0, 20000000) print(a[-1], a[-2]) # 32bit的IntTensor精度有限致使溢出 b = t.LongTensor() t.arange(0, 200000, out=b) # 64bit的LongTensor不會溢出 b[-1],b[-2]
tensor(19999999) tensor(19999998) (tensor(199999), tensor(199998))
a = t.randn(2,3) a
tensor([[ 0.8271, 0.2385, -1.1914], [ 0.0143, -0.7331, -0.7166]])
t.set_printoptions(precision=10) a
tensor([[ 0.8271387815, 0.2384591550, -1.1913520098], [ 0.0142685520, -0.7330791354, -0.7166479230]])
線性迴歸是機器學習入門知識,應用十分普遍。線性迴歸利用數理統計中迴歸分析,來肯定兩種或兩種以上變量間相互依賴的定量關係的,其表達形式爲
爲偏差服從均值爲0的正態分佈。首先讓咱們來確認線性迴歸的損失函數:
而後利用隨機梯度降低法更新參數
來最小化損失函數,最終學得
和b的數值。
import torch as t %matplotlib inline from matplotlib import pyplot as plt from IPython import display
# 設置隨機數種子,保證在不一樣電腦上運行時下面的輸出一致 t.manual_seed(1000) def get_fake_data(batch_size=8): ''' 產生隨機數據:y=x*2+3,加上了一些噪聲''' x = t.rand(batch_size, 1) * 20 y = x * 2 + (1 + t.randn(batch_size, 1))*3 return x, y
# 來看看產生的x-y分佈 x, y = get_fake_data() plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())
<matplotlib.collections.PathCollection at 0x214ad3f4f60>
# 隨機初始化參數 w = t.rand(1, 1) b = t.zeros(1, 1) lr =0.001 # 學習率 for ii in range(20000): x, y = get_fake_data() # forward:計算loss y_pred = x.mm(w) + b.expand_as(y) # x@W等價於x.mm(w);for python3 only loss = 0.5 * (y_pred - y) ** 2 # 均方偏差 loss = loss.sum() # backward:手動計算梯度 dloss = 1 dy_pred = dloss * (y_pred - y) dw = x.t().mm(dy_pred) db = dy_pred.sum() # 更新參數 w.sub_(lr * dw) b.sub_(lr * db) if ii%1000 ==0: # 畫圖 display.clear_output(wait=True) x = t.arange(0, 20).view(-1, 1) y = x.mm(w) + b.expand_as(x) plt.plot(x.numpy(), y.numpy()) # predicted x2, y2 = get_fake_data(batch_size=20) plt.scatter(x2.numpy(), y2.numpy()) # true data plt.xlim(0, 20) plt.ylim(0, 41) plt.show() plt.pause(0.5) print(w.squeeze()[0], b.squeeze()[0])
2.0185186862945557 3.03572154045105
可見程序已經基本學出w=二、b=3,而且圖中直線和數據已經實現較好的擬合。
雖然上面提到了許多操做,可是隻要掌握了這個例子基本上就能夠了,其餘的知識,讀者往後遇到的時候,能夠再看看這部份的內容或者查找對應文檔。