1.引言

现有的主流机器翻译模型,基本都是基于encoder-decoder的结构,其思想就是对于输入句子序列,通过RNN先进行编码(encoder),转化为一个上下文向量context vector,然后利用另一个RNN对上下文向量context vector进行解码(decoder)。其结构如下:

之后,又有学者在该结构的基础上,做了各种改进,其中主要有两方面的改进,一种是添加了注意力机制,其思想就是在decoder的每一步输出时,不再仅仅关注encoder最后的输出状态,而是综合encoder每一个时间步的输出状态,并对其进行加权求和,从而使得在decoder的每一个时间步输出时,可以对输入句子序列中的个别词汇有所侧重,进而提高模型的准确率,另一种改进是替换encoder和decoder的RNN模型,比如Facebook提出的Fairseq模型,该模型在encoder和decoder都采用卷积神经网络模型,以及Googlet提出的Transformer模型,该模型在encoder和decoder都采用attention,这两种模型的出发点都是为了解决RNN没法并行计算的缺点,因此,在训练速度上得到了很大的提升。但是,这些改进其实都没有脱离encoder-decoder的结构。

因此,《Pervasive Attention: 2D Convolutional Neural Networks for Sequence-to-Sequence Prediction》一文作者提出了一种新的结构,不再使用encoder-decoder的结构,而是采用一种基于2D卷积神经网络(2D CNN)的结构,其思想就是将输入序列和目标序列embedding之后的矩阵进行拼接,转化为一个3D的矩阵,然后直接使用卷积网络进行卷积操作,其卷积网络的结构采用的是DenseNet的结构,并且在进行卷积时对卷积核进行遮掩,以防止在卷积时后续信息的流入,拼接后的feature map如下图所示:

2.相关符号定义

论文中涉及到的相关符号及其定义分别如下:

  • (s,t)(s,t):输入序列和目标序列句子对
  • \left | s \right |:输入序列长度
  • \left | t \right |:目标序列长度
  • d_s:输入序列embedding的维度
  • d_t:目标序列embedding的维度
  • \left \{ x_1,x_2,...,x_\left | s \right | \right \}:已经经过embedding的输入序列矩阵
  • \left \{ y_1,y_2,...,y_\left | t \right | \right \}:已经经过embedding的目标序列矩阵
  • g:growth rate,与DenseNet中的growth rate含义相同,是每个dense layer的输出维度

3. pervasive attention模型介绍

pervasive attention模型的结构主要还是借鉴DenseNet的结构,在结构方面其实并没有多新奇,其主要特别的地方是将输入序列和目标序列的数据进行融合,转化为一个3D的矩阵,从而可以避开encoder-decoder的结构,下面对该模型具体展开介绍。

3.1 模型的输入(Input source-target tensor)

首先是模型的输入,记\left \{ x_1,x_2,...,x_\left | s \right | \right \}\left \{ y_1,y_2,...,y_\left | t \right | \right \}分别表示输入序列和目标序列经过embedding后的二维矩阵,其中\left | s \right |表示输入序列的长度,\left | t \right |表示目标序列的长度,d_s表示输入序列embedding后的维度,d_t表示目标序列embedding后的维度。接着,将这两个矩阵进行拼接,得到一个三维的矩阵,记为X\in \mathbb{R}^\left | t \right |\times \left | s \right |\times f_0,其中f_0=d_t+d_sX_i_j=[y_i, x_j]。这里有一个地方需要注意的是,作者在论文中是将数据转化为\left | t \right |\times \left | s \right |\times f_0的形式,这时,在后面的卷积操作时,卷积核的mask就应该是对行方向进行mask,而不是上图显示的列方向。

另外,笔者在查看作者源代码时,发现其实在将数据进行拼接之前,作者其实还做了一个conv embedding的操作,即对embedding后的输入序列和输出序列矩阵进行1维的卷积操作,这样使得后面每个单词其实都可以融合前一个单词的信息。

3.2 卷积层(Convolutional layers)

