【学习笔记】计算机视觉对比学习综述
计算机视觉对比学习综述
- 前言
- 百花齐放
- InstDisc
- InvaSpread
- CPC
- CMC
- CV双雄
- MoCo
- SimCLR
- MoCo v2
- SimCLR v2
- SwAV
- 不用负样本
- BYOL
- SimSiam
- Transformer
- MoCo v3
- DINO
- 总结
- 参考链接
前言
本篇对比学习综述内容来自于沐神对比学习串讲视频以及其中所提到的论文和博客,对应的链接详见第六节。本篇博客所涉及的对比学习内容均应用在CV领域,算是到21年为止比较全面的对比学习综述内容讲解。对比学习作为一种自监督学习方法,其在CV领域的发展也印证着自监督学习的发展,科研工作者也在一步步揭开自监督学习的神秘面纱,通过构建不同的代理任务,采用不同的模型架构,来极力挖掘自监督学习的潜力。相信通过本篇博客的学习,你能够对对比学习有着更深刻的认识,万变不离其宗,当你领悟到对比学习的本质,你就能
百花齐放
InstDisc
InstDisc文章设计了一个全新的代理任务——个体判别任务,并提出了memory bank方法,即记忆库,该方法在MoCo这篇文章中反复被比较,如果说MoCo是一个里程碑式的工作,那么InstDisc就是巨人的肩膀,MoCo正是站在这个肩膀上进行优化的。下面对这篇工作进行简单的介绍。
上图是监督学习对图片的识别结果,可以看到排名前列的都是和豹子相似的动物,这并不是因为这些动物的标签和豹子相近,而是图片本身就很相近。作者根据这个观察,提出了个体判别任务。
整个工作流程如上图所示。首先通过一个卷积神经网络将所有图片编码成128维度的特征,并且希望这些特征能够尽可能分开,因为对于个体判别任务来说,每个图片就是一个单独的类,不同图片的特征属于不同类。由于采用对比学习的方法,需要构建大量负样本,因此所有特征存在记忆库中作为负样本采样使用。每次训练时,采样K个负样本用于计算与正样本之间的NCE损失,并更新网络。网络更新完重新编码正样本的特征,替换记忆库中旧的特征,如此反复进行模型更新,记忆库的更新,以达到让每张图片的特征区分开的目的。
此外,InstDisc中也在训练中加入了约束,让特征能够动量式更新。
InvaSpread
这篇工作就是MoCo中进行比较的端到端执行的方法,即在一个mini-batch中挑选正负样本,而不采用记忆库存储。InvaSpread的核心思想是说,对于相似的图片,其通过编码的特征应该是相近的,而不相似的图片编码得到的特征应该是远离的。它同样采用了个体判别的代理任务。
上图是整个训练流程。首先对于输入的mini-batch中的图片进行数据增强,比如256张图片,经过增强得到256张增强图片,这样对于一张图片,就有一个正样本,和510个负样本,然后将这些样本输入到同一个编码器中得到表征,通过对比损失来更新参数。
通过上面的描述我们可以看到,端到端学习的特点在于共享同一个编码器,没有采用格外的数据结构来存储编码好的特征。但是这也是模型性能没有那么突出的原因,mini-batch的样本个数有限,文中最多仅支持256个样本,学习到特征较少,性能提升没有那么明显。
CPC
在代理任务上也可以做文章。除了判别式的代理任务,还有生成式的代理任务。CPC这篇工作采用的就是生成式的代理任务,流程如下所示:
对于任意的音频、文本序列或者图像块,都可以以时序的方式输入到模型中,这里的模型可以是LSTM这样的时序模型,通过处理得到表征,采用t时刻最后一层输出用于预测后面序列,后面 t~t+4 时刻的输入得到的表征都可以作为正样本,而负样本可以是任意其它时刻输入得到的表征。
CMC
CMC工作的核心在于学习所有视角的互信息,它认为像眼睛、耳朵、皮肤等一系列传感器所感知的信息是一致且共享的,比如对于一只狗,你可以看到它、听到它或者感受到它,这些信息都指向这只狗。如果能够将这些所有传感器的信息整合起来,那么将会学习到特别强大的特征。
整体流程如上图所示,输入的是一个图像的多个视角信息,包括原始图像,图像对应的深度信息,表面法线信息和语义分割信息,通过不同的编码器编码得到特征,并互为正样本,其余的任何不配对的视角都是负样本,与这些绿色的正样本特征远离。
这篇工作思想很简单,和个体判别任务类似,只不过是多个视角多张图片作为正样本,但是它展示了对比学习的灵活性,只要你脑洞大开,就可以联想到多模态场景,一张图片和其对应的文本的编码可以作为正样本,其余都是负样本,只不过可能需要多个编码器进行编码,计算代价比较高。
上面多个编码器的gap已经被解决了,由于Transformer架构的通用性和可扩展性,多模态的数据可以都使用同一个Transformer进行编码,极 大程度上降低了计算代价。
CV双雄
MoCo
MoCo的主要贡献是将之前的对比学习方法都总结为字典查询问题,提出了队列和动量编码器,得到又大又一致的字典。
MoCo方法本质上是对InstDisc的改进,但是其简单有效,证明了CV中无监督特征学习也能比有监督特征学习的预训练模型性能要好,这是具有里程碑意义的。
从写作上来看,MoCo明显要比一般的套路要高一筹,它并不是先介绍对比学习,阐述别的工作的不足,提出创新点,而是从无监督学习在CV和NLP领域的差异入手,然后归纳之前对比学习方法的本质,将问题升华了,也提升了整个工作的格局。
SimCLR
SimCLR这篇工作可以看做是对InvaSpread的改进,整体流程如下:
对于mini-batch大小为n的输入,分别通过两个数据增强的方式得到增强后的 x ~ i \tilde{x}_i x~i和 x ~ j \tilde{x}_j x~j,然后输入到相同的编码器中(比如res50)得到各自的表征,之后就是SimCLR与InvaSpread最为不同的地方,它将得到的表征又输入到共享的非线性MLP中,得到降维的特征,然后进行正负样本的判别,这样的简单操作能够将模型的性能提升多大10个点,并且在下游任务上,SIMCLR并不使用MLP,目的是和其它工作对齐,也证实其在无监督预训练部分的优越性。
当然SimCLR还有很多细节的trick,其贡献可以总结如下:
- 大量的数据增强。
- 编码器后添加非线性变换的MLP。
- 更大的batch-size。
MoCo v2
MoCo v2这篇工作就是在原先MoCo的基础上加上了SimCLR的trick,即MLP,数据增强,余弦学习率和更长的训练次数。其效果如下表所示:
可以看到其性能提升还是很明显的,尤其是非线性MLP层的加入,直接提升了6个点。接下来是与SimCLR的对比:
可以看到无论是200个epoch还是更大的训练epoch,MoCo v2都要更胜一筹,并且大大降低了显存和时间上的成本。
SimCLR v2
恰如其标题,大的自监督模型是好的半监督学习者。因此SimCLR v2这篇工作的核心是在说如何做半监督的学习。其工作流程如下:
首先在大规模无标签数据集上进行预训练,然后将预训练好的模型在少量的监督数据上微调,最后在无标注的数据上基于特定任务进行自学习。
对于这篇工作,本文主要关注其从v1升级到v2的部分。分成三个点:
- 更大的模型,无监督训练会更好。
- 两层的MLP层。
- 采用动量编码器。
SwAV
SwAV这篇工作将对比学习和聚类结合在一起,因为聚类的思想和对比学习的目标和做法都很相近。
上图是SwAV方法与之前对比学习方法的对比。前面部分的操作相同,对于输入的x,进行两次数据增强,再输入到编码器中得到编码的特征。之前的对比学习方法直接将得到的特征进行比较,但是在SwAV中,编码后的特征还要和聚类的中心进行点乘,其中,,表示聚类中心的个数。 得到的结果即是预测的分类,最后与真实的聚类结果进行比较。
SwAV结合聚类方法的优势如下:
- 之前的对比学习方法需要与上万个负样本进行对比,如ImageNet有128万个样本,那么每个样本本质上就需要和128个负样本进行对比,但是聚类后,只需要和很少的聚类中心进行比较(本文是3000个),大大降低了计算成本。
- 聚类中心具有明确的语义含义,之前的对比学习方法过于随机,可能部分正样本也被当做负样本,并且抽取的类别不均衡。
通过上图可以看出,SwAV的性能已经和监督训练的性能没有明显差异。基本上达到了ImageNet上对比学习的天花板。
上面的故事虽然很精彩,但是对SwAV性能有着大幅提升的是采用了Multi-crop。之前的对比学习方法采用了两个crop,但是这样采样的crop重叠部分很多,学习的只是全局的特征。Multi-crop加入了学习局部特征的部分,即在采样两个crop的基础上,加入了随机采取四个小crop的操作,为了不增加更多的计算成本,作者也减小了前两个crop的大小。比如之前是采样两个224224的crop,现在是采样两个160160的crop和4个96*96的crop。
Multi-crop效果显著,它已经不只只是SwAV上的一个trick,而是一个通用的方法,应用在之前的对比学习方法上也能有显著的提升。
不用负样本
BYOL
BYOL这篇工作开创了对比学习的先河,不用负样本就可以很好的学习,而不会出现模型坍塌等问题。它的核心思想是利用一个视角的信息来预测另一个视角的信息。
整体流程如上图所示,前面的步骤和正常的对比学习流程一致,都是先数据增强,通过编码器得到特征,然后经过MLP得到映射,上面紫色部分相当于query的编码器,通过梯度更新,下面相当于key的编码器,通过动量更新。与之前工作不同,得到映射后的query还要经过一层映射得到预测,用于预测,将配对的问题转化为了预测的问题。
这种训练方式模型不坍塌,可能的原因在于BN操作。一个有道理的结论是说,BN让整个min-batch中的数据发生了泄露,其他样本变成了隐式的负样本,类似于SwAV和聚类中的对比,这里相当于是和mini-batch的平均图片进行了对比。
但是这样的结论就大大降低了BYOL的创新性,因为BYOL的卖点就在于不用负样本进行对比,但是上面的结论显然打脸了。于是BYOL的团队又写了篇工作进行回应,即 BYOL works even without batch statistics。详尽的实验过程如下表所示:
通过SimCLR最后一个结果可以发现,当编码器和projector都没有用BN时,SimCLR也失败了,隐式负样本的说法不攻自破。因此最后的结论是,BN能提升模型的稳健性,并且如果模型初始化很好,没有BN也能学得很好。
SimSiam
通过上述相关内容的描述,可以看到对比学习加入的trick越来越多,性能也越来越好。SimSiam这篇工作来自何恺明团队,它将对比学习的工作化繁为简,即不需要负样本,也不需要大的batch-size,也不需要动量编码器,就能取得很好的效果。
整体的流程和伪代码如上所示,可以看到方法真的是非常简单。就是数据增强,然后将增强的两个图片输入到相同的编码器得到表征,接着就是得到二者互相预测的loss反向传播更新参数。SimSiam和其它工作的对比如下图所示:
可以看到SimSiam和BYOL几乎一模一样,只不过没有采用动量编码器。在ImageNet的表现和其它模型的对比如下表所示:
可以看到MoCo v2在长时间训练下表现最好,这也间接印证了动量编码器的优势。下表是在下游任务上训练的结果:
可以看到MoCo v2和SimSiam表现最好。
Transformer
MoCo v3
MoCo v3的工作核心在于如何解决ViT自监督训练的不稳定性。它并没有什么创新性的工作,但是却展示了自监督学习很多有意思的部分。它的核心算法如下伪代码所示:
可以看到,MoCo v2相当于是MoCo v2和SimSiam的合体。只不过把模型的骨干网络换成了ViT。ViT在训练时有一个问题,如下图所示:
随着batch-size的增大,模型的波动也随着增大,并且性能也在逐渐降低。作者通过观察梯度发现,ViT的第一层,即将token编码为embedding时会在波动时产生大的变化,因此作者就将这一层直接冻结,结果就解决了这个问题。
DINO
DINO工作本质上是BYOL工作的延续。其流程如下图所示:
它的优势在于融合了Transformer,其它部分基本上和BYOL一致。在伪代码上也和MoCo v3几乎相同,只是在损失函数上有所区分:
总结
以上工作可以总结为一张图。所以对比学习的发展从大的方向上分为四个阶段,第一阶段百花齐放,所有工作都在摸索如何使用对比学习,如何定义代理任务,如何设计损失函数。紧接着这些工作就根据负样本存储方式分为了两个主流方法,一个是基于记忆库的MoCo方法,另一个是基于端到端的SimCLR算法,二者称为CV双雄。第三阶段以BYOL为首,其发现对比学习无需负样本也能进行,样本通过不同视图的各自预测,就能实现自己和自己比较,从而学习到特征。最后阶段当然是ViT的出现,改变了对比学习的backbone,Transformer架构的优越性得以体现,这个阶段没有创新性的工作,只是替换了模型架构,就实现了很好的性能。
总而言之,对比学习本质上是一种自监督学习方法,其特征学习的核心还是在于代理任务的构建,无论是个体判别,还是多视图预测,亦或是生成,这些都是不同的任务,才是特征学习的核心部分。而其它的trick,包括memory bank,非线性MLP,都是为了帮助完成代理任务的。所以在我看来,对比学习和MAE,BERT,GPT这些预训练模型的本质相同,越好、越复杂的预训练任务,在越大的数据集上,往往能学习到更为丰富的特征。
参考链接
https://www.bilibili.com/video/BV19S4y1M7hm
https://openaccess.thecvf.com/content_cvpr_2018/papers/Wu_Unsupervised_Feature_Learning_CVPR_2018_paper.pdf
https://openaccess.thecvf.com/content_CVPR_2019/papers/Ye_Unsupervised_Embedding_Learning_via_Invariant_and_Spreading_Instance_Feature_CVPR_2019_paper.pdf
https://arxiv.org/pdf/1807.03748.pdf?fbclid=IwAR2G_jEkb54YSIvN0uY7JbW9kfhogUq9KhKrmHuXPi34KYOE8L5LD1RGPTo
https://arxiv.org/pdf/1906.05849.pdf
https://openaccess.thecvf.com/content_CVPR_2020/papers/He_Momentum_Contrast_for_Unsupervised_Visual_Representation_Learning_CVPR_2020_paper.pdf
http://proceedings.mlr.press/v119/chen20j/chen20j.pdf
https://arxiv.org/pdf/2003.04297.pdf
https://proceedings.neurips.cc/paper/2020/file/fcbc95ccdd551da181207c0c1400c655-Paper.pdf
https://proceedings.neurips.cc/paper_files/paper/2020/file/70feb62b69f16e0238f741fab228fec2-Paper.pdf
https://proceedings.neurips.cc/paper_files/paper/2020/file/f3ada80d5c4ee70142b17b8192b2958e-Paper.pdf
https://arxiv.org/pdf/2010.10241.pdf
https://openaccess.thecvf.com/content/CVPR2021/papers/Chen_Exploring_Simple_Siamese_Representation_Learning_CVPR_2021_paper.pdf
https://arxiv.org/pdf/2104.02057.pdf
https://openaccess.thecvf.com/content/ICCV2021/papers/Caron_Emerging_Properties_in_Self-Supervised_Vision_Transformers_ICCV_2021_paper.pdf
相关文章:

【学习笔记】计算机视觉对比学习综述
计算机视觉对比学习综述 前言百花齐放InstDiscInvaSpreadCPCCMC CV双雄MoCoSimCLRMoCo v2SimCLR v2SwAV 不用负样本BYOLSimSiam TransformerMoCo v3DINO 总结参考链接 前言 本篇对比学习综述内容来自于沐神对比学习串讲视频以及其中所提到的论文和博客,对应的链接详…...

【Linux】fork函数的基础知识
文章目录 前言一、fork的返回值二、常见问题 1.为什么fork要给子进程返回0,给父进程返回子进程pid?2.一个函数返回两次值怎么理解? 3.一个变量怎么会有不同的内容? 4.fork函数干了什么? 前言 fork初识: …...

代码随想录算法训练营day48 | LeetCode 198. 打家劫舍 213. 打家劫舍 II 337. 打家劫舍 III
198. 打家劫舍(题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台) 思路:dp题除背包外的另外一类题目,重点不在于看前面的情况,而在于考虑本节点的情况。一种情况…...

【已解决】Java 后端使用数组流 Array.stream() 将数组格式的 Cookie 转换成字符串格式
🎉工作中遇到这样一个场景:远程调用某个接口,该接口需要用户的 Cookie 信息进行权限认证,认证通过之后才可以打通并返回数据。 在后端拿到 httpServletRequest 后,调用 getCookies() 方法,返回的是一个 Coo…...

Redis——》如何评估锁过期时间
推荐链接: 总结——》【Java】 总结——》【Mysql】 总结——》【Redis】 总结——》【Kafka】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 总结——》【Linux】 总结——》【MongoD…...

完整开发实现公众号主动消息推送,精彩内容即刻到达
🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年6月CSDN上海赛道top4。 🏆数年电商行业从业经验,历任核心研发工程师…...

获取ip(公网和内网) 前端通过高德api获取位置信息
获取ip(公网和内网) 前端通过高德api获取位置信息 获取ip //获取公网ip getIp() {this.$axios.get(http://api.ipify.org).then((res) > {if (res) {console.log(res, 公网ip);}}).catch((e) > {console.log(e, e);}); },//获取内网ip this.getIP(…...

linux打开端口命令是什么
linux打开端口命令是什么 linux开启端口的命令是 1 firewall-cmd --zonepublic --add-port端口/通讯协议 --permanent 需要注意的是,我们在开启指定端口后需要重启防火墙。 示例如下: 1、开启防火墙 1 systemctl start firewalld 2、开放指定端…...

从《孤注一掷》出发,聊聊 SSL 证书的重要性
你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 图片来源于电影《孤注一掷》…...

专题:曲面的切平面、法线
假设曲面方程为隐函数 F ( x , y , z ) 0 ,点 M ( x 0 , y 0 , z 0 ) 是其上一点 又在点 M 处任意引一条在曲面上的曲线,设该曲线参数方程为: { x φ ( t ) y ψ ( t ) z ω ( t ) ,且当 t t 0 时, x x 0 , y y…...

数据结构:排序解析
文章目录 前言一、常见排序算法的实现1.插入排序1.直接插入排序2.希尔排序 2.交换排序1.冒泡排序2.快速排序1.hoare版2.挖坑版3.前后指针版4.改进版5.非递归版 3.选择排序1.直接选择排序2.堆排序 4.归并排序1.归并排序递归实现2.归并排序非递归实现 5.计数排序 二、排序算法复杂…...

Revit SDK:AutoJoin 自动合并体量
前言 Revit 有一套完整的几何造型能力,每一个体量都是一个GenericForm,这些体量可以通过拉伸、扫掠等创建。这个例子介绍如何将他们合并成一个体量。 内容 合并体量的关键接口: // Autodesk.Revit.DB.Document public GeomCombination Com…...

MYSQL(索引、事务)
文章目录 一、索引二、事务 一、索引 数据库中的表、数据、索引之间的关系,类似于书架上的图书、书籍内容和书籍目录的关系 1. 概述 概念:相当于是一本书的目录,是以‘列’为维度进行建立的使用场景:如果我们要查询一个表中的某个…...

部署问题集合(二十三)设置Docker容器内的中文字符集,解决某些情况下中文乱码的问题
前言: 同事给了一个服务,在Windows环境下怎么跑都正常,但一到Linux虚拟机里就中文乱码起初就想到了可能是字符集的问题,但调整了半天也没见效果,最后隔了几天突然想到,我是构建Docker跑的,而且…...

Web AP—PC端网页特效
PC端网页特效 代码下载 元素偏移量 offset 系列 offset 翻译过来就是偏移量, 我们使用 offset系列相关属性可以动态的得到该元素的位置(偏移)、大小等。 获得元素距离带有定位父元素的位置获得元素自身的大小(宽度高度&#x…...

Spring线程池ThreadPoolTaskExecutor使用
为什么使用线程池? 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行…...

spring mvc的执行流程
请求拦截。用户发起请求,请求先被sevlet拦截,转发给spring mvc框架请求转发。spring mvc里面的DispcherServlet会接收到请求并转发给HandlerMapping匹配接口。HandlerMapping负责解析请求,根据请求信息和配置信息找到匹配的controller类&…...

docker作业
目录 1、使用mysql:5.6和 owncloud 镜像,构建一个个人网盘。 1.1启动镜像 1.2启动cloud镜像 1.3浏览器访问 编辑 2、安装搭建私有仓库 Harbor 2.1下载docker-compose 2.2 磁盘挂载,保存harbor 2.3 修改配置文件 2.4安装 2.5浏览器访问 2.6 新…...

java实现本地文件转文件流发送到前端
java实现本地文件转文件流发送到前端 Controller public void export(HttpServletResponse response) {// 创建file对象response.setContentType("application/octet-stream");// 文件名为 sresponse.setHeader("Content-Disposition", "attachment;…...
2020ICPC南京站
K K Co-prime Permutation 题意:给定n和k,让你构造n的排列,满足gcd(pi, i)1的个数为k。 思路:因为x和x-1互质,1和任何数互质,任何数和它本身不互质 当k为奇数时,p11,后面k-1个数…...

Linux 中的 chsh 命令及示例
介绍 bash shell 是 Linux 最流行的登录 shell 之一。但是,对于不同的命令行操作,可以使用替代方法。chshLinux 中的( change shell )命令使用户能够修改登录 shell 。 以下教程...

JavaScript 数组如何实现冒泡排序?
冒泡排序是一种简单但效率较低的排序算法,常用于对小型数据集进行排序。它的原理是多次遍历数组,比较相邻元素的大小,并根据需要交换它们的位置,将最大(或最小)的元素逐渐“冒泡”到数组的一端。这个过程会…...

ZooKeeper集群环境搭建
🥇🥇【大数据学习记录篇】-持续更新中~🥇🥇 个人主页:beixi 本文章收录于专栏(点击传送):【大数据学习】 💓💓持续更新中,感谢各位前辈朋友们支持…...

【跟小嘉学 Rust 编程】二十、进阶扩展
系列文章目录 【跟小嘉学 Rust 编程】一、Rust 编程基础 【跟小嘉学 Rust 编程】二、Rust 包管理工具使用 【跟小嘉学 Rust 编程】三、Rust 的基本程序概念 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念 【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据 【跟小嘉学…...

pytorch学习过程中一些基础语法
1、tensor.view()函数,通俗理解就是reshape,#参数这里的-1需要注意,可以根据原张量size自行计算 data1torch.randn((4,2)) data2data1.view(2,4) data3data2.view(-1,8)2、tensor.max()函数,在分类问题中,通常需要使用…...

判断聚类 n_clusters
目录 基本原理 代码实现: 肘部法则(Elbow Method): 轮廓系数(Silhouette Coefficient) Gap Statistic(间隙统计量): Calinski-Harabasz Index(Calinski-…...

基于深度学习的网络异常检测方法研究
摘要: 本文提出了一种基于深度学习的网络异常检测方法,旨在有效地识别网络中潜在的异常行为。通过利用深度学习算法,结合大规模网络流量数据的训练,我们实现了对复杂网络环境下的异常行为的准确检测与分类。实验结果表明…...

SSM 基于注解的整合实现
一、pom.xml <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><modelV…...

工具类APP如何解决黏性差、停留短、打开率低等痛点?
工具产品除了需要把自己的功能做到极致之外,其实需要借助一些情感手段、增设一些游戏机制、输出高质量内容、搭建社区组建用户关系链等方式,来提高产品的用户黏性,衍生产品的价值链。 工具类产品由于进入门槛低,竞争尤为激烈&…...

使用Java MVC开发高效、可扩展的Web应用
在当今的Web开发领域,高效和可扩展性是我们追求的目标。Java作为一种强大且广泛使用的编程语言,提供了丰富的工具和框架来支持Web应用的开发。其中,MVC模式是一种被广泛采用的架构模式,它能够有效地组织和管理代码,使得…...