2022년 모두의 주목을 받고 있지는 않은 언어
julia입니다.
언어적인 장단점 논할만한 수준이 안되는지라 그런 얘기는 안할거고
그냥 언어가 어떻게 생겼는지 궁금한 사람도 있을 것 같아서 연습용 코드 만든거 올립니다.
별건 아니고 단순한 다층 퍼셉트론이고, 코드는 더럽습니다.
아는 것도 없는데다가 중간에 만들다가 멘탈이 터졌거든요.
뉴비 개고생하는거 보는 마음으로 팝콘 와자작 하시면 되겠습니다.
대충 코드 설명을 하자면,
1) input data와 label, 계산용 임시변수들을 담을 구조체를 만들고
2) 활성화 함수들과 미분함수들을 호출할 dictionary를 만들고
3) 전처리용 함수, 순전파 함수, 역전파 함수를 만들고 이를 module로 내보내는 구조입니다.
module MLCore
export Model, learn_core, batchCreator, labelcreator, functionDict, newModel, tester, successrate, costcalc, maxconvert
MLCore라는 모듈명으로 요런 함수랑 구조체를 export하고요
naming convention에 안 맞게 왔다리갔다리 하는 것 같지만 고치기 귀찮으니 넘어갑시다.
using Pkg
Pkg.activate("MLEnv")
using Images, ImageView, LinearAlgebra, Plots, BenchmarkTools, Random, Distributions
가상환경 활성화하고 패키지 불러옵니다.
사실 작동하는 데 필요한 건 Distributions 패키지 하나만 있으면 되는데 뭔 생각으로 module안에 다 넣어놨는지 모르겠네요.
mutable struct Temp
layers::Vector{Matrix{
Float64}}
backlayer_diff::Matrix{
Float64}
difftemps::Vector{Matrix{
Float64}}
reverse_delta_weights::Vector{Matrix{
Float64}}
reverse_delta_biases::Vector{Matrix{
Float64}}
Temp()
=new(Vector{Matrix{
Float64}}(), permutedims(
Float64[]), Vector{Function}(), Vector{Matrix{
Float64}}(),Vector{Matrix{
Float64}}())
end
mutable struct Model
input::Matrix{
Float64}
label::Matrix{
Float64}
modelStruct::Vector{
Int64}
funcStruct::Vector{String}
weightMatrixes::Vector{Matrix{
Float64}}
biases::Vector{Matrix{
Float64}}
learningRate::
Float64
L2::
Float64
functionDict::Dict{String, Tuple{Function, Function}}
temps::Temp
end
구조체 설정해줍니다.
필드명 뒤에 타입 설정해주지 않아도 작동은 하는데, 그럴 경우 any 타입으로 인식하면서 속도 저하가 좀 생깁니다.
저는 안그래도 안좋은 컴터 성능 최대한 뽑아먹으려고 귀찮더라도 써줬습니다.
가만 보면 파이썬과 달리 필드만 있고 메서드는 정의되어있지 않을 걸 볼 수 있는데요.
일반적으로 julia에서는 구조체에는 필드만 넣고, 메서드는 외부에서 함수를 정의해주는 식으로 하기 때문에 그대로 했습니다.
다만 그렇다고 해서 파이썬처럼 .(메서드명)으로 함수를 호출하지 못하는 건 아니고, 구조체에 함수 필드를 넣어준 후 constructor에서 함수를 정의해 주면 사용할 수 있습니다.
예를 들면 다음과 같은 식이죠.
mutable struct someModel
a
b
c
someModel()=(x=new(4,5);x.c=()->x.a*x.b;x)
end
>someInstance=someModel()
>someInstance.c()
20
대신 이렇게 쓰게되면 외부에서 함수 정의해 주는 것보다 속도가 좀 느립니다.
직접 벤치마크 돌려보니까 함수 자체의 연산량이 많이 커지면 그 차이가 무의미해지긴 합니다만
이 코드 짤때는 그런거 잘 알지도 못했고 이것저것 생각하기도 복잡해서 그냥 분리해 뒀습니다.
sigmoid_core
=x::
Float64->1/(
1+exp(
-(clamp(x,
-250,
250))))
sigmoid
=x::Matrix{
Float64}
->sigmoid_core
.(x)
diff_sigmoid_core
=x::
Float64->clamp(x,
-250,
250)
*(
1-clamp(x,
-250,
250))
diff_sigmoid
=x::Matrix{
Float64}
->diff_sigmoid_core
.(x)
iden_core
=x::
Float64->clamp(x,
-250,
250)
iden
=x::Matrix{
Float64}
->iden_core
.(x)
diff_iden_core
=x::
Float64->x
>-250&&x
<250 ? 1. :
0
diff_iden
=x::Matrix{
Float64}
->diff_iden_core
.(x)
softmax
=x::Matrix{
Float64}
->(exp
.(clamp
.(x,
-250,
250)))
./(sum(exp
.(clamp
.(x,
-250,
250)), dims
=2))
diff_softmax
=x::Matrix{
Float64}
->diff_sigmoid(x)
relu_core
=x::
Float64->max(
0,clamp(x,
-250,
250))
relu
=x::Matrix{
Float64}
->relu_core
.(x)
diff_relu_core
=x::
Float64->x
>=0&&x
<250 ? 1. :
0.
diff_relu
=x::Matrix{
Float64}
->diff_relu_core
.(x)
leaky_relu_core
=x::
Float64->max(clamp(x,
-250,
250),
0.1*clamp(x,
-250,
250))
leaky_relu
=x::Matrix{
Float64}
->leaky_relu_core
.(x)
diff_leaky_relu_core
=x::
Float64->clamp(x,
-250,
250)
>=0 ? 1. :
0.1
diff_leaky_relu
=x::Matrix{
Float64}
->diff_leaky_relu_core
.(x)
relu6_core
=x::
Float64->min(max(
0,x),
6)
relu6
=x::Matrix{
Float64}
->relu6_core
.(x)
diff_relu6_core
=x::
Float64->x
>=0&&x
<6 ? 1. :
0.
diff_relu6
=x::Matrix{
Float64}
->diff_relu6_core
.(x)
functionDict
=Dict{String, Tuple{Function,Function}}([(
"sigmoid",(sigmoid, diff_sigmoid)), (
"iden", (iden, diff_iden)), (
"softmax", (softmax, diff_softmax)), (
"relu", (relu, diff_relu))])
functionDict[
"leaky_relu"]
=(leaky_relu, diff_leaky_relu)
functionDict[
"relu6"]
=(relu6, diff_relu6)
활성화 함수랑 미분함수를 dictionary에 넣어주는 부분이고요.
여기서 개인적으로 얘기하고 싶은 julia의 특징이 2가지 있습니다.
첫째는 julia의 broadcasting 기능인데요.
상황에 맞게 함수 앞 또는 뒤에 "."을 붙여주면 배열이나 매트릭스 등의 원소별로 함수를 적용시켜주는 기능입니다.
예를 들어 Int64를 제곱해주는 어떤 함수 square(x)가 있고, [1,2,3,4,5]라는 벡터가 있다고 할 때,
[1,2,3,4,5] 원소 각각에 square를 적용해주기 위해서는 square.([1,2,3,4,5])만 해주면 됩니다.
만능은 아니지만 확실히 편리하고, 제가 julia에서 제일 좋아하는 기능 중 하나입니다.
둘째는 julia의 namespace 관리 문제인데요.
개인적으로 julia에서 제일 귀찮아하는 문제가 이겁니다.
사실 julia에서 함수를 정의하는 일반적인 방식은 다음과 같습니다.
function someFunc(a, b)
...
end
하지만 위의 코드에서는 익명함수의 alias를 설정해주는 식으로 정의했습니다.
someFunc=(a,b)->....
제가 후자의 방법을 선호하는 이유는 코드가 짧아져서이기도 하지만, 전자의 방법으로 정의할 경우 그 함수명은 constant가 되기 때문입니다.
그리고 한번 constant가 됐으면 julia를 다시 시작하지 않는 이상 저 함수명은 다른 곳에 쓸 수가 없는데, 다시 시작하게 되면 그동안 사용했던 패키지나 코드들을 다시 컴파일해야해서 시간이 좀 걸립니다.
초기 단계에 함수명이 자주 바뀌거나 할 때는 좀 불편하기 때문에, 익명함수의 alias 역할만 해주는 후자 방법을 애용합니다.
구조체도 마찬가지로 이름이 constant가 되어버리는데, 이런 경우에는 구조체를 module안에 넣고 쓸 때 alias로 연결시켜주는 식이 개인적으로는 제일 마음 편합니다.
costcalc
=x::Matrix{
Float64}
->sum(y
->y
^2,x)
/2function labelcreator(x::
Int64)
label
=Int64.(zeros(
10))
label[x
+1]
=1
return label
end
function matrixCreator(elementType::String, matrixSize::Tuple{Vararg{
Int64,N} where N})
if elementType
=="zero"
return zeros(matrixSize)
elseif elementType
=="ones"
return ones(matrixSize)
elseif elementType
=="rand"
return rand(Normal(),matrixSize)
else
return "Undefined elementType"
end
end
function selectIndex(input::Matrix{
Float64}, n::
Int64)
return rand((
1:size(input,
1)),n)
end
function batchCreator(input::Matrix{
Float64}, inputlabel::Matrix{
Int64}, n::
Int64)
batch
=Float64[]
label
=Float64[]
indexes
=selectIndex(input,n)
for i
in 1:length(indexes)
append
!(batch, input[indexes[i],:])
append
!(label, inputlabel[indexes[i],:])
end
return reshape(batch, (
Int64(length(batch)
/n),n))
'|>copy, reshape(label, (
Int64(length(label)
/n), n))
'|>copy
end
function maxconvert(result::Matrix{
Float64})
a
=max(result
...)
for i
in 1:length(result)
result[i]
=(result[i]
==a
? 1 :
0)
end
return Int64.(result)
end
function propagate(self::Model, index::
Int64, funcname::String)
frontlayer
=self
.temps
.layers[index]
#backlayer=self.layers[index+1]
weightmatrix
=self
.weightMatrixes[index]
bias
=self
.biases[index]
somefunc, diffsomefunc
=self
.functionDict[funcname]
linear
=frontlayer
*weightmatrix
.+bias
output
=somefunc(linear)
difftemp
=diffsomefunc(output)
#println(output[1,:])
push
!(self
.temps
.layers, output)
# println(diffsomefunc.(output))
push
!(self
.temps
.difftemps, difftemp)
end
function backpropagate(self::Model, startindex::
Int64)
layerindex
=length(self
.temps
.layers)
-startindex
weightmatrix
=self
.weightMatrixes[layerindex]
frontlayer
=self
.temps
.layers[layerindex]
#backlayer=self.temps.layers[layerindex+1]
backlayer_diff
=self
.temps
.backlayer_diff
difftemp
=self
.temps
.difftemps[layerindex]
#frontlayer:1*n matrix
#println(backlayer_diff[1,:])
#println(maximum(backlayer_diff[1,:], dims=1))
#println(maximum(-backlayer_diff[1,:], dims=1))
backdiff
=backlayer_diff
.*difftemp
delta_weight
=frontlayer
'*backdiff
delta_bias
=sum(backdiff, dims
=1)
delta_frontlayer
=backdiff
*weightmatrix
'
#println(delta_bias)
push
!(self
.temps
.reverse_delta_weights, delta_weight)
push
!(self
.temps
.reverse_delta_biases, delta_bias)
self
.temps
.backlayer_diff
=delta_frontlayer
end
function newModel(input::Matrix{
Float64}, label::Matrix{
Float64}, hiddenLayerStruct::Tuple{Vararg{
Int64,N} where N}, funcStruct::Tuple{Vararg{String, N} where N}, numOutput::
Int64, learningRate::
Float64, L2::
Float64, functionDict::Dict{String, Tuple{Function, Function}} )
modelStruct
=vcat(size(input,
2), hiddenLayerStruct
..., numOutput)
weightMatrixes
=Matrix{
Float64}[]
biases
=Matrix{
Float64}[]
funcStr
=String[]
funcStr
=[i
for i
in funcStruct]
for i
in 1:length(modelStruct)
-1
push
!(weightMatrixes, matrixCreator(
"rand", (modelStruct[i], modelStruct[i
+1])))
push
!(biases, matrixCreator(
"rand", (
1, modelStruct[i
+1])))
end
return Model(input, label, modelStruct, funcStr, weightMatrixes, biases, learningRate, L2, functionDict, Temp())
end
function learn_core(self::Model)
#initialize temp values
self
.temps
=Temp()
push
!(self
.temps
.layers, self
.input)
#propagate
for i
in 1:length(self
.modelStruct)
-1
propagate(self, i, self
.funcStruct[i])
end
#backpropagate
#println(self.temps.layers+-self.label)
self
.temps
.backlayer_diff
=self
.temps
.layers[
end]
-self
.label
#self.temps.difftemps+.=1.0
#println(self.temps.backlayer_diff)
for i
in 1:length(self
.modelStruct)
-1
backpropagate(self,i)
end
self
.weightMatrixes
-=self
.learningRate
*reverse(self
.temps
.reverse_delta_weights)
self
.biases
-=self
.learningRate
*reverse(self
.temps
.reverse_delta_biases)
end
function tester(testinput::Matrix{
Float64}, self::Model)
layer
=Vector{Matrix{
Float64}}()
matrixes
=self
.weightMatrixes
biases
=self
.biases
funcStruct
=self
.funcStruct
functionDict
=self
.functionDict
push
!(layer, testinput)
for i
in 1:length(matrixes)
#println(i)
#println(layer[i])
#println((reshape(layer[i], (1,length(layer[i])))*mnistModel.weightMatrixes[i]+mnistModel.biases[i]).|>sigmoid)
somefunc, _
=functionDict[funcStruct[i]]
push
!(layer, (layer[i]
*matrixes[i]
.+biases[i])
|>somefunc)
end
return layer[
end]
end
function successrate(test)
return length(filter(x
->x
==true,test))
/length(test)
end
전처리용 함수들이랑 순전파, 역전파 함수들입니다.
선형대수용 함수들이 기본으로 내장되어 있어서 짜는게 더 편했던 것 같습니다.
전치행렬(정확히는 수반행렬)도 작은따옴표를 사용하면 바로 되기 때문에 편리하고요.
다만 파이썬에서 numpy 사용하던 분들이 넘어오게 되면 좀 헷갈릴수가 있는게
numpy는 행부터 세지만 julia는 열부터 세기 때문에, 이 차이를 고려하지 않으면 행렬을 reshape한다던가 할 때 이미지가 뒤집힐 수 있습니다.
그리고 작은따옴표 함수의 결과물은 원래의 행렬과 아직 연결되어 있기 때문에, 독립적인 행렬을 만들려면 이어서 copy함수를 써 줘야 합니다.
참고로 julia에서 |>은 파이프 연산자입니다(somefunc(a)라고 쓸 걸 a|>somefunc으로 쓰게 해주는 연산자).
함수들이 좀 쓸데없는 것들도 있고, L2규제도 구현 안해놓긴 했지만 일단 여기서 끝냈습니다.
마지막으로 mnist 손글씨 데이터셋 함 돌려보겠습니다.
은닉층 하나짜리로 sigmoid랑 softmax 쓰도록 설정했습니다.


