1章 Julia入門

Juliaは、PythonやMatlabのように動的型付けに対応しながらC言語のように早く動作させることができるほか、行列演算なども標準でサポートされており科学計算にも向いている。
Juliaのメリット
新興プログラミング言語であるため普及率は高くないが今後その応用に期待されている。 Juliaのインストール方法などについて簡単にまとめる。

1.1 Juliaとは

Juliaは速さとか書きやすさとかなんかすごいらしいですよ。ホームページに書いてあった(Julia in a Nutshell)
Juliaはまだ普及段階ですが、某大学の線形代数の授業で使用されていたり、Google Colabで使用できる裏技が用意されていたり、ITのプロフェッショナル達からこっそり注目を集めています。

行列の演算や高速な数値計算を行うとき、PythonではおまじないのようにNumpyをインポートしていましたがJuliaではその必要はありません。 基本的な行列演算はデフォルトでサポートされており、一部の統計処理や線形代数的な計算はパッケージをインポートする必要はありますがそのパッケージも標準で搭載してあります。
また、パッケージマネージャもJulia自身に内蔵されており、Pythonの用にpipとcondaどちらを使うべきか、pipとconda一方でしかインストールできないパッケージがあったり環境が混ざって壊れてしまうという心配がありません。

一方で、JuliaはC言語のように早く動作すると謳ってはいるものの、プログラマの記述方法に依存している、つまり、Julia言語らしい早く動作する書き方が存在しており実際に動作速度を向上させるためにはJulia言語独特の仕様を理解する必要があります。

JuliaではJulia言語自体を用いて開発されているFluxというディープラーニングパッケージもあり、研究開発やデータサイエンスといった分野で将来存分に活躍しうるポテンシャルを持っているといえるでしょう。

1.2 Juliaのインストール

ここではJuliaのインストールに際して注意する点を記述します。
このリポジトリで用いる環境は次の通りです。

  • Julia 1.6.1
  • Plots (backend GR)
  • HDF5
  • OrderedCollections

Plotsについては後述します。
PyPlotなどを用いたい場合は適宜読み替えてください。

これから初めてJuliaをインストールするという方を対象に、Juliaのインストール方法を説明します。
上記の要件を満たしている方は読み飛ばしてください。

1.2.1 Juliaのバージョン

Juliaのバージョンは執筆時点で最新のVer.1.6.1を使用します(2021/07)。

julia_version
JuliaのダウンロードページからOSに対応したバイナリをダウンロードしてインストールしましょう。

インストール後はパスを通すために以下の環境変数を作成しPATHに追加してください。

  • JULIA_BINDIRJuliaをインストールしたディレクトリ/julia-1.6.1/bin
# Note
Juliaにて設定できる環境変数の詳細はDocumentsを参照してください。
https://docs.julialang.org/en/v1/manual/environment-variables/
# Tips
SSDに細かいファイルを大量に書き込みたくない!という方は環境変数JULIA_DEPOT_PATHを追加することで設定ファイルやパッケージがインストールされるディレクトリを変更することができます。

1.2.2 使用する外部ライブラリ

書籍の目標とすることは、ゼロからディープラーニングを実装することです。
そのため、外部のライブラリは極力使用しないというのが方針ですが、 PlotsHDF5 および OrderedCollections は例外として用いることにします。

Plots はグラフ描画のためのライブラリです。
Plots を用いれば、実験結果の可視化や、また、ディープラーニングの実行途中のデータを視覚的に確認することができます。

HDF5 はデータを保存するためのパッケージです。
Juliaの計算で取り扱うデータや数値を保存したり、また再利用するために用います。

OrderedCollections は辞書型(Dict)や集合(Set)の拡張型を追加するパッケージです。
Juliaの辞書型で内容の順序を指定するために用います。

1.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プログラミングの簡単な例を見ていくことにします。

1.3.1 算術計算

加算や乗算などの算術計算は、次のように行います。

julia> 1 - 2
-1

julia> 4 * 5
20

julia> 7 / 5
1.4

julia> 3 ^ 2
9

*は乗算、/は除算、^は累乗を意味します( 3 ^ 2 は3の2乗)。

1.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(文字列型)という型であることがわかります。

1.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からは無視されます。

1.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と実質同義となります。

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配列を作成する場合は、「;(セミコロン)」で区切ることで行を追加することができます。

1.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

配列の算術演算は基本的に、数学的な行列演算に従います。
ただし、行列に対してスカラ値で算術演算を行う場合や、行列同士で要素ごと乗除算などを行う場合はブロードキャスト演算子を用いる必要があります。

1.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配列などがより大きい次元数を持つもう一方の配列と同じ形状となるように “賢く” 変形されて、要素ごとの演算が行われます。

1.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)

1.3.9 ブーリアン

Juliaには、Boolという型があります。これは、truefalse という2つの値のどちらかを取ります。
また、Bool型に対する演算子として、&(論理積)や|(論理和)、!(論理否定)があります(数値に対する演算子は、+-*/などがあるように、型に応じて使用できる演算子があります)。

julia> hungry = true   # お腹空いてる?
true

julia> sleepy = false; # 眠い?

julia> typeof(hungry)
Bool

julia> !hungry
false

julia> hungry & sleepy # 空腹 かつ 眠い
false

julia> hungry | sleepy # 空腹 または 眠い
true

1.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

1.3.11 for文

ループ処理を行うには、for文を用います。

julia> for i = [1, 2, 3]
           println(i)
       end
1
2
3

ここでは、[1, 2, 3]という配列の中の要素を出力する例を示しています。
for ... = ... という構文を用いると、配列などのデータ集合の各要素に順にアクセスすることができます。

1.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()関数を使用します。

1.4 Juliaスクリプトファイル

これまで、Juliaインタプリンタによる例で見てきました。
Juliaインタプリンタ(厳密にはREPL)は、対話的にJuliaを実行できるモードで、簡単な実験を行うにはとても便利です。
しかし、まとまった処理を行おうとすると、毎回プログラムを入力する必要があるので少し不便です。
そのような場合は、Juliaプログラムをファイルとして保存し、そのファイルを(まとめて)実行します。
ここでは、そのようなJuliaスクリプトファイルによる例を見ていきます。

1.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プログラムを実行することができます。

1.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`

1.5.1 Plotsのインポート

Plotsは外部パッケージです。
ここで言う「外部」とは、標準のJuliaには含まれていないということです。
そのため、まず初めにPlotsパッケージを読み込む(インポートする)作業を行います。

import Plots

Juliaでは、パッケージを読み込むために、import という文を用います。
また、using という文を用いることでもパッケージを読み込むことができます。

using Plots

using を用いた場合はモジュール内で export されているメソッドなどをモジュール名の指定無しで使用できるようになります。

1.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の画像が表示されます。

fig1-3
図1-3 sin関数のグラフ

1.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のグラフになります。
図のタイトルや軸のラベル名が記載されていることがわかります。

fig1-4
図1-4 sin関数とcos関数のグラフ