当前位置:   article > 正文

【深度学习】基于BERT模型的情感分类(附实战代码)_bert模型情感分析

bert模型情感分析

个人主页:Yang-ai-cao

系列专栏:深度学习

   博学而日参省乎己,知明而行无过矣


目录

个人主页:Yang-ai-cao

系列专栏:深度学习

   博学而日参省乎己,知明而行无过矣

1.BERT介绍

2.BERT结构

3.输入表示

4.BERT训练

4.1 MLM(Masked Language Model)

4.2 NSP(Next Sentence Prediction)

5.BERT微调

6.BERT、GPT、ELMo的比较

7.BERT模型实现情感分类(代码示例)

7.1前提准备

7.2 数据预处理并创建数据集

7.2 定义训练函数与评估函数

7.3 设置损失函数、优化方法、BertTokenizer词嵌入

训练模型并预测结果

7.4 总结


1.BERT介绍

BERT的全称为Bidirectional Encoder Representation from Transformers,是一个预训练的语言  表征模型,它强调不再像以往一样采用传统的单向语言模型或者把两个单向语言模型进行浅层拼接的方法进行预训练。它旨在通过在所有层中对左右上下文进行联合调节,采用新的masked language model(MLM),用于语言理解的深度双向转换器的预训练,从未标记的文本中预训练深度双向表示。因此,只需一个额外的输出层即可对预训练的 BERT 模型进行微调,从而为各种任务(例如问答和语言推理)创建较为先进的模型,而无需对特定于任务的架构进行大量修改。

·    BERT在 11 项自然语言处理任务上获得了最先进的新结果,包括将 GLUE 分数提高到 80.5% (绝对提高 7.7%),将 MultiNLI 准确率提高到 86.7%(绝对提高 4.6%),将 SQuAD v1.1 问答测试 F1 提高到 93.2(绝对提高 1.5 分),将 SQuAD v2.0 测试 F1 提高到 83.1(绝对提高5.1 分)。

·     BERT的网络架构主要使用的是《Attention is all you need》中提出的多层Transformer结构,Transformer结构在NLP领域中已经得到了广泛应用,其最大的特点是抛弃了传统的RNN和CNN。通过Self-Attention机制将任意位置的两个单词的距离进行特定转换,有效的解决了NLP 中棘手的长期依赖问题。

2.BERT结构

BERT利用MLM进行预训练并且采用深层的双向Transformer组件进行构建模型,总体结构是将多个Transformer Encoder一层一层地堆叠起来。在论文中,作者分别用12层和24层Transformer Encoder组装出两套BERT模型,两套模型的参数总计分别为110M和340M。(Transformer模型详解(图解最完整版) - 知乎 (zhihu.com)

 

图2.1 Transformer整体结构

隐藏Transformer详细结构,表示如下:

                                                        图2.2  Transformer黑箱图

Transformer结构进行堆叠,形成更深的神经网络(可理解为将Transformer encoder进行堆叠):

图2.3 多层堆叠Transformer

经过多层Transformer结构的堆叠后,形成BERT的主体结构(可视化:大雄007)

图2.4 Bert整体结构

图2.5 BERT的总体预训练和微调程序

3.输入表示

图3.1 token表征的组成

输入向量由三层组合:

3.1.Token Embeddings 即词向量。

·   要将各个词转换成固定维度的向量。中文目前尚未对输入文本进行分词,而是直接对单子构成为本的输入单位。特别的,英文词汇会做更细粒度的切分,比如上图中的playing 切割成 play 和 ##ing;将词切割成更细粒度的 Word Piece 是为了解决未登录词的常见方法。

·   [CLS] 表示开始标志,同时[CLS]表示该特征用于分类模型,对非分类模型,该符号可以省去。[SEP]表示分句符号,用于断开输入语料中的多个句子。

·   Bert 在处理英文文本时只需 30522 个词,Token Embeddings 层会将每个词转换成 768 维向量,例如:‘I like dog’ ,3个Token 会被转换成一个 (5, 768) 的矩阵或 (1, 5, 768) 的张量。

3.2. Segment Embeddings 段落向量。

·   BERT 能够处理对输入句子对的分类任务,这类任务就像判断两个文本是否语义相似。句子对中的两个句子被简单的拼接在一起后送入到模型中,BERT通过segment embeddings去区分一个句子对中的两个句子,因为预训练不单单做LM,还得做以两个句子为输入的分类任务。

·   Segement Embeddings 层有两种向量表示,前一个向量是把 0 赋值给第一个句子的各个 Token,后一个向量是把1赋值给各个 Token,问答系统等任务要预测下一句,因此输入是有关联的句子。而文本分类只有一个句子,那么 Segement embeddings 就全部是 0。

3.3. Position Embeddings 位置向量。

·   一般认为,Transformers无法编码输入的序列的顺序性,加入position embeddings会让BERT理解不同位置的words,Bert的位置向量不使用三角函数,而是需要进行训练学习。

·   例如:出现在文本不同位置的字/词所携带的语义信息存在差异(如 ”你爱爸爸“ 和 ”爸爸爱你“),你、爸爸虽然都和爱字很接近,但是位置不同,表示的含义不同。

·   BERT 中处理的最长序列是 512 个 Token,长度超过 512 会被截取,BERT 在各个位置上学习一个向量来表示序列顺序的信息编码进来,这意味着 Position Embeddings 实际上是一个 (512, 768) 的 lookup 表,表第一行是代表第一个序列的每个位置,第二行代表序列第二个位置。

   最后,BERT 模型将 Token Embeddings (1, n, 768) + Segment Embeddings(1, n, 768) + Position Embeddings(1, n, 768) 求和的方式得到一个 Embedding(1, n, 768) 作为模型的输入。

4.BERT训练

·   预训练的概念在CV(Computer Vision,计算机视觉)中的应用十分广泛,CV中采用的预训练任务一般是ImageNet图像分类任务,完成图像分类任务的前提是必须能抽取出良好的图像特征,而ImageNet数据集有规模大、质量高的优点,因此常常能够获得很好的效果。

·   尽管NLP领域没有类似于ImageNet这样质量高的人工标注数据,但是可以利用大规模文本数据的自监督性质来构建预训练任务。因此,BERT模型的预训练(Pre-training)任务是由两个自监督任务组成,即MLMMasked Language Model)和NSPNext Sentence Prediction)。

·   对于预训练语料库,我们使用BooksCorpus(8亿个单词)(Zhu et al.,2015)和英语维基百科(2500万个单词)。对于维基百科,我们只提取文本段落并忽略列表、表和标头。为了提取长连续序列,使用文档级语料库而不是打乱的句子级语料库(如Billion Word Benchmark(Chelba et al.,2013))是至关重要的

4.1 MLM(Masked Language Model

·   MLM是BERT能够不受单向语言模型所限制的原因,简单来说就是用mask token([MASK])随机地以15%的概率对每一个训练序列中的token进行替换,然后预测出[MASK]位置原有的单词,这种实验类似于我们高中时期做过的完形填空。

·   在BERT的实验中,训练模型时,一个句子会被多次喂到模型中用于参数学习,但是Google并没有在每次都mask掉这些单词,而是在确定要Mask掉的单词之后,做以下处理:

1)80%是[MASK]。如,my dog is cute——>my dog is [MASK]

2)10%是随机的其他token。如,my dog is cute——>my dog is apple

3)10%是原来的token。如,my dog is cute——>my dog is cute

再用该位置对应的 T_{i}去预测出原来的token(输入到全连接,然后用softmax输出每个token的概率,最后用交叉熵计算loss)。该策略令到BERT不再只对[MASK]敏感,而是对所有的token都敏感,以致能抽取出任何token的表征信息。

·   基于以上思考:如果句子中的某个Token 100%都会被mask掉,那么在fine-tuning的时候模型就会有一些没有见过的单词。加入随机Token的原因是因为Transformer要保持对每个输入token的分布式表征,否则模型就会记住这个[mask]是某个出现的具体token。至于单词带来的负面影响,因为一个单词被随机替换掉的概率只有15%*10% =1.5%,这个负面影响其实是可以忽略不计的。 文章也指出每次只预测15%的单词,因此模型收敛的比较慢。

论文中关于该策略的实验数据:

图4.1  Ablation over different masking strategies

4.2 NSPNext Sentence Prediction

