xinmeng_wit 发表于 2024-10-28 22:04

《动手学深度学习(PyTorch版)》4、深度学习计算

本帖最后由 xinmeng_wit 于 2024-10-28 22:04 编辑

<p><span style="font-size:16px;"><strong>一、介绍</strong></span></p>

<p>本次分享,我们将深入探索深度学习计算的关键组件, 即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘, 以及利用GPU实现显著的加速。 这些知识将使读者从深度学习&ldquo;基础用户&rdquo;变为&ldquo;高级用户&rdquo;。 虽然本章不介绍任何新的模型或数据集, 但后面的高级模型章节在很大程度上依赖于本章的知识。</p>

<p>&nbsp;</p>

<p><span style="font-size:16px;"><strong>二、层和块</strong></span></p>

<p>研究讨论&ldquo;比单个层大&rdquo;但&ldquo;比整个模型小&rdquo;的组件更有价值。 例如,在计算机视觉中广泛流行的ResNet-152架构就有数百层, 这些层是由<em>层组</em>(groups of layers)的重复模式组成。 这个ResNet架构赢得了2015年ImageNet和COCO计算机视觉比赛 的识别和检测任务。 目前ResNet架构仍然是许多视觉任务的首选架构。 在其他的领域,如自然语言处理和语音, 层组以各种重复模式排列的类似架构现在也是普遍存在。</p>

<p>为了实现这些复杂的网络,我们引入了神经网络<em>块</em>的概念。&nbsp;<em>块</em>(block)可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件, 这一过程通常是递归的,如下图所示。 通过定义代码来按需生成任意复杂度的块, 我们可以通过简洁的代码实现复杂的神经网络。</p>

<div style="text-align: left;"></div>

<div style="text-align: left;">&nbsp;</div>

<div style="text-align: left;">块由<em>类</em>(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数。 注意,有些块不需要任何参数。 最后,为了计算梯度,块必须具有反向传播函数。 在定义我们自己的块时,由于自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。</div>

<div style="text-align: left;">给个例子:</div>

<pre>
<code class="language-python">import torch
from torch import nn
from torch.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = torch.rand(2, 20)
net(X)</code></pre>

<p>上面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层, 然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。</p>

<p>在这个例子中,通过实例化<code>nn.Sequential</code>来构建我们的模型, 层的执行顺序是作为参数传递的。 可以认为,<code>nn.Sequential</code>定义了一种特殊的<code>Module</code>, 即在PyTorch中表示一个块的类, 它维护了一个由<code>Module</code>组成的有序列表。注意,两个全连接层都是<code>Linear</code>类的实例,&nbsp;<code>Linear</code>类本身就是<code>Module</code>的子类。这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。</p>

<p>&nbsp;</p>

<p><strong>1、自定义块</strong></p>

<p>要想直观地了解块是如何工作的,最简单的方法就是自己实现一个。 在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能。</p>

<ol>
        <li>
        <p><em>将输入数据作为其前向传播函数的参数。</em></p>
        </li>
        <li>
        <p><em>通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。</em></p>
        </li>
        <li>
        <p><em>计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。</em></p>
        </li>
        <li>
        <p><em>存储和访问前向传播计算所需的参数。</em></p>
        </li>
        <li>
        <p><em>根据需要初始化模型参数。</em></p>
        </li>
</ol>

<p>如下代码,从零开始编写一个块。它包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层。</p>

<p>注意,下面的<code>MLP</code>类继承了表示块的类。 我们的实现只需要提供我们自己的构造函数(Python中的<code>__init__</code>函数)和前向传播函数。</p>

<pre>
<code class="language-python">class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
      # 调用MLP的父类Module的构造函数来执行必要的初始化。
      # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
      super().__init__()
      self.hidden = nn.Linear(20, 256)# 隐藏层
      self.out = nn.Linear(256, 10)# 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
      # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
      return self.out(F.relu(self.hidden(X)))</code></pre>

<p>接着我们实例化多层感知机的层,然后在每次调用前向传播函数时调用这些层。 注意一些关键细节: 首先,我们定制的<code>__init__</code>函数通过<code>super().__init__()</code>调用父类的<code>__init__</code>函数, 省去了重复编写模版代码的痛苦。 然后,我们实例化两个全连接层, 分别为<code>self.hidden</code>和<code>self.out。</code>注意,除非我们实现一个新的运算符, 否则我们不必担心反向传播函数或参数初始化, 系统将自动生成这些。</p>

<p>块的一个主要优点是它的多功能性。 我们可以子类化块以创建层(如全连接层的类)、 整个模型(如上面的<code>MLP</code>类)或具有中等复杂度的各种组件。 我们在接下来的章节中充分利用了这种多功能性, 比如在处理卷积神经网络时。</p>

<p>&nbsp;</p>

<p><strong>2、顺序块</strong></p>

<p>为了构建我们自己的简化的<code>MySequential</code>, 我们只需要定义两个关键函数:</p>

<ol>
        <li>
        <p><em>一种将块逐个追加到列表中的函数;</em></p>
        </li>
        <li>
        <p><em>一种前向传播函数,用于将输入按追加块的顺序传递给块组成的&ldquo;链条&rdquo;。</em></p>
        </li>
</ol>

<p>下面的<code>MySequential</code>类提供了与默认<code>Sequential</code>类相同的功能。</p>

<pre>
<code class="language-python">class MySequential(nn.Module):
    def __init__(self, *args):
      super().__init__()
      for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。_module的类型是OrderedDict
            self._modules = module

    def forward(self, X):
      # OrderedDict保证了按照成员添加的顺序遍历它们
      for block in self._modules.values():
            X = block(X)
      return X</code></pre>

<p>当<code>MySequential</code>的前向传播函数被调用时, 每个添加的块都按照它们被添加的顺序执行。 现在可以使用我们的<code>MySequential</code>类重新实现多层感知机。</p>

<pre>
<code class="language-python">net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)</code></pre>

