使用PEFT库进行ChatGLM3-6B模型的QLORA高效微调

news2024/11/13 19:27:49

PEFT库进行ChatGLM3-6B模型QLORA高效微调

  • QLORA微调ChatGLM3-6B模型
    • 安装相关库
    • 使用ChatGLM3-6B
    • 模型GPU显存占用
    • 准备数据集
    • 加载数据集
    • 数据处理
    • 数据集处理
    • 加载量化模型-4bit
    • 预处理量化模型
    • 配置LoRA适配器
    • 训练超参数配置
    • 开始训练
    • 保存LoRA模型
    • 模型推理
    • 合并模型
    • 使用微调后的模型

QLORA微调ChatGLM3-6B模型

LoRA的核心思想是将可调整的低秩矩阵注入到Transformer架构的每一层中,充当"适配器"的作用。这样可以使模型针对特定任务进行调整和专门化,同时最大限度地减少额外的参数数量,提高参数效率。

QLoRA是LoRA的扩展版本,在微调过程中引入了量化技术,以进一步提高参数效率。QLoRA利用LoRA的原理,并引入了4位NormalFloat(NF4)量化和双重量化技术,进一步减少了存储和计算资源的使用。

安装相关库

pip install datasets==2.17.0 pandas transformers==4.37.2 peft==0.8.0  accelerate==0.27.0 bitsandbytes autoawq optimum

使用ChatGLM3-6B

直接调用ChatGLM3-6B模型来生成对话

from transformers import AutoTokenizer, AutoModel

model_id = "/root/work/chatglm3-6b"
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
#model = AutoModel.from_pretrained(model_id, trust_remote_code=True).half().cuda()
model = AutoModel.from_pretrained(model_id, trust_remote_code=True, device='cuda')

model = model.eval()
response, history = model.chat(tokenizer, "你好", history=history)
print(response)

在这里插入图片描述

模型GPU显存占用

默认情况下,模型以半精度(float16)加载,模型权重需要大概 13GB显存。

获取当前模型占用的GPU显存

memory_bytes = model.get_memory_footprint()
# 转换为GB
memory_gb = memory_bytes / (1024 ** 3)  
print(f"{memory_gb :.2f}GB")

注意:

与实际进程占用有差异,差值为预留给PyTorch的显存

准备数据集

准备数据集其实就是指令集构建,LLM的微调一般指指令微调过程。所谓指令微调,就是使用的微调数据格式、形式。

训练目标是让模型具有理解并遵循用户指令的能力。因此在指令集构建时,应该针对目标任务,针对性的构建任务指令集。

[
	{
	  "instruction": "用户指令(必填)",
	  "input": "用户输入(选填)",
	  "output": "模型回答(必填)",
	}
]
instruction:用户指令,告知模型其需要完成的任务

input:用户输入,是完成用户指令所必须的输入内容

output:模型回答,模型应该给出的输出

这里根据企业私有文档数据,生成相关格式的训练数据集,大概格式如下:

[  
	{
	    "instruction": "内退条件是什么?",
	    "input": "",
	    "output": "内退条件包括与公司签订正式劳动合同并连续工作满20年及以上,以及距离法定退休年龄不足5年。特殊工种符合国家相关规定可提前退休的也可在退休前5年内提出内退申请。"
	  },
]

作者:CodeDevMaster
链接:https://juejin.cn/post/7384636970033938482
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

加载数据集

from datasets import load_dataset  # 导入 load_dataset 函数

# 加载处理好的训练数据集
dataset = load_dataset("json", data_files="./train_data.json")
print(dataset)  # 打印加载的数据集对象
DatasetDict({
    train: Dataset({
        features: ['instruction', 'output'],
        num_rows: 20182
    })
})

数据处理

Lora训练数据是需要经过tokenize编码处理,然后后再输入模型进行训练。一般需要将输入文本编码为input_ids,将输出文本编码为labels,编码之后的结果都是多维的向量。

加载AutoTokenizer

from transformers import AutoTokenizer  # 导入 AutoTokenizer 类

# 使用 AutoTokenizer 类的 from_pretrained 方法加载指定模型的 tokenizer
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)

需要定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典。

