自制深度学习框架
一、深度学习数据结构设计
在构建深度学习框架时,数据结构是整个框架设计的起点。本章选择 ndarray 作为底层基础,其原因在于,它已经提供了成熟且高效的多维数组计算能力,包括向量化运算、广播机制以及底层的高性能实现。这使得我们无需重新实现底层数值计算,就可以直接获得接近工业级的计算效率,从而将精力集中在框架层的设计上。然而,深度学习并不仅仅是数值计算问题,还涉及“计算过程的管理”。例如,张量之间的依赖关系、梯度传播路径、以及模型参数的更新机制,这些都是 ndarray 本身所不具备的能力。换言之,ndarray 只解决了“算得快”的问题,但没有解决“怎么学”的问题。因此,本章在 ndarray 的基础上进行封装,引入 Tensor 这一新的数据结构,使其不仅保留高效的数值计算能力,同时也具备进入深度学习计算系统所需的语义扩展能力。通过这一改造,数据从单纯的数值载体,升级为能够参与计算图与学习过程的基本单元。
1.1 Tensor数据结构
Tensor的最底层必须依赖ndarray,因为所有的计算都由它完成。因此,Tensor的第一步完成ndarray封装。
class Tensor:
def __init__(self, data):
self.data = data
二、基于 Tensor 的计算设计
深度学习框架的本质不仅在于“存储数据”,更在于“定义计算过程”。因此,仅有 Tensor 这一数据结构是不够的,还需要在其之上进一步设计一套基于 Tensor 的计算机制,使数据能够在不同操作之间流动、组合,并逐步构建为可用于学习的计算结构。
2.1 Function 的结构设计
本章通过引入类来封装计算过程,其核心目的在于将神经网络的前向传播与反向传播过程统一为可管理的对象结构,使计算过程能够被保存、追踪与复用,从而为后续的自动求导与模型优化提供基础。
2.1.1 前向与反向传播
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Tensor(y)
return output
def forward(self, x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.input.data
return np.exp(x)
2.2 单变量自动求导

2.2.1 单变量计算图
从上图可以看出,一个变量(Variable)需要存储三个部分:ndarray、梯度以及生成该变量的函数,它们各自的作用如下:
- ndarray:用于存储变量的实际数据,是前向传播过程中参与各种数学运算的对象。
- 梯度:用于存储损失函数对该变量的偏导数。
- 生成该变量的函数:用于记录生成当前变量的函数对象。当执行反向传播时,可以根据该函数找到当前变量的来源,并调用相应的反向传播方法计算输入变量的梯度,从而将梯度沿计算图逐层传递,实现自动求导。
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.grad)
func = [self.creator]
while func:
f = func.pop()
x, y = f.input, f.output
x.grad = f.backward(y.grad)
if x.creator is not None:
func.append(x.creator)
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Tensor(as_ndarray(y))
output.set_creator(self)
self.input = input
self.output = output
return output
def forward(self, x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.input.data
return np.exp(x)
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
2.3 多变量自动求导
前面介绍的自动求导仅适用于单变量计算,而实际的深度学习模型通常包含多个输入变量,各变量之间存在复杂的计算关系。为了能够处理更加复杂的计算图,并实现神经网络中多输入的梯度计算,需要将自动求导机制从单变量扩展到多变量。因此,本节将在单变量自动求导的基础上,引入多变量自动求导,使其能够支持更加复杂的数学运算。
2.3.1 可变参数
本章节之前涉及的函数,其输入、输出都只有一个变量,但有些函数需要多个变量输入,例如加法运算和乘法运算。可考虑到这些情况,我们需要将单变量自动求导,处理成可变输入和输出。可变意味着参数的数量是可以变化的,数量可以是不小于1的整数值。
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.grad)
func = [self.creator]
while func:
f = func.pop()
dys = [output.grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
x.grad = dx
if x.creator is not None:
func.append(x.creator)
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
if output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
2.3.2 重复变量
(1) 问题描述
相同变量进行加法运算时,不能正确求导。下面是测试结果:
if __name__ == "__main__":
x = Tensor(np.array(3.0))
y = add(x, x)
print(y.data)
y.backward()
print(x.grad)
测试结果:
6.0
1
(2) 优化方法
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.grad)
func = [self.creator]
while func:
f = func.pop()
dys = [output.grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
func.append(x.creator)
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
if output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
2.3.3 导数重置
(1) 问题描述
相同变量进行不同计算时,不能正确求导。下面是测试结果:
if __name__ == "__main__":
# 第一个计算
x = Tensor(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad)
# 第二个计算
y = add(add(x, x), x)
y.backward()
print(x.grad)
测试结果:
2
5
(2) 优化方法
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def zero_grad(self):
self.grad = None # 进行程序前,对梯度清零
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.grad)
funcs = [self.creator]
while funcs:
f = funcs.pop()
dys = [output.grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
func.append(x.creator)
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
2.3.4 传播顺序
前面我们处理的都是笔直得计算图,然而变量与函数并不局限于这种简单得连接方式。我们可以建立复杂的连接,但是不能正确求出这种连接方式的导数,即不能反向传播。

从图2.2可以看出,变量 a 是后续两个分支(B 和 C)的共同输入,因此其梯度应当是两条传播路径梯度的累加。然而,图中的导数传播顺序与神经网络反向传播的实际过程并不一致,不能正确反映梯度沿计算图逐层反向传播的特点。

本章节在正向传播中实现层级设置,然后在反向传播中,按照从最高层级到最低层级的顺序取函数。修改完成后,无论计算图多么复杂,反向传播都会按照正确的顺序进行。
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
self.layers = 0 # 层级
def set_creator(self, func):
self.creator = func
self.layers = func.layers + 1
def zero_grad(self):
self.grad = None # 进行程序前,对梯度清零
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.grad)
# 优先队列法(未实现)
funcs, seen_set = [], set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f) # 防止重复调用
funcs.sort(key=lambda x: x.layers)
add_func(self.creator)
while funcs:
f = funcs.pop()
dys = [output.grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
add_func(x.creator)
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
# 通过输入变量层级记录函数层级
self.layers = max([input.layers for input in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
2.4 内存管理
我们写的 Python 代码本身不能直接被计算机执行,需要通过一个叫 Python 解释器 的程序来运行它。最常用的解释器是 CPython,它是用 C 语言实现的,也是 Python 官方默认的实现方式。简单来说,Python 负责让我们方便地编写程序,而 CPython 负责帮助计算机理解并执行这些程序。Cpython使用两种方式管理内存:一种引用计数,另一种是分代垃圾回收。
2.4.1 引用计数法
GC 是一种“释放怎么都无法被引用的对象的机制”。显然,可以让所有对象事先记录下“有多少程序引用自己”。让各对象知道自己的“人气指数”,从而让没有人气的对象自己消失,这就是引用计数法,它是George E. Collins于1960年钻研出来的。
2.4.2 分代垃圾回收
分代垃圾回收在对象中导入“年龄”的概念,通过优先回收容易成为垃圾的对象,提高垃圾回收效率。人们从众多程序案例中总结出一个经验:“大部对象在生成后马上变成垃圾”,很少有对象活得很久。分代垃圾回收利用该经验,在对象中导入年龄概念,经历一次GC后,活下来的对象年龄为1岁。Python的分代回收是一种垃圾回收策略,其核心思想是对象存活时间越长,以后存活的概率越大,新创建的对象更容易成为垃圾。因此,Python不会每次都扫描所有对象,而把对象按年龄划分,提高垃圾回收效率。

2.4.3 弱引用
对象的弱引用不能保证对象存活:当所指对象的引用只剩弱引用时, 垃圾回收 可以销毁所指对象,并将其内存重新用于其它用途。但是,在实际销毁对象之前,即使没有强引用,弱引用也能返回该对象。
import weakref
import numpy as np
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
self.layers = 0 # 层级
def set_creator(self, func):
self.creator = func
self.layers = func.layers + 1
def zero_grad(self):
self.grad = None # 进行程序前,对梯度清零
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.grad)
# 优先队列法(未实现)
funcs, seen_set = [], set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f) # 防止重复调用
funcs.sort(key=lambda x: x.layers)
add_func(self.creator)
while funcs:
f = funcs.pop()
dys = [output().grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
add_func(x.creator)
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
# 通过输入变量层级记录函数层级
self.layers = max([input.layers for input in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = [weakref.ref(output) for output in outputs]
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
2.4.4 清除无用导数
在深度学习中,只有终端变量的导数需要通过反向传播求得。中间变量的导数几乎用不到。因此,我们可以增加一种机制消除中间变量的导数。
import weakref
import numpy as np
class Tensor:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.creator = None
self.layers = 0 # 层级
def set_creator(self, func):
self.creator = func
self.layers = func.layers + 1
def zero_grad(self):
self.grad = None # 进行程序前,对梯度清零
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.grad)
# 优先队列法(未实现)
funcs, seen_set = [], set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f) # 防止重复调用
funcs.sort(key=lambda x: x.layers)
add_func(self.creator)
while funcs:
f = funcs.pop()
dys = [output().grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for output in f.outputs:
output().grad = None
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
# 通过输入变量层级记录函数层级
self.layers = max([input.layers for input in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = [weakref.ref(output) for output in outputs]
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def add(x0, x1):
return Add()(x0, x1)
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
if __name__ == "__main__":
x_0 = Tensor(np.array(1.0))
x_1 = Tensor(np.array(1.0))
t = add(x_0, x_1)
y = add(x_0, t)
y.backward()
print(y.grad, t.grad)
print(x_0.grad, x_1.grad)
2.5 模式切换
神经网络训练阶段既需要执行正向传播,也需要完成反向传播以计算参数梯度;而模型推理阶段仅需前向计算,无需构建梯度计算图、执行反向传播,因此需要临时关闭自动微分功能。本文借助 Python 的with上下文语句,实现 “禁用反向传播” 模式的临时切换。
import weakref
import contextlib
import numpy as np
class Tensor:
def __init__(self, data, name=None):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.name = name
self.creator = None
self.layers = 0 # 层级
def set_creator(self, func):
self.creator = func
self.layers = func.layers + 1
def clear_grad(self):
self.grad = None
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.grad)
# 优先队列法(未实现)
funcs, seen_set = [], set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f) # 防止重复调用
funcs.sort(key=lambda x: x.layers)
add_func(self.creator)
while funcs:
f = funcs.pop()
dys = [output().grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for output in f.outputs:
output().grad = None
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
if Config.enable_backprop:
# 通过输入变量层级记录函数层级
self.layers = max([input.layers for input in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = [weakref.ref(output) for output in outputs]
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def add(x0, x1):
return Add()(x0, x1)
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
class Config:
enable_backprop = True
@contextlib.contextmanager
def using_config(name, value):
old_value = getattr(Config, name)
setattr(Config, name, value)
try:
yield
finally:
setattr(Config, name, old_value)
def zero_grad():
return using_config("enable_backprop", False)
if __name__ == "__main__":
x_0 = Tensor(np.array(1.0))
x_1 = Tensor(np.array(1.0))
t = add(x_0, x_1)
y = add(x_0, t)
y.backward()
print(y.grad, t.grad)
print(x_0.grad, x_1.grad)
2.6 属性扩展
为了省去频繁书写.data 的冗余代码、对齐 PyTorch 的使用习惯,我们基于底层存储的 numpy 数组,借助 @property 扩展了 shape、ndim、size、dtype 四个只读属性,同时重载__len__、__repr__魔法方法。
import weakref
import contextlib
import numpy as np
class Tensor:
def __init__(self, data, name=None):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.name = name
self.creator = None
self.layers = 0 # 层级
@property
def shape(self):
return self.data.shape
@property
def ndim(self):
return self.data.ndim
@property
def size(self):
return self.data.size
@property
def dtype(self):
return self.data.dtype
def __len__(self):
return len(self.data)
def __repr__(self):
if self.data is None:
return 'Tensor(None)'
return f'Tensor({str(self.data)}, {self.data.dtype})'
def set_creator(self, func):
self.creator = func
self.layers = func.layers + 1
def clear_grad(self):
self.grad = None
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.grad)
# 优先队列法(未实现)
funcs, seen_set = [], set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f) # 防止重复调用
funcs.sort(key=lambda x: x.layers)
add_func(self.creator)
while funcs:
f = funcs.pop()
dys = [output().grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for output in f.outputs:
output().grad = None
class Function:
def __call__(self, *inputs):
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
if Config.enable_backprop:
# 通过输入变量层级记录函数层级
self.layers = max([input.layers for input in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = [weakref.ref(output) for output in outputs]
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def add(x0, x1):
return Add()(x0, x1)
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
class Config:
enable_backprop = True
@contextlib.contextmanager
def using_config(name, value):
old_value = getattr(Config, name)
setattr(Config, name, value)
try:
yield
finally:
setattr(Config, name, old_value)
def zero_grad():
return using_config("enable_backprop", False)
if __name__ == "__main__":
x_0 = Tensor(np.array([[1.0, 2.9], [2.0, 9.0]]))
print(x_0)
2.7 运算符重载
运算符重载的目的是让自定义张量类像普通数字一样,直接使用数学符号运算,代码贴合数学公式、简洁易读,同时统一封装前向、反向传播的计算图逻辑。
import weakref
import contextlib
import numpy as np
class Tensor:
def __init__(self, data, name=None):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError(f"{type(data)} is not supported!")
self.data = data
self.grad = None
self.name = name
self.creator = None
self.layers = 0 # 层级
@property
def shape(self):
return self.data.shape
@property
def ndim(self):
return self.data.ndim
@property
def size(self):
return self.data.size
@property
def dtype(self):
return self.data.dtype
def __len__(self):
return len(self.data)
def __repr__(self):
if self.data is None:
return 'Tensor(None)'
return f'Tensor({str(self.data)}, {self.data.dtype})'
def set_creator(self, func):
self.creator = func
self.layers = func.layers + 1
def clear_grad(self):
self.grad = None
def __mul__(self, other):
return mul(self, other)
def __add__(self, other):
return add(self, other)
def __neg__(self):
return neg(self)
def __sub__(self, other):
return sub(self, other)
def __truediv__(self, other):
return div(self, other)
def __pow__(self, c):
return power(self, c)
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.grad)
# 优先队列法(未实现)
funcs, seen_set = [], set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f) # 防止重复调用
funcs.sort(key=lambda x: x.layers)
add_func(self.creator)
while funcs:
f = funcs.pop()
dys = [output().grad for output in f.outputs]
dxs = f.backward(*dys)
if not isinstance(dxs, tuple):
dxs = (dxs,)
for x, dx in zip(f.inputs, dxs):
# 重复值优化
if x.grad is None:
x.grad = dx
else:
x.grad = x.grad + dx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for output in f.outputs:
output().grad = None
class Function:
def __call__(self, *inputs):
inputs = [as_tensor(input) for input in inputs]
xs = [input.data for input in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Tensor(as_ndarray(y)) for y in ys]
if Config.enable_backprop:
# 通过输入变量层级记录函数层级
self.layers = max([input.layers for input in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = [weakref.ref(output) for output in outputs]
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, *x):
raise NotImplementedError
def backward(self, dy):
raise NotImplementedError
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, dy):
x = self.inputs[0].data
return np.exp(x)
class Add(Function):
def forward(self, x_0, x_1):
y = x_0 + x_1
return y
def backward(self, dy):
return dy, dy
def add(x0, x1):
return Add()(x0, x1)
class Mul(Function):
def forward(self, x_0, x_1):
y = x_0 * x_1
return y
def backward(self, dy):
x_0, x_1 = self.inputs[0].data, self.inputs[1].data
return x_1 * dy, x_0 * dy
def mul(x_0, x_1):
return Mul()(x_0, x_1)
class Neg(Function):
def forward(self, x):
return -x
def backward(self, dy):
return -dy
def neg(x):
return Neg()(x)
class Sub(Function):
def forward(self, x_0, x_1):
y = x_0 - x_1
return y
def backward(self, dy):
return dy, -dy
def sub(x_0, x_1):
return Sub()(x_0, x_1)
class Div(Function):
def forward(self, x_0, x_1):
y = x_0 / x_1
return y
def backward(self, dy):
x0, x1 = self.inputs[0].data, self.inputs[1].data
return dy/x1, (-x0/x1**2)*dy
def div(x0, x1):
return Div()(x0, x1)
class Pow(Function):
def __init__(self, c):
self.c = c
def forward(self, x):
y = x ** self.c
return y
def backward(self, dy):
x = self.inputs[0].data
return dy * self.c * x ** (self.c - 1)
def power(x, c):
return Pow(c)(x)
def as_ndarray(x):
if np.isscalar(x):
return np.array(x)
return x
def as_tensor(tensor):
if isinstance(tensor, Tensor):
return tensor
else:
return Tensor(np.array(tensor))
class Config:
enable_backprop = True
@contextlib.contextmanager
def using_config(name, value):
old_value = getattr(Config, name)
setattr(Config, name, value)
try:
yield
finally:
setattr(Config, name, old_value)
def zero_grad():
return using_config("enable_backprop", False)
if __name__ == "__main__":
x_0 = Tensor(np.array([0.1, 0.1]))
x_1 = 2
print(x_0**2)
三、Python打包
3.1 概述
Python打包在很长一段时间内处于混乱不堪的状态,人们花了很多年才使得这一主题重新变得有组织。一切都从1998年引入的distutils包开始,随后在2003年setuptools对其进行改进。这两个项目开启了一段漫长而有纠结的故事,故事包括派生fork、替代项目与完全重写编写,都想要彻底修复Python打包生态。但是,大部分尝试都没有成功。
幸运的是,这种状态正在逐渐被改变。有一个Python Packaging Authority组织,将秩序和组织性带回到打包生态系统中。PyPA维护的Python打包用户指南(Python Packaging User Guide,https://packaging.python.org)是关于最新打包工具和最佳实践的权威信息来源。由于PyPA的参与,构建发行版已经逐渐弃用egg格式,二十支持使用wheel格式。在Python打包用户指南中,推荐包创建与分发的工具如下。
- 使用setuptools来定义项目,并创建源代码发行版。
- 使用wheel而不是egg来创建发行版。
- 使用twine向PyPI上传包的发行版。
3.2 项目配置
组织大型应用的代码最简单的方法是将其分成几个包。这使得代码更加简单,也更容易理解、维护和修改。这样也使得每个包的可复用性最大化。它们的作用就像组件一样。
- setup.py
对于一个需要被分发的包来说,其根目录包含一个setup.py脚本。它定义了distutils模块中描述的所有元数据,并将其合并为标准的setup()函数调用的参数。虽然distutils 是一个标准库模块,但建议你使用setuptools 包来代替,它对标准的distutils 做了一些改进。
from setuptools import setup
setup(
name="PyTensor",
version="1.0.0",
description="A lightweight deep learning tensor framework built from scratch based on NumPy",
author="Lee Wen-tsao",
author_email="liwenchao36@163.com",
packages=["PyTensor"],
install_requires=[
'numpy',
],
python_requires=">=3.11",
)
执行打包:
python setup.py bdist_wheel
595



被折叠的 条评论
为什么被折叠?