该论文中卷积层的结构主要参考的DenseNet的dense block结构,在每一个卷积block中,都包含以下7层:

  • Batch_normalizes:第一层标准化层,对输入数据进行batch标准化
  • ReLU:第一层激活层
  • Conv(1):第一层卷积层,采用(1,1)的卷积核,输入的通道数是f_0+(l-1)g,其中,l表示当前的层数,l\in \left \{ 1,...,L \right \}L为dense layer的层数,g称作growth rate。因为采用的是DenseNet的结构,因此,需要将当前层前面的l-1层的输出作为附加的通道数与Input一起拼接。第一层卷积操作后的输出通道数设置为4g
  • Batch_normalizes:第二层标准化层
  • ReLU:第二层激活层
  • Conv(k):第二层卷积层,采用\left ( k,\left \lceil \frac{k}{2} \right \rceil \right )的卷积核,输出通道数是g
  • dropout:dropout层

具体的模型结构如下图所示:

不过需要注意的是,笔者在查看作者源代码时,发现其实在最开始的Input与dense layer之间,其实还有一层DenseNet的transition操作,即对输入数据进行卷积,使得通道数减半,这样在后续的卷积操作时,数据量不会太大。

3.3 输出层(Target sequence prediction)

卷积层结束后,记模型的输出为H^l\in \mathbb{R}^\left | t \right |\times \left | s \right |\times f_l,其中f_l为输出的通道数,由于输出的是一个3维的结构,因此,需要对第2维进行折叠,使其转化为\left | t \right |\times f_l的形式,这里作者介绍了两种主要的操作方法,分别是pooling和注意力机制:

  • pooling:可以选择max_pooling或average_pooling,其计算方式分别如下:    
                                                           
                                                      
                                                       
    这里主要需要注意的是做average_pooling时,作者不是直接计算平均,而是除以句子长度的开根号,作者通过实验发现这种做法效果更好,并且作者在实验时发现用max_pooling效果要比average_pooling好。
  • 注意力机制:与传统的注意力机制一样操作,这里不具体展开细讲了

得到折叠后的结果后,再将结果传入一个全连接层,使得输出的size转化为 \left | t \right |\times \nu,这里\nu是目标序列的词汇总数,并将结果传入一个softmax层即可得到最终的概率分布,计算如下:

p_i=SoftMax(EH_i^p^o^o^l)

4.pervasive attention的keras实现

笔者用keras框架对pervasive attention进行了复现,下面对主要的代码模块按照上面介绍的模型结构进行讲解。首先导入相关的依赖库和函数。代码如下:

from keras.layers import Input, Embedding, \Lambda, Concatenate, BatchNormalization, \Conv2D, Dropout, Dense, MaxPool2D, ZeroPadding2D, \AveragePooling2D, ZeroPadding1D
from keras.layers import Activation, TimeDistributed, Conv1D
from keras.models import Model
import keras.backend as K
from keras import optimizers

接着是模型的Input中的embedding部分,其中max_enc_len表示输入序列的最大长度,max_dec_len表示目标序列的最大长度,src_word_num表示输入序列的词汇数,tgt_word_num表示目标序列的词汇数,这里+2是为了添加<UNK>、<PAD>两个特殊字符。另外,这里加了一个conv embedding,作者在论文中没有提及,但是源代码里面其实是含有这一层,笔者发现加了conv embedding后,每个单词可以融合前一个单词的信息,有助于提升模型的效果,这里conv embedding的思想其实类似Fairseq的思想。

# Inputs
src_input = Input(shape=(max_enc_len,), name='src_input')
tgt_input = Input(shape=(max_dec_len,), name='tgt_input')# embedding
src_embedding = Embedding(src_word_num + 2,embedding_dim,name='src_embedding')(src_input)
tgt_embedding = Embedding(tgt_word_num + 2,embedding_dim,name='tgt_embedding')(tgt_input)# implement a convEmbedding
for i in range(conv_emb_layers):src_embedding = Conv1D(embedding_dim, 3, padding='same',data_format='channels_last', activation='relu')(src_embedding)tgt_embedding = ZeroPadding1D(padding=(2, 0))(tgt_embedding)tgt_embedding = Conv1D(embedding_dim, 3, padding='valid',data_format='channels_last', activation='relu')(tgt_embedding)

然后对embedding之后的数据进行拼接,使其转化为一个3D的结构,这里笔者的代码与作者有点不一样的地方是将数据转化为X\in \mathbb{R}^\left | s \right |\times \left | t \right |\times f_0的形式,这样方便后面的卷积mask操作,本质上是一样的。

def src_reshape_func(src_embedding, repeat):"""对embedding之后的source sentence的tensor转换成pervasive-attention model需要的shapearxiv.org/pdf/1808.03867.pdf:param src_embedding: source sentence embedding之后的结果[tensor]:param repeat: 需要重复的次数, target sentence t的长度[int]:return: 2D tensor (?, s, t, embedding_dim)"""input_shape = src_embedding.shapesrc_embedding = K.reshape(src_embedding, [-1, 1, input_shape[-1]])src_embedding = K.tile(src_embedding, [1, repeat, 1])src_embedding = K.reshape(src_embedding, [-1, input_shape[1], repeat, input_shape[-1]])return src_embeddingdef tgt_reshape_func(tgt_embedding, repeat):"""对embedding之后的target sentence的tensor转换成pervasive-attention model需要的shapearxiv.org/pdf/1808.03867.pdf:param tgt_embedding: target sentence embedding之后的结果[tensor]:param repeat: 需要重复的次数, source sentence s的长度[int]:return: 2D tensor (?, s, t, embedding_dim)"""input_shape = tgt_embedding.shapetgt_embedding = K.reshape(tgt_embedding, [-1, 1, input_shape[-1]])tgt_embedding = K.tile(tgt_embedding, [1, repeat, 1])tgt_embedding = K.reshape(tgt_embedding, [-1, input_shape[1], repeat, input_shape[-1]])tgt_embedding = K.permute_dimensions(tgt_embedding, [0, 2, 1, 3])return tgt_embeddingdef src_embedding_layer(src_embedding, repeat):"""转换成Lambda层:param src_embedding: source sentence embedding之后的结果[tensor]:param repeat: 需要重复的次数, target sentence t的长度[int]:return: 2D tensor (?, s, t, embedding_dim)"""return Lambda(src_reshape_func,arguments={'repeat': repeat})(src_embedding)def tgt_embedding_layer(tgt_embedding, repeat):"""转换层Lambda层:param tgt_embedding: target sentence embedding之后的结果[tensor]:param repeat: 需要重复的次数, target sentence t的长度[int]:return: 2D tensor (?, s, t, embedding_dim)"""return Lambda(tgt_reshape_func,arguments={'repeat': repeat})(tgt_embedding)# concatenate
src_embedding = src_embedding_layer(src_embedding, repeat=max_dec_len)
tgt_embedding = tgt_embedding_layer(tgt_embedding, repeat=max_enc_len)
src_tgt_embedding = Concatenate(axis=3)([src_embedding, tgt_embedding])

拼接操作后, 为了避免后续卷积时数据太大,并且预测过多地依赖模型的初始信息,先将数据进行一次卷积操作,使得数据的通道数减半,这里conv2_filters即为卷积后的通道数,笔者设为原数据embedding维度大小。

# densenet conv1 1x1
x = Conv2D(conv1_filters, 1, strides=1)(src_tgt_embedding)
x = BatchNormalization(axis=3, epsilon=1.001e-5)(x)
x = Activation('relu')(x)
x = MaxPool2D((2, 1), strides=(2, 1))(x)

接下来是模型的卷积层部分,采用的是DenseNet的结构,由于句子比较长,因此,笔者在transition函数里做了一点修改,即每次transition操作对输入序列的维度进行降维,采用的是pooling操作,使得每次输入序列的维度可以不断下降,而更多的空间给通道数的增加,这里transition操作是一个可选操作,作者在论文中没讲,但是DenseNet原始的结构是有这一个操作的。另外,在卷积操作时,原作者是对卷积核的权重进行mask,比如卷积核为\left ( 3,3 \right )时,直接对最后一列变为0,从而保证非法信息不会被传入,但是这里笔者直接采用\left ( 3,2 \right )的卷积核,并对数据进行左padding两列,这样就不用重写卷积层了。

