这篇文章会记录在 Android 应用程序中使用 tensorflow 训练的 CNN+GRU 模型来进行活动(体育活动:爬楼梯,走路,跑步等;呼吸活动:正常呼吸,咳嗽,窒息等)识别。这个项目使用的数据是来自三维 accelerometer 的数据,并没有用到陀螺仪。这个 accelerometer 是通过蓝牙低功耗连接的 respeck 传入的。在这里我更关心的是 tensorflow 模型和 Android app 的接入,所以蓝牙连接组件怎么写需要你自己去研究。我们使用的 CNN+GRU 模型在检测窗口是 20 条数据的配置下,可以达到实时检测率 100% 的准确度。
训练模型
完整代码放在 这里 了。因为学术道德要求,没有包含数据,需要自行收集。我提供了一个示例数据以供参考格式。
Feature Engineering
示例数据中包含 accelerometer 和 gyroscope 信息。我们只会使用 accelerometer 来进行训练和识别。如果你加上 gyroscope 的话,体育活动的成功检测率很容易就能到 90% 以上。我们会使用三轴的加速度来计算一个加速幅度 m, 即:
$$m = \sqrt{acc_x^2 + acc_y^2 + acc_z^2}$$
这个人工制造的特性 m 大大提升了我们识别的准确度。
模型构造
基于 accelerometer 的活动识别,算是对时间序列数据的工作,那么第一个想到的就是 RNN. 可是如果直接用 RNN 的话,最后的 F1 准确率会比较低,应该是特征有点复杂了。所以我们先让数据过一层卷积,再给 RNN 处理。有很多文献选择了 Long Short-term Memory (LSTM) 作为 RNN 层,别的组也几乎都用的是 LSTM。我们选择 GRU 是因为它对比起 LSTM,是更新的一个技术,并且训练时间能快一些。就我们自己的测试来看,LSTM 和 GRU 在准确率上没有太大的差距。
用 tf.keras 库,我们构建的 CNN+GRU 模型结构如下。Conv1D:MaxPooling1D:GRU:Flatten:Dense(relu):Dropout:Dense(softmax). 在查阅文献的时候,有的学者构建了两个全连接层,一个用 tanh,一个用 relu. 我们在实验的时候对比没有发现明显的差距,所以只用了一个全连接层。
调参与获取模型
基于 tf 的调参非常简单,用 kera_tunner
这个模块就好了。比如:
import keras_tuner as kt
def build_1d_cnn_gru_model(hp, ..)
# 在模型中记录需要调配的参数
model.add(MaxPooling1D(pool_size=hp.Choice('pool_size', values=[2, 3, 4])))
# 构建 tunner
tuner = kt.Hyperband(
lambda hp: build_1d_cnn_gru_model(hp, ..),
objective='val_accuracy',
max_epochs=200,
factor=4,
directory='my_dir',
project_name='cnn_gru_tuning'
)
# 这样 tunner 就能用网格搜索帮你找到最好的参数了
tuner.search(X_train, y_train_one_hot, epochs=50, validation_data=(X_test, y_test_one_hot))
# 获取最好的参数,并用这个参数构建模型
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
model = build_1d_cnn_gru_model(best_hps, ..)
如果你的 GPU 是 N 卡的话,记得装上 cuda,这样模型训练的时候 tensorflow 就会自动用显卡加速了。
为了在 android 程序中使用,我们需要把这个 tf 模型转换成 tflite 格式并保存。这个也很简单。
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
Android 中使用 tflite 模型
这里使用的是 Android Studio 进行开发。直接在 File -> New -> Other -> Tensorflow Lite Model 就可以导入已经训练好的模型。因为我们用了 kera 的 GRU 层,所以需要调用 google ai 下面的 litert 包。Android Studio 导入的时候自动加入的依赖太老了,不支持 GRU。所以在导入的时候 不要 勾选自动添加依赖。找到你 App 的 build.gradle
文件,在 dependencies 里面加入下面这两个包:
implementation 'com.google.ai.edge.litert:litert-support:1.0.1'
implementation 'com.google.ai.edge.litert:litert-metadata:1.0.1'
implementation 'com.google.ai.edge.litert:litert-gpu:1.0.1'
implementation("org.tensorflow:tensorflow-lite-select-tf-ops:2.16.1")
调用模型的时候,需要注意 input window 的大小,我的 window size 是 20,有 4 个 feature,数据大小是 4,那我就需要创建一个 4 *20*4 的 bytebuffer。最后转成 float buffer。
val byteBuffer = ByteBuffer.allocate(4 * 20 * 4)
byteBuffer.order(ByteOrder.nativeOrder())
for (data in bufferedData) {
// construct a new byte buffer for each data point
var dataBuffer = ByteBuffer.allocate(4 * 4)
dataBuffer.order(ByteOrder.nativeOrder())
for (value in data) {
dataBuffer.putFloat(value)
}
dataBuffer.flip()
byteBuffer.put(dataBuffer)
}
byteBuffer.flip()
val floatArray = FloatArray(byteBuffer.capacity() / 4)
byteBuffer.asFloatBuffer().get(floatArray)
然后把 float buffer 转换成 tensor,以供模型读取。
val inputFeature0 = TensorBuffer.createFixedSize(intArrayOf(1, 20, 4), DataType.FLOAT32)
inputFeature0.loadBuffer(byteBuffer)
然后就是调用模型,获取返回结果了。这个和示例代码是一样的。
val outputs = model.process(inputFeature0)
val outputArray = outputs.outputFeature0AsTensorBuffer.floatArray
var result = outputArray.indexOfFirst { it == outputArray.maxOrNull()!! }
除另有声明外,本博客文章均采用 知识共享(Creative Commons) 署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。