# tokenize_func 函数
def tokenize_func(example, tokenizer, ignore_label_id=-100):
    """
    对单个数据样本进行tokenize处理。

    参数:
    example (dict): 包含'content'和'summary'键的字典,代表训练数据的一个样本。
    tokenizer (transformers.PreTrainedTokenizer): 用于tokenize文本的tokenizer。
    ignore_label_id (int, optional): 在label中用于填充的忽略ID,默认为-100。

    返回:
    dict: 包含'tokenized_input_ids'和'labels'的字典,用于模型训练。
    """
    prompt_text = ''                          # 所有数据前的指令文本
    max_input_length = 512                    # 输入的最大长度
    max_output_length = 1536                  # 输出的最大长度

    # 构建问题文本
    question = prompt_text + example['instruction']
    if example.get('input', None) and example['input'].strip():
        question += f'\n{example["input"]}'

    # 构建答案文本
    answer = example['output']

    # 对问题和答案文本进行tokenize处理
    q_ids = tokenizer.encode(text=question, add_special_tokens=False)
    a_ids = tokenizer.encode(text=answer, add_special_tokens=False)

    # 如果tokenize后的长度超过最大长度限制,则进行截断
    if len(q_ids) > max_input_length - 2:  # 保留空间给gmask和bos标记
        q_ids = q_ids[:max_input_length - 2]
    if len(a_ids) > max_output_length - 1:  # 保留空间给eos标记
        a_ids = a_ids[:max_output_length - 1]

    # 构建模型的输入格式
    input_ids = tokenizer.build_inputs_with_special_tokens(q_ids, a_ids)
    question_length = len(q_ids) + 2  # 加上gmask和bos标记

    # 构建标签,对于问题部分的输入使用ignore_label_id进行填充
    labels = [ignore_label_id] * question_length + input_ids[question_length:]

    return {'input_ids': input_ids, 'labels': labels}

可以从从原始数据集中提取出一个较小规模的训练子集,以便稍后对这个子集进行处理、训练或其他操作。

from datasets import DatasetDict  # 导入 DatasetDict 类

small_dataset = DatasetDict()  # 创建一个空的 DatasetDict 对象

# 在 small_dataset 中存储一个部分数据集,包括以下步骤:
# 1. 从原始数据集的 'train' 部分中随机打乱数据(使用随机种子 16)
# 2. 选择前 1000 条样本构成一个小的数据集
small_dataset["train"] = dataset["train"].shuffle(seed=16).select(range(1000))

# 打印数据集
print(small_dataset)

进行数据映射处理,同时删除特定列

# 获取 'train' 部分的列名
column_names = dataset['train'].column_names  

# 使用lambda函数调用tokenize_func函数,并传入example和tokenizer作为参数
tokenized_dataset = small_dataset['train'].map(
    lambda example: tokenize_func(example, tokenizer),
    batched=False,  # 不按批次处理
    remove_columns=column_names  # 移除特定列(column_names中指定的列)
)

查看处理结果

print(tokenized_dataset[0])

在这里插入图片描述

数据集处理

需要使用一个数据收集器,可以使用transformers 中的DataCollatorForSeq2Seq数据收集器

from transformers import DataCollatorForSeq2Seq

data_collator = DataCollatorForSeq2Seq(
    tokenizer,
    model=model,
    label_pad_token_id=-100,
    pad_to_multiple_of=None,
    padding=True
)

或者自定义实现一个数据收集器

import torch
from typing import List, Dict, Optional

# DataCollatorForChatGLM 类
class DataCollatorForChatGLM:
    """
    用于处理批量数据的DataCollator,尤其是在使用 ChatGLM 模型时。

    该类负责将多个数据样本(tokenized input)合并为一个批量,并在必要时进行填充(padding)。

    属性:
    pad_token_id (int): 用于填充(padding)的token ID。
    max_length (int): 单个批量数据的最大长度限制。
    ignore_label_id (int): 在标签中用于填充的ID。
    """

    def __init__(self, pad_token_id: int, max_length: int = 2048, ignore_label_id: int = -100):
        """
        初始化DataCollator。

        参数:
        pad_token_id (int): 用于填充(padding)的token ID。
        max_length (int): 单个批量数据的最大长度限制。
        ignore_label_id (int): 在标签中用于填充的ID,默认为-100。
        """
        self.pad_token_id = pad_token_id
        self.ignore_label_id = ignore_label_id
        self.max_length = max_length

    def __call__(self, batch_data: List[Dict[str, List]]) -> Dict[str, torch.Tensor]:
        """
        处理批量数据。

        参数:
        batch_data (List[Dict[str, List]]): 包含多个样本的字典列表。

        返回:
        Dict[str, torch.Tensor]: 包含处理后的批量数据的字典。
        """
        # 计算批量中每个样本的长度
        len_list = [len(d['input_ids']) for d in batch_data]
        batch_max_len = max(len_list)  # 找到最长的样本长度

        input_ids, labels = [], []
        for len_of_d, d in sorted(zip(len_list, batch_data), key=lambda x: -x[0]):
            pad_len = batch_max_len - len_of_d  # 计算需要填充的长度
            # 添加填充,并确保数据长度不超过最大长度限制
            ids = d['input_ids'] + [self.pad_token_id] * pad_len
            label = d['labels'] + [self.ignore_label_id] * pad_len
            if batch_max_len > self.max_length:
                ids = ids[:self.max_length]
                label = label[:self.max_length]
            input_ids.append(torch.LongTensor(ids))
            labels.append(torch.LongTensor(label))

        # 将处理后的数据堆叠成一个tensor
        input_ids = torch.stack(input_ids)
        labels = torch.stack(labels)

        return {'input_ids': input_ids, 'labels': labels}

data_collator = DataCollatorForChatGLM(pad_token_id=tokenizer.pad_token_id)

加载量化模型-4bit

对于加载4bit量化模型,需要设置多个参数,具体参数如下:使用nf4量化数据类型加载模型,开启双量化配置,以bf16混合精度训练

以4-bits量化加载ChatGLM3-6B模型,只需要大约4GB左右显存。

from transformers import AutoModel, BitsAndBytesConfig

# QLoRA量化配置
q_config = BitsAndBytesConfig(load_in_4bit=True, # 是否在4位精度下加载模型
                              bnb_4bit_quant_type='nf4', # 4位精度量化的类型,表示使用nf4量化类型
                              bnb_4bit_use_double_quant=True,  # 是否使用双精度量化
                              bnb_4bit_compute_dtype=torch.float16) # 4位精度计算的数据类型,使用半精度浮点数

model = AutoModel.from_pretrained('./chatglm3-6b',
                                  quantization_config=q_config,
                                  device_map='auto',
                                  trust_remote_code=True)

# 优化内存使用和计算效率
model.supports_gradient_checkpointing = True  
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
model.config.use_cache = False 

预处理量化模型

预处理量化后的模型,使其可以支持低精度微调训练

peft库中的prepare_model_for_kbit_training函数是用于为模型执行量化准备的工具。这个函数通常用于准备在低比特宽度(如8-bit, 4-bit, 2-bit等)下进行模型训练或微调。

该函数的主要功能是调整模型的权重和激活,使其适应在指定的比特位宽度下进行有效的训练。

from peft import prepare_model_for_kbit_training

kbit_model = prepare_model_for_kbit_training(model)

配置LoRA适配器

在peft中使用LoRA非常简单。借助PeftModel抽象,可以快速将低秩适配器(LoRA)应用到任意模型中。

在初始化相应的微调配置类(LoraConfig)时,需要显式指定在哪些层新增适配器(Adapter),并将其设置正确。

ChatGLM3-6B模型通过以下方式获取需要训练的模型层的名字

from peft.utils import TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING

target_modules = TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING['chatglm']

在PEFT库的 constants.py 文件中定义了不同的 PEFT 方法,在各类大模型上的微调适配模块。

在这里插入图片描述

lora_config = LoraConfig(
    target_modules=target_modules,
    r=4,# LoRA秩
    lora_alpha=32,
    lora_dropout=0.05,
    bias='none',
    inference_mode=False,
    task_type=TaskType.CAUSAL_LM
)

# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
qlora_model = get_peft_model(kbit_model, lora_config)

