欢迎光临散文网 会员登陆 & 注册

【TF/Guide笔记】 03. Automatic differentiation

2022-02-27 16:54 作者:纪一希  | 我要投稿

    由于我是半路出家,没有学过基础课程,结果在梯度的这个问题上绕了好久才想明白。

    给理解带来最大障碍的反而是我们以前实现自动求导的方式,从前我们的每一个Variable内部包含了两个Tensor,一个数据data一个梯度diff,在一个op实现了从上游data转换为下游data的同时,附加实现了下游diff转为上游diff的操作,这样用户创建的一个op在计算图实际上被展开为了两组计算节点和输入输出,一个正向一个反向的操作直到loss那里才连成了通路。

    正是受这个先入为主的概念的影响,在看tf是怎么实现的时候一下就绕不明白了,对于y=x^2来说,我从数学上可以理解求导是2x,也可以理解当x=3时梯度是6,但这个梯度跟以前的diff值怎么都对不上。

    绕了半天才想明白,tf里求解的梯度就是真正的梯度,也就是函数在某一点的斜率,数学上应该是dy/dx,而我们以前实现的梯度,其实得到的是dx,它是一个跟loss有关的具体数值,把loss当成最终的y的话,我们实际上隐式的假设了loss的目标必须是0,所以dy其实就是loss-0,然后使用这个dy一步一步的往回推到出dx,这个dx就是我算出来的梯度下降的值,已经并非梯度本身了。

    困惑了我半天的是,tf求出来的loss是怎么作用到梯度上的,答案就是他根本没作用,一个标准的流程应该是先求出loss对于每个变量的梯度dy/dx,然后设定dy(也就是梯度下降的步长)再计算得出dx,tf求出来的就是纯粹的梯度,至于你要怎么用,那你再用代码去写,我们以前的框架给训练预设了太多前提。


    基本原理理顺了,做法一下就清晰了。

    对于一个op y=f(x),dy/dx直接对函数f求导就行了,这个结果只与x的值有关,跟下游变量是无关的,每一个op都可以针对输入求出一个dy/dx,然后根据求导的链式法则把一连串op的结果乘起来,就能得到d loss/dx了。

    声明tape则是为了记录每个op的梯度计算式,又或许tape里直接存放了梯度值本身,毕竟你都用tape了不可能最后不求导吧,这样tape里就存放了一张图,节点上是梯度,边则是变量,tape.gradient(y, x)就是由用户指定了链式法则的起点和终点,这条链路上相乘就得到了dy/dx。

    使用tape的好处是,你显示的告诉了框架那些操作是需要算梯度而哪些不要的,这样diff tensor就始终只存在于tape的体系里,Variable内部并不需要两个tensor,这样的确非常合理,因为当我们假定了Variable里必须包含data和diff的时候,很多很多op其实压根儿没有算diff的意义(比如格式转换),但是为了统一性还必须要写这段空逻辑,造成了大量的代码冗余和难以理解。

    这种方式可能对精度也比较友好,虽然理论上我们的算法和tf的算法得出的结果是一样的,但很常见的一种情况是由于初始参数不好,头一轮的loss是非常爆炸的,用这个超大的dy去一步一步往前推导dx,可以想象它的精度会在中间损失多少,而如果先算清楚dy/dx的话,由于这里涉及的计算使用的都是输入,噪点肯定是在早期处理掉的,所以剃度的精度会算的很高,最后再把乘完alpha的loss带进去,会让结果有较大的保障。也难怪我们以前小数据去跟tf对拍出来都是对的,但经常用着用着就觉得自家的精度莫名其妙的没了。


    通常来讲,需要算梯度的部分和其他操作是可以比较容易的划分开的,但要是真有不太好划分出来的Variable,就可以用到之前提过的trainable参数,所以trainable只是标记了这个变量是否要进tape,跟他本身有没有diff是没关系的。

    Variable内部只有一个tensor,又支持所有tensor的操作,所以Variable和Tensor在本质上的区别就是是否自动被tape记录。不过tf支持了手动记录的接口,tape.watch(tensor),这样看来tape内部记录和操作的还是tensor,Variable并没有做太多封装。

    tape同时支持关闭自动记录变量,改为手动指定想要计算梯度的Variable,不过只要某个x处于watch状态,显然由它计算得到的那些变量就都会进入tape内部的链式图,相当于下游的中间变量是一定被watch的。

    根据文档说的,tape如果不开persistent=True的话,他就只能调用一次,这其实间接说明了tape内部记录的是每个op里dy/dx的结果,而不是只记录了一个function用的时候现算,如果是用时现算的话是没有什么临时变量值得释放的。这样设计的目的大概在于,在forward这个过程里执行尽可能多的计算,这样就可以实现更多的优化,比如计算y=f(x)之后紧接着计算y=f'(x),那么x的数据就可以只进一次高级缓存,应该会有明显的速度差异。


    虽然后面提了很多不同target的情况下梯度的计算结果,但只要按链式法则去理解应该就行了。不过链式的中间肯定要涉及tensor的升降维,所以也不是完全连乘那么简单。

    同样的,利用链式图就可以很容易理解,为什么控制流里的变量无法求导,为什么必须所有操作都用tf操作,为什么重新赋值会导致无法求导,为什么带状态的变量无法参与求导,因为这些操作都会让链式图断裂或者无法定义。

    不得不感慨,即便强大如tf,也会对用户级的代码有诸多限制,我们以前没几个人力还想做各种兼容真是太不自量力了。

【TF/Guide笔记】 03. Automatic differentiation的评论 (共 条)

分享到微博请遵守国家法律