简介:压缩包内含一套去噪扩散概率模型(Diffusion Model)的Python实现,适合深度学习、计算机视觉方向的学生与算法工程师用于图像生成实验或二次开发。代码覆盖模型核心组件、训练工具与数据集加载逻辑,并针对CelebA-HQ、LSUN、CIFAR等常用数据集提供运行脚本,可帮助理解扩散模型的训练与采样流程。整个zip共20个文件,以17个.py代码文件为主,同时包含README说明、requirements依赖清单和png效果图,总大小约985KB,量级轻量、结构直观。目前已有325人浏览学习,适合作为课程设计、期末大作业或毕业设计的参考基线;代码采用参数化编程,注释明细,便于修改超参数和替换数据集,结合附带的预览图与说明文档可快速验证生成效果。
1. diffusion去噪扩散概率模型:这份Python代码包里藏着一个能训练的DDPM
很多人第一次接触diffusion去噪扩散概率模型是因为Stable Diffusion的生图效果,但真要复现一个能训练、能采样、能出图的扩散模型,大多数时候卡在论文公式和工程实现的缝隙里。这份资源不是零散的笔记,而是一个按可运行方式组织的Python工程,入口脚本、扩散工具、U-Net模型和数据接口都齐了,覆盖CIFAR、CelebA-HQ、LSUN三个数据集。适合做课程设计、期末大作业和毕业设计的计算机、电子信息、数学方向学生,也适合想从零跑通一次DDPM的从业者:直接照着入口脚本改参数,先跑通再回头读公式,比从零写整个框架省力得多。就算你之前习惯在Matlab里做算法仿真,这种参数化组织和清晰注释也能让你快速把思路迁到自己的工具链上。
2. 从加噪到去噪:DDPM前向过程与反向训练的核心公式
2.1 前向过程:为什么说扩散就是把图像一步步抹成噪声
拿到这个包,第一件事别急着跑训练,先把diffusion_utils.py里的变量名和公式对上。DDPM的前向过程是一条马尔可夫链:给定一张真实图像 x_0,每一步按 x_t = sqrt(1 - beta_t) * x_{t-1} + sqrt(beta_t) * z_t 注入高斯噪声 z_t。这里 beta_t 是预设的噪声权重,原论文用线性调度,从 beta_1 = 1e-4 一路加到 beta_T = 0.02,时间步 T 默认取 1000。也就是说,模型看到的不是"一步步变模糊",而是一张图在 1000 步内被逐渐淹没成标准正态噪声的过程。
你可能会问,为什么要搞 1000 步这么多?因为只有把加噪过程切得足够细,反向去噪时每一步的改动才足够小,神经网络才容易拟合。这个直觉贯穿整个DDPM设计。实际代码里不会真的逐步迭代 1000 次来制造训练样本,而是利用高斯噪声的可叠加性,直接写出从 x_0 到任意时刻 x_t 的闭合表达式:
# q_sample: 直接从 x_0 跳到任意时刻 t 的带噪样本,避免逐步迭代 def q_sample(x_start, t, noise, alphas_cumprod): # alphas_cumprod: 由 (1 - beta_t) 连乘得到的累计序列,长度等于 T sqrt_acp = torch.sqrt(alphas_cumprod[t])[:, None, None, None] sqrt_one_minus_acp = torch.sqrt(1.0 - alphas_cumprod[t])[:, None, None, None] return sqrt_acp * x_start + sqrt_one_minus_acp * noise这个函数是 diffusion_utils.py 里最底层的一段逻辑,我用 PyTorch 写法示意,资源包里的 TensorFlow 实现变量名和公式一一对应。q_sample 存在的意义是:训练时不用从 t=0 迭代到 t=999,而是随机抽一个时刻 t,一步算出 x_t。否则每个 batch 都跑 1000 步前向,训练速度会慢到完全没法用。参数上要注意三点:第一,alphas_cumprod 是提前算好的长度为 T 的向量,别在训练循环里动态计算;第二,t 通常按 batch 里的每个样本独立随机采样,形状要和 batch 维对齐;第三,noise 要每个样本单独采样,不能把同一个噪声广播到整个 batch,否则模型会学到样本间的伪相关性。
2.2 训练目标:预测噪声比预测图像更好收敛
如果把训练目标定成"直接预测 x_0",早期时间步的 x_t 里几乎全是噪声,回归目标方差巨大,网络很容易陷在模糊平均里。DDPM 的关键改动是把目标换成"预测这个时刻被注入的噪声"。网络不再猜完整图像,只回答"我手上这团带噪图里混了什么噪声",这件事在每一个时间步上都是良定义的。训练损失也因此异常简单,就是一个 MSE:
# p_losses: 训练时只做一件事,让预测噪声逼近真实噪声 def p_losses(denoise_fn, x_start, t, noise): x_noisy = q_sample(x_start=x_start, t=t, noise=noise) # 先加噪 noise_pred = denoise_fn(x_noisy, t) # U-Net 输出预测噪声 loss = F.mse_loss(noise, noise_pred) # 与真实噪声做 MSE return lossdenoise_fn 对应 models/nn.py 里的 U-Net,输入是带噪图像和时间步 t,t 先经过正弦位置编码,再和图像特征在通道维度上融合。为什么用 MSE 不用 L1?DDPM 原文在噪声预测上用的就是 MSE,配合 L2 损失,梯度在整个时间步上更平滑,训练过程更平稳。训练时每个样本随机抽一个 t,等于让同一个网络同时学会"去一点噪"和"去很多噪"两件事;推理时再把它们串成一个从纯噪声到图像的循环。实际训练中 loss 收敛到接近噪声方差附近就可以考虑采样本看看,不必死等它降到 0——它本来就降不到 0。
2.3 反向采样:从纯噪声逐步还原图像的迭代循环
推理阶段和训练完全反过来:从 x_T ~ N(0, I) 开始,按照逆过程 p(x_{t-1} | x_t) 一级一级去噪。DDPM 采样公式里,给定网络预测的噪声 eps,x_{t-1} 的均值可以解析地算出来,方差则是预先算好的后验方差。采样核心代码通常是这个样子:
# p_sample: 单步反向去噪,从 x_t 还原出 x_{t-1} def p_sample(model, x_t, t, alphas, alphas_cumprod, alphas_cumprod_prev): eps = model(x_t, t) mu = (x_t - (1 - alphas[t]) / torch.sqrt(1 - alphas_cumprod[t]) * eps) / torch.sqrt(alphas[t]) sigma = torch.sqrt((1 - alphas_cumprod_prev[t]) / (1 - alphas_cumprod[t]) * (1 - alphas[t])) return mu + sigma * torch.randn_like(x_t) # 最后一步 t=0 时 sigma 应强制为 0mu 公式直接来自 DDPM 论文的采样过程,分子里的 (1 - alphas[t]) 是当前步的噪声权重,分母里的 sqrt(1 - alphas_cumprod[t]) 用来把累积噪声归一化;sigma 是后验方差,在最后一个时间步 t=0 时没有随机噪声,必须把 sigma 强制置 0,否则生成结果会带一层甩不掉的颗粒感。这段逻辑在源码包里一般写在 diffusion_utils.py 的 p_mean_variance 附近,变量名可能是 alphas_cumprod_prev 和 posterior_variance,思路完全一致。整个采样循环就是从 T 递减到 0,把上一轮输出当成下一轮输入,最后拿到的 x_0 就是生成图。
2.4 调度参数与时间索引:两个经常被忽略的细节
很多人在小图上跑通了一次,换数据集就翻车,问题常出在 beta 调度和时间索引上。beta 调度不只有 linear 一种,后续工作里 cosine 调度也很常见,它在中间段加噪更缓和,高分辨率数据上更容易收敛。如果你在脚本参数里看到 beta_schedule 选项,默认 linear 可以先不动,但要知道把 T 从 1000 改成 500 时,alphas_cumprod 长度必须同步变,否则索引越界或采样公式错位。
另一个细节是 alphas_cumprod_prev 的移位处理。采样时计算 t-1 步的累积量,一般用 prev = cat([1, alphas_cumprod[:-1]]) 这种写法,让 t=0 时有 prev=1,这样最后一个时间步才能正确终止。很多魔改代码丢失这个 shift,生成结果会整体偏色。遇到这类问题,先打印 alphas_cumprod 最后几个值和 prev 的前几个值,一眼就能看出错没错。
2.5 为什么这份代码用TensorFlow实现:对复现的影响
资源包里的 diffusion_tf、tpu_utils.py、run_*.py 都指向 TensorFlow 实现风格,这也是 DDPM 早期开源生态的一个特点,不少论文官方代码就是 TF,后来大家才陆续移植到 PyTorch。对复现来说这不是阻碍:扩散模型的数学和网络架构都是通用的,TF 版本反而有一个好处,就是能直接沿用当年的 TPU 训练配置。tpu_utils.py 里那些分布式参数在单卡 GPU 上多数用不上,但它的存在提醒你,这套代码是按大规模训练设计的,不是玩具。
如果你本地只有一张消费级 GPU,建议把它当参考实现,重点读 diffusion_utils.py 和 models/nn.py 的逻辑。若实在不习惯 TF,把这两处改写成 PyTorch 是常规迁移路径,先跑通小数据集小分辨率,确认 loss 和采样行为正常,再往上扩规模。代码里 TensorFlow 的 Eager 模式和 PyTorch 的 eager 语义高度接近,逐个函数对照搬就行。
3. 源码包结构拆解:从文件组织看懂一条完整训练链路
3.1 解压后先认路:入口脚本与核心模块的分工
解压后先别急着装依赖,对着文件结构走一遍,把每个文件在训练链路里的位置标出来。这个包解压后大致是下面这个结构,个别发布版本可能略有出入:
diffusion_tf/ ├── models/ │ ├── __init__.py # 模型包导出 │ └── nn.py # U-Net 噪声预测网络 ├── diffusion_utils.py # 前向加噪、损失、采样核心工具 ├── diffusion_utils_2.py # 兼容层/补充工具,部分实现保留在这里 ├── tpu_utils.py # TPU 训练参数与分布式策略 ├── utils.py # 图像保存、日志、EMA 等通用函数 ├── scripts/ │ ├── run_cifar.py # CIFAR-10 训练与采样入口 │ ├── run_celebahq.py # CelebA-HQ 训练与采样入口 │ └── run_lsun.py # LSUN 训练与采样入口 ├── requirements.txt # 依赖清单 ├── README.md # 使用说明,先读它 └── samples.png # 参考生成结果,用来和你的输出对比models/nn.py 是 U-Net,输入带噪图 x_t 和时间步 t,输出与输入同尺寸的预测噪声。DDPM 用的 U-Net 比分割任务版本多了时间嵌入融合和更多残差块,中间层还加了 attention,参数规模通常在几十百万量级。diffusion_utils.py 是核心工具集合,训练时调 q_sample 和 p_losses,采样时调 p_sample,后文调参主要待的地方也是这里。diffusion_utils_2.py 从命名习惯看像是保留函数集的兼容层,如果主文件里某个函数在调用链上没找到定义,可以来这里翻一翻。
tpu_utils.py 只在 TPU 训练时才会真正参与,本机单卡训练可以直接忽略。utils.py 里通常装着 EMA 更新和图像保存逻辑,EMA 权重对采样质量影响非常大,后面避坑章会再提。scripts 目录下三个入口脚本分别对应三个数据集,命令行参数不一样,跑之前用 --help 确认。
3.2 调用链:一处训练step里nn.py与diffusion_utils.py如何配合
训练脚本里一个标准的 step 只做三件事:采样时间步、加噪、预测噪声求 loss。写成伪代码更直白:
# 训练循环中一次前向的典型顺序 x_noisy = q_sample(x_start, t, noise, alphas_cumprod) # 来自 diffusion_utils noise_pred = model(x_noisy, t) # 来自 models/nn.py loss = mean_squared_error(noise, noise_pred)这里的关键是模型完全不接收 x_0,只接收 x_noisy 和 t。所以它对加噪程度的感知全部来自时间嵌入,t 的编码方式和 U-Net 里的特征融合方式直接决定模型能不能区分"轻度噪声"和"重度噪声"。改分辨率时,U-Net 的输入通道不变,主要改中间特征维度和 attention 块数;改 T 时,只需要改 beta 调度长度和 alphas_cumprod,网络结构完全不用动。这个解耦设计是扩散模型工程实现里最省心的地方。
3.3 三个入口脚本:run_cifar、run_celebahq、run_lsun分别适合什么场景
| 脚本 | 数据集 | 图像特点 | 主要开销 | 适合用途 |
|---|---|---|---|---|
| run_cifar.py | CIFAR-10 | 32x32 小图 | 单卡可训练 | 快速验证原理、课设首选 |
| run_celebahq.py | CelebA-HQ | 1024 高清人脸 | 显存敏感 | 人脸生成、质量评估 |
| run_lsun.py | LSUN | 类别众多、量大 | 数据读取耗时长 | 类别条件生成实验 |
run_cifar.py 是新手第一站,小图训练速度快,跑一两个小时就能看到像样的轮廓。run_celebahq.py 和 run_lsun.py 在数据读取上走不同目录结构:CelebA-HQ 要求把图片按文件夹整理好并生成文件列表,LSUN 通常用 lmdb 格式,脚本一般支持 --category 指定类别,比如 bedroom、tower。跑之前先确认数据目录结构是不是脚本预期的那样,这一条在避坑章会展开。
3.4 samples.png的正确用法:不是装饰图,是验收基准
包里那张 samples.png 不是给你看效果的装饰图,而是验收基准。正式训练前先打开它,记住正常生成结果该长什么样:颜色分布自然、轮廓可辨认、没有大面积纯色块。训练过程中每隔一段时间跑一次采样,和 samples.png 并排对比。如果颜色分布接近,说明网络学到了数据分布;如果全是灰块或高频噪点,按第 5 章的排查顺序走,别急着调网络结构。
3.5 requirements.txt与README:安装和运行前先看这两个文件
很多下载资源的 README 被当成摆设,这个包不建议这样。README 里通常会写数据集准备的命令、版本要求、已知问题,比任何教程都贴近这个源码包的实际状态。requirements.txt 则锁定了 TensorFlow、NumPy、Pillow 等依赖的版本范围,Python 环境里版本冲突是最常见的安装失败原因。先建虚拟环境再装依赖,能避开和系统 Python 包之间的冲突。
4. 把环境搭起来并跑通CIFAR:依赖安装与参数选型
4.1 环境准备:虚拟环境、依赖与帮助命令
环境准备这一步值得多花十分钟,后面能省下几小时的排错时间。先把项目目录拿过来,建独立虚拟环境,装依赖,然后不要急着训练,先看一遍帮助信息:
cd diffusion_tf python -m venv venv source venv/bin/activate pip install -r requirements.txt python scripts/run_cifar.py --helpPython 版本建议 3.8 及以上,具体以 README 里写的为准。requirements.txt 里一般包含 TensorFlow、NumPy、Pillow 这些基础库,直接 pip 安装即可。--help 这一步是关键习惯:不同发布版本的脚本参数名可能有出入,比如有的叫 --data_dir,有的叫 --data_root,先确认再训练能少踩很多坑。如果装依赖时遇到 TensorFlow 版本和本机 CUDA 不匹配,优先看 README 是否给了指定版本,没有的话选择当前 Python 版本下安装最顺利的官方版本。
4.2 训练CIFAR:入口命令与参数含义
CIFAR-10 是 32x32 的小图,单张消费级显卡就能训练,适合做第一个完整实验。训练命令按这个模板改:
python scripts/run_cifar.py \ --data_dir=./data/cifar10 \ --out_dir=./checkpoints \ --batch_size=64 \ --num_epochs=100 \ --lr=2e-4脚本会先检查数据目录是否存在,不存在时可能自动下载 CIFAR-10,也可能提示你手动放置。参数含义按下面的表理解就够:
| 参数 | 典型值 | 作用与注意事项 |
|---|---|---|
| data_dir | ./data/cifar10 | 数据目录,目录不存在时脚本一般会提示下载方式 |
| out_dir | ./checkpoints | checkpoint 与日志输出目录,建议按实验时间命名 |
| batch_size | 64 | 单卡建议从 32 起步,OOM 时优先减小这一项 |
| num_epochs | 100 | DDPM 在小数据集上也需较多迭代,看 loss 收敛曲线判断 |
| lr | 2e-4 | 原版常用 2e-4,过高容易出现 nan loss |
训练过程中注意看两点:loss 是否整体向下走,以及日志里每个 epoch 的速度是否稳定。如果 loss 曲线平得像条直线,先确认数据加载是不是真的返回了图像,别在空数据上训练了一晚上。训练和采样一般共用同一个入口脚本,用 --mode 或单独子命令区分,具体以 --help 输出的参数为准。
4.3 采样与验收:如何确认模型真的学会了生成
训练到一半或者全部结束后,用采样模式生成一批图像:
python scripts/run_cifar.py \ --mode=sample \ --ckpt_dir=./checkpoints \ --sample_dir=./samples \ --num_samples=64采样命令通常不需要 data_dir,因为推理阶段只从随机噪声出发,不碰训练数据。脚本会从 checkpoint 恢复到指定 epoch,然后从纯噪声迭代 1000 步。检查输出时先看整体颜色分布,再看边缘锐利度:颜色分布对了说明网络学到了数据分布,边缘清楚说明高频细节也学到了。如果生成结果全是同一种颜色,或者所有图长得一模一样,基本是收敛异常或采样公式问题,去第 5 章对照排查。
4.4 换数据集:CelebA-HQ和LSUN的注意点
跑通 CIFAR 之后再换数据集,主要变化在分辨率和数据读取。下面这张表是我在单卡环境下的典型配置参考:
| 数据集 | 常用分辨率 | 单卡显存建议 | Batch Size 参考 |
|---|---|---|---|
| CIFAR-10 | 32x32 | 4 GB 以上 | 64 |
| CelebA-HQ | 128x128 或 256x256 | 12 GB 以上 | 16 或 8 |
| LSUN | 128x128 | 12 GB 以上 | 16 |
CelebA-HQ 原图是 1024x1024,直接把原图丢进去训练,普通消费级显卡根本顶不住。常见做法是先把图片缩放到 128x128 或 256x256,再做中心裁剪和归一化,run_celebahq.py 里的输入管线通常已经预留了 resize 配置,你只需要把目标分辨率参数填上。LSUN 的 bedroom 类有上百万张图,单机单卡全量训练要跑很久,常见做法是先用子集或低分辨率做冒烟测试,确认数据和模型都正常再上全量。
5. 避坑与常见问题排查:DDPM训练里最容易翻车的四个环节
扩散模型训练里的坑,很多属于"看着像玄学,其实都有迹可循"的类型。我按自己踩过的顺序整理四条,每一条都是现象、原因、解决三步走。
5.1 loss正常下降,采样结果却全是噪声或灰块
现象:训练日志里 loss 曲线稳定下滑,看起来一切正常,但跑出来的 samples 图是一堆高频噪点或者整片灰,几乎没有图像结构。
原因通常有两个。一是模型还在欠拟合阶段,CIFAR 上通常要跑比较多的迭代才能看到明显轮廓,训练几十个 epoch 就采样,结果当然是噪声。二是采样代码用了刚保存的原始权重,而不是 EMA 权重。原始权重是训练过程的实时快照,噪声大;EMA 权重是全程权重的滑动平均,推理时稳得多。解决:先确认 checkpoint 里保存的是否包含 EMA 参数,如果 utils.py 里有 ema 模块,采样前强制加载它。再检查 p_sample 里最后一个时间步的 sigma 是否被置零,如果没置零,最后一步的随机噪声会盖掉整张图。
5.2 单卡GPU训练直接OOM,显存完全不够
现象:训练脚本启动后没跑几个 step,直接报资源耗尽,进程退出。
原因:扩散模型同时保存输入图和多个中间特征,U-Net 的通道数、attention 层都比普通分类网络吃显存很多。很多人把 batch_size 设成分类任务的习惯值,比如 128,直接爆掉。解决:先把 batch_size 降到 16 甚至 8,确认能跑起来再往上加。如果还想提升训练效率,打开混合精度,在保留 float32 主权重的同时用 float16 计算;脚本没有内置的话,用梯度累积模拟大 batch,也就是多个小 batch 前向求平均后再反向更新。OOM 报错信息里会给出当前申请到的显存大小,拿它和显卡总显存对比,能粗略估算出下一步该调到多少。
5.3 CelebA-HQ与LSUN数据加载经常报路径或尺寸错误
现象:换到 CelebA-HQ 或 LSUN 后,训练脚本报文件找不到、lmdb 读取失败,或者图片尺寸不一致导致 reshape 出错。
原因:这两个数据集不是直接解压就能用的。CelebA-HQ 需要按目录整理图片并准备文件列表,LSUN 是 lmdb 格式且分多个类别,脚本预期的目录结构和网上直接下载的原始包经常对不上。解决:先按 README 或脚本顶部注释要求的目录结构放置数据。我一般会在正式训练前写一个几行的数据自检脚本,打印一个 batch 的 shape 和像素范围,确认没问题再跑。批量 resize 到统一分辨率,再存成 tfrecord 或 numpy 格式,能省后面很多事。
5.4 训练中后期频繁出现nan loss,模型直接崩掉
现象:loss 正常降了几千步,突然变成 nan,后续 step 全部异常,checkpoint 也无法继续加载。
原因:学习率偏高在深层网络里会放大梯度,diffusion 模型尤其敏感;混合精度下 float16 的 loss scaling 不够,分母里出现极小值会溢出;U-Net 某层归一化的 epsilon 默认值太小也会导致除零。解决:把学习率降到 1e-4 或 5e-5,先确认学习率调度没问题;如果用混合精度,把 loss scaling 初始值调大,并在反向更新前跳过 inf 梯度;在 nn.py 和 diffusion_utils.py 里把归一化层的 epsilon 从 1e-5 改成 1e-6 再试。大多数 nan 问题都能被这三步定位到。改完先重跑一小段,确认 loss 能跨过之前崩溃的步数阈值。
5.5 一类通用排查习惯:先缩小问题空间,再上全量训练
如果上面的现象都对不上,我习惯先做一个极端小的冒烟测试:把图像缩到 16x16 或 32x32,batch_size 设为 4,跑 20 个 epoch,看模型能不能在这么小的数据上过拟合。这个测试能一次性验证数据管线、模型前向、loss 计算、checkpoint 保存四条链路是否正常。如果小规模都跑不通,问题一定在代码层面;如果小规模能过拟合,全量跑不通,再往显存、数据量、学习率方向查。
这套检查顺序能筛掉大半无声的配置错误,比直接上全量训练然后盯着日志猜原因高效得多。
6. 进阶技巧:给DDPM接入DDIM采样与类别条件控制
6.1 把DDPM采样换成DDIM,用50步替代1000步
DDPM 采样要迭代 1000 步,生产环境里太慢。DDIM 的核心改动是把随机项系数设为可调,当 eta=0 时采样过程完全确定性,50 步左右就能生成质量可接受的图像。代码上只改采样函数一小段,训练完全不动:
# DDIM单步更新:eta=0 时完全确定,可用 50 步左右生成 def ddim_sample(model, x_t, t, acp_t, acp_prev, eta=0.0): eps = model(x_t, t) x0_pred = (x_t - torch.sqrt(1 - acp_t) * eps) / torch.sqrt(acp_t) sigma = eta * torch.sqrt((1 - acp_prev) / (1 - acp_t) * (1 - acp_t / acp_prev)) noise = torch.randn_like(x_t) if eta > 0 else 0 x_prev = torch.sqrt(acp_prev) * x0_pred + \ torch.sqrt(1 - acp_prev - sigma**2) * eps + sigma * noise return x_prev在 diffusion_utils.py 里另写一个 ddim_sample 函数,采样循环只改调用,训练代码一行都不用动。eta 调大能增加生成多样性,但也会引入随机颗粒感,常见做法是先设 0,确认效果后再微调。
6.2 给U-Net加类别条件
如果你想做"按指定标签生成",常见做法是把类别标签做 embedding,然后和 time embedding 相加后一起注入残差块。用 run_lsun.py 做类别实验时,这个入口本身就暗示了类别支持的路径。改动集中在一个位置:在模型 forward 里加 class_embedding,维度对齐 time embedding,后续融合方式照抄时间嵌入的融合逻辑。要注意类别数变化时 embedding 表也要同步重建,别沿用旧 checkpoint。
6.3 EMA权重的保存习惯
生成质量最稳的经验是:训练期维护一份 EMA 权重,checkpoint 主权重每 N 步保存一次,EMA 权重单独保存,评估或者采样本时全部用 EMA。这个习惯在 CIFAR 上可能区别不大,在 CelebA-HQ 这种高清数据集上差别非常明显。从那以后我每接手一个 diffusion 代码包,都强制先跑通一张图的过拟合冒烟测试,再改 batch 和分辨率上全量训练。这个习惯帮我避掉了无数次无声的配置错误。希望帮到你。
本文还有配套的精品资源,点击获取