1. 程式人生 > 實用技巧 >基於MindSpore詳解推薦模型的原理與實踐

基於MindSpore詳解推薦模型的原理與實踐



相信大家都在淘寶購買過相關的商品。 比如某一天使用者購買了一臺臺式主機後,淘寶會立刻給該使用者推送與電腦相關的商品,例如電腦顯示屏,鍵盤和滑鼠等等。 這些推薦的物品完全是淘寶根據大量的點選資料去學習到的。
比如,在100個人中,有99個人在買了電腦之後,還會去購買滑鼠,鍵盤,無線網絡卡等等。 也就是說,使用者最近是否買了電腦是一個非常強的訊號,暗示著如果使用者最近買了電腦,那麼使用者再次購買電腦顯示屏,鍵盤等物品時的概率非常高,那麼這個時候推薦系統及時的去推薦這些物品給使用者,使用者可能很可能會購買這些物品,這些商鋪也能提高自身的銷量。 因此,可以通過特徵工程生成很多個這樣強關聯的訊號,提升推薦系統的效能。


除了電商領域,推薦系統還廣泛的應用在電影推薦,興趣推薦等等。 相信最近大家都看過大火的<隱祕的角落>,可以堪稱是今年最好看的國產劇。 在豆瓣上的評分如下:

看完這部劇之後,還覺得不過癮怎麼辦? 往下翻,你就會驚喜的發現一系列的推薦:

是不是感覺推薦的電影都很符合自己的口味? <隱祕的角落>屬於懸疑類的連續劇,而下面推薦的相關劇集也大多屬於這個風格。
這個例子也同樣引出了推薦系統中一個重要的分支——

協同過濾
它主要可以分為基於使用者的,基於物品的和基於模型的協同過濾演算法。 基於使用者的協同過濾演算法的核心思想是: 給當前使用者推薦相似的使用者看過的物品


例如,使用者A看過了<白夜追凶>和<無證之罪>,而資深懸疑劇愛好者不僅看過這兩部劇,還看過了<非自然死亡>和<我們與惡的距離>。 那麼根據一定的相似度演算法,在所有的使用者中找到和使用者A最相似的使用者,比如我們找到了使用者B,將使用者B看過的另外兩部劇推薦給了使用者A,這樣一次推薦過程就完成了。


推薦系統的另一個典型場景是廣告點選率預估

。 在我們瀏覽手機的應用商店時,經常可以看到商店會給我們推薦某些應用,某些推薦的應用往往是廣告主通過付費來進行推廣。
為了提高廣告投放的準確度,廣告點選率預估模型需要評價使用者點選某些應用的概率,將使用者最可能點選的應用進行推送,達到準確的投放廣告的目的。 一旦我們點選了其中的一個應用,商店就可以成功的向廣告投放商進行收費,而廣告主們也達到了應用推廣的目的

除了上述領域,推薦系統還廣泛的應用在生活中的方方面面,可以說是機器學習在工業界中最成功的落地場景之一了。 美國著名的電影和電視節目提供商Netflix曾經發起了獎金為百萬美元的推薦系統比賽,旨在提升推薦系統的準確度。 在廣告點選率預估的場景,效能提高了1%的模型往往可以為公司帶來巨大的收入



傳統的推薦系統以協同過濾為代表,而隨著深度學習的快速發展,越來越多的研究者嘗試應用深度學習技術到推薦系統中。 在此我們主要介紹其中的Wide&Deep網路。

總體流程

在此我們以APP商店中的推薦系統為例,其整體流程可以如下圖所示


推薦系統流程圖


給定一個查詢,這個查詢可能是使用者相關的特徵,推薦系統首先會從資料庫中檢索到查詢相關的APP,由於APP的數量非常巨大,因此我們可以取最相關的100個檢索結果作為候選APP,這一過程通常叫作 粗排


然後將候選的100個APP送入排序模型中,此處的排序模型就是我們下面將要介紹的
Wide&Deep模型 ,這一過程也被叫作 精排


排序完成後,我們可以將點選概率最高的APP放置於使用者最容易注意到的地方。 無論使用者是否點選了我們的推薦結果,我們都可以構造一個新的日誌檔案。
在累積了一定數量的日誌檔案後,就可以繼續微調排序模型,提高模型的準確程度



Wide&Deep模型自從被提出後就引起來非常大的關注。 正如其名, 此模型主要可以分成兩個部分,Wide部分和Deep部分


Wide模型

Wide部分就是一個線性網路,和我們在初中學的多元線性方程是一樣的,只不過其中的引數求解採用梯度下降的方式。 例如下式
y=wx


它的設計目的是 為了記住資料中特定的特徵組合方式 。 例如前面介紹購買電腦的場景,購買了電腦主機的使用者,再次購買顯示器,鍵盤等物件的概率特別高。 因此可以將使用者最近是否購買了電腦作為輸入模型的輸入,也就是特徵。


假設 當前wide網路只有一個特徵 ,當該特徵取1時,y=wx可以得到y=0.9(假設w的值是0.9),當該特徵取0時,y=0。 y輸出值越大,會增大模型對應使用者點選概率的估計。


回到我們APP應用推薦的場景中,可以看到圖右側的Cross Product Transformation,就是我們的wide部分的輸入。 Cross Product Transformation是指特徵交叉,即將User Installed App和Impression APP進行組合。


例如使用者手機已經安裝了微信,且當前的待估計的APP為QQ,那麼這個組合特徵就是(User Installed App=’微信’, Impression APP=’QQ’)。



Deep模型


介紹完Wide部分,我們重新回來再看下Deep部分。 Deep部分的設計是為了模型具有較好的泛化能力 ,在輸入的資料沒有在訓練集中出現時,它依然能夠保持相關性較好的輸出


在下圖中, Deep模型輸入都是一些含義不是非常明顯的特徵 ,例如裝置型別,使用者統計資料等類別特徵(Categorical Features)。 類別特徵一般屬於高維特徵。


例如手機的種類可能存在成千上萬個,因此我們通常把這些類別特徵通過 嵌入(Embedding) 的方式, 對映成低維空間的引數向量 。 這個向量可以被認為表示了原先這個類別特徵的資訊。 對於連續特徵,其數值本身就具備一定的含義,因此可以直接將和其他嵌入向量進行拼接。


2Wide&Deep模型圖


在拼接完成後,可以得到大致為1200維度的向量 。 將其作為三層全連線層網路的輸入,並且選擇Relu作為啟用函式,其中每層的輸出維度分別為[1024, 512, 256]。



在廣告點選率預估的場景中, 模型的輸出是一個0~1之間的值 表示當前候選APP被點選的概率 。 因此可以採用邏輯迴歸函式,將Wide&Deep部分的輸出壓縮到0~1在之間。


首先, Deep部分的輸出是一個256維度的向量 ,可以通過一個線性變換將其對映為維度為1的值,然後 和Wide部分的輸出進行求和,將求和後的結果輸入到邏輯迴歸函式中。



在介紹完模型之後,現在開始動手實踐了,由於谷歌公司並沒有將其在論文中使用的APP商店資料集進行公開,因此我們現在 採用Criteo公司釋出在Kaggle的廣告點選率預估資料集Criteo


對應的Mindpsore程式碼可以在此處找到: https://gitee.com/mindspore/mindspore/tree/master/model_zoo/wide_and_deep 其中包含了模型的定義、訓練以及資料的預處理過程。



在模型定義前,我們需要對資料進行預處理。 資料處理的目的是將資料集中的特徵取值對映為數值id,並且去除一些出現次數過少的特徵值 ,避免特徵值出現次數過少,導致當特徵值對應的引數向量的更新次數過少,影響模型的精度。 Criteo資料集由13類連續特徵和26列類別特徵,已經通過雜湊方式對映為了32位數值。 對應的標籤(label)的取值為0和1。


資料處理的核心思路如下圖所示,針對類別特徵,建立一個詞表,裡面記錄著每次出現的特徵取值的編號。 然後再遍歷每一列的特徵取值,將原始的特徵值根據詞表對映為對應的id。


還記得之前提到過的低維嵌入過程嗎? 對映操作是為了模型中的嵌入向量查詢而準備的。 例如,輸入資料中的A,B,C被對映為了0,1,2。 而0,1,2分別表示在嵌入矩陣中(Embedding Table)的第0行,第1行和第2行。


將Criteo資料集下載後解壓 ,可以看到train.txt和test.txt檔案。 檢視train.txt中的檔案, 可以看到其中某些行存在缺失



我們可以呼叫


model_zoo/wide_and_deep/src/preprocess_data.py


進行資料的下載和處理 對於缺失值,可以將其標記為OOV(Out Of Vocabulary)對應的id



介紹完資料處理後,我們在此開始 定義模型 。 模型的核心程式碼在mindspore倉庫下


model_zoo/wide_and_deep/src/wide_and_deep.py


在MindSpore中,網路的定義方式和Pytorch比較接近,先定義定義的操作,然後再construct函式中對呼叫對應的操作對輸入進行處理。


首先我們再回憶下 Wide&Deep網路 ,它由一個Wide部分和Deep部分。 其中 Wide部分是一個線性網路


在實現上,我們將線性網路中的權重視為維度為1,通過嵌入矩陣查詢的方式即可獲得輸入x對應的權重,然後將其和輸入的mask相乘,將結果求和。


值得注意的是, 我們將連續特徵和類別特徵進行等同處理,因此這裡的mask是為了將連續特徵和類別特徵進行區而設計的 連續特徵mask中的值即為連續特徵值,類別特徵mask中的值為1 。 Wide部分的核心程式碼如下所示,我們定義個名為self.wide_w的權重,它的形狀為[詞表大小,1]。


class WideDeepModel(nn.Cell):
    def __init__(self, config):
        super(WideDeepModel, self).__init__()
        …
        init_acts = [('Wide_w', [self.vocab_size, 1], self.emb_init),
                     ('V_l2', [self.vocab_size, self.emb_dim], self.emb_init),
                     ('Wide_b', [1], self.emb_init)]
        var_map = init_var_dict(self.init_args, init_acts)
        self.wide_w = var_map["Wide_w"]
        self.wide_b = var_map["Wide_b"]
        self.embeddinglookup = nn.EmbeddingLookup()
        self.mul = P.Mul()
        self.reduce_sum = P.ReduceSum(keep_dims=False)
        self.reshape = P.Reshape()
        self.square = P.Square()

​
    def construct(self, id_hldr, wt_hldr):
        mask = self.reshape(wt_hldr, (self.batch_size, self.field_size, 1))
        # Wide layer
        wide_id_weight = self.embeddinglookup(self.wide_w, id_hldr, 0)
        wx = self.mul(wide_id_weight, mask)
        wide_out = self.reshape(self.reduce_sum(wx, 1) + self.wide_b, (-1, 1))
        out = wide_out

現在我們開始定義Deep部分。 Deep部分同樣有一個嵌入矩陣查詢 ,以及5層的全連線層構成。

