推薦系統的EE問題 | Bandit算法概述

轉自:https://blog.csdn.net/heyc861221/article/details/80129310

推薦系統裏面有兩個經典問題:EE和冷啓動。前者涉及到平衡準確和多樣,後者涉及到產品算法運營等一系列。Bandit算法是一種簡單的在線學習算法,常常用於嘗試解決這兩個問題,本文爲你介紹基礎的Bandit算法及一系列升級版,以及對推薦系統這兩個經典問題的思考。

什麼是Bandit算法

爲選擇而生:我們會遇到很多選擇的場景。上哪個大學,學什麼專業,去哪家公司,中午吃什麼等等。這些事情,都讓選擇困難症的我們頭很大。那麼,有算法能夠很好地對付這些問題嗎?

當然有!那就是Bandit算法。Bandit算法來源於歷史悠久的賭博學,它要解決的問題是這樣的:

一個賭徒,要去搖老虎機,走進賭場一看,一排老虎機,外表一模一樣,但是每個老虎機吐錢的概率可不一樣,他不知道每個老虎機吐錢的概率分佈是什麼,那麼每次該選擇哪個老虎機可以做到最大化收益呢?這就是多臂賭博機問題(Multi-armed bandit problem, K-armed bandit problem, MAB)。

怎麼解決這個問題呢?最好的辦法是去試一試,不是盲目地試,而是有策略地快速試一試,這些策略就是Bandit算法。

這個多臂問題,推薦系統裏很多問題都與它類似:

  1. 假設一個用戶對不同類別的內容感興趣程度不同,那麼我們的推薦系統初次見到這個用戶時,怎麼快速地知道他對每類內容的感興趣程度?這就是推薦系統的冷啓動。
  2. 假設我們有若干廣告庫存,怎麼知道該給每個用戶展示哪個廣告,從而獲得最大的點擊收益?是每次都挑效果最好那個麼?那麼新廣告如何纔有出頭之日?
  3. 我們的算法工程師又想出了新的模型,有沒有比A/B test更快的方法知道它和舊模型相比誰更靠譜?
  4. 如果只是推薦已知的用戶感興趣的物品,如何才能科學地冒險給他推薦一些新鮮的物品?

Bandit算法與推薦系統

在推薦系統領域裏,有兩個比較經典的問題常被人提起,一個是EE問題,另一個是用戶冷啓動問題。

什麼是EE問題?又叫exploit-explore問題。exploit就是:對用戶比較確定的興趣,當然要利用開採迎合,好比說已經掙到的錢,當然要花;explore就是:光對着用戶已知的興趣使用,用戶很快會膩,所以要不斷探索用戶新的興趣才行,這就好比雖然有一點錢可以花了,但是還得繼續搬磚掙錢,不然花完了就得喝西北風。

用戶冷啓動問題,也就是面對新用戶時,如何能夠通過若干次實驗,猜出用戶的大致興趣。

我想,屏幕前的你已經想到了,推薦系統冷啓動可以用Bandit算法來解決一部分。這兩個問題本質上都是如何選擇用戶感興趣的主題進行推薦,比較符合Bandit算法背後的MAB問題。

比如,用Bandit算法解決冷啓動的大致思路如下:用分類或者Topic來表示每個用戶興趣,也就是MAB問題中的臂(Arm),我們可以通過幾次試驗,來刻畫出新用戶心目中對每個Topic的感興趣概率。這裏,如果用戶對某個Topic感興趣(提供了顯式反饋或隱式反饋),就表示我們得到了收益,如果推給了它不感興趣的Topic,推薦系統就表示很遺憾(regret)了。如此經歷“選擇-觀察-更新-選擇”的循環,理論上是越來越逼近用戶真正感興趣的Topic的,

怎麼選擇Bandit算法?

現在來介紹一下Bandit算法怎麼解決這類問題的。Bandit算法需要量化一個核心問題:錯誤的選擇到底有多大的遺憾?能不能遺憾少一些?

王家衛在《一代宗師》裏寄出一句臺詞:

人生要是無憾,那多無趣?

而我說:算法要是無憾,那應該是過擬合了。

所以說:怎麼衡量不同Bandit算法在解決多臂問題上的效果?首先介紹一個概念,叫做累積遺憾(regret)
在這裏插入圖片描述
這個公式就是計算Bandit算法的累積遺憾,解釋一下:

首先,這裏我們討論的每個臂的收益非0即1,也就是伯努利收益。

然後,每次選擇後,計算和最佳的選擇差了多少,然後把差距累加起來就是總的遺憾。

wB(i)是第i次試驗時被選中臂的期望收益, w*是所有臂中的最佳那個,如果上帝提前告訴你,我們當然每次試驗都選它,問題是上帝不告訴你,所以就有了Bandit算法,我們就有了這篇文章。

這個公式可以用來對比不同Bandit算法的效果:對同樣的多臂問題,用不同的Bandit算法試驗相同次數,看看誰的regret增長得慢。

那麼到底不同的Bandit算法有哪些呢?

常用Bandit算法

Thompson sampling算法

Thompson sampling算法簡單實用,因爲它只有一行代碼就可以實現[3]。簡單介紹一下它的原理,要點如下:

  1. 假設每個臂是否產生收益,其背後有一個概率分佈,產生收益的概率爲p。
  2. 我們不斷地試驗,去估計出一個置信度較高的“概率p的概率分佈”就能近似解決這個問題了。
  3. 怎麼能估計“概率p的概率分佈”呢? 答案是假設概率p的概率分佈符合beta(wins, lose)分佈,它有兩個參數: wins,
    lose。
  4. 每個臂都維護一個beta分佈的參數。每次試驗後,選中一個臂,搖一下,有收益則該臂的wins增加1,否則該臂的lose增加1。
  5. 每次選擇臂的方式是:用每個臂現有的beta分佈產生一個隨機數b,選擇所有臂產生的隨機數中最大的那個臂去搖。
  6. List item
import  numpy as np
import  pymc
#wins 和 trials 是一個N維向量,N是賭博機的臂的個數,每個元素記錄了
choice = np.argmax(pymc.rbeta(1 + wins, 1 + trials - wins)) 
wins[choice] += 1
trials += 1

UCB

UCB算法全稱是Upper Confidence Bound(置信區間上界),它的算法步驟如下:

  1. 初始化:先對每一個臂都試一遍;
  2. 按照如下公式計算每個臂的分數,然後選擇分數最大的臂作爲選擇:

在這裏插入圖片描述
3. 觀察選擇結果,更新t和Tjt。其中加號前面是這個臂到目前的收益均值,後面的叫做bonus,本質上是均值的標準差,t是目前的試驗次數,Tjt是這個臂被試次數

這個公式反映一個特點:均值越大,標準差越小,被選中的概率會越來越大,同時哪些被選次數較少的臂也會得到試驗機會。

import numpy as np

T = 1000  # T輪試驗
N = 10  # N個老虎機

true_rewards = np.random.uniform(low=0, high=1, size=N)  # 每個老虎機真實的吐錢概率
estimated_rewards = np.zeros(N)  # 每個老虎機吐錢的觀測概率,初始都爲0
chosen_count = np.zeros(N)  # 每個老虎機當前已經探索的次數,初始都爲0
total_reward = 0


# 計算delta
def calculate_delta(T, item):
    if chosen_count[item] == 0:
        return 1
    else:
        return np.sqrt(2 * np.log(T) / chosen_count[item])

# 計算每個老虎機的p+delta,同時做出選擇
def UCB(t, N):
    upper_bound_probs = [estimated_rewards[item] + calculate_delta(t, item) for item in range(N)]
    item = np.argmax(upper_bound_probs)
    reward = np.random.binomial(n=1, p=true_rewards[item])
    return item, reward


for t in range(1, T):  # 依次進行T次試驗
    # 選擇一個老虎機,並得到是否吐錢的結果
    item, reward = UCB(t, N)
    total_reward += reward  # 一共有多少客人接受了推薦

    # 更新每個老虎機的吐錢概率
    estimated_rewards[item] = ((t - 1) * estimated_rewards[item] + reward) / t
    chosen_count[item] += 1

Epsilon-Greedy算法

這是一個樸素的Bandit算法,有點類似模擬退火的思想:

  1. 選一個(0,1)之間較小的數作爲epsilon;
  2. 每次以概率epsilon做一件事:所有臂中隨機選一個;
  3. 每次以概率1-epsilon 選擇截止到當前,平均收益最大的那個臂。

是不是簡單粗暴?epsilon的值可以控制對Exploit和Explore的偏好程度。越接近0,越保守,只想花錢不想掙錢。

樸素Bandit算法

