Việt Nam, 2024

Machine Learning
Forecasting

Ở đây ta sẽ học về mô hình machine learning được ứng dụng nhiều nhất trong việc phân tích dữ liệu thời gian là RNNLSTM.

1 Mô hình RNN:

1.1 Định nghĩa:

Điểm chung là cả hai mô hình đều thuộc phân lớp Deep learning - nghĩa là học máy sâu với đặc điểm chung là phân chia dữ liệu thành nhiều lớp và bắt đầu “học” dần qua từng lớp để đưa ra kết quả cuối cùng. Ở hình dưới đây, \(X_o\) đại diện cho dữ liệu đầu vào, \(h_t\) là output đầu ra của từng step và \(A\) là những gì đã “học” được tại step đó và được truyền cho step tiếp theo. Trong tài liệu chuẩn thì họ thường kí hiệu là \(X_t\), \(Y_t\), \(h_{t-1}\).

Hình 1: Minh họa về sự phân chia dữ liệu thành nhiều lớp
Source: Link to Image

Khi nhìn hình thì bạn có thể bối rối chưa hiểu các kí tự và hình ảnh thì bạn có thể tưởng tượng học máy như 1 đứa trẻ và để nó có thể hiểu được câu: “Hôm nay con đi học” thì nó phải học từng chữ cái như: a,b,c,… trước ròi mới ghép thành từ đơn như: “Hôm”,“Nay”,… rồi ghép thành câu trên.

Vậy giả sử như hôm nay học được từ “Hôm” thì nó sẽ bắt đầu ghi nhớ từ đã học vào trong \(A\). Nếu sau này ta cần học máy hiểu câu “Hôm sau con đi chơi” thì tốc độ học của học máy sẽ nhanh lên vì thay vì nó phải học 5 chữ đơn như thông thường thì nó chỉ cần học 4 chữ còn lại trừ chữ “hôm”. Vậy bạn đã hiểu ý tưởng nền tảng của RNN rồi ha!

1.2 Nguyên lí hoạt động:

Đầu tiên, RNN sẽ tính toán hidden state\(h_t\) với công thức là:

\[ \mathbf{h}_t = \text{activation}(\mathbf{W}_\text{hh} \mathbf{h}_{t-1} + \mathbf{W}_\text{xh} \mathbf{x}_t + \mathbf{b}_\text{h}) \]

Sau đó, \(h_t\) sẽ được làm input cho các state sau và dựa vào đó để tính output với công thức là:

\[ y_t = W_y \cdot h_t + b_y \]

Ví dụ: Mình muốn dự đoán hành động trong câu nói “I am reading book” bằng mô hình RNN như sau:

  • Bước 1: Chuyển đổi thành dạng số bằng embedding layer:

Mình sẽ gán từng từ đơn sang dạng số như:

  • “I” -> \(x_1\)

  • “am” -> \(x_2\)

  • “reading” -> \(x_3\)

  • “book” -> \(x_4\)

  • Bước 2: Thêm hidden layer và bắt đầu tính toán:

Cho input: “I”

\[ h_1 = \tanh(W_x \cdot x_1 + W_h \cdot h_0 + b) \]

Cho input: “am”

\[ h_2 = \tanh(W_x \cdot x_2 + W_h \cdot h_1 + b) \]

Cho input: “reading”

\[ h_3 = \tanh(W_x \cdot x_3 + W_h \cdot h_2 + b) \]

Cho input: “book”

\[ h_4 = \tanh(W_x \cdot x_4 + W_h \cdot h_3 + b) \]

  • Bước 3: Tính toán output: Dùng hàm activation softmax để phân lớp theo xác suất.

\[ \hat{y} = \text{softmax}(W_y \cdot h_4 + b_y) \]

Nếu muốn hiểu thêm về cách hoạt động RNN, bạn có thể tham khảo link này: Recurrent Neural Network: Từ RNN đến LSTM.

1.3 Vấn đề lớn của RNN:

RNN có 1 vấn đề lớn là Vanishing Gradient nghĩa là mô hình sẽ không còn “học” thêm được nữa cho dù tăng số epochs. Nguyên nhân vì sao như vậy thì bạn có thể tham khảo phần chứng minh của anh Tuấn.

Hình 2: Vanishing Gradient Problem
Source: Link to Image

Vấn đề này sẽ làm network khó update weight dẫn tới thời gian học lâu và khó để đạt được output. Bạn có thể hiểu đơn giản như việc bạn học liên tục dẫn tới quá tải và RNN cũng không như vậy. Do đó, RNN chỉ học các thông tin từ state gần và đó là lí do ra đời LSTM - Long short term memory.

Warning

Lưu ý: Điều này không có nghĩa LSTM luôn tốt hơn RNN vì có những bài toán với đầu vào đơn giản thì mô hình chỉ cần học các step đầu là đã “học” đầy đủ thông tin cần thiết. Mô hình LSTM phổ biến với các bài toán phức tạp như tự động dịch ngôn ngữ, ghi chép lại theo giọng nói…

1.4 Mô hình LSTM:

Có thể xem mô hình LSTM như biến thể của RNN. Về cấu trúc, LSTM phức tạp hơn RNN:

Hình 3: So sánh mô hình RNN và LSTM
Source: Link to Image

Cấu trúc cơ bản gồm:

  • Cổng quên (Forget Gate): có tác dụng quyết định thông tin nào cần bị quên trong trạng thái ô nhớ.

  • Cổng nhập (Input Gate): Xác định thông tin nào cần được ghi vào trạng thái ô nhớ.

  • Cổng xuất (Output Gate): Quyết định thông tin nào sẽ được xuất ra từ trạng thái ô nhớ để ảnh hưởng đến dự đoán tiếp theo.

Bạn có thể kham khảo thêm bài viết của dominhhai về cách hoạt động của RNNLSTM để hiểu thêm.

Tiếp theo, ta sẽ bắt đầu xây dựng thử mô hình trong R.

2 Xây dựng mô hình:

2.1 Load dữ liệu:

Đầu tiên ta sẽ load dữ liệu lại như trước. Ở đây, để đơn giản, mình chỉ xây dựng mô hình cho product A thôi.

Giả sử công ty mình đang kinh doanh 3 loại mặt hàng product A,product B,product C và đây là biểu đồ thể hiện nhu cầu của cả 3 mặt hàng từ tháng 5 tới tháng 10.

Code
library(highcharter)
sales_data |> 
  select(-Weekday) |> 
  pivot_longer(cols = c(Product_A, Product_B, Product_C),
               names_to = "Product",
               values_to = "Sales") |> 
  hchart("line", hcaes(x = Date, y = Sales, group = Product))

Nếu ta phân tich sâu về nhu cầu của từng mặt hàng theo thứ trong tuần, ta sẽ thấy rằng mặt hàng A, B thì bán chạy vào thứ 4 và thứ 7, còn mặt hàng C thì bán chạy vào thứ 2 và thứ 3.

Thông thường dữ liệu để train model trong machine learning thường cần trải qua bước normalize data nghĩa là đưa tất cả dữ liệu về chung 1 thước đo và phạm vi. Nguyên do vì điều này giúp nhiều thuật toán học máy dễ dàng hội tụ hơn. Ví dụ, các thuật toán như k-Nearest Neighbors (KNN)Support Vector Machines (SVM) rất nhạy cảm với khoảng cách giữa các điểm dữ liệu nên nếu dữ liệu không được chuẩn hóa, thuật toán có thể ưu tiên các đặc trưng có phạm vi lớn hơn và bỏ qua các đặc trưng có phạm vi nhỏ hơn, dẫn đến hiệu suất kém. Và công thức phổ biến nhất cho chuẩn hóa là:

\[ \text{Normalized Value} = \frac{x - \min(x)}{\max(x) - \min(x)} \]

Code
# Create a data frame with the adjusted sales data
sales_data <- data.frame(
  Date = dates,
  Weekday = weekdays,
  Product_A = product_a_sales,
  Product_B = product_b_sales,
  Product_C = product_c_sales
)

