一 三种模型对比
1 Transformer是一个基础架构,是许多现代AI模型的发送机
2 LLM和Stable Diffusion是两种不同的顶级车型,分别用于处理语言和图像
3 开源 是这些模型的发布和协作模式
二 下面我们详细拆解
2.1 Transformer 一切的基石
本质,一种神经网络架构 2017
不是具体的模型,而是一种设计思想,核心创新是自注意力机制,让模型在处理序列数据时,能动态的关注所有部分的重要关系,并行高效的学习。
类比:就像汽车的内燃机或电动平台。是一种基础技术,可以被用来制造各种不同类型的车。
影响:彻底改变了自然语言处理领域,并逐渐扩展到视觉,音频等多模态领域,当今绝大多数先进的LLM都是基于Transformer架构构建的。
2.2 LLM vs Stable Diffusion 不同赛道上的顶级选手
|-------|--------------------------------------------|-------------------------------------------------------------|
| 维度 | LLM | Stable Diffusion |
| 核心任务 | 理解和生成人类语言 文本,例如,对话,协作,翻译,代码生成。 | 生成和编辑图像,根据文本描述prompt 生成图片,或者对现有图片进行修改 |
| 技术基础 | 主要基于Transformer架构的解码器部分,如GPT系列,或者编码器-解码器部分 | 基于扩散模型架构,结合了Transformer 用于理解文本提示,和U-net 用于去噪生成图像 |
| 处理对象 | 离散的符号序列 | 连续的图像数据,像素矩阵 |
| 工作原理 | 根据上下文预测下一个最可能的词 token,自回归生成文本 | 从一个纯随机噪声开始,通过多步去噪过程,逐步将其塑造成符号文本描述的清晰图像 |
| 代表性模型 | GPT系列,LLaMA系列,ChatGLM,通义千问,文心一言 | Stable Diffusion系列及其变体,SDXL, SD 1.5等,DALL-E 2/3, Midjourney |
| 输出/输出 | 输入:文本提示,输出,文本 | 输入,文本提示 + 可选参数,输出图像 |
关键差异点
架构不同:LLM的核心是Transformer;Stable Diffusion的核心扩散模型,只是借用Transformer作为其文本理解器
数据模态不同:一个处理文本,一个处理图像
生成逻辑不通:LLM是从左到右的序列预测;Stable Diffusion时从噪声到清晰图的迭代去噪。
2.3 开源的角色,共同的催化剂
问题中的开源模型是一个关键属性。LLM、Stable Diffusion和Transformer本身都受益于开源运动,但程度和方式不同。
Transformer:其论文,<Attention Is All You Need> 是彻底开源的,代码实现被广泛公开,称为行业标准。
Stable Diffusion:由Stability AI公司主动开源 2002年,这一举动引爆了AI绘画革命,社区可以自由使用,修改,微调模型,催生了海量工具和生态
LLM:情况更复杂
闭源代表:OpenAI的GPT-4, Google的Gemini Advanced,仅仅提供API,模型细节不公开。
开源代表:Meta的LLaMA系列,Mistral AI的Mistral/Mixtral,国产的ChatGLM, Qwen, Baichuan等。这些模型公开了权重和架构,允许研究者和开发者本地部署,微调和研究。
开源的影响:对于LLM和Stable Diffusion, 开源极大的降低了应用门槛,促进了安全性研究,垂直领域适配,成本下降和技术民主化。
三 Stable Diffusion模型实现示例
#DDPM` `主程序入口`
`#` `这个文件是DDPM` `模型的启动脚本,负责`
`#` `1` `配置所有训练/评估参数`
`#` `2` `根据配置选择训练或评估模式`
`#` `3` `调用相应的训练或评估函数`
`#使用方法:Python Main.py`
`#使用默认配置训练或评估`
`from Diffusion.Train` `import` `train, eval`
`def main(model_config=None):`
`#主函数:配置参数并启动训练或评估`
`#` `args:model_config` `可选的配置字典,如果提供则覆盖默认配置`
`#模式配置字典`
`#包含所有训练和评估所需的超参数`
`modelConfig` `= {`
`#` `运行模式`
`"state":` `"train",` `#train` `或eval`
`#` `train` `训练模式,从头开始训练或继续训练模型`
`#` `eval` `评估模式,加载已训练模型并生成图像`
`#` `训练超参数`
`"epoch":` `200,` `#训练的总轮数`
`"batch_size":` `80,` `#每个批次的样本数量`
`"lr":` `1e-4,` `#初始学习率,learning` `rate`
`"multiplier":` `2.` `#学习率预热后的倍数,预热后学习率 =` `lr *` `multiplier`
`"grad_clip":` `1.` `#梯度裁剪阈值,防止梯度爆炸`
`#扩散过程参数`
`"T": 1000,` `#扩散步数` `前向过程的步数`
`#扩散过程` `x_0 -> x_1 -> ... -> x_T`
`#更多步数通常能生成更高质量的图像,但计算成本更高`
`"beta_1": 1e-4, # 初始噪声系数(第 1 步的噪声方差)`
` "beta_T": 0.02, # 最终噪声系数(第 T 步的噪声方差)`
`#beta_t` `定义了每一步添加的噪声量`
`#通常从很小的值` `1e-4` `线性增加到较大的值` `0.02`
`#这确保了前向过程能逐渐将图像变成纯噪声`
`#UNet模型架构参数`
`"channel": 128,` `#基础通道数,第一层的通道数`
`"channel_mult":` `[1,2,3,4]` `#通道倍数列表`
`#定义了下采样过程中通道数的变化`
`# 例如:[1, 2, 3, 4] 表示:`
` # - 第 1 层:128 通道`
` # - 第 2 层:128*2 = 256 通道`
` # - 第 3 层:128*3 = 384 通道`
` # - 第 4 层:128*4 = 512 通道`
`"attn":` `[2],` `#在哪些层级添加注意力机制`
`#例如,[2]表示在第2个分辨率级别添加注意力层`
`#注意力机制能帮助模型捕获长距离依赖,提升生成质量`
`"num_res_blocks": 2,` `#每个分辨率级别的残差块数量`
`#残差块是,UNet` `的基本构建单元,包含`
`#时间步嵌入,time embedding`
`#卷积层`
`#` `可选的注意力层`
`"dropout":` `0.15` `#Dropout` `比率,训练时的正则化`
`#Dropout` `随即将部分神经元输出置零,防止过拟合`
`#数组相关参数`
`"img_size":` `32,` `#图像尺寸`
`#设备配置`
`"device":` `"cuda:0",` `#计算设备`
`#cuda` `0使用第1块GPU如果有`
`#CPU` `使用CPU训练会很慢`
`#注意DDPM` `训练需要大量计算,强烈建议使用GPU`
`#模型权重相关`
`"training_load_weight": None, # 训练时加载的预训练权重文件名(None 表示从头训练)`
` "save_weight_dir": "./Checkpoints/", # 模型权重保存目录`
` "test_load_weight": "ckpt_199_.pt", # 评估时加载的模型权重文件名`
`#图像保存相关`
` "sampled_dir": "./SampledImgs/", # 生成图像的保存目录`
` "sampledNoisyImgName": "NoisyNoGuidenceImgs.png", # 初始噪声图像文件名`
` "sampledImgName": "SampledNoGuidenceImgs.png", # 生成图像文件名`
` "nrow": 8, # 保存图像时每行显示的图像数量 `
`}`
`#如果提供了自定义配置,则使用自定义配置覆盖默认值`
`if model_config is` `not None:`
`modelCOnfig.update(model_config)`
`#根据配置选择运行模式`
`if ` `modelConfig["state"]` `== "train":`
`train(modelConfig)`
`else`
`eval(modelConfig) `
`if __name__` `==` `'__main__':`
`#程序入口`
`#当直接运行此脚本时,而非作为模块导入,执行main()函数`
`main()`
`
四 训练代码
#DDPM` `Denoising` `Diffusion` `Probabilistic` `训练和评估模块`
`#` `DDPM` `核心思想`
`#前向过程` `Forward Process` `逐步向真实图像添加高斯噪声,直到变成纯噪声`
`#q(x_t | x_{t-1})` `= N` `(x_t; sqrt(1 - beta_t) * x_{t-1}, beta_t * I)` `经过T步后` `x_t` `近似为标准高斯分布N(0, I)`
`#` `反向过程,` `Reverse` `Process` `训练神经网络学习从噪声逐步去噪,恢复原始图像` `p_theta(x_{t-1} | x_t)` `= N(x_{t-1}; )mu_theta(x_t, t), sigma_t^2 * I)`
`#` `模型预测每一步的噪声,然后从x_t 中减去预测的噪声得到x_{t-1}`
`#` `训练目标,最小化预测噪声和真实噪声之间的MSE损失,`
`#L=` `E[||epsilon - epsilon_theta(x_t, t)||^2]`
`#其中,` `epsilon` `是前向过程中添加的真实噪声,eplison_theta` `是模型预测的噪声`
`import os`
`from typing` `import Dict`
`import` `torch`
`import` `torch.optim` `as optim`
`from` `tqdm` `import tqdm`
`from` `torch.utils.data` `import DataLoader`
`from torchvision` `import transforms`
`from torchvision.datasets` `import` `CIFAR10`
`from torchvision.utils` `import save_iamge`
`from Diffusion` `import GaussianDiffusionSampler, GaussianDiffusionTrainer`
`from Diffusion.Model` `import UNet`
`from Scheduler import GradualWarmupScheduler`
`def train(modelConfig: Dict):`
`#` `DDPM` `模型训练函数`
`#` `训练流程`
`#` `1 加载数据集CIFAR-10`
`#` `初始化` `UNet模型` `用于预测噪声`
`#` `设置优化器和学习率调度器`
`#` `使用` `GaussianDiffusionTrainer` `进行训练循环`
`#随机采样时间步`
`#根据t` `向图像添加噪声得到x_t`
`#模型预测噪声,计算损失并反向传播`
`#args:` `modelCOnfig:` `包含所有训练配置参数的字典`
`#设置计算设备,GPU或CPU`
`device` `= torch.device(modelConfig["device"])`
`#数据集准备`
`#加载CIFAR-10训练集`
`#CIFAR-10` `10类型色图像,每张图像32x32像素`
`dataset` `= CIFAR10(`
`root=./CIFAR10,` `数据集保存路径`
`train =` `True, 使用训练集`
`download=True,` `如果不存在则自动下载`
`transform` `= transforms.Compose([`
`transforms.RandomHorizontalFlip(), #随机水平翻转,数据增强`
`transforms.ToTensor(), #转换为张量]0, 1[`
`#归一化到[-1, 1]范围,因为DDPM` `通常在这个范围内工作`
`#mean = (0.5, 0.5, 0.5),` `std=(0.5, 0.5, 0.5)` `将[0, 1]` `映射到` `[-1, 1]`
`transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),`
`]))`
`#创建数据加载器`
`dataloader = DataLoader(`
`dataset,`
`batch_size=modelConfig["batch_size"], #批次大小`
`shuffle=True, #每个epoch` `打乱数据`
`num_workers=4,` `#数据加载的并行进程数`
`drop_last=True,` `#丢弃最后一个不完整的批次`
`pin_memory=True` `#将数据固定在内存中,加速GPU传输`
`)`
`#模型初始化`
`#创建UNet模型`
`#UNet` `是DDPM` `的核心网络,用于预测每一步噪声`
`#输入,带噪声的图像x_t和时间步t`
`#` `输出:预测的噪声epsilon_theta(x_t, t)`
`net_model` `= UNet(`
`T =` `modelConfig["T"],` `扩散步数` `通常为1000`
`ch = modelConfig["channel"],` `#基础通道数`
`ch_mult=modelConfig["channel_mult"]` `#通道倍数列表,如[1,2,3,4]表示下采样时通道数逐渐增加`
`attn = modelConfig["attn"]` `#在哪些层级添加注意力机制`
`num_res_blocks=modelConfig["num_res_blocks"]` `#每个分辨率级别的残差块数量`
`dropout=modelConfig["dropout"]` `#Dropout` `比率,防止过拟合`
`).to(device)`
`#如果指定了预训练权重,则加载,用于继续训练`
`if modelConfig["trainning_load_weight"]` `is` `not None:`
`net_model.load_state_dict(torch.load(ps.path.join(`
`modelConfig["save_weight_dir"],` `modelConfig["trainning_load_weight"],`
`map_location=device`
`)))`
`#优化器设置`
`#使用Adam` `优化器的改进版本,权重衰减更合理`
`optimizer = torch.optim.AdamW(`
`net_model.parameters(),`
`lr=modelConfig["lr"],初始学习率`
`weight_decay=1e-4` `#L2正则化系数`
`)`
`#学习率调度器设置`
`#余弦退火调度器,学习率按余弦函数从最大值降到最小值`
`cosineScheduler =` `optim.lr_scheduler.CosineAnnealingLR(`
`optimizer=optimizer,`
`T_max =modelConfig["epoch"],` `#余弦周期的长度,总epoch数`
`eta_min=0,` `#最小学习率`
`last_epoch=-1` `#从初始学习率开始`
`)`
`#预热调度器,在训练初期逐渐增加学习率,然后切换到余弦退火`
`#这有助于训练初期的稳定性`
`warmUpScheduler=GradualWarmupScheduler(`
`optimizer=optimizer,`
`multiplier` `= modelConfig["multiplier"],` `#预热后学习率时初始学习率的倍数`
`warm_epoch=modelConfig["epoch"]` `// 10,` `#预热epoch数总epoch的10%`
`after_scheduler=cosineScheduler #预热后使用的调度器`
`)`
`#扩散训练器初始化`
`# # GaussianDiffusionTrainer 封装了 DDPM 的前向扩散过程`
`#随机采样时间步` `t`
`#根据t向图像添加噪声得到x_t`
`#模型预测噪声,计算损失并反向传播`
`#args` `modelConfig` `包含所有训练配置参数的字典`
`#设置计算设备GPU或CPU`
`device =` `torch.device(modelConfig["device"])`
`#数据集准备`
`#加载CIFAR` `10训练集`
`#CIFAR-10 10类彩色图像,每张图像32x32像素`
`dataset = CIFAR10(`
`root='./CIFAR10', #数据集保存路径`
`train = True,` `#使用训练集`
`download=True,` `#如果不存在则自动下载`
`transform=transforms.Compose([`
`transforms.RandomHorizontaFlip(), #随机水平翻转,数据增强`
`transforms.ToTensor(),` `#转换为张量` `0, 1`
`#归一化到[-1, 1]范围,因为DDPM通常在这个范围工作`
`#mean=(0.5, 0.5, 0.5)`
`transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),`
`])`
`#创建数据加载器`
`dataloader` `= DataLoader(`
`dataset, `
`batch_szie=modelConfig["batch_size"], #批次大小`
`shuffle=True, 每个Epoch打乱数据`
`num_workers=4, #数据加载的并行进程数`
`drop_last=True,` `丢弃最后一个不完整的批次`
`pin_memory=True` `#将数据固定在内存中,加速GPU传输`
`)`
`#模型初始化`
`#创建UNet模型`
`#输入:带噪声的图像x_t和时间步t`
`#输出` `预测的噪声epsilon` `theta`
`net_model = UNet(`
`T=modelConfig T` `扩散步数`
`ch=modelConfig` `channel` `基础通道数`
`ch_mult=modelConfig` `channel_mult 通道倍数列表,如` `1,2,3,4` `表示下采样时通道数逐渐增加`
`attn = modelCOnfig` `attn` `#在哪些层级添加注意力机制`
`num_res_blocks=modelConfig` `[num_res_blocks]` `#每个分辨率级别的残差块数量`
`dropout` `= modelConfig["dropout"]` `#Dropout比率,防止过拟合`
`).to(device)`
`#如果指定了预训练权重,则加载` `用于继续训练`
`if modelConfig["training_load_weight"]` `is not None:`
`net_model.load_state_dict(torch.load(os.path.join(`
`modelConfig["save_weight_dir"],` `modelConfig["training_load_weight"],`
`map_location=device`
`)))`
`#优化器配置`
`#使用AdamW优化器,Adam的改进版本,权重衰减更合理`
`optimizer =` `torch.optim.AdamW(`
`net_model.parameters(),`
`lr=modelConfig["lr"], #初始学习率`
`weight_decay=1e-4 #L2正则化系数`
`)`
`#学习率调度器设置`
`#余弦退火调度器,学习率按余弦函数从最大值降到最小值`
`cosineScheduler = optim.lr_scheduler.CosineAnnealingLR(`
`optimizer=optimizer,`
`T_max=modelConfig["epoch"], #余弦周期的长度总epoch数`
`eta_min = 0, #最小学习率`
`last_epoch=-1` `#从初始学习率开始`
`)`
`#预热调度器,在训练初期逐渐增加学习率,然后切换到余弦退火`
`#这有助于训练初期的稳定性`
`warmUpScheduler` `= GraduaWarmupScheduler(`
`optimizer=optimizer,`
`multiplier=modelConfig["multiplier"],` `预热后学习率是初始学习率的倍数`
`warm_epoch=modelConfig["epoch"]` `// 10, #预热epoch数`
`after_scheduler=cosineScheduler` `#预热后使用的调度器`
`)`
`#` `扩散训练器初始化`
`#GaussianDiffusionTrainer` `封装了DDPM的前向扩散过程`
`#会在训练时`
`#随机采样时间步` `t` `~` `Uniform`
`#根据t计算噪声系数,向x_0` `添加噪声得到`
`#3返回预测噪声和真实噪声之间的损失`
`trainer` `= GaussianDiffusionTrainer(`
`net_model,` `#UNet模型`
`modelConfig["beta_1"]` `初始噪声系数,通常很小,如1e-4`
`modelConfig["beta_T"],` `最终噪声系数,`
`modelCOnfig["T"]` `扩散步数`
`).to(device)`
`#开始训练循环`
`for e in range(modelConfig["epoch"]):`
`#使用tqdm显示训练进度条`
`with` `tqdm(dataloader, dynamic_ncols=True)` `as tqdmDataLoader:`
`for images, labels in` `tqdmDataLoader:`
`#清零梯度,`
` optimizer.zero_grad()`
`#将图像移到指定设备,GPU`
`x_0 = images.to(device)` `#x_0` `是原始干净图像,形状,batch_size,` `3, 32, 32`
`#前向传播,计算损失`
`#trainer` `x_0 内部会`
`#1 随机采样时间步t`
`#2 采样噪声` `epsilon`
`#3` `计算x_t=sqrt(alpha_bar_t)` `* x_0` `+ sqrt(1 - alpha_bar_t) * epsilon`
`#模型预测噪声` `epsilon_theta = model(x_t, t)`
`#计算MSE损失loss` `= ||epsilon - epsilon_theta||^2`
`#返回每个样本的损失,形状,batch_size`
`loss` `= tranier(x_0).sum()/1000.` `求和后除以1000进行缩放数值稳定性`
`#反向传播,计算梯度`
`loss.backward()`
`#梯度裁剪,防止梯度爆炸`
`#将所有参数的梯度范围裁剪到grad_clip以内`
`torch.nn.utils.clip_grad_norm_(`
`net_model.parameters(),`
`modelCOnfig["grad_clip"]`
`)`
`#更新模型参数`
`optimizer.step()`
`#更新进度条显示信息`
`tqdmDataLoader.set_postfix(ordered_dict=(`
`"epoch":` `e, #当前epoch`
`"loss:": loss.item(),` `#当前批次损失`
`"img shape ":` `x_0.shape, # 图像形状`
`"LR": optimizer.state_dict()['param_groups'][0]["lr"] # 当前学习率`
`))`
`#每个epoch结束后更新学习率`
`warmUpScheduler.step()`
`#保存模型检查点`
`#每个epoch` `都保存,方便后续选择最大模型或继续训练`
`torch.save(net_model.state_dict(), os.path.join(`
` modelConfig["save_weight_dir"], 'ckpt_' + str(e) + "_.pt")) `
`def` `eval(modelConfig: Dict):`
`DDPM` `模型评估/采样函数`
`评估流程,实际上是生成新图像`
`1加载训练好的模型`
`2` `从标准高斯分布采样初始噪声x_T `
`3 使用GaussianDiffusionSampler逐步去噪`
`从t=T 到t=0.,逐步预测并去除噪声`
`最终得到生成的图像x_0`
`#args: modelCOnfig` `包含所有评估配置参数的字典`
`#使用torch.no_grad()` `禁用梯度计算,节省内存和加速`
`with` `torch.no_grad():`
`device=torch.device(modelCOnfig["device"])`
`#加载训练好的模型`
`#创建模型结构, 评估时dropout设为0`
`model=UNet(`
`T=modelConfig["T"], `
` ch=modelConfig["channel"], `
` ch_mult=modelConfig["channel_mult"], `
` attn=modelConfig["attn"],`
` num_res_blocks=modelConfig["num_res_blocks"], `
` dropout=0. # 评估时不需要 dropout`
`)`
` # 加载模型权重`
` ckpt = torch.load(os.path.join(`
` modelConfig["save_weight_dir"], modelConfig["test_load_weight"]), `
` map_location=device)`
` model.load_state_dict(ckpt)`
` print("model load weight done.")`
`#设置为评估模式,影响batchNorm, Dropout等层的行为`
`model.eval()`
`#初始化采样器`
`#GaussianDiffusionSampler` `封装了DDPM的反向去噪过程`
`#会在采样时`
`# 1. 从 x_T ~ N(0, I) 开始`
` # 2. 从 t=T 到 t=1,逐步预测噪声并去噪`
` # 3. 最终得到生成的图像 x_0`
`sampler=GaussianDiffusionSampler(`
` model, `
` modelConfig["beta_1"], `
` modelConfig["beta_T"], `
` modelConfig["T"]`
` ).to(device)`
` # ========== 生成图像 ==========`
` # 从标准高斯分布采样初始噪声`
` # 这是反向过程的起点:x_T ~ N(0, I)`
` noisyImage = torch.randn(`
` size=[modelConfig["batch_size"], 3, 32, 32], # 形状: [batch_size, channels, height, width]`
` device=device`
` )`
` # 保存初始噪声图像(用于可视化对比)`
` # 将 [-1, 1] 范围转换回 [0, 1] 用于保存`
` saveNoisy = torch.clamp(noisyImage * 0.5 + 0.5, 0, 1)`
` save_image(saveNoisy, os.path.join(`
` modelConfig["sampled_dir"], modelConfig["sampledNoisyImgName"]), `
` nrow=modelConfig["nrow"]) # nrow: 每行显示的图像数量`
` # 执行采样过程:从噪声逐步去噪生成图像`
` # sampler 内部会执行 T 步去噪:`
` # for t in [T, T-1, ..., 1]:`
` # 预测噪声 epsilon_theta = model(x_t, t)`
` # 计算 x_{t-1} 的均值`
` # 采样 x_{t-1} ~ N(mean, var)`
` # 最终得到 x_0`
` print("Starting sampling process...")`
` sampledImgs = sampler(noisyImage)`
` # 将生成的图像从 [-1, 1] 范围转换到 [0, 1] 范围,便于保存和可视化`
` sampledImgs = sampledImgs * 0.5 + 0.5 # [0 ~ 1]`
` # 保存生成的图像`
` save_image(sampledImgs, os.path.join(`
` modelConfig["sampled_dir"], modelConfig["sampledImgName"]), `
` nrow=modelConfig["nrow"])`
` print(f"Sampling completed! Images saved to {modelConfig['sampled_dir']}") `
`)`
`