1. 项目概述:从一道赛题看人类活动识别的核心挑战

看到“人类活动分类”这个题目,很多初次接触数学建模的同学可能会觉得,这不就是个简单的模式识别问题吗?给点传感器数据,套个机器学习模型,分个类就完事了。但如果你真这么想,那可能就错过了这道题背后90%的深度和价值。2022年小美赛C题,恰恰是把一个看似成熟的“分类”问题,放到了一个充满现实世界“噪音”和约束的复杂场景中,来考察参赛者系统性的问题拆解、建模与求解能力。

这道题的核心,远不止是调包调用一个分类算法。它模拟的是诸如智能手表、健康监测设备、安防系统等真实应用中,如何利用有限的、可能带有噪声的传感器数据(如加速度计、陀螺仪),去准确推断佩戴者正在进行的活动(如行走、跑步、上楼梯、坐下等)。这里面埋着好几个“坑”:数据可能是非平稳的、不同人行为模式差异巨大、传感器佩戴位置不固定、还有那个最经典的“类间不平衡”问题——你一天中坐着的时间远多于跑楼梯的时间。组委会不会给你一个干干净净的、特征工程都做好的数据集,他们给的往往是原始的、需要你自己去清洗、理解和构造的时序信号。

所以,面对这道题,你的思路如果直接跳到“我用SVM还是用神经网络”,那方向就偏了。正确的打开方式是先成为一个“数据侦探”和“场景架构师”。你需要思考:给定的数据是什么格式?采样频率如何?有哪些潜在的噪声源?不同活动在传感器信号上最本质的差异体现在哪里(是幅度、频率还是序列模式)?如何从原始数据中提取出对分类最有效的特征?在计算资源或实时性要求有限的情况下,如何设计一个既准确又高效的模型?这才是数学建模竞赛考察的精髓——将模糊的实际问题,转化为清晰的数学与计算问题,并给出一个平衡了性能、复杂度和可解释性的解决方案。

接下来,我将以这道题为蓝本,拆解一个完整的人类活动识别建模流程。我会重点分享那些在优秀论文里可能一笔带过,但在实战中却能决定成败的细节、抉择与技巧。无论你是为了备战未来的数模竞赛,还是真正想入门传感器数据分析,这些经验都能让你少走很多弯路。

2. 解题核心思路与整体框架设计

接到题目后,切忌一头扎进代码里。花至少30%的时间来设计整体框架,是最高效的投资。对于人类活动分类(HAR),一个稳健的建模流程通常遵循“数据理解 → 预处理 → 特征工程 → 模型选择与训练 → 后处理与评估”的路径,但每一步都有其独特的考量和技巧。

2.1 问题定义与数据勘探

首先,必须明确任务边界。小美赛C题通常会提供来自智能手机或穿戴设备的三轴加速度计和陀螺仪数据,以及对应的活动标签。你的第一个任务就是彻底“读懂”这些数据。

  1. 数据格式解析 :数据通常是CSV或TXT文件。每一行代表一个时间戳下的采样点,列可能包括:时间戳、acc_x, acc_y, acc_z(加速度)、gyro_x, gyro_y, gyro_z(角速度)、label(活动类型,如1-行走,2-跑步等)。首先用Pandas读入,查看数据形状、统计摘要和缺失值情况。

    import pandas as pd
    import numpy as np
    
    data = pd.read_csv('activity_data.csv')
    print(f"数据形状: {data.shape}")
    print(data.head())
    print(data.describe())
    print(data.isnull().sum())
    

    这一步能立刻告诉你数据量大小、是否有异常值(比如加速度值超出了重力加速度的合理范围)、以及标签的分布是否均衡。

  2. 信号可视化 :这是 至关重要 的一步。别嫌麻烦,务必把每个传感器轴、每种活动类型的原始信号片段画出来看看。

    import matplotlib.pyplot as plt
    
    # 选取‘Walking’和‘Running’各一段数据
    walk_data = data[data['label'] == 'Walking'].iloc[1000:2000]
    run_data = data[data['label'] == 'Running'].iloc[1000:2000]
    
    fig, axes = plt.subplots(3, 2, figsize=(15, 10))
    axes[0, 0].plot(walk_data['acc_x'].values)
    axes[0, 0].set_title('Walking - Acc X')
    axes[0, 1].plot(run_data['acc_x'].values)
    axes[0, 1].set_title('Running - Acc X')
    # ... 绘制其他轴
    plt.tight_layout()
    plt.show()
    

    通过可视化,你可以直观感受不同活动的信号差异:跑步的加速度振幅更大、频率更高;静止时信号平稳且围绕重力加速度分量波动;上下楼梯可能呈现出周期性但不完全对称的模式。这种直觉对后续的特征设计和模型选择有巨大帮助。

  3. 活动标签分布分析 :计算每个活动类别的样本数量。如果发现“坐着”的样本数是“上楼梯”的20倍,那么你就遇到了严重的类不平衡问题。在评估模型时,仅看总体准确率(Accuracy)会严重失真,必须引入精确率(Precision)、召回率(Recall)、F1-score以及宏平均(Macro-average)等指标。

