一.BERT微调

1.介绍

自然语言推断是一个序列级别的文本对分类问题,而微调BERT只需要一个额外的基于多层感知机的架构对预训练好的BERT权重参数进行微调,如下图所示。下面将下载一个预训练好的小版本的BERT,然后对其进行微调,以便在SNLI数据集上进行自然语言推断。

2.加载预训练的BERT

在前面博客BERT预训练第二篇:李沐动手学深度学习V2-bert预训练数据集和代码实现和 BERT预训练第三篇:李沐动手学深度学习V2-BERT预训练和代码实现介绍了预训练的BERT(注意原始的BERT模型是在更大的语料库上预训练的,原始的BERT模型有数以亿计的参数)。在下面提供了两个版本的预训练的BERT:“bert.base”与原始的BERT基础模型一样大,需要大量的计算资源才能进行微调,而“bert.small”是一个小版本,以便于演示。

import os
import torch
from torch import nn
import d2l.torch
import json
import multiprocessing
d2l.torch.DATA_HUB['bert.base'] = (d2l.torch.DATA_URL + 'bert.base.torch.zip','225d66f04cae318b841a13d32af3acc165f253ac')
d2l.torch.DATA_HUB['bert.small'] = (d2l.torch.DATA_URL + 'bert.small.torch.zip','c72329e68a732bef0452e4b96a1c341c8910f81f')

两个预训练好的BERT模型都包含一个定义词表的“vocab.json”文件和一个预训练BERT参数的“pretrained.params”文件,load_pretrained_model函数用于加载预先训练好的BERT参数。

def load_pretrained_model(pretrained_model,num_hiddens,ffn_num_hiddens,num_heads,num_layers,dropout,max_len,devices):data_dir = d2l.torch.download_extract(pretrained_model)# 定义空词表以加载预定义词表vocab = d2l.torch.Vocab()vocab.idx_to_token = json.load(open(os.path.join(data_dir,'vocab.json')))vocab.token_to_idx = {token:idx for idx,token in enumerate(vocab.idx_to_token)}bert = d2l.torch.BERTModel(len(vocab),num_hiddens=num_hiddens,norm_shape=[256],ffn_num_input=256,ffn_num_hiddens=ffn_num_hiddens,num_heads=num_heads,num_layers=num_layers,dropout=dropout,max_len=max_len,key_size=256,query_size=256,value_size=256,hid_in_features=256,mlm_in_features=256,nsp_in_features=256)# bert = nn.DataParallel(bert,device_ids=devices).to(devices[0])# bert.module.load_state_dict(torch.load(os.path.join(data_dir,'pretrained.params')),strict=False)# 加载预训练BERT参数bert.load_state_dict(torch.load(os.path.join(data_dir,'pretrained.params')))return bert,vocab

为了便于在大多数机器上演示,下面加载和微调经过预训练BERT的小版本(“bert.mall”)。

devices = d2l.torch.try_all_gpus()[2:4]
bert,vocab = load_pretrained_model('bert.small',num_hiddens=256,ffn_num_hiddens=512,num_heads=4,num_layers=2,dropout=0.1,max_len=512,devices=devices)

3. 微调BERT的数据集

对于SNLI数据集的下游任务自然语言推断,定义一个定制的数据集类SNLIBERTDataset。在每个样本中,前提和假设形成一对文本序列,并被打包成一个BERT输入序列,片段索引用于区分BERT输入序列中的前提和假设。利用预定义的BERT输入序列的最大长度(max_len),持续移除输入文本对中较长文本的最后一个标记,直到满足max_len。为了加速生成用于微调BERT的SNLI数据集,使用4个工作进程并行生成训练或测试样本。

class SNLIBERTDataset(torch.utils.data.Dataset):def __init__(self,dataset,max_len,vocab=None):all_premises_hypotheses_tokens = [[p_tokens,h_tokens] for p_tokens,h_tokens in zip(*[d2l.torch.tokenize([s.lower() for s in sentences]) for sentences in dataset[:2]])]self.vocab = vocabself.max_len = max_lenself.labels = torch.tensor(dataset[2])self.all_tokens_id,self.all_segments,self.all_valid_lens = self._preprocess(all_premises_hypotheses_tokens)print(f'read {len(self.all_tokens_id)} examples')def _preprocess(self,all_premises_hypotheses_tokens):pool = multiprocessing.Pool(4)# 使用4个进程out = pool.map(self._mp_worker,all_premises_hypotheses_tokens)all_tokens_id = [tokens_id for tokens_id,segments,valid_len in out]all_segments = [segments for tokens_id,segments,valid_len in out]all_valid_lens = [valid_len for tokens_id,segments,valid_len in out]return torch.tensor(all_tokens_id,dtype=torch.long),torch.tensor(all_segments,dtype=torch.long),torch.tensor(all_valid_lens)def _mp_worker(self,premises_hypotheses_tokens):p_tokens,h_tokens = premises_hypotheses_tokensself._truncate_pair_of_tokens(p_tokens,h_tokens)tokens,segments = d2l.torch.get_tokens_and_segments(p_tokens,h_tokens)valid_len = len(tokens)tokens_id = self.vocab[tokens]+[self.vocab['<pad>']]*(self.max_len-valid_len)segments = segments+[0]*(self.max_len-valid_len)return (tokens_id,segments,valid_len)def _truncate_pair_of_tokens(self,p_tokens,h_tokens):# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置while (len(p_tokens)+len(h_tokens))>self.max_len-3:if len(p_tokens)>len(h_tokens):p_tokens.pop()else:h_tokens.pop()def __getitem__(self, idx):return (self.all_tokens_id[idx],self.all_segments[idx],self.all_valid_lens[idx]),self.labels[idx]def __len__(self):return len(self.all_tokens_id)

下载完SNLI数据集后,通过实例化SNLIBERTDataset类来生成训练和测试样本,这些样本将在自然语言推断的训练和测试期间进行小批量读取。

#在原始的BERT模型中,max_len=512
batch_size,max_len,num_workers = 512,128,d2l.torch.get_dataloader_workers()
data_dir = d2l.torch.download_extract('SNLI')
train_set = SNLIBERTDataset(d2l.torch.read_snli(data_dir,is_train=True),max_len,vocab)
test_set = SNLIBERTDataset(d2l.torch.read_snli(data_dir,is_train=False),max_len,vocab)
train_iter = torch.utils.data.DataLoader(train_set,batch_size,num_workers=num_workers,shuffle=True)
test_iter = torch.utils.data.DataLoader(test_set,batch_size,num_workers=num_workers,shuffle=False)

4. BERT微调

**用于自然语言推断的微调BERT只需要一个额外的多层感知机,该多层感知机由两个全连接层组成,**与前面BERT实现的博客BERT预训练第一篇:李沐动手学深度学习V2-bert和代码实现中BERTClassifier类中进行nsp预测的self.hidden和self.output的多层感知机结构一个。这个多层感知机将特殊的“”词元的BERT表示进行了转换,该词元同时编码前提和假设的信息,经过多层感知机后得到自然语言推断的输出分类特征维:蕴涵、矛盾和中性。

class BERTClassifier(nn.Module):def __init__(self,bert):super(BERTClassifier,self).__init__()self.encoder = bert.encoderself.hidden = bert.hiddenself.output = nn.Linear(256,3)def forward(self,inputs):tokens_X,segments_X,valid_lens_X = inputsencoded_X = self.encoder(tokens_X,segments_X,valid_lens_X)return self.output(self.hidden(encoded_X[:,0,:]))

下面将预训练的BERT模型bert被送到用于下游应用的BERTClassifier实例net中。在BERT微调的常见实现中,只有额外的多层感知机(net.output)的输出层的参数将从零开始学习。预训练BERT编码器(net.encoder)和额外的多层感知机的隐藏层(net.hidden)的所有参数都将进行微调

net = BERTClassifier(bert)

在BERT预训练中MaskLM类和NextSentencePred类在其使用的多层感知机中都有一些参数,这些参数是预训练BERT模型bert中参数的一部分,然而这些参数仅用于计算预训练过程中的遮蔽语言模型损失和下一句预测损失。这两个损失函数与微调下游应用无关,因此当BERT微调时,MaskLM和NextSentencePred中采用的多层感知机的参数不会更新(陈旧的,staled)。
通过d2l.train_batch_ch13()函数使用SNLI的训练集(train_iter)和测试集(test_iter)对net模型进行训练和评估,结果如下图所示。

