RNN公式详细推导(结合广为流传的八位二进制加法代码)与代码详解

最近在查找RNN的公式详解中,发现大多数的公式都是一通链式求导结束,与网络上广为流传的八位二进制代码的计算过程完全不符,经过研究推导,觉得有必要给后来的同学推导一下代码中的计算过程。(代码附在文后)

先上图,

 

 

RNN的结构图都是这样,按时间展开,S是隐藏层和输出层的激活函数,这里用Sigmoid,求导比较特殊,W是权重角标表示走向,O是输出层,I是输入层,H是隐藏层,a是单元的输入,b是单元的输出,上下角标是层和时间序列,x是输入层的输入,这里方便,就不给单元设置阈值了,有的代码里会有一个阈值,一般用b表示,影响不大。

神经网络的目标就是通过调整W和阈值b(不是单元的输出的b啊),来让误差变小,所谓的网络就是误差足够小的那些W和阈值b。

再上公式,简单的前向传播

再定义一个损失函数,就是上面说的优化问题的目标函数,一般为了好求导这样给出

 

这都比较简单,麻烦的是反向传播,先看简单的隐藏层到输入层的转换矩阵的所谓梯度 

 接下来大的两个权重矩阵的梯度就是麻烦了,很多的文章这里就开始含糊不清,我尽量给读者写清楚,需要注意,两个矩阵的梯度都是会受到未来时刻的影响的。

首先,还是继续t时刻的蓝色线的来自输出层的反向传播

接着要看红色的这条反向传播 ,需要注意,这里的t+1不单单指t+1时刻的,而是t时刻未来的所有时刻,因为t+1时刻会包含t+2时刻的信息,这一点会在之后的误差项定义中完美体现

两条反向传播先不着急合再一起 

来看代码里的前向传播部分,先把注释删掉,文后的代码会有注释

里面的函数在这

 麻烦的地方在第四句,其余都好懂,这个layer_2_deltas是什么鬼,再来看一下它的用处

给出反向传播和权重更新

下面这三行代码就是三个矩阵的更新,后面那三个加上去的就是他们对应的梯度,也就是那三个链式求导,1是隐藏层到输出层的,h是隐藏层到隐藏层的,0是输入层到隐藏层的 

先分析一下这个

 包含两部分,左边的误差乘以后边的函数,前边的误差出现时因为特殊的损失函数求导后出现的,后边的函数是

这一环是链式求导里频繁出现的一环,推理确实很麻烦,直接给出结果吧 

麻烦的地方在于layer_1_delta和 layer_2_delta是什么鬼,先给出结果,这两个是误差项,定义如下,

误差项就是对该层的输入求偏导 ,那么代码里的layer_2_delta就是输出层的误差项,即损失函数对输出层的输入求得偏导

那么隐藏层到输出层矩阵的梯度就可以写成

接着看隐藏层的误差项layer_1_delta,即损失函数对隐藏层的输入求导,直接把两部分梯度结合起来

需要说明的是,这个式子里包含了t+1时刻的隐藏层的误差项,代码里实现就是反向传播的计算方向和前向传播的计算方向是相反的,这样反向传播计算机会先得到下一次循环的”下一时刻“的隐藏层误差项,可以让变量保存下来。

有了 隐藏层的误差项,给最后一个矩阵求梯度就好算了

 算好梯度后,就可以计算权重矩阵的更新

后面的常数是步长,或者学习速率。

代码附上

% implementation of RNN 
clc
clear
close all
%% training dataset generation
binary_dim = 8;

largest_number = 2^binary_dim - 1;
binary = cell(largest_number,1);
int2binary = cell(largest_number,1);
for i = 1:largest_number+1
    binary{i} = dec2bin(i-1, 8);
    int2binary{i} = binary{i};
end

%% input variables
alpha = 0.1;
input_dim = 2;
hidden_dim = 16;
output_dim = 1;

%% initialize neural network weights
synapse_0 = 2*rand(input_dim,hidden_dim) - 1;
synapse_1 = 2*rand(hidden_dim,output_dim) - 1;
synapse_h = 2*rand(hidden_dim,hidden_dim) - 1;%产生[-1,1]之间的随机数

synapse_0_update = zeros(size(synapse_0));
synapse_1_update = zeros(size(synapse_1));
synapse_h_update = zeros(size(synapse_h));

