“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

之前一直都是直接使用深度学习的框架,但对里面所涉及到的基本算法却没有深入研究。看了吴恩达的机器学习视频之后,决定使用MATLAB实现一个简单的神经网络,深刻体会到只有用代码从头实现一个算法,才会对这个算法理解得更加深刻,也才能真正掌握该算法。

机器学习定义如下:一个程序被认为能从经验E中学习,解决任务T,达到性能P,当且仅当,有了经验E之后,经过度量P的评判,程序在处理T的性能有所提升。

神经网络是机器学习中的一类算法,在训练过程中,神经网络内部不断地调整其内部参数的大小,使得神经网络的输出不断地向标签靠拢。其中,“调整参数大小”这个过程一般是“梯度下降”,在一个多层的神经网络中,要达到“梯度下降”这个目的,就不得不提一下反向传播算法(Backpropagation Algorithm, BP)。

BP算法的核心就是链式求导法则,这里对BP算法的理论推导如下,主要参考[1]-[3]。

如图所示,考虑一个有三层、每层有两个神经元的神经网络:

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

上图为决策面可视化结果,蓝色为判为(0,1)与(1,0)的区域,红色为判为(0,0),(1,1)的区域。

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

“手撕”BP算法——使用MATLAB搭建简单的神经网络(附代码)

上图为修改损失函数为交叉熵时的结果,学习率同样为0.5,可以看出在400次左右便达到收敛,因此损失函数的设计对整个神经网络的参数调整至关重要,至于为什么将损失函数调整为交叉熵后,收敛速度会增加,可自行查阅资料,这里给出一个结论:当输出层采用线性激活函数时,损失函数采用MSE会收敛更快;当输出层采用sigmoid函数时,一般采用交叉熵会收敛更快,且不容易陷入局部最优的情况。

此外,实验表明,学习率对神经网络的收敛也有比较大的影响。当学习率较大时,有可能收敛较快,当然也可能根本不收敛;当学习率较小时,容易进入局部最小值,且收敛较慢。还有就是网络深度不够或者神经元节点较少时,难以训练出较复杂的判决面(例如程序中想训练出一个圆形判决面,但是效果不好)。

如有精通矩阵的同学,可尝试推导一下损失函数对每个参数求偏导的矩阵求导写法。

接下来是“talk is cheap, show you the code”环节。

MATLAB代码及详细注释如下:

主函数:BP_xor_classifier.m

clc;close all;clear;

%% 训练集

%% xor训练集

X = [0,0;0,1;1,0;1,1]'; %一列为一个样本

Y = [1,0;0,1;0,1;1,0]'; %标签  一列为一个标签 onehot编码后的

%% 圆训练集(网络太浅,分类效果不好)

% train_number = 400;

% r_small = rand(1,train_number);%分布在小圆内的100个点

% r_big = r_small + rand(1,train_number);

% angle_small  = 2 * pi *rand(1,train_number);

% angle_big = 2 * pi * rand(1,train_number);

% small(1,:) = r_small .* cos(angle_small);

% small(2,:) = r_small .* sin(angle_small);

% small(3:4,:) = [ones(1,size(r_small,2));zeros(1,size(r_small,2))];

% big(1,:) = r_big .* cos(angle_big);

% big(2,:) = r_big .* sin(angle_big);

% big(3:4,:) = [zeros(1,size(r_small,2));ones(1,size(r_small,2))];

% joint_temp = [small,big];

% rerank_idx = randperm(size(joint_temp,2));

% joint = joint_temp(:,rerank_idx);

% X = joint(1:2,:);

% Y = joint(3:4,:);

%% 参考博客数据:测试用

% Y = [0.01;0.99];

% X = [0.05;0.1];

% theta_layer1 = [0.35,0.15,0.2;0.35,0.25,0.3];

% theta_layer2 = [0.6,0.4,0.45;0.6,0.5,0.55];

%% 学习率和训练次数

learning_rate = 0.1;

iter = 10000;

%% 搭建前向传播网络,初始化网络

layer1_unit = 2;%各层神经元的个数第一层为输入层,最后一层为输出层

layer2_unit = 2;

layer3_unit = 2;

%参数初始化

theta_layer1 = rand(layer2_unit,layer1_unit+1);%即M*N的随机矩阵,行数为下一层的神经元个数,一行为一组参数,即(b1,w1,w2)

theta_layer2 = rand(layer3_unit,layer2_unit+1);%列数为前一层神经元个数+1(加上权重的参数1)

 

for i = 1:iter

for j = 1:size(X,2)%一列为一个样本,一个一个遍历该样本

%% 搭建前馈式网络

%输入层

out_layer1 = [ones(1,size(X(:,j),2));X(:,j)];%往X的第一行插入一行1,即权重b的系数,用于后面的向量化相乘,这是第一层的输出(隐藏层的输入)

%隐藏层

net_H = theta_layer1 * out_layer1;

out_H = sigmoid(net_H);

out_H_plus1 = [ones(1,size(out_H,2));out_H];%加一行,同上

%输出层

net_O = theta_layer2 * out_H_plus1;

out_O = sigmoid(net_O);

%% 反向传播

%损失函数为MSE,即loss = (Y - out_O) .^ 2 / 2

%% 首先求损失对theta_layer2求偏导(链式求导法则)

%diff(loss/theta_layer2) = diff(loss/out_O) * diff(out_O/net_O) *diff(net_O/theta_layer2)

%即损失对网络输出求导 * 网络输出对未激活的输出求导 * 未激活的输出对theta_layer2求导

%diff(loss/out_O) = -(Y - out_O )

%diff(out_O/net_O) = diff(sigmoid(net_O))

%diff(net_O/theta_layer2) = out_H_plus1

 

% theta_layer2_der = -(Y(:,j) - out_O) .* sigmoid_der(net_O) *out_H_plus1';

 

%上式中,diff(net_O/theta_layer2)(即out_H_plus1)中每一列的列向量,

%分别是net_O对theta_layer2求的偏导结果

%这里做转置的原因是:前两项矩阵元素相乘的结果(2*1),

%每一项都要与out_H_plus1(3*1)中的元素相乘,得到一个2*3的矩阵

%% 上式为矩阵写法,这里,可改写为以下程序更容易理解

% temp = -(Y(:,j) - out_O) .* sigmoid_der(net_O);

% for m = 1:size(temp,1)

% theta_layer2_der(m,:) = temp(m) * out_H_plus1;

% end

%% 接下来对theta_layer1求偏导(同理,链式求导法则)

%diff(loss/theta_layer1) = diff(loss/out_H) * diff(out_H/net_H) *diff(net_H/theta_layer1)

%diff(loss/out_H) = diff(loss/out_O) * diff(out_O/net_O) *diff(net_O/out_H_plus1)

%上式中,与diff(loss/theta_layer2)的区别只是最后一项是对out_H_plus1求的偏导

%diff(net_O/out_H_plus1) = theta_layer2',矩阵乘法求导,Y = AX,则A需要转置

%上式中,diff(net_O/out_H_plus1)的结果包含偏置项的系数,在反向传播中不需要,将其舍去(结果的第一行)

%diff(out_H/net_H) = diff(sigmoid(net_H))

%diff(net_H/theta_layer1) = out_layer1

 

% theta_layer1_der = theta_layer2(:,2:end)' * (-(Y(:,j) - out_O)) .*sigmoid_der(net_O) ...

%     .* sigmoid_der(net_H) *out_layer1';

 

%% 另外的写法:记每一层的误差为Delta

%diff(loss/net_O) = diff(loss/out_O) * diff(out_O/net_O)

% Delta_3 = -(Y(:,j) - out_O) .* sigmoid_der(net_O); %损失函数为MSE时

Delta_3 = (out_O - Y(:,j))./((1 - out_O) .* out_O) .* sigmoid_der(net_O);%损失函数为交叉熵

 

%diff(loss/net_H) = diff(loss/out_O) * diff(out_O/net_O) *diff(net_O/out_H_plus1) * diff(out_H_plus1/net_H)

%这里,需要将偏置那一项去掉,即diff(net_O/out_H_plus1) = theta_layer2(:,2:end)'

Delta_2 = theta_layer2(:,2:end)' * Delta_3 .* sigmoid_der(net_H);

 

theta_layer2_der = Delta_3 * out_H_plus1';

theta_layer1_der = Delta_2 * out_layer1';

%% 更新参数

theta_layer2 = theta_layer2 - learning_rate * theta_layer2_der;

theta_layer1 = theta_layer1 - learning_rate * theta_layer1_der;

%注:这里两种方法计算所得的theta_layer1在小数点后第6位不相同,推测是由于软件精度所致

end

%% 计算损失并作图动态显示

% loss(i) = sum((Y(:,j) - out_O) .^ 2 / 2);

loss(i) = sum(-Y(:,j) .* log(out_O) - (1 - Y(:,j)) .* log(1 - out_O));

% figure(1)

% plot(i,loss,'<');

% drawnow;

% hold on;

end

%% 做预测

a = 0:0.01:1;

b = 0:0.01:1;

X = [];

for i = 1:size(a,2)

   for j = 1:size(b,2)

       temp = [a(i);b(j)];

       X = [X,temp];

   end

end

% X = [0.7,10]';

for j = 1:size(X,2)

    out_layer1 =[ones(1,size(X(:,j),2));X(:,j)];

    %隐藏层

    net_H = theta_layer1 *out_layer1;

    out_H = sigmoid(net_H);

    out_H_plus1 =[ones(1,size(out_H,2));out_H];%加一行,同上

    %输出层

    net_O = theta_layer2 *out_H_plus1;

    predict(:,j) = sigmoid(net_O);

end

%% 作图查看判决面(分类面)

figure(1)

plot(loss);%损失随迭代次数的变化

[~,c2]=max(predict);

class_one_idx = find(c2 == 1);

class_two_idx = find(c2 == 2);

class_one = X(:,class_one_idx);

class_two = X(:,class_two_idx);

figure(2)

plot(class_one(1,:),class_one(2,:),'ro');

hold on;

plot(class_two(1,:),class_two(2,:),'b<');

 

sigmoid函数:sigmoid.m

function a = sigmoid(X)

for i = 1:size(X,1)

    for j = 1:size(X,2)

        a(i,j) = 1/(1+exp(-X(i,j)));%激活函数

    end

end

end

 

sigmoid的导数:sigmoid_der.m

%% 此函数为sigmoid的导数

function a = sigmoid_der(x)

a = zeros(size(x));

for i = 1:size(x,1)

    a(i,:) = sigmoid(x(i,:)) *(eye(size(x,2)) - diag(sigmoid(x(i,:))));

end

参考:

[1]https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/?spm=a2c4e.10696291.0.0.358f19a4xonKKs.

[2] https://blog.csdn.net/zhaomengszu/article/details/77834845

[3]  吴恩达机器学习视频


上一篇:【题解】力扣995. K 连续位的最小翻转次数


下一篇:react中diff算法(面试必备)