lr,num_epochs = 1e-4,5
optim = torch.optim.Adam(params=net.parameters(),lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
d2l.torch.train_ch13(net,train_iter,test_iter,loss,optim,num_epochs,devices)

5. 小结

  • 针对下游应用对预训练的BERT模型进行微调,例如在SNLI数据集上进行自然语言推断。
  • 在微调过程中,BERT模型成为下游应用模型的一部分,再加上多层感知机进行下游应用模型任务的训练和评估。

6. 使用原始BERT的预训练模型进行微调

微调一个更大的预训练BERT模型,该模型与原始的BERT基础模型一样大。修改load_pretrained_model函数中的参数设置:将“bert.mall”替换为“bert.base”,将num_hiddens=256、ffn_num_hiddens=512、num_heads=4和num_layers=2的值分别增加到768、3072、12和12,同时修改多层感知机输出层的Linear层为(nn.Linear(768,3),因为现在经过BERT模型输出特征维变为768),增加微调迭代轮数,代码如下所示。

import os
import torch
from torch import nn
import d2l.torch
import json
import multiprocessingd2l.torch.DATA_HUB['bert.base'] = (d2l.torch.DATA_URL + 'bert.base.torch.zip','225d66f04cae318b841a13d32af3acc165f253ac')
d2l.torch.DATA_HUB['bert.small'] = (d2l.torch.DATA_URL + 'bert.small.torch.zip','c72329e68a732bef0452e4b96a1c341c8910f81f')devices = d2l.torch.try_all_gpus()
def load_pretrained_model1(pretrained_model,num_hiddens,ffn_num_hiddens,num_heads,num_layers,dropout,max_len,devices):data_dir = d2l.torch.download_extract(pretrained_model)vocab = d2l.torch.Vocab()vocab.idx_to_token = json.load(open(os.path.join(data_dir,'vocab.json')))vocab.token_to_idx = {token:idx for idx,token in enumerate(vocab.idx_to_token)}bert = d2l.torch.BERTModel(len(vocab),num_hiddens=num_hiddens,norm_shape=[768],ffn_num_input=768,ffn_num_hiddens=ffn_num_hiddens,num_heads=num_heads,num_layers=num_layers,dropout=dropout,max_len=max_len,key_size=768,query_size=768,value_size=768,hid_in_features=768,mlm_in_features=768,nsp_in_features=768)# bert = nn.DataParallel(bert,device_ids=devices).to(devices[0])# bert.module.load_state_dict(torch.load(os.path.join(data_dir,'pretrained.params')),strict=False)bert.load_state_dict(torch.load(os.path.join(data_dir,'pretrained.params')))return bert,vocabbert,vocab = load_pretrained_model1('bert.base',num_hiddens=768,ffn_num_hiddens=3072,num_heads=12,num_layers=12,dropout=0.1,max_len=512,devices=devices)
class SNLIBERTDataset(torch.utils.data.Dataset):def __init__(self, dataset, max_len, vocab=None):all_premises_hypotheses_tokens = [[p_tokens, h_tokens] for p_tokens, h_tokens inzip(*[d2l.torch.tokenize([s.lower() for s in sentences]) for sentences indataset[:2]])]self.vocab = vocabself.max_len = max_lenself.labels = torch.tensor(dataset[2])self.all_tokens_id, self.all_segments, self.all_valid_lens = self._preprocess(all_premises_hypotheses_tokens)print(f'read {len(self.all_tokens_id)} examples')def _preprocess(self, all_premises_hypotheses_tokens):pool = multiprocessing.Pool(4)  # 使用4个进程out = pool.map(self._mp_worker, all_premises_hypotheses_tokens)all_tokens_id = [tokens_id for tokens_id, segments, valid_len in out]all_segments = [segments for tokens_id, segments, valid_len in out]all_valid_lens = [valid_len for tokens_id, segments, valid_len in out]return torch.tensor(all_tokens_id, dtype=torch.long), torch.tensor(all_segments,dtype=torch.long), torch.tensor(all_valid_lens)def _mp_worker(self, premises_hypotheses_tokens):p_tokens, h_tokens = premises_hypotheses_tokensself._truncate_pair_of_tokens(p_tokens, h_tokens)tokens, segments = d2l.torch.get_tokens_and_segments(p_tokens, h_tokens)valid_len = len(tokens)tokens_id = self.vocab[tokens] + [self.vocab['<pad>']] * (self.max_len - valid_len)segments = segments + [0] * (self.max_len - valid_len)return (tokens_id, segments, valid_len)def _truncate_pair_of_tokens(self, p_tokens, h_tokens):# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置while (len(p_tokens) + len(h_tokens)) > self.max_len - 3:if len(p_tokens) > len(h_tokens):p_tokens.pop()else:h_tokens.pop()def __getitem__(self, idx):return (self.all_tokens_id[idx], self.all_segments[idx], self.all_valid_lens[idx]), self.labels[idx]def __len__(self):return len(self.all_tokens_id)#在原始的BERT模型中,max_len=512
batch_size, max_len, num_workers = 512, 128, d2l.torch.get_dataloader_workers()
data_dir = d2l.torch.download_extract('SNLI')
train_set = SNLIBERTDataset(d2l.torch.read_snli(data_dir, is_train=True), max_len, vocab)
test_set = SNLIBERTDataset(d2l.torch.read_snli(data_dir, is_train=False), max_len, vocab)
train_iter = torch.utils.data.DataLoader(train_set, batch_size, num_workers=num_workers, shuffle=True)
test_iter = torch.utils.data.DataLoader(test_set, batch_size, num_workers=num_workers, shuffle=False)class BERTClassifier(nn.Module):def __init__(self, bert):super(BERTClassifier, self).__init__()self.encoder = bert.encoderself.hidden = bert.hiddenself.output = nn.Linear(768, 3)def forward(self, inputs):tokens_X, segments_X, valid_lens_X = inputsencoded_X = self.encoder(tokens_X, segments_X, valid_lens_X)return self.output(self.hidden(encoded_X[:, 0, :]))net = BERTClassifier(bert)
lr, num_epochs = 1e-4, 20
optim = torch.optim.Adam(params=net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
d2l.torch.train_ch13(net, train_iter, test_iter, loss, optim, num_epochs, devices)

7. 全部代码

import os
import torch
from torch import nn
import d2l.torch
import json
import multiprocessingd2l.torch.DATA_HUB['bert.base'] = (d2l.torch.DATA_URL + 'bert.base.torch.zip','225d66f04cae318b841a13d32af3acc165f253ac')
d2l.torch.DATA_HUB['bert.small'] = (d2l.torch.DATA_URL + 'bert.small.torch.zip','c72329e68a732bef0452e4b96a1c341c8910f81f')def load_pretrained_model(pretrained_model, num_hiddens, ffn_num_hiddens, num_heads, num_layers, dropout, max_len,devices):data_dir = d2l.torch.download_extract(pretrained_model)# 定义空词表以加载预定义词表vocab = d2l.torch.Vocab()vocab.idx_to_token = json.load(open(os.path.join(data_dir, 'vocab.json')))vocab.token_to_idx = {token: idx for idx, token in enumerate(vocab.idx_to_token)}bert = d2l.torch.BERTModel(len(vocab), num_hiddens=num_hiddens, norm_shape=[256], ffn_num_input=256,ffn_num_hiddens=ffn_num_hiddens, num_heads=num_heads, num_layers=num_layers,dropout=dropout, max_len=max_len, key_size=256, query_size=256, value_size=256,hid_in_features=256, mlm_in_features=256, nsp_in_features=256)# bert = nn.DataParallel(bert,device_ids=devices).to(devices[0])# bert.module.load_state_dict(torch.load(os.path.join(data_dir,'pretrained.params')),strict=False)# 加载预训练BERT参数bert.load_state_dict(torch.load(os.path.join(data_dir, 'pretrained.params')))return bert, vocabdevices = d2l.torch.try_all_gpus()[2:4]
bert, vocab = load_pretrained_model('bert.small', num_hiddens=256, ffn_num_hiddens=512, num_heads=4, num_layers=2,dropout=0.1, max_len=512, devices=devices)class SNLIBERTDataset(torch.utils.data.Dataset):def __init__(self, dataset, max_len, vocab=None):all_premises_hypotheses_tokens = [[p_tokens, h_tokens] for p_tokens, h_tokens inzip(*[d2l.torch.tokenize([s.lower() for s in sentences]) for sentences indataset[:2]])]self.vocab = vocabself.max_len = max_lenself.labels = torch.tensor(dataset[2])self.all_tokens_id, self.all_segments, self.all_valid_lens = self._preprocess(all_premises_hypotheses_tokens)print(f'read {len(self.all_tokens_id)} examples')def _preprocess(self, all_premises_hypotheses_tokens):pool = multiprocessing.Pool(4)  # 使用4个进程out = pool.map(self._mp_worker, all_premises_hypotheses_tokens)all_tokens_id = [tokens_id for tokens_id, segments, valid_len in out]all_segments = [segments for tokens_id, segments, valid_len in out]all_valid_lens = [valid_len for tokens_id, segments, valid_len in out]return torch.tensor(all_tokens_id, dtype=torch.long), torch.tensor(all_segments,dtype=torch.long), torch.tensor(all_valid_lens)def _mp_worker(self, premises_hypotheses_tokens):p_tokens, h_tokens = premises_hypotheses_tokensself._truncate_pair_of_tokens(p_tokens, h_tokens)tokens, segments = d2l.torch.get_tokens_and_segments(p_tokens, h_tokens)valid_len = len(tokens)tokens_id = self.vocab[tokens] + [self.vocab['<pad>']] * (self.max_len - valid_len)segments = segments + [0] * (self.max_len - valid_len)return (tokens_id, segments, valid_len)def _truncate_pair_of_tokens(self, p_tokens, h_tokens):# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置while (len(p_tokens) + len(h_tokens)) > self.max_len - 3:if len(p_tokens) > len(h_tokens):p_tokens.pop()else:h_tokens.pop()def __getitem__(self, idx):return (self.all_tokens_id[idx], self.all_segments[idx], self.all_valid_lens[idx]), self.labels[idx]def __len__(self):return len(self.all_tokens_id)#在原始的BERT模型中,max_len=512
batch_size, max_len, num_workers = 512, 128, d2l.torch.get_dataloader_workers()
data_dir = d2l.torch.download_extract('SNLI')
train_set = SNLIBERTDataset(d2l.torch.read_snli(data_dir, is_train=True), max_len, vocab)
test_set = SNLIBERTDataset(d2l.torch.read_snli(data_dir, is_train=False), max_len, vocab)
train_iter = torch.utils.data.DataLoader(train_set, batch_size, num_workers=num_workers, shuffle=True)
test_iter = torch.utils.data.DataLoader(test_set, batch_size, num_workers=num_workers, shuffle=False)class BERTClassifier(nn.Module):def __init__(self, bert):super(BERTClassifier, self).__init__()self.encoder = bert.encoderself.hidden = bert.hiddenself.output = nn.Linear(256, 3)def forward(self, inputs):tokens_X, segments_X, valid_lens_X = inputsencoded_X = self.encoder(tokens_X, segments_X, valid_lens_X)return self.output(self.hidden(encoded_X[:, 0, :]))net = BERTClassifier(bert)
lr, num_epochs = 1e-4, 5
optim = torch.optim.Adam(params=net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
d2l.torch.train_ch13(net, train_iter, test_iter, loss, optim, num_epochs, devices)

8. 相关链接

BERT预训练第一篇:李沐动手学深度学习V2-bert和代码实现
BERT预训练第二篇:李沐动手学深度学习V2-bert预训练数据集和代码实现
BERT预训练第三篇:李沐动手学深度学习V2-BERT预训练和代码实现
BERT微调第一篇:李沐动手学深度学习V2-自然语言推断与数据集SNLI和代码实现
BERT微调第二篇:李沐动手学深度学习V2-BERT微调和代码实现

李沐动手学深度学习V2-BERT微调和代码实现相关推荐

  1. 李沐动手学深度学习v2/总结1

    总结 编码过程 数据 数据预处理 模型 参数,初始化参数 超参数 损失函数,先计算损失,清空梯度(防止有累积的梯度),再对损失后向传播计算损失关于参数的梯度 优化算法,使用优化算法更新参数 训练求参数 ...

  2. 李沐动手学深度学习:08 线性回归(代码逐行理解)

    目录 一.相关资料连接 1.1 李沐视频 1.2 代码.PPT 二.代码及笔记(使用Jupyter Notebook) 2.1 线性回归从零开始实现 2.1.1 基本概念 2.1.2 基础优化算法 2 ...

  3. 14李沐动手学深度学习v2/权重衰退简洁实现

    # 权重衰退是广泛应用的正则化技术 %matplotlib inline import torch from torch import nn from d2l import torch as d2l ...

  4. 李沐动手学深度学习V2-全卷积网络FCN和代码实现

    一.全卷积网络FCN 1. 介绍 语义分割是对图像中的每个像素分类,全卷积网络(fully convolutional network,FCN)采用卷积神经网络实现了从图像像素到像素类别的变换 ,与前 ...

  5. 李沐动手学深度学习(pytorch版本)d2lzh_pytorch包的缺少安装问题

    学习深度学习时候,很多人参考的是李沐的动手学深度学习Pytorch版本(附上官方地址:https://tangshusen.me/Dive-into-DL-PyTorch/#/). 在学习3.5.1节 ...

  6. 【李沐动手学深度学习】读书笔记 01前言

    虽然之前已经学过这部分内容和深度学习中的基础知识,但总觉得学的不够系统扎实,所以希望再通过沐神的课程以及书籍,系统条理的学习一遍.在读书过程中,利用导图做了一下梳理,形成了这个读书笔记.如有侵权,请联 ...

  7. 关于李沐动手学深度学习(d2l)pytorch环境本地配置

    本地安装d2l 由于之前试了很多次d2l课本的安装方法失败了,这里提供一种我可以成功安装d2l包的方法. pytorch安装 首先安装cuda.cudnn.pytroch(gpu版本).可以参考这篇文 ...

  8. windows上配置深度学习(李沐-动手学深度学习)

    1.安装miniconda windows下安装,去清华大学开源镜像下载,速度比较快. 选中Miniconda3-latest-Windos-x86_64.exe下载安装包(目前最新的是py3.9) ...

  9. 李沐动手学深度学习第四章-4.9.环境和分布偏移

    我们从来没有想过数据最初从哪里来?以及我们计划最终如何处理模型的输出? 根据测试集的精度衡量,模型表现得非常出色. 但是当数据分布突然改变时,模型在部署中会出现灾难性的失败. 解决方案很简单(要求&q ...

  10. 李沐动手学深度学习V2-图像增广和代码实现

    图像增广 大型数据集是成功应用深度神经网络的先决条件,因为解决了大型复杂网络的过拟合性. 图像增广在对训练图像进行一系列的随机变化之后,生成相似但不同的训练样本,从而扩大了训练集的规模. 此外,应用图 ...

最新文章

  1. Python Qt GUI设计:QLineEdit和QTextEdit文本框类(基础篇—13)
  2. QCon2016旧金山大会焦点分享者确认
  3. c 服务器文件是否存在,c服务器文件是存在
  4. 神策数据薛创宇:数据分析与场景实践之“坑位运营”
  5. Git——[error: RPC failed; curl 56 OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 10054]解决方案
  6. Windows Phone(三)WP7版 记账本 开发(使用SQLite数据库)
  7. Unicode和Ascii转byte,Unicode占二个byte,Ascii占一个byte
  8. 宽字符串忽略大小写比较的实现(原)
  9. [mstsc]解决win8 win10 平板无法被mstsc连接的问题
  10. VC++ 的 CImageList 绘制背景透明的位图
  11. TDA4VM芯片手册阅读笔记
  12. 计算机cpu占用率高,CPU占用率高怎么解决?Win10系统电脑CPU占用率100%的解决方法...
  13. 十四、反射(高琪java300集+java从入门到精通笔记)
  14. 程序员 谨防猝死
  15. 考研数学一无穷级数总结
  16. 阿里云S6机型与N4机型哪个好?
  17. Oracle数据仓库参考架构
  18. photoshop入门笔记1:PS的快捷键
  19. Java Json格式化工具
  20. 按头安利 好看又实用的婚礼婚庆ppt模板素材看这里

热门文章

  1. 程序员在大城市奋斗的理由,仅为追求那些不曾见过的五光十色
  2. JavaScript之时间线(代码解释)
  3. Linux 下修改时间和时区
  4. java毕业生设计原创网络文学管理系统计算机源码+系统+mysql+调试部署+lw
  5. 什么是黑盒测试?它的常用方法有哪些?
  6. 图片尺寸怎么修改?分享2种方法快速修改图片尺寸大小
  7. Xcode Accounts连接到Apple ID 服务器时出错
  8. metro风格的特点
  9. xp系统工作组计算机没有权限访问权限,XP系统工作组计算机没有法访问如何处理?...
  10. jeecms idea部署