TensorRT加速深度學習線上部署
本次的分享主要分為兩部分:
一、TensorRT理論介紹:基礎介紹TensorRT是什麼;做了哪些優化;為什麼在有了框架的基礎上還需要TensorRT的優化引擎。
二、TensorRT高階介紹:對於進階的使用者,出現TensorRT不支援的網路層該如何處理;低精度運算如fp16,大家也知道英偉達最新的v100帶的TensorCore支援低精度的fp運算,包括上一代的Pascal的P100也是支援fp16運算,當然我們針對這種推斷(Inference)的版本還支援int8,就是說我們用8位的整型來取代原來的fp32做計算,大家可以想象速度上肯定會有很大提升,但是也會需要進行一些額外的工作。
隨著傳統的高效能運算和新興的深度學習在百度、京東等大型的網際網路企業的普及發展,作為訓練和推理載體的GPU也被越來越多的使用。我們團隊的目標是讓大家能更好地利用GPU,使其在做深度學習訓練的時候達到更好的效果。
一、TensorRT理論解釋
TensorRT專案立項的時候名字叫做GPU Inference Engine(簡稱GIE),Tensor表示資料流動以張量的形式。所謂張量大家可以理解為更加複雜的高維陣列,一般一維陣列叫做Vector(即向量),二維陣列叫做Matrix,再高緯度的就叫Tensor,Matrix其實是二維的Tensor。在TensoRT中,所有的資料都被組成最高四維的陣列,如果對應到CNN中其實就是{N, C, H, W},N表示batch size,即多少張圖片或者多少個推斷(Inference)的例項;C表示channel數目;H和W表示影象或feature maps的高度和寬度。TR表示的是Runtime。
下圖是NVDIA針對深度學習平臺的一系列完整的解決方案(官網有更新版本)。如果大家對深度學習有些瞭解的話可能會知道,它分為訓練和部署兩部分,訓練部分首先也是最重要的是構建網路結構,準備資料集,使用各種框架進行訓練,訓練要包含validation和test的過程,最後對於訓練好的模型要在實際業務中進行使用。訓練的操作一般線上下,實時資料來之後線上訓練的情況比較少,大多數情況下資料是離線的,已經收集好的,資料更新不頻繁的一天或一週一收集,資料更新頻繁的可能幾十分鐘,線上下有大規模的叢集開始對資料或模型進行更新,這樣的訓練需要消耗大量的GPU,相對而言一般會給一個比較大的batchsize,因為它的實時性要求相對較低,一般訓練模型給的是128,甚至有些極端的1024,大的batch的好處是可以充分的利用GPU裝置。但是到推斷(Inference)的時候就是不同的概念了,推斷(Inference)的時候只需要做一個前向計算,將輸入通過神經網路得出預測的結果。而推斷(Inference)的實際部署有多種可能,可能部署在Data Center(雲端資料中心),比如說大家常見的手機上的語音輸入,目前都還是雲端的,也就是說你的聲音是傳到雲端的,雲端處理好之後把資料再返回來;還可能部署在嵌入端,比如說嵌入式的攝像頭、無人機、機器人或車載的自動駕駛,當然車載的自動駕駛可能是嵌入式的裝置,也可能是一臺完整的主機,像這種嵌入式或自動駕駛,它的特點是對實時性要求很高。同樣的,Data Center也是對實時性要求很高,做一個語音識別,不能說說完了等很長時間還沒有返回,所以線上的部署最大的特點是對實時性要求很高,它對latency非常敏感,要我們能非常快的給出推斷(Inference)的結果。做一個不同恰當的比方,訓練(Training)這個階段如果模型比較慢,其實是一個砸錢可以解決的問題,我們可以用更大的叢集、更多的機器,做更大的資料並行甚至是模型並行來訓練它,重要的是成本的投入。而部署端不只是成本的問題,如果方法不得當,即使使用目前最先進的GPU,也無法滿足推斷(Inference)的實時性要求。因為模型如果做得不好,沒有做優化,可能需要二三百毫秒才能做完一次推斷(Inference),再加上來回的網路傳輸,使用者可能一秒後才能得到結果。在語音識別的場景之下,使用者可以等待;但是在駕駛的場景之下,可能會有性命之庾。
efd04488a20a08c298a01b2c31b54b08e11a1b6a
在部署階段,latency是非常重要的點,而TensorRT是專門針對部署端進行優化的,目前TensorRT支援大部分主流的深度學習應用,當然最擅長的是CNN(卷積神經網路)領域,但是的TensorRT 3.0也是有RNN的API,也就是說我們可以在裡面做RNN的推斷(Inference)。
fc3f858464c413e149ecf979ae5b68fe00b98ed4
最典型的應用是圖片的分類,這也是最經典的,實際上也是深度學習目前解決的比較好的一些問題。其他的例如,圖片的語義分割、目標檢測等都是以圖片分類網路為基礎進行改進的。目標檢測是比較典型的例子(如下圖),訓練(Training)是對已經打好框的圖片進行前向計算,得出的框和實際的框(ground truth)進行對比,然後再做後向更新,更新模型。真正做推斷(Inference)的時候,比如一個攝像頭,基本上要保證是實時的,也就是說起碼要保證每秒25-30幀的速度,鑑於實際應用可能是二三十路攝像頭同時進來的資料,這時候必須保證一塊卡做到實時處理,還是比較有挑戰性的工作。
00dcf80a1db3ff57bbf06c8db7b4b4ec25d815f9
總結一下推斷(Inference)和訓練(Training)的不同:
1. 推斷(Inference)的網路權值已經固定下來,無後向傳播過程,因此可以
1)模型固定,可以對計算圖進行優化
2) 輸入輸出大小固定,可以做memory優化(注意:有一個概念是fine-tuning,即訓練好的模型繼續調優,只是在已有的模型做小的改動,本質上仍然是訓練(Training)的過程,TensorRT沒有fine-tuning
2. 推斷(Inference)的batch size要小很多,仍然是latency的問題,因為如果batch size很大,吞吐可以達到很大,比如每秒可以處理1024個batch,500毫秒處理完,吞吐可以達到2048,可以很好地利用GPU;但是推斷(Inference)不能做500毫秒處理,可以是8或者16,吞吐降低,沒有辦法很好地利用GPU.
3. 推斷(Inference)可以使用低精度的技術,訓練的時候因為要保證前後向傳播,每次梯度的更新是很微小的,這個時候需要相對較高的精度,一般來說需要float型,如FP32,32位的浮點型來處理資料,但是在推斷(Inference)的時候,對精度的要求沒有那麼高,很多研究表明可以用低精度,如半長(16)的float型,即FP16,也可以用8位的整型(INT8)來做推斷(Inference),研究結果表明沒有特別大的精度損失,尤其對CNN。更有甚者,對Binary(二進位制)的使用也處在研究過程中,即權值只有0和1。目前FP16和INT8的研究使用相對來說比較成熟。低精度計算的好處是一方面可以減少計算量,原來計算32位的單元處理FP16的時候,理論上可以達到兩倍的速度,處理INT8的時候理論上可以達到四倍的速度。當然會引入一些其他額外的操作,後面的講解中會詳細介紹FP18和INT8;另一方面是模型需要的空間減少,不管是權值的儲存還是中間值的儲存,應用更低的精度,模型大小會相應減小。
下圖展示的是TensorRT的效果,當然這是一個比較極端的例子,因為該例中使用的是最先進的GPU卡V100,V100添加了專門針對深度學習優化的TensorCore,TensorCore可以完成4×4矩陣的半精度乘法,也就是可以完成一個4×4的FP16矩陣和另外一個4×4的FP16矩陣相乘,當然可以再加一個矩陣(FP16 或FP32),得到一個FP32或者FP16的矩陣的過程。TensorCore在V100上理論峰值可以達到120 Tflops.(開個玩笑,電影終結者中整個天網的計算能力相當於兩塊V100)。回到圖中,先看一下如果只是用CPU來做推斷(Inference),首先它的吞吐只能達到140,也就是說每秒只能處理140張圖片,同時整個處理過程需要有14ms的延遲,也就是說使用者提交請求後,推斷(Inference)階段最快需要14ms才能返回結果;如果使用V100,在TensorFlow中去做推斷(Inference),大概是6.67ms的延時,但是吞吐只能達到305;如果使用V100加TensorRT,在保證延遲不變的情況下,吞吐可以提高15倍,高達5700張圖片每秒,這個差別是很大的。十幾倍的吞吐的提升實際上是在保證延遲的情況下成本的縮減 。
835e330a80d14756262dcc1d4a2b95e452fa14fa
回到TensorRT的主題,之前大家普遍存在的一個疑問是在訓練過程中可以使用不同的框架,為什麼推斷(Inference)不能用各種框架,比如TensorFlow等。當然是可以用的,但是問題是靈活性和效能是一種trade-off的關係,這是在做深度學習或訓練過程中經常會遇到的一個問題。比如像TensorFlow的設計初衷是為各種各樣的操作來做準備的,在早期的框架,例如Caffe中很多前後處理並不在框架裡面完成,而是通過額外的程式或指令碼處理,但是TensorFlow支援將所有的操作放入框架之中來完成,它提供了操作(Operation)級別的支援,使得靈活性大大提高,但是靈活性可能是以犧牲效率為代價的。TensorFlow在實現神經網路的過程中可以選擇各種各樣的高階庫,如用nn來搭建,tf.nn中的convolution中可以加一個卷積,可以用slim來實現卷積,不同的卷積實現效果不同,但是其對計算圖和GPU都沒有做優化,甚至在中間卷積演算法的選擇上也沒有做優化,而TensorRT在這方面做了很多工作。
在講TensorRT做了哪些優化之前, 想介紹一下TensorRT的流程, 首先輸入是一個預先訓練好的FP32的模型和網路,將模型通過parser等方式輸入到TensorRT中,TensorRT可以生成一個Serialization,也就是說將輸入串流到記憶體或檔案中,形成一個優化好的engine,執行的時候可以調取它來執行推斷(Inference)。
a314a47ec74b9886924eff8acb7cb0ad442c25b9
如上圖所示TensorRT整個過程可以分三個步驟,即模型的解析(Parser),Engine優化和執行(Execution)。暫時拋開TensorRT,如果讓大家從頭寫一個深度學習模型的前向過程,具體過程應該是
1) 首先實現NN的layer,如卷積的實現,pooling的實現。
2) 管理memory,資料在各層之間如何流動。
3) 推斷(Inference)的engine來呼叫各層的實現。
以上三個步驟在TendorRT都已經實現好了,使用者需要做的是如何將網路輸入到TensorRT中。目前TensorRT支援兩種輸入方式:
1. 一種是Parser的方式,即模型解析器,輸入一個caffe的模型,可以解析出其中的網路層及網路層之間的連線關係,然後將其輸入到TensorRT中,但是TensorRT是如何知道這些連線關係呢?答案是API。
2. API介面可以新增一個convolution或pooling。而Parser是解析模型檔案,比如TensorFlow轉換成的uff,或者是caffe的模型,再用API新增到TensorRT中,構建好網路。構建好後就可以做優化。
a) 考慮到一個情況,如果有一個網路層不支援,這個有可能,TensorRT只支援主流的操作,比如說一個神經網路專家開發了一個新的網路層,新型卷積和以前的卷積都不一樣,TensorRT是不知道是做什麼的。比如說最常見的檢測網路,有一些網路層也是不支援的,這個時候涉及到customer layer的功能,即使用者自定義層,構建使用者自定義層需要告訴TensorRT該層的連線關係和實現方式,這樣TensorRT才能去做。
b) 目前API支援兩種介面實現方式,一種是C++,另一種是Python,Python介面可能在一些快速實現上比較方便一些。
c) Parser目前有三個,一個是caffe Parser,這個是最古老的也是支援最完善的;另一個是uff,這個是NV定義的網路模型的一種檔案結構,現在TensorFlow可以直接轉成uff;另外下一個版本3.5或4.0會支援的onnx,是Facebook主導的開源的可交換的各個框架都可以輸出的,有點類似於文件編輯中的word格式或AutoCAD中CAD的格式,雖然是由一個公司提出,但是有希望成為一個標準,各個APP去支援這個標準。像pytorch和caffe 2都是支援這個格式的,這個目前只在NGC (NVDIA GPU Cloud)上支援,但是下一個版本發行都會支援。如果某個公司新推出一個特別火的框架不支援怎麼辦,仍然可以採用API的方式,一層一層的新增進去,告訴TensorRT連線關係,這也是OK的。
模型解析後,engine會進行優化,具體的優化稍後會介紹。得到優化好的engine可以序列化到記憶體(buffer)或檔案(file),讀的時候需要反序列化,將其變成engine以供使用。然後在執行的時候建立context,主要是分配預先的資源,engine加context就可以做推斷(Inference)。
以上是TensorRT的整個過程,大家在疑惑TensorRT是否支援TensorFlow,首先大家寫的網路計算層可能都是支援的,但是有些網路層可能不支援,在不支援的情況下可以用customer layer的方式新增進去,但是有時候為了使用方便,可能沒辦法一層一層的去新增,需要用模型檔案形式,這個取決於Parser是否完全支援。相對而言,大家在框架有過比較後會發現,caffe這個框架的特點是非常不靈活,如果要新增一個新的網路層,需要修改原始碼;TensorFlow的優點卻是非常的靈活。
剛才講到TensorRT所做的優化,總結下來主要有這麼幾點:
第一,也是最重要的,它把一些網路層進行了合併。大家如果瞭解GPU的話會知道,在GPU上跑的函式叫Kernel,TensorRT是存在Kernel的呼叫的。在絕大部分框架中,比如一個卷積層、一個偏置層和一個reload層,這三層是需要呼叫三次cuDNN對應的API,但實際上這三層的實現完全是可以合併到一起的,TensorRT會對一些可以合併網路進行合併;再比如說,目前的網路一方面越來越深,另一方面越來越寬,可能並行做若干個相同大小的卷積,這些卷積計算其實也是可以合併到一起來做的。
第二,比如在concat這一層,比如說這邊計算出來一個1×3×24×24,另一邊計算出來1×5×24×24,concat到一起,變成一個1×8×24×24的矩陣,這個叫concat這層這其實是完全沒有必要的,因為TensorRT完全可以實現直接接到需要的地方,不用專門做concat的操作,所以這一層也可以取消掉。
第三,Kernel可以根據不同的batch size 大小和問題的複雜程度,去選擇最合適的演算法,TensorRT預先寫了很多GPU實現,有一個自動選擇的過程。
第四,不同的batch size會做tuning。
第五,不同的硬體如P4卡還是V100卡甚至是嵌入式裝置的卡,TensorRT都會做優化,得到優化後的engine。
下圖是一個原始的GoogleNet的一部分,首先input後會有多個卷積,卷積完後有Bias和ReLU,結束後將結果concat(連線拼接)到一起,得到下一個input。
dfdc53c0dbe6ba51972c8a303b057682a45cc11f
以上的整個過程可以做些什麼優化呢?首先是convolution, Bias和ReLU這三個操作可以合併成CBR,合併後的結果如下所示,其中包含四個1×1的CBR,一個3×3的CBR和一個5×5的CBR。
d76cf3d63c7772de306bc44f7e600ba8abedf2d6
接下來可以繼續合併三個相連的1×1的CBR為一個大的1×1的CBR(如下圖),這個合併就可以更好地利用GPU。
ffe7111efeb7bbf589e9c3c9edce3901da8aa47a
繼而concat層可以消除掉,直接連線到下一層的next input(如下圖)。
20f27170cfd5a662f987025d6ce9bccf5ff49d40
另外還可以做併發(Concurrency),如下圖左半部分(max pool和1×1 CBR)與右半部分(大的1×1 CBR,3×3 CBR和5×5 CBR)彼此之間是相互獨立的兩條路徑,本質上是不相關的,可以在GPU上通過併發來做,來達到的優化的目標。
efac9c62262ecff12f897e7534de18617acfaca1
二、TensorRT高階特徵介紹
前面介紹了TesorRT的基礎,更多資訊可以查詢官網,或者反饋給我。接下來和大家分享一些TensorRT比較高階的特徵,這塊主要針對有一定經驗或者做過一些線上部署的人。
1. 外掛支援
首先TensorRT是支援外掛(Plugin)的,或者前面提到的Customer layer的形式,也就是說我們在某些層TensorRT不支援的情況下,最主要是做一些檢測的操作的時候,很多層是該網路專門定義的,TensorRT沒有支援,需要通過Plugin的形式自己去實現。實現過程包括如下兩個步驟:
1) 首先需要過載一個IPlugin的基類,生成自己的Plugin的實現,告訴GPU或TensorRT需要做什麼操作,要構建的Plugin是什麼樣子,其實就是類似於開發一個應用軟體的外掛,需要在上面實現什麼功能。
2) 其次要將外掛新增到合適的位置,在這裡是要新增到網路裡去。
注意,只有TensorRT 2.1和更高的版本支援外掛的功能(該視訊講的時候的版本是3.0.2,支援外掛功能)。
2. 低精度支援
低精度指的是之前所說過的FP16和INT8,其中FP16主要是Pascal P100和V100(tensor core)這兩張卡支援;而INT8主要針對的是 P4和P40這兩張卡,P4是專門針對線上做推斷(Inference)的小卡,和IPhone手機差不多大,75瓦的一張卡,功耗和效能非常好。
3. Python介面和更多的框架支援
TensorRT目前支援Python和C++的API,剛才也介紹瞭如何新增,Model importer(即Parser)主要支援Caffe和Uff,其他的框架可以通過API來新增,如果在Python中呼叫pyTouch的API,再通過TensorRT的API寫入TensorRT中,這就完成了一個網路的定義。
TensorRT去做推斷(Inference)的時候是不再需要框架的,用Caffe做推斷(Inference)需要Caffe這個框架,TensorRT把模型導進去後是不需要這個框架的,Caffe和TensorFlow可以通過Parser來匯入,一開始就不需要安裝這個框架,給一個Caffe或TensorFlow模型,完全可以在TensorRT高效的跑起來。
三、使用者自定義層
使用外掛建立使用者自定義層主要分為兩個步驟:
第一步是建立使用IPlugin介面建立使用者自定義層,IPlugin是TensorRT中預定義的C++抽象類,使用者需要定義具體實現了什麼。
第二步是將建立的使用者自定義層新增到網路中,如果是Caffe的模型,不支援這一層,將名字改成IPlugin是可以識別的,當然還需要一些額外的操作,說明這一層的操作是對應哪個Plugin的實現;而對於Uff是不支援Plugin的Parser,也就是說TensorFlow的模型中有一個Plugin的話,是不能從模型中識別出來的,這時候需要用到addPlugin()的方法去定義網路中Plugin的相關資訊。
IPlugin介面中需要被過載的函式有以下幾類:
1) 確定輸出:一個是通過int getNbOutput()得到output輸出的數目,即使用者所定義的一層有幾個輸出。另一個是通過Dims getOutputDimensions (int index, const Dims* inputs, int nbInputDims) 得到整個輸出的維度資訊,大家可能不一定遇到有多個輸出,一般來講只有一個輸出,但是大家在做檢測網路的時候可能會遇到多個輸出,一個輸出是實際的檢測目標是什麼,另一個輸出是目標的數目,可能的過個輸出需要設定Dimension的大小。
2) 層配置:通過void configure() 實現構建推斷(Inference) engine時模型中相應的引數大小等配置,configure()只是在構建的時候呼叫,這個階段確定的東西是在執行時作為外掛引數來儲存、序列化/反序列化的。
3) 資源管理:通過void Initialize()來進行資源的初始化,void terminate()來銷燬資源,甚至中間可能會有一些臨時變數,也可以使用這兩個函式進行初始化或銷燬。需要注意的是,void Initialize()和void terminate()是在整個執行時都被呼叫的,並不是做完一次推斷(Inference)就去呼叫terminate。相當於線上的一個服務,服務起的時候會呼叫void Initialize(),而服務止的時候呼叫void terminate(),但是服務會進進出出很多sample去做推斷(Inference)。
4) 執行(Execution):void enqueue()來定義使用者層的操作
5) 序列化和反序列化:這個過程是將層的引數寫入到二進位制檔案中,需要定義一些序列化的方法。通過size_t getSerializationSize()獲得序列大小,通過void serialize()將層的引數序列化到快取中,通過PluginSample()從快取中將層引數反序列化。需要注意的是,TensorRT沒有單獨的反序列化的API,因為不需要,在實習建構函式的時候就完成了反序列化的過程
6) 從Caffe Parser新增Plugin:首先通過Parsernvinfer1::IPlugin* createPlugin()實現nvcaffeparser1::IPlugin 介面,然後傳遞工廠例項到ICaffeParser::parse(),Caffe的Parser才能識別
7) 執行時建立外掛:通過IPlugin* createPlugin()實現nvinfer1::IPlugin介面,傳遞工廠例項到IInferRuntime::deserializeCudaEngine()
四、使用者自定義層-YOLOv2例項
我們用一個例子YOLOv2來給大家講一下完整的流程:
準備:首先要準備 Darknet framework(https://github.com/pjreddie/darknet.git),它是一個非常小眾的cfg的形式,然後需要準備需要訓練的資料集(VOC 2007 & VOC 2012),測試的指令如下:
./darknet detector test cfg/voc.data cfg/yolo-voc-relu.cfg \
backup/yolo-voc-relu_final.weights \ data/dog.jpg
模型轉換:如下圖所示,根據darknet的配置檔案生成caffe的prototxt檔案,注意使用ReLu而不是leaky-ReLu;另外darknet中儲存順序不同,首先儲存偏移;darknet的配置檔案中padding的意義不同,pad = 1表示有padding,darknet中padding的大小是Kernel的大小除以2。
ede03b373a51bae2233d61e01163c5cdfe706052
以下是darknet cuDNN和TensorRT FP32的效能對比,FP32是4.8ms,而Darknet是11.3ms。
04467dc62da869d215b7e2fd03177797c5b23dce
五、低精度的推斷(Inference)
TensorRT通過使用Pascal GPU低精度的技術,實現高效能。以下是FP16和INT8兩種型別的效能對比。
64126ae563c6e7f1e745ce71e188a57ac73f41e0
1. FP16 推斷(Inference)
TensorRT支援高度自動化的FP16推斷(Inference),解析模型要將模型的的資料型別設定為DataType::kHALF,同時通過builder- >setHalf2Mode(true)指令將推斷(Inference)設定為FP16的模式。需要注意兩點,一點是FP16推斷(Inference)不需要額外的輸入,只需要輸入預先訓練好的FP32模型,另一點是目前只有Tesla P100/V100支援原生的FP16。
下圖展示了將模型從FP32轉換成FP16,並以FP16的形式儲存的過程:
25134c0782573b58c0ddd17e3e380f46f42b5480
2.  INT8 推斷(Inference)
對於INT8 推斷(Inference),需要生成一個校準表來量化模型。接下來主要關注INT8推斷(Inference)的幾個方面,即:如何生成校準表,如何使用校準表,和INT8推斷(Inference)例項。
1) 如何生成校準表?
校準表的生成需要輸入有代表性的資料集, 對於分類任務TensorRT建議輸入五百張到一千張有代表性的圖片,最好每個類都要包括。生成校準表分為兩步:第一步是將輸入的資料集轉換成batch檔案;第二步是將轉換好的batch檔案喂到TensorRT中來生成基於資料集的校準表,可以去統計每一層的情況。
2) 如何使用校準表?
校準這個過程如果要跑一千次是很昂貴的,所以TensorRT支援將其存入文件,後期使用可以從文件載入,其中儲存和載入的功能通過兩個方法來支援,即writeCalibrationCache和readCalibrationCache。最簡單的實現是從write()和read()返回值,這樣就必須每次執行都做一次校準。如果想要儲存校準時間,需要實現使用者自定義的write/read方法,具體的實現可以參考TensorRT中的simpleINT8例項。
3) INT8推斷(Inference)例項
通過下圖的例項可以發現,在YOLOv2例項中,使用TensorRT INT8做推斷(Inference)的效能可以達到2.34ms。
a17a981c74416b6deff7c4c9faf5728ab3794126
下圖展示了用ResNet50中FP32和INT8的效能對比,可以發現,對於P4卡,在bachsize是64的時候,INT8推斷(Inference)大概可以達到1720fps,相對於FP32有3.6倍的加速,這個是相當可觀的。
4e70e801b6b72da0daa21a84774ed35d2607e910
至於大家關心的精度問題對比(如下圖),INT8通過用5張圖,10張圖,50張圖去修正,精度相差基本上都是在百分之零點零幾,這個效果是非常好的。
8aa6b09c3792ce5ba1a5625ba802ae9b6c29cbf5
最後總結一下TensorRT的優點:
1. TensorRT是一個高效能的深度學習推斷(Inference)的優化器和執行的引擎;
2. TensorRT支援Plugin,對於不支援的層,使用者可以通過Plugin來支援自定義建立;
3. TensorRT使用低精度的技術獲得相對於FP32二到三倍的加速,使用者只需要通過相應的程式碼來實現。