ゼロから作るDeep Learning ~Pythonで学ぶディープラーニングの理論と実装~
https://www.oreilly.co.jp/books/9784873117584/
https://github.com/oreilly-japan/deep-learning-from-scratch
「ゼロから作るDeep Learning」の序章ではPythonの使い方について記述されています。
この記事は書籍の1章におおよそ沿いながらJuliaについて書いたものです。Julia使い方を紹介していきます。
Deep Learningやニューラルネットワークを学ぶ予定がない方でもJuliaの使い方については参考になるかと思います。
文中に含まれるPythonコードや配布されているコードをすべてJuliaへと書き換えたリポジトリも作りました。
書籍の本文についてはほとんど記載していませんが、実際にJuliaでコードを書き進めるうちに気が付いたPythonとの違いやJuliaで使えるテクニックなどを都度まとめているので、ゼロから作るDeep LearningをJuliaで学んでみたい方は覗いてみてください。
Julia入門
Juliaは、PythonやMatlabのように動的型付けに対応しながらC言語のように早く動作させることができるほか、行列演算なども標準でサポートされており科学計算にも向いている。Juliaのメリット
新興プログラミング言語であるため普及率は高くないが今後その応用に期待されている。Juliaのインストール方法などについて簡単にまとめる。
1 Juliaとは
Juliaは速さとか書きやすさとかなんかすごいらしいですよ。ホームページに書いてあった(Julia in a Nutshell)。
Juliaはまだ普及段階ですが、某大学の線形代数の授業で使用されていたり、Google Colabで使用できる裏技が用意されていたり、ITのプロフェッショナル達からこっそり注目を集めています。
行列の演算や高速な数値計算を行うとき、PythonではおまじないのようにNumpyをインポートしていましたがJuliaではその必要はありません。基本的な行列演算はデフォルトでサポートされており、一部の統計処理や線形代数的な計算はパッケージをインポートする必要はありますがそのパッケージも標準で搭載してあります。また、パッケージマネージャもJulia自身に内蔵されており、Pythonの様にpipとcondaどちらを使うべきか、pipとconda一方でしかインストールできないパッケージがあったり環境が混ざって壊れてしまうという心配がありません。
むしろ、Pythonを使いこなしている人からすれば、Pythonに加えてJuliaのパッケージも使用できて、処理によってはPythonよりも高速に動作させることができるでしょう。
一方で、JuliaはC言語のように早く動作すると謳ってはいるものの、プログラマの記述方法に依存している、つまり、Julia言語らしい早く動作する書き方が存在しており実際に動作速度を向上させるためにはJulia言語独特の仕様を理解する必要があります。
JuliaではJulia言語自体を用いて開発されているFluxというディープラーニングパッケージもあり、研究開発やデータサイエンスといった分野で将来存分に活躍しうるポテンシャルを持っているといえるでしょう。
2 Juliaのインストール
ここではJuliaのインストールに際して注意する点を記述します。
このリポジトリで用いる環境は次の通りです。
- Julia 1.6.1
- Plots (backend GR)
- HDF5
- OrderedCollections
Plotsについては後述します。
PyPlotなどを用いたい場合は適宜読み替えてください。
これから初めてJuliaをインストールするという方を対象に、Juliaのインストール方法を説明します。
上記の要件を満たしている方は読み飛ばしてください。
2.1 Juliaのバージョン
Juliaのバージョンは執筆時点で最新のVer.1.6.1を使用しています(2021/07)。
JuliaのダウンロードページからOSに対応したバイナリをダウンロードしてインストールしましょう。
インストール後はパスを通すために以下の環境変数を作成しPATHに追加してください。
-
JULIA_BINDIR
:Juliaをインストールしたディレクトリ
/julia-1.6.1/bin
Juliaにて設定できる環境変数の詳細はDocumentsを参照してください。
https://docs.julialang.org/en/v1/manual/environment-variables/
SSDに細かいファイルを大量に書き込みたくない!という方は環境変数JULIA_DEPOT_PATH
を追加することで設定ファイルやパッケージがインストールされるディレクトリを変更することができます。
2.2 使用する外部ライブラリ
書籍の目標とすることは、ゼロからディープラーニングを実装することです。
そのため、外部のライブラリは極力使用しないというのが方針ですが、
PlotsとHDF5およびOrderedCollectionsは例外として用いることにします。
Plots はグラフ描画のためのライブラリです。
Plots を用いれば、実験結果の可視化や、また、ディープラーニングの実行途中のデータを視覚的に確認することができます。
HDF5 はデータを保存するためのパッケージです。
Juliaの計算で取り扱うデータや数値を保存したり、また再利用するために用います。
OrderedCollections は辞書型(Dict)や集合(Set)の拡張型を追加するパッケージです。
Juliaの辞書型で内容の順序を指定するために用います。
3 Juliaインタプリンタ
Juliaをインストールしたら、Juliaのバージョンをまず初めに確認します。
ターミナル(Windowsの場合はコマンドプロンプト)を開き、julia --version
というコマンドを入力してみましょう。
このコマンドは、インストールされたJuliaのバージョンを出力します。
$ julia --version
julia version 1.6.1
上のように、julia version 1.6.1と表示されていたら、正常にインストールされています。
続いて、julia
と入力し、Juliaインタプリンタを起動します。
$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.6.1 (2021-04-23)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia>
Juliaインタプリンタは「インタラクティブモード」とも呼ばれ、ユーザーとJuliaが対話的にプログラミングを行うことができます。
対話的というのは、たとえば、ユーザーが「1+2は?」と尋ねれば、Juliaインタプリンタが「3です」と答えるようなやり取りが行えることを意味します。
それでは、実際に入力してみます。
julia> 1 + 2
3
このように、Juliaインタプリンタでは、対話的に(インタラクティブに)プログラミングを行うことができます。
ここでは、このインタラクティブモードを使って、Juliaプログラミングの簡単な例を見ていくことにします。
3.1 算術計算
加算や乗算などの算術計算は、次のように行います。
julia> 1 - 2
-1
julia> 4 * 5
20
julia> 7 / 5
1.4
julia> 3 ^ 2
9
*
は乗算、/
は除算、^
は累乗を意味します( 3 ^ 2
は3の2乗)。
3.2 データ型
プログラミングではデータ型(data type)というものがあります。
データ型とは、データの性質を表すもので、たとえば、整数、少数、文字列といった型があります。
Juliaではtypeof()
という関数があり、この関数でデータの型を調べることができます。
julia> typeof(10)
Int64
julia> typeof(2.718)
Float64
julia> typeof("hello")
String
上の結果より、10はInt64
(64bit整数型)、2.718はFloat64
(64bit浮動小数点型)、"hello"はString
(文字列型)という型であることがわかります。
3.3 変数
xやyなどのアルファベットを使って変数(variable)を定義することができます。
また、変数を使って計算したり、変数に別の値を代入したりすることもできます。
julia> x = 10; # 初期化
julia> print(x) # xを表示する
10
julia> x = 100; # 代入
julia> print(x)
100
julia> y = 5;
julia> x + y
105
なお、「;
」は処理の区切りを表し、1行中に複数の処理を記述したい場合や結果を出力せずにコマンドを実行したいときに利用します。
「#
」はコメントアウトと言い、それ以降の文字はJuliaからは無視されます。
3.4 配列の生成
ディープラーニングの実装では、配列や行列の計算が多く登場します。
ここでは、これから先使用していく配列(行列)について簡単に説明します。
julia> a = [1 2 3 4 5]; # 配列の作成
julia> print(a)
[1, 2, 3, 4, 5]
julia> length(a) # 配列の長さを取得
5
julia> a[1] # 最初の要素にアクセス
1
julia> a[5]
5
julia> a[5] = 99 # 値を代入
99
julia> print(a)
[1, 2, 3, 4, 99]
要素へのアクセスはa[1]
のように行います。この [ ] の中にある数を、インデックス(添字)と呼び、インデックスは1からスタートします(インデックスの1が最初の要素に対応します)。
また、Juliaの配列にはスライシング(Juliaの場合は異なるかも?)という便利な記法が用意されています。
スライシングを用いれば、単一の値へのアクセスだけでなく、配列のサブリスト(部分配列)にアクセスすることができます。
julia> print(a)
[1, 2, 3, 4, 99]
julia> print(a[1:2]) # インデックスの1番目から2番目まで取得
[1, 2]
julia> print(a[2:end]) # インデックスの2番目から最後まで取得
[2, 3, 4, 99]
julia> print(a[begin:3]) # 最初からインデックスの3番目まで取得
[1, 2, 3]
julia> print(a[1:end-1]) # 最初から最後の要素の1つ前まで取得
[1, 2, 3, 4]
julia> print(a[begin:end-2]) # 最初から最後の要素の2つ前まで取得
[1, 2, 3]
リストのスライシングを行うには、a[1:2]
のように書きます。
a[1:2]
により、インデックスの1番目から2番目までの要素が取り出されます。
なお、end
を指定することで最後の要素、end-1
は最後から1つ前の要素に対応します。
begin
を指定することで最初の要素を取得できますが、1と実質同義となります。
3.5 N次元配列
Juliaでは、1次元の配列だけでなく、多次元の配列も作成することができます。
たとえば、2次元配列は次のように作成できます。
julia> A = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> size(A)
(2, 3)
julia> eltype(A)
Int64
julia> typeof(A)
Matrix{Int64} (alias for Array{Int64, 2})
julia> typeof([1.0, 2.0, 3.0])
Vector{Float64} (alias for Array{Float64, 1})
ここでは、2×3のA
という行列を作成しました。
なお、行列A
の形状はsize()
関数で、行列A
の要素のデータ型はeltype()
で参照することができます。
また、配列の型はArray
型ですが、1次元配列の場合Vector
型、2次元配列の場合はMatrix
型というエイリアスがあり、同じものとして扱われます。
julia> typeof([1, 2, 3]) # カンマ区切り
Vector{Int64} (alias for Array{Int64, 1})
julia> typeof([1 2 3]) # スペース区切り
Matrix{Int64} (alias for Array{Int64, 2})
julia> typeof([1 2 3, 4 5 6])
ERROR: syntax: unexpected comma in matrix expression
Stacktrace:
[1] top-level scope
@ none:1
julia> typeof([1 2 3; 4 5 6]) # セミコロンで改行
Matrix{Int64} (alias for Array{Int64, 2})
配列を作成する際、要素を「,
(カンマ)」で区切るとVector配列となります。
スペースで区切ると列方向に並ぶ(1行N列の)Matrix配列となります。
複数行のMatrix配列を作成する場合は、「;
(セミコロン)」で区切ることで行を追加することができます。
3.6 配列の算術演算
配列の算術計算の例を示します。
julia> X = [1 2; 3 4]; Y = [3 0; 0 6];
julia> X + Y
2×2 Matrix{Int64}:
4 2
3 10
julia> X * Y
2×2 Matrix{Int64}:
3 12
9 24
julia> X / Y
2×2 Matrix{Float64}:
0.333333 0.333333
1.0 0.666667
julia> X * inv(Y)
2×2 Matrix{Float64}:
0.333333 0.333333
1.0 0.666667
配列の算術演算は基本的に、数学的な行列演算に従います。
ただし、行列に対してスカラ値で算術演算を行う場合や、行列同士で要素ごと乗除算などを行う場合はブロードキャスト演算子を用いる必要があります。
3.7 ブロードキャスト
算術演算を行う際に各演算子の前に.
(ドット)を付けるとブロードキャスト演算子として扱われます。
julia> A = [1 2; 3 4];
julia> A .+ 10
2×2 Matrix{Int64}:
11 12
13 14
julia> A .* 10
2×2 Matrix{Int64}:
10 20
30 40
julia> B = [10 20]
1×2 Matrix{Int64}:
10 20
julia> A .+ B
2×2 Matrix{Int64}:
11 22
13 24
julia> C = [5, 6]
2-element Vector{Int64}:
5
6
julia> A .* C
2×2 Matrix{Int64}:
5 10
18 24
ブロードキャストを用いることで、スカラ値やVector配列、1行のMartix配列などがより大きい次元数を持つもう一方の配列と同じ形状となるように “賢く” 変形されて、要素ごとの演算が行われます。
3.8 ディクショナリ
配列は1から始まるインデックス番号で、1, 2, 3, … という順に値が格納されていました。
ディクショナリは、キーと値をペアとしてデータを格納します。
julia> me = Dict("height"=>180) # ディクショナリを作成
Dict{String, Int64} with 1 entry:
"height" => 180
julia> me["height"] # 要素にアクセス
180
julia> me["weight"] = 70 # 新しい要素を追加
70
julia> print(me)
Dict("height" => 180, "weight" => 70)
3.9 ブーリアン
Juliaには、Bool
という型があります。これは、true
と false
という2つの値のどちらかを取ります。
また、Bool
型に対する演算子として、&
(論理積)や|
(論理和)、!
(論理否定)があります(数値に対する演算子は、+
、-
、*
、/
などがあるように、型に応じて使用できる演算子があります)。
julia> hungry = true # お腹空いてる?
true
julia> sleepy = false; # 眠い?
julia> typeof(hungry)
Bool
julia> !hungry
false
julia> hungry & sleepy # 空腹 かつ 眠い
false
julia> hungry | sleepy # 空腹 または 眠い
true
3.10 if文
条件に応じて、処理を分岐するにはif
/else
を用います。
julia> hungry = true;
julia> if hungry
print("I'm hungry")
end
I'm hungry
julia> hungry = false;
julia> if hungry
print("I'm hungry")
else
print("I'm not hungry")
print("I'm sleepy")
end
I'm not hungryI'm sleepy
3.11 for文
ループ処理を行うには、for
文を用います。
julia> for i = [1, 2, 3]
println(i)
end
1
2
3
ここでは、[1, 2, 3]
という配列の中の要素を出力する例を示しています。
for ... = ...
という構文を用いると、配列などのデータ集合の各要素に順にアクセスすることができます。
3.12 関数
まとまりのある処理を関数(function)として定義することができます。
julia> function hello()
print("Hello World!")
end
hello (generic function with 1 method)
julia> hello()
Hello World!
また、関数は引数を取ることができます。
julia> function hello(object)
print("Hello " * object * "!")
end
hello (generic function with 2 methods)
julia> hello("cat")
Hello cat!
なお、文字列の連結は*
で行います。
Juliaインタプリンタを終了するには、exit()
関数を使用します。
4 Juliaスクリプトファイル
これまで、Juliaインタプリンタによる例で見てきました。
Juliaインタプリンタ(厳密にはREPL)は、対話的にJuliaを実行できるモードで、簡単な実験を行うにはとても便利です。
しかし、まとまった処理を行おうとすると、毎回プログラムを入力する必要があるので少し不便です。
そのような場合は、Juliaプログラムをファイルとして保存し、そのファイルを(まとめて)実行します。
ここでは、そのようなJuliaスクリプトファイルによる例を見ていきます。
4.1 ファイルに保存
それでは、テキストエディタを開き、hungry.jl
というファイルを作成します。
hungry.jl
は次の1行だけからなるファイルです。
print("I'm hungry!")
続いて、ターミナルを開き、先のhungry.jl
が作成された場所に移動します。
そして、hungry.jl
というファイル名を指定して、julia
コマンドを実行します。
ここでは、~/dlfs_by_julia/ch01
というディレクトリにhungry.jl
があるものと仮定します。
$ cd ~/dlfs_by_julia/ch01
$ julia hungry.jl
I'm hungry!
このように、julia hungry.jl
というコマンドから、Juliaプログラムを実行することができます。
julia ***.jl
というコマンドから***.jl
の内容を実行することはできますが、Juliaにおいてはこのようにプログラムを実行することは稀です。
大きなパッケージを利用したり短いプログラムを実行する際には、Juliaでは1つのREPLやJupyterカーネル上などでプログラムを実行していく方が効率的です。
その理由はこの記事では解説しませんが、「1 Juliaとは」の節で少し触れたように、「JuliaはC言語のように早く動作する」仕組みがあるのですが、その仕様を理解すると何故1つのREPLやJupyterカーネル上などでプログラムを実行していく方が効率的なのかわかると思います。
5 Plots
ディープラーニングの実験においては、グラフの描画やデータの可視化は重要になってきます。
Plots はグラフ描画のためのライブラリです。
Plots を使えば、グラフの描画やデータの可視化が簡単に行えます。
ここでは、グラフの描画方法と画像の表示方法について説明します。
まずはPlotsをインストールします。
julia> # ]
(@v1.6) pkg> # Julia REPLでは"]"を入力することでパッケージモードになる
(@v1.6) pkg> add Plots, HDF5 # HDF5も同時に追加します
Resolving package versions...
Updating `/julia/depot/environments/v1.6/Project.toml`
No Changes to `E:/julia/depot/environments/v1.6/Manifest.toml`
5.1 Plotsのインポート
Plotsは外部パッケージです。
ここで言う「外部」とは、標準のJuliaには含まれていないということです。
そのため、まず初めにPlotsパッケージを読み込む(インポートする)作業を行います。
import Plots
Juliaでは、パッケージを読み込むために、import
という文を用います。
また、using
という文を用いることでもパッケージを読み込むことができます。
using Plots
using
を用いた場合はモジュール内で export
されているメソッドなどをモジュール名の指定無しで使用できるようになります。
5.2 単純なグラフ描画
グラフを描画するためには、plot
関数を利用します。
早速、sin関数を描画する例を見てみましょう。
using Plot
# データの作成
x = range(0, 6, step=0.1) # 0から6まで0.1刻みで生成
y = sin.(x)
# グラフの描画
plot(x, y)
ここでは、range
関数によって[0, 0.1, 0.2, ... , 5.8, 5.9, 6]というデータを生成し、これをx
としています。
このx
の各要素を対象に、sin関数を適用したいのですが、sin(x)
と記述するとエラーが起きてしまいます。
これはsin関数がスカラ値に対して作用する関数であるためです。
このような関数を配列の各要素を対象に実行したい場合は関数名と引数の間に.
を入れ、sin.(x)
とすることで実行することができます。
次に、xとyのデータ配列をPlots.plotメソッドに与え、グラフを描画します。
上のコードを実行すると、図1-3の画像が表示されます。
5.3 Plotsの機能
先のsin関数に加えて、cos関数も追加して描画してみます。
さらに、タイトルやx軸名の描画など、Plotsの他の機能も利用してみます。
using Plot
# データの作成
x = range(0, 6, step=0.1) # 0から6まで0.1刻みで生成
y1 = sin.(x)
y2 = cos.(x)
# グラフの描画
plot(xlabel="x", ylabel="y", title="sin & cos") # x軸、y軸のラベルとタイトルの設定
plot!(x, y1, label="sin")
plot!(x, y2, label="cos", linestyle=:dash)
結果は図1-4のグラフになります。
図のタイトルや軸のラベル名が記載されていることがわかります。
図1-4 sin関数とcos関数のグラフ
この記事ではディープラーニングを実装してくための下準備、Juliaの使い方について学びました。
書籍ではこの後、パーセプトロン、ニューラルネットワークの構造、学習方法からその際に起こりうる諸問題まで具体的に記述されています。
実際に読んでみてニューラルネットワークについて学ぶ際必ずと言っていいほど名前が挙がる理由がわかる、納得の1冊だと思いました。
PythonだけでなくJuliaでコードを書く練習にもなり、とても良い経験になったと思います。
以上になります。