深度学习代码优化(Config,Registry,Hook)

news2025/1/20 20:14:02

社区开放麦#9 | OpenMMLab 模块化设计背后的功臣

1. 配置文件管理Config

1.1 早期配置参数加载

早期深度学习项目的代码大多使用parse_args,在代码启动入口加载大量参数,不利于维护。

在这里插入图片描述
在这里插入图片描述
常见的配置文件有3中格式:pythonjsonyaml 格式的配置文件,推荐使用Yaml文件来配置训练参数。

基本所有能影响你模型的因素,都被涵括在了这个文件里,而在代码中,你只需要用一个简单的 yaml.load()就能把这些参数全部读到一个dict里。更关键的是,这个配置文件可以随着你的checkpoint一起被存到相同的文件夹,方便你直接拿来做断点训练、finetune或者直接做测试,用来做测试时你也可以很方便把结果和对应的参数对上。

1.2 方案:Click+OmegaConf

效果和hydra类似,把所有的参数都写在 YAML 文件中。click读取命令行中的config文件路径(也可以不传入,使用代码中默认的config文件路径)然后用Omegaconf根据传入的路径读取配置文件,因此只需要在命令行指定配置文件路径,而不是用argparse控制所有的参数,参数一多命令行参数在shell文件中就会特别长,看起来很乱。

pretrained_model_path: "./ckpt/stable-diffusion-v1-5"
pretrained_controlnet_model_path: "./ckpt/sd-controlnet-canny"
control_type: 'canny'

dataset_config:
    video_path: "videos/hat.mp4"
    prompt: "A woman with a white hat"
    n_sample_frame: 1
    # n_sample_frame: 22
    sampling_rate: 1
    stride: 80
    offset: 
        left: 0
        right: 0
        top: 0
        bottom: 0

editing_config:
    use_invertion_latents: True
    use_inversion_attention: True
    guidance_scale: 12
    editing_type: "attribute"
    dilation_kernel: 3
    editing_phrase: "hat"  # P_obj
    use_interpolater: True  # frame interpolater

    editing_prompts: "A woman with a pink hat"  # P_tgt
        # source prompt
    clip_length: "${..dataset_config.n_sample_frame}"
    num_inference_steps: 50
    prompt2prompt_edit: True

    
model_config:
    lora: 160
    # temporal_downsample_time: 4
    SparseCausalAttention_index: ['first','second','last'] 
    least_sc_channel: 640
    # least_sc_channel: 100000

test_pipeline_config:
    target: video_diffusion.pipelines.p2p_ddim_spatial_temporal_controlnet.P2pDDIMSpatioTemporalControlnetPipeline
    num_inference_steps: "${..validation_sample_logger.num_inference_steps}"

seed: 0

yaml文件全部放在configs路径下:

├── configs
│   ├── LOVECon.yaml
│   ├── TokenFlow.yaml
│   ├── Tune-A-Video.yaml
└── main.py

我们就可以对启动函数 run() 使用装饰器@click传入config.yaml路径,然后用OmegaConf像属性一样读写,处理好参数之后,再加载主函数main()

import click
from typing import Optional,Dict
from omegaconf import DictConfig, OmegaConf
from rich import print  # colorful print

def main(
    config: str,
    **kwargs):
    print("Training...")

@click.command()
@click.option("--config", type=str, default="Project_Manage\configs\data.yaml")
def run(config):
    # load config
    omega_dict = OmegaConf.load(config)
    print(omega_dict)
    # read config
    print(omega_dict.data_setting.data_path)
    # write config
    omega_dict.seed = 2
    # add config
    omega_dict.update({"num": 2})
    # merge config
    merge_dict = OmegaConf.merge(omega_dict, OmegaConf.load("Project_Manage\configs\model.yaml"))
    print(merge_dict)
	# save config
	OmegaConf.save(merge_dict, "Project_Manage\configs\merge.yaml")

    main(config=config, **omega_dict)

if __name__ == "__main__":  
    run()

2. 注册器机制Registry

2.1 预备知识:python装饰器

  • 一等对象first class:python中一切皆对象,函数不例外。first class是指可以运行时创建、可以赋值给变量、可以当参数传递、可以做函数返回值的东西。
    在这里插入图片描述

  • 高阶函数high order function:拿其他函数作为参数返回值的函数。
    在这里插入图片描述

  • 内层函数、外层函数:当函数嵌套定义的时候,外层函数的变量作用域 会扩展到 内层函数(说人话就是:inner函数可以使用outer函数的变量)。outer()作为高阶函数,返回一等对象inner()

def outer(a):
    def inner():
        return a
    return inner  # outer函数返回:inner函数(一等对象)
outer(1)()  # 最后的()调用inner函数
> 1
# 等价于 #
def outer(a):
    def inner():
        return a
    return inner()  # outer函数返回:inner函数调用结果
outer(1)
> 1
  • 闭包:当一个函数返回另一个函数时,内部函数访问外部函数的变量参数内部函数可见的外部对象们(变量或函数)就构成一个闭包环境__closure__。在下面例子中,inner函数形成了一个闭包,包含2个int对象,分别对应outer函数的参数a和b(闭包环境__closure__中可能有多个变量,是一个list)。当outer函数被调用时,它会返回inner函数的引用,同时实例化inner闭包环境中的int对象,inner函数仍然可以访问outer函数传递的参数a和b完成调用。
def outer(a, b):
    def inner():
        return a + b
    return inner  

inner = outer(1, 2)  # outer函数返回:inner函数(一等对象)
inner.__closure__  # inner的闭包环境:(<cell : int object>, <cell : int object>)
inner.__closure__[0].cell_contents  # 1
inner.__closure__[1].cell_contents  # 2
inner()  # 3
  • 万能形参*是对序列进行解包打包*args就是对传入的多个value参数(也叫positional arguments)进行打包成元组**kwargs就是对传入的多个key=value参数(也叫keyword arguments)进行打包成字典*args必须写在**kwargs之前)。 使用了万能形参,管你多少个参数,管你什么类型,我都可以扔到这两个里面。这就减少了重复写同名函数(避免函数重载)。
def foo(*number):  # 对1, 2, 3, 4, 5打包
    print(type(number), number)
foo(1, 2, 3, 4, 5)

def f(a, b, c):  # 对[1,2,3]解包
    print(a, b, c)
f(*[1, 2, 3])
def foo(*args, **kwargs):
    print ('args = ', args)    
    print ('kwargs = ', kwargs)
    print ("-"*40)
if __name__ == '__main__':
    foo(1 ,2 ,3 ,4)  # 对 value 参数进行打包
    foo(a=1 ,b=2 ,c=3)  # 对 key=value 参数进行打包
    foo(1 ,2 ,3 ,4, a=1 ,b=2 ,c=3)
    foo('a', 1, None, a=1, b='2', c=3)
args =  (1, 2, 3, 4)
kwargs =  {}
----------------------------------------
args =  ()
kwargs =  {'a': 1, 'b': 2, 'c': 3}
----------------------------------------
args =  (1, 2, 3, 4)
kwargs =  {'a': 1, 'b': 2, 'c': 3}
----------------------------------------
args =  ('a', 1, None)
kwargs =  {'a': 1, 'b': '2', 'c': 3}
----------------------------------------
  • 装饰器:用@语法糖定义和应用装饰器装饰器是一种高阶函数,可以修改其他函数的行为添加额外的功能。my_decorator是一个装饰器函数,它接受一个函数func作为参数,在原始函数执行前后添加了一些额外的操作,并返回一个新的函数wrapper。具体来说有4种类型:(真正的装饰器接受func,可能会加上外层函数接受装饰器的配置参数)

(1)装饰器需要配置,原函数需要包装。

def decorator(func):  # 外层装饰器接受func
    print('do something')
    return func  # 不包装直接返回func

# 使用 @ 语法糖应用装饰器
@decorator
def my_function():
    print("excute my func")

# 调用被装饰后的函数
my_function()

do something
excute my func

(2)装饰器需要配置,原函数需要包装。返回的wrapper是真正的装饰器函数。

def decorator(num):  # 外层函数接受配置参数num
    def wrapper(func):  # 内层wrapper才是真正的装饰器
        print('do something', num)
        return func  # 不包装直接返回func
    return wrapper

# 使用 @ 语法糖应用装饰器
@decorator(123)
def my_function():
    print("excute my func")

# 调用被装饰后的函数
my_function()

