推薦系統學習-特征工程(LR,FM)-代碼


在一口氣看完項亮老師的《推薦系統實踐》后,又花費幾天看完了王喆老師的《深度學習推薦系統》,雖然學過一門深度學習的課,但是直接看推薦系統的深度學習還是有點不懂的(手動狗頭×)。在上一篇的協同過濾后,這一篇來記錄協同過濾后推薦系統的發展,也就是特征工程
簡單實例
(圖片有點大,可右鍵點擊查看)

推薦系統前沿 之 特征工程

協同過濾只使用了用戶和物品的信息,缺少了其他的內容、社交、上下文信息,導致推薦結果不理想,所以引入LogisticRegression模型(LR),在預測用戶是否點擊推薦物品時,加上用戶和物品的其他信息。接下來的發展,普通的特征直接構建模型轉變成對特征進行交叉處理獲得更有表達能力的特征。普通的二階交叉就是接下來我實現的FM(因子分解機),高階交叉有GBDT+LR和神經網絡(前面幾層就相當於是對特征進行處理,最后面一層是利用處理好的特征進行判斷)。

基本概念

  • CTR:Click Through Rate,點擊率問題,用戶是否點擊為最終目標。對於一條用戶、物品和上下文信息的輸入,用戶若是觀看了則真實輸出為1。也就是說將推薦問題轉換為點擊率預估問題。
    但是一般說來,用戶日志記錄中只存在正樣本。也就是說對於一名用戶,知道他看了什么,換句話說知道了他喜歡看什么,但是對於一個未知的物品,並不能准確判斷這名用戶的態度,即缺少負樣本。
  • 負樣本抽取:在《推薦系統實踐》中介紹了並且評估了四種方法。
方法 效果
對於一個用戶,用他所沒有過行為的物品作為負樣本 1
對於一個用戶,他所沒有過行為的物品中均勻采樣出一些物品作為負樣本 3
對於一個用戶,他所沒有過行為的物品中采樣出一些物品作為負樣本,並保證數目相當 4
對於一個用戶,他所沒有過行為的物品中采樣出一些物品作為負樣本,並偏重那些不熱門的物品 2

在KDD Cup的Yahoo!Music推薦系統比賽中,發現了負樣本抽取的原則:一是要保證對於每個用戶,正負樣本平衡,二是對於每個用戶的負樣本采樣時要選取那些很熱門但是用戶卻沒有行為的物品。

邏輯回歸

公式:\(\hat{y}=\sum_{i=1}^n w_ix_i\)
代碼和過程之前我的理解:將推薦問題轉化為CTR問題后,就會發現其實就是一種分類問題。只是相比於常見的分類,輸入的特征不同,需要的處理方法不同而已。
推薦過程:
(1) 將用戶年齡、性別、物品屬性、物品描述、當前時間、當前地點等特征轉換成數值型向量。(十分簡單的轉換,深度學習大部分利用的Embedding技術)
(2) 確定邏輯回歸模型的優化目標(以優化“點擊率”為例),利用已有樣本數據對邏輯回歸進行訓練,確定邏輯回歸模型的內部參數。(這里就要用到負樣本抽取)
(3) 在模型服務階段,將特征向量輸入邏輯回歸模型,經過邏輯回歸模型好的推斷,得到用戶“點擊”物品的概率。(模型輸出\(\hat{y}\)
(4) 對於每個用戶,利用點擊概率進行排序,得到推薦列表。(最終推薦精確率來自於推薦列表的元素有多少在驗證集中,因為驗證集是用戶真實點擊了的)
我利用pytorch實現了這個模型。

import pandas as pd
import numpy as np
from tqdm import tqdm
import torch as t 
import torch.nn as nn 
import torch.nn.functional as F 
from datetime import *
import matplotlib.pyplot as plt 
import ipdb
import fire
import random
import copy

def Get_occupation2number(file='./ml-100k/u.occupation'):
    '''
    職業轉換成數值方便模型處理
    @params
    file:職業對應的文件
    @return
    table:職業轉換表
    '''
    table=pd.read_csv(file,header=None)
    table['index']=range(table.shape[0])
    table=table.set_index([0])
    return table

def Get_movie2number(file='./ml-100k/u.genre'):
    '''
    電影類型轉換成數值方便模型處理
    @params
    file:電影類型對應的文件
    @return
    table:電影類型轉換表
    '''
    table=pd.read_csv(file,header=None,delimiter='|')
    table=table.set_index([0])
    return table

occupation2number=Get_occupation2number()
movie2number=Get_movie2number()

def Get_user_table(file='./ml-100k/u.user',occupation2number=occupation2number):
    '''
    獲得用戶信息表,用戶id為索引
    @params
    file:用戶信息表對應文件
    occupation2number:職業轉換表,將user信息中的職業轉成數值
    @return
    table:用戶信息表
    '''
    table=pd.read_csv(file,header=None,delimiter='|')
    table.columns=['user','age','gender','occupation','zip_code']
    table['gender']=table['gender'].map(lambda x: (1 if x=='M' else 0))
    table['occupation']=table['occupation'].map(lambda x:occupation2number.loc[x,'index'])
    table['big_mailset']=table['zip_code'].map(lambda x:(int(x[0]) if x.isdigit() else 5))
    table['medium_mailset']=table['zip_code'].map(lambda x:(int(x[1:3]) if x.isdigit() else 54))
    table['small_emailset']=table['zip_code'].map(lambda x:(int(x[3:]) if x.isdigit() else 14))
    table.drop(['zip_code'],axis=1,inplace=True)
    table['g_user']=table['user']  # 用戶id也是一個重要的特征
    table=table.set_index(['user'])
    mean=table.mean(axis=0)
    std=table.std(axis=0)
    table=(table-mean)/std
    return table

def Get_item_table(file='./ml-100k/u.item',movie2number=movie2number):
    '''
    獲得物品信息表,物品id為索引
    @params
    file:物品信息表對應文件
    occupation2number:電影類型轉換表,將item中的電影類型轉成數值
    @return
    table:物品信息表
    '''
    table=pd.read_csv(file,header=None,delimiter='|',encoding='unicode-escape')
    col=['movie','title','release_date','date','url']
    col.extend(list(movie2number.index))    
    table.columns=col
    month_dict={'Jan':1,'Feb':2,'Mar':3,'Apr':4,'May':5,'Jun':6,'Jul':7,'Aug':8,'Sep':9,'Oct':10,'Nov':11,'Dec':12}
    table['release_date']=table['release_date'].fillna('01-Jan-1995')
    table['month']=table['release_date'].map(lambda x:month_dict[x.split('-')[1]])
    table['day']=table['release_date'].map(lambda x:int(x.split('-')[0]))
    table['year']=table['release_date'].map(lambda x:int(x.split('-')[2]))
    table.drop(['title','date','url','release_date'],axis=1,inplace=True)
    table['g_movie']=table['movie']  # 物品id也是一個重要特征
    table=table.set_index(['movie'])
    mean=table.mean(axis=0)
    std=table.std(axis=0)
    table=(table-mean)/std
    return table

u_table=Get_user_table()
i_table=Get_item_table()

def Get_train_data(train_file,test_file):
    '''
    獲得訓練用的數據
    @params
    train_file:訓練數據集文件
    test_file:測試數據集文件
    @return
    train:訓練數據,DataFrame格式
    test:測試數據,DataFrame格式
    '''
    train=pd.read_csv(train_file,header=None,delimiter='\t')
    test=pd.read_csv(test_file,header=None,delimiter='\t')
    col=['user','movie','ratings','timestap']
    train.columns=col
    test.columns=col
    combine=[train,test]
    for dataset in combine:
        dataset['watch_year']=dataset['timestap'].map(lambda x:date.fromtimestamp(x).year)
        dataset['watch_month']=dataset['timestap'].map(lambda x:date.fromtimestamp(x).month)
        dataset['watch_day']=dataset['timestap'].map(lambda x:date.fromtimestamp(x).day)
        dataset.drop(['timestap'],axis=1,inplace=True)
    train,test=combine
    return train,test

def Insert_neg_samples(data):
    '''
    為訓練樣本插入負樣本,這里實現的是對於每個用戶,插入差不多相同數量的負樣本呢,並沒有考慮插入樣本熱門程度
    @params
    data:待插入的數據集
    @return
    res_data:插入后的數據集
    '''
    # print('插入負樣本之前:{}'.format(data.shape))
    res_data=data.copy()
    for user in data['user'].unique():
        watched_movie=list(data.loc[(data['user']==user).tolist(),'movie'])
        l=len(watched_movie)
        count=0
        no_watched={'user':user,'movie':[],'ratings':0}
        iindex=copy.copy(list(i_table.index))
        random.shuffle(iindex)
        for movie in iindex:
            if movie in watched_movie:
                continue
            elif np.random.randint(0,10)>5:
                no_watched['movie'].append(movie)
                count+=1
                if count==l:
                    break
        no_watched_df=pd.DataFrame(no_watched,index=range(len(no_watched['movie'])))
        no_watched_df=no_watched_df.loc[:,['user','movie','ratings']]
        res_data=res_data.append(no_watched_df,ignore_index=True)
    # print('插入負樣本之后:{}'.format(res_data.shape))
    return res_data

class LR(nn.Module):
    '''
    邏輯回歸單元++
    '''
    def __init__(self,input_dim):
        super(LR,self).__init__()
        self.fc1=nn.Linear(input_dim,2)

    def forward(self,input):
        return self.fc1(input)

class rec_dataset(t.utils.data.Dataset):
    def __init__(self,filename,train=True,test=False):
        self.train_data,self.test_data=Get_train_data('./ml-100k/u{}.base'.format(filename),'./ml-100k/u{}.test'.format(filename))
        self.u_table=u_table
        self.i_table=i_table
        self.data=None
        if train:
            self.train_data=self.train_data[['user','movie','ratings']]
            self.data=Insert_neg_samples(self.train_data,i_table)
        elif test:
            self.test_data=self.test_data[['user','movie','ratings']]
            self.data=self.test_data
        else:
            print('未找到合適的數據')

    def __len__(self):
        return self.data.shape[0]

    def __getitem__(self,index):
        label=(1 if self.data.loc[index,'ratings']>0 else 0)  # 分類問題,而不是預測評分問題。TOPN問題
        user=self.data.loc[index,'user']
        movie=self.data.loc[index,'movie']
        info=Combine(user,movie)
        return t.tensor(info,dtype=t.float),t.tensor(np.array(label,dtype=np.int64))

def draw(y):
    '''
    畫出y的變化
    @params
    y:序列,array結構
    '''
    plt.plot(np.arange(1,len(y)+1),y,'go-')
    plt.xlabel('EPOCH')
    plt.ylabel('LOSS')
    plt.title('Training Loss')
    plt.show()

def Train(filename,batch_size=64,model_path=None,epoches=10,device='cpu'):
    '''
    利用插入好的樣本進行訓練
    @params
    filename:訓練的數據文件
    batch_size:每次訓練的batch大小
    model_path:模型路徑,判斷是否存在訓練好的模型
    epoches:迭代次數
    device:訓練的硬件設備
    @return
    None:但是會保存訓練好的模型
    '''
    # ipdb.set_trace()
    train_dataset=rec_dataset(filename,u_table=u_table,i_table=i_table)
    # test_dataset=rec_dataset(filename,train=False,test=True,u_table=u_table,i_table=i_table)
    
    train_dataloader=t.utils.data.DataLoader(train_dataset,batch_size=batch_size,shuffle=True,num_workers=0)
    # test_dataloader=t.utils.data.DataLoader(test_dataset,batch_size=batch_size,shuffle=True,num_workers=0)

    model=LR(30)
    if model_path!=None:
        model.load_state_dict(t.load(model_path))
    model=model.to(device)
    model.train()
    criterion=nn.CrossEntropyLoss()
    optimizer=t.optim.Adam(model.parameters())

    e_loss=[]
    for epoch in range(1,epoches+1):
        a_loss=0
        count=0
        for i,(data,label) in tqdm(enumerate(train_dataloader)):
            count+=1
            data=data.to(device)
            label=label.to(device)
            optimizer.zero_grad()
            pred=model(data)
            loss=criterion(pred.squeeze(),label.squeeze())
            loss.backward()
            optimizer.step()
            a_loss+=loss.item()
        e_loss.append(a_loss/count)

    draw(e_loss)

    t.save(model.state_dict(),'./logistic.pth')
    print('{}文件訓練完成'.format(filename))

@t.no_grad()
def Recommend(model_path,filename,N=50,device='cpu'):
    '''
    利用訓練好的模型,對每個用戶進行推薦
    @params
    model_path:訓練好的模型路徑
    filename:用戶點擊過的物品,相當於驗證集
    N:每次推薦的數目大小
    device:測試的硬件設備
    '''
    # ipdb.set_trace()
    model=LR(30)
    model.load_state_dict(t.load(model_path))
    model.to(device)

    train_data,test_data=Get_train_data('./ml-100k/u{}.base'.format(filename),'./ml-100k/u{}.test'.format(filename))

    test_data=pd.crosstab(index=test_data['user'],columns=test_data['movie'])

    rec_dict=dict()
    for user in tqdm(list(u_table.index)):
        watched=list(train_data.loc[(train_data['user']==user).tolist(),'movie'])
        rec_dict[user]=[]
        for movie in list(i_table.index):
            if movie in watched:
                continue
            score=model(t.tensor(Combine(user,movie),dtype=t.float)).view(-1,2)
            prob=F.softmax(score,dim=1)[:,0].detach()
            rec_dict[user].append((movie,prob))

        rec_dict[user]=sorted(rec_dict[user],key=lambda x:x[1])[-N:]

    pre,rec=Precision_and_Recall(rec_dict,test_data)
    print('准確率:{}     召回率:{}'.format(pre,rec))

def Precision_and_Recall(pred_dict,test):
    '''
    計算精確率和召回率
    @params
    pred_dict:為每個用戶推薦的物品列表
    test:crosstab形式,用戶真實看過的物品
    @return
    精確率和召回率
    '''
    all_pre=0
    all_rec=sum(test==1)
    shot=0
    for user in pred_dict.keys():
        if user not in list(test.index):
            continue
        for info in pred_dict[user]:
            all_pre+=1
            if info not in list(test.columns):
                continue
            if test.loc[user,info]==1:  # 只有看過才是命中
                shot+=1

    return 1.0*shot/all_pre,1.0*shot/all_rec

def Combine(user,movie):
    '''
    user->user_id,movie->movie_id,利用id組合出用於訓練的信息
    @params
    user:user_id
    movie:movie_id
    @return
    info:用於訓練的信息
    '''
    user_info=u_table.loc[user,:].values
    movie_info=i_table.loc[movie,:].values
    info=np.concatenate([user_info,movie_info],axis=0).squeeze()
    return info

因子分解機FM

基礎認識氺博客

上述的邏輯回歸模型輸入的特征無法進行“高級”的操作,比如特征交叉,特征篩選等,造成表達能力不強,不可避免造成信息的損失,故大牛們開始了新的征程--特征工程。

\ 名稱 特點 不足
👉 PLOY2 所有特征之間兩兩進行交叉 參數太多,參數數量\(n^2\)
👇 FM 每個特征有一個隱向量,兩兩交叉的特征的系數為隱向量內積 二階交叉,過高會出現組合爆炸問題
👇 FFM 引入特征域,每個特征有一組隱向量 二階交叉,過高會出現組合爆炸問題
👇 GBDT+LR 擬合GBDT讓特征進行高接交叉,最終葉子節點的排列為特征向量 容易過擬合,丟失了大量特征的數值信息
👇 DL 利用EMbedding,模型融合等方式提高模型的記憶和泛化能力 復雜

本次主要介紹FM,使用pytorch或其他框架的話就不需要計算梯度,但是我腦抽了,忘記這回事兒所以自己計算的梯度,也不知道正不正確(哭嘰嘰🐒

Math Warning

公式和參數 介紹(不你不想
\(x_i^k\) 第k個樣本的第i個特征
\(v_i\) 第i個特征對應的隱向量
\(w\) 權重,參數
\(\hat{y}=\sigma(w_0+\sum^n_ \limits{i=1}w_ix_i+\sum^n_ \limits{i=1}\sum^n_ \limits{j=1}(v_i\cdot v_j)x_ix_j)=\sigma(f(x^k))\) 前向傳播公式
\(loss=\sum^n_ \limits{k=1}[-y_klog\hat{y}_k)-(1-y_k)log(1-\hat{y}_k)]\) 損失函數
\(\frac{\partial loss}{\partial w_0}=\sum^n_ \limits{k=1}[\sigma(f(x^k))-y^k]\) loss對\(w_0\)的偏導
\(\frac{\partial loss}{\partial w_i}=\sum^n_ \limits{k=1}[\sigma(f(x^k))-y^k]x_i^k\) loss對\(w_i\)的偏導
\(\frac{\partial loss}{\partial v_i}=\sum^n_ \limits{k=1}[[\sigma(f(x^k))-y^k]\sum^n_ \limits{j=1,j\neq i}v_j(x_j^kx_i^k)]\) loss對\(v_i\)的偏導

Code Ending

根據上面公式和介紹,便可以編寫代碼了++
PS:本博客最開始的第一張圖,可以看到接下來發展的深度學習推薦算法中很多都來自於FM的靈感,但是博主在看完書后,書中也提到了后面的深度學習用到的FM中的特征和原始的FM是不同的。不同的點,我舉個栗子:原始的FM中,“性別”特征可以取值為0(男生)或者1(女生);而深度學習中的FM“性別”特征是取值為[1,0](男生)或者[0,1](女生),類似於“是否是男生”和“是否是女生”是一個特征。這是引入了特征域方便embedding。

def sigmoid(x):
    '''
    sigmoid函數,鑒於文中輸入的是array而且為了防止溢出而采取的計算方式。
    '''
    y=[(1.0/(1+np.exp(-i))) if i>=0 else (np.exp(i)/(np.exp(i)+1)) for i in x ]
    return np.array(y)

# 防止導入和處理訓練和測試集,引入全局變量
g_train=None
g_test=None
gd=False

def Generator(filename,batch_size):
    '''
    沒有采用pytorch框架,只能手動構造生成器
    @params
    filename:訓練集和測試集的文件名
    batch_size:每次訓練的時候使用的樣本大小
    '''
    # ipdb.set_trace()
    # 在函數體內改變全局變量需要在函數體內global聲明該全局變量
    global g_train
    global g_test
    global gd

    train,test=Get_train_data('./ml-100k/u{}.test'.format(filename),'./ml-100k/u{}.test'.format(filename))
    train=train[['user','movie','ratings']]
    train=Insert_neg_samples(train)
    ii=list(train.index)
    random.shuffle(ii)
    train=train.loc[ii,:]

    data=pd.merge(left=train,right=u_table,left_on='user',right_index=True)
    data=pd.merge(left=data,right=i_table,left_on='movie',right_index=True)
    label=data['ratings'].apply(lambda x:(1 if x>0 else 0))
    data.drop(['ratings'],axis=1,inplace=True)
    batches=data.shape[0]//batch_size

    if gd==False:
        g_test=pd.crosstab(index=test['user'],columns=test['movie'])
        g_train=data
        gd=True

    for i in range(batches+1):
        yield data.iloc[i*batch_size:(i+1)*batch_size,2:].T,label[i*batch_size:(i+1)*batch_size]  # 訓練的時候不傳入未標准化的user和movie

def Cross(data,dim=0):
    '''
    特征交叉
    @params
    data:用於交叉的數據,輸入n,放回n(n-1)/2
    dim:指明交叉的維度,因為在整個程序中交叉的方式有可能是兩個數相乘,也有可能是兩個向量內積
    @return
    res:交叉好的array
    '''
    l=data.shape[dim]
    res=[]
    for i in range(l):
        for j in range(i+1,l):
            if dim==0:
                res.append(data[i,:]*data[j,:])
            elif dim==1:
                res.append(np.dot(data[:,i],data[:,j]))
    return np.array(res).reshape((len(res),-1))

def Initial_args(k):
    '''
    初始化模型參數
    @params
    k:隱向量的維度,這是需要人工決定的
    @return
    args_dict:模型參數字典,包括了w0,wi,vi 
    '''
    args=list(u_table.columns)
    args.extend(list(i_table.columns))
    nums=len(args)

    args_dict=dict()
    args_dict['W0']=0
    args_dict['Wi']=np.zeros((nums,1))
    args_dict['v']=pd.DataFrame(np.random.randn(k,nums),columns=args)

    return args_dict

def Forward(x,y,args_dict,epsilon=1e-5):  
    '''
    模型前向傳播,計算損失和預估值
    @params
    x:DataFrame格式的訓練數據
    y:DataFrame格式的訓練標簽數據
    args_dict:模型參數字典
    epsilon:為了防止出現log0,加上的極小的數
    @return
    loss:該模型參數下的損失值
    y_hat:利用該模型參數下的預估值
    '''
    W0=args_dict['W0']  
    Wi=args_dict['Wi']  
    v=args_dict['v'] 
    x=x.values
    y=y.values

    cross_args=Cross(v.values,dim=1)  
    cross=Cross(x)
    y_hat=sigmoid(W0+np.sum(Wi*x,axis=0).squeeze()+np.sum(cross_args*cross,axis=0).squeeze())
    loss=np.sum(-y*np.log(y_hat+epsilon)-(1-y)*np.log(1-y_hat+epsilon))
    return loss,y_hat

def Backward(loss,x,y,y_hat,args_dict):
    '''
    模型反向傳播計算梯度
    @params
    loss:該模型參數下的損失值
    x:DataFrame格式的訓練數據
    y:DataFrame格式的訓練標簽數據
    y_hat:利用該模型參數下的預估值
    args_dict:模型參數字典
    @return
    dera_dict:模型參數梯度字典,包括了dW0,dWi,dv
    '''
    W0=args_dict['W0']  
    Wi=args_dict['Wi']  
    v=args_dict['v']  
    y=y.values

    dera_dict=dict()
    error=y_hat-y
    dera_dict['dW0']=np.sum(error)
    dera_dict['dWi']=(x.values@error).reshape(x.shape[0],1)
    dera_dict['dv']=dict()
    # 計算的梯度的方式沒有按照公式那樣一步步的算,那樣復雜度太高,而是利用的矩陣計算快的方法,盡量采用矩陣來算
    for key in v.columns:
        cum=None
        temp_x=copy.copy(x)
        xi=temp_x.loc[key,:]
        temp_x=pd.DataFrame(xi*x,index=x.index)
        temp_x.loc[key,:]=0
        temp_x=error*temp_x.values
        dera_dict['dv'][key]=np.sum(v.values@temp_x,axis=1)
    dera_dict['dv']=pd.DataFrame(dera_dict['dv'])
    return dera_dict

def Update_parameters(args_dict,dera_dict,lr=0.01):
    '''
    更新模型參數
    @params
    args_dict:待更新的模型參數字典
    dera_dict:模型參數梯度字典
    @return
    args_dict:更新完的模型參數字典
    '''
    args_dict['W0']-=(lr*dera_dict['dW0'])
    args_dict['Wi']-=(lr*dera_dict['dWi'])
    args_dict['v']-=(lr*dera_dict['dv'])
    return args_dict

def Predict(x,args_dict):
    '''
    預測點擊率
    @params
    x:DataFrame格式的預測數據
    args_dict:模型參數字典
    @return
    y_hat:對於每個樣本預測的點擊率
    '''
    W0=args_dict['W0']  
    Wi=args_dict['Wi'] 
    v=args_dict['v'] 

    cross_args=Cross(v.values,dim=1)  # (nums_c,1)
    cross=Cross(x)
    y_hat=sigmoid(W0+np.sum(Wi*x,axis=0).squeeze()+np.sum(cross_args*cross,axis=0).squeeze())
    return y_hat

def FM_train(filename,args_dict,k,batch_size,epoches):
    '''
    訓練模型
    @params
    filename:訓練文件
    args_dict:模型參數字典
    k:隱向量維度
    batch_size:每次梯度下降時樣本的數目
    epoches:迭代次數
    @return
    args_dict:訓練完的模型參數字典
    '''
    # ipdb.set_trace()
    al_loss=[]
    for epoch in range(1,epoches+1):
        gen=Generator(filename,batch_size=batch_size)
        em_loss=0
        for data,label in tqdm(gen):
            loss,y_hat=Forward(data,label,args_dict)
            dera_dict=Backward(loss,data,label,y_hat,args_dict)
            args_dict=Update_parameters(args_dict,dera_dict)
            em_loss+=loss
        al_loss.append(em_loss)
        print('第{}EPOCH LOSS:{}'.format(epoch,em_loss))
    draw(al_loss)
    return args_dict

def FM(filename='a',k=5,batch_size=64,epoches=10,N=50):
    '''
    整體的操作main函數
    '''
    args_dict=Initial_args(k)
    args_dict=FM_train(filename,args_dict,k,batch_size,epoches)

    # 給每個用戶的推薦過程
    watched_data=copy.copy(g_train)
    pred_dict=dict()
    for user in tqdm(list(u_table.index)):
        watched=list(watched_data.loc[(watched_data['user']==user).tolist(),'movie'])
        not_watched=[i for i in i_table.index if i not in watched]
        test_data=np.array([Combine(user,i) for i in not_watched]).T
        test_label=Predict(test_data,args_dict)
        pred_dict[user]=(pd.Series(test_label,index=not_watched).sort_values(ascending=False).index)[:N]

    pre,rec=Precision_and_Recall(pred_dict,g_test)
    print('准確率:{}    召回率:{}'.format(pre,rec))

總結一下:有時間用pytorch寫下這個FM。推薦系統的基礎認識,我幾乎都過了一遍了,接下來就是不斷地鞏固和嘗試新的知識了。對於推薦系統的架構,發現自己有很多不懂的地方,不過架構方面的知識可能要緩一緩(感覺深似海)。

數據集

這里將我的數據打包放在百度網盤了,需要自取。密碼: gwoa

人生此處,絕對樂觀


免責聲明!

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



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