Part 1: 摘要:事件辨識應用說明
- 如下圖所示, 假設事件是由1紅球與1綠球構成, 球由左向右移動, 經過事件感測器, 感測器將會產生訊號, 這些球大小相似, 但會有略微差異, 而球與球的間距則不一定, 因此感測器生成的時間序列就如圖所示, 而由於人知道1紅1綠通過就是一個事件, 引此我們可以用人的判別方式對訊號標示事件發生的起點與終點, 亦即綠球何時進入感測區域, 紅球何時離開感測區域.
事件辨識應用介紹
目標是經過一段時間記錄後, 自動辨識該段時間所有事件發生起點與終點.
然而我們可以注意到, 由於感測器無法辨識出顏色, 因此綠球與紅球的訊號會很相似, 此外球的大小及球與球的間距皆非定值, 這些都造成了自動辨識的困難.
在無法有效以演算法的方式處理下, 我們嘗試用機器學習來解決問題, 這裡特別提出深度學習 (keras + tensorflow-gpu). 使用常見的sliding window (位移n時間單位) 處理原始序列 (長度L), 生成長度為T的子序列, 可產生 (L - T)/n + 1筆子序列.
深度學習使用簡述
分類模型對各時間點之分類正確度 > 88%, 但對於此類對稱性事件, 在既有訊號種類, 尚無法達到良好的事件辨識率. 在可接受的誤差範圍內, 測試組中63%的事件被正確找出.
此外, 從模型分析來看, 再增加資料對模型改善有限, 需要做的是找出紅球與綠球的具差異性的訊號, 例如目前尚未加入的顏色辨識.
Part 2: 資料前處理與樣本準備
Sys.setlocale("LC_TIME", "English")
#caret亦用於建模過程
suppressMessages(library(readr))
suppressMessages(library(dplyr))
suppressMessages(library(plotly))
suppressMessages(library(keras))
suppressMessages(df <- read_csv("df_raw.csv", guess_max = 60000))資料概述
value: 感測器因事件出現回傳的訊號值
label: 人眼判別事件發生與否 (1個事件會佔據一段連續的時間)
sensor: 感測器id
| value | label | sensor |
|---|---|---|
| 0.3352 | 1 | 2 |
| 0.3451 | 1 | 2 |
| 0.3563 | 1 | 2 |
| 0.3563 | 1 | 2 |
| 0.4718 | 1 | 2 |
| 0.6183 | 1 | 2 |
| 0.7648 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.4155 | 1 | 2 |
| 0.6183 | 1 | 2 |
| 0.8225 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.8169 | 1 | 2 |
| 0.6479 | 1 | 2 |
| 0.4789 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.4549 | 1 | 2 |
| 0.6958 | 1 | 2 |
| 0.9366 | 1 | 2 |
| 0.8831 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.6986 | 1 | 2 |
| 0.6986 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.4606 | 1 | 2 |
| 0.4606 | 1 | 2 |
| 0.3028 | 1 | 2 |
| 0.4423 | 1 | 2 |
| 0.5831 | 1 | 2 |
| 0.6493 | 1 | 2 |
| 0.6493 | 1 | 2 |
| 0.6592 | 1 | 2 |
| 0.5915 | 1 | 2 |
| 0.4662 | 1 | 2 |
| 0.4676 | 1 | 2 |
| 0.6831 | 1 | 2 |
| 0.5803 | 1 | 2 |
| 0.4606 | 1 | 2 |
| 0.3803 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.3366 | 1 | 2 |
| 0.3394 | 1 | 2 |
| 0.3437 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.2944 | 1 | 2 |
| 0.3183 | 1 | 2 |
| 0.3423 | 1 | 2 |
| 0.3310 | 1 | 2 |
| 0.5423 | 1 | 2 |
| 0.6958 | 1 | 2 |
| 0.8493 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.4817 | 1 | 2 |
| 0.6859 | 1 | 2 |
| 0.8901 | 1 | 2 |
| 0.8901 | 1 | 2 |
| 0.8662 | 1 | 2 |
| 0.8662 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.6887 | 1 | 2 |
| 0.6000 | 1 | 2 |
| 0.5113 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.5268 | 1 | 2 |
| 0.7070 | 1 | 2 |
| 0.8859 | 1 | 2 |
| 0.8859 | 1 | 2 |
| 0.4070 | 1 | 2 |
| 0.5845 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.4704 | 1 | 2 |
| 0.6408 | 1 | 2 |
| 0.8113 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.1634 | 1 | 2 |
| 0.3958 | 1 | 2 |
| 0.6296 | 1 | 2 |
| 0.6986 | 1 | 2 |
| 0.7042 | 1 | 2 |
| 0.7338 | 1 | 2 |
| 0.5380 | 1 | 2 |
| 0.4366 | 1 | 2 |
| 0.5465 | 1 | 2 |
| 0.6718 | 1 | 2 |
| 0.4732 | 1 | 2 |
| 0.4535 | 1 | 2 |
| 0.0000 | 1 | 2 |
| 0.3028 | 1 | 2 |
| 0.3408 | 1 | 2 |
| 0.3183 | 1 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
| 0.0000 | 0 | 2 |
- 時間序列處理與樣本生成
2.1 每個sensor代表一個連續記錄的時間序列, 長度為L, 而事件會間隔些許時間出現.
2.2 排列事件發生的時間長度, 取95%分位數的2倍 (T) 做為模型訓練序列的長度.
2.3 模型訓練用的序列之sliding window的位移長度n = T/3.
# 藉由事件發生之前label = 0來定義事件編號 (count)
dft <- df %>%
group_by(sensor) %>%
mutate(lag_label = lag(label),
lag_label = ifelse(is.na(lag_label), 0, lag_label),
count = ifelse(label == 1 & lag_label == 0, 1, 0),
count = cumsum(count),
# 將count與sensor結合, 成為唯一值
count = paste0(sensor, "_", count)
) %>%
ungroup() %>%
mutate(count = as.numeric(as.factor(count))) %>%
# 事件長度 = 事件發生時間 + 到下一事件發生前的間歇時間
group_by(count) %>%
mutate(len = length(count)) %>%
filter(row_number() == 1) %>%
ungroup()
# 計算時間長度分佈, 取log處理後的95分位數
event_len_ln <- quantile(log(dft$len, exp(1)), probs = 0.95)[[1]]
#還原event_len_ln, 取其整數2倍做為取樣序列長度 (T = timestep)
timestep <- ceiling(exp(event_len_ln))*2
#將timestep設定為3可以整除, 便於後續資料sliding window取樣及將test切片後樣本還原
if(timestep %% 3 != 0){
timestep <- timestep - timestep%%3
}2.4 處理測試樣本: 對連續的原始序列, 以T長度進行切割, 並記錄切割的參考點, 用於模型分類完後, 將預測值重組成原始序列; 假設原始序列1,2,3,4,5,6,7,8,9, 會先頭尾padding成0,0,0,1,2,3,4,5,6,7,8,9,0,0,0, 然後產生測試片段如下:
0,0,0,1,2,3,4,5,6 -> 經模型分類完成後, 只取中間1,2,3的分類結果
1,2,3,4,5,6,7,8,9 -> 只取中間4,5,6的分類結果
4,5,6,7,8,9,0,0,0 -> 只取中間7,8,9的分類結果
如此可將測試結果重組, 並對照回原本的測試序列
# 使用20%的sensor之原始序列做為測試組
unique_sensor <- unique(df$sensor)
sensor_test <- sample(x = unique_sensor, size = ceiling(0.2*length(unique_sensor)))
for(i in sensor_test){
sub_test <- df %>% filter(sensor == i)
#將時間序列以1/3 timestep區隔, 若最後一個不滿1/3 timestep, 將其padding
if(nrow(sub_test)%%(timestep/3) != 0){
idx1 <- nrow(sub_test) + 1
idx2 <- (timestep/3)*(nrow(sub_test)%/%(timestep/3)+ 1)
# padding sensor = sensor used in the loop
# padding value & label = 0
sub_test[idx1:idx2, c("sensor")] <- i
sub_test <- sub_test %>%
mutate(value = ifelse(is.na(value), 0, value),
label = ifelse(is.na(label), 0, label)
)
#註記padding長度, 用於後續資料還原
padding_len <- idx2 - idx1 + 1
rm(idx1, idx2)
} else{
padding_len <- 0
}
# 對頭尾各補上1/3 timesteps, 並padding資料
# 先在尾端增加2/3長度的 timesteps
idx1 <- nrow(sub_test) + 1
idx2 <- nrow(sub_test) + (timestep/3)*2
sub_test[idx1:idx2, c("sensor")] <- i
# 將原始序列往後位移 1/3 timesteps, 形成頭尾的空白padding
sub_test <- sub_test %>%
mutate(value = lag(value, (timestep/3)),
label = lag(label, (timestep/3)),
sensor = lag(sensor, (timestep/3)),
value = ifelse(is.na(value), 0, value),
label = ifelse(is.na(label), 0, label),
sensor = ifelse(is.na(sensor), i, sensor)
)
rm(idx1, idx2)
#計算seq_segment = 有幾個1/3 timesteps長度的seq
#建構測試樣本片段: 從第2個seq (真實訊號的起始區間) 開始取, 並以前1及後1個seq分別padding所選片段的頭與尾, 持續迴圈直到倒數第2個seq (真實訊號的最後區間)
seq_segment <- nrow(sub_test) %/% (timestep/3)
for(j in 2:(seq_segment-1)){
idx1 <- (timestep/3)*(j-2) + 1
idx2 <- (timestep/3)*(j+1)
if(j == 2){
sub_test_seg <- sub_test %>%
filter(row_number() >= idx1 & row_number() <= idx2)
} else{
sub_test_seg_con <- sub_test %>%
filter(row_number() >= idx1 & row_number() <= idx2)
sub_test_seg <- rbind(sub_test_seg, sub_test_seg_con)
}
}
if(i == first(sensor_test)){
# sub_test_seg為每一sensor切割padding完成後的資料
# test_final為所有sensor的sub_test_seg的集合
test_final <- sub_test_seg
#計算每一sensor需要被還原的真實樣本位置
select_idx <- NULL
for(k in 1:(nrow(sub_test_seg)%/%timestep)){
if(k < (nrow(sub_test_seg)%/%timestep)){
select1 <- timestep*(k-1) + timestep/3 + 1
select2 <- timestep*(k-1) + timestep/3*2
select_idx <- c(select_idx, select1:select2)
} else{
select1 <- timestep*(k-1) + timestep/3 + 1
select2 <- timestep*(k-1) + timestep/3*2 - padding_len
select_idx <- c(select_idx, select1:select2)
}
}
} else {
for(k in 1:(nrow(sub_test_seg)%/%timestep)){
if(k < (nrow(sub_test_seg)%/%timestep)){
select1 <- timestep*(k-1) + timestep/3 + 1 + nrow(test_final)
select2 <- timestep*(k-1) + timestep/3*2 + nrow(test_final)
select_idx <- c(select_idx, select1:select2)
} else{
select1 <- timestep*(k-1) + timestep/3 + 1 + nrow(test_final)
select2 <- timestep*(k-1) + timestep/3*2 - padding_len + nrow(test_final)
select_idx <- c(select_idx, select1:select2)
}
}
test_final <- rbind(test_final, sub_test_seg)
}
}
# 編碼test_final內各段落的id, 用於keras資料格式處理
test_final <- test_final %>%
group_by(sensor) %>%
mutate(id = ceiling(row_number()/timestep)
) %>%
ungroup() %>%
mutate(id = paste0(sensor, "_", id),
id = as.integer(factor(id, levels = unique(id)))
) %>%
select(-sensor)
# 建立測試組的id, 用於keras資料處理時分出train與test
test_id <- 1:max(test_final$id)
rm(dft, sub_test, sub_test_seg, sub_test_seg_con, idx1, idx2, select1, select2, seq_segment)
invisible(gc())2.5 處理訓練樣本: 直接sliding window切割各sensor的資料, 不用像測試樣本做客製化padding處理, 後續keras資料準備時進行簡易padding即可.
max_id <- max(test_final$id)
# 產生測試組的原始資料序列, 可用於分類完成後的分析
eval <- df %>%
mutate(match = match(sensor, sensor_test, nomatch = NA)) %>%
filter(!(is.na(match)))
# 移除df內的測試組資料
df <- df %>%
mutate(match = match(sensor, sensor_test, nomatch = NA)) %>%
filter(is.na(match))
# sliding window取樣的方式是以重新編碼id來達成
for(i in 0:2){
dft <- df %>%
group_by(sensor) %>%
mutate(id = ceiling(lag(row_number(), timestep/3*i)/timestep)) %>%
ungroup() %>%
mutate(id = ifelse(is.na(id), 0, id),
id = paste0(sensor, "_", id),
id = as.integer(factor(id, levels = unique(id))) + max_id
) %>%
select(-sensor, -match)
if(i == 0){
train_final <- dft
} else{
train_final <- rbind(train_final, dft)
}
max_id <- max(train_final$id)
}
df_final <- rbind(test_final, train_final)
rm(dft, df, test_final, train_final)
invisible(gc())Part 3: keras資料前處理與建立深度學習模型
- 將dataframe轉換成keras可讀取的list資料格式, 並依照之前的id設定, 分出train與test組別.
y_list <- list()
x_list <- list()
for(i in unique(df_final$id)){
x_list[length(x_list)+1] <- list(df_final$value[df_final$id == i])
y_list[length(y_list)+1] <- list(df_final$label[df_final$id == i])
}
x_train <- pad_sequences(x_list, maxlen = timestep, padding='pre',
value = 0)
y_train <- pad_sequences(y_list, maxlen = timestep, padding='pre',
value = 0)
#dim最後一個argument是data dimension (features)
x_train <- array(x_train, dim = c(length(x_list), timestep, 1))
y_train <- array(y_train, dim = c(length(y_list), timestep, 1))
x_test <- array(x_train[test_id, ,], dim = c(length(test_id), timestep, 1))
y_test <- array(y_train[test_id, ,], dim = c(length(test_id), timestep, 1))
x_train <- array(x_train[-test_id, ,], dim = c(length(x_list) - length(test_id), timestep, 1))
y_train <- array(y_train[-test_id, ,], dim = c(length(y_list) - length(test_id), timestep, 1))
rm(df_final, x_list, y_list)
invisible(gc())- 建立深度學習模型, 只使用bidirectional LSTM. 未來可建立更深的模型, 或使用ResNet架構來訓練.
model <- keras_model_sequential()
model %>%
bidirectional(
layer_cudnn_lstm(units = 128, return_sequences = TRUE, input_shape = c(timestep, 1)),
merge_mode = 'concat'
) %>%
layer_dense(units = 1, activation = 'sigmoid') %>%
compile(
loss = 'binary_crossentropy',
optimizer = 'adam',
metrics = c('accuracy')
)
# epoch需要測試, 越高model越fit training set
history <- model %>% fit(
x_train, y_train,
epochs = 100, batch_size = 32,
validation_split = 0.1
)- 以訓練好的模型對測試組分類, 並將結果重組還原, 以對應測試組原始的資料序列.
pred <- predict(model, x_test, batch_size = 32, verbose = 1)
pred_DL <- NULL
for(i in 1:dim(pred)[1]){
pred_DL <- c(pred_DL, pred[i, 1:timestep, 1])
}
eval$pred_DL <- pred_DL[select_idx]
rm(pred, pred_DL)Part 4: 分類模型表現分析
- 觀察train-vliad accuracy history可以發現epoch到60後, valid未隨train的提升而提升.
模型表現隨epoch變化圖
分析訓練集資料量對模型表現的影響:
全部事件為10000筆, 固定測試集為特定的2000筆事件, 訓練集最多可使用8000筆事件, 觀察訓練集使用比例對準確度的影響.
結果顯示繼續增加資料量對於模型表現改善有限, 如要改良模型需要增加其他種類資料.
| train_data | train_accuracy | valid_accuracy | test_accuracy |
|---|---|---|---|
| 12.5% | 0.900 | 0.891 | 0.886 |
| 25% | 0.928 | 0.892 | 0.868 |
| 50% | 0.902 | 0.883 | 0.892 |
| 75% | 0.917 | 0.895 | 0.896 |
| 100% | 0.913 | 0.890 | 0.887 |
藉由前面的觀察, 使用所有訓練資料及epoch = 60建立最終用於測試組別的模型, 得到的分類準確度為0.887, 這代表每個時間點被正確分類的準確度.
然而, 我們更有興趣的是事件被成功定位的準確度, 我們定義如下:
\[ 事件辨識準確度 = 成功辨識事件(TP)/(實際事件數 + 錯誤辨識事件(FP)) \]
定義分類結果連續的1為一個事件, 例如11100011代表內有2個事件, 而這兩個事件發生的起訖點分別為(1,3)及(7,8).
從模型分類結果產生的事件, 其發生時間的起訖點與實際事件的起迄點之誤差不可大於10%事件時間長度, 例如事件時間長度為100, 則允許誤差為10. 符合條件為TP, 不符合條件為FP.
10000筆事件中, 長度分布在80~800個時間單位, 中位數為220.
# 設定允許誤差比例
allow_err_ratio <- 0.1
# 實際事件的起迄時間點
label_tbl <- eval %>%
mutate(position = row_number()) %>%
group_by(sensor) %>%
mutate(lag_label = lag(label),
lag_label = ifelse(is.na(lag_label), 0, lag_label),
count = ifelse(label == 1 & lag_label == 0, 1, 0),
count = cumsum(count),
count = paste0(sensor, "_", count)
) %>%
ungroup() %>%
mutate(count = as.numeric(as.factor(count))) %>%
filter(!(label == 0)) %>%
group_by(count) %>%
mutate(start = first(position),
end = last(position)
) %>%
filter(row_number() == 1) %>%
ungroup() %>%
select(start, end) %>%
mutate(allow_err = floor((end- start + 1)*allow_err_ratio),
# 計算在允許誤差範圍內的可接受start位置
start_1 = start - allow_err,
start_1 = ifelse(start_1 < 1, 1, start_1),
start_2 = start + allow_err,
start_2 = ifelse(start_1 > nrow(eval), nrow(eval), start_2)
)
# 模型辨識事件的起迄時間點
pred_tbl <- eval %>%
mutate(position = row_number()) %>%
group_by(sensor) %>%
mutate(lag_pred = lag(pred_DL),
lag_pred = ifelse(is.na(lag_pred), 0, lag_pred),
count = ifelse(pred_DL == 1 & lag_pred == 0, 1, 0),
count = cumsum(count),
count = paste0(sensor, "_", count)
) %>%
ungroup() %>%
mutate(count = as.numeric(as.factor(count))) %>%
filter(!(pred_DL == 0)) %>%
group_by(count) %>%
mutate(start = first(position),
end = last(position)
) %>%
filter(row_number() == 1) %>%
ungroup() %>%
select(start, end) %>%
mutate(result = 0)
for(i in 1:nrow(label_tbl)){
s1 <- label_tbl$start_1[i]
s2 <- label_tbl$start_2[i]
# 查找模型產生的事件中, 是否有符合實際事件的開始時間(s1:s2)
# 如果沒有, 則換到下一個實際事件
idx <- which(pred_tbl$start %in% s1:s2)
if(length(idx) == 0){next}
# 計算剩餘可使用誤差時間單位
allow_err_left <- abs(pred_tbl$start[idx] - label_tbl$start[i])
# 計算實際事件結束時間允許範圍
e1 <- label_tbl$end[i] - allow_err_left
e2 <- label_tbl$end[i] + allow_err_left
if(i == nrow(label_tbl)){
e2 <- label_tbl$end[i]
}
# 確認模型產生的事件結束時間符合誤差範圍
if(pred_tbl$end[idx] %in% e1:e2){
pred_tbl$result[idx] <- 1
}
}
event_acc <- sum(pred_tbl$result == 1)/(nrow(label_tbl) + sum(pred_tbl$result == 0))
cat("事件辨識準確度 = ", round(event_acc, digits = 3))## 事件辨識準確度 = 0.301
Part 5: 結果與討論
分類模型對各時間點之分類正確度 > 88%, 但對於此類對稱性事件, 在既有訊號種類, 尚無法達到良好的事件辨識率. 在可接受的誤差範圍內, 測試組中63%的事件被正確找出, 而假陽性為50%.
此外, 從模型分析來看, 再增加資料對模型改善有限, 需要做的是找出紅球與綠球的具差異性的訊號, 例如目前尚未加入的顏色辨識.
使用特徵工程 + lightGBM的機器學習模型, 可以在更短的建模時間下, 達到更高的事件辨識準確度 (36%).
對於對稱性較低, 但同樣無法有效撰寫演算法辨識的事件 (如下圖), 則可達到90%以上的事件辨識準確度.
低對稱性複雜訊號事件辨識
email: chtsai0108@gmail.com