当前位置:   article > 正文

Transformer的PositionEncoding源代码解读_transformer位置编码 源码

transformer位置编码 源码

1. trax库了解一下

详细了解戳 trax

trax库是google开源的一个深度学习代码库,基于tensorflow, jax实现了主流的深度学习模型。市场上有这么多开源的深度学习模型实现库,为什么还要搞个trax呢?它的特点就是聚焦端到端的深度学习模型,主打的就是实现简洁,好理解。当然也可以用它来实现自己的模型,CPU、TPU、GPU都能支持,选择trax的源代码, 当然也是因为google对模型的实现正确性更有保证(这一点很重要,对于初学者,看到错误的实现不一定能识别出来)。

tranformer源代码地址:transformer

2. PositionEncodingTransformer模型中的位置

这里上经典的tranformer架构图看一下,如下图,这个PositionEncoding在Encoder与Decoder的输入端都有使用,从图上看是PositionEncoding的输出与输入相加后做为Encoder-block与Decoder-block的输入, 实际的实现又是怎么做的呢?继续往下看。。。

3. PositionEncoding层的原理

位置编码是为了把序列的位置信息考虑进模型,在tranformer中,作者说因为模型不包含递归(如RNN)和卷积,所以加入PositionEncoding以利用序列的位置信息。

看一下原论文:Attention is all you need, 给出了以下两个公式:

 其中pos表示序列的位置,i代表维度(指嵌入维度,tranformer的嵌入维度是512,即dmodel是512,i的取值范围是0<= 2i < 2i + 1 <= 512, 这也就符合论文中说的函数波长从2π 到 10000 · 2π)。

从这个公式可以看出第0和1个嵌入维度位置函数周期是相同的,一个使用sin, 一个使用cos; 类推,第2,3个嵌入维度周期也是相同,波长按指数级扩大一些。。。

位置信息处理都有哪些方法呢?看一下new bing的回答,可以参考一下:

 

4. trax中PositionEncoding层的实现

在开始看PositionEncoding层之前,先看一下Tranformer模型层的组织方式,上源代码,函数也不长,直接都贴上了:

  1. def Transformer(input_vocab_size,
  2. output_vocab_size=None,
  3. d_model=D_MODEL,
  4. d_ff=D_FF,
  5. n_encoder_layers=N_LAYERS,
  6. n_decoder_layers=N_LAYERS,
  7. n_heads=N_HEADS,
  8. max_len=MAX_SEQUENCE_LENGTH,
  9. dropout=DROPOUT_RATE,
  10. dropout_shared_axes=DROPOUT_SHARED_AXES,
  11. mode=MODE,
  12. ff_activation=FF_ACTIVATION_TYPE):
  13. # Avoid 'predict' mode in encoder, since encoder doesn't run stepwise.
  14. encoder_mode = 'eval' if mode == 'predict' else mode
  15. # Share embedding weights if no separate output vocab size.
  16. in_embedder = tl.Embedding(input_vocab_size, d_model)
  17. if output_vocab_size is None:
  18. out_embedder = in_embedder
  19. output_vocab_size = input_vocab_size
  20. else:
  21. out_embedder = tl.Embedding(output_vocab_size, d_model)
  22. def _Dropout():
  23. return tl.Dropout(rate=dropout, shared_axes=dropout_shared_axes, mode=mode)
  24. def _EncBlock():
  25. return _EncoderBlock(d_model, d_ff, n_heads, dropout, dropout_shared_axes,
  26. mode, ff_activation)
  27. def _Encoder():
  28. encoder = tl.Serial(
  29. in_embedder,
  30. _Dropout(),
  31. # 这是编码器的位置编码层
  32. tl.PositionalEncoding(max_len=max_len, mode=encoder_mode),
  33. [_EncBlock() for _ in range(n_encoder_layers)],
  34. tl.LayerNorm(),
  35. )
  36. return tl.Cache(encoder) if mode == 'predict' else encoder
  37. def _EncDecBlock():
  38. return _EncoderDecoderBlock(d_model, d_ff, n_heads, dropout,
  39. dropout_shared_axes, mode, ff_activation)
  40. # Input to model is encoder-side tokens and decoder-side tokens: tok_d, tok_e
  41. # Model output is decoder-side vectors and decoder-side tokens: vec_d tok_d
  42. return tl.Serial(
  43. tl.Select([0, 1, 1]), # Copies decoder tokens for use in loss.
  44. # Encode.
  45. tl.Branch([], tl.PaddingMask()), # tok_e masks tok_d tok_d
  46. _Encoder(),
  47. # Decode.
  48. tl.Select([2, 1, 0]), # Re-orders inputs: tok_d masks vec_e .....
  49. tl.ShiftRight(mode=mode), # 预测时直接返回x
  50. out_embedder,
  51. _Dropout(),
  52. # 这是解码器的位置编码层
  53. tl.PositionalEncoding(max_len=max_len, mode=mode),
  54. tl.Branch([], tl.EncoderDecoderMask()), # vec_d masks_e masks vec_e tok_d ..... .....
  55. [_EncDecBlock() for _ in range(n_decoder_layers)],
  56. tl.LayerNorm(),
  57. tl.Select([0], n_in=3), # Drops masks and encoding vectors.
  58. # Map vectors to match output vocab size.
  59. tl.Dense(output_vocab_size),
  60. )

