尝试使用了pytorch,相比其他深度学习框架,pytorch显得简洁易懂。花时间读了部分源码,主要结合简单例子带着问题阅读,不涉及源码中C拓展库的实现。

一个简单例子

实现单层softmax二分类,输入特征维度为4,输出为2,经过softmax函数得出输入的类别概率。代码示意:定义网络结构;使用SGD优化;迭代一次,随机初始化三个样例,每个样例四维特征,target分别为1,0,1;前向传播,使用交叉熵计算loss;反向传播,最后由优化算法更新权重,完成一次迭代。

  1.  
    import torch
  2.  
    import torch.nn as nn
  3.  
    import torch.nn.functional as F
  4.  
     
  5.  
    class Net(nn.Module):
  6.  
     
  7.  
    def __init__(self):
  8.  
    super(Net, self).__init__()
  9.  
    self.linear = nn.Linear(4, 2)
  10.  
     
  11.  
    def forward(self, input):
  12.  
    out = F.softmax(self.linear(input))
  13.  
    return out
  14.  
     
  15.  
    net = Net()
  16.  
    sgd = torch.optim.SGD(net.parameters(), lr=0.001)
  17.  
    for epoch in range(1):
  18.  
    features = torch.autograd.Variable(torch.randn(3, 4), requires_grad=True)
  19.  
    target = torch.autograd.Variable(torch.LongTensor([1, 0, 1]))
  20.  
    sgd.zero_grad()
  21.  
     
  22.  
    out = net(features)
  23.  
    loss = F.cross_entropy(out, target)
  24.  
    loss.backward()
  25.  
    sgd.step()

从上面的例子,带着下面的问题阅读源码:

  • pytorch的主要概念:Tensor、autograd、Variable、Function、Parameter、Module(Layers)、Optimizer;
  • 自定义Module如何组织网络结构和网络参数;
  • 前向传播、反向传播实现流程
  • 优化算法类如何实现,如何和自定义Module联系并更新参数。

pytorch的主要概念

pytorch的主要概念官网有很人性化的教程Deep Learning with PyTorch: A 60 Minute Blitz, 这里简单概括这些概念:

Tensor

类似numpy的ndarrays,强化了可进行GPU计算的特性,由C拓展模块实现。如上面的torch.randn(3, 4) 返回一个3*4的Tensor。和numpy一样,也有一系列的Operation,如

  1.  
    x = torch.rand(5, 3)
  2.  
    y = torch.rand(5, 3)
  3.  
    print x + y
  4.  
    print torch.add(x, y)
  5.  
    print x.add_(y)

Varaiable与autograd

Variable封装了Tensor,包括了几乎所有的Tensor可以使用的Operation方法,主要使用在自动求导(autograd),Variable类继承_C._VariableBase,由C拓展类定义实现。
Variable是autograd的计算单元,Variable通过Function组织成函数表达式(计算图):

  • data 为其封装的tensor值
  • grad 为其求导后的值
  • creator 为创建该Variable的Function,实现中grad_fn属性则指向该Function。
    如:
    1.  
      import torch
    2.  
      from torch.autograd import Variable
    3.  
      x = Variable(torch.ones(2, 2), requires_grad=True)
    4.  
      y = x + 2
    5.  
      print y.grad_fn
    6.  
      print "before backward: ", x.grad
    7.  
      y.backward()
    8.  
      print "after backward: ", x.grad

    输出结果:

    1.  
      <torch.autograd.function.AddConstantBackward object at 0x7faa6f3bdd68>
    2.  
      before backward: None
    3.  
      after backward: Variable containing:
    4.  
      1
    5.  
      [torch.FloatTensor of size 1x1]

    调用y的backward方法,则会对创建y的Function计算图中所有requires_grad=True的Variable求导(这里的x)。例子中显然dy/dx = 1。

Parameter

Parameter 为Variable的一个子类,后面还会涉及,大概两点区别:

  • 作为Module参数会被自动加入到该Module的参数列表中;
  • 不能被volatile, 默认require gradient。

Module

