尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

PyTorch网络结构修改实战:从参数调整到层替换的完整指南

PyTorch网络结构修改实战:从参数调整到层替换的完整指南 1. 项目概述为什么我们需要修改PyTorch网络结构在深度学习的实际项目里我们几乎不可能一次性就设计出完美的网络模型。更多的时候模型是在不断的迭代和调试中完善的。你可能遇到过这些情况训练时发现某个层梯度消失严重需要替换成更稳定的结构为了适配新的输入数据尺寸需要调整卷积核的步长或填充或者为了模型轻量化需要剪掉一些冗余的层。这些操作都指向一个核心技能动态修改已定义的PyTorch网络。这不仅仅是“改代码”那么简单。PyTorch的动态计算图Dynamic Computational Graph特性赋予了它极高的灵活性但同时也意味着修改网络结构需要遵循其自身的逻辑和内存管理机制。盲目地增删层或修改参数可能会导致前向传播出错、梯度无法回传甚至破坏整个模型的状态字典state_dict使得预训练权重加载失败。因此掌握一套系统、安全的方法来修改网络是每个PyTorch使用者从“会用”到“精通”的关键一步。本文将从一个资深从业者的角度拆解在PyTorch中修改网络结构的各种场景和具体操作。我们会涵盖从最基础的层参数微调到复杂的层替换、插入和删除并深入探讨其背后的原理和必须注意的“坑”。无论你是正在调试自己的模型还是需要复用或魔改一个开源模型这里的内容都能提供直接的参考。2. 网络修改的核心思路与PyTorch机制解析在动手之前我们必须理解PyTorch中神经网络模块nn.Module的组织方式。一个nn.Module可以包含子模块nn.Module、参数nn.Parameter和缓冲区buffer。它们通过self.add_module(name, module)或直接赋值如self.conv1 nn.Conv2d(...)进行注册。这种注册机制使得PyTorch能够跟踪所有组件并在调用.to(device)、.parameters()或.state_dict()时正确地处理它们。2.1 修改的两种范式定义时修改与运行时修改修改网络通常发生在两个阶段定义时修改在实例化网络类之后但在将其投入训练循环之前进行修改。这是最常见和最安全的方式因为此时计算图尚未被复杂的数据流频繁构建。例如根据数据集调整分类器的输出维度。运行时动态修改在训练或推理过程中根据某些条件动态地改变网络结构。这更高级也更容易出错需要谨慎处理计算图的动态构建和优化器状态。例如实现某种条件计算或动态深度网络。我们的讨论将主要集中在定义时修改因为这是绝大多数应用场景。理解了基础运行时修改的原理也就触类旁通了。2.2 关键属性named_children,named_modules,named_parameters要对网络进行手术首先得知道它的“解剖结构”。这几个方法是你必须熟悉的model.named_children(): 返回直接子模块的迭代器。对于Sequential容器这很方便。但注意它不会递归进入子模块的子模块。model.named_modules(): 返回模型中所有模块包括自身和所有嵌套子模块的迭代器。这是最全面的视图用于定位深层次的层。model.named_parameters(): 返回模型中所有参数的迭代器requires_gradTrue的nn.Parameter。一个快速查看网络结构的小技巧for name, module in model.named_modules(): print(f{name}: {module})这能帮你清晰地看到每一层的名称和类型是修改前必不可少的诊断步骤。2.3 修改的核心原则保持状态一致性这是最重要的一条经验法则当你替换或移除一个含有可学习参数或缓冲区的层时必须考虑其状态字典state_dict的兼容性。假设你有一个预训练模型其features.0.weight是一个形状为[64, 3, 7, 7]的张量。如果你把第一层卷积换成了输入输出通道数不同的卷积那么直接加载旧权重就会因为形状不匹配而失败。因此任何结构性修改都必须配套考虑权重初始化或迁移策略。3. 层参数修改最基础的调整操作修改现有层的参数而不改变其类型和连接关系是最常见的需求。这通常很安全。3.1 直接属性赋值对于层的超参数如nn.Dropout的p值nn.ReLU的inplace可以直接修改。import torch.nn as nn model nn.Sequential(nn.Linear(10, 20), nn.ReLU(), nn.Dropout(p0.5)) print(fOriginal dropout rate: {model[2].p}) # 输出 0.5 # 直接修改dropout率 model[2].p 0.3 print(fModified dropout rate: {model[2].p}) # 输出 0.3注意修改像inplaceTrue这样的参数要格外小心。如果在训练中途将ReLU(inplaceTrue)改为False可能会影响之前已进行的计算图的正确性尤其是在使用梯度检查点checkpointing时。建议在训练开始前就确定这些设置。3.2 修改具有可学习参数的层属性对于像nn.Conv2d、nn.Linear、nn.BatchNorm2d这样的层其权重weight和偏置bias是nn.Parameter。你可以直接访问并修改它们的数据但通常不直接替换整个Parameter对象。场景你想对某一层的权重进行特定的初始化如用预训练的部分权重填充新权重矩阵的一部分。操作直接操作.data属性。conv_layer nn.Conv2d(3, 16, kernel_size3) print(fWeight shape: {conv_layer.weight.shape}) # [16, 3, 3, 3] # 假设我们想将所有权重乘以一个缩放因子 conv_layer.weight.data * 0.1 # 或者用特定的值填充一部分权重谨慎操作 # conv_layer.weight.data[:8, :, :, :] torch.ones_like(conv_layer.weight.data[:8, :, :, :]) * 0.05关键点这里修改的是weight.data即张量本身的值。你不能直接给conv_layer.weight赋一个新的torch.Tensor因为weight本身是一个nn.Parameter对象它与模块的注册机制绑定。直接赋值会破坏这种绑定导致该参数无法被优化器识别。正确做法永远是修改.data属性。3.3 修改层的配置参数如stride, padding有时你需要改变层的计算行为比如为了适配不同分辨率的输入需要修改卷积的stride或padding。这些参数在层实例化时确定并作为该层的属性存在。在PyTorch中大多数层的这些配置参数在创建后是只读的。例如你不能直接做conv.stride 2。正确做法替换整个层。这是下一节的重点。你需要创建一个新的层其具有你想要的配置然后将其“安装”到原网络对应位置。4. 网络结构的增、删、改、查这是修改网络的核心部分涉及结构的变动。4.1 增在指定位置插入新层假设我们有一个简单的CNN想在某个卷积层后添加一个BatchNorm层。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier nn.Linear(64 * 16 * 16, 10) # 假设输入是32x32 def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x model SimpleCNN() print(model)现在我们想在第一个ReLU之后、第二个Conv2d之前插入一个BatchNorm2d层。方法一修改nn.Sequential的_modules有序字典nn.Sequential内部使用一个OrderedDict在Python 3.7后是dict但保持顺序来存储子模块。我们可以直接操作这个字典但要注意重新构建顺序。# 创建一个新的有序字典来构建新的Sequential new_features_dict nn.ModuleDict() # 或者使用 collections.OrderedDict() # 但更Pythonic的方式是直接构建一个新的Sequential列表 new_layers [] for i, (name, layer) in enumerate(model.features.named_children()): new_layers.append(layer) if i 1: # 在第一个ReLU索引1之后插入 new_layers.append(nn.BatchNorm2d(32)) # 输入通道数需匹配上一层的输出 # 用新的层列表替换原来的features model.features nn.Sequential(*new_layers) print(model)方法二使用nn.Sequential的切片和拼接更直观将nn.Sequential视为一个层列表我们可以用Python的列表切片来操作。# 将Sequential转换为列表注意这不会破坏原有的模块引用 layers_list list(model.features.children()) # 在索引2的位置即第二个元素之后插入BatchNorm # 原结构: [Conv2d, ReLU, Conv2d, ReLU, MaxPool2d] # 插入后: [Conv2d, ReLU, BatchNorm, Conv2d, ReLU, MaxPool2d] layers_list.insert(2, nn.BatchNorm2d(32)) # 用新的列表重建Sequential model.features nn.Sequential(*layers_list) print(model)实操心得方法二更清晰易懂也是我最常用的方式。关键在于list(model.features.children())会获取所有子模块的引用insert操作后用nn.Sequential(*list)重新包装。这确保了所有模块都被正确地重新注册到新的Sequential容器中。4.2 删移除特定层移除层同样可以通过操作层列表来实现。# 接上例假设我们觉得第二个ReLU层索引4是多余的想移除它 layers_list list(model.features.children()) del layers_list[4] # 删除索引为4的层第二个ReLU model.features nn.Sequential(*layers_list) print(model)更复杂的场景删除由nn.Module命名的子模块如果网络不是Sequential而是像SimpleCNN那样有命名的子模块如self.features,self.classifier删除操作更直接但也需要确保前向传播逻辑被更新。# 假设我们想删除整个classifier并换一个新的 del model.classifier # 直接删除属性 model.new_classifier nn.Sequential( nn.Linear(64 * 16 * 16, 128), nn.ReLU(), nn.Linear(128, 10) ) # 别忘了在forward方法中也要相应修改 # 需要重写model的forward方法或者确保model.new_classifier被正确调用。注意事项直接del一个模块并新建对于简单模型是可行的。但对于复杂的、forward逻辑紧密耦合的网络这样做风险很高。更好的做法是继承原模型类重写__init__和forward方法。这引出了下一个技巧通过继承来安全修改复杂网络。4.3 改替换现有层替换层是插入和删除的结合也是最常见的操作之一。核心步骤是1定位目标层2创建新层3替换。场景将SimpleCNN中第一个Conv2d(3, 32, 3)替换为Conv2d(3, 64, 5, padding2)以获得更大的感受野和更多通道。# 方法通过修改Sequential的子模块字典 # 首先获取features Sequential的所有子模块 # 使用 named_children 可以同时得到名字和模块但这里我们只需要按索引操作 layers_list list(model.features.children()) # 替换索引0的层 old_conv layers_list[0] # 创建新层。注意输入通道不变3输出通道从32变为64kernel从3变为5padding需相应调整以保持输出尺寸如果希望保持。 # 假设输入是32x32我们希望输出尺寸不变。对于kernel_size5, padding2可以保持尺寸。 new_conv nn.Conv2d(in_channels3, out_channels64, kernel_size5, padding2) layers_list[0] new_conv # 重要如果原网络有预训练权重且你希望部分迁移这里需要手动处理权重。 # 例如可以将旧权重中通道数匹配的部分拷贝过来这通常很复杂需要具体分析。 # if hasattr(old_conv, weight) and hasattr(new_conv, weight): # with torch.no_grad(): # min_out min(old_conv.weight.size(0), new_conv.weight.size(0)) # min_in min(old_conv.weight.size(1), new_conv.weight.size(1)) # new_conv.weight.data[:min_out, :min_in, ...] old_conv.weight.data[:min_out, :min_in, ...] model.features nn.Sequential(*layers_list) print(model)通过继承进行安全替换推荐用于复杂模型对于像ResNet、VGG这类结构清晰但内部复杂的网络直接“动手术”容易出错。最佳实践是创建一个新的模型类来继承它。from torchvision import models class ModifiedResNet18(models.ResNet): def __init__(self, num_classes1000): # 首先调用父类的__init__加载ResNet18的标准结构 super().__init__(blockmodels.resnet.BasicBlock, layers[2, 2, 2, 2], num_classesnum_classes) # 现在self 包含了所有标准ResNet18的层 # 替换第一层卷积原为Conv2d(3,64,7,2,3)我们改为Conv2d(3,32,3,1,1)减小计算量 original_conv1 self.conv1 self.conv1 nn.Conv2d(3, 32, kernel_size3, stride1, padding1, biasFalse) # 替换全连接层原为Linear(512, num_classes)我们增加一个中间层 original_fc self.fc self.fc nn.Sequential( nn.Linear(512, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) # 可选初始化新层的权重 nn.init.kaiming_normal_(self.conv1.weight, modefan_out, nonlinearityrelu) for m in self.fc.modules(): if isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0) # forward方法通常不需要重写除非连接关系也改变了。 # 因为ResNet的forward逻辑如x self.conv1(x)使用的是self.conv1这个属性 # 我们已经替换了它所以forward会自动使用新层。 # 使用修改后的模型 model ModifiedResNet18(num_classes10) print(model.conv1) # 输出将是 Conv2d(3, 32, kernel_size(3, 3), ...)这种方法的最大好处是安全和清晰。你完全控制了模型的构建过程并且可以方便地复用父类如ResNet的预训练权重尽管替换层后需要谨慎处理权重加载见下文。5. 权重处理修改网络后的初始化与迁移学习修改了网络结构尤其是替换了含有参数的层后权重初始化是必须考虑的问题。5.1 新层的初始化对于新增的层如我们插入的BatchNorm2d或新的Linear层PyTorch会使用其默认初始化。对于Linear和Conv层默认是Kaiming均匀初始化针对ReLU。对于BatchNormweight初始为1bias初始为0。这通常是可以接受的。如果你有特殊需求可以手动初始化def init_weights(m): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) m.bias.data.fill_(0.01) elif isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) if m.bias is not None: nn.init.constant_(m.bias, 0) # 对模型应用初始化函数 model.apply(init_weights)apply函数会递归地将函数应用到每一个子模块上。5.2 加载预训练权重时的部分加载这是修改网络后最关键的步骤之一。你有一个预训练模型A修改后得到模型B。你想把A中结构相同的部分的权重加载到B中而结构不同的部分则保留B的随机初始化。PyTorch的load_state_dict函数有一个strict参数。当strictTrue默认时要求两个状态字典的键完全匹配。如果键不匹配因为结构变了就会报错。我们的策略是设置strictFalse并理解其行为。# 假设 pretrained_model 是原始的、未修改的模型 pretrained_model models.resnet18(pretrainedTrue) original_state_dict pretrained_model.state_dict() # modified_model 是我们通过继承或修改得到的模型 modified_model ModifiedResNet18(num_classes10) # 尝试加载strictFalse允许键不匹配 load_result modified_model.load_state_dict(original_state_dict, strictFalse) # load_result 是一个 namedtuple (missing_keys, unexpected_keys) print(fMissing keys: {load_result.missing_keys}) # B中有但A中没有的键如新加的层 print(fUnexpected keys: {load_result.unexpected_keys}) # A中有但B中没有的键如被删除的层 # 通常missing_keys 对应我们新增的层这是预期的。 # unexpected_keys 对应被我们删除或替换掉的旧层的权重这些权重被忽略也是预期的。关键点strictFalse模式下PyTorch会尽力匹配键名。对于名称和形状都完全一致的层权重会被成功加载。对于名称不匹配或形状不匹配的层则跳过。这完美地解决了部分权重迁移的问题。5.3 处理形状不匹配的权重迁移有时我们替换的层类型相同但形状不同如Conv2d(3,32,3)-Conv2d(3,64,5)。strictFalse会因形状不匹配而跳过该层。如果你希望进行更精细的权重迁移例如将旧权重的一部分复制到新权重中就需要手动操作。# 假设我们只修改了conv1其他层不变 pretrained_state_dict pretrained_model.state_dict() new_state_dict modified_model.state_dict() for key in new_state_dict: if key in pretrained_state_dict: # 键名存在 if new_state_dict[key].shape pretrained_state_dict[key].shape: # 形状相同直接复制 new_state_dict[key] pretrained_state_dict[key] else: # 形状不同进行自定义处理 if key conv1.weight: old_weight pretrained_state_dict[key] # shape: [64, 3, 7, 7] new_weight new_state_dict[key] # shape: [32, 3, 3, 3] # 示例只拷贝前32个输出通道的权重并且只拷贝中心3x3的区域假设从7x7中心裁剪 # 这是一个非常特定场景的操作实际逻辑取决于你的修改 with torch.no_grad(): # 这里只是一个示例实际裁剪逻辑需要根据新旧kernel_size和padding计算 # 例如从7x7中心取3x3 start (7 - 3) // 2 new_weight.data old_weight[:32, :, start:start3, start:start3].clone() print(fManually processed weights for: {key}) else: print(fSkipping {key} due to shape mismatch: {pretrained_state_dict[key].shape} vs {new_state_dict[key].shape}) else: print(f{key} not found in pretrained model, keeping random initialization.) # 将处理后的状态字典加载回模型 modified_model.load_state_dict(new_state_dict)重要警告这种手动权重移植非常复杂且容易出错必须对网络结构和张量维度有清晰的理解。在大多数情况下如果修改不大让新层随机初始化并从零开始训练或微调可能是更简单、更稳定的选择。只有当预训练权重极其宝贵且修改是局部、可精确映射时才考虑这种方法。6. 实战案例修改一个图像分类网络以适应新的输入尺寸让我们通过一个完整的例子将上述技巧串联起来。任务我们有一个在224x224 ImageNet上预训练的ResNet18现在要将其用于一个分辨率为128x128的新数据集并且类别数从1000改为10。挑战输入尺寸变小原网络第一层conv1的stride2和maxpool的stride2可能导致特征图过早缩小丢失信息。分类器fc的输入维度由最后的特征图尺寸决定改变输入尺寸会改变这个值。输出类别数需要改变。解决方案import torch import torch.nn as nn from torchvision import models class ResNetFor128x128(models.ResNet): def __init__(self, num_classes10): # 1. 初始化标准ResNet18 super().__init__(blockmodels.resnet.BasicBlock, layers[2, 2, 2, 2], num_classes1000) # 先按父类默认1000初始化 # 2. 修改第一层卷积减小stride避免过早下采样 # 原: Conv2d(3,64,kernel_size7,stride2,padding3,biasFalse) # 新: Conv2d(3,64,kernel_size7,stride1,padding3,biasFalse) # stride从2改为1 self.conv1 nn.Conv2d(3, 64, kernel_size7, stride1, padding3, biasFalse) # 3. 修改第一个最大池化层同样减小stride或直接移除激进做法 # 原: MaxPool2d(kernel_size3, stride2, padding1) # 新: MaxPool2d(kernel_size3, stride1, padding1) # stride从2改为1 # 或者更激进一点直接移除它用Identity层代替 # self.maxpool nn.Identity() # 这里我们选择修改stride self.maxpool nn.MaxPool2d(kernel_size3, stride1, padding1) # 4. 重新计算分类器的输入维度 # 原ResNet18在224x224输入下经过一系列下采样后avgpool层之前的特征图是7x7通道512。 # 现在输入是128x128且我们修改了conv1和maxpool的stride需要重新计算。 # 一个简单可靠的方法创建一个伪输入前向传播到avgpool之前看特征图的展平尺寸。 self._recompute_fc_input_features(num_classes) # 5. 替换全连接层 # 我们先创建一个占位符在_recompute_fc_input_features中再确定in_features self.fc None def _recompute_fc_input_features(self, num_classes): 通过前向传播伪数据计算avgpool后特征向量的长度 with torch.no_grad(): # 创建一个伪输入张量 (batch1, channel3, height128, width128) dummy_input torch.zeros(1, 3, 128, 128) # 前向传播直到avgpool之前 x self.conv1(dummy_input) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) # 此时x的形状为 [1, C, H, W] # 应用自适应平均池化 (原网络是nn.AdaptiveAvgPool2d((1, 1))) # 但为了计算长度我们直接展平 features_flattened torch.flatten(x, 1) in_features features_flattened.size(1) print(fComputed in_features for fc layer: {in_features}) # 现在创建新的全连接层 self.fc nn.Linear(in_features, num_classes) def forward(self, x): # 标准ResNet forward但使用的是我们修改过的conv1, maxpool和fc x self.conv1(x) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) x self.fc(x) return x # 实例化模型 model ResNetFor128x128(num_classes10) print(model) # 加载预训练权重除fc层和因形状不匹配的层外 pretrained models.resnet18(pretrainedTrue) load_info model.load_state_dict(pretrained.state_dict(), strictFalse) print(fMissing keys (expected: fc.weight, fc.bias, and maybe others due to stride change): {load_info.missing_keys}) print(fUnexpected keys (weights from layers we modified or removed): {load_info.unexpected_keys}) # 测试前向传播 dummy_input torch.randn(2, 3, 128, 128) output model(dummy_input) print(fOutput shape: {output.shape}) # 应为 [2, 10]这个案例展示了如何系统性地修改一个网络从分析需求输入尺寸变化到制定修改策略调整早期下采样再到实现修改层参数、动态计算分类器输入、处理预训练权重。其中_recompute_fc_input_features方法是一个实用技巧避免了手动计算特征图尺寸的繁琐和易错。7. 常见陷阱、调试技巧与高级话题即使掌握了基本操作在实际修改网络时还是会遇到各种问题。这里记录一些常见的“坑”和解决方法。7.1 陷阱一忘记更新前向传播函数当你通过直接替换属性如self.conv1 new_conv修改网络时前向传播函数forward中如果直接引用了旧的层例如在forward里写死了x self.old_conv1(x)就会导致运行时错误。最佳实践是在forward中始终使用self.属性来引用层这样当你替换属性后forward逻辑会自动生效。如果forward逻辑本身也需要改变例如跳连结构被修改那么你必须重写forward方法。7.2 陷阱二nn.Sequential的索引陷阱使用list(model.features.children())[i]来获取和修改层时务必确保你对网络的结构了如指掌。一个常见的错误是数错索引尤其是在网络很深的时候。强烈建议在修改前先打印出带有索引的层列表for idx, (name, layer) in enumerate(model.features.named_children()): print(f{idx}: {name} - {layer})7.3 陷阱三批量归一化层BatchNorm的running_mean和running_varBatchNorm层除了可学习的weight和bias还有在训练过程中统计的running_mean和running_var缓冲区。当你替换一个BatchNorm层时这些统计量会被重置。如果你是在一个预训练模型中间插入BatchNorm这些新层的统计量需要从头开始累积这可能会在微调初期导致不稳定的训练。一种缓解方法是在少量数据上先进行几次前向传播model.train()模式让running_mean/var有一个粗略的估计然后再开始正式训练。7.4 调试技巧使用torchsummary或手动打印特征图形状修改网络后第一件事就是验证前向传播是否能跑通以及各层特征图形状是否符合预期。torchsummary库非常有用pip install torchsummaryfrom torchsummary import summary summary(model, input_size(3, 128, 128), devicecpu)如果没有torchsummary可以在forward函数中关键位置插入打印语句调试后记得删除def forward(self, x): print(fInput: {x.shape}) x self.features(x) print(fAfter features: {x.shape}) x x.view(x.size(0), -1) print(fAfter view: {x.shape}) x self.classifier(x) print(fOutput: {x.shape}) return x7.5 高级话题动态网络与nn.ModuleList/nn.ModuleDict对于需要动态增减层的场景如根据输入决定网络深度nn.Sequential的静态列表就不够用了。这时应该使用nn.ModuleList或nn.ModuleDict。nn.ModuleList像一个普通的Python列表但专门用于存放nn.Module。PyTorch能正确注册其中的所有模块。class DynamicNet(nn.Module): def __init__(self, num_layers): super().__init__() self.layers nn.ModuleList([nn.Linear(10, 10) for _ in range(num_layers)]) def forward(self, x): for i, layer in enumerate(self.layers): x layer(x) if i % 2 0: x torch.relu(x) return x # 可以动态添加或删除 # model.layers.append(nn.Linear(10,10)) # del model.layers[0]nn.ModuleDict类似地是一个存储模块的字典。使用它们可以构建非常灵活的网络结构但需要你在forward中手动实现逻辑。修改PyTorch网络是一项融合了理解、设计和实操的综合能力。从简单的参数调整到复杂的结构手术每一步都需要清晰的目标和对框架机制的把握。最稳妥的路径永远是先理解原有结构named_modules制定修改计划在小规模测试上验证前向/反向传播最后再处理权重迁移。当你熟悉了这些模式后驾驭各种开源模型进行定制化改造就会变得得心应手。
返回列表