TensorFlow學習指南四、分散式
譯者:飛龍
自豪地採用谷歌翻譯
自定義函式
Conway 的生命遊戲是一個有趣的電腦科學模擬,它在地圖上發生,有許多正方形的單元格,就像棋盤一樣。 模擬以特定的時間步驟進行,並且板上的每個單元可以是 1(生存)或 0(死亡)。 經過特定的時間步驟後,每個單元格都處於生存狀態或死亡狀態:
- 如果細胞是活著的,但是有一個或零個鄰居,它會由於“人口不足”而“死亡”。
- 如果細胞存活並且有兩個或三個鄰居,它就會活著。
- 如果細胞有三個以上的鄰居,它就會因人口過多而死亡。
- 任何有三個鄰居的死細胞都會再生。
雖然這些規則似乎非常病態,但實際的模擬非常簡單,創造了非常有趣的模式。 我們將建立一個 TensorFlow 程式來管理 Conway 的生命遊戲,並在此過程中瞭解自定義py_func
函式,並生成如下動畫:
http://learningtensorflow.com/images/game.mp4
首先,讓我們生成地圖。 這是非常基本的,因為它只是一個 0 和 1 的矩陣。 我們隨機生成初始地圖,每次執行時都會提供不同的地圖:
import tensorflow as tf
from matplotlib import pyplot as plt
shape = (50, 50)
initial_board = tf.random_uniform(shape, minval=0, maxval=2, dtype=tf.int32)
with tf.Session() as session:
X = session.run(initial_board)
fig = plt.figure()
plot = plt.imshow(X, cmap=`Greys`, interpolation=`nearest`)
plt.show()
我們生成一個隨機選擇的 0 和 1 的initial_board
,然後執行它來獲取值。 然後我們使用matplotlib.pyplot
來顯示它,使用imshow
函式,它基本上只根據一些cmap
顏色方案繪製矩陣中的值。 在這種情況下,使用`Greys`
會產生黑白矩陣,以及我們生命遊戲的單個初始起點:
更新地圖的狀態
由於生命遊戲的地圖狀態表示為矩陣,因此使用矩陣運算子更新它是有意義的。 這應該提供一種快速方法,更新給定時間點的狀態。
非常有才華的 Jake VanderPlas 在使用 SciPy 和 NumPy 更新生命遊戲中的特定狀態方面做了一些出色的工作。 他的寫作值得一讀,可以在[這裡]找到。 如果你對以下程式碼的工作原理感興趣,我建議你閱讀 Jake 的說明。 簡而言之,convolve2d
那行標識每個單元有多少鄰居(這是計算機視覺中的常見操作符)。 我稍微更新了程式碼以減少行數,請參閱下面的更新後的函式:
def update_board(X):
# Check out the details at: https://jakevdp.github.io/blog/2013/08/07/conways-game-of-life/
# Compute number of neighbours,
N = convolve2d(X, np.ones((3, 3)), mode=`same`, boundary=`wrap`) - X
# Apply rules of the game
X = (N == 3) | (X & (N == 2))
return X
update_board
函式是 NumPy 陣列的函式。 它不適用於張量,迄今為止,在 TensorFlow 中沒有一種好方法可以做到這一點(雖然你可以使用現有的工具自己編寫它,它不是直截了當的)。
在 TensorFlow 的 0.7 版本中,新增了一個新函式py_func
,它接受 python 函式並將其轉換為 TensorFlow 中的節點。
在撰寫本文時(3 月 22 日),0.6 是正式版,並且它沒有py_func
。 我建議按照 TensorFlow 的 Github 頁面上的說明為你的系統安裝每晚構建。 例如,對於 Ubuntu 使用者,你下載相關的 wheel 檔案(python 安裝檔案)並安裝它:
python -m wheel install --force ~/Downloads/tensorflow-0.7.1-cp34-cp34m-linux_x86_64.whl
請記住,你需要正確啟用 TensorFlow 源(如果你願意的話)。
最終結果應該是你安裝了 TensorFlow 的 0.7 或更高版本。 你可以通過在終端中執行此程式碼來檢查:
python -c "import tensorflow as tf; print(tf.__version__)"
結果將是版本號,在編寫時為 0.7.1。
在程式碼上:
board = tf.placeholder(tf.int32, shape=shape, name=`board`)
board_update = tf.py_func(update_board, [board], [tf.int32])
從這裡開始,你可以像往常一樣,對張量操作節點(即board_update
)執行初始地圖。 要記住的一點是,執行board_update
的結果是一個矩陣列表,即使我們的函式只定義了一個返回值。 我們通過在行尾新增[0]
來獲取第一個結果,我們更新的地圖儲存在X
中。
with tf.Session() as session:
initial_board_values = session.run(initial_board)
X = session.run(board_update, feed_dict={board: initial_board_values})[0]
所得值X
是初始配置之後更新的地圖。 它看起來很像一個初始隨機地圖,但我們從未顯示初始的(雖然你可以更新程式碼來繪製兩個值)
迴圈
這是事情變得非常有趣的地方,儘管從 TensorFlow 的角度來看,我們已經為本節做了很多努力。 我們可以使用matplotlib
來顯示和動畫,因此顯示時間步驟中的模擬狀態,就像我們的原始 GIF 一樣。 matplotlib
動畫的複雜性有點棘手,但是你建立一個更新並返回繪圖的函式,並使用該函式呼叫動畫程式碼:
import matplotlib.animation as animation
def game_of_life(*args):
X = session.run(board_update, feed_dict={board: X})[0]
plot.set_array(X)
return plot,
ani = animation.FuncAnimation(fig, game_of_life, interval=200, blit=True)
plt.show()
提示:你需要從早期程式碼中刪除
plt.show()
才能執行!
我將把拼圖的各個部分作為練習留給讀者,但最終結果將是一個視窗出現,遊戲狀態每 200 毫秒更新一次。
如果你實現了,請給我們發訊息!
1)獲取完整的程式碼示例,使用matplotlib
和 TensorFlow 生成遊戲的動畫
2)康威的生命遊戲已被廣泛研究,並有許多有趣的模式。 建立一個從檔案載入模式的函式,並使用它們而不是隨機地圖。 我建議從 Gosper 的滑翔槍開始。
3)生命遊戲的一個問題(特徵?)是地圖可以重複,導致迴圈永遠不會停止。 編寫一些跟蹤之前遊戲狀態的程式碼,並在遊戲狀態重複時停止迴圈。
使用 GPU
GPU(圖形處理單元)是大多數現代計算機的元件,旨在執行 3D 圖形所需的計算。 它們最常見的用途是為視訊遊戲執行這些操作,計算多邊形向使用者顯示遊戲。 總的來說,GPU 基本上是一大批小型處理器,執行高度並行化的計算。 你現在基本上有了一個迷你超級計算機!
注意:不是真正的超級計算機,但在許多方面有些相似。
雖然 GPU 中的每個“CPU”都很慢,但它們中有很多並且它們專門用於數字處理。 這意味著 GPU 可以同時執行許多簡單的數字處理任務。 幸運的是,這正是許多機器學習演算法需要做的事情。
沒有 GPU 嗎?
大多數現代(最近10年)的計算機都有某種形式的 GPU,即使它內建在你的主機板上。 出於本教程的目的,這就足夠了。
你需要知道你有什麼型別的顯示卡。 Windows 使用者可以遵循這些說明,其他系統的使用者需要查閱他們系統的文件。
非 N 卡使用者
雖然其他顯示卡可能是受支援的,但本教程僅在最近的 NVidia 顯示卡上進行測試。 如果你的顯示卡屬於不同型別,我建議你尋找 NVidia 顯示卡來學習,購買或者借用。 如果這對你來說真的很難,請聯絡你當地的大學或學校,看看他們是否可以提供幫助。 如果你仍然遇到問題,請隨意閱讀以及使用標準 CPU 進行操作。 你將能夠在以後遷移所學的東西。
安裝 GPU 版的 TensorFlow
如果你之前沒有安裝支援 GPU 的 TensorFlow,那麼我們首先需要這樣做。我們在第 1 課中沒有說明,所以如果你沒有按照你的方式啟用 GPU 支援,那就是沒有了。
我建議你為此建立一個新的 Anaconda 環境,而不是嘗試更新以前的環境。
在你開始之前
前往 TensorFlow 官方安裝說明,並遵循 Anaconda 安裝說明。這與我們在第 1 課中所做的主要區別在於,你需要為你的系統啟用支援 GPU 的 TensorFlow 版本。但是,在將 TensorFlow 安裝到此環境之前,你需要使用 CUDA 和 CuDNN,將計算機設定為啟用 GPU 的。TensorFlow 官方文件逐步概述了這一點,但如果你嘗試設定最近的 Ubuntu 安裝,我推薦本教程。主要原因是,在撰寫本文時(2016 年 7 月),尚未為最新的 Ubuntu 版本構建 CUDA,這意味著該過程更加手動。
使用你的 GPU
真的很簡單。 至少是字面上。 只需將這個:
# 起步操作
with tf.Session() as sess:
# 執行你的程式碼
改為這個:
with tf.device("/gpu:0"):
# 起步操作
with tf.Session() as sess:
# 執行你的程式碼
這個新行將建立一個新的上下文管理器,告訴 TensorFlow 在 GPU 上執行這些操作。
我們來看一個具體的例子。 下面的程式碼建立一個隨機矩陣,其大小在命令列中提供。 我們可以使用命令列選項在 CPU 或 GPU 上執行程式碼:
import sys
import numpy as np
import tensorflow as tf
from datetime import datetime
device_name = sys.argv[1] # Choose device from cmd line. Options: gpu or cpu
shape = (int(sys.argv[2]), int(sys.argv[2]))
if device_name == "gpu":
device_name = "/gpu:0"
else:
device_name = "/cpu:0"
with tf.device(device_name):
random_matrix = tf.random_uniform(shape=shape, minval=0, maxval=1)
dot_operation = tf.matmul(random_matrix, tf.transpose(random_matrix))
sum_operation = tf.reduce_sum(dot_operation)
startTime = datetime.now()
with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as session:
result = session.run(sum_operation)
print(result)
# 很難在終端上看到具有大量輸出的結果 - 新增一些換行符以提高可讀性。
print("
" * 5)
print("Shape:", shape, "Device:", device_name)
print("Time taken:", datetime.now() - startTime)
print("
" * 5)
你可以在命令列執行此命令:
python matmul.py gpu 1500
這將使用 GPU 和大小為 1500 平方的矩陣。 使用以下命令在 CPU 上執行相同的操作:
python matmul.py cpu 1500
與普通的 TensorFlow 指令碼相比,在執行支援 GPU 的程式碼時,你會注意到的第一件事是輸出大幅增加。 這是我的計算機在列印出任何操作結果之前列印出來的內容。
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcublas.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcudnn.so.5 locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcufft.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcuda.so.1 locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcurand.so locally
I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:925] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
I tensorflow/core/common_runtime/gpu/gpu_init.cc:102] Found device 0 with properties:
name: GeForce GTX 950M
major: 5 minor: 0 memoryClockRate (GHz) 1.124
pciBusID 0000:01:00.0
Total memory: 3.95GiB
Free memory: 3.50GiB
I tensorflow/core/common_runtime/gpu/gpu_init.cc:126] DMA: 0
I tensorflow/core/common_runtime/gpu/gpu_init.cc:136] 0: Y
I tensorflow/core/common_runtime/gpu/gpu_device.cc:838] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 950M, pci bus id: 0000:01:00.0)
如果你的程式碼沒有產生與此類似的輸出,那麼你沒有執行支援 GPU 的 Tensorflow。或者,如果你收到ImportError: libcudart.so.7.5: cannot open shared object file: No such file or directory
這樣的錯誤,那麼你還沒有正確安裝 CUDA 庫。在這種情況下,你需要返回,遵循指南來在你的系統上安裝 CUDA。
嘗試在 CPU 和 GPU 上執行上面的程式碼,慢慢增加數量。從 1500 開始,然後嘗試 3000,然後是 4500,依此類推。你會發現 CPU 開始需要相當長的時間,而 GPU 在這個操作中真的非常快!
如果你有多個 GPU,則可以使用其中任何一個。 GPU 是從零索引的 – 上面的程式碼訪問第一個 GPU。將裝置更改為gpu:1
使用第二個 GPU,依此類推。你還可以將部分計算髮送到一個 GPU,然後是另一個 GPU。此外,你可以以類似的方式訪問計算機的 CPU – 只需使用cpu:0
(或其他數字)。
我應該把什麼樣的操作傳送給 GPU?
通常,如果該過程的步驟可以描述,例如“執行該數學運算數千次”,則將其傳送到 GPU。 示例包括矩陣乘法和計算矩陣的逆。 實際上,許多基本矩陣運算是 GPU 的拿手好戲。 作為一個過於寬泛和簡單的規則,應該在 CPU 上執行其他操作。
更換裝置和使用 GPU 還需要付出代價。 GPU 無法直接訪問你計算機的其餘部分(當然,除了顯示器)。 因此,如果你在 GPU 上執行命令,則需要先將所有資料複製到 GPU,然後執行操作,然後將結果複製回計算機的主存。 TensorFlow 在背後處理這個問題,因此程式碼很簡單,但仍需要執行工作。
並非所有操作都可以在 GPU 上完成。 如果你收到以下錯誤,你正在嘗試執行無法在 GPU 上執行的操作:
Cannot assign a device to node ‘PyFunc’: Could not satisfy explicit device specification ‘/device:GPU:1’ because no devices matching that specification are registered in this process;
如果是這種情況,你可以手動將裝置更改為 CPU 來執行此函式,或者設定 TensorFlow,以便在這種情況下自動更改裝置。 為此,請在配置中設定allow_soft_placement
為True
,作為建立會話的一部分。 原型看起來像這樣:
with tf.Session(config=tf.ConfigProto(allow_soft_placement=True)):
# 在這裡執行你的圖
我還建議在使用 GPU 時記錄裝置的放置,這樣可以輕鬆除錯與不同裝置使用情況相關的問題。 這會將裝置的使用情況列印到日誌中,從而可以檢視裝置何時更改以及它對圖的影響。
with tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)):
# 在這裡執行你的圖
1)設定你的計算機,將 GPU 用於 TensorFlow(或者如果你最近沒有 GPU,就借一臺)。
2)嘗試在 GPU 上執行以前的練習的解決方案。 哪些操作可以在 GPU 上執行,哪些不可以?
3)構建一個在 GPU 和 CPU 上都使用操作的程式。 使用我們在第 5 課中看到的效能分析程式碼,來估計向 GPU 傳送資料和從 GPU 獲取資料的影響。
4)把你的程式碼發給我! 我很樂意看到你的程式碼示例,如何使用 Tensorflow,以及你找到的任何技巧。
分散式計算
TensorFlow 支援分散式計算,允許在不同的程式上計算圖的部分,這些程式可能位於完全不同的伺服器上! 此外,這可用於將計算分發到具有強大 GPU 的伺服器,並在具有更多記憶體的伺服器上完成其他計算,依此類推。 雖然介面有點棘手,所以讓我們從頭開始構建。
這是我們的第一個指令碼,我們將在單個程式上執行,然後轉移到多個程式。
import tensorflow as tf
x = tf.constant(2)
y1 = x + 300
y2 = x - 66
y = y1 + y2
with tf.Session() as sess:
result = sess.run(y)
print(result)
到現在為止,這個指令碼不應該特別嚇到你。 我們有一個常數和三個基本方程。 結果(238)最後列印出來。
TensorFlow 有點像伺服器 – 客戶端模型。 這個想法是你創造了一大堆能夠完成繁重任務的工作器。 然後,你可以在其中一個工作器上建立會話,它將計算圖,可能將其中的一部分分發到伺服器上的其他叢集。
為此,主工作器,主機,需要了解其他工作器。 這是通過建立ClusterSpec
來完成的,你需要將其傳遞給所有工作器。 ClusterSpec
使用字典構建,其中鍵是“作業名稱”,每個任務包含許多工作器。
下面是這個圖表看上去的樣子。
以下程式碼建立一個ClusterSpect
,其作業名稱為local
,和兩個工作器程式。
請注意,這些程式碼不會啟動這些程式,只會建立一個將啟動它們的引用。
import tensorflow as tf
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
接下來,我們啟動程式。 為此,我們繪製其中一個工作器的圖,並啟動它:
server = tf.train.Server(cluster, job_name="local", task_index=1)
上面的程式碼在local
作業下啟動localhost:2223
工作器。
下面是一個指令碼,你可以從命令列執行來啟動這兩個程式。 將程式碼在你的計算機上儲存為create_worker.py
並執行python create_worker.py 0
然後執行python create_worker.py 1
。你需要單獨的終端來執行此操作,因為指令碼不會自己停止(他們正在等待指令)。
# 從命令列獲取任務編號
import sys
task_number = int(sys.argv[1])
import tensorflow as tf
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
server = tf.train.Server(cluster, job_name="local", task_index=task_number)
print("Starting server #{}".format(task_number))
server.start()
server.join()
執行此操作後,你將發現伺服器執行在兩個終端上。 我們準備分發!
“分發”作業的最簡單方法是在其中一個程式上建立一個會話,然後在那裡執行圖。 只需將上面的session
行更改為:
with tf.Session("grpc://localhost:2222") as sess:
現在,這並沒有真正分發,不足以將作業傳送到該伺服器。 TensorFlow 可以將程式分發到叢集中的其他資源,但可能不會。 我們可以通過指定裝置來強制執行此操作(就像我們在上一課中對 GPU 所做的那樣):
import tensorflow as tf
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
x = tf.constant(2)
with tf.device("/job:local/task:1"):
y2 = x - 66
with tf.device("/job:local/task:0"):
y1 = x + 300
y = y1 + y2
with tf.Session("grpc://localhost:2222") as sess:
result = sess.run(y)
print(result)
現在我們正在分發! 這可以通過根據名稱和任務編號,為工作器分配任務來實現。 格式為:
/job:JOB_NAME/task:TASK_NUMBER
通過多個作業(即識別具有大型 GPU 的計算機),我們可以以多種不同方式分發程式。
對映和歸約
MapReduce 是執行大型操作的流行正規化。 它由兩個主要步驟組成(雖然在實踐中還有一些步驟)。
第一步稱為對映,意思是“獲取列表,並將函式應用於每個元素”。 你可以在普通的 python 中執行這樣的對映:
def myfunction(x):
return x + 5
map_result = map(myfunction, [1, 2, 3])
print(list(map_result))
第二步是歸約,這意味著“獲取列表,並使用函式將它們組合”。 常見的歸約操作是求和 – 即“獲取數字列表並通過將它們全部加起來組合它們”,這可以通過建立相加兩個數字的函式來執行。 reduce
的原理是獲取列表的前兩個值,執行函式,獲取結果,然後使用結果和下一個值執行函式。 總之,我們將前兩個數字相加,取結果,加上下一個數字,依此類推,直到我們到達列表的末尾。 同樣,reduce
是普通 python 的一部分(儘管它不是分散式的):
from functools import reduce
def add(a, b):
return a + b
print(reduce(add, [1, 2, 3]))
譯者注:原作者這裡的話並不值得推薦,比如
for
你更應該使用reduce
,因為它更安全。
回到分散式 TensorFlow,執行map
和reduce
操作是許多非平凡程式的關鍵構建塊。 例如,整合學習可以將單獨的機器學習模型傳送給多個工作器,然後組合分類結果來形成最終結果。另一個例子是一個程式。
這是我們將分發的另一個基本指令碼:
import numpy as np
import tensorflow as tf
x = tf.placeholder(tf.float32, 100)
mean = tf.reduce_mean(x)
with tf.Session() as sess:
result = sess.run(mean, feed_dict={x: np.random.random(100)})
print(result)
import numpy as np
import tensorflow as tf
x = tf.placeholder(tf.float32, 100)
mean = tf.reduce_mean(x)
with tf.Session() as sess:
result = sess.run(mean, feed_dict={x: np.random.random(100)})
print(result)
轉換為分散式版本只是對先前轉換的更改:
import numpy as np
import tensorflow as tf
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
x = tf.placeholder(tf.float32, 100)
with tf.device("/job:local/task:1"):
first_batch = tf.slice(x, [0], [50])
mean1 = tf.reduce_mean(first_batch)
with tf.device("/job:local/task:0"):
second_batch = tf.slice(x, [50], [-1])
mean2 = tf.reduce_mean(second_batch)
mean = (mean1 + mean2) / 2
with tf.Session("grpc://localhost:2222") as sess:
result = sess.run(mean, feed_dict={x: np.random.random(100)})
print(result)
如果你從對映和歸約的角度來考慮它,你會發現分發計算更容易。 首先,“我怎樣才能將這個問題分解成可以獨立解決的子問題?” – 這就是你的對映。 第二,“我如何將答案結合起來來形成最終結果?” – 這就是你的歸約。
在機器學習中,對映最常用的場景就是分割資料集。 線性模型和神經網路通常都非常合適,因為它們可以單獨訓練,然後再進行組合。
1)將ClusterSpec
中的local
更改為其他內容。 你還需要在指令碼中進行哪些更改才能使其正常工作?
2)計算平均的指令碼目前依賴於切片大小相同的事實。 嘗試使用不同大小的切片並觀察錯誤。 通過使用tf.size
和以下公式來組合切片的平均值來解決此問題:
overall_average = ((size_slice_1 * mean_slice_1) + (size_slice_2 * mean_slice_2) + ...) / total_size
3)你可以通過修改裝置字串來指定遠端計算機上的裝置。 例如,/job:local/task:0/gpu:0
會定位local
作業的 GPU。 建立一個使用遠端 GPU 的作業。 如果你有備用的第二臺計算機,請嘗試通過網路執行此操作。
相關文章
- 當Spark遇上TensorFlow分散式深度學習框架原理和實踐Spark分散式深度學習框架
- 分散式TensorFlow入坑指南:從例項到程式碼帶你玩轉多機器深度學習分散式深度學習
- 深度學習:基於K8S的分散式Tensorflow系統深度學習K8S分散式
- TensorFlow分散式實踐分散式
- jmeter學習指南之分散式測試的來龍去脈JMeter分散式
- 函式學習四函式
- 分散式學習記錄分散式
- 分散式理論學習分散式
- springcloud微服務實戰 學習筆記四 分散式配置中心SpringGCCloud微服務筆記分散式
- 學習etcd分散式鎖分散式
- python 學習 -- 分散式程式Python分散式
- 分散式系統學習思路分散式
- 分散式計算如果學習分散式
- tensorflow相關函式學習總結函式
- Tensorflow 學習
- 多工學習分散式化及聯邦學習分散式聯邦學習
- 分散式系統學習筆記分散式筆記
- 分散式爬蟲學習筆記分散式爬蟲筆記
- [原始碼解析] TensorFlow 分散式之 MirroredStrategy原始碼分散式
- 分散式事務~從seata例項來學習分散式事務分散式
- TS學習筆記(四):函式筆記函式
- 彈性分散式深度學習系統分散式深度學習
- 深入分散式快取 — 學習總結分散式快取
- 一起來學習分散式鎖分散式
- GlusterFS分散式儲存學習筆記分散式筆記
- ZooKeeper學習筆記四:使用ZooKeeper實現一個簡單的分散式鎖筆記分散式
- [原始碼解析] TensorFlow 之 分散式變數原始碼分散式變數
- [原始碼解析] TensorFlow 分散式之 ClusterCoordinator原始碼分散式
- 關於分散式鎖原理的一些學習與思考-redis分散式鎖,zookeeper分散式鎖分散式Redis
- 分散式多工學習論文閱讀(四):去偏lasso實現高效通訊分散式
- 深度學習中tensorflow框架的學習深度學習框架
- hadoop 偽分散式模式學習筆記Hadoop分散式模式筆記
- 分散式網站架構學習資源分散式網站架構
- tensorflow語法學習
- TensorFlow學習資源
- TensorFlow 學習筆記筆記
- 分散式事務(四)之TCC分散式
- jmeter學習指南之常用函式的使用JMeter函式