chatglm2-2b+sdxl1.0+langchain打造私有AIGC(五)

一、实现目标

上篇文章写完LLM的Agent之后,流程应该是进入到了SDXL的“文生图”“图生图”阶段了

目标很明确,使用SDXL为ChatGLM生成的内容进行配图,说明:大部分使用SD模型的大神都是使用SD模型配套的开源WebUI,因为我主打一个折腾,所以自己使用diffusers库开发自己的应用

1.要根据不同内容生成不同“风格”的图片,比如:电影风格,摄影风格,动漫风格,水墨风格,彩绘风格....

2.某些特定场景下需要有人物出境,模型预训练的时候的人物原型并不能满足“私有AIGC”这个flag,所以必须微调模型打造自己的人物角色

3.某些时候我希望能够进行多风格融合,比如人物写真融入彩绘,原始模型无法完成,所以必须通过代码进行loRA模型融合

需求目标明确之后,就开始实现吧...

二、实现路径

2.1、sdxl原始模型接入

2.1.1 模型下载

从huggingface下载SDXL模型(SDXL模型相较于SD模型,在prompt上更具友好性,XL是通过类自然语言进行提示词解析(也支持Tag标记),而SD是通过Tag标记进行解析,而且XL在图像处理的细腻度上个人感觉比SD更好)

下载地址:

https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0

https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0

为什么会有两个地址?,因为SDXL模型分为base模型和refiner模型,base是基础模型,refiner是精炼模型,意思就是一张图首先通过base进行处理,然后交给refiner进行精加工,最后产生最终的图片。

从huggingface上拔下来的一张流程图

当然,这两个模型并不是必须结合在一起使用,通常情况下可以只使用Base模型,由Base模型直接产出1024x1024的最终图,也是没有任何问题的,后面我会给大家展示一下结合使用和分开使用的代码和最终图片效果

2.1.2 加载模型

模型下载到本地之后,可以通过diffusers库对模型进行加载

from diffusers import DiffusionPipeline, StableDiffusionXLImg2ImgPipeline

 base模型加载:

 def load_model(self):
     if self.base_model is None:
     # 这里并不是一定要制定vae,如果不指定vae,DiffusionPipeline会加载sdxl模型内部原本的vae
        self.vae = AutoencoderKL.from_pretrained(
                f"{self.model_id}\vae_fix", torch_dtype=torch.float16)
        # 初始化模型
        self.base_model = DiffusionPipeline.from_pretrained(
                self.model_id, # 模型存放本地磁盘路径
                vae=self.vae, # 指定模型使用的vae组件,可选项,不是必须指定
                torch_dtype=torch.float16, # 加载半精度的模型参数,显存够大的可以忽略
                use_safetensors=True, # 是否允许使用saftensor格式的参数文件
                variant="fp16"  # 加载半精度的模型参数,显存够大的可以忽略
            )
        # 在GPU上进行推理 显存够大的可以直接上CUDA推理
        if self.is_cuda is True:
           self.base_model.to("cuda")
        # GPU+CPU联合推理
        else:
           self.base_model.enable_model_cpu_offload()

在我这篇文章中就不详细解释SD模型内部的unet,vae之类的概念了,有兴趣的同学可以自己去查一查,在这里我主要讲怎么用,科普类的东西如果有时间,我后面专门讲。这里我只简单介绍一些为什么我要单独指定vae,因为,在sdxl的官方微调文章里说明了sdxl原本的vae数值并不稳定,如果涉及到微调建议使用madebyollin/sdxl-vae-fp16-fix,为了打造自己的私有人物角色,所以我必须微调,进而在这里指定了vae

refiner模型加载:

def load_model(self):
        if self.is_combine_base is True: # 如果要和base模型结合使用
            # 处理base模型 SD_Base_Model是自己写的一个封装base模型的类
            if self.base_model is None:
                self.base_model = SD_Base_Model.instance(
                    n_steps=self.n_steps, # 模型在处理图片时迭代次数
                    high_noise_frac=self.high_noise_frac, # 结合n_steps使用表示,base模型处理百分比
                    is_cuda=self.is_cuda, # 是否在cuda上推理
                    H=self.H / 2, # 图片在base模型中的高
                    W=self.W / 2) # 图片在base模型中的宽
            if self.base_model.base_model is None:
                self.base_model.load_model()
            # 处理refinerModel
            if self.refiner_model is None:
                self.refiner_model = DiffusionPipeline.from_pretrained(
                    self.model_id,
                    torch_dtype=torch.float16,
                    variant="fp16",
                    use_safetensors=True,
                    text_encoder_2=self.base_model.base_model.text_encoder_2, # 提示词编码器使用base模型的编码器
                    vae=self.base_model.base_model.vae) # vae使用base模型的vae
        else: # 如果单独使用refiner,通常情况是"图片生成图片"的场景
            self.base_model = None
            if self.refiner_model is None:
                self.refiner_model = StableDiffusionXLImg2ImgPipeline.from_pretrained(
                    self.model_id,
                    torch_dtype=torch.float16,
                    variant="fp16",
                    use_safetensors=True)
        if self.is_cuda is True:
            self.refiner_model.to("cuda")
        else:
            self.refiner_model.enable_model_cpu_offload()

注意:单独使用refiner的话,建议使用StableDiffusionXLImg2ImgPipeline类进行加载,通常实在以图生图的场景下使用

另外再单独说一个参数:high_noise_frac,这个参数的意思是,在base与refiner结合使用的情况下,如果总的图像处理迭代次数为50,high_noise_frac为0.8的话,那么在base中将进行50*0.8=40次迭代,在refiner中进行剩下的10次迭代

base模型单独使用时生成图片方法定义:

# 使用基础模型生成图片 返回PIL图片
def get_image_by_single_prompt(self,query: str, image_count: int = 1,
                                negative_prompt: str = None):
    # seed = 1337
    # generator = torch.Generator("cuda").manual_seed(seed)
    # self.base_model 就是DiffusionPipeline.from_pretrained()这个方法返回的对象,
    # 参见上面base模型加载片段
    images = self.base_model(query,
              num_inference_steps=self.n_steps,
              guidance_scale=self.guidance_scale, # 数值越高越prompt越相符合
              num_images_per_prompt=image_count, # 针对每一条prompt生成多少张图片
              negative_prompt=negative_prompt, # 负面提示词,告诉模型在生成图像时,不要生成什么
              height=self.H,
              width=self.W).images
    return images

这个方法返回的是一个PIL图片集合,可以直接使用PIL的save方法将图片保存到本地,这个方法适用于base模型单独使用的时候

from PIL import Image...for i in range(len(images)):
   images[i].save(f"D:\pics\image_{i}.jpg", "JPEG")

refiner模型单独使用时生成图片方法定义:

def get_image_to_image_single_prompt(self,
                                         query: str,
                                         image_url: str = None,
                                         image_count: int = 1,
                                         negative_prompt: str = None):  
        target_size: tuple[int, int] = (self.H, self.W) 
        init_image = load_image(image_url).convert("RGB")
        # self.refiner_model就是StableDiffusionXLImg2ImgPipeline.from_pretrained()这个方法返回的对象,
        # 参见上面refiner模型加载片段
        images = self.refiner_model(prompt=query,
                                        image=init_image,
                                        num_inference_steps=self.n_steps,
                                        guidance_scale=self.guidance_scale,
                                        negative_prompt=negative_prompt,
                                        num_images_per_prompt=image_count,
                                        target_size=target_size).images
        return images

注意:此方法是针对于以图生图的场景使用,refiner模型没有W,H这两个参数,官方将这两个参数封装到了target_size这个元组类型的变量中了

base与refiner结合生成图片的方法定义:

说到base与refiner的结合使用,首先要知道一个概念“潜在空间”,从使用的角度上说,base输出的图像是在潜在空间上,而后由refiner进一步加工,最后在像素空间完成最终图片(关于潜在空间的概念详细,请自行百度)

# base模型在潜在空间生成图片
def get_base_latent_image_single_prompt(self,
                                            query: str,
                                            image_count: int = 1,
                                            negative_prompt: str = None):
    images = self.base_model(query,
                                 num_inference_steps=self.n_steps,
                                 denoising_end=self.high_noise_frac,
                                 num_images_per_prompt=image_count,
                                 negative_prompt=negative_prompt,
                                 # 这里output_type="latent"就表示,输出的图片为潜在空间图片
                                 output_type="latent").images  
    return images

# 通过基础图片+单条提示词得到另一些精炼加工图片
# 对【refiner模型单独使用时生成图片方法定义】段落中的方法进行改造
# 将单独使用refiner和结合base使用,整合到一个方法里面
def get_image_to_image_single_prompt(self,
                                         query: str,
                                         image_url: str = None,
                                         image_count: int = 1,
                                         negative_prompt: str = None):

        # 获取潜在空间的图片,这个内部方法中在调用base模型类的
        # get_base_latent_image_single_prompt方法
        def _get_base_latent_images_single_prompt(query: str,
                                                  image_count: int = 1,
                                                  negative_prompt: str = None):
            if self.base_model is not None:
                images = self.base_model.get_base_latent_image_single_prompt(
                    query=query,
                    image_count=image_count,
                    negative_prompt=negative_prompt)
                return images
            else:
                return None
        # 定义图片尺寸
        target_size: tuple[int, int] = (self.H, self.W)
        if image_url is None and self.is_combine_base is True:
            # 结合base模型一起使用refiner
            init_images = _get_base_latent_images_single_prompt(
                query, image_count, negative_prompt)
            images = self.refiner_model(prompt=query,
                                        num_inference_steps=self.n_steps,
                                        denoising_start=self.high_noise_frac,
                                        num_images_per_prompt=image_count,
                                        negative_prompt=negative_prompt,
                                        image=init_images,
                                        target_size=target_size).images
        else:
            # 单独使用refiner
            init_image = load_image(image_url).convert("RGB")
            images = self.refiner_model(prompt=query,
                                        image=init_image,
                                        num_inference_steps=self.n_steps,
                                        guidance_scale=self.guidance_scale,
                                        negative_prompt=negative_prompt,
                                        num_images_per_prompt=image_count,
                                        target_size=target_size).images
        return images

