pytorch优化器——add_param_group()介绍及示例、Yolov7 优化器代码示例

news2024/9/28 1:25:37

系列文章目录

基础函数2——enumerate()、hasattr()、isinstance()
pytorch学习率设置——optimizer.param_groups、对不同层设置学习率、动态调整学习率。


文章目录

  • 系列文章目录
  • 前言
    • 1、关于pytorch优化器
    • 2、add_param_group()
    • 3、pytorch优化器
    • 4、pytorch优化器测试总代码
    • 5、Yolov7 优化器代码示例
  • 总结


前言

最近遇到了一个关于优化器的bug,困扰了我很多天,然后自己系统学习了以下pytorch优化器的知识,总结记录如下。


1、关于pytorch优化器

PyTorch优化器是一个用于优化神经网络模型的工具,它的作用是根据损失函数和模型参数来更新模型的参数,从而使模型的性能得到优化。PyTorch提供了多种优化器,包括SGD、Adam、Adagrad等。

PyTorch优化器的原理是通过反向传播算法计算损失函数对每个模型参数的梯度,然后根据梯度大小和学习率来更新模型参数。具体来说,优化器会根据一定的策略计算出一个梯度下降的方向,并根据这个方向对模型的参数进行调整。优化器的目标是使损失函数最小化,从而提高模型的性能。

在使用PyTorch优化器时,需要设置优化器的超参数,包括学习率、动量、权重衰减等。这些超参数的设置会影响优化器的性能和结果,因此需要根据具体的模型和数据集进行调整和优化。

总之,PyTorch优化器是神经网络模型优化的重要工具,它通过反向传播算法计算梯度,并根据一定的策略更新模型参数,从而使模型性能得到优化。

PyTorch是一种开源机器学习框架,提供了多种优化器来优化模型。以下是一些常用的优化器:

SGD:随机梯度下降优化器,是最基本的优化器之一,它计算每个样本的梯度并根据学习率更新模型参数。

Adam:自适应矩估计优化器,是一种自适应学习率优化器,它根据每个参数的梯度和梯度平方的移动平均值来计算自适应学习率。

Adagrad:自适应梯度算法优化器,是一种自适应学习率优化器,它根据每个参数的梯度平方和历史梯度的平方和来计算自适应学习率。

Adadelta:自适应学习率算法优化器,是一种自适应学习率优化器,它根据每个参数的梯度平方和历史梯度平方的平均值来计算自适应学习率。

RMSprop:均方根传播优化器,是一种自适应学习率优化器,它根据每个参数的梯度平方和历史梯度平方的移动平均值来计算自适应学习率。

这些优化器都可以在PyTorch中使用,并且可以通过设置各种参数来进行调整和优化。

2、add_param_group()

add_param_group()是PyTorch中的一个方法,它允许用户向优化器添加一个新的参数组。参数组是一个字典,描述了特定模型参数集的参数、超参数和优化选项。当我们想要为神经网络中的不同层或参数集使用不同的学习率、权重衰减或动量时,它很有用。

add_param_group()方法接受一个字典作为输入,该字典应包含以下键:
params:需要优化的参数张量列表。
lr:参数组的学习率。
weight_decay:参数组的权重衰减值。
momentum:参数组的动量因子。
dampening:参数组动量修正的衰减。
nesterov:是否使用nesterov动量作为参数组。

可以多次调用此方法以向优化器添加不同的参数组。
示例:

import torch.optim as optim

optimizer = optim.SGD(model.parameters(), lr=0.1)

# add a new parameter group with a different learning rate and weight decay
optimizer.add_param_group({'params': model.fc.parameters(), 'lr': 0.01, 'weight_decay': 0.001})

# add another parameter group with a different momentum
optimizer.add_param_group({'params': model.conv.parameters(), 'lr': 0.1, 'momentum': 0.9})

测试:

import torch
import torch.optim as optim

# ----------------------------------- add_param_group
w1 = torch.randn(2, 2)
w1.requires_grad = True

w2 = torch.randn(2, 2)
w2.requires_grad = True

w3 = torch.randn(2, 2)
w3.requires_grad = True

