前言

上一篇 初探 Tensorflow 機器學習 中,已完成神經網路的訓練及推理。

本文將使用 Tensorflow.js 實作 Chrome 擴充功能。


關於 Tensorflow.js

TensorFlow.js 是一個開源、WebGL 加速的 JavaScript 機器學習套件。

它將高性能的機器學習模型帶到你的指尖、讓你在瀏覽器上訓練或在推理模式下使用預先訓練好的模型。


基本概念

張量

在 TensorFlow.js 中,資料的核心單位是張量(Tensor):一組形成一維或多維陣列的數值。

一個 Tensor 實體有shape屬性,定義了陣列的形狀(即陣列的每個維度裡有幾個值)。

建構低階張量時,推薦使用以下方法來增加程式碼的閱讀性:

  • tf.scalar
  • tf.tensor1d
  • tf.tensor2d
  • tf.tensor3d
  • tf.tensor4d
const c = tf.tensor2d([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
c.print();
// 輸出: [[1 , 2 , 3 ],
//        [10, 20, 30]]

同時也提供了方便的方法來建立全部值為 0 的張量(tf.zeros)或全部為 1 的張量(tf.ones):

// 3x5 張量,值都為 0
const zeros = tf.zeros([3, 5]);
// 輸出: [[0, 0, 0, 0, 0],
//        [0, 0, 0, 0, 0],
//        [0, 0, 0, 0, 0]]

在 TensorFlow.js 中,張量是不可改變的(immutable),一旦建立,你就不能再改變它的值。

反之,你可以對他們執行操作來產生新的張量。


運算子

張量可以讓你儲存資料,而運算子可以讓你操控資料。

TensorFlow.js 提供了各式各樣適合線性代數和機器學習的運算子,能套用在張量上。

由於張量不可改變,這些運算子不會改變他們的值,而會返回新的張量。

二元運算子像addsubmul

const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const e_plus_f = e.add(f);
e_plus_f.print();
// 輸出: [[6 , 8 ],
//        [10, 12]]

記憶體管理

由於 TensorFlow.js 使用 GPU 來加速數學運算,使用張量和變數時管理 GPU 記憶體是必要的。

TensorFlow.js 提供了兩個方法來幫助這個:disposetidy

在張量或變數上呼叫dispose以清除它,並釋放 GPU 記憶體:

const x = tf.tensor2d([[0.0, 2.0], [4.0, 6.0]]);
const x_squared = x.square();

x.dispose();
x_squared.dispose();

進行大量張量操作時,使用dispose可能變得有點麻煩。

TensorFlow.js 提供了另外一個方法:tidy

它可以在 GPU 端的張量做到類似 JavaScript 中區域(regular scope)的作用:

// tf.tidy 執行一個方法,並在最後清理它。
const average = tf.tidy(() => {
  // tf.tidy 會清理這方法內所有被張量用掉的記憶體,除了需要的張量以外。
  //
  // 即使是像下面的簡單操作,一些中間產物張量也會產生。所以保持簡潔的數學運算是很重要的。
  const y = tf.tensor1d([1.0, 2.0, 3.0, 4.0]);
  const z = tf.ones([4]);

  return y.sub(z).square().mean();
});

average.print() // 輸出: 3.5

使用tidy能避免你的應用程式記憶體洩漏,也可以用來更仔細的控制何時回收記憶體。


模型處理

Python 匯出模型

(此節已有 TF2 版本的更新貼文)

繼上一篇,「訓練」總共載入了 160 張 14x19 的樣本,所以其輸入的shape[160,266]

但是進行「推理」的時候,只有四個數字,故其輸入的shape[4,266]

由於模型shape固定,若使用「訓練」匯出模型來推理,它會要求輸入的shape必須為[160,266]

上一篇已經先從「訓練」中儲存參數後,再於「推理」中重新建構輸入是[4,266]的神經網路了。

所以,直接從載入參數後的「推理」中匯出模型即可。

請注意,placeholder必須用參數定義名稱name="tf_x",輸出也必須用identity定義名稱。

tf_x = tf.placeholder(tf.float32, x.shape, name="tf_x") # 輸入 x,定義名稱

l1 = tf.layers.dense(tf_x, 10, tf.nn.relu) # 隱藏層
output = tf.layers.dense(l1, 10)           # 輸出層

sess = tf.Session() # 建立會話
saver = tf.train.Saver()
saver.restore(sess, "./params") # 讀取載入參數

tf.identity(output, name="output") # 定義名稱
tf.saved_model.simple_save(sess,"./model",
                           inputs={"inputs": tf_x},
                           outputs={"outputs": output}) # 匯出模型

模型轉檔

(此節已有 TF2 版本的更新貼文)

Python 匯出模型saved_model.pb,需要轉換成 TensorFlow.js 可讀取的模型格式,才能用於推理。

pip install tensorflowjs

tensorflowjs_converter \
    --input_format=tf_saved_model \
    --saved_model_tags=serve \
    ./model \
    ./web_model

./modelsaved_model.pb路徑,./web_model為輸出路徑。

輸出完成後將./web_model路徑下的所有檔案放入專案目錄的/public/web_model/


核心 JS 撰寫

創建 Sess 類別

首先,import所需的軟體包,再創建Sess類別,初始化函式為該類別下的load_model()

import "babel-polyfill"
import * as tf from "@tensorflow/tfjs"


class Sess
{
  constructor() { this.load_model() }
}

驗證碼圖像張量化

直接使用fromPixels轉成一個通道(黑白)的張量,再用tidy自動釋放張量記憶體。

class Sess
{
  load_pic()
  {
    return tf.tidy(() =>
    {
      return tf.browser.fromPixels(document.querySelector("#captchaBox > img"), 1)
    })
  }
}

圖像張量分割並標準化

使用stridedSlice分割並用flatten扁平化,tidy自動釋放張量記憶體。

moments會回傳平均值(mean)跟方差(variance)。

但是 Z-score 是用標準差,方差是標準差的平方,所以方差要開根號sqrt

img1.sub(moments.mean).div(tf.sqrt(moments.variance))= ( img1 - 平均值 ) / 標準差。

接著toInt轉成整數,但是當初模型設定輸入為浮點數,所以再toFloat轉成浮點數。

最後,用stack將四個張量合併為一個shape[4,266]的張量。

class Sess
{
  z_score(image)
  {
    return tf.tidy(() =>
    {
      var img1 = image.stridedSlice([7, 7], [26, 21], [1, 1]).flatten()
      var moments = tf.moments(img1)
      img1 = img1.sub(moments.mean).div(tf.sqrt(moments.variance)).toInt().toFloat()


      var img2 = image.stridedSlice([7, 21], [26, 35], [1, 1]).flatten()
      moments = tf.moments(img2)
      img2 = img2.sub(moments.mean).div(tf.sqrt(moments.variance)).toInt().toFloat()


      var img3 = image.stridedSlice([7, 34], [26, 48], [1, 1]).flatten()
      moments = tf.moments(img3)
      img3 = img3.sub(moments.mean).div(tf.sqrt(moments.variance)).toInt().toFloat()


      var img4 = image.stridedSlice([7, 48], [26, 62], [1, 1]).flatten()
      moments = tf.moments(img4)
      img4 = img4.sub(moments.mean).div(tf.sqrt(moments.variance)).toInt().toFloat()

      return tf.stack([img1, img2, img3, img4])
    })
  }
}

推理並輸出預測值

使用predict輸出預測值,tidy自動釋放張量記憶體。

輸出的預測值是四個一維張量,每個張量有 10 個元素,分別代表 0-9 的相似度。

所以用argMax(1)取最大值的索引後,arraySync轉成非張量的一般 JS 陣列。

class Sess
{
  predict(tensor_2d)
  {
    return tf.tidy(() =>
    {
      return this.model.predict(tensor_2d).argMax(1).arraySync()
    })
  }
}

load_model()

(此節已有 TF2 版本的更新貼文)

功能函式都寫好後,接著回來寫一開始的load_model()

首先,使用 Chrome 的getURL函式取得本機/public/web_model/下的模型。

再用loadGraphModel載入模型,接著以全是 0 的張量來初始化(非必要)。

順利載入模型後,將剛才的功能函式一個一個套用進去推理,得到一個 JS 陣列。

使用join將陣列轉成字串,並填入inputTextvalue

class Sess
{
  async load_model()
  {
    console.log("[*]模型載入中")
    const startTime = performance.now()
    const MODEL_URL = chrome.extension.getURL('web_model/model.json')
    try
    {
      this.model = await tf.loadGraphModel(MODEL_URL)
      tf.tidy(() => { this.model.predict(tf.zeros([4, 266])) })
      const totalTime = Math.floor(performance.now() - startTime)
      console.log("[*]模型初始化完成,共耗時 " + totalTime + " ms")
    }
    catch
    {
      console.error("[!]無法從下列網址載入模型: " + MODEL_URL)
      return
    }


    const predict = this.predict(this.z_score(this.load_pic()))
    const string = predict.join("")
    console.log("[*]預測驗證碼為:" + string)
    document.querySelector("#baseContent_cph_confirm_txt").value = string
    console.log("[*]已填入驗證碼")
    console.log("[*]執行完畢")
  }
}

頁面判別

最後,在Sess類別外加入頁面判別功能。

倘若頁面中有 captchaBox 元素,即初始化一個Sess實體。

window.addEventListener("load", function()
{
  if(document.querySelector("#captchaBox > img"))
  {
    console.log("[*]已獲取 captchaBox")
    document.querySelector("#baseContent_cph_confirm_txt").value = "####"
    const sess = new Sess();
  }
})

實作 Chrome 擴充功能

實作的範例檔已經打包好了,下載 NuuIn.zip 並解壓縮,目錄結構如下:

.
├── public(擴充功能主程式)
├── src(核心 JS 檔)
└── package.json

若有編譯需求,可於終端機下指令,利用 yarn 安裝依賴包並編譯:

yarn
yarn build

若無編譯需求,亦可直接將目錄public拖入 chrome://extensions/ 中安裝。