微信开发工程师带你一文了解推荐领域最新工作( 二 )


先从一个机器翻译的小例子来看:
”The animal didn't cross the street because it was too tired”
单词"it"表示什么呢,是"animal"还是"street",对机器算法来说识别这个不是一件简单的事情,算法在处理每个词的时候需要知道上下文信息 。
较早一代 NLP 算法中 RNN、LSTM、SRU 等序列模型可以处理这种场景,基本做法是将句子分词,然后每个词转化为对应的词向量序列,经过 RNN 算法来处理词序列信息,在翻译这种场景仅用 RNN 很难做到高水准,这样出现了 RNN 的变种:Encoder-Decoder,也叫 Seq2Seq 。如下图所示,在 Encoder 阶段,输入数据编码成一个上下文语义向量 c(语义向量 c 可以有多种表达方式,最简单的方法就是把 Encoder 的最后一个隐状态赋值给 c),Encoder 是一个 RNN 。Decoder 是同样的 RNN(也可以不一样的结构),拿到 c 之后,对其进行解码操作,c 当做之前的初始状态 h0 输入到 Decoder 中,每一次得到一个最有可能的翻译结果,然后让所有单词的 cross entropy 达到最小 。

微信开发工程师带你一文了解推荐领域最新工作

文章插图
 
RNN、LSTM 这种类型算法自然可以很好的处理序列信息,结构相对比较简单,本质上都是递归处理结构,缺点是无法做到并行,训练速度比较慢,另外它也是一种马尔科夫决策过程,无法很好的学习全局结构信息 。那有没有改进的算法呢?Attention,对"Attention is All you need",这也是谷歌 17 年的一篇经典论文,后面在此基础上也诞生了各种优秀论文 。
接着看上面翻译的小例子,这里的"it"表示"animal"还是"street"呢,联系上下文,就知道 it 很大概率指的是 animal 。
下图直观地展示了 self-attention 机制,计算每个单词与其他单词之间的关联,这里用 attention score 来表示关联度,处理"it"时"the"、"animal"就有比较高的 attention score 。这些 score 在 self-attention 中就是权重的概念,对输入 vector 加权然后喂入前馈神经网络中,得到新的表示,这样可以很好的获取上下文信息 。
微信开发工程师带你一文了解推荐领域最新工作

文章插图
 
Self-attention layer:一般的注意力层定义如下:
微信开发工程师带你一文了解推荐领域最新工作

文章插图
 

微信开发工程师带你一文了解推荐领域最新工作

文章插图
 
attention 计算:
微信开发工程师带你一文了解推荐领域最新工作

文章插图
 
Q 是查询,K 是键,V 是数值 。光从上面图中理解起来比较抽象,结合原论文以及代码,就比较容易理解了,先看看核心部分代码:
def scaled_dot_product_attention(q, k, v, mask): """ 参数: q: 请求的形状 == (..., seq_len_q, depth) k: 主键的形状 == (..., seq_len_k, depth) v: 数值的形状 == (..., seq_len_v, depth_v) (..., seq_len_q, seq_len_k) 。默认为None 。返回值: 输出,注意力权重 """ matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k) # 缩放 matmul_qk dk = tf.cast(tf.shape(k)[-1], tf.float32) scaled_attention_logits = matmul_qk / tf.math.sqrt(dk) # softmax 在最后一个轴(seq_len_k)上归一化,因此分数 # 相加等于1 。attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k) output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v) return output, attention_weightsclass MultiHeadAttention(tf.keras.layers.Layer): def __init__(self, d_model, num_heads): super(MultiHeadAttention, self).__init__() self.num_heads = num_heads self.d_model = d_model assert d_model % self.num_heads == 0 self.depth = d_model // self.num_heads self.wq = tf.keras.layers.Dense(d_model) self.wk = tf.keras.layers.Dense(d_model) self.wv = tf.keras.layers.Dense(d_model) self.dense = tf.keras.layers.Dense(d_model) def split_heads(self, x, batch_size): """分拆最后一个维度到 (num_heads, depth). 转置结果使得形状为 (batch_size, num_heads, seq_len, depth) """ x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth)) return tf.transpose(x, perm=[0, 2, 1, 3]) def call(self, v, k, q, mask): batch_size = tf.shape(q)[0] q = self.wq(q) # (batch_size, seq_len, d_model) k = self.wk(k) # (batch_size, seq_len, d_model) v = self.wv(v) # (batch_size, seq_len, d_model) q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth) k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth) v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth) # scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth) # attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k) scaled_attention, attention_weights = scaled_dot_product_attention( q, k, v, mask) scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_q, num_heads, depth) concat_attention = tf.reshape(scaled_attention, (batch_size, -1, self.d_model)) # (batch_size, seq_len_q, d_model) output = self.dense(concat_attention) # (batch_size, seq_len_q, d_model) return output, attention_weights


推荐阅读