回顾:trainer.py中的CustomTrainer就调用了这里面的回调方法

源代码 

class SaveProcessorCallback(TrainerCallback):
    def __init__(self, processor: "ProcessorMixin") -> None:
        r"""
        Initializes a callback for saving the processor.
        """
        self.processor = processor

    def on_train_end(self, args: "TrainingArguments", state: "TrainerState", control: "TrainerControl", **kwargs):
        r"""
        Event called at the end of training.
        """
        if args.should_save:
            getattr(self.processor, "image_processor").save_pretrained(args.output_dir)


class PissaConvertCallback(TrainerCallback):
    r"""
    Initializes a callback for converting the PiSSA adapter to a normal one.
    """

    def on_train_begin(self, args: "TrainingArguments", state: "TrainerState", control: "TrainerControl", **kwargs):
        r"""
        Event called at the beginning of training.
        """
        if args.should_save:
            model = kwargs.pop("model")
            pissa_init_dir = os.path.join(args.output_dir, "pissa_init")
            logger.info("Initial PiSSA adatper will be saved at: {}.".format(pissa_init_dir))
            if isinstance(model, PeftModel):
                init_lora_weights = getattr(model.peft_config["default"], "init_lora_weights")
                setattr(model.peft_config["default"], "init_lora_weights", True)
                model.save_pretrained(pissa_init_dir, safe_serialization=args.save_safetensors)
                setattr(model.peft_config["default"], "init_lora_weights", init_lora_weights)

让我们逐行解释 SaveProcessorCallback 和 PissaConvertCallback 类的代码。这些类继承自 TrainerCallback 并实现了特定的回调功能

SaveProcessorCallback 类

类定义初始化方法

python

复制

class SaveProcessorCallback(TrainerCallback):
    def __init__(self, processor: "ProcessorMixin") -> None:
        r"""
        Initializes a callback for saving the processor.
        """
        self.processor = processor
  • 类定义SaveProcessorCallback 继承自 TrainerCallback,用于在训练结束时保存处理器。
  • 初始化方法
    • __init__ 方法接受一个 processor 参数,并将其赋值给实例变量 self.processor
    • processor 的类型注解为 "ProcessorMixin"字符串形式的类型注解,用于解决前向引用)。
on_train_end 方法

python

复制

    def on_train_end(self, args: "TrainingArguments", state: "TrainerState", control: "TrainerControl", **kwargs):
        r"""
        Event called at the end of training.
        """
        if args.should_save:
            getattr(self.processor, "image_processor").save_pretrained(args.output_dir)
  • 方法定义on_train_end 方法在训练结束时被调用
  • 参数
    • args"TrainingArguments"):训练参数。
    • state"TrainerState"):训练状态。
    • control"TrainerControl"):训练控制。
    • **kwargs:其他关键字参数。
  • 方法实现
    • 检查 args.should_save 是否为真,如果为真,则调用 self.processor 的 image_processor 属性的 save_pretrained 方法,将处理器保存到 args.output_dir 指定的目录中

PissaConvertCallback 类

类定义和文档字符串

python

复制

class PissaConvertCallback(TrainerCallback):
    r"""
    Initializes a callback for converting the PiSSA adapter to a normal one.
    """
  • 类定义PissaConvertCallback 继承自 TrainerCallback,用于在训练开始时转换 PiSSA 适配器。
  • 文档字符串描述了该类的功能,即将 PiSSA 适配器转换为普通适配器的回调。
on_train_begin 方法

python

复制

    def on_train_begin(self, args: "TrainingArguments", state: "TrainerState", control: "TrainerControl", **kwargs):
        r"""
        Event called at the beginning of training.
        """
        if args.should_save:
            model = kwargs.pop("model")
            pissa_init_dir = os.path.join(args.output_dir, "pissa_init")
            logger.info("Initial PiSSA adatper will be saved at: {}.".format(pissa_init_dir))
            if isinstance(model, PeftModel):
                init_lora_weights = getattr(model.peft_config["default"], "init_lora_weights")
                setattr(model.peft_config["default"], "init_lora_weights", True)
                model.save_pretrained(pissa_init_dir, safe_serialization=args.save_safetensors)
                setattr(model.peft_config["default"], "init_lora_weights", init_lora_weights)
  • 方法定义on_train_begin 方法在训练开始时被调用。
  • 参数
    • args"TrainingArguments"):训练参数。
    • state"TrainerState"):训练状态。
    • control"TrainerControl"):训练控制。
    • **kwargs其他关键字参数
  • 方法实现
    • 检查 args.should_save 是否为真,如果为真,执行以下操作:
      1. 从 kwargs 中弹出 model 参数。
      2. 构造 pissa_init_dir 路径,用于保存初始 PiSSA 适配器。
      3. 记录一条信息,指示初始 PiSSA 适配器将被保存的位置。
      4. 检查 model 是否为 PeftModel 实例
        • 获取当前 init_lora_weights 的值。
        • 将 init_lora_weights 设置为 True
        • 调用 model 的 save_pretrained 方法,将模型保存到 pissa_init_dir,并根据 args.save_safetensors 决定是否进行安全序列化。
        • 恢复原始的 init_lora_weights 值。

总结

  • SaveProcessorCallback 类在训练结束时保存处理器。
  • PissaConvertCallback 类在训练开始时将 PiSSA 适配器转换为普通适配器并保存初始适配器。

这两个回调类通过在训练过程中的不同时间点执行自定义逻辑,增强了训练流程的灵活性和功能。

def on_train_end(self, args: "TrainingArguments", state: "TrainerState", control: "TrainerControl", **kwargs):
        r"""
        Event called at the end of training.
        """
        if args.should_save:
            model = kwargs.pop("model")
            pissa_init_dir = os.path.join(args.output_dir, "pissa_init")
            pissa_backup_dir = os.path.join(args.output_dir, "pissa_backup")
            pissa_convert_dir = os.path.join(args.output_dir, "pissa_converted")
            logger.info("Converted PiSSA adapter will be saved at: {}.".format(pissa_convert_dir))
            # 1. save a pissa backup with init_lora_weights: True
            # 2. save a converted lora with init_lora_weights: pissa
            # 3. load the pissa backup with init_lora_weights: True
            # 4. delete the initial adapter and change init_lora_weights to pissa
            if isinstance(model, PeftModel):
                init_lora_weights = getattr(model.peft_config["default"], "init_lora_weights")
                setattr(model.peft_config["default"], "init_lora_weights", True)
                model.save_pretrained(pissa_backup_dir, safe_serialization=args.save_safetensors)
                setattr(model.peft_config["default"], "init_lora_weights", init_lora_weights)
                model.save_pretrained(
                    pissa_convert_dir, safe_serialization=args.save_safetensors, convert_pissa_to_lora=pissa_init_dir
                )
                model.load_adapter(pissa_backup_dir, "default", is_trainable=True)
                model.set_adapter("default")
                model.delete_adapter("pissa_init")
                setattr(model.peft_config["default"], "init_lora_weights", init_lora_weights)

继续逐行解释上述代码的后续部分,这是 PissaConvertCallback 类中的 on_train_end 方法的实现。

on_train_end 方法

方法定义和文档字符串

python

复制

def on_train_end(self, args: "TrainingArguments", state: "TrainerState", control: "TrainerControl", **kwargs):
    r"""
    Event called at the end of training.
    """
  • 方法定义on_train_end 方法在训练结束时被调用。
  • 参数
    • args"TrainingArguments"):训练参数。
    • state"TrainerState"):训练状态。
    • control"TrainerControl"):训练控制。
    • **kwargs:其他关键字参数。
获取模型和目录路径

python

复制

    if args.should_save:
        model = kwargs.pop("model")
        pissa_init_dir = os.path.join(args.output_dir, "pissa_init")
        pissa_backup_dir = os.path.join(args.output_dir, "pissa_backup")
        pissa_convert_dir = os.path.join(args.output_dir, "pissa_converted")
        logger.info("Converted PiSSA adapter will be saved at: {}.".format(pissa_convert_dir))
  • 检查:检查 args.should_save 是否为真。
  • 获取模型:从 kwargs 中弹出 model 参数。
  • 定义路径
    • pissa_init_dir:初始 PiSSA 适配器保存路径。
    • pissa_backup_dir:PiSSA 备份保存路径。
    • pissa_convert_dir:转换后的 PiSSA 适配器保存路径。
  • 日志记录:记录转换后的 PiSSA 适配器将被保存的位置。
处理 PiSSA 适配器

python

复制

        # 1. save a pissa backup with init_lora_weights: True
        # 2. save a converted lora with init_lora_weights: pissa
        # 3. load the pissa backup with init_lora_weights: True
        # 4. delete the initial adapter and change init_lora_weights to pissa
        if isinstance(model, PeftModel):
            init_lora_weights = getattr(model.peft_config["default"], "init_lora_weights")
            setattr(model.peft_config["default"], "init_lora_weights", True)
            model.save_pretrained(pissa_backup_dir, safe_serialization=args.save_safetensors)
            setattr(model.peft_config["default"], "init_lora_weights", init_lora_weights)
            model.save_pretrained(
                pissa_convert_dir, safe_serialization=args.save_safetensors, convert_pissa_to_lora=pissa_init_dir
            )
            model.load_adapter(pissa_backup_dir, "default", is_trainable=True)
            model.set_adapter("default")
            model.delete_adapter("pissa_init")
            setattr(model.peft_config["default"], "init_lora_weights", init_lora_weights)
  • 注释:列出了处理 PiSSA 适配器的四个步骤:

    1. 使用 init_lora_weights: True 保存一个 PiSSA 备份。
    2. 使用 init_lora_weights: pissa 保存一个转换后的 lora。
    3. 加载使用 init_lora_weights: True 保存的 PiSSA 备份。
    4. 删除初始适配器并将 init_lora_weights 更改为 pissa
  • 检查模型类型:检查 model 是否为 PeftModel 实例。

  • 获取 init_lora_weights:获取当前 init_lora_weights 的值。

  • 设置 init_lora_weights 为 True:将 init_lora_weights 设置为 True

  • 保存 PiSSA 备份:调用 model 的 save_pretrained 方法,将模型保存到 pissa_backup_dir,并根据 args.save_safetensors 决定是否进行安全序列化。

  • 恢复 init_lora_weights:将 init_lora_weights 恢复为原始值。

  • 保存转换后的 lora

    • 调用 model 的 save_pretrained 方法,将模型保存到 pissa_convert_dir,同时传递 convert_pissa_to_lora=pissa_init_dir 参数用于转换。
  • 加载 PiSSA 备份

    • 调用 model 的 load_adapter 方法,从 pissa_backup_dir 加载适配器,并设置为可训练。
  • 设置适配器

    • 调用 model 的 set_adapter 方法,设置当前适配器为 default
  • 删除初始适配器

    • 调用 model 的 delete_adapter 方法,删除 pissa_init 适配器。
  • 恢复 init_lora_weights:将 init_lora_weights 恢复为原始值。

总结

  • on_train_end 方法在训练结束时被调用,执行一系列操作来处理 PiSSA 适配器,包括备份、转换、加载和删除。
  • 检查是否应该保存模型,如果是,则执行适配器的备份、转换和加载操作。
  • 通过设置和恢复 init_lora_weights,确保适配器的正确处理和保存。

这些操作确保了在训练结束时,PiSSA 适配器能够被正确地转换和保存,从而可以在后续的训练或推理过程中使用。

前向引用是什么意思,和其他的引用有什么区别

