项目目标
自从Open AI发布了DALL-E之后,我对如何使用Stable Diffusion来根据文本提示生成图像产生了兴趣。随着AI工具和平台(如Hugging Face)的日益流行,现在似乎是开始探索的完美时机!该项目的主要目标是提高我的理解,而技术目标包括:
1. 使用文本提示将卡通角色的图像改变为不同的情境和艺术风格。
2. 使用一种经过预训练但又经过我微调的模型来实现。
3. 讨论和审查我使用的模型的伦理影响和使用限制。
概念评审 — Stable Diffusion
Stable Diffusion模型的最终目标是根据文本提示生成一张图像。这种图像生成是有条件的,也就是说我们在图像生成过程中添加了一个条件(文本提示)。为了帮助我们理解,让我们从无条件生成开始,无条件生成不使用文本提示。
无条件生成仍然需要进行训练,为了理解这一点,我们来看一个初始图片:
上面的图像在我们所称的像素空间中。这实际上意味着图像是用像素表示的。为了举例说明,我们假设图像是1024x1024像素。对我们来说,理解这个图像很简单,但对机器来说,这实际上是非常复杂的。这就是Stable Diffusion流水线的第一部分——编码的作用所在。
在编码过程中,图像被传递给图像编码器,将其转换成潜在空间。与像素空间相比,潜在空间对我们来说更难理解,但对流水线中的计算来说更容易。在潜在空间中,模型开始理解图像的特征而不是像素。例如,它可能学习到人脸的特征。在这个阶段,图像被表示为一个张量,你可能在其他项目或类似tensorflow的软件包中听说过。后来,图像将通过解码器从潜在空间解码回像素空间。这是很多步骤;让我们用一个简单的图示来回顾一下:
上面的图像应该更清楚地说明了:如果我们忽略Stable Diffusion,编码器和解码器只是用来接收图像、学习潜在特征,然后通过这些特征再次生成相同的图像。重要的是,潜在特征可以用来表示和再生产图像。
在潜在空间中,我们开始执行Stable Diffusion的“扩散”部分。示例将展示这一过程发生在像素空间中,这更容易表示。扩散过程利用高斯噪声为图像添加随机性。这个过程在许多时间步骤中进行,时间步骤与添加的噪声量相关。例如:
随着时间步数增加,图像看起来越不像初始图片,或者变得更加嘈杂。但是这对我们有什么帮助呢?这就是有趣的地方;嘈杂的图像会传递给一个预测器,它尝试猜测图像中有多少噪音。因此,我们可以得到噪音的表示。这是一个复杂的过程,让我们来回顾一下到目前为止已经进行的过程。
这可能听起来很混乱,因为我们想要的是一个预测器告诉我们图像的样子,而不是噪声的样子!然而,通过成功预测添加到图像中的噪声的量,我们可以开始从噪声图像中减去这些噪声。所以实际上我们只是在做之前的相反过程,但这次是将图像从噪声状态恢复到原始图像的一个新版本。
这真是关于无条件图像生成的全部内容。现在我们可以从完全的噪声或随机张量开始,并预测和减去噪声,直到最终得到一张像训练中使用的图像。
在开始限制之前,模型需要理解用于限制图像的文本。首先,将文本提示进行分词并传递给一个嵌入层。嵌入层将单词表示为数值向量,其中相似的单词更靠近彼此。例如,“女王”和“国王”之间的关系就如同“女人”和“男人”之间的关系一样。对于这些嵌入向量,将它们想象为一个图表的格式可能更有帮助,尽管简化了实际情况。
这个嵌入已经快要可以用于引导(或条件)图像生成了。但在此之前,它会经过一个转换器,帮助管道理解提示中的单词上下文。这一步的输出是被转换后的提示,现在可以用于条件设置。这个转换步骤还涉及将文本表示为张量,和之前对图像的处理非常相似。
生成的条件部分使用了一种称为交叉注意力的技术,它基本上做的正是其名字所描述的。在生成过程中,当嘈杂的图像变为新的图像时,整个管道都会同时关注图像和文本。因此,模型会根据提示要求的内容来去除噪音。嘈杂的图像现在已经根据文本提示的指导得到了去噪处理!
Stable Diffusion模型的微调
对于文章的这部分,我将使用Keras/TensorFlow文档和他们提供的令人惊叹的案例研究代码。
在进行任何图片生成之前,需要分别准备三个主要组件:
1. 模型:为此,我将使用Keras计算机视觉(Keras CV)Stable Diffusion模型。
2. 训练图像:这些图像都来自Kaggle,并且是公开许可的;
3. 训练提示:用于训练嵌入的提示将由我生成!它们都是手写的,并且非常普遍。
现在了解了这些组件,让我们确定模型的目标:
使用不同的艺术风格和场景,使用Stable Diffusion生成主题(《猫和老鼠》中的汤姆)的新图像。
为了实现这个目标,模型首先需要理解《猫和老鼠》中的汤姆是什么样子的!因此,图像数据集需要经过处理。这一切都是使用Keras完成的。简而言之,下面的代码将会:
1. 将图像裁剪为相同的尺寸
2. 将它们转换为相同的像素范围
3. 将它们从数组转换为张量
4. 将它们随机地以一定的随机变化(在水平轴上翻转)的顺序组装成一个数据集。
这样就得到了一组可以用来训练模型并教给它汤姆的潜在特征的图:
def assemble_image_dataset(files):
# Load images
resize = keras.layers.Resizing(height=512, width=512, crop_to_aspect_ratio=True) # set up the resizing options
images = [keras.utils.load_img(img) for img in files] # load image from file
images = [keras.utils.img_to_array(img) for img in images] # convert image to array
images = np.array([resize(img) for img in images]) # convert list of images to array
# The StableDiffusion image encoder requires images to be normalized to the
# [-1, 1] pixel value range
images = images / 127.5 - 1
# Create the tf.data.Dataset -- this is the part of the code that converts the array to Tensors
image_dataset = tf.data.Dataset.from_tensor_slices(images)
# Shuffle the dataset and introduce random changes and noise through cropping, size changes and aspect ratio
image_dataset = image_dataset.shuffle(50, reshuffle_each_iteration=True)
image_dataset = image_dataset.map(
cv_layers.RandomCropAndResize(
target_size=(512, 512),
crop_area_factor=(0.8, 1.0),
aspect_ratio_factor=(1.0, 1.0),
),
num_parallel_calls=tf.data.AUTOTUNE,
)
# perform random flips to increase the size of the dataset with some images flipped on the horizontal axis
image_dataset = image_dataset.map(
cv_layers.RandomFlip(mode="horizontal"),
num_parallel_calls=tf.data.AUTOTUNE,
)
return image_dataset
下一步是组装文本数据集。为此,需要一个提示的列表。在这种情况下,提示类似于Chat GPT等工具的提示。第一组提示需要代表训练中使用的动画帧的内容。因此,在这种情况下,它们将是相当一般的东西,比如:
1. 一个静止的卡通动画帧,显示 <猫和老鼠中的猫汤姆>
2. 以二维动画形式呈现的在开放的场地中的 <猫和老鼠中的猫汤姆>
然后将这些提示组装成一个自己随机排序的数据集。
# build a text dataset using the tokenizer and concert to a TF dataset
def assemble_text_dataset(prompts):
# add the place holder to the prompt in place of {}
prompts = [prompt.format(placeholder_token) for prompt in prompts]
# encode the prompt
embeddings = [stable_diffusion.tokenizer.encode(prompt) for prompt in prompts]
# pad to the appropriate length and convert to array
embeddings = [np.array(pad_embedding(embedding)) for embedding in embeddings]
# convert to Tensor in a TF dataset and shuffle
text_dataset = tf.data.Dataset.from_tensor_slices(embeddings)
text_dataset = text_dataset.shuffle(100, reshuffle_each_iteration=True)
return text_dataset
# list out some prompts that could be used to describe the training images
tom_prompts = [
'A two dimensional cartoon of {}',
'A cartoon featuring {}',
'An artists impression of {}',
'{} drawn by a cartoonist',
'An animated still from a cartoon containing {}',
'A distance shot of {}',
'A rendering of {}',
'{} trying to catch a mouse',
'{} holding equipment including a tennis racquet',
'A picture of {} surrounded by trees',
'An animation featuring {} outside on a sunny day'
]
现在在上述提示中,短语<Tom from Tom and Jerry>被放在尖括号中,而在代码中,"{}"符号表示将使用<Tom from Tom and Jerry>的位置。这是为了表示它是一个单个标记。在上面的概念复习中,解释了模型将使用标记形式的文本。这有助于模型将文本描述转化为图像的一部分。这很好地引出了培训过程的下一部分,即培训嵌入来理解标记。
首先要做的是为标记获取一组基本值。假设模型可以从某个随机点开始,最终理解“Tom from Tom and Jerry”是不合理的!所以我们给予它帮助,告诉它从“cat”的标记开始:
# initialise the new token <tom-from-tom-and-jerry> to cat
tokenized_initializer = stable_diffusion.tokenizer.encode("cat")[1]
new_weights = stable_diffusion.text_encoder.layers[2].token_embedding(
tf.constant(tokenized_initializer)
)
# Get len of .vocab instead of tokenizer
new_vocab_size = len(stable_diffusion.tokenizer.vocab)
# The embedding layer is the 2nd layer in the text encoder
old_token_weights = stable_diffusion.text_encoder.layers[
2
].token_embedding.get_weights()
old_position_weights = stable_diffusion.text_encoder.layers[
2
].position_embedding.get_weights()
# update the new token to have the same values as the cat token
old_token_weights = old_token_weights[0]
new_weights = np.expand_dims(new_weights, axis=0)
new_weights = np.concatenate([old_token_weights, new_weights], axis=0)
现在模型已经有了一个起点,可以通过一个Fine Tuning对象来训练图像编码器的嵌入。这段代码非常复杂,完整的代码笔记在这个页面上没有显示以避免过载。重要的是要注意,代码中实施了概念复习中的步骤。例如,向图像添加噪声的代码表示如下:
# Sample from the predicted distribution for the training image
latents = sample_from_encoder_outputs(training_image_encoder(images))
# The latents must be downsampled to match the scale of the latents used
# in the training of StableDiffusion. This number is truly just a "magic"
# constant that they chose when training the model.
latents = latents * 0.18215
# Produce random noise in the same shape as the latent sample
noise = tf.random.normal(tf.shape(latents))
batch_dim = tf.shape(latents)[0]
# Pick a random timestep for each sample in the batch to add noise based upon
timesteps = tf.random.uniform(
(batch_dim,),
minval=0,
maxval=noise_scheduler.train_timesteps,
dtype=tf.int64,
)
# Add noise to the latents based on the timestep for each sample
noisy_latents = self.noise_scheduler.add_noise(latents, noise, timesteps)
有一些因素可能会导致混淆,但总体上的想法是噪声调度器是根据选择的时间步数向图像添加一定量的噪声!这个类的其他部分将使用交叉注意力来教模型根据文本提示使用两个编码器进行生成。
模型训练完后,只需开始生成新的图像!模型只需要一组新的提示。这次对提示的限制较少,因为它们代表我们想要生成的内容。本项目中使用的一些提示包括:
1. 三维动画风格的<Tom and Jerry里的Tom>的一帧动画,逼真的照片
2. 高科技机器人版本的<Tom and Jerry里的Tom>在太空中飞行,线描动画
3. <Tom and Jerry里的Tom>的龙与地下城角色卡
将提示传递给Stable Diffusion对象的方法只需简单调用:
generated = stable_diffusion.text_to_image(
f"{placeholder_token} holding a tennis racquet in an open field during the day",
batch_size=3,
)
plot_images(generated)
当然,进行对Tom的图像微调前,模型被告知<Tom from Tom and Jerry>仅仅代表"猫",因此在微调之前,模型返回的图像如下:
这是有道理的,因为我们将 <Tom from Tom and Jerry> 这个令牌的值初始化为“猫”,所以模型只是返回了猫的表示。经过一段时间的模型微调后,我们开始可以看到一些与 Tom 相似的图片!
上面的图片显示了精调阶段后的模型结果。从左上到右依次是Tom变成一个高科技太空机器人的形象,一个3D动画,拿着网球拍的形象,出现在《龙与地下城》角色卡上的形象,出现在一部恐怖电影海报上的形象,最后以抽象的黑白风格呈现。
在打包模型并继续下一步之前,我尝试添加一些与艺术风格相关的提示,以便更加抽象地了解模型学到的东西。我使用的提示试图让模型生成自己对《汤姆和杰瑞》中杰瑞的诠释!
正如你所看到的,模型主要只学会了Tom是一个动画卡通人物,角色设计特征并没有真正传递过来。然而,有趣的是,该模型仍然成功地复制了样式的基本特征。
回顾
我在这个项目中使用的工具非常简单易用。TensorFlow和Keras为使用Stable Diffusion模型提供了非常高级的封装。这些图像的质量相当高,但也存在一些明显的缺陷。这些缺陷主要可以通过训练时使用的迭代次数来解释。
尽管我使用了Kaggle环境并且可以免费使用GPU,但仍然受到了限制!如果我使用了太多图像,或者训练次数过多,并且有太多的回调函数,训练往往会崩溃。真正的限制在于计算资源的内存。当然,在实际应用和真实项目中,这种计算能力将会大幅提升!
关于模型本身,还有一个需要注意的事项是我进行了进一步探索,以了解模型如何学习新的样式。例如,在一次训练中,我试图教模型学习《猫和老鼠》的艺术风格,而不是学习角色本身。这种稍微抽象一些的想法在某些方面处理得很好,但不如教机器学习对象(Tom)那样一致。以下是这些结果:
给模型生成图像所提供的提示从左到右分别是使用《汤姆和杰瑞》卡通中的一场阿波罗11号登月任务,用《汤姆和杰瑞》卡通风格制作的恐怖电影以及将米老鼠放入《汤姆和杰瑞》卡通中。登月任务和恐怖电影都显示出了很大的潜力,但将一个卡通风格转换为另一个风格似乎对我所使用的训练资源来说过于困难。这绝对是我微调过程中的一个限制,可以通过教授模型不仅仅是一个新的标记,而是更新其他标记的权重来解决这个问题。