使用pytorch框架實現使用FM模型在movielen數據集上的電影評分預測(rendle的工作)


一、FM介紹

(1)實驗的主要任務:使用FMmovielen數據集上進行電影評分預測任務(rendle的工作,經典的特征選擇

(2)參考論文:Factorization Machines

(3)部署環境:python37 + pytorch1.3

(4)數據集:Movielensmall數據集,使用的rating.csv文件。數據集按照8:2的比例進行划分,隨機挑選80%的數據當做訓練集,剩余的20%當做測試集。

從數據集中選取的特征包括:userId , movieId , lastmovie , rating

lastmovie數據的構造過程為:將數據集按照userId進行排序,在對於每一個用戶按照時間戳進行排序,找出對應於某個電影的上一個電影的movieId

(5)代碼結構:

進行數據預處理以及數據划分的代碼在divideData.py文件中,划分之后得到rating_train.csvrating_test.csv兩個文件。(data文件夾下的ratings.csv為原始數據集,其中會得到一些中間文件:ratings_sort.csv文件為按照useId以及timestamp對數據集排序后得到的文件;rating_addLastMovie.csv文件為增加用戶看的上一部電影的movieId得到文件;ratingsNoHead.csv文件為去掉數據集的表頭得到的文件。)

fm_model.py文件是讀取訓練集以及測試集,並使用pytorch框架編寫FM訓練模型,最后使用rmse作為評價指標,使用測試集對模型進行測試。模型訓練過程中采用batch對數據集進行分批訓練,同時每訓練完一輪之后使用測試集進行測試,檢驗測試效果,並最終以曲線的形式展現出來。最終訓練集與測試集的曲線圖如下圖所示:

(6)參數的調節:

特征因子k的選取:在test_loss_for_k.py文件中含有繪制lossk的關系圖的代碼,通過觀察曲線的走向,選取合適的k值(前提是要先將loss與對應的k的數據存儲存儲到csv文件中,對應為data文件夾下的test_loss.csv文件

學習率的選取,同樣在test_loss_for_k.py文件中含有繪制loss與學習率的關系圖的代碼,通過觀察曲線的走向,選取合適的lr值。(同樣應該將loss與學習率lr的對應曲線存儲到csv文件中,對應於data文件夾下的test_loss_for_lr.csv文件

同樣的訓練次數、正則化次數也是通過這種方法進行選取。

7)評價標准:采用rmse作為評價指標,使用測試集對模型進行測試。(實驗只使用了數據集中的一部分數據,同樣也使用了完整的數據集進行了測試,測試誤差為1.2。由於數據集較大,這里只上傳使用的部分數據集。)

二、代碼 

1.代碼結構:

 2.divideData.py代碼:

# coding: utf-8
"""
該文件主要是對數據進行預處理,將評分數據按照8:2分為訓練數據與測試數據
"""
import pandas as pd
import csv
import os

#將文件中的數據按照userId進行排序,如果userId相同則按照timestamp進行排序
origin_f = open('data/ratings.csv','rt',encoding='utf-8',errors="ignore")
new_f= open('data/ratings_sort.csv','wt',encoding='utf-8',errors="ignore",newline="")
reader=csv.reader(origin_f)
writer=csv.writer(new_f)
sortedlist=sorted(reader,key=lambda x:(x[0],x[3]),reverse=True)
print(sortedlist.__len__())
i=0
for row in sortedlist:
    if i==0:
        # 添加表頭
        writer.writerow(('userId','movieId','rating','timestamp'))
    if(i==(sortedlist.__len__()-1)):
        continue
    writer.writerow(row)
    i=i+1
origin_f.close()
new_f.close()

#增加last_movie字段
csvfile1=open('data/ratings_sort.csv','rt')
reader=csv.DictReader(csvfile1)
rows=[row for row in reader]
# print(rows[0].get("userId"))
i=0
csvfile2=open('data/ratings_sort.csv','rt')
reader=csv.DictReader(csvfile2)
rating_addLastMovie=open('data/rating_addLastMovie.csv','wt',encoding='utf-8',errors="ignore",newline="")
writer=csv.writer(rating_addLastMovie)
for row in reader:
    # 添加用戶看得上一部電影
    if i<(rows.__len__()-1) and rows[i].get("userId")==rows[i+1].get("userId"):
        row["last_movie"]=rows[i+1].get("movieId")
    else:
        row["last_movie"]=0
    # print(row.values())
    if i==0:
        writer.writerow(('userId', 'movieId', 'rating', 'timestamp','last_movie'))
    writer.writerow(row.values())
    i=i+1
csvfile1.close()
csvfile2.close()
rating_addLastMovie.close()

# 刪除文件中的表頭
origin_f = open('data/rating_addLastMovie.csv','rt',encoding='utf-8',errors="ignore")
new_f = open('data/ratingsNoHead.csv','wt+',encoding='utf-8',errors="ignore",newline="")
reader = csv.reader(origin_f)
writer = csv.writer(new_f)
i=0
for i,row in enumerate(reader):
    if i>1:
        writer.writerow(row)
origin_f.close()
new_f.close()

#將數據按照8:2的比例進行划分得到訓練數據集與測試數據集
df = pd.read_csv('data/ratingsNoHead.csv', encoding='utf-8')
# df.drop_duplicates(keep='first', inplace=True)  # 去重,只保留第一次出現的樣本
# print(df)
df = df.sample(frac=1.0)  # 全部打亂
cut_idx = int(round(0.2 * df.shape[0]))
df_test, df_train = df.iloc[:cut_idx], df.iloc[cut_idx:]
# 打印數據集中的數據記錄數
print(df.shape,df_test.shape,df_train.shape)
# print(df_train)
# 將數據記錄存儲到csv文件中
# 存儲訓練數據集
df_train=pd.DataFrame(df_train)
df_train.to_csv('data/ratings_train_tmp.csv',index=False)
# 由於一些不知道為什么的原因,使用pandas讀取得到的數據多了一行,在存儲時也會將這一行存儲起來,所以應該刪除這一行(如果有時間在查一查看能不能解決這個問題)
origin_f = open('data/ratings_train_tmp.csv','rt',encoding='utf-8',errors="ignore")
new_f = open('data/ratings_train.csv','wt+',encoding='utf-8',errors="ignore",newline="")     #必須加上newline=""否則會多出空白行
reader = csv.reader(origin_f)
writer = csv.writer(new_f)
for i,row in enumerate(reader):
    if i>0:
        writer.writerow(row)
origin_f.close()
new_f.close()
os.remove('data/ratings_train_tmp.csv')
# 存儲測試數據集
df_test=pd.DataFrame(df_test)
df_test.to_csv('data/ratings_test_tmp.csv',index=False)
origin_f = open('data/ratings_test_tmp.csv','rt',encoding='utf-8',errors="ignore")
new_f = open('data/ratings_test.csv','wt+',encoding='utf-8',errors="ignore",newline="")
reader = csv.reader(origin_f)
writer = csv.writer(new_f)
for i,row in enumerate(reader):
    if i>0:
        writer.writerow(row)
origin_f.close()
new_f.close()
os.remove('data/ratings_test_tmp.csv')

3. fm_model代碼:

import pandas as pd
import torch as pt
import torch.utils.data as Data
import matplotlib.pyplot as plt
from sklearn.feature_extraction import DictVectorizer

BATCH_SIZE=15

cols=['user','item','rating','timestamp','last_movie']
train = pd.read_csv('data/ratings_train.csv', encoding='utf-8',names=cols)
test = pd.read_csv('data/ratings_test.csv', encoding='utf-8',names=cols)

train=train.drop(['timestamp'],axis=1)   #時間戳是不相關信息,可以去掉
test=test.drop(['timestamp'],axis=1)

# DictVectorizer會把數字識別為連續特征,這里把用戶id、item id和lastmovie強制轉為 catogorical identifier
train["item"]=train["item"].apply(lambda x:"c"+str(x))
train["user"]=train["user"].apply(lambda  x:"u"+str(x))
train["last_movie"]=train["last_movie"].apply(lambda  x:"l"+str(x))

test["item"]=test["item"].apply(lambda x:"c"+str(x))
test["user"]=test["user"].apply(lambda x:"u"+str(x))
test["last_movie"]=test["last_movie"].apply(lambda  x:"l"+str(x))

# 在構造特征向量時應該不考慮評分,只考慮用戶數和電影數
train_no_rating=train.drop(['rating'],axis=1)
test_no_rating=test.drop(['rating'],axis=1)
all_df=pd.concat([train_no_rating,test_no_rating])
# all_df=pd.concat([train,test])
data_num=all_df.shape
print("all_df shape",all_df.shape)
# 打印前10行
# print("all_df head",all_df.head(10))

# 進行特征向量化,有多少特征,就會新創建多少列
vec=DictVectorizer()
vec.fit_transform(all_df.to_dict(orient='record'))
# 合並訓練集與驗證集,是為了one hot,用完可以釋放
del all_df

x_train=vec.transform(train.to_dict(orient='record')).toarray()
x_test=vec.transform(test.to_dict(orient='record')).toarray()
# print(vec.feature_names_)   #查看轉換后的別名
print("x_train shape",x_train.shape)
print("x_test shape",x_test.shape)
#
y_train=train['rating'].values.reshape(-1,1)
y_test=test['rating'].values.reshape(-1,1)
print("y_train shape",y_train.shape)
print("y_test shape",y_test.shape)
#
n,p=x_train.shape
#
train_dataset = Data.TensorDataset(pt.tensor(x_train),pt.tensor(y_train))
test_dataset=Data.TensorDataset(pt.tensor(x_test),pt.tensor(y_test))

# 訓練集分批處理
loader = Data.DataLoader(
    dataset=train_dataset,      # torch TensorDataset format
    batch_size=BATCH_SIZE,      # 最新批數據
    shuffle=False           # 是否隨機打亂數據
)
# 測試集分批處理
loader_test=Data.DataLoader(
    dataset=test_dataset,
    batch_size=BATCH_SIZE,
    shuffle=False
)

class FM_model(pt.nn.Module):
    def __init__(self,p,k):
        super(FM_model,self).__init__()
        self.p=p    #feature num
        self.k=k     #factor num
        self.linear=pt.nn.Linear(self.p,1,bias=True)   #linear part
        self.v=pt.nn.Parameter(pt.rand(self.k,self.p))  #interaction part

    def fm_layer(self,x):
        #linear part
        linear_part=self.linear(x.clone().detach().float())
        #interaction part
        inter_part1 = pt.mm(x.clone().detach().float(), self.v.t())
        inter_part2 = pt.mm(pt.pow(x.clone().detach().float(), 2), pt.pow(self.v, 2).t())
        pair_interactions=pt.sum(pt.sub(pt.pow(inter_part1,2),inter_part2),dim=1)
        output=linear_part.transpose(1,0)+0.5*pair_interactions
        return output

    def forward(self, x):
        output=self.fm_layer(x)
        return output

k=5   #因子的數目
fm=FM_model(p,k)
fm
# print("paramaters len",len(list(fm.parameters())))
# # print(list(fm.parameters()))
# for name,param in fm.named_parameters():
#     if param.requires_grad:
#         print(name)

#評價指標rmse
def rmse(pred_rate,real_rate):
    #使用均方根誤差作為評價指標
    loss_func=pt.nn.MSELoss()
    mse_loss=loss_func(pred_rate,pt.tensor(real_rate).float())
    rmse_loss=pt.sqrt(mse_loss)
    return rmse_loss

#訓練網絡
learing_rating=0.05
optimizer=pt.optim.SGD(fm.parameters(),lr=learing_rating)   #學習率為
loss_func= pt.nn.MSELoss()
loss__train_set=[]
loss_test_set=[]
for epoch in range(35):      #對數據集進行訓練
    # 訓練集
    for step,(batch_x,batch_y) in enumerate(loader):     #每個訓練步驟
        #此處省略一些訓練步驟
        optimizer.zero_grad()  # 如果不置零,Variable的梯度在每次backwrd的時候都會累加
        output = fm(batch_x)
        output = output.transpose(1, 0)
        # 平方差
        rmse_loss = rmse(output, batch_y)
        l2_regularization = pt.tensor(0).float()
        # 加入l2正則
        for param in fm.parameters():
            l2_regularization += pt.norm(param, 2)
        # loss = rmse_loss + l2_regularization
        loss = rmse_loss
        loss.backward()
        optimizer.step()  # 進行更新
    # 將每一次訓練的數據進行存儲,然后用於繪制曲線
    loss__train_set.append(loss)

    #測試集
    for step,(batch_x,batch_y) in enumerate(loader_test):     #每個訓練步驟
        #此處省略一些訓練步驟
        optimizer.zero_grad()  # 如果不置零,Variable的梯度在每次backwrd的時候都會累加
        output = fm(batch_x)
        output = output.transpose(1, 0)
        # 平方差
        rmse_loss = rmse(output, batch_y)
        l2_regularization = pt.tensor(0).float()
        # print("l2_regularization type",l2_regularization.dtype)
        # 加入l2正則
        for param in fm.parameters():
            # print("param type",pt.norm(param,2).dtype)
            l2_regularization += pt.norm(param, 2)
        # loss = rmse_loss + l2_regularization
        loss = rmse_loss
        loss.backward()
        optimizer.step()  # 進行更新
    # 將每一次訓練的數據進行存儲,然后用於繪制曲線
    loss_test_set.append(loss)

plt.clf()
plt.plot(range(epoch+1),loss__train_set,label='Training data')
plt.plot(range(epoch+1),loss_test_set,label='Test data')
plt.title('The MovieLens Dataset Learing Curve')
plt.xlabel('Number of Epochs')
plt.ylabel('RMSE')
plt.legend()
plt.grid()
plt.show()
print("train_loss",loss)
# print(y_train[0:5],"  ",output[0:5])
# 保存訓練好的模型
pt.save(fm.state_dict(),"data/fm_params.pt")
test_save_net=FM_model(p,k)
test_save_net.load_state_dict(pt.load("data/fm_params.pt"))
#測試網絡
pred=test_save_net(pt.tensor(x_test))
pred=pred.transpose(1,0)
rmse_loss=rmse(pred,y_test)
print("test_loss",rmse_loss)
# print(y_test[0:5],"  ",pred[0:5])

# 存儲test_loss與k及學習率的數據到文件中,來繪制曲線
# new_f= open('data/test_loss_for_lr.csv','a',encoding='utf-8',errors="ignore",newline="")
# writer=csv.writer(new_f)
# writer.writerow((learing_rating,rmse_loss.detach().numpy()))
# new_f.close()

4. test_loss_for_k.py代碼:

import matplotlib.pyplot as plt
import pandas as pd

# 繪制loss與k的關系圖
# cols=['k','loss']
# loss_for_k=pd.read_csv('data/test_loss.csv', encoding='utf-8',names=cols)
# k=loss_for_k["k"]
# loss=loss_for_k['loss']
# epoch=loss.shape[0]
# plt.plot(k,loss,marker='o',label='loss data')
# plt.title('loss for k')
# plt.xlabel('k')
# plt.ylabel('loss')
# plt.legend()
# plt.show()

# 繪制loss與學習率的關系圖
cols=['lr','loss']
loss_for_k=pd.read_csv('data/test_loss_for_lr.csv', encoding='utf-8',names=cols)
lr=loss_for_k["lr"]
loss=loss_for_k['loss']
epoch=loss.shape[0]
plt.plot(lr,loss,marker='o',label='loss data')
plt.title('loss for lr')
plt.xlabel('lr')
plt.ylabel('loss')
plt.legend()
plt.show()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM