PyTorch--Tensor


PyTorch–Tensor

幾乎所有的深度學習框架背后的設計核心都是張量和計算圖,PyTorch也不例外

一.Tensor的簡介

Tensor,又名張量,可能對這個名詞似曾相識,因它不僅在PyTorch中出現過,它也是Theano、TensorFlow、

Torch和MxNet中重要的數據結構。關於張量的本質不乏深度的剖析,但從工程角度來講,可簡單地認為它就是一個數組,且支持高效的科學計算。它可以是一個數(標量)、一維數組(向量)、二維數組(矩陣)和更高維的數組(高階數據)。Tensor和Numpy的ndarrays類似,但PyTorch的tensor支持GPU加速。

本節將系統講解tensor的使用,力求面面俱到,但不會涉及每個函數。對於更多函數及其用法,讀者可通過在IPython/Notebook中使用函數名加?查看幫助文檔,或查閱PyTorch官方文檔[1]

# Let's begin
from __future__ import print_function
import torch  as t

二.Tensor基礎操作

學習過Numpy的讀者會對本節內容感到非常熟悉,因tensor的接口有意設計成與Numpy類似,以方便用戶使用。但不熟悉Numpy也沒關系,本節內容並不要求先掌握Numpy。

從接口的角度來講,對tensor的操作可分為兩類:

  1. torch.function,如torch.save等。
  2. 另一類是tensor.function,如tensor.view等。

為方便使用,對tensor的大部分操作同時支持這兩類接口,在本書中不做具體區分,如torch.sum (torch.sum(a, b))tensor.sum (a.sum(b))功能等價。

而從存儲的角度來講,對tensor的操作又可分為兩類:

  1. 不會修改自身的數據,如 a.add(b), 加法的結果會返回一個新的tensor。
  2. 會修改自身的數據,如 a.add_(b), 加法的結果仍存儲在a中,a被修改了。

函數名以_結尾的都是inplace方式, 即會修改調用者自己的數據,在實際應用中需加以區分。

1.創建Tensor

在PyTorch中新建tensor的方法有很多,具體如表3-1所示。

表3-1: 常見新建tensor的方法

函數 功能
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??
Type:            Size
String form:     torch.Size([2, 3])
Length:          2
File:            e:\anaconda\envs\mytensorflow\lib\site-packages\torch\__init__.py
Docstring:       <no docstring>
Class docstring:
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.]])

2.常用Tensor操作

通過tensor.view方法可以調整tensor的形狀,但必須保證調整前后元素總數一致。view不會修改自身的數據,返回的新tensor與源tensor共享內存,也即更改其中的一個,另外一個也會跟着改變。在實際應用中可能經常需要添加或減少某一維度,這時候squeezeunsqueeze兩個函數就派上用場了。

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]])

3.索引操作

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)

4.高級索引

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]]])

5.Tensor類型

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)是通用的做法,同時還有floatlonghalf等快捷方法。CPU tensor與GPU tensor之間的互相轉換通過tensor.cudatensor.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??
Docstring: <no docstring>
Type:      builtin_function_or_method
# 恢復之前的默認設置
t.set_default_tensor_type('torch.FloatTensor')

6.逐元素操作

這部分操作會對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)

7.歸並操作

此類操作會使輸出形狀小於輸入形狀,並可以沿着某一維度進行指定操作。如加法sum,既可以計算整個tensor的和,也可以計算tensor中每一行或每一列的和。常用的歸並操作如表3-5所示。

表3-5: 常用歸並操作

函數 功能
mean/sum/median/mode 均值/和/中位數/眾數
norm/dist 范數/距離
std/var 標准差/方差
cumsum/cumprod 累加/累乘

以上大多數函數都有一個參數**dim**,用來指定這些操作是在哪個維度上執行的。關於dim(對應於Numpy中的axis)的解釋眾說紛紜,這里提供一個簡單的記憶方式:

假設輸入的形狀是(m, n, k)

  • 如果指定dim=0,輸出的形狀就是(1, n, k)或者(n, k)
  • 如果指定dim=1,輸出的形狀就是(m, 1, k)或者(m, k)
  • 如果指定dim=2,輸出的形狀就是(m, n, 1)或者(m, n)

size中是否有"1",取決於參數keepdimkeepdim=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]])

8.比較

比較函數中有一些是逐元素比較,操作類似於逐元素操作,還有一些則類似於歸並操作。常用比較函數如表3-6所示。

表3-6: 常用比較函數

函數 功能
gt/lt/ge/le/eq/ne 大於/小於/大於等於/小於等於/等於/不等
topk 最大的k個數
sort 排序
max/min 比較兩個tensor最大最小值

表中第一行的比較操作已經實現了運算符重載,因此可以使用a>=ba>ba!=ba==b,其返回結果是一個ByteTensor,可用來選取元素。max/min這兩個操作比較特殊,以max來說,它有以下三種使用情況:

  • t.max(tensor):返回tensor中最大的一個數
  • t.max(tensor,dim):指定維上最大的數,返回tensor和下標
  • t.max(tensor1, tensor2): 比較兩個tensor相比較大的元素

至於比較一個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.]])

8.線性代數

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

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的廣播法則定義如下:

  • 讓所有輸入數組都向其中shape最長的數組看齊,shape中不足的部分通過在前面加1補齊
  • 兩個數組要么在某一個維度的長度一致,要么其中一個為1,否則不能計算
  • 當輸入數組的某個維度的長度為1時,計算時沿此維度復制擴充成一樣的形狀

PyTorch當前已經支持了自動廣播法則,但是筆者還是建議讀者通過以下兩個函數的組合手動實現廣播法則,這樣更直觀,更不易出錯:

  • unsqueeze或者view:為數據某一維的形狀補1,實現法則1
  • expand或者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)

1.內部結構

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來實現)。

2.其它有關Tensor的話題

這部分的內容不好專門划分一小節,但是筆者認為仍值得讀者注意,故而將其放在這一小節。

持久化

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]])

3.小試牛刀:線性回歸

線性回歸是機器學習入門知識,應用十分廣泛。線性回歸利用數理統計中回歸分析,來確定兩種或兩種以上變量間相互依賴的定量關系的,其表達形式為

為誤差服從均值為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>

output_128_1.png

# 隨機初始化參數
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])

output_129_0.png

2.0185186862945557 3.03572154045105

可見程序已經基本學出w=2、b=3,並且圖中直線和數據已經實現較好的擬合。

雖然上面提到了許多操作,但是只要掌握了這個例子基本上就可以了,其他的知識,讀者日后遇到的時候,可以再看看這部份的內容或者查找對應文檔。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM