Переваги HDF5: Організація, гнучкість, сумісність
Одними з основних переваг HDF5 є його ієрархічна структура (подібна до папок / файлів), необов'язкові довільні метадані, що зберігаються з кожним елементом, та гнучкість (наприклад, стиснення). Ця організаційна структура та зберігання метаданих може здатися тривіальним, але це дуже корисно на практиці.
Ще однією перевагою HDF є те, що набори даних можуть бути як фіксованого, так і гнучкого розміру. Тому легко додати дані до великого набору даних, не створюючи повністю нової копії.
Крім того, HDF5 - це стандартизований формат із бібліотеками, доступними майже для будь-якої мови, тому обмін даними на диску між, скажімо, Matlab, Fortran, R, C та Python дуже простий із HDF. (Чесно кажучи, це не надто складно і з великим двійковим масивом, якщо ви знаєте порядок C проти F та знаєте форму, тип і т.д. збереженого масиву.)
Переваги HDF для великого масиву: Швидше введення / виведення довільного фрагмента
Так само, як TL / DR: для 3D-масиву ~ 8 Гб читання "повного" фрагмента вздовж будь-якої осі займало ~ 20 секунд із фрагментованим набором даних HDF5 та 0,3 секунди (найкращий випадок) - понад три години (найгірший випадок) для збережений масив тих самих даних.
Окрім перерахованих вище речей, є ще одна велика перевага у "шматованому" * форматі даних на диску, наприклад HDF5: Читання довільного фрагмента (наголос на довільному) зазвичай відбувається набагато швидше, оскільки дані на диску є суміжнішими середній.
*
(HDF5 не обов’язково має бути форматом фрагментованих даних. Він підтримує фрагментування, але цього не вимагає. Насправді, за замовчуванням для створення набору даних за замовчуванням h5py
не передбачається фрагментація, якщо я правильно пам’ятаю.)
В основному, ваша найкраща швидкість читання з диска та найгірша швидкість читання з диска для даного фрагмента вашого набору даних будуть досить близькими до фрагментованого набору даних HDF (за умови, що ви вибрали розумний розмір фрагменту або дозволите бібліотеці вибрати один для вас). За допомогою простого двійкового масиву найкращий випадок є швидшим, а найгірший - набагато гіршим.
Одне застереження: якщо у вас є SSD, ви, швидше за все, не помітите величезної різниці в швидкості читання / запису. Однак на звичайному жорсткому диску послідовне читання відбувається набагато швидше, ніж випадкове читання. (Тобто звичайний жорсткий диск має довгий seek
час.) HDF ще має перевагу на SSD, але це більше з - за його інші функції (наприклад , метадані, організації і т.д.) , ніж з - за сиру швидкість.
По-перше, для усунення плутанини доступ до h5py
набору даних повертає об’єкт, який поводиться досить подібно до масиву numpy, але не завантажує дані в пам’ять, доки їх не наріжуть. (Подібно до memmap, але не ідентично.) Подивіться на h5py
вступ для отримання додаткової інформації.
Нарізання набору даних завантажить підмножину даних в пам’ять, але, мабуть, ви хочете щось з цим зробити, і в цей момент це все одно вам знадобиться в пам’яті.
Якщо ви хочете робити позаядерні обчислення, ви можете досить легко отримати табличні дані за допомогою pandas
або pytables
. Це можливо за допомогою h5py
(приємніше для великих масивів ND), але вам потрібно опуститись на нижчий рівень і обробити ітерацію самостійно.
Тим не менш, майбутнє за нумі-подібними позаядерними обчисленнями - Blaze. Подивіться на це, якщо ви дійсно хочете піти цим шляхом.
Справа "без відключення"
Спочатку розглянемо 3D-впорядкований масив, записаний на диск (я змоделюю його, зателефонувавши arr.ravel()
та роздрукувавши результат, щоб зробити речі більш видимими):
In [1]: import numpy as np
In [2]: arr = np.arange(4*6*6).reshape(4,6,6)
In [3]: arr
Out[3]:
array([[[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11],
[ 12, 13, 14, 15, 16, 17],
[ 18, 19, 20, 21, 22, 23],
[ 24, 25, 26, 27, 28, 29],
[ 30, 31, 32, 33, 34, 35]],
[[ 36, 37, 38, 39, 40, 41],
[ 42, 43, 44, 45, 46, 47],
[ 48, 49, 50, 51, 52, 53],
[ 54, 55, 56, 57, 58, 59],
[ 60, 61, 62, 63, 64, 65],
[ 66, 67, 68, 69, 70, 71]],
[[ 72, 73, 74, 75, 76, 77],
[ 78, 79, 80, 81, 82, 83],
[ 84, 85, 86, 87, 88, 89],
[ 90, 91, 92, 93, 94, 95],
[ 96, 97, 98, 99, 100, 101],
[102, 103, 104, 105, 106, 107]],
[[108, 109, 110, 111, 112, 113],
[114, 115, 116, 117, 118, 119],
[120, 121, 122, 123, 124, 125],
[126, 127, 128, 129, 130, 131],
[132, 133, 134, 135, 136, 137],
[138, 139, 140, 141, 142, 143]]])
Ці значення будуть зберігатися на диску послідовно, як показано в рядку 4 нижче. (Давайте ігноруємо деталі та фрагментацію файлової системи на даний момент.)
In [4]: arr.ravel(order='C')
Out[4]:
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77,
78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])
У найкращому випадку візьмемо зріз уздовж першої осі. Зверніть увагу, що це лише перші 36 значень масиву. Це буде дуже швидко читати! (один шукати, один читати)
In [5]: arr[0,:,:]
Out[5]:
array([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17],
[18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29],
[30, 31, 32, 33, 34, 35]])
Аналогічним чином, наступний зріз уздовж першої осі буде просто наступними 36 значеннями. Щоб прочитати повний зріз уздовж цієї осі, нам потрібна лише одна seek
операція. Якщо все, що ми будемо читати, - це різні фрагменти вздовж цієї осі, то це ідеальна структура файлу.
Однак давайте розглянемо найгірший сценарій: зріз вздовж останньої осі.
In [6]: arr[:,:,0]
Out[6]:
array([[ 0, 6, 12, 18, 24, 30],
[ 36, 42, 48, 54, 60, 66],
[ 72, 78, 84, 90, 96, 102],
[108, 114, 120, 126, 132, 138]])
Щоб прочитати цей фрагмент, нам потрібно 36 пошуків та 36 читань, оскільки всі значення розділені на диску. Жоден з них не примикає!
Це може здатися досить незначним, але по мірі того, як ми потрапляємо до все більших і більших масивів, кількість і розмір seek
операцій швидко зростає. Для великого (~ 10 Гб) 3D-масиву, що зберігається таким чином і зчитується через memmap
, читання повного фрагмента вздовж "найгіршої" осі може легко зайняти десятки хвилин, навіть із сучасним обладнанням. У той же час зріз уздовж найкращої осі може зайняти менше секунди. Для простоти я показую лише «повні» зрізи вздовж однієї осі, але те саме відбувається з довільними зрізами будь-якої підмножини даних.
До речі, існує кілька форматів файлів, які цим скористалися і в основному зберігають на диску три копії величезних тривимірних масивів: одну в C-порядку, одну в F-порядку та одну в проміжних між ними. (Прикладом цього є формат D3D Geoprobe, хоча я не впевнений, що це десь задокументовано.) Кому цікаво, чи остаточний розмір файлу становить 4 ТБ, зберігання дешеве! Божевільне в цьому полягає в тому, що, оскільки основним варіантом використання є вилучення одного під-зрізу в кожному напрямку, читання, які ви хочете зробити, дуже і дуже швидкі. Це працює дуже добре!
Простий "кусатий" випадок
Скажімо, ми зберігаємо 2x2x2 "шматки" 3D-масиву як суміжні блоки на диску. Іншими словами, щось на зразок:
nx, ny, nz = arr.shape
slices = []
for i in range(0, nx, 2):
for j in range(0, ny, 2):
for k in range(0, nz, 2):
slices.append((slice(i, i+2), slice(j, j+2), slice(k, k+2)))
chunked = np.hstack([arr[chunk].ravel() for chunk in slices])
Тож дані на диску будуть виглядати так chunked
:
array([ 0, 1, 6, 7, 36, 37, 42, 43, 2, 3, 8, 9, 38,
39, 44, 45, 4, 5, 10, 11, 40, 41, 46, 47, 12, 13,
18, 19, 48, 49, 54, 55, 14, 15, 20, 21, 50, 51, 56,
57, 16, 17, 22, 23, 52, 53, 58, 59, 24, 25, 30, 31,
60, 61, 66, 67, 26, 27, 32, 33, 62, 63, 68, 69, 28,
29, 34, 35, 64, 65, 70, 71, 72, 73, 78, 79, 108, 109,
114, 115, 74, 75, 80, 81, 110, 111, 116, 117, 76, 77, 82,
83, 112, 113, 118, 119, 84, 85, 90, 91, 120, 121, 126, 127,
86, 87, 92, 93, 122, 123, 128, 129, 88, 89, 94, 95, 124,
125, 130, 131, 96, 97, 102, 103, 132, 133, 138, 139, 98, 99,
104, 105, 134, 135, 140, 141, 100, 101, 106, 107, 136, 137, 142, 143])
І лише, щоб показати, що це блоки 2x2x2 arr
, зверніть увагу, що це перші 8 значень chunked
:
In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0, 1],
[ 6, 7]],
[[36, 37],
[42, 43]]])
Щоб читати будь-який зріз уздовж осі, ми читали 6 або 9 суміжних шматків (удвічі більше даних, ніж нам потрібно), а потім зберігали лише ту частину, яку хотіли. Це найгірший випадок - максимум 9 пошуків проти максимум 36 пошуків для версії без фрагментів. (Але найкращий випадок все-таки 6 шукає проти 1 для заповненого масиву.) Оскільки послідовне читання відбувається дуже швидко в порівнянні з пошуками, це значно зменшує час, необхідний для читання довільної підмножини в пам’яті. Ще раз, цей ефект стає більшим із більшими масивами.
HDF5 робить це на кілька кроків далі. Шматки не потрібно зберігати суміжно, і вони індексуються B-Деревом. Крім того, вони не повинні мати однаковий розмір на диску, тому стиснення можна застосувати до кожного фрагмента.
Колоні масиви з h5py
За замовчуванням h5py
не створює фрагментовані файли HDF на диску (я думаю pytables
, навпаки). chunks=True
Однак, якщо ви вкажете при створенні набору даних, ви отримаєте фрагментований масив на диску.
Як швидкий, мінімальний приклад:
import numpy as np
import h5py
data = np.random.random((100, 100, 100))
with h5py.File('test.hdf', 'w') as outfile:
dset = outfile.create_dataset('a_descriptive_name', data=data, chunks=True)
dset.attrs['some key'] = 'Did you want some metadata?'
Зверніть увагу , що chunks=True
говорить , h5py
щоб автоматично вибрати розмір порції для нас. Якщо ви знаєте більше про найпоширеніший варіант використання, ви можете оптимізувати розмір / форму шматка, вказавши кортеж фігури (наприклад, (2,2,2)
у простому прикладі вище). Це дозволяє зробити читання вздовж певної осі більш ефективними або оптимізувати для читання / запису певного розміру.
Порівняння продуктивності вводу-виводу
Щоб наголосити на цьому, давайте порівняємо читання фрагментами з фрагментованого набору даних HDF5 та великого (~ 8 Гб) замовленого Фортраном 3D-масиву, що містить ті самі точні дані.
Я очистив усі кеші ОС між кожним запуском, тому ми спостерігаємо "холодну" продуктивність.
Для кожного типу файлу ми перевіримо читання у вигляді "повного" x-зрізу вздовж першої осі та "повного" z-розміру вздовж останньої осі. Для упорядкованого Фортраном меммапсованого масиву зріз «x» є найгіршим випадком, а зріз «z» - найкращим.
Використовуваний код міститься в сутності (включаючи створення hdf
файлу). Я не можу легко поділитися даними, які тут використовуються, але ви можете імітувати їх за допомогою масиву нулів однакової форми ( 621, 4991, 2600)
і типу np.uint8
.
У chunked_hdf.py
виглядає наступним чином :
import sys
import h5py
def main():
data = read()
if sys.argv[1] == 'x':
x_slice(data)
elif sys.argv[1] == 'z':
z_slice(data)
def read():
f = h5py.File('/tmp/test.hdf5', 'r')
return f['seismic_volume']
def z_slice(data):
return data[:,:,0]
def x_slice(data):
return data[0,:,:]
main()
memmapped_array.py
схожий, але має набагато складніший спосіб забезпечити завантаження фрагментів у пам’ять (за замовчуванням memmapped
буде повернено інший масив, що не буде порівнянням між яблуками та яблуками).
import numpy as np
import sys
def main():
data = read()
if sys.argv[1] == 'x':
x_slice(data)
elif sys.argv[1] == 'z':
z_slice(data)
def read():
big_binary_filename = '/data/nankai/data/Volumes/kumdep01_flipY.3dv.vol'
shape = 621, 4991, 2600
header_len = 3072
data = np.memmap(filename=big_binary_filename, mode='r', offset=header_len,
order='F', shape=shape, dtype=np.uint8)
return data
def z_slice(data):
dat = np.empty(data.shape[:2], dtype=data.dtype)
dat[:] = data[:,:,0]
return dat
def x_slice(data):
dat = np.empty(data.shape[1:], dtype=data.dtype)
dat[:] = data[0,:,:]
return dat
main()
Давайте спочатку подивимось на продуктивність HDF:
jofer at cornbread in ~
$ sudo ./clear_cache.sh
jofer at cornbread in ~
$ time python chunked_hdf.py z
python chunked_hdf.py z 0.64s user 0.28s system 3% cpu 23.800 total
jofer at cornbread in ~
$ sudo ./clear_cache.sh
jofer at cornbread in ~
$ time python chunked_hdf.py x
python chunked_hdf.py x 0.12s user 0.30s system 1% cpu 21.856 total
"Повний" х-зріз і "повний" зріз займають приблизно однакову кількість часу (~ 20 с). Враховуючи, що це масив розміром 8 Гб, це не так вже й погано. Більшу частину часу
І якщо ми порівняємо це із запам'ятовуваним часом масиву (це впорядковано Фортраном: "z-зріз" - найкращий випадок, а "x-зріз" - найгірший випадок.):
jofer at cornbread in ~
$ sudo ./clear_cache.sh
jofer at cornbread in ~
$ time python memmapped_array.py z
python memmapped_array.py z 0.07s user 0.04s system 28% cpu 0.385 total
jofer at cornbread in ~
$ sudo ./clear_cache.sh
jofer at cornbread in ~
$ time python memmapped_array.py x
python memmapped_array.py x 2.46s user 37.24s system 0% cpu 3:35:26.85 total
Так, ви правильно прочитали. 0,3 секунди для одного напрямку зрізу та ~ 3,5 години для іншого.
Час нарізки в напрямку "х" набагато довший, ніж час, необхідний для завантаження всього масиву 8 Гб в пам'ять і вибору потрібного нам зрізу! (Знову ж таки, це масив, впорядкований Фортраном. Протилежний час зрізу x / z мав би місце для масиву, упорядкованого C).
Однак, якщо ми завжди хочемо взяти фрагмент у найкращому випадку, великий двійковий масив на диску дуже хороший. (~ 0,3 сек!)
З заповненим масивом ви застрягли в цій невідповідності вводу / виводу (або, можливо, анізотропія - кращий термін). Однак, із фрагментованим набором даних HDF, ви можете вибрати зменшення, щоб доступ був рівним або оптимізованим для конкретного випадку використання. Це надає вам набагато більшу гнучкість.
Підсумовуючи
Будемо сподіватися, що це допоможе прояснити одну частину вашого питання, у будь-якому випадку. HDF5 має багато інших переваг перед "необробленими" меммапами, але тут у мене немає місця розширювати всі. Стиснення може пришвидшити деякі речі (дані, з якими я працюю, не мають великої користі від стиснення, тому я їх рідко використовую), а кешування на рівні ОС часто грає приємніше з файлами HDF5, ніж із "необробленими" меммапами. Крім цього, HDF5 - це справді фантастичний формат контейнера. Це дає вам велику гнучкість в управлінні вашими даними і може використовуватися з більш-менш будь-якої мови програмування.
Загалом, спробуйте і перевірте, чи добре це працює для вашого випадку використання. Я думаю, ви можете бути здивовані.
h5py
краще підходить для наборів даних, таких як ваш, ніжpytables
. Крім того ,h5py
це НЕ повертає масив Numpy в пам'яті. Натомість він повертає те, що поводиться як одне, але не завантажується в пам’ять (подібно доmemmapped
масиву). Я пишу більш повну відповідь (можливо, не закінчую), але, сподіваюся, цей коментар тим часом трохи допоможе.