python(八):python使用lmdb數據庫


一、入門代碼

LMDB的全稱是Lightning Memory-Mapped Database(快如閃電的內存映射數據庫),它的文件結構簡單,包含一個數據文件和一個鎖文件:

LMDB文件可以同時由多個進程打開,具有極高的數據存取速度,訪問簡單,不需要運行單獨的數據庫管理進程,只要在訪問數據的代碼里引用LMDB庫,訪問時給文件路徑即可。

讓系統訪問大量小文件的開銷很大,而LMDB使用內存映射的方式訪問文件,使得文件內尋址的開銷非常小,使用指針運算就能實現。數據庫單文件還能減少數據集復制/傳輸過程的開銷。

在python中使用lmdb: linux中,可以使用指令‘pip install lmdb' 安裝lmdb包。

1. 生成一個空的lmdb數據庫文件

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-
import lmdb
  
# 如果train文件夾下沒有data.mbd或lock.mdb文件,則會生成一個空的,如果有,不會覆蓋
# map_size定義最大儲存容量,單位是kb,以下定義1TB容量
env = lmdb. open ( "./train" ,map_size = 1099511627776 )
env.close()

2. LMDB數據的添加、修改、刪除

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding: utf-8 -*-
import lmdb
  
# map_size定義最大儲存容量,單位是kb,以下定義1TB容量
env = lmdb. open ( "./train" , map_size = 1099511627776 )
  
txn = env.begin(write = True )
  
# 添加數據和鍵值
txn.put(key = '1' , value = 'aaa' )
txn.put(key = '2' , value = 'bbb' )
txn.put(key = '3' , value = 'ccc' )
  
# 通過鍵值刪除數據
txn.delete(key = '1' )
  
# 修改數據
txn.put(key = '3' , value = 'ddd' )
  
# 通過commit()函數提交更改
txn.commit()
env.close()

3. 查詢lmdb數據庫內容

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
# -*- coding: utf-8 -*-
import lmdb
  
env = lmdb. open ( "./train" )
  
# 參數write設置為True才可以寫入
txn = env.begin(write = True )
############################################添加、修改、刪除數據
  
# 添加數據和鍵值
txn.put(key = '1' , value = 'aaa' )
txn.put(key = '2' , value = 'bbb' )
txn.put(key = '3' , value = 'ccc' )
  
# 通過鍵值刪除數據
txn.delete(key = '1' )
  
# 修改數據
txn.put(key = '3' , value = 'ddd' )
  
# 通過commit()函數提交更改
txn.commit()
############################################查詢lmdb數據
txn = env.begin()
  
# get函數通過鍵值查詢數據
print txn.get( str ( 2 ))
  
# 通過cursor()遍歷所有數據和鍵值
for key, value in txn.cursor():
   print (key, value)
    
############################################
  
env.close()

4. 讀取已有.mdb文件內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: utf-8 -*-
import lmdb
  
env_db = lmdb.Environment( 'trainC' )
# env_db = lmdb.open("./trainC")
  
txn = env_db.begin()
  
# get函數通過鍵值查詢數據,如果要查詢的鍵值沒有對應數據,則輸出None
print txn.get( str ( 200 ))
  
for key, value in txn.cursor(): #遍歷
   print (key, value)
  
env_db.close()

二、進階

LMDB 介紹

 

LMDB 全稱為 Lightning Memory-Mapped Database,就是非常快的內存映射型數據庫,LMDB使用內存映射文件,可以提供更好的輸入/輸出性能,對於用於神經網絡的大型數據集( 比如 ImageNet ),可以將其存儲在 LMDB 中。

 

因為最開始 Caffe 就是使用的這個數據庫,所以網上的大多數關於 LMDB 的教程都通過 Caffe 實現的,對於不了解 Caffe 的同學很不友好,所以本篇文章只講解 LMDB。

 

LMDB屬於key-value數據庫,而不是關系型數據庫( 比如 MySQL ),LMDB提供 key-value 存儲,其中每個鍵值對都是我們數據集中的一個樣本。LMDB的主要作用是提供數據管理,可以將各種各樣的原始數據轉換為統一的key-value存儲。

 

LMDB效率高的一個關鍵原因是它是基於內存映射的,這意味着它返回指向鍵和值的內存地址的指針,而不需要像大多數其他數據庫那樣復制內存中的任何內容。

 

LMDB不僅可以用來存放訓練和測試用的數據集,還可以存放神經網絡提取出的特征數據。如果數據的結構很簡單,就是大量的矩陣和向量,而且數據之間沒有什么關聯,數據內沒有復雜的對象結構,那么就可以選擇LMDB這個簡單的數據庫來存放數據。

 

LMDB的文件結構很簡單,一個文件夾,里面是一個數據文件和一個鎖文件。數據隨意復制,隨意傳輸。它的訪問簡單,不需要單獨的數據管理進程。只要在訪問代碼里引用LMDB庫,訪問時給文件路徑即可。

 

用LMDB數據庫來存放圖像數據,而不是直接讀取原始圖像數據的原因:

  • 數據類型多種多樣,比如:二進制文件、文本文件、編碼后的圖像文件jpeg、png等,不可能用一套代碼實現所有類型的輸入數據讀取,因此通過LMDB數據庫,轉換為統一數據格式可以簡化數據讀取層的實現。
  • lmdb具有極高的存取速度,大大減少了系統訪問大量小文件時的磁盤IO的時間開銷。LMDB將整個數據集都放在一個文件里,避免了文件系統尋址的開銷,你的存儲介質有多快,就能訪問多快,不會因為文件多而導致時間長。LMDB使用了內存映射的方式訪問文件,這使得文件內尋址的開銷大幅度降低。

 

 

LMDB 的基本函數

  • env = lmdb.open():創建 lmdb 環境
  • txn = env.begin():建立事務
  • txn.put(key, value):進行插入和修改
  • txn.delete(key):進行刪除
  • txn.get(key):進行查詢
  • txn.cursor():進行遍歷
  • txn.commit():提交更改

 

創建一個 lmdb 環境:

# 安裝:pip install lmdb import lmdb env = lmdb.open(lmdb_path, map_size=1099511627776)

