- Apache MXNet 教程
- Apache MXNet - 首頁
- Apache MXNet - 簡介
- Apache MXNet - 安裝 MXNet
- Apache MXNet - 工具包和生態系統
- Apache MXNet - 系統架構
- Apache MXNet - 系統元件
- Apache MXNet - 統一運算元 API
- Apache MXNet - 分散式訓練
- Apache MXNet - Python 包
- Apache MXNet - NDArray
- Apache MXNet - Gluon
- Apache MXNet - KVStore 和視覺化
- Apache MXNet - Python API ndarray
- Apache MXNet - Python API gluon
- Apache MXNet - Python API autograd 和初始化器
- Apache MXNet - Python API Symbol
- Apache MXNet - Python API Module
- Apache MXNet 有用資源
- Apache MXNet - 快速指南
- Apache MXNet - 有用資源
- Apache MXNet - 討論
Apache MXNet - 統一運算元 API
本章提供有關 Apache MXNet 中統一運算元應用程式程式設計介面 (API) 的資訊。
SimpleOp
SimpleOp 是一個新的統一運算元 API,它統一了不同的呼叫過程。一旦呼叫,它就會返回到運算元的基本元素。統一運算元專為一元運算和二元運算而設計。這是因為大多數數學運算子處理一個或兩個運算元,而更多的運算元使得與依賴性相關的最佳化變得有用。
我們將藉助一個示例來了解其 SimpleOp 統一運算元的工作原理。在這個示例中,我們將建立一個充當**平滑 L1 損失**的運算元,它是 L1 和 L2 損失的混合。我們可以定義和編寫如下所示的損失:
loss = outside_weight .* f(inside_weight .* (data - label)) grad = outside_weight .* inside_weight .* f'(inside_weight .* (data - label))
這裡,在上面的示例中,
.* 代表逐元素乘法
**f,f’** 是我們假設在**mshadow**中的平滑 L1 損失函式。
將此特定損失實現為一元或二元運算子似乎是不可能的,但 MXNet 為其使用者提供了符號執行中的自動微分,這將損失簡化為 f 和 f’。這就是為什麼我們當然可以將此特定損失實現為一元運算子。
定義形狀
眾所周知,MXNet 的**mshadow 庫**需要顯式記憶體分配,因此我們需要在任何計算發生之前提供所有資料形狀。在定義函式和梯度之前,我們需要提供輸入形狀一致性和輸出形狀,如下所示
typedef mxnet::TShape (*UnaryShapeFunction)(const mxnet::TShape& src, const EnvArguments& env); typedef mxnet::TShape (*BinaryShapeFunction)(const mxnet::TShape& lhs, const mxnet::TShape& rhs, const EnvArguments& env);
函式 mxnet::Tshape 用於檢查輸入資料形狀和指定的輸出資料形狀。在這種情況下,如果您沒有定義此函式,則預設輸出形狀將與輸入形狀相同。例如,在二元運算子的情況下,lhs 和 rhs 的形狀預設情況下被檢查為相同。
現在讓我們繼續我們的**平滑 L1 損失示例**。為此,我們需要在標頭檔案實現**smooth_l1_unary-inl.h**中定義一個 XPU 到 cpu 或 gpu。原因是在**smooth_l1_unary.cc**和**smooth_l1_unary.cu**中重用相同的程式碼。
#include <mxnet/operator_util.h>
#if defined(__CUDACC__)
#define XPU gpu
#else
#define XPU cpu
#endif
就像在我們的**平滑 L1 損失示例**中一樣,輸出與源具有相同的形狀,我們可以使用預設行為。它可以寫成如下:
inline mxnet::TShape SmoothL1Shape_(const mxnet::TShape& src,const EnvArguments& env) {
return mxnet::TShape(src);
}
定義函式
我們可以使用一個輸入建立一個一元或二元函式,如下所示:
typedef void (*UnaryFunction)(const TBlob& src, const EnvArguments& env, TBlob* ret, OpReqType req, RunContext ctx); typedef void (*BinaryFunction)(const TBlob& lhs, const TBlob& rhs, const EnvArguments& env, TBlob* ret, OpReqType req, RunContext ctx);
以下是包含執行時執行所需資訊的**RunContext ctx 結構**:
struct RunContext {
void *stream; // the stream of the device, can be NULL or Stream<gpu>* in GPU mode
template<typename xpu> inline mshadow::Stream<xpu>* get_stream() // get mshadow stream from Context
} // namespace mxnet
現在,讓我們看看如何將計算結果寫入**ret**。
enum OpReqType {
kNullOp, // no operation, do not write anything
kWriteTo, // write gradient to provided space
kWriteInplace, // perform an in-place write
kAddTo // add to the provided space
};
現在,讓我們繼續我們的**平滑 L1 損失示例**。為此,我們將使用 UnaryFunction 來定義此運算子的函式,如下所示
template<typename xpu>
void SmoothL1Forward_(const TBlob& src,
const EnvArguments& env,
TBlob *ret,
OpReqType req,
RunContext ctx) {
using namespace mshadow;
using namespace mshadow::expr;
mshadow::Stream<xpu> *s = ctx.get_stream<xpu>();
real_t sigma2 = env.scalar * env.scalar;
MSHADOW_TYPE_SWITCH(ret->type_flag_, DType, {
mshadow::Tensor<xpu, 2, DType> out = ret->get<xpu, 2, DType>(s);
mshadow::Tensor<xpu, 2, DType> in = src.get<xpu, 2, DType>(s);
ASSIGN_DISPATCH(out, req,
F<mshadow_op::smooth_l1_loss>(in, ScalarExp<DType>(sigma2)));
});
}
定義梯度
除了**Input、TBlob**和**OpReqType**之外,二元運算子的梯度函式具有類似的結構。讓我們檢視下面,我們在這裡建立了一個具有各種型別輸入的梯度函式
// depending only on out_grad typedef void (*UnaryGradFunctionT0)(const OutputGrad& out_grad, const EnvArguments& env, TBlob* in_grad, OpReqType req, RunContext ctx); // depending only on out_value typedef void (*UnaryGradFunctionT1)(const OutputGrad& out_grad, const OutputValue& out_value, const EnvArguments& env, TBlob* in_grad, OpReqType req, RunContext ctx); // depending only on in_data typedef void (*UnaryGradFunctionT2)(const OutputGrad& out_grad, const Input0& in_data0, const EnvArguments& env, TBlob* in_grad, OpReqType req, RunContext ctx);
如上所定義的**Input0、Input、OutputValue**和**OutputGrad**都共享**GradientFunctionArgument**的結構。它定義如下:
struct GradFunctionArgument {
TBlob data;
}
現在讓我們繼續我們的**平滑 L1 損失示例**。為了啟用梯度的鏈式法則,我們需要將來自頂部的**out_grad**乘以**in_grad**的結果。
template<typename xpu>
void SmoothL1BackwardUseIn_(const OutputGrad& out_grad, const Input0& in_data0,
const EnvArguments& env,
TBlob *in_grad,
OpReqType req,
RunContext ctx) {
using namespace mshadow;
using namespace mshadow::expr;
mshadow::Stream<xpu> *s = ctx.get_stream<xpu>();
real_t sigma2 = env.scalar * env.scalar;
MSHADOW_TYPE_SWITCH(in_grad->type_flag_, DType, {
mshadow::Tensor<xpu, 2, DType> src = in_data0.data.get<xpu, 2, DType>(s);
mshadow::Tensor<xpu, 2, DType> ograd = out_grad.data.get<xpu, 2, DType>(s);
mshadow::Tensor<xpu, 2, DType> igrad = in_grad->get<xpu, 2, DType>(s);
ASSIGN_DISPATCH(igrad, req,
ograd * F<mshadow_op::smooth_l1_gradient>(src, ScalarExp<DType>(sigma2)));
});
}
將 SimpleOp 註冊到 MXNet
建立形狀、函式和梯度後,我們需要將它們都恢復到 NDArray 運算子和符號運算子中。為此,我們可以使用如下所示的註冊宏:
MXNET_REGISTER_SIMPLE_OP(Name, DEV)
.set_shape_function(Shape)
.set_function(DEV::kDevMask, Function<XPU>, SimpleOpInplaceOption)
.set_gradient(DEV::kDevMask, Gradient<XPU>, SimpleOpInplaceOption)
.describe("description");
**SimpleOpInplaceOption**可以定義如下:
enum SimpleOpInplaceOption {
kNoInplace, // do not allow inplace in arguments
kInplaceInOut, // allow inplace in with out (unary)
kInplaceOutIn, // allow inplace out_grad with in_grad (unary)
kInplaceLhsOut, // allow inplace left operand with out (binary)
kInplaceOutLhs // allow inplace out_grad with lhs_grad (binary)
};
現在讓我們繼續我們的**平滑 L1 損失示例**。為此,我們有一個依賴於輸入資料的梯度函式,因此該函式無法就地編寫。
MXNET_REGISTER_SIMPLE_OP(smooth_l1, XPU)
.set_function(XPU::kDevMask, SmoothL1Forward_<XPU>, kNoInplace)
.set_gradient(XPU::kDevMask, SmoothL1BackwardUseIn_<XPU>, kInplaceOutIn)
.set_enable_scalar(true)
.describe("Calculate Smooth L1 Loss(lhs, scalar)");
SimpleOp 上的 EnvArguments
眾所周知,某些操作可能需要以下內容:
一個標量作為輸入,例如梯度縮放
一組控制行為的關鍵字引數
一個臨時空間來加速計算。
使用 EnvArguments 的好處是它提供了額外的引數和資源,使計算更具可擴充套件性和效率。
示例
首先讓我們定義如下所示的結構:
struct EnvArguments {
real_t scalar; // scalar argument, if enabled
std::vector<std::pair<std::string, std::string> > kwargs; // keyword arguments
std::vector<Resource> resource; // pointer to the resources requested
};
接下來,我們需要從**EnvArguments.resource**請求額外的資源,如**mshadow::Random<xpu>**和臨時記憶體空間。這可以透過以下方式完成:
struct ResourceRequest {
enum Type { // Resource type, indicating what the pointer type is
kRandom, // mshadow::Random<xpu> object
kTempSpace // A dynamic temp space that can be arbitrary size
};
Type type; // type of resources
};
現在,註冊將從**mxnet::ResourceManager**請求宣告的資源請求。之後,它將資源放置在**EnvAgruments**中的**std::vector<Resource> resource**中。
我們可以藉助以下程式碼訪問資源:
auto tmp_space_res = env.resources[0].get_space(some_shape, some_stream); auto rand_res = env.resources[0].get_random(some_stream);
如果您在我們的平滑 L1 損失示例中看到,需要一個標量輸入來標記損失函式的轉折點。這就是為什麼在註冊過程中,我們在函式和梯度宣告中使用**set_enable_scalar(true)**和**env.scalar**。
構建張量運算
這裡出現了一個問題,為什麼我們需要構建張量運算?原因如下:
計算利用 mshadow 庫,有時我們沒有現成的函式。
如果操作不是以逐元素方式執行的,例如 softmax 損失和梯度。
示例
這裡,我們使用上述平滑 L1 損失示例。我們將建立兩個對映器,即平滑 L1 損失和梯度的標量情況
namespace mshadow_op {
struct smooth_l1_loss {
// a is x, b is sigma2
MSHADOW_XINLINE static real_t Map(real_t a, real_t b) {
if (a > 1.0f / b) {
return a - 0.5f / b;
} else if (a < -1.0f / b) {
return -a - 0.5f / b;
} else {
return 0.5f * a * a * b;
}
}
};
}