本博客內(nèi)容來(lái)源于網(wǎng)絡(luò)以及其他書籍,結(jié)合自己學(xué)習(xí)的心得進(jìn)行重編輯,因?yàn)榭戳撕芏辔恼虏槐阋灰粯?biāo)注引用,如圖片文字等侵權(quán),請(qǐng)告知?jiǎng)h除。
傳統(tǒng)2D計(jì)算機(jī)視覺(jué)學(xué)習(xí)筆記目錄------->傳送門
傳統(tǒng)3D計(jì)算機(jī)視覺(jué)學(xué)習(xí)筆記目錄------->傳送門
深度學(xué)習(xí)學(xué)習(xí)筆記目錄 ------------------->傳送門
本文簡(jiǎn)介
本文的主要目的就是描述出怎么使用numpy實(shí)現(xiàn)一個(gè)簡(jiǎn)單的神經(jīng)網(wǎng)絡(luò),通過(guò)反向傳播完成訓(xùn)練的過(guò)程,正如題目一樣。當(dāng)然我們不會(huì)像成熟的深度學(xué)習(xí)框架一樣內(nèi)部實(shí)現(xiàn)自動(dòng)求導(dǎo),那就太麻煩了。通過(guò)自己手寫這么一份代碼,可以讓自己加深深度神經(jīng)網(wǎng)絡(luò)到底是怎么運(yùn)作的,以達(dá)到我們的目的,而不再是完完全全的黑箱了。
目前網(wǎng)上有很多相關(guān)的文章,我自己也通過(guò)那些文章得到很多的認(rèn)識(shí)再最初學(xué)習(xí)的時(shí)候,但是總是感覺(jué)有一些不足,比如為了追求代碼簡(jiǎn)潔,而失去了結(jié)構(gòu)性,而我們使用的pytorch或者tensorflow有很好的面型對(duì)象的結(jié)構(gòu)。所以本文實(shí)現(xiàn)的代碼更注重結(jié)構(gòu)性,和可拓展性,可以在此基礎(chǔ)上在實(shí)現(xiàn)其他的一些簡(jiǎn)單的層。那么開(kāi)始吧

