热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

PaddlePaddle实战|情感分析算法从原理到实战全解

在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类&#x

在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤);也可以是三类,如(积极,消极,中性)等等。

情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论;或为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。

今天是5月20日,PaddlePaddle教你用情感分析算法体会女神心意。

在下文中,我们将以情感分析为例,介绍使用深度学习的方法进行端对端的短文本分类,并使用PaddlePaddle完成全部相关实验。

项目地址:

https://github.com/PaddlePaddle/book/blob/develop/06.understand_sentiment/README.cn.md

应用背景

在自然语言处理中,情感分析属于典型的文本分类问题,即把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示分类方法两个问题。

在深度学习的方法出现之前,主流的文本表示方法为词袋模型BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression)等等。

对于一段文本,BOW表示会忽略其词顺序、语法和句法,将这段文本仅仅看做是一个词集合,因此BOW方法并不能充分表示文本的语义信息。

例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。

在本教程中,我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升[1]。

模型概览

本教程所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。下面依次介绍这几个模型。

文本卷积神经网络简介(CNN)

对卷积神经网络来说,首先使用卷积处理输入的词向量序列,产生一个特征图(feature map),对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征,最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示,对于文本分类问题,将其连接至softmax即构建出完整的模型。

在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵,这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子,图1表示卷积神经网络文本分类模型,不同颜色表示不同大小的卷积核操作。

对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率[1]。若想得到更抽象更高级的文本特征表示,可以构建深层文本卷积神经网络[2,3]。

循环神经网络(RNN)

循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的[4]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory[5]等)在自然语言处理的多个领域,如语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。

循环神经网络按时间展开后如图2所示:在第t时刻,网络读入第t个输入(向量表示)及前一时刻隐层的状态值 (向量表示,一般初始化为0向量),计算得出本时刻隐层的状态值,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为f,则其公式可表示为:

其中是输入到隐层的矩阵参数,是隐层到隐层的矩阵参数,为隐层的偏置向量(bias)参数,σ为sigmoid函数。

在处理自然语言时,一般会先将词(one-hot表示)映射为其词向量表示,然后再作为循环神经网络每一时刻的输入。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。

长短期记忆网络(LSTM)

对于较长的序列数据,循环神经网络的训练过程中容易出现梯度消失或爆炸现象[6]。LSTM能够解决这一问题。相比于简单的循环神经网络,LSTM增加了记忆单元c、输入门i、遗忘门f及输出门o。这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。若将基于LSTM的循环神经网络表示的函数记为F,则其公式为:

F由下列公式组合而成[7]:

其中,,,,分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的W及b为模型参数,tanh为双曲正切函数,⊙表示逐元素(elementwise)的乘法操作。输入门控制着新输入进入记忆单元c的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,它们各自以不同的方式控制着记忆单元c,如图3所示:

LSTM通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有Gated Recurrent Unit (GRU)[8],其设计更为简洁一些。这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:

其中,Recrurent可以表示简单的循环神经网络、GRU或LSTM。

栈式双向LSTM(Stacked Bidirectional LSTM)

对于正常顺序的循环神经网络,包含了t时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法(深层神经网络往往能得到更抽象和高级的特征表示),我们可以通过构建更加强有力的基于LSTM的栈式双向循环神经网络[9],来对时序数据进行建模。

如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示(这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象),最后我们将文本表示连接至softmax构建分类模型。

基于PaddlePaddle的实战

PaddlePaddle简介

PaddlePaddle(paddlepaddle.org)是百度研发的深度学习框架。除了核心框架之外,PaddlePaddle还提供了丰富的工具组件。官方开源了多个工业级应用模型,涵盖自然语言处理、计算机视觉、推荐引擎等多个领域,并开放了多个领先的预训练中文模型。4月23日深度学习开发者峰会上,PaddlePaddle发布了一系列新特性和应用案例。

数据集介绍

我们以IMDB情感分析数据集为例进行介绍。IMDB数据集的训练集和测试集分别包含25000个已标注过的电影评论。其中,负面评论的得分小于等于4,正面评论的得分大于等于7,满分10分。

