python面向对象、向量化来实现神经网络和反向传播(三)
現(xiàn)在,我們要根據(jù)前面的算法,實(shí)現(xiàn)一個基本的全連接神經(jīng)網(wǎng)絡(luò),這并不需要太多代碼。我們在這里依然采用面向?qū)ο笤O(shè)計。
理論知識參考:https://www.zybuluo.com/hanbingtao/note/476663,這里只擼代碼。
由于自身的對象編程意識比較弱,這里重點(diǎn)分析下算法的面向?qū)ο缶幊獭?/p>
# -*- coding: utf-8 -*- #!/usr/bin/env python # -*- coding: UTF-8 -*- import random from numpy import *# 定義激活函數(shù) def sigmoid(inX):return 1.0 / (1 + exp(-inX))# 定義節(jié)點(diǎn)類:負(fù)責(zé)記錄和維護(hù)節(jié)點(diǎn)自身信息以及與這個節(jié)點(diǎn)相關(guān)的上下游連接,實(shí)現(xiàn)輸出值和誤差項的計算。 class Node(object):def __init__(self, layer_index, node_index):'''構(gòu)造節(jié)點(diǎn)對象。layer_index: 節(jié)點(diǎn)所屬的層的編號node_index: 節(jié)點(diǎn)的編號'''self.layer_index = layer_indexself.node_index = node_indexself.downstream = [] self.upstream = []self.output = 0self.delta = 0def set_output(self, output):'''設(shè)置節(jié)點(diǎn)的輸出值。如果節(jié)點(diǎn)屬于輸入層會用到這個函數(shù)。'''self.output = outputdef calc_output(self):'''根據(jù)式1計算節(jié)點(diǎn)的輸出'''output = reduce(lambda ret, conn: ret + conn.upstream_node.output * conn.weight, self.upstream, 0)self.output = sigmoid(output)def append_downstream_connection(self, conn):'''添加一個到下游節(jié)點(diǎn)的連接'''self.downstream.append(conn)def append_upstream_connection(self, conn):'''添加一個到上游節(jié)點(diǎn)的連接'''self.upstream.append(conn)def calc_hidden_layer_delta(self):'''節(jié)點(diǎn)屬于隱藏層時,根據(jù)式4計算delta'''downstream_delta = reduce(lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,self.downstream, 0.0)self.delta = self.output * (1 - self.output) * downstream_deltadef calc_output_layer_delta(self, label):'''節(jié)點(diǎn)屬于輸出層時,根據(jù)式3計算delta'''self.delta = self.output * (1 - self.output) * (label - self.output)def __str__(self):'''打印節(jié)點(diǎn)的信息'''node_str = '%u-%u: output: %f delta: %f' % (self.layer_index, self.node_index, self.output, self.delta)downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')upstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.upstream, '')return node_str + '\n\tdownstream:' + downstream_str + '\n\tupstream:' + upstream_str # ConstNode對象,為了實(shí)現(xiàn)一個輸出恒為1的節(jié)點(diǎn)(計算偏置項時需要) class ConstNode(object):def __init__(self, layer_index, node_index):'''構(gòu)造節(jié)點(diǎn)對象layer_index: 節(jié)點(diǎn)所屬的層的編號node_index: 節(jié)點(diǎn)的編號''' self.layer_index = layer_indexself.node_index = node_indexself.downstream = []self.output = 1def append_downstream_connection(self, conn):'''添加一個到下游節(jié)點(diǎn)的連接''' self.downstream.append(conn)def calc_hidden_layer_delta(self):'''節(jié)點(diǎn)屬于隱藏層時,根據(jù)式4計算delta'''downstream_delta = reduce(lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,self.downstream, 0.0)self.delta = self.output * (1 - self.output) * downstream_deltadef __str__(self):'''打印節(jié)點(diǎn)的信息'''node_str = '%u-%u: output: 1' % (self.layer_index, self.node_index)downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')return node_str + '\n\tdownstream:' + downstream_str# Layer對象,負(fù)責(zé)初始化一層。此外,作為Node的集合對象,提供對Node集合的操作 class Layer(object):def __init__(self, layer_index, node_count):'''初始化一層layer_index: 層編號node_count: 層所包含的節(jié)點(diǎn)個數(shù)'''self.layer_index = layer_indexself.nodes = [] # 節(jié)點(diǎn)對象存儲到數(shù)列中for i in range(node_count): # 對每層的各個節(jié)點(diǎn)建立節(jié)點(diǎn)對象self.nodes.append(Node(layer_index, i))self.nodes.append(ConstNode(layer_index, node_count)) # 節(jié)點(diǎn)數(shù)列添加ConstNode對象,為了實(shí)現(xiàn)一個輸出恒為1的節(jié)點(diǎn)def set_output(self, data):'''設(shè)置層的輸出。當(dāng)層是輸入層時會用到。'''for i in range(len(data)): # 直接讓每個節(jié)點(diǎn)的輸出就是輸入的向量self.nodes[i].set_output(data[i]) def calc_output(self):'''計算層的輸出向量'''for node in self.nodes[:-1]:node.calc_output()def dump(self):'''打印層的信息'''for node in self.nodes:print node# Connection對象,主要職責(zé)是記錄連接的權(quán)重,以及這個連接所關(guān)聯(lián)的上下游節(jié)點(diǎn) class Connection(object):def __init__(self, upstream_node, downstream_node):'''初始化連接,權(quán)重初始化為是一個很小的隨機(jī)數(shù)upstream_node: 連接的上游節(jié)點(diǎn)downstream_node: 連接的下游節(jié)點(diǎn)'''self.upstream_node = upstream_nodeself.downstream_node = downstream_nodeself.weight = random.uniform(-0.1, 0.1)self.gradient = 0.0def calc_gradient(self):'''計算梯度'''self.gradient = self.downstream_node.delta * self.upstream_node.outputdef update_weight(self, rate):'''根據(jù)梯度下降算法更新權(quán)重'''self.calc_gradient()self.weight += rate * self.gradientdef get_gradient(self):'''獲取當(dāng)前的梯度'''return self.gradientdef __str__(self):'''打印連接信息'''return '(%u-%u) -> (%u-%u) = %f' % (self.upstream_node.layer_index, self.upstream_node.node_index,self.downstream_node.layer_index, self.downstream_node.node_index, self.weight)# Connections對象,提供Connection集合操作 class Connections(object):def __init__(self):self.connections = []def add_connection(self, connection):self.connections.append(connection)def dump(self):for conn in self.connections:print conn# Network對象,提供API class Network(object):def __init__(self, layers):'''初始化一個全連接神經(jīng)網(wǎng)絡(luò)layers: 二維數(shù)組,描述神經(jīng)網(wǎng)絡(luò)每層節(jié)點(diǎn)數(shù)'''self.connections = Connections()self.layers = [] # 存儲每層網(wǎng)絡(luò)的信息:第幾層網(wǎng)絡(luò),每層網(wǎng)絡(luò)的節(jié)點(diǎn)數(shù),每個節(jié)點(diǎn)的信息# 進(jìn)行其他對象的調(diào)用和初始化連接權(quán)重layer_count = len(layers)node_count = 0for i in range(layer_count): # 定義Layer對象,layers數(shù)組層存放的是每個層的節(jié)點(diǎn)對象 self.layers.append(Layer(i, layers[i])) #print 'layers:\n',self.layers[i].dump() for layer in range(layer_count - 1): # 遍歷第一層和第二層到第三層,進(jìn)行節(jié)點(diǎn)間的連接#print 'layer:',layerconnections = [Connection(upstream_node, downstream_node) for upstream_node in self.layers[layer].nodesfor downstream_node in self.layers[layer + 1].nodes[:-1]] print len(connections ) # 打印權(quán)重個數(shù)for conn in connections:#print 'conn:::',connself.connections.add_connection(conn) # 對于兩層間的連接,進(jìn)行集合操作conn.downstream_node.append_upstream_connection(conn) # 初始化全連接網(wǎng)絡(luò)的權(quán)重conn.upstream_node.append_downstream_connection(conn)#print 'conn.upstream_node:',conn.upstream_node#print 'conn.downstream_node:',conn.downstream_nodedef train(self, labels, data_set, rate, epoch):'''訓(xùn)練神經(jīng)網(wǎng)絡(luò)labels: 數(shù)組,訓(xùn)練樣本標(biāo)簽。每個元素是一個樣本的標(biāo)簽。data_set: 二維數(shù)組,訓(xùn)練樣本特征。每個元素是一個樣本的特征。'''for i in range(epoch): # 遍歷每一次迭代for d in range(len(data_set)): # 遍歷每一個樣本,進(jìn)行每一個樣本的訓(xùn)練self.train_one_sample(labels[d], data_set[d], rate)# print 'sample %d training finished' % ddef train_one_sample(self, label, sample, rate):'''內(nèi)部函數(shù),用一個樣本訓(xùn)練網(wǎng)絡(luò)'''self.predict(sample)self.calc_delta(label)self.update_weight(rate)def calc_delta(self, label):'''內(nèi)部函數(shù),計算每個節(jié)點(diǎn)的delta'''output_nodes = self.layers[-1].nodesfor i in range(len(label)): # 計算輸出層的每個節(jié)點(diǎn)的誤差項output_nodes[i].calc_output_layer_delta(label[i])for layer in self.layers[-2::-1]: # 依次計算隱層和輸入層節(jié)點(diǎn)的誤差項for node in layer.nodes:node.calc_hidden_layer_delta()def update_weight(self, rate):'''內(nèi)部函數(shù),更新每個連接權(quán)重'''for layer in self.layers[:-1]:for node in layer.nodes:for conn in node.downstream:conn.update_weight(rate)def calc_gradient(self):'''內(nèi)部函數(shù),計算每個連接的梯度'''for layer in self.layers[:-1]:for node in layer.nodes:for conn in node.downstream:conn.calc_gradient()def get_gradient(self, label, sample):'''獲得網(wǎng)絡(luò)在一個樣本下,每個連接上的梯度label: 樣本標(biāo)簽sample: 樣本輸入'''self.predict(sample)self.calc_delta(label)self.calc_gradient()def predict(self, sample):'''根據(jù)輸入的樣本預(yù)測輸出值(前向傳播)sample: 數(shù)組,樣本的特征,也就是網(wǎng)絡(luò)的輸入向量'''self.layers[0].set_output(sample) # 第一層的輸出for i in range(1, len(self.layers)): # 后面兩層的輸出計算self.layers[i].calc_output()return map(lambda node: node.output, self.layers[-1].nodes[:-1]) # 返回輸出層的每個節(jié)點(diǎn)的輸出def dump(self):for layer in self.layers:layer.dump()class Normalizer(object):def __init__(self):self.mask = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80 ] # 1,2,4,8,16,32,64,128def norm(self, number):return map(lambda m: 0.9 if number & m else 0.1, self.mask) # & 是位運(yùn)算,and 是邏輯運(yùn)算。def denorm(self, vec):binary = map(lambda i: 1 if i > 0.5 else 0, vec)for i in range(len(self.mask)):binary[i] = binary[i] * self.mask[i]return reduce(lambda x,y: x + y, binary)def mean_square_error(vec1, vec2):return 0.5 * reduce(lambda a, b: a + b, map(lambda v: (v[0] - v[1]) * (v[0] - v[1]),zip(vec1, vec2)))# 構(gòu)造訓(xùn)練數(shù)據(jù)集 def train_data_set():normalizer = Normalizer()data_set = []labels = []for i in range(0, 256, 8):n = normalizer.norm(int(random.uniform(0, 256)))#print 'n:',ndata_set.append(n)labels.append(n)return labels, data_set# 訓(xùn)練網(wǎng)絡(luò) def train(network):labels, data_set = train_data_set()#print 'labels, data_set:',labels, '\n',data_setnetwork.train(labels, data_set, 0.2, 100)def test(network, data):normalizer = Normalizer()norm_data = normalizer.norm(data)predict_data = network.predict(norm_data)print '\ttestdata(%u)\tpredict(%u)' % (data, normalizer.denorm(predict_data))def correct_ratio(network):normalizer = Normalizer()correct = 0.0;for i in range(256):if normalizer.denorm(network.predict(normalizer.norm(i))) == i:correct += 1.0print 'correct_ratio: %.2f%%' % (correct / 256 * 100)if __name__ == '__main__':net = Network([8, 3, 8]) train(net)net.dump()correct_ratio(net)
運(yùn)行結(jié)果:
27 32 0-0: output: 0.100000 delta: 0.000946downstream:(0-0) -> (1-0) = 0.018301(0-0) -> (1-1) = -2.615214(0-0) -> (1-2) = 0.622107upstream: 0-1: output: 0.100000 delta: 0.001988downstream:(0-1) -> (1-0) = 0.673368(0-1) -> (1-1) = 0.341187(0-1) -> (1-2) = 0.489427upstream:... ... ...2-7: output: 0.437030 delta: 0.113907downstream:upstream:(1-0) -> (2-7) = 1.519429(1-1) -> (2-7) = -1.701739(1-2) -> (2-7) = 0.632402(1-3) -> (2-7) = 0.005931 2-8: output: 1downstream: correct_ratio: 6.64%這里有幾個地方需要注意下:
https://www.zybuluo.com/hanbingtao/note/476663
下面改變網(wǎng)絡(luò)層后的運(yùn)行結(jié)果:
(1)Network([8, 5, 8])
if __name__ == '__main__':net = Network([8, 5, 8]) train(net)net.dump()correct_ratio(net)正確率:
correct_ratio: 21.09%(2)Network([8, 8, 8])
if __name__ == '__main__':net = Network([8, 8, 8]) train(net)net.dump()correct_ratio(net)正確率:
correct_ratio: 36.72%(3)Network([8, 20, 8])
if __name__ == '__main__':net = Network([8, 20, 8]) train(net)net.dump()correct_ratio(net)正確率:
correct_ratio: 92.19%可以看出隨著網(wǎng)絡(luò)層的增加,正確率是逐漸增高的,但也不是絕對的增高,而且每次的運(yùn)行結(jié)果可能不同,最好的一次是92.19%的正確率,這里主要是關(guān)注編程的方法和思想,而訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)的構(gòu)建不是很科學(xué),所以正確率不穩(wěn)定。
2. 向量化編程
在經(jīng)歷了漫長的訓(xùn)練之后,我們可能會想到,肯定有更好的辦法!是的,程序員們,現(xiàn)在我們需要告別面向?qū)ο缶幊塘?#xff0c;轉(zhuǎn)而去使用另外一種更適合深度學(xué)習(xí)算法的編程方式:向量化編程。主要有兩個原因:一個是我們事實(shí)上并不需要真的去定義Node、Connection這樣的對象,直接把數(shù)學(xué)計算實(shí)現(xiàn)了就可以了;另一個原因,是底層算法庫會針對向量運(yùn)算做優(yōu)化(甚至有專用的硬件,比如GPU),程序效率會提升很多。所以,在深度學(xué)習(xí)的世界里,我們總會想法設(shè)法的把計算表達(dá)為向量的形式。
下面,我們用向量化編程的方法,重新實(shí)現(xiàn)前面的全連接神經(jīng)網(wǎng)絡(luò)。
首先,我們需要把所有的計算都表達(dá)為向量的形式。對于全連接神經(jīng)網(wǎng)絡(luò)來說,主要有三個計算公式。
前向計算,我們發(fā)現(xiàn)式2已經(jīng)是向量化的表達(dá)了:
現(xiàn)在,我們根據(jù)上面幾個公式,重新實(shí)現(xiàn)一個類:FullConnectedLayer。它實(shí)現(xiàn)了全連接層的前向和后向計算:
運(yùn)行結(jié)果:
after epoch 1 loss: 0.001780 after epoch 2 loss: 0.000960 after epoch 3 loss: 0.000747 after epoch 4 loss: 0.000691 after epoch 5 loss: 0.000674 after epoch 6 loss: 0.000667 after epoch 7 loss: 0.000665 after epoch 8 loss: 0.000663 after epoch 9 loss: 0.000663 after epoch 10 loss: 0.000663 correct_ratio: 100.00%同樣這里的訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)也不是很科學(xué),所以正確率達(dá)到了100%,這里旨在學(xué)習(xí)編程的過程和思想。
上面這個類一舉取代了原先的Layer、Node、Connection等類,不但代碼更加容易理解,而且運(yùn)行速度也快了很多倍。
向量化的編程看著清爽多了。。
3. 神經(jīng)網(wǎng)絡(luò)實(shí)戰(zhàn)——手寫數(shù)字識別
針對這個任務(wù),我們采用業(yè)界非常流行的MNIST數(shù)據(jù)集。MNIST大約有60000個手寫字母的訓(xùn)練樣本,我們使用它訓(xùn)練我們的神經(jīng)網(wǎng)絡(luò),然后再用訓(xùn)練好的網(wǎng)絡(luò)去識別手寫數(shù)字。
手寫數(shù)字識別是個比較簡單的任務(wù),數(shù)字只可能是0-9中的一個,這是個10分類問題。
輸入層節(jié)點(diǎn)數(shù)是確定的。因?yàn)镸NIST數(shù)據(jù)集每個訓(xùn)練數(shù)據(jù)是28*28的圖片,共784個像素,因此,輸入層節(jié)點(diǎn)數(shù)應(yīng)該是784,每個像素對應(yīng)一個輸入節(jié)點(diǎn)。
輸出層節(jié)點(diǎn)數(shù)也是確定的。因?yàn)槭?0分類,我們可以用10個節(jié)點(diǎn),每個節(jié)點(diǎn)對應(yīng)一個分類。輸出層10個節(jié)點(diǎn)中,輸出最大值的那個節(jié)點(diǎn)對應(yīng)的分類,就是模型的預(yù)測結(jié)果。
隱藏層節(jié)點(diǎn)數(shù)量是不好確定的,從1到100萬都可以。下面有幾個經(jīng)驗(yàn)公式:
因此,我們可以先根據(jù)上面的公式設(shè)置一個隱藏層節(jié)點(diǎn)數(shù)。如果有時間,我們可以設(shè)置不同的節(jié)點(diǎn)數(shù),分別訓(xùn)練,看看哪個效果最好就用哪個。我們先拍一個,設(shè)隱藏層節(jié)點(diǎn)數(shù)為300吧。
對于3層784*300*10的全連接網(wǎng)絡(luò),總共有300*(784+1)+10*(300+1)=238510個參數(shù)!神經(jīng)網(wǎng)絡(luò)之所以強(qiáng)大,是它提供了一種非常簡單的方法去實(shí)現(xiàn)大量的參數(shù)。目前百億參數(shù)、千億樣本的超大規(guī)模神經(jīng)網(wǎng)絡(luò)也是有的。因?yàn)镸NIST只有6萬個訓(xùn)練樣本,參數(shù)太多了很容易過擬合,效果反而不好。
代碼實(shí)現(xiàn)
首先,我們需要把MNIST數(shù)據(jù)集處理為神經(jīng)網(wǎng)絡(luò)能夠接受的形式。MNIST訓(xùn)練集的文件格式可以參考官方網(wǎng)站,這里不在贅述。每個訓(xùn)練樣本是一個28*28的圖像,我們按照行優(yōu)先,把它轉(zhuǎn)化為一個784維的向量。每個標(biāo)簽是0-9的值,我們將其轉(zhuǎn)換為一個10維的one-hot向量:如果標(biāo)簽值為n,我們就把向量的第n維(從0開始編號)設(shè)置為0.9,而其它維設(shè)置為0.1。例如,向量[0.1,0.1,0.9,0.1,0.1,0.1,0.1,0.1,0.1,0.1]表示值2。
運(yùn)行結(jié)果:
image count: 60000 get train_data_set, train_labels... image count: 10000 get test_data_set, test_labels..... get network: fc.py:11: RuntimeWarning: overflow encountered in exp return 1.0 / (1.0 + np.exp(-weighted_input)) <fc.Network object at 0x7f3600119490> 2018-05-09 13:15:09.911136 epoch 1 finished 2018-05-09 13:16:06.405420 epoch 2 finished 2018-05-09 13:17:00.673801 epoch 3 finished 2018-05-09 13:17:52.530145 epoch 4 finished 2018-05-09 13:18:40.793027 epoch 5 finished 2018-05-09 13:19:27.942110 epoch 6 finished 2018-05-09 13:20:13.373169 epoch 7 finished 2018-05-09 13:21:00.198157 epoch 8 finished 2018-05-09 13:21:45.633917 epoch 9 finished 2018-05-09 13:22:31.279932 epoch 10 finished 2018-05-09 13:22:32.188052 after epoch 10, error ratio is 0.496400 2018-05-09 13:23:17.678800 epoch 11 finished 2018-05-09 13:24:03.042221 epoch 12 finished 2018-05-09 13:24:48.333277 epoch 13 finished 2018-05-09 13:25:33.668334 epoch 14 finished 2018-05-09 13:26:18.961969 epoch 15 finished 2018-05-09 13:27:04.220283 epoch 16 finished 2018-05-09 13:27:49.499477 epoch 17 finished 2018-05-09 13:28:34.895613 epoch 18 finished 2018-05-09 13:29:20.178684 epoch 19 finished 2018-05-09 13:30:05.451835 epoch 20 finished 2018-05-09 13:30:06.340205 after epoch 20, error ratio is 0.541500這里有兩個問題:
第一個問題:
- 運(yùn)行中有一個exp的溢出警告,在網(wǎng)上查了下:一種是說numpy.exp返回值在float64中存儲不下,會截取一部分,給出的是個wearing,這里認(rèn)為不影響最終的結(jié)果。
- 另一種是同比縮小數(shù)值大小,以防止溢出。
第二個問題:
錯誤率比較大,所以說還要進(jìn)一步的調(diào)參,這里只是提供一種思路。
總結(jié)
以上是生活随笔為你收集整理的python面向对象、向量化来实现神经网络和反向传播(三)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 餐厅灯数量风水 灯数最好为单
- 下一篇: 二套房在成都首付多少?