# 打印出模型中可训练的参数
qlora_model.print_trainable_parameters()

训练超参数配置

训练总步数steps的计算:

epoch: 1个epoch表示对全部训练集样本进行一次完整的训练

num_train_epochs:多少个epoch, 表示进行多少个、多少轮完整数据集的训练

每个epoch的训练步数:

steps/epoch = num_train_examples / (batch_size * gradient_accumulation_steps)

训练总步数:

total_steps = steps/epoch * num_train_epochs
from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir="models/chatglm3-6b",          # 输出目录
    per_device_train_batch_size=4,                     # 每个设备的训练批量大小
    gradient_accumulation_steps=4,                     # 梯度累积步数
    # per_device_eval_batch_size=8,                      # 每个设备的评估批量大小
    learning_rate=1e-3,                                # 学习率
    num_train_epochs=1,                                # 训练轮数
    lr_scheduler_type="linear",                        # 学习率调度器类型
    warmup_ratio=0.1,                                  # 预热比例
    logging_steps=10,                                 # 日志记录步数
    save_strategy="steps",                             # 模型保存策略
    save_steps=100,                                    # 模型保存步数
    # evaluation_strategy="steps",                       # 评估策略
    # eval_steps=500,                                    # 评估步数
    optim="adamw_torch",                               # 优化器类型
    fp16=True,                                        # 是否使用混合精度训练
)

启用指标评估

from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir="models/chatglm3-6b",          # 输出目录
    per_device_train_batch_size=4,                     # 每个设备的训练批量大小
    gradient_accumulation_steps=4,                     # 梯度累积步数
    learning_rate=1e-3,                                # 学习率
    max_steps=100,                                     # 训练步数
    lr_scheduler_type="linear",                        # 学习率调度器类型
    warmup_ratio=0.1,                                  # 预热比例
    logging_steps=10,                                 # 日志记录步数
    save_strategy="steps",                             # 模型保存策略
    save_steps=20,                                    # 模型保存步数
    optim="adamw_torch",                               # 优化器类型
    fp16=True,                                        # 是否使用混合精度训练
)

开始训练

trainer = Trainer(
        model=qlora_model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=data_collator
    )
    
trainer.train()

保存LoRA模型

lora_model_path = "lora/chatglm3-6b"
trainer.model.save_pretrained(lora_model_path )
#model.save_pretrained(lora_model_path )

在这里插入图片描述

模型推理

使用QLoRA微调后的ChatGLM3-6B模型进行对比模型推理

import torch
from transformers import AutoModel, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel, PeftConfig

# QLoRA量化配置
q_config = BitsAndBytesConfig(load_in_4bit=True,
                              bnb_4bit_quant_type='nf4',
                              bnb_4bit_use_double_quant=True,
                              bnb_4bit_compute_dtype=torch.float32)

# 加载量化后的原始模型
base_model = AutoModel.from_pretrained('./chatglm3-6b',
                                       quantization_config=q_config,
                                       trust_remote_code=True,
                                       device_map='auto')
# 优化
base_model.requires_grad_(False)
base_model.eval()

# 加载tokenizer 
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True)

使用原始ChatGLM3-6B模型进行推理测试

input_text = 'xxxx?'
response, history = base_model.chat(tokenizer=tokenizer, query=input_text)
print(f'ChatGLM3-6B 微调前:\n{response}')

使用PeftModel合并源model与PEFT微调后的参数,然后进行推理测试

input_text = 'xxxx?'
# 合并
model = PeftModel.from_pretrained(base_model, './chatglm3-6b-lora')
response, history = model.chat(tokenizer=tokenizer, query=input_text)
print(f'ChatGLM3-6B 微调后: \n{response}')

合并模型

将lora权重合并到大模型中,将模型参数加载为16位浮点数

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch 

model_path="./chatglm3-6b"
peft_model_path="./lora/chatglm3-6b"
save_path = "chatglm3-6b-lora"

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, low_cpu_mem_usage=True, torch_dtype=torch.float16, device_map="auto")
model = PeftModel.from_pretrained(model, peft_model_path)
model = model.merge_and_unload()

tokenizer.save_pretrained(save_path)
model.save_pretrained(save_path)

查看合并文件
在这里插入图片描述

使用微调后的模型

from transformers import AutoTokenizer, AutoModel

tokenizer = AutoTokenizer.from_pretrained("chatglm3-6b-lora", trust_remote_code=True)
model = AutoModel.from_pretrained("chatglm3-6b-lora", trust_remote_code=True, device='cuda')

model = model.eval()
response, history = model.chat(tokenizer, "内退条件是什么?", history=[])
print(response)

在这里插入图片描述

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

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

相关文章

UE5 本地化多语言方案

导入插件: https://www.unrealengine.com/marketplace/zh-CN/product/07e1d9bd9ced444c9b2a7e232161f74d​www.unrealengine.com/marketplace/zh-CN/product/07e1d9bd9ced444c9b2a7e232161f74d 打开测试关卡 打开插件下图目录,csv文件可以添加多个&…

MD5加密和注册页面的编写

MD5加密 1.导入包 npm install --save ts-md5 2.使用方式 import { Md5 } from ts-md5; //md5加密后的密码 const md5PwdMd5.hashStr("123456").toUpperCase(); 遇见的问题及用到的技术 注册页面 register.vue代码 <template><div class"wappe…

模型评估、交叉验证

目录 一、模型评估&#xff1a;二、交叉验证&#xff1a; 一、模型评估&#xff1a; 模型评估用来检验模型的预测精度。 首先数据集分为训练集和测试集两部分&#xff0c;使用训练集进行模型的训练&#xff0c;使用测试集进行模型的评估。 注意&#xff0c;模型评估阶段应该…

A股强反之后继续下单,难道是主力再使障眼法?

今天的A股太不对劲了&#xff0c;主力再使障眼法&#xff0c;很多散户都被成功迷惑&#xff0c;市场有望出现芙蓉出水&#xff0c;不废话&#xff0c;直奔主题&#xff1a; 1、今天两市低开再次探底&#xff0c;太不对劲了&#xff0c;昨天主力大幅流入&#xff0c;今天又跑了…

IT入门知识第十部分《IT职业发展》(10/10)

IT职业发展的深度解析&#xff1a;技能、路径与规划 在当今这个快速发展的数字化时代&#xff0c;IT行业不仅是技术革新的前沿阵地&#xff0c;也是职业发展和个人成长的沃土。本文将深入探讨IT行业的职业发展&#xff0c;包括职业路径的选择、技能提升的策略、以及职业规划的…

FastAPI 学习之路(三十七)元数据和文档 URL

实现前的效果 那么如何实现呢&#xff0c;第一种方式如下&#xff1a; from routers.items import item_router from routers.users import user_router""" 自定义FastApi应用中的元数据配置Title&#xff1a;在 OpenAPI 和自动 API 文档用户界面中作为 API 的…

迅为3A5000LoongArch架构核心主板支持国产麒麟、统信、以及实时系统翼辉SylixOS

性能强 采用全国产龙芯3A5000处理器&#xff0c;基于龙芯自主指令系统 (LoongArch)的LA464微结构&#xff0c;并进一步提升频率&#xff0c;降低功耗&#xff0c;优化性能。 桥片 采用龙芯 7A2000&#xff0c;支持PCIE 3.0、USB 3.0和 SATA 3.0.显示接口2 路、HDMI 和1路 VGA&a…

昆法尔The Quinfall在Steam上怎么搜索 Steam上叫什么名字

昆法尔The Quinfall是一款全新的MMORPG&#xff0c;在中世纪的深处&#xff0c;参与独特的战斗和沉浸式的故事&#xff0c;有几十个不同的职业。而游戏中的战斗系统更是丰富多彩&#xff0c;无论是陆地激战、海上鏖战还是城堡围攻&#xff0c;都能让玩家感受到前所未有的刺激和…

pytorch实现水果2分类(蓝莓,苹果)

1.数据集的路径&#xff0c;结构 dataset.py 目的&#xff1a; 输入&#xff1a;没有输入&#xff0c;路径是写死了的。 输出&#xff1a;返回的是一个对象&#xff0c;里面有self.data。self.data是一个列表&#xff0c;里面是&#xff08;图片路径.jpg&#xff0c;标签&…

