从0到1实现一个自己的大模型,实践中了解模型流程细节

news2024/11/25 20:33:36

前言

最近看了很多大模型,也使用了很多大模型。对于大模型理论似乎很了解,但是好像又缺点什么,思来想去决定自己动手实现一个 toy 级别的模型,在实践中加深对大语言模型的理解。

在这个系列的文章中,我将通过亲手实践,构建一个 1.2B 的模型,完成模型搭建、tokenizer 训练、模型预训练和指令微调这些流程。记录整个开发过程和其中遇到的各种挑战和对应解决方案。

最后里面所有的内容都是我对于大模型的理解形成的,如果您发现有任何过时或不准确的地方,请不吝指出。

训练组件

在使用 Pytorch 训练模型的时候,一个常见的流程就是前向传播、反向传播然后更新梯度,因此我们一步一步完成其中的组件。

优化器

现在训练大模型常用的优化器是 AdamW,它使用一阶动量和二阶动量保持梯度稳定,从而使损失不会过于震荡。这里不详细介绍原理,给出 Pytorch 中的实现。

from torch.optim import AdamW

optimizer = AdamW(
    params=model.parameters(),
    lr=args.lr,
    weight_decay=args.weight_decay,
)

这里我们设置了学习率和权重衰减,详细参数请见官方文档,这里不过多赘述。

调度器

学习率调度器可以在训练中动态调度学习率,从而提高训练效率和模型性能。合适的学习率有助于帮助模型脱离局部最优,达到一个更好的最优解。

这里我们采用 warmup 结合余弦退火的学习率调度策略。warmup 学习率预热开始从一个小学习率开始训练,然后再修正为指定的学习率。因为刚刚开始训练时模型权重随机初始化的,此时选择一个较大的学习率可能导致模型训练震荡。

余弦退火就是采用余弦方式对学习率进行衰减,这里我们给出调度器的实现:

from torch.optim.lr_scheduler import CosineAnnealingLR, LambdaLR, SequentialLR

def get_lr_warmup(warmup_steps: int):

    def lr_warmup(current_step: int):
        return float(current_step) / float(max(1, warmup_steps))

    return lr_warmup

warmup_steps = xxx
cosine_steps = xxx
warmup_scheduler = LambdaLR(
    optimizer=optimizer, lr_lambda=get_lr_warmup(warmup_steps=warmup_steps)
)
cosine_scheduler = CosineAnnealingLR(optimizer=optimizer, T_max=cosine_steps)
scheduler = SequentialLR(
    optimizer=optimizer,
    schedulers=[warmup_scheduler, cosine_scheduler],
    milestones=[warmup_steps],
)

warmup 阶段,我们采用线性递增的方式慢慢增大学习率,由于 Pytorch 没有相关实现,因此我们需要自己定义学习率调度的函数。

余弦退火阶段中 T_max 指定一个波峰到波谷的周期,也就是退火阶段迭代次数。最后将两个调度器组合起来,组成我们希望的调度器。

数据集

首先加载数据集

from datasets import load_dataset, Dataset

dataset: Dataset = load_dataset(
    "json",
    data_files=[
        "nlp_datas/part-000020-a894b46e.jsonl.tar.gz",
        "nlp_datas/part-000065-a894b46e.jsonl.tar.gz",
    ],
    split="train",
)

当然如果需要加载更大的数据,可以指定参数 streaming=True 减少加载的内存占用。加载之后就需要对数据集进行分词,得到模型需要的输入 input_idsattention_mask

from tokenization_custom import CustomTokenizer

tokenizer = CustomTokenizer.from_pretrained("tokenizer")
tokenized_dataset = dataset.map(
    lambda x: tokenizer(x["content"], truncation=True, max_length=2048),
    batched=True,
    remove_columns=dataset.column_names,
)

注意在这里我们只对文本进行了截断,但是没有对文本做填充,这样得到的文本可能是长短不一的。我们不需要对整体进行填充,这样会按照整体最大长度填充,占用大量存储,我们只需要对当时送入模型的一批数据进行填充即可。

因为我们采用的是预测下一个 token,因此 labelsinput_ids 相同,同时我们不希望 padding_token 参与计算损失,因为这是无意义的,所以对于 padding_token 的位置,对应的 label 是 -100,这里采用 DataCollatorForLanguageModeling 可以方便的完成这项操作。

这些都准备好,就可以得到 data_loader ,通过遍历 data_loader 就可以方便进行模型训练了。

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
data_loader = DataLoader(
    dataset=dataset,
    batch_size=args.batch_size,
    shuffle=True,
    collate_fn=data_collator,
    num_workers=8,
)

训练流程

有了上面的各种组件,就可以进行训练了,首先确定模型结构,这里采用一个三层的结构。

from config import CustomConfig
from modeling_custom import CustomForCausalLM

config = CustomConfig(
    vocab_size=len(tokenizer.get_vocab()),
    max_position_embeddings=2048,
    hidden_size=4096,
    intermediate_size=16384,
    num_hidden_layers=3,
    pad_token_id=tokenizer.pad_token_id,
)
model = CustomForCausalLM(config)

这里简单实现一个函数来计算模型参数量

def get_model_size(model: nn.Module):
    """
    获取模型参数量
    """
    return sum(p.numel() for p in model.parameters())

模型准备好后就可以进行模型训练,下面是一个简单的训练流程:

for epoch in range(args.epochs):
    for idx, batch in enumerate(data_loader):
        batch = {k: v.to(device) for k, v in batch.items()}
        outputs = model(**batch)
        logits, loss = outputs

        # 反向传播
        loss.backward()

        # 梯度裁剪
        torch.nn.utils.clip_grad_norm_(
            model.parameters(), max_norm=args.max_norm
        )

        # 梯度更新
        optimizer.step()
        # 学习率更新
        scheduler.step()
        # 清除梯度
        optimizer.zero_grad()

对于一个 1.2B 模型,模型权重优化器状态梯度三部分大约占用显存计算如下:

1.2×109×4(FP32)10243×4≈17.88GB\frac{1.2 \times 10^9 \times 4(FP32)}{1024^3} \times 4 \approx 17.88GB102431.2×109×4(FP32)​×4≈17.88GB

这里简单计算一下中间激活值占用显存,假设 batchsize 为16,这一批 padding 之后的长度为512,因此 input_ids 的大小为 (16,512)。

  • 经过 embedding 层之后维度变为 (16,512,4096)
  • 经过注意力层会投影到 Q K V,三个维度均为 (16,512,4096)
  • 经过输出投影,维度为 (16,512,4096)
  • 经过前馈网络中上投影和门控,得到两个维度 (16,512,16384)
  • 经过下投影得到维度 (16,512,4096)
  • 经过词汇表大小投影(这里大约57000的大小)得到维度 (16,512,57000)

embedding 层的结果大约 32M,一层 Attention 层结果大约 128M,一层前馈网络结果大约 288M,最后词汇表投影大约 445M。这个模型中使用 3 层解码器层,不考虑层归一化的中间结果,这个模型总共中间结果大约有 1725M结果,每个结果占用 4Bytes,则最后总共显存占用大约 6.7GB。

这是长度为 512 的情况,实际上我的训练文本中大量存在 2k 左右文本,它会使占用显存成倍数增加,假设一个 2k 的文本,则显存占用会扩展到 26.8GB。

上面最理想的情况,实际计算中还会产生各种变量占用显存,很快就会导致显存溢出而从无法训练。幸运的是在实现模型结构时加入了梯度检查点,只需要保存关键节点的中间结果,反向传播时重新从最近节点开始计算即可,这样大大节省了显存。

1717660331127.png

在这个模型中只需要调用 model.enable_gradient_checkpoint() 即可开启梯度检查点。

