上一期講到我們進(jìn)行一整套特征工程,然而可怕的是,事實(shí)上這樣存儲(chǔ)的訓(xùn)練集數(shù)據(jù)體積無敵大,這樣的數(shù)據(jù)在計(jì)算圖中將遇到內(nèi)存不足的關(guān)鍵問題,給大家看看我在讀取數(shù)據(jù)的時(shí)候的壯觀景象:

因此,在進(jìn)行LR之前,我還是冒著被大家打的風(fēng)險(xiǎn),寫一篇挽尊之作,教大家設(shè)計(jì)更合適的方法存取數(shù)據(jù)集。
簡單說思路
口述思路
現(xiàn)在的當(dāng)務(wù)之急是找到一個(gè)合適的方式壓縮存儲(chǔ)空間,從而保證空間復(fù)雜度較低,然而在更多現(xiàn)實(shí)場景,其實(shí)還會(huì)涉及很多問題,因此在這里,我統(tǒng)一講講如何設(shè)計(jì)一個(gè)好的方式進(jìn)行數(shù)據(jù)集的存取,說白了就是要思考這幾個(gè)問題:
- 用什么存?純文本?CSV?SQL?excel?甚至是其他更新更多樣化的操作
- 用什么結(jié)構(gòu)存?
那么,在進(jìn)行選擇的時(shí)候,實(shí)際上要考慮的是這幾個(gè)問題:
- 安全性。存取安全,一定級別下防止泄露。
- 完整性。不會(huì)存在數(shù)據(jù)出錯(cuò)。
- 高效性。存取的復(fù)雜度要在可控范圍內(nèi)。
實(shí)際問題
在我們的這個(gè)問題下,安全性不要求;完整性上述方案基本能保證,所以也沒問題;問題就在于高效性,現(xiàn)在這個(gè)是我們目前面臨的重大問題,所以我們要好好處理,保證數(shù)據(jù)不失真的情況下去處理。
壓縮的核心在于略去不必要的信息,或者用更簡單的方式來描述更多的信息,例如條件允許的情況下二元數(shù)組可以用一元數(shù)組+函數(shù)的方式存儲(chǔ),那么在此處,我們先看看數(shù)據(jù)。
- 我們的數(shù)據(jù)是一套o(hù)ne-hot數(shù)據(jù)
- one-hot數(shù)據(jù)本身是一種稀疏矩陣的結(jié)構(gòu)
- 稀疏矩陣中含有大量的0,僅有少部分非0
因此,我們可以用稀疏矩陣的方式進(jìn)行存取,只記住非零位置下的值,其他位置為0即可。
稀疏矩陣存取
稀疏矩陣的有關(guān)理論在此處不贅述,可以自行查閱,此處給出一種我最終選擇的方案,用CSR格式,技術(shù)方案是用scipy.sparse。
簡單說說CSR,CSR格式實(shí)際上就是用一個(gè)三元組數(shù)組來表示這個(gè)稀疏矩陣,三元組分別表示(col,row,data),即行,列,數(shù)值,非零的位置的數(shù)值得以保留,然后其他位置都是0。
存儲(chǔ)
首先來看,用舊方案和新方案的數(shù)據(jù)結(jié)構(gòu):
舊數(shù)據(jù):純onehot,每行接近1w個(gè)數(shù)據(jù)
新數(shù)據(jù):分為兩塊存儲(chǔ),X部分用稀疏矩陣,然后用npz存儲(chǔ),Y部分用one-hot存儲(chǔ)。
0.0 0.0 1.0 0.0 0.0 23,1.0 3737,1.0 3750,1.0 3757,1.0 6249,1.0 9801,1.0 9806,1.0 9819,1.0
存儲(chǔ)完,體積只有59M,這樣讀取到內(nèi)存的體積也會(huì)小很多,甚至整塊都讀進(jìn)去都沒問題。
這里會(huì)用到稀疏矩陣的2個(gè)函數(shù),此處導(dǎo)入:
from scipy.sparse import csr_matrix, save_npz
下面來看看怎么實(shí)現(xiàn)的,下面是根據(jù)之前上游合并好的數(shù)據(jù),利用加載得到的oh_encoder,分別進(jìn)行轉(zhuǎn)化,分為兩塊輸出,一方面是x特征數(shù)據(jù)矩陣,另一方面是y標(biāo)簽one-hot矩陣。
def gen_res(source_data, oh_encoder):
col_all = []
row_all = []
data_all = []
idx = 0
y_res = []
with open(source_data, encoding="utf8") as f:
for line in f:
if idx == 0:
idx = 1
continue
ll = line.strip().split("::")
ll = line.strip().split("::")
data_item = []
scores_item = []
scores_item = scores_item + oh_encoder["scores"].transform([[ll[0]]])[0].tolist()
data_item = data_item + oh_encoder["movie_id"].transform([[ll[1]]])[0].tolist()
data_item = data_item + oh_encoder["movie_year"].transform([[get_year(ll[2])]])[0].tolist()
data_item = data_item + get_movie_type_oh(ll[3], oh_encoder["movie_type"]).tolist()
data_item = data_item + oh_encoder["user_id"].transform([[ll[4]]])[0].tolist()
data_item = data_item + oh_encoder["user_gentle"].transform([[ll[5]]])[0].tolist()
data_item = data_item + oh_encoder["user_age"].transform([[ll[6]]])[0].tolist()
data_item = data_item + oh_encoder["user_occupation"].transform([[ll[7]]])[0].tolist()
# Y處理
y_res.append(scores_item)
# X處理
col, data = sparse_list(data_item)
col_all = col_all + col
row_all = row_all + [idx - 1 for item in range(len(col))]
data_all = data_all + data
idx = idx + 1
if idx % 10000 == 0:
print("generating %s data items" % (idx))
# break
x_res = csr_matrix((data_all, (row_all, col_all)), shape=(max(row_all) + 1, 9831))
return x_res, y_res
總結(jié)這么幾個(gè)點(diǎn):
- scores數(shù)據(jù)我還是保留著one-hot,存儲(chǔ)差別不是很大,后面的代碼也不用改太多,條件允許的話偷個(gè)懶吧
- 其他的onehot數(shù)據(jù),我是先用transform計(jì)算出來,進(jìn)行組合,然后再用sparse_list轉(zhuǎn)化,此處,只需要抽取col和data,存儲(chǔ)即可(row因?yàn)闆]必要存,行數(shù)后面取可以算出來)
- csr_matrix是一個(gè)轉(zhuǎn)化為稀疏矩陣的函數(shù)
- 9831是除了特征維度,此處規(guī)范化,避免特征出現(xiàn)個(gè)數(shù)不足的現(xiàn)象
- 有關(guān)稀疏函數(shù)在這塊的應(yīng)用,非常建議大家多看看API文檔,傳送門在這里:https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html
- 這塊的時(shí)間其實(shí)挺長的,畢竟涉及大量的處理,有條件的各位可以考慮用mapreduce分布式嘗試
下面給出sparse_list的定義,這塊的任務(wù)是對原來的向量,轉(zhuǎn)為用[col,data]存儲(chǔ)的形式:
def sparse_list(array_get):
col = []
data = []
for idx in range(len(array_get)):
if array_get[idx] != 0:
col.append(idx)
data.append(array_get[idx])
return col, data
在gen_res得到結(jié)果后,就可以進(jìn)行進(jìn)一步存儲(chǔ),下面給出一個(gè)訓(xùn)練數(shù)據(jù)方面的例子。
# 訓(xùn)練數(shù)據(jù)生成
print("generating training data")
x_train, y_train = gen_res(TRAIN_DATA_PATH, oh_encoder)
with open(GEN_DATA_TRAIN_Y_PATH, "w", encoding='utf8') as f:
for item in y_train:
f.write("%s\n" % (",".join([str(i) for i in item])))
save_npz(GEN_DATA_TRAIN_X_PATH, x_train)
print("training data generation done")
大寫基本是寫死的路徑和參數(shù),oh_encoder是使用的轉(zhuǎn)化器,save_npz是存儲(chǔ)稀疏矩陣的函數(shù)。
讀取
這塊本來是在后續(xù)機(jī)器學(xué)習(xí)模型那集才會(huì)說的,此處為了內(nèi)容完善寫出來提早放出來~下一集這個(gè)函數(shù)就不單獨(dú)放出來,直接調(diào)用啦。
首先是看這里需要的重要包。
from scipy.sparse import load_npz
load_npz是對應(yīng)稀疏函數(shù)的加載包。
def load_dataset(x_path, y_path):
y_ = []
y_oh = []
idx = 0
with open(y_path, encoding="utf8") as f:
for line in f:
ll = line.strip().split(",")
y_item = max([idx * int(float(ll[idx])) for idx in range(5)]) + 1
y_oh_item = [int(float(item)) for item in ll[:5]]
y_.append(y_item)
y_oh.append(y_oh_item)
idx = idx + 1
if idx % 10000 == 0:
print("loading %s" % idx)
x_ = load_npz(x_path)
return x_, y_, y_oh
同樣簡單粗暴地給關(guān)鍵點(diǎn)吧
- 此處要用scipy給的數(shù)據(jù)結(jié)構(gòu)來存
- 原因是scipy.sparse這個(gè)數(shù)據(jù)類型能夠直接放在sklearn下的機(jī)器學(xué)習(xí)模型訓(xùn)練輸入中
- 存取比較直接,不用關(guān)心內(nèi)部邏輯,復(fù)雜度一般不會(huì)太高
- x和y分別取出分別放好,畢竟后續(xù)訓(xùn)練的時(shí)候也是要分開放入的
- 此處的輸出我分了三個(gè),作為稀疏矩陣的特征,數(shù)值型的y,和one-hot的y,日后結(jié)果評估會(huì)有用。
小結(jié)
算法從來不止是機(jī)器學(xué)習(xí),而是對整個(gè)項(xiàng)目流程的把握,根據(jù)特定目標(biāo)做好規(guī)劃,一整個(gè)項(xiàng)目才得以完成。本文主要以movielens為例談?wù)摂?shù)據(jù)集存取策略的問題,是特征工程后確定數(shù)據(jù)存儲(chǔ)格式、存取方式的重要一步,合理高效的存取策略會(huì)令整個(gè)系統(tǒng)在進(jìn)行模型計(jì)算時(shí)更加高效,省時(shí)省空間。