最樸素的Bandit算法就是:先隨機試若干次,計算每個臂的平均收益,一直選均值最大那個臂。這個算法是人類在實際中最常採用的,不可否認,它還是比隨機亂猜要好。

以上五個算法,我們用10000次模擬試驗的方式對比了其效果如圖4,實驗代碼來源 :
在這裏插入圖片描述
算法效果對比一目瞭然:UCB算法和Thompson採樣算法顯著優秀一些。

至於你實際上要選哪一種Bandit算法,你可以選一種Bandit算法來選Bandit算法。

Bandit算法與線性迴歸

UCB算法

UCB算法在做EE(Exploit-Explore)的時候表現不錯,但它是上下文無關(context free)的Bandit算法,它只管埋頭幹活,根本不觀察一下面對的都是些什麼特點的arm,下次遇到相似特點但不一樣的arm也幫不上什麼忙。

UCB解決Multi-armed bandit問題的思路是:用置信區間。置信區間可以簡單地理解爲不確定性的程度,區間越寬,越不確定,反之亦反之。

每個item的回報均值都有個置信區間,隨着試驗次數增加,置信區間會變窄(逐漸確定了到底回報豐厚還是可憐)。每次選擇前,都根據已經試驗的結果重新估計每個Item的均值及置信區間。 選擇置信區間上限最大的那個Item。

“選擇置信區間上界最大的那個Item”這句話反映了幾個意思:

  1. 如果Item置信區間很寬(被選次數很少,還不確定),那麼它會傾向於被多次選擇,這個是算法冒風險的部分;
  2. 如果Item置信區間很窄(備選次數很多,比較確定其好壞了),那麼均值大的傾向於被多次選擇,這個是算法保守穩妥的部分;

UCB是一種樂觀的算法,選擇置信區間上界排序,如果時悲觀保守的做法,是選擇置信區間下界排序。

UCB算法加入特徵信息

Yahoo!的科學家們在2010年發表了一篇論文[6],給UCB引入了特徵信息,同時還把改造後的UCB算法用在了Yahoo!的新聞推薦中,算法名叫LinUCB,劉鵬博士在《計算廣告》一書中也有介紹LinUCB在計算廣告中的應用。

單純的老虎機回報情況就是老虎機自己內部決定的,而在廣告推薦領域,一個選擇的回報,是由User和Item一起決定的,如果我們能用Feature來刻畫User和Item這一對CP,在每次選擇Item之前,通過Feature預估每一個arm(item)的期望回報及置信區間,選擇的收益就可以通過Feature泛化到不同的Item上。

爲UCB算法插上了特徵的翅膀,這就是LinUCB最大的特色。

在這裏插入圖片描述
圖5 應用LinUCB算法的Yahoo!首頁

LinUCB算法做了一個假設:一個Item被選擇後推送給一個User,其回報和相關Feature成線性關係,這裏的“相關Feature”就是context,也是實際項目中發揮空間最大的部分。

於是試驗過程就變成:用User和Item的特徵預估回報及其置信區間,選擇置信區間上界最大的Item推薦,觀察回報後更新線性關係的參數,以此達到試驗學習的目的。

LinUCB基本算法描述如下:

在這裏插入圖片描述
對照每一行解釋一下(編號從1開始):

  1. 設定一個參數\alpha,這個參數決定了我們Explore的程度;
  2. 開始試驗迭代;
  3. 獲取每一個arm的特徵向量xa,t;
  4. 開始計算每一個arm的預估回報及其置信區間;
  5. 如果arm還從沒有被試驗過,那麼:
  6. 用單位矩陣初始化Aa;
  7. 用0向量初始化ba;
  8. 處理完沒被試驗過的arm;
  9. 計算線性參數\theta;
  10. 用\theta和特徵向量xa,t計算預估回報,同時加上置信區間寬度;
  11. 處理完每一個arm;
  12. 選擇第10步中最大值對應的arm,觀察真實的回報rt;
  13. 更新Aat;
  14. 更新bat;
  15. 算法結束。

注意到上面的第4步,給特徵矩陣加了一個單位矩陣,這就是嶺迴歸(ridge regression),嶺迴歸主要用於當樣本數小於特徵數時,對迴歸參數進行修正。

對於加了特徵的Bandit問題,正符合這個特點:試驗次數(樣本)少於特徵數。

每一次觀察真實回報之後,要更新的不止是嶺迴歸參數,還有每個arm的回報向量ba。

