1. 程式人生 > >TensorFlow 學習指南 四、分散式

TensorFlow 學習指南 四、分散式

原文:LearningTensorFlow.com

譯者:飛龍

協議:CC BY-NC-SA 4.0

自豪地採用谷歌翻譯

自定義函式

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("\n" * 5)
print("Shape:", shape, "Device:", device_name)
print("Time taken:", datetime.now() - startTime)

print("\n" * 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_placementTrue,作為建立會話的一部分。 原型看起來像這樣:

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,執行mapreduce操作是許多非平凡程式的關鍵構建塊。 例如,整合學習可以將單獨的機器學習模型傳送給多個工作器,然後組合分類結果來形成最終結果。另一個例子是一個程序。

這是我們將分發的另一個基本指令碼:

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 的作業。 如果你有備用的第二臺計算機,請嘗試通過網路執行此操作。