项目_华为杯’数模研赛复盘_第二问

目录


前言

碎碎念:遇事不决先问春风,春风不语。


============================================================================

第二问要解决的"问题"是什么?

-----------------------------------------------------------------------------

第一问把每段振动信号提成了一条 20 维的结构化特征向量(bearing_features.csv)。

第二问的任务,本质上就是:用这些特征训练一个"源域监督分类器",并给出可复现的

训练-验证-对比-可视化流程,回答"这些手工特征是否足以区分四种状态(Normal/IR/OR/B)"。

注意:第二问不涉及目标域 A-P.mat 的预测,它是一个"在源域上做诊断建模"的参考组,

其价值在于:

- 作为后续迁移学习(第三问 DANN)的对照组:不迁移时在源域能做到什么水平?

- 检查第一问特征质量:若源域上也分不开,后续迁移更难成立。

=============================================================================

第二问的价值

它的价值并不在于"最终一定要选出某个最强模型",而在于把两个关键问题厘清楚:其一,这20维手工特征是否真的"有区分度",能否在源域上把四类状态(Normal/Inner/Outer/Ball)分开;其二,如果在源域上能分开,传统模型的上限大致在哪里,为第三问的迁移学习提供一个可以对照的基准。

输入

整个第二问的输入是第一问输出的 `bearing_features.csv`。这张表的列结构在代码中被显式利用:`Filename`是样本来源路径,`Label`是监督学习标签,`RPM`是转速元信息,其余列都是特征(例如 `RMS`、`Kurtosis`、`BPFO_Amplitude`、`WPT_E1`等)。

数据预处理

解决样本泄露

第二问的数据预处理做得很"标准",但每一步都有它必须存在的原因。代码首先读取CSV并打印标签分布,为了在建模前确认类别是否严重不均衡;如果某一类样本过少,那么简单的准确率会被大类"抬高",而模型对小类的召回会很差。紧接着,代码用列表推导`['Filename','Label','RPM']`排除掉得到特征列,这里隐含了一个重要判断:把 `Filename`喂给模型会直接引入"样本ID泄漏"(模型可能学到目录名规律而不是故障机理);而 `RPM`虽然看似有用,但在本项目里RPM有一部分是"从文件名解析/默认值补全"的工程性字段,它可能带来两种风险------一是数据质量不稳定(某些样本RPM并不可靠),二是模型可能过度依赖转速差异而忽略真正的故障模式。本文在第二问阶段选择不把RPM当特征,是一种偏保守但更利于可解释性的做法:先证明"只靠信号特征也能分类",再在后续扩展实验里讨论"加上RPM是否提升/是否引入偏差"会更清晰。

数据集划分

完成特征列选择后,代码将文本标签用 `LabelEncoder`编码为0..K-1的整数,这是传统sklearn训练器普遍要求的格式。更关键的一步是训练测试划分:`train_test_split(..., stratify=y_encoded)`。这一步相当于在划分时强制保持各类比例一致,避免出现某一类在测试集中几乎没有样本的情况。对于故障诊断这种多分类问题,分层抽样能显著降低评估方差,使得后续的分类报告(precision/recall/F1)更可信。这里还固定了 `random_state=42`,这保证了复现性:同一份数据、同一份代码,不会因为随机划分不同而得到完全不同的结果;在竞赛与论文场景里,复现性本身就是"可信度"的一部分。

标准化

接下来是标准化(`StandardScaler`)这一步。第二问选择的模型中,KNN和SVM本质上都依赖"距离/间隔"概念:KNN用欧氏距离或其他距离度量找最近邻,SVM(特别是RBF核)对特征尺度很敏感。如果不标准化,像 `WPT_E*`这种能量特征可能数值范围极大,而 `Skewness`、`Kurtosis`这种统计量范围相对小,距离计算会被大尺度特征完全主导,模型会在一种"虚假的几何空间"里做分类,最终表现不稳定。代码里标准化的方式也遵循了避免信息泄漏的原则:只在训练集上 `fit`,再对训练集和测试集分别 `transform`。这点在写复盘时值得强调:如果在全数据上fit scaler,再切分训练测试,会让测试集的信息(均值方差)提前进入训练流程,从而高估模型泛化能力。

python 复制代码
# ---------------------------- 1) 数据加载与预处理 ----------------------------
    def load_and_prepare_data(self):
        """
        加载 bearing_features.csv,并完成:
        - 选择特征列(排除 Filename/Label/RPM)
        - 标签编码
        - stratify 分层划分训练集/测试集
        - 用训练集拟合 StandardScaler,再变换 train/test
        """
        print("=" * 60)
        print("第一步: 数据加载与预处理")
        print("=" * 60)

        try:
            self.data = pd.read_csv(self.csv_file_path)
            print(f"✓ 成功加载数据文件: {self.csv_file_path}")
            print(f"✓ 数据形状: {self.data.shape}")
            print(f"✓ 标签分布:\n{self.data['Label'].value_counts()}")
        except FileNotFoundError:
            print(f"错误: 找不到文件 {self.csv_file_path}")
            return False
        except Exception as e:
            print(f"数据加载错误: {e}")
            return False

        # 特征列:把文件名、标签、转速这些"元数据"排除掉
        # - Filename:只是索引,不该喂给模型
        # - Label:监督学习的 y
        # - RPM:在第一问里是从文件/文件名取的转速;这里作者选择不使用它做特征
        #   (也可以把 RPM 加进去做一个对照实验:看是否提升/是否引入泄漏)
        self.feature_columns = [
            col for col in self.data.columns if col not in ["Filename", "Label", "RPM"]
        ]
        X = self.data[self.feature_columns]
        y = self.data["Label"]

        # 标签编码:例如 Normal->2,Inner_Ring_Fault->1(顺序由 LabelEncoder 规则决定)
        y_encoded = self.label_encoder.fit_transform(y)
        self.label_names = self.label_encoder.classes_

        # 分层划分:保证 train/test 中各类比例基本一致
        # 对竞赛/论文来说,这一步非常关键:否则小类可能在测试集中样本过少,评估方差会很大
        self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(
            X, y_encoded, test_size=0.2, random_state=42, stratify=y_encoded
        )

        # 标准化:只用训练集拟合参数,避免信息泄漏;再用同样参数变换测试集
        self.X_train_scaled = self.scaler.fit_transform(self.X_train)
        self.X_test_scaled = self.scaler.transform(self.X_test)

        print("✓ 数据预处理阶段完成!\n")
        return True

模型的选择和模型背后的意义

模型选择是第二问的核心设计之一。代码定义了四个典型模型:逻辑回归(LR)、K近邻(KNN)、随机森林(RF)和支持向量机(SVM),它们分别代表了四种不同的归纳偏置与复杂度控制方式。逻辑回归是线性模型,若它都能取得不错效果,说明特征空间中四类可能具有较强线性可分性;KNN是基于局部邻域的非参数方法,它更依赖样本在特征空间中的"聚类结构",如果KNN表现好往往意味着同类样本在特征空间里确实形成紧密团簇;随机森林通过集成多棵决策树捕捉非线性特征交互,同时对异常点相对稳健,是一个很好的"强基线";SVM则在小样本、非线性边界场景下常常表现稳定,尤其当特征维度不高(本项目20维)时,RBF核可以提供足够的表达能力。重要的是,第二问并不预设"哪个模型最好",而是用同一套训练与调参流程公平比较,从而把讨论建立在可验证的实验结果上,而不是经验判断。

python 复制代码
  # ---------------------------- 2) 模型定义与超参 ----------------------------
    def define_models_and_parameters(self):
        """
        定义要对比的传统机器学习模型,以及各自的超参数搜索范围。

        这里选择的四个模型代表了四种"归纳偏置":
        - LR:线性可分/线性边界(配合正则化)
        - KNN:基于距离的局部邻域投票
        - RF:非线性树模型,能处理复杂交互,且对尺度相对不敏感
        - SVM:最大间隔分类(线性核/高斯核)

        通过 GridSearchCV 做一个小规模网格搜索,让对比更公平。
        """
        print("=" * 60)
        print("第二步: 模型定义与超参数设置")
        print("=" * 60)

        self.models = {
            "Logistic Regression": {
                "model": LogisticRegression(random_state=42, max_iter=1000),
                "params": {"C": [0.1, 1, 10]},
            },
            "K-Nearest Neighbors": {
                "model": KNeighborsClassifier(),
                "params": {"n_neighbors": [3, 5, 7]},
            },
            "Random Forest": {
                "model": RandomForestClassifier(random_state=42),
                "params": {"n_estimators": [50, 100], "max_depth": [None, 10]},
            },
            "Support Vector Machine": {
                "model": SVC(random_state=42),
                "params": {"C": [0.1, 1, 10], "kernel": ["rbf", "linear"]},
            },
        }

        print("✓ 已定义以下模型:")
        for name in self.models.keys():
            print(f"  - {name}")
        print("✓ 模型定义完成!\n")

    # ---------------------------- 3) 训练与调参 ----------------------------
    def train_and_optimize_models(self):
        """
        对每个模型进行 GridSearchCV 超参搜索,指标使用 f1_macro。

        选 f1_macro 的原因:
        - 数据集中各类别样本数可能不均衡
        - accuracy 容易被大类"撑高",而宏平均 F1 对每一类同等对待
        """
        print("=" * 60)
        print("第三步: 模型训练与超参数优化")
        print("=" * 60)

        for name, config in self.models.items():
            print(f"\n正在训练 {name}...")

            # n_jobs=1:作者刻意避免 Windows 下中文路径/并行引发的潜在编码问题
            grid_search = GridSearchCV(
                estimator=config["model"],
                param_grid=config["params"],
                cv=3,  # 这里用 3 折,速度更快;也可以换成 5 折更稳
                scoring="f1_macro",
                n_jobs=1,
                verbose=0,
            )

            grid_search.fit(self.X_train_scaled, self.y_train)

            self.best_models[name] = grid_search.best_estimator_
            print(f"✓ {name} 训练完成")
            print(f"  最佳参数: {grid_search.best_params_}")
            print(f"  交叉验证最佳得分: {grid_search.best_score_:.4f}")

        print("\n✓ 所有模型训练完成!\n")

    # ---------------------------- 4) 测试集评估 ----------------------------
    def evaluate_models(self):
        """
        在测试集上评估每个"最佳模型",并把结果写入 self.results。
        """
        print("=" * 60)
        print("第四步: 模型性能评估")
        print("=" * 60)

        for name, model in self.best_models.items():
            # 测试集预测
            y_pred = model.predict(self.X_test_scaled)

            # 指标
            accuracy = accuracy_score(self.y_test, y_pred)
            f1_macro = f1_score(self.y_test, y_pred, average="macro")

            self.results[name] = {"accuracy": accuracy, "f1_macro": f1_macro, "y_pred": y_pred}

            print(f"\n评估 {name}:")
            # classification_report 会输出每一类的 precision/recall/F1 与宏/微/加权汇总
            print(
                classification_report(
                    self.y_test, y_pred, target_names=self.label_names, digits=4
                )
            )

        print("✓ 模型评估完成!\n")

    # ---------------------------- 5) 汇总与"最佳模型"选择 ----------------------------
    def create_performance_summary(self):
        """
        把各模型的 accuracy / f1_macro 汇总成表,并用 accuracy 选一个"最佳模型"。

        说明:
        - 这里用 accuracy 选最优,而 GridSearch 用 f1_macro 调参,属于"训练阶段与汇总阶段
          指标不完全一致"的做法。论文里可以讨论:如果要严格一致,最好统一使用 f1_macro
          选最优模型。
        """
        print("=" * 60)
        print("第五步: 性能汇总")
        print("=" * 60)

        summary_data = []
        for name, metrics in self.results.items():
            summary_data.append(
                {
                    "模型": name,
                    "准确率": f"{metrics['accuracy']:.4f}",
                    "宏平均F1": f"{metrics['f1_macro']:.4f}",
                }
            )

        summary_df = pd.DataFrame(summary_data)
        summary_df["准确率_数值"] = summary_df["准确率"].astype(float)
        summary_df = summary_df.sort_values("准确率_数值", ascending=False).drop(
            "准确率_数值", axis=1
        )

        print("模型性能汇总表:")
        print(summary_df.to_string(index=False))

        best_model_name = summary_df.iloc[0]["模型"]
        print(f"\n最佳模型: {best_model_name}")
        print(f"   准确率: {self.results[best_model_name]['accuracy']:.4f}")

        return best_model_name

调参策略

调参策略使用的是 `GridSearchCV`,并且把评估指标定为 `f1_macro`。这背后体现的是**"诊断任务更关心各类均衡表现"的思想**:如果类别不均衡,单纯追求accuracy可能会牺牲少数类(例如某类故障样本少,但在工程上它恰恰重要)。宏平均F1会对每一类的F1同等加权,因此能更早暴露"某些故障根本分不出来"的问题。代码里交叉验证折数设为3折(`cv=3`),这是一个折中:折数越大评估越稳定,但训练时间越长;在竞赛节奏下,先用3折快速定位合理超参范围是可接受的。另一个工程细节是 `n_jobs=1`,注释里已经说明其意图:在Windows环境里,中文路径、临时文件、并行化有时会触发编码问题,尤其是在某些Anaconda/locale配置不一致时。把并行关掉不是算法层面的选择,而是为了保证"能稳稳跑完并产出结果",这在复盘里其实也是一个合理的工程取舍:稳定性优先于速度。

复盘

从整体流程看,第二问是:先把数据处理到一个"模型能公平比较"的状态(分层划分、标准化、编码),再用同一套调参框架对多模型做横向对比,最后用分类报告、混淆矩阵和特征分布图把结果解释回"故障诊断问题本身"。从数据到模型、从模型到解释的闭环,诊断关心各类均衡表现,所以用宏平均F1;诊断需要知道错在哪里,所以看混淆矩阵;诊断最终要能被解释,所以画特征分布并分析重叠区。

也正因为第二问做的是传统模型,它能把"特征是否靠谱"这件事先说清楚------如果第二问已经能在源域上得到稳定表现,那么第三问再引入迁移学习时,读者会更容易接受"困难来自域偏移,而不是特征/模型本身完全无效"。反之,如果第二问在源域上表现就很差,那复盘应该回到第一问:可能需要重新设计频域窗口、增加带通滤波或包络解调前的预处理、或引入更有判别力的时频特征,

相关推荐
八月瓜科技2 小时前
AI侵权频发:国内判例定边界,国际判决敲警钟
大数据·人工智能·科技·深度学习·机器人
v_for_van2 小时前
力扣刷题记录1(无算法背景,纯C语言)
算法·leetcode·职场和发展
sjjhd6522 小时前
C++模拟器开发实践
开发语言·c++·算法
七夜zippoe2 小时前
大模型低成本高性能演进 从GPT到DeepSeek的技术实战手记
人工智能·gpt·算法·架构·deepseek
二年级程序员2 小时前
qsort函数的使用与模拟实现
c语言·数据结构·算法·排序算法
汗流浃背了吧,老弟!2 小时前
LangChain RAG PDF 问答 Demo
人工智能·深度学习
ajole2 小时前
C++学习笔记——C++11
数据结构·c++·笔记·学习·算法·stl
hoiii1872 小时前
分布式电源选址定容的MATLAB算法实现
分布式·算法·matlab
客卿1232 小时前
力扣二叉树简单题整理(第二集)
算法·leetcode·职场和发展