[关闭]
@pockry 2017-01-23T23:06:44.000000Z 字数 20287 阅读 4187

基于智能手机传感器数据的人类行为识别

移动 人工智能


人类行为识别的目的是通过一系列的观察,对人类的动作类型、行为模式进行分析和识别,并使用自然语言等方式对其进行描述的计算机技术。由于人类行为的复杂性和多样性,往往识别出的结果是多样性的,并且连带着行为类型的概率输出的。随着信息技术的发展,各种移动设备和可穿戴设备正在以加速度的方式增长,其性能和嵌入的传感器也变的多样化,例如:高清相机、光传感器、陀螺仪传感器、加速度传感器、GPS以及温度传感器等。各种各样的传感器都在时刻的记录着使用者的信息,这些记录信息不仅可以用于用户位置的预测,也可以进行用户行为的识别等。

本文使用了智能设备加速度传感器的数据,结合支持向量机的特性进行人类行为识别模型的设计和应用。

image

如上图所示,信号数据的采集来自于嵌入在智能手机中的加速度传感器,实验选用了人类日常行为中的六类常见行为,分别为:走路、慢跑、上楼梯、下楼梯、坐、站立,数据收集后,对数据进行特征抽取,抽取后的特征使用支持向量机的分类功能对特征进行分类,最后识别出人类的六类行为。

关于支持向量机(SVM)

支持向量机(Support Vector Machine)是Cortes和Vapnik于1995年首先提出的,它在解决小样本、非线性及高维模式识别中表现出许多特有的优势,并能够推广应用到函数拟合等其他机器学习问题中。SVM算法是基于间隔最大化的一种监督学习算法,包含线性和非线性两种模型,对于线性不可分问题,通常会加入核函数进行处理。

支持向量机本质上是一个二类分类方法,它的基本模型是定义在特征空间上的间隔最大化的线性分类器,间隔最大化使它有别于感知机。对于线性可分的训练集,感知机的分离超平面是不唯一的,会有无穷个,而支持向量机会对分离超平面增加约束条件,使得分类超平面唯一。

假设我们有一组分属于两类的二维点,分别用星和圆表示,这些点可以通过直线分割,我们需要找到一条最优的分割线:

image

我们的目的是选择更好地分割两个类的超平面,因此上图中可以看到超平面 B 已经能够完成分割的工作。

image

上图中,针对任意一个类,最大化最近的数据点和超平面之间的距离将有助于我们选择正确的超平面,这个距离称为边距,如下图:

image

可以看到,超平面C距离两个类别的边缘比A和B都要高,因此我们将超平面C定为最优的超平面。选择边距最高的超平面的另一个重要的原因是鲁棒性,假设我们选择最低边距的超平面,那么分类结果的错误率将会极大的升高。

image

可能看到上图,第一印象最优的超平面是B,因为它比超平面A有更高的边距。但是,这里是一种意外情况,支持向量机会选择在将边距最大化之前对类进行精确分类的超平面。这里,超平面B具有分类误差,超平面A已经正确的分类,因此此情况下,最优超平面则是A

image

上图中,一个星出现在了圆所在的区域内,此星可称为利群点。但是支持向量机具有忽略异常点并找到具有最大边距的超平面的特征,因此,可以说,支持向量机是鲁棒性的。最终最优超平面如下图所示:

image

image

支持向量机可以轻松的解决,它引入了一些附加的特性来解决此类问题。这里,我们添加一个新的特征。重新绘制坐标轴上的数据点如下:

image

在支持向量机中,已经很容易在这两个类直接找到线性超平面了,但是,出现的另一个重要的问题是,我们是否要手动处理这样的问题呢?当然不需要,在支持向量机中,有一个核函数的技术,它会将低维空间的输入转换为高维空间,形成映射。由此会将某些不可分的问题转换为可分问题,主要用于一些非线性分类问题中。

当我们查看场景5中的超平面是,可能会如下图所示:

image

以上仅仅是关于支持向量机的一点介绍,支持向量机有这复杂的算法以及完备的证明,这里不再累述,可参考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 公开了两个数据集,一个是在实验室环境采集的;另一个是在真实使用场景中采集的,这里使用的是实验室环境采集的数据。

数据分析

实验室采集数据下载地址下载数据集压缩包,解压后可以看到下面这些文件:

我们需要的是包含 RAW 数据的WISDM_ar_v1.1_raw.txt 文件,其他的是转换后的或者说明文件。先看看这些数据的分布情况:

  1. import matplotlib.pyplot as plt
  2. import numpy as np
  3. import pandas as pd
  4. from sklearn.metrics import precision_score, recall_score, f1_score, confusion_matrix, roc_curve, auc
  5. if __name__ == "__main__":
  6. column_names = ['user-id', 'activity', 'timestamp', 'x-axis', 'y-axis', 'z-axis']
  7. df = pd.read_csv("WISDM_ar_v1.1_raw.txt", header=None, names=column_names)
  8. n = 10
  9. print df.head(n)
  10. subject = pd.DataFrame(df["user-id"].value_counts(), columns=["Count"])
  11. subject.index.names = ['Subject']
  12. print subject.head(n)
  13. activities = pd.DataFrame(df["activity"].value_counts(), columns=["Count"])
  14. activities.index.names = ['Activity']
  15. print activities.head(n)
  16. activity_of_subjects = pd.DataFrame(df.groupby("user-id")["activity"].value_counts())
  17. print activity_of_subjects.unstack().head(n)
  18. activity_of_subjects.unstack().plot(kind='bar', stacked=True, colormap='Blues', title="Distribution")
  19. plt.show()

WISDM_ar_v1.1_raw.txt 文件不是合法的 CSV 文件,每行后面有个 ; 号,如果使用 Pandasread_csv方法直接加载会出错,需要先将这些分号全部删除。

image

查看数据集各个行为的占比情况,绘制饼图如下:

image

可以看到此数据集是一个不平衡的数据集,但是这里暂时忽略其不平衡性。

数据预处理

在LIBSVM的官方文档中可以看到,LIBSVM所使用的数据集是有严格的格式规范:

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

<label>:对于分类问题代表样本的类别,使用整数表示,支持多个类别;对于回归问题代表目标变量,可以是任意实数。

<index1>:<value1>:表示特征项。其中 <index> 代表特征项的编号,使用从 1 开始的整数表示,可以不连续;<value> 代表该特征项对应的特征值,使用实数表示。在实际的操作中,如果样本缺少某个特征项,可以直接省略,LIBSVM 会自动把该项的特征值赋为 0

标签和每项特征之间使用空格分割,每行数据使用\n分割。

只有符合这样格式的数据,才能够被LIBSVM使用,否则会直接报错。这对准备好的数据,此类库还提供了一个tools/checkdata.py核查工具,以便核查数据集是否符合要求。针对特征的提取,为了简单,这里仅提取五类特征:

了解了所需要的数据格式后,开始进行数据的预处理,并转换为所需要的格式文件。接下来分别将训练和测试数据集进行特征抽取并按照LIBSVM的数据格式重组,代码如下:

  1. import ast
  2. import math
  3. import numpy as np
  4. FEATURE = ("mean", "max", "min", "std")
  5. STATUS = ("Sitting", "Walking", "Upstairs", "Downstairs", "Jogging", "Standing")
  6. def preprocess(file_dir, Seg_granularity):
  7. gravity_data = []
  8. with open(file_dir) as f:
  9. index = 0
  10. for line in f:
  11. clear_line = line.strip().lstrip().rstrip(';')
  12. raw_list = clear_line.split(',')
  13. index = index + 1
  14. if len(raw_list) < 5:
  15. continue
  16. status = raw_list[1]
  17. acc_x = float(raw_list[3])
  18. acc_y = float(raw_list[4])
  19. print index
  20. acc_z = float(raw_list[5])
  21. if acc_x == 0 or acc_y == 0 or acc_z == 0:
  22. continue
  23. gravity = math.sqrt(math.pow(acc_x, 2)+math.pow(acc_y, 2)+math.pow(acc_z, 2))
  24. gravity_tuple = {"gravity": gravity, "status": status}
  25. gravity_data.append(gravity_tuple)
  26. # split data sample of gravity
  27. splited_data = []
  28. cur_cluster = []
  29. counter = 0
  30. last_status = gravity_data[0]["status"]
  31. for gravity_tuple in gravity_data:
  32. if not (counter < Seg_granularity and gravity_tuple["status"] == last_status):
  33. seg_data = {"status": last_status, "values": cur_cluster}
  34. # print seg_data
  35. splited_data.append(seg_data)
  36. cur_cluster = []
  37. counter = 0
  38. cur_cluster.append(gravity_tuple["gravity"])
  39. last_status = gravity_tuple["status"]
  40. counter += 1
  41. # compute statistics of gravity data
  42. statistics_data = []
  43. for seg_data in splited_data:
  44. np_values = np.array(seg_data.pop("values"))
  45. seg_data["max"] = np.amax(np_values)
  46. seg_data["min"] = np.amin(np_values)
  47. seg_data["std"] = np.std(np_values)
  48. seg_data["mean"] = np.mean(np_values)
  49. statistics_data.append(seg_data)
  50. # write statistics result into a file in format of LibSVM
  51. with open("WISDM_ar_v1.1_raw_svm.txt", "a") as the_file:
  52. for seg_data in statistics_data:
  53. row = str(STATUS.index(seg_data["status"])) + " " + \
  54. str(FEATURE.index("mean")) + ":" + str(seg_data["mean"]) + " " + \
  55. str(FEATURE.index("max")) + ":" + str(seg_data["max"]) + " " + \
  56. str(FEATURE.index("min")) + ":" + str(seg_data["min"]) + " " + \
  57. str(FEATURE.index("std")) + ":" + str(seg_data["std"]) + "\n"
  58. # print row
  59. the_file.write(row)
  60. if __name__ == "__main__":
  61. preprocess("WISDM_ar_v1.1_raw.txt", 100)
  62. pass

成功转换后的数据格式形如:

  1. .
  2. .
  3. .
  4. 5 0:9.73098373254 1:10.2899465499 2:9.30995703535 3:0.129482033438
  5. 5 0:9.74517171235 1:10.449291842 2:9.15706284788 3:0.161143714697
  6. 5 0:9.71565678822 1:10.4324206204 2:9.41070666847 3:0.136704694206
  7. 5 0:9.70622803003 1:9.7882020821 2:9.60614907234 3:0.0322246639852
  8. 5 0:9.74443440742 1:10.2915256401 2:9.28356073929 3:0.165543789197
  9. 0 0:9.28177794859 1:9.47500395778 2:8.92218583084 3:0.0700079500015
  10. 0 0:9.27218416165 1:9.40427562335 2:9.14709243421 3:0.0433805537826
  11. 0 0:9.27867211792 1:9.39755287296 2:9.1369415014 3:0.037533026091
  12. 0 0:9.27434585368 1:9.33462907672 2:9.21453200114 3:0.0263815511773
  13. .
  14. .
  15. .

由于该数据集并未区分训练和测试数据集,因此为了最终的模型验证,首先需要分割该数据集为两份,分别进行训练和模型验证,分割方法就使用最简单的2\8原则,使用LIBSVM提供的工具tools/subset.py进行数据分割:

工具使用介绍:

  1. Usage: subset.py [options] dataset subset_size [output1] [output2]
  2. This script randomly selects a subset of the dataset.
  3. options:
  4. -s method : method of selection (default 0)
  5. 0 -- stratified selection (classification only)
  6. 1 -- random selection
  7. output1 : the subset (optional)
  8. output2 : rest of the data (optional)
  9. If output1 is omitted, the subset will be printed on the screen.

使用工具进行数据分割:

  1. 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,即可看到此命令的使用方式和参数说明,假设我们使用默认的参数进行模型训练:

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

其中-b 的含义是probability_estimates,是否训练一个SVC或者SVR模型用于概率统计,设置为1,以便最终的模型评估使用。

训练过程的可能会消耗一点时间,主要在于所使用的训练数据集的大小,训练时的日志输出形如:

  1. .
  2. .
  3. .
  4. optimization finished, #iter = 403
  5. nu = 0.718897
  6. obj = -478.778647, rho = -0.238736
  7. nSV = 508, nBSV = 493
  8. Total nSV = 508
  9. *
  10. optimization finished, #iter = 454
  11. nu = 0.734417
  12. obj = -491.057723, rho = -0.318206
  13. nSV = 518, nBSV = 507
  14. Total nSV = 518
  15. *
  16. optimization finished, #iter = 469
  17. nu = 0.722888
  18. obj = -604.608449, rho = -0.360926
  19. nSV = 636, nBSV = 622
  20. Total nSV = 4136
  21. .
  22. .
  23. .

其中:#iter 是迭代次数,nu 是选择的核函数类型的参数,obj 为 SVM 文件转换为的二次规划求解得到的最小值,rho 为判决函数的偏置项 bnSV 是标准支持向量个数(0 < a[i] < c),nBSV 是边界上的支持向量个数(a[i] = c),Total nSV 是支持向量总个数。

这样我们就得到了模型文件raw_trained.model,首先使用你所熟悉的文本编译工具打开此文件,让我们查看一下此文件中的内容:

  1. svm_type c_svc //所选择的 svm 类型,默认为 c_svc
  2. kernel_type rbf //训练采用的核函数类型,此处为 RBF 核
  3. gamma 0.333333 //RBF 核的 gamma 系数
  4. nr_class 6 //类别数,此处为六元分类问题
  5. total_sv 4136 //支持向量总个数
  6. rho -0.369589 -0.28443 -0.352834 -0.852275 -0.831555 0.267266 0.158289 -0.777357 -0.725441 -0.271317
  7. -0.856933 -0.798849 -0.807448 -0.746674 -0.360926 //判决函数的偏置项 b
  8. label 4 1 2 3 0 5 //类别标识
  9. probA -3.11379 -3.0647 -3.2177 -5.78365 -5.55416 -2.30133 -2.26373 -6.05582 -5.99505 -1.07317 -4.50318
  10. -4.51436 -4.48257 -4.71033 -1.18804
  11. probB 0.099704 -0.00543388 -0.240146 -0.43331 -1.01639 0.230949 0.342831 -0.249265 -0.817104 -0.0249471
  12. -0.209852 -0.691243 -0.0803133 -0.940074 0.272984
  13. nr_sv 558 1224 880 825 325 324 //每个类的支持向量机的个数
  14. SV
  15. //以下为各个类的权系数及相应的支持向量
  16. 1 0 0 0 0 0:14.384883 1:24.418964 2:2.5636304 3:5.7143112
  17. 1 1 1 0 0 0:11.867873 1:23.548919 2:4.5479318 3:4.5074937
  18. 1 0 0 0 0 0:14.647238 1:24.192184 2:4.0759445 3:5.367968
  19. 1 0 0 0 0 0:14.374831 1:24.286867 2:2.0045062 3:5.5710882
  20. 1 0 0 0 0 0:14.099495 1:24.03442 2:2.42664 3:5.7580063
  21. 1 0 0 0 0 0:14.313538 1:25.393975 2:1.9496137 3:5.6174387
  22. ...

得到模型文件之后,首先要进行的就是模型的测试验证,还记得开始进行数据准备的时候,我们分割了训练和测试数据集吗?训练数据集进行了模型的训练,接下来就是测试数据集发挥作用的时候了。

验证模型,LIBSVM提供了另一个命令方法svm-predict,使用介绍如下:

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

使用测试数据集进行模型验证:

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

执行此命令后,LIBSVM会进行识别预测,由于我们使用了-b 1参数,因此最终会输出各个类别的识别概率到predict.out文件中,并且会输出一个总体的正确率:

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

可以看到此时我们训练的模型的识别正确率为78.4932%

predict.out 文件内容形如:

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

其中,第一行为表头,第一列是识别出的类别标签,后面依次跟着各个标签的识别概率。

那么问题来了,难道模型的识别正确率就只能到这个程度了吗?我们再次回顾svm-train命令,其中有很多的参数我们都使用了默认的设置,并没有进行特定的设置。通过查看LIBSVM官方的文档,发现竟然提供了参数寻优的工具tools/grid.py,通过此工具可以自动寻找训练数据集中的最优参数C系数和gamma系数,以在训练的时候使用。具体用法如下:

  1. Usage: grid.py [grid_options] [svm_options] dataset
  2. grid_options :
  3. -log2c {begin,end,step | "null"} : set the range of c (default -5,15,2)
  4. begin,end,step -- c_range = 2^{begin,...,begin+k*step,...,end}
  5. "null" -- do not grid with c
  6. -log2g {begin,end,step | "null"} : set the range of g (default 3,-15,-2)
  7. begin,end,step -- g_range = 2^{begin,...,begin+k*step,...,end}
  8. "null" -- do not grid with g
  9. -v n : n-fold cross validation (default 5)
  10. -svmtrain pathname : set svm executable path and name
  11. -gnuplot {pathname | "null"} :
  12. pathname -- set gnuplot executable path and name
  13. "null" -- do not plot
  14. -out {pathname | "null"} : (default dataset.out)
  15. pathname -- set output file path and name
  16. "null" -- do not output file
  17. -png pathname : set graphic output file path and name (default dataset.png)
  18. -resume [pathname] : resume the grid task using an existing output file (default pathname is dataset.out)
  19. This is experimental. Try this option only if some parameters have been checked for the SAME data.
  20. svm_options : additional options for svm-train

又是一堆的参数,但是不必担心,对于初学者来说,这里的大部分参数都可以不用设置,直接使用默认值即可,如果你需要查看参数寻优的过程,还需要安装gnuplot并按照官方说明配置。

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

执行此命令后,会不断的输出不同的C系数和gamma系数取值情况下的分类准确率,并在最后一行输出最优的参数选择:

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

并且会在当前目录下生成输出文件raw_train.txt.out和对应的图形文件raw_train.txt.png

image

经过最优参数的寻找,最终给出了C系数为8192.0,gamma系数为0.03125的情况下,模型分类的准确率最高,为79.3446。

接下来我们再次使用svm-train方法,并设置当前最优C系数值和gamma系数值,重新训练我们的模型:

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

训练完成后,得到新的模型文件raw_bestP_trained.model,再次使用测试数据集进行验证:

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

最终输出结果如下:

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

可以看到模型的预测正确率明显提升了不少。上面的参数寻优仅仅是使用了默认的参数进行寻找,你也可以继续尝试设置各个参数进行参数寻优,以进一步提升模型识别正确率,这里不在进行进一步的参数寻优。

小结

可以看到SVM进行用户行为识别,可以得到较好的效果,本文中使用的数据是实验室数据,并且特征也仅仅提取了基本的几个,准确率即可达到79%以上,此方案可以继续进行优化,使用真实世界采集的数据,进行更加详细的特征准备,提高训练时的迭代次数等,进行模型重新训练优化,最终达到更好的分类效果。

下面,我们将在iOS平台下构建应用,并使用LIBSVM和本文中训练所得到的模型,进行准实时人类行为识别。

前面,我们简单介绍了支持向量机以及如何使用LIBSVM类库和加速度传感器数据进行特征的抽取、模型的训练、参数的调优和模型的测试等,在本文中,将使用上篇最终得到的模型文件,以及LIBSVM类库,在iOS平台下构建一个能够识别当前客户端用户的行为类型的应用。

随着移动终端设备的性能越来越高,其集成的传感器设备也越来越多,侦测精度越来越高的情况,应用于移动终端设备上的机器学习应用也多了起来。在iOS平台下,苹果官方的很多应用中也呈现出了机器学习的影子。例如iOS 10 系统中的相册,能够进行人脸识别并进行照片的自动分类、邮件中的自动垃圾邮件归类、Siri智能助理、健康应用中的用户运动类型分类等。

用户的运动类型

在iOS系统的健康应用中,可以看到你的运行类型,其中包含了行走、跑步、爬楼梯、步数、骑自行车等类型。

image

在iOS SDK中也提供了一个专用于运动类型获取的类CMMotionActivityManager,其中包含了

  1. stationary
  2. walking
  3. running
  4. automotive
  5. cycling
  6. 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.

因此用户行为的识别并不是严格意义上的准确的,在机器学习领域,预测都会有一个概率的输出,引申出的就是正确率,正确率也是评估一个机器学习模型的标准之一。

关于加速度传感器

苹果的移动设备中,集成了多种传感器,本文所演示的仅仅使用加速度传感器,你也可以增加传感器类型,提高数据的维度等。

image

加速度传感器数据 CMAccelerometerData 的类型为CMAcceleration,提供了三轴加速度值,如下:

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

此加速度值是当前设备总的加速度值,想要获取加速度分量的时候,可以使用CMDeviceMotion进行获取。

构建iOS项目,收集传感器数据

在上篇中,我们已经知道,LIBSVM具有多种语言的接口,这里我们直接使用其C语言接口,在iOS项目中构建SVM分类器。

1. 传感器数据收集

首先需要收集加速度传感器数据,并进行数据特征抽取和数据准备,以便SVM算法识别使用。在iOS的CoreMotion框架中,已经提供了获取加速度传感器的API,开发者可以直接调用接口获取加速度传感器数据:

  1. CMMotionManager *motionManager = [[CMMotionManager alloc] init];
  2. if ([motionManager isAccelerometerAvailable]) {
  3. [motionManager setAccelerometerUpdateInterval:0.02];
  4. startTime = [[NSDate date] timeIntervalSince1970];
  5. [motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
  6. if (error) {
  7. NSLog(@"%@", error.description);
  8. }else{
  9. [self handleDeviceAcc:accelerometerData];
  10. }
  11. }];
  12. }

2. 数据批量化处理

我们在本文开始介绍训练数据集的时候,提到了数据的采集频率是 20 Hz,因此我们在进行数据采集的时候也需要同样的频率,并且将传感器数据进行批量化处理,以便于模型识别时具有合适数量的数据。

  1. NSArray *valueArr = @[
  2. @(accelerometerData.acceleration.x * g_value),
  3. @(accelerometerData.acceleration.y * g_value),
  4. @(accelerometerData.acceleration.z * -g_value)];
  5. NSMutableDictionary *sample = [NSMutableDictionary dictionary];
  6. [sample setValue:currenStatus forKey:@"status"];
  7. [sample setValue:@"acc" forKey:@"sensorName"];
  8. [sample setValue:@([self getTimeStampByMiliSeconds]) forKey:@"timestamp"];
  9. [sample setValue:valueArr forKey:@"values"];
  10. if (sampleDatas == nil) {
  11. sampleDatas = [NSMutableArray array];
  12. }
  13. if ([sampleDatas count] == 256) {
  14. NSArray *readySamples = [NSArray arrayWithArray:sampleDatas];
  15. sampleDatas = nil;
  16. [self stopMotionAccelerometer];
  17. [self recognitionData:[readySamples copy]];
  18. }else{
  19. [sampleDatas addObject:sample];
  20. }

3. 特征抽取

在开始此步骤之前,我们需要导入LIBSVM的类库到项目工程中,这里仅需要导入svm.hsvm.cpp两个文件即可。

在训练模型的时候,我们使用了五种特征,最终生成所需要的数据格式,这里同样,我们也需要针对数据进行特征提取,并重新组合数据成为LIBSVM所要求的数据格式:

  1. for (NSUInteger index = 0; index < [raw_datas count]; index++) {
  2. NSDictionary *jsonObject = raw_datas[index];
  3. NSArray *valuesArray = jsonObject[@"values"];
  4. if (!valuesArray || valuesArray.count <= 0) {
  5. break;
  6. }
  7. id acc_x_num = valuesArray[0];
  8. id acc_y_num = valuesArray[1];
  9. id acc_z_num = valuesArray[2];
  10. acc_x_axis[index] = acc_x_num;
  11. acc_y_axis[index] = acc_y_num;
  12. acc_z_axis[index] = acc_z_num;
  13. gravity[index] = @(sqrt(pow([acc_x_num doubleValue], 2) + pow([acc_y_num doubleValue], 2) + pow([acc_z_num doubleValue], 2)));
  14. }
  15. NSMutableArray *values = [NSMutableArray array];
  16. /* mean Feature */{
  17. struct svm_node node_x_mean = {0, [StatisticFeature mean:acc_x_axis]};
  18. NSValue *node_x_mean_value = [NSValue valueWithBytes:&node_x_mean objCType:@encode(struct svm_node)];
  19. [values addObject:node_x_mean_value];
  20. struct svm_node node_y_mean = {1, [StatisticFeature mean:acc_y_axis]};
  21. NSValue *node_y_mean_value = [NSValue valueWithBytes:&node_y_mean objCType:@encode(struct svm_node)];
  22. [values addObject:node_y_mean_value];
  23. struct svm_node node_z_mean = {2, [StatisticFeature mean:acc_z_axis]};
  24. NSValue *node_z_mean_value = [NSValue valueWithBytes:&node_z_mean objCType:@encode(struct svm_node)];
  25. [values addObject:node_z_mean_value];
  26. struct svm_node node0 = {3, [StatisticFeature mean:gravity]};
  27. NSValue *value0 = [NSValue valueWithBytes:&node0 objCType:@encode(struct svm_node)];
  28. [values addObject:value0];
  29. }
  30. /* max Feature */{
  31. struct svm_node node_x_max = {4, [StatisticFeature max:acc_x_axis]};
  32. NSValue *node_x_max_value = [NSValue valueWithBytes:&node_x_max objCType:@encode(struct svm_node)];
  33. [values addObject:node_x_max_value];
  34. struct svm_node node_y_max = {5, [StatisticFeature max:acc_y_axis]};
  35. NSValue *node_y_max_value = [NSValue valueWithBytes:&node_y_max objCType:@encode(struct svm_node)];
  36. [values addObject:node_y_max_value];
  37. struct svm_node node_z_max = {6, [StatisticFeature max:acc_z_axis]};
  38. NSValue *node_z_max_value = [NSValue valueWithBytes:&node_z_max objCType:@encode(struct svm_node)];
  39. [values addObject:node_z_max_value];
  40. struct svm_node node1 = {7, [StatisticFeature max:gravity]};
  41. NSValue *value1 = [NSValue valueWithBytes:&node1 objCType:@encode(struct svm_node)];
  42. [values addObject:value1];
  43. }
  44. /* min Feature */{
  45. struct svm_node node_x_min = {8, [StatisticFeature min:acc_x_axis]};
  46. NSValue *node_x_min_value = [NSValue valueWithBytes:&node_x_min objCType:@encode(struct svm_node)];
  47. [values addObject:node_x_min_value];
  48. struct svm_node node_y_min = {9, [StatisticFeature min:acc_y_axis]};
  49. NSValue *node_y_min_value = [NSValue valueWithBytes:&node_y_min objCType:@encode(struct svm_node)];
  50. [values addObject:node_y_min_value];
  51. struct svm_node node_z_min = {10, [StatisticFeature min:acc_z_axis]};
  52. NSValue *node_z_min_value = [NSValue valueWithBytes:&node_z_min objCType:@encode(struct svm_node)];
  53. [values addObject:node_z_min_value];
  54. struct svm_node node2 = {11, [StatisticFeature min:gravity]};
  55. NSValue *value2 = [NSValue valueWithBytes:&node2 objCType:@encode(struct svm_node)];
  56. [values addObject:value2];
  57. }
  58. /* stev Feature */{
  59. struct svm_node node_x_stev = {12, [StatisticFeature stev:acc_x_axis]};
  60. NSValue *node_x_stev_value = [NSValue valueWithBytes:&node_x_stev objCType:@encode(struct svm_node)];
  61. [values addObject:node_x_stev_value];
  62. struct svm_node node_y_stev = {13, [StatisticFeature stev:acc_y_axis]};
  63. NSValue *node_y_stev_value = [NSValue valueWithBytes:&node_y_stev objCType:@encode(struct svm_node)];
  64. [values addObject:node_y_stev_value];
  65. struct svm_node node_z_stev = {14, [StatisticFeature stev:acc_z_axis]};
  66. NSValue *node_z_stev_value = [NSValue valueWithBytes:&node_z_stev objCType:@encode(struct svm_node)];
  67. [values addObject:node_z_stev_value];
  68. struct svm_node node3 = {15, [StatisticFeature stev:gravity]};
  69. NSValue *value3 = [NSValue valueWithBytes:&node3 objCType:@encode(struct svm_node)];
  70. [values addObject:value3];
  71. }

这里需要注意的是,特征的顺序必须和模型训练时训练数据集中的特征顺序一致,否则预测的结果将出现严重的偏差。

4. 导入模型文件并加载

完成了数据准备之后,我们导入之前训练好的模型文件raw_bestP_trained.model到项目中,然后使用LIBSVM提供的模型加载方法,加载模型到svm_model结构体对象:

  1. struct svm_model * model = svm_load_model([model_dir UTF8String]);
  2. if (model == NULL) {
  3. NSLog(@"Can't open model file: %@",model_dir);
  4. return nil;
  5. }
  6. if (svm_check_probability_model(model) == 0) {
  7. NSLog(@"Model does not support probabiliy estimates");
  8. return nil;
  9. }

4. 行为识别

LIBSVM提供了多个方法进行预测,为了最终看到预测的概率,我们使用

  1. double svm_predict_probability(const struct svm_model *model,
  2. const struct svm_node *x,
  3. double* prob_estimates);

方法,在输出预测结果的时候,会带有对应的概率:

  1. //Type of svm model
  2. int svm_type = svm_get_svm_type(model);
  3. //Count of labels
  4. int nr_class = svm_get_nr_class(model);
  5. //Label of svm model
  6. int *labels = (int *) malloc(nr_class*sizeof(int));
  7. svm_get_labels(model, labels);
  8. // Probability of each possible label in result
  9. double *prob_estimates = (double *) malloc(nr_class*sizeof(double));
  10. // Predicting
  11. // result of prediction including:
  12. // - Most possible label
  13. // - Probability of each possible label
  14. double label = 0.0;
  15. if (svm_type == C_SVC || svm_type == NU_SVC) {
  16. label = svm_predict_probability(model, X, prob_estimates);
  17. NSLog(@"svm_predict_probability label: %f",label);
  18. }else{
  19. NSLog(@"svm_type is not support !!!");
  20. return nil;
  21. }

通过以上的预测之后,最终的预测结果就是label,并在会在prob_estimates中输出各个分类标签的预测概率。

!!注意 !!
** prob_estimates 中仅仅会输出概率,并不会输出概率和标签的对应关系。prob_estimates中的概率顺序是和模型中的输入标签顺序一致的,需要注意! **

最终的预测结果如下:

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

关于模型的评估

分类模型的度量有很多方式,例如混淆矩阵(Confusion Matrix)、ROC曲线、AUC面积、Lift(提升)和Gain(增益)、K-S图、基尼系数等,这里我们使用ROC曲线评估我们最终得到的模型,以查看模型的质量,最终的ROC曲线图如下:

image

可以看到该模型针对某些行为的识别能力较好,例如站立、慢跑,但是对另一些行为的识别却不怎么好了,例如下楼梯。

总结

可以看到SVM在分类问题上能够很好的识别特征进行类别区分。由于篇幅原因,本文中并没有对数据的特征进行更加细致的选择和抽取,可能会导致一些行为类型的识别不能达到理想的效果,但是相信在足量的数据下,进行更加细致的特征工程后,利用SVM在分类能力上的优势,能够构建出更加优秀的人类行为类型识别的智能应用。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注