背景
word2vec用于推荐对象item的相似度的计算,虽然一般是设计到embedding层,但也可以单独作为一个模块训练。
计算流程
- 用户操作清单清洗
一般需要筛选出最偏好的X个item,需要针对时间衰减,用户操作次数,不同的操作入口等给出不同打分。
如果要更细致评分,可类似用营销效果模型的夏普利值归因分析, 评估每个操作对收入或者活跃的贡献。这个就比较复杂了。
最终的数据形态,是比较简单的不定长数组,每一行都是一个用户的操作itemlist:
为了能够测试效果,item拆分为1-10和11-20两组,每个用户只操作其中一组,则预期最终item的相似度,同一组的相似度很高,不同组很低才符合预期。 - pyspark包的导入和初始化
import pyspark
from pyspark.sql import SparkSession
from pyspark.storagelevel import StorageLevel
from pyspark.ml.feature import Tokenizer,HashingTF
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import MulticlassClassificationEvaluator,BinaryClassificationEvaluator
from pyspark.ml import Pipeline,PipelineModel
from pyspark.ml.linalg import Vector
from pyspark.sql import Row
#SparkSQL的许多功能封装在SparkSession的方法接口中
spark = SparkSession.builder \
.appName("dbscan") \
.config("master","local[4]") \
.enableHiveSupport() \
.getOrCreate()
sc = spark.sparkContext
- 读取CSV文件
使用标准的spark方法读取本地文件,同时指定了列名。
如果是在平台上测试,则读取hive库表:
#读取csv文件
dftmp = spark.read.option("header","False") \
.option("inferSchema","true") \
.option("delimiter", " ") \
.csv("XXXXXXXXXX/testw2v5.csv")
df=dftmp.withColumnRenamed("_c0", "trainlist")
df.show(5)
df.printSchema()
- 数据预处理
把数据拆分成数组存储到新的字段里:
from pyspark.sql.functions import split, explode, concat, concat_ws
df_split = df.withColumn("split_trainlist", split(df['trainlist'], ","))
df_split.show()
- 使用word2vec训练
整体训练代码如下,指定列的数量,mincount就不指定了,并得到模型:
所有的item无论编号多少,统一当成字符串处理,训练时统一编码,因为一般itemid都不是从0开始编号,把编号当编码反而会增加onehot编码空间,增加计算量(如果很长时间算不出结果,就要关注这点了):
word2Vec = Word2Vec(vectorSize=3, minCount=0, inputCol="split_trainlist", outputCol="result")
model = word2Vec.fit(df_split)
- 结果预测
本质上训练,还是要得到每个item的词向量,并用于计算相似度:
这里采用偷懒的方法遍历了测试数据的所有item,再进行预测:
df_test = spark.createDataFrame([
("1".split(" "), ),
("2".split(" "), ),
("3".split(" "), ),
("4".split(" "), ),
("5".split(" "), ),
("6".split(" "), ),
("7".split(" "), ),
("8".split(" "), ),
("9".split(" "), ),
("10".split(" "), ),
("11".split(" "), ),
("12".split(" "), ),
("13".split(" "), ),
("14".split(" "), ),
("15".split(" "), ),
("16".split(" "), ),
("17".split(" "), ),
("18".split(" "), ),
("19".split(" "), ),
("20".split(" "), )
], ["split_trainlist"])
df_vector = model.transform(df_test)
- 效果验证
- item两两笛卡尔积:
#计算相似度
#笛卡尔积
df_vector_join=df_vector.withColumn("birthyear",df_vector["split_trainlist"])
rdd_vector=df_vector.rdd.map(lambda p: p)
rdd_vector_join=rdd_vector.cartesian(rdd_vector)
- 相似度函数定义
import math
def cos_cal(v_x,v_y,v_cnt):
"""计算余弦相似度"""
dotxy=0
len2_x=0
len2_y=0
for i in range(v_cnt):
dotxy=dotxy+v_x[i]*v_y[i]
len2_x=len2_x+v_x[i]*v_x[i]
len2_y=len2_y+v_y[i]*v_y[i]
return dotxy*1.0/(math.sqrt(len2_x)*math.sqrt(len2_y))
- 相似度计算
rdd_vector_res=rdd_vector_join.map(lambda x:(x[0][0][0],x[1][0][0],x[0][1],x[1][1],float(cos_cal(x[0][1],x[1][1],3))))
from pyspark.sql import functions as F
df_vector_res=rdd_vector_res.toDF(["key1","key2","vec1","vec2","simcos"])
df_vector_res.where("key1>10 and key2<=10").show(5)
- 计算不同组和相同组的相似度
最终结果符合预期,不同组之间相似度不一样:
df_vector_res.where("key1<=10 and key2<=10").agg({"simcos":"avg"}).show()
df_vector_res.where("key1>10 and key2<=10").agg({"simcos":"avg"}).show()
df_vector_res.where("key1<=10 and key2>10").agg({"simcos":"avg"}).show()
df_vector_res.where("key1>10 and key2>10").agg({"simcos":"avg"}).show()
后记
可以看到整体代码量很少,真正word2vec训练的就2行代码,封装的相当好了。大部分工作量还是在方案的设计,预处理,结果分析。
要评估一个算法方案工作量,需要关注整体宏观分析,而不只是看到代码的一点点。
所以一个算法工具的建设,如果只是在测试数据上跑通过流程,基本相当于耍流氓,不能算是算法做好了。只有在真实数据上计算并产生效果,才算是做好了。