# Convert the sales data to a time series (ts) object for Product A
product_a_ts <- ts(sales_data$Product_A, start = c(2024, 5), 
                   frequency = 365)
                   

# Normalzie data:
time_series_data<-scale(product_a_ts)

library(highcharter)
highchart() |>
  hc_add_series(data = as.numeric(time_series_data), type = "line", name = "Sales of Product A") |>
  hc_title(text = "Normalized Time Series of Product A") |>
  hc_xAxis(title = list(text = "Date")) |>
  hc_yAxis(title = list(text = "Normalized Sales")) |>
  hc_tooltip(shared = TRUE) |>
  hc_plotOptions(line = list(marker = list(enabled = FALSE)))

2.2 Chia dữ liệu:

Vậy để train data, mình sẽ chia bộ dữ liệu thành 3 phần:

  • Training data: dùng để huấn luyện và xây dựng mô hình.

  • Evaluating data: đánh giá mô hình vừa huấn luyện.

  • Testing data: dùng để đánh giá lại nếu muốn mô hình học lại dữ liệu

Code
sales_data <- data.frame(
  Date = dates,
  Weekday = weekdays,
  Product_A = product_a_sales,
  Product_B = product_b_sales,
  Product_C = product_c_sales
)

# Convert the sales data to a time series (ts) object for Product A
time_series_data <- scale(ts(sales_data$Product_A, start = c(2024, 5), 
                   frequency = 365))
                   

create_supervised_data <- function(series, n) {
  series <- as.vector(series)  # Convert time series object to vector
  data <- data.frame(series)    # Initialize data frame with the original series
  
  # Create lag columns
  for (i in 1:n) {
    lagged_column <- lag(series, i)  # Get lagged values
    data <- cbind(data, lagged_column)  # Add lagged column to the data
  }
  
  # Name the columns properly
  colnames(data) <- c(paste0('t-', n:1), 't+1')
  
  # Remove rows with NA values (those at the start of the series due to lagging)
  data <- na.omit(data)
  
  return(data)
}

# Prepare the data with 12 input lags and 1 output (next time step)
supervised_data <- create_supervised_data(time_series_data,
                                          n = 50)

# Step 2: Split data into training and test sets
train_size <- round(0.7 * nrow(supervised_data))   # 70% for training
val_size <- round(0.1 * nrow(supervised_data))     # 10% for validation
test_size <- nrow(supervised_data) - train_size - val_size  # 20% for testing

train_data <- supervised_data[1:train_size, ]
val_data <- supervised_data[(train_size + 1):(train_size + val_size), ]
test_data <- supervised_data[(train_size + val_size + 1):nrow(supervised_data), ]

# Correct column selection
x_train <- as.matrix(train_data[, 1:50])  # Input features (12 lags)
y_train <- as.matrix(train_data[, 't+1'])  # Target output (next time step)

x_val <- as.matrix(val_data[, 1:50])  # Input features for validation
y_val <- as.matrix(val_data[, 't+1'])  # Actual output for validation

x_test <- as.matrix(test_data[, 1:50])  # Input features for testing
y_test <- as.matrix(test_data[, 't+1'])  # Actual output for testing

## Plot the result:
library(xts)
n<-quantile(sales_data$Date, 
            probs = c(0, 0.7, 0.8,1), 
            type = 1)

m1<-sales_data |> 
  filter(Date <= n[[2]])
m2<-sales_data |> 
  filter(Date <= n[[3]] & Date > n[[2]])
m3<-sales_data |> 
  filter(Date <= n[[4]] & Date > n[[3]])

demand_training<-xts(x=m1$Product_A,
                     order.by=m1$Date)
demand_testing<-xts(x=m2$Product_A,
                     order.by=m2$Date)
demand_forecasting<-xts(x=m3$Product_A,
                     order.by=m3$Date)

library(dygraphs)
lines<-cbind(demand_training,
             demand_testing,
             demand_forecasting)
dygraph(lines,
        main = "Training and testing data", 
        ylab = "Quantity order (Unit: Millions)") |> 
  dySeries("demand_training", label = "Training data") |>
  dySeries("demand_testing", label = "Testing data") |>
  dySeries("demand_forecasting", label = "Forecasting data") |>
  dyOptions(fillGraph = TRUE, fillAlpha = 0.4) |> 
  dyRangeSelector(height = 20)

2.3 Mô hình RNN:

Sau đó, ta sẽ bắt đầu train model bằng cách tạo thêm 12 cột giá trị là giá trị quá khứ của demand. Bạn sẽ bắt đầu định nghĩa mô hình gồm:

  • Input: dùng hàm layer_input(shape = input_shape) với input_shape là số lượng predictor.

  • Layer: là các hidden layer trong mô hình thêm vào bằng hàm layer_dense(x, units = 64, activation = 'relu') với đối số units thường là bội số của 32 như 32,64,256,…

  • Output: dùng hàm layer_dense(x, units = 1) để định nghĩa là đầu ra chỉ có 1 giá trị.

Đối với các mô hình truyền thống như linear regression thì bạn đã quen với thông số \(R^2\) để đánh giá mô hình, còn với mô hình Machine learning thì dùng khái niệm loss function - hàm mất mát. Về khái niệm, loss function sẽ đo lường chênh lệch giữa predictedactual trong bộ training data nên khi càng tăng epochs nghĩa là tăng số lần học lại dữ liệu thì loss function sẽ tính ra giá trị càng thấp. Như mô hình trên thì mình đặt đối số loss = mse nghĩa là sử dụng Mean Squared Error để tối ưu quy trình học của học máy. Công thức như sau:

\[ MSE = \frac{1}{n} \sum_{i=1}^{n} (y_{\text{pred}}(i) - y_{\text{true}}(i))^2 \]

Còn đối số metrics = c('mae') nghĩa là tiêu chí khác để theo dõi và đánh giá mô hình. Vậy tại sao cần có 2 tham số đánh giá song song như vậy là vì như đã nói, nếu bạn càng tăng epochs thì giá trị loss càng thấp trong khi dùng metrics sẽ đưa ra đánh giá khách quan hơn về mô hình mà không phụ thuộc vào số lần epochs. Công thức như sau:

\[ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_{\text{pred}}(i) - y_{\text{true}}(i)| \]

Vậy khi chạy code, R sẽ return output như biểu đồ dưới đây là so sánh tham số của msemae giữa training dataevaluating data. Ý tưởng là đánh giá thử mô hình có dự đoán tốt không khi có dữ liệu mới vào.

Tiếp theo, ta sẽ dùng test data để đánh giá mô hình vừa xây dựng. Kết quả có vẻ khá ổn vì mô hình gần như theo sát được dữ liệu của test data.

Code
# Step 6: Make predictions
RNN_forecast <- RNN_model |> 
  predict(x_test)
1/1 - 0s - 291ms/step
Code
# Step 7: Combine predicted and observed
plot_data <- data.frame(
  actual = y_test,  # Actual values from the test set
  forecast = RNN_forecast  # Forecasted values
)

# Step 8: Plot using Highcharts
highchart() |>
  hc_title(text = "Time Series Forecasting with Highcharts") |>
  hc_xAxis(
    categories = plot_data$time,
    title = list(text = "Time")
  ) |>
  hc_yAxis(
    title = list(text = "Value"),
    plotLines = list(list(
      value = 0,
      width = 1,
      color = "gray"
    ))
  ) |>
  hc_add_series(
    name = "Actual Data",
    data = plot_data$actual,
    type = "line",
    color = "#1f77b4"  # Blue color for actual data
  ) |>
  hc_add_series(
    name = "Forecast",
    data = plot_data$forecast,
    type = "line",
    color = "#ff7f0e"  # Orange color for forecast data
  ) |>
  hc_tooltip(
    shared = TRUE,
    crosshairs = TRUE
  ) |>
  hc_legend(
    enabled = TRUE
  )

2.4 Mô hình LSTM:

Tiếp theo, ta sẽ xây dựng thử mô hình LSTM. Mô hình LSTM thường bao gồm các lớp sau:

  • Lớp LSTM: Đây là lớp chính, có thể có một hoặc nhiều lớp LSTM chồng lên nhau. Mỗi lớp LSTM có thể trả về toàn bộ chuỗi bằng return_sequences = TRUE hoặc chỉ trả về giá trị cuối cùng bằng return_sequences = FALSE.

  • Lớp Dense: Sau khi thông tin được xử lý qua các lớp LSTM, nó sẽ được đưa qua các lớp Dense (lớp fully connected) để đưa ra dự đoán cuối cùng.

  • Lớp Dropout (tùy chọn): Để tránh overfitting, có thể thêm lớp dropout để tắt ngẫu nhiên một số nơ-ron trong quá trình huấn luyện.

Vậy giờ ta sẽ so sánh với mô hình RNN trước với mô hình LSTM qua 2 thông số đã chọn msemae.

Code
# Extract metrics into a data frame
results_df <- data.frame(
  Model = c("RNN", "LSTM"),
  MSE = c(RNN_result[[1]],RNN_result[[2]]),
  MAE = c(LSTM_result[[1]], LSTM_result[[2]])
)

library(gt)
# Create a gt table
results_df |>
  gt() |>
  tab_header(
    title = "Model Performance Metrics",
    subtitle = "Comparison of MSE and MAE for RNN and LSTM"
  ) |>
  fmt_number(
    columns = vars(MSE, MAE),
    decimals = 6
  ) |>
  cols_label(
    Model = "Model Type",
    MSE = "Mean Squared Error",
    MAE = "Mean Absolute Error"
  ) |>
  tab_options(
    table.font.size = 14,
    heading.title.font.size = 16,
    heading.subtitle.font.size = 14
  )
Model Performance Metrics
Comparison of MSE and MAE for RNN and LSTM
Model Type Mean Squared Error Mean Absolute Error
RNN 0.622324 0.790441
LSTM 0.637329 0.735569

Kết quả cho thấy mô hình LSTM đưa ra kết quả tốt hơn RNN với độ sai số thấp hơn nhưng nếu bạn để ý thì thấy trong biểu đồ mình vẫn để % âm để dễ phân biệt giữa việc outstockhigh inventory (bởi vì bạn đang dự báo cho nhu cầu của khách hàng). Bạn có thể so sánh thêm 1 bước nữa về tổng chi phí giữa 2 mô hình về outstockholding cost để có cái nhìn tổng quan nhất.

Code
LSTM_forecast <- LSTM_model |> 
  predict(x_test)
1/1 - 0s - 350ms/step
Code
compare<-data.frame(Date = 1:length(y_test),
                    LSTM = round((LSTM_forecast - y_test)/y_test,3),
                    RNN = round((RNN_forecast - y_test)/y_test,3)
)

# Create the highchart plot with percentage formatting for y-axis
highchart() |>
  hc_chart(type = "line") |>
  hc_title(text = "Residual Comparison: LSTM vs RNN") |>
  hc_xAxis(
    categories = compare$Date,
    title = list(text = "Date")
  ) |>
  hc_yAxis(
    title = list(text = "Residuals"),
    labels = list(
      formatter = JS("function() { return (this.value).toFixed(0) + '%'; }")  # Format labels as percentages
    ),
    plotLines = list(
      list(value = 0, color = "gray", width = 1, dashStyle = "Dash")
    )
  ) |>
  hc_add_series(
    name = "LSTM Residuals",
    data = compare$LSTM,
    color = "#1f77b4"
  ) |>
  hc_add_series(
    name = "RNN Residuals",
    data = compare$RNN,
    color = "#ff7f0e"
  ) |>
  hc_tooltip(shared = TRUE) |>
  hc_legend(enabled = TRUE)

2.5 Xác định cấu trúc mô hình:

Nếu bạn để ý, thực chất code cho mô hình cho như mình đã trình bày thì khá đơn giản và điều khó nhất trong mô hình là xác định số lớp layer trong mô hình. Như bài toán time series forecasting thì mình chỉ cần 2,3 lớp layer đơn giản là đã đạt kết quả tốt với sai số rất thấp (< 0.03), còn với các bài toán phức tạp hơn thì số layer sẽ nhiều hơn.

Vậy quy tắc xác định mô hình là như thế nào ? Câu trả lời là không có quy tắc nào cả và chỉ có các tips mà mình lụm nhặt trên mạng như sau:

2.5.1 Number of layer:

Số layer nên nằm giữa số input và số output. Như bài thực hành trên thì số layer nên nằm trong khoảng (1,12). Hoặc bạn có thể sử dụng hàm dưới đây để xác định.

\[ N_h = \frac{N_s}{\alpha \cdot (N_i + N_o)} \]

Với các tham số gồm:

  1. \(N_h\) là số lượng hidden neurons.

  2. \(N_s\) là số lượng mẫu trong training data.

  3. \(\alpha\) là yếu tố tỷ lệ tùy ý (thường từ 2-10).

  4. \(N_i\) là số lượng nơ-ron input

  5. \(N_o\) là số lượng nơ-ron output.

Ví dụ như ở mô hình trên thì số hidden layer sẽ khoảng 1-4 layer là ổn (Như trên thì mình dùng 1 layer cho mô hình RNN, 2 layer cho mô hình LSTM)

2.5.2 Choose acvtivation function:

Các hàm activation dùng để tính weighted sum và mỗi layer sẽ cần có 1 hoặc nhiều hàm để tính. Việc lựa chọn hàm ảnh hưởng lớn đến performance của mô hình, thường sẽ được chia thành 3 phần là:

  1. Activation for input layer: thường ko dùng hàm gì cả. Bạn chỉ thực hiện processing dữ liệu để training.

  2. Activation for Hidden Layers:

Thông thường, hàm Tanh thì phù hợp cho dự báo giá trị liên tục từ dữ liệu chuỗi, ReLU giúp cho quá trình training nhanh hơn và không gây ra vanishing problem do không bị chặn, Softmax thường dùng ở output layer cho bài toán classification, Sigmoid thường dùng cho hồi quy logic. Ngoài ra, cách đơn giản hơn là tùy vào loại mô hình bạn đang xây dựng để lựa chọn, ví dụ như tips dưới đây mình tìm hiểu được:

Hình 4: Tips chọn hàm activation cho hidden layer
Source: Link to Image
  1. Activation for Output Layers:

Đối với output layer, bạn sẽ lựa chọn hàm dựa trên class của output mà bạn đang hướng đến. Các hàm thông thường sẽ gồm:

  • Linear: hay còn gọi là “identity” (nhân với 1.0) hoặc “no activation” bởi vì hàm linear tuyến tính không thay đổi weighted sum của input theo bất kỳ cách nào và thay vào đó trả về giá trị trực tiếp. Hàm này thường dùng cho output dạng liên tục.

  • Logistic (Sigmoid): áp dụng cho output dạng [0,1] hay còn gọi là binary classification (ví dụ mô hình nhằm đưa ra quyết định có/không trong việc đầu tư vào cổ phiểu này chẳng hạn).

  • Softmax: Hàm này sẽ chuyển đổi một vector thành các giá trị xác suất có tổng bằng 1 (Nó giống như tìm hàm mật độ (PDF) cho một biến). Ứng dụng để dán nhãn cho multiclass thay vì 2 class như hàm sigmoid bên trên. Mỗi nhãn sẽ có 1 giá trị xác suất và dựa vào đó dự đoán khả năng xảy ra của từng class.

2.5.3 Number of neurons:

Số lượng nơ-ron trong một lớp quyết định lượng thông tin mà mạng có thể lưu trữ. Nhiều nơ-ron giúp mạng học được các mẫu phức tạp hơn, nhưng cũng làm tăng nguy cơ overfitting (quá khớp) và yêu cầu nhiều tài nguyên tính toán hơn. Bạn có thể bắt đầu với một số lượng nơ-ron tương đối nhỏ, như 128 hoặc 256…

3 Kết luận:

Như vậy, chúng ta đã được học về mô hình RNNLSTM và cách xây dựng chúng trong R. Tiếp theo, ta sẽ học tiếp về mô hình Transformer

Contact Me

Contact Me