前端AI實戰——告訴世界前端也能做AI

jerryOnlyZRJ發表於2018-10-08

我想大多數人和我一樣,第一次聽見“人工智慧”這個詞的時候都會覺得是一個很高大上、遙不可及的概念,特別像我這樣一個平凡的前端,和大部分人一樣,都覺得人工智慧其實離我們很遙遠,我們對它的印象總是停留在各種各樣神奇而又複雜的演算法,這些彷彿都是那些技術專家或者海歸博士才有能力去做的工作。我也曾一度以為自己和這個行業沒有太多緣分,但自從Tensorflow釋出了JS版本之後,這一領域又引起了我的注意。在python壟斷的時代,釋出JS工具庫不就是意味著我們前端工程師也可以參與其中?

當我決定開始投身這片領域做一些自己感興趣的事情的時候,卻發現身邊的人投來的都是鄙夷的目光,他們對前端的印象,還總是停留在上個年代那些只會寫寫頁面指令碼的切圖仔,只有身處這片領域的我們才知道大前端時代早已發生了翻天覆地的變革。

今天,我就帶領大家從原理開始,儘可能用最通俗易懂的方式,讓JS的愛好者們快速上手人工智慧。

具體專案可參照:github.com/jerryOnlyZR…

本文就單拿人工智慧下的一塊小領域——“影像識別”作一些簡單介紹和實戰指引,當然這些都只是這片大領域下的冰山一角,還有很多很多知識等著你去發掘。

1.CNN卷積神經網路原理剖析

如果我不講解這部分內容,而是直接教你們怎麼使用一個現成的庫,那這篇文章就沒什麼價值了,看完之後給你們留下的也一定都會是“開局一張圖,過程全靠編”的錯覺。因此,要真正瞭解人工智慧,就應該進入這個黑盒,裡面的思想才是精華。

1.1.影像灰度級與灰度圖

1.1.1.基本概念

要做影像識別,我們肯定要先從影像下手,大家先理解一個概念——影像灰度級。

眾所周知,我們的圖片都是由許多畫素點組成的,就好像一張100*100畫素的圖片,就表示它是由10000個畫素點呈現的。但你可曾想過,這些畫素點可以由一系列的數字表示嘛?

就先不拿彩色的圖片吧,彩色太複雜了,我們就先拿一張黑白的圖片為例,假設我們以黑色畫素的深淺為基準,將白色到黑色的漸變過程分為不同的等級,這樣,圖片上每一個畫素點都能用一個最為臨近的等級數字表示出來:

前端AI實戰——告訴世界前端也能做AI

如果我們用1表示白色,用0表示黑色,將影像二值化,最後以向量(數字)的形式呈現出來,結果大概就是這樣:(下圖是一張5*5的二值化影像,沒有具體表示含義,只作示例)

前端AI實戰——告訴世界前端也能做AI

同理,如果是彩色的影像,那我們是不是可以把R、G、B三個維度的畫素單獨提取出來分別處理呢?這樣,每一個維度不就可以單獨視為一張灰度圖。

前端AI實戰——告訴世界前端也能做AI

1.1.2.平滑影像與特徵點

如果一張影像沒有什麼畫素突變,比如一張全白的圖片,如果以數字表示,自然都是0,那我們可以稱這張圖片的畫素點是平滑的。再比如這張全白的圖片上有一個黑點,自然,灰度圖上就會有一個突兀的數值,我們就把它稱作特徵點,通常來說,影像的特徵點有可能是噪聲、邊緣或者圖片的實際特徵。

1.2.神經網路與模型訓練

tensorflow在釋出了JS版本的工具庫後,也同時製作了一個Tensorflow遊樂場,開啟之後,引入眼簾的網頁中央這個東西便是神經網路:

前端AI實戰——告訴世界前端也能做AI

從圖中,我們可以看到神經網路有很多不同的層級,就是圖中的Layers,每一層都是前一層經過濾波器計算後的結果,越多的層級以及越多的“神經元”經過一次計算過程計算出來的結果誤差越小,同時,計算的時間也會增加。神經網路正是模仿了我們人類腦袋裡的神經元經過了一系列計算然後學習事物的過程。這裡推薦阮一峰的《神經網路入門》這篇文章,能夠幫助大家更加淺顯地瞭解神經網路是什麼。

在我們的卷積神經網路中,這些層級都有不同的名字:輸入層、卷積層、池化層以及輸出層。

  • 輸入層:我們輸入的向量化之後的影像
  • 卷積層:經過濾波器卷積計算之後的影像
  • 池化層:經過池化濾波器卷積計算之後的影像
  • 輸出層:輸出資料

Features就是我們的運算元,也稱為濾波器,但是每種不同的濾波器對最後的輸出結果都會有不同的影響,進過訓練之後,機器會通過我們賦予的演算法(比如啟用函式等等)計算出哪些濾波器會對輸出結果造成較大的誤差,哪些濾波器對輸出結果壓根沒有影響(原理很簡單,第一次計算使用所有濾波器,第二次計算拿掉某一個濾波器,然後觀察誤差值(Training loss)就可以知道這個被拿掉的濾波器所起到的作用了),機器會為比較重要的濾波器賦予較高的權重,我們將這樣一個過程稱為“訓練”。最終,我們將得到的整個帶有權重的神經網路稱為我們通過機器訓練出的“模型”,我們可以拿著這個模型去讓機器學習事物。

這就是機器學習中“訓練模型”的過程,Tensorflow.js就是為我們提供的訓練模型的工具庫,當你真正掌握了模型訓練的奧義之後,Tensorflow對你而言就像JQuery用起來一般簡單。

大家看完這些介紹之後肯定還是一臉茫然,什麼是濾波器?什麼又是卷積計算?不著急,下一個版塊的內容將會為大家揭開所有謎題。

1.3.卷積演算法揭祕

1.3.1.卷積演算法

還記得我們在1.1.1裡說到一張圖片可以用向量的形式表示每個畫素點嘛?卷積計算就是在這基礎上,使用某些運算元對這些畫素點進行處理,而這些運算元,就是我們剛剛提到的濾波器(比如左邊,就是一張經過二值化處理的5*5的圖片,中間的就是我們的濾波器):

前端AI實戰——告訴世界前端也能做AI

那計算的過程又是怎樣的呢?卷積這東西聽起來感覺很複雜,但實際上就是把我們的濾波器套到影像上,乘積求和,然後將影像上位於濾波器中心的值用計算結果替換,大概的效果就是下面這張動圖這樣:

前端AI實戰——告訴世界前端也能做AI

對,所謂高大上的卷積就是這樣一個過程,我們的濾波器每次計算之後就向右移動一個畫素,所以我們可以稱濾波器的步長為1,以此類推。不過我們發現,經過濾波器處理後的影像,好像“變小了”!原來是5*5的圖片這下變成了3*3,這是卷積運算帶來的必然副作用,如果不想讓圖片變小,我們可以為原影像加上一定畫素且值均為0的邊界(padding)去抵消副作用,就像下面這樣:

前端AI實戰——告訴世界前端也能做AI

1.3.2.池化演算法

其實在平時訓練模型的過程中,我們輸入的影像肯定不只有5*5畫素這麼小,我們最經常見到的圖片許多都是100*100畫素以上的,這樣使用我們的機器去計算起來肯定是比較複雜的,因此,我們常常會使用池化演算法進行特徵提取或者影像平滑處理,池化的過程其實就是按照某種規律將圖片等比縮小,過程就像下面這樣:

前端AI實戰——告訴世界前端也能做AI

而池化演算法最常用的有兩大類:取均值演算法和取最大值演算法,顧名思義,取均值演算法就是取濾波器中的平均值作為結果,取最大值演算法就是取濾波器中的最大值作為輸出結果:

前端AI實戰——告訴世界前端也能做AI

上圖就是取最大值演算法的處理過程,大家也能很直觀的看出,在池化層中,濾波器的步長大都是等於濾波器自身大小的(比較方便控制縮放比例)。並且,取最大值演算法肯定是很容易取到濾波器中的特徵點(還記得特徵點嘛?忘記的話快回去1.1.2看看哦~),所以我們可以講取最大值演算法的池化處理稱為特徵提取;同理,取均值演算法因為把所有的畫素點的灰度級都平均了,所以我們可以稱之為平滑處理。

關於卷積神經網路的知識,可以具體參照這篇文章:《卷積神經網路(1)卷積層和池化層學習》。瞭解了這些知識之後,就可以開始我們的實戰啦~

2.影像識別實戰

說了那麼多理論,也不比實操來得有感覺。在大家瞭解了卷積神經網路的基本原理之後,就可以使用我們的工具庫來幫助我們完成相關工作,這裡我推薦ConvNetJS。這款工具庫的本質就是我們在1.2中提到的別人訓練好的模型,我們只需要拿來“學習”即可。

2.1.使用ConvNetJS

我們可以看到在ConvNetJS的README裡有這樣一段官方demo,具體的含義我已經用註釋在程式碼裡標註:

// 定義一個神經網路
var layer_defs = [];
// 輸入層:即是32*32*3的影像
layer_defs.push({type:'input', out_sx:32, out_sy:32, out_depth:3}); 
// 卷積層 
// filter:用16個5*5的濾波器去卷積
// stride:卷積步長為1
// padding:填充寬度為2(為保證輸出的影像大小不會發生變化)
// activation:啟用函式為relu(還有Tanh、Sigmoid等等函式,功能不同)
layer_defs.push({type:'conv', sx:5, filters:16, stride:1, pad:2, activation:'relu'});
// 池化層
// 池化濾波器的大小為2*2
// stride:步長為2
// 在這裡我們無法看出這個框架池化是使用的Avy Pooling還是Max Pooling演算法,先視為後者
layer_defs.push({type:'pool', sx:2, stride:2});
// 反覆卷積和池化減小模型誤差
layer_defs.push({type:'conv', sx:5, filters:20, stride:1, pad:2, activation:'relu'});
layer_defs.push({type:'pool', sx:2, stride:2});
layer_defs.push({type:'conv', sx:5, filters:20, stride:1, pad:2, activation:'relu'});
layer_defs.push({type:'pool', sx:2, stride:2});
// 輸出層
// 分類器:輸出10中不同的類別
layer_defs.push({type:'softmax', num_classes:10});

