Upload ms-swift/docs/source/Customization/插件化.md with huggingface_hub
Browse files
ms-swift/docs/source/Customization/插件化.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 插件化
|
| 2 |
+
|
| 3 |
+
插件化是SWIFT3.0中新增的重要能力。我们希望通过插件化的方式,让开发者对开发流程的定制更加自然。
|
| 4 |
+
|
| 5 |
+
## callback回调
|
| 6 |
+
|
| 7 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/callback.py).
|
| 8 |
+
|
| 9 |
+
`callback`机制是transformers Trainer中的一种训练定制化机制。开发者可以在callback中控制训练流程。通常来说,callback的定制化类似下面的样子:
|
| 10 |
+
```python
|
| 11 |
+
class CustomCallback(TrainerCallback):
|
| 12 |
+
|
| 13 |
+
def on_train_begin(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs):
|
| 14 |
+
# Doing something when the training begins.
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
def on_save(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs):
|
| 18 |
+
# Doing something when save checkpoint
|
| 19 |
+
pass
|
| 20 |
+
```
|
| 21 |
+
callback会在trainer构造前注册进trainer中,example中给出了一个简单版本的EarlyStop方案。注册你自己的callback的方式比较简单:
|
| 22 |
+
```python
|
| 23 |
+
extra_callbacks = [CustomCallback()]
|
| 24 |
+
```
|
| 25 |
+
开发者可以在plugin/callback.py中增加新的callback,并定制自己的训练流程。callback的具体参数可以查看[这里](https://huggingface.co/docs/transformers/main_classes/callback)。
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
## 定制化loss
|
| 29 |
+
|
| 30 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/loss.py).
|
| 31 |
+
|
| 32 |
+
SWIFT支持在plugin中定制loss。如果不使用这个能力,默认会使用交叉熵Loss(CE Loss)。开发者可以在这个文件中编写代码,注册后trainer会自动使用你定制的loss方法。
|
| 33 |
+
例如在plugin/loss.py中添加下面的代码:
|
| 34 |
+
```python
|
| 35 |
+
@register_loss_func("custom_loss")
|
| 36 |
+
def loss_scale_func(outputs, labels, loss_scale=None, num_items_in_batch=None) -> torch.Tensor:
|
| 37 |
+
# Write your own loss calculating here
|
| 38 |
+
return loss
|
| 39 |
+
```
|
| 40 |
+
需要注意的是,loss和trainer训练的任务是强相关的,目前的loss定制针对pt和sft任务,如果是人类对齐任务(例如DPO、PPO等)或分类任务(seq_cls)任务在插件中是无法定制的。
|
| 41 |
+
|
| 42 |
+
## 定制化loss_scale
|
| 43 |
+
|
| 44 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/loss_scale.py).
|
| 45 |
+
|
| 46 |
+
loss_scale机制在SWIFT中是非常重要的机制之一。在pt和sft任务中,可训练token的loss是均匀的,即每个token平等的进行bp。但在某些情况下,某些token的权重比较大,需要被额外关注,
|
| 47 |
+
在这种情况下就需要更高的权重。loss_scale可以让开发者自由地定义自己的token权重。
|
| 48 |
+
```python
|
| 49 |
+
class LastRoundLossScale(LossScale):
|
| 50 |
+
|
| 51 |
+
def get_loss_scale(self, context: str, context_type: ContextType, is_last_round: bool, **kwargs):
|
| 52 |
+
if context_type == ContextType.RESPONSE:
|
| 53 |
+
return [context], [float(is_last_round)]
|
| 54 |
+
return super().get_loss_scale(context, context_type, is_last_round)
|
| 55 |
+
```
|
| 56 |
+
在上面的代码中,返回了一个Tuple,第一个返回是context(或拆解后的context),第二个参数是context对应的loss_scale,float值代表了权重。例如下面的权重设置:
|
| 57 |
+
```text
|
| 58 |
+
["学习", "好", "数学", "是", "重要", "的"]
|
| 59 |
+
[1.0, 0.5, 2.0, 0.5, 2.0, 0.1]
|
| 60 |
+
```
|
| 61 |
+
我们更看重数学和重要两个词,因此我们把它们的权重提升到2.0。
|
| 62 |
+
回到上面的代码,我们判断了传入的context是否是response,如果是response且如果是多轮对话的最后一轮才返回[1],在其他情况下使用基类的实现(在本场景下loss_scale时[0])。使用这种方案,
|
| 63 |
+
我们做到了只有最后一轮的response参与训练,其他response不参与训练。使用这种方式,可以让所有token(prompt、response)参与训练,或针对agent某些特殊字符重点训练等。
|
| 64 |
+
在pt和sft中,loss_scale是整体支持(是否参与训练,以及权重大小)的,而人类对齐中只能支持某些token是否参与训练,无法支持权重大小。
|
| 65 |
+
|
| 66 |
+
## 定制化metric
|
| 67 |
+
|
| 68 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/metric.py).
|
| 69 |
+
|
| 70 |
+
metric可以定制训练时使用的评测参数:
|
| 71 |
+
```python
|
| 72 |
+
METRIC_MAPPING = {
|
| 73 |
+
'acc': (compute_acc_metrics, preprocess_logits_for_acc),
|
| 74 |
+
'nlg': (compute_nlg_metrics, None),
|
| 75 |
+
'custom': (custom_metric, custom_preprocess),
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_metric(metric: str):
|
| 80 |
+
return METRIC_MAPPING[metric]
|
| 81 |
+
```
|
| 82 |
+
在上面的定义中,我们添加了新的custom metric,它的value有两个值,第一个值是计算metric的过程,返回一个包含metric key-value对的dict,第二个值是针对logits做前处理,返回实际的predictions。
|
| 83 |
+
|
| 84 |
+
## 定制化optimizer
|
| 85 |
+
|
| 86 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/optimizer.py).
|
| 87 |
+
|
| 88 |
+
用户可以在这里增加自己的optimizer和lr_scheduler实现:
|
| 89 |
+
```python
|
| 90 |
+
def create_custom_optimizers(args, model, dataset):
|
| 91 |
+
# 创建自己的optimizer
|
| 92 |
+
return CustomOptimizer(optimizer_grouped_parameters, **optimizer_kwargs), CustomScheduler(...)
|
| 93 |
+
|
| 94 |
+
optimizers_map = {
|
| 95 |
+
'custom': create_custom_optimizers,
|
| 96 |
+
...
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
当开发者需要使用其他optimizer,例如某些新论文中定义的optimizer时,可以在这里定义其创建过程,并在参数中使用:
|
| 101 |
+
```shell
|
| 102 |
+
--optimizer custom
|
| 103 |
+
```
|
| 104 |
+
就可以实际调用了。
|
| 105 |
+
|
| 106 |
+
## 定制化tools
|
| 107 |
+
|
| 108 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/tools.py).
|
| 109 |
+
|
| 110 |
+
可以在这里定义Agent训练的tools格式。tools格式是指训练和推理时如何将工具枚举在system字段中,例如glm4就有其独特的tools格式:
|
| 111 |
+
```python
|
| 112 |
+
def format_glm4(tool_names, tool_descs):
|
| 113 |
+
GLM4_PROMPT = """你是一个名为 ChatGLM 的人工智能助手。你是基于智谱AI训练的语言模型 GLM-4 模型开发的,你的任务是针对用户的问题和要求提供适当的答复和支持。
|
| 114 |
+
|
| 115 |
+
# 可用工具
|
| 116 |
+
|
| 117 |
+
{tool_list}"""
|
| 118 |
+
tool_descs = [json.dumps(t) if not isinstance(t, str) else t for t in tool_descs]
|
| 119 |
+
tool_list = ''
|
| 120 |
+
for name, tool in zip(tool_names, tool_descs):
|
| 121 |
+
tool_list += f'## {name}\n\n{tool}\n\n'
|
| 122 |
+
return GLM4_PROMPT.format(tool_list=tool_list)
|
| 123 |
+
```
|
| 124 |
+
system中的完整格式类似于这样:
|
| 125 |
+
```text
|
| 126 |
+
你是一个名为 ChatGLM 的人工智能助手。你是基于智谱AI训练的语言模型 GLM-4 模型开发的,你的任务是针对用户的问题和要求提供适当的答复和支持。
|
| 127 |
+
|
| 128 |
+
# 可用工具
|
| 129 |
+
|
| 130 |
+
## 查看天气
|
| 131 |
+
|
| 132 |
+
...
|
| 133 |
+
|
| 134 |
+
## 搜索网络
|
| 135 |
+
|
| 136 |
+
...
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
## 定制化tuner
|
| 140 |
+
|
| 141 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/tuner.py).
|
| 142 |
+
|
| 143 |
+
tuner定制也是swift中有特色的能力之一,开发者可以无视复杂的tuner初始化流程和代码整合成本,将新的tuner注册在这里:
|
| 144 |
+
```python
|
| 145 |
+
class IA3(Tuner):
|
| 146 |
+
|
| 147 |
+
@staticmethod
|
| 148 |
+
def prepare_model(args: 'TrainArguments', model: torch.nn.Module) -> torch.nn.Module:
|
| 149 |
+
model_arch: ModelKeys = MODEL_ARCH_MAPPING[model.model_meta.model_arch]
|
| 150 |
+
ia3_config = IA3Config(
|
| 151 |
+
target_modules=find_all_linears(model), feedforward_modules='.*' + model_arch.mlp.split('{}.')[1] + '.*')
|
| 152 |
+
return get_peft_model(model, ia3_config)
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
def save_pretrained(
|
| 156 |
+
model: torch.nn.Module,
|
| 157 |
+
save_directory: str,
|
| 158 |
+
state_dict: Optional[dict] = None,
|
| 159 |
+
safe_serialization: bool = True,
|
| 160 |
+
**kwargs,
|
| 161 |
+
) -> None:
|
| 162 |
+
model: PeftModel
|
| 163 |
+
model.save_pretrained(save_directory, state_dict=state_dict, safe_serialization=safe_serialization, **kwargs)
|
| 164 |
+
|
| 165 |
+
@staticmethod
|
| 166 |
+
def from_pretrained(model: torch.nn.Module, model_id: str, **kwargs) -> torch.nn.Module:
|
| 167 |
+
return PeftModel.from_pretrained(model, model_id, **kwargs)
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
上面的例子中,我们将peft的IA3应用于模型训练中,在这个类中包含了三个方法:
|
| 171 |
+
- prepare_model: 如何将原始模型使用tuner进行封装,并设置好可训练参数
|
| 172 |
+
- save_pretrained: 如何在训练中保存模型
|
| 173 |
+
- from_pretrained: 如何在后续训练和推理中将之前存下来的checkpoint重新拉起
|
| 174 |
+
|
| 175 |
+
上面的三个方法会在swift训练流程中被调用,这样就做到了开发者可以不阅读复杂的训练代码而使用自己的tuner。
|
| 176 |
+
|
| 177 |
+
## PRM
|
| 178 |
+
|
| 179 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/prm.py)。
|
| 180 |
+
|
| 181 |
+
PRM是过程奖励模型,PRM会在`swift sample`命令中使用。PRM需要支持的接口比较简单:
|
| 182 |
+
```python
|
| 183 |
+
class PRM:
|
| 184 |
+
|
| 185 |
+
def __init__(self):
|
| 186 |
+
# init here
|
| 187 |
+
pass
|
| 188 |
+
|
| 189 |
+
def __call__(self, infer_requests: List[InferRequest], **kwargs) -> List[Union[float, List[float]]]:
|
| 190 |
+
raise NotImplementedError
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
其中的InferRequest来自于`swift.llm`,返回的`List[Union[float, List[float]]]`,列表中可能是reward也可能是若干reward。开发者可以在infer_requests中拿到queries和responses,并按照自己的方式进行切分,例如:
|
| 194 |
+
```text
|
| 195 |
+
Let's think step by step.
|
| 196 |
+
|
| 197 |
+
Step1: xxx
|
| 198 |
+
|
| 199 |
+
Step2: xxx
|
| 200 |
+
|
| 201 |
+
So, the answer is ...
|
| 202 |
+
```
|
| 203 |
+
开发者可以在这里对过程进行切分,并按batch传入PRM中进行推理并返回rewards。更通用来说,开发者可以在这里调用一个远端URL,例如一个闭源PRM大模型并返回rewards。
|
| 204 |
+
|
| 205 |
+
## ORM
|
| 206 |
+
|
| 207 |
+
example在[这里](https://github.com/modelscope/swift/blob/main/swift/plugin/orm.py)。
|
| 208 |
+
|
| 209 |
+
ORM是结果奖励模型。ORM一般使用正则表达式来进行,ORM决定了response是否是正确的。例如:
|
| 210 |
+
|
| 211 |
+
```python
|
| 212 |
+
class MathORM(ORM):
|
| 213 |
+
|
| 214 |
+
@staticmethod
|
| 215 |
+
def extract_boxed_result(text):
|
| 216 |
+
pattern = r'\\boxed{([^}]*)}'
|
| 217 |
+
match = re.search(pattern, text)
|
| 218 |
+
if match:
|
| 219 |
+
return match.group(1).strip()
|
| 220 |
+
else:
|
| 221 |
+
return None
|
| 222 |
+
|
| 223 |
+
def __call__(self, infer_requests: List[InferRequest], ground_truths: List[str],
|
| 224 |
+
**kwargs) -> List[float]:
|
| 225 |
+
rewards = []
|
| 226 |
+
predictions = [request.messages[-1]['content'] for request in infer_requests]
|
| 227 |
+
for prediction, ground_truth in zip(predictions, ground_truths):
|
| 228 |
+
res1 = MathORM.extract_boxed_result(prediction) or ''
|
| 229 |
+
res2 = MathORM.extract_boxed_result(ground_truth) or ''
|
| 230 |
+
rewards.append(float(res1.strip() == res2.strip()))
|
| 231 |
+
|
| 232 |
+
return rewards
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
orms = {
|
| 236 |
+
'math': MathORM,
|
| 237 |
+
}
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
在上面的代码中,我们定义了一个对数学response进行解析的过程,如果结果相同则返回score为1.0,否则为0.0。和PRM不同,这个类的infer中有一个额外参数`ground_truths`,
|
| 241 |
+
该参数是对应的infer_requests的实际label(数据集中定义的标准response)。
|