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