はじめに
Juliaを2つ目の言語として勉強を始め、意味が分からず挫折しそうになりました。
前回の記事で手始めにタイタニックのデータを使ってLightGBMモデルを作成しようとしたら全く意味が分からず、気付けばコントローラーを握った自分がいました。前回記事↓
さすがに、勉強し始めて1日でリタイアするのはプライドが許さなかったので、とりあえず別のモデルでも試すか!ということでランダムフォレストでモデルを作成したのでまとめようと思います。
また、今回はJuliaの書き方についても丁寧にまとめていきたいと思います。
なぜなら、記事が圧倒的に少なくて、目的の情報が大きなスーパーの中から塩昆布を探すくらい見当たりません。
まじでどこにあるんや...って探し当てたとしても英語の記事ばかり。くそくらえ
ということで、今後Juliaの人気がさらに出てきた際に困る人が1人でも少なくなってくれればということを願い記事にしていきたいと思います。
Juliaの書き方
ランダムフォレストのモデルを作成する前のデータ前処理の段階からコードを説明していこうと思います。
また、前回はjlファイルを作成して、REPL(インタラクティブモード)で出力させていましたが、今回はJupyter Notebookでも実行できるみたいなので、そっちで動かしていきます。
それではいきましょう!
必要なパッケージをインポート
JuliaはPythonライクな書き方ができ、かつC言語のように高速であることが特徴です。
Pythonと同じように、まずは必要なパッケージをインポートします(Juliaはパッケージにおんぶにだっこです)。
using Pkg
Pkg.add("パッケージ名")
using パッケージ名
パッケージをインポートするためのパッケージであるPkg
をまずはusing Pkg
でインポートします。
その後、Pkg.add("パッケージ名")
で必要なライブラリをインストールします。
今回は、以下のパッケージをインストールします。
- DataFrames:R言語(data.frame)やPython(pandas DataFrame)のデータフレームに似たデータ構造を提供。列志向のデータ構造を通じて、様々な操作を行うことが可能。
- CSV:csvファイルを読み書きする。
- Statistics:標準ライブラリの一部であり、基本的な統計関数(平均、中央値、標準偏差など)を提供。
- StatsBase:基本的な統計ツールとデータ型を提供。平均、分散、標準偏差などの計算に加えて、より高度な統計手法も含まれる。
- CategoricalArrays:カテゴリーデータを効率的に扱うことができる。限定された数のカテゴリーを持つデータ(性別や国籍など)のための特殊な配列タイプを提供。
- MLJ:Machine Learning in Juliaの略で、機械学習フレームワーク。機械学習アルゴリズムへのアクセス、データの前処理、モデルの訓練、評価およびハイパーパラメータの調整機能を提供。
- MLJDecisionTreeInterface:MLJフレームワーク内で使用するための決定木モデルへのインターフェースを提供。
これらをパッケージ名にぶち込んでインポートしてください。
データを読み込む
それでは、データを読み込んでいきましょう。
df = CSV.read("your_file_path", DataFrame)
first(df, 5)
わたしはPythonしか知りませんが、かなりPythonに近い書き方だなと感じました。
CSVパッケージを用いて、ファイルを指定し、データフレームとして読み込んでるんだなって感覚でわかるかと思います。
また、first(df,n)
で最初のn行を表示できます。
Pythonのdf.head()
と同じ感じです。
ちなみに、最後の行を表示したいならlast(df,n)
、特定の範囲であればdf[6:10,:]
のようにして6行目から10行目を表示、ランダムな行を表示するにはsample(df,n)
です。sapmle関数を用いる際はRandom
パッケージをインストール、インポートしてから使用しましょう。
さらに、ちなみにJuliaはインデックスが1から始まります。Pythonを使ってる方はインデックスが0からなので少し違和感があるかもしれませんが、0から数えるより1から数えた方が圧倒的に楽です。
表示されたデータフレームはこんな感じです。
普通に感動したんですけど、カラム名の下に型が書いてあるんですよね。
型調べなくてもいいの!?ってテンションあがりました。
まあ、型変更したりした場合は確認したりした方がいいですけどね。
ちなみに型の確認はtypeof()
です。
typeof(df.Name)
またはtypeof(df[!,:Name])
でNameカラムの型を調べることができます。
データフレームの各カラムへのアクセスはdf[!,:カラム名]
の方が都合がいいのでこちらを覚えましょう。
!
はカラムに対してアクセスし操作をするぞという合図で、操作したいカラムを指定する場合は:カラム名
で指定します。
データの前処理
まずはいらないカラムを削除します。
dropcols = [:PassengerId, :Name, :Ticket, :Cabin]
df = select(df, Not(dropcols))
削除したいカラムのリストを指定してあげて、select関数でデータフレームの削除したいカラム以外を選択してる感じですね。これも感覚的に何をしているのかわかるんじゃないでしょうか?
次に、欠損値の確認をしていきます。
for col_name in names(df)
missing_count = sum(ismissing, df[!,col_name])
if missing_count > 0
println("$col_name: $missing_count missing values")
end
end
Pythonのfor文、if文に似てますね。
names(df)
でデータフレームの全カラム名をリストで取得できます。
sum(ismissing, df[!,col_name])
で各カラムの欠損値がtrue
である合計値を算出します。
その後、
if missing_count > 0
println("$col_name: $missing_count missing values")
で欠損値の合計が1以上であるカラム名の欠損値の数を出力させています。
また、for文とif文を書いた後はend
で終了させておきましょう。
では、欠損値の補完をしていきましょう。今回は、Ageカラムは中央値、Embarkedカラムは最頻値で補完させます。
median_age = median(skipmissing(df[!,:Age]))
df[!,:Age] = coalesce.(df[!,:Age], median_age)
mode_Embarked = mode(skipmissing(df[!,:Embarked]))
df[!,:Embarked] = coalesce.(df[!,:Embarked], mode_Embarked)
ちょっと難しいですが、median関数とmode関数を用いて、中央値と最頻値を算出し、それをcoalesce関数で各カラムの欠損値に対して置換させます。
最後の処理として、SexカラムとEmbarkedカラムのデータが文字列型なので、文字列→カテゴリー→整数値に変換していきます。
要するにラベルエンコーディングさせてます。
df[!,:Sex] = categorical(df[!,:Sex])
df[!,:Embarked] = categorical(df[!,:Embarked])
df[!,:Sex] = CategoricalArrays.levelcode.(df[!,:Sex])
df[!,:Embarked] = CategoricalArrays.levelcode.(df[!,:Embarked])
なんかよくわかりませんがこれでいけます(暴論)。
この辺りはもう少し勉強します!
ここで、謎コードを書いていきます。
なぜ書くのかは後程モデルを学習させたときに説明します。
y = map(val -> Int(val=="group1"), y)
y = identity(y)
y = categorical(y)
マジでわからん
後程詳しく書きますが、カテゴリー型に再度更新させてます。
もう一回言わせてもらう。マジでわからん
ランダムフォレストモデルを作成
まずは特徴量とターゲットを指定します。
X = select(df, Not(:Survived))
y = df[!,:Survived]
データフレームからSurvivedカラム以外を特徴量、Survivedカラムをターゲットに指定しました。
次にデータの分割を行います。
train, test = partition(eachindex(y), 0.7, shuffle=true)
partition関数を用いて、データを分割します。
引数に、y
のインデックスを70%:30%で分割し、分割はランダムで行うよって感じですね。
なぜy
を指定してるのかわかりませんが、y
意外だとエラーでます。
ここでも言わせてくれ。マジでわからん
ではモデルをロードして起動させましょう。
model = @load DecisionTreeClassifier pkg=DecisionTree
model = model()
MLJDecisionTreeClassifierパッケージからDecisionTree
モデルをロードしてるんだと思います。
そして、それを起動?
まあ、よく見かけますが決まり文句みたいなもんです。
その後、モデルを学習させます。
tree = machine(model, X, y)
fit!(tree, rows=train, force=true)
引数のforce=true
は学習を上書きするかどうかですね。
学習させたモデルを更新させたいときはtrue
を指定しておきましょう。
また、このfit関数
、ここで先ほど言っていた、なぜカテゴリー型に更新したかの問題が出てきます。
上記ページで議論されてますが、原因がわかってないそうです。
カテゴリー型に更新せずに実行してしまうと
DomainError with 0:
Can only convert categorical elements to integers.
というエラーが発生し、カテゴリー型のデータを整数値に変換しようとしているらしいが、カテゴリー型のまま渡しても、整数値に変換して渡しても同じエラーが発生するだけです。
しかし、再度カテゴリー型に更新することでエラーは解消されます。
は?
また、開発者らしき方が、y=categorical(y)
の前にy=identity(y)
をすることでeltype
を強化できると発言してるので、一応私も真似てみました(*ノωノ)
ちなみに、eltype
とは一言で説明するのであれば、「要素型」を返す関数です。
この辺りは追々理解します。
次に学習させたモデルを用いて予測値を算出します。
yhat = MLJ.predict(tree, rows=test)
ここまじで解決するのに時間がかかったんですけど、
公式のMLJ.jlのドキュメントにはpredict(tree, X[test,:])
と書いていましたが、一生predict
が定義されてないってエラーが出てました。以下、ドキュメントとかです。
そしていろいろ調べていると、
どうも変数スコープというのがあるみたいで、例えば
s = 0
for i in 1:10
s = s + 1
end
というコードを実行するとエラーが生じます。s
なんか定義してねえよ。と。
いやs=0
でしてんじゃんと思いましたが、どうもfor文の中でs
を変更したかったら、以下のようにグローバル変数としなけらばならないみたいです。
s = 0
for i in 1:10
grobal s = s + 1
end
ここからインスピレーションを受けて、「MLJ.jlの中に関数としてあるけど、スコープの関係からMLJの外にあるんじゃね?」と考え、MLJ.predict
と変更したところうまくいきました。
エラーが解消したときは、思わずガッツポーズしちゃいましたね。
最後にモデルの評価を行います。
今回はAccuracy(精度)を評価します。
accuracy(yhat, y[test])
これで算出されたAcuuracyはなんと
(´・ω・)ん?
(*っдс) ゴシゴシ
まとめ
皆さんお疲れさまでした。
いかがだったでしょうか。
Juliaで機械学習モデルを作成しましたが、書き終えるまでにめちゃくちゃ調べましたが、いざ書き終えると大したこと書いてないなって感じでした。
なんといっても参考記事の少ないこと!
まじで英語の記事しかないし、それすらも少ないのでドキュメント漁るしか方法はありません。
と言いたいところですが、さすがChatGPTですね。
今回、GPTsを用いて、Juliaコードを書くことに特化させたGPTを作成しました。
まあ、情報の少なさから精度はあてになりませんが…
けど、今回の記事のコードはほとんどこいつがやってくれました。
URLを貼っておくので、Juliaやってみたいよ!って方は是非使ってみてください。
それでは!!!
最後にコード全体を貼っておきます。
using Pkg
Pkg.add("DataFrames")
Pkg.add("CSV")
Pkg.add("MLJ")
Pkg.add("CategoricalArrays")
Pkg.add("Statistics")
Pkg.add("StatsBase")
Pkg.add("MLJDecisionTreeInterface")
using DataFrames, CSV, MLJ, CategoricalArrays, Statistics, StatsBase, MLJDecisionTreeInterface
df = CSV.read("./Unity_Share_Hub/data/titanic_data.csv", DataFrame)
first(df,5)
for col_name in names(df)
missing_count = sum(ismissing, df[!,col_name])
if missing_count > 0
println("$col_name: $missing_count missing values")
end
end
median_age = median(skipmissing(df[!, :Age]))
df[!, :Age] = coalesce.(df[!, :Age], median_age)
mode_embarked = mode(skipmissing(df[!, :Embarked]))
df[!, :Embarked] = coalesce.(df[!, :Embarked], mode_embarked)
df[!, :Sex] = categorical(df[!, :Sex])
df[!, :Embarked] = categorical(df[!, :Embarked])
df[!, :Sex] = CategoricalArrays.levelcode.(df[!, :Sex])
df[!, :Embarked] = CategoricalArrays.levelcode.(df[!, :Embarked])
dropcols = [:PassengerId, :Name, :Cabin, :Ticket]
df = select(df, Not(dropcols))
y = df[!, :Survived]
X = select(df, Not(:Survived))
y = map(val -> Int(val=="group1"), y)
y = identity.(y)
y = categorical(y)
train, test = partition(eachindex(y), 0.7, shuffle=true)
model = @load DecisionTreeClassifier pkg=DecisionTree
model = model()
tree = machine(model, X, y)
fit!(tree, rows=train, force=true)
yhat = MLJ.predict(tree, rows=test)
accuracy(yhat, y[test])