lmdb_path 指定存放生成的lmdb數據庫的文件夾路徑,如果沒有該文件夾則自動創建。

 

map_size 指定創建的新數據庫所需磁盤空間的最小值,1099511627776B=1T。可以在這里進行 存儲單位換算

 

會在指定路徑下創建 data.mdb 和 lock.mdb 兩個文件,一是個數據文件,一個是鎖文件。

 

修改數據庫內容:

txn = env.begin(write=True) # insert/modify txn.put(str(1).encode(), "Alice".encode()) txn.put(str(2).encode(), "Bob".encode()) # delete txn.delete(str(1).encode()) txn.commit()

先創建一個事務(transaction) 對象 txn,所有的操作都必須經過這個事務對象。因為我們要對數據庫進行寫入操作,所以將 write 參數置為 True,默認其為 False

 

使用 .put(key, value) 對數據庫進行插入和修改操作,傳入的參數為鍵值對。

 

值得注意的是,需要在鍵值字符串后加 .encode() 改變其編碼格式,將 str 轉換為 bytes 格式,否則會報該錯誤:TypeError: Won't implicitly convert Unicode to bytes; use .encode()。在后面使用 .decode() 對其進行解碼得到原數據。

 

使用 .delete(key) 刪除指定鍵值對。

 

對LMDB的讀寫操作在事務中執行,需要使用 commit 方法提交待處理的事務。

 

查詢數據庫內容:

txn = env.begin() print(txn.get(str(2).encode())) for key, value in txn.cursor(): print(key, value) env.close()

每次 commit() 之后都要用 env.begin() 更新 txn(得到最新的lmdb數據庫)。

 

使用 .get(key) 查詢數據庫中的單條記錄。

 

使用 .cursor() 遍歷數據庫中的所有記錄,其返回一個可迭代對象,相當於關系數據庫中的游標,每讀取一次,游標下移一位。

 

也可以想文件一樣使用 with 語法:

with env.begin() as txn: print(txn.get(str(2).encode())) for key, value in txn.cursor(): print(key, value)

 

完整的demo如下:

import lmdb import os, sys def initialize(): env = lmdb.open("lmdb_dir") return env def insert(env, sid, name): txn = env.begin(write=True) txn.put(str(sid).encode(), name.encode()) txn.commit() def delete(env, sid): txn = env.begin(write=True) txn.delete(str(sid).encode()) txn.commit() def update(env, sid, name): txn = env.begin(write=True) txn.put(str(sid).encode(), name.encode()) txn.commit() def search(env, sid): txn = env.begin() name = txn.get(str(sid).encode()) return name def display(env): txn = env.begin() cur = txn.cursor() for key, value in cur: print(key, value) env = initialize() print("Insert 3 records.") insert(env, 1, "Alice") insert(env, 2, "Bob") insert(env, 3, "Peter") display(env) print("Delete the record where sid = 1.") delete(env, 1) display(env) print("Update the record where sid = 3.") update(env, 3, "Mark") display(env) print("Get the name of student whose sid = 3.") name = search(env, 3) print(name) # 最后需要關閉關閉lmdb數據庫 env.close() # 執行系統命令 os.system("rm -r lmdb_dir")

 

圖片數據示例

在圖像深度學習訓練中我們一般都會把大量原始數據集轉化為lmdb格式以方便后續的網絡訓練。因此我們也需要對該數據集進行lmdb格式轉化。

 

將圖片和對應的文本標簽存放到lmdb數據庫:

import lmdb image_path = './cat.jpg' label = 'cat' env = lmdb.open('lmdb_dir') cache = {} # 存儲鍵值對 with open(image_path, 'rb') as f: # 讀取圖像文件的二進制格式數據 image_bin = f.read() # 用兩個鍵值對表示一個數據樣本 cache['image_000'] = image_bin cache['label_000'] = label with env.begin(write=True) as txn: for k, v in cache.items(): if isinstance(v, bytes): # 圖片類型為bytes txn.put(k.encode(), v) else: # 標簽類型為str, 轉為bytes txn.put(k.encode(), v.encode()) # 編碼 env.close()

這里需要獲取圖像文件的二進制格式數據,然后用兩個鍵值對保存一個數據樣本,即分開保存圖片和其標簽。

 

然后分別將圖像和標簽寫入到lmdb數據庫中,和上面例子一樣都需要將鍵值轉換為 bytes 格式。因為此處讀取的圖片格式本身就為 bytes,所以不需要轉換,標簽格式為 str,寫入數據庫之前需要先進行編碼將其轉換為 bytes

 

從lmdb數據庫中讀取圖片數據:

import cv2 import lmdb import numpy as np env = lmdb.open('lmdb_dir') with env.begin(write=False) as txn: # 獲取圖像數據 image_bin = txn.get('image_000'.encode()) label = txn.get('label_000'.encode()).decode() # 解碼 # 將二進制文件轉為十進制文件(一維數組) image_buf = np.frombuffer(image_bin, dtype=np.uint8) # 將數據轉換(解碼)成圖像格式 # cv2.IMREAD_GRAYSCALE為灰度圖,cv2.IMREAD_COLOR為彩色圖 img = cv2.imdecode(image_buf, cv2.IMREAD_COLOR) cv2.imshow('image', img) cv2.waitKey(0)

先通過 lmdb.open() 獲取之前創建的lmdb數據庫。

 

這里通過鍵得到圖片和其標簽,因為寫入數據庫之前進行了編碼,所以這里需要先解碼。

  • 標簽通過 .decode() 進行解碼重新得到字符串格式。
  • 讀取到的圖片數據為二進制格式,所以先使用 np.frombuffer() 將其轉換為十進制格式的文件,這是一維數組。然后可以使用 cv2.imdecode() 將其轉換為灰度圖(二維數組)或者彩色圖(三維數組)。

