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

(全)WordEmbedding

原文链接:https:blog.csdn.netL_R_H000articledetails81320286最近做完UNIT一个小项目后,结合同时期看K

原文链接:https://blog.csdn.net/L_R_H000/article/details/81320286

最近做完UNIT一个小项目后,结合同时期看KBQA的文章,对NLP/NLU方向产生了比较大的兴趣,想深入学习一下,结合一篇综述Recent Trends in Deep Learning Based Natural Language Processing(参考文献[5]为其阅读笔记)的阐述顺序,把相关的知识补一补,本文即第一部分Word Embedding。

主要参考文献:

[1] word2vec 中的数学原理详解

[2] Word Embedding与Word2Vec

[3] 自然语言处理中的N-Gram模型详解

[4] 有谁可以解释下word embedding?——知乎

[5] 2017-基于DL的NLP研究近况

目录

一、Word Embedding概述

二、Word2vec之前

2.1 one-hot

2.2 n-gram

2.3 co-occurrence matrix

2.4 NLM

三、Word2vec

3.1 CBOW

3.1.1 基于Hierarchical Softmax

3.1.2 基于Negative Sampling

3.2 Skip-gram

3.2.1 基于Hierarchical Softmax

3.2.2 基于Negative Sampling


一、Word Embedding概述

简单来说,词嵌入(Word Embedding)或者分布式向量(Distributional Vectors)是将自然语言表示的单词转换为计算机能够理解的向量或矩阵形式的技术。由于要考虑多种因素比如词的语义(同义词近义词)、语料中词之间的关系(上下文)和向量的维度(处理复杂度)等等,我们希望近义词或者表示同类事物的单词之间的距离可以理想地近,只有拿到很理想的单词表示形式,我们才更容易地去做翻译、问答、信息抽取等进一步的工作。

在Word Embedding之前,常用的方法有one-hot、n-gram、co-occurrence matrix,但是他们都有各自的缺点,下面会说明。2003年,Bengio提出了NLM,是为Word Embedding的想法的雏形,而在2013年,Mikolov对其进行了优化,即Word2vec,包含了两种类型,Continuous Bag-of-Words Model 和 skip-gram model。

Word Embedding是基于分布式假设(distributional hypothesis):

总的来说,word embedding就是一个词的低维向量表示(一般用的维度可以是几十到几千)。有了一个词的向量之后,各种基于向量的计算就可以实施,如用向量之间的相似度来度量词之间的语义相关性。其基于的分布式假设就是出现在相同上下文(context)下的词意思应该相近所有学习word embedding的方法都是在用数学的方法建模词和context之间的关系。


作者:李明磊9527
链接:https://www.zhihu.com/question/32275069/answer/197721342
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

但是Word Embedding也有其局限性, 比如:

  1. 难以对词组做分布式表达
  2. 受限于上下文window的尺寸,有些词(例如好或坏)的上下文可能没什么不同甚至完全一样,这对情感分析任务的影响非常大

 此外,Word Embedding对于应用场景的依赖很强,所以针对特殊的应用场景可能需要重新训练,这样就会很消耗时间和资源,为此Bengio提出了基于负采样(negative sampling)的模型。

下面本文会将对Word2vec之前的常用方法和Word2vec的两种模型做比较详细的记录和理解。

二、Word2vec之前

2.1 one-hot

one-hot是最简单的一种处理方式。通俗地去讲,把语料中的词汇去重取出,按照一定的顺序(字典序、出现顺序等)排列为词汇表,则每一个单词都可以表示为一个长度为N的向量,N为词汇表长度,即单词总数。该向量中,除了该词所在的分量为1,其余均置为0。

例如,有语料库如下:

John likes to watch movies. Mary likes movies too.

John also likes to watch football games.

假设我们的词汇表排序结果如下:

{"John": 1, "likes": 2, "to": 3, "watch": 4, "movies": 5, "also":6, "football": 7, "games": 8, "Mary": 9, "too": 10}

那么则有如下word的向量表示:

John: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

likes: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]

……

 用这样的方式可以利用向量相加进一步表示句子和文本了,但是one-hot有很大的局限性:

  1. 语义的相似性,“woman”、“madam”、“lady”从语义上将可能是相近的,one-hot无法表示
  2. 英语单词中的复数时态,我们不会在排序是就把同一单词的不同形态区别开来,继而再进行向量表示
  3. 单词之间的位置关系,很多时候句内之间多个单词(比如术语)会同时出现多次,one-hot无法表示
  4. 词向量长度很大,一方面2的原因,另一方面本身大规模语料所含的词数很多,处理会很棘手

