GAN(Generative Adversarial Nets)

news2024/12/23 9:40:26

GAN(Generative Adversarial Nets)

引言

GAN由Ian J. Goodfellow等人提出,是Ian J. Goodfellow的代表作之一,他还出版了大家耳熟能详的花书(Deep Learning深度学习),GAN主要的思想是同时训练两个模型,生成模型G用于获取数据分布,判别模型D用于估计样本来自训练数据而不是G的概率。G的训练过程是最大化D犯错误的概率。这个过程对应于一个极小极大两人博弈。在任意函数G和D的空间中,存在唯一解,其中G恢复训练数据分布,并且D处处等于1/2。

用一个警察与小偷的故事来阐述:假设一个城市里有许多小偷,在这些小偷中,部分是技艺高超的偷窃高手,另一部分则是毫无技术的新手。警察开始进行对小偷的抓捕,其中一批“学艺不精”的小偷就被捉住了。这些小偷被抓住或许是因为识别他们毫无难度,警察不需要特殊本领,但是剩下的“偷窃高手”警察就很难抓捕。于是警察们开始继续训练自己的破案技术,开始抓住那些技艺高超的小偷。随着这些他们的落网,警察们也练就了特别的本事,他们能很快能从一群人中识别终逮捕嫌犯;随着警察们的水平大大提高,为了避免被捕,小偷们努力表现得不那么“可疑”,而魔高一尺、道高一丈,警察也在不断提高自己的水平,争取将小偷和无辜的普通群众区分开。随着警察和小偷之间的这种“交流”与“切磋”,小偷们都变得非常谨慎,他们有着极高的偷窃技巧,表现得跟普通群众一模一样,而警察们都练就了“火眼金睛”,一旦发现可疑人员,就能马上发现并及时控制——最终,我们同时得到了最强的小偷和最强的警察。其中,小偷就可以视作生成模型G,警察可以视作判别模型D,通过G和D的对抗,能够获得效果较好的生成模型(判别模型也是如此)参考链接

主要架构

根据引言,一个GAN主要包含两个基础模型:生成器(G)与判别器(D)。其中,生成器用于生成新数据,其生成数据的基础往往是一组噪音或者随机数,而判别器用于判断生成的数据和真实数据哪个才是真的。生成器执行无监督任务;而判别器执行有监督任务,用于二分类,其label是“假与真”(0与1)。
在这里插入图片描述

生成器的目标是生成尽量真实的数据(这也是我们对生成对抗网络的要求),最好能够以假乱真、让判别器判断不出来,因此生成器的学习目标是让判别器上的判断准确性越来越低;相反,判别器的目标是尽量判别出真伪,因此判别器的学习目标是让自己的判断准确性越来越高。

当生成器生成的数据越来越真时,判别器为维持住自己的准确性,就必须向判别能力越来越强的方向迭代。当判别器越来越强大时,生成器为了降低判别器的判断准确性,就必须生成越来越真的数据。在这个奇妙的关系中,判别器与生成器同时训练、相互内卷,对损失函数的影响此消彼长。参考链接

理论支撑

m i n G m a x D V ( D , G ) = E x ∼ p d a t a ( x ) [ l o g D ( x ) ] + E z ∼ p z ( z ) [ l o g ( 1 − D ( G ( z ) ) ) ] \underset{G}{min}\underset{D}{max}V(D,G) = {\mathbb E_{x \sim{p}_{data}(x)} [logD(x)]} + {\mathbb E _{z \sim{p}_{z}(z)}[{log(1-D({G(z)}))}]} GminDmaxV(D,G)=Expdata(x)[logD(x)]+Ezpz(z)[log(1D(G(z)))]
V V V是一个值函数(损失函数), x x x表示真实数据, p d a t a p_{data} pdata表示数据的真实分布, z z z是与真实数据相同分布的随机数据, G ( z ) G(z) G(z)是生成器中基于 z z z生成的数据, D ( x ) D(x) D(x)是判别器在真实数据 x x x上判断的结果, D ( G ( x ) ) D(G(x)) D(G(x))表示判别器在生成器生成的数据 G ( z ) G(z) G(z)上判断出的结果。

那么需要做的就是:

1. 对于判别器D来说,尽可能找到生成器生成的数据

2. 对于生成器G来说,尽可能让生成的数据接近真实数据,使得判别器D无法判别出来

上面表达式需要做的是,首先固定G,在D的层面使得值最大(即让判别器能够精确区分真实数据和生成数据),然后固定D,在G的层面使得值最小(即在判别器能够精确区分数据的情况下,让生成器能够生成更接近真实的数据,使得判别器无法区分),从而实现了D和G的对抗,如此可以找到最好的生成器(生成模型)。
在这里插入图片描述

