AI大模型应用(2)ChatGLM3本地部署及其在alpaca_zh数据集上的低精度微调

AI大模型应用(2)ChatGLM3部署及其在alpaca_zh数据集上的低精度微调

  • 我们之前已经了解了HuggingFace中peft库的几种高效微调方法。

参数高效微调PEFT(一)快速入门BitFit、Prompt Tuning、Prefix Tuning

参数高效微调PEFT(二)快速入门P-Tuning、P-Tuning V2

参数高效微调PEFT(三)快速入门LoRA、AdaLoRA

参数高效微调PEFT(四)快速入门(IA)3_ia3微调

  • 之前我们都是以单精度FP32加载模型,因此在训练过程中,模型本身占用的显存大小并没有改变。
  • 今天我们了解下ChatGLM3模型,以及低精度微调。
    • https://github.com/THUDM/ChatGLM3

1 ChatGLM3 API快速入门

ChatGLM3 是智谱AI和清华大学 KEG 实验室联合发布的对话预训练模型。ChatGLM3-6B 是 ChatGLM3 系列中的开源模型,在保留了前两代模型对话流畅、部署门槛低等众多优秀特性的基础上,ChatGLM3-6B 引入了如下特性:

  1. 更强大的基础模型: ChatGLM3-6B 的基础模型 ChatGLM3-6B-Base 采用了更多样的训练数据、更充分的训练步数和更合理的训练策略。
  2. 更完整的功能支持: ChatGLM3-6B 采用了全新设计的 Prompt 格式 ,除正常的多轮对话外。同时原生支持工具调用(Function Call)、代码执行(Code Interpreter)和 Agent 任务等复杂场景。
  3. 更全面的开源序列: 除了对话模型 ChatGLM3-6B 外,还开源了基础模型 ChatGLM3-6B-Base 、长文本对话模型 ChatGLM3-6B-32K 和进一步强化了对于长文本理解能力的 ChatGLM3-6B-128K。

1.1 本地GPU部署

首先需要下载本仓库:

git clone https://github.com/THUDM/ChatGLM3
cd ChatGLM3

然后使用 pip 安装依赖:

pip install -r requirements.txt
  • 为了保证 torch 的版本正确,请严格按照 官方文档 的说明安装。
  • 默认情况下,模型以 FP16 精度加载,需要大概 13GB 显存。
  • 如果本地没有GPU,可以租借云GPU:AutoDL平台租借GPU详解
  • 官方提供了很多demo示例,例如:模型微调、网页版对话 Demo、命令行对话 Demo、LangChain Demo、OpenAI API / Zhipu API Demo等,可以参考官方文档进行使用。

1.2 API快速调用

1.3.1 HuggingFace风格API调用

# 注意需要安装transformers库
# 可以参考:https://blog.csdn.net/qq_44665283/article/details/133823613
from transformers import AutoTokenizer, AutoModelForCausalLM# 本地模型路径
# 可以利用魔搭社区下载:https://blog.csdn.net/qq_44665283/article/details/139244306
model_path = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)# 1、非量化(默认是FP16精度加载,大概需要13GB显存)
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, device='cuda')# 2、量化为int8
# model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).quantize(8).cuda()# 3、量化为int4
# model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).quantize(4).cuda()for name, param in model.named_parameters():print(name, param.dtype)model.eval()
response, history = model.chat(tokenizer, "你好", history=[])print(f'response = {response}')
print(f'history = {history}')

1.3.2 OpenAI风格的API调用

作者团队已经推出了 OpenAI / ZhipuAI 格式的 开源模型 API 部署代码,可以作为任意基于 ChatGPT 的应用的后端。
目前,可以通过运行仓库中的 api_server.py 进行部署

root@autodl-container-c4c240bdcc-422ab4ba:~/autodl-tmp/llm/ChatGLM3# cd openai_api_demo# 1、设置从本地路径加载
# 因此需要修改api_server.py中的模型路径
# 还需要设置EMBEDDING_PATH,我这里使用的是text2vec-large-chinese模型
root@autodl-container-c4c240bdcc-422ab4ba:~/autodl-tmp/llm/ChatGLM3/openai_api_demo# vim api_server.py# MODEL_PATH = os.environ.get('MODEL_PATH', 'THUDM/chatglm3-6b')
# TOKENIZER_PATH = os.environ.get("TOKENIZER_PATH", MODEL_PATH)
# set Embedding Model path
# EMBEDDING_PATH = os.environ.get('EMBEDDING_PATH', 'BAAI/bge-m3')# set LLM path
MODEL_PATH = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'
TOKENIZER_PATH = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'
EMBEDDING_PATH = r'/root/autodl-tmp/models/ZhipuAI/text2vec-large-chinese'# 2、启动API服务,默认端口8000
root@autodl-container-c4c240bdcc-422ab4ba:~/autodl-tmp/llm/ChatGLM3/openai_api_demo# python api_server.py
  • 启动服务后,我们就可以使用OpenAI风格的API进行调用了:
from openai import OpenAIbase_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)def simple_chat(use_stream=True):messages = [{"role": "system","content": "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user's instructions carefully. "},{"role": "user","content": "你是谁"}]response = client.chat.completions.create(model="chatglm3-6b",    # 模型名称messages=messages,      # 会话历史stream=use_stream,      # 指定是否使用流式传输模式,如果设置为True,则返回一个生成器对象,可以逐个获取生成的文本片段;如果设置为False,则一次性返回完整的生成结果。max_tokens=256,         # 最多生成字数temperature=0.8,        # 温度presence_penalty=1.1,   # 控制生成回答时对已出现词汇的惩罚强度,较高的值会减少重复词汇的出现top_p=0.8)              # 采样概率print(response)if response:if use_stream:for chunk in response:print(chunk.choices[0].delta.content)else:content = response.choices[0].message.contentprint(content)else:print("Error:", response.status_code)
if __name__ == "__main__":simple_chat(use_stream=False)# simple_chat(use_stream=True)

输出:

ChatCompletion(id='', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='我是一个名为 ChatGLM3 的大型语言模型,由 Zhipu.AI 训练。我的目的是通过回答用户的问题来帮助他们解决问题。', role='assistant', function_call=None, tool_calls=None, name=None))], created=1721811856, model='chatglm3-6b', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=34, prompt_tokens=44, total_tokens=78)
)
  • 启动API服务后,我们也可以对文本进行embedding了
from openai import OpenAIbase_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)def embedding():response = client.embeddings.create(# model="bge-large-zh-1.5",model="text2vec-large-chinese",input=["你好,给我讲一个故事,大概100字"],)embeddings = response.data[0].embeddingprint("嵌入完成,维度:", len(embeddings))return embeddingsif __name__ == "__main__":# 嵌入完成,维度: 1024embedding()
  • 当然,我们也可以使用function_call了
  • 更详细的使用案例,可以参考官方tools_using_demo文件夹下内容
from openai import OpenAIbase_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)tools = [{"type": "function","function": {"name": "get_current_weather","description": "Get the current weather in a given location","parameters": {"type": "object","properties": {"location": {"type": "string","description": "The city and state, e.g. San Francisco, CA",},"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},},"required": ["location"],},},}]messages = [{"role": "user", "content": "What's the weather like in BeiJing?"}]
response = client.chat.completions.create(model="chatglm3-6b",messages=messages,tools=tools,tool_choice="auto",)print(response)
content = response.choices[0].message.contentprint(f'content:\n{content}')
"""
content:
get_current_weather```python
tool_call(location='Beijing', unit='celsius')
​```
"""

输出:

ChatCompletion(id='', choices=[Choice(finish_reason='function_call', index=0, logprobs=None, message=ChatCompletionMessage(content="get_current_weather\n ```python\ntool_call(location='Beijing', unit='celsius')\n```", role='assistant'# 模型识别到了function_call, function_call=FunctionCall(arguments='{"location": "Beijing", "unit": "celsius"}', name='get_current_weather'), tool_calls=None, name=None))], created=1721811783, model='chatglm3-6b', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=28, prompt_tokens=222, total_tokens=250))

2 ChatGLM3的低精度微调

具体微调可以参考官方源码finetune_demo,我们今天利用transformer库来了解下低精度微调的过程。

  • 我们知道,模型训练时候的显存占用主要如下:

    • 模型权重:4Bytes*模型参数量
    • 优化器状态(AdamW优化器):8Bytes*模型参数量
    • 梯度:4Bytes*模型参数量
    • 当然也受批次大小,序列长度等多个因素的影响。
  • 我们以ChatGLM3-6B为例,如果以FP32(4Bytes)进行加载,大概需要24GB的显存。

    • FP32也叫做 float32,两种叫法是完全一样的,全称是Single-precision floating-point(单精度浮点数)
    • 如下图所示,FP32是用32位二进制来表示的浮点数:
      • Sign(符号位): 1位,0表示整数,1表示负数
      • Exponent(指数位):8位,表示整数部分,偏置值是127
      • Fraction(尾数位):23位,表示小数部分,隐含了首位的1,实际的尾数精度为24位

在这里插入图片描述

  • 而利用transformers库加载时,默认是FP16(2Bytes)精度加载,大概需要13GB显存。我们发现,在参数量不变的情况下,降低模型中每个参数占用的字节数,能够降低模型的显存占用。
  • 常见的低精度数据类型有:float16(半精度)、bfloat16、int8、fp4、nf4等

2.1 半精度训练

2.1.1 FP16简介

  • FP16也叫做 float16,两种叫法是完全一样的,全称是Half-precision floating-point(半精度浮点数)

  • 如下图所示,是用16位二进制来表示的浮点数:

    • Sign(符号位): 1位,0表示整数;1表示负数。

    • Exponent(指数位):5位,简单地来说就是表示整数部分。

      • 范围为00001(1)到11110(30),正常来说整数范围就是 2 1 − 2 30 2^{1}-2^{30} 21230,注:当指数位都为00000和11111时,表示的是特殊情况。
      • 但为了指数位能够表示负数,引入了一个偏置值,偏置值是一个固定的数,它被加到实际的指数上,在二进制16位浮点数中,偏置值是15。
      • 这个偏置值确保了指数位可以表示从-14到+15的范围,即 2 − 14 − 2 15 2^{-14}-2^{15} 214215
    • Fraction(尾数位):10位,简单地来说就是表示小数部分。

      • 存储的尾数位数为10位,但其隐含了首位的1,因此实际的尾数精度为11位。

      • 这里的隐含位简单通俗来说,假设尾数部分为1001000000,为默认在其前面加一个1,最后变成1.1001000000然后换成10进制就是:

        1.1001000000 = 1 + 576(1001000000转换为10进制)/1024 = 1.5625
        

在这里插入图片描述

  • 因此,FP16所表示10进制数的计算公式为:

( − 1 ) s i g n × 2 e x p o n e n t − 15 × ( 1 + f r a c t i o n ( 转为 10 进制 ) 1024 ) (-1)^{sign}×2^{exponent-15}×(1+\frac{fraction(转为10进制)}{1024}) (1)sign×2exponent15×(1+1024fraction(转为10进制))

我们可以计算下FP16表示的范围:
在这里插入图片描述

我们用pytorch框架进行验证:

>>> import torch>>> torch.finfo(torch.float16)
finfo(resolution=0.001   # 在十进制上的分辨率,表示两个不同值之间的最小间隔。, min=-65504       # 最小值, max=65504        # 最大值, eps=0.000976562  # 表示在给定数据类型下,比1大的最小浮点数, smallest_normal=6.10352e-05  # 最小正规数,大于零的最小浮点数, tiny=6.10352e-05 # 最小非零数,大于零的最小浮点数, dtype=float16
)# 这里重点解释下resolution,这个是我们以十进制来说的两个数之间的最小间隔
# float16下,resolution为0.001,而3.141和3.1415间隔只有0.0005,所以在float16下结果是一样的# 把10进制数转化为 torch.float16
>>> num = 3.141
>>> num_fp16 = torch.tensor(num).half()
>>> print(num_fp16)
tensor(3.1406, dtype=torch.float16)>>> num = 3.1415
>>> num_fp16 = torch.tensor(num).half()
>>> print(num_fp16)
tensor(3.1406, dtype=torch.float16)
# 可以看到3.141和3.1415间隔只有0.0005,所以在float16下结果是一样的# 我们看下float32的信息,可以看到表示范围远远大于float16
>>> torch.finfo(torch.float32)
finfo(resolution=1e-06    # 在十进制上的分辨率,表示两个不同值之间的最小间隔。, min=-3.40282e+38  # 最小值, max=3.40282e+38   # 最大值, eps=1.19209e-07, smallest_normal=1.17549e-38, tiny=1.17549e-38 , dtype=float32
)
  • 如下图所示,使用半精度进行计算会导致的两个问题的处理:舍入误差(Rounding Error)和溢出错误(Grad Overflow / Underflow)。

    • 舍入误差:舍入误差指的是当梯度过小时,该次梯度更新可能会失败。
    >>> num1 =  2**(-3)
    >>> num2 =  2**(-14)>>> num_fp16_1 = torch.tensor(num1).half()
    >>> num_fp16_2 = torch.tensor(num2).half()>>> print(num_fp16_1)
    0.125
    >>> print(num_fp16_2)
    6.103515625e-05
    >>> print(num_fp16_1 + num_fp16_2)
    tensor(0.1250, dtype=torch.float16)
    
    • 溢出错误:

      • float16的有效的动态范围约为 ( 5.96 × 1 0 − 8 至 6.55 × 1 0 4 5.96×10^{-8}至6.55×10^4 5.96×1086.55×104)
        最大正值上面已经计算过: 0 _ 11110 _ 1111111111 = ( − 1 ) 0 × 2 30 − 15 × ( 1 + 1023 1024 ) = 65504 计算最小正值时,指数位全为 0 ,也就是所谓的非规格数,此时指数位置固定为 ( 1 − 偏置项 ) 另外小数位的隐藏也不再是 1 ,而是 0 ,因此最小正值为: 0 _ 00000 _ 0000000001 = ( − 1 ) 0 × 2 1 − 15 × ( 0 + 1 1024 ) = 2 − 24 = 5.96 × 1 0 − 8 最大正值上面已经计算过:\\ 0\_11110\_1111111111=(-1)^0×2^{30-15}×(1+\frac{1023}{1024})=65504\\ 计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1-偏置项)\\ 另外小数位的隐藏也不再是1,而是0,因此最小正值为:\\ 0\_00000\_0000000001=(-1)^0×2^{1-15}×(0+\frac{1}{1024})=2^{-24}=5.96×10^{-8} 最大正值上面已经计算过:0_11110_1111111111=(1)0×23015×(1+10241023)=65504计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1偏置项)另外小数位的隐藏也不再是1,而是0,因此最小正值为:0_00000_0000000001=(1)0×2115×(0+10241)=224=5.96×108

      • 比单精度的float32( 1.4 × 1 0 − 45 至 3.4 × 1 0 38 1.4×10^{-45}至3.4×10^{38} 1.4×10453.4×1038)要狭窄很多,注意:这里不是从最小值到最大值, 而是说的正数的部分;
        计算最小正值时,指数位全为 0 ,也就是所谓的非规格数,此时指数位置固定为 ( 1 − 偏置项 ) 另外小数位的隐藏也不再是 1 ,而是 0 ,因此最小正值为: ( − 1 ) 0 × 2 1 − 127 × ( 0 + 1 2 23 ) = 2 − 149 = 1.4 × 1 0 − 45 计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1-偏置项)\\ 另外小数位的隐藏也不再是1,而是0,因此最小正值为:\\ (-1)^0×2^{1-127}×(0+\frac{1}{2^{23}})=2^{-149}=1.4×10^{-45} 计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1偏置项)另外小数位的隐藏也不再是1,而是0,因此最小正值为:(1)0×21127×(0+2231)=2149=1.4×1045

      • 精度下降(小数点后16相比较小数点后8位要精确的多)会导致得到的值大于或者小于fp16的有效动态范围,也就是上溢出或者下溢出。

      • 因此,使用FP16会损失掉梯度更新小于 2 − 24 2^{-24} 224的值,有研究表明大约占网络所有梯度更新的5%。因此,现在大型模型在训练时候,一般会使用混合精度训练(Mixed Precision,而且更常用是BF16的数据类型。

在这里插入图片描述

  • BF16也叫做bfloat16(这是最常叫法),由Google Brain开发的。如下图所示,其指数位数与FP32相同,都是8位,因此表示的数据范围更广,但是精度比FP16要差。

  • BF16的指数域位数(8位)和float32一样多,能表示的大小范围类似,只是精度降低了(也就是相邻数之间的间隔略微变大,大多数情况下对神经网络的表现影响不显著),而float16的指数域位数只有5,可以表达的大数上限降低,接近0的小数下限升高,比BF16更容易发生上溢和下溢等数值问题,因此大模型的训练和推理更常用BF16。
    B F 16 最小正值为: ( − 1 ) 0 × 2 1 − 127 × ( 0 + 1 2 7 ) = 2 − 133 = 9.2 × 1 0 − 41 B F 16 最大正值范围和 F P 32 类似,可以看到 B F 16 有效动态范围比 F P 16 大很多 BF16最小正值为:\\ (-1)^0×2^{1-127}×(0+\frac{1}{2^{7}})=2^{-133}=9.2×10^{-41}\\ BF16最大正值范围和FP32类似,可以看到BF16有效动态范围比FP16大很多 BF16最小正值为:(1)0×21127×(0+271)=2133=9.2×1041BF16最大正值范围和FP32类似,可以看到BF16有效动态范围比FP16大很多

>>> import torch
>>> torch.finfo(torch.bfloat16)
# 结果
finfo(resolution=0.01    # 在十进制上的分辨率,表示两个不同值之间的最小间隔。, min=-3.38953e+38 # 最小值, max=3.38953e+38  # 最大值, eps=0.0078125, smallest_normal=1.17549e-38, tiny=1.17549e-38, dtype=bfloat16
)

在这里插入图片描述

  • 这里要注意一下,并不是所有的硬件都支持bfloat16,可以用下面代码验证是否支持bfloat16

    import transformerstransformers.utils.import_utils.is_torch_bf16_gpu_available()
    # 结果为True就是支持
    

2.1.2 使用半精度微调ChatGLM3-6B模型

开启半精度训练很简单:

# 推荐做法如下
# 在模型加载时,指定torch_dtype为torch.half或者torch.float16
# 如果,你的机器支持torch.bfloat16,推荐使用
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.bfloat16, device_map="cuda:0")

我们使用transformers库进行低精度微调:

  • 数据集:https://huggingface.co/datasets/shibing624/alpaca-zh

  • 微调模型:https://modelscope.cn/models/ZhipuAI/chatglm3-6b

  • 代码和之前PEFT微调代码相似,最主要的区别有两点:

    • 我们这次使用半精度进行微调,加载的模型是ChatGLM3-6B
    • 数据预处理逻辑和之前不同(ChatGLM模型新设计的prompt模板)
1、加载数据集
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer# 1、加载数据集
ds = Dataset.load_from_disk("./data/alpaca_data_zh/")
ds

输出:

Dataset({features: ['output', 'input', 'instruction'],num_rows: 26858
})
# 查看一些数据
ds[:3]

输出:

{'output': ['以下是保持健康的三个提示:\n\n1. 保持身体活动。每天做适当的身体运动,如散步、跑步或游泳,能促进心血管健康,增强肌肉力量,并有助于减少体重。\n\n2. 均衡饮食。每天食用新鲜的蔬菜、水果、全谷物和脂肪含量低的蛋白质食物,避免高糖、高脂肪和加工食品,以保持健康的饮食习惯。\n\n3. 睡眠充足。睡眠对人体健康至关重要,成年人每天应保证 7-8 小时的睡眠。良好的睡眠有助于减轻压力,促进身体恢复,并提高注意力和记忆力。','4/16等于1/4是因为我们可以约分分子分母都除以他们的最大公约数4,得到(4÷4)/ (16÷4)=1/4。分数的约分是用分子和分母除以相同的非零整数,来表示分数的一个相同的值,这因为分数实际上表示了分子除以分母,所以即使两个数同时除以同一个非零整数,分数的值也不会改变。所以4/16 和1/4是两种不同的书写形式,但它们的值相等。','朱利叶斯·凯撒,又称尤利乌斯·恺撒(Julius Caesar)是古罗马的政治家、军事家和作家。他于公元前44年3月15日被刺杀。 \n\n根据历史记载,当时罗马元老院里一些参议员联合起来策划了对恺撒的刺杀行动,因为他们担心恺撒的统治将给罗马共和制带来威胁。在公元前44年3月15日(又称“3月的艾达之日”),恺撒去参加元老院会议时,被一群参议员包围并被攻击致死。据记载,他身中23刀,其中一刀最终致命。'],'input': ['', '输入:4/16', ''],'instruction': ['保持健康的三个提示。', '解释为什么以下分数等同于1/4', '朱利叶斯·凯撒是如何死亡的?']}
# 2、加载tokenizer
model_path = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
2、获取ChatGLM3模型需要的数据格式

ChatGLM3模型需要的数据格式呢?

  • 我们看下model.chat(tokenizer, "你好", history=[])这行代码中的chat方法:
	# modeling_chatglm.py@torch.inference_mode()def chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, role: str = "user",max_length: int = 8192, num_beams=1, do_sample=True, top_p=0.8, temperature=0.8, logits_processor=None,**kwargs):if history is None:history = []if logits_processor is None:logits_processor = LogitsProcessorList()logits_processor.append(InvalidScoreLogitsProcessor())gen_kwargs = {"max_length": max_length, "num_beams": num_beams, "do_sample": do_sample, "top_p": top_p,"temperature": temperature, "logits_processor": logits_processor, **kwargs}# 1、调用tokenizer.build_chat_input方法对user的输入query进行处理inputs = tokenizer.build_chat_input(query, history=history, role=role)inputs = inputs.to(self.device)# 注意:这里是调用 tokenizer.get_command("<|user|>")获得token_ideos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"),tokenizer.get_command("<|observation|>")]# 2、generate方法outputs = self.generate(**inputs, **gen_kwargs, eos_token_id=eos_token_id)# 3、解码outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):-1]response = tokenizer.decode(outputs)history.append({"role": role, "content": query})# 后处理阶段response, history = self.process_response(response, history)return response, history

我们调用用build_chat_input方法:

>>> from transformers import AutoTokenizer, AutoModelForCausalLM>>> model_path = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'>>> tokenizer.build_chat_input("考试的技巧有哪些?", history=[], role="user")
{'input_ids': tensor([[64790, 64792, 64795, 30910, 13, 30910, 32227, 54530, 33741, 34953, 31514, 64796]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'position_ids': tensor([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]])
}# 我们对input_ids进行解码
>>> tokenizer.decode([64790, 64792, 64795, 30910, 13, 30910, 32227, 54530, 33741, 34953,31514, 64796])
'[gMASK]sop<|user|> \n 考试的技巧有哪些?<|assistant|>'# 需要注意的是,由于chatglm3支持工具调用等功能,因此我们还需关注下后处理
    # modeling_chatglm.py  后处理def process_response(self, output, history):content = ""history = deepcopy(history)for response in output.split("<|assistant|>"):# 注意:又利用"\n"进行了split# 前面为metadata,工具调用时使用,比如为:get_current_weather# 我们这里虽然没有metadata,但是需要手动拼接"\n",否则会把conent当作metadatametadata, content = response.split("\n", maxsplit=1)if not metadata.strip():# 没有metadata,只返回contentcontent = content.strip()history.append({"role": "assistant", "metadata": metadata, "content": content})content = content.replace("[[训练时间]]", "2023年")else:history.append({"role": "assistant", "metadata": metadata, "content": content})if history[0]["role"] == "system" and "tools" in history[0]:content = "\n".join(content.split("\n")[1:-1])def tool_call(**kwargs):return kwargsparameters = eval(content)content = {"name": metadata.strip(), "parameters": parameters}else:content = {"name": metadata.strip(), "content": content}return content, history
# 因此,我们得到最终的数据格式为:
# '[gMASK]sop<|user|> \n Prompt<|assistant|> \n Response eos_token'
  • 根据以上步骤,我们得到了ChatGLM3最终的数据格式,不过这样有些麻烦

  • 目前,一些框架支持多种模型的微调,那么不同的模型肯定数据预处理是不一样的,这些框架是如何处理的呢?

    • 这里介绍一个比较火的训练框架LLaMA-Factory:https://github.com/hiyouga/LLaMA-Factory

    • 通过LLaMA-Factory的WebUI,小白也可以快速的训练出自己需要的模型(不仅支持SFT,还支持PPO、DPO等方法)。

    • LLaMA-Factory中有一个template.py,提供了大量模型的数据预处理,我们找到chatglm3的模板如下。

    # LLaMA-Factory\src\llamafactory\data\template.py# 这里我们关注format_prefix + format_user + format_assistant
    # format_prefix=[gMASK]sop
    # format_user=<|user|>\n{{content}}<|assistant|>
    # format_assistant=\n{{content}}# 因此,我们可以得到模板为:
    # [gMASK]sop<|user|> \n Prompt<|assistant|> \n Response eos_token
    _register_template(name="chatglm3",format_user=StringFormatter(slots=[{"token": "<|user|>"}, "\n", "{{content}}", {"token": "<|assistant|>"}]),format_assistant=StringFormatter(slots=["\n", "{{content}}"]),format_system=StringFormatter(slots=[{"token": "<|system|>"}, "\n", "{{content}}"]),format_function=FunctionFormatter(slots=[], tool_format="glm4"),format_observation=StringFormatter(slots=[{"token": "<|observation|>"}, "\n", "{{content}}", {"token": "<|assistant|>"}]),format_tools=ToolFormatter(tool_format="glm4"),format_prefix=EmptyFormatter(slots=[{"token": "[gMASK]"}, {"token": "sop"}]),stop_words=["<|user|>", "<|observation|>"],efficient_eos=True,
    )# 假如我们现在需要微调qwen模型,我们可以找到qwen的模板
    # 这里我们关注format_system + format_user
    # format_system=<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n
    # format_user=<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n
    # 最终的模板为:
    """
    <|im_start|>system
    You are a helpful assistant.<|im_end|>
    <|im_start|>user
    Prompt<|im_end|>
    <|im_start|>assistant
    Response eos_token
    """
    _register_template(name="qwen",format_user=StringFormatter(slots=["<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),format_system=StringFormatter(slots=["<|im_start|>system\n{{content}}<|im_end|>\n"]),format_observation=StringFormatter(slots=["<|im_start|>tool\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),format_separator=EmptyFormatter(slots=["\n"]),default_system="You are a helpful assistant.",stop_words=["<|im_end|>"],replace_eos=True,
    )
    
3、数据预处理
def process_func(example):MAX_LENGTH = 384input_ids, attention_mask, labels = [], [], []instruction = "\n".join([example["instruction"], example["input"]]).strip()     # queryinstruction = tokenizer.build_chat_input(instruction, history=[], role="user")  # [gMASK]sop<|user|> \n query<|assistant|>response = tokenizer("\n" + example["output"], add_special_tokens=False)        # \n response, 缺少eos tokeninput_ids = instruction["input_ids"][0].numpy().tolist() + response["input_ids"] + [tokenizer.eos_token_id]attention_mask = instruction["attention_mask"][0].numpy().tolist() + response["attention_mask"] + [1]# 这里instruction部分设置为-100,不计算Losslabels = [-100] * len(instruction["input_ids"][0].numpy().tolist()) + response["input_ids"] + [tokenizer.eos_token_id]if len(input_ids) > MAX_LENGTH:input_ids = input_ids[:MAX_LENGTH]attention_mask = attention_mask[:MAX_LENGTH]labels = labels[:MAX_LENGTH]return {"input_ids": input_ids,"attention_mask": attention_mask,"labels": labels}tokenized_ds = ds.map(process_func, remove_columns=ds.column_names)
tokenized_ds

输出:

在这里插入图片描述

4、加载模型进行训练
import torch# 1、单精度进行加载(此时加载模型需要约24GB显存,因此官方默认是半精度加载)
# model = AutoModelForCausalLM.from_pretrained(model_path
#                                              , trust_remote_code=True
#                                              , torch_dtype=torch.float32
#                                              , device_map="cuda:0")# 2、半精度进行加载(此时加载模型需要13GB显存)
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.bfloat16, device_map="cuda:0")
# 利用Lora进行微调
from peft import LoraConfig, TaskType, get_peft_model, PeftModelconfig = LoraConfig(task_type=TaskType.CAUSAL_LM, target_modules=["query_key_value"])
model = get_peft_model(model, config)# 配置训练参数
args = TrainingArguments(output_dir="./chatbot-chatglm3",per_device_train_batch_size=2,gradient_accumulation_steps=16,logging_steps=10,num_train_epochs=1,learning_rate=1e-4,remove_unused_columns=False,save_strategy="epoch"
)trainer = Trainer(model=model,args=args,train_dataset=tokenized_ds.select(range(6000)),data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)trainer.train()# 模型预测
model.eval()
print(model.chat(tokenizer, "数学考试怎么考高分?", history=[])[0])
# 训练过程中,显存消耗如下
(llm) root@autodl-container-c4c240bdcc-422ab4ba:~# nvidia-smi    
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.78                 Driver Version: 550.78         CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3090        On  |   00000000:0E:00.0 Off |                  N/A |
| 49%   58C    P2            310W /  350W |   15518MiB /  24576MiB |     95%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------++-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+

2.2 8bit量化模型微调

2.2.1 为什么需要进行模型的量化

把Float类型 (FP32,FP16) 的模型参数和激活值,用整数 (Int8,Int4)来代替,同时尽可能减少量化后模型推理的误差。
模型量化带来的好处:

  • 减少模型的存储空间和显存的占用
  • 减少显存和TensorCore之间的数据传输量,从而加快模型推理时间。
  • 显卡对整数运算速度快于浮点型数据,从而加快模型推理时间。

2.2.2 如何进行量化和反量化

下图就是非对称量化和反量化的过程:

在这里插入图片描述

但是上面的量化存在下面的问题:

  • 不能处理离群值问题

    假设hidden state中有一个向量A:
    A=[-0.10, -0.23, 0.08, -0.38, -0.28, -0.29, -2.11, 0.34, -0.53, -67.0]。
    向量A有一个emergent feature -67.0。如果我们去掉emergent feature -67.0对向量A做量化和反量化,
    处理后的结果是:[-0.10, -0.23, 0.08, -0.38, -0.28, -0.28, -2.11, 0.33, -0.53]。
    出现的误差只有-0.29 -> -0.28。但是如果我们在保留emergent feature -67.0的情况下对该向量做量化和反量化,
    处理后的结果是:[ -0.00, -0.00, 0.00, -0.53, -0.53, -0.53, -2.11, 0.53, -0.53, -67.00]。大部分信息在处理后都丢失了。
    
  • 8位精度表示的动态范围有限,因此量化具有多个大值得向量会产生严重的误差

  • 误差在累计过程中会导致模型最终性能的大幅度下降

对此,可以采用混合精度分解量化(LLM.int8()):
在这里插入图片描述

  • 将包含了Emergent Features的几个维度从矩阵中分离出来,对其做高精度的矩阵乘法(按列提取离群值 ,即大于某个阈值的值),其余部分进行量化;

  • 对 FP16 离群值矩阵和INT8 非离群值矩阵分别作矩阵乘法。

  • 反量化非离群值的矩阵乘结果与离群值矩阵乘结果相加,获得最终的 FP16 结果。

在这里插入图片描述

2.2.3 8bit量化模型微调

# 3、量化为int8
# 通过transformers库和bitsandbytes库,如下所示很容易进行8bit的量化
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.half, device_map="cuda:0", load_in_8bit=True )
  • 此时显存消耗情况如下,可以看到显存降低了不少
# 训练过程中,显存消耗如下,可以看到显存降低了不少
(llm) root@autodl-container-c4c240bdcc-422ab4ba:~# nvidia-smi        
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.78                 Driver Version: 550.78         CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3090        On  |   00000000:0E:00.0 Off |                  N/A |
| 38%   53C    P2            271W /  350W |   11308MiB /  24576MiB |     71%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------++-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+

2.3 4bit量化模型微调(QLora)

QLoRA的解决方案主要包括三个部分:

(1)NF4 Quantization(4-bit量化):经NF4量化的权重信息损失较小,从而保证模型整体精度的最小损失。

(2)Double Quantization(双重量化):对初次完成量化的常量进行二次量化,进一步缩减模型存储体积。

(3)Paged Optimizers(分页优化器):利用NVIDIA的统一内存管理功能,该技术可以在CPU和GPU之间自动进行页对页的传输,使得即便在GPU偶发地内存溢出(OOM)时仍能够继续进行训练。

2.3.1 NF4量化

  • int8量化是一种常见的线性量化过程,其计算公式是线性的: q = ( w / s c a l e ) + z e r o _ p o i n t q = (w / scale) + zero\_point q=(w/scale)+zero_point

    • 但这存在一个问题:若数据分布不均匀,量化后的值有可能“粘连”堆叠在一起

    • 例如, [ 0.001 , 0.0015 , 0.0016 , 0.002 , 55.0 ] [0.001, 0.0015, 0.0016, 0.002, 55.0] [0.001,0.0015,0.0016,0.002,55.0]在经量化处理后,变为: [ − 128 , − 128 , − 128 , − 128 , 127 ] [-128,-128,-128,-128,127] [128,128,128,128,127]

    • 这四个原本不同的权重经量化后全数转化为相同的数值,导致模型出现较大误差。

  • 一般的模型参数通常呈正态分布,而非均匀分布。

    • 若依照线性方式进行量化,极可能导致多个不同的值被量化到相同的数值上。
    • 如参数符合标准正态分布,(0,1)区间内的值差异性将远大于(10,11),造成相同值概率的不均衡。
  • nf4量化则采取一种非对称量化方式,它基于分位数来执行量化映射。在标准正态分布里,由于靠近中心0点的取值较多,非对称量化能为这些取值提供更多的“格子”,以维持数据的精细度

    • 以4bit为例,表示范围为16个值,将权重从小到大排序,找到十五个分位数,将其切分为十六块,权重数值落在第几块,量化的表示就是多少,范围[0-15];
    • 此外,由于涉及到反量化,还需要给这16个值一个浮点数的映射,这个映射可以取分位区间两侧分位点的均值,用于反量化,这部分称之为量化值;
    • 具体操作时,我们只需要把待量化的数跟量化值进行比较,取最相近的值即可作为该数值的量化值,对应的表示可以通过分位数进行确定,存储时同时存储4bit的表示与对应量化值,反量化后进行计算
  • 大多数权重整体呈现正态分布,那么可以将其统一缩放至[-1,1],根据标准正态分布得到16个量化值,并将量化值也缩放至[-1,1],此时,便可利用前面提到的方法,将权重进行量化

  • 为了减少异常值的问题,采用分块量化,块大小为64,即64个值为一组进行量化

在这里插入图片描述

2.3.2 双重量化

  • 在QLoRA框架中,采用64个参数构成一个block进行量化,即block_size=64,每个块计算出一个对称量化中用到的Scale值。

  • 如果以32位浮点数存储Scale,那么每个block将会额外存储一个32位数字,这意味着每个参数实际上需要额外的 32 / 64 = 0.5 b i t 32/64=0.5bit 32/64=0.5bit存储空间。因此,每个参数实际占用的存储空间变成了 4 + 0.5 = 4.5 b i t s 4+0.5=4.5bits 4+0.5=4.5bits

为了优化这一存储需求,研究人员提出了Double Quant策略,即对Scale本身再进行一次量化;不过这里使用的是线性量化方法,量化后的格式为FP8,其中block_size=256。

  • Double Quant 后,每个参数做量化只需要额外的 8/64 + 32 / (64*256) = 0.127 bits 显存。

  • Double Quant策略通过对量化系数Scale再次进行量化,有效地降低了每个参数所需的额外存储开销。在这一策略的应用之下,每个参数的总量化开销降至大约0.127 bits的额外显存,极大程度上节约了资源。

2.3.3 4bit量化模型微调

# 4、量化为int4
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.half, device_map="cuda:0", load_in_4bit=True                        , bnb_4bit_compute_dtype=torch.half  , bnb_4bit_quant_type="nf4"         , bnb_4bit_use_double_quant=True    
)
"""
参数解释:
load_in_4bit:替换Linear层为FP4/NF4层,启用4位量化
bnb_4bit_compute_dtype:设置计算类型,它可能与输入时的类型不同。例如,输入可能是fp32,但计算可以设置为bf16以获得速度提升。
bnb_4bit_use_double_quant:是否开启double_quant(双重量化)
bnb_4bit_quant_type:有两个参数可以选fp4和nf4,默认是fp4
"""
  • 训练过程中,显存消耗如下,现在显存降到了一块2080Ti就能对其进行微调了
# 训练过程中,显存消耗如下,现在显存降到了一块2080Ti就能对其进行微调了
(llm) root@autodl-container-c4c240bdcc-422ab4ba:~# nvidia-smi    
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.78                 Driver Version: 550.78         CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3090        On  |   00000000:0E:00.0 Off |                  N/A |
| 48%   58C    P2            305W /  350W |    7260MiB /  24576MiB |    100%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------++-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

deepseek杀疯了,偷摸开源全球一梯队大模型——DeepSeek-V2-Chat-0628

就在今年6月&#xff0c;深度求索团队发布了DeepSeek-V2模型后不久&#xff0c;新版本DeepSeek-V2-Chat-0628 模型也在7月开源了。其推理能力有了极大提升。尤其在数学解题、逻辑推理、编程、指令跟随、Json格式输出不同维度上&#xff0c;最高有16%的性能提升。 在Arena-Hard…

推荐一款前端滑动验证码插件(Vue、uniapp)

uniapp版本&#xff1a;滑块拼图验证码&#xff0c;有后端&#xff0c;简单几步即可实现&#xff0c;小程序、h5都可以用 - DCloud 插件市场 Vue版本及cdn版本可以查阅文档&#xff1a; 行为验证 | Poster 文档 示例代码&#xff1a; <template><view id"app&…

YesPlayMusic本地服务器部署并实现远程在线访问听歌

文章目录 前言1. 安装Docker2. 本地安装部署YesPlayMusic3. 安装cpolar内网穿透4. 固定YesPlayMusic公网地址 前言 本文主要介绍如何在本地快速搭建YesPlayMusic云音乐播放器&#xff0c;并且结合cpolar内网穿透工具实现随时随地远程访问局域网内的音乐播放器听歌。 YesPlayM…

保研408真题练习:2009年全国硕士研究生入学统一考试(单选篇2)

&#x1f9ca;&#x1f9ca;&#x1f9ca;单项选择题&#xff08;共40道&#xff09; &#x1f9ca;操作系统&#xff08;8道&#xff09; &#x1f965;1.进程调度算法 高响应比优先调度&#xff1a;选出响应比最高的进程投入执行&#xff0c;响应比R(等待时间&#xff0b;执…

排序算法:归并排序,golang实现

目录 前言 归并排序 代码示例 1. 算法包 2. 归并排序代码 3. 模拟程序 4. 运行程序 5. 从大到小排序 归并排序主要操作 1. 合并 2. 分割&#xff08;Divide&#xff09;与递归排序&#xff08;Conquer&#xff09; 总体思想 循环次数测试 假如 10 条数据进行排序…

10、billu-b0x2

难度 中 目标 root权限 首先确定靶机ip地址 netdiscover -i eth0 -r 192.168.189.0/24 kali 192.168.189.58 靶机 192.168.189.184 信息收集端口扫描 看到一个80和8080&#xff0c;先重点摸一下网站的内容 然后看到信息里有个robots.txt 首先就去访问一下 看到有许多不允许…

【C语言】数组和函数实践:扫雷游戏

扫雷游戏 1. 扫雷游戏分析和设计1.1 扫雷游戏的功能说明1.2 游戏的分析和设计1.2.1 数据结构的分析1.2.2 ⽂件结构设计 2. 扫雷游戏的代码实现&#xff08;1&#xff09;菜单menu函数&#xff08;2&#xff09;设计main函数&#xff08;3&#xff09;设计game函数&#xff08;4…

华为od机试真题:求幸存数之和(Python)

2024华为OD机试&#xff08;C卷D卷&#xff09;最新题库【超值优惠】Java/Python/C合集 题目描述 给一个正整数列nums&#xff0c;一个跳数jump&#xff0c;及幸存数量left。运算过程为:从索引为0的位置开始向后跳&#xff0c;中间跳过 J 个数字&#xff0c;命中索引为 J1的数…

腾讯云短信服务的开通流程

目录 一、开通服务二、创建secretId和secretKey三、创建应用四、创建实名资质五、创建签名六、创建正文模板一、开通服务 从控制台进入短信模块,点击【开始接入】开通服务: 认证主体首次开通短信服务可获赠国内短信,免费试用: 二、创建secretId和secretKey 创建链接:…

创意无限:11个设计圈热议的UI设计灵感网站集锦

无论你是一个经验丰富的UI设计师还是一个新的UI设计师&#xff0c;拥有一些高质量、可靠的UI设计网站灵感库都能加速你的设计过程。借助灵感资源&#xff0c;您可以更快、更有效地启动该项目。与此同时&#xff0c;优秀的UI设计网站也能帮助您探索新的设计解决方案&#xff0c;…

个性化你的生产力工具:待办事项App定制指南

国内外主流的10款待办事项软件对比&#xff1a;PingCode、Worktile、滴答清单、番茄ToDo、Teambition、Todoist、Microsoft To Do、TickTick、Any.do、Trello。 在寻找合适的待办事项软件时&#xff0c;你是否感到选择众多、难以决断&#xff1f;一个好的待办事项工具可以大大提…

【C++BFS】802. 找到最终的安全状态

本文涉及知识点 CBFS算法 LeetCode802. 找到最终的安全状态 有一个有 n 个节点的有向图&#xff0c;节点按 0 到 n - 1 编号。图由一个 索引从 0 开始 的 2D 整数数组 graph表示&#xff0c; graph[i]是与节点 i 相邻的节点的整数数组&#xff0c;这意味着从节点 i 到 graph…

专硕复试线298/295!哈尔滨理工大学计算机考研考情分析!

哈尔滨理工大学&#xff08;Harbin University of Science and Technology&#xff09;&#xff0c;位于哈尔滨市&#xff0c;是黑龙江省人民政府与国家国防科技工业局共建高校&#xff0c;入选“中西部基础能力建设工程”高校、国家“特色重点学科项目”建设高校、教育部“卓越…

MCU单片机GPIO初始化该按什么顺序配置?为什么初始化时有电平跳变?

GPIO初始化时有时钟配置、模式配置、输出配置、复用配置&#xff0c;那么在编写初始化代码时&#xff0c;到底该按什么顺序执行呢&#xff1f;如果顺序不当那初始化过程可能会出现短暂的电平跳变。 第一步&#xff0c;初始化MCU外设时&#xff0c;一般都需要先打开对应寄存器的…

【Qwen-Audio部署实战】Qwen-Audio-Chat模型之对话机器人部署测试

系列篇章&#x1f4a5; No.文章1【Qwen部署实战】探索Qwen-7B-Chat&#xff1a;阿里云大型语言模型的对话实践2【Qwen2部署实战】Qwen2初体验&#xff1a;用Transformers打造智能聊天机器人3【Qwen2部署实战】探索Qwen2-7B&#xff1a;通过FastApi框架实现API的部署与调用4【Q…

【Hot100】LeetCode—169. 多数元素

目录 题目1- 思路2- 实现⭐169. 多数元素——题解思路 3- ACM 实现 题目 原题连接&#xff1a;169. 多数元素 1- 思路 定义两个变量 一个是 count&#xff1a;维护当前元素的出现次数一个是 ret &#xff1a;维护当前元素 思路 遍历整个数组**①如果 count 0 **&#xff…

【TS】TypeScript中的接口(Interface):对象类型的强大工具

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 TypeScript中的接口(Interface):对象类型的强大工具引言1. 接口的基本概念1.1 什…

APP抓包之 Burpsuite+MuMu模拟器12抓包

写在前面 高版本的安卓不能直接安装证书了&#xff0c;比较麻烦。步骤如下。 前置工作 安装adb https://blog.csdn.net/x2584179909/article/details/108319973 安装openssl https://blog.csdn.net/zyhse/article/details/108186278 adb配置环境变量&#xff0c;openssl下载…

如何用Python删除电脑中的重复文件?

在生活中&#xff0c;我们经常会遇到电脑中文件重复的情况。 在文件较少的情况下&#xff0c;这类情况还比较容易处理&#xff0c;最不济就是一个个手动对比删除&#xff1b; 而在重复文件很多的时候&#xff0c;我们很难保证把重复文件全部删完。 这里给大家带来了一个便捷…

【C++的剃刀】我不允许你还不会AVL树

​ 学习编程就得循环渐进&#xff0c;扎实基础&#xff0c;勿在浮沙筑高台 循环渐进Forward-CSDN博客 Hello,这里是kiki&#xff0c;今天继续更新C部分&#xff0c;我们继续来扩充我们的知识面&#xff0c;我希望能努力把抽象繁多的知识讲的生动又通俗易懂&#xff0c;今天要…