『深度長文』Tensorflow程式碼解析(三)

深度學習大講堂發表於2017-01-13

4. TF – Kernels模組

TF中包含大量Op運算元,這些運算元組成Graph的節點集合。這些運算元對Tensor實現相應的運算操作。圖 4.1列出了TF中的Op運算元的分類和舉例。

『深度長文』Tensorflow程式碼解析(三)

圖 4.1 TensorFlow核心庫中的部分運算

4.1 OpKernels 簡介

OpKernel類(core/framework/op_kernel.h)是所有Op類的基類。繼承OpKernel還可以自定義新的Op類。用的較多的Op如(MatMul,  Conv2D,  SoftMax,  AvgPooling, Argmax等)。

所有Op包含註冊(Register Op)和實現(正向計算、梯度定義)兩部分。

所有Op類的實現需要overide抽象基函式 void Compute(OpKernelContext* context),實現自身Op功能。使用者可以根據需要自定義新的Op操作,參考[12]。

TF中所有Op操作的屬性定義和描述都在 ops/ops.pbtxt。如下Add操作,定義了輸入引數x、y,輸出引數z。

『深度長文』Tensorflow程式碼解析(三)

4.2 UnaryOp & BinaryOp

UnaryOp和BinaryOp定義了簡單的一元操作和二元操作,類定義在/core/kernels/ cwise_ops.h檔案,類實現在/core/kernels/cwise_op_*.cc型別的檔案中,如cwise_op_sin.cc檔案。

一元操作全稱為Coefficient-wise unary operations,一元運算有abs, sqrt, exp, sin, cos,conj(共軛)等。如abs的基本定義:

『深度長文』Tensorflow程式碼解析(三)

二元操作全稱為Coefficient-wise binary operations,二元運算有add,sub, div, mul,mod等。如sum的基本定義:

『深度長文』Tensorflow程式碼解析(三)

4.3 MatMul

4.3.1 Python相關部分

在Python指令碼中定義matmul運算:

『深度長文』Tensorflow程式碼解析(三)

根據Ops名稱MatMul從Ops庫中找出對應Ops型別

『深度長文』Tensorflow程式碼解析(三)

建立ops節點

『深度長文』Tensorflow程式碼解析(三)

建立ops節點並指定相關屬性和裝置分配

『深度長文』Tensorflow程式碼解析(三)

4.3.2 C++相關部分

Python指令碼通過swig呼叫進入C介面API檔案core/client/tensor_c_api.cc,呼叫TF_NewNode函式生成節點,同時還需要指定輸入變數,TF_AddInput函式設定first輸入變數,TF_AddInputList函式設定other輸入變數。這裡op_type為MatMul,first輸入變數為a,other輸入變數為b。

『深度長文』Tensorflow程式碼解析(三)

建立節點根據節點型別從註冊的Ops工廠中生成,即TF通過工廠模式把一系列Ops註冊到Ops工廠中。其中MatMul的註冊函式為如下

『深度長文』Tensorflow程式碼解析(三)

4.3.3 MatMul正向計算

MatMul的實現部分在core/kernels/matmul_op.cc檔案中,類MatMulOp繼承於OpKernel,成員函式Compute完成計算操作。

『深度長文』Tensorflow程式碼解析(三)

MatMul的測試用例core/kernels/matmul_op_test.cc檔案,要除錯這個測試用例,可通過如下方式:

『深度長文』Tensorflow程式碼解析(三)

在TF中MatMul實現了CPU和GPU兩個版本,其中CPU版本使用Eigen庫,GPU版本使用cuBLAS庫。

CPU版的MatMul使用Eigen庫,呼叫方式如下:

『深度長文』Tensorflow程式碼解析(三)

簡而言之就是呼叫eigen的constract函式。

『深度長文』Tensorflow程式碼解析(三)

GPU版的MatMul使用cuBLAS庫,準確而言是基於cuBLAS的stream_executor庫。Stream executor是google開發的開源平行計算庫,呼叫方式如下:

『深度長文』Tensorflow程式碼解析(三)

其中stream類似於裝置控制程式碼,可以呼叫stream executor中的cuda模組完成運算。

4.3.4 MatMul梯度計算

MatMul的梯度計算本質上也是一種kernel ops,描述為MatMulGrad。MatMulgrad操作是定義在grad_ops工廠中,類似於ops工廠。定義方式如下:

『深度長文』Tensorflow程式碼解析(三)

MatmulGrad由FDH(Function Define Helper)完成定義,

『深度長文』Tensorflow程式碼解析(三)

其中attr_adj_x="transpose_a" ax0=false, ax1=true, attr_adj_y= "transpose_b", ay0=true, ay1=false, *g屬於FunctionDef類,包含MatMul的梯度定義。

從FDH定義中可以看出MatMulGrad本質上還是MatMul操作。在矩陣求導運算中:

『深度長文』Tensorflow程式碼解析(三)

MatMulGrad的測試用例core/ops/math_grad_test.cc檔案,要除錯這個測試用例,可通過如下方式:

『深度長文』Tensorflow程式碼解析(三)

4.4 Conv2d

關於conv2d的python呼叫部分和C++建立部分可參考MatMul中的描述。

4.4.1 Conv2d正向計算部分

TF中conv2d介面如下所示,簡單易用:

『深度長文』Tensorflow程式碼解析(三)

實現部分在core/kernels/conv_ops.cc檔案中,類Conv2DOp繼承於抽象基類OpKernel。

Conv2DOp的測試用例core/kernels/eigen_spatial_convolutions_test.cc檔案,要除錯這個測試用例,可通過如下方式:

『深度長文』Tensorflow程式碼解析(三)

Conv2DOp的成員函式Compute完成計算操作。

『深度長文』Tensorflow程式碼解析(三)

為方便描述,假設tf.nn.conv2d中input引數的shape為[batch, in_rows, in_cols, in_depth],filter引數的shape為[filter_rows, filter_cols, in_depth, out_depth]。

首先,計算卷積運算後輸出tensor的shape。

『深度長文』Tensorflow程式碼解析(三)

Ø  若padding=VALID,output_size = (input_size - filter_size + stride) / stride;

Ø  若padding=SAME,output_size = (input_size + stride - 1) / stride;

其次,根據計算結果給輸出tensor分配記憶體。

『深度長文』Tensorflow程式碼解析(三)

然後,開始卷積計算。Conv2DOp實現了CPU和GPU兩種模式下的卷積運算。同時,還需要注意input tensor的輸入格式,通常有NHWC和NCHW兩種格式。在TF中,Conv2d-CPU模式下目前僅支援NHWC格式,即[Number, Height, Weight, Channel]格式。Conv2d-GPU模式下以NCHW為主,但支援將NHWC轉換為NCHW求解。C++中多維陣列是row-major順序儲存的,而Eigen預設是col-major順序的,則C++中[N, H, W, C]相當於Eigen中的[C, W, H, N],即dimention order是相反的,需要特別注意。

Conv2d-CPU模式下呼叫Eigen庫函式。

『深度長文』Tensorflow程式碼解析(三)

Eigen庫中卷積函式的詳細程式碼參見圖 4.2。

『深度長文』Tensorflow程式碼解析(三)

圖 4.2 Eigen卷積運算的定義

Ø  Tensor::extract_image_patches() 為卷積或池化操作抽取與kernel size一致的image patches。該函式的定義在eigen3/unsupported/Eigen/CXX11/src/Tensor/ TensorBase.h中,參考該目錄下ReadME.md。

Ø  Tensor::extract_image_patches() 的輸出與input tensor的data layout有關。設input tensor為ColMajor格式[NHWC],則image patches輸出為[batch, filter_index, filter_rows, filter_cols, in_depth],並reshape為[batch * filter_index, filter_rows * filter_cols * in_depth],而kernels維度為[filter_rows * filter_cols * in_depth, out_depth],然後kernels矩陣乘image patches得到輸出矩陣[batch * filter_index, out_depth],並reshape為[batch, out_rows, out_cols, out_depth]。

Conv2d-GPU模式下呼叫基於cuDNN的stream_executor庫。若input tensor為NHWC格式的,則先轉換為NCHW格式

『深度長文』Tensorflow程式碼解析(三)

呼叫cudnn庫實現卷積運算:

『深度長文』Tensorflow程式碼解析(三)

計算完成後再轉換成HHWC格式的

『深度長文』Tensorflow程式碼解析(三)

4.4.2 Conv2d梯度計算部分

Conv2D梯度計算公式,假設output=Conv2d(input, filter),則

『深度長文』Tensorflow程式碼解析(三)

Conv2D梯度計算的測試用例core/kernels/eigen_backward_spatial_convolutions_test.cc檔案,要除錯這個測試用例,可通過如下方式:

『深度長文』Tensorflow程式碼解析(三)

Conv2d的梯度計算函式描述為Conv2DGrad。Conv2DGrad操作定義在grad_ops工廠中。註冊方式如下:

『深度長文』Tensorflow程式碼解析(三)

Conv2DGrad由FDH(Function Define Helper)完成定義,參見圖 4.3。

『深度長文』Tensorflow程式碼解析(三)

『深度長文』Tensorflow程式碼解析(三)

