本文是《MATLAB智能算法30个案例分析(第二版)》一书第三章的学习笔记。
BP神经网络是一类多层的前馈神经网络。它的名字源于在网络训练的过程中,调整网络的权值的算法是误差的反向传播的学习算法,即为BP学习算法。BP神经网络是人工神经网络中应用广泛的算法,但依然存在着一些缺陷,例如学习收敛速度太慢、不能保证收敛到全局最小点、网络结构不易确定等。
另外,网络结构、初始连接权值和阈值的选择对网络训练的影响很大,但是又无法准确获得,针对这些特点可以采用遗传算法对神经网络进行优化。
对于一般的模式识别问题,三层网络可以很好地解决问题。并且,在三层网络中,隐含层神经网络个数 n 2 n_{2} n2和输入层神经元个数 n 1 n_{1} n1之间有着近似关系:
n 2 = 2 × n 1 + 1 n_{2}=2\times n_{1}+1 n2=2×n1+1
本案例中,由于样本有15个输入参数,3个输出参数,故这里的 n 2 n_{2} n2取值为31,设置的BP神经网络结构为15 − - − 31 − - − 3,即输入层15个节点、隐含层31个节点、输出层3个节点,权值个数为15 × \times × 31 + + + 31 × \times × 3 = = = 558,阈值个数为31 + + + 3 = = = 34,则遗传算法中的决策变量个数为592。
将测试样本的测试误差向量的模长作为衡量网络的一个泛化能力(即适应度),适应度越小,误差越小,个体越优。
神经网络的隐含层神经元的传递函数使用S型正切函数tansig(),输出层神经元的传递函数使用S型对数函数logsig(),可以满足网络的输出要求(结果为0或1)。令样本矩阵为R,创建网络可以使用以下代码:
旧版写法(不要这样写)
%% 旧版写法
net = newff(minmax(R), [31,3], {'tansig','logsig'}, 'trainlm');
标准的newff函数用法(旧版)如下所示:
%% 旧版写法
net = newff(PR, [S1 S2 ... SN], {TF1 TF2 ... TFN}, BTF, BLF, PF);
% 其中,PR为R*2矩阵,包含输入向量R各元素的取值范围,R为输入向量元素的数目
% [S1 S2 ... SN]中的S_i为第i层的神经元个数,案例中的31就是隐含层神经元个数,3就是输出层神经元的个数
% {TF1 TF2 ... TFN}中的TF_i为第i层的传输函数
% BTF为训练函数,默认为'trainlm'函数
% BLF为权值/阈值训练函数,默认为'learngdm'函数
% PF为性能函数,默认为'mse'函数
新版写法
%% 新版写法
net = newff(P,T,[31],{'tansig','logsig'},'trainlm');
% 新版写法中,matlab会自动计算输入向量的取值范围和输出层的神经元个数
标准的newff函数用法(新版)如下所示:
net =newff(P, T, [S1 S2...S(N-l)], {TF1 TF2...TFNl}, BTF, BLF, PF, IPF, OPF, DDF)
% 其中,P为输入的样本矩阵,每一列为一个样本,T为样本矩阵对应的样本结果,也是每一列代表一个样本
% [S1 S2...S(N-l)]中的S_i为第i个隐含层的神经元个数,默认为空
% {TF1 TF2 ... TFN}中的TF_i为第i层的传输函数
% BTF为训练函数,默认为'trainlm'函数
% BLF为权值/阈值训练函数,默认为'learngdm'函数
% PF为性能函数,默认为'mse'函数
% 剩下的一般用不到
设置训练参数为:训练次数为1000次,训练要求精度为0.01,学习速率为0.1(学习速率越大收敛越快,但可能不成熟)
% 设置网络参数
net.trainParam.epochs = 1000; % 训练次数为1000
net.trainParam.goal = 0.01; % 训练要求精度为0.01;
net.trainParam.lr = 0.1; % 学习速率为0.1
net.trainParam.showWindow = false; % 不显示训练迭代过程
新版的newff函数新增了net.divideFcn属性,将测试样本三等分成了训练集、验证集和测试集,默认比例是6:2:2。为了达到和书中相同的效果,这里需要清除该属性,否则训练误差向量的模长会比较大。
net.divideFcn = '';
net.inputs{1}.processFcns = {}; % 不对输入层数据进行归一化
net.outputs{2}.processFcns = {}; % 不对输出层数据进行归一化
这里贴一个关于是否需要设置processFcns属性的帖子链接,不过是英文的,没怎么看懂。。。
https://www.mathworks.com/matlabcentral/answers/582257-is-it-necessary-to-set-net-inputs-i-processfcns-when-the-network-is-created-using-netwok-command
在上文的网络创建部分中,已经计算出了各层的权值、阈值个数,接下来只需要进行赋值即可
% 设置网络初始权值、阈值
weight1_num = input_num*hidden_num; % 输入层到隐含层的权值数
weight2_num = hidden_num*output_num; % 隐含层到输出层的权值数
weight1 = X([1:weight1_num]); % 输入层到隐含层的权值
threshold_1 = X([weight1_num+1:weight1_num+hidden_num]); % 隐含层阈值
weight2 = X([weight1_num+hidden_num+1:weight1_num+hidden_num+weight2_num]); % 隐含层到输出层的权值
threshold_2 = X([weight1_num+hidden_num+weight2_num+1:end]); % 输出层阈值
% 赋值
net.iw{1,1} = reshape(weight1,hidden_num,input_num); % 输入层到隐含层的权值
net.lw{2,1} = reshape(weight2,output_num,hidden_num); % 隐含层到输出层的权值
net.b{1} = reshape(threshold_1,hidden_num,1); % 隐含层阈值
net.b{2} = reshape(threshold_2,output_num,1); % 输出层阈值
关于net.iw、net.lw和net.b的解释:
% net.iw为输入层到网络层(隐含层+输出层)的权值
% 通过访问net.iw{i,j}可以获得第i个网络层来自第j个输入向量的权值向量
% 本案例中,net.iw{1,1}即表示第1个网络层(即隐含层)来自第1个输入向量的权值向量
% net.lw为一个网络层到另一个网络层的权值
% 通过访问net.lw{i,j}可以获得第i个网络层来自第j个网络层的权值向量
% 本案例中,net.lw{2,1}即表示第2个网络层(即输出层)来自第1个网络层(即隐含层)的权值向量
% net.b为各网络层的阈值
% net.b{i}为第i个网络层的阈值向量
关于神经网络中其他参数的解释可以参考书籍《神经网络模型及其MATLAB仿真程序设计》
用到了下面的函数
net = train(net,P,T);
使用测试样本P_test、T_test测试神经网络的误差,并取误差向量的模长作为返回值(适应度)
% 测试网络
Y = sim(net,P_test);
error = norm(Y-T_test);
function error = bp_fun(X)
% bp_fun 根据输入的权值矩阵X训练神经网络
% X input: 权值矩阵X
% error output: 误差向量的模长(范数)
load data
% 初始参数列表
input_num = size(P,1);
output_num = size(T,1);
hidden_num = 2*input_num+1;
% 新建BP神经网络
net = newff(P,T,[hidden_num],{'tansig','logsig'},'trainlm');
net.divideFcn = '';
net.inputs{1}.processFcns = {};
net.outputs{2}.processFcns = {};
% 设置网络参数
net.trainParam.epochs = 1000; % 训练次数为1000
net.trainParam.goal = 0.01; % 训练要求精度为0.01;
net.trainParam.lr = 0.1; % 学习速率为0.1
net.trainParam.showWindow = false; % 不显示训练迭代过程
% 设置网络初始权值、阈值
weight1_num = input_num*hidden_num; % 输入层到隐含层的权值数
weight2_num = hidden_num*output_num; % 隐含层到输出层的权值数
weight1 = X([1:weight1_num]); % 输入层到隐含层的权值
threshold_1 = X([weight1_num+1:weight1_num+hidden_num]); % 隐含层阈值
weight2 = X([weight1_num+hidden_num+1:weight1_num+hidden_num+weight2_num]); % 隐含层到输出层的权值
threshold_2 = X([weight1_num+hidden_num+weight2_num+1:end]); % 输出层阈值
net.iw{1,1} = reshape(weight1,hidden_num,input_num); % 输入层到隐含层的权值
net.lw{2,1} = reshape(weight2,output_num,hidden_num); % 隐含层到输出层的权值
net.b{1} = reshape(threshold_1,hidden_num,1); % 隐含层阈值
net.b{2} = reshape(threshold_2,output_num,1); % 输出层阈值
% 训练网络
net = train(net,P,T);
% 测试网络
Y = sim(net,P_test);
error = norm(Y-T_test);
end
个体编码采用二进制编码,每个个体均为一个二进制字符串,由输入层到隐含层的权值(465个)、隐含层阈值(31个)、隐含层到输出层的权值(31 × \times × 3 = = = 93个)、输出层阈值(3个)连接而成。设每个编码长度为10,则一个个体的字符串长度为5920。
此函数用于将二进制字符串转换成十进制数
function decimalX = decode_fun(Nind, Lind, binaryStr, codeLen, Xmin, Xmax)
% decode_fun 解码
% Nind input: 种群规模
% Lind input: 染色体个数
% binaryStr input: 二进制码串矩阵
% codeLen input: 每个自变量的码串长度
% Xmin input: 下界
% Xmax input: 上界
% decimalX output: 十进制数矩阵
n = linspace(1,codeLen,codeLen)-1;
decimalX = zeros(Nind,Lind);
for i = 1:Nind
% 对每个染色体对应的二进制码串进行解码
for j = 1:Lind
decimalX(i,j) = sum(binaryStr(i,[1+codeLen*(j-1):j*codeLen]).*(2.^n));
end
decimalX(i,:) = Xmin+decimalX(i,:).*(Xmax-Xmin)/(2^codeLen-1);
end
end
function fitness = obj_fun(individuals, Nind)
% obj_fun 计算目标函数值
% individuals input: 权值与阈值
% Nind input: 种群规模
% fitness output: 种群的目标函数值
for i = 1:Nind
fitness(i,1) = bp_fun(individuals.decimalX(i,:));
end
end
本文使用随机联赛选择作为选择算子。
随机联赛选择也是一种基于个体适应度之间大小关系的选择方法。其基本思想是每次随机选取 N N N个个体,选出其中适应度最高的一个个体遗传到下一代群体中。
在随机联赛选择操作中,只有个体适应度之间的大小比较运算,而无个体适应度之间的算术运算,所以它对个体适应度去正值还是取负值无特别影响。
随机联赛选择的具体操作过程为:
(1)从群体中随机选取 N N N个个体进行适应度大小的比较,将其中适应度最高的个体遗传到下一代群体中。
(2)将上述过程重复 M M M次( M M M为种群规模),就可得到下一代群体中的 M M M个个体。
注意:这里选择是适应度较小的个体(即误差更小)进入子代,而不是适应度较大的个体。
function new_individuals = Select(individuals, Nind)
% select 选择算子,基于随机联赛策略选择较优的个体
% individuals input: 种群
% Nind input: 种群规模
% new_chrom output: 经选择算子生成的新种群
N = 2; % 每次随机选择的个体数
index = []; % 存储每次选择的个体的索引
for i = 1:Nind
race_indi_index = randi(Nind, [2,1]); % 被选中的个体的索引
race_indi_fit = individuals.fitness(race_indi_index); % 被选中的个体的适应度
better_index = find(individuals.fitness == min(race_indi_fit));
index = [index; better_index(1)];
end
individuals.chrom = individuals.chrom(index,:);
individuals.fitness = individuals.fitness(index,:);
new_individuals = individuals;
end
使用最简单的单点交叉算子
function new_individuals = cross_fun(individuals, Nind, Lind, codeLen, Pc)
% cross_fun 交叉算子,采用单点交叉算子
% individuals input: 原始种群
% Nind input: 种群规模
% Lind input: 染色体个数
% codeLen input: 二进制码串长度
% Pc input: 交叉概率
% new_individuals output: 经交叉算子产生的新种群
new_individuals = individuals;
for i = 1:Nind
Pick = rand; % 进行交叉操作的概率
if Pick < Pc
% 选择父代个体
parent = randi([1,Nind-1],[1,2]);
% 解决生成的两个随机数相同的情况
while parent(1) == parent(2)
parent = randi([1,Nind],[1,2]);
end
% 进行交叉
posit = randi([1,codeLen]); % 选择交叉位置
for j = 1:Lind
new_individuals.chrom(i,[1+(j-1)*codeLen:(j-1)*codeLen+posit]) = individuals.chrom(parent(1),[1+(j-1)*codeLen:(j-1)*codeLen+posit]);
new_individuals.chrom(i,[(j-1)*codeLen+(posit+1):j*codeLen]) = individuals.chrom(parent(2),[(j-1)*codeLen+(posit+1):j*codeLen]);
end
end
end
end
对每个基因,产生一个随机数,若该随机数小于变异概率Pm,则选取部分位置,将这些位置上的二进制数取反(1—>0,0—>1)
function new_individuals = Mutation(individuals, Nind, Lind, codeLen, Pm)
% Mutation 变异算子,基于一定的概率产生变异基因
% individuals input: 原始种群
% Nind input: 种群规模
% Lind input: 染色体个数
% codeLen input: 二进制码串长度
% Pm input: 变异概率
new_individuals = individuals;
clear individuals;
for i = 1:Nind
for j = 1:Lind
Pick = rand; % 进行变异的概率
if Pick < Pm
% 选取变异的位置
Posit = randperm(10);
Posit = Posit([1:randi(10)]) + (j-1)*codeLen;
% 取反(变异)
new_individuals.chrom(i,Posit) = ~new_individuals.chrom(i,Posit);
end
end
end
end
%% 基于遗传算法的BP神经网络优化算法
% 一、结合思想
% 神经网络的权值和阈值一般是通过随机初始化为[-0.5,0.5]内的随机数,这个初始化参数对
% 网络训练的影响很大,但是又无法准确获得,对于相同的初始权重值和阈值,网络的训练效果
% 是一样的,引入遗传算法就是为了优化出最佳的初始权重值和阈值
% 二、算法流程
% Step 1:创建网络
% Step 2.确定网络的初始权重值和阈值,对其进行编码得到初始种群;
% Step 2:while 不满足终止条件时
% a.解码、计算适应度,选出最优个体
% -->1. 对种群中的每个个体,将其作为网络的初始权值、阈值,使用训练样本对网络进行训练;
% -->2. 计算训练误差,并将其视为适应度;
% b.进行遗传算法的操作--->选择、交叉、变异,得到新种群
% end
% Step 3:对最终得到的种群进行解码,得到最优的神经网络权重值和阈值
% 三、算法实现
tic;clear;clc
% (1)种群初始化
% 1.1 初始参数列表
Nind = 20; % 种群规模
Lind = 592; % 染色体个数
codeLen = 10; % 编码长度
maxGen = 50; % 进化次数(迭代次数)
Pc = 0.7; % 交叉概率
Pm = 0.01; % 变异概率
Xmin = -0.5*ones(Lind,1)'; % 下界
Xmax = 0.5*ones(Lind,1)'; % 上界
individuals = struct('chrom', [], 'decimalX', [],'fitness', zeros(Nind,1));
avg_Fit = []; % 平均适应度
trace = []; % 最佳适应度
best_chrom = []; % 最佳适应度对应的个体
% 1.2 建立初始种群
for i = 1:Nind
individuals.chrom(i,:) = randi([0,1],[1,codeLen*Lind]);
end
% (2)开始迭代
for gen = 1:maxGen
fprintf("正在进行第%d次迭代\n", gen);
% 解码
individuals.decimalX = decode_fun(Nind,Lind,individuals.chrom,codeLen,Xmin,Xmax);
% 计算各个个体的适应度
individuals.fitness = obj_fun(individuals,Nind);
% 选出历代最优个体
[best_f best_index] = min(individuals.fitness);
best_chrom = individuals.chrom(best_index,:);
trace = [trace;best_f];
avg_Fit = [avg_Fit;mean(individuals.fitness)];
% 选择算子
individuals = Select(individuals,Nind);
% 交叉算子
individuals = cross_fun(individuals,Nind,Lind,codeLen,Pc);
% 变异算子
individuals = Mutation(individuals,Nind,Lind,codeLen,Pm);
end
clc
disp(['最优适应度为:',num2str(trace(end))])
figure(1)
plot(trace,'LineWidth',1)
title(['适应度进化曲线(最大迭代次数maxGen=',num2str(maxGen),')'])
xlabel('遗传代数')
ylabel('历代误差最小值/均值')
hold on
plot(avg_Fit, 'LineWidth', 1)
legend('历代误差最小值', '历代平均误差')
toc