TensorFlow從1到2(十五)(完結)在瀏覽器作機器學習

TensorFlow的Javascript版

TensorFlow一直努力擴展本身的基礎平臺環境,除了熟悉的Python,當前的TensorFlow還實現了支持Javascript/C++/Java/Go/Swift(預發佈版)共6種語言。
愈來愈多的普通程序員,能夠容易的在本身工做的環境加入機器學習特徵,讓產品更智能。javascript

在Javascript語言方面,TensorFlow又分爲兩個版本。一個是使用node.js支持,用於服務器端開發的@tensorflow/tfjs-node。安裝方法:html

npm install @tensorflow/tfjs-node
# ...GPU版本...
npm install @tensorflow/tfjs-node-gpu

另外一個則是在瀏覽器中就可使用的前端機器學習包@tensorflow/tfjs。安裝方法:前端

npm install @tensorflow/tfjs

前者跟Python的版本同樣,能夠工做在單機、工做站、服務器環境。後者則只須要支持HTML5的瀏覽器就能良好的執行,瀏覽器版本目前還不支持GPU運算。java

瀏覽器機器學習快速入門

瀏覽器版本的TensorFlow是其家族中性能最弱的一個發佈,但極可能也是容易產生最多應用的版本。畢竟無需考慮運行環境,瀏覽即執行,能最大限度上下降對用戶的額外要求。
我以爲未來極可能發展爲在服務器端經過GPU支持完成模型的開發和訓練,而後瀏覽器做爲最方便的客戶端只用來完成預測和反饋給用戶直接的結果。node

不少前端程序員還不喜歡使用node.js和npm幫助管理總體開發。因此咱們直接從網頁入手。並且這種方式,也更容易讓人理解程序完整的運行方式。
首先是基礎的網頁,我在下面給出一個模板。TensorFlow.js的開發,都集中在js程序中,因此這個網頁能夠保存下來。不一樣的項目,只要更換不一樣的js程序就好。程序員

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>TensorFlow.js 練習</title>
  <!-- 引入機器學習庫TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
  <!-- 引入機器學習可視化庫tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>
  <!-- 機器學習主程序(本身編寫) -->
  <script src="script.js"></script>
</head>
<body>
  <!-- 放你的網頁內容 -->
  本頁無正文<p />
</body>
</html>

其實就是一個空白的網頁,分別引入了三個js文件。第一個js是TensorFlow的主要庫,必不可少。第二個是用於TensorFlow可視化圖表顯示的,在正式發佈的程序中根據須要使用。第三個是本身編寫的程序。npm

接着咱們使用《TensorFlow從1到2(七)》中,油耗預測的數據集,也完成一個簡單的油耗預測的示例。
原始的數據結構請到第七篇中查看。這裏爲了js處理的方便,已經預先轉成了json格式。下面是頭兩條記錄的樣子:json

[
  {
    "Name": "chevrolet chevelle malibu",
    "Miles_per_Gallon": 18,
    "Cylinders": 8,
    "Displacement": 307,
    "Horsepower": 130,
    "Weight_in_lbs": 3504,
    "Acceleration": 12,
    "Year": "1970-01-01",
    "Origin": "USA"
  },
  {
    "Name": "buick skylark 320",
    "Miles_per_Gallon": 15,
    "Cylinders": 8,
    "Displacement": 350,
    "Horsepower": 165,
    "Weight_in_lbs": 3693,
    "Acceleration": 11.5,
    "Year": "1970-01-01",
    "Origin": "USA"
  },
    ...

咱們只是想演示TensorFlow.js的使用,因此把問題簡化一下,只保留功率數據(Horsepower)和油耗數據(MPG),MPG這裏同時也是標註信息。
由於咱們作過這個練習,咱們知道樣本中有無效數據。因此數據預處理的時候,還要把數據作一個清洗(固然數據清洗應當養成習慣)。
隨後,瀏覽器不是命令行,不能簡單的在命令行輸出信息。這時候輪到TensorFlow-vis出場了,咱們作一個二維映射把基礎數據顯示在屏幕上。
第一步先不走那麼快,咱們只完成這一部分功能,先執行起來看一看。
下面是完成剛纔所說功能的代碼,別忘了文件名是script.js,跟index.html要放在同一目錄:api

// 獲取數據,只保留感興趣的字段,並進行數據清洗
async function getData() {
  const carsDataReq = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');  
  const carsData = await carsDataReq.json();  
  const cleaned = carsData.map(car => ({
    //只保留兩個字段
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower,
  }))
  //清洗無效數據
  .filter(car => (car.mpg != null && car.horsepower != null));
  
  return cleaned;
}
// 至關於主程序,執行入口
async function run() {
  //載入數據
  const data = await getData();
  //創建繪圖數據
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));
  //使用tfvis繪圖
  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values}, 
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
}
//載入後開始執行run()函數
document.addEventListener('DOMContentLoaded', run);