# 一个参数组
optimizer_1 = optim.SGD([w1, w2], lr=0.1)
print('当前参数组个数: ', len(optimizer_1.param_groups))
print(optimizer_1.param_groups, '\n')

# 增加一个参数组
print('增加一组参数 w3\n')
optimizer_1.add_param_group({'params': w3, 'lr': 0.001, 'momentum': 0.8})

print('当前参数组个数: ', len(optimizer_1.param_groups))
print(optimizer_1.param_groups, '\n')

Yolov7 调用示例:
在这里插入图片描述

3、pytorch优化器

以下函数用到了这几个函数。
hasattr() 函数用于判断对象是否包含对应的属性。
isinstance()检查对象是否是指定的类型。
append() 向列表末尾添加元素
详细介绍转:基础函数2——enumerate()、hasattr()、isinstance()

# 构造损失函数和优化函数
# 损失
criterion = torch.nn.CrossEntropyLoss()
pg0, pg1, pg2 ,pg3= [], [], [], [] 
for name, p in model.named_modules():
    if hasattr(p, "bias") and isinstance(p.bias, nn.Parameter):  # 把带有bias属性且性质为nn.Parameter的层选出来 添加到到pg2列表
        pg2.append(p.bias) 
    if isinstance(p, nn.BatchNorm2d) or "bn" in name: # 把标准化层选出来 添加到到pg0列表
        pg0.append(p.weight)
    elif hasattr(p, "weight") and isinstance(p.weight, nn.Parameter):  # 把带有weight属性且性质为nn.Parameter的层选出来 添加到到pg1列表
        pg1.append(p.weight)
        #print('22',name,p)  # print打印出来 调试用
        
optimizer = torch.optim.SGD(pg0, lr=0.1, momentum=0.5)  #初始化优化器,定义一个参数组
optimizer.add_param_group({"params": pg1})    # 增加一组参数 性质与pg0一样
optimizer.add_param_group({"params": pg2})    # 增加一组参数 性质与pg0一样
optimizer.add_param_group({"params": model.w,'lr': 0.12, 'momentum': 0.8}) # 这个是我网络中定义的自学习权重参数 

可以看到,参数组是一个list,一个元素是一个dict,每个dict中都有lr, momentum等参数,这些都是可单独管理,单独设定。
train函数调用

		outputs = model(inputs)              #代入模型
        loss = criterion(outputs, target)    #计算损失值
        loss.backward()                      #反向传播计算得到每个参数的梯度值
        optimizer.step()                     #梯度下降参数更新
        optimizer.zero_grad()                #将梯度归零

在这里插入图片描述

4、pytorch优化器测试总代码

代码是以resnet18分类手写数字体识别mini数据集为例。

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import os  # 添加代码①
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"  # 添加代码②

batch_size = 256    #设置batch大小
transform = transforms.Compose([
    transforms.ToTensor(),                        #转换为张量
    transforms.Normalize((0.1307,), (0.3081,))    #设定标准化值
])
#训练集
train_dataset = datasets.MNIST(
    root='../data/mnist',
    train=True,
    transform=transform,
    download=True)
#测试集
test_dataset = datasets.MNIST(
    root='../data/mnist',
    train=False,
    transform=transform,
    download=True)
 
#训练集加载器
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size,shuffle=True)
#测试集加载器
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size, shuffle=False)

class resnet18(torch.nn.Module):
    def __init__(self):
        super(resnet18, self).__init__()
        self.block1 = torch.nn.Sequential(
            torch.nn.Conv2d(1, 10, 5),
            torch.nn.MaxPool2d(2),
            torch.nn.ReLU(True),
            torch.nn.BatchNorm2d(10),
        )
        self.block2 = torch.nn.Sequential(
            torch.nn.Conv2d(10, 20, 5),
            torch.nn.MaxPool2d(2),
            torch.nn.ReLU(True),
            torch.nn.BatchNorm2d(20),
        )
        self.fc = torch.nn.Sequential(
            torch.nn.Flatten(),
            torch.nn.Linear(320, 10)
        )
        self.w = torch.nn.Parameter(torch.ones(2)) # 定义自学习参数
        
    def forward(self, x):
        x = self.block1(x)*(self.w[0])
        x = self.block2(x)*(self.w[1])
        x = self.fc(x)
        return x
    
