1. 程式人生 > >基於智慧手機感測器資料的人類行為識別

基於智慧手機感測器資料的人類行為識別

人類行為識別的目的是通過一系列的觀察,對人類的動作型別、行為模式進行分析和識別,並使用自然語言等方式對其進行描述的計算機技術。由於人類行為的複雜性和多樣性,往往識別出的結果是多樣性的,並且連帶著行為型別的概率輸出的。隨著資訊科技的發展,各種移動裝置和可穿戴裝置正在以加速度的方式增長,其效能和嵌入的感測器也變的多樣化,例如:高清相機、光感測器、陀螺儀感測器、加速度感測器、GPS以及溫度感測器等。各種各樣的感測器都在時刻的記錄著使用者的資訊,這些記錄資訊不僅可以用於使用者位置的預測,也可以進行使用者行為的識別等。

本文使用了智慧裝置加速度感測器的資料,結合支援向量機的特性進行人類行為識別模型的設計和應用。


如上圖所示,訊號資料的採集來自於嵌入在智慧手機中的加速度感測器,實驗選用了人類日常行為中的六類常見行為,分別為:走路、慢跑、上樓梯、下樓梯、坐、站立,資料收集後,對資料進行特徵抽取,抽取後的特徵使用支援向量機的分類功能對特徵進行分類,最後識別出人類的六類行為。


關於支援向量機(SVM)


支援向量機(Support Vector Machine)是Cortes和Vapnik於1995年首先提出的,它在解決小樣本、非線性及高維模式識別中表現出許多特有的優勢,並能夠推廣應用到函式擬合等其他機器學習問題中。SVM演算法是基於間隔最大化的一種監督學習演算法,包含線性和非線性兩種模型,對於線性不可分問題,通常會加入核函式進行處理。


支援向量機本質上是一個二類分類方法,它的基本模型是定義在特徵空間上的間隔最大化的線性分類器,間隔最大化使它有別於感知機。對於線性可分的訓練集,感知機的分離超平面是不唯一的,會有無窮個,而支援向量機會對分離超平面增加約束條件,使得分類超平面唯一。


假設我們有一組分屬於兩類的二維點,分別用星和圓表示,這些點可以通過直線分割,我們需要找到一條最優的分割線:



  • 找到正確的超平面(場景1) :這裡,我們有三個超平面(A、B、C),我們需要找到正確的超平面來分割星和圓:



我們的目的是 選擇更好地分割兩個類的超平面 ,因此上圖中可以看到超平面 B 已經能夠完成分割的工作。



  • 找到正確的超平面(場景2) :同樣有三個超平面(A、B、C),我們需要找到正確的超平面來分割星和圓:



上圖中,針對任意一個類,最大化最近的資料點和超平面之間的距離將有助於我們選擇正確的超平面,這個距離稱為 邊距 ,如下圖:


可以看到,超平面 C 距離兩個類別的邊緣比A和B都要高,因此我們將超平面 C 定為最優的超平面。選擇邊距最高的超平面的另一個重要的原因是 魯棒性 ,假設我們選擇最低邊距的超平面,那麼分類結果的錯誤率將會極大的升高。



  • 找到正確的超平面(場景3) :同樣有三個超平面(A、B、C),我們使用場景2的規則尋找正確的超平面來分割星和圓:



可能看到上圖,第一印象最優的超平面是 B ,因為它比超平面A有更高的邊距。但是,這裡是一種意外情況, 支援向量機會選擇在將邊距最大化之前對類進行精確分類的超平面。 這裡,超平面B具有分類誤差,超平面A已經正確的分類,因此此情況下,最優超平面則是 A



  • 能夠分類兩個類別(場景4) :針對利群點情況,尋找最優超平面:



上圖中,一個星出現在了圓所在的區域內,此星可稱為利群點。但是支援向量機具有忽略異常點並找到具有最大邊距的超平面的特徵,因此,可以說,支援向量機是魯棒性的。最終最優超平面如下圖所示:



  • 找到超平面並分類(場景5) :以上的場景均是線性超平面。在下面的場景中,我們無法直接在兩個類之間找到線性超平面,那麼支援向量機如何分類這兩個類呢?



支援向量機可以輕鬆的解決,它引入了一些附加的特性來解決此類問題。這裡,我們新增一個新的特徵。重新繪製座標軸上的資料點如下:


在支援向量機中,已經很容易在這兩個類直接找到線性超平面了,但是,出現的另一個重要的問題是,我們是否要手動處理這樣的問題呢?當然不需要,在支援向量機中,有一個 核函式 的技術,它會將低維空間的輸入轉換為高維空間,形成對映。由此會將某些不可分的問題轉換為可分問題,主要用於一些非線性分類問題中。


當我們檢視場景5中的超平面是,可能會如下圖所示:


