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