torch 深度學習(3)
前面我們已經完成對數據的預處理和模型的構建,那么接下來為了訓練模型應該定義模型的損失函數,然后使用BP算法對模型參數進行調整
損失函數 Criterion
加載包
require 'torch'
require 'nn' -- 各種損失函數也是 'nn'這個模塊里面的
設定命令行參數
if not opt then
print "==> processing options:"
cmd = torch.CmdLine()
cmd:text()
cmd:text('Options:')
cmd:text()
cmd:option('-loss','nll','type of loss function to minimize: nll | mse | margin')
-- nll: negative log-likelihood; mse:mean-square error; margin: margin loss(SVM 類似的最大間隔准則)
cmd:text()
opt=cmd:parse(arg or {})
model = nn. Sequential()
-- 這個model主要是為了能夠使該損失函數文件能夠單獨運行,最后運行整個項目時,並不會執行到這里
end
定義損失函數
noutputs = 10 -- 這個主要是 mse 損失函數會用到
if opt.loss == 'margin' then
criterion = nn.MultiMarginCriterion()
elseif opt.loss == 'nll' then
-- 由於negative log-likelihood 計算需要輸入是一種概率分布,所以需要對模型輸出進行適當的歸一化,一般可以使用 logsoftmax層
model:add(nn.LogSoftMax()) --注意這里輸出的是向量,概率分布
criterion = nn.NLLCriterion()
elseif opt.loss = 'mse' then
-- 這個損失函數用於數據的擬合,而不是數據的分類,因為對於分類問題,只要分正確就可以,沒必要非得和標號一致。而且對於分類問題,比如兩類,可以標號為 1,2,也可以標號為3,4,擬合並沒有實際意義。
-- 這里主要是順便了解一下如何定義,並不會用到這個損失函數
criterion = nn.MSECriterion()
-- Compared to the other losses, MSE criterion needs a distribution as a target, instead of an index.
-- So we need to transform the entire label vectors:
if trainData then
-- convert training labels
local trsize = (#trainData.labels)[1]
local trlabels = torch.Tensor(trsize,noutputs)
trlabels:fill(-1)
for i=1,trsize then
trlabels[{i,trainData.labels[1]}] =1 -- 1表示屬於該類
end
trainData.labels=trlabels
-- convert test labels
local tesize = testData.labels:size()[1]
local telabels = torch.Tensor(tesize,noutputs):fill(-1)
for i=1,tesize do
telabels[{{i},{testData.labels[i]}}]=1
end
testData.labels=telabels
end
else
error('unknown -loss')
end
print ('損失函數為')
print (criterion)
可以發現損失函數的定義很簡單,都是一句話的事,只是在調用對應的損失函數時要注意損失函數的輸入輸出形式。更多的損失函數定義和使用方法見torch/nn/Criterions
模型的訓練
加載模塊
require 'torch'
require 'xlua' -- 主要用於顯示進度條
require 'optim' -- 包含各種優化算法,以及混淆矩陣
預定義命令行
if not opt then
print '==> processiing options:'
cmd=torch.CmdLine()
cmd:text()
cmd:text('options:')
cmd:text()
cmd:option('-save','results','subdirectory to save/log experiments in') --結果保存路徑
cmd:option('-visualize',false,'visualize input data and weights during training')
cmd:option('-plot',false,'live plot') -- 這兩個參數可以參見optim/Logger的用法
-- 下面的幾個參數就是關於優化函數和對應參數的了
cmd:option('-optimization','SGD','optimization method: SGD | ASGD | CG | LBFGS')
-- 分別是隨機梯度下降法、平均梯度下降法、共軛梯度法、線性BFGS搜索方法
cmd:option('-learningRate',1e-3,'learning rate at t=0') -- 步長
cmd:option('-batchSize',1,'mini-batch size (1 = pure stochastic)') -- 批量梯度下降法的大小,當大小為1時就是隨機梯度下降法
cmd:option('-weightDecay',0,'weight decay (SGD only)') -- 正則項系數衰減速度
cmd:option('-momentum',0,'momentum (SGD only)') --慣性系數
cmd:option('-t0',1, 'start averaging at t0 (ASGD only) in nb of epochs) cmd:option('-maxIter',2,'maximum nb of iterations for CG and LBFGS') --最大迭代次數,CG和LBFGS使用 cmd:text() end
這里要說明下。傳統的隨機梯度下降法,一般就是,其中
是上一步的梯度,
是學習速率,就是步長,步長太大容易導致震盪,步長太小容易導致收斂較慢且可能掉進局部最優點,所以,一般算法開始時會有相對大一點的步長,然后步長會逐步衰減。
為了使BP算法有更好的收斂性能,可以在權值的更新過程中引入“慣性項”,也就是上一次的梯度方向和這一次梯度方向的合成方向作為新的搜索方向,,這里的慣性系數
就是參數momentum
正則項主要是為了防止模型過擬合,控制模型的復雜度。
定義了一些分析工具
classes = {'1','2','3','4','5','6','7','8','9','0'}
confusion = optim.ConfusionMatrix(classes) -- 定義混淆矩陣用於評價模型性能,后續計算正確率,召回率等
trainLogger = optim.Logger(paths.concat(opt.save,'train.log'))
testLogger = optim.Logger(paths.concat(opt.save,'test.log'))
-- 創建了兩個記錄器,保存訓練日志和測試日志
混淆矩陣參見混淆矩陣,optim里面的ConfusionMatrix 主要使用到的有三個量 一個是 valid,也就是召回率 TPR(True Positive Rate), 一個是 unionValid,這個值是召回率和正確率的一個綜合值 unionValid = M(t,t)/(行和+列和-M(t,t)),M(t,t)表示矩陣對角線的第t個值
最后一個就是整體的評價指標 totalValid = sum(diag(M))/sum(M(:))
開始訓練
if model then
parameters,gradParameters = model:getParameters()
end
注意 torch中模型參數更新方式有兩種,一種直接調用函數updateParameters(learningRate)更新,另一種就要手工更新,即parameters:add(-learningRate,gradParameters),具體請參看torch/nn/overview
接下來定義訓練函數
function train()
epoch = epoch or 1
-- 所有樣本循環的次數
local time = sys.clock() -- 當前時間
shuffle =torch.randperm(trsize) -- 將樣本次序隨機排列permutation
for t=1,trsize,opt.batchSize do --批處理,批梯度下降
xlua.progress(t,trainData:size()) --進度條
inputs={} --存儲該批次的輸入
targets ={} -- 存儲該批次的真實標簽
for i=t,math.min(t+opt.batchSize-1,trainData:size()) do --min操作是處理不能整分的情況
local input = trainData.data[shuffle[i]]:double()
local target = trainData.labels[shuffle[i]]
table.insert(inputs,input)
table.inset(targets,target)
end
-- 定義局部函數,這個函數作為優化函數的接口函數
local feval = function(x)
if x~=parameters then
parameters:copy(x)
end
gradParameters:zero() -- 每一次更新過程都要清零梯度
local f=0 -- 累積誤差
for i=1,#inputs do
local output = model:forward(inputs[i])
local err = criterion:forward(output,targets[i]) -- 前向計算
f=f+err -- 累積誤差
local df_do = criterion:backward(output,targets[i]) -- 反向計算損失層梯度
model:backward(inputs[i],df_do) -- 反向計算梯度,這里的梯度已將保存到gradParameters中,下面會解釋為什么
local _, indice = torch.sort(output,true)
confusion:add(indices[1],targets[i])
-- 更新混淆矩陣,參數分別為預測值和真實值,add操作是在混淆矩陣的[真實值][預測值]位置加1
-- ==Note==需要注意的是,教程上這里代碼錯了,他沒有對output進行排序,而是直接將output放入confusion的更新參數中,但是output是一個向量,那樣會導致得到的矩陣只有一行更新。。。我排查了好久。。。
end
gradParamters:div(#inputs)
f=f/#inputs
-- 因為是批處理,所以這里應該計算均值
return f, gradParameters
end
-- feval 這個函數的形式可以參見優化方法的定義,下面有鏈接
-- 開始優化
if opt.optimization == 'CG' then
config = config or {maxiter = opt.maxIter}
optim.cg(feval,parameters,config)
elseif opt.optimization == 'SGD' then
config =config or {learning = opt.learningRate,
weightDecay = opt.weightDecay,
learningRateDecay = 5e-7} --最后一個參數是步長的衰減速率
optim.sgd(feval,parameters,config)
elseif opt.optimization=='LBFGS' then
config =config or {learning = opt.learningRate,
maxIter =opt.maxIter,
nCorrection = 10}
optim.lbfgs(feval,parameters,config)
elseif opt.optimization=='ASGD' then
config = config or {eta0 = opt.learningRate, t0 = trsize*opt.t0}
_,_,average = optim.asgd(feval,parameters,config)
else
error ('unknown -optimization method')
end
end
-- 這里關於各種優化函數的原型請參考[1]
-- 遍歷一次進行記錄
time =sys.clock()-time --時間
time =time/trainData:size() -- 平均時間
print(confusion) --這里顯示了混淆矩陣
-- confusion:zero() --混淆矩陣清零為了下一次遍歷 注意!文檔中這句話也放錯了位置,因為還沒log不能清空,應該放到后面
trainLogger:add{['% mean class accuracy (train set)'] = confusion.totalValid*100} -- 這個地方保存的是 accuracy
if opt.plot then
trainLogger:style{['% mean class accuracy (train set)']='-'}
trainLogger.plot() -- 繪制隨着迭代進行,結果的變化趨勢圖
end
confusion:zero() --混淆矩陣清零為了下一次遍歷 應該放到這里
local filename = paths.concat(opt.save,'model.net')
os.excute('mkdir -p ' .. sys.dirname(filename)) --創建文件
torch.save(filename,model) --在新文件中保存模型
epoch =epoch+1
end
這里稍微有點難以理解的是,每一次計算梯度,梯度是怎么更新的呢?我們並沒有顯示的見到梯度是如何更新的。
這主要是因為 'parameters,gradParameters = model:getParameters()'這個函數其實返回的是指針,然后在優化函數中對參數進行了更新,比如我們看看 sgd中有部分代碼
...
x:add(-clr,state.deltaParameters)
else
x:add(-clr,dfdx)
end
這里x就是 我們調用時輸入的parameters指針,dfdx就是調用的函數feval返回的gradParameters指針。
另外 'model:backward(inputs[i],df_do)'函數內部修改了gradParamters上的值,因為指針傳遞,所以沒有返回值。
補充一點 epoch,batchSize和iteration關系
隨機梯度法是將所有的樣本一次送到模型中進行訓練,那么后輸入的樣本調整了模型后並不能保證之前的樣本獲得的結果仍然很好,這時候就要重復的輸入樣本,讓系統慢慢慢慢的收斂到對所有的樣本都能有一個較好的結果。
而1個epoch就等於將所有的訓練集中的樣本訓練一次
1個batchSize是每次進行梯度更新所采用的樣本的個數,如果batchsize=1的話就是最簡單的隨機梯度下降法,batchSize=#{訓練集},那么就是梯度下降法
1個iteration 等於使用batchsize個樣本訓練一次
實驗結果
這里就不給結果了,等下一節,學習了如何測試數據,同時給出模型訓練結果,和測試結果的變化。