Torch7入門續集(一)----- 更加深入理解Tensor
續集寫作緣由
好吧,無語了,又開始寫Torch7了。前面寫的Torch入門,貌似都沒什麼人看,可能是該框架用的還是比較小眾,應該大部分人用Caffe,Tensorflow, mxnet之類了吧。無所謂了,主要是貌似我研究的方向作者的程式碼基本上還是Torch啊,沒法子。那我為什麼要寫續集呢?主要是發現以前寫的Torch7學入門專欄還是基本的,入門可能夠了,一些技巧和強大的函式,記錄一下,總有好處。
續集寫作原則
這裡只寫出我認為常見的重要的東西,有一些會和以前寫的有重複,不過無所謂了。有一些預設大家都知道了,也就不怎麼提。簡單一句話,只寫我認為該寫的!
續集寫作目的
- 更加好的掌握Torch已有的強大函式
續集教程指的是除了基本的使用方式外,如何能使用內建的函式完成更多的任務,這才是這篇系列寫作的原因。舉個栗子,比如我要在一個Tensor加入padding,並且是“對稱型”的padding,然後再進行其他操作。然後自己寫了個函式,這裡預設輸入的是三維的Tensor
function ex_tensor(input, pad, method)
-- mirror and zero
assert((pad-1)%2 == 0, 'pad should be odd number!')
local padding = (pad-1)/2
local method = method or 'mirror'
local k = input:size()
local output = torch.Tensor(k[1], k[2]+padding*2, k[3]+padding*2):typeAs(input):zero()
output[{{},{padding+1, -padding-1},{padding+1, -padding-1}}] = input:clone()
if method == 'mirror' then
for i = 1, padding do
output[{{},{i},{padding+1 , -padding-1}}] = output[{{},{padding*2+1-i},{padding+1, -padding-1}}] -- up
output[{{},{-i},{padding+1, -padding-1}}] = output[{{},{-padding*2-1+i},{padding+1, -padding-1}}] --down
output[{{},{padding+1, -padding-1},{i}}] = output[{{},{padding+1, -padding-1},{padding*2+1-i}}] --left
output[{{},{padding+1, -padding-1},{-i}}] = output[{{},{padding+1, -padding-1},{-padding*2-1+i}}] --right
end
for i = 1, padding do
output[{{},{1, padding},{i}}] = output[{{},{1, padding},{padding*2+1-i}}] --left_up
output[{{},{-padding,-1},{i}}] = output[{{},{-padding,-1},{padding*2+1-i}}] --left_down
output[{{},{1, padding},{-i}}] = output[{{},{1, padding},{-padding*2-1+i}}] --right_up
output[{{},{-padding,-1},{-i}}] = output[{{},{-padding, -1},{-padding*2-1+i}}] --right_down
end
else
-- done
end
return output
end
是不是感覺很麻煩。。後來才發現Torch內部 nn.SpatialReplicationPadding 就可以完成這個任務。知道真相的我眼淚掉下來。。
2. 學習他人寫torch的技巧
隨時記錄一些好的技巧,可能以後會用到。
3. 讀N遍文件
這個很重要!這個也是續集的重點,著重讀重要的文件!
再探Tensor
Tensor的內部儲存
Tensor的同一維度的每個元素之間的步長是一樣的,第i個維度的步長為stride(i)。Tensor的首地址可以用storageOffset()來獲得。因此:
x = torch.Tensor(4,5)
s = x:storage()
for i=1,s:size() do -- fill up the Storage
s[i] = i
end
> x -- s is interpreted by x as a 2D matrix
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
[torch.DoubleTensor of dimension 4x5]
由於stride(i)不為1時,該Tensor的記憶體空間就是不連續的。但是最後一個維度是連續的。
x = torch.Tensor(4,5)
i = 0
x:apply(function()
i = i + 1
return i
end)
> x
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
[torch.DoubleTensor of dimension 4x5]
> x:stride()
5
1 -- element in the last dimension are contiguous!
[torch.LongStorage of size 2]
值得注意的是,這種方式是類C的方法,與matlab(類似Fortran)的矩陣是不一樣的。
熟讀下面句子,並背誦。。。
One could say that a Tensor is a particular way of viewing a Storage: a Storage only represents a chunk of memory, while the Tensor interprets this chunk of memory as having dimensions
Tensor其實就是對儲存空間的另一種view!Tensor型別就是在儲存空間中看成是有維度的!!
另外,所有的Tensor操作都是直接在原儲存空間中進行操作!這一切都是通過內部的stride()與storageOffset()來實現的。要想得到一個新的Tensor。用clone()
x = torch.Tensor(5):zero()
> x
0
0
0
0
0
[torch.DoubleTensor of dimension 5]
> x:narrow(1, 2, 3):fill(1) -- narrow() returns a Tensor
-- referencing the same Storage as x
> x
0
1
1
1
0
[torch.Tensor of dimension 5]
y = x:clone()
Tensor構造的三種方式
下面2種比較常用。
-- 最多到4維度
x = torch.Tensor(2,5):fill(3.14)
-- 大於4維度的
x = torch.Tensor(torch.LongStorage({4,4,3,2}))
注意:第二個函式呼叫方式是torch.Tensor(sizes, [strides])
。sizes和[strides]都是LongStorage型別的。並且strides是每一維度中,一個元素到下一個元素之間的步長。並且可以隨意設定!
x = torch.Tensor(torch.LongStorage({4}), torch.LongStorage({0})):zero() -- zeroes the tensor
x[1] = 1 -- all elements point to the same address!
> x
1
1
1
1
可以看到上面令stride為0了,也就是說x雖然有4個單元,但都實際上指向實體地址的同一個單元!讓x[1]=1,整個x都變成1了。厲害了。
另外一個就是通過table進行構造。
> torch.Tensor({{1,2,3,4}, {5,6,7,8}})
1 2 3 4
5 6 7 8
[torch.DoubleTensor of dimension 2x4]
附:torch為了方便大家的使用,內部內部的絕大部分函式都可以用2種方法來進行呼叫—– src:function()或是torch.function(src, …)。也就是說,第二種風格的呼叫是將自身作為第一個引數進行呼叫。然而由於內部還是不太完善,有些函式只能用第一種方法或是第二種方法。比如
--下面兩種是一樣的
x = torch.Tensor(2,3):fill(2)
y = torch.fill(torch.Tensor(2,3),2)
--下面只能由第二種方法
local x = torch.transpose(vecInput, 2,3) --這個會出錯
local y = vecInput:transpose(2,3) --這個正確
這些只能由自己去測試了,當然絕大部分都是完美支援兩種的。所以也沒啥好擔心的。
Tensor的常用的函式
- clone
這個沒啥好說的,上面已經演示了。簡單來說
x = torch.Tensor(2,3)
y = x -- 這個x和y是一樣的!沒有開闢新的記憶體
y = x:clone() --這個才等價於大多數語言 y = x 進行的操作。
- contiguous
有時候你賦值時會出現xxx is not contiguous的錯誤資訊。那麼用這個函式就可以將記憶體變連續。
這個函式對於那些已經是記憶體連續的函式,則返回該函式的同一記憶體地址,如果該函式記憶體不連續,那麼就會新申請空間,進行賦值。這樣做,符合torch就是儘量讓運算加快的原則。
那麼如果是申請Tensor我就用torch.Tensor(2,3,4)
常規的操作,那得到的Tensor肯定是連續的吧,那是什麼原因導致tensor不連續的呢?沒錯,就是對Tensor的擷取!
a = torch.Tensor(3,4)
th> a
6.9436e-310 6.9436e-310 0.0000e+00 0.0000e+00
2.0337e-110 4.0708e-27 6.9981e-308 7.6284e+228
1.0626e+248 6.1255e-154 3.9232e-85 1.9473e-57
[torch.DoubleTensor of size 3x4]
th> a:stride()
4
1
[torch.LongStorage of size 2]
-- 顯然a是連續的。
th> b = a[{{1,3},{2}}]
[0.0000s]
th> b
6.9436e-310
4.0708e-27
6.1255e-154
[torch.DoubleTensor of size 3x1]
th> b:stride()
4
1
[torch.LongStorage of size 2]
th> b:isContiguous()
false
b顯然不連續,因為b的6.9e-310後面第一個單元是a的第二個單元,並不是b的第二個單元!這時候
th> k = b:contiguous()
[0.0000s]
th> k
0.0000e+00
7.6284e+228
1.9473e-57
[torch.DoubleTensor of size 3x1]
[0.0001s]
th> k:stride()
1
1
[torch.LongStorage of size 2]
可看到,k的stride變成1 1了,此時連續!
3. type與其他一些函式
x = torch.Tensor(3):fill(3.14)
> x
3.1400
3.1400
3.1400
[torch.DoubleTensor of dimension 3]
y = x:type('torch.IntTensor')
> y
3
3
3
[torch.IntTensor of dimension 3]
直接在Tensor後面可以進行int(), byte(), float()等操作。
一些看名字就知道怎麼用的函式,記記有好處:
isSameSizeAs, isSize(Tensor),
typeAs(Tensor)可以將一個Tensor變成指定Tensor的大小
stride(i),求第i維度的步長
size(dim)與(#x)[dim]是等價的。(你可能會問為什麼不是 #x[1],因為#是size()的簡寫。返回的是一個LongStorage資料!並不是一個number!LongStorage就是Tensor的內部儲存形式!因此相當於一個Tensor!所以我們要用(#x)[dim]來獲得number型別的數!
這個resize除了正常的等空間的變換,還可以變大的。。
th> a = torch.Tensor(3,4):fill(2)
[0.0001s]
th> a
2 2 2 2
2 2 2 2
2 2 2 2
[torch.DoubleTensor of size 3x4]
[0.0002s]
th> a:resize(3,4,2)
(1,.,.) =
2.0000e+00 2.0000e+00
2.0000e+00 2.0000e+00
2.0000e+00 2.0000e+00
2.0000e+00 2.0000e+00
(2,.,.) =
2.0000e+00 2.0000e+00
2.0000e+00 2.0000e+00
9.2205e+140 9.8730e+169
1.2016e-306 5.7623e-114
(3,.,.) =
2.0869e-76 8.5004e-96
9.0050e+130 2.4793e-70
1.1195e-307 2.9068e-14
3.1443e-120 1.7743e+159
[torch.DoubleTensor of size 3x4x2]
[0.0003s]
-- 一個比較重要的問題是,如果直接在最前面加一維度,則其自身的資料會在最前面,而後面的是不確定的數。這可以作為一個靈活的操作。
th> b = torch.Tensor(2,4):fill(1)
[0.0001s]
th> b:resize(2,2,4)
(1,.,.) =
1.0000e+00 1.0000e+00 1.0000e+00 1.0000e+00
1.0000e+00 1.0000e+00 1.0000e+00 1.0000e+00
(2,.,.) =
1.3069e+179 1.1757e+214 5.9669e-154 1.7713e+159
8.2678e+140 4.0622e-66 7.2010e+252 3.8143e+228
[torch.DoubleTensor of size 2x2x4]
resizeAs(Tensor)
擷取Tensor資料
x[index],等價與select(1,index). 如果x是二維資料,那麼x[2]代表第2行!就是相當於 x:select(1,2)。 選取第一維的第二個。
narrow, select, sub略!
這三個函式可以用x[{ {dim1s, dim1e}, {dim2s, dim2e},… }]來替代!
x = torch.Tensor(2,5,6):zero()
-- 第一種,如果是隻有一個大括號,裡面是單純數字的話,
-- 就說明是"全部"
-- 選擇"第一塊,第三行,沒指定列,所以是全部"
x[{1,3}] = 1
th> x
(1,.,.) =
0 0 0 0 0 0
0 0 0 0 0 0
1 1 1 1 1 1
0 0 0 0 0 0
0 0 0 0 0 0
(2,.,.) =
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
[torch.DoubleTensor of size 2x5x6]
-- 我麼也可以通過這個快速取某個座標的值
-- 比如 x[{1,3,2}],而不用寫成x[{ {1},{3},{2}]
x[{ {dim1s, dim1e}, {dim2s, dim2e},… }]的用法略。
取Tensor中的某個座標的值
x = torch.Tensor(2,3,4)
v = x[{1,2,2}] --取座標值時,用擷取Tensor資料的方法,當index的元素個數等於x的nDimension時,才是取某個值!否則是等價於`select`的!!!!!!!!!!!!!!!!!!!!!!!!
v_false = x[{{1},{2},{3}}] --這個是錯的!這個返回的是Tensor型別,不是num!
x[v] = 2 -- 這個會讓x的一半的值都變成2!!!!
Tensor的賦值
以下只說對單個元素的賦值。
x = torch.Tensor(2,3,4)
--以下下兩種方法等價!
x[{1,2,3}] = 2
x[{{1},{2},{3}}] = 2
Tensor的搜尋
這個就一個函式nonzero(). 返回非0元素的座標的。
> x = torch.rand(4, 4):mul(3):floor():int()
> x
2 0 2 0
0 0 1 2
0 2 2 1
2 1 2 2
[torch.IntTensor of dimension 4x4]
> torch.nonzero(x)
1 1
1 3
2 3
2 4
3 2
3 3
3 4
4 1
4 2
4 3
4 4
[torch.LongTensor of dimension 11x2]
Expanding/Replicating/Squeezing Tensors
expand
這個函式的作用就是在singleton維度進行擴充套件!嚇人的是,這裡使用了一個技巧:expand Tensor時並不需要開闢新的空間,而是直接讓被擴充套件的那個維度的stride為0!
x = torch.rand(10,1)
> x
0.3837
0.5966
0.0763
0.1896
0.4958
0.6841
0.4038
0.4068
0.1502
0.2239
[torch.DoubleTensor of dimension 10x1]
>x:stride()
1
1
y = torch.expand(x,10,2)
> y
0.3837 0.3837
0.5966 0.5966
0.0763 0.0763
0.1896 0.1896
0.4958 0.4958
0.6841 0.6841
0.4038 0.4038
0.4068 0.4068
0.1502 0.1502
0.2239 0.2239
[torch.DoubleTensor of dimension 10x2]
>y:stride()
1
0
可以看見,被擴充套件的維度變成0了!!
又比如:
th> x = torch.rand(5,1,4)
[0.0001s]
th> x:stride()
4 -- 1*4
4 -- 4
1
[torch.LongStorage of size 3]
th> x = torch.rand(5,2,4)
[0.0001s]
th> x:stride()
8 -- 2*4
4 -- 4
1
[torch.LongStorage of size 3]
這是因為直接呼叫建構函式,不指定stride的話,都是連續的。
th> x = torch.rand(10,1,5)
[0.0001s]
th> x:stride()
5
5
1
[torch.LongStorage of size 3]
th> y = torch.expand(x,10,2,5)
[0.0001s]
th> y:stride()
5
0
1
[torch.LongStorage of size 3]
repeatTensor
這個函式是需要新申請空間的!
x = torch.rand(5)
> x
0.7160
0.6514
0.0704
0.7856
0.7452
[torch.DoubleTensor of dimension 5]
> torch.repeatTensor(x,3,2)
0.7160 0.6514 0.0704 0.7856 0.7452 0.7160 0.6514 0.0704 0.7856 0.7452
0.7160 0.6514 0.0704 0.7856 0.7452 0.7160 0.6514 0.0704 0.7856 0.7452
0.7160 0.6514 0.0704 0.7856 0.7452 0.7160 0.6514 0.0704 0.7856 0.7452
[torch.DoubleTensor of dimension 3x10]
Squeeze
這個函式就是將所有singleton的維度壓縮去除。
View—重新看待儲存Storage
這裡主要是5個函式:view,viewAs, transpose(), t(), permute()。
這些都是對於Tensor以另一種角度取看待!因此是直接對原儲存空間進行更改的!
These methods are very fast, because they do not involve any memory copy.
x = torch.zeros(4)
> x:view(2,2)
0 0
0 0
[torch.DoubleTensor of dimension 2x2]
> x:view(2,-1)
0 0
0 0
[torch.DoubleTensor of dimension 2x2]
-- 利用view是進行增加一個“1”維的很好的方法
x = x:view(2,1,2)
th> x
(1,.,.) =
0 0
(2,.,.) =
0 0
[torch.DoubleTensor of size 2x1x2]
th> x:stride()
2
2
1
[torch.LongStorage of size 3]
其他函式略