Bill注:解釋一下,這文章是用) ( _ . = ; " , ' * / { } : - 0 + 1 [ ] 這20個特殊符號出現頻率+人工神經元的判斷來識別
、Java 或者 Python
根據一個簡化的統計,人腦由百億條神經組成 ―
每條神經平均連結到其它幾千條神經。通過這種連結方式,神經可以收發不同數量的能量。神經的一個非常重要的功能是它們對能量的接受并不是立即作出響應,而
是將它們累加起來,當這個累加的總和達到某個臨界閾值時,它們將它們自己的那部分能量發送給其它的神經。大腦通過調節這些連結的數目和強度進行學習。盡管
這是個生物行為的簡化描述。但同樣可以充分有力地被看作是神經網絡的模型。
閾值邏輯單元(Threshold Logic
Unit,TLU)
理解神經網絡的第一步是從對抽象生物神經開始,并把重點放在
閾值邏輯單元(TLU)這一特征上。一個
TLU
是一個對象,它可以輸入一組加權系數的量,對它們進行求和,如果這個和達到或者超過了某個閾值,輸出一個量。
讓我們用符號標注這些功能,首先,有輸入值以及它們的權系數:X
1,
X
2, ..., X
n和 W
1, W
2,
..., W
n。接著是求和計算出的 X
i*W
i
,產生了激發層 a,換一種方法表示:
a = (X1 * W1)+(X2 * W2)+...+(Xi * Wi)+...+ (Xn * Wn)
閾值稱為 theta。最后,輸出結果 y。當 a >=theta 時 y=1,反之
y=0。請注意輸出可以是連續的,因為它也可以由一個 squash 函數 s(或
sigma)判定,該函數的自變量是 a,函數值在 0 和 1 之間,y=s(a)。
圖 1. 閾值邏輯單元,帶有 sigma 函數(頂部)和 cutoff
函數(底部)
TLU 會分類,假設一個 TLU 有兩個輸入值,它們的權系數等于 1,theta
值等于 1.5。當這個 TLU 輸入 <0,0>、<0,1>、<1,0>
和 <1,1> 時,它的輸出分別為 0、0、0、1。TLU
將這些輸入分為兩組:0 組和 1 組。就像懂得邏輯連接(布爾運算
AND)的人腦可以類似地將邏輯連接的句子分類那樣,TLU
也懂得一點邏輯連接之類的東西。
TLU
能夠用幾何學上的解釋來闡明這種現象。它的四種可能輸入對應于笛卡爾圖的四個點。從等式
X
1*W
1+ X
2*W
2 =
theta,換句話說,也即 TLU
轉換其分類行為的點開始,它的點都分布在曲線 X
2 =
-X
1 + 1.5 上。這個方程的曲線將 4
個可能的輸入分成了兩個對應于 TLU 分類的區域。這是 TLU
原理中更為普通的實例。在 TLU 有任意數目的 N
個輸入的情況下,一組可能的輸入對應于 N
維空間中的一個點集。如果這些點可以被超平面 ―
換句話說,對應于上面示例中的線的 N
維的幾何外形切割,那么就有一組權系數和一個閾值來定義其分類剛好與這個切割相匹配的
TLU。
N維空間就是N個輸入節點
TLU
的學習原理
既然 TLU
懂得分類,它們就知道素材。神經網絡也可假定為可以學習。它們的學習機制是模仿大腦調節神經連結的原理。TLU
通過改變它的權系數和閾值來學習。實際上,從數學的觀點看,權系數閾值的特征有點武斷。讓我們回想一下當
SUM(Xi * Wi) >= theta 時 TLU 在臨界點時輸出的是 1 而不是
0,這相當于說臨界點是出現在 SUM(X
i* W
i)+ (-1
* theta) >= 0 的時候。所以,我們可以把 -1
看成一個常量輸入,它的權系數 theta
在學習(或者用技術術語,稱為
培訓)的過程中進行調整。這樣,當
SUM(X
i* W
i)+ (-1 * theta) >= 0
時,y=1,反之 y=0。
在培訓過程中,神經網絡輸入:
- 一系列需要分類的術語示例
- 它們的正確分類或者目標
這樣的輸入可以看成一個向量:<X
1, X
2,
..., X
n, theta, t>,這里 t
是一個目標或者正確分類。神經網絡用這些來調整權系數,其目的使培訓中的目標與其分類相匹配。更確切地說,這是有指導的培訓,與之相反的是無指導的培訓。前者是基于帶目標的示例,而后者卻只是建立在統計分析的基礎上。權系數的調整有一個學習規則,一個理想化的學習算法如下所示:
清單 1.
理想化的學習算法
fully_trained = FALSE DO UNTIL (fully_trained): fully_trained = TRUE FOR EACH training_vector = <X1, X2, ..., Xn, theta, target>:: # Weights compared to theta a = (X1 * W1)+(X2 * W2)+...+(Xn * Wn) - theta y = sigma(a) IF y != target: fully_trained = FALSE FOR EACH Wi: MODIFY_WEIGHT(Wi) # According to the training rule IF (fully_trained): BREAK
|
您或許想知道,“哪些培訓規則?”有很多,不過有一條似乎合理的規則是基于這樣一種思想,即權系數和閾值的調整應該由分式
(t - y) 確定。這個規則通過引入 alpha (0 < alpha < 1)
完成。我們把 alpha 稱為
學習率。W
i 中的更改值等于
(alpha * (t - y)* Xi)。當 alpha 趨向于 0
時,神經網絡的權系數的調整變得保守一點;當 alpha 趨向于 1
時,權系數的調整變得激進。一個使用這個規則的神經網絡稱為
感知器,并且這個規則被稱為
感知器學習規則。Rosenblatt 于 1962 年下的結論是,如果 N
維空間的點集被超平面切割,那么感知器的培訓算法的應用將會最終導致權系數的分配,從而定義了一個
TLU,它的超平面會進行需要的分割。當然,為了記起
Keynes,最終我們都切斷了與外界的聯系,專心思考。但是在計算時間之外,我們仍瀕臨危險,因為我們需要自己的神經網絡對可能輸入的空間進行不止一次的切割。
文章開始的難題舉例說明了這個,假設給您 N
個字符的代碼段,您知道是 C、C++、Java 或者
Python。難的是構造一個程序來標識編寫這段代碼的語言。用 TLU
來實現需要對可能的輸入空間進行不止一次的分割。它需要把空間分成四個區域。每種語言一個區域。把神經網絡培訓成能實現兩個切割就可完成這種工作。第一個切割將
C/C++ 和 Java/Python 分開來,另一個將 C/Java 和 C++/Python
分開。一個能夠完成這些切割的網絡同樣可以識別源代碼樣本中的語言。但是這需要網絡有不同結構,在描述這個不同之處之前,先來簡單地看一下實踐方面的考慮。
圖 2. 初步的(不完整的)感知器學習模型
考慮到排除取得 N 個字符代碼所需的計算時間,統計從 ASCII 碼的 32
到 127 的范圍內可視 ASCII
碼字符出現的頻率,并在這個統計以及關于代碼語言的目標信息的基礎上培訓神經網絡。我們的方法是將字符統計限制到
C、C++、Java 和 Python 代碼字符庫中最常用的 20
個非字母數字字符。由于關注浮點運算的執行,我們打算用一種規格化因素將這
20
字符統計分開來,并以此培訓我們的網絡。顯然,一個結構上的不同是我們的網絡有
20
個輸入節點,但這是很正常的,因為我們的描述已經暗示了這種可能性。一個更有意思的區別是出現了一對中間節點,N1
和 N2,以及輸出節點數量從兩個變成了四個(O1 到 O4)。
我們將培訓 N1,這樣當它一看到 C 或 C++,設置 y1=1,看到 Java 或
Python,它將設置 y1=0。同理培訓 N2,當它一看到 C 或 Java,設置
y2=1,看到 C++ 或 Python,設置 y2=0。此外,N1 和 N2 將輸出 1 或 0
給 Oi。現在如果 N1 看到 C 或 C++,而且 N2 看到 C 或者
Java,那么難題中的代碼是 C。而如果 N1 看到 C 或 C++,N2 看到 C++ 或
Python,那么代碼就是 C++。這個模式很顯而易見。所以假設 Oi
已被培訓并根據下面表格的情況輸出 1 或 0。
映射到輸出(作為布爾函數)的中間節點
N1
|
N2
|
O1 (C)
|
O2 (C++)
|
O3 (Java)
|
O4 (Python)
|
0
|
0
|
0
|
0
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
0
|
1
|
0
|
0
|
1
|
0
|
0
|
1
|
1
|
1
|
0
|
0
|
0
|
如果這樣可行的話,我們的網絡就可以從代碼示例中識別出語言了。這個想法很好。但是在實踐上卻有些難以置信。不過這種解決方案預示了
C/C++ 和 Java/Python 輸入被一個超平面切割了,同樣 C/Java 和
C++/Python
輸入被另一個切割。這是一個網絡培訓的解決方案,迂回地解決了這個輸入空間的設想。
關于 delta
規則
另一種培訓的規則叫做 delta 規則。感知器培訓規則是基于這樣一種思路
― 權系數的調整是由目標和輸出的差分方程表達式決定。而
delta
規則是基于梯度降落這樣一種思路。這個復雜的數學概念可以舉個簡單的例子來表示。從給定的幾點來看,向南的那條路徑比向東那條更陡些。向東就像從懸崖上掉
下來,但是向南就是沿著一個略微傾斜的斜坡下來,向西像登一座陡峭的山,而北邊則到了平地,只要慢慢的閑逛就可以了。所以您要尋找的是到達平地的所有路徑
中將陡峭的總和減少到最小的路徑。在權系數的調整中,神經網絡將會找到一種將誤差減少到最小的權系數的分配方式。
將我們的網絡限制為沒有隱藏節點,但是可能會有不止一個的輸出節點,設
p 是一組培訓中的一個元素,t(p,n) 是相應的輸出節點 n
的目標。但是,設 y(p,n) 由以上提到的 squash 函數 s 決定,這里
a(p,n) 是與 p 相關的 n 的激活函數,或者用 (p,n) = s( a(p,n) )
表示為與 p 相關的節點 n 的 squash
過的激活函數。為網絡設定權系數(每個 Wi),也為每個 p 和 n 建立
t(p,n) 與 y(p,n) 的差分,這就意味著為每個 p
設定了網絡全部的誤差。因此對于每組權系數來說有一個平均誤差。但是
delta
規則取決于求平均值方法的精確度以及誤差。我們先不討論細節問題,只是說一些與某些
p 和 n 相關的誤差:?* square( t(p,n) - y(p,n) )。現在,對于每個
Wi,平均誤差定義如下:
清單 2.
查找平均誤差
sum = 0 FOR p = 1 TO M: # M is number of training vectors FOR n = 1 TO N: # N is number of output nodes sum = sum + (1/2 * (t(p,n)-y(p,n))^2) average = 1/M * sum
|
delta
規則就是依據這個誤差的定義來定義的。因為誤差是依據那些培訓向量來說明的,delta
規則是一種獲取一個特殊的權系數集以及一個特殊的向量的算法。而改變權系數將會使神經網絡的誤差最小化。我們不需要討論支持這個算法的微積分學,只要認為任何
Wi 發生的變化都是如下所示就夠了:
alpha * s'(a(p,n)) * (t(p,n) - y(p,n)) * X(p,i,n).
|
X(p,i,n) 是輸入到節點 n 的 p 中的第 i 個元素,alpha
是已知的學習率。最后 s'( a(p,n) ) 是與 p 相關的第 n 個節點激活的
squashing 函數的變化(派生)率,這就是 delta 規則,并且 Widrow 和
Stearns 向我們展示了當
alpha
非常小的時候,權系數向量接近某個將誤差最小化的向量。用于權系數調節的基于
delta 規則的算法就是如此。
梯度降落(直到誤差小到適當的程度為止)
step 1: for each training vector, p, find a(p) step 2: for each i, change Wi by: alpha * s'(a(p,n)) * (t(p,n)-y(p,n)) * X(p,i,n)
|
這里有一些與感知器算法相區別的重要不同點。顯然,在權系數調整的公式下有著完全不同的分析。delta
規則算法總是在權系數上調整,而且這是建立在相對輸出的激活方式上。最后,這不一定適用于存在隱藏節點的網絡。
反向傳播這一算法把支持 delta
規則的分析擴展到了帶有隱藏節點的神經網絡。為了理解這個問題,設想
Bob 給 Alice 講了一個故事,然后 Alice 又講給了 Ted,Ted
檢查了這個事實真相,發現這個故事是錯誤的。現在 Ted
需要找出哪些錯誤是 Bob 造成的而哪些又歸咎于
Alice。當輸出節點從隱藏節點獲得輸入,網絡發現出現了誤差,權系數的調整需要一個算法來找出整個誤差是由多少不同的節點造成的,網絡需要問,“是誰讓我誤入歧途?到怎樣的程度?如何彌補?”這時,網絡該怎么做呢?
圖 3:“代碼識別”反向傳播的神經網絡
反向傳播算法同樣來源于梯度降落原理,在權系數調整分析中的唯一不同是涉及到
t(p,n) 與 y(p,n) 的差分。通常來說 W
i的改變在于:
alpha * s'(a(p,n)) * d(n) * X(p,i,n)
|
其中 d(n) 是隱藏節點 n 的函數,讓我們來看(1)n
對任何給出的輸出節點有多大影響;(2)輸出節點本身對網絡整體的誤差有多少影響。一方面,n
影響一個輸出節點越多,n
造成網絡整體的誤差也越多。另一方面,如果輸出節點影響網絡整體的誤差越少,n
對輸出節點的影響也相應減少。這里 d(j)
是對網絡的整體誤差的基值,W(n,j) 是 n 對 j 造成的影響,d(j) *
W(n,j) 是這兩種影響的總和。但是 n
幾乎總是影響多個輸出節點,也許會影響每一個輸出結點,這樣,d(n)
可以表示為:
這里 j 是一個從 n
獲得輸入的輸出節點,聯系起來,我們就得到了一個培訓規則,第 1
部分:在隱藏節點 n 和輸出節點 j 之間權系數改變,如下所示:
alpha * s'(a(p,n))*(t(p,n) - y(p,n)) * X(p,n,j)
|
第 2 部分:在輸入節點 i 和輸出節點 n
之間權系數改變,如下所示:
alpha * s'(a(p,n)) * sum(d(j) * W(n,j)) * X(p,i,n)
|
這里每個從 n 接收輸入的輸出節點 j
都不同。關于反向傳播算法的基本情況大致如此。
將 Wi 初始化為小的隨機值。
使誤差小到適當的程度要遵循的步驟
第 1
步:輸入培訓向量。
第 2 步:隱藏節點計算它們的輸出
第 3 步:輸出節點在第 2 步的基礎上計算它們的輸出。
第 4 步:計算第 3 步所得的結果和期望值之間的差。
第 5 步:把第 4 步的結果填入培訓規則的第 1 部分。
第 6 步:對于每個隱藏節點 n,計算 d(n)。
第 7 步:把第 6 步的結果填入培訓規則的第 2 部分。
|
通常把第 1 步到第 3 步稱為
正向傳播,把第 4 步到第 7
步稱為
反向傳播。反向傳播的名字由此而來。
在掌握了反向傳播算法后,可以來看我們的識別源代碼樣本語言的難題。為了解決這個問題,我們提供了
Neil Schemenauer 的 Python 模型
bpnn。用它的模型解決問題真是難以置信的簡單,在我們的類
NN2
里定制了一個類
NN
,不過我們的改變只是調整了表達方式和整個過程的輸出,并沒有涉及到算法。基本的代碼如下所示:
清單 3:用 bpnn.py
建立一個神經網絡
# Create the network (number of input, hidden, and training nodes) net = NN2(INPUTS, HIDDEN, OUTPUTS) # create the training and testing data trainpat = [] testpat = [] for n in xrange(TRAINSIZE+TESTSIZE): #... add vectors to each set # train it with some patterns net.train(trainpat, iterations=ITERATIONS, N=LEARNRATE, M=MOMENTUM) # test it net.test(testpat) # report trained weights net.weights()
|
當然我們需要輸入數據,實用程序 code2data.py
提供了這個功能。它的界面很直觀:只要將一堆擴展名各不相同的文件放到一個子目錄
./code
中,然后運行這個實用程序,并列舉那些擴展名作為命令選項。例如:
python code2data.py py c java
|
您得到的是一堆 STDOUT
上的向量,可以把這些向量輸入到另一個進程或者重定向到一個文件,它的輸出如下所示:
清單 4:Code2Data
的輸出向量
0.15 0.01 0.01 0.04 0.07 0.00 0.00 0.03 0.01 0.00 0.00 0.00 0.05 0.00 > 1 0 0 0.14 0.00 0.00 0.05 0.13 0.00 0.00 0.00 0.02 0.00 0.00 0.00 0.13 0.00 > 1 0 0 [...]
|
讓我們回憶一下輸入值都是不同特殊字符出現的規格化數目,目標值(在大于號以后)是
YES/NO,它代表包含這些字符的源代碼文件的類型,不過對于什么是什么來說,并沒有非常明顯的東西。數字可以是輸入或期望的
任意值,這才是最重要的。
下一步是運行實際的 code_recognizer.py 程序。這需要(在
STDIN
中)像上面一樣的向量集。這個程序有一個包,它能夠根據實際文件推斷出需要多少輸入節點(計算在內的和期望的),選擇隱藏節點的數目是一個訣竅。對于源代碼的識別,6
到 8
個隱藏節點似乎工作得很好。如果打算試驗網絡從而發現對于這些不同的選項它是如何做的,您可以覆蓋命令行中的所有參數,但每一次運行還是會耗費一些時間。值得注意的是,
code_recognizer.py 將它的(大的)測試結果文件發送到
STDOUT,而將一些友好的消息放在 STDERR
里。這樣在大部分時間里,為了安全保管,您將會把 STDOUT
定向到一個文件,并監視針對進程和結果概要的 STDERR。
清單 5:運行
code_recognizer.py
> code2data.py py c java | code_recognizer.py > test_results.txt Total bytes of py-source: 457729 Total bytes of c-source: 245197 Total bytes of java-source: 709858 Input set: ) ( _ . = ; " , ' * / { } : - 0 + 1 [ ] HIDDEN = 8 LEARNRATE = 0.5 ITERATIONS = 1000 TRAINSIZE = 500 OUTPUTS = 3 MOMENTUM = 0.1 ERROR_CUTOFF = 0.01 TESTSIZE = 500 INPUTS = 20 error -> 95.519... 23.696... 19.727... 14.012... 11.058... 9.652... 8.858... 8.236... 7.637... 7.065... 6.398... 5.413... 4.508... 3.860... 3.523... 3.258... 3.026... 2.818... 2.631... 2.463... 2.313... 2.180... 2.065... 1.965... 1.877... 1.798... 1.725... [...] 0.113... 0.110... 0.108... 0.106... 0.104... 0.102... 0.100... 0.098... 0.096... 0.094... 0.093... 0.091... 0.089... 0.088... 0.086... 0.085... 0.084... Success rate against test data: 92.60%
|
不斷減少誤差是個很好的兆頭,這至少在一段長時間里所獲得的一種進步,且最后的結果必然是深入人心的。就我們的觀點而言,網絡完成了一項值得尊敬的工作,來識別代碼
― 我們將會樂意傾聽,對于您的數字向量它是如何做的。