扩散模型课程第一单元第二部分:扩散模型从零到一

前言

于 11 月底正式开课的扩散模型课程正在火热进行中,在中国社区成员们的帮助下,我们组织了「抱抱脸中文本地化志愿者小组」并完成了扩散模型课程的中文翻译,感谢 @darcula1993、@XhrLeokk、@hoi2022、@SuSung-boy 对课程的翻译!

如果你还没有开始课程的学习,我们建议你从 第一单元:扩散模型简介 开始。

扩散模型从零到一

这个 Notebook 我们将展示相同的步骤(向数据添加噪声、创建模型、训练和采样),并尽可能简单地在 PyTorch 中从头开始实现。然后,我们将这个「玩具示例」与 diffusers 版本进行比较,并关注两者的区别以及改进之处。这里的目标是熟悉不同的组件和其中的设计决策,以便在查看新的实现时能够快速确定关键思想。

让我们开始吧!

有时,只考虑一些事务最简单的情况会有助于更好地理解其工作原理。我们将在本笔记本中尝试这一点,从“玩具”扩散模型开始,看看不同的部分是如何工作的,然后再检查它们与更复杂的实现有何不同。

你将跟随本文的 Notebook 学习到

  • 损坏过程(向数据添加噪声)

  • 什么是 UNet,以及如何从零开始实现一个极小的 UNet

  • 扩散模型训练

  • 抽样理论

然后,我们将比较我们的版本与 diffusers 库中的 DDPM 实现的区别

  • 对小型 UNet 的改进

  • DDPM 噪声计划

  • 训练目标的差异

  • timestep 调节

  • 抽样方法

这个笔记本相当深入,如果你对从零开始的深入研究不感兴趣,可以放心地跳过!

还值得注意的是,这里的大多数代码都是出于说明的目的,我不建议直接将其用于您自己的工作(除非您只是为了学习目的而尝试改进这里展示的示例)。

准备环境与导入:

!pip install -q diffusers
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
from diffusers import DDPMScheduler, UNet2DModel
from matplotlib import pyplot as plt

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f'Using device: {device}')

数据

在这里,我们将使用一个非常小的经典数据集 mnist 来进行测试。如果您想在不改变任何其他内容的情况下给模型一个稍微困难一点的挑战,请使用 torchvision.dataset,FashionMNIST 应作为替代品。

dataset = torchvision.datasets.MNIST(root="mnist/", train=True, download=True, transform=torchvision.transforms.ToTensor())
train_dataloader = DataLoader(dataset, batch_size=8, shuffle=True)
x, y = next(iter(train_dataloader))
print('Input shape:', x.shape)
print('Labels:', y)
plt.imshow(torchvision.utils.make_grid(x)[0], cmap='Greys');

该数据集中的每张图都是一个数字的 28x28 像素的灰度图,像素值的范围是从 0 到 1。

损坏过程

假设你没有读过任何扩散模型的论文,但你知道这个过程会增加噪声。你会怎么做?

我们可能想要一个简单的方法来控制损坏的程度。那么,如果我们要引入一个参数来控制输入的“噪声量”,那么我们会这么做:

noise = torch.rand_like(x)

noisy_x = (1-amount)*x + amount*noise

如果 amount = 0,则返回输入而不做任何更改。如果 amount = 1,我们将得到一个纯粹的噪声。通过这种方式将输入与噪声混合,我们将输出保持在相同的范围(0 to 1)。

我们可以很容易地实现这一点(但是要注意 tensor 的 shape,以防被广播 (broadcasting) 机制不正确的影响到):

def corrupt(x, amount):
  """Corrupt the input `x` by mixing it with noise according to `amount`"""
  noise = torch.rand_like(x)
  amount = amount.view(-1, 1, 1, 1) # Sort shape so broadcasting works
  return x*(1-amount) + noise*amount

让我们来可视化一下输出的结果,以了解是否符合我们的预期:

# Plotting the input data
fig, axs = plt.subplots(2, 1, figsize=(12, 5))
axs[0].set_title('Input data')
axs[0].imshow(torchvision.utils.make_grid(x)[0], cmap='Greys')

# Adding noise
amount = torch.linspace(0, 1, x.shape[0]) # Left to right -> more corruption
noised_x = corrupt(x, amount)

# Plottinf the noised version
axs[1].set_title('Corrupted data (-- amount increases -->)')
axs[1].imshow(torchvision.utils.make_grid(noised_x)[0], cmap='Greys');

当噪声量接近 1 时,我们的数据开始看起来像纯随机噪声。但对于大多数的噪声情况下,您还是可以很好地识别出数字。你认为这是最佳的吗?

模型

我们想要一个模型,它可以接收 28px 的噪声图像,并输出相同形状的预测。一个比较流行的选择是一个叫做 UNet 的架构。最初被发明用于医学图像中的分割任务,UNet 由一个“压缩路径”和一个“扩展路径”组成。“压缩路径”会使通过该路径的数据被压缩,而通过“扩展路径”会将数据扩展回原始维度(类似于自动编码器)。模型中的残差连接也允许信息和梯度在不同层级之间流动。

一些 UNet 的设计在每个阶段都有复杂的 blocks,但对于这个玩具 demo,我们只会构建一个最简单的示例,它接收一个单通道图像,并通过下行路径上的三个卷积层(图和代码中的 down_layers)和上行路径上的 3 个卷积层,在下行和上行层之间具有残差连接。我们将使用 max pooling 进行下采样和 nn.Upsample 用于上采样。某些比较复杂的 UNets 的设计会使用带有可学习参数的上采样和下采样 layer。下面的结构图大致展示了每个 layer 的输出通道数:

9fd3a230e3fb10416ff431dcbec6d421.jpeg

代码实现如下:

class BasicUNet(nn.Module):
  """A minimal UNet implementation."""
  def __init__(self, in_channels=1, out_channels=1):
    super().__init__()
    self.down_layers = torch.nn.ModuleList([ 
      nn.Conv2d(in_channels, 32, kernel_size=5, padding=2),
      nn.Conv2d(32, 64, kernel_size=5, padding=2),
      nn.Conv2d(64, 64, kernel_size=5, padding=2),
    ])
    self.up_layers = torch.nn.ModuleList([
      nn.Conv2d(64, 64, kernel_size=5, padding=2),
      nn.Conv2d(64, 32, kernel_size=5, padding=2),
      nn.Conv2d(32, out_channels, kernel_size=5, padding=2), 
    ])
    self.act = nn.SiLU() # The activation function
    self.downscale = nn.MaxPool2d(2)
    self.upscale = nn.Upsample(scale_factor=2)

  def forward(self, x):
    h = []
    for i, l in enumerate(self.down_layers):
      x = self.act(l(x)) # Through the layer n the activation function
      if i < 2: # For all but the third (final) down layer:
        h.append(x) # Storing output for skip connection
        x = self.downscale(x) # Downscale ready for the next layer
              
    for i, l in enumerate(self.up_layers):
      if i > 0: # For all except the first up layer
        x = self.upscale(x) # Upscale
        x += h.pop() # Fetching stored output (skip connection)
        x = self.act(l(x)) # Through the layer n the activation function
            
    return x

