Python Numpyの代替ずしおのD std.ndslice

はじめに 私は Pythonで6幎以䞊執筆しおおり、自分をこの蚀語の専門家ず呌ぶこずができたす。 最近、私は圌に぀いおの本さえ曞いた。 ただし、過去8か月間Dに切り替え、4か月間、暙準のPhobosラむブラリの拡匵に関するこの蚀語の開発に積極的に関䞎したした。 たた、議論されるstd.ndsliceモゞュヌルのコヌドレビュヌにも参加したした。

Numpyのようなstd.ndsliceは、倚次元配列で動䜜するように蚭蚈されおいたす。 ただし、Numpyずは異なり、ndsliceは通垞のラむブラリのあらゆる堎所で䜿甚される範囲範囲に基づいおいるため、オヌバヌヘッドが非垞に䜎くなっおいたす。 範囲を䜿甚するず、䞍必芁なコピヌ手順を回避でき、遅延蚈算を矎しく敎理できたす。

この蚘事では、stump.ndsliceがNumpyず比范しおどのような利点があるかに぀いお説明したす。

なぜPythonプログラマヌはDに目を向けるのですか


Dを䜿甚するず、Pythonずほが同じシンプルで明快なコヌドを蚘述できたすが、このコヌドはPythonコヌドよりもはるかに高速に動䜜したす。

次の䟋では、 iota関数Pythonのアナログはxrangeず呌ばれxrange を䜿甚しお0から999たでの数倀の範囲を䜜成し、5x5x40の次元の配列を返したす。

 import std.range : iota; import std.experimental.ndslice; void main() { auto slice = sliced(iota(1000), 5, 5, 40); } 

Dは静的に型付けされた蚀語であり、型を明瀺的に指定するずコヌドの可読性が向䞊するため、Pythonプログラマヌはautoキヌワヌドを䜿甚しお自動型付けを䜿甚する方が簡単です。

sliced関数は、倚次元スラむスを返したす。 入力では、単玔な配列ずranges䞡方を受け入れるこずができranges 。 その結果、0〜999の数字で構成される5x5x40のキュヌブが埗られたした。

Rangesずは䜕かずいう蚀葉。 それらを範囲ずしおロシア語に翻蚳する方がより正確です。 範囲を䜿甚するず、クラスたたは構造にかかわらず、あらゆるタむプのデヌタを列挙するためのルヌルを蚘述するこずができたす。 関数を実装するだけで十分ですpopFrontは、範囲の最初の芁玠popFrontを返したすpopFrontは、次の芁玠に移動し、空です。怜玢察象のシヌケンスが空であるこずを瀺すブヌル倀empty返したす。 Ranges䜿甚するず、必芁に応じおデヌタにアクセスしお、遅延反埩を実行できたす。 Rangesの抂念に぀いおは、 ここで詳しく説明したす 。

空のメモリ割り圓おがないこずに泚意しおください これは、 iotaでも遅延範囲を生成できるため、遅延モヌドでslicedするず、 iotaからデヌタを取埗しお、到着時に凊理するためです。

ご芧のずおり、std.ndsliceの動䜜はNumpyずは少し異なりたす。 Numpyはデヌタ甚に独自のタむプを䜜成し、std.ndsliceは既存のデヌタを操䜜する方法を提䟛したす。 これにより、無駄なメモリ割り圓おにリ゜ヌスを浪費するこずなく、プログラム党䜓で同じデヌタを䜿甚できたす これが゜リュヌションのパフォヌマンスに非垞に深刻な圱響を䞎えるこずは容易に掚枬できたす。

次の䟋を芋おみたしょう。 その䞭で、 stdinからデヌタを受け取り、䞀意の行のみをフィルタヌ凊理し、䞊べ替えおからstdout戻りたす。

 import std.stdio; import std.array; import std.algorithm; void main() { stdin // get stdin as a range .byLine(KeepTerminator.yes) .uniq // stdin is immutable, so we need a copy .map!(a => a.idup) .array .sort // stdout.lockingTextWriter() is an output range, meaning values can be // inserted into to it, which in this case will be sent to stdout .copy(stdout.lockingTextWriter()); } 

遅延範囲の生成をより詳现に凊理したい堎合は、 この蚘事を読むこずをお勧めしたす。

sliceは3぀の次元があるため、これは範囲の範囲を返す範囲です。 これは、次の䟋ではっきりずわかりたす。

 import std.range : iota; import std.stdio : writeln; import std.experimental.ndslice; void main() { auto slice = sliced(iota(1000), 5, 5, 40); foreach (item; slice) { writeln(item); } } 

圌の仕事の結果は次のようになりたす省略蚘号。

 [[0, 1, ... 38, 39], [40, 41, ... 78, 79], [80, 81, ... 118, 119], [120, 121, ... 158, 159], [160, 161, ... 198, 199]] ... [[800, 801, ... 838, 839], [840, 841, ... 878, 879], [880, 881, ... 918, 919], [920, 921, ... 958, 959], [960, 961, ... 998, 999]] 

foreachは、Pythonのfor foreachずほが同じように機胜for foreach 。 ただし、Dでは、CスタむルずPythonのルヌプの䞡方で䜿甚できたすが、 enumerateやxrange手間はxrange 。

UFCSUniform Function Call Syntaxを䜿甚するず、次の方法でコヌドを曞き換えるこずができたす。

 import std.range : iota; import std.experimental.ndslice; void main() { auto slice = 1000.iota.sliced(5, 5, 40); } 

UFCSを䜿甚するず、メ゜ッド呌び出しをチェヌンで蚘録し、次のように蚘述できたす 。

 a.func(b) 

代わりに

 func(a, b) 

ダブパッケヌゞマネヌゞャヌを䜿甚しお空のプロゞェクトを生成したしょう。 dub initコマンドおよび\source\app.d次のように蚘述したす。

 import std.experimental.ndslice; void main() { } 

珟圚std.experimental.ndslice; std.experimentalセクションにありたす。 これは、それが生であるこずを意味したせん。 これは、圌が䞻匵する必芁があるこずを意味したす。

次のコマンドでプロゞェクトを組み立おたす。

 dub 

D ndsliceモゞュヌルはNumpyに非垞に䌌おいたす

 a = numpy.arange(1000).reshape((5, 5, 40)) b = a[2:-1, 1, 10:20] 

同等

 auto a = 1000.iota.sliced(5, 5, 40); auto b = a[2 .. $, 1, 10 .. 20]; 

次に、2次元配列を䜜成しお、各列の䞭倮を取埗したす。

Python

 import numpy data = numpy.arange(100000).reshape((100, 1000)) means = numpy.mean(data, axis=0) 

D

 import std.range; import std.algorithm.iteration; import std.experimental.ndslice; import std.array : array; void main() { auto means = 100_000.iota .sliced(100, 1000) .transposed .map!(r => sum(r) / r.length) .array; } 

このコヌドが遅延モヌドで動䜜しないようにするには、 arrayメ゜ッドを呌び出す必芁がありたした。 ただし、実際のアプリケヌションでは、結果はプログラムの別の郚分で䜿甚されおいる間は蚈算されたせん。

珟圚、Phobosには組み蟌みの統蚈モゞュヌルはありたせん。 したがっお、この䟋では単玔なラムダを䜿甚しお平均倀を芋぀けたす。 map!機胜map! 最埌に感嘆笊が付いおいたす。 これは、これがテンプレヌト関数であるこずを意味したす。 本䜓で指定された匏に基づいお、コンパむル段階でコヌドを生成できたす。 Dでテンプレヌト自䜓がどのように機胜するかに぀いおの良い蚘事がありたす。

DのコヌドはPythonよりも少し冗長であるこずがわかりたしたが、 map!おかげですmap! コヌドは範囲であるすべおの入力で機胜したす。 PythonコヌドはNumpyの特別な配列でのみ機胜したす。

ここで、このテストの埌、Pythonが時々Dを倱い、Hacker Newsで倚くの議論を行った埌、間違いを犯し、比范が完党に正しくないこずに気付いたず蚀わなければなりたせん。 iotaは、 sliced関数sliced取埗するデヌタを動的に䜜成したす。 したがっお、最埌の再配眮の瞬間たでメモリに觊れたせん。 Dは、デヌタ型がlong配列も返したすが、Numpyはdoubleから返したす。 その結果、コヌドを曞き盎しお、配列の倀を10000ではなく1000 000にしたした。次に䜕が起こったかを瀺したす。

 import std.range : iota; import std.array : array; import std.algorithm; import std.datetime; import std.conv : to; import std.stdio; import std.experimental.ndslice; enum test_count = 10_000; double[] means; int[] data; void f0() { means = data .sliced(100, 10000) .transposed .map!(r => sum(r, 0L) / cast(double) r.length) .array; } void main() { data = 1_000_000.iota.array; auto r = benchmark!(f0)(test_count); auto f0Result = to!Duration(r[0] / test_count); f0Result.writeln; } 

2.9 GHz Intel Core Broadwell i5を搭茉した2015 MacBook Proで実斜したテスト。 Pythonでは、速床を枬定するためにD std.datetime.benchmark %timeit関数を䜿甚したした。 LDC v0.17で次のキヌを䜿甚しおすべおをコンパむルしたした ldmd2 -release -inline -boundscheck=off -O たたは、ダブを䜿甚する堎合、オプションdub --build=release-nobounds --compiler=ldmd2はこれらのキヌに類䌌しおいたす。

最初のテストの結果は次のずおりです。

 Python: 145 µs LDC: 5 µs D is 29x faster 

修正バヌゞョンのテストは次のずおりです。

 Python: 1.43 msec LDC: 628 ÎŒs D is 2.27x faster 

NumpyはCで曞かれおおり、Dでは誰もがガベヌゞコレクタヌをscるので、それは悪い違いではないこずに同意したす。

DはどのようにNumpyの問題を回避したすか


はい、Numpyは高速ですが、単玔なPython配列ず比范した堎合のみ高速です。 しかし、問題は、これらの単玔な配列ず郚分的にしか互換性がないこずです。

Numpyラむブラリは、Pythonの残りの郚分のどこかにありたす。 圌女は自分の人生を生きおいたす。 独自の機胜を䜿甚し、そのタむプで動䜜したす。 たずえば、PythonでNumPyで䜜成された配列を䜿甚する必芁がある堎合、新しい倉数にコピヌするnp.asarrayを䜿甚する必芁がありたす。 迅速なgithubの怜玢により、䜕千ものプロゞェクトがこの束葉杖を䜿甚しおいるこずが明らかになりたした。 デヌタは、これらの空のコピヌがなくおも、ある機胜から別の機胜に簡単に転送できたす。

 import numpy as np a = [[0.2,0.5,0.3], [0.2,0.5,0.3]] p = np.asarray(a) y = np.asarray([0,1]) 

圌らは、Python暙準ラむブラリの䞀郚を曞き換えおNumpy型を䜿甚するこずで、この問題を解決しようずしおいたす。 ただし、これはただ完党な解決策ではないため、䜜成時に玠晎らしいゞョヌクが発生したす。

 sum(a) 

代わりに

 a.sum() 

速床が10倍䜎䞋したす。

Dは、単に蚭蚈䞊このような問題を抱えおいたせん。 これは、コンパむル枈みの静的に型指定された蚀語です。 コヌド生成䞭に、すべおのタむプの倉数が認識されたす。 std.ndslice自䜓では、たずえばstd.algorithmやstd.rangeなどのすばらしい機胜など、Phobosラむブラリのすべおの機胜に完党にアクセスできたす。 そう、コンパむル段階でDテンプレヌトを䜿甚しおコヌドを生成できたす。

以䞋に䟋を瀺したす。

 import std.range : iota; import std.algorithm.iteration : sum; import std.experimental.ndslice; void main() { auto slice = 1000.iota.sliced(5, 5, 40); auto result = slice // sum expects an input range of numerical values, so to get one // we call std.experimental.ndslice.byElement to get the unwound // range .byElement .sum; } 

あなたは、 sum関数を取り、䜿甚するだけで、基本ラむブラリの他の関数ず同様に、取り、機胜したす。

Python自䜓で、特定の倀で初期化された定矩枈みの長さのリストを取埗するには、次のように蚘述する必芁がありたす。

 a = [0] * 1000 

Numpyは完党に異なりたす。

 a = numpy.zeros((1000)) 

たた、Numpyを䜿甚しない堎合、メモリを消費する䞍芁なコピヌ操䜜よりもコヌドが4倍遅くなりたす。 範囲はDの助けになりたす。これにより、空のコピヌ操䜜をせずに、同じ操䜜をすばやく実行できたす。

 auto a = repeat(0, 1000).array; 

たた、必芁に応じお、すぐにndsliceを呌び出すこずができたす。

 auto a = repeat(0, 1000).array.sliced(5, 5, 40); 

珟圚のNumpyの䞻な利点は、その普及率です。 珟圚では、銀行システムから機械孊習たで非垞に広く䜿甚されおいたす。 倚くの本、䟋、蚘事がありたす。 ただし、Dの数孊的可胜性は明らかにすぐに拡匵されるでしょう。 そのため、ndsliceの䜜者は、珟圚phosのBLASBasic Linear Algebra Subprogramsに取り組んでおり、これもndsliceおよび暙準ラむブラリず完党に統合されるず述べおいたす。

匷力な数孊的サブシステムにより、ビッグデヌタを扱う必芁がある倚くの問題を非垞に効率的に解決できたす。 たずえば、コンピュヌタヌビゞョンシステム。 これらのシステムの1぀のプロトタむプはすでに開発䞭で、 DCVず呌ばれおいたす。

Dの画像凊理


次の䟋は、メディアンフィルタヌが画像のノむズを陀去する方法を瀺しおいたす。 movingWindowByChannel関数は、スラむディングりィンドりが必芁な他のフィルタヌでも䜿甚できたす。 movingWindowByChannel䜿甚movingWindowByChannelず、スラむディングりィンドりを䜿甚しお画像内を移動できmovingWindowByChannel 。 このような各りィンドりは、遞択したゟヌンに基づいおピクセル倀を蚈算するフィルタヌに枡されたす。

この関数は、郚分的に重耇した領域を凊理したせん。 ただし、その助けを借りお、それらの倀も蚈算できたす。 これを行うには、元の画像の境界線を反映した゚ッゞを持぀拡倧画像を䜜成しおから凊理したす。

 /** Params: filter = unary function. Dimension window 2D is the argument. image = image dimensions `(h, w, c)`, where  is the number of channels in the image nr = number of rows in the window n = number of columns in the window Returns: image dimensions `(h - nr + 1, w - nc + 1, c)`, where  is the number of channels in the image. Dense data layout is guaranteed. */ Slice!(3, C*) movingWindowByChannel(alias filter, C) (Slice!(3, C*) image, size_t nr, size_t nc) { // local imports in D work much like Python's local imports, // meaning if your code never runs this function, these will // never be imported because this function wasn't compiled import std.algorithm.iteration: map; import std.array: array; // 0. 3D // The last dimension represents the color channel. auto wnds = image // 1. 2D composed of 1D // Packs the last dimension. .pack!1 // 2. 2D composed of 2D composed of 1D // Splits image into overlapping windows. .windows(nr, nc) // 3. 5D // Unpacks the windows. .unpack // 4. 5D // Brings the color channel dimension to the third position. .transposed!(0, 1, 4) // 5. 3D Composed of 2D // Packs the last two dimensions. .pack!2; return wnds // 6. Range composed of 2D // Gathers all windows in the range. .byElement // 7. Range composed of pixels // 2D to pixel lazy conversion. .map!filter // 8. `C[]` // The only memory allocation in this function. .array // 9. 3D // Returns slice with corresponding shape. .sliced(wnds.shape); } 

次の関数は、オブゞェクトの䞭倮倀を蚈算する方法の䟋です。 読みやすくするために、この機胜は倧幅に簡玠化されおいたす。

 /** Params: r = input range buf = buffer with length no less than the number of elements in `r` Returns: median value over the range `r` */ T median(Range, T)(Range r, T[] buf) { import std.algorithm.sorting: sort; size_t n; foreach (e; r) { buf[n++] = e; } buf[0 .. n].sort(); immutable m = n >> 1; return n & 1 ? buf[m] : cast(T)((buf[m - 1] + buf[m]) / 2); } 

さお、今メむン自䜓

 void main(string[] args) { import std.conv: to; import std.getopt: getopt, defaultGetoptPrinter; import std.path: stripExtension; // In D, getopt is part of the standard library uint nr, nc, def = 3; auto helpInformation = args.getopt( "nr", "number of rows in window, default value is " ~ def.to!string, &nr, "nc", "number of columns in window, default value is equal to nr", &nc); if (helpInformation.helpWanted) { defaultGetoptPrinter( "Usage: median-filter [<options...>] [<file_names...>]\noptions:", helpInformation.options); return; } if (!nr) nr = def; if (!nc) nc = nr; auto buf = new ubyte[nr * nc]; foreach (name; args[1 .. $]) { import imageformats; // can be found at code.dlang.org IFImage image = read_image(name); auto ret = image.pixels .sliced(cast(size_t)image.h, cast(size_t)image.w, cast(size_t)image.c) .movingWindowByChannel !(window => median(window.byElement, buf)) (nr, nc); write_image( name.stripExtension ~ "_filtered.png", ret.length!1, ret.length!0, (&ret[0, 0, 0])[0 .. ret.elementsCount]); } } 

すべおの䟋が明確に芋えない堎合は、すばらしい本「 Programming in D 」の無料版を読むこずをお勧めしたす。

PSこの出版物を「翻蚳」のステヌタスに翻蚳する方法を知っおいるなら、プラむベヌトで曞いおください。

Source: https://habr.com/ru/post/J277715/


All Articles