除了梯度检查点,还可以通过减少 batchsize 来减少中间激活值占用显存,但是减少批量大小可能导致损失震荡无法收敛,这里我们采用多步累加解决这个问题,在一个小批次反向传播计算梯度之后,先不更新权重和清除梯度,而是累计多个小批次之后一起更新然后清除梯度。

最后还可以采用混合精度训练,这样不仅能加快训练速度还能显著减少中间激活值空间占用。

有了以上策略,可以尝试愉快训练模型了,训练前为了方便修改配置,我们进行一些封装,同时添加一些日志信息,方便最后观测整个训练过程,这里直接给出最后的代码。

import json
import os
import random
from dataclasses import dataclass
from typing import Optional, Union

import numpy as np
import torch
import torch.nn as nn
from datasets import Dataset
from torch.cuda.amp import GradScaler, autocast
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR, LambdaLR, SequentialLR
from torch.utils.data import DataLoader
from tqdm import tqdm
from transformers import DataCollatorForLanguageModeling

from config import CustomConfig
from modeling_custom import CustomForCausalLM
from tokenization_custom import CustomTokenizer
from utils import get_model_size

SEED = 42

def set_seed(seed: int):
    torch.manual_seed(seed=seed)
    torch.cuda.manual_seed(seed=seed)
    torch.cuda.manual_seed_all(seed=seed)
    np.random.seed(seed=seed)
    random.seed(seed)

def get_lr_warmup(warmup_steps: int):

    def lr_warmup(current_step: int):
        return float(current_step) / float(max(1, warmup_steps))

    return lr_warmup

@dataclass
class TrainingArgs:
    output_dir: str
    logging_steps: int = 500
    saving_steps: int = 500
    batch_size: int = 1
    epochs: int = 3
    lr: float = 1e-4
    weight_decay: float = 1e-4
    max_norm: float = 1.0
    warm_up_ratio: float = 0.1
    gradient_checkpointing: bool = False
    gradient_accumulation_steps: int = 24

def train(
    model: nn.Module,
    args: TrainingArgs,
    dataset: Dataset,
    device: Optional[Union[str, torch.device]] = None,
    data_collator=None,
):
    data_loader = DataLoader(
        dataset=dataset,
        batch_size=args.batch_size,
        shuffle=True,
        collate_fn=data_collator,
        num_workers=8,
    )
    # 完整的有效步
    complete_steps_per_epoch = len(data_loader) // args.gradient_accumulation_steps
    # 不完整的有效步,最后剩余的小批量
    last_mini_steps = len(data_loader) % args.gradient_accumulation_steps
    # 一个 epoch 等效步
    if last_mini_steps != 0:
        steps_per_epoch = complete_steps_per_epoch + 1
    else:
        steps_per_epoch = complete_steps_per_epoch

    total_steps = steps_per_epoch * args.epochs

    # 优化器
    optimizer = AdamW(
        params=model.parameters(),
        lr=args.lr,
        weight_decay=args.weight_decay,
    )

    # 学习率调度
    warmup_steps = int(total_steps * args.warm_up_ratio)
    cosine_steps = total_steps - warmup_steps
    warmup_scheduler = LambdaLR(
        optimizer=optimizer, lr_lambda=get_lr_warmup(warmup_steps=warmup_steps)
    )
    cosine_scheduler = CosineAnnealingLR(optimizer=optimizer, T_max=cosine_steps)
    scheduler = SequentialLR(
        optimizer=optimizer,
        schedulers=[warmup_scheduler, cosine_scheduler],
        milestones=[warmup_steps],
    )

    # 设备
    if device is None:
        device = "cuda" if torch.cuda.is_available() else "cpu"

    os.makedirs(args.output_dir, exist_ok=True)

    model = model.to(device=device)
    if args.gradient_checkpointing:
        model.enable_gradient_checkpoint()
    loggin_info = []
    current_step = 0

    progress_bar = tqdm(range(total_steps))
    scaler = GradScaler()
    for epoch in range(args.epochs):
        current_loss = 0.0
        for idx, batch in enumerate(data_loader):
            batch = {k: v.to(device) for k, v in batch.items()}
            if last_mini_steps == 0 or len(data_loader) - (idx + 1) > last_mini_steps:
                current_accumulation = args.gradient_accumulation_steps
            else:
                current_accumulation = last_mini_steps

            with autocast(dtype=torch.bfloat16):
                outputs = model(**batch)
                logits, loss = outputs
                loss /= current_accumulation
            current_loss += loss.item()
            # 反向传播
            scaler.scale(loss).backward()

            if (idx + 1) % args.gradient_accumulation_steps == 0 or (idx + 1) == len(
                data_loader
            ):
                # 梯度裁剪
                scaler.unscale_(optimizer=optimizer)
                torch.nn.utils.clip_grad_norm_(
                    model.parameters(), max_norm=args.max_norm
                )

                # 梯度更新
                scaler.step(optimizer=optimizer)

                # 更新缩放因子
                scaler.update()

                # 学习率更新
                scheduler.step()

                # 清除梯度
                optimizer.zero_grad()

                progress_bar.update(1)

                current_step += 1
                if current_step % args.logging_steps == 0:
                    current_epochs = current_step / steps_per_epoch
                    info = {
                        "Epoch": f"{current_epochs:.2f}/{args.epochs}",
                        "Step": f"{current_step}/{total_steps}",
                        "Loss": current_loss,
                        "LR": scheduler.get_last_lr()[0],
                    }
                    loggin_info.append(info)
                    print(info)

                if current_step % args.saving_steps == 0:
                    ckpt_path = os.path.join(
                        args.output_dir,
                        f"checkpoint-{current_step}.pt",
                    )
                    torch.save(model.state_dict(), ckpt_path)

                current_loss = 0.0

    ckpt_path = os.path.join(
        args.output_dir,
        "last.pt",
    )
    torch.save(model.state_dict(), ckpt_path)
    with open("logging.jsonl", "w", encoding="utf-8") as fw:
        for logging_data in loggin_info:
            fw.write(json.dumps(logging_data) + "\n")

if __name__ == "__main__":
    set_seed(SEED)

    tokenizer = CustomTokenizer.from_pretrained("tokenizer")
    config = CustomConfig(
        vocab_size=len(tokenizer.get_vocab()),
        max_position_embeddings=2048,
        hidden_size=4096,
        intermediate_size=16384,
        num_hidden_layers=3,
        pad_token_id=tokenizer.pad_token_id,
    )
    model = CustomForCausalLM(config)
    print(f"Model size is {get_model_size(model)}")

    dataset = Dataset.load_from_disk("nlp_datas/cached")

    data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
    args = TrainingArgs(
        output_dir="result",
        gradient_checkpointing=True,
        batch_size=4,
        logging_steps=50,
        warm_up_ratio=0.03,
        epochs=1,
        gradient_accumulation_steps=8,
        lr=1e-3,
        weight_decay=1e-5,
    )

    train(model=model, args=args, dataset=dataset, data_collator=data_collator)

结语

至此我们成功完成了模型训练,为其注入了先验知识。现在它拥有各种工具,但是无法进行使用,后面我们进行 sft 教模型如何使用这些工具。

那么,我们该如何学习大模型?

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

一、大模型全套的学习路线

学习大型人工智能模型,如GPT-3、BERT或任何其他先进的神经网络模型,需要系统的方法和持续的努力。既然要系统的学习大模型,那么学习路线是必不可少的,下面的这份路线能帮助你快速梳理知识,形成自己的体系。

L1级别:AI大模型时代的华丽登场

L2级别:AI大模型API应用开发工程

L3级别:大模型应用架构进阶实践

L4级别:大模型微调与私有化部署

一般掌握到第四个级别,市场上大多数岗位都是可以胜任,但要还不是天花板,天花板级别要求更加严格,对于算法和实战是非常苛刻的。建议普通人掌握到L4级别即可。

以上的AI大模型学习路线,不知道为什么发出来就有点糊,高清版可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

