虽然名字叫「反向传播」,但其实原理跟「反向」毛关系都没有。
首先,在输出层之后加上一个节点 C(即 cost function )。
现在,直接用链式求导法则就行了。
因为神经网络同一层内部不允许互联,只可能层级之间互联,所以只可能这三种情况。这样就可以用递归解决了。考虑到很多偏导数会多次用到,可以考虑用动态规划把中间结果存起来[1]。
另外,C 节点的偏导数和其他节点不同,但也很简单。
将以上结论化简,很容易得到「How the backpropagation algorithm works」中的结论。
明明是几百年前数学上搞出来的链式法则,偏偏取个「反向传播」的名号。数学家看了一定会发笑。要是数学论文也这么写,那每求一个具体函数的导数都可以发一篇《关于某某函数的快速算法与实现》。
不过说到「取名字的本事」,各个学科都大哥不要笑二哥。
那些神奇的 bug 🐞
用「异或问题」小试了一把。跑 5000 次,初始权重随机。最后结果倒还不错,只是:
为什么 total error 变化会这么剧烈?!☹️
本来不想贴的,不过想想反正也没什么人看,丢脸就丢一点吧。
require 'pp'
class Array
def second
self[1]
end
end
class Network
def initialize(structure)
@learning_rate = 0.5
@weights = [nil]
structure[1..-1].each_with_index do |num_of_neuron, layer|
ws = []
num_of_neuron.times do
cs = []
(structure[layer] + 1).times { cs << rand } # weights[layer][0] is the bias.
ws << cs
end
@weights << ws
end
end
def sigmoid(z) 1.0 / (1 + Math.exp(-z)) end
def sigmoid_prime(z) sigmoid(z) * (1 - sigmoid(z)) end
def dot_product(v1, v2) v1.zip(v2).map { |a,b| a*b }.inject {|sum,el| sum+el} end
def v_minus(v1, v2) v1.zip(v2).map { |a, b| a - b } end
def m_column(m, idx)
col = []
m.each do |row|
col << row[idx]
end
col
end
def a(layer, x) # activation
return x if layer == 0
z(layer, x).map { |a| sigmoid(a)}
end
def z(layer, x)
return x if layer == 0
zs = []
a_last_layer = a(layer - 1, x).dup.unshift(1)
@weights[layer].each do |ws|
zs << dot_product(a_last_layer, ws)
end
zs
end
def output(x) a(@weights.size - 1, x) end
def delta(layer, x, y)
if layer == @weights.size
# layer L
return [dot_product( v_minus(a(layer - 1, x) , [y]), z(layer - 1, x).map {|v| sigmoid_prime(v)} )]
else
# layer l
ds = []
ws = @weights[layer + 1] || [[1]]
ws.first.size.times do |j|
ds << dot_product( m_column(ws, j), delta(layer + 1, x, y) )
end
ds
end
end
def pd(layer, from_node, to_node, x, y)
if from_node == 0
c = 1
else
# p "a: #{a(layer - 1, x)}, from: #{from_node}"
c = a(layer - 1, x)[from_node-1]
end
# p "c: #{c}"
c * delta(layer, x, y)[to_node]
end
def train(x, y)
@weights.each_with_index do |ws, layer|
next if layer == 0
ws.each_with_index do |w_to, to_node|
w_to.each_with_index do |w, from_node|
# p "l: #{layer}, from: #{from_node}, to: #{to_node}, w: #{w}"
@weights[layer][to_node][from_node] = w - @learning_rate * pd(layer, from_node, to_node, x, y)
end
end
end
end
def cost(x, y) 0.5 * (a(2, x).first - y)**2 end
def fit(training_set, epochs)
min_cost = 1000
op_w = []
trace = []
epochs.times do
total_cost = 0
training_set.each do |sample|
train(sample.first, sample.second)
total_cost += cost(sample.first, sample.second)
end
# p "cost: #{total_cost}"
# p total_cost
trace << total_cost
if total_cost < min_cost
min_cost = total_cost
op_w = @weights.dup
end
end
[op_w, trace]
end
end
pp nn = Network.new([2, 3, 1])
training_set = [[[1, 1], 0],
[[-1, -1], 0],
[[1, -1], 1],
[[-1, 1], 1]]
pp nn.output(training_set[0].first)
pp nn.output(training_set[1].first)
pp nn.output(training_set[2].first)
pp nn.output(training_set[3].first)
puts "-----------------------"
w, trace = nn.fit(training_set, 5000)
pp w, trace
puts "-----------------------"
pp nn.output(training_set[0].first)
pp nn.output(training_set[1].first)
pp nn.output(training_set[2].first)
pp nn.output(training_set[3].first)
这个版本没有采用动态规划把中间结果缓存下来。这其实会导致严重的效率问题。
关于神经网络的思考
我们通过构造一个两层神经网络达成了目标,可它到底是如何办到的呢?
我们仔细观察它的构造:第一层实质上是一个从二维平面到三维空间的映射;第二层是通过在三维空间中切了一刀,完成了分类。
是的,我们知道它大概是怎么回事,可是很难直观地「看到」具体发生了什么。
-
只有这时候才能与「反向」搭上关系,因为计算实质上是从最末一个节点开始的。 ↩