日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

清华大学王晨阳:轻量级Top-K推荐框架及相关论文介绍

發布時間:2024/10/8 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 清华大学王晨阳:轻量级Top-K推荐框架及相关论文介绍 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文內容整理自 PaperWeekly 和 biendata 在 B 站組織的直播回顧,點擊文末閱讀原文即可跳轉至 B 站收看本次分享完整視頻錄像,如需嘉賓課件,請在?PaperWeekly 公眾號回復關鍵詞課件下載獲取下載鏈接。

構建一個公平的推薦算法“合唱團”,這也是框架名稱 ReChorus 的由來 圖片出處:pixabay

作者簡介:王晨陽,清華大學計算機系人智所信息檢索課題組二年級博士生,研究方向為推薦系統中用戶的動態需求,主要包括序列推薦、引入知識及時間動態性的意圖理解等,在WWW、SIGIR等會議發表多篇論文。

推薦系統中基于深度學習的方法近幾年來層出不窮,然而不同工作之間實驗設定和實現細節的差異使得我們很難直接比較不同論文的相對效果。有論文針對推薦領域中實驗的可復現性提出了質疑,認為百花齊放的表象背后的實際情況是推薦系統領域長時間的停滯不前。

為此,我們基于最近發表在 SIGIR’20 工作的代碼,整理出了一個輕量級的 Top-K推薦框架 ReChorus,旨在分離模型間共同的實驗設定和不同的模型設計,使得各個模型能夠在一個公平的 benchmark 上進行對比。ReChorus 足夠簡單易上手,既適合初學者了解推薦領域的經典模型,也適合研究者快速實現自己的想法;同時 ReChorus 足夠靈活,可以輕松適配個性化的數據格式和評測流程。

本文還會介紹目前 ReChorus 中表現最好的模型——引入商品關系和時間動態性的商品表示。這個工作顯式建模了目標商品和近期交互商品之間的關系,以及不同關系所產生的影響如何隨時間變化。實驗表明該方法得到的商品表示可以靈活地應用于各種推薦算法并取得顯著的效果提升。

推薦系統領域的劣幣驅逐良幣


在開始介紹 ReChorus 前,讓我們先思考幾個問題。

第一個問題,如上圖所示,簡單回憶的話,推薦系統領域所用的 baseline 是不是往往就那么幾個?可能研究者們不再像過去那樣傾向于同某個領域或問題設定下最好的?baseline?做比較,轉而和比較流行的 baseline 做比較。只做到了比某個流行的baseline 表現好一點,研究者就宣稱自己達到了?state-of-the-art(SOTA) 的性能。如果你去找那些非常強的 SOTA?的模型做比較,提升就會相對變小,論文相對更難發,而這樣的狀況很可能導致劣幣驅逐良幣的趨勢。

近期就有這樣的一批論文發表,研究者們都自稱達到了 SOTA 的性能。可以預見的是后續的研究者撰寫新一批論文,選擇?baseline?進行比較時會更傾向于選這批論文中表現比較差的,由此對比出他們的論文算法有了比較大的提升。雖然這樣會讓整體的論文發表呈現百家爭鳴、百花齊放的表象。但是這樣的機制會使真正高質量?baseline?在浪潮中被淹沒。

在推薦領域,即使是比較資深的專家,也很難指出在某個任務設定下 SOTA 模型到底是哪一個。或許你會覺得這有什么難的,直接將新論文同一個 baseline 相對提升的幅度做比較不就可以了?

這就引出來了第二個問題,比較后會發現一個詭異的現象,同一組的?baseline?在不同的論文中相對的優劣是不一樣的。雖然有時候審稿人會指出這些問題,但是在很多已發表的論文中也能觀察到這樣的現象,雖然數據集在其中造成了一定的影響,但是我覺得很大程度上還是因為沒有把參數調好。

現在很多研發者不會下功夫對 baseline 做調整,調出一個差不多的結果后就做罷了。但這樣的后果是,如果想比較兩個自身達到了?SOTA?性能的模型,其相對提升就不會有特別明顯的可比性,可能其中一個 baseline?調的非常好,另外一個則沒有。這時就很難比較到底哪個模型在整個領域上達到了一個更優的效果。

那么,將在同一個數據集上進行過實驗的模型拿來比較效果不就行了嗎?這就引出了第三個問題:很多論文即使是在同一個數據集上,實驗結果也不太有可比性。其中的原因有很多,我們來看幾個比較有代表性的例子:

1. 推薦領域比較常見的數據預處理,是否去除了那些交互數量比較少的用戶item(常見去掉小于等于5的)?

2. 是否去掉了比較 popular 的 item ?

3. 在數據劃分時,直接用 leave-one-out 的方法把每個用戶的最后一個序列作為測試集,還是為了防止時間泄露,設置一個時間來做一刀切來做數據集?

4. 在負例選取上是直接選用戶沒有交互過的作為負例,還是選擇一個按照 item popular 的程度進行加權的負例采樣?

上述例子看起來都是一些細節的設定,可能并不會非常明確的在論文中體現,但它們對模型的效果卻有很大的影響。在同一個數據集上,兩篇論文的實驗結果可能會因為這些細節設定的差別存在跨數量級的差別。

最近?RecSys?的一篇論文 Are we really making much progress? A worrying analysis of recent neural recommendation approaches?也講了這個問題。在推薦系統中,實驗設定有很多具有分歧的地方,在這篇論文中就總結了多達8個分歧點,看起來不多,但假如說每一個分歧點至少有兩種選擇,2的8次方就是256種實驗組合。

雖然實際中可能并不會這么多,常見的情況可能接近10種。這依然意味著很難保證想比較的兩篇論文采取了完全一樣的實驗設定,也就導致即使兩篇在同一個數據集上進行實驗的論文,它們的結果也無法做比較科學的比較。

可能你會產生新的疑問,現在代碼不都開源嗎?我直接將代碼在我的實驗設定下跑一跑就可以了。我們來看看會發生什么:假設你準備下周一跟導師開組會匯報,你和導師說這周要把一個開源的?baseline?在自己的實驗設定下跑一跑結果。周二你下載了代碼,但發現把它改到能在自己的實驗設定下去跑是非常困難的。

這其實是現在比較常見的一個現象,按說代碼開源要滿足的最低要求是可以復現論文的結果,先不說有些開源代碼連這個最低的要求都沒有實現,即使是達到最低要求的代碼可能也很難匹配你的實驗設定。我遇到過一個最極端的例子,當時要去補充一個帶商品關系的 baseline(具體的名字就不說了)。

拿到代碼后首先發現這篇論文有兩個數據集,但奇怪的是代碼載入時好像只有一個數據集的相關的內容。再看模型就更奇怪了,這份商品關系的baseline帶有一個知識圖譜,其中所有關于商品關系的代碼,都按商品關系的數量去寫了多份,比如數據其實有兩種關系,就需要把同樣的代碼段寫兩份,把變量名做一個像X1、X2這樣的區分。

我當時非常震驚,第二個數據集怎么辦?果然有其他人問了論文作者同樣的問題,作者怎么回應呢?給了一個百度網盤的鏈接用來下載第二個數據集的代碼,我下載后發現第二個數據集有3種關系,類似第一個數據集,作者又把所有跟這種商品關系相關的代碼段寫了3遍,所有的變量用X1、X2、X3來替代,這讓我非常崩潰,我的數據集有十幾種關系,如果像他這樣寫,就要寫上一個幾千行且bug非常多的程序。

其實整個開源領域的代碼質量非常參差不齊。回到上文假設的場景,整整一周你都沒有把開源的?baseline 在自己的實驗設定下真正跑起來。但和跟老師匯報時,老師可能會質疑你這一周都干了啥?既然是開源代碼,為什么一周連一個實驗的結果都沒跑出來,你到底有沒有在做實驗?你只能一肚子的委屈。


上面的這些問題,也是一、兩年間我們在做推薦領域研究中觀察到的。上圖右邊,去年 RecSys 上的 best paper 也討論了這些問題,在推薦系統領域,我們是否在真正的 making progress ?作者選擇了18篇推薦領域的論文,但他只成功復現了其中的7篇,這7篇中效果能超過優質傳統模型的又少之又少。

在目前看來,推薦系統論文百花齊放的表象下,確實很有可能隱藏著一個長期的停滯不前,或至少是一個比較緩慢的前行實際情況。

ReChorus推薦框架介紹

2.1 ReChorus推薦框架介紹

如何改善這樣的狀況呢?我們認為關鍵點在于是提供一個比較公平的 benchmark 評測平臺,讓不同的模型在同樣的設定下進行評測。研究者可以直觀、清晰地看到不同的模型之間的優劣關系。好比看家用顯卡天梯圖一樣:我基于現在的預算,就知道我該選什么樣的顯卡。

類似的,有了評測平臺提供的模型天梯圖,研究者就能知道基于自己的實驗設定,應該選擇哪個 baseline 去作為我對比的目標、超越的對象,同時也可以幫助初學者更快地了解常見推薦算法。

我們基于上述的想法,同時在整理這次 SIGIR 一篇論文的代碼時,就思考如何做成更通用的操作,可以推薦框架,促進領域中的模型來做公平的對比,從而構建一個真正的推薦算法“合唱團”,這也是框架名稱 ReChorus 的由來。

在設計框架時,最主要的核心思想是如何分離模型間共同的實驗設定到共享的類中,突出不同的模型的細節,從而讓不同的模型可以在完全相同的實驗設定下進行對比。另外我們希望框架具有以下四個特點:

  • 輕量:易上手,代碼self-contain;

  • 高效:盡可能加速通用的訓練和評測過程;

  • 靈活:適配不同的數據輸入格式和實驗設定;

  • 專注:實現新模型時只需要關注一個文件。

針對第4點再補充說明幾句,為什么很多開源出來不是特別好的代碼,都是一個文件把一個模型寫完?因為這樣的好處非常明顯,使得研究者在調試時非常方便,只關注這一個文件,哪里有問題直接翻到那去找。

而一些框架會分很多很多類,非常面向對象。研究者可能寫一個模型代碼,在數據準備時要翻到前面去,看看所用的類如何適配自己的模型,這需要翻很多其他的文件,甚至還要對文件做改動,牽一發而動全身,研究者又要顧及改動會不會影響自己構建的其他模型,構建模型的思路就會被打亂。

我們希望實現把模型間不同的部分盡可能都集中到統一文件中去。

上圖顯示了已有的模型?目前實現的模型主要是基于 SIGIR 那篇論文的 baseline ,添加了一些常見的模型,還在繼續擴充當中,上圖的右面的二維碼指向GitHub的鏈接,歡迎查看。

可以看到目前實現的模型包括從2009年比較經典的BPR,到后續的2016年、2017年、2018年、2019年、2020年的算法,既包括傳統的模型,也有序列的模型,同時結合知識圖譜、結合時間信息的也有了一些實踐,性能的對比、各自的特點和運行時間列在了上圖右側。后文還會再講這個結果,這里先不做詳細分析。

2.2?框架主體

如上圖,首先把框架分成了兩個類型的類,核心的模型類和幫助類。核心的模型類以 model 結尾,主要用來定義模型的細節,也就是體現模型之間差異化的內容,以及如何構建輸入的batch,這些都放在同一個文件里面,而且并不長。

幫助類分reader 和 runner 。reader 從硬盤中讀取文件、數據集放到內存里,然后進行統一的預處理。runner 控制模型訓練和評測的過程,會和所用到的深度學習框架訓練和評測的代碼有關,這里是基于pytorch的一個實現。

從上圖可以看出,模型可以共享幫助類。雖然目前幫助類只實現了兩個(base reader 和 base runner),如果我們的實驗有變化,比如數據集的格式有變化,我們可以實現新的 reader,也可以用其他的 runner 來實現不同的評測的機制。這些 reader 和 runner 幫助類都是可以指定給每個模型,有點像 OOP 里面的設計模式,這些就像它的“廚師”,可以把它指定給每一個模型,實現靈活的適配。

接下來帶大家從代碼的層面梳理一遍 ReChorus 框架。

文件夾層面大概分data、log和src,log包含輸出的log文件,src包含主要的模型代碼。下面快速看一下data中的內容,數據集大概長什么樣。

上面的代碼非常簡單,包含四個文件,其中train、test和dev 這三個文件比較重要,關鍵數值是user ID、item ID,還有每個的時間戳。

對于測試集和驗證集來說,測試的時候我們一般會 sample 一些負例,和正例組成 candidate set,然后把正例和負例一起做排序,看正例到底排在第幾位,所以train、test和dev這三個文件是必須的。后面選擇性的提供 item 的、特征知識圖譜的一些信息。r_complement 部分代表第一個item跟這一類item有互補的關系。

r_complement 部分代表第一個item跟這一類item有互補的關系。到這個 src 中的代碼層面。主要分為三部分,一個是前文說的幫助類,實現了 base reader 和 base runner,第二部分 models 層面除了 base model 是一個基本的類以外,可以理解為一個抽象類,后面對于每個模型實現一個類,繼承這個 base model 來實現它具體的功能。第三部分 util 層面是一些工具性的函數。函數主要入口在main,從main開始來看一下完整的框架。

# -*- coding: UTF-8 -*-import os import sys import pickle import logging import argparse import numpy as np import torchfrom models import * from helpers import * from utils import utilsdef parse_global_args(parser):parser.add_argument('--gpu', type=str, default='0',help='Set CUDA_VISIBLE_DEVICES')parser.add_argument('--verbose', type=int, default=logging.INFO,help='Logging Level, 0, 10, ..., 50')parser.add_argument('--log_file', type=str, default='',help='Logging file path')parser.add_argument('--random_seed', type=int, default=2019,help='Random seed of numpy and pytorch.')parser.add_argument('--load', type=int, default=0,help='Whether load model and continue to train')parser.add_argument('--train', type=int, default=1,help='To train the model or not.')parser.add_argument('--regenerate', type=int, default=0,help='Whether to regenerate intermediate files.')return parserdef main():logging.info('-' * 45 + ' BEGIN: ' + utils.get_time() + ' ' + '-' * 45)exclude = ['check_epoch', 'log_file', 'model_path', 'path', 'pin_memory','regenerate', 'sep', 'train', 'verbose']logging.info(utils.format_arg_str(args, exclude_lst=exclude))# Random seednp.random.seed(args.random_seed)torch.manual_seed(args.random_seed)torch.cuda.manual_seed(args.random_seed)# GPUos.environ["CUDA_VISIBLE_DEVICES"] = args.gpulogging.info("# cuda devices: {}".format(torch.cuda.device_count()))# Read datacorpus_path = os.path.join(args.path, args.dataset, model_name.reader + '.pkl')if not args.regenerate and os.path.exists(corpus_path):logging.info('Load corpus from {}'.format(corpus_path))corpus = pickle.load(open(corpus_path, 'rb'))else:corpus = reader_name(args)logging.info('Save corpus to {}'.format(corpus_path))pickle.dump(corpus, open(corpus_path, 'wb'))# Define modelmodel = model_name(args, corpus)logging.info(model)model = model.double()model.apply(model.init_weights)model.actions_before_train()if torch.cuda.device_count() > 0:model = model.cuda()# Run modeldata_dict = dict()for phase in ['train', 'dev', 'test']:data_dict[phase] = model_name.Dataset(model, corpus, phase)runner = runner_name(args)logging.info('Test Before Training: ' + runner.print_res(model, data_dict['test']))if args.load > 0:model.load_model()if args.train > 0:runner.train(model, data_dict)logging.info(os.linesep + 'Test After Training: ' + runner.print_res(model, data_dict['test']))model.actions_after_train()logging.info(os.linesep + '-' * 45 + ' END: ' + utils.get_time() + ' ' + '-' * 45)

首先定義了一些global的參數,主要控制整體的,比如 manual_seed的問題。主函數部分還包含一些比較通用的設置,像隨機數參數(隨機數的種子)、具體用哪一個GPU。我調用 reade r這個類去進行 corpus 的構建。

有一些預處理會比較花費時間,所以默認把讀入數據進行存儲,也可以修改比如 regenerate 這樣的參數,讓它實現每一次都進行一個重復的預處理。還可以定義 model,根據所定義的model的內容,來做初始化參數的操作以及決定是否輸入到顯卡中。

之后調用 runner 類對模型進行評測和訓練。還定義了每個的 dataset ,也就是pytorch 面內置的 dataset 一個集成的類,可以看到我把 dataset 寫到了 model 中作為一個內部類。

為什么不把準備batch寫到reader里面去?基于前文說過的設計框架指導原則,就是要把模型間不同的地方都集中到一個文件里,其實準備batch不同模型往往非常不一樣,所以我就把它集成到了模型這類里面去。runner 通過 runner.train 這行代碼控制整個訓練的過程,看一下訓練結果這部分就結束了。以上,main主要就是把所有的部分串聯起來。

class BaseReader(object):@staticmethoddef parse_data_args(parser):parser.add_argument('--path', type=str, default='../data/',help='Input data dir.')parser.add_argument('--dataset', type=str, default='Grocery_and_Gourmet_Food',help='Choose a dataset.')parser.add_argument('--sep', type=str, default='\t',help='sep of csv file.')parser.add_argument('--history_max', type=int, default=20,help='Maximum length of history.')return parserdef __init__(self, args):self.sep = args.sepself.prefix = args.pathself.dataset = args.datasetself.history_max = args.history_maxt0 = time.time()self._read_data()self._append_info()logging.info('Done! [{:<.2f} s]'.format(time.time() - t0) + os.linesep)def _read_data(self):logging.info('Reading data from \"{}\", dataset = \"{}\" '.format(self.prefix, self.dataset))self.data_df, self.item_meta_df = dict(), pd.DataFrame()self._read_preprocessed_df()logging.info('Formating data type...')for df in list(self.data_df.values()) + [self.item_meta_df]:for col in df.columns:df[col] = df[col].apply(lambda x: eval(str(x)))logging.info('Constructing relation triplets...')self.triplet_set = set()relation_types = [r for r in self.item_meta_df.columns if r.startswith('r_')]heads, relations, tails = [], [], []for idx in range(len(self.item_meta_df)):head_item = self.item_meta_df['item_id'][idx]for r_idx, r in enumerate(relation_types):for tail_item in self.item_meta_df[r][idx]:heads.append(head_item)relations.append(r_idx + 1)tails.append(tail_item)self.triplet_set.add((head_item, r_idx + 1, tail_item))self.relation_df = pd.DataFrame()self.relation_df['head'] = headsself.relation_df['relation'] = relationsself.relation_df['tail'] = tailslogging.info('Counting dataset statistics...')self.all_df = pd.concat([self.data_df[key][['user_id', 'item_id', 'time']] for key in ['train', 'dev', 'test']])self.n_users, self.n_items = self.all_df['user_id'].max() + 1, self.all_df['item_id'].max() + 1self.n_relations = self.relation_df['relation'].max() + 1logging.info('"# user": {}, "# item": {}, "# entry": {}'.format(self.n_users, self.n_items, len(self.all_df)))logging.info('"# relation": {}, "# triplet": {}'.format(self.n_relations, len(self.relation_df)))def _append_info(self):"""Add history info to data_df: item_his, time_his, his_length! Need data_df to be sorted by time in ascending order:return:"""logging.info('Adding history info...')user_his_dict = dict() # store the already seen sequence of each userfor key in ['train', 'dev', 'test']:df = self.data_df[key]i_history, t_history = [], []for uid, iid, t in zip(df['user_id'], df['item_id'], df['time']):if uid not in user_his_dict:user_his_dict[uid] = []i_history.append([x[0] for x in user_his_dict[uid]])t_history.append([x[1] for x in user_his_dict[uid]])user_his_dict[uid].append((iid, t))df['item_his'] = i_historydf['time_his'] = t_historyif self.history_max > 0:df['item_his'] = df['item_his'].apply(lambda x: x[-self.history_max:])df['time_his'] = df['time_his'].apply(lambda x: x[-self.history_max:])df['his_length'] = df['item_his'].apply(lambda x: len(x))self.user_clicked_set = dict()for uid in user_his_dict:self.user_clicked_set[uid] = set([x[0] for x in user_his_dict[uid]])def _read_preprocessed_df(self):for key in ['train', 'dev', 'test']:self.data_df[key] = pd.read_csv(os.path.join(self.prefix, self.dataset, key + '.csv'), sep=self.sep)item_meta_path = os.path.join(self.prefix, self.dataset, 'item_meta.csv')if os.path.exists(item_meta_path):self.item_meta_df = pd.read_csv(item_meta_path, sep=self.sep)if __name__ == '__main__':logging.basicConfig(level=logging.INFO)parser = argparse.ArgumentParser()parser = BaseReader.parse_data_args(parser)args, extras = parser.parse_known_args()args.path = '../../data/'corpus = BaseReader(args)corpus_path = os.path.join(args.path, args.dataset, 'Corpus.pkl')logging.info('Save corpus to {}'.format(corpus_path))pickle.dump(corpus, open(corpus_path, 'wb'))

接著看上面 base reader 的代碼,先看如何把數據集加載到內存里,其中兩個函數 read_data 和 append_info。read_data 把數據讀到內存中,轉成 dataframe 的形式,可能會去根據 item_meta_data 構建三元組的形式,也會做整個數據集的統計特征。

appen_info 主要做統一的、之后模型可能都會用到的預處理。具體工作主要包括把item交互的歷史拼到對應的dataframe里面去,tradeoff 整個的訓練過程非常快,不過可能比較占內存,對于更大一點數據集可以考慮把它放到 dataset 里面在多線程準備的時動態的找對應的這個歷史。

補充一點說明,需要把數據一次性讀到內存里面去嗎?確實是,推薦領域中,至少在研究中很少很少像 CV 領域,可能因為圖片都比較大,無法一次全部裝載到內存里。在推薦領域,如上文展示的那種數據集的格式,主要是 ID 和一些特征,直接講 CSV 裝載到內存還是比較方便的。

如果整個數據集比較大,無法預先的把歷史和一些特征先準備好的話,可以之后寫在 batch 里做動態的準備,犧牲一點時間來減少內存的使用。?

總結base reader 這部分就是講數據讀到 dataframe 里,做一個統一的預處理。

class BaseRunner(object):@staticmethoddef parse_runner_args(parser):parser.add_argument('--epoch', type=int, default=100,help='Number of epochs.')parser.add_argument('--check_epoch', type=int, default=1,help='Check some tensors every check_epoch.')parser.add_argument('--early_stop', type=int, default=5,help='The number of epochs when dev results drop continuously.')parser.add_argument('--lr', type=float, default=1e-3,help='Learning rate.')parser.add_argument('--l2', type=float, default=0,help='Weight decay in optimizer.')parser.add_argument('--batch_size', type=int, default=256,help='Batch size during training.')parser.add_argument('--eval_batch_size', type=int, default=256,help='Batch size during testing.')parser.add_argument('--optimizer', type=str, default='Adam',help='optimizer: GD, Adam, Adagrad, Adadelta')parser.add_argument('--num_workers', type=int, default=5,help='Number of processors when prepare batches in DataLoader')parser.add_argument('--pin_memory', type=int, default=1,help='pin_memory in DataLoader')parser.add_argument('--topk', type=str, default='[5,10]',help='The number of items recommended to each user.')parser.add_argument('--metric', type=str, default='["NDCG","HR"]',help='metrics: NDCG, HR')return parserdef __init__(self, args):self.epoch = args.epochself.check_epoch = args.check_epochself.early_stop = args.early_stopself.learning_rate = args.lrself.batch_size = args.batch_sizeself.eval_batch_size = args.eval_batch_sizeself.l2 = args.l2self.optimizer_name = args.optimizerself.num_workers = args.num_workersself.pin_memory = args.pin_memoryself.topk = eval(args.topk)self.metrics = [m.strip().upper() for m in eval(args.metric)]self.main_metric = '{}@{}'.format(self.metrics[0], self.topk[0]) # early stop based on main_metricself.time = None # will store [start_time, last_step_time]def _check_time(self, start=False):if self.time is None or start:self.time = [time()] * 2return self.time[0]tmp_time = self.time[1]self.time[1] = time()return self.time[1] - tmp_timedef _build_optimizer(self, model):optimizer_name = self.optimizer_name.lower()if optimizer_name == 'gd':logging.info("Optimizer: GD")optimizer = torch.optim.SGD(model.customize_parameters(), lr=self.learning_rate, weight_decay=self.l2)elif optimizer_name == 'adagrad':logging.info("Optimizer: Adagrad")optimizer = torch.optim.Adagrad(model.customize_parameters(), lr=self.learning_rate, weight_decay=self.l2)elif optimizer_name == 'adadelta':logging.info("Optimizer: Adadelta")optimizer = torch.optim.Adadelta(model.customize_parameters(), lr=self.learning_rate, weight_decay=self.l2)elif optimizer_name == 'adam':logging.info("Optimizer: Adam")optimizer = torch.optim.Adam(model.customize_parameters(), lr=self.learning_rate, weight_decay=self.l2)else:raise ValueError("Unknown Optimizer: " + self.optimizer_name)return optimizerdef train(self, model, data_dict):main_metric_results, dev_results, test_results = list(), list(), list()self._check_time(start=True)try:for epoch in range(self.epoch):# Fitself._check_time()loss = self.fit(model, data_dict['train'], epoch=epoch + 1)training_time = self._check_time()# Observe selected tensorsif len(model.check_list) > 0 and self.check_epoch > 0 and epoch % self.check_epoch == 0:utils.check(model.check_list)# Record dev and test resultsdev_result = self.evaluate(model, data_dict['dev'], self.topk[:1], self.metrics)test_result = self.evaluate(model, data_dict['test'], self.topk[:1], self.metrics)testing_time = self._check_time()dev_results.append(dev_result)test_results.append(test_result)main_metric_results.append(dev_result[self.main_metric])logging.info("Epoch {:<5} loss={:<.4f} [{:<.1f} s]\t dev=({}) test=({}) [{:<.1f} s] ".format(epoch + 1, loss, training_time, utils.format_metric(dev_result),utils.format_metric(test_result), testing_time))# Save model and early stopif max(main_metric_results) == main_metric_results[-1] or \(hasattr(model, 'stage') and model.stage == 1):model.save_model()if self.early_stop and self.eval_termination(main_metric_results):logging.info("Early stop at %d based on dev result." % (epoch + 1))breakexcept KeyboardInterrupt:logging.info("Early stop manually")exit_here = input("Exit completely without evaluation? (y/n) (default n):")if exit_here.lower().startswith('y'):logging.info(os.linesep + '-' * 45 + ' END: ' + utils.get_time() + ' ' + '-' * 45)exit(1)# Find the best dev result across iterationsbest_epoch = main_metric_results.index(max(main_metric_results))logging.info(os.linesep + "Best Iter(dev)={:>5}\t dev=({}) test=({}) [{:<.1f} s] ".format(best_epoch + 1, utils.format_metric(dev_results[best_epoch]),utils.format_metric(test_results[best_epoch]), self.time[1] - self.time[0]))model.load_model()def fit(self, model, data, epoch=-1):gc.collect()torch.cuda.empty_cache()if model.optimizer is None:model.optimizer = self._build_optimizer(model)data.negative_sampling() # must sample before multi thread startmodel.train()loss_lst = list()dl = DataLoader(data, batch_size=self.batch_size, shuffle=True, num_workers=self.num_workers,collate_fn=data.collate_batch, pin_memory=self.pin_memory)for batch in tqdm(dl, leave=False, desc='Epoch {:<3}'.format(epoch), ncols=100, mininterval=1):batch = utils.batch_to_gpu(batch)model.optimizer.zero_grad()prediction = model(batch)loss = model.loss(prediction)loss.backward()model.optimizer.step()loss_lst.append(loss.detach().cpu().data.numpy())return np.mean(loss_lst)def eval_termination(self, criterion):if len(criterion) > 20 and utils.non_increasing(criterion[-self.early_stop:]):return Trueelif len(criterion) - criterion.index(max(criterion)) > 20:return Truereturn Falsedef evaluate(self, model, data, topks, metrics):"""Evaluate the results for an input dataset.:return: result dict (key: metric@k)"""predictions = self.predict(model, data)return utils.evaluate_method(predictions, topks, metrics)def predict(self, model, data):"""The returned prediction is a 2D-array, each row corresponds to all the candidates,and the ground-truth item poses the first.Example: ground-truth items: [1, 2], 2 negative items for each instance: [[3,4], [5,6]]predictions order: [[1,3,4], [2,5,6]]"""model.eval()predictions = list()dl = DataLoader(data, batch_size=self.eval_batch_size, shuffle=False, num_workers=self.num_workers,collate_fn=data.collate_batch, pin_memory=self.pin_memory)for batch in tqdm(dl, leave=False, ncols=100, mininterval=1, desc='Predict'):prediction = model(utils.batch_to_gpu(batch))predictions.extend(prediction.cpu().data.numpy())return np.array(predictions)def print_res(self, model, data):"""Construct the final result string before/after training:return: test result string"""result_dict = self.evaluate(model, data, self.topk, self.metrics)res_str = '(' + utils.format_metric(result_dict) + ')'return res_str

上面是是 base runner 部分,主要控制整個訓練的流程和評測。通過設置參數控制整個訓練的流程,如check time是一些工具性的函數,通過build_optimizer 去構建具體的優化器。

訓練方面,可以看到主要調用的是 train 函數,去調用后面的fit的函數,對訓練集做參數的更新,它主要解決驗證集上的結果,在測試集上的結果進行一個輸出,看是否在驗證集上達到最好。達到最好的話需要 save model,是否滿足 early_stop 的條件,如果滿足就 break ,這里其實也檢測了手動的 Ctrl C break ,可能訓練到中間的某一個輪次覺得這個明顯不會好,所以就先去掉。

去掉了之后,它會問你是否要真正退出,如果最后想要再評測一下看最后效果、最后的指標,可以不退出,如果連最后指標都不想看,可以完全退出。

最后訓練完,我會找到在驗證集上最優的一輪,去做模型的load ,方便后續進行測試。fit 這部分是剛才 train 中去調用的,代碼都是 pytorch 用戶非常熟悉的。

每一個 batch 參數的更新,前面是準備性工作,包括訓練的時候因為是 top_k 的訓練,應是一個 ranking loss ,會采樣一些負例。這里還會定義 dataloader,dataloader 是 pytorch 內置的類,是 dataset 相應的那個類,它會返回一個迭代器,當你每次去迭代它的時候,它會多線程從 data 中去準備相應的 batch。

這個 batch 具體是什么樣是要靠你在 dataset 類里面去設定的。它會根據參數的不同,是否 shuffle ,每次返回對應的batch,這就相當于是 for dataloader 得到對應的batch之后,讓model 得到 batch prediction 的結果,進行參數的更新,以上就是fit 這部分代碼所做的工作。

def eval_termination 開始這部分是之前調用的、判斷是否 early_stop 的標準,evaluate 這部分這些比較簡單,直接調用 prediction ,得到 predictions 之后,用到工具類中寫到的評測函數去進行評測。這個評測其實也針對目前的 topk 實驗設定進行了相應優化,會讓整個算 NDCG、算 HR 都會非常快。

predict 與 fit 比較像,不需要進行參數的更新,也是定義相應的 dataloader ,每一個 batch 得到預測的結果即可,最后規范輸出的 string 格式。

整個 baserunner 大概不到200行,整個核心框架不到800行,所以說這是一個非常容易上手的框架。上述很多是比較細節的信息,希望幫助新手能更快上手。

class BaseModel(torch.nn.Module):reader = 'BaseReader'runner = 'BaseRunner'extra_log_args = []@staticmethoddef parse_model_args(parser):parser.add_argument('--model_path', type=str, default='',help='Model save path.')parser.add_argument('--num_neg', type=int, default=1,help='The number of negative items during training.')parser.add_argument('--dropout', type=float, default=0.2,help='Dropout probability for each deep layer')parser.add_argument('--buffer', type=int, default=1,help='Whether to buffer feed dicts for dev/test')return parser@staticmethoddef init_weights(m):if 'Linear' in str(type(m)):torch.nn.init.normal_(m.weight, mean=0.0, std=0.01)if m.bias is not None:torch.nn.init.normal_(m.bias, mean=0.0, std=0.01)elif 'Embedding' in str(type(m)):torch.nn.init.normal_(m.weight, mean=0.0, std=0.01)def __init__(self, args, corpus):super(BaseModel, self).__init__()self.model_path = args.model_pathself.num_neg = args.num_negself.dropout = args.dropoutself.buffer = args.bufferself.item_num = corpus.n_itemsself.optimizer = Noneself.check_list = list() # observe tensors in check_list every check_epochself._define_params()self.total_parameters = self.count_variables()logging.info('#params: %d' % self.total_parameters)"""Methods must to override"""def _define_params(self):self.item_bias = torch.nn.Embedding(self.item_num, 1)def forward(self, feed_dict):""":param feed_dict: batch prepared in Dataset:return: prediction with shape [batch_size, n_candidates]"""i_ids = feed_dict['item_id']prediction = self.item_bias(i_ids)return prediction.view(feed_dict['batch_size'], -1)"""Methods optional to override"""def loss(self, predictions):"""BPR ranking loss with optimization on multiple negative samples@{Recurrent neural networks with top-k gains for session-based recommendations}:param predictions: [batch_size, -1], the first column for positive, the rest for negative:return:"""pos_pred, neg_pred = predictions[:, 0], predictions[:, 1:]neg_softmax = (neg_pred - neg_pred.max()).softmax(dim=1)neg_pred = (neg_pred * neg_softmax).sum(dim=1)loss = F.softplus(-(pos_pred - neg_pred)).mean()# ↑ For numerical stability, we use 'softplus(-x)' instead of '-log_sigmoid(x)'return lossdef customize_parameters(self):# customize optimizer settings for different parametersweight_p, bias_p = [], []for name, p in filter(lambda x: x[1].requires_grad, self.named_parameters()):if 'bias' in name:bias_p.append(p)else:weight_p.append(p)optimize_dict = [{'params': weight_p}, {'params': bias_p, 'weight_decay': 0}]return optimize_dict"""Auxiliary methods"""def save_model(self, model_path=None):if model_path is None:model_path = self.model_pathutils.check_dir(model_path)torch.save(self.state_dict(), model_path)logging.info('Save model to ' + model_path[:50] + '...')def load_model(self, model_path=None):if model_path is None:model_path = self.model_pathself.load_state_dict(torch.load(model_path))logging.info('Load model from ' + model_path)def count_variables(self):total_parameters = sum(p.numel() for p in self.parameters() if p.requires_grad)return total_parametersdef actions_before_train(self):passdef actions_after_train(self):pass"""Define dataset class for the model"""

再來看最關鍵basemodel,這涉及到模型具體是怎么實現的。首先用靜態變量的方式指定 reader 和 runner ,指定了它的幫助類是什么,具體用哪個 reader 去讀數據,用哪個 runner 去訓練和評測模型。

這里有一些通用的與模型相關的參數,可以增量的添加。前面是一些與模型相關的參數,包括定義模型里面具體有哪些可學習的參數,prediction 怎么去進行,loss 具體是什么,每個 customize parameters 應該是怎么樣去設置。

后面有一些工具類的函數,再往后是上文提到的把 dataset 的類寫成一個 model 的內部類,目的主要還是希望能在寫模型的過程中,在一個文件里既準備對應的? ?batch,同時定義模型具體在前面怎么forward的。

因為在構建模型時,特別在研究過程中,經常需要變換輸入的信息、輸入的格式,在 forward 中來做相應的這種變換,如果經常需要換文件,或者改動調試,是比較痛苦的,所以考慮把它以內部類的形式呈現。

代碼繼承的 basedataset 其實是 pytorch 中內置的傳給 dataloader 的 dataset ,只是把它改了一個名字,因為這個類本身也想要dataset。如果想用dataset,通過官方方式去使用它的話,一般需要去重寫兩個函數,一個是 len 函數,一個是 getitem 函數。len 函數完成的任務是獲得 basedataset 中存的數據一共有多少個?getitem 是根據給定的 index ,去獲得對應數據中的 index ,要輸入給模型的 batch 。

如何實現這兩個函數?這里面的data是什么?是basereader 讀進來的dataframe ,但是這里為了方便,準備了多線程 batch(dataframe 對于多線程訪問不太友好),所把它轉成一個dict。

class Dataset(BaseDataset):def __init__(self, model, corpus, phase):self.model = modelself.corpus = corpusself.phase = phaseself.data = utils.df_to_dict(corpus.data_df[phase])# ↑ DataFrame is not compatible with multi-thread operationsself.neg_items = None if phase == 'train' else self.data['neg_items']# ↑ Sample negative items before each epoch during trainingself.buffer_dict = dict()self.buffer = self.model.buffer and self.phase != 'train'self._prepare()def __len__(self):for key in self.data:return len(self.data[key])def __getitem__(self, index):return self.buffer_dict[index] if self.buffer else self._get_feed_dict(index)# Prepare model-specific variables and buffer feed dictsdef _prepare(self):if self.buffer:for i in tqdm(range(len(self)), leave=False, ncols=100, mininterval=1,desc=str('Prepare ' + self.phase)):self.buffer_dict[i] = self._get_feed_dict(i)# Key method to construct input data for a single instancedef _get_feed_dict(self, index):target_item = self.data['item_id'][index]neg_items = self.neg_items[index]item_ids = np.concatenate([[target_item], neg_items])feed_dict = {'item_id': item_ids}return feed_dict# Sample negative items for all the instances (called before each epoch)def negative_sampling(self):self.neg_items = np.random.randint(1, self.corpus.n_items, size=(len(self), self.model.num_neg))for i, u in enumerate(self.data['user_id']):user_clicked_set = self.corpus.user_clicked_set[u]for j in range(self.model.num_neg):while self.neg_items[i][j] in user_clicked_set:self.neg_items[i][j] = np.random.randint(1, self.corpus.n_items)# Collate a batch according to the list of feed dictsdef collate_batch(self, feed_dicts):feed_dict = dict()for key in feed_dicts[0]:stack_val = np.array([d[key] for d in feed_dicts])if stack_val.dtype == np.object: # inconsistent length (e.g. history)feed_dict[key] = pad_sequence([torch.from_numpy(x) for x in stack_val], batch_first=True)else:feed_dict[key] = torch.from_numpy(stack_val)feed_dict['batch_size'] = len(feed_dicts)feed_dict['phase'] = self.phasereturn feed_dict

上面的代碼是我主要的 data ,確定phase具體在哪個階段,是train 的階段還是在評測 validation 、test 階段。

len 部分直接獲得了data的長度,被很多人吐槽,把data變成了一個dict的形式,它本身是一個data frame,每一列的長度是一樣的,直接返回了第一列的長度。

getitem主要的功能放在 get_feed_dict 里面去完成。這是根據是否需要 buffer 去做選擇。在數據集比較小的時候,如果條件允許的話,對于驗證集跟測試集,完全可以把它所有的 batch 提前準備好放在內存里,這樣訓練、測試就會更快一些。如果不去 buffer 的話,每次現場做準備都要重復工作。

get_feed_dict 主要給index 返回對應的 predict ,也就是輸入到模型的 batch 。base model 本來可能是抽象的,但還是把它寫成可以運行的類。模型之后可以回返回來再看,根據給定的每一個 item ID ,去定義每個 item ID 對應的 bias ,然后直接把輸出的 bias 作為預測的值。所以在這要為它準備 item ID ,target_item ID可以直接從data中item ID的類直接取出即可。

我們會提前準備好負例。訓練集通過函數在每一輪之前進行采樣。可以通過這一步從成員變量中直接獲得對應的負例,并和target一起傳到模型里,相當于返回了每一個index 對應的 free_dict 。而要傳遞模型的一個 batch 相當于一個群組,好多index 組成一個 batch ,也相當于一個 free_dict 的list 來組成一個 batch ,等于把相同的 key 當中的 value 組合到了一起。

注意看代碼部分默認帶有函數,由于后面可能涉及到不同的歷史、長度,這里需要做動態的pad,這部分也重寫了一些。當然,如果檢測到序列的長度不一致,也會進行一個填充的操作,填充到同樣的長度。也可以去添加一些整體上的控制變量。

以上這部分這是 dataset 比較重要的一部分,控制了怎么去給模型輸入,包含了每個 batch 必須要有的內容。

可以再看看整體的模型, item_bias 部分對于我輸入的 item_ID ,可以直接取對應的 bias 作為 prediction ,進而返回對應的 prediction 結果。

到這一部分,其實整個框架已經完成了對使用者的幫助工作。全部的代碼量非常少,所以使用者可以很快上手。

2.3?實例演示

看完以上的引導,還是不知道怎么創建新模型怎么辦?下面繼續手把手教到底,通過一段視頻教大家怎么基于框架在 5 分鐘時間里實現一個 BPR。

看完快速上手視頻,我們對整個框架做完了比較細致的梳理,希望能夠幫助大家更好地上手、更好地使用它。

相關論文方法介紹

下面準備了一些相關具體算法的介紹,也是我們最近一項工作的介紹,可能比較偏模型、偏理論一些。

上圖是我們現在所實現的模型的性能對比,可以看到,基于深度模型的NCF,如果在調參調得不好的情況下,比 BPR 還要差很多。引入了時間信息的 Tensor ,效果會有明顯提升。對于序列的模型來說,因為有序列的信息效果是不錯的。

我們逐一簡單講一講:

1. SASRec 基于 self_attention,如果好好調參,效果確實會非常好。

2. TiSASRec是今年剛提出來的,把時間間隔用embedding的方式去融入到self_attention,也能取得稍微更好一點點的結果,但它的運行時間就會多很多。

3. CFKG 則是一個融入知識圖譜的推薦,效果也是很不錯的。

4. 最后兩個模型,是把知識圖譜、時間相關,還有序列的信息都用進來,也獲得非常好的結果。

接著介紹一下,這里面表現最好的模型,大概是一個什么樣的結構。

這是我們團隊在SIGIR的論文:Make It a Chorus:Knowledge-and Time-aware Item Modeling for Sequential Recommendation

首先是motivation,做這項工作的目的在于,我們感覺現在的推薦系統有很多問題。舉個例子,我剛買完手機,你認為我會很喜歡電子產品,所以就會推薦很多款手機,但其實我此時已經不需要了。

如果比較智能的算法,可能會去推薦Air Pods,作為配件而言,我對它的需求可能會提升。但是這樣的智能可能還是不夠,如果我已經在其他平臺上買過無線耳機,我現在也就不需要無線耳機,系統可能覺得我需要,但是我實際不需要。我剛開始可能覺得系統挺智能的,但是如果一直去推Air Pods,我會覺得很蠢。

不同的推薦應該會隨著時間有一定的衰減,所以這篇文章所提出來的主要想法,就是每一個item可能在不同的context下,在不同的時間下,扮演不同的角色。

還有一些具體的例子,如果我之前買的是iphone,它對于目標商品Air Pods有沒有互補的關系?它對我購買Air Pods影響應該短期內是正的,但會隨時間慢慢遞減的影響。而如果我之前買的商品是Air Pods同類商品,是替代品Powerbeats,那么短期內應該是有負向的影響,但是隨著時間的增長,可能到該換耳機的時候,反而會得到正向的影響,是分配時間和負向變化正向的這樣的一個過程。

具體怎么去設計這個模型?我們想讓模型在item扮演不同角色的時候,有不一樣的靜態表示,比如在context下扮演互補品、替代品的時候是怎樣的角色。然后根據序列的情況,把這些靜態的表示,與現在有沒有在扮演這個角色進行動態結合,包括之間間隔的時間,每一個扮演的角色有可能有正向、負向影響或者不起作用。例如給AirPods一個基本的表示,還有作為互補品的表示,作為替代品的表示,在不同context下就會都會起到更多的作用,下圖是具體的模型圖。


上圖左邊部分,進行了知識圖譜嵌入,但這其實并不是工作重點,所以我們用了一個比較常見的關系建模,對商品之間的關系進行向量的切入,這些向量也會作為每個商品的基本表示進行數據化。

上圖右半部分是第二個階段,基于左邊的表示,即對每個商品有很基本的表示,我們還希望得到它跟relation相關的表示,通過這樣的translation,在第一個階段里面使用的translation方式,去得到扮演不同角色時的靜態表示,這樣對于每一個商品,都有了基本表示和扮演不同角色時的表示。

這時候,就需要根據 context 去對它們做動態加和,用到叫做 time_aware integration weight 的方法,它是怎么設計的呢?就是去挑選歷史里面跟目標商品有關系的歷史交互,看它們對我的影響到底是什么,這個具體的影響有一個稱為temporal kernel function 設計的函數去控制,是一個疊加的效應。

temporal kernel function 的方式怎么設計?其實會根據先驗知識,或者是希望這個系統展現出來一個什么樣的效果去設計,比如對于互補品設計成遞減,對于替代品則是從負向到正向的變化,這樣能控制扮演不同角色時的靜態 embedding 在整個動態的結合過程中所做的貢獻。

基于這樣動態表示,就等于得到了一個目標商品在目前context下的動態表示,這個表示可以用到很多基于embedding模型里面,比如像BPR、GMF最后會統一去ranking loss。

上圖是大概數據的信息,和剛才所提到的兩種關系。

上圖是實驗結果,大致情況是我們的模型能夠比之前所提到的引入知識、引入時間動態性的模型有比較明顯的提升。

上圖是 relation 的分析,圖中的\R跟\T分別是去掉第一階段的 knowledge graph 和第二階段的 temporal kernel function ,不考慮時間動態變化的影響。可以看到,影響最大的還是商品關系所帶來的,但是有時候商品關系可能處理得不好,這個時候動態結果就起到很大作用,如果不對不同的關系做時間動態變化的結合,\T會帶來非常嚴重的損失,所以時間在所有數據上也有比較一致的提升。

最后,有趣的是,我們看了不同類型商品所求出的 temporal kernel function 方式長什么樣?是否反映該類商品的一些特征?

上圖左邊是互補品求出來的 temporal kernel function ,它相較于可替代商品,下降曲線會更緩一些。這說明什么?說明可能用戶過了一段時間之后,還會對這種可替代商品,比如替換的電池和之前老的智能手機還有興趣,有可能過很長時間才會換。

而對于頭戴式耳機來說,interest 下降就會非常快。就像上文提到的,有可能這個耳機我就不需要了,所以這個分數很快降下來,而不會過多打擾到用戶。

上圖右邊是替代品的 temporal kernel function 方式寫出來的結果。對于像手機殼這一類商品,它的負向影響基本上被削平了,主要是正向的影響,使得它的峰值會不太一樣。這說明了,之前購買手機殼的行為對于購買下一個手機殼,其實沒有很多負向影響。用戶可能因為很多原因去換手機殼,比如摔壞了一個角,或者只是看到外觀就換了,所以負向影響非常少。

而對于充電器、手機,它的負向影響和正向影響都非常明顯。比較奇妙的是,兩者峰值大概都處于一個位置,這其實也說明它倆是有一定依賴關系,因為可能不同的類型的手機配不同類型的充電頭,反映了商品內部的這種關聯。

這個模型在我們現在的框架中表現也是比較突出的。總結來說,這種模型主要提出了對于目標商品的動態表示,能夠比較方便運用到各種基于 embedding 的方法中,并且進一步提升模型的性能等。

總結

最后做個總結,我們介紹 ReChorus 這種 top k 推薦框架,它目前會比較適合兩類人群:作為初學者,可能想要了解一些經典推薦系統相關的算法,可以通過它去快速了解經典算法具體是怎么實現的;對于研究者來說,也可用它來測試一些新的idea,比較模型的性能。

但現在我覺得 ReChorus 還有很多的問題,包括只有一個內置數據集去比較,可能某些實驗設定上還需要進一步提煉,比如最近一篇 ACL best paper 提出的思路,用類似軟工的形式進行NLP的全面評測。不知道之后推薦系統方向是否會有相應的內容。

ReChorus 未來存在很多可以改善的空間,也非常歡迎廣大同行研究者們提交 issue 來完善這個框架,共同構建真正的推薦算法的“合唱團”,不僅僅實現表面上的百花齊放(大量論文的涌現),也去真正推動這個領域一步一個腳印、實打實地進步。

關于數據實戰派

數據實戰派希望用真實數據和行業實戰案例,幫助讀者提升業務能力,共建有趣的大數據社區。

更多閱讀

#投 稿?通 道#

?讓你的論文被更多人看到?

如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。

總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。?

PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學習心得技術干貨。我們的目的只有一個,讓知識真正流動起來。

?????來稿標準:

? 稿件確系個人原創作品,來稿需注明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向)?

? 如果文章并非首發,請在投稿時提醒并附上所有已發布鏈接?

? PaperWeekly 默認每篇文章都是首發,均會添加“原創”標志

?????投稿郵箱:

? 投稿郵箱:hr@paperweekly.site?

? 所有文章配圖,請單獨在附件中發送?

? 請留下即時聯系方式(微信或手機),以便我們在編輯發布時和作者溝通

????

現在,在「知乎」也能找到我們了

進入知乎首頁搜索「PaperWeekly」

點擊「關注」訂閱我們的專欄吧

關于PaperWeekly

PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。

總結

以上是生活随笔為你收集整理的清华大学王晨阳:轻量级Top-K推荐框架及相关论文介绍的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

