召回表設計與模型召回、 離線用戶基於模型召回集

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(不定時更新)


3.4 召回表設計與模型召回

學習目標

  • 目標
    • 知道ALS模型推薦API使用
    • 知道StringIndexer的使用
  • 應用
    • 應用spark完成離線用戶基於模型的協同過濾推薦

3.4.1 召回表設計

我們的召回方式有很多種,多路召回結果存儲模型召回與內容召回的結果需要進行相應頻道推薦合併。

  • 方案:基於模型與基於內容的召回結果存入同一張表,避免多張表進行讀取處理
    • 由於HBASE有多個版本數據功能存在的支持
    • TTL=>7776000, VERSIONS=>999999
create 'cb_recall', {NAME=>'als', TTL=>7776000, VERSIONS=>999999}
alter 'cb_recall', {NAME=>'content', TTL=>7776000, VERSIONS=>999999}
alter 'cb_recall', {NAME=>'online', TTL=>7776000, VERSIONS=>999999}

# 例子:
put 'cb_recall', 'recall:user:5', 'als:1',[45,3,5,10]
put 'cb_recall', 'recall:user:5', 'als:1',[289,11,65,52,109,8]
put 'cb_recall', 'recall:user:5', 'als:2',[1,2,3,4,5,6,7,8,9,10]
put 'cb_recall', 'recall:user:2', 'content:1',[45,3,5,10,289,11,65,52,109,8]
put 'cb_recall', 'recall:user:2', 'content:2',[1,2,3,4,5,6,7,8,9,10]


