Skip to content

Instantly share code, notes, and snippets.

@genkuroki
Last active July 12, 2020 01:29
Show Gist options
  • Save genkuroki/969c4ac8f518e343f44bd6e8d771a94a to your computer and use it in GitHub Desktop.
Save genkuroki/969c4ac8f518e343f44bd6e8d771a94a to your computer and use it in GitHub Desktop.
追加メモリ消費がない演算
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "# メモリ割当がない演算\n\n黒木玄\n\n2020-07-05, 2020-07-12"
},
{
"metadata": {
"toc": true
},
"cell_type": "markdown",
"source": "<h1>目次<span class=\"tocSkip\"></span></h1>\n<div class=\"toc\"><ul class=\"toc-item\"><li><span><a href=\"#in-place計算ではメモリ割当は生じない\" data-toc-modified-id=\"in-place計算ではメモリ割当は生じない-1\"><span class=\"toc-item-num\">1&nbsp;&nbsp;</span>in-place計算ではメモリ割当は生じない</a></span></li><li><span><a href=\"#dot-付きの-=-による代入ではメモリ割当が生じない\" data-toc-modified-id=\"dot-付きの-=-による代入ではメモリ割当が生じない-2\"><span class=\"toc-item-num\">2&nbsp;&nbsp;</span>dot 付きの = による代入ではメモリ割当が生じない</a></span></li><li><span><a href=\"#viewによる部分配列のメモリ割当は生じない\" data-toc-modified-id=\"viewによる部分配列のメモリ割当は生じない-3\"><span class=\"toc-item-num\">3&nbsp;&nbsp;</span>viewによる部分配列のメモリ割当は生じない</a></span></li><li><span><a href=\"#sum(f(x)-for-x-in-A)-ではメモリ割当はほぼ生じない\" data-toc-modified-id=\"sum(f(x)-for-x-in-A)-ではメモリ割当はほぼ生じない-4\"><span class=\"toc-item-num\">4&nbsp;&nbsp;</span>sum(f(x) for x in A) ではメモリ割当はほぼ生じない</a></span></li></ul></div>"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "VERSION",
"execution_count": 1,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 1,
"data": {
"text/plain": "v\"1.5.0-rc1.0\""
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "using BenchmarkTools\nusing LinearAlgebra",
"execution_count": 2,
"outputs": []
},
{
"metadata": {},
"cell_type": "markdown",
"source": "## in-place計算ではメモリ割当は生じない\n\nhttps://docs.julialang.org/en/v1/stdlib/LinearAlgebra/#Low-level-matrix-operations-1"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "n = 1024\n\n@time A = randn(n, n)\n@time B = randn(n, n)\n@time C = randn(n, n);",
"execution_count": 3,
"outputs": [
{
"output_type": "stream",
"text": " 0.080893 seconds (144.67 k allocations: 15.690 MiB, 10.62% gc time)\n 0.005487 seconds (2 allocations: 8.000 MiB)\n 0.005638 seconds (2 allocations: 8.000 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "n^2*8/1024^2",
"execution_count": 4,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 4,
"data": {
"text/plain": "8.0"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime A*B;",
"execution_count": 5,
"outputs": [
{
"output_type": "stream",
"text": " 30.124 ms (2 allocations: 8.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `A*B` では `A*B` の分の8MBのメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime A*B*C;",
"execution_count": 6,
"outputs": [
{
"output_type": "stream",
"text": " 66.953 ms (4 allocations: 16.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `A*B*C` では `Tmp = A*B` と `Tmp*C` の分の16MBのメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime A^2*B*C;",
"execution_count": 7,
"outputs": [
{
"output_type": "stream",
"text": " 104.131 ms (6 allocations: 24.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime A^15*B*C;",
"execution_count": 8,
"outputs": [
{
"output_type": "stream",
"text": " 286.271 ms (16 allocations: 64.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように行列の冪乗でもメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "Y = similar(A)\n@btime mul!(Y, A, B)\nY == A*B",
"execution_count": 9,
"outputs": [
{
"output_type": "stream",
"text": " 22.128 ms (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 9,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のようにin-place計算すればメモリ割当が生じない."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime X = 2A;",
"execution_count": 10,
"outputs": [
{
"output_type": "stream",
"text": " 2.976 ms (2 allocations: 8.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように行列のスカラー倍でもメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "Y = copy(A)\n@btime lmul!(2, Y)\nY == 2A",
"execution_count": 11,
"outputs": [
{
"output_type": "stream",
"text": " 433.408 μs (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 11,
"data": {
"text/plain": "false"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように行列のスカラー倍もin-place計算にすればメモリ割当が生じない."
},
{
"metadata": {},
"cell_type": "markdown",
"source": "## dot 付きの = による代入ではメモリ割当が生じない\n\nhttps://docs.julialang.org/en/v1/manual/performance-tips/#More-dots:-Fuse-vectorized-operations-1"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "sindot(A) = sin.(A)",
"execution_count": 12,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 12,
"data": {
"text/plain": "sindot (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime X = sindot(A)\nX = sindot(A);",
"execution_count": 13,
"outputs": [
{
"output_type": "stream",
"text": " 15.778 ms (2 allocations: 8.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `X = sindot(A)` では `sin.(A)` の分だけメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "sindots!(Y, A) = Y .= sin.(A)",
"execution_count": 14,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 14,
"data": {
"text/plain": "sindots! (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime sindots!(Y, A)\nsindots!(Y, A)\nY == X",
"execution_count": 15,
"outputs": [
{
"output_type": "stream",
"text": " 13.371 ms (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 15,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のようにin-place計算するようにすればメモリ割当は生じない."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "sinatdot!(Y, A) = @.(Y = sin(A))",
"execution_count": 16,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 16,
"data": {
"text/plain": "sinatdot! (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime sinatdot!(Y, A)\nsinatdot!(Y, A)\nY == X",
"execution_count": 17,
"outputs": [
{
"output_type": "stream",
"text": " 13.170 ms (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 17,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `@.` マクロでまとめてdotsを付けてもメモリ割当は生じなくなる."
},
{
"metadata": {},
"cell_type": "markdown",
"source": "## viewによる部分配列のメモリ割当は生じない\n\nhttps://docs.julialang.org/en/v1/manual/performance-tips/#Consider-using-views-for-slices-1"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "cutboundary(A) = A[2:end-1, 2:end-1]",
"execution_count": 18,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 18,
"data": {
"text/plain": "cutboundary (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime X = cutboundary(A)\nX = cutboundary(A);",
"execution_count": 19,
"outputs": [
{
"output_type": "stream",
"text": " 2.850 ms (2 allocations: 7.97 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `X = cutboundary(A)` では `A[2:end-1, 2:end-1]` の分だけメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "cutboundary!(Y, A) = @. @views Y = A[2:end-1, 2:end-1]",
"execution_count": 20,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 20,
"data": {
"text/plain": "cutboundary! (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "Y = Array{eltype(A), 2}(undef, size(A) .- (2, 2))\n@btime cutboundary!(Y, A)\ncutboundary!(Y, A)\nX == Y",
"execution_count": 21,
"outputs": [
{
"output_type": "stream",
"text": " 1.958 ms (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 21,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のようにin-place計算にすればメモリ割当は生じない."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "mulinteriors(A, B) = A[2:end-1, 2:end-1]*B[2:end-1, 2:end-1]",
"execution_count": 22,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 22,
"data": {
"text/plain": "mulinteriors (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "X = mulinteriors(A, B)\n@btime X = mulinteriors(A, B);",
"execution_count": 23,
"outputs": [
{
"output_type": "stream",
"text": " 38.730 ms (6 allocations: 23.91 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `X = mulinteriors(A, B)` では `A[2:end-1, 2:end-1]` と `B[2:end-1, 2:end-1]` と `A[2:end-1, 2:end-1]*B[2:end-1, 2:end-1]` の分だけメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "mulinteriors!(Y, A, B) = @views mul!(Y, A[2:end-1, 2:end-1], B[2:end-1, 2:end-1])",
"execution_count": 24,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 24,
"data": {
"text/plain": "mulinteriors! (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "Y = Array{eltype(A), 2}(undef, size(A) .- (2, 2))\n@btime mulinteriors!(Y, A, B)\nmulinteriors!(Y, A, B)\nX == Y",
"execution_count": 25,
"outputs": [
{
"output_type": "stream",
"text": " 24.934 ms (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 25,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のようにin-place計算にすればメモリ割当は生じない."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "x = y =range(-1, 1, length=n)\nu = @. x^2 + (y')^2;",
"execution_count": 26,
"outputs": []
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "function Δ_for(u)\n m, n = size(u)\n v = Array{eltype(u), 2}(undef, m-2, n-2)\n for j in 2:n-1, i in 2:m-1\n v[i-1, j-1] = (\n u[i-1, j] +\n u[i+1, j] +\n u[i, j-1] +\n u[i, j+1] - 4u[i, j]\n )\n end\n v\nend",
"execution_count": 27,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 27,
"data": {
"text/plain": "Δ_for (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime z = Δ_for(u)\nz = Δ_for(u)\nextrema(z)",
"execution_count": 28,
"outputs": [
{
"output_type": "stream",
"text": " 4.788 ms (2 allocations: 7.97 MiB)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 28,
"data": {
"text/plain": "(1.5288635095700442e-5, 1.5288635099253156e-5)"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `z = Δ_for(u)` では `Δ_for(u)` の分だけメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "function Δ_dotsviews(u)\n @. @views (\n u[1:end-2, 2:end-1] +\n u[3:end, 2:end-1] +\n u[2:end-1, 1:end-2] +\n u[2:end-1, 3:end ] - 4u[2:end-1, 2:end-1]\n )\nend",
"execution_count": 29,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 29,
"data": {
"text/plain": "Δ_dotsviews (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime v = Δ_dotsviews(u)\nv = Δ_dotsviews(u)\nv == z",
"execution_count": 30,
"outputs": [
{
"output_type": "stream",
"text": " 5.014 ms (2 allocations: 7.97 MiB)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 30,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `v = Δ_dotsviews(u)` でも `Δ_dotsviews(u)` の分だけメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "function Δ!(w, u)\n @. @views w = (\n u[1:end-2, 2:end-1] +\n u[3:end, 2:end-1] +\n u[2:end-1, 1:end-2] +\n u[2:end-1, 3:end ] - 4u[2:end-1, 2:end-1]\n )\nend",
"execution_count": 31,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 31,
"data": {
"text/plain": "Δ! (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "w = Array{eltype(u), 2}(undef, n-2, n-2)\n@btime Δ!(w, u)\nΔ!(w, u)\nw == v",
"execution_count": 32,
"outputs": [
{
"output_type": "stream",
"text": " 3.061 ms (0 allocations: 0 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 32,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のようにin-place計算にすればメモリ割当が生じない."
},
{
"metadata": {},
"cell_type": "markdown",
"source": "## sum(f(x) for x in A) ではメモリ割当はほぼ生じない\n\nhttps://docs.julialang.org/en/v1/manual/arrays/#Generator-Expressions-1"
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "sumsindot(A) = sum(sin.(A))",
"execution_count": 33,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 33,
"data": {
"text/plain": "sumsindot (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime x = sumsindot(A)",
"execution_count": 34,
"outputs": [
{
"output_type": "stream",
"text": " 16.134 ms (3 allocations: 8.00 MiB)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 34,
"data": {
"text/plain": "-1239.293008526668"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `sum(sin.(A))` では `sin.(A)` の分だけメモリ割当が生じる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "sumsinfor(A) = sum(sin(a) for a in A)",
"execution_count": 35,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 35,
"data": {
"text/plain": "sumsinfor (generic function with 1 method)"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime y = sumsinfor(A)",
"execution_count": 36,
"outputs": [
{
"output_type": "stream",
"text": " 12.885 ms (1 allocation: 16 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 36,
"data": {
"text/plain": "-1239.2930085267185"
},
"metadata": {}
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように `sum(sin(a) for a in A)` ではメモリ割当がほぼ生じない."
},
{
"metadata": {},
"cell_type": "markdown",
"source": "以下は配列 `[sin(a) for a in A]` とGenerator `(sin(a) for a in A)` の違いの確認."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "T = sin.(A)\ntypeof(T), size(T)",
"execution_count": 37,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 37,
"data": {
"text/plain": "(Array{Float64,2}, (1024, 1024))"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "S = [sin(a) for a in A]\ntypeof(S), size(S)",
"execution_count": 38,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 38,
"data": {
"text/plain": "(Array{Float64,2}, (1024, 1024))"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "G = (sin(a) for a in A)\ntypeof(G), size(G)",
"execution_count": 39,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 39,
"data": {
"text/plain": "(Base.Generator{Array{Float64,2},typeof(sin)}, (1024, 1024))"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "collect(G) == S == T",
"execution_count": 40,
"outputs": [
{
"output_type": "execute_result",
"execution_count": 40,
"data": {
"text/plain": "true"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime S = [sin(a) for a in A];",
"execution_count": 41,
"outputs": [
{
"output_type": "stream",
"text": " 15.228 ms (3 allocations: 8.00 MiB)\n",
"name": "stdout"
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime G = (sin(a) for a in A);",
"execution_count": 42,
"outputs": [
{
"output_type": "stream",
"text": " 74.179 ns (1 allocation: 16 bytes)\n",
"name": "stdout"
}
]
},
{
"metadata": {},
"cell_type": "markdown",
"source": "上のように, 配列 `S = [sin(a) for a in A]` と違って, Generator `G = (sin(a) for a in A)` の作成にほとんど時間はかからない. Generatorの作成時に `sin(a)` 達は計算されていない."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime sum(S)",
"execution_count": 43,
"outputs": [
{
"output_type": "stream",
"text": " 302.780 μs (1 allocation: 16 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 43,
"data": {
"text/plain": "-1239.293008526668"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "@btime sum(G)",
"execution_count": 44,
"outputs": [
{
"output_type": "stream",
"text": " 12.906 ms (1 allocation: 16 bytes)\n",
"name": "stdout"
},
{
"output_type": "execute_result",
"execution_count": 44,
"data": {
"text/plain": "-1239.2930085267185"
},
"metadata": {}
}
]
},
{
"metadata": {
"trusted": true
},
"cell_type": "markdown",
"source": "上のように, Generatorの和では和の計算時に `sin(a)` が計算されるのでその分だけ時間がかかる."
},
{
"metadata": {
"trusted": true
},
"cell_type": "code",
"source": "",
"execution_count": null,
"outputs": []
}
],
"metadata": {
"@webio": {
"lastKernelId": null,
"lastCommId": null
},
"_draft": {
"nbviewer_url": "https://gist.github.com/969c4ac8f518e343f44bd6e8d771a94a"
},
"gist": {
"id": "969c4ac8f518e343f44bd6e8d771a94a",
"data": {
"description": "追加メモリ消費がない演算",
"public": true
}
},
"kernelspec": {
"name": "julia-1.5",
"display_name": "Julia 1.5.0-rc1",
"language": "julia"
},
"language_info": {
"file_extension": ".jl",
"name": "julia",
"mimetype": "application/julia",
"version": "1.5.0"
},
"toc": {
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": true,
"base_numbering": 1,
"title_cell": "目次",
"title_sidebar": "Contents",
"toc_cell": true,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment