先闡述一些概念性的東西(也是對之前的東西的回顧,記性不好,老忘):
迴歸問題與分類問題:
迴歸:計算圓形面積的例子就屬於迴歸
問題,即我們的目的是對於一個輸入x
,預測其輸出值y
,且這個y
值是根據x
連續變化的值。
分類:分類
問題則是事先給定若干個類別,對於一個輸入x
,判斷其屬於哪個類別,即輸出一般是離散的
監督學習和無監督學習:
監督學習:通過訓練讓機器自己找到特徵和標籤之間的聯繫(注:也就是學習的訓練集包含輸入和輸出,得到了最優參數模型之後 ,新來的數據集在面對只有特徵沒有標籤的情況下時,可以判斷出標籤)
無監督學習:訓練數據中只有特徵沒有標籤,輸入數據沒有被標記,也沒有確定的結果。樣本數據類別未知,需要根據樣本間的相似性對樣本集進行分類。(注:不一定"分類",沒有訓練集,旨在尋找規律性,不予以某種預先分類標籤對上號爲目的)
損失函數,二元損失函數:
quadratic loss function
通過計算h
和y
之間差值的二次方,來表達一個神經網絡的效果。具體形式如下:
J(Θ)=(h(Θ,X)−Y)2J(Θ)=(h(Θ,X)−Y)2
這個公式幾乎就是我們在上面提到的對模型的誤差求平方,只是稍微再複雜一點。其中的希臘字母theta
代表網絡中的所有參數,X
代表向神經網絡輸入的數據,Y
代表輸入數據對應的預期正確輸出值。
實際上,在這裏theta
纔是自變量,因爲我們一開始不知道讓網絡能夠正確的工作的參數值是多少。我們的學習算法learn
按照某種策略,通過不斷的更新參數值來使損失函數J(theta, X, Y)
的值減小。在這裏,theta
是不斷變化的量。那X
和Y
就不是自變量了嗎?對,它們確實不是自變量!因爲對於我們要解決的一個具體的問題(比如圖片字母識別),其訓練數據中包含的信息其實代表的是這個問題本身的性質,我們的學習算法learn
其實最終就是要從訓練數據data
中學習到這些性質,並通過網絡模型model
的結構和其中的參數把這些性質表達出來。對於一個特定的問題來說,可以認爲它們是常量!
梯度下降算法:來幫助我們減小損失函數的值
參數和超參數:
我們將模型model
中的參數theta
稱爲參數
,而學習算法(即梯度下降算法)裏的參數alhpa
被稱爲超參數。
叫法不同,是因爲它們的作用及我們設置它們值的方式不一樣。theta
被稱爲 “參數”,是因爲theta
決定了我們的模型model
的性質,並且theta
的最終值是由我們的學習算法learn
學習得到的,不需要我們手工設定。
而alpha
則不同,在這裏alpha
並不是模型model
中的參數,而是在學習算法learn
中決定我們的梯度下降算法
每一步走多遠的參數,它需要我們手工設定,且決定了得到最優theta
的過程。即超參數
決定如何得到最優參數
。
超參數alpha又被
稱爲學習速率(learning rate)
。因爲alpha
越大時,我們的參數theta
更新的幅度越大,我們可能會更快的到達最低點。但是alpha
不能設置的太大,否則有可能一次變化的太大導致 “步子太長”,會直接越過最低點,甚至導致損失函數值不降反升。
反向傳播算法:
對於一個具體的參數值,我們只需要把每個節點的值代入求得的導函數公式就可以求得導數(偏導數),進而得到梯度。 這很簡單,我們先從計算圖的底部開始向上,逐個節點計算函數值並保存下來。這個步驟,叫做前向計算(forward)
。
然後,我們從計算圖的頂部開始向下,逐步計算損失函數對每個子節點的導函數,代入前向計算
過程中得到的節點值,得到導數值。這個步驟,叫做反向傳播(backward)
或者更明確一點叫做反向梯度傳播
。
全連接層:
我們將層與層之間的每個點都有連接的層叫做全連接(fully connect)層
以sigmoid爲例進行簡易的反向傳播代碼:
import numpy as np
class FullyConnect:
def __init__(self, l_x, l_y): # 兩個參數分別爲輸入層的長度和輸出層的長度
self.weights = np.random.randn(l_y, l_x) # 使用隨機數初始化參數
print(self.weights)
self.bias = np.random.randn(1) # 使用隨機數初始化參數
print(self.bias)
def forward(self, x):
self.x = x # 把中間結果保存下來,以備反向傳播時使用
self.y = np.dot(self.weights, x) + self.bias # 計算w11*a1+w12*a2+bias1
return self.y # 將這一層計算的結果向前傳遞
def backward(self, d):
self.dw = d * self.x # 根據鏈式法則,將反向傳遞回來的導數值乘以x,得到對參數的梯度
self.db = d
self.dx = d * self.weights
return self.dw, self.db # 返回求得的參數梯度,注意這裏如果要繼續反向傳遞梯度,應該返回self.db
class Sigmoid:
def __init__(self): # 無參數,不需初始化
pass
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, x):
self.x = x
self.y = self.sigmoid(x)
return self.y
def backward(self): # 這裏sigmoid是最後一層,所以從這裏開始反向計算梯度
sig = self.sigmoid(self.x)
self.dx = sig * (1 - sig)
return self.dx # 反向傳遞梯度
def main():
fc = FullyConnect(2, 1)
sigmoid = Sigmoid()
x = np.array([[1], [2]])
print('weights:', fc.weights, ' bias:', fc.bias, ' input: ', x)
# 執行前向計算
y1 = fc.forward(x)
y2 = sigmoid.forward(y1)
print('forward result: ', y2)
# 執行反向傳播
d1 = sigmoid.backward()
dx = fc.backward(d1)
print('backward result: ', dx)
if __name__ == '__main__':
main()
激活函數:
在多層神經網絡中,上層節點的輸出和下層節點的輸入之間具有一個函數關係,這個函數稱爲激活函數(又稱激勵函數)。
激活函數的意義也就是加入非線性因素,讓神經網絡具備非線性的表達能力(當然不是真正意義上的非線性,不過可以逼近任意的非線性函數)
epoch:我們稱所有訓練圖片都已參與一遍訓練的一個週期稱爲一個epoch
。每個epoch
結束時,我們會將訓練數據重新打亂,這樣可以獲得更好的訓練效果。我們通常會訓練多個epoch
。
以淺層神經網絡識別圖片中的英文字母案例爲例:
代碼:
# encoding=utf-8
from scipy import misc
import numpy as np
def chuli(src, dst):
with open(src, 'r') as f: # 讀取圖片列表
list = f.readlines()
data = []
labels = []
for i in list:
name, label = i.strip('\n').split(' ') # 將圖片列表中的每一行拆分成圖片名和圖片標籤
img = misc.imread(name) # 將圖片讀取出來,存入一個矩陣
img = img / 255 # 將圖片轉換爲只有0、1值的矩陣
img.resize((img.size, 1)) # 爲了之後的運算方便,我們將圖片存儲到一個img.size*1的列向量裏面
data.append(img)
labels.append(int(label))
print('write to npy')
np.save(dst, [data, labels]) # 將訓練數據以npy的形式保存到成本地文件
print('completed')
#數據層,讀入數據的操作放到一個數據層
class Data:
def __init__(self, name, batch_size): # 數據所在的文件名name和batch中圖片的數量batch_size
with open(name, 'rb') as f:
data = np.load(f)
self.x = data[0] # 輸入x
self.y = data[1] # 預期正確輸出y
self.l = len(self.x)
self.batch_size = batch_size
self.pos = 0 # pos用來記錄數據讀取的位置
def forward(self):
pos = self.pos
bat = self.batch_size
l = self.l
if pos + bat >= l: # 已經是最後一個batch時,返回剩餘的數據,並設置pos爲開始位置0
ret = (self.x[pos:l], self.y[pos:l])
self.pos = 0
index = range(l)
np.random.shuffle(list(index)) # 將訓練數據打亂
self.x = self.x[index]
self.y = self.y[index]
else: # 不是最後一個batch, pos直接加上batch_size
ret = (self.x[pos:pos + bat], self.y[pos:pos + bat])
self.pos += self.batch_size
return ret, self.pos # 返回的pos爲0時代表一個epoch已經結束
def backward(self, d): # 數據層無backward操作
pass
class FullyConnect:
def __init__(self, l_x, l_y): # 兩個參數分別爲輸入層的長度和輸出層的長度
#l_x爲輸入單個數據向量的長度,在這裏是17*17=289,l_y代表全連接層輸出的節點數量,由於大寫英文字母有26個,所以這裏的l_y=26
self.weights = np.random.randn(l_y, l_x) / np.sqrt(l_x) # 使用隨機數初始化參數,請暫時忽略這裏爲什麼多了np.sqrt(l_x)
self.bias = np.random.randn(l_y, 1) # 使用隨機數初始化參數
self.lr = 0 # 先將學習速率初始化爲0,最後統一設置學習速率
def forward(self, x):
self.x = x # 把中間結果保存下來,以備反向傳播時使用
self.y = np.array([np.dot(self.weights, xx) + self.bias for xx in x]) # 計算全連接層的輸出
return self.y # 將這一層計算的結果向前傳遞
def backward(self, d):
ddw = [np.dot(dd, xx.T) for dd, xx in zip(d, self.x)] # 根據鏈式法則,將反向傳遞回來的導數值乘以x,得到對參數的梯度
self.dw = np.sum(ddw, axis=0) / self.x.shape[0]
self.db = np.sum(d, axis=0) / self.x.shape[0]
self.dx = np.array([np.dot(self.weights.T, dd) for dd in d])
# 更新參數
self.weights -= self.lr * self.dw
self.bias -= self.lr * self.db
return self.dx # 反向傳播梯度
#激活函數層
class Sigmoid:
def __init__(self): # 無參數,不需初始化
pass
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, x):
self.x = x
self.y = self.sigmoid(x)
return self.y
def backward(self, d):
sig = self.sigmoid(self.x)
self.dx = d * sig * (1 - sig)
return self.dx # 反向傳遞梯度
#損失函數層
class QuadraticLoss:
def __init__(self):
pass
def forward(self, x, label):
self.x = x
self.label = np.zeros_like(x) # 由於我們的label本身只包含一個數字,我們需要將其轉換成和模型輸出值尺寸相匹配的向量形式
for a, b in zip(self.label, label):
a[b] = 1.0 # 只有正確標籤所代表的位置概率爲1,其他爲0
self.loss = np.sum(np.square(x - self.label)) / self.x.shape[0] / 2 # 求平均後再除以2是爲了表示方便
return self.loss
def backward(self):
self.dx = (self.x - self.label) / self.x.shape[0] # 2被抵消掉了
return self.dx
#準確率層
class Accuracy:
def __init__(self):
pass
def forward(self, x, label): # 只需forward
self.accuracy = np.sum([np.argmax(xx) == ll for xx, ll in zip(x, label)]) # 對預測正確的實例數求和
self.accuracy = 1.0 * self.accuracy / x.shape[0]
return self.accuracy
#構建神經網絡
def main():
datalayer1 = Data('train.npy', 1024) # 用於訓練,batch_size設置爲1024
datalayer2 = Data('validate.npy', 10000) # 用於驗證,所以設置batch_size爲10000,一次性計算所有的樣例
inner_layers = []
inner_layers.append(FullyConnect(17 * 17, 26))
inner_layers.append(Sigmoid())
losslayer = QuadraticLoss()
accuracy = Accuracy()
for layer in inner_layers:
layer.lr = 1000.0 # 爲所有中間層設置學習速率
epochs = 20
for i in range(epochs):
print('epochs:', i)
losssum = 0
iters = 0
while True:
data, pos = datalayer1.forward() # 從數據層取出數據
x, label = data
for layer in inner_layers: # 前向計算
x = layer.forward(x)
loss = losslayer.forward(x, label) # 調用損失層forward函數計算損失函數值
losssum += loss
iters += 1
d = losslayer.backward() # 調用損失層backward函數層計算將要反向傳播的梯度
for layer in inner_layers[::-1]: # 反向傳播
d = layer.backward(d)
if pos == 0: # 一個epoch完成後進行準確率測試
data, _ = datalayer2.forward()
x, label = data
for layer in inner_layers:
x = layer.forward(x)
accu = accuracy.forward(x, label) # 調用準確率層forward()函數求出準確率
print('loss:', losssum / iters)
print('accuracy:', accu)
break
if __name__ == '__main__':
#chuli('train.txt', 'train.npy')
#chuli('test.txt', 'test.npy')
#chuli('validate.txt', 'validate.npy')
main()
數據集下載:
http://labfile.oss.aliyuncs.com/courses/814/data.tar.gz
說明上面代碼的大體處理步驟:
1:下載數據集,加載到項目裏,然後規範好訓練集
、驗證集
和測試集所用圖片都是哪些,從規範好的列表中讀取圖片,轉化爲只有0,1值的矩陣,
存儲到一個img.size*1的列向量,將轉化結果輸出到文件中。
2:編寫數據層處理函數,生成一份用於訓練,一份用於驗證的數據。
3:將數據傳入全連接層,以及激活函數層進行前向計算。
4:將前向計算返回的值代入損失函數層forward函數,計算損失函數值,
用損失層backward函數層計算將要反向傳播的梯度。
5:用訓練好的模型帶入驗證數據前向計算,完成後數據加入準確率函數中進行計算。
6:查看損失值以及正確率的變化情況。
可以將平方損失函數層替換成交叉熵損失函數層加上隱層等優化。