【Bug已解决】How to access the network weights while using PyTorch ‘nn.Sequential‘? 解决方案

【Bug已解决】How to access the network weights while using PyTorch 'nn.Sequential'? 解决方案

问题描述

在 PyTorch 中,nn.Sequential 是一个方便的容器模块,用于按顺序串联多个层。然而,当使用 nn.Sequential 构建模型时,访问内部各层的权重参数不如自定义 nn.Module 那样直观。许多开发者不知道如何正确地获取、修改或检查 nn.Sequential 中各层的权重,导致在模型调试、迁移学习、权重初始化等场景中遇到困难。

典型问题包括:

  • 如何获取 nn.Sequential 中特定层的权重?
  • 如何修改某一层的权重?
  • 如何遍历所有层的权重?
  • 如何给 nn.Sequential 中的层命名以便更方便地访问?
  • 如何在 nn.Sequential 中插入或替换层?

nn.Sequential 的设计理念是简洁——它通过整数索引(0, 1, 2, ...)来访问内部模块,而不是通过属性名。这使得它在简单模型中非常方便,但在需要精细控制权重的复杂场景中显得不够灵活。

错误复现

场景一:无法通过名称访问层

import torch
import torch.nn as nn

# 使用 nn.Sequential 构建模型
model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

# 尝试通过名称访问 - 失败
try:
    layer = model.fc1  # AttributeError
except AttributeError as e:
    print(f"错误: {e}")
    # 'Sequential' object has no attribute 'fc1'

场景二:不知道如何获取特定层权重

# 想获取第一个 Linear 层的权重
# 但不知道如何操作
weights = ???  # 如何获取?

# 尝试直接访问
try:
    weights = model.weight  # 失败
except AttributeError as e:
    print(f"错误: {e}")
    # 'Sequential' object has no attribute 'weight'

场景三:修改权重时出错

# 尝试修改第一个 Linear 层的权重
try:
    model[0].weight = nn.Parameter(torch.zeros(256, 784))
    # 可能成功,但如果形状不匹配会报错
except Exception as e:
    print(f"错误: {e}")

场景四:遍历权重时混淆参数和模块

# 想遍历所有层的权重
for name, param in model.named_parameters():
    print(f"{name}: {param.shape}")
# 输出:
# 0.weight: torch.Size([256, 784])
# 0.bias: torch.Size([256])
# 2.weight: torch.Size([10, 256])
# 2.bias: torch.Size([10])
# 注意:ReLU 没有参数,索引跳过了 1

# 想获取模块列表
for name, module in model.named_modules():
    print(f"{name}: {module}")
# 输出包含模型本身和各子模块

根因分析

1. nn.Sequential 的索引访问机制

nn.Sequential 将子模块存储在 OrderedDict 中,键为整数索引(0, 1, 2, ...)。访问内部模块需要使用整数索引:

model[0]  # 第一个模块
model[1]  # 第二个模块

这与自定义 nn.Module 中通过属性名访问(如 model.fc1)不同。

2. 参数命名规则

nn.Sequential 中,参数名由模块索引和参数名组成,格式为 {index}.{param_name}

# model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
# 参数名:
# 0.weight, 0.bias  (第一个 Linear)
# 2.weight, 2.bias  (第二个 Linear,ReLU 没有参数所以索引为 2)

3. named_parameters vs named_modules

  • named_parameters() 返回所有参数(权重和偏置),不包括无参数的模块(如 ReLU)
  • named_modules() 返回所有模块(包括 ReLU),但会递归返回子模块
  • named_children() 返回直接子模块(不递归)

4. nn.Sequential 不支持命名层

默认情况下,nn.Sequential 不支持给层命名。但可以使用 OrderedDict 来实现命名:

from collections import OrderedDict

model = nn.Sequential(OrderedDict([
    ('fc1', nn.Linear(784, 256)),
    ('relu', nn.ReLU()),
    ('fc2', nn.Linear(256, 10))
]))

# 现在可以通过名称访问
model.fc1  # nn.Linear(784, 256)

解决方案

方案一:通过索引访问层和权重

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

# 通过索引访问层
first_layer = model[0]  # nn.Linear(784, 256)
print(f"第一层: {first_layer}")

# 获取权重
weights = model[0].weight  # shape: (256, 784)
bias = model[0].bias       # shape: (256,)
print(f"权重形状: {weights.shape}")
print(f"偏置形状: {bias.shape}")

# 修改权重
model[0].weight.data.fill_(0)  # 将权重初始化为 0
print(f"修改后权重均值: {model[0].weight.data.mean()}")

# 访问特定层
last_layer = model[-1]  # 也可以使用负索引
print(f"最后一层: {last_layer}")

方案二:使用 OrderedDict 命名层

from collections import OrderedDict

model = nn.Sequential(OrderedDict([
    ('fc1', nn.Linear(784, 256)),
    ('relu1', nn.ReLU()),
    ('fc2', nn.Linear(256, 128)),
    ('relu2', nn.ReLU()),
    ('fc3', nn.Linear(128, 10))
]))

# 通过名称访问
print(model.fc1)  # nn.Linear(784, 256)
print(model.fc2)  # nn.Linear(256, 128))

# 通过索引访问(仍然支持)
print(model[0])   # nn.Linear(784, 256)

# 获取命名参数
for name, param in model.named_parameters():
    print(f"{name}: {param.shape}")
# fc1.weight: torch.Size([256, 784])
# fc1.bias: torch.Size([256])
# fc2.weight: torch.Size([128, 256])
# ...

方案三:遍历所有层和参数

# 方法 A: 遍历所有子模块
for i, module in enumerate(model):
    print(f"层 {i}: {module}")
    if hasattr(module, 'weight'):
        print(f"  权重: {module.weight.shape}")
    if hasattr(module, 'bias') and module.bias is not None:
        print(f"  偏置: {module.bias.shape}")

# 方法 B: 使用 named_children
for name, module in model.named_children():
    print(f"{name}: {module}")

# 方法 C: 使用 named_parameters
for name, param in model.named_parameters():
    print(f"{name}: {param.shape}, requires_grad={param.requires_grad}")

# 方法 D: 使用 parameters() 获取所有参数
all_params = list(model.parameters())
print(f"参数张量总数: {len(all_params)}")

方案四:提取和加载特定层权重

# 提取特定层权重
fc1_weights = model[0].weight.data.clone()
fc1_bias = model[0].bias.data.clone()
print(f"fc1 权重: {fc1_weights.shape}")

# 提取所有权重到字典
state_dict = model.state_dict()
print("State dict keys:")
for key in state_dict:
    print(f"  {key}: {state_dict[key].shape}")

# 加载特定层权重
model[0].weight.data.copy_(fc1_weights)
model[0].bias.data.copy_(fc1_bias)

# 从一个模型复制权重到另一个模型
model2 = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)
model2.load_state_dict(model.state_dict())

方案五:动态修改 Sequential

# 替换层
model[0] = nn.Linear(784, 512)  # 替换第一个层
print(f"替换后: {model}")

# 使用 add_module 添加层
model.add_module('dropout', nn.Dropout(0.5))
model.add_module('fc4', nn.Linear(10, 5))
print(f"添加后: {model}")

# 切片获取子序列
sub_model = model[:3]  # 前三个层
print(f"子模型: {sub_model}")

完整修复代码

以下是一个完整的工具模块,提供 nn.Sequential 权重访问和管理的各种功能:

"""
PyTorch nn.Sequential 权重访问与管理工具
"""

import torch
import torch.nn as nn
from collections import OrderedDict
from typing import Dict, List, Tuple, Optional


def get_layer_by_index(model: nn.Sequential, index: int) -> nn.Module:
    """通过索引获取层"""
    return model[index]


def get_layer_by_name(model: nn.Sequential, name: str) -> Optional[nn.Module]:
    """通过名称获取层(如果使用了 OrderedDict)"""
    for n, module in model.named_children():