四、AI大模型商业化落地方案

img

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1797281.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【OPENMV】学习记录 (持续更新)

一、基础知识 1 设置彩色/黑白: sensor.set_pixformat() 设置像素模式。 sensor.GRAYSCALE: 灰度,每个像素8bit。sensor.RGB565: 彩色,每个像素16bit。 2 设置图像大小: sensor.set_framesize() 设置图像的大小 sen…

day51 动态规划 121. 买卖股票的最佳时机 122.买卖股票的最佳时机II

121. 买卖股票的最佳时机 动态规划 1.确定dp数组(dp table)以及下标的含义 dp[i][0] 表示第i天持有股票所得最多现金 ,这里可能有同学疑惑,本题中只能买卖一次,持有股票之后哪还有现金呢? 其实一开始现…

解密智慧校园解决方案:赋能数字化教育的未来

在当今数字化时代,智慧校园解决方案正以惊人的速度改变着教育界的面貌。随着科技的快速发展,数字化教育已经逐渐成为现代教育的核心。智慧校园解决方案作为一个集技术、教育和创新于一体的综合性项目,为学校提供了许多机遇和挑战。本文将揭示…

期望18K,4年前端Cvte 视源股份一面挂

一面 1、自我介绍?毕业的时候一直在 xx 公司,你基本都在做什么项目? 2、你讲一下你主要负责哪一块的?balabala 3、你们的 json 是怎么定义组件间的联动的? 4、怎么确定区分两个 input? 5、你们是怎么触…

Vue3入门 - vue3相比于vue2的优点,及如何创建Vue3项目

目录 一、认识Vue3 1. Vue2 选项式 API vs Vue3 组合式API 2. Vue3的优势 二、使用create-vue搭建Vue3项目 1. 认识create-vue 2. 使用create-vue创建项目 3.熟悉项目和关键文件 一、认识Vue3 1. Vue2 选项式 API vs Vue3 组合式API <script>export default …

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《考虑发用电相似性的海上风电中长期双边协商交易优化决策模型》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

【CV算法工程师必看】作为一个图像算法工程师,需要会什么,要学哪些技术栈?

作为一个图像算法工程师,除了基本的编程技能和理论知识,还需要掌握一系列的技术栈。以下是详细的技能和技术栈分类: 编程语言 Python: 主要用于快速开发和原型设计。常用库:OpenCV、Pillow、NumPy、SciPy、Scikit-image、TensorFlow、PyTorch。C++: 高性能要求的项目中广…

使用Ollama+OpenWebUI部署和使用Phi-3微软AI大模型完整指南

&#x1f3e1;作者主页&#xff1a; 点击&#xff01; &#x1f916;AI大模型部署与应用专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月6日23点50分 &#x1f004;️文章质量&#xff1a;96分 欢迎来到Phi-3模型的奇妙世界&#xff01;Phi-3是由微软…

零售数据分析围绕人、货、场分析了什么?

零售数据分析围绕人、货、场分析了什么&#xff1f;通过全面分析零售业务中的人、货、场三个核心要素&#xff0c;为零售商提供深入的市场洞察和业务优化建议。通过对客流量、商品销售、场地布局等数据的收集和数据可视化分析&#xff0c;帮助企业更好地了解消费者行为、商品销…

一起学大模型 - 一起动笔练习prompt的用法

文章目录 前言一、代码演示二、代码解析1. 导入所需的库和模块&#xff1a;2. 设置日志记录和初始化模型&#xff1a;3. 定义一个函数用于清理GPU内存&#xff1a;4. 定义一个继承自LLM基类的QianWenChatLLM类&#xff0c;并实现对话生成的逻辑&#xff1a;5. 示例代码的主体部…

【Linux】ip命令详解

Linux网络排查 目录 一、ip命令介绍 1.1 ip命令简介 1.2 ip命令的由来 二、ip命令使用帮助 2.1 ip命令的help帮助信息 2.2 ip命令对象介绍 2.3 ip命令选项介绍 三、查看网络信息 3.1 显示当前网络接口信息 3.2 显示网络设备运行状态 3.3 显示详细设备信息 3.4 查看…

