tenflow.js--三種方法用js搭建神經網絡實現曲線擬合

引言

這一段時間研究生生涯已經走進了尾聲,也一直忙于論文沒有關注前端方面的工作。偶然的機會,在知乎上看到了一篇文章前端人工智能?TensorFlow.js 學會游戲通關。我的內心是很激動的,終于,也能在前端直接搭神經網絡,跑分類了。不過該文章,對于tensorflowjs的介紹太少,直觀的游戲ai應用確實很好,但是對于初次接觸的人,還是從最基礎的框架使用開始更好,于是就有了這篇文章。本文拋開了復雜的實際問題,選擇曲線擬合這個最簡單易懂的情景,使用3種逐步深入的方法來完成目標問題的解決。從最基礎的數學方法,到借助底層api構建神經網絡,再到最終借助高層次api構建神經網絡,一步步熟悉框架的使用,希望能夠幫助后來者快速上手。 demo地址(目測手機版后面兩個方法訓練不出來,建議pc版訪問)

目標問題介紹

本文的目標是實現曲線擬合。直觀上,曲線可看成空間質點運動的軌跡,用數學表達式來就是y=f(x),舉個例子,y=x,y=x^2+5......等等,就是曲線。而曲線擬合,用簡單的話來說,就是知道一條曲線的某些點,來預測要 y=f(x) 的表達式是什么。如下圖:


image.png

圖中,藍色點點就是已知曲線中的某些點(本例中數目為100),紅色曲線就是擬合出的結果,也就是本文要實現的曲線擬合。

工具,環境說明

用到的前端相關的庫有:

  1. tensorflow.js,用來搭建神經網絡,訓練等。tensorflow的文檔寫的很好,第一篇講了核心概念,第二篇就講到了如何擬合一條曲線,不過它只使用了線性模型的方法進行擬合,沒有通過神經網絡,這也是本文存在的理由,筆者在看了該文檔之后,一方面,將文檔中提到的方法采用面向對象的方法重構,另一方面,通過學習其api,搭建神經網絡來實現相同的功能。
  2. 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。
知道算法模型之后,原理如下:

  1. 初始化a,b,c,d,取隨機值即可
  2. 根據隨機的參數a,b,c,d按照模型 y = a*x^3 + b*x^2 + c*x + d對100個點進行計算,根據得到的結果,采取一定的手段(原理是偏導,但是這里不需要自己計算,tensorflow會解決這里的調整問題)調整a,b,c,d。使計算出來的y與原來的y差值最小。
  3. 經過多次步驟二,y與原本的y值差值足夠小,就可以認為a,b,c,d就是要求的最終參數。此時,根據該曲線計算出結果并繪制出來,就實現了曲線的擬合。

將上述過程進行抽象,可以得到以下幾個過程:

  1. predict(inputXs),根據已知的樣點,計算該樣點對應的y值
  2. loss(predectedYs,inputYs),計算y與輸入的y的差值
  3. train(inputXs,inputYs),進行一次訓練,通過調整參數來使得loss更小
  4. 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,可以很方便快捷的搭建想要的神經網絡,十分好用。希望能讓后來者少走一些彎路。當然,可能文中也會有些錯誤,如有發現,還請指出,謝謝??。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容