图(a)中展示了生成器G、判别器D以及真实数据初始状态,此时真实数据与生成数据分布明显不同,判别器此时也只是初始状态;图(b)展示了判别器经过训练后能够进行区分真实数据和生成数据;图©展示了生成器经过训练后能够更加接近真实分布;图(d)展示了经过多次循环之后,生成器和判别器的状态,此时生成数据已经无限接近真实数据分布,同时判别器难以区分出真实数据和生成数据,导致判别答案始终为1/2。

算法

在这里插入图片描述
算法存在的一个问题是需要选择一个较好的k,在算法中要保证:不能一次性让判别器就能够准确的识别出所有生成数据,这会导致生成器没有办法继续提升,生成更加接近真实分布的数据,同时也不能让生成模型一下子生成非常接近真实分布的数据,这会导致判别器难以进行识别能力的提升。

公式分析

对于判别器D

判别器的作用是尽可能找出生成器生成的数据与真实数据分布之间的差异,这是一个二分类的问题,将G固定后,公式就变为:
m a x D V ( D , G ) = E x ∼ p d a t a ( x ) [ l o g D ( x ) ] + E z ∼ p z ( z ) [ l o g ( 1 − D ( G ( z ) ) ) ] \underset{D}{max}V(D,G) = {\mathbb E_{x \sim{p}_{data}(x)} [logD(x)]} + {\mathbb E _{z \sim{p}_{z}(z)}[{log(1-D({G(z)}))}]} DmaxV(D,G)=Expdata(x)[logD(x)]+Ezpz(z)[log(1D(G(z)))]
该公式等价于交叉熵,只不过交叉熵是取负的对数。这个函数的输入一部分是真实数据,分布为 p d a t a {p_{data}} pdata,一部分是生成器的数据(噪声数据),生成器接收的数据 z z z服从分布 p ( z ) p(z) p(z),输入 z z z经过生成器的计算生产的数据分布设为 p G ( x ) {p_{G}}(x) pG(x),这个函数要取得最大值,必然是对于真实数据 D ( x ) = 1 D(x)=1 D(x)=1,对于生成数据 D ( x ) = 0 D(x)=0 D(x)=0,这一步用于优化D,因此可以简写为
D G ∗ = m a x D V ( G , D ) D_G^* = \underset D {max}V(G,D) DG=DmaxV(G,D)
此时,这是D的一元函数,进行求导,得到
在这里插入图片描述
取导数为0,算最优点得到
在这里插入图片描述

对于生成器G

当且仅当 P G ( x ) = P d a t a ( x ) {P_G}(x) = {P_{data}}(x) PG(x)=Pdata(x)时,有
D G ∗ = P d a t a ( x ) P G ( x ) + P d a t a ( x ) = 1 2 D_G^* = \frac{{{P_{data}}(x)}}{{{P_G}(x) + {P_{data}}(x)}} = \frac{1}{2} DG=PG(x)+Pdata(x)Pdata(x)=21
此时生成器无法判别数据是真实数据或者生成数据。
我们假设 P G ( x ) = P d a t a ( x ) {P_G}(x) = {P_{data}}(x) PG(x)=Pdata(x),可以反向推出
V ( G , D G ∗ ) = ∫ x P d a t a ( x ) l o g 1 2 + P G ( x ) l o g ( 1 − 1 2 ) d x {V(G,D_G^*) = \int_x {{P_{data}}(x)log\frac{1}{2}}+ {P_G}(x)log(1 - \frac{1}{2})dx} V(G,DG)=xPdata(x)log21+PG(x)log(121)dx
⇔ V ( G , D G ∗ ) = − log ⁡ 2 ∫ x P G ( x ) d x − log ⁡ 2 ∫ x P d a t a ( x ) d x = − 2 log ⁡ 2 = − log ⁡ 4 \Leftrightarrow {V(G,D_G^*) = - \log 2\int\limits_x {{P_G}(x)} dx - \log 2\int\limits_x {{P_{data}}(x)} dx = - 2\log 2 = - \log 4} V(G,DG)=log2xPG(x)dxlog2xPdata(x)dx=2log2=log4
该值是全局最小值的候选,因为它只有在 P G ( x ) = P d a t a ( x ) {P_G}(x) = {P_{data}}(x) PG(x)=Pdata(x)
的时候才出现。
对于任意一个G,将 D ∗ {D^*} D带入到 V ( G , D ) V(G,D) V(G,D)中:
在这里插入图片描述