以上僅僅是關於支援向量機的一點介紹,支援向量機有這複雜的演算法以及完備的證明,這裡不再累述,可參考 Support_vector_machine 檢視學習。


對於支援向量機來說,比較有名的類庫當屬臺灣大學林智仁(LinChih-Jen)教授所構建的 LIBSVM 類庫,由於LIBSVM程式小,運用靈活,輸入引數少,並且是開源的,易於擴充套件,因此成為目前應用最多的支援向量機的庫。 另外還提供了多種語言的介面,便於在不同的平臺下使用,本文中使用的也是這個類庫。 關於Mac下此類庫的編譯安裝,請參考文件 Install libsvm on Mac OSX ,本文會在Mac下進行訓練資料預處理、模型訓練、引數調優等,最終得到模型會使用在iOS專案中,當然該模型也可以使用在Android以及其他任何可以使用的地方。


針對支援向量機以及LIBSVM詳細的介紹,可檢視官方給出的文件: PDF


感測器資料集


本文使用了 WISDM (Wireless Sensor Data Mining) Lab 實驗室公開的 Actitracker 的資料集。 WISDM 公開了兩個資料集,一個是在實驗室環境採集的;另一個是在真實使用場景中採集的,這裡使用的是實驗室環境採集的資料。



  • 測試記錄:1,098,207 條

  • 測試人數:36 人

  • 取樣頻率:20 Hz

  • 行為型別:6 種

    • 走路

    • 慢跑

    • 上樓梯

    • 下樓梯


    • 站立


  • 感測器型別:加速度

  • 測試場景:手機放在衣兜裡面


資料分析


實驗室採集資料下載地址 下載資料集壓縮包,解壓後可以看到下面這些檔案:



  • readme.txt

  • WISDM_ar_v1.1_raw_about.txt

  • WISDM_ar_v1.1_trans_about.txt

  • WISDM_ar_v1.1_raw.txt

  • WISDM_ar_v1.1_transformed.arff


我們需要的是包含 RAW 資料的 WISDM_ar_v1.1_raw.txt 檔案,其他的是轉換後的或者說明檔案。先看看這些資料的分佈情況:


import matplotlib.pyplot as plt 
import numpy as np
import pandas as pd
from sklearn.metrics import precision_score, recall_score, f1_score, confusion_matrix, roc_curve, auc

if name == "main":
column_names = ['user-id', 'activity', 'timestamp', 'x-axis', 'y-axis', 'z-axis']
df = pd.read_csv("WISDM_ar_v1.1_raw.txt", header=None, names=column_names)
n = 10
print df.head(n)
subject = pd.DataFrame(df["user-id"].value_counts(), columns=["Count"])
subject.index.names = ['Subject']
print subject.head(n)
activities = pd.DataFrame(df["activity"].value_counts(), columns=["Count"])
activities.index.names = ['Activity']
print activities.head(n)
activity_of_subjects = pd.DataFrame(df.groupby("user-id")["activity"].value_counts())
print activity_of_subjects.unstack().head(n)
activity_of_subjects.unstack().plot(kind='bar', stacked=True, colormap='Blues', title="Distribution")
plt.show()


WISDM_ar_v1.1_raw.txt檔案不是合法的 CSV 檔案,每行後面有個 ; 號,如果使用 Pandasread_csv 方法直接載入會出錯,需要先將這些分號全部刪除。


檢視資料集各個行為的佔比情況,繪製餅圖如下:


可以看到此資料集是一個不平衡的資料集,但是這裡暫時忽略其不平衡性。


資料預處理


在LIBSVM的官方文件中可以看到,LIBSVM所使用的資料集是有嚴格的格式規範:


<label> <index1>:<value1> <index2>:<value2> ...

<label> :對於分類問題代表樣本的類別,使用整數表示,支援多個類別;對於迴歸問題代表目標變數,可以是任意實數。


<index1>:<value1> :表示特徵項。其中 <index> 代表特徵項的編號,使用從 1 開始的整數表示,可以不連續; <value> 代表該特徵項對應的特徵值,使用實數表示。在實際的操作中,如果樣本缺少某個特徵項,可以直接省略,LIBSVM 會自動把該項的特徵值賦為 0


標籤和每項特徵之間使用 空格 分割,每行資料使用 \n 分割。


只有符合這樣格式的資料,才能夠被LIBSVM使用,否則會直接報錯。這對準備好的資料,此類庫還提供了一個 tools/checkdata.py 核查工具,以便核查資料集是否符合要求。針對特徵的提取,為了簡單,這裡僅提取五類特徵:



  • 平均值

  • 最大值

  • 最小值

  • 方差

  • 組合三軸的加速度值 math.sqrt(math.pow(acc_x, 2)+math.pow(acc_y, 2)+math.pow(acc_z, 2))