import numpy as np

class LinUCB:
"""
首先我們設定一些超參數,比如α,正反饋和負反饋的獎勵程度r1,r0,上下文特徵的長度d

我們設定我們的幾個矩陣,比如A和A的逆矩陣,b(x和r的乘積),以及參數矩陣:
"""
    def __init__(self):
        self.alpha = 0.25
        self.r1 = 0.6
        self.r0 = -16
        self.d = 6  # dimension of user features
        self.Aa = {} # Aa : collection of matrix to compute disjoint part for each article a, d*d
        self.AaI = {}  # AaI : store the inverse of all Aa matrix

        self.ba = {}  # ba : collection of vectors to compute disjoin part, d*1
        self.theta = {}
        self.a_max = 0
        self.x = None
        self.xT = None

"""
初始化矩陣對應上面的4-7步,A設置爲單位矩陣,b設置爲0矩陣,參數也設置爲0矩陣,注意的是,每個arm都有這麼一套矩陣:
"""
    def set_articles(self,art):
        for key in art:
            self.Aa[key] = np.identity(self.d) # 創建單位矩陣
            self.ba[key] = np.zeros((self.d,1))

            self.AaI[key] = np.identity(self.d)
            self.theta[key] = np.zeros((self.d,1))


"""
這對應於上面的12-13步,根據選擇的最優arm,以及得到的用戶反饋,我們更新A和b矩陣:
"""
    def update(self,reward):
        if reward == -1:
            pass
        elif reward == 1 or reward == 0:
            if reward == 1:
                r = self.r1
            else:
                r = self.r0

            self.Aa[self.a_max] += np.dot(self.x,self.xT)
            self.ba[self.a_max] += r * self.x
            self.AaI[self.a_max] = np.linalg.inv(self.Aa[self.a_max])
            self.theta[self.a_max] = np.dot(self.AaI[self.a_max],self.ba[self.a_max])

        else:
            # error
            pass

"""
計算推薦結果對應於上面的8-11步,我們直接根據公式計算當前的最優參數和置信上界,並選擇最大的arm作爲推薦結果。代碼中有個小trick,及對所有的arm來說,共同使用一個特徵,而不是每一個arm單獨使用不同的特徵:
"""
    def recommend(self,timestamp,user_features,articles):
        xaT = np.array([user_features]) # d * 1
        xa = np.transpose(xaT)

        AaI_tmp = np.array([self.AaI[article] for article in articles])
        theta_tmp = np.array([self.theta[article] for article in articles])
        art_max = articles[np.argmax(np.dot(xaT,theta_tmp) + self.alpha * np.sqrt(np.dot(np.dot(xaT,AaI_tmp),xa)))]

        self.x = xa
        self.xT = xaT
        self.a_max = art_max

        return self.a_max

Exploit-Explore這一對矛盾一直客觀存在,Bandit算法是公認的一種比較好的解決EE問題的方案。除了Bandit算法之外,還有一些其他的explore的辦法,比如:在推薦時,隨機地去掉一些用戶歷史行爲(特徵)。

解決Explore,勢必就是要冒險,勢必要走向未知,而這顯然就是會傷害用戶體驗的:明知道用戶肯定喜歡A,你還偏偏以某個小概率給推薦非A。

實際上,很少有公司會採用這些理性的辦法做Explore,反而更願意用一些盲目主觀的方式。究其原因,可能是因爲:

  1. 互聯網產品生命週期短,而Explore又是爲了提升長期利益的,所以沒有動力做;
  2. 用戶使用互聯網產品時間越來越碎片化,Explore的時間長,難以體現出Explore 的價值;
  3. 同質化互聯網產品多,用戶選擇多,稍有不慎,用戶用腳投票,分分鐘棄你於不顧;
  4. 已經成規模的平臺,紅利槓槓的,其實是沒有動力做Explore的。

基於這些,我們如果想在自己的推薦系統中引入Explore機制,需要注意以下幾點:

  1. 用於Explore的Item要保證其本身質量,縱使用戶不感興趣,也不至於引起其反感;
  2. Explore本身的產品需要精心設計,讓用戶有耐心陪你玩兒;
  3. 深度思考,這樣纔不會做出腦殘的產品,產品不會早早夭折,纔有可能讓Explore機制有用武之地。

轉自:https://blog.csdn.net/heyc861221/article/details/80129310

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