aclImdb
|- test|-- neg|-- pos
|- train|-- neg|-- pos

PaddlePaddle在 dataset/imdb.py 中实现了imdb数据集的自动下载和读取,并提供了读取字典、训练数据、测试数据等API。

配置模型

在该示例中,我们实现了两种文本分类算法,文本卷积神经网络,和栈式双向LSTM。我们首先引入要用到的库和定义全局变量:

from __future__ import print_function
import paddle
import paddle.fluid as fluid
import numpy as np
import sys
import mathCLASS_DIM = 2     #情感分类的类别数
EMB_DIM = 128     #词向量的维度
HID_DIM = 512     #隐藏层的维度
STACKED_NUM = 3   #LSTM双向栈的层数
BATCH_SIZE = 128  #batch的大小

文本卷积神经网络

我们构建神经网络 convolution_net,示例代码如下。 需要注意的是:fluid.nets.sequence_conv_pool 包含卷积和池化层两个操作。

#文本卷积神经网络
def convolution_net(data, input_dim, class_dim, emb_dim, hid_dim):emb = fluid.layers.embedding(input=data, size=[input_dim, emb_dim], is_sparse=True)conv_3 = fluid.nets.sequence_conv_pool(input=emb,num_filters=hid_dim,filter_size=3,act="tanh",pool_type="sqrt")conv_4 = fluid.nets.sequence_conv_pool(input=emb,num_filters=hid_dim,filter_size=4,act="tanh",pool_type="sqrt")prediction = fluid.layers.fc(input=[conv_3, conv_4], size=class_dim, act="softmax")return prediction

网络的输入 input_dim 表示的是词典的大小,class_dim 表示类别数。这里,我们使用 sequence_conv_pool API实现了卷积和池化操作。

栈式双向LSTM

栈式双向神经网络stacked_lstm_net的代码片段如下:

#栈式双向LSTM
def stacked_lstm_net(data, input_dim, class_dim, emb_dim, hid_dim, stacked_num):assert stacked_num % 2 == 1#计算词向量emb = fluid.layers.embedding(input=data, size=[input_dim, emb_dim], is_sparse=True)#第一层栈#全连接层fc1 = fluid.layers.fc(input=emb, size=hid_dim)#lstm层lstm1, cell1 = fluid.layers.dynamic_lstm(input=fc1, size=hid_dim)inputs = [fc1, lstm1]#其余的所有栈结构for i in range(2, stacked_num + 1):fc = fluid.layers.fc(input=inputs, size=hid_dim)lstm, cell = fluid.layers.dynamic_lstm(input=fc, size=hid_dim, is_reverse=(i % 2) == 0)inputs = [fc, lstm]#池化层fc_last = fluid.layers.sequence_pool(input=inputs[0], pool_type='max')lstm_last = fluid.layers.sequence_pool(input=inputs[1], pool_type='max')#全连接层,softmax预测prediction = fluid.layers.fc(input=[fc_last, lstm_last], size=class_dim, act='softmax')
return prediction

以上的栈式双向LSTM抽象出了高级特征并把其映射到和分类类别数同样大小的向量上。最后一个全连接层的’softmax’激活函数用来计算分类属于某个类别的概率。

重申一下,此处我们可以调用 convolution_net 或 stacked_lstm_net 的任何一个网络结构进行训练学习。我们以 convolution_net 为例。

接下来我们定义预测程序(inference_program)。预测程序使用convolution_net 来对 fluid.layer.data 的输入进行预测。

def inference_program(word_dict):data = fluid.layers.data(name="words", shape=[1], dtype="int64", lod_level=1)dict_dim = len(word_dict)net = convolution_net(data, dict_dim, CLASS_DIM, EMB_DIM, HID_DIM)# net = stacked_lstm_net(data, dict_dim, CLASS_DIM, EMB_DIM, HID_DIM, STACKED_NUM)
return net

我们这里定义了 training_program。它使用了从 inference_program 返回的结果来计算误差。我们同时定义了优化函数 optimizer_func 。