模型所有的层都放在一个tl.Serial组合器内,根据return语句上方注释可以看到,模型的输入是两个token序列,一个是编码器的输入tok_e, 一个是解码器的输入tok_d, tok_e和tok_d可以放在一个元组里一起输入给模型,它们形状都是(batch, seq_length)(tl.Serial接受一个张量或一个元组/列表作为输入,参数在Serial的各层传递是按stack方式处理的,这一点有点奇葩,没有接触过的很容易掉坑里)。

tok_d,tok_e在经过词嵌入层处理后,会进行一次Dropout处理,然后进入位置编码层PositionEncodeing处理。下面看PositionEncodeing的源代码, 它包括两部分:初始化和调用,初始化的代码如下:

  1. def init_weights_and_state(self, input_signature):
  2. """Randomly initializes the positional encoding vectors.
  3. Args:
  4. input_signature: :py:class:`ShapeDtype` instance characterizing the input
  5. this layer should compute on.
  6. """
  7. d_feature = input_signature.shape[-1]
  8. if self._d_feature is not None:
  9. d_feature = self._d_feature
  10. # 初始化一个保存位置编码的矩阵,形状(seq_length, d_feature)
  11. pe = np.zeros((self._max_len, d_feature), dtype=np.float32)
  12. # 序列位置数组position, (seq_length, 1)
  13. position = np.arange(0, self._max_len)[:, np.newaxis]
  14. # 嵌入维度位置数组div_term, 形状(d_feature/2, ), 是一个一维数组
  15. div_term = np.exp(
  16. np.arange(0, d_feature, 2) * -(np.log(10000.0) / d_feature))
  17. # 填充位置编码矩阵嵌入维度偶数下标位置的数据, position * div_term得到形状(seq_length, d_feature/2)的矩阵
  18. pe[:, 0::2] = np.sin(position * div_term)
  19. # 嵌入维度奇数下标位置的数据
  20. pe[:, 1::2] = np.cos(position * div_term) # [self._max_len, d_feature]
  21. if self._use_bfloat16:
  22. pe = pe.astype(jnp.bfloat16)
  23. w = jnp.array(pe) # Trainable parameters, initialized above.
  24. if self._d_feature is not None:
  25. ff = init.GlorotUniformInitializer()(
  26. (d_feature, input_signature.shape[-1]), self.rng)
  27. self.weights = w, ff
  28. else:
  29. self.weights = w
  30. if self._mode == 'predict':
  31. self.state = jnp.zeros((), dtype=jnp.int32)

在计算pos/(10000^(2i/dmodel))时, 分成了两部分计算:pos和1/(10000^(2i/dmodel))

再看下调用时的代码:

  1. def forward(self, inputs):
  2. """Returns the input activations, with added positional information."""
  3. weights = self.weights
  4. if self._d_feature is not None:
  5. weights, ff = weights
  6. weights = jnp.dot(weights[:inputs.shape[1], :], ff)
  7. if len(weights.shape) < 3: # old checkpoints have 1 in first dim already
  8. # 初始化时weight是(len, d_feature)形态的, 这里给它加了一个batch维度, 即第0维
  9. weights = weights[None, :, :] # [1, self._max_len, d_feature]
  10. if self._mode != 'predict':
  11. # 模型训练跑的分支
  12. x = inputs
  13. symbol_size = jnp.shape(x)[1]
  14. if self._mode != 'train' or self._start_from_zero_prob >= 1.0:
  15. # 指定从0位置开始或非训练模式,如eval模式, 从位置0取symbol_size个序列作为位置编码数据
  16. px = weights[:, :symbol_size, :]
  17. else:
  18. # 随机从位置0开始取位置编码数据
  19. rng1, rng2 = fastmath.random.split(self.rng, 2)
  20. start = fastmath.random.randint(rng1, (), 0, self._max_offset_to_add)
  21. start_from_zero = fastmath.random.uniform(rng2, (), jnp.float32, 0, 1)
  22. start = jnp.where(start_from_zero < self._start_from_zero_prob,
  23. jnp.zeros((), dtype=jnp.int32), start)
  24. px = fastmath.dynamic_slice_in_dim(weights, start, symbol_size,
  25. axis=1)
  26. # dropout规则处理
  27. if self._dropout == 0:
  28. return x + px
  29. else:
  30. noise_shape = list(px.shape)
  31. for dim in self._dropout_broadcast_dims:
  32. noise_shape[dim] = 1
  33. keep_prob = 1.0 - self._dropout
  34. keep = fastmath.random.bernoulli(self.rng, keep_prob,
  35. tuple(noise_shape))
  36. multiplier = keep.astype(x.dtype) / keep_prob
  37. return x + px * multiplier
  38. else:
  39. # 模型预测跑的分支
  40. if self._dropout != 0:
  41. raise ValueError(f'In predict mode, but dropout rate '
  42. f'({self._dropout}) is not zero.')
  43. # State in this class is only used for fast inference. In that case,
  44. # the model is called with consecutive elements position-by-position.
  45. # This positional encoding layer stores the index of the current
  46. # position and increments it on each call.
  47. # 根据输入序列形状,从weight取一个相同开关的位置编码矩阵相加
  48. emb = fastmath.dynamic_slice_in_dim(
  49. weights, self.state, inputs.shape[1], axis=1)
  50. self.state += inputs.shape[1]
  51. return inputs + emb

看代码可以看出这个PositionEncoding层是需要初始化的,不初始化,forward函数是会报错的,因为weight不初始化是一个空tuple, 没有shape属性, if len(weights.shape) < 3: 这个判断会出错, 但是在模型的定义中并没有看到初始化的代码?这个问题后面有时间再看看,有兴趣的也可以自己分析一下,如果有答案,也欢迎留言讨论。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号