有趣的機器學習概念縱覽:從多元擬合,神經網絡到深度學習,給每個感興趣的人
筆者自大學以來一直斷斷續續的學過機器學習啊、自然語言處理啊等等方面的內容,相信基本上每個本科生或者研究生都會接觸過這方面,畢竟是一個如此大的Flag。不過同樣的,在機器學習,或者更大的一個概念,數據科學這個領域中,同樣是學了忘忘了學。不可否認,數學是機器學習的一個基石,但是也是無數人,包括筆者學習機器學習的一個高的門檻,畢竟數學差。而在這篇文章中,原作者并沒有講很多的數學方面的東西,而是以一個有趣實用的方式來介紹機器學習。另一方面,其實很多數學原理也是很有意思的,筆者記得當年看完數學之美有一個不小的感觸,就是知道了TF-IDF的計算公式是怎么來的~
What is Machine Learning: Machine Learning的概念與算法介紹
估計你已經厭煩了聽身邊人高談闊論什么機器學習、深度學習但是自己摸不著頭腦,這篇文章就由淺入深高屋建瓴地給你介紹一下機器學習的方方面面。本文的主旨即是讓每個對機器學習的人都有所得,因此你也不能指望在這篇文章中學到太多高深的東西。言歸正傳,我們先來看看到底什么是機器學習:
Machine learning is the idea that there are generic algorithms that can tell you something interesting about a set of data without you having to write any custom code specific to the problem. Instead of writing code, you feed data to the generic algorithm and it builds its own logic based on the data.
筆者在這里放了原作者的英文描述,以幫助更好地理解。Machine Learning即是指能夠幫你從數據中尋找到感興趣的部分而不需要編寫特定的問題解決方案的通用算法的集合。通用的算法可以根據你不同的輸入數據來自動地構建面向數據集合最優的處理邏輯。舉例而言,算法中一個大的分類即分類算法,它可以將數據分類到不同的組合中。而可以用來識別手寫數字的算法自然也能用來識別垃圾郵件,只不過對于數據特征的提取方法不同。相同的算法輸入不同的數據就能夠用來處理不同的分類邏輯。
“Machine learning” is an umbrella term covering lots of these kinds of generic algorithms.
Two kinds of Machine Learning Algorithms: 兩類機器學習算法
粗淺的劃分,可以認為機器學習攘括的算法主要分為有監督學習與無監督學習,概念不難,但是很重要。
Supervised Learning: 有監督學習
假設你是一位成功的房地產中介,你的事業正在蒸蒸日上,現在打算雇傭更多的中介來幫你一起工作。不過問題來了,你可以一眼看出某個房子到底估值集合,而你的實習生們可沒你這個本事。為了幫你的實習生盡快適應這份工作,你打算寫個小的APP來幫他們根據房子的尺寸、鄰居以及之前賣的類似的屋子的價格來評估這個屋子值多少錢。因此你翻閱了之前的資料,總結成了下表:
利用這些數據,我們希望最后的程序能夠幫我們自動預測一個新的屋子大概能賣到多少錢:
解決這個問題的算法呢就是叫做監督學習,你已知一些歷史數據,可以在這些歷史數據的基礎上構造出大概的處理邏輯。在將這些訓練數據用于算法訓練之后,通用的算法可以根據這個具體的場景得出最優的參數,有點像下面這張圖里描述的一個簡單的智力題:
這個例子里,你能夠知道根據左邊的數字的不同推導出不同的右邊的數字,那么你腦子里就自然而然生成了一個處理該問題的具體的邏輯。在監督學習里,你則是讓機器幫你推導出這種關系,一旦知道了處理特定系列問題的數學方法,其他類似的問題也就都能迎刃而解。
Unsupervised Learning: 無監督學習
我們再回到最初的那個問題,如果你現在不知道每個房間的售價,而只知道房間大小、尺寸以及臨近的地方,那咋辦呢?這個時候,就是無監督學習派上用場的時候了。
這種問題有點類似于某人給了你一長串的數字然后跟你說,我不知道每個數字到底啥意思,不過你看看能不能通過某種模式或者分類或者啥玩意找出它們之間是不是有啥關系。那么對于你的實習生來說,這種類型的數據有啥意義呢?你雖然不能知道每個屋子的價格,但是你可以把這些屋子劃分到不同的市場區間里,然后你大概能發現購買靠近大學城旁邊的屋子的人們更喜歡更多的小臥室戶型,而靠近城郊的更喜歡三個左右的臥室。知道不同地方的購買者的喜好可以幫助你進行更精確的市場定位。
另外你也可以利用無監督學習發現些特殊的房產,譬如一棟大廈,和其他你售賣的屋子差別很大,銷售策略也不同,不過呢卻能讓你收獲更多的傭金。本文下面會更多的關注于有監督學習,不過千萬不能覺得無監督學習就無關緊要了。實際上,在大數據時代,無監督學習反而越來越重要,因為它不需要標注很多的測試數據。
這里的算法分類還是很粗淺的,如果要了解更多的細致的分類可以參考:
House Price Estimation With Supervised Learning: 利用監督學習進行房屋價格估計
作為高等智慧生物,人類可以自動地從環境與經歷中進行學習,所謂熟讀唐詩三百首,不會做詩也會吟,你房子賣多了那自然而然看到了某個屋子也就能知道價格以及該賣給啥樣的人了。這個 Strong_AI 項目也就是希望能夠將人類的這種能力復制到計算機上。不過目前的機器學習算法還沒這么智能,它們只能面向一些特定的有一定限制的問題。因此, Learning 這個概念,在這里更應該描述為:基于某些測試數據找出解決某個問題的等式,筆者也喜歡描述為對于數據的非線性擬合。希望五十年后看到這篇文章的人,能夠推翻這個論述。
Let's Write the Program
基本的思想很好理解,下面就開始簡單的實戰咯。這里假設你還沒寫過任何機器學習的算法,那么直觀的來說,我們可以編寫一些簡單的條件判斷語句來進行房屋價格預測,譬如:
def estimate_house_sales_price(num_of_bedrooms, sqft, neighborhood): price = 0 # 俺們這嘎達,房子基本上每平方200 price_per_sqft = 200 if neighborhood == "hipsterton": # 市中心會貴一點 price_per_sqft = 400 elif neighborhood == "skid row": # 郊區便宜點 price_per_sqft = 100 # 可以根據單價*房子大小得出一個基本價格 price = price_per_sqft * sqft # 基于房間數做點調整 if num_of_bedrooms == 0: # 沒房間的便宜點 price = price?—?20000 else: # 房間越多一般越值錢 price = price + (num_of_bedrooms * 1000) return price
這就是典型的簡答的基于經驗的條件式判斷,你也能通過這種方法得出一個較好地模型。不過如果數據多了或者價格發生較大波動的時候,你就有心無力了。而應用機器學習算法則是讓計算機去幫你總結出這個規律,大概如下所示:
def estimate_house_sales_price(num_of_bedrooms, sqft, neighborhood): price = <computer, plz do some math for me> return price
通俗的理解,價格好比一鍋燉湯,而臥室的數量、客廳面積以及鄰近的街區就是食材,計算機幫你自動地根據不同的食材燉出不同的湯來。如果你是喜歡數學的,那就好比有三個自變量的方程,代碼表述的話大概是下面這個樣子:
def estimate_house_sales_price(num_of_bedrooms, sqft, neighborhood): price = 0 # a little pinch of this price += num_of_bedrooms * .841231951398213 # and a big pinch of that price += sqft * 1231.1231231 # maybe a handful of this price += neighborhood * 2.3242341421 # and finally, just a little extra salt for good measure price += 201.23432095 return price
注意,上面那些譬如 .841... 這樣奇怪的數據,它們就是被稱為 權重 ,只要我們能根據數據尋找出最合適的權重,那我們的函數就能較好地預測出房屋的價格。
Weights
首先,我們用一個比較機械式的方法來尋找最佳的權重。
Step 1
首先將所有的權重設置為1:
def estimate_house_sales_price(num_of_bedrooms, sqft, neighborhood): price = 0 # a little pinch of this price += num_of_bedrooms * 1.0 # and a big pinch of that price += sqft * 1.0 # maybe a handful of this price += neighborhood * 1.0 # and finally, just a little extra salt for good measure price += 1.0 return price
Step 2
拿已知的數據來跑一波,看看預測出來的值和真實值之間有多少差距,大概效果如下所示:
咳咳,可以看出這個差距還是很大的啊,不過不要擔心,獲取正確的權重參數的過程漫漫,我們慢慢來。我們將每一行的真實價格與預測價格的差價相加再除以總的屋子的數量得到一個差價的平均值,即將這個平均值稱為 cost ,即所謂的代價函數。最理想的狀態就是將這個代價值歸零,不過基本上不太可能。因此,我們的目標就是通過不斷的迭代使得代價值不斷地逼近零。
Step 3
不斷測試不同的權重的組合,從而找出其中最靠近零的一組。
Mind Blowage Time
很簡單,不是嗎?讓我們再回顧下你剛才做了啥,拿了一些數據,通過三個泛化的簡單的步驟獲取一個預測值,不過在進一步優化之前,我們先來討論一些小小的思考:
-
過去40年來,包括語言學、翻譯等等在內的很多領域都證明了通用的學習算法也能表現出色,盡管這些算法本身看上去毫無意義。
-
剛才咱寫的那個函數也是所謂的無聲的,即函數中,并不知道臥室數目bedrooms、客廳大小square_feet這些變量到底是啥意思,它只知道輸入某些數字然后得出一個值。這一點就很明顯地和那些面向特定的業務邏輯的處理程序有很大區別。
-
估計你是猜不到哪些權重才是最合適的,或許你連自己為啥要這么寫函數都不能理解,雖然你能證明這么寫就是有用的。
-
如果我們把參數 sqft 改成了圖片中的像素的敏感度,那么原來輸出的值是所謂的價格,而現在的值就是所謂的圖片的類型,輸入的不同,輸出值的意義也就可以不一樣。
Try every number?
言歸正傳,我們還是回到尋找最優的權重組合上來。你可以選擇去帶入所有的可能的權重組合,很明顯是無窮大的一個組合,這條路肯定是行不通的。是時候展示一波數學的魅力了,這里我們介紹一個數學中常見的優化求值的方法:
首先,我們將Step 2中提出的代價方程公式化為如下形式:
然后,我們將這個代價方程變得更加通用一點:
這個方程就代表了我們目前的權重組合離真實的權重組合的差距,如果我們測試多組數據,那么大概可以得出如下的數據圖:
圖中的藍色低點即意味著代價最小,也就是權重組合最接近完美值的時候。
有了圖之后是不是感覺形象多了?我們尋找最優權重的過程就是一步一步走到谷底的過程,如果我們每次小小地修改權重而使得其不斷向谷底靠近,我們也就在向結果靠近。如果你還記得微積分的一些知識,應該知道函數的導數代表著函數的切線方向,換言之,在圖中的任何一點我們通過計算函數的導數就知道變化的方向,即梯度下降的方向。我們可以計算出代價函數中每個變量的偏導數然后將每個當前變量值減去該偏導數,即按照梯度相反的方向前進,那就可以逐步解決谷底咯。如果你感興趣的話,可以深入看看 批量梯度下降 相關的知識。
如果你是打算找個機器學習的工具庫來輔助工具,那么到這里你的知識儲備已經差不多咯,下面我們再扯扯其他的東西。
Something Skip Over: 剛才沒提到的一些東西
上文提到的所謂三步的算法,用專業的名詞表述應該是多元線性回歸。即通過輸入含有多個自變量的訓練數據獲得一個有效的計算表達式用于預測未來的部分房屋的價格。但是上面所講的還是一個非常簡單的例子,可能并不能在真實的環境中完美地工作,這時候就會需要下文即將介紹的包括神級網絡、SVM等等更復雜一點的算法了。另外,我還沒提到一個概念:overfitting(過擬合)。在很多情況下,只要有充足的時間我們都能得到一組在訓練數據集上工作完美的權重組合,但是一旦它們用于預測,就會跌破眼鏡,這就是所謂的過擬合問題。同樣的,關于這方面也有很多的方法可以解決,譬如 正則化 或者使用 交叉驗證 。
一言以蔽之,盡管基礎的概念非常簡單,仍然會需要一些技巧或者經驗來讓整個模型更好地工作,就好像一個才學完Java基礎的菜鳥和一個十年的老司機一樣。
Further Reading: 深入閱讀
可能看完了這些,覺著ML好簡單啊,那這么簡單的東西又是如何應用到圖片識別等等復雜的領域的呢?你可能會覺得可以用機器學習來解決任何問題,只要你有足夠多的數據。不過還是要潑點冷水,千萬記住,機器學習的算法只在你有足夠的解決某個特定的問題的數據的時候才能真正起作用。譬如,如果你想依靠某個屋子內盆栽的數目來預測某個屋子的價格,呵呵。這是因為房屋的價格和里面的盆栽數目沒啥必然聯系,不管你怎么嘗試,輸入怎么多的數據,可能都不能如你所愿。
所以,總結而言,如果是能夠手動解決的問題,那計算機可能解決的更快,但是它也無法解決壓根解決不了的問題。在原作者看來,目前機器學習存在的一個很大的問題就是依然如陽春白雪般,只是部分科研人員或者商業分析人員的關注對象,其他人并不能簡單地理解或者使用,在本節的最后也推薦一些公開的課程給對機器學習有興趣的朋友:
Neural Network: 神級網絡
上文中,我們通過一個簡單的房價預測的例子了解了機器學習的基本含義,在本節,我們將會繼續用一些泛化的算法搭配上一些特定的數據做些有趣的事情。本節的例子大概如下圖所示,一個很多人的童年必備的游戲:馬里奧,讓我們用神級網絡幫你設計一些新奇的關卡吧。
在正文之前,還是要強調下,本文是面向所有對機器學習有興趣的朋友,所以大牛們看到了勿笑。
Introduction To Neural Networks: 神經網絡模型初探
上文中我們是使用了多元線性回歸來進行房屋價格預測,數據格式大概這個樣子:
最后得到的函數是:
def estimate_house_sales_price(num_of_bedrooms, sqft, neighborhood): price = 0 # a little pinch of this price += num_of_bedrooms * 0.123 # and a big pinch of that price += sqft * 0.41 # maybe a handful of this price += neighborhood * 0.57 return price
如果用圖來表示的話,大概是這個樣子:
不過正如上文中提到的,這個算法只能處理一些較為簡單的問題,即結果與輸入的變量之間存在著某些線性關系。Too young,Too Simple,真實的房價和這些可不僅僅只有簡單的線性關系,譬如鄰近的街區這個因子可能對面積較大和面積特別小的房子有影響,但是對于那些中等大小的毫無關系,換言之,price與neighborhood之間并不是線性關聯,而是類似于二次函數或者拋物線函數圖之間的非線性關聯。這種情況下,我們可能得到不同的權重值(形象來理解,可能部分權重值是收斂到某個局部最優):
現在等于每次預測我們有了四個獨立的預測值,下一步就是需要將四個值合并為一個最終的輸出值:
What is Neural Network?: 神經網絡初識
我們將上文提到的兩個步驟合并起來,大概如下圖所示:
咳咳,沒錯,這就是一個典型的神級網絡,每個節點接收一系列的輸入,為每個輸入分配權重,然后計算輸出值。通過連接這一系列的節點,我們就能夠為復雜的函數建模。同樣為了簡單起見,我在這里也跳過了很多概念,譬如 feature scaling 以及 activation function ,不過核心的概念是:
-
每個能夠接收一系列的輸入并且能夠按權重求和的估值函數被稱為Neuron(神經元)
-
多個簡單的神經元的連接可以用來構造處理復雜問題的模型
有點像樂高方塊,單個的樂高方塊非常簡單,而大量的樂高方塊卻可以構建出任何形狀的物體:
Giving Our Neural Network a Memory: 給神級網絡加點上下文
目前,整個神級網絡是無狀態的,即對于任何相同的輸入都返回相同的輸出。這個特性在很多情況下,譬如房屋價格估計中是不錯的,不過這種模式并不能處理時間序列的數據。舉個栗子,我們常用的輸入法中有個智能聯想的功能,可以根據用戶輸入的前幾個字符預測下一個可能的輸入字符。最簡單的,可以根據常見的語法來推測下一個出現的字符,而我們也可以根據用戶歷史輸入的記錄來推測下一個出現的字符。基于這些考慮,我們的神經網絡模型即如下所示:
譬如用戶已經輸入了如下的語句:
Robert Cohn was once middleweight boxi
你可能會猜想是 n ,這樣整個詞匯就是 boxing ,這是基于你看過了前面的語句以及基本的英文語法得出的推論,另外, middleweight 這個單詞也給了我們額外的提示,跟在它后面的是 boxing 。換言之,在文本預測中,如果你能將句子的上下文也考慮進來,再加上基本的語法知識就能較為準確地預測出下一個可能的字符。因此,我們需要給上面描述的神經網絡模型添加一些狀態信息,也就是所謂的上下文的信息:
在神經網絡模型中也保持了對于上下文的追蹤,從而使得該模型不僅僅能預測第一個詞是啥,也能預測最有可能出現的下一個詞匯。該模型就是所謂的Recurrent Neural Network:循環神經網絡的基本概念。每次使用神經網絡的同時也在更新其參數,也就保證了能夠根據最新的輸入內容預測下一個可能的字符,只要有足夠的內存情況下,它可以將完整的時序上下文全部考慮進去。
Generating a story: 生成一個完整的故事
正如上文所說,文本預測在實際應用中一個典型的例子就是輸入法,譬如iPhone里面會根據你之前輸入的字符自動幫你補全:
不過我們做的更瘋狂一點,既然模型可以根據上一個字符自動預測下一個字符,那我們何不讓模型來自動構建一個完整的故事? 我們在這里使用 Andrej Karpathy 創建的 Recurrent Neural Network implementation 框架來進行實驗,他也發表了一系列 關于如何使用RNN進行文檔生成的博客 。我們還是使用 The Sun Also Rises 這篇文章,該文章包括大小寫、標點符號等在內一共有84個不同的字符以及362239個詞匯。這個數據集合的大小和真實環境中的應用文本相比還是很小的,為了盡可能模仿原作者的風格,最好的是能有數倍的文本進行訓練,不過作為例子這邊還是足夠的。經過大概100次迭代之后,得到的結果是:
hjCTCnhoofeoxelif edElobe negnk e iohehasenoldndAmdaI ayio pe e h’e btentmuhgehi bcgdltt. gey heho grpiahe. Ddelnss.eelaishaner” cot AAfhB ht ltny ehbih a”on bhnte ectrsnae abeahngy amo k ns aeo?cdse nh a taei.rairrhelardr er deffijha
慘不忍睹啊,繼續進行訓練,大概1000次迭代之后,內容看起來好一點了:
hing soor ither. And the caraos, and the crowebel for figttier and ale the room of me? Streat was not to him Bill-stook of the momansbed mig out ust on the bull, out here. I been soms inick stalling that aid. “Hon’t me and acrained on .Hw’s don’t you for the roed,” In’s pair.” “Alough marith him.”
已經能夠識別基本的句型結構與語法規則咯,甚至能夠較好地為上下文添加標點符號了,不過還是存在著大量的無意義詞匯,我們繼續增加訓練的次數:
He went over to the gate of the café. It was like a country bed. “Do you know it’s been me.” “Damned us,” Bill said. “I was dangerous,” I said. “You were she did it and think I would a fine cape you,” I said. “I can’t look strange in the cab.” “You know I was this is though,” Brett said. “It’s a fights no matter?” “It makes to do it.” “You make it?” “Sit down,” I said. “I wish I wasn’t do a little with the man.” “You found it.” “I don’t know.” “You see, I’m sorry of chatches,” Bill said. “You think it’s a friend off back and make you really drunk.”
現在差不多能看了,有些句式還是很模仿Hemingway’s的風格的,而原作者的內容是:
There were a few people inside at the bar, and outside, alone, sat Harvey Stone. He had a pile of saucers in front of him, and he needed a shave. “Sit down,” said Harvey, “I’ve been looking for you.” “What’s the matter?” “Nothing. Just looking for you.” “Been out to the races?” “No. Not since Sunday.” “What do you hear from the States?” “Nothing. Absolutely nothing.” “What’s the matter?”
Super Mario: 利用神級網絡進行Mario過關訓練
In 2015, Nintendo 宣布了 Super Mario Maker? 用于Wii U游戲系統上。
這個制作器能夠讓你去手動制作馬里奧的一些關卡,很不錯的和朋友之間進行互動的小工具。你可以添加常見的障礙物或者敵人到你自己設計的關卡中,有點像可視化的樂高工作臺。我們可以使用剛才創建的用于預測Hemingway文本的模型來自動地創建一個超級馬里奧的關卡。首先呢,我們還是需要去找一些訓練數據,最早的1985年推出的經典的超級馬里奧的游戲不錯:
這個游戲有大概32個關卡,其中70%的場景都有相似的外觀,很適合用來做訓練數據啊。我找來了每一關的設計方案,網上有很多類似的教程教你怎么從內存中讀取游戲的設計方案,有興趣的話你也可以試試。下面呢就是一個經典的全景視圖:
用放大鏡觀察的話,可以看出每一關都是由一系列的網格狀對象組成的:
這樣的話,我們可以將每個網格中的對象用一個字符代替,而整個關卡的字符化表述就是:
-------------------------- -------------------------- -------------------------- #??#---------------------- -------------------------- -------------------------- -------------------------- -##------=--=----------==- --------==--==--------===- -------===--===------====- ------====--====----=====- =========================-
其中:
-
- 代表空白
-
= 代表堅固的方塊
-
# 代表那些可以被撞破的塊
-
? 代表錢幣塊
仔細瞅瞅這個文件,你會發現如果按照一行一行從左到右讀的話,好像是毫無頭緒:
不過如果按照列的次序從上往下讀的話,你會發現還是有點套路的:
為了更好地訓練數據,我們打算按列來分割數據,這里我們會使用 特征選擇 的技術來將數據轉化為最合適的表示。首先,我們將整個文本旋轉90度:
-----------= -------#---= -------#---= -------?---= -------#---= -----------= -----------= ----------@= ----------@= -----------= -----------= -----------= ---------PP= ---------PP= ----------== ---------=== --------==== -------===== ------====== -----======= ---========= ---=========
然后就可以使用上面創建好的模型進行訓練咯,經過幾輪訓練之后大概可以得出這個樣子:
-------------------------- LL+<&=------P------------- -------- ---------------------T--#-- ----- -=--=-=------------=-&--T-------------- -------------------- --=------$-=#-=-_ --------------=----=<---- -------b -
最初的訓練里模型認知到應該大量的出現 - 與 = 字符,不過還是很粗糙,再經過幾千次的訓練,得出的內容是:
-- -----------= ----------= --------PP= --------PP= -----------= -----------= -----------= -------?---= -----------= -----------=
此時模型已經能夠認知到需要將每行保證相同的長度,甚至開始尋找出Mario內在的規律:管道呢一般都是兩個塊這么寬,所以它將所有的 P 都放到了2*2的矩陣中,聰明了一點啊。繼續學習:
--------PP= --------PP= ----------= ----------= ----------= ---PPP=---= ---PPP=---= ----------=
看上去像模像樣了,其中有幾個需要特別注意的地方:
-
Lakitu,就是那個小怪獸被放到了半空中,跟Mario關卡一樣一樣的。
-
它認知到了應該把管道插入大地
-
并沒有讓玩家無路可走
-
看起來風格非常像最傳統的馬里奧的版本
最后生成出來的游戲截圖大概是這樣的:
你可以在 這里 觀看完整的游戲視頻。
Toys VS Real World Applications
這里用于訓練模型的循環神經網絡算法與真實環境下大公司用于解決語音識別以及文本翻譯等常見問題的算法一本同源,而讓我們的模型看上去好像個玩具一樣的原因在于我們的訓練數據。僅僅取自最早期的超級馬里奧的一些關卡數據遠遠不足以讓我們的模型出類拔萃。如果我們能夠獲取由其他玩家創建的成百上千的關卡信息,我們可以讓模型變得更加完善。不過可惜的是我們壓根獲取不到這些數據。
隨著機器學習在不同的產業中變得日漸重要,好的程序與壞的程序之間的差異越發體現在輸入數據的多少。這也就是為啥像Google或者非死book這樣的大公司千方百計地想獲取你的數據。譬如Google最近開源的 TensorFlow ,一個用于大規模可擴展的機器學習的集群搭建應用,它本身就是Google內部集群的重要組成部分。不過沒有Google的海量數據作為輔助,你壓根創建不了媲美于Google翻譯那樣的牛逼程序。下次你再打開 Google Maps Location History 或者 非死book Location History ,想想它們是不是記錄下你日常的東西。
Further Reading
條條大道通羅馬,在機器學習中解決問題的辦法也永遠不止一個。你可以有很多的選項來決定如何進行數據預處理以及應該用啥算法。 增強學習 正是可以幫你將多個單一的方法組合起來的好途徑。如果你想更深入的了解,你可以參考下面幾篇較為專業的論文:
-
Amy K. Hoover ’s team used an approach that represents each type of level object (pipes, ground, platforms, etc) as if it were single voice in an overall symphony . Using a process called functional scaffolding, the system can augment levels with blocks of any given object type. For example, you could sketch out the basic shape of a level and it could add in pipes and question blocks to complete your design.
-
Steve Dahlskog ’s team showed that modeling each column of level data as a series of n-gram “words” makes it possible to generate levels with a much simpler algorithm than a large RNN.
Object Recognition In Images With Deep Learning: 利用深度學習對于圖片中對象進行識別
近年來關于深度學習的討論非常火爆,特別是之前阿爾法狗大戰李世乭之后,更是引發了人們廣泛地興趣。南大的周志華教授在《機器學習》這本書的引言里,提到了他對于深度學習的看法:深度學習掀起的熱潮也許大過它本身真正的貢獻,在理論和技術上并沒有太大的創新,只不過是由于硬件技術的革命,從而得到比過去更精細的結果。相信讀者看完了第三部分也會有所感。
仁者見仁智者見智,這一章節就讓我們一起揭開深度學習的神秘面紗。在本章中,我們還是基于一個實際的例子來介紹下深度學習的大概原理,這里我們會使用簡單的卷積神級網絡來進行圖片中的對象識別。換言之,就類似于Google Photos的以圖搜圖的簡單實現,大概最終的產品功能是這個樣子的:
就像前兩章一樣,本節的內容盡量做到即不云山霧罩,不知所云,也不陽春白雪,曲高和寡,希望每個隊機器學習感興趣的人都能有所收獲。這里我們不會提到太多的數學原理與實現細節,所以也不能眼高手低,覺得深度學習不過爾爾呦。
Recognizing Objects: 對象識別
先來看一個有趣的漫畫:
這個漫畫可能有點夸張了,不過它的靈感還是來自于一個現實的問題:一個三歲的小孩能夠輕易的辨別出照片中的鳥兒,而最優秀的計算機科學家需要用50年的時間來教會機器去識別鳥兒。在過去的數年中,我們發現了一個對象識別的好辦法,即是利用深度卷積神級網絡。有點像William Gibson的科幻小說哈,不過只要跟著本文一步一步來,你就會發現這事一點也不神秘。Talk is cheap, Show you the word~
Starting Simple: 先來點簡單的
在嘗試怎么識別照片中的鳥兒之前,我們先從一些簡單的識別開始:怎么識別手寫的數字8。在上一章節,我們了解了神級網絡是如何通過鏈式連接組合大量的簡單的neurons(神經元)來解決一些復雜的問題。我們創建了一個簡單的神級網絡來基于床鋪數目、房間大小以及鄰居的類型來預測某個屋子的可能的價格。
再重述下機器學習的理念,即是一些通用的,可以根據不同的數據來處理不同的問題的算法。因此我們可以簡單地修改一些神級網絡就可以識別手寫文字,在這里簡單起見,我們只考慮一個字符:手寫的數字8。
大量的數據是機器學習不可代替的前提條件與基石,首先我們需要去尋找很多的訓練數據。索性對于這個問題的研究已持續了很久,也有很多的開源數據集合,譬如 MNIST關于手寫數字的數據集 。MNIST提供了60000張不同的關于手寫數字的圖片,每個都是18*18的大小,其中部分關于8的大概是這個樣子:
上章節中構造的神級網絡有三個輸入,在這里我們希望用神級網絡來處理圖片,第二步就是需要將一張圖片轉化為數字的組合,即是計算機可以處理的樣子。表擔心,這一步還是很簡單的。對于電腦而言,一張圖片就是一個多維的整型數組,每個元素代表了每個像素的模糊度,大概是這樣子:
為了能夠將圖片應用到我們的神經網絡模型中,我們需要將18*18像素的圖片轉化為324個數字:
這次的共有324個輸入,我們需要將神經網絡擴大化轉化為324個輸入的模型:
注意,我們的神經網絡模型中有兩個輸出,第一個輸出預測該圖片是8的概率,第二個輸出是預測圖片不是8的概率。對于要辨別的圖片,我們可以使用神經網絡將對象劃分到不同的群組中。雖然這次我們的神經網絡比上次大那么多,但是現代的電腦仍然可以在眨眼間處理上百個節點,甚至于能夠在你的手機上工作。(PS:Tensorflow最近支持iOS了)在訓練的時候,當我們輸入一張確定是8的圖片的時候,會告訴它概率是100%,不是8的時候輸入的概率就是0%。我們部分的訓練數據如下所示:
Tunnel Vision
雖然我們上面一直說這個任務不難,不過也沒那么簡單。首先,我們的識別器能夠對于標準的圖片,就是那些數字端端正正坐在中間,不歪不扭的圖片,可以非常高效準確地識別,譬如:
不過實際情況總不會如我們所愿,當那些熊孩子一般的8也混進來的時候,我們的識別器就懵逼了。
1. Searching with a Sliding Window: 基于滑動窗口的搜索
雖然道路很曲折,但是問題還是要解決的,我們先來試試暴力搜索法。我們已經創建了一個可以識別端端正正的8的識別器,我們的第一個思路就是把圖片分為一塊塊地小區域,然后對每個區域進行識別,判斷是否屬于8,大概思路如下所示:
這方法叫滑動窗口法,典型的暴力搜索解決方法。在部分特定的情況下能起到較好地作用,不過效率非常低下。譬如對于同一類型但是大小不同的圖片,你可能就需要一遍遍地搜索。
1. More data and a Deep Neural Net
剛才那個識別器訓練的時候,我們只是把部分規規矩矩的圖片作為輸入的訓練數據。不過如果我們選擇更多的訓練數據時,自然也包含那些七歪八斜的8的圖片,會不會起到什么神奇的效果呢?我們甚至不需要去搜集更多的測試數據,只要寫個腳本然后把8放到圖片不同的位置上即可:
用這種方法,我們可以方便地創建無限的訓練數據。數據有了,我們也需要來擴展下我們的神經網絡,從而使它能夠學習些更復雜的模式。具體而言,我們需要添加更多的中間層:
這個呢,就是我們所謂的 深度神經網絡 ,因為它比傳統的神經網絡有更多的中間層。這個概念從十九世紀六十年代以來就有了,不過訓練大型的神經網絡一直很緩慢而無法達到真實的應用預期。不過近年來隨著我們認識到使用3D圖卡來代替傳統的CPU處理器來進行神經網絡的訓練,使用大型的神經網絡突然之間就變得不再那么遙不可及。
不過盡管我們可以依靠3D圖卡解決計算問題,仍然需要尋找合適的解決方案。我們需要尋找合適的將圖片處理能夠輸入神經網絡的方法。好好考慮下,我們訓練一個網絡用來專門識別圖片頂部的8與訓練一個網絡專門用來識別圖片底部的8,把這兩個網絡分割開來,好像壓根沒啥意義。因此,我們最終要得到的神經網絡是要能智能識別無論在圖片中哪個位置的8。
The Solution is Convolution:卷積神經網絡
人們在看圖片的時候一般都會自帶層次分割的眼光,譬如下面這張圖:
你可以一眼看出圖片中的不同的層次:
-
地上覆蓋著草皮與水泥
-
有個寶寶
-
寶寶坐在個木馬上
-
木馬在草地上
更重要的是,不管寶寶坐在啥上面,我們都能一眼看到那嘎達有個寶寶。即使寶寶坐在汽車、飛機上,我們不經過重新的學習也可以一眼分辨出來。可惜現在我們的神經網絡還做不到這一點,它會把不同圖片里面的8當成不同的東西對待,并不能理解如果在圖片中移動8,對于8而言是沒有任何改變的。也就意味著對于不同位置的圖片仍然需要進行重新學習。我們需要賦予我們的神經網絡能夠理解平移不變性:不管8出現在圖片的哪個地方,它還是那個8。我們打算用所謂的卷積的方法來進行處理,這個概念部分來自于計算機科學,部分來自生物學,譬如神經學家教會貓如何去辨別圖片。
How Convolution Works
上面我們提到一個暴力破解的辦法是將圖片分割到一個又一個的小網格中,我們需要稍微改進下這個辦法。
1. 將圖片分割為重疊的磚塊
譬如上面提到的滑動窗口搜索,我們將原圖片分割為獨立的小塊,大概如下圖所示:
通過這一步操作,我們將原始圖片分割為了77張大小相同的小圖片。
2. 將每個圖片瓷磚輸入到小的神經網絡中
之前我們就訓練一個小的神經網絡可以來判斷單個圖片是否屬于8,不過在這里我們的輸出并不是直接判斷是不是8,而是處理輸出一個特征數組:
對于不同的圖片的瓷磚塊,我們都會使用 具有相同權重的神經網絡 來進行處理。換言之,我們將不同的圖片小塊都同等對待,如果在圖片里發現了啥好玩的東西,我們會將該圖片標識為待進一步觀察的。
3. 將每個小塊的處理結果存入一個新的數組
對于每個小塊輸出的數組,我們希望依然保持圖片塊之間的相對位置關聯,因此我們將每個輸出的數組仍然按照之前的圖片塊的次序排布:
到這里,我們輸入一個大圖片,輸出一個相對而言緊密一點的數組,包含了我們可能剛興趣的塊的記錄。
4. 縮減像素采樣
上一步的結果是輸出一個數組,會映射出原始圖片中的哪些部分是我們感興趣的。不過整個數組還是太大了:
為了縮減該特征數組的大小,我們打算使用所謂的 max pooling 算法來縮減像素采樣數組的大小,這算法聽起來高大上,不過還是挺簡單的:
Max pooling處理過程上呢就是將原特征矩陣按照2*2分割為不同的塊,然后從每個方塊中找出最有興趣的位保留,然后丟棄其他三個數組。
5. 進行預測
截至目前,一個大圖片已經轉化為了一個相對較小地數組。該數組中只是一系列的數字,因此我們可以將該小數組作為輸入傳入另一個神經網絡,該神經網絡會判斷該圖片是否符合我們的預期判斷。為了區別于上面的卷積步驟,我們將此稱為 fully connected 網絡,整個步驟呢,如下所示:
6. 添加更多的步驟
上面的圖片處理過程可以總結為以下步驟:
-
Convolution: 卷積
-
Max-pooling: 特征各維最大匯總
-
Full-connected: 全連接網絡
在真實的應用中,這幾個步驟可以組合排列使用多次,你可以選擇使用兩個、三個甚至十個卷積層,也可以在任何時候使用Max-pooling來減少數據的大小。基本的思想就是將一個大圖片不斷地濃縮直到輸出一個單一值。使用更多地卷積步驟,你的網絡就可以處理學習更多地特征。舉例而言,第一個卷積層可以用于識別銳邊,第二個卷積層能夠識別尖銳物體中的鳥嘴,而第三個卷積層可以基于其對于鳥嘴的知識識別整個鳥。下圖就展示一個更現實點地深度卷積網絡:
在這個例子中,最早地是輸入一個224*224像素的圖片,然后分別使用兩次卷積與Max-pooling,然后再依次使用卷積與Max-pooling,最后使用兩個全連接層。最后的結果就是圖片被分到哪一類。
Building our Bird Classifier: 構建一個真實的鳥兒分類器
概念了解了,下面我們就動手寫一個真正的鳥類分類器。同樣地,我們需要先收集一些數據。免費的 CIFAR10 data set 包含了關于鳥兒的6000多張圖片以及52000張不是鳥類的圖片。如果不夠, Caltech-UCSD Birds-200–2011 data set 中還有12000張鳥類的圖片。其中關于鳥類的圖片大概如下所示:
非鳥類的圖片大概這樣:
這邊我們會使用 TFLearn 來構建我們的程序,TFLearn是對于Google的 TensorFlow 深度學習庫的一個包裹,提供了更易用的API,可以讓編寫卷積神經網絡就好像編譯我們其他的網絡層一樣簡單:
# -*- coding: utf-8 -*- """ Based on the tflearn example located here: https://github.com/tflearn/tflearn/blob/master/examples/images/convnet_cifar10.py """ from __future__ import division, print_function, absolute_import # Import tflearn and some helpers import tflearn from tflearn.data_utils import shuffle from tflearn.layers.core import input_data, dropout, fully_connected from tflearn.layers.conv import conv_2d, max_pool_2d from tflearn.layers.estimator import regression from tflearn.data_preprocessing import ImagePreprocessing from tflearn.data_augmentation import ImageAugmentation import pickle # Load the data set X, Y, X_test, Y_test = pickle.load(open("full_dataset.pkl", "rb")) # Shuffle the data X, Y = shuffle(X, Y) # Make sure the data is normalized img_prep = ImagePreprocessing() img_prep.add_featurewise_zero_center() img_prep.add_featurewise_stdnorm() # Create extra synthetic training data by flipping, rotating and blurring the # images on our data set. img_aug = ImageAugmentation() img_aug.add_random_flip_leftright() img_aug.add_random_rotation(max_angle=25.) img_aug.add_random_blur(sigma_max=3.) # Define our network architecture: # Input is a 32x32 image with 3 color channels (red, green and blue) network = input_data(shape=[None, 32, 32, 3], data_preprocessing=img_prep, data_augmentation=img_aug) # Step 1: Convolution network = conv_2d(network, 32, 3, activation='relu') # Step 2: Max pooling network = max_pool_2d(network, 2) # Step 3: Convolution again network = conv_2d(network, 64, 3, activation='relu') # Step 4: Convolution yet again network = conv_2d(network, 64, 3, activation='relu') # Step 5: Max pooling again network = max_pool_2d(network, 2) # Step 6: Fully-connected 512 node neural network network = fully_connected(network, 512, activation='relu') # Step 7: Dropout - throw away some data randomly during training to prevent over-fitting network = dropout(network, 0.5) # Step 8: Fully-connected neural network with two outputs (0=isn't a bird, 1=is a bird) to make the final prediction network = fully_connected(network, 2, activation='softmax') # Tell tflearn how we want to train the network network = regression(network, optimizer='adam', loss='categorical_crossentropy', learning_rate=0.001) # Wrap the network in a model object model = tflearn.DNN(network, tensorboard_verbose=0, checkpoint_path='bird-classifier.tfl.ckpt') # Train it! We'll do 100 training passes and monitor it as it goes. model.fit(X, Y, n_epoch=100, shuffle=True, validation_set=(X_test, Y_test), show_metric=True, batch_size=96, snapshot_epoch=True, run_id='bird-classifier') # Save model when training is complete to a file model.save("bird-classifier.tfl") print("Network trained and saved as bird-classifier.tfl!")
如果你有足夠的RAM,譬如Nvidia GeForce GTX 980 Ti或者更好地硬件設備,大概能在1小時內訓練結束,如果是普通的電腦,時間要耗費地更久一點。隨著一輪一輪地訓練,準確度也在不斷提高,第一輪中準確率只有75.4%,十輪之后準確率到91.7%,在50輪之后,可以達到95.5%的準確率。
Testing out Network
我們可以使用如下腳本進行圖片的分類預測:
# -*- coding: utf-8 -*- from __future__ import division, print_function, absolute_import import tflearn from tflearn.layers.core import input_data, dropout, fully_connected from tflearn.layers.conv import conv_2d, max_pool_2d from tflearn.layers.estimator import regression from tflearn.data_preprocessing import ImagePreprocessing from tflearn.data_augmentation import ImageAugmentation import scipy import numpy as np import argparse parser = argparse.ArgumentParser(description='Decide if an image is a picture of a bird') parser.add_argument('image', type=str, help='The image image file to check') args = parser.parse_args() # Same network definition as before img_prep = ImagePreprocessing() img_prep.add_featurewise_zero_center() img_prep.add_featurewise_stdnorm() img_aug = ImageAugmentation() img_aug.add_random_flip_leftright() img_aug.add_random_rotation(max_angle=25.) img_aug.add_random_blur(sigma_max=3.) network = input_data(shape=[None, 32, 32, 3], data_preprocessing=img_prep, data_augmentation=img_aug) network = conv_2d(network, 32, 3, activation='relu') network = max_pool_2d(network, 2) network = conv_2d(network, 64, 3, activation='relu') network = conv_2d(network, 64, 3, activation='relu') network = max_pool_2d(network, 2) network = fully_connected(network, 512, activation='relu') network = dropout(network, 0.5) network = fully_connected(network, 2, activation='softmax') network = regression(network, optimizer='adam', loss='categorical_crossentropy', learning_rate=0.001) model = tflearn.DNN(network, tensorboard_verbose=0, checkpoint_path='bird-classifier.tfl.ckpt') model.load("bird-classifier.tfl.ckpt-50912") # Load the image file img = scipy.ndimage.imread(args.image, mode="RGB") # Scale it to 32x32 img = scipy.misc.imresize(img, (32, 32), interp="bicubic").astype(np.float32, casting='unsafe') # Predict prediction = model.predict([img]) # Check the result. is_bird = np.argmax(prediction[0]) == 1 if is_bird: print("That's a bird!") else: print("That's not a bird!")
How accurate is 95% accurate?: 怎么理解這95%的準確率
剛才有提到,我們的程序有95%的準確度,不過這并不意味著你拿張圖片來,就肯定有95%的概率進行準確分類。舉個栗子,如果我們的訓練數據中有5%的圖片是鳥類而其他95%的都不是鳥類,那么就意味著每次預測其實不是鳥類的準確度達到95%。因此,我們不僅要關注整體的分類的準確度,還需要關注分類正確的數目,以及哪些圖片分類失敗,為啥失敗的。這里我們假設預測結果并不是簡單的正確或者錯誤,而是分到不同的類別中:
-
首先,我們將正確被標識為鳥類的鳥類圖片稱為:True Positives
-
其次,對于標識為鳥類的非鳥類圖片稱為:True Negatives
-
對于劃分為鳥類的非鳥類圖片稱為:False Positives
-
對于劃分為非鳥類的鳥類圖片稱為:False Negatives
最后的值可以用如下矩陣表示:
這種分法的一個現實的意義譬如我們編寫一個程序在MRI圖片中檢測癌癥,false positives的結果很明顯好于false negatives。False negatives意味著那些你告訴他沒得癌癥但是人家就是癌癥的人。這種錯誤的比例應該著重降低。除此之外,我們還計算 Precision_and_recall 來衡量整體的準確度:
來自: https://segmentfault.com/a/1190000005746236