圖 4.3 Conv2DGrad的函式定義


Conv2DGrad梯度函式定義中依賴Conv2DBackpropInput和Conv2DBackpropFilter兩種Ops,二者均定義在kernels/conv_grad_ops.cc檔案中。

Conv2DBackpropInputOp和Conv2DBackpropFilterOp的實現分為GPU和CPU版本。

Conv2D運算的GPU版實現定義在類Conv2DSlowBackpropInputOp<GPUDevice, T>和類Conv2DSlowBackprop FilterOp <GPUDevice, T>中。

Conv2D運算的CPU版有兩種實現形式,分別為custom模式和fast模式。Custom模式基於賈揚清在caffe中的思路實現,相關類是Conv2DCustomBackpropInputOp<CPUDevice, T>和Conv2DCustomBackpropFilterOp<CPUDevice, T>。Fast模式基於Eigen計算庫,由於在GPU下會出現nvcc編譯超時,目前僅適用於CPU環境,相關類是Conv2DFastBackpropInputOp<CPUDevice, T>和Conv2DFastBackpropFilterOp<CPUDevice, T>。 

根據Conv2DGrad的函式定義,從程式碼分析Conv2D-GPU版的實現程式碼,即分析Conv2DBackpropInput和Conv2DBackpropFilter的實現方式。

『深度長文』Tensorflow程式碼解析(三)

Conv2DSlowBackpropInputOp的成員函式Compute完成計算操作。

『深度長文』Tensorflow程式碼解析(三)

Compute實現部分呼叫stream executor的相關函式,需要先獲取庫的stream控制程式碼,再呼叫卷積梯度函式。

『深度長文』Tensorflow程式碼解析(三)

stream executor在卷積梯度運算部分仍然是藉助cudnn庫實現的。

『深度長文』Tensorflow程式碼解析(三)

4.4.3 MaxPooling計算部分

在很多影象分類和識別問題中都用到了池化運算,池化操作主要有最大池化(max pooling)和均值池化(avg pooling),本章節主要介紹最大池化的實現方法。呼叫TF介面可以很容易實現池化操作。

『深度長文』Tensorflow程式碼解析(三)

類MaxPoolingOp繼承於類OpKernel,成員函式Compute實現了最大池化運算。

『深度長文』Tensorflow程式碼解析(三)

最大池化運算呼叫Eigen庫實現。

『深度長文』Tensorflow程式碼解析(三)

Eigen庫中最大池化的詳細描述如下:

『深度長文』Tensorflow程式碼解析(三)

其中最大池化運算主要分為兩步,第一步中extract_image_patch為池化操作抽取與kernel size一致的image patches,第二步計算每個image patch的最大值。

4.5 SendOp & RecvOp

TF所有操作都是節點形式表示的,包括計算節點和非計算節點。在跨裝置通訊中,傳送節點(SendOp)和接收節點(RecvOp)為不同裝置的兩個相鄰節點完成完成資料通訊操作。Send和Recv通過TCP或RDMA來傳輸資料。

TF採用Rendezvous(回合)通訊機制,Rendezvous類似生產者/消費者的訊息信箱。引用TF描述如下:

『深度長文』Tensorflow程式碼解析(三)

TF的訊息傳遞屬於採用“傳送不阻塞/接收阻塞”機制,實現場景有LocalRendezvous

(本地訊息傳遞)、RpcRemoteRendezvous (分散式訊息傳遞)。除此之外還有IntraProcessRendezvous用於本地不同裝置間通訊。

TF會在不同裝置的兩個相鄰節點之間新增Send和Recv節點,通過Send和Recv之間進行通訊來達到op之間通訊的效果,如圖 4.4右子圖所示。圖中還涉及到一個優化問題,即a->b和a->c需要建立兩組send/recv連線的,但兩組連線是可以共用的,所以合併成一組連線。

『深度長文』Tensorflow程式碼解析(三)

圖 4.4 Graph跨裝置通訊


Send和Recv分別對應OpKernel中的SendOp和RecvOp兩個類(kernels/sendrecv_ops.h)。

SendOp的計算函式。

『深度長文』Tensorflow程式碼解析(三)

SendOp作為傳送方需要先獲取封裝ctx訊息,然後藉助Rendezvous模組傳送給接收方。

『深度長文』Tensorflow程式碼解析(三)

RecvOp的計算函式如下。

『深度長文』Tensorflow程式碼解析(三)

RecvOp作為接收方藉助Rendezvous模組獲取ctx訊息。

『深度長文』Tensorflow程式碼解析(三)

其中parsed變數是類ParsedKey的例項。圖 5‑5是Rendezvous封裝的ParsedKey訊息實體示例。