三、原理

  • Linux內存布局
    • 低址到高址分別是Text segment(代碼段),數據段(已初始化的全局,靜態變量),BSS段(未初始化的全局,靜態變量),堆,內存映射區以及棧。
    • 內存分配的兩種方式:
      • brk
        • brk是將數據段(.data)的最高地址指針_edata往高地址推。
        • malloc小於128k的內存,使用brk分配內存。
        • brk分配的內存需要等到高地址內存釋放以后才能釋放。
      • mmap
        • mmap是在進程的虛擬地址空間中(堆和棧中間,稱為文件映射區域的地方)找一塊空閑的虛擬內存。
        • malloc大於128k的內存,使用mmap分配內存。
        • mmap分配的內存可以單獨釋放。
    • 這兩種方式分配的都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操作系統負責分配物理內存,然后建立虛擬內存和物理內存之間的映射關系。

 

    • LMDB(Lightning Memory-Mapped Database)是一個小型數據庫,具有一些強大的功能:
      • 有序映射接口(鍵始終排序,支持范圍查找)
      • 帶有MVCC(多版本並發控制)的完全事務性,完全ACID(原子性,一致性,隔離性,耐久性)語義。
      • 讀者/寫者事務:讀者不會阻止寫者,寫者也不會阻止讀者。
        • 寫入程序已完全序列化,因此寫入始終無死鎖。
      • 讀事務非常低開銷,可以不使用malloc或任何其他阻塞調用來執行。
      • 支持多線程和多進程並發,環境可以由同一主機上的多個進程打開。
      • 可以創建多個子數據庫,其中事務覆蓋所有子數據庫。
      • 內存映射,允許零拷貝查找和迭代。
      • 免維護,無需外部過程或后台清理/壓縮。
      • 防崩潰,無需日志或崩潰恢復過程。
      • 沒有應用程序級緩存。
        • LMDB充分利用了操作系統的緩沖區高速緩存。
      • 32KB的目標代碼和C的6KLOC。

 

  • LMDB的基本結構

 

    • 內存映射(Memory Map)
      • 內存映射就是把物理內存映射到進程的地址空間之內,這些應用程序就可以直接使用輸入輸出的地址空間。
        • 使用內存映射文件處理存儲於磁盤上的文件時,將不需要由應用程序對文件執行I/O操作,這意味着在對文件進行處理時將不必再為文件申請並分配緩存,所有的文件緩存操作均由系統直接管理,由於取消了將文件數據加載到內存、數據從內存到文件的回寫以及釋放內存塊等步驟,使得內存映射文件在處理大數據量的文件時能起到相當重要的作用。

 

    • 一般文件的io過程(兩次數據拷貝)
  1. 首先調用read()(系統調用)先將文件內容從硬盤拷貝到內核空間的一個緩沖區(一次數據拷貝)。
  2. 然后再將這些數據拷貝到用戶空間。(一次數據拷貝)
  • 內存映射的文件io過程(一次數據拷貝)
  1. 調用mmap()(系統調用)分配邏輯地址。
  2. 邏輯地址轉換成物理地址。
  3. 進程第一次訪問ptr指向的內存地址,發生缺頁中斷。由中斷處理函數將數據拷貝到相應的內存地址上。(一次數據拷貝)
  4. 虛擬地址置換。
    • lmdb使用mmap文件映射,不管這個文件存儲實在內存上還是在持久存儲上。
      • lmdb的所有讀取操作都是通過mmap將要訪問的文件只讀的映射到虛擬內存中,直接訪問相應的地址。
      • 因為使用了read-only的mmap,同樣避免了程序錯誤將存儲結構寫壞的風險。
      • 寫操作,則是通過write系統調用進行的,這主要是為了利用操作系統的文件系統一致性,避免在被訪問的地址上進行同步。
      • lmdb把整個虛擬存儲組織成B+Tree存儲,索引和值讀存儲在B+Tree的頁面上(聚集索引)。
      • LMDB中使用append-only B+樹,其更新操作最終都會轉換為B+樹的物理存儲的append操作,文件不支持內部的修改,只支持append。
        • append增加了存儲開銷,因為舊的數據依然存在。帶來一個額外的好處就是,舊的鏈路依然存在,依然可以正常的訪問,例如過去有個人持有了過去的root(root9)的指針,那么過去的整棵樹都完整的存在。

    • COW(Copy-on-write)寫時復制
      • 如果有多個調用者(callers)同時要求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統才會真正復制一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。
      • 因此多個調用者只是讀取操作時可以共享同一份資源。
      • 優點
        • 如果調用者沒有修改該資源,就不會有副本(private copy)被創建。

 

  • 當前讀
    • 讀取的是記錄的最新版本,讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖。
      • 例如select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操作都是一種當前讀
  • 快照讀
    • 快照讀的實現是基於多版本並發控制,即MVCC,避免了加鎖操作,降低了開銷。
    • MVCC(Multiversion concurrency control )多版並發本控制
      • 當MVCC數據庫需要更新數據項時,它不會用新數據覆蓋舊數據,而是將舊數據標記為已過時並在其他位置添加新版本。
        • 因此存儲了多個版本,但只有一個是最新版本。
      • MVCC通過保存數據的歷史版本,根據比較版本號來處理數據的是否顯示,從而達到讀取數據的時候不需要加鎖就可以保證事務隔離性的效果。
      • 數據庫系統維護當前活躍的事務ID列表m_ids,其中最小值up_limit_id和最大值low_limit_id,被訪問的事務ID:trx_id。
        • 如果trx_id< up_limit_id,說明trx_id對應的事務在生成可讀視圖前已經被提交了,可以被當前事務訪問
        • 如果trx_id> low_limit_id,說明事務trx_id生成可讀視圖后才生成的,所以不可以被當前事務訪問到。
        • 如果up_limit_id <=trx_id<= low_limit_id,判斷trx_id是否在m_ids中,若在,則說明trix_id在生成可讀視圖時還處於活躍,不可以被訪問到;弱國不在m_ids中,說明在生成可讀視圖時該事務已經被提交了,故可以被訪問到。

 

  • MVCC/COW在LMDB中的實現
    • LMDB對MVCC加了一個限制,即只允許一個寫線程存在,從根源上避免了寫寫沖突,當然代價就是寫入的並發性能下降。
      • 因為只有一個寫線程,所以不會不需要wait日志、讀寫依賴隊列、鎖隊列等一系列控制並發、事務回滾、數據恢復的基礎工具。
    • MVCC的基礎就是COW,對於不同的用戶來說,若其在整個操作過程中不進行任何的數據改變,其就使用同一份數據即可。若需要進行改變,比如增加、刪除、修改等,就需要在私有數據版本上進行,修改完成提交之后才給其他事務可見。
    • LMDB的事務實現
      • Atom(A)原子性:LMDB中通過txn數據結構和cursor數據結構的控制,通過將臟頁列表放入 dirtylist中,當txn進行提交時再一次性統一刷新到磁盤中或者abort時都不提交保證事務要不全成功、要不全失敗。對於長事務,若頁面spill到磁盤,因為COW技術,這些頁面未與整棵B-Tree的rootpage產生關聯,因此后續的事務還是不能訪問到這些頁面,同樣保證了事務的原子性。
      • Consistency(C)一致性: 有如上的操作,保證其數據就是一致的,不存在因為多線程同時寫數據導致數據產生錯誤的情況。
      • Isolation(I)隔離性:事務隔離通過鎖控制(MUTEX),LMDB支持的鎖互斥是進程級別/線程級別,支持的隔離方式為鎖表支持,讀讀之間不鎖,寫等待讀完成之后開始,讀等待寫完成后開始。
      • Duration(D)持久性:,沒有使用WAL、undo/redo log等技術來保證系統崩潰時數據庫的可用性,其保證數據持續可用的技術是COW技術和只有一線程寫技術。
        • 假如LMDB或者系統崩潰時,只有讀操作,那么數據本來就沒有發生變化,因此數據將不可能遭到破壞。假如崩潰時,有一個線程在進行寫操作,則只需要判斷最后的頁面號與成功提交到數據庫中的頁面號是否一致,若不一致則說明寫操作沒有完成,則最后一個事務寫失敗,數據在最后一個成功的頁面前的是正確的,后續的屬於崩潰事務的,不能用,這樣就保證了數據只要序列化到磁盤則一定可用,要不其就是還沒有遵循ACI原則序列化到磁盤。

 

  • 聚集索引和輔助索引
    • 聚集索引
      • 指索引項的排序方式和表中數據記錄排序方式一致的索引,每張表只能有一個聚集索引,聚集索引的葉子節點存儲了整個行數據。
        • 如果一個主鍵被定義了,那么這個主鍵就是作為聚集索引。
        • 如果沒有主鍵被定義,那么該表的第一個唯一非空索引被作為聚集索引。
        • 如果沒有主鍵也沒有合適的唯一索引,那么innodb內部會生成一個隱藏的主鍵作為聚集索引,這個隱藏的主鍵是一個6個字節的列,該列的值會隨着數據的插入自增。
      • 由於實際的數據頁只能按照一顆B+樹進行排序,因此每張表只能有一個聚集索引。
    • 輔助索引
      • 一個表中的所有索引除了聚集索引,其他的都是二級(輔助)索引(secondary index)。
      • 輔助索引,其葉子節點並不包含行記錄的全部數據,葉子結點除了包含鍵值以外,每個葉子結點中的索引行還包含了一個書簽,該書簽用來告訴存儲引擎可以在哪找到相應的數據行,由於innodb引擎表是索引組織表,因此innodb存儲引擎的輔助索引的書簽就是相應行數據的聚集索引鍵,
  • 示例代碼