Module为所有神经网络模块的父类,如开始的例子,Net继承该类,____init____中指定网络结构中的模块,并重写forward方法实现前向传播得到指定输入的输出值,以此进行后面loss的计算和反向传播。

Optimizer

Optimizer是所有优化算法的父类(SGD、Adam、...),____init____中传入网络的parameters, 子类实现父类step方法,完成对parameters的更新。

自定义Module

该部分说明自定义的Module是如何组织定义在构造函数中的子Module,以及自定义的parameters的保存形式,eg:

  1.  
    class Net(nn.Module):
  2.  
    def __init__(self):
  3.  
    super(Net, self).__init__()
  4.  
    self.linear = nn.Linear(4, 2)
  5.  
     
  6.  
    def forward(self, input):
  7.  
    out = F.softmax(self.linear(input))
  8.  
    return out

首先看构造函数,Module的构造函数初始化了Module的基本属性,这里关注_parameters和_modules,两个属性初始化为OrderedDict(),pytorch重写的有序字典类型。_parameters保存网络的所有参数,_modules保存当前Module的子Module。
module.py:

  1.  
    class Module(object):
  2.  
     
  3.  
    def __init__(self):
  4.  
    self._parameters = OrderedDict()
  5.  
    self._modules = OrderedDict()
  6.  
    ...

下面来看自定义Net类中self.linear = nn.Linear(4, 2)语句和_modules、_parameters如何产生联系,或者self.linear及其参数如何被添加到_modules、_parameters字典中。答案在Module的____setattr____方法,该Python内建方法会在类的属性被赋值时调用。
module.py:

  1.  
    def __setattr__(self, name, value):
  2.  
    def remove_from(*dicts):
  3.  
    for d in dicts:
  4.  
    if name in d:
  5.  
    del d[name]
  6.  
     
  7.  
    params = self.__dict__.get('_parameters')
  8.  
    if isinstance(value, Parameter): # ----------- <1>
  9.  
    if params is None:
  10.  
    raise AttributeError(
  11.  
    "cannot assign parameters before Module.__init__() call")
  12.  
    remove_from(self.__dict__, self._buffers, self._modules)
  13.  
    self.register_parameter(name, value)
  14.  
    elif params is not None and name in params:
  15.  
    if value is not None:
  16.  
    raise TypeError("cannot assign '{}' as parameter '{}' "
  17.  
    "(torch.nn.Parameter or None expected)"
  18.  
    .format(torch.typename(value), name))
  19.  
    self.register_parameter(name, value)
  20.  
    else:
  21.  
    modules = self.__dict__.get('_modules')
  22.  
    if isinstance(value, Module):# ----------- <2>
  23.  
    if modules is None:
  24.  
    raise AttributeError(
  25.  
    "cannot assign module before Module.__init__() call")
  26.  
    remove_from(self.__dict__, self._parameters, self._buffers)
  27.  
    modules[name] = value
  28.  
    elif modules is not None and name in modules:
  29.  
    if value is not None:
  30.  
    raise TypeError("cannot assign '{}' as child module '{}' "
  31.  
    "(torch.nn.Module or None expected)"
  32.  
    .format(torch.typename(value), name))
  33.  
    modules[name] = value
  34.  
    ......
  35.  
     

调用self.linear = nn.Linear(4, 2)时,父类____setattr____被调用,参数name为“linear”, value为nn.Linear(4, 2),内建的Linear类同样是Module的子类。所以<2>中的判断为真,接着modules[name] = value,该linear被加入_modules字典。
同样自定义Net类的参数即为其子模块Linear的参数,下面看Linear的实现:
linear.py:

  1.  
    class Linear(Module):
  2.  
     
  3.  
    def __init__(self, in_features, out_features, bias=True):
  4.  
    super(Linear, self).__init__()
  5.  
    self.in_features = in_features
  6.  
    self.out_features = out_features
  7.  
    self.weight = Parameter(torch.Tensor(out_features, in_features))
  8.  
    if bias:
  9.  
    self.bias = Parameter(torch.Tensor(out_features))
  10.  
    else:
  11.  
    self.register_parameter('bias', None)
  12.  
    self.reset_parameters()
  13.  
     
  14.  
    def reset_parameters(self):
  15.  
    stdv = 1. / math.sqrt(self.weight.size(1))
  16.  
    self.weight.data.uniform_(-stdv, stdv)
  17.  
    if self.bias is not None:
  18.  
    self.bias.data.uniform_(-stdv, stdv)
  19.  
     
  20.  
    def forward(self, input):
  21.  
    return F.linear(input, self.weight, self.bias)