如何分辨AI生成的内容?AI生成内容检测工具对比实验

检测人工智能生成的文本对各个领域的组织都提出了挑战&#xff0c;包括学术界和新闻界等。生成式AI与大语言模型根据短描述来进行内容生成的能力&#xff0c;产生了一个问题&#xff1a;这篇文章/内容/作业/图像到底是由人类创作的&#xff0c;还是AI创作的&#xff1f;虽然 LL…

【数据库】Redis主从复制、哨兵模式、集群

目录 一、Redis的主从复制 1.1 主从复制的架构 1.2 主从复制的作用 1.3 注意事项 1.4 主从复制用到的命令 1.5 主从复制流程 1.6 主从复制实现 1.7 结束主从复制 1.8 主从复制优化配置 二、哨兵模式 2.1 哨兵模式原理 2.2 哨兵的三个定时任务 2.3 哨兵的结构 2.4 哨…

MT3047 区间最大值

思路&#xff1a; 使用哈希表map和set&#xff08;去重&#xff09;维护序列 代码&#xff1a; #include <bits/stdc.h> using namespace std; const int N 1e5 10; int n, k, A[N]; map<int, int> mp; // 元素出现的次数 set<int> s; // 维护出现…

Android平台GB28181记录仪在电网巡检抢修中的应用和技术实现

技术背景 在探讨Android平台GB28181设备接入端在电网巡检抢修优势之前&#xff0c;我们已经在执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景有了丰富的经验积累&#xff0c;…

linux创建定时任务

crontab方式 先查看是否有cron systemctl status crond 没有的话就安装 yum install cronie 打开你的crontab文件进行编辑。使用以下命令打开当前用户的crontab文件&#xff1a; crontab -e * * * * * /export/test.sh >> /export/test.log 2>&1/export/test.s…

什么是量化机器人?它能来作些什么?一篇文章带你了解!

在科技日新月异的今天&#xff0c;我们经常会听到一些听起来高大上的词汇&#xff0c;比如“人工智能”、“大数据”和“量化交易”。而在这其中&#xff0c;“量化机器人”更是一个让人既好奇又略感神秘的存在。今天&#xff0c;我们就用通俗易懂的语言&#xff0c;一起来揭开…

通知notification

通知 权限&#xff1a;manifest.xml&#xff0c;可以不提前写&#xff0c;后面写代码时显示缺少点击添加即可。 <uses-permission android:name"android.permission.VIBRATE"/>//振动权限 <uses-permission android:name"android.permission.POST_NOT…

洛谷 7.10 数数

Vanya and Books - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) ac代码 #include<bits/stdc.h> typedef long long ll;#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) const ll N1e3; using namespace std;int main() {IOS;ll x;cin>>x;ll ans0,px…

阿一课代表随堂分享:红队反向代理之使用frp搭建反向代理

frp反向代理 frp简介 frp 是一个开源、简洁易用、高性能的内网穿透和反向代理软件&#xff0c;支持 tcp, udp, http, https等协议。 frp 是一个可用于内网穿透的高性能的反向代理应用&#xff0c;分为服务端frps和客户端frpc&#xff0c;支持 tcp, udp, http, https 协议。详…

明白这两大关键点,轻松脱单不再是难题!

很多未婚男女都渴望找到心仪的伴侣&#xff0c;建立稳定的情感关系&#xff0c;但往往在脱单的过程中跌跌撞撞。平时与同学、同事之间相处得很融洽&#xff0c;一旦遇到心仪的异性&#xff0c;情商直接掉线&#xff0c;难道情商也会选择性地发挥作用吗&#xff1f;其实&#xf…

怎么将3张照片合并成一张?这几种拼接方法很实用!

怎么将3张照片合并成一张&#xff1f;在我们丰富多彩的日常生活里&#xff0c;是否总爱捕捉那些稍纵即逝的美好瞬间&#xff0c;将它们定格为一张张珍贵的图片&#xff1f;然而&#xff0c;随着时间的推移&#xff0c;这些满载回忆的宝藏却可能逐渐演变成一项管理挑战&#xff…