import lmdb
# map_size定義最大儲存容量,單位是kb,以下定義1TB容量
env = lmdb.open("./train", map_size=1099511627776)

# 參數write設置為True才可以寫入
txn = env.begin(write=True) # 開啟事務

# 通過cursor()遍歷所有數據和鍵值
for key, value in txn.cursor():
        print (key, value)

# 添加數據和鍵值
txn.put(key = '1', value = 'aaa')
txn.put(key = '2', value = 'bbb')
txn.put(key = '3', value = 'ccc')

# 通過鍵值刪除數據
txn.delete(key = '1')

# 修改數據
txn.put(key = '3', value = 'ddd')

# 通過commit()函數提交更改
txn.commit()                        # 提交事務

env.close()         # 關閉事務

HDF5

HDF5 (Hierarchical Data Format) 是一種常見的跨平台數據儲存文件,可以存儲不同類型的圖像和數碼數據,並且可以在不同類型的機器上傳輸,同時還有統一處理這種文件格式的函數庫。

  • HDF5 文件結構中有 2 primary objects:
    • Groups
      • 類似於文件夾,每個 HDF5 文件其實就是根目錄 (root)為 group'/'
    • Datasets
      • 類似於 NumPy 中的數組 array
      • 每個 dataset 可以分成兩部分
        • 原始數據 (raw) data values
        • 元數據 metadata
          • 描述並提供有關其他數據的信息的數據
  • 整個HDF5結構
+-- /
|     +-- group_1
|     |     +-- dataset_1_1
|     |     |     +-- attribute_1_1_1
|     |     |     +-- attribute_1_1_2
|     |     |     +-- ...
|     |     |
|     |     +-- dataset_1_2
|     |     |     +-- attribute_1_2_1
|     |     |     +-- attribute_1_2_2
|     |     |     +-- ...
|     |     |
|     |     +-- ...
|     |
|     +-- group_2
|     |     +-- dataset_2_1
|     |     |     +-- attribute_2_1_1
|     |     |     +-- attribute_2_1_2
|     |     |     +-- ...
|     |     |
|     |     +-- dataset_2_2
|     |     |     +-- attribute_2_2_1
|     |     |     +-- attribute_2_2_2
|     |     |     +-- ...
|     |     |
|     |     +-- ...
|     |
|     +-- ...
|
  • HDF5的特性
    • 自述性
      • 對於一個HDF 文件里的每一個數據對象,有關於該數據的綜合信息(元數據)
        • 在沒有任何外部信息的情況下,HDF 允許應用程序解釋HDF文件的結構和內容。
    • 通用性
      • 許多數據類型都可以被嵌入在一個HDF文件里
        • 例如,通過使用合適的HDF 數據結構,符號、數字和圖形數據可以同時存儲在一個HDF 文件里。
    • 靈活性
      • HDF允許用戶把相關的數據對象組合在一起,放到一個分層結構中,向數據對象添加描述和標簽。
      • 它還允許用戶把科學數據放到多個HDF 文件里。
    • 擴展性
      • HDF極易容納將來新增加的數據模式,容易與其他標准格式兼容。
    • 跨平台性
      • HDF 是一個與平台無關的文件格式。HDF 文件無需任何轉換就可以在不同平台上使用。

四、pytorch使用

轉:https://blog.csdn.net/P_LarT/article/details/103208405

https://www.yuque.com/lart/ugkv9f/hbnym1

文章https://blog.csdn.net/jyl1999xxxx/article/details/53942824中介紹了使用LMDB的原因:

Caffe使用LMDB來存放訓練/測試用的數據集,以及使用網絡提取出的feature(為了方便,以下還是統稱數據集)。數據集的結構很簡單,就是大量的矩陣/向量數據平鋪開來。數據之間沒有什么關聯,數據內沒有復雜的對象結構,就是向量和矩陣。既然數據並不復雜,Caffe就選擇了LMDB這個簡單的數據庫來存放數據。

圖像數據集歸根究底從圖像文件而來。引入數據庫存放數據集,是為了減少IO開銷。讀取大量小文件的開銷是非常大的,尤其是在機械硬盤上。LMDB的整個數據庫放在一個文件里,避免了文件系統尋址的開銷。LMDB使用內存映射的方式訪問文件,使得文件內尋址的開銷非常小,使用指針運算就能實現。數據庫單文件還能減少數據集復制/傳輸過程的開銷。一個幾萬,幾十萬文件的數據集,不管是直接復制,還是打包再解包,過程都無比漫長而痛苦。LMDB數據庫只有一個文件,你的介質有多塊,就能復制多快,不會因為文件多而慢如蝸牛。

在文章http://shuokay.com/2018/05/14/python-lmdb/中類似提到:

為什么要把圖像數據轉換成大的二進制文件?

簡單來說,是因為讀寫小文件的速度太慢。那么,不禁要問,圖像數據也是二進制文件,單個大的二進制文件例如 LMDB 文件也是二進制文件,為什么單個圖像讀寫速度就慢了呢?這里分兩種情況解釋。

  1. 機械硬盤的情況:機械硬盤的每次讀寫啟動時間比較長,例如磁頭的尋道時間占比很高,因此,如果單個小文件讀寫,尤其是隨機讀寫單個小文件的時候,這個尋道時間占比就會很高,最后導致大量讀寫小文件的時候時間會很浪費;
  2. NFS 的情況:在 NFS 的場景下,系統的一次讀寫首先要進行上百次的網絡通訊,並且這個通訊次數和文件的大小無關。因此,如果是讀寫小文件,這個網絡通訊時間占據了整個讀寫時間的大部分。

固態硬盤的情況下應該也會有一些類似的開銷,目前沒有研究過。

 

總而言之,使用LMDB可以為我們的數據讀取進行加速。

 

具體操作

 

LMDB主要類

 

pip install imdb

 

lmdb.Environment

 

lmdb.open() class lmdb.Environment(path, map_size=10485760, subdir=True, readonly=False, metasync=True, sync=True, map_async=False, mode=493, create=True, readahead=True, writemap=False, meminit=True, max_readers=126, max_dbs=0, max_spare_txns=1, lock=True)https://lmdb.readthedocs.io/en/release/#environment-class

 

這是數據庫環境的結構。 一個環境可能包含多個數據庫,所有數據庫都駐留在同一共享內存映射和基礎磁盤文件中。

幾個重要的實例方法:

  •  begin(db=None, parent=None, write=False, buffers=False): 可以調用事務類 lmdb.Transaction open_db(key=None, txn=None, reverse_key=False, dupsort=False, create=True, integerkey=False, integerdup=False, dupfixed=False): 打開一個數據庫,返回一個不透明的句柄。重復Environment.open_db() 調用相同的名稱將返回相同的句柄。作為一個特殊情況,主數據庫總是開放的。命名數據庫是通過在主數據庫中存儲一個特殊的描述符來實現的。環境中的所有數據庫共享相同的文件。因為描述符存在於主數據庫中,所以如果已經存在與數據庫名稱匹配的 key ,創建命名數據庫的嘗試將失敗。此外,查找和枚舉可以看到key 。如果主數據庫keyspace與命名數據庫使用的名稱沖突,則將主數據庫的內容移動到另一個命名數據庫。

 

>>> env = lmdb.open('/tmp/test', max_dbs=2)
>>> with env.begin(write=True) as txn
...     txn.put('somename', 'somedata')
>>> # Error: database cannot share name of existing key!
>>> subdb = env.open_db('somename')

 

lmdb.Transaction

 

這和事務對象有關。

 

class lmdb.Transaction(env, db=None, parent=None, write=False, buffers=False) 。

 

關於這個類的參數:https://lmdb.readthedocs.io/en/release/#transaction-class

 

所有操作都需要事務句柄,事務可以是只讀或讀寫的。寫事務可能不會跨越線程。事務對象實現了上下文管理器協議,因此即使面對未處理的異常,也可以可靠地釋放事務:

 

# Transaction aborts correctly:
with env.begin(write=True) as txn:
   crash()
# Transaction commits automatically:
with env.begin(write=True) as txn:
   txn.put('a', 'b')

 

