minimind/course/notes/19_lora_notes.md

158 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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()` 是"我直接管理的子节点 + 我可以替换它们"