processor 的类型注解为 "ProcessorMixin"(字符串形式的类型注解,用于解决前向引用)。

前向引用(forward reference)是指在代码中引用一个尚未定义的类或类型。在 Python 中,有时候需要在类定义之前引用该类。使用字符串形式的类型注解是一种解决这种情况的方法。

前向引用的解释

在 Python 的类型注解中,如果你需要引用一个在代码中尚未定义的类或类型,可以使用字符串形式的类型注解。这通常发生在以下两种情况中:

  1. 类的自引用:一个类的方法需要引用该类自身。例如,一个链表节点类的方法可能返回另一个链表节点。
  2. 相互引用的类:两个类相互引用对方。例如,A 类的方法返回 B 类的实例,而 B 类的方法返回 A 类的实例。

示例

使用字符串形式的前向引用

python

复制

class A:
    def __init__(self, b: "B"):
        self.b = b

class B:
    def __init__(self, a: "A"):
        self.a = a

在上面的例子中,类 A 和 B 相互引用对方。在定义 A 时,B 还没有定义,因此我们使用字符串形式 "B" 作为类型注解。同样,在定义 B 时,A 也已经定义好。

使用 from __future__ import annotations

从 Python 3.7 开始,可以使用 from __future__ import annotations 让所有的类型注解以字符串形式解析,从而避免前向引用的问题:

python

复制

from __future__ import annotations

class A:
    def __init__(self, b: B):
        self.b = b

class B:
    def __init__(self, a: A):
        self.a = a

与其他引用的区别

  • 普通引用:在类型注解中直接引用已经定义的类或类型。这种方式要求被引用的类或类型已经定义。

    python

    复制
    class B:
        pass
    
    class A:
        def __init__(self, b: B):  # 普通引用
            self.b = b
    
  • 前向引用:在类型注解中引用尚未定义的类或类型,需要使用字符串形式。

    python

    复制
    class A:
        def __init__(self, b: "B"):  # 前向引用
            self.b = b
    
    class B:
        pass
    

结论

前向引用允许在类定义之前引用该类,这在类的自引用或相互引用时非常有用。使用字符串形式的类型注解是实现前向引用的常见方法。通过 from __future__ import annotations,可以在 Python 3.7 及以上版本中自动将所有类型注解解释为字符串形式,从而简化代码。

回调函数是干嘛的,和普通函数的区别在哪

GPT-4o的机器人图片

GPT-4o

Poe

回调函数的定义

回调函数(callback function)是指通过参数传递另一函数的函数。当这个函数完成某项任务或达到某个特定条件时,它会调用(或“回调”)之前传递给它的函数。回调函数通常用于异步编程、事件驱动编程或需要在特定条件下执行的操作。

回调函数的用途

回调函数通常用于以下场景:

  1. 异步操作:比如网络请求、文件读取等需要时间的操作,通过回调函数可以在操作完成后执行特定的代码
  2. 事件驱动编程:在图形用户界面(GUI)编程中,按钮点击、鼠标移动等事件触发时调用回调函数。
  3. 函数的扩展:允许在函数执行过程中插入额外的操作,而无需修改函数的内部实现。

回调函数与普通函数的区别

  • 调用方式:普通函数在代码中直接调用,而回调函数通常是作为参数传递给另一个函数,由后者在特定事件或条件下调用。
  • 执行时机普通函数在调用时立即执行,而回调函数的执行时机由传递它的函数决定。
  • 灵活性:回调函数提供了更高的灵活性,可以让调用者决定在特定时间点执行哪些操作。

回调函数示例

基本示例

python

复制

def my_callback():
    print("Callback function called!")

def process(callback):
    print("Processing...")
    # 模拟某种处理
    callback()  # 调用回调函数

process(my_callback)

在这个例子中,my_callback 是一个回调函数,它被传递给 process 函数并在特定时机(这里是处理完成后)被调用。

异步操作示例

python

复制

import time

def my_callback(result):
    print(f"Callback called with result: {result}")

def async_process(data, callback):
    print("Starting async process...")
    time.sleep(2)  # 模拟耗时操作
    result = data * 2
    callback(result)  # 操作完成后调用回调函数

async_process(5, my_callback)

在这个例子中,async_process 模拟了一个异步操作,操作完成后调用回调函数 my_callback 并传递结果

高级示例:事件驱动编程

使用回调函数在事件驱动编程中的应用,通常见于 GUI 编程。例如,在一个按钮点击事件中调用回调函数:

python

复制

import tkinter as tk

def on_button_click():
    print("Button clicked!")

root = tk.Tk()
button = tk.Button(root, text="Click Me", command=on_button_click)
button.pack()

root.mainloop()

在这个例子中,on_button_click 是一个回调函数,当按钮被点击时,它将被调用。

总结

回调函数提供了一种在特定时机执行代码的机制,使得代码更加灵活和模块化。与普通函数不同,回调函数的调用被延迟到某个特定事件或条件发生时。通过使用回调函数,可以实现异步操作、事件驱动编程和函数的扩展,从而提高代码的可复用性和可维护性。

command=on_button_click就是调用该函数对吗

GPT-4o的机器人图片

GPT-4o

Poe

不完全对。command=on_button_click 并不是在立即调用 on_button_click 函数,而是将 on_button_click 函数作为一个回调函数传递给按钮的 command 参数。按钮点击事件发生时,Tkinter 框架会调用这个回调函数

详细解释

当你使用 command=on_button_click 时,你实际上是在将函数 on_button_click 的引用(而不是调用结果)传递给按钮的 command 属性。这样,当按钮被点击时,Tkinter 框架会执行 on_button_click 函数。

示例

python

复制

import tkinter as tk

def on_button_click():
    print("Button clicked!")

root = tk.Tk()
button = tk.Button(root, text="Click Me", command=on_button_click)  # 传递函数引用
button.pack()

root.mainloop()

在这个例子中:

  • on_button_click 是一个回调函数,用于处理按钮点击事件。
  • command=on_button_click 将 on_button_click 函数的引用传递给按钮的 command 参数。
  • 当用户点击按钮时,Tkinter 框架会调用 on_button_click 函数,执行其中的代码。

回调函数与立即调用的区别

直接调用

如果你在 command 参数中使用 on_button_click()(带括号),就相当于立即调用该函数并将其返回值(如果有)赋给 command 参数。这通常不是你想要的效果,因为你希望在按钮被点击时才执行该函数,而不是在创建按钮时就执行。

python

复制

import tkinter as tk

def on_button_click():
    print("Button clicked!")

root = tk.Tk()
button = tk.Button(root, text="Click Me", command=on_button_click())  # 错误:立即调用了函数
button.pack()

root.mainloop()

在这个错误的例子中,on_button_click() 会在创建按钮时立即被调用,而不是在按钮被点击时调用。

使用 lambda 传递带参数的回调函数

如果你的回调函数需要参数,可以使用 lambda 表达式来传递参数。

python

复制

import tkinter as tk

def on_button_click(message):
    print(message)

root = tk.Tk()
button = tk.Button(root, text="Click Me", command=lambda: on_button_click("Button clicked!"))
button.pack()

root.mainloop()

在这个例子中,lambda 表达式创建了一个匿名函数,当按钮被点击时,该匿名函数会调用 on_button_click("Button clicked!")

总结

  • command=on_button_click 将函数引用传递给按钮的 command 属性,当按钮被点击时,Tkinter 框架调用该函数。
  • 使用 command=on_button_click() 会立即调用函数,而不是在按钮点击时调用,通常不是你想要的效果。
  • 使用 lambda 可以创建匿名函数,以便传递带参数的回调函数。

通过正确使用回调函数,可以让你的应用程序具有响应用户交互的能力,从而实现更复杂的逻辑和功能。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