1. 程式人生 > >深度學習2---任意結點數的三層全連線神經網路

深度學習2---任意結點數的三層全連線神經網路

上一篇文章:深度學習1—最簡單的全連線神經網路 
  我們完成了一個三層(輸入+隱含+輸出)且每層都具有兩個節點的全連線神經網路的原理分析和程式碼編寫。本篇文章將進一步探討如何把每層固定的兩個節點變成任意個節點,以方便我們下一篇文章用本篇文章完成的網路來訓練手寫字符集“mnist”。
  對於前向傳播,基本上沒有什麼變化,就不用說了。主要看看後向傳播的梯度下降公式。先放上上篇文章的網路圖。
在這裡插入圖片描述
  上篇文章我們推知,含有兩個節點的隱含層到輸出層的權值對誤差的偏導數,公式如下:
  EW11()=(O1T1)O1(1O1)Y1\frac{\partial E_{總}}{\partial W_{11}^{(2)}}=(O_{1}-T_{1})*O_{1}(1-O_{1})*Y_{1}


  而含有兩個節點的輸入層到隱含層的權值對於誤差梯度的偏導數公式如下:
  EW11(1)=((O1T1)O1(1O1)W11(2)+(O2T2)O2(1O2)W21(2))Y1(1Y1)X1\frac{\partial E_{總}}{\partial W_{11}^{(1)}}=((O_{1}-T_{1})*O_{1}(1-O_{1})*W_{11}^{(2)}+(O_{2}-T_{2})*O_{2}(1-O_{2})*W_{21}^{(2)})*Y_{1}(1-Y_{1})*X_{1}

  現在我們來總結規律,先看第一道公式:
  W11(2)W_{11}^{(2)}位於輸出層第一個結點的後方,與隱含層第一個結點相連線,其對總誤差求偏導的式子結果也只與這兩個節點的相關引數有關,與輸入層、隱含層的第二個節點、輸出層的第二個節點均無關。因此,也可以說,無論各層的節點數怎麼樣變化,隱含層到輸出層的權值只與它連線的兩個節點的引數相關。上式可以寫成:
  EWxy(2)=(OxTx)Ox
(1Ox)Yy\frac{\partial E_{總}}{\partial W_{xy}^{(2)}}=(O_{x}-T_{x})*O_{x}(1-O_{x})*Y_{y}

  再看第二道公式:
  W11(1)W_{11}^{(1)}位於隱含層第一個節點的後方,與輸入層第一個節點相連線,第一個隱含層節點又與輸出層所有節點相連線。公式中出現的項與上面描述到的節點相關,因此如果輸出層節點增加,則表示式需要增加對增加節點的計算。而隱含層和輸入層增加則無所謂。因此表示式可以寫成:
  EWxy(1)=[k=1n(OkTk)Ok(1Ok)Wkx(2)]Yx(1Yx)Xy\frac{\partial E_{總}}{\partial W_{xy}^{(1)}}=[\sum_{k=1}^{n}(O_{k}-T_{k})*O_{k}(1-O_{k})*W_{kx}^{(2)}]*Y_{x}(1-Y_{x})*X_{y}
  大功告成!規律總結完了,下面就只剩下寫程式碼了。不得不承認,這一篇原理部分蠻少的,應該只能算是上一篇文章的補充。但從固定到任意還是挺重要的,因此獨立出來寫。
  下面我們用程式碼來實現一個任意節點數(程式碼中取的輸入層、隱含層、輸出層分別為5、10、5個節點,當然你高興的話可以隨意改動)的三層全連線神經網路。

C++實現

直接上程式碼

#include <iostream>
#include <cmath>
#include<ctime>
using namespace std;

#define IPNNUM 5     //輸入層節點數
#define HDNNUM 10    //隱含層節點數
#define OPNNUM 5     //輸出層節點數

//結點類,用以構成網路
class node 
{
public:
	double value; //數值,儲存結點最後的狀態
	double *W=NULL;    //結點到下一層的權值

	void initNode(int num);//初始化函式,必須呼叫以初始化權值個數
	~node();	  //解構函式,釋放掉權值佔用記憶體
};

void node::initNode(int num)
{
	W = new double[num];
	srand((unsigned)time(NULL));
	for (size_t i = 0; i < num; i++)//給權值賦一個隨機值
	{
		W[i]= (rand() % 100)/(double)100;
	}
}

node::~node()
{
	if (W!=NULL)
	{
		delete[]W;
	}
}

//網路類,描述神經網路的結構並實現前向傳播以及後向傳播
class net 
{
public:
	node inlayer[IPNNUM]; //輸入層
	node hidlayer[HDNNUM];//隱含層
	node outlayer[OPNNUM];//輸出層

	double yita = 0.1;//學習率η
	double k1;//輸入層偏置項權重
	double k2;//隱含層偏置項權重
	double Tg[OPNNUM];//訓練目標
	double O[OPNNUM];//網路實際輸出

	net();//建構函式,用於初始化各層和偏置項權重
	double sigmoid(double z);//啟用函式
	double getLoss();//損失函式,輸入為目標值
	void forwardPropagation(double *input);//前向傳播,輸入為輸入層節點的值
	void backPropagation(double *T);//反向傳播,輸入為目標輸出值
	void printresual(int trainingTimes);//列印資訊
};

net::net()
{
	//初始化輸入層和隱含層偏置項權值,給一個隨機值
	srand((unsigned)time(NULL));
	k1= (rand() % 100) / (double)100;
	k2 = (rand() % 100) / (double)100;
	//初始化輸入層到隱含層節點個數
	for (size_t i = 0; i < IPNNUM; i++)
	{
		inlayer[i].initNode(HDNNUM);
	}
	//初始化隱含層到輸出層節點個數
	for (size_t i = 0; i < HDNNUM; i++)
	{
		hidlayer[i].initNode(OPNNUM);
	}
}
//啟用函式
double net::sigmoid(double z)
{
	return 1/(1+ exp(-z));
}
//損失函式
double net::getLoss()
{
	double mloss = 0;
	for (size_t i = 0; i < OPNNUM; i++)
	{
		mloss += pow(O[i] - Tg[i], 2);
	}
	return mloss / OPNNUM;
}
//前向傳播
void net::forwardPropagation(double *input)
{
	for (size_t iNNum = 0; iNNum < IPNNUM; iNNum++)//輸入層節點賦值
	{
		inlayer[iNNum].value = input[iNNum];
	}
	for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)//算出隱含層結點的值
	{
		double z = 0;
		for (size_t iNNum = 0; iNNum < IPNNUM; iNNum++)
		{
			z+= inlayer[iNNum].value*inlayer[iNNum].W[hNNum];
		}
		z+= k1;//加上偏置項
		hidlayer[hNNum].value = sigmoid(z);
	}
	for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)//算出輸出層結點的值
	{
		double z = 0;
		for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)
		{
			z += hidlayer[hNNum].value*hidlayer[hNNum].W[oNNum];
		}
		z += k2;//加上偏置項
		O[oNNum] = outlayer[oNNum].value = sigmoid(z);
	}
}
//反向傳播,這裡為了公式好看一點多寫了一些變數作為中間值
//計算過程用到的公式在博文中已經推導過了,如果程式碼沒看明白請看看博文
void net::backPropagation(double *T)
{	
	for (size_t i = 0; i < OPNNUM; i++)
	{
		Tg[i] = T[i];
	}
	for (size_t iNNum = 0; iNNum < IPNNUM; iNNum++)//更新輸入層權重
	{
		for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)
		{
			double y = hidlayer[hNNum].value;
			double loss = 0;
			for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)
			{
				loss += (O[oNNum] - Tg[oNNum])*O[oNNum] * (1 - O[oNNum])*hidlayer[hNNum].W[oNNum];
			}
			inlayer[iNNum].W[hNNum] -= yita*loss*y*(1 - y)*inlayer[iNNum].value;
		}
	}
	for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)//更新隱含層權重
	{
		for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)
		{
			hidlayer[hNNum].W[oNNum]-= yita*(O[oNNum] - Tg[oNNum])*
				O[oNNum] *(1- O[oNNum])*hidlayer[hNNum].value;
		}
	}
}

void net::printresual(int trainingTimes)
{
	double loss = getLoss();
	cout << "訓練次數:" << trainingTimes << endl;
	cout << "loss:" << loss << endl;
	for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)
	{
		cout << "輸出" << oNNum+1<< ":" << O[oNNum] << endl;
	}
}

void main()
{
	net mnet;
	double minput[IPNNUM] = { 0.1, 0.2, 0.3, 0.4, 0.5 };
	double mtarget[IPNNUM] = { 0.2, 0.4, 0.6, 0.8, 1 };
	for (size_t i = 0; i < 10000; i++)
	{
		mnet.forwardPropagation(minput);//前向傳播
		mnet.backPropagation(mtarget);//反向傳播
		if (i%1000==0)
		{
			mnet.printresual(i);//資訊列印
		}
	}
}