同样继承Module类,____init____中参数为输入输出维度,是否需要bias参数。在self.weight = Parameter(torch.Tensor(out_features, in_features))的初始化时,同样会调用父类Module的____setattr____, name为“weight”,value为Parameter,此时<1>判断为真,调用self.register_parameter(name, value),该方法中对参数进行合法性校验后放入self._parameters字典中。

Linear在reset_parameters方法对权重进行了初始化。

最终可以得出结论自定义的Module以树的形式组织子Module,子Module及其参数以字典的方式保存。

前向传播、反向传播

前向传播

例子中out = net(features)实现了网络的前向传播,该语句会调用Module类的forward方法,该方法被继承父类的子类实现。net(features)使用对象作为函数调用,会调用Python内建的____call____方法,Module重写了该方法。

module.py:

  1.  
    def __call__(self, *input, **kwargs):
  2.  
    for hook in self._forward_pre_hooks.values():
  3.  
    hook(self, input)
  4.  
    result = self.forward(*input, **kwargs)
  5.  
    for hook in self._forward_hooks.values():
  6.  
    hook_result = hook(self, input, result)
  7.  
    if hook_result is not None:
  8.  
    raise RuntimeError(
  9.  
    "forward hooks should never return any values, but '{}'"
  10.  
    "didn't return None".format(hook))
  11.  
    if len(self._backward_hooks) > 0:
  12.  
    var = result
  13.  
    while not isinstance(var, Variable):
  14.  
    var = var[0]
  15.  
    grad_fn = var.grad_fn
  16.  
    if grad_fn is not None:
  17.  
    for hook in self._backward_hooks.values():
  18.  
    wrapper = functools.partial(hook, self)
  19.  
    functools.update_wrapper(wrapper, hook)
  20.  
    grad_fn.register_hook(wrapper)
  21.  
    return result

____call____方法中调用result = self.forward(*input, **kwargs)前后会查看有无hook函数需要调用(预处理和后处理)。
例子中Net的forward方法中out = F.softmax(self.linear(input)),同样会调用self.linear的forward方法F.linear(input, self.weight, self.bias)进行矩阵运算(仿射变换)。
functional.py:

  1.  
    def linear(input, weight, bias=None):
  2.  
    if input.dim() == 2 and bias is not None:
  3.  
    # fused op is marginally faster
  4.  
    return torch.addmm(bias, input, weight.t())
  5.  
     
  6.  
    output = input.matmul(weight.t())
  7.  
    if bias is not None:
  8.  
    output += bias
  9.  
    return output

最终经过F.softmax,得到前向输出结果。F.softmax和F.linear类似前面说到的Function(Parameters的表达式或计算图)。

反向传播

得到前向传播结果后,计算loss = F.cross_entropy(out, target),接下来反向传播求导数d(loss)/d(weight)和d(loss)/d(bias):

loss.backward()

backward()方法同样底层由C拓展,这里暂不深入,调用该方法后,loss计算图中的所有Variable(这里linear的weight和bias)的grad被求出。

Optimizer参数更新

在计算出参数的grad后,需要根据优化算法对参数进行更新,不同的优化算法有不同的更新策略。
optimizer.py:

  1.  
    class Optimizer(object):
  2.  
     
  3.  
    def __init__(self, params, defaults):
  4.  
    if isinstance(params, Variable) or torch.is_tensor(params):
  5.  
    raise TypeError("params argument given to the optimizer should be "
  6.  
    "an iterable of Variables or dicts, but got " +
  7.  
    torch.typename(params))
  8.  
     
  9.  
    self.state = defaultdict(dict)
  10.  
    self.param_groups = list(params)
  11.  
    ......
  12.  
     
  13.  
    def zero_grad(self):
  14.  
    """Clears the gradients of all optimized :class:`Variable` s."""
  15.  
    for group in self.param_groups:
  16.  
    for p in group['params']:
  17.  
    if p.grad is not None:
  18.  
    if p.grad.volatile:
  19.  
    p.grad.data.zero_()
  20.  
    else:
  21.  
    data = p.grad.data
  22.  
    p.grad = Variable(data.new().resize_as_(data).zero_())
  23.  
     
  24.  
    def step(self, closure):
  25.  
    """Performs a single optimization step (parameter update).
  26.  
     
  27.  
    Arguments:
  28.  
    closure (callable): A closure that reevaluates the model and
  29.  
    returns the loss. Optional for most optimizers.
  30.  
    """
  31.  
    raise NotImplementedError

Optimizer在init中将传入的params保存到self.param_groups,另外两个重要的方法zero_grad负责将参数的grad置零方便下次计算,step负责参数的更新,由子类实现。
以列子中的sgd = torch.optim.SGD(net.parameters(), lr=0.001)为例,其中net.parameters()返回Net参数的迭代器,为待优化参数;lr指定学习率。
SGD.py:

  1.  
    class SGD(Optimizer):
  2.  
     
  3.  
    def __init__(self, params, lr=required, momentum=0, dampening=0,
  4.  
    weight_decay=0, nesterov=False):
  5.  
    defaults = dict(lr=lr, momentum=momentum, dampening=dampening,
  6.  
    weight_decay=weight_decay, nesterov=nesterov)
  7.  
    if nesterov and (momentum <= 0 or dampening != 0):
  8.  
    raise ValueError("Nesterov momentum requires a momentum and zero dampening")
  9.  
    super(SGD, self).__init__(params, defaults)
  10.  
     
  11.  
    def __setstate__(self, state):
  12.  
    super(SGD, self).__setstate__(state)
  13.  
    for group in self.param_groups:
  14.  
    group.setdefault('nesterov', False)
  15.  
     
  16.  
    def step(self, closure=None):
  17.  
    """Performs a single optimization step.
  18.  
     
  19.  
    Arguments:
  20.  
    closure (callable, optional): A closure that reevaluates the model
  21.  
    and returns the loss.
  22.  
    """
  23.  
    loss = None
  24.  
    if closure is not None:
  25.  
    loss = closure()
  26.  
     
  27.  
    for group in self.param_groups:
  28.  
    weight_decay = group['weight_decay']
  29.  
    momentum = group['momentum']
  30.  
    dampening = group['dampening']
  31.  
    nesterov = group['nesterov']
  32.  
     
  33.  
    for p in group['params']:
  34.  
    if p.grad is None:
  35.  
    continue
  36.  
    d_p = p.grad.data
  37.  
    if weight_decay != 0:
  38.  
    d_p.add_(weight_decay, p.data)
  39.  
    if momentum != 0:
  40.  
    param_state = self.state[p]
  41.  
    if 'momentum_buffer' not in param_state:
  42.  
    buf = param_state['momentum_buffer'] = d_p.clone()
  43.  
    else:
  44.  
    buf = param_state['momentum_buffer']
  45.  
    buf.mul_(momentum).add_(1 - dampening, d_p)
  46.  
    if nesterov:
  47.  
    d_p = d_p.add(momentum, buf)
  48.  
    else:
  49.  
    d_p = buf
  50.  
     
  51.  
    p.data.add_(-group['lr'], d_p)
  52.  
     
  53.  
    return loss
  54.  
     

SGD的step方法中,判断是否使用权重衰减和动量更新,如果不使用,直接更新权重param := param - lr * d(param)。例子中调用sgd.step()后完成一次epoch。这里由于传递到Optimizer的参数集是可更改(mutable)的,step中对参数的更新同样是Net中参数的更新。

小结

到此,根据一个简单例子阅读了pytorch中Python实现的部分源码,没有深入到底层Tensor、autograd等部分的C拓展实现,后面再继续读一读C拓展部分的代码。

转自链接:https://www.jianshu.com/p/f5eb8c2e671c

pytorch源码解析:Python层 pytorchmodule源码的更多相关文章

  1. 【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...

  2. Django生命周期 URL ----> CBV 源码解析-------------- 及rest_framework APIView 源码流程解析

    一.一个请求来到Django 的生命周期   FBV 不讨论 CBV: 请求被代理转发到uwsgi: 开始Django的流程: 首先经过中间件process_request (session等) 然后 ...

  3. Tensorflow源码解析1 -- 内核架构和源码结构

    1 主流深度学习框架对比 当今的软件开发基本都是分层化和模块化的,应用层开发会基于框架层.比如开发Linux Driver会基于Linux kernel,开发Android app会基于Android ...

  4. java架构之路-(SpringMVC篇)SpringMVC主要流程源码解析(上)源码执行流程

    做过web项目的小伙伴,对于SpringMVC,Struts2都是在熟悉不过了,再就是我们比较古老的servlet,我们先来复习一下我们的servlet生命周期. servlet生命周期 1)初始化阶 ...

  5. Redis源码解析(1)——源码目录介绍

    概念 redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合)和zset(有序集合).这些 ...

  6. ubuntu源与常用python配置pip源(win)、pip常用命令

    pip常用命令 ubuntu更新系统源 首先备份/etc/apt/sources.list mv /etc/apt/sources.list /etc/apt/sources.list.bak 然后下 ...

  7. SOFARPC源码解析-搭建环境

    文档地址:https://www.sofastack.tech 简介摘要 SOFA 是蚂蚁金服自主研发的金融级分布式中间件,包含构建金融级云原生架构所需的各个组件,包括微服务研发框架,RPC 框架,服 ...

  8. Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析

    目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...

  9. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

随机推荐

  1. For循环和闭包问题

    考虑一下以下的代码片段: for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.append ...

  2. “本地视频使用flashFXP上传虚拟服务器“的方法

    一.视频转换格式 首先,想要在网页中直接嵌入视频,就得用video标签,而<video>支持的仅有的几种格式中,MP4是兼容性,通用性各方面相对友好的,所以,建议上传之前先转换格式并压缩. ...

  3. Java 8最快的垃圾收集器是什么?

    OpenJDK 8 有多种 GC(Garbage Collector)算法,如 Parallel GC.CMS 和 G1.哪一个才是最快的呢?如果在 Java 9 中将 Java 8 默认的 GC 从 ...

  4. git之操作准则

    每天下班前合一次代码,每次合代码先pull 不要多人同时修改同一个文件,避免冲突 在每个人自己的分支进行开发,先合并到dev分支解决冲突,确认无冲突后再合并到master

  5. Spring Boot:Boot2.0版本整合Neo4j

    前面介绍了Boot 1.5版本集成Neo4j,Boot 2.0以上版本Neo4j变化较大. 场景还是电影人员关系 Boot 2.0主要变化 GraphRepository在Boot2.0下不支持了,调 ...

  6. AudioToolbox.framework框架学习

    http://quding0308.iteye.com/blog/1596710 AudioFile AudioFileStream类 提供了一个接口,用来解析一个流音频文件. //创建一个new s ...

  7. HTML5拖放API实现拖放排序的实例代码

    想要拖放某个元素,必须设置该元素的 draggable 属性为 true,当该属性为 false 时,将不允许拖放.而 img 元素和 a 元素都默认设置了 draggable 属性为 true,可直 ...

  8. svn钩子(hooks)自动部署代码到web目录

      版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/li956732806/article/details/71158869   web目录:/hoe ...

  9. (五)IO流之ByteArrayInput/OutputStream

    ByteArrayInputStream:是把字节数组当成源的输入流 String string="hello shanghai"; ByteArrayInputStream bi ...

  10. 2019.9.19登陆注册猜数字给奖品combo

    #注册函数 def register(): registor_count = 0 while registor_count < 3: username_inp = input('user nam ...