生成モデルpix2pixを動かしてみる
pix2pix is 何
- 2016年11月に発表された、任意の画像を入力にして、それを何らかの形で加工して出力する、というある種の条件付きGAN。
GANって何: 画像等のデータ入力を真似て偽造する生成器と、そのデータが生成されたものか本物かを識別する鑑別器を互いに競わせるように訓練することで、本物によく似たなデータを作り出せるようにする「生成モデルおよびその訓練手法」
- 2014年に公表された当初は訓練の不安定性の問題が大きかったが、Batch Normalizationの導入や条件付けなど安定を増すノウハウが蓄積され、ここ2年注目を浴びている。
- 先週GAN / DCGANについてまとめてみたエントリを書いたので参考になるかも
「画像を入力にして画像を出力にする」ようなタスクは世の中に無数に存在していて、その潜在的な適用範囲の広さが特徴。(着彩、塗り分け、単純化etc..)
下記はpix2pixサイトのサンプル。
より詳細なサンプル例は下記で見られる
pix2pixの構成
大きく3つのネットワークから構成される:
- Encoder: 画像の畳み込みにより、入力画像の特徴量を圧縮する。
- Decoder: Encoderで圧縮された特徴量を逆畳み込み(転置畳み込み)x6層したあと畳み込みx1層により画像に変換する
Discriminator: 「入力画像」と「真の出力サンプルまたはDecoderの出力」2つの画像入力を行う、「本物らしさ」を出力する
(追記) Decoder 層は、直前の層の他に 層のEncoderの出力も同時に受け取る(U-Netというらしい)
- (追記) Discriminatorは、"Patch GAN"と名付けられているようだけどちゃんと読み解けてないです...
3つのネットワークの損失関数:
- Encoder: 「生成された偽画像と真の画像の差異」と「Enc->Decが出力した画像がDiscriminatorに偽物と思われた度合いのsoftplus->バッチ・画素数平均」の重み付き和
- Decoder: Encoderの損失関数と同じ
- Discriminator: 本物の画像(t_out)を偽物と判定した度合い(softplus->バッチ画素平均)と偽物の画像(x_out)を本物と判定した度合いの和
動かしてみる
例によってMattyaさんが例によって神速でChainer実装しているので、今日はそのまま動かしてみる。
- git cloneする
- 訓練用データとなるFacadeという様々な建物の前面部分写真/構成情報のデータを落としておく。 CMP Facade Database
- データの場所、出力の場所、処理するGPU番号等をオプションで指定し起動する
と、たったこれだけ。 (前提環境を整えるのは先回のエントリの記載内容。ただし、chainerは1.19以上が必要)
- 訓練条件は、デフォルトのまま
- ミニバッチサイズ1
- 300枚の画像セットについてランダムな順番で300イテレーションで1エポック
- エポック数200で全訓練完了
結果
入力画像 / 生成画像 / 正解画像
- 20000 イテレーション
入力画像 / 生成画像 / 正解画像
- 30000 イテレーション
入力画像 / 生成画像 / 正解画像
- 40000 イテレーション
入力画像 / 生成画像 / 正解画像
- 50000 イテレーション
入力画像 / 生成画像 / 正解画像
- 59800 イテレーション(200エポック訓練完了)
(生成し損ねた..)
- 損失関数の推移
思ったこととか
1万回目くらいで既に建物にしか見えない画像を生成できるようになってる。まずこれがすごい
Enc / Decの損失関数って、「正解画像との一致度」「Discriminator騙せた度合い」両方損失として使えそうだけど どうやるんだろう... > からの「重みづけして足すだけ」という分かりやすさ。この重み変えると学習結果にどう影響するのか興味ある。 きっと重要なハイパーパラメータ。
回が進むにつれて、画像による明度差が生まれやすくなっている。
- 色相の差が生まれづらいのは、入力画像と建物色の間にはっきりした相関関係が無いから?
- じゃ何で明度差を生み出せたんだろう(?)
- 色相の差が生まれづらいのは、入力画像と建物色の間にはっきりした相関関係が無いから?
まっすぐあるべき線がまっすぐにならないのは他の生成手法でもよくある。(ニューラルネットワークって「まっすぐな線」引くの苦手ですよね...)生成画像の黒ずみと相俟ってスラムみたいな印象に...
損失関数の推移だけみると、まだ学習が収束してないのでepoch数を増やしても良さそう
Mattyaさんの実装で使われていた「CBR層」(畳み込み/BN/ReLU/Dropoutをセットにした層)が便利。
- up / down指定するだけで畳み込み / 逆畳み込み両方に使える(!)
新しいタスクへの適用
- 新しいデータセットにしたいと思って、pixivからイラスト拾い集めてみている(やっと500枚集まった)。うまくいけば着彩タスクを作れるかもしれない
- 未着彩の線画提供 / 着彩させてblogに載せさせてやってもいいぜっていう絵師さんいませんか
Numpy逆引きメモ
- Numpyでよく使う操作のメモ。自分用、随時追記
生成
リストで指定した内容の行列を生成
>>> x = np.array([[0,1],[3,4]]) >>> x array([[0, 1], [3, 4]])
numpy行列からlistへの変換
>>> x = np.array([3,4,5]) >>> x array([3, 4, 5]) >>> list(x) [3, 4, 5]
2次元配列で同じことをすると
>>> x array([[0, 1], [3, 4]]) >>> >>> list(x) [array([0, 1]), array([3, 4])]
となる。もし多重リスト形式に変換したければ、
>>> x = np.array([[0,1],[3,4]]) >>> x.tolist() [[0, 1], [3, 4]] >>> x_list = x.tolist() >>> x_list [[0, 1], [3, 4]]
1で埋めた行列を生成
>>> np.ones([2,3]) array([[ 1., 1., 1.], [ 1., 1., 1.]])
0で埋めた行列を生成
>>> np.zeros([2,3]) array([[ 0., 0., 0.], [ 0., 0., 0.]])
対角行列の生成
>>> np.eye(4) array([[ 1., 0., 0., 0.], [ 0., 1., 0., 0.], [ 0., 0., 1., 0.], [ 0., 0., 0., 1.]])
必ず正方行列になる。
ランダム要素で埋めた行列を生成
一様分布
np.random.uniform(2, -3, (5, 4))
上記の意味: 2から-3の間の値で埋めた5, 4の行列を生成する (4つの値が入ったリストが5つ入っているリスト)
指定区間の数列を生成する
>>> X = np.arange(10) >>> X array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Stepや始点・終点も指定可
>>> X = np.arange(8,20,2) >>> X array([ 8, 10, 12, 14, 16, 18])
指定数がStepの倍数でない場合
>>> X = np.arange(70,40,-11) >>> X array([70, 59, 48])
型指定も可
>>> X = np.arange(10.,dtype=np.float32) >>> X array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.], dtype=float32)
情報取得
行列の要素数を取得する
>>> h = np.array([2,3,4]) >>> h.size 3 >>> h = np.array([[1,2,3],[4,5,6]]) >>> h.size 6
行列の形状を取得する
>>> h = np.array([[1,2,3],[4,5,6]]) >>> h.shape (2, 3)
行列の次元数を取得する
>>> h = np.array([[1,2,3],[4,5,6]]) >>> h.ndim 2
行列のrankを取得する
>>> A array([[1, 2], [3, 4]]) >>> np.linalg.matrix_rank(A) 2
形の一致する二つの行列で値が一致する数をカウントする
>>> y=np.array([1,1,2,3,7]) >>> t=np.array([1,2,3,4,7]) >>> >>> np.sum(y==t) 2
行列から最大値を抽出する
>>> hoge = np.array([[3,4,5],[2,3,4]]) >>> hoge.__class__ <class 'numpy.ndarray'> >>> np.max(hoge) 5
行列から最大値のindexを取り出す
>>> hoge = np.ndarray([[3,4,5],[2,3,4]]) >>> np.argmax(hoge) 2
軸を指定してsumの最大値を取り出す
>>> A = np.array([[3,4,5],[2,3,4]]) >>> np.max(A, axis=0) array([3, 4, 5]) >>> np.max(A, axis=1) array([5, 4])
特定の条件を満たす要素のindexを取得する
>>> a array([ 1, 3, 4, 2, 5, 8, 10]) >>> np.where(a>3) (array([2, 4, 5, 6]),) >>> >>> np.where(a==3) (array([1]),)
形状操作
行列の次元数を追加する
>>> h = np.array([1,2,3]) >>> h array([1, 2, 3]) >>> >>> h = h[np.newaxis,:] >>> h array([[1, 2, 3]]) >>> h = h[:, np.newaxis] >>> h array([[1], [2], [3]])
行列を結合する(連結する)
>>> A array([[1, 2], [3, 4]]) >>> B array([5, 6])
のとき、
縦に連結
>>> np.r_[A,B] array([[1, 2], [3, 4], [5, 6], [7, 8]])
横に連結
>>> np.c_[A,B] array([[1, 2, 5, 6], [3, 4, 7, 8]])
Flattenする
>>> a = np.array([[1,2], [3,4]]) >>> a.flatten() array([1, 2, 3, 4])
行列を切り落とす(y軸)
>>> h array([[1, 2, 3], [3, 4, 5], [4, 5, 6]]) >>> h[1:] array([[3, 4, 5], [4, 5, 6]]) >>> h[:1] array([[1, 2, 3]])
行列を切り落とす(x軸)
>>> h array([[1, 2, 3], [3, 4, 5], [4, 5, 6]]) >>> h[:,1:] array([[2, 3], [4, 5], [5, 6]]) >>> h[:,:2] array([[1, 2], [3, 4], [4, 5]])
軸操作
多重リスト要素の最大値のインデックスをリストで返す
>>> y = [[1,2,3],[5,6,7]] >>> t = [[0,0,1],[0,1,0]] >>> np.argmax(y, axis=1) >>> y = np.argmax(y, axis=1) >>>y array([2, 2]) >>> t = np.argmax(t, axis=1) >>> t array([2, 1])
多次元行列から特定次元だけ抜き出す
data = [ [1 ,0.4834], [2 ,0.5526], [3 ,0.6076], [4 ,0.5436], ... [299 ,0.9718], [300 ,0.97] ]
という2次元行列があるときに、
[1, 2, 3, ... 299, 300]
を抜き出すには
data[: , 0]
[0.4834, 0.5526, 0.6076, ... 0.97]
を抜き出すには
data[:, 1]
とすればよい
他の例:
>>> y array([[0, 1], [2, 3], [4, 5]])
に対して、
>>> y[:,0] array([0, 2, 4]) >>> y[:,1] array([1, 3, 5]) >>> y[:,-1] array([1, 3, 5])
(要素数2なので、前から数えて1番目と後ろから数えて-1番目は同じ)
要素の順番を入れ替える
>>> y array([[0, 1], [2, 3], [4, 5]]) >>> y[:,::-1] array([[1, 0], [3, 2], [5, 4]])
演算
行列の積を求める
>>> X = np.array([[2,3],[3,4]]) >>> Y = np.array([2,2]) >>> X.dot(Y) array([10, 14])
"*"による演算は、要素同士の掛け算になるため注意。
>>> X = np.array([[2,3],[3,4]]) >>> Y = np.array([2,2]) >>> X*Y array([[4, 6], [6, 8]])
行列要素の和を取る
>>> A array([[1, 2], [3, 4]]) >>> np.sum(A) 10
線形代数的操作
行列を転置する
>>> h array([[1, 2, 3], [3, 4, 5], [6, 7, 8]]) >>> np.transpose(h) array([[1, 3, 6], [2, 4, 7], [3, 5, 8]])
もしくは
>>> h array([[1, 2, 3], [3, 4, 5], [6, 7, 8]]) >>> h.transpose() array([[1, 3, 6], [2, 4, 7], [3, 5, 8]])
transposeは、数式に似せて
>>> h.T array([[1, 3, 6], [2, 4, 7], [3, 5, 8]])
と書くこともできる
多次元行列については、引数を与えなければx,y,z => z,y,xのように逆順になるが、 順番を与えることもできる
>>> x.transpose(0,1,2) array([[[ 0, 1, 2], [ 3, 4, 5]], [[ 6, 7, 8], [ 9, 10, 11]]]) >>> x.transpose(0,2,1) array([[[ 0, 3], [ 1, 4], [ 2, 5]], [[ 6, 9], [ 7, 10], [ 8, 11]]]) >>> x.transpose(2,0,1) array([[[ 0, 3], [ 6, 9]], [[ 1, 4], [ 7, 10]], [[ 2, 5], [ 8, 11]]])
行列式を出力する
>>> A array([[1, 2], [3, 4]]) >>> np.linalg.det(A) -2.0000000000000004
>>> P array([[2, 3], [4, 5]]) >>> np.linalg.det(P) -2.0
行列のtraceを出力する
>>> A array([[1, 2], [3, 4]]) >>> np.trace(A) 5
行列の対角要素を抜き出す
>>> A array([[1, 2], [3, 4]]) >>> np.diag(A) array([1, 4])
np.diag(A)は、Aの対角要素を返す。
対角要素から対角行列を生成する
>>> a array([ 1., 2., 3.]) >>> np.diag(a) array([[ 1., 0., 0.], [ 0., 2., 0.], [ 0., 0., 3.]])
ある行列の固有値の対角行列を得る
>>> np.diag(np.linalg.eigvals(A)) array([[-0.37228132, 0. ], [ 0. , 5.37228132]])
行列の固有値を求める
>>> A array([[1, 2], [3, 4]]) >>> np.linalg.eigvals(A) array([-0.37228132, 5.37228132])
行列の固有値、固有ベクトルを求める
>>> A array([[1, 2], [3, 4]]) >>> la, P = np.linalg.eig(A) >>> la array([-0.37228132, 5.37228132]) >>> P array([[-0.82456484, -0.41597356], [ 0.56576746, -0.90937671]])
laが固有値、Pが固有ベクトル。 la[n]がP[n,:]に対応する。
行列の逆行列を作る
>>> np.linalg.inv(A) array([[-2. , 1. ], [ 1.5, -0.5]])
行列の対角化を行う
>>> A array([[ 0, 14, 2], [-1, 9, -1], [-2, 4, 8]])
のとき、
>>> l, P = np.linalg.eig(A) >>> l array([ 4., 6., 7.]) >>> P array([[ 9.42809042e-01, -9.12870929e-01, -8.94427191e-01], [ 2.35702260e-01, -3.65148372e-01, -4.47213595e-01], [ 2.35702260e-01, -1.82574186e-01, 2.25257672e-15]])
lが固有値、Pが固有ベクトルを組み合わせて作った行列。 l[n]がP[n,:]に対応する(nは0,1,2)
固有値を対角行列化する
>>> D = np.diag(l) >>> D array([[ 4., 0., 0.], [ 0., 6., 0.], [ 0., 0., 7.]])
PDP-1がAと一致することを確認
>>> P.dot(np.diag(l)).dot(np.linalg.inv(P)) array([[ 9.79179023e-16, 1.40000000e+01, 2.00000000e+00], [ -1.00000000e+00, 9.00000000e+00, -1.00000000e+00], [ -2.00000000e+00, 4.00000000e+00, 8.00000000e+00]])
ベクトルの内積を求める
>>> x array([1, 2, 3]) >>> y array([3, 1, 0]) >>> x.dot(y) 5
行列のノルムを求める
>>> x array([1, 2, 3]) >>> np.linalg.norm(x) 3.7416573867739413
ベクトル同士のcos類似度を求める
内積を双方のノルムで割れば良いので
>>> x array([1, 2, 3]) >>> y array([3, 1, 0]) >>> x.dot(y) / (np.linalg.norm(x) * np.linalg.norm(y)) 0.42257712736425829
その他よく使うイディオム
多次元配列の要素をイテレーションする
配列の要素のインデックスを順番に取得できる
>>> x array([[0, 1], [3, 4]]) >>> it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite']) >>> it <numpy.nditer object at 0x10a444c10> >>> >>> it.multi_index (0, 0) >>> it.iternext() True >>> it.multi_index (0, 1) >>> it.iternext() True >>> it.multi_index (1, 0) >>> it.iternext() True >>> it.multi_index (1, 1) >>> it.iternext() False
行列要素の型を変換する
>>> A array([[1, 2], [3, 4]]) >>> A.astype("float32") array([[ 1., 2.], [ 3., 4.]], dtype=float32)
(わかりづらいが、小数点が付いてfloat型になっている)
map適用相当の関数適用
x = np.array([1, 2, 3, 4, 5]) squarer = lambda t: t ** 2 squares = np.array([squarer(xi) for xi in x])
printしたときに省略せずに表示する
np.set_printoptions(threshold=np.inf)
をコードのどこかで宣言したあとでprint
Chainer環境をAWSのUbuntu16.04+CUDA8.0上に構築する
目的
AWSのUbuntu 16.04上にChainer環境を構築する
構成
- OS: Ubuntu 16.04 (AMI ID: ami-0567c164)
- CUDA 8.0
- Cudnn8.0
- Chainer 1.18
手順
cudaのインストール
$ wget https://developer.nvidia.com/compute/machine-learning/cudnn/secure/v5.1/prod/8.0/cudnn-8.0-linux-x64-v5.1-tgz $ sudo dpkg -i cuda-repo-ubuntu1604_8.0.44-1_amd64.deb $ sudo apt-get update $ sudo apt-get install cuda
cudnnのインストール
agreementのせいでwgetで落とせないので、 https://developer.nvidia.com/rdp/cudnn-download から一旦PCに落としてscpでupした。
$ tar zxvf cudnn-8.0-linux-x64-v5.1.tgz
cudaディレクトリが解凍されるので、
$ sudo cp -r cuda/include/* /usr/local/cuda/include $ sudo cp -r cuda/lib64/* /usr/local/cuda/lib64
として、既存の/usr/local/cuda配下に配置する
cudaへのパスを通す
/usr/local/cuda/bin と /usr/local/cuda/lib64 にパスを通すために、
export PATH="/usr/local/cuda/bin:$PATH" export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
を.bashrc (ログインシェル環境なら.bash_profileに)追記する
linux-genericのインストール
AWSのイメージには、GPUを動かすのに必要なlinux-genericがインストールされていない という情報があったため、実施。(やらなくても動くかも?未検証)
$ sudo apt install linux-generic $ sudo reboot (再起動後) $ sudo apt remove linux-virtual $ sudo apt remove linux-virtual $ sudo apt autoremove
pipのインストール / アップグレード
$ sudo apt install python-pip $ pip install --upgrade pip
pyenvで必要になるので、opensslをインストール
$ sudo apt-get install libssl-dev
pyenv環境の構築
- Chainer等のバージョンアップが今後多くなることを想定し、pyenv環境を作っておく。
$ cd ~ $ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ~/.bashrc echo 'if [ -d "${PYENV_ROOT}" ]; then' >> ~/.bashrc echo ' export PATH=${PYENV_ROOT}/bin:$PATH' >> ~/.bashrc echo ' export PATH=${PYENV_ROOT}/shims:$PATH' >> ~/.bashrc echo ' eval "$(pyenv init -)"' >> ~/.bashrc echo 'fi' >> ~/.bashrc
- その場でも読み込んでおく(再ログインでも可)
$ source ~/.bashrc
上記は、インタラクティブシェルの場合。ログインシェル環境なら.bash_profileに追記し、source ~/.bash_profileする
- pyenv-virtualenvもインストールする
$ cd ~/.pyenv/plugins $ git clone git://github.com/yyuu/pyenv-virtualenv.git
- pyenvで利用するpythonをインストール(インストールできるものはpyenv install -l で確認)
$ pyenv install 3.5.2
- インストール完了の確認
$ pyenv versions * system (set by /home/ubuntu/.pyenv/version) 3.5.2
- pipもpyenv化されていることを確認する
$ which pip /home/ubuntu/.pyenv/shims/pip
/home/ubuntu/.local/bin/pip となっていたらおそらくパス設定をミスしている。
- 利用するpyenvを設定する
$ pyenv global 3.5.2
- 念のためログインし直しても同じ状態をキープしているか確認しておく
$ pyenv versions system * 3.5.2 (set by /home/ubuntu/.pyenv/version)
pip パッケージのインストール
下記の内容をrequirements.txtとして保存して、依存パッケージのインストール
appdirs==1.4.0 chainer==1.18.0 cupy==1.0.0 cycler==0.10.0 decorator==4.0.10 filelock==2.0.7 matplotlib==1.5.3 nose==1.3.7 numpy==1.11.2 Pillow==3.4.2 protobuf==3.1.0.post1 py==1.4.31 pyparsing==2.1.10 pytest==3.0.4 python-dateutil==2.6.0 pytools==2016.2.4 pytz==2016.7 six==1.10.0
Pillowとかは最初なくてもよいかも。
$ pip install -r requirements.txt
これで、3.5.2 env配下にpipがインストールされた。chainerのバージョンをあげたりした場合は、 envを消して別のenvを定義して再インストールすればよい。
動作確認
- CUDAの動作確認を行う。
$ python >>> import chainer >>> chainer.cuda.available True >>> chainer.cuda.cudnn_enabled True
- 公式サンプルを落として、実際に動かしてみる。
$ git clone https://github.com/pfnet/chainer.git $ cd chainer/examples/mnist/ $ python train_mnist.py -g=0
GPUなし: real 12m44.315s user 30m1.920s sys 60m23.720s
GPUあり: real 1m20.846s user 1m22.468s sys 0m1.708s
10倍速になっており、確かにCUDAによる高速化が実現できている。
お掃除
不要なディレクトリやファイルを削除する
$ cd ~ $ rm cuda-repo-ubuntu1604_8.0.44-1_amd64.deb $ rm cudnn-8.0-linux-x64-v5.1.tgz $ rm -r cuda $ rm requirements.txt
経緯:
AWSでGPU演算環境を確保しようと思い、適切なAMIを探していた。CUDAが最初からインストールされたAMIを使おうとしたらスポットリクエストには対応していなかったため、デフォルトのUbuntuでAMIを作成するのが結局割安に付きそうだったためクリーンインストールしてマイAMIを作成した
できるだけ丁寧にGANとDCGANを理解する
目的
- Chainerの扱いに慣れてきたので、ニューラルネットワークを使った画像生成に手を出してみたい
- いろいろな手法が提案されているが、まずは今年始めに話題になったDCGANを実際に試してみるたい
- そのために、 DCGANをできるだけ丁寧に理解することがこのエントリの目的
- 将来GAN / DCGANを触る人の助けになったり、理解間違ってるところにツッコミがあると嬉しい
本エントリの構成
- DCGANの前提となっているGANの論文の要点をまとめる
- DCGANでGANとの差分として提案されている要点をまとめる
- DCGANのmattyaさんの実装を読み通して詳細を理解する
1. GANについて
- GANは、サンプル群と似たような性質を持つ出力を生成するためのフレームワーク
- 2014年にIan J. Goodfellowらによって提案された
論文: Generative Adversarial Nets [リンク]
以下の2つのモデルの訓練を同時に進め、互いに競わせる
- D: Discriminator(鑑別器): (生成したいサンプルとGの出力物を正しく鑑別できることを目指す)
- G: Generator(生成器):(ランダムノイズを入力として、Dが誤ってサンプルであると認識する率を高めることを目指す)
- GANは下記の式の価値関数V(G, D)で表現されるminimaxゲームとして定義できる
- この式を、前提含め日本語で書き下してみると
[前提] ・はある種の任意の分布(例えば一様分布)を表す。zは個々のノイズサンプルを表す。 ・Gはzを入力とし、に分布させる。はジェネレータGから生成された出力の分布を示す ・Dは入力がサンプルから来た確率を表す(1であれば入力はサンプル分布から、0であればGの出力分布からと判断) [左辺] ・価値関数Vは関数DとGを引数に取る、右辺で表される関数である ・Dについての最大、Gについての最小となるようD, Gを定める [右辺] ・確率変数xは確率分布に従う ・確率変数zは確率分布に従う ・このとき、の期待値と、の期待値の和を評価関数とする (Dがサンプルを正しくサンプルと判定できればが大きくなり、DがGの出力をサンプルだと判定するとが小さくなる)
となる。(と思うのですが、間違っていたらご指摘いただければ嬉しいです..)
この式自体は、G、Dがニューラルネットワークであることを前提とはしていない(言い換えれば、別な関数最適化手法であっても適用できる、かもしれない)
論文ではD, Gにニューラルネットワークを使うことで、既存の最尤推定による生成モデルで手に負えないほど計算量が増える問題をbackpropagationで回避できるとしている
論文掲載のアルゴリズムは下記となる
- ミニバッチサイズm個のノイズをから取り出す(生成する)
(論文はからになってるけど、の誤植のような..?) - ミニバッチサイズm個のサンプルをデータ生成分布から取り出す
- 下記式の、における確率的勾配を上るように鑑別器Dを更新する
- 上記までをk回繰り返す
- ミニバッチサイズm個のノイズをから取り出す (ここもが正しいような..?)
- 下記式の、における確率的勾配を下るように生成器Gを更新する
- ここまで全てを、訓練回数分だけ繰り返す
- ミニバッチサイズm個のノイズをから取り出す(生成する)
鑑別器Dを十分な回数(k回)更新した上で生成器Gを1回更新することで、常に鑑別器が新しいGの状態に適用できるように学習を進める
4.1 ~ 4.2 [tex: p_g = p{data} ] の時にD, Gそれぞれについての最適化が達成される
==> このため、 を [tex: p{data} ] に近似させることが上記評価関数の解への近似として正当化される利点と欠点
- 欠点: 明示的なが最初は存在せず、DはGとシンクロさせて訓練しなければならない (特に、DをupdateせずにGだけを訓練すると、Gが入力ノイズzの多くをxと同じ値に収束させてしまう点に注意)
- 利点:
- マルコフ鎖で複数のモデルを混ぜるためにぼやけたものになるが、GANではマルコフ鎖が不要でシャープな画像が生成できる。
- 勾配を得るためにBPが使えるため、学習に近似が不要。
- 様々なモデルを用いることができる
- そして何より、「計算可能(computational)である」。
- サンプルと直接比較するのではなく、Discriminatorの評価を介して生成するため、inputの部品をGがそのまま丸覚えすることを避けられる。
2. DCGANについて
- GANは具体的なネットワークの構成に言及していない。(少なくとも論文中では)
- DCGAN(Deep Convolutional Generative Adversarial Networks) は、GANに対して畳み込みニューラルネットワークを適用して、うまく学習が成立するベストプラクティスについて提案したもの。
- 元になった論文 Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks リンク
- 要点をまとめると下記のようになる
- プーリング層を全て下記で置き換える
- バッチノルムを使用する(重要らしい)
- 深い構成では全結合層を除去する
- 生成器ではReLUを出力層以外の全ての層で活性化関数として使用し、出力層ではtanhを使用する
- 識別器ではLeakyReLUを全ての層で使用する
3. DCGANのコードを読む
- ChainerでのmattyaさんによるDCGAN実装を見て、実際にどのように構成されているかを確認する
前処理
- import / 定数宣言は解説省略
- image_dirから全てのイメージを読み込んで、dataset配列に追加している。
- ELU: exponential Linear Unitの定義。 数式でいうとの元で
グラフでいうと
(出典: Djork-Arne Clevert, Thomas Unterthiner & Sepp Hochreiter 2016 https://arxiv.org/pdf/1511.07289v5.pdf )
となるもので、LeakyReLUをなめらかにした感じのものらしい。
Generatorの定義:
- 一様分布ノイズz(1次元 / 100要素)を入力として、
=> 100入力、66512 出力のLinear Unit で(100, 512, 6, 6 )に=> BatchNormalization => relu => => 512チャネル入力、256チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D で(100, 256, 12, 12)に=> BN => relu
=> 256チャネル入力、128チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D で(100, 128, 24, 24)に=> BN => relu
=> 128チャネル入力、64チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D で(100, 64, 48, 48)に=> BN => relu
=> 64チャネル入力、3チャネル出力、pad1、stride2、フィルタサイズ4のDeconvolution2D
として、最終的に(100, 3, 96, 96) (つまり、ミニバッチ100枚 x RGB3チャネル x w96 x h96)のデータが得られる。
Discriminatorの定義:
- サンプルイメージ or Gの出力(3, 96, 96)を入力として、
=> 3チャネル入力、64チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで (64, 48, 48)に => 上述のELU
=> 64チャネル入力、128チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで(128, 24, 24)に => BN => ELU
=> 128チャネル入力、256チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで(256, 12, 12)に => BN => ELU
=> 256チャネル入力、512チャネル出力、pad1、stride2、フィルタサイズ4のConvolution2Dで(516, 6, 6)に => BN => ELU
=> 512 x 6 x 6 入力、2出力の全結合層
訓練
- Gの訓練
- Dの訓練
- x2をDの入力にして、yl2を出力。
- Dの損失に「yl2とbatch数だけ並んだ0とのソフトマックスクロスエントロピー関数出力」を足し合わせる
- 勾配初期化とbackpropagateによる重み更新
- image_save_interval回の訓練毎にGENを使って画像を100枚。
この際の生成のタネにする乱数は訓練開始前に生成したzvisを常に利用する
- 毎epoch完了ごとにdis, gen, o_dis, o_genを保存
- 諸々のインスタンスを生成して訓練を起動する
わからなかった点
- 何故Discriminatorが「2つ」の出力を持つようにしているのかわからない。入力のかららしさだけを出力するのなら、1出力でよさそうに思えてしまう。損失関数の計算方法から見るに、どちらか1つの要素がG、もう一つがdataからの入力というわけでもなさそうに思える。
その他
- なんちゃって!DCGANでコンピュータがリアルな絵を描く - PlayGround が参考になりました。ありがとうございます。
- Deconvolutionの処理、transposed(転置) convolutionと呼ばれる理由などはtheanoのドキュメントがわかりやすかった。気が向いたらまとめてみる
Chainerでフォントを識別してみる (2)
前回、二つのフォントをいい感じに識別できていたので、識別対象フォントを12に増やしてみた。
今回識別対象にしたフォントたち
Helvetica
Bodoni
Futura
Optima
Clarendon
GillSans
Caslon
Garamond
Avenir
Times New Roman
American Typewriter
Trajan
訓練データとテストデータの構成
- 訓練データは[a-mA-M]の64x64 グレースケール画像。画像ごとに位置はランダム
- テストデータは[n-zN-Z]の64x64 グレースケール画像。同じく画像ごとに位置はランダム化してある
実験
試行1: 3層多層パーセプトロン(隠れ層ニューロン数100)
前回使ったのと々、隠れ層2層、出力層1層の多層パーセプトロン。
=> 87%程度で識別率が頭打ちになった。2クラスへの識別だけであれば、同条件で97%まで行っていたので、
12クラスへの識別になって認識率が落ちたことが確認できる。
試行2: 3層多層パーセプトロン(隠れ層ニューロン数200)
2つの隠れ層の表現能力が足りないのかもしれないと思い、ニューロン数を200に増やしてみたパターン。
=> 90%程度で頭打ち。試行1よりかすかに識別率が上がった
試行3: CNN
CNN(畳み込みニューラルネットワーク)を導入。畳み込み+relu+Max Poolingを3セットの後にLinear層を2層。
epoch数100を超えたあたりで識別率が99.9%以上となった。
今後
・十分な汎化性能がありそうなので、多種多様な日本語フォントの識別を行ってみる
・各フォントにはItalicやBoldなどいろいろなスタイルもあるため、スタイル変更した際にも識別を行えるか試す
・スタイル転写とか使って、英字フォントから日本語フォント作れたら面白い気がする
Chainerでフォントを識別してみる (1)
Chainerで何か分類してみるにあたって、
1. 分類できて嬉しい
2. サンプル収集の目処が立ちそう
3. 莫大なデータ量にならない
な分類対象として、文字のフォントの分類をしてみようと考えた。
2. データセットの作成
- 大文字もしくは小文字を一文字ずつ64*64の画像に印字したデータを作成する。これより画像が小さいと、ジャギが立ってしまうので、フォント判別に良く無いと判断した
- 用意するデータ毎にx位置y位置をずらしておく。(フォントによって、ベースライン位置が異なっていたりするため、これを手掛かりにさせず、あくまで字形で分類させたいため)
- Train用のデータセットでは[a-mA-M]、Test用のデータセットでは[n-zN-Z]の文字を用いる
train用
test用
(※データをちゃんと作れたか確認のため画像出力したが、実際にはメモリ上でリスト保持するのみ)
- ChainerのOptimizer、Trainerを用いる事ができるように、DatasetMixinを継承したクラスとしてデータセットクラスを作成した。
(DataMixinを継承したクラスで__len__とget_exampleだけ定義してあげればChainerのIterationに渡せるようになる)
- Datasetのコンストラクト引数で、サンプル数とtrain / testの指定を行えるようにした
import sys import random import numpy as np from PIL import ImageFont from PIL import Image from PIL import ImageDraw import chainer from chainer import cuda, Function, gradient_check, report, training, utils, Variable from chainer import datasets, iterators, optimizers, serializers from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L from chainer.training import extensions class FontImageDataset(chainer.dataset.DatasetMixin): def __init__(self, datanum=10, normalize=True, flatten=True, train=True): self._normalize = normalize self._flatten = flatten self._train = train pairs = [] for _ in range(datanum): image_array, label = self.generate_image() pairs.append([image_array, label]) self._pairs = pairs def __len__(self): return len(self._pairs) def generate_image(self): fonts = [ 'font_files/Helvetica.ttf', 'font_files/BodoniSvtyTwoITCTT-Book.ttf', 'font_files/Futura-Medium.ttf', 'font_files/Optima-Regular.ttf' ] label = random.randint(0,len(fonts)-1) fontFile = fonts[label] font = ImageFont.truetype(fontFile, 60) train_characters = [ 'A','B','C','D','E','F','G','H','I','J','K','L','M', 'a','b','c','d','e','f','g','h','i','j','k','l','m' ] test_characters = [ 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z', 'n','o','p','q','r','s','t','u','v','w','x','y','z' ] text = '' if self._train: text = random.choice(train_characters) else: text = random.choice(test_characters) w, h = 64, 64 text_w, text_h = font.getsize(text) text_x, text_y = (w - text_w) * random.random(), (h - text_h) * random.random() im = Image.new('L', (w, h), 255) draw = ImageDraw.Draw(im) draw.text((text_x, text_y), text, fill=(0), font=font) #im.save('image' + str(random.randint(0, 100)) + '.png') #if self._train: # im.save('temp/image_train' + str(random.randint(0, 100)) + '.png') #else: # im.save('temp/image_test' + str(random.randint(0, 100)) + '.png') image_array = np.asarray(im) if self._normalize: image_array = image_array / np.max(image_array) if self._flatten: image_array = image_array.flatten() # type cast image_array = image_array.astype('float32') label = np.int32(label) return image_array, label def get_example(self, i): image_array, label = self._pairs[i][0], self._pairs[i][1] return image_array, label
3. 学習モデルと訓練
ほとんどまだチュートリアルのまま回した感じ。これから分類クラス数を増やしたり
モデルを差し替えたりハイパーパラメータをあれこれして楽しみたい
- モデル: Chainerチュートリアルそのまんまのアフィン3層MLP
- ミニバッチサイズ200、epoch数300のSGDで訓練。
import numpy as np import chainer from chainer import cuda, Function, gradient_check, report, training, utils, Variable from chainer import datasets, iterators, optimizers, serializers from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L from chainer.training import extensions from font_image_dataset import * train_data = FontImageDataset(5000, train=True) test_data = FontImageDataset(5000, train=False) train_iter = iterators.SerialIterator(train_data, batch_size=200, shuffle=True) test_iter = iterators.SerialIterator(test_data, batch_size=200, repeat=False, shuffle=False) class MLP(Chain): def __init__(self, n_units, n_out): super(MLP, self).__init__( l1 = L.Linear(None, n_units), l2 = L.Linear(None, n_units), l3 = L.Linear(None, n_out) ) def __call__(self, x): h1 = F.relu(self.l1(x)) h2 = F.relu(self.l2(x)) y = self.l3(h2) return y class Classifier(Chain): def __init__(self, predictor): super(Classifier, self).__init__(predictor=predictor) def __call__(self, x, t): y = self.predictor(x) loss = F.softmax_cross_entropy(y, t) accuracy = F.accuracy(y, t) report({'loss': loss, 'accuracy': accuracy}, self) return loss model = L.Classifier(MLP(100, 2)) optimizer = optimizers.SGD() optimizer.setup(model) updater = training.StandardUpdater(train_iter, optimizer, device=-1) trainer = training.Trainer(updater, (300, 'epoch'), out='result') print("start running") trainer.extend(extensions.Evaluator(test_iter, model)) trainer.extend(extensions.LogReport()) trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy'])) trainer.extend(extensions.ProgressBar()) trainer.run() print("end running")
5. この後
- 分類クラス数を増やしてみると識別率上限が低くなると思うのでいろんなモデルを試して見る
「ゼロから作るDeep Learning」を読み終わった
「ゼロから作るDeep Learning」を読み終わった。
ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装
- 作者: 斎藤康毅
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/09/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (5件) を見る
Pros:
1ニューロンのパーセプトロンを作るところから始めて、3層NN、誤差逆伝搬法、CNNまで手作りしていく。
理論と実践のバランスが良く、該当分野の入門書としてとてもお勧めできると感じた。
中でも誤差逆伝搬法の説明が懇切丁寧で、他のNeural Network本を読んで
「で、全結合層以外の誤差逆伝搬はどうやって考えればいいんだ..」と悩んでいた自分にはぴったりだった。
あと、NNへ学習を導入する流れについても
=> 学習するっていうのはつまりどういうことか
=> 微分とは何か、勾配とは何か
=> 数値微分があればどんな関数でも勾配計算できるんだぜ
=> これでSGDすれば学習できるよ
=> でも遅いよね
=> じゃあ誤差逆伝搬を使ってみよう
という感じで、目的と手段をちゃんと切り分けて解説をしているところがとてもわかりやすい。
Cons:
贅沢を言えば、CNN層、Pooling層の逆伝搬についても解説があると嬉しかった。
(※ちなみに、入門用のもう一つのお勧めはオンラインテキスト "Neural networks and deep learning" 途中まで日本語版はこちら。こちらは、「なぜニューラルネットワークが画像を認識できるのか」の解説がとてもわかりやすかった。)