在支持HTML5的瀏覽器中打開index.html文件就開始了程序執行,由於是本地文件,一般雙擊打開就能夠。程序一開始首先下載樣本數據,視網絡環境不一樣,速度會有區別。執行結束後會自動在瀏覽器的右側彈出圖表窗口顯示咱們繪製的樣本分佈圖。
除了可能的輸入拼寫錯誤,文件下載是最可能出現的問題,若是碰到這種狀況,請根據數據文件的路徑自行下載到本地來進行試驗。

這部分至關於一個Hello World吧。從示例中能夠看出,js在數據處理中,雖然沒有Python的優點,但對於肯定的數據類型也有本身的優勢。在圖表的顯示上更是方便,無需第三方模塊的支持。況且大多數現代瀏覽器也都包括console工具,必要狀況下經過輸出console的調試信息也能夠達到不少目的。
此外有一點須要說明的,是稍微可能耗時的函數,應當儘可能使用異步方式,也就是function關鍵字以前的async。以免阻塞整個程序的執行。
固然使用了異步方式,程序的總體邏輯必定要多思考,想清楚,避免執行過程當中順序混亂。瀏覽器

用js定義模型

TensorFlow.js完整模仿了Keras的模型定義方式,因此若是使用過Keras,那使用TensorFlow.js徹底無壓力。
下面就是本例中的模型定義:

// 創建神經網絡模型
function createModel() {
    // 使用sequential對象創建模型
    const model = tf.sequential(); 
    // 輸入層
    model.add(tf.layers.dense({inputShape: [1], units: 128, useBias: true}));
    // 隱藏層
    model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 25, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 5, activation: 'sigmoid'}));    
    // 輸出層
    model.add(tf.layers.dense({units: 1, useBias: true}));

    return model;
}

代碼中除了沒有了tf.keras的關鍵字其它沒有什麼特殊的東西。你可能也注意到了,定義模型操做自己速度是很快的,並不須要異步執行。
模型定義完成後,可視化工具提供了modelSummary方法,用於將模型顯示在瀏覽器中供用戶檢查。

// 在圖表窗口顯示模型摘要信息
tfvis.show.modelSummary({name: 'Model Summary'}, model);

數據預處理

在數據載入的時候咱們已經進行了一些預處理的工做。這個數據預處理主要是指把js數據轉換爲TensorFlow處理起來更高效的張量類型。此外還須要作數據的規範化。
在這裏有很重要的一點須要說明。js語言在大規模數據的處理上,不如Python的高效。固然這必定程度上是瀏覽器的限制。
其中最突出的問題是內存的垃圾回收,這個問題困擾js已久,相信不作機器學習你也碰到過。而同時,用戶對於瀏覽器的內存佔用自己也是很是敏感的。
TensorFlow.js爲了解決這個問題,專門提供了tf.tidy()函數。使用方法是把大規模的內存操做,放置在這個函數的回調中執行。函數調用完成後,tf.tidy()獲得控制權,進行內存的清理工做,防止內存泄露。
其它沒有什麼須要特殊說明的,能夠看源碼中的註釋:

// 將數據轉換爲張量
function convertToTensor(data) {
  // 數據預處理的過程必然會產生不少中間結果,將佔用大量內存
  // tf.tidy()負責清理這些中間結果,因此要把數據處理包含在這個函數以內
  // 這一點很重要
  return tf.tidy(() => {
    // 把樣本數據亂序排列
    tf.util.shuffle(data);

    // 將數據轉換爲張量,功率值做爲特徵值,油耗值做爲標定目標
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    // 數據規範化,把數據從最小到最大轉換爲0-1浮點空間
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();  
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // 把數據範圍值也要返回,咱們後面繪圖會用到
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    }
  });  
}

完整代碼

程序核心的訓練和測試(預測)的代碼在TensorFlow中很是簡單,咱們早就有經驗了。惟一須要說明的是,除了跟Python中同樣使用model.fit()作訓練,以及model.predict()作預測,咱們的過程和結果,也會使用TensorFLow-vis圖表工具可視化出來,顯示在瀏覽器中。
其中訓練部分,是使用回調函數,這種機制咱們在Python中也見過。目的是可以動態的顯示訓練的過程,而不是所有訓練枯燥、漫長的等待完成才顯示一次。

預測部分的數據少,速度很快,就是執行完成後一次顯示。
但預測部分的數據有大量的轉換過程,這個過程消耗內存大,因此放在tf.tidy()中執行以防止內存泄露。
好了,代碼秀出,請參考註釋閱讀:

// 獲取數據,只保留感興趣的字段,並進行數據清洗
async function getData() {
  const carsDataReq = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');  
  const carsData = await carsDataReq.json();  
  const cleaned = carsData.map(car => ({
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower,
  }))
  .filter(car => (car.mpg != null && car.horsepower != null));
  
  return cleaned;
}
// 至關於主程序,執行入口
async function run() {
  //載入數據
  const data = await getData();
  //創建繪圖數據
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));
  //使用tfvis繪圖
  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values}, 
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
  // 創建神經網絡模型
  const model = createModel();  
  // 在圖表窗口顯示模型摘要信息
  tfvis.show.modelSummary({name: 'Model Summary'}, model);

  // 將數據從js對象轉換爲張量,並完成預處理
  const tensorData = convertToTensor(data);

  // 使用樣本數據訓練模型,訓練時只須要x/y的值
  const {inputs, labels} = tensorData;
  await trainModel(model, inputs, labels);
  // 訓練完成在console輸出完成信息(須要打開瀏覽器console窗口才能看到)
  console.log('Done Training');
  
  // 使用訓練完成的模型進行預測並顯示結果
  testModel(model, data, tensorData);
}

// 載入完成執行主函數run()
document.addEventListener('DOMContentLoaded', run);

// 創建神經網絡模型
function createModel() {
    // 使用sequential對象創建模型
    const model = tf.sequential(); 
    // 輸入層
    model.add(tf.layers.dense({inputShape: [1], units: 128, useBias: true}));
    // 隱藏層
    model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 25, activation: 'sigmoid'}));    
    model.add(tf.layers.dense({units: 5, activation: 'sigmoid'}));    
    // 輸出層
    model.add(tf.layers.dense({units: 1, useBias: true}));

    return model;
}

// 將數據轉換爲張量
function convertToTensor(data) {
  // 數據預處理的過程必然會產生不少中間結果,將佔用大量內存
  // tf.tidy()負責清理這些中間結果,因此要把數據處理包含在這個函數以內
  // 這一點很重要
  return tf.tidy(() => {
    // 把樣本數據亂序排列
    tf.util.shuffle(data);

    // 將數據轉換爲張量,功率值做爲特徵值,油耗值做爲標定目標
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    // 數據規範化,把數據從最小到最大轉換爲0-1浮點空間
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();  
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // 把數據範圍值也要返回,咱們後面繪圖會用到
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    }
  });  
}
async function trainModel(model, inputs, labels) {
  // 編譯模型 
  model.compile({
      optimizer: tf.train.adam(),
      loss: tf.losses.meanSquaredError,
      metrics: ['mse'],
  });
  //每批次數據數量和訓練迭代數量
  const batchSize = 28;
  const epochs = 25;

  // 訓練
  return await model.fit(inputs, labels, {
      batchSize,
      epochs,
      shuffle: true,
      // 使用回調函數繪製訓練過程,曲線指標loss/mse
      callbacks: tfvis.show.fitCallbacks(
        { name: 'Training Performance' },
        ['loss', 'mse'], 
        { height: 200, callbacks: ['onEpochEnd'] }
      )
  });
}

// 測試模型
function testModel(model, inputData, normalizationData) {
  // 獲取數據集的取值範圍
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData;  

  // 防止內存泄露,依然要把大量數據的操做放在tf.tidy值中
  const [xs, preds] = tf.tidy(() => {
      //功率數據直接使用0-1空間,至關於遍歷全部樣本空間
      const xs = tf.linspace(0, 1, 100);
      //批量預測
      const preds = model.predict(xs.reshape([100, 1]));      
      
      // 預測結果也是規範化的0-1值,因此使用數據集取值範圍還原到原始樣本模型
      const unNormXs = xs
        .mul(inputMax.sub(inputMin))
        .add(inputMin);
      const unNormPreds = preds
        .mul(labelMax.sub(labelMin))
        .add(labelMin);
      
      // 返回最終結果
      return [unNormXs.dataSync(), unNormPreds.dataSync()];
  });
  // 準備成繪圖數據
  const predictedPoints = Array.from(xs).map((val, i) => {
      return {x: val, y: preds[i]}
  });
  // 原始的樣本生成散列點同屏顯示
  const originalPoints = inputData.map(d => ({
      x: d.horsepower, y: d.mpg,
  }));

  //繪圖
  tfvis.render.scatterplot(
      {name: 'Model Predictions vs Original Data'}, 
      {values: [originalPoints, predictedPoints], series: ['original', 'predicted']}, 
      {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
      }
  );
}

程序執行,最終預測測試的輸出結果以下:

結語

本連載目標定位讓已經有TensorFlow使用經驗的技術人員,快速上手TensorFlow 2.0開發。
不知不覺,連載15篇。但還有不少內容未能包含進來。好比分佈式訓練、好比圖像內容描述等。建議有須要的朋友繼續到官網文檔中學習。
水平所限,文中錯誤、疏漏很多,歡迎批評指正。

(連載完)

相關文章
相關標籤/搜索