https://blog.csdn.net/yxhlfx/article/details/79093456
前言
隨着17年阿爾法狗(AlphaGo)擊敗人類職業圍棋選手、
戰勝圍棋世界冠軍,AI、人工智能等詞匯也成為了時下人們追求的一個潮流,各種相關產業和人工智能為主題的創業公司也如雨后春筍般相繼涌現,因此人工智能也成為了2017年的關鍵詞。關於人工智能的概念從計算機誕生之初就已經有了,1936年艾倫·圖靈(Alan Turing)提出了著名的 “圖靈機”(Turing Machine)的設想,在十多年后,其為了明確機器是否具備智能更是提出了著名的圖靈測試。因此在過去的半個多世紀,人工智能其實不算什么新鮮的話題,甚至許多好萊塢大片都不吝以此概念作為噱頭,比如黑客帝國、人工智能、機械姬等(PS:絕沒有推薦電影的意思)。但是處在21世紀的我們聽到這些概念時仍然會不自覺的感到興奮,因為這是第一次科技的力量讓我們感受到科幻與現實離我們是如此的接近,也正是在這個信息高度聚合與高速傳播的時代,使我們大部分人都能夠參與到其中,去實現每個人心中的科幻夢。
注意事項
技能要求
接下來的知識不會涉及到高深的數學知識,對編程的要求也是極低,並且所有涉及到的知識點都會用通俗的語言進行解釋,所以不要擔心數學不夠好,不會編程等問題。當然,便於理解本文,如果你數學基礎足夠好那是那是有好處的。同時關鍵部分將會給出某些重要信息的解釋或是數學名詞的解釋鏈接,在閱讀本文過程中適當的去理解其中的某些概念是有必要的,必要的時候還請停下閱讀進度確定自己已經理解。最后希望在看完這篇文章后能夠讓你產生對數學和機器學習的濃厚興趣。所以Come on!請花15分鍾認真看完!請花15分鍾認真看完!請花15分鍾認真看完!
內容概述
接下來我們將從機器學習中最基礎的模型感知機(Perceptron)出發去探索機器學習的奧秘,在這兒我們可以不用理解它是什么意思,也不用去查各種資料使自己迷失在知識的海洋里,花15分鍾時間仔細讀完本文,你將了解人工智能現在真正的樣子,同時你也將正式開始入門人工智能這個領域。人類在統計與概率的基礎上建立起了人工智能這么一套體系,在今天這么個日新月異的世界,也是越加的蓬勃發展。雖然人工智能的內容涵蓋很廣,但是其主線脈絡卻是明確的,從發現問題,分析問題再到解決問題,在發展出的那一套基礎框架下,人工智能在眾多的領域遍地開花,產生了許許多多應用於不同領域的新奇想法。因此我們在追逐學科前沿,面多眾多令人眼花繚亂的模型或算法時,夯實自己的基礎,提升自己發現問題並解決問題的能力顯得更加總要。
機器學習的核心
機器學習已經成為一門完整的學科,在學科建設的基礎上,已經出現了針對機器學習領域標准的研究方法和技巧。下圖展示了機器學習的三要素,也是其核心內容。
Machine_Learning_3item
從分類問題開始
分類在我們日常生活中很常見,商品分類,垃圾分類,食物分類……這些分類的場景在我們的生活中都是司空見慣的。正因為司空見慣,所以就讓人感覺分類是理所當然的事,其過程也沒有任何難度。但這簡單的事兒對機器而言卻並不是那么簡單。如果要讓機器來完成各種分類它能不能完成呢?答案是能,只要我們為特定的機器設定特定的規則使它能循環運轉起來就行,而這兒那個規則就是分類的關鍵。比如將商場賣的橘子分成兩類,一類長得好看的為橘子君A,一類長得難看的為橘子君B,這兒有一個規則Rule,能夠判斷一個橘子是好看還是不好看,那么將Rule告訴這個機器豈不是完美?以后再也不用擔心吃不到好看的橘子了。
橘子
好吧跑偏了,通過上面舉的的例子其實就能夠看到人工智能研究中的一個重要步驟——選擇模型。橘子分類中的Rule就是需要選擇的模型,有了模型,有了Rule,機器才能知道它拿到一個橘子應該怎么做。那么現在有了模型還要干什么?如果那模型是我們從一個有限的樣本空間中通過嚴格的推導得到的,並且我們也只將該模型應用於該有限樣本空間,那么就沒有什么事兒需要做了,但實際上這在現實中是不可能的,因為我既然從那一堆橘子推導出了一個模型,那那堆橘子就肯定已經被我們全都分好類了,那這個模型拿來還有什么意義?雖然從上面的角度我們似乎做了無用功,但統計學的知識卻給了我們啟發。那就是,我們推導出來的模型除了應用於我們用於推導模型所用的那堆橘子外,很大概率還能應用於那些還沒有被分類的橘子。從概率論與統計學的觀點上看,這是正確的。因為我們以橘子的美丑對橘子進行分類,而美和丑是那些橘子所共有的一些特征。就好比人,每個人都長着五官,在一小群人中我們以一個標准區分其中的美丑,那么將這個標准應用於全體人當中也是可行的,但也可能出現我們的標准無法區分的情況發生。
好了,到這兒有沒有想到些什么?由概率論的知識我們能夠想到,從部分數據中尋找其中我們關注的共性特征對其進行識別分類,那么這些共性特征也能夠幫助我們識別分類那些未知的數據,這就是基於概率與統計學習的機器學習的核心原理與思想。看吧,其實很多感覺高深的知識就是我們身邊隨處可見的問題。有了這個思想有沒有對機器學習的概念有一個初步的了解呢?機器從我們給的數據學習一個能夠正確解決問題的方法,我們再用那個方法去應用到我們的實際問題中去,而那個方法就是上面多次提到的模型。
在二維平面中怎么分類?
讀到這兒的朋友應該對機器學習還有較深的困惑,但這不要緊,現在需要你發揮一定的想象能力。同時如果手邊有筆和紙也請拿起筆跟我一起畫一個二維坐標系,這個坐標系是一個二維空間,在這個空間中分布着無數點它們都是這個二維世界的原住民,現在你是這個世界的上帝,你隨意在這個空間中畫了一條看不見直線,里面的人被你分成了兩類A和B,分別位於直線兩側。下圖展示了在平面上的一條直線2x−y+3=0
將平面上的點分成兩類,
有一天,這個二維世界中穿越來了一個從三維世界來的人C,小C遇見了很多這個世界的人,他發現了這個世界上的人被分成了A,B兩類。並且這些被分成兩類的人類別存在較明顯的位置特征,於是小C想了一個辦法來描述這個世界上被分成兩類的人,即假設這個世界的人可以用一條直線來划分。小C想到的辦法如下:
f(x)=sign(x)={1−1x>0x<=0
y=f(w⋅x+b),x表示每個原住民在該空間中的坐標
通過以上計算方法,能夠將所有二維世界中的原住民分成兩類,也就是以每個原住民的坐標為參數,通過模型計算結果為1的為一類,為-1的為另一類,So easy有木有!到目前為止小C離成功猜到你是怎么給這個二維世界的原住民分類的又近了一步。小C還需要什么?它需要知道w
和b的值是什么。它們顯然是兩個模型必須的參數值,這兩個參數值影響着分類模型的結果,而現在要怎么來確定這兩個值呢?可能有人會很快想到,我們拿一組二維人的坐標數據和它的實際分類作輸入去算不就行了嗎?是的,我們需要以有限的輸入去算這兩個值,許多人到這兒就迷茫了,怎么算?對於怎么算的問題才是機器學習中的關鍵,回到上面的模型,我們要開始算就需要一個明確的w,b值,這與我們所要解決的問題相矛盾了,因此只能假設一個初始值。這個初始值是什么不重要,重要的是正因為有了一個初始值小C的這個模型成為了一個可以實際進行運算的模型了,而我們運算的目的也就變成了不斷進行迭代運算使得w,b
的值不斷向着接近這個世界分類真相的方向前進。自然的我們得到如下兩個基本策略(迭代過程該怎么做的方法):
模型運算結果與實際結果相符,不做任何額外操作,繼續輸入新的數據
模型運算結果與實際結果不符(誤分類),調整w,b
的值,使得其朝真相的方向逐步靠近
到這兒我想大家應該知道需要做什么了,那就是找到一個方法來調整w,b
的值。在討論該怎么調整w,b的值之前先來想想進行這些計算的目的,其目的顯然是為了得到一組w,b,使得模型運算與實際結果不符的數量最小。在機器學習中,稱這么一個關於w,b的函數叫做損失函數,將損失函數極小化(極小化即求極小值)的過程就是求w,b的過程,而損失函數的一個自然選擇是誤分類的總數(自然選擇就是最接近人類思維方向邏輯推斷),但這樣的話損失函數就不是w,b的連續可導函數。這兒為什么要求損失函數是關於w,b的連續可導函數,因為只有函數是連續可導的,我們才能方便的在該函數上確定極大值或極小值,對於這個問題可參考此處。好了,損失函數不適合表示為誤分類點的總數,那么能尋找其它表現形式。這兒有一個選擇就是將被誤分類的所有點距離模型表示直線或平面的總距離作為損失函數的意義。這也是我們能想到的最自然的表示了,比如當點被誤分類,誤分類點肯定出現在了當前模型的錯誤一側,我們的有效矯正方式就是調整w,b的值使得模型表示平面/直線往該點的方向平移一定距離,也就是縮小它們之間的距離,但是在最優化問題當中,我們對於單點來說可能使其達到最優了,即誤差點相對模型的距離為0了,但對於這個空間中的所有點來說,可能反而會隨着單個點這樣的調整而出現更多的誤差點,因此我們需要保證w,b
的調整總是朝着好的方向進行,也就是總體誤差點最少,換成距離的概念就變成了誤差點距離平面/直線的總長度最小,這樣就能保證我們訓練得到的模型最接近真實模型。由點到平面的距離公式我們可以得到任意一點距離我們上面定義的模型的距離為:
len(xi)=1‖w‖|w⋅xi+b|
在這里不加證明地給出這個公式,如果有興趣自己推導的同學可參考點到平面距離公式的七種推導方法探討,這里需要解釋的一點是因為我們的模型有兩個變量x,(x∈Rn)和y,(y∈−1,1),所以在這兒所謂的距離實際是指的我們的模型包含的某一維度的距離,更科學的描述稱為到超平面的距離,因此大家在計算距離公式時切記一點,針對f(x)=w⋅x+b這個形式來求距離時,可能有人會很困惑,總感覺上面的距離公式是錯的?其實從我們的場景來說,這個模型函數中的x實際上是一個向量,即它包含兩個維度的值。這樣我們就可以將能夠影響一個點位置信息的x維度上的值和y維度上的值一起進行評估來得到一個綜合的評估值。這一點從我們遇到的問題出發去看也是顯而易見的,因為設計的這個分類方法中,實際輸入就是一個坐標點,輸出是一個其它值。因此針對上面的公式也就不難理解了,其結果實際上是求到w1x1+w2x2+b=0平面的距離。在這兒必須要理解這點,這將使我們更清晰的看出目前以及之后的所有公式推導的理由及意義。如果還是不能理解也沒關系,只要知道為了明確知道模型的好壞,需要有一個方法來對其進行評估,在這兒我們只是選取了距離這個概念來描述模型的好壞,在其它更多場景中還有更多其它各種各樣的方法,而重要的是能夠在各種復雜場景中找到一個合適高效的方法。
有了計算距離的方式,下面我們來看看損失函數究竟怎么定義。由於對於模型來說,在分類錯誤的情況下,若w⋅xi+b>0
,則實際的yi應該是等於-1,而當w⋅xi+b<0時,yi
等於1,因此由這個特性我們可以去掉上面的絕對值符號,將公式轉化為:
len(xi)=−1‖w‖yi(w⋅xi+b)
如此得到最終的損失函數為:
L(w,b)=∑xi∈Mlen(xi)⇒L(w,b)=∑xi∈M−yi(w⋅xi+b)
正如上面所示,1‖w‖
這個因子在這兒可以不用考慮,因為它對結果的影響與w,b是等效的,因此只用單獨考慮w,b
就可以,這樣可以減小運算復雜度。到這一步問題就變得簡單了,那就是求L(w,b)的極小值。對於極大值極小值的求解方法有許多,這兒首先講述一種梯度下降的方法求極小值,根據梯度的定義,我們可以得到損失函數的梯度有:
∇wL(w,b)=−∑xi∈Myixi∇bL(w,b)=−∑xi∈Myi
根據梯度下降所描述的方法,我們只需要在每次出現誤分類時按如下方法更新w,b
的值即可,
w←w+ηyixib←b+ηyi
以上更新方法就是每次出現誤分類時w
或b分別減去它們各自在該誤分類點的梯度值,這兒更新w,b的方式稱為隨機梯度下降法,因此會發現更新w,b
時是不帶求和符號的,所謂隨機梯度下降就是每次取梯度值是隨機的取某點在該模型上的梯度值,這兒的隨機性取決與你的輸入。當然也可以通過計算求得一個總體平均的梯度,但那樣的話當輸入數據很多時訓練模型將變成一個很漫長的事兒,因此到底哪種好哪種不好我們需要根據實際情況去權衡取舍。
到這兒是不是一切變得豁然開朗?小C在你所創造的二維世界中已經找到了方法來得到你對其中的二維原住民分類的方式,只要小C在那個世界發現足夠多的原住民,每當找到一個原住民就用他的模型對其分類,只要分類結果與實際不符時,就用上面的方法更新模型,那么小C將得到一個無限接近你對二維世界原住民分類的模型。
讓模型運轉起來
上面我們已經確定了給二維世界原住民分類的方案,並且知道了怎么來使得隨着數據的輸入讓模型變得越來越接近真實情況。而上面描述的模型還有一個高大上的名字叫感知機模型。是不是格調瞬間就上來了?那么我們來看看怎么用編程語言實現這個計算過程。
Python實現
Python具有很方便的數值計算庫和簡單的語法,因此我們用Python實現感知機模型試試看.
from random import randint
import numpy as np
import matplotlib.pyplot as plt
class TrainDataLoader:
def __init__(self):
pass
def GenerateRandomData(self, count, gradient, offset):
x1 = np.linspace(1, 5, count)
x2 = gradient*x1 + np.random.randint(-10,10,*x1.shape)+offset
dataset = []
y = []
for i in range(*x1.shape):
dataset.append([x1[i], x2[i]])
real_value = gradient*x1[i]+offset
if real_value > x2[i]:
y.append(-1)
else:
y.append(1)
return x1,x2,np.mat(y),np.mat(dataset)
class SimplePerceptron:
def __init__(self, train_data = [], real_result = [], eta = 1):
self.w = np.zeros([1, len(train_data.T)], int)
self.b = 0
self.eta = eta
self.train_data = train_data
self.real_result = real_result
def nomalize(self, x):
if x > 0 :
return 1
else :
return -1
def model(self, x):
# Here are matrix dot multiply get one value
y = np.dot(x, self.w.T) + self.b
# Use sign to nomalize the result
predict_v = self.nomalize(y)
return predict_v, y
def update(self, x, y):
# w = w + n*y_i*x_i
self.w = self.w + self.eta*y*x
# b = b + n*y_i
self.b = self.b + self.eta*y
def loss(slef, fx, y):
return fx.astype(int)*y
def train(self, count):
update_count = 0
while count > 0:
# count--
count = count - 1
if len(self.train_data) <= 0:
print("exception exit")
break
# random select one train data
index = randint(0,len(self.train_data)-1)
x = self.train_data[index]
y = self.real_result.T[index]
# wx+b
predict_v, linear_y_v = self.model(x)
# y_i*(wx+b) > 0, the classify is correct, else it's error
if self.loss(y, linear_y_v) > 0:
continue
update_count = update_count + 1
self.update(x, y)
print("update count: ", update_count)
pass
def verify(self, verify_data, verify_result):
size = len(verify_data)
failed_count = 0
if size <= 0:
pass
for i in range(size):
x = verify_data[i]
y = verify_result.T[i]
if self.loss(y, self.model(x)[1]) > 0:
continue
failed_count = failed_count + 1
success_rate = (1.0 - (float(failed_count)/size))*100
print("Success Rate: ", success_rate, "%")
print("All input: ", size, " failed_count: ", failed_count)
def predict(self, predict_data):
size = len(predict_data)
result = []
if size <= 0:
pass
for i in range(size):
x = verify_data[i]
y = verify_result.T[i]
result.append(self.model(x)[0])
return result
if __name__ == "__main__":
# Init some parameters
gradient = 2
offset = 10
point_num = 1000
train_num = 50000
loader = TrainDataLoader()
x, y, result, train_data = loader.GenerateRandomData(point_num, gradient, offset)
x_t, y_t, test_real_result, test_data = loader.GenerateRandomData(100, gradient, offset)
# First training
perceptron = SimplePerceptron(train_data, result)
perceptron.train(train_num)
perceptron.verify(test_data, test_real_result)
print("T1: w:", perceptron.w," b:", perceptron.b)
# Draw the figure
# 1. draw the (x,y) points
plt.plot(x, y, "*", color='gray')
plt.plot(x_t, y_t, "+")
# 2. draw y=gradient*x+offset line
plt.plot(x,x.dot(gradient)+offset, color="red")
# 3. draw the line w_1*x_1 + w_2*x_2 + b = 0
plt.plot(x, -(x.dot(float(perceptron.w.T[0]))+float(perceptron.b))/float(perceptron.w.T[1])
, color='green')
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
如下是由以上代碼實現的模型分類結果圖,其中紅色直線為實際的分類模型,綠色直線為通過訓練數據訓練后得到的模型,灰色’*’符號組成的點集為訓練數據集,藍色的’+’號組成的點集為驗證數據集:
感知機就這樣?
看了以上的內容很多人可能感覺機器學習也不過如此!那么首先恭喜你,有這感覺證明對於機器學習你開始入門了,但是還有更多的東西在等着你。這兒有幾個疑問步驟你有沒有想過:
模型是怎么確定的,為什么就能想到用感知機這樣的模型呢?
損失函數就只能靠那種思路得到嗎?還有沒有更好的方式?
損失函數都是求最小值嗎,有沒有求最大值的情形,最小/最大值真的能求出來嗎?
求最小/最大值的方法還有什么?我們能不能換其它方法來替換隨機梯度下降/上升?
我相信,讀完整篇文章這些疑問應該是自然產生的,其中的這些問題希望大家自己能夠隨着學習的深入找到答案。
感知機的對偶形式變形
“對偶”一詞聽着挺奇怪的,但可以將其理解為形式不一樣但結果相等的意思。如下就是感知機模型的一種對偶形式:
f(x)=sign⟮∑j=1Nαjyjxj⋅x+b⟯
從上式可以看出與前面的模型相比僅替換了w
的值,這種變化是基於前面的模型推導得到的。當現在出現了i次誤分類,而造成i次誤分類的點分別為(x1,y1),(x2,y2),⋯,(xi,yi),則當前w
的值必定為
w=η(y1x1+y2x2+⋯+yixi)
由此我們假設總共有N個點被錯誤分類,ni
表示這N個點中的第i個點在訓練過程中被分類錯誤的總次數。因此上式可化簡為:
w=∑i=1Nniηyixi
那么自然的,我們令αi=niη,則有
w=∑i=1Nαiyixi
經過如上變換后,每次訓練迭代更新就需要更新α與b
值。而當η等於1時,α的物理意義為與前面討論的ni相同。因此,當η等於1時,每次更新失敗αi都應該加1,從另一個角度說,αi
就變成了訓練過程中的一個記錄器,用於記錄每個點分別被誤分類的次數,因此得到訓練過程的更新策略如下:
αi←αi+1b←b+ηyi
同時觀察模型可以發現xi
與xj
以內積的形式出現,通過這個特征可以想到Gram矩陣的定義為:
G=[xi⋅xj]N×N
因此我們可以提前計算出Gram矩陣用於后面直接通過查詢Gram矩陣知道xj⋅xi
的值。上面沒有講到損失函數,實際上對偶形式的損失函數和原始形式的損失函數是一樣的,而它們更新參數的策略都是圍繞則一個目的實現的,那就是求得損失函數的極小值的最優解。在最前面介紹的感知機的原始形式求損失函數最優解的策略應該很多人都是很容易理解的,就是非常直觀的梯度下降。而在對偶形式當中,α與損失函數進行梯度下降的次數是緊密聯系的,隨着α的不斷增加,損失函數執行梯度下降的次數不斷增加,模型也越接近真實情況。如果你會matlab,使用matlab去模擬這個更新過程,你將能夠更直觀的看到α
的增加是如何影響着其它值的。好了,知道了這些,我們就能夠將這些公式轉化為代碼去實現這個模型了,還有不懂的地方可以結合代碼在梳理一遍。
感知機對偶形式實現
# Init the parameter
from random import randint
import numpy as np
import matplotlib.pyplot as plt
class TrainDataLoader:
def __init__(self):
pass
def GenerateRandomData(self, count, gradient, offset):
x1 = np.linspace(1, 5, count)
x2 = gradient*x1 + np.random.randint(-10,10,*x1.shape)+offset
dataset = []
y = []
for i in range(*x1.shape):
dataset.append([x1[i], x2[i]])
real_value = gradient*x1[i]+offset
if real_value > x2[i]:
y.append(-1)
else:
y.append(1)
return x1,x2,np.mat(y),np.mat(dataset)
class SimplePerceptron:
def __init__(self, train_data = [], real_result = [], eta = 1):
self.alpha = np.zeros([train_data.shape[0], 1], int)
self.w = np.zeros([1, train_data.shape[1]], int)
self.b = 0
self.eta = eta
self.train_data = train_data
self.real_result = real_result
self.gram = np.matmul(train_data[0:train_data.shape[0]], train_data[0:train_data.shape[0]].T)
def nomalize(self, x):
if x > 0 :
return 1
else :
return -1
def train_model(self, index):
temp = 0
y = self.real_result.T
# Here are matrix dot multiply get one value
for i in range(len(self.alpha)):
alpha = self.alpha[i]
if alpha == 0:
continue
gram_value = self.gram[index].T[i]
temp = temp + alpha*y[i]*gram_value
y = temp + self.b
# Use sign to nomalize the result
predict_v = self.nomalize(y)
return predict_v, y
def verify_model(self, x):
# Here are matrix dot multiply get one value
y = np.dot(x, self.w.T) + self.b
# Use sign to nomalize the result
predict_v = self.nomalize(y)
return predict_v, y
def update(self, index, x, y):
# alpha = alpha + 1
self.alpha[index] = self.alpha[index] + 1
# b = b + n*y_i
self.b = self.b + self.eta*y
def loss(slef, fx, y):
return fx.astype(int)*y
def train(self, count):
update_count = 0
train_data_num = self.train_data.shape[0]
print("train_data:", self.train_data)
print("Gram:",self.gram)
while count > 0:
# count--
count = count - 1
if train_data_num <= 0:
print("exception exit")
break
# random select one train data
index = randint(0, train_data_num-1)
if index >= train_data_num:
print("exceptrion get the index")
break;
x = self.train_data[index]
y = self.real_result.T[index]
# w = \sum_{i=1}^{N}\alpha_iy_iGram[i]
# wx+b
predict_v, linear_y_v = self.train_model(index)
# y_i*(wx+b) > 0, the classify is correct, else it's error
if self.loss(y, linear_y_v) > 0:
continue
update_count = update_count + 1
self.update(index, x, y)
for i in range(len(self.alpha)):
x = self.train_data[i]
y = self.real_result.T[i]
self.w = self.w + float(self.alpha[i])*x*float(y)
print("update count: ", update_count)
pass
def verify(self, verify_data, verify_result):
size = len(verify_data)
failed_count = 0
if size <= 0:
pass
for i in range(size-1):
x = verify_data[i]
y = verify_result.T[i]
if self.loss(y, self.verify_model(x)[1]) > 0:
continue
failed_count = failed_count + 1
success_rate = (1.0 - (float(failed_count)/size))*100
print("Success Rate: ", success_rate, "%")
print("All input: ", size, " failed_count: ", failed_count)
def predict(self, predict_data):
size = len(predict_data)
result = []
if size <= 0:
pass
for i in range(size):
x = verify_data[i]
y = verify_result.T[i]
result.append(self.model(x)[0])
return result
if __name__ == "__main__":
# Init some parameters
gradient = 2
offset = 10
point_num = 1000
train_num = 1000
loader = TrainDataLoader()
x, y, result, train_data = loader.GenerateRandomData(point_num, gradient, offset)
x_t, y_t, test_real_result, test_data = loader.GenerateRandomData(100, gradient, offset)
# train_data = np.mat([[3,3],[4,3],[1,1]])
# First training
perceptron = SimplePerceptron(train_data, result)
perceptron.train(train_num)
perceptron.verify(test_data, test_real_result)
print("T1: w:", perceptron.w," b:", perceptron.b)
# Draw the figure
# 1. draw the (x,y) points
plt.plot(x, y, "*", color='gray')
plt.plot(x_t, y_t, "+")
# 2. draw y=gradient*x+offset line
plt.plot(x,x.dot(gradient)+offset, color="red")
# 3. draw the line w_1*x_1 + w_2*x_2 + b = 0
plt.plot(x, -(x.dot(float(perceptron.w.T[0]))+float(perceptron.b))/float(perceptron.w.T[1])
, color='green')
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
下圖為以1000組數據訓練,100組數據做驗證的結果圖,綠色直線為訓練得到的模型。
感知機的限制與推廣
感知機是什么?就是如上面所講述的那種模型定義,而感知機有一個非常明顯的特征——它是線性的。這兒先來各出一個結論:線性模型不可分類異或問題。到這兒很多人可能會糊塗了,異或問題是什么鬼?不能把話講明白嗎?這兒給出一個直觀的例子,還是以前面描述的二維世界為例。我們知道二維世界的每個人都具有一個標簽,就像身份證一樣,那就是它們的坐標。假設你給它們分類的時候不是直接在里面畫一條直線,直線一側的是一類,另一側的是另一類;而是以它們的(x,y)
坐標值來分類,x,y
值相同的為一類,不同的為另一類,那么小C還能用上面的方法分類嗎?顯然是不可能的,而這個問題就屬於異或問題,異或問題就屬於線性不可分問題。就像下圖,下圖中相同符號表示的點屬於一類,當你會發現不管你怎么畫線去分,甚至畫再多條直線也不可能如下圖所示的兩種類別分開,這就是最簡單的一種線性不可分問題的情形。
現在我們知道用感知機來解決分類問題是有限制的,也就是不能解決線性不可分問題,因此在應用感知機模型之前需要判斷該問題是否是線性不可分的,至於應該如何具體的去判斷?這個將留到之后的章節去討論。讀完前面我們已經掌握了怎么用感知機講一個東西分成兩類了,但是在現實當中講一個東西分成兩類的場景太少也太簡單了。真正有需求的是將事物分成多類,如果感知機模型具有這樣的功能它在現實中才具有更多的價值,我們也才有學習它的意義。因此我們需要放飛我們的思維,來直觀上看看怎么將事物分成N類.
多維空間的多分類問題
在這我們來進行一次邏輯推導.以感知機為例,假設x
與w
都是一維的,那感知機的形式應該如下:
f(x)=sign(wx+b)
即y=wx+b
是一條直線上的某點,我們通過函數sign將其結果y二值化為1或-1,這從分類的角度看就代表着y=wx+b這條直線上的點集可以被我們的模型分成兩類,即在直線y=wx+b上的點((x,y)中,y大於0的屬於一類,而
y$小於等於0時屬於另一類。我們現在從一維直線的點分類推廣到二維空間的點分類,我們從初等數學已經學過了,平面的表示如下:
ax+by+cz+d=0
那么我們可以得到一個關於平面的函數g(x)=ax+by+d
,那如果要將其分成兩類只需要將g(x)的結果使用sign
函數進行二值化就可以了,其感知機模型的形式如下:
f(x)=sign(g(x))=sign(ax+by+d)
到這兒大家我想大家就能夠想像在三維空間中的點集分類了,三維空間中的一個線性點集表示為g(x)=ax+by+cz+d
,它表示三維空間中的一條線,在這兒我們要對三維空間中的點進行分類同樣構建如下模型即可:
f(x)=sign(g(x))=sign(ax+by+cz+d)
一直到四維空間,五維空間甚至N維空間。從上面我們發現,每多一維,f(x)
就會多一個影響分類結果的因式(子),而在這為了表示方便,影響因式(子)的變量集合我們用x=[x1,x2,x3,…,xn]表示,每個影響因式(子)中的變量所對應的常量因子的集合我們用w=[w1,w2,w3,…,wn]
表示,首先我們先將上面的式子化為更常規的形式:
f(x)=sign(w1x1+w2x2+w3x3+⋯+wnxn+b)
上面的公式的表達是不是感覺很復雜很亂?我們來對其進行整理,由向量的點積性質x⋅w=w1x1+w2x2+w3x3+…+wnxn
,可將f(x)
作如下形式的化簡:
f(x)=sign(w⋅x+b)
看到沒,化簡后就得到了我們在文章開頭所見到的那種形式,這也是獲得感知機模型的一個自然推導的過程,而在這兒x
也有了它的特殊命名——特征空間,因此當見到特征空間這個名詞時回顧一下這兒就能夠理解其是什么意思了。
說到這兒我們了解了分類在高維空間中的表現形式,但是卻還沒有提到多分類的事兒,不知道讀到這里的朋友有沒有自己想到多分類應該怎么做呢?道理很簡單,我們看到以上的所有分類例子都是用sign
函數的二值化特性進行分類,那么我們將其進行一下小小的修改不就實現了多分類嗎?以下為修改示例:
sign(x)=⎧⎩⎨⎪⎪10−1,x>0,x=0,x<0
像上面那樣,豈不就實現了將點分成三類?分類從線性代數的概念來理解其實是一個映射的問題,前面我們提到的二分類問題的映射表示如下:
f:Rnx→{1,−1}
因此多分類的一般表示就應該如下:
f:Rnx→Rny
其實經過前面將w與x向量化之后,我們其實還可以更進一步對其進行擴展,如下所示:
⎡⎣⎢⎢⎢⎢f1(x)f2(x)⋯fm(x)⎤⎦⎥⎥⎥⎥=⎡⎣⎢⎢⎢⎢w1,1w2,1⋯wm,1w1,2w2,2wm,2⋯⋯⋯w1,nw2,nwm,n⎤⎦⎥⎥⎥⎥[x1x2⋯xn]+⎡⎣⎢⎢⎢⎢b1b2⋯bn⎤⎦⎥⎥⎥⎥
進行如上擴展后我們就能夠將N維特征向量空間直接映射到M維空間,通俗點講就是將x
表示的數據集分成了m
類。有的人說上式還不夠精簡,那么我們運用矩陣加法與乘法的性質將其再次化簡得:
⎡⎣⎢⎢⎢⎢f1(x)f2(x)⋯fm(x)⎤⎦⎥⎥⎥⎥=⎡⎣⎢⎢⎢⎢w1,1w2,1⋯wm,1w1,2w2,2wm,2⋯⋯⋯w1,nw2,nwm,nb1b2bm⎤⎦⎥⎥⎥⎥[x1x2⋯xn1]
好了根據這種矩陣表示我們應該也能夠得到分類的一種直觀表示,如圖:
上圖中有四類點,我們要將其各自分開就直接化多條直線就行。到這兒推導先告一段落,我們接下來看看多分類具體是怎么實現與應用的。
感知機如何應用在實際中
介紹與推導
相信到這你已經對感知機了解了,是不是產生了世界盡在我手的感覺?但也可能有可能會打擊你,對你說:”你給我用感知機做個MNIST圖片分類看看?”如果你真的是一個剛入門的人,經過短暫思考后可能會陷入迷茫,貌似感知機學是學了,但完全沒法用來做事啊!別急,接下來讓我們一起來分析MNIST分類究竟該怎么用感知機模型實現。
MNIST是什么很簡單,網上資料也一大堆,總的來說它就是一堆由人手寫的阿拉伯數字的圖片。所謂的MNIST分類就是識別出某張圖片上寫的數字是幾。但是怎么做呢?在這兒有一個關於MNIST的圖片的信息,那就是其中的每一張圖片的大小都是28×28
。在這我們為了簡單起見可以直接將圖片中的每個像素作為特征,也就是特征向量x將會是一個擁有28×28=784
個方向的向量。輸入確定了那么再想想還缺什么?我們要能夠識別數字換個說法其實就是需要將MNIST的圖片分成10類,因為手寫數字都是0到9的數字,如果包含幾十上百的數字,那情況將變得更加復雜。
回到正題,圖片的特征值有784個,且我們需要將圖片分成10類,則我們的模型應該是這樣的:
⎡⎣⎢⎢⎢⎢f1(x)f2(x)⋯f10(x)⎤⎦⎥⎥⎥⎥=⎡⎣⎢⎢⎢⎢w1,1w2,1⋯w10,1w1,2w2,2w10,2⋯⋯⋯w1,784w2,784w10,784b1b2b10⎤⎦⎥⎥⎥⎥[x1x2⋯x7841]
好了模型確定了,還需要做什么?回顧前面對二維世界的原住民分類問題,接下來應該是思考怎么來判斷模型的好壞。由上式可知,在785維空間中,超平面f(x)
將點(x,f(x))分成了兩類。因為該空間存在m個超平面,所以其中的點總共被分為m類。同時我們可以定義當結果f(x)的值越大,這表示該位置對應的數字是機器的預測值。這句話比較拗口,但結合模型表達式細細體味,很容易理解的。我們直接引用上面介紹的二分類問題的損失函數定義,易得w,b
矩陣的更新策略如下:
⎡⎣⎢⎢⎢⎢⎢⎢⎢w1,1w2,1⋯wi,1⋯w10,1w1,2w2,2wi,2w10,2⋯⋯⋯⋯w1,784w2,784wi,784w10,784b1b2bib10⎤⎦⎥⎥⎥⎥⎥⎥⎥=⎡⎣⎢⎢⎢⎢⎢⎢⎢w1,1w2,1⋯wi,1⋯w10,1w1,2w2,2wi,2w10,2⋯⋯⋯⋯w1,784w2,784wi,784w10,784b1b2bib10⎤⎦⎥⎥⎥⎥⎥⎥⎥+⎡⎣⎢⎢⎢⎢⎢⎢⎢sign(1,x)ηsign(2,x)η⋯sign(i,x)η⋯sign(10,x)η⎤⎦⎥⎥⎥⎥⎥⎥⎥[x1x2⋯xi⋯x7841]
以上結果是根據之前感知機的推導過程自然推導得到,如果有不理解的可以再返回去看看感知機的推導過程,這兒就不在贅述了,其中唯一的區別就是這兒使用矩陣直接將更新方式表示了出來,同時涉及的消絕對值符號的地方是構造了一個輔助函數sign(i,x),其意義同前面的sign(x),只是分別將其應用與每個超平面上。到這一步,我們就可以以此寫出代碼了。
實現
Github
討論
上面我們看到了,運用感知機再稍微進行一點原始且直觀的擴展就能實現對圖片的分類,那還可以怎么做呢?對於分類的問題在機器學習領域已經很成熟,因此也產生了許多應用於各種場景,用於解決各種不同分類問題的有效算法。而那些算法有人也直接將其稱作分類器。因此,在這兒對MNIST進行分類只是進行了簡單粗暴的運用了感知機的思想,真正工程中所有考慮的問題將更加復雜,比如還得考慮過擬合問題,算法是否能有效收斂,損失函數的懲值是否合理等等。因此在這留下幾個問題:
特征空間應該怎么獲得與選取?
損失函數的確定還需要注意什么?
如何用機器學習算法分類手寫字母?
如何用機器學習算法分類音頻?
結束語
通過感知機模型我們看到了機器學習的過程,當然感知機是非常非常非常簡單的機器學習模型,它能夠處理的問題也是非常有限。但這並不能妨礙我們了解機器學習是怎樣一門學科,它應該去怎么學。在半個多世紀的發展中,機器學習也產生了很多分支,同時出現了無數經典的模型,但那些都是構建在機器學習基本理論框架下所產生的變化。因此立即機器學習的本質,對於去理解哪些種類繁多的算法將變得更加容易。同時這兒也要闡述一個事實就是,機器學習的核心不是那些各種算法,而是整個機器學習這門學科處理問題的基本思路和流程,我們通過時間的積累掌握可以應用於更多不同場景的算法,這可以幫助我們更快更好的處理問題,但是永遠不要忘記我們使用那些工具的能力。
最后非常感謝您能讀完本文!由於本人知識有限,其中不可避免有不當錯漏之處,還請批評指出。同時也非常感謝李航老師所著《統計學習方法》,讀完受益匪淺!由於篇幅有限很多嚴格的邏輯推導和基本概念在本文沒能講到,因此讀完本文的朋友可以結合該書相互印證,相信你又能有不一樣的理解。
任何意見或建議隨時聯系:
Gmail: yxhlfx@gmail.com
qq : 1137924614
2018.1
---------------------
作者:雙林子木
來源:CSDN
原文:https://blog.csdn.net/yxhlfx/article/details/79093456
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!