《深度学习入门——自制框架》读书笔记 第一章自动微分

1. 自动微分

step2 创建变量的函数

# 箱子类,存放一个变量数据
class Variable: 
	def __init__(self, data):
		self.data = data

# 函数类的基类
class Function:
    # __call__方法是一个特殊的Python方法。
    # 定义了这个方法后,当f = Function()时,就可以通过编写f(...)来调用__call__方法了
	def __call__(self, input):
		x = input.data
		y = self.forward(x)  # 具体的计算在forward方法中进行
		output = Variable(y)
		return output

	def forward(self, x):
		raise NotImplementedError()  # 告诉使用了Function类forward方法的人这个方法应该通过继承来实现

# 函数类的具体实现类
class Square(Function):
	def forward(self, x):
		return x ** 2


if __name__ == "__main__":
	x = Variable(10)
	f = Square()
	y = f(x)
	print(type(y))
	print(y.data)

step4 数值微分

利用中心差分近似来计算导数:

\[f'(x) \approx \frac{f(x+ \epsilon)-f(x- \epsilon)}{2 \epsilon} \]

设计numerical_diff函数:

class Variable: 
	def __init__(self, data):
		self.data = data


class Function:
	def __call__(self, input):
		x = input.data
		y = self.forward(x)  # 具体的计算在forward方法中进行
		output = Variable(y)
		return output

	def forward(self, x):
		raise NotImplementedError()  # 告诉使用了Function类forward方法的人这个方法应该通过继承来实现


class Square(Function):
	def forward(self, x):
		return x ** 2


def numerical_diff(f, x, eps=1e-4):
	x0 = Variable(x.data - eps)
	x1 = Variable(x.data + eps)
	y0 = f(x0)
	y1 = f(x1)
	return (y1.data - y0.data) / (2 * eps)


if __name__ == "__main__":
	f = Square()
	x = Variable(2.0)
	dy = numerical_diff(f, x)
	print(dy)

复合函数的导数

def f(x):
    A = Square()
    B = Exp()
    C = Square()
    return C(B(A(x)))


if __name__ == "__main__":
	x = Variable(np.array(0.5))
	dy = numerical_diff(f, x)
	print(dy)

step6 手动进行反向传播

用反向传播计算导数

import numpy as np


class Variable: 
	def __init__(self, data):
		self.data = data
		self.grad = None  # gradient梯度的缩写

class Function:
	def __call__(self, input):
		x = input.data
		y = self.forward(x)  # 具体的计算在forward方法中进行
		output = Variable(y)
		self.input = input  # 保存输入的变量
		# 将input设置为实例变量。
		# 这样一来,当调用backward方法时,向函数输入的Variable实例就可以作为self.input使用

		return output

	def forward(self, x):
		raise NotImplementedError()  # 告诉使用了Function类forward方法的人这个方法应该通过继承来实现

	def backward(self, gy):
		raise NotImplementedError()


class Square(Function):
	def forward(self, x):
		y = x ** 2
		return y

	def backward(self, gy):
		x = self.input.data
		gx = 2 * x * gy
		return gx


class Exp(Function):
	def forward(self, x):
		return np.exp(x)

	def backward(self, gy):
		x = self.input.data
		gx = np.exp(x) * gy
		return gx


if __name__ == "__main__":
	A = Square()
	B = Exp()
	C = Square()

	x = Variable(np.array(0.5))
	a = A(x)
	b = B(a)
	y = C(b)

	y.grad = np.array(1.0)  # dy/dy = 1
	b.grad = C.backward(y.grad)
	a.grad = B.backward(b.grad)
	x.grad = A.backward(a.grad)

	print(x.grad)

step7 反向传播的自动化

import numpy as np


class Variable: 
	def __init__(self, data):
		self.data = data
		self.grad = None  # gradient梯度的缩写
		self.creator = None

	def set_creator(self, func):
		self.creator = func

	def backward(self):
		f = self.creator  # 1. 获取函数
		if f is not None:
			x = f.input  # 2. 获取函数的输入
			x.grad = f.backward(self.grad)  # 3. 调用函数的backward方法
			x.backward()  # 调用自己前面那个变量的backward方法(递归)


class Function:
	def __call__(self, input):
		x = input.data
		y = self.forward(x)  # 具体的计算在forward方法中进行
		output = Variable(y)
		output.set_creator(self)  # 让输出变量保存创造者信息
		self.input = input  # 保存输入的变量
		# 将input设置为实例变量。
		# 这样一来,当调用backward方法时,向函数输入的Variable实例就可以作为self.input使用

		self.output = output  # 也保存输出变量
		return output

	def forward(self, x):
		raise NotImplementedError()  # 告诉使用了Function类forward方法的人这个方法应该通过继承来实现

	def backward(self, gy):
		raise NotImplementedError()


class Square(Function):
	def forward(self, x):
		y = x ** 2
		return y

	def backward(self, gy):
		x = self.input.data
		gx = 2 * x * gy
		return gx


class Exp(Function):
	def forward(self, x):
		return np.exp(x)

	def backward(self, gy):
		x = self.input.data
		gx = np.exp(x) * gy
		return gx


if __name__ == "__main__":
	A = Square()
	B = Exp()
	C = Square()

	x = Variable(np.array(0.5))
	a = A(x)
	b = B(a)
	y = C(b)

	y.grad = np.array(1.0)  # dy/dy = 1
	y.backward()

	print(x.grad)

step8 从递归到循环

用循环的方式实现backward

修改之后的backward如下

class Variable: 
    ... 
	def backward(self):
		funcs = [self.creator]  # 1. 获取函数
		while funcs:
			f = funcs.pop()  # 获取函数
			x, y = f.input, f.output  # 2. 获取函数的输入
			x.grad = f.backward(y.grad)  # 3. 调用函数的backward方法
			
			if x.creator is not None:
				funcs.append(x.creator)  # 将前一个函数添加到列表中

step9 让函数更易用

主要3点改进:

  1. 把函数类变成python函数
  2. 简化backward方法
  3. 让Variable只支持ndarray类型

函数类变成python函数

def square(x):
  f = Square()
  return f(x)

# 或者
def square(x):
  return Square()(x)

这样在调用平方函数时就可以简化为:

y = square(x)

也支持连续调用

y = square(exp(square(x)))

简化backward方法

之前每次调用backward时都得手动设置一次y.grad = np.array(1.0),对此可以简化为:

class Variable: 
    ...
	def backward(self):
        # ================ 新加的代码 ====================
        if self.grad is None:
            self.grad = np.ones_like(self.data)
            # ones_like创建一个ndarray实例,其形状和数据类型与self.data相同,数据全为1
            # 即若self=[0.2 0.22],则创建实例[1, 1]
        # ===============================================
		funcs = [self.creator]  # 1. 获取函数
		while funcs:
			f = funcs.pop()  # 获取函数
			x, y = f.input, f.output  # 2. 获取函数的输入
			x.grad = f.backward(y.grad)  # 3. 调用函数的backward方法
			
			if x.creator is not None:
				funcs.append(x.creator)  # 将前一个函数添加到列表中

只支持ndarray

需要改三个地方:

  1. 在Variable类中进行类型判断
if data is not None:
  if not isinstance(data, np.ndarray):
    raise TypeError('{} is not supported'.format(type(data)))
  1. 添加as_array函数
def as_array(x):
	if np.isscalar(x):
		return np.array(x)
	return x

之所以需要这个函数,是因为NumPy的工作机制:零维ndarray进行数值运算之后将产生ndarray之外的类型(如numpy.float64、numpy.float32)。

  1. Function类中的output = Variable(y)改为output = Variable(as_array(y))
posted @ 2023-12-01 19:49  码鸽  阅读(22)  评论(0编辑  收藏  举报