2.2 核心建模策略选择:基于传统机器学习还是深度学习?

这是路线选择的分水岭,也直接决定了你后续绝大部分工作的内容。

  • 传统机器学习流水线(推荐给初赛或追求高可解释性)

    • 思路 :将连续的时序信号,通过滑动窗口分割成固定长度的小片段(如2秒,对应100个采样点,假设采样率50Hz)。对每个窗口内的原始信号,计算一系列手工设计的特征(统计特征、频域特征等)。这样,每个窗口就从一串时间序列变成了一个特征向量,然后就可以用SVM、随机森林、XGBoost等分类器进行训练。
    • 优点 :流程清晰,可解释性强,计算量相对较小,对数据量要求不高。非常适合在竞赛中快速构建一个强基线模型,并且便于你分析哪些特征对分类贡献最大。
    • 缺点 :特征工程依赖领域知识和大量实验,可能无法捕捉非常复杂的时序动态关系。
  • 深度学习端到端模型(适合数据量充足、追求极致性能)

    • 思路 :同样使用滑动窗口分割数据,但直接将窗口内的原始多轴传感器数据(可以视为一个 [窗口长度, 传感器通道数] 的矩阵)输入神经网络。常用模型包括一维卷积神经网络(1D CNN)、长短时记忆网络(LSTM)或两者的结合(CNN-LSTM)。
    • 优点 :能自动学习从原始信号到活动标签的复杂映射,省去了繁琐且需要专业知识的手工特征工程,在足够数据下通常能获得更高的性能。
    • 缺点 :需要更多的数据、更长的训练时间,模型像黑盒,可解释性差,且容易过拟合。

我的实战心得 :在数模竞赛的有限时间内,我强烈建议采用 “传统特征工程 + 集成学习模型” 作为主力方案。原因有三:第一,它稳定可控,不容易因为调参不当而崩盘;第二,它的结果易于分析和呈现,你在论文里可以详细展示特征重要性,这比展示一个神经网络的混淆矩阵更有“建模感”;第三,计算快,留出更多时间进行迭代优化和撰写论文。你可以把深度学习模型作为一个对比实验或性能上限的探索,但不要把所有赌注都押在上面。

3. 从原始信号到特征向量:数据预处理与特征工程详解

这是整个流程中技术含量最高、也最体现功底的部分。一个优秀的特征集,能让一个简单的随机森林模型打败一个未经精心调优的复杂神经网络。

3.1 数据预处理:为特征提取铺平道路

  1. 噪声过滤 :传感器原始信号包含高频噪声。一个简单有效的办法是使用低通滤波器,比如巴特沃斯滤波器,滤除高于人体活动频率(通常认为在10-20Hz以上)的噪声。

    from scipy import signal
    
    def butter_lowpass_filter(data, cutoff_freq, fs, order=4):
        nyquist = 0.5 * fs
        normal_cutoff = cutoff_freq / nyquist
        b, a = signal.butter(order, normal_cutoff, btype='low', analog=False)
        y = signal.filtfilt(b, a, data) # 使用filtfilt实现零相位滤波
        return y
    
    fs = 50 # 采样频率,根据题目数据设定
    cutoff = 10 # 截止频率10Hz
    data['acc_x_filtered'] = butter_lowpass_filter(data['acc_x'], cutoff, fs)
    

    注意 filtfilt 函数进行前向-后向滤波,避免了相位失真,比普通的 lfilter 更适用于此类分析。

  2. 重力分量分离 :三轴加速度计数据包含两个部分:设备相对于地球的静态重力加速度,和由人体运动产生的动态线性加速度。对于许多活动识别任务,我们更关心动态部分。可以通过高通滤波器(如截止频率0.3Hz)粗略分离,或使用更复杂的传感器融合算法(但这在竞赛中可能过于复杂)。

  3. 滑动窗口分割 :这是将时序问题转化为样本问题的关键。窗口长度(如2秒)和重叠率(如50%)是需要调优的超参数。

    def create_segments(data, window_size, step_size):
        segments = []
        labels = []
        for start in range(0, len(data) - window_size, step_size):
            end = start + window_size
            segment = data.iloc[start:end] # 包含所有传感器轴
            label = stats.mode(data['label'].iloc[start:end])[0][0] # 取窗口内众数作为标签
            segments.append(segment[['acc_x','acc_y','acc_z','gyro_x','gyro_y','gyro_z']].values)
            labels.append(label)
        return np.array(segments), np.array(labels)
    
    window_size = 100 # 2秒 * 50Hz
    step_size = 50 # 50%重叠
    X, y = create_segments(filtered_data, window_size, step_size)
    

    关键点 :窗口内的标签通常取众数(mode),前提是窗口时间远小于一次活动的持续时间。重叠是为了增加样本量,防止在窗口边界处切碎一个活动。

3.2 特征工程:构建模型的“语言”

对每个窗口数据,计算以下特征。这些特征构成了分类器认识不同活动的“词汇表”。

  1. 时域统计特征 (最基础且有效):

    • 均值 :反映信号的直流偏移或平均加速度。
    • 标准差/方差 :反映信号的波动强度,跑步的标准差远大于静坐。
    • 最大值、最小值、峰峰值
    • 偏度、峰度 :描述信号分布的形状,有助于识别特殊的运动模式。
    • 四分位距、绝对中位差 :对异常值更鲁棒的离散度度量。
    • 过零率 :信号穿过零点的频率,简单反映频率信息。
    • 信号幅度面积(SMA) :各轴绝对值之和的均值,是活动强度的综合指标。
  2. 频域特征 (揭示周期性):

    • 对信号做快速傅里叶变换(FFT),从频谱中提取:
    • 频谱峰值 频谱重心 频谱方差
    • 能量 :在特定频带(如0.1-5Hz)内的积分。
    from scipy.fft import fft
    
    def extract_freq_features(signal, fs):
        n = len(signal)
        fft_vals = np.abs(fft(signal))
        fft_freq = np.fft.fftfreq(n, 1.0/fs)
        # 只取正频率部分
        positive_freq_mask = fft_freq > 0
        fft_vals = fft_vals[positive_freq_mask]
        fft_freq = fft_freq[positive_freq_mask]
    
        spectral_centroid = np.sum(fft_freq * fft_vals) / np.sum(fft_vals)
        spectral_energy = np.sum(fft_vals**2)
        # ... 计算其他特征
        return spectral_centroid, spectral_energy
    
  3. 时频域特征 (如小波变换特征):能同时捕捉时间和频率信息,更强大但也更复杂,在竞赛中如果时间充裕可以尝试。

  4. 多轴合成特征

    • 合加速度 acc_magnitude = np.sqrt(acc_x^2 + acc_y^2 + acc_z^2) 。这是一个极其强大的特征,因为它消除了设备方向的影响,直接反映运动的总体强度。
    • 合角速度 :同理计算陀螺仪信号的模。
    • 对合成信号再计算上述的时域和频域特征。

特征工程避坑指南

  1. 不要盲目堆砌特征 :计算所有轴、所有类型的特征,很容易产生上千维的特征向量,导致“维数灾难”和过拟合。应该先广泛计算,然后 必须进行特征选择 。可以使用随机森林的 feature_importances_ 属性,或者基于统计检验(如卡方检验、互信息)来选择最重要的前50-100个特征。
  2. 一定要做特征标准化 :将特征缩放到均值为0,方差为1。这对基于距离的模型(如SVM)和依赖梯度下降的模型至关重要。使用 sklearn.preprocessing.StandardScaler 切记 :用训练集拟合scaler,然后同时转换训练集和测试集,避免数据泄露。
  3. 为不同传感器分别设计特征 :加速度计和陀螺仪提供互补信息。前者感知线性运动,后者感知旋转。分别对它们提取特征,能提供更丰富的视图。

4. 模型构建、训练与优化实战

特征准备好后,我们进入模型环节。这里以随机森林为例,因为它对特征量纲不敏感、能处理非线性关系、且能给出特征重要性,非常契合竞赛需求。

4.1 基于随机森林的建模流程

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
from sklearn.preprocessing import LabelEncoder, StandardScaler

# 1. 编码标签
le = LabelEncoder()
y_encoded = le.fit_transform(y)

# 2. 划分训练集和测试集(如果题目未提供独立测试集)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_encoded, test_size=0.2, random_state=42, stratify=y_encoded)

# 3. 特征标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # 注意:只用训练集的参数转换测试集

# 4. 初始化随机森林模型
rf_clf = RandomForestClassifier(n_estimators=200, random_state=42, n_jobs=-1, class_weight='balanced')

# 5. 使用交叉验证评估
cv_scores = cross_val_score(rf_clf, X_train_scaled, y_train, cv=5, scoring='f1_macro')
print(f"交叉验证F1宏平均分数: {cv_scores.mean():.4f} (+/- {cv_scores.std()*2:.4f})")

# 6. 在训练集上训练最终模型
rf_clf.fit(X_train_scaled, y_train)

# 7. 在测试集上预测并评估
y_pred = rf_clf.predict(X_test_scaled)
print("测试集分类报告:")
print(classification_report(y_test, y_pred, target_names=le.classes_))
print("混淆矩阵:")
print(confusion_matrix(y_test, y_pred))

4.2 超参数调优:让模型性能再上一个台阶

使用网格搜索(GridSearchCV)或随机搜索(RandomizedSearchCV)来优化关键参数。对于随机森林,主要调:

  • n_estimators : 树的数量,越多越好,但计算成本增加。通常100-500。
  • max_depth : 树的最大深度,控制模型复杂度,防止过拟合。
  • min_samples_split : 内部节点再划分所需最小样本数。
  • min_samples_leaf : 叶子节点最少样本数。
  • max_features : 寻找最佳分割时考虑的特征数。
param_grid = {
    'n_estimators': [100, 200, 300],
    'max_depth': [10, 20, 30, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'max_features': ['sqrt', 'log2']
}

grid_search = GridSearchCV(RandomForestClassifier(random_state=42, class_weight='balanced'),
                           param_grid, cv=5, scoring='f1_macro', n_jobs=-1, verbose=1)
grid_search.fit(X_train_scaled, y_train)

print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳交叉验证分数: {grid_search.best_score_:.4f}")

best_rf = grid_search.best_estimator_

重要提示 :在竞赛中,如果时间紧张,可以优先调整 n_estimators max_depth ,这两个参数对性能影响最大。同时,设置 class_weight='balanced' 或根据各类别样本数手动设置权重,是处理类不平衡问题的简单有效方法。

4.3 模型融合与集成策略

单一模型可能遇到瓶颈。可以考虑集成学习:

  • 投票法 :训练随机森林、XGBoost、SVM等多个不同类型的模型,让它们对同一个样本投票。
  • 堆叠法 :将多个基学习器的预测结果作为新的特征,再训练一个元学习器(如逻辑回归)。这种方法潜力更大,但更容易过拟合,需要更多的数据和谨慎的交叉验证。

5. 结果分析、可视化与论文呈现要点

模型跑出结果不是终点,如何分析和呈现它,决定了你论文的深度和说服力。

  1. 超越准确率:多维度评估

    • 务必展示 混淆矩阵 。它能清晰揭示模型具体在哪些活动上容易混淆(例如,把“上楼梯”误判为“下楼梯”)。
    • 对于每一类活动,汇报 精确率、召回率、F1-score 。对于不平衡数据, 宏平均F1 比总体准确率更有参考价值。
    • 计算 Cohen‘s Kappa系数 ,它考虑了随机分类的预期一致性,对于类别不平衡的评价更公平。
  2. 特征重要性分析

    importances = best_rf.feature_importances_
    indices = np.argsort(importances)[::-1]
    feature_names = your_feature_name_list # 你之前提取的特征名称列表
    
    plt.figure(figsize=(10,6))
    plt.title("Top 20 Feature Importances")
    plt.bar(range(20), importances[indices[:20]])
    plt.xticks(range(20), [feature_names[i] for i in indices[:20]], rotation=90)
    plt.tight_layout()
    plt.show()
    

    在论文中分析哪些特征最重要。例如,如果“合加速度的标准差”和“频谱重心”排名靠前,你就可以论证: 活动的强度(幅度)和节奏(频率)是区分不同人类行为的关键物理维度 。这极大地提升了论文的洞察力。

  3. 错误案例分析 : 从测试集中找出被错误分类的样本窗口。回溯查看它的原始信号,思考为什么模型会分错。是因为这个“行走”片段恰好有跳跃?还是信号噪声太大?或者是这个活动本身处于两种状态的过渡期?这种分析能体现你对问题和模型的深刻理解,是论文的加分项。

  4. 模型对比实验 : 在论文中设计一个对比实验章节。展示你尝试过的不同模型(如逻辑回归、SVM、随机森林、简单的1D CNN)在相同预处理和特征下的性能对比表格。用数据说明为什么你最终选择了随机森林(例如,它在保持高F1分数的同时,训练速度最快,最适合本题数据规模)。

6. 竞赛实战中的高频问题与解决方案

在紧张的竞赛时间里,你一定会遇到各种突发问题。这里记录几个我踩过的坑和解决方案。

问题1:数据预处理耗时太长,影响迭代速度。

  • 解决方案 :将特征提取函数向量化,避免在Python层使用 for 循环。使用 NumPy SciPy 的向量操作。对于滑动窗口,可以尝试使用 sklearn TimeSeriesSplit tsfresh 库(功能强大但较重)。更关键的是, 先在小样本(10%)上快速验证流程和想法 ,整个流程跑通且效果合理后,再放到全量数据上运行。

问题2:特征维度爆炸,模型训练慢且过拟合。

  • 解决方案 :严格执行特征选择。除了基于模型的重要性排序,也可以先用方差阈值( VarianceThreshold )过滤掉方差极小的特征(几乎为常数)。然后使用递归特征消除(RFE)或SelectFromModel进行筛选。将特征维度控制在100-200以内通常是个安全范围。

问题3:某些类别(如“跳跃”)的召回率极低。

  • 解决方案 :这是典型的小样本问题。除了设置 class_weight ,还可以尝试:
    • 数据增强 :对少数类的传感器信号片段进行小幅度的缩放、添加高斯噪声、时间轴上的微小扭曲,来人工生成更多样本。
    • 重采样 :对少数类进行过采样(如SMOTE算法),但注意SMOTE对时序数据需谨慎使用,可能破坏信号的时序结构。更安全的方法是简单复制少数类样本。
    • 改变评估策略 :在交叉验证中采用分层抽样( StratifiedKFold ),确保每一折的类别分布与整体一致。

问题4:模型在本地交叉验证表现很好,但总感觉不踏实。

  • 解决方案 :模拟现实中的 受试者独立 验证。如果数据包含不同人的ID,最严谨的做法是 按人划分训练集和测试集 (例如,前8个人的数据训练,后2个人的数据测试)。这能更好地检验模型的泛化能力,防止模型只是记住了特定人的行为模式。如果题目未提供ID,也要在论文中讨论这种潜在风险。

问题5:如何让论文的“模型”部分更出彩?

  • 不要只贴代码和结果 :用流程图清晰地展示你的整体建模框架(数据流图)。用示意图说明你的滑动窗口和重叠机制。用图表展示特征重要性。用混淆矩阵的热力图展示错误模式。
  • 讨论模型的局限性 :主动指出你的模型在哪些场景下可能失效(例如,设备佩戴位置变化、从未见过的新活动、信号严重丢失等),并简要讨论可能的改进方向(如引入注意力机制、使用迁移学习)。这体现了批判性思维,是高水平论文的标志。

人类活动分类是一个连接信号处理、机器学习和实际应用的经典问题。通过2022年小美赛C题这样一个完整的项目实践,你真正掌握的不仅仅是如何调用几个Sklearn函数,而是如何系统地思考一个数据科学问题:从数据的初步窥探,到特征的精心雕琢,再到模型的选择与权衡,最后对结果进行冷静的批判性分析。这个过程里,对物理世界的直觉(什么样的运动产生什么样的信号)和对数学工具的熟练运用同等重要。最后,在竞赛中,一个清晰、完整、有深度的故事线,往往比一个精度高0.5%但黑盒般的复杂模型,更能打动评委。毕竟,数学建模的核心是“建模”,是用数学语言清晰地描述和解决一个实际问题,而不仅仅是跑出一个漂亮的数字。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