从零实现深度学习框架实现自己的Tensor对象

2022-08-03 14:48:36

引言

本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。

要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部完备的框架前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。
本系列文章首发于微信公众号:JavaNLP

本文基于前面介绍的计算图知识,开始实现我们自己的深度学习框架。

就像PyTorch用Tensor来表示张量一样,我们也创建一个自己的Tensor

数据类型

由于我们自己的Tensor也需要进行矩阵运算,因此我们直接封装最常用的矩阵运算工具——NumPy。

首先,我们增加帮助函数来确保用到的数据类型为np.ndarray

# 默认数据类型
_type= np.float32# 可以转换为Numpy数组的类型
Arrayable= Union[float,list, np.ndarray]defensure_array(arrayable: Arrayable)-> np.ndarray:"""
    :param arrayable:
    :return:
    """ifisinstance(arrayable, np.ndarray):# 如果本身是ndarrayreturn arrayable# 转换为Numpy数组return np.array(arrayable, dtype=_type)

Tensor初探

所有的代码都尽量添加类型提示(Typing),已增加代码的可读性。接下来,创建我们自己的Tensor实现:

classTensor:def__init__(self, data: Arrayable, requires_grad:bool=False)->None:'''
        初始化Tensor对象
        Args:
            data: 数据
            requires_grad: 是否需要计算梯度
        '''# data 是 np.ndarray
        self._data= ensure_array(data)

        self.requires_grad= requires_grad# 保存该Tensor的梯度
        self._grad=Noneif self.requires_grad:# 初始化梯度
            self.zero_grad()# 用于计算图的内部变量
        self._ctx=None

调用ensure_array确保传过来的是一个Numpy数组。requires_grad表示是否需要计算梯度。

下面增加一些属性方法(属于上面Tensor类):

@propertydefgrad(self):return self._grad@propertydefdata(self)-> np.ndarray:return self._data@data.setterdefdata(self, new_data: np.ndarray)->None:
        self._data= ensure_array(new_data)# 重新赋值后就没有梯度了
        self._grad=None

通过@property来确保梯度是只读的,同时让保存的数据data是可读可写的,当修改data时,需要清空梯度。因为绑定的数据已经发生了变化。

我们知道Tensor作为张量,它是有形状(shape)、维度(dimension)等相关属性的,下面我们就来实现:

# ****一些常用属性****@propertydefshape(self)-> Tuple:'''返回Tensor各维度大小的元素'''return self.data.shape@propertydefndim(self)->int:'''返回Tensor的维度个数'''return self.data.ndim@propertydefdtype(self)-> np.dtype:'''返回Tensor中数据的类型'''return self.data.dtype@propertydefsize(self)->int:'''
        返回Tensor中元素的个数 等同于np.prod(a.shape)
        Returns:
        '''return self.data.size

Tensor的初始化方法中,有进行梯度初始化的方法,看一下是如何实现的:

defzero_grad(self)->None:'''
        将梯度初始化为0
        Returns:

        '''
        self._grad= Tensor(np.zeros_like(self.data, dtype=_type))

为了方便调试,我们实现了了__repr__方法。同时实现__len_魔法方法,返回数据的长度。

def__repr__(self)->str:returnf"Tensor({self.data}, requires_grad={self.requires_grad})"def__len__(self)->int:returnlen(self.data)

最后,实现两个比较有用的方法。

defassign(self, x)->"Tensor":'''将x的值赋予当前Tensor'''
        x= ensure_tensor(x)# 维度必须一致assert x.shape== self.shape
        self.data= x.datareturn selfdefnumpy(self)-> np.ndarray:"""转换为Numpy数组"""return self.data

assign用于给当前Tensor赋值,因为我们上面让data是只读了,所以需要额外提供这个方法。

numpy则是将当前Tensor对象转换为Numpy数组。

类似ensure_array,我们也提供了一个确保为Tensor的帮助方法。

Tensorable= Union["Tensor",float, np.ndarray]defensure_tensor(tensoralbe: Tensorable)->"Tensor":'''
    确保是Tensor对象
    '''ifisinstance(tensoralbe, Tensor):return tensoralbereturn Tensor(tensoralbe)

测试

写完代码进行测试是一个好习惯,我们今天暂且在__main__里面测试:

if __name__=='__main__':
    t= Tensor(range(10))print(t)print(t.shape)print(t.size)print(t.dtype)

输出:

Tensor([0. 1. 2. 3. 4. 5. 6. 7. 8. 9.], requires_grad=False)
(10,)
10
float32

完整代码

完整代码笔者上传到了程序员最大交友网站上去了,地址:👉 https://github.com/nlp-greyfoss/metagrad

总结

本文我们实现了Tensor对象的基本框架,下篇文章就会学习如何实现基本的反向传播。

  • 作者:愤怒的可乐
  • 原文链接:https://helloai.blog.csdn.net/article/details/122054811
    更新时间:2022-08-03 14:48:36