中文模型的奋起直追:MOSS、baichuan-7B和ChatGLM2-6B的原理、部署与微调

news2024/11/14 2:46:23

第一部分 复旦MOSS

MOSS是复旦大学邱锡鹏团队推出的一个支持中英双语和多种插件的开源对话语言模型,moss-moon系列模型具有160亿参数,在FP16精度下可在单张A100/A800或两张3090显卡运行,在INT4/8精度下可在单张3090显卡运行

其基座语言模型在约七千亿中英文以及代码单词上预训练得到,后续经过对话指令微调、插件增强学习和人类偏好训练具备多轮对话能力及使用多种插件的能力

5.1 已开源的模型/数据

5.1.1 已开源的模型

  • moss-moon-003-base: MOSS-003基座模型,在高质量中英文语料上自监督预训练得到,预训练语料包含约700B单词,计算量约6.67x1022次浮点数运算。
  • moss-moon-003-sft: 基座模型在约110万多轮对话数据上微调得到,具有指令遵循能力、多轮对话能力、规避有害请求能力。
  • moss-moon-003-sft-plugin: 基座模型在约110万多轮对话数据和约30万插件增强的多轮对话数据上微调得到,在moss-moon-003-sft基础上还具备使用搜索引擎、文生图、计算器、解方程等四种插件的能力。
  • moss-moon-003-sft-int4: 4bit量化版本的moss-moon-003-sft模型,约占用12GB显存即可进行推理。
  • moss-moon-003-sft-int8: 8bit量化版本的moss-moon-003-sft模型,约占用24GB显存即可进行推理。
  • moss-moon-003-sft-plugin-int4: 4bit量化版本的moss-moon-003-sft-plugin模型,约占用12GB显存即可进行推理。
  • moss-moon-003-sft-plugin-int8: 8bit量化版本的moss-moon-003-sft-plugin模型,约占用24GB显存即可进行推理。
  • moss-moon-003-pm: 在基于moss-moon-003-sft收集到的偏好反馈数据上训练得到的偏好模型,将在近期开源。
  • moss-moon-003: 在moss-moon-003-sft基础上经过偏好模型moss-moon-003-pm训练得到的最终模型,具备更好的事实性和安全性以及更稳定的回复质量,将在近期开源。
  • moss-moon-003-plugin: 在moss-moon-003-sft-plugin基础上经过偏好模型moss-moon-003-pm训练得到的最终模型,具备更强的意图理解能力和插件使用能力,将在近期开源。

5.1.2 已开源的数据

  • moss-002-sft-data: MOSS-002所使用的多轮对话数据,覆盖有用性、忠实性、无害性三个层面,包含由text-davinci-003生成的约57万条英文对话和59万条中文对话
  • moss-003-sft-data: moss-moon-003-sft所使用的多轮对话数据,基于MOSS-002内测阶段采集的约10万用户输入数据和gpt-3.5-turbo构造而成,相比moss-002-sft-datamoss-003-sft-data更加符合真实用户意图分布,包含更细粒度的有用性类别标记、更广泛的无害性数据和更长对话轮数,约含110万条对话数据。目前仅开源少量示例数据,完整数据将在近期开源
  • moss-003-sft-plugin-data: moss-moon-003-sft-plugin所使用的插件增强的多轮对话数据,包含支持搜索引擎、文生图、计算器、解方程等四个插件在内的约30万条多轮对话数据。目前仅开源少量示例数据,完整数据将在近期开源
  • moss-003-pm-datamoss-moon-003-pm所使用的偏好数据,包含在约18万额外对话上下文数据及使用moss-moon-003-sft所产生的回复数据上构造得到的偏好对比数据,将在近期开源

5.2 MOSS模型量化版部署过程

我司七月杜助教写了一篇部署MOSS的教程,详情请点击:MOSS模型量化版部署过程

  • 项目背景
  • 配置环境与准备
  • 部署推理
    • 命令行部署
      • 报错1
      • 报错2:
  • 使用免费试用的阿里云GPU部署
  • 在AutoDL平台上部署

第二部分 baichuan-7B:与LLaMA的结构相同且表现优秀可商用

2.1 基于Transformer/RoPE/RMSNorm/SwiGLU + 1.2万亿训练数据/上下文窗口4096

baichuan-7B 是由百川智能(CEO为原搜狗创始人王小川)开发的一个开源可商用的大规模预训练语言模型

  • 基于 Transformer 结构,采用了和 LLaMA 一样的模型设计,比如
    位置编码:用的现阶段被大多模型采用的 rotary-embedding 方案,具有更好的外延效果
    激活层:SwiGLU, Feedforward 变化为 8/3 倍的隐含层大小,即 11,008
    Layer-Normalization: 基于 RMSNorm 的 Pre-Normalization

    关于LLaMA结构的解读,请参见:类ChatGPT模型LLaMA的解读与其微调:Alpaca-LoRA/Vicuna/BELLE
  • 在大约 1.2 万亿 tokens 上训练的 70 亿参数模型,支持中英双语
  • 上下文窗口长度为 4096
  • 在标准的中文和英文权威 benchmark(C-EVAL/MMLU)上均取得同尺寸最好的效果
    具体而言,C-Eval 数据集是一个全面的中文基础模型评测数据集,涵盖了 52 个学科和四个难度的级别
    我们使用该数据集的 dev 集作为 few-shot 的来源,在 test 集上进行了 5-shot 测试
    除了中文之外,作者团队也测试了模型在英文上的效果,MMLU 是包含 57 个多选任务的英文评测数据集,涵盖了初等数学、美国历史、计算机科学、法律等,难度覆盖高中水平到专家水平,是目前主流的LLM评测数据集


    一句话总结,即是在C-EVAL/MMLU等数据集上的表现好于ChatGLM-6B (当然,ChatGLM2-6B又变更强了)

2.2 baichuan-7B相比LLaMA-7B的优势

虽然baichuan-7B采用了和LLaMA一样的模型设计,但他们在原本的 LLaMA 框架上进行诸多修改

比如为提升模型的效果以及解码效率,做了

  • 分词改进
    词表大小为64K ,而LLaMA词表大小为32K

    具体而言,参考学术界方案使用 SentencePiece 中的 Byte-Pair Encoding (BPE) 作为分词算法,并且进行了以下的优化:
    目前大部分开源模型主要基于英文优化,因此对中文语料存在效率较低的问题,使用 2000 万条以中英为主的多语言语料训练分词模型,显著提升对于中文的压缩率

    对于数学领域,我们参考了 LLaMA 和 Galactica 中的方案,对数字的每一位单独分开,避免出现数字不一致的问题,对于提升数学能力有重要帮助
    对于罕见字词(如特殊符号等),支持 UTF-8 characters 的 byte 编码,因此做到未知字词的全覆盖
  • 数据集改进
    使用了大约 1.2T 中英 tokens 进行训练(基于开源的中英文数据和自行抓取的中文互联网数据以及部分高质量知识性数据进行的数据清洗),而 LLaMA 7B 使用 1T 英文 tokens 进行训练

比如为提升训练时的吞吐,做了以下优化

  • 算子优化技术:采用更高效算子,如 Flash-Attention,NVIDIA apex 的 RMSNorm 等。
  • 算子切分技术:将部分计算算子进行切分,减小内存峰值。
  • 混合精度技术:降低在不损失模型精度的情况下加速计算过程。
  • 训练容灾技术:训练平台和训练框架联合优化,IaaS + PaaS 实现分钟级的故障定位和任务恢复。
  • 通信优化技术,具体包括:
    采用拓扑感知的集合通信算法,避免网络拥塞问题,提高通信效率
    根据卡数自适应设置 bucket size,提高带宽利用率
    根据模型和集群环境,调优通信原语的触发时机,从而将计算和通信重叠

基于上述的几个优化技术,使得在千卡 A800 显卡上达到了 7B 模型 182 TFLOPS 的吞吐,GPU 峰值算力利用率高达 58.3%

2.3 baichuan-7B的微调

本次微调参考项目:https://github.com/wp931120/baichuan_sft_lora

由于baichuan没有 supervised finetune 这一步,没有和人类意图进行对齐,经常听不懂你下达的指令。该项目遂利用belle 0.5M 指令微调数据,采用qlora的量化微调的方式对百川大模型进行人类意图对齐训练

训练前置条件,先从huggingface 中将baichuan7b 大模型权重 ,然后,最后运行sft_lora.py 脚本
先将百川LLM 采用qlora的 nf4 和双重量化方式进行量化
在采用lora进行指令微调

本次微调baichuan-7B的步骤如下

  1. 微调之前的准备
    下载项目仓库
    git clone https://github.com/wp931120/baichuan_sft_lora.git
    cd baichuan_sft_lora
    配置环境
    conda create -n baichuan-7b python=3.9
    conda activate baichuan-7b
    pip install -r requirements.txt
    数据集下载
    sft 数据集采用的是belle 0.5M
    下载地址:https://huggingface.co/datasets/BelleGroup/train_0.5M_CN/tree/main
    将 belle 数据集 train_0.5M_CN 下载到本地放到项目目录下的dataset文件夹下
  2. 将百川LLM 采用qlora的 nf4 和双重量化方式进行量化
  3. 再采用lora进行指令微调
    wp931120x/baichuan_4bit_lora · Hugging Face
  4. 修改并运行sft_lora.py文件
    将sft_lora.py中的模型路径设置为自己的模型路径
    执行python sft_lora.py运行代码
    import os  # 导入os模块,这个模块提供了一种方便的使用操作系统依赖功能的方式
    os.environ['CUDA_VISIBLE_DEVICES'] = '0'  # 设置CUDA可见设备,'0'表示仅使用第一块GPU
    
    from datasets import load_dataset  # 导入load_dataset函数,用于加载数据集
    import transformers                # 导入transformers库,这是一个常用的NLP库
    
    # 导入Trainer和TrainingArguments,分别用于模型的训练和训练参数的设置
    from transformers import Trainer, TrainingArguments
    # 导入AutoTokenizer和AutoModelForCausalLM,分别用于自动化地从预训练模型中获取Tokenizer和模型
    from transformers import AutoTokenizer, AutoModelForCausalLM
    # 导入BitsAndBytesConfig,用于设置模型的量化配置  
    from transformers import BitsAndBytesConfig
      
    # 导入一些特定的函数和配置类
    from peft import (
        LoraConfig,
        get_peft_model,
        prepare_model_for_kbit_training,
        set_peft_model_state_dict,
    )
    import torch  # 导入PyTorch库,这是一个常用的深度学习库
    
    
    # 定义一些配置信息
    CUTOFF_LEN = 1024  
    VAL_SET_SIZE = 2000
    DATA_PATH = "./dataset/Belle_open_source_0.5M.json" 
    OUTPUT_DIR = "baichuansft"
    resume_from_checkpoint = "baichuansft"
    
    # 设置设备映射,""表示默认设备,0表示设备编号
    device_map = {"": 0}
    # 使用AutoTokenizer从预训练模型中获取Tokenizer
    tokenizer = AutoTokenizer.from_pretrained("./baichuan-7B",trust_remote_code=True)
    # 使用AutoModelForCausalLM从预训练模型中获取模型,并设置量化配置
    model = AutoModelForCausalLM.from_pretrained("./baichuan-7B",
                                                 trust_remote_code=True,
                                                 quantization_config=BitsAndBytesConfig(
                                                     load_in_4bit=True,
                                                     bnb_4bit_compute_dtype=torch.bfloat16,
                                                     bnb_4bit_use_double_quant=True,
                                                     bnb_4bit_quant_type='nf4'
                                                 ),
                                                 device_map=device_map)
    
    model = prepare_model_for_kbit_training(model)  # 准备模型进行kbit训练
    
    # 导入bitsandbytes模块
    import bitsandbytes as bnb
    
    # 定义一个函数,用于找到模型中所有的线性层的名称
    def find_all_linear_names(model):
        cls = bnb.nn.Linear4bit 
        lora_module_names = set()
        for name, module in model.named_modules():  # 遍历模型中的所有模块
            if isinstance(module, cls):  # 如果模块是线性层
                names = name.split('.')
                lora_module_names.add(names[0] if len(names) == 1 else names[-1])  # 添加到线性层名称集合中
    
        if 'lm_head' in lora_module_names:  # 如果'lm_head'在名称集合中,需要移除
            lora_module_names.remove('lm_head')
        return list(lora_module_names)  # 返回线性层名称列表
    
    # 获取所有的线性层的名称
    modules = find_all_linear_names(model)
    
    # 设置LoRA配置
    config = LoraConfig(
        r=8,
        lora_alpha=16,
        lora_dropout=0.05,
        bias="none",
        target_modules=modules,
        task_type="CAUSAL_LM",
    )
    
    # 获取用于训练的模型
    model = get_peft_model(model, config)
    tokenizer.pad_token_id = 0  # 设置tokenizer的pad_token_id为0
    
    # 如果有设置从检查点恢复
    if resume_from_checkpoint:
        # 检查可用的权重并加载
        checkpoint_name = os.path.join(
            resume_from_checkpoint, "pytorch_model.bin"
        )  # 完整的检查点
        # 如果完整的检查点不存在,则加载LoRA模型的检查点
        if not os.path.exists(checkpoint_name):
            checkpoint_name = os.path.join(
                resume_from_checkpoint, "adapter_model.bin"
            )  # 仅LoRA模型 - 上面的LoRA配置必须匹配
            resume_from_checkpoint = (
                False  # 所以训练器不会尝试加载状态
            )
        if os.path.exists(checkpoint_name):
            print(f"Restarting from {checkpoint_name}")
            adapters_weights = torch.load(checkpoint_name)
            set_peft_model_state_dict(model, adapters_weights)  # 设置模型的状态字典
        else:
            print(f"Checkpoint {checkpoint_name} not found")
    
    # 加载数据集
    data = load_dataset("json", data_files=DATA_PATH)
    
    # 定义tokenize函数,用于将输入进行tokenize
    def tokenize(prompt, add_eos_token=True):
        # 这里是tokenize的具体操作
        result = tokenizer(
            prompt,
            truncation=True,
            max_length=CUTOFF_LEN,
            padding=False,
            return_tensors=None,
        )
        # 添加EOS token
        if (
                result["input_ids"][-1] != tokenizer.eos_token_id
                and len(result["input_ids"]) < CUTOFF_LEN
                and add_eos_token
        ):
            result["input_ids"].append(tokenizer.eos_token_id)
            result["attention_mask"].append(1)
    
        if add_eos_token and len(result["input_ids"]) >= CUTOFF_LEN:
            result["input_ids"][CUTOFF_LEN - 1] = tokenizer.eos_token_id
            result["attention_mask"][CUTOFF_LEN - 1] = 1
    
        # 输入和标签都是input_ids
        result["labels"] = result["input_ids"].copy()
    
        return result
    
    # 定义generate_and_tokenize_prompt函数,用于生成并tokenize输入
    def generate_and_tokenize_prompt(data_point):
        instruction = data_point['instruction']
        input_text = data_point["input"]
        input_text = "Human: " + instruction + input_text + "\n\nAssistant: "
        input_text = tokenizer.bos_token + input_text if tokenizer.bos_token != None else input_text
        target_text = data_point["output"] + tokenizer.eos_token
        full_prompt = input_text + target_text
        tokenized_full_prompt = tokenize(full_prompt)
        return tokenized_full_prompt
    
    # 划分训练集和验证集,并进行shuffle和map操作
    if VAL_SET_SIZE > 0:
        train_val = data["train"].train_test_split(
            test_size=VAL_SET_SIZE, shuffle=True, seed=42
        )
        train_data = train_val["train"].shuffle().map(generate_and_tokenize_prompt)
        val_data = train_val["test"].shuffle().map(generate_and_tokenize_prompt)
    else:
        train_data = data['train'].shuffle().map(generate_and_tokenize_prompt)
        val_data = None
    
    # 创建Trainer对象,用于进行训练
    trainer = Trainer(
        model=model,
        train_dataset=train_data,
        eval_dataset=val_data,
        args=TrainingArguments(
            num_train_epochs=1,
            per_device_train_batch_size=1,
            per_device_eval_batch_size=1,
            learning_rate=3e-4,
            gradient_accumulation_steps=4,
            evaluation_strategy="steps" if VAL_SET_SIZE > 0 else "no",
            save_strategy="steps",
            eval_steps=2000 if VAL_SET_SIZE > 0 else None,
            save_steps=2000,
            output_dir=OUTPUT_DIR,
            report_to = "tensorboard",
            save_total_limit=3,
            load_best_model_at_end=True if VAL_SET_SIZE > 0 else False,
            optim="adamw_torch"
        ),
        data_collator=transformers.DataCollatorForSeq2Seq(tokenizer,
                                                          pad_to_multiple_of=8,
                                                          return_tensors="pt",
                                                          padding=True),
    )
    
    # 进行训练
    trainer.train(resume_from_checkpoint=False)
    # 保存预训练模型
    model.save_pretrained(OUTPUT_DIR)
    最终,显存占用为7G左右

 第三部分 ChatGLM2-6B的部署与微调

// 待更

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

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

相关文章

【React笔记】react循环列表的写法

react循环的写法 简单循环输出人名简单循环输出json格式数组 简单循环输出人名 循环输出people数组中的四个人名 import ReactDOM from react-dom/client;const people [宋江,卢俊义,吴用,公孙胜 ]; const root ReactDOM.createRoot(document.getElementById(root)); root.…

深度学习入门知识总结

0、前言&#xff1a;学习了深度学习入门的鱼书&#xff0c;很多基础概念有了大概了解&#xff0c;及时总结&#xff0c;方便日后查找 1、神经网络&#xff08;深度学习&#xff09;的起源算法——感知机&#xff1a; 定义&#xff1a;感知机接收多个输入信号&#xff0c;输出一…

【C++修炼之路】33.特殊类设计

每一个不曾起舞的日子都是对生命的辜负 特殊类设计 一.设计一个类&#xff0c;不能被拷贝二.设计一个类&#xff0c;只能在堆上创建对象1. 普通类的创建对象2.只能在堆上创建对象的类 三.设计一个类&#xff0c;只能在栈上创建对象四.设计一个类&#xff0c;不能被继承五.单例模…

Flink使用总结

本文主要是为Flink的java客户端使用和flink-sql使用的大致介绍&#xff0c;具体使用查看文档页面。 java client使用 文档 Apache Flink Documentation | Apache Flink 数据处理模型 maven依赖 <?xml version"1.0" encoding"UTF-8"?> <pro…

【YOLOv8-Seg】实战二:LabVIEW+OpenVINO加速YOLOv8-seg实例分割

‍‍&#x1f3e1;博客主页&#xff1a; virobotics的CSDN博客&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『LabVIEW深度学习实战』 &#x1f37b;上期文章&#xff1a; 【YOLOv8-seg】实战一&#xff1a;手把手教你使用YOLOv8实现实例分割 …

【数据分析 - 基础入门之NumPy⑥】- NumPy案例巩固强化

文章目录 前言一、NumPy基础训练1.1 创建一个长度为10的一维全为0的ndarray对象&#xff0c;并让第5个元素为11.2 创建一个元素为从10到49的ndarray对象1.3 将第2题的所有元素位置反转1.4 创建一个10*10的ndarray对象并打印最大最小元素1.5 创建一个10*10的ndarray对象&#xf…

程序设计相关概念

计算机概念 计算机是根据指令操作数据的设备。具有功能性和可编程性的特点。 功能性&#xff1a;对数据的操作&#xff0c;表现为数据计算、输入输出处理和结果存储等。 可编程性&#xff1a;根据一系列指令自动地、可预测地、准确地完成操作者的意图。 计算机的发展 计算机…

【LVS负载均衡集群】

文章目录 一.什么是集群1.集群的含义 二.集群使用在那个场景三.集群的分类1.负载均衡器集群2.高可用集群3.高性能运算集群 四.负载集群的架构1.第一层&#xff0c;负载调度器2.第二层&#xff0c;服务器池3.第三层&#xff0c;共享存储 五.负载均衡集群的工作模式1.地址转换 &a…

Unity减少等待快速进入运行

我们平时播放时一旦修改了c#的脚本总要加载进行等待&#xff0c;网上也缺乏如何设置&#xff0c;以及为什么&#xff1f;这样做可以达到这样的效果。 ------如何设置&#xff1f;【默认并不会开启】 Edit->Project Settings->Editor->Enter Player Mode Options 这样…

