1.Tensor
張量(Tensor)是現(xiàn)代機(jī)器學(xué)習(xí)的基礎(chǔ)喜最。它的核心是一個(gè)數(shù)據(jù)容器皮钠。
Tensor在pytorch中的實(shí)現(xiàn)
Tensor由存儲(chǔ)和解釋組成怕享。存儲(chǔ)即底層數(shù)據(jù)的存儲(chǔ)空間同眯,和內(nèi)存管理相關(guān)南蓬;解釋賦予了存儲(chǔ)空間含義调炬,例如存儲(chǔ)空間的數(shù)據(jù)類型是什么语盈,數(shù)據(jù)的維度是多少等等。
- Tensor類在
aten/src/ATen/templates/TensorBody.h
中
/// Returns a `Tensor`'s dtype (`TypeMeta`). Defined in TensorMethods.h
caffe2::TypeMeta dtype() const noexcept;
/// Returns a `Tensor`'s device.
Device device() const;
int64_t numel() const ;
// Length of one array element in bytes. This is the traditional
// Numpy naming.
size_t itemsize() const;
...
//TensorImpl指針
c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl> impl_;
Tensor類提供了眾多成員方法查詢Tensor的相關(guān)信息缰泡。在pytorch里真正意義上的Tensor實(shí)現(xiàn)類是TensorImpl刀荒,Tensor類中包含impl_這個(gè)TensorImpl類變量。
- TensorImpl類在
c10/core/TensorImpl.h
中
//存儲(chǔ)類
Storage storage_;
//自動(dòng)梯度元數(shù)據(jù)
std::unique_ptr<c10::AutogradMetaInterface> autograd_meta_ = nullptr;
TensorImpl維護(hù)一個(gè)存儲(chǔ)類(Storage)的對(duì)象(storage_)棘钞,即Storage表示了Tensor存儲(chǔ)空間照棋。
- Storage類在
c10/core/Storage.h
中
c10::intrusive_ptr<StorageImpl> storage_impl_;
和Tensor、TensorImpl類比武翎,Storage類中包含StorageImpl類變量烈炭,pytorch真正意義上的存儲(chǔ)類是StorageImpl
- Storage類在
c10/core/StorageImpl.h
中
private:
DataPtr data_ptr_;//數(shù)據(jù)指針
size_t size_bytes_;//所占的空間大小
bool resizable_;
// Identifies that Storage was received from another process and doesn't have
// local to process cuda memory allocation
bool received_cuda_;
Allocator* allocator_;//分配器
其中,最重要的是分配器(Allocator)和數(shù)據(jù)指針(DataPtr)宝恶。
- DataPtr類和Allocator類的定義在
c10/core/Allocator.h
中
// A DataPtr is a unique pointer (with an attached deleter and some
// context for the deleter) to some memory, which also records what
// device is for its data.
Dataptr指向一段內(nèi)存符隙,并且記錄了數(shù)據(jù)所在設(shè)備和內(nèi)存的析構(gòu)函數(shù)
class DataPtr
private:
c10::detail::UniqueVoidPtr ptr_;//指向內(nèi)存的指針并帶有析構(gòu)函數(shù)
Device device_;
class UniqueVoidPtr
private:
// Lifetime tied to ctx_
void* data_;
//當(dāng)data_指針生命周期到趴捅,自動(dòng)執(zhí)行DeleteFnPtr函數(shù)
std::unique_ptr<void, DeleterFnPtr> ctx_;
因此數(shù)據(jù)指針DataPtr三個(gè)重要部分是void* data
指針、設(shè)備類型霹疫、deleter拱绑。
struct C10_API Allocator {
virtual ~Allocator() = default;
//分配指定大小的內(nèi)存,返回DataPtr丽蝎,用來Storage使用
virtual DataPtr allocate(size_t n) const = 0;
virtual DeleterFnPtr raw_deleter() const {
return nullptr;
}
//分配指定大小的內(nèi)存猎拨,返回指針
void* raw_allocate(size_t n) {
auto dptr = allocate(n);
AT_ASSERT(dptr.get() == dptr.get_context());
return dptr.release_context();
}
//釋放指針指向空間
void raw_deallocate(void* ptr) {
auto d = raw_deleter();
AT_ASSERT(d);
d(ptr);
}
};
Allocator
類作為分配類的基類,有三個(gè)重要的函數(shù):分配返回原始指針屠阻、分配返回DataPtr红省、釋放空間。其中virtual DataPtr allocate(size_t n) const = 0;
虛擬方法供不同的繼承類實(shí)現(xiàn)国觉。pytorch實(shí)現(xiàn)了不同內(nèi)存的分配類吧恃。
- DefaultCPUAllocator類在
c10/core/CPUAllocator.cpp
中
分配CPU內(nèi)存
at::DataPtr allocate(size_t nbytes) const override {
void* data = alloc_cpu(nbytes);
profiledCPUMemoryReporter().New(data, nbytes);
return {data, data, &ReportAndDelete, at::Device(at::DeviceType::CPU)};
}
其中分配函數(shù)alloc_cpu調(diào)用 posix_memalign
或者_aligned_malloc
或者memalign
void* alloc_cpu(size_t nbytes) {
...
void* data;
#ifdef __ANDROID__
data = memalign(gAlignment, nbytes);
#elif defined(_MSC_VER)
data = _aligned_malloc(nbytes, gAlignment);
#else
int err = posix_memalign(&data, gAlignment, nbytes);
...
#endif
...
return data;
}
- THCCachingHostAllocator類在
aten/src/THC/THCCachingHostAllocator.cpp
中
分配pinned memory - CudaCachingAllocator類在
c10/cuda/CUDACachingAllocator.cpp
中
分配GPU內(nèi)存
2.數(shù)據(jù)讀入
數(shù)據(jù)讀入深度學(xué)習(xí)程序運(yùn)行的第一個(gè)操作。數(shù)據(jù)讀入也就是把磁盤中的數(shù)據(jù)讀入內(nèi)存麻诀,又可以分為兩個(gè)部分:第一步痕寓,在內(nèi)存中分配空間,寫入分配的內(nèi)存中蝇闭。
在pytorch呻率,數(shù)據(jù)都是以Tensor形式存在的。因此呻引,數(shù)據(jù)的輸入以創(chuàng)建Tensor再填充Tensor的形式礼仗。填充Tensor也就是對(duì)Tensor的數(shù)據(jù)內(nèi)存上賦值,通過數(shù)據(jù)指針訪問內(nèi)存苞七。
創(chuàng)建Tensor
創(chuàng)建Tensor主要是分配Tensor所需的空間
pytorch提供empty
函數(shù)創(chuàng)建一個(gè)Tensor
在cpu上的實(shí)現(xiàn)代碼在aten/src/ATen/native/TensorFactories.cpp
中
在cuda上的實(shí)現(xiàn)代碼aten/src/ATen/native/cuda/TensorFactories.cu
中
empty的主要流程可以分為4個(gè)部分藐守。
1.分配器
c10::Allocator* allocator;
2.計(jì)算分配空間
int64_t size_bytes = nelements * dtype.itemsize();
3.構(gòu)建storageImpl對(duì)象
auto storage_impl = c10::make_intrusive<StorageImpl>(
c10::StorageImpl::use_byte_size_t(),
size_bytes,
allocator->allocate(size_bytes),//返回DataPtr對(duì)象
allocator,
/*resizeable=*/true);
4.構(gòu)建Tensor
auto tensor = detail::make_tensor<TensorImpl>(
std::move(storage_impl), at::DispatchKey::CPU, dtype);
CIFAR10數(shù)據(jù)集輸入
CIFAR10二進(jìn)制文件
一個(gè)CIFAR10二進(jìn)制文件包括10000條記錄,每一條記錄包括label和數(shù)據(jù)蹂风,數(shù)據(jù)是一張33232的圖片的像素值卢厂,二進(jìn)制文件以這樣的格式存儲(chǔ)。因此文件大小是10000*3073個(gè)字節(jié)
<1×標(biāo)簽> <3072×像素>
....
<1×標(biāo)簽> <3072×像素>
CIFAR10數(shù)據(jù)輸入
1.創(chuàng)建10000*3*32*32的圖像Tensor和10000的labelTensor
images_ = torch::empty({10000, 3,32, 32}, torch::kByte);
targets_ = torch::empty(10000, torch::kByte);
2.根據(jù)目錄創(chuàng)建輸入流
std::ifstream reader(path,std::ios::binary);
for(int i=0;i<10000;i++){
3.輸入流讀取指定的字節(jié)到Tensor的數(shù)據(jù)指針指向的內(nèi)存
reader.read(reinterpret_cast<char*>(targets_.data_ptr())+i/**偏移值**/,1);
reader.read(reinterpret_cast<char*>(images_.data_ptr())+i*3072/**偏移值**/,3072);
}
代碼創(chuàng)建的Images_,targets_是一個(gè)二進(jìn)制文件的輸入Tensor惠啄,包含10000張圖片慎恒。之后如果設(shè)置batch size,可以根據(jù)偏移取輸入Tensor的子Tensor作為一個(gè)iteration的輸入撵渡。
3.計(jì)算
深度學(xué)習(xí)的核心部分就是計(jì)算操作融柬,深度學(xué)習(xí)的訓(xùn)練過程包括了一系列的數(shù)學(xué)運(yùn)算。pytorch里實(shí)現(xiàn)成百上千個(gè)計(jì)算子趋距,包括簡(jiǎn)單的加減數(shù)學(xué)運(yùn)算和復(fù)雜的卷積操作等粒氧。
1.前向計(jì)算函數(shù)
static inline Tensor mul(const Tensor & self, const Tensor & other);
static inline Tensor & mul_out(Tensor & out, const Tensor & self, const Tensor & other);
static inline Tensor mul(const Tensor & self, Scalar other);
static inline Tensor mv(const Tensor & self, const Tensor & vec);
2后向計(jì)算函數(shù)
variable_list MulBackward0::apply(variable_list&& grads) ;
variable_list MvBackward::apply(variable_list&& grads);
在pytorch中,最簡(jiǎn)單的計(jì)算過程代碼是
y = net(input);
y.backward();
這兩句分別代表了前向計(jì)算和后向計(jì)算节腐。
網(wǎng)絡(luò)結(jié)構(gòu)
網(wǎng)絡(luò)由layer組成外盯,layer封裝了計(jì)算子和參數(shù)摘盆。
例
net是由三層全連接層組成的。
struct net : torch::nn::Module{
torch::Tensor forward(Torch::Tensor x){
torch::Tensor x1 = fc1->forward(x);
torch::Tensor x2 = fc2->forward(x);
torch::Tensor x3 = fc3->forward(x);
}
torch::nn::Linear fc1;
torch::nn::Linear fc2;
torch::nn::Linear fc2;
};
前向計(jì)算
net的前向計(jì)算依次經(jīng)過三層全連接層的forward
那么具體全連接層的forward執(zhí)行什么樣的操作饱苟,先了解全連接層的類定義
class TORCH_API LinearImpl : public Cloneable<LinearImpl> {
public:
...
Tensor forward(const Tensor& input);
LinearOptions options;
Tensor weight;
Tensor bias;
...
};
看代碼可以知道LinearImpl類中含有參數(shù)weight和bias孩擂,由此可見,網(wǎng)絡(luò)的參數(shù)其實(shí)是由每一層的參數(shù)組成箱熬。再看一下forward函數(shù)的具體實(shí)現(xiàn)
return torch::addmm(bias, input, weight.t());
追蹤代碼类垦,發(fā)現(xiàn)foward函數(shù)最終調(diào)用的addmm計(jì)算操作,輸入為linear層的輸入城须,參數(shù)weight和bias蚤认。
那么這個(gè)網(wǎng)絡(luò)的前向計(jì)算也可以展開為
類似的,復(fù)雜的網(wǎng)絡(luò)前向計(jì)算過程也可以看成參數(shù)酿傍、輸入烙懦、中間數(shù)據(jù)經(jīng)過計(jì)算子的計(jì)算圖
后向計(jì)算
后向計(jì)算的作用是更新參數(shù)的梯度驱入,更新梯度需要用到中間數(shù)據(jù)赤炒,因此中間數(shù)據(jù)暫存在內(nèi)存中。x1雖然在執(zhí)行第三次addmm操作時(shí)沒有被訪問亏较,但是需要暫存在內(nèi)存等待后向傳播時(shí)使用莺褒。
前向傳播的數(shù)學(xué)公式為
后向傳播的數(shù)學(xué)公式為
從公式中可以發(fā)現(xiàn),后向傳播的計(jì)算遵從鏈?zhǔn)椒▌t雪情,梯度從后向前傳播遵岩。
根據(jù)圖和公式,
addmm前向計(jì)算的輸入是w1巡通、x尘执、b1,輸出是x1
后向傳播的輸入是w1宴凉、x誊锭、b1和(目標(biāo)值對(duì)前向輸出x1的梯度),輸出是目標(biāo)值對(duì)w1弥锄、x丧靡、b1的梯度
那么pytorch是怎么實(shí)現(xiàn)這樣的計(jì)算呢
與操作前向相對(duì)應(yīng)的,pytorch也實(shí)現(xiàn)了操作的后向計(jì)算籽暇,例如addmm
的后向函數(shù)是AddmmBackward
但是温治,與前向不同的時(shí),后向傳播搭建顯示的靜態(tài)計(jì)算圖戒悠。構(gòu)成靜態(tài)圖的節(jié)點(diǎn)是后向操作熬荆,通過邊連接后向傳播的輸入輸出。例子的后向過程就如下圖所示
后向函數(shù)的輸入除了前一個(gè)后向函數(shù)的輸出绸狐,還有前向函數(shù)的輸入卤恳,也就是說前向函數(shù)的輸入也是后向函數(shù)的輸入捏顺。
那么后向是怎么通過后向操作構(gòu)建計(jì)算流程的呢,后向操作又是怎么訪問前向操作的輸入呢纬黎?
Node是一個(gè)關(guān)鍵的類幅骄,定義在
torch/csrc/autograd/function.h
中各種后向操作都繼承了這個(gè)類,
AddmmBackward
也是繼承了Node
類本今。在Tensor類定義中,有g(shù)rad_fn成員函數(shù)
const std::shared_ptr<torch::autograd::Node>& grad_fn() const;
grad_fn理解為梯度函數(shù)拆座,這個(gè)函數(shù)的意思是,tensor生成操作的后向函數(shù)冠息。例如x1由前向傳播圖中第一個(gè)addmm生成挪凑,
x1.grad_fn()
就是后向傳播圖上的第一個(gè)AddmmBackward
。x1的梯度函數(shù)是什么時(shí)候創(chuàng)建的呢逛艰,是在進(jìn)行前向計(jì)算的過程中躏碳。
1. 創(chuàng)建后向函數(shù)
std::shared_ptr<AddmmBackward> grad_fn;
if (compute_requires_grad( self, mat1, mat2 )) {
grad_fn = std::shared_ptr<AddmmBackward>(new AddmmBackward(), deleteNode);
grad_fn->set_next_edges(collect_next_edges( self, mat1, mat2 ));
2. 保存后向函數(shù)需要的輸入,這里的mat1散怖,mat2也是前向函數(shù)的輸入
grad_fn->mat1_ = SavedVariable(mat1, false);
grad_fn->mat2_ = SavedVariable(mat2, false);
grad_fn->alpha = alpha;
grad_fn->mat2_sizes = mat2.sizes().vec();
grad_fn->beta = beta;
}
3 進(jìn)行前向計(jì)算菇绵,result即為前向函數(shù)的輸出Tensor
auto tmp = ([&]() {
at::AutoNonVariableTypeMode non_var_type_mode(true);
return at::addmm(self_, mat1_, mat2_, beta, alpha);
})();
auto result = std::move(tmp);
4 設(shè)置result的梯度函數(shù)
if (grad_fn) {
set_history(flatten_tensor_args( result ), grad_fn);
}
經(jīng)過4個(gè)步驟,在進(jìn)行前向傳播的同時(shí)镇眷,對(duì)每一個(gè)輸出Tensor都構(gòu)建后向操作咬最,同時(shí)把輸入的數(shù)據(jù)放入后向操作變量中,對(duì)這些后向操作節(jié)點(diǎn)構(gòu)建圖(計(jì)算流程)欠动。
x3.backward()
這一句簡(jiǎn)單的后向傳播也就可以分解為永乌,
通過x3.grad_fn()獲得梯度函數(shù),執(zhí)行這個(gè)函數(shù)具伍,輸出三個(gè)梯度翅雏。
x3.grad_fn()的下一個(gè)節(jié)點(diǎn)是x2.grad_fn(),并且其中x3對(duì)x2的梯度作為x2.grad_fn()的一個(gè)輸入。最后執(zhí)行x1.grad_fn()人芽。
最終望几,參數(shù)w1、w2啼肩、w3橄妆、b1、b2祈坠、b3都獲得了梯度害碾。
附
后向傳播的代碼主要在torch/csrc/autograd
中