【天生模子】Stable Diffusion原理+代码

[复制链接]
发表于 昨天 23:01 | 显示全部楼层 |阅读模式

文章目次



  • 前言
  • 一、Latent space
  • 二、AutoEncoder 和 VAE


    • 1.AutoEncoder:
    • 2.VAE:

  • 三、Diffusion扩散模子


    • 1.前向过程
    • 2.逆向过程

  • 四、多模态 cross attention
  • 五、Stable Diffusion原理


    • 1.训练过程:


      • ResnetBlock
      • Spatial Transformer(Cross Attention)
      • DownSample/UpSample

    • 2.前向过程

  • 六、代码分析*


    • 0.安装提示
    • 1.团体代码
    • 2.unet分析


      • 1、self.input_blocks
      • 2、middle_blocks
      • 3、self.output_blocks


  • 总结

前言

Stable diffusion是一个基于 Latent Diffusion Models(潜伏扩散模子,LDMs)的文图天生(text-to-image)模子。详细来说,得益于 Stability AI 的盘算资源支持和在 LAION-5B 的一个子集数据支持训练,用于文图天生。
Latent Diffusion Models 通过在一个潜伏表现空间中迭代“去噪”数据来天生图像,然后将表现效果解码为完备的图像,让文图天生可以大概在斲丧级GPU上,在10秒级别时间天生图片。现在,Stable Diffusion发布了v2版本。v1版是Latent Diffusion Models的一个详细实现,模子架构设置:主动编码器下采样因子为8,UNet巨细为860M,文本编码器为CLIP ViT-L/14。官方现在提供了以下权重


提示:以下是本篇文章正文内容,下面案例可供参考
一、Latent space

隐空间是压缩数据的一个表现。数据压缩的目标是学习数据中较紧张的信息。以编码器-解码器网络为例,起首使用全卷积神经网(FCN)络学习图片特性,我们将特性提取中对数据的降维看作一种有损压缩。由于解码器必要重修(reconstruct)数据,模子必须学习怎样储存全部相干信息而且忽略噪音,压缩(降维)的长处在于可以去掉多余的信息从而关注于最关键的特性。
二、AutoEncoder 和 VAE

1.AutoEncoder:

(1)AE是一个预训练的自编码器,优化目标是通过 Encoder 压缩数据,再通过decoder 还原数据,使得输入输出的数据只管雷同
(2)对于图像数据,decoder 还原数据可以看做是一个天生器,由于 decoder 输入数据z属于R空间,输入z的分布无法被固定住,以是大部门天生的图片是偶然义的。
2.VAE:

(1)给定输入解码器的z一个分布可以办理上述题目,假设一个服从标准多元高斯分布的多维随机变量的数据集X,根据已知分布采样得到的zi,来训练decoder神经网络,从而得到多元高斯分布的均值和方差,从而乐成得到一个逼近真实分布p(X)的p’(X)
(2)求解p’(X|z)的概率分布

(3)通过极大似然估计,最大化p’(X)的概率,但由于xi的维度很大,zi的维度也很大,必要准确找到与xi分布相干的zi,必要大量的采样,因此必要在encoder中引入后验分布p’(z|xi),让xi与zi关联起来
(4)使用encoder通过假设已知数据的分布,拟合其参数,从而逼近真实的后验分布p’(z|xi),在这里假设后验分布是基于多元高斯分布,则让encoder输出分布的均值和方差
(5)总体流程

三、Diffusion扩散模子

1.前向过程

   1.t 时候的分布便是 t-1 时候的分布+随机高斯分布的噪音,此中α是噪音的衰减值

2.恣意时候的分布 Xt ,都可以通过 X0 初始状态,以及步数盘算出来:

  2.逆向过程

已知 Xt,求初始状态的 X0,这里使用贝叶斯公式来推测 X0:
起首求已知 Xt 的分布求 **Xt-1**时候的分布 (详细推导见上篇博客) :

四、多模态 cross attention

Unet 中央层引入cross attention,参加多模态的条件(文本,种别,layout,mask),实现如下:此中Q来自latent spaceK,V来自文本等另一序列:


五、Stable Diffusion原理


1.训练过程:

(1)使用预训练的CLIP模子,对必要训练的图像数据天生对应的形貌词语。
(2)使用预训练的通用VAE,先用Encoder,将输入图片降维到 latent space(通常降采样倍数4-16)
(3) 将latent space输入diffusion model,举行扩散(正向采样),一步步天生噪声(在这个过程中,通过权重 β 控制每步天生噪声的强度,直到天生纯噪声,并纪录每步天生噪声的数据,作为GT
(4)使用cross attention 将 latent space的特性与另一模态序列的特性融合,并添加到diffusion model的逆向过程,通过Unet逆向推测每一步必要镌汰的噪音,通过GT噪音与推测噪音的丧失函数盘算梯度。
(5)此中Denoising Unet的布局如下:

ResnetBlock

左下角小图所示, ResnetBlock 继承两个输入:latent 向量颠末卷积变更后和颠末全毗连投影的 timestep_embedding 做加和,再和颠末 skip connection 的原始 latent 向量做加和,送入另一个卷积层,得到经 Resnet 编码变更后的 latent 输出。
注意左侧的 ResnetBlock 和右侧的 ResnetBlock 的渺小差别。左侧的 Resnet Block 继承的 latent 向量从 UNET 的上一层传入,而右侧的 ResnetBlock 除了继承 UNET 上一层的效果 latent 外,还必要继承左侧对应的 UNET 层的输出,两个 latent concat 起来作为 输入。以是,如果右侧的 ResnetBlock 上层的输出效果 shape 为 (64, 64, 320),左侧对应 UNET 层的输出效果为 (64, 64, 640),那么这个 ResnetBlock 得到的输入 latent 的 shape 为 (64, 64, 960)。
Spatial Transformer(Cross Attention)

右下角小图所示,Spatial Transformer 同样继承两个输入:颠末上一个网络模块(一样平常为 ResnetBlock)处理处罚和变更后的 latent 向量(对应的是是图片 token),及对应的 context embedding(文本 prompt 颠末 CLIP 编码后的输出), cross attention 之后,得到变更后的 latent 向量(通过注意力机制,将 token 对应的语义信息注入到模子以为应该影响的图片 patch 中)。 Spatial Transformer 输出的 shape 和输入划一,但在对应的位置上融合了语义信息。
DownSample/UpSample

DownSample 将 latent 向量的前两个轴的巨细缩减 50%,而 UpSample 将 latent 向量的前两个轴的巨细增大一倍。DownSample 使用一个步长为 2 的二维卷积来实现,同时将输入 latent 向量的 channel 数厘革成输出 latent 向量的 channel 数;而 UpSample 使用插值算法来实现,在插值之后举行一个步长为 1 的卷积,同时通过一个步长为 1 的二维卷积来将输入 latent 向量的 channel 数厘革成输出 latent 向量的 channel 数。
必要注意的是,在整个 UNET 实行一次的过程中,timestep_embedding 和 content embedding 始终保持稳固。而在 UNET 反复实行多次的过程中,timestep_embedding 每次都会发生厘革,而 content embedding 始终保持稳固。在迭代过程中,每次 UNET 输出的 noise_slice 都与原来 latent 向量相减,作为下次迭代时,UNET 的 Latent 输入。
2.前向过程

一个 Image Auto Encoder-Decoder,用于将 Image 编码成隐含向量
,大概从隐含向量
中还原出图片;
一个 UNET 布局,使用 UNET 举行迭代降噪,在文本引导下举行多轮推测,将随机高斯噪声
转化成图片隐含向量

   1.用文本编码器( CLIP 的 ViT-L/14 ),将用户输入的 Prompt 文本转化成 text embedding;
2.根据假定分布(一样平常是多元高斯分布),天生一张纯噪音图像;
3.使用VAE encoder 压缩到latent space;
4.实行Denoising Unet,使用cross attention融合多模态信息,并推测每一步必要减去的噪音:
5.使用VAE decoder还原到同一分布下的原图巨细
  六、代码分析*

0.安装提示

0.1 必要下载 transformers 和 diffusers
  1. pip install --upgrade diffusers
  2. pip install --upgrade diffusers transformers
复制代码
我的版本
  1. diffusers: 0.6.0
  2. transformers: 4.23.1
复制代码
0.2 注册Huggingface token
进入官网:https://huggingface.co/ ,右上角 settings(注册token,必要邮箱链接验证):


0.3 在终端登录:huggingface-cli login
下令行输入:huggingface-cli login,使用刚刚的token登岸
(输入的token不表现,返回successful即为乐成)

1.团体代码

  1. 1、prompt编码为token。编码器为FrozenCLIPEmbedde(包括1层的 CLIPTextEmbeddings 和12层的自注意力encoder)
  2. c = self.cond_stage_model.encode(c)    # (c为输入的提示语句,重复2次)  输出:(2,77,768)
  3.     batch_encoding = self.tokenizer(text, truncation=True, max_length=self.max_length, return_length=True,
  4.                                         return_overflowing_tokens=False, padding="max_length", return_tensors="pt")
  5.     # self.tokenizer来自 transformers包中的 预训练CLIPTokenizer
  6.     tokens = batch_encoding["input_ids"].to(self.device)             # (2,77)一句话编码为77维
  7.     outputs = self.transformer(input_ids=tokens).last_hidden_state   # 12层self-atten,结果(2,77,768)
  8. 2、samples_ddim, _ = sampler.sample(S=opt.ddim_steps,
  9.                                    conditioning=c,
  10.                                    batch_size=opt.n_samples,
  11.                                    shape=shape,
  12.                                    verbose=False,
  13.                                    unconditional_guidance_scale=opt.scale,
  14.                                    unconditional_conditioning=uc,
  15.                                    eta=opt.ddim_eta,
  16.                                    x_T=start_code)
  17.      01、self.make_schedule(ddim_num_steps=S, ddim_eta=eta, verbose=verbose)    # S=50
  18.      # 这一步是ddim中,预先register超参数,如a的连乘等
  19.      # Data shape for PLMS sampling is (2, 4, 32, 32)
  20.      02、samples, intermediates = self.plms_sampling(conditioning, size,
  21.                                                 callback=callback,
  22.                                                 img_callback=img_callback,
  23.                                                 quantize_denoised=quantize_x0,
  24.                                                 mask=mask, x0=x0,
  25.                                                 ddim_use_original_steps=False,
  26.                                                 noise_dropout=noise_dropout,
  27.                                                 temperature=temperature,
  28.                                                 score_corrector=score_corrector,
  29.                                                 corrector_kwargs=corrector_kwargs,
  30.                                                 x_T=x_T )
  31.           img = torch.randn(shape, device=device)    # (2,4,32,32)
  32.           for i, step in enumerate(iterator):
  33.                 index = total_steps - i - 1                                        # index=50-i-1, step=981
  34.                 ts = torch.full((b,), step, device=device, dtype=torch.long)       # [981,981]
  35.                 outs = self.p_sample_plms(img, cond, ts, index=index, use_original_steps=ddim_use_original_steps,
  36.                                           quantize_denoised=quantize_denoised, temperature=temperature,
  37.                                           noise_dropout=noise_dropout, score_corrector=score_corrector,
  38.                                           corrector_kwargs=corrector_kwargs,
  39.                                           unconditional_guidance_scale=unconditional_guidance_scale,
  40.                                           unconditional_conditioning=unconditional_conditioning,
  41.                                           old_eps=old_eps, t_next=ts_next)
  42.                     c_in = torch.cat([unconditional_conditioning, c])    # 添加一个空字符,与promt拼接
  43.                     e_t_uncond, e_t = self.model.apply_model(x_in, t_in, c_in).chunk(2)
  44.                           t_emb = timestep_embedding(timesteps, self.model_channels, repeat_only=False)    # timesteps:[981,981,981,981] -> (4,320)
  45.                           emb = self.time_embed(t_emb)           # 2*linear:(4,320) -> (4,1280)
  46.                           
  47.                           # unet中带入embed与prompt,具体代码见下节
  48.                           for module in self.input_blocks:
  49.                               h = module(h, emb, context)        # 输入(4,4,32,32) (4,1280) (4,77,768)
  50.                               hs.append(h)
  51.                           h = self.middle_block(h, emb, context)
  52.                           for module in self.output_blocks:
  53.                               h = th.cat([h, hs.pop()], dim=1)   # (4,1280,4,4) -> (4,2560,4,4)
  54.                               h = module(h, emb, context)
  55.                           return self.out(h)                     # (4,320,32,32)卷积为(4,4,32,32)
  56. 3、e_t_uncond, e_t = self.model.apply_model(x_in, t_in, c_in).chunk(2)   # 上步中得到的结果拆开:(2,4,32,32
  57.    e_t = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond)  # 用7.5乘以二者差距,再加回空语句生成的图
  58.    x_prev, pred_x0 = get_x_prev_and_pred_x0(e_t, index)                  # DDIM计算:e_t(2,4,32,32) index:49  -> (2,4,32,32)
  59. 4、x_samples_ddim = model.decode_first_stage(samples_ddim)    # (2,4,32,32)
  60.         h = self.conv_in(z)    # 卷积4->512
  61.         x = torch.nn.functional.interpolate(h, scale_factor=2.0, mode="nearest")  #(2,512,64,64)
  62.         h = self.up[i_level].block[i_block](h)    # 经过几次卷积与上采样
  63.         h = self.norm_out(h)   # (2,128,256,256)
  64.         h = nonlinearity(h)    # x*torch.sigmoid(x)
  65.         h = self.conv_out(h)   # conv(128,3) -》(2,3,256,256)
  66. 5、后处理
  67. x_samples_ddim = torch.clamp((x_samples_ddim + 1.0) / 2.0, min=0.0, max=1.0)
  68. x_samples_ddim = x_samples_ddim.cpu().permute(0, 2, 3, 1).numpy()
  69. x_checked_image, has_nsfw_concept = check_safety(x_samples_ddim)
  70. x_checked_image_torch = torch.from_numpy(x_checked_image).permute(0, 3, 1, 2)
  71. x_sample = 255. * rearrange(x_sample.cpu().numpy(), 'c h w -> h w c')
  72. img = Image.fromarray(x_sample.astype(np.uint8))
  73. img.save(os.path.join(sample_path, f"{base_count:05}.png"))
复制代码
2.unet分析

DDIM中的Unet 包罗输入模块中央模块输出模块三部门:
1、self.input_blocks

包罗12个差别的 TimestepEmbedSequential布局,下面摆列三种:
  1. 1、self.input_blocks
  2. ModuleList(
  3.   (0): TimestepEmbedSequential(
  4.     (0): Conv2d(4, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  5.   )
  6.   (1): TimestepEmbedSequential(
  7.     (0): ResBlock(
  8.       (in_layers): Sequential(
  9.         (0): GroupNorm32(32, 320, eps=1e-05, affine=True)
  10.         (1): SiLU()
  11.         (2): Conv2d(320, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  12.       )
  13.       (h_upd): Identity()
  14.       (x_upd): Identity()
  15.       (emb_layers): Sequential(
  16.         (0): SiLU()
  17.         (1): Linear(in_features=1280, out_features=320, bias=True)
  18.       )
  19.       (out_layers): Sequential(
  20.         (0): GroupNorm32(32, 320, eps=1e-05, affine=True)
  21.         (1): SiLU()
  22.         (2): Dropout(p=0, inplace=False)
  23.         (3): Conv2d(320, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  24.       )
  25.       (skip_connection): Identity()
  26.     )
  27.     (1): SpatialTransformer(
  28.       (norm): GroupNorm(32, 320, eps=1e-06, affine=True)
  29.       (proj_in): Conv2d(320, 320, kernel_size=(1, 1), stride=(1, 1))
  30.       (transformer_blocks): ModuleList(
  31.         (0): BasicTransformerBlock(
  32.           (attn1): CrossAttention(
  33.             (to_q): Linear(in_features=320, out_features=320, bias=False)
  34.             (to_k): Linear(in_features=320, out_features=320, bias=False)
  35.             (to_v): Linear(in_features=320, out_features=320, bias=False)
  36.             (to_out): Sequential(
  37.               (0): Linear(in_features=320, out_features=320, bias=True)
  38.               (1): Dropout(p=0.0, inplace=False)
  39.             )
  40.           )
  41.           (ff): FeedForward(
  42.             (net): Sequential(
  43.               (0): GEGLU(
  44.                 (proj): Linear(in_features=320, out_features=2560, bias=True)
  45.               )
  46.               (1): Dropout(p=0.0, inplace=False)
  47.               (2): Linear(in_features=1280, out_features=320, bias=True)
  48.             )
  49.           )
  50.           (attn2): CrossAttention(
  51.             (to_q): Linear(in_features=320, out_features=320, bias=False)
  52.             (to_k): Linear(in_features=768, out_features=320, bias=False)
  53.             (to_v): Linear(in_features=768, out_features=320, bias=False)
  54.             (to_out): Sequential(
  55.               (0): Linear(in_features=320, out_features=320, bias=True)
  56.               (1): Dropout(p=0.0, inplace=False)
  57.             )
  58.           )
  59.           (norm1): LayerNorm((320,), eps=1e-05, elementwise_affine=True)
  60.           (norm2): LayerNorm((320,), eps=1e-05, elementwise_affine=True)
  61.           (norm3): LayerNorm((320,), eps=1e-05, elementwise_affine=True)
  62.         )
  63.       )
  64.       (proj_out): Conv2d(320, 320, kernel_size=(1, 1), stride=(1, 1))
  65.     )
  66.   )
  67.   (6): TimestepEmbedSequential(
  68.     (0): Downsample(
  69.       (op): Conv2d(640, 640, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
  70.     )
  71.   )
复制代码
前向过程
为h添加emb和交与propmt的交织注意力,会实行多次
  1. emb_out = self.emb_layers(emb)      # (4,1280)卷积为(4,320)
  2. h = h + emb_out                     # (4,320,32,32)+(4,320,1,1)
  3. x = self.attn1(self.norm1(x)) + x                     # 自注意力:x(4,1024,320)映射到qkv,均320维
  4. x = self.attn2(self.norm2(x), context=context) + x    # 交叉注意力:context(4,77,768)映射到kv的320维
  5. x = self.ff(self.norm3(x)) + x
复制代码
噪音图像h(4,4,32,32)在此中厘革为:(4,320,32,32)(4,320,16,16)(4,640,16,16)(4,1280,8,8)(4,1280,4,4)
2、middle_blocks

  1. TimestepEmbedSequential(
  2.   (0): ResBlock(
  3.     (in_layers): Sequential(
  4.       (0): GroupNorm32(32, 1280, eps=1e-05, affine=True)
  5.       (1): SiLU()
  6.       (2): Conv2d(1280, 1280, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  7.     )
  8.     (h_upd): Identity()
  9.     (x_upd): Identity()
  10.     (emb_layers): Sequential(
  11.       (0): SiLU()
  12.       (1): Linear(in_features=1280, out_features=1280, bias=True)
  13.     )
  14.     (out_layers): Sequential(
  15.       (0): GroupNorm32(32, 1280, eps=1e-05, affine=True)
  16.       (1): SiLU()
  17.       (2): Dropout(p=0, inplace=False)
  18.       (3): Conv2d(1280, 1280, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  19.     )
  20.     (skip_connection): Identity()
  21.   )
  22.   (1): SpatialTransformer(
  23.     (norm): GroupNorm(32, 1280, eps=1e-06, affine=True)
  24.     (proj_in): Conv2d(1280, 1280, kernel_size=(1, 1), stride=(1, 1))
  25.     (transformer_blocks): ModuleList(
  26.       (0): BasicTransformerBlock(
  27.         (attn1): CrossAttention(
  28.           (to_q): Linear(in_features=1280, out_features=1280, bias=False)
  29.           (to_k): Linear(in_features=1280, out_features=1280, bias=False)
  30.           (to_v): Linear(in_features=1280, out_features=1280, bias=False)
  31.           (to_out): Sequential(
  32.             (0): Linear(in_features=1280, out_features=1280, bias=True)
  33.             (1): Dropout(p=0.0, inplace=False)
  34.           )
  35.         )
  36.         (ff): FeedForward(
  37.           (net): Sequential(
  38.             (0): GEGLU(
  39.               (proj): Linear(in_features=1280, out_features=10240, bias=True)
  40.             )
  41.             (1): Dropout(p=0.0, inplace=False)
  42.             (2): Linear(in_features=5120, out_features=1280, bias=True)
  43.           )
  44.         )
  45.         (attn2): CrossAttention(
  46.           (to_q): Linear(in_features=1280, out_features=1280, bias=False)
  47.           (to_k): Linear(in_features=768, out_features=1280, bias=False)
  48.           (to_v): Linear(in_features=768, out_features=1280, bias=False)
  49.           (to_out): Sequential(
  50.             (0): Linear(in_features=1280, out_features=1280, bias=True)
  51.             (1): Dropout(p=0.0, inplace=False)
  52.           )
  53.         )
  54.         (norm1): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)
  55.         (norm2): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)
  56.         (norm3): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)
  57.       )
  58.     )
  59.     (proj_out): Conv2d(1280, 1280, kernel_size=(1, 1), stride=(1, 1))
  60.   )
  61.   (2): ResBlock(
  62.     (in_layers): Sequential(
  63.       (0): GroupNorm32(32, 1280, eps=1e-05, affine=True)
  64.       (1): SiLU()
  65.       (2): Conv2d(1280, 1280, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  66.     )
  67.     (h_upd): Identity()
  68.     (x_upd): Identity()
  69.     (emb_layers): Sequential(
  70.       (0): SiLU()
  71.       (1): Linear(in_features=1280, out_features=1280, bias=True)
  72.     )
  73.     (out_layers): Sequential(
  74.       (0): GroupNorm32(32, 1280, eps=1e-05, affine=True)
  75.       (1): SiLU()
  76.       (2): Dropout(p=0, inplace=False)
  77.       (3): Conv2d(1280, 1280, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  78.     )
  79.     (skip_connection): Identity()
  80.   )
复制代码
3、self.output_blocks

与输入模块雷同,包罗12个 TimestepEmbedSequential,次序相反

总结

团体布局比力简单,先用预训练CLIP将prompt变为token; DDIM模子将噪音与token逆扩散为图像;再采取VAE的decoder将图像复原到正常巨细:
关于AI绘画技能储备

学好 AI绘画 岂论是就业还是做副业赢利都不错,但要学会 AI绘画 还是要有一个学习规划。末了各人分享一份全套的 AI绘画 学习资料,给那些想学习 AI绘画 的小搭档们一点资助!
对于0底子小白入门:
   如果你是零底子小白,想快速入门AI绘画是可以思量的。
  一方面是学习时间相对较短,学习内容更全面更会合。
二方面是可以找到恰当自己的学习方案
  包罗:stable diffusion安装包、stable diffusion0底子入门全套PDF,视频学习教程。带你从零底子体系性的学好AI绘画!

零底子AI绘画学习资源先容

👉stable diffusion新手0底子入门PDF👈

(全套教程文末领取哈)

👉AI绘画必备工具👈


👉AI绘画底子+速成+进阶使用教程👈

观看零底子学习视频,看视频学习是最快捷也是最有用果的方式,跟着视频中老师的思绪,从底子到深入,还是很容易入门的。

温馨提示:篇幅有限,已打包文件夹,获取方式在:文末
👉大厂AIGC实战案例👈

光学理论是没用的,要学会跟着一起敲,要动手实操,才气将自己的所学运用到现实当中去,这时间可以搞点实战案例来学习。

👉12000+AI关键词大合集👈


这份完备版的学习资料已经上传CSDN,朋侪们如果必要可以微信扫描下方CSDN官方认证二维码免费领取【包管100%免费】


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表