专栏名称: 天池大数据科研平台
天池,基于阿里云的开放数据处理服务ODPS,面向学术界开放海量数据和分布式计算资源,旨在打造“数据众智、众创”第一平台。在这里,人人都可以玩转大数据,共同探索数据众创新模式。
目录
相关文章推荐
大数据分析和人工智能  ·  免费领取DeepSeek教程 ·  21 小时前  
玉树芝兰  ·  对科研工作者来说,OpenAI Deep ... ·  2 天前  
数据派THU  ·  LossVal:一种集成于损失函数的高效数据 ... ·  4 天前  
51好读  ›  专栏  ›  天池大数据科研平台

【Pytorch 】笔记二:动态图、自动求导及逻辑回归

天池大数据科研平台  · 公众号  · 大数据  · 2020-10-20 22:05

正文

↑↑↑关注后" 星标 "天池大数据科研平台

人人都可以玩转大数据

阿里云天池严选

来源:阿泽的学习笔记 作者:Miracle8070
编辑:阿泽


【导读】 Miracle 是一个优秀的同学,他记录的 pytorch 系列共有十篇,这是第一篇,他记录的 pytorch 系列共有十篇,这是第二篇。不要觉得 Pytorch 很简单,一定有你所忽视的地方,耐心一点,欢迎复习,也欢迎追剧。

【Pytorch】笔记一:数据载体张量与线性回归

1.写在前面

疫情在家的这段时间,想系统的学习一遍 Pytorch 基础知识,因为我发现虽然直接 Pytorch 实战上手比较快,但是关于一些内部的原理知识其实并不是太懂,这样学习起来感觉很不踏实, 对 Pytorch 的使用依然是模模糊糊, 跟着人家的代码用 Pytorch 玩神经网络还行,也能读懂,但自己亲手做的时候,直接无从下手,啥也想不起来, 我觉得我这种情况就不是对于某个程序练得不熟了,而是对 Pytorch 本身在自己的脑海根本没有形成一个概念框架,不知道它内部运行原理和逻辑,所以自己写的时候没法形成一个代码逻辑,就无从下手。这种情况即使背过人家这个程序,那也只是某个程序而已,不能说会 Pytorch, 并且这种背程序的思想本身就很可怕, 所以我还是习惯学习知识先有框架(至少先知道有啥东西)然后再通过实战(各个东西具体咋用)来填充这个框架。而 「这个系列的目的就是在脑海中先建一个 Pytorch 的基本框架出来, 学习知识,知其然,知其所以然才更有意思 :)」

今天是该系列的第二篇, 接着上次的学习 Pytorch 的数据载体张量与线性回归 进行整理,这次主要包括 Pytorch 的计算图机制和自动求导机制,并且最后基于前面的所学玩一个逻辑回归。

「大纲如下:」

  • 计算图与 Pytorch 的动态图机制(计算图的概念,动态图与静态图的差异和搭建过程)
  • Pytorch 的自动求导机制
  • 基于前面所学玩一个逻辑回归
  • 总结梳理

思维导图如下:

在这里插入图片描述

2.计算图

2.1 计算图

前面已经整理了张量的一系列操作,而深度学习啊,其实就是对各种张量进行操作,随着操作的种类和数量的增大,会导致各种想不到的问题,比如多个操作之间该并行还是顺序执行,底层的设备如何协同,如何避免冗余的操作等,这些问题都会影响我们算法的执行效率,甚至会出现一些 bug。而计算图就是为了解决这些问题而产生的,那么什么是计算图呢?

计算图是用来 「描述运算」 的有向五环图。主要有两个因素:节点和边。其中节点表示数据,如向量,矩阵,张量,而边表示运算,如加减乘除,卷积等。下面我们看一下具体这东西具体是什么样子:

使用计算图的好处不仅让计算看起来更加简洁,还有个更大的优势就是让梯度求导也变得更加方便。下面我们看看y对w进行求导的过程:

y对w求导,就是从计算图中找到所有y到w的路径。把各个路径的导数进行求和。我们通过程序来验证一下:

w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)

y.backward()
print(w.grad)   # tensor([5.])

下面,我们基于这个计算图来说几个张量里面重要的属性:

  1. 叶子节点这个属性(还记得张量的属性里面有一个 is_leaf 吗): 叶子节点:用户创建的节点, 比如上面的 x 和 w。叶子节点是非常关键的,在上面的正向计算和反向计算中,其实都是依赖于我们叶子节点进行计算的。is_leaf: 指示张量是否是叶子节点。

    为什么要设置叶子节点的这个概念的?主要是为了节省内存,因为我们在反向传播完了之后,非叶子节点的梯度是默认被释放掉的。我们可以根据上面的那个计算过程,来看看 w,x, a, b, y 的 is_leaf 属性,和它们各自的梯度情况:

#查看叶子结点
print("is_leaf:\n", w.is_leaf, x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf)
#查看梯度, 默认是只保留叶子节点的梯度的
print("gradient:\n", w.grad, x.grad, a.grad, b.grad, y.grad)

## 结果:
is_leaf:
 True True False False False
gradient:
 tensor([5.]) tensor([2.]) None None None

我们可以发现, 只有 w, x 的 is_leaf 属性是 True,说明这俩是叶子节点。gradient 上,也只有叶子节点的梯度被保留了下来,a, b, y 的梯度都默认释放了,所以是空。但是我们如果想用这里面的某个梯度呢?比如我想保留a的梯度,那么可以使用 retain_grad() 方法。就是在执行反向传播之前,执行一行代码:a.retain_grad() 即可。

w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
a.retain_grad()
b = torch.add(w, 1)
y = torch.mul(a, b)

y.backward()
#查看梯度, 默认是只保留叶子节点的梯度的
print("gradient:\n", w.grad, x.grad, a.grad, b.grad, y.grad)

## 结果:a的梯度被保留了下来
gradient:
 tensor([5.]) tensor([2.]) tensor([2.]) None None
  1. grad_fn:记录创建该张量时所用的方法(函数),记录这个方法主要 「用于梯度的求导」 。要不然怎么知道具体是啥运算?
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
a.retain_grad()
b = torch.add(w, 1)
y = torch.mul(a, b)

y.backward()

# 查看 grad_fn   这个表示怎么得到的
print("grad_fn:\n", w.grad_fn, x.grad_fn, a.grad_fn, b.grad_fn, y.grad_fn)

## 结果:
 None None 0x0000029AECF56D08> 0x0000029AEEFEB248> 0x0000029AEEFEB748>

这个属性,会记录变量具体是怎么得到的,比如两数相加,或者两数相乘,这样反向计算梯度的时候才能使用相应的法则求变量的梯度。当然知道是用于反向传播即可。

2.2 动态图

根据计算图的搭建方式,可以将计算图分为动态图和静态图。

  • 静态图:先搭建图,后运算。高效,不灵活(TensorFlow)
  • 动态图:运算与搭建同时进行。灵活,易调节(Pytorch)

这个就类似于旅游的时候你找旅行团还是自己去旅游一样,找旅行团的这种就类似于静态图,游览的路线和流程都已经确定,跟着走就行了。如果你熟悉 TensorFlow 的话,会知道 TensorFlow 的计算方式,是先把图搭建好,然后开启一个会话, 在那里面才开始喂入数据进行流动计算, 在这个过程中,张量就会根据搭建好的图进行计算。我们可以看看上面的那个例子:

# 声明两个常量
w = tf.constant(1.)
x = tf.constant(2.)

# 搭建静态图
a = tf.add(w, x)
b = tf.add(w, 1)
y = tf.multiply(a, b)

# 这时候还没开始计算
print(y)   # Tensor("Mul_4:0", shape=(), dtype=float32), 只是计算图的一个节点

