先看Tensor,
看Tensor的源码之前,先看看如何使用Tensor,这有助于理解Tensor设计的目的。
这个在torch7中最主要的类:https://gitee.com/anjiang2020_admin/torch7/blob/master/doc/tensor.md
多维矩阵
一个 Tensor 一个 多维矩阵multi-dimensional matrix. 矩阵可以有多少维度?由LongStorage决定
例子程序:
--- creation of a 4D-tensor 4x5x6x2
z = torch.Tensor(4,5,6,2)
--- for more dimensions, (here a 6D tensor) one can do:
s = torch.LongStorage(6)
s[1] = 4; s[2] = 5; s[3] = 6; s[4] = 2; s[5] = 7; s[6] = 3;
x = torch.Tensor(s)
用 nDimension() 或者 dim()可以或者到Tensor的维度个数。
具体第i个维度的长度可使用size(i)得到.。
一个LongStorage 所以维度的具体长度。
> x:nDimension()
6
> x:size()
4
5
6
2
7
3
[torch.LongStorage of size 6]
Tensor就是一个多维矩阵,multi-dimensional matrix。矩阵的维度没有被限制,决定于LongStorage,可以是3维矩阵,4维矩阵,...,6维矩阵等。
内部数据
Tensor对应的具体数据保存在Storage中,可用Storage()访问。
Tensor的内存对应唯一的Storage,地址可以不相邻:Storage的地址通过storageOffset()获取,如果要访问某维度i的第j个元素,就需要用stride(i)跳过去,即这个元素的地址在:storageOffset()+stride(i)*(j-1),我们来看个例子,又一个3D Tensor:
x = torch.Tensor(7,7,7)
获取(3,4,5)位置的元素可以这样:
> x[3][4][5]
或者也可以这样: (但是这样访问很慢!)
> x:storage()[x:storageOffset()
+(3-1)*x:stride(1)+(4-1)*x:stride(2)+(5-1)*x:stride(3)]
或者我们可以这样说:Tensor只不过是Stroage的一种特殊访问方式,这种访问方式就好像让这一堆存储对应成了多维数据。
x = torch.Tensor(4,5)
s = x:storage()
for i=1,s:size() do -- 填充Storage
s[i] = i
end
> x -- s这个storage被x解释为一个2D矩阵
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]
同时注意到,在Torch7中,同一行的元素(即多维矩阵的最后一个维度的元素)在内存中是连续的。
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了(不是Fortran).
不同数据类型的Tensors
存在下列Tensor:
ByteTensor -- contains unsigned chars
CharTensor -- contains signed chars
ShortTensor -- contains shorts
IntTensor -- contains ints
LongTensor -- contains longs
FloatTensor -- contains floats
DoubleTensor -- contains doubles
大部分的数值计算,只实现了FloatTensor和DoubleTensor。其他类型的Tensor只在你想节省存储时才有用。
高效的内存管理
Efficient memory management
All tensor operations in this class do not make any memory copy. All these methods transform the existing tensor, or return a new tensor referencing the same storage. This magical behavior is internally obtained by good usage of the stride() and storageOffset(). Example:
所有对tensor的操作,都不能开启新的内存空间。
都是对已经存在的内存做in-place操作,
操作完成后,返回一个新的tensor,但是指向同样的内存。
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]
使用copy()才会开启新内存:
y = torch.Tensor(x:size()):copy(x)
或者使用clone()
y = x:clone()
下面我们看Tensor的methods,如果你想操作不同类型的Tensor,使用CharTensor即可。
Tensor有很多methods。像add,mul,填充fill,更改大小,获取子矩阵等。
这里就查看一个比较特殊的methods吧,
[Tensor] gather(dim, index)
挑选出一些元素组成新的矩阵
当dim=1时,挑选的某个元素序号为::index[i][j][k],j, k
当dim=2时,挑选的某个元素序号为::i , index[i][j][k] , k
当dim=3时,挑选的某个元素序号为::i, j, index[i][j][k]
-- dim = 1
result[i][j][k]... = src[index[i][j][k]...][j][k]...
-- dim = 2
result[i][j][k]... = src[i][index[i][j][k]...][k]...
-- etc.
src 是被挑选的 Tensor.
例如:src 的 size 是 n x m x p x q, dim = 3, 我们希望在第3维度上取3个值,我们将得到一个 size 为 n x m x k x q 的矩阵。
gather操作返回的矩阵,将保存一个新的地址。
x = torch.rand(5, 5)
> x
0.7259 0.5291 0.4559 0.4367 0.4133
0.0513 0.4404 0.4741 0.0658 0.0653
0.3393 0.1735 0.6439 0.1011 0.7923
0.7606 0.5025 0.5706 0.7193 0.1572
0.1720 0.3546 0.8354 0.8339 0.3025
[torch.DoubleTensor of size 5x5]
y = x:gather(1, torch.LongTensor{{1, 2, 3, 4, 5}, {2, 3, 4, 5, 1}})
> y
0.7259 0.4404 0.6439 0.7193 0.3025
0.0513 0.1735 0.5706 0.8339 0.4133
[torch.DoubleTensor of size 2x5]
z = x:gather(2, torch.LongTensor{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 1}})
> z
0.7259 0.5291
0.4404 0.4741
0.6439 0.1011
0.7193 0.1572
0.3025 0.1720
[torch.DoubleTensor of size 5x2]
去看下Tensor的具体实现代码:
这个文件里只是一些include,先看下general.h
除了一些std库的文件,还有一些预定义,最有用的就是8--9 行:luaT.h,TH.h
TH.h里面果然有内容,THTensor.h,THStorage.h等都是重点。
只看头文件,大概可以猜测:torch7中的tensor,是在对TH库的封装。那么这个TH是啥意思?Torch取第一个字母和最后一个字母的意思?
姑且先这么认为吧。
我们找到TH库中,lib/TH/generic/THTensor.h
THTensor是C中的结构体,这就是张量的基本数据结构了。
long *size: 存储THTensor每个维度的长度的数组
long *stride: 存储每个维度在寻址时跳转的量的数组
这两个量的存在,是为了用维度的方式去访问那一堆内存地址的。
THStorage *storage: Tensor元素存储的内存
ptrdiff_t storageOffset: 访问具体某个元素时,地址的开始量
int refcount: 引用计数,有点像智能指针的计数
char flag:还不清楚,
上图是THTensor结构体的访问函数,这个语法有点奇怪,也不知道该怎么查。
这里函数名字是:THTensor_(storage),难道这是个预定义?
从语法上理解,THTensor_只能是个宏定义,果然:
宏定义内部还有宏TH_CONCAT_4,不过这个从名字上看,就是连接4个字符串的意思啦,我们搜索一下:
其中116行的TH_CONCAT_4_EXPAND是源头的宏定义。
回到最开始的问题:
这里函数名字是:THTensor_(storage),就可以展开为:THRealTensor_storage,这就是实际的函数名。
每个函数前,都有个TH_API,
原来这里是指定extern “C”,C++调用C 时,需要指定。参考:https://www.cnblogs.com/xiangtingshen/p/10980055.html
和dllexport或者dllimport的,
具体:https://www.cnblogs.com/foohack/p/4119207.html
终于把语法搞清楚了。
可以看这个结构是如何构建的了:
THRealTensornew函数内部先THAlloc,然后又THRealTensorrawInit了一下。
上图又是两个建立新tensor的函数。可以看到,这个过程中,就是在不断调用THStora ge的过程。
再接着看看计算部分:
这里有个cadd函数,它是TH库的函数
它用在了torch7/generic/TensorOperator.c中,被算子所调用了。
但是这个TensorOperator.c中,是定义了几种简单的算子:加,减,乘,除等。
我搜索了整个torch7,都没法优先?地方调用卷积函数。
难道在torch7库中,只是定义了,没有使用它?
终于,看了torch7中的TensorMath.lua,发现了conv2等算子。
torch7中,使用了luaT把 TH库中的C代码封装了一下,然后又些lua代码来调用TH中定义的结构体和函数。
lua这门脚本语言也挺简单的,看这里:https://www.runoob.com/lua/lua-object-oriented.html
luaT是把C封装到lua中使用的库,在这里:https://gitee.com/anjiang2020_admin/torch7/tree/master/lib/luaT#luat_newmetatable
看torch7的文件结构:
torch7编译好以后,应该可以写lua脚本来调用。
编译过程:https://blog.csdn.net/real_myth/article/details/52291636
编译好后,写lua脚本即可,可以参考torch7的项目开发示范代码:
https://gitee.com/anjiang2020_admin/torch7/tree/master/tutorials-master
找到这个train()函数了,如下
后面可以train函数里面使用的相关函数为起点,再追查到luaT库和TH库,从而把torch7的整个架构搞清楚。
比如说,以
图中所示的model:forward函数为起点。
在查找model时,发现处理torch包,还有nn包,就回到torch的github上看,原来这里不止troch一个包。
整个torch包含torch7,nn,tutorial, distro,demos,cutorch等,
需要将nn,demos等下载,然后再做解读。
总结:
torch7只是一个提供基本数据结构与cpu算子的包,如果需要完成网络构建,需要使用lua语言nn库,如果使用gpu,需要使用cutorch包。
这份代码非常底层,就比较做到逻辑清晰,相比直接看pytorch的代码,显然看torch代码会理解的更快。