稀疏矩阵与稠密矩阵以及 LibRec 中的实现

稀疏矩阵

在矩阵中,若数值为0的元素数目远远多于非0元素的数目时,则称该矩阵为稀疏矩阵。与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵

注:压缩存储的矩阵可以分为特殊矩阵和稀疏矩阵。对于那些具有相同元素或零元素在矩阵中分布具有一定规律的矩阵,被称之为特殊矩阵。对于那些零元素数据远远多于非零元素数目,并且非零元素的分布没有规律的矩阵称之为稀疏矩阵。

稀疏矩阵的特性

  • 稀疏矩阵其非零元素的个数远远小于零元素的个数,而且这些非零元素的分布也没有规律。

  • 稀疏因子是用于描述稀疏矩阵的非零元素的比例情况。设一个 n*m 的稀疏矩阵 A 中有 t 个非零元素,则稀疏因子δ 的计算公式:δ=t/m*n​ (当这个值小于等于0.05时,可以认为是稀疏矩阵)

稀疏矩阵的压缩存储

存储矩阵的一般方法是采用二维数组,其优点是可以随机地访问每一个元素,因而能够较容易地实现矩阵的各种运算,如转置运算、加法运算、乘法运算等。但对于稀疏矩阵来说,采用二维数组的存储方法既浪费大量的存储单元用来存放零元素,又要在运算中花费大量的时间来进行零元素的无效计算,显然不科学。所以必须考虑对稀疏矩阵进行压缩存储。

对稀疏矩阵进行压缩存储的一种较好的方法是:只存储在矩阵中极少数的非零元素,为此必须对每一个非零元素,保存它的下标和值。可以采用一个三元组Trituple<row,column,value>来唯一地确定一个矩阵元素。因此,稀疏矩阵需要使用一个三元组数组(亦称为三元组表)来表示。

常见的压缩存储方式有压缩行和压缩列。

对于如下矩阵

image

以三元组的方式(假设索引从1开始)为:

(row_index,col_index,value)

按行存储
[(1,1,10),(1,5,-2),         //第1行
(2,1,3), (2,2,9),(2,6,3),   //第2行
(3,2,7),(3,3,8),(3,4,7),    //第3行
(4,1,3),(4,5,5),            //第4行
(5,2,8),(5,4,9),(5,5,9),    //第5行
(6,2,4),(6,5,2),(6,6,-1)]   //第6行
     
按列存储
[(1,1,10),(2,1,3),(4,1,3),          //第1列
(2,2,9),(3,2,7),(5,2,8),(6,2,4),    //第2列
(3,3,8),                            //第3列
(3,4,7),(5,4,9),                    //第4列
(1,5,-2),(4,5,5),(5,5,9),(6,5,2),   //第5列
(2,6,3),(6,6,-1)]                   //第6列

Compressed Row Storage (CRS) 压缩行存储

压缩行和列存储格式是最通用的:它们不对矩阵的稀疏性结构做出假设,并且不存储任何不必要的元素。 但是,效率不高,需要对矩阵向量积或预处理器求解中的每个标量运算进行间接寻址步骤。

CSR矩阵格式通过用三个一维的数组来存储一个m×n的矩阵M。定义nnz(Num-non-zero)为矩阵M中非0元素的个数。

  1. val[] : 存储非 0 元素,长度为 nnz。
  2. col_ind[]:存储 val 中元素的列索引,nnz。
  3. row_ptr[]:存储每一行中第一个非零元素在 val 中的下标,长度为 m+1,其中 row\_ptr[m+1] = nnz+1 。这里即将行元素索引压缩了。

我们只需要 2*nnz+m+1的空间去存储矩阵。

index     = [1,  2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13,14, 15,16] //数组下标
value     = [10,-2, 3, 9, 3, 7, 8, 7, 3, 5, 8, 9, 9, 4, 2, -1] //按行读取时,依次出现的非零值
col_index = [1,  5, 1, 2, 6, 2, 3, 4, 1, 5,  2, 4, 5, 2, 6,  6] //按行读取时,依次出现的非零值所在的列下标
row_ptr   = [1,  3, 6, 9,11,14,17] //按行读取时,每一行中第一个出现的非零值在value数组中的下标值,可以参考三元组中按行存储。

Compressed Column Storage (CCS) 压缩列存储

index     = [1,  2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13,14, 15,16] //数组下标
value     = [10, 3, 3, 9, 7, 8, 4, 8, 7, 9, -2, 5, 9, 2, 3, -1] //按列读取时,依次出现的非零值
row_index = [1,  2, 4, 2, 3, 5, 6, 3, 3, 5,  1, 4, 5, 6, 2,  6] //案列读取时,依次出现非零值所在的行下标
col_ptr   = [1, 4, 8, 9,11,15,17] //按列读取时,每一列中第一个出现的非零值在value数组中的下标值,可以参考三元组中按列存储。

LibRec 实现

由于推荐系统场景中,用户物品的行为矩阵是一个稀疏矩阵,LibRec 2.0 中的 SparseMatrix.java类 实现了这一数据结构。其同时实现了 CRS 与 CCS 压缩存储。

我们先查看 LibRec 如何读取输入的用户评分/行为矩阵。以 TextDataConvertor.java为例,readData()函数实现了数据读取,存入 SparseMatrx。主要查看以下部分。

// Table {row-id, col-id, rate}  矩阵的三元组
Table<Integer, Integer, Double> dataTable = HashBasedTable.create();
// Map {col-id, multiple row-id}: used to fast build a rating matrix 是一个Multimap,存储列索引对应的行索引用以加快稀疏矩阵构建
Multimap<Integer, Integer> colMap = HashMultimap.create();

....
    // 不断读取数据行
        for (int i = 0; i < loopLength; i++) {
                    String line = bufferData[i];
                    String[] data = line.trim().split("[ \t,]+");
                    String user = data[0];
                    String item = data[1];
                    Double rate = ((dataColumnFormat.equals("UIR") ||       dataColumnFormat.equals("UIRT")) && data.length >= 3) ? Double.valueOf(data[2]) : 1.0;

                    // binarize the rating for item recommendation task
                    if (binThold >= 0) {
                        rate = rate > binThold ? 1.0 : 0.0;
                    }

                    // inner id starting from 0 
                    //稀疏矩阵的行索引,以判断 userId 是否存在来递增
                    int row = userIds.containsKey(user) ? userIds.get(user) : userIds.size();
                    userIds.put(user, row);
                  //稀疏矩阵的列索引,以判断 itemId 是否存在来递增
                    int col = itemIds.containsKey(item) ? itemIds.get(item) : itemIds.size();
                    itemIds.put(item, col);

                    dataTable.put(row, col, rate);
                    colMap.put(col, row);
                    // record rating's issuing time
                    if (StringUtils.equals(dataColumnFormat, "UIRT") && data.length >= 4) {
                        if (timeTable == null) {
                            timeTable = HashBasedTable.create();
                        }
                        // convert to million-seconds
                        long mms = 0L;
                        try {
                            mms = Long.parseLong(data[3]); // cannot format
                            // 9.7323480e+008
                        } catch (NumberFormatException e) {
                            mms = (long) Double.parseDouble(data[3]);
                        }
                        long timestamp = timeUnit.toMillis(mms);
                        timeTable.put(row, col, timestamp);
                    }
                }
                if (!isComplete) {
                    bufferLine = bufferData[bufferData.length - 1];
                } else {
                    bufferLine = "";
                }
                buffer.clear();
            }
            fileRead.close();
            fis.close();
        }
        int numRows = numUsers(), numCols = numItems();
        // build rating matrix  
        preferenceMatrix = new SparseMatrix(numRows, numCols, dataTable, colMap);
        if (timeTable != null)
            datetimeMatrix = new SparseMatrix(numRows, numCols, timeTable, colMap);
        // release memory of data table
        dataTable = null;
        timeTable = null;
    }

我们通过构造器来查看如何进行稀疏矩阵构建。

/**
     * Construct a sparse matrix with both CRS and CCS structures
     *
     * @param rows       number of rows
     * @param cols       number of columns
     * @param dataTable  data table  传入原始数据矩阵的三元组
     * @param colMap     column map  存储列索引对应的行索引用以加快稀疏矩阵构建
     */
    public SparseMatrix(int rows, int cols, Table<Integer, Integer, ? extends Number> dataTable, Multimap<Integer, Integer> colMap) {
        numRows = rows;
        numColumns = cols;
        valueSet = new TreeSet<>();
        construct(dataTable, colMap);
    }

 /**
     * Construct a sparse matrix
     *
     * @param dataTable       data table
     * @param columnStructure column structure
     */
    private void construct(Table<Integer, Integer, ? extends Number> dataTable,
                           Multimap<Integer, Integer> columnStructure) {
        int nnz = dataTable.size();

        // CRS 压缩行存储
        rowPtr = new int[numRows + 1];
        colInd = new int[nnz];
        rowData = new double[nnz];

        int j = 0;
        for (int i = 1; i <= numRows; ++i) {
            // 存储 rowPtr,为稀疏矩阵中每行第一个非0元素在 rowData 中的索引
            Set<Integer> cols = dataTable.row(i - 1).keySet();
            rowPtr[i] = rowPtr[i - 1] + cols.size();
            // 存储 colInd,按行读取时,依次出现的非零值所在的列下标
            for (int col : cols) {
                colInd[j++] = col;
                if (col < 0 || col >= numColumns)
                    throw new IllegalArgumentException("colInd[" + j + "]=" + col
                            + ", which is not a valid column index");
            }
            // 排序,保证列是从小到大的顺序
            Arrays.sort(colInd, rowPtr[i - 1], rowPtr[i]);
        }

        // CCS
        colPtr = new int[numColumns + 1];
        rowInd = new int[nnz];
        colData = new double[nnz];

        j = 0;
        for (int i = 1; i <= numColumns; ++i) {
            // dataTable.col(i-1) is more time-consuming than columnStructure.get(i-1)
            // 由于 dataTable.col(i-1) 比 columnStructure.get(i-1) 慢很多,使用 colMap 来加快构建速度
            Collection<Integer> rows = columnStructure != null ? columnStructure.get(i - 1) : dataTable.column(i - 1)
                    .keySet();
            colPtr[i] = colPtr[i - 1] + rows.size();

            for (int row : rows) {
                rowInd[j++] = row;
                if (row < 0 || row >= numRows)
                    throw new IllegalArgumentException("rowInd[" + j + "]=" + row + ", which is not a valid row index");
            }

            Arrays.sort(rowInd, colPtr[i - 1], colPtr[i]);
        }

        // set data  设置稀疏矩阵中的非0值,包括 rowData 和 colData
        for (Cell<Integer, Integer, ? extends Number> en : dataTable.cellSet()) {
            int row = en.getRowKey();
            int col = en.getColumnKey();
            double val = en.getValue().doubleValue();

            set(row, col, val);
        }
    }

以设置矩阵非0值与通过行列索引查找 rowData 的相应索引位置

/**
     * Set a value to entry [row, column]
     *
     * @param row    row id
     * @param column column id
     * @param val    value to set
     */
    public void set(int row, int column, double val) {
        int index = getCRSIndex(row, column);
        rowData[index] = val;

        index = getCCSIndex(row, column);
        colData[index] = val;

        valueSetAdd(val);
    }
/**
     * Finds the insertion index of CRS
     * 通过行列索引查找 rowData 的相应索引位置
     * @param row  the index of row
     * @param col  the index of column
     */
    private int getCRSIndex(int row, int col) {
        // 二分查找, colInd 存储 rowData 对应的矩阵列下标,rowPtr[row] 代表了 colIndex 中第 row 行第一个非零元素对应的位置,col 是需要查找的值
        int i = Arrays.binarySearch(colInd, rowPtr[row], rowPtr[row + 1], col);

        if (i >= 0 && colInd[i] == col)
            return i;
        else
            throw new IndexOutOfBoundsException("Entry (" + (row + 1) + ", " + (col + 1)
                    + ") is not in the matrix structure");
    }

查找稀疏矩阵中的对应元素

/**
     * Retrieve value at entry [row, column]
     *
     * @param row    row id
     * @param column column id
     * @return value at entry [row, column]
     */
    public double get(int row, int column) {
        // 与 getCRSIndex 原理相同,通过 rowPtr 和 colInd 来进行 rowData 的元素定位
        int index = Arrays.binarySearch(colInd, rowPtr[row], rowPtr[row + 1], column);

        if (index >= 0)
            return rowData[index];
        else
            return 0;
    }

参考

Compressed Row Storage (CRS)

Compressed Column Storage (CCS)

LibRec

http://westerly-lzh.github.io/cn/2014/12/Sparse-Matrix-Storage-Style/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容