对抗生成网络GAN系列——Spectral Normalization原理详解及源码解析

news2024/11/30 8:55:10

🍊作者简介:秃头小苏,致力于用最通俗的语言描述问题

🍊专栏推荐:深度学习网络原理与实战

🍊近期目标:写好专栏的每一篇文章

🍊支持小苏:点赞👍🏼、收藏⭐、留言📩

 

对抗生成网络GAN系列——Spectral Normalization原理详解及源码解析

写在前面

Hello,大家好,我是小苏🧒🏽🧒🏽🧒🏽

在前面的文章中,我已经介绍过挺多种GAN网络了,感兴趣的可以关注一下我的专栏:深度学习网络原理与实战 。目前专栏主要更新了GAN系列文章、Transformer系列和语义分割系列文章,都有理论详解和代码实战,文中的讲解都比较通俗易懂,如果你希望丰富这方面的知识,建议你阅读试试,相信你会有蛮不错的收获。🍸🍸🍸

在阅读本篇教程之前,你非常有必要阅读下面两篇文章:

  • [1]对抗生成网络GAN系列——DCGAN简介及人脸图像生成案例
  • [2]对抗生成网络GAN系列——WGAN原理及实战演练

其实啊,我相信大家来看这篇文章的时候,一定是对上文提到的文章有所了解了,因此大家要是觉得自己对GAN和WGAN了解的已经足够透彻了,那么完全没有必要再浪费时间阅读了。如果你还对它们有一些疑惑或者过了很久已经忘了希望回顾一下的话,那么文章[1]和文章[2]获取对你有所帮助。

大家准备好了嘛,我们这就开始准备学习Spectral Normalization啦!🚖🚖🚖

 

Spectral Normalization原理详解

​  首先,让我们简单的回顾一下WGAN。🌞🌞🌞由于原始GAN网络存在训练不稳定的现象,究其本质,是因为它的损失函数实际上是JS散度,而JS散度不会随着两个分布的距离改变而改变(这句不严谨,细节参考WGAN中的描述),这就会导致生成器的梯度会一直不变,从而导致模型训练效果很差。WGAN为了解决原始GAN网络训练不稳定的现象,引入了EM distance代替原有的JS散度,这样的改变会使生成器梯度一直变化,从而使模型得到充分训练。但是WGAN的提出伴随着一个难点,即如何让判别器的参数矩阵满足Lipschitz连续条件。

​  如何解决上述所说的难点呢?在WGAN中,我们采用了一种简单粗暴的方式来满足这一条件,即直接对判别器的权重参数进行剪裁,强制将权重限制在[-c,c]范围内。大家可以动动我们的小脑瓜想想这种权重剪裁的方式有什么样的问题——(滴,揭晓答案🍍🍍🍍)如果权重剪裁的参数c很大,那么任何权重可能都需要很长时间才能达到极限,从而使训练判别器达到最优变得更加困难;如果权重剪裁的参数c很小,这又容易导致梯度消失。因此,如何确定权重剪裁参数c是重要的,同时这也是困难的。WGAN提出之后,又提出了WGAN-GP来实现Lipschitz 连续条件,其主要通过添加一个惩罚项来实现。【关于WGAN-GP我没有做相关教程,如果不明白的可以评论区留言】那么本文提出了一种归一化的手段Spectral Normalization来实现Lipschitz连续条件,这种归一化具体是怎么实现的呢,下面听我慢慢道来。🍻🍻🍻


我们还是来先回顾一下Lipschitz连续条件,如下:

​             ∣ f ( x 1 ) − f ( x 2 ) ∣ ≤ K ∣ x 1 − x 2 ∣ |f(x_1)-f(x_2)| \le K|x_1-x_2| f(x1)f(x2)Kx1x2

这个式子限制了函数 f ( ⋅ ) {\rm{f}}( \cdot ) f()的导数,即其导数的绝对值小于K, ∣ f ( x 1 ) − f ( x 2 ) ∣ ∣ x 1 − x 2 ∣ ≤ K \frac{|f(x_1)-f(x_2)|}{|x_1-x_2|} \le K x1x2f(x1)f(x2)K。 🍋🍋🍋

本文介绍的Spectral Normalization的K=1,让我们一起来看看怎么实现的吧!!!


  上文提到,WGAN的难点是如何让判别器的参数矩阵满足Lipschitz连续条件。那么我们就从判别器入手和大家唠一唠。实际上,判别器也是由多层卷积神经网络构成的,我们用下式表示第n层网络输出和第n-1层输入的关系:

​             X n = a n ( W n ⋅ X n − 1 + b n ) X_n=a_n(W_n \cdot X_{n-1}+b_n) Xn=an(WnXn1+bn)

  其中 a n ( ⋅ ) a_n(\cdot) an()表示激活函数, W n W_n Wn表示权重参数矩阵。为了方便起见,我们不设置偏置项 b n b_n bn,即 b n = 0 b_n=0 bn=0。那么上式变为:

​             X n = a n ( W n ⋅ X n − 1 ) X_n=a_n(W_n \cdot X_{n-1}) Xn=an(WnXn1)

  再为了方便起见🤸🏽‍♂️🤸🏽‍♂️🤸🏽‍♂️,我们设 a n ( ⋅ ) a_n(\cdot) an(),即激活函数为Relu。Relu函数在大于0时为y=x,小于0时为y=0,函数图像如下图所示:

image-20221114112127282

​  这样的话式 X n = a n ( W n ⋅ X n − 1 ) X_n=a_n(W_n \cdot X_{n-1}) Xn=an(WnXn1)可以写成 X n = D n ⋅ W n ⋅ X n − 1 X_n=D_n \cdot W_n \cdot X_{n-1} Xn=DnWnXn1,其中 D n D_n Dn为对角矩阵。【大家这里能否理解呢?如果我们的输入为正数时,通过Relu函数值是不变的,那么此时 D n D_n Dn对应的对角元素应该为1;如果我们的输入为负数时,通过Relu函数值将变成0,那么此时 D n D_n Dn对应的对角元素应该为0。也就是说我们将 X n X_n Xn改写成 D n ⋅ W n ⋅ X n − 1 D_n \cdot W_n \cdot X_{n-1} DnWnXn1形式是可行的。】

​  接着我们做一些简单的推理,得到判别器第n层输出和原始输入的关系,如下图所示:

image-20221114144627781

  最后一层的输出 X n X_n Xn即为判别器的输出,接下来我们用 f ( x ) f(x) f(x)表示;原始输入数据 x 0 x_0 x0我们接下来用 x x x表示。则判别器最终输入输出的关系式如下:

​    f ( x ) = D n ⋅ W n ⋅ D n − 1 ⋅ W n − 1 ⋯ D 3 ⋅ W 3 ⋅ D 2 ⋅ W 2 ⋅ D 1 ⋅ W 1 ⋅ x f(x) = {D_n} \cdot {W_n} \cdot {D_{n - 1}} \cdot {W_{n - 1}} \cdots {D_3} \cdot {W_3} \cdot {D_2} \cdot {W_2} \cdot {D_1} \cdot {W_1} \cdot x f(x)=DnWnDn1Wn1D3W3D2W2D1W1x

  上文说到Lipschitz连续条件本质上就是限制函数 f ( ⋅ ) {\rm{f}}( \cdot ) f()的导数变化范围,其实就是对 f ( x ) f(x) f(x)梯度提出限制,如下:

∣ ∣ ∇ x f ( x ) ∣ ∣ 2 = ∣ ∣ D n ⋅ W n ⋅ D n − 1 ⋅ W n − 1 ⋯ D 3 ⋅ W 3 ⋅ D 2 ⋅ W 2 ⋅ D 1 ⋅ W 1 ∣ ∣ 2 ≤ ∣ ∣ D n ∣ ∣ 2 ⋅ ∣ ∣ W n ∣ ∣ 2 ⋅ ∣ ∣ D n − 1 ∣ ∣ 2 ⋅ ∣ ∣ W n − 1 ∣ ∣ 2 ⋯ ∣ ∣ D 1 ∣ ∣ 2 ⋅ ∣ ∣ W 1 ∣ ∣ 2 ||{\nabla _x}f(x)|{|_2} = ||{D_n} \cdot {W_n} \cdot {D_{n - 1}} \cdot {W_{n - 1}} \cdots {D_3} \cdot {W_3} \cdot {D_2} \cdot {W_2} \cdot {D_1} \cdot {W_1}|{|_2} \le ||{D_n}|{|_2} \cdot ||{W_n}|{|_2} \cdot ||{D_{n - 1}}|{|_2} \cdot ||{W_{n - 1}}|{|_2} \cdots ||{D_1}|{|_2} \cdot ||{W_1}|{|_2} ∣∣xf(x)2=∣∣DnWnDn1Wn1D3W3D2W2D1W12∣∣Dn2∣∣Wn2∣∣Dn12∣∣Wn12∣∣D12∣∣W12

  其中 ∣ ∣ A ∣ ∣ 2 ||A||_2 ∣∣A2表示矩阵A的2范数,也叫谱范数,它的值为 λ 1 \sqrt {{\lambda _1}} λ1 λ 1 {\lambda _1} λ1 A H A {{\rm{A}}^H}{\rm{A}} AHA的最大特征值。 λ 1 \sqrt {{\lambda _1}} λ1 又称作矩阵A的奇异值**【注:奇异值是 A H A {{\rm{A}}^H}{\rm{A}} AHA的特征值的开根号,也就是说$\sqrt {{\lambda _1}} 为 A 的其中一个奇异值或谱范数是最大的奇异值】 ∗ ∗ < / f o n t > ,这里我们将谱范数,即最大的奇异值记作 为A的其中一个奇异值或谱范数是最大的奇异值】**</font>,这里我们将谱范数,即最大的奇异值记作 A的其中一个奇异值或谱范数是最大的奇异值】</font>,这里我们将谱范数,即最大的奇异值记作\sigma {(A)} = \sqrt {{\lambda _1}}$。由于D是对角矩阵且由0、1构成,其奇异值总是小于等于1,故有下式:

image-20221114155057050

  即 ∇ x f ( x ) ∣ ∣ 2 = ∣ ∣ D n ∣ ∣ 2 ⋅ ∣ ∣ W n ∣ ∣ 2 ⋯ ∣ ∣ D 1 ∣ ∣ 2 ⋅ ∣ ∣ W 1 ∣ ∣ 2 ≤ Π 1 n σ ( W i ) {\nabla _x}f(x)|{|_2}= ||{D_n}|{|_2}\cdot ||{W_n}|{|_2} \cdots ||{D_1}|{|_2} \cdot ||{W_1}|{|_2} \le \mathop \Pi \limits_1^{\rm{n}} \sigma ({W_i}) xf(x)2=∣∣Dn2∣∣Wn2∣∣D12∣∣W121Πnσ(Wi)。为满足Lipschitz连续条件,我们应该让 ∣ ∣ ∇ x f ( x ) ∣ ∣ 2 ≤ K ||{\nabla _x}f(x)|{|_2} \le K ∣∣xf(x)2K ,这里的K设置为1。那具体要怎么做呢,其实就是对上式做一个归一化处理,让每一层参数矩阵除以该层参数矩阵的谱范数,如下:

​   ∣ ∣ ∇ x f ( x ) ∣ ∣ 2 = ∣ D n ∣ ∣ 2 ⋅ ∣ ∣ W n ∣ ∣ 2 σ ( W n ) ⋯ ∣ ∣ D 1 ∣ ∣ 2 ⋅ ∣ ∣ W 1 ∣ ∣ 2 σ ( W 1 ) ≤ Π 1 n σ ( W i ) σ ( W i ) = 1 ||{\nabla _x}f(x)|{|_2} = |{D_n}|{|_2} \cdot \frac{{||{W_n}|{|_2}}}{{\sigma ({W_n})}} \cdots ||{D_1}|{|_2} \cdot \frac{{||{W_1}|{|_2}}}{{\sigma ({W_1})}} \le \mathop \Pi \limits_1^{\rm{n}} \frac{{\sigma ({W_i})}}{{\sigma ({W_i})}} = 1 ∣∣xf(x)2=Dn2σ(Wn)∣∣Wn2∣∣D12σ(W1)∣∣W121Πnσ(Wi)σ(Wi)=1

  这样,其实我们的Spectral Normalization原理就讲的差不多了,最后我们要做的就是求得每层参数矩阵的谱范数,然后再进行归一化操作。要想求矩阵的谱范数,首先得求矩阵的奇异值,具体求法我放在附录部分。

  但是按照正常求奇异值的方法会消耗大量的计算资源,因此论文中使用了一种近似求解谱范数的方法,伪代码如下图所示:

image-20221114164733970

  在代码的实战中我们就是按照上图的伪代码求解谱范数的,届时我们会为大家介绍。🍄🍄🍄


注:大家阅读这部分有没有什么难度呢,我觉得可能还是挺难的,你需要一些矩阵分析的知识,我已经尽可能把这个问题描述的简单了,有的文章写的很好,公式推导的也很详尽,我会在参考链接中给出。但是会涉及到最优化的一些理论,估计这就让大家更头疼了,所以大家慢慢消化吧!!!🍚🍚🍚在最后的附录中,我会给出本节内容相关的矩阵分析知识,是我上课时的一些笔记,笔记包含本节的知识点,但针对性可能不是很强,也就是说可能包含一些其它内容,大家可以选择忽略,当然了,你也可以细细的研究研究每个知识点,说不定后面就用到了呢!!!🥝🥝🥝

 

Spectral Normalization源码解析

源码下载地址:Spectral Normalization📥📥📥

  这个代码使用的是CIFAR10数据集,实现的是一般生成对抗网络的图像生成任务。我不打算再对每一句代码进行详细的解释,有不明白的可以先去看看我专栏中的其它GAN网络的文章,都有源码解析,弄明白后再看这篇你会发现非常简单。那么这篇文章我主要来介绍一下Spectral Normalization部分的内容,其相关内容在spectral_normalization.py文件中,我们理论部分提到Spectral Normalization关键的一步是求解每个参数矩阵的谱范数,相关代码如下:

def _update_u_v(self):
    u = getattr(self.module, self.name + "_u")
    v = getattr(self.module, self.name + "_v")
    w = getattr(self.module, self.name + "_bar")
    height = w.data.shape[0]
    for _ in range(self.power_iterations):
        u.data = l2normalize(torch.mv(w.view(height, -1).data, v.data))  
        v.data = l2normalize(torch.mv(torch.t(w.view(height,-1).data), u.data))

    sigma = u.dot(w.view(height, -1).mv(v))
    setattr(self.module, self.name, w / sigma.expand_as(w))
    
    
    
def l2normalize(v, eps=1e-12):
    return v / (v.norm() + eps)

  对上述代码做一定的解释,6,7,8,9,10行做的就是理论部分伪代码的工作,最后会得到谱范数sigma。11行为使用参数矩阵除以谱范数sigma,以此实现归一化的作用。【torch.mv实现的是矩阵乘法的操作,里面可能还有些函数你没见过,大家百度一下用法就知道了,非常简单】

其实关键的代码就这些,是不是发现特别简单呢🍸🍸🍸每次介绍代码时我都会强调自己动手调试的重要性,很多时候写文章介绍源码都觉得有些力不从心,一些想表达的点总是很难表述,总之,大家要是有什么不明白的就尽情调试叭,或者评论区留言,我天天在线摸鱼滴喔。⭐⭐⭐后期我也打算出一些视频教学了,这样的话就可以带着大家一起调试,我想这样介绍源码彼此都会轻松很多。🛩🛩🛩

 

小结

  Spectral Normalization确实是有一定难度的,我也有许多地方理解的也不是很清楚,对于这种难啃的问题我是这样认为的。我们可以先对其有一个大致的了解,知道整个过程,知道代码怎么实现,能使用代码跑通一些模型,然后考虑能否将其用在自己可能需要使用的地方,如果加入的效果不好,我们就没必要深究原理了,如果发现效果好,这时候我们再回来慢慢细嚼原理也不迟。最后,希望各位都能获取新知识,能够学有所成叭!!!🌹🌹🌹

 

参考链接

GAN — Spectral Normalization 🍁🍁🍁

Spectral Normalization for GAN🍁🍁🍁

详解GAN的谱归一化(Spectral Normalization)🍁🍁🍁

谱归一化(Spectral Normalization)的理解🍁🍁🍁

 

附录

  这部分是我学习矩阵分析这门课程时的笔记,截取一些包含此部分的内容,有需求的感兴趣的可以看一看。🌱🌱🌱

image-20221114213810323

image-20221114213405314

image-20221114213500828

 
 

如若文章对你有所帮助,那就🛴🛴🛴

         一键三连 (1).gif

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

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

相关文章

JavaEE-HTTP协议(二)

目录HTTP请求的方法GET方法POST 方法其他方法“报头”User-AgentRefererCookieHTTP响应200 OK404 Not Found403 Forbidden405 Method Not Allowed500 Internal Server Error504 Gateway Timeout302 Move temporarily301 Moved PermanentlyHTTP请求的方法 GET方法 GET 是最常用…

Jmeter之直连数据库框架搭建简介

案例简介 通过直连数据库让程序代替接口访问数据库&#xff0c;如果二者预期结果不一致&#xff0c;就找到了程序的缺陷。 下面通过一个案例分析讲解如何实现&#xff1a;获取某个字段值&#xff0c;放在百度上搜索。 实现方式 1、Jmeter本身不具备直连数据库的功能&#xf…

机器学习笔记之生成模型综述(四)概率图模型 vs 神经网络

机器学习笔记之生成模型综述——概率图模型vs神经网络引言回顾&#xff1a;概率图模型与前馈神经网络贝叶斯网络 VS\text{VS}VS 神经网络表示层面观察两者区别推断、学习层面观察两者区别引言 本节将介绍概率图模型与神经网络之间的关联关系和各自特点。 回顾&#xff1a;概率…

Javaweb安全——Dubbo 反序列化(一)

Dubbo 反序列化&#xff08;一&#xff09; Dubbo 基础 Apache Dubbo 是一款 RPC 服务开发框架。提供三个核心功能&#xff1a;面向接口的远程方法调用、智能容错和负载均衡&#xff0c;以及服务自动注册和发现。 节点角色 节点角色说明Provider暴露服务的服务提供者Consume…

leaflet 加载KML数据显示图形(方法3)

第061个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载kml文件,并解析后在地图上显示图形,这里是第三种方法,前两种方法请参考目录查询。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共81行…

大数据培训课程分享:Python数据分析与挖掘实战课程介绍

《Python数据分析与挖掘实战》课程内容以Python数据分析与挖掘的常用技术与真实案例相结合的方式&#xff0c;深入浅出地介绍Python数据分析与挖掘的重要内容&#xff0c;共分为基础篇&#xff08;第1~5章&#xff09;和实战篇&#xff08;第6~11章&#xff09;。 基础篇内容包…

Git 安装和使用(非常详细教程)

Git 安装和使用Tips 目录&#xff1a;导读 1. git的安装 1)首先去下载 2)傻瓜式下一步再下一步地去安装 2. git的常见命令 提交代码 下载代码 分支提交代码 3. git的常见问题 1) 提示出错信息&#xff1a;fatal: remote origin already exists. 2) 发现日志等文件没…

通过异常处理错误

写在前面Java的基本理念是"结构不佳的代码不能运行"。发现错误的理想时机是在编译阶段, 也就是在你试图运行程序之前。然而, 编译期间并不能找出所有的错误, 余下的问题必须在运行期间解决。这就需要错误源能通过某种方式, 把适当的信息传递给某个接收者——该接收者…

情人节特刊 | “恋爱容易,相守难!” 犀思老兵谈破局之道!

付出甘之如饴&#xff0c;所得归于欢喜。 主动付出真心&#xff0c;问心无愧&#xff0c;未来无悔。老吴是我们公司十多年经验的售后服务主管&#xff0c;平时聊的不多&#xff0c;中午一起吃饭&#xff0c;偶然看到新闻说春节后多地都有排队办理离婚的现象。我不禁感叹一句&am…

三种查找Windows10环境变量的方法

文章目录一.在设置中查看二. 在我的电脑中查看三. 在资源管理器里查看一.在设置中查看 在系统中搜索设置 打开设置&#xff0c;在设置功能里&#xff0c;点击第一项 系统 在系统功能里&#xff0c;左侧菜单找到关于 在关于的相关设置里可以看到高级系统设置 点击高级系…

Java如何整合FFmpeg、FFprobe等音视频处理工具,零基础照样玩

前言&#xff1a;时隔一年多了&#xff0c;不知不觉博客停更那么久了&#xff0c;那不忘初心还记得吗&#xff1f; 最近在做音视频相关的开发&#xff0c;没什么资料并且之前也没有接触过这方面&#xff0c; 咨询了T届的好友&#xff0c;拿到了下面的这张表情包&#xff0c;问题…

从事架构师岗位快2年了,聊一聊我和ChatGPT对架构的一些感受和看法

从事架构师岗位快2年了&#xff0c;聊一聊我和ChatGPT对架构的一些感受和看法 职位不分高低&#xff0c;但求每天都能有新的进步&#xff0c;永远向着更高的目标前进。 文章目录踏上新的征程架构是什么&#xff1f;架构师到底是干什么的&#xff1f;你的终极目标又是什么&#…

链表带头结点与不带头节点的区别

链表是一种物理存储结构上非连续&#xff0c;非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的 链表可分为&#xff1a;单链表和双链表&#xff0c;带头结点的链表和不带头结点的链表&#xff0c;循环链表和非循环链表 为了表示每个元素与其…

MySQL数据库调优————SQL性能分析

TIPS 本文基于MySQL 8.0 本文探讨如何深入SQL内部&#xff0c;去分析其性能&#xff0c;包括了三种方式&#xff1a; SHOW PROFILEINFORMATION_SCHEMA.PROFILINGPERFORMANCE_SCHEMA SHOW PROFILE SHOW PROFILE是MySQL的一个性能分析命令&#xff0c;可以跟踪SQL各种资源消耗。…

VLAN间通信,看完这个就完全懂了(单臂路由和三层交换)

第九章&#xff1a;实现VLAN间通信 划分VLAN后&#xff0c;由于广播报文只在同VLAN内转发&#xff0c;所以不同VLAN的用户间不能二层互访&#xff0c;这样能起到隔离广播的作用。但实际应用中&#xff0c;不同VLAN的用户又常有互访的需求&#xff0c;此时就需要实现不同VLAN的…

KDNM5000-10A-2剩余电流保护器测试仪

一、产品概述 KDNM5000-10A-2型剩余电流保护器测试仪(以下简称测试仪)&#xff0c;是本公司改进产品&#xff0c;是符合国家标准《剩余电流动作保护器》&#xff08;GB6829—95&#xff09;中第8.3条和GB16917.1—1997中第9.9条验证AC型交流脱扣器动作特性要求的专用测试仪器。…

Nacos微服务笔记

Nacos安装Nacos 的 Github&#xff08;Tags alibaba/nacos GitHub&#xff09;下载我们所需的 Nacos 版本&#xff0c;可以选择 windows 或者 Linux。 进入官网&#xff0c;选择合适版本&#xff0c;tar.gz为linux版本&#xff0c;zip为windows版本。下载并解压 nacos-server…

【C++进阶】一、继承(总)

目录 一、继承的概念及定义 1.1 继承概念 1.2 继承定义 1.3 继承基类成员访问方式的变化 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 五、继承与友元 六、继承与静态成员 七、菱形继承及菱形虚拟继承 7.1 继承的分类 7.2 菱形虚拟…

【SAP Abap】X-DOC:SE11 - 创建配置表并分配事务码

SE11 - 创建配置表并分配事务码1、创建自定义表2、创建表维护功能3、功能验证4、设置事务码5、带出字段默认值&#xff08;1&#xff09;方法一&#xff1a;表维护事件&#xff08;2&#xff09;方法二&#xff1a;屏幕事件1、创建自定义表 SE11&#xff0c;创建自定义表&…

结构体的三种定义方法、结构体类型名(可选标志符)什么时候可以省略

结构体的三种定义方法 一、单独定义&#xff1a; 先定义结构体类型&#xff0c;再定义变量   定义结构体的格式如下&#xff1a;    struct 结构体名 {    若干数据项&#xff1b;    } &#xff1b;   其中&#xff0c;struct为关键字&#xff1b; 结构体名是用户定…