电影推荐实例--基于协同过滤和DL特征提取的比较

最近读到了一篇有意思的文章:如何用深度学习推荐电影,我顺着文章的思路实现了一遍,补全了原文中缺失的code,也加入了一些避免重复操作的code,亦做记录亦作分享。本项目主要实现了基于「1」协同过滤「2」DL特征提取进行电影推荐。

    • 数据集
    • 理论背景
    • 电影相似性
    • 深度学习
    • 总结

数据集

来源于 MovieLens 中的ml-latest-small.zip,当然也可以从本文最后我的github中找到。
本项目主要用到其中的两个csv文件:

  • ml-latest-small/ratings.csvz:包含671个用户,共100004个打分
  • ml-latest-small/links.csv:包含9125个电影及其IMDBid和TMDBid

分别长这样:

#ratings.csvz:
   userId  movieId  rating   timestamp
0       1       31     2.5  1260759144
1       1     1029     3.0  1260759179
2       1     1061     3.0  1260759182
3       1     1129     2.0  1260759185
4       1     1172     4.0  1260759205
......

#links.csv:
   movieId  imdbId   tmdbId
0        1  114709    862.0
1        2  113497   8844.0
2        3  113228  15602.0
3        4  114885  31357.0
4        5  113041  11862.0
......

扒电影海报的网站:
Movie Database,网站提供API,申请不麻烦。不过你们要是能够保证不做坏事的话,可以悄悄告诉你在github里我没有删掉我的API。

理论背景

粗略地说,推荐系统有三种类型(不包括简单的评级方法):

  • 基于内容的推荐
  • 协同过滤
  • 混合模型

“基于内容的推荐”是一个回归问题,我们把电影内容作为特征,对用户对电影的评分做预测。

“协同过滤”中,一般无法提前获得内容特征。是通过用户之间的相似度(用户们给了用一个电影相同的评级)和电影之间的相似度(有相似用户评级的电影)来学习潜在特征,同时预测用户对电影的评分。在学习了电影的特征之后,我们便可以衡量电影之间的相似度,并根据用户历史观影信息,向他/她推荐最相似的电影。

“基于内容的推荐”和“协同过滤”是10多年前最先进的技术。很显然,现在有很多模型和算法可以提高预测效果。比如,针对事先缺乏用户电影评分信息的情况,可以使用隐式矩阵分解,用偏好和置信度取代用户电影打分——比如用户对电影推荐有多少次点击,以此进行协同过滤。另外,我们还可以将“内容推荐”与“协同过滤”的方法结合起来,将内容作为侧面信息来提高预测精度。这种混合方法,可以用“学习进行排序”(”Learning to Rank” )算法来实现。

在该项目中,采用的方法是“协同过滤”。首先,用电影和用户相似度来找出相似度最高的海报,并基于相似度做电影推荐。然后,我将讨论如何Deep Learning学习潜在特征、做电影推荐。最后会谈谈如何在推荐系统中使用深度学习。

电影相似性

对于基于协同过滤的推荐系统,首先要建立评分矩阵。其中,每一行表示一个用户,每一列对应其对某一电影的打分。建立的评分矩阵如下:

#!/usr/bin/env python3
import pandas as pd
import numpy as np
from sklearn.metrics import mean_squared_error

rating_f = 'ml-latest-small/ratings.csv'
link_f   = 'ml-latest-small/links.csv'

df    = pd.read_csv(rating_f,sep=',')
df_id = pd.read_csv(link_f,sep=',')
df    = pd.merge(df,df_id,on=['movieId'])

rating_matrix = np.zeros((df.userId.unique().shape[0],max(df.movieId)))
for row in df.itertuples():
    rating_matrix[row[1]-1,row[2]-1] = row[3]
rating_matrix = rating_matrix[:,:9000] #get first 9000 movies

“ratings.csv”包含用户id,电影id, 评级,和时间信息,其中同一个用户id可能对多个电影进行打分;“links.csv”包含电影id, IMDB id,和TMDB id。这两个文件中包含的电影总数是不同的(rating.csv: 9066, links.csv: 9125)。我们将两个文件依movieId合并,并在此基础上得到评分矩阵rating_matrix[userId:movieId]=grade。

计算评分矩阵的稀疏性:

#Evaluate sparsity of matrix
sparsity = float(len(rating_matrix.nonzero()[0]))
sparsity /= (rating_matrix.shape[0]*rating_matrix.shape[1])
sparsity *= 100
print('Sparsity is {0}%'.format(sparsity))