python實現

直接上程式碼

import math
import random
import numpy as np

IPNNUM=5     #輸入層節點數
HDNNUM=10    #隱含層節點數
OPNNUM=3     #輸出層節點數

class node:
    #結點類,用以構成網路
    def __init__(self,connectNum=0):
        self.value=0 #數值,儲存結點最後的狀態,對應到文章示例為X1,Y1等值
        self.W = (2*np.random.random_sample(connectNum)-1)*0.01

class net:
    #網路類,描述神經網路的結構並實現前向傳播以及後向傳播
    def __init__(self):
        #初始化函式,用於初始化各層間節點和偏置項權重
        #輸入層結點
        self.inlayer=[node(HDNNUM)];
        for obj in range(1, IPNNUM):
            self.inlayer.append(node(HDNNUM)) 
        #隱含層結點
        self.hidlayer=[node(OPNNUM)];
        for obj in range(1, HDNNUM):
            self.hidlayer.append(node(OPNNUM))             
        #輸出層結點
        self.outlayer=[node(0)];
        for obj in range(1, OPNNUM):
            self.outlayer=[node(0)]                 

        self.yita = 0.1                                           #學習率η
        self.k1=random.random()                       #輸入層偏置項權重
        self.k2=random.random()                       #隱含層偏置項權重
        self.Tg=np.zeros(OPNNUM)                   #訓練目標
        self.O=np.zeros(OPNNUM)                     #網路實際輸出

    def sigmoid(self,z):
        #啟用函式
        return 1 / (1 + math.exp(-z))

    def getLoss(self):
        #損失函式
        loss=0
        for num in range(0, OPNNUM):
            loss+=pow(self.O[num] -self.Tg[num],2)
        return loss/OPNNUM

    def forwardPropagation(self,input):
        #前向傳播
        for i in range(0, IPNNUM):
            #輸入層節點賦值
            self.inlayer[i].value = input[i]
        for hNNum in range(0,HDNNUM):
             #算出隱含層結點的值
            z = 0
            for iNNum in range(0,IPNNUM):
                z+=self.inlayer[iNNum].value*self.inlayer[iNNum].W[hNNum]
            #加上偏置項
            z+= self.k1
            self.hidlayer[hNNum].value = self.sigmoid(z)
        for oNNum in range(0,OPNNUM):
            #算出輸出層結點的值
            z = 0
            for hNNum in range(0,HDNNUM):
                z += self.hidlayer[hNNum].value* self.hidlayer[hNNum].W[oNNum]
            z += self.k2
            self.O[oNNum] = self.sigmoid(z)

    def backPropagation(self,T):
        #反向傳播,這裡為了公式好看一點多寫了一些變數作為中間值
        for num in range(0, OPNNUM):
            self.Tg[num] = T[num]
        for iNNum in range(0,IPNNUM):
            #更新輸入層權重
            for hNNum in range(0,HDNNUM):
                y = self.hidlayer[hNNum].value
                loss = 0
                for oNNum in range(0, OPNNUM):
                    loss+=(self.O[oNNum] - self.Tg[oNNum])*self.O[oNNum] * (1 - self.O[oNNum])*self.hidlayer[hNNum].W[oNNum]
                self.inlayer[iNNum].W[hNNum] -= self.yita*loss*y*(1- y)*self.inlayer[iNNum].value
        for hNNum in range(0,HDNNUM):
            #更新隱含層權重
            for oNNum in range(0,OPNNUM):
                self.hidlayer[hNNum].W[oNNum]-= self.yita*(self.O[oNNum] - self.Tg[oNNum])*self.O[oNNum]*\
                    (1- self.O[oNNum])*self.hidlayer[hNNum].value

    def printresual(self,trainingTimes):
        #資訊列印
        loss = self.getLoss()
        print("訓練次數:", trainingTimes)
        print("loss",loss)
        for oNNum in range(0,OPNNUM):
            print("輸出",oNNum,":",self.O[oNNum])

#主程式
mnet=net()
input=np.array([0.1,0.2,0.3,0.4,0.5])
target=np.array([0.1,0.4,0.5])

for n in range(0,1000):   
    mnet.forwardPropagation(input)
    mnet.backPropagation(target)
    if (n%200==0):
        mnet.printresual(n)