<p><strong>3、在前向传播函数中执行代码</strong><br />
<code>Sequential</code>类使模型构造变得简单, 允许我们组合新的架构,而不必定义自己的类。 然而,并不是所有的架构都是简单的顺序架构。 当需要更强的灵活性时,我们需要定义自己的块。 例如,我们可能希望在前向传播函数中执行Python的控制流。 此外,我们可能希望执行任意的数学运算, 而不是简单地依赖预定义的神经网络层。</p>

<p>到目前为止, 我们网络中的所有操作都对网络的激活值及网络的参数起作用。 然而,有时我们可能希望合并既不是上一层的结果也不是可更新参数的项, 我们称之为<em>常数参数</em>(constant parameter)。 例如,我们需要一个计算函数&nbsp;f(x,w)=c&sdot;w⊤x的层, 其中x是输入,&nbsp;w是参数,&nbsp;c是某个在优化过程中没有更新的指定常量。 因此我们实现了一个<code>FixedHiddenMLP</code>类,如下所示:</p>

<pre>
<code class="language-python">class FixedHiddenMLP(nn.Module):
    def __init__(self):
      super().__init__()
      # 不计算梯度的随机权重参数。因此其在训练期间保持不变
      self.rand_weight = torch.rand((20, 20), requires_grad=False)
      self.linear = nn.Linear(20, 20)

    def forward(self, X):
      X = self.linear(X)
      # 使用创建的常量参数以及relu和mm函数
      X = F.relu(torch.mm(X, self.rand_weight) + 1)
      # 复用全连接层。这相当于两个全连接层共享参数
      X = self.linear(X)
      # 控制流
      while X.abs().sum() &gt; 1:
            X /= 2
      return X.sum()</code></pre>

<p>&nbsp;</p>

<p><span style="font-size:16px;"><strong>三、参数管理</strong></span></p>

<p>参数管理包括:</p>

<ul>
        <li>
        <p><em>访问参数,用于调试、诊断和可视化;</em></p>
        </li>
        <li>
        <p><em>参数初始化;</em></p>
        </li>
        <li>
        <p><em>在不同模型组件间共享参数。</em></p>
        </li>
</ul>

<p><strong>1、参数访问</strong></p>

<p>&nbsp;当通过<code>Sequential</code>类定义模型时, 我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,我们可以检查第二个全连接层的参数。</p>

<pre>
<code class="language-python">import torch
from torch import nn

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)
print(net.state_dict())
</code></pre>

<p>输出:</p>

<pre>
<code class="language-python">OrderedDict([('weight', tensor([[-0.1789,  0.1440,  0.1701, -0.0851, -0.2492,  0.2091, -0.0592,  0.0114]])), ('bias', tensor([-0.1112]))])</code></pre>

<p>输出的结果告诉我们一些重要的事情: 首先,这个全连接层包含两个参数,分别是该层的权重和偏置。 两者都存储为单精度浮点数(float32)。 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。</p>

<p>还有一些访问参数的方法,本篇不做详细介绍。</p>

<p>&nbsp;</p>

<p><strong>2、参数初始化</strong></p>

<p>深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。</p>

<p>默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的<code>nn.init</code>模块提供了多种预置初始化方法。</p>

<p><strong>--&gt;内置初始化</strong></p>

<p>下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0。</p>

<pre>
<code class="language-python">def init_normal(m):
    if type(m) == nn.Linear:
      nn.init.normal_(m.weight, mean=0, std=0.01)
      nn.init.zeros_(m.bias)
net.apply(init_normal)
net.weight.data, net.bias.data</code></pre>

<p>我们还可以将所有参数初始化为给定的常数,比如初始化为1。</p>

