微调BaiChuan13B来做命名实体识别

传统上,一般把NLP的研究领域大致分为自然语言理解(NLU)和自然语言生成(NLG)两种。

NLU侧重于如何理解文本,包括文本分类、命名实体识别、指代消歧、句法分析、机器阅读理解等;

NLG则侧重于理解文本后如何生成自然文本,包括自动摘要、机器翻译、问答系统、对话机器人等。

但是以ChatGPT为代表的大模型出来后,这些传统的NLP的细分研究领域基本可以说都失去了独立研究的价值。

为什么呢?因为大模型可以用统一的范式通通将它们搞定,并且效果非常出众。

在之前的例子中,我们演示了使用QLoRA算法来对BaiChuan-13B实施微调以处理最简单的文本分类任务。

Baichuan-13B 保姆级微调范例

在外卖评论数据集上,微调后测试集acc由0.8925提升到0.9015约提升了1个百分点。

在本例中,我们使用几乎相同的流程和方法来微调BaiChuan-13B以更好地处理命名实体识别任务。

实验结果显示,在NER任务上经过微调,我们的f1-score取得了不可忽略的提升(0.4313—>0.8768)。

注:跑完本流程需要至少32G的CPU,需要约2个小时的训练时间。

公众号算法美食屋后台回复关键词:torchkeras,获取本文notebook源码和dfner_13k.pkl数据集下载链接~

在我们正式开始之前,请允许我用简短的话给没有NLP基础知识的小伙伴讲解一下什么是命名实体识别。

命名实体识别NER任务是NLP的一个常见基础任务,

它是Named Entity Recognization的简称。

简单地说,就是识别一个句子中的各种 名称实体,诸如:人名,地名,机构 等。

例如对于下面这句话:

小明对小红说:"你听说过安利吗?"

其命名实体可以抽取表示如下:

{"人名": ["小明","小红"], "组织": ["安利"]}

〇,预训练模型

我们需要从 https://huggingface.co/baichuan-inc/Baichuan-13B-Chat 下载baichuan-13b-chat的模型。

国内可能速度会比较慢,总共有25个G左右,网速不太好的话,大概可能需要两到三个小时。

如果网络不稳定,也可以手动从这个页面一个一个下载全部文件然后放置到 一个文件夹中例如 'baichuan-13b' 以便读取。

import warnings
warnings.filterwarnings('ignore')
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, AutoModel, BitsAndBytesConfig
from transformers.generation.utils import GenerationConfig
import torch.nn as nn#使用QLoRA引入的 NF4量化数据类型以节约显存
model_name_or_path ='../baichuan-13b' #远程 'baichuan-inc/Baichuan-13B-Chat'bnb_config=BitsAndBytesConfig(load_in_4bit=True,bnb_4bit_compute_dtype=torch.float16,bnb_4bit_use_double_quant=True,bnb_4bit_quant_type="nf4",llm_int8_threshold=6.0,llm_int8_has_fp16_weight=False,)tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True)model = AutoModelForCausalLM.from_pretrained(model_name_or_path,quantization_config=bnb_config,trust_remote_code=True) model.generation_config = GenerationConfig.from_pretrained(model_name_or_path)
from IPython.display import clear_output 
messages = []
messages.append({"role": "user","content": "世界上第二高的山峰是哪座?"})
response = model.chat(tokenizer,messages=messages,stream=True)
for res in response:print(res)clear_output(wait=True)

12d320eac78665857c561268c86bb5db.png

下面我们设计一个7-shot-prompt方法,测试一下BaiChuan13b的实体抽取能力。

prefix = '''命名实体识别:抽取文本中的 人名,地点,组织 这三类命名实体,并按照json格式返回结果。下面是一些范例:小明对小红说:"你听说过安利吗?" -> {"人名": ["小明","小红"], "组织": ["安利"]}
现在,每年有几十万中国人到美国访问,几千名中国留学生到美国就学。 -> {"地点": ["中国", "美国"]}
中国是联合国安理会常任理事国之一。 -> {"地点": ["中国"], "组织": ["联合国"]}请对下述文本进行实体抽取,返回json格式。'''def get_prompt(text):return prefix+text+' -> 'def get_message(prompt,response):return [{"role": "user", "content": f'{prompt} -> '},{"role": "assistant", "content": response}]
messages  = [{"role": "user", "content": get_prompt("一些摩洛哥球迷已按捺不住,在看台上欢呼雀跃")}]
response = model.chat(tokenizer, messages)
print(response)
{"地点":["摩洛哥"], "组织":[]}
messages = messages+[{"role": "assistant", "content": "{'地点': ['摩洛哥']}"}]
messages.extend(get_message("这次轮到北京国安队,不知会不会再步后尘?","{'组织': ['北京国安队']}"))
messages.extend(get_message("革命党人孙中山在澳门成立同盟会分会","{'人名': ['孙中山'], '地名': ['澳门'], '组织': ['同盟会']}"))
messages.extend(get_message("我曾在安徽芜湖市和上海浦东打工。","{'地点': ['安徽芜湖市', '上海浦东']}"))
display(messages)
def predict(text,temperature=0.01):model.generation_config.temperature=temperatureresponse = model.chat(tokenizer, messages = messages+[{'role':'user','content':f'{text} -> '}])return response
predict('杜甫是李白的粉丝。')
"{'人名': ['杜甫', '李白']}"

我们拿一个开源的中文NER数据集来测试一下未经微调,仅仅使用7-shot-prompt的预训练模型的效果。

from sklearn.model_selection import train_test_split
import pandas as pd df = pd.read_pickle('dfner_13k.pkl')
dfdata,dftest = train_test_split(df,test_size=300,random_state=42)
dftrain,dfval = train_test_split(dfdata,test_size=200,random_state=42)
preds = ['' for x in dftest['target']]
for i in tqdm(range(len(preds))):preds[i] = predict(dftest['text'].iloc[i])
def toset(s):try:dic = eval(str(s))res = []for k,v in dic.items():for x in v:if x:res.append((k,x))return set(res)except Exception as err:print(err)return set()
dftest['pred'] = [toset(x) for x in preds]
dftest['gt'] = [toset(x) for x in dftest['target']]
dftest['tp_cnt'] = [len(pred&gt) for pred,gt in zip(dftest['pred'],dftest['gt'])]
dftest['pred_cnt'] = [len(x) for x in dftest['pred']]
dftest['gt_cnt'] = [len(x) for x in dftest['gt']]precision = sum(dftest['tp_cnt'])/sum(dftest['pred_cnt'])
print('precision = '+str(precision))recall = sum(dftest['tp_cnt'])/sum(dftest['gt_cnt'])
print('recall = '+str(recall))f1 = 2*precision*recall/(precision+recall)
print('f1_score = '+str(f1))
precision = 0.4316109422492401
recall = 0.45151033386327505
f1_score = 0.44133644133644134

微调前 f1_score为 0.44.

一,准备数据

我们仿照百川模型的 model._build_chat_input 方法来进行token编码,同时把需要学习的内容添加label.

1,token编码

import torch #将messages编码成 token, 同时返回labels
#注意baichuan-13b通过插入tokenizer.user_token_id和tokenizer.assistant_token_id 来区分用户和机器人会话内容# reference@ model._build_chat_input?
def build_chat_input(messages, model=model,tokenizer=tokenizer, max_new_tokens: int=0):max_new_tokens = max_new_tokens or model.generation_config.max_new_tokensmax_input_tokens = model.config.model_max_length - max_new_tokensmax_input_tokens = max(model.config.model_max_length // 2, max_input_tokens)total_input, round_input, total_label, round_label = [], [], [], []for i, message in enumerate(messages[::-1]):content_tokens = tokenizer.encode(message['content'])if message['role'] == 'user':round_input = [model.generation_config.user_token_id] + content_tokens + round_inputround_label = [-100]+[-100 for _ in content_tokens]+ round_labelif total_input and len(total_input) + len(round_input) > max_input_tokens:breakelse:total_input = round_input + total_inputtotal_label = round_label + total_labelif len(total_input) >= max_input_tokens:breakelse:round_input = []round_label = []elif message['role'] == 'assistant':round_input = [model.generation_config.assistant_token_id] + content_tokens + [model.generation_config.eos_token_id] + round_inputif i==0: #仅对最后一轮的target进行学习round_label = [-100] + content_tokens + [model.generation_config.eos_token_id]+ round_labelelse:round_label = [-100] + [-100 for _ in content_tokens] + [-100]+ round_labelelse:raise ValueError(f"message role not supported yet: {message['role']}")total_input = total_input[-max_input_tokens:]  # truncate lefttotal_label = total_label[-max_input_tokens:]total_input.append(model.generation_config.assistant_token_id)total_label.append(-100)return total_input,total_label

2,做数据集

from torch.utils.data import Dataset,DataLoader 
from copy import deepcopy
class MyDataset(Dataset):def __init__(self,df,messages):self.df = df self.messages = messagesdef __len__(self):return len(self.df)def get_samples(self,index):samples = []d = dict(self.df.iloc[index])samples.append(d)return samplesdef get_messages(self,index):samples = self.get_samples(index)messages = deepcopy(self.messages)for i,d in enumerate(samples):messages.append({'role':'user','content':d['text']+' -> '})messages.append({'role':'assistant','content':str(d['target'])})return messagesdef __getitem__(self,index):messages = self.get_messages(index)input_ids, labels = build_chat_input(messages)return {'input_ids':input_ids,'labels':labels}def show_sample(self,index):samples = self.get_samples(index)print(samples)
ds_train = MyDataset(dftrain,messages)
ds_val = MyDataset(dfval,messages)

3,创建管道

def data_collator(examples: list):len_ids = [len(example["input_ids"]) for example in examples]longest = max(len_ids) #之后按照batch中最长的input_ids进行paddinginput_ids = []labels_list = []for length, example in sorted(zip(len_ids, examples), key=lambda x: -x[0]):ids = example["input_ids"]labs = example["labels"]ids = ids + [tokenizer.pad_token_id] * (longest - length)labs = labs + [-100] * (longest - length)input_ids.append(torch.LongTensor(ids))labels_list.append(torch.LongTensor(labs))input_ids = torch.stack(input_ids)labels = torch.stack(labels_list)return {"input_ids": input_ids,"labels": labels,}
import torch 
dl_train = torch.utils.data.DataLoader(ds_train,num_workers=2,batch_size=1,pin_memory=True,shuffle=True,collate_fn = data_collator)dl_val = torch.utils.data.DataLoader(ds_val,num_workers=2,batch_size=1,pin_memory=True,shuffle=False,collate_fn = data_collator)
for batch in dl_train:break
#试跑一个batch
out = model(**batch)
out.loss
#采样300个batch作为一个epoch,便于较快验证
dl_train.size = 300

二,定义模型

下面我们将使用QLoRA(实际上用的是量化的AdaLoRA)算法来微调Baichuan-13b模型。

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  # silence the warnings. Please re-enable for inference!
import bitsandbytes as bnb 
def find_all_linear_names(model):"""找出所有全连接层,为所有全连接添加adapter"""cls = bnb.nn.Linear4bitlora_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-bitlora_module_names.remove('lm_head')return list(lora_module_names)
from peft import prepare_model_for_kbit_training 
model = prepare_model_for_kbit_training(model)
lora_modules = find_all_linear_names(model)
print(lora_modules)
['down_proj', 'gate_proj', 'W_pack', 'o_proj', 'up_proj']
from peft import AdaLoraConfig
peft_config = AdaLoraConfig(task_type=TaskType.CAUSAL_LM, inference_mode=False,r=16,lora_alpha=16, lora_dropout=0.05,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()
trainable params: 41,843,040 || all params: 7,002,181,160 || trainable%: 0.5975715144165165
out = peft_model.forward(**batch)
out[0]

三,训练模型

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,stageself.optimizer,self.lr_scheduler = optimizer,lr_schedulerself.accelerator = accelerator if accelerator is not None else Accelerator() if self.stage=='train':self.net.train() else:self.net.eval()def __call__(self, batch):#losswith self.accelerator.autocast():loss = self.net.forward(**batch)[0]#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.0return step_losses,step_metricsKerasModel.StepRunner = StepRunner #仅仅保存QLora可训练参数
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 osself.net.load_state_dict(torch.load(os.path.join(ckpt_path,'adapter_model.bin')),strict =False)self.from_scratch = FalseKerasModel.save_ckpt = save_ckpt 
KerasModel.load_ckpt = load_ckpt
optimizer = bnb.optim.adamw.AdamW(peft_model.parameters(),lr=6e-05,is_paged=True)  #'paged_adamw'
keras_model = KerasModel(peft_model,loss_fn =None,optimizer=optimizer) 
ckpt_path = 'baichuan13b_ner'
# keras_model.load_ckpt(ckpt_path) #支持加载微调后的权重继续训练(断点续训)
keras_model.fit(train_data = dl_train,val_data = dl_val,epochs=100,patience=10,monitor='val_loss',mode='min',ckpt_path = ckpt_path)

dac6089e530a879d119dbd34a1e49cf6.png

四,保存模型

为减少GPU压力,此处可重启kernel释放显存

import warnings 
warnings.filterwarnings('ignore')
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, AutoModel, BitsAndBytesConfig
from transformers.generation.utils import GenerationConfig
import torch.nn as nn
model_name_or_path ='../baichuan-13b'
ckpt_path = 'baichuan13b_ner'
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path,trust_remote_code=True
)
model_old = AutoModelForCausalLM.from_pretrained(model_name_or_path,trust_remote_code=True,low_cpu_mem_usage=True,torch_dtype=torch.float16,device_map='auto'
)
from peft import PeftModel#可能需要5分钟左右
peft_model = PeftModel.from_pretrained(model_old, ckpt_path)
model_new = peft_model.merge_and_unload()
from transformers.generation.utils import GenerationConfig
model_new.generation_config = GenerationConfig.from_pretrained(model_name_or_path)
from IPython.display import clear_output
messages = []
messages.append({"role": "user","content": "世界上第二高的山峰是什么?"})
response = model_new.chat(tokenizer,messages=messages,stream=True)
for res in response:print(res)clear_output(wait=True)

乔戈里峰。世界第二高峰———乔戈里峰西方登山者称其为k2峰,海拔高度是8611米,位于喀喇昆仑山脉的中巴边境上.

save_path = 'baichuan-13b-ner'
tokenizer.save_pretrained(save_path)
model_new.save_pretrained(save_path)
!cp ../baichuan-13b/*.py  baichuan-13b-ner

五,使用模型

为减少GPU压力,此处可再次重启kernel释放显存。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, BitsAndBytesConfig
from transformers.generation.utils import GenerationConfig
import torch.nn as nnimport warnings
warnings.filterwarnings('ignore')model_name_or_path = 'baichuan-13b-ner'...
...

我们测试一下微调后的效果。

import pandas as pd 
import numpy as np 
import datasets 
from tqdm import tqdm from sklearn.model_selection import train_test_split
import pandas as pd df = pd.read_pickle('dfner_13k.pkl')
dfdata,dftest = train_test_split(df,test_size=300,random_state=42)
dftrain,dfval = train_test_split(dfdata,test_size=200,random_state=42)
...
...
...precision = sum(dftest['tp_cnt'])/sum(dftest['pred_cnt'])
print('precision = '+str(precision))recall = sum(dftest['tp_cnt'])/sum(dftest['gt_cnt'])
print('recall = '+str(recall))f1 = 2*precision*recall/(precision+recall)
print('f1_score = '+str(f1))
precision = 0.9139280125195618
recall = 0.8427128427128427
f1_score = 0.876876876876877

微调后的f1_score为0.8768,相比微调前的f1_score=0.44,取得了不可忽视的巨大提升。

公众号算法美食屋台回复关键词:torchkeras,获取本文notebook源码和更多有趣范例~

c9bdc6028fe37dde27ff90953b76ba62.png

9aed5f4cdf536299d46d604aee5930fd.png

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

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

相关文章

为业务场景打造技术矩阵,网易智企畅谈融合通信与 AI 商业化最佳实践

在 QCon 全球软件开发大会 2022上海站上,一场特殊的专题吸引了与会者的目光。 与其他以个人身份参与的出品人不同,《融合通信技术探索与 AI 技术商业化实战》这一专场的出品人是一个略显神秘的“技术俱乐部”——网易智企技术委员会。 设立 7 大分委会&a…

Qcon · 上海丨融合通信技术探索与 AI 技术商业化实战专场,看看网易智企都将分享哪些干货?...

沟通和交流是人与人产生信息交换的重要方式,从文字到图片,再到音视频,云通信已成为连接的“刚需”底层技术。伴随着物联网、VR/AR、5G 等新场景应用的出现,云通信的应用边界正在不断外延,也为各类厂商带来了新的挑战。…

来,亮点抢先看!网易智企机器之心即将联合发布 AI 白皮书

可信 AI、多模态等前沿 AI 技术离我们还有多远?风控系统布局中,如何构建快速识别能力?如何高效降低资源消耗?如何实现敏捷响应?如何获得具有场景泛化能力的 AI 算法模型?如何更好驾驭音频技术和视频处理技术…

网易云信 Crash 异常治理实践 | 智企技术委员会技术专题系列

前言 Crash(造成用户无法使用客户端所承载的服务)作为客户端稳定治理的最主要问题之一。云信作为国内业界领先的 RTC/IM PaaS 服务商,对于客户端 SDK(PaaS 服务商对外服务的主要载体)的 Crash 治理再重视也不为过。关于…

面向企业服务,网易智企的深耕与拓进

编辑:阿由 设计:紫菜 机会总是留给有准备的人,以及企业。 新冠病毒给商业带来的影响,比想象中更大。不过,得益于数字化转型,部分企业不仅表现出足够的业务韧性,甚至还迎来了一波新的发展高峰。 …

社交出海,网易智企与亚马逊云科技齐助力

互联网与移动应用的快速发展为游戏、社交、即时通讯等行业带来了新的体验,人们可以借助数字化的平台随时随地的进行娱乐活动并与人交流互动。但这一数字世界中也并非只有阳光,海量的信息传递难免掺杂着各类不良的信息,对于互联网业务的经营者…

网易智企:做正确的事,然后相信持续创新的复利效应

上世纪 70 年代,日本正式超越德国成为世界第二大经济体。彼时的企业们雄心勃勃,在电器生产、汽车、半导体等诸多行业都做到了全球领先水平。 但先发者未必总是正确。在取得领先优势后,许多企业陷入了狭隘的“创新怪圈”,追求极致…

【亚马逊运营】不得不防!恶意投诉层出不穷,跨境卖家该如何应对?

众所周知,侵权一直都是亚马逊不可触碰的红线,一旦被投诉成功,轻则listing下架、库存积压,重则店铺被关、资金冻结。 后果有多惨重,卖家懂,无良同行更懂,想出各种恶意投诉套路来“借刀杀人”。 …

亚马逊、eBay新品期没有出单怎么办?自养买家号的重要性和技巧

一:新品期没有出单怎么办? 1.刚开始,低bid 调整,20元预算,让位置可以靠后点,因为前期跟前面比,比不赢,不如去后面比。 2.价格不是由卖家单一决定,而是由市场决定的。 3.切记自嗨型…

跨境电商遭遇知识产权侵权如何成功申诉?

本博主原文链接:跨境电商遭遇知识产权侵权如何成功申诉? 对于从事海外电商行业的卖家来说,最让人恐惧的问题之一无疑就是知识产权侵权的问题。有时一天有好几个品牌向法院递交侵权诉状。虽然国内没有太强的知识产权保护意识,但是这…

亚马逊信用卡手机号关联被封了怎么申诉解封?

亚马逊信用卡手机号关联被封了怎么申诉解封? 1.若是跟违规封号的账户关联,那基本是回天乏术,救不回来 2.若是新旧账户关联,那么可以选择关闭掉其中一个账户来寻求解封 3.如果卖家和买家账户形成关联,那么申诉的几率会相…

跨境电商培训分享:亚马逊账号被封怎么办 亚马逊账号申诉步骤

亚马逊店铺被封是亚马逊卖家最不愿意看到的,毕竟这个问题处理起来是非常麻烦的,那么如果遇到亚马逊账号被封应该怎么处理呢?亚马逊账号申诉步骤又是怎么样的呢?今天海熹跨境人才网就来和大家分享一下这个问题的处理方法,一起来了解一下吧。…

亚马逊申诉信怎么写?快速申诉秘诀

亚马逊关联申诉需要多久会收到回复?亚马逊的回复周期是1-25天,在提交申诉7天后还没回复的话,可以再次提交。正常情况下,7天以内是会有回复的,最快当天就会回复。但是也有亚马逊商家跟小编反应,他的一个封号…

亚马逊产品申诉信怎么写?亚马逊受限ASIN如何申诉

相信你经常在朋友圈会刷到 非常多卖亚马逊账号的服务商; ​ 是由于跨境电商渠道中; 亚马逊账号的注册难度大,资料繁琐 渠道规矩较多,账号很软弱; 造成账号异常的珍贵; 点击此处添加图片说明文字 有一个根本的常识来是: 那个跨境电商渠道…

GPT-4初体验!

作为去年12月初ChatGPT的第一批用户,这几个月一直在见证OpenAI和ChatGPT在NLP大模型和通用人工智能(AGI)上的狂飙。 月中GPT-4发布的时候,觉得暂未开放多模态输入功能的话,先暂不升级账号。但随着ChatGPT全面接入笔者日…

2023 年前端十大 Web 发展趋势

很长一段时间,Web 开发的前景似乎没有什么进展(2016 年至 2021 年),但在刚刚过去的 2022 年中确实又猛窜了一波。今天主要想跟大家聊聊最新 Web 开发趋势。相信这波浪潮会继续激发 Web 开发者的关注,对万象更新的 2023…

2023 年前端 Web 发展趋势

虽然就个人观点,我觉得 Web 开发在最近几年都没什么进展(2016 年至 2021 年),但在刚刚过去的 2022 年中确实又出现了一些新的技术。在本文中,我想跟大家聊聊自己看到的最新 Web 开发的发展趋势。相信这波浪潮会继续激发…

使用 Prompts 和 Chains 让 ChatGPT 成为神奇的生产力工具!

ChatGPT 诞生后,因其非常强大的又难以置信的的能力,得到了非常广泛的关注。用户将 ChatGPT 视作一种有趣且知识渊博的聊天工具。但事实上,使用合适的 Prompts 和 Chains,可以将 ChatGPT 作为一个神奇的生产力工具,能够…

如何打开查看网页html源码

1、双击html浏览器打开网页 方法二:html文件点击右键,选择记事本打开,ios电脑或者部分电脑可能没有此功能。 方法三:软件打开》单机html文件鼠标不放拖到软件打开,或者通过方法二的右键打开方式》选择软件打开

小程序运营推广怎么做?有什么小程序运营推广策略?

小程序由于具备无需下载、无需安装、随时可用的特点,能够给用户良好的体验。因此成为了不少商家吸引用户、培养忠诚用户的方式。由于微信也具备庞大的用户基数,因此开发微信小程序也成为企业的优先选择。在完成小程序开发后,如何进行运营推广…