# transition layer
def transition_block(x,reduction):"""A transition block.该transition block与densenet的标准操作不一样,此处不包括pooling层pervasive-attention model中的transition layer需要保持输入tensor的shape不变 arxiv.org/pdf/1808.03867.pdf# Argumentsx: input tensor.reduction: float, the rate of feature maps need to retain.# Returnsoutput tensor for the block."""x = BatchNormalization(axis=3, epsilon=1.001e-5)(x)x = Activation('relu')(x)x = Conv2D(int(K.int_shape(x)[3] * reduction), 1, use_bias=False)(x)x = MaxPool2D((2, 1), strides=(2, 1))(x)return x# building block
def conv_block(x,growth_rate,dropout):"""A building block for a dense block.该conv block与densenet的标准操作不一样,此处通过增加Zeropadding2D层实现论文中的mask操作,并将Conv2D的kernel size设置为(3, 2)# Argumentsx: input tensor.growth_rate: float, growth rate at dense layers.dropout: float, dropout rate at dense layers.# ReturnsOutput tensor for the block."""x1 = BatchNormalization(axis=3,epsilon=1.001e-5)(x)x1 = Activation('relu')(x1)x1 = Conv2D(4 * growth_rate, 1, use_bias=False)(x1)x1 = BatchNormalization(axis=3, epsilon=1.001e-5)(x1)x1 = Activation('relu')(x1)x1 = ZeroPadding2D(padding=((1, 1), (1, 0)))(x1)  # mask sakex1 = Conv2D(growth_rate, (3, 2), padding='valid')(x1)x1 = Dropout(rate=dropout)(x1)x = Concatenate(axis=3)([x, x1])return x# dense block
def dense_block(x,blocks,growth_rate,dropout):"""A dense block.# Argumentsx: input tensor.blocks: integer, the number of building blocks.growth_rate:float, growth rate at dense layers.dropout: float, dropout rate at dense layers.# Returnsoutput tensor for the block."""for i in range(blocks):x = conv_block(x, growth_rate=growth_rate, dropout=dropout)return x# densenet 4 dense block
if len(blocks) == 1:x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)
else:for i in range(len(blocks) - 1):x = dense_block(x, blocks=blocks[i], growth_rate=growth_rate, dropout=dropout)x = transition_block(x, reduction)x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)

卷积操作结束后,是模型的pooling操作,对s维度进行折叠,这里笔者只写了pooling操作。

# avg pooling
def h_avg_pooling_layer(h):"""实现论文中提到的均值池化 arxiv.org/pdf/1808.03867.pdf:param h: 由densenet结构输出的shape为(?, s, t, fl)的tensor[tensor]:return: (?, t, fl)"""h = Lambda(lambda x: K.permute_dimensions(x, [0, 2, 1, 3]))(h)h = AveragePooling2D(data_format='channels_first',pool_size=(h.shape[2], 1))(h)h = Lambda(lambda x: K.squeeze(x, axis=2))(h)return h# max pooling
def h_max_pooling_layer(h):"""实现论文中提到的最大池化 arxiv.org/pdf/1808.03867.pdf:param h: 由densenet结构输出的shape为(?, s, t, fl)的tensor[tensor]:return: (?, t, fl)"""h = Lambda(lambda x: K.permute_dimensions(x, [0, 2, 1, 3]))(h)h = MaxPool2D(data_format='channels_first',pool_size=(h.shape[2], 1))(h)h = Lambda(lambda x: K.squeeze(x, axis=2))(h)return h# Max pooling
h = h_max_pooling_layer(x)

最后是模型的输出,是一个全连接层+softmax层,这里没什么好讲的,代码如下:

# Max pooling
h = h_max_pooling_layer(x)# Target sequence prediction
output = Dense(tgt_word_num + 2, activation='softmax')(h)

以上对整个模型各个模块代码分别进行了讲解,最后,将上面的代码串联起来,汇总如下:

# pervasive-attention model
def pervasive_attention(blocks,conv1_filters=64,growth_rate=12,reduction=0.5,dropout=0.2,max_enc_len=200,max_dec_len=200,embedding_dim=128,src_word_num=4000,tgt_word_num=4000,samples=12000,batch_size=8,conv_emb_layers=6):"""build a pervasive-attention model with a densenet-like cnn structure.:param blocks: a list with length 4, indicates different number ofbuilding blocks in 4 dense blocks, e.g which [6, 12, 48, 32]for DenseNet201 and [6, 12, 32, 32] for DenseNet169. [list]:param conv1_filters: the filters used in first 1x1 conv toreduce the channel size of embedding input. [int]:param growth_rate: float, growth rate at dense layers. [int]:param reduction: float, the rate of feature maps whichneed to retain after transition layer. [float]:param dropout: dropout rate used in each conv block, default 0.2. [float]:param max_enc_len: the max len of source sentences. [int]:param max_dec_len: the max len of target sentences. [int]:param embedding_dim: the hidden units of first two embedding layers. [int]:param src_word_num: the vocabulary size of source sentences. [int]:param tgt_word_num: the vocabulary size of target sentences. [int]:param samples: the size of the training data. [int]:param batch_size: batch size. [int]:param conv_emb_layers: the layers of the convolution embedding. [int]:return:"""# Inputssrc_input = Input(shape=(max_enc_len,), name='src_input')tgt_input = Input(shape=(max_dec_len,), name='tgt_input')# embeddingsrc_embedding = Embedding(src_word_num + 2,embedding_dim,name='src_embedding')(src_input)tgt_embedding = Embedding(tgt_word_num + 2,embedding_dim,name='tgt_embedding')(tgt_input)# implement a convEmbeddingfor i in range(conv_emb_layers):src_embedding = Conv1D(embedding_dim, 3, padding='same',data_format='channels_last', activation='relu')(src_embedding)tgt_embedding = ZeroPadding1D(padding=(2, 0))(tgt_embedding)tgt_embedding = Conv1D(embedding_dim, 3, padding='valid',data_format='channels_last', activation='relu')(tgt_embedding)# concatenatesrc_embedding = src_embedding_layer(src_embedding, repeat=max_dec_len)tgt_embedding = tgt_embedding_layer(tgt_embedding, repeat=max_enc_len)src_tgt_embedding = Concatenate(axis=3)([src_embedding, tgt_embedding])# densenet conv1 1x1x = Conv2D(conv1_filters, 1, strides=1)(src_tgt_embedding)x = BatchNormalization(axis=3, epsilon=1.001e-5)(x)x = Activation('relu')(x)x = MaxPool2D((2, 1), strides=(2, 1))(x)# densenet 4 dense blockif len(blocks) == 1:x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)else:for i in range(len(blocks) - 1):x = dense_block(x, blocks=blocks[i], growth_rate=growth_rate, dropout=dropout)x = transition_block(x, reduction)x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)# Max poolingh = h_max_pooling_layer(x)# Target sequence predictionoutput = Dense(tgt_word_num + 2, activation='softmax')(h)# compilemodel = Model([src_input, tgt_input], [output])adam = optimizers.Adam(lr=0.0001,beta_1=0.9,beta_2=0.999,epsilon=1e-08,decay=0.05 * batch_size / samples)model.compile(optimizer=adam, loss='categorical_crossentropy')return model

5.小结

以上就是pervasive attention模型的整体结构及其复现,其实整个模型的思路都不算太难,下面谈一谈笔者自己对这个模型的一个感受吧:

  • 优点:①抛弃了以往的encoder和decoder结构,可以直接采用卷积操作进行计算,从而实现并行化;②参数量整体比seq2seq要少很多;③可以在每一层的结果实现attention,这也是模型为什么叫pervasive attention的原因。
  • 缺点:该模型由于对输入序列和目标序列的数据进行拼接,当序列的长度比较长时,对GPU的内存要求就很高,特别是当层数和growth rate比较大时,对GPU的性能要求就特别大。

最后,附上原论文的地址和作者源代码的地址:

  • 论文地址:arxiv.org/pdf/1808.03867.pdf
  • Pytorch实现:github.com/elbayadm/attn2d

招聘信息:

熊猫书院算法工程师:

https://www.lagou.com/jobs/4842081.html

希望对深度学习算法感兴趣的小伙伴们可以加入我们,一起改变教育!

