推薦系統概括
推薦系統方法綜述
推薦系統的目的
評價指標
Accuracy
缺點 對樣本分佈敏感。在測試集裏,有100個sample,99個反例,只有1個正例。如果我的模型不分青紅皁白對任意一個sample都預測是反例,那麼我的模型的accuracy是 正確的個數/總個數 = 99/100 = 99%
logloss
2分類問題:
AUC
爲什麼 AUC 和 logloss 比 accuracy 更常用呢?因爲很多機器學習的模型對分類問題的預測結果都是概率,如果要計算 accuracy,需要先把概率轉化成類別,這就需要手動設置一個閾值,如果對一個樣本的預測概率高於這個預測,就把這個樣本放進一個類別裏面,低於這個閾值,放進另一個類別裏面。
所以這個閾值很大程度上影響了 accuracy 的計算。使用 AUC 或者 logloss 可以避免把預測概率轉換成類別1。
AUC 是 Area under curve 的首字母縮寫。AUC 就是 ROC 曲線下的面積,衡量學習器優劣的一種性能指標。
ROC 曲線的橫座標是假正率,縱座標是真正率,由上圖:
其縱軸、橫軸均是條件於真實 label Y的概率的,我們講這個叫條件概率。那麼意思就是說,無論Y的真實概率是多少,都不會影響 TPR 和 FPR。也就是說,這兩個 metric 是不會受 imbalanced data 影響的,相反,是基於預測label Y 的概率的,會隨着測試集裏面的正反比例而變化。
AUC代表模型預估樣本之間的排序關係,即正負樣本之間預測的 gap 越大,AUC越大。另外值得注意的是,AUC的計算方法同時考慮了學習器對於正例和負例的分類能力,在樣本不平衡的情況下,依然能夠對分類器做出合理的評價。AUC對樣本類別是否均衡並不敏感,這也是不均衡樣本通常用AUC評價學習器性能的一個原因。
ROC 曲線越靠近左上角,模型的查全率就越高。最靠近左上角的ROC曲線上的點是分類錯誤最少的最好閾值,其假正例和假反例總數最少。可以對不同的學習器比較性能。將各個學習器的ROC曲線繪製到同一座標中,直觀地鑑別優劣,靠近左上角的ROC曲所代表的學習器準確性最高。
ROC曲線能很容易的查出任意閾值對學習器的泛化性能影響。
有助於選擇最佳的閾值。ROC曲線越靠近左上角,模型的查全率就越高。最靠近左上角的ROC曲線上的點是分類錯誤最少的最好閾值,其假正例和假反例總數最少。
可以對不同的學習器比較性能。將各個學習器的ROC曲線繪製到同一座標中,直觀地鑑別優劣,靠近左上角的ROC曲所代表的學習器準確性最高。
F1 score
基本上問題就是如果你的兩個模型,一個precision特別高,recall特別低,另一個recall特別高,precision特別低的時候,f1-score可能是差不多的,你也不能基於此來作出選擇。
Collaborative Fliter
CTR
根據用戶的詢問,給於用戶信息度相關的廣告,會極大的增加用戶點擊廣告的可能性,會增加用戶的體驗,同時也會增加廣告公司的收入。
CTR 指的是click through rate,代表點擊該廣告的概率。例如某個廣告ad在過去一段時間內被展示了1000次,並且受到50次的點擊次數,那麼 CTR (ad)=0.05.
因此,如何根據歷史廣告信息和用戶信息,較爲精準的預測新的廣告會被點擊的概率具有重要意義,
但是這裏存在一個問題:這種根據過去的信息預測點擊率的方法有着極大的 Variance ,並且根據 大數定理 廣告需要被大量投遞後,才能獲得較爲精準的 CTR 結果。
因此如何能夠精準構建廣告信息和 CTR 之間的數學模型有着極其重要的意義。
進一步思考,我們可以發現廣告點擊問題本質上是一個2分類問題: {點擊,不點擊} ,因此可以構建基於機器學習方法的 CTR 估計模型。
大數定理 :大數定律說如果統計數據足夠大,那麼事物出現的頻率就能無限接近他的期望值。
大數定理有3種:辛欽大數定理、切比雪夫、伯努利大數定理辛欽大數定理:
切比雪夫不等式:在不知道隨機變量的具體概率密度函數的情況下,可以根據總體的均值和方差時,用切比雪夫不等式來估算一定條件下的概率。
伯努利大數定理:
伯努利大數定理是日常中最常被使用的,它的直觀表達就是隻要做的試驗夠多,出現的次數除以總次數的結果接近統計概率p,這也是頻率到概率概念演變的理論基礎舉個例子,結合概率論裏面的概念。”拋5次硬幣“是一種試驗,一共作n重,“ 3次出現正面”,稱爲事件A,n重試驗出現A的次數爲fA另外已知"拋5次3次正面"的概率是p,這是一個先驗可統計概率。如果n很大,則出現A的次數除以n就可做爲統計的概率p.
引用博文大數定理的通俗理解2。中心極限定理
(1). 樣本的平均值約等於總體的平均值
(2).不管樣本是什麼分佈,任意一個總體的樣本平均值會圍繞着總體平均值周圍,並且呈正態分佈。
中心極限定理3
基於機器學習LR的CTR估計的發展歷程如下圖:
LR
根據前文所訴:CTR 估計問題本質上是一個2分類問題,不妨設{點擊,不點擊}={1,0},LR模型使用Logit將廣告的特徵值加權和映射到(0,1)區間,映射後的值就是 CTR 估計值.
將輸入 通過logit函數之後獲得ctr值。
優化目標:
首先可求當根據原始特徵時,預測爲1的概率:
因此預測爲0的概率:
根據極大似然思想:
當label={-1,1}時:
當label是多目標時,有個類別:
LR方法存在很明顯的缺陷: (1).LR方法無法捕捉特徵之間的聯繫。(2).LR方法在數據特徵有缺失時或者特徵空間很大時表現不佳。
POLY2
由於LR方法無法捕捉特徵之間的聯繫,因此可以組合特徵建立特徵之間的關係:
由於對特徵進行了組合,因此數據矩陣必定是一個非常稀疏的矩陣。由於
POLY2 方法雖然能夠捕捉特徵之間的聯繫(度爲2),但是由於數據稀疏問題,往往不能得到較好的結果。
FM
FM 方法不同於 PLOY2 ,FM 方法通過對每一位特徵引入一個隱向量,使得 FM 方法能夠使用稀疏的數據矩陣中,獲得可信的結果。
FM 方法將特徵的聯繫矩陣分解爲。其理論依據是,任何的正定矩陣總存在,使得,在充分大的條件下。
FM 方法通過提出隱向量的思想解決了 PLOY2 方法中在或者時不能得到很好的調整的問題。FM 通過分解打破了矩陣裏面參數的獨立性,即,和 可以由其他特徵下學習的結果,因此 FM 能夠在稀疏數據集上獲得較好的結果。
FM 方法在不調整計算的情況下的時間複雜度,但是通過調整上式,其時間複雜度可降爲。
很明顯的可以看出這一項不需要重複計算,因此時間複雜度降爲。
求解 通過SGD方法求解:
FM 的優點:
(1). 可以在稀疏的數據矩陣中對特徵進行交叉。
(2).預測和參數的調整都是基於的時間複雜度。
#########2020.5.1 hwf#############
import tensorflow as tf
import os
import numpy as np
class Fm():
def __init__(self,num_classes,k,batch_size,feature_size,train_feature,train_label,save_dir):
self.num_classes=num_classes
self.k=k
self.batch_size=batch_size
self.feature_size=feature_size
self.train_feature=train_feature
self.train_label=train_label
self.save_dir=save_dir
def bulid_model(self):
self.x=tf.placeholder('float32',[None,self.feature_size])
self.y=tf.placeholder('float32',[None,self.num_classes])
with tf.variable_scope('linear_layer'):
w0=tf.get_variable('w0',shape=[self.num_classes],initializer=tf.zeros_initializer)
w=tf.get_variable('w',shape=[self.feature_size,self.num_classes],initializer=tf.truncated_normal_initializer(mean=0.0,stddev=0.01))
linear_out=tf.add(tf.matmul(self.x,w),w0)
with tf.variable_scope('interaction_layer'):
embeding=tf.get_variable('v',shape=[self.feature_size,self.k],initializer=tf.truncated_normal_initializer(mean=0.0,stddev=0.01))
interaction_out=tf.multiply(0.5
,tf.reduce_sum(
tf.subtract(
tf.pow(tf.matmul(self.x,embeding),2),
tf.matmul(tf.pow(self.x,2),tf.pow(embeding,2))),axis=1,keepdims=True))
with tf.variable_scope('out_layer'):
output=tf.add(linear_out,interaction_out)
with tf.variable_scope('accuracy'):
correct_prediction = tf.equal(tf.cast(tf.argmax(output,1), tf.float32), tf.cast(tf.argmax(self.y,1), tf.float32))
self.accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# add summary to accuracy
tf.summary.scalar('accuracy', self.accuracy)
if self.num_classes==2:
y_prob=tf.nn.sigmoid(output)
cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.y, logits=y_prob)
elif self.num_classes>2:
y_prob=tf.nn.softmax(output)
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=self.y, logits=y_prob)
mean_loss = tf.reduce_mean(cross_entropy)
self.loss = mean_loss
tf.summary.scalar('loss', self.loss)
self.optimizer=tf.train.AdamOptimizer()
self.train_op=self.optimizer.minimize(self.loss)
# def next_batch(self):
# print('mext+_natch',self.train_feature)
# print(self.batch_size)
# input_queue = tf.train.slice_input_producer([self.train_feature,self.train_label], shuffle=False)
# feature_batch, label_batch = tf.train.batch(input_queue, batch_size=self.batch_size, num_threads=2, capacity=128,allow_smaller_final_batch=True)
# return feature_batch,label_batch
def shuffle_list(self,data):
num = data[0].shape[0]
p = np.random.permutation(num)
return [d[p] for d in data]
def batch_generator(self,data, batch_size, shuffle=False):
if shuffle:
data = self.shuffle_list(data)
batch_count = 0
while True:
if batch_count * batch_size + batch_size > len(data[0]):
batch_count = 0
if shuffle:
data = self.shuffle_list(data)
start = batch_count * batch_size
end = start + batch_size
batch_count += 1
yield [d[start:end] for d in data]
def train(self,iteration):
self.Saver = tf.train.Saver(max_to_keep=100)
merge = tf.summary.merge_all()
nums_batch=len(self.train_label)//self.batch_size+1
init = tf.initialize_all_variables()
ckpt=tf.train.get_checkpoint_state(self.save_dir)
with tf.Session() as self.sess:
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(self.sess, coord)
self.sess.run(init)
train_writer=tf.summary.FileWriter('./log/train_logs',self.sess.graph)
if ckpt and ckpt.model_model_checkpoint_path:
self.Saver.restore(self.sess,ckpt.model_model_checkpoint_path)
print("加載模型成功"+ckpt.model_model_checkpoint_path)
global_step=int(ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1])
else:
global_step =0
for epoch in range(iteration):
for _ in range(nums_batch):
#feature_batch, label_batch = self.next_batch()
feature, label = next(self.batch_generator([self.train_feature,self.train_label],batch_size=self.batch_size))
#feature,label = self.sess.run([feature_batch, label_batch])
feed_dic={self.x:feature, self.y:label}
loss, accuracy, summary, _ = self.sess.run([self.loss, self.accuracy,merge, self.train_op], feed_dict=feed_dic)
train_writer.add_summary(summary, global_step=global_step)
global_step+=1
if epoch % 100 == 0:
print('save_model_{}'.format(epoch))
self.Saver.save(self.sess, os.path.join(self.save_dir, 'fm'), global_step=global_step)
print("Epoch {1}, loss = {0:.3g}, accuracy={2:.3g}".format(loss, epoch + 1,accuracy))
coord.request_stop()
coord.join(threads)
if __name__=='__main__':
x_train, x_test,y_train, y_test = load_data()
print(x_train)
# initialize the model
num_classes = 5
lr = 0.01
batch_size = 128
k = 40
#reg_l1 = 2e-2
#reg_l2 = 0
feature_length = x_train.shape[1]
save_dir=r'./model/'
# initialize FM model
model =Fm(num_classes,k,batch_size,feature_length,x_train,y_train,save_dir=save_dir)
model.bulid_model()
model.train(12222)
# build graph for model
FFM
FM 方法有仍然存在問題, 離散特徵進行onehot編碼後,會被編碼成,這些feature被稱爲一個field。其中特徵的隱向量跟其他的filed的特徵進行組合時,如 和由於 和屬於不同field,因此他們的隱向量組合也會不同。
FFM 對的每一個feature都會包含個隱向量,是其他field的個數。
ffm的變量個數爲,時間複雜度爲。
小結
Model | variable | complexity |
---|---|---|
LM | ||
Poly2 | ||
FM | ||
FFm |
LR+GBDT
核心思想:模型想要得到好的預測結果的最重要的影響因素是 好的特徵組合,即特徵決定下限,模型決定上限。因此利用GBDT算法對原始特徵進行特徵重要性劃分這種思想,對原始特徵進行高維特徵組合,減少人工的特徵組合。
如上圖所示假設有個樹,不妨設,樣本在第一棵樹中被劃分到節點2,在第二棵樹中被劃分到節點3,則通過GBDT後的:
之後再結合和模型獲得預測結果。
from sklearn.ensemble.gradient_boosting import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import OneHotEncoder
from xgboost import XGBClassifier
import numpy as np
from lightgbm import LGBMClassifier
from sklearn.model_selection import train_test_split
#gbdt.apply(x_test)
#返回值爲 dimension[n,n_estimator,n_leaves_index]
#假設gbdt.apply(x_test)返回值[[1,1,2],[2,2,2],[3,1,3],[4,5,4]]
#其中[1,1,2],[2,2,2],[3,1,3],[4,5,4]。表示第1,2,3,4個樣本,[1,1,2]表示樣本1,被劃分到第一棵樹的第一個葉子節點,被劃分到第二棵樹的第一個葉子節點,被劃分到第3棵樹的第二個葉子節點。
#通過OneHotEncoder()之後可以獲得每個樹的葉子節點的特徵組合。
# oneHot=OneHotEncoder()
# oneHot.fit([[1,1,2],[2,2,2],[3,1,3],[4,5,4]])
# print(oneHot.transform([[1,1,2],[2,2,2],[3,1,3],[4,5,4]]).toarray())
#[[1. 0. 0. 0. 1. 0. 0. 1. 0. 0.]
# [0. 1. 0. 0. 0. 1. 0. 1. 0. 0.]
# [0. 0. 1. 0. 1. 0. 0. 0. 1. 0.]
# [0. 0. 0. 1. 0. 0. 1. 0. 0. 1.]]
random_seed=10
class GBDT_LR():
def __init__(self,data,label,gbdt_name):
self.gbdt_set=['xgboost','gbdt','lgb']
self.gbdt_name=gbdt_name
self.data=data
self.label=label
self.x_train,self.x_test,self.y_train,self.y_test=train_test_split(self.data,self.label,train_size=0.7,random_state=random_seed)
self.gbdt=self.init_gbdt()
def init_gbdt(self):
if self.gbdt_name == 'xgboost':
gbdt = XGBClassifier()
elif self.gbdt_name=='gbdt':
gbdt=GradientBoostingClassifier()
elif self.gbdt_name=='lgb':
gbdt=LGBMClassifier()
else:
print('no valid gbdt model')
return gbdt
def gbdt_train(self):
self.gbdt.fit(self.x_train,self.y_train)
def gbdt_predict(self):
self.gbdt_predict =self.gbdt.predict_proba(self.x_test)
def cal_auc(self):
gbdt_auc=roc_auc_score(self.y_test,self.gbdt_predict)
def LR(self):
gbdt_encoder=OneHotEncoder()
self.lr=LogisticRegression()
self.x_train_leafs=self.gbdt.apply(self.x_train)
self.x_test_leafs=self.gbdt.apply(self.x_test)
gbdt_encoder.fit(self.x_train_leafs)
x_train_encoder=gbdt_encoder.transform(self.x_train_leafs)
x_test_encoder = gbdt_encoder.transform(self.x_test_leafs)
self.lr.fit(x_train_encoder,self.y_train)
self.gbdt_lr_predict=self.lr.predict_proba(x_test_encoder)
gbdt_lr_auc=roc_auc_score(self.y_test,self.gbdt_lr_predict)
print('基於gbdt編碼後的LR AUC值:{:.2f}'.format(gbdt_lr_auc))
lr2=LogisticRegression()
lr2.fit(self.x_train)
lr_predict=lr2.predict_proba(self.x_test)
lr_auc=roc_auc_score(self.x_test,lr_predict)
print('LR AUC值:{:.2f}'.format(lr_auc))
LR+DNN
Wide and deep
廣度模型(FM,FFM)一般只能學習一階和二階的特徵組合,不能很好的發現特徵之間的更加高維和抽象的關係。
深度模型(FNN/DNN)一般學習的是不可視的高階特徵的組合,但是丟失了低階特徵的信息。
wide and deep模型結合廣度模型和深度模型的優點,充分利用特徵的低階組合和高階組合,能夠達到單一模型達不到的精度。
wide and deep 模型有兩個重要的概念:
memorization :通過一系列人工的特徵叉乘(cross-product)來構造這些非線性特徵,捕捉sparse特徵之間的低階(因爲一般只做2階的,更高階難以計算)相關性,即“記憶” 歷史數據中曾共同出現過的特徵對。
優點:模型可解釋強,實現高效,特徵重要度易於分析。
缺點:需要人工的特徵工程,無法捕捉未出現過的特徵對,過高階的特徵叉乘容易出現過擬合。
generalization :Generalization 爲sparse特徵學習低維的dense embeddings來捕獲特徵相關性,學習到的embeddings本身帶有一定的語義信息。可以聯想到NLP中的詞向量,不同詞的詞向量有相關性,因此文中也稱Generalization是基於相關性之間的傳遞。這類模型的代表是DNN和FM。
Memorization趨向於更加保守,Memorization根據歷史行爲數據,產生的推薦通常和用戶已有行爲的物品直接相關的物品。而Generalization會學習新的特徵組合,提高推薦物品的多樣性。
這個是從人類的認知學習過程中演化來的。人類的大腦很複雜,它可以記憶(memorize)下每天發生的事情(麻雀可以飛,鴿子可以飛)然後泛化(generalize)這些知識到之前沒有看到過的東西(有翅膀的動物都能飛)。但是泛化的規則有時候不是特別的準,有時候會出錯(有翅膀的動物都能飛嗎)。那怎麼辦那,沒關係,記憶(memorization)可以修正泛化的規則(generalized rules),叫做特例(企鵝有翅膀,但是不能飛)。
Wide&Deep Mode就是希望計算機可以像人腦一樣,可以同時發揮memorization和generalization的作用。–Heng-Tze Cheng(Wide&Deep作者)
link
如上圖所示:wide and deep模型仍然存在問題,需要人工的特徵工程爲wide部分選取合適的特徵。
DeepFM
DeepFM 方法基於wide and deep思想,解決了wide and deep中需要進行人爲的特徵工程的問題。
deepfm結合FM中對每一位特徵構建一個隱向量的思想,對onehot後稀疏特徵進行embedding。
由上式可知 DeepFM 可以分爲 FM 和 DNN 兩個模塊,下面分開來將。
在講兩個模塊之前,必須得清楚 DeepFM sparse feature 和 dense embeddings是什麼。
由上圖,每個都是隻有某一個index的位置爲1的稀疏向量。每個特徵都蘊含一個隱向量,上圖中的dense embeddings層就是每一個非零特徵對應的隱向量,由於一個field只有一個index爲1,其餘都爲零,因此dense embeddings節點個數。這是因爲在FM中,只有當特徵不爲0時,對應的隱向量纔會得到調整。上圖中表示field 1中的某一位不爲0的特徵的隱向量。不妨設的隱向量矩陣爲,其中表示中的特徵的個數,表示隱向量的維數。,不妨設,則。
FM 模塊:
FM 部分可回顧章節 FM 部分。
DNN DNN部分直接使用dense embeddings作爲輸入層
表示等個filed的embedding,表示field的個數。
DeepFM 總結:
1.不需要任何的預訓練
2.結合wide and deep思想,可以同時具備高階和低階的特徵組合
3.使用了共享隱向量embedding的思想,避免了人爲的特徵工程。