with tf.Session() as sess:  
    print(sess.run(y))   # 这里才开始进行计算, 6.0

而自己去旅游这个就类似于动态图,游览的路线和流程都不确定,想去哪去哪,随时调整。Pytorch 就是采用的这种机制,这种机制就是边建图边执行,从上面的例子中也能看出来,比较灵活, 有错误可以随时改,也更接近我们一般的想法。毕竟没有谁做一件事情之前就能把所有的流程都能规划好,一般人都是有一个大体的框架,然后一步一步边走边调整。依然是上面的例子:

w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
print(y)    # tensor([6.], grad_fn=)

这里会发现直接就算出了 y 的结果,这说明上面的每一步都进行了计算。

3.自动求导机制

Pytorch 自动求导机制使用的是 torch.autograd.backward 方法,功能就是自动求取梯度。

  • tensors 表示用于求导的张量,如 loss。
  • retain_graph 表示保存计算图, 由于 Pytorch 采用了动态图机制,在每一次反向传播结束之后,计算图都会被释放掉。如果我们不想被释放,就要设置这个参数为 True
  • create_graph 表示创建导数计算图,用于高阶求导。
  • grad_tensors 表示多梯度权重。如果有多个 loss 需要计算梯度的时候,就要设置这些 loss 的权重比例。

这时候我们就有疑问了啊?我们上面写代码的过程中并没有见过这个方法啊?我们当时不是直接 y.backward() 吗?哪有什么 torch.autograd.backward() 啊? 其实,当我们执行 y.backward() 的时候,背后其实是在调用后面的这个函数,不行?我们来调试一下子就清楚了: 我们在这一行打断点,然后进行调试。我们进入这个函数之后,会发现:

def backward(self, gradient=None, retain_graph=None, create_graph=False):
  torch.autograd.backward(self, gradient, retain_graph, create_graph)

这样就清楚了,这个 backward 函数就是在调用这个自动求导的函数。

backward() 里面有个参数叫做 retain_graph,这个是控制是否需要保留计算图的,默认是不保留,即一次反向传播之后,计算图就会被释放掉,这时候,如果再次调用 y.backward, 就会报错: 报错信息就是说的计算图已经释放掉了。所以我们把第一次用反向传播的那个retain_graph设置为True就OK了:

y.backward(retain_graph=True)

这里面还有一个比较重要的参数叫做grad_tensors, 这个是当有多个梯度的时候,控制梯度的权重,这个是什么意思,依然拿上面的那个举例: 上面这个过程会报错,这时候我们就需要用到 gradient 这个参数了, 给两个梯度设置权重,最后得到的 w 的梯度就是带权重的这两个梯度之和。

grad_tensors = torch.tensor([1.1.])
loss.backward(gradient=grad_tensors)    
print(w.grad)   #  这时候会是tensor([7.])   5+2

grad_tensors = torch.tensor([1.2.])
loss.backward(gradient=grad_tensors)    
print(w.grad)   #  这时候会是tensor([9.])   5+2*2

除了 backward()方法,还有一个比较常用的方法叫做:torch.autograd.grad(), 这个方法的功能是求取梯度,这个可以实现高阶的求导。

  • outputs: 用于求导的张量,如 loss
  • inputs: 需要梯度的张量,如上面例子的 w
  • create_graph: 创建导数计算图,用于高阶求导
  • retain_graph: 保存计算图
  • grad_outputs: 多梯度权重

拿个例子看一下就明白了这个方法如何使用了:

x = torch.tensor([3.], requires_grad=True)
y = torch.pow(x, 2)   # y=x^2

# 一次求导
grad_1 = torch.autograd.grad(y, x, create_graph=True)   # 这里必须创建导数的计算图, grad_1 = dy/dx = 2x
print(grad_1)   # (tensor([6.], grad_fn=),) 这是个元组,二次求导的时候我们需要第一部分

# 二次求导
grad_2 = torch.autograd.grad(grad_1[0], x)    # grad_2 = d(dy/dx) /dx = 2
print(grad_2)  # (tensor([2.]),)

这个函数还允许对多个自变量求导数:

x1 = torch.tensor(1.0,requires_grad = True# x需要被求导
x2 = torch.tensor(2.0,requires_grad = True)

y1 = x1*x2
y2 = x1+x2


# 允许同时对多个自变量求导数
(dy1_dx1,dy1_dx2) = torch.autograd.grad(outputs=y1,inputs = [x1,x2],retain_graph = True)
print(dy1_dx1,dy1_dx2)        # tensor(2.) tensor(1.)

# 如果有多个因变量,相当于把多个因变量的梯度结果求和
(dy12_dx1,dy12_dx2) = torch.autograd.grad(outputs=[y1,y2],inputs = [x1,x2])
print(dy12_dx1,dy12_dx2)        # tensor(3.) tensor(2.)

关于 Pytorch 的自动求导系统要注意:

  1. 梯度不自动清零:就是每一次反向传播,梯度都会叠加上去,这个要注意,举个例子:
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

for i in range(4):
    a = torch.add(w, x)
    b = torch.add(w, 1)
    y = torch.mul(a, b)

    y.backward()
    print(w.grad)

## 结果:
tensor([5.])
tensor([10.])
tensor([15.])
tensor([20.])

会发现,每次 w 的梯度都会累加, 执行了四次,最后是 20 了。这样就会发生错误了,尤其是训练神经网络的时候特别注意。毕竟我们肯定不是训练一次,所以每一次反向传播之后,我们 「要手动的清除梯度」 这里会发现个 zero_(),这里有个下划线,这个代表原位操作,后面第三条会详细说。

  1. 依赖于叶子节点的节点,requires_grad 默认为 True,这是啥意思? 拿上面的计算图过来解释一下,依赖于叶子节点的节点,在上面图中w,x是叶子节点,而依赖于叶子节点的节点,其实这里说的就是 a,b, 也就是 a,b 默认就是需要计算梯度的。这个也好理解,因为计算 w,x 的梯度的时候是需要先对 a, b 进行求导的,要用到 a, b 的梯度,所以这里直接默认 a, b 是需要计算梯度的。在代码中,也就是这个意思:
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)

y = torch.mul(a, b)    # y0=(x+w) * (w+1)     dy0 / dw = 5
print(w.requires_grad, a.requires_grad, b.requires_grad)  # 这里会都是True, w的我们设置了True, 而后面这里是依赖于叶子,所以默认是True
  1. 叶子节点不可执行 in-place(这个 in-place 就是原位操作) 首先先看看什么是 in-place 操作, 这个操作就是在原始内存当中去改变这个数据,这个理解起来的话,其实就是这个意思:我们拿一个 a+1 的例子看一下,我们知道数字的话理论上是一个不可变数据对象,类似于字符串,元组这种,比如我假设 a=1, 然后我执行 a=a+1, 这样的话,a 虽然是 2,但是这两个 a 其实指向的对象是不一样的,原来的 1 并没有改变,执行 a+1, 「是新建了一个对象出来」 , 然后改变了原来 a 的指向。这就是数字的不可变现象。 如果不理解,对比一下子就清楚了, 我们知道列表示可变的,假设 a=[1,5,3],我们可以 a.append(4),此时 a 指向的对象就变成了 [1,5,3,4],但其实是在原对象 [1,2,3] 上进行的添加, 「此过程没有新对象产生」 。我们还可以 a.sort(), 这时候 a 指向的对象就变成了 [1, 3, 4, 5], 但依然是原对象上进行的改变。说清楚了吧? 没有的话可以看看我 python 查缺补漏的第一篇文章, 那里面说的更详细些。这里重点说原位操作, 将数字进行原位操作之后, 这个数字就类似于列表这种,是在本身的内存当中改变的数,这时候就没有新对象建立出来。a+=1 就是一种原位操作。我们看个例子吧:
a = torch.ones((1,))
print(id(a), a)    # 1407221517192 tensor([1.])

# 我们执行普通的a = a+1操作
a = a + torch.ones((1,))
print(id(a), a)    # 1407509388808 tensor([2.])  
# 会发现上面这两个a并不是同一个内存空间

# 那么执行原位操作呢?
a = torch.ones((1,))
print(id(a), a)    # 2112218352520 tensor([1.])
a += torch.ones((1,))
print(id(a), a)   # 2112218352520 tensor([2.])

好了,原位操作差不多理解了吧。 其实比较简单。那么为什么叶子节点不能进行原位操作呢? 先看看叶子节点进行原位操作是怎么回事?下面这个报错: 这是为什么呢? 这个要从计算图求取梯度的过程来理解,依然得把计算图拿过来: 我们来看这个求取梯度的过程, 我们要求w的梯度的时候,我们发现会先 ∂y/∂a=w+1, 然后 ∂a/∂w, 也就是说反向传播的过程的 ∂y/∂a 就用到了 w, 这时候是怎么找到 w 的呢?其实正向传播的时候,会把 w 的地址给记下来,然后反向传播的这一步,就是根据这个地址去找 w 的值。如果在反向传播之前,就用原位操作把这个 w 的值给变了,那么反向传播再拿到这个 w 的值的时候,就出错了。所以 Pytorch 不允许对叶子使用原位操作。这就类似于去超市买东西存包取包的过程,假设我们去超市,需要把包先存到柜子,管理员给了我们一个号码牌 10 号,我们把包存进了 10 号柜子, 但如果管理员把 10 号柜子的东西换成了别人的包,你购物回来之后再拿 10 号的牌子去取自己的包,发现不在了,不就出错了?

前面已经学习了数据的载体张量,学习了如何通过前向传播搭建计算图,同时通过计算图进行梯度的求解,有了数据,计算图和梯度,我们就可以正式的训练机器学习模型了。接下来,我们就玩一个逻辑回归模型吧。

4.逻辑回归模型

逻辑回归模型是 「线性」 「二分类」 模型,模型的表达式如下:

这里的 就是 sigmoid 函数了, 也成为了 logistic 函数。还记得 sigmoid 函数吗? 为什么说是二分类的问题呢?

我们是根据这个y的取值进行分类的,当取值小于 0.5, 就判别为类别 0, 大于 0.5, 就判别为类别 1.

那为什么称为线性呢?我们可以对比一下线性回归和逻辑回归的区别:

  • 线性回归:自变量是 X, 因变量是 y, 关系:y=wx + b, 图像是一条直线。是分析自变量 x 和因变量 y (标量)之间关系的方法。注意这里的线性是针对于 w 进行说的, 一个 w 只影响一个 x。决策边界是一条直线
  • 逻辑回归:自变量是 X, 因变量是 y, 只不过这里的 y 变成了概率。关系:
    图像也是一条直线。是分析自变量 x 与因变量 y(概率)之间的关系。这里注意不要只看到那个 sigmoid 函数就感觉逻辑回归是非线性的。因为这个 sigmoid 函数在这里只是为了更好的描述分类置信度。如果我们不用这个函数,其实也是可以进行二分类的,比如 wx+b>0,我们判定为 1, wx+b<0, 我们判定类别 0, 这样其实也是可以的,就会发现,依然是一个 w 只影响一个 y, 决策边界是一条直线。所以依然是线性的。关于线性和非线性模型的区别,这个解释不错:

逻辑回归也叫做对数几率回归,这是为啥呢?首先,什么是对数几率回归,我们知道线性回归是 , 而如果我们把几率 (这个表示样本 X 为正样本的可能性)取对数,让它等于 ,就叫做对数几率回归,即







请到「今天看啥」查看全文