企业为什么要做自动化测试?如何成功实施自动化测试?

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 企业为什么需要自…

【LeetCode热题100】打卡第33天:环形链表LRU缓存

文章目录 【LeetCode热题100】打卡第33天&#xff1a;环形链表&LRU缓存⛅前言 环形链表&#x1f512;题目&#x1f511;题解 LRU缓存&#x1f512;题目&#x1f511;题解 【LeetCode热题100】打卡第33天&#xff1a;环形链表&LRU缓存 ⛅前言 大家好&#xff0c;我是知…

C++ 实现生产者消费者模型 (线程同步、互斥锁、条件变量锁)详细注释

代码结构 任务&#xff1a;这里用一个int类型的taskNumber代替任务任务队列类&#xff1a;封装了任务队列&#xff0c;存&#xff0c;取等操作。生产者工作函数&#xff1a;生产者执行的函数&#xff0c;向任务队列中添加任务&#xff0c;每个生产者生产3个任务消费者工作函数…

脱离产品怎么可能完成测试?

“脱离应用场景谈技术毫无意义”。其实很多东西都是如此&#xff0c;这个有点哲理的味道了。我们是做engineering&#xff0c;软件工程也是工程&#xff0c;工程的特点就是不能停留在理论和方法&#xff0c;最后要做出东西来&#xff0c;软的也好&#xff0c;硬的也好。 人有…

爬虫反反爬

目录 为什么要反爬&#xff1f; 经常被反爬的主要人群 常见的反爬策略 通过headers字段来反爬 通过headers中的User-Agent字段来反爬 通过referer字段或者是其他字段来反爬 通过cookie来反爬 通过请求参数来反爬 通过从html静态文件中获取请求数据(github登录数据) 通…

【Go】vscode 安装go环境gopls失败

项目场景&#xff1a; 想要在VSCode安装go环境&#xff0c;但是gopls下载失败&#xff0c;导致vscode无法使用language server 问题描述 自动下载失败&#xff0c;在打开命令面板&#xff08;CtrlshiftP&#xff09;之后&#xff0c;输入go install/update 下载也失败 $ g…

并发编程 - Event Bus 设计模式

文章目录 Pre设计CodeBus接口自定义注解 Subscribe同步EventBus异步EventBusSubscriber注册表RegistryEvent广播Dispatcher 测试简单的Subscriber同步Event Bus异步Event Bus 小结 Pre 我们在日常的工作中&#xff0c;都会使用到MQ这种组件&#xff0c; 某subscriber在消息中间…

PillarNext论文解读

这篇文章是轻舟智航23年的一篇论文&#xff0c;是对pillarNet进行改进。 改进方面&#xff1a; 1.训练更长的时间在检测头增加IOU预测score&#xff0c;这个iou分数预测不太清楚&#xff0c;不知道是不是iouloss 2.扩大感受野&#xff0c;包括Neck部分使用FPN或者BiFPN.使用…

3.zabbix操作二

文章目录 zabbix操作二部署zabbix代理服务器安装zabbix_proxy安装数据库配置代理服务器配置文件web端添加agent代理并连接主机 部署zabbix高可用群集zabbix监控Windows系统zabbix监控java应用zabbix监控SNMP zabbix操作二 部署zabbix代理服务器 分布式监控的作用&#xff1a;…

Flink web UI配置账号密码,权限控制

由于Flink自带的web UI界面没有账号密码&#xff0c;需要通过nginx实现该效果。 1.安装httpd-tools工具 yum install httpd-tools -y 2.生成用户名密码文件 htpasswd -c /usr/local/nginx/conf/flinkuser username passwd flinkuser&#xff1a;为生成的用户名密码文件名称 …

Apache Doris (二十一) :Doris Rollup物化索引创建与操作

目录 1. 创建测试表 2. 创建Rollup物化索引表 3. 查看Rollup物化索引表 4. 删除Rollup物化索引表 5. 验证Rollup物化索引使用 进入正文之前&#xff0c;欢迎订阅专题、对博文点赞、评论、收藏&#xff0c;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; 宝子们点…