到这里为止就将SDXL模型简单的集成到了自己的应用中

2.2、sdxl画风定义

现在要满足我需要风格多样性的需求,可以通过lora微调画风实现,当然更直接有效的方法就是通过"提示词",在sdxl预训练的过程中早就已经定义好了多种风格,通过提示词就可以让最终的图片按既定风格输出,我先展示一下我生成的部分风格的图片,

电影风格:

自己输入的提示词:一个红衣女孩走向雪山深处

摄影风格:

提示词:草原在连绵不断的雪山脚下,天空中飘着朵朵白云,白云显得很低

动漫风格:

提示词:青山环绕着碧绿的湖水

漫画风格:

提示词:一位拿着长剑身穿金甲的将军

还有很多,不一一展示了,直接看怎么实现吧

首先需要定义一个风格数组

style_list = [
    {
        "name": "(No style)",
        "prompt": "{prompt}",
        "negative_prompt": "",
    },
    {
        "name":
        "电影",
        "prompt":
        "cinematic still, {prompt} . emotional, harmonious, vignette, highly detailed, high budget, bokeh, cinemascope, moody, epic, gorgeous, film grain, grainy",
        "negative_prompt":
        "anime, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, bad finger, finger, extra finger, strong finger",
    },
    {
        "name":
        "摄影",
        "prompt":
        "cinematic photo {prompt} . 35mm photograph, film, bokeh, professional, 4k, highly detailed",
        "negative_prompt":
        "drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly",
    },
    {
        "name":
        "动漫",
        "prompt":
        "anime artwork {prompt} . anime style, key visual, vibrant, studio anime,  highly detailed",
        "negative_prompt":
        "photo, deformed, black and white, realism, disfigured, low contrast",
    },
    {
        "name":
        "漫画",
        "prompt":
        "manga style {prompt} . vibrant, high-energy, detailed, iconic, Japanese comic style",
        "negative_prompt":
        "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, Western comic style",
    },
    {
        "name": "数字艺术",
        "prompt":
        "concept art {prompt} . digital artwork, illustrative, painterly, matte painting, highly detailed",
        "negative_prompt": "photo, photorealistic, realism, ugly",
    },
    {
        "name":
        "像素艺术",
        "prompt":
        "pixel-art {prompt} . low-res, blocky, pixel art style, 8-bit graphics",
        "negative_prompt":
        "sloppy, messy, blurry, noisy, highly detailed, ultra textured, photo, realistic",
    },
    {
        "name":
        "幻想艺术",
        "prompt":
        "ethereal fantasy concept art of  {prompt} . magnificent, celestial, ethereal, painterly, epic, majestic, magical, fantasy art, cover art, dreamy",
        "negative_prompt":
        "photographic, realistic, realism, 35mm film, dslr, cropped, frame, text, deformed, glitch, noise, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, sloppy, duplicate, mutated, black and white",
    },
    {
        "name":
        "赛博朋克",
        "prompt":
        "neonpunk style {prompt} . cyberpunk, vaporwave, neon, vibes, vibrant, stunningly beautiful, crisp, detailed, sleek, ultramodern, magenta highlights, dark purple shadows, high contrast, cinematic, ultra detailed, intricate, professional",
        "negative_prompt":
        "painting, drawing, illustration, glitch, deformed, mutated, cross-eyed, ugly, disfigured",
    },
    {
        "name": "3D模型",
        "prompt":
        "professional 3d model {prompt} . octane render, highly detailed, volumetric, dramatic lighting",
        "negative_prompt": "ugly, deformed, noisy, low poly, blurry, painting",
    },
    {
        "name":
        "国风",
        "prompt":
        "Chinese ink painting, masterpiece, best quality, aesthetic, editorial photography, urban street photography, {prompt} . cold color theme, depth of field",
        "negative_prompt":
        "watermark, low quality, medium quality, blurry, censored, wrinkles, deformed, mutated text, watermark, low quality, medium quality, blurry, censored, wrinkles, deformed, mutated,two head",
    },
    {
        "name":
        "人物",
        "prompt":
        " {prompt} . Best quality, realistic, photorealistic, (hyperdetailed:1.15), 4K, masterpiece, ultra high res, (photorealistic:1.4), raw photo, film grain, hard lighting, beautiful face",
        "negative_prompt":
        "blurry, bad feet, cropped, poorly drawn hands, poorly drawn face, mutation, deformed, worst quality, low quality, normal quality, extra fingers, fewer digits, extra limbs, extra arms, extra legs, malformed limbs, fused fingers,too many fingers, long neck, cross-eyed, mutated hands, polar lowres, bad body, bad proportions, gross proportions, error, missing fingers, missing arms, missing legs, extra digit, extra arms, extra leg, extra foot, (((missing arms))),(((missing legs))), (((extra arms))), (((extra legs))), illustration, 3d, sepia, painting, cartoons, sketch, (worst quality:2), ((monochrome)), ((grayscale:1.2)), (backlight:1.2), analog, analogphoto",
    },
]

这个素组的每一个元素,都有name(风格名称)prompt(提示词)negative_prompt(负面提示词),大部分都是固定的,我们不需要做任何修改,我们只需要写代码将我们输入的提示词替换掉选定风格prompt属性里的{prompt}即可(我们输入的提示词可以是Tag标记,也可以是一段自然语言,主要是描述图片里需要包含什么信息,而图片的风格,光照,效果这些上述文档根据不同风格都已经定义好了,几乎不用调整)

风格定义好之后就需要写代码替换掉{prompt} 然后调用refiner的图片生成方法(在没有挂在lora模型的时候我是base+refiner结合使用的)

特别说明:SDXL的提示词都必须使用英文,它对中文的理解几乎可以说是不及格,所以用中文做提示词必定生成的图片也不及格

我是调用百度翻译的API来翻译中文的,至于怎么调用,可以参见百度翻译的文档

# 翻译prompt
def get_image_flags(self, text_to_image_list: list, style: dict):     
        texts_result: List[str] = []
        for item in text_to_image_list:
            time.sleep(3)
            # 调用百度翻译的API,将中文翻译成英文,如果调用频率太高会报错,
            # 所以每次休眠3秒后再调用
            texts_result.append(Translation_Baidu.excute_translation(item))
        return {
            "texts_or_images": texts_result,
            "style": style
        }
        # 为每一张图片命名def _name_image(self, sentence: str):
        words = sentence.split()
        initials = [word[0] for word in words]
        return "".join(initials)

# 生成图片
def generate_images(self, texts_or_images:list, style: dict, loras: list):
        sd_model = SD_Refiner_Model.instance(is_combine_base=True)
        sd_model.load_model()
        image_addr = []
        for item in texts_or_images:
            try:
                name = self._name_image(item)           
                prompt = style["prompt"].format(prompt=item).lower()
                negative_prompt = style["negative_prompt"]
                target_image = sd_model.get_image_to_image_single_prompt(
                    query=prompt,
                    image_count=self.num_per_prompt_image,
                    negative_prompt=negative_prompt)
                for i in range(len(target_image)):
                    target_image[i].save(
                        f"{self.base_file_path}\{name}_{i}.jpg", "JPEG")
                    image_addr.append(f"{self.base_file_path}\{name}_{i}.jpg")
            except Exception:
                pass
        return image_addr

2.3、loRA微调真人角色

第二个需求:打造私有的人物角色,基于这个需求,采用loRA微调是一个很普遍的选择,简单介绍一下,loRA微调的意思就是,基于原始模型的参数W,用少量的“图片+文本”数据,训练出另一组参数△W,在使用时将原始模型的参数和loRA参数加起来满足按既定设想输出图片的需求,及 W'=W+△W,在生成图片的时候模型的参数就变成W'

那么如何训练呢?网上一搜一大把,这里我只简单介绍一下具体过程,我会详细指出其中的关键点,因为我在训练loRA的过程中其实花费了大概2周时间,训练了十几次,效果都不理想,最后总结出规律,loRA真人训练没有任何技术上的壁垒,影响结果质量好坏的关键在于训练素材:

一.图片一定要清晰,一定要清晰,

二.图片构图一定要简单,一定要简单(花里胡哨的背景就不要来了),

三.人物脸部不要遮挡,人物脸部不要遮挡(什么墨镜,什么用手挡住半边脸这种不要来)

四.Tag标记要准确

关于Tag标记也是我老是训练效果不理想的罪魁祸首,如果你想AI画出来的人物和真实人物几乎一样,那么你的tag标记里面就不要有任何脸部的描述如果你想保留真实人物的发型,那么就不要有关于头发的描述,一句话:想要AI的成图里保留什么,你就不要在Tag里面标记什么。

之前我在训练真人的时候,是把每一个细节都Tag出来,比如,眼睛,嘴唇,鼻子,牙齿,耳环,长发,咧着嘴唇,微笑,闭着嘴唇.....,这样一来AI就会将这些标记的部分分开去学习,最终出图的时候确实不好控制这些器官组合到一张脸上的效果。看看我训练的真人图像吧,都是base模型挂载lora之后出的原图没做任何精炼处理

提示词:higirl,一个女孩站在海边
提示词:higirl,一个女孩在雪地里
提示词:higirl,一个女孩穿着一件黑色T恤

higirl是我这个lora模型的触发词, 由于是真人,真人照片就不方便展示了,我只能说有7-8层相似

2.3.1、lora训练流程:

1、准备炼丹炉(训练器)

网上有很多大神开源了各种炼丹炉,比如赛博炼丹炉,秋叶炼丹炉,目前赛博好像还不能炼SDXL,秋叶炼丹炉可以,但是我是两个都用了,赛博炼丹炉用来图片预处理,打Tag(可视化的Tag,很直观),秋叶炼丹炉用来炼制lora模型

赛博丹炉下载地址:

网盘链接:https://pan.baidu.com/s/1_yB_pNrNGotudYmOOwjp8g提取码:fapv

秋叶丹炉下载地址:

链接:https://pan.baidu.com/s/1-AN-ulR3PTS6KYyWVPARNA提取码:vtse

2、图片预处理

赛博怎么预处理图片,百度上很多教程,没有任何难度,大家自己百度吧,我主要是说一些其中的关键细节点,可以参考stable diffusion LORA模型训练最全最详细教程 - 知乎

我准备了40张半身正面照,20张侧身照,赛博单溜处理半身照的时候自动裁剪了脸部,所以一共有100张图片

closeup就是脸部特写,fromside就是侧身,portrait就是半身正面

文件夹前面的18代表了,这个文件夹里的图片,每一张都要训练18次(文件夹的名称格式里面必须是“数字_xxx”)

3、使用秋叶训练器开始训练

具体教程可以参考:SDXL模型lora训练参数详细设置,显存占用22G,不用修脸原图直出 - 知乎

炼制lora的过程本身是没有什么难度的,照着网上的教程走,完全没问题,如果出现质量问题,一定是训练素材的原因,还是那句话,图片质量+Tag标记,没有任何必成的技术,只能自己不断尝试,不断调整,多花时间,多花耐心.....最后补充一句,秋叶训练器里面训练sdxl时候如果训练精度选择fp16是会报错的,可能是显卡的原因(我的显卡:NVIDIA RTX 3090 24G),所以只能选择bf16,但是保存精度可以选择fp16

炼制lora都是以base模型为基础的,所以如果在代码中要使用lora最好还是选择单独使用base,结合refiner使用也行,只是最终效果可能会有些偏差

4、参数设置:

全部详细参数:

model_train_type = "sdxl-lora"
pretrained_model_name_or_path = "./sd-models/sd_xl_base_1.0.safetensors"
vae = "./sd-models/sdxl_vae.safetensors"
v2 = false
train_data_dir = "./train/girl"
prior_loss_weight = 1
resolution = "512,768"
enable_bucket = true
min_bucket_reso = 256
max_bucket_reso = 1_024
bucket_reso_steps = 32
output_name = "higirl_015"
output_dir = "./output/higirl_1115"
save_model_as = "safetensors"
save_precision = "fp16"
save_every_n_epochs = 1
max_train_epochs = 10
train_batch_size = 1
gradient_checkpointing = false
network_train_unet_only = false
network_train_text_encoder_only = false
learning_rate = 1
unet_lr = 1
text_encoder_lr = 1
lr_scheduler = "constant"
lr_warmup_steps = 0
optimizer_type = "Prodigy"
network_module = "networks.lora"
network_dim = 128
network_alpha = 128
network_dropout = 0
scale_weight_norms = 0
sample_prompts = "./toml/sample_prompts.txt"
sample_sampler = "ddim"
sample_every_n_epochs = 2
log_with = "tensorboard"
logging_dir = "./logs"
caption_extension = ".txt"
shuffle_caption = true
weighted_captions = false
keep_tokens = 0
max_token_length = 255
seed = 1_337
clip_skip = 2
mixed_precision = "bf16"
full_fp16 = false
full_bf16 = true
xformers = true
lowram = false
cache_latents = true
cache_latents_to_disk = true
cache_text_encoder_outputs = false
cache_text_encoder_outputs_to_disk = false
persistent_data_loader_workers = true
optimizer_args = ["decouple=True","weight_decay=0.01","use_bias_correction=True","d_coef=2.0"]

 在我的显卡上,训练时候暂用的显存是19个G,训练了3个小时,损失值在0.04左右

这是我的训练图片中的某一张图片对应的Tag,higirl是触发词,所以在第一个位置

在这个tag里面higirl(触发词,自己随便怎么定义都行),1girl,solo(表示单人出镜),parted lips(微微张着嘴唇)looking at viewer(正面面向镜头),slip dress(吊带裙)

解释一下触发词这个概念,在我们将Lora与原始模型结合在一起之后,想要让模型生成预想中的真人,我们通常会在提示词的前面加一个词,这个词就是触发词,这样就可以让模型知道要使用lora的参数,当然如果我在prompt里面加上slip dress,也是可以触发的,但是不是所有的训练照片都有slip dress,但是所有的训练照片都有higirl,所以使用higirl作为触发词是最好的选择

5、lora模型使用:

大部分人都是在webui上使用,我是在自己的代码里融合模型使用,参见代码:

定义lora模型列表

lora_list = [
    {
        "name": "higirl_v1", # lora模型名称
        "tag_words": "higirl", # 触发词,在代码中会将这个触发词,添加到prompt前面
        "scale": 0.9, # lora被挂载到模型中之后,参数融合百分比
        "sored": 1 # lora模型列表排序
    },
    {
        "name": "InkWashpainting",
        "tag_words": "InkWashpainting",
        "scale": 0.9,
        "sored": 2
    },
    {
        "name": "SDXLPaintSplash",
        "tag_words": "Colorsplash",
        "scale": 0.9,
        "sored": 3
    },]

 将lora模型列表传入到base模型,循环挂载

def fuse_lora(self, loras: list):
        if self.base_model is not None:
            if len(loras) > 0:
                for lora in loras:
                    self.base_model.load_lora_weights(
                        "D:\ChatGLM2-6B\stable-diffusion-xl-base-1.0\lora",
                        weight_name=f"{lora['name']}.safetensors",
                        adapter_name=lora['name'])
                self.base_model.fuse_lora(lora_scale=loras[-1]["scale"])