因为是有监督的学习,训练集的标签也在fluid.layers.data中定义了。在训练过程中,交叉熵用来在fluid.layer.cross_entropy中作为损失函数。

在测试过程中,分类器会计算各个输出的概率。第一个返回的数值规定为cost。

def train_program(prediction):label = fluid.layers.data(name="label", shape=[1], dtype="int64")cost = fluid.layers.cross_entropy(input=prediction, label=label)avg_cost = fluid.layers.mean(cost)accuracy = fluid.layers.accuracy(input=prediction, label=label)return [avg_cost, accuracy]   #返回平均cost和准确率acc#优化函数
def optimizer_func():return fluid.optimizer.Adagrad(learning_rate=0.002)

训练模型

定义训练环境

定义你的训练是在CPU上还是在GPU上:

use_cuda = False  #在cpu上进行训练
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

定义数据提供器

下一步是为训练和测试定义数据提供器。提供器读入一个大小为 BATCH_SIZE的数据。paddle.dataset.imdb.word_dict 每次会在乱序化后提供一个大小为BATCH_SIZE的数据,乱序化的大小为缓存大小buf_size。

注意:读取IMDB的数据可能会花费几分钟的时间,请耐心等待。

print("Loading IMDB word dict....")
word_dict = paddle.dataset.imdb.word_dict()print ("Reading training data....")
train_reader = paddle.batch(paddle.reader.shuffle(paddle.dataset.imdb.train(word_dict), buf_size=25000),batch_size=BATCH_SIZE)
print("Reading testing data....")
test_reader = paddle.batch(
paddle.dataset.imdb.test(word_dict), batch_size=BATCH_SIZE)feed_order = ['words', 'label']
pass_num = 1

word_dict 是一个字典序列,是词和label的对应关系,运行下一行可以看到具体内容:

word_dict

每行是如(’limited’: 1726)的对应关系,该行表示单词limited所对应的label是1726。

构造训练器

训练器需要一个训练程序和一个训练优化函数。

main_program = fluid.default_main_program()
star_program = fluid.default_startup_program()
prediction = inference_program(word_dict)
train_func_outputs = train_program(prediction)
avg_cost = train_func_outputs[0]test_program = main_program.clone(for_test=True)sgd_optimizer = optimizer_func()
sgd_optimizer.minimize(avg_cost)
exe = fluid.Executor(place)

该函数用来计算训练中模型在test数据集上的结果

def train_test(program, reader):count = 0feed_var_list = [program.global_block().var(var_name) for var_name in feed_order]feeder_test = fluid.DataFeeder(feed_list=feed_var_list, place=place)test_exe = fluid.Executor(place)accumulated = len([avg_cost, accuracy]) * [0]for test_data in reader():avg_cost_np = test_exe.run(program=program,feed=feeder_test.feed(test_data),fetch_list=[avg_cost, accuracy])accumulated = [x[0] + x[1][0] for x in zip(accumulated, avg_cost_np)]count += 1return [x / count for x in accumulated]

提供数据并构建主训练循环

feed_order 用来定义每条产生的数据和 fluid.layers.data 之间的映射关系。比如,imdb.train 产生的第一列的数据对应的是words这个特征。

# Specify the directory path to save the parameters
params_dirname = "understand_sentiment_conv.inference.model"feed_order = ['words', 'label']
pass_num = 1  #训练循环的轮数#程序主循环部分
def train_loop():
#启动上文构建的训练器
feed_var_list_loop = [main_program.global_block().var(var_name) for var_name in feed_order
]
feeder = fluid.DataFeeder(feed_list=feed_var_list_loop,place=place) 
exe.run(star_program)#训练循环for epoch_id in range(pass_num):for step_id, data in enumerate(train_reader()):#运行训练器  metrics = exe.run(main_program,feed=feeder.feed(data),fetch_list=[var.name for var in train_func_outputs])#测试结果print("step: {0}, Metrics {1}".format(step_id, list(map(np.array, metrics))))if (step_id + 1) % 10 == 0:avg_cost_test, acc_test = train_test(test_program,test_reader)print('Step {0}, Test Loss {1:0.2}, Acc {2:0.2}'.format(step_id, avg_cost_test, acc_test))print("Step {0}, Epoch {1} Metrics {2}".format(step_id, epoch_id, list(map(np.array, metrics))))if math.isnan(float(metrics[0])):sys.exit("got NaN loss, training failed.")if params_dirname is not None:fluid.io.save_inference_model(params_dirname, ["words"],prediction, exe) #保存模型
train_loop()

训练过程处理

我们在训练主循环里打印了每一步输出,可以观察训练情况。

开始训练

最后,我们启动训练主循环来开始训练。训练时间较长,如果为了更快的返回结果,可以通过调整损耗值范围或者训练步数,以减少准确率的代价来缩短训练时间。

train_loop(fluid.default_main_program())

应用模型

构建预测器

和训练过程一样,我们需要创建一个预测过程,并使用训练得到的模型和参数来进行预测,params_dirname 用来存放训练过程中的各个参数。

place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
exe = fluid.Executor(place)
inference_scope = fluid.core.Scope()

生成测试用输入数据

为了进行预测,我们任意选取3个评论。请随意选取您看好的3个。我们把评论中的每个词对应到word_dict中的id。如果词典中没有这个词,则设为unknown。 然后我们用create_lod_tensor来创建细节层次的张量

reviews_str = ['read the book forget the movie', 'this is a great movie', 'this is very bad'
]
reviews = [c.split() for c in reviews_str]UNK = word_dict['']
lod = []
for c in reviews:lod.append([word_dict.get(words, UNK) for words in c])base_shape = [[len(c) for c in lod]]tensor_words = fluid.create_lod_tensor(lod, base_shape, place)

应用模型并进行预测

现在我们可以对每一条评论进行正面或者负面的预测啦。

with fluid.scope_guard(inference_scope):[inferencer, feed_target_names,fetch_targets] = fluid.io.load_inference_model(params_dirname, exe)reviews_str = ['read the book forget the moive’,’this is a great moive','this is very bad'
]
reviews = [c.split() for c in reviews_str]UNK = word_dict['']
lod = []
for c in reviews:lod.append([np.int64(word_dict.get(words, UNK)) for words in c])base_shape = [[len(c) for c in lod]]tensor_words = fluid.create_lod_tensor(lod, base_shape,place)
assert feed_target_names[0] == "words"
results = exe.run(inferencer,feed={feed_target_names[0]: tensor_words},fetch_list=fetch_targets,return_numpy=False)np_data = np.array(results[0])for i, r in enumerate(np_data):print("Predict probability of ", r[0], " to be positive and ", r[1]," to be negative for review \'", reviews_str[i], "\'")

感兴趣的小伙伴可以在PaddlePaddle官网上阅读其他相关文档内容:http://www.paddlepaddle.org/

参考文献:

  1. Kim Y. Convolutional neural networks for sentence classification[J]. arXiv preprint arXiv:1408.5882, 2014.

  2. Kalchbrenner N, Grefenstette E, Blunsom P. A convolutional neural network for modelling sentences[J]. arXiv preprint arXiv:1404.2188, 2014.

  3. Yann N. Dauphin, et al. Language Modeling with Gated Convolutional Networks[J] arXiv preprint arXiv:1612.08083, 2016.

  4. Siegelmann H T, Sontag E D. On the computational power of neural nets[C]//Proceedings of the fifth annual workshop on Computational learning theory. ACM, 1992: 440-449.

  5. Hochreiter S, Schmidhuber J. Long short-term memory[J]. Neural computation, 1997, 9(8): 1735-1780.

  6. Bengio Y, Simard P, Frasconi P. Learning long-term dependencies with gradient descent is difficult[J]. IEEE transactions on neural networks, 1994, 5(2): 157-166.

  7. Graves A. Generating sequences with recurrent neural networks[J]. arXiv preprint arXiv:1308.0850, 2013.

  8. Cho K, Van Merriënboer B, Gulcehre C, et al. Learning phrase representations using RNN encoder-decoder for statistical machine translation[J]. arXiv preprint arXiv:1406.1078, 2014.

  9. Zhou J, Xu W. End-to-end learning of semantic role labeling using recurrent neural networks[C]//Proceedings of the Annual Meeting of the Association for Computational Linguistics. 2015.


推荐阅读
  • 【MySQL】frm文件解析
    官网说明:http:dev.mysql.comdocinternalsenfrm-file-format.htmlfrm是MySQL表结构定义文件,通常frm文件是不会损坏的,但是如果 ... [详细]
  • 可参照github代码:https:github.comrabbitmqrabbitmq-tutorialsblobmasterjavaEmitLogTopic.ja ... [详细]
  • MySQL Decimal 类型的最大值解析及其在数据处理中的应用艺术
    在关系型数据库中,表的设计与SQL语句的编写对性能的影响至关重要,甚至可占到90%以上。本文将重点探讨MySQL中Decimal类型的最大值及其在数据处理中的应用技巧,通过实例分析和优化建议,帮助读者深入理解并掌握这一重要知识点。 ... [详细]
  • 深入解析经典卷积神经网络及其实现代码
    深入解析经典卷积神经网络及其实现代码 ... [详细]
  • 2019年斯坦福大学CS224n课程笔记:深度学习在自然语言处理中的应用——Word2Vec与GloVe模型解析
    本文详细解析了2019年斯坦福大学CS224n课程中关于深度学习在自然语言处理(NLP)领域的应用,重点探讨了Word2Vec和GloVe两种词嵌入模型的原理与实现方法。通过具体案例分析,深入阐述了这两种模型在提升NLP任务性能方面的优势与应用场景。 ... [详细]
  • 2018年热门趋势:轻松几步构建高效智能聊天机器人
    2018年,构建高效智能聊天机器人的简易步骤成为行业焦点。作为AI领域的关键应用,聊天机器人不仅被视为企业市场智能化转型的重要工具,也是技术变现的主要途径之一。随着自然语言处理技术的不断进步,越来越多的企业开始重视并投资于这一领域,以期通过聊天机器人提升客户服务体验和运营效率。 ... [详细]
  • 如何选择机器学习方法http:scikit-learn.orgstabletutorialmachine_learning_mapindex.html通用学习模式只需要先定义 ... [详细]
  • 深度学习: 目标函数
    Introduction目标函数是深度学习之心,是模型训练的发动机。目标函数(objectfunction)损失函数(lossfunction)代价函数(costfunction) ... [详细]
  • 分隔超平面:将数据集分割开来的直线叫做分隔超平面。超平面:如果数据集是N维的,那么就需要N-1维的某对象来对数据进行分割。该对象叫做超平面,也就是分类的决策边界。间隔:一个点 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 统一知识图谱学习和建议:更好地理解用户偏好
    本文介绍了一种将知识图谱纳入推荐系统的方法,以提高推荐的准确性和可解释性。与现有方法不同的是,本方法考虑了知识图谱的不完整性,并在知识图谱中传输关系信息,以更好地理解用户的偏好。通过大量实验,验证了本方法在推荐任务和知识图谱完成任务上的优势。 ... [详细]
  • GPT-3发布,动动手指就能自动生成代码的神器来了!
    近日,OpenAI发布了最新的NLP模型GPT-3,该模型在GitHub趋势榜上名列前茅。GPT-3使用的数据集容量达到45TB,参数个数高达1750亿,训练好的模型需要700G的硬盘空间来存储。一位开发者根据GPT-3模型上线了一个名为debuid的网站,用户只需用英语描述需求,前端代码就能自动生成。这个神奇的功能让许多程序员感到惊讶。去年,OpenAI在与世界冠军OG战队的表演赛中展示了他们的强化学习模型,在限定条件下以2:0完胜人类冠军。 ... [详细]
  • cs231n Lecture 3 线性分类笔记(一)
    内容列表线性分类器简介线性评分函数阐明线性分类器损失函数多类SVMSoftmax分类器SVM和Softmax的比较基于Web的可交互线性分类器原型小结注:中文翻译 ... [详细]
author-avatar
你一句话就逼我撤退
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有