分步實(shí)現(xiàn)思路
首先我們知道神經(jīng)網(wǎng)絡(luò)是有一些layer(層)組成的的,我們目前主要關(guān)注隱藏層,因?yàn)樯窠?jīng)網(wǎng)絡(luò)的主要計(jì)算是在隱藏層。這些層分別可以進(jìn)行前向推導(dǎo),反向傳播,參數(shù)更新,所以我們先寫這些層的基類,為方便調(diào)試,我們?cè)诔跏蓟悤r(shí),要給層一個(gè)名字。
class BaseLayer:
def __init__(self,name):
self.name = name
def forward(self, input): #前向推導(dǎo)
pass
def backward(self,grad): #反向傳播
pass
def update(self): #參數(shù)更新
pass
接著我們要實(shí)現(xiàn)全連接層,激活函數(shù),以及損失函數(shù)。激活函數(shù)我們實(shí)現(xiàn)簡(jiǎn)單的sigmoid激活函數(shù),損失函數(shù)我們實(shí)現(xiàn)帶有softmax的CrossEntropyLoss。有關(guān)簡(jiǎn)單的激活函數(shù)和損失函數(shù)我會(huì)在其他文章詳細(xì)描述。
我們先實(shí)現(xiàn)sigmoid激活函數(shù),由于sigmoid 中我們不需要更新任何的參數(shù),所以不用重載參數(shù)更新函數(shù)。
class SigmoidLayer(BaseLayer):
def __init__(self, name):
super(SigmoidLayer,self).__init__(name)
def forward(self,input):
self.output = 1/(1+np.exp(-input))
return self.output
def backward(self,grad):
grad = grad * self.output*(1-self.output)
return grad
然后我們實(shí)現(xiàn)全連接層,在此我們將學(xué)習(xí)率簡(jiǎn)化為1,初始參數(shù)設(shè)置為正太分布隨機(jī)參數(shù),優(yōu)化器也是最簡(jiǎn)單的批量梯度下降(BGD)
class LinearLayer(BaseLayer):
def __init__(self,name,input_channels,output_channels):
super(LinearLayer,self).__init__(name)
self.weight = np.random.randn( input_channels,output_channels )
self.bias = np.random.randn(1,output_channels)
def forward(self,input):
self.input = input
self.output = np.dot(self.input,self.weight)+ self.bias # y = wx +b
return self.output
def backward(self,grad):
self.batch_size = grad.shape[0]
self.grad_w = np.dot(self.input.T,grad )/self.batch_size # δw = δg * x
self.grad_b = np.sum( grad , axis=0,keepdims= True )/self.batch_size
grad = np.dot(grad,self.weight.T)
return grad
def update(self):
self.weight -= self.grad_w
self.bias -= self.grad_b
然后我們來(lái)實(shí)現(xiàn)損失函數(shù),以及softmax,我們可以將softmax的反向傳播與CrossEntropy反向傳播一起執(zhí)行,可以簡(jiǎn)化整個(gè)過(guò)程。
class SoftMaxLayer(BaseLayer):
def __init__(self, name):
super(SoftMaxLayer,self).__init__(name)
def forward(self,input):
vec_max = np.max( input,axis=1 )[np.newaxis,:].T
input -= vec_max
exp = np.exp(input)
output = exp / (np.sum(exp,axis=1)[np.newaxis,:].T)
return output
class SMCrossEntropyLossLayer(BaseLayer):
def __init__(self, name):
super(SMCrossEntropyLossLayer,self).__init__(name)
def forward(self,pred,real):
self.softmax_p = SoftMaxLayer("softmax").forward(pred)
self.real = real
loss = 0
for i in range(self.real.shape[0]):
loss += -np.log( self.softmax_p[i,real[i]] )
loss /= self.real.shape[0]
return loss
def backward(self):
for i in range(self.real.shape[0]):
self.softmax_p[i,self.real[i]] -= 1
self.softmax_p = self.softmax_p / self.real.shape[0]
return self.softmax_p
現(xiàn)在我們將神經(jīng)網(wǎng)絡(luò)的基本的幾個(gè)層實(shí)現(xiàn)完了,現(xiàn)在我們要將這些隱層組建成一個(gè)網(wǎng)絡(luò)。我們實(shí)現(xiàn)一個(gè)基本的網(wǎng)絡(luò)框架,然后再通過(guò)新的子類繼承基類,只需要該變隱層結(jié)構(gòu)就可以了。由于準(zhǔn)備訓(xùn)練一個(gè)mnist手寫數(shù)字?jǐn)?shù)據(jù),所以第一層的輸入的維度是784。
class NetBase:
def __init__(self):
self.layers = []
def forward(self,input):
for layer in self.layers:
input = layer.forward(input)
pred = SoftMaxLayer("softmax").forward(input)
return input,pred
def backward(self,grad):
for layer in reversed(self.layers):
grad = layer.backward(grad)
layer.update()
class SimpleNet(NetBase):
def __init__(self):
super(SimpleNet,self).__init__()
self.layers = [
LinearLayer(name="full1",input_channels= 784, output_channels= 512),
SigmoidLayer(name="relu1"),
LinearLayer(name="full2",input_channels=512,output_channels=128),
SigmoidLayer(name="sigmoid2"),
LinearLayer(name="full3",input_channels=128,output_channels=10)
]
整體代碼
現(xiàn)在我們將網(wǎng)絡(luò)結(jié)構(gòu)的代碼以及訓(xùn)練代碼放到一起。
#BaseNet.py
import numpy as np
class BaseLayer:
def __init__(self,name):
self.name = name
def forward(self, input):
pass
def backward(self,grad):
pass
def update(self):
pass
class SigmoidLayer(BaseLayer):
def __init__(self, name):
super(SigmoidLayer,self).__init__(name)
def forward(self,input):
self.output = 1/(1+np.exp(-input))
return self.output
def backward(self,grad):
grad = grad * self.output*(1-self.output)
return grad
class LinearLayer(BaseLayer):
def __init__(self,name,input_channels,output_channels):
super(LinearLayer,self).__init__(name)
self.weight = np.random.randn( input_channels,output_channels )
self.bias = np.random.randn(1,output_channels)
def forward(self,input):
self.input = input
self.output = np.dot(self.input,self.weight)+ self.bias
return self.output
def backward(self,grad):
self.batch_size = grad.shape[0]
self.grad_w = np.dot(self.input.T,grad )/self.batch_size
self.grad_b = np.sum( grad , axis=0,keepdims= True )/self.batch_size
grad = np.dot(grad,self.weight.T)
return grad
def update(self):
self.weight -= self.grad_w
self.bias -= self.grad_b
class SoftMaxLayer(BaseLayer):
def __init__(self, name):
super(SoftMaxLayer,self).__init__(name)
def forward(self,input):
vec_max = np.max( input,axis=1 )[np.newaxis,:].T
input -= vec_max
exp = np.exp(input)
output = exp / (np.sum(exp,axis=1)[np.newaxis,:].T)
return output
class SMCrossEntropyLossLayer(BaseLayer):
def __init__(self, name):
super(SMCrossEntropyLossLayer,self).__init__(name)
def forward(self,pred,real):
self.softmax_p = SoftMaxLayer("softmax").forward(pred)
self.real = real
loss = 0
for i in range(self.real.shape[0]):
loss += -np.log( self.softmax_p[i,real[i]] )
loss /= self.real.shape[0]
return loss
def backward(self):
for i in range(self.real.shape[0]):
self.softmax_p[i,self.real[i]] -= 1
self.softmax_p = self.softmax_p / self.real.shape[0]
return self.softmax_p
class NetBase:
def __init__(self):
self.layers = []
def forward(self,input):
for layer in self.layers:
input = layer.forward(input)
pred = SoftMaxLayer("softmax").forward(input)
return input,pred
def backward(self,grad):
for layer in reversed(self.layers):
grad = layer.backward(grad)
layer.update()
class SimpleNet(NetBase):
def __init__(self):
super(SimpleNet,self).__init__()
self.layers = [
LinearLayer(name="full1",input_channels= 784, output_channels= 512),
SigmoidLayer(name="relu1"),
LinearLayer(name="full2",input_channels=512,output_channels=128),
SigmoidLayer(name="sigmoid2"),
LinearLayer(name="full3",input_channels=128,output_channels=10)
]
訓(xùn)練部分代碼,由于numpy沒(méi)有使用gpu來(lái)進(jìn)行訓(xùn)練,訓(xùn)練整體還是比較慢的,所以我們只訓(xùn)練了 前100個(gè)數(shù)據(jù),通過(guò)觀察loss 就可以驗(yàn)證我們的網(wǎng)絡(luò)是否進(jìn)行工作。
#train.py
import BaseNet
import numpy as np
import matplotlib.pyplot as plt
import os
training_set_inputs = []
training_set_outputs = []
def read_mnist(mnist_image_file, mnist_label_file):
if 'train' in os.path.basename(mnist_image_file):
num_file = 60000
else:
num_file = 10000
with open(mnist_image_file, 'rb') as f1:
image_file = f1.read()
with open(mnist_label_file, 'rb') as f2:
label_file = f2.read()
image_file = image_file[16:]
label_file = label_file[8:]
for i in range(num_file):
label = int(label_file[i])
image_list = [int(item) for item in image_file[i*784:i*784+784]]
image_np = np.array(image_list, dtype=np.uint8).reshape(28*28)
training_set_outputs.append([label])
training_set_inputs.append( image_np )
train_image_file = '/home/eric/data/mnist/train-images-idx3-ubyte'
train_label_file = '/home/eric/data/mnist/train-labels-idx1-ubyte'
read_mnist(train_image_file, train_label_file)
training_set_inputs = np.array( training_set_inputs )
training_set_outputs = np.array( training_set_outputs )
training_set_inputs = training_set_inputs[:100,:]
training_set_outputs = training_set_outputs[:100,:]
net = BaseNet.SimpleNet()
loss = BaseNet.SMCrossEntropyLossLayer("loss")
x = []
y=[]
for i in range(10000):
input = training_set_inputs
output,pred = net.forward(input)
loss_value = np.squeeze(loss.forward(output,training_set_outputs))
print(i,loss_value,np.sum( (np.equal(pred.argmax(axis = 1),training_set_outputs.T)))/ training_set_outputs.shape[0] )
x.append(i)
y.append(loss_value)
delta = loss.backward()
net.backward(delta)
plt.plot(x,y,'r--')
plt.title('loss')
plt.show()
總結(jié)
寫完這篇文章,才發(fā)現(xiàn)代碼太多,沒(méi)有太多的文字?jǐn)⑹?,感覺(jué)要是一點(diǎn)點(diǎn)解釋,怕是累死我,估計(jì)沒(méi)有人像我這么笨吧。自己認(rèn)為學(xué)習(xí)的過(guò)程還是需要自己用手就敲一遍,觀察一下每個(gè)狀態(tài)的輸出,才能更好的理解。雖然代碼很多但是其實(shí)也可以壓縮成十幾行,但是對(duì)初學(xué)者就太不友好了。
重要的事情說(shuō)三遍:
如果我的文章對(duì)您有所幫助,那就點(diǎn)贊加個(gè)關(guān)注唄 ( * ^ __ ^ * )
如果我的文章對(duì)您有所幫助,那就點(diǎn)贊加個(gè)關(guān)注唄 ( * ^ __ ^ * )
如果我的文章對(duì)您有所幫助,那就點(diǎn)贊加個(gè)關(guān)注唄 ( * ^ __ ^ * )
傳統(tǒng)2D計(jì)算機(jī)視覺(jué)學(xué)習(xí)筆記目錄------->傳送門
傳統(tǒng)3D計(jì)算機(jī)視覺(jué)學(xué)習(xí)筆記目錄------->傳送門
深度學(xué)習(xí)學(xué)習(xí)筆記目錄 ------------------->傳送門
任何人或團(tuán)體、機(jī)構(gòu)全部轉(zhuǎn)載或者部分轉(zhuǎn)載、摘錄,請(qǐng)保留本博客鏈接或標(biāo)注來(lái)源。博客地址:開(kāi)飛機(jī)的喬巴
作者簡(jiǎn)介:開(kāi)飛機(jī)的喬巴(WeChat:zhangzheng-thu),現(xiàn)主要從事機(jī)器人抓取視覺(jué)系統(tǒng)以及三維重建等3D視覺(jué)相關(guān)方面,另外對(duì)slam以及深度學(xué)習(xí)技術(shù)也頗感興趣,歡迎加我微信或留言交流相關(guān)工作。