Part 1: 摘要:事件辨識應用說明

  1. 如下圖所示, 假設事件是由1紅球與1綠球構成, 球由左向右移動, 經過事件感測器, 感測器將會產生訊號, 這些球大小相似, 但會有略微差異, 而球與球的間距則不一定, 因此感測器生成的時間序列就如圖所示, 而由於人知道1紅1綠通過就是一個事件, 引此我們可以用人的判別方式對訊號標示事件發生的起點與終點, 亦即綠球何時進入感測區域, 紅球何時離開感測區域.
事件辨識應用介紹

事件辨識應用介紹

  1. 目標是經過一段時間記錄後, 自動辨識該段時間所有事件發生起點與終點.

  2. 然而我們可以注意到, 由於感測器無法辨識出顏色, 因此綠球與紅球的訊號會很相似, 此外球的大小及球與球的間距皆非定值, 這些都造成了自動辨識的困難.

  3. 在無法有效以演算法的方式處理下, 我們嘗試用機器學習來解決問題, 這裡特別提出深度學習 (keras + tensorflow-gpu). 使用常見的sliding window (位移n時間單位) 處理原始序列 (長度L), 生成長度為T的子序列, 可產生 (L - T)/n + 1筆子序列.

深度學習使用簡述

深度學習使用簡述

  1. 分類模型對各時間點之分類正確度 > 88%, 但對於此類對稱性事件, 在既有訊號種類, 尚無法達到良好的事件辨識率. 在可接受的誤差範圍內, 測試組中63%的事件被正確找出.

  2. 此外, 從模型分析來看, 再增加資料對模型改善有限, 需要做的是找出紅球與綠球的具差異性的訊號, 例如目前尚未加入的顏色辨識.

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))
  1. 資料概述

    1. value: 感測器因事件出現回傳的訊號值

    2. label: 人眼判別事件發生與否 (1個事件會佔據一段連續的時間)

    3. 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
  1. 時間序列處理與樣本生成

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資料前處理與建立深度學習模型

  1. 將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())
  1. 建立深度學習模型, 只使用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
)
  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: 分類模型表現分析

  1. 觀察train-vliad accuracy history可以發現epoch到60後, valid未隨train的提升而提升.
模型表現隨epoch變化圖

模型表現隨epoch變化圖

  1. 分析訓練集資料量對模型表現的影響:

    1. 全部事件為10000筆, 固定測試集為特定的2000筆事件, 訓練集最多可使用8000筆事件, 觀察訓練集使用比例對準確度的影響.

    2. 結果顯示繼續增加資料量對於模型表現改善有限, 如要改良模型需要增加其他種類資料.

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
  1. 藉由前面的觀察, 使用所有訓練資料及epoch = 60建立最終用於測試組別的模型, 得到的分類準確度為0.887, 這代表每個時間點被正確分類的準確度.

  2. 然而, 我們更有興趣的是事件被成功定位的準確度, 我們定義如下:

    \[ 事件辨識準確度 = 成功辨識事件(TP)/(實際事件數 + 錯誤辨識事件(FP)) \]

  3. 定義分類結果連續的1為一個事件, 例如11100011代表內有2個事件, 而這兩個事件發生的起訖點分別為(1,3)及(7,8).

  4. 從模型分類結果產生的事件, 其發生時間的起訖點與實際事件的起迄點之誤差不可大於10%事件時間長度, 例如事件時間長度為100, 則允許誤差為10. 符合條件為TP, 不符合條件為FP.

  5. 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: 結果與討論

  1. 分類模型對各時間點之分類正確度 > 88%, 但對於此類對稱性事件, 在既有訊號種類, 尚無法達到良好的事件辨識率. 在可接受的誤差範圍內, 測試組中63%的事件被正確找出, 而假陽性為50%.

  2. 此外, 從模型分析來看, 再增加資料對模型改善有限, 需要做的是找出紅球與綠球的具差異性的訊號, 例如目前尚未加入的顏色辨識.

  3. 使用特徵工程 + lightGBM的機器學習模型, 可以在更短的建模時間下, 達到更高的事件辨識準確度 (36%).

  4. 對於對稱性較低, 但同樣無法有效撰寫演算法辨識的事件 (如下圖), 則可達到90%以上的事件辨識準確度.

低對稱性複雜訊號事件辨識

低對稱性複雜訊號事件辨識

email: chtsai0108@gmail.com