结合KL散度得到:
= − 2 l o g 2 + K L ( P d a t a ( x ) ∣ ∣ P d a t a ( x ) + P G ( x ) 2 ) + K L ( P G ( x ) ∣ ∣ P d a t a ( x ) + P G ( x ) 2 ) { = - 2log2 + KL({P_{data}}(x)||\frac{{{P_{data}}(x) + {P_G}(x)}}{2}) + KL({P_G}(x)||\frac{{{P_{data}}(x) + {P_G}(x)}}{2})} =2log2+KL(Pdata(x)∣∣2Pdata(x)+PG(x))+KL(PG(x)∣∣2Pdata(x)+PG(x))
最后根据JS散度得到:
V ( G , D ) = − log ⁡ 4 + 2 ∗ J S D ( P d a t a ( x ) ∣ P G ( x ) ) V(G,D) = - \log 4 + 2*JSD({P_{data}}(x)|{P_G}(x)) V(G,D)=log4+2JSD(Pdata(x)PG(x))
根据他的属性:当 P G ( x ) = P d a t a ( x ) {P_G}(x) = {P_{data}}(x) PG(x)=Pdata(x)
时,
为0。综上所述,生成分布当前仅当等于真实数据分布式时,我们可以取得最优生成器。前后逻辑自洽。

注:
对于判别器D的优化:这是一个二分类,满足 y l o g q + ( 1 − y ) l o g ( 1 − q ) ylogq+(1-y)log(1-q) ylogq+(1y)log(1q),对于x,标签只会为1,因此只有log(D(x))这一项;对于g(z),其标签只会为0,因此只有log(1-D(G(z)))这一项,因此可以有损失函数:
l o s s = c r o s s E n t r o p y L o s s ( D ( x ) , 1 ) + c r o s s E n t r o p y L o s s ( D ( x ) , 0 ) loss = crossEntropyLoss(D(x),1)+crossEntropyLoss(D(x),0) loss=crossEntropyLoss(D(x),1)+crossEntropyLoss(D(x),0)
对于生成器G的优化:因为D(x)这一项,并不包含生成器的优化参数,因此在求梯度的时候D(x)这一项为0,因此只有log(1-D(G(z)))这一项,损失函数:
l o s s = c r o s s E n t r o p y L o s s ( D ( G ( z ) ) , 1 ) loss = crossEntropyLoss(D(G(z)),1) loss=crossEntropyLoss(D(G(z)),1)

代码

import argparse
import os
import numpy as np
import math

import torchvision.transforms as transforms
from torchvision.utils import save_image

from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable

import torch.nn as nn
import torch.nn.functional as F
import torch

os.makedirs("images", exist_ok=True)

parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=50, help="number of epochs of training")
parser.add_argument("--batch_size", type=int, default=64, help="size of the batches")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=28, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval betwen image samples")
#使用jupyter时需要传入list,
opt = parser.parse_args(args=[])
#opt = parser.parse_args()
#print(opt)

#图像形状为:1*28*28,图像大小为784
img_shape = (opt.channels, opt.img_size, opt.img_size)

#这里提前做了一下cuda的判断
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#生成器G,用于进行数据的生成
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        
        #定义模型的中间块
        def block(in_feat, out_feat, normalize=True):
            layers = [nn.Linear(in_feat, out_feat)] #多个线性层的组合
            if normalize:
                layers.append(nn.BatchNorm1d(out_feat, 0.8)) #进行正则化
            layers.append(nn.LeakyReLU(0.2, inplace=True)) #激活函数
            return layers
        
        #多个模型块进行组合(MLP),输入维度的映射过程为:input_dim->128->256->512->1024->1*28*28
        self.model = nn.Sequential(
            *block(opt.latent_dim, 128, normalize=False),
            *block(128, 256),
            *block(256, 512),
            *block(512, 1024),
            nn.Linear(1024, int(np.prod(img_shape))),
            nn.Tanh() #Tanh取值范围为-1,1,即将输出映射到-1,1
        )

    def forward(self, z):
        img = self.model(z) #对噪声数据处理进行数据生成
        img = img.view(img.size(0), *img_shape) #生成数据为(batch_size,1,28,28)
        return img


#判别器D,用于区分真实数据和生成数据
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        #维度映射过程:1*28*28->512->256->1,使用sigmoid进行二分类激活,映射为0,1之间的数
        self.model = nn.Sequential(
            nn.Linear(int(np.prod(img_shape)), 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1),
            nn.Sigmoid(),
        )

    def forward(self, img):
        img_flat = img.view(img.size(0), -1) #将输入展平
        validity = self.model(img_flat) #进行判别

        return validity


