DataWhaleX魔搭_AI夏令营_四期AIGC方向_task01 学习笔记
AI生图
学习主题介绍
从零入门AI生图原理&实践 是 Datawhale 2024 年 AI 夏令营第四期的学习活动(“AIGC”方向),基于魔搭社区“可图Kolors-LoRA风格故事挑战赛”开展的实践学习——
适合想 入门并实践 AIGC文生图、工作流搭建、LoRA微调 的学习者参与
学习内容提要:从文生图实现方案逐渐进阶,教程偏重图像工作流、微调、图像优化等思路,最后会简单介绍AIGC应用方向、数字人技术(选学)
比赛介绍
本次学习任务基于可图Kolors-LoRA风格故事挑战赛,该比赛同样由阿里云天池平台联合魔搭社区举办。进行学习任务,需要报名参加该比赛来更好地检验自己的学习成果。
赛题内容
参赛者需在可图Kolors 模型的基础上训练LoRA 模型,生成无限风格,如水墨画风格、水彩风格、赛博朋克风格、日漫风格……
基于LoRA模型生成 8 张图片组成连贯故事,故事内容可自定义;基于8图故事,评估 LoRA 风格的美感度及连贯性
对样例的想法与解说
由八图故事表现形式,联想到早期四格漫画
八图故事,顾名思义,由八张图片组成连贯的故事。其实我最初看到这个要求的时候,脑中立马闪过小时候很流行的“四格漫画”。
啥是四格漫画?
四格漫画,是以四个画面分格来完成一个小故事或一个创意点子的表现形式。
四格漫画短短几格就涵盖了一个事件的发生、情节转折及幽默的结局。让人看完不觉莞尔,会心一笑或捧腹大笑。
四格漫画着重点子创意,画面不需很复杂,角色也不需要太多,对白精简,让人容易轻松阅读。
四格漫画是一种古老的艺术表现形式,四格漫画在表现上的特点主要强调叙事。
早期的漫画除了传统的单幅外,就是四格讽刺漫画:开头,发展,高潮,结尾,所以是四格。六格和八格是后来才出现的,情节展开会比较多。其实四格漫画对格数已经没有限制。
当时的四格漫画大多画风简单而突出重点,富有表现力,故事情节简单而富有转折、诙谐幽默冷幽默。
早期四格漫画形式的借鉴意义
而现在,我们用AI生成故事,也就不需要拘泥于漫画的表现风格了,赛题要求中也写道:
如水墨画风格、水彩风格、赛博朋克风格、日漫风格……
而我们要讲的故事所适合的风格,我想这是我们需要思考的。
如在交流群中,有一位同学想讲“指环王里一开头弗罗多的故事”,但生成的图片风格却是比较写实的,头发画的比较细腻,光线也打的较为柔和,整体全是人物近景,有背景的图片中,背景又虚化比较严重……导致该同学表示“生成的好像和我想的一点儿关系也没有,除了衣服配上了”,这就说明该同学存在的问题是描述细节多,而对整体风格要求可能较少。
风格上,我们不必拘泥于早期四格漫画形式,但在如何讲好一个故事上,我想四格漫画有很重要的参考意义。
在早期四格漫画中,故事大多具有转折或幽默特点,四格中一般即起、承、转、结,前三格铺陈蓄势,第四格让人意想不到;
使用的对白尽量精简,不用对白只靠表情跟动作是最好也是最难的演出;但是,由于AI绘图很难绘制出正确的文字,所以这一点我们可以不予考虑(记得要在反向提示词中加入”text”以防万一),取而代之的是,我们可以在故事的描述文字上(示例中每张图片上方的文字)多做注意,做到文字简明、概括。
动作、表情、对白、情节安排必须符合各个角色设定的性格特征与一贯性。如果在仅仅八格的内容中就出现了角色前后形象大不一致(不是由于剧情发展而引起的正常转变)、甚至直接人设崩塌,这是绝对不能接受的。
样例是如何生成的?
你可能使用过AI生成图片,那么对于我们要做的事情应该很清楚:给AI提示词。
什么是 prompt ?
在使用大语言模型时,使用 prompt 往往可以获得更好、更精确的结果。prompt直译为“提示语”。现在已经有专门研究大语言模型prompt的工作(prompt engineer)。
我们也可以简单地理解为在这次任务中,我们对于AI的要求。试想:我们在请人类作画时,尚且需要提出要求呢。
我们可以打开baseline.ipynb查看样例所给的prompt。
由于现在还没有按照教程打开魔搭社区notebook,且我们只是要先下载baseline.ipynb文件看一眼,所以我在本地新建一个临时文件夹进行了clone:
git lfs install
git clone https://www.modelscope.cn/datasets/maochase/kolors.git
打开文件夹,找到baseline.ipynb文件并用notepad–等类似软件打开,在稍后面一点的位置,找到了八组prompt。去掉格式化字符”\n”等后内容如下:
8组prompt内容
二次元,一个紫色短发小女孩,在家中沙发上坐着,双手托着腮,很无聊,全身,粉色连衣裙
丑陋、变形、嘈杂、模糊、低对比度
二次元,日系动漫,演唱会的观众席,人山人海,一个紫色短发小女孩穿着粉色吊带漏肩连衣裙坐在演唱会的观众席,舞台上衣着华丽的歌星们在唱歌
丑陋、变形、嘈杂、模糊、低对比度
二次元,一个紫色短发小女孩穿着粉色吊带漏肩连衣裙坐在演唱会的观众席,露出憧憬的神情
丑陋、变形、嘈杂、模糊、低对比度,色情擦边
二次元,一个紫色短发小女孩穿着粉色吊带漏肩连衣裙,对着流星许愿,闭着眼睛,十指交叉,侧面
丑陋、变形、嘈杂、模糊、低对比度,扭曲的手指,多余的手指
二次元,一个紫色中等长度头发小女孩穿着粉色吊带漏肩连衣裙,在练习室练习唱歌
丑陋、变形、嘈杂、模糊、低对比度
二次元,一个紫色长发小女孩穿着粉色吊带漏肩连衣裙,在练习室练习唱歌,手持话筒
丑陋、变形、嘈杂、模糊、低对比度
二次元,紫色长发少女,穿着黑色连衣裙,试衣间,心情忐忑
丑陋、变形、嘈杂、模糊、低对比度
二次元,紫色长发少女,穿着黑色礼服,连衣裙,在台上唱歌
丑陋、变形、嘈杂、模糊、低对比度
我们再将大赛所给样例拿出来进行对照:
这样一来,你是否理解了prompt、negative_prompt的作用呢?其中,negative_prompt为排除项,即negative_prompt中写上“多余的手指”,则AI生成的图片中就不会有多余的手指。
我们在写自己的prompt时,可以借助于一些prompt工具,如我使用的AI绘画提示词生成器,预览如下:
样例存在哪些问题?
大赛所给样例只是最基本的完成情况,我们可以挑一挑其中存在的问题,在我们自己作画时避免。
首先是故事讲述的问题,这是一个极其平淡的故事。
其次,整个画面风格有些奇怪。这应该也是由于prompt对风格要求较少导致的。
注意到图片4中negative_prompt多次出现手指
首先我们看到:图片4的prompt中有 十指交叉
的要求。而即使图片4negative_prompt中排除了扭曲的手指,多余的手指,AI绘制出来的手指也稍显奇怪。
为什么AI总是画不好人的手呢?这是一个非常经典的问题,了解这个问题可以让我们对AI绘画的原理理解更深。
其实原因很简单:
- 人手动作极多。人的手看似结构简单,只有五根指头,仿佛画好五根指头就可以了。然而,对于AI来说,人手做出的每个动作都是完全不同的。举个例子:我们先虚握拳头,让手围成一个圈,这是第一个动作;然后紧握拳头,这是第二个动作。对于人类来说,我们知道这两个动作是类似的,可能只是幅度的不同。然而对于AI来说,给它两张这样的动作图片,它将感到十分困惑:为什么手有时候是这样的,有时候是那样的?而这只是一个浅显的例子,在这个例子中,我们尚且可以告诉AI:第一个动作叫虚握,第二个动作叫实握,从而让AI理解。但是人手的动作实在太多了,我们既不可能给每个有细微差别的动作都起名,也不大可能准备如此海量的训练数据喂给AI。
- 人手空间变化多。人的手十分灵巧,关节很多、自由度很大,而我们训练生成图片的AI,使用的自然也是图片,这就导致一个问题:在照片中,当人手做出某个动作时,就可能有部分手指看不到,这就导致AI不清楚人类到底应该有几根手指。另一个严重的问题是使AI分不清楚各个手指的粗细关系。比如,同样是竖两根手指,比个正常的耶(✌),AI发现这两根手指差不多长。接着我们竖起来食指和小拇指,小拇指比食指短很多,AI就要变得困惑了。
那么,如果我们一直只喂给AI手完全打开的、正面的图片,AI是不是就不会画错了呢?
是的,但AI也就只能画出手完全打开的、正面的图片了。
- 人手的细节非常复杂,包括指甲、指纹、皮肤褶皱等,这些需要很高的算力才能画出。
同样的问题有时也会出现在AI试图绘画更小的特征时,比如耳朵,看起来像没有复杂软骨结构的肉质漩涡;或者牙齿,不正确地放在嘴里;或者瞳孔,变成了小山羊的斑点。AI可以掌握视觉模式,但无法掌握潜在的生物逻辑。
暂时地,AI还有很多东西都画不好。
文生图的历史
(点击展开介绍)
早期探索(20世纪60年代-20世纪90年代)
文生图的概念最早出现于计算机视觉和图像处理的早期研究中。
早期的图像生成技术主要依赖于规则和模板匹配,通过预定义的规则将文本转换为简单的图形。
然而,由于计算能力和算法的限制,这一阶段的技术能力非常有限,生成的图像质量较低,应用场景也非常有限。
基于统计模型的方法(2000年代)
进入2000年代,随着统计模型和机器学习技术的发展,文生图技术开始得到更多关注。
研究者们开始利用概率图模型和统计语言模型来生成图像。尽管这一阶段的技术在生成图像的多样性和质量上有了一定提升,但由于模型的复杂性和计算资源的限制,生成的图像仍然较为粗糙,不够逼真。
深度学习的崛起(2010年代)
2010年代是文生图技术发展的一个重要转折点。
随着深度学习,尤其是卷积神经网络(CNN)和生成对抗网络(GAN)的发展,文生图技术取得了突破性进展。2014年,Goodfellow等人提出的GAN模型通过生成器和判别器的对抗训练,极大地提升了图像生成的质量。随后,各类变种GAN模型被提出,如DCGAN、Pix2Pix等,使得文生图技术在生成逼真图像方面达到了前所未有的高度。
大规模预训练模型(2020年代)
进入2020年代,大规模预训练模型如OpenAI的CLIP、DALL-E以及Stable Diffusion等的出现,标志着文生图技术进入了一个新的时代。
CLIP通过大规模的文本和图像配对数据训练,能够理解和生成高度一致的文本和图像;DALL-E和Stable Diffusion进一步提升了生成图像的创意和细节表现能力,使得通过简单的文本描述生成高质量、复杂图像成为可能。
这些技术的应用范围从艺术创作、广告设计到辅助医疗诊断,展现了广泛的商业价值和社会影响力。
Task1 流程回顾
该部分内容详见Task1学习手册,以下仅作简要回顾。
开通阿里云PAI-DSW试用
报名赛事
在魔搭社区创建实例
如果 在魔搭无法授权 或 点击【打开】无法打开,可到阿里云控制台创建 & 打开实例
如果之前试用的额度已经过期,可使用魔搭的免费Notebook实例
跑代码!
下载baseline文件
git lfs install
git clone https://www.modelscope.cn/datasets/maochase/kolors.git
安装环境,然后重启kernel
打开baseline文件,运行第一块儿代码。
安装 Data-Juicer 和 DiffSynth-Studio
Data-Juicer:数据处理和转换工具,旨在简化数据的提取、转换和加载过程
DiffSynth-Studio:高效微调训练大模型工具
然后重启kernel。最上方点击这个按钮。
认准鼠标悬停时显示 restart
的那个。
调整prompt
但不要运行这部分代码块儿。
依次顺序运行剩余的代码块
需要一个一个去点代码框左上角执行按钮。
为节省时间,以上两步可同时进行,即在运行前面的代码块儿同时修改prompt。
但要注意代码块儿执行需要按顺序执行。
代码块按照功能主要分成这几类:
- 使用Data-Juicer处理数据,整理训练数据文件
- 使用DiffSynth-Studio在基础模型上,使用前面整理好的数据文件进行训练微调
- 加载训练微调后的模型
- 使用微调后的模型,生成用户指定的prompt提示词的图片
上传结果
关闭实例
代码一个小提示
images = [np.array(Image.open("{i}.jpg")) for i in range(1, 9)],
image = np.concatenate([
np.concatenate(images[0:2], axis=1),
np.concatenate(images[2:4], axis=1),
np.concatenate(images[4:6], axis=1),
np.concatenate(images[6:8], axis=1),
], axis=0),
image = Image.fromarray(image).resize((1024, 2048)),
image
这是最后一段代码,运行完这段代码后,将在主界面显示我们八张图片组成的故事。这是因为在python中,输入变量的名称回车,会返回变量的值。
而这段代码中,变量名称就是image。我们还可以在下面补上一句:
image.save("all.jpg")
这样它还会将我们的八图组成的图片保存为”all.jpg”。
部分代码解读
!pip install simple-aesthetics-predictor
!pip install -v -e data-juicer
!pip uninstall pytorch-lightning -y
!pip install peft lightning pandas torchvision
!pip install -e DiffSynth-Studio
我们上文已经提到,这部分是安装环境。
#下载数据集
from modelscope.msdatasets import MsDataset
ds = MsDataset.load(
'AI-ModelScope/lowres_anime',
subset_name='default',
split='train',
cache_dir="/mnt/workspace/kolors/data"
)
import json, os
from data_juicer.utils.mm_utils import SpecialTokens
from tqdm import tqdm
os.makedirs("./data/lora_dataset/train", exist_ok=True)
os.makedirs("./data/data-juicer/input", exist_ok=True)
with open("./data/data-juicer/input/metadata.jsonl", "w") as f:
for data_id, data in enumerate(tqdm(ds)):
image = data["image"].convert("RGB")
image.save(f"/mnt/workspace/kolors/data/lora_dataset/train/{data_id}.jpg")
metadata = {"text": "二次元", "image": [f"/mnt/workspace/kolors/data/lora_dataset/train/{data_id}.jpg"]}
f.write(json.dumps(metadata))
f.write("\n")
下载数据集。
data_juicer_config = """
# global parameters
project_name: 'data-process'
dataset_path: './data/data-juicer/input/metadata.jsonl' # path to your dataset directory or file
np: 4 # number of subprocess to process your dataset
text_keys: 'text'
image_key: 'image'
image_special_token: '<__dj__image>'
export_path: './data/data-juicer/output/result.jsonl'
# process schedule
# a list of several process operators with their arguments
process:
- image_shape_filter:
min_width: 1024
min_height: 1024
any_or_all: any
- image_aspect_ratio_filter:
min_ratio: 0.5
max_ratio: 2.0
any_or_all: any
"""
with open("data/data-juicer/data_juicer_config.yaml", "w") as file:
file.write(data_juicer_config.strip())
!dj-process --config data/data-juicer/data_juicer_config.yaml
import pandas as pd
import os, json
from PIL import Image
from tqdm import tqdm
texts, file_names = [], []
os.makedirs("./data/lora_dataset_processed/train", exist_ok=True)
with open("./data/data-juicer/output/result.jsonl", "r") as file:
for data_id, data in enumerate(tqdm(file.readlines())):
data = json.loads(data)
text = data["text"]
texts.append(text)
image = Image.open(data["image"][0])
image_path = f"./data/lora_dataset_processed/train/{data_id}.jpg"
image.save(image_path)
file_names.append(f"{data_id}.jpg")
data_frame = pd.DataFrame()
data_frame["file_name"] = file_names
data_frame["text"] = texts
data_frame.to_csv("./data/lora_dataset_processed/train/metadata.csv", index=False, encoding="utf-8-sig")
data_frame
处理数据集,保存数据处理结果。
# 下载模型
from diffsynth import download_models
download_models(["Kolors", "SDXL-vae-fp16-fix"])
#模型训练
import os
cmd = """
python DiffSynth-Studio/examples/train/kolors/train_kolors_lora.py \
--pretrained_unet_path models/kolors/Kolors/unet/diffusion_pytorch_model.safetensors \
--pretrained_text_encoder_path models/kolors/Kolors/text_encoder \
--pretrained_fp16_vae_path models/sdxl-vae-fp16-fix/diffusion_pytorch_model.safetensors \
--lora_rank 16 \
--lora_alpha 4.0 \
--dataset_path data/lora_dataset_processed \
--output_path ./models \
--max_epochs 1 \
--center_crop \
--use_gradient_checkpointing \
--precision "16-mixed"
""".strip()
os.system(cmd)
lora微调。
Stable Diffusion中的Lora(LoRA)模型是一种轻量级的微调方法,它代表了“Low-Rank Adaptation”,即低秩适应。Lora不是指单一的具体模型,而是指一类通过特定微调技术应用于基础模型的扩展应用。在Stable Diffusion这一文本到图像合成模型的框架下,Lora被用来对预训练好的大模型进行针对性优化,以实现对特定主题、风格或任务的精细化控制。
from diffsynth import ModelManager, SDXLImagePipeline
from peft import LoraConfig, inject_adapter_in_model
import torch
def load_lora(model, lora_rank, lora_alpha, lora_path):
lora_config = LoraConfig(
r=lora_rank,
lora_alpha=lora_alpha,
init_lora_weights="gaussian",
target_modules=["to_q", "to_k", "to_v", "to_out"],
)
model = inject_adapter_in_model(lora_config, model)
state_dict = torch.load(lora_path, map_location="cpu")
model.load_state_dict(state_dict, strict=False)
return model
# Load models
model_manager = ModelManager(torch_dtype=torch.float16, device="cuda",
file_path_list=[
"models/kolors/Kolors/text_encoder",
"models/kolors/Kolors/unet/diffusion_pytorch_model.safetensors",
"models/kolors/Kolors/vae/diffusion_pytorch_model.safetensors"
])
pipe = SDXLImagePipeline.from_model_manager(model_manager)
# Load LoRA
pipe.unet = load_lora(
pipe.unet,
lora_rank=16, # This parameter should be consistent with that in your training script.
lora_alpha=2.0, # lora_alpha can control the weight of LoRA.
lora_path="models/lightning_logs/version_0/checkpoints/epoch=0-step=500.ckpt"
)
加载微调好的模型。
torch.manual_seed(0)
image = pipe(
prompt="二次元,一个紫色短发小女孩,在家中沙发上坐着,双手托着腮,很无聊,全身,粉色连衣裙",
negative_prompt="丑陋、变形、嘈杂、模糊、低对比度",
cfg_scale=4,
num_inference_steps=50, height=1024, width=1024,
)
image.save("1.jpg")
图片生成(改变种子值重复8次)。
最后是我们上一节中提到的将8张图片合为一张。
思考
这个图片的风格能不能修改,能不能微调出来一个其他风格类型的图片?
点击展开
尽管比赛要求要不同的风格、我们也在注意不同风格的尝试,但经过不断的调整和观察对比大家的结果,我认为如果不对代码部分进行其它调整的话,这个图片的风格是不会改变的,即一直倾向于二次元风格。我认为这可能与训练数据有关。
而在上文代码解读中, 下载数据集
部分里也写明了我们训练的数据只有“二次元”。
结果展示
自己写了prompt
组成故事
无明显bug
效果满意
我的第一张图片是测试上文中提到的prompt生产工具,按照自己喜好选了几个prompt生成的。接下来7张图都是调整prompt试图生成一张《孤独摇滚》中的波奇。没有进行连贯故事的尝试,
画风崩坏还是挺严重的。
从效果上看,第6张与波奇最为接近,但同时第6张也是作画bug最大的一张:手臂不协调、手指数量不正确、吉他那儿有不明意义的大幅色块儿。
关于发卡:从一开始我就没指望AI能正确理解波奇的发卡长什么样子,但是画成这样还是让我很惊喜了。整体的氛围也很像《孤独摇滚》。