pytorch的CPU實現

其實pytorch本身就已經支援任意節點數,所以這裡只是隨意的改了下上一篇文章中程式碼的引數部分。

# coding=UTF-8
import time
import torch
import torch.nn as nn
from torch.autograd import Variable

class Net(nn.Module):
    def __init__(self):
        #定義Net的初始化函式,這個函式定義了該神經網路的基本結構
        super(Net, self).__init__() #複製並使用Net的父類的初始化方法,即先執行nn.Module的初始化函式
        self.intohid_layer = nn.Linear(5, 10) #定義輸入層到隱含層的連結關係函式
        self.hidtoout_layer = nn.Linear(10, 5)#定義隱含層到輸出層的連結關係函式

    def forward(self, input):
        #定義該神經網路的向前傳播函式,該函式必須定義,一旦定義成功,向後傳播函式也會自動生成
        x = torch.nn.functional.sigmoid(self.intohid_layer(input))    #輸入input在輸入層經過經過加權和與啟用函式後到達隱含層
        x = torch.nn.functional.sigmoid(self.hidtoout_layer(x))       #類似上面
        return x

mnet = Net()
target=Variable(torch.FloatTensor([0.2, 0.4, 0.6, 0.8, 1]))   #目標輸出
input=Variable(torch.FloatTensor([0.1, 0.2, 0.3, 0.4, 0.5]))    #輸入

loss_fn = torch.nn.MSELoss()                       #損失函式定義,可修改
optimizer = torch.optim.SGD(mnet.parameters(), lr=0.5, momentum=0.9);

start = time.time()

for t in range(0,5000):
    optimizer.zero_grad()      #清空節點值
    out=mnet(input)            #前向傳播
    loss = loss_fn(out,target) #損失計算
    loss.backward()            #後向傳播
    optimizer.step()           #更新權值
    if (t%1000==0):
        print(out)

end = time.time()
print(end - start)

pytorch的GPU實現

直接上程式碼

# coding=UTF-8
import time
import torch
import torch.nn as nn
from torch.autograd import Variable

class Net(nn.Module):
    def __init__(self):
        #定義Net的初始化函式,這個函式定義了該神經網路的基本結構
        super(Net, self).__init__() #複製並使用Net的父類的初始化方法,即先執行nn.Module的初始化函式
        self.intohid_layer = nn.Linear(5, 10) #定義輸入層到隱含層的連結關係函式
        self.hidtoout_layer = nn.Linear(10, 5)#定義隱含層到輸出層的連結關係函式

    def forward(self, input):
        #定義該神經網路的向前傳播函式,該函式必須定義,一旦定義成功,向後傳播函式也會自動生成
        x = torch.nn.functional.sigmoid(self.intohid_layer(input))    #輸入input在輸入層經過經過加權和與啟用函式後到達隱含層
        x = torch.nn.functional.sigmoid(self.hidtoout_layer(x))       #類似上面
        return x

mnet = Net().cuda()
target=Variable(torch.cuda.FloatTensor([0.2, 0.4, 0.6, 0.8, 1]))   #目標輸出
input=Variable(torch.cuda.FloatTensor([0.1, 0.2, 0.3, 0.4, 0.5]))    #輸入

loss_fn = torch.nn.MSELoss()                       #損失函式定義,可修改
optimizer = torch.optim.SGD(mnet.parameters(), lr=0.5, momentum=0.9);

start = time.time()

for t in range(0,5000):
    optimizer.zero_grad()      #清空節點值
    out=mnet(input)            #前向傳播
    loss = loss_fn(out,target) #損失計算
    loss.backward()            #後向傳播
    optimizer.step()           #更新權值
    if (t%1000==0):
        print(out)

end = time.time()
print(end - start)

到此就用程式碼實現了任意結點數的三層全連線神經網路,程式碼執行的結果都是對的,但相對來說pytorch收斂的快一些,可能跟其預設初始化的引數有關(自己寫的程式碼都是用隨機數初始化的)。
  下一篇文章我們將用一個輸入層、隱含層、輸出層分別為784、100、10的三層全連線神經網路來訓練聞名已久的MNIST手寫數字字符集,然後自己手寫一個數字來看看網路是否能比較給力的工作。
  最後再預告下篇文章,深度學習3—用三層全連線神經網路訓練MNIST手寫數字字符集
  另外寫文章累人,寫程式碼掉頭髮,如果覺得文章有幫助,哈哈哈
在這裡插入圖片描述