摒弃encoder-decoder结构,Pervasive Attention模型与Keras实现相关推荐

  1. Seq2Seq Attention模型

    首先强调一下图文来源,真正的完全图解Seq2Seq Attention模型.本人目前用到此模型,发现该知乎作者的文章写得通俗易懂,于是整理作为自己的笔记.如果小伙伴对模型中各种隐层状态感到不解,请看我 ...

  2. [转] 图解Seq2Seq模型、RNN结构、Encoder-Decoder模型 到 Attention

    from : https://caicai.science/2018/10/06/attention%E6%80%BB%E8%A7%88/ 一.Seq2Seq 模型 1. 简介 Sequence-to ...

  3. 基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)

    日萌社 人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新) Encoder编码器-Decoder解码器框架 + Atten ...

  4. 机器翻译 MXNet(使用含注意力机制的编码器—解码器,即 Encoder编码器-Decoder解码器框架 + Attention注意力机制)

    日萌社 人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新) Encoder编码器-Decoder解码器框架 + Atten ...

  5. 基于Seq2Seq的中文聊天机器人编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制)

    日萌社 人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新) Encoder编码器-Decoder解码器框架 + Atten ...

  6. encoder decoder模型_3分钟|聊一聊 Decoder 模块

    微信公众号:NLP从入门到放弃 本文大概需要阅读 4.1 分钟 聊一下对 Decoder 的个人总结和理解,我保证里面大部分内容你没在别人那看过,绝对原创. 我先说一个很小的细节点,当时花了点时间才琢 ...

  7. encoder decoder模型_机器学习11 -- 无监督学习之Auto-Encoder

    1 什么是Auto-Encoder 自编码器Auto-Encoder是无监督学习的一种方式,可以用来做降维.特征提取等.它包括两部分 Encoder:对原始样本进行编码 Decoder:对经过编码后的 ...

  8. encoder decoder 模型理解

    encoder decoder 模型是比较难理解的,理解这个模型需要清楚lstm 的整个源码细节,坦率的说这个模型我看了近十天,不敢说完全明白. 我把细胞的有丝分裂的图片放在开头,我的直觉细胞的有丝分 ...

  9. 深度学习的Attention模型

    前面看到谷歌发表的运用在机器翻译上的论文<Attention is all you need>,很是让人惊讶,这是一种全新的模型,与之前的经典的seq2seq模型改动较大,它完全摒弃了RN ...

最新文章

  1. 各大品牌液晶显示器工程模式
  2. CSDN上究竟可以上载多大的GIF文件?
  3. 网站分类前导:获取网站标题和描述及对相关信息进行分词处理
  4. 毕业论文 | 基于单片机的烟雾报警器设计(完整源代码、流程图、电路图)
  5. 佳能单反相机二次开发包介绍_家用单反相机什么牌子好
  6. Amazon S3和Swift鉴权机制分析
  7. spoj Favorite Dice(概率dp+期望)
  8. select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET
  9. android studio导入eclipse项目各种问题,eclipse项目导入android studio 各类问题及解决方法...
  10. java快捷键禁用_pycharm 掌握这些快捷键,你就是大神!!
  11. php 当前ip_php获取本机ip(远程IP地址)
  12. 计算机二级计划总结,计算机二级细节总结
  13. idea @value提示_IDEA 中springboot 项目使用 注解Autowired 出现红线
  14. linux 屏幕输出 高亮_通过printf设置Linux终端输出的颜色和显示方式
  15. Visual Studio Code for DBA的介绍
  16. 【WiFi密码破解详细图文教程】ZOL仅此一份 详细介绍从CDlinux U盘启动到设置扫描破解-破解软件论坛-ZOL中关村在线...
  17. linux cvs账户,在linux下为cvs创建用户
  18. Spring Boot教程系列
  19. MySQL字符串拼接的两种方式
  20. linux定时任务生效_linux设置定时任务

热门文章

  1. Android 消息推送OnePush
  2. PPT结尾只会说“谢谢”?学会这些PPT结尾,观众主动为你鼓掌
  3. matlab寻找一段曲线的极值点
  4. Fiddle打开后,打开浏览器上不了网的问题解决
  5. 计算机毕业设计Java大学生学籍管理系统(源码+系统+mysql数据库+lw文档)
  6. 电子罗盘详解之与GPS导航区别
  7. 王者荣耀英雄全皮肤4K高清大图,python爬虫帮你保存下来
  8. idea如何import_导入项目
  9. 你如何看待计算机图形的未来,2019上半年高中信息技术教师资格面试真题及答案(第二批)...
  10. 什么性格的人适合做产品经理?