可以得到sparsity=1.4%,认为是稀疏矩阵。
现在,为了训练和测试,我们将评分矩阵分解成两个矩阵,从每行(userId)评分矩阵中抠出了10个评分,将其放入测试集。

#Splite to train/test matrix
train_matrix = rating_matrix.copy()
test_matrix  = np.zeros(rating_matrix.shape)

for i in range(rating_matrix.shape[0]):
        rating_index = np.random.choice(rating_matrix[i].nonzero()[0],size=10,replace=True) #return a list
        train_matrix[i,rating_index] = 0.0
        test_matrix[i,rating_index] = rating_matrix[i,rating_index]

根据余弦相似性计算两个特征间的夹角(具体过程参见另外一篇博文:余弦相似定理和新闻分类):

cos(θ)=<b,c>bc

即:

cos(θ)=x1y1+x2y2++xnynx21+x22++x2ny21+y22++y2n

同理计算用户/电影中的余弦相似性:

s(u,v)=rurvrurv

这里s(u,v)是用户u和v之间的余弦相似度。

#Cosine similarity
similarity_usr = train_matrix.dot(train_matrix.T) + 1e-9
norms = np.array([np.sqrt(np.diagonal(similarity_usr))])
similarity_usr = (similarity_usr/(norms*norms.T))

similarity_mv = train_matrix.T.dot(train_matrix) + 1e-9
norms = np.array([np.sqrt(np.diagonal(similarity_mv))])
similarity_mv = (similarity_mv/(norms*norms.T))

我们利用其他所有用户对某一电影i的评分来预测当前用户u对该电影的评分,并且用相似度作为权重,然后标准化:

r̂ uv=vs(u,v)rvivs(u,v)

prediction = similarity_usr.dot(train_matrix)/np.array([np.abs(similarity_usr).sum(axis=1)]).T
prediction = prediction[test_matrix.nonzero()]
test_vector = test_matrix[test_matrix.nonzero()]
mse = mean_squared_error(prediction,test_vector)

print('mse: {0}'.format(mse))

预测的MSE是9.8。到此我们实现了根据其他用户的评分来预测某一个用户对某个电影的评分并且进行评价。

但这个数根本看不出来是什么鬼,也不知道预测的怎么样。因此我们换一个直观的方法,直接看相似度:由于矩阵similarity_mv[i,j]表示第i个电影和第j个电影的相关性,所以对于第i个电影找出该行最大的5个值所在的列号,就是和这个电影最相似的电影的movieId。
我们使用IMDB id,使用API从Movie Database 网站获取海报。

从“links.csv”获得movieId和IMDB id的映射关系:

n_display   = 5
base_mv_idx = 0
idx_to_mv   = {}

for row in df_id.itertuples():
        idx_to_mv[row[1]-1] = row[2]

base_mv_idx表示要找出和这个movieId相似的电影,n_display表示找这么多个相似的。

mv = [idx_to_mv[x] for x in np.argsort(similarity_mv[base_mv_idx])[:-n_display-1:-1]]
mv = filter(lambda imdb: len(str(imdb))==6, mv)

下面要把这n_display个电影的海报通过API从Movie Database上扒下来,先获得存储它们的URL:

import requests
import json

#Get posters from Movie Database by API
headers = {'Accept':'application/json'}
payload = {'api_key':'***'} #You can apply your own API from Movie Database
response = requests.get('http://api.themoviedb.org/3/configuration',params=payload,headers=headers)
response = json.loads(response.text)

base_url = response['images']['base_url']+'w185'

def get_poster(imdb,base_url):
    #query themovie.org API for movie poster path.
    imdb_id = 'tt0{0}'.format(imdb)
    movie_url = 'http://api.themoviedb.org/3/movie/{:}/images'.format(imdb_id)
    response = requests.get(movie_url,params=payload,headers=headers)
    try:
        file_path = json.loads(response.text)['posters'][0]['file_path']
    except:
        print('Something wrong, cannot get the poster for imdb id: {0}!'.format(imdb))

    return base_url+file_path

URL = [0]*len(list(mv))
for i,m in enumerate(mv):
        URL[i] = get_poster(m,base_url)

根据得到的URL将这n_display个电影的海报一起显示出来:

from IPython.display import Image
from IPython.display import display
from IPython.display import HTML

images = ''
for i in range(n_display):
        images+="" % URL[i]

#print(images)
display(HTML(images))

Show Time!!!有意思的来了,我们来看和base_mv_idx = 0相似的5个电影:
电影推荐实例--基于协同过滤和DL特征提取的比较_第1张图片

和原文类似,也出来了《阿甘正传》,而且第三个电影也感觉完全不对劲啊,说明ratings.csv里的这届点评人不行。

改变base_mv_idx的值,再玩几轮:
电影推荐实例--基于协同过滤和DL特征提取的比较_第2张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第3张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第4张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第5张图片

这里博主遇到一个问题,就是在kernel中执行脚本,电影的图片无法显示只返回IPython的对象,但是在Jupyter中却没问题。如果你也有这个问题,可以在Jupyter中玩。如果你知道是什么原因和解决办法,也请不吝赐教。

好了,先玩到这,直观上讲推荐效果不太好,总结一下基于协同过滤的推荐系统的弱点:

  • 协同过滤方法通过使用数据,来发现类似的用户和电影,这将导致热门电影比小众电影更容易被推荐。
  • 由于新上映的电影没有太多的使用数据,指望协同过滤向用户推荐任何新电影很不现实。

接下来,我们考虑采用另一种方法来处理协同过滤问题——用深度学习推荐电影。

深度学习

以下搞事情的出发点:相似类型的电影具有风格相似的海报,以此为原理寻找相似的海报来推荐电影。借用原作者的话:“我们甚至可以仅仅根据海报字体,来推测这个电影的情绪”。这个论断我们不去推敲其普适性,但可以说一定程度上是有道理的。

深度学习现在火得不得了,当然用cnn来提取feature也确实厉害。我们采用Keras框架(基于Theano或TensorFlow,封装程度较高),选取VGG16做模型,不进行调参直接采用官方依据ImageNet训练好的参数,对海报进行feature提取。再将生成的feature flaten成向量,然后就和上面一样了,计算不同feature的余弦相似度,得到最相似的电影。

因为要基于海报进行特征提取,所以就需要尽可能多的扒下来存到本地。由于是CPU计算并没有GPU加速,所以博主就只扒了1000个海报下来,用了半个小时吧。

先计算得到要下载1000个电影的IMDB id并保存在list中:

#!/usr/bin/env python3
import pandas as pd
import numpy as np
import requests
import json
import urllib
import os.path

link_f    = 'ml-latest-small/links.csv'
poster_pt = './posters/'
download_posters = 1000

headers = {'Accept':'application/json'}
payload = {'api_key':'***'} #You can apply your own API from Movie Database
response = requests.get('http://api.themoviedb.org/3/configuration',params=payload,headers=headers)
response = json.loads(response.text)

base_url = response['images']['base_url']+'w185'

def get_poster(imdb,base_url):
    #query themovie.org API for movie poster path.
    file_path = ''
    imdb_id = 'tt0{0}'.format(imdb)
    movie_url = 'http://api.themoviedb.org/3/movie/{:}/images'.format(imdb_id)
    response = requests.get(movie_url,params=payload,headers=headers)
    try:
        file_path = json.loads(response.text)['posters'][0]['file_path']
    except:
        print('Failed to get url for imdb: {0}'.format(imdb))

    return base_url+file_path

df_id = pd.read_csv(link_f,sep=',')
idx_to_mv = {}
for row in df_id.itertuples():
    idx_to_mv[row[1]-1] = row[2]

mvs = [0]*len(idx_to_mv.keys())
for i in range(len(mvs)):
    if i in idx_to_mv.keys() and len(str(idx_to_mv[i])) == 6:
        mvs[i] = idx_to_mv[i]
mvs = list(filter(lambda imdb:imdb!=0,mvs))
mvs = mvs[:download_posters]
total_mvs = len(mvs)

用urllib模块下载电影海报,并且为了方便多次执行,加入code判断海报是否存在,要是存在的话就跳过,不要浪费时间再下一次:

URL = [0]*total_mvs
URL_IMDB = {'url':[],'imdb':[]}

i = 0
for m in mvs:
    if(os.path.exists(poster_pt+str(i)+'.jpg')):
        print('Skip downloading exists jpg: {0}.jpg'.format(poster_pt+str(i)))
        i += 1
        continue
    URL[i] = get_poster(m,base_url)
    if(URL[i] == base_url):
        print('Bad imdb id: {0}'.format(m))
        mvs.remove(m)
        continue
    print('No.{0}: Downloading jpg(imdb {1}) {2}'.format(i,m,URL[i]))
    urllib.request.urlretrieve(URL[i],poster_pt+str(i)+'.jpg')
    URL_IMDB['url'].append(URL[i])
    URL_IMDB['imdb'].append(m)
    i += 1

