神经网络入门

前言

本文主要介绍最基础的神经网络,包括其结构,学习方法, C++ \texttt{C++} C++ 的实现代码。 Python \texttt{Python} Python 的代码可以搜索互联网得到。

前排提示:本人涉及一丁点数学知识。

神经网络的结构

神经网络包括多个层次(Layer)。一般来说神经网络包括三个部分:输入层隐藏层输出层 。输入层和输出层都只有一个,而隐藏层可以有多个。顺序为 输入层,隐藏层,输出层。

每一个层次会有若干的神经元。我们称第 L l L_l Ll 层为输出层,也就是最后一层。对于第 L L L 层(从 0 0 0 开始计数),我们用 n L n_L nL 表示其神经元的数量。

对于第 L L L 层的第 i i i 个神经元(从 0 0 0 开始计数),我们用 a i L a^L_i aiL 表示其输出到下一层神经元或者作为结果的值。对于生物体的神经元, a i L a_i^L aiL 的取值只有 0 , 1 0,1 0,1 ,但是对于本文而言,计算机的神经元有 a i L ∈ [ 0 , 1 ] a_i^L\in[0,1] aiL[0,1] 。同时,每个神经元还有一个偏移值 b i L b^L_i biL

除输入层外,每一个神经元都与上一层的所有神经元都有一个连接,对于第 L ( 1 < L ) L(1<L) L(1<L) 的第 i i i 个神经元,它与 L − 1 L-1 L1 层的所有神经元都有连接,其中它与第 j j j 个神经元的连接有权重 w i , j L w^L_{i,j} wi,jL

下图展示了一个简单的神经网络的构成:

图中有 n 0 = 3 , n 1 = n 2 = 2 n_0=3,n_1=n_2=2 n0=3,n1=n2=2 ,且第 0 0 0 层为输入层,第 2 2 2 层为输出层。

PS \texttt{PS} PS: 此图没有展示神经元的偏移值。

在这里插入图片描述

输出值的计算

对于输入层而言,其输出值 a i 0 ( i ∈ [ 0 , n 0 ) ∩ Z ) a^0_i (i\in[0,n_0)\cap \mathbb{Z}) ai0(i[0,n0)Z) ,其输出值与神经网络的输入相同。

对于隐藏层和输出层,其计算方式如下:
a i L = f ( z i L ) z i L = b i L + ∑ j = 0 n L − 1 w i , j L a j L − 1 \begin{aligned} a^L_i&=f\left(z^L_i\right)\\ z^L_i&=b^L_i + \sum_{j=0}^{n_{L-1}} w^L_{i,j}a^{L-1}_j \end{aligned} aiLziL=f(ziL)=biL+j=0nL1wi,jLajL1
其中, f ( x ) f(x) f(x) 函数一般为 S i g m o i d \mathrm{Sigmoid} Sigmoid 函数,常见于生物种群数量的自然增长。其定义和导数如下:
f ( x ) = 1 1 + e − x f ′ ( x ) = f ( x ) ( 1 − f ( x ) ) \begin{aligned} f(x)&=\frac{1}{1+e^{-x}}\\ f'(x)&=f(x)(1-f(x)) \end{aligned} f(x)f(x)=1+ex1=f(x)(1f(x))
这个函数可以让我们的输出值取值范围固定在 [ 0 , 1 ] [0,1] [0,1] 之间。