這個類的實例包含着很多有用的操作方法。

 

  • abort(): 中止掛起的事務。重復調用 abort() 在之前成功的 commit()  或 abort() 后或者在相關環境關閉后是沒有效果的。
  • 提交掛起的事務。
  • Shortcut for lmdb.Cursor(db, self) 
  • delete(key, value='', db=None): Delete a key from the database.
  • The key to delete.
  • value:如果數據庫是以 dupsort = True 打開的,並且 value 不是空的 bytestring ,則刪除僅與此 (key, value) 對匹配的元素,否則該 key 的所有值都將被刪除。
  • Returns True  if at least one key was deleted.
  • cursors
  • pop(key, db=None): 使用臨時cursor調用 Cursor.pop() 。
  • put(key, value, dupdata=True, overwrite=True, append=False, db=None): 存儲一條記錄(record),如果記錄被寫入,則返回 True ,否則返回 False ,以指示key已經存在並且 overwrite = False 。成功后,cursor位於新記錄上。
  • key: Bytestring key to store.
  • value: Bytestring value to store.
  • dupdata: 如果 True ,並且數據庫是用 dupsort = True 打開的,如果給定 key 已經存在,則添加鍵值對作為副本。否則覆蓋任何現有匹配的 key 。
  • overwrite: 
  • append: 如果為 True ,則將對附加到數據庫末尾,而不首先比較其順序。附加不大於現有最高 key 的 key 將導致損壞。
  • db: 要操作的命名數據庫。如果未指定,默認為
  • replace(key, value, db=None): 使用臨時cursor調用 Cursor.replace() .
  • Named database to operate on. If unspecified, defaults to the database given to the Transaction constructor.
  • stat(db): Return statistics like Environment.stat() , except for a single DBI. db  must be a database handle returned by open_db() .

 

Imdb.Cursor 

 

class lmdb.Cursor(db, txn) 是用於在數據庫中導航(navigate)的結構。

  • Database to navigate.
  • txn: 

     

    As a convenience, Transaction.cursor()  can be used to quickly return a cursor:

     

    >>> env = lmdb.open('/tmp/foo')
    >>> child_db = env.open_db('child_db')
    >>> with env.begin() as txn:
    ...     cursor = txn.cursor()           # Cursor on main database.
    ...     cursor2 = txn.cursor(child_db)  # Cursor on child database.

     

    游標以未定位的狀態開始。如果在這種狀態下使用 iternext() 或 iterprev() ,那么迭代將分別從開始處和結束處開始。迭代器直接使用游標定位,這意味着在同一游標上存在多個迭代器時會產生奇怪的行為

     

    從Python綁定的角度來看,一旦任何掃描或查找方法(例如 next() 、 prev_nodup() 、 set_range() )返回 False 或引發異常,游標將返回未定位狀態。這主要是為了確保在面對任何錯誤條件時語義的安全性和一致性。

    當游標返回到未定位的狀態時,它的 key() 和 value() 返回空字符串,表示沒有活動的位置,盡管在內部,LMDB游標可能仍然有一個有效的位置。

    這可能會導致在迭代 dupsort=True 數據庫的 key 時出現一些令人吃驚的行為,因為 iternext_dup() 等方法將導致游標顯示為未定位,盡管它返回 False 只是為了表明當前鍵沒有更多的值。在這種情況下,簡單地調用 next() 將導致在下一個可用鍵處繼續迭代。

    This behaviour may change in future.

     

    Iterator methods such as iternext()  and iterprev()  accept keys and values arguments. If both are True , then the value of item()  is yielded on each iteration. If only keys is True , key()  is yielded, otherwise only value() is yielded.

     

    在迭代之前,游標可能定位在數據庫中的任何位置

     

    >>> with env.begin() as txn:
    ...     cursor = txn.cursor()
    ...     if not cursor.set_range('5'): # Position at first key >= '5'.
    ...         print('Not found!')
    ...     else:
    ...         for key, value in cursor: # Iterate from first key >= '5'.
    ...             print((key, value))

     

    不需要迭代來導航,有時會導致丑陋或低效的代碼。在迭代順序不明顯的情況下,或者與正在讀取的數據相關的情況下,使用 set_key() 、 set_range() 、 key() 、 value() 和 item() 可能是更好的選擇。

     

    >>> # Record the path from a child to the root of a tree.
    >>> path = ['child14123']
    >>> while path[-1] != 'root':
    ...     assert cursor.set_key(path[-1]), \
    ...         'Tree is broken! Path: %s' % (path,)
    ...     path.append(cursor.value())

     

    幾個實例方法:

    • set_key(key): Seek exactly to key, returning True  on success or False  if the exact key was not found. 對於 set_key() ,空字節串是錯誤的。對於使用 dupsort=True 打開的數據庫,移動到鍵的第一個值(復制)。
    • set_range(key): Seek to the first key  greater than or equal to key , returning True  on success, or False  to indicate key  was past end of database. Behaves like first()  if key is the empty bytestring. 對於使用 dupsort=True 打開的數據庫,移動到鍵的第一個值(復制)。
    • get(key, default=None): Equivalent to set_key() , except value()  is returned when key is found, otherwise default.
    • item(): Return the current (key, value) pair.
    • key(): Return the current key.
    • value(): Return the current value.

     

    操作流程

     

    概況地講,操作LMDB的流程是:

     

    • 通過 env = lmdb.open() 打開環境
    • 通過 txn = env.begin() 建立事務
    • 通過 txn.put(key, value)  進行插入和修改
    • 通過 txn.delete(key)  進行刪除
    • 通過 txn.get(key)  進行查詢
    • 通過 txn.cursor()  進行遍歷
    • 通過 txn.commit()  提交更改

     

    這里要注意:

    1. put 和 delete 后一定注意要 commit ,不然根本沒有存進去
    2. 每一次 commit 后,需要再定義一次 txn=env.begin(write=True) 

     

    來自的代碼:

     

    #!/usr/bin/env python
    
    import lmdb
    import os, sys
    
    def initialize():
    	env = lmdb.open("students");
    	return env;
    
    def insert(env, sid, name):
    	txn = env.begin(write = True);
    	txn.put(str(sid), name);
    	txn.commit();
    
    def delete(env, sid):
    	txn = env.begin(write = True);
    	txn.delete(str(sid));
    	txn.commit();
    
    def update(env, sid, name):
    	txn = env.begin(write = True);
    	txn.put(str(sid), name);
    	txn.commit();
    
    def search(env, sid):
    	txn = env.begin();
    	name = txn.get(str(sid));
    	return name;
    
    def display(env):
    	txn = env.begin();
    	cur = txn.cursor();
    	for key, value in cur:
    		print (key, value);
    
    env = initialize();
    
    print "Insert 3 records."
    insert(env, 1, "Alice");
    insert(env, 2, "Bob");
    insert(env, 3, "Peter");
    display(env);
    
    print "Delete the record where sid = 1."
    delete(env, 1);
    display(env);
    
    print "Update the record where sid = 3."
    update(env, 3, "Mark");
    display(env);
    
    print "Get the name of student whose sid = 3."
    name = search(env, 3);
    print name;
    
    env.close();
    
    os.system("rm -r students");

     

    創建圖像數據集

     

    這里主要借鑒自https://github.com/open-mmlab/mmsr/blob/master/codes/data_scripts/create_lmdb.py的代碼。

     

    改寫為:

     

    import glob
    import os
    import pickle
    import sys
    
    import cv2
    import lmdb
    import numpy as np
    from tqdm import tqdm
    
    
    def main(mode):
        proj_root = '/home/lart/coding/TIFNet'
        datasets_root = '/home/lart/Datasets/'
        lmdb_path = os.path.join(proj_root, 'datasets/ECSSD.lmdb')
        data_path = os.path.join(datasets_root, 'RGBSaliency', 'ECSSD/Image')
        
        if mode == 'creating':
            opt = {
                'name': 'TrainSet',
                'img_folder': data_path,
                'lmdb_save_path': lmdb_path,
                'commit_interval': 100,  # After commit_interval images, lmdb commits
                'num_workers': 8,
            }
            general_image_folder(opt)
        elif mode == 'testing':
            test_lmdb(lmdb_path, index=1)
    
    
    def general_image_folder(opt):
        """
        Create lmdb for general image folders
        If all the images have the same resolution, it will only store one copy of resolution info.
            Otherwise, it will store every resolution info.
        """
        img_folder = opt['img_folder']
        lmdb_save_path = opt['lmdb_save_path']
        meta_info = {'name': opt['name']}
        
        if not lmdb_save_path.endswith('.lmdb'):
            raise ValueError("lmdb_save_path must end with 'lmdb'.")
        if os.path.exists(lmdb_save_path):
            print('Folder [{:s}] already exists. Exit...'.format(lmdb_save_path))
            sys.exit(1)
        
        # read all the image paths to a list
        
        print('Reading image path list ...')
        all_img_list = sorted(glob.glob(os.path.join(img_folder, '*')))
        # cache the filename, 這里的文件名必須是ascii字符
        keys = []
        for img_path in all_img_list:
            keys.append(os.path.basename(img_path))
        
        # create lmdb environment
        
        # 估算大概的映射空間大小
        data_size_per_img = cv2.imread(all_img_list[0], cv2.IMREAD_UNCHANGED).nbytes
        print('data size per image is: ', data_size_per_img)
        data_size = data_size_per_img * len(all_img_list)
        env = lmdb.open(lmdb_save_path, map_size=data_size * 10)
        # map_size:
        # Maximum size database may grow to; used to size the memory mapping. If database grows larger
        # than map_size, an exception will be raised and the user must close and reopen Environment.
        
        # write data to lmdb
        
        txn = env.begin(write=True)
        resolutions = []
        tqdm_iter = tqdm(enumerate(zip(all_img_list, keys)), total=len(all_img_list), leave=False)
        for idx, (path, key) in tqdm_iter:
            tqdm_iter.set_description('Write {}'.format(key))
            
            key_byte = key.encode('ascii')
            data = cv2.imread(path, cv2.IMREAD_UNCHANGED)
            
            if data.ndim == 2:
                H, W = data.shape
                C = 1
            else:
                H, W, C = data.shape
            resolutions.append('{:d}_{:d}_{:d}'.format(C, H, W))
            
            txn.put(key_byte, data)
            if (idx + 1) % opt['commit_interval'] == 0:
                txn.commit()
                # commit 之后需要再次 begin
                txn = env.begin(write=True)
        txn.commit()
        env.close()
        print('Finish writing lmdb.')
        
        # create meta information
        
        # check whether all the images are the same size
        assert len(keys) == len(resolutions)
        if len(set(resolutions)) <= 1:
            meta_info['resolution'] = [resolutions[0]]
            meta_info['keys'] = keys
            print('All images have the same resolution. Simplify the meta info.')
        else:
            meta_info['resolution'] = resolutions
            meta_info['keys'] = keys
            print('Not all images have the same resolution. Save meta info for each image.')
        
        pickle.dump(meta_info, open(os.path.join(lmdb_save_path, 'meta_info.pkl'), "wb"))
        print('Finish creating lmdb meta info.')
    
    
    def test_lmdb(dataroot, index=1):
        env = lmdb.open(dataroot, readonly=True, lock=False, readahead=False, meminit=False)
        meta_info = pickle.load(open(os.path.join(dataroot, 'meta_info.pkl'), "rb"))
        print('Name: ', meta_info['name'])
        print('Resolution: ', meta_info['resolution'])
        print('# keys: ', len(meta_info['keys']))
        
        # read one image
        key = meta_info['keys'][index]
        print('Reading {} for test.'.format(key))
        with env.begin(write=False) as txn:
            buf = txn.get(key.encode('ascii'))
        img_flat = np.frombuffer(buf, dtype=np.uint8)
        
        C, H, W = [int(s) for s in meta_info['resolution'][index].split('_')]
        img = img_flat.reshape(H, W, C)
        
        cv2.namedWindow('Test')
        cv2.imshow('Test', img)
        cv2.waitKeyEx()
    
    
    if __name__ == "__main__":
        # mode = creating or testing
        main(mode='creating')

     

    配合DataLoader

     

    這里僅對訓練集進行LMDB處理,測試機依舊使用的原始的讀取圖片的方式。

     

    import os
    import pickle
    
    import lmdb
    import numpy as np
    from PIL import Image
    from prefetch_generator import BackgroundGenerator
    from torch.utils.data import DataLoader, Dataset
    from torchvision import transforms
    
    from utils import joint_transforms
    
    
    def _get_paths_from_lmdb(dataroot):
        """get image path list from lmdb meta info"""
        meta_info = pickle.load(open(os.path.join(dataroot, 'meta_info.pkl'),
                                     'rb'))
        paths = meta_info['keys']
        sizes = meta_info['resolution']
        if len(sizes) == 1:
            sizes = sizes * len(paths)
        return paths, sizes
    
    
    def _read_img_lmdb(env, key, size):
        """read image from lmdb with key (w/ and w/o fixed size)
        size: (C, H, W) tuple"""
        with env.begin(write=False) as txn:
            buf = txn.get(key.encode('ascii'))
        img_flat = np.frombuffer(buf, dtype=np.uint8)
        C, H, W = size
        img = img_flat.reshape(H, W, C)
        return img
    
    
    def _make_dataset(root, prefix=('.jpg', '.png')):
        img_path = os.path.join(root, 'Image')
        gt_path = os.path.join(root, 'Mask')
        img_list = [
            os.path.splitext(f)[0] for f in os.listdir(gt_path)
            if f.endswith(prefix[1])
        ]
        return [(os.path.join(img_path, img_name + prefix[0]),
                 os.path.join(gt_path, img_name + prefix[1]))
                for img_name in img_list]
    
    
    class TestImageFolder(Dataset):
        def __init__(self, root, in_size, prefix):
            self.imgs = _make_dataset(root, prefix=prefix)
            self.test_img_trainsform = transforms.Compose([
                # 輸入的如果是一個tuple,則按照數據縮放,但是如果是一個數字,則按比例縮放到短邊等於該值
                transforms.Resize((in_size, in_size)),
                transforms.ToTensor(),
                transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
            ])
        
        def __getitem__(self, index):
            img_path, gt_path = self.imgs[index]
            
            img = Image.open(img_path).convert('RGB')
            img_name = (img_path.split(os.sep)[-1]).split('.')[0]
            
            img = self.test_img_trainsform(img)
            return img, img_name
        
        def __len__(self):
            return len(self.imgs)
    
    
    class TrainImageFolder(Dataset):
        def __init__(self, root, in_size, scale=1.5, use_bigt=False):
            self.use_bigt = use_bigt
            self.in_size = in_size
            self.root = root
            
            self.train_joint_transform = joint_transforms.Compose([
                joint_transforms.JointResize(in_size),
                joint_transforms.RandomHorizontallyFlip(),
                joint_transforms.RandomRotate(10)
            ])
            self.train_img_transform = transforms.Compose([
                transforms.ColorJitter(0.1, 0.1, 0.1),
                transforms.ToTensor(),
                transforms.Normalize([0.485, 0.456, 0.406],
                                     [0.229, 0.224, 0.225])  # 處理的是Tensor
            ])
            # ToTensor 操作會將 PIL.Image 或形狀為 H×W×D,數值范圍為 [0, 255] 的 np.ndarray 轉換為形狀為 D×H×W,
            # 數值范圍為 [0.0, 1.0] 的 torch.Tensor。
            self.train_target_transform = transforms.ToTensor()
            
            self.gt_root = '/home/lart/coding/TIFNet/datasets/DUTSTR/DUTSTR_GT.lmdb'
            self.img_root = '/home/lart/coding/TIFNet/datasets/DUTSTR/DUTSTR_IMG.lmdb'
            self.paths_gt, self.sizes_gt = _get_paths_from_lmdb(self.gt_root)
            self.paths_img, self.sizes_img = _get_paths_from_lmdb(self.img_root)
            self.gt_env = lmdb.open(self.gt_root, readonly=True, lock=False, readahead=False,
                                    meminit=False)
            self.img_env = lmdb.open(self.img_root, readonly=True, lock=False, readahead=False,
                                     meminit=False)
        
        
        def __getitem__(self, index):
            gt_path = self.paths_gt[index]
            img_path = self.paths_img[index]
            
            gt_resolution = [int(s) for s in self.sizes_gt[index].split('_')]
            img_resolution = [int(s) for s in self.sizes_img[index].split('_')]
            img_gt = _read_img_lmdb(self.gt_env, gt_path, gt_resolution)
            img_img = _read_img_lmdb(self.img_env, img_path, img_resolution)
            if img_img.shape[-1] != 3:
                img_img = np.repeat(img_img, repeats=3, axis=-1)
            img_img = img_img[:, :, [2, 1, 0]]  # bgr => rgb
            img_gt = np.squeeze(img_gt, axis=2)
            gt = Image.fromarray(img_gt, mode='L')
            img = Image.fromarray(img_img, mode='RGB')
            
            img, gt = self.train_joint_transform(img, gt)
            gt = self.train_target_transform(gt)
            img = self.train_img_transform(img)
            
            if self.use_bigt:
                gt = gt.ge(0.5).float()  # 二值化
            
            img_name = self.paths_img[index]
            return img, gt, img_name
        
        def __len__(self):
            return len(self.paths_img)
    
    
    class DataLoaderX(DataLoader):
        def __iter__(self):
            return BackgroundGenerator(super(DataLoaderX, self).__iter__())

     

     

     