引入Keras VGG16模型,这里的VGG16舍掉了最后的3层FC层,因为我们的出发点就是采用cnn来提取特征,而不需要分类。对每一个海报数据进行预处理,然后喂入模型计算提取(预测)后的特征。这里同样也加入了code来保存模型对每一个海报预测后的特征向量,保存到“prediction_result.csv”中,因为1000个计算还是需要些时间的。相对的加入code判断“prediction_result.csv”是否存在,存在的话以后执行就直接读进来不需要再计算一遍了。

from keras.applications import VGG16
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing import image as kimage

image = [0]*total_mvs
x     = [0]*total_mvs
prediction_result = 'prediction_result.csv'

def save_predict_res(matrix_res):
    pe = dict([(x,[]) for x in range(matrix_res.shape[0])])
    for i in range(matrix_res.shape[0]):
        pe[i].extend(matrix_res[i])
    df = pd.DataFrame(data=pe)
    df.to_csv(prediction_result)

def load_result_matrix(file):
    if not os.path.exists(file):
        return None
    m_r = pd.read_csv(file,sep=',')
    f_r = np.zeros((m_r.shape[1]-1,m_r.shape[0]))
    for i in range(m_r.values.shape[1]-1):
        f_r[i] = m_r[str(i)].values.tolist()
    return f_r

matrix_res = load_result_matrix(prediction_result)
if matrix_res is None:
    for i in range(total_mvs):
        image[i] = kimage.load_img(poster_pt+str(i)+'.jpg',target_size=(224,224))
        x[i] = kimage.img_to_array(image[i])
        x[i] = np.expand_dims(x[i],axis=0)
        x[i] = preprocess_input(x[i])

    model = VGG16(include_top=False,weights='imagenet')
    prediction = [0]*total_mvs
    matrix_res = np.zeros([total_mvs,25088])
    for i in range(total_mvs):
        prediction[i]=model.predict(x[i]).ravel()
        matrix_res[i,:] = prediction[i]

    save_predict_res(matrix_res)

计算代表电影海报的特征向量间的余弦相似性:

#Calculate cosine similarity
similarity_deep = matrix_res.dot(matrix_res.T)
norms = np.array([np.sqrt(np.diagonal(similarity_deep))])
similarity_deep = (similarity_deep/(norms*norms.T))

计算与电影base_mv_idx最相似的n_display个海报:

n_display   = 5
base_mv_idx = 0
mv = [x for x in np.argsort(similarity_deep[base_mv_idx])[:-n_display-1:-1]]

显示这些海报:

from IPython.display import Image
from IPython.display import display
from IPython.display import HTML

images = ''
for i in range(len(mv)):
    images+="".format(poster_pt+str(mv[i]))

display(HTML(images))

好了,又到激动人心的时刻了,我们来看看效果怎么样:
电影推荐实例--基于协同过滤和DL特征提取的比较_第6张图片

全是动画片海报,风格也类似,不得不说cnn提取feature确实厉害。

改变base_mv_idx的值,再玩几轮:
电影推荐实例--基于协同过滤和DL特征提取的比较_第7张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第8张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第9张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第10张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第11张图片
电影推荐实例--基于协同过滤和DL特征提取的比较_第12张图片

可以看到,海报的相似性相当不错。因此能够得出结论:利用DL学习特征,来预测相似性的直观准确率是很高的。不过这里需要注意一点,就是海报库要尽可能的大,不然如果根本没有那么多相似的海报却非要找那么多相似的,强扭的瓜不甜的。

总结

在推荐系统中有几种使用深度学习的方法:

  • 无监督学习
  • 从协同过滤中预测潜在特征
  • 将深度学习生成的特征作为辅助信息

电影海报具有创造噱头和兴趣的视觉元素。这个项目中,我们使用了无监督深度学习,通过海报来学习电影的相似性。显然,这只是在推荐系统中使用深度学习的第一步,我们还可以尝试很多东西。例如,我们可以用深度学习来预测协同过滤生成的潜在特征。Spotify的音乐推荐也使用了类似的方法,区别于图像处理,他们通过处理歌曲的声音,来用深度学习来预测协同过滤中的潜在特征。还有一个可能的方向。是把深度学习学到的特征作为辅助信息,来提高预测的准确性。

项目GitHub:https://github.com/elliotzhao/Recommend-movies


你可能感兴趣的:(机器学习,深度学习,python)