# Loss function
adversarial_loss = torch.nn.BCELoss() #损失函数使用BCE(二分类交叉熵)

# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()

if cuda:
    generator.to(device)
    discriminator.to(device)
    adversarial_loss.to(device)

# Configure data loader
os.makedirs("./data/mnist", exist_ok=True)
#数据集加载
dataloader = torch.utils.data.DataLoader(
    datasets.MNIST(
        "./data/mnist",
        train=True,
        download=True,
        transform=transforms.Compose(
            [transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
        ),
    ),
    batch_size=opt.batch_size,
    shuffle=True,
)

# Optimizers
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))


# ----------
#  Training
# ----------

for epoch in range(opt.n_epochs):
    for i, (imgs, _) in enumerate(dataloader):

        # Adversarial ground truths
        valid = torch.tensor([[1.0]] * imgs.size(0), requires_grad=False).to(device) #定义真实数据标号为1
        fake = torch.tensor([[0.0]] * imgs.size(0), requires_grad=False).to(device)  #定义虚假数据标号为0

        # Configure input
        real_imgs = torch.tensor(imgs.type(torch.Tensor)).to(device)

        # -----------------
        #  Train Generator
        # -----------------

        optimizer_G.zero_grad()

        # Sample noise as generator input
        z = torch.tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim)), dtype=torch.float32).to(device) #随机生成噪声


        # Generate a batch of images
        gen_imgs = generator(z) #生成数据

        # Loss measures generator's ability to fool the discriminator
        g_loss = adversarial_loss(discriminator(gen_imgs), valid)

        g_loss.backward()
        optimizer_G.step()

        # ---------------------
        #  Train Discriminator
        # ---------------------

        optimizer_D.zero_grad()

        # Measure discriminator's ability to classify real from generated samples
        real_loss = adversarial_loss(discriminator(real_imgs), valid)
        fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)
        d_loss = (real_loss + fake_loss) / 2

        d_loss.backward()
        optimizer_D.step()

        print(
            "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
            % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
        )

        batches_done = epoch * len(dataloader) + i
        if batches_done % opt.sample_interval == 0:
            save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)

参考

参考:
文章1
文章2
文章3
代码

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

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

相关文章

CentOS 7 安装并部署 Mysql

安装 Mysql 下载并添加库 sudo yum localinstall https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm安装 Mysql 包(一路键入y即可) yum -y install mysql mysql-server --nogpgcheck- -nogpgcheck 作用为 禁掉GPG验证检查 配…

Serilog文档翻译系列(七) - 应用设置、调试和诊断、开发接收器

01应用设置 Serilog 支持在 App.config 和 Web.config 文件中使用简单的 配置语法,以设置最低日志级别、为事件添加额外属性以及控制日志输出。 Serilog 主要通过代码进行配置,设置支持旨在作为补充功能。虽然不是全面的,但大多数日志记录配…

SpringBoot3实战:实现接口签名验证

有时候我们要把自己的服务暴露给第三方去调用,为了防止接口不被授权访问,我们一般采用接口签名的方式去保护接口。 接下来松哥和大家聊一聊这个话题。 一 场景分析 什么时候需要接口签名? 接口签名是一种重要的安全机制,用于确…

Jmeter链接数据库、分布式

目录 一、Jmeter链接数据库 连接准备 有两种添加驱动的方法 第一种: 第二种: 连接方法: 1.先添加一个配置元件中的jdbc connection configuration 2、配置内容 使用: 二、Jmeter做分布式操作 1、准备多台电脑 2、多台电…

顶象生僻字点选模型识别

注意,本文只提供学习的思路,严禁违反法律以及破坏信息系统等行为,本文只提供思路 如有侵犯,请联系作者下架 该文章模型已经上线ocr识别网站,欢迎测试!!,地址:http://yxlocr.nat300.top/ocr/textclick/5 某网站使用顶象的生僻字点选模型,部分数据集如下: 这种数据集…

【Vue3 + TS + Vite】从0到1搭建后台管理系统

前言 没搭建过Vue3的项目,从0开始搭建一下,记录一下自己的步骤。 技术栈: vue3 ts scss pinia vite 我尽量写的详细一些,后续也会记录我在项目过程中,遇到的一些问题。 文章目录 前言环境搭建一、创建项目1. 使用…

使用IOT-Tree Server制作一个边缘计算设备(Arm Linux)

最近实现了一个小项目,现场有多个不同厂家的设备,用户需要对此进行简单的整合,并实现一些联动控制。 我使用了IOT-Tree Server这个软件轻松实现了,不外乎有如下过程: 1)使用Modbus协议对接现有设备&#…

