一般的前饋神經網絡中, 輸出的結果只與當前輸入有關與歷史狀態無關, 而遞歸神經網絡(Recurrent Neural Network, RNN)神經元的歷史輸出參與下一次預測.
本文中我們將嘗試使用RNN處理二進制加法問題: 兩個加數作為兩個序列輸入, 從右向左處理加數序列.和的某一位不僅與加數的當前位有關, 還與上一位的進位有關.
詞語的含義與上下文有關, 未來的狀態不僅與當前相關還與歷史狀態相關. 因為這種性質, RNN非常適合自然語言處理和時間序列分析等任務.
RNN與前饋神經網絡最大的不同在於多了一條反饋回路, 將RNN展開即可得到前饋神經網絡.
RNN同樣采用BP算法進行訓練, 誤差反向傳播時需要逆向通過反饋回路.
定義輸出層誤差為:
其中, \(O_j\)是預測輸出, \(T_j\)是參考輸出.
因為隱含層沒有參考輸出, 采用下一層的誤差加權和代替\(T_j - O_j\). 對於隱含層神經元而言這里的下一層可能是輸出層, 也可能是其自身.
更多關於BP算法的內容可以參考BP神經網絡與Python實現
定義RNN結構
完整的代碼可以在rnn.py找到.
因為篇幅原因, 相關工具函數請在完整源碼中查看, 文中不再贅述.
這里我們定義一個簡單的3層遞歸神經網絡, 隱含層神經元的輸出只與當前狀態以及上一個狀態有關.
定義RNN
類:
class RNN:
def __init__(self):
self.input_n = 0
self.hidden_n = 0
self.output_n = 0
self.input_weights = [] # (input, hidden)
self.output_weights = [] # (hidden, output)
self.hidden_weights = [] # (hidden, hidden)
def setup(self, ni, nh, no):
self.input_n = ni
self.hidden_n = nh
self.output_n = no
self.input_weights = make_rand_mat(self.input_n, self.hidden_n)
self.output_weights = make_rand_mat(self.hidden_n, self.output_n)
self.hidden_weights = make_rand_mat(self.hidden_n, self.hidden_n)
這里定義了幾個比較重要的矩陣:
-
input_weights
: 輸入層和隱含層之間的連接權值矩陣. -
output_weights
: 隱含層和輸出層之間的連接權值矩陣 -
hidden_weights
: 隱含層反饋回路權值矩陣, 反饋回路從一個隱含層神經元出發到另一個隱含層神經元.
因為本文的RNN只有一階反饋, 因此只需要一個反饋回路權值矩陣.對於n階RNN來說需要n個反饋權值矩陣.
定義test()
方法作為示例代碼的入口:
def test(self):
self.setup(2, 16, 1)
for i in range(20000):
a_int = int(rand(0, 127))
a = int_to_bin(a_int, dim=8)
a = np.array([int(t) for t in a])
b_int = int(rand(0, 127))
b = int_to_bin(b_int, dim=8)
b = np.array([int(t) for t in b])
c_int = a_int + b_int
c = int_to_bin(c_int, dim=8)
c = np.array([int(t) for t in c])
guess, error = self.do_train([a, b], c, dim=8)
if i % 1000 == 0:
print("Predict:" + str(guess))
print("True:" + str(c))
print("Error:" + str(error))
out = 0
for index, x in enumerate(reversed(guess)):
out += x * pow(2, index)
print str(a_int) + " + " + str(b_int) + " = " + str(out)
result = str(self.predict([a, b], dim=8))
print(result)
print "==============="
do_train
方法僅進行一次訓練, 這里我們生成了20000組訓練數據每組數據僅執行一次訓練.
predict方法
predict
方法執行一次前饋過程, 以給出預測輸出序列.
def predict(self, case, dim=0):
guess = np.zeros(dim)
hidden_layer_history = [np.zeros(self.hidden_n)]
for i in range(dim):
x = np.array([[c[dim - i - 1] for c in case]])
hidden_layer = sigmoid(np.dot(x, self.input_weights) + np.dot(hidden_layer_history[-1], self.hidden_weights))
output_layer = sigmoid(np.dot(hidden_layer, self.output_weights))
guess[dim - i - 1] = np.round(output_layer[0][0]) # if you don't like int, change it
hidden_layer_history.append(copy.deepcopy(hidden_layer))
初始化guess
向量作為預測輸出, hidden_layer_history
列表保存隱含層的歷史值用於計算反饋的影響.
自右向左遍歷序列, 對每個元素進行一次前饋.
hidden_layer = sigmoid(np.dot(x, self.input_weights) + np.dot(hidden_layer_history[-1], self.hidden_weights))
上面這行代碼是前饋的核心, 隱含層的輸入由兩部分組成:
-
來自輸入層的輸入
np.dot(x, self.input_weights)
. -
來自上一個狀態的反饋
np.dot(hidden_layer_history[-1], self.hidden_weights)
.
output_layer = sigmoid(np.dot(hidden_layer, self.output_weights))
guess[dim - position - 1] = np.round(output_layer[0][0])
上面這行代碼執行輸出層的計算, 因為二進制加法的原因這里對輸出結果進行了取整.
train方法
定義train
方法來控制迭代過程:
def train(self, cases, labels, dim=0, learn=0.1, limit=1000):
for i in range(limit):
for j in range(len(cases)):
case = cases[j]
label = labels[j]
self.do_train(case, label, dim=dim, learn=learn)
do_train
方法實現了具體的訓練邏輯:
def do_train(self, case, label, dim=0, learn=0.1):
input_updates = np.zeros_like(self.input_weights)
output_updates = np.zeros_like(self.output_weights)
hidden_updates = np.zeros_like(self.hidden_weights)
guess = np.zeros_like(label)
error = 0
output_deltas = []
hidden_layer_history = [np.zeros(self.hidden_n)]
for i in range(dim):
x = np.array([[c[dim - i - 1] for c in case]])
y = np.array([[label[dim - i - 1]]]).T
hidden_layer = sigmoid(np.dot(x, self.input_weights) + np.dot(hidden_layer_history[-1], self.hidden_weights))
output_layer = sigmoid(np.dot(hidden_layer, self.output_weights))
output_error = y - output_layer
output_deltas.append(output_error * sigmoid_derivative(output_layer))
error += np.abs(output_error[0])
guess[dim - i - 1] = np.round(output_layer[0][0])
hidden_layer_history.append(copy.deepcopy(hidden_layer))
future_hidden_layer_delta = np.zeros(self.hidden_n)
for i in range(dim):
x = np.array([[c[i] for c in case]])
hidden_layer = hidden_layer_history[-i - 1]
prev_hidden_layer = hidden_layer_history[-i - 2]
output_delta = output_deltas[-i - 1]
hidden_delta = (future_hidden_layer_delta.dot(self.hidden_weights.T) +
output_delta.dot(self.output_weights.T)) * sigmoid_derivative(hidden_layer)
output_updates += np.atleast_2d(hidden_layer).T.dot(output_delta)
hidden_updates += np.atleast_2d(prev_hidden_layer).T.dot(hidden_delta)
input_updates += x.T.dot(hidden_delta)
future_hidden_layer_delta = hidden_delta
self.input_weights += input_updates * learn
self.output_weights += output_updates * learn
self.hidden_weights += hidden_updates * learn
return guess, error
訓練邏輯中兩次遍歷序列, 第一次遍歷執行前饋過程並計算輸出層誤差.
第二次遍歷計算隱含層誤差, 下列代碼是計算隱含層誤差的核心:
hidden_delta = (future_hidden_layer_delta.dot(self.hidden_weights.T) +
output_delta.dot(self.output_weights.T)) * sigmoid_derivative(hidden_layer)
因為隱含層在前饋過程中參與了兩次, 所以會有兩層神經元反向傳播誤差:
- 輸出層傳遞的誤差加權和
output_delta.dot(self.output_weights.T)
- 反饋回路中下一層隱含神經元傳遞的誤差加權和
future_hidden_layer_delta.dot(self.hidden_weights.T)
將兩部分誤差求和然后乘自身輸出的sigmoid導數sigmoid_derivative(hidden_layer)
即為隱含層誤差, 這里與普通前饋網絡中的BP算法是一致的.
測試結果
執行test()
方法可以看到測試結果:
Predict:[1 0 0 0 1 0 1 0]
True:[1 0 0 0 1 0 1 0]
123 + 15 = 138
===============
Error:[ 0.22207356]
Predict:[1 0 0 0 1 1 1 1]
True:[1 0 0 0 1 1 1 1]
72 + 71 = 143
===============
Error:[ 0.3532948]
Predict:[1 1 0 1 0 1 0 0]
True:[1 1 0 1 0 1 0 0]
118 + 94 = 212
===============
Error:[ 0.35634191]
Predict:[0 1 0 0 0 0 0 0]
True:[0 1 0 0 0 0 0 0]
41 + 23 = 64
預測精度還是很令人滿意的.