在前面幾篇關于推薦模型的文章中,筆者均給出了示例代碼,有讀者反饋——想知道在 TensorFlow 中用戶特征和商品特征是如何 Embedding 的?因此,筆者特意寫作此文加以解答。
1. 何為 Embedding ?
關于 Embedding,筆者很久之前寫過一篇文章《推薦系統(十一):推薦系統中的 Embedding》,現在看來,差強人意,不過,對 Embedding 的概念解讀還是不錯的,只是缺乏代碼案例解讀。在本文中,筆者將基于 TensorFlow 來做解讀,讓讀者加深理解。
如下圖所示,為一個極簡(CTR 和 CVR 共享了交互層) “雙塔模型”(詳見文章《推薦系統(十五):基于雙塔模型的多目標商品召回/推薦系統》),簡單解讀一下:
- User Feature 和 Item Feature 先經過 Embedding Layer 處理,得到特征的 Embedding;
- User Feature Embedding 和 Item Feature Embedding 經過 Concat Layer 連接后輸入到 DNN 網絡;這樣直接 Concat 得到的 Embedding 結果被稱為 User 和 Item 的 “表示(Representation)”,顯然,這種 “表示” 比較粗糙;
- 經過 MLP 處理,得到 User Vector 和 Item Vector,相較于上一步的 “表示形式”,User Vector 和 Item Vector 要 “精細” 得多,是真正意義上的 User Embedding 和 Item Embedding。
- User Embedding 和 Item Embedding 計算內積后經過 Sigmoid 函數處理(即圖中的 Prediction),即可得到一個 0~1 之間的數值,即概率。
- 對于商品點擊(1-點擊,0-未點擊)和商品轉化(1-轉化,0-未轉化)這種二分類問題,結合模型預測的概率和樣本 Label,很容易計算出損失(二分類問題一般采用交叉墑損失)。
- 對于 CTR 和 CVR 這種多任務場景,需要將 CTR Loss 和 CVR Loss 加權融合作為最終的損失,進而指導訓練模型。
2.特征工程中的 Embedding
2.1 ID 類特征
在 User Feature 和 Item Feature 中,User ID 和 Item ID 是最為重點的特征之一,是典型的 “高維稀疏” 特征。直接以原始數據形式輸入模型是不行的,必須經過 Embedding Layer 的處理。在此,以 Item ID 為例,Embedding 處理的代碼如下:
# 模擬生成商品特征,其中 item_id 取值[1, 10000]
num_items = 10000
item_data = {'item_id': np.arange(1, num_items + 1),'item_category': np.random.choice(['electronics', 'books', 'clothing'], size=num_items),'item_brand': np.random.choice(['brandA', 'brandB', 'brandC'], size=num_items),'item_price': np.random.randint(1, 199, size=num_items)
}# 基于 TensorFlow 對原始的 item_id 進行 Embedding 處理,分為兩步
item_id = feature_column.categorical_column_with_identity('item_id', num_buckets=num_items)
item_id_emb = feature_column.embedding_column(item_id, dimension=8)
1. 分類列的創建:categorical_column_with_identity
item_id = feature_column.categorical_column_with_identity('item_id', num_buckets=num_items)
- 功能:將輸入的整數 item_id 直接映射為分類標識。例如,若 num_items=1000,則輸入的 item_id 必須是 [0,
1, 2, …, 999] 范圍內的整數。 - 本質:這類似于對 item_id 做 One-Hot 編碼(但底層實現更高效,不顯式生成稀疏矩陣)。
2.嵌入列的創建:embedding_column
item_id_emb = feature_column.embedding_column(item_id, dimension=8)
- 功能:將高維稀疏的分類 ID(如 num_items=1000 維的 One-Hot 向量)映射到低維稠密的連續向量空間(維度為 8)。
- 關鍵點:嵌入矩陣的維度是 [num_items, 8],即每個 item_id 對應一個 8 維向量。這個嵌入矩陣是一個可訓練參數,初始值隨機(如 Glorot 初始化),通過神經網絡的反向傳播逐步優化。
3.嵌入向量的訓練過程
- 何時生成:嵌入矩陣的值并非預先計算,而是在模型訓練時動態學習。
- 如何學習:
1-輸入數據中的 item_id 會觸發嵌入層查找對應的 8 維向量。
2-在反向傳播時,優化器(如Adam)根據損失函數的梯度調整嵌入矩陣的值。
3-模型通過最小化損失函數,迫使相似的 item_id 在嵌入空間中靠近,從而捕捉潛在語義關系(如用戶行為中的物品相似性)。
4. 嵌入層的底層實現
當你在模型中調用 item_id_emb 時,TensorFlow 會隱式完成以下操作:
# 偽代碼解釋
embedding_matrix = tf.Variable( # 可訓練參數initial_value=tf.random.uniform([num_items, 8]), name="item_id_embedding"
)
# 根據輸入的item_id查找嵌入向量
item_id_emb = tf.nn.embedding_lookup(embedding_matrix, input_item_ids)
5. 嵌入的優勢
- 降維:將高維稀疏特征壓縮為低維稠密向量(例如從1000 維的 One-Hot 降到 8 維)。
- 語義學習:模型自動學習嵌入空間中的幾何關系(如相似物品的向量距離更近)。
- 泛化性:即使某些 item_id 在訓練數據中出現次數少,其嵌入向量仍可通過相似物品的梯度更新得到合理表示。
6. 完整流程示例
假設你的模型是一個推薦系統,處理流程如下:
- 輸入層:接收原始特征(如 {‘item_id’: 5})。
- 特征轉換:通過 item_id_emb 將 item_id=5 轉換為一個 8 維向量。
- 神經網絡:將嵌入向量輸入全連接層(如 DNN)、激活函數等后續結構。
- 訓練:通過損失函數(如點擊率預測的交叉熵)反向傳播,更新嵌入矩陣和其他權重。
2.2 類別特征
以用戶性別為例:
# 模擬生成用戶特征,其中用戶性別是可以枚舉的類別特征:male,female
user_data = {'user_id': np.arange(1, num_users + 1),'user_age': np.random.randint(18, 65, size=num_users),'user_gender': np.random.choice(['male', 'female'], size=num_users),'user_occupation': np.random.choice(['student', 'worker', 'teacher'], size=num_users),'city_code': np.random.randint(1, 2856, size=num_users), # 城市編碼,中國有 2856 個城市'device_type': np.random.randint(0, 5, size=num_users) # 設備類型(0=Android,1=iOS等)
}
# 對性別特征進行 Embedding 處理
user_gender = feature_column.categorical_column_with_vocabulary_list('user_gender', ['male', 'female'])
user_gender_emb = feature_column.embedding_column(user_gender, dimension=2)
1. 定義分類特征列
代碼如下:
user_gender = feature_column.categorical_column_with_vocabulary_list('user_gender', ['male', 'female'])
- 作用:將字符串類型的性別特征(如 ‘male’ 或 ‘female’)映射為整數索引。
- 細節: 輸入特征名為 ‘user_gender’,詞匯表為 [‘male’, ‘female’]。 模型會根據詞匯表將 ‘male’ 編碼為
0,‘female’ 編碼為 1。 如果輸入的值不在詞匯表中(如 ‘unknown’),默認會被映射為 -1(可通過
num_oov_buckets 參數調整)。
2. 創建嵌入列(Embedding Column)
如下代碼:
user_gender_emb = feature_column.embedding_column(user_gender, dimension=2)
- 作用:將稀疏的整數索引轉換為密集的低維向量(嵌入向量)。
- 細節:
1.嵌入矩陣的維度:嵌入矩陣的形狀為 (vocab_size, embedding_dimension),即 (2, 2)。 行數 2:對應詞匯表中的兩個類別(male 和 female);列數 2:指定的嵌入維度 dimension=2。
2.嵌入初始化:嵌入向量的初始值默認通過隨機均勻分布生成(可通過 initializer 參數自定義)。
3.訓練過程:嵌入向量會在模型訓練時通過反向傳播自動優化,學習與任務相關的語義表示。
2.3 數值特征
數值特征是一種簡單的特征,按照常理,可以直接用原始數據進行模型訓練和預測,然而,由于不同類型的數值特征存在 “量綱差異”,從而使得不同類型的數值特征 “不可比較”(如年齡數值區間(0~150),價格區間(0~10000000)),因此,數值特征也需要處理,比如標準化/歸一化,好處如下:
- 統一特征尺度,避免梯度下降因不同特征量綱而震蕩。
- 所有特征在相同尺度下,模型權重更新更均衡。
- L1/L2正則化對所有特征施加相似強度的懲罰。
以用戶年齡為例:
scaler_age = StandardScaler()
df['user_age'] = scaler_age.fit_transform(df[['user_age']])
user_age = feature_column.numeric_column('user_age')
1. 數據標準化處理
代碼如下:
scaler_age = StandardScaler()
- 作用:創建一個標準化處理器,用于對數值型特征(如年齡)進行均值方差標準化(Z-Score標準化)。
- 細節:StandardScaler 是 scikit-learn 庫中的標準化工具,核心操作為:標準化值 =(原始值?均值)/ 標準差 標準化后,數據分布均值為 0,標準差為 1,消除量綱差異。適用于數值范圍大、分布不均衡的特征(如年齡范圍可能從 0 到 100)。
2.應用標準化到年齡列
代碼如下:
df['user_age'] = scaler_age.fit_transform(df[['user_age']])
- 作用:對 DataFrame 中的 user_age 列進行擬合和轉換,實現標準化。
- 細節:
1. fit_transform 兩步合并。fit:計算 user_age 列的均值(μ)和標準差(σ)。transform:使用公式:(X?μ)/ σ,對所有樣本進行標準化。
2. 示例:假設原始年齡數據為 [20, 30, 40],均值為 30,標準差為 8.16,標準化后為 [-1.22, 0, 1.22]。
3. 存儲參數:scaler_age 對象會保存計算出的 μ 和 σ,便于后續對新數據(如測試集)使用 transform 而非重新擬合。
3.創建數值特征列
代碼如下:
user_age = feature_column.numeric_column('user_age')
- 作用:定義 TensorFlow 模型可接收的數值型特征列,將標準化后的年齡值直接輸入模型。
- 細節:
1. 輸入數據類型:該列接收的是連續數值(如標準化后的 -1.22、0、1.22)。
2. 模型中的處理:在訓練時,每個樣本的 user_age 值會以浮點數形式直接傳遞給神經網絡,無需進一步編碼。
3. 參數擴展性: 可結合其他參數增強特征(例如 normalizer_fn 可添加自定義歸一化,但此處已提前標準化,通常不再需要)。