Torch手札2:一小时Torch入门

干货哟

Posted by Kriz on 2017-05-03

需要少量深度网络知识。Torch和iTorch的安装配置请参考上一篇相关文章。

准备

  • Torch基于Lua语言,运行于Lua-JIT(Just-in-time compiler)。
  • 未经明确local定义的变量都是global。
  • 只有table这一种内置的数据结构,用一组大括号表示。
  • 下标从1开始。
  • foo:bar是foo.bar(foo)的语法糖。

开始!

Strings, numbers, tables

1
2
3
4
5
6
7
8
9
10
11
a = 'hello'
print(a)
b = {}
b[1] = a -- 下标是从1开始下标是从1开始下标是从1开始
print(b)
b[2] = 30
for i = 1,#b do -- Lua中的#b可以理解为b.length
print(b[i])
end

Tensors

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
27
28
29
30
31
32
33
a = torch.Tensor(5,3) -- 建立未初始化的5*3矩阵
a = torch.rand(5,3) -- 对矩阵进行随机初始化
b = torch.rand(3,4) -- 可以直接调用rand函数,以建立随机初始化的5*3矩阵
--[[ 乘法相关 --]]
a*b -- 矩阵相乘方法1
torch.mm(a,b) -- 矩阵相乘方法2
c = torch.Tensor(5,4)
c:mm(a,b) -- 矩阵相乘方法3
--[[ 加减法相关 --]]
a = torch.ones(5,2)
b = torch.Tensor(2,5):fill(4) -- 两种同数值初始化方式
print(addTensor(a,b)) -- 两个Tensor相加时,size可以不同,但element的总数须相同
c = torch.add(a,b) -- 返回新Tensor
torch.add(res,a,b) -- 存储a+b的结果到res中
d:add(a,b) -- 存储a+b的结果到d中
e:add(a) -- 将a中所有element相加,存入e中
b:add(2,a) -- a中每个element都乘以scalar value(此处是2)再和b相加,结果存入b
f:add(b,2,a) -- b+2*a,存入f
torch.add(b,2,a) -- b+2*a,返回新Tensor
torch.add(g,b,2,a) -- b+2*a,存入g
b:csub(a) -- 减法,b-a存入b

Tensor的重要方法能写三篇文,这里就不阐述了。如果有兴趣,可参考这里

CUDA Tensors

1
2
3
4
5
6
require 'cutorch';
a = a:cuda() -- 使用cuda()函数将tensor移动到GPU端
b = b:cuda()
c = c:cuda()
c:mm(a,b) -- 使用GPU完成运算

Neural Networks

正片开始

在Torch中使用神经网络,只需要引入依赖包:

1
require 'nn';

nn的每一个部分是一个Module,把它们扔进Containers里,组合成一个网络整体。

这里还是以被玩坏了的手写数字识别为例:

mnist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
net = nn.Sequential() -- 建立一个网络容器
net:add(nn.SpatialConvolution(1,6,5,5)) -- 建立一个卷积层,1个输入,6个输出,5*5卷积核
net:add(nn.ReLU()) -- 激活函数
net:add(nn.SpatialMaxPooling(2,2,2,2)) -- 2*2窗口做最大池化
net:add(nn.SpatialConvolution(6,16,5,5))
net:add(nn.ReLU())
net:add(nn.SpatialMaxPooling(2,2,2,2))
net:add(nn.View(16*5*5)) -- 将3D Tensor转变为一维向量
net:add(nn.Linear(16*5*5, 120)) -- 全连接(输入和权值相乘)
net:add(nn.ReLU())
net:add(nn.Linear(120, 84))
net:add(nn.ReLU())
net:add(nn.Linear(84, 10)) -- 10类输出
net:add(nn.LogSoftMax()) -- 分类问题决策
print('Lenet5\n' .. net:__tostring());

说句题外话,刚入门的旁友们如果不能抽象理解神经网络每一层的特征提取过程,可以试着来这里可视化地感受一下。

Torch提供三种不同的nn container:

nnimg

Torch对每个Module提供自动微分(automatic differentiation)机制,囊括前向传播和反向传播。

