一種比較省記憶體的稀疏矩陣Python儲存方案

pythontab發表於2014-05-22

推薦系統中經常需要處理類似user_id, item_id, rating這樣的資料,其實就是數學裡面的稀疏矩陣,scipy中提供了sparse模組來解決這個問題,但scipy.sparse有很多問題不太合用:1、不能很好的同時支援data[i, ...]、data[..., j]、data[i, j]快速切片;2、由於資料儲存在記憶體中,不能很好的支援海量資料處理。

要支援data[i, ...]、data[..., j]的快速切片,需要i或者j的資料集中儲存;同時,為了儲存海量的資料,也需要把資料的一部分放在硬碟上,用記憶體做buffer。這裡的解決方案比較簡單,用一個類Dict的東西來儲存資料,對於某個i(比如9527),它的資料儲存在dict['i9527']裡面,同樣的,對於某個j(比如3306),它的全部資料儲存在dict['j3306']裡面,需要取出data[9527, ...]的時候,只要取出dict['i9527']即可,dict['i9527']原本是一個dict物件,儲存某個j對應的值,為了節省記憶體空間,我們把這個dict以二進位制字串形式儲存,直接上程式碼:

'''
Sparse Matrix
'''
import struct
import numpy as np
import bsddb
from cStringIO import StringIO
 
class DictMatrix():
    def __init__(self, container = {}, dft = 0.0):
        self._data  = container
        self._dft   = dft
        self._nums  = 0
 
    def __setitem__(self, index, value):
        try:
            i, j = index
        except:
            raise IndexError('invalid index')
 
        ik = ('i%d' % i)
        # 為了節省記憶體,我們把j, value打包成字二進位制字串
        ib = struct.pack('if', j, value)
        jk = ('j%d' % j)
        jb = struct.pack('if', i, value)
 
        try:
            self._data[ik] += ib
        except:
            self._data[ik] = ib
        try:
            self._data[jk] += jb
        except:
            self._data[jk] = jb
        self._nums += 1
 
    def __getitem__(self, index):
        try:
            i, j = index
        except:
            raise IndexError('invalid index')
 
        if (isinstance(i, int)):
            ik = ('i%d' % i)
            if not self._data.has_key(ik): return self._dft
            ret = dict(np.fromstring(self._data[ik], dtype = 'i4,f4'))
            if (isinstance(j, int)): return ret.get(j, self._dft)
 
        if (isinstance(j, int)):
            jk = ('j%d' % j)
            if not self._data.has_key(jk): return self._dft
            ret = dict(np.fromstring(self._data[jk], dtype = 'i4,f4'))
 
        return ret
 
    def __len__(self):
        return self._nums
 
    def __iter__(self):
        pass
 
    '''
    從檔案中生成matrix
    考慮到dbm讀寫的效能不如記憶體,我們做了一些快取,每1000W次批次寫入一次
    考慮到字串拼接效能不太好,我們直接用StringIO來做拼接
    '''
    def from_file(self, fp, sep = 't'):
        cnt = 0
        cache = {}
        for l in fp:
            if 10000000 == cnt:
                self._flush(cache)
                cnt = 0
                cache = {}
            i, j, v = [float(i) for i in l.split(sep)]
 
            ik = ('i%d' % i)
            ib = struct.pack('if', j, v)
            jk = ('j%d' % j)
            jb = struct.pack('if', i, v)
 
            try:
                cache[ik].write(ib)
            except:
                cache[ik] = StringIO()
                cache[ik].write(ib)
 
            try:
                cache[jk].write(jb)
            except:
                cache[jk] = StringIO()
                cache[jk].write(jb)
 
            cnt += 1
            self._nums += 1
 
        self._flush(cache)
        return self._nums
 
    def _flush(self, cache):
        for k,v in cache.items():
            v.seek(0)
            s = v.read()
            try:
                self._data[k] += s
            except:
                self._data[k] = s
 
if __name__ == '__main__':
    db = bsddb.btopen(None, cachesize = 268435456)
    data = DictMatrix(db)
    data.from_file(open('/path/to/log.txt', 'r'), ',')

測試4500W條rating資料(整形,整型,浮點格式),922MB文字檔案匯入,採用記憶體dict儲存的話,12分鐘構建完畢,消耗記憶體1.2G,採用示例程式碼中的bdb儲存,20分鐘構建完畢,佔用記憶體300~400MB左右,比cachesize大不了多少,資料讀取測試:

import timeit
timeit.Timer('foo = __main__.data[9527, ...]', 'import __main__').timeit(number = 1000)

消耗1.4788秒,大概讀取一條資料1.5ms。

採用類Dict來儲存資料的另一個好處是你可以隨便用記憶體Dict或者其他任何形式的DBM,甚至傳說中的Tokyo Cabinet….

好的,碼完收工。


相關文章