// 例項化一個神經網路
net = new convnetjs.Net();
net.makeLayers(layer_defs);

// 模型訓練
const trainer = new convnetjs.SGDTrainer(net, { learning_rate: 0.01, momentum: 0.9, batch_size: 5, l2_decay: 0.0 });
trainer.train(imgVol, classIndex);

// 使用訓練好的模型進行影像識別
var x = convnetjs.img_to_vol(document.getElementById('some_image'))
var output_probabilities_vol = net.forward(x)
複製程式碼

如果想要更形象點,上述過程可以用這樣一幅圖表示:

前端AI實戰——告訴世界前端也能做AI

中間的“卷積-池化-卷積-池化……“就是我們定義並訓練的神經網路,我們輸入向量化處理後的影像後,先進行卷積運算,不同的濾波器得到了不同的結果,官方demo裡是使用了16個不同的濾波器(PS:這裡給大家留一個思考的問題,一個3*3的二值化濾波器,能寫出多少種可能?),自然能卷積出16種不同的結果,再拿著這些結果池化處理,不斷重複這個過程,最終得出影像識別結果:

前端AI實戰——告訴世界前端也能做AI

2.2.實戰專案解析

來,我們一起詳細梳理一下使用ConvNetJS這個工具庫完成整個影像識別的具體流程,

(PS:專案程式碼具體參照:github.com/jerryOnlyZR…

首先,我們必須先有資料供我們的模型去學習,至少你該讓這個模型知道啥是啥對吧,在專案裡的 net 資料夾裡的 car.js 檔案,存放的就是我們的學習資料,如果你們感興趣可以開啟看看,裡面的資料就是告訴機器什麼樣的車標對應的是車的什麼品牌。

在我們的專案裡,是通過這樣一段程式碼完成機器學習的:

const trainer = new convnetjs.SGDTrainer(net, { learning_rate: 0.01, momentum: 0.9, batch_size: 5, l2_decay: 0.0 });
let imageList = [];
const loadData = i => {
    return function () {
        return new Promise(function (resolve, reject) {
    		let image = new Image();
		    image.crossOrigin = "anonymous";
 		    image.src = carList[i].url;
		    image.onload = function () {
        		let vol = convnetjs.img_to_vol(image);
                // 逐張訓練圖片
        		trainer.train(vol, i);
       		 	resolve();
    		};
   		  	image.onerror = reject;
		})
    }
}
// 遍歷圖片資源
for (let j = 0; j < carList.length; j++) {
    imageList.push(loadData(j));
}
var testBtn = document.getElementById("test")
function training(){
    testBtn.disabled = true
    return new Promise((resolve, reject) => {
        Promise.all(imageList.map(imageContainer => imageContainer())).then(() => {
    		console.log("模型訓練好了!!!?")
    		testBtn.disabled = false
    		resolve()
		})
    })
}
複製程式碼

我們試著去列印一下影像識別的輸出結果,得到的是這樣一個東西:

前端AI實戰——告訴世界前端也能做AI

從識別結果中我們可以看到,我們得到的是一個陣列,這就是經過分類器分類的10個不同類別,對應的自然是我們的車的品牌,值就是每個類別對應的概率。所以,我們只要拿到概率的最大值,就是預測得出的最傾向的結果。

3.結語

隨著JS引擎的計算能力不斷增強,人工智慧領域的不斷髮展,可以預見的是,在不久的將來,肯定能有一些簡單的演算法可以被移植到使用者前端執行,這樣既能減少請求,又能分擔後端壓力。這一切並不是無稽之談,為什麼tensorflow.js會應運而生,正是因為JS的社群在不斷壯大,JS這款便捷的語言也在得到更為普遍的使用。所以,請對你所從事的這份前端事業,有足夠的信心!

還是那句老話:

技術從來不會受限於語言,受限你的,永遠只是思想。

我並不是什麼演算法工程師,我也不是CS專業出來的科班生,我只是一枚普普通通的前端,和絕大多數人一樣,沒有多深厚的基礎,但我願意去學,我享受克服困難的過程,而那份對人工智慧的執著,只是來源於那份不滿足於現狀的倔性和對這片領域一成不變的初心。

如果您覺得這篇文章對您有幫助,還請麻煩您為文章提供的示例demo專案點個star;如果您對我的其他專案感興趣,也歡迎follow哦~

4.鳴謝

本文專案資源大部分來自京程一燈,感謝京程一燈袁志佳老師對本文以及我個人提供的支援和幫助,如果你也在前端前進路上感到迷茫,京程一燈也許是你不錯的選擇。

相關文章