Press "Enter" to skip to content

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

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

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

 

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

 

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

 

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

 

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

 

 

 

 

 

 

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

 

 

 

上图为修改损失函数为交叉熵时的结果,学习率同样为 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] 吴恩达机器学习视频

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注