Transformer多頭自注意力相關
QKV投影及反向傳播更新參數(∂loss/∂W_q,∂loss/∂W_k,∂loss/∂W_v)
∂loss/∂W_q:鏈式法則層層傳遞梯度,最終告訴 W_q:往這個方向更新,可以減少損失函數(達到優化模型效果)
其他可訓練參數也都是求損失函數對這個參數的偏導數,然後朝着損失函數值降低的方向改變參數值(即權重值)達到優化模型的目的
計算自注意時,如果直接點積(不使用投影):
attention = [主機] · [耳機]
問題:
單一語義空間:原始embedding混合了多種信息(品類、價格、品牌等),直接點積只能得到粗糙的相似度
缺乏靈活性:無法學習"什麼類型的相關性"對預測有用
使用QKV投影的優勢(通過不同的投影矩陣,創建專門的語義空間)
W_q @ [手柄] → "我需要什麼類型的商品?"(query查詢視角)
W_k @ [耳機] → "我能提供什麼類型的信息?"(key鍵視角)
W_v @ [手柄] → "我的實際價值是什麼?"(value值視角)
W_q @ [手柄]:將手柄本身投影到"查詢空間"
@: 表示矩陣乘法
矩陣乘法規則:(m, n) @ (n, p) = (m, p)
W_q、W_k、W_v矩陣會通過反向傳播來更新,
完成訓練後 W_q、W_k、W_v 是固定的
【訓練階段的QKV運算過程舉例】
QKV投影 → 多頭分割 → 計算注意力
如果先分割再投影會導致:每個頭只能看到輸入的1/4特徵,丟失全局信息,導致不同頭之間獨立學習
訓練樣本:
正樣本: 用户行為[主機, 手柄] → 點擊了[耳機] (標籤=1)
負樣本: 用户行為[主機, 手柄] → 沒點擊[鍵盤] (標籤=0)
參數設置:embedding_dim = 768 # 原始商品特徵維度
d_model = 64 # QKV投影后的總維度
num_heads = 4 # 多頭數量
d_k = d_model // num_heads = 16 # 每個頭的維度
訓練迭代第1輪 (Epoch 1, Batch 1)
步驟0: 隨機初始化W_q, W_k, W_v
代碼位置:
MultiHeadTemporalAttention 類的 build 方法
self.query_dense = layers.Dense(self.hidden_size, name="query_projection") #W_Q
self.key_dense = layers.Dense(self.hidden_size, name="key_projection") #W_K
self.value_dense = layers.Dense(self.hidden_size, name="value_projection") #W_V
這裏的 Dense 層在創建時會自動初始化權重矩陣,相當於步驟0中的隨機初始化。
Q (Query - 查詢):詢問:“在歷史興趣中,哪些與我最相關?”(關注哪些信息)
K (Key - 鍵):代表歷史興趣狀態的“標籤”或“標識”。由每個歷史興趣狀態 h(t) 經過一個線性變換(投影)得到。用於與Query進行匹配。(有哪些信息值得被關注)
V (Value - 值):當Query與某個Key匹配度高時,對應的Value信息將被更多地提取。
# W_q, W_k, W_v此時還未學到任何知識
W_q = np.random.randn(64, 768) * 0.01 # (64, 768)
W_k = np.random.randn(64, 768) * 0.01 # (64, 768)
W_v = np.random.randn(64, 768) * 0.01 # (64, 768)
步驟1: 準備訓練數據
對應代碼位置: UniversalDIENDataGenerator 類的 generate_training_samples 方法:
# 正樣本:用户實際點擊的下一個商品
historical_sequence = user_seq[:-1]
positive_target = user_seq[-1]
user_behavior_batch.append(historical_sequence)
target_item_batch.append([positive_target])
labels_batch.append([1])
behavior_length_batch.append(len(historical_sequence))
以及 UniversalDIENModel 的 call 方法中的嵌入層處理:
# 嵌入層
behavior_embeddings = self.embedding_layer(user_behavior)
target_embedding = tf.squeeze(self.embedding_layer(target_item), axis=1)
# 正樣本
行為序列 = [主機, 手柄, 耳機]
候選商品 = 耳機
標籤 = 1
# 商品的原始embedding (768,)
主機_embedding = [0.2, 0.5, 0.1, ..., 0.3] # 768維
手柄_embedding = [0.4, 0.3, 0.6, ..., 0.2] # 768維
耳機_embedding = [0.1, 0.7, 0.2, ..., 0.5] # 768維
步驟2: 前向傳播 - QKV投影
對應代碼位置: MultiHeadTemporalAttention 的 call 方法
# 生成 QKV (維度不變)
query = self.query_dense(inputs) # [32, 50, 64]
key = self.key_dense(inputs) # [32, 50, 64]
value = self.value_dense(inputs) # [32, 50, 64]
這裏的 query_dense, key_dense, value_dense 就是步驟2中的W_q, W_k, W_v矩陣。
# Query投影(使用W_q矩陣)
Q[主機] = W_q @ 主機_embedding: (64, 768) @ (768,) = (64,)
= [-0.012, 0.018, 0.005, -0.008, ..., 0.003] # 64維
# 同理計算其他商品的Query投影
Q[手柄] = W_q @ 手柄_embedding = [0.021, -0.015, 0.009, ..., -0.006] # 64維
Q[耳機] = W_q @ 耳機_embedding = [-0.018, 0.024, -0.011, ..., 0.007] # 64維
# Key投影(使用W_k矩陣)
K[主機] = W_k @ 主機_embedding = [0.015, -0.009, 0.012, ..., -0.004] # 64維
K[手柄] = W_k @ 手柄_embedding = [-0.011, 0.019, -0.007, ..., 0.010] # 64維
K[耳機] = W_k @ 耳機_embedding = [0.013, -0.016, 0.008, ..., -0.005] # 64維
# Value投影(使用W_v矩陣)
V[主機] = W_v @ 主機_embedding = [0.009, 0.014, -0.006, ..., 0.011] # 64維
V[手柄] = W_v @ 手柄_embedding = [-0.007, 0.012, 0.015, ..., -0.008] # 64維
V[耳機] = W_v @ 耳機_embedding = [0.010, -0.013, 0.009, ..., 0.006] # 64維
此時的Q/K/V值都是隨機的
步驟3: 多頭分割
對應代碼位置: MultiHeadTemporalAttention 的 _reshape_to_multi_heads 方法 (第334-339行)
def _reshape_to_multi_heads(self, tensor: tf.Tensor, batch_size: int, seq_len: int) -> tf.Tensor:
# 步驟1: 重塑維度:[32, 50, 64] → [32, 50, 4, 16],64維向量 = 4個頭 × 每頭16維
tensor = tf.reshape(tensor, [batch_size, seq_len, self.num_heads, self.head_dim])
# 步驟2: 轉置:[32, 50, 4, 16] → [32, 4, 50, 16],轉置原因: 方便後續矩陣乘法 (每個頭獨立計算)
return tf.transpose(tensor, [0, 2, 1, 3])
在 call 方法中被調用:
# 多頭分割: [32, 50, 64] → [32, 4, 50, 16]
query = self._reshape_to_multi_heads(query, batch_size, seq_len)
key = self._reshape_to_multi_heads(key, batch_size, seq_len)
value = self._reshape_to_multi_heads(value, batch_size, seq_len)
將64維的Q分割成4個頭,每個16維(按照嵌入值的順序分割)
Q[手柄] = [0.021, -0.015, 0.009, -0.006, ..., -0.003] # 64維
# 分割後:
Q_head1[手柄] = [0.021, -0.015, 0.009, -0.006, ...] # 維度0-15,16維
Q_head2[手柄] = [-0.012, 0.018, -0.005, 0.008, ..] # 維度16-31,16維
Q_head3[手柄] = [0.007, -0.011, 0.014, -0.009, ..] # 維度32-47,16維
Q_head4[手柄] = [-0.004, 0.013, -0.010, 0.006, ..] # 維度48-63,16維
# 分割K和V
K_head1[主機] = [0.015, -0.009, 0.012, -0.004, ...] # 16維
K_head1[手柄] = [-0.011, 0.019, -0.007, 0.010, ...] # 16維
K_head1[耳機] = [0.013, -0.016, 0.008, -0.005, ...] # 16維
V_head1[主機] = [0.009, 0.014, -0.006, 0.011, ...] # 16維
V_head1[手柄] = [-0.007, 0.012, 0.015, -0.008, ...] # 16維
V_head1[耳機] = [0.010, -0.013, 0.009, 0.006, ...] # 16維
步驟4: 多頭分割後計算注意力分數(頭1為例)
對應代碼位置: MultiHeadTemporalAttention 的 call 方法
# 矩陣乘法: Q × K^T :[32, 4, 50, 16] × [32, 4, 16, 50] = [32, 4, 50, 50]
attention_scores = tf.matmul(query, key, transpose_b=True)
# 縮放 (防止梯度消失);為什麼除以√head_dim:因為點積結果隨維度增大而增大
attention_scores = attention_scores / tf.math.sqrt(tf.cast(self.head_dim, tf.float32))
以及Softmax歸一化 :
# 注意力分數轉為概率分佈,axis=-1: 對最後一維(50個位置)做softmax
attention_weights = tf.nn.softmax(attention_scores, axis=-1)
Q × K^T = 每個Query和所有Key的相似度:
Q × K^T 是批量點積:一次性計算所有Query對所有Key的相似度,和這裏的點積兩者等價
用Q_head1[手柄]查詢所有商品的K_head1
# 計算點積 (16,) · (16,) = (1,) (矩陣乘法由點積構成)
分數_手柄→主機 = Q_head1[手柄] · K_head1[主機]:
= 0.0012 # 隨機小數值
分數_手柄→手柄 = Q_head1[手柄] · K_head1[手柄]
= -0.0008 # 隨機負值
分數_手柄→耳機 = Q_head1[手柄] · K_head1[耳機]
= 0.0005 # 隨機小數值
# 縮放(除以√d_k):除以√每個頭的維度(比如每個頭16維),因為點積結果隨維度增大而增大
縮放後分數 = [0.0012, -0.0008, 0.0005] / √16
= [0.0003, -0.0002, 0.00013]
# Softmax歸一化(隨機初始化導致權重接近均勻分佈)
注意力權重_head1 = softmax([0.0003, -0.0002, 0.00013])
= [0.334, 0.333, 0.333] # 手柄對所有商品的權重幾乎相等(這裏的權重是用來計算商品新表示(增加了上下文信息))
問題:隨機初始化導致注意力權重幾乎均勻,無法區分重要性
步驟5: 加權求和Value
對應代碼位置: MultiHeadTemporalAttention 的 call 方法
# 加權求和(生成上下文):[32, 4, 50, 50] × [32, 4, 50, 16] = [32, 4, 50, 16]
# context[手柄] = 0.18 * value[主機]+ 0.31 * value[手柄] + ... (其他商品)
context = tf.matmul(attention_weights, value)
這裏實現了注意力權重與Value的加權求和。
# 頭1的輸出
輸出_head1[手柄] = 0.334 * V_head1[主機] +0.333 * V_head1[手柄] +0.333 * V_head1[耳機]
= 0.334 * [0.009, 0.014, ...] + 0.333 * [-0.007, 0.012, ...] +0.333 * [0.010, -0.013, ...]
= [0.004, 0.004, ..., 0.003] # 16維,幾乎是平均
# 其他頭也類似(都接近均勻權重)
輸出_head2[手柄] = [0.003, -0.002, ..., 0.001] # 16維
輸出_head3[手柄] = [-0.001, 0.005, ..., -0.003] # 16維
輸出_head4[手柄] = [0.002, -0.004, ..., 0.002] # 16維
步驟6: 拼接多頭輸出
對應代碼位置: MultiHeadTemporalAttention 的 call 方法
# _merge_heads - 定義合併多頭的函數
def _merge_heads(self, tensor: tf.Tensor, batch_size: int, seq_len: int) -> tf.Tensor:
"""合併多頭:[batch, heads, seq, head_dim] -> [batch, seq, hidden]"""
# 步驟1: 轉置 [32, 4, 50, 16] → [32, 50, 4, 16]
tensor = tf.transpose(tensor, [0, 2, 1, 3])
# 步驟2: Reshape:[32, 50, 4, 16] → [32, 50, 64]
return tf.reshape(tensor, [batch_size, seq_len, self.hidden_size])
# 合併多頭
context = self._merge_heads(context, batch_size, seq_len)
# _merge_heads:拼接4個頭,最終輸出[手柄] 融合上下文後的新表示:
= concat([輸出_head1[手柄], 輸出_head2[手柄], 輸出_head3[手柄], 輸出_head4[手柄]])
= [0.004, 0.004, ..., 0.003, # head1的16維
0.003, -0.002, ..., 0.001, # head2的16維
-0.001, 0.005, ..., -0.003, # head3的16維
0.002, -0.004, ..., 0.002] # head4的16維
= [64維向量] # 但幾乎是噪聲值
步驟7: 計算預測分數
對應代碼位置: UniversalDIENModel 的 call 方法中的預測網絡部分
# CTR預測
combined_features = tf.concat([final_interest, target_projected], axis=1)
prediction = self.prediction_network(combined_features, training=training)
以及 _build_prediction_network 方法
def _build_prediction_network(self):
"""構建通用的CTR預測網絡"""
return tf.keras.Sequential([
layers.Dense(self.hidden_size, activation="relu", name="predict_dense_1"),
layers.Dropout(self.dropout_rate, name="predict_dropout_1"),
DiceActivation(self.hidden_size, name="dice_1"),
layers.Dense(self.hidden_size // 2, activation="relu", name="predict_dense_2"),
layers.Dropout(self.dropout_rate, name="predict_dropout_2"),
DiceActivation(self.hidden_size // 2, name="dice_2"),
layers.Dense(1, activation="sigmoid", name="predict_output")
], name="prediction_network")
# 使用最終輸出[手柄]與候選商品[耳機]計算相似度
預測分數 = 最終輸出[手柄] · 耳機_embedding
= [0.004, 0.004, ..., 0.002] · [0.1, 0.7, ..., 0.5]
= 0.15 # 隨機小數值
# 通過Sigmoid轉為概率
預測概率 = sigmoid(0.15) = 0.537 # 接近0.5,表示第一輪模型預測結果無參考性
步驟8: 計算損失
對應代碼位置: DIENTrainer 的 train_step 方法
def train_step(self, user_behavior, behavior_length, target_item, labels,
auxiliary_weight: float = 0.5):
"""單步訓練"""
with tf.GradientTape() as tape:
predictions, details = self.model(
[user_behavior, behavior_length, target_item],
training=True,
return_details=True
)
main_loss = self.loss_fn(labels, predictions) # 主損失:二元交叉熵
total_loss = main_loss + auxiliary_weight * details['auxiliary_loss'] # 總損失包含輔助損失
其中 self.loss_fn 在初始化時定義為:
self.loss_fn = tf.keras.losses.BinaryCrossentropy() # 二元交叉熵損失
# 正樣本的損失(標籤=1,預測=0.537)
loss = -[1 * log(0.537) + 0 * log(0.463)]
= -log(0.537)
= 0.621 # 損失較大,因為預測不準確
# 如果是負樣本(標籤=0,預測=0.537)
loss_negative = -[0 * log(0.537) + 1 * log(0.463)]
= -log(0.463)
= 0.770 # 損失更大,因為預測概率偏高
步驟9: 反向傳播更新參數
對應代碼位置: DIENTrainer 的 train_step 方法
gradients = tape.gradient(total_loss, self.model.trainable_variables)
self.optimizer.apply_gradients(zip(gradients, self.model.trainable_variables))
這裏 tape.gradient() 計算所有可訓練參數的梯度,optimizer.apply_gradients() 根據梯度更新模型參數。
● 總結:步驟0-9與代碼的對應關係
| 步驟 | 描述 | 對應代碼位置
| 步驟0 | 隨機初始化W_q, W_k, W_v | MultiHeadTemporalAttention.build()
| 步驟1 | 準備訓練數據 | UniversalDIENDataGenerator.generate_training_samples()
| 步驟2 | 前向傳播-QKV投影 | MultiHeadTemporalAttention.call()
| 步驟3 | 多頭分割 | MultiHeadTemporalAttention._reshape_to_multi_heads()
| 步驟4 | 計算注意力分數 | MultiHeadTemporalAttention.call()
| 步驟5 | 加權求和Value | MultiHeadTemporalAttention.call()
| 步驟6 | 拼接多頭輸出 | MultiHeadTemporalAttention.call()
| 步驟7 | 計算預測分數 | UniversalDIENModel.call()
| 步驟8 | 計算損失 | DIENTrainer.train_step()
| 步驟9 | 反向傳播更新參數 | DIENTrainer.train_step()
核心流程:
1. 數據準備 → 2. 嵌入層 → 3. 興趣提取層(GRU+Attention) → 4. 興趣演化層(AUGRU) → 5. 預測網絡 → 6. 損失計算 → 7. 反向傳播
W_q矩陣的參數根據損失函數來更新(朝着減少損失函數值的方向),從而達到優化模型的效果
鏈式法則組合所有梯度
∂Loss/∂W_q = ∂Loss/∂ŷ
× ∂ŷ/∂final_representation
× ∂final_representation/∂output[手柄]
× ∂output[手柄]/∂attention[手柄→主機]
× ∂attention[手柄→主機]/∂score[手柄→主機]
× ∂score[手柄→主機]/∂Q[手柄]
× ∂Q[手柄]/∂W_q
梯度下降更新learning_rate = 0.001
W_q_new = W_q - learning_rate × ∂Loss/∂W_q
核心邏輯:
1. 損失函數測量模型預測與真實標籤的差距
2. 梯度指明參數應該往哪個方向調整,才能減小損失
3. 梯度下降按照梯度的反方向更新參數(因為我們要減小損失)
自注意力矩陣解析
自注意力權重矩陣依賴損失函數的梯度更新
attention_weights.shape = [batch, heads, seq, seq]= [32,4,50,50]
|
索引位置 |
維度名稱 |
含義 |
示例值 |
|
第1維 [0] |
batch |
批次中的第幾個用户 |
0:第1個用户 |
|
第2維 [0] |
heads |
第幾個注意力頭 |
0:第1個頭 |
|
第3維 [i] |
query_position |
查詢位置(行) |
i=2表示"手柄" |
|
第4維 [j] |
key_position |
被關注位置(列) |
j=4表示"耳機" |
attention_weights[0, 0]:第1個樣本 第1個注意力頭
attention_weights[0, 0] =
主機 手機 手柄 護膚 耳機
主機 [[0.25, 0.05, 0.30, 0.05, 0.35], ← 主機關注其他位置的權重
手機 [0.05, 0.70, 0.05, 0.10, 0.10], ← 手機關注其他位置的權重
手柄 [0.28, 0.05, 0.22, 0.05, 0.40], ← 手柄關注其他位置的權重
護膚 [0.05, 0.10, 0.05, 0.70, 0.10], ← 護膚關注其他位置的權重
耳機 [0.30, 0.05, 0.35, 0.05, 0.25]] ← 耳機關注其他位置的權重
規則1: 每行和為1(Softmax歸一化):sum([0.25, 0.05, 0.30, 0.05, 0.35]) = 1.0
規則2: 行表示"誰在關注",列表示"被關注的對象"
業務含義解析:
主機 [0.25, 0.05, 0.30, 0.05, 0.35]
自己 手機 手柄 護膚 耳機
- 0.25(自己):保留自身特徵(對角線值)
- 0.30(手柄):高度關注手柄 → 主機+手柄是核心遊戲體驗
- 0.35(耳機):主機最關注耳機 → 發現"遊戲耳機"是遊戲生態的關鍵配件
- 0.05(護膚/手機殼):幾乎不關注 → 判斷為噪聲行為
當用户瀏覽"主機"時,模型學到:應該同時考慮"手柄"和"耳機"的信息,因為它們構成完整的遊戲生態
手機 [0.05, 0.70, 0.05, 0.10, 0.10]
主機 自己 手柄 護膚 耳機
解讀:
- 0.70(自己):極高自關注 → 無法找到相關上下文
- 其他位置均≤0.10:與所有其他行為都弱相關
- 對角線值遠高於非對角線 → 典型的"孤立點"模式
業務價值:"手機"可能是: 誤點擊 - 用户無意中點進去,臨時需求 - 與核心興趣無關
矩陣作用:
1、興趣聚類
自動發現用户興趣羣組:
遊戲生態羣組 = {主機, 手柄, 耳機} # 高權重互相關聯
孤立興趣點 = {手機, 護膚} # 高自關注,低互關注
2、噪聲過濾
推薦"遊戲耳機"時,模型只看重遊戲相關行為,自動忽略"手機"和"護膚"的干擾。
3、時序關係建模
傳統RNN只能看到: t1 → t2 → t3 → t4 → t5(單向依賴)
自注意力可以看到:
主機(t1) ←→ 耳機(t5) 權重0.35(跨越4個時間步的長期依賴)
即使"主機"和"耳機"在序列中相隔很遠,模型也能發現它們的強關聯性。
自注意力矩陣與多頭機制的關係
4個頭對應4個矩陣,每個頭因為關注重點不一樣,權重矩陣也不一樣,每個頭單獨按照權重矩陣計算商品上下文後會再彙總
頭0:類目相關性(上面舉例的矩陣)
attention_weights[0, 0] =
主機 手機 手柄 護膚 耳機
主機 [[0.25, 0.05, 0.30, 0.05, 0.35], # 關注遊戲生態
手機 [0.05, 0.70, 0.05, 0.10, 0.10], # 孤立
手柄 [0.28, 0.05, 0.22, 0.05, 0.40], # 關注遊戲生態
護膚 [0.05, 0.10, 0.05, 0.70, 0.10], # 孤立
耳機 [0.30, 0.05, 0.35, 0.05, 0.25]] # 關注遊戲生態
- 遊戲類商品互相關注(主機↔手柄↔耳機)
- 非遊戲類商品孤立(手機、護膚)
頭1:價格敏感性
attention_weights[0, 1] =
主機 手機 手柄 護膚 耳機
主機 [[0.40, 0.05, 0.15, 0.35, 0.05], # 關注高價商品(主機、護膚)
手機 [0.35, 0.50, 0.10, 0.10, 0.20], # 中檔價位
手柄 [0.15, 0.10, 0.30, 0.10, 0.35], # 關注中低價配件類(手柄、耳機)
護膚 [0.10, 0.05, 0.10, 0.45, 0.05], # 關注高價商品
耳機 [0.10, 0.15, 0.40, 0.10, 0.25]] # 關注中低價配件類
- 主機(3000元) ↔手機(5000元)(高價商品羣組)
- 手柄(300元) ↔ 耳機(400元)(配件價格羣組)
頭2:時間序列模式
attention_weights[0, 2] =
主機 手機 手柄 護膚 耳機
主機 [[0.20, 0.25, 0.30, 0.15, 0.10], # 更關注時間臨近的(t2,t3)
手機 [0.20, 0.30, 0.25, 0.15, 0.10], # 向前看模式
手柄 [0.15, 0.15, 0.25, 0.25, 0.20], # 均勻分佈
護膚 [0.10, 0.15, 0.20, 0.30, 0.25], # 向後看模式
耳機 [0.05, 0.10, 0.15, 0.25, 0.45]] # 強自關注(最近行為)
頭3:品牌忠誠度
attention_weights[0, 3] =
主機 手機 手柄 護膚 耳機
主機 [[0.50, 0.05, 0.30, 0.05, 0.10], # 高自關注(品牌忠誠)
手機 [0.05, 0.80, 0.05, 0.05, 0.05], # 極高自關注
手柄 [0.25, 0.05, 0.55, 0.05, 0.10], # 高自關注
護膚 [0.05, 0.05, 0.05, 0.75, 0.10], # 高自關注
耳機 [0.10, 0.05, 0.10, 0.05, 0.70]] # 高自關注
- 對角線值普遍高 → 用户傾向於重複購買同類/同品牌,適用於識別"品牌忠誠用户"
多頭如何協同工作(代碼層面)
步驟1:每個頭獨立計算注意力
在 MultiHeadTemporalAttention.call() 中:
1. 輸入分割成多個頭
query.shape = [32, 4, 50, 16] # 4個頭,每個頭16維
key.shape = [32, 4, 50, 16]
value.shape = [32, 4, 50, 16]
2. 每個頭獨立計算注意力分數
attention_scores = tf.matmul(query, key, transpose_b=True)
[32, 4, 50, 50] - 4個頭同時計算,但使用不同的參數
3. 每個頭獨立生成權重矩陣
attention_weights = tf.nn.softmax(attention_scores, axis=-1)
attention_weights[0, 0] - 頭0的權重(類目相關性)
attention_weights[0, 1] - 頭1的權重(價格敏感性)
attention_weights[0, 2] - 頭2的權重(時序模式)
attention_weights[0, 3] - 頭3的權重(品牌忠誠度)
步驟2:每個頭生成自己的上下文向量
4. 每個頭用自己的權重加權 value
context = tf.matmul(attention_weights, value)
context.shape = [32, 4, 50, 16]
利用自注意力生成"主機"的上下文向量:
context[0, :, 0, :] = [
# 頭0的上下文(基於類目相關性權重)→ [16維向量] 突出遊戲生態特徵
0.25×value[主機] + 0.05×value[手機] + 0.30×value[手柄] + 0.05×value[護膚] + 0.35×value[耳機]
# 頭1的上下文(基於價格敏感性權重)→ [16維向量] 突出高價商品特徵
0.40×value[主機] + 0.35×value[手機] + 0.15×value[手柄] + 0.05×value[護膚] + 0.05×value[耳機]
# 頭2的上下文(基於時序模式權重) → [16維向量] 突出臨近時序特徵
0.20×value[主機] + 0.25×value[手機] + 0.30×value[手柄] + 0.15×value[護膚] + 0.10×value[耳機]
# 頭3的上下文(基於品牌忠誠度權重)→ [16維向量] 突出自身品牌特徵
0.50×value[主機] + 0.05×value[手機] + 0.30×value[手柄] + 0.05×value[護膚] + 0.10×value[耳機]
]
步驟3:合併多個頭的輸出
對於"主機"位置:
context[0, 0] = [頭0的16維, 頭1的16維, 頭2的16維, 頭3的16維] = 64維完整向量
業務含義: "主機"的最終表示同時包含:
- 類目信息(頭0)
- 價格信息(頭1)
- 時序信息(頭2)
- 品牌信息(頭3)
為什麼需要分割多頭?
場景:預測用户是否點擊"遊戲鍵盤"
用户歷史: [主機(t1), 手機(t2), 手柄(t3), 護膚(t4), 耳機(t5)]
候選商品: 遊戲鍵盤
單頭模型(只有頭0)
比如只能學到類目相關性:
主機 → 關注: 手柄(0.30), 耳機(0.35)
預測: 用户喜歡遊戲生態 → 推薦遊戲鍵盤
缺失其他維度信息:
不知道用户的價格敏感度(可能只買高端鍵盤)
不知道用户的時序偏好(可能最近興趣已轉移)
不知道用户的品牌忠誠度(可能只買羅技品牌)
多頭模型(4個頭)
頭0: 類目相關性:主機 → 關注遊戲生態 → 遊戲鍵盤匹配
頭1: 價格敏感性:主機(3000元) + 手機(9000元) → 傾向高價商品 → 推薦高端鍵盤(1000元)
頭2: 時序模式:最新行為是耳機(t5) → 用户當前興趣仍在遊戲 → 推薦遊戲鍵盤
頭3: 品牌忠誠度:主機(索尼)、手柄(索尼)、耳機(索尼) → 品牌忠誠 → 推薦索尼鍵盤
最終綜合判斷:
CTR預測 = 加權融合(類目0.9, 價格0.8, 時序0.85, 品牌0.95) = 0.875(高點擊率)
多頭注意力如何通過梯度下降自動學習不同的關聯模式
模型自己通過梯度下降學到了不同模式,我們事後分析發現它們對應品類、價格等
階段1:隨機初始化(沒有任何含義)
# 創建多頭注意力
mha = MultiHeadTemporalAttention(hidden_size=64, num_heads=4)
# 初始狀態:4個頭的權重完全隨機
頭1的Q投影矩陣: 隨機值 [[0.02, -0.15, 0.33, ...], ...]
頭1的K投影矩陣: 隨機值 [[-0.08, 0.21, -0.11, ...], ...]
頭1的V投影矩陣: 隨機值 [[0.14, -0.05, 0.29, ...], ...]
頭2的Q投影矩陣: 隨機值(與頭1不同)
頭2的K投影矩陣: 隨機值
頭2的V投影矩陣: 隨機值
頭3、頭4類似,此時: 4個頭完全不知道要捕捉什麼模式
階段2:前向傳播計算注意力
用户A的歷史行為
歷史序列 = [耳機(ID=15), 數據線(ID=20), 充電寶(ID=25)]
目標商品 = 藍牙音箱(ID=30)
真實標籤 = 1(用户點擊了)
嵌入後(簡化為2維)
耳機嵌入 = [0.8, 0.3]
數據線嵌入 = [0.2, 0.9]
充電寶嵌入 = [0.7, 0.4]
4個頭計算注意力(初始階段)
# 頭1(隨機權重)
Q1 = 投影(輸入, W_Q1) → 隨機計算
K1 = 投影(輸入, W_K1) → 隨機計算
頭1的attention_weights = Q1 × K1^T → [0.3, 0.4, 0.3] # 隨機的關聯
# 頭2(不同的隨機權重)
頭2的attention_weights = Q2 × K2^T → [0.5, 0.2, 0.3] # 不同的隨機關聯
# 頭3、頭4類似,都是隨機的
階段3:預測並計算損失
最終預測(利用4個頭的信息)
預測CTR = 0.4 # 預測用户有40%概率點擊
真實標籤 = 1 # 用户實際點擊了
損失函數(二元交叉熵)
loss = -[1 × log(0.4) + 0 × log(1-0.4)]= -log(0.4)
= 0.916 # 損失較大,預測不準
階段4:反向傳播梯度
損失 Loss = 0.916
↓ ∂Loss/∂預測
輸出層 Dense(1, sigmoid)
↓ ∂Loss/∂context
注意力加權求和 context = Σ(attention_weights × V)
↓ ∂Loss/∂attention_weights ∂Loss/∂V
Softmax(Q × K^T / √d)
↓ ∂Loss/∂Q ∂Loss/∂K
Q/K/V投影矩陣
↓ ∂Loss/∂W_Q ∂Loss/∂W_K ∂Loss/∂W_V
各個頭的參數
梯度更新示例
假設訓練數據中有這樣的模式:
- 買耳機的用户,80%會買充電寶
- 買數據線的用户,60%會買手機殼
# 頭1在某次迭代中計算出:耳機的自注意力
頭1的attention_weights = [耳機:0.1, 數據線:0.2, 充電寶:0.7]
發現了"耳機→充電寶"的關聯
# 基於這個關聯的預測
預測 = 0.75 # 比之前的0.4好多了
損失 = -log(0.75) = 0.288 # 損失降低
# 反向傳播
梯度告訴頭1: "你這樣做是對的,繼續強化這個模式"
W_Q1 += learning_rate × 正梯度 # 強化"耳機→充電寶"的Q投影
W_K1 += learning_rate × 正梯度 # 強化"耳機→充電寶"的K投影
階段5:不同頭學到不同模式
原因1:初始化不同
頭1的初始權重: [0.02, -0.15, 0.33, ...]
頭2的初始權重: [-0.08, 0.21, -0.11, ...] # 完全不同
# 導致初期計算的注意力分數不同
頭1注意力: [0.3, 0.4, 0.3]
頭2注意力: [0.5, 0.2, 0.3]
原因2:數據中存在多種有用的模式
# 訓練數據中的真實規律
模式1: 買耳機 → 買充電寶(品類關聯)
模式2: 買低價商品 → 買低價商品(價格關聯)
模式3: 最近行為 > 早期行為(時序關聯)
模式4: 買小米手機 → 買小米配件(品牌關聯)
原因3:梯度優化的自組織
# 第1000次迭代後
頭1偶然在"品類關聯"上效果好 → 梯度強化這個方向 → 越來越擅長品類
頭2偶然在"價格關聯"上效果好 → 梯度強化這個方向 → 越來越擅長價格
頭3偶然在"時序關聯"上效果好 → 梯度強化這個方向 → 越來越擅長時序
頭4偶然在"品牌關聯"上效果好 → 梯度強化這個方向 → 越來越擅長品牌
數學過程
1. 損失函數
# 主損失(CTR預測)
L_main = -Σ [y × log(ŷ) + (1-y) × log(1-ŷ)]
# 輔助損失(興趣演化)
L_aux = -Σ [log(P(正確的下一個興趣)) + log(1 - P(錯誤的下一個興趣))]
# 總損失
L_total = L_main + 0.5 × L_aux
2. 梯度計算(鏈式法則)
以頭1的Q投影矩陣為例:
∂L/∂W_Q1 = ∂L/∂預測 × ∂預測/∂context × ∂context/∂attention_weights
× ∂attention_weights/∂scores × ∂scores/∂Q × ∂Q/∂W_Q1
梯度計算舉例:
用户歷史 = [耳機, 充電寶, 數據線]
目標商品 = 藍牙音箱
真實標籤 = 1(點擊)
# 頭1當前的注意力分數
頭1注意力 = softmax([Q1×K1_耳機, Q1×K1_充電寶, Q1×K1_數據線]) = [0.6, 0.3, 0.1] # 重點關注耳機
# 基於這個加權的預測
context1 = 0.6×V_耳機 + 0.3×V_充電寶 + 0.1×V_數據線
預測 = sigmoid(DNN(context1 + context2 + context3 + context4)) = 0.85 # 預測概率
# 損失
loss = -log(0.85) = 0.163
# 反向傳播
∂loss/∂(注意力權重[耳機]) = -0.05 # 負梯度:應該減少對耳機的關注
∂loss/∂(注意力權重[充電寶]) = +0.08 # 正梯度:應該增加對充電寶的關注!
# 更新W_Q1
W_Q1 -= learning_rate × ∂loss/∂W_Q1
# 結果:下次會更關注"耳機→充電寶"的關聯
多次迭代後的演化
# 初始(第1次迭代)
頭1注意力: [0.33, 0.33, 0.34] # 均勻分佈,沒有模式
頭2注意力: [0.32, 0.35, 0.33]
頭3注意力: [0.34, 0.33, 0.33]
頭4注意力: [0.33, 0.34, 0.33]
# 訓練10000次後(收斂)
頭1注意力: [0.85, 0.10, 0.05] # 強烈關注品類關聯(耳機↔充電寶)
頭2注意力: [0.05, 0.90, 0.05] # 強烈關注價格關聯(數據線↔手機殼)
頭3注意力: [0.10, 0.15, 0.75] # 強烈關注時序關聯(最近 > 早期)
頭4注意力: [0.60, 0.30, 0.10] # 關注品牌關聯(小米生態鏈)
訓練過程動畫
迭代1:
頭1: [●●●●] 隨機關聯
頭2: [●●●●] 隨機關聯
頭3: [●●●●] 隨機關聯
頭4: [●●●●] 隨機關聯
↓ 梯度更新
迭代100:
頭1: [█●●●] 開始偏向某方向
頭2: [●█●●] 偏向不同方向
頭3: [●●█●] 又不同
頭4: [●●●█] 再不同
↓ 繼續更新
迭代10000:
頭1: [█████ ] 品類關聯專家
頭2: [ █████] 價格關聯專家
頭3: [ ███ ] 時序關聯專家
頭4: [███ ] 品牌關聯專家
為什麼會自動分化?(信息理論)
# 如果4個頭都學同樣的模式(比如都學品類)
頭1: 品類關聯 → 貢獻信息量 I
頭2: 品類關聯 → 額外貢獻 ≈0(重複了)
頭3: 品類關聯 → 額外貢獻 ≈0
頭4: 品類關聯 → 額外貢獻 ≈0
總信息量 ≈ I
# 如果4個頭學不同模式(梯度下降自動選擇此模式)
頭1: 品類關聯 → 貢獻 I1
頭2: 價格關聯 → 貢獻 I2(新信息)
頭3: 時序關聯 → 貢獻 I3(新信息)
頭4: 品牌關聯 → 貢獻 I4(新信息)
總信息量 = I1 + I2 + I3 + I4 >> I
|
問題 |
答案 |
|
頭是如何學到不同模式的? |
通過隨機初始化 + 梯度下降自動分化 |
|
和損失函數的關係? |
損失函數提供訓練信號,梯度指導參數更新方向 |
|
品類、價格等標籤從哪來? |
事後分析發現的,不是事先設計 |
|
為什麼不都學一樣的? |
學不同模式的總信息量更大,損失更低 |
|
學習到的模式 |
取決於數據和初始化 |
注:"語義解釋"都是事後賦予的,模型只看數學
模型不知道"遊戲"、"音頻"、"娛樂"
只知道:根據損失函數對參數的梯度來調整參數 → 損失函數變小 → 獲得獎勵
模型的優化目標(唯一且純粹):
minimize Loss = -Σ [y_i × log(ŷ_i) + (1-y_i) × log(1-ŷ_i)]
優化方法(唯一且機械):
W_q ← W_q - learning_rate × ∂Loss/∂W_q
W_k ← W_k - learning_rate × ∂Loss/∂W_k
W_v ← W_v - learning_rate × ∂Loss/∂W_v
數學驅動 vs 語義解釋
數據中的規律(語義解釋):
"遊戲相關商品" 的用户傾向點擊 "音頻設備"
"遊戲相關商品" 的用户不傾向點擊 "廚具"
模型的視角(數學驅動):
當 W_q 使得:
Q[手柄] 與 K[耳機] 的相似度高
Q[手柄] 與 K[廚具] 的相似度低
則:
正樣本的預測分數高 → Loss_正 低
負樣本的預測分數低 → Loss_負 低
梯度下降自然會推動 W_q 往這個方向更新
相似商品的嵌入向量如何學習
相似商品的嵌入向量是通過"共現模式"和"損失函數的優化目標"自動學習
本質上是 Word2Vec 的"分佈式假設"在推薦系統中的應用:"經常出現在相似上下文中的商品具有相似的語義"
# 訓練數據(來自真實用户行為)
訓練樣本1: [遊戲主機=10, 遊戲手柄=15] → 遊戲耳機=18 (標籤=1,用户點擊了)
訓練樣本2: [遊戲主機=10, 遊戲手柄=15] → 廚具=50 (標籤=0,用户沒點擊)
訓練樣本3: [遊戲手柄=15, 遊戲盤=20] → 遊戲耳機=18 (標籤=1)
訓練樣本4: [遊戲手柄=15, 遊戲盤=20] → 衣服=60 (標籤=0)
訓練樣本5: [遊戲耳機=18, 遊戲盤=20] → 遊戲手柄=15 (標籤=1)
對應代碼
class UniversalDIENDataGenerator:
def _create_samples(self, user_sequences: List[List[int]]):
"""從序列創建訓練樣本(正樣本+負樣本)"""
samples = {
'behavior': [],
'length': [],
'target': [],
'label': []
}
for user_seq in user_sequences:
if len(user_seq) < 2:
continue
# 生成正樣本:[主機, 手柄] → 耳機 (標籤=1)
historical = user_seq[:-1] # [主機=10, 手柄=15]
positive_target = user_seq[-1] # 耳機=18
samples['behavior'].append(historical)
samples['target'].append([positive_target])
samples['label'].append([1]) # 標籤=1
samples['length'].append(len(historical))
# 生成負樣本:[主機, 手柄] → 廚具 (標籤=0)
for _ in range(self.config.negative_sample_ratio):
negatives = [item for item in self.all_item_ids if item not in user_seq]
if negatives:
negative_target = np.random.choice(negatives) # 廚具=50
samples['behavior'].append(historical)
samples['target'].append([negative_target])
samples['label'].append([0]) # 標籤=0
samples['length'].append(len(historical))
人類視角:
遊戲相關商品(10, 15, 18, 20)經常一起出現
遊戲商品和非遊戲商品(50, 60)很少一起出現
模型需要學習:
讓 embedding[10], [15], [18], [20] 在向量空間中靠近
讓 embedding[50], [60] 遠離遊戲商品
機制1:損失函數梯度驅動相似性學習(核心原因“共現”)
訓練樣本1:[主機, 手柄] → 耳機 (標籤=1)
# 初始化嵌入向量(完全隨機)
embedding[10_主機] = [ 0.02, -0.05, 0.01, 0.03] # 4維簡化示例
embedding[15_手柄] = [-0.03, 0.04, -0.02, 0.01]
embedding[18_耳機] = [ 0.01, 0.02, 0.05, -0.04]
embedding[50_廚具] = [ 0.03, -0.01, 0.02, 0.05]
對應代碼
class UniversalDIENModel(Model):
def build(self, input_shape):
# 創建嵌入層(自動隨機初始化)
self.embedding_layer = layers.Embedding(
self.config.feature_dim, # 100個商品
self.config.embedding_dim, # 每個16維
name="feature_embedding"
)
# 內部會創建權重矩陣 [100, 16],初始化為隨機值
初始化發生在哪裏?
# Keras的Embedding層默認使用"uniform"初始化器
# 等價於:
embedding_matrix = tf.random.uniform(
shape=(100, 16),
minval=-0.05,
maxval=0.05
)
# 此時計算相似度(隨機,無規律)
similarity(主機, 手柄) = 0.02×(-0.03) + (-0.05)×0.04 + ... = -0.0012 # 接近0
similarity(主機, 耳機) = 0.02×0.01 + (-0.05)×0.02 + ... = -0.0008
similarity(手柄, 耳機) = (-0.03)×0.01 + 0.04×0.02 + ... = 0.0005
similarity(手柄, 廚具) = (-0.03)×0.03 + 0.04×(-0.01) + ... = -0.0013
問題:所有相似度都接近0,模型無法區分相關和不相關的商品
# 步驟1:初始化嵌入向量
behavior_emb = [embedding[10], embedding[15]] # 行為序列
target_emb = embedding[18] # 目標商品
class UniversalDIENModel(Model):
def call(self, inputs, training=False, return_details=False):
user_behavior, behavior_length, target_item = inputs
# 步驟1:查表獲取嵌入向量
behavior_embeddings = self.embedding_layer(user_behavior)
# user_behavior = [[10, 15]] → behavior_embeddings = [embedding[10], embedding[15]]
# 形狀:[batch=1, seq=2, emb_dim=16]
target_embedding = tf.squeeze(self.embedding_layer(target_item), axis=1)
# target_item = [[18]] → target_embedding = embedding[18]
# 形狀:[batch=1, emb_dim=16]
# 步驟2:興趣提取(GRU + Attention)
# 簡化:假設最終興趣表示是行為序列的加權平均
interest = 0.5 * embedding[10] + 0.5 * embedding[15]
= 0.5 * [ 0.02, -0.05, 0.01, 0.03] + 0.5 * [-0.03, 0.04, -0.02, 0.01]
= [-0.005, -0.005, -0.005, 0.020]
class UniversalDIENModel(Model):
def call(self, inputs, training=False, return_details=False):
# ...(續上)
# 步驟2:興趣提取(實際代碼更復雜,使用GRU+注意力)
interest_states, attention_weights = self.interest_extractor(
behavior_embeddings, # [embedding[10], embedding[15]]
behavior_length,
training=training
)
# interest_states: [batch, seq, hidden_size=64]
興趣提取的詳細實現:
class InterestExtractor(layers.Layer):
def call(self, inputs, sequence_lengths, training=False):
# GRU提取時序特徵
gru_output = self.gru_layer(inputs, mask=mask, training=training)
# 將 [embedding[10], embedding[15]] 編碼為時序表示
# 多頭注意力增強
attention_output, attention_weights = self.temporal_attention(
gru_output, mask=mask, training=training
)
# 最終的興趣表示(GRU+多頭自注意力)
output = self.layer_norm(attention_output + gru_output)
return output, attention_weights
# 步驟3:計算預測分數(即生成的最終興趣表示與目標商品的相似度)
score = interest · target_emb
= [-0.005, -0.005, -0.005, 0.020] · [ 0.01, 0.02, 0.05, -0.04]
= -0.005×0.01 + (-0.005)×0.02 + (-0.005)×0.05 + 0.020×(-0.04)
= -0.0012
# 步驟4:Sigmoid轉概率
prediction = sigmoid(-0.0012) ≈ 0.4997 # 接近0.5,隨機猜測!
損失計算
# 真實標籤:1(用户點擊了耳機)
# 預測概率:0.4997(模型説"不太可能點擊")
loss = -[1 × log(0.4997) + 0 × log(0.5003)]
= -log(0.4997)
= 0.6934 # 損失很大!
反向傳播
訓練樣本1: [遊戲主機=10, 遊戲手柄=15] → 遊戲耳機=18 (標籤=1,用户點擊了)
# 鏈式法則:損失對嵌入向量的梯度(嵌入向量 = 可訓練參數)
∂loss/∂embedding[10] = (∂loss/∂prediction) × (∂prediction/∂interest) × (∂interest/∂embedding[10])
∂loss/∂embedding[15] = (∂loss/∂prediction) × (∂prediction/∂interest) × (∂interest/∂embedding[15])
∂loss/∂embedding[18] = (∂loss/∂prediction) × (∂prediction/∂interest) × (∂interest/∂embedding[18])
梯度:
∂loss/∂embedding[10] = [-0.0025, -0.005, -0.0125, 0.010]
∂loss/∂embedding[15] = [-0.0025, -0.005, -0.0125, 0.010] # 遊戲主機和遊戲手柄的梯度相同
梯度更新
learning_rate = 0.1
更新主機的嵌入
embedding[10] = embedding[10] - lr × ∂loss/∂embedding[10]
= [ 0.02, -0.05, 0.01, 0.03] - 0.1 × [-0.0025, -0.005, -0.0125, 0.010]
= [ 0.02025, -0.0495, 0.01125, 0.029]
更新手柄的嵌入
embedding[15] = [-0.03, 0.04, -0.02, 0.01] - 0.1 × [-0.0025, -0.005, -0.0125, 0.010]
= [-0.02975, 0.0405, -0.01875, 0.009]
更新耳機的嵌入
embedding[18] = [ 0.01, 0.02, 0.05, -0.04] - 0.1 × [ 0.0025, 0.0025, 0.0025, -0.010]
= [ 0.00975, 0.01975, 0.04975, -0.039]
為什麼主機和手柄的嵌入變得相近?:梯度的方向一致
∂loss/∂embedding[10] = [-0.0025, -0.005, -0.0125, 0.010]
∂loss/∂embedding[15] = [-0.0025, -0.005, -0.0125, 0.010] # 梯度相同
原因:它們在同一個行為序列中,通過 interest = 0.5 * emb[10] + 0.5 * emb[15]
損失對 interest 的梯度會"平等地"傳遞給序列中的每個商品
經過多輪訓練,它們會朝着相同的方向移動 → 逐漸靠近
訓練樣本2:[主機, 手柄] → 廚具 (標籤=0)
使用主機和手柄更新後的嵌入
interest = 0.5 * embedding[10] + 0.5 * embedding[15]
= 0.5 * [ 0.02025, -0.0495, 0.01125, 0.029]
+ 0.5 * [-0.02975, 0.0405, -0.01875, 0.009]
= [-0.00475, -0.0045, -0.00375, 0.019]
target_emb = embedding[50] = [ 0.03, -0.01, 0.02, 0.05]
score = interest · target_emb
= [-0.00475, -0.0045, -0.00375, 0.019] · [ 0.03, -0.01, 0.02, 0.05]
= -0.0001425 + 0.000045 - 0.000075 + 0.00095
= 0.0007775
prediction = sigmoid(0.0007775) ≈ 0.5002
損失計算
# 真實標籤:0(用户沒點擊廚具)
# 預測概率:0.5002
loss = -[0 × log(0.5002) + 1 × log(0.4998)]
= -log(0.4998)
= 0.6936 # 損失很大
反向傳播
∂loss/∂prediction = -(0/0.5002 - 1/0.4998) = 2.001 # 需要減小預測值
# 這次梯度方向相反
∂loss/∂embedding[10] = 2.001 × [ 0.03, -0.01, 0.02, 0.05] × 0.5
= [ 0.030, -0.010, 0.020, 0.050]
∂loss/∂embedding[15] = [ 0.030, -0.010, 0.020, 0.050]
∂loss/∂embedding[50] = 2.001 × sigmoid'(0.0007775) × interest
= 2.001 × 0.25 × [-0.00475, -0.0045, -0.00375, 0.019]
= [-0.00238, -0.00225, -0.00188, 0.0095]
梯度更新
[遊戲主機=10, 遊戲手柄=15] → 廚具=50
# 更新主機和手柄(推離廚具)
embedding[10] = [ 0.02025, -0.0495, 0.01125, 0.029] - 0.1 × [ 0.030, -0.010, 0.020, 0.050] #這是梯度
= [ 0.01725, -0.0485, 0.00925, 0.024] # 更新後的嵌入值
embedding[15] = [-0.02975, 0.0405, -0.01875, 0.009] - 0.1 × [ 0.030, -0.010, 0.020, 0.050]
= [-0.03275, 0.0415, -0.02075, 0.004] # 與主機同步調整
# 更新廚具嵌入(遠離遊戲商品)
embedding[50] = [ 0.03, -0.01, 0.02, 0.05] - 0.1 × [-0.00238, -0.00225, -0.00188, 0.0095]
= [ 0.030238, -0.009775, 0.020188, 0.04905]
機制2:用户行為序列提供"共現信息"(和機制1的梯度相關)
# 訓練數據統計
商品對的共現次數:
(主機=10, 手柄=15): 出現在同一序列中 100 次
(主機=10, 耳機=18): 出現在同一序列中 80 次
(手柄=15, 耳機=18): 出現在同一序列中 120 次
(手柄=15, 廚具=50): 出現在同一序列中 2 次(幾乎沒有)
高共現 → 梯度方向一致 → 嵌入向量靠近
低共現 → 梯度方向不一致 → 嵌入向量遠離
體現在數據生成:
class UniversalDIENDataGenerator:
def generate_sequences(self, num_users=1000, max_seq_length=20):
"""生成訓練序列"""
user_sequences = []
for _ in range(num_users):
# 隨機生成用户序列(模擬真實行為)
user_seq = np.random.choice(
self.all_item_ids,
size=seq_length,
replace=False
).tolist()
# 如果數據來自真實用户行為,遊戲商品會頻繁共現
# 例如:[主機, 手柄, 耳機, 遊戲盤] 這樣的序列會很多
user_sequences.append(user_seq)
體現在訓練循環:
def train(self, dataset):
for epoch in range(self.config.epochs):
for batch_data in dataset:
# 每個batch都包含多個共現樣本
# batch中可能同時包含:
# - [主機, 手柄] → 耳機
# - [手柄, 耳機] → 遊戲盤
# - [主機, 遊戲盤] → 手柄
# 這些樣本的梯度累積效果 → 遊戲商品嵌入靠近
metrics = self.train_step(...)
機制3:多頭注意力的作用
# 步驟1:計算注意力分數
score[手柄→主機] = Q[手柄] · K[主機] # 手柄對主機的關注度
score[手柄→耳機] = Q[手柄] · K[耳機] # 手柄對耳機的關注度
attention_weights = softmax([score[手柄→主機], score[手柄→耳機], ...])
# 步驟2:加權求和
output[手柄] = attention[手柄→主機] × V[主機]+ attention[手柄→耳機] × V[耳機] + ...
反向傳播時的梯度流動:
∂loss/∂V[耳機] = (∂loss/∂output[手柄]) × attention[手柄→耳機]
為什麼會變相似?
1. 共同的優化目標:V[耳機] 和 V[手柄] 都在優化同一個損失函數
2. 相互耦合的梯度:它們通過注意力權重相互影響對方的梯度
3. 收斂到相似空間:最終它們會收斂到語義相近的表示空間
比如語義相關的詞(如"國王"和"王后")的詞向量會很接近
經常一起出現的實體會有相似的表示
對應代碼
class MultiHeadTemporalAttention(layers.Layer):
def call(self, inputs, mask=None, training=False):
# 步驟1:計算注意力分數
query = self.query_dense(inputs) # [batch, seq, 64]
key = self.key_dense(inputs)
value = self.value_dense(inputs)
# Q[手柄] × K[耳機]^T → 計算手柄對耳機的關注度
attention_scores = tf.matmul(query, key, transpose_b=True)
attention_weights = tf.nn.softmax(attention_scores, axis=-1)
# 如果手柄和耳機經常共現,訓練後 attention_weights[手柄→耳機] 會很大
# 步驟2:加權求和
context = tf.matmul(attention_weights, value)
# context[手柄] = attention[手柄→耳機] × V[耳機] + ...
# 反向傳播時:
# ∂loss/∂V[耳機] ∝ attention[手柄→耳機]
# 高注意力權重 → 大梯度 → V[耳機]和V[手柄]更新方向相似
嵌入向量學習過程
# 初始化(隨機)
embedding[10_主機] = [ 0.02, -0.05, 0.01, 0.03]
embedding[15_手柄] = [-0.03, 0.04, -0.02, 0.01]
embedding[18_耳機] = [ 0.01, 0.02, 0.05, -0.04]
embedding[50_廚具] = [ 0.03, -0.01, 0.02, 0.05]
相似度:
similarity(手柄, 耳機) = -0.0012 # 接近0,無關聯
similarity(手柄, 廚具) = -0.0013
# 訓練100輪後(學到語義)
embedding[10_主機] = [ 0.78, -0.23, 0.45, 0.12]
embedding[15_手柄] = [ 0.82, -0.19, 0.51, 0.09] # 與主機相近!
embedding[18_耳機] = [ 0.71, -0.25, 0.48, 0.14] # 與手柄相近!
embedding[50_廚具] = [-0.15, 0.42, -0.31, 0.67] # 完全不同方向!
相似度:
similarity(手柄, 耳機) = 0.82×0.71 + (-0.19)×(-0.25) + ... = 0.88 # 高相似度
similarity(手柄, 廚具) = 0.82×(-0.15) + (-0.19)×0.42 + ... = -0.53 # 負相關
對應代碼
def train_dien_model(config):
# 完整訓練流程
model = UniversalDIENModel(config)
trainer = DIENTrainer(model, config)
# 訓練多個epoch
history = trainer.train(dataset)
# 每個epoch都會更新嵌入矩陣
# 訓練完成後,查看嵌入向量:
embedding_matrix = model.embedding_layer.get_weights()[0]
# embedding_matrix.shape = [100, 16]
# 提取特定商品的嵌入
embedding_手柄 = embedding_matrix[15] # [0.82, -0.19, 0.51, ...]
embedding_耳機 = embedding_matrix[18] # [0.71, -0.25, 0.48, ...]
# 計算相似度
similarity = np.dot(embedding_手柄, embedding_耳機)
# similarity ≈ 0.88(訓練後)vs 0.0005(訓練前)
可視化嵌入演化的代碼:
def visualize_embedding_evolution():
"""觀察訓練過程中嵌入向量的變化"""
tracked_items = {10: "主機", 15: "手柄", 18: "耳機", 50: "廚具"}
embedding_history = {item_id: [] for item_id in tracked_items}
for epoch in range(100):
# 訓練一個epoch
trainer.train_step(...)
# 記錄當前嵌入
current_embeddings = model.embedding_layer.get_weights()[0]
for item_id in tracked_items:
embedding_history[item_id].append(current_embeddings[item_id].copy())
為什麼會這樣?
1. 正樣本拉近
訓練樣本:[手柄] → 耳機 (標籤=1)
# 梯度更新讓 embedding[手柄] 和 embedding[耳機] 的點積增大
embedding[手柄] ← embedding[手柄] + α × embedding[耳機] # 靠近耳機
embedding[耳機] ← embedding[耳機] + α × embedding[手柄] # 靠近手柄
經過100次這樣的更新 → 兩者夾角變小 → 相似度提高
2. 負樣本推遠
訓練樣本:[手柄] → 廚具 (標籤=0)
# 梯度更新讓 embedding[手柄] 和 embedding[廚具] 的點積減小
embedding[手柄] ← embedding[手柄] - β × embedding[廚具] # 遠離廚具
embedding[廚具] ← embedding[廚具] - β × embedding[手柄] # 遠離手柄
經過100次這樣的更新 → 兩者夾角變大 → 相似度降低
3. 序列內商品互相拉近
訓練樣本:[主機, 手柄] → 任意商品
interest = f(embedding[主機], embedding[手柄])
反向傳播時:
∂loss/∂embedding[主機] 和 ∂loss/∂embedding[手柄] 包含相同的"上下文信息"
結果:主機和手柄的嵌入會朝着相似的方向更新 → 逐漸靠近
總結:
單次訓練迭代的完整路徑
# 1. 數據準備
user_behavior = [[10, 15]] # [主機, 手柄]
target_item = [[18]] # 耳機
labels = [[1]] # 點擊
# 2. 嵌入查表 (UniversalDIENModel.call)
behavior_embeddings = embedding_layer(user_behavior) # 查表
target_embedding = embedding_layer(target_item)
# 3. 興趣提取 (InterestExtractor.call)
interest_states = GRU(behavior_embeddings) + MultiHeadAttention(...)
# 4. 興趣演化 (InterestEvolution.call)
final_interest = AUGRU(interest_states, target_embedding)
# 5. CTR預測 (prediction_network)
prediction = DNN([final_interest, target_embedding])
# 6. 損失計算 (DIENTrainer.train_step)
loss = BinaryCrossentropy(labels, prediction)
機制1:損失函數的優化目標
# 損失函數隱式地定義了"相似性"
經常被點擊的商品,其嵌入會與行為序列中的商品嵌入對齊
機制2:用户行為序列提供共現信息
# 數據中的隱藏規律:
if 商品A和商品B經常出現在同一用户序列中:
embedding[A] 和 embedding[B] 會接收相似的梯度→ 朝着相同方向更新→ 逐漸靠近
if 商品A和商品C從不一起出現:embedding[A] 和 embedding[C] 獨立更新→ 可能朝着不同方向→ 保持距離
機制3:注意力機制放大共現效應
# 多頭注意力:
attention[手柄→耳機] = softmax(Q[手柄] · K[耳機])
# 如果手柄和耳機經常共現 → 模型學會分配高注意力權重
→ 反向傳播時梯度流動更強
→ 嵌入更新更快靠近
相似商品的嵌入向量變得相近,是因為:
1. 損失函數定義了優化目標:最大化行為序列商品與被點擊商品的相似度
2. 用户行為序列提供共現信息:經常一起出現的商品會接收相似的梯度,朝着相同方向更新
3. 反向傳播自動調整嵌入:梯度下降讓能夠預測用户行為的商品嵌入自然地聚集到一起
測試時的商品需要在訓練集中見過,嵌入向量才是有意義
一、商品初始嵌入的邏輯:嵌入層的創建和初始化
# 在 UniversalDIENModel.build() 中
self.embedding_layer = layers.Embedding(
self.feature_dim, # 100 - 詞表大小(支持100個不同的商品ID)
self.embedding_dim, # 16 - 每個商品的嵌入維度
name="feature_embedding")
這會創建一個形狀為 [100, 16] 的權重矩陣:
# 初始化時(隨機值)
embedding_matrix = [
[0.023, -0.045, 0.012, ..., 0.067], # 商品ID=0的嵌入向量(16維)
[0.031, 0.078, -0.023, ..., -0.014], # 商品ID=1的嵌入向量
[-0.056, 0.019, 0.043, ..., 0.032], # 商品ID=2的嵌入向量
...
[0.041, -0.027, 0.065, ..., -0.018] # 商品ID=99的嵌入向量
] # 形狀: [100, 16]
調用嵌入層
# 在 UniversalDIENModel.call() 中
behavior_embeddings = self.embedding_layer(user_behavior) # 行為序列嵌入
target_embedding = self.embedding_layer(target_item) # 目標商品嵌入
關鍵點:行為序列和目標商品共享同一個嵌入層
user_behavior = [[15, 16, 17]] # 用户行為序列
target_item = [[18]] # 目標商品
# 查表過程(訓練和預測階段都調用商品的嵌入向量值)
behavior_embeddings = [
embedding_matrix[15], # 商品15的嵌入向量
embedding_matrix[16], # 商品16的嵌入向量
embedding_matrix[17] # 商品17的嵌入向量
] # 形狀: [1, 3, 16]
target_embedding = [
embedding_matrix[18] # 商品18的嵌入向量
] # 形狀: [1, 1, 16]
【訓練階段(查表 + 更新)】
# 訓練第500輪
user_behavior = [[15, 16, 17]]
target_item = [[18]]
# 步驟1: 查表(從當前的嵌入矩陣獲取向量)
behavior_embeddings = [
embedding_matrix[15], # [0.652, -0.189, 0.423, ...] ← 當前學到的向量
embedding_matrix[16], # [0.598, -0.145, 0.391, ...]
embedding_matrix[17] # [0.723, -0.212, 0.467, ...]
]
# 步驟2: 前向傳播 → 計算loss
# 步驟3: 反向傳播 → 更新嵌入矩陣
∂loss/∂embedding_matrix[15] = [-0.012, 0.034, -0.023, ...]
embedding_matrix[15] -= learning_rate × gradient # 更新嵌入向量值
# 訓練1000輪後(學到了語義)
embedding_matrix[15] = [0.782, -0.234, 0.456, ..., 0.123] # "遊戲手柄"的語義向量
embedding_matrix[18] = [0.691, -0.187, 0.512, ..., 0.098] # "遊戲耳機"的語義向量(與手柄相近)
embedding_matrix[99] = [0.041, -0.027, 0.065, ..., -0.018] # 如果商品99從未出現在訓練數據中,保持隨機值
【預測階段(僅查表)】
# 訓練完成後,嵌入矩陣已固定
embedding_matrix[15] = [0.782, -0.234, 0.456, ...] # 最終學到的語義
# 新用户的待預測數據
user_behavior = [[15, 20, 25]]
target_item = [[30]]
# 查表過程(從訓練好的嵌入矩陣獲取)
behavior_embeddings = [
embedding_matrix[15], # [0.782, -0.234, 0.456, ...] ← 訓練好的語義向量
embedding_matrix[20], # [0.645, -0.198, 0.412, ...]
embedding_matrix[25] # [0.701, -0.221, 0.434, ...]
]
前向傳播 → 得到預測概率
不進行反向傳播,嵌入矩陣保持不變
二、測試集和訓練集的商品必須一樣,商品才能查得學到的語義信息向量
# 階段1:初始化(隨機值)
embedding_matrix[15] = [0.023, -0.045, ...] # 無語義
# 階段2:訓練學習(通過損失函數的梯度更新)
# 訓練樣本:[主機, 手柄] → 耳機 (點擊)
# 反向傳播 → 更新 embedding_matrix[主機], [手柄], [耳機]
# 階段3:收斂(學到語義)
embedding_matrix[15] = [0.782, -0.234, ...] # "遊戲手柄"的語義
embedding_matrix[18] = [0.691, -0.187, ...] # "遊戲耳機"的語義(相近)
# 階段4:預測(凍結,不再更新)
model.embedding_layer.trainable = False
測試集和訓練集的商品必須一致
|
場景 |
商品ID |
嵌入向量 |
預測結果 |
|
訓練集中 |
15(手柄) |
[0.782, -0.234, ...] |
可靠 |
|
測試集中(已訓練) |
15(手柄) |
[0.782, -0.234, ...] |
可靠(相同向量) |
|
測試集中(未訓練) |
99(新商品) |
[0.023, -0.045, ...] |
不可靠(隨機值) |
|
超出範圍 |
100(越界) |
報錯! |
無法處理 |
1. 共享嵌入層:行為序列和目標商品使用同一個 embedding_layer,保證相同ID得到相同向量
2. 訓練覆蓋:訓練數據必須覆蓋所有可能在測試/推理時出現的商品ID
3. 向量凍結:訓練完成後,嵌入向量固定,預測時不再更新
4. 冷啓動處理:新商品需要特殊處理(OOV標記或基於特徵的嵌入)
AUGRU相關概念
AttentionUnit 和 AUGRUCell 的關係:
interest_states: [跑步鞋, 運動襪, 瑜伽墊] (歷史)
target_item: 籃球 (目標)
AttentionUnit計算注意力分數:這個歷史商品與目標商品的相關性是多少:[0.65, 0.52, 0.18]
AUGRUCell:用這個相關性分數來調節興趣更新強度,每步使用對應分數
如果沒有 AttentionUnit:
即GRU(無注意力),所有歷史商品同等權重
問題:瑜伽墊與籃球不相關,卻仍然影響興趣演化
for time_step in range(seq_len):
new_h = GRUCell(current_input, hidden_state) # 跑步鞋、運動襪、瑜伽墊對興趣的影響一樣大
有 AttentionUnit 的 AUGRU
根據相關性動態調節
attention_scores = AttentionUnit(籃球, [跑步鞋, 運動襪, 瑜伽墊]) → [0.65, 0.52, 0.18]
優勢:自動過濾無關商品,精準捕捉與目標相關的興趣
for time_step in range(seq_len):
new_h = AUGRUCell(current_input, hidden_state, attention_scores[t]) # 瑜伽墊的影響被抑制(0.18),跑步鞋影響更大(0.65)
相關性分數[0.65, 0.52, 0.18]更新機制:注意力分數本身不是可訓練參數,但會通過梯度反向傳播更新 AttentionUnit 的內部參數,從而間接改變未來計算的注意力分數
用户序列 [遊戲主機, 手機, 遊戲手柄, 耳機],每個行為都會依次執行一次AUGRU的call方法:
初始狀態:
hidden_state = [0.0, 0.0, 0.0, 0.0] # 初始興趣狀態
第1步:處理"遊戲主機"
inputs = 遊戲主機特徵 = [遊戲性:0.9, 價格:0.8, 品牌:0.7, 性能:0.6]
attention_score = 0.8 # 遊戲主機與目標商品的相關性
# 經過AUGRU計算:
candidate = [遊戲興趣:0.7, 高端偏好:0.6, 品牌傾向:0.5, 性能需求:0.4]
new_hidden_state = [0.56, 0.48, 0.40, 0.32] # 包含遊戲主機興趣的新狀態
第2步:處理"手機"
inputs = 手機特徵 = [遊戲性:0.1, 價格:0.7, 品牌:0.8, 便攜性:0.9]
attention_score = 0.3 # 手機與目標商品的相關性
# 基於上一步的狀態繼續演化:
hidden_state = [0.56, 0.48, 0.40, 0.32] # 來自上一步
candidate = [科技興趣:0.6, 移動需求:0.7, 品牌忠誠:0.5, 便攜重視:0.8]
new_hidden_state = [0.45, 0.52, 0.42, 0.48] # 在遊戲主機基礎上適度(目標商品相關性較低)融入手機興趣的狀態
第3步:處理"遊戲手柄"
inputs = 遊戲手柄特徵 = [遊戲性:0.8, 價格:0.4, 品牌:0.6, 操控性:0.9]
attention_score = 0.9 # 遊戲手柄與目標商品高度相關
# 繼續演化:
hidden_state = [0.45, 0.52, 0.42, 0.48] # 來自上一步
candidate = [遊戲生態:0.8, 操控體驗:0.9, 價格理性:0.3, 配件需求:0.7]
new_hidden_state = [0.68, 0.65, 0.35, 0.55] # 大幅強化(目標商品相關性高)遊戲相關興趣
第4步:處理"耳機"
inputs = 耳機特徵 = [遊戲性:0.3, 價格:0.5, 品牌:0.6, 音頻性:0.9]
attention_score = 0.7 # 耳機與目標商品中等相關
# 最終演化:
hidden_state = [0.68, 0.65, 0.35, 0.55] # 來自上一步
candidate = [音頻體驗:0.7, 遊戲音頻:0.8, 品質要求:0.6, 舒適需求:0.5]
final_interest = [0.67, 0.67, 0.42, 0.53] # 最終興趣狀態
GRU門控機制和注意力調節機制的區別:
gate_kernel和注意力分數[0.65, 0.52, 0.18]更新的區別
gate_kernel和注意力分數的關係:用注意力分數動態調節更新門,modulated_update_gate = attention_score * update_gate
GRU門控(僅序列建模)
GRU更新門的計算(不知道目標商品)
combined = concat([當前輸入, 歷史狀態])
update_gate = sigmoid(combined @ W_gate) # 只看當前商品嵌入和歷史狀態:update_gate的高低取決於和W_gate的匹配度,而非商品嵌入向量的數值大小
W_gate:可訓練的矩陣參數,用於學習"什麼樣的商品特徵組合代表重要信息"(即權重矩陣數值怎麼變換組合讓損失函數最小)
W_gate 的優化過程:
1. 隨機初始化:W_gate 的每個元素隨機賦值
2. 前向傳播:用當前 W_gate 計算預測結果
3. 計算損失:預測值與真實值的差距
4. 反向傳播:計算損失對 W_gate矩陣中每個元素的梯度
5. 梯度下降:沿着梯度反方向微調 W_gate
6. 重複迭代:直到損失函數收斂到最小(找到最優的數值組合,讓損失函數最小)
# 含義:
update_gate = 0.7 # "這個商品信息本身看起來重要(和W_gate匹配度較高),應該70%更新狀態"
判斷邏輯(無注意力調節):
if 當前商品嵌入特徵很突出(比如高價、熱門品牌):
update_gate → 0.9 # "這個商品看起來重要,大幅更新"
elif 當前商品特徵平淡:
update_gate → 0.2 # "這個商品不重要,小幅更新"
問題:
GRU 不知道最終要預測什麼商品,只能根據"商品本身的顯著性"來判斷,可能會讓和目標商品不相關但和W_gate匹配的商品主導興趣狀態
注意力調節
# 注意力分數的計算(明確知道目標商品)
attention_score = AttentionUnit(query=目標商品嵌入,key=當前商品嵌入)
# 含義:
attention_score = 0.9 # "這個商品與目標商品高度相關,應該大幅影響興趣"
判斷邏輯(目標商品指導):
if 當前商品與目標商品相關(品類、功能、場景相似):
attention_score → 0.9 # "與目標高度相關,應該重點學習"
else:
attention_score → 0.1 # "與目標無關,應該忽略"
優勢:明確知道要預測的目標商品,只強化與目標相關的歷史行為,自動過濾無關但可能和W_gate匹配的商品
GRU和AUGRU的區別
標準GRU的問題
用户歷史:[遊戲主機, 耳機, 瑜伽墊, 手柄]
目標:遊戲碟
GRU學到的興趣狀態(未引入注意力調節,更混亂)
final_interest = [
遊戲維度:0.5, # 遊戲和手柄貢獻
健康維度:0.3, # 瑜伽墊貢獻(噪音!)
音樂維度:0.2 # 耳機貢獻(噪音!)]
預測CTR(遊戲碟| 混亂興趣) = 0.6 # 不夠精準
AUGRU的優勢
用户歷史:[遊戲主機, 耳機, 瑜伽墊, 手柄]
目標:遊戲碟
AUGRU學到的興趣狀態(引入注意力調節後更精準)
final_interest = [
遊戲維度:0.85, # 遊戲主機和手柄大幅貢獻
配件維度:0.65, # 手柄貢獻
健康維度:0.05, # 瑜伽墊被注意力機制抑制
音樂維度:0.10 # 耳機被注意力機制抑制
]
預測CTR(遊戲碟| 精準遊戲興趣) = 0.92 # 高精度!
AUGRUCell的加權和多頭自注意力加權的區別:
|
特性 |
MultiHeadTemporalAttention |
AUGRUCell |
|
加權對象 |
序列內所有時間步的 value |
歷史狀態 + 候選狀態 |
|
加權維度 |
橫向(空間維度,跨時間步,每個時間步都會聚合其他時間步的信息) |
縱向(時間維度,單時間步,單向時間流,逐步積累信息) |
|
權重來源 |
注意力分數(Q·K相似度) |
更新門(輸入+歷史狀態) |
|
目的 |
捕捉序列內部關聯 |
控制狀態更新速度 |
|
信息流向 |
全局聚合(所有時間步→當前) |
時序演化(過去→現在) |
MultiHeadTemporalAttention 的加權求和:形成商品的上下文增強的表示,
context[手柄] = Σ attention_weights[i] * value[i]
= 0.18 * value[主機] + 0.31 * value[手柄] + 0.25 * value[運動襪] + 0.26 * value[遊戲碟]
AUGRUCell 的加權求和:基於當前輸入和歷史狀態
h_1 = (1-0.5)*h_0 + 0.5*candidate_1= 0.5 * 歷史 + 0.5 * 新信息
"時間演化:從 h_0 流向 h_1"
h_2 = (1-0.7)*h_1 + 0.7*candidate_2= 0.3 * 歷史 + 0.7 * 新信息
"時間演化:從 h_1 流向 h_2"
多頭加權和AUGRU加權兩者配合使用:
# 階段1:MultiHeadTemporalAttention(InterestExtractor)
輸入:原始嵌入序列 [主機, 手柄, 運動襪, 遊戲碟]
↓
多頭自注意力(橫向聚合)
↓
輸出:上下文增強的興趣狀態
[主機_context(融合了手柄、遊戲碟信息),
手柄_context(融合了主機、遊戲碟信息),
運動襪_context(知道自己與其他商品的差異),
遊戲碟_context(融合了主機、手柄生態信息)]
# 階段2:AUGRUCell(InterestEvolution)
輸入:上下文增強的興趣狀態(來自階段1)
↓
逐步演化(縱向累積)
↓
t=0: h_0 = 主機_context
t=1: h_1 = 融合(h_0, 手柄_context) ← AUGRUCell加權
t=2: h_2 = 融合(h_1, 運動襪_context) ← AUGRUCell加權
t=3: h_3 = 融合(h_2, 遊戲碟_context) ← AUGRUCell加權
↓
輸出:最終演化的興趣表示 h_3