动手学深度学习——PyTorch 神经网络基础:参数管理详解

动手学深度学习——PyTorch 神经网络基础:参数管理详解 一、前言在上一节“模型构造”中我们已经知道了如何用 PyTorch 定义一个神经网络比如用nn.Sequential搭建顺序模型继承nn.Module自定义网络在__init__()中定义层在forward()中定义前向传播过程但当我们真正开始训练模型时很快就会遇到一个更具体的问题模型里的参数到底存在哪里我们该怎么查看、访问、初始化、共享和修改这些参数这就是“参数管理”要解决的核心问题。因为神经网络训练的本质说到底就是不断更新模型参数使模型输出越来越接近真实目标。所以如果不会管理参数那么你看不懂模型内部到底学了什么你不会初始化参数你不会冻结某些层你不会做参数共享你也很难真正理解训练过程因此这一节虽然表面看起来像是在学 PyTorch 的接口但实际上它非常关键。它会帮助我们真正走近神经网络内部。二、什么是参数管理所谓参数管理本质上就是对模型中的可学习参数进行查看、访问、修改和组织。在神经网络中最常见的参数包括权重weight偏置bias例如一个全连接层这里的(W) 就是权重参数(b) 就是偏置参数训练时优化器不断更新的就是这些东西。所以参数管理关注的是怎么拿到这些参数怎么看它们的形状怎么单独访问某一层的参数怎么对参数做初始化怎么让不同层共享同一组参数三、为什么参数管理很重要1. 训练本质上是在更新参数你如果不知道参数在哪里、长什么样就等于不知道模型真正学的是什么。2. 很多高级操作都依赖参数管理例如自定义初始化冻结某些层迁移学习参数共享模型保存与加载这些都离不开参数管理。3. 它能帮助你理解模型内部结构一个模型表面看是很多层往里看其实就是很多参数张量。学会参数管理之后你对网络的理解会更“落地”。四、先看一个简单模型下面先定义一个最基础的多层感知机import torch from torch import nn net nn.Sequential( nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1) ) print(net)这个模型里有两层线性层第一层Linear(4, 8)第二层Linear(8, 1)而真正有参数的是这两层Linear中间的ReLU没有可学习参数。五、如何查看某一层的参数例如我们想查看第二个线性层也就是net[2]可以直接打印它的参数print(net[2].state_dict())输出通常会类似于OrderedDict([ (weight, tensor(...)), (bias, tensor(...)) ])这说明这个层中包含weightbias也就是说state_dict()可以把该层当前保存的参数状态展示出来。六、state_dict() 到底是什么state_dict()是 PyTorch 里非常重要的一个方法。它的作用可以理解为返回模型当前所有参数和缓冲区的状态字典。对初学阶段来说你可以简单把它理解成模型参数的“总账本”比如print(net.state_dict())你会看到整个模型中所有层的参数都被组织在一个字典中例如0.weight0.bias2.weight2.bias这里的0和2表示Sequential中对应层的编号。七、如何直接访问权重和偏置如果你只想访问某一层的权重可以这样写print(net[0].weight) print(net[0].bias)例如第一层Linear(4, 8)权重形状通常是[8, 4]偏置形状通常是[8]因为这层表示[y Wx b]输入 4 维输出 8 维所以权重矩阵大小就是8 × 4。八、param.data 和 param 本身有什么区别当你访问参数时比如print(net[0].weight)你拿到的是一个Parameter对象它本质上是特殊的张量。如果你只想看里面实际的数据值可以写print(net[0].weight.data)简单理解weight是参数对象包含梯度等训练信息weight.data更偏向于参数内部的纯数值张量平时查看数值时经常会看到.data。九、named_parameters()更系统地查看参数如果你想遍历一个层或整个模型中的参数可以使用for name, param in net.named_parameters(): print(name, param.shape)输出一般类似0.weight torch.Size([8, 4]) 0.bias torch.Size([8]) 2.weight torch.Size([1, 8]) 2.bias torch.Size([1])这个方法非常实用因为它可以同时告诉你参数名字参数形状这对于调试模型特别方便。十、parameters() 和 named_parameters() 的区别这两个方法很像但有一点差别。1.parameters()只返回参数本身for param in net.parameters(): print(param.shape)2.named_parameters()同时返回参数名称和参数本身for name, param in net.named_parameters(): print(name, param.shape)所以一般来说只想把参数交给优化器用parameters()想调试、查看名字用named_parameters()十一、为什么优化器只更新参数层例如optimizer torch.optim.SGD(net.parameters(), lr0.1)这里传进去的是net.parameters()。这意味着优化器会自动找到模型中所有可学习参数。而像ReLU这种没有参数的层不会出现在里面。所以你会发现不是所有层都有参数只有可学习层才有参数。比如Linear有参数Conv2d有参数BatchNorm有参数ReLU没有参数Sigmoid没有参数十二、如何从嵌套块中访问参数很多模型不是简单一层层平铺而是嵌套结构。例如def block1(): return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 4), nn.ReLU()) def block2(): net nn.Sequential() for i in range(4): net.add_module(fblock {i}, block1()) return net rgnet nn.Sequential(block2(), nn.Linear(4, 1)) print(rgnet)这个模型会比较复杂里面套了很多Sequential。如果你想访问某个深层参数可以一层层索引print(rgnet[0][1][0].bias.data)也就是说rgnet[0]第一个大块[1]里面第二个子块[0]这个子块里的第一层线性层虽然写起来有点长但这说明PyTorch 的模型结构本质上是可以层层访问的。十三、如何一次性初始化所有参数参数管理中最常见的需求之一就是自定义初始化。例如我们想把所有线性层的权重初始化为均值 0、标准差 0.01 的正态分布def init_normal(m): if type(m) nn.Linear: nn.init.normal_(m.weight, mean0, std0.01) nn.init.zeros_(m.bias) net.apply(init_normal) print(net[0].weight.data[0], net[0].bias.data[0])这里的核心是net.apply(init_normal)它会把init_normal函数递归地应用到模型中的每个子模块。十四、常见初始化方式有哪些1. 正态分布初始化nn.init.normal_(m.weight, mean0, std0.01)2. 常数初始化nn.init.constant_(m.weight, 1) nn.init.zeros_(m.bias)3. Xavier 初始化nn.init.xavier_uniform_(m.weight)4. He 初始化nn.init.kaiming_uniform_(m.weight, nonlinearityrelu)这些初始化方式你在前面的“模型初始化”博客里已经接触过了。现在到了参数管理这一节你会发现它们终于能真正和代码对上了。十五、如何只初始化某些参数有时候我们不想把所有层都一样初始化而是只改某一层。比如只初始化第一层nn.init.normal_(net[0].weight, mean0, std0.01) nn.init.zeros_(net[0].bias)或者只对名字里包含weight的参数做处理for name, param in net.named_parameters(): if weight in name: nn.init.normal_(param, mean0, std0.01)这样就更灵活。十六、可以直接修改参数值吗可以。比如net[0].weight.data[:] 42 net[0].bias.data[:] 0这表示第一层所有权重全改成 42第一层所有偏置全改成 0当然实际训练中一般不会这么干但这个例子能说明参数本质上就是张量所以是可以直接操作的。也正因为如此PyTorch 在调试和实验时非常灵活。十七、参数共享是什么意思参数共享是这一节非常有代表性的内容。所谓参数共享就是多个地方使用同一组参数而不是各自拥有独立参数。例如我们先定义一个共享层shared nn.Linear(8, 8)然后把它放进网络中多次使用net nn.Sequential( nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1) )这里shared被用了两次。十八、为什么说这是“共享”而不是“复制”因为这里不是重新创建了两个Linear(8,8)而是把同一个层对象用了两次。所以这两个位置实际指向的是同一组参数。你可以验证print(net[2].weight.data[0] net[4].weight.data[0])一般会发现它们完全一样。进一步如果你修改其中一个net[2].weight.data[0, 0] 100 print(net[4].weight.data[0, 0])另一个对应位置也会变化。这就说明它们确实共享同一份参数。十九、参数共享有什么用参数共享在深度学习中非常重要不只是一个“技巧”。1. 减少参数量共享参数后模型参数更少内存占用更低。2. 强制不同位置学习相同模式例如卷积神经网络本质上就带有参数共享思想。同一个卷积核在不同位置滑动其实就是在共享参数。3. 在某些结构设计中很自然比如Siamese Network某些递归结构特定残差或重复模块所以理解参数共享对后面学习 CNN 等结构很有帮助。二十、如何判断参数是不是同一个有时我们想确认两层参数到底是不是共享的可以直接比较对象标识print(net[2].weight is net[4].weight)如果结果是True就说明它们真的是同一个参数对象而不是值恰好相等。这一点比只比较数值更本质。二十一、这一节的核心思想到底是什么如果把“参数管理”这一节压缩成一句话我觉得就是模型不是神秘黑箱本质上就是一组可以被组织、访问、修改和共享的参数。前面“模型构造”让我们知道了网络怎么搭这一节“参数管理”则让我们开始真正接触网络内部。从这时候开始你对神经网络的理解就不只是“层和层”而是每层里有什么参数参数长什么样参数如何初始化参数如何被优化器更新参数如何被共享和复用这会让你对模型的认识更深入一层。二十二、初学者最容易混淆的几个点1. 不是所有层都有参数例如ReLU没有参数Linear才有。2.state_dict()看到的是参数状态不只是权重它是整个模型参数的总表。3.parameters()和named_parameters()不一样前者只有参数后者还有名字。4.self.xxx 某层才会注册参数临时变量不会被模型自动管理。5. 参数共享不是“数值一样”而是“对象本身就是同一个”这一点很关键。二十三、一个适合 CSDN 的完整示例下面给你一份适合放博客里的完整演示代码import torch from torch import nn # 1. 定义模型 net nn.Sequential( nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1) ) # 2. 查看整个模型参数 print( state_dict ) print(net.state_dict()) # 3. 查看每个参数名字和形状 print(\n named_parameters ) for name, param in net.named_parameters(): print(name, param.shape) # 4. 自定义初始化 def init_normal(m): if type(m) nn.Linear: nn.init.normal_(m.weight, mean0, std0.01) nn.init.zeros_(m.bias) net.apply(init_normal) print(\n 初始化后的第一层权重 ) print(net[0].weight.data) # 5. 参数共享 shared nn.Linear(8, 8) net2 nn.Sequential( nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1) ) print(\n 参数共享验证 ) print(net2[2].weight is net2[4].weight)这段代码基本把本节最核心的内容都串起来了。二十四、我对这一节的理解学完“参数管理”之后我最大的感受是以前写神经网络时更多只是把层堆起来感觉模型像个整体黑箱但这一节让我第一次开始真正把模型拆开来看。我发现每一层其实就是若干参数张量参数是可以查看的参数是可以单独初始化的参数甚至还可以共享这让我对神经网络的理解更具体了。原来训练模型不是某种神秘过程而是在不断调整这些权重和偏置。所以这一节虽然不像卷积层那样“看起来很炫”但它非常基础也非常关键。二十五、结语“参数管理”是 PyTorch 神经网络基础中非常重要的一节。如果说“模型构造”解决的是“怎么搭网络”那么“参数管理”解决的就是怎么真正看懂网络里面的参数并对它们进行控制。这一节学扎实之后后面你再去学自定义层模型初始化参数冻结卷积网络模型保存与加载都会轻松很多。所以建议这一节一定不要只停留在“看懂”最好自己亲手试一下state_dict()named_parameters()apply()参数共享这些操作。二十六、重点速记版1. 什么是参数管理对模型中的权重和偏置进行查看、访问、修改和组织。2. 查看某层参数用什么state_dict()3. 遍历模型参数用什么parameters()或named_parameters()4. 初始化整个模型常用什么net.apply(初始化函数)5. 参数能不能直接改可以本质上它们是张量。6. 什么是参数共享多个位置使用同一组参数对象。7. 为什么参数共享重要可以减少参数量并让不同位置学习相同模式。以上就是我对《动手学深度学习》中PyTorch 神经网络基础——参数管理这一节的学习整理。这一节让我对神经网络的理解进一步从“搭结构”深入到了“看参数”。也让我真正明白模型训练的本质其实就是对这些参数不断进行更新和优化。对于刚开始学 PyTorch 的同学来说这一节非常值得自己动手敲一遍。因为只有真正看过参数、改过参数、初始化过参数后面学习更复杂的网络结构时才不会一直停留在“黑箱使用”的阶段。