model = resnet18()
device=torch.device("cuda:0"if torch.cuda.is_available()else"cpu")#使用GPU进行计算
model.to(device)#把model模型放进去
#---------------------------------------------------------------------# 
# 构造损失函数和优化函数
# 损失
criterion = torch.nn.CrossEntropyLoss()
pg0, pg1, pg2 ,pg3= [], [], [], [] 
for name, p in model.named_modules():
    if hasattr(p, "bias") and isinstance(p.bias, nn.Parameter):  # 把带有bias属性且性质为nn.Parameter的层选出来 添加到到pg2列表
        pg2.append(p.bias) 
    if isinstance(p, nn.BatchNorm2d) or "bn" in name: # 把标准化层选出来 添加到到pg0列表
        pg0.append(p.weight)
    elif hasattr(p, "weight") and isinstance(p.weight, nn.Parameter):  # 把带有weight属性且性质为nn.Parameter的层选出来 添加到到pg1列表
        pg1.append(p.weight)
        #print('22',name,p)  # print打印出来 调试用
        
# hasattr() 函数用于判断对象是否包含对应的属性。
# isinstance()检查对象是否是指定的类型。
# append() 向列表末尾添加元素

optimizer = torch.optim.SGD(pg0, lr=0.1, momentum=0.5)  #初始化优化器,定义一个参数组
optimizer.add_param_group({"params": pg1})    # 增加一组参数 性质与pg0一样
optimizer.add_param_group({"params": pg2})    # 增加一组参数 性质与pg0一样
optimizer.add_param_group({"params": model.w,'lr': 0.12, 'momentum': 0.8}) # 这个是我网络中定义的自学习权重参数 
# 可以看到,参数组是一个list,一个元素是一个dict,每个dict中都有lr, momentum等参数,这些都是可单独管理,单独设定。
def train(epoch):
    
#     adjust_learning_rate(optimizer, epoch, start_lr)  # 动态调整学习率
#     print("Lr:{}".format(optimizer.state_dict()['param_groups'][0]['lr']))  # 查看学习率
#     print("Lr:{}".format(optimizer.state_dict()['param_groups'][1]['lr']))
#     print("Lr:{}".format(optimizer.state_dict()['param_groups'][2]['lr']))
#     print(optimizer.state_dict()["param_groups"])  # 查看优化器完整参数

    running_loss = 0.0        #每一轮训练重新记录损失值
    for batch_idx, data in enumerate(train_loader, 0):    #提取训练集中每一个样本
        inputs, target = data        
        inputs, target = inputs.to(device), target.to(device)  # 这里的数据(原数据)也要迁移过去
        # outputs输出为0-9的概率  256*10
        outputs = model(inputs)              #代入模型
        loss = criterion(outputs, target)    #计算损失值
        loss.backward()                      #反向传播计算得到每个参数的梯度值
        optimizer.step()                     #梯度下降参数更新
        optimizer.zero_grad()                #将梯度归零
        running_loss += loss.item()          #损失值累加
    
        if batch_idx % 300 == 299:           #每300个样本输出一下结果
            print('[%d,%5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
            running_loss = 0.0          # (训练轮次,  该轮的样本次,  平均损失值)
    
    return running_loss


 
def test():
    correct = 0
    total = 0
    with torch.no_grad():            #执行计算,但不希望在反向传播中被记录
        for data in test_loader:     #提取测试集中每一个样本
            images, labels = data
            images, labels = images.to(device), labels.to(device)
            # outputs输出为0-9的概率  256*10
            outputs = model(images)  #带入模型
            # torch.max()这个函数返回的是两个值,第一个值是具体的value(我们用下划线_表示)
            # 第二个值是value所在的index(也就是predicted)
            _, pred = torch.max(outputs.data, dim=1)    #获得结果中的最大值
            total += labels.size(0)                     #测试数++
            correct += (pred == labels).sum().item()    #将预测结果pred与标签labels对比,相同则正确数++
        print('%d %%' % (100 * correct / total))    #输出正确率
        
        
        
if __name__ == '__main__':
    # 这两个数组主要是为了画图
    lossy = []        #定义存放纵轴数据(损失值)的列表
    epochx = []       #定义存放横轴数据(训练轮数)的列表
    
    for epoch in range(10):    #训练10轮
        epochx.append(epoch)   #将本轮轮次存入epochy列表
        lossy.append(train(epoch))  #执行训练,将返回值loss存入lossy列表  
        test()                 #每轮训练完都测试一下正确率
    path = "D:/code/text/model2.pth"
    #torch.save(model,path)
    torch.save(model.state_dict(),path)   # 保存模型
    model = torch.load("D:/code/text/model2.pth")  # 加载模型
 
    #可视化一下训练过程
    plt.plot(epochx, lossy)
    plt.grid()
    plt.show()

5、Yolov7 优化器代码示例

#------------------------------------------------------------------#
#   optimizer_type  使用到的优化器种类,可选的有adam、sgd
#                   当使用Adam优化器时建议设置  Init_lr=1e-3
#                   当使用SGD优化器时建议设置   Init_lr=1e-2
#   momentum        优化器内部使用到的momentum参数
#   weight_decay    权值衰减,可防止过拟合
#                   adam会导致weight_decay错误,使用adam时建议设置为0
#------------------------------------------------------------------#
optimizer_type      = "sgd"
momentum            = 0.937
weight_decay        = 5e-4
lr_decay_type       = "cos" # 使用到的学习率下降方式,可选的有step、cos
UnFreeze_Epoch      = 10    #  eporch
nbs             = 64
lr_limit_max    = 1e-3 if optimizer_type == 'adam' else 5e-2
lr_limit_min    = 3e-4 if optimizer_type == 'adam' else 5e-4
Init_lr_fit     = min(max(batch_size / nbs * Init_lr, lr_limit_min), lr_limit_max)
Min_lr_fit      = min(max(batch_size / nbs * Min_lr, lr_limit_min * 1e-2), lr_limit_max * 1e-2)
                
# 学习率下降公式函数
def get_lr_scheduler(lr_decay_type, lr, min_lr, total_iters, warmup_iters_ratio = 0.05, warmup_lr_ratio = 0.1, no_aug_iter_ratio = 0.05, step_num = 10):
    def yolox_warm_cos_lr(lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter, iters):
        if iters <= warmup_total_iters:
            # lr = (lr - warmup_lr_start) * iters / float(warmup_total_iters) + warmup_lr_start
            lr = (lr - warmup_lr_start) * pow(iters / float(warmup_total_iters), 2
            ) + warmup_lr_start
        elif iters >= total_iters - no_aug_iter:
            lr = min_lr
        else:
            lr = min_lr + 0.5 * (lr - min_lr) * (
                1.0
                + math.cos(
                    math.pi
                    * (iters - warmup_total_iters)
                    / (total_iters - warmup_total_iters - no_aug_iter)
                )
            )
        return lr
 
 # 动态调整学习率函数
def set_optimizer_lr(optimizer, lr_scheduler_func, epoch):
    lr = lr_scheduler_func(epoch)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr
        
#---------------------------------------#
#   根据optimizer_type选择优化器
#---------------------------------------#
pg0, pg1, pg2 = [], [], []  
for k, v in model.named_modules():
    if hasattr(v, "bias") and isinstance(v.bias, nn.Parameter):
        pg2.append(v.bias)    
    if isinstance(v, nn.BatchNorm2d) or "bn" in k:
        pg0.append(v.weight)    
    elif hasattr(v, "weight") and isinstance(v.weight, nn.Parameter):
        pg1.append(v.weight)

optimizer = {
     'adam'  : optim.Adam(pg0, Init_lr_fit, betas = (momentum, 0.999)),
     'sgd'   : optim.SGD(pg0, Init_lr_fit, momentum = momentum, nesterov=True)
    }[optimizer_type]
optimizer.add_param_group({"params": pg1, "weight_decay": weight_decay})
optimizer.add_param_group({"params": pg2})

# 以下两行代码不是初始化代码,放eporch for循环中 ,每个eporch执行一次
lr_scheduler_func = get_lr_scheduler(lr_decay_type, Init_lr_fit, Min_lr_fit, UnFreeze_Epoch)
# 学习率调整
set_optimizer_lr(optimizer, lr_scheduler_func, epoch)  # 根据迭代epoch更新学习率

这段代码参考了http://t.csdn.cn/cVhoQ


总结

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

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

相关文章

如何保证 RabbitMQ 的消息可靠性

前言 项目开发中经常会使用消息队列来完成异步处理、应用解耦、流量控制等功能。虽然消息队列的出现解决了一些场景下的问题&#xff0c;但是同时也引出了一些问题&#xff0c;其中使用消息队列时如何保证消息的可靠性就是一个常见的问题。如果在项目中遇到需要保证消息一定被…

大数据技术之Hadoop-入门

第1章 Hadoop概述 1.1 Hadoop是什么 分布式&#xff1a;多台服务器共同完成某一项任务。 1.2 Hadoop发展历史 1.3 Hadoop三大发行版本 Hadoop三大发行版本&#xff1a;Apache、Cloudera、Hortonworks。 Apache版本最原始&#xff08;最基础&#xff09;的版本&#xff0c…

【三十天精通Vue 3】第二十六天 Vue3 与 TypeScript 最佳实践

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 引言一、为什么使用TypeScript&#xff1f;二、Vue 3和TypeScript…

Golang每日一练(leetDay0055)

目录 159.至多包含两个不同字符的最长子串 Longest-substring-with-at-most-two-distinct-characters &#x1f31f;&#x1f31f; 160. 相交链表 Intersection-of-two-linked-lists &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 …

Stable Diffusion +ChatGPT+文本转语音+剪映制作视频

目录 chatgpt免费体验入口网址 模型下载 huggingface.co civitai.com 使用Deliberate模型案例 StableDeffusion做的图片&#xff0c;chatGPT出的文案&#xff0c;微软文本转语音配的音&#xff0c;使用剪映做的视频 chatgpt免费体验入口网址 http://chat.xutongbao.top …

【Java数据结构】顺序表、队列、栈、链表、哈希表

顺序表 定义 存放数据使用数组但是可以编写一些额外的操作来强化为线性表&#xff0c;底层依然采用顺序存储实现的线性表&#xff0c;称为顺序表 代码实现 创建类型 先定义一个新的类型 public class ArrayList<E> {int capacity 10; //顺序表的最大容量int size …

UNIX环境高级编程——信号

10.1 引言 信号是软件中断&#xff1b;信号提供了一种处理异步事件的方法。 10.2 信号概念 每个信号都有一个名字&#xff0c;这些名字都以3个字符SIG开头&#xff1b;在头文件<signal.h>中&#xff0c;信号名都被定义为正整数常量&#xff08;信号编号&#xff09;&a…

架构设计-高性能篇

大家好&#xff0c;我是易安&#xff01;今天我们谈一谈架构设计中的高性能架构涉及到的底层思想。本文分为缓存架构&#xff0c;单服务器高性能模型&#xff0c;集群下的高性能模型三个部分&#xff0c;内容很干&#xff0c;希望你仔细阅读。 高性能缓存架构 在某些复杂的业务…

代码审计笔记之java多环境变量设置

在做java代码审计时&#xff0c;为了要成功运行目标环境&#xff0c;时长要对于jdk版进行切换&#xff0c;且在装多个jdk时还时长会遇到安装配置后环境变量不生效的情况&#xff0c;下文介绍&#xff1b; 1、为什么安装了新的jdk&#xff0c;有的时候环境变量中的jdk版本确还是…

如何设计出好的测试用例?

软件测试培训之如何设计出好的测试用例? 一句话概括&#xff1a;对被测软件的需求有深入的理解。 深入理解被测软件需求的最好方法是&#xff0c;测试工程师在需求分析和设计阶段就开始介入&#xff0c;因为这个阶段是理解和掌握软件的原始业务需求的最好时机。 只有真正理解了…

【VAR模型 | 时间序列】帮助文档:VAR模型的引入和Python实践(含源代码)

向量自回归 (VAR) 是一种随机过程模型&#xff0c;用于捕获多个时间序列之间的线性相互依赖性。 VAR 模型通过允许多个进化变量来概括单变量自回归模型&#xff08;AR 模型&#xff09;。 VAR 中的所有变量都以相同的方式进入模型&#xff1a;每个变量都有一个方程式&#xff…

轻松掌握在已有K8s环境上安装KubeSphere

官方文档地址&#xff1a;https://kubesphere.io/zh/docs/v3.3/quick-start/minimal-kubesphere-on-k8s/ 1、基于已有K8s环境上安装KubeSphere 1、前置环境 1、安装nfs及动态存储类PV/PVC 安装默认存储类型&#xff0c;这里使用nfs&#xff0c;关于nfs的安装在PV/PVC的文章…

出道的第八年,依然AI着......

今天&#xff0c;是数说故事8周岁的生日 8年&#xff0c;和您一起走过2,922天 8年&#xff0c;我们对AI的探索从未停止 8年&#xff0c;我们将数据的热爱进行到底 因为热“AI” 我们与您的故事有了连接 8年的连接&#xff0c;我们与您也擦出了无数花火 我们将每一个闪烁的…

Optional参数类使用

目录 介绍 使用 常用方法 是否为空 对象比较 Optional 是一个对象容器&#xff0c;具有以下两个特点&#xff1a; 使用 1. 创建 2. 获取&#xff1a; 3. 判断&#xff1a; 4. 过滤&#xff1a; 5. 映射&#xff1a; 介绍 在使用值判断的时候使用方便 使用 import j…

linux系统TP-ti,tsc2046外设调试

一、整体调试思路 tp外设属于比较常见且比较简单的外设&#xff0c;今天以ti,tsc2046这款为例简述下tp外设的调试。 整体思路 1、配置设备树----驱动调试的device部分 2、tp驱动编译及匹配—driver部分 3、驱动整体调试 二、配置设备树 对于ti,tsc2046我们可以参考内核Docum…

复杂美科技多项区块链技术产品被纳入《2021-2022区块链产业图谱》区块链蓝皮书

2022年9月3日&#xff0c;由中国社会科学院社会科学文献出版社、北京金融科技产业联盟指导&#xff0c;北京区块链技术应用协会&#xff08;BBAA&#xff09;主办的 “Web 3.0发展趋势高峰论坛暨2022元宇宙、区块链、金融科技蓝皮书发布会” 在服贸会上成功举办。 大会隆重发布…

身份鉴别解读与技术实现分析(1)

6.1.4.1 身份鉴别 本项要求包括: a) 应对登录的用户进行身份标识和鉴别,身份标识具有唯一性,身份鉴别信息具有复杂度要求并定期更换; b) 应具有登录失败处理功能,应配置并启用结束会话、限制非法登录次数和当登录连接超时自动退出等相关措施 在等级保护体系中,级别越高…

数字时代下网络安全的重要性

在数字时代&#xff0c;网络安全比以往任何时候都更加重要。 随着我们越来越依赖技术来存储和传输敏感信息&#xff0c;网络攻击的风险也在增加。网络攻击可能来自世界任何地方&#xff0c;对个人和企业都可能是毁灭性的。 AkamaiTechnologies首席安全官BoazGelbord在最近的一…

【YOLO系列】YOLOv7论文超详细解读(翻译 +学习笔记)

前言 终于读到传说中的YOLOv7了~≖‿≖✧ 这篇是在美团的v6出来不到一个月就高调登场&#xff0c;作者还是我们熟悉的AB大神&#xff08;对&#xff0c;就是v4那个&#xff09;&#xff0c;读起来又是“熟悉”的感觉&#xff08;贯穿了我的整个五一假期&#xff08;╯&#x…

Vue组件设计-多列表拖拽交换排序

在前端开发中&#xff0c;拖拽排序是一种提升用户体验非常好的方式&#xff0c;常见的场景有单列表拖拽排序&#xff0c;多列表拖拽交换排序&#xff0c;比如以下这种效果&#xff1a; 下面将以这种效果为例&#xff0c;设计一个组件。 1. 安装所需依赖 npm install vuedragg…