「从零入门推荐系统」19:H&M推荐系统代码实战案例

这篇具有很好参考价值的文章主要介绍了「从零入门推荐系统」19:H&M推荐系统代码实战案例。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者 | gongyouliu

编辑 | gongyouliu

我们在上一章中利用Netflix prize数据集讲解了最基础、最简单的一些推荐系统召回、排序算法,大家应该对怎么基于Python实现推荐算法有了一些基本的了解了。接着上一章的思路,本章我们会基于一个更复杂、更近代一点的数据集来实现一些我们在前面章节中讲到的更复杂的一些推荐召回、排序算法。本章我们讲解的算法跟上一章完全不重复,因此是对上一章内容的增补。

本章的讲解逻辑我们采用跟上一章类似的结构。我们会分数据集介绍、数据预处理、推荐算法实现等3个部分来讲解。通过本章的内容讲解,希望读者可以充分熟悉该数据集及怎么基于此实现各种更复杂的召回、排序算法。

19.1 H&M数据集介绍

H&M集团应该大家都知道,它是一家全球连锁的服装品牌,在全球有近5000家线下门店,我相信很多读者曾经也买过他们家的衣服。有了这个熟悉的背景,相信大家可以更好地理解下面我们将要讲解的数据集。

这个数据集是在2022年H&M在kaggle上组织的一次推荐系统竞赛(见参考文献1)。H&M的网上商店为购物者提供了大量可供浏览的产品。但由于选择太多,客户可能无法很快找到他们感兴趣的商品或他们正在寻找的商品,最终他们可能无法购买。为了提升购物体验,产品推荐是关键。所以,基于此背景他们组织了这次竞赛,希望参赛者能够提供一些比较好的推荐算法思路,可以帮助他们提升网上商店的购物体验。

在本次比赛中,H&M集团希望参赛者根据以前交易的数据以及客户和产品元数据制定产品推荐策略。可用的元数据从简单的数据(如服装类型、客户年龄等)到产品描述中的文本数据,再到服装海报中的图像数据。H&M数据集具体数据参考我们的github工程(https://github.com/liuq4360/recommender_systems_abc)中data目录下的hm子目录,包含如下5类数据,下面我们对数据进行详细说明。

  1. images,目录下是商品的图片,不是每个商品都有图片的,子目录的名字是商品id的前3个数字;

  2. articles.csv,商品相关信息,包含如下25个字段:

字段

说明

article_id

物品id,10位数字字符,如 0108775015

product_code

产品code,7位数字字符,如 0108775,是 article_id 的前 7 位

prod_name

产品名,如 Strap top(系带上衣)

product_type_no

产品类型no,2位或者3位数字,有 -1 值

product_type_name

产品类型名。如 Vest top(背心)

product_group_name

产品组名称,如 Garment Upper body(服装上身)

graphical_appearance_no

图案外观no,如 1010016

graphical_appearance_name

图案外观名,如 Solid(固体;立体图形)

colour_group_code

颜色组code,如 09,2位数字

colour_group_name

颜色组名称, 如 Black

perceived_colour_value_id

感知颜色值id, -1,1,2,3,4,5,6,7,一共这几个值。

perceived_colour_value_name

感知颜色值名称,如 Dark(黑暗的),Dusty Light等

perceived_colour_master_id

感知颜色主id,1位或者2位数字

perceived_colour_master_name

感知颜色主名称,如 Beige(浅褐色的)

department_no

部门no,4位数字

department_name

部门名称, 如 Outdoor/Blazers DS

index_code

索引code,单个大写字母,如 A G F D 等

index_name

索引名,如 Lingeries/Tights(内衣/紧身裤)

index_group_no

索引组no,1位或者2位数字

index_group_name

索引组名称, 如 Ladieswear(女装)

section_no

部门no, 2位数字

section_name

部门名称, 如 Womens Everyday Basics(女性日常基础知识)

garment_group_no

服装组no,4位数字

garment_group_name

服装组名称, 如 Jersey Basic(泽西岛基本款)

detail_desc

细节的文本描述, 如 Jersey top with narrow shoulder straps(窄肩带的泽西上衣)

  1. customers.csv,用户相关信息,包含如下7个字段:

字段

说明

customer_id

用户id,字符串,如:00000dbacae5abe5e23885899a1fa44253a17956c6d1c3d25f88aa139fdfc657

FN

35%有值,值为1,65% 缺失

Active

34%值为1, 66% 无值

club_member_status

俱乐部成员状态, 93% ACTIVE,7% PRE-CREATE(预先创建)

fashion_news_frequency

时尚新闻频率, 64% NONE,35% Regularly(有规律地),1% 其它值

age

年龄,有缺失值,1%缺失

postal_code

邮政代码,很长的字符串。例如,52043ee2162cf5aa7ee79974281641c6f11a68d276429a91f8ca0d4b6efa8100

  1. sample_submission.csv,提交预测文件,需要为这里面的每个用户进行推荐预测,包含如下2个字段:

字段

说明

customer_id

用户id,字符串,如:00000dbacae5abe5e23885899a1fa44253a17956c6d1c3d25f88aa139fdfc657

prediction

推荐的预测值,如:0706016001  0706016002 0372860001  0610776002  0759871002  0464297007 0372860002  0610776001  0399223001  0706016003  0720125001  0156231001

这里预测的是物品id,空格隔开

  1. transactions_train.csv,用户行为数据,包含如下5个字段:

字段

说明

t_dat

时间,就是商品的购买时间。如2019-09-18,只精确到日。

customer_id

用户id

article_id

商品id

price

价格

sales_channel_id

销售渠道id,值只有1、2两个,应该是线上、线下两个

上面对H&M数据集提供的5类数据进行了简单介绍,更细节的了解,可以查看原始数据或者对数据进行简单的统计分析(kaggle官网上有数据的基础统计分析,见参考文献1,我们这里就不做分析了)。我们在下面一节对构建推荐算法依赖的数据进行预处理,构建相关特征。

19.2 数据预处理于特征工程

前面一节我们讲解了H&M几个数据集的基本特性和各自的字段描述。为了方便我们后面构建各种召回、排序算法,我们在本节对相关数据进行预处理和相关特征工程的工作。

19.2.1 基于物品信息构建物品特征矩阵

我们在19.3.1.3节会利用kmeans算法进行物品聚类,在这之前我们需要对物品数据(即上面提到的articles.csv)进行处理,构建特征矩阵。具体实现代码如下:

 
 
art = pd.read_csv("../../data/hm/articles.csv")
# customers = pd.read_csv("../../data/hm/customers.csv")
# len(pd.unique(art['article_id'])) 某列唯一值的个数
# trans = pd.read_csv("../../data/hm/transactions_train.csv")
# 类似用户画像部分,我们只关注下面6个类别特征,先将类别特征one-hot编码,然后进行聚类。
art = art[['article_id', 'product_code', 'product_type_no', 'graphical_appearance_no',
           'colour_group_code', 'perceived_colour_value_id', 'perceived_colour_master_id']]
# 'product_code' : 47224 个不同的值。
# 'product_type_no':132 个不同的值。
# 'graphical_appearance_no':30 个不同的值。
# 'colour_group_code':50 个不同的值。
# 'perceived_colour_value_id':8 个不同的值。
# 'perceived_colour_master_id':20 个不同的值。
# product_code:取出现次数最多的前10个,后面的合并。
most_freq_top10_prod_code = np.array(Counter(art.product_code).most_common(10))[:, 0]
# 如果color不是最频繁的10个color,那么就给定一个默认值0,减少one-hot编码的维度
art['product_code'] = art['product_code'].apply(lambda t: t if t in most_freq_top10_prod_code else -1)
# product_type_no:取出现次数最多的前10个,后面的合并。
most_frequent_top10_product_type_no = np.array(Counter(art.product_type_no).most_common(10))[:, 0]
# 如果color不是最频繁的10个color,那么就给定一个默认值0,减少one-hot编码的维度
art['product_type_no'] = art['product_type_no'].apply(
    lambda t: t if t in most_frequent_top10_product_type_no else -1)
one_hot = OneHotEncoder(handle_unknown='ignore')
one_hot_data = art[['product_code', 'product_type_no', 'graphical_appearance_no',
                    'colour_group_code', 'perceived_colour_value_id', 'perceived_colour_master_id']]
one_hot.fit(one_hot_data)
feature_array = one_hot.transform(np.array(one_hot_data)).toarray()
# 两个ndarray水平合并,跟data['id']合并,方便后面两个DataFrame合并
feature_array_add_id = np.hstack((np.asarray([art['article_id'].values]).T, feature_array))
# one_hot_features_df = DataFrame(feature_array, columns=one_hot.get_feature_names())
df_train = DataFrame(feature_array_add_id, columns=np.hstack((np.asarray(['article_id']),
                                                              one_hot.get_feature_names_out())))
df_train['article_id'] = df_train['article_id'].apply(lambda t: int(t))
# df_train = df_train.drop(columns=['article_id'])
# index = 0 写入时不保留索引列。
df_train.to_csv('../../output/hm/kmeans_train.csv', index=0)

通过上面的处理之后,我们可以获得一个130维的特征矩阵,行代表的是每个物品,列是对应特征或者原始特征做one-hot编码后的特征。由于列数比较多,我们这里就不展示出来了。

19.2.2 基于标签构建用户画像

我们先简单描述一下用户画像的构建逻辑,然后再给出对应的代码实现。具体逻辑是:物品是有特征的(比如颜色、品牌等),那么如果用户购买过某物品,该物品对应的特征就可以自动成为该用户的兴趣画像特征了(比如用户买了黑色的衣服,那么黑色就是用户的兴趣特征),我们的用户画像就是这么构建的。

首先,我们先生成物品对应的特征,我们构建一个物品到特征对应的字典,方便后面构建用户画像的时候使用,代码如下:

 
 
# 为每个物品生成对应的特征,这里我们只用到了product_code、product_type_no、graphical_appearance_no、
# colour_group_code、perceived_colour_value_id、perceived_colour_master_id这6个特征。
art = pd.read_csv("../../data/hm/articles.csv")
article_dict = dict()  # {12:{id1,id2,...,id_k}, 34:{id1,id2,...,id_k}}, 这里面每个物品对应的特征权重都一样
for _, row in art.iterrows():
    article_id = row['article_id']
    product_code = row['product_code']
    product_type_no = row['product_type_no']
    graphical_appearance_no = row['graphical_appearance_no']
    colour_group_code = row['colour_group_code']
    perceived_colour_value_id = row['perceived_colour_value_id']
    perceived_colour_master_id = row['perceived_colour_master_id']
    feature_dict = dict()
    feature_dict['product_code'] = product_code
    feature_dict['product_type_no'] = product_type_no
    feature_dict['graphical_appearance_no'] = graphical_appearance_no
    feature_dict['colour_group_code'] = colour_group_code
    feature_dict['perceived_colour_value_id'] = perceived_colour_value_id
    feature_dict['perceived_colour_master_id'] = perceived_colour_master_id
    article_dict[article_id] = feature_dict
# print(article_dict)
np.save("../../output/hm/article_dict.npy", article_dict)

第二步是基于物品相关信息,为每个特征生成对应的倒排索引字典(key是对应的特征,value是具备该特征的所有物品集合),具体代码实现如下:

# 基于物品的特征,为每个特征生成对应的倒排索引,倒排索引可以存到Redis中。
# 需要生成倒排索引的特征包括如下几个:
# product_code, 产品code,7位数字字符,如 0108775,是 article_id 的前 7 位。
# prod_name, 产品名,如 Strap top(系带上衣)
# product_type_no, 产品类型no,2位或者3位数字,有 -1 值。
# product_type_name, 产品类型名。如 Vest top(背心)
# graphical_appearance_no, 图案外观no,如 1010016。
# graphical_appearance_name, 图案外观名,如 Solid(固体;立体图形)
# colour_group_code, 颜色组code,如 09,2位数字
# colour_group_name, 颜色组名称, 如 Black。
# perceived_colour_value_id, 感知颜色值id。-1,1,2,3,4,5,6,7,一共这几个值。
# perceived_colour_value_name, 感知颜色值名称。如 Dark(黑暗的),Dusty Light等
# perceived_colour_master_id, 感知颜色主id。1位或者2位数字。
# perceived_colour_master_name, 感知颜色主名称。如 Beige(浅褐色的)




art = pd.read_csv("../../data/hm/articles.csv")
product_code_unique = np.unique(art[["product_code"]])  # 取某一列的所有唯一值,array([108775, 111565, ..., 959461])
product_type_no_unique = np.unique(art[["product_type_no"]])
graphical_appearance_no_unique = np.unique(art[["graphical_appearance_no"]])
colour_group_code_unique = np.unique(art[["colour_group_code"]])
perceived_colour_value_id_unique = np.unique(art[["perceived_colour_value_id"]])
perceived_colour_master_id_unique = np.unique(art[["perceived_colour_master_id"]])
product_code_portrait_dict = dict()  # {12:{id1,id2,...,id_k}, 34:{id1,id2,...,id_k}}, 这里面每个物品对应的特征权重都一样
product_type_no_portrait_dict = dict()
graphical_appearance_no_portrait_dict = dict()
colour_group_code_portrait_dict = dict()
perceived_colour_value_id_portrait_dict = dict()
perceived_colour_master_id_portrait_dict = dict()
for _, row in art.iterrows():
    article_id = row['article_id']
    product_code = row['product_code']
    product_type_no = row['product_type_no']
    graphical_appearance_no = row['graphical_appearance_no']
    colour_group_code = row['colour_group_code']
    perceived_colour_value_id = row['perceived_colour_value_id']
    perceived_colour_master_id = row['perceived_colour_master_id']
    if product_code in product_code_portrait_dict:
        product_code_portrait_dict[product_code].add(article_id)
    else:
        product_code_portrait_dict[product_code] = set([article_id])
    if product_type_no in product_type_no_portrait_dict:
        product_type_no_portrait_dict[product_type_no].add(article_id)
    else:
        product_type_no_portrait_dict[product_type_no] = set([article_id])
    if graphical_appearance_no in graphical_appearance_no_portrait_dict:
        graphical_appearance_no_portrait_dict[graphical_appearance_no].add(article_id)
    else:
        graphical_appearance_no_portrait_dict[graphical_appearance_no] = set([article_id])
    if colour_group_code in colour_group_code_portrait_dict:
        colour_group_code_portrait_dict[colour_group_code].add(article_id)
    else:
        colour_group_code_portrait_dict[colour_group_code] = set([article_id])
    if perceived_colour_value_id in perceived_colour_value_id_portrait_dict:
        perceived_colour_value_id_portrait_dict[perceived_colour_value_id].add(article_id)
    else:
        perceived_colour_value_id_portrait_dict[perceived_colour_value_id] = set([article_id])
    if perceived_colour_master_id in perceived_colour_master_id_portrait_dict:
        perceived_colour_master_id_portrait_dict[perceived_colour_master_id].add(article_id)
    else:
        perceived_colour_master_id_portrait_dict[perceived_colour_master_id] = set([article_id])
# print(product_code_portrait_dict)
# print(product_type_no_portrait_dict)
# print(graphical_appearance_no_portrait_dict)
# print(colour_group_code_portrait_dict)
# print(perceived_colour_value_id_portrait_dict)
# print(perceived_colour_master_id_portrait_dict)
np.save("../../output/hm/product_code_portrait_dict.npy", product_code_portrait_dict)
np.save("../../output/hm/product_type_no_portrait_dict.npy", product_type_no_portrait_dict)
np.save("../../output/hm/graphical_appearance_no_portrait_dict.npy", graphical_appearance_no_portrait_dict)
np.save("../../output/hm/colour_group_code_portrait_dict.npy", colour_group_code_portrait_dict)
np.save("../../output/hm/perceived_colour_value_id_portrait_dict.npy", perceived_colour_value_id_portrait_dict)
np.save("../../output/hm/perceived_colour_master_id_portrait_dict.npy", perceived_colour_master_id_portrait_dict)

有了上面2步的准备工作,就可以基于用户的行为数据(即19.1节中的transactions_train.csv)构建用户的兴趣画像,具体的代码实现如下:

 
 
# 基于用户行为数据,为每个用户生成用户画像。
trans = pd.read_csv("../../data/hm/transactions_train.csv")
user_portrait = dict()
article_dict = np.load("../../output/hm/article_dict.npy", allow_pickle=True).item()
for _, row in trans.iterrows():
    customer_id = row['customer_id']
    article_id = row['article_id']
    feature_dict = article_dict[article_id]
    # article_dict[957375001]
    # {'product_code': 957375, 'product_type_no': 72,
    # 'graphical_appearance_no': 1010016, 'colour_group_code': 9,
    # 'perceived_colour_value_id': 4, 'perceived_colour_master_id': 5}
    product_code = feature_dict['product_code']
    product_type_no = feature_dict['product_type_no']
    graphical_appearance_no = feature_dict['graphical_appearance_no']
    colour_group_code = feature_dict['colour_group_code']
    perceived_colour_value_id = feature_dict['perceived_colour_value_id']
    perceived_colour_master_id = feature_dict['perceived_colour_master_id']
    if customer_id in user_portrait:
        portrait_dict = user_portrait[customer_id]
        # { 'product_code': set([108775, 116379])
        #   'product_type_no': set([253, 302, 304, 306])
        #   'graphical_appearance_no': set([1010016, 1010017])
        #   'colour_group_code': set([9, 11, 13])
        #   'perceived_colour_value_id': set([1, 3, 4, 2])
        #   'perceived_colour_master_id': set([11, 5 ,9])
        #   }
        if 'product_code' in portrait_dict:
            portrait_dict['product_code'].add(product_code)
        else:
            portrait_dict['product_code'] = set([product_code])
        if 'product_type_no' in portrait_dict:
            portrait_dict['product_type_no'].add(product_type_no)
        else:
            portrait_dict['product_type_no'] = set([product_type_no])
        if 'graphical_appearance_no' in portrait_dict:
            portrait_dict['graphical_appearance_no'].add(graphical_appearance_no)
        else:
            portrait_dict['graphical_appearance_no'] = set([graphical_appearance_no])
        if 'colour_group_code' in portrait_dict:
            portrait_dict['colour_group_code'].add(colour_group_code)
        else:
            portrait_dict['colour_group_code'] = set([colour_group_code])
        if 'perceived_colour_value_id' in portrait_dict:
            portrait_dict['perceived_colour_value_id'].add(perceived_colour_value_id)
        else:
            portrait_dict['perceived_colour_value_id'] = set([perceived_colour_value_id])
        if 'perceived_colour_master_id' in portrait_dict:
            portrait_dict['perceived_colour_master_id'].add(perceived_colour_master_id)
        else:
            portrait_dict['perceived_colour_master_id'] = set([perceived_colour_master_id])
        user_portrait[customer_id] = portrait_dict
    else:
        portrait_dict = dict()
        portrait_dict['product_code'] = set([product_code])
        portrait_dict['product_type_no'] = set([product_type_no])
        portrait_dict['graphical_appearance_no'] = set([graphical_appearance_no])
        portrait_dict['colour_group_code'] = set([colour_group_code])
        portrait_dict['perceived_colour_value_id'] = set([perceived_colour_value_id])
        portrait_dict['perceived_colour_master_id'] = set([perceived_colour_master_id])
        user_portrait[customer_id] = portrait_dict
np.save("../../output/hm/user_portrait.npy", user_portrait)

上面代码中我们将物品特征字典、特征倒排索引字典、用户画像存到了本地磁盘中了,在实际使用过程中,更优的做法是存到Redis等NoSQL中,这样有更高的稳定性,读取性能也会更好。在后续的代码优化中,我们会采用Redis来存储。

19.2.3 构建推荐算法的特征矩阵

后面我们的logistics回归、FM、GBDT、wide&deep等排序算法都需要构建模型的特征向量,方便模型进行训练。本节的目的就是构建这些特征向量,这里我们只讲解logistics回归的特征构建,其它算法的特征可以复用logistics回归的,或者可以基于logistics的实现进行简单调整,这里不再赘述。

logistics回归中的特征有数值特征,也有离散特征经过one-hot进行编码后形成的特征,具体的数据预处理及特征工程实现的代码如下:

r"""
利用scikit-learn 中的 logistics回归 算法来进行召回。模型的主要特征有如下3类:
1、用户相关特征:基于customers.csv表格中的数据。下面6个字段都作为特征。
    FN,   35%有值,值为1;65% 缺失。
    Active,  34% 1, 66% 无值。
    club_member_status, 俱乐部成员状态, 93% ACTIVE,7% PRE-CREATE(预先创建)。
    fashion_news_frequency, 时尚新闻频率, 64% NONE,35% Regularly(有规律地),1% 其它值。
    age, 年龄,有缺失值,1%缺失。
    postal_code, 邮政代码,很长的字符串。例如,52043ee2162cf5aa7ee79974281641c6f11a68d276429a91f8ca0d4b6efa8100。
2、物品相关特征:基于articles.csv表格中的数据。下面6个字段作为特征,还有很多字段没用到,也可能能用,读者可以自己探索。
    product_code, 产品code,7位数字字符,如 0108775,是 article_id 的前 7 位。
    product_type_no, 产品类型no,2位或者3位数字,有 -1 值。
    graphical_appearance_no, 图案外观no,如 1010016。
    colour_group_code, 颜色组code,如 09,2位数字
    perceived_colour_value_id, 感知颜色值id。-1,1,2,3,4,5,6,7,一共这几个值。
    perceived_colour_master_id, 感知颜色主id。1位或者2位数字。
3、用户行为相关特征:基于transactions_train.csv数据。下面2个特征需要使用。
    t_dat,时间,就是商品的购买时间。如2019-09-18,只精确到日。
    price, 价格
    sales_channel_id, 销售渠道id,值只有1、2两个,估计是线上、线下两个。
   另外再准备几个用户行为统计特征,具体如下:
    用户购买频次:总购买次数/用户最近购买和最远购买之间的星期数。
    用户客单价:该用户所有购买的平均价格。
"""




art = pd.read_csv("../../data/hm/articles.csv")
cust = pd.read_csv("../../data/hm/customers.csv")
cust.loc[:, ['FN']] = cust.loc[:, ['FN']].fillna(0)
cust.loc[:, ['Active']] = cust.loc[:, ['Active']].fillna(0)
cust.loc[:, ['club_member_status']] = cust.loc[:, ['club_member_status']].fillna('other')
cust.loc[:, ['fashion_news_frequency']] = cust.loc[:, ['fashion_news_frequency']].fillna('other')
cust.loc[:, ['age']] = cust.loc[:, ['age']].fillna(int(cust['age'].mode()[0]))
cust['age'] = cust['age']/100.0
# len(pd.unique(art['article_id']))  # 某列唯一值的个数
trans = pd.read_csv("../../data/hm/transactions_train.csv")  # 都是正样本。
# 到目前为止经历的年数。
trans['label'] = 1
positive_num = trans.shape[0]
# 数据中没有负样本,还需要人工构建一些负样本。
# 负样本中的price,用目前正样本的price的平均值。
price = trans[['article_id', 'price']].groupby('article_id').mean()
price_dict = price.to_dict()['price']
# 负样本的sales_channel_id用正样本的中位数。
channel = trans[['article_id', 'sales_channel_id']].groupby('article_id').median()
channel['sales_channel_id'] = channel['sales_channel_id'].apply(lambda x: int(x))
channel_dict = channel.to_dict()['sales_channel_id']
t = trans['t_dat']
date = t.mode()[0]  # 用众数来表示负样本的时间。'2019-09-28'
# 采用将正样本的customer_id、article_id两列随机打散的思路(这样customer_id和article_id就可以
# 随机组合了)来构建负样本。
cust_id = shuffle(trans['customer_id']).to_list()
art_id = shuffle(trans['article_id']).to_list()
data = {'customer_id': cust_id, 'article_id': art_id}
negative_df = pd.DataFrame(data, index=list(range(positive_num, 2*positive_num, 1)))
negative_df['t_dat'] = date
negative_df['price'] = negative_df['article_id'].apply(lambda i: price_dict[i])
negative_df['sales_channel_id'] = negative_df['article_id'].apply(lambda i: channel_dict[i])
# 调整列的顺序,跟正样本保持一致
negative_df = negative_df[['t_dat', 'customer_id', 'article_id', 'price', 'sales_channel_id']]
negative_df['label'] = 0
df = pd.concat([trans, negative_df], ignore_index=True)  # 重新进行索引
df['t_dat'] = pd.to_datetime(df['t_dat']).rsub(pd.Timestamp('now').floor('d')).dt.days/365.0
df = shuffle(df)
df.reset_index(drop=True, inplace=True)
df = df.merge(cust, on=['customer_id'],
              how='left').merge(art, on=['article_id'], how='left')
df = df[['customer_id', 'article_id', 't_dat', 'price', 'sales_channel_id',
         'product_code', 'product_type_no', 'graphical_appearance_no', 'colour_group_code',
         'perceived_colour_value_id', 'perceived_colour_master_id', 'FN',
         'Active', 'club_member_status', 'fashion_news_frequency', 'age', 'postal_code', 'label']]
# product_code:取出现次数最多的前10个,后面的合并。
most_frequent_top10_product_code = np.array(Counter(df.product_code).most_common(10))[:, 0]
# 如果color不是最频繁的10个color,那么就给定一个默认值0,减少one-hot编码的维度
df['product_code'] = df['product_code'].apply(lambda x: x if x in most_frequent_top10_product_code else -1)
# product_type_no:取出现次数最多的前10个,后面的合并。
most_frequent_top10_product_type_no = np.array(Counter(df.product_type_no).most_common(10))[:, 0]
# 如果color不是最频繁的10个color,那么就给定一个默认值0,减少one-hot编码的维度
df['product_type_no'] = df['product_type_no'].apply(lambda x: x if x in most_frequent_top10_product_type_no else -1)
# postal_code:取出现次数最多的前10个,后面的合并。
most_frequent_top100_postal_code = np.array(Counter(df.postal_code).most_common(100))[:, 0]
# 如果color不是最频繁的10个color,那么就给定一个默认值0,减少one-hot编码的维度
df['postal_code'] = df['postal_code'].apply(lambda x: x if x in most_frequent_top100_postal_code else "other")
df.to_csv('../../output/hm/logistic_source_data.csv', index=0)
# df = pd.read_csv("../../output/hm/logistic_source_data.csv")
one_hot = OneHotEncoder(handle_unknown='ignore')
one_hot_data = df[['sales_channel_id', 'product_code', 'product_type_no', 'graphical_appearance_no',
                   'colour_group_code', 'perceived_colour_value_id', 'perceived_colour_master_id',
                   'FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'postal_code']]
one_hot.fit(one_hot_data)
feature_array = one_hot.transform(np.array(one_hot_data)).toarray()
# 两个ndarray水平合并,跟data['id']合并,方便后面两个DataFrame合并
feature_array_add_id = np.hstack((np.asarray([df['customer_id'].values]).T,
                                  np.asarray([df['article_id'].values]).T, feature_array))
one_hot_df = DataFrame(feature_array_add_id,
                       columns=np.hstack((np.asarray(['customer_id']),
                                          np.asarray(['article_id']),
                                          one_hot.get_feature_names_out())))
one_hot_df['customer_id'] = one_hot_df['customer_id'].apply(lambda x: int(x))
one_hot_df['article_id'] = one_hot_df['article_id'].apply(lambda x: int(x))
# 三类特征合并。
final_df = df.merge(one_hot_df, on=['customer_id', 'article_id'], how='left')
final_df = final_df.drop(columns=['sales_channel_id', 'product_code', 'product_type_no', 'graphical_appearance_no',
                                  'colour_group_code', 'perceived_colour_value_id', 'perceived_colour_master_id',
                                  'FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'postal_code'])
# index = 0 写入时不保留索引列。
final_df.to_csv('../../output/hm/logistic_model_data.csv', index=0)
# read
# data_and_features_df = pd.read_csv(data_path + '/' + r'logistic_model_data.csv')
# 将数据集划分为训练集logistic_train_df和测试集logistic_test_df。训练集logistic_train_df
# 用于logistic回归模型的训练,而测试集logistic_test_df用于测试训练好的logistic回归模型的效果。
logistic_train_df, logistic_test_df = train_test_split(final_df,
                                                       test_size=0.3, random_state=42)
logistic_train_df.to_csv('../../output/hm/logistic_train_data.csv', index=0)
logistic_test_df.to_csv('../../output/hm/logistic_test_data.csv', index=0)

19.3 推荐系统算法实现

讲完了数据预处理与特征工程,下面我们基于前面介绍的H&M数据集实现我们在第6-13章节实现的各种更复杂的召回、排序算法。

19.3.1 召回算法

H&M数据集我们一共实现了5类召回算法,分别是基于标签的物品关联物品召回、基于item2vec的物品关联物品召回、基于聚类的个性化召回、基于用户最新的兴趣物品的召回和基于标签的用户画像召回,下面我们分别介绍。

19.3.1.1 基于标签的物品关联召回

这个召回算法非常简单,本节的代码实现对应我们git仓库recall/hm目录下的item_tags_jaccard_similar.py。我们利用物品的标签,基于 jaccard 相似性计算物品相似度,为每个物品计算最相似的N个物品。这个算法的算法原理我们在第7章的7.2.1节中已经做了介绍,不过我们的实现方式跟7.2.1节的不一样,本节我们的实现方式更简单、更直观。

计算jaccard相似度,就是看两个集合中重复元素的个数除以两个集合一共有多少元素,具体代码如下:

 
 
def jaccard_similarity(set_1, set_2):
    """
    计算两个集合的jaccard相似度。jaccard_similarity = || set_1 & set_2 || / || set_1 | set_2 ||
    :param set_1: 集合1
    :param set_2: 集合2
    :return: 相似度
    """
    return len(set_1 & set_2)*1.0/len(set_1 | set_2)

对于H&M数据集,我们的物品有很多字段,我们只利用部分字段(下面代码中会提到)作为特征。那么任何一个字段就可以利用上面的jaccard相似度计算两个物品在该字段的相似度,所有字段的相似度加起来就是这两个物品的相似度了,具体代码实现如下:

def article_jaccard_similarity(article_1, article_2):
    """
    计算两个article的jaccard相似性。
    :param article_1: 物品1的metadata,数据结构是一个dict,基于articles.csv的行构建的。
    :param article_2: 物品2的metadata,数据结构是一个dict,基于articles.csv的行构建的。
    :return: sim,返回 article_1 和 article_2 的相似性。
    """
    sim = 0.0
    for key in article_1.keys():
        sim = sim + jaccard_similarity(set(article_1[key]), set(article_2[key]))
    return sim/len(article_1)

通过上面的讲解,我们能够计算任意两个物品的相似度了。那么,针对所有物品,我们可以利用两个嵌套循环(我们的实现比较简单,可能运行比较慢,读者可以用Spark进行分布式实现,实现方案也非常简单,7.2.1节有实现的原理介绍,我们这里就不提供Spark版本的分布式实现代码了)就可以实现为每个物品计算最相似的topN的物品了。具体代码实现如下:

 
 
art = pd.read_csv("../../data/hm/articles.csv")
# art = art[['prod_name', 'product_type_name', 'product_group_name', 'graphical_appearance_name',
# 'colour_group_name', 'perceived_colour_value_name', 'perceived_colour_master_name', 'department_name',
# 'index_name', 'index_group_name', 'section_name', 'garment_group_name']]
# art_1 = dict(art.loc[0])  # 取第一行
# art_2 = dict(art.loc[3])  # 取第二行
# print(article_jaccard_similarity(art_1, art_2))
jaccard_sim_rec_map = dict()
rec_num = 30
articles = art.iloc[:, 0].drop_duplicates().to_list()  # 取第一列的值,然后去重,转为list
for a in articles:
    row_a = art[art['article_id'] == a]  # 取 art中 'article_id' 列值为 a 的行
    tmp_a = row_a[['prod_name', 'product_type_name', 'product_group_name', 'graphical_appearance_name',
                   'colour_group_name', 'perceived_colour_value_name', 'perceived_colour_master_name',
                   'department_name', 'index_name', 'index_group_name', 'section_name', 'garment_group_name']]
    art_a = dict(tmp_a.loc[tmp_a.index[0]])
    sim_dict = dict()
    for b in articles:
        if a != b:
            row_b = art[art['article_id'] == b]
            tmp_b = row_b[['prod_name', 'product_type_name', 'product_group_name', 'graphical_appearance_name',
                           'colour_group_name', 'perceived_colour_value_name', 'perceived_colour_master_name',
                           'department_name', 'index_name', 'index_group_name', 'section_name', 'garment_group_name'
                           ]]
            art_b = dict(tmp_b.loc[tmp_b.index[0]])
            sim_ = article_jaccard_similarity(art_a, art_b)
            sim_dict[b] = sim_
    sorted_list = sorted(sim_dict.items(), key=lambda item: item[1], reverse=True)
    res = sorted_list[:rec_num]
    jaccard_sim_rec_map[a] = res
jaccard_sim_rec_path = "../../output/netflix_prize/jaccard_sim_rec.npy"
np.save(jaccard_sim_rec_path, jaccard_sim_rec_map)

19.3.1.2 基于item2vec的物品关联召回

这个算法实现的功能跟上面的类似(对应我们github代码仓库recall/hm目录下的item2vec.py),主要是获取物品关联物品的召回,这个算法的原理我们在第9章的9.1节中已经做过介绍。本节的代码我们是通过利用gensim框架(见参考文献2、3)来实现item嵌入(采用item2vec算法实现)的,然后利用向量相似来计算最相关的物品。

由于使用了第三方框架,代码实现起来相对容易,需要读者熟悉相关的类和方法,具体代码参考下面的代码块。这里我们不对各种类和方法进行解释了,工作留给读者去熟悉。顺便说一句,gensim框架是一个非常不错的框架,有很多好的工具可以使用,读者可以多了解。item2vec嵌入效果也相当不错,作者之前就用了该方法来做过视频的关联推荐。

 
 
trans = pd.read_csv("../../data/hm/transactions_train.csv")
tmp_df = trans[['customer_id', 'article_id']]
grouped_df = tmp_df.groupby('customer_id')
groups = grouped_df.groups
train_data = []
for customer_id in groups.keys():
    customer_df = grouped_df.get_group(customer_id)
    tmp_lines = list(customer_df['article_id'].values)
    lines = []
    for word in tmp_lines:
        lines.append(str(word))
    train_data.append(lines)
model = Word2Vec(sentences=train_data, vector_size=100, window=5, min_count=3, workers=4)
model.save("../../output/hm/word2vec.model")
# model = Word2Vec.load("../../output/hm/word2vec.model")
# vector = model.wv['computer']  # get numpy vector of a word
sims = model.wv.most_similar('657395002', topn=10)  # get other similar words
print(sims)

19.3.1.3 基于聚类的物品关联召回

这个召回算法我们利用scikit-learn中的kmeans算法对物品进行聚类(只利用了物品本身的特征信息),然后利用物品所在的类作为该物品的关联召回推荐,这个算法的原理我们在第8章的8.2节已经介绍过。具体的代码实现参考下面的代码块(对应我们github代码仓库recall/hm目录下的k_means.py):

 
 
n_clusters = 1000
# X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
# k_means = KMeans(n_clusters=2, random_state=0).fit(X)
# n_clusters: 一共聚多少类,默认值8
# init:选择中心点的初始化方法,默认值k-means++
# n_init:算法基于不同的中心点运行多少次,最后的结果基于最好的一次迭代的结果,默认值10
# max_iter: 最大迭代次数,默认值300
k_means = KMeans(init='k-means++', n_clusters=n_clusters, n_init=10,
                 max_iter=300).fit(df_train.drop(columns=['article_id']).values)
# 训练样本中每条记录所属的类别
print(k_means.labels_)
# 预测某个样本属于哪个聚类
# print(k_means.predict(np.random.rand(1, df_train.shape[1])))
print(k_means.predict(np.random.randint(20, size=(2, df_train.drop(columns=['article_id']).shape[1]))))
# 每个聚类的聚类中心
print(k_means.cluster_centers_)
result_array = np.hstack((np.asarray([df_train['article_id'].values]).T,
                          np.asarray([k_means.labels_]).T))
# 将物品id和具体的类别转化为DataFrame。
cluster_result = DataFrame(result_array, columns=['article_id', 'cluster'])
# index = 0 写入时不保留索引列。
cluster_result.to_csv('../../output/hm/kmeans.csv', index=0)
# read
# cluster_result = pd.read_csv('../../output/hm/kmeans.csv')
# 给用户推荐的物品数量的数量
rec_num = 10
df_cluster = pd.read_csv('../../output/hm/kmeans.csv')
# 每个id对应的cluster的映射字典。
id_cluster_dict = dict(df_cluster.values)
tmp = df_cluster.values
cluster_ids_dict = {}
for i in range(tmp.shape[0]):
    [id_, cluster_] = tmp[i]
    if cluster_ in cluster_ids_dict.keys():
        cluster_ids_dict[cluster_] = cluster_ids_dict[cluster_] + [id_]
    else:
        cluster_ids_dict[cluster_] = [id_]
# 一共有多少个类
# cluster_num = len(cluster_ids_dict)
# 打印出每一个类有多少个元素,即每类有多少物品
for x, y in cluster_ids_dict.items():
    print("cluster " + str(x) + " : " + str(len(y)))
# source_df = pd.read_csv("../../data/hm/articles.csv")
# 基于聚类,为每个物品关联k个最相似的物品。
def article_similar_recall(art_id, k):
    rec = cluster_ids_dict.get(id_cluster_dict.get(art_id))
    if art_id in rec:
        rec.remove(art_id)
    return random.sample(rec, k)
article_id = 952937003
topn_sim = article_similar_recall(article_id, rec_num)

当然,我们也可以基于上面一小节提到的item2vec获得物品的嵌入向量,然后利用该向量进行kmeans聚类。从经验上来说,基于item2vec再kmeans聚类的效果应该会更好一些。

19.3.1.4 基于用户兴趣的种子物品召回

这个召回算法是利用用户最近有兴趣的几个物品(比如抖音上用户最近看完的短视频)作为种子,利用每个种子的相似关联物品作为待召回的物品,将所有种子进行这样关联,所有的待召回的物品的集合的并集就是最终的召回物品。这个算法的原理我们在第7章的7.2.2.1节已经做过介绍,具体代码参考如下(对应我们github代码仓库recall/hm目录下的seed_items_tags_jaccard.py):

 
 
def seeds_recall(seeds, rec_num):
    """
    基于用户喜欢的种子物品,为用户召回关联物品。
    :param seeds: list,用户种子物品 ~ [item1,item2, ..., item_i]
    :param rec_num: 最终召回的物品数量
    :return: list ~ [(item1,score1),(item2,score2), ..., (item_k,score_k)]
    """
    jaccard_sim_rec_path = "../../output/netflix_prize/jaccard_sim_rec.npy"
    sim = np.load(jaccard_sim_rec_path, allow_pickle=True).item()
    recalls = []
    for seed in seeds:
        recalls.extend(sim[seed])
    # 可能不同召回的物品有重叠,那么针对重叠的,可以将score累加,然后根据score降序排列。
    tmp_dict = dict()
    for (i, s) in recalls:
        if i in tmp_dict:
            tmp_dict[i] = tmp_dict[i] + s
        else:
            tmp_dict[i] = s
    rec = sorted(tmp_dict.items(), key=lambda item: item[1], reverse=True)
    return rec[0:rec_num]

19.3.1.5 基于标签的用户画像召回

这个召回算法需要先基于用户行为构建用户的兴趣画像(这个我们在19.2.2节中已经讲解过,这里不赘述),然后基于用户兴趣画像,将与用户兴趣相关的物品作为召回物品集。这个算法的原理我们在第7章的7.2.2.2节中已经讲过,具体代码实现参考下面的代码块(对应我们github代码仓库recall/hm目录下的tags_user_portrait.py):

 
 
rec_num = 30
user_portrait = np.load("../../output/hm/user_portrait.npy", allow_pickle=True).item()
product_code_portrait_dict = np.load("../../output/hm/product_code_portrait_dict.npy", allow_pickle=True).item()
product_type_no_portrait_dict = np.load("../../output/hm/product_type_no_portrait_dict.npy", 
                                        allow_pickle=True).item()
graphical_appearance_no_portrait_dict = np.load("../../output/hm/graphical_appearance_no_portrait_dict.npy", 
                                                allow_pickle=True).item()
colour_group_code_portrait_dict = np.load("../../output/hm/colour_group_code_portrait_dict.npy", 
                                          allow_pickle=True).item()
perceived_colour_value_id_portrait_dict = np.load("../../output/hm/perceived_colour_value_id_portrait_dict.npy", 
                                                  allow_pickle=True).item()
perceived_colour_master_id_portrait_dict = np.load("../../output/hm/perceived_colour_master_id_portrait_dict.npy", 
                                                   allow_pickle=True).item()
# {12:{id1,id2,...,id_k}, 34:{id1,id2,...,id_k}}, 这里面每个物品对应的特征权重都一样
customer_rec = dict()
for customer in user_portrait.keys():
    portrait_dict = user_portrait[customer]
    # { 'product_code': set([108775, 116379])
    #   'product_type_no': set([253, 302, 304, 306])
    #   'graphical_appearance_no': set([1010016, 1010017])
    #   'colour_group_code': set([9, 11, 13])
    #   'perceived_colour_value_id': set([1, 3, 4, 2])
    #   'perceived_colour_master_id': set([11, 5 ,9])
    #   }
    product_code_rec = set()
    product_type_no_rec = set()
    graphical_appearance_no_rec = set()
    colour_group_code_rec = set()
    perceived_colour_value_id_rec = set()
    perceived_colour_master_id_rec = set()
    rec = []
    # 针对6类特征画像类型,用户在某个类型中都可能有兴趣点,针对每个兴趣点获得对应的物品id,将同一个画像类型
    # 中所有的兴趣点的物品推荐聚合到一起,最后对该兴趣画像类型,只取 rec_num 个推荐。
    # 最后,对6个兴趣画像类型的推荐,最终合并在一起,只取 rec_num 个作为最终的推荐。
    if 'product_code' in portrait_dict:
        product_code_set = portrait_dict['product_code']
        for product_code in product_code_set:
            product_code_rec = product_code_rec | product_code_portrait_dict[product_code]
        rec = rec.append(random.sample(product_code_rec, rec_num))
    if 'product_type_no' in portrait_dict:
        product_type_no_set = portrait_dict['product_type_no']
        for product_type_no in product_type_no_set:
            product_type_no_rec = product_type_no_rec | product_type_no_portrait_dict[product_type_no]
        rec = rec.append(random.sample(product_type_no_rec, rec_num))
    if 'graphical_appearance_no' in portrait_dict:
        graphical_appearance_no_set = portrait_dict['graphical_appearance_no']
        for graphical_appearance_no in graphical_appearance_no_set:
            graphical_appearance_no_rec = graphical_appearance_no_rec | graphical_appearance_no_portrait_dict[graphical_appearance_no]
        rec = rec.append(random.sample(graphical_appearance_no_rec, rec_num))
    if 'colour_group_code' in portrait_dict:
        colour_group_code_set = portrait_dict['colour_group_code']
        for colour_group_code in colour_group_code_set:
            colour_group_code_rec = colour_group_code_rec | colour_group_code_portrait_dict[colour_group_code]
        rec = rec.append(random.sample(colour_group_code_rec, rec_num))
    if 'perceived_colour_value_id' in portrait_dict:
        perceived_colour_value_id_set = portrait_dict['perceived_colour_value_id']
        for perceived_colour_value_id in perceived_colour_value_id_set:
            perceived_colour_value_id_rec = perceived_colour_value_id_rec | perceived_colour_value_id_portrait_dict[perceived_colour_value_id]
        rec = rec.append(random.sample(perceived_colour_value_id_rec, rec_num))
    if 'perceived_colour_master_id' in portrait_dict:
        perceived_colour_master_id_set = portrait_dict['perceived_colour_master_id']
        for perceived_colour_master_id in perceived_colour_master_id_set:
            perceived_colour_master_id_rec = perceived_colour_master_id_rec | perceived_colour_master_id_portrait_dict[perceived_colour_master_id]
        rec = rec.append(random.sample(perceived_colour_master_id_rec, rec_num))
    rec = random.sample(rec, rec_num)
    customer_rec[customer] = rec
np.save("../../output/hm/customer_rec.npy", customer_rec)

基于用户画像的召回是一种工程实现上非常简单的、并且效果也不错的召回算法,在工业界上是非常实用的方法。这个算法的最大特点是可解释性强,适合用于基于用户画像的各种运营策略中。该算法也可以非常方便地跟公司的用户画像体系打通。

19.3.2 排序算法

前面一小节我们讲解完了5种基于H&M数据集的召回算法,本节我们讲解5个排序算法,即logistics回归排序、FM排序、GBDT排序、匹配用户兴趣画像的排序和wide&deep排序。本节的排序算法比上一章的排序算法更加重要,也比上一章的要复杂,读者需要很好地理解和掌握。

19.3.2.1 基于logistics回归排序

logistics回归是最基础、最简单的一类线性模型,我们可以利用它来进行2元分类模型(即预测用户是否点击,1代表点击,0代表未点击)的构建(在第12章12.1节我们对logistics进行排序的原理已经做过详细介绍)。本节我们利用scikit-learn中的logistics回归函数来实现logistics回归排序。特征工程的工作我们在19.2.3节中已经讲解过,本节我们只讲解排序代码实现,具体的代码实现如下(对应我们github代码仓库ranking/hm目录下的logistics_regression.py):

"""
该脚本主要完成3件事情:
1. 训练logistic回归模型;
2. 针对测试集进行预测;
3. 评估训练好的模型在测试集上的效果;
这个脚本中的所有操作都可以借助scikit-learn中的函数来实现,非常简单。
这里为了简单起见,将模型训练、预测与评估都放在这个文件中了。
关于logistic回归模型各个参数的含义及例子可以参考,https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression
关于模型评估的案例可以参考:https://scikit-learn.org/stable/auto_examples/miscellaneous/plot_display_object_visualization.html#sphx-glr-auto-examples-miscellaneous-plot-display-object-visualization-py
"""
logistic_train_df = pd.read_csv('../../output/hm/logistic_train_data.csv')
"""
下面代码是训练logistic回归模型。
"""
clf = LogisticRegression(penalty='l2',
                         solver='liblinear', tol=1e-6, max_iter=1000)
X_train = logistic_train_df.drop(columns=['customer_id', 'article_id', 'label', ])
y_train = logistic_train_df['label']
clf.fit(X_train, y_train)
# clf.coef_
# clf.intercept_
# clf.classes_
"""
下面的代码用上面训练好的logistic回归模型来对测试集进行预测。
"""
logistic_test_df = pd.read_csv('../../output/hm/logistic_test_data.csv')
X_test = logistic_test_df.drop(columns=['customer_id', 'article_id', 'label', ])
y_test = logistic_test_df['label']
# logistic回归模型预测出的结果为y_score
y_score = clf.predict(X_test)
# 包含概率值的预测
# y_score = clf.predict_proba(X_test)
# np.unique(Z)
# Counter(Z).most_common(2)
# logistic_test_df.label.value_counts()
"""
下面的代码对logistic回归模型进行效果评估,主要有3种常用的评估方法:
1. 混淆矩阵:confusion matrix
2. roc曲线:roc curve
3. 精准度和召回率:precision recall
"""
# confusion matrix
# 混淆矩阵参考百度词条介绍:https://baike.baidu.com/item/%E6%B7%B7%E6%B7%86%E7%9F%A9%E9%98%B5/10087822?fr=aladdin
y_score = clf.predict(X_test)
cm = confusion_matrix(y_test, y_score)
cm_display = ConfusionMatrixDisplay(cm).plot()
# roc curve
# ROC 和 AUC 的介绍见:
# 1. https://baijiahao.baidu.com/s?id=1671508719185457407&wfr=spider&for=pc
# 2. https://blog.csdn.net/yinyu19950811/article/details/81288287
fpr, tpr, _ = roc_curve(y_test, y_score, pos_label=clf.classes_[1])
roc_display = RocCurveDisplay(fpr=fpr, tpr=tpr).plot()
# precision recall
# 准确率和召回率的介绍参考:
# 1. https://www.zhihu.com/question/19645541/answer/91694636
pre, recall, _ = precision_recall_curve(y_test, y_score, pos_label=clf.classes_[1])
pr_display = PrecisionRecallDisplay(precision=pre, recall=recall).plot()

19.3.2.2 基于FM排序

本节讲解的FM算法的基本原理我们在第12章的12.2节中介绍过,这里不赘述。这里代码实现我们利用一个开源的FM实现(见参考文献4)。FM相关的特征工程跟logistics回归是基本一样的,logistics回归的特征可以简单处理后直接复用到FM中,我们这里不重复讲解。利用FM进行排序的代码实现如下(对应我们github代码仓库ranking/hm目录下的fm.py):

r"""
  基于 xlearn(pip3 install xlearn 或者直接从源码来安装) 包来实现 fm 算法。
  https://github.com/aksnzhy/xlearn
  输入数据格式:
          CSV format:
           y    value_1  value_2  ..  value_n
           0      0.1     0.2     0.2   ...
           1      0.2     0.3     0.1   ...
           0      0.1     0.2     0.4   ...
  example:
    # Load dataset
    iris_data = load_iris()
    X = iris_data['data']
    y = (iris_data['target'] == 2)
    X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.3, random_state=0)
    # param:
    #  0. binary classification
    #  1. model scale: 0.1
    #  2. epoch number: 10 (auto early-stop)
    #  3. learning rate: 0.1
    #  4. regular lambda: 1.0
    #  5. use sgd optimization method
    linear_model = xl.LRModel(task='binary', init=0.1,
                              epoch=10, lr=0.1,
                              reg_lambda=1.0, opt='sgd')
    # Start to train
    linear_model.fit(X_train, y_train,
                     eval_set=[X_val, y_val],
                     is_lock_free=False)
    # Generate predictions
    y_pred = linear_model.predict(X_val)
"""




# Training task
fm_model = xl.create_fm()  # Use factorization machine
fm_model.setTrain("../../output/hm/fm_train_data.csv")  # Training data




param = {"task": "binary",
         "lr": 0.2,
         "lambda": 0.002,
         "metric": 'acc',
         "epoch": 20,
         "opt": 'sgd',
         "init": 0.1,
         "k": 15  # Dimensionality of the latent factors
         }




# Use cross-validation
# fm_model.cv(param)




# Start to train
# The trained model will be stored in model.out
fm_model.fit(param, '../../output/hm/fm_model.out')




# Prediction task
fm_model.setTest("../../output/hm/predict_data.csv")  # Test data
fm_model.setSigmoid()  # Convert output to 0-1




# Start to predict
# The output result will be stored in output.txt
fm_model.predict("../../output/hm/fm_model.out", "../../output/hm/fm_predict.csv")

19.3.2.3 基于GBDT排序

GBDT进行排序的算法原理我们在第12章的12.3节中讲过,本节的代码实现我们是基于开源的xgboost(见参考文献5)框架来实现的。特征部分可以复用logistics的特征(当然部分特征可以不用进行one-hot编码,读者可以尝试一下,我们在本章中没有详细讲解GBDT的特征部分,下面代码中xgb_X=full_preprocessing_feature[cols],xgb_Y = full_feature['target'] 部分就是处理好的特征),具体的GBDT模型训练及模型预测,可以参考下面的代码实现(对应我们github代码仓库ranking/hm目录下的gbdt.py)。另外,我们的代码中还包括超参数调优的实现,供读者参考。

r"""
利用xgboost包来进行 gbdt 模型的学习。
https://github.com/dmlc/xgboost
"""
import matplotlib.pyplot as plt
import xgboost as xgb
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import accuracy_score, roc_auc_score, roc_curve
from sklearn import metrics
from tqdm import tqdm_notebook
tqdm_notebook().pandas()




def cal_metrics(model, x_train, y_train, x_test, y_test):
    """ Calculate AUC and accuracy metric
        Args:
            model: model that need to be evaluated
            x_train: feature of training set
            y_train: target of training set
            x_test: feature of test set
            y_test: target of test set
    """
    y_train_pred_label = model.predict(x_train)
    y_train_pred_proba = model.predict_proba(x_train)
    accuracy = accuracy_score(y_train, y_train_pred_label)
    auc = roc_auc_score(y_train, y_train_pred_proba[:, 1])
    print("Train set: accuracy: %.2f%%" % (accuracy*100.0))
    print("Train set: auc: %.2f%%" % (auc*100.0))
    y_pred = model.predict(x_test)
    accuracy = accuracy_score(y_test, y_pred)
    y_test_proba = model.predict_proba(x_test)
    auc = roc_auc_score(y_test, y_test_proba[:, 1])
    print("Test set: accuracy: %.2f%%" % (accuracy*100.0))
    print("Test set: auc: %.2f%%" % (auc*100.0))




def model_iteration_analysis(alg, feature, predictors, use_train_cv=True,
                             cv_folds=5, early_stopping_rounds=50):
    """ The optimal iteration times of the model are analyzed
        Args:
            alg: model
            feature: feature of train set
            predictors: target of train set
            use_train_cv: whether to cross-validate
            cv_folds: the training set id divided into several parts
            early_stopping_rounds: observation window size of iteration number
        Return:
            alg: optimal model
    """
    if use_train_cv:
        xgb_param = alg.get_xgb_params()
        xgb_train = xgb.DMatrix(feature, label=predictors)
        # 'cv' function, can use cross validation on each iteration and return the desired number of decision trees.
        cv_result = xgb.cv(xgb_param, xgb_train, num_boost_round=alg.get_params()['n_estimators'], nfold=cv_folds,
                          metrics='auc', early_stopping_rounds=early_stopping_rounds, verbose_eval=1)
        print("cv_result---", cv_result.shape[0])
        print(cv_result)
        alg.set_params(n_estimators=cv_result.shape[0])
    # Fit the algorithm on the data
    alg.fit(feature, predictors, eval_metric='auc')
    # Predict training set:
    predictions = alg.predict(feature)
    pred_prob = alg.predict_proba(feature)[:, 1]
    # Print model report:
    print("\nModel Report")
    print("Accuracy : %.4g" % metrics.accuracy_score(predictors, predictions))
    print("AUC Score (Train): %f" % metrics.roc_auc_score(predictors, pred_prob))
    return alg




if __name__ == "__main__":
    # build Xgboost model
    import warnings
    warnings.filterwarnings('ignore')
    TEST_RATIO = 0.3
    RANDOM_STATE = 33
    xgb_X = full_preprocessing_feature[cols]
    xgb_Y = full_feature['target']
    X_full_train, X_full_test, y_full_train, y_full_test = \
        train_test_split(xgb_X, xgb_Y, test_size=TEST_RATIO, random_state=RANDOM_STATE)
    # build the base model using the initial values
    base_model = xgb.XGBClassifier(
        learning_rate=0.1,
        n_estimators=200,
        booster='gbtree',
        colsample_bytree=1,
        gamma=0,
        max_depth=6,
        min_child_weight=1,
        reg_alpha=0,
        reg_lambda=1,
        scale_pos_weight=1,
        subsample=1,
        verbosity=0,
        objective='binary:logistic',
        seed=666
    )
    # train model
    base_model.fit(X_full_train, y_full_train)
    cal_metrics(base_model, X_full_train, y_full_train, X_full_test, y_full_test)
    # adjust tree structure
    param_tree_struction = {
        'max_depth': range(3, 16, 2),
        'min_child_weight': range(1, 8, 2)
    }
    # grid search
    full_tree_struction_gsearch = GridSearchCV(estimator=base_model,
                                               param_grid=param_tree_struction, scoring='roc_auc',
                                               cv=5, verbose=0, iid=False)
    full_tree_struction_gsearch.fit(X_full_train, y_full_train)
    print(full_tree_struction_gsearch.best_params_, full_tree_struction_gsearch.best_score_,
          full_tree_struction_gsearch.best_estimator_)
    cal_metrics(full_tree_struction_gsearch.best_estimator_, X_full_train, y_full_train, X_full_test, y_full_test)
    # continue to adjust the tree structure more precisely
    param_tree_struction2 = {
        'max_depth': [6, 7, 8],
        'min_child_weight': [4, 5, 6]
    }
    tree_struction_gsearch2 = GridSearchCV(estimator=base_model,param_grid=param_tree_struction2,
                                           scoring='roc_auc', cv=5, verbose=0, iid=False)
    tree_struction_gsearch2.fit(X_full_train, y_full_train)
    print(tree_struction_gsearch2.best_params_, tree_struction_gsearch2.best_score_, tree_struction_gsearch2.best_estimator_)
    cal_metrics(tree_struction_gsearch2.best_estimator_, X_full_train, y_full_train, X_full_test, y_full_test)
    adjust_tree_struction_model = xgb.XGBClassifier(
        learning_rate=0.1,
        n_estimators=200,
        booster='gbtree',
        colsample_bytree=1,
        gamma=0,
        max_depth=6,
        min_child_weight=6,
        n_jobs=4,
        reg_alpha=0,
        reg_lambda=1,
        scale_pos_weight=1,
        subsample=1,
        verbosity=0,
        objective='binary:logistic',
        seed=666
    )
    # adjusting the Gamma parameter
    param_gamma = {
        'gamma': [i / 10 for i in range(0, 10)]
    }
    gamma_gsearch = GridSearchCV(estimator=adjust_tree_struction_model, param_grid=param_gamma, scoring='roc_auc',
                                 cv=5, verbose=0, iid=False)
    gamma_gsearch.fit(X_full_train, y_full_train)
    print(gamma_gsearch.best_params_, gamma_gsearch.best_score_, gamma_gsearch.best_estimator_)
    # calculate AUC and accuracy metric
    cal_metrics(gamma_gsearch.best_estimator_, X_full_train, y_full_train, X_full_test, y_full_test)




    adjust_gamma_model = xgb.XGBClassifier(
        learning_rate=0.1,
        n_estimators=200,
        booster='gbtree',
        colsample_bytree=1,
        gamma=0,
        max_depth=6,
        min_child_weight=6,
        n_jobs=4,
        reg_alpha=0,
        reg_lambda=1,
        scale_pos_weight=1,
        subsample=1,
        verbosity=0,
        objective='binary:logistic',
        seed=666
    )
    # adjust sample ratio and column sampling ratio parameters
    param_sample = {
        'subsample': [i / 10 for i in range(6, 11)],
        'colsample_bytree': [i / 10 for i in range(6, 11)],
    }
    sample_gsearch = GridSearchCV(estimator=adjust_gamma_model, param_grid=param_sample,
                                  scoring='roc_auc', cv=5, verbose=0, iid=False)
    sample_gsearch.fit(X_full_train, y_full_train)
    print(sample_gsearch.best_params_, sample_gsearch.best_score_, sample_gsearch.best_estimator_)
    cal_metrics(sample_gsearch.best_estimator_, X_full_train, y_full_train, X_full_test, y_full_test)
    adjust_sample_model = xgb.XGBClassifier(
        learning_rate=0.1,
        n_estimators=200,
        booster='gbtree',
        colsample_bytree=0.8,
        gamma=0,
        max_depth=6,
        min_child_weight=6,
        n_jobs=4,
        reg_alpha=0,
        reg_lambda=1,
        scale_pos_weight=1,
        subsample=0.8,
        verbosity=0,
        objective='binary:logistic',
        seed=666
    )
    # adjust regularization param
    param_L = {
        'reg_lambda': [1e-5, 1e-3, 1e-2, 1e-1, 1, 100],
    }
    L_gsearch = GridSearchCV(estimator=adjust_sample_model, param_grid=param_L, scoring='roc_auc', cv=5, verbose=0, iid=False)
    L_gsearch.fit(X_full_train, y_full_train)
    print(L_gsearch.best_params_, L_gsearch.best_score_, L_gsearch.best_estimator_)
    model_iteration_analysis(L_gsearch.best_estimator_, X_full_train, y_full_train, early_stopping_rounds=30)
    # adjusted learning rate
    param_learning_rate = {
        'learning_rate': [0.005, 0.01, 0.02, 0.05, 0.08, 0.1, 0.15, 0.2],
    }
    learning_rate_gsearch = GridSearchCV(estimator=adjust_sample_model, param_grid=param_learning_rate, scoring='roc_auc', cv=5, verbose=0, iid=False)
    learning_rate_gsearch.fit(X_full_train, y_full_train)
    print(learning_rate_gsearch.best_params_, learning_rate_gsearch.best_score_, learning_rate_gsearch.best_estimator_)
    model_iteration_analysis(learning_rate_gsearch.best_estimator_,
                             X_full_train, y_full_train, early_stopping_rounds=30)
    # optimal model
    best_model = xgb.XGBClassifier(
        learning_rate=0.1,
        n_estimators=200,
        booster='gbtree',
        colsample_bytree=0.7,
        gamma=0.6,
        max_depth=6,
        min_child_weight=2,
        reg_alpha=0,
        reg_lambda=1,
        scale_pos_weight=1,
        subsample=0.9,
        verbosity=0,
        objective='binary:logistic',
        seed=666
    )
    best_model.fit(X_full_train, y_full_train)
    print('--- the training set and test set metrics of Xgboost model ---\n')
    cal_metrics(best_model, X_full_train,y_full_train, X_full_test, y_full_test)
    print('\n')
    print(best_model.get_xgb_params())
    # according to XgBoost model, the importance of features was analyzed
    print('\n')
    print('---according to xGBoost model, the importance of features was analyzed---\n')
    from xgboost import plot_importance
    fig, ax = plt.subplots(figsize=(10,15))
    plot_importance(best_model, height=0.5, max_num_features=100, ax=ax)
    plt.show()
    # Draw ROC curve
    y_pred_proba = best_model.predict_proba(X_full_test)
    fpr, tpr, thresholds = roc_curve(y_full_test, y_pred_proba[:, 1])
    print('---ROC curve of xgboost model ---\n')
    plt.title('roc_curve of xgboost (AUC=%.4f)' % (roc_auc_score(y_full_test, y_pred_proba[:, 1])))
    plt.xlabel('FPR')
    plt.ylabel('TPR')
    plt.plot(fpr, tpr)
    plt.show()

19.3.2.4 基于匹配用户画像兴趣的排序

该排序算法基于输入的几个召回算法提供的召回列表,基于用户的兴趣画像,利用召回列表中的物品来匹配用户兴趣画像,按照与用户兴趣画像的匹配度对召回物品进行降序排列,最终将topN作为最终的排序结果推荐给用户,算法原理我们在第11章的11.4节中已经做过介绍,不熟悉的读者可以参考一下,具体的代码实现如下(对应我们github代码仓库ranking/hm目录下的n_recalls_matching_user_portrait.py)。

 
 
r"""
基于用户兴趣画像,利用召回物品跟用户画像的匹配度来进行排序。
"""
import numpy as np
article_dict = np.load("../../output/hm/article_dict.npy", allow_pickle=True).item()
user_portrait = np.load("../../output/hm/user_portrait.npy", allow_pickle=True).item()




def user_portrait_similarity(portrait, article_id):
    """
    计算某个article与用户画像的相似度。
    :param portrait: 用户画像。
        { 'product_code': set([108775, 116379])
          'product_type_no': set([253, 302, 304, 306])
          'graphical_appearance_no': set([1010016, 1010017])
          'colour_group_code': set([9, 11, 13])
          'perceived_colour_value_id': set([1, 3, 4, 2])
          'perceived_colour_master_id': set([11, 5 ,9])
        }
    :param article_id: 物品id。
    :return: sim,double,相似度。
    """
    feature_dict = article_dict[article_id]
    # article_dict[957375001]
    # {'product_code': 957375, 'product_type_no': 72,
    # 'graphical_appearance_no': 1010016, 'colour_group_code': 9,
    # 'perceived_colour_value_id': 4, 'perceived_colour_master_id': 5}
    sim = 0.0
    features = {'product_code', 'product_type_no', 'graphical_appearance_no',
                'colour_group_code', 'perceived_colour_value_id', 'perceived_colour_master_id'}
    for fea in features:
        fea_value = feature_dict[fea]
        if fea_value in portrait[fea]:
            sim = sim + 1.0  # 只要用户的某个画像特征中包含这个物品的该画像值,那么就认为跟用户的兴趣匹配
    return sim/6




def user_portrait_ranking(portrait, recall_list, n):
    """
    利用用户画像匹配度进行排序。
    :param portrait: 用户画像。
        { 'product_code': set([108775, 116379])
          'product_type_no': set([253, 302, 304, 306])
          'graphical_appearance_no': set([1010016, 1010017])
          'colour_group_code': set([9, 11, 13])
          'perceived_colour_value_id': set([1, 3, 4, 2])
          'perceived_colour_master_id': set([11, 5 ,9])
        }
    :param recall_list: [recall_1, recall_2, ..., recall_k].
        每个recall的数据结构是 recall_i ~ [(v1,s1),(v2,s2),...,(v_t,s_t)]
    :param n: 推荐数量
    :return: rec ~ [(v1,s1),(v2,s2),...,(v_t,s_t)]
    """
    rec_dict = dict()
    for recall in recall_list:
        for (article_id, _) in recall:
            sim = user_portrait_similarity(portrait, article_id)
            if article_id in rec_dict:
                rec_dict[article_id] = rec_dict[article_id] + sim
                # 如果多个召回列表,召回了相同的物品,那么相似性相加。
            else:
                rec_dict[article_id] = sim
    rec = sorted(rec_dict.items(), key=lambda item: item[1], reverse=True)
    return rec[0:n]




if __name__ == "__main__":
    rec_num = 5
    customer = "00083cda041544b2fbb0e0d2905ad17da7cf1007526fb4c73235dccbbc132280"
    customer_portrait = user_portrait[customer]
    recall_1 = [(111586001, 0.45), (112679048, 0.64), (158340001, 0.26)]
    recall_2 = [(176550016, 0.13), (189616038, 0.34), (212629035, 0.66)]
    recall_3 = [(233091021, 0.49), (244853032, 0.24), (265069020, 0.71)]
    recalls = [recall_1, recall_2, recall_3]
    rec = user_portrait_ranking(customer_portrait, recalls, rec_num)
    print(rec)

19.3.2.5 基于wide&deep模型的排序

我们最后要讲解的排序算法是wide&deep排序,该算法的原理我们在第13章的13.1节中做过介绍,这个算法是非常经典的深度学习排序算法,读者需要好好掌握。本节的代码实现我们利用开源的pytorch-widedeep框架(见参考文献6、7),这个框架是我能够找到的最好的wide&deep的框架,实现比较简洁,抽象合理,并且还对wide&deep进行了拓展,可以整合文本、图像特征,读可以自行学习一下。

pytorch-widedeep框架下的wide&deep的特征跟logistics类似,这里不展开了,下面贴出具体的模型训练和预测的代码(对应我们github代码仓库ranking/hm目录下的wide_and_deep.py),读者可以基于该代码实现和pytorch-widedeep的官网进行学习,搞清楚具体每一步的实现逻辑。

 
 
r"""
    利用 pytorch 实现wide & deep模型,我们用开源的pytorch-widedeep来实现。
    代码仓库:https://github.com/jrzaurin/pytorch-widedeep
    参考文档:https://pytorch-widedeep.readthedocs.io/en/latest/index.html
"""




# Define the 'column set up'
wide_cols = [
    "sales_channel_id",
    "Active",
    "club_member_status",
    "fashion_news_frequency",
]
crossed_cols = [("product_code", "product_type_no"), ("product_code", "FN"),
                ("graphical_appearance_no", "FN"), ("colour_group_code", "FN"),
                ("perceived_colour_value_id", "FN"), ("perceived_colour_master_id", "FN")]




cat_embed_cols = [
    "sales_channel_id",
    "product_code",
    "product_type_no",
    "graphical_appearance_no",
    "colour_group_code",
    "perceived_colour_value_id",
    "perceived_colour_master_id",
    "FN",
    "Active",
    "club_member_status",
    "fashion_news_frequency",
    "postal_code"
]
continuous_cols = ["t_dat", "price", "age"]
target = "label"
target = df_train[target].values




# prepare the data
wide_preprocessor = WidePreprocessor(wide_cols=wide_cols, crossed_cols=crossed_cols)
X_wide = wide_preprocessor.fit_transform(df_train)




tab_preprocessor = TabPreprocessor(
    cat_embed_cols=cat_embed_cols, continuous_cols=continuous_cols  # type: ignore[arg-type]
)
X_tab = tab_preprocessor.fit_transform(df_train)




# build the model
wide = Wide(input_dim=np.unique(X_wide).shape[0], pred_dim=1)
tab_mlp = TabMlp(
    column_idx=tab_preprocessor.column_idx,
    cat_embed_input=tab_preprocessor.cat_embed_input,
    cat_embed_dropout=0.1,
    continuous_cols=continuous_cols,
    mlp_hidden_dims=[400, 200],
    mlp_dropout=0.5,
    mlp_activation="leaky_relu",
)
model = WideDeep(wide=wide, deeptabular=tab_mlp)




# train and validate
accuracy = Accuracy(top_k=2)
precision = Precision(average=True)
recall = Recall(average=True)
f1 = F1Score()
early_stopping = EarlyStopping()
model_checkpoint = ModelCheckpoint(
    filepath="../../output/hm/tmp_dir/wide_deep_model",
    save_best_only=True,
    verbose=1,
    max_save=1,
)
trainer = Trainer(model, objective="binary",
                  optimizers=torch.optim.AdamW(model.parameters(), lr=0.001),
                  callbacks=[early_stopping, model_checkpoint],
                  metrics=[accuracy, precision, recall, f1])
trainer.fit(
    X_wide=X_wide,
    X_tab=X_tab,
    target=target,
    n_epochs=30,
    batch_size=256,
    val_split=0.2
)




# predict on test
X_wide_te = wide_preprocessor.transform(df_test)
X_tab_te = tab_preprocessor.transform(df_test)
pred = trainer.predict(X_wide=X_wide_te, X_tab=X_tab_te)
# pred_prob = trainer.predict_proba(X_wide=X_wide_te, X_tab=X_tab_te)  # 预测概率
y_test = df_test['label']
print(accuracy_score(y_test, pred))




# Save and load
trainer.save(
    path="../../output/hm/model_weights",
    model_filename="wd_model.pt",
    save_state_dict=True,
)




# prepared the data and defined the new model components:
# 1. Build the model
model_new = WideDeep(wide=wide, deeptabular=tab_mlp)
model_new.load_state_dict(torch.load("../../output/hm/model_weights/wd_model.pt"))




# 2. Instantiate the trainer
trainer_new = Trainer(model_new, objective="binary",
                      optimizers=torch.optim.AdamW(model.parameters(), lr=0.001),
                      callbacks=[early_stopping, model_checkpoint],
                      metrics=[accuracy, precision, recall, f1])




# 3. Either start the fit or directly predict
pred = trainer_new.predict(X_wide=X_wide_te, X_tab=X_tab_te)

总结

本章我们讲解了所有基于H&M数据集的召回和排序算法,本章的算法比上一章更复杂、更有挑战、也更重要,因此,本章的内容读者要非常熟悉,特别是代码实现细节,需要读者能够完全理解并掌握,最好自己能够跟着本章提供的代码初稿,自己重新跑一下,如果能够对我们提供的代码进行优化完善,那再好不过了。我们也希望读者可以贡献更多、更好的代码实现。

到目前为止,我们介绍完了本书所有的召回、排序算法的代码实战案例,这些算法也覆盖了我们在第6-第13章的绝大多数召回、排序算法,唯一的例外是基于YouTube的召回、排序算法(YouTube召回参考第9章9.2节,YouTube排序算法参考第13章13.2节)。主要是YouTube算法利用的数据集比较特殊,需要用户的搜索、观看记录,预测的是用户的播放时长,比较适合视频类场景,不过github上还是有一个不错的、基于构造的数据集给出了YouTube_DNN的实现(见参考文献8),后面我们也会基于该代码实现进行适当微调整合到我们的github代码仓库中。

前面的章节,我们已经介绍完了本书关于推荐系统算法、工程、代码实现的最核心的部分了。后面我们会增加一些行业应用案例,最后一章会对推荐系统的未来发展进行梳理和预测。

参考文献

  1. https://www.kaggle.com/competitions/h-and-m-personalized-fashion-recommendations/overview

  2. https://github.com/RaRe-Technologies/gensim

  3. https://radimrehurek.com/gensim/

  4. https://github.com/aksnzhy/xlearn

  5. https://github.com/dmlc/xgboost

  6. https://github.com/jrzaurin/pytorch-widedeep

  7. https://pytorch-widedeep.readthedocs.io/en/latest/index.html

  8. https://github.com/hyez/Deep-Youtube-Recommendations

大家如果对推荐系统感兴趣,可以点击下面链接购买我出版的图书《构建企业级推荐系统》,全面深入地学习企业级推荐系统的方法论。


「从零入门推荐系统」19:H&M推荐系统代码实战案例文章来源地址https://www.toymoban.com/news/detail-465613.html

到了这里,关于「从零入门推荐系统」19:H&M推荐系统代码实战案例的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【深度学习时间序列预测案例】零基础入门经典深度学习时间序列预测项目实战(附代码+数据集+原理介绍)

    🚨注意🚨 :最近经粉丝反馈,发现有些订阅者将此专栏内容进行二次售卖,特在此声明,本专栏内容仅供学习,不得以任何方式进行售卖,未经作者许可不得对本专栏内容行使发表权、署名权、修改权、发行权、转卖权、信息网络传播权,如有违者,追究其法律责任。 👑

    2023年04月15日
    浏览(60)
  • 小说作者推荐:春溪笛晓合集

    文案: 紫电满天,雷声轰鸣。 这次,彻底结束了吗? 扶苏闭上眼,任由神魂在空中飘荡。 “你的天赋极好,可惜道心不稳。” 一声叹息传入扶苏耳中。 “这么多年了,你还是没放下啊。” 放不下什么? 扶苏眼前掠过许多画面,他跪地劝谏父皇,父皇勃然大怒,命他前往

    2024年02月01日
    浏览(26)
  • 19.云原生CICD之ArgoCD入门CD过程实战

    云原生专栏大纲 总结ArgoCD是基于 kubernetes 的声明式 Gitops 持续部署工具 应用定义,配置和环境变量管理等等,都是声明式基于云原生的。 所有声明清单都存储在代码仓库中,受版本管理 应用发布和生命周期管理都是自动化的,可审计的。 Argo CD 是一个基于 GitOps 的持续交付

    2024年01月24日
    浏览(37)
  • 从零开始搭建搜索推荐系统(五十三)QUERY从分词检索进阶

    聊的不止技术。跟着小帅写代码,还原和技术大牛一对一真实对话,剖析真实项目筑成的一砖一瓦,了解最新最及时的资讯信息,还可以学到日常撩妹小技巧哦,让我们开始探索主人公小帅的职场生涯吧! (PS:本系列文章以幽默风趣风格为主,较真侠和杠精请绕道~) 一、

    2024年02月04日
    浏览(35)
  • 大数据教材推荐|Python数据挖掘入门、进阶与案例分析

      主   编: 卢滔,张良均,戴浩,李曼,陈四德 出版社: 机械工业出版社 内容提要 本书从实践出发,结合11个 “泰迪杯” 官方推出的赛题,按照赛题的难易程度进行排序,由浅入深地介绍数据挖掘技术在 商务、教育、交通、传媒、旅游、电力、制造业等行业的应用 。因

    2024年02月10日
    浏览(38)
  • 2023最新网络安全书单推荐——助你从零基础入门到成为网络安全工程师到进阶首席安全官

    开门见山,各位从零基础入门网络安全时,一定要有阅读书籍的习惯,因为你在学习时看的视频只会让你的知识体系浮于表面,而书籍的作用就是进一步的弥补你的基础。但是如果初学者一上来就抱着书来啃自然是更加不现实,所以这里我给大家整理了一整套从零基础小白入

    2024年02月16日
    浏览(68)
  • LLM系列 | 19 : Llama 2实战(上篇)-本地部署(附代码)

    小伙伴们好,我是《小窗幽记机器学习》的小编:卖热干面的小女孩。紧接前文:万字长文细说ChatGPT的前世今生,后续会尝试以理论+实践的方式逐步对主流的各大LLM进行实测和汉化。今天这篇关于Llama2的小作文其实比较长,所以分为上下两篇,上篇主要介绍 Llama2的基本情况

    2024年02月07日
    浏览(40)
  • 一个基于SpringBoot开发的RBAC系统,非常适合新手入门JavaWeb代码审计实战的系统,长文警告,要好好学习。

    嗨,大家好,我是闪石星曜CyberSecurity创始人Power7089。 欢迎大家搜索我的微信公众号:闪石星曜CyberSecurity 本文是【炼石计划@Java代码审计】内部圈子原创课程,现分享给大家学习。 如需转载,请详细注明来源。 欢迎大家搜索并添加我的好友【Power_7089】,备注CSDN,邀请你进入

    2024年02月11日
    浏览(53)
  • [皮尔逊相关系数corrwith]使用案例:电影推荐系统

    协同过滤算法用于发现用户与物品之间的相关性,主要有两种:基于用户的和基于物品的。 基于用户: 用户1购买了物品A、B、C、D,并给了好评;而用户2也买了A、B、C,那么认为用户1和用户2是同类型用户,也可以把D推荐给用户2。 基于物品: 物品A和物品B都被用户1、2、

    2024年02月10日
    浏览(39)
  • 推荐源哥和川川的新书:《Pyhton网络爬虫从入门到实战》

    ❤️作者主页:小虚竹 ❤️作者简介:大家好,我是小虚竹。2022年度博客之星评选TOP 10🏆,Java领域优质创作者🏆,CSDN博客专家🏆,华为云享专家🏆,掘金年度人气作者🏆,阿里云专家博主🏆,51CTO专家博主🏆 ❤️技术活,该赏 ❤️点赞 👍 收藏 ⭐再看,养成习惯 老规

    2024年02月07日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包