<pre>
<code class="language-python">def init_constant(m):
    if type(m) == nn.Linear:
      nn.init.constant_(m.weight, 1)
      nn.init.zeros_(m.bias)
net.apply(init_constant)
net.weight.data, net.bias.data</code></pre>

<p>我们还可以对某些块应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。</p>

<pre>
<code class="language-python">def init_xavier(m):
    if type(m) == nn.Linear:
      nn.init.xavier_uniform_(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
      nn.init.constant_(m.weight, 42)

net.apply(init_xavier)
net.apply(init_42)
print(net.weight.data)
print(net.weight.data)</code></pre>

<p><strong>--&gt;自定义参数</strong></p>

<pre>
<code class="language-python">net.weight.data[:] += 1
net.weight.data = 42
net.weight.data</code></pre>

<p><span style="font-size:16px;"><strong>四、自定义层</strong></span></p>

<p>有时我们会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 在这些情况下,必须构建自定义层。本节将展示如何构建自定义层。</p>

<p>下面的例子构造了一个没有任何参数的自定义层:</p>

<pre>
<code class="language-python">import torch
import torch.nn.functional as F
from torch import nn


class CenteredLayer(nn.Module):
    def __init__(self):
      super().__init__()

    def forward(self, X):
      return X - X.mean()</code></pre>

<p>现在,我们可以将层作为组件合并到更复杂的模型中。</p>

<pre>
<code class="language-python">net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())</code></pre>

<p><span style="font-size:16px;"><strong>五、读写文件</strong></span></p>

<p><strong>1、加载和保存张量</strong></p>

<p>对于单个张量,我们可以直接调用<span style="background-color: rgb(250, 250, 250); color: rgb(56, 58, 66); font-family: &quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif; font-size: 12px;">load</span>和<span style="background-color: rgb(250, 250, 250); color: rgb(56, 58, 66); font-family: &quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif; font-size: 12px;">save</span>函数分别读写它们。 这两个函数都要求我们提供一个名称,<span style="background-color: rgb(250, 250, 250); color: rgb(56, 58, 66); font-family: &quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif; font-size: 12px;">save</span>要求将要保存的变量作为输入。</p>

<pre>
<code class="language-python">import torch
from torch import nn
from torch.nn import functional as F

x = torch.arange(4)
torch.save(x, 'x-file')</code></pre>

<p>我们现在可以将存储在文件中的数据读回内存。</p>

<pre>
<code class="language-python">x2 = torch.load('x-file')
x2</code></pre>

<p>我们可以存储一个张量列表,然后把它们读回内存。</p>

<pre>
<code class="language-python">y = torch.zeros(4)
torch.save(,'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)</code></pre>

<p>我们甚至可以写入或读取从字符串映射到张量的字典。 当我们要读取或写入模型中的所有权重时,这很方便。</p>

<pre>
<code class="language-python">mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2</code></pre>

<p><strong>2、加载和保存模型参数</strong></p>

<p>保存单个权重向量(或其他张量)确实有用, 但是如果我们想保存整个模型,并在以后加载它们, 单独保存每个向量则会变得很麻烦。 毕竟,我们可能有数百个参数散布在各处。 因此,深度学习框架提供了内置函数来保存和加载整个网络。 需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。 例如,如果我们有一个3层多层感知机,我们需要单独指定架构。 因为模型本身可以包含任意代码,所以模型本身难以序列化。 因此,为了恢复模型,我们需要用代码生成架构, 然后从磁盘加载参数。 让我们从熟悉的多层感知机开始尝试一下。</p>

<p>&nbsp;</p>

<p>保存模型参数:</p>

<pre>
<code class="language-python">torch.save(net.state_dict(), 'mlp.params')</code></pre>

<p>为了恢复模型,我们实例化了原始多层感知机模型的一个备份。 这里我们不需要随机初始化模型参数,而是直接读取文件中存储的参数。</p>

<pre>
<code class="language-python">clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()</code></pre>

<p><span style="font-size:16px;"><strong>六、GPU</strong></span></p>

<p>本次不做介绍,因为需要至少两个GPU,这对于一般的个人用户来说太奢侈了。哈哈~~~</p>

Jacktang 发表于 2024-10-29 07:27

<p>保存模型的参数而不是保存整个模型,这点同意</p>

hellokitty_bean 发表于 2024-10-29 08:58

Jacktang 发表于 2024-10-29 07:27
保存模型的参数而不是保存整个模型,这点同意

<p>天呐,这么早就上论坛了????</p>

<p>果然是early bird。。。。。。。。<img height="48" src="https://bbs.eeworld.com.cn/static/editor/plugins/hkemoji/sticker/facebook/loveliness.gif" width="48" /></p>
页: [1]
查看完整版本: 《动手学深度学习(PyTorch版)》4、深度学习计算