demo展示
這是一個剪刀石頭布預測模型,會根據最近20局的歷史資料訓練模型,神經網路輸入為最近2局的歷史資料。
如何擁有較為平滑的移植體驗?
- 保持兩種語言,和兩個框架的API文件處於開啟狀態,並隨時查閱:Python,JavaScript;Pytorch,TensorFlow JS(用瀏覽器 F3 搜尋關鍵詞)。
- 可選閱讀,《動手學深度學習》,掌握解決常見學習問題時,Pytorch 和 TensorFlow 的實現方法。
- 精讀 TensorFlow JS 的官方教程,和指南。
- 精讀 TensorFlow JS 的官方文件:與 Python tf.keras 的區別。
- 深入瞭解 JavaScript 特色物件:生成器 Generator,Promise,async await。
- 多用谷歌。
一些碎碎念
- JavaScript 不存在像 numpy 之於 python 一樣著名且好用的資料處理庫,所以請放棄對 JavaScript 原生型別 Array 進行操作的嘗試,轉而尋找基於 TensorFlow JS API 的解決方法。
- JavaScript 作為一門前端語言,一大特色是包含了大量非同步程式設計(即程式碼不是順序執行的,瀏覽器自有一套標準去調整程式碼的執行順序),這是為了保證前端頁面不被卡死,所必備的性質。也因此,TensorFlow JS的函式中,許多輸入輸出傳遞的都不是資料,而是Promise物件。很多功能支援非同步,但如果沒有完全搞懂非同步程式設計,不妨多用同步的思路:用 tf.Tensor.arraySync() 把 Tensor 的值取出,具體來說是將 Tensor 物件以同步的方式(即立即執行)複製生成出一個新的 array 物件。
- Promise 物件是ES6新增的物件,一般與then一起使用,但掌握 async & await 就夠了,這是更簡潔的寫法。
- 多關注 API 文件中物件方法的返回型別,返回 Promise 物件則與非同步程式設計相關,如果要獲取Promise物件儲存的值,需要在有 async function 包裹的程式碼中前置 await 關鍵字。
- Pytorch 中的張量可以透過索引訪問其元素,而 TensorFlow JS 則不能,需要轉換為 array 進行訪問。
常用平替整理
將張量轉換為陣列
- Python, Pytorch:
tensor = torch.tensor([1,2,3])
np_array = tensor.numpy()
- JS, tfjs:
// 方式一:arraySync() let tensor = tf.tensor1d([1,2,3]); let array = tensor.arraySync(); console.log(array); // [1,2,3] // 方式二:在async函式體內操作 async function fun() { let tensor = tf.tensor1d([1,2,3]); let array = await tensor.array(); console.log(array); // [1,2,3] } fun(); // 注意,下面的寫法是不行的,因為async函式的返回值是Promise物件 array = async function (){ return await tensor.array(); }(); console.log(array); // Promise object // 方式三:用then取出async函式返回Promise物件中的值 let a (async function() { let array = await tensor.array(); return array })().then(data => {a = data;}) console.log(a); // [1,2,3]
訪問張量中的元素
- Python,Pytorch:
tensor = torch.tensor([1,2,3]) print(tensor[0]) print(tensor[-1])
- JS,tfjs(不能直接透過訪問tensor,需要轉換成array):
const tensor = tf.tensor1d([1,2,3]); const array = tensor.arraySync(); console.log(array[0]);
console.log(array[array.length - 1]);
獲取字典/物件的關鍵字
- Python:
actions = {'up':[1,0,0,0], 'down':[0,1,0,0], 'left':[0,0,1,0], 'right':[0,0,0,1]} actions_keys_list = list(actions.keys())
- JS:
const actions = {'up':[1,0,0,0], 'down':[0,1,0,0], 'left':[0,0,1,0], 'right':[0,0,0,1]};
const actionsKeysArray = Object.keys(actions);
“先進先出”棧
- Python:
memory = [1,2,3] memory.append(4) # 入棧 memory.pop(0) # 出棧
- JS:
let memory = [1,2,3]; memory.push(4); // 入棧 memory.splice(0,1); // 出棧
“後進先出”棧
- Python:
memory = [1,2,3] memory.append(4) # 入棧 memory.pop() # 出棧
- JS:
let memory = [1,2,3]; memory.push(4); // 入棧 memory.pop(); // 出棧
根據機率分佈取樣元素
- Python,Numpy:
actions = ['up','down','left','right'] prob = [0.1, 0.4, 0.4, 0.1] sample_action = np.random.choice(actions, p=prob))
- JS,tfjs:
const actions = ['up', 'down', 'left', 'right']; const prob = [0.1, 0.4, 0.4, 0.1]; sampleActionIndex = tf.multinomial(prob, 1, null, true).arraySync(); // tf.Tensor 不能作為索引,需要用 arraySync() 同步地傳輸為 array sampleAction = actions[sampleActionIndex];
找到陣列中最大值的索引(Argmax)
- Python,Numpy,Pyorch:
actions = ['up', 'down', 'left', 'right'] prob = [0.1, 0.3, 0.5, 0.1] prob_tensor = torch.tensor(prob) action_max_prob = actions[np.array(prob).argmax()] # np.array 可以作為索引 action_max_prob = actions[prob_tensor.argmax().numpy()] # torch.tensor 不能作為索引,需要轉換為 np.array
- JS, tfjs:
const actions = ['up', 'down', 'left', 'right']; const prob = [0.1, 0.3, 0.5, 0.1]; const probTensor = tf.tensor1d(prob); const actionsMaxProb = actions[probTensor.argmax().arraySync()]; // tf.Tensor 不能作為索引,需要用 arraySync()同步地傳輸為 array
生成等差數列陣列
- Python:
range_list = list(range(1,10,1))
- JS, tfjs:
const rangeArray = tf.range(1, 10, 1).arraySync();
打亂陣列
- Python:
actions = ['up', 'down', 'left', 'right'] print(random.shuffle(actions))
- JS:How to randomize (shuffle) a JavaScript array? 關於原生JS解決方案的大討論。
- tfjs:(1)用 tf.util 類操作,處理常規的需求。
const actions = ['up', 'down', 'left', 'right'];
tf.util.shuffle(actions);
console.log(actions);
(2)用 tf.data.shuffle 操作,不建議,該類及其方法一般僅與 神經網路模型更新 繫結使用。
極簡邏輯迴歸
- Python,Numpy,Pytorch:
import numpy as np import torch from torch import nn import random class Memory(object): # 向Memory輸送的資料可以是list,也可以是np.array def __init__(self, size=100, batch_size=32): self.memory_size = size self.batch_size = batch_size self.main = [] def save(self, data): if len(self.main) == self.memory_size: self.main.pop(0) self.main.append(data) def sample(self): samples = random.sample(self.main, self.batch_size) return map(np.array, zip(*samples)) class Model(object): # Model中所有方法的輸入和返回都是np.array def __init__(self, lr=0.01, device=None): self.LR = lr self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 呼叫GPU 若無則CPU self.network = nn.Sequential(nn.Flatten(), nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 5), nn.Softmax(dim=1)).to(self.device) self.loss = nn.CrossEntropyLoss(reduction='mean') self.optimizer = torch.optim.Adam(self.network.parameters(), lr=self.LR) def predict_nograd(self, _input): with torch.no_grad(): _input = np.expand_dims(_input, axis=0) _input = torch.from_numpy(_input).float().to(self.device) _output = self.network(_input).cpu().numpy() _output = np.squeeze(_output) return _output def update(self, input_batch, target_batch): # 設定為訓練模式 self.network.train() _input_batch = torch.from_numpy(input_batch).float().to(self.device) _target_batch = torch.from_numpy(target_batch).float().to(self.device) self.optimizer.zero_grad() _evaluate_batch = self.network(_input_batch) batch_loss = self.loss(_evaluate_batch, _target_batch) batch_loss.backward() self.optimizer.step() batch_loss = batch_loss.item() # 設定為預測模式 self.network.eval() if __name__ == '__main__': memory = Memory() model = Model() # 產生資料並輸送到記憶體中 # 假設一個5分類問題 for i in range(memory.memory_size): example = np.random.randint(0,2,size=10) label = np.eye(5)[np.random.randint(0,5)] data = [example, label] memory.save(data) # 訓練100次,每次從記憶體中隨機抽取一個batch的資料 for i in range(100): input_batch, target_batch = memory.sample() model.update(input_batch, target_batch) # 預測 prediction = model.predict_nograd(np.random.randint(0,2,size=10)) print(prediction)
- JS,tfjs(網頁應用一般不使用GPU):
const Memory = { memorySize : 100, main : [], saveData : function (data) { // data = [input:array, label:array] if (this.main.length == this.memorySize) { this.main.splice(0,1); } this.main.push(data); }, getMemoryTensor: function () { let inputArray = [], labelArray = []; for (let i = 0; i < this.main.length; i++) { inputArray.push(this.main[i][0]) labelArray.push(this.main[i][1]) } return { inputBatch: tf.tensor2d(inputArray), labelBatch: tf.tensor2d(labelArray) } } } const Model = { batchSize: 32, epoch: 200, network: tf.sequential({ layers: [ tf.layers.dense({inputShape: [10], units: 16, activation: 'relu'}), tf.layers.dense({units: 5, activation: 'softmax'}), ] }), compile: function () { this.network.compile({ optimizer: tf.train.sgd(0.1), shuffle: true, loss: 'categoricalCrossentropy', metrics: ['accuracy'] }); }, predict: function (input) { // input = array // Return tensor1d return this.network.predict(tf.tensor2d([input])).squeeze(); }, update: async function (inputBatch, labelBatch) { // inputBatch = tf.tensor2d(memorySize × 10) // labelBatch = tf.tensor2d(memorySize × 5) this.compile(); await this.network.fit(inputBatch, labelBatch, { epochs: this.epoch, batchSize: this.batchSize }).then(info => { console.log('Final accuracy', info.history.acc); }); } } // 假設一個5分類問題 // 隨機生成樣例和標籤,並填滿記憶體 let example, label, rnd, data; for (let i = 0; i < Memory.memorySize; i++) { example = tf.multinomial(tf.tensor1d([.5, .5]), 10).arraySync(); rnd = Math.floor(Math.random()*5); label = tf.oneHot(tf.tensor1d([rnd], 'int32'), 5).squeeze().arraySync(); data = [example, label]; Memory.saveData(data); } // 將記憶體中儲存的資料匯出為tensor,並訓練模型 let {inputBatch, labelBatch} = Memory.getMemoryTensor(); Model.update(inputBatch, labelBatch);