158 lines
4.2 KiB
Markdown
158 lines
4.2 KiB
Markdown
# Lesson 19 笔记:named_children vs named_modules 详解
|
||
|
||
## 目录
|
||
|
||
- [核心区别](#核心区别)
|
||
- [named_modules() 的特点](#named_modules-的特点)
|
||
- [named_children() 的特点](#named_children-的特点)
|
||
- [原项目为什么用 named_modules()](#原项目为什么用-named_modules)
|
||
- [如果要做模块替换用什么](#如果要做模块替换用什么)
|
||
- [对比总结](#对比总结)
|
||
|
||
---
|
||
|
||
## 核心区别
|
||
|
||
| 方法 | 遍历范围 | 适合场景 |
|
||
|------|----------|----------|
|
||
| `named_modules()` | 递归所有后代模块 | 读、统计、原地修改 |
|
||
| `named_children()` | 只遍历直接子模块 | 替换子模块 |
|
||
|
||
---
|
||
|
||
## named_modules() 的特点
|
||
|
||
`named_modules()` 会递归遍历整棵模块树,返回**所有后代模块**:
|
||
|
||
```python
|
||
for name, module in model.named_modules():
|
||
print(name, module)
|
||
```
|
||
|
||
输出类似:
|
||
```
|
||
"" -> 整个 model 自己
|
||
"layers" -> ModuleList
|
||
"layers.0" -> 第 0 个 block
|
||
"layers.0.self_attn.q_proj" -> q_proj Linear
|
||
"layers.0.self_attn.k_proj" -> k_proj Linear
|
||
...
|
||
```
|
||
|
||
**特点:**
|
||
- 给你的是完整路径和模块对象本身
|
||
- **没有直接给你父模块**
|
||
- 适合"查看、统计、收集参数"
|
||
|
||
---
|
||
|
||
## named_children() 的特点
|
||
|
||
`named_children()` 只遍历**直接子模块**:
|
||
|
||
```python
|
||
for name, child in model.named_children():
|
||
print(name, child)
|
||
```
|
||
|
||
输出类似:
|
||
```
|
||
"embed_tokens" -> Embedding
|
||
"layers" -> ModuleList
|
||
"norm" -> RMSNorm
|
||
```
|
||
|
||
**特点:**
|
||
- 每一层递归时,你都站在**父模块**上
|
||
- 可以直接 `setattr(model, name, new_child)` 替换子模块
|
||
- 需要自己递归进入子模块
|
||
|
||
---
|
||
|
||
## 原项目为什么用 named_modules()
|
||
|
||
原项目 `apply_lora` 的代码:
|
||
|
||
```python
|
||
def apply_lora(model, rank=16):
|
||
for name, module in model.named_modules():
|
||
if isinstance(module, nn.Linear) and module.in_features == module.out_features:
|
||
lora = LoRA(module.in_features, module.out_features, rank=rank).to(model.device)
|
||
setattr(module, "lora", lora)
|
||
original_forward = module.forward
|
||
|
||
def forward_with_lora(x, layer1=original_forward, layer2=lora):
|
||
return layer1(x) + layer2(x)
|
||
|
||
module.forward = forward_with_lora
|
||
```
|
||
|
||
**关键:这里没有替换模块,而是修改现有模块对象本身。**
|
||
|
||
```
|
||
原始:q_proj: nn.Linear
|
||
|
||
改后:q_proj: nn.Linear (被 monkey-patch 了 forward)
|
||
└── 多了 .lora 属性
|
||
```
|
||
|
||
- `module.forward = new_forward` 直接改了 module 自己的方法
|
||
- `setattr(module, "lora", lora)` 直接在 module 上挂属性
|
||
- **不需要父模块参与**,只需要拿到 module 对象
|
||
|
||
所以 `named_modules()` 完全够用。
|
||
|
||
---
|
||
|
||
## 如果要做模块替换用什么
|
||
|
||
如果想**替换**一个模块,而不是修改它:
|
||
|
||
```python
|
||
# 这样写是无效的!
|
||
for name, module in model.named_modules():
|
||
if isinstance(module, nn.Linear):
|
||
module = LoRALinear(module) # 只改了局部变量,模型树没变
|
||
```
|
||
|
||
模型里的 `q_proj` 还是原来的 Linear。
|
||
|
||
**必须改父模块:**
|
||
|
||
```python
|
||
# 解析路径
|
||
parent_path, child_name = name.rsplit(".", 1)
|
||
parent = model.get_submodule(parent_path)
|
||
setattr(parent, child_name, LoRALinear(module))
|
||
```
|
||
|
||
**这比 named_children() 递归麻烦多了。**
|
||
|
||
用 `named_children()` 递归更自然:
|
||
|
||
```python
|
||
def apply_lora_to_linear_layers(model):
|
||
for name, child in model.named_children():
|
||
if isinstance(child, nn.Linear):
|
||
setattr(model, name, LoRALinear(child)) # 站在父模块上替换
|
||
else:
|
||
apply_lora_to_linear_layers(child)
|
||
```
|
||
|
||
每一层递归时,`model` 就是当前父模块,`name` 是直接子模块名,可以直接 `setattr` 替换。
|
||
|
||
---
|
||
|
||
## 对比总结
|
||
|
||
| 场景 | 用哪个 | 为什么 |
|
||
|------|--------|--------|
|
||
| monkey-patch forward / 挂属性 | `named_modules()` | 只需要对象本身,不需要父模块 |
|
||
| 替换模块 | `named_children()` | 需要父模块才能执行 `setattr(parent, child_name, new_module)` |
|
||
| 统计参数 / 收集信息 | `named_modules()` | 要遍历所有模块 |
|
||
|
||
**一句话总结:**
|
||
|
||
- `named_modules()` 是"我看到的所有节点"
|
||
- `named_children()` 是"我直接管理的子节点 + 我可以替换它们"
|