实际上,神经网络使用的 f ( x ) f(x) f(x) 不一定需要让输出值固定在 [ 0 , 1 ] [0,1] [0,1] 之间,比如卷积神经网络(CNN)中有一个 f ( x ) f(x) f(x) R e L U \mathrm{ReLU} ReLU 函数,其定义为:
f ( x ) = { x x > 0 0 o t h e r w i s e f(x)=\begin{cases} x & x>0\\ 0 & \mathrm{otherwise} \end{cases} f(x)={x0x>0otherwise

请留意 z i L z^L_i ziL 这个值,我们暂且将其称为成为中间值

神经网络的学习方法

整个神经网络的计算过程,可以认为是一个庞大的函数 F F F ,其自变量的个数是输入层神经元的个数,而因变量的个数是输出层神经元的个数。而我们需要的让这个函数去拟合一个标准函数 G G G

而神经网络的学习过程,实际上是一个用提供的若干个输入输出(我们称这些输入输出为训练集)不断调整权重 w w w 以及偏移值 b b b ,使得 F F F 拟合 G G G 的过程。

过拟合

实际上在神经网络的学习过程中,因为参数过多(或者其他原因),容易出现过拟合(overfitting)的现象。表现为在对于训练集中的数据,神经网络可以给出优秀的结果,但是对于训练集以外的数据,则出现很多错误。

一个形象的理解方法就是:当我们用给定的一些三次函数 G G G 上的点,用一个 6 6 6 次函数 F F F 去拟合这个 3 3 3 次函数 G G G 的时候,对于给定的点, F F F 都和 G G G 的输出一致,但是本应该是 0 0 0 4 , 5 , 6 4,5,6 4,5,6 次项不全是 0 0 0 ,导致 F F F 会出现一些弯曲,这时候就出现了过拟合。下图是一个过拟合的示意图:

在这里插入图片描述

其中绿色的点是训练集, G ( x ) G(x) G(x) 是我们需要的函数, F ( x ) F(x) F(x) 为神经网络拟合出的函数。

一般来说,我们会使用另一套输入输出(我们称之为测试集)来评判神经网络的学习效果。

在当前状态下,我们并不考虑过拟合问题。

反向传播方法(Backprop,简称BP)

反向传播方法是神经网络学习的核心算法。

对于训练集中的一个输入输出 k k k ,我们用 O i O_i Oi 表示理想状态下,神经网络的输出层的第 i i i 个神经元应该有的输出值。 这时候,我们可以计算出神经网络当前的函数 F F F G G G 的误差,这里我们用类似方差的表达式表示其误差:
C k = ∑ i = 0 N L l ( a i L l − O i ) 2 C_k=\sum_{i=0}^{N_{L_l}} (a^{L_l}_i-O_i)^2 Ck=i=0NLl(aiLlOi)2

实际上有其他表示误差的方式,本人不是很了解。

显然的,我们需要让 C k C_k Ck 越小越好,这时候,我们需要知道 a i L l a_i^{L_l} aiLl 应该如何变化才能使得 C k C_k Ck 变小,可以想到运用导数计算出 a a a 应该有的变化,也就是:
∂ C k ∂ a i L l = 2 ( a i L l − O i ) \frac{\partial C_k}{\partial a^{L_l}_i} =2\left(a^{L_l}_i - O_i\right) aiLlCk=2(aiLlOi)
想让 C k C_k Ck 变小,就需要让 a i L l a_i^{L_l} aiLl 向着 − ∂ C k a i L l -\frac{\partial C_k}{a^{L_l}_i} aiLlCk 方向减小,至于如何做到这一点,我们可以调整 b i L l , w i , ∗ L l , a ∗ L l − 1 b^{L_l}_i,w^{L_l}_{i,*}, a^{L_{l-1}}_* biLl,wi,Ll,aLl1。而调整这些参数,也可以使用求导的方式 ( 0 ≤ j < n l − 1 0\leq j<n_{l-1} 0j<nl1):
∂ C k ∂ b i L l = ∂ z i L l ∂ b i L l ∂ a i L l ∂ z i L l ∂ C k ∂ a i L l = 1 ⋅ f ′ ( z i L l ) ∂ C k ∂ a i L l ∂ C k ∂ w i , j L l = ∂ z i L l ∂ w i , j L l ∂ a i L l ∂ z i L l ∂ C k ∂ a i L l = a j L l − 1 f ′ ( z i L l ) ∂ C k ∂ a i L l ∂ C k ∂ a j L l − 1 = ∑ i = 0 n L l ∂ z i L l ∂ a j L l − 1 ∂ a i L l ∂ z i L l ∂ C k ∂ a i L l = ∑ i = 0 n L l − 1 w i , j L l f ′ ( z i L l ) ∂ C k ∂ a i L l \begin{aligned} \frac{\partial C_k}{\partial b^{L_l}_i} &= \frac{\partial z^{L_l}_i}{\partial b^{L_l}_i} \frac{\partial a^{L_l}_i}{\partial z^{L_l}_i} \frac{\partial C_k}{\partial a^{L_l}_i} = 1\cdot f'\left(z^{L_l}_i\right)\frac{\partial C_k}{\partial a^{L_l}_i}\\ \frac{\partial C_k}{\partial w^{L_l}_{i,j}} &= \frac{\partial z^{L_l}_i}{\partial w^{L_l}_{i,j}} \frac{\partial a^{L_l}_i}{\partial z^{L_l}_i} \frac{\partial C_k}{\partial a^{L_l}_i} = a^{L_{l}-1}_j f'\left(z^{L_l}_i\right)\frac{\partial C_k}{\partial a^{L_l}_i}\\ \frac{\partial C_k}{\partial a^{L_{l}-1}_j} &= \sum_{i=0}^{n_{L_l}}\frac{\partial z^{L_l}_i}{\partial a^{L_{l}-1}_j} \frac{\partial a^{L_l}_i}{\partial z^{L_l}_i} \frac{\partial C_k}{\partial a^{L_l}_i} = \sum_{i=0}^{n_{L_l}-1}w^{L_l}_{i,j}f'\left(z^{L_l}_i\right)\frac{\partial C_k}{\partial a^{L_l}_i} \end{aligned} biLlCkwi,jLlCkajLl1Ck=biLlziLlziLlaiLlaiLlCk=1f(ziLl)aiLlCk=wi,jLlziLlziLlaiLlaiLlCk=ajLl1f(ziLl)aiLlCk=i=0nLlajLl1ziLlziLlaiLlaiLlCk=i=0nLl1wi,jLlf(ziLl)aiLlCk
要调整这些参数,只需要向着导数的反方向增减这些变量即可,至于 a j L l − 1 a^{L_l-1}_j ajLl1 的调整,需要调整 b j L l − 1 b^{L_l-1}_j bjLl1 以及 w j , ∗ L l − 1 w^{L_l-1}_{j,*} wj,Ll1 ,其调整方式也是求导,且结构和上述的式子大致相同:
∀ L ∈ [ 1 , L l − 1 ] ∩ Z ∂ C k ∂ b i L = ∂ z i L ∂ b i L ∂ a i L ∂ z i L ∂ C k ∂ a i L = 1 ⋅ f ′ ( z i L ) ∂ C k ∂ a i L ∂ C k ∂ w i , j L = ∂ z i L ∂ w i , j L ∂ a i L ∂ z i L ∂ C k ∂ a i L = a j L − 1 f ′ ( z i L ) ∂ C k ∂ a i L ∂ C k ∂ a j L = ∑ i = 0 n L + 1 − 1 ∂ z i L + 1 ∂ a j L ∂ a i L + 1 ∂ z i L + 1 ∂ C k ∂ a i L + 1 = ∑ i = 0 n L + 1 − 1 w i , j L + 1 f ′ ( z i L + 1 ) ∂ C k ∂ a i L + 1 \begin{aligned} &\forall L\in [1,L_l-1] \cap \mathbb{Z}\\ &\frac{\partial C_k}{\partial b^{L}_i} = \frac{\partial z^{L}_i}{\partial b^{L}_i} \frac{\partial a^{L}_i}{\partial z^{L}_i} \frac{\partial C_k}{\partial a^{L}_i} = 1\cdot f'\left(z^{L}_i\right)\frac{\partial C_k}{\partial a^{L}_i}\\ &\frac{\partial C_k}{\partial w^{L}_{i,j}} = \frac{\partial z^{L}_i}{\partial w^{L}_{i,j}} \frac{\partial a^{L}_i}{\partial z^{L}_i} \frac{\partial C_k}{\partial a^{L}_i} = a^{L-1}_j f'\left(z^{L}_i\right)\frac{\partial C_k}{\partial a^{L}_i}\\ &\frac{\partial C_k}{\partial a^{L}_j} = \sum_{i=0}^{n_{L+1}-1}\frac{\partial z^{L+1}_i}{\partial a^{L}_j} \frac{\partial a^{L+1}_i}{\partial z^{L+1}_i} \frac{\partial C_k}{\partial a^{L+1}_i} = \sum_{i=0}^{n_{L+1}-1}w^{L+1}_{i,j}f'\left(z^{L+1}_i\right)\frac{\partial C_k}{\partial a^{L+1}_i} \end{aligned} L[1,Ll1]ZbiLCk=biLziLziLaiLaiLCk=1f(ziL)aiLCkwi,jLCk=wi,jLziLziLaiLaiLCk=ajL1f(ziL)aiLCkajLCk=i=0nL+11ajLziL+1ziL+1aiL+1aiL+1Ck=i=0nL+11wi,jL+1f(ziL+1)aiL+1Ck
至此,我们得到了为了拟合数据 k k k 而需要调整的数据的内容,以及调整之前的网络的信息,我们用两个数组(或者认为是矩阵,向量)将其整合一下:
∇ C k = [ ∂ C k ∂ b 0 0 ∂ C k ∂ b 1 0 ⋮ ∂ C k ∂ b n L l − 1 L l ∂ C k ∂ w 0 , 0 1 ∂ C k ∂ w 0 , 1 1 ⋮ ∂ C k ∂ w n L l − 1 , n L l − 1 − 1 L l ] , D = [ b 0 0 b 1 0 ⋮ b n L l − 1 L l w 0 , 0 1 w 0 , 1 1 ⋮ w n L l − 1 , n L l − 1 − 1 L l ] \nabla C_k=\begin{bmatrix} \frac{\partial C_k}{\partial b^{0}_0}\\ \frac{\partial C_k}{\partial b^{0}_1}\\ \vdots\\ \frac{\partial C_k}{\partial b^{L_l}_{n_{L_l}-1}}\\ \frac{\partial C_k}{\partial w^{1}_{0,0}}\\ \frac{\partial C_k}{\partial w^{1}_{0,1}}\\ \vdots\\ \frac{\partial C_k}{\partial w^{L_l}_{n_{L_l}-1,n_{L_l-1}-1}} \end{bmatrix} , D=\begin{bmatrix} b^{0}_0\\ b^{0}_1\\ \vdots\\ b^{L_l}_{n_{L_l}-1}\\ w^{1}_{0,0}\\ w^{1}_{0,1}\\ \vdots\\ w^{L_l}_{n_{L_l}-1,n_{L_l-1}-1} \end{bmatrix} Ck= b00Ckb10CkbnLl1LlCkw0,01Ckw0,11CkwnLl1,nLl11LlCk ,D= b00b10bnLl1Llw0,01w0,11wnLl1,nLl11Ll
当我们调整网络的时候,可以使用一个参数 η \eta η 调整这个数据对学习的影响,而调整方式,可以用 D ′ = D − η ⋅ ∇ C k D'=D-\eta \cdot \nabla C_k D=DηCk 表示对这个数据的学习。可想而知 η \eta η 越大,这个数据对网络的影响越强。而 η \eta η 的调节,是一个需要讨论的问题,但是本人不太懂,所以本文中的 η = 1 \eta =1 η=1

Mini-Batch

如果对于每个数据,都进行一次调整 ,那么效率会变得很低,为此,我们可以使用一个小技巧,也就是 Mini-Batch \texttt{Mini-Batch} Mini-Batch 。具体操作方式如下:

首先设定一个大小 S S S ,本文中,我们设其为 10 10 10 。接着,将数据集打乱,然后每 S S S 个数据组成一个 B a t c h \mathrm{Batch} Batch 。对于每一个 B a t c h \mathrm{Batch} Batch ,设其中的数据编号为 1 , 2 , 3 , … , S 1,2,3,\dots, S 1,2,3,,S ,对于每一个数据 i i i ,都用 BP \texttt{BP} BP 计算出一个 ∇ C i \nabla C_i Ci ,接着将 B a t c h \mathrm{Batch} Batch 中的调整信息整合,这里我们用平均值进行整合: ∇ C = 1 S ∑ i = 1 S ∇ C i \nabla C=\frac{1}{S}\sum_{i=1}^S \nabla C_i C=S1i=1SCi ,接着用这个 ∇ C \nabla C C 调整神经网络。

代码

NN . h \texttt{NN}.h NN.h

#pragma once

#include <vector>
#include <cstdarg>
#include "gloconst.h"


class Network {
private:
	double **a, **b, **da, **db, **z;
	double ***w, ***dw;
	std::vector<int> network_size;
	/// <summary>
	/// 进行逆向传播
	/// </summary>
	/// <param name="output">学习的输出</param>
	void Backprop(double *output);
public:
	Network(std::vector<int> &network_size);
	~Network();
	
	/// <summary>
	/// 计算一个数据
	/// </summary>
	/// <param name="input">输入</param>
	void Calculate(double *input);
	/// <summary>
	/// 对模型进行训练
	/// </summary>
	/// <param name="data">数据 data[i][0] 表示第i个输入,data[i][1] 表示第i个输出</param>
	/// <param name="size">数据的数量</param>
	/// <param name="batch_size">mini-batch的大小</param>
	void Train(double ***data, int size, int batch_size = 100);
	/// <summary>
	/// 得到输出值a最大的节点的id
	/// </summary>
	/// <returns></returns>
	int GetMaxOutputNode();
};

NN.cpp \texttt{NN.cpp} NN.cpp

#include "NN.h"
#include <algorithm>
#include <ctime>
#include <cassert>
#include <random>

unsigned seed = time(NULL);
std::default_random_engine gen(seed);
std::normal_distribution<double> dis(0, 1);

//生成正态分布的随机数 mu=0,sigma=1
inline double randn() {
	return dis(gen);
}
Network::Network(std::vector<int> &network_size) {
	
	this->network_size = network_size;
	a = new double *[network_size.size()];
	b = new double *[network_size.size()];
	da = new double *[network_size.size()];
	db = new double *[network_size.size()];
	z = new double *[network_size.size()];
	w = new double **[network_size.size() - 1];
	dw = new double **[network_size.size() - 1];

	for (int i = 0; i < network_size.size(); i++) { 
		int size_i = network_size[i];
		b[i] = new double[size_i], a[i] = new double[size_i], da[i] = new double[size_i], db[i] = new double[size_i], z[i] = new double[size_i];
		for (int j = 0; j < size_i; j++) b[i][j] = randn();
		if (i) {
			w[i] = new double *[size_i];
			dw[i] = new double *[size_i];
			for (int j = 0; j < size_i; j++) {
				int size2 = network_size[i - 1];
				w[i][j] = new double[size2];
				dw[i][j] = new double[size2];

				for (int k = 0; k < size2; k++)
					w[i][j][k] = randn();
			}
		}
	}
}

Network::~Network() {
	for (int i = 0; i < network_size.size(); i++) {
		int size_i = network_size[i];
		delete[] a[i], delete[] b[i], delete[] da[i], delete[] db[i], delete[] z[i];
		if (i) for (int j = 0; j < size_i; j++)
				delete[] w[i][j], delete[] dw[i][j];
	}
}

void Network::Calculate(double *input) {
	memcpy(a[0], input, sizeof(double) * network_size[0]);
	for (int i = 1; i < network_size.size(); i++) {
		int sizei = network_size[i], size_lst = network_size[i - 1];
		for (int j = 0; j < sizei; j++) {
			z[i][j] = b[i][j];
			for (int k = 0; k < size_lst; k++) z[i][j] += w[i][j][k] * a[i - 1][k];
			a[i][j] = Sigmoid(z[i][j]);
		}
	}
}

void Network::Backprop(double *output) {
	int lstid = network_size.size() - 1, sizei = network_size[network_size.size() - 1];
	for (int i = 0; i < sizei; i++) {
		da[lstid][i] = (a[lstid][i] - output[i]),
		db[lstid][i] = DSigmoid(z[lstid][i]) * da[lstid][i];
		for (int j = 0; j < network_size[lstid - 1]; j++)
			dw[lstid][i][j] = a[lstid - 1][j] * db[lstid][i];
	}
	for (int i = lstid - 1; i > 0; i--) {
		for (int j = 0; j < network_size[i]; j++) {
			da[i][j] = 0;
			for (int k = 0; k < network_size[i + 1]; k++)
				da[i][j] += w[i + 1][k][j] * db[i + 1][k];
			db[i][j] = DSigmoid(z[i][j]) * da[i][j];
			for (int k = 0; k < network_size[i - 1]; k++)
				dw[i][j][k] = a[i - 1][k] * db[i][j];
		}
	}
}

void array_add(double *dst, double *src, int sz) {
	for (int i = 0; i < sz; i++) dst[i] += src[i];
}
void array_mul(double *dst, double x, int sz) {
	for (int i = 0; i < sz; i++) dst[i] *= x;
}

void Network::Train(double ***data, int size, int batch_size) {
	batch_size = std::min(batch_size, size);
	std::random_shuffle(data, data + size);
	double **avr_db, ***avr_dw;
	avr_db = new double *[network_size.size()];
	avr_dw = new double **[network_size.size()];
	for (int i = 0; i < network_size.size(); i++) {
		avr_db[i] = new double[network_size[i]];
		avr_dw[i] = new double*[network_size[i]];
		if (i) for (int j = 0; j < network_size[i]; j++)
				avr_dw[i][j] = new double[network_size[i - 1]];
	}
	const double eta = 1;
	for (int st = 0; st < size; st += batch_size) {
		//clean the avr array
		for (int i = 0; i < network_size.size(); i++) {
			memset(avr_db[i], 0, sizeof(double) * network_size[i]);
			if (i) for (int j = 0; j < network_size[i]; j++)
					memset(avr_dw[i][j], 0, sizeof(double) * network_size[i - 1]);
		}
		for (int x = st; x < st + batch_size; x++) {
			Calculate(data[x][0]);
			Backprop(data[x][1]);
			for (int i = 0; i < network_size.size(); i++) {
				array_add(avr_db[i], db[i], network_size[i]);
				if (i) for (int j = 0; j < network_size[i]; j++)
						array_add(avr_dw[i][j], dw[i][j], network_size[i - 1]);
			}
			
		}
		for (int i = 1; i < network_size.size(); i++) {
			array_mul(avr_db[i], -eta / batch_size, network_size[i]);
			array_add(b[i], avr_db[i], network_size[i]);
			if (i) for (int j = 0; j < network_size[i]; j++) {
					array_mul(avr_dw[i][j], -eta / batch_size, network_size[i - 1]),
					array_add(w[i][j], avr_dw[i][j], network_size[i - 1]);
				}
		}
	}
	for (int i = 0; i < network_size.size(); i++) {
		delete[] avr_db[i];
		if (i) {
			for (int j = 0; j < network_size[i]; j++) delete[] avr_dw[i][j];
			delete[] avr_dw[i];
		}
	}
	delete[] avr_db, delete[] avr_dw;
}

int Network::GetMaxOutputNode() {
	int res = 0;
	double *lyr = a[network_size.size() - 1];
	int size = network_size[network_size.size() - 1];
	for (int i = 1; i < size; i++)
		res = (lyr[res] > lyr[i] ? res : i);
	return res;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值