我们可以验证输出 shape 是否如我们期望的那样与输入相同:

net = BasicUNet()
x = torch.rand(8, 1, 28, 28)
net(x).shape
torch.Size([8, 1, 28, 28])

该网络有 30 多万个参数:

sum([p.numel() for p in net.parameters()])
309057

您可以尝试更改每个 layer 中的通道数或尝试不同的结构设计。

训练模型

那么,模型到底应该做什么呢?同样,对这个问题有各种不同的看法,但对于这个演示,让我们选择一个简单的框架:给定一个损坏的输入 noisy_x,模型应该输出它对原本 x 的最佳猜测。我们将通过均方误差将预测与真实值进行比较。

我们现在可以尝试训练网络了。

  • 获取一批数据

  • 添加随机噪声

  • 将数据输入模型

  • 将模型预测与干净图像进行比较,以计算 loss

  • 更新模型的参数

你可以自由进行修改来尝试获得更好的结果!

# Dataloader (you can mess with batch size)
batch_size = 128
train_dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# How many runs through the data should we do?
n_epochs = 3

# Create the network
net = BasicUNet()
net.to(device)

# Our loss finction
loss_fn = nn.MSELoss()

# The optimizer
opt = torch.optim.Adam(net.parameters(), lr=1e-3) 

# Keeping a record of the losses for later viewing
losses = []

# The training loop
for epoch in range(n_epochs):

  for x, y in train_dataloader:
    # Get some data and prepare the corrupted version
    x = x.to(device) # Data on the GPU
    noise_amount = torch.rand(x.shape[0]).to(device) # Pick random noise amounts
    noisy_x = corrupt(x, noise_amount) # Create our noisy x

    # Get the model prediction
    pred = net(noisy_x)

    # Calculate the loss
    loss = loss_fn(pred, x) # How close is the output to the true 'clean' x?

    # Backprop and update the params:
    opt.zero_grad()
    loss.backward()
    opt.step()

    # Store the loss for later
    losses.append(loss.item())

    # Print our the average of the loss values for this epoch:
    avg_loss = sum(losses[-len(train_dataloader):])/len(train_dataloader)
    print(f'Finished epoch {epoch}. Average loss for this epoch: {avg_loss:05f}')

# View the loss curve
plt.plot(losses)
plt.ylim(0, 0.1);
Finished epoch 0. Average loss for this epoch: 0.026736
Finished epoch 1. Average loss for this epoch: 0.020692
Finished epoch 2. Average loss for this epoch: 0.018887
17acb12c64d1d0f8707203e031a67015.png

我们可以尝试通过抓取一批数据,以不同的数量损坏数据,然后喂进模型获得预测来观察结果:

#@markdown Visualizing model predictions on noisy inputs:

# Fetch some data
x, y = next(iter(train_dataloader))
x = x[:8] # Only using the first 8 for easy plotting

# Corrupt with a range of amounts
amount = torch.linspace(0, 1, x.shape[0]) # Left to right -> more corruption
noised_x = corrupt(x, amount)

# Get the model predictions
with torch.no_grad():
  preds = net(noised_x.to(device)).detach().cpu()

# Plot
fig, axs = plt.subplots(3, 1, figsize=(12, 7))
axs[0].set_title('Input data')
axs[0].imshow(torchvision.utils.make_grid(x)[0].clip(0, 1), cmap='Greys')
axs[1].set_title('Corrupted data')
axs[1].imshow(torchvision.utils.make_grid(noised_x)[0].clip(0, 1), cmap='Greys')
axs[2].set_title('Network Predictions')
axs[2].imshow(torchvision.utils.make_grid(preds)[0].clip(0, 1), cmap='Greys');
ca91b4eb81be7a25f3f0c15a4382c52e.png

你可以看到,对于较低的噪声水平数量,预测的结果相当不错!但是,当噪声水平非常高时,模型能够获得的信息就开始逐渐减少。而当我们达到 amount = 1 时,模型会输出一个模糊的预测,该预测会很接近数据集的平均值。模型通过这样的方式来猜测原始输入。

取样(采样)

如果我们在高噪声水平下的预测不是很好,我们如何才能生成图像呢?

如果我们从完全随机的噪声开始,检查一下模型预测的结果,然后只朝着预测方向移动一小部分,比如说 20%。现在我们有一个噪声很多的图像,其中可能隐藏了一些关于输入数据的结构的提示,我们可以将其输入到模型中以获得新的预测。希望这个新的预测比第一个稍微好一点(因为我们这一次的输入稍微减少了一点噪声),所以我们可以用这个新的更好的预测再往前迈出一小步。

如果一切顺利的话,以上过程重复几次以后我们就会得到一个新的图像!以下图例是迭代了五次以后的结果,左侧是每个阶段的模型输入的可视化,右侧则是预测的去噪图像。请注意,即使模型在第 1 步就预测了去噪图像,我们也只是将输入向去噪图像变换了一小部分。重复几次以后,图像的结构开始逐渐出现并得到改善 , 直到获得我们的最终结果为止。

#@markdown Sampling strategy: Break the process into 5 steps and move 1/5'th of the way there each time:
n_steps = 5
x = torch.rand(8, 1, 28, 28).to(device) # Start from random
step_history = [x.detach().cpu()]
pred_output_history = []

for i in range(n_steps):
  with torch.no_grad(): # No need to track gradients during inference
    pred = net(x) # Predict the denoised x0
  pred_output_history.append(pred.detach().cpu()) # Store model output for plotting
  mix_factor = 1/(n_steps - i) # How much we move towards the prediction
  x = x*(1-mix_factor) + pred*mix_factor # Move part of the way there
  step_history.append(x.detach().cpu()) # Store step for plotting

fig, axs = plt.subplots(n_steps, 2, figsize=(9, 4), sharex=True)
axs[0,0].set_title('x (model input)')
axs[0,1].set_title('model prediction')
for i in range(n_steps):
  axs[i, 0].imshow(torchvision.utils.make_grid(step_history[i])[0].clip(0, 1), cmap='Greys')
  axs[i, 1].imshow(torchvision.utils.make_grid(pred_output_history[i])[0].clip(0, 1), cmap='Greys')
078f8a7f0729fcab5348b0278fd773a7.png

我们可以将流程分成更多步骤,并希望通过这种方式获得更好的图像:

#@markdown Showing more results, using 40 sampling steps
n_steps = 40
x = torch.rand(64, 1, 28, 28).to(device)
for i in range(n_steps):
  noise_amount = torch.ones((x.shape[0], )).to(device) * (1-(i/n_steps)) # Starting high going low
  with torch.no_grad():
    pred = net(x)
  mix_factor = 1/(n_steps - i)
  x = x*(1-mix_factor) + pred*mix_factor
fig, ax = plt.subplots(1, 1, figsize=(12, 12))
ax.imshow(torchvision.utils.make_grid(x.detach().cpu(), nrow=8)[0].clip(0, 1), cmap='Greys')
<matplotlib.image.AxesImage at 0x7f27567d8210>
0acee447898c766251527b25b3b6c4f3.png