hbase(main):084:0> desc 'cb_recall'
Table cb_recall is ENABLED                                                                             
cb_recall                                                                                              
COLUMN FAMILIES DESCRIPTION                                                                            
{NAME => 'als', VERSIONS => '999999', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'false'
, KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE => 'false', DATA_BLOCK_ENCODING => 'NONE', TTL => 
'7776000 SECONDS (90 DAYS)', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', CACHE
_INDEX_ON_WRITE => 'false', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_BLOCKS_ON_
OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536'}                    
{NAME => 'content', VERSIONS => '999999', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'fa
lse', KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE => 'false', DATA_BLOCK_ENCODING => 'NONE', TTL
 => '7776000 SECONDS (90 DAYS)', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', C
ACHE_INDEX_ON_WRITE => 'false', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_BLOCKS
_ON_OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536'}                
{NAME => 'online', VERSIONS => '999999', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'fal
se', KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE => 'false', DATA_BLOCK_ENCODING => 'NONE', TTL 
=> '7776000 SECONDS (90 DAYS)', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', CA
CHE_INDEX_ON_WRITE => 'false', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_BLOCKS_
ON_OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536'}                 
3 row(s)

在HIVE用戶數據數據庫下建立HIVE外部表,若hbase表有修改,則進行HIVE 表刪除更新

create external table cb_recall_hbase(
user_id STRING comment "userID",
als map<string, ARRAY<BIGINT>> comment "als recall",
content map<string, ARRAY<BIGINT>> comment "content recall",
online map<string, ARRAY<BIGINT>> comment "online recall")
COMMENT "user recall table"
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,als:,content:,online:")
TBLPROPERTIES ("hbase.table.name" = "cb_recall");

增加一個歷史召回結果表

create 'history_recall', {NAME=>'channel', TTL=>7776000, VERSIONS=>999999}


put 'history_recall', 'recall:user:5', 'als:1',[1,2,3]
put 'history_recall', 'recall:user:5', 'als:1',[4,5,6,7]
put 'history_recall', 'recall:user:5', 'als:1',[8,9,10]

爲什麼增加歷史召回表?

  • 直接在存儲召回結果部分進行過濾,比之後排序過濾,節省排序時間

3.4.2 基於模型召回集合計算

初始化信息

import os
import sys
# 如果當前代碼文件運行測試需要加入修改路徑,避免出現後導包問題
BASE_DIR = os.path.dirname(os.path.dirname(os.getcwd()))
sys.path.insert(0, os.path.join(BASE_DIR))

PYSPARK_PYTHON = "/miniconda2/envs/reco_sys/bin/python"
# 當存在多個版本時,不指定很可能會導致出錯
os.environ["PYSPARK_PYTHON"] = PYSPARK_PYTHON
os.environ["PYSPARK_DRIVER_PYTHON"] = PYSPARK_PYTHON

from offline import SparkSessionBase

class UpdateRecall(SparkSessionBase):

    SPARK_APP_NAME = "updateRecall"
    ENABLE_HIVE_SUPPORT = True

    def __init__(self):
        self.spark = self._create_spark_session()

ur = UpdateRecall()

3.4.2.1 ALS模型推薦實現

  • 目標:利用ALS模型召回,推薦文章給用戶

ALS模型複習:

  • 步驟:
    • 1、數據類型轉換,clicked以及用戶ID與文章ID處理
    • 2、ALS模型訓練以及推薦
    • 3、推薦結果解析處理
    • 4、推薦結果存儲

數據類型轉換,clicked

ur.spark.sql("use profile")
user_article_click = ur.spark.sql("select * from user_article_basic").\
            select(['user_id', 'article_id', 'clicked'])
# 更換類型
def change_types(row):
    return row.user_id, row.article_id, int(row.clicked)

user_article_click = user_article_click.rdd.map(change_types).toDF(['user_id', 'article_id', 'clicked'])

用戶ID與文章ID處理,編程ID索引

from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline
# 用戶和文章ID超過ALS最大整數值,需要使用StringIndexer進行轉換
user_id_indexer = StringIndexer(inputCol='user_id', outputCol='als_user_id')
article_id_indexer = StringIndexer(inputCol='article_id', outputCol='als_article_id')
pip = Pipeline(stages=[user_id_indexer, article_id_indexer])
pip_fit = pip.fit(user_article_click)
als_user_article_click = pip_fit.transform(user_article_click)

3.4.2.2 ALS 模型訓練與推薦

ALS模型需要輸出用戶ID列,文章ID列以及點擊列

from pyspark.ml.recommendation import ALS
# 模型訓練和推薦默認每個用戶固定文章個數
als = ALS(userCol='als_user_id', itemCol='als_article_id', ratingCol='clicked', checkpointInterval=1)
model = als.fit(als_user_article_click)
recall_res = model.recommendForAllUsers(100)

3.4.2.3 推薦結果處理

通過StringIndexer變換後的下標知道原來的和用戶ID

# recall_res得到需要使用StringIndexer變換後的下標
# 保存原來的下表映射關係
refection_user = als_user_article_click.groupBy(['user_id']).max('als_user_id').withColumnRenamed(
'max(als_user_id)', 'als_user_id')
refection_article = als_user_article_click.groupBy(['article_id']).max('als_article_id').withColumnRenamed(
'max(als_article_id)', 'als_article_id')

# Join推薦結果與 refection_user映射關係表
# +-----------+--------------------+-------------------+
# | als_user_id | recommendations | user_id |
# +-----------+--------------------+-------------------+
# | 8 | [[163, 0.91328144]... | 2 |
#        | 0 | [[145, 0.653115], ... | 1106476833370537984 |
recall_res = recall_res.join(refection_user, on=['als_user_id'], how='left').select(
['als_user_id', 'recommendations', 'user_id'])

對推薦文章ID後處理:得到推薦列表,獲取推薦列表中的ID索引

# Join推薦結果與 refection_article映射關係表
# +-----------+-------+----------------+
# | als_user_id | user_id | als_article_id |
# +-----------+-------+----------------+
# | 8 | 2 | [163, 0.91328144] |
# | 8 | 2 | [132, 0.91328144] |
import pyspark.sql.functions as F
recall_res = recall_res.withColumn('als_article_id', F.explode('recommendations')).drop('recommendations')

# +-----------+-------+--------------+
# | als_user_id | user_id | als_article_id |
# +-----------+-------+--------------+
# | 8 | 2 | 163 |
# | 8 | 2 | 132 |
def _article_id(row):
  return row.als_user_id, row.user_id, row.als_article_id[0]

進行索引對應文章ID獲取

als_recall = recall_res.rdd.map(_article_id).toDF(['als_user_id', 'user_id', 'als_article_id'])
als_recall = als_recall.join(refection_article, on=['als_article_id'], how='left').select(
  ['user_id', 'article_id'])
# 得到每個用戶ID 對應推薦文章
# +-------------------+----------+
# | user_id | article_id |
# +-------------------+----------+
# | 1106476833370537984 | 44075 |
# | 1 | 44075 |

獲取每個文章對應的頻道,推薦給用戶時按照頻道存儲

ur.spark.sql("use toutiao")
news_article_basic = ur.spark.sql("select article_id, channel_id from news_article_basic")

als_recall = als_recall.join(news_article_basic, on=['article_id'], how='left')
als_recall = als_recall.groupBy(['user_id', 'channel_id']).agg(F.collect_list('article_id')).withColumnRenamed(
  'collect_list(article_id)', 'article_list')

als_recall = als_recall.dropna()

3.4.2.4 召回結果存儲

  • 存儲位置,選擇HBASE

HBASE表設計:

put 'cb_recall', 'recall:user:5', 'als:1',[45,3,5,10,289,11,65,52,109,8]
put 'cb_recall', 'recall:user:5', 'als:2',[1,2,3,4,5,6,7,8,9,10]

存儲代碼如下:

        def save_offline_recall_hbase(partition):
            """離線模型召回結果存儲
            """
            import happybase
            pool = happybase.ConnectionPool(size=10, host='hadoop-master', port=9090)
            for row in partition:
                with pool.connection() as conn:
                    # 獲取歷史看過的該頻道文章
                    history_table = conn.table('history_recall')
                    # 多個版本
                    data = history_table.cells('reco:his:{}'.format(row.user_id).encode(),
                                               'channel:{}'.format(row.channel_id).encode())

                     history = []
                      if len(_history_data) > 1:
                        for l in _history_data:
                          history.extend(l)

                    # 過濾reco_article與history
                    reco_res = list(set(row.article_list) - set(history))

                    if reco_res:

                        table = conn.table('cb_recall')
                        # 默認放在推薦頻道
                        table.put('recall:user:{}'.format(row.user_id).encode(),
                                  {'als:{}'.format(row.channel_id).encode(): str(reco_res).encode()})

                        # 放入歷史推薦過文章
                        history_table.put("reco:his:{}".format(row.user_id).encode(),
                                          {'channel:{}'.format(row.channel_id): str(reco_res).encode()})
                    conn.close()

        als_recall.foreachPartition(save_offline_recall_hbase)


召回表設計

1.hbase的召回結果cb_recall表:列簇有'als'、'content'、'online'
	1.create 'cb_recall', {NAME=>'als', TTL=>7776000, VERSIONS=>999999}  存儲模型召回的結果
	2.alter 'cb_recall', {NAME=>'content', TTL=>7776000, VERSIONS=>999999} 存儲內容召回的結果
	3.alter 'cb_recall', {NAME=>'online', TTL=>7776000, VERSIONS=>999999} 存儲在線計算的結果
	4.熱門召回和新文章召回可以存儲到redis
	5.TTL=>7776000:
		設置數據的過期時間,過期之後會自動刪除。可以是每隔幾小時就更新一次存儲的召回結果。
		設置過期時間的作用,就是如果用戶長期不登錄就不需要再存儲該召回結果了。7776000代表3個月的過期時間。
	6.VERSIONS=>999999:表示多個版本數據功能,代表999999次的版本數據。
		作用是:假如給某用戶(user)在某個頻道(channel_id)推薦了多次召回結果,但是該用戶一次都還沒去獲取的話,那麼就需要保存着多次更新的召回結果了。
			因爲hbase默認是一個key就對應一個value,那麼就需要設置VERSIONS=>999999來增加版本信息,那麼就可以用於當用戶沒有那麼積極地獲取的時候,
			hbase就可以在同一個key同一個列簇的情況下,存儲多個版本的value(也即保存多次的召回結果)。
			因爲離線召回部分是定時每隔幾小時就更新一次召回結果,那麼用戶並不是每次都立即獲取推薦結果的,因此就涉及到異步存儲多次召回結果,
			那麼也就要設置過期時間。
		比如:同一個key(user:5)用戶ID爲5的用戶,同一個列簇(als:1)頻道ID爲1 會有多個召回結果
			put 'cb_recall', 'recall:user:5', 'als:1',[45,3,5,10]
			put 'cb_recall', 'recall:user:5', 'als:1',[289,11,65,52,109,8]

2.hbase的歷史召回結果history_recall表:
	1.歷史召回意思就是給這個用戶推的召回結果有哪些歷史數據。
	2.比如多路召回的時候給A用戶在18頻道推薦了1000個召回數據存儲到hbase集羣中,那麼用戶在請求的時候,可以先去redis獲取,如獲取不到可再去hbase獲取1000個召回數據,
	  那麼此時就需要對1000個召回數據進行排序精選出TOP-N再進行推薦給用戶。
	3.而離線部分的多路召回會每隔一段時間定時就會推出若干個召回數據,此時自動再給A用戶在18頻道又推薦了500個召回數據,
	  假如之前一次推送的1000個召回數據和這一次推送的500個召回數據其中有200個是重複相同的召回數據,那麼爲防止用戶在第一次請求獲取1000個召回數據進行排序,
	  第二次請求獲取500個召回數據又進行排序,其中多次排序的過程中可能對大量的重複數據進行排序,爲避免這種性能浪費可進行優化,
	  那麼優化的方法就是設置歷史召回結果history_recall表。那麼就可以把第一次歷史推薦的1000個召回數據存儲到歷史召回表中,
	  那麼在離線部分中多路召回下一次定時自動推送500個召回數據的時候,就可以把其中的200個重複數據過濾掉不進行推送,僅推送300個不重複的召回數據到hbase中。
	  這樣下一次用戶在請求獲取300個召回數據的時候進行排序,就不是對包含200個重複數據的500個召回結果進行排序了,這樣就可以優化性能了。
	  所以每次先對離線部分中多路召回推送的召回數據先進行過濾,再存儲到hbase,這樣就可以優化排序時候的實時性能。
	4.因此創建歷史召回結果history_recall表也需要設置TTL(過期時間)和VERSIONS(多個版本信息)。
		create 'history_recall', {NAME=>'channel', TTL=>7776000, VERSIONS=>999999}
		put 'history_recall', 'reco:his:5', 'channel:1',[1,2,3]
		put 'history_recall', 'reco:his:5', 'channel:1',[4,5,6,7]
		設置列簇'channel',TTL=>7776000(3個月的過期時間),VERSIONS=>999999(999999次的版本數據)
		歷史召回history_recall表負責過濾多路召回的結果,並且歷史召回表每次也會存儲多路召回的數據。

基於模型召回集合計算:ALS模型召回實現

把“用戶user-物品item-用戶對物品的評分”這個[U,I]矩陣分解成下圖中的2個隨機參數矩陣X([U,K])和Y([K,I])。
“用戶user-物品item-用戶對物品的評分”這個[U,I]矩陣爲真實評分矩陣。
隨機參數矩陣X([U,K])和Y([K,I]),X*Y即[U,K]*[K,I]等於[U,I]爲預測評分矩陣。
真實矩陣[U,I]和預測矩陣[U,I]兩者並不完全相同,那麼就可以根據兩個矩陣中對應位置上的每個評分值進行計算損失。
然後還可以根據梯度下降優化算法優化兩個隨機參數矩陣X([U,K])和Y([K,I])中的值,可以理解爲權重值。
那麼優化好的矩陣X([U,K])和Y([K,I])進行相乘就可以得出預測評分矩陣,其中的值就可以作爲預測評分。

下面爲優化好的矩陣X([U,K])和Y([K,I])進行相乘所得出來的預測評分[U,I]矩陣,即“用戶user-物品item-用戶對物品的預測評分”矩陣。
行爲用戶user,列爲物品item,那麼下圖中藍色正方形爲用戶對物品的真實評分,灰色正方形爲用戶對物品的預測評分。
比如預測評分矩陣中第一行爲用戶1對所有物品的預測評分(灰色正方形)和用戶對物品的真實評分(藍色正方形)兩者組成,
那麼可以對第一行中的用戶1對所有物品的預測評分進行排序得出最高的預測評分然後對用戶進行推薦。

“用戶user-物品item-用戶對物品的評分”矩陣的數據來源:
	用戶對物品的行爲日誌數據,比如用戶對物品的點贊/收藏/點擊查看/投幣/評論等行爲日誌數據。
	然後根據打分規則對“用戶對物品的”每個行爲數據進行打分,得到用戶對物品的總評分。

ur.spark.sql("use profile")
# 用戶ID'user_id', 文章ID'article_id', 點擊'clicked'
user_article_click = ur.spark.sql("select * from user_article_basic").select(['user_id', 'article_id', 'clicked'])
# 點擊'clicked'字段的布爾值轉換爲int值
def change_types(row):
    return row.user_id, row.article_id, int(row.clicked)

user_article_click = user_article_click.rdd.map(change_types).toDF(['user_id', 'article_id', 'clicked'])

#用戶ID'user_id'和文章ID'article_id'要轉換爲索引值:通過Pipeline(stages=[字段1的StringIndexer, 字段2的StringIndexer])的方式進行索引值的轉換
#有N個用戶ID/文章ID的話,那麼就要StringIndexer索引值化出0到N個的索引值。
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline
user_id_indexer = StringIndexer(inputCol='user_id', outputCol='als_user_id')
article_id_indexer = StringIndexer(inputCol='article_id', outputCol='als_article_id')
#通過Pipeline(stages=[字段1的StringIndexer, 字段2的StringIndexer])的方式進行索引值的轉換
pip = Pipeline(stages=[user_id_indexer, article_id_indexer])
pip_fit = pip.fit(user_article_click)
als_user_article_click = pip_fit.transform(user_article_click)

# ALS模型需要輸出用戶列'user_id', 文章列'article_id', 點擊列'clicked'
from pyspark.ml.recommendation import ALS
# 模型訓練和推薦默認每個用戶固定文章個數
# 利用點擊數據訓練ALS模型。告訴ALS模型用戶數據爲'userId'列,物品數據爲'cateId'列,評分數據爲'clicked'點擊列。
# checkpointInterval=1:模型訓練的時候每迭代1次緩存一次到本地檢查點,設置的數值越大模型訓練的耗時越短。
# 如果'userId'和'article_id'的原列值的位數都不長的話,也可以直接傳入ALS模型訓練,
# 但是如果userCol和itemCol的輸入字段列值的位數非常長的話,那麼便需要把原列值先進行StringIndexer方式轉換爲索引值再輸入進行訓練。
als = ALS(userCol='als_user_id', itemCol='als_article_id', ratingCol='clicked', checkpointInterval=1)
model = als.fit(als_user_article_click)
# model.recommendForAllUsers(N) 給所有用戶推薦TOP-N個物品
recall_res = model.recommendForAllUsers(100)
#recall_res預測結果數據:
#	第1列als_user_id:用戶ID(user_id)的索引值化
#	第2列recommendations:爲二維列表,其中裏面每個一維列表裏面有兩個值,第一個值是als_article_id列值,第二個值是clicked預測值。
#	因爲評分值ratingCol='clicked'本身值就是0/1,因此clicked預測值也就便爲0到1之間的值

# 需要把索引值化的als_user_id列值對應回user_id原列值。使用max或min都可以。
# 因爲使用了max('als_user_id')的關係,內在的'als_user_id'查詢出來的列名會變成max('als_user_id'),
# 因此要使用withColumnRenamed('max(als_user_id)', 'als_user_id')重新把max('als_user_id')轉換爲 'als_user_id'。
# refection_user:包含了'user_id'、'als_user_id'
refection_user = als_user_article_click.groupBy(['user_id']).max('als_user_id').withColumnRenamed('max(als_user_id)', 'als_user_id')
# 需要把索引值化的als_article_id列值對應回article_id原列值。使用max或min都可以。
# 因爲使用了max('als_article_id')的關係,內在的'als_article_id'查詢出來的列名會變成max('als_article_id'),
# 因此要使用withColumnRenamed('max(als_article_id)', 'als_article_id')重新把max('als_article_id')轉換爲 'als_article_id'。
# refection_article:包含了'article_id'、'als_article_id'
refection_article = als_user_article_click.groupBy(['article_id']).max('als_article_id').withColumnRenamed('max(als_article_id)', 'als_article_id')
# on=['als_user_id']:recall_res和refection_user之間以'als_user_id'作爲連接條件,how='left'表示左連接
recall_res = recall_res.join(refection_user, on=['als_user_id'], how='left').select(['als_user_id', 'recommendations', 'user_id'])

# 對推薦文章ID後處理:得到推薦列表,獲取推薦列表中的ID索引
import pyspark.sql.functions as F
# F.explode('recommendations'):把'recommendations'中二維列表中的第1個一維列表數據導出到'als_article_id'字段數據。
# drop('recommendations'):然後把原'recommendations'字段列數據丟棄
recall_res = recall_res.withColumn('als_article_id', F.explode('recommendations')).drop('recommendations')
 
def _article_id(row):
  # row.als_article_id[0]:推薦數據一維列表包含'article_id'文章ID和預測值,從中取出'article_id'文章ID
  return row.als_user_id, row.user_id, row.als_article_id[0]

# 'als_article_id'字段數據中推薦數據一維列表包含'article_id'文章ID和預測值
als_recall = recall_res.rdd.map(_article_id).toDF(['als_user_id', 'user_id', 'als_article_id'])
# refection_article:包含了'article_id'、'als_article_id'
# on=['als_article_id']:als_recall和refection_article之間以'als_article_id'作爲連接條件。how='left'表示左連接
# als_recall包含了用戶ID'user_id'和給該用戶推薦的文章ID'article_id'
als_recall = als_recall.join(refection_article, on=['als_article_id'], how='left').select(['user_id', 'article_id'])

#獲取每個'article_id'文章ID對應的頻道,推薦給用戶時按照頻道進行推薦。設置了有18個頻道,按每個不同的頻道進行推薦不同的文章。
# put 'cb_recall', 'recall:user:5', 'channel:1',[45,3,5,10]:比如按照'user:5'用戶對應的'channel:1'頻道進行推薦[45,3,5,10]
ur.spark.sql("use toutiao")
#查詢'article_id'文章ID 和 對應的channel_id頻道ID
news_article_basic = ur.spark.sql("select article_id, channel_id from news_article_basic")
# on=['article_id']表示als_recall和news_article_basic之間以'article_id'作爲連接條件。 how='left'表示左連接。
# als_recall包含了用戶ID'user_id'、給該用戶推薦的文章ID'article_id'、channel_id頻道ID
# 查詢出來的als_recall中的user_id用戶ID對應的channel_id頻道ID可能會有若干個的'article_id'文章ID
als_recall = als_recall.join(news_article_basic, on=['article_id'], how='left')

# 因爲查詢出來的als_recall中的user_id用戶ID對應的channel_id頻道ID可能會有若干個的'article_id'文章ID,因此可以對'user_id'和'channel_id'一起分組,
# 每個用戶對應的每個頻道都獲取出一個列表包含的多個article_id文章ID
# agg(F.collect_list('article_id')):聚合函數把對應某個用戶下的每個頻道對應的多個article_id文章ID都封裝到一個列表中
# 因爲使用了聚合函數所以列名變成了collect_list('article_id'),因此要通過withColumnRenamed('collect_list(article_id)', 'article_list')
# 把'collect_list(article_id)'的列名 變回到'article_list'的列名
als_recall = als_recall.groupBy(['user_id', 'channel_id']).agg(F.collect_list('article_id')).withColumnRenamed('collect_list(article_id)', 'article_list')
# 去掉有空值所在的整行數據
als_recall = als_recall.dropna()

召回結果存儲,存儲位置選擇HBASE
HBASE表設計:
put 'cb_recall', 'recall:user:5', 'als:1',[45,3,5,10,289,11,65,52,109,8]
put 'cb_recall', 'recall:user:5', 'als:2',[1,2,3,4,5,6,7,8,9,10]

存儲代碼如下:
	# ALS的推薦結果存儲到hbase中,遍歷每個分區,每個分區中是若干行的數據 
        def save_offline_recall_hbase(partition):
            #離線模型召回結果存儲
            import happybase
            pool = happybase.ConnectionPool(size=10, host='hadoop-master', port=9090)
            for row in partition:
                with pool.connection() as conn:
                    # 獲取用戶的某頻道下的歷史推薦頻道文章
                    history_table = conn.table('history_recall')
		  # 從'history_recall'歷史召回表中查看對應的用戶下的某頻道中歷史的推薦結果,其中可能會包含多個版本的推薦結果信息
		  # cells可以獲取這個key(user_id)對應的列簇(channel_id)下的多個版本的推薦結果信息,因爲給該用戶的某頻道可能推薦了多次推薦數據
                    _history_data = history_table.cells('reco:his:{}'.format(row.user_id).encode(), 'channel:{}'.format(row.channel_id).encode())

                     history = []
		   # 如果該用戶對應的頻道下有多個版本的推薦結果信息的話,把多個版本的歷史推薦結果都拿出來
                     if len(_history_data) > 1:
                        for l in _history_data:
                          history.extend(l)

                    # 當前頻道推薦的結果article_list過濾掉歷史推薦的結果history,即可以對當前頻道推薦的結果article_list過濾掉歷史重複推薦的信息
                    reco_res = list(set(row.article_list) - set(history))
		  # 如果過濾之後還有數據的話
                    if reco_res:
		      # 實時推薦的表
                        table = conn.table('cb_recall')
                        # 默認放在推薦頻道
                        table.put('recall:user:{}'.format(row.user_id).encode(), {'als:{}'.format(row.channel_id).encode(): str(reco_res).encode()})
                        # 放入歷史推薦過文章
                        history_table.put("reco:his:{}".format(row.user_id).encode(), {'channel:{}'.format(row.channel_id): str(reco_res).encode()})
                    conn.close()
	
	# ALS的推薦結果存儲到hbase中,遍歷每個分區,每個分區中是若干行的數據
        als_recall.foreachPartition(save_offline_recall_hbase)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章