TensorFlow.js 是一個用于機器學習的開源 JavaScript 庫。 它由 Google 開發(fā)耳奕,是 Python 中 TensorFlow 的配套庫绑青。
此工具使您能夠構(gòu)建可在瀏覽器或 Node.js 中運行的機器學習應用程序。
這樣屋群,用戶無需安裝任何軟件或驅(qū)動程序闸婴。 只需打開網(wǎng)頁即可與程序進行交互。
2.1 TensorFlow.js 基礎
TensorFlow.js 由 WebGL 提供支持芍躏,并提供用于定義模型的高級層 API 和用于線性代數(shù)的低級 API(以前是 deeplearn.js)邪乍。
它還支持導入從 TensorFlow 和 Keras 保存的模型。
TensorFlow 的核心是張量对竣。 張量是一種數(shù)據(jù)單位庇楞,是一組形成一個或多個維度的數(shù)組的值。 它類似于多維數(shù)組否纬。
2.1.1 創(chuàng)建張量
例如吕晌,您可以想象以下示例二維數(shù)組。
const data = [
[0.456, 0.378, 0.215],
[0.876, 0.938, 0.276],
[0.629, 0.287, 0.518]
];
將其轉(zhuǎn)換為張量以便與 TensorFlow.js 一起使用的方法是使用內(nèi)置方法 tf.tensor 包裝它临燃。
const data = [
[0.456, 0.378, 0.215],
[0.876, 0.938, 0.276],
[0.629, 0.287, 0.518]
];
const dataTensor = tf.tensor(data);
現(xiàn)在睛驳,變量 dataTensor 可以與其他 TensorFlow 方法一起使用來訓練模型、生成預測等膜廊。
一些額外的屬性可以在 tf.tensor 中訪問乏沸,例如 rank、shape 和 dtype爪瓜。
rank:表示張量包含多少維
shape:定義數(shù)據(jù)每個維度的大小
dtype:定義張量的數(shù)據(jù)類型
const tensor = tf.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
console.log("shape: ", tensor.shape); // [3,3]
console.log("rank: ", tensor.rank); // 2
console.log("dtype: ", tensor.dtype); // float32
在所示的示例張量中蹬跃,形狀返回 [3, 3],因為我們可以看到 3 個包含 3 個值的數(shù)組钥勋。
當我們使用二維數(shù)組時炬转,rank 屬性會打印 2。 如果我們向數(shù)組中添加了另一個維度算灸,則排名將為 3扼劈。
最后,dtype 是 float32菲驴,因為這是默認數(shù)據(jù)類型荐吵。
還可以使用其他數(shù)據(jù)類型(如 bool、int32赊瞬、complex64 和 string dtypes)創(chuàng)建張量先煎。 為此,我們需要將形狀作為第二個參數(shù)傳遞巧涧,將 dtype 作為第三個參數(shù)傳遞給 tf.tensor薯蝎。
const tensor = tf.tensor([[1, 2], [4, 5]], [2,2], "int32");
console.log("shape: ", tensor.shape); // [2,2]
console.log("rank: ", tensor.rank); // 2
console.log("dtype: ", tensor.dtype); // int32
在到目前為止顯示的示例代碼中,我們使用 tf.tensor 創(chuàng)建張量谤绳; 但是占锯,可以使用更多方法來創(chuàng)建具有不同維度的它們袒哥。
根據(jù)您正在處理的數(shù)據(jù),您可以使用從 tf.tensor1d 到 tf.tensor6d 的方法來創(chuàng)建最多六維的張量消略。
如果你要轉(zhuǎn)換的數(shù)據(jù)是一個六層的多維數(shù)組堡称,你可以同時使用 tf.tensor 和 tf.tensor6d; 但是艺演,使用 tf.tensor6d 會使代碼更具可讀性却紧,因為您可以自動知道維度的數(shù)量。
const tensor = tf.tensor6d([
[
[
[
[[1], [2]],
[[3], [4]]
],
[
[[5], [6]],
[[7], [8]]
]
]
]
]);
// Is the same thing as
const sameTensor = tf.tensor([
[
[
[
[[1], [2]],
[[3], [4]]
],
[
[[5], [6]],
[[7], [8]]
]
]
]
]);
創(chuàng)建張量時胎撤,您還可以傳入一個平面數(shù)組并指示您希望張量具有的形狀晓殊。
const tensor = tf.tensor2d([
[1, 2, 3],
[4, 5, 6]
]);
// is the same thing as
const sameTensor = tf.tensor([1, 2, 3, 4, 5, 6], [2, 3]);
一旦張量被實例化,就可以使用 reshape 方法改變它的形狀哩照。
2.1.2 訪問張量中的數(shù)據(jù)
創(chuàng)建張量后挺物,您可以使用 tf.array() 或 tf.data() 獲取其值。
tf.array() 返回值的多維數(shù)組飘弧,而 tf.data() 返回扁平化的數(shù)據(jù)。
const tensor = tf.tensor2d([[1, 2, 3], [4, 5, 6]]);
const array = tensor.array().then(values => console.log("array: ": values));
// array: [ [1, 2, 3], [4, 5, 6] ]
const data = tensor.data().then(values => console.log("data: ", values));
// data: Float32Array [1, 2, 3, 4, 5, 6];
正如您在前面的示例中看到的砚著,這兩個方法返回一個承諾次伶。
在 JavaScript 中,promise 是在調(diào)用 promise 時尚未創(chuàng)建的值的代理稽穆。 Promise 表示尚未完成的操作冠王,因此它們與異步操作一起使用,以在操作完成后的某個時間點提供值舌镶。
但是柱彻,還使用 arraySync() 和 dataSync() 提供了同步版本。
const tensor = tf.tensor2d([[1, 2, 3], [4, 5, 6]]);
const values = tensor.arraySync();
console.log("values: ": values); // values: [ [1, 2, 3], [4, 5, 6] ]
const data = tensor.dataSync()
console.log("data: ", values); // data: Float32Array [1, 2, 3, 4, 5, 6];
不建議在生產(chǎn)應用程序中使用它們餐胀,因為它們會導致性能問題哟楷。
2.1.3 張量運算
在上一節(jié)中,我們了解到張量是一種數(shù)據(jù)結(jié)構(gòu)否灾,允許我們以 TensorFlow.js 可以使用的方式存儲數(shù)據(jù)卖擅。 我們看到了如何創(chuàng)造它們、塑造它們以及獲取它們的價值墨技。
現(xiàn)在鸠真,讓我們研究一些允許我們操縱它們的不同操作幔崖。
這些操作可以組織成類別。 其中一些允許您對張量進行算術(shù)運算,例如千贯,將多個張量相加,其他操作專注于執(zhí)行邏輯運算窖梁,例如評估張量是否大于另一個,而其他操作則提供了一種進行基本數(shù)學運算的方法统刮,例如計算 張量中所有元素的平方。
完整的操作列表可在 https://js.tensorflow.org/api/latest/#Operations 獲得账千。
以下是如何使用這些操作的示例侥蒙。
const tensorA = tf.tensor([1, 2, 3, 4]);
const tensorB = tf.tensor([5, 6, 7, 8]);
const tensor = tf.add(tensorA, tensorB); // [6, 8, 10, 12]
// or
// const tensor = tensorA.add(tensorB);
在這個例子中,我們將兩個張量相加匀奏。 如果您查看 tensorA 的第一個值鞭衩,即 1,以及 tensorB 的第一個值娃善,即 5论衍,那么 1 + 5 的結(jié)果是數(shù)字 6,即我們最終張量的第一個值聚磺。
為了能夠使用這種操作坯台,您的張量必須具有相同的形狀,但不一定具有相同的等級瘫寝。
如果您還記得前幾頁蜒蕾,形狀是張量每個維度中值的數(shù)量,而秩是維度數(shù)量焕阿。
讓我們用另一個例子來說明這一點咪啡。
const tensorA = tf.tensor2d([[1, 2, 3, 4]]);
const tensorB = tf.tensor([5, 6, 7, 8]);
const tensor = tf.add(tensorA, tensorB); // [[6, 8, 10, 12],]
在這種情況下,tensorA 現(xiàn)在是一個 2D 張量暮屡,但 tensorB 仍然是一維的撤摸。
將兩者相加的結(jié)果現(xiàn)在是一個張量,其值與以前相同褒纲,但維數(shù)不同准夷。
但是,如果我們嘗試添加多個不同形狀的張量莺掠,則會導致錯誤衫嵌。
const tensorA = tf.tensor([1, 2, 3, 4]);
const tensorB = tf.tensor([5, 6, 7]);
const tensor = tf.add(tensorA, tensorB);
// Error: Operands could not be broadcast together with shapes 3 and 4.
這個錯誤告訴我們的是,這個操作數(shù)不能與這兩個張量一起使用汁蝶,因為其中一個有四個元素渐扮,另一個只有三個。
張量是不可變的掖棉,因此這些操作不會改變原始張量墓律,而是始終返回一個新的 tf.Tensor。
2.1.4 內(nèi)存
最后幔亥,在使用張量時耻讽,您需要使用 dispose() 或 tf.dispose() 顯式清除內(nèi)存。
const tensor = tf.tensor([1, 2, 3, 4]);
tensor.dispose();
// or
tf.dispose(tensor);
另一種管理內(nèi)存的方法是在鏈接操作時使用 tf.tidy() 帕棉。
由于張量是不可變的针肥,因此每次操作的結(jié)果都是一個新的張量饼记。 為了避免對您生成的所有張量調(diào)用 dispose,使用 tf.tidy() 允許您只保留所有操作生成的最后一個并處理所有其他張量慰枕。
const tensorA = tf.tensor([1, 2, 3, 4]);
const tensor = tf.tidy(() => {
return tensorA.square().neg();
});
console.log(tensor.dataSync()); // [-1, -4, -9, -16]
在這個例子中具则,square() 的結(jié)果將被處理,而 neg() 的結(jié)果不會因為它返回函數(shù)的值具帮。
現(xiàn)在我們已經(jīng)介紹了 TensorFlow.js 的核心內(nèi)容以及如何使用張量博肋,讓我們看看該庫提供的不同功能,以更好地了解可能的情況蜂厅。
2.2 特點
在本子章節(jié)中匪凡,我們將探索 TensorFlow.js 當前可用的三個主要功能。這包括使用預先訓練的模型掘猿;進行遷移學習病游,這意味著使用自定義輸入數(shù)據(jù)重新訓練模型;并在 JavaScript 中完成所有工作稠通,即創(chuàng)建模型衬衬、訓練模型和運行預測,所有這些都在瀏覽器中完成采记。
我們將涵蓋從最簡單到最復雜的這些功能佣耐。
2.2.1 使用預訓練模型
在本書的第一章中,我們將術(shù)語“模型”定義為一個數(shù)學函數(shù)唧龄,它可以采用新的參數(shù),根據(jù)訓練過的數(shù)據(jù)進行預測奸远。
如果這個定義對你來說仍然有點混亂既棺,希望在談論第一個特性時把它放在上下文中會讓它更清楚一些。
在機器學習中懒叛,為了能夠預測結(jié)果丸冕,我們需要一個模型。但是薛窥,沒有必要自己構(gòu)建模型胖烛。使用所謂的“預訓練模型”完全沒問題。
術(shù)語“預訓練”意味著該模型已經(jīng)用某種類型的輸入數(shù)據(jù)進行了訓練诅迷,并且是為特定目的而開發(fā)的佩番。
例如,您可以找到一些專注于對象檢測和識別的開源預訓練模型罢杉。這些模型已經(jīng)接受了數(shù)百萬個對象的圖像趟畏,已經(jīng)完成了所有的訓練過程,現(xiàn)在在預測新實體時應該具有令人滿意的準確度滩租。
創(chuàng)建這些模型的公司或機構(gòu)將它們開源赋秀,因此開發(fā)人員可以在他們的應用程序中使用它們利朵,并有機會更快地構(gòu)建機器學習項目。
可以想象猎莲,收集數(shù)據(jù)绍弟、格式化、標記著洼、試驗不同算法和參數(shù)的過程可能需要大量時間樟遣,因此能夠使用預訓練模型替代這項工作可以節(jié)省大量時間專注于構(gòu)建應用程序。
當前可用于 TensorFlow.js 的預訓練模型包括身體分割郭脂、姿勢估計年碘、對象檢測、圖像分類展鸡、語音命令識別和情感分析屿衅。
在您的應用程序中使用預先訓練的模型相對容易。
在以下代碼示例中莹弊,我們將使用 mobilenet 對象檢測模型來預測新圖像中的實體涤久。
const img = document.getElementById("img");
const model = await mobilenet.load();
const predictions = await model.classify(img);
return predictions;
在實際應用中,此代碼需要事先需要 TensorFlow.js 庫和 mobilenet 預訓練模型忍弛,但在我們深入構(gòu)建實際項目時响迂,將在接下來的幾章中展示更完整的代碼示例。
前面的示例首先獲取應包含我們想要預測的圖像的 HTML 元素细疚。 下一步是異步加載 mobilenet 模型蔗彤。
模型的大小可能相當大,有時有幾兆字節(jié)疯兼,因此需要使用 async/await 加載它們然遏,以確保在您運行預測時完全完成此操作。
模型準備就緒后吧彪,您可以調(diào)用該模型的分類()方法待侵,在該方法中傳遞您的 HTML 元素,該方法將返回一個預測數(shù)組姨裸。
在您將使用貓圖像的示例中秧倾,預測的輸出看起來與此類似。
使用classify() 的結(jié)果始終是一個包含三個對象的數(shù)組傀缩,其中包含兩個鍵:className 和probability那先。
className 是一個包含標簽或類的字符串,模型根據(jù)之前訓練過的數(shù)據(jù)對新輸入進行了分類扑毡。
概率是一個介于 0 和 1 之間的浮點值胃榕,表示輸入數(shù)據(jù)屬于 className 的可能性,0 表示不太可能,1 表示非逞郑可能苦掘。
它們按降序排列,因此數(shù)組中的第一個對象是最有可能為真的預測楔壤。
在之前的輸出中鹤啡,模型以 70% 的可能性預測圖像包含“老虎貓”。
其余預測的概率值大幅下降蹲嚣,其中包含“虎斑貓”的概率為 21%递瑰,包含“領結(jié)”的概率約為 0.02%。
通常隙畜,您會關(guān)注預測中返回的第一個值抖部,因為它的概率最高;然而议惰,70%實際上并沒有那么高慎颗。
在機器學習中,您的目標是在使用預測時獲得盡可能高的概率言询。在這種情況下俯萎,我們只預測了圖像中貓的存在,但在實際應用中运杭,您可以想象 30% 的預測錯誤輸出的可能性是不可接受的夫啊。
為了改善這一點,一般來說辆憔,我們會做所謂的“超參數(shù)調(diào)整”并重新訓練模型撇眯。
超參數(shù)調(diào)整是調(diào)整和優(yōu)化生成模型時使用的參數(shù)的過程。它可能是在神經(jīng)網(wǎng)絡中添加層虱咧、更改批量大小等叛本,并查看這些更改對模型性能和準確性的影響。
但是彤钟,在使用預訓練模型時,您將無法執(zhí)行此操作跷叉,因為您唯一可以訪問的是輸出模型逸雹,而不是為創(chuàng)建它而編寫的代碼。
這是使用預訓練模型的限制之一云挟。
使用這些模型時梆砸,您無法控制它們的創(chuàng)建方式和修改方式。您通常無法訪問訓練過程中使用的數(shù)據(jù)集园欣,因此您無法確定它是否滿足您的應用程序的要求帖世。
此外,您還冒著繼承公司或機構(gòu)偏見的風險沸枯。
如果您的應用程序涉及實施面部識別日矫,并且您決定使用開源預訓練模型赂弓,則無法確定該模型是在不同的人數(shù)據(jù)集上訓練的。因此哪轿,您可能會在不知不覺中通過使用某些偏見來支持它們盈魁。
過去曾出現(xiàn)過面部識別模型僅在白人身上表現(xiàn)良好的問題,而留下了大量膚色較深的用戶窃诉。
盡管已經(jīng)做了一些工作來解決這個問題杨耙,但我們經(jīng)常聽到機器學習模型做出有偏見的預測,因為用于訓練它們的數(shù)據(jù)不夠多樣化飘痛。
如果您決定在生產(chǎn)應用程序中使用預先訓練的模型珊膜,我認為事先做一些研究很重要。
2.2.2 遷移學習
TensorFlow.js 中可用的第二個功能稱為“遷移學習”宣脉。
遷移學習是重用為任務開發(fā)的模型的能力车柠,作為第二個任務的模型的起點。
如果您想象一個對象識別模型已經(jīng)在您無法訪問的數(shù)據(jù)集上進行了預訓練脖旱,那么該模型的核心功能是識別圖像中的實體堪遂。使用遷移學習,您可以利用此模型創(chuàng)建一個新模型萌庆,該模型的功能相同溶褪,但使用您的自定義輸入數(shù)據(jù)進行訓練。
遷移學習是一種生成半定制模型的方法践险。您仍然無法修改模型本身猿妈,但您可以將自己的數(shù)據(jù)提供給它,這可以提高預測的準確性巍虫。
如果我們重用上一節(jié)中使用預訓練模型檢測圖片中是否存在貓的示例彭则,我們可以看到預測返回時帶有“老虎貓”標簽。這意味著模型是用標記為這樣的圖像進行訓練的占遥,但是如果我們想要檢測非常不同的東西俯抖,比如金荊樹(澳大利亞花)怎么辦?
第一步是搜索模型可以預測的類別列表瓦胎,看看它是否包含這些花芬萍。如果是,則表示該模型可以直接使用搔啊,就像上一節(jié)所示柬祠。
但是,如果它沒有使用 Golden Wattles 的圖像進行訓練负芋,那么在我們使用遷移學習生成新模型之前漫蛔,它將無法檢測到它們。
為此,部分代碼類似于上一節(jié)中顯示的示例莽龟,因為我們?nèi)匀恍枰獜念A訓練的模型開始蠕嫁,但我們引入了一些新邏輯。
我們需要首先將 K 近鄰分類器與 TensorFlow.js 和 mobilenet 預訓練模型一起導入到我們的應用程序中轧房。
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@1.0.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/knn-classifier"></script>
這樣做使我們可以訪問 knnClassifier 對象拌阴。
要實例化它,我們需要調(diào)用 create 方法奶镶。
const classifier = knnClassifier.create();
該分類器將用于使我們能夠根據(jù)自定義輸入數(shù)據(jù)進行預測迟赃,而不僅僅是使用預訓練模型。
此過程中的主要步驟涉及對模型進行所謂的推理厂镇,這意味著將 mobilenet 模型應用于新數(shù)據(jù)纤壁,將這些示例添加到分類器,并預測類別捺信。
const img = await webcam.capture();
const activation = model.infer(img, 'conv_preds');
classifier.addExample(activation, classId);
前面的代碼示例不完整酌媒,但當我們專注于在應用程序中實現(xiàn)遷移學習時,我們將在接下來的章節(jié)中更深入地介紹它迄靠。
這里最重要的是要了解我們將網(wǎng)絡攝像頭饋送中的圖像保存在一個變量中秒咨,將其用作模型上的新數(shù)據(jù),并將其作為帶有類(標簽)的示例添加到分類器中掌挚,因此最終結(jié)果是 一種模型雨席,它不僅能夠識別與 mobilenet 模型初始訓練過程中使用的數(shù)據(jù)相似的數(shù)據(jù),還能夠識別我們的新樣本吠式。
向分類器提供單個新圖像和示例還不足以使其能夠準確識別我們的新輸入數(shù)據(jù)陡厘; 因此,此步驟必須重復多次特占。
一旦您認為您的分類器已準備就緒糙置,您就可以像這樣預測輸入。
const img = await webcam.capture();
const activation = model.infer(img, 'conv_preds');
const result = await classifier.predictClass(activation);
const classes = ["A", "B", "C"];
const prediction = classes[result.label];
第一步是相同的是目,但我們沒有將示例添加到分類器中谤饭,而是使用 predictClass 方法返回它認為新輸入的結(jié)果。
我們將在下一章更深入地討論遷移學習懊纳。
2.2.3 創(chuàng)建网持、訓練和預測
最后,第三個功能允許您自己創(chuàng)建模型长踊、運行訓練過程并使用它,所有這些都在 JavaScript 中萍倡。
此功能比前兩個功能更復雜身弊,但將在第 5 章中更深入地介紹,當我們使用我們自己創(chuàng)建的模型構(gòu)建應用程序時。
重要的是要知道自己創(chuàng)建模型需要反復試驗的方法阱佛。
沒有單一的方法可以解決問題帖汞,如果您決定沿著這條路走下去,您將需要對不同的算法凑术、參數(shù)等進行大量試驗翩蘸。
最常用的模型類型是可以使用層列表創(chuàng)建的順序模型。
此類模型的示例可能如下所示淮逊。
const model = tf.sequential();
model.add(tf.layers.conv2d({
inputShape: [28, 28, 1],
kernelSize: 5,
filters: 8,
strides: 1,
activation: 'relu',
kernelInitializer: 'VarianceScaling'
}));
model.add(tf.layers.maxPooling2d({
poolSize: [2, 2],
strides: [2, 2]
}));
model.add(tf.layers.conv2d({
kernelSize: 5,
filters: 16,
strides: 1,
activation: 'relu',
kernelInitializer: 'VarianceScaling'
}));
我們首先使用 tf.sequential 實例化它催首,并向其添加多個不同的層。
這一步有點隨意泄鹏,因為選擇層的類型和數(shù)量郎任,以及傳遞給層的參數(shù),與其說是科學备籽,不如說是一門藝術(shù)舶治。
您的模型在您第一次編寫時可能并不完美,并且需要多次更改才能最終得到性能最高的結(jié)果车猬。
要記住的一件重要事情是在模型的第一層提供 inputShape 參數(shù)霉猛,以指示模型將要訓練的數(shù)據(jù)的形狀。 后續(xù)層不需要它珠闰。
創(chuàng)建模型后惜浅,下一步是用數(shù)據(jù)對其進行訓練。 這一步是使用 fit 方法完成的铸磅。
await model.fit(data, label, options);
通常赡矢,在調(diào)用此方法之前,您需要將數(shù)據(jù)分批以一點一點地訓練模型阅仔。整個數(shù)據(jù)集通常太大而無法一次使用吹散,因此將其分成批次很重要。
傳遞給函數(shù)的 options 參數(shù)是一個包含有關(guān)訓練過程信息的對象八酒。您可以指定 epoch 數(shù)空民,即整個數(shù)據(jù)集通過神經(jīng)網(wǎng)絡的時間,以及批次大小羞迷,表示單個批次中存在的訓練示例數(shù)界轩。
由于數(shù)據(jù)集在 fit 方法中被分批傳遞,因此我們還需要考慮使用完整數(shù)據(jù)集訓練模型所需的迭代次數(shù)衔瓮。
例如浊猾,如果我們的數(shù)據(jù)集包含 1000 個示例,并且我們的批次大小為一次 100 個示例热鞍,則需要 10 次迭代才能完成 1 個 epoch葫慎。
因此衔彻,我們需要循環(huán)調(diào)用我們的 fit 方法 10 次,每次更新批處理數(shù)據(jù)偷办。
一旦模型經(jīng)過完全訓練艰额,就可以使用 predict 方法進行預測。
const prediction = model.predict(data);
關(guān)于此功能還有更多內(nèi)容要介紹椒涯,但我們將在接下來的幾章中通過實際示例進一步研究它柄沮。