结果并不是非常好,但是已经出现了一些可以被认出来的数字!您可以尝试训练更长时间(例如,10 或 20 个 epoch),并调整模型配置、学习率、优化器等。此外,如果您想尝试稍微困难一点的数据集,您可以尝试一下 fashionMNIST,只需要一行代码的替换就可以了。

与 DDPM 做比较

在本节中,我们将看看我们的“玩具”实现与其他笔记本中使用的基于 DDPM 论文的方法有何不同: 扩散器简介 Notebook。

  • 扩散器简介 Notebook:
    https://github.com/huggingface/diffusion-models-class/blob/main/unit1/01_introduction_to_diffusers.ipynb

我们将会看到的

  • 模型的表现受限于随迭代周期 (timesteps) 变化的控制条件,在前向传导中时间步 (t) 是作为一个参数被传入的

  • 有很多不同的取样策略可选择,可能会比我们上面所使用的最简单的版本更好

  • diffusers UNet2DModel 比我们的 BasicUNet 更先进

  • 损坏过程的处理方式不同

  • 训练目标不同,包括预测噪声而不是去噪图像

  • 该模型通过调节 timestep 来调节噪声水平 , 其中 t 作为一个附加参数传入前向过程中。

  • 有许多不同的采样策略可供选择,它们应该比我们上面简单的版本更有效。

自 DDPM 论文发表以来,已经有人提出了许多改进建议,但这个例子对于不同的可用设计决策具有指导意义。读完这篇文章后,你可能会想要深入了解这篇论文《Elucidating the Design Space of Diffusion-Based Generative Models》,它对所有这些组件进行了详细的探讨,并就如何获得最佳性能提出了新的建议。

Elucidating the Design Space of Diffusion-Based Generative Models 论文链接:
https://arxiv.org/abs/2206.00364

如果你觉得这些内容对你来说太过深奥了,请不要担心!你可以随意跳过本笔记本的其余部分或将其保存以备不时之需。

UNet

diffusers 中的 UNet2DModel 模型比上述基本 UNet 模型有许多改进:

  • GroupNorm 层对每个 blocks 的输入进行了组标准化(group normalization)

  • Dropout 层能使训练更平滑

  • 每个块有多个 resnet 层(如果 layers_per_block 未设置为 1)

  • 注意机制(通常仅用于输入分辨率较低的 blocks)

  • timestep 的调节。

  • 具有可学习参数的下采样和上采样块

让我们来创建并仔细研究一下 UNet2DModel:

model = UNet2DModel(
  sample_size=28,           # the target image resolution
  in_channels=1,            # the number of input channels, 3 for RGB images
  out_channels=1,           # the number of output channels
  layers_per_block=2,       # how many ResNet layers to use per UNet block
  block_out_channels=(32, 64, 64), # Roughly matching our basic unet example
  down_block_types=( 
    "DownBlock2D",        # a regular ResNet downsampling block
    "AttnDownBlock2D",    # a ResNet downsampling block w/ spatial self-attention
    "AttnDownBlock2D",
  ), 
  up_block_types=(
    "AttnUpBlock2D", 
    "AttnUpBlock2D",      # a ResNet upsampling block with spatial self-attention
    "UpBlock2D",          # a regular ResNet upsampling block
  ),
)
print(model)
UNet2DModel(
  (conv_in): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (time_proj): Timesteps()
  (time_embedding): TimestepEmbedding(
    (linear_1): Linear(in_features=32, out_features=128, bias=True)
    (act): SiLU()
    (linear_2): Linear(in_features=128, out_features=128, bias=True)
  )
  (down_blocks): ModuleList(
    (0): DownBlock2D(
      (resnets): ModuleList(
        (0): ResnetBlock2D(
          (norm1): GroupNorm(32, 32, eps=1e-05, affine=True)
          (conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
          (norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
        )
        (1): ResnetBlock2D(
          (norm1): GroupNorm(32, 32, eps=1e-05, affine=True)
          (conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
          (norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
        )
      )
      (downsamplers): ModuleList(
        (0): Downsample2D(
          (conv): Conv2d(32, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
        )
      )
    )
    (1): AttnDownBlock2D(
      (attentions): ModuleList(
        (0): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
        (1): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
      )
      (resnets): ModuleList(
        (0): ResnetBlock2D(
          (norm1): GroupNorm(32, 32, eps=1e-05, affine=True)
          (conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(32, 64, kernel_size=(1, 1), stride=(1, 1))
        )
        (1): ResnetBlock2D(
          (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
          (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
        )
      )
      (downsamplers): ModuleList(
        (0): Downsample2D(
          (conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
        )
      )
    )
    (2): AttnDownBlock2D(
      (attentions): ModuleList(
        (0): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
        (1): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
      )
      (resnets): ModuleList(
        (0): ResnetBlock2D(
          (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
          (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
        )
        (1): ResnetBlock2D(
          (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
          (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
        )
      )
    )
  )
  (up_blocks): ModuleList(
    (0): AttnUpBlock2D(
      (attentions): ModuleList(
        (0): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
        (1): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
        (2): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
      )
      (resnets): ModuleList(
        (0): ResnetBlock2D(
          (norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
          (conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
        )
        (1): ResnetBlock2D(
          (norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
          (conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
        )
        (2): ResnetBlock2D(
          (norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
          (conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
        )
      )
      (upsamplers): ModuleList(
        (0): Upsample2D(
          (conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        )
      )
    )
    (1): AttnUpBlock2D(
      (attentions): ModuleList(
        (0): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
        (1): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
        (2): AttentionBlock(
          (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
          (query): Linear(in_features=64, out_features=64, bias=True)
          (key): Linear(in_features=64, out_features=64, bias=True)
          (value): Linear(in_features=64, out_features=64, bias=True)
          (proj_attn): Linear(in_features=64, out_features=64, bias=True)
        )
      )
      (resnets): ModuleList(
        (0): ResnetBlock2D(
          (norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
          (conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
        )
        (1): ResnetBlock2D(
          (norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
          (conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
        )
        (2): ResnetBlock2D(
          (norm1): GroupNorm(32, 96, eps=1e-05, affine=True)
          (conv1): Conv2d(96, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
          (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(96, 64, kernel_size=(1, 1), stride=(1, 1))
        )
      )
      (upsamplers): ModuleList(
        (0): Upsample2D(
          (conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        )
      )
    )
    (2): UpBlock2D(
      (resnets): ModuleList(
        (0): ResnetBlock2D(
          (norm1): GroupNorm(32, 96, eps=1e-05, affine=True)
          (conv1): Conv2d(96, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
          (norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(96, 32, kernel_size=(1, 1), stride=(1, 1))
        )
        (1): ResnetBlock2D(
          (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
          (conv1): Conv2d(64, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
          (norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(64, 32, kernel_size=(1, 1), stride=(1, 1))
        )
        (2): ResnetBlock2D(
          (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
          (conv1): Conv2d(64, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
          (norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
          (dropout): Dropout(p=0.0, inplace=False)
          (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (nonlinearity): SiLU()
          (conv_shortcut): Conv2d(64, 32, kernel_size=(1, 1), stride=(1, 1))
        )
      )
    )
  )
  (mid_block): UNetMidBlock2D(
    (attentions): ModuleList(
      (0): AttentionBlock(
        (group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
        (query): Linear(in_features=64, out_features=64, bias=True)
        (key): Linear(in_features=64, out_features=64, bias=True)
        (value): Linear(in_features=64, out_features=64, bias=True)
        (proj_attn): Linear(in_features=64, out_features=64, bias=True)
      )
    )
    (resnets): ModuleList(
      (0): ResnetBlock2D(
        (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
        (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
        (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
        (dropout): Dropout(p=0.0, inplace=False)
        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (nonlinearity): SiLU()
      )
      (1): ResnetBlock2D(
        (norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
        (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
        (norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
        (dropout): Dropout(p=0.0, inplace=False)
        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (nonlinearity): SiLU()
      )
    )
  )
  (conv_norm_out): GroupNorm(32, 32, eps=1e-05, affine=True)
  (conv_act): SiLU()
  (conv_out): Conv2d(32, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)

正如你所看到的,还有更多!它比我们的 BasicUNet 有多得多的参数量:

sum([p.numel() for p in model.parameters()]) # 1.7M vs the ~309k parameters of the BasicUNet
1707009

我们可以用这个模型代替原来的模型来重复一遍上面展示的训练过程。我们需要将 x 和 timestep 传递给模型(这里我会传递 t = 0,以表明它在没有 timestep 条件的情况下工作,并保持采样代码简单,但您也可以尝试输入 (amount*1000),使 timestep 与噪声水平相当)。如果要检查代码,更改的行将显示为“#<<<。

#@markdown Trying UNet2DModel instead of BasicUNet:

# Dataloader (you can mess with batch size)
batch_size = 128
train_dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# How many runs through the data should we do?
n_epochs = 3

# Create the network
net = UNet2DModel(
  sample_size=28,  # the target image resolution
  in_channels=1,  # the number of input channels, 3 for RGB images
  out_channels=1,  # the number of output channels
  layers_per_block=2,  # how many ResNet layers to use per UNet block
  block_out_channels=(32, 64, 64),  # Roughly matching our basic unet example
  down_block_types=( 
    "DownBlock2D",  # a regular ResNet downsampling block
    "AttnDownBlock2D",  # a ResNet downsampling block with spatial self-attention
    "AttnDownBlock2D",
  ), 
  up_block_types=(
    "AttnUpBlock2D", 
    "AttnUpBlock2D",  # a ResNet upsampling block with spatial self-attention
    "UpBlock2D",   # a regular ResNet upsampling block
  ),
) #<<<
net.to(device)

# Our loss finction
loss_fn = nn.MSELoss()

# The optimizer
opt = torch.optim.Adam(net.parameters(), lr=1e-3) 

# Keeping a record of the losses for later viewing
losses = []

# The training loop
for epoch in range(n_epochs):

  for x, y in train_dataloader:

    # Get some data and prepare the corrupted version
    x = x.to(device) # Data on the GPU
    noise_amount = torch.rand(x.shape[0]).to(device) # Pick random noise amounts
    noisy_x = corrupt(x, noise_amount) # Create our noisy x

    # Get the model prediction
    pred = net(noisy_x, 0).sample #<<< Using timestep 0 always, adding .sample

    # Calculate the loss
    loss = loss_fn(pred, x) # How close is the output to the true 'clean' x?

    # Backprop and update the params:
    opt.zero_grad()
    loss.backward()
    opt.step()

    # Store the loss for later
    losses.append(loss.item())

    # Print our the average of the loss values for this epoch:
    avg_loss = sum(losses[-len(train_dataloader):])/len(train_dataloader)
    print(f'Finished epoch {epoch}. Average loss for this epoch: {avg_loss:05f}')

# Plot losses and some samples
fig, axs = plt.subplots(1, 2, figsize=(12, 5))

# Losses
axs[0].plot(losses)
axs[0].set_ylim(0, 0.1)
axs[0].set_title('Loss over time')

# Samples
n_steps = 40
x = torch.rand(64, 1, 28, 28).to(device)
for i in range(n_steps):
  noise_amount = torch.ones((x.shape[0], )).to(device) * (1-(i/n_steps)) # Starting high going low
  with torch.no_grad():
    pred = net(x, 0).sample
  mix_factor = 1/(n_steps - i)
  x = x*(1-mix_factor) + pred*mix_factor

axs[1].imshow(torchvision.utils.make_grid(x.detach().cpu(), nrow=8)[0].clip(0, 1), cmap='Greys')
axs[1].set_title('Generated Samples');
Finished epoch 0. Average loss for this epoch: 0.018925
Finished epoch 1. Average loss for this epoch: 0.012785
Finished epoch 2. Average loss for this epoch: 0.011694
729646f2824dfb0dfc4e49cd9b95bcbe.png

这看起来比我们的第一组结果好多了!您可以尝试调整 UNet 配置或更长时间的训练,以获得更好的性能。

损坏过程

DDPM 论文描述了一个为每个“timestep”添加少量噪声的损坏过程。为某些 timestep 给定 , 我们可以得到一个噪声稍稍增加的 :

bc69ad4a77d3f62bc35b4c92799990ae.png

这就是说,我们取 , 给他一个 的系数,然后加上带有 系数的噪声。这里 是根据一些管理器来为每一个 t 设定的,来决定每一个迭代周期中添加多少噪声。现在,我们不想把这个推演进行 500 次来得到 ,所以我们用另一个公式来根据给出的 计算得到任意 t 时刻的 :

86d4ab7941f0cbaf6161951d03bedadf.png

数学符号看起来总是很吓人!幸运的是,调度器为我们处理了所有这些(取消下一个单元格的注释以检查代码)。我们可以画出 (标记为 sqrt_alpha_prod) 和 (标记为 sqrt_one_minus_alpha_prod) 来看一下输入 (x) 与噪声是如何在不同迭代周期中量化和叠加的 :

#??noise_scheduler.add_noise
noise_scheduler = DDPMScheduler(num_train_timesteps=1000)
plt.plot(noise_scheduler.alphas_cumprod.cpu() ** 0.5, label=r"${\sqrt{\bar{\alpha}_t}}$")
plt.plot((1 - noise_scheduler.alphas_cumprod.cpu()) ** 0.5, label=r"$\sqrt{(1 - \bar{\alpha}_t)}$")
plt.legend(fontsize="x-large");
b50bc076e0ed233b71fd51af41e2263d.png

一开始 , 噪声 x 里绝大部分都是 x 自身的值  (sqrt_alpha_prod ~= 1),但是随着时间的推移,x 的成分逐渐降低而噪声的成分逐渐增加。与我们根据 amount 对 x 和噪声进行线性混合不同,这个噪声的增加相对较快。我们可以在一些数据上看到这一点:

#@markdown visualize the DDPM noising process for different timesteps:

# Noise a batch of images to view the effect
fig, axs = plt.subplots(3, 1, figsize=(16, 10))
xb, yb = next(iter(train_dataloader))
xb = xb.to(device)[:8]
xb = xb * 2. - 1. # Map to (-1, 1)
print('X shape', xb.shape)

# Show clean inputs
axs[0].imshow(torchvision.utils.make_grid(xb[:8])[0].detach().cpu(), cmap='Greys')
axs[0].set_title('Clean X')

# Add noise with scheduler
timesteps = torch.linspace(0, 999, 8).long().to(device)
noise = torch.randn_like(xb) # << NB: randn not rand
noisy_xb = noise_scheduler.add_noise(xb, noise, timesteps)
print('Noisy X shape', noisy_xb.shape)

# Show noisy version (with and without clipping)
axs[1].imshow(torchvision.utils.make_grid(noisy_xb[:8])[0].detach().cpu().clip(-1, 1),  cmap='Greys')
axs[1].set_title('Noisy X (clipped to (-1, 1)')
axs[2].imshow(torchvision.utils.make_grid(noisy_xb[:8])[0].detach().cpu(),  cmap='Greys')
axs[2].set_title('Noisy X');
X shape torch.Size([8, 1, 28, 28])
Noisy X shape torch.Size([8, 1, 28, 28])
cf4f255bcab19ede884db3ed1c4d483e.png

在运行中的另一个变化:在 DDPM 版本中,加入的噪声是取自一个高斯分布(来自均值 0 方差 1 的 torch.randn),而不是在我们原始 corrupt 函数中使用的 0-1 之间的均匀分布(torch.rand),当然对训练数据做正则化也可以理解。在另一篇笔记中,你会看到 Normalize(0.5, 0.5) 函数在变化列表中,它把图片数据从 (0, 1) 区间映射到 (-1, 1),对我们的目标来说也‘足够用了’。我们在此篇笔记中没使用这个方法,但在上面的可视化中为了更好的展示添加了这种做法。

训练目标

在我们的玩具示例中,我们让模型尝试预测去噪图像。在 DDPM 和许多其他扩散模型实现中,模型则会预测损坏过程中使用的噪声(在缩放之前,因此是单位方差噪声)。在代码中,它看起来像是这样:

noise = torch.randn_like(xb) # << NB: randn not rand
noisy_x = noise_scheduler.add_noise(x, noise, timesteps)
model_prediction = model(noisy_x, timesteps).sample
loss = mse_loss(model_prediction, noise) # noise as the target

你可能认为预测噪声(我们可以从中得出去噪图像的样子)等同于直接预测去噪图像。那么,为什么要这么做呢?这仅仅是为了数学上的方便吗?

这里其实还有另一些精妙之处。我们在训练过程中,会计算不同(随机选择)timestep 的 loss。这些不同的目标将导致这些 loss 的不同的“隐含权重”,其中预测噪声会将更多的权重放在较低的噪声水平上。你可以选择更复杂的目标来改变这种“隐性损失权重”。或者,您选择的噪声管理器将在较高的噪声水平下产生更多的示例。也许你让模型设计成预测 “velocity” v,我们将其定义为由噪声水平影响的图像和噪声组合(请参阅“扩散模型快速采样的渐进蒸馏”- 'PROGRESSIVE DISTILLATION FOR FAST SAMPLING OF DIFFUSION MODELS')。也许你将模型设计成预测噪声,然后基于某些因子来对 loss 进行缩放:比如有些理论指出可以参考噪声水平(参见“扩散模型的感知优先训练”-'Perception Prioritized Training of Diffusion Models'),或者基于一些探索模型最佳噪声水平的实验(参见“基于扩散的生成模型的设计空间说明”-'Elucidating the Design Space of Diffusion-Based Generative Models')。

一句话解释:选择目标对模型性能有影响,现在有许多研究者正在探索“最佳”选项是什么。目前,预测噪声(epsilon 或 eps)是最流行的方法,但随着时间的推移,我们很可能会看到库中支持的其他目标,并在不同的情况下使用。

迭代周期(Timestep)调节

UNet2DModel 以 x 和 timestep 为输入。后者被转化为一个嵌入(embedding),并在多个地方被输入到模型中。

这背后的理论支持是这样的:通过向模型提供有关噪声水平的信息,它可以更好地执行任务。虽然在没有这种 timestep 条件的情况下也可以训练模型,但在某些情况下,它似乎确实有助于性能,目前来说绝大多数的模型实现都包括了这一输入。

取样(采样)

有一个模型可以用来预测在带噪样本中的噪声(或者说能预测其去噪版本),我们怎么用它来生成图像呢?

我们可以给入纯噪声,然后就希望模型能一步就输出一个不带噪声的好图像。但是,就我们上面所见到的来看,这通常行不通。所以,我们在模型预测的基础上使用足够多的小步,迭代着来每次去除一点点噪声。

具体我们怎么走这些小步,取决于使用上面取样方法。我们不会去深入讨论太多的理论细节,但是一些顶层想法是这样:

  • 每一步你想走多大?也就是说,你遵循什么样的“噪声计划(噪声管理)”?

  • 你只使用模型当前步的预测结果来指导下一步的更新方向吗(像 DDPM,DDIM 或是其他的什么那样)?你是否要使用模型来多预测几次来估计一个更高阶的梯度来更新一步更大更准确的结果(更高阶的方法和一些离散 ODE 处理器)?或者保留历史预测值来尝试更好的指导当前步的更新(线性多步或遗传取样器)?

  • 你是否会在取样过程中额外再加一些随机噪声,或你完全已知的(deterministic)来添加噪声?许多取样器通过参数(如 DDIM 中的 'eta')来供用户选择。

对于扩散模型取样器的研究演进的很快,随之开发出了越来越多可以使用更少步就找到好结果的方法。勇敢和有好奇心的人可能会在浏览 diffusers library 中不同部署方法时感到非常有意思,可以查看 Schedulers 代码 或看看 Schedulers 文档,这里经常有一些相关的论文。

  • Schedulers 代码:
    https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers

  • Schedulers 文档:
    https://huggingface.co/docs/diffusers/main/en/api/schedulers

结语

希望这可以从一些不同的角度来审视扩散模型提供一些帮助。这篇笔记是 Jonathan Whitaker 为 Hugging Face 课程所写的,如果你对从噪声和约束分类来生成样本的例子感兴趣。问题与 bug 可以通过 GitHub issues 或 Discord 来交流。

致谢第一单元第二部分社区贡献者

感谢社区成员们对本课程的贡献:

@darcula1993、@XhrLeokk:魔都强人工智能孵化者,二里街调参记录保持人,一切兴趣使然的 AIGC 色图创作家的庇护者,图灵神在五角场的唯一指定路上行走。

感谢茶叶蛋蛋对本文贡献设计素材!

欢迎通过链接加入我们的本地化小组与大家共同交流:
https://bit.ly/3G40j6U

相关推荐

Transformer 中的专家混合模型 (MoE)

每个设备只加载分配给自己的那部分专家,负责对应的计算,并在最后参与结果的汇总。在专家混合 (MoE) 模型中,每个 token 会被路由到不同的专家。时,模型会从标准的张量并行 (Tensor Parallel, TP) 策略切换为专家并行 (Expert Parallel, EP) 策略,并采用专门的切分 (sharding) 方式。将全局专家索引映射为本地索引,屏蔽不属于当前设备的专家,确保每个设备只使用本地专家进行计算,并通过 all-reduce 在设备之间汇总部分计算结果。欢迎告诉我们你希望在。

Hugging Face 680

在 Hugging Face Hub 上引入 Storage Buckets

在未来的规划中,我们将支持 Buckets 与仓库之间的双向直接传输:例如将最终的 checkpoint 权重提升到模型仓库,或者在数据流水线完成后,把处理好的数据分片提交到数据集仓库。如果你来自 S3 风格的对象存储环境,Buckets 也提供了熟悉的使用模式,同时更适配 AI 产物,并能无缝过渡到 Hub 上的最终发布流程。对于一次性操作,可以使用。提供了原生的存放位置,比如 checkpoint、处理后的数据、Agent trace、日志,以及所有在最终定稿之前仍然有价值的中间产物。

Hugging Face 636

LeRobot v0.5.0 正式发布

成为目前规模最大的一次发布 —— 几乎在所有方向上都实现了扩展:支持更多机器人 (包括首个类人机器人) 、更多策略模型 (包括回归的自回归 VLA) 、更快的数据集处理、可以直接从 Hub 加载的仿真环境,以及基于。:我们将 SO-100 和 SO-101 的实现整合到一个更简洁的代码库中 (包括双臂配置) ,减少重复代码,更易维护,同时保持原有功能。从更严格的类型检查到更健壮的测试基础设施,我们正在持续加强 LeRobot 的基础架构,以支持未来更大规模的发展。这大大降低了分享自定义仿真环境的门槛。

Hugging Face 815

全球开源 AI 生态的未来:从 DeepSeek 到 AI+

资源约束并未限制中国 AI 的发展,相反,在某些方面,它们重塑了其发展轨迹。截至 2025 年中,Qwen 成为 Hugging Face 上衍生模型数量最多的基础模型,已有超过 11.3 万个模型以 Qwen 为基座构建,超过 20 万个模型仓库标注了 Qwen,远高于 Meta 的 Llama(2.7 万)或 DeepSeek(6 千)。通过在更开放的系统中打通模型、芯片与 PaddlePaddle,百度得以在算力、成本和监管等共同约束下,降低成本、吸引开发者并影响行业标准,同时保持自身的战略控制力。

Hugging Face 656

DeepSeek之后:中国开源人工智能生态的架构选择

与此同时,训练侧的信号也开始显现。相比之下,定制化和约束性更强的许可证因为不被熟知、理解成本高,增加了新的法律风险,在实际使用中显著增加了摩擦成本,从而降低了采用意愿,这也直接导致了图中所显示的整体下滑。在统一的能力框架下,根据任务的复杂度与价值,动态激活不同数量的专家,从而在不同请求与部署环境中分配计算资源。例如,在 DeepSeek-V3.2-Exp 发布时,华为昇腾与寒武纪芯片实现了“首日支持”,并非云端演示,而是与模型权重一同发布的可复现推理流水线,使开发者能够直接验证真实世界中的性能表现。

Hugging Face 1000

<span class=“js_title_inner“>DeepSeek之后:中国开源人工智能生态的架构选择</span>

与此同时,训练侧的信号也开始显现。相比之下,定制化和约束性更强的许可证因为不被熟知、理解成本高,增加了新的法律风险,在实际使用中显著增加了摩擦成本,从而降低了采用意愿,这也直接导致了图中所显示的整体下滑。在统一的能力框架下,根据任务的复杂度与价值,动态激活不同数量的专家,从而在不同请求与部署环境中分配计算资源。例如,在 DeepSeek-V3.2-Exp 发布时,华为昇腾与寒武纪芯片实现了“首日支持”,并非云端演示,而是与模型权重一同发布的可复现推理流水线,使开发者能够直接验证真实世界中的性能表现。

Hugging Face 934

推出 AnyLanguageModel:在 Apple 平台统一本地与远程大语言模型的 API

对于 Xcode 项目 (目前还不支持直接声明 trait) , 你可以创建一个内部的 Swift 包,指定所需 trait 依赖 AnyLanguageModel, 然后将这个包作为本地依赖添加到你的项目中。我们在这里做了有意识的取舍, 也许我们现在的设计将来会和 Apple 官方的实现产生冲突, 但这正是废弃警告 (deprecation warnings) 存在的意义。这个项目是一个起点: 你可以 fork 它、扩展它、替换不同的模型, 了解各个模块如何协作,并根据自己的需求进行定制。

Hugging Face 644

使用 Hugging Face 轻松构建并共享 ROCm 内核

自定义内核是高性能深度学习的基础,它让 GPU 操作能完全贴合你的工作负载需求——无论是图像处理、张量变换,还是其他计算密集型任务。然而,要为正确的架构编译这些内核、配置各种编译标志并干净地整合到 PyTorch 扩展中,往往会变成一团乱麻 (CMake/Nix、编译错误、ABI 问题等)。这保证了你的内核既高效、又可移植,还能无缝集成到 PyTorch 中。一旦构建完成,只需上传到 Hugging Face Hub,社区中的其他人就能直接使用你的自定义内核——几行代码即可完成集成,轻松共享高性能成果。

Hugging Face 1356

“DeepSeek 时刻” 一周年

DeepSeek 的 R1 模型降低了获取先进模型能力的门槛,并提供了一种清晰、可供效仿的发展范式,从而解锁了新的发展层级。来自美国机构的重要发布。表面上看似合作的行为,更准确地说,是在共同的技术、经济与监管压力下形成的对齐。这是中国的开源模型首次进入全球主流榜单,并且在随后的一年中,每当有新模型发布时,它都反复被用作重要的参考基准。与 2024 年相比,R1 发布之后,中国的人工智能格局逐步形成了一种新的态势:大型科技公司率先行动,初创公司迅速跟进,来自垂直行业的公司也越来越多地进入这一领域。

Hugging Face 872

Transformers v5 中的分词系统:更简洁、更清晰、更模块化

最糟糕的是,你无法创建一个空的分词器架构。如果你想用自己的数据训练一个 LLaMA 风格的分词器,没有简单的方法可以初始化一个“空白”的 LLaMA 分词器并填入自定义的词表和合并规则。最终得到的分词器将拥有你自定义的词表和合并规则,但在处理文本时的行为将与标准的 LLaMA 分词器完全一致:空格处理、特殊 token 规则、解码行为都相同。这些模型专属的分词器类会根据各自模型的需求,对后端进行配置,包括正确的词表、合并规则、特殊 token,以及标准化设置等,确保分词行为与模型训练时保持一致。

Hugging Face 805

<span class=“js_title_inner“>Transformers v5 中的分词系统:更简洁、更清晰、更模块化</span>

最糟糕的是,你无法创建一个空的分词器架构。如果你想用自己的数据训练一个 LLaMA 风格的分词器,没有简单的方法可以初始化一个“空白”的 LLaMA 分词器并填入自定义的词表和合并规则。最终得到的分词器将拥有你自定义的词表和合并规则,但在处理文本时的行为将与标准的 LLaMA 分词器完全一致:空格处理、特殊 token 规则、解码行为都相同。这些模型专属的分词器类会根据各自模型的需求,对后端进行配置,包括正确的词表、合并规则、特殊 token,以及标准化设置等,确保分词行为与模型训练时保持一致。

Hugging Face 912

构建开放的未来——我们与 Google Cloud 的全新合作伙伴关系

我们非常期待通过与 Google Cloud 的深入合作,让这一愿景加速实现——无论你使用的是 Vertex AI Model Garden、Google Kubernetes Engine、Cloud Run,还是 Hugging Face Inference Endpoints。通过这个 CDN Gateway,Hugging Face 的模型和数据集将直接缓存在 Google Cloud 上,大幅减少下载时间,并提升 Google Cloud 用户在模型供应链方面的稳定性和可靠性。

Hugging Face 656

使用 NVIDIA Isaac 构建医疗机器人:从仿真到部署

该流程通过约 70 个仿真演示和 10–20 个真实演示相结合,在模拟多样场景的同时,也保留真实数据的可靠性。NVIDIA Isaac for Healthcare 是一个专为 AI 医疗机器人开发者打造的框架,提供从数据采集到训练、评估再到部署的全流程工具链,适用于仿真环境与真实硬件。仿真一直是医学影像中弥补数据缺口的重要手段,但在医疗机器人领域,它过去往往速度太慢、系统割裂,或难以迁移到现实应用中。值得一提的是,超过 93% 的策略训练数据来自仿真环境,充分说明仿真技术在缩小机器人数据鸿沟方面的优势。

Hugging Face 1681

Codex 正在推动开源 AI 模型的训练与发布

你可以训练 0.5B 到 7B 参数规模的模型,将它们转换为 GGUF 格式便于本地运行,还可以通过多阶段流程结合不同方法。Trackio 会通过自动运行评估任务,帮助你监控训练和评估的得分,并选择表现最好的模型检查点 (checkpoint)。这里的重点不是“Codex 自己开源模型”,而是让 Codex 作为编码代理,参与并自动化开源模型的训练、评估与发布全流程。我们已经展示了 Codex 如何处理模型微调的完整生命周期,验证数据、选择硬件、生成训练脚本、提交任务、监控进度,以及转换输出。

Hugging Face 880

经同意的语音克隆

每次生成时,系统会随机选择一个日常话题 (如天气、美食或音乐) ,使句子内容丰富多样,也更自然好读,有助于录音清晰、自然,并具备良好的语音质量,同时包含明确的同意声明。换句话说,模型不会擅自“说出”你的声音,除非你亲口同意。也就是说,“同意”成为系统运行的前提条件,让原本抽象的伦理原则变成了具体可操作的系统规则,并形成可追溯、可审核的交互记录:AI 模型只会在明确同意之后才运行。现在很多语音克隆模型只需要一句话就能模仿说话人的声音,因此这句用于表达“同意”的句子,也可以同时作为语音克隆的输入数据。

Hugging Face 1159

流式数据集:效率提升 100 倍!

比如,在 Hugging Face 上传大型数据集时,Xet 的去重机制大幅减少了数据传输量,上传更快;流式加载一直是快速了解数据集的好方法,但在训练模型时,大多数人仍然选择将数据预先下载到本地,或使用 S3 等云存储——我们在训练。包,支持 Spark 直接读写 HF 数据集,内置对 Parquet CDC 和 Xet 的支持,大幅加快大数据处理。在机器学习中,特别是在处理 TB 级别的数据时,数据加载一直是个大难题。库还不支持,或者你希望获得更高的控制权,我们也提供了强大的自定义流式能力。

Hugging Face 975

huggingface_hub 1.0 正式版现已发布:开源机器学习基础五周年回顾

在 Hugging Face Hub 的早期阶段,研究人员和开发者常常面临一个困扰: 训练一个先进的模型不仅耗时、耗资源,而且在训练完成后,模型往往“被困”在个人电脑里,只能通过不稳定的 Google Drive 链接进行分享。不再依赖单一的推理后端,而是与多家无服务器推理平台合作,包括 Together AI、SambaNova、Replicate、Cerebras、Groq 等,用户通过一个统一的 API 即可调用多个后端,路由透明,按请求计费,真正实现了“按需调用,轻松推理”。

Hugging Face 1613

Hugging Face 论文页面功能指南

认领成功后,系统会将你标记为论文的提交者,即可向 Daily Papers 提交论文。在每篇论文的页面右侧,你都能找到相关资源链接,例如模型、数据集、Spaces 以及其他有用的集合。作者只需在相关资源(如模型或数据集)的 README.md 中添加该论文的 arXiv 链接,即可将资源与论文自动关联。论文提交不限于你自己的工作,你也可以分享任何你认为对社区有价值的研究论文。如果你的论文尚未在 Hugging Face 论文页面建立索引,只需点击 index 按钮,即可一步添加。如果你是首次分享论文。

Hugging Face 639

LeRobot v0.4.0 正式发布:全面提升开源机器人的学习能力

LeRobot v0.4.0 为开源机器人领域带来重要升级:引入可扩展的 Datasets v3.0、强大的新 VLA (视觉-语言-动作) 模型如 PI0.5 与 GR00T N1.5,以及全新的插件系统,简化硬件集成。从重构的数据集到灵活的编辑工具、新的仿真环境,以及面向硬件的全新插件系统,LeRobot 正在持续演进,以满足前沿具身智能 (Embodied AI) 不断发展的需求。模型需要在正确设备上的规范化、按批次的张量,而你的机器人硬件则需要特定格式的动作命令。更多细节可阅读我们此前的。

Hugging Face 1789

Gaia2 与 ARE:赋能社区的智能体评测

现有的评测环境通常与特定任务紧密耦合,缺乏真实世界的灵活性,也无法反映开放世界中混乱的现实:模拟页面不会加载失败,事件不会自发发生,也不存在异步混乱。在理想情况下,AI 智能体应当是可靠的助手。当接收到任务时,它们能够轻松处理指令中的歧义,构建逐步执行的计划,正确识别所需资源,按计划执行而不被干扰,并在突发事件中灵活适应,同时保持准确性,避免幻觉。当接收到任务时,它们能够轻松处理指令中的歧义,构建逐步执行的计划,正确识别所需资源,按计划执行而不被干扰,并在突发事件中灵活适应,同时保持准确性,避免幻觉。

Hugging Face 1806

ZeroGPU Spaces 加速实践:PyTorch 提前编译全解析

它高效、灵活,非常适合演示,不过需要注意的是,ZeroGPU 并不能在所有场景下完全发挥 GPU 与 CUDA 栈的全部潜能,比如生成图像或视频可能需要相当多的时间。根据模型的不同,这个过程可能需要几十秒到几分钟,对于 Spaces 中的实际 GPU 任务来说,这显然太慢了。通过提前编译,我们可以在一开始导出已编译的模型,将其保存,然后在任意进程中即时加载。否则,如果存在不兼容问题,就会报错。例如,在 Wan 系列视频生成模型中,如果你希望编译后的模型能够生成不同分辨率的内容,就会遇到这种情况。

Hugging Face 959

开源开发者须知:欧盟《人工智能法案》对通用人工智能模型的最新要求

根据 其第 3 条第 63 款 的定义,GPAI 模型是指:“人工智能模型,包括那些通过大规模自监督方式训练、具备显著通用性,并能够胜任多种不同任务的模型,无论其在市场上的投放方式如何,该类模型都能够被集成到各种下游系统或应用中。虽然《网络弹性法案》的措辞可能对《人工智能法案》的解释不具备约束力,但其中的这一表述以及其他相关措辞确实指向一种理解,即在FOSS 许可下提供的软件,并不总是、也并非本质上就应当被视为《网络弹性法案》意义下的“商业活动”,特别是在生产者未对其进行“商业化”的情况下。

Hugging Face 2111

欢迎 GPT OSS —— 来自 OpenAI 的全新开放模型家族!

在 GPT OSS 中,system 消息遵循严格格式,并包含当前日期、模型身份以及推理强度等级等信息,而 “developer” 消息则更为自由,这 (令人困惑地) 使它类似于其他模型的 “system” 消息。Hugging Face 与 Azure 合作,将最受欢迎的开源开放模型 (涵盖文本、视觉、语音与多模态任务) 直接引入 Azure AI Model Catalog,便于客户在托管在线端点中安全部署,借助 Azure 的企业级基础设施、自动扩缩与监控能力。轻松在多张 GPU 上部署。

Hugging Face 2546

Hugging Face 开源机器人 Reachy Mini 开启预定

无论是 AI 开发者、极客或黑客、科研人员、教育工作者、机器人爱好者,还是与孩子一同学习编程的家长,Reachy Mini 都能帮助你在桌面环境中高效完成 AI 应用的开发、最新算法的测试、项目的实际部署以及创新成果的共享。:通过上传、下载和分享机器人行为,实现行为复用与组合创新,随着 Reachy Mini 社区的发展,可能性将呈指数级扩展。:与 Hugging Face 社区超千万用户一起,上传、分享与下载机器人行为,共同扩展智能机器人的边界。:集成摄像头、麦克风与扬声器,支持基于 AI 的视听交互。

Hugging Face 1800

开源人工智能:数字主权的基石

像 OLMO2(一个相对较小的模型,其训练方法和数据完全透明,性能与 OpenAI 的 o1-mini 相当)或 OlympicCoder(一个更小的模型,在复杂编码任务上性能超过最新的 Claude 模型)这样的开源项目,充分体现了协同创新在构建先进人工智能模型中的潜力。这种能力的积累,不仅有助于形成符合本地价值观与监管要求的人工智能系统,更是面向未来的关键竞争力,在人工智能深度塑造经济、治理与社会结构的时代背景下,具有不可替代的长期战略意义。:确保在一国领土内生成的数据受该国法律和治理结构的约束。

Hugging Face 1275

nanoVLM: 简洁、轻量的纯 PyTorch 视觉-语言模型训练代码库

该模块将视觉主干网络产生的图像嵌入作为输入,并将它们转换为与语言模型嵌入层的文本嵌入兼容的嵌入。减少了图像标记的数量,这有助于降低计算成本并加快训练速度,特别是对于对输入长度敏感的基于 transformer 的语言解码器。下图演示了这个概念。在这篇博客中,我们介绍了什么是 VLM,探讨了支撑 nanoVLM 的架构选择,并详细解释了训练和推理工作流程。,可以帮助你构建和训练一个能够理解图像和文本,并基于此生成文本的模型。如果你尝试了它,并在它的基础上尝试构建,或者你只是有问题,我们都很乐意听到你的反馈。

Hugging Face 1471

HF Papers 直播| AI Insight Talk 强化学习专场

研究方向为多模态通用感知模型、多模态大模型后训练算法,在人工智能国际顶级会议及期刊发表论文十余篇,谷歌学术累计引用超过1900次,作为核心作者(共一二作)参与研发的多模态大模型InternVL-Chat系列模型在HuggingFace社区累计下载量突破1000万次,开源代码在Github获得stars超过8000个。已在 NeurIPS,ICLR,ACL 等国际顶级会议上发表多篇论文,谷歌学术引用量 600+,担任 NeurIPS 等顶级会议审稿人。为了更好的社区互动过,我们在本次直播加入了圆桌讨论环节。

Hugging Face 1257

SmolVLA: 让机器人更懂 “看听说做” 的轻量化解决方案

从左到右: (1) 异步的拾取 - 放置立方体计数,(2) 同步的拾取 - 放置立方体计数,(3) 在扰动下的拾取 - 放置立方体计数,(4) 在真实世界 SO101 上的乐高积木拾取 - 放置任务泛化。与学术基准不同,社区数据集自然地捕捉到了杂乱、现实的互动: 多变的光照、不完美的演示、非常规物体和异质的控制方案。在过去的几年里,Transformers 技术推动了人工智能的显著进展,从能够进行类人推理的语言模型到理解图像和文本的多模态系统。然而,在实际的机器人领域,进展则相对较慢。

Hugging Face 2425

参加 Hugging Face 组织的 Gradio & MCP 智能体主题黑客松

一场以智能体 (Agent) 和模型上下文协议 (Model Context Protocol,简称 MCP) 为核心的全球在线黑客松即将来袭!Model Context Protocol (MCP) 是一个开放协议,旨在标准化应用如何向大语言模型 (LLM) 提供上下文信息。:在 6 月 8 日 23:59 (UTC) 之前提交至对应的 Hugging Face Spaces。构建一个完整的 AI 智能体展示应用 (可使用 MCP,也可以不使用) ,让我们看到它真正能做些什么!

Hugging Face 1603

大模型评估排障指南 | 关于可复现性

它们在标准测试集 MATH-Hard 上预测的答案完全正确,但在 few-shot 简单模板测试中评估得分反而较低,很可能就是过拟合了 GSM8K 数据集 (另一个数学测试集) 的 prompt 和答案格式。,如果不看代码,你可能觉得就是上文提到的那样,直接对比预测结果和参考答案,不过: 如果查看代码会发现,在做对比之前预测结果还多做了一步归一化 (去除标点符号、统一数字格式等),这明显会改变对比得分。(没错不用惊讶,不一致时也会影响结果差异,因为不同模型对不同样本的表现是不一样的)。

Hugging Face 1156
上一篇: Hugging Face 每周速递:Spaces 上边可以跑 Jupyter Notebook;百度 UIE-X 模型展示发布...
下一篇: 使用英特尔 Sapphire Rapids 加速 PyTorch Transformers 模型
Hugging Face
Hugging Face Hugging Face 官方账号 Hugging Face 官方账号
博客等级 码龄4年 1861粉丝 378原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值