0
点赞
收藏
分享

微信扫一扫

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


引言

本着“凡我不能创造的,我就不能理解”的思想,​​本系列​​文章会基于纯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 条评论