大模型的实践应用17-利用QLoRA技术来微调ChatGLM2时所遇到的一些问题与解决方案。

本文介绍了利用QLoRA技术微调ChatGLM2时遇到的导入包和数据准备等问题,以及QLoRA技术的原理与应用。QLoRA是一种针对大型语言模型的低秩和量化微调方法,能有效减少计算资源和存储空间,保持模型性能。通过构造数据、定义模型和训练,展示了QLoRA在ChatGLM2上的应用实例。
摘要由CSDN通过智能技术生成

大家好,我是微学AI,今天给大家介绍一下大模型的应用17-利用QLoRA技术来微调ChatGLM2时所遇到的一些问题与解决方案。
QLoRA技术是一种用于微调大型语言模型的技术,其全称为Quantized Low-Rank Adaptation。该技术通过将模型参数分解为低秩矩阵,并将这些矩阵量化为低比特数,从而减少了微调所需的计算资源和存储空间。QLoRA技术可以将微调过程中的计算量降低几个数量级,同时保持了模型的效果。
本文将介绍利用QLoRA技术来微调ChatGLM2,这里提供一条样本构成的数据集来微调ChatGLM2,并演示讲解数据的整个处理过程。在这个过程中,我们会遇到模型包的导入问题,和模型加载配置问题,给大家介绍一下这些函数的应用。利用QLoRA技术来微调ChatGLM2,可以使得ChatGLM2更加灵活和高效。微调后的ChatGLM2可以在各种任务中表现出更好的性能,例如文本分类、机器翻译、问答等。此外,QLoRA技术还可以使得ChatGLM2能够更好地适应不同的领域和数据集,从而提高其在实际应用中的效果和泛化能力。

在这里插入图片描述

一、导入包的问题

这里如果使用最新的transformers,则不支持bitsandbytes0.39.1的版本,所以我采用了transformers4.37.1版本。
首先安装包的下载,可以用以下的命令

!pip install transformers==4.37.1 -i https://pypi.tuna.tsinghua.edu.cn/simple/
#finetune需要
!pip install -q 'bitsandbytes==0.39.1' #提供4bit量化支持,版本限制非常重要,否则可能报错
!pip install -q datasets
!pip install -q git+https://github.com/huggingface/accelerate
!pip install  -q git+https://github.com/huggingface/peft  #使用最新版本非常重要,否则可能报错
!pip install  -q git+https://github.com/lyhue1991/torchkeras 

导入常用模块

# 导入常用模块
import numpy as np
import pandas as pd 
import torch
from torch import nn 
from torch.utils.data import Dataset,DataLoader 

预训练模型的加载

from transformers import AutoTokenizer,AutoConfig, AutoModel, BitsAndBytesConfig

#为了能够在kaggle中使用,需要设置 bnb_config
model_name_or_path = 'THUDM/chatglm2-6b' 
bnb_config=BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_compute_dtype=torch.float16,
            bnb_4bit_use_double_quant=True, #QLoRA 设计的 Double Quantization
            bnb_4bit_quant_type="nf4", #QLoRA 设计的 Normal Float 4 量化数据类型
            llm_int8_threshold=6.0,
            llm_int8_has_fp16_weight=False,
        )
tokenizer = AutoTokenizer.from_pretrained(
    model_name_or_path, trust_remote_code=True) # cache_dir='./' 缓存到当前工作路径

model = AutoModel.from_pretrained(model_name_or_path,
                quantization_config=bnb_config,
                trust_remote_code=True)  # cache_dir='./'

运行模型推理

这里问一下他没见过的问题

#stream_chat 流聊天接口(打字机风格)
text = '你听说过微学AI吗?'
result = model.stream_chat(tokenizer,query=text,history=[])
for response,history in result:
    print(response,end='\r')

回答结果:

微学AI(Microlearning AI)是一种机器学习技术,旨在通过小规模、低成本的数据集来训练模型,以实现与传统机器学习方法相当的效果。相对于传统机器学习方法,微学AI具有更低的计算成本和更小的数据需求,因此可以在各种应用中发挥作用,如嵌入式设备、物联网、边缘计算等。

二、准备数据

构造数据

import pandas as pd
#定义一条知识样本~

keyword = '微学AI'

description = '''微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,主攻深度学习实战案例、机器学习实战案例、Python数据挖掘实战项目,研究方向包括:深度学习应用技巧,pytorch搭建模型,机器学习经典模型,自然语言处理,知识图谱,类ChatGPT大语言模型,智能OCR,目标检测等,项目主要运用于医疗健康、政府、教育、金融、生物学、物理学、企业管理等领域。'''

#对prompt使用一些简单的数据增强的方法,以便更好地收敛。
def get_prompt_list(keyword):
    return [f'你好啊{keyword}', 
            f'你知道{keyword}吗?',
            f'{keyword}是什么?',
            f'介绍一下{keyword}',
            f'你听过{keyword}吗?',
            f'你认识{keyword}吗?',
            f'{keyword}是谁?',
            f'我想知道{keyword},告诉我?',
           ]

data =[{'prompt':x,'response':description} for x in get_prompt_list(keyword) ]
dfdata = pd.DataFrame(data)
display(dfdata) 

prompt response
0 你好啊微学AI 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
1 你知道微学AI吗? 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
2 微学AI是什么? 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
3 介绍一下微学AI 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
4 你听过微学AI吗? 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
5 你认识微学AI吗? 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
6 微学AI是谁? 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…
7 我想知道微学AI,告诉我? 微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,…

from torch.utils.data import Dataset,DataLoader
class MyDataset(Dataset):
    def __init__(self,df,tokenizer,
                 prompt_col = 'prompt',
                 response_col = 'response',
                 history_col = 'history',
                 max_context_length = 1024,
                 max_target_length = 1024
                ):
        super().__init__()
        self.__dict__.update(locals())
        
    def __len__(self):
        return len(self.df)

    
    def get(self,index):
        data = dict(self.df.iloc[index])
        example = {}
        #context根据prompt和history以及
        example['context'] = self.tokenizer.build_prompt(
            query = data[self.prompt_col],
            history = data.get(self.history_col,None))
        example['target'] = data[self.response_col]
        return example 
    
    def __getitem__(self,index):
        example = self.get(index)
#       print(example['context'])
#       print(example['target'])
        a_ids = self.tokenizer.encode(text=example['context'], 
                add_special_tokens=True, truncation=True,
                max_length=self.max_context_length)
      
        b_ids = self.tokenizer.encode(text=example['target'], 
                                      add_special_tokens=False, truncation=True,
                                     max_length=self.max_target_length)
        input_ids = a_ids + b_ids + [tokenizer.eos_token_id]

        labels = [-100]*len(a_ids)+b_ids+[tokenizer.eos_token_id]
        
        
        return {'input_ids':input_ids,'labels':labels}

ds_train = ds_val = MyDataset(dfdata,tokenizer)
print(ds_train[0]) 

结果:
{‘input_ids’: [64790, 64792, 790, 30951, 517, 30910, 30939, 30996, 13, 13, 54761, 31211, 39701, 55674, 55072, 54545, 23833, 13, 13, 55437, 31211, 30910, 55072, 54545, 23833, 36853, 31863, 50559, 31123, 34226, 34797, 32970, 32569, 54631, 31123, 39346, 33207, 33070, 32121, 31123, 32104, 30978, 54794, 23833, 31930, 47544, 31123, 54590, 55521, 33442, 31658, 38729, 33301, 31201, 33290, 31658, 38729, 33301, 31201, 24035, 8164, 31786, 34948, 38729, 31671, 31123, 46841, 31779, 31211, 33442, 31658, 32002, 33741, 31123, 30925, 4226, 64569, 35132, 34030, 31123, 33290, 31658, 32698, 34030, 31123, 31799, 32330, 31940, 31123, 31848, 54959, 56296, 31123, 54931, 30942, 1960, 30964, 8705, 54539, 32330, 34030, 31123, 32093, 4678, 30951, 31123, 31919, 32348, 54609, 31123, 31671, 31703, 54797, 32419, 32171, 31740, 31201, 31683, 31201, 31634, 31201, 32021, 31201, 40505, 31201, 44651, 31201, 46817, 38444, 31155, 2], ‘labels’: [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 30910, 55072, 54545, 23833, 36853, 31863, 50559, 31123, 34226, 34797, 32970, 32569, 54631, 31123, 39346, 33207, 33070, 32121, 31123, 32104, 30978, 54794, 23833, 31930, 47544, 31123, 54590, 55521, 33442, 31658, 38729, 33301, 31201, 33290, 31658, 38729, 33301, 31201, 24035, 8164, 31786, 34948, 38729, 31671, 31123, 46841, 31779, 31211, 33442, 31658, 32002, 33741, 31123, 30925, 4226, 64569, 35132, 34030, 31123, 33290, 31658, 32698, 34030, 31123, 31799, 32330, 31940, 31123, 31848, 54959, 56296, 31123, 54931, 30942, 1960, 30964, 8705, 54539, 32330, 34030, 31123, 32093, 4678, 30951, 31123, 31919, 32348, 54609, 31123, 31671, 31703, 54797, 32419, 32171, 31740, 31201, 31683, 31201, 31634, 31201, 32021, 31201, 40505, 31201, 44651, 31201, 46817, 38444, 31155, 2]}

构建管道

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

dl_train = DataLoader(ds_train,batch_size = 4,
                      num_workers = 2, shuffle = True, collate_fn = data_collator 
                     )
dl_val = DataLoader(ds_val,batch_size = 4,
                      num_workers = 2, shuffle = False, collate_fn = data_collator 
                     )
for batch in dl_train:
    break
print(batch)

三、定义模型

QLoRA技术微调模型技术

QLoRA(Quantized LoRA) 是 LoRA(Low-Rank Adaptation)的一种变体,旨在通过量化的方式减少模型大小和提高计算效率,同时保持或提升微调的性能。QLoRA 主要改进了 LoRA 在处理大型模型时的效率问题,尤其是在硬件资源有限的情况下。
LoRA 本身是一种参数高效的微调方法,它通过引入低秩矩阵来捕捉基础模型参数的微小变化,从而只需要微调这些低秩矩阵,而不是整个模型的参数。这样做可以大大减少需要更新的参数数量,从而加快训练速度并减少内存需求。

QLoRA 针对 LoRA 的改进

  1. 量化:QLoRA 对低秩矩阵进行量化,提出了一种叫做NF4(4-bit NormalFloat)的量化数据类型。这意味着它将矩阵中的值限制在一个有限的集合中。量化可以显著减少模型的内存占用,因为它减少了表示每个参数所需的位数。例如,可以使用4位或8位整数来表示浮点数,从而减少内存使用。
  2. 性能提升:尽管量化通常会带来一定的性能损失,但 QLoRA 通过精细化的设计和优化,尽量减少了这种损失。在某些情况下,QLoRA 甚至能够提供与全精度 LoRA 相似的微调性能。
  3. 计算效率:量化后的参数需要更少的计算资源来处理,这意味着 QLoRA 可以在更便宜的硬件上运行,或者在同一硬件上提供更快的计算速度。
  4. 能源效率:减少模型大小和计算需求通常可以转化为更低的能源消耗,这对于大规模部署和环境保护都是有益的。
    QLoRA 通过结合量化和低秩适配器技术,为微调大型模型提供了一种高效的解决方案。它特别适合于那些希望在不牺牲性能的情况下减少计算和存储成本的场景。然而,QLoRA 的具体实现和优化可能需要根据不同的模型和应用场景进行调整。

QLoRA 的应用代码实例

from peft import get_peft_config, get_peft_model, TaskType

model.supports_gradient_checkpointing = True  #
model.gradient_checkpointing_enable()
model.enable_input_require_grads()

model.config.use_cache = False  

import bitsandbytes as bnb 
def find_all_linear_names(model):
    """
    找出所有全连接层,为所有全连接添加低秩adapter
    """
    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:  # needed for 16-bit
        lora_module_names.remove('lm_head')
    return list(lora_module_names)

lora_modules = find_all_linear_names(model)

print(lora_modules)

from peft import LoraConfig

peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, inference_mode=False,
    r=8,
    lora_alpha=32, lora_dropout=0.1,
    target_modules= lora_modules 
)


peft_model = get_peft_model(model, peft_config)

peft_model.is_parallelizable = True
peft_model.model_parallel = True
peft_model.print_trainable_parameters()

# 注意到LoRA算法 B矩阵的初始权重为0,所以训练前peft_model的输出等价于预训练模型model的输出
for name,para in peft_model.named_parameters():
    if '.1.' in name:
        break 
    if 'lora' in name.lower():
        print(name+':')
        print('shape = ',list(para.shape),'\t','sum = ',para.sum().item())
        print('\n')

peft_model.train();
out = peft_model(**batch)

find_all_linear_names函数的目的是遍历一个给定的模型,找出所有的4位宽度的全连接层(bnb.nn.Linear4bit),并将这些层的名称存储在一个集合中。然后,它会从集合中移除名为’lm_head’的层,最后返回一个包含所有其他4位全连接层名称的列表。
函数的工作流程如下:
定义一个类cls,这里是bnb.nn.Linear4bit,它代表4位宽度的全连接层。
遍历模型中的所有模块,使用model.named_modules()。
检查每个模块是否是bnb.nn.Linear4bit类型的实例。
如果是,将模块的名称添加到一个集合lora_module_names中。名称是通过将模块的全路径分割成段,然后选择最后一个段(如果是顶层模块)或者最后一个段(如果是嵌套模块)。
检查集合中是否包含名为’lm_head’的模块,如果包含,将其从集合中移除。
最后,将集合转换为列表并返回。

QLoRA 的peft库介绍

以上代码我们使用了peft库,这是一个用于为大型语言模型添加适配器(adapters)的库,以便进行参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)。参数高效微调是一种技术,它允许在不修改基础模型的大部分参数的情况下对模型进行微调,从而减少了计算资源和训练时间的需求。
让我们逐步解释这段代码的作用:
LoraConfig类用于创建一个配置对象,该对象包含了适配器的各种参数。这些参数包括:
task_type: 任务类型,这里设置为TaskType.CAUSAL_LM,表示这是因果语言模型任务。
inference_mode: 是否处于推理模式,这里设置为False,表示不是推理模式。
r: 低秩适配器的内部维度大小。
lora_alpha: LoRA适配器的超参数,用于控制适配器矩阵的大小。
lora_dropout: dropout比率,用于在训练过程中提高模型的泛化能力。
target_modules: 一个列表,包含了之前找到的需要添加适配器的全连接层名称。
get_peft_model函数使用上述配置对象来创建一个适配器模型。这个函数通常需要传入基础模型和配置对象作为参数。
peft_model.is_parallelizable = True和peft_model.model_parallel = True这两行代码设置了模型并行化。模型并行化是一种技术,用于将模型的不同部分分布在多个处理器上,以便更高效地使用计算资源。
peft_model.print_trainable_parameters()函数用于打印模型中可训练的参数数量。这有助于了解适配器模型的结构和大小。

四、训练模型

训练模型的搭建

from torchkeras import KerasModel 
from accelerate import Accelerator 

class StepRunner:
    def __init__(self, net, loss_fn, accelerator=None, stage = "train", metrics_dict = None, 
                 optimizer = None, lr_scheduler = None
                 ):
        self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
        self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
        self.accelerator = accelerator if accelerator is not None else Accelerator() 
        if self.stage=='train':
            self.net.train() 
        else:
            self.net.eval()
    
    def __call__(self, batch):
        
        #loss
        with self.accelerator.autocast():
            loss = self.net(**batch).loss

        #backward()
        if self.optimizer is not None and self.stage=="train":
            self.accelerator.backward(loss)
            if self.accelerator.sync_gradients:
                self.accelerator.clip_grad_norm_(self.net.parameters(), 1.0)
            self.optimizer.step()
            if self.lr_scheduler is not None:
                self.lr_scheduler.step()
            self.optimizer.zero_grad()
            
        all_loss = self.accelerator.gather(loss).sum()
        
        #losses (or plain metrics that can be averaged)
        step_losses = {self.stage+"_loss":all_loss.item()}
        
        #metrics (stateful metrics)
        step_metrics = {}
        
        if self.stage=="train":
            if self.optimizer is not None:
                step_metrics['lr'] = self.optimizer.state_dict()['param_groups'][0]['lr']
            else:
                step_metrics['lr'] = 0.0
        return step_losses,step_metrics
    
KerasModel.StepRunner = StepRunner 


#仅仅保存lora可训练参数
def save_ckpt(self, ckpt_path='checkpoint', accelerator = None):
    unwrap_net = accelerator.unwrap_model(self.net)
    unwrap_net.save_pretrained(ckpt_path)
    
def load_ckpt(self, ckpt_path='checkpoint'):
    import os
    self.net.load_state_dict(
        torch.load(os.path.join(ckpt_path,'adapter_model.bin')),strict =False)
    self.from_scratch = False
    
KerasModel.save_ckpt = save_ckpt 
KerasModel.load_ckpt = load_ckpt 

# 此处设置is_paged=True,即使用Paged Optimizer,减少训练过程中Cuda OOM的风险。
optimizer = bnb.optim.adamw.AdamW(peft_model.parameters(),
                                  lr=5e-05,is_paged=True)  


keras_model = KerasModel(peft_model,loss_fn = None,
        optimizer=optimizer) 

ckpt_path = 'chatglm2_qlora'

dfhistory = keras_model.fit(train_data = dl_train,
                val_data = dl_val,
                epochs=30,
                patience=4,
                monitor='val_loss',
                mode='min',
                ckpt_path = ckpt_path,
                gradient_accumulation_steps = 2
               )

在这里插入图片描述

五、模型预测

QLoRA的NF4量化后的模型

# 导入常用模块
import numpy as np
import pandas as pd 
import torch
from torch import nn 
from torch.utils.data import Dataset,DataLoader 

from transformers import AutoTokenizer, AutoModel

bnb_config=BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_compute_dtype=torch.float16,
            bnb_4bit_use_double_quant=True, #QLoRA 设计的 Double Quantization
            bnb_4bit_quant_type="nf4", #QLoRA 设计的 Normal Float 4 量化数据类型
            llm_int8_threshold=6.0,
            llm_int8_has_fp16_weight=False,
        )
#为了能够在kaggle中使用,需要设置 bnb_config
model_name_or_path = "THUDM/chatglm2-6b"

tokenizer = AutoTokenizer.from_pretrained(
    model_name_or_path, trust_remote_code=True) # cache_dir='./' 缓存到当前工作路径

model = AutoModel.from_pretrained(model_name_or_path,quantization_config=bnb_config,
                trust_remote_code=True) # cache_dir='./' 

from peft import PeftModel
ckpt_path = 'chatglm2_qlora/'
peft_loaded = PeftModel.from_pretrained(model,ckpt_path)
print(peft_loaded)

lora模型添加后的结构

PeftModelForCausalLM(
  (base_model): LoraModel(
    (model): ChatGLMForConditionalGeneration(
      (transformer): ChatGLMModel(
        (embedding): Embedding(
          (word_embeddings): Embedding(65024, 4096)
        )
        (rotary_pos_emb): RotaryEmbedding()
        (encoder): GLMTransformer(
          (layers): ModuleList(
            (0-27): 28 x GLMBlock(
              (input_layernorm): RMSNorm()
              (self_attention): SelfAttention(
                (query_key_value): lora.Linear4bit(
                  (base_layer): Linear4bit(in_features=4096, out_features=4608, bias=True)
                  (lora_dropout): ModuleDict(
                    (default): Dropout(p=0.1, inplace=False)
                  )
                  (lora_A): ModuleDict(
                    (default): Linear(in_features=4096, out_features=8, bias=False)
                  )
                  (lora_B): ModuleDict(
                    (default): Linear(in_features=8, out_features=4608, bias=False)
                  )
                  (lora_embedding_A): ParameterDict()
                  (lora_embedding_B): ParameterDict()
                )
                (core_attention): CoreAttention(
                  (attention_dropout): Dropout(p=0.0, inplace=False)
                )
                (dense): lora.Linear4bit(
                  (base_layer): Linear4bit(in_features=4096, out_features=4096, bias=False)
                  (lora_dropout): ModuleDict(
                    (default): Dropout(p=0.1, inplace=False)
                  )
                  (lora_A): ModuleDict(
                    (default): Linear(in_features=4096, out_features=8, bias=False)
                  )
                  (lora_B): ModuleDict(
                    (default): Linear(in_features=8, out_features=4096, bias=False)
                  )
                  (lora_embedding_A): ParameterDict()
                  (lora_embedding_B): ParameterDict()
                )
              )
              (post_attention_layernorm): RMSNorm()
              (mlp): MLP(
                (dense_h_to_4h): lora.Linear4bit(
                  (base_layer): Linear4bit(in_features=4096, out_features=27392, bias=False)
                  (lora_dropout): ModuleDict(
                    (default): Dropout(p=0.1, inplace=False)
                  )
                  (lora_A): ModuleDict(
                    (default): Linear(in_features=4096, out_features=8, bias=False)
                  )
                  (lora_B): ModuleDict(
                    (default): Linear(in_features=8, out_features=27392, bias=False)
                  )
                  (lora_embedding_A): ParameterDict()
                  (lora_embedding_B): ParameterDict()
                )
                (dense_4h_to_h): lora.Linear4bit(
                  (base_layer): Linear4bit(in_features=13696, out_features=4096, bias=False)
                  (lora_dropout): ModuleDict(
                    (default): Dropout(p=0.1, inplace=False)
                  )
                  (lora_A): ModuleDict(
                    (default): Linear(in_features=13696, out_features=8, bias=False)
                  )
                  (lora_B): ModuleDict(
                    (default): Linear(in_features=8, out_features=4096, bias=False)
                  )
                  (lora_embedding_A): ParameterDict()
                  (lora_embedding_B): ParameterDict()
                )
              )
            )
          )
          (final_layernorm): RMSNorm()
        )
        (output_layer): Linear(in_features=4096, out_features=65024, bias=False)
      )
    )
  )
)

模型预期实例

#stream_chat 流聊天接口(打字机风格)
text = '为微学AI写一下简单的介绍'
response,history= peft_loaded.chat(tokenizer,query=text,history=[])
print(response)

结果如下:
微学AI是一位网络博主,他是人工智能高级研发者,名校硕士学历毕业,拥有6项AI领域发明专利,主攻深度学习实战案例、机器学习实战案例、Python数据挖掘实战项目,研究方向包括:深度学习应用技巧,pytorch搭建模型,机器学习经典模型,自然语言处理,知识图谱,类ChatGPT大语言模型,智能OCR,目标检测等,项目主要运用于医疗健康、政府、教育、金融、生物学、物理学、企业管理等领域。

我们后续还要其他数据类型的训练案例与方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微学AI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值