毋庸諱言,和傳統架構(BS開發/CS開發)相比,人工智慧技術確實有一定的基礎門檻,它註定不是大眾化,普適化的東西。但也不能否認,人工智慧技術也具備像傳統架構一樣“套路化”的流程,也就是說,我們大可不必自己手動構建基於神經網路的機器學習系統,直接使用深度學習框架反而更加簡單,深度學習可以幫助我們自動地從原始資料中提取特徵,不需要手動選擇和提取特徵。
之前我們手動構建了一個小型的神經網路,解決了機器學習的分類問題,本次我們利用深度學習框架Tensorflow2.11構建一套基於神經網路協同過濾模型(NCF)的影片推薦系統,解決預測問題,完成一個真正可以落地的專案。
推薦系統發展歷程
“小夥子,要光碟嗎?新的到貨了,內容相當精彩!”
大約20年前,在北京中關村的街頭,一位抱著嬰兒的中年大媽興奮地拽著筆者的胳臂,手舞足蹈地推薦著她的“產品”,大概這就是最原始的推薦系統雛形了。
事實上,時至今日,依然有類似產品使用這樣的套路,不管三七二十一,弄個首頁大Banner,直接懟使用者臉上,且不論使用者感不感興趣,有沒有使用者點選和轉化,這種強買強賣式的推薦,著實不怎麼令人愉快。
所以推薦系統解決的痛點應該是使用者的興趣需求,給使用者推薦喜歡的內容,才是推薦系統的核心。
於是乎,啟發式推薦演算法(Memory-based algorithms)就應運而生了。
啟發式推薦演算法易於實現,並且推薦結果的可解釋性強。啟發式推薦演算法又可以分為兩類:
基於使用者的協同過濾(User-based collaborative filtering):主要考慮的是使用者和使用者之間的相似度,只要找出相似使用者喜歡的物品,並預測目標使用者對對應物品的評分,就可以找到評分最高的若干個物品推薦給使用者。舉個例子,李老師和閆老師擁有相似的電影喜好,當新電影上映後,李老師對其表示喜歡,那麼就能將這部電影推薦給閆老師。
基於物品的協同過濾(Item-based collaborative filtering):主要考慮的是物品和物品之間的相似度,只有找到了目標使用者對某些物品的評分,那麼就可以對相似度高的類似物品進行預測,將評分最高的若干個相似物品推薦給使用者。舉個例子,如果使用者A、B、C給書籍X,Y的評分都是5分,當使用者D想要買Y書籍的時候,系統會為他推薦X書籍,因為基於使用者A、B、C的評分,系統會認為喜歡Y書籍的人在很大程度上會喜歡X書籍。
啟發式協同過濾演算法是一種結合了基於使用者的協同過濾和基於專案的協同過濾的演算法,它透過啟發式規則來預測使用者對物品的評分。
然而,啟發式協同過濾演算法也存在一些缺陷:
難以處理冷啟動問題:當一個使用者或一個物品沒有足夠的評分資料時,啟發式協同過濾演算法無法對其進行有效的預測,因為它需要依賴於已有的評分資料。
對資料稀疏性敏感:如果資料集中存在大量的缺失值,啟發式協同過濾演算法的預測準確率會受到影響,因為它需要依賴於完整的評分資料來進行預測。
演算法的可解釋性較差:啟發式協同過濾演算法的預測結果是透過啟發式規則得出的,這些規則可能很難被解釋和理解。
受限於啟發式規則的質量:啟發式協同過濾演算法的預測準確率受到啟發式規則的質量影響,如果啟發式規則得不到有效的最佳化和更新,演算法的效能可能會受到影響。
說白了,這種基於啟發式的協同過濾演算法,很容易陷入一個小範圍的困境,就是如果某個使用者特別喜歡體育的影片,那麼這種系統就會玩命地推薦體育影片,實際上這個人很有可能也喜歡藝術類的影片,但是囿於冷啟動問題,無法進行推薦。
為了解決上面的問題,基於神經網路的協同過濾演算法誕生了,神經網路的協同過濾演算法可以透過將使用者和物品的特徵向量作為輸入,來預測使用者對新物品的評分,從而解決冷啟動問題。
對資料稀疏性的魯棒性:神經網路的協同過濾演算法可以自動學習使用者和物品的特徵向量,並能夠透過這些向量來預測評分,因此對於資料稀疏的情況也能進行有效的預測。
更好的預測準確率:神經網路的協同過濾演算法可以透過多層非線性變換來學習使用者和物品之間的複雜關係,從而能夠提高預測準確率。
可解釋性和靈活性:神經網路的協同過濾演算法可以透過調整網路結構和引數來最佳化預測準確率,並且可以透過視覺化方法來解釋預測結果。
所以基於神經網路協同過濾模型是目前推薦系統的主流形態。
基於稀疏矩陣的影片完播資料
首先構造我們的資料矩陣test.csv檔案:
User,Video 1,Video 2,Video 3,Video 4,Video 5,Video 6
User1,10,3,,,,
User2,,10,,10,5,1
User3,,,9,,,
User4,6,1,,8,,9
User5,1,,1,,10,4
User6,1,4,1,,10,1
User7,,2,1,2,,8
User8,,,,1,,
User9,1,,10,,3,1
這裡橫軸是影片資料,縱軸是使用者,對應的資料是使用者對於影片的完播程度,10代表看完了,1則代表只看了百分之十,留空的代表沒有看。
編寫ncf.py指令碼,將資料讀入記憶體並輸出:
import pandas as pd
# set pandas to show all columns without truncation and line breaks
pd.set_option('display.max_columns', 1000)
pd.set_option('display.width', 1000)
# data = np.loadtxt('data/test-data.csv', delimiter=',', dtype=int, skiprows=1,)
data = pd.read_csv('data/test-data.csv')
print(data)
程式返回:
User Video 1 Video 2 Video 3 Video 4 Video 5 Video 6
0 User1 10.0 3.0 NaN NaN NaN NaN
1 User2 NaN 10.0 NaN 10.0 5.0 1.0
2 User3 NaN NaN 9.0 NaN NaN NaN
3 User4 6.0 1.0 NaN 8.0 NaN 9.0
4 User5 1.0 NaN 1.0 NaN 10.0 4.0
5 User6 1.0 4.0 1.0 NaN 10.0 1.0
6 User7 NaN 2.0 1.0 2.0 NaN 8.0
7 User8 NaN NaN NaN 1.0 NaN NaN
8 User9 1.0 NaN 10.0 NaN 3.0 1.0
一目瞭然。
有資料的列代表使用者看過,1-10代表看了之後的完播程度,如果沒看過就是NAN,現在我們的目的就是“猜”出來這些沒看過的影片的完播資料是多少?從而根據完播資料完成影片推薦系統。
矩陣拆解演算法
有一種推薦演算法是基於矩陣拆解,透過假設的因素去“猜”稀疏矩陣的空缺資料,猜出來之後,再透過反向傳播的逆運算來反推稀疏矩陣已存在的資料是否正確,從而判斷“猜”出來的資料是否正確:
通俗地講,跟算命差不多,但是基於數學原理,如果透過反推證明針對一個人的算命策略都是對的,那麼就把這套流程應用到其他人身上。
但是這套邏輯過於線性,也就是因素過於單一,比如我喜歡黑色的汽車,那麼就會給我推所有黑色的東西,其實可能黑色的因素僅侷限於汽車,是多重因素疊加導致的,所以矩陣拆解並不是一個非常好的解決方案。
基於神經網路
使用神經網路計算,必須將資料進行向量化操作:
# reset the column.index to be numeric
user_index = data[data.columns[0]]
book_index = data.columns
data = data.reset_index(drop=True)
data[data.columns[0]] = data.index.astype('int')
# print(data)
# print(data)
scaler = 10
# data = pd.DataFrame(data.to_numpy(), index=range(0,len(user_index)), columns=range(0,len(book_index)))
df_long = pd.melt(data, id_vars=[data.columns[0]],
ignore_index=True,
var_name='video_id',
value_name='rate').dropna()
df_long.columns = ['user_id', 'video_id', 'rating']
df_long['rating'] = df_long['rating'] / scaler
# replace the user_id to user by match user_index
df_long['user_id'] = df_long['user_id'].apply(lambda x: user_index[x])
# data = df_long.to_numpy()
print(df_long)
程式返回:
user_id video_id rating
0 User1 Video 1 1.0
3 User4 Video 1 0.6
4 User5 Video 1 0.1
5 User6 Video 1 0.1
8 User9 Video 1 0.1
9 User1 Video 2 0.3
10 User2 Video 2 1.0
12 User4 Video 2 0.1
14 User6 Video 2 0.4
15 User7 Video 2 0.2
20 User3 Video 3 0.9
22 User5 Video 3 0.1
23 User6 Video 3 0.1
24 User7 Video 3 0.1
26 User9 Video 3 1.0
28 User2 Video 4 1.0
30 User4 Video 4 0.8
33 User7 Video 4 0.2
34 User8 Video 4 0.1
37 User2 Video 5 0.5
40 User5 Video 5 1.0
41 User6 Video 5 1.0
44 User9 Video 5 0.3
46 User2 Video 6 0.1
48 User4 Video 6 0.9
49 User5 Video 6 0.4
50 User6 Video 6 0.1
51 User7 Video 6 0.8
53 User9 Video 6 0.1
這裡scaler=10作為資料範圍的閾值,讓計算機將完播資料雜湊成0-1之間的浮點數,便於神經網路進行計算。
隨後安裝Tensorflow框架:
pip3 install tensorflow
如果是Mac使用者,請安裝mac版本:
pip3 install tensorflow-macos
接著針對資料進行打標籤操作:
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
# dataset = pd.read_csv(url, compression='zip', usecols=['userId', 'movieId', 'rating'])
dataset = df_long
# Encode the user and video IDs
user_encoder = LabelEncoder()
video_encoder = LabelEncoder()
dataset['user_id'] = user_encoder.fit_transform(dataset['user_id'])
dataset['video_id'] = video_encoder.fit_transform(dataset['video_id'])
# Split the dataset into train and test sets
# train, test = train_test_split(dataset, test_size=0.2, random_state=42)
train = dataset
# Model hyperparameters
num_users = len(dataset['user_id'].unique())
num_countries = len(dataset['video_id'].unique())
隨後定義64個維度針對向量進行處理:
embedding_dim = 64
# Create the NCF model
inputs_user = tf.keras.layers.Input(shape=(1,))
inputs_video = tf.keras.layers.Input(shape=(1,))
embedding_user = tf.keras.layers.Embedding(num_users, embedding_dim)(inputs_user)
embedding_video = tf.keras.layers.Embedding(num_countries, embedding_dim)(inputs_video)
# Merge the embeddings using concatenation, you can also try other merging methods like dot product or multiplication
merged = tf.keras.layers.Concatenate()([embedding_user, embedding_video])
merged = tf.keras.layers.Flatten()(merged)
# Add fully connected layers
dense = tf.keras.layers.Dense(64, activation='relu')(merged)
dense = tf.keras.layers.Dense(32, activation='relu')(dense)
output = tf.keras.layers.Dense(1, activation='sigmoid')(dense)
# Compile the model
model = tf.keras.Model(inputs=[inputs_user, inputs_video], outputs=output)
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
這裡定義了一個64維度的 embedding 類用來對向量進行處理。相當於就是把屬於資料當中的所有特徵都設定成一個可以用一個64維向量標識的東西,然後透過降維處理之後使得機器能以一個低維的資料流形來“理解”高維的原始資料的方式來“理解”資料的“含義”,
從而實現機器學習的目的。而為了檢驗機器學習的成果(即機器是否有真正理解特徵的含義),則使用mask(遮罩)的方式,將原始資料當中的一部分無關核心的內容“遮掉”,然後再嘗試進行輸入輸出操作,如果輸入輸出操作的結果與沒有遮罩的結果進行比較後足夠相近,或者完全相同,則判定機器有成功學習理解到向量的含義。
這裡需要注意的是,因為embedding 這個詞其實是有一定程度的誤用的關係,所以不要嘗試用原來的語義去理解這個詞,通俗地講,可以把它理解為“特徵(feature)”,即從原始資料中提取出來的一系列的特徵屬性,至於具體是什麼特徵,不重要。
這裡有64個維度,那就可以認為是從輸入的原始資料當中提取64個“特徵”,然後用這個特徵模型去套用所有的輸入的原始資料,然後再將這些資料透過降維轉換,最終把每一個輸入的向量轉換成一個1維的特殊字串,然後讓機器實現“理解複雜的輸入”的目的,而那個所謂的訓練過程,其實也就是不斷地用遮罩mask去遮掉非核心的資料,然後對比輸出結果,來看機器是否成功實現了學習的目的。
說白了,和矩陣拆解差不多,只不過矩陣拆解是線性單維度,而神經網路是非線性多維度。
最後進行訓練和輸出:
model.fit(
[train['user_id'].values, train['video_id'].values],
train['rating'].values,
batch_size=64,
epochs=100,
verbose=0,
# validation_split=0.1,
)
result_df = {}
for user_i in range(1, 10):
user = f'User{user_i}'
result_df[user] = {}
for video_i in range(1, 7):
video = f'Video {video_i}'
pred_user_id = user_encoder.transform([user])
pred_video_id = video_encoder.transform([video])
result = model.predict(x=[pred_user_id, pred_video_id], verbose=0)
result_df[user][video] = result[0][0]
result_df = pd.DataFrame(result_df).T
result_df *= scaler
print(result_df)
程式返回:
Video 1 Video 2 Video 3 Video 4 Video 5 Video 6
User1 9.143433 3.122697 5.831852 8.930688 9.223139 9.148163
User2 2.379406 9.317654 9.280337 9.586231 5.115635 0.710877
User3 6.046935 8.950342 9.335093 9.546472 8.487216 5.069511
User4 6.202362 1.341177 2.609368 7.755390 9.160558 8.974072
User5 1.134012 1.772043 0.634183 3.741076 9.297663 3.924277
User6 0.488006 4.060344 1.116192 4.625140 9.264144 1.199519
User7 2.820735 0.898690 0.560579 2.215827 8.604731 7.889819
User8 0.244587 1.062029 0.360087 1.069786 7.698551 1.286932
User9 1.337930 8.537857 9.329366 9.123328 3.074733 0.774436
我們可以看到,機器透過神經網路的“學習”,直接“猜出來”所有使用者未播放影片的完播程度。那麼,我們只需要給這些使用者推薦他未看的,但是機器“猜”他完播高的影片即可。
總結
我們可以看到,整個流程簡單的令人髮指,深度學習框架Tensorflow幫我們做了大部分的工作,我們其實只是簡單的提供了基礎資料而已。
首先定義一個embedding (多維空間) 用來理解需要學習的原始資料 :
一個使用者物件(含一個屬性userId)
一個影片物件(含三個屬性:videoId, userId, rating (完播向量))
這裡需要進行學習的具體就是讓機器理解那個“完播向量:rating”的含義)這裡定義的embedding 維度為64, 本質就是讓機器把完播向量rating 的值當作成一個64維度的空間來進行理解(其實就是從這個rating值當中提取出64個特徵來重新定義這個rating)
隨後對embedding 進行降維處理:
具體的操作與使用的降維函式曲線有關,這裡採用的是先降為32維再降為1維的兩道操作方式,原來的代表rating 的embedding 空間從64維降低到了1維。而此時的輸出output 物件就是機器對rating完播向量所做出來的“自己的理解”。
最後透過對學習完的輸出項output 進行mask(遮罩)測試,透過變換不同的mask(遮罩)來測試結果是否與原始資料相近,或一致,從而來證實機器學習的效果,也就是上文提到的反向傳播方式的逆運算。
結語
可能依然有朋友對這套系統的底層不太瞭解,那麼,如果我們用“白話文”的形式進行解釋:比如有一幅油畫,油畫相比完播量,肯定是多維度的,因為畫裡面有顏色、風格、解析度、對比度、飽和度等等特徵引數,此時我們讓機器先看完整的這幅畫,然後用機器學習的方式讓它學習(即embedding方式),接著把這幅畫遮掉一部分與主題無關的部分,然後再測試機器讓它用學習到的資料(即embedding完成降維處理之後的資料)去嘗試復原整幅畫,隨後對比復原的整幅畫和原始油畫有多大差別,如果差別沒有或者很小,則證明機器學習成功了,機器確實學會了這副畫,然後就讓機器按照這套邏輯去畫類似的畫,最後把這一“類”的畫推薦給沒有鑑賞過的使用者,從而完成推薦系統,就這麼簡單。
最後,奉上影片推薦系統專案程式碼,與眾鄉親同饗:github.com/zcxey2911/NeuralCollaborativeFiltering_NCF_Tensorflow