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

首发于21ic论坛

前言

之前学习了贝叶斯更新的相关内容,正好现在也在玩开发板,板子上面有几个小的单击按键,一般识别按键动作的做法就很简单,不是中断就是查询,基本都是靠边沿或者电平的状态来进行的,这一套就很无聊,没有实现的欲望,所以想用点不一样的方法。

这就有了本片文章的出现,基于朴素贝叶斯分类,使用滑动窗口捕捉电平序列,提取特征进行模式识别,理想情况下识别效果杠杠的,但是出现边界以及混合的情况,效果一言难尽,目前水平不够,这应该也是后续需要解决的主要问题了。

技术要点

核心原理

  1. 贝叶斯定理

本文实现的方法基于朴素贝叶斯分类器,主要就是两方面内容:贝叶斯定理条件独立假设,涉及的概念有先验概率后验概率条件概率,其中先验和条件概率都是提前准备好的,可以是主观经验的,也可以是统计量化的,而贝叶斯定理中的条件概率(不是后验概率),又称为似然概率。

这个方法的基本思想是:对于给定的待分类项(就是窗口中的电平序列),求解当这个待分类项出现时,各个已经定义过的模式类别出现的概率,哪个概率最大,那么这个待分类项就属于哪个模式。

在开始分类之前需要一些必要的准备工作:

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

这里的窗口是实时更新的窗口,老数据移出,新数据加入,滑动窗口确定电平序列数据的范围,只有处在窗口中的序列数据才会得到特征提取的机会,它的长度与序列的时间长度成比例,也就是说采样频率会影响到窗口时效性。

它需要考虑的问题是怎么捕捉到完整的信号,对应于滑动的步长,以及特征提取的周期。

基本步骤

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

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

具体实现

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

上面记录的数据可以作为一个样本,我通过观察和测量确定了几种模式,以及一些帮助识别的特征属性,在实验过程中使用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
  • 后验概率 :使用贝叶斯公式计算各模式的后验概率,先计算提取的特征在每个模式下的联合似然,基于条件独立假设,可以直接相乘,然后计算后验并归一化可得最终的概率表

代码实现

  1. 数据采集与预处理

把逻辑分析仪中的数据导出为csv文件,代码首先实现了 read_sigrok_csv_simple 函数,用于读取 sigrok CSV 格式的按键数据:

python 复制代码
def read_sigrok_csv_simple(filename):

    time_data = []
    signal_data = []

    with open(filename, 'r', newline='') as csvfile:

        reader = csv.reader(csvfile)

        for row in reader:

            # 跳过注释行和空行

            if not row or row[0].startswith(';'):

                continue

            # 确保行有两个列

            if len(row) >= 2:

                try:

                    time_val = float(row[0])

                    data_val = float(row[1])

                    time_data.append(time_val)

                    signal_data.append(data_val)

                except ValueError:

                    # 跳过无法转换为数字的行

                    continue

    return time_data, signal_data

该函数读取 CSV 文件中的时间戳和信号值,返回两个列表分别存储时间数据和信号数据,通过plot输出采样的数据图如下所示:

  1. 识别器类设计

核心实现是 BayesianButtonRecognizer 类,用于实现基于贝叶斯分类的按键模式识别:

python 复制代码
class BayesianButtonRecognizer:

    """基于滑动窗口和贝叶斯更新的按键模式识别器"""

    def __init__(self, window_size=20, sample_interval=0.01, 

    threshold=0.7):

        """

        初始化识别器

        Args:

            window_size: 滑动窗口大小

            sample_interval: 采样间隔(秒)

            threshold: 判定阈值

        """

        self.window_size = window_size

        self.sample_interval = sample_interval

        self.threshold = threshold

        # 滑动窗口存储最近的观测序列

        self.window = deque(maxlen=window_size)

        # 模式类别

        self.modes = ['无效', '单击', '双击', '长按']

        # 先验概率 - 初始等可能

        self.prior = np.array([0.25, 0.25, 0.25, 0.25])

        # 特征提取相关的参数(单位:采样点数)

        self.short_press_max = 15  # 短按最大持续时间

        self.long_press_min = 30    # 长按最小持续时间  

        self.double_click_interval = 10  # 双击间隔阈值

        # 初始化特征分布参数(基于物理理解预设)

        self._init_feature_distributions()

        # 特征权重

        self.featwight={

            "无效":np.array([1.2,0.8,0.8,1.2]),

            "单击":np.array([1,1.2,1.2,1]),

            "双击":np.array([1,1.2,1.2,0.8]),

            "长按":np.array([1.2,0.8,0.8,1.2])

        }
  1. 特征分布初始化

识别器初始化时设置了各模式下特征的概率分布参数:

python 复制代码
def _init_feature_distributions(self):

    """初始化各模式下特征的概率分布参数"""

    # 高电平占比的分布参数

    self.high_ratio_params = {

        '无效': 0.05,   # 无效时高电平占比很低

        '单击': 0.2,    # 单击时有短暂高电平

        '双击': 0.3,    # 双击时高电平占比稍高

        '长按': 0.9     # 长按时高电平占比很高

    }

    # 上升沿数量的分布参数

    self.rise_count_params = {

        '无效': 0.1,    # 无效时几乎无上升沿

        '单击': 1,    # 单击时有1个上升沿

        '双击': 2,    # 双击时有2个上升沿  

        '长按': 0.7     # 长按有1个上升沿

    }

    # 最长高电平持续时间的分布参数(正态分布:均值,标准差)

    self.max_duration_params = {

        '无效': (0, 2),     # 无效时持续时间很短

        '单击': (0.2, 5),     # 单击中等持续时间

        '双击': (0.17, 3),     # 双击每次按下时间短

        '长按': (0.9, 10)    # 长按持续时间长

    }
  1. 特征提取

从滑动窗口数据中提取特征,其中高电平占比是通过求序列平均值来获得的,然后边沿计数对应了记录序列跳变数量,最长高电平时间通过记录连续高电平时长获取:

python 复制代码
def extract_features(self, window_data):

    """从滑动窗口数据中提取特征"""

    if len(window_data) == 0:

        return None

    data = np.array(window_data)

    # 特征1: 高电平占比

    high_ratio = np.mean(data)

    # 特征2: 上升沿数量(0->1的变化)

    rises = 0

    for i in range(1, len(data)):

        if data[i-1] == 0 and data[i] == 1:

            rises += 1

    # 特征3: 下降沿数量(1->0的变化)

    falls = 0

    for i in range(1, len(data)):

        if data[i-1] == 1 and data[i] == 0:

            falls += 1

    # 特征4: 最长连续高电平持续时间

    max_duration = 0

    current_duration = 0

    for val in data:

        if val == 1:

            current_duration += 1

            max_duration = max(max_duration, current_duration)

        else:

            current_duration = 0

    return {

        'high_ratio': high_ratio,

        'rise_count': rises, 

        'fall_count': falls,

        'max_duration': max_duration

    }
  1. 似然概率计算

计算给定模式下观测到特征值的似然概率,即条件概率,通过上面定义的分布参数,使用正态分布近似,在python中通过stats.norm.pdf求特征对应每个模式的似然程度,然后基于条件独立的假设,求解联合似然,表示样本对某一模式的最终似然结果:

python 复制代码
def calculate_likelihood(self, features, mode):

    """计算给定模式下观测到特征值的似然概率"""

    if features is None:

        return 1.0  # 无特征时返回中性似然

    # 使用概率密度函数计算各特征的似然

    likelihoods = []

    # 1. 高电平占比的似然

    target_ratio = self.high_ratio_params[mode]

    # 使用正态分布近似, 标准差根据经验设定

    like_ratio = stats.norm.pdf(features['high_ratio'], 

                              target_ratio, 0.2)

    likelihoods.append(like_ratio + 1e-10)  # 避免零

    # 2. 上升沿数量的似然

    target_rises = self.rise_count_params[mode]

    like_rises = stats.norm.pdf(features['rise_count'], 

    target_rises,0.3)

    likelihoods.append(like_rises + 1e-10)

    # 3. 下降沿(同上升沿)数量的似然

    target_falls = self.rise_count_params[mode]

    like_falls = stats.norm.pdf(features['fall_count'], 

    target_falls,0.3)

    likelihoods.append(like_falls + 1e-10)

    # 4. 最长持续时间的似然(使用正态分布)

    target_dur, std_dur = self.max_duration_params[mode]

    target_dur *= self.window_size

    like_duration = stats.norm.pdf(features['max_duration'], 

                                 target_dur, std_dur)

    likelihoods.append(like_duration + 1e-10)

    # 组合各特征的似然(假设特征条件独立)

    total_likelihood = np.prod(np.array(likelihoods))

    print("特征在mode[%s]的似然:"%{mode},likelihoods,"最终联合似然:%.

    3f"%total_likelihood)

    return total_likelihood
  1. 滑动窗口更新
python 复制代码
def slide_window(self,io_state):

    # 移除最旧的值

    self.window.popleft()

    # 将新观测值加入滑动窗口

    self.window.append(io_state)
  1. 信念更新与模式判断

计算完样本对每个模式的似然后,就于先验概率相乘,就得到了后验概率,然后归一化得到最终结果,同时使用阈值判定机制,当最大后验超过判定阈值后,才会识别具体模式,否则就是不确定

python 复制代码
def update_belief(self, io_state):

    """根据新观测值更新信念"""

    # 提取当前窗口的特征

    features = self.extract_features(self.window)

    print("特征提取:",features)

    # 计算各模式的似然

    likelihoods = np.array([self.calculate_likelihood(features, 

    mode) 

                          for mode in self.modes])

    # 贝叶斯更新: 后验 ∝ 似然 × 先验

    unnormalized_posterior = likelihoods * self.prior

    evidence = np.sum(unnormalized_posterior)

    if evidence > 0:

        posterior = unnormalized_posterior / evidence

    else:

        posterior = self.prior.copy()

    # 更新先验(用于下一次迭代)

    # self.prior = posterior

    # 判断当前模式

    best_mode_idx = np.argmax(posterior)

    best_prob = posterior[best_mode_idx]

    print("后验:",posterior)

    if best_prob > self.threshold:

        detected_mode = self.modes[best_mode_idx]

    else:

        detected_mode = '不确定'

    return detected_mode, posterior
  1. 主函数与演示

因为定义了高电平为有效电平,但实际中低电平,或者说下降沿是按键动作的反应,所以处理数据序列时做了相应的取反处理。

python 复制代码
if __name__ == "__main__":

    DeltaT = 0.01 # 采样间隔

    UnitTime = 1e-06 # 原始数据点的时基

    SampleInterval = math.floor(DeltaT / UnitTime)

    filename = "key_data_20s_all.csv"  # 逻辑分析仪导出的数据

    recognizer = BayesianButtonRecognizer(window_size=100, 

    threshold=0.8)

    recognizer.reset()

    time_data, signal_data = read_sigrok_csv_simple(filename)

    print(f"成功读取数据,共 {len(time_data)} 个数据点")

    print(f"时间范围: {time_data[0]}s 到 {time_data[-1]}s")

    plt.figure(1)

    sample_data = []

    res_data = []

    sample_num = math.floor(len(signal_data) / SampleInterval)

    print("sample size is:",sample_num)

    for i in range(sample_num-1):

        sample_data.append(int(not signal_data[SampleInterval*i]))

        recognizer.slide_window(int(not signal_data

        [SampleInterval*i]))

        if i%recognizer.window_size==0:

            res,postrior=recognizer.update_belief(i)

            if(res not in["不确定","无效"]):

                res_data.append(res)

            print("win[%d]:"%i,res)

            plt.plot(recognizer.window)

            plt.show()

    plt.figure(1)

    plt.plot(sample_data)

    plt.show()

    print(res_data)

当窗口中样本序列是理想情况时,识别效果相当好:

无效样本示例:

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

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

单击样本示例:

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

可以看到特征提取的信息是正确的,最终的识别结果也是正确的

双击样本示例:

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

可以看出特征提取信息正确,有两个上升沿和两个下降沿,然后最终的后验概率中也是双击的概率最大,并且超过阈值判定正确。

下面给出一些因为信号完整性缺失造成的误判示例。

边界双击情况示例:

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

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

边界单击情况示例:

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

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

总结

在这次实验中,基于朴素贝叶斯分类方法,通过滑动窗口采集数据、提取多维度特征、计算概率分布和应用贝叶斯更新,学到了不少,也融合了很多内容,算是一次不小的学习体验吧,虽然目前测试下来效果有限,还无法真正用在项目中,也总结了一些不足的地方。

比如信号完整性保证不了,不同特征属性对不同模式的权重实际并不一致等,这些都是需要解决的问题,虽然对现在的我来说很困难,但探索新方法的过程还是蛮喜欢的,也可能是对现有方法的审美疲劳导致的吧。

但有一说一,传统的方法,还是简单高效的,也不涉及到什么数学的内容,全凭逻辑加判断就可以搞定了,真是省时省力啊。

相关推荐
小李独爱秋21 小时前
机器学习与深度学习实验项目3 卷积神经网络实现图片分类
人工智能·深度学习·机器学习·分类·cnn·mindspore·模式识别
良许Linux2 天前
FPGA原理和应用
stm32·单片机·fpga开发·程序员·嵌入式·编程
pie_thn5 天前
小容量32单片机也上bootloader?拆机烧录的苦谁懂,能上抓紧上
嵌入式·编程
程序员鱼皮6 天前
40 个 Agent Skills 精选资源:入门教程 + 实用工具 + 必装推荐
前端·后端·计算机·ai·程序员·互联网·编程
良许Linux6 天前
嵌入式处理器架构
stm32·单片机·程序员·嵌入式·编程
小李独爱秋7 天前
机器学习与深度学习实验项目2 数据降维
人工智能·python·深度学习·学习·机器学习·模式识别
爱思德学术7 天前
中国计算机学会(CCF)推荐学术会议-B(软件工程/系统软件/程序设计语言):ICFP 2026
编程
爱思德学术12 天前
中国计算机学会(CCF)推荐学术会议-B(软件工程/系统软件/程序设计语言):ECOOP 2026
编程·编程语言
良许Linux13 天前
51单片机都有哪些优缺点
单片机·程序员·嵌入式·编程