文章https://blog.csdn.net/jyl1999xxxx/article/details/53942824中介紹了使用LMDB的原因:

 

五、實戰

from pathlib import Path
import shutil
import lmdb
import numpy as np
from faiss_modules.faiss_index import FaissIndex

class DetectConfusion(object):
    def __init__(self, dataset_path=None, rebuild_cache=False, cache_path = ".similar_metrix"):
        if rebuild_cache or not Path(cache_path).exists():
            shutil.rmtree(cache_path, ignore_errors=True)
            if dataset_path is None:
                raise ValueError('創建緩存失敗,數據集路徑為空。')
            self.__build_cache(dataset_path, cache_path)
        self.env = lmdb.open(cache_path, create=False, lock=False, readonly=True)

    def __build_cache(self, dataset_path, cache_path):
        with lmdb.open(cache_path, map_size=int(1e10)) as env:
            with env.begin(write=True) as txn:
                score, line_index, avg = FaissIndex().detect()
                txn.put(b'score', score.tobytes())
                txn.put(b'line_index', line_index.tobytes())
                txn.put(b'avg', avg.tobytes())

    def main(self):
        with self.env.begin(write=False) as txn:
            score = np.frombuffer(txn.get(b'score'), dtype=np.uint32)
            line_index = np.frombuffer(txn.get(b'line_index'), dtype=np.uint32)
            avg = np.frombuffer(txn.get(b'avg'), dtype=np.uint32)
        print(score, line_index, avg)



if __name__ == '__main__':
   a = DetectConfusion("data/", True).main()

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM