Lời nói đầu: Why Julia?

Julia là một ngôn ngữ lập trình được công bố với thế giới vào đúng ngày Valentine năm 2012 với mục đích sử dụng chính trong lĩnh vực tính toán khoa học (scientific computing).
Nhiều người sẽ thắc mắc là sự ra đời của một ngôn ngữ mới có thật sự cần thiết khi đã có rất nhiều ngôn ngữ đã và đang được sử dụng thành công trong lĩnh vực này như C, C++, Fortran, Matlab, R và gần đây là Python, tuy nhiên

  • C, C++, Fortran: Tốc độ nhanh nhưng quá low level, việc phát triển xây dựng mô hình tính toán với những ngôn ngữ này quá phức tạp và tốn thời gian
  • Matlab, R: Rất tiện lợi, support đầy đủ các tính năng, hàm cần thiết cho lĩnh vực tính toán khoa học, phân tích dữ liệu, phân tích số học. Tuy nhiên tốc độ xử lý lại khá chậm, không thích hợp cho việc xây dựng những mô hình lớn, việc khá thường thấy trong lĩnh vực tính toán khoa học. Trong những trường hợp như vậy, các ngôn ngữ này thường được dùng để xây dựng và kiểm thử mô hình prototype, còn việc xây dựng mô hình thật sẽ sử dụng các ngôn ngữ như C, C++
  • Python: Ngôn ngữ mới nổi trong giới tính toán khoa học, dễ học, dễ dùng với rất nhiều các thư viện hỗ trợ. Tuy nhiên, là một interpreter language, tốc độ xử lý của Python cũng khá chậm. Việc sử dụng những thư viện với những hàm tính toán được viết bằng C (Numpy,...) phần nào giải quyết được vấn đề này. Ngoài ra, các cơ chế lập trình song song (parallel computing) hay đồng thời (concurrency) của Python khá hạn chế, trong khi đây là các tính năng quan trọng trong tương lai khi mà lượng data cần xử lý ngày càng lớn hơn.

Và Julia được ra đời với mục đích lớn lao là giải quyết hết các vấn đề mà các ngôn ngữ trên gặp phải.

  • Với việc sử dụng cơ chế biên dịch JIT (Just In Time) được xây dựng trên nền tảng LLVM, code Julia sẽ được dịch trực tiếp thành mã máy, do đó có tốc độ xử lý có thể sánh bằng với C hay C++.
  • Julia là một ngôn ngữ multi-paradigm, lập trình viên có thể kết hợp các phương thức lập tình mệnh lệnh (imperative), hàm (functional) hay hướng đối tượng (object-oriented). Ngoài ra dù là một dynamic language nhưng Julia lại hỗ trợ type inferenceoptional typing như các ngôn ngữ static (type của Julia là object tồn tại ở runtime, chứ không phải là ở lúc compile).
  • Standard library của Julia giống như Matlab support sẵn các phép xử lý cần thiết trong tính toán khoa học như là tính toán vector hay ma trận.
  • Hỗ trợ các cơ chế lập trình song song và đồng thời
  • ...

Để có thể giới thiệu hết các tính năng của Julia trong 1 bài viết quả thật là điều không thể. Do vậy bài viết này sẽ giới thiệu các tính năng cơ bản nhất của Julia, đủ dùng để viết các chương trình đơn giản. Các tính năng cao cấp hơn như user-defined type, multiple dispatching, metaprogramming, parallel computing sẽ để dành cho những bài viết sau

Cơ bản về Julia

1. Cài đặt Julia, thêm các thư viện bên thứ 3, môi trường lập trình

  • Cài đặt Julia: Download bộ cài đặt từ trang https://julialang.org/downloads/. Sau đó gõ câu lệnh julia ở trong terminal là ta đã có thể sẵn sàng để test thử Julia rồi

  • Môi trường lập trình:

  • IDE: Juno

  • Sử dụng Jupyter Notebook với package IJulia

  • Trong bài viết này ta sẽ sử dụng Jupyter Notebook với Julia. Trước tiên ta cần cài đặt thư viện IJulia. Trong màn hình lệnh của Julia gõ câu lệnh sau:

julia> Pkg.add("IJulia")
  • Sau đó để sử dụng Jupyter Notebook ta có thể sử dụng 1 trong 2 cách sau. Đây cũng là 2 cách import và sử dụng package trong Julia

    • Import package và gọi hàm

      julia> import IJulia
      julia> IJulia.notebook()
      
    • Sử dụng các hàm được export sẵn bởi package

      julia> using IJulia
      julia> notebook()
      
  • Và đây là màn hình sau khi đã khởi tạo 1 notebook mới và cài đặt thêm package Plots để test thử tính năng tạo đồ thị

  • Dù chưa phong phú như hệ thống thư viện của Python, nhưng hiện tại hệ thống thư viện của Julia đang ngày càng được mở rộng, các bạn có thể tra cứu tên các thư viện ở 2 link sau:

2. Data Type cơ bản của Julia

  • Boolean
julia> x = true
julia> y = false

julia> typeof(x)
Bool
# true: 1, false: 0
julia> true + false
1 
  • Integers và Floats
julia> typeof(1)
Int64 # Do đang dùng hệ điều hành 64bit
julia> typeof(1.0)
Float64 

julia> x = 3; y = 2.0
julia> x + y # Tương đương với +(x, y)
5.0
julia> x * y # Tương đương với *(x, y)
6.0
julia> 2x # Tương đương với 2*x. Khi thực hiện phép nhân giữa 1 giá trị với 1 biến thì có thể bỏ bỏ dấu *
6  
julia> x / y # Tương đương với /(x, y)
1.5
julia> x^2
9
  • Số phức. Phần phức sẽ được biểu diễn bằng ký tự im
julia> x = 1 + 2im
1 + 2im
  • String
julia> x = "vietnamlab"
julia> typeof(x)
String
# String interpolation
julia> "$x is a good company"
"vietnamlab is a good company"
# Nối chuỗi
julia> x * " No.1"
"vietnamlab No.1"

3. Container Type

  • Array Array của Julia giống như Matlab có index từ 1
julia> x = Array{Float64}(2, 2) # Khởi tạo 1 array 2 chiều rỗng
2×2 Array{Float64,2}:
 2.20403e-314  2.23184e-314
 2.23283e-314  2.32633e-314

julia> x = [0, 3.0, true, "test"]
4-element Array{Any,1}:
    0
    3.0
 true
     "test"

julia> x[end]
"test"

julia> x[1:3]
3-element Array{Any,1}:
    0
    3.0
 true
  • Tuple Tương tự như Tuple của Python
julia> x = ("Amor", 1) # tương đương với x = "Amor", 1
("Amor", 1)

julia> typeof(x)
Tuple{String,Int64}
  • Dictionary Tương tự như của Python
julia> x = Dict("name" => "King", "age" => 29)
Dict{String,Any} with 2 entries:
  "name" => "King"
  "age"  => 29

julia> x["name"]
"King"

4. Conditional Expression và Loop Expression

  • Conditional Expression
if x < y
    println("x is less than y")
elseif x > y
    println("x is greater than y")
else
    println("x is equal to y")
end
  • For loop
julia> for x in 1:6
       print(x)
       end
123456
  • Tương tự như Python, Julia cũng hỗ trợ list comprehension. Không chỉ vậy, mà cả dictionary comprehension nữa.

    julia> [i + j for i in 1:3, j in 4:5]
    3×2 Array{Int64,2}:
     5  6
     6  7
     7  8
     
    julia> Dict("$a" => 0 for a in ["Hoan", "TA", "Dong"])
    Dict{String,Int64} with 3 entries:
      "Hoan" => 0
      "TA"   => 0
      "Dong" => 0
    

5. Compound Expression và Function

5.1. Compound Expression
  • Với compound expression, ta có thể gộp nhiều expression vào 1 expression duy nhất và giá trị trả về sẽ là giá trị cuối cùng của expresion được gộp. Có 2 cách để tạo compound expresion
  • Dùng begin block
julia> z = begin
           x = 1
           y = 2
           x + y
       end
3
  • Dùng chuỗi (;)
julia> z = (x = 1; y = 2; x + y)
3
5.2. Function
  • Trong function của Julia thì keyword return là không bắt buộc. Trong trường hợp không có return thì giá trị cuối của block code trong function sẽ là giá trị trả về (tương tự Scala)
function multiply(a, b)
    a * b # Giá trị trả về của hàm
end
  • Các biến số của function cũng có thể được set giá trị mặc định
function simulate(param1, param2, max_iterations=100, error_tolerance=0.01)
# ...
# ...
end
  • Ta cũng có thể định nghĩa function ở thể rút gọn như sau

    julia> plus(x, y) = 2x + y
    plus (generic function with 1 method)
    
    julia> plus(5, 6)
    16
    
  • Cũng như trong Python hay Scala, function trong Julia là fist-class object, có thể được gán cho biến hay truyền cho function khác như là 1 tham số. Ngoài ra, chúng cũng không cần có tên và có thể được tạo một cách nặc danh bằng 1 trong 2 cách sau:

    julia> x -> x^2 + 2x - 1
    (::#1) (generic function with 1 method)
    
    julia> function (x)
               x^2 + 2x - 1
           end
    (::#3) (generic function with 1 method)
    
  • Với tính chất của function như vậy, giống như trong Python, ta cũng có các hàm xử lý các phần tử của chuỗi với 1 function được truyền vào như là map:

    julia> poli2 = x -> x^2 + 2x - 1
    (::#17) (generic function with 1 method)
    
    julia> map(poli2, [1.2, 3.4, 4.5])
    3-element Array{Float64,1}:
      2.84
     17.36
     28.25
    

6. Vectors, Arrays và Matrices (Ma trận)

  • Trong Julia thì ngoài Array, ta còn 2 loại type là VectorMatrix nhằm phục vụ cho các thao tác tính toán đại số tuyến tính. Tuy nhiên bản chất chúng chỉ là Array 1 chiều và Array 2 chiều
julia> Array{Int64, 1} == Vector{Int64}
true
julia> Array{Int64, 2} == Matrix{Int64}
true
  • Tương tự như khi sử dụng numpy với Python, ta cũng có thể đổi số chiều của Array với reshape. Tuy nhiên, hàm này không trả về 1 array mới mà chỉ trả về 1 view trên array cũ, do đó nếu ta thay đổi dữ liệu của array được trả về, array cũ cũng bị thay đổi
julia> a = [1, 3, 5, 7]
4-element Array{Int64,1}:
 1
 3
 5
 7

julia> b = reshape(a, 2, 2)
2×2 Array{Int64,2}:
 1  5
 3  7
Các phép toán trên Vector và Matrix
  • Nếu ai đã dùng Matlab thì các phép toán của Julia với Vector và Matrix cũng tương tự như vậy
  • Với Vector:
julia> a = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> a * 3
3-element Array{Int64,1}:
 3
 6
 9

julia> a / 2
3-element Array{Float64,1}:
 0.5
 1.0
 1.5

julia> a + 3
3-element Array{Int64,1}:
 4
 5
 6

julia> b = [4, 5, 6]
3-element Array{Int64,1}:
 4
 5
 6

julia> a + b
3-element Array{Int64,1}:
 5
 7
 9
  • Với Matrix hay Array 2 chiều, thì các phép toán */ giữa 2 Matrix sẽ là phép nhân và chia ma trận trong đại số tuyến tính
julia> b = [ 2 4; 2 1 ]
2×2 Array{Int64,2}:
 2  4
 2  1

julia> c = [1, 5]
2-element Array{Int64,1}:
 1
 5

julia> b * c
2-element Array{Int64,1}:
 22
  7

julia> b' * c # b' là ma trận ngược của b
2-element Array{Int64,1}:
 12
  9

julia> b \ c
2-element Array{Float64,1}:
  3.16667
 -1.33333

julia> inv(b) * c # inv là hàm trả về ma trận nghịch đảo (nếu có)
2-element Array{Float64,1}:
  3.16667
 -1.33333
Các thao tác trên từng phần tử và Vectorized Functions
  • Trong trường hợp muốn thực hiện các phép toán nhân hay chia ở dạng ma trận mà với từng phần tử thì ta thêm dấu . vào trước chúng
julia> a = [ 1 5; 2 5 ]
2×2 Array{Int64,2}:
 1  5
 2  5

julia> b = ones(2, 2)
2×2 Array{Float64,2}:
 1.0  1.0
 1.0  1.0

julia> a .* b
2×2 Array{Float64,2}:
 1.0  5.0
 2.0  5.0

julia> a ./ b
2×2 Array{Float64,2}:
 1.0  5.0
 2.0  5.0

julia> a .^ 2
2×2 Array{Int64,2}:
 1  25
 4  25
  • Phương pháp tương tự cũng được áp dụng với các thao tác so sánh như >, <, >=, <=, ==. Lợi dụng phương pháp này ta có thể lấy được các phần tử thoả mãn 1 điều kiện từ 1 Array.
julia> a = [ 1 2 3; 4 5 6 ]
2×3 Array{Int64,2}:
 1  2  3
 4  5  6

julia> a .> 2
2×3 BitArray{2}:
 false  false  true
  true   true  true

julia> a[a .> 2]
4-element Array{Int64,1}:
 4
 5
 3
 6
  • Với các thao tác tính toán thì là như vậy, nhưng trong trường hợp ta muốn áp dụng 1 hàm với mỗi 1 phần tử của Matrix hay Array thì sao. Sử dụng map hay list comprehension? Julia có cách hay hơn nhiều: thêm dấu . vào sau tên function và ta sẽ gọi đến phiên bản Vectorized của function đó.
julia> a = [ 1, 7, 8, 9 ]
4-element Array{Int64,1}:
 1
 7
 8
 9

julia> log.(a)
4-element Array{Float64,1}:
 0.0
 1.94591
 2.07944
 2.19722
  • Ngoài các function do Julia cung cấp, các function do người dùng tự viết cũng có cơ chế tương tự:
julia> poli2 = x -> x^2 + 2x - 1
(::#31) (generic function with 1 method)

julia> poli2.(a)
4-element Array{Int64,1}:
  2
 62
 79
 98

Wow, bài viết đến đây cũng là khá dài, tuy nhiên tất cả cũng vẫn chỉ là cưỡi ngựa xem hoa, chưa thể hiện được hết sức mạnh của Julia. Dù vậy, với những gì đã biết ở trên, ta có thể bắt tay vào viết một vài chương trình đơn giản với Julia.

Linear Regression đơn giản với Julia

Với những gì đã biết kèm thêm chút kĩ năng Google, chúng ta sẽ chuyển bài tập đầu tiên đơn giản nhất từ course Machine Learning trên Coursera, Linear Regression 1 biến số (không Normalization), từ Octave sang Julia.

  • Trước hết ta cùng nhìn lại công thức của Linear Regression 1 biến số:

  • Tiếp theo là công thức của Cost Function (nói đơn giản thì là độ sai trễ của model với gía trị thật) của model này

  • Tiếp theo nữa là công thức Gradient Descent

  • Một cách dễ hiểu: để train model Linear Regression 1 biến số này thì ta sẽ dùng Gradient Descent update các giá trị Theta trong công thức Linear regression sao cho giá trị Theta này sẽ khiến Cost Function có giá trị nhỏ nhất.

  • Xong phần lý thuyết, trước hết, ta cần load data và biểu diễn dưới dạng đồ thị:

using Plots

data = readdlm("data.txt", ',', Float64)
X = data[:, 1]
y = data[:, 2]

scatter(X, y, title="Profit ~ Population")
xlabel!("Profit/10000")
ylabel!("Population/10000")

  • Nhìn cũng thấy data này thích hợp với Linear Regression
  • Tiếp theo, viết hàm tính giá trị Cost Function. Dù không dùng khi training, nhưng ta nên biết được gía trị của Cost Function với mỗi Theta
function compute_cost(X, y, theta)
    num_ex = length(y)
    sum((((X * theta) - y) .^ 2) / (2 * num_ex))
end
  • Tiếp theo là hàm dùng Gradient Descent để update Theta. Đơn giản là cứ lặp theo số lần muốn update và ốp công thức. Ngoài việc trả về giá trị theta cuối cùng, thì hàm này cũng trả về giá trị Cost Function với giá trị Theta ở mỗi lần update
function gradient_descent(X, y, theta, alpha, num_iters)
    num_ex = length(y)
    j_hist = Array{Float64}(num_iters);

    for iter in 1:num_iters
        old_theta = theta

        for i in 1:length(theta)
          grad = sum(((X * old_theta) - y) .* X[:, i])
          theta[i] = old_theta[i] - (alpha/num_ex) * grad
        end
        
        j_hist[iter] = compute_cost(X, y, theta);
    end
    
    theta, j_hist
end
  • Vì vector X mới chỉ có 1 cột thể hiện biến x trong công thức Linear Regresion nên cần thêm 1 cột vào X với giá trị 1 tương ứng với hệ số Theta0. Đồng thời, ta cũng khởi tạo giá trị Theta ban đầu (là 0):
X_new = hcat(ones(length(X)), X); 
init_theta = zeros(2, 1)
  • Giai đoạn cuối cùng là training model. Ta sẽ chạy Gradient Descent 1500 bước (1500 lần update Theta) với learning rate 0.01
iterations = 1500
alpha = 0.01

thetas, j_hist = gradient_descent(X_new, y, init_theta, alpha, iterations)
  • Cuối cùng là đồ thị kết quả prediction của model với gía trị thực tế. Ngoài ra, ta cũng vẽ luôn đồ thị thể hiện giá trị Cost Function qua mỗi bước update Theta
scatter(X, hcat(X_new * thetas, y), title="Profit ~ Population")
xlabel!("Profit/10000")
ylabel!("Population/10000")

plot(1:iterations, j_hist, lw=3)
xlabel!("Step")
ylabel!("J")


Kết luận

  • Trong phạm vi của blog này cũng như ví dụ thực hành khá đơn giản, khó có thể thể hiện hết sức mạnh của Julia. Tuy nhiên, ta cũng có thể thấy Julia rất dễ học và sử dụng, ai đã quen thuộc với Python, Matlab đều có thể nhanh chóng làm quen và viết chương trình ngay lập tức được.

  • Xin hẹn gặp lại vào các bài viết đi sâu hơn các tính năng cao cấp của Julia

Link tham khảo