特别说明:可以使用peft对lora模型进行挂载,但是必须要求transformers的版本大于4.33.3,我目前用的4.30.2,因为ChatGLM要求使用4.30.2,如果使用4.33.3会导致ChatGLM报错,所以我没有使用peft

fuse_lora这个方法就是在融合lora模型的参数,将多个lora模型参数都融合到原始base模型中,这样就会出现叠加的化学反应哦...,我将我自己炼制的真人lora和从C站上下载的彩绘lora进行融合后得到的图片效果:

人是我的真人lora,彩绘是别人炼制的lora
人是我的真人lora,彩绘是别人炼制的lora

三、模块封装 

3.1、base类

import os
from typing import List
from diffusers import DiffusionPipeline, AutoencoderKL
import torch
from threading import RLock
from peft import PeftModel
import transformers

class SD_Base_Model:

    model_id: str = "D:\ChatGLM2-6B\stable-diffusion-xl-base-1.0"
    export: bool = True
    single_lock = RLock()
    is_cuda: bool = False  # 是否只在CUDA上运行推理
    n_steps: int = 50  # 迭代去噪步数
    high_noise_frac: float = 0.8  # 噪声优化层度
    guidance_scale: float = 9  # 数值越高越prompt相符合
    H: int = 1024  # 图片的高
    W: int = 1024  # 图片的宽

    def __init__(self, **kwargs) -> None:
        self.init_params(**kwargs)
        self.base_model = None
        self.vae = None

    def init_params(self, **kwargs):
        if "model_id" in kwargs:
            self.model_id = kwargs["model_id"]
        if "is_cuda" in kwargs:
            self.is_cuda = kwargs["is_cuda"]
        if "n_steps" in kwargs:
            self.n_steps = int(kwargs["n_steps"])
            if self.n_steps > 100 or self.n_steps < 30:
                self.n_steps = 40
        if "guidance_scale" in kwargs:
            self.guidance_scale = float(kwargs["guidance_scale"])
            if self.guidance_scale > 8:
                self.guidance_scale = 7.5
        if "high_noise_frac" in kwargs:
            self.high_noise_frac = float(kwargs["high_noise_frac"])
            if self.high_noise_frac > 0.9 or self.high_noise_frac < 0.5:
                self.high_noise_frac = 0.8
        if "H" in kwargs:
            self.H = int(kwargs["H"])
            if self.H > 1024 or self.H < 128:
                self.H = 512
        if "H" in kwargs:
            self.W = int(kwargs["W"])
            if self.W > 1024 or self.W < 128:
                self.W = 512

    # 使用基础模型生成图片 返回PIL图片
    def get_image_to_image_single_prompt(self,
                                         query: str,
                                         image_count: int = 1,
                                         negative_prompt: str = None):
        # seed = 1337
        # generator = torch.Generator("cuda").manual_seed(seed)
        image = self.base_model(query,
                                num_inference_steps=self.n_steps,
                                guidance_scale=self.guidance_scale,
                                num_images_per_prompt=image_count,
                                negative_prompt=negative_prompt,
                                height=self.H,
                                width=self.W).images
        return image

    # 通过提示词集合获取对应图片集合
    def get_base_images_multiple_prompts(self,
                                         prompts: List[str],
                                         image_count: int = 1,
                                         negative_prompt: str = None):
        if len(prompts) <= 0:
            raise ValueError("未能获取到对应提示词")
        negative_prompts: List[str] = []
        for item in prompts:
            negative_prompts.append(negative_prompt)
        images = self.base_model(prompt=prompts,
                                 num_inference_steps=self.n_steps,
                                 guidance_scale=self.guidance_scale,
                                 num_images_per_prompt=image_count,
                                 negative_prompt=negative_prompts,
                                 height=self.H,
                                 width=self.W).images
        return images

    # 在潜在空间生成图片
    def get_base_latent_image_single_prompt(self,
                                            query: str,
                                            image_count: int = 1,
                                            negative_prompt: str = None):
        images = self.base_model(query,
                                 num_inference_steps=self.n_steps,
                                 denoising_end=self.high_noise_frac,
                                 num_images_per_prompt=image_count,
                                 negative_prompt=negative_prompt,
                                 output_type="latent").images
        return images

    # 在潜在空间生成图片
    def get_base_latent_image_multiple_prompts(self,
                                               prompts: List[str],
                                               image_count: int = 1,
                                               negative_prompt: str = None):
        if len(prompts) <= 0:
            raise ValueError("未能获取到对应提示词")
        negative_prompts: List[str] = []
        for item in prompts:
            negative_prompts.append(negative_prompt)
        images = self.base_model(prompts,
                                 num_inference_steps=self.n_steps,
                                 denoising_end=self.high_noise_frac,
                                 num_images_per_prompt=image_count,
                                 negative_prompt=negative_prompts,
                                 output_type="latent").images
        return images

    def unload_model(self):
        if self.base_model is not None:
            self.base_model = None
        torch.cuda.empty_cache()

    def load_model(self):
        self.unload_model()
        if self.base_model is None:
            self.vae = AutoencoderKL.from_pretrained(
                f"{self.model_id}\vae_fix", torch_dtype=torch.float16)
            # 初始化模型
            self.base_model = DiffusionPipeline.from_pretrained(
                self.model_id,
                vae=self.vae,
                torch_dtype=torch.float16,
                use_safetensors=True,
                variant="fp16")
            # 在GPU上进行推理
            if self.is_cuda is True:
                self.base_model.to("cuda")
            # GPU+CPU联合推理
            else:
                self.base_model.enable_model_cpu_offload()

    def fuse_lora(self, loras: list):
        if self.base_model is not None:
            self.base_model.unfuse_lora()
            if len(loras) > 0:
                for lora in loras:
                    self.base_model.load_lora_weights(
                        "D:\ChatGLM2-6B\stable-diffusion-xl-base-1.0\lora",
                        weight_name=f"{lora['name']}.safetensors",
                        adapter_name=lora['name'])
                self.base_model.fuse_lora(lora_scale=loras[-1]["scale"])

    @classmethod
    def instance(cls, *args, **kwargs):
        if not hasattr(SD_Base_Model, "_instance"):
            with SD_Base_Model.single_lock:
                if not hasattr(SD_Base_Model, "_instance"):
                    SD_Base_Model._instance = cls(*args, **kwargs)
        else:
            SD_Base_Model._instance.init_params(**kwargs)
        return SD_Base_Model._instance