손실함수 잘 내려가고 정확도도 92.5%까지 올려지네요.
대강 돌아가기는 하는 것 같으니 만족합니다.
사실 julia가 이미 Flux라는 머신러닝용 패키지가 있기 때문에 굳이 이걸 만들 필요가 없긴 했는데
공부하는 겸 해서 만들어봤습니다.
라이브러리 배우는게 지겨워서 차라리 직접 만드는게 낫지 않을까 하고 생각도 하곤 했는데
막상 만들어보니까 잘 모르는 부분도 많았고 쉽지가 않더라고요.
꼴랑 이거 하겠다고 14일동안 10번 갈아엎었습니다.
앞으로는 라이브러리 있으면 그냥 감사히 쓸 것 같네요.
그리고 julia하면서 느낀 게, 일단 언어 자체가 어렵지도 않고, 딱히 특이한 문법이 있다거나 한 것도 아니어서 무난합니다.
r이나 파이썬 쓰시던 분들이 넘어오기에 별 무리는 없을 것 같아요.
속도 면에서도 좋은 편이고, 언어적으로도 r, 파이썬 비해 조금은 더 우위에 있지 않나 싶을 때가 있습니다.
다만 성능이나 언어적인 차이가 실제 사용할 때 유의미할 정도냐 하면 그건 아닌 것 같습니다.
웬만한 기능들은 다른 언어들도 다 있고, julia가 속도에서 우월한 분야들은 다른 언어들도 c코드로 돌려버리니까요.
반면 julia 커뮤니티를 보면, 나름 준수한 편이고 웬만한 패키지들은 다 있긴 하지만, 아무래도 r이나 파이썬에 비하면 좀 처지는 게 사실입니다.
일반적인 프로그래밍 관련해서는 파이썬에만, 심화된 통계 관련해서는 r에만 관련 패키지가 있는 걸 경험할 때가 가끔 있었습니다.
관련 설명이나 자료를 찾는 게 더 힘들기도 하고요.
그래도 julia 자체만으로도 고성능 코드를 짤 수 있다는 점, broadcasting 같이 코딩을 쉽게 해주는 기능들이 있다는 것은 확실한 장점인 것 같습니다.
계산이 많이 필요한 분야에서 자체적으로 뭔가 개발하고 싶으면 사용해 볼 만한 것 같습니다.
hilite.me 를 쓰면 신택스 하이라이팅이 가능합니다
본문 적용했습니다 감사합니다!
커헠좌가 곧 따봉날릴 글
줄리아가 파이썬 대체할때까지 숨참는다