日韩精品在线免费播放 | 2022中文字幕在线观看 | 久久久久免费精品国产小说色大师 | 国产亚洲日| 亚洲男男gⅴgay双龙 | 久久精品欧美日韩精品 | 开心激情五月网 | www.一区二区三区 | 日韩黄色中文字幕 | 国产成人在线免费观看 | 夜夜夜夜操 | 国产一区二区免费在线观看 | a黄色一级 | 国产高清在线精品 | 成人免费在线观看av | 人人玩人人添人人澡97 | 亚洲精品五月天 | 四虎在线免费 | 在线看国产 | 中文字幕日韩精品有码视频 | 国产精品无 | 97夜夜澡人人双人人人喊 | 97视频人人澡人人爽 | 在线观看mv的中文字幕网站 | 中文字幕在线人 | 丰满少妇对白在线偷拍 | 香蕉视频网站在线观看 | 91精品在线视频观看 | 国产精品免费在线播放 | 在线观看电影av | 96视频免费在线观看 | 亚洲精品99久久久久久 | 婷婷六月激情 | 婷色| 麻豆久久一区二区 | 天天插狠狠干 | 国产色在线 | 在线观看视频在线 | 永久精品视频 | 夜色资源站国产www在线视频 | 色婷婷狠狠操 | 欧美午夜a| 亚洲高清视频在线 | 91热视频 | www天天干 | 96在线 | 黄色免费高清视频 | 日韩久久精品一区二区三区下载 | 久草在线视频网站 | 久久手机精品视频 | 日韩精品视频一二三 | 久久国产精品一区二区三区 | 久久精品久久99精品久久 | 久久精品美女 | 91精品久久久久久粉嫩 | 99热这里只有精品8 久久综合毛片 | 国产成人精品在线播放 | 欧美精品一区二区免费 | 国产精久久久久久妇女av | 国产精品美女www爽爽爽视频 | 免费三级网 | 在线一区观看 | 国产精品18久久久 | 色五月情 | 又爽又黄又无遮挡网站动态图 | 日韩av在线网站 | 日韩精品一区二区在线观看 | 极品中文字幕 | 亚洲精品色视频 | 男女激情免费网站 | 成人播放器 | 九九九热精品免费视频观看 | 国产日韩精品久久 | 免费国产亚洲视频 | 欧美日韩高清一区二区 | 国产又黄又爽又猛视频日本 | 97在线免费观看视频 | 天天撸夜夜操 | 国产高清专区 | 国产一级片播放 | 97手机电影网 | 激情网五月天 | 日日摸日日添日日躁av | 国产日韩欧美在线观看视频 | 久久综合久久综合九色 | 国产成人免费观看 | 久久热首页 | 999视频在线播放 | 国产一区二区高清不卡 | 中文字幕久久网 | 欧美日比视频 | 在线视频1卡二卡三卡 | 青青草国产精品 | 99久久婷婷国产综合精品 | 久久综合色影院 | 欧美一级片在线免费观看 | 青青河边草免费观看 | 久精品一区 | 在线观看视频你懂的 | 国产精品免费久久久久 | 欧美一区二区在线 | 免费观看不卡av | 99精品视频免费全部在线 | 伊人色综合久久天天网 | 99视频精品全国免费 | 国产美女免费观看 | 婷婷六月丁香激情 | 久久久久久久久久久黄色 | 在线精品在线 | 欧美a在线免费观看 | 日韩av在线免费看 | 国产二级视频 | 91精品免费在线 | 国产精品一区二区av影院萌芽 | 最近中文字幕免费大全 | 国产亚洲精品成人av久久ww | 人人爽人人看 | 夜色资源站wwwcom | 成人小视频在线观看免费 | 成人精品久久久 | 日韩免费在线网站 | av大全在线播放 | 国产成人一区二区三区在线观看 | 亚洲国产天堂av | 久久国产三级 | 日本久热 | 日韩一区二区三 | 精品99久久| 成年人电影免费看 | 在线av资源| 亚洲激情视频在线 | 看黄色.com| 国内精品久久久久久中文字幕 | 国产精品系列在线观看 | 日韩动态视频 | 中文字幕中文字幕在线一区 | 国产亚洲精品久久久久久 | 91大神精品视频 | 不卡视频在线看 | 日韩av电影一区 | 国产成人精品三级 | 久久免费播放 | 中文在线中文资源 | 国产精品久久久久四虎 | 狠狠色丁香婷婷综合久久片 | 国产精品入口传媒 | 久草网视频在线观看 | 久草综合在线 | 亚洲国产精品va在线 | 国产精品不卡在线观看 | 欧美日韩p片 | 最新av在线免费观看 | 91香蕉视频 mp4 | 久久一区二区免费视频 | 欧美精品亚洲精品日韩精品 | 超碰人人干人人 | 国产亚洲视频在线免费观看 | 婷婷亚洲综合五月天小说 | 久久久影院| 欧美激情综合色综合啪啪五月 | 国产免费激情久久 | 国产成人免费 | 97国产一区 | 中文av在线播放 | 国产一级片网站 | 国产精品久久久免费看 | 欧美日韩国产亚洲乱码字幕 | 伊人影院在线观看 | 欧美日本在线视频 | 精品国产一区二区久久 | 尤物97国产精品久久精品国产 | av超碰在线| 国产成本人视频在线观看 | 夜夜躁狠狠躁日日躁视频黑人 | 黄色www | 在线观看视频h | 午夜天使 | 久久久久亚洲精品中文字幕 | 亚洲高清在线视频 | 精品亚洲免a | 日狠狠 | 日日夜夜国产 | 欧美精品久久久久久久 | 欧美一级看片 | 天天射夜夜爽 | 国产91精品一区二区麻豆网站 | 欧美日韩国产精品爽爽 | 99r国产精品| 99久久超碰中文字幕伊人 | avwww在线观看 | www成人av| 丁香花在线观看免费完整版视频 | 久久亚洲区 | 日本mv大片欧洲mv大片 | 国产精品国内免费一区二区三区 | 蜜臀av一区 | 成人午夜毛片 | 五月婷婷天堂 | 亚洲九九九在线观看 | 中文字幕亚洲综合久久五月天色无吗'' | 午夜国产在线 | 国产日韩在线播放 | 国产精品6 | 欧美精品第一 | 久久久在线免费观看 | 美女视频是黄的免费观看 | 色婷在线 | 色视频在线 | 午夜精品久久久久久久99无限制 | 揉bbb玩bbb少妇bbb | 久久99久久久久 | 国产欧美在线一区二区三区 | 天天操人人要 | 亚洲精品乱码久久久久久蜜桃91 | 91在线一区二区 | 天天插天天干天天操 | 亚洲精品va | 999久久久久 | 亚洲国产精彩中文乱码av | 免费一级片在线 | 国产成人一区二区精品非洲 | 欧美一级特黄高清视频 | 精选久久 | 91福利视频久久久久 | 免费av电影网站 | 欧美日韩久久不卡 | 久久久久久久免费看 | 日韩免费在线观看网站 | 视频成人永久免费视频 | 最新的av网站 | 日韩亚洲欧美中文字幕 | av电影在线不卡 | 九九久久电影 | 国产美女在线精品免费观看 | 伊人夜夜| 日韩av中文| www.天天色.com | 久草视频免费观 | 91精品国产一区二区在线观看 | 中文字幕在线一区观看 | 国产色小视频 | 久久亚洲综合国产精品99麻豆的功能介绍 | 成人精品一区二区三区电影免费 | 天天添夜夜操 | 在线观看免费高清视频大全追剧 | 久久天天综合网 | 国产在线看 | 欧美日本日韩aⅴ在线视频 插插插色综合 | 在线观看黄色小视频 | 91成品人影院 | av一区在线| 美女精品在线 | 欧美性生活一级片 | 91丨九色丨国产丨porny精品 | 四虎影视精品 | 日韩黄色免费 | 在线观看日韩中文字幕 | 日韩高清在线一区二区 | 天天综合在线观看 | 久草精品视频在线播放 | 美女福利视频 | 99精品视频一区 | 九九免费精品视频在线观看 | 在线观看一区 | 国产r级在线观看 | 超碰人在线| 成人a大片 | 天堂av在线| 国内久久视频 | 国产成人精品一区二区在线 | 在线观看的a站 | 久久精品三 | 国产精品av在线 | 日本亚洲国产 | 91精品视频免费观看 | 91视频下载 | 亚洲综合一区二区精品导航 | 深夜国产福利 | 久久久免费视频播放 | 久久99久久99精品 | 999在线精品 | 极品久久久久久久 | 色中射| 国产99免费 | 亚洲激色| 青青久草在线视频 | 精品国产网址 | 在线观看亚洲专区 | 91亚洲永久精品 | www.久久免费视频 | 国产精品久久久久久五月尺 | 久久久高清免费视频 | 欧美大片在线观看一区 | 久久精品国产亚洲精品 | 欧美极品在线播放 | 日韩久久精品一区二区 | av免费在线观看网站 | 欧美精品中文在线免费观看 | 久久综合给合久久狠狠色 | 天天伊人网 | 爱爱一区 | 97视频网站| 久久久久久综合 | 亚洲综合最新在线 | 免费看一及片 | 97精品国产91久久久久久 | 黄色软件在线观看 | 国产流白浆高潮在线观看 | 亚洲国产视频a | 天天综合五月天 | 日本69hd | 日本九九视频 | 激情中文在线 | 国产黄色精品网站 | 欧美国产日韩一区二区三区 | 欧美国产日韩一区 | 久久高清av | 日本韩国精品一区二区在线观看 | 亚洲精品在线一区二区三区 | 97视频在线播放 | 午夜 免费 | 国产精品久久久久婷婷二区次 | 欧美一区三区四区 | 激情欧美一区二区三区免费看 | 亚洲午夜av | 正在播放国产一区二区 | 久久精品一区二区三区四区 | 98久久| 国产午夜麻豆影院在线观看 | 国产精品理论在线观看 | 二区中文字幕 | av手机版 | 午夜精品一区二区三区免费视频 | 九九久久免费视频 | 最新超碰| 最新av免费在线 | 97成人在线免费视频 | 天天夜夜操 | 最近中文字幕高清字幕在线视频 | 免费看片网址 | 99久高清在线观看视频99精品热在线观看视频 | 免费在线观看不卡av | av在线h| 在线视频婷婷 | 国产精品日韩在线观看 | 日韩在线视频一区 | 在线欧美a| 日韩xxx视频| 欧美日韩免费在线观看视频 | 中文字幕日本在线观看 | 亚洲激情久久 | 狠狠的操狠狠的干 | 成人a免费看 | 久久av免费 | 亚洲精品视频在线观看网站 | 久久国产精品免费观看 | 美女视频黄网站 | 欧美综合在线视频 | 国产高清视频免费最新在线 | 欧美日韩国产一区二区三区在线观看 | 2021国产视频 | 国产系列在线观看 | 日韩精品免费在线视频 | 国产日本在线 | 天天射天天干天天 | 伊人网站 | 婷婷久久五月 | 99久久精品午夜一区二区小说 | 久久成人午夜视频 | 国产高清视频 | 精品国产一区二区三区久久影院 | 欧美一区二区精美视频 | 日韩电影一区二区三区 | 一级做a爱片性色毛片www | 亚洲一级黄色片 | 超碰人人乐 | 久久区二区 | 久草视频在线新免费 | 中文字幕在线观看免费高清电影 | 一级a性色生活片久久毛片波多野 | 久久一久久 | 国产中文字幕在线免费观看 | 8x成人免费视频 | 在线观看中文字幕一区二区 | 在线午夜电影神马影院 | 草久在线视频 | 超碰成人免费电影 | 亚洲精品视频在线播放 | 婷婷在线网站 | 973理论片235影院9 | 狠狠狠狠狠狠狠狠干 | 亚色视频在线观看 | 欧美成人一二区 | 亚洲精品乱码久久久久久蜜桃动漫 | 毛片一区二区 | 国产精品久久毛片 | 欧美日韩aaaa| 欧美色一色 | 国产免费观看高清完整版 | 福利片视频区 | 国产精品91一区 | 在线观看免费黄视频 | 久久久久亚洲天堂 | 亚洲电影av在线 | 成人h视频在线 | 探花视频在线版播放免费观看 | 91免费试看 | 97综合网| 亚洲综合小说 | 四虎欧美 | 日日日干 | 国产手机av在线 | 波多野结衣久久精品 | 九九视频一区 | 国内三级在线观看 | 激情综合婷婷 | 国产探花在线看 | 国产涩图 | 色综合久久久久综合体 | 国产在线a不卡 | 精品欧美乱码久久久久久 | 亚洲女人av | 国产综合精品久久 | 91亚洲国产成人久久精品网站 | 亚洲精品自拍 | 成年人黄色大全 | 91在线porny国产在线看 | 国产中文| 人人草在线观看 | 日韩成人精品一区二区三区 | 国产在线永久 | 欧美色综合久久 | 亚洲首页 | 激情 婷婷| 国产成人精品一区二区三区福利 | av成人资源 | 国产视频中文字幕在线观看 | 国产精品嫩草影视久久久 | 久久99精品久久久久蜜臀 | 日本精品视频在线 | 亚洲欧美日韩国产一区二区三区 | 一级a毛片高清视频 | 婷婷激情小说网 | 国产福利小视频在线 | 久久人人爽人人爽 | 天堂视频一区 | 亚洲精品乱码久久久久久写真 | 亚洲女同videos | 日韩美精品视频 | 在线看一区 | 精品国产视频一区 | 在线观看黄色国产 | 亚洲影视九九影院在线观看 | av电影在线免费观看 | 九九久久电影 | 天天拍天天色 | 精品99在线观看 | 日韩高清国产精品 | 中国精品一区二区 | 久久免费精品视频 | 超碰97成人 | 亚洲精品伦理在线 | 美女免费视频观看网站 | 99久久精品午夜一区二区小说 | 免费www视频 | 久久久久久久久久国产精品 | 天天干天天做天天爱 | 日p视频 | 一色屋精品视频在线观看 | 美女黄频 | 色亚洲激情 | 最新极品jizzhd欧美 | 亚洲视频分类 | 91热视频| 中文字幕一区二区三区乱码不卡 | 五月天精品视频 | 欧美日韩亚洲第一 | 国产午夜三级一区二区三 | 91精品少妇偷拍99 | 在线播放一区二区三区 | 免费色视频 | 午夜视频在线观看一区二区三区 | 婷婷黄色片 | 美女黄频视频大全 | 欧美色综合天天久久综合精品 | 国产日韩欧美精品在线观看 | 久久精品香蕉视频 | 亚洲人在线 | 黄色一级免费电影 | 99精品免费在线 | 91人人网| 成人中文字幕av | 久久综合狠狠综合久久激情 | 日韩有码中文字幕在线 | 91精品国产自产老师啪 | 探花视频免费在线观看 | 日韩久久在线 | 成人亚洲综合 | 国产一级片免费播放 | 五月婷婷网站 | 精品久久久久久久久久久久久久久久久久 | 色a在线观看 | 精品国产乱码久久久久久三级人 | 日韩午夜在线播放 | 欧美日韩国产高清视频 | 午夜av色 | 日韩中文字幕a | 欧美一级日韩三级 | 亚洲精品综合一二三区在线观看 | 久久综合久久综合这里只有精品 | 97超在线视频 | 国产在线观看91 | 99热只有精品在线观看 | 国产欧美精品一区二区三区四区 | 亚洲精品免费在线播放 | 一区二区三区在线观看免费视频 | 93久久精品日日躁夜夜躁欧美 | 97成人在线观看视频 | 久久尤物电影视频在线观看 | 337p西西人体大胆瓣开下部 | 探花视频在线观看免费版 | 国产一区二区三区在线 | 蜜桃av久久久亚洲精品 | 久久久免费精品 | 日日夜夜综合网 | 久久伦理网 | 欧美性色综合 | 亚洲 中文 在线 精品 | 1000部国产精品成人观看 | 在线观看免费成人av | 狠狠夜夜| 中文字幕人成不卡一区 | 日韩av电影中文字幕在线观看 | 91你懂的 | 日韩视频免费观看高清完整版在线 | 丁香婷婷深情五月亚洲 | 免费观看视频的网站 | 精品国产伦一区二区三区观看方式 | 中文字幕专区高清在线观看 | 五月天综合网站 | 成人免费视频免费观看 | 欧美日韩高清在线一区 | 国产精品久久久视频 | 日韩免费一区二区在线观看 | 久久人人做 | 女人魂免费观看 | av在线色| 久久超| 亚洲最大成人网4388xx | 久久第四色 | 超碰97中文| 91完整版在线观看 | 激情婷婷 | 久久tv | 国产一区二区午夜 | 日韩精品一区二区在线视频 | 国产黄色片一级 | 亚洲国产成人高清精品 | 久久天天躁夜夜躁狠狠85麻豆 | 亚洲天堂在线观看完整版 | 国产一级在线观看 | 国产精品久久嫩一区二区免费 | 成人三级黄色 | 美女视频免费一区二区 | 中文字幕精品三级久久久 | 在线亚洲高清视频 | 欧美日韩国产精品一区二区亚洲 | 国产 中文 日韩 欧美 | 五月婷婷丁香色 | 人人干干人人 | 91亚洲国产成人 | 国产成人精品一区一区一区 | 中文av日韩 | 99久久精品免费看国产麻豆 | 亚洲精品国产精品乱码在线观看 | 少妇自拍av | 久久综合偷偷噜噜噜色 | 久久精品久久精品 | 天天综合区 | 久久久久免费网站 | 亚洲视频资源在线 | 免费三级影片 | 日韩大片在线看 | 亚洲成av人片一区二区梦乃 | 91免费网站在线观看 | 午夜精品久久久久久久99水蜜桃 | 五月天亚洲婷婷 | 国产精品99久久久精品 | 国产三级av在线 | 91在线中字 | 香蕉影视在线观看 | 婷婷国产在线 | 亚洲狠狠婷婷综合久久久 | 亚洲一区精品二人人爽久久 | 97av视频在线| 成人福利在线 | 久久免费黄色大片 | 成人黄色片在线播放 | 91九色蝌蚪国产 | 婷五月激情 | 国产原创在线 | 国产精品九九久久99视频 | 久久精品国产亚洲精品2020 | 国产成人三级在线观看 | 亚洲成人高清在线 | 国产一级淫片免费看 | 国产手机视频 | 黄色av一级| 亚州成人av在线 | 精品一区二区在线观看 | 正在播放国产一区 | 国产一级黄色片免费看 | 久久精品久久久精品美女 | 808电影免费观看三年 | 人人网av| av在线短片 | 欧美黄色特级片 | 最新极品jizzhd欧美 | 婷婷丁香激情 | 国产一区二区三区 在线 | 中文字幕 在线看 | 婷婷综合久久 | 亚洲一级特黄 | 亚洲国产精品一区二区尤物区 | 在线免费观看视频你懂的 | 国产美女精品人人做人人爽 | 国产日韩精品一区二区三区 | 亚洲电影一区二区 | 视频二区 | www夜夜操| 五月婷av | 99视频在线精品免费观看2 | 激情五月综合网 | 久草免费手机视频 | 高清日韩一区二区 | 91丨精品丨蝌蚪丨白丝jk | 在线观看中文字幕第一页 | 久久国产午夜精品理论片最新版本 | 天天爽夜夜爽人人爽曰av | 免费久久视频 | 高清视频一区 | 国产成人精品网站 | 人人网av | 五月婷婷久久综合 | 亚洲欧美国产精品va在线观看 | av色影院| 亚洲欧洲精品视频 | 国产一区免费看 | 黄色网址国产 | 久草资源免费 | 亚洲成人精品久久久 | 69av免费视频 | 在线播放一区二区三区 | 99综合久久 | 人人澡人人爱 | 亚洲第一区在线播放 | 成人av动漫在线观看 | 在线观看成年人 | www.天天干.com| 亚洲视频电影在线 | 黄色一级片视频 | 日韩欧美视频一区二区三区 | a级片在线播放 | 国产精品网红福利 | 国产人免费人成免费视频 | 国产一区二区日本 | 日韩精品一区二区三区第95 | 日本在线精品视频 | 天天综合在线观看 | 蜜桃视频在线观看一区 | 成人黄色在线观看视频 | 六月丁香社区 | 天天色天天操综合网 | 91伊人久久大香线蕉蜜芽人口 | 成人国产电影在线观看 | a电影在线观看 | 日韩 精品 一区 国产 麻豆 | 在线日本v二区不卡 | 日韩亚洲欧美中文字幕 | 久久你懂的 | 丁香花在线视频观看免费 | 日韩在线观看视频在线 | 97超碰在线久草超碰在线观看 | 美女久久久久久久 | 欧美精品乱码久久久久 | 99精品国产免费久久 | 日韩a级免费视频 | 国产婷婷色 | 在线观看av中文字幕 | 91精品国产综合久久久久久久 | 久久激情日本aⅴ | 中文字幕 国产精品 | 99av国产精品欲麻豆 | 99精彩视频在线观看免费 | 国产精品美女久久久久久久久 | 麻豆国产网站入口 | 日韩特级毛片 | 久久综合久久综合久久 | 日日夜夜天天久久 | 玖玖国产精品视频 | 国产手机在线观看视频 | 日本三级不卡 | 三级黄色理论片 | 国产精品久久久久久久久搜平片 | 久久这里有精品 | 久久久免费高清视频 | 国产系列 在线观看 | 2017狠狠干 | 欧美另类美少妇69xxxx | 日韩精品一区二区不卡 | 国产亚洲成av人片在线观看桃 | 亚洲一区二区高潮无套美女 | 日韩欧美高清在线 | aa一级片| 亚洲麻豆精品 | 日韩av二区 | 婷婷丁香在线 | 国产一区二区在线看 | 久久久黄色免费网站 | 日韩高清二区 | 欧美日比视频 | 女人18毛片a级毛片一区二区 | 欧美一进一出抽搐大尺度视频 | 久久综合网色—综合色88 | 亚洲九九九在线观看 | 日韩一区二区三区视频在线 | 91中文字幕一区 | 在线导航av | 亚洲在线网址 | 香蕉在线观看 | 国产精品av免费 | 中文字幕在线观看视频一区二区三区 | 成人黄视频| 国产高清第一页 | 九九精品视频在线观看 | 亚洲成a人片综合在线 | 少妇超碰在线 | 欧美精品一区二区三区四区在线 | 久久综合中文字幕 | 久草在线视频中文 | 黄色大全免费观看 | 久久久亚洲电影 | 激情小说网站亚洲综合网 | 精品在线观看一区二区三区 | 最新真实国产在线视频 | 亚州精品成人 | 99爱精品视频 | 麻豆视频在线观看 | 激情视频91 | 精品免费视频123区 午夜久久成人 | 成片人卡1卡2卡3手机免费看 | 日韩女同av| 97超碰在线播放 | 一区二区中文字幕在线播放 | 国产一卡二卡四卡国 | 天天天天色综合 | 在线激情影院一区 | 亚洲国产丝袜在线观看 | 超碰人人舔 | 最近高清中文在线字幕在线观看 | 免费久久99精品国产 | 国产色网| 狠狠狠狠狠狠天天爱 | 日韩大片在线免费观看 | 日韩在线高清视频 | 麻豆精品在线 | 91尤物国产尤物福利在线播放 | av久久久| 欧美视频不卡 | 日韩免费在线观看视频 | 国产中的精品av小宝探花 | 日日草天天草 | 久草9视频 | 99热手机在线 | 久久精品这里热有精品 | 亚洲激情视频在线 | 婷婷丁香激情五月 | 亚洲成人精品在线观看 | 日韩在线观看你懂的 | 欧美日性视频 | 2000xxx影视 | 日韩一区二区三区在线看 | 久久艹99| 九九免费观看视频 | 久久69精品久久久久久久电影好 | 婷婷精品国产欧美精品亚洲人人爽 | www.福利| 国产亚洲va综合人人澡精品 | 天天摸夜夜添 | 91麻豆精品国产91久久久无限制版 | 色婷婷视频网 | 欧美在线不卡一区 | 欧美日韩视频在线一区 | 福利视频导航网址 | 精品一区二区三区四区在线 | 色狠狠婷婷 | 激情伊人| 国产精品女同一区二区三区久久夜 | 日韩美女免费线视频 | 午夜视频免费播放 | 久久99热精品这里久久精品 | 日韩色爱| 久草网在线观看 | 国产精品理论片 | 久草99 | 精品一二三四五区 | 久久久久久久久爱 | 在线观看一级 | 日韩精品三区四区 | 激情五月伊人 | 成人在线观看日韩 | 国产综合精品久久 | 午夜精品麻豆 | 色视频在线看 | 摸bbb搡bbb搡bbbb | 欧美激情精品久久久久 | 夜夜操天天干 | 久久久久夜色 | 能在线看的av | 中文字幕日韩电影 | 在线观看中文字幕一区二区 | 久久亚洲欧美日韩精品专区 | 午夜视频在线网站 | 日韩高清一区在线 | 99热在线观看 | 9999国产精品 | 97精品国产97久久久久久粉红 | 91女人18片女毛片60分钟 | 香蕉视频4aa | 亚洲二区精品 | 在线午夜 | 国产精品 日韩精品 | 毛片一级免费一级 | 久久久久久久久久久久99 | 久久久久久久久久久福利 | 日本最新中文字幕 | 在线观看你懂的网址 | 日本爱爱免费视频 | 欧美一级小视频 | 国产视频一区在线 | 国产精品永久免费在线 | 一区二区三区四区免费视频 | 国产亚洲小视频 | avlulu久久精品 | 日本久久成人 | 国产精品9999久久久久仙踪林 | 色99久久| 欧美精品久久久久久久亚洲调教 | 成人在线免费av | 亚洲精品一区二区精华 | 探花视频在线观看免费 | 日韩精品aaa| www·22com天天操 | 亚洲砖区区免费 | 又黄又爽又刺激的视频 | 91精品国产欧美一区二区成人 | 91在线超碰| 国产 在线 高清 精品 | 人人狠狠综合久久亚洲 | 激情黄色一级片 | 麻豆免费在线视频 | 色www永久免费 | 手机看片久久 | 国产a国产 | 国产一级视频在线免费观看 | 日本最大色倩网站www | 天天射天天干天天 | 免费精品在线视频 | 成人国产电影在线观看 | 免费看一级特黄a大片 | 808电影免费观看三年 | 黄色一级在线视频 | 久久中文网 | 国产精品无 | 啪啪午夜免费 | av电影av在线 | 亚洲专区视频在线观看 | 色偷偷888欧美精品久久久 | 五月综合婷 | 亚洲综合导航 | 亚洲午夜精品久久久久久久久久久久 | 色婷婷成人网 | 欧美精品亚洲精品日韩精品 | 国产精品婷婷午夜在线观看 | 97视频入口免费观看 | 久久人人做 | 欧美日韩高清一区二区三区 | av在线免费播放网站 | 97综合视频| av+在线播放在线播放 | 免费高清男女打扑克视频 | 一级黄色片在线观看 | 久久综合狠狠综合 | 欧美亚洲精品一区 | 久久国产精品久久国产精品 | 久草网首页 | 丁香九月激情综合 | 亚洲高清久久久 | 玖玖爱在线观看 | 欧美少妇18p | 国产精品成人自产拍在线观看 | 日韩动漫免费观看高清完整版在线观看 | 97国产| 五月开心激情 | 国产精品久久久久影院日本 | 亚洲精品66 | 日韩高清dvd | 中文字幕在线观看第三页 | 免费在线a| 五月天网站在线 | 色偷偷人人澡久久超碰69 | av电影免费 | japanesefreesexvideo高潮| 视频一区久久 | 日韩视频二区 | 精品国产一区二区三区在线观看 | 婷婷六月天综合 | 一级免费观看 | 免费成人短视频 | 99久久99久国产黄毛片 | 欧美a影视 | 综合色狠狠 | 日本精品一区二区在线观看 | 中文字幕中文中文字幕 | 99久久精品久久久久久清纯 | 99热最新地址 | 激情婷婷亚洲 | 欧美精品一区在线发布 | 97视频人人免费看 | 91麻豆精品国产91久久久无限制版 | 四虎伊人 | 国产日韩欧美在线 | 99久久久成人国产精品 | 久久久久国产精品午夜一区 | 国产精品五月天 | 色播五月激情综合网 | 99精品乱码国产在线观看 | 亚洲精品毛片一级91精品 | 在线观看视频黄 | 黄色特级一级片 | av爱干| 在线 影视 一区 | 亚洲天天摸日日摸天天欢 | 日本黄色免费在线观看 | 亚州精品在线视频 | 99久久久国产精品美女 | 久久久久久久免费观看 | 日本精品一区二区三区在线观看 | 美国人与动物xxxx | 国产小视频免费在线观看 | 黄色中文字幕在线 | 欧美日韩亚洲第一 | 国产福利中文字幕 | 国产一级片在线播放 | 91在线播 | 在线免费黄色 | 在线观看韩日电影免费 | 久久国产精品免费一区 | 天天爽天天爽天天爽 | 亚洲理论电影 | 婷婷福利影院 | 久久图 | 国产高清精品在线观看 | 日本公妇在线观看高清 | 亚洲成a人片在线观看中文 中文字幕在线视频第一页 狠狠色丁香婷婷综合 | 天堂资源在线观看视频 | 黄网站色成年免费观看 | 欧美日韩精 | 999视频网| 五月婷婷另类国产 | 98久久 | 天天摸天天操天天舔 | 国产综合在线视频 | 一区二区 精品 | 免费黄色在线 | 欧美性生活免费 | 日韩免费观看一区二区三区 | 精品亚洲一区二区三区 | 日韩成年视频 | 婷婷5月色| 久av在线| 欧美在线一二区 | 在线亚洲午夜片av大片 | 国产 欧美 日产久久 | 久久久香蕉视频 | 国产精品高清在线观看 | 久久人人爽爽人人爽人人片av | 国内精品久久天天躁人人爽 | 久草在线手机视频 | www.色五月.com | 欧美日韩国产在线精品 | 国产首页| 国产精品麻豆视频 | 午夜精品久久久久久久99水蜜桃 | av一级片| 久草在线在线视频 | 中文字幕在线观看视频一区 | 亚洲婷婷丁香 |