diff --git a/2024/02/04/dl_rec_al/index.html b/2024/02/04/dl_rec_al/index.html
index 2895229..af27cc9 100644
--- a/2024/02/04/dl_rec_al/index.html
+++ b/2024/02/04/dl_rec_al/index.html
@@ -44,7 +44,7 @@
-
+
@@ -231,7 +231,7 @@
- 43k words
+ 44k words
@@ -242,7 +242,7 @@
- 360 mins
+ 363 mins
@@ -284,7 +284,7 @@
- Last updated on March 8, 2024 am
+ Last updated on March 11, 2024 pm
@@ -1088,6 +1088,15 @@ 深度学习模型(Rank)
alt="image-20240308111249239" />
image-20240308111249239
+
+
+image-20240311120735173
+
+对于1层cross和1层deep的DCN网络输入经过embedding和stack处理后维度为d,Cross部分网络参数为2d,Deep为d*d,当MLP的层数增多时,deep部分的参数量也急速增加。DCN网路的绝大部分参数都用于对隐性交叉特征进行建模。Cross部分的表达能力反而受限。
+DCN-v2优化了cross网络的建模方式,增加了cross网络部分的表达能力;deep部分保持不变。
+低维空间的交叉特征建模使得我们可以利用MoE。MoE由两部分组成:experts专家和gating门(一个关于输入x的函数)。我们可以使用多个专家,每个专家学习不同的交叉特征,最后通过gating将各个专家的学习结果整合起来,作为输出。这样就又能进一步增加对交叉特征的建模能力。
@@ -1156,7 +1165,7 @@ 深度学习模型(Rank)
diff --git a/local-search.xml b/local-search.xml
index 5d6bc06..ef8c62c 100644
--- a/local-search.xml
+++ b/local-search.xml
@@ -46,7 +46,7 @@
/2024/02/04/dl_rec_al/
- 推荐系统算法总结 推荐系统近几年有了深度学习的助推发展之势迅猛,从前深度学习的传统推荐模型(协同过滤,矩阵分解,LR, FM, FFM,GBDT)到深度学习的浪潮之巅(DNN, Deep Crossing, DIN, DIEN, Wide&Deep,Deep&Cross, DeepFM, AFM, NFM, PNN, FNN,DRN)。推荐系统通过分析用户的历史行为给用户的兴趣建模,从而主动给用户推荐给能够满足他们兴趣和需求的信息
传统模型(Recall)
数据集介绍 https://github.com/ZiyaoGeng/RecLearn/wiki/Movielens
rating 标签列表为:UserID::MovieID::Rating::Timestamp
UserIDs:用户ID(1~6040) MovieIDs:电影ID(1~3952) Ratings:评分(1~5) Timestamp:时间戳 user 标签列表为:UserID::Gender::Age::Occupation::Zip-code
Gender:性别, "M"代表男, "F"代表女; Age:年龄,分为多个区间:1,18, 25, 35, 45, 50;
Occupation:职业,0~20; movies 标签列表为:MovieID::Title::Genres
基于内容的推荐 这是一种比较简单的推荐方法,基于内容的推荐方法是非常直接的,它以物品的内容描述信息为依据来做出的推荐,本质上是基于对物品和用户自身的特征或属性的直接分析和计算。例如,假设已知电影A是一部喜剧,而恰巧我们得知某个用户喜欢看喜剧电影,那么我们基于这样的已知信息,就可以将电影A推荐给该用户。具体实现步骤:
构建物品画像(主要包括物品的分类信息,标题, 各种属性等等) 构建用户画像(主要包括用户的喜好, 行为的偏好,基本的人口学属性,活跃程度,风控维度) 根据用户的兴趣, 去找相应的物品, 实施推荐。 基本流程 建立物品画像基于用户给电影打的tag和电影的分类值,得到每一部电影的总标签 求每一部电影标签的tf-idf值 根据tf-idf的结果,为每一部电影选择top-n(tf-idf值较大)的关键词作为整部电影的关键词,最后得到了电影id— 关键词 — 关键词权重
建立倒排索引这个是为了能够根据关键词找到对应的电影,好方便得到用户画像之后(用户喜欢啥样的电影)对用户进行一些推荐 建立用户画像看用户看过哪些电影, 基于前面的物品画像找到电影对应的关键词 把用户看过的所有关键词放到一起, 统计词频, 每个词出现了几次 出现次数最多的关键词作为用户的兴趣词, 这个就是用户的画像 根据用户的兴趣词, 基于倒排表找到电影,就可以对用户实施推荐了。 物品画像 本质上是基于对物品和用户自身的特征或属性 的直接分析和计算
构建标签数据集
数据集的四列关键词为:userId, movieId, tag, timestamp
1 2 3 4 5 6 7 _tags = pd.read_csv("ml-latest-small/all-tags.csv" , usecols=range (1 , 3 )).dropna() tags = _tags.groupby("movieId" ).agg(list ) tags.head()
构建物料数据
数据集关键标签为:movieId, title, genres
1 2 3 4 5 6 7 movies = pd.read_csv("ml-latest-small/movies.csv" , index_col="movieId" ) movies['genres' ] = movies['genres' ].apply(lambda x: x.split("|" )) movies.head()
合并物品和标签的列表
1 2 3 4 5 6 7 8 9 10 11 movies_index = set (movies.index) & set (tags.index) new_tags = tags.loc[list (movies_index)] ret = movies.join(new_tags) ret.head()
image-20240204115110070 数据处理并补充缺失值
1 2 3 df = map (lambda x: (x[0 ], x[1 ], x[2 ], x[2 ]+x[3 ]) if x[3 ] is not np.nan else (x[0 ], x[1 ], x[2 ], []), ret.itertuples()) movies_dataset = pd.DataFrame(df, columns=['movieId' , 'title' , 'genres' , 'tags' ]) movies_dataset.head()
这段代码的作用是对retDataFrame中的tags列进行处理,将缺失值替换为空列表,并创建一个新的DataFrame,以便于后续的数据分析和处理
数据预处理补充缺失值 TF-IDF模型 引入相关的软件包,其中包含了TfidfModel
相当于是TFIDF模型
1 2 3 from gensim.models import TfidfModelfrom pprint import pprintfrom gensim.corpora import Dictionary
通常用于创建词典,它将数据集中的所有唯一单词作为键,每个单词出现的次数作为值
1 2 3 4 5 6 7 8 dataset = movies_dataset["tags" ].values dct = Dictionary(dataset) corpus = [dct.doc2bow(line) for line in dataset]
构建TF-IDF模型并进行展示
1 2 3 4 model = TfidfModel(corpus) model[corpus[0 ]]
1 2 3 4 5 6 7 8 9 movie_profile = {}for i, mid in enumerate (movies_dataset.index): tfidf_vec = model[corpus[i]] movies_tags = sorted (tfidf_vec, key=lambda x: x[1 ], reverse=True )[:30 ] movie_profile[mid] = dict (map (lambda x: (dct[x[0 ]], x[1 ]), movies_tags))
得出最终的结果
1 2 movie_profile = create_movie_profile(movie_dataset) movie_profile.head()
建立倒排索引 倒排索引 就是用物品的其他数据作为索引,去提取他们对应的物品的ID列表
1 2 3 4 5 6 7 8 9 10 11 12 def create_inverted_table (movie_profile ): inverted_table = {} for mid, weights in movie_profile['weights' ].iteritems(): for tag, weight in weights.items(): _ = inverted_table.get(tag, []) _.append((mid, weight)) inverted_table.setdefault(tag, _) return inverted_table
1 inverted_table = create_inverted_table(movie_profile)
这样就可以直接根据标签去推荐电影 了
用户画像 构建步骤:
根据用户的评分历史,结合物品画像,将有观影记录的电影的画像标签作为初始标签反打到用户身上 通过对用户观影标签的次数进行统计,计算用户的每个初始标签的权重值,排序后选取TOP-N作为用户最终的画像标签 1 2 import collectionsfrom functools import reduce
1 2 3 4 5 6 watch_record = pd.read_csv("ml-latest-small/ratings.csv" , usecols=range (2 ), dtype={"userId" :np.int32, "movieId" : np.int32}) watch_record = watch_record.groupby("userId" ).agg(list )
产生Top-N的推荐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 user_profile = {}for uid, mids in watch_record.itertuples(): record_movie_profile = movie_profile.loc[list (mids)] counter = collections.Counter(reduce(lambda x, y: list (x) + list (y), record_movie_profile['profile' ].values)) interest_words = counter.most_common(50 ) maxcount = interest_words[0 ][1 ] interest_words = [(w, round (c/maxcount, 4 )) for w, c, in interest_words] user_profile[uid] = interest_words
这个地方根据用户观看的视频以及视频对应的关键词和每个关键词的权重,给出每个用户感兴趣的关键词的权重归一化之后的值
注意,这个地方给出的归一化的操作是使用频率进行归一化操作
下面给用户进行视频的推荐
输出对应的用户的uid,推荐的视频编号,以及视频的推荐概率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for uid, interest_words in user_profile.items(): result_table = {} for interest_word, interest_weight in interest_words: related_movies = inverted_table[interest_word] for mid, relate_weight in related_movies: _ = result_table.get(mid, []) _.append(interest_weight) result_table.setdefault(mid, _) rs_result = map (lambda x: (x[0 ], sum (x[1 ])), result_table.items()) rs_result = sorted (rs_result, key=lambda x: x[1 ], reverse=True )[:100 ] print (uid) pprint(rs_result) break
冷启动算法 这里主要包括两个很厉害的技术:
* Word2Vec : 这个可以根据得到电影标签的词向量,根据这个词向量, 就能够得到tag之间的相似性,这样就能够根据用户看过的某个电影, 得到这个电影的标签,然后根据这些标签得到与其近似的标签,然后得到这些近似标签下的电影对该用户产生推荐
1 2 3 4 5 6 7 8 9 10 11 from gensim.models import Word2Vec sentences = list (movie_profile["profile" ].values) model = Word2Vec(sentences, window=3 , min_count=1 ) words = input ("words: " ) ret = model.wv.most_similar(positive=[words], topn=10 ) print (ret)
* Doc2Vec :这个可以根据电影的所有标签,训练一个模型来得到最终电影的影片向量, 根据这个,就能够直接计算用户看过的某个电影与其他电影的相似性,然后根据这个相似性给用户推荐最相似的几篇文章。
1 2 3 4 5 6 7 from gensim.models.doc2vec import Doc2Vec, TaggedDocumentfrom gensim.test.utils import get_tmpfile documents = [TaggedDocument(words, [movie_id]) for movie_id, words in movie_profile["profile" ].iteritems()] documents
1 2 3 4 5 6 7 8 9 10 11 12 13 14 model = Doc2Vec(documents, vector_size=100 , window=3 , min_count=1 , epochs=20 ) words = movie_profile["profile" ].loc[6 ]print (words) inferred_vector = model.infer_vector(words) sims = model.docvecs.most_similar([inferred_vector], topn=10 )print (sims)
协同过滤算法 协同过滤(Collaborative Filtering)算法,基本思想是根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品(基于对用户历史行为数据的挖掘发现用户的喜好偏向,并预测用户可能喜好的产品进行推荐),一般是仅仅基于用户的行为数据(评价、购买、下载等),而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄,性别等)。目前应用比较广泛的协同过滤算法是基于邻域的方法,而这种方法主要有下面两种算法:
基于用户的协同过滤算法(UserCF):给用户推荐和他兴趣相似的其他用户喜欢的产品 基于物品的协同过滤算法(ItemCF):给用户推荐和他之前喜欢的物品相似的物品 两种CF算法介绍 用户协同和物品协同的使用场景
*UserCF的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度;
* ItemCF的推荐更加个性化,反映了用户自己的兴趣传承
UserCF适合于新闻推荐
热门程度和时效性是个性化新闻推荐的重点,而个性化相对于这两点略显次要 UserCF需要维护一个用户兴趣相似表,而ItemCF需要维护一个物品相似表,在新闻推荐系统中物品的更新速度是很快的,那么如果采用ItemCF的话,物品相似度表也需要很快地更新,这是难以实现的 ItemCF适合于图书、电子商务和电影网站
用户的兴趣是比较固定和持久的 这些系统中用户不太需要流行度来辅助他们判断一个物品的好坏,而是可以通过自己熟知的领域的知识自己判断物品的质量 UserCF的适用场合
用户较少 的场合,如果用户很多,计算用户相似度度矩阵代价很大(新闻网站)时效性较强,用户个性化兴趣不太明显 的领域 不需要给出令用户信服的推荐解释 ItemCF的适用场合
适用于物品的数量明显小于用户的数量 的场合,如物品很多(网站),计算物品的相似度矩阵代价很大 长尾物品丰富,用户个性化 需求强烈的领域 需要利用用户的历史行为给用户做推荐解释,可以令用户比较信服 该实验使用的数据集来自:http://grouplens.org/datasets/movielens/
算法基本流程 不管是UserCF还是ItemCF, 行文逻辑都是下面的四个步骤: 1. 导入数据,读取文件得到"用户-电影"的评分数据, 并且分为训练集和测试集 2.计算用户(userCF)或者电影(itemcf)之间的相似度 3. 针对目标用户u,找到其最相似的k个用户/产品, 产生N个推荐 4. 产生推荐之后,通过准确率、召回率和覆盖率等进行评估。
工业界协同过滤的流程 数据处理 对行为少不活跃的用户进行过滤, 行为少的用户, 数据太过于稀疏,召回难度大 对用户中热门物品进行过滤, 热门物品可能大部分用户都有过行为 非常活跃的用户, 用户协同可能会出现一种情况,就是每个用户的topN相似用户里都有些非常活跃的用户,所有需要适当过滤掉这些用户 建立用户embedding和物品embedding, 或者可以像案例这样,直接建立共现矩阵, 也可以训练embedding 计算用户和N个用户的相似度, 保存N个相似用户曾经看过的TopK个物品 模型(矩阵)进行定期更新, 这个要根据不同项目组的情况,可能是一天更新一次, 也可能不是, 看具体的情况,更新的时候使用前N天(N一般3-10)的活跃用户的数据进行更新 每次召回一次N条, 刷完N条再继续召回 还有可能用户两次行为(上拉或者下滑)之间间隔很长时间,也会进行重新召回 每次召回的数量,需要根据召回通道以及各个召回通道配置的召回占比进行配置 为了保证用户不疲劳, 一般情况下, 利用user-cf计算召回结果后,会做一定的类别去重, 保证召回覆盖度 实际过程中, 根据公司核心用户的数量大小, 考虑实现工具,如果数据量较大, 可使用spark进行用户协同的结果计算 如果用户量实在太过巨大, 可考虑使用稀疏存储的方式进行存储,即只存储含有1(或者其他值)的位置坐标索引index以及对应的值 用户协同过滤算法 TopN推荐的任务是预测用户会不会对某部电影评分,而不是预测用户在准备对某部电影评分的前提下给电影评多少分 ,下面我们开始, 从逻辑上看, 其实这个任务主要分为下面的步骤: 1.导入数据, 读取文件得到"用户-电影"的评分数据, 并且分为训练集和测试集 2.计算用户之间的相似度 3. 针对目标用户u, 找到其最相似的k个用户,产生N个推荐 4. 产生推荐之后, 通过准确率、召回率和覆盖率等进行评估。
引入依赖包和读取数据
1 2 3 4 5 6 7 8 9 10 11 import randomimport numpy as npimport pandas as pdimport mathfrom operator import itemgetter data_path = './ml-latest-small/' data = pd.read_csv(data_path+'ratings.csv' ) data.head() data.pivot(index='userId' , columns='movieId' , values='rating' )
存在稀疏的情况举例 存在大量的稀疏的情况,因此后续会对此进行数据的补全
将数据集划分成训练集和测试集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 trainSet, testSet = {}, {} trainSet_len, testSet_len = 0 , 0 pivot = 0.75 for ele in data.itertuples(): user, movie, rating = getattr (ele, 'userId' ), getattr (ele, 'movieId' ), getattr (ele, 'rating' ) if random.random() < pivot: trainSet.setdefault(user, {}) trainSet[user][movie] = rating trainSet_len += 1 else : testSet.setdefault(user, {}) testSet[user][movie] = rating testSet_len += 1 print ('Split trainingSet and testSet success!' )print ('TrainSet = %s' % trainSet_len)print ('TestSet = %s' % testSet_len)
1 2 3 Split trainingSet and testSet success! TrainSet = 75732 TestSet = 25104
建立用户相似度的表
如果直接遍历用户表会产生比较大的时间复杂度,不如直接建立一个物品到用户的倒排表,对每个物品都保存对该物品都产生过的用户列表
具体的代码如下所示:
第一步将原来的列表转化成电影-用户的倒排索引 第二步计算每个用户之间的相似性矩阵 1 2 3 4 5 6 7 8 9 10 11 12 user_sim_matrix = {}print ('Building movie-user table ...' ) movie_user = {}for user, movies in trainSet.items(): for movie in movies: if movie not in movie_user: movie_user[movie] = set () movie_user[movie].add(user) print ('Build movie-user table success!' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 movie_count = len (movie_user)print ('Total movie number = %d' % movie_count)print ('Build user co-rated users matrix ...' )for movie, users in movie_user.items(): for u in users: for v in users: if u == v: continue user_sim_matrix.setdefault(u, {}) user_sim_matrix[u].setdefault(v, 0 ) user_sim_matrix[u][v] += 1 print ('Build user co-rated users matrix success!' )print ('Calculating user similarity matrix ...' )for u, related_users in user_sim_matrix.items(): for v, count in related_users.items(): user_sim_matrix[u][v] = count / math.sqrt(len (trainSet[u]) * len (trainSet[v])) print ('Calculate user similarity matrix success!' )
针对用户u,找到和他最相似的k个用户,并产生N个推荐
得到用户的兴趣相似度后,UserCF算法会对用户推荐和他兴趣最相似的K个用户喜欢的物品。下面公式度量了UserCF算法中用户u对物品i的感兴趣程度:
\[p(u, i)=\sum_{v \in S(u, K) \cap N(i)}w_{u v} r_{v i}\]
其中, \(S(u,k)\)包含和兴趣u兴趣最接近的K个用户,\(N(i)\) 是对物品i有过行为的用户集合,\(w_{uv}\) 是用户u和用户v的兴趣相似度,\(r_{vi}\) 代表用户v对物品i的兴趣,因为使用单一行为的隐反馈数据, 所以这里\(r_{vi}=1\)
所以下面的代码逻辑是这样: * 首先, 给定我一个用户ID,我先拿到这个用户ID目前看过的所有电影, 以防后面推荐重了。 * 然后从相似性矩阵中,找到与当前用户最相近的K个用户 *遍历他们看过的电影, 如果当前用户没有看过, 该电影的权重等级累加 *最后给所有的电影进行排序, 推荐前n部给当前用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 k = 20 n = 10 aim_user = 3 rank ={} watched_movies = trainSet[aim_user] for v, wuv in sorted (user_sim_matrix[aim_user].items(), key=lambda x: x[1 ], reverse=True )[0 :k]: for movie in trainSet[v]: if movie in watched_movies: continue rank.setdefault(movie, 0 ) rank[movie] += wuv rec_movies = sorted (rank.items(), key=itemgetter(1 ), reverse=True )[:n]
推荐结果的评估
召回率
对用户u推荐N个物品记为\(R(u)\) ,令用户u在测试集上喜欢的物品集合为\(T(u)\), 那么召回率定义为: \[\operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|}\]这个意思就是说在用户真实购买或者看过的影片里面,我模型真正预测出了多少,这个考察的是模型推荐的一个全面性。分母的位置是测试集上的总的数量
准确率
准确率定义为: \[\operatorname{Precision}=\frac{\sum_{u} \mid R(u) \capT(u)}{\sum_{u}|R(u)|}\] 这个意思在我推荐的所有物品中, 用户真正看的有多少,这个考察的是我模型推荐的一个准确性。
覆盖率
覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高,说明推荐算法越能将长尾中的物品推荐给用户。 \[\text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|}\] 该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户,那么覆盖率是100%
新颖度
用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门, 说明推荐的新颖度较低。由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定,在计算平均流行度时对每个物品的流行度取对数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 hit = 0 rec_count = 0 test_count = 0 all_rec_movies = set () item_populatity = dict () for user, items in trainSet.items(): for item in items.keys(): if item not in item_populatity: item_populatity[item] = 0 item_populatity[item] += 1 ret = 0 ret_cou = 0 for user, items in trainSet.items(): test_movies = testSet.get(user, {}) rec_movies = recommend(user) for movie, w in rec_movies: if movie in test_movies: hit += 1 all_rec_movies.add(movie) ret += math.log(1 +item_populatity[movie]) ret_cou += 1 rec_count += n test_count += len (test_movies) precision = hit / (1.0 * rec_count) recall = hit / (1.0 * test_count) coverage = len (all_rec_movies) / movie_count ret /= ret_cou*1.0 print ('precisioin = %.4f\nrecall = %.4f\ncoverage = %.4f\npopularity = %.4f' % (precision, recall, coverage, ret))
物品协同过滤算法 导入数据, 读取文件得到"用户-电影"的评分数据,并且分为训练集和测试 计算电影之间的相似度 针对目标用户u, 找到其最相似的k个用户, 产生N个推荐 产生推荐之后, 通过准确率、召回率和覆盖率等进行评估。 这一步之前都和基于用户的协同过滤算法一样,都进行了数据集的训练和测试的划分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 trainSet, testSet = {}, {} trainSet_len, testSet_len = 0 , 0 pivot = 0.75 for ele in data.itertuples(): user, movie, rating = getattr (ele, 'userId' ), getattr (ele, 'movieId' ), getattr (ele, 'rating' ) if random.random() < pivot: trainSet.setdefault(user, {}) trainSet[user][movie] = rating trainSet_len += 1 else : testSet.setdefault(user, {}) testSet[user][movie] = rating testSet_len += 1 print ('Split trainingSet and testSet success!' )print ('TrainSet = %s' % trainSet_len)print ('TestSet = %s' % testSet_len)
和UserItemCF 相似, 这里同样需要建立一个倒排表,只不过这里的倒排变成了{用户:物品} 的倒排表, 如下:
我们这里的存储正好是“用户-物品"评分表, 所以现在正好是倒排的形式,所以不用刻意建立建立倒排表, 直接遍历trainSet即可, 但是在这之前,我们先计算下每部电影的流行程度, 也就是被用户观看的总次数,这个在衡量相似度的时候作为分母, 这里的其他逻辑和UserCF基本一致了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 movie_popular = {}for user, movies in trainSet.items(): for movie in movies: if movie not in movie_popular: movie_popular[movie] = 0 movie_popular[movie] += 1 movie_count = len (movie_popular)print ('Total movie number = %d' % movie_count)print ('Build user co-rated movies matrix ...' ) movie_sim_matrix = {}for user, movies in trainSet.items(): for m1 in movies: for m2 in movies: if m1 == m2: continue movie_sim_matrix.setdefault(m1, {}) movie_sim_matrix[m1].setdefault(m2, 0 ) movie_sim_matrix[m1][m2] += 1 print ('Build user co-rated movies matrix success!' )print ('Calculating movies similarity matrix ...' )for m1, related_movies in movie_sim_matrix.items(): for m2, count in related_movies.items(): if movie_popular[m1] == 0 or movie_popular[m2] == 0 : movie_sim_matrix[m1][m2] = 0 else : movie_sim_matrix[m1][m2] = count / math.sqrt(movie_popular[m1] * movie_popular[m2]) print ('Calculate movies similarity matrix success!' )
找到相似性最高的K个物品并进行N推荐
所以下面的代码逻辑是这样: * 首先, 给定我一个用户ID,我先拿到这个用户ID目前看过的所有电影, 以防后面推荐重了。 * 然后从相似性矩阵中,找到与当前用户看的物品的最相近的K个物品 *遍历他们看过的电影, 如果当前用户没有看过, 该电影的权重等级累加 *最后给所有的电影进行排序, 推荐前n部给当前用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 k = 20 n = 10 aim_user = 10 rank ={} watched_movies = trainSet[aim_user] for movie, rating in watched_movies.items(): for related_movie, w in sorted (movie_sim_matrix[movie].items(), key=itemgetter(1 ), reverse=True )[:k]: if related_movie in watched_movies: continue rank.setdefault(related_movie, 0 ) rank[related_movie] += w * float (rating) rec_movies = sorted (rank.items(), key=itemgetter(1 ), reverse=True )[:n]
对模型的性能进行评估
这部分的内容和基于用户的协同过滤算法一样
召回率 准确率 覆盖率 新颖率 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 hit = 0 rec_count = 0 test_count = 0 all_rec_movies = set () item_populatity = dict () for user, items in trainSet.items(): for item in items.keys(): if item not in item_populatity: item_populatity[item] = 0 item_populatity[item] += 1 ret = 0 ret_cou = 0 for user, items in trainSet.items(): test_movies = testSet.get(user, {}) rec_movies = recommend(user) for movie, w in rec_movies: if movie in test_movies: hit += 1 all_rec_movies.add(movie) ret += math.log(1 +item_populatity[movie]) ret_cou += 1 rec_count += n test_count += len (test_movies) precision = hit / (1.0 * rec_count) recall = hit / (1.0 * test_count) coverage = len (all_rec_movies) / movie_count ret /= ret_cou*1.0 print ('precisioin = %.4f\nrecall = %.4f\ncoverage = %.4f\npopularity = %.4f' % (precision, recall, coverage, ret))
隐语义模型与矩阵分解 参考文档https://blog.csdn.net/wuzhongqiang/article/details/108173885
矩阵分解算法 矩阵分解算法:BacisSVD, RSVD, ASVD, SVD++
隐语义模型其实就是在想办法基于这个评分矩阵去找到上面例子中的那两个矩阵,也就是用户兴趣和物品的隐向量表达,然后就把这个评分矩阵分解成Q和P两个矩阵乘积的形式,这时候就可以基于这两个矩阵去预测某个用户对某个物品的评分了。然后基于这个评分去进行推荐 。
首先, 先初始化用户矩阵P和物品矩阵Q,P的维度是[users_num, F]
,Q的维度是[item_nums, F]
, 这个F是隐向量的维度。也就是通过隐向量的方式把用户的兴趣和F的特点关联了起来。初始化这两个矩阵的方式很多, 但根据经验,随机数需要和1/sqrt(F)
成正比。 有了两个矩阵之后, 我就可以根据用户已经打分的数据去更新参数,这就是训练模型的过程, 方法很简单, 就是遍历用户, 对于每个用户,遍历它打分的电影,这样就拿到了该用户和电影的隐向量,然后两者相乘加上偏置就是预测的评分, 这时候与真实评分有个差距,根据上面的梯度下降就可以进行参数的更新 训练好模型之后, 就可以进行预测评分, 根据预测的评分对用户推荐 评估模型, 评估方式还是协同过滤里面的四种评估标准 矩阵分解示意图 矩阵分解算法将m × n维的共享矩阵R分解成m × k维的用户矩阵U和k ×n维的物品矩阵V相乘的形式。 其中m是用户数量, n是物品数量,k是隐向量维度, 也就是隐含特征个数,只不过这里的隐含特征变得不可解释了, 即我们不知道具体含义了,要模型自己去学。
最常用的方法是特征值分解(EVD) 或者奇异值分解(SVD) ,EVD要求分解的矩阵是方阵,显然用户-物品矩阵不满足这个要求
SVD矩阵分解算法 这个算法的思路就是深度学习的思路
首先先初始化这两个矩阵 把用户评分矩阵里面已经评过分的那些样本当做训练集的label
,把对应的用户和物品的隐向量当做features,这样就会得到(features, label)
相当于训练集 通过两个隐向量乘积得到预测值pred
根据label
和pred
计算损失 然后反向传播,通过梯度下降的方式,更新两个隐向量的值 未评过分的那些样本当做测试集,通过两个隐向量就可以得到测试集的label
值 这样就填充完了矩阵, 下一步就可以进行推荐了 有个问题就是当参数很多的时候, 就是两个矩阵很大的时候,往往容易陷入过拟合的困境, 这时候,就需要在目标函数上面加上正则化的损失, 就变成了RSVD
读取数据和对测试数据和训练数据的划分的步骤和之前没有差别具体的步骤可以看之前的协同过滤算法
建立SVD模型
这里按照矩阵分解法初始化隐特征矩阵,转化为一个最优化问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def __init__ (self, rating_data, F=5 , alpha=0.1 , lmbda=0.1 , max_iter=100 ): self.F = F self.P = dict () self.Q = dict () self.bu = dict () self.bi = dict () self.mu = 0.0 self.alpha = alpha self.lmbda = lmbda self.max_iter = max_iter self.rating_data = rating_data cnt = 0 for user, items in self.rating_data.items(): self.P[user] = [random.random() / math.sqrt(self.F) for x in range (0 , F)] self.bu[user] = 0 cnt += len (items) for item, rating in items.items(): if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in range (0 , F)] self.bi[item] = 0 self.mu /= cnt
训练的过程:采用的是梯度下降法进行更新参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def train (self ): for step in range (self.max_iter): for user, items in self.rating_data.items(): for item, rui in items.items(): rhat_ui = self.predict(user, item) e_ui = rui - rhat_ui self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user]) self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item]) for k in range (0 , self.F): self.P[user][k] += self.alpha * (e_ui*self.Q[item][k] - self.lmbda * self.P[user][k]) self.Q[item][k] += self.alpha * (e_ui*self.P[user][k] - self.lmbda * self.Q[item][k]) self.alpha *= 0.1
根据此产生推荐的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 movie_list = []for user, items in trainSet.items(): for item in items.keys(): if item not in movie_list: movie_list.append(item)def recommend (aim_user, n=10 ): rank = {} watched_movies = trainSet[aim_user] for movie in movie_list: if movie in watched_movies: continue rank[movie] = basicsvd.predict(aim_user, movie) return sorted (rank.items(), key=lambda x: x[1 ], reverse=True )[:n]
最后进行模型结果的评估:准确率、召回率、覆盖率
RSVD矩阵分解算法 在目标函数中加入正则化参数(加入惩罚项), 对于目标函数来说, \(Q\) 矩阵和 \(V\) 矩阵中的所有值都是变量,这些变量在不知道哪个变量会带来过拟合的情况下,对所有变量都进行惩罚:\[\begin{aligned}\mathrm{SSE} & =\frac{1}{2} \sum_{\mathrm{u}, \mathrm{i}}\mathrm{e}_{\mathrm{ui}}^2+\frac{1}{2} \lambda\sum_{\mathrm{u}}\left|\mathrm{p}_{\mathrm{u}}\right|^2+\frac{1}{2}\lambda \sum_{\mathrm{i}}\left|\mathrm{q}_{\mathrm{i}}\right|^2 \\& =\frac{1}{2} \sum_{\mathrm{u}, \mathrm{i}}\mathrm{e}_{\mathrm{ui}}^2+\frac{1}{2} \lambda \sum_{\mathrm{u}}\sum_{\mathrm{k}=0}^{\mathrm{K}} \mathrm{p}_{\mathrm{u},\mathrm{k}}^2+\frac{1}{2} \lambda \sum_{\mathrm{i}}\sum_{\mathrm{k}=0}^{\mathrm{K}} \mathrm{q}_{\mathrm{k}, \mathrm{i}}^2\end{aligned}\]
这时候目标函数对参数的导数就发生了变化, 前面的那块没变,无非就是加入了后面的梯度。所以此时对 \(\mathrm{p}_{\mathrm{u}, \mathrm{k}}\) 求导,得到: \[\begin{aligned}& \frac{\partial}{\partial p_{u, k}}\mathrm{SSE}=-\mathrm{e}_{\mathrm{ui}} \mathrm{q}_{\mathrm{k},\mathrm{i}}+\lambda \mathrm{p}_{\mathrm{u}, \mathrm{k}} \\& \frac{\partial}{\partial \mathrm{q}_{\mathrm{i}, \mathrm{k}}}\mathrm{SSE}=-\mathrm{e}_{\mathrm{u}, \mathrm{i}}\mathrm{p}_{\mathrm{u}, \mathrm{k}}+\lambda \mathrm{q}_{\mathrm{i},\mathrm{k}}\end{aligned}\]
这样, 正则化之后, 梯度的更新公式就变成了: \[\begin{aligned}p_{\mathrm{u}, \mathrm{k}} & =\mathrm{p}_{\mathrm{u},\mathrm{k}}+\eta\left(\mathrm{e}_{\mathrm{ui}} \mathrm{q}_{\mathrm{k},\mathrm{i}}-\lambda \mathrm{p}_{\mathrm{u}, \mathrm{k}}\right) \\\mathrm{q}_{\mathrm{k}, \mathrm{i}} & =\mathrm{q}_{\mathrm{k},\mathrm{i}}+\eta\left(\mathrm{e}_{\mathrm{ui}} \mathrm{p}_{\mathrm{u},\mathrm{k}}-\lambda \mathrm{q}_{\mathrm{i}, \mathrm{k}}\right)\end{aligned}\]
SVD++矩阵分解 SVD++, 它将用户历史评分的物品加入到了LFM模型里
之前的矩阵分解是只分解的当前的共现矩阵, 比如某个用户\(u\)对于某个物品\(i\)的评分, 就单纯的分解成用户\(u\)的隐向量与物品\(i\)的隐向量乘积再加上偏置项,这时候注意并没有考虑该用户评分的历史物品
这个式子是预测用户 \(\mathrm{u}\) 对于物品 \(\mathrm{i}\) 的打分, \(\mathrm{N}(\mathrm{u})\) 表示用户 \(\mathrm{u}\) 打过分的历史物品, \(\mathrm{w}_{\mathrm{ij}}\) 表示物品 \(\mathrm{ij}\) 的相似度,当然这里的这个相似度不再是ItemCF那样, 通过向量计算的, 而是想向LFM那样,让模型自己学出这个参数来, 那么相应的就可以通过优化的思想: \[\mathrm{SSE}=\sum_{(\mathrm{u}, \mathrm{i}) \in \text { Train}}\left(\mathrm{r}_{\mathrm{ui}}-\sum_{\mathrm{j} \in\mathrm{N}(\mathrm{u})} \mathrm{w}_{\mathrm{ij}}\mathrm{r}_{\mathrm{uj}}\right)^2+\lambda \mathrm{w}_{\mathrm{ij}}^2\]
但是呢, 这么模型有个问题, 就是 \(\mathrm{w}\) 比较稠密, 存储需要很大的空间,因为如果有 \(\mathrm{n}\) 个物品,那么模型的参数就是 \(\mathrm{n}^2\) ,参数一多, 就容易造成过拟合。所以Koren提出应该对 \(\mathrm{w}\) 矩阵进行分解, 将参数降到了\(2 * \mathrm{n} * \mathrm{~F}\) :\[\hat{\mathrm{r}}_{\mathrm{ui}}=\frac{1}{\sqrt{|\mathrm{N}(\mathrm{u})|}}\sum_{\mathrm{j} \in \mathrm{N}(\mathrm{u})}\mathrm{x}_{\mathrm{i}}^{\mathrm{T}}\mathrm{y}_{\mathrm{j}}=\frac{1}{\sqrt{|\mathrm{N}(\mathrm{u})|}}\mathrm{x}_{\mathrm{i}}^{\mathrm{T}} \sum_{\mathrm{j} \in\mathrm{N}(\mathrm{u})} \mathrm{y}_{\mathrm{j}}\]
相当于用 \(\mathrm{x}_{\mathrm{i}}^{\mathrm{T}}\mathrm{y}_{\mathrm{j}}\) 代替了 \(\mathrm{w}_{\mathrm{ij}}\), 这里的 \(\mathrm{x}_{\mathrm{i}},\mathrm{y}_{\mathrm{j}}\) 是两个 \(\mathrm{F}\) 维的向量。有没有发现在这里,就出现了点 \(\mathrm{FM}\) 的改进身影了。这里其实就是又对物品 \(\mathrm{i}\) 和某个用户 \(\mathrm{u}\)买过的历史物品又学习一波隐向量, 这次是 \(\mathrm{F}\) 维, 为了衡量出物品 \(\mathrm{i}\) 和历史物品 \(\mathrm{j}\) 之间的相似性来。这时候,参数的数量降了下来,并同时也考虑进来了用户的历史物品记录。所以这个和之前的LFM相加就得到了:\[\hat{r}_{u i}=\mu+b_u+b_i+p_u^T \cdot q_i+\frac{1}{\sqrt{|N(u)|}} x_i^T\sum_{j \in N(u)} y_j\]
FM+FFM模型 基本的框架
因子分解机(Factorization Machine, FM)和域感知因子分解机(Field-awareFactorization Machine, FFM)
FM算法与使用 在逻辑回归里面, 如果想得到组合特征,往往需要人工在特征工程的时候手动的组合特征, 然后再进行篮选,但这个比较低效, 第一个是这个会有经验的成分在里面,第二个是可能会比较玄学, 不太好找到有用的组合特征。于是乎,采用POLY2模型进行特征的“暴力”组合就成了可行的选择。POLY2是二阶多项式模型,数学形式如下: \[y=w_0+\sum_{i=1}^n w_i x_i+\sum_{i=1}^{n-1} \sum_{i+1}^n w_{i j} x_i x_j\]
看到这个基本上不用怎么解释就明白了,这个模型对所有的特征进行了两两的交叉,然后又算得了一个权重,这个其实和逻辑回归依然是超级像的,如果我们在逻辑回归中,做特征工程的时候,也可以自己做出这样的一些特征来
POLY2缺点: 任意两个参数相互独立,这时候如果数据非常稀疏, 再要训练这么多参数, 无疑是非常困难的,最终模型也不会很好。POLY2模型虽然是引入了特征的二阶交叉组合,但是由于其模型参数, 稀疏场景受限的问题使得FM登场了
FM算法思路:
对于稀疏的评分矩阵, 我们有办法分解成两个向量相乘的形式,那么为何不把这种思想用到解决POLY2的缺陷上呢?无非就是评分矩阵换成POLY2后面的W矩阵(所有二次项系数 \(w_{ij}\)组成的)就是把W矩阵进行分解成两个矩阵相乘的方式
对于二次项参数 \(\mathrm{w}_{\mathrm{ij}}\) 组成的对称阵\(\mathrm{W}\) (为了方面说明 \(\mathrm{FM}\) 的由来,对角元素设置为正实数),我们就可以分解成 \(\mathrm{V}^{\mathrm{T}} \mathrm{V}\)的形式, \(\mathrm{V}\) 的第 \(j\) 列 \(v_j\) 表示的是第 \(j\) 维特征 \(x_j\) 的隐向量。换句话说,特征分量 \(x_i\) 和 \(x_j\) 的交叉系数就等于 \(x_i\) 和 \(x_j\) 对应的隐向量的内积,即每个参数 \(\mathrm{w}_{\mathrm{ij}}=<\mathrm{v}_{\mathrm{i}},\mathrm{v}_{\mathrm{j}}>\), 这就是 \(F M\) 模型的核心思想: \[\mathrm{W}^{\star}=\left[\begin{array}{cccc}\omega_{11} & \omega_{12} & \ldots & \omega_{1 \mathrm{n}}\\\omega_{21} & \omega_{22} & \ldots & \omega_{2 \mathrm{n}}\\\ldots & \ldots & \ldots & \ldots \\\omega_{\mathrm{n} 1} & \omega_{\mathrm{n} 2} & \ldots &\omega_{\mathrm{nn}}\end{array}\right]=\mathrm{V}^{\mathrm{T}}\mathrm{V}=\left[\begin{array}{c}\mathrm{V}_1 \\\mathrm{~V}_2 \\\ldots \\\mathrm{V}_{\mathrm{n}}\end{array}\right] \times\left[\mathrm{V}_1, \mathrm{~V}_2, \ldots,\mathrm{V}_{\mathrm{n}}\right]=\left[\begin{array}{cccc}\mathrm{v}_{11} & \mathrm{v}_{12} & \ldots & \mathrm{v}_{1\mathrm{k}} \\\mathrm{v}_{21} & \mathrm{v}_{22} & \ldots & \mathrm{v}_{2\mathrm{k}} \\\ldots & \ldots & \ldots & \ldots \\\mathrm{v}_{\mathrm{n} 1} & \mathrm{v}_{\mathrm{n} 2} & \ldots& \mathrm{v}_{\mathrm{nk}}\end{array}\right] \times\left[\begin{array}{c}\mathrm{v}_{11} \\\mathrm{v}_{12} \\\ldots \\\mathrm{v}_{1 \mathrm{k}}\end{array}\right.\]
这时候, 为了求 \(w_{i j}\) ,我们需要求出特征分量 \(x_i\) 的辅助向量\(v_i=\left(v_{i 1}, v_{i 2}, \ldots v_{ik}\right), v_j=\left(v_{j 1}, v_{j 2}, \ldots v_{j k}\right)\) 所以, 有了这样的一个铺垫, 就可以写出FM的模型方程了,就是POLY2 的基础上,把 \(\mathrm{w}_{\mathrm{ij}}\) 写成了两个隐向量相乘的方式。 \[\hat{y}(X)=\omega_0+\sum_{i=1}^n \omega_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n<v_i, v_j>x_i x_j\] FM的公式是一个通用的拟合方程,可以采用不同的损失函数用于解决regression、classification等问题,比如可以采用MSE(MeanSquare Error)loss function来求解回归问题,也可以采用Hinge/Cross-Entropyloss来求解分类问题。
安装相关的包pip install git+https://github.com/coreylynch/pyFM
1 2 3 4 5 6 from pyfm import pylibfmfrom sklearn.feature_extraction import DictVectorizerfrom sklearn.preprocessing import OneHotEncoderimport numpy as npimport pandas as pd
使用这个类最简单的方式就是把数据存成字典的形式,然后用DictVectorizer进行one-hot
1 2 3 4 5 6 7 8 9 train = [ {'user' : '1' , 'item' : '5' , 'age' : 19 }, {'user' : '2' , 'item' : '43' , 'age' : 33 }, {'user' : '3' , 'item' : '20' , 'age' : 55 }, {'user' : '4' , 'item' : '10' , 'age' : 20 } ] v = DictVectorizer() X = v.fit_transform(train) X.toarray()
1 2 3 y = np.repeat(1 , X.shape[0 ]) fm = pylibfm.FM() fm.fit(X, y)
进行测试
1 2 3 test = v.transform({'user' : "1" , 'item' : "10" , 'age' : 24 }) fm.predict(test)
给出另一个例子说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def loadData (): rating_data={1 : {'A' : 5 , 'B' : 3 , 'C' : 4 , 'D' : 4 }, 2 : {'A' : 3 , 'B' : 1 , 'C' : 2 , 'D' : 3 , 'E' : 3 }, 3 : {'A' : 4 , 'B' : 3 , 'C' : 4 , 'D' : 3 , 'E' : 5 }, 4 : {'A' : 3 , 'B' : 3 , 'C' : 1 , 'D' : 5 , 'E' : 4 }, 5 : {'A' : 1 , 'B' : 5 , 'C' : 5 , 'D' : 2 , 'E' : 1 } } return rating_data rating_data = loadData() df = pd.DataFrame(rating_data).T df = df.stack().reset_index() df.columns = ['user' , 'item' , 'rating' ] df['user' ] = df['user' ].astype('str' ) item_map = {item: str (idx) for idx, item in enumerate (set (df['item' ]))} df['item' ] = df['item' ].map (item_map) train_data = df[['user' , 'item' ]] y = df['rating' ] one = OneHotEncoder() x = one.fit_transform(train_data) fm = pylibfm.FM(num_factors=10 , num_iter=100 , verbose=True , task='regression' , initial_learning_rate=0.001 , learning_rate_schedule='optimal' ) fm.fit(x, y) test = {'user' : '1' , 'item' : '4' } x_test = one.transform(pd.DataFrame(test, index=[0 ])) pred_rating = fm.predict(x_test)print ('FM的预测评分:{}' .format (pred_rating[0 ]))
FM算法的回归与分类任务 回归任务
数据集的下载地址: http://www.grouplens.org/system/files/ml-100k.zip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npfrom sklearn.feature_extraction import DictVectorizerfrom pyfm import pylibfmdef loadData (filename, path='ml-100k/' ): data = [] y = [] users = set () items = set () with open (path+filename) as f: for line in f: (user, movieid, rating, ts) = line.split('\t' ) data.append({'user_id' : str (user), 'movie_id' : str (movieid)}) y.append(float (rating)) users.add(user) items.add(movieid) return (data, np.array(y), users, items)
数据类型:
1 2 3 4 v = DictVectorizer() X_train = v.fit_transform(train_data) X_test = v.transform(test_data)
1 2 3 4 5 fm = pylibfm.FM(num_factors=10 , num_iter=100 , verbose=True , task='regression' , initial_learning_rate=0.001 , learning_rate_schedule='optimal' ) fm.fit(X_train, y_train)
FM的具体参数函数如下: 这里面重点需要设置的已标出(详细的可以参考源码)* num_factors : 隐向量的维度, 也就是k *num_iter : 迭代次数, 由于使用的SGD, 随机梯度下降,要指明迭代多少个epoch * k0, k1: k0表示是否用偏置(看FM的公式),k1表示是否要第二项, 就是单个特征的, 这俩默认True * init_stdev:初始化隐向量时候的方差, 默认0.01 * validation_size :验证集的比例, 默认0.01 * learning_rate_schedule: 学习率衰减方式,有constant, optimal, 和invscaling三种方式, 具体公式看源码 *initial_learning_rate : 初始学习率, 默认0.01 *power_t, t0: 逆缩放学习率的指数,最优学习率分母常数,这两个和上面学习率衰减方式的计算有关 * task :分类或者回归任务, 要指明 * verbose: 是否打印当前的迭代次数, 训练误差 *shuffle_training: 是否在学习之前打乱训练集 * seed: 随机种子
1 2 3 4 5 preds = fm.predict(X_test)from sklearn.metrics import mean_squared_errorprint ('FM MSE: %.4f' % mean_squared_error(y_test, preds))
分类任务
创建一个随机的分类数据集并对数据集进行测试集和验证集的划分
1 2 3 4 5 6 7 from sklearn.datasets import make_classification from sklearn.model_selection import train_test_splitfrom sklearn.metrics import log_loss X, y = make_classification(n_samples=1000 , n_features=100 , n_clusters_per_class=1 ) data = [{v: k for k, v in dict (zip (i, range (len (i)))).items()} for i in X] x_train, x_test, y_train, y_test = train_test_split(data, y, test_size=0.1 , random_state=42 )
对数据集进行one-hot的处理
1 2 3 v = DictVectorizer() x_train = v.fit_transform(x_train) x_test = v.transform(x_test)
1 2 3 4 5 6 7 8 fm = pylibfm.FM(num_factors=50 , num_iter=10 , verbose=True , task='classification' , initial_learning_rate=0.0001 , learning_rate_schedule='optimal' ) fm.fit(x_train, y_train) y_pre = fm.predict(x_test)print ('validation log loss: %.4f' % log_loss(y_test, y_pre))
FFM算法介绍与使用 FFM是基于FM进行的修改,FFM模型引入了特征域感知(filed-aware) ,我们先回顾一下FM模型公式: \[\hat{y}(X)=\omega_0+\sum_{i=1}^n \omega_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n<v_i, v_j>x_i x_j\] FFM就是一个特征对应多个隐向量。这样在与不同域(类)里面特征交叉的时候,用相应的隐向量去交叉计算权重,这样做的好处是学习隐向量的时候只需要考虑相应的域的数据,与不同类的特征进行关联采用不同的隐向量,这和不同类特征的内在差异也比较相符。 这其实就是FFM在FM的基础上做的改进,引入了域的概念 ,对于每个特征,针对不同的交叉域要学习不同的隐向量特征
细品的话, 不同单词不同身份的时候,会有不同的embedding对待,其实这里的FFM域embedding,如果经过上面的铺垫感觉FFM差不多了,那么下面就是模型的方程了:
\[\hat{y}(X)=\omega_0+\sum_{i=1}^n \omega_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n<v_{i, f_j}, v_{j, f_i}>x_i x_j\] \(<v_{i, f_j}, v_{j,f_i}>\) 注意这里的已经是加上域的内容之后的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 class FFM_Node (object ): ''' 通常x是高维稀疏向量,所以用链表来表示一个x,链表上的每个节点是个3元组(j,f,v) ''' __slots__ = ['j' , 'f' , 'v' ] def __init__ (self, j, f, v ): """ j: Feature index (0-n-1) f: field index(0-m-1) v: value """ self.j = j self.f = f self.v = v class FFM (object ): def __init__ (self, m, n, k, eta, lambd ): """ m: Number of fields n: Number of features k: Number of latent factors eta: learning rate lambd: regularization coefficient """ self.m = m self.n = n self.k = k self.eta = eta self.lambd = lambd self.w = np.random.rand(n, m, k) / math.sqrt(k) self.G = np.ones(shape=(n, m, k), dtype=np.float64) self.log = Logistic() def phi (self, node_list ): """ 特征组合式的线性加权求和 param node_list: 用链表存储x中的非0值 """ z = 0.0 for a in range (len (node_list)): node1 = node_list[a] j1 = node1.j f1 = node1.f v1 = node1.v for b in range (a+1 , len (node_list)): node2 = node_list[b] j2 = node2.j f2 = node2.f v2 = node2.v w1 = self.w[j1, f2] w2 = self.w[j2, f1] z += np.dot(w1, w2) * v1 * v2 return z def predict (self, node_list ): """ 输入x, 预测y的值 """ z = self.phi(node_list) y = self.log.decide_by_tanh(z) return y def sgd (self, node_list, y ): """ 根据一个样本更新模型参数: node_list: 链表存储x中的非0值 y: 正样本1, 负样本-1 """ kappa = -y / (1 +math.exp(y*self.phi(node_list))) for a in range (len (node_list)): node1 = node_list[a] j1 = node1.j f1 = node1.f v1 = node1.v for b in range (a+1 , len (node_list)): node2 = node_list[b] j2 = node2.j f2 = node2.f v2 = node2.v c = kappa * v1 * v2 g_j1_f2 = self.lambd * self.w[j1, f2] + c * self.w[j2, f1] g_j2_f1 = self.lambd * self.w[j2, f1] + c * self.w[j1, f2] self.G[j1, f2] += g_j1_f2 ** 2 self.G[j2, f1] += g_j2_f1 ** 2 self.w[j1, f2] -= self.eta / np.sqrt(self.G[j1, f2]) * g_j1_f2 self.w[j2, f1] -= self.eta / np.sqrt( self.G[j2, f1]) * g_j2_f1 def train (self, sample_generator, max_echo, max_r2 ): """ 根据一堆样本训练模型 sample_generator: 样本生成器,每次yield (node_list, y),node_list中存储的是x的非0值。通常x要事先做好归一化,即模长为1,这样精度会略微高一点 max_echo: 最大迭代次数 max_r2: 拟合系数r2达到阈值时即可终止学习 """ for itr in range (max_echo): print ("echo: " , itr) y_sum = 0.0 y_sqare_sum = 0.0 err_square_sum = 0.0 population = 0 for node_list, y in sample_generator: y = 0.0 if y == -1 else y self.sgd(node_list, y) y_hat = self.predict(node_list) y_sum += y y_sqare_sum += y ** 2 err_square_sum += (y-y_hat) ** 2 population += 1 var_y = y_sqare_sum - y_sum * y_sum / population r2 = 1 - err_square_sum / var_y print ("r2: " , r2) if r2 > max_r2: print ("r2 have reach" , r2) break def save_model (self, outfile ): ''' 序列化模型 :param outfile: :return: ''' np.save(outfile, self.w) def load_model (self, infile ): ''' 加载模型 :param infile: :return: ''' self.w = np.load(infile)
调用的过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 n = 5 m = 2 k = 2 train_file = "dataset/train.txt" valid_file = "dataset/test.txt" model_file = "ffm.npy" eta = 0.01 lambd = 1e-2 max_echo = 30 max_r2 = 0.9 sample_generator = Sample(train_file) ffm = FFM(m, n, k, eta, lambd) ffm.train(sample_generator, max_echo, max_r2) ffm.save_model(model_file)
逻辑回归模型与GBDT+LR模型
协同过滤和矩阵分解利用用户的物品“相似度”进行推荐 ,逻辑回归模型将问题看成了一个分类问题,通过预测正样本的概率对物品进行排序
这里的正样本可以是用户“点击”了某个商品或者“观看”了某个视频,均是推荐系统希望用户产生“正反馈”行为,因此逻辑回归模型将推荐问题转成成了一个点击率预估问题
逻辑回归LR算法 逻辑回归是在线性回归的基础上加了一个 Sigmoid函数(非线形)映射,使得逻辑回归称为了一个优秀的分类算法,学习逻辑回归模型,首先要记住一句话:逻辑回归假设数据服从伯努利分布,通过极大化似然函数的方法,运用梯度下降来求解参数,来达到将数据二分类的目的。
逻辑回归模型已经将推荐问题转换成了一个点击率预测的问题 ,而点击率预测就是一个典型的二分类, 正好适合逻辑回归进行处理,那么逻辑回归是如何做推荐的呢?
将用户年龄、性别、物品属性、物品描述、当前时间、当前地点等特征转成数值型向量
确定逻辑回归的优化目标,比如把点击率预测转换成二分类问题,这样就可以得到分类问题常用的损失作为目标, 训练模型
在预测的时候, 将特征向量输入模型产生预测,得到用户“点击”物品的概率
利用点击概率对候选物品排序, 得到推荐列表
训练和推断的过程 每个特征的权重参数\(w\) ,我们一般是使用梯度下降的方式, 首先会先随机初始化一批\(w\),然后将特征向量(也就是我们上面数值化出来的特征)输入到模型,就会通过计算会得到模型的预测概率, 然后通过对目标函数求导得到每个\(w\)的梯度, 然后进行更新\(w\)
优点:
LR模型形式简单,可解释性好,从特征的权重可以看到不同的特征对最后结果的影响。
训练时便于并行化,在预测时只需要对特征进行线性加权,所以性能比较好,往往适合处理海量id类特征,用id类特征有一个很重要的好处,就是防止信息损失(相对于范化的CTR 特征),对于头部资源会有更细致的描述
资源占用小,尤其是内存。在实际的工程应用中只需要存储权重比较大的特征及特征对应的权重
方便输出结果调整。逻辑回归可以很方便的得到最后的分类结果,因为输出的是每个样本的概率分数,我们可以很容易的对这些概率分数进行cutoff,也就是划分阈值(大于某个阈值的是一类,小于某个阈值的是一类)
工程化需要, 在深度学习技术之前, 逻辑回归凭借易于并行化,模型简单,训练开销小等特点,占领工程领域的主流,因为即使工程团队发现了复杂模型会提升效果,但一般如果没有把握击败逻辑回归的话仍然不敢尝试或者升级。
当然, 逻辑回归模型也有一定的局限性
表达能力不强, 无法进行特征交叉 ,特征筛选等一系列“高级“操作(这些工作都得人工来干,这样就需要一定的经验, 否则会走一些弯路), 因此可能造成信息的损失 准确率并不是很高。因为这毕竟是一个线性模型加了个sigmoid,形式非常的简单(非常类似线性模型),很难去拟合数据的真实分布 处理非线性数据较麻烦。逻辑回归在不引入其他方法的情况下,只能处理线性可分的数据,如果想处理非线性,首先对连续特征的处理需要先进行离散化(离散化的目的是为了引入非线性),如上文所说,人工分桶的方式会引入多种问题。 LR需要进行人工特征组合,这就需要开发者有非常丰富的领域经验,才能不走弯路。这样的模型迁移起来比较困难,换一个领域又需要重新进行大量的特征工程。 GBDT原理 GBDT全称梯度提升决策树 ,GBDT(Gradient BoostingDecisionTree,梯度提升决策树)是一种机器学习算法,它通过优化损失函数的梯度来构建一组树模型的集合,并希望这些树模型能够协同工作以获得更准确的结果。
GBDT的工作原理:
初始化模型:使用一个简单的树模型作为起点。 训练模型:评估当前模型的损失函数。 根据当前模型的预测值来计算损失函数的梯度。 在当前模型的基础上,寻找能够最小化梯度下降的树模型。 组合模型:将新树模型的预测结果与原模型的预测结果结合,作为下一轮迭代的初始模型。 重复迭代:重复步骤2和3,直到达到预设的迭代次数或模型性能不再提升 GBDT的优点:
灵活性:可以处理非线性问题。 效率:在预测时,每个节点的计算只与少数特征有关,因此计算效率相对较高。 效果:在许多任务中,尤其是回归和分类任务中,GBDT都能取得很好的效果。 GBDT的缺点:
过拟合:由于模型复杂,容易过拟合,需要通过正则化、限制树的数量或深度等方法来控制。 调参复杂:需要调整多个超参数,如学习率、树的数量、节点的最大深度等。 GBDT+LR算法原理 利用GBDT自动进行特征筛选和组合 ,进而生成新的离散特征向量,再把该特征向量当做LR模型的输入 , 来产生最后的预测结果,这就是著名的GBDT+LR模型了。GBDT+LR使用最广泛的场景是CTR点击率预估,即预测当给用户推送的广告会不会被用户点击
训练时 ,GBDT建树的过程相当于自动进行的特征组合和离散化,然后从根结点到叶子节点的这条路径就可以看成是不同特征进行的特征组合,用叶子节点可以唯一的表示这条路径,并作为一个离散特征传入LR 进行二次训练
比如上图中,有两棵树,x为一条输入样本,遍历两棵树后,x样本分别落到两颗树的叶子节点上,每个叶子节点对应LR一维特征,那么通过遍历树,就得到了该样本对应的所有LR特征。构造的新特征向量是取值0/1的。比如左树有三个叶子节点,右树有两个叶子节点,最终的特征即为五维的向量。对于输入x,假设他落在左树第二个节点,编码[0,1,0],落在右树第二个节点则编码[0,1],所以整体的编码为[0,1,0,0,1],这类编码作为特征,输入到线性分类模型(LRor FM)中进行分类。
预测时, 会先走 GBDT的每棵树,得到某个叶子节点对应的一个离散特征(即一组特征组合),然后把该特征以one-hot 形式传入 LR 进行线性加权预测。
GBDT+LR编程实践 这个比赛的任务就是:开发预测广告点击率(CTR)的模型。给定一个用户和他正在访问的页面,预测他点击给定广告的概率是多少?比赛的地址链接:https://www.kaggle.com/c/criteo-display-ad-challenge/overview
导入相关的依赖包和数据集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npimport pandas as pdfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_splitimport lightgbm as lgbfrom sklearn.preprocessing import MinMaxScaler, OneHotEncoder, LabelEncoderfrom sklearn.metrics import log_lossimport gcfrom scipy import sparseimport warnings warnings.filterwarnings('ignore' ) path = 'data/' df_train = pd.read_csv(path + 'train.csv' ) df_test = pd.read_csv(path + 'test.csv' )
数据预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 path = 'data/' df_train = pd.read_csv(path + 'train.csv' ) df_test = pd.read_csv(path + 'test.csv' ) df_train.drop(['Id' ], axis=1 , inplace=True ) df_test.drop(['Id' ], axis=1 , inplace=True ) df_test['Label' ] = -1 data = pd.concat([df_train, df_test]) data.fillna(-1 , inplace=True ) continuous_fea = ['I' +str (i+1 ) for i in range (13 )] category_fea = ['C' +str (i+1 ) for i in range (26 )]
LR模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def lr_model (data, category_fea, continuous_fea ): scaler = MinMaxScaler() for col in continuous_fea: data[col] = scaler.fit_transform(data[col].values.reshape(-1 , 1 )) for col in category_fea: onehot_feats = pd.get_dummies(data[col], prefix=col) data.drop([col], axis=1 , inplace=True ) data = pd.concat([data, onehot_feats], axis=1 ) train = data[data['Label' ] != -1 ] target = train.pop('Label' ) test = data[data['Label' ] == -1 ] test.drop(['Label' ], axis=1 , inplace=True ) x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2 , random_state=2020 ) lr = LogisticRegression() lr.fit(x_train, y_train) tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1 ]) val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1 ]) print ('tr_logloss: ' , tr_logloss) print ('val_logloss: ' , val_logloss) y_pred = lr.predict_proba(test)[:, 1 ] print ('predict: ' , y_pred[:10 ])
1 2 lr_model(data.copy(), category_fea, continuous_fea)
LR预测的结果:
GBDT建模
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 def gbdt_model (data, category_fea, continuous_fea ): for col in category_fea: onehot_feats = pd.get_dummies(data[col], prefix=col) data.drop([col], axis=1 , inplace=True ) data = pd.concat([data, onehot_feats], axis=1 ) train = data[data['Label' ] != -1 ] target = train.pop('Label' ) test = data[data['Label' ] == -1 ] test.drop(['Label' ], axis=1 , inplace=True ) x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2 , random_state=2020 ) gbm = lgb.LGBMClassifier(boosting_type='gbdt' , objective='binary' , subsample=0.8 , min_child_weight=0.5 , colsample_bytree=0.7 , num_leaves=100 , max_depth=12 , learning_rate=0.01 , n_estimators=10000 ) gbm.fit(x_train, y_train, eval_set=[(x_train, y_train), (x_val, y_val)], eval_names=['train' , 'val' ], eval_metric='binary_logloss' , early_stopping_rounds=100 , ) tr_logloss = log_loss(y_train, gbm.predict_proba(x_train)[:, 1 ]) val_logloss = log_loss(y_val, gbm.predict_proba(x_val)[:, 1 ]) print ('tr_logloss: ' , tr_logloss) print ('val_logloss: ' , val_logloss) y_pred = gbm.predict_proba(test)[:, 1 ] print ('predict: ' , y_pred[:10 ])
1 2 gbdt_model(data.copy(), category_fea, continuous_fea)
预测的结果:
LR + GBDT建模
下面就是把上面两个模型进行组合, GBDT负责对各个特征进行交叉和组合,把原始特征向量转换为新的离散型特征向量, 然后在使用逻辑回归模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 def gbdt_lr_model (data, category_feature, continuous_feature ): x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.2 , random_state = 2020 ) gbm = lgb.LGBMClassifier(objective='binary' , subsample= 0.8 , min_child_weight= 0.5 , colsample_bytree= 0.7 , num_leaves=100 , max_depth = 12 , learning_rate=0.01 , n_estimators=1000 , ) gbm.fit(x_train, y_train, eval_set = [(x_train, y_train), (x_val, y_val)], eval_names = ['train' , 'val' ], eval_metric = 'binary_logloss' , early_stopping_rounds = 100 , ) model = gbm.booster_ gbdt_feats_train = model.predict(train, pred_leaf = True ) gbdt_feats_test = model.predict(test, pred_leaf = True ) gbdt_feats_name = ['gbdt_leaf_' + str (i) for i in range (gbdt_feats_train.shape[1 ])] df_train_gbdt_feats = pd.DataFrame(gbdt_feats_train, columns = gbdt_feats_name) df_test_gbdt_feats = pd.DataFrame(gbdt_feats_test, columns = gbdt_feats_name) train = pd.concat([train, df_train_gbdt_feats], axis = 1 ) test = pd.concat([test, df_test_gbdt_feats], axis = 1 ) train_len = train.shape[0 ] data = pd.concat([train, test]) del train del test gc.collect() scaler = MinMaxScaler() for col in continuous_feature: data[col] = scaler.fit_transform(data[col].values.reshape(-1 , 1 )) for col in gbdt_feats_name: onehot_feats = pd.get_dummies(data[col], prefix = col) data.drop([col], axis = 1 , inplace = True ) data = pd.concat([data, onehot_feats], axis = 1 ) train = data[: train_len] test = data[train_len:] del data gc.collect() x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.3 , random_state = 2018 ) lr = LogisticRegression() lr.fit(x_train, y_train) tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1 ]) print ('tr-logloss: ' , tr_logloss) val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1 ]) print ('val-logloss: ' , val_logloss) y_pred = lr.predict_proba(test)[:, 1 ] print (y_pred[:10 ])
深度学习模型(Rank) image-20240308111249239 ]]>
+ 推荐系统算法总结 推荐系统近几年有了深度学习的助推发展之势迅猛,从前深度学习的传统推荐模型(协同过滤,矩阵分解,LR, FM, FFM,GBDT)到深度学习的浪潮之巅(DNN, Deep Crossing, DIN, DIEN, Wide&Deep,Deep&Cross, DeepFM, AFM, NFM, PNN, FNN,DRN)。推荐系统通过分析用户的历史行为给用户的兴趣建模,从而主动给用户推荐给能够满足他们兴趣和需求的信息
传统模型(Recall)
数据集介绍 https://github.com/ZiyaoGeng/RecLearn/wiki/Movielens
rating 标签列表为:UserID::MovieID::Rating::Timestamp
UserIDs:用户ID(1~6040) MovieIDs:电影ID(1~3952) Ratings:评分(1~5) Timestamp:时间戳 user 标签列表为:UserID::Gender::Age::Occupation::Zip-code
Gender:性别, "M"代表男, "F"代表女; Age:年龄,分为多个区间:1,18, 25, 35, 45, 50;
Occupation:职业,0~20; movies 标签列表为:MovieID::Title::Genres
基于内容的推荐 这是一种比较简单的推荐方法,基于内容的推荐方法是非常直接的,它以物品的内容描述信息为依据来做出的推荐,本质上是基于对物品和用户自身的特征或属性的直接分析和计算。例如,假设已知电影A是一部喜剧,而恰巧我们得知某个用户喜欢看喜剧电影,那么我们基于这样的已知信息,就可以将电影A推荐给该用户。具体实现步骤:
构建物品画像(主要包括物品的分类信息,标题, 各种属性等等) 构建用户画像(主要包括用户的喜好, 行为的偏好,基本的人口学属性,活跃程度,风控维度) 根据用户的兴趣, 去找相应的物品, 实施推荐。 基本流程 建立物品画像基于用户给电影打的tag和电影的分类值,得到每一部电影的总标签 求每一部电影标签的tf-idf值 根据tf-idf的结果,为每一部电影选择top-n(tf-idf值较大)的关键词作为整部电影的关键词,最后得到了电影id— 关键词 — 关键词权重
建立倒排索引这个是为了能够根据关键词找到对应的电影,好方便得到用户画像之后(用户喜欢啥样的电影)对用户进行一些推荐 建立用户画像看用户看过哪些电影, 基于前面的物品画像找到电影对应的关键词 把用户看过的所有关键词放到一起, 统计词频, 每个词出现了几次 出现次数最多的关键词作为用户的兴趣词, 这个就是用户的画像 根据用户的兴趣词, 基于倒排表找到电影,就可以对用户实施推荐了。 物品画像 本质上是基于对物品和用户自身的特征或属性 的直接分析和计算
构建标签数据集
数据集的四列关键词为:userId, movieId, tag, timestamp
1 2 3 4 5 6 7 _tags = pd.read_csv("ml-latest-small/all-tags.csv" , usecols=range (1 , 3 )).dropna() tags = _tags.groupby("movieId" ).agg(list ) tags.head()
构建物料数据
数据集关键标签为:movieId, title, genres
1 2 3 4 5 6 7 movies = pd.read_csv("ml-latest-small/movies.csv" , index_col="movieId" ) movies['genres' ] = movies['genres' ].apply(lambda x: x.split("|" )) movies.head()
合并物品和标签的列表
1 2 3 4 5 6 7 8 9 10 11 movies_index = set (movies.index) & set (tags.index) new_tags = tags.loc[list (movies_index)] ret = movies.join(new_tags) ret.head()
image-20240204115110070 数据处理并补充缺失值
1 2 3 df = map (lambda x: (x[0 ], x[1 ], x[2 ], x[2 ]+x[3 ]) if x[3 ] is not np.nan else (x[0 ], x[1 ], x[2 ], []), ret.itertuples()) movies_dataset = pd.DataFrame(df, columns=['movieId' , 'title' , 'genres' , 'tags' ]) movies_dataset.head()
这段代码的作用是对retDataFrame中的tags列进行处理,将缺失值替换为空列表,并创建一个新的DataFrame,以便于后续的数据分析和处理
数据预处理补充缺失值 TF-IDF模型 引入相关的软件包,其中包含了TfidfModel
相当于是TFIDF模型
1 2 3 from gensim.models import TfidfModelfrom pprint import pprintfrom gensim.corpora import Dictionary
通常用于创建词典,它将数据集中的所有唯一单词作为键,每个单词出现的次数作为值
1 2 3 4 5 6 7 8 dataset = movies_dataset["tags" ].values dct = Dictionary(dataset) corpus = [dct.doc2bow(line) for line in dataset]
构建TF-IDF模型并进行展示
1 2 3 4 model = TfidfModel(corpus) model[corpus[0 ]]
1 2 3 4 5 6 7 8 9 movie_profile = {}for i, mid in enumerate (movies_dataset.index): tfidf_vec = model[corpus[i]] movies_tags = sorted (tfidf_vec, key=lambda x: x[1 ], reverse=True )[:30 ] movie_profile[mid] = dict (map (lambda x: (dct[x[0 ]], x[1 ]), movies_tags))
得出最终的结果
1 2 movie_profile = create_movie_profile(movie_dataset) movie_profile.head()
建立倒排索引 倒排索引 就是用物品的其他数据作为索引,去提取他们对应的物品的ID列表
1 2 3 4 5 6 7 8 9 10 11 12 def create_inverted_table (movie_profile ): inverted_table = {} for mid, weights in movie_profile['weights' ].iteritems(): for tag, weight in weights.items(): _ = inverted_table.get(tag, []) _.append((mid, weight)) inverted_table.setdefault(tag, _) return inverted_table
1 inverted_table = create_inverted_table(movie_profile)
这样就可以直接根据标签去推荐电影 了
用户画像 构建步骤:
根据用户的评分历史,结合物品画像,将有观影记录的电影的画像标签作为初始标签反打到用户身上 通过对用户观影标签的次数进行统计,计算用户的每个初始标签的权重值,排序后选取TOP-N作为用户最终的画像标签 1 2 import collectionsfrom functools import reduce
1 2 3 4 5 6 watch_record = pd.read_csv("ml-latest-small/ratings.csv" , usecols=range (2 ), dtype={"userId" :np.int32, "movieId" : np.int32}) watch_record = watch_record.groupby("userId" ).agg(list )
产生Top-N的推荐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 user_profile = {}for uid, mids in watch_record.itertuples(): record_movie_profile = movie_profile.loc[list (mids)] counter = collections.Counter(reduce(lambda x, y: list (x) + list (y), record_movie_profile['profile' ].values)) interest_words = counter.most_common(50 ) maxcount = interest_words[0 ][1 ] interest_words = [(w, round (c/maxcount, 4 )) for w, c, in interest_words] user_profile[uid] = interest_words
这个地方根据用户观看的视频以及视频对应的关键词和每个关键词的权重,给出每个用户感兴趣的关键词的权重归一化之后的值
注意,这个地方给出的归一化的操作是使用频率进行归一化操作
下面给用户进行视频的推荐
输出对应的用户的uid,推荐的视频编号,以及视频的推荐概率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for uid, interest_words in user_profile.items(): result_table = {} for interest_word, interest_weight in interest_words: related_movies = inverted_table[interest_word] for mid, relate_weight in related_movies: _ = result_table.get(mid, []) _.append(interest_weight) result_table.setdefault(mid, _) rs_result = map (lambda x: (x[0 ], sum (x[1 ])), result_table.items()) rs_result = sorted (rs_result, key=lambda x: x[1 ], reverse=True )[:100 ] print (uid) pprint(rs_result) break
冷启动算法 这里主要包括两个很厉害的技术:
* Word2Vec : 这个可以根据得到电影标签的词向量,根据这个词向量, 就能够得到tag之间的相似性,这样就能够根据用户看过的某个电影, 得到这个电影的标签,然后根据这些标签得到与其近似的标签,然后得到这些近似标签下的电影对该用户产生推荐
1 2 3 4 5 6 7 8 9 10 11 from gensim.models import Word2Vec sentences = list (movie_profile["profile" ].values) model = Word2Vec(sentences, window=3 , min_count=1 ) words = input ("words: " ) ret = model.wv.most_similar(positive=[words], topn=10 ) print (ret)
* Doc2Vec :这个可以根据电影的所有标签,训练一个模型来得到最终电影的影片向量, 根据这个,就能够直接计算用户看过的某个电影与其他电影的相似性,然后根据这个相似性给用户推荐最相似的几篇文章。
1 2 3 4 5 6 7 from gensim.models.doc2vec import Doc2Vec, TaggedDocumentfrom gensim.test.utils import get_tmpfile documents = [TaggedDocument(words, [movie_id]) for movie_id, words in movie_profile["profile" ].iteritems()] documents
1 2 3 4 5 6 7 8 9 10 11 12 13 14 model = Doc2Vec(documents, vector_size=100 , window=3 , min_count=1 , epochs=20 ) words = movie_profile["profile" ].loc[6 ]print (words) inferred_vector = model.infer_vector(words) sims = model.docvecs.most_similar([inferred_vector], topn=10 )print (sims)
协同过滤算法 协同过滤(Collaborative Filtering)算法,基本思想是根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品(基于对用户历史行为数据的挖掘发现用户的喜好偏向,并预测用户可能喜好的产品进行推荐),一般是仅仅基于用户的行为数据(评价、购买、下载等),而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄,性别等)。目前应用比较广泛的协同过滤算法是基于邻域的方法,而这种方法主要有下面两种算法:
基于用户的协同过滤算法(UserCF):给用户推荐和他兴趣相似的其他用户喜欢的产品 基于物品的协同过滤算法(ItemCF):给用户推荐和他之前喜欢的物品相似的物品 两种CF算法介绍 用户协同和物品协同的使用场景
*UserCF的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度;
* ItemCF的推荐更加个性化,反映了用户自己的兴趣传承
UserCF适合于新闻推荐
热门程度和时效性是个性化新闻推荐的重点,而个性化相对于这两点略显次要 UserCF需要维护一个用户兴趣相似表,而ItemCF需要维护一个物品相似表,在新闻推荐系统中物品的更新速度是很快的,那么如果采用ItemCF的话,物品相似度表也需要很快地更新,这是难以实现的 ItemCF适合于图书、电子商务和电影网站
用户的兴趣是比较固定和持久的 这些系统中用户不太需要流行度来辅助他们判断一个物品的好坏,而是可以通过自己熟知的领域的知识自己判断物品的质量 UserCF的适用场合
用户较少 的场合,如果用户很多,计算用户相似度度矩阵代价很大(新闻网站)时效性较强,用户个性化兴趣不太明显 的领域 不需要给出令用户信服的推荐解释 ItemCF的适用场合
适用于物品的数量明显小于用户的数量 的场合,如物品很多(网站),计算物品的相似度矩阵代价很大 长尾物品丰富,用户个性化 需求强烈的领域 需要利用用户的历史行为给用户做推荐解释,可以令用户比较信服 该实验使用的数据集来自:http://grouplens.org/datasets/movielens/
算法基本流程 不管是UserCF还是ItemCF, 行文逻辑都是下面的四个步骤: 1. 导入数据,读取文件得到"用户-电影"的评分数据, 并且分为训练集和测试集 2.计算用户(userCF)或者电影(itemcf)之间的相似度 3. 针对目标用户u,找到其最相似的k个用户/产品, 产生N个推荐 4. 产生推荐之后,通过准确率、召回率和覆盖率等进行评估。
工业界协同过滤的流程 数据处理 对行为少不活跃的用户进行过滤, 行为少的用户, 数据太过于稀疏,召回难度大 对用户中热门物品进行过滤, 热门物品可能大部分用户都有过行为 非常活跃的用户, 用户协同可能会出现一种情况,就是每个用户的topN相似用户里都有些非常活跃的用户,所有需要适当过滤掉这些用户 建立用户embedding和物品embedding, 或者可以像案例这样,直接建立共现矩阵, 也可以训练embedding 计算用户和N个用户的相似度, 保存N个相似用户曾经看过的TopK个物品 模型(矩阵)进行定期更新, 这个要根据不同项目组的情况,可能是一天更新一次, 也可能不是, 看具体的情况,更新的时候使用前N天(N一般3-10)的活跃用户的数据进行更新 每次召回一次N条, 刷完N条再继续召回 还有可能用户两次行为(上拉或者下滑)之间间隔很长时间,也会进行重新召回 每次召回的数量,需要根据召回通道以及各个召回通道配置的召回占比进行配置 为了保证用户不疲劳, 一般情况下, 利用user-cf计算召回结果后,会做一定的类别去重, 保证召回覆盖度 实际过程中, 根据公司核心用户的数量大小, 考虑实现工具,如果数据量较大, 可使用spark进行用户协同的结果计算 如果用户量实在太过巨大, 可考虑使用稀疏存储的方式进行存储,即只存储含有1(或者其他值)的位置坐标索引index以及对应的值 用户协同过滤算法 TopN推荐的任务是预测用户会不会对某部电影评分,而不是预测用户在准备对某部电影评分的前提下给电影评多少分 ,下面我们开始, 从逻辑上看, 其实这个任务主要分为下面的步骤: 1.导入数据, 读取文件得到"用户-电影"的评分数据, 并且分为训练集和测试集 2.计算用户之间的相似度 3. 针对目标用户u, 找到其最相似的k个用户,产生N个推荐 4. 产生推荐之后, 通过准确率、召回率和覆盖率等进行评估。
引入依赖包和读取数据
1 2 3 4 5 6 7 8 9 10 11 import randomimport numpy as npimport pandas as pdimport mathfrom operator import itemgetter data_path = './ml-latest-small/' data = pd.read_csv(data_path+'ratings.csv' ) data.head() data.pivot(index='userId' , columns='movieId' , values='rating' )
存在稀疏的情况举例 存在大量的稀疏的情况,因此后续会对此进行数据的补全
将数据集划分成训练集和测试集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 trainSet, testSet = {}, {} trainSet_len, testSet_len = 0 , 0 pivot = 0.75 for ele in data.itertuples(): user, movie, rating = getattr (ele, 'userId' ), getattr (ele, 'movieId' ), getattr (ele, 'rating' ) if random.random() < pivot: trainSet.setdefault(user, {}) trainSet[user][movie] = rating trainSet_len += 1 else : testSet.setdefault(user, {}) testSet[user][movie] = rating testSet_len += 1 print ('Split trainingSet and testSet success!' )print ('TrainSet = %s' % trainSet_len)print ('TestSet = %s' % testSet_len)
1 2 3 Split trainingSet and testSet success! TrainSet = 75732 TestSet = 25104
建立用户相似度的表
如果直接遍历用户表会产生比较大的时间复杂度,不如直接建立一个物品到用户的倒排表,对每个物品都保存对该物品都产生过的用户列表
具体的代码如下所示:
第一步将原来的列表转化成电影-用户的倒排索引 第二步计算每个用户之间的相似性矩阵 1 2 3 4 5 6 7 8 9 10 11 12 user_sim_matrix = {}print ('Building movie-user table ...' ) movie_user = {}for user, movies in trainSet.items(): for movie in movies: if movie not in movie_user: movie_user[movie] = set () movie_user[movie].add(user) print ('Build movie-user table success!' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 movie_count = len (movie_user)print ('Total movie number = %d' % movie_count)print ('Build user co-rated users matrix ...' )for movie, users in movie_user.items(): for u in users: for v in users: if u == v: continue user_sim_matrix.setdefault(u, {}) user_sim_matrix[u].setdefault(v, 0 ) user_sim_matrix[u][v] += 1 print ('Build user co-rated users matrix success!' )print ('Calculating user similarity matrix ...' )for u, related_users in user_sim_matrix.items(): for v, count in related_users.items(): user_sim_matrix[u][v] = count / math.sqrt(len (trainSet[u]) * len (trainSet[v])) print ('Calculate user similarity matrix success!' )
针对用户u,找到和他最相似的k个用户,并产生N个推荐
得到用户的兴趣相似度后,UserCF算法会对用户推荐和他兴趣最相似的K个用户喜欢的物品。下面公式度量了UserCF算法中用户u对物品i的感兴趣程度:
\[p(u, i)=\sum_{v \in S(u, K) \cap N(i)}w_{u v} r_{v i}\]
其中, \(S(u,k)\)包含和兴趣u兴趣最接近的K个用户,\(N(i)\) 是对物品i有过行为的用户集合,\(w_{uv}\) 是用户u和用户v的兴趣相似度,\(r_{vi}\) 代表用户v对物品i的兴趣,因为使用单一行为的隐反馈数据, 所以这里\(r_{vi}=1\)
所以下面的代码逻辑是这样: * 首先, 给定我一个用户ID,我先拿到这个用户ID目前看过的所有电影, 以防后面推荐重了。 * 然后从相似性矩阵中,找到与当前用户最相近的K个用户 *遍历他们看过的电影, 如果当前用户没有看过, 该电影的权重等级累加 *最后给所有的电影进行排序, 推荐前n部给当前用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 k = 20 n = 10 aim_user = 3 rank ={} watched_movies = trainSet[aim_user] for v, wuv in sorted (user_sim_matrix[aim_user].items(), key=lambda x: x[1 ], reverse=True )[0 :k]: for movie in trainSet[v]: if movie in watched_movies: continue rank.setdefault(movie, 0 ) rank[movie] += wuv rec_movies = sorted (rank.items(), key=itemgetter(1 ), reverse=True )[:n]
推荐结果的评估
召回率
对用户u推荐N个物品记为\(R(u)\) ,令用户u在测试集上喜欢的物品集合为\(T(u)\), 那么召回率定义为: \[\operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|}\]这个意思就是说在用户真实购买或者看过的影片里面,我模型真正预测出了多少,这个考察的是模型推荐的一个全面性。分母的位置是测试集上的总的数量
准确率
准确率定义为: \[\operatorname{Precision}=\frac{\sum_{u} \mid R(u) \capT(u)}{\sum_{u}|R(u)|}\] 这个意思在我推荐的所有物品中, 用户真正看的有多少,这个考察的是我模型推荐的一个准确性。
覆盖率
覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高,说明推荐算法越能将长尾中的物品推荐给用户。 \[\text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|}\] 该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户,那么覆盖率是100%
新颖度
用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门, 说明推荐的新颖度较低。由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定,在计算平均流行度时对每个物品的流行度取对数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 hit = 0 rec_count = 0 test_count = 0 all_rec_movies = set () item_populatity = dict () for user, items in trainSet.items(): for item in items.keys(): if item not in item_populatity: item_populatity[item] = 0 item_populatity[item] += 1 ret = 0 ret_cou = 0 for user, items in trainSet.items(): test_movies = testSet.get(user, {}) rec_movies = recommend(user) for movie, w in rec_movies: if movie in test_movies: hit += 1 all_rec_movies.add(movie) ret += math.log(1 +item_populatity[movie]) ret_cou += 1 rec_count += n test_count += len (test_movies) precision = hit / (1.0 * rec_count) recall = hit / (1.0 * test_count) coverage = len (all_rec_movies) / movie_count ret /= ret_cou*1.0 print ('precisioin = %.4f\nrecall = %.4f\ncoverage = %.4f\npopularity = %.4f' % (precision, recall, coverage, ret))
物品协同过滤算法 导入数据, 读取文件得到"用户-电影"的评分数据,并且分为训练集和测试 计算电影之间的相似度 针对目标用户u, 找到其最相似的k个用户, 产生N个推荐 产生推荐之后, 通过准确率、召回率和覆盖率等进行评估。 这一步之前都和基于用户的协同过滤算法一样,都进行了数据集的训练和测试的划分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 trainSet, testSet = {}, {} trainSet_len, testSet_len = 0 , 0 pivot = 0.75 for ele in data.itertuples(): user, movie, rating = getattr (ele, 'userId' ), getattr (ele, 'movieId' ), getattr (ele, 'rating' ) if random.random() < pivot: trainSet.setdefault(user, {}) trainSet[user][movie] = rating trainSet_len += 1 else : testSet.setdefault(user, {}) testSet[user][movie] = rating testSet_len += 1 print ('Split trainingSet and testSet success!' )print ('TrainSet = %s' % trainSet_len)print ('TestSet = %s' % testSet_len)
和UserItemCF 相似, 这里同样需要建立一个倒排表,只不过这里的倒排变成了{用户:物品} 的倒排表, 如下:
我们这里的存储正好是“用户-物品"评分表, 所以现在正好是倒排的形式,所以不用刻意建立建立倒排表, 直接遍历trainSet即可, 但是在这之前,我们先计算下每部电影的流行程度, 也就是被用户观看的总次数,这个在衡量相似度的时候作为分母, 这里的其他逻辑和UserCF基本一致了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 movie_popular = {}for user, movies in trainSet.items(): for movie in movies: if movie not in movie_popular: movie_popular[movie] = 0 movie_popular[movie] += 1 movie_count = len (movie_popular)print ('Total movie number = %d' % movie_count)print ('Build user co-rated movies matrix ...' ) movie_sim_matrix = {}for user, movies in trainSet.items(): for m1 in movies: for m2 in movies: if m1 == m2: continue movie_sim_matrix.setdefault(m1, {}) movie_sim_matrix[m1].setdefault(m2, 0 ) movie_sim_matrix[m1][m2] += 1 print ('Build user co-rated movies matrix success!' )print ('Calculating movies similarity matrix ...' )for m1, related_movies in movie_sim_matrix.items(): for m2, count in related_movies.items(): if movie_popular[m1] == 0 or movie_popular[m2] == 0 : movie_sim_matrix[m1][m2] = 0 else : movie_sim_matrix[m1][m2] = count / math.sqrt(movie_popular[m1] * movie_popular[m2]) print ('Calculate movies similarity matrix success!' )
找到相似性最高的K个物品并进行N推荐
所以下面的代码逻辑是这样: * 首先, 给定我一个用户ID,我先拿到这个用户ID目前看过的所有电影, 以防后面推荐重了。 * 然后从相似性矩阵中,找到与当前用户看的物品的最相近的K个物品 *遍历他们看过的电影, 如果当前用户没有看过, 该电影的权重等级累加 *最后给所有的电影进行排序, 推荐前n部给当前用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 k = 20 n = 10 aim_user = 10 rank ={} watched_movies = trainSet[aim_user] for movie, rating in watched_movies.items(): for related_movie, w in sorted (movie_sim_matrix[movie].items(), key=itemgetter(1 ), reverse=True )[:k]: if related_movie in watched_movies: continue rank.setdefault(related_movie, 0 ) rank[related_movie] += w * float (rating) rec_movies = sorted (rank.items(), key=itemgetter(1 ), reverse=True )[:n]
对模型的性能进行评估
这部分的内容和基于用户的协同过滤算法一样
召回率 准确率 覆盖率 新颖率 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 hit = 0 rec_count = 0 test_count = 0 all_rec_movies = set () item_populatity = dict () for user, items in trainSet.items(): for item in items.keys(): if item not in item_populatity: item_populatity[item] = 0 item_populatity[item] += 1 ret = 0 ret_cou = 0 for user, items in trainSet.items(): test_movies = testSet.get(user, {}) rec_movies = recommend(user) for movie, w in rec_movies: if movie in test_movies: hit += 1 all_rec_movies.add(movie) ret += math.log(1 +item_populatity[movie]) ret_cou += 1 rec_count += n test_count += len (test_movies) precision = hit / (1.0 * rec_count) recall = hit / (1.0 * test_count) coverage = len (all_rec_movies) / movie_count ret /= ret_cou*1.0 print ('precisioin = %.4f\nrecall = %.4f\ncoverage = %.4f\npopularity = %.4f' % (precision, recall, coverage, ret))
隐语义模型与矩阵分解 参考文档https://blog.csdn.net/wuzhongqiang/article/details/108173885
矩阵分解算法 矩阵分解算法:BacisSVD, RSVD, ASVD, SVD++
隐语义模型其实就是在想办法基于这个评分矩阵去找到上面例子中的那两个矩阵,也就是用户兴趣和物品的隐向量表达,然后就把这个评分矩阵分解成Q和P两个矩阵乘积的形式,这时候就可以基于这两个矩阵去预测某个用户对某个物品的评分了。然后基于这个评分去进行推荐 。
首先, 先初始化用户矩阵P和物品矩阵Q,P的维度是[users_num, F]
,Q的维度是[item_nums, F]
, 这个F是隐向量的维度。也就是通过隐向量的方式把用户的兴趣和F的特点关联了起来。初始化这两个矩阵的方式很多, 但根据经验,随机数需要和1/sqrt(F)
成正比。 有了两个矩阵之后, 我就可以根据用户已经打分的数据去更新参数,这就是训练模型的过程, 方法很简单, 就是遍历用户, 对于每个用户,遍历它打分的电影,这样就拿到了该用户和电影的隐向量,然后两者相乘加上偏置就是预测的评分, 这时候与真实评分有个差距,根据上面的梯度下降就可以进行参数的更新 训练好模型之后, 就可以进行预测评分, 根据预测的评分对用户推荐 评估模型, 评估方式还是协同过滤里面的四种评估标准 矩阵分解示意图 矩阵分解算法将m × n维的共享矩阵R分解成m × k维的用户矩阵U和k ×n维的物品矩阵V相乘的形式。 其中m是用户数量, n是物品数量,k是隐向量维度, 也就是隐含特征个数,只不过这里的隐含特征变得不可解释了, 即我们不知道具体含义了,要模型自己去学。
最常用的方法是特征值分解(EVD) 或者奇异值分解(SVD) ,EVD要求分解的矩阵是方阵,显然用户-物品矩阵不满足这个要求
SVD矩阵分解算法 这个算法的思路就是深度学习的思路
首先先初始化这两个矩阵 把用户评分矩阵里面已经评过分的那些样本当做训练集的label
,把对应的用户和物品的隐向量当做features,这样就会得到(features, label)
相当于训练集 通过两个隐向量乘积得到预测值pred
根据label
和pred
计算损失 然后反向传播,通过梯度下降的方式,更新两个隐向量的值 未评过分的那些样本当做测试集,通过两个隐向量就可以得到测试集的label
值 这样就填充完了矩阵, 下一步就可以进行推荐了 有个问题就是当参数很多的时候, 就是两个矩阵很大的时候,往往容易陷入过拟合的困境, 这时候,就需要在目标函数上面加上正则化的损失, 就变成了RSVD
读取数据和对测试数据和训练数据的划分的步骤和之前没有差别具体的步骤可以看之前的协同过滤算法
建立SVD模型
这里按照矩阵分解法初始化隐特征矩阵,转化为一个最优化问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def __init__ (self, rating_data, F=5 , alpha=0.1 , lmbda=0.1 , max_iter=100 ): self.F = F self.P = dict () self.Q = dict () self.bu = dict () self.bi = dict () self.mu = 0.0 self.alpha = alpha self.lmbda = lmbda self.max_iter = max_iter self.rating_data = rating_data cnt = 0 for user, items in self.rating_data.items(): self.P[user] = [random.random() / math.sqrt(self.F) for x in range (0 , F)] self.bu[user] = 0 cnt += len (items) for item, rating in items.items(): if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in range (0 , F)] self.bi[item] = 0 self.mu /= cnt
训练的过程:采用的是梯度下降法进行更新参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def train (self ): for step in range (self.max_iter): for user, items in self.rating_data.items(): for item, rui in items.items(): rhat_ui = self.predict(user, item) e_ui = rui - rhat_ui self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user]) self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item]) for k in range (0 , self.F): self.P[user][k] += self.alpha * (e_ui*self.Q[item][k] - self.lmbda * self.P[user][k]) self.Q[item][k] += self.alpha * (e_ui*self.P[user][k] - self.lmbda * self.Q[item][k]) self.alpha *= 0.1
根据此产生推荐的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 movie_list = []for user, items in trainSet.items(): for item in items.keys(): if item not in movie_list: movie_list.append(item)def recommend (aim_user, n=10 ): rank = {} watched_movies = trainSet[aim_user] for movie in movie_list: if movie in watched_movies: continue rank[movie] = basicsvd.predict(aim_user, movie) return sorted (rank.items(), key=lambda x: x[1 ], reverse=True )[:n]
最后进行模型结果的评估:准确率、召回率、覆盖率
RSVD矩阵分解算法 在目标函数中加入正则化参数(加入惩罚项), 对于目标函数来说, \(Q\) 矩阵和 \(V\) 矩阵中的所有值都是变量,这些变量在不知道哪个变量会带来过拟合的情况下,对所有变量都进行惩罚:\[\begin{aligned}\mathrm{SSE} & =\frac{1}{2} \sum_{\mathrm{u}, \mathrm{i}}\mathrm{e}_{\mathrm{ui}}^2+\frac{1}{2} \lambda\sum_{\mathrm{u}}\left|\mathrm{p}_{\mathrm{u}}\right|^2+\frac{1}{2}\lambda \sum_{\mathrm{i}}\left|\mathrm{q}_{\mathrm{i}}\right|^2 \\& =\frac{1}{2} \sum_{\mathrm{u}, \mathrm{i}}\mathrm{e}_{\mathrm{ui}}^2+\frac{1}{2} \lambda \sum_{\mathrm{u}}\sum_{\mathrm{k}=0}^{\mathrm{K}} \mathrm{p}_{\mathrm{u},\mathrm{k}}^2+\frac{1}{2} \lambda \sum_{\mathrm{i}}\sum_{\mathrm{k}=0}^{\mathrm{K}} \mathrm{q}_{\mathrm{k}, \mathrm{i}}^2\end{aligned}\]
这时候目标函数对参数的导数就发生了变化, 前面的那块没变,无非就是加入了后面的梯度。所以此时对 \(\mathrm{p}_{\mathrm{u}, \mathrm{k}}\) 求导,得到: \[\begin{aligned}& \frac{\partial}{\partial p_{u, k}}\mathrm{SSE}=-\mathrm{e}_{\mathrm{ui}} \mathrm{q}_{\mathrm{k},\mathrm{i}}+\lambda \mathrm{p}_{\mathrm{u}, \mathrm{k}} \\& \frac{\partial}{\partial \mathrm{q}_{\mathrm{i}, \mathrm{k}}}\mathrm{SSE}=-\mathrm{e}_{\mathrm{u}, \mathrm{i}}\mathrm{p}_{\mathrm{u}, \mathrm{k}}+\lambda \mathrm{q}_{\mathrm{i},\mathrm{k}}\end{aligned}\]
这样, 正则化之后, 梯度的更新公式就变成了: \[\begin{aligned}p_{\mathrm{u}, \mathrm{k}} & =\mathrm{p}_{\mathrm{u},\mathrm{k}}+\eta\left(\mathrm{e}_{\mathrm{ui}} \mathrm{q}_{\mathrm{k},\mathrm{i}}-\lambda \mathrm{p}_{\mathrm{u}, \mathrm{k}}\right) \\\mathrm{q}_{\mathrm{k}, \mathrm{i}} & =\mathrm{q}_{\mathrm{k},\mathrm{i}}+\eta\left(\mathrm{e}_{\mathrm{ui}} \mathrm{p}_{\mathrm{u},\mathrm{k}}-\lambda \mathrm{q}_{\mathrm{i}, \mathrm{k}}\right)\end{aligned}\]
SVD++矩阵分解 SVD++, 它将用户历史评分的物品加入到了LFM模型里
之前的矩阵分解是只分解的当前的共现矩阵, 比如某个用户\(u\)对于某个物品\(i\)的评分, 就单纯的分解成用户\(u\)的隐向量与物品\(i\)的隐向量乘积再加上偏置项,这时候注意并没有考虑该用户评分的历史物品
这个式子是预测用户 \(\mathrm{u}\) 对于物品 \(\mathrm{i}\) 的打分, \(\mathrm{N}(\mathrm{u})\) 表示用户 \(\mathrm{u}\) 打过分的历史物品, \(\mathrm{w}_{\mathrm{ij}}\) 表示物品 \(\mathrm{ij}\) 的相似度,当然这里的这个相似度不再是ItemCF那样, 通过向量计算的, 而是想向LFM那样,让模型自己学出这个参数来, 那么相应的就可以通过优化的思想: \[\mathrm{SSE}=\sum_{(\mathrm{u}, \mathrm{i}) \in \text { Train}}\left(\mathrm{r}_{\mathrm{ui}}-\sum_{\mathrm{j} \in\mathrm{N}(\mathrm{u})} \mathrm{w}_{\mathrm{ij}}\mathrm{r}_{\mathrm{uj}}\right)^2+\lambda \mathrm{w}_{\mathrm{ij}}^2\]
但是呢, 这么模型有个问题, 就是 \(\mathrm{w}\) 比较稠密, 存储需要很大的空间,因为如果有 \(\mathrm{n}\) 个物品,那么模型的参数就是 \(\mathrm{n}^2\) ,参数一多, 就容易造成过拟合。所以Koren提出应该对 \(\mathrm{w}\) 矩阵进行分解, 将参数降到了\(2 * \mathrm{n} * \mathrm{~F}\) :\[\hat{\mathrm{r}}_{\mathrm{ui}}=\frac{1}{\sqrt{|\mathrm{N}(\mathrm{u})|}}\sum_{\mathrm{j} \in \mathrm{N}(\mathrm{u})}\mathrm{x}_{\mathrm{i}}^{\mathrm{T}}\mathrm{y}_{\mathrm{j}}=\frac{1}{\sqrt{|\mathrm{N}(\mathrm{u})|}}\mathrm{x}_{\mathrm{i}}^{\mathrm{T}} \sum_{\mathrm{j} \in\mathrm{N}(\mathrm{u})} \mathrm{y}_{\mathrm{j}}\]
相当于用 \(\mathrm{x}_{\mathrm{i}}^{\mathrm{T}}\mathrm{y}_{\mathrm{j}}\) 代替了 \(\mathrm{w}_{\mathrm{ij}}\), 这里的 \(\mathrm{x}_{\mathrm{i}},\mathrm{y}_{\mathrm{j}}\) 是两个 \(\mathrm{F}\) 维的向量。有没有发现在这里,就出现了点 \(\mathrm{FM}\) 的改进身影了。这里其实就是又对物品 \(\mathrm{i}\) 和某个用户 \(\mathrm{u}\)买过的历史物品又学习一波隐向量, 这次是 \(\mathrm{F}\) 维, 为了衡量出物品 \(\mathrm{i}\) 和历史物品 \(\mathrm{j}\) 之间的相似性来。这时候,参数的数量降了下来,并同时也考虑进来了用户的历史物品记录。所以这个和之前的LFM相加就得到了:\[\hat{r}_{u i}=\mu+b_u+b_i+p_u^T \cdot q_i+\frac{1}{\sqrt{|N(u)|}} x_i^T\sum_{j \in N(u)} y_j\]
FM+FFM模型 基本的框架
因子分解机(Factorization Machine, FM)和域感知因子分解机(Field-awareFactorization Machine, FFM)
FM算法与使用 在逻辑回归里面, 如果想得到组合特征,往往需要人工在特征工程的时候手动的组合特征, 然后再进行篮选,但这个比较低效, 第一个是这个会有经验的成分在里面,第二个是可能会比较玄学, 不太好找到有用的组合特征。于是乎,采用POLY2模型进行特征的“暴力”组合就成了可行的选择。POLY2是二阶多项式模型,数学形式如下: \[y=w_0+\sum_{i=1}^n w_i x_i+\sum_{i=1}^{n-1} \sum_{i+1}^n w_{i j} x_i x_j\]
看到这个基本上不用怎么解释就明白了,这个模型对所有的特征进行了两两的交叉,然后又算得了一个权重,这个其实和逻辑回归依然是超级像的,如果我们在逻辑回归中,做特征工程的时候,也可以自己做出这样的一些特征来
POLY2缺点: 任意两个参数相互独立,这时候如果数据非常稀疏, 再要训练这么多参数, 无疑是非常困难的,最终模型也不会很好。POLY2模型虽然是引入了特征的二阶交叉组合,但是由于其模型参数, 稀疏场景受限的问题使得FM登场了
FM算法思路:
对于稀疏的评分矩阵, 我们有办法分解成两个向量相乘的形式,那么为何不把这种思想用到解决POLY2的缺陷上呢?无非就是评分矩阵换成POLY2后面的W矩阵(所有二次项系数 \(w_{ij}\)组成的)就是把W矩阵进行分解成两个矩阵相乘的方式
对于二次项参数 \(\mathrm{w}_{\mathrm{ij}}\) 组成的对称阵\(\mathrm{W}\) (为了方面说明 \(\mathrm{FM}\) 的由来,对角元素设置为正实数),我们就可以分解成 \(\mathrm{V}^{\mathrm{T}} \mathrm{V}\)的形式, \(\mathrm{V}\) 的第 \(j\) 列 \(v_j\) 表示的是第 \(j\) 维特征 \(x_j\) 的隐向量。换句话说,特征分量 \(x_i\) 和 \(x_j\) 的交叉系数就等于 \(x_i\) 和 \(x_j\) 对应的隐向量的内积,即每个参数 \(\mathrm{w}_{\mathrm{ij}}=<\mathrm{v}_{\mathrm{i}},\mathrm{v}_{\mathrm{j}}>\), 这就是 \(F M\) 模型的核心思想: \[\mathrm{W}^{\star}=\left[\begin{array}{cccc}\omega_{11} & \omega_{12} & \ldots & \omega_{1 \mathrm{n}}\\\omega_{21} & \omega_{22} & \ldots & \omega_{2 \mathrm{n}}\\\ldots & \ldots & \ldots & \ldots \\\omega_{\mathrm{n} 1} & \omega_{\mathrm{n} 2} & \ldots &\omega_{\mathrm{nn}}\end{array}\right]=\mathrm{V}^{\mathrm{T}}\mathrm{V}=\left[\begin{array}{c}\mathrm{V}_1 \\\mathrm{~V}_2 \\\ldots \\\mathrm{V}_{\mathrm{n}}\end{array}\right] \times\left[\mathrm{V}_1, \mathrm{~V}_2, \ldots,\mathrm{V}_{\mathrm{n}}\right]=\left[\begin{array}{cccc}\mathrm{v}_{11} & \mathrm{v}_{12} & \ldots & \mathrm{v}_{1\mathrm{k}} \\\mathrm{v}_{21} & \mathrm{v}_{22} & \ldots & \mathrm{v}_{2\mathrm{k}} \\\ldots & \ldots & \ldots & \ldots \\\mathrm{v}_{\mathrm{n} 1} & \mathrm{v}_{\mathrm{n} 2} & \ldots& \mathrm{v}_{\mathrm{nk}}\end{array}\right] \times\left[\begin{array}{c}\mathrm{v}_{11} \\\mathrm{v}_{12} \\\ldots \\\mathrm{v}_{1 \mathrm{k}}\end{array}\right.\]
这时候, 为了求 \(w_{i j}\) ,我们需要求出特征分量 \(x_i\) 的辅助向量\(v_i=\left(v_{i 1}, v_{i 2}, \ldots v_{ik}\right), v_j=\left(v_{j 1}, v_{j 2}, \ldots v_{j k}\right)\) 所以, 有了这样的一个铺垫, 就可以写出FM的模型方程了,就是POLY2 的基础上,把 \(\mathrm{w}_{\mathrm{ij}}\) 写成了两个隐向量相乘的方式。 \[\hat{y}(X)=\omega_0+\sum_{i=1}^n \omega_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n<v_i, v_j>x_i x_j\] FM的公式是一个通用的拟合方程,可以采用不同的损失函数用于解决regression、classification等问题,比如可以采用MSE(MeanSquare Error)loss function来求解回归问题,也可以采用Hinge/Cross-Entropyloss来求解分类问题。
安装相关的包pip install git+https://github.com/coreylynch/pyFM
1 2 3 4 5 6 from pyfm import pylibfmfrom sklearn.feature_extraction import DictVectorizerfrom sklearn.preprocessing import OneHotEncoderimport numpy as npimport pandas as pd
使用这个类最简单的方式就是把数据存成字典的形式,然后用DictVectorizer进行one-hot
1 2 3 4 5 6 7 8 9 train = [ {'user' : '1' , 'item' : '5' , 'age' : 19 }, {'user' : '2' , 'item' : '43' , 'age' : 33 }, {'user' : '3' , 'item' : '20' , 'age' : 55 }, {'user' : '4' , 'item' : '10' , 'age' : 20 } ] v = DictVectorizer() X = v.fit_transform(train) X.toarray()
1 2 3 y = np.repeat(1 , X.shape[0 ]) fm = pylibfm.FM() fm.fit(X, y)
进行测试
1 2 3 test = v.transform({'user' : "1" , 'item' : "10" , 'age' : 24 }) fm.predict(test)
给出另一个例子说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def loadData (): rating_data={1 : {'A' : 5 , 'B' : 3 , 'C' : 4 , 'D' : 4 }, 2 : {'A' : 3 , 'B' : 1 , 'C' : 2 , 'D' : 3 , 'E' : 3 }, 3 : {'A' : 4 , 'B' : 3 , 'C' : 4 , 'D' : 3 , 'E' : 5 }, 4 : {'A' : 3 , 'B' : 3 , 'C' : 1 , 'D' : 5 , 'E' : 4 }, 5 : {'A' : 1 , 'B' : 5 , 'C' : 5 , 'D' : 2 , 'E' : 1 } } return rating_data rating_data = loadData() df = pd.DataFrame(rating_data).T df = df.stack().reset_index() df.columns = ['user' , 'item' , 'rating' ] df['user' ] = df['user' ].astype('str' ) item_map = {item: str (idx) for idx, item in enumerate (set (df['item' ]))} df['item' ] = df['item' ].map (item_map) train_data = df[['user' , 'item' ]] y = df['rating' ] one = OneHotEncoder() x = one.fit_transform(train_data) fm = pylibfm.FM(num_factors=10 , num_iter=100 , verbose=True , task='regression' , initial_learning_rate=0.001 , learning_rate_schedule='optimal' ) fm.fit(x, y) test = {'user' : '1' , 'item' : '4' } x_test = one.transform(pd.DataFrame(test, index=[0 ])) pred_rating = fm.predict(x_test)print ('FM的预测评分:{}' .format (pred_rating[0 ]))
FM算法的回归与分类任务 回归任务
数据集的下载地址: http://www.grouplens.org/system/files/ml-100k.zip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npfrom sklearn.feature_extraction import DictVectorizerfrom pyfm import pylibfmdef loadData (filename, path='ml-100k/' ): data = [] y = [] users = set () items = set () with open (path+filename) as f: for line in f: (user, movieid, rating, ts) = line.split('\t' ) data.append({'user_id' : str (user), 'movie_id' : str (movieid)}) y.append(float (rating)) users.add(user) items.add(movieid) return (data, np.array(y), users, items)
数据类型:
1 2 3 4 v = DictVectorizer() X_train = v.fit_transform(train_data) X_test = v.transform(test_data)
1 2 3 4 5 fm = pylibfm.FM(num_factors=10 , num_iter=100 , verbose=True , task='regression' , initial_learning_rate=0.001 , learning_rate_schedule='optimal' ) fm.fit(X_train, y_train)
FM的具体参数函数如下: 这里面重点需要设置的已标出(详细的可以参考源码)* num_factors : 隐向量的维度, 也就是k *num_iter : 迭代次数, 由于使用的SGD, 随机梯度下降,要指明迭代多少个epoch * k0, k1: k0表示是否用偏置(看FM的公式),k1表示是否要第二项, 就是单个特征的, 这俩默认True * init_stdev:初始化隐向量时候的方差, 默认0.01 * validation_size :验证集的比例, 默认0.01 * learning_rate_schedule: 学习率衰减方式,有constant, optimal, 和invscaling三种方式, 具体公式看源码 *initial_learning_rate : 初始学习率, 默认0.01 *power_t, t0: 逆缩放学习率的指数,最优学习率分母常数,这两个和上面学习率衰减方式的计算有关 * task :分类或者回归任务, 要指明 * verbose: 是否打印当前的迭代次数, 训练误差 *shuffle_training: 是否在学习之前打乱训练集 * seed: 随机种子
1 2 3 4 5 preds = fm.predict(X_test)from sklearn.metrics import mean_squared_errorprint ('FM MSE: %.4f' % mean_squared_error(y_test, preds))
分类任务
创建一个随机的分类数据集并对数据集进行测试集和验证集的划分
1 2 3 4 5 6 7 from sklearn.datasets import make_classification from sklearn.model_selection import train_test_splitfrom sklearn.metrics import log_loss X, y = make_classification(n_samples=1000 , n_features=100 , n_clusters_per_class=1 ) data = [{v: k for k, v in dict (zip (i, range (len (i)))).items()} for i in X] x_train, x_test, y_train, y_test = train_test_split(data, y, test_size=0.1 , random_state=42 )
对数据集进行one-hot的处理
1 2 3 v = DictVectorizer() x_train = v.fit_transform(x_train) x_test = v.transform(x_test)
1 2 3 4 5 6 7 8 fm = pylibfm.FM(num_factors=50 , num_iter=10 , verbose=True , task='classification' , initial_learning_rate=0.0001 , learning_rate_schedule='optimal' ) fm.fit(x_train, y_train) y_pre = fm.predict(x_test)print ('validation log loss: %.4f' % log_loss(y_test, y_pre))
FFM算法介绍与使用 FFM是基于FM进行的修改,FFM模型引入了特征域感知(filed-aware) ,我们先回顾一下FM模型公式: \[\hat{y}(X)=\omega_0+\sum_{i=1}^n \omega_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n<v_i, v_j>x_i x_j\] FFM就是一个特征对应多个隐向量。这样在与不同域(类)里面特征交叉的时候,用相应的隐向量去交叉计算权重,这样做的好处是学习隐向量的时候只需要考虑相应的域的数据,与不同类的特征进行关联采用不同的隐向量,这和不同类特征的内在差异也比较相符。 这其实就是FFM在FM的基础上做的改进,引入了域的概念 ,对于每个特征,针对不同的交叉域要学习不同的隐向量特征
细品的话, 不同单词不同身份的时候,会有不同的embedding对待,其实这里的FFM域embedding,如果经过上面的铺垫感觉FFM差不多了,那么下面就是模型的方程了:
\[\hat{y}(X)=\omega_0+\sum_{i=1}^n \omega_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n<v_{i, f_j}, v_{j, f_i}>x_i x_j\] \(<v_{i, f_j}, v_{j,f_i}>\) 注意这里的已经是加上域的内容之后的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 class FFM_Node (object ): ''' 通常x是高维稀疏向量,所以用链表来表示一个x,链表上的每个节点是个3元组(j,f,v) ''' __slots__ = ['j' , 'f' , 'v' ] def __init__ (self, j, f, v ): """ j: Feature index (0-n-1) f: field index(0-m-1) v: value """ self.j = j self.f = f self.v = v class FFM (object ): def __init__ (self, m, n, k, eta, lambd ): """ m: Number of fields n: Number of features k: Number of latent factors eta: learning rate lambd: regularization coefficient """ self.m = m self.n = n self.k = k self.eta = eta self.lambd = lambd self.w = np.random.rand(n, m, k) / math.sqrt(k) self.G = np.ones(shape=(n, m, k), dtype=np.float64) self.log = Logistic() def phi (self, node_list ): """ 特征组合式的线性加权求和 param node_list: 用链表存储x中的非0值 """ z = 0.0 for a in range (len (node_list)): node1 = node_list[a] j1 = node1.j f1 = node1.f v1 = node1.v for b in range (a+1 , len (node_list)): node2 = node_list[b] j2 = node2.j f2 = node2.f v2 = node2.v w1 = self.w[j1, f2] w2 = self.w[j2, f1] z += np.dot(w1, w2) * v1 * v2 return z def predict (self, node_list ): """ 输入x, 预测y的值 """ z = self.phi(node_list) y = self.log.decide_by_tanh(z) return y def sgd (self, node_list, y ): """ 根据一个样本更新模型参数: node_list: 链表存储x中的非0值 y: 正样本1, 负样本-1 """ kappa = -y / (1 +math.exp(y*self.phi(node_list))) for a in range (len (node_list)): node1 = node_list[a] j1 = node1.j f1 = node1.f v1 = node1.v for b in range (a+1 , len (node_list)): node2 = node_list[b] j2 = node2.j f2 = node2.f v2 = node2.v c = kappa * v1 * v2 g_j1_f2 = self.lambd * self.w[j1, f2] + c * self.w[j2, f1] g_j2_f1 = self.lambd * self.w[j2, f1] + c * self.w[j1, f2] self.G[j1, f2] += g_j1_f2 ** 2 self.G[j2, f1] += g_j2_f1 ** 2 self.w[j1, f2] -= self.eta / np.sqrt(self.G[j1, f2]) * g_j1_f2 self.w[j2, f1] -= self.eta / np.sqrt( self.G[j2, f1]) * g_j2_f1 def train (self, sample_generator, max_echo, max_r2 ): """ 根据一堆样本训练模型 sample_generator: 样本生成器,每次yield (node_list, y),node_list中存储的是x的非0值。通常x要事先做好归一化,即模长为1,这样精度会略微高一点 max_echo: 最大迭代次数 max_r2: 拟合系数r2达到阈值时即可终止学习 """ for itr in range (max_echo): print ("echo: " , itr) y_sum = 0.0 y_sqare_sum = 0.0 err_square_sum = 0.0 population = 0 for node_list, y in sample_generator: y = 0.0 if y == -1 else y self.sgd(node_list, y) y_hat = self.predict(node_list) y_sum += y y_sqare_sum += y ** 2 err_square_sum += (y-y_hat) ** 2 population += 1 var_y = y_sqare_sum - y_sum * y_sum / population r2 = 1 - err_square_sum / var_y print ("r2: " , r2) if r2 > max_r2: print ("r2 have reach" , r2) break def save_model (self, outfile ): ''' 序列化模型 :param outfile: :return: ''' np.save(outfile, self.w) def load_model (self, infile ): ''' 加载模型 :param infile: :return: ''' self.w = np.load(infile)
调用的过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 n = 5 m = 2 k = 2 train_file = "dataset/train.txt" valid_file = "dataset/test.txt" model_file = "ffm.npy" eta = 0.01 lambd = 1e-2 max_echo = 30 max_r2 = 0.9 sample_generator = Sample(train_file) ffm = FFM(m, n, k, eta, lambd) ffm.train(sample_generator, max_echo, max_r2) ffm.save_model(model_file)
逻辑回归模型与GBDT+LR模型
协同过滤和矩阵分解利用用户的物品“相似度”进行推荐 ,逻辑回归模型将问题看成了一个分类问题,通过预测正样本的概率对物品进行排序
这里的正样本可以是用户“点击”了某个商品或者“观看”了某个视频,均是推荐系统希望用户产生“正反馈”行为,因此逻辑回归模型将推荐问题转成成了一个点击率预估问题
逻辑回归LR算法 逻辑回归是在线性回归的基础上加了一个 Sigmoid函数(非线形)映射,使得逻辑回归称为了一个优秀的分类算法,学习逻辑回归模型,首先要记住一句话:逻辑回归假设数据服从伯努利分布,通过极大化似然函数的方法,运用梯度下降来求解参数,来达到将数据二分类的目的。
逻辑回归模型已经将推荐问题转换成了一个点击率预测的问题 ,而点击率预测就是一个典型的二分类, 正好适合逻辑回归进行处理,那么逻辑回归是如何做推荐的呢?
将用户年龄、性别、物品属性、物品描述、当前时间、当前地点等特征转成数值型向量
确定逻辑回归的优化目标,比如把点击率预测转换成二分类问题,这样就可以得到分类问题常用的损失作为目标, 训练模型
在预测的时候, 将特征向量输入模型产生预测,得到用户“点击”物品的概率
利用点击概率对候选物品排序, 得到推荐列表
训练和推断的过程 每个特征的权重参数\(w\) ,我们一般是使用梯度下降的方式, 首先会先随机初始化一批\(w\),然后将特征向量(也就是我们上面数值化出来的特征)输入到模型,就会通过计算会得到模型的预测概率, 然后通过对目标函数求导得到每个\(w\)的梯度, 然后进行更新\(w\)
优点:
LR模型形式简单,可解释性好,从特征的权重可以看到不同的特征对最后结果的影响。
训练时便于并行化,在预测时只需要对特征进行线性加权,所以性能比较好,往往适合处理海量id类特征,用id类特征有一个很重要的好处,就是防止信息损失(相对于范化的CTR 特征),对于头部资源会有更细致的描述
资源占用小,尤其是内存。在实际的工程应用中只需要存储权重比较大的特征及特征对应的权重
方便输出结果调整。逻辑回归可以很方便的得到最后的分类结果,因为输出的是每个样本的概率分数,我们可以很容易的对这些概率分数进行cutoff,也就是划分阈值(大于某个阈值的是一类,小于某个阈值的是一类)
工程化需要, 在深度学习技术之前, 逻辑回归凭借易于并行化,模型简单,训练开销小等特点,占领工程领域的主流,因为即使工程团队发现了复杂模型会提升效果,但一般如果没有把握击败逻辑回归的话仍然不敢尝试或者升级。
当然, 逻辑回归模型也有一定的局限性
表达能力不强, 无法进行特征交叉 ,特征筛选等一系列“高级“操作(这些工作都得人工来干,这样就需要一定的经验, 否则会走一些弯路), 因此可能造成信息的损失 准确率并不是很高。因为这毕竟是一个线性模型加了个sigmoid,形式非常的简单(非常类似线性模型),很难去拟合数据的真实分布 处理非线性数据较麻烦。逻辑回归在不引入其他方法的情况下,只能处理线性可分的数据,如果想处理非线性,首先对连续特征的处理需要先进行离散化(离散化的目的是为了引入非线性),如上文所说,人工分桶的方式会引入多种问题。 LR需要进行人工特征组合,这就需要开发者有非常丰富的领域经验,才能不走弯路。这样的模型迁移起来比较困难,换一个领域又需要重新进行大量的特征工程。 GBDT原理 GBDT全称梯度提升决策树 ,GBDT(Gradient BoostingDecisionTree,梯度提升决策树)是一种机器学习算法,它通过优化损失函数的梯度来构建一组树模型的集合,并希望这些树模型能够协同工作以获得更准确的结果。
GBDT的工作原理:
初始化模型:使用一个简单的树模型作为起点。 训练模型:评估当前模型的损失函数。 根据当前模型的预测值来计算损失函数的梯度。 在当前模型的基础上,寻找能够最小化梯度下降的树模型。 组合模型:将新树模型的预测结果与原模型的预测结果结合,作为下一轮迭代的初始模型。 重复迭代:重复步骤2和3,直到达到预设的迭代次数或模型性能不再提升 GBDT的优点:
灵活性:可以处理非线性问题。 效率:在预测时,每个节点的计算只与少数特征有关,因此计算效率相对较高。 效果:在许多任务中,尤其是回归和分类任务中,GBDT都能取得很好的效果。 GBDT的缺点:
过拟合:由于模型复杂,容易过拟合,需要通过正则化、限制树的数量或深度等方法来控制。 调参复杂:需要调整多个超参数,如学习率、树的数量、节点的最大深度等。 GBDT+LR算法原理 利用GBDT自动进行特征筛选和组合 ,进而生成新的离散特征向量,再把该特征向量当做LR模型的输入 , 来产生最后的预测结果,这就是著名的GBDT+LR模型了。GBDT+LR使用最广泛的场景是CTR点击率预估,即预测当给用户推送的广告会不会被用户点击
训练时 ,GBDT建树的过程相当于自动进行的特征组合和离散化,然后从根结点到叶子节点的这条路径就可以看成是不同特征进行的特征组合,用叶子节点可以唯一的表示这条路径,并作为一个离散特征传入LR 进行二次训练
比如上图中,有两棵树,x为一条输入样本,遍历两棵树后,x样本分别落到两颗树的叶子节点上,每个叶子节点对应LR一维特征,那么通过遍历树,就得到了该样本对应的所有LR特征。构造的新特征向量是取值0/1的。比如左树有三个叶子节点,右树有两个叶子节点,最终的特征即为五维的向量。对于输入x,假设他落在左树第二个节点,编码[0,1,0],落在右树第二个节点则编码[0,1],所以整体的编码为[0,1,0,0,1],这类编码作为特征,输入到线性分类模型(LRor FM)中进行分类。
预测时, 会先走 GBDT的每棵树,得到某个叶子节点对应的一个离散特征(即一组特征组合),然后把该特征以one-hot 形式传入 LR 进行线性加权预测。
GBDT+LR编程实践 这个比赛的任务就是:开发预测广告点击率(CTR)的模型。给定一个用户和他正在访问的页面,预测他点击给定广告的概率是多少?比赛的地址链接:https://www.kaggle.com/c/criteo-display-ad-challenge/overview
导入相关的依赖包和数据集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npimport pandas as pdfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_splitimport lightgbm as lgbfrom sklearn.preprocessing import MinMaxScaler, OneHotEncoder, LabelEncoderfrom sklearn.metrics import log_lossimport gcfrom scipy import sparseimport warnings warnings.filterwarnings('ignore' ) path = 'data/' df_train = pd.read_csv(path + 'train.csv' ) df_test = pd.read_csv(path + 'test.csv' )
数据预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 path = 'data/' df_train = pd.read_csv(path + 'train.csv' ) df_test = pd.read_csv(path + 'test.csv' ) df_train.drop(['Id' ], axis=1 , inplace=True ) df_test.drop(['Id' ], axis=1 , inplace=True ) df_test['Label' ] = -1 data = pd.concat([df_train, df_test]) data.fillna(-1 , inplace=True ) continuous_fea = ['I' +str (i+1 ) for i in range (13 )] category_fea = ['C' +str (i+1 ) for i in range (26 )]
LR模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def lr_model (data, category_fea, continuous_fea ): scaler = MinMaxScaler() for col in continuous_fea: data[col] = scaler.fit_transform(data[col].values.reshape(-1 , 1 )) for col in category_fea: onehot_feats = pd.get_dummies(data[col], prefix=col) data.drop([col], axis=1 , inplace=True ) data = pd.concat([data, onehot_feats], axis=1 ) train = data[data['Label' ] != -1 ] target = train.pop('Label' ) test = data[data['Label' ] == -1 ] test.drop(['Label' ], axis=1 , inplace=True ) x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2 , random_state=2020 ) lr = LogisticRegression() lr.fit(x_train, y_train) tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1 ]) val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1 ]) print ('tr_logloss: ' , tr_logloss) print ('val_logloss: ' , val_logloss) y_pred = lr.predict_proba(test)[:, 1 ] print ('predict: ' , y_pred[:10 ])
1 2 lr_model(data.copy(), category_fea, continuous_fea)
LR预测的结果:
GBDT建模
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 def gbdt_model (data, category_fea, continuous_fea ): for col in category_fea: onehot_feats = pd.get_dummies(data[col], prefix=col) data.drop([col], axis=1 , inplace=True ) data = pd.concat([data, onehot_feats], axis=1 ) train = data[data['Label' ] != -1 ] target = train.pop('Label' ) test = data[data['Label' ] == -1 ] test.drop(['Label' ], axis=1 , inplace=True ) x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2 , random_state=2020 ) gbm = lgb.LGBMClassifier(boosting_type='gbdt' , objective='binary' , subsample=0.8 , min_child_weight=0.5 , colsample_bytree=0.7 , num_leaves=100 , max_depth=12 , learning_rate=0.01 , n_estimators=10000 ) gbm.fit(x_train, y_train, eval_set=[(x_train, y_train), (x_val, y_val)], eval_names=['train' , 'val' ], eval_metric='binary_logloss' , early_stopping_rounds=100 , ) tr_logloss = log_loss(y_train, gbm.predict_proba(x_train)[:, 1 ]) val_logloss = log_loss(y_val, gbm.predict_proba(x_val)[:, 1 ]) print ('tr_logloss: ' , tr_logloss) print ('val_logloss: ' , val_logloss) y_pred = gbm.predict_proba(test)[:, 1 ] print ('predict: ' , y_pred[:10 ])
1 2 gbdt_model(data.copy(), category_fea, continuous_fea)
预测的结果:
LR + GBDT建模
下面就是把上面两个模型进行组合, GBDT负责对各个特征进行交叉和组合,把原始特征向量转换为新的离散型特征向量, 然后在使用逻辑回归模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 def gbdt_lr_model (data, category_feature, continuous_feature ): x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.2 , random_state = 2020 ) gbm = lgb.LGBMClassifier(objective='binary' , subsample= 0.8 , min_child_weight= 0.5 , colsample_bytree= 0.7 , num_leaves=100 , max_depth = 12 , learning_rate=0.01 , n_estimators=1000 , ) gbm.fit(x_train, y_train, eval_set = [(x_train, y_train), (x_val, y_val)], eval_names = ['train' , 'val' ], eval_metric = 'binary_logloss' , early_stopping_rounds = 100 , ) model = gbm.booster_ gbdt_feats_train = model.predict(train, pred_leaf = True ) gbdt_feats_test = model.predict(test, pred_leaf = True ) gbdt_feats_name = ['gbdt_leaf_' + str (i) for i in range (gbdt_feats_train.shape[1 ])] df_train_gbdt_feats = pd.DataFrame(gbdt_feats_train, columns = gbdt_feats_name) df_test_gbdt_feats = pd.DataFrame(gbdt_feats_test, columns = gbdt_feats_name) train = pd.concat([train, df_train_gbdt_feats], axis = 1 ) test = pd.concat([test, df_test_gbdt_feats], axis = 1 ) train_len = train.shape[0 ] data = pd.concat([train, test]) del train del test gc.collect() scaler = MinMaxScaler() for col in continuous_feature: data[col] = scaler.fit_transform(data[col].values.reshape(-1 , 1 )) for col in gbdt_feats_name: onehot_feats = pd.get_dummies(data[col], prefix = col) data.drop([col], axis = 1 , inplace = True ) data = pd.concat([data, onehot_feats], axis = 1 ) train = data[: train_len] test = data[train_len:] del data gc.collect() x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.3 , random_state = 2018 ) lr = LogisticRegression() lr.fit(x_train, y_train) tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1 ]) print ('tr-logloss: ' , tr_logloss) val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1 ]) print ('val-logloss: ' , val_logloss) y_pred = lr.predict_proba(test)[:, 1 ] print (y_pred[:10 ])
深度学习模型(Rank) image-20240308111249239 image-20240311120735173 对于1层cross和1层deep的DCN网络输入经过embedding和stack处理后维度为d,Cross部分网络参数为2d,Deep为d*d,当MLP的层数增多时,deep部分的参数量也急速增加。DCN网路的绝大部分参数都用于对隐性交叉特征进行建模。Cross部分的表达能力反而受限。
DCN-v2优化了cross网络的建模方式,增加了cross网络部分的表达能力;deep部分保持不变。
低维空间的交叉特征建模使得我们可以利用MoE。MoE由两部分组成:experts专家和gating门(一个关于输入x的函数)。我们可以使用多个专家,每个专家学习不同的交叉特征,最后通过gating将各个专家的学习结果整合起来,作为输出。这样就又能进一步增加对交叉特征的建模能力。
]]>
diff --git a/search.xml b/search.xml
index 5b388e0..8e51e67 100644
--- a/search.xml
+++ b/search.xml
@@ -3683,6 +3683,15 @@ src="https://gitee.com/lihaibineric/picgo/raw/master/pic/image-20240308111249239
alt="image-20240308111249239" />
image-20240308111249239
+
+
+image-20240311120735173
+
+对于1层cross和1层deep的DCN网络输入经过embedding和stack处理后维度为d,Cross部分网络参数为2d,Deep为d*d,当MLP的层数增多时,deep部分的参数量也急速增加。DCN网路的绝大部分参数都用于对隐性交叉特征进行建模。Cross部分的表达能力反而受限。
+DCN-v2优化了cross网络的建模方式,增加了cross网络部分的表达能力;deep部分保持不变。
+低维空间的交叉特征建模使得我们可以利用MoE。MoE由两部分组成:experts专家和gating门(一个关于输入x的函数)。我们可以使用多个专家,每个专家学习不同的交叉特征,最后通过gating将各个专家的学习结果整合起来,作为输出。这样就又能进一步增加对交叉特征的建模能力。
]]>
深度学习