DeepLearningは最近ブームであり,その有名なライブラリとしてTensorflowがあります.
この記事ではDeepLearningの基本的な部分を数式を使って書き下すこととTensorflowの使い方を紹介します.
今更っていう気もしますが…,そこは気にしないでおくことにします
主な対象はベクトル空間やテンソル積等をある程度知っているけれど,DeepLearningは知らない人です.
なので表記も大学の数学でよく出てくるものしています.
なおニューラルネットワークの積分表現には触れません.
三層パーセプトロン
ニューラルネットワークの基本的な形の一つである三層パーセプトロンを定義します.
定義 (三層パーセプトロン)
行列$W_1 \in M_{n_0 n_1}(\mathbb{R}),W_2 \in M_{n_1 n_2}(\mathbb{R})$とベクトル$b_1 \in \mathbb{R}^{n_1} ,b_2 \in \mathbb{R}^{n_2}$と$ \sigma,\tau:\mathbb{R} \to \mathbb{R}$が存在し,
$$F = \overline{\tau} \circ f_{W_2,b_2} \circ \overline{\sigma} \circ f_{W_1,b_1} $$
と書ける時,関数$F:\mathbb{R}^{n_0} \to \mathbb{R}^{n_2}$を 三層パーセプトロン という.
ただし,$f_{W_i,b_i}:\mathbb{R}^{n_{i-1}} \to \mathbb{R}^{n_i}$は$ x \mapsto W_ix+b_i$で定める写像であり,$\overline{\sigma},\overline{\tau}$はそれぞれ成分毎に$\sigma,\tau$で写した写像とする.
三層パーセプトロンはよく下の図のように描かれます.
この図の◯一つが$\mathbb{R}$で→が◯と◯の間の写像です.
この画像は人工知能であそぶで公開されていたものを使わせていただきました.
数学に慣れがあれば、多層に拡張するのは簡単だと思うので,一般的な定義は読者の演習問題にします.
なお,ここでは三層パーセプトロンを層の数を一般化したものとCNNとRNNをニューラルネットワークと呼ぶこととします.
問題設定
三層パーセプトロンを定義したところで,機械学習の数学的な問題設定を述べておきます.
問題
ある関数$f :\mathbb{R}^n \to \mathbb{R}^m$に対し,
有限集合$A \subset \mathbb{R}^n$上の関数の値,すなわち$\{(a,f(a)) \mid a \in A\}$を用いて$f$が求められるか?
上の問題が想定しているのは,$f$は具体的にどんな関数かはわかっていないが,その一部分のデータ$A$から$f$あるいは人間の体感的には$f$と思ってもよいだろうという関数を求めたいというものです.
例えば,画像から犬と猫を分類したいとします.
この時,(誰も書いたことがない画像が存在するので)全ての犬画像と猫画像は当然得られないのですが、適当に何枚か犬と猫の画像データを使って,可能な限り精度の高い犬か猫かを判定する判定機を作れるかという問題になります.
余談
定義域や値域が$\mathbb{R}$の直和に制限する必要はないでしょう.その(ユークリッド位相での)コンパクト部分集合で十分な気もしますし,そもそも$\mathbb{Q}$の直和の方が自然な気もします.
三層パーセプトロンを含むニューラルネットワークでは上の問題の解答をニューラルネットワークを使って求めようとします.
つまり与えられたデータから,もともとの$f$に"近い"ニューラルネットワークを求める問題となります.
そうするとそもそもの疑問として以下が思い浮かびます.
疑問
ニューラルネットワークには元々知りたい$f$に近いものが存在するのか?
この問いに対して,定義域がコンパクトな場合はよい答えが知られています.
定理(普遍性定理)
定義域がコンパクトな連続関数全体の空間上,$\sigma,\tau$にシグモイド関数を取った三層パーセプトロン全体のなす集合はsupノルムが定める位相に対して稠密である
例えばRIMSの講究録に詳しい記述があります.
ニューラルネットワークの学習アルゴリズム
実際に関数$f$に近いニューラルネットを求めるアルゴリズムを説明します.
三層パーセプトロンでいうと,$W_1,W_2,b_1,b_2$を定めるアルゴリズムになります.
ニューラルネットワークを定めるアルゴリズムに従い計算する行為を学習といいます.
機械学習では,データの特徴を学習して関数を作るという意識があるので,学習という名前がついています.
アルゴリズム
一意にアルゴリズムが定まっているわけではありませんが、ニューラルネットワークの学習アルゴリズムはおおよそ、以下のように行われます.
- ニューラルネットワークの構造を決定
- ニューラルネットワークを構成する行列等に初期値を与える
- ニューラルネットワークに入力データを代入して結果を得る
- 計算結果と正解との誤差を求める
- 誤差を減らすように計算グラフの値を変更
- 関数がいい感じになるまで,3.~5.を繰り返す
これらの処理を具体的に見ていきます.
ニューラルネットワークの構造を決定
三層パーセプトロンの場合で言う, $n_0,n_1,n_2$および$\sigma,\tau$を何にするか決める行為です.
これらはよりよい結果が得られるように定めたいわけですが,どうすればよりよくなるのかは非常に難しく,
層の数を単に増やせばいいのか,あるいは$n_1$を増やすべきなのか,はたまた,CNN等の別の構造を考えるべきなのか…
データをもとにヒューリスティックに試行錯誤するしかないのかなあと思っています.
ニューラルネットワークを構成する行列等に初期値を与える
初期値が何にするべきかは結構難しい問題で,何も考えない時は適当な乱数で定めます.
工夫をする時はその問題の特徴を見て決めるのか,あるいは転移学習のように別で学習したデータを適用するという方法もあります.
ニューラルネットワークに入力データを代入して結果を得る
ある関数が定義されているので,それを使って入力データに対する出力を計算します.
コンピュータ上で計算を高速化する場合は
行列の計算をうまく並列にする等の工夫がいります.
計算結果と正解との誤差を求める
誤差を計算するにはその前に誤差関数を定義する必要があります.
わかりやすいのは二乗平均誤差です.
求めたい関数を$f:\mathbb{R}^m \to \mathbb{R}^n$,ニューラルネットワークを$f'$とします.
有限集合$A \subset \mathbb{R}^m$でのデータを使ってよい場合,誤差を
$$ \sum_{x \in A}|f(x) - f'(x)|^2 $$
で定めます.
ここにはいろいろな方法があって例えば$A$を全て使うのではなく適当に$A$の部分集合だけで計算する場合もあります.また,出力を確率的に扱うとうまくいく場合が多いので,確率同士の誤差の場合は$\log$を使う場合もあります.ロジットはここでは本質ではないので詳しく説明しませんが,ここやここ等に記載されています.
誤差を減らすように行列やベクトルの値を変更
誤差が少なくなるように行列やベクトルの値を変更します.
基本的な発想はGradient Descnetです.
つまり,$W,b$の成分で方向微分を計算し,誤差が減る方向に$W,b$の値をずらすという方法です.
ただ,毎回全てのデータに対して計算をすると,計算時間が足りなくなる場合があるので,ランダムに$A$の部分集合$B$をとって誤差の計算をします.これをstochastic gradient descentと言います.
$W,b$を減らす方法は他にもAdam等多数あるのですが,一つ一つの具体例を書くのもおかしいのでここでは割愛します.
DeepLearningの教科書ではこの計算を求めるアルゴリズムとしてbackpropagationが強調されています.
backpropagationは微分を$W,b$を全て計算するために層の個数$n$が大きい方,つまり出力の近い側から計算しようというものです.
どの順番で計算しようと計算結果は変わりませんが,計算時間は大きく変わります.
Gradient Descentだとしてback propagtaionがどういうものか書いておきます.
$f : \mathbb{R}^{d_1} \to \mathbb{R}$(損失関数を想定しているので,行き先は一次元にしています.)が$f = f_n^{W^n} \circ \cdots f_1^{W^1}$と書けたとする.$(W^1,\ldots,W^n)$は$f$で使われる行列を表しています,back propagationでは最終的にこの行列の値を変更します.
.また,表記を書く都合上,$f_k \circ \cdots f_1(x)$を$x_k$と書きます.
$w^l_{11}=v^l_{11}$の場合の$w^l_{11}$での偏微分は
$$
\frac{\partial f}{\partial w^l_{11}} = \sum_{i_n=1}^{d_n}
\frac{\partial f_n}{\partial x^{i_n}}|_{x_{n - 1}}
\cdots \sum_{i_{l+1}=1}^{d_{l+1}}
\frac{\partial f_{l+1}}{\partial x^{i_{l+1}}}|_{x_{l}}
\frac{\partial f_l(x_l)}{\partial w^l_{11}}|_{v^l_{11}}
$$
となる.
これを使って$(w^l_{ij})$での$\nabla f$を計算すれば良い.
さすがに式がこれ以上になると増えすぎて見にくいので,今回は省略します.
余談
上は$W$が同じでも入力の$x$が違うと異なる値を取ります.
backpropagationの説明を見るとそのことが曖昧であったり,$\sum$が省略されていたり,値が書かれていなかったりするものが多かったので,そこは丁寧に書いてみました.
また偏微分導関数の像が一次元になっているのは損失関数で一次元だからです.一般には高次元にできます.
上の式をみればわかるように,$l-1$次の偏微分導関数を計算する場合は$ \sum_{i_n=1}^{d_n}
\frac{\partial f_n}{\partial x^{i_n}}|_{x_{n - 1}}
\cdots \sum_{i_{l+1}=1}^{d_{l+1}}
\frac{\partial f_{l+1}}{\partial x^{i_{l+1}}}|_{x_{l}}
$を流用できます.
関数がいい感じになるまで,3.~5.を繰り返す
何をもっていい感じかは非常に難しいのですが,適当に回数を繰り返すか等で終わりにしている事が多いです.
評価
ここまでアルゴリズムを説明しましたが,我々が知りたいのは$A$以外の部分の定義域で$f$に近いニューラルネットワークが得られたかです.
上のアルゴリズムは$A$内に閉じています.なので,学習が終わった後,$A$以外の有限集合$C$上で誤差を評価します.
Tensorflow
DeepLearningの基本的な部分を説明しました.
ここからは,Tensorflowの説明をします.
とりあえず学習をかけられることを目指します.
Tensorflowはニューラルネットワークの計算をするプログラムなので,上で書いた1.~6.を実施しています.
典型的となる部分を裏で自動的に計算する仕組みを持っており,おかげで人間がソースコードを書く量を減らしてくれてます.
具体的にいうと5.はニューラルネットワークの構成と誤差を表す関数(損失関数)と入力さえわかれば,計算できるので、プログラムで自動で計算してくれます.
また,計算する毎に$W,b$の値が変わっていくので,5.を実現するために,以前の計算結果を覚えています.
計算を覚えておくためにTensorflowではSessionという機能があります.
Tensorflowで使われるクラス
Tensorflowで実際に使われるクラスを説明します.
その前にプログラムをあまり知らない人向けなので,クラスを雰囲気だけ説明します.
数学に例えると,クラスとは、構造の一種です.
ものの集まりとその集まりが持つ構造(定数や関数)を定めたものです.
例えば,開集合の公理をイメージするのが自然かもしれません.
プログラムではクラスをふんだんに活用します.
なぜ使うかは例えばここやここ
を参照してみてください.
Variable
$W,b$等が使われるクラスです.このクラスの元は単に計算できるだけでなく,以下の特徴があります.
- ランダムに初期値な設定をしやすくしてある.
- backpropagationで値を変更できる.
placeholder
関数の入力と出力等に使われるクラス.
実際の際に人間が設定して,動的に値を設定できるものです.
例えば二次元の配列を与えてforwardpropagation→backpropagation.
その後別のデータで同じ処理をする.
余談
本来はVariableやplaceholderクラスの定義をしないといけないと思うのですが,複雑なこともあって、いつ使うかと何がうれしいかだけを書きました.
気になる場合はTensorflowの公式サイトやソースをみてください.
Session
Sessionは計算結果を覚えておくクラスです.
tensorflowの処理
Tensorflowで学習する際、プログラムが実施する処理は以下になります.
- データの取得
- Sessionの作成
- 計算グラフの作成
- Variableの初期化
- 計算の実行
- Sessionの終了
この処理の順に何をするか記載します.
データの取得
fileやDBからデータを取得します.
また、とても有名なデータセットに対しては読み込みするメソッドが用意されています.
例えば,mnistの場合だと以下のようにすればデータセットを取得できます.
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
Sessionの作成
sess = tf.Session() #Sessionの作成
計算グラフの作成
プログラムの処理として計算グラフの作成自体が厳密にこのタイミングで実行されているのかわかりませんが,
ソースとしては次に書くことになります.
今まで計算グラフという言葉を使っていませんでしたが、
計算グラフは演算の処理をグラフとして定義したものです.
詳細はここ等を参考にしてください.
ソースコードとしては計算を記載するだけです.
ソースのコメントにどういう処理かを記載します.
初めて見たときに僕が難しかったのは入力のところです.
関数を定義するのですが、普通、関数$f$だと$x \mapsto f(x)$を定めます.
ですがDeepLearningでは複数のデータをまとめて計算した方が都合がいいので,
${x_1,\dots,x_n} \mapsto {f(x_1),\dots,f(x_n)}$を定めています.
また、この$n$は計算を代入する時に定めた方が都合がいいことが多いです.
そのため,計算グラフを作成する場合は$n$に依存しないNoneと記載して動くようになっています.
# 定義域の元を定義
x = tf.placeholder(tf.float32, [None, 784])
# 行列の定義
W = tf.Variable(tf.zeros([784, 10]))
# ベクトルの定義
b = tf.Variable(tf.zeros([10]))
# sigmaとしてsoftmax関数を設定し、一つの層として計算
y = tf.nn.softmax(tf.matmul(x, W) + b)
# xの正解の関数fの値を定義
y_ = tf.placeholder(tf.float32, [None, 10])
# 誤差を定義
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
# 誤差を最小化するようにbackpropagationwを実施
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
損失関数は具体的に定義しましたが,いくつかの典型的なパターンについてはtf.losses配下のメソッドになっています.
例えば以下があります.
- tf.losses.softmax_cross_entropy()
- tf.losses.mean_squared_error()
Variableの初期化
sess.run(tf.global_variables_initializer()) # Varibleの初期化
計算の実行
for _ in range(1000):
batch_xs, batch_ys = mnist.train.next_batch(100)
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
Sessionの終了
sess.close() #SessionのClose
注意
これで計算自体はできているのですが,実際に$W,b$がどういう値になったかや,汎化誤差がどのようになったのかがわかりません.
そのためには別のデータに対して評価します.
変数の値の確認
Tensorflowの計算はSession中に値がかわっていくものなので,計算結果を知りたい場合は途中で知りたくなります.
例えば$y$の値がSessionをCloseする前に以下を実施すればみれます.
y_output = y.eval(sess, feed_dict={x: batch_xs, y_: batch_ys})
print(y_output)
微分の実装は浮動小数点の評価等も含め面倒なイメージがあるのですが,Tensorflowを使えばこの計算は一瞬でできるわけですね.
微分のことは自分でしろと言われる時代じゃないわけです.
データの保存
$W,b$等を保存しておかないと後で活用できません,そのために学習では覚えておく必要がありますそれは以下のようにすればよいです
saver = tf.train.Saver()
saver.save(sess, "model.ckpt")
余談
今回は説明の通りに実装しました。そのため、プログラムにとって処理しやすい順序にしているわけではありません.例えばsessionはcloseするのではなく,withを使うとか、namescopeの話,データを可視化してくれるtensorboardの話もしていません.また精度の評価もしていません.
最後に全体のソースを添付しておきます.
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
sess = tf.Session() #Sessionの作成
# 定義域の元を定義
x = tf.placeholder(tf.float32, [None, 784])
# 行列の定義
W = tf.Variable(tf.zeros([784, 10]))
# ベクトルの定義
b = tf.Variable(tf.zeros([10]))
# sigmaとしてsoftmax関数を設定し、一つの層として計算
y = tf.nn.softmax(tf.matmul(x, W) + b)
# xの正解の関数fの値を定義
y_ = tf.placeholder(tf.float32, [None, 10])
# 誤差を定義
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
# 誤差を最小化するようにbackpropagationwを実施
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
sess.run(tf.global_variables_initializer()) # Varibleの初期化
for _ in range(10):
batch_xs, batch_ys = mnist.train.next_batch(100)
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
y_output = sess.run(y, feed_dict={x: batch_xs, y_: batch_ys})
print(y_output)
sess.close()
CNN
Deep LearningはCNNを用いた一般物体検出で一気有名になりました.
CNNはConvolutional Neural Networkの略です.
Convolutionとは
数学だと,ConvolutionはFourier変換等を勉強すると現れます.
定義
$f,g :\mathbb{R}^n \to \mathbb{R}$に対し,$f$と$g$のConvolution$fg$を以下で定義する.
$$ fg = \int_{\mathbb{R}^n}f(x-y)g(y)dy $$
Convolutionでは,例えば以下の定理が有名です.
定理
$f,g \in L^1(\mathbb{R}^n)$の時,$f * g \in L^1(\mathbb{R}^n)$であり,$\mathcal{F}(f)$で$f \in L^1(\mathbb{R}^n) $のフーリエ変換を表すとする.この時以下が成り立つ.
$$ \mathcal{F} (f * g ) (z) = \mathcal{F}(f)(z) \cdot \mathcal{F}(g)(z) $$
これは関数同士の畳み込みはフーリエ変換した世界での積になっているということです.
送った先でとても自然な演算になっているものは数学でよく出てきます.
本当は上の意味でのConvolutionとの関係を調べて一般的に記述しようと思ったのですが,よくわかなかった上に調べてる時間がなくなったので、読者の演習問題とします.
画像データ
CNNでは主に画像を対象にしています.
画像がどうデータとして扱われるかは例えばここをみてください.
よくあるものだと,縦×横×チャンネル(RGB)個の0~255の整数として表現されます.
画像は近いもの同士には関係があるだろうということで,縦,横の前後関係保持できる形の多次元配列として定めている物が多いです.
Tensorflowでも画像を多次元配列のようなものとして扱います.
数学的にはテンソル積を取っているだけです.
ただしプログラムで扱うために基底をFixしています.
例えば$\mathbb{R}^n$と$\mathbb{R}^m$の$\mathbb{R}$上のテンソル積の元を標準基底同士から誘導される基底を使って表しています.
縦10ピクセル,横16ピクセル、チャンネル3の画像は
$a \in \mathbb{R}^{10} \otimes \mathbb{R}^{16} \otimes \mathbb{R}^3$
の元として扱います.
これを$e_i.f_j.g_j$をそれぞれの標準基底として取ることで,$a = \sum \alpha_{ijk}e_i \otimes f_j \otimes g_k$と書くことができます
多次元配列としては,$\alpha_{ijk}$を並べたものとして記載されます.
CNNにおけるConvolutionの処理
実際にCNNでConvolutionがどうやって定義されているかを説明します.
行列に対する操作の説明
$m_1 > m_2,n_1 >n_2$とし,$(x_{ij})_{ij} \in M_{m_1n_1}(\mathbb{R})$と$(w_{ij})_{ij} \in M_{m_2 n_2}(\mathbb{R})$に対し、$(a_{ij})_{ij} \in M_{m_1 -m_2 ,n_1 -n_2}(\mathbb{R})$を以下で定義する.
$$ a_{ij} = \sum_{k=0}^{m_2} \sum_{l=0}^{n_2} x_{i+k,j+l}w_{kl} $$
この処理の気持ちは,画像データだと一点に関係あるのは近くの情報だけだろうから,全体で行列を書けるような演算はむしろ無駄で近くだけで演算すればいいよねというものです.
padding
計算の都合上Convolutionをした後の元$(a_{ij})$を$M_{m_1n_1}(\mathbb{R})$の元とみなした方が都合がいい場面があります.
この時そのために0で埋め込む操作をpaddingといいます.
Max pooling
Max poolingという操作もCNNではします.これは近くの中で値が一番大きいものだけを取るというものです.
説明量が膨大になってきたので今回は省きます.
気になる箇所は仕様を調べてみてください.
余談
CNNの定義はここでは述べていません.Convolution,Maxpoolingを少なくとも一度は含むとするべきなのかもしれませんが,ResNetのようにConvolution,Maxpooling,全結合以外の操作もするNNも含めようとすると,関数として定義しにくいものになっています.
おそらくニューラルネットを考えている人の思考やプログラムが数学の関数と対応していないからだと思いますが.このあたりをうまく定式化する方法はないものかなあと悩みます.
TensorflowにおけるConvolutionのメソッド
Tensorflowでもいろんなところで定義されていますが,
例えば以下のメソッドで定義されています.
tf.nn.conv2d
詳細は仕様に記載されていますが、僕はこれだけでもなかなかわからなかったので、動作のメモを記載しておきます.
先程のConvolutionの定義をみると,二次元配列が二つあればいいだけに感じますが,引数はそれよりも多いものです.
conv2d(
input,
filter,
strides,
padding,
use_cudnn_on_gpu=True,
data_format='NHWC',
name=None
)
引数の説明
-
input:
4次元配列です.これは画像の場合は、画像の枚数、縦、横、チャンネル(RGB)の4次元配列になります. -
filter
4次元配列です.縦,横,入力のチャンネル数,出力のチャンネル数の4次元配列です.
私はoutputのチャンネルを変更する理由が理解できていないのですが,そのほうがいいのでしょうか. -
stride
画像をどれだけずらすかを表します. -
padding
"SAME", "VALID"のどちらかを表し.どうやって0を埋めるか等を表します. -
use_cudnn_on_gpu
数式だけを見ているのでは関係ないですが,GPUを使うか否かの指定を表します. -
data_format
入力データのフォーマットを表します.チャンネルがどの次元に入るかを意味しています.
"NHWC", "NCHW"のどちらかを減らべます。特に指定意思ないと"NHWC"になります. -
name
演算に対して名前を定義します.
計算グラフ上名前をつけて区別できるようにしておくのは重要なので,設定されています.
この関数の挙動をいくつか調べます.
調べる時は有名なものを使いたいので,TensorFlowのTutorialをベースに実験します.
strideの挙動
- strides=[2, 1, 1, 1]で実行
InvalidArgumentError (see above for traceback): Current implementation does not yet support strides in the batch and depth dimensions.
[[Node: conv1/Conv2D = Conv2D[T=DT_FLOAT, data_format="NHWC", padding
="SAME", strides=[2, 1, 1, 1], use_cudnn_on_gpu=true, _device="/job:localhost/
replica:0/task:0/gpu:0"](reshape/Reshape, conv1/Variable/read)]]
どうやら,batch方向,ch方向が1でないとエラーになるようです.
いずれサポートされるのか、そもそもサポートしないという方針なのかどちらなんでしょう.
- stdies=[1,2,2,1]で実行すると,
h_conv = h_conv1.eval(session=sess,feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})
print(h_conv.shape)
(50, 14, 14, 32)となりました.
- padding="VALID"の時
def conv2d(x, W):
"""conv2d returns a 2d convolution layer with full stride."""
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='VALID')
の時,
print(h_conv.shape)
(50, 24, 24, 32)
となり,0 paddingされていないことがわかります.
- チャンネル数が実際のどの程度違いをうむのか?
チャンネル数を落としたら精度が落ちることが確認できました.
このあたりを実験しておけば,動作がなんとなくわかってくるのではないでしょうか.
早く使い慣れたいものです.
注意
shapeが合わない行列の計算もエラーになるので,一つを変えて残りも正しく動作させるためには影響箇所を全てサイズを変更する必要があります.
まとめ
量が増えてきたので,今回はこのあたりで終わりにします.
ここでは機械学習の最低限の数式とTensorflowの使い方を紹介しました.
新しいことを学ぶ時は最初の入り口が大変だったりするので,参考になれば嬉しいです.
今後時間が取れたらRNNの紹介と数学や機械学習でお世話になっているArxivから論文を使って遊ぶToy Problemを載せようと思います
P.S.
Arxivで遊んでいるものがありました.しかも,コンピュータと数学のアドベントカレンダーの記事です.