神经网络的训练和误差反向传播
神经网络的训练,简单的说就是通过梯度下降,沿着代价函数的梯度向量反方向生成一个反向量,由此得到关于神经单元的新的权重值和截距,以使得神经网络的精度得以提高。
误差反向传播,是这一过程的快速算法。具体的实现我这里不过多展开,还是那句话,推荐有兴趣的读者去阅读《深度学习的数学》,这几章仅仅是对此书的一篇学习笔记。简单来说,我们需要先求得误差函数在每个 zeta 上的偏导数,这里称为 delta 值,通过 delta 值,我们可以简单快速的求得每个 weight 和 b 对 Cost 的导数值。再根据学习率 eta ,可以得到我们在当次训练中需要对 weight 或 b 做的修正。
所以,求解 alpha 的时候,我们是先得到输入层的值,再递归的逐层向后求出每一层的值。而求解 delta 的时候,我们先由 alpha 和 t 得到输出层的 delta ,再逐层向前求出所有隐藏层的值,输入层由于总是将输入值作为 alpha,不需要进行训练。
输出层每一个节点的 delta 都很容易得到:
(defnoutput-delta
"这里仅针对点火函数为 sigmoid 的输出层神经元给出 delta 值,接收的参数为 alpha值,zeta 和正值"
[a z t]
(*(-a t) (d-sigmoid z)))
如果得到了一整层 delta 后,上一层的每个节点的 delta 也很容易得到:
(defnresolve-delta
"利用误差反向传播法递推指定节点的误差 delta 值。参数为节点的 idx,计算结果,下一层节点和下一层的结果"
[idx node-result next-layer next-layer-results]
(let[weights (map#(->% :w (nthidx)) next-layer)
deltas (map:d next-layer-results)]
(*(reduce +0 (map *weights deltas))
(d-sigmoid (:z node-result)))))
但是其实利用输出层 delta 求解隐藏层 delta 的过程,我实现的意外的麻烦:
(defnupdate-hidden
"依据已得到输出层 delta 的结果集,更新指定网络的隐藏层结果,将求得的 delta 附在结果集中"
[result network]
(loop[layer-index (-(countresult) 2) result result]
(if(=layer-index 0)
result
(recur
(declayer-index)
(update
result
layer-index
(fn[layer]
(map-indexed
(fn[indexnode]
(assocnode
:d
(resolve-delta
indexnode
(nthnetwork (inclayer-index))
(nthresult (inclayer-index)))))
layer)))))))
这里,我猜用一个“平凡”的编程语言,比如java,可能写起来更简单,因为这里的计算逻辑都是依据索引定位的。
有了这些函数,我们就能根据结果集和正值集合,得到一个带有 delta 值的集合:
(defnupdate-delta
"根据结果集和正值生成整个网络(隐藏层和输出层)的误差,生成与结果集合并的结果"
[result t-vector network]
(let[last-index (->result countdec)]
(->result
(update last-index (fn[layer] (map(fn[nodet]
(assoc node:d (output-delta (:a node) (:z node) t)))
layer t-vector)))
(update-hidden network))))
顾名思义,update-delta 会生成一个新的集合,这里面包含神经网络的计算结果,并且在每一个节点都附带求得的 delta 。将它们合并在一个新的集合中,可以更方便的进行后续的计算:
在求得delta的前提下,可以通过一组函数,方便的求得各节点的权重和截距的偏导数:
(defnnode-differential
"单节点微分函数,根据当前节点的计算结果(delta)以及上一层节点的计算结果(alpha),计算权重和偏置量的偏微分结果"
[result prev-layer-results]
{:w (vec (map#(*(:d result) (:a %)) prev-layer-results))
:b (:d result)})
(defnlayer-differential
"层微分函数,根据当前层和上一层的计算结果,生成整层的微分结果。"
[layer-results prev-layer-results]
(vec (map#(node-differential % prev-layer-results) layer-results)))
(defndifferential
"根据 delta 结果集求得神经元网络的偏微分结果集"
[result]
(loop[layer-index 1 dataset [(firstresult)]]
(if(=(->result count) layer-index)
dataset
(let[next-layer (inclayer-index)]
(recur next-layer
(assocdataset
layer-index
(layer-differential
(nthresult layer-index)
(nthresult (declayer-index)))))))))
再根据偏导数的值和学习率 eta ,我们就可以对神经网络进行修正了:
(defnnode-fix
"跟据给定的 offset 数据集生成修正后的 node"
[nodeoffset]
(->node
(update :w #(vec (map -% (:w offset))))
(update :b -(:b offset))))
(defnlayer-fix
"根据给定的 offset 层生成修订后的 layer"
[layer offset]
(mapnode-fix layer offset))
这个训练过程可以封装进一个 train 函数:
(defntrain
"根据 delta 结果集对神经网络进行训练"
[network eta deltas]
(reduceconj
[(firstnetwork)]
(maplayer-fix
(restnetwork)
(rest(create-fix eta (mapdifferential deltas))))))
那么,给定了期望达到的误差范围,我们可以将训练过程自动化,让程序反复进行训练,直至 cost 得到的误差足够小:
(defntrain
[dataset t-set network eta d]
(loop[network network]
(let[delta-set (->dataset
(resolve-results network)
(resolve-deltas t-set network))
w (total-cost delta-set t-set)]
(printlnw)
(if(
network
(recur (n/train network eta (valsdelta-set)))))))
封装单次训练的 train 函数,在 liu.mars.ml.neural 中,而封装自动化训练过程的 train函数,我放在了 liu.mars.ml.repl 中,以便在 repl 环境中调用。
(defnew-network (learn dataset t-all network eta 0.1))
在下图中,我们可以形象的看出梯度下降的过程中误差逐步减小
现在,我们可以用新生成的神经网络,重新计算一组结果,看一下它的准确程度:
(defresults
(resolve-results dataset new-network))
(into(sorted-map)
(map#(vector(key%) (->> % val last(map:a) n/binary-pair))
results))
>>>
{1 [1 0],
2 [1 0],
3 [1 0],
4 [1 0],
5 [1 0],
6 [1 0],
7 [1 0],
...
53 [0 1],
54 [0 1],
55 [0 1],
56 [0 1],
57 [0 1],
58 [0 1],
59 [0 1],
60 [0 1],
61 [0 1],
62 [0 1],
63 [0 1],
64 [0 1]}
在我的这次实验中,新的神经网络已经能够达到足够好的效果。
在今年十月份的上海 QCon ,我会向大家演示同一个算法的 PostgreSQL 实现。