语法:

1
2
:forward(input)
:backward(input,gradient)

范例:

1
2
3
4
5
input = torch.rand(1,32,32)
output = net:forward(input) -- 把input数据放入网络,正向传播得到结果
net:zeroGradParameters() -- 将Module内部的偏导数清零,不做这一步的话偏导会累加
gradInput = net:backward(input,torch.rand(10)) -- 反向推导出正向传播结果值得偏导数

接下来定义损失函数:

1
2
3
4
5
6
criterion = nn.ClassNLLCriterion() -- 使用负对数似然(negative log-likelihood)评测多分类
criterion:forward(output,groundTruth) -- 调用前向传播算法,输入值为预测内容和训练样本所属内容
gradients = criterion:backward(output,groundTruth) -- 返回损失函数的梯度
net:backward(input,gradients)
net:updateParameters(0.001) -- 更新权重,输入值为学习率

以随机梯度下降为例,训练网络时,网络的自适应公式为

weight = weight + learningRate * gradWeight

nn.StochasticGradient中的函数:train(dataset)可以进行基于SGD的权值更新。

说了这么多,我们来模拟一个全部过程吧。

使用CIFAR-10数据集。

cifar

整个过程可以分为这样五个重要的流程:

  1. 读取及规范化数据(Load and normalize data)

    导入50000张训练集图像整理成的50000x3x32x32的4D ByteTensor,同理导入测试集整理成的10000x3x32x32的Tensor。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    require 'paths'
    if (not paths.filep("cifar10torchsnall.zip")) then
    os.execute('wget -c https://s3.amazonaws.com/torch7/data/cifar10torchsmall.zip')
    os.execute('unzip cifar10torchsmall.zip')
    end -- 准备图像集
    trainset = torch.load('cifar10-train.t7')
    testset = torch.load('cifar10-test.t7')
    classes = {'airplane', 'automobile', 'bird', 'cat',
    'deer', 'dog', 'frog', 'horse', 'ship', 'truck'}
    print(#trainset.data)

    要使用nn.StochasticGradient需要两个条件,数据集必须存在:size()函数定义;dataset[i]需要返回对应下标的实例。(谜之押韵

    这里使用了lua的setmetatable语法,不用在意太多细节。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    setmetatable(trainset,
    {__index = function(t, i)
    return {t.data[i], t.label[i]}
    end}
    );
    trainset.data = trainset.data:double() -- 转换成DoubleTensor
    function trainset:size()
    return self.data:size(1)
    end
    print(trainset:size())
    print(trainset[33])

    这里提一下,Torch中的Tensor种类有

    1
    2
    3
    4
    5
    6
    7
    ByteTensor -- contains unsigned chars
    CharTensor -- contains signed chars
    ShortTensor -- contains shorts
    IntTensor -- contains ints
    LongTensor -- contains longs
    FloatTensor -- contains floats
    DoubleTensor -- contains doubles

    很多数学运算只支持FloatTensorDoubleTensor。其他种类大多是为了性能考虑。

    数据处理的最后一步是归一化(normalize),我们使其符合标准正态分布,均值置0,标准差置1。归一化的目的是加快收敛。在这里需要先介绍一下索引操作(Tensor indexing operator),一般使用中括号套花括号的形式:

    [{ }]

    而索引中的内容是使用大括号套住的。比如:

    1
    redChannel = trainset.data[{ {}, {}, {}, {} }]

    已知trainset.data是一个四维Tensor,分别为第几张图像图像的通道图像的纵像素图像的横像素。使用print(#redChannel)测试一下,输出50000、3、32、32。

    如果改成

    1
    redChannel = trainset.data[{ {100,105}, {1}, {}, {} }]

    则输出会变成6、1、32、32。这是因为大括号中的数字充当了索引,最终取到的只有100-105张图片的R通道。

    归一化操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mean = {} -- 均值
    stdv = {} -- 标准差
    for i=1,3 do -- 对每个通道分别执行操作
    mean[i] = trainset.data[{ {}, {i}, {}, {} }]:mean() -- 计算均值
    print('Channel ' .. i .. ', Mean: ' .. mean[i])
    trainset.data[{ {}, {i}, {}, {} }]:add(-mean[i]) -- 减去均值(令最终均值为0)
    stdv[i] = trainset.data[{ {}, {i}, {}, {} }]:std() -- 计算标准差
    print('Channel ' .. i .. ', Standard Deviation: ' .. stdv[i])
    trainset.data[{ {}, {i}, {}, {} }]:div(stdv[i]) -- 标准差归1
    end

    这一步完成之后,数据的处理就告一段落了。

  2. 构造网络(Define Neural Network)

    可以把刚才的网络直接拿来用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    net = nn.Sequential()
    net:add(nn.SpatialConvolution(3,6,5,5)) -- 这里改一下输入深度就可以了
    net:add(nn.ReLU())
    net:add(nn.SpatialMaxPooling(2,2,2,2))
    net:add(nn.SpatialConvolution(6,16,5,5))
    net:add(nn.ReLU())
    net:add(nn.SpatialMaxPooling(2,2,2,2))
    net:add(nn.View(16*5*5))
    net:add(nn.Linear(16*5*5, 120))
    net:add(nn.ReLU())
    net:add(nn.Linear(120, 84))
    net:add(nn.ReLU())
    net:add(nn.Linear(84, 10))
    net:add(nn.LogSoftMax())
  3. 适配损失函数(Define Loss function)

    ClassNLLCriterion似乎很好用的样子

    1
    criterion = nn.ClassNLLCriterion()
  4. 根据数据训练网络(Train network on training data)

    1
    2
    3
    4
    5
    trainer = nn.StochasticGradient(net, criterion)
    trainer.learningRate = 0.001
    trainer.maxIteration = 5 -- 跑5个迭代,随便一测试
    trainer:train(trainset)
  5. 测试网络(Test network on test data)

    在这一步就要看看机器到底学到什么了没。

    试着显示一张测试集中的内容,来熟悉对测试集的操作:

    1
    2
    print(classes[testset.label[100]])
    itorch.image(testset.data[100])

    接下来用训练集的数据对测试集进行归一化:

    1
    2
    3
    4
    5
    6
    7
    8
    testset.data = testset.data:double()
    for i=1,3 do
    testset.data[{ {}, {i}, {}, {} }]:add(-mean[i])
    testset.data[{ {}, {i}, {}, {} }]:div(stdv[i])
    end
    horse = testset.data[100]
    print(horse:mean(), horse:std())

    看看识别结果:

    1
    2
    predicted = net:forward(testset.data[100])
    print(predicted:exp()) -- 上一步的输出是对数概率,要做一下转换

    为了清晰一点,可以带着类名一起输出:

    1
    2
    3
    for i=1,predicted:size(1) do
    print(classes[i], predicted[i])
    end

    对于多个样例的检验方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    correct = 0
    for i = 1,10000 do
    local groundTruth = test.label(i)
    local prediction = net:forward(testset.data[i])
    local confidences, indices = torch.sort(prediction, true) -- 倒序排列
    if groundTruth == indices[1] then
    correct = correct + 1
    end
    end
    print(correct, 100*correct/10000 .. ' % ')

    这样能检测出整体的信息,不过如果想知道每个类单独的测试情况,可以改成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class_performance = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
    for i=1,10000 do
    local groundtruth = testset.label[i]
    local prediction = net:forward(testset.data[i])
    local confidences, indices = torch.sort(prediction, true)
    if groundtruth == indices[1] then
    class_performance[groundtruth] = class_performance[groundtruth] + 1
    end
    end
    for i=1,#classes do
    print(classes[i], 100*class_performance[i]/1000 .. ' %')
    end

    如果想要使用GPU,就要祭出cunn了。转换到GPU端运行是非常简单的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    require 'cunn';
    ...
    net = net:cuda()
    ...
    criterion = criterion:cuda()
    ...
    trainset.data = trainset.data:cuda()
    trainset.label = trainset.label:cuda()
    ...

参考文献