【多模态】37、TextSquare | 借助 Gemini-Pro 通过四个步骤来生成高质量的文本问答数据

文章目录 一、背景二、方法2.1 Square-10M2.2 模型结构2.3 使用 Square-10M 进行有监督微调 三、效果3.1 实验设置3.2 Benchmark 测评 论文&#xff1a;TextSquare: Scaling up Text-Centric Visual Instruction Tuning 代码&#xff1a;暂无 出处&#xff1a;字节 | 华中科技…

大学生必备搜题神器app?分享3个软件和公众号,来对比看看吧 #媒体#其他#笔记

以下分享的软件提供了各种实用的功能&#xff0c;如数学公式计算、语文阅读辅助等&#xff0c;让大学生们在学习过程中更加高效和便利。 1.同声传译王 譬如我们在搜题或是浏览一些资料文档到时候&#xff0c;经常会访问到一些外文网页或文档&#xff0c;所以一款趁手的翻译工…

2024年四川省国家大学科技园申报条件对象和支持政策

2024年四川省国家大学科技园申报对象 国家大学科技园是以具有较强科研实力的大学为依托&#xff0c;将大学的综合智力资源优势与其它社会优势资源相结合&#xff0c;为高等学校&#xff08;以下简称为高校&#xff09;科技成果转化、高新技术企业孵化、创新创业人才培养、产学…

白酒:茅台镇白酒的丰富历史与文化传承

茅台镇&#xff0c;位于中国贵州省仁怀市&#xff0c;是中国著名的白酒产区&#xff0c;以其得天独厚的自然环境和与众不同的酿造工艺而闻名于世。作为茅台镇的白酒品牌之一&#xff0c;云仓酒庄豪迈白酒承载着丰富的历史与文化传承。 茅台镇的历史可以追溯到汉代&#xff0c;当…

@vue-office/excel 解决移动端预览excel文件触发软键盘

先直接上代码 不耽误大家时间 标明下插件库 非常感谢作者提供预览插件 vue-office/excel 只需要控制CSS :deep(.x-spreadsheet-overlayer) {.x-spreadsheet-selectors {display: none !important;} } :deep(.x-spreadsheet-bottombar) {li.active {user-select: none !import…

visual studio打包qt算子时,只生成dll没有生成lib等文件

问题&#xff1a;在visual studio配置了qt项目&#xff0c;并打包成dll&#xff0c;原则上会生成一堆文件&#xff0c;包括dll,lib等文件。 解决办法&#xff1a; 挨个右击源代码的所有头文件-》属性-》项类型。改成qt头文件形式&#xff0c;如下。

4秒惊艳!Stable Cascade AI绘画神器,设计师和普通用户的无限创意新选择

近日&#xff0c;一款AI绘画模型Stable Cascade发布。 只需输入一段描述文字&#xff0c;即可在4秒钟内获得令人惊艳的图像。 无论你是设计师、艺术家&#xff0c;还是普通用户&#xff0c;都能轻松上手&#xff0c;释放无限创意。 Stable Cascade不仅在使用上极具便捷性&am…

tomcat-valve通过servlet处理请求

上一节说到请求url定位servlet的过程&#xff0c;tomcat会把请求url和容器的映射关系保存到MappingData中&#xff0c;org.apache.catalina.connector.Request类实现了HttpServletRequest&#xff0c;其中定义了属性mappingDataprotected final MappingData mappingData new M…

机器学习笔记 - stable diffusion web-ui安装教程

一、Stable Diffusion WEB UI 屌丝劲发作了,所以本地调试了Stable Diffusion之后,就去看了一下Stable Diffusion WEB UI,网络上各种打包套件什么的好像很火。国内的也就这个层次了,老外搞创新,国内跟着屁股后面搞搞应用层,就叫大神了。 不扯闲篇了,我们这里从git源码直接…