ヒートマップをお手軽に 〜テンソルでも機械学習でもない Elixir Nx の使い方〜

これは [fukuoka.ex Elixir／Phoenix Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fukuokaex)の11日目です。昨日は @tuchiro さんの [業務でプログラムを始める入門者のために保守エンジニアの観点から読みやすいコードの構造を考えてみた](https://qiita.com/tuchiro/items/3281426e51db4d7a5fa3)でした。

# はじめに
計算途中や計算結果のデータをあまり手間をかけずに可視化したいときってありますよね。チャチャっとグラフやら表やらにしたくなること、多くないですか。でもじゃあ Phoenix/LiveView でって言いたいところですけど、ササッとやるには LiveView でも大変そうじゃありませんか。今回はヒートマップをチョ〜簡単に作ってみます。それも、あの Nx を使って。

## Nx とは
Nx は Elixir のテンソル計算用ライブラリです。テンソルとは自然科学で出てくる値の表現で、スカラー・ベクトル・行列、にさらに続いて出てくる数学の構造です。ですので Nx は自然科学全般に広く使えるでしょう。特に Nx は機械学習目的に作成されたようで、これで Elixir の応用範囲も大きく広がると期待されています。でも、今回はテンソル的な使い方は一切しません。

### ドキュメント
現状で Nx は Hex に上がっておらず、よくあるライブラリの中身を見る手段が使えません。
インストール前に個別の関数の詳細を見ようとすると [github 上の Nx のコード](https://github.com/elixir-nx/nx) を見るしかないようです。インストールすると iex のヘルプでドキュメントが見えます。

なお、Nx の入門については [NX 公式ページの Resouces](https://github.com/elixir-nx/nx/tree/main/nx#resources) が役に立ちそうです。

## ヒートマップ
ヒートマップとは、面上の図形に濃淡・色で値を表現したグラフです。比較的馴染みがあるのは携帯電話のサービスエリアの表示でしょうか。以下は NTT docomo のページで調べた私の職場周辺のサービスヒートマップです。かろうじて高知工科大学（地図の右上の赤ピン）が範囲に入っています。

![20211229_docomo_KUT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/f8151f97-d08b-1e16-6ae0-d602aad93c1f.png)

これ、ヒートマップでお馴染みの例を出しただけなので、今回の記事でこんなのが作れるのかと勘違いしないようにしてください。要は2次元の空間にz軸方向の値を色や濃淡で表現したグラフということを言いたいのです。

## インストール
インストール手順はざっくり以下です。

- `mix project_name` して Elixir プロジェクトを作成する
- mix.exs に Nx への依存関係を追加する
- `mix deps.get` する

[^nx-version]: Nx のバージョンはこの記事を書いている時点の[Nx Installation](https://github.com/elixir-nx/nx/tree/main/nx#installation)です。常に最新のものをチェックしてください。

以下に依存関係の記述の例を示します[^nx-version]。

```exs:mix.exs
def deps do
  [
    {:nx, "~> 0.1.0-dev", github: "elixir-nx/nx", branch: "main", sparse: "nx"}
  ]
end
```

より詳しくは [@mokichi さんの記事](https://qiita.com/mokichi/items/1716b75709559b18ef6c) やそこから参照している他の記事を御覧ください。

# Nx によるヒートマップ
一通り `Nx.to_heatmap` の機能を見てみましょう。

## テンソル型の値を作る
Nx にはデータ型として `Nx.Tensor` なる型があります。Nx の関数は基本的にこの型のデータに対して作用します。この型の値はリストから `Nx.tensor` 関数で生成することができます。

```exs
iex(43)> Nx.tensor([[0,1,2],[3,4,5]])                 
#Nx.Tensor<
  s64[2][3]
  [
    [0, 1, 2],
    [3, 4, 5]
  ]
>
```

## 簡単なヒートマップを作る
ヒートマップを作る関数は `Nx.to_heatmap` です。ヒートマップを作るのはすべてこの `Nx.Tensor` 型のデータに対してです。iex で `h Nx.to_heatmap` とやると使い方がわかります。
![help.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/02bb0f35-c448-9a4c-cf52-76b4de84e97d.png)
返すのは `Nx.Heatmap` なる、引き数で与えたテンソルの値自体とそれからできるヒートマップをくっつけた型の値になります。

では先程のテンソルの値をヒートマップで見てみましょう。以下のようにすることで `[[0,1,2],[3,4,5]]` の値全体をヒートマップで見ることができます。`Nx.tensor([[0,1,2],[3,4,5]]) |> Nx.to_heatmap` とやってみてください。
![heatmap1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/fa5668f2-0cce-b425-718f-fa22c81a5298.png)
リスト `[[0,1,2],[3,4,5]]` の内容が、白から黒までのグレースケールで表現されています。

## ヒートマップの濃さの決め方
ヒートマップは白から黒までの灰色の濃さで表現されています。

上の例では要素中の最小値である 0 が黒になってます。同様に要素中の最大値である 5 が白になっています。その途中の 1〜4 は灰色の濃さで表現されています。

値に対する灰色の濃さは与えられたテンソルの要素中の最大・最小の値で自動で調節されます。例えば 0 と 1 からなるヒートマップは市松模様になります。以下では 0 が黒、1 が白で表現されています。
![heatmap_check.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/7b045d91-5422-ccf9-f52c-72dfe5e0097a.png)

セルの絶対的な濃さは値の絶対的な大きさとは無関係なことに注意してください。例えば `Nx.tensor([[-2,10,-2],[10,-2,10]]) |> Nx.to_heatmap` とやっても、先程のと同じヒートマップが出ます。
![heatmap_check2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/5b1cf3dd-0002-65cf-7444-ad23c16e837b.png)

## 大きなヒートマップ
これまでは 3x2 の大きさのテンソルでした。もっと大きいのを作ってみましょう。横に長めのリストを作ってみます。

```exs
iex(6)> 0..19 |> Enum.to_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
```

これのヒートマップは `0..19 |> Enum.to_list |> Nx.tensor |> Nx.to_heatmap` で作れます。
![heatmap_0_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/d4039af0-d133-ae3c-28dc-8ed563a3c47d.png)
パッと見、20段階はあるので、これ以上の細かいグレースケールは可能そうです。

## ターミナルエミュレータの大きさ
となると一気に100でどうだとか思ってしまいます。`0..99 |> Enum.to_list |> Nx.tensor |> Nx.to_heatmap` の結果がこれ。
![heatmap_0_99.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/2b273b17-dee0-a9df-5b18-b8a79ccf57cf.png)
あれえ、なんだか濃さが変ですね。

これ、MacOSのターミナルエミュレータの `ターミナル` アプリを横幅を80文字に設定して使ってます。もっと横幅が大きくて1行を改行で折り返さないで済むような横に伸ばしたターミナルエミュレータでやってみます。するときちんとグラデーションが見えます。
![heatmap_0_99_long.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/ee50a32e-16c4-d9c1-64d4-481d9ef79bd6.png)
MacOS のターミナルアプリが悪いのか、Nx の `to_heatmap` が悪いのか、はたまた Elixir の `IO.ANSI` ライブラリが悪いのか、分かりません。とりあえず改行がないような環境でしのいでください。

んで、もとに戻って、よく見てみると100段階の値に対してヒートマップのグレースケールは100段階はないですね。

## ヒートマップのオプション
この `to_heatmap/2` 関数の第2引数はオプションです。このオプションには2種類あります。

### ANSIシーケンス対応でない端末を使う
`Nx.to_heatmap` には `:ansi_enabled` というオプションがあります。

これは、もしあなたが [ASR-33](https://ja.wikipedia.org/wiki/ASR-33) テレタイプライタとか [IBM タイプライタ](https://ja.wikipedia.org/wiki/IBM_Selectric_typewriter) とかのANSIシーケンス非対応な端末で Nx をお使いの場合に使うオプションです。もしあなたが「そんな機械式の端末を今どき使うかね。私が使ってるのは ANSI シーケンス原器とも言える [DEC VT100](https://ja.wikipedia.org/wiki/VT100) だよ」と思っても用心してください。私が思うにおそらく `IO.ANSI` を開発した連中は VT100 で検証してないはずです。

関数を呼び出す際に `:ansi_enabled` を設定しない場合は `IO.ANSI.enabled?` の結果を用います。これにより現在の端末が ANSI シーケンス対応かどうかを自動的に判別してます。
![IO_ansi_enabled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/e93f22c6-9b4b-2e0f-25e5-3836f8522d44.png)
もし明示的にANSIシーケンスを使わないようにする場合はこの値を `false` にして関数を呼び出してください。以下は `0..9 |> Enum.to_list |> Nx.tensor |> Nx.to_heatmap(ansi_enabled: false)` の結果です。
![ansi_enabled_false.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/193feecc-4352-dc99-476a-52ba54b42ed3.png)
このように色の濃さではなく数値が入ります。これはたまたま 0〜9 の値しかないテンソル値のヒートマップとして 0〜9 を返していますが、テンソルの中身によらず常に 0〜9 の範囲に収めて返されます。以下は `0..19` と `0..99` の場合です。
![ansi_false_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/d482f17a-d2eb-91b5-06c1-33f45b049952.png)
![ansi_false_100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/a10b8068-28a0-1bc3-586c-3da42b882001.png)
`0..99` の場合は、返ってくる 0〜9 の数が10個ずつになっていないことに注意してください。0 が6個連続して、その後 1〜8 がそれぞれ11個連続して、最後にまた 9 が6個連続しています。

### 基礎になる文字を変える
もう一つのオプションは `:ansi_whitespace` です。一つ々々のセルはそれぞれ1文字になっており、デフォルトでは Unicode の `"\u3000"` を使います。フォントセットによりますが、それぞれのセルが真四角の文字で印刷されることが期待されます。これを変更することも可能です。別の文字にしたいという要求がどれぐらいあるのか怪しいですが、例えば `○` すなわち `"\u3007"` に変える場合は `0..9 |> Enum.to_list |> Nx.tensor |> Nx.to_heatmap(ansi_whitespace: "\u3007")` とします。
![heatmap_u3007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/e770c0c7-ae77-9716-7596-f89992d7b8f1.png)
要りますかね、この機能…

# Nx を使ってみる
ここまで `Nx.to_heatmap` の基本的な扱いを書いてきました。ここからはもう少し大きなテンソルでこの関数を使ってみましょう。

まず、決め打ちで `0..9` とか書くのをやめたいのと、いろんな関数の結果を出したいので、そこまでをひとまとめにした関数 `mkheatmap/3` を作ります。引数は以下。

- x: 横のセル数
- y: 縦のセル数
- fun: 縦方向(0〜x-1)と横方向(0〜y-1)の整数値を取ってなにかのスカラ値を返す関数

```exs
iex(52)> mkheatmap = fn(x, y, fun) -> 
...(52)>   0..y-1
...(52)>     |> Enum.to_list
...(52)>     |> Enum.map(fn j ->
...(52)>       (0..x-1
...(52)>         |> Enum.to_list
...(52)>         |> Enum.map(fn i -> fun.(i, j) end))
...(52)>     end)
...(52)>   |> Nx.tensor
...(52)>   |> Nx.to_heatmap
...(52)>   end
#Function<42.65746770/3 in :erl_eval.expr/5>
```
長いですね。一発で書くとこうなります。

```exs
mkheatmap = fn(x, y, fun) -> 0..y-1 |> Enum.to_list |> Enum.map(fn j -> (0..x-1 |> Enum.to_list |> Enum.map(fn i -> fun.(i, j) end)) end)  |> Nx.tensor |> Nx.to_heatmap end
```


## 市松模様
ではまず、市松模様を作ってみます。奇遇性で 0 と 1 を分ければ良いので、こんな関数 `checker/2` を作りました。

```
checker = fn(m, n) -> rem(m + n, 2) end
```

これを使って 横20 × 縦10 の市松模様をつくるには `mkheatmap.(20, 10, checker)` とします。

![checker20x10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/60b7425c-d6b2-1079-a3e9-f8ec3f84a69b.png)

## 九九表
掛け算する関数 `mul9x9 = fn(m, n) -> m * n end` を使って九九のヒートマップも作れます。これで `mkheatmap.(10, 10, mul9x9)` した結果が以下。

![mul9x9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/a79f8876-fd85-86e0-fe8c-bc499324ec2d.png)

ちなみに足し算の九九の場合は `sum9_9 = fn(m, n) -> m + n end` を使って `mkheatmap.(10, 10, sum9_9)` します。
![sum9_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/734fc220-73b3-131e-7252-fdce884db3da.png)

## すり鉢
すり鉢状のポテンシャルを入れてみましょう。縦横を 11x11 と決め打ちして `mortar/2` という関数を作り

```exs
mortar = fn(m, n) -> :math.sqrt((m - 5) * (m - 5) + (n - 5) * (n - 5)) end
```
`mkheatmap.(11, 11, mortar)` としたのが以下です。

![mortar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/4f7c99aa-a50c-2029-e90d-994fbf9d2721.png)

ちなみに、このヒートマップ、じっと見てるとなんだか濃淡が変化して見えませんかね。

## リサージュ
三角関数も使ってみましょう。x軸方向(0〜31)いっぱいに1周期を取る sin 関数と、y軸方向(0〜31)いっぱいに1周期を取る cos 関数を使って、それらの積を返す関数 `lissajous/2` を作り

```exs
lissajous = fn(m, n) -> :math.sin(m / 30.0 * 2.0 * :math.pi) * :math.cos(n / 30.0 * 2.0 * :math.pi) end
```
`mkheatmap.(31, 31, lissajous)` としたのが以下です。
![lissajous.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/583e8030-9844-6f9c-cd24-10d5b958461f.png)

これ、複素数が使えたりするともっと面白くなりそうです。

## ライフゲーム
計算機理論にセルオートマトンという一分野があります。その例として最も有名なのが *Conway's Game of Life* で、日本語では通称「ライフゲーム」と呼ばれています。随分前の Elixir 入門初期に[ライフゲームのプログラム](https://qiita.com/kikuyuta/items/e16dd260a39fe6429473)を作成したことがあります。

このときの出力はコンソールに `0` `1` をそのまま出力するものだったので、これをヒートマップで出してみることにします。

```exs
defmodule Nxlife do
  @moduledoc """
  This module plays Conway's Game of Life in single process.
  """

  @doc"""
  The function nth_gen generates new generations
  from the initial field `world` for `n` times
  where nth_gen(n, i, world).
  Please feed 0 to `i` for displaying the generation cycle.

  For exapmle,
    Nxlife.nth_gen(10, 0,
      {{0, 0, 0, 0, 0},
       {0, 0, 1, 0, 0},
       {0, 0, 0, 1, 0},
       {0, 1, 1, 1, 0},
       {0, 0, 0, 0, 0}})
  shows a flying glider with 10 generations.
  """

  def nth_gen(n, i, world, interval \\ 200) do
    pp(i, world)
    if n == i do
      :ok
    else
      Process.sleep(interval)
      nth_gen(n, i+1, next_gen(world), interval)
    end
  end

  def pp(gen, world) do
    IO.puts("#{gen}th")
    for j <- 0..tuple_size(world)-1 do
      elem(world, j) |> Tuple.to_list
    end
      |> Nx.tensor |> Nx.to_heatmap |> IO.inspect
  end

  def next_gen(current) do
    (for j <- 0..tuple_size(current)-1 do
       (for i <- 0..tuple_size(elem(current, 0))-1 do
         get_neighbor(i, j, current) |> dead_or_alive()
       end) |> List.to_tuple
    end) |> List.to_tuple
  end

  def get_neighbor(x, y, world) do
    rx = tuple_size(elem(world, 0))
    ry = tuple_size(world)

    [elem(elem(world, rem(y+ry-1, ry)), rem(x+rx-1, rx)),
     elem(elem(world, rem(y+ry-1, ry)),     x),
     elem(elem(world, rem(y+ry-1, ry)), rem(x+   1, rx)),
     elem(elem(world,     y),           rem(x+rx-1, rx)),
     elem(elem(world,     y),               x),
     elem(elem(world,     y),           rem(x+   1, rx)),
     elem(elem(world, rem(y+   1, ry)), rem(x+rx-1, rx)),
     elem(elem(world, rem(y+   1, ry)),     x),
     elem(elem(world, rem(y+   1, ry)), rem(x+   1, rx))]
  end

  def dead_or_alive(neighbor) do
    me_now = Enum.at(neighbor, 4)
    case (Enum.sum(neighbor) - me_now) do
      2 -> me_now
      3 -> 1
      _ -> 0
    end
  end

  def test5() do
    Nxlife.nth_gen(20, 0,
      {{0, 0, 0, 0, 0},
       {0, 0, 1, 0, 0},
       {0, 0, 0, 1, 0},
       {0, 1, 1, 1, 0},
       {0, 0, 0, 0, 0}})
  end

  def test10() do
    Nxlife.nth_gen(100, 0,
      {
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
      },
      100)
  end
end
```

これの `Nxlife.test10` とかをやってみた結果が以下です。

https://twitter.com/kikuyuta/status/1476205911886929921

# ヒートマップを使えない場合
ここまでやってて今更なのですが、単に行列のヒートマップを表示するというのに何も考えずに使うとエラー起こして止まる場合があることに気づきました。

## 極端な値の要素を持っている場合
テンソル内の値に特別な値があるとうまくいきません。以下は $1.0^{100}$ と $1.0^{-100}$ とを要素に持つテンソルのヒートマップを出そうとした場合です。

```exs
iex(2)> [1.0e100, 1.0e-100] |> Nx.tensor |> Nx.to_heatmap
%Inspect.Error{
  message: "got MatchError with message \"no match of right hand side value: <<0, 0, 128, 127>>\" while inspecting %{__struct__: Nx.Heatmap, opts: [], tensor: #Nx.Tensor<\n    f32[2]\n    [Inf, 0.0]\n  >}"
}
```

これはヒートマップを出す前の、テンソルに変換した段階ですでに単精度(32bit)浮動小数点数の表現範囲を超えてしまってます。Elixir/Erlang の浮動小数点の扱いは64bitの倍精度浮動小数点数しかありませんが、Nx には浮動小数点数に4種類あってデフォールトが32bitです。ですので

```exs
iex(17)> [1.0e100, 1.0e-100] |> Nx.tensor
#Nx.Tensor<
  f32[2] 
  [Inf, 0.0]
>
```
と、大きい方の値は `Inf` に、小さい方の値は `0.0` に丸められてます。この `Inf` という `NaN` に対応出来ないのです。これについては Nx の問題というより [Erlang/OTP の NaN の扱いの問題なので、それが解決するのを待っているような状況](https://github.com/elixir-nx/nx/pull/596#discussion_r776983992) のようです。

ちなみにこの場合は `[1.0e100, 1.0e-100] |> Nx.tensor(type: {:f, 64}) |> Nx.to_heatmap` と倍精度(64bit)浮動小数点数を明示的に使うことで、エラーからは逃れられます。

## テンソル内のすべての要素が同一の場合
追記：これは 2021.12.30 時点の話です。これはすでに修正されています[^nx_pr596]。

[^nx_pr596]: この記事を書いてて気がついて [プルリク(#596)](https://github.com/elixir-nx/nx/pull/596/) を出したところ、翌日の大晦日にマージされました。些細なことですが貢献できてハッピーです。

テンソル値内の **全ての要素が同じ値の場合にはエラー**してしまいます。

試しに以下をやってみます。

- `[0]  |> Nx.tensor |> Nx.to_heatmap`
- `[1, 1]  |> Nx.tensor |> Nx.to_heatmap`
- `[[-1, -1, -1], [-1, -1, -1]]  |> Nx.tensor |> Nx.to_heatmap`

![heatmap-0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/333850b6-8fdd-7c78-5e1a-0bbe085eff48.png)
![heatmap-1_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/5137210e-53b6-23c5-72d8-a80e5cfb5980.png)
![heatmap-xxx-xxx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/01e5cd75-c39f-ac19-75a1-7af3f010c69d.png)
これを回避するのに安直に考えると以下の方法があります。

- 与えるテンソルに余分な要素を追加して、全部の要素が同じ値にならないようにする
- 与えるテンソル内の要素を一部変更して、全部の要素が同じ値にならないようにする
- 全部の要素の値が同じになる場合を検出して、その場合は値を渡さない

なおこれ、ソースコード [heatmap.ex](https://github.com/elixir-nx/nx/blob/main/nx/lib/nx/heatmap.ex)[^nx_version] を見てみるとバグらしき部分がありましたので年末にデバッグとプルリクに勤しみたいと思います[^nx_pr596]。

[^nx_version]: 見ているのは https://github.com/elixir-nx/nx/blob/main/nx/lib/nx/heatmap.ex の 2021.12.29 のものです。


# まとめ
Elixir のテンソル計算ライブラリ Nx にある to_heatmap 関数を使ってお手軽にヒートマップを作ってみました。プログラム書いてて「データをサッと可視化したい、でも自分で可視化部分を書くのは面倒」というときに思い出してください。

さて、明日の [fukuoka.ex Elixir／Phoenix Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fukuokaex) の記事は @the_haigo さんの [LiveViewとNxで画像処理](https://qiita.com/the_haigo/items/236e0e3c6cfbbd12799a) です。お楽しみに！

# 参考文献

- [Nx の readme.md](https://github.com/elixir-nx/nx/tree/main/nx#readme)
- [ヒートマップ wikipedia](https://ja.wikipedia.org/wiki/ヒートマップ)
- [docomo の 5Gエリアヒートマップ](https://www.nttdocomo.co.jp/area/5g/)
- [Elixirの革新的ライブラリ「Nx」をMacでも動かしてみた by @mokichi さん](https://qiita.com/mokichi/items/1716b75709559b18ef6c)
- [Elixir IO.ANSI](https://hexdocs.pm/elixir/1.12/IO.html)
- [Unicode一覧 3000-3FFF, Wikipedia](https://ja.wikipedia.org/wiki/Unicode一覧_3000-3FFF)
- [Conway's Game of Life, Wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)
- [ライフゲーム, Wikipedia](https://ja.wikipedia.org/wiki/ライフゲーム)
- [はじめてな Elixir(25) ライフゲームを作ってみる](https://qiita.com/kikuyuta/items/e16dd260a39fe6429473)