總之,兩者都是用來重塑tensor的shape的。view只適合對滿足連續(xù)性條件(contiguous)的tensor進行操作,而reshape同時還可以對不滿足連續(xù)性條件的tensor進行操作,具有更好的魯棒性。view能干的reshape都能干,如果view不能干就可以用reshape來處理。別看目錄挺多,但內(nèi)容很細(xì)呀~其實原理并不難啦~我們開始吧~
目錄
一、PyTorch中tensor的存儲方式
1、PyTorch張量存儲的底層原理
2、PyTorch張量的步長(stride)屬性
二、對“視圖(view)”字眼的理解
三、view() 和reshape() 的比較
1、對 torch.Tensor.view() 的理解
2、對 torch.reshape() 的理解
四、總結(jié)
一、PyTorch中tensor的存儲方式
想要深入理解view與reshape的區(qū)別,首先要理解一些有關(guān)PyTorch張量存儲的底層原理,比如tensor的頭信息區(qū)(Tensor)和存儲區(qū) (Storage)以及tensor的步長Stride。不用慌,這部分的原理其實很簡單的(^-^)!
1、PyTorch張量存儲的底層原理
tensor數(shù)據(jù)采用頭信息區(qū)(Tensor)和存儲區(qū) (Storage)分開存儲的形式,如圖1所示。變量名以及其存儲的數(shù)據(jù)是分為兩個區(qū)域分別存儲的。比如,我們定義并初始化一個tensor,tensor名為A,A的形狀size、步長stride、數(shù)據(jù)的索引等信息都存儲在頭信息區(qū),而A所存儲的真實數(shù)據(jù)則存儲在存儲區(qū)。另外,如果我們對A進行截取、轉(zhuǎn)置或修改等操作后賦值給B,則B的數(shù)據(jù)共享A的存儲區(qū),存儲區(qū)的數(shù)據(jù)數(shù)量沒變,變化的只是B的頭信息區(qū)對數(shù)據(jù)的索引方式 。
圖1 Torch中Tensor的存儲結(jié)構(gòu)
舉個例子:
a = torch.arange(5 ) # 初始化張量 a 為 [0, 1, 2, 3, 4]
b = a[2 :] # 截取張量a的部分值并賦值給b,b其實只是改變了a對數(shù)據(jù)的索引方式
print('id of storage of a:' , id(a.storage)) # 打印a的存儲區(qū)地址
print('id of storage of b:' , id(b.storage)) # 打印b的存儲區(qū)地址,可以發(fā)現(xiàn)兩者是共用存儲區(qū)
print('==================================================================' )
b[1 ] = 0 # 修改b中索引為1,即a中索引為3的數(shù)據(jù)為0
print('id of storage of a:' , id(a.storage)) # 打印a的存儲區(qū)地址,可以發(fā)現(xiàn)a的相應(yīng)位置的值也跟著改變,說明兩者是共用存儲區(qū)
print('id of storage of b:' , id(b.storage)) # 打印b的存儲區(qū)地址
a: tensor([0 , 1 , 2 , 3 , 4 ])
id of storage of a: 140424434241200
id of storage of b: 140424434241200
==================================================================
a: tensor([0 , 1 , 2 , 0 , 4 ])
id of storage of a: 140424434241200
id of storage of b: 140424434241200
2、PyTorch張量的步長(stride)屬性
torch的tensor也是有步長屬性的,說起stride屬性是不是很耳熟?是的,卷積神經(jīng)網(wǎng)絡(luò)中卷積核對特征圖的卷積操作也是有stride屬性的,但這兩個stride可完全不是一個意思哦。tensor的步長可以理解為從索引中的一個維度跨到下一個維度中間的跨度 。為方便理解,就直接用圖1說明了,您細(xì)細(xì)品(^-^):
圖2 對張量的stride屬性的理解
舉個例子:
a = torch.arange(6 ).reshape(2 , 3 ) # 初始化張量 a
b = torch.arange(6 ).view(3 , 2 ) # 初始化張量 b
print('stride of a:' , a.stride()) # 打印a的stride
print('stride of b:' , b.stride()) # 打印b的stride
二、對“視圖(view)”字眼的理解
視圖是數(shù)據(jù)的一個別稱或引用,通過該別稱或引用亦便可訪問、操作原有數(shù)據(jù),但原有數(shù)據(jù)不會產(chǎn)生拷貝。如果我們對視圖進行修改,它會影響到原始數(shù)據(jù),物理內(nèi)存在同一位置,這樣避免了重新創(chuàng)建張量的高內(nèi)存開銷。由上面介紹的PyTorch的張量存儲方式可以理解為:對張量的大部分操作就是視圖操作!
與之對應(yīng)的概念就是副本。副本是一個數(shù)據(jù)的完整的拷貝,如果我們對副本進行修改,它不會影響到原始數(shù)據(jù),物理內(nèi)存不在同一位置。
有關(guān)視圖與副本,在NumPy中也有著重要的應(yīng)用。可參考這里 。
三、view() 和reshape() 的比較
1、對 torch.Tensor.view() 的理解
定義:
view(*shape) → Tensor
作用: 類似于reshape,將tensor轉(zhuǎn)換為指定的shape,原始的data不改變。返回的tensor與原始的tensor共享存儲區(qū)。返回的tensor的size和stride必須與原始的tensor兼容。每個新的tensor的維度必須是原始維度的子空間,或滿足以下連續(xù)條件:
式1 張量連續(xù)性條件
否則需要先使用contiguous() 方法將原始tensor轉(zhuǎn)換為滿足連續(xù)條件的tensor,然后就可以使用view方法進行shape變換了?;蛘咧苯邮褂胷eshape方法進行維度變換,但這種方法變換后的tensor就不是與原始tensor共享內(nèi)存了,而是被重新開辟了一個空間。
如何理解tensor是否滿足連續(xù)條件 吶?下面通過一系列例子來慢慢理解下:
首先,我們初始化一個張量 a ,并查看其stride、storage等屬性:
a = torch.arange(9 ).reshape(3 , 3 ) # 初始化張量a
print('struct of a:\n' , a)
print('size of a:' , a.size()) # 查看a的shape
print('stride of a:' , a.stride()) # 查看a的stride
size of a: torch.Size([3 , 3 ])
stride of a: (3 , 1 ) # 注:滿足連續(xù)性條件
把上面的結(jié)果帶入式1,可以發(fā)現(xiàn)滿足tensor連續(xù)性條件。
我們再看進一步處理——對a進行轉(zhuǎn)置后的結(jié)果:
a = torch.arange(9 ).reshape(3 , 3 ) # 初始化張量a
b = a.permute(1 , 0 ) # 對a進行轉(zhuǎn)置
print('struct of b:\n' , b)
print('size of b:' , b.size()) # 查看b的shape
print('stride of b:' , b.stride()) # 查看b的stride
size of b: torch.Size([3 , 3 ])
stride of b: (1 , 3 ) # 注:此時不滿足連續(xù)性條件
將a轉(zhuǎn)置后再看最后的輸出結(jié)果,帶入到式1中,是不是發(fā)現(xiàn)等式不成立了?所以此時就不滿足tensor連續(xù)的條件了。這是為什么那?我們接著往下看:
首先,輸出a和b的存儲區(qū)來看一下有沒有什么不同:
a = torch.arange(9 ).reshape(3 , 3 ) # 初始化張量a
print('id of storage of a: ' , id(a.storage)) # 查看a的storage區(qū)的地址
print('storage of a: \n' , a.storage()) # 查看a的storage區(qū)的數(shù)據(jù)存放形式
b = a.permute(1 , 0 ) # 轉(zhuǎn)置
print('id of storage of b: ' , id(b.storage)) # 查看b的storage區(qū)的地址
print('storage of b: \n' , b.storage()) # 查看b的storage區(qū)的數(shù)據(jù)存放形式
id of storage of a: 1977594687560
[torch.LongStorage of size 9 ]
id of storage of b: 1977594687560
[torch.LongStorage of size 9 ]
由結(jié)果可以看出,張量a、b仍然共用存儲區(qū),并且存儲區(qū)數(shù)據(jù)存放的順序沒有變化,這也充分說明了b與a共用存儲區(qū),b只是改變了數(shù)據(jù)的索引方式。那么為什么b就不符合連續(xù)性條件了吶(T-T)?其實原因很簡單,我們結(jié)合圖3來解釋下:
圖3 對張量連續(xù)性條件的理解
轉(zhuǎn)置后的tensor只是對storage區(qū)數(shù)據(jù)索引方式的重映射,但原始的存放方式并沒有變化.因此,這時再看tensor b的stride,從b第一行的元素1到第二行的元素2,顯然在索引方式上已經(jīng)不是原來+1了,而是變成了新的+3了,你在仔細(xì)琢磨琢磨是不是這樣的(^-^)。所以這時候就不能用view來對b進行shape的改變了,不然就報錯咯,不信你看下面;
a = torch.arange(9 ).reshape(3 , 3 ) # 初始化張量a
print('============================================' )
b = a.permute(1 , 0 ) # 轉(zhuǎn)置
tensor([0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ])
============================================
Traceback (most recent call last):
File "此處打碼" , line 23 , in <module>
RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
但是嘛,上有政策下有對策,這種情況下,直接用view不行,那我就先用contiguous() 方法將原始tensor轉(zhuǎn)換為滿足連續(xù)條件的tensor,在使用view進行shape變換,值得注意的是,這樣的原理是contiguous() 方法開辟了一個新的存儲區(qū)給b,并改變了b原始存儲區(qū)數(shù)據(jù)的存放順序! 同樣的例子:
a = torch.arange(9 ).reshape(3 , 3 ) # 初始化張量a
print('storage of a:\n' , a.storage()) # 查看a的stride
print('+++++++++++++++++++++++++++++++++++++++++++++++++' )
b = a.permute(1 , 0 ).contiguous() # 轉(zhuǎn)置,并轉(zhuǎn)換為符合連續(xù)性條件的tensor
print('size of b:' , b.size()) # 查看b的shape
print('stride of b:' , b.stride()) # 查看b的stride
print('viewd b:\n' , b.view(9 )) # 對b進行view操作,并打印結(jié)果
print('+++++++++++++++++++++++++++++++++++++++++++++++++' )
print('storage of a:\n' , a.storage()) # 查看a的存儲空間
print('storage of b:\n' , b.storage()) # 查看b的存儲空間
[torch.LongStorage of size 9 ]
+++++++++++++++++++++++++++++++++++++++++++++++++
size of b: torch.Size([3 , 3 ]) # 注意:這時的b就滿足連續(xù)性條件了
tensor([0 , 3 , 6 , 1 , 4 , 7 , 2 , 5 , 8 ])
+++++++++++++++++++++++++++++++++++++++++++++++++
storage of a: # 注意:這時b的存儲區(qū)變化了,a的還沒變
0 # 這說明contiguous方法為b另外開辟了存儲區(qū)
[torch.LongStorage of size 9 ]
[torch.LongStorage of size 9 ]
2、對 torch.reshape() 的理解
定義:
torch.reshape(input, shape) → Tensor
作用: 與view方法類似,將輸入tensor轉(zhuǎn)換為新的shape格式。
但是reshape方法更強大,可以認(rèn)為a.reshape = a.view() + a. contiguous().view() 。
即:在滿足tensor連續(xù)性條件時,a.reshape 返回的結(jié)果與a.view() 相同,否則返回的結(jié)果與a. contiguous().view() 相同。
不信你就看人家官方的解釋嘛,您在細(xì)細(xì)品:
關(guān)于兩者區(qū)別,還可以參考這個鏈接:https:///questions/49643225/whats-the-difference-between-reshape-and-view-in-pytorch
四、總結(jié)
torch的view()與reshape()方法都可以用來重塑tensor的shape,區(qū)別就是使用的條件不一樣。view()方法只適用于滿足連續(xù)性條件的tensor,并且該操作不會開辟新的內(nèi)存空間,只是產(chǎn)生了對原存儲空間的一個新別稱和引用,返回值是視圖。而reshape()方法的返回值既可以是視圖,也可以是副本,當(dāng)滿足連續(xù)性條件時返回view,否則返回副本[ 此時等價于先調(diào)用contiguous()方法在使用view() ]。因此當(dāng)不確能否使用view時,可以使用reshape。如果只是想簡單地重塑一個tensor的shape,那么就是用reshape,但是如果需要考慮內(nèi)存的開銷而且要確保重塑后的tensor與之前的tensor共享存儲空間,那就使用view()。
2020.10.23
以上是我個人看了官網(wǎng)的的解釋并實驗得到的結(jié)論,所以有沒有dalao知道為啥沒把view廢除那?是不是還有我不知道的地方
2020.11.14
為什么沒把view廢除那?最近偶然看到了些資料,又想起了這個問題,覺得有以下原因:
1、在PyTorch不同版本的更新過程中,view先于reshape方法出現(xiàn),后來出現(xiàn)了魯棒性更好的reshape方法,但view方法并沒因此廢除。其實不止PyTorch,其他一些框架或語言比如OpenCV也有類似的操作。
2、view的存在可以顯示地表示對這個tensor的操作只能是視圖操作而非拷貝操作。這對于代碼的可讀性以及后續(xù)可能的bug的查找比較友好。
總之,我們沒必要糾結(jié)為啥a能干的b也能干,b還能做a不能干的,a存在還有啥意義的問題。就相當(dāng)于馬云能日賺1個億而我不能,那我存在的意義是啥。。。存在不就是意義嗎?存在即合理,最重要的是我們使用不同的方法可以不同程度上提升效率,何樂而不為?