(3)装饰器需要配置,原函数需要包装。最经典应用的就是pre_processpost_process使用time.time(),计算func的执行时间。

def decorator(func):  # 外层装饰器接受func
    print('do something')
    def wrapper(*args, **kwargs):  # 包装函数func为wrapper
        print('pre_process')
        result = func(*args, **kwargs)
        print('post_process')
        return result  # 返回包装函数wrapper执行结果
    return wrapper

# 使用 @ 语法糖应用装饰器
@decorator
def my_function():
    print("excute my func")

# 调用被装饰后的函数
my_function()

(4)装饰器需要配置,原函数需要包装。

def decorator(x):  # 外层函数接受配置参数num
    def inner_dec(func):  # 内层装饰器接受func
        print("do something", x)
        def wrapper(*args, **kwargs):  # 包装函数func为wrapper
            print('pre_process')
            result = func(*args, **kwargs)
            print('post_process')
            return result
        return wrapper
    return inner_dec

# 使用 @ 语法糖应用装饰器
@decorator(123)
def my_function():
    print("excute my func")

# 调用被装饰后的函数
my_function()
  • 类装饰器:装饰器也不一定只能用函数来写,也可以使用类装饰器,用法与函数装饰器并没有太大区别,实质是使用了类方法中的__call__魔法方法来实现类的直接调用。
class logging(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[DEBUG]: enter {}()".format(self.func.__name__))
        return self.func(*args, **kwargs)

@logging
def hello(a, b, c):
    print(a, b, c)

hello("hello,","good","morning")
-----------------------------
>>>[DEBUG]: enter hello()
>>>hello, good morning

类装饰器也是可以带参数的,如下实现

class logging(object):
    def __init__(self, level):
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("[{0}]: enter {1}()".format(self.level, func.__name__))
            return func(*args, **kwargs)
        return wrapper

@logging(level="TEST")
def hello(a, b, c):
    print(a, b, c)

hello("hello,","good","morning")
-----------------------------
>>>[TEST]: enter hello()
>>>hello, good morning

2.2 Registry机制

前面我们读取到的Config实际上是一个大型的字典,仅实现了对参数的模块化解析:包含dataset的configmodel的configlr的configoptmizer的configtrain的config等。
在这里插入图片描述

但是这些都是字典参数,并没有对各个模块进行实例化,Registry要做的就是,从配置文件Config中直接解析出对应模块的信息,用Registry把模型结构与训练策略给实例化出来

在众多深度学习开源库的代码中经常出现Registry代码块,例如OpenMMlab,facebookresearch、BasicSR中都使用了注册器机制。下面以BasicSR为例,解释一下Registry:

class Registry():
    """
    The registry that provides name -> object mapping, to support third-party
    users' custom modules.
    To create a registry (e.g. a backbone registry):
    .. code-block:: python
        BACKBONE_REGISTRY = Registry('BACKBONE')
    To register an object:
    .. code-block:: python
        @BACKBONE_REGISTRY.register()
        class MyBackbone():
            ...
    Or:
    .. code-block:: python
        BACKBONE_REGISTRY.register(MyBackbone)
    """

    def __init__(self, name):
        """
        Args:
            name (str): the name of this registry
        """
        self._name = name
        self._obj_map = {}

    def _do_register(self, name, obj, suffix=None):
        if isinstance(suffix, str):
            name = name + '_' + suffix

        assert (name not in self._obj_map), (f"An object named '{name}' was already registered "
                                             f"in '{self._name}' registry!")
        self._obj_map[name] = obj

    def register(self, obj=None, suffix=None):
        """
        Register the given object under the the name `obj.__name__`.
        Can be used as either a decorator or not.
        See docstring of this class for usage.
        """
        if obj is None:
            # used as a decorator
            def deco(func_or_class):
                name = func_or_class.__name__
                self._do_register(name, func_or_class, suffix)
                return func_or_class

            return deco

        # used as a function call
        name = obj.__name__
        self._do_register(name, obj, suffix)

    def get(self, name, suffix='basicsr'):
        ret = self._obj_map.get(name)
        if ret is None:
            ret = self._obj_map.get(name + '_' + suffix)
            print(f'Name {name} is not found, use name: {name}_{suffix}!')
        if ret is None:
            raise KeyError(f"No object named '{name}' found in '{self._name}' registry!")
        return ret

    def __contains__(self, name):
        return name in self._obj_map

    def __iter__(self):
        return iter(self._obj_map.items())

    def keys(self):
        return self._obj_map.keys()


DATASET_REGISTRY = Registry('dataset')
ARCH_REGISTRY = Registry('arch')
MODEL_REGISTRY = Registry('model')
LOSS_REGISTRY = Registry('loss')
METRIC_REGISTRY = Registry('metric')

上面的代码为数据集,架构,网络,损失以及度量方式都创建了一个注册器对象。核心代码在register函数里,register函数使用了装饰器的设计,也就是只要在功能模块前进行@xx.register()进行装饰,就会对原有功能模块进行注册,并且最终返回原始的功能模块,不修改其原有功能。

在更下层的_do_register()中可以看到,这里使用的是一个字典来执行注册操作,记录的键值对分别是模块的名称以及模块本身。这样一来,读取配置文件中的模块字符串后,我们就能够直接通过函数名或者类名找到其具体实现。

使用方法如下所示,只需要在此类前加上装饰,后期则直接能够从字符串L1Loss找到其对应的实现。

@LOSS_REGISTRY.register()
class L1Loss(nn.Module):
    """L1 (mean absolute error, MAE) loss.
    Args:
        loss_weight (float): Loss weight for L1 loss. Default: 1.0.
        reduction (str): Specifies the reduction to apply to the output.
            Supported choices are 'none' | 'mean' | 'sum'. Default: 'mean'.
    """

    def __init__(self, loss_weight=1.0, reduction='mean'):
        super(L1Loss, self).__init__()
        if reduction not in ['none', 'mean', 'sum']:
            raise ValueError(f'Unsupported reduction mode: {reduction}. Supported ones are: {_reduction_modes}')

        self.loss_weight = loss_weight
        self.reduction = reduction

    def forward(self, pred, target, weight=None, **kwargs):
        """
        Args:
            pred (Tensor): of shape (N, C, H, W). Predicted tensor.
            target (Tensor): of shape (N, C, H, W). Ground truth tensor.
            weight (Tensor, optional): of shape (N, C, H, W). Element-wise weights. Default: None.
        """
        return self.loss_weight * l1_loss(pred, target, weight, reduction=self.reduction)

3. Hook

推荐Pytorch_linghtning,对于训练的封装。(mmcv的Runner也类似)

3.1 钩子编程

hook允许你在特定的代码点插入自定义的代码。通过使用钩子(hooks),你可以在程序执行到特定的位置时注入自己的代码以便进行额外的处理或修改程序的行为

如下面的例子,正常的git commit添加pre-commit-hook后,就会在git commit前执行一些检查操作(文件大小是否合格等):

在这里插入图片描述
但是随着需求不断增加,插入的代码也越来越乱,相比于直接修改原始代码这种侵入式的修改,我们需要一种非侵入式的修改,使得hook加入的更加清晰直观。如下,直接在forward中添加打印模型结构和参数的代码。
在这里插入图片描述
在实际操作中,我们常常在函数执行的前后注册hook函数,实现非侵入式的修改。如pytorch的nn.Module的forward底层是__call__方法,它在执行forward之前会执行_forward_pre_hooks,在执行forward之后会执行_forward_hooks
在这里插入图片描述

3.2 Pytorch_Lightning hook介绍

在这里插入图片描述

下面PL模型的实现可以在fit(train + validate), validate, test, predict每个epoch每个batch前后添加hook函数:如setupon_xxx_epoch_endon_xxx_batch_end等(end函数一般用来作为loss和acc的log hook)。

class LitModel(pl.LightningModule):
    def __init__(...):
    # init: 初始化,包括模型和系统的定义。
    def prepare_data(...):
    # 准备数据,包括下载数据、预处理等等
    def setup(...):
    # 执行fit(train + validate), validate, test, or predict前的hook function,进行数据划分等操作
    def configure_optimizers(...)
	# configure_optimizers: 优化器定义,返回一个优化器,或数个优化器,或两个List(优化器,Scheduler)
	
    def forward(...):
    # forward: 前向传播,和正常的Ptorch的forward一样
    
    def train_dataloader(...)
    # 加载train data
    def training_step(...)
	# training_step(self, batch, batch_idx): 即每个batch的处理函数, z=self(x)等价于z=forward(x)
    def on_train_epoch_end(...)
	# training epoch end hook function
	
	def validation_dataloader(...)
    # 加载validationdata
    def validation_step(...)
	# validation_step(self, batch, batch_idx): 即每个batch的处理函数
    def on_validation_epoch_end(...)
	# validation epoch end hook function

    def test_dataloader(...)
    # 加载testdata
    def test_step(...)
	# test_step(self, batch, batch_idx): 即每个batch的处理函数
    def on_test_epoch_end(...)
	# test epoch end hook function

    def any_extra_hook(...)

上面介绍的PL的hook函数只是比较常用的,更多更全的PL ho
ok介绍可以在官网中查看:https://lightning.ai/docs/pytorch/stable/_modules/lightning/pytorch/core/hooks.html

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1268996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Cytoscape软件下载、安装、插件学习[基础教程]

写在前面 今天分享的内容是自己遇到问题后&#xff0c;咨询社群里面的同学&#xff0c;帮忙解决的总结。 关于Cytoscape&#xff0c;对于做组学或生物信息学的同学基本是陌生的&#xff0c;可能有的同学用这个软件作图是非常溜的&#xff0c;做出来的网络图也是十分的好看&am…

Golang中rune和Byte,字符和字符串有什么不一样

Rune和Byte&#xff0c;字符和字符串有什么不一样 String Go语言中&#xff0c; string 就是只读的采用 utf8 编码的字节切片(slice) 因此用 len 函数获取到的长度并不是字符个数&#xff0c;而是字节个数。 for循环遍历输出的也是各个字节。 Rune rune 是 int32 …

Google分析中的基础概念

当提到Google分析时&#xff0c;我们通常指的是一种用于跟踪和分析网站和应用程序数据的工具。在使用Google分析之前&#xff0c;了解其基础概念对于正确配置和有效使用该工具非常重要。 1、帐户&#xff08;Account&#xff09;&#xff1a;帐户是Google分析中的最高层级。一…

Linux系统之uptime命令的基本使用

Linux系统之uptime命令的基本使用 一、uptime介绍二、uptime命令使用帮助2.1 uptime的help帮助信息2.2 uptime的语法解释 三、uptime的基本使用3.1 直接使用uptime命令3.2 显示uptime版本信息3.3 显示系统运行时间3.4 显示系统最后一次启动时间 四、uptime命令的使用注意事项 一…

案例,linux环境下OpenCV+Java,实现证件照在线更换背景色

先看效果&#xff08;图片来自网络&#xff0c;如有侵权&#xff0c;请联系作者删除&#xff09; 主要是通过java实现的&#xff0c;linux环境编译安装opencv及证件照背景色更换的核心算法在前面一篇文章中有写到。 目前算法还有瞎呲&#xff0c;当照片光线不均的时候会出现误…

Spring---对象的存储和读取

文章目录 Spring对象的存储创建Bean对象将Bean对象存储到spring中添加配置文件存储Bean对象 Spring对象的读取得到Spring上下文对象从Spring中取出Bean对象使用Bean对象 Spring对象的存储 创建Bean对象 Bean对象其实就是一个普通的Java对象。我们按照创建Java对象的方式来创建…

独家揭秘!8种平面设计类型,你都了解吗?

当我们谈起平面设计时&#xff0c;大部分人可能会误以为平面设计只局限于处理二维&#xff08;2D&#xff09;元素&#xff0c;例如设计logo或海报等。这实际上是一个普遍的误解。事实上&#xff0c;平面设计的定义和应用范围要远远超越这个简单的概念。它更多的是采用各种平面…

【代码】考虑灵活性供需平衡的电力系统优化调度模型

程序名称&#xff1a;考虑灵活性供需平衡的电力系统优化调度模型 实现平台&#xff1a;matlab-yalmip-cplex/gurobi 代码简介&#xff1a;最可再生能源发电设备和并网技术快速发展&#xff0c;以新能源为主导的新型电力系统逐步形成。高比例新能源的随机波动性导致电力系统的…

物联网开发(一)新版Onenet 基础配置

onenet新创建的账号&#xff0c;没有了多协议接入&#xff0c;只有新的物联网开放平台 第一讲&#xff0c;先给大家讲一下&#xff1a;新版Onenet 基础配置 创建产品 产品开发-->创建产品 产品的品类选择个&#xff1a;大致符合你项目的即可&#xff0c;没有影响 选择智…

智能客服核心技术——预测会话与答案生成

1.信息检索 2. 句型模板匹配标准问题生成答案 3.根据知识图谱推理得到答案

模拟算法【1】

文章目录 &#x1f600;1576. 替换所有的问号&#x1f606;题目&#x1f929;算法原理&#x1f642;代码实现 &#x1f60a;495.提莫攻击&#x1fae0;题目&#x1f609;算法原理&#x1f917;代码实现 模拟算法 通俗的来说&#xff0c;模拟算法就是依葫芦画瓢&#xff0c;将题…

网络运维与网络安全 学习笔记2023.11.29

网络运维与网络安全 学习笔记 第三十天 今日更新太晚啦&#xff01;&#xff01;&#xff01; 主要是今天工作时挨了一天骂&#xff0c;服了&#xff0c;下次记得骂的轻一点&#xff01;&#xff01;&#xff01; &#xff08;要不是为了那点微薄的薪资&#xff0c;谁愿意听你…

MySQL进阶知识:三

前言 未更新完毕&#xff01;大概明天更完&#xff01; 锁 MySQL中的锁&#xff0c;按照锁的粒度分&#xff0c;分为以下三类 全局锁&#xff1a;锁定数据库中的所有表。表级锁&#xff1a;每次操作锁住整张表。行级锁&#xff1a;每次操作锁住对应的行数据。 全局锁 全局…

Spark-java版

SparkContext初始化 相关知识 SparkConf 是SparkContext的构造参数&#xff0c;储存着Spark相关的配置信息&#xff0c;且必须指定Master(比如Local)和AppName&#xff08;应用名称&#xff09;&#xff0c;否则会抛出异常&#xff1b;SparkContext 是程序执行的入口&#xf…

基于Halcon的二维码姿态矫正

任务要求&#xff1a; 下图中的二维码进行校正。&#xff08;HALCON10.0自带例图&#xff0c;路径&#xff1a;“images/datacode/ecc200/ecc200_to_preprocess_001.png”&#xff09; 任务分析&#xff1a; 图中的二维码存在畸变&#xff0c;需对其进行透射变换。首先获得图…

Java EE 多线程

文章目录 1. 认识线程1.1 什么是进程1.2 什么是线程1.2.1. 线程是怎么做到的呢&#xff1f;1.2.2. 进程和线程的关系 1.3 多线程编程1.3.1. 第一个多线程程序1.3.2. 使用 jconsole 命令查看线程1.3.3. 实现 Runnable 接口&#xff0c;重写 run1.3.4. 继承 Thread 重写 run&…

机器学习:领域自适应学习

训练一个分类器是小问题 上难度 训练数据和测试数据不一致&#xff0c;比如训练数据是黑白的&#xff0c;测试时彩色的&#xff0c;结果准确率非常低。 训练数据和测试数据有点差距的时候&#xff0c;能不能效果也能好呢&#xff1f;这就用到了领域自使用domain adptation 用一…

Hive数据倾斜之:数据类型不一致导致的笛卡尔积

Hive数据倾斜之&#xff1a;数据类型不一致导致的笛卡尔积 目录 Hive数据倾斜之&#xff1a;数据类型不一致导致的笛卡尔积一、问题描述二、原因分析三、精度损失四、问题解决 一、问题描述 如果两张表的jion&#xff0c;关联键分布较均匀&#xff0c;没有明显的热点问题&…

【Android Jetpack】Room数据库

文章目录 引入EntitiesPrimary Key主键索引和唯一性对象之间的关系外键获取关联的Entity对象嵌套对象Data Access Objects&#xff08;DAOs&#xff09;使用Query注解的方法简单的查询带参数查询返回列的子集可被观察的查询 数据库迁移用法 引入 原始的SQLite有以下两个缺点: …

uniapp2023年微信小程序头像+昵称分别获取

1、DOM <view class"m-user"><view class"user-info"><!--头像 GO--><button class"avatar avatar-wrapper" open-type"chooseAvatar" chooseavatar"onChooseAvatar"slot"right"><im…