%% train logic
for j = 0:19999 
    % generate a simple addition problem (a + b = c)
    a_int = randi(round(largest_number/2)); % int version 产生一个小于128的随机数
    a = int2binary{a_int+1}; % binary encoding
    
    b_int = randi(floor(largest_number/2)); % int version 产生一个小于127的随机数
    b = int2binary{b_int+1}; % binary encoding
    
    % true answer
    c_int = a_int + b_int;
    c = int2binary{c_int+1};
    
    % where we'll store our best guess (binary encoded)
    d = zeros(size(c));%c的维数是1*8
    
    if length(d)<8
        pause;
    end
    
    overallError = 0;
    
    layer_2_deltas = [];
    layer_1_values = [];
    layer_1_values = [layer_1_values; zeros(1, hidden_dim)];
    
    % 开始对一个序列进行处理,搞清楚一个东西,一个LSTM单元的输出其实就是隐含层
    for position = 0:binary_dim-1
        X = [a(binary_dim - position)-'0' b(binary_dim - position)-'0'];   % X 是 input
        y = [c(binary_dim - position)-'0']';                               % Y 是label,用来计算最后误差
        
        % 这里是RNN,因此隐含层比较简单
        % X ------------------------> input
        % sunapse_0 ----------------> U_i
        % layer_1_values(end, :) ---> previous hidden layer (S(t-1))
        % synapse_h ----------------> W_i
        % layer_1 ------------------> new hidden layer (S(t))

        layer_1 = sigmoid(X*synapse_0 + layer_1_values(end, :)*synapse_h);
        
        % layer_1 ------------------> hidden layer (S(t))
        % layer_2 ------------------> 最终的输出结果,其维度应该与 label (Y) 的维度是一致的
        % 这里的 sigmoid 其实就是一个变换,将 hidden layer (size: 1 x 16) 变换为 1 x 1
        % 有些时候,如果输入与输出不匹配的话,使可以使用 softmax 进行变化的
        % output layer (new binary representation)
        layer_2 = sigmoid(layer_1*synapse_1);
        
        % 计算误差,根据误差进行反向传播
        % layer_2_error ------------> 此次(第 position+1 次的误差)
        % l 是真实结果
        % layer_2 是输出结果
        % layer_2_deltas 输出层的变化结果,使用了反向传播,见那个求导(输出层的输入是 layer_2,那就对输入求导即可,然后乘以误差就可以得到输出的diff)
        % did we miss?... if so, by how much?
        layer_2_error = y - layer_2;
        layer_2_deltas = [layer_2_deltas; layer_2_error*sigmoid_output_to_derivative(layer_2)];
        
        % 总体的误差(误差有正有负,用绝对值)
        overallError = overallError + abs(layer_2_error(1));
        
        % decode estimate so we can print it out
        % 就是记录此位置的输出,用于显示结果
        d(binary_dim - position) = round(layer_2(1));
        
        % 记录下此次的隐含层 (S(t))
        % store hidden layer so we can use it in the next timestep
        layer_1_values = [layer_1_values; layer_1];
    end
    
    % 计算隐含层的diff,用于求参数的变化,并用来更新参数,还是每一个timestep来进行计算
    future_layer_1_delta = zeros(1, hidden_dim);
    
    % 开始进行反向传播,计算 hidden_layer 的diff,以及参数的 diff
    for position = 0:binary_dim-1
        % 因为是通过输入得到隐含层,因此这里还是需要用到输入的
        % a -> (operation) -> y, x_diff = derivative(x) * y_diff
        % 注意这里从最后开始往前推
        X = [a(position+1)-'0' b(position+1)-'0'];
        % layer_1 -----------------> 表示隐含层 hidden_layer (S(t))
        % prev_layer_1 ------------> (S(t-1))
        layer_1 = layer_1_values(end-position, :);
        prev_layer_1 = layer_1_values(end-position-1, :);
        
        % layer_2_delta -----------> 就是隐含层的diff
        % hidden_layer_diff,根据这个可以推算输入的diff以及上一个隐含层的diff
        % error at output layer 输出层的误差项
        layer_2_delta = layer_2_deltas(end-position, :);
        % 这个地方的 hidden_layer 来自两个方面,因为 hidden_layer -> next timestep, hidden_layer -> output,
        % 因此其反向传播也是两方面
        % error at hidden layer 隐藏层的误差项
        layer_1_delta = (future_layer_1_delta*(synapse_h') + layer_2_delta*(synapse_1')).* sigmoid_output_to_derivative(layer_1);
        
        % let's update all our weights so we can try again
        synapse_1_update = synapse_1_update + (layer_1')*(layer_2_delta);
        synapse_h_update = synapse_h_update + (prev_layer_1')*(layer_1_delta);
        synapse_0_update = synapse_0_update + (X')*(layer_1_delta);
        
        future_layer_1_delta = layer_1_delta;
    end
    
    synapse_0 = synapse_0 + synapse_0_update * alpha;
    synapse_1 = synapse_1 + synapse_1_update * alpha;
    synapse_h = synapse_h + synapse_h_update * alpha;
    
    synapse_0_update = synapse_0_update * 0;
    synapse_1_update = synapse_1_update * 0;
    synapse_h_update = synapse_h_update * 0;
    
    if(mod(j,1000) == 0)%余数
        err = sprintf('Error:%s\n', num2str(overallError)); fprintf(err);
        d = bin2dec(num2str(d));
        pred = sprintf('Pred:%s\n',dec2bin(d,8)); fprintf(pred);
        Tru = sprintf('True:%s\n', num2str(c)); fprintf(Tru);
        out = 0;
        size(c)
        sep = sprintf('-------------\n'); fprintf(sep);
    end
    
end
function y = sigmoid(x)
    n = length(x);
    for i = 1:n
        y(i) = 1/(1+exp(-x(i)));
    end
end
function y = sigmoid_output_to_derivative(x)
    n = length(x);
    for i = 1:n
        y(i) = x(i)*(1-x(i));
    end
end

 兄弟也是第一次写,如果你看到结尾了,就给点一个赞支持一下吧,会接着这种公式加代码的讲述风格继续努力的。

 

 

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值