Hlavní navigace

Načítání a ukládání dat uložených v N-rozměrných polích v jazyku Go

23. 3. 2023
Doba čtení: 38 minut

Sdílet

 Autor: The Go Authors, podle licence: Public Domain
Popíšeme si knihovnu nazvanou npyio, která slouží pro načítání a ukládání n-rozměrných polí (typicky vektorů a matic) do souborů ve formátu NPY. Ten byl primárně vytvořen pro potřeby Numpy, ale používá se i jinde.

Obsah

1. Načítání a ukládání dat uložených v N-rozměrných polích v programovacím jazyku Go

2. Formát FITS (Flexible Image Transport System)

3. Formát GRIB (GRIdded Binary)

4. Formát NetCDF (Network Common Data Form)

5. Formát HDF (Hierarchical Data Format)

6. Standardní binární soubor knihovny Numpy

7. Interní struktura souboru ve formátu NPY

8. Uložení vektoru (jednorozměrného pole) do binárního souboru s využitím knihovny Numpy

9. Uložení a načtení matice do/ze standardního binárního souboru, opět s využitím knihovny Numpy

10. Knihovna npyio pro práci s NPY soubory v programovacím jazyku Go

11. Uložení vektoru s prvky typu int8 do souboru typu NPY

12. Uložení vektoru s deseti prvky typu int32

13. Uložení dvojrozměrné matice do souboru typu NPY

14. Načtení vektoru ze souboru typu NPY

15. Kontrola typů prvků při načítání

16. Načítání matic

17. Uložení a zpětné načtení rozsáhlejších souborů s velikostí přesahujících gigabyte

18. Závěrečné zhodnocení

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Načítání a ukládání dat uložených v N-rozměrných polích v programovacím jazyku Go

V mnoha oblastech souvisejících s IT se setkáme s daty, která jsou uložena v N-rozměrných polích (ND array). Nejčastěji se s velkou pravděpodobností setkáme s jednorozměrnými poli (neboli vektory), protože například zvukové záznamy jsou vlastně tvořeny sekvencí hodnot zvukových vzorků (samplů). A pochopitelně prakticky každý IT systém pracuje s obrazovými daty (ty si můžeme představit buď jako matice nebo jako trojrozměrná pole, v případě, že barvové roviny tvoří třetí dimenzi). Relativně často se setkáme i s vícerozměrnými poli, například v oblasti statistiky, lineární algebry, datové analýzy, strojového učení, zpracování medicínských či astronomických dat apod. Současně se jedná o datové struktury a operace, u nichž má velký smysl využít SIMD instrukce, které jsou dostupné na všech moderních mikroprocesorových architekturách. A právě z tohoto důvodu jsme se na stránkách Roota již mnohokrát setkali s programovacími jazyky, popř. s knihovnami, které jsou určeny právě pro zpracování n-rozměrných polí.

Víme již, že práce s N-rozměrnými poli je poměrně dobře podporována jak ve specializovaných programovacích jazycích (APL, J, K, …), tak i například v Pythonu, pro nějž byla vytvořena populární knihovna Numpy. Taktéž jsme se setkali s balíčky pro práci s N-rozměrnými poli určenými pro programovací jazyk Go. Připomeňme si, že se jednalo především o balíčky Gonum Numerical Packages a taktéž o balíček narray. Kvůli tomu, že se v oblasti statistiky, datové analýzy či strojového učení stále více používá programovací jazyk Python, je mnohdy nutné zajistit předávání dat (reprezentovaných ve formě N-rozměrných polí) právě mezi Pythonem a nástroji vytvořenými v jazyku Go. Této problematice se budeme věnovat v dnešním článku.

Data mezi Pythonem a Go lze pochopitelně předávat v různých formátech. Může se jednat o některé standardizované (či de facto standardizované) formáty typu XML, JSON či CSV, ovšem vzhledem k tomu, že N-rozměrná pole mnohdy obsahují miliony prvků, se většinou nebude jednat o to nejlepší řešení, nehledě na to, že všechny tři zmíněné formáty nepodporují všechny vyžadované formáty prvků N-rozměrných polí (což mohou být bity, bajty, víceslovní hodnoty se znaménkem i bez znaménka, hodnoty s plovoucí řádovou čárkou se zvolenou přesností a v některých případech můžeme pracovat i s komplexními čísly).

2. Formát FITS (Flexible Image Transport System)

Velmi zajímavý a potenciálně užitečný formát určený pro ukládání n-rozměrných polí (zejména však matic – tedy 2D struktur) je formát nazvaný FITS neboli Flexible Image Transport System, jenž se primárně používá v astronomii a s ní souvisejících odvětvích (využívá ho například NASA atd). V tomto formátu je možné do jediného souboru uložit několik takzvaných HDU neboli Header/Data Unit(s). Každá taková „jednotka“ se skládá z textové hlavičky a datového bloku. Hlavička je uložena jako čistý text složený z dvojic klíč = hodnota. Jednotlivé dvojice však od sebe nejsou odděleny koncem řádku, ale jsou zarovnány na osmdesát bajtů (mezerami). To nám dnes může připadat zvláštní, ale jedná se o formát vycházející ze „starobylé“ koncepce děrných štítků. Za hlavičkou následují hodnoty prvků polí, přičemž pole jsou typicky 1D vektory, 2D matice (obrazy) nebo 3D struktury.

Příklady dat uložených ve FITS najdeme například na adrese https://fits.gsfc.nasa.gov/fit­s_samples.html. Hlavička souboru https://fits.gsfc.nasa.gov/sam­ples/UITfuv2582gc.fits začíná těmito informacemi s velmi specifickými metainformacemi o uloženém obrázku (povšimněte si, že symbol = je vždy uveden v devátém sloupci):