2.2 n-gram

n-gram可以表示单词间的位置关系所反映的语义关联,在说明n-gram之前,我们从最初的句子概率进行推导。

假设一个句子S为n个单词有序排列,记为:

                                                                           \LARGE S=\overline{w_{1}w_{2}...w_{n}}

我们将其简记为 W_{1}^{n},则这个句子的概率为:

                                      \LARGE p(W_{1}^{n})=p(\overline{w_{1}w_{2}...w_{n}})\\ =p(w_{1})p(w_{2}|w_1)p(w_3|w_1w_2)...p(w_n|w_1w_2...w_{n-1})\\ =p(w_1)\prod_{i=2}^{n}p(w_i|W_{1}^{i-1})

对于单个概率意思为该单词在前面单词给定的情况下出现的概率,我们利用贝叶斯公式可以得到:

                                       \LARGE p(w_i|W_{1}^{i-1})=\tfrac{p(\overline{W_{1}^{i-1}w_i})}{p(W_{1}^{i-1})}=\tfrac{p(W_{1}^{i})}{p(W_{1}^{i-1})}=\tfrac{count(W_{1}^{i})}{count(W_{1}^{i-1})}

其中最后一项为W_{1}^{i}在语料中出现的频数。但是长句子或者经过去标点处理后的文本可能很长,而且太靠前的词对于词的预测影响不是很大,于是我们利用马尔可夫假设,取该词出现的概率仅依赖于该词前面的n-1个词,这就是n-gram模型的思想。

所以上面的公式变为:

                                        \LARGE p(w_i|W_{1}^{i-1})\approx p(w_i|W_{i-(n-1)}^{i-1})=\tfrac{count(W_{i-n+1}^{i})}{count(W_{i-n+1}^{i-1})}

在这里,我们不对n的确定做算法复杂度上的讨论,详细请参考文献[1],一般来说,n取3比较合适。此外对于一些概率为0的情况所出现的稀疏数据,采用平滑化处理,此类算法很多,以后有时间再具体展开学习。

所以n-gram的主要工作在于确定n之后,对语料中的各种吃词串进行频数统计和平滑化处理,对于所需要的句子概率,只要将之前语料中相关概率取出计算就可以了。

当然实际情况是对\large p(w_i|W_{i-(n-1)}^{i-1})做最优化处理,参数确定后以后的概率就可以通过函数确定了,这就需要构造函数,后面的NLM就是做这个工作。

n-gram模型会将前文的语义关联纳入考虑,从而形成联合分布概率表达,但是尽管去前n-1个单词,语料大的情况下计算量还是很大,在模拟广义情境时严重受到了“维度灾难(curse of dimensionality)”。

2.3 co-occurrence matrix

共现矩阵也是考虑语料中词之间的关系来表示:

一个非常重要的思想是,我们认为某个词的意思跟它临近的单词是紧密相关的。这是我们可以设定一个窗口(大小一般是5~10),如下窗口大小是2,那么在这个窗口内,与rests 共同出现的单词就有life、he、in、peace。然后我们就利用这种共现关系来生成词向量。

例如,现在我们的语料库包括下面三份文档资料:

I like deep learning.

I like NLP.

I enjoy flying.

作为示例,我们设定的窗口大小为1,也就是只看某个单词周围紧邻着的那个单词。此时,将得到一个对称矩阵——共现矩阵。因为在我们的语料库中,I 和 like做为邻居同时出现在窗口中的次数是2,所以下表中I 和like相交的位置其值就是2。这样我们也实现了将word变成向量的设想,在共现矩阵每一行(或每一列)都是对应单词的一个向量表示。

虽然Cocurrence matrix一定程度上解决了单词间相对位置也应予以重视这个问题。但是它仍然面对维度灾难。也即是说一个word的向量表示长度太长了。这时,很自然地会想到SVD或者PCA等一些常用的降维方法。当然,这也会带来其他的一些问题。

 窗口大小的选择跟n-gram中确定n也是一样的,窗口放大则矩阵的维度也会增加,所以本质上还是带有很大的计算量,而且SVD算法运算量也很大,若文本集非常多,则不具有可操作性。

2.4 NLM

神经语言模型(Neural Language Model)是Word Embeddings的基本思想,在很多其他文献中也有神经概率语言模型(Neural Probabilistic Language Model,NPLM)或者神经网络语言模型(Neural Network Language Model,NNLM),都是指一个东西。

