引言
這一段時(shí)間研究生生涯已經(jīng)走進(jìn)了尾聲,也一直忙于論文沒(méi)有關(guān)注前端方面的工作。偶然的機(jī)會(huì),在知乎上看到了一篇文章前端人工智能?TensorFlow.js 學(xué)會(huì)游戲通關(guān)。我的內(nèi)心是很激動(dòng)的,終于,也能在前端直接搭神經(jīng)網(wǎng)絡(luò),跑分類了。不過(guò)該文章,對(duì)于tensorflowjs的介紹太少,直觀的游戲ai應(yīng)用確實(shí)很好,但是對(duì)于初次接觸的人,還是從最基礎(chǔ)的框架使用開始更好,于是就有了這篇文章。本文拋開了復(fù)雜的實(shí)際問(wèn)題,選擇曲線擬合這個(gè)最簡(jiǎn)單易懂的情景,使用3種逐步深入的方法來(lái)完成目標(biāo)問(wèn)題的解決。從最基礎(chǔ)的數(shù)學(xué)方法,到借助底層api構(gòu)建神經(jīng)網(wǎng)絡(luò),再到最終借助高層次api構(gòu)建神經(jīng)網(wǎng)絡(luò),一步步熟悉框架的使用,希望能夠幫助后來(lái)者快速上手。 demo地址(目測(cè)手機(jī)版后面兩個(gè)方法訓(xùn)練不出來(lái),建議pc版訪問(wèn))
目標(biāo)問(wèn)題介紹
本文的目標(biāo)是實(shí)現(xiàn)曲線擬合。直觀上,曲線可看成空間質(zhì)點(diǎn)運(yùn)動(dòng)的軌跡,用數(shù)學(xué)表達(dá)式來(lái)就是y=f(x),舉個(gè)例子,y=x,y=x^2+5......等等,就是曲線。而曲線擬合,用簡(jiǎn)單的話來(lái)說(shuō),就是知道一條曲線的某些點(diǎn),來(lái)預(yù)測(cè)要 y=f(x) 的表達(dá)式是什么。如下圖:

圖中,藍(lán)色點(diǎn)點(diǎn)就是已知曲線中的某些點(diǎn)(本例中數(shù)目為100),紅色曲線就是擬合出的結(jié)果,也就是本文要實(shí)現(xiàn)的曲線擬合。
工具,環(huán)境說(shuō)明
用到的前端相關(guān)的庫(kù)有:
- tensorflow.js,用來(lái)搭建神經(jīng)網(wǎng)絡(luò),訓(xùn)練等。tensorflow的文檔寫的很好,第一篇講了核心概念,第二篇就講到了如何擬合一條曲線,不過(guò)它只使用了線性模型的方法進(jìn)行擬合,沒(méi)有通過(guò)神經(jīng)網(wǎng)絡(luò),這也是本文存在的理由,筆者在看了該文檔之后,一方面,將文檔中提到的方法采用面向?qū)ο蟮姆椒ㄖ貥?gòu),另一方面,通過(guò)學(xué)習(xí)其api,搭建神經(jīng)網(wǎng)絡(luò)來(lái)實(shí)現(xiàn)相同的功能。
- vega-embed,這個(gè)就是用來(lái)繪制曲線和散點(diǎn)的工具,用escharts等可視化工具也行,這里不是關(guān)鍵。
另外由于在代碼中使用了class,async等等es6,es7的用法,故在實(shí)際使用的時(shí)候需要用到babel。本文使用的打包工具是parcel,號(hào)稱是零配置的 Web 應(yīng)用程序打包器,體驗(yàn)了一下,確實(shí)好用。
源碼放在了github上,使用說(shuō)明見README.md。demo更加直觀。
樣本點(diǎn)數(shù)產(chǎn)生方法
在這個(gè)問(wèn)題中,首先需要產(chǎn)生樣點(diǎn)。本文設(shè)定曲線方程為 y = ax^3 + bx^2 + c*x + d,首先隨機(jī)產(chǎn)生100個(gè)x值,再帶入方程計(jì)算y值。最終產(chǎn)生本文的測(cè)試樣點(diǎn),核心代碼如下:其中涉及到一些tensorflow的api,會(huì)穿插在注釋中簡(jiǎn)單介紹。
//導(dǎo)入tensorflow
import * as tf from '@tensorflow/tfjs';
/*
*輸入?yún)?shù):
*num:樣點(diǎn)數(shù)目
*coeff:參數(shù)對(duì)象{a: , b: , c: , d: };
*sigma:樣點(diǎn)偏移原曲線范圍
*輸出參數(shù):
{
x: 樣點(diǎn)橫坐標(biāo)值
y: 樣點(diǎn)縱坐標(biāo)值
}
*/
export function generateData(num, coeff, sigma = 0.04) {
//將代碼用tf.tidy包裹,可以清除執(zhí)行過(guò)程中的tensor變量
return tf.tidy(() => {
//tf.scalar: 產(chǎn)生一個(gè)tensor變量,其值為輸入的參數(shù)
const [a, b, c, d] = [
tf.scalar(coeff.a), tf.scalar(coeff.b), tf.scalar(coeff.c),
tf.scalar(coeff.d)
]
//tf.randomUniform([num],-1,1): 產(chǎn)生-1,1之間的均勻分布的值組成的[num]
//矩陣,此處就是1*100的矩陣, [2,3]表示2*3的矩陣(二維數(shù)組)
const x = tf.randomUniform([num], -1, 1);
//計(jì)算a*x^3 + b*x^2 + c*x + d+(0~sigma之間服從正太分布的隨機(jī)值)
const y = a.mul(x.pow(tf.scalar(3)))
.add(b.mul(x.square()))
.add(c.mul(x))
.add(d)
.add(tf.randomNormal([num], 0, sigma));
//對(duì)輸出值進(jìn)行歸一化
const ymin = y.min();
const ymax = y.max();
const yrange = ymax.sub(ymin);
const yNormalized = y.sub(ymin).div(yrange);
return {
x,
yNormalized
};
})
}
其中,api僅僅介紹了當(dāng)前情景的功能,其還有一些可選參數(shù)沒(méi)有進(jìn)行介紹,具體可以查看官方文檔,基本上看名字能猜出大概,結(jié)合官方文檔不難理解,此處不再過(guò)多介紹。
從上述代碼可以看出,TensorFlow 提供了很好的api,包括服從均勻分布,正態(tài)分布參數(shù)的產(chǎn)生,tensor類型帶來(lái)的矩陣加減乘除運(yùn)算,最大最小值計(jì)算等功能,以及自帶的緩存清理函數(shù)tidy。這些方法構(gòu)成了整個(gè)運(yùn)算的基礎(chǔ),大大方便了使用者。
這一小節(jié)中,通過(guò)上述代碼,產(chǎn)生num個(gè)x,y,構(gòu)成了本文所述的樣本點(diǎn),為后文曲線擬合做好了樣本準(zhǔn)備。后續(xù)將借助TensorFlow 通過(guò)3種不同方法實(shí)現(xiàn)對(duì)該曲線的擬合。
線性模型方法實(shí)現(xiàn)曲線擬合
首先介紹第一種方法,也就是tensorflow文檔第二篇中提到的方法---構(gòu)建線性模型進(jìn)行擬合。這個(gè)方法需要有一個(gè)已知條件,即已經(jīng)知道預(yù)測(cè)的模型為 y = a*x^3 + b*x^2 + c*x + d。
知道算法模型之后,原理如下:
- 初始化a,b,c,d,取隨機(jī)值即可
- 根據(jù)隨機(jī)的參數(shù)a,b,c,d按照模型 y = a*x^3 + b*x^2 + c*x + d對(duì)100個(gè)點(diǎn)進(jìn)行計(jì)算,根據(jù)得到的結(jié)果,采取一定的手段(原理是偏導(dǎo),但是這里不需要自己計(jì)算,tensorflow會(huì)解決這里的調(diào)整問(wèn)題)調(diào)整a,b,c,d。使計(jì)算出來(lái)的y與原來(lái)的y差值最小。
- 經(jīng)過(guò)多次步驟二,y與原本的y值差值足夠小,就可以認(rèn)為a,b,c,d就是要求的最終參數(shù)。此時(shí),根據(jù)該曲線計(jì)算出結(jié)果并繪制出來(lái),就實(shí)現(xiàn)了曲線的擬合。
將上述過(guò)程進(jìn)行抽象,可以得到以下幾個(gè)過(guò)程:
- predict(inputXs),根據(jù)已知的樣點(diǎn),計(jì)算該樣點(diǎn)對(duì)應(yīng)的y值
- loss(predectedYs,inputYs),計(jì)算y與輸入的y的差值
- train(inputXs,inputYs),進(jìn)行一次訓(xùn)練,通過(guò)調(diào)整參數(shù)來(lái)使得loss更小
- fit(inputXs,inputYs,iterations),進(jìn)行曲線擬合,多次調(diào)用train來(lái)完成訓(xùn)練
根據(jù)上述方法,構(gòu)建了一個(gè)簡(jiǎn)單的線性模型類,代碼如下:
import {Model} from './model';
import * as tf from '@tensorflow/tfjs';
function random(){
return (Math.random()-0.5)*2;
}
export class Linear_Model extends Model{
constructor(){
super();
this.init();
}
init(){
this.weights = [];
this.weights[0] = tf.variable(tf.scalar(random()));//對(duì)應(yīng)參數(shù)a
this.weights[1] = tf.variable(tf.scalar(random()));//對(duì)應(yīng)參數(shù)b
this.weights[2] = tf.variable(tf.scalar(random()));//對(duì)應(yīng)參數(shù)c
this.bias = tf.variable(tf.scalar(random()));//對(duì)應(yīng)參數(shù)d
this.learningRate = 0.5;
//設(shè)置優(yōu)化器,自動(dòng)調(diào)整參數(shù)
this.optimizer = tf.train.sgd(0.5);
}
//根據(jù)輸入樣點(diǎn)計(jì)算輸出
predict(inputXs){
return tf.tidy(()=>{
//y = weight[0]*x^3+weight[1]*x^2+weight[2]*x+biases
return this.weights[0].mul(inputXs.pow(tf.scalar(3)))
.add(this.weights[1].mul(inputXs.square()))
.add(this.weights[2].mul(inputXs))
.add(this.bias);
})
}
train(inputXs,inputYs){
//通過(guò)優(yōu)化器的minimize方法來(lái)實(shí)現(xiàn)對(duì)參數(shù)的減少
this.optimizer.minimize(()=>{
//根據(jù)輸入預(yù)測(cè)輸出
const predictedYs = this.predict(inputXs);
//計(jì)算預(yù)測(cè)輸出與原本的輸出差值
return this.loss(predictedYs,inputYs);
})
}
//計(jì)算差值,此處采用均方誤差,就是差值平方再取平均值
loss(predictedYs,inputYs){
return predictedYs.sub(inputYs).square().mean();
}
//多次調(diào)用train來(lái)調(diào)整參數(shù)
fit(inputXs,inputYs,iterationCount = 100){
for(let i = 0;i<iterationCount;i++){
this.train(inputXs,inputYs);
}
}
}
在上述代碼中,用到了tensorflow的tf.train.sgd();方法,這個(gè)方法定義了一個(gè)優(yōu)化器,就是通過(guò)調(diào)整參數(shù)來(lái)實(shí)現(xiàn)loss的不斷降低,sgn是梯度下降法,類似的還有adam等。其優(yōu)化的變量涉及到了inputXs,inputYs,weights(對(duì)應(yīng)之前說(shuō)的a,b,c,d),那么它是如何判斷哪些參數(shù)可以調(diào)整,哪些不能調(diào)整的呢?答案是tf.variable,在優(yōu)化器優(yōu)化的過(guò)程中,只能調(diào)整涉及到的通過(guò)tf.variable定義過(guò)的變量,在這個(gè)例子中,就只有this.weights。當(dāng)執(zhí)行train方法的時(shí)候,優(yōu)化器會(huì)根據(jù)loss的計(jì)算過(guò)程,調(diào)整variable參數(shù),使得loss往小的方向去走(嚴(yán)格來(lái)講,不一定,和學(xué)習(xí)率等很多因素有關(guān),但是這里問(wèn)題比較簡(jiǎn)單,故不討論,感興趣可以看看coursera上吳恩達(dá)的機(jī)器學(xué)習(xí)課程)。經(jīng)過(guò)多次train之后,就可以得到合適的參數(shù),此時(shí)loss只要足夠低,那么使用這些參數(shù)得到的結(jié)果就與愿結(jié)果無(wú)限趨近,可以認(rèn)為實(shí)現(xiàn)了曲線的擬合。
最終調(diào)用代碼如下:
import {
Linear_Model
} from './linear_model';
import {
generateData
} from './data';
import {
plotData,
plotCoeff,
plotDataAndPredictions
} from './ui'
import * as tf from '@tensorflow/tfjs';
async function liner_method() {
//新建線性預(yù)測(cè)模型
const linear_model = new Linear_Model();
const trueCoefficients = {
a: -.8,
b: -.2,
c: .9,
d: .5
};
//調(diào)用數(shù)據(jù)產(chǎn)生函數(shù),產(chǎn)生測(cè)試樣本
const trainingData = generateData(100, trueCoefficients);
//調(diào)用ui層的方法進(jìn)行樣點(diǎn)的繪制,此處ui層不做詳細(xì)介紹
await plotData('#data .plot', trainingData.x, trainingData.yNormalized);
//先做一次預(yù)測(cè),看看初始參數(shù)擬合的曲線形狀
const predictionsBefore = linear_model.predict(trainingData.x);
//繪制樣點(diǎn)和曲線
await plotDataAndPredictions('#random .plot', trainingData.x, trainingData.yNormalized, predictionsBefore);
//調(diào)用fit方法進(jìn)行訓(xùn)練
linear_model.fit(trainingData.x, trainingData.yNormalized);
//再次計(jì)算曲線,此時(shí)參數(shù)已經(jīng)經(jīng)過(guò)訓(xùn)練
const predictionsAfter = linear_model.predict(trainingData.x);
//繪制曲線
await plotDataAndPredictions('#trained .plot', trainingData.x, trainingData.yNormalized, predictionsAfter);
}
上述代碼就是對(duì)本文定義的Linear_Model的一個(gè)使用方法,最終完成了曲線擬合這個(gè)目標(biāo)。
這種方法的訓(xùn)練速度快,但是缺點(diǎn)在于需要事先知道模型形狀(y = ax^3 + bx^2 + c*x + d),不然不好進(jìn)行預(yù)測(cè)。到這里,其實(shí)還沒(méi)有涉及到神經(jīng)網(wǎng)絡(luò)的使用,但是所謂神經(jīng)網(wǎng)絡(luò)本質(zhì)上也是參數(shù)的不斷調(diào)整,只是更加復(fù)雜一些。加下來(lái)將使用底層api構(gòu)建一個(gè)包含一層隱含層的神經(jīng)網(wǎng)絡(luò)來(lái)解決這個(gè)問(wèn)題。不需要事先知道模型形狀也能完成曲線的擬合。
底層api構(gòu)建神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)曲線擬合
神經(jīng)網(wǎng)絡(luò)的原理這里就不介紹了,感興趣可以看coursera上吳恩達(dá)或者ufldl的介紹,都比較詳細(xì)。
在這個(gè)問(wèn)題中,由于是線性函數(shù),所以擬合起來(lái)并不困難,這里采取1-6-1的結(jié)構(gòu),第一個(gè)1是輸入層,這里是一維,所以輸入層為1,隱含層選擇6,這里其實(shí)5,4,7都可以,只是需要訓(xùn)練的次數(shù)不同而已。最后一層輸出層為1,因?yàn)檩敵鼍褪莥值,也是一維的。
按照上一節(jié)抽象出來(lái)的過(guò)程,也需要手動(dòng)實(shí)現(xiàn)predict,loss等函數(shù)。神經(jīng)網(wǎng)絡(luò),與上述不同的地方就在與參數(shù)的構(gòu)建,predict計(jì)算方式不同,其它地方其實(shí)基本一樣。代碼如下:
import {
Model
} from './model';
import * as tf from '@tensorflow/tfjs';
export default class NNModel extends Model {
constructor({
inputSize = 3,
hiddenLayerSize = inputSize * 2,
outputSize = 2,
learningRate = 0.1
} = {}) {
super();
//定義隱藏層,輸入層,輸出層,優(yōu)化器函數(shù)
this.hiddenLayerSize = hiddenLayerSize;
this.inputSize = inputSize;
this.outputSize = outputSize;
this.optimizer = tf.train.adam(learningRate);
this.init();
}
//初始化神經(jīng)網(wǎng)絡(luò)參數(shù)
init() {
this.weights = [];
this.biases = [];
//第一層參數(shù)為1*6的矩陣
this.weights[0] = tf.variable(
tf.randomNormal([this.inputSize, this.hiddenLayerSize])
);
//第一層偏置
this.biases[0] = tf.variable(tf.scalar(Math.random()));
// Output layer
//第二層參數(shù)為6*1的矩陣
this.weights[1] = tf.variable(
tf.randomNormal([this.hiddenLayerSize, this.outputSize])
);
this.biases[1] = tf.variable(tf.scalar(Math.random()));
}
//預(yù)測(cè)函數(shù),激活函數(shù)選擇sigmoid,matMux表示矩陣乘法
predict(inputXs) {
const x = tensor(inputXs);
return tf.tidy(()=>{
const hiddenLayer = tf.sigmoid(x.matMul(this.weights[0]).add(this.biases[0]));
const outputLayer = tf.sigmoid(hiddenLayer.matMul(this.weights[1]).add(this.biases[1]));
return outputLayer;
})
}
train(inputXs,inputYs){
this.optimizer.minimize(()=>{
const predictedYs = this.predict(inputXs);
return this.loss(predictedYs,inputYs);
})
}
loss(predictedYs, inputYs) {
const meanSquareError = predictedYs
.sub(tensor(inputYs))
.square()
.mean();
return meanSquareError;
}
}
上述代碼中,涉及到了sigmoid函數(shù),也就是神經(jīng)網(wǎng)絡(luò)的激活函數(shù),很基礎(chǔ)的概念,不多介紹。另外一個(gè)就是matMux,相當(dāng)于矩陣乘法。
在實(shí)際使用時(shí),方法和線性模型幾乎一致,此處不貼代碼,最終需要進(jìn)行500多次的訓(xùn)練,才能達(dá)到和上述線性模型同樣的效果。但是在不知道模型的情況下,還能擬合該曲線,這就是神經(jīng)網(wǎng)絡(luò)方法最大的優(yōu)勢(shì)。不需要人為構(gòu)建模型,也能解決問(wèn)題。
但是上述的寫法存在一個(gè)問(wèn)題,就是現(xiàn)在是1個(gè)隱含層,計(jì)算可以通過(guò)predict中兩三行的代碼搞定,但是層數(shù)多了之后,手動(dòng)的一次次編寫中間代碼,實(shí)在也是一個(gè)體力活,而且容易出錯(cuò)。為了解決這個(gè)問(wèn)題,tensorflow提供了一種更高層次的構(gòu)建方法,就是下一節(jié)要介紹的方法。
高層次api構(gòu)建神經(jīng)網(wǎng)絡(luò)
在tensorflow中,有一個(gè)高層次的api,tf.sequential(),其用法直接通過(guò)實(shí)例來(lái)解釋:
const model = tf.sequential();
model.add(tf.layers.dense({
units: 6,
inputShape: [1],
activation:'sigmoid'
}));
model.add(tf.layers.dense({
units:1,
activation:'sigmoid'
}));
model.compile({
optimizer:tf.train.adam(0.1),
loss:'meanSquaredError'
})
通過(guò)上述的代碼,就構(gòu)建了一個(gè)神經(jīng)網(wǎng)絡(luò),該網(wǎng)絡(luò)有3層,一個(gè)是輸入層,1維,隱藏層,6維,最后輸出層,1維。上述代碼中,model.add了兩次,這是因?yàn)檩斎雽悠鋵?shí)就是輸入樣本,不需要計(jì)算,所以不需要添加,只需要在后續(xù)層添加的時(shí)候指定inputShape即可。其中,activation就是激活函數(shù),這里直接選擇signoid,而compile,就是完成模型的構(gòu)建,需要指定優(yōu)化器和loss計(jì)算方法(可以用字符串也可以傳入一個(gè)自定義計(jì)算的函數(shù))。此時(shí),就完成了一個(gè)神經(jīng)網(wǎng)絡(luò)的搭建。用法如下:
//預(yù)測(cè)樣本對(duì)應(yīng)的值
const predictionsBefore = model.predict(trainingData_nn.x);
//繪制結(jié)果
await plotDataAndPredictions('#random3 .plot', trainingData.x, trainingData.yNormalized, predictionsBefore);
//進(jìn)行訓(xùn)練
const h = await model.fit(trainingData_nn.x,trainingData_nn.yNormalized,{
epochs:200,
batchSize:100
})
//訓(xùn)練結(jié)束后再次計(jì)算曲線y值
const predictionsAfter = model.predict(trainingData_nn.x);
//繪制結(jié)果
await plotDataAndPredictions('#trained3 .plot', trainingData.x, trainingData.yNormalized, predictionsAfter);
可以看出,tensorflow提供的model,可以直接使用fit,predict,同時(shí)不需要手動(dòng)指定weights,可以說(shuō)是很方便了。
小結(jié)
本文算是對(duì)官方文檔的一個(gè)深入,選擇最簡(jiǎn)單的曲線擬合問(wèn)題入手,從最簡(jiǎn)單的線性模型到手動(dòng)搭建神經(jīng)網(wǎng)絡(luò),再到利用高層api來(lái)搭建神經(jīng)網(wǎng)絡(luò),解決了曲線擬合的問(wèn)題。
總體來(lái)說(shuō),最好的搭建姿勢(shì)還是借助高層api,可以很方便快捷的搭建想要的神經(jīng)網(wǎng)絡(luò),十分好用。希望能讓后來(lái)者少走一些彎路。當(dāng)然,可能文中也會(huì)有些錯(cuò)誤,如有發(fā)現(xiàn),還請(qǐng)指出,謝謝??。