探索循环神经网络RNN:解锁序列数据的奥秘

在这个数据驱动的时代,机器学习模型已经深入到我们生活的方方面面,从智能推荐系统到自然语言处理,无一不彰显其强大的能力。在众多模型中,循环神经网络(Recurrent Neural Network, RNN)以其独特的结构和对序…

Java日志(总结)

一、logback日志 Logback是由log4j创始人设计的又一个开源日记组件。logback当前分成三个模块:logback-core,logback- classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个 改良版本。此外logback-classic完整实现SLF4J …

elasticsearch创建索引

1对比关系型数据库,创建索引就等同于创建数据库 在postman中,向ES服务器发PUT请求 显示已经创建成功了 http://192.168.1.108:9200/shopping 请求方式get http://192.168.1.108:9200/shopping 请求全部的index的url地址 get 请求 http://192.168.1.10…

OpenHarmony(鸿蒙南向开发)——轻量系统内核(LiteOS-M)【扩展组件】

往期知识点记录: 鸿蒙(HarmonyOS)应用层开发(北向)知识点汇总 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~ 持续更新中…… C支持 基本概念 C作为目前使用最广泛的编程语言之一,…

同样的颜色在iOS和Flutter中显示不一样?色域差异解析

同样的颜色在iOS和Flutter中显示不一样?色域差异解析 在移动应用开发中,颜色的一致性对于提供良好的用户体验至关重要。然而,开发者有时会遇到一个令人困惑的问题:为什么同样的颜色代码在iOS的xib和Flutter的Container中显示的效…

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-09

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-09 目录 文章目录 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-09目录1. Can LLMs plan paths with extra hints from solvers?摘要创新点算法模型实验效果重要数据与结论 推荐阅读指数 2. Sc…

数据库软题8-数据库的控制功能

一、事务管理 题1-事物的四个特性(原子、一致、隔离、永久) 1.隔离性 2.持久性 3.原子性 二、数据库的备份与恢复 题1-数据库恢复就是回到故障发生前的状态 题2 题3 三、并发控制 题1-排它锁 选D题2.共享锁排他锁 1. 加了排他锁,不能再加…

毕设 深度学习语义分割实现弹幕防遮(源码分享)

文章目录 0 简介1 课题背景2 技术原理和方法2.1基本原理2.2 技术选型和方法 3 实例分割4 实现效果最后 0 简介 今天学长向大家分享一个毕业设计项目 毕业设计 深度学习语义分割实现弹幕防遮(源码分享) 🧿 项目分享:见文末! 1 课题背景 弹幕是显示在视频上的评论…

设计模式、系统设计 record part04

结构型模式 结构型模式分为: 1.类结构型模式 2.对象结构型模式 3。类结构型,使用继承机制,耦合度高,不灵活 4.对象结构型,使用组合、聚合关系,耦合低,灵活 代理模式 1.代理就是中介 2.静态代理&…

64.DDR3读写控制器的设计与验证(1)(MIG IP核的配置)

(1)DRAM-动态随机存储器,SDRAM-同步动态随机存储器 DDR3 SDRAM- 第三代双倍速率同步动态随机存储器 双倍速率指的是时钟上升沿和下降沿都可以传输数据。同步指的是数据写入或读取时,是按时钟同步的。动态指的是硬件使用电容去存…

C# 自适应屏幕分辨率

一、新增AutoSizeFormClass.cs class AutoSizeFormClass{//(1).声明结构,只记录窗体和其控件的初始位置和大小。public struct controlRect{public int Left;public int Top;public int Width;public int Height;}//(2).声明 1个对象//注意这里不能使用控件列表记录 List nCtr…

云手机哪款好用?2024年云手机推荐对比指南

随着云手机市场的快速扩展,消费者在选择云手机时面临着众多选择。为了帮助大家找到最适合自己的云手机,小编特意整理了一份当前市场上几款备受关注的云手机品牌对比,大家一起往下看吧。 1. Ogphone云手机 Ogphone云手机是近年来海外业务版块迅…

图解C#高级教程(五):枚举器和迭代器

本章主要介绍 C# 当中枚举器、可枚举类型以及迭代器相关的知识。 文章目录 1. 枚举器和可枚举类型2. IEnumerator 和 IEnumerable 接口2.1 IEnumerator 接口2.2 IEnumerable 接口 3. 泛型枚举接口4. 迭代器4.1 使用迭代器创建枚举器4.2 使用迭代器创建可枚举类4.3 迭代器作为属…