class WideDeepModel(nn.Cell):
    def __init__(self, config):
        super(WideDeepModel, self).__init__()
        …
        init_acts = [('Wide_w', [self.vocab_size, 1], self.emb_init),
                     ('V_l2', [self.vocab_size, self.emb_dim], self.emb_init),
                     ('Wide_b', [1], self.emb_init)]
        var_map = init_var_dict(self.init_args, init_acts)
        self.wide_w = var_map["Wide_w"]
        self.wide_b = var_map["Wide_b"]
        self.embedding_table = var_map["V_l2"]
        self.dense_layer_1 = DenseLayer(self.all_dim_list[0],
                                        self.all_dim_list[1],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_2 = DenseLayer(self.all_dim_list[1],
                                        self.all_dim_list[2],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_3 = DenseLayer(self.all_dim_list[2],
                                        self.all_dim_list[3],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_4 = DenseLayer(self.all_dim_list[3],
                                        self.all_dim_list[4],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_5 = DenseLayer(self.all_dim_list[4],
                                        self.all_dim_list[5],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        use_activation=False, convert_dtype=True, drop_out=config.dropout_flag)

​
        self.embeddinglookup = nn.EmbeddingLookup()
        self.mul = P.Mul()
        self.reduce_sum = P.ReduceSum(keep_dims=False)
        self.reshape = P.Reshape()
        self.square = P.Square()
        self.shape = P.Shape()
        self.tile = P.Tile()
        self.concat = P.Concat(axis=1)
        self.cast = P.Cast()

​
    def construct(self, id_hldr, wt_hldr):
        """
        Args:
            id_hldr: batch ids;
            wt_hldr: batch weights;
        """
        mask = self.reshape(wt_hldr, (self.batch_size, self.field_size, 1))
        # Wide layer
        wide_id_weight = self.embeddinglookup(self.wide_w, id_hldr, 0)
        wx = self.mul(wide_id_weight, mask)
        wide_out = self.reshape(self.reduce_sum(wx, 1) + self.wide_b, (-1, 1))
        # Deep layer
        deep_id_embs = self.embeddinglookup(self.embedding_table, id_hldr, 0)
        vx = self.mul(deep_id_embs, mask)
        deep_in = self.reshape(vx, (-1, self.field_size * self.emb_dim))
        deep_in = self.dense_layer_1(deep_in)
        deep_in = self.dense_layer_2(deep_in)
        deep_in = self.dense_layer_3(deep_in)
        deep_in = self.dense_layer_4(deep_in)
        deep_out = self.dense_layer_5(deep_in)
        out = wide_out + deep_out
        return out, self.embedding_table

我們採用交叉熵作為損失函式,Wide部分採用FTRL作為優化器 。 FTRL可以產生較好的稀疏權重,可以幫助篩選有價值的特徵,並且可以壓縮模型權重。 Deep採用Adam優化器。



train_and_eval.py中的定義了資料,模型的初始化以及訓練過程,在定義完模型中,可以將初始化的網路送入Model類中,這個類和Tensorflow中Estimator比較接近,可以通過簡單的介面實現網路的訓練(model.train)和評估(model.eval)。

model_zoo/wide_and_deep/train_and_eval.py


另外,此函式中定義了一些回撥函式 https://www.mindspore.cn/tutorial/-zhCN/master/advanced_use/customized_debugging_information.html 可以列印損失值等相關資訊。

def test_train_eval(config):
    """
    test_train_eval
    """
    data_path = config.data_path
    batch_size = config.batch_size
    epochs = config.epochs
    ds_train = create_dataset(data_path, train_mode=True, epochs=epochs, batch_size=batch_size)
    ds_eval = create_dataset(data_path, train_mode=False, epochs=epochs + 1, batch_size=batch_size)
    print("ds_train.size: {}".format(ds_train.get_dataset_size()))
    print("ds_eval.size: {}".format(ds_eval.get_dataset_size()))

​
    net_builder = ModelBuilder()

​
    train_net, eval_net = net_builder.get_net(config)
    train_net.set_train()
    auc_metric = AUCMetric()

​
    model = Model(train_net, eval_network=eval_net, metrics={"auc": auc_metric})

​
    eval_callback = EvalCallBack(model, ds_eval, auc_metric, config)

​
    callback = LossCallBack(config=config)
    ckptconfig = CheckpointConfig(save_checkpoint_steps=ds_train.get_dataset_size(), keep_checkpoint_max=5)
    ckpoint_cb = ModelCheckpoint(prefix='widedeep_train', directory=config.ckpt_path, config=ckptconfig)

​
    out = model.eval(ds_eval)
    print("=====" * 5 + "model.eval() initialized: {}".format(out))
    model.train(epochs, ds_train,
                callbacks=[TimeMonitor(ds_train.get_dataset_size()), eval_callback, callback, ckpoint_cb])

一旦訓練完成後 ,就可以裝載模型引數進行評估。 評估網路和訓練網路類似,只不過輸出經過了一個Sigmoid層

class PredictWithSigmoid(nn.Cell):
    def __init__(self, network):
        super(PredictWithSigmoid, self).__init__()
        self.network = network
        self.sigmoid = P.Sigmoid()

​
    def construct(self, batch_ids, batch_wts, labels):
        logits, _, _, = self.network(batch_ids, batch_wts)
        pred_probs = self.sigmoid(logits)
        return logits, pred_probs, labels

採用AUC作為評價指標 。 AUC廣泛的應用在分類模型的評估中, 可以較好的反映模型學習的好壞,其值在0~1之間,值越高,模型的效能越好


本文內容介紹了推薦系統的原理和實踐程式碼。 首先講述了推薦系統在我們生活中的應用場景,並且介紹了推薦系統的核心原理。 然後詳細介紹了Wide&Deep網路以及相關的程式碼實踐,期望可以幫助大家入門



參考文獻

[1] Cheng H T, Koc L, Harmsen J, et al. Wide & deep learning for recommender systems[C]//Proceedings of the 1st workshop on deep learning for recommender systems. 2016: 7-10.

[2]https://www.mindspore.cn/tutorial/-zhCN/master/advanced_use/customized_debugging_information.html



長按下方二維碼關注↓