這學期選了一門名叫《web智能與社交運算》的課,老師最後偷懶,最後的課程project作業直接讓我們參加百度的一個電影推薦系統演算法大賽,然後以在這個比賽中的成績作為這門課大作業的成績。不過,最終的結果並不需要百度官方的評估,只需要我們的即可(參看百度雲平台),例如下面這個:
上面最重要的就是RMSE的數值,數值越小代表偏差越小,百度熱門排行榜就是按值從小到大來排列的,這些人使用的可能是比SVD更好的演算法,即使這樣達到一定範圍後再想進步就很難了,估計不會有人低於0.6這個值。
言歸正傳,下面來說說針對百度這個比賽如何如何用SVD來實現推薦系統,為了瞭解基本原理可以看看這篇文章:推薦系統相關演算法(1):SVD (後面提到的三篇論文也值得一讀)
1、資料預先處理
本課程的要求是只完成任務一即可,做任務一的時候只需要用到兩個資料集,一個是訓練資料training_set,一個是待預測的資料predict,百度的要求如下:
l 任務一
n training_set.txt使用者評分資料共三列,從左至右依次為userId、movieId、rating。即使用者id、電影id、該使用者對該電影的評分。列之間以’\t’分隔,行之間以’\r\n’分隔。
n predict.txt為預測集合,共兩列。從左至右依次是userId,movieId。即使用者id,電影id.列之間以’\t’分隔,行之間以’\r\n’分隔。參賽者需要預測出第三列,即該使用者對該電影的評分,作為第三列,並提交給評測平台。需要注意的是,參賽者最終提的predict.txt是三列,列之間以’\t’分隔,行之間以’\r\n’分隔。行之間的順序不能亂,行的總數不能少。
下載下來以後探索資料並不是從0開始計數的,training_set格式如下:
72454819627294.072454813564054.072454818363834.072454812845504.072454817235814.072454818273054.072454815727864.072454814736904.0...............................................................
predict集資料格式如下:
72454817941717245481381060724548177600272454819807057245481354292724548173873572454816245617245481985808724548137834972454817782697245481242057.........................................................
在使用SVD的時候需要用數組儲存每一個使用者和每一個電影,這裡使用者的ID都是7位,電影的都是6位,如果直接開個七八位大數組來儲存記憶體估計不夠,而且還浪費空間,為此需要先把使用者和電影ID進行映射到從0開始一個較小的範圍內。
userMap = {}movieMap = {}with open('training_set.txt') as fp: fp_user = open('usermap.txt', 'w') fp_movis = open('moviemap.txt', 'w') fp_out = open('smallMatrix.txt', 'w') fp_prediction = open('test.txt', 'r') fp_out2 = open('smallPredictionMatrix.txt','w') for line in fp: line = line.strip() if line == '': continue tup = line.split() raw_user = tup[0] raw_movie = tup[1] rate = float(tup[2]) if raw_user not in userMap: userMap[raw_user] = len(userMap.keys()) user_id = userMap[raw_user] if raw_movie not in movieMap: movieMap[raw_movie] = len(movieMap.keys()) movie_id = movieMap[raw_movie] fp_out.write('{0} {1} {2}\n'.format(user_id, movie_id, rate)) for raw_user, user_id in userMap.items(): fp_user.write('{0} {1}\n'.format(raw_user, user_id)) for raw_movie, movie_id in movieMap.items(): fp_movis.write('{0} {1}\n'.format(raw_movie, movie_id)) for line2 in fp_prediction: line2 = line2.strip() if line2 == '': continue tup2 = line2.split() raw_user2 = tup2[0] raw_movie2 = tup2[1] user_id2 = userMap[raw_user2] movie_id2 = movieMap[raw_movie2] fp_out2.write('{0} {1}\n'.format(user_id2, movie_id2))
上面代碼實現了使用者ID和電影ID的映射過程,實現思想很簡單,來一個ID,看看之前是否存在過,如果存在用那個值替換,如果不存在新加入一個,這裡是從0開始儲存的,處理完以後得到四個檔案,使用者ID,電影ID的索引值對,處理完的小範圍訓練集,處理完的小範圍預測集。
小範圍的訓練集如下:
0 0 4.00 1 4.00 2 4.00 3 4.00 4 4.00 5 4.00 6 4.00 7 4.00 8 4.00 9 4.00 10 4.00 11 4.00 12 4.00 13 4.00 14 4.00 15 4.00 16 4.00 17 4.00 18 4.00 19 4.00 20 4.00 21 4.00 22 4.00 23 4.00 24 4.00 25 4.00 26 4.00 27 4.00 28 4.00 29 4.00 30 4.00 31 4.00 32 4.00 33 4.00 34 4.00 35 4.00 36 4.00 37 4.0...
小範圍預測集如下:
0 6170 5670 5750 12110 17350 12550 6200 7950 8900 7060 5990 12480 16510 6210 19960 10030 2347...
經過這步處理,就不需要再開大數組儲存,統計下來不同的使用者數,電影數都不到一萬個。
2、利用SVD進行訓練
得到了小規模資料,修改svd.conf檔案裡面的值,這裡avarageScore這個值需要自己計算後填入,userNum,itemNum是使用者和電影數目的範圍,後面幾個值也並不是固定的,可以根據實際情況進行修改
3.579231 10000 10000 10 0.01 0.05averageScore userNum itemNum factorNum learnRate regularization
然後運行svd.py檔案即可,我們這裡訓練資料時迭代一次一般需要十五秒的時間,顯然訓練是很耗時的,為了簡單就迭代了五次而已。
3、預測資料
預測資料其實就是矩陣的乘法運算,相比訓練來說速度要快很多。這裡我們參考上面那篇部落格裡面的代碼,把訓練和預測放在一起執行,最終代碼如下:
import mathimport randomimport cPickle as pickle#calculate the overall averagedef Average(fileName):fi = open(fileName, 'r')result = 0.0cnt = 0for line in fi:cnt += 1arr = line.split()result += int(arr[2].strip())return result / cntdef InerProduct(v1, v2):result = 0for i in range(len(v1)):result += v1[i] * v2[i]return resultdef PredictScore(av, bu, bi, pu, qi):pScore = av + bu + bi + InerProduct(pu, qi)if pScore < 1:pScore = 1elif pScore > 5:pScore = 5return pScore#def SVD(configureFile, testDataFile, trainDataFile, modelSaveFile):def SVD(configureFile, trainDataFile, modelSaveFile):#get the configurefi = open(configureFile, 'r')line = fi.readline()arr = line.split()averageScore = float(arr[0].strip())userNum = int(arr[1].strip())itemNum = int(arr[2].strip())factorNum = int(arr[3].strip())learnRate = float(arr[4].strip())regularization = float(arr[5].strip())fi.close()bi = [0.0 for i in range(itemNum)]bu = [0.0 for i in range(userNum)]temp = math.sqrt(factorNum)qi = [[(0.1 * random.random() / temp) for j in range(factorNum)] for i in range(itemNum)]pu = [[(0.1 * random.random() / temp) for j in range(factorNum)] for i in range(userNum)]print("initialization end\nstart training\n")#train modelpreRmse = 1000000.0for step in range(5):fi = open(trainDataFile, 'r')for line in fi:arr = line.split()uid = int(arr[0].strip()) - 1iid = int(arr[1].strip()) - 1score = int(arr[2].strip())prediction = PredictScore(averageScore, bu[uid], bi[iid], pu[uid], qi[iid])eui = score - prediction#update parametersbu[uid] += learnRate * (eui - regularization * bu[uid])bi[iid] += learnRate * (eui - regularization * bi[iid])for k in range(factorNum):temp = pu[uid][k]#attention here, must save the value of pu before updatingpu[uid][k] += learnRate * (eui * qi[iid][k] - regularization * pu[uid][k])qi[iid][k] += learnRate * (eui * temp - regularization * qi[iid][k])fi.close()#learnRate *= 0.9#curRmse = Validate(testDataFile, averageScore, bu, bi, pu, qi)#print("test_RMSE in step %d: %f" %(step, curRmse))#if curRmse >= preRmse:#break#else:#preRmse = curRmse#write the model to filesfo = file(modelSaveFile, 'wb')pickle.dump(bu, fo, True)pickle.dump(bi, fo, True)pickle.dump(qi, fo, True)pickle.dump(pu, fo, True)fo.close()print("model generation over")#validate the modeldef Validate(testDataFile, av, bu, bi, pu, qi):cnt = 0rmse = 0.0fi = open(testDataFile, 'r')for line in fi:cnt += 1arr = line.split()uid = int(arr[0].strip()) - 1iid = int(arr[1].strip()) - 1pScore = PredictScore(av, bu[uid], bi[iid], pu[uid], qi[iid])tScore = int(arr[2].strip())rmse += (tScore - pScore) * (tScore - pScore)fi.close()return math.sqrt(rmse / cnt)#use the model to make predictdef Predict(configureFile, modelSaveFile, testDataFile, resultSaveFile):#get parameterfi = open(configureFile, 'r')line = fi.readline()arr = line.split()averageScore = float(arr[0].strip())fi.close()#get modelfi = file(modelSaveFile, 'rb')bu = pickle.load(fi)bi = pickle.load(fi)qi = pickle.load(fi)pu = pickle.load(fi)fi.close()#predictfi = open(testDataFile, 'r')fo = open(resultSaveFile, 'w')for line in fi:arr = line.split()uid = int(arr[0].strip()) - 1iid = int(arr[1].strip()) - 1pScore = PredictScore(averageScore, bu[uid], bi[iid], pu[uid], qi[iid])fo.write("%f\n" %pScore)fi.close()fo.close()print("predict over")if __name__ == '__main__':configureFile = 'svd.conf'trainDataFile = 'ml_data\\smallMatrix.txt'testDataFile = 'ml_data\\smallPredictionMatrix.txt'modelSaveFile = 'svd_model.pkl'resultSaveFile = 'prediction.txt'#print("%f" %Average("ua.base"))SVD(configureFile, trainDataFile, modelSaveFile)Predict(configureFile, modelSaveFile, testDataFile, resultSaveFile)4、資料後處理
預測結果作為一列輸出到一個單獨的檔案,按照百度的要求需要把結果插入到預測集的最後一列,用python處理一下就好
fp1 = open('predict.txt')fp2 = open('prediction.txt')fp_out = open('file3.txt', 'w')for line1, line2 in zip(fp1, fp2): line1 = line1.strip() line2 = line2.strip() fp_out.write('{0}\t{1}\n'.format(line1, line2))
最終我們就得到了可以提交到百度雲平台上面進行評價的檔案,格式如下:
72454817941713.87944072454813810604.02826272454817760024.15225172454819807053.98621772454813542923.75888472454817387353.92580472454816245613.88090572454819858083.77607872454813783493.90212872454817782693.89224272454812420573.87125872454816488983.86134072454811712183.69646972454818971363.83417672454815727853.91779572454815186613.83507572454815448403.87351972454811316203.72518572454816003533.89968472454818650193.878535..............................................................................
評價結果就是博文最開始的那張圖了。
5、改進和思考
顯然我們還能夠讓RMSE值再小一些,其實一般來說svd在訓練的時候還需要有一個測試資料來驗證訓練的好壞,但百度沒有給測試資料,這裡我們也沒弄,如果有測試資料訓練的效果可能會好一些。同時在訓練時迭代次數的選擇也有技巧,選擇太少或太多效果都可能不好,需要自己把握,我們這裡只迭代了五次顯然不夠。svd.conf裡面最後三個參數的選擇應該還存在技巧。也許還存在其他一些改進的方法,例如考慮到使用者之間的關係這些,不過那個處理起來就有點複雜了,任務二貌似就要考慮到這一點。
總的來說,利用SVD僅僅迭代五次就能有這樣的結果確實讓人驚訝,想法簡單但結果卻不錯,看來簡單的並不意外著是不好,一些問題的完美解決往往蘊含在簡單之中。