【小白學PyTorch】9 tensor數據結構與存儲結構

文章來自微信公衆號【機器學習煉丹術】。
上一節課,講解了MNIST圖像分類的一個小實戰,現在我們繼續深入學習一下pytorch的一些有的沒的的小知識來作爲只是儲備。

參考目錄:
@

1 pytorch數據結構

1.1 默認整數與浮點數

【pytorch默認的整數是int64】

pytorch的默認整數是用64個比特存儲,也就是8個字節(Byte)存儲的。

【pytorch默認的浮點數是float32】

pytorch的默認浮點數是用32個比特存儲,也就是4個字節(Byte)存儲的。

import torch
import numpy as np
#----------------------
print('torch的浮點數與整數的默認數據類型')
a = torch.tensor([1,2,3])
b = torch.tensor([1.,2.,3.])
print(a,a.dtype)
print(b,b.dtype)

輸出:

torch的浮點數與整數的默認數據類型
tensor([1, 2, 3]) torch.int64
tensor([1., 2., 3.]) torch.float32

1.2 dtype修改變量類型

print('torch的浮點數與整數的默認數據類型')
a = torch.tensor([1,2,3],dtype=torch.int8)
b = torch.tensor([1.,2.,3.],dtype = torch.float64)
print(a,a.dtype)
print(b,b.dtype)

輸出結果:

torch的浮點數與整數的默認數據類型
tensor([1, 2, 3], dtype=torch.int8) torch.int8
tensor([1., 2., 3.], dtype=torch.float64) torch.float64

1.3 變量類型有哪些

張量的數據類型其實和numpy.array基本一一對應,除了不支持str,主要有下面幾種形式:

torch.float64 # 等同於(torch.double)
torch.float32 # 默認,FloatTensor
torch.float16
torch.int64   # 等同於torch.long
torch.int32   # 默認
torch.int16
torch.int8
torch.uint8   # 二進制碼,表示0-255
torch.bool

在創建變量的時候,想要創建指定的變量類型,上文中提到了用dtype關鍵字來控制,但是我個人更喜歡使用特定的構造函數:

print('torch的構造函數')
a = torch.IntTensor([1,2,3])
b = torch.LongTensor([1,2,3])
c = torch.FloatTensor([1,2,3])
d = torch.DoubleTensor([1,2,3])
e = torch.tensor([1,2,3])
f = torch.tensor([1.,2.,3.])
print(a.dtype)
print(b.dtype)
print(c.dtype)
print(d.dtype)
print(e.dtype)
print(f.dtype)

輸出結果:

torch的構造函數
torch.int32
torch.int64
torch.float32
torch.float64
torch.int64
torch.float32

因此我們可以得到結果:

  • torch.IntTensor對應torch.int32
  • torch.LongTensor對應torch.int64,LongTensor常用在深度學習中的標籤值 ,比方說分類任務中的類別標籤0,1,2,3等,要求用ing64的數據類型;
  • torch.FloatTensor對應torch.float32FloatTensor常用做深度學習中可學習參數或者輸入數據的類型
  • torch.DoubleTensor對應torch.float64
  • torch.tensor則有一個推斷的能力,加入輸入的數據是整數,則默認int64,相當於LongTensor;假如輸入數據是浮點數,則默認float32,相當於FLoatTensor。剛好對應深度學習中的標籤和參數的數據類型,所以一般情況下,直接使用tensor就可以了,但是假如出現報錯的時候,也要學會使用dtype或者構造函數來確保數據類型的匹配

1.4 數據類型轉換

【使用torch.float()方法】

print('數據類型轉換')
a = torch.tensor([1,2,3])
b = a.float()
c = a.double()
d = a.long()
print(b.dtype)
print(c.dtype)
print(d.dtype)
>>> 數據類型轉換
>>> torch.float32
>>> torch.float64
>>> torch.int64

我個人比較習慣這個的方法。

【使用type方法】

b = a.type(torch.float32)
c = a.type(torch.float64)
d = a.type(torch.int64)
print(b.dtype) # torch.float32
print(c.dtype) # torch.float64
print(d.dtype) # torch.int64

2 torch vs numpy

PyTorch是一個python包,目的是加入深度學習應用, torch基本上是實現了numpy的大部分必要的功能,並且tensor是可以利用GPU進行加速訓練的。

2.1 兩者轉換

轉換時非常非常簡單的:

import torch
import numpy as np

a = np.array([1.,2.,3.])
b = torch.tensor(a)
c = b.numpy()
print(a)
print(b)
print(c)

輸出結果:

[1. 2. 3.]
tensor([1., 2., 3.], dtype=torch.float64)
[1. 2. 3.]

下面的內容就變得有點意思了,是內存複製相關的。假如a和b兩個變量共享同一個內存,那麼改變a的話,b也會跟着改變;如果a和b變量的內存複製了,那麼兩者是兩個內存,所以改變a是不會改變b的。下面是講解numpy和torch互相轉換的時候,什麼情況是共享內存,什麼情況下是內存複製 (其實這個問題,也就是做個瞭解罷了,無用的小知識)

【Tensor()轉換】
當numpy的數據類型和torch的數據類型相同時,共享內存;不同的時候,內存複製

print('numpy 和torch互相轉換1')
a = np.array([1,2,3],dtype=np.float64)
b = torch.Tensor(a)
b[0] = 999
print('共享內存' if a[0]==b[0] else '不共享內存')
>>> 不共享內存

因爲np.float64和torch.float32數據類型不同

print('numpy 和torch互相轉換2')
a = np.array([1,2,3],dtype=np.float32)
b = torch.Tensor(a)
b[0] = 999
print('共享內存' if a[0]==b[0] else '不共享內存')
>>> 共享內存

因爲np.float32和torch.float32數據類型相同

【from_numpy()轉換】

print('from_numpy()')
a = np.array([1,2,3],dtype=np.float64)
b = torch.from_numpy(a)
b[0] = 999
print('共享內存' if a[0]==b[0] else '不共享內存')
>>> 共享內存
a = np.array([1,2,3],dtype=np.float32)
b = torch.from_numpy(a)
b[0] = 999
print('共享內存' if a[0]==b[0] else '不共享內存')
>>> 共享內存

如果你使用from_numpy()的時候,不管是什麼類型,都是共享內存的。

【tensor()轉換】

更常用的是這個tensor(),注意看T的大小寫, 如果使用的是tensor方法,那麼不管輸入類型是什麼,torch.tensor都會進行數據拷貝,不共享內存。

【.numpy()】
tensor轉成numpy的時候,.numpy方法是內存共享的哦。如果想改成內存拷貝的話,可以使用.numpy().copy()就不共享內存了。或者使用.clone().numpy()也可以實現同樣的效果。clone是tensor的方法,copy是numpy的方法。

【總結】

記不清的話,就記住,tensor()數據拷貝了,.numpy()共享內存就行了。

2.2 兩者區別

【命名】

雖然PyTorch實現了Numpy的很多功能,但是相同的功能卻有着不同的命名方式,這讓使用者迷惑。

例如創建隨機張量的時候:

print('命名規則')
a = torch.rand(2,3,4)
b = np.random.rand(2,3,4)

【張量重塑】

這部分會放在下一章節詳細說明~

3 張量

  • 標量:數據是一個數字
  • 向量:數據是一串數字,也是一維張量
  • 矩陣:數據二維數組,也是二維張量
  • 張量:數據的維度超過2的時候,就叫多維張量

3.1 張量修改尺寸

  • pytorch常用reshape和view
  • numpy用resize和reshape
  • pytorch也有resize但是不常用

【reshape和view共享內存(常用)】

a = torch.arange(0,6)
b = a.reshape((2,3))
print(b)
c = a.view((2,3))
print(c)
a[0] = 999
print(b)
print(c)

輸出結果:

tensor([[0, 1, 2],
        [3, 4, 5]])
tensor([[0, 1, 2],
        [3, 4, 5]])
tensor([[999,   1,   2],
        [  3,   4,   5]])
tensor([[999,   1,   2],
        [  3,   4,   5]])

上面的a,b,c三個變量其實是共享同一個內存,遷一而動全身。而且要求遵旨規則:原始數據有6個元素,所以可以修改成\(2\times 3\)的形式,但是無法修改成\(2\times 4\)的形式 ,我們來試試:

a = torch.arange(0,6)
b = a.reshape((2,4))

會拋出這樣的錯誤:

【torch的resize_(不常用)】

但是pytorch有一個不常用的函數(對我來說用的不多),resize,這個方法可以不遵守這個規則:

a = torch.arange(0,6)
a.resize_(2,4)
print(a)

輸出結果爲:

自動的補充了兩個元素。雖然不知道這個函數有什麼意義。。。。。。

這裏可以看到函數resize後面有一個_,這個表示inplace=True的意思,當有這個_或者參數inplace的時候,就是表示所作的修改是在原來的數據變量上完成的,也就不需要賦值給新的變量了。

【numpy的resize與reshape(常用)】

import numpy as np
a = np.arange(0,6)
a.resize(2,3)
print(a)
import numpy as np
a = np.arange(0,6)
b = a.reshape(2,3)
print(b)

兩個代碼塊的輸出都是下面的,區別在於numpy的resize是沒有返回值的,相當於inplace=True了,直接在原變量的進行修改,而reshape是有返回值的,不在原變量上修改(但是呢reshape是共享內存的):

[[0 1 2]
 [3 4 5]]

3.2 張量內存存儲結構

tensor的數據結構包含兩個部分:

  • 頭信息區Tensor:保存張量的形狀size,步長stride,數據類型等信息
  • 存儲區Storage:保存真正的數據

頭信息區Tensor的佔用內存較小,主要的佔用內存是Storate。

每一個tensor都有着對應的storage,一般不同的tensor的頭信息可能不同,但是卻可能使用相同的storage。(這裏就是之前共享內存的view、reshape方法,雖然頭信息的張量形狀size發生了改變,但是其實存儲的數據都是同一個storage)

3.3 存儲區

我們來查看一個tensor的存儲區:

import torch
a = torch.arange(0,6)
print(a.storage())

輸出爲:

 0
 1
 2
 3
 4
 5
[torch.LongStorage of size 6]

然後對tensor變量做一個view的變換:

b = a.view(2,3)

這個b.storage()輸出出來時和a.storate(),相同的,這也是爲什麼view變換是內存共享的了。

# id()是獲取對象的內存地址
print(id(a)==id(b)) # False
print(id(a.storage)==id(b.storage)) # True

可以發現,其實a和b雖然存儲區是相同的,但是其實a和b整體式不同的。自然,這個不同就不同在頭信息區,應該是尺寸size改變了。這也就是頭信息區不同,但是存儲區相同,從而節省大量內存

我們更進一步,假設對tensor切片了,那麼切片後的數據是否共享內存,切片後的數據的storage是什麼樣子的呢?

print('研究tensor的切片')
a = torch.arange(0,6)
b = a[2]
print(id(a.storage)==id(b.storage))

輸出結果爲:

>>> True

沒錯,就算切片之後,兩個tensor依然使用同一個存儲區,所以相比也是共享內存的,修改一個另一個也會變化。

#.data_ptr(),返回tensor首個元素的內存地址。
print(a.data_ptr(),b.data_ptr())
print(b.data_ptr()-a.data_ptr())

輸出爲:

2080207827328 2080207827344
16

這是因爲b的第一個元素和a的第一個元素內存地址相差了16個字節,因爲默認的tesnor是int64,也就是8個字節一個元素,所以這裏相差了2個整形元素

3.4 頭信息區

依然是上面那兩個tensor變量,a和b

a = torch.arange(0,6)
b = a.view(2,3)
print(a.stride(),b.stride())

輸出爲:

(1,) (3, 1)

變量a是一維數組,並且就是[0,1,2,3,4,5],所以步長stride是1;而b是二維數組,是[[0,1,2],[3,4,5]],所以就是先3個3個分成第一維度的,然後再1個1個的作爲第二維度。

由此可見,絕大多數操作並不修改 tensor 的數據,只是修改了 tensor 的頭信息,這種做法更節省內存,同時提升了處理速度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章