3.2、refiner类

from typing import List
from sdxl.sd_base_model import SD_Base_Model
from diffusers import DiffusionPipeline, StableDiffusionXLImg2ImgPipeline
from diffusers.utils import load_image
import torch
from threading import RLock
from diffusers.utils import load_image

class SD_Refiner_Model:

    model_id: str = "D:\ChatGLM2-6B\stable-diffusion-xl-refiner-1.0"
    export: bool = True
    single_lock = RLock()
    is_cuda: bool = False
    n_steps: int = 50
    high_noise_frac: float = 0.8
    guidance_scale: float = 9 
    is_combine_base: bool = True
    H: int = 1024  # 图片的高
    W: int = 1024  # 图片的宽

    def __init__(self, **kwargs) -> None:
        self.init_params(**kwargs)
        self.base_model: SD_Base_Model = None
        self.refiner_model = None

    def init_params(self, **kwargs):
        if "model_id" in kwargs:
            self.model_id = kwargs["model_id"]
        if "is_cuda" in kwargs:
            self.is_cuda = bool(kwargs["is_cuda"])
        if "n_steps" in kwargs:
            self.n_steps = int(kwargs["n_steps"])
            if self.n_steps > 100 or self.n_steps < 30:
                self.n_steps = 40
        if "guidance_scale" in kwargs:
            self.guidance_scale = float(kwargs["guidance_scale"])
            if self.guidance_scale > 8:
                self.guidance_scale = 7.5
        if "is_combine_base" in kwargs:
            self.is_combine_base = bool(kwargs["is_combine_base"])
        if "H" in kwargs:
            self.H = int(kwargs["H"])
            if self.H > 1024 or self.H < 128:
                self.H = 1024
        if "H" in kwargs:
            self.W = int(kwargs["W"])
            if self.W > 1024 or self.W < 128:
                self.W = 1024

    # 通过基础图片+单条提示词得到另一些精炼加工图片
    def get_image_to_image_single_prompt(self,
                                         query: str,
                                         image_url: str = None,
                                         image_count: int = 1,
                                         negative_prompt: str = None):
        # 获取潜在空间的图片通过单条提示词
        def _get_base_latent_images_single_prompt(query: str,
                                                  image_count: int = 1,
                                                  negative_prompt: str = None):
            if self.base_model is not None:
                images = self.base_model.get_base_latent_image_single_prompt(
                    query=query,
                    image_count=image_count,
                    negative_prompt=negative_prompt)
                return images
            else:
                return None

        target_size: tuple[int, int] = (self.H, self.W)
        if image_url is None and self.is_combine_base is True:
            init_images = _get_base_latent_images_single_prompt(
                query, image_count, negative_prompt)
            images = self.refiner_model(prompt=query,
                                        num_inference_steps=self.n_steps,
                                        denoising_start=self.high_noise_frac,
                                        num_images_per_prompt=image_count,
                                        negative_prompt=negative_prompt,
                                        image=init_images,
                                        target_size=target_size).images
        else:

            init_image = load_image(image_url).convert("RGB")
            images = self.refiner_model(prompt=query,
                                        image=init_image,
                                        num_inference_steps=self.n_steps,
                                        guidance_scale=self.guidance_scale,
                                        negative_prompt=negative_prompt,
                                        num_images_per_prompt=image_count,
                                        target_size=target_size).images
        return images

    # 通过基础图片+多条提示词得到另一些精炼加工图片
    def get_image_to_image_multiple_prompts(self,
                                            prompts: List[str],
                                            image_count: int = 1,
                                            negative_prompt: str = None):

        target_size: tuple[int, int] = (self.H, self.W)

        # 获取潜在空间的图片通过多条提示词
        def _get_base_latent_image_multiple_prompts(
                prompts: List[str],
                image_count: int = 1,
                negative_prompt: str = None):
            if self.base_model is not None:
                images = self.base_model.get_base_latent_image_multiple_prompts(
                    prompts=prompts,
                    image_count=image_count,
                    negative_prompt=negative_prompt)
                return images
            else:
                return None

        if self.is_combine_base is True:
            negative_prompts: List[str] = []
            for item in prompts:
                negative_prompts.append(negative_prompt)
            init_images = _get_base_latent_image_multiple_prompts(
                prompts=prompts,
                image_count=image_count,
                negative_prompt=negative_prompt)

            images = self.refiner_model(
                prompt=prompts,
                num_inference_steps=self.n_steps,
                denoising_start=self.high_noise_frac,
                num_images_per_prompt=image_count,
                image=init_images,
                target_size=target_size,
                negative_prompt=negative_prompts).images
        else:
            raise ValueError("REFINER模型并未定义成需要和BASE模型一起使用")
        return images

    def unload_model(self):
        if self.refiner_model is not None:
            self.refiner_model = None
        if self.base_model is not None:
            if self.base_model.base_model is not None:
                self.base_model.base_model = None
        torch.cuda.empty_cache()

    def load_model(self):
        self.unload_model()
        if self.is_combine_base is True:
            # 处理baseModel
            if self.base_model is None:
                self.base_model = SD_Base_Model.instance(
                    n_steps=self.n_steps,
                    high_noise_frac=self.high_noise_frac,
                    is_cuda=self.is_cuda,
                    H=self.H / 2,
                    W=self.W / 2)
            if self.base_model.base_model is None:
                self.base_model.load_model()
            # 处理refinerModel
            if self.refiner_model is None:
                self.refiner_model = DiffusionPipeline.from_pretrained(
                    self.model_id,
                    torch_dtype=torch.float16,
                    variant="fp16",
                    use_safetensors=True,
                    text_encoder_2=self.base_model.base_model.text_encoder_2,
                    vae=self.base_model.base_model.vae)
        else:
            self.base_model = None
            if self.refiner_model is None:
                self.refiner_model = StableDiffusionXLImg2ImgPipeline.from_pretrained(
                    self.model_id,
                    torch_dtype=torch.float16,
                    variant="fp16",
                    use_safetensors=True)
        if self.is_cuda is True:
            self.refiner_model.to("cuda")
        else:
            self.refiner_model.enable_model_cpu_offload()

    @classmethod
    def instance(cls, *args, **kwargs):
        if not hasattr(SD_Refiner_Model, "_instance"):
            with SD_Refiner_Model.single_lock:
                if not hasattr(SD_Refiner_Model, "_instance"):
                    SD_Refiner_Model._instance = cls(*args, **kwargs)
        else:
            SD_Refiner_Model._instance.init_params(**kwargs)
        return SD_Refiner_Model._instance

