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í
17. Uložení a zpětné načtení rozsáhlejších souborů s velikostí přesahujících gigabyte
19. Repositář s demonstračními příklady
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/fits_samples.html. Hlavička souboru https://fits.gsfc.nasa.gov/samples/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
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).
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/reference/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.
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í:
- 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“.
- Následuje bajt obsahující hlavní (major) číslo verze, v našem případě 1.
- Další bajt obsahuje vedlejší (minor) číslo verze, konkrétně 0.
- 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ů.
- 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.
- 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).
- 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)
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
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
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ý:
- f := os.Create
- npyio.Write(f, vektor)
- 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
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.):
- f := os.Create
- npyio.Write(f, vektor či matice)
- 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):
- f := os.Open(„soubor.npy“)
- var m []int8 nebo jiný typ
- npyio.Read(f, &m)
- 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⎦
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/matrix_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/matrix_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/matrix_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/matrix_load2.py |
20. Odkazy na Internetu
- Git repositář balíčku gonpy
https://github.com/kshedden/gonpy - Git repositář balíčku npyio
https://github.com/sbinet/npyio - NEP 1 – A simple file format for NumPy arrays
https://numpy.org/neps/nep-0001-npy-format.html - 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/ - 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/ - .NPY File Extension
https://fileinfo.com/extension/npy - What is .npy files and why you should use them…
https://towardsdatascience.com/what-is-npy-files-and-why-you-should-use-them-603373c78883 - A Simple File Format for NumPy Arrays
https://docs.scipy.org/doc/numpy-1.14.2/neps/npy-format.html - Hierarchical Data Format
https://en.wikipedia.org/wiki/Hierarchical_Data_Format - HDF Group
https://www.hdfgroup.org/ - GRIB (Wikipedia)
https://en.wikipedia.org/wiki/GRIB - FITS (Wikipedia)
https://en.wikipedia.org/wiki/FITS - A Primer on the FITS Data Format
https://fits.gsfc.nasa.gov/fits_primer.html - The FITS Support Office
https://fits.gsfc.nasa.gov/ - FITS File Handling (astropy.io.fits)
https://docs.astropy.org/en/stable/io/fits/index.html - FITS reader pro jazyk Go
https://github.com/siravan/fits - FITS Standard Document
https://fits.gsfc.nasa.gov/fits_standard.html - Package narray
https://github.com/akualab/narray - Dokumentace k balíčku narray/na32
https://pkg.go.dev/github.com/akualab/narray/na32 - Dokumentace k balíčku narray/na64
https://pkg.go.dev/github.com/akualab/narray/na64 - The Gonum Numerical Computing Package
https://www.gonum.org/post/introtogonum/ - Gonum Numerical Packages
https://www.gonum.org/ - Accelerating data processing in Go with SIMD instructions
https://docs.google.com/presentation/d/1MYg8PyhEf0oIvZ9YU2panNkVXsKt5UQBl_vGEaCeB1k/htmlpresent#! - Array Programming
https://en.wikipedia.org/wiki/Array_programming - Discovering Array Languages
http://archive.vector.org.uk/art10008110 - no stinking loops – Kalothi
http://www.nsl.com/ - Vector (obsahuje odkazy na články, knihy a blogy o programovacích jazycích APL, J a K)
http://www.vector.org.uk/ - APL Wiki
https://aplwiki.com/wiki/ - The Array Cast
https://www.arraycast.com/episodes/episode-03-what-is-an-array - EnthusiastiCon 2019 – An Introduction to APL
https://www.youtube.com/watch?v=UltnvW83_CQ - Dyalog
https://www.dyalog.com/ - Try APL!
https://tryapl.org/ - PyNIO
https://www.pyngl.ucar.edu/Nio.shtml - A GUIDE TO THE CODE FORM FM 92-IX Ext. GRIB Edition 1
https://old.wmo.int/extranet/pages/prog/www/WMOCodes/Guides/GRIB/GRIB1-Contents.html - What is HDF5?
https://support.hdfgroup.org/HDF5/whatishdf5.html - Using The Right File Format For Storing Data
https://www.analyticsvidhya.com/blog/2021/09/using-the-right-file-format-for-storing-data/ - Simple Data Format
https://en.wikipedia.org/wiki/Simple_Data_Format - Simple Data Format Manifesto
http://solarmuri.ssl.berkeley.edu/~fisher/public/software/SDF/SDF_MANIFESTO.txt - Sample FITS Files
https://fits.gsfc.nasa.gov/fits_samples.html - Gophernotes: kombinace interaktivního prostředí Jupyteru s jazykem Go
https://www.root.cz/clanky/gophernotes-kombinace-interaktivniho-prostredi-jupyteru-s-jazykem-go/ - Popis vybraných balíčků nabízených projektem Gonum
https://www.root.cz/clanky/popis-vybranych-balicku-nabizenych-projektem-gonum/