关于如何实现一个自己的神经网络模型

基础

神经网络的基础就是感知器,也就是MP结构,它包含多个输入和一个输出,对于感知器,输出的是一个常量而输入的是一个向量,所以他的计算应该是在这样的:
$$
z=\sum_i^nw_ix_i+b_i
$$

易错点1:对每一个节点来说,w都是一个向量,b只是一个常数,如果将某一层的w和b组合起来之后,得到的就是w矩阵和b向量,这两个参数用于反向传播。

在前馈神经网络进行前向传播的时候最关键的步骤就是梯度的记录,我们在计算损失函数关于神经网络输出值的梯度的时候,得到的是个向量,也就是下面的式子:
$$
\frac{\partial L}{\partial z}=y-\hat y
$$
其中的$\hat y$是神经网络的期望输出,一般是专家系统或者人工评定的标签向量(标签向量是只有一个元素为1的向量,表示某一类别)。


前向传播的过程中,我们需要将层的每个节点的输出组合成一个向量用作下一层的输入,在这个过程中会有维度的损失,比如输入维度是3输出维度是6,那么中间提升的三个维度在计算梯度时就需要按向量求导,这里需要复习一下几种导数:

  • 当输出是向量,输入是常数时,就需要将函数内的向量依次求导,如果f=wx+b的话,w一定为向量,那么导数就是w

$$
\frac{d\vec f}{dx}=w
$$

$$
\frac{d\vec f}{dw}=[f_1’(w),f_2’(w),f_3’(w),……,f_n’(w)]
$$

其中$f_n$表示f的各个基上的分量。最后导完应该为一个矩阵(Hessian Matrix)。

  • 当输入是向量,输出是常数时,系数一定为一个向量,这样两向量求内积才可以得到常数。
  • 输出关于bias的导数一定是一个常数,在反向传播时无法使用常数进行推导,所以我们保存节点的梯度的时候,一定要乘以激活函数的导数值,这样在之后的反向传播可以少关注一步,同时训练bias时需要提取一层所有的偏置,让他变成一个向量,这样就可以乘上一层的梯度得到一个向量了。向量按顺序的分量就是不同节点的梯度,使用相应的学习方法就可以了。

样例

我们随便使用一个全连接网络,我们让$w_{ij}$表示第i层第j个单元的权重向量,$b_{ij}$同理不过是常数,下面写出伪代码

1
2
3
4
5
6
7
8
9
10
#define Net 很多层组合起来的网络对象
vector temp = 输入
vector nextinput = 空
for line in Net
if nextinput != 空 then temp = nextinput
for neural in line
nextinput 添加 neural.forward(temp)
end
end
end

这样我们就完成了每一层的前向传播,其中每个节点的forward都是已经写好的,计算公式为前面所述的w和x的内积+bias。

易错点2:小心重复计算梯度

  • 前向传播的过程时需要保存梯度,在训练的时候是需要按层进行训练,所以务必在保存时就乘以激活函数的导数值

最后一个至关重要的点就是别怕麻烦,梯度计算需要细心,避免算错,下面使用i表示层数,j表示所在层的第几个元素,o为输出,根据他的角标是层数还是第几个元素决定它是层的输出还是节点的输出I表示层的输入
$$
损失层:\delta_j = \sigma’(z)(o_j-t_j)
$$
这里的t为预期输出向量的某个值,输出层的元素数与预期值的维相同,所以可以进行加减法,得到关于不同节点的梯度,结果为一个数。
$$
输出层:\frac{\partial L}{\partial w_{ij}}=\delta_jI
$$
我们得到不同节点关于损失层的梯度后,我们可以把$\delta$组合成向量。


计算完输出层,我们需要计算隐含层,隐含层的$\delta$需要更新,且是按照向量的方式更新,具体方式为:
$$
\delta_{i-1}=\delta_io_j
$$
也就是梯度乘以某个节点的输出的输出,这样得到的就是一个向量了。然后使用和输出层一样的更新权重方法就可以了。然后我们更新delta
$$
\delta_{i-1} = \sigma’(z)<\vec \delta_i,\vec w_i>
$$
我们把delta和层内所有的权重向量求内积后乘以节点激活函数的导数,这样求出来的是一个数值,层内所有的数值结合为一个向量后,就可以用于前一层的更新了。

易错点:容易混淆隐含层与输出层梯度的计算

代码

这里使用Java实现以下前馈神经网络的计算过程,如果有需要可以前往我的github下载插件源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package cn.minloha.NeuralWork;

import cn.minloha.Type.Matrix;
import cn.minloha.Type.Vector;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// 神经节点对象
public class Neural {
private Vector weight;
private Double bias;
private Double z;

private Vector nabla_w; // out/weight
private Double nabla_b; // out/b
private Vector nabla_input; // out/x
private final Double lita;

private Double sig;

private final Function itemFunction = new Function();

public Neural(int inputdim,double lita){
this.lita = lita; List<Double> a = new ArrayList<>();
for(int x = 0;x<inputdim;x++){
a.add(0.1);
}
this.weight = new Vector(a);
this.bias = 0.1;
}

public Neural(DataClass d,Double lita){
this.weight = d.getWeight();
this.bias = d.getBias();
this.lita = lita;
}

public void changeDataclass(DataClass dataClass){
this.weight = dataClass.getWeight();
this.bias = dataClass.getBias();
}

public Double getZ();
public Vector getWeight();
public Double getBias();
public Vector getNabla();
// 获取一个单例对象
public DataClass getDataClass();

// 节点的前向传播,得到的结果需要给层拼接,得到下一层的输入
public Double forward(Vector in){
this.z = Vector.Multiplicate(this.weight,in) + this.bias;
double k = itemFunction.Sigmoid(z);
this.sig = k * (1-k);
this.nabla_w = in;
this.nabla_input = this.weight;
this.bias = 1.0;
return k;
}

// 反向传播w
public Double backward_w(Vector delta,boolean code,int t,Vector out,Vector in){
// delta(需要乘以这层的输出) * nabla_w(他就是这层的输入)
double de = 0.0;
if(code) de = delta.getAsList().get(t) * this.sig;
else de = this.sig * Vector.Multiplicate(delta,out);
Vector kk = Vector.Multiplicate(de,in);

this.weight = Vector.add(this.weight,Vector.Multiplicate(-1*lita,kk));
return de;
}

public void backward_b(Vector delta,boolean code,int t,Vector out){
// bias是一个值,也就是对应位置的delta,不过delta需要更新为可用形式
double de = 0.0;
if(code) de = delta.getAsList().get(t) * this.sig;
else de = this.sig * Vector.Multiplicate(delta,out);
this.bias -= de;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package cn.minloha.NeuralWork;

import cn.minloha.ModelLoader.ANNmodel;
import cn.minloha.ModelLoader.ModelLoad;
import cn.minloha.Type.Matrix;
import cn.minloha.Type.Vector;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class NetWork {

private final Function itemFunction = new Function();
// 损失函数的导数值
private Vector nabla_outer;
// 学习率(我直接折中)
private double lita = 0.5;

// 层,面向对象而已
private class Linear{
private final List<Neural> neuralList = new ArrayList<>();
private Vector out;
private Vector input;

// 私有构造,本来打算用tensorflow或者pytorch那种构造方法了,不过也不需要那么多激活函数,也就这样了
private Linear(int nums,int inputDimens){
for(int h = 0;h<nums;h++){
neuralList.add(new Neural(inputDimens,0.2));
}
}

// 前向传播的同时就把权重矩阵和偏置向量拼接出来
public Vector forward(Vector in){
List<Double> res = new ArrayList<>();
List<Double> nore = new ArrayList<>();
for(Neural nn : this.neuralList){
res.add(nn.forward(in));
nore.add(nn.getZ());
}
this.input = in;
this.out = new Vector(nore);
return new Vector(res);
}

// 反向传播,需要判断是不是输出层,输出层的导数和隐含层的导数计算方式相似但是不同
public Vector backward(Vector delta,boolean lastone){
List<Double> ch = new ArrayList<>();
// 每层第k个节点
for(int k = this.neuralList.size() - 1;k>=0;k--){
Neural nnn = neuralList.get(k);
double sb = nnn.backward_w(delta,lastone,k,this.out,this.input);
nnn.backward_b(delta,lastone,k,this.out);
ch.add(sb);
}
return new Vector(ch);
}
}


// LinkedList进行管理
private final List<Linear> net = new ArrayList<>();

// 展示结构(没啥用,就是看看)
public void showNN(){
System.out.println("net shape is:");
for(Linear l : net) System.out.print(l.neuralList.size() + "\t");
System.out.println();
}

// 构造方法
public NetWork(int inputDimens,int ...nums){
List<Integer> rk = new ArrayList<>();
rk.add(inputDimens);
for(int n : nums) rk.add(n);
for(int k = 0;k<rk.size()-1;k++){
net.add(new Linear(rk.get(k+1),rk.get(k)));
}
}

// forward
public double forward(Vector input,Vector except){
Vector in = input;
for(Linear l : this.net){
in = l.forward(in);
}
this.nabla_outer = Vector.add(in,Vector.Multiplicate(-1,except));
return itemFunction.CovLoss(except,in);
}

public Vector forward(Vector input){
Vector in = input;
for(Linear l : this.net){
in = l.forward(in);
}
return in;
}

// backward
public void backward(){
Vector update = this.nabla_outer;
List<Double> kkk = new ArrayList<>();
for(int m = net.size()-1; m>=0; m--){
boolean lastone = (m == net.size()-1);
update = net.get(m).backward(update,lastone);
}
}


public void saveModel(String FilePath) throws IOException;

public void loadModel(String modelPath) throws IOException;
}


在代码实现过程中遇到了一个模型的问题,就是我需要避免训练过多的内容和训练错文件,所以在这个过程中,我需要一个单例类进行传递,也就是说这个类的构造函数是私有的,而构造只能使用类提供的接口方法:

1
2
3
4
5
6
7
8
9
10
11
// 单例类的实现
public class PIPE {
private static PIPE instance = new PIPE();
private PIPE(){};

private List<String> pipe = new ArrayList<>();

public static PIPE getInstance(){
return instance;
}
}

进度

学习进度的话,神经网络马上就结课了,学习程度还剩下一点机器学习和强化学习,接下来就要全心去攻克计算机视觉了。至于这个插件我不打算发,但是会开源,有兴趣可以在过几日打开本页面找到下载的github地址,有兴趣也可以点亮star

Github地址:https://github.com/iMinloha/Hamino.git


关于如何实现一个自己的神经网络模型
https://blog.minloha.cn/posts/110437646e67422023010421.html
作者
Minloha
发布于
2023年1月4日
更新于
2024年4月8日
许可协议