用了差不多一天多的时间,把自动微分基本整明白了,并且实现(抄)了一遍。
李理大神的这篇博客讲的非常清楚明白,即使我这样的小白也能看明白:http://fancyerii.github.io/books/autodiff/ ,理论讲解都在这篇里了,这里不再赘述。
代码的话是参考这篇:https://zhuanlan.zhihu.com/p/161635270 ,找了 github 上的一些自动微分的实现,都是比较成系统的集成工具,东西很多很全面,一时半会儿也看不明白,只会感觉自己智商受到了碾压,只有这篇的实现比较适合小白练手。
自动微分其实就是利用链式求导法则,构建计算图,将函数拆解为原子操作,然后利用图算法进行梯度累加,不得不说,想出这种方法进行自动微分的人简直是天才,我这种智障连看懂都需要很久的时间,别说自己提出了orz
首先需要构建一个类,这种数据类型就是计算图中的节点 Node:
import math
class Node():
'''
计算图的节点
'''
global_id = -1
def __init__(self, operation, inputs):
self.id = Node.global_id # 该节点的id
Node.global_id += 1
self.inputs = inputs # 该节点的输入
self.operation = operation # 该节点进行的运算操作
self.grad = 0.0 # 该节点的初始化梯度
self.calculate() # 初始化时即计算出该节点的值
def input2value(self):
'''
节点的输入是另一个节点,但是计算要取具体的数值
'''
value_input = []
for i in self.inputs:
if isinstance(i, Node):
i = i.value
value_input.append(i)
return value_input
def calculate(self):
self.value = self.operation.compute(self.input2value()) # 该节点求输出值
# 写到函数里是为了方便可以在构建 topo_list 的时候计算
def __str__(self):
'''
返回对象的描述信息
'''
return "Node %d : %s %s = %s, grad: %.3f" %(self.id, self.input2value(), self.operation.name, self.value, self.grad)
然后需要定义所有需要用到的原子操作类型,这里实现了:加法、减法、乘法、除法、平方、sigmoid、ln
class Operation():
'''
所有原子操作的基类,所有原子操作的输入都不多于两个
'''
def __init__(self):
self.name = ''
'''
产生一个新的 Node,表示此次计算的结果。后面构建 topo_list 的时候会用到,每个操作必须要为输入产生孩子节点
__call__ 表示该类可以像方法一样被调用
'''
def __call__(self):
pass
'''
该操作的计算
'''
def compute(self, inputs):
pass
'''
该操作的导数
'''
def gradient(self, output_grad):
pass
class Identity(Operation):
'''
该操作将输入变量转化为接节点
'''
def __init__(self):
self.name = "identity"
def __call__(self, a):
return Node(self, [a])
def compute(self, inputs):
return inputs[0]
def gradient(self, inputs, output_grad):
return [output_grad]
class Add(Operation):
'''
加法
'''
def __init__(self):
self.name = "add"
def __call__(self, a, b):
return Node(self, [a,b])
def compute(self, inputs):
return inputs[0] + inputs[1]
def gradient(self, inputs, output_grad): # output_grad 似乎为外界对该操作整体的梯度
return [output_grad, output_grad]
class Sub(Operation):
'''
减法
'''
def __init__(self):
self.name = "sub"
def __call__(self, a, b):
return Node(self, [a,b])
def compute(self, inputs):
return inputs[0] - inputs[1]
def gradient(self, inputs, output_grad):
return [output_grad, -output_grad]
class Mul(Operation):
'''
乘法
'''
def __init__(self):
self.name = "mul"
def __call__(self, a, b):
return Node(self, [a,b])
def compute(self, inputs):
return inputs[0] * inputs[1]
def gradient(self, inputs, output_grad):
return [inputs[1] * output_grad, inputs[0] * output_grad]
class Div(Operation):
'''
除法
'''
def __init__(self):
self.name = "div"
def __call__(self, a, b):
return Node(self, [a,b])
def compute(self, inputs):
return inputs[0] / inputs[1]
def gradient(self, inputs, output_grad):
return [1.0 / inputs[1] * output_grad, -inputs[0] / inputs[1] ** 2 * output_grad]
class Square(Operation):
'''
平方
'''
def __init__(self):
self.name = "square"
def __call__(self, a):
return Node(self, [a])
def compute(self, inputs):
return inputs[0] ** 2
def gradient(self, inputs, output_grad):
return [2 * inputs[0] * output_grad]
class Ln(Operation):
'''
取对数
'''
def __init__(self):
self.name = "ln"
def __call__(self, a):
return Node(self, [a])
def compute(self, inputs):
return math.log(inputs[0])
def gradient(self, inputs, output_grad):
return [1.0 / inputs[0] * output_grad]
class Sigmoid(Operation):
'''
sigmoid 函数
'''
def __init__(self):
self.name = "sigmoid"
def __call__(self, a):
return Node(self, [a])
def compute(self, inputs):
return 1.0 / (1 + math.exp(-inputs[0]))
def gradient(self, inputs, output_grad):
res = self.compute(inputs)
return [res * (1 - res) * output_grad]
Executor 类是最重要最核心的一个类,它实现了对计算图进行前向计算、对所有节点进行拓扑排序、以及对输入变量进行求微分的操作。
class Executor():
'''
计算图的执行和自动微分
'''
def __init__(self, root):
self.topo_list = self.topological_sort(root) # 拓扑排序的顺序是孩子节点在前,父节点在后
self.root = root
'''
给一个节点,将该节点下面的所有孩子节点从低到高加入到 topo_list 中
'''
def dfs(self, topo_list, node):
if node is None or not isinstance(node, Node):
return
for term in node.inputs:
self.dfs(topo_list, term)
topo_list.append(node) # 先添加孩子节点,再添加自己;某孩子节点可能会被添加多次
# Q1:node 为空的判别条件?
# Q2:能不能先添加自己,再添加孩子节点?
'''
给定根节点,将根节点下面的所有节点加入 topo_list
'''
def topological_sort(self, root):
li = []
self.dfs(li, root)
return li
'''
计算每个节点的前向值。其实前面生成节点的时候已经计算过了
'''
def node_calculate(self):
node_calcued = set()
for term in self.topo_list:
if term not in node_calcued: # 保证取到的节点是还没有被计算过的
term.calculate()
node_calcued.add(term)
return self.root.value # 返回最终前向计算的结果
'''
反向对各输入变量求微分
'''
def derivation(self):
reverse_topo_list = list(reversed(self.topo_list))
reverse_topo_list[0].grad = 1.0
for node in reverse_topo_list: # Q:有重复节点怎么办?
# 节点对其输入各变量的梯度
print(node)
grads = node.operation.gradient(node.input2value(), node.grad)
for child, child_grad in zip(node.inputs, grads):
if isinstance(child, Node):
child.grad += child_grad
for node in reverse_topo_list:
print(node)
举几个栗子试验一下:
# 原子操作实例化
identity = Identity()
add = Add()
sub = Sub()
mul = Mul()
div = Div()
square = Square()
ln = Ln()
sigmoid = Sigmoid()
# 第一个例子:y = ln(x1) + x1*x2
x1, x2 = identity(2), identity(5)
y = add(ln(x1), mul(x1, x2)) # y 是根节点
executor = Executor(y)
print(executor.node_calculate())
executor.derivation()
print("gradient of x1 is %.3f" % x1.grad)
print("gradient of x2 is %.3f" % x2.grad)
# 第二个例子:y = (x1 + x2) * (x2 + 1)
x1, x2 = identity(2), identity(1)
y = mul(add(x1, x2), add(x2, 1))
executor = Executor(y)
print(executor.node_calculate())
executor.derivation()
print("gradient of x1 is %.3f" % x1.grad)
print("gradient of x2 is %.3f" % x2.grad)
# 第三个例子:用= (x1 + σ(x2)) / (σ(x1) + (x1 + x2)^2)
x1, x2 = identity(3), identity(-4)
numerator = add(x1, sigmoid(x2))
denominator = add(sigmoid(x1), square(add(x1, x2)))
y = div(numerator, denominator)
executor = Executor(y)
print(executor.node_calculate())
executor.derivation()
print("gradient of x1 is %.3f" % x1.grad)
print("gradient of x2 is %.3f" % x2.grad)
这里实现的是静态图,现在的深度学习框架里用的都是动态图了。