入门pytorch-联邦学习

本文联邦学习的代码引用于https://github.com/shaoxiongji/federated-learning

本篇文章相当于带大家读一遍联邦学习的代码,同时加深了大家对联邦学习Pytorch框架的理解。

这里想简单介绍一下联邦学习。

联邦学习说白了,就是假如有 N N N个数据拥有者 F 1 , . . . , F N {F_1,...,F_N} F1,...,FN,他们希望使用这些数据来训练机器学习模型,但是又各自想隐藏自己的数据不被别人所知道(隐私保护),这个过程每个用户传递本地模型参数到中心服务器训练模型 M F E D M_{FED} MFED,该过程中任何数据拥有者 F i F_i Fi都不会暴露其数据 D i D_i Di给其他人。而传统的方法将所有的数据放到一起(中心服务器)并使用 D = D 1 ∪ D 2 . . . D N D=D_1 \cup D_2...D_N D=D1D2...DN训练模型 M S U M M_{SUM} MSUM,但是在传统方法的过程中,中心服务器会得知所有用户的数据,故有了联邦学习这个概念,并由此衍生出了针对联邦学习的攻击与防御等。

在这里,我们对比模型 M F E D M_{FED} MFED和模型 M S U M M_{SUM} MSUM的精度 V F E D V_{FED} VFED V S U M V_{SUM} VSUM应该非常接近,如果其精度有了损失,可能会因隐私保护而得不偿失了,下面的 δ \delta δ是联邦学习算法的精度值损失 ∣ V F E D − V S U M ∣ < δ |V_{FED}-V_{SUM}|<\delta VFEDVSUM<δ


整体架构(main函数)

首先,我们先从整体进行大览整体逻辑。首先初始化全局模型,然后划分每个用户的本地数据集,开始训练,由每个客户端进行本地训练,然后将参数传递给中心服务器,进行全局平均更新模型参数并将将新的参数传递给每个客户端。迭代数轮,最终就训练好了模型

if __name__ == '__main__':# parse argsargs = args_parser() # 参数解析args.device = torch.device('cuda:{}'.format(args.gpu) if torch.cuda.is_available() and args.gpu != -1 else 'cpu') # 切换设备# load dataset and split usersif args.dataset == 'mnist': # 加载数据集trans_mnist = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])dataset_train = datasets.MNIST('./data/mnist/', train=True, download=True, transform=trans_mnist)dataset_test = datasets.MNIST('./data/mnist/', train=False, download=True, transform=trans_mnist)# sample usersif args.iid: # 是否服从独立同分布的划分数据集dict_users = mnist_iid(dataset_train, args.num_users)else:dict_users = mnist_noniid(dataset_train, args.num_users)elif args.dataset == 'cifar': # 加载数据集trans_cifar = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])dataset_train = datasets.CIFAR10('./data/cifar', train=True, download=True, transform=trans_cifar)dataset_test = datasets.CIFAR10('./data/cifar', train=False, download=True, transform=trans_cifar)if args.iid:dict_users = cifar_iid(dataset_train, args.num_users)else:exit('Error: only consider IID setting in CIFAR10')else:exit('Error: unrecognized dataset')img_size = dataset_train[0][0].shape# build modelif args.model == 'cnn' and args.dataset == 'cifar': # 选定模型net_glob = CNNCifar(args=args).to(args.device)elif args.model == 'cnn' and args.dataset == 'mnist':net_glob = CNNMnist(args=args).to(args.device)elif args.model == 'mlp':len_in = 1for x in img_size:len_in *= xnet_glob = MLP(dim_in=len_in, dim_hidden=200, dim_out=args.num_classes).to(args.device)else:exit('Error: unrecognized model')print(net_glob)net_glob.train()# copy weightsw_glob = net_glob.state_dict() # 获取全局权重# trainingloss_train = []cv_loss, cv_acc = [], []val_loss_pre, counter = 0, 0net_best = Nonebest_loss = Noneval_acc_list, net_list = [], []if args.all_clients: print("Aggregation over all clients") # 所有客户端的聚合w_locals = [w_glob for i in range(args.num_users)] # 将初始化权重分配给每个用户for iter in range(args.epochs): # 总的训练轮次loss_locals = [] #if not args.all_clients:w_locals = []''' args.frac每次梯度下降的比例	args.num_users客户端数量 '''m = max(int(args.frac * args.num_users), 1) # 选择需要进行梯度下降的用户数量idxs_users = np.random.choice(range(args.num_users), m, replace=False) # 随机选择for idx in idxs_users:local = LocalUpdate(args=args, dataset=dataset_train, idxs=dict_users[idx])w, loss = local.train(net=copy.deepcopy(net_glob).to(args.device)) # 获取全局模型并开始本地训练if args.all_clients:w_locals[idx] = copy.deepcopy(w) # 获取每个客户端的本地参数else:w_locals.append(copy.deepcopy(w))loss_locals.append(copy.deepcopy(loss)) # 获取损失函数# update global weightsw_glob = FedAvg(w_locals) # 进行聚合平均# copy weight to net_globnet_glob.load_state_dict(w_glob) # 更新参数# print lossloss_avg = sum(loss_locals) / len(loss_locals)print('Round {:3d}, Average loss {:.3f}'.format(iter, loss_avg))loss_train.append(loss_avg)# plot loss curveplt.figure()plt.plot(range(len(loss_train)), loss_train)plt.ylabel('train_loss')plt.savefig('./save/fed_{}_{}_{}_C{}_iid{}.png'.format(args.dataset, args.model, args.epochs, args.frac, args.iid))# testingnet_glob.eval()acc_train, loss_train = test_img(net_glob, dataset_train, args)acc_test, loss_test = test_img(net_glob, dataset_test, args)print("Training accuracy: {:.2f}".format(acc_train))print("Testing accuracy: {:.2f}".format(acc_test))

参数处理

这里给出我们所使用到的参数,还有一些参数在代码中并没有使用到

参数解释
epochs中心服务器训练的轮次
num_users客户端数量
frac每次进行梯度下降的比例
local_ep本地训练模型的轮次
local_bs本地批量大小
lr学习率
momentumSGD梯度下降法的动量大小
model选用模型
dataset所用数据集
iid数据集划分是否符合独立同分布
num_classes模型的通道数
gpu选用模型
stopping_rounds选用模型
verbose详细打印
seed随机种子
all_clients聚合所有的客户端
def args_parser():parser = argparse.ArgumentParser()# federated argumentsparser.add_argument('--epochs', type=int, default=10, help="rounds of training")parser.add_argument('--num_users', type=int, default=100, help="number of users: K")parser.add_argument('--frac', type=float, default=0.1, help="the fraction of clients: C")parser.add_argument('--local_ep', type=int, default=5, help="the number of local epochs: E")parser.add_argument('--local_bs', type=int, default=10, help="local batch size: B")parser.add_argument('--bs', type=int, default=128, help="test batch size")parser.add_argument('--lr', type=float, default=0.01, help="learning rate")parser.add_argument('--momentum', type=float, default=0.5, help="SGD momentum (default: 0.5)")parser.add_argument('--split', type=str, default='user', help="train-test split type, user or sample")# model argumentsparser.add_argument('--model', type=str, default='mlp', help='model name')parser.add_argument('--kernel_num', type=int, default=9, help='number of each kind of kernel')parser.add_argument('--kernel_sizes', type=str, default='3,4,5',help='comma-separated kernel size to use for convolution')parser.add_argument('--norm', type=str, default='batch_norm', help="batch_norm, layer_norm, or None")parser.add_argument('--num_filters', type=int, default=32, help="number of filters for conv nets")parser.add_argument('--max_pool', type=str, default='True',help="Whether use max pooling rather than strided convolutions")# other argumentsparser.add_argument('--dataset', type=str, default='mnist', help="name of dataset")parser.add_argument('--iid', action='store_true', help='whether i.i.d or not')parser.add_argument('--num_classes', type=int, default=10, help="number of classes")parser.add_argument('--num_channels', type=int, default=3, help="number of channels of imges")parser.add_argument('--gpu', type=int, default=-1, help="GPU ID, -1 for CPU")parser.add_argument('--stopping_rounds', type=int, default=10, help='rounds of early stopping')parser.add_argument('--verbose', action='store_true', help='verbose print')parser.add_argument('--seed', type=int, default=1, help='random seed (default: 1)')parser.add_argument('--all_clients', action='store_true', help='aggregation over all clients')args = parser.parse_args()return args

独立同分布划分数据集

可以设置独立同分布还是非独立同分布划分数据集(其实这块内容基本使用不到),在目前学术界大都采用Dirichlet分布或者Pathological分布

其实这块内容从某个方面来看,在常规阶段提点无法有较大提升,所以提出了新的场景(Dirichlet分布Pathological分布),在新的场景中可以完成显著的提点,当然这个场景本身是没有问题的,以及衍生的算法也没有问题。但是我们可以学习到一个学术思路,将我们创新出的算法放置到一个新场景或许有意料之外的效果。其实也有点先射箭后画靶的意思了。好了,这里不再过多谈论了

def mnist_iid(dataset, num_users): # 独立同分布划分"""Sample I.I.D. client data from MNIST dataset:param dataset::param num_users::return: dict of image index"""num_items = int(len(dataset)/num_users)dict_users, all_idxs = {}, [i for i in range(len(dataset))]for i in range(num_users): # 遍历每个用户dict_users[i] = set(np.random.choice(all_idxs, num_items, replace=False)) # 进行抽取all_idxs = list(set(all_idxs) - dict_users[i]) # 删除已经抽取过的数据return dict_users # 返回划分好的字典def mnist_noniid(dataset, num_users): # 非独立同分布划分"""Sample non-I.I.D client data from MNIST dataset:param dataset::param num_users::return:"""num_shards, num_imgs = 200, 300idx_shard = [i for i in range(num_shards)]dict_users = {i: np.array([], dtype='int64') for i in range(num_users)}idxs = np.arange(num_shards*num_imgs)labels = dataset.train_labels.numpy()# sort labelsidxs_labels = np.vstack((idxs, labels))idxs_labels = idxs_labels[:,idxs_labels[1,:].argsort()]idxs = idxs_labels[0,:]# divide and assignfor i in range(num_users):rand_set = set(np.random.choice(idx_shard, 2, replace=False))idx_shard = list(set(idx_shard) - rand_set)for rand in rand_set:dict_users[i] = np.concatenate((dict_users[i], idxs[rand*num_imgs:(rand+1)*num_imgs]), axis=0)return dict_users

模型

MLP模型

class MLP(nn.Module):def __init__(self, dim_in, dim_hidden, dim_out):super(MLP, self).__init__()self.layer_input = nn.Linear(dim_in, dim_hidden)self.relu = nn.ReLU()self.dropout = nn.Dropout()self.layer_hidden = nn.Linear(dim_hidden, dim_out)def forward(self, x):x = x.view(-1, x.shape[1]*x.shape[-2]*x.shape[-1])x = self.layer_input(x)x = self.dropout(x)x = self.relu(x)x = self.layer_hidden(x)return x

卷积模型

class CNNMnist(nn.Module):def __init__(self, args):super(CNNMnist, self).__init__()self.conv1 = nn.Conv2d(args.num_channels, 10, kernel_size=5)self.conv2 = nn.Conv2d(10, 20, kernel_size=5)self.conv2_drop = nn.Dropout2d()self.fc1 = nn.Linear(320, 50)self.fc2 = nn.Linear(50, args.num_classes)def forward(self, x):x = F.relu(F.max_pool2d(self.conv1(x), 2))x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))x = x.view(-1, x.shape[1]*x.shape[2]*x.shape[3])x = F.relu(self.fc1(x))x = F.dropout(x, training=self.training)x = self.fc2(x)return xclass CNNCifar(nn.Module):def __init__(self, args):super(CNNCifar, self).__init__()self.conv1 = nn.Conv2d(3, 6, 5)self.pool = nn.MaxPool2d(2, 2)self.conv2 = nn.Conv2d(6, 16, 5)self.fc1 = nn.Linear(16 * 5 * 5, 120)self.fc2 = nn.Linear(120, 84)self.fc3 = nn.Linear(84, args.num_classes)def forward(self, x):x = self.pool(F.relu(self.conv1(x)))x = self.pool(F.relu(self.conv2(x)))x = x.view(-1, 16 * 5 * 5)x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = self.fc3(x)return x

本地模型训练

每个客户端经过本地训练,上传模型置中央服务器上,服务器进行聚合并将聚合后的模型下发到各个客户端。迭代数次,一个泛化性强大的模型便训练好了。

这里谈到了泛化性,我便多说一点,与之对应的便是个性化。此时引出来了个性化联邦学习PFL,其实所谓的PFL从技术层面上看就是取巧了,更多的强调的是个性化,那么它是否丧失了泛化性呢?说实话,其实大部分这方面论文早已丧失了泛化性,本身就是本地训练个模型,但是将其中的某些层经过聚合,实际上不经过聚合,模型的点数也很高。其实这里已经给出了为什么PFL的点数如此之高

当然,是否存在两者兼具的算法呢?当让存在,只不过其它方面又存在一些问题。学术其实就是这样,不断打补丁

class LocalUpdate(object):def __init__(self, args, dataset=None, idxs=None):self.args = argsself.loss_func = nn.CrossEntropyLoss()self.selected_clients = []self.ldr_train = DataLoader(DatasetSplit(dataset, idxs), batch_size=self.args.local_bs, shuffle=True)def train(self, net):net.train() # 设置为训练模式# train and updateoptimizer = torch.optim.SGD(net.parameters(), lr=self.args.lr, momentum=self.args.momentum)epoch_loss = []for iter in range(self.args.local_ep): # 本地训练的轮次batch_loss = []for batch_idx, (images, labels) in enumerate(self.ldr_train):images, labels = images.to(self.args.device), labels.to(self.args.device)net.zero_grad() # 梯度清零log_probs = net(images) # 预测loss = self.loss_func(log_probs, labels) # 计算损失函数loss.backward() # 反向传播optimizer.step() # 进行优化if self.args.verbose and batch_idx % 10 == 0: # 详细打印程度print('Update Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(iter, batch_idx * len(images), len(self.ldr_train.dataset),100. * batch_idx / len(self.ldr_train), loss.item()))batch_loss.append(loss.item())epoch_loss.append(sum(batch_loss)/len(batch_loss))return net.state_dict(), sum(epoch_loss) / len(epoch_loss)

聚合权重

中心服务器每轮接受到了客户端传递的参数,进行平均聚合(同样的,也可以采用加权聚合),然后再下发给每个客户端

def FedAvg(w):w_avg = copy.deepcopy(w[0]) # 对第一个客户端进行深层拷贝for k in w_avg.keys(): # 遍历每一个参数for i in range(1, len(w)): # 遍历每一个客户端并相加w_avg[k] += w[i][k]w_avg[k] = torch.div(w_avg[k], len(w)) # 最后求平均return w_avg

个性化联邦学习(Personalized federated learning, PFL)

谈论到了联邦学习(Federated Learning),那就不得不谈论PFL了,代码引用于https://github.com/TsingZ0/PFLlib

上述的PFLib库不仅仅有PFL,同样也集成了FL。跑联邦的实验很好用,建议使用此框架完成对比实验,这里就不再详细介绍代码了,PFLib作者本身做的还是很不错的,代码架构清晰明了。

看别人的代码清晰明了,看自己的代码不堪入目ε(┬┬﹏┬┬)3

实际上,PFL是个伪需求(未来有可能成为真是需求)。我们回顾一下FL的诞生,FL诞生之前是分布式学习,摇身一变成为了联邦学习。再过几年,个性化联邦学习应运而生。FL是为了隐私保护,保护各个客户端的数据不被其他人获取,但是依然希望获取一个泛化性能较强的模型,而PFL为了追求个性化,其实是在泛化性和个性化作了一个平衡,但是随时间各个论文为了提点,不得不更偏向于个性化

过度个性化 ≈ \approx 本地训练模型

从这里就可以看到,目前近几年部分论文在底层上,实际与本地训练个模型别无二致,无非添加了些概念(截至2025.6)

谈论到这里,其实还一种学术思路,便是将其他领域的方法嫁接到本领域当中,比如对抗学习、元学习、对比学习、知识蒸馏等等(这些都已有论文了)。所以往往不要在本领域寻找创新点,创新点可能在其它领域中等待着发现。

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

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

相关文章

半导体和PN结

1. 什么是半导体&#xff1f; 导体&#xff0c;电阻率小&#xff0c;即电流容易通过的材料&#xff1b;Cu 绝缘体&#xff0c;导电性低&#xff0c;即电流不易通过的材料&#xff1b;塑料 半导体&#xff0c;常温下导电性能介于导体和绝缘体之间&#xff0c;是一种导电可控的…

如何分析大语言模型(LLM)的内部表征来评估文本的“诚实性”

如何分析大语言模型(LLM)的内部表征来评估文本的“诚实性” 基于这样一个假设:模型在生成诚实和不诚实回答时,其内部状态会存在系统性差异 LAT :线性人工断层扫描 我们通过一个生活化的例子来理解如何通过分析大语言模型的内部表征评估文本的“诚实性”。 场景类比:判…

【算法】动态规划 矩阵 :62. 不同路径

62. 不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&…

LabVIEW调用Excel宏实现数据可视化

通过LabVIEW 的 ActiveX 接口&#xff0c;调用 Excel 应用程序&#xff0c;实现打开指定Excel 工作簿并运行其中宏&#xff08;如 “GraphData” 宏&#xff09;&#xff0c;将工作表数据以图表形式展示。通过 ActiveX 自动化技术&#xff0c;打通 LabVIEW 与 Excel 交互通道&a…

初始CNN(卷积神经网络)

卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;简称 CNN&#xff09;作为深度学习的重要分支&#xff0c;在图像识别、目标检测、语义分割等领域大放异彩。无论是手机上的人脸识别解锁&#xff0c;还是自动驾驶汽车对道路和行人的识别&#xff0c;背后都离…

深度解析Spring Bean生命周期:从字节码到可用对象的奇幻旅程

&#x1f331; 深度解析Spring Bean生命周期&#xff1a;从字节码到可用对象的奇幻旅程 你是否曾困惑&#xff1a;为什么PostConstruct有时不执行&#xff1f;为什么循环依赖报错如此难解&#xff1f;为什么AOP代理在某些场景失效&#xff1f; 本文将彻底拆解Spring Bean的16个…

MySQL 复合查询和内外连接 -- 子查询,多表查询,自连接,合并查询,表的内外连接

目录 1. 子查询 1.1 单行子查询 1.2 多行子查询 1.3 多列子查询 1.4 在 from 子句中使用子查询 2. 多表查询 3. 自连接 4. 合并查询 4.1 union 4.2 union all 5. 表的内连接 6. 表的外连接 下列先给出该博客中所用到的所有表的数据。 &#xff08;1&#xff09;部…

【STM32+LAN9252+HAL库】EtherCAT从站搭建 保姆级教程

目录 一、生成协议栈及XML文件 二、使用stm32CuboMX配置外设 三、协议栈移植 鉴于本人对EtherCAT的掌握程度十分有限&#xff0c;这篇文章仅作为我搭建基础从站的过程记录不做更多讲解。本文内容主要为SPI模式的基础搭建&#xff0c;更多深入的学习资料和细节&#xff0c;大家…

【LeetCode 热题 100】239. 滑动窗口最大值——(解法二)滑动窗口+单调队列

Problem: 239. 滑动窗口最大值 题目&#xff1a;给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值 。 【LeetCode 热题 100】239. 滑…

MySQL 8.0 连接 5.x 服务器认证问题

总的来说&#xff0c;答案是&#xff1a;可以&#xff0c;但是需要特别注意认证方式的兼容性问题。 MySQL 8.0 引入了新的默认认证插件 caching_sha2_password&#xff0c;而 MySQL 5.x&#xff08;及更早版本&#xff09;使用的是 mysql_native_password。当你用一个 8.0 的客…

Spring原理揭秘(一)

什么是spring&#xff1f; spring框架是一个轻量级的开源的JavaEE框架。 所谓轻量级则是&#xff1a;占用空间小&#xff0c;代码侵入性低&#xff0c;代码耦合度低&#xff0c;降低代码复杂度&#xff0c;可以轻易适配多种框架。 随着spring的不断发展&#xff0c;它所占用…

Visual Studio Code自用搜索技巧整理

多文件跨行搜索 用途 在多个日志文件中搜索跨行日志 方法 1.用VS Code打开待搜索文件所在的目录&#xff1b; 2.按快捷键&#xff08;CtrlShiftF&#xff09;打开全局搜索&#xff1b; 3.点击搜索框右侧的开启正则表达式&#xff1b; 4.输入正则表达式&#xff0c;例如&…

Axure PR 9 验证码登录 案例

大家好&#xff0c;我是大明同学。 这期内容&#xff0c;我们来用Axure来制作一个短信验证登录页面的小案例。 验证码登录小案例 创建手机号输入框所需的元件 1.打开一个新的 RP 文件并在画布上打开 Page 1。 2.在元件库中拖出一个矩形元件&#xff0c;选中矩形元件&#xf…

监听器模式

1. 问题背景 假设我们有一个 银行账户管理系统&#xff0c;该系统需要监控用户账户余额的变动&#xff0c;并在发生变动时&#xff0c;自动执行一些相关的操作&#xff0c;比如发送 余额变动通知&#xff08;如短信、邮件等&#xff09;。为了实现这一功能&#xff0c;我们希望…

帕鲁杯应急响应赛题:知攻善防实验室

一、背景信息 在这个跳跃的数字舞台上&#xff0c;数据安全成了政企单位稳航的重要压舱石。某政企单位&#xff0c;作为一艘驶向未来 的巨轮&#xff0c;对数据的把控丝毫不敢松懈。眼下&#xff0c;我们即将启航一场无与伦比的探险——“信息安全探索之 旅”。 这趟旅程的目的…

【硬核数学】2.2 深度学习的“微积分引擎”:自动微分与反向传播《从零构建机器学习、深度学习到LLM的数学认知》

欢迎来到本系列的第七篇文章。在上一章&#xff0c;我们用张量武装了我们的线性代数知识&#xff0c;学会了如何描述和操作神经网络中的高维数据流。我们知道&#xff0c;一个神经网络的“前向传播”过程&#xff0c;就是输入张量经过一系列复杂的张量运算&#xff08;矩阵乘法…

DAY 45 Tensorboard使用介绍

浙大疏锦行https://blog.csdn.net/weixin_45655710知识点回顾&#xff1a; tensorboard的发展历史和原理tensorboard的常见操作tensorboard在cifar上的实战&#xff1a;MLP和CNN模型 作业&#xff1a;对resnet18在cifar10上采用微调策略下&#xff0c;用tensorboard监控训练过程…

2023年全国硕士研究生招生考试英语(一)试题总结

文章目录 题型与分值分布完形填空错误 1&#xff1a;考察连词 or 前后内容之间的逻辑关系错误2&#xff1a;错误3&#xff1a;错误4&#xff1a;这个错得最有价值&#xff0c;因为压根没读懂错误5&#xff1a;学到的短语&#xff1a; 仔细阅读排序/新题型翻译小作文大作文 题型…

react-数据Mock实现——json-server

什么是mock&#xff1f; 在前后端分离的开发模式下&#xff0c;前端可以在没有实际后端接口的支持下先进行接口数据的模拟&#xff0c;进行正常的业务功能开发 json-server实现数据Mock json-server是一个node的包&#xff0c;可以在不到30秒内获得零编码的完整Mock服务 实现…

使用POI导入解析excel文件

首先校验 /*** 校验导入文件* param file 上传的文件* return 校验结果&#xff0c;成功返回包含成功状态的AjaxResult&#xff0c;失败返回包含错误信息的AjaxResult*/private AjaxResult validateImportFile(MultipartFile file) {if (file.isEmpty()) {return AjaxResult.er…