SIMPLE  =                    T  / FLIGHT22 05Apr96 RSH
BITPIX  =                   16  / SIGNED 16-BIT INTEGERS
NAXIS   =                    2  / 2-DIMENSIONAL IMAGES
NAXIS1  =                  512  / SAMPLES PER LINE
NAXIS2  =                  512  / LINES PER IMAGE
EXTEND  =                    T  / FILE MAY HAVE EXTENSIONS
DATATYPE= 'INTEGER*2'           / SAME INFORMATION AS BITPIX
TELESCOP= 'UIT     '            / TELECOPE USED
INSTRUME= 'INTENSIFIED-FILM'    / DETECTOR USED
OBJECT  = 'NGC4151 '            / TARGET NAME
OBJECT2 = '_       '            / ALTERNATIVE TARGET NAME
CATEGORY= 'FLIGHT  '            / TARGET CATEGORY
JOTFID  = '8116-14 '            / ASTRO MISSION TARGET ID
IMAGE   = 'FUV2582 '            / IMAGE NUMBER
ORIGIN  = 'UIT/GSFC'            / WHERE TAPE WRITTEN
ASTRO   =                    2  / ASTRO MISSION NUMBER
FRAMENO = 'b0582   '            / ANNOTATED FRAME NUMBER
CATHODE = 'CSI     '            / IMAGE TUBE PHOTOCATHODE
FILTER  = 'B1      '            / CAMERA/FILTER IDENTIFIER
PDSDATIM= '06-JUL-1995  07:20'  / MICRODENSITOMETRY DATE & TIME
PDSID   =                   21  / MICRODENSITOMETER IDENT
PDSAPERT=                   20  / MICROD. APERTURE, MICRONS
PDSSTEP =                   10  / MICROD. STEP SIZE, MICRONS
PIXELSIZ=        8.0000000E+01  / CURRENT PIXEL SIZE, MICRONS
EQUINOX =        2.0000000E+03  / EQUINOX OF BEST COORDINATES
NOMRA   =             182.0044  / 1950 I.P.S.  R.A., DEGREES
NOMDEC  =              39.6839  / 1950 I.P.S.  DEC., DEGREES
NOMROLL =             323.9500  / I.P.S. ROLL ANGLE
NOMSCALE=        5.6832500E+01  / NOMINAL PLATE SCL (ARCSEC/MM)
CALIBCON=          5.00000E-16  / PREFLIGHT LAB CALIB FOR CAMERA
FEXPTIME= '8355    '            / EXPOSURE TIME, APPLICABLE FRM
DATE-OBS= '13/03/95'            / DATE OF OBSERVATION (GMT)
TIME-OBS=        6.2728000E+00  / TIME OF OBS (HOURS GMT)
BSCALE  =        2.0587209E-16  / CALIBRATION CONST
BUNIT   = 'ERGS/CM**2/S/ANGSTRM'
BZERO   =              0.00000  / ADDITIVE CONST FOR CALIB.
PATCHFIL= 'PATCH2  '            / FILE WITH PATCH INFORMATION
FADJPROG= 'UITBAK  '            / FOG ADJUSTMENT PROGRAM
FADJVER = '2.1     '            / FOG ADJUSTMENT PROGRAM VERSION
FADJDTIM= 'Jul 22,1996 12:53:24'
FOGLL   =        2.8988638E+02  / LOWER LEFT CORNER FOG

Za touto hlavičkou následují binární data – matice o velikosti 512×512 prvků, přičemž každý prvek je reprezentován jako šestnáctibitové celé číslo se znaménkem. Tyto základní informace přečteme z prvních čtyř metainformací z hlavičky:

BITPIX  =                   16  / SIGNED 16-BIT INTEGERS
NAXIS   =                    2  / 2-DIMENSIONAL IMAGES
NAXIS1  =                  512  / SAMPLES PER LINE
NAXIS2  =                  512  / LINES PER IMAGE
Poznámka: tímto velmi užitečným formátem se budeme podrobněji zabývat v samostatném článku.

3. Formát GRIB (GRIdded Binary)

Další formát, který se používá pro uložení matic, resp. dvoudimenzionálních polí s prvky různých (volitelných) hodnot, se jmenuje GRIB neboli GRIdded Binary, popř. General Regularly-distributed Information in Binary form. Z praktického pohledu je tento formát vlastně relativně jednoduchý, protože každý soubor se skládá z kolekce na sobě nezávislých dvoudimenzionálních polí s metadaty, která jsou v souboru uložena za sebou. Soubory však neobsahují žádná metadata, která by popisovala vztahy mezi jednotlivými poli (například fakt, že první pole reprezentuje vypočtenou teplotu povrchu/vzduchu a druhé vlhkost atd. pro stejnou plochu – tyto vztahy musí odvodit až nějaký program). Na druhou stranu je možné do metadat pro každé pole uložit mnoho důležitých informací, například rozlišení původních senzorů (či výpočetního modelu), souřadnice měření/výpočtů i vlastní jednotky veličiny uložené v poli (teplota, rychlost větru, koncentrace ozónu atd.). Tento formát je podporován například v Mathematice nebo v QGIS (což je open source GIS systém).

4. Formát NetCDF (Network Common Data Form)

Třetím formátem, o němž se v dnešním článku alespoň ve stručnosti zmíníme, je formát nazvaný NetCDF neboli Network Common Data Form. Opět se jedná o formát primárně určený pro ukládání polí. Jedná se o poměrně starý formát, protože práce na na něm začaly již v roce 1988. Původní formát se dnes nazývá „classic NetCDF format“ a zajímavé je, že se stále používá (ostatně stále se používá například i grafický formát GIF z roku 1987, resp. jeho nová verze z roku 1989). Čtvrtá verze formátu NetCDF z roku 2008 již přímo zmiňuje formát HDF zmíněný v navazující kapitole. NetCDF obsahuje hlavičku za níž následují jednotlivá pole a prakticky libovolné množství záznamů s metadaty o těchto polích ve formě dvojic klíč-hodnota. V současnosti je tento formát podporován v mnoha ekosystémech, například v ekosystému programovacího jazyka Python, jazyka Julia, v Mathematice, MATLABu atd. (již z tohoto výčtu je patrné, že tento formát je primárně určen pro uložení vědeckých dat).

Poznámka: tímto formátem se budeme zabývat v navazujícím článku.

5. Formát HDF (Hierarchical Data Format)

Posledním potenciálně užitečným formátem pro ukládání n-dimenzionálních polí, o němž se v dnešním článku zmíníme, je formát nazývaný HDF, resp. celým jménem Hierarchical Data Format. Ve skutečnosti se nejedná o jediný formát, ale o sadu formátů se stejným jménem, za nímž je uvedeno číslo verze (takže například HDF4 či HDF5, pokud máme jmenovat ty verze, s nimiž se lze nejčastěji setkat). V tomto formátu je možné uložit větší množství n-dimenzionálních polí včetně metadat, ale například HDF5 umožňuje ukládání dalších datových struktur. Zajímavé je, že pole (a další struktury) jsou uloženy ve struktuře, která připomíná souborový systém – jedná se tedy o stromovou strukturu a tím pádem i o strukturu, v níž je možné velmi snadno určovat hierarchii jednotlivých objektů. To ovšem vede k vyšší celkové složitosti celého formátu, zejména v porovnání s „primitivním“ formátem NPY, jímž se zabýváme dnes.

