自然语言处理中的Attention机制总结[通俗易懂]

自然语言处理中的Attention机制总结[通俗易懂]    在面试的过程中被问到了attention,原来虽然其实已经实际用过attention了,也知道个大概原理是加权求和,但是对于加权的具体方法以及权值得分的计算并不是很清晰,面试答的一般,正好最近实习的地方

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

       在面试的过程中被问到了attention,原来虽然其实已经实际用过attention了,也知道个大概原理是加权求和,但是对于加权的具体方法以及权值得分的计算并不是很清晰,面试答的一般,正好最近实习的地方要讲attention机制,所以就跟着多学习了一下,在此做一个总结。

       因为这篇文章基本上是参考了很多前人的总结性文章,所以我先把参考的文章放到前面,觉得写得不是很清楚的地方的可以直接去看我参考的文章来看,

参考文章

  1. 斯坦福大学的课件
  2. 博客之一
  3. 博客之二
  4. 博客之三
  5. 博客之四
  6. 雷锋网文章
  7. 苏剑林科学空间
    感谢以上的作者

本篇文章讲解的整体结构

  1. seq2seq中的简单attention机制
  2. attention机制的通用定义(Stanford课件中的定义)
  3. attention的各种变体
  4. 更多attention种类
  5. 示例代码
  6. 总结
    下面就开始正式开始博客内容。

1. seq2seq中的attention机制

       首先我们快速过一下seq2seq中的attention机制,要了解seq2seq模型的话,不妨去参考上面的雷锋网那篇文章,这里我就快速过一下。下面的图都是seq2seq的示意图,先放的图,然后再放的图片描述:
这里写图片描述

  • 我们在encoder的过程中保留每个RNN单元的隐藏状态(hidden state)得到(h1……hN)
  • 然后对于decoder的每一个timestep,因为有此时decoder的输入和上一步的隐藏状态输出,所以我们可以得到当前步的隐藏状态。假设第t步的(根据上一步隐藏状态输出与当前输入得到的)隐藏状态为St,
    在每个第t步利用St和hi进行dot点积得到attention score,也称为“相似度“或者“影响度”,或者“匹配得分”
    PS.(注意,看到多个网友提出这里的attention score应该用st-1和hi来算。其实最开始的论文在encoder-decoder里面的当前decoder的attention得分用的是st-1和hi来算,但斯坦福教材上图上确实是画的st和hi来算,而且后续论文大多是用的这种方式,即当前步的attention score用的当前步的隐藏状态st和前面的hi去算的,感兴趣的可以看下面的论文,自己理解吧,对应的TensorFlow.contrib.seq2seq里面的两种attention机制。
    原始论文:https://arxiv.org/pdf/1409.0473.pdf 对应tf.contrib.seq2seq.BahdanauAttention 计算方式St-1->at->ct->St
    后面论文:https://arxiv.org/pdf/1508.04025.pdf 对应tf.contrib.seq2seq.LuongAttention 计算方式(St-1,input)->St->at->ct->S’t
    我觉得这种细节性的地方,因为我参考的是斯坦福的教程,所以后面都统一的attention score是使用st和hi来的,大家仁者见仁智者见智,不用喷的那么厉害,我是个彩笔我承认好吧,大家合理讨论就好)

    这里写图片描述
    α t = s o f t m a x ( e t ) {
    {\alpha }^{t}}=softmax ({
    {e}^{t}})
    αt=softmax(et)

    利用softmax函数将attention scores转化为概率分布。
    这里写图片描述
    a t = ∑ i N α i h i {
    {\mathrm{a}}_{\mathrm{t}}}=\sum\limits_{i}^{N}{
    {
    {\alpha }_{i}}}{
    {\mathrm{h}}_{\mathrm{i}}}
    at=iNαihi

    按照刚才的概率分布,计算encoder的hidden states的加权求和。到这里其实attention的计算就结束了,得到的这个at就已经是decoder的第t时刻的注意力向量了(在后面的文章中,这个 a t \mathrm{a}_{t} at也称作是上下文向量,context vector,符号表示也可能是用 c t c_{t} ct来表示的)。
    这里写图片描述
    最后将注意力向量at,以及decoder的t时刻的hidden state st,并联起来,然后做后续的步骤(比如加个dense全连接层做标签预测之类的。
    到这里,seq2seq的最简单最基础的attention机制,就讲完了,如果还有疑问的同学,可以去看原始论文

2. attention的一个通用定义

按照Stanford大学课件上的描述,attention的通用定义如下:

  • 给定一组向量集合values,以及一个向量query,attention机制是一种根据该query计算values的加权求和的机制。
  • attention的重点就是这个集合values中的每个value的“权值”的计算方法。
  • 有时候也把这种attention的机制叫做query的输出关注了(或者说叫考虑到了)原文的不同部分。(Query attends to the values)

举例:刚才seq2seq中,哪个是query,哪个是values?
each decoder hidden state attends to the encoder hidden states (decoder的第t步的hidden state—-st是query,encoder的hidden states是values)
从定义来看Attention的感性认识:

  • The weighted sum is a selective summary of the information contained in the values, where the query determines which values to focus on.
    换句话说,attention机制就是一种根据某些规则或者某些额外信息(query)从向量表达集合(values)中抽取特定的向量进行加权组合(attention)的方法。简单来讲,只要我们从部分向量里面搞了加权求和,那就算用了attention。

3. attention的计算变体

首先从大的概念来讲,针对attention的变体主要有两种方式:
1.一种是在attention 向量的加权求和计算方式上进行创新
2.另一种是在attention score(匹配度或者叫权值)的计算方式上进行创新

当然还有一种就是把二者都有改变的结合性创新,或者是迁移性创新,比如借鉴CNN的Inception思想等等,后续会提到一点,详细的应该是在以后可能要讲的Tranformer里面会详细提到。
我们先针对第一种方法讲讲区别,其实虽然名字变来变去,他们的差异没有那么多。

3.1 针对attention向量计算方式变体

大概分成这么几种:

  • Soft attention、global attention、动态attention

  • Hard attention

  • “半软半硬”的attention (local attention)

  • 静态attention

  • 强制前向attention

Soft attention、global attention、动态attention

       这三个其实就是Soft attention,也就是我们上面讲过的那种最常见的attention,是在求注意力分配概率分布的时候,对于输入句子X中任意一个单词都给出个概率,是个概率分布,把attention变量(context vecor)用 c t c_t ct表示,attention得分在经过了softmax过后的权值用 a l p h a alpha alpha表示
这里写图片描述
论文:Neural machine translation by jointly learning to align and translate

Hard attention

       Soft是给每个单词都赋予一个单词match概率,那么如果不这样做,直接从输入句子里面找到某个特定的单词,然后把目标句子单词和这个单词对齐,而其它输入句子中的单词硬性地认为对齐概率为0,这就是Hard Attention Model的思想。
这里写图片描述
       Hard attention的这个pt,我也没有详细研究,不过我觉得可能跟下面讲的local attention机制的找法差不多。据说Hard attention 一般用在图像里面,当图像区域被选中时,权重为1,剩下时候为0。
https://www.cnblogs.com/Determined22/p/6914926.html

local attention (半软半硬attention)

Soft attention 每次对齐的时候都要考虑前面的encoder的所有hi,所以计算量会很大,因此一种朴素的思想是只考虑部分窗口内的encoder隐藏输出,其余部分为0,在窗口内使用softmax的方式转换为概率。这个local attention相反概念的是global attention,global attention其实就是softmax attention,这里不多赘述global attention了。

这里写图片描述

论文:Effective Approaches to Attention-based Neural Machine Translation
在这个模型中,对于是时刻t的每一个目标词汇,模型首先产生一个对齐的位置 pt(aligned position),context vector 由编码器中一个集合的隐藏层状态计算得到,编码器中的隐藏层包含在窗口[pt-D,pt+D]中,D的大小通过经验选择。

上式之中,大S指的是源句子的长度,Wp和vp是指的模型的参数,通过训练得到,为了支持pt附近的对齐点,设置一个围绕pt的高斯分布,其中小s是在以pt为中心的窗口中的整数,pt是一个在[0,S]之间的实数。小Sigma σ 一般取窗口大小的一半。

静态attention

静态attention:对输出句子共用一个St的attention就够了,一般用在Bilstm的首位hidden state输出拼接起来作为st(在图所示中为u)
这里写图片描述
论文:Teaching Machines to Read and Comprehend 以及
Supervised Sequence Labelling with Recurrent Neural Networks

上面这个图是我从论文摘过来的静态attention的示意图,有同学可能会注意到的是:这个前面的每个hidden state 不应该都和这里的 u 算一次attention score吗,怎么这里只有一个r和u进行了交互?
其实他这里的r表示的是加权平均的self attention,这个权就是attention ct向量,这个图里面把attention ct的计算过程省略了。直接跳到了ct和st计算真正的s’t的部分。他这个里面用的实际的attention score的计算并不是用点积,是additive attention,什么是additive attention呢?这个下面就会讲根据按照attention score计算的不同的attention model变体。

3.2 针对Attention score的计算方式变体

讲完了在加权向量 α i \alpha_i αi(注意,此处这个 α i \alpha_i αi指的是得分softmax之后的向量的第i个分量)的计算上面的各种attention变体,我们现在再回到原始的soft attention公式上面来。在softmax 的Attention向量的计算总是依赖于权重求和,而权重往往是attention score的softmax。
这里写图片描述
我们可以看到,式子中的变量e代表的就是attention score,alpha是attention的权重,a就是context vector
根据attention score的计算方式不同,我们可以将attention进一步细分分类。attention score的计算主要有以下几种:
这里写图片描述
注意点积attention score这里有个假设,就是s和h的维数要一样才能进行点积,很好理解。
第二个W矩阵是训练得到的参数,维度是d2 x d1,d2是s的hidden state输出维数,d1是hi的hidden state维数
最后就是上面提到的additive attention,是对两种hidden state 分别再训练矩阵然后激活过后再乘以一个参数向量变成一个得分。
其中,W1 = d3xd1,W2 = d3*d2,v = d3x1 ,d1,d2,d3分别为h和s还有v的维数,属于超参数。

4.更加特殊的attention

4.1 self attention

  • 思想:Self attention也叫做intra-attention在没有任何额外信息的情况下,我们仍然可以通过允许句子使用–self attention机制来处理自己,从句子中提取关注信息。
  • 它在很多任务上都有十分出色的表现,比如阅读理解 (Cheng et al., 2016) 、文本继承 (textual entailment/Parikh et al., 2016) 、自动文本摘要 (Paulus et al., 2017) 。

其已经被证明是非常有效的,而且所以我们下面重点关注一下。

论文:
Cheng, J., Dong, L., & Lapata, M. (2016). Long Short-Term Memory-Networks for Machine Reading
Parikh, A. P., Täckström, O., Das, D., & Uszkoreit, J. (2016). A Decomposable Attention Model for Natural Language Inference. In Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing.
Paulus, R., Xiong, C., & Socher, R. (2017). A Deep Reinforced Model for Abstractive Summarization. In arXiv preprint arXiv:1705.04304,.

Self attention计算方式

  1. 以当前的隐藏状态去计算和前面的隐藏状态的得分,作为当前隐藏单元的attention score,例如:
自然语言处理中的Attention机制总结[通俗易懂]

2. 以当前状态本身去计算得分作为当前单元attention score,这种方式更常见,也更简单,例如:

自然语言处理中的Attention机制总结[通俗易懂]

其中va和Wa是参数,通过训练得到,Wa维数dxd,hi维数dx1,va维数dx1(第一个公式)
前面的w是dx1的向量,hi是dx1,b是(1,)(第二个公式),注意这里其实是简化过后的表示:原始论文 https://arxiv.org/pdf/1512.08756.pdf

自然语言处理中的Attention机制总结[通俗易懂]

上面的式子中,使用的va向量是一个通用的向量,每个隐藏状态并没有区分,如果我们对不同状态计算的时候学习不同的向量va,,也就是一个Va矩阵,得到的就是一个attention矩阵。
论文:
https://arxiv.org/pdf/1703.03130.pdf

上面的式子中,H是nx2u(双向lstm的结果拼接,每个单向LSTM的hidden units是u),Wa是dx2u,Va是rxd,得到的attention 矩阵 A是rxn。
C = rx2u,按论文意思来看就是把一句话编码成了一个rx2u的矩阵(sentence embedding)
在实际操作的过程中,我们也可能在学习时候给A一个F2范式约束项来避免过拟合。

key-value attention

这种attention就是比较新的了,简单来说Key-value attention 是将hi拆分成了两部分[ k e y t key_t keyt; v a l u e t value_t valuet],然后使用的时候只针对key部分计算attention权重,然后加权求和的时候只使用value部分进行加权求和。公式如下,attention权重计算如下:
这里写图片描述
论文:Daniluk, M., Rockt, T., Welbl, J., & Riedel, S. (2017). Frustratingly Short Attention Spans in Neural Language Modeling. In ICLR 2017.

其中呢,这个L是attention窗口长度
WY=kxk,ki=kx1,Wh = kxk,1^T是一个1xL的向量
w 是一个kx1的向量
Wr = kxk,
Wx = kxk
最后得到kx1的ht*与ht一起参与下一步的运算

4.2 multi-head attention

最后简单讲一下google的attention is all you need 里面的attention,这一段基本上是摘抄的苏剑林的科学空间的博文了。
       Google 在 attention is all you need 中发明了一种叫transformer的网络结构,其中用到了multi-head attention。Transformer在下一节课里面会讲,所以我们就简单介绍一下他里面用到的attention。
这里写图片描述
这里写图片描述
首先,google先定义了一下attention的计算,也是定义出key,value,query三个元素(在seq2seq里面,query是st,key和value都是hi)在self 里面,query 是当前要计算的hi,k和v仍然一样,是其他单元的hidden state。在key value attention里面key和value则是分开了的。
然后除以了一下根号dk,为了让内积不至于太大(太大的话softmax后就非0即1了,不够“soft”了)
这里我们不妨假设,Q是nxdk,K是mxdk,v是mxdv,忽略归一化和softmax的话就是三个矩阵相乘,得到的是n*dv的矩阵。我们可以说,通过这么一个attention层,就将一个nxdk的序列Q,提取信息编码成nxdv的序列了。
Wi用来先在算attention对三个矩阵做不同的矩阵变换映射一下,变成nxdk’,mxdk’,mxdv’维度。
最后做并联,有点类似于inception 里面多个卷积核的feature map并联的感觉。

4.3 channel attention

CV里面呢,有一种新的attention机制流行了起来,最出名的文章是Squeeze-and-excitation networks,里面就是这个模块就是先将一个whc的图像,做globalpooling之后压缩再还原作为权重乘上原来的feature map。
Hu J, Shen L, Sun G. Squeeze-and-excitation networks[C]//Proceedings of the IEEE conference on computer vision and pattern recognition
在这里插入图片描述

那么,如果我们想要在NLP里面中,用上channel attention,应该怎么用呢?
笔者认为,可以这样做一个简单的实现并迁移:

### keras 代码
time_step = 512
emb_size = 128
input = Input(shape=(time_step,emb_size)) #
g_pooling = Lambda(lambda x : K.mean(x,axis=-1))(input) #bs,time_step
squeeze = Dense(time_step // 4, activation='relu')(g_pooling) #bs,512//4
scale = Dense(time_step,activation='sigmoid')(squeeze) #bs,time_step
scale = Lambda(lambda x : x[...,tf.newaxis])(scale) #bs,time_step,1
attention_feature = multiply([input,scale])

这个思路就是我们将一句话的中的每个词的词向量(1emb_size)当做cnn里面的wh,然后每个timestep当做channel,这样做了把每个词的average pooling之后当做这个词的表征来算一下attention,这种设计笔者个人认为在一些很长的句子的时候,可能会有一点用(比如举例的timestep是512个词)

在这里插入图片描述
另一种实现思路是同样把句子真的过几个textCNN变成多channel的,这样的话,每个channel就代表了整个句子的表征,哪种好的话可以尝试一下,我自己是没试过的:

time_step = 512
emb_size = 128
input = Input(shape=(time_step,emb_size)) #
conv = Conv1D(32 ,3,padding='same')(input) #bs,time_step,filter_size ##这里也可以多来几个,concat
g_pooling = Lambda(lambda x : K.mean(x,axis=1,keepdims=True))(conv) #bs,1,filter_size
filter_num = K.int_shape(g_pooling)[-1]
squeeze = Dense(filter_num// 4, activation='relu')(g_pooling) #bs,1,filter_size//4
scale = Dense(filter_num,activation='sigmoid')(squeeze) #bs,1,filter_size
attention_feature = multiply([conv,scale])

5. 代码示例

这里写图片描述

以在网上找到的一段keras实现的self attention的代码,其实现原理应该是这篇论文,结构如上图所示,是利用一个mlp来计算h(t)的attention得分,然后这个代码里面进行了进一步简化,让把mlp简化得只有一个共享的w向量,但是每个ht有一个不同的偏置。
类似于 α ( h t ) = t a n h ( w T h t + b t ) \alpha(h_t)=tanh(w^T{h_t}+b_t) α(ht)=tanh(wTht+bt)的感觉。

#coding=utf-8
'''
Single model may achieve LB scores at around 0.043
Don't need to be an expert of feature engineering
All you need is a GPU!!!!!!!

The code is tested on Keras 2.0.0 using Theano backend, and Python 3.5

referrence Code:https://www.kaggle.com/lystdo/lstm-with-word2vec-embeddings
'''

########################################
## import packages
########################################
import os
import re
import csv
import codecs
import numpy as np
import pandas as pd

########################################
## set directories and parameters
########################################



from keras import backend as K
from keras.engine.topology import Layer
# from keras import initializations
from keras import initializers, regularizers, constraints

np.random.seed(2018)

class Attention(Layer):
    def __init__(self,
                 W_regularizer=None, b_regularizer=None,
                 W_constraint=None, b_constraint=None,
                 bias=True, **kwargs):
        """
        Keras Layer that implements an Attention mechanism for temporal data.
        Supports Masking.
        Follows the work of Raffel et al. [https://arxiv.org/abs/1512.08756]
        # Input shape
            3D tensor with shape: `(samples, steps, features)`.
        # Output shape
            2D tensor with shape: `(samples, features)`.
        :param kwargs:

        """
        self.supports_masking = True
        # self.init = initializations.get('glorot_uniform')
        self.init = initializers.get('glorot_uniform')

        self.W_regularizer = regularizers.get(W_regularizer)
        self.b_regularizer = regularizers.get(b_regularizer)

        self.W_constraint = constraints.get(W_constraint)
        self.b_constraint = constraints.get(b_constraint)

        self.bias = bias

        self.features_dim = 0
        super(Attention, self).__init__(**kwargs)

    def build(self, input_shape):
        self.step_dim = input_shape[1]
        assert len(input_shape) == 3 # batch ,timestep , num_features
        print(input_shape)
        self.W = self.add_weight((input_shape[-1],), #num_features
                                 initializer=self.init,
                                 name='{}_W'.format(self.name),
                                 regularizer=self.W_regularizer,
                                 constraint=self.W_constraint)
        self.features_dim = input_shape[-1]

        if self.bias:
            self.b = self.add_weight((input_shape[1],),#timesteps
                                     initializer='zero',
                                     name='{}_b'.format(self.name),
                                     regularizer=self.b_regularizer,
                                     constraint=self.b_constraint)
        else:
            self.b = None

        self.built = True

    def compute_mask(self, input, input_mask=None):
        # do not pass the mask to the next layers
        return None

    def call(self, x, mask=None):
        features_dim = self.features_dim
        step_dim = self.step_dim
        print(K.reshape(x, (-1, features_dim)))# n, d
        print(K.reshape(self.W, (features_dim, 1)))# w= dx1
        print(K.dot(K.reshape(x, (-1, features_dim)), K.reshape(self.W, (features_dim, 1))))#nx1

        eij = K.reshape(K.dot(K.reshape(x, (-1, features_dim)), K.reshape(self.W, (features_dim, 1))), (-1, step_dim))#batch,step
        print(eij)
        if self.bias:
            eij += self.b

        eij = K.tanh(eij)

        a = K.exp(eij)

        # apply mask after the exp. will be re-normalized next
        if mask is not None:
            # Cast the mask to floatX to avoid float64 upcasting in theano
            a *= K.cast(mask, K.floatx())

        a /= K.cast(K.sum(a, axis=1, keepdims=True) + K.epsilon(), K.floatx())
        print(a)
        a = K.expand_dims(a)
        print("expand_dims:")
        print(a)
        print("x:")
        print(x)
        weighted_input = x * a
        print(weighted_input.shape)
        return K.sum(weighted_input, axis=1)

    def compute_output_shape(self, input_shape):
        # return input_shape[0], input_shape[-1]
        return input_shape[0], self.features_dim

6. 总结

  • 总的来说,attention的机制就是一个加权求和的机制,只要我们使用了加权求和,不管你是怎么花式加权,花式求和,只要你是根据了已有信息计算的隐藏状态的加权和求和,那么就是使用了attention,而所谓的self attention就是仅仅在句子内部做加权求和(区别与seq2seq里面的decoder对encoder的隐藏状态做的加权求和)。
  • self attention我个人认为作用范围更大一点,而key-value其实是对attention进行了一个更广泛的定义罢了,我们前面的attention都可以套上key-value attention,比如很多时候我们是把k和v都当成一样的算来,做self的时候还可能是quey=key=value。
    最后,我把分享的ppt也放到文库了,免积分下载(设置未成功,默认要1积分…),有需要的人自取吧。

大佬同学自己搞了个公众号,帮忙宣传一波,wx公众号“包包算法笔记”。公众号的主人是我的同学,kaggle的GrandMaster,而且这个大哥为人幽默有趣,心态开放。目前公众号里面主要聊的都是自己的算法见解和经验分享,当然也有自己的一些最近的活动啥的,干货也比较多,最良心的没有卖课广告,不会像各种所谓的技术公众号一样,先给你制造一波忧虑,然后告诉你“买了公众号分享的课之后你就无敌了”一样打广告。
感兴趣的同学不妨关注一下~

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/161475.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • C++ this指针详解

    C++ this指针详解this指针只能在一个类的成员函数中调用,它表示当前对象的地址。下面是一个例子:​voidDate::setMonth(intmn){month=mn;//这三句是等价的this->month=mn;(*this).month=mn;}​1.this只能在成员函数中使用。…

  • 电脑爱好者必须懂得的电脑简单英语单词[通俗易懂]

    电脑爱好者必须懂得的电脑简单英语单词[通俗易懂]使用电脑必懂的53个英文单词和缩写!  ·PC:个人计算机PersonalComputer  ·CPU:中央处理器CentralProcessingUnit  ·CPUFan:中央处理器的“散热器”(Fan)  ·MB:主机板MotherBoard  ·RAM:内存RandomAccessMemory,以PC-代号划分规格,如PC-133,PC-1066,PC-27…

  • 让引擎不再是你的唯一,对百度再见

    让引擎不再是你的唯一,对百度再见

  • 第十四章《redis》

    第十四章《redis》

  • pycharm运行环境配置_pycharm安装django

    pycharm运行环境配置_pycharm安装django1.安装Python前往https://www.python.org/downloads/release/python-383/,根据环境下载对应的python安装包安装。2.安装pycharmPycharm是一个可用作工程开发的工具。前往https://www.jetbrains.com/pycharm/,点击下图中的下载链接:分Professional和Community两个版本,deployment等功能仅前者开放。·Community版本:免费。·Professi

  • shell中各种括号的作用()、(())、[]、[[]]、{}「建议收藏」

    shell中各种括号的作用()、(())、[]、[[]]、{}「建议收藏」一、小括号,园括号()1、单小括号()①命令组。括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用。括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必有空格。②命令替换。等同于`cmd`,shell扫描一遍命令行,发现了$(cmd)结构,便将$(cmd)中的cmd执行一次,得到其标准输出,再将

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号