瞭解了所需要的資料格式後,開始進行資料的預處理,並轉換為所需要的格式檔案。接下來分別將訓練和測試資料集進行特徵抽取並按照LIBSVM的資料格式重組,程式碼如下:


import ast 
import math
import numpy as np

FEATURE = ("mean", "max", "min", "std")
STATUS = ("Sitting", "Walking", "Upstairs", "Downstairs", "Jogging", "Standing")

def preprocess(file_dir, Seg_granularity):
gravity_data = []
with open(file_dir) as f:
index = 0
for line in f:
clear_line = line.strip().lstrip().rstrip(';')
raw_list = clear_line.split(',')
index = index + 1
if len(raw_list) < 5:
continue
status = raw_list[1]
acc_x = float(raw_list[3])
acc_y = float(raw_list[4])
print index
acc_z = float(raw_list[5])

        if acc_x == 0 or acc_y == 0 or acc_z == 0:
            continue

        gravity = math.sqrt(math.pow(acc_x, 2)+math.pow(acc_y, 2)+math.pow(acc_z, 2))
        gravity_tuple = {&quot;gravity&quot;: gravity, &quot;status&quot;: status}
        gravity_data.append(gravity_tuple)

# split data sample of gravity
splited_data = []
cur_cluster  = []
counter      = 0
last_status  = gravity_data[0][&quot;status&quot;]
for gravity_tuple in gravity_data:
    if not (counter &lt; Seg_granularity and gravity_tuple[&quot;status&quot;] == last_status):
        seg_data = {&quot;status&quot;: last_status, &quot;values&quot;: cur_cluster}
        # print seg_data
        splited_data.append(seg_data)
        cur_cluster = []
        counter = 0
    cur_cluster.append(gravity_tuple[&quot;gravity&quot;])
    last_status = gravity_tuple[&quot;status&quot;]
    counter += 1
# compute statistics of gravity data
statistics_data = []
for seg_data in splited_data:
    np_values = np.array(seg_data.pop(&quot;values&quot;))
    seg_data[&quot;max&quot;]  = np.amax(np_values)
    seg_data[&quot;min&quot;]  = np.amin(np_values)
    seg_data[&quot;std&quot;]  = np.std(np_values)
    seg_data[&quot;mean&quot;] = np.mean(np_values)
    statistics_data.append(seg_data)
# write statistics result into a file in format of LibSVM
with open(&quot;WISDM_ar_v1.1_raw_svm.txt&quot;, &quot;a&quot;) as the_file:
    for seg_data in statistics_data:
        row = str(STATUS.index(seg_data[&quot;status&quot;])) + &quot; &quot; + \
              str(FEATURE.index(&quot;mean&quot;)) + &quot;:&quot; + str(seg_data[&quot;mean&quot;]) + &quot; &quot; + \
              str(FEATURE.index(&quot;max&quot;)) + &quot;:&quot; + str(seg_data[&quot;max&quot;]) + &quot; &quot; + \
              str(FEATURE.index(&quot;min&quot;)) + &quot;:&quot; + str(seg_data[&quot;min&quot;]) + &quot; &quot; + \
              str(FEATURE.index(&quot;std&quot;)) + &quot;:&quot; + str(seg_data[&quot;std&quot;]) + &quot;\n&quot;
        # print row
        the_file.write(row)        

if name == "main":
preprocess("WISDM_ar_v1.1_raw.txt", 100)
pass


成功轉換後的資料格式形如:


. 
.
.
5 0:9.73098373254 1:10.2899465499 2:9.30995703535 3:0.129482033438
5 0:9.74517171235 1:10.449291842 2:9.15706284788 3:0.161143714697
5 0:9.71565678822 1:10.4324206204 2:9.41070666847 3:0.136704694206
5 0:9.70622803003 1:9.7882020821 2:9.60614907234 3:0.0322246639852
5 0:9.74443440742 1:10.2915256401 2:9.28356073929 3:0.165543789197
0 0:9.28177794859 1:9.47500395778 2:8.92218583084 3:0.0700079500015
0 0:9.27218416165 1:9.40427562335 2:9.14709243421 3:0.0433805537826
0 0:9.27867211792 1:9.39755287296 2:9.1369415014 3:0.037533026091
0 0:9.27434585368 1:9.33462907672 2:9.21453200114 3:0.0263815511773
.
.
.

由於該資料集並未區分訓練和測試資料集,因此為了最終的模型驗證,首先需要分割該資料集為兩份,分別進行訓練和模型驗證,分割方法就使用最簡單的2\8原則,使用LIBSVM提供的工具 tools/subset.py 進行資料分割:


工具使用介紹:


Usage: subset.py [options] dataset subset_size [output1] [output2]

This script randomly selects a subset of the dataset.

options:
-s method : method of selection (default 0)
0 -- stratified selection (classification only)
1 -- random selection

output1 : the subset (optional)
output2 : rest of the data (optional)
If output1 is omitted, the subset will be printed on the screen.


使用工具進行資料分割:


python subset.py -s 0 WISDM_ar_v1.1_raw_svm.txt 2190 raw_test.txt raw_train.txt

**


!! 注意 !!


上面程式碼段中的 2190 就是subset.py工具子資料集的大小,該大小並不是檔案的大小,而是根據原始檔案中的行數進行2\8分後的行數。subset.py會隨機抽取所設定行數的資料到指定的檔案中。


**


完成後,我們就得到了訓練資料集 raw_train.txt 和測試資料集 raw_test.txt


到此,所需要使用的資料集已經完全轉換為LIBSVM所需要的格式,如果不放心資料格式,可以使用 tools/checkdata.py 工具進行檢查。


模型建立與訓練


在關於支援向量機部分,如果已經在Mac上安裝好了libsvm,那麼在你的命令列工具中輸入 svm-train ,即可看到此命令的使用方式和引數說明,假設我們使用預設的引數進行模型訓練:


svm-train -b 1 raw_train.txt raw_trained.model

其中 -b 的含義是probability_estimates,是否訓練一個SVC或者SVR模型用於概率統計,設定為 1 ,以便最終的模型評估使用。


訓練過程的可能會消耗一點時間,主要在於所使用的訓練資料集的大小,訓練時的日誌輸出形如:


. 
.
.
optimization finished, #iter = 403
nu = 0.718897
obj = -478.778647, rho = -0.238736
nSV = 508, nBSV = 493
Total nSV = 508
*
optimization finished, #iter = 454
nu = 0.734417
obj = -491.057723, rho = -0.318206
nSV = 518, nBSV = 507
Total nSV = 518
*
optimization finished, #iter = 469
nu = 0.722888
obj = -604.608449, rho = -0.360926
nSV = 636, nBSV = 622
Total nSV = 4136
.
.
.

其中: #iter 是迭代次數, nu 是選擇的核函式型別的引數, obj 為 SVM 檔案轉換為的二次規劃求解得到的最小值, rho 為判決函式的偏置項 b, nSV 是標準支援向量個數(0 < a[i] < c), nBSV 是邊界上的支援向量個數(a[i] = c), Total nSV 是支援向量總個數。


這樣我們就得到了模型檔案 raw_trained.model ,首先使用你所熟悉的文字編譯工具開啟此檔案,讓我們檢視一下此檔案中的內容:


svm_type c_svc      //所選擇的 svm 型別,預設為 c_svc 
kernel_type rbf //訓練採用的核函式型別,此處為 RBF 核
gamma 0.333333 //RBF 核的 gamma 係數
nr_class 6 //類別數,此處為六元分類問題
total_sv 4136 //支援向量總個數
rho -0.369589 -0.28443 -0.352834 -0.852275 -0.831555 0.267266 0.158289 -0.777357 -0.725441 -0.271317
-0.856933 -0.798849 -0.807448 -0.746674 -0.360926 //判決函式的偏置項 b
label 4 1 2 3 0 5 //類別標識
probA -3.11379 -3.0647 -3.2177 -5.78365 -5.55416 -2.30133 -2.26373 -6.05582 -5.99505 -1.07317 -4.50318
-4.51436 -4.48257 -4.71033 -1.18804
probB 0.099704 -0.00543388 -0.240146 -0.43331 -1.01639 0.230949 0.342831 -0.249265 -0.817104 -0.0249471
-0.209852 -0.691243 -0.0803133 -0.940074 0.272984
nr_sv 558 1224 880 825 325 324 //每個類的支援向量機的個數
SV
//以下為各個類的權係數及相應的支援向量
1 0 0 0 0 0:14.384883 1:24.418964 2:2.5636304 3:5.7143112
1 1 1 0 0 0:11.867873 1:23.548919 2:4.5479318 3:4.5074937
1 0 0 0 0 0:14.647238 1:24.192184 2:4.0759445 3:5.367968
1 0 0 0 0 0:14.374831 1:24.286867 2:2.0045062 3:5.5710882
1 0 0 0 0 0:14.099495 1:24.03442 2:2.42664 3:5.7580063
1 0 0 0 0 0:14.313538 1:25.393975 2:1.9496137 3:5.6174387
...

得到模型檔案之後,首先要進行的就是模型的測試驗證,還記得開始進行資料準備的時候,我們分割了訓練和測試資料集嗎?訓練資料集進行了模型的訓練,接下來就是測試資料集發揮作用的時候了。


驗證模型,LIBSVM提供了另一個命令方法 svm-predict ,使用介紹如下:


Usage: svm-predict [options] test_file model_file output_file 
options:
-b probability_estimates: whether to predict probability estimates, 0 or 1 (default 0);
for one-class SVM only 0 is supported
-q : quiet mode (no outputs)

使用測試資料集進行模型驗證:


svm-predict -b 1 raw_test.txt raw_trained.model predict.out

執行此命令後,LIBSVM會進行識別預測,由於我們使用了 -b 1 引數,因此最終會輸出各個類別的識別概率到predict.out檔案中,並且會輸出一個總體的正確率:


Accuracy = 78.4932% (1719/2190) (classification)

可以看到此時我們訓練的模型的識別正確率為78.4932%。


predict.out檔案內容形如:


labels 4 1 2 3 0 5 
4 0.996517 0.000246958 0.00128824 0.00123075 0.000414204 0.000303014
4 0.993033 0.000643327 0.00456298 0.00103339 0.000427387 0.000299934
1 0.0117052 0.773946 0.128394 0.0848292 0.00065714 0.0004682
1 0.0135437 0.484226 0.343907 0.156548 0.00105013 0.0007251
1 0.0117977 0.885448 0.0256842 0.0761578 0.000513167 0.000399136
3 0.00581106 0.380545 0.120613 0.490377 0.00179286 0.000861917
1 0.0117571 0.91544 0.0145561 0.0573158 0.000524352 0.000406782
1 0.0122297 0.811546 0.0824789 0.0924932 0.000704449 0.000547972
...

其中,第一行為表頭,第一列是識別出的類別標籤,後面依次跟著各個標籤的識別概率。


那麼問題來了,難道模型的識別正確率就只能到這個程度了嗎?我們再次回顧 svm-train 命令,其中有很多的引數我們都使用了預設的設定,並沒有進行特定的設定。通過檢視LIBSVM官方的文件,發現竟然提供了 引數尋優 的工具 tools/grid.py ,通過此工具可以自動尋找訓練資料集中的最優引數C係數和gamma係數,以在訓練的時候使用。具體用法如下:


Usage: grid.py [grid_options] [svm_options] dataset

grid_options :
-log2c {begin,end,step | "null"} : set the range of c (default -5,15,2)
begin,end,step -- c_range = 2^{begin,...,begin+k*step,...,end}
"null" -- do not grid with c
-log2g {begin,end,step | "null"} : set the range of g (default 3,-15,-2)
begin,end,step -- g_range = 2^{begin,...,begin+k*step,...,end}
"null" -- do not grid with g
-v n : n-fold cross validation (default 5)
-svmtrain pathname : set svm executable path and name
-gnuplot {pathname | "null"} :
pathname -- set gnuplot executable path and name
"null" -- do not plot
-out {pathname | "null"} : (default dataset.out)
pathname -- set output file path and name
"null" -- do not output file
-png pathname : set graphic output file path and name (default dataset.png)
-resume [pathname] : resume the grid task using an existing output file (default pathname is dataset.out)
This is experimental. Try this option only if some parameters have been checked for the SAME data.

svm_options : additional options for svm-train


又是一堆的引數,但是不必擔心,對於初學者來說,這裡的大部分引數都可以不用設定,直接使用預設值即可,如果你需要檢視引數尋優的過程,還需要安裝 gnuplot 並按照 官方說明 配置。


python /tools/grid.py -b 1 raw_train.txt

執行此命令後,會不斷的輸出不同的C係數和gamma係數取值情況下的分類準確率,並在最後一行輸出最優的引數選擇:


...  
[local] 13 -15 73.1217 (best c=8192.0, g=0.03125, rate=79.3446)
[local] 13 3 72.8477 (best c=8192.0, g=0.03125, rate=79.3446)
[local] 13 -9 77.8488 (best c=8192.0, g=0.03125, rate=79.3446)
[local] 13 -3 78.3741 (best c=8192.0, g=0.03125, rate=79.3446)
8192.0 0.03125 79.3446

並且會在當前目錄下生成輸出檔案raw_train.txt.out和對應的圖形檔案raw_train.txt.png:


經過最優引數的尋找,最終給出了C係數為8192.0,gamma係數為0.03125的情況下,模型分類的準確率最高,為79.3446。


接下來我們再次使用 svm-train 方法,並設定當前最優C係數值和gamma係數值,重新訓練我們的模型:


svm-train -b 1 -c  -g   raw_train.txt raw_bestP_trained.model

訓練完成後,得到新的模型檔案raw_bestP_trained.model,再次使用測試資料集進行驗證:


svm-predict -b 1 raw_test.txt raw_bestP_trained.model bestP_predict.out

最終輸出結果如下:


Accuracy = 79.1324% (1733/2190) (classification)

可以看到模型的預測正確率明顯提升了不少。上面的引數尋優僅僅是使用了預設的引數進行尋找,你也可以繼續嘗試設定各個引數進行引數尋優,以進一步提升模型識別正確率,這裡不在進行進一步的引數尋優。


小結


可以看到SVM進行使用者行為識別,可以得到較好的效果,本文中使用的資料是實驗室資料,並且特徵也僅僅提取了基本的幾個,準確率即可達到79%以上,此方案可以繼續進行優化,使用真實世界採集的資料,進行更加詳細的特徵準備,提高訓練時的迭代次數等,進行模型重新訓練優化,最終達到更好的分類效果。


下面,我們將在iOS平臺下構建應用,並使用LIBSVM和本文中訓練所得到的模型,進行準實時人類行為識別。


前面,我們簡單介紹了支援向量機以及如何使用LIBSVM類庫和加速度感測器資料進行特徵的抽取、模型的訓練、引數的調優和模型的測試等,在本文中,將使用上篇最終得到的模型檔案,以及LIBSVM類庫,在iOS平臺下構建一個能夠識別當前客戶端使用者的行為型別的應用。


隨著移動終端裝置的效能越來越高,其整合的感測器裝置也越來越多,偵測精度越來越高的情況,應用於移動終端裝置上的機器學習應用也多了起來。在iOS平臺下,蘋果官方的很多應用中也呈現出了機器學習的影子。例如iOS 10 系統中的相簿,能夠進行人臉識別並進行照片的自動分類、郵件中的自動垃圾郵件歸類、Siri智慧助理、健康應用中的使用者運動型別分類等。


使用者的運動型別


在iOS系統的健康應用中,可以看到你的執行型別,其中包含了行走、跑步、爬樓梯、步數、騎自行車等型別。



stationary  
walking
running
automotive
cycling
unknown

幾種行為型別,但是在使用的過程中,可能會遇到當前行為和此類給出的結果不相同或者同一時刻有東中型別的情況,這裡引用蘋果給出的一段結論:


An estimate of the user’s activity based on the motion of the device.


The activity is exposed as a set of properties, the properties are not


mutually exclusive.


For example, if you’re in a car stopped at a stop sign the state might


look like:


stationary = YES, walking = NO, running = NO, automotive = YES


Or a moving vehicle,


stationary = NO, walking = NO, running = NO, automotive = YES


Or the device could be in motion but not walking or in a vehicle.


stationary = NO, walking = NO, running = NO, automotive = NO.


Note in this case all of the properties are NO.


因此使用者行為的識別並不是嚴格意義上的準確的,在機器學習領域,預測都會有一個概率的輸出,引申出的就是正確率, 正確率 也是評估一個機器學習模型的標準之一。


關於加速度感測器


蘋果的移動裝置中,集成了 多種感測器 ,本文所演示的僅僅使用加速度感測器,你也可以增加感測器型別,提高資料的維度等。


加速度感測器資料 CMAccelerometerData 的型別為CMAcceleration,提供了三軸加速度值,如下:


typedef struct { 
double x;
double y;
double z;
} CMAcceleration;
// A structure containing 3-axis acceleration data.

此加速度值是當前裝置總的加速度值,想要獲取加速度分量的時候,可以使用 CMDeviceMotion 進行獲取。


構建iOS專案,收集感測器資料


在上篇中,我們已經知道,LIBSVM具有多種語言的介面,這裡我們直接使用其C語言介面,在iOS專案中構建SVM分類器。


1. 感測器資料收集


首先需要收集加速度感測器資料,並進行資料特徵抽取和資料準備,以便SVM演算法識別使用。在iOS的 CoreMotion 框架中,已經提供了獲取加速度感測器的API,開發者可以直接呼叫介面獲取加速度感測器資料:


CMMotionManager *motionManager = [[CMMotionManager alloc] init]; 
if ([motionManager isAccelerometerAvailable]) {
[motionManager setAccelerometerUpdateInterval:0.02];
    startTime = [[NSDate date] timeIntervalSince1970];
    [motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
        if (error) {
            NSLog(@&quot;%@&quot;, error.description);
        }else{
            [self handleDeviceAcc:accelerometerData];
        }
    }];
}</pre> 

2. 資料批量化處理


我們在本文開始介紹訓練資料集的時候,提到了資料的採集頻率是 20 Hz ,因此我們在進行資料採集的時候也需要同樣的頻率,並且將感測器資料進行批量化處理,以便於模型識別時具有合適數量的資料。


NSArray *valueArr = @[ 
@(accelerometerData.acceleration.x * g_value),
@(accelerometerData.acceleration.y * g_value),
@(accelerometerData.acceleration.z * -g_value)];
NSMutableDictionary *sample = [NSMutableDictionary dictionary];
[sample setValue:currenStatus forKey:@&quot;status&quot;];
[sample setValue:@&quot;acc&quot; forKey:@&quot;sensorName&quot;];
[sample setValue:@([self getTimeStampByMiliSeconds]) forKey:@&quot;timestamp&quot;];
[sample setValue:valueArr forKey:@&quot;values&quot;];

if (sampleDatas == nil) {
    sampleDatas = [NSMutableArray array];
}

if ([sampleDatas count] == 256) {
    NSArray *readySamples = [NSArray arrayWithArray:sampleDatas];
    sampleDatas = nil;
    [self stopMotionAccelerometer];

    [self recognitionData:[readySamples copy]];
}else{
    [sampleDatas addObject:sample];
}</pre> 

3. 特徵抽取


在開始此步驟之前,我們需要匯入LIBSVM的類庫到專案工程中,這裡僅需要匯入 svm.hsvm.cpp 兩個檔案即可。


在訓練模型的時候,我們使用了五種特徵,最終生成所需要的資料格式,這裡同樣,我們也需要針對資料進行特徵提取,並重新組合資料成為LIBSVM所要求的資料格式:


for (NSUInteger index = 0; index < [raw_datas count]; index++) { 
NSDictionary *jsonObject = raw_datas[index];
NSArray *valuesArray = jsonObject[@"values"];
if (!valuesArray || valuesArray.count <= 0) {
break;
}
id acc_x_num = valuesArray[0];
id acc_y_num = valuesArray[1];
id acc_z_num = valuesArray[2];
    acc_x_axis[index] = acc_x_num;
    acc_y_axis[index] = acc_y_num;
    acc_z_axis[index] = acc_z_num;

    gravity[index] = @(sqrt(pow([acc_x_num doubleValue], 2) + pow([acc_y_num doubleValue], 2) + pow([acc_z_num doubleValue], 2)));

}

NSMutableArray *values = [NSMutableArray array];
/* mean Feature */{
    struct svm_node node_x_mean = {0, [StatisticFeature mean:acc_x_axis]};
    NSValue *node_x_mean_value = [NSValue valueWithBytes:&amp;node_x_mean objCType:@encode(struct svm_node)];
    [values addObject:node_x_mean_value];

    struct svm_node node_y_mean = {1, [StatisticFeature mean:acc_y_axis]};
    NSValue *node_y_mean_value = [NSValue valueWithBytes:&amp;node_y_mean objCType:@encode(struct svm_node)];
    [values addObject:node_y_mean_value];

    struct svm_node node_z_mean = {2, [StatisticFeature mean:acc_z_axis]};
    NSValue *node_z_mean_value = [NSValue valueWithBytes:&amp;node_z_mean objCType:@encode(struct svm_node)];
    [values addObject:node_z_mean_value];

    struct svm_node node0 = {3, [StatisticFeature mean:gravity]};
    NSValue *value0 = [NSValue valueWithBytes:&amp;node0 objCType:@encode(struct svm_node)];
    [values addObject:value0];
}
/* max Feature */{
    struct svm_node node_x_max = {4, [StatisticFeature max:acc_x_axis]};
    NSValue *node_x_max_value = [NSValue valueWithBytes:&amp;node_x_max objCType:@encode(struct svm_node)];
    [values addObject:node_x_max_value];

    struct svm_node node_y_max = {5, [StatisticFeature max:acc_y_axis]};
    NSValue *node_y_max_value = [NSValue valueWithBytes:&amp;node_y_max objCType:@encode(struct svm_node)];
    [values addObject:node_y_max_value];

    struct svm_node node_z_max = {6, [StatisticFeature max:acc_z_axis]};
    NSValue *node_z_max_value = [NSValue valueWithBytes:&amp;node_z_max objCType:@encode(struct svm_node)];
    [values addObject:node_z_max_value];

    struct svm_node node1 = {7, [StatisticFeature max:gravity]};
    NSValue *value1 = [NSValue valueWithBytes:&amp;node1 objCType:@encode(struct svm_node)];
    [values addObject:value1];
}
/* min Feature */{
    struct svm_node node_x_min = {8, [StatisticFeature min:acc_x_axis]};
    NSValue *node_x_min_value = [NSValue valueWithBytes:&amp;node_x_min objCType:@encode(struct svm_node)];
    [values addObject:node_x_min_value];

    struct svm_node node_y_min = {9, [StatisticFeature min:acc_y_axis]};
    NSValue *node_y_min_value = [NSValue valueWithBytes:&amp;node_y_min objCType:@encode(struct svm_node)];
    [values addObject:node_y_min_value];

    struct svm_node node_z_min = {10, [StatisticFeature min:acc_z_axis]};
    NSValue *node_z_min_value = [NSValue valueWithBytes:&amp;node_z_min objCType:@encode(struct svm_node)];
    [values addObject:node_z_min_value];

    struct svm_node node2 = {11, [StatisticFeature min:gravity]};
    NSValue *value2 = [NSValue valueWithBytes:&amp;node2 objCType:@encode(struct svm_node)];
    [values addObject:value2];
}
/* stev Feature */{
    struct svm_node node_x_stev = {12, [StatisticFeature stev:acc_x_axis]};
    NSValue *node_x_stev_value = [NSValue valueWithBytes:&amp;node_x_stev objCType:@encode(struct svm_node)];
    [values addObject:node_x_stev_value];

    struct svm_node node_y_stev = {13, [StatisticFeature stev:acc_y_axis]};
    NSValue *node_y_stev_value = [NSValue valueWithBytes:&amp;node_y_stev objCType:@encode(struct svm_node)];
    [values addObject:node_y_stev_value];

    struct svm_node node_z_stev = {14, [StatisticFeature stev:acc_z_axis]};
    NSValue *node_z_stev_value = [NSValue valueWithBytes:&amp;node_z_stev objCType:@encode(struct svm_node)];
    [values addObject:node_z_stev_value];

    struct svm_node node3 = {15, [StatisticFeature stev:gravity]};
    NSValue *value3 = [NSValue valueWithBytes:&amp;node3 objCType:@encode(struct svm_node)];
    [values addObject:value3];
}</pre> 

這裡需要注意的是,特徵的順序必須和模型訓練時訓練資料集中的特徵順序一致,否則預測的結果將出現嚴重的偏差。


4. 匯入模型檔案並載入


完成了資料準備之後,我們匯入之前訓練好的模型檔案 raw_bestP_trained.model 到專案中,然後使用LIBSVM提供的模型載入方法,載入模型到 svm_model 結構體物件:


struct svm_model * model = svm_load_model([model_dir UTF8String]);

if (model == NULL) {
    NSLog(@&quot;Can't open model file: %@&quot;,model_dir);
    return nil;
}
if (svm_check_probability_model(model) == 0) {
    NSLog(@&quot;Model does not support probabiliy estimates&quot;);
    return nil;
}</pre> 

4. 行為識別


LIBSVM提供了多個方法進行預測,為了最終看到預測的概率,我們使用


double svm_predict_probability(const struct svm_model *model,  
const struct svm_node *x,
double* prob_estimates);

方法,在輸出預測結果的時候,會帶有對應的概率:


//Type of svm model 
int svm_type = svm_get_svm_type(model);
//Count of labels
int nr_class = svm_get_nr_class(model);
//Label of svm model
int labels = (int ) malloc(nr_class*sizeof(int));
svm_get_labels(model, labels);
// Probability of each possible label in result
double *prob_estimates = (double *) malloc(nr_class*sizeof(double));
// Predicting
// result of prediction including:
// - Most possible label
// - Probability of each possible label
double label = 0.0;
if (svm_type == C_SVC || svm_type == NU_SVC) {
      label = svm_predict_probability(model, X, prob_estimates); 
      NSLog(@&quot;svm_predict_probability label: %f&quot;,label);
}else{
    NSLog(@&quot;svm_type is not support !!!&quot;);
    return nil;
}</pre> 

通過以上的預測之後,最終的預測結果就是 label ,並在會在 prob_estimates 中輸出各個分類標籤的預測概率。


!!注意 !!


* prob_estimates 中僅僅會輸出概率,並不會輸出概率和標籤的對應關係。prob_estimates中的概率順序是和模型中的輸入標籤順序一致的,需要注意! *


最終的預測結果如下:


label: 4 -- prob: 0.491513  
label: 1 -- prob: 0.285421
label: 2 -- prob: 0.119973
label: 3 -- prob: 0.096848
label: 0 -- prob: 0.002580
label: 5 -- prob: 0.003665

關於模型的評估


分類模型的度量有很多方式,例如混淆矩陣(Confusion Matrix)、ROC曲線、AUC面積、Lift(提升)和Gain(增益)、K-S圖、基尼係數等,這裡我們使用ROC曲線評估我們最終得到的模型,以檢視模型的質量,最終的ROC曲線圖如下:


可以看到該模型針對某些行為的識別能力較好,例如站立、慢跑,但是對另一些行為的識別卻不怎麼好了,例如下樓梯。


總結


可以看到SVM在分類問題上能夠很好的識別特徵進行類別區分。由於篇幅原因,本文中並沒有對資料的特徵進行更加細緻的選擇和抽取,可能會導致一些行為型別的識別不能達到理想的效果,但是相信在足量的資料下,進行更加細緻的特徵工程後,利用SVM在分類能力上的優勢,能夠構建出更加優秀的人類行為型別識別的智慧應用。


感謝徐川對本文的審校。


給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至[email protected]。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微訊號: InfoQChina )關注我們。