『深度長文』Tensorflow程式碼解析(三)

4.6 ReaderOp & QueueOp

4.6.1 TF資料讀取

TF系統定義了三種資料讀取方式[13]:

Ø  供給資料(Feeding): 在TensorFlow程式執行的每一步, 通過feed_dict來供給資料。

Ø  從檔案讀取資料: 在TensorFlow圖的起始, 讓一個輸入管線(piplines)從檔案中讀取資料放入佇列,通過QueueRunner供給資料,其中佇列可以實現多執行緒非同步計算。

Ø  預載入資料: 在TensorFlow圖中定義常量或變數來儲存所有資料,如Mnist資料集(僅適用於資料量比較小的情況)。

除了以上三種資料讀取方式外,TF還支援使用者自定義資料讀取方式,即繼承ReaderOpKernel類建立新的輸入讀取類[14]。本章節主要講述通過piplines方式讀取資料的方法。

Piplines利用佇列實現非同步計算

從piplines讀取資料也有兩種方式:一種是讀取所有樣本檔案路徑名轉換成string tensor,使用input_producer將tensor亂序(shuffle)或slice(切片)處理放入佇列中;另一種是將資料轉化為TF標準輸入格式,即使用TFRecordWriter將樣本資料寫入tfrecords檔案中,再使用TFRecordReader將tfrecords檔案讀取到佇列中。

圖 4.6描述了piplines讀取資料的第一種方式,這些流程通過節點和邊串聯起來,成為graph資料流的一部分。

從左向右,第一步是載入檔案列表,使用convert_to_tensor函式將檔案列表轉化為tensor,如cifar10資料集中的image_files_tensor和label_tensor。

第二步是使用input_producer將image_files_tensor和label_tensor放入圖中的檔案佇列中,這裡的input_producer作用就是將樣本放入佇列節點中,有string_input_producer、range_input_producer和slice_input_producer三種,其中slice_input_producer的切片功能支援亂序,其他兩種需要藉助tf.train.shuffle_batch函式作亂序處理,有關三種方式的具體描述可參考tensorflow/python/training/input.py註釋說明。

第三步是使用tf.read_file()讀取佇列中的檔案資料到記憶體中,使用解碼器如tf.image.decode_jpeg()解碼成[height, width, channels]格式的資料。

最後就是使用batch函式將樣本資料處理成一批批的樣本,然後使用session執行訓練。

『深度長文』Tensorflow程式碼解析(三)

圖 4.6 使用piplines讀取資料


4.6.2 TFRecords使用

TFRecords是TF支援的標準檔案格式,這種格式允許將任意的資料轉換為TFRecords支援的檔案格式。TFRecords方法需要兩步:第一步是使用TFRecordWriter將樣本資料寫入tfrecords檔案中,第二步是使用TFRecordReader將tfrecords檔案讀取到佇列中。

圖 4.7是TFRecords檔案寫入的簡單示例。tf.train.Example將資料填入到Example協議記憶體塊(protocol buffer),將協議記憶體塊序列化為一個字串,通過TFRecordWriter寫入到TFRecords檔案,圖中定義了label和image_raw兩個feature。Example協議記憶體塊的定義請參考檔案core/example/example.proto。

『深度長文』Tensorflow程式碼解析(三)

圖 4.7 TFRecordWriter寫入資料示例

圖 4.8是TFRecords檔案讀取的簡單示例。tf.parse_single_example解析器將Example協議記憶體塊解析為張量,放入example佇列中,其中features命名和型別要與Example寫入的一致。

『深度長文』Tensorflow程式碼解析(三)

圖 4.8 TFRecrodReader讀取資料示例

4.6.3 ReaderOps分析

ReaderOpsKernel類封裝了資料讀取的入口函式Compute,通過繼承ReaderOpsKernel類可實現各種自定義的資料讀取方法。圖 4.9是ReaderOp相關的UML檢視。

『深度長文』Tensorflow程式碼解析(三)

圖 4.9 ReaderOp相關的UML檢視

ReaderOpKernel子類必須重新定義成員函式SetReaderFactory實現對應的資料讀取邏輯。TFRecordReaderOp的讀取方法定義在TFRecordReader類中。

『深度長文』Tensorflow程式碼解析(三)

TFRecordReader呼叫RecordReader::ReadRecord()函式逐步讀取.tfrecord檔案中的資料,每讀取一次,offset向後移動一定長度。

『深度長文』Tensorflow程式碼解析(三)

其中offset的計算方式。

『深度長文』Tensorflow程式碼解析(三)


原文連結:https://mp.weixin.qq.com/s/vwSlxxD5Ov0XwQCKy1oyuQ

相關文章