torch深度学习教程——神经网络(翻译+个人见解)

原文地址:https://dp.readthedocs.io/en/latest/neuralnetworktutorial/index.html#neural-network-tutorial

神经网络教程

我们从一个简单的神经网络示例(代码)开始。 第一行加载dp包,其任务是加载依赖项(详情请见init.lua):

require 'dp'

注意:如上图在init.lua中,Moses包导入时用_指代。 所以_不能用于虚拟变量,而是使用__,或诸如此类的表示方法。

命令行参数

让我们定义一些命令行参数。 这些将存储在表opt中,将在脚本启动时打印。 命令行参数可以轻松控制实验并尝试不同的超参数,而无需修改任何代码

--[[command line arguments]]--
cmd = torch.CmdLine()
cmd:text()
cmd:text('Image Classification using MLP Training/Optimization')
cmd:text('Example:')
cmd:text('$> th neuralnetwork.lua --batchSize 128 --momentum 0.5')
cmd:text('Options:')
cmd:option('--learningRate', 0.1, 'learning rate at t=0')
cmd:option('--lrDecay', 'linear', 'type of learning rate decay : adaptive | linear | schedule | none')
cmd:option('--minLR', 0.00001, 'minimum learning rate')
cmd:option('--saturateEpoch', 300, 'epoch at which linear decayed LR will reach minLR')
cmd:option('--schedule', '{}', 'learning rate schedule')
cmd:option('--maxWait', 4, 'maximum number of epochs to wait for a new minima to be found. After that, the learning rate is decayed by decayFactor.')
cmd:option('--decayFactor', 0.001, 'factor by which learning rate is decayed for adaptive decay.')
cmd:option('--maxOutNorm', 1, 'max norm each layers output neuron weights')
cmd:option('--momentum', 0, 'momentum')
cmd:option('--hiddenSize', '{200,200}', 'number of hidden units per layer')
cmd:option('--batchSize', 32, 'number of examples per batch')
cmd:option('--cuda', false, 'use CUDA')
cmd:option('--useDevice', 1, 'sets the device (GPU) to use')
cmd:option('--maxEpoch', 100, 'maximum number of epochs to run')
cmd:option('--maxTries', 30, 'maximum number of epochs to try to find a better local minima for early-stopping')
cmd:option('--dropout', false, 'apply dropout on hidden neurons')
cmd:option('--batchNorm', false, 'use batch normalization. dropout is mostly redundant with this')
cmd:option('--dataset', 'Mnist', 'which dataset to use : Mnist | NotMnist | Cifar10 | Cifar100')
cmd:option('--standardize', false, 'apply Standardize preprocessing')
cmd:option('--zca', false, 'apply Zero-Component Analysis whitening')
cmd:option('--progress', false, 'display progress bar')
cmd:option('--silent', false, 'dont print anything to stdout')
cmd:text()
opt = cmd:parse(arg or {})
opt.schedule = dp.returnString(opt.schedule)
opt.hiddenSize = dp.returnString(opt.hiddenSize)
if not opt.silent then
   table.print(opt)
end

预处理

可以切换cmd-line参数--standardize和--zca以对数据执行一些预处理。

--[[preprocessing]]--
local input_preprocess = {}
if opt.standardize then
   table.insert(input_preprocess, dp.Standardize())
end
if opt.zca then
   table.insert(input_preprocess, dp.ZCA())
end
if opt.lecunlcn then
   table.insert(input_preprocess, dp.GCN())
   table.insert(input_preprocess, dp.LeCunLCN{progress=true})
end

一种非常常见且简单的预处理技术是标准化数据集,标准化的操作是使数据减去平均值并除以标准差。 统计数据(平均值和标准差)仅在训练组上进行测量,这是预处理数据时的常见模式。 当需要在不同的示例中测量统计数据时(如在ZCA和LecunLCN预处理中),我们将预处理器在训练集上进行适应并将其应用于所有集合(训练,交叉验证集和测试)。 但是,某些预处理要求仅在每个示例上测量统计信息,如全局约束标准化([GCN]](preprocess.md#dp.GCN)),这种情况则不需要在训练集上适应。

数据集

我们打算构建并训练神经网络,因此我们需要一些数据,这些数据封装在DataSource对象中。 dp提供了对不同数据集进行训练的选项,特别是MNISTNotMNISTCIFAR-10CIFAR-100。 这个脚本的默认数据集是MNIST。 但是,您可以使用--dataset参数指定其他图像分类数据集。

--[[data]]--
if opt.dataset == 'Mnist' then
   ds = dp.Mnist{input_preprocess = input_preprocess}
elseif opt.dataset == 'NotMnist' then
   ds = dp.NotMnist{input_preprocess = input_preprocess}
elseif opt.dataset == 'Cifar10' then
   ds = dp.Cifar10{input_preprocess = input_preprocess}
elseif opt.dataset == 'Cifar100' then
   ds = dp.Cifar100{input_preprocess = input_preprocess}
else
    error("Unknown Dataset")
end

DataSource最多包含三个DataSet:train,valid和test。 第一个用于训练模型。 第二个用于早期停止和交叉验证。 第三个用于发表发表论文时比较不同模型的结果。

模型

好的,我们有一个DataSource,现在我们需要一个模型。 让我们构建带有一个或多个参数的非线性层的多层感知器(MLP)(注意,在忽略隐藏层的情况下(--hiddenSize'{}'),模型只是一个线性分类器):

--[[Model]]--
model = nn.Sequential()
model:add(nn.Convert(ds:ioShapes(), 'bf')) -- batchSize x nFeature(也是类型转换)

-- hidden layers
inputSize = ds:featureSize()
for i,hiddenSize in ipairs(opt.hiddenSize) do
   model:add(nn.Linear(inputSize, hiddenSize)) -- parameters
   if opt.batchNorm then
      model:add(nn.BatchNormalization(hiddenSize))
   end
   model:add(nn.Tanh())
   if opt.dropout then
      model:add(nn.Dropout())
   end
   inputSize = hiddenSize
end

-- output layer
model:add(nn.Linear(inputSize, #(ds:classes())))
model:add(nn.LogSoftMax())

输出和隐藏层使用线性定义,其中包含将要学习的参数,后接非线性激励函数,如Tanh(用于隐藏神经元)和LogSoftMax(用于输出层)。后者可能看起来很奇怪(为什么不使用SoftMax?),但ClassNLLCriterion仅适用于LogSoftMax(或使用SoftMax + Log)。

线性模块使用2个参数构造,inputSize(输入单元数)和outputSize(输出单元数)。对于第一层,inputSize是输入图像中的特征数量。在我们的例子中,这是1x28x28 = 784,这是ds:featureSize()返回的。

nn.Convert模块有两个目的。首先,无论收到什么类型的Tensor,它都会输出该模型使用的Tensor类型。其次,它将转化收到的不同形状的Tensor。典型图像的形状是bchw,是批次,颜色/通道,高度和宽度的缩写。像SpatialConvolution和SpatialMaxPooling模块期望的输入类型就是bchw。另一方面,我们的MLP期望输入类型是bf,即批次和特征的缩写。实际上,这是一个非常简单的转换,您需要做的就是将chw维度展平为单个f维度(在这种情况下,尺寸为784)。

对于那些不熟悉nn包的人,上面代码片段中的所有nn.*都是Module子类。即使对于Sequential模型也是如此。虽然后者很特别,它是其他模块的容器,即复合模型。

传播

接下来我们初始化一些传播模块。 每个这样的传播模块都将从不同的数据集开始传播。 采样器迭代数据集以生成批量样本标签对(输入和目标)以便在模型中传播:

--[[Propagators]]--
if opt.lrDecay == 'adaptive' then
   ad = dp.AdaptiveDecay{max_wait = opt.maxWait, decay_factor=opt.decayFactor}
elseif opt.lrDecay == 'linear' then
   opt.decayFactor = (opt.minLR - opt.learningRate)/opt.saturateEpoch
end

train = dp.Optimizer{
   acc_update = opt.accUpdate,
   loss = nn.ModuleCriterion(nn.ClassNLLCriterion(), nil, nn.Convert()),
   epoch_callback = function(model, report) -- 每轮epoch调用
      -- learning rate decay
      if report.epoch > 0 then
         if opt.lrDecay == 'adaptive' then
            opt.learningRate = opt.learningRate*ad.decay
            ad.decay = 1
         elseif opt.lrDecay == 'schedule' and opt.schedule[report.epoch] then
            opt.learningRate = opt.schedule[report.epoch]
         elseif opt.lrDecay == 'linear' then 
            opt.learningRate = opt.learningRate + opt.decayFactor
         end
         opt.learningRate = math.max(opt.minLR, opt.learningRate)
         if not opt.silent then
            print("learningRate", opt.learningRate)
         end
      end
   end,
   callback = function(model, report) -- called every batch
      if opt.accUpdate then
         model:accUpdateGradParameters(model.dpnn_input, model.output, opt.learningRate)
      else
         model:updateGradParameters(opt.momentum) -- 修改gradParams
         model:updateParameters(opt.learningRate) -- 修改params
      end
      model:maxParamNorm(opt.maxOutNorm) -- 修改params
      model:zeroGradParameters() -- 修改gradParams 
   end,
   feedback = dp.Confusion(),
   sampler = dp.ShuffleSampler{batch_size = opt.batchSize},
   progress = opt.progress
}
valid = dp.Evaluator{
   feedback = dp.Confusion(),  
   sampler = dp.Sampler{batch_size = opt.batchSize}
}
test = dp.Evaluator{
   feedback = dp.Confusion(),
   sampler = dp.Sampler{batch_size = opt.batchSize}
}

对于此示例,我们使用优化器训练数据集,以及两个评估器,一个用于交叉验证,另一个用于测试。 现在让我们探索不同的构造函数参数。

取样器(sampler)

评估器使用一个简单的取样器,它按顺序遍历数据集。 另一方面,优化器使用Shuffle取样器。这个取样器在每次传递数据集中的所有样本数据之前对数据集的(索引)进行shuffle(混洗)。 这种混洗对于训练很有用,因为模型通过数据集从不同批次的序列中学习。 这使得训练算法更具随机性(受限于每个样本在每个时期呈现一次且每个时期仅一次)。

损失函数(loss)

每个传播器还可以指定训练或评估的损失函数。该参数很重要,因为它是反向传播所必需的。 如果您以前使用过nn软件包,那么这里没有什么新内容。 损失函数定义了标准。 每个样本都有一个目标类,我们的模型输出是LogSoftMax,因此我们使用ClassNLLCriterion,该被包含在ModuleCriterion中,它是一个修饰器,允许我们在传递给损失函数之前,通过模块传递每个输入和目标。 在我们的例子中,我们希望确保每个目标都转换为损失函数的类型。

反馈(feedback)

反馈参数用于向我们提供反馈(如每轮epoch后的性能指标和统计数据)。我们使用Confusion,它封装了optim库的ConfusionMatrix。 损失函数测量了不同数据集上的负似然对数(NLL),反馈了分类的度(我们将用于早期停止并将我们的模型与现有技术进行比较)。

回调(callback) 

由于优化器用于在数据集上训练模型,因此我们需要指定一个在连续的前向/后向调用之后调用的回调函数。 回调一般应该是updateParameters或accUpdateGradParameters。updateGradParameters也可使用命令行中指定的内容(通常称为动量学习)。 你也可以选择用weightDecay或maxParamNorm来规范它(个人而言,我更喜欢后者)。在任何情况下,回调都是一个可以定义以满足您需求的函数。

epoch_callback

回调函数是在每个batch后调用,但epoch_callback在epochs之间调用。 这对于衰减超参数(例如学习速率)非常有用,这是我们在此示例中所做的。 由于学习速率是最重要的超参数,因此在超优化期间尝试不同的学习速率衰减调度是一个好主意。

--lrDecay 'linear'是默认的命令行参数。它指定起始学习率--learningRate,最小学习率--minLR以及达到该最小值的时期:--saturateEpoch。 

--lrDecay 'adaptive'使用指数衰减的学习率。 默认情况下,此模式衰减学习率直到找到--maxWait设定的epochs的最小值。 但是通过使用--maxWait -1,我们可以使用以下规则衰减每个时期的学习率:lr = lr * decayFactor。 这将比线性衰减更快地衰减学习速率。 

另一个选项(--lDeDeaysSdudule)是通过--schedule`指定将学习率映射到诸如“{[200] = 0.01,[300] = 0.001}”等时期表来手动指定学习率 ,这将控制学习率在给定时期衰减到给定值。 

当然,因为这个参数只是一个回调函数,所以您可以通过编写自己的函数来使用它。

acc_update

设置为true时,参数(即gragraParameters)对应的梯度在前向和后向传播后对参数(即parameters)进行更新中。换句话说,当acc_update = true,传播批处理的顺序基本上是:

1、updateOutput
2、updateGradInput
3、accUpdateGradParameters

而不是更常见的:

1、updateOutput
2、updateGradInput
3、accGradParameters
4、updateParameters

如果默认值为false,这意味着内部实际上没有使用gradParameters。某些方法如updateGradParameters(动量学习)和weightDecay的情况不适用于acc_update,因为它们需要在添加到参数之前填充gradParameters的tensor。

进展

最后,我们设置优化器的进度条,以便监控训练进度。

实验

现在是时候把它们放在一起做实验:

--[[Experiment]]--
xp = dp.Experiment{
   model = model,
   optimizer = train,
   validator = valid,
   tester = test,
   observer = {
      dp.FileLogger(),
      dp.EarlyStopper{
         error_report = {'validator','feedback','confusion','accuracy'},
         maximize = true,
         max_epochs = opt.maxTries
      }
   },
   random_seed = os.time(),
   max_epoch = opt.maxEpoch
}

observer

可以使用observer列表初始化实验,顺序不重要。 observer监听mediator频道。 当某些事件发生时,mediator会调回observer。 特别是,observer可以收听doneEpoch频道,以便在每个epoch之后接收来自实验的报告。 报告只不过是一堆与实验对象结构相匹配的嵌套表。 在每个epoch之后,实验的组件对象(observer除外)可以各自向其复合父项提交报告,从而形成报告树。observer可以分析并修改它们所属的组件(在本例中为实验)。observer可以被分配到实验,传播者,访客等组件。

文件日志(fileLogger)

这里我们使用一个简单的FileLogger,它将序列化的报告存储在一个简单的文本文件中供以后使用。 相应的报告里包含每个实验唯一的ID,fileLogger可以通过这些ID命名文件。

早期停止(EarlyStopper)

EarlyStopper用于在错误率未减少或准确率尚未达到最大值时停止实验。它还会在找到新的最佳版本的版时将实验保存到磁盘。 它使用通道初始化。在这次情况下,我们打算在准确性上进行早期停止,该准确性字段属于验证器反馈表的混淆表。 {'validator','feedback','confusion','accuracy'}在每个训练时期之后测量模型在验证数据集上的准确性。 因此,通过早期停止这一措施,我们希望找到一个能够很好地推广的模型。 参数max_epochs表示在通过doneExperiment Mediator Channel发出信号之前,进行多少轮连续的训练epoch才能找到新的最佳模型。

运行实验

一旦我们初始化了实验,我们只需要在数据集上运行它就可以开始训练。

xp:run(ds)

我们不使用数据集初始化实验,这样我们可以轻松将此实验与数据分开(实验不修改)。

让我们从cmd-line运行脚本(使用默认参数):

nicholas@xps:~/projects/dp$ th examples/neuralnetwork.lua 

首先,它打印存储在opt中的命令行参数:

{
   batchNorm : false
   batchSize : 32
   cuda : false
   dataset : "Mnist"
   dropout : false
   hiddenSize : {200,200}
   learningRate : 0.1
   lecunlcn : false
   maxEpoch : 100
   maxOutNorm : 1
   maxTries : 30
   momentum : 0
   progress : false
   schedule : {[200]=0.01,[400]=0.001}
   silent : false
   standardize : false
   useDevice : 1
   zca : false
}   

之后它输出模型。

Model : 
nn.Sequential {
  [input -> (1) -> (2) -> (3) -> (4) -> (5) -> (6) -> (7) -> output]
  (1): nn.Convert
  (2): nn.Linear(784 -> 200)
  (3): nn.Tanh
  (4): nn.Linear(200 -> 200)
  (5): nn.Tanh
  (6): nn.Linear(200 -> 10)
  (7): nn.LogSoftMax
}

然后FileLogger打印将保存epoch日志的位置。可以使用$TORCH_DATA_PATH或$DEEP_SAVE_PATH环境变量来控制日志位置。默认存储地址为$HOME/save。

FileLogger: log will be written to /home/nicholas/save/xps:1432747515:1/log 

最后,我们来看有趣的部分:实际训练。 每个epoch的一些性能数据都会输出到stdout:

==> epoch # 1 for optimizer :   
==> example speed = 4508.3691689025 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.012714211946021    
xps:1432747515:1:optimizer:confusion accuracy = 0.8877  
xps:1432747515:1:validator:confusion accuracy = 0.9211  
xps:1432747515:1:tester:confusion accuracy = 0.9292 
==> epoch # 2 for optimizer :   
==> example speed = 4526.7213369494 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.0072034133582363   
xps:1432747515:1:optimizer:confusion accuracy = 0.93302 
xps:1432747515:1:validator:confusion accuracy = 0.9405  
xps:1432747515:1:tester:confusion accuracy = 0.9428 
==> epoch # 3 for optimizer :   
==> example speed = 4486.8207535058 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.0056732489919492   
xps:1432747515:1:optimizer:confusion accuracy = 0.94704 
xps:1432747515:1:validator:confusion accuracy = 0.9512  
xps:1432747515:1:tester:confusion accuracy = 0.9518 
==> epoch # 4 for optimizer :   
==> example speed = 4524.4831336064 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.0047361240094285   
xps:1432747515:1:optimizer:confusion accuracy = 0.95672 
xps:1432747515:1:validator:confusion accuracy = 0.9565  
xps:1432747515:1:tester:confusion accuracy = 0.9584 
==> epoch # 5 for optimizer :   
==> example speed = 4527.7260154406 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.0041567858616232   
xps:1432747515:1:optimizer:confusion accuracy = 0.96188 
xps:1432747515:1:validator:confusion accuracy = 0.9603  
xps:1432747515:1:tester:confusion accuracy = 0.9613 
SaveToFile: saving to /home/nicholas/save/xps:1432747515:1.dat  
==> epoch # 6 for optimizer :   
==> example speed = 4519.2735741475 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.0037086909102431   
xps:1432747515:1:optimizer:confusion accuracy = 0.9665  
xps:1432747515:1:validator:confusion accuracy = 0.9602  
xps:1432747515:1:tester:confusion accuracy = 0.9629 
==> epoch # 7 for optimizer :   
==> example speed = 4528.1356378239 examples/s  
xps:1432747515:1:optimizer:loss avgErr 0.0033203622647625   
xps:1432747515:1:optimizer:confusion accuracy = 0.97062 
xps:1432747515:1:validator:confusion accuracy = 0.966   
xps:1432747515:1:tester:confusion accuracy = 0.9665 
SaveToFile: saving to /home/nicholas/save/xps:1432747515:1.dat  

在5个时期之后,实验将具有最低 xps:1432747515:1:validator:confusion accuracy版本的模型保存到磁盘来实施早期停止。 该字符串的第一部分(xps:1432747515:1)代表了是实验的唯一ID。它包括计算机的主机名(在本例中为xps)和时间戳。

加载已保存的实验

实验保存在/home/nicholas/save/xps:1432747515:1.dat。 您可以加载它并访问模型:

require 'dp'
require 'cuda' -- 如果你使用cmd-line参数 --cuda
require 'optim'

xp = torch.load("/home/nicholas/save/xps:1432747515:1.dat")
model = xp:model()
print(torch.type(model))
nn.Serial

为了提高效率,这里的模型使用nn.Serial。 您可以通过添加以下内容来访问模型:

model = model.module
print(torch.type(model))
nn.Sequential

超参数优化

超优化是深度学习中最难的部分。 在许多方面,感觉它更像是一门艺术而不是科学。

一人dp用户曾经提出这样一个问题:

如果我尝试使用自定义数据集训练NN,该如何优化参数? 我的意思是,如果NN需要大约3-5天来完成训练..你如何进行小型实验/调整(学习率,每层节点) - 这样你就不用花5天时间 - 基本上什么是最快的方法找到最佳参数?

这是我的回答:

在我看来,它是经验和计算资源的混合体。 第一个来自于使用不同的数据集和模型组合。 最后,你只知道什么样的模型运作良好。 至于第二个,它允许您同时尝试不同的超参数。 如果您可以访问4-8个GPU,那么可以同时尝试4-8个实验。 您还可以明显地看到其中一些实验比其他实验要慢得多。 在这种情况下,你可以杀死他们并尝试其他有效的方法。 此外,最重要的超参数是学习率。 找到不会使培训发散的最高值,这样你的实验就会更快收敛。 您也可以将其衰减到最后以获得一定的准确性提升(并非总是如此)。

首先,我建议使用maxParamNorm进行正则化。 通常max_out_norm等于2是一个很好的出发点,也可尝试1,10,只有在特殊情况时才尝试-1。其次,您可以改变epoch大小,以更好地划分评估和训练之间的处理时间。通常最好尽可能的情况下保持交叉验证集较小(例如,少于所有数据的10%)。除此之外,训练数据越多越好。

但这些都是常见的指导方针。没有人能告诉你具体如何进行超参数的优化。您需要尝试自己优化数据集,以找到适合自己的方法和技巧。dp GitHub repository还提供了一个wiki,可用于共享超参数配置以及相应的性能指标和观察。

最后,作为一个团队进行超参数优化比单独进行更容易。每个人都会在某个方面有自己独特的见解。例如,不久前我被提醒了一个好的表格对于跟踪实验的重要性。这个人的cmd-line参数中的每个超参数都有一列,错误/准确度和观察都有一列,每个实验都有一行。对我来说,我懒得自己制作这个漂亮的电子表格,我只是用纸和笔......但后来我注意到他找到最好的超参数配置是多么容易。无论如何,我从这个人身上学到了不少。因此,请继续关注超参数优化艺术中的课程。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值