Tf.js 實作 Chrome 擴充功能
前言
上一篇 初探 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 提供了各式各樣適合線性代數和機器學習的運算子,能套用在張量上。
由於張量不可改變,這些運算子不會改變他們的值,而會返回新的張量。
二元運算子像add
、sub
、mul
:
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 提供了兩個方法來幫助這個:dispose
和tidy
。
在張量或變數上呼叫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 匯出模型
繼上一篇,「訓練」總共載入了 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}) # 匯出模型
模型轉檔
Python 匯出模型saved_model.pb
,需要轉換成 TensorFlow.js 可讀取的模型格式,才能用於推理。
pip install tensorflowjs
tensorflowjs_converter \
--input_format=tf_saved_model \
--saved_model_tags=serve \
./model \
./web_model
./model
為saved_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()
功能函式都寫好後,接著回來寫一開始的load_model()
。
首先,使用 Chrome 的getURL
函式取得本機/public/web_model/
下的模型。
再用loadGraphModel
載入模型,接著以全是 0 的張量來初始化(非必要)。
順利載入模型後,將剛才的功能函式一個一個套用進去推理,得到一個 JS 陣列。
使用join
將陣列轉成字串,並填入inputText
的value
。
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/ 中安裝。