NLM的输入是词向量,根据参考文献[1],词向量和模型参数(最终的语言模型)可以通过神经网络训练一同得到。相比于n-gram通过联合概率考虑词之间的位置关系,NLM则是利用词向量进一步表示词语之间的相似性,比如近义词在相似的上下文里可以替代,或者同类事物的词可以在语料中频数不同的情况下获得相近的概率。结合参考文献[1],举一个简单例子:

在一个语料C中,S1=“A dog is sitting in the room.”共出现了10000次,S2="A cat is sitting in the room"出现了1次,按照n-gram的模型,当我们输入“A _____ is sitting in the room”来预测下划线上应该填入的词时,dog的概率会远大于cat,这是针对于语料C得到的概率。但是我们希望相似含义的词在目标向量空间中的距离比不相关词的距离更近,比如v(man)-v(woman)约等于v(gentleman)-v(madam),用这样生成的词向量或者已经训练好的模型在去做翻译、问答等后续工作时,就会很有效果,而NLM利用词向量表示就能达到这样的效果。

注:在参考文献[1]中,作者举的例子是从句子概率角度,我自己的理解稍有不同,将原例放在下面:

 

NLM的神经网络训练样本同n-gram的取法,取语料中任一词w的前n-1个词作为Context(w),则(Context(w),w)就是一个训练样本了。这里的每一个词都被表示为一个长度为L的词向量,然后将Context(w)的n-1个词向量首位连接拼成(n-1)L的长向量。下面为NLM图解:

【注】此图向量和矩阵的维度与参考文献中相反了

我们得到的输出结果为长度为词汇总数的向量,如果想要第i个分量去表示当上下为context(w)时下一个词为词典中第i个词的概率,还需要softmax归一化,然后我们最初想要的结果便是:

                                                                     \LARGE p(w|Context(w))=\tfrac{e^{y_{index_{w}}}}{\sum_{i=1}^{D}e^{y_i}}

注意:这只是取一个词w后输出的向量y,我们需要的就是通过训练集所有的词都做一遍这个过程来优化得到理想的W,q和U,b

那么样本中最初的词向量如何获得呢?在参考文献[1]中有这样两段话:


 

目前我还没有彻底搞懂神经网络中具体的机制,所以暂时标记一下,初步推测是初始化一个矩阵或者可以粗暴地用one-hot(不过这样输入层的L=D,计算量大了很多),然后随着训练的过程,词向量也是不断更新的,详细还要参考最优化理论

下面要说的Word2vec便是在NLM基础上的优化。

三、Word2vec

目前学习了解到的Word2vec有基于Hierarchical Softmax和基于Negative Sampling两种方式,参考文献[1]是从两种方式分别讲解了CBOW和Skip-gram的数学构建思路和过程,由于这两个模型是相反的过程,即CBOW是在给定上下文基础上预测中心词,Skip-gram在有中心词后预测上下文,我个人是把两个模型按照两种不同的计算方法做了梳理,当然数学推导还是一样的,只不过我自己看起来更舒服。在此再次感谢@peghoty大牛的详解。

3.1 CBOW

基于前面的介绍,CBOW的思想是取目标词w的上下文(前后相邻词)而不是仅之前的词作为预测前提,类似于共现矩阵的窗口,不同于NLM的是,Context(w)的向量不再是前后连接,而是求和,我们记为\large \mathbf{x}_w,此外还将NLM的隐藏层去掉了。当然最大的区别还是在输出层,基于Hierarchical Softmax的CBOW输出层为一颗霍夫曼树,叶子节点为语料中的词汇,构建依据便是各词的出现频数;基于Negative Sampling则是用随机负采样代替霍夫曼树的构建。

3.1.1 基于Hierarchical Softmax

霍夫曼树的构建在这里就不展开说了,比较简单的算法。沿用文献[1]的表示,基于Hierarchical Softmax的CBOW所要构建的霍夫曼树所需参数如下:

\LARGE p^{w}:从根结点到w对应结点的路径

\LARGE n^{w}:路径上包含结点个数

\LARGE N_{1}^{w},N_{2}^{w},...,N_{n^{w}}^{w}:到w路径上的的结点

\LARGE d_{2}^{w},d_{3}^{w},...,d_{n^w}\in \{0,1\}:结点编码,根结点不编码

\LARGE \theta _{1}^{w},\theta _{2}^{w},...,\theta _{n^w-1}^{w}\in \mathbb{R}^L:非叶子结点(包括根结点)对应的向量

霍夫曼树构建按照频数大小有左右两种,其实都是自己约定的,在这里就不麻烦了,构建后左结点编码为0,为正类,右结点为1,为负类。

根据逻辑回归,一个结点被分为正类的概率为

                                                                                 \LARGE \sigma (\mathbf{x}_{w}^{\top}\theta_{i}^{w})=\tfrac{1}{1+e^{-\mathbf{x}_{w}^{\top}\theta_{i}^{w}}}

\LARGE \sigma的一些性质,后面用的到:

                                                                               \LARGE 1-\sigma(x)=\sigma(-x)

所以之前我们要构造的目标函数就可以写为以下形式:

                                                     \LARGE p(w|Context(w))=\prod_{i=2}^{N^w}p(d_{i}^{w}|\mathbf{x}_w,\theta_{i-1}^{w})

这个公式跟之前看的概率图模型有点像,不过现在有点记不清了,后面我再梳理一下,看看能不能串起来。其中

                                                 \LARGE p(d_{i}^{w}|\mathbf{x}_w,\theta_{i-1}^{w})=\left\{\begin{matrix}\sigma(\mathbf{x}_{w}^{\top}\theta_{i-1}^{w}), d_i^w=0; \\ \\1-\sigma(\mathbf{x}_{w}^{\top}\theta_{i-1}^{w}),d_i^w=1. \end{matrix}\right.

整体表达式

                                         \LARGE p(d_{i}^{w}|\mathbf{x}_w,\theta_{i-1}^{w})=[\sigma(\mathbf{x}_{w}^{\top}\theta_{i-1}^{w})]^{1-d_i^w}\ast [1-\sigma(\mathbf{x}_{w}^{\top}\theta_{i-1}^{w})]^{d_i^w}

这是一个单词,我们把对连乘做对数似然函数,然后将语料中所有单词都求和,则目标函数如下:

                                             \large \L =\sum_{w\in C}\sum_{i=2}^{N^w}\{(1-d_i^w)\log[\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]+d_i^w\log[1-\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]\}

明确参数有\large x\large \theta,我们取其中子式来做关于两个参数的梯度:

                                         \large \frac{\partial \L (w,i)}{\partial \theta_{i-1}^w}=\frac{\partial }{\partial \theta_{i-1}^w}\{(1-d_i^w)\log[\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]+d_i^w\log[1-\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]\}\\ \\ =(1-d_i^w)[1-\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]x_w+d_i^w\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)\mathbf{x}_w\\ \\ =[1-d_i^w-\sigma(x_w^{\top}\theta_{i-1}^w)]\mathbf{x}_w

因为\large x\large \theta是对称的,所以\large x的为:

                                                                   \large \frac{\partial \L (w,i)}{\partial \mathbf{x}_w} =[1-d_i^w-\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]\theta_{i-1}^w

所以两者就可以更新了:

                                                      \LARGE \theta_{i-1}^w=\theta_{i-1}^w+\eta [1-d_i^w-\sigma(\mathbf{x}_w^{\top}\theta_{i-1}^w)]\mathbf{x}_w

                                           \LARGE \mathbf{v}(\tilde{w})=\mathbf{v}(\tilde{w})+\eta\sum_{i=2}^{N^w}\frac{\partial \L (w,i)}{\partial \mathbf{x}_w},\tilde{w}\in Context(w)

至此,我们完成了对参数的优化。

参考文献[1]提出了这样一个问题:

 

 我的理解是可以的,可能我对最优化方法的学习还不够全面,从公式拆解上好像更能说的过去,但是对于收敛速度的影响可能会很大,取平均可能优化得到较好的结果较慢。

3.1.2 基于Negative Sampling

对于大规模语料,构建霍夫曼树的工作量是巨大的,而且叶子节点为N的霍夫曼数需要新添(N-1)个结点,而随着树的深度增加,参数计算的量也会增加很多很多,得到的词向量也会不够好,为此,Mikolov作出了优化,将构建霍夫曼树改为随机负采样方法。

对于给定的上下文Context(w)去预测w,如果从语料中就是存在(Context(w),w),那么w就是正样本,其他词就是负样本。

我们设负样本集为\large N(w),词的标签:

                                                                         \LARGE L^w(\tilde{w})=\left\{\begin{matrix} 1,\tilde{w}=w \\ 0,\tilde{w}\neq w \end{matrix}\right.

即正样本标签为1,负样本标签为0,等同于霍夫曼结点的左右编码,只不过与其取值相反,这样后面的公式也就很好理解了:

                                                      \LARGE g(w)=\sigma(\mathbf{x}_w^{\top}\theta ^w)\prod_{u \in N(w)}[1-\sigma(\mathbf{x}_w^{\top}\theta ^u)]

                                            \large \L =\log\prod_{w \in C}g(w) \\ =\sum_{w\in C}\sum_{u\in \{w\}\cup N(w)} \{L^w(u)\log[\sigma(\mathbf{x}_w^{\top}\theta^u)]+[1-L^w(u)]\log[1-\sigma(\mathbf{x}_w^{\top}\theta^u)]\}

同样,我们对两个参数求导:

                                                           \LARGE \frac{\partial \L(w,u)}{\partial \theta^u}=[L^w(u)-\sigma(\mathbf{x}_w^{\top}\theta^{u})]\mathbf{x}_w

                                                           \LARGE \frac{\partial \L(w,u)}{\partial \mathbf{x}_w}=[L^w(u)-\sigma(\mathbf{x}_w^{\top}\theta^{u})]\theta^u

然后更新参数,公式形式是一样的,不再写了。

可见,对于单词w,基于Hierarchical Softmax将其频数用来构建霍夫曼树,正负样本标签取自结点左右编码;而基于Negative Sampling将其频数作为随机采样线段的子长度,正负样本标签取自从语料中随机取出的词是否为目标词,构造复杂度小于前者。

3.2 Skip-gram

由于Skip-gram是CBOW的相反操作,输入输出稍有不同,在这里仅贴出关键公式,不再具体说明。

3.2.1 基于Hierarchical Softmax

 

以上均来自参考文献[1],变量表示稍有不同。 

3.2.2 基于Negative Sampling

这个作者分析较多,还没有完全看懂,后面再补

 

 



推荐阅读
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 在PHP中如何正确调用JavaScript变量及定义PHP变量的方法详解 ... [详细]
  • 独家解析:深度学习泛化理论的破解之道与应用前景
    本文深入探讨了深度学习泛化理论的关键问题,通过分析现有研究和实践经验,揭示了泛化性能背后的核心机制。文章详细解析了泛化能力的影响因素,并提出了改进模型泛化性能的有效策略。此外,还展望了这些理论在实际应用中的广阔前景,为未来的研究和开发提供了宝贵的参考。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 本文详细探讨了使用纯JavaScript开发经典贪吃蛇游戏的技术细节和实现方法。通过具体的代码示例,深入解析了游戏逻辑、动画效果及用户交互的实现过程,为开发者提供了宝贵的参考和实践经验。 ... [详细]
  • 本文深入探讨了JavaScript中`this`关键字的多种使用方法和技巧。首先,分析了`this`作为全局变量时的行为;接着,讨论了其在对象方法调用中的表现;然后,介绍了`this`在构造函数中的作用;最后,详细解释了通过`apply`等方法改变`this`指向的机制。文章旨在帮助开发者更好地理解和应用`this`关键字,提高代码的灵活性和可维护性。 ... [详细]
  • com.sun.javadoc.PackageDoc.exceptions()方法的使用及代码示例 ... [详细]
  • javascript分页类支持页码格式
    前端时间因为项目需要,要对一个产品下所有的附属图片进行分页显示,没考虑ajax一张张请求,所以干脆一次性全部把图片out,然 ... [详细]
  • 在《Linux高性能服务器编程》一书中,第3.2节深入探讨了TCP报头的结构与功能。TCP报头是每个TCP数据段中不可或缺的部分,它不仅包含了源端口和目的端口的信息,还负责管理TCP连接的状态和控制。本节内容详尽地解析了TCP报头的各项字段及其作用,为读者提供了深入理解TCP协议的基础。 ... [详细]
  • ### 优化后的摘要本学习指南旨在帮助读者全面掌握 Bootstrap 前端框架的核心知识点与实战技巧。内容涵盖基础入门、核心功能和高级应用。第一章通过一个简单的“Hello World”示例,介绍 Bootstrap 的基本用法和快速上手方法。第二章深入探讨 Bootstrap 与 JSP 集成的细节,揭示两者结合的优势和应用场景。第三章则进一步讲解 Bootstrap 的高级特性,如响应式设计和组件定制,为开发者提供全方位的技术支持。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • POJ 2482 星空中的星星:利用线段树与扫描线算法解决
    在《POJ 2482 星空中的星星》问题中,通过运用线段树和扫描线算法,可以高效地解决星星在窗口内的计数问题。该方法不仅能够快速处理大规模数据,还能确保时间复杂度的最优性,适用于各种复杂的星空模拟场景。 ... [详细]
  • 本文探讨了使用JavaScript在不同页面间传递参数的技术方法。具体而言,从a.html页面跳转至b.html时,如何携带参数并使b.html替代当前页面显示,而非新开窗口。文中详细介绍了实现这一功能的代码及注释,帮助开发者更好地理解和应用该技术。 ... [详细]
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社区 版权所有