tenflow.js--三種方法用js搭建神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)曲線擬合

引言

這一段時(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á)式是什么。如下圖:


image.png

圖中,藍(lán)色點(diǎn)點(diǎn)就是已知曲線中的某些點(diǎn)(本例中數(shù)目為100),紅色曲線就是擬合出的結(jié)果,也就是本文要實(shí)現(xiàn)的曲線擬合。

工具,環(huán)境說(shuō)明

用到的前端相關(guān)的庫(kù)有:

  1. 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)相同的功能。
  2. 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。
知道算法模型之后,原理如下:

  1. 初始化a,b,c,d,取隨機(jī)值即可
  2. 根據(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差值最小。
  3. 經(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ò)程:

  1. predict(inputXs),根據(jù)已知的樣點(diǎn),計(jì)算該樣點(diǎn)對(duì)應(yīng)的y值
  2. loss(predectedYs,inputYs),計(jì)算y與輸入的y的差值
  3. train(inputXs,inputYs),進(jìn)行一次訓(xùn)練,通過(guò)調(diào)整參數(shù)來(lái)使得loss更小
  4. 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)指出,謝謝??。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容