·   一些任务如问答、自然语言推断等需要理解两个句子之间的关系,而MLM任务倾向于抽取token层次的表征,因此不能直接获取句子层次的表征。为了使模型能够有能力理解句子间的关系,BERT使用了NSP任务来预训练,简单来说就是预测两个句子是否连在一起。具体的做法是:我们在语料库中挑选出句子A和句子B来组成一个训练样例,50%的概率句子B就是句子A的下一句(是句子A的下文则标注为IsNext),剩下50%的概率句子B是语料库中的随机句子(不是句子A的下文则标注为NotNext)。之后把训练样例输入到BERT模型中,用[CLS]对应的C信息去进行二分类的预测。训练样例如下:

Input1=[CLS] He likes the [MASK] tree [SEP] the tree can bear [MASK] fruit [SEP]

Label1=IsNext

Input2=[CLS] He likes the [MASK] tree [SEP] penguin [MASK] are flight ##less birds [SEP]

Label2=NotNext

之后把每一个训练样例输入到BERT中可以相应获得两个任务对应的loss,再把这两个loss加在一起就是整体的预训练loss我们明显看到,这两个任务所需的数据均可以从无标签的文本数据中构建,这是自监督性质的体现,比CV中需要人工标注的ImageNet数据集简单多了。

·   NSP任务可能并不是必要的,消除NSP损失在下游任务的性能上能够与原始BERT持平或略有提高。针对“以单句子为单位输入,模型无法学习到词之间的远程依赖关系”,后续的RoBERTa、ALBERT、spanBERT都移去了NSP任务。

5.BERT微调

图5.1  BERT在不同任务上的微调

        图5.1展示了BERT在不同任务上的微调情况。我们的特定任务模型是通过将BERT与一个额外的输出层结合而形成的,因此只需要从头开始学习极少量的参数。在这些任务中,(a)和(b)是序列级任务,而(c)和(d)是令牌级任务。在图中,E代表输入嵌入,Ti代表令牌i的上下文表示,[CLS]是用于分类输出的特殊符号,[SEP]是用于分隔非连续令牌序列的特殊符号。

·   微调是直接的,因为Transformer中的自注意力机制允许BERT通过交换适当的输入和输出来建模许多下游任务——无论它们涉及单文本还是文本对。对于涉及文本对的应用,一种常见的模式是在应用双向交叉注意力之前独立地对文本对进行编码,如Parikh等(2016);Seo等(2017)。BERT使用自注意力机制来统一这两个阶段,因为使用自注意力对连接的文本对进行编码实际上包括两个句子之间的双向交叉注意力。对于每个任务,我们只需将特定于任务的输入和输出插入到BERT中,并进行端到端的微调。在输入方面,预训练中的句子A和句子B类似于(1)释义中的句子对,(2)蕴含中的假设前提对,(3)问答中的问题段落对,以及(4)文本分类或序列标记中的退化文本对。在输出方面,将令牌表示形式输入到输出层以进行令牌级任务,如序列标记或问答,并将[CLS]表示形式输入到输出层以进行分类,如蕴含或情感分析。与预训练相比,微调相对便宜。使用相同的预训练模型,本文中的所有结果都可以在单个Cloud TPU上最多复制1小时,或在GPU上复制几个小时。

·   对于微调,大多数模型超参数与预训练时相同,但批处理大小、学习率和训练周期数除外。dropout概率始终保持在0.1。最佳超参数值是特定于任务的,但我们发现以下范围的可能值在所有任务中表现良好:

  • 批处理大小:16,32
  • 学习率(Adam):5e-5,3e-5,2e-5
  • 训练周期数:2,3,4

我们还观察到,大型数据集(例如,100k+标记的训练示例)对超参数选择的敏感度远低于小型数据集。微调通常非常快,因此合理地运行上述参数的穷举搜索并选择在开发集上表现最好的模型是合理的。

·   在海量的语料上训练完BERT之后,便可以将其应用到NLP的各个任务中了。 微调(Fine-Tuning)可实现的任务包括:基于句子对的分类任务,基于单个句子的分类任务,问答任务,命名实体识别等:

     ·   基于句子对的分类任务QNLI:用于判断文本是否包含问题的答案,类似于我们做阅读理解定位问题所在的段落。STS-B:预测两个句子的相似性,包括5个级别。MRPC:也是判断两个句子是否是等价的。RTE:类似于MNLI,但是只是对蕴含关系的二分类判断,而且数据集更小。SWAG:从四个句子中选择为可能为前句下文的那个。
     ·   基于单个句子的分类任务SST-2:电影评价的情感分析。CoLA:句子语义判断,是否是可接受的(Acceptable)。
     ·   问答任务SQuAD v1.1:给定一个句子(通常是一个问题)和一段描述文本,输出这个问题的答案,类似于做阅读理解的简答题。
     ·   命名实体识别CoNLL-2003 NER:判断一个句子中的单词是不是Person,Organization(组织),Location(位置),Miscellaneous(杂项)或者other(无命名实体)。​

6.BERT、GPT、ELMo的比较

图6.1 预训练模型架构的差异

·  上图所示,Trm代表的是Transformer层,E代表的是Token Embedding,即输入Token映射成的向量,T代表的是模型输出的每个Token的特征向量表示。

·  在这里,我们研究了最近流行的表示学习模型的差异,包括ELMo、OpenAI GPT和BERT。BERT使用双向Transformer,OpenAI GPT 使用从左到右的 Transformer,ELMo 使用独立训练的从左到右和从右到左的 LSTM 的串联来为下游任务生成特征。在这三者中,只有 BERT 表示在所有层中同时以左上下文和右上下文为条件。除了架构差异之外,BERT 和 OpenAI GPT 是微调方法,而 ELMo 是一种基于功能的方法。

·  与BERT最具可比性的现有预训练方法是OpenAI GPT,它在大型文本编辑器上训练从左到右的Transformer LM。事实上,BERT中的许多设计决策都是有意使其尽可能接近GPT,以便将这两种方法进行最小程度的比较。这项工作的核心论点是,模型的双向性和两个预训练任务占了经验改进的大部分,但我们确实注意到,BERT和GPT的训练方式还有其他几个差异:

          •GPT是在BooksCorpus上训练的(800M字);BERT在BooksCor pus(8亿字)和维基百科(2.5亿字)上接受培训。

          •GPT使用仅在微调时引入的句子分隔符([SEP])和分类器标记([CLS];BERT在预训练期间学习[SEP]、[CLS]和句子A/B嵌入。

          •GPT被训练了1M个步骤,批量大小为32000个单词;BERT接受了1Msteps的训练,批量大小为128000个单词。

          •GPT对所有微调实验使用5e-5的相同学习率;BERT选择特定任务的微调学习率,该学习率在开发集上表现最好。

7.BERT模型实现情感分类(代码示例)

7.1前提准备

1.确保安装了必要的库:我们使用 Hugging Face的BertTokenizer、 BertModel模型和Pytorch;BertTokenizer将输入的评论语句转化为输入Bert模型的向量信息,BertModel根据输入信息输出结果。

pip install torch transformers

2.数据集准备

准备15万条手机评论信息和对应的情感标签, 0, 1, 2 代表 差中好 。

7.2 数据预处理并创建数据集

读取文件中的评论信息,对数据进行去重。

  1. import csv
  2. import pandas as pd
  3. import random
  4. import torch
  5. from transformers import BertTokenizer, BertModel
  6. from torch import nn
  7. from d2l import torch as d2l
  8. from tqdm import tqdm
  9. """
  10. 读取评论文件的评论信息
  11. """
  12. def read_file(file_name):
  13. comments_data = None
  14. # 读取评论信息
  15. with open(file_name, 'r', encoding='UTF-8') as f:
  16. reader = csv.reader(f)
  17. # 读取评论数据和对应的标签信息
  18. comments_data = [[line[0], int(line[1])] for line in reader if len(line[0]) > 0]
  19. # 打乱数据集
  20. random.shuffle(comments_data)
  21. data = pd.DataFrame(comments_data)
  22. same_sentence_num = data.duplicated().sum() # 统计重复的评论内容个数
  23. if same_sentence_num > 0:
  24. data = data.drop_duplicates() # 删除重复的样本信息
  25. f.close()
  26. return data

读取数据集,展示样本的长度

  1. comments_data = read_file('文件路径')
  2. len(comments_data)

查看所有样本信息

print(comments_data)

运行结果如下:

以 7:3 的比例拆分训练集与测试集,设定切分线

  1. split = 0.7
  2. split_line = int(len(comments_data) * split)
  3. print(split_line)

划分训练集 train_comments, train_lables 与测试集 test_comments,test_lables 并输出它们的长度

  1. # 划分训练集与测试集,并将pandas数据类型转化为列表类型
  2. train_comments, train_labels = list(comments_data[: split_line][0]), list(comments_data[: split_line][1])
  3. test_comments, test_labels = list(comments_data[split_line:][0]), list(comments_data[split_line:][1])
  4. print(len(train_comments),len(train_labels), len(test_comments), len(test_labels))

现在微调Bert,使用Bert来实现情感分析(文本分类)的效果。默认这里使用基本模Bert_base(bert-base-chinese),使用12层Transformer编码器块,768个隐藏单元和12个自注意头。只需要在Bert的输出信息中提取出综合上下文信息 ‘<cls>’,并外接一层全连接层,即可完成情感分析(文本分类)效果。不懂点击这里

  1. """
  2. 定义BERTClassifier分类器模型
  3. """
  4. class BERTClassifier(nn.Module):
  5. # 初始化加载 bert-base-chinese 原型,即Bert中的Bert-Base模型
  6. def __init__(self, output_dim, pretrained_name='bert-base-chinese'):
  7. super(BERTClassifier, self).__init__()
  8. # 定义 Bert 模型
  9. self.bert = BertModel.from_pretrained(pretrained_name)
  10. # 外接全连接层
  11. self.mlp = nn.Linear(768, output_dim)
  12. def forward(self, tokens_X):
  13. # 得到最后一层的 '<cls>' 信息, 其标志全部上下文信息
  14. res = self.bert(**tokens_X)
  15. # res[1]代表序列的上下文信息'<cls>',外接全连接层,进行情感分析
  16. return self.mlp(res[1])

7.2 定义训练函数与评估函数

设计评估函数和训练函数,用以对模型进行训练测试

  1. """
  2. 评估函数,用以评估数据集在神经网络下的精确度
  3. """
  4. def evaluate(net, comments_data, labels_data):
  5. sum_correct, i = 0, 0
  6. while i <= len(comments_data):
  7. comments = comments_data[i: min(i + 8, len(comments_data))]
  8. tokens_X = tokenizer(comments, padding=True, truncation=True, return_tensors='pt').to(device=device)
  9. res = net(tokens_X) # 获得到预测结果
  10. y = torch.tensor(labels_data[i: min(i + 8, len(comments_data))]).reshape(-1).to(device=device)
  11. sum_correct += (res.argmax(axis=1) == y).sum() # 累加预测正确的结果
  12. i += 8
  13. return sum_correct/len(comments_data) # 返回(总正确结果/所有样本),精确率
  14. """
  15. 训练bert_classifier分类器
  16. """
  17. def train_bert_classifier(net, tokenizer, loss, optimizer, train_comments, train_labels, test_comments, test_labels,
  18. device, epochs):
  19. max_acc = 0.5 # 初始化模型最大精度为0.5
  20. # 先测试未训练前的模型精确度
  21. train_acc = evaluate(net, train_comments, train_labels)
  22. test_acc = evaluate(net, test_comments, test_labels)
  23. # 输出精度
  24. print('--epoch', 0, '\t--train_acc:', train_acc, '\t--test_acc', test_acc)
  25. # 累计训练18万条数据 epochs 次,优化模型
  26. for epoch in tqdm(range(epochs)):
  27. i, sum_loss = 0, 0 # 每次开始训练时, i 为 0 表示从第一条数据开始训练
  28. # 开始训练模型
  29. while i < len(train_comments):
  30. comments = train_comments[i: min(i + 8, len(train_comments))] # 批量训练,每次训练8条样本数据
  31. # 通过 tokenizer 数据化输入的评论语句信息,准备输入bert分类器
  32. tokens_X = tokenizer(comments, padding=True, truncation=True, return_tensors='pt').to(device=device)
  33. # 将数据输入到bert分类器模型中,获得结果
  34. res = net(tokens_X)
  35. # 批量获取实际结果信息
  36. y = torch.tensor(train_labels[i: min(i + 8, len(train_comments))]).reshape(-1).to(device=device)
  37. optimizer.zero_grad() # 清空梯度
  38. l = loss(res, y) # 计算损失
  39. l.backward() # 后向传播
  40. optimizer.step() # 更新梯度
  41. sum_loss += l.detach() # 累加损失
  42. i += 8 # 样本下标累加
  43. # 计算训练集与测试集的精度
  44. train_acc = evaluate(net, train_comments, train_labels)
  45. test_acc = evaluate(net, test_comments, test_labels)
  46. # 输出精度
  47. print('\n--epoch', epoch+1, '\t--loss:', sum_loss / (len(train_comments) / 8), '\t--train_acc:', train_acc,
  48. '\t--test_acc', test_acc)
  49. # 如果测试集精度 大于 之前保存的最大精度,保存模型参数,并重设最大值
  50. if test_acc > max_acc:
  51. # 更新历史最大精确度
  52. max_acc = test_acc
  53. # 保存模型
  54. torch.save(net.state_dict(), 'bert.parameters')

7.3 设置损失函数、优化方法、BertTokenizer词嵌入

本次实验,我们使用交叉熵损失函数、小批量随机梯度下降,并定义 BertTokenizer 将输入的评论语句(次元序列)转化为输入Bert的数据。

  1. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  2. # 获取模型设备
  3. net = BERTClassifier(output_dim=3) # BERTClassifier分类器,因为最终结果为3分类,所以输出维度为3,代表概率分布
  4. net = net.to(device) # 将模型存放到GPU中,加速计算
  5. # 定义tokenizer对象,用于将评论语句转化为BertModel的输入信息
  6. tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
  7. loss = nn.CrossEntropyLoss() # 损失函数
  8. optimizer = torch.optim.SGD(net.parameters(), lr=1e-4) # 小批量随机梯度下降算法
训练模型并预测结果
train_bert_classifier(net, tokenizer, loss, optimizer, train_comments, train_labels, test_comments, test_labels, device, 20)

运行结果如下:

经过数小时的模型训练,在测试集上的精确度最高能够达到82.18%,相对于之前的RNN系列的结果(75%左右)提升7个百分比,Bert预训练模型很强大。

现在,我们再次可视化测试一下模型预测的准确度(测试集上),如下:

  1. # 定义模型
  2. net = BERTClassifier(output_dim=3)
  3. net = net.to(device)
  4. # 加载训练好的模型参数
  5. net.load_state_dict(torch.load('./bert.parameters'))
  6. start = 0
  7. while start < 20:
  8. comment = test_comments[start]
  9. token_X = tokenizer(comment, padding=True, truncation=True, return_tensors='pt').to(device)
  10. label = test_labels[start] # 实际结果
  11. result = net(token_X).argmax(axis=1).item() # 得到预测结果
  12. # 打印评论语句
  13. print(comment)
  14. # 输出预测结果
  15. if result == 0:
  16. print('预测结果: ', 0, '----》差评', end='\t')
  17. elif result == 1:
  18. print('预测结果: ', 1, '----》中评', end='\t')
  19. else:
  20. print('预测结果: ', 2, '----》好评', end='\t')
  21. # 输出实际结果
  22. if label == 0:
  23. print('实际结果: ', 0, '----》差评', end='\t')
  24. elif label == 1:
  25. print('实际结果: ', 1, '----》中评', end='\t')
  26. else:
  27. print('实际结果: ', 2, '----》好评', end='\t')
  28. if result == label:
  29. print('预测正确')
  30. else:
  31. print('预测错误')
  32. start += 1

运行结果如下:

7.4 总结

虽然使用BERT进行情感分类取得了不错的效果,但仍有许多改进空间:

例如,我们尚未对评论数据进行预处理,也未对BERT分类器的基本结构进行优化,只是简单地外接了一个全连接层。此外,我们还未尝试更先进的优化算法和动态学习率调整。

BERT模型本身也存在一些限制:

它是基于字粒度来划分文本数据的,这可能限制了其对某些语境的理解。基于此,我们可以考虑使用BERT的改进版,如RoBERTa模型,或者更适合中文语境的ERNIE模型,来构建我们的预训练模型。通过这些改进,有望进一步提升情感分类的准确性和效率!


 参考文献:

1.《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》

2.源于产业实践的开源深度学习平台

3.飞桨-一站式学习平台

  特别鸣谢:

知乎网:大雄007、Jeffery

CSDN:Gaolw1102

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/356531
推荐阅读
相关标签
  

闽ICP备14008679号