本文分享一个开源项目:课程推荐系统。

项目地址见文末参考资料[1].

该项目基于协同过滤算法,输入要预测的用户id以及要推荐的课程总数,输出推荐结果。

整个项目将使用Flask框架进行部署。

代码结构

Alt text

主要逻辑在generator.py中,与其并列的testing.py作测试用,其余文件都是些工程上的代码(内存占用优化,日志文件处理,前端展示)。

数据处理

原数据是日志文件,作者已经针对这些日志文件做了处理(使用Input Preprocessing中的模块),并提供了处理好的数据(.csv),因此这一步可以忽略。

读取处理后的数据:

1
2
import pandas as pd
df=pd.read_csv('features_sample.csv')

Alt text

共49205条这样的数据,除了前两列用于标识不同课程和用户外,其余列均为特征列。

值得注意的是,对于前面两列,不同行可能有相同的取值。这是因为某一课程通常会被多个用户观看,且某个用户通常会观看不止一门课程。
Alt text

代码解读

generator模块中,作者定义了一个recommendationGenerator类,它接收两个参数:userIDN,分别表示要预测的用户id和推荐的课程总数。

load_data方法:读取上一步处理好的.csv文件,将其拆分成两个数据框,data数据框只包含courseIDuserID两列,features数据框包含从剩余列(特征列)中筛选出来的7列。

1
2
3
4
5
6
7
8
9
10
11
def load_data(self,datapath):
features=pd.read_csv(datapath, usecols = ['click_courseware','load_video','pause_video','problem_check','problem_get','seek_video','stop_video'])
features = features.apply(pd.to_numeric, errors='coerce')
features = features.fillna(0)

data = pd.read_csv(datapath, usecols = ['courseID','userID'])
cols = data.columns.drop('courseID')
data[cols] = data[cols].apply(pd.to_numeric, errors='coerce')
data = data.fillna(0)

return features, data

generate_recommendations方法:接受load_data方法返回的两个数据框,以产生推荐结果。

具体地,首先获取要预测的用户(userID)历史观看课程的索引(所在行数),并据此获取对应行的特征,存入变量X。比如某用户共观看过5门课程,那么X的维度就是(5,7),7是特征维度。

然后,计算X与之前得到的features(维度为(49205,7))之间余弦相似度,输出结果到变量cossim_mat,其维度为(5,49205),这里面存储了用户观看过的5门课程所对应的特征分别与全部数据记录所对应的特征(每一行是一条记录)之间的相似度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def generate_recommendations(self,features,data, print_rec = False):
index = data[dalta['userID'] == self.userID].index.tolist()
#Storig the rows into a new dataframe
X = features.iloc[index]
#Applying cosine similarity and storing the matrix
cossim_mat = cosine_similarity(X = X.to_numpy(copy = True),Y =features.to_numpy(copy = True), dense_output= False)
#Get top N recommendations
recomm_indices = self.largest_indices(cossim_mat,self.N,data)

if (print_rec == True):
#Print the recommendations from the obtained recomm_indices
self.print_recommendations(recomm_indices,data)

return
else:
#Return the list of recommendations
recomm = []
i = 0
for x in data['courseID'][recomm_indices].unique():
i+=1
recomm.append(x)
if(i==self.N):
break

return recomm

largest_indices方法:使用获取到的相似度结果,找到前N个(相似度从大到小)相似度结果对应的数据记录索引(所在行数),然后将结果降序排列。因为每条记录对应的课程可能是重复的,因此作者递归地调用该方法,直到推荐的不同课程数达到N。

ps:这部分本来直接可以排序然后拿Top-N结果的,但是这样需要对全部数据排序,在实际应用中时间开销较大,所以这里采用了np.argpartation,具体请看代码注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def largest_indices(self,ary, top_N, data ):
flat = ary.flatten()
#用第len(flat)-top-N大的数字将flat切成两部分,前一部分是所有小于该数字的元素下标,后一部分是所有大于该元素的下标
#再辅之以[-top_N:],就得到了flat中前top-N的元素所在下标indices
indices = np.argpartition(flat, -top_N)[-top_N:]

#当然,此时的到的top-N元素是无序的,因此需要再次排序
#加负号,可以让排序结果从大到小排列
indices = indices[np.argsort(-flat[indices])]

#因为ary做了flatten,原本是m行,ary.shape[1]=49205列的
#m指的是该userID在数据记录中出现的次数,详见generate_recommendations方法
#因此需要除以49025
indices = indices % ary.shape[1]

n = data['courseID'][indices].unique().shape[0]
#print('n',n)
if (n < self.N):
indices = self.largest_indices(ary,top_N + (top_N-n),data)

#Performing MOD by the orignal size as we initially flattened the array
indices = indices % ary.shape[1]

return indices

至此,核心代码就完成了,执行testing.py可以测试一下:
Alt text

Falsk部署

这一部分涉及到了些开发的知识,这里仅作展示后端代码,前端的HTML就不放这儿了,我已将代码做了精简并打包,公众号后台回复“课程推荐”即可获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#Setting path variables
import sys
from configs import *
sys.path.append(PROJECT_PATH)

#Importing required libraries
from flask import Flask,render_template,request
from Recommendation_Generator.generator import recommendationGenerator

features,data = recommendationGenerator.load_data(recommendationGenerator, datapath= DATA_PATH)
users = data['userID'].unique()

app = Flask(__name__)

#HomePage
@app.route("/")
def home():
return render_template("index.html",max = users.shape[0] -1)

#ResultPage
@app.route("/result", methods=["GET", "POST"])
def result():
if request.method == "POST":

#Collecting the form responses
userdata = request.form

#Extracting the values for UserIndex and No. of recommendations
user = int(userdata.get("index"))
N = int(userdata.get("N"))

#Getting the userID from the user Index
users = data['userID'].unique()
userID = int(users[user])

#Running the model, generating recommendations and passing the list to the HTML page
model = recommendationGenerator(userID, N)
recomm = model.generate_recommendations(features,data)

return render_template("result.html",userID = userID ,rec_list= recomm)

else:

return "Sorry, there was an error."

if __name__ == "__main__":
app.run(debug= True)

效果:
Alt text
Alt text

参考: