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

news2024/7/4 5:54:36

作者 | 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

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


7a867ab7774c4541efc7d6e69bd5a0da.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/592282.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA企业级开发 1.5 初探Spring AOP

一、提出游吟诗人唱赞歌任务 骑士执行任务前和执行任务后,游吟诗人唱赞歌 (一)采用传统方式实现 修改day04子包的勇敢骑士类 修改day04子包里的救美骑士类 执行测试类 - TestKnight (二)采用传统方式实现的缺…

Hive部署本地模式

本地模式 使用mysql替换derby进行元数据的存储,hive的相关进程都是在同一台机器上,即本地模式。mysql因为是独立的进程,所以mysql可以和hive在同一机器上,也可以在其他机器上。 说明: 通常使用关系型数据库来进行元数据…

微信公众号、支付接口认证:一步步教您如何实现

1、微信公众号接口认证方案 1.1 认证流程 1)官方配置Token验证 Token不在网络中传递 2)开发一个Token验证接口 Token及其它参数拼接并字典排序再做sha摘要计算微信定期调用此接口来验证身份正确性通过摘要验证判断请求来源微信(Token配置…

TensorFlow进行MNIST数据集手写数字识别,保存模型并且进行外部手写图片测试

首先,你已经配置好Anaconda3的环境,下载了TensorFlow模块,并且会使用jupyter了,那么接下来就是MNIST实验步骤。 数据集官网下载:MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burgeshttp…

apple pencil的替代品买啥比较好?平价电容笔推荐

随着技术的发展,出现了许多种类的电容笔。一款好的电容笔,不但可以极大地提升我们的工作效率,也可以极大地改善我们的学习效果。平替电容笔无论是在技术方面,还是在产品质量方面,都有着非常广泛的应用前景。下面就是我…

Java领域的序列化与反序列化,Java的对象如何传输,常用序列化技术

文章目录 一、引出问题:Java原生的序列化1、基于Socket传输对象案例2、什么是序列化3、Java 原生序列化4、serialVersionUID 的作用5、transient 关键字绕开 transient 机制的办法writeObject 和 readObject 原理 6、Java 序列化的一些简单总结 二、分布式架构下常见…

【智能座舱系列| AR-HUD增强现实】—AR-HUD到底是“鸡肋”还是“真”香?

AR-HUD 概念 HUD,即抬头显示(Head Up Display),又叫平视显示系统。它的作用,就是把时速、导航等重要的行车信息,投影到驾驶员前面的挡风玻璃上,让驾驶员尽量做到不低头、不转头就能看到。 这种显示系统,原是军用战斗机上的显示系统,飞行员不必低头,就能在挡风玻璃上…

ChatGPT学习笔记;Meta发布Megabyte AI模型抗衡Transformer

AI知识 ChatGPT学习笔记 文章包括如下的内容: ChatGPT 介绍科普 背景知识ChatGPT 功能ChatGPT 原理 等等,文章的地址在这里。 AI新闻 🚀 Meta发布Megabyte AI模型抗衡Transformer:解决后者已知问题、速度提升4成 摘要&…

笔试强训5

作者:爱塔居 专栏:笔试强训 作者简介:大三学生,希望和大家一起进步 目录 day6 day7 day6 1.关于抽象类与最终类,下列说法错误的是? A 抽象类能被继承,最终类只能被实例化。 B 抽象类和最终类…

NET HELPMSG 3534 报错

使用了带管理员权限的 PowerShell(即在管理员权限下运行CMD) 然后进行安装和服务启动操作 1、清空 MySQL 下的 data 文件夹; 2、确保系统环境变量中已经配置了 mysql 的 bin 目录到Path中; 3、执行以下命令: sc delet…

《Opencv3编程入门》学习笔记—第四章

《Opencv3编程入门》学习笔记 记录一下在学习《Opencv3编程入门》这本书时遇到的问题或重要的知识点。 第四章 OpenCV数据结构与基本绘图 四、基础图像容器Mat (一)数字图像存储概述 图像在数码设备中的表现形式为包含众多强度值的像素点矩阵。 &a…

JAVA键盘录入

文章目录 JAVA键盘录入1.导包2.创建对象3.接受数据接收 b o o l e a n \color{red}{boolean} boolean类型数据接收 b y t e \color{red}{byte} byte类型数据接收 s h o r t \color{red}{short} short类型数据接收 i n t \color{red}{int} int类型数据接收 l o n g \color{red}{…

库的制作与使用

什么是库 库是一种可执行的二进制文件,是编译好的代码。使用库可以提高开发效率。在 Linux 下有静态库和动 态库。因此编译出来的体积就比较大。 静态库在程序编译的时候会被链接到目标代码里面。所以程序在运行的时候不再需要静态库了。因此 编译出来的体积就比较大…

轻松学习白嫖GPT-4,已经标星38K,不再害怕高昂的AI模型费用!

文章目录 白嫖方式GPT-4当前可用站点 白嫖方式GPT-4 计算机专业学生xtekky在GitHub上发布了一个名为gpt4free的开源项目,该项目允许您免费使用GPT4和GPT3.5模型。这个项目目前已经获得了380000颗星。 开源地址:https://github.com/xtekky/gpt4free 简而…

vue ts写法

Vue.js 和 TypeScript 结合使用可以让你的项目更加健壮和易于维护。在 Vue 3 中,你可以使用 Vue.js 的 Composition API 和 TypeScript 一起使用。以下是一个简单的 Vue.js 和 TypeScript 结合使用的例子: 首先,确保你已经安装了 Vue.js 和 T…

如何从电机控制转换为运动控制

随着越来越多的技术广泛应用于工业自动化,我们已经进入了工业4.0时代。新技术不断涌现,赋能人工智能和机器学习、数据分析、工业网络、网络安全和功能安全。然而,大多数工业自动化作为其他所有技术的核心,仍然依靠机器人和运动控制…

【PWN · ret2text | PIE 】[NISACTF 2022]ezpie

简单的PIE绕过 目录 前言 一、题目重述 二、解题思路 1.现有信息 2.思考过程 3.exp 总结 前言 所接触的PIE保护的第一题,也非常简单。 一、题目重述 二、解题思路 1.现有信息 PIE保护——程序可能被加载到任意位置,所以位置是可变的。程序返回…

聚观早报 | 英伟达推「AI」超算;中国2030年前载人登月

今日要闻:英伟达推「AI」超算;中国2030年前载人登月;AI大热,游戏股全线大涨;ofo创始人二次创业项目陷入困境;微信视频号原创标记已对外显示 英伟达推「AI」超算 5 月 29 日,NVIDIA 宣布推出一款…

安捷伦E4440A 26.5G频谱分析仪Agilent e4440a 销售/回收

Agilent E4440A HP E4440A频谱分析仪,3 Hz - 26.5 GHz(PSA 系列) Agilent / Keysight PSA 系列 E4440A 高性能频谱分析仪提供强大的一键式测量、多功能功能集和前沿技术,可满足您的项目和需求。选项可供您选择(详情请…

maven 项目中引入第三方jar,并且打包到项目的运行jar包中

背景说明 项目中遇到了人大金仓数据库的jar连接驱动&#xff0c;需要在maven中引入依赖信息 实践 方案1&#xff1a; 1.在官网下载jar包&#xff0c;https://www.kingbase.com.cn/zxwd/index.htm 下载地址。在项目文件中创建libs目录。 修改pom文件的配置信息 <depende…