找回密码
 立即注册
首页 业界区 业界 这也行?按键动作模式识别也能用贝叶斯? ...

这也行?按键动作模式识别也能用贝叶斯?

章绮云 2026-2-18 11:40:00
首发于21ic论坛
前言

之前学习了贝叶斯更新的相关内容,正好现在也在玩开发板,板子上面有几个小的单击按键,一般识别按键动作的做法就很简单,不是中断就是查询,基本都是靠边沿或者电平的状态来进行的,这一套就很无聊,没有实现的欲望,所以想用点不一样的方法。
这就有了本片文章的出现,基于朴素贝叶斯分类,使用滑动窗口捕捉电平序列,提取特征进行模式识别,理想情况下识别效果杠杠的,但是出现边界以及混合的情况,效果一言难尽,目前水平不够,这应该也是后续需要解决的主要问题了。
技术要点

核心原理


  • 贝叶斯定理
本文实现的方法基于朴素贝叶斯分类器,主要就是两方面内容:贝叶斯定理与条件独立假设,涉及的概念有先验概率后验概率条件概率,其中先验和条件概率都是提前准备好的,可以是主观经验的,也可以是统计量化的,而贝叶斯定理中的条件概率(不是后验概率),又称为似然概率。
这个方法的基本思想是:对于给定的待分类项(就是窗口中的电平序列),求解当这个待分类项出现时,各个已经定义过的模式类别出现的概率,哪个概率最大,那么这个待分类项就属于哪个模式。
在开始分类之前需要一些必要的准备工作:

  • 定义有哪些模式类别,这些模式边界要明确,不然不容易分析特征
  • 定义这些模式的特征属性,这些属性在不同模式下的表现是不同的,这是识别的关键,对应了贝叶斯定理中的似然概率

  • 滑动窗口
这里的窗口是实时更新的窗口,老数据移出,新数据加入,滑动窗口确定电平序列数据的范围,只有处在窗口中的序列数据才会得到特征提取的机会,它的长度与序列的时间长度成比例,也就是说采样频率会影响到窗口时效性。
它需要考虑的问题是怎么捕捉到完整的信号,对应于滑动的步长,以及特征提取的周期。
基本步骤

通过以下步骤实现按键动作模式识别:

  • 滑动窗口采集:使用固定大小的滑动窗口持续采集按键状态数据
  • 特征提取:从窗口数据中提取多个维度的特征
  • 概率计算:基于先验概率和似然概率计算后验概率
  • 模式判断:根据后验概率和阈值确定当前按键模式
具体实现

为了验证设想的可行性,通过逻辑分析仪记录按键的引脚电平变化,低电平表示按键按下,高电平表示无按键动作,采样率1MHz,时长20s,在后面的实验中,认为序列是连续的,这就是电平序列的来源,具体序列如下图所示:
1.png

上面记录的数据可以作为一个样本,我通过观察和测量确定了几种模式,以及一些帮助识别的特征属性,在实验过程中使用python进行了方法验证。
模式定义

我在设计过程中定义了四种按键模式,分别如下:

  • 无效:无有效按键动作
  • 单击:单次短暂按键动作
  • 双击:快速连续两次按键动作
  • 长按:持续时间较长的按键动作
动作的实施都是通过一个单按键来进行的,其中单击和双击涉及到电平的较快速变化,是识别的难点
特征选择

基于对提取的特征包括:

  • 高电平占比:窗口内高电平信号的比例
  • 上升沿数量:信号从低到高的转换次数
  • 下降沿数量:信号从高到低的转换次数
  • 最长连续高电平持续时间:窗口内持续高电平的最长时间
概率模型


  • 先验概率:初始假设四种模式等概率出现,即每个模式的先验都是0.25。并且和一般的贝叶斯方法不同的是,在实现过程中认为先验是不需要更新的,也就是在每一次识别时认为每个模式都是等概率出现的,没有转移概率或者历史因素影响
  • 似然概率:基于特征分布参数计算观测到当前特征的概率,其中的分布参数是根据实际捕捉的序列数据来设计的,概率分布模型采用正态分布来近似,需要均值和标准差,统一使用概率密度表达似然结果

    • 高电平占比的分布参数

      • 无效:0.05,0.2
      • 单击:0.2,0.2
      • 双击:0.3,0.2
      • 长按:0.9,0.2

    • (上升沿/下降沿)数量的分布参数

      • 无效:0.1,0.3
      • 单击:1,0.3
      • 双击:2,0.3
      • 长按:0.7,0.3

    • 最长高电平持续时间的分布参数

      • 无效:0,2
      • 单击:0.2,5
      • 双击:0.17,3
      • 长按:0.9,10


  • 后验概率:使用贝叶斯公式计算各模式的后验概率,先计算提取的特征在每个模式下的联合似然,基于条件独立假设,可以直接相乘,然后计算后验并归一化可得最终的概率表
代码实现


  • 数据采集与预处理
把逻辑分析仪中的数据导出为csv文件,代码首先实现了 read_sigrok_csv_simple 函数,用于读取 sigrok CSV 格式的按键数据:
  1. def read_sigrok_csv_simple(filename):
  2.     time_data = []
  3.     signal_data = []
  4.     with open(filename, 'r', newline='') as csvfile:
  5.         reader = csv.reader(csvfile)
  6.         for row in reader:
  7.             # 跳过注释行和空行
  8.             if not row or row[0].startswith(';'):
  9.                 continue
  10.             # 确保行有两个列
  11.             if len(row) >= 2:
  12.                 try:
  13.                     time_val = float(row[0])
  14.                     data_val = float(row[1])
  15.                     time_data.append(time_val)
  16.                     signal_data.append(data_val)
  17.                 except ValueError:
  18.                     # 跳过无法转换为数字的行
  19.                     continue
  20.     return time_data, signal_data
复制代码
该函数读取 CSV 文件中的时间戳和信号值,返回两个列表分别存储时间数据和信号数据,通过plot输出采样的数据图如下所示:
2.png


  • 识别器类设计
核心实现是 BayesianButtonRecognizer 类,用于实现基于贝叶斯分类的按键模式识别:
  1. class BayesianButtonRecognizer:
  2.     """基于滑动窗口和贝叶斯更新的按键模式识别器"""
  3.     def __init__(self, window_size=20, sample_interval=0.01, 
  4.     threshold=0.7):
  5.         """
  6.         初始化识别器
  7.         Args:
  8.             window_size: 滑动窗口大小
  9.             sample_interval: 采样间隔(秒)
  10.             threshold: 判定阈值
  11.         """
  12.         self.window_size = window_size
  13.         self.sample_interval = sample_interval
  14.         self.threshold = threshold
  15.         # 滑动窗口存储最近的观测序列
  16.         self.window = deque(maxlen=window_size)
  17.         # 模式类别
  18.         self.modes = ['无效', '单击', '双击', '长按']
  19.         # 先验概率 - 初始等可能
  20.         self.prior = np.array([0.25, 0.25, 0.25, 0.25])
  21.         # 特征提取相关的参数(单位:采样点数)
  22.         self.short_press_max = 15  # 短按最大持续时间
  23.         self.long_press_min = 30    # 长按最小持续时间  
  24.         self.double_click_interval = 10  # 双击间隔阈值
  25.         # 初始化特征分布参数(基于物理理解预设)
  26.         self._init_feature_distributions()
  27.         # 特征权重
  28.         self.featwight={
  29.             "无效":np.array([1.2,0.8,0.8,1.2]),
  30.             "单击":np.array([1,1.2,1.2,1]),
  31.             "双击":np.array([1,1.2,1.2,0.8]),
  32.             "长按":np.array([1.2,0.8,0.8,1.2])
  33.         }
复制代码

  • 特征分布初始化
识别器初始化时设置了各模式下特征的概率分布参数:
  1. def _init_feature_distributions(self):
  2.     """初始化各模式下特征的概率分布参数"""
  3.     # 高电平占比的分布参数
  4.     self.high_ratio_params = {
  5.         '无效': 0.05,   # 无效时高电平占比很低
  6.         '单击': 0.2,    # 单击时有短暂高电平
  7.         '双击': 0.3,    # 双击时高电平占比稍高
  8.         '长按': 0.9     # 长按时高电平占比很高
  9.     }
  10.     # 上升沿数量的分布参数
  11.     self.rise_count_params = {
  12.         '无效': 0.1,    # 无效时几乎无上升沿
  13.         '单击': 1,    # 单击时有1个上升沿
  14.         '双击': 2,    # 双击时有2个上升沿  
  15.         '长按': 0.7     # 长按有1个上升沿
  16.     }
  17.     # 最长高电平持续时间的分布参数(正态分布:均值,标准差)
  18.     self.max_duration_params = {
  19.         '无效': (0, 2),     # 无效时持续时间很短
  20.         '单击': (0.2, 5),     # 单击中等持续时间
  21.         '双击': (0.17, 3),     # 双击每次按下时间短
  22.         '长按': (0.9, 10)    # 长按持续时间长
  23.     }
复制代码

  • 特征提取
从滑动窗口数据中提取特征,其中高电平占比是通过求序列平均值来获得的,然后边沿计数对应了记录序列跳变数量,最长高电平时间通过记录连续高电平时长获取:
  1. def extract_features(self, window_data):
  2.     """从滑动窗口数据中提取特征"""
  3.     if len(window_data) == 0:
  4.         return None
  5.     data = np.array(window_data)
  6.     # 特征1: 高电平占比
  7.     high_ratio = np.mean(data)
  8.     # 特征2: 上升沿数量(0->1的变化)
  9.     rises = 0
  10.     for i in range(1, len(data)):
  11.         if data[i-1] == 0 and data[i] == 1:
  12.             rises += 1
  13.     # 特征3: 下降沿数量(1->0的变化)
  14.     falls = 0
  15.     for i in range(1, len(data)):
  16.         if data[i-1] == 1 and data[i] == 0:
  17.             falls += 1
  18.     # 特征4: 最长连续高电平持续时间
  19.     max_duration = 0
  20.     current_duration = 0
  21.     for val in data:
  22.         if val == 1:
  23.             current_duration += 1
  24.             max_duration = max(max_duration, current_duration)
  25.         else:
  26.             current_duration = 0
  27.     return {
  28.         'high_ratio': high_ratio,
  29.         'rise_count': rises, 
  30.         'fall_count': falls,
  31.         'max_duration': max_duration
  32.     }
复制代码

  • 似然概率计算
计算给定模式下观测到特征值的似然概率,即条件概率,通过上面定义的分布参数,使用正态分布近似,在python中通过stats.norm.pdf求特征对应每个模式的似然程度,然后基于条件独立的假设,求解联合似然,表示样本对某一模式的最终似然结果:
  1. def calculate_likelihood(self, features, mode):
  2.     """计算给定模式下观测到特征值的似然概率"""
  3.     if features is None:
  4.         return 1.0  # 无特征时返回中性似然
  5.     # 使用概率密度函数计算各特征的似然
  6.     likelihoods = []
  7.     # 1. 高电平占比的似然
  8.     target_ratio = self.high_ratio_params[mode]
  9.     # 使用正态分布近似, 标准差根据经验设定
  10.     like_ratio = stats.norm.pdf(features['high_ratio'], 
  11.                               target_ratio, 0.2)
  12.     likelihoods.append(like_ratio + 1e-10)  # 避免零
  13.     # 2. 上升沿数量的似然
  14.     target_rises = self.rise_count_params[mode]
  15.     like_rises = stats.norm.pdf(features['rise_count'], 
  16.     target_rises,0.3)
  17.     likelihoods.append(like_rises + 1e-10)
  18.     # 3. 下降沿(同上升沿)数量的似然
  19.     target_falls = self.rise_count_params[mode]
  20.     like_falls = stats.norm.pdf(features['fall_count'], 
  21.     target_falls,0.3)
  22.     likelihoods.append(like_falls + 1e-10)
  23.     # 4. 最长持续时间的似然(使用正态分布)
  24.     target_dur, std_dur = self.max_duration_params[mode]
  25.     target_dur *= self.window_size
  26.     like_duration = stats.norm.pdf(features['max_duration'], 
  27.                                  target_dur, std_dur)
  28.     likelihoods.append(like_duration + 1e-10)
  29.     # 组合各特征的似然(假设特征条件独立)
  30.     total_likelihood = np.prod(np.array(likelihoods))
  31.     print("特征在mode[%s]的似然:"%{mode},likelihoods,"最终联合似然:%.
  32.     3f"%total_likelihood)
  33.     return total_likelihood
复制代码

  • 滑动窗口更新
  1. def slide_window(self,io_state):
  2.     # 移除最旧的值
  3.     self.window.popleft()
  4.     # 将新观测值加入滑动窗口
  5.     self.window.append(io_state)
复制代码

  • 信念更新与模式判断
计算完样本对每个模式的似然后,就于先验概率相乘,就得到了后验概率,然后归一化得到最终结果,同时使用阈值判定机制,当最大后验超过判定阈值后,才会识别具体模式,否则就是不确定
  1. def update_belief(self, io_state):
  2.     """根据新观测值更新信念"""
  3.     # 提取当前窗口的特征
  4.     features = self.extract_features(self.window)
  5.     print("特征提取:",features)
  6.     # 计算各模式的似然
  7.     likelihoods = np.array([self.calculate_likelihood(features, 
  8.     mode) 
  9.                           for mode in self.modes])
  10.     # 贝叶斯更新: 后验 ∝ 似然 × 先验
  11.     unnormalized_posterior = likelihoods * self.prior
  12.     evidence = np.sum(unnormalized_posterior)
  13.     if evidence > 0:
  14.         posterior = unnormalized_posterior / evidence
  15.     else:
  16.         posterior = self.prior.copy()
  17.     # 更新先验(用于下一次迭代)
  18.     # self.prior = posterior
  19.     # 判断当前模式
  20.     best_mode_idx = np.argmax(posterior)
  21.     best_prob = posterior[best_mode_idx]
  22.     print("后验:",posterior)
  23.     if best_prob > self.threshold:
  24.         detected_mode = self.modes[best_mode_idx]
  25.     else:
  26.         detected_mode = '不确定'
  27.     return detected_mode, posterior
复制代码

  • 主函数与演示
因为定义了高电平为有效电平,但实际中低电平,或者说下降沿是按键动作的反应,所以处理数据序列时做了相应的取反处理。
  1. if __name__ == "__main__":
  2.     DeltaT = 0.01 # 采样间隔
  3.     UnitTime = 1e-06 # 原始数据点的时基
  4.     SampleInterval = math.floor(DeltaT / UnitTime)
  5.     filename = "key_data_20s_all.csv"  # 逻辑分析仪导出的数据
  6.     recognizer = BayesianButtonRecognizer(window_size=100, 
  7.     threshold=0.8)
  8.     recognizer.reset()
  9.     time_data, signal_data = read_sigrok_csv_simple(filename)
  10.     print(f"成功读取数据,共 {len(time_data)} 个数据点")
  11.     print(f"时间范围: {time_data[0]}s 到 {time_data[-1]}s")
  12.     plt.figure(1)
  13.     sample_data = []
  14.     res_data = []
  15.     sample_num = math.floor(len(signal_data) / SampleInterval)
  16.     print("sample size is:",sample_num)
  17.     for i in range(sample_num-1):
  18.         sample_data.append(int(not signal_data[SampleInterval*i]))
  19.         recognizer.slide_window(int(not signal_data
  20.         [SampleInterval*i]))
  21.         if i%recognizer.window_size==0:
  22.             res,postrior=recognizer.update_belief(i)
  23.             if(res not in["不确定","无效"]):
  24.                 res_data.append(res)
  25.             print("win[%d]:"%i,res)
  26.             plt.plot(recognizer.window)
  27.             plt.show()
  28.     plt.figure(1)
  29.     plt.plot(sample_data)
  30.     plt.show()
  31.     print(res_data)
复制代码
当窗口中样本序列是理想情况时,识别效果相当好:
无效样本示例:
3.png

上图是一个无效按键样本序列图,保持无效电平,没有边沿变化。下图给出了识别的过程和结果:
4.png

可以看到特征提取的信息是正确的,高电平占比为0,边沿计数为0,最长高电平延时为0,在各个模式的似然列表中,给出了对应的似然结果,同时从列数据对比来看,也可以直接从数值上看出样本特征更偏向哪个模式,最终的后验结果,确实是无效模式的概率最高,即判定窗口中的序列为无效。
单击样本示例:
5.png

上图是一个单击按键样本序列图,有边沿变化,一个上升沿,一个下降沿,高电平占比大约0.2。下图给出了识别的过程和结果:
6.png

可以看到特征提取的信息是正确的,最终的识别结果也是正确的
双击样本示例:
7.png

上图是一个双击样本的示例图,可以看到由两个高电平组成,下图给出识别过程和结果:
8.png

可以看出特征提取信息正确,有两个上升沿和两个下降沿,然后最终的后验概率中也是双击的概率最大,并且超过阈值判定正确。
下面给出一些因为信号完整性缺失造成的误判示例。
边界双击情况示例:
9.png

上图中可以看出很明显是一个双击的动作,但是由于窗口长度固定的原因,导致一部分序列缺失,下图给出识别结果:
10.png

特征提取的信息倒是正确的,识别出下降沿只有1个,在计算似然过程中,相应位置的似然结果也反应了这一点,最终的后验表中可以看到前两个大的概率是单击和双击,但是都没超过阈值,所以判定为不确定
边界单击情况示例:
11.png

可以看出这个情况像是单击,但是实际上是一段长按序列,下图给出识别过程:
12.png

特征信息提取是正确的,然后似然结果都偏低,表示不偏向某一个模式,但在最终的后验结果中单击的后验概率异常的高,应该是在归一化过程中,单击概率占比比其他概率大很多导致的,这也是同样的问题,也就是信号完整性缺失导致了误判
总结

在这次实验中,基于朴素贝叶斯分类方法,通过滑动窗口采集数据、提取多维度特征、计算概率分布和应用贝叶斯更新,学到了不少,也融合了很多内容,算是一次不小的学习体验吧,虽然目前测试下来效果有限,还无法真正用在项目中,也总结了一些不足的地方。
比如信号完整性保证不了,不同特征属性对不同模式的权重实际并不一致等,这些都是需要解决的问题,虽然对现在的我来说很困难,但探索新方法的过程还是蛮喜欢的,也可能是对现有方法的审美疲劳导致的吧。
但有一说一,传统的方法,还是简单高效的,也不涉及到什么数学的内容,全凭逻辑加判断就可以搞定了,真是省时省力啊。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

2026-2-21 04:47:18

举报

您需要登录后才可以回帖 登录 | 立即注册