這一篇文章較早被作者寫出,最早是在2016年7月24日。因爲最近要用到對稱矩陣,因此翻譯過來以備後續回顧。
在計算機科學中,對稱矩陣可用於存儲對象之間的距離或表示爲無向圖的鄰接矩陣。與經典矩陣相比,使用對稱矩陣的主要優勢在於較小的內存需求。在這篇文章中,描述了這種矩陣的Python實現。
介紹
在對稱矩陣中, 是等同於 的。由於這個特性,一個 的對稱矩陣需要存儲的也僅僅是 個元素 而不是一般矩陣中的 個元素。此類矩陣的示例如下所示:
矩陣對角線可以看作是一面鏡子。此鏡像上方的每個元素都會反射到該鏡像下方的元素。因此,不需要存儲對角線上方的元素。如前所述,對稱矩陣可用於表示距離或鄰接矩陣。在本文的以下部分中,將逐步解釋對稱矩陣的Python實現及其用法。
創建矩陣
在本節及後續各節中,我將首先展示特定用法,然後再展示實現。以下源代碼顯示瞭如何創建一個4 × 4 對稱矩陣:
>>> from symmetric_matrix import SymmetricMatrix as SM
>>> sm = SM(4)
爲了使此代碼可運行,必須實現 SymmetricMatrix
類。目前,僅需編寫一種特殊的方法,特別是該__init__()
方法僅需一個稱爲的參數size
。此參數指定行數。由於對稱矩陣是正方形,因此無需傳遞列數。__init__()
首先檢查提供的內容size
是否有效。如果不是,ValueError
則會引發異常。否則,size
存儲矩陣中的一個,並初始化矩陣的數據存儲(在這種情況下爲列表)。以下代碼顯示了實現:
class SymmetricMatrix:
def __init__(self, size):
if size <= 0:
raise ValueError('size has to be positive')
self._size = size
self._data = [0 for i in range((size + 1) * size // 2)]
值得注意的是 _data
用於存儲矩陣的存儲空間。它小於 。爲了解釋元素數量的計算,假設我們有一個 的對稱矩陣。爲了節省空間,僅需要保存對角線以下和其上的元素。因此,對於第一行,只需要存儲一個元素,對於第二行,則要保存兩個元素,依此類推。因此,對於第 N 行,有 N 個元素需要保存。如果我們彙總所有行中需要保存的所有元素,則會得到以下結果:
獲取矩陣大小
有可能使用一種標準的Python方法來獲得矩陣大小(即len()
函數),將是很好的。因此,爲了獲得矩陣大小,我們希望可以使用以下代碼:
>>> print(len(sm))
4
要啓動先前的代碼,必須實現另一種魔術方法。此方法是,__len__()
並且它唯一的責任是返回_size
屬性:
def __len__(self):
return self._size
訪問矩陣
到現在爲止,我們已經能夠創建一個對稱矩陣,並將所有元素初始化爲零並獲取其大小。但是,這在現實生活中不是很有用。我們還需要寫入和讀取矩陣。由於我們希望矩陣的使用盡可能舒適和自然,因此[]
在訪問矩陣時將使用下標運算符:
>>> sm[1, 1] = 1
>>> print('sm[1, 1] is', sm[1, 1])
sm[1, 1] is 1
寫入
首先,讓我們專注於寫入矩陣。在Python中,sm[1, 1]
執行對的賦值時,解釋器將調用__setitem__()
magic方法。爲了實現預期的行爲,必須在中實現此方法SymmetricMatrix
。由於僅存儲對角線下方和對角線上的元素,並且整個矩陣保存在一維數據存儲中,因此需要計算對該存儲的正確索引。現在,假設該_get_index()
方法返回此索引。稍後,將顯示此方法的實現。現在,有了索引後,我們可以使用__setitem__()
基礎存儲提供的方法,可以簡單地將其稱爲self._data[index] = value
:
def __setitem__(self, position, value):
index = self._get_index(position)
self._data[index] = value
讀取
爲了從矩陣中獲得一個元素,我們將以類似的方式進行。因此,__getitem__()
必須實現另一種魔術方法,特別是該方法。與前面的情況類似,要從矩陣中獲取所需的元素,必須將位置轉換爲基礎存儲的適當索引。該服務是通過_get_index()
本節最後一部分專門介紹的方法來完成的。當我們擁有正確的索引時,將返回基礎存儲中此位置上的元素:
def __getitem__(self, position):
index = self._get_index(position)
return self._data[index]
計算指標
現在,該展示如何_get_index()
實現了。傳遞的位置是一對錶格(row, column)
。此方法的源代碼可以分爲兩個步驟,必須按提供的順序執行:
- 將對角線上方的位置轉換爲對角線下方的適當位置,然後
- 計算到基礎存儲的正確索引。
如果給定位置(row, column)
處於對角線上方,則將row
與交換column
,因爲對角線上方的每個元素的(column, row)
位置都恰好在對角線上。爲了闡明第二部分,特別是使用存儲器的索引的計算,將使用上圖和下表:
Row | Size of All Previous Rows | Matrix Position | Calculated Index |
---|---|---|---|
1 | 0 | (0,column) | 0+column |
2 | 0+1 | (1,column) | 0+1+column |
3 | 0+1+2 | (2,column) | 0+1+2+column |
… | … | … | … |
row+1 | 0+1+2+3+⋯+row | (row,column) | 0+1+2+3+⋯+row+column |
請注意,對於第一行,該對的列部分(row, column)
足以用作基礎存儲的索引。對於第二行,該對的前一行和列部分中的元素數量(row, column)
足夠。對於第二行,計算得出的索引爲 ,因爲上一行僅包含一個元素。對於第三行,情況有些複雜,因爲必須對所有先前行中的元素進行求和。因此,該(2, column)
位置的索引是 。因此,對於該(row, column)
位置,正確的索引是 。由於有限的算術級數,該表達式可以簡化如下:
最後,下面的源代碼顯示了計算到基礎存儲中的索引的實現:
def _get_index(self, position):
row, column = position
if column > row:
row, column = column, row
index = (0 + row) * (row + 1) // 2 + column
return index
使用自己的存儲類型
現在,我們有了對稱矩陣的有效實現。由於使用這種類型的矩陣的主要動機是內存效率,因此可能出現的問題是,是否可以進行更高效的內存實現。這使我們考慮使用的list
是否是用於存儲的最佳數據結構。如果您熟悉的Python實現list
,則可能知道其中list
不包含您要插入其中的元素。實際上,它包含指向這些元素的指針。因此,list
對於例如array.array
直接存儲元素的存儲要求要更高。當然,還有其他數據結構比的存儲效率更高list
。因此,問題是應該使用哪一個。假設我們選擇array.array
了list
在對稱矩陣實現過程中。以後,此矩陣需要在多個進程之間共享。當然,由於array.array
它不應該由不同的進程共享,因此它將不起作用。
因此,選擇基礎數據結構時更好的解決方案是留出空間供用戶根據自己的需求選擇存儲類型。如果沒有特殊要求,則list
可以用作默認存儲類型。否則,用戶將在矩陣創建期間傳遞其存儲類型,如以下示例所示:
>>> def create_storage(size):
... return multiprocessing.Array(ctypes.c_int64, size)
...
>>> sm = SM(3, create_storage)
>>> sm[1, 2] = 5
上面的代碼create_storage()
返回一個數組,其中包含64b個整數,可以由不同的進程共享。
爲了實現這種改進,該__init__()
方法僅需要很小的改變。首先,添加一個參數,即create_storage
默認值設置爲None
。如果未傳遞該參數的參數,list
則將用作存儲類型。否則,需要一個採用一個參數(特別是存儲空間的大小)並返回創建的存儲空間的函數:
def __init__(self, size, create_storage=None):
if size <= 0:
raise ValueError('size has to be positive')
if create_storage is None:
create_storage = lambda n: [0 for i in range(n)]
self._size = size
self._data = create_storage((size + 1) * size // 2)
基準測試
爲了比較引入的對稱矩陣和通過numpy
模塊創建的矩陣,我編寫了一個基準腳本,該腳本使用4000 × 4000矩陣以顯示實現的對稱矩陣和numpy
矩陣的內存要求和平均訪問時間。創建對稱矩陣時,array.array()
將其用作基礎存儲。創建numpy
矩陣,numpy.zeros()
稱爲。兩個矩陣中的元素都是64b整數。
內存使用情況
首先,比較內存使用情況。模塊中的asizeof.asizeof()
函數pympler
計算所創建矩陣的大小。該實驗的結果可以在下表中看到。
Matrix Type | Memory Usage |
---|---|
Symmetric Matrix (via array ) |
61.05 MB |
numpy Matrix |
122.07 MB |
我們可以看到對稱矩陣可以節省大約50%的存儲空間。
訪問時間
接下來,針對兩種矩陣類型,計算用於寫入整個矩陣的訪問時間。進行五次該計算,然後計算平均結果。實驗在Intel四核i7-4700HQ(6M高速緩存,2.40 GHz)處理器上運行。從下表中可以看出,已實現的對稱矩陣的平均訪問時間比矩陣的平均訪問時間差得多numpy
:
Matrix Type | Access Time |
---|---|
Symmetric Matrix (via array ) |
11.26 sec |
numpy Matrix |
2.00 sec |
該cProfile
模塊可以揭示對稱矩陣訪問時間較慢的原因。在使用cProfile
模塊運行腳本之前,僅存在相關部分。因此,比較內存需求的第一部分和使用該numpy
代碼的所有部分均未包含在配置文件中。
$ python -m cProfile -s cumtime benchmark.py
...
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
130/1 0.005 0.000 74.098 74.098 {built-in method exec}
1 0.000 0.000 74.098 74.098 benchmark.py:8(<module>)
1 0.002 0.002 74.028 74.028 benchmark.py:91(main)
1 25.421 25.421 74.026 74.026 benchmark.py:56(benchmark_access_time)
80000000 24.473 0.000 48.290 0.000 symmetric_matrix.py:22(__setitem__)
80000000 23.817 0.000 23.817 0.000 symmetric_matrix.py:30(_get_index)
1 0.000 0.000 0.315 0.315 symmetric_matrix.py:9(__init__)
1 0.315 0.315 0.315 0.315 benchmark.py:21(create_array)
...
爲了理解上面的輸出,只有三列對我們很重要,即ncalls
,cumtime
和filename:lineno(function)
。第一個名爲ncalls
,表示從中filename:lineno(function)
調用該函數的次數。該cumtime
列通知我們有關在所有調用期間此功能和所有子功能所花費的累積時間。從輸出中可以看出,時間主要花費在__setitem__()
和中_get_index()
。開銷是由於Python的內部工作和對基礎存儲的計算索引所致。因此,這種對稱矩陣實現方式適用於內存使用量比處理器功率大的問題。
源代碼
GitHub上 SymmetricMatrix
提供了已實現類的完整源代碼,以及單元測試和基準腳本。所有代碼都是使用Python 3.4編寫,測試和分析的。