本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。
要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部完备的框架前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。
本系列文章首发于微信公众号:JavaNLP
本文基于前面介绍的计算图知识,开始实现我们自己的深度学习框架。
就像PyTorch用Tensor来表示张量一样,我们也创建一个自己的Tensor。
数据类型由于我们自己的Tensor也需要进行矩阵运算,因此我们直接封装最常用的矩阵运算工具——NumPy。
首先,我们增加帮助函数来确保用到的数据类型为np.ndarray。
# 默认数据类型 _type = np.float32 # 可以转换为Numpy数组的类型 Arrayable = Union[float, list, np.ndarray] def ensure_array(arrayable: Arrayable) -> np.ndarray: """ :param arrayable: :return: """ if isinstance(arrayable, np.ndarray): # 如果本身是ndarray return arrayable # 转换为Numpy数组 return np.array(arrayable, dtype=_type)Tensor初探
所有的代码都尽量添加类型提示(Typing),已增加代码的可读性。接下来,创建我们自己的Tensor实现:
class Tensor: 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 = None if self.requires_grad: # 初始化梯度 self.zero_grad() # 用于计算图的内部变量 self._ctx = None
调用ensure_array确保传过来的是一个Numpy数组。requires_grad表示是否需要计算梯度。
下面增加一些属性方法(属于上面Tensor类):
@property def grad(self): return self._grad @property def data(self) -> np.ndarray: return self._data @data.setter def data(self, new_data: np.ndarray) -> None: self._data = ensure_array(new_data) # 重新赋值后就没有梯度了 self._grad = None
通过@property来确保梯度是只读的,同时让保存的数据data是可读可写的,当修改data时,需要清空梯度。因为绑定的数据已经发生了变化。
我们知道Tensor作为张量,它是有形状(shape)、维度(dimension)等相关属性的,下面我们就来实现:
# ****一些常用属性**** @property def shape(self) -> Tuple: '''返回Tensor各维度大小的元素''' return self.data.shape @property def ndim(self) -> int: '''返回Tensor的维度个数''' return self.data.ndim @property def dtype(self) -> np.dtype: '''返回Tensor中数据的类型''' return self.data.dtype @property def size(self) -> int: ''' 返回Tensor中元素的个数 等同于np.prod(a.shape) Returns: ''' return self.data.size
在Tensor的初始化方法中,有进行梯度初始化的方法,看一下是如何实现的:
def zero_grad(self) -> None: ''' 将梯度初始化为0 Returns: ''' self._grad = Tensor(np.zeros_like(self.data, dtype=_type))
为了方便调试,我们实现了了__repr__方法。同时实现__len_魔法方法,返回数据的长度。
def __repr__(self) -> str: return f"Tensor({self.data}, requires_grad={self.requires_grad})" def __len__(self) -> int: return len(self.data)
最后,实现两个比较有用的方法。
def assign(self, x) -> "Tensor": '''将x的值赋予当前Tensor''' x = ensure_tensor(x) # 维度必须一致 assert x.shape == self.shape self.data = x.data return self def numpy(self) -> np.ndarray: """转换为Numpy数组""" return self.data
assign用于给当前Tensor赋值,因为我们上面让data是只读了,所以需要额外提供这个方法。
numpy则是将当前Tensor对象转换为Numpy数组。
类似ensure_array,我们也提供了一个确保为Tensor的帮助方法。
Tensorable = Union["Tensor", float, np.ndarray] def ensure_tensor(tensoralbe: Tensorable) -> "Tensor": ''' 确保是Tensor对象 ''' if isinstance(tensoralbe, Tensor): return tensoralbe return 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对象的基本框架,下篇文章就会学习如何实现基本的反向传播。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)