3.3、调用类

# 将中文提示词翻译成英文
def get_image_flags(self, content: str, style: dict, loras: list):
        images = re.findall('【(.*?)】', content)
        text_to_image_list = []
        if images and len(images) > 0:
            for i in range(0, len(images)):
                text_to_image_list.append(images[i])
        else:
            content = self.generate_image_sentence(content)
            text_to_image_list = content.split("~")

        # 翻译成英文
        texts_result: List[str] = []
        # texts_result.extend(text_to_image_list)
        for item in text_to_image_list[:self.max_image_count]:
            time.sleep(3)
            texts_result.append(Translation_Baidu.excute_translation(item))
        return {
            "texts_or_images": texts_result,
            "style": style,
            "loras": loras
        }# 为图片命名def _name_image(self, sentence: str):
        words = sentence.split()
        initials = [word[0] for word in words]
        return "".join(initials)
# 生成图片
def generate_images(self, texts_or_images: list, style: dict, loras: list):
        # 加载了Lora,只使用basemodel,效果最好
        if len(loras) > 0:
            sd_model = SD_Base_Model.instance()
            sd_model.load_model()
            sd_model.fuse_lora(loras=loras)
        # 不加载lora 使用refiner+base,效果最好
        else:
            sd_model = SD_Refiner_Model.instance(is_combine_base=True)
            sd_model.load_model()
        image_addr = []
        for item in texts_or_images:
            try:
                name = self._name_image(item)
                prefix = ", ".join([(i["tag_words"]) for i in loras
                                    ]) + ", " if len(loras) > 0 else ""
                prompt = prefix + style["prompt"].format(prompt=item).lower()
                negative_prompt = style["negative_prompt"]
                target_image = sd_model.get_image_to_image_single_prompt(
                    query=prompt,
                    image_count=self.num_per_prompt_image,
                    negative_prompt=negative_prompt)
                for i in range(len(target_image)):
                    target_image[i].save(
                        f"{self.base_file_path}\{name}_{i}.jpg", "JPEG")
                    image_addr.append(f"{self.base_file_path}\{name}_{i}.jpg")
            except Exception:
                pass
         return image_addr

今天就先到这里,下期给大家看看整个AIGC系统的全貌,以及产出成果的样子最近我也会将代码再整理一下,然后开源出来。