![配图](https://i-blog.csdnimg.cn/img_convert/45059327d12f008392c801cf37ee193a.png)

        if n == name:
            return module
    return None


def get_all_weights(model: nn.Sequential) -> Dict[str, torch.Tensor]:
    """
    获取所有层的权重
    
    Returns:
        字典: {层标识: 权重张量}
    """
    weights = {}
    for name, param in model.named_parameters():
        weights[name] = param.data.clone()
    return weights


def get_layer_weights(model: nn.Sequential, index: int) -> Dict[str, torch.Tensor]:
    """
    获取指定层的所有权重
    
    Args:
        model: nn.Sequential 模型
        index: 层索引
    
    Returns:
        字典: {参数名: 张量}
    """
    layer = model[index]
    weights = {}
    for name, param in layer.named_parameters():
        weights[name] = param.data.clone()
    return weights


def set_layer_weights(model: nn.Sequential, index: int, 
                      weights: Dict[str, torch.Tensor]):
    """
    设置指定层的权重
    
    Args:
        model: nn.Sequential 模型
        index: 层索引
        weights: 权重字典
    """
    layer = model[index]
    for name, param in layer.named_parameters():
        if name in weights:
            param.data.copy_(weights[name])


def print_model_summary(model: nn.Sequential):
    """打印模型摘要信息"""
    print("=" * 70)
    print(f"模型类型: {model.__class__.__name__}")
    print(f"层数: {len(model)}")
    print("=" * 70)
    
    total_params = 0
    trainable_params = 0
    
    for i, module in enumerate(model):
        # 获取层名
        layer_name = None
        for name, m in model.named_children():
            if m is module:
                layer_name = name
                break
        
        display_name = layer_name if layer_name else str(i)
        
        # 计算参数
        layer_params = sum(p.numel() for p in module.parameters())
        layer_trainable = sum(p.numel() for p in module.parameters() if p.requires_grad)
        
        total_params += layer_params
        trainable_params += layer_trainable
        
        # 层信息
        print(f"\n[{display_name}] {module.__class__.__name__}")
        print(f"  参数数: {layer_params:,} (可训练: {layer_trainable:,})")
        
        # 权重详情
        for name, param in module.named_parameters():
            shape_str = 'x'.join(str(s) for s in param.shape)
            grad_str = "可训练" if param.requires_grad else "冻结"
            print(f"  .{name}: [{shape_str}] ({grad_str})")
            
            # 统计信息
            if param.dim() > 0:
                print(f"    均值: {param.data.mean():.6f}, "
                      f"标准差: {param.data.std():.6f}, "
                      f"最小值: {param.data.min():.6f}, "
                      f"最大值: {param.data.max():.6f}")
    
    print("\n" + "=" * 70)
    print(f"总参数数: {total_params:,}")
    print(f"可训练参数: {trainable_params:,}")
    print(f"冻结参数: {total_params - trainable_params:,}")
    print("=" * 70)


def create_named_sequential(layers: List[Tuple[str, nn.Module]]) -> nn.Sequential:
    """
    创建带命名的 nn.Sequential
    
    Args:
        layers: [(name, module), ...] 列表
    
    Returns:
        nn.Sequential with named layers
    """
    return nn.Sequential(OrderedDict(layers))


def extract_features(model: nn.Sequential, x: torch.Tensor, 
                     up_to_index: int) -> torch.Tensor:
    """
    使用 Sequential 的前 N 层提取特征
    
    Args:
        model: nn.Sequential 模型
        x: 输入张量
        up_to_index: 提取到第几层(不包含)
    
    Returns:
        特征张量
    """
    for i, module in enumerate(model):
        if i >= up_to_index:
            break
        x = module(x)
    return x


def get_intermediate_outputs(model: nn.Sequential, x: torch.Tensor,
                              return_indices: Optional[List[int]] = None
                              ) -> Dict[int, torch.Tensor]:
    """
    获取中间层的输出
    
    Args:
        model: nn.Sequential 模型
        x: 输入张量
        return_indices: 要返回的层索引列表,None 表示返回所有层
    
    Returns:
        {层索引: 输出张量} 字典
    """
    outputs = {}
    for i, module in enumerate(model):
        x = module(x)
        if return_indices is None or i in return_indices:
            outputs[i] = x.clone()
    return outputs


def freeze_layers(model: nn.Sequential, freeze_indices: List[int]):
    """
    冻结指定层的参数
    
    Args:
        model: nn.Sequential 模型
        freeze_indices: 要冻结的层索引列表
    """
    for idx in freeze_indices:
        for param in model[idx].parameters():
            param.requires_grad = False
    print(f"已冻结层: {freeze_indices}")


def init_weights_sequential(model: nn.Sequential, 
                             init_type: str = 'xavier_uniform',
                             init_gain: float = 1.0):
    """
    初始化 nn.Sequential 中所有层的权重
    
    Args:
        model: nn.Sequential 模型
        init_type: 初始化方法 ('xavier_uniform', 'xavier_normal', 
                   'kaiming_uniform', 'kaiming_normal', 'normal', 'constant')
        init_gain: 初始化增益
    """
    def init_func(m):
        classname = m.__class__.__name__
        if hasattr(m, 'weight') and m.weight is not None:
            if classname.find('Conv') != -1 or classname.find('Linear') != -1:
                if init_type == 'xavier_uniform':
                    nn.init.xavier_uniform_(m.weight.data, gain=init_gain)
                elif init_type == 'xavier_normal':
                    nn.init.xavier_normal_(m.weight.data, gain=init_gain)
                elif init_type == 'kaiming_uniform':
                    nn.init.kaiming_uniform_(m.weight.data, a=0, mode='fan_in')
                elif init_type == 'kaiming_normal':
                    nn.init.kaiming_normal_(m.weight.data, a=0, mode='fan_in')
                elif init_type == 'normal':
                    nn.init.normal_(m.weight.data, mean=0.0, std=init_gain)
                elif init_type == 'constant':
                    nn.init.constant_(m.weight.data, val=init_gain)
                else:
                    raise NotImplementedError(f'初始化方法 {init_type} 不支持')
            
            if hasattr(m, 'bias') and m.bias is not None:
                nn.init.constant_(m.bias.data, val=0.0)
        
        elif classname.find('BatchNorm') != -1:
            if hasattr(m, 'weight') and m.weight is not None:
                nn.init.constant_(m.weight.data, val=1.0)
            if hasattr(m, 'bias') and m.bias is not None:
                nn.init.constant_(m.bias.data, val=0.0)
    
    model.apply(init_func)
    print(f"权重初始化完成: {init_type}")


# ============================================================
# 完整示例
# ============================================================

def demo():
    """完整演示"""
    print("=" * 70)
    print("nn.Sequential 权重访问演示")
    print("=" * 70)
    
    # 创建带命名的 Sequential
    model = create_named_sequential([
        ('fc1', nn.Linear(784, 256)),
        ('relu1', nn.ReLU()),
        ('dropout1', nn.Dropout(0.3)),
        ('fc2', nn.Linear(256, 128)),
        ('relu2', nn.ReLU()),
        ('fc3', nn.Linear(128, 10)),
    ])
    
    # 打印模型摘要
    print_model_summary(model)
    
    # 初始化权重
    print("\n--- 权重初始化 ---")
    init_weights_sequential(model, init_type='xavier_uniform')
    
    # 检查初始化后的统计
    print(f"\nfc1 权重均值: {model.fc1.weight.data.mean():.6f}")
    print(f"fc1 权重标准差: {model.fc1.weight.data.std():.6f}")
    
    # 通过索引和名称访问
    print("\n--- 层访问 ---")
    print(f"通过索引 model[0]: {model[0]}")
    print(f"通过名称 model.fc1: {model.fc1}")
    
    # 获取和设置权重
    print("\n--- 权重操作 ---")
    weights = get_layer_weights(model, 0)
    print(f"fc1 权重键: {list(weights.keys())}")
    print(f"fc1 权重形状: {weights['weight'].shape}")
    
    # 提取特征
    print("\n--- 特征提取 ---")
    x = torch.randn(4, 784)
    features = extract_features(model, x, up_to_index=3)  # 前 3 层
    print(f"输入形状: {x.shape}")
    print(f"特征形状 (前3层): {features.shape}")
    
    # 中间层输出
    print("\n--- 中间层输出 ---")
    outputs = get_intermediate_outputs(model, x, return_indices=[0, 3, 5])
    for idx, out in outputs.items():
        print(f"  层 {idx} 输出: {out.shape}")
    
    # 冻结层
    print("\n--- 冻结层 ---")
    freeze_layers(model, freeze_indices=[0, 3])  # 冻结 fc1 和 fc2
    
    trainable = [p for p in model.parameters() if p.requires_grad]
    print(f"可训练参数张量数: {len(trainable)}")
    
    # 解冻
    for param in model.parameters():
        param.requires_grad = True
    print("已解冻所有层")
    
    # 保存和加载权重
    print("\n--- 权重保存/加载 ---")
    all_weights = get_all_weights(model)
    print(f"权重字典键: {list(all_weights.keys())}")
    
    # 创建新模型并加载权重
    model2 = create_named_sequential([
        ('fc1', nn.Linear(784, 256)),
        ('relu1', nn.ReLU()),
        ('dropout1', nn.Dropout(0.3)),
        ('fc2', nn.Linear(256, 128)),
        ('relu2', nn.ReLU()),
        ('fc3', nn.Linear(128, 10)),
    ])
    model2.load_state_dict(model.state_dict())
    print("权重加载成功!")
    
    # 验证权重一致
    assert torch.allclose(model.fc1.weight.data, model2.fc1.weight.data)
    print("权重验证通过!")


if __name__ == "__main__":
    demo()

常见陷阱与注意事项

1. 索引与参数名的对应关系

nn.Sequential 中,参数名使用整数索引作为前缀。注意无参数的层(如 ReLU)也会占用索引:

model = nn.Sequential(
    nn.Linear(10, 5),  # 索引 0 -> 参数名 0.weight, 0.bias
    nn.ReLU(),         # 索引 1 -> 无参数
    nn.Linear(5, 2),   # 索引 2 -> 参数名 2.weight, 2.bias
)

# 参数名是 0.weight, 0.bias, 2.weight, 2.bias
# 注意没有 1.xxx

2. model.parameters() vs model.children()

  • model.parameters() 返回所有参数张量(不含模块)
  • model.children() 返回所有子模块(不含参数)
  • model.modules() 递归返回所有模块(包括 Sequential 本身)
# 参数
for param in model.parameters():
    print(param.shape)

# 子模块
for module in model.children():
    print(module)

# 所有模块(递归)
for module in model.modules():
    print(module)

3. 修改权重时使用 .data

直接修改权重时,使用 .data 避免影响计算图:

# 正确 - 使用 .data
model[0].weight.data.fill_(0)
model[0].weight.data.copy_(new_weights)

# 也可以使用 inplace 操作
model[0].weight.data.normal_(mean=0, std=0.01)

4. load_state_dict 的严格匹配

load_state_dict 默认要求参数名完全匹配。如果模型结构不同,需要设置 strict=False

# 部分加载
model.load_state_dict(pretrained_dict, strict=False)
# 只加载匹配的参数,不匹配的跳过

5. 使用 named_parameters 过滤特定层

# 只获取 Linear 层的权重
for name, param in model.named_parameters():
    if 'weight' in name:
        print(f"{name}: {param.shape}")

# 按层名过滤(使用 OrderedDict 命名时)
for name, param in model.named_parameters():
    if name.startswith('fc'):
        print(f"{name}: {param.shape}")

6. nn.Sequential 的局限性

nn.Sequential 只支持单线前向传播。如果模型有分支、跳跃连接(如 ResNet)或条件执行,需要自定义 nn.Module

# nn.Sequential 无法实现跳跃连接
class ResidualBlock(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.fc1 = nn.Linear(dim, dim)
        self.fc2 = nn.Linear(dim, dim)
    
    def forward(self, x):
        residual = x
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x + residual  # 跳跃连接

总结

在 PyTorch 中使用 nn.Sequential 时,访问和管理内部层的权重需要理解其索引访问机制和参数命名规则。

核心要点总结:

  1. 通过索引访问层:使用 model[index] 访问 nn.Sequential 中的层,如 model[0].weight 获取第一个层的权重。支持负索引 model[-1]

  2. 使用 OrderedDict 命名层:通过 nn.Sequential(OrderedDict([('name', layer), ...])) 给层命名,之后可以通过 model.name 访问,使代码更可读。

  3. 遍历参数:使用 named_parameters() 获取所有参数(格式 {index}.{param_name}),使用 named_children() 获取所有子模块。

  4. 修改权重:使用 .data 属性直接修改权重值,如 model[0].weight.data.copy_(new_weights),避免影响计算图。

  5. 提取中间特征:通过遍历前 N 层或收集每层输出,可以提取中间层特征用于可视化或迁移学习。

  6. 冻结特定层:通过 model[index].parameters() 获取特定层参数并设置 requires_grad=False

  7. 权重初始化:使用 model.apply(init_func) 对所有子模块应用初始化函数。

  8. Sequential 的局限nn.Sequential 只支持单线前向传播。对于有分支或跳跃连接的模型,需要自定义 nn.Module

通过掌握这些技巧,你可以在使用 nn.Sequential 时灵活地访问、修改和管理模型权重,满足调试、迁移学习和权重分析等各种需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

放风铃的兔子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值