python函数求导_python构建计算图1——简单实现自动求导

机器学习和深度学习中比较重要的内容便是计算图,主流的框架如tensorflow,pytorch都是以计算图为主要框架。而计算图的核心便是自动求导。

所谓自动求导,就是在表达式或者网络结构确定之时,其(导数)梯度便也同时确定了。自动求导听上去很玄幻,很厉害,但其本质还是有向箭头的传递,该箭头是从自变量指向最终结果。

我们先定义表达式(由初等函数构造而成),在表达式构造完成之前不进行计算。完成后,传入自变量,在函数值通过箭头传递的同时,导数值也同时在箭头上传递(静态图的简单描述。静态图的概念这里先不说明,可以先按照函数表达式来理解,我们写一个函数的时候就是f(x) = ...的形式,先写好函数是如何计算的,再给X带入具体数值计算。)

这样之后给人的感觉就是计算表达式的同时,导数值也自动算出来了。

一. 手动求导

举个简单的例子,对于表达式:

equation?tex=f%28x%29+%3D+exp%28sin%28ln%28x%5E2%29%29%29

我们在求导的时候,先求外层函数的导数,再求内层函数的导数,最终两者相乘,即:

equation?tex=%5Cfrac%7Bdf%7D%7Bdx%7D%3De%5Eu%5Ccdot+u%5E%7B%27%7D%28x%29%2C+u%28x%29%3Dsin%28ln%28x%5E2%29%29

接着对u(x)求导,最后得到:

equation?tex=%5Cfrac%7Bdf%7D%7Bdx%7D%3De%5E%7Bsin%28ln%28x%5E2%29%29%7D%5Ccdot+cos%28ln%28x%5E2%29%29%5Ccdot+%5Cfrac%7B1%7D%7Bx%5E2%7D%5Ccdot+2x

再换一种写法的话:

equation?tex=%5Cfrac%7Bdf%7D%7Bdx%7D%3D%5Cfrac%7Bdf%7D%7Bdu%7D%5Ccdot+%5Cfrac%7Bdu%7D%7Bdt%7D%5Ccdot+%5Cfrac%7Bdt%7D%7Bdw%7D%5Ccdot+%5Cfrac%7Bdw%7D%7Bdx%7D

这表明对函数求导的核心便是链式法则,而链式法则的每一步求导正和计算图的有向结构类似(这里先不讨论计算图的概念,只针对自动求导) 。

二. 引入算符

算符在量子力学中,是作用在一个函数得到另一个函数的操作。简单理解为将一个量变换为另一个量。

我们把这里的每一个函数看做一个算符,每一个算符看做一个节点,每一个量在通过节点之后就变成了另一个量。

在上面的表达式中,指数函数,三角函数,对数函数和幂函数一共四个函数,就是四个算符,四个节点,每一个节点都存在若干个变量:value(函数值),grad(导数),root(自变量),next(下一节点,最后一个节点的next为None)。

除了节点之外,最重要的就是自变量了。这里用placeholder(占位符)表示(模仿tensorflow)的写法。placeholder正如其名,作用就是占位(采用占位符的原因后续简述,这里先按照这个模式使用),像极了人们书写函数时f(x)中的x(或者说就是)。它也有grad(可有可无,自变量对自变量求导结果是1),size(数据尺寸,这里不会用到),root(就是自己)和next。

我们在构造一个函数表达式时,前一节点的输出变成了后一节点的输入。在计算节点value时,通过上一个节点的输出作为此节点中函数的输入计算而来。计算节点的grad时,通过前一节点的梯度乘该节点的导数得到。

三. 程序实现

我们用一个基类Variable作为上述placeholder与算符的基类。该类具有四个变量:value,grad,next,root。两个方法:func,func_grad。

# -*- coding: utf-8 -*-

import numpy as np

class Variable:

def __init__(self, value=None):

self.value = value

self.grad = None

self.next = None

self.root = None

def func(X):

return

def func_grad(X):

return

1.对于占位符,本来是没有value变量的,但为了形式的统一,在建立完方程形式,开始运行的时候,我还是给予了placeholder一个value属性(父亲遗传下来的东西,不用怎么行)。

class placeholder(Variable):

def __init__(self, size):

super().__init__(self)#运行父类的构造函数

self.size = size

self.root = self#root就是自己

self.grad = 1

2.对于算符,这里先简单的构建exp,sin,cos,ln和平方算符:

class exp(Variable):

def __init__(self, X):

super().__init__()#继承父类

X.next = self#作为上一个节点的下一步运算

self.root = X.root#声明自变量,复合函数自变量都是前一函数的自变量

def func(self, X):

return np.exp(X)

def func_grad(self, X):

return np.exp(X)

class sin(Variable):

def __init__(self, X):

super().__init__()

X.next = self

self.root = X.root

def func(self, X):

return np.sin(X)

def func_grad(self, X):

return np.cos(X)

class cos(Variable):

def __init__(self, X):

super().__init__()

X.next = self

self.root = X.root

def func(self, X):

return np.cos(X)

def func_grad(self, X):

return -np.sin(X)

class log(Variable):

def __init__(self, X):

super().__init__()

X.next = self

self.root = X.root

def func(self, X):

return np.log(X)

def func_grad(self, X):

return 1 / X

class square(Variable):

def __init__(self, X):

super().__init__()

X.next = self

self.root = X.root

def func(self, X):

return np.square(X)

def func_grad(self, X):

return 2 * X

构造节点的时候,我没有用全局列表保存每一步的操作以及名称,而是利用next的指针指向下一个需要计算的节点。

这样可以用最少的变量数目保存我们需要的所有信息。(放在堆上的类减少,栈上的变量增多,但是调用结束后,栈上的空间立刻就被释放了)

节点中存在root变量是为了开始计算的时候更方便些,不需要通过判断节点的上一节点是不是占位符来确定计算起始点,也不需要保存函数表达式,然后从头开始算。

3.最后定义Session对表达式求值

class Session:

def run(self, operator, feed_dict):

root = operator.root#计算起始点

root.value = feed_dict[root]#传入自变量的数据

while root.next is not operator.next:#计算到operator便停止计算

root.next.value = root.next.func(root.value)#计算节点的值

root.next.grad = root.grad * root.next.func_grad(root.value)#计算梯度

root = root.next#去往下一个节点

return root.value

通过判断目前计算的节点是否是目标节点来决定是否停止计算,这样便可以通过Session的run方法直接返回目标节点的value。

到此为止简单的自动求导就已经写好了,整个过程借鉴了tensorflow的套路,当然我没看过tensorflow的源码,只是根据自己目前为止用到过的内容简单搭建。

下面验证一下结果:

#构建表达式(计算图)

xs = placeholder((2, 2))

h1 = square(xs)

h2 = log(h1)

h3 = sin(h2)

h4 = exp(h3)

#具体数据

X = np.array([[1, 2], [3, 4]])

#建立Session计算

sess = Session()

out = sess.run(h4, feed_dict = {xs: X})

#自动求导

grad = h4.grad

#手动求导

grad0 = np.exp(np.sin(np.log(np.square(X)))) * np.cos(np.log(np.square(X))) * 1/np.square(X) * 2 * X

print()

print("自动求导:\n", grad)

print("手动求导:\n", grad0)

运行结果:

完全正确√

结语:

一开始我搭建表达式的时候,对每个节点记录其上一步操作,这样可以通过递归的方式找到计算的起始点,然后喂入数据再计算。本来全连接层已经按这种方式写好,正在写卷积,但是某天突然醒悟之后便写下了此文,然后开始努力更改以前的东西QAQ。

实现了自动求导之后,下面就要开始静态图的搭建,还是会按照tensorflow2.0前的方式,不过既然新版本都出了,我还迷恋旧版本,ε=(´ο`*)))唉。

本人应用物理系,对ML, DL较感兴趣。可毕竟不是计算机系啦,用语不那么专业,操作可能显得复杂,希望大家能多多指导哦!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值