专栏首页PaddlePaddle基于飞桨PaddlePaddle的语义角色标注任务全解析

基于飞桨PaddlePaddle的语义角色标注任务全解析

自然语言处理中的自然语言句子级分析技术,可以大致分为词法分析、句法分析、语义分析三个层面。

  • 词法分析:第一层面的词法分析 (lexical analysis) 包括汉语分词和词性标注两部分。
  • 句法分析:对输入的文本句子进行分析以得到句子的句法结构的处理过程。
  • 语义分析 (semantic parsing):语义分析的最终目的是 理解句子表达的真实语义。语义角色标注是实现浅层语义分析的一种方式。在一个句子中,谓词是对主语的陈述或说明,指出「做什么」、「是什么」或「怎么样,代表了一个事件的核心,跟谓词搭配的名词称为论元。语义角色是指论元在动词所指事件中担任的角色。主要有:施事者(Agent)、受事者(Patient)、客体(Theme)、经验者(Experiencer)、受益者(Beneficiary)、工具(Instrument)、处所(Location)、目标(Goal)和来源(Source)等。

以上示例表示,「遇到」是谓词(Predicate,通常简写为「Pred」),「小明」是施事者(Agent),「小红」是受事者(Patient),「昨天」是事件发生的时间(Time),「公园」是事情发生的地点(Location)。

语义角色标注(Semantic Role Labeling,SRL)以句子的谓词为中心,不对句子所包含的语义信息进行深入分析,只分析句子中各成分与谓词之间的关系,即句子的谓词(Predicate)- 论元(Argument)结构,并用语义角色来描述这些结构关系,是许多自然语言理解任务(如信息抽取,篇章分析,深度问答等)的一个重要中间步骤。

基于语块(chunk)的 SRL 方法

基于语块的 SRL 方法将 SRL [1] 作为一个序列标注问题来解决。序列标注任务一般都会采用 BIO 表示方式来定义序列标注的标签集,我们先来介绍这种表示方法。在 BIO 表示法中,B 代表语块的开始,I 代表语块的中间,O 代表语块结束。通过 B、I、O 三种标记将不同的语块赋予不同的标签,例如:对于一个由角色 A 拓展得到的语块组,将它所包含的第一个语块赋予标签 B-A,将它所包含的其它语块赋予标签 I-A,不属于任何论元的语块赋予标签 O。

从上面的例子可以看到,根据序列标注结果可以直接得到论元的语义角色标注结果,是一个相对简单的过程。这种简单性体现在:(1)依赖浅层句法分析,降低了句法分析的要求和难度;(2)没有了候选论元剪除这一步骤;(3)论元的识别和论元标注是同时实现的。这种一体化处理论元识别和论元标注的方法,简化了流程,降低了错误累积的风险,往往能够取得更好的结果。

深度双向 LSTM(DB-LSTM)SRL 模型

与基于语块的 SRL 方法类似,在本文中我们也将 SRL 看作一个序列标注问题,不同的是,我们只依赖输入文本序列,不依赖任何额外的语法解析结果或是复杂的人造特征,利用深度神经网络构建一个端到端学习的 SRL 系统。

循环神经网络(Recurrent Neural Network)是一种对序列建模的重要模型,在自然语言处理任务中有着广泛地应用。不同于前馈神经网络(Feed-forward Neural Network),RNN 能够处理输入之间前后关联的问题。LSTM 是 RNN 的一种重要变种,常用来学习长序列中蕴含的长程依赖关系,这里我们就使用 LSTM 来解决 SRL 问题。

在 SRL 任务中,输入是「谓词」和「一句话」,目标是从这句话中找到谓词的论元,并标注论元的语义角色。如果一个句子含有 n 个谓词,这个句子会被处理 n 次。一个最为直接的模型是下面这样:

  1. 构造输入;
  2. 输入 1 是谓词,输入 2 是句子;
  3. 将输入 1 扩展成和输入 2 一样长的序列,用 one-hot 方式表示;
  4. one-hot 方式的谓词序列和句子序列通过词表,转换为实向量表示的词向量序列;
  5. 将步骤 2 中的 2 个词向量序列作为双向 LSTM 的输入,学习输入序列的特征表示;
  6. CRF 以步骤 3 中模型学习到的特征为输入,以标记序列为监督信号,实现序列标注;

大家可以尝试上面这种方法。这里,我们提出一些改进,引入两个简单但对提高系统性能非常有效的特征:

  • 谓词上下文:上面的方法中,只用到了谓词的词向量表达谓词相关的所有信息,这种方法始终是非常弱的,特别是如果谓词在句子中出现多次,有可能引起一定的歧义。从经验出发,谓词前后若干个词的一个小片段,能够提供更丰富的信息,帮助消解歧义。于是,我们把这样的经验也添加到模型中,为每个谓词同时抽取一个「谓词上下文」片段,也就是从这个谓词前后各取 n 个词构成的一个窗口片段;
  • 谓词上下文区域标记:为句子中的每一个词引入一个 0-1 二值变量,表示它们是否在「谓词上下文」片段中;

修改后的模型如下(下图是一个深度为 4 的模型结构示意图):

  1. 构造输入;
  2. 输入 1 是句子序列,输入 2 是谓词序列,输入 3 是谓词上下文,从句子中抽取这个谓词前后各 n 个词,构成谓词上下文,用 one-hot 方式表示,输入 4 是谓词上下文区域标记,标记了句子中每一个词是否在谓词上下文中;
  3. 将输入 2~3 均扩展为和输入 1 一样长的序列;
  4. 输入 1~4 均通过词表取词向量转换为实向量表示的词向量序列;其中输入 1、3 共享同一个词表,输入 2 和 4 各自独有词表;
  5. 第 2 步的 4 个词向量序列作为双向 LSTM 模型的输入;LSTM 模型学习输入序列的特征表示,得到新的特性表示序列;
  6. CRF 以第 3 步中 LSTM 学习到的特征为输入,以标记序列为监督信号,完成序列标注;

图:SRL 任务上的深层双向 LSTM 模型

接下来,我们就一步一步的实践这个任务:给定一句话和这句话里的一个谓词,通过序列标注的方式,从句子中找到谓词对应的论元,同时标注它们的语义角色。

数据介绍

在本文中,我们选用 CoNLL 2005SRL 任务开放出的数据集作为示例。需要特别说明的是,CoNLL 2005 SRL 任务的训练数集和开发集在比赛之后并非免费进行公开,目前,能够获取到的只有测试集,包括 Wall Street Journal 的 23 节和 Brown 语料集中的 3 节。在本文中,我们以测试集中的 WSJ 数据为训练集来讲解模型。但是,由于测试集中样本的数量远远不够,如果希望训练一个可用的神经网络 SRL 系统,请考虑付费获取全量数据。

原始数据中同时包括了词性标注、命名实体识别、语法解析树等多种信息。我们使用 test.wsj 文件夹中的数据进行训练和测试,并只会用到 words 文件夹(文本序列)和 props 文件夹(标注结果)下的数据。本文使用的数据目录如下:

conll05st-release/
└── test.wsj
    ├── props  # 标注结果
    └── words  # 输入文本序列

标注信息源自 Penn TreeBank[2] 和 PropBank[3] 的标注结果。PropBank 标注结果的标签和我们在文章一开始示例中使用的标注结果标签不同,但原理是相同的,关于标注结果标签含义的说明,请参考论文 [4]。

原始数据需要进行数据预处理才能被飞桨处理,预处理包括下面几个步骤:

  1. 将文本序列和标记序列其合并到一条记录中;
  2. 一个句子如果含有 n 个谓词,这个句子会被处理 n 次,变成 n 条独立的训练样本,每个样本一个不同的谓词;
  3. 抽取谓词上下文和构造谓词上下文区域标记;
  4. 构造以 BIO 法表示的标记;
  5. 依据词典获取词对应的整数索引。

预处理完成之后一条训练样本数据包含 9 个域,分别是:句子序列、谓词、谓词上下文(占 5 列)、谓词上下区域标志、标注序列。下表是一条训练样本的示例。

除数据之外,我们同时提供了以下资源:

我们在英文维基百科上训练语言模型得到了一份词向量用来初始化 SRL 模型。在 SRL 模型训练过程中,词向量不再被更新。关于语言模型和词向量可以参考:

基于PaddlePaddle的词向量实战 | 深度学习基础任务教程系列(二)》。我们训练语言模型的语料共有 995,000,000 个 token,词典大小控制为 4900,000 词。CoNLL 2005 训练语料中有 5% 的词不在这 4900,000 个词中,我们将它们全部看作未登录词,用<unk>表示。

获取词典,打印词典大小:

from __future__ import print_function

import math, os
import numpy as np
import paddle
import paddle.dataset.conll05 as conll05
import paddle.fluid as fluid
import six
import time

with_gpu = os.getenv('WITH_GPU', '0') != '0'

word_dict, verb_dict, label_dict = conll05.get_dict()
word_dict_len = len(word_dict)
label_dict_len = len(label_dict)
pred_dict_len = len(verb_dict)

print('word_dict_len: ', word_dict_len)
print('label_dict_len: ', label_dict_len)
print('pred_dict_len: ', pred_dict_len)

模型配置说明

定义输入数据维度及模型超参数。

mark_dict_len = 2    # 谓上下文区域标志的维度,是一个0-1 2值特征,因此维度为2
word_dim = 32        # 词向量维度
mark_dim = 5         # 谓词上下文区域通过词表被映射为一个实向量,这个是相邻的维度
hidden_dim = 512     # LSTM隐层向量的维度 : 512 / 4
depth = 8            # 栈式LSTM的深度
mix_hidden_lr = 1e-3 # linear_chain_crf层的基础学习率

IS_SPARSE = True     # 是否以稀疏方式更新embedding
PASS_NUM = 10        # 训练轮数
BATCH_SIZE = 10      # batch size 大小

embedding_name = 'emb'

这里需要特别说明的是,参数 hidden_dim = 512 实际指定了 LSTM 隐层向量的维度为 128,关于这一点请参考飞桨官方文档中 dynamic_lstm 的说明。

如上文提到,我们用基于英文维基百科训练好的词向量来初始化序列输入、谓词上下文总共 6 个特征的 embedding 层参数,在训练中不更新。

# 这里加载PaddlePaddle保存的二进制参数
def load_parameter(file_name, h, w):
    with open(file_name, 'rb') as f:
        f.read(16)  # skip header.
        return np.fromfile(f, dtype=np.float32).reshape(h, w)

训练模型

我们根据网络拓扑结构和模型参数来进行训练,在构造时还需指定优化方法,这里使用最基本的 SGD 方法 (momentum 设置为 0),同时设定了学习率、正则等。

定义训练过程的超参数

use_cuda = False #在cpu上执行训练
save_dirname = "label_semantic_roles.inference.model" #训练得到的模型参数保存在文件中
is_local = True

数据输入层定义

定义了模型输入特征的格式,包括句子序列、谓词、谓词上下文的 5 个特征、和谓词上下区域标志

# 句子序列
word = fluid.layers.data(
    name='word_data', shape=[1], dtype='int64', lod_level=1)

# 谓词
predicate = fluid.layers.data(
    name='verb_data', shape=[1], dtype='int64', lod_level=1)

# 谓词上下文5个特征
ctx_n2 = fluid.layers.data(
    name='ctx_n2_data', shape=[1], dtype='int64', lod_level=1)
ctx_n1 = fluid.layers.data(
    name='ctx_n1_data', shape=[1], dtype='int64', lod_level=1)
ctx_0 = fluid.layers.data(
    name='ctx_0_data', shape=[1], dtype='int64', lod_level=1)
ctx_p1 = fluid.layers.data(
    name='ctx_p1_data', shape=[1], dtype='int64', lod_level=1)
ctx_p2 = fluid.layers.data(
    name='ctx_p2_data', shape=[1], dtype='int64', lod_level=1)

# 谓词上下区域标志
mark = fluid.layers.data(
    name='mark_data', shape=[1], dtype='int64', lod_level=1)

定义网络结构

首先预训练并定义模型输入层:

#预训练谓词和谓词上下区域标志
predicate_embedding = fluid.layers.embedding(
    input=predicate,
    size=[pred_dict_len, word_dim],
    dtype='float32',
    is_sparse=IS_SPARSE,
    param_attr='vemb')

mark_embedding = fluid.layers.embedding(
    input=mark,
    size=[mark_dict_len, mark_dim],
    dtype='float32',
    is_sparse=IS_SPARSE)

#句子序列和谓词上下文5个特征并预训练
word_input = [word, ctx_n2, ctx_n1, ctx_0, ctx_p1, ctx_p2]
# 因词向量是预训练好的,这里不再训练embedding表,
# 参数属性trainable设置成False阻止了embedding表在训练过程中被更新
emb_layers = [
    fluid.layers.embedding(
        size=[word_dict_len, word_dim],
        input=x,
        param_attr=fluid.ParamAttr(
            name=embedding_name, trainable=False)) for x in word_input
]
#加入谓词和谓词上下区域标志的预训练结果
emb_layers.append(predicate_embedding)
emb_layers.append(mark_embedding)

定义 8 个 LSTM 单元以「正向/反向」的顺序对所有输入序列进行学习。

# 共有8个LSTM单元被训练,每个单元的方向为从左到右或从右到左,
# 由参数`is_reverse`确定
# 第一层栈结构
hidden_0_layers = [
    fluid.layers.fc(input=emb, size=hidden_dim, act='tanh')
    for emb in emb_layers
]

hidden_0 = fluid.layers.sums(input=hidden_0_layers)

lstm_0 = fluid.layers.dynamic_lstm(
    input=hidden_0,
    size=hidden_dim,
    candidate_activation='relu',
    gate_activation='sigmoid',
    cell_activation='sigmoid')

# 用直连的边来堆叠L-LSTM、R-LSTM
input_tmp = [hidden_0, lstm_0]

# 其余的栈结构
for i in range(1, depth):
    mix_hidden = fluid.layers.sums(input=[
        fluid.layers.fc(input=input_tmp[0], size=hidden_dim, act='tanh'),
        fluid.layers.fc(input=input_tmp[1], size=hidden_dim, act='tanh')
    ])

    lstm = fluid.layers.dynamic_lstm(
        input=mix_hidden,
        size=hidden_dim,
        candidate_activation='relu',
        gate_activation='sigmoid',
        cell_activation='sigmoid',
        is_reverse=((i % 2) == 1))

    input_tmp = [mix_hidden, lstm]

# 取最后一个栈式LSTM的输出和这个LSTM单元的输入到隐层映射,
# 经过一个全连接层映射到标记字典的维度,来学习 CRF 的状态特征
feature_out = fluid.layers.sums(input=[
    fluid.layers.fc(input=input_tmp[0], size=label_dict_len, act='tanh'),
    fluid.layers.fc(input=input_tmp[1], size=label_dict_len, act='tanh')
])

# 标注序列
target = fluid.layers.data(
    name='target', shape=[1], dtype='int64', lod_level=1)

# 学习 CRF 的转移特征
crf_cost = fluid.layers.linear_chain_crf(
    input=feature_out,
    label=target,
    param_attr=fluid.ParamAttr(
        name='crfw', learning_rate=mix_hidden_lr))


avg_cost = fluid.layers.mean(crf_cost)

# 使用最基本的SGD优化方法(momentum设置为0)
sgd_optimizer = fluid.optimizer.SGD(
    learning_rate=fluid.layers.exponential_decay(
        learning_rate=0.01,
        decay_steps=100000,
        decay_rate=0.5,
        staircase=True))

sgd_optimizer.minimize(avg_cost)

数据介绍部分提到 CoNLL 2005 训练集付费,这里我们使用测试集训练供大家学习。conll05.test() 每次产生一条样本,包含 9 个特征,shuffle 和组完 batch 后作为训练的输入。

crf_decode = fluid.layers.crf_decoding(
    input=feature_out, param_attr=fluid.ParamAttr(name='crfw'))

train_data = paddle.batch(
    paddle.reader.shuffle(
        paddle.dataset.conll05.test(), buf_size=8192),
    batch_size=BATCH_SIZE)

place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

通过 feeder 来指定每一个数据和 data_layer 的对应关系, 下面的 feeder 表示 conll05.test() 产生数据的第 0 列对应的 data_layer 是 word。

feeder = fluid.DataFeeder(
    feed_list=[
        word, ctx_n2, ctx_n1, ctx_0, ctx_p1, ctx_p2, predicate, mark, target
    ],
    place=place)
exe = fluid.Executor(place)

开始训练

main_program = fluid.default_main_program()

exe.run(fluid.default_startup_program())
embedding_param = fluid.global_scope().find_var(
    embedding_name).get_tensor()
embedding_param.set(
    load_parameter(conll05.get_embedding(), word_dict_len, word_dim),
    place)

start_time = time.time()
batch_id = 0
for pass_id in six.moves.xrange(PASS_NUM):
    for data in train_data():
        cost = exe.run(main_program,
                       feed=feeder.feed(data),
                       fetch_list=[avg_cost])
        cost = cost[0]

        if batch_id % 10 == 0:
            print("avg_cost: " + str(cost))
            if batch_id != 0:
                print("second per batch: " + str((time.time(
                ) - start_time) / batch_id))
            # Set the threshold low to speed up the CI test
            if float(cost) < 60.0:
                if save_dirname is not None:
                    fluid.io.save_inference_model(save_dirname, [
                        'word_data', 'verb_data', 'ctx_n2_data',
                        'ctx_n1_data', 'ctx_0_data', 'ctx_p1_data',
                        'ctx_p2_data', 'mark_data'
                    ], [feature_out], exe)
                break

        batch_id = batch_id + 1

应用模型

训练完成之后,需要依据某个我们关心的性能指标选择最优的模型进行预测,可以简单的选择测试集上标记错误最少的那个模型。以下我们给出一个使用训练后的模型进行预测的示例。

首先设置预测过程的参数

use_cuda = False #在cpu上进行预测
save_dirname = "label_semantic_roles.inference.model" #调用训练好的模型进行预测

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

设置输入,用 LoDTensor 来表示输入的词序列,这里每个词的形状 base_shape 都是 [1],是因为每个词都是用一个 id 来表示的。假如基于长度的 LoD 是 [[3, 4, 2]],这是一个单层的 LoD,那么构造出的 LoDTensor 就包含 3 个序列,其长度分别为 3、4 和 2。

注意 LoD 是个列表的列表:

lod = [[3, 4, 2]]
base_shape = [1]

# 构造假数据作为输入,整数随机数的范围是[low, high]
word = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=word_dict_len - 1)
pred = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=pred_dict_len - 1)
ctx_n2 = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=word_dict_len - 1)
ctx_n1 = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=word_dict_len - 1)
ctx_0 = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=word_dict_len - 1)
ctx_p1 = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=word_dict_len - 1)
ctx_p2 = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=word_dict_len - 1)
mark = fluid.create_random_int_lodtensor(
    lod, base_shape, place, low=0, high=mark_dict_len - 1)

使用 fluid.io.load_inference_model 加载 inference_program,feed_target_names 是模型的输入变量的名称,fetch_targets 是预测对象。

[inference_program, feed_target_names,
 fetch_targets] = fluid.io.load_inference_model(save_dirname, exe)

构造 feed 字典 {feed_target_name: feed_target_data},results 是由预测目标构成的列表:

assert feed_target_names[0] == 'word_data'
assert feed_target_names[1] == 'verb_data'
assert feed_target_names[2] == 'ctx_n2_data'
assert feed_target_names[3] == 'ctx_n1_data'
assert feed_target_names[4] == 'ctx_0_data'
assert feed_target_names[5] == 'ctx_p1_data'
assert feed_target_names[6] == 'ctx_p2_data'
assert feed_target_names[7] == 'mark_data'

执行预测

results = exe.run(inference_program,
                  feed={
                      feed_target_names[0]: word,
                      feed_target_names[1]: pred,
                      feed_target_names[2]: ctx_n2,
                      feed_target_names[3]: ctx_n1,
                      feed_target_names[4]: ctx_0,
                      feed_target_names[5]: ctx_p1,
                      feed_target_names[6]: ctx_p2,
                      feed_target_names[7]: mark
                  },
                  fetch_list=fetch_targets,
                  return_numpy=False)

输出结果

print(results[0].lod())
np_data = np.array(results[0])
print("Inference Shape: ", np_data.shape)

总结

语义角色标注是许多自然语言理解任务的重要中间步骤。本文我们以语义角色标注任务为例,介绍如何利用飞桨进行序列标注任务。本文所介绍的模型来自我们发表的论文 [5]。由于 CoNLL 2005 SRL 任务的训练数据目前并非完全开放,本文中只使用测试数据作为示例。在这个过程中,我们希望减少对其它自然语言处理工具的依赖,利用神经网络数据驱动、端到端学习的能力,得到一个和传统方法可比、甚至更好的模型。在论文中我们证实了这种可能性。关于模型更多的信息和讨论可以在论文中找到。

  • 论文地址:https://www.aclweb.org/anthology/P15-1109
  • 项目地址:https://github.com/PaddlePaddle/book/blob/develop/07.label_semantic_roles/README.cn.md

参考文献

1. Sun W, Sui Z, Wang M, et al. Chinese semantic role labeling with shallow parsing[C]//Proceedings of the 2009 Conference on Empirical Methods in Natural Language Processing: Volume 3-Volume 3. Association for Computational Linguistics, 2009: 1475-1483.

2. Marcus M P, Marcinkiewicz M A, Santorini B. Building a large annotated corpus of English: The Penn Treebank[J]. Computational linguistics, 1993, 19(2): 313-330.

3. Palmer M, Gildea D, Kingsbury P. The proposition bank: An annotated corpus of semantic roles[J]. Computational linguistics, 2005, 31(1): 71-106.

4. Carreras X, Màrquez L. Introduction to the CoNLL-2005 shared task: Semantic role labeling[C]//Proceedings of the Ninth Conference on Computational Natural Language Learning. Association for Computational Linguistics, 2005: 152-164.

5. 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.

本文分享自微信公众号 - PaddlePaddle(PaddleOpenSource)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 用飞桨做命名实体识别,手把手教你实现经典模型 BiGRU + CRF

    命名实体识别(Named Entity Recognition,NER)是 NLP 几个经典任务之一,通俗易懂的来说,就是从一段文本中抽取出需求的关键词,如地名...

    用户1386409
  • 都9102年了还不懂动态图吗?一文带你了解飞桨动态图

    导读:飞桨PaddlePaddle致力于让深度学习技术的创新与应用更简单。飞桨核心框架已提供了动态图(DyGraph)相关的API和文档,并且还附有Langua...

    用户1386409
  • 拆解式解读如何用飞桨复现胶囊神经网络(Capsule Network)

    【飞桨开发者说】王成,深度学习爱好者,淮阴师范学院,研究方向为计算机视觉图像与视频处理。

    用户1386409
  • 2017年公有云成潮流,云支出是重点

    T客汇官网:tikehui.com 撰文 | 徐婧欣 ? 核心提示:2016 年是云计算火热发展的一年,而在 2017 年还会持续这样的势头,那么云计算发展的...

    人称T客
  • 从一个优秀开源项目来谈前端架构

    Peter谭金杰
  • JS 判断字符串是否为手机号

    week
  • 基于 xorm 的服务端框架 XGoServer

    作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:htt...

    林冠宏-指尖下的幽灵
  • 简单聊聊Python中的wraps修饰

    在了解wraps修饰器之前,我们首先要了解partial和update_wrapper这两个函数,因为在wraps的代码中,用到了这两个函数。

    py3study
  • 基于 xorm 的服务端框架 XGoServer

    开源地址:https://github.com/af913337456/XGoServer

    林冠宏-指尖下的幽灵
  • PHP变量类型测试函数的使用:二、is_int的用法

    昨天刚讲完PHP变量类型测试函数【is_bool】的用法,今天来讲讲PHP变量类型测试函数【is_int】的用法。

    大脸猫

扫码关注云+社区

领取腾讯云代金券