Tento formát je v současnosti podporován v mnoha ekosystémech, například v Julii, Mathematice, MATLABu a R (tedy v „matematických ekosystémech“), ale i v Pythonu či právě v jazyce Go nebo v Rustu.

6. Standardní binární soubor knihovny Numpy

Z předchozích kapitol je patrné, že je možné n-rozměrná pole ukládat do binárních souborů, a to hned v několika standardizovaných formátech. Tyto formáty jsou ovšem mnohdy relativně složité a jejich použití vyžaduje instalaci dalších knihoven. Komplikované formáty nejsou v mnoha případech ideálním řešením (data je totiž mnohdy nutné uchovávat po dlouhou dobu, kdy původní systémy již nemusí být funkční) a mj. i proto byl vyvinut dnes již taktéž standardní binární formát určený pro ukládání n-rozměrných polí. Tento formát se nazývá NPY a jeho popis lze nalézt na stránce https://numpy.org/devdocs/re­ference/generated/numpy.lib­.format.html. Jedná se o přímou serializaci pole do souboru, ovšem před vlastní hodnoty prvků je uložena jednoduchá hlavička se všemi důležitými informacemi – včetně endianity, kterou jsme prozatím vůbec neřešili.

Poznámka: pokud se má uložit větší množství polí, lze soubory NPY zabalit do ZIP archivu a použít koncovku NPZ. Podrobnosti si ukážeme dále.

7. Interní struktura souboru ve formátu NPY

Interní struktura formátu NPY je relativně jednoduchá. Na začátku je uloženo několik bajtů s informacemi o typu souboru a o verzi formátu. Následuje hlavička v textovém formátu (JSON), která bývá mezerami rozšířena tak, aby další blok začínal na offsetu dělitelném šestnácti. A po této hlavičce již následují hodnoty jednotlivých prvků pole – bez oddělovačů a výplní.

Podívejme se na jednoduchý příklad:

0000000 93 4e 55 4d 50 59 01 00 76 00 7b 27 64 65 73 63  >.NUMPY..v.{'desc<
0000020 72 27 3a 20 27 3c 66 32 27 2c 20 27 66 6f 72 74  >r': '<f2', 'fort<
0000040 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61 6c 73  >ran_order': Fals<
0000060 65 2c 20 27 73 68 61 70 65 27 3a 20 28 31 30 2c  >e, 'shape': (10,<
0000100 29 2c 20 7d 20 20 20 20 20 20 20 20 20 20 20 20  >), }            <
0000120 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
0000140 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
0000160 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 0a  >               .<
0000200 00 3c 00 40 00 42 00 44 00 45 00 46 00 47 00 48  >.<.@.B.D.E.F.G.H<
0000220 80 48 00 49                                      >.H.I<

Význam jednotlivých údajů v souboru je následující:

  1. Prvních šest bajtů obsahuje vždy stejnou sekvenci bajtů: hexadecimální hodnotu 0×93 následovanou pěti bajty tvořícími řetězec „NUMPY“.
  2. Následuje bajt obsahující hlavní (major) číslo verze, v našem případě 1.
  3. Další bajt obsahuje vedlejší (minor) číslo verze, konkrétně 0.
  4. Následuje dvojice bajtů (v pořadí little endian) s délkou metadat. V předchozím příkladu byla délka metadat nastavena na 0×76=118 bajtů.
  5. Samotná metadata obsahují informace v textovém formátu: „{‚descr‘: ‚<f2‘, ‚fortran_order‘: False, ‚shape‘: (10,), }“. Tato data popisují především tvar pole, typ prvků pole (f2) i způsob uložení little endian/big endian/jednotlivé bajty.
  6. To ještě není vše, protože šestice magických bajtů, číslo verze, délka metadat a celá struktura s metadaty bývá zarovnána na násobky šestnácti bajtů (to proto, aby se celé pole dalo načíst nějakou formou rychlého blokového čtení. Za hlavičkou tedy většinou následuje sekvence mezer (0×20) ukončená znakem pro nový řádek (0×0a). V našem konkrétním případě je délka metadat menší než zmíněných 118 bajtů, ovšem na tuto hodnotu je hlavička dorovnána sekvencí mezer a koncem řádku (celková délka začátku souboru je tedy 6+2+2+118=128 bajtů, což je číslo dělitelné šestnácti).
  7. Dále již následují surová data prvků pole ve zvoleném formátu.

8. Uložení vektoru (jednorozměrného pole) do binárního souboru s využitím knihovny Numpy

Formát NPY byl primárně určen pro použití v knihovně Numpy, takže si nejprve ukažme způsob jeho použití právě s využitím programovacího jazyka Python a knihovny Numpy.

Vektor s prvky libovolného typu se uloží do standardního binárního formátu funkcí save. Té je možné (a vhodné) předat parametr allow_pickle=False aby se zabránilo případné serializaci objektů v případě, že vektor bude nějaké objekty obsahovat (taková data by v Go postrádala smysl):

"""Uložení obsahu vektoru do standardního binárního souboru."""
 
import numpy as np
 
# vektor obsahující hodnoty s plovoucí řádovou čárkou
# s poloviční přesností (half)
v = np.linspace(1, 10, 10, dtype="e")
print(v)
 
np.save("vector.npy", v, allow_pickle=False)

Výsledný binární soubor si vypíšeme jak v hexadecimálním tvaru, tak i jako sekvenci znaků. Pro tento účel použijeme standardní nástroj od:

$ od -t x1z -v vector.npy

Obsah tohoto souboru bude následující:

0000000 93 4e 55 4d 50 59 01 00 76 00 7b 27 64 65 73 63  >.NUMPY..v.{'desc<
0000020 72 27 3a 20 27 3c 66 32 27 2c 20 27 66 6f 72 74  >r': '<f2', 'fort<
0000040 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61 6c 73  >ran_order': Fals<
0000060 65 2c 20 27 73 68 61 70 65 27 3a 20 28 31 30 2c  >e, 'shape': (10,<
0000100 29 2c 20 7d 20 20 20 20 20 20 20 20 20 20 20 20  >), }            <
0000120 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
0000140 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
0000160 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 0a  >               .<
0000200 00 3c 00 40 00 42 00 44 00 45 00 46 00 47 00 48  >.<.@.B.D.E.F.G.H<
0000220 80 48 00 49                                      >.H.I<
0000224

Hlavička v tomto případě obsahuje mj. i informace o typu prvků „f2“ i o tvaru pole (v čitelném tvaru). Samotná data v tomto případě začínají na offsetu 128 (tedy 200 oktalově). Samotná délka dat je rovna dvaceti bajtům, protože každý prvek vektoru zabere dva bajty a prvků je uloženo deset.

Tento binární soubor s obsahem vektoru lze načíst velmi snadno, a to konkrétně funkcí numpy.load():

"""Přečtení obsahu vektoru ze standardního binárního souboru."""
 
import numpy as np
 
v = np.load("vector.npy")
print(v)
print(v.dtype)
Poznámka: poněkud předbíháme, ovšem již nyní je vhodné si říci, že dále popsaná knihovna github.com/sbinet/npyio datový formát float16 nepodporuje. Je tomu tak z toho důvodu, že tento formát není nativně podporován ani samotným programovacím jazykem Go.

9. Uložení a načtení matice do/ze standardního binárního souboru, opět s využitím knihovny Numpy

Naprosto stejným způsobem jako s vektory se v případě standardního binárního souboru NPY a knihovny Numpy pracuje s maticemi. Uložení matice je z pohledu programátora triviální operací:

"""Uložení obsahu matice do standardního binárního souboru."""
 
import numpy as np
 
# matice obsahující celočíselné 8bitové hodnoty (byte)
m = np.linspace(1, 12, 12, dtype="b").reshape(3, 4)
print(m)
 
np.save("matrix1.npy", m, allow_pickle=False)

Zpětné načtení matice můžeme realizovat takto:

"""Přečtení obsahu matice ze standardního binárního souboru."""
 
import numpy as np
 
m = np.load("matrix1.npy")
print(m)
print(m.dtype)

Přitom je vytvořen soubor nazvaný matrix1.npy, jehož vnitřní strukturu si můžeme prohlédnout nástrojem od:

$ od -Ax -t x1z -v matrix1.npy

S výsledkem:

000000 93 4e 55 4d 50 59 01 00 76 00 7b 27 64 65 73 63  >.NUMPY..v.{'desc<
000010 72 27 3a 20 27 7c 69 31 27 2c 20 27 66 6f 72 74  >r': '|i1', 'fort<
000020 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61 6c 73  >ran_order': Fals<
000030 65 2c 20 27 73 68 61 70 65 27 3a 20 28 33 2c 20  >e, 'shape': (3, <
000040 34 29 2c 20 7d 20 20 20 20 20 20 20 20 20 20 20  >4), }           <
000050 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
000060 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
000070 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 0a  >               .<
000080 01 02 03 04 05 06 07 08 09 0a 0b 0c              >............<
00008c
Poznámka: povšimněte si, že tvar je popsán řetězcem (3, 4). Samotné hodnoty pole jsou uloženy od offsetu 0×80, tedy ve výpisu na posledním řádku. Taktéž si povšimněte, že hodnoty typu „bajt“ neboli i1 nemají specifikováno pořadí bajtů (tedy ani little endian ani big endian).

Stejný příklad, ovšem s maticí obsahující prvky typu „float“:

"""Uložení obsahu matice do standardního binárního souboru."""
 
import numpy as np
 
m = np.linspace(1, 12, 12, dtype="f").reshape(3, 4)
print(m)
 
np.save("matrix2.npy", m, allow_pickle=False)

Zpětné načtení matice:

"""Přečtení obsahu matice ze standardního binárního souboru."""
 
import numpy as np
 
m = np.load("matrix2.npy")
print(m)
print(m.dtype)

Druhý binární soubor má pochopitelně odlišný obsah, neboť nyní je hodnota každého prvku uložena ve čtyřech bajtech:

$ od -Ax -t x1z -v matrix2.npy
 
000000 93 4e 55 4d 50 59 01 00 76 00 7b 27 64 65 73 63  >.NUMPY..v.{'desc<
000010 72 27 3a 20 27 3c 66 34 27 2c 20 27 66 6f 72 74  >r': '<f4', 'fort<
000020 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61 6c 73  >ran_order': Fals<
000030 65 2c 20 27 73 68 61 70 65 27 3a 20 28 33 2c 20  >e, 'shape': (3, <
000040 34 29 2c 20 7d 20 20 20 20 20 20 20 20 20 20 20  >4), }           <
000050 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
000060 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20  >                <
000070 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 0a  >               .<
000080 00 00 80 3f 00 00 00 40 00 00 40 40 00 00 80 40  >...?...@..@@...@<
000090 00 00 a0 40 00 00 c0 40 00 00 e0 40 00 00 00 41  >...@...@...@...A<
0000a0 00 00 10 41 00 00 20 41 00 00 30 41 00 00 40 41  >...A.. A..0A..@A<
0000b0
Poznámka: nyní je explicitně nastaveno pořadí bajtů „little endian“, a to řetězcem <f4.

10. Knihovna npyio pro práci s NPY soubory v programovacím jazyku Go

V případě programovacího jazyka Go existuje pro ukládání vektorů a matic do formátu NPY i pro zpětné načtení těchto datových struktur hned několik knihoven. První knihovnou, s níž se setkáme v dnešním článku, je knihovna nazvaná npyio. Její předností je fakt, že se s daty pracuje zcela idiomatickým způsobem. Mezi nevýhody lze naopak řadit velmi pomalé načítání (nikoli však ukládání) vektorů a matic z větších souborů (100MB a výše) a taktéž fakt, že není kompatibilní s balíčkem narray, s nímž jsme se ve stručnosti seznámili v tomto článku.

Ve starších verzích jazyka Go je instalace balíčku npyio snadná, protože postačuje použít příkaz go get v následujícím tvaru:

$ go get github.com/sbinet/npyio

U novějších verzí Go je nejprve nutné vytvořit nový projekt příkazem:

$ go mod init jméno-projektu

A následně ve zdrojovém kódu naimportovat knihovnu npyio:

package main
 
import (
        "github.com/sbinet/npyio"
)
...
...
...

Při pokusu o překlad takto upraveného projektu si překladač jazyka Go vyžádá instalaci balíčku npyio; ovšem tato instalace musí být spuštěna z adresáře, v němž je uložen projekt. Povšimněte si, že se v průběhu instalace kromě vlastního balíčku npyio mj. nainstaluje i celý balíček gonum, jímž jsme se na stránkách Roota taktéž zabývali v článcích Gophernotes: kombinace interaktivního prostředí Jupyteru s jazykem Go a Popis vybraných balíčků nabízených projektem Gonum:

go: downloading github.com/sbinet/npyio v0.7.0
go: downloading github.com/campoy/embedmd v1.0.0
go: downloading gonum.org/v1/gonum v0.9.3
go: downloading github.com/pmezard/go-difflib v1.0.0
go: added github.com/campoy/embedmd v1.0.0
go: added github.com/pmezard/go-difflib v1.0.0
go: added github.com/sbinet/npyio v0.7.0
go: added gonum.org/v1/gonum v0.9.3

Projektový soubor go.mod by nyní měl vypadat následovně (pochopitelně se může lišit jméno projektu a taktéž verze jazyka Go, která projekt vytvořila):

module write-npy1
 
go 1.20
 
require (
        github.com/campoy/embedmd v1.0.0 // indirect
        github.com/pmezard/go-difflib v1.0.0 // indirect
        github.com/sbinet/npyio v0.7.0 // indirect
        gonum.org/v1/gonum v0.9.3 // indirect
)

Zajímavé je zjistit, jak vypadá „dependency hell“, tedy obsah souboru go.sum. Většina tranzitivních závislostí je způsobena balíčkem gonum (a ukazuje se tak, že primární závislost npyio na gonum není zcela šťastná):

dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/sbinet/npyio v0.7.0 h1:KH8n5VrI1O2FeNAHwa0WmC1f9nGNtXNzQHBkyoU8tuE=
github.com/sbinet/npyio v0.7.0/go.mod h1:4jmxspVr/RFRPc6zSGR/8FP6nb9m7EpypUXrU/cf/nU=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

11. Uložení vektoru s prvky typu int8 do souboru typu NPY

Základní vlastnosti knihovny npyio si otestujeme na několika demonstračních příkladech. První příklad je skutečně základní – pokusíme se v něm totiž do souboru ve formátu NPY uložit vektor deseti hodnot typu int8, což jsou celá čísla v rozsahu od –128 do 127. Povšimněte si, že celý postup je (pokud vynecháme testy úspěšnosti jednotlivých operací) až triviálně jednoduchý:

  1. f := os.Create
  2. npyio.Write(f, vektor)
  3. f.Close()

Se všemi deklaracemi, zajištěním uzavření souboru a kontrolou chyb by mohl příklad vypadat následovně:

package main
 
import (
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Create("int8_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        m := []int8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        err = npyio.Write(f, m)
        if err != nil {
                log.Fatalf("error writing to file: %v\n", err)
        }
}

Po překladu a spuštění tohoto příkladu by měl vzniknout soubor vector.npy, který můžeme prozkoumat nástrojem od:

$ od -Ax -t x1z -v int_vector.npy

Výsledek bude vypadat následovně:

000000 93 4e 55 4d 50 59 02 00 42 00 00 00 7b 27 64 65  >.NUMPY..B...{'de<
000010 73 63 72 27 3a 20 27 7c 69 31 27 2c 20 27 66 6f  >scr': '|i1', 'fo<
000020 72 74 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61  >rtran_order': Fa<
000030 6c 73 65 2c 20 27 73 68 61 70 65 27 3a 20 28 31  >lse, 'shape': (1<
000040 30 2c 29 2c 20 7d 20 20 20 20 20 20 20 0a 00 01  >0,), }       ...<
000050 02 03 04 05 06 07 08 09                          >........<
000058
Poznámka: povšimněte si, že úvodní bajty společně s hlavičkou nejsou zarovnány na násobek šestnácti. Nejedná se přímo o chybu, ale o nedodržení doporučení o tom, jak má být formát NPY využit.

12. Uložení vektoru s deseti prvky typu int32

Prakticky stejným způsobem je možné realizovat uložení vektoru s deseti prvky typu int32, tj. 32bitových hodnot se znaménkem:

package main
 
import (
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Create("int_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        m := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        err = npyio.Write(f, m)
        if err != nil {
                log.Fatalf("error writing to file: %v\n", err)
        }
}

Povšimněte si, že nyní je korektně uveden jak typ prvků („i4“), tak i fakt, že bajty každého prvku jsou uloženy ve formátu little endian (znak „<“ před typem prvku). A opět není provedeno zarovnání hlavičky na násobky šestnácti bajtů:

$ od -Ax -t x1z -v int_vector.npy
 
000000 93 4e 55 4d 50 59 02 00 42 00 00 00 7b 27 64 65  >.NUMPY..B...{'de<
000010 73 63 72 27 3a 20 27 3c 69 34 27 2c 20 27 66 6f  >scr': '<i4', 'fo<
000020 72 74 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61  >rtran_order': Fa<
000030 6c 73 65 2c 20 27 73 68 61 70 65 27 3a 20 28 31  >lse, 'shape': (1<
000040 30 2c 29 2c 20 7d 20 20 20 20 20 20 20 0a 00 00  >0,), }       ...<
000050 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00  >................<
000060 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 00  >................<
000070 00 00 09 00 00 00                                >......<
000076

13. Uložení dvojrozměrné matice do souboru typu NPY

V mnoha úlohách se nepracuje s pouhými vektory, ale spíše s dvourozměrnými maticemi. I ty lze do souborů typu NPY pochopitelně uložit, ovšem při použití knihovny npyio pro jazyk Go je nutné pro konstrukci matice využít knihovnu gonum, kterou jsme se již na stránkách Roota zabývali. Příkladem může být vytvoření plné (husté) matice konstruktorem mat.NewDense, kterému se předají rozměry matice a taktéž hodnoty jednotlivých prvků matice. V knihovně gonum jsou prvky matice reprezentovány typem float64:

m := mat.NewDense(3, 4, []float64{
        1, 2, 3,
        4, 5, 6,
        7, 8, 9,
        10, 11, 12})

Takovou matici uložíme do NPY stejně, jako vektor:

f, err := os.Create("float_matrix.npy")
err = npyio.Write(f, m)
err := f.Close()

Úplný zdrojový kód obsahující všechny (nutné) kontroly možných chybových stavů může vypadat následovně:

package main
 
import (
        "log"
        "os"
 
        "github.com/sbinet/npyio"
        "gonum.org/v1/gonum/mat"
)
 
func main() {
        f, err := os.Create("float_matrix.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        m := mat.NewDense(3, 4, []float64{
                1, 2, 3,
                4, 5, 6,
                7, 8, 9,
                10, 11, 12})
 
        err = npyio.Write(f, m)
        if err != nil {
                log.Fatalf("error writing to file: %v\n", err)
        }
}

Opět se podíváme na to, jak vypadá vygenerovaný soubor s maticí:

$ od -Ax -t x1z -v float_matrix.npy

V souboru můžeme vidět jak definici typu prvků matice („f8“), tak i specifikaci tvaru (shape) matice. Hlavička je nyní korektně zarovnána na hodnotu dělitelnou šestnácti; za hlavičkou a výplní jsou již uloženy hodnoty jednotlivých prvků:

000000 93 4e 55 4d 50 59 02 00 44 00 00 00 7b 27 64 65  >.NUMPY..D...{'de<
000010 73 63 72 27 3a 20 27 3c 66 38 27 2c 20 27 66 6f  >scr': '<f8', 'fo<
000020 72 74 72 61 6e 5f 6f 72 64 65 72 27 3a 20 46 61  >rtran_order': Fa<
000030 6c 73 65 2c 20 27 73 68 61 70 65 27 3a 20 28 33  >lse, 'shape': (3<
000040 2c 20 34 29 2c 20 7d 20 20 20 20 20 20 20 20 0a  >, 4), }        .<
000050 00 00 00 00 00 00 f0 3f 00 00 00 00 00 00 00 40  >.......?.......@<
000060 00 00 00 00 00 00 08 40 00 00 00 00 00 00 10 40  >.......@.......@<
000070 00 00 00 00 00 00 14 40 00 00 00 00 00 00 18 40  >.......@.......@<
000080 00 00 00 00 00 00 1c 40 00 00 00 00 00 00 20 40  >.......@...... @<
000090 00 00 00 00 00 00 22 40 00 00 00 00 00 00 24 40  >......"@......$@<
0000a0 00 00 00 00 00 00 26 40 00 00 00 00 00 00 28 40  >......&@......(@<
0000b0

14. Načtení vektoru ze souboru typu NPY

Víme již, že uložení vektoru či matice do souboru typu NPY se provádí následující sekvencí operací (zjednodušeno – bez kontroly chyb atd.):

  1. f := os.Create
  2. npyio.Write(f, vektor či matice)
  3. f.Close()

Načtení vektoru ze souboru typu NPY je podobně jednoduché, pouze je nutné dopředu vytvořit řez (slice), do kterého budou prvky vektoru uloženy. Připomeňme si, že řez může v jazyce Go „růst“, což znamená, že nemusíme dopředu alokovat paměť. Navíc se řez do funkce npyio.Read musí předávat odkazem (protože se interně změní všechny tři jeho atributy – ukazatel na pole, délka řezu i jeho kapacita):

  1. f := os.Open(„soubor.npy“)
  2. var m []int8 nebo jiný typ
  3. npyio.Read(f, &m)
  4. f.Close()

Takto vypadá kód demonstračního příkladu, který načte soubor obsahující vektor prvků typu int8 neboli osmibitové hodnoty se znaménkem:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Open("int8_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        var m []int8
        err = npyio.Read(f, &m)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", m)
}

Výsledek získaný po spuštění tohoto příkladu:

loaded vector = [0 1 2 3 4 5 6 7 8 9]

15. Kontrola typů prvků při načítání

Pokusme se nyní o načtení souboru „int_vector.npy“, který obsahuje vektor prvků typu int32 do řezu s prvky typu int8. Otestujeme si tedy, jakým způsobem balíček npyio kontroluje datové typy:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Open("int_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        var m []int8
        err = npyio.Read(f, &m)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", m)
}

Pokus o spuštění tohoto příkladu skončí (podle očekávání) s chybou, protože se typy zkontrolují v době běhu aplikace (runtime):

2023/03/19 08:15:22 error reading from file: npy: types don't match

Oprava je v tomto případě jednoduchá – je nutné změnit typ řezu na:

var m []int32

Opravený příklad:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Open("int_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        var m []int32
        err = npyio.Read(f, &m)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", m)
}

Výsledek získaný po spuštění tohoto demonstračního příkladu je již zcela korektní:

loaded vector = [0 1 2 3 4 5 6 7 8 9]

16. Načítání matic

Prozatím jsme si ukázali, jakým způsobem se do souborů s formátem NPY ukládají vektory a matice. Vektory již také umíme načíst zpět do datového typu „řez“ (slice). Ovšem jakým způsobem se načítají matice? V prvním příkladu načteme obsah matice (a skutečně se jedná o matici – viz hlavičku souboru) do řezu, takže vlastně provedeme operaci, která se v některých jazycích nazývá flatten – data se sice načtou, ale ztratíme jejich původní strukturu, protože budou reprezentována jako jednorozměrný řez:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Open("float_matrix.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        var m []float64
        err = npyio.Read(f, &m)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", m)
}

Získaný výsledek bude v tomto případě vypadat takto:

loaded vector = [1 2 3 4 5 6 7 8 9 10 11 12]

Samozřejmě nám nic nebrání v načtení prvků matice do řezu a jejich následný explicitní převod na matici (z balíčku gonum). Tento postup lze realizovat následovně:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
        "gonum.org/v1/gonum/mat"
)
 
func main() {
        f, err := os.Open("float_matrix.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        var v []float64
        err = npyio.Read(f, &v)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", v)
 
        m := mat.NewDense(3, 4, v)
        fmt.Printf("converted matrix =\n%v\n", mat.Formatted(m))
}

S tímto výsledkem:

loaded vector = [1 2 3 4 5 6 7 8 9 10 11 12]
 
converted matrix =
⎡ 1   2   3   4⎤
⎢ 5   6   7   8⎥
⎣ 9  10  11  12⎦

Při konverzi lze zvolit i jiné rozměry matice, protože vlastně zcela ignorujeme původní hlavičku s metainformacemi i tvaru (shape) uloženého n-dimenzionálního pole a převod na matici provádíme v kódu explicitně. Jako pokus tedy prohodíme počet řádků a sloupců matice:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
        "gonum.org/v1/gonum/mat"
)
 
func main() {
        f, err := os.Open("float_matrix.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        var v []float64
        err = npyio.Read(f, &v)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", v)
 
        m := mat.NewDense(4, 3, v)
        fmt.Printf("converted matrix =\n%v\n", mat.Formatted(m))
}

Nyní bude výsledek vypadat odlišně:

loaded vector = [1 2 3 4 5 6 7 8 9 10 11 12]
 
converted matrix =
⎡ 1   2   3⎤
⎢ 4   5   6⎥
⎢ 7   8   9⎥
⎣10  11  12⎦

Nicméně ideální by bylo, aby se načetla přímo matice tak, jak byla do souboru s formátem NPY uložena. To je již nepatrně komplikovanější problém, který se řeší následovně – explicitním načtením hlavičky, alokací vektoru na základě informací získaných z hlavičky (což je ovšem poměrně ošklivé řešení) a načtením samotného bloku s daty:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
        "gonum.org/v1/gonum/mat"
)
 
func main() {
        f, err := os.Open("float_matrix.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        r, err := npyio.NewReader(f)
        if err != nil {
                log.Fatal(err)
        }
 
        fmt.Printf("npy-header: %v\n", r.Header)
        shape := r.Header.Descr.Shape
 
        v := make([]float64, shape[0]*shape[1])
 
        err = r.Read(&v)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector = %v\n", v)
 
        m := mat.NewDense(shape[0], shape[1], v)
        fmt.Printf("converted matrix =\n%v\n", mat.Formatted(m))
}

Nyní ve výpisu nalezneme jak informace získané z hlavičky souboru NPY, tak i korektně vytvořenou matici:

npy-header: Header{Major:2, Minor:0, Descr:{Type:<f8, Fortran:false, Shape:[3 4]}}
 
loaded vector = [1 2 3 4 5 6 7 8 9 10 11 12]
 
converted matrix =
⎡ 1   2   3   4⎤
⎢ 5   6   7   8⎥
⎣ 9  10  11  12⎦
Poznámka: jak je patrné, není možné tímto způsobem načítat vícerozměrná pole, což je – vedle problému zmíněného o odstavec níže – největší nedostatek knihovny npyio.

17. Uložení a zpětné načtení rozsáhlejších souborů s velikostí přesahujících gigabyte

Ještě si vyzkoušejme, zda a jak dokáže knihovna npyio pracovat s poněkud rozsáhlejšími soubory.

Pro vytvoření vektoru, jehož počet prvků dosahuje hodnoty 233, slouží následující demonstrační příklad. Ten po svém spuštění vytvoří soubor s příslušnými daty za několik sekund až několik desítek sekund, takže v tomto případě vše pracuje zcela podle očekávání:

package main
 
import (
        "fmt"
        "log"
        "math"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Create("large_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        v := make([]byte, math.MaxUint32*2)
        fmt.Println(len(v))
 
        err = npyio.Write(f, v)
        if err != nil {
                log.Fatalf("error writing to file: %v\n", err)
        }
}

Soubor získaný předchozím demonstračním příkladem se nyní pokusíme načíst zpět do paměti, a to konkrétně touto jednoduchou aplikací:

package main
 
import (
        "fmt"
        "log"
        "os"
 
        "github.com/sbinet/npyio"
)
 
func main() {
        f, err := os.Open("large_vector.npy")
        if err != nil {
                log.Fatal(err)
        }
        defer func() {
                err := f.Close()
                if err != nil {
                        log.Fatalf("error closing file: %v\n", err)
                }
        }()
 
        r, err := npyio.NewReader(f)
        if err != nil {
                log.Fatal(err)
        }
 
        fmt.Printf("npy-header: %v\n", r.Header)
        shape := r.Header.Descr.Shape
 
        v := make([]byte, shape[0])
 
        err = r.Read(&v)
        if err != nil {
                log.Fatalf("error reading from file: %v\n", err)
        }
 
        fmt.Printf("loaded vector size = %v\n", len(v))
}

Výše uvedený příklad je sice plně funkční, ovšem s jedním významným „ale“ – jeho dokončení se dočkáte až po několika desítkách minut! Přitom bude jedno procesorové jádro neustále vytíženo na 100%. Jedná se o velmi významný nedostatek řešení postaveného na balíčku npyio, protože soubory o velikosti od 100MB do 1GB (i výše) jsou v této oblasti informatiky zcela běžné.

18. Závěrečné zhodnocení

Práce s knihovnou npyio je pro programátora znalého programovacího jazyka Go poměrně snadná, a to z toho důvodu, že použité operace čtení a zápisu jsou realizovány (pro Go) idiomaticky. Ukládání a načítání vektorů je triviální, protože se zde využívá standardní datový typ jazyka Go, konkrétně řez (slice). Pro ukládání dvojdimenzionálních polí jsou využity matice z knihovny Gonum, což ovšem nemusí každému vyhovovat (osobně bych například byl raději za podporu narray). Ovšem největší problém spočívá ve velmi pomalém načítání (nikoli však ukládání) polí, jejichž velikost přesahuje zhruba 100 MB. V praxi jsou taková pole zcela běžná, typicky ještě o řád či o dva řády větší, takže v tomto případě stojí za uváženou, zda tuto knihovnu vůbec použít. Většinou je lepší výpočet realizovat raději přímo v Pythonu+Numpy, zejména ve chvíli, kdy lze veškeré operace popsat operacemi nad n-dimenzionálními poli Numpy; v tomto případě se totiž výpočty realizují v nativním kódu Numpy a nikoli v pomalém Pythonu (samozřejmě lze uvažovat o použití jazyka Julia, což lze v této oblasti IT obecně jen doporučit).

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů naprogramovaných v jazyku Go byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad/soubor Stručný popis Cesta
1 write-npy1 uložení vektoru s deseti prvky typu int8 https://github.com/tisnik/go-root/blob/master/article_A6/write-npy1/
2 write-npy2 uložení vektoru s deseti prvky typu int32 https://github.com/tisnik/go-root/blob/master/article_A6/write-npy2/
3 write-npy3 uložení matice s 3×4 prvky typu float32 https://github.com/tisnik/go-root/blob/master/article_A6/write-npy3/
4 write-npy4 uložení vektoru s počtem prvků přesahujícím 232 https://github.com/tisnik/go-root/blob/master/article_A6/write-npy4/
       
5 read-npy1 načtení vektoru s deseti prvky typu int8 https://github.com/tisnik/go-root/blob/master/article_A6/read-npy1
6 read-npy2 načtení vektoru s deseti prvky typu int32 (špatné použití typů) https://github.com/tisnik/go-root/blob/master/article_A6/read-npy2
7 read-npy3 načtení vektoru s deseti prvky typu int32 (korektní použití typů) https://github.com/tisnik/go-root/blob/master/article_A6/read-npy3
8 read-npy4 načtení matice s 3×4 prvky typu float32 do vektoru https://github.com/tisnik/go-root/blob/master/article_A6/read-npy4
9 read-npy5 načtení matice s 3×4 prvky typu float32 do matice se specifikací jejího tvaru https://github.com/tisnik/go-root/blob/master/article_A6/read-npy5
10 read-npy6 načtení matice s 3×4 prvky typu float32 do matice se specifikací jejího tvaru https://github.com/tisnik/go-root/blob/master/article_A6/read-npy6
11 read-npy7 alternativní způsob načtení vektoru nebo matice ze souboru typu NPY https://github.com/tisnik/go-root/blob/master/article_A6/read-npy7
12 read-npy8 načtení vektoru s počtem prvků přesahujícím 232 https://github.com/tisnik/go-root/blob/master/article_A6/read-npy8

Pro úplnost si ještě uveďme odkazy na příklady naprogramované v Pythonu, které pracovaly s formátem NPY:

# Demonstrační příklad Stručný popis příkladu Cesta
1 vector_save.py uložení obsahu vektoru do standardního binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/numpy/vector_save.py
2 vector_load.py načtení obsahu vektoru ze standardního binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/numpy/vector_load.py
3 matrix_save1.py uložení matice s prvky typu „byte“ do standardního binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/numpy/ma­trix_save1.py
4 matrix_save2.py uložení matice s prvky typu „float“ do standardního binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/numpy/ma­trix_save2.py
5 matrix_load1.py načtení matice s prvky typu „byte“ ze standardního binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/numpy/ma­trix_load1.py
6 matrix_load2.py načtení matice s prvky typu „float“ ze standardního binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/numpy/ma­trix_load2.py

20. Odkazy na Internetu

  1. Git repositář balíčku gonpy
    https://github.com/kshedden/gonpy
  2. Git repositář balíčku npyio
    https://github.com/sbinet/npyio
  3. NEP 1 – A simple file format for NumPy arrays
    https://numpy.org/neps/nep-0001-npy-format.html
  4. Operace s daty uloženými v binárních souborech v knihovnách NumPy a Pandas
    https://www.root.cz/clanky/operace-s-daty-ulozenymi-v-binarnich-souborech-v-knihovnach-numpy-a-pandas/
  5. Operace s daty uloženými v binárních souborech v knihovnách NumPy a Pandas (dokončení)
    https://www.root.cz/clanky/operace-s-daty-ulozenymi-v-binarnich-souborech-v-knihovnach-numpy-a-pandas-dokonceni/
  6. .NPY File Extension
    https://fileinfo.com/extension/npy
  7. What is .npy files and why you should use them…
    https://towardsdatascience.com/what-is-npy-files-and-why-you-should-use-them-603373c78883
  8. A Simple File Format for NumPy Arrays
    https://docs.scipy.org/doc/numpy-1.14.2/neps/npy-format.html
  9. Hierarchical Data Format
    https://en.wikipedia.org/wi­ki/Hierarchical_Data_Format
  10. HDF Group
    https://www.hdfgroup.org/
  11. GRIB (Wikipedia)
    https://en.wikipedia.org/wiki/GRIB
  12. FITS (Wikipedia)
    https://en.wikipedia.org/wiki/FITS
  13. A Primer on the FITS Data Format
    https://fits.gsfc.nasa.gov/fit­s_primer.html
  14. The FITS Support Office
    https://fits.gsfc.nasa.gov/
  15. FITS File Handling (astropy.io.fits)
    https://docs.astropy.org/en/sta­ble/io/fits/index.html
  16. FITS reader pro jazyk Go
    https://github.com/siravan/fits
  17. FITS Standard Document
    https://fits.gsfc.nasa.gov/fit­s_standard.html
  18. Package narray
    https://github.com/akualab/narray
  19. Dokumentace k balíčku narray/na32
    https://pkg.go.dev/github­.com/akualab/narray/na32
  20. Dokumentace k balíčku narray/na64
    https://pkg.go.dev/github­.com/akualab/narray/na64
  21. The Gonum Numerical Computing Package
    https://www.gonum.org/pos­t/introtogonum/
  22. Gonum Numerical Packages
    https://www.gonum.org/
  23. Accelerating data processing in Go with SIMD instructions
    https://docs.google.com/pre­sentation/d/1MYg8PyhEf0oIv­Z9YU2panNkVXsKt5UQBl_vGEa­CeB1k/htmlpresent#!
  24. Array Programming
    https://en.wikipedia.org/wi­ki/Array_programming
  25. Discovering Array Languages
    http://archive.vector.org­.uk/art10008110
  26. no stinking loops – Kalothi
    http://www.nsl.com/
  27. Vector (obsahuje odkazy na články, knihy a blogy o programovacích jazycích APL, J a K)
    http://www.vector.org.uk/
  28. APL Wiki
    https://aplwiki.com/wiki/
  29. The Array Cast
    https://www.arraycast.com/e­pisodes/episode-03-what-is-an-array
  30. EnthusiastiCon 2019 – An Introduction to APL
    https://www.youtube.com/wat­ch?v=UltnvW83_CQ
  31. Dyalog
    https://www.dyalog.com/
  32. Try APL!
    https://tryapl.org/
  33. PyNIO
    https://www.pyngl.ucar.edu/Nio.shtml
  34. A GUIDE TO THE CODE FORM FM 92-IX Ext. GRIB Edition 1
    https://old.wmo.int/extra­net/pages/prog/www/WMOCodes/Gu­ides/GRIB/GRIB1-Contents.html
  35. What is HDF5?
    https://support.hdfgroup.or­g/HDF5/whatishdf5.html
  36. Using The Right File Format For Storing Data
    https://www.analyticsvidhy­a.com/blog/2021/09/using-the-right-file-format-for-storing-data/
  37. Simple Data Format
    https://en.wikipedia.org/wi­ki/Simple_Data_Format
  38. Simple Data Format Manifesto
    http://solarmuri.ssl.berke­ley.edu/~fisher/public/sof­tware/SDF/SDF_MANIFESTO.txt
  39. Sample FITS Files
    https://fits.gsfc.nasa.gov/fit­s_samples.html
  40. Gophernotes: kombinace interaktivního prostředí Jupyteru s jazykem Go
    https://www.root.cz/clanky/gophernotes-kombinace-interaktivniho-prostredi-jupyteru-s-jazykem-go/
  41. Popis vybraných balíčků nabízených projektem Gonum
    https://www.root.cz/clanky/popis-vybranych-balicku-nabizenych-projektem-gonum/

Byl pro vás článek přínosný?