当前位置: 首页 > news >正文

基于XGBoost与SHAP的气味分子分类:从结构预测到可解释性分析

1. 项目概述与核心价值

气味,作为人类最古老也最神秘的感官体验之一,长久以来都依赖于调香师和风味专家的个人经验与直觉。一个分子闻起来是“果香”还是“硫磺味”,背后是复杂的化学结构与嗅觉受体相互作用的奥秘。传统上,解码这种“结构-气味关系”是一项高度依赖专业知识的挑战。然而,随着机器学习,特别是可解释性机器学习模型的发展,我们终于有了一把数据驱动的钥匙,能够系统性地探索分子结构与其感知气味之间的定量联系。

这个项目的核心,就是利用XGBoost这一强大的集成学习算法,结合SHAP这一模型可解释性工具,构建一个能够根据分子结构预测其气味类别的分类器。但它的独特之处在于,我们并非直接预测上百种零散的气味描述符(如“玫瑰”、“焦糖”),而是引入了两种气味分类法来对描述符进行分组:一种是基于领域专家知识的专家分类法,另一种是基于数据共现模式自动生成的数据驱动分类法。这种做法本质上是一种“数据增强”,它将高维、稀疏、不平衡的原始标签空间,压缩为更具语义意义、更易于模型学习的16个高级气味类别(如“花香”、“果香”、“硫磺/鲜味”等)。

我之所以对这个项目感兴趣,是因为它完美地结合了预测性能科学洞察。我们不仅得到了一个表现不错的分类模型(数据驱动分类法的平均AUROC达到了0.698),更重要的是,通过SHAP分析,我们能够像“打开黑箱”一样,理解模型做出“这是硫磺味”判断时,究竟是基于分子的哪些结构特征。这对于理性设计新气味分子、理解嗅觉感知的化学基础,乃至开发下一代人工智能嗅觉系统,都具有实实在在的指导意义。无论你是计算化学、风味科学的研究者,还是对机器学习在感官科学应用感兴趣的开发者,这个项目都能为你提供一个从数据处理、模型构建到结果深度解读的完整实践范例。

2. 整体方案设计与核心思路拆解

2.1 问题定义与挑战分析

气味预测本质上是一个多标签分类问题:一个分子可能同时具有多种气味属性。原始数据集包含了5331个分子和146种气味描述符,这带来了几个典型挑战:

  1. 标签稀疏与高维:每个分子平均只有少数几个描述符,导致特征-标签矩阵极其稀疏。
  2. 类别极度不平衡:像“玫瑰”、“香草”这类常见气味的样本数,远多于“马肉”、“汗味”等罕见气味。
  3. 语义模糊与主观性:气味描述高度依赖文化和个体经验,例如“洁净感”在不同语境下可能指向不同的气味。

直接使用原始146个描述符作为预测目标,模型很容易过拟合到少数常见类别,且难以学到有泛化能力的规律。

2.2 分类法构建:从混乱到秩序

为了解决上述挑战,项目引入了分类法的概念,将细粒度描述符聚合为粗粒度的“气味家族”。

专家分类法:由调香师、气味科学家等人文与科学领域的专家共同制定。它基于历史知识、香水轮和化学常识,将描述符归纳为16个类别,如“酒精类”、“动物体味”、“花香”、“果香”等。这种分类具有明确的层级结构(例如,“果香”下可分“浆果”、“柑橘”等子类),反映了人类对气味空间的传统认知。

数据驱动分类法:完全由算法生成。我们计算了所有146个描述符在数据集中共同出现的频率,构建了一个共现矩阵。然后使用层次聚类算法(采用欧氏距离和Ward连接方法),将经常被同时用来描述同一个分子的气味词聚成一类。最终,同样得到了16个聚类。有趣的是,这种纯粹基于数据“共生关系”的分类,与专家分类既有重叠又有差异。

核心思路解析:为什么分类法能提升模型性能? 这不仅仅是减少了输出维度(从146到16)。关键在于,分类法引入了先验知识(专家法)或数据内在结构(驱动法),对标签空间进行了“平滑”和“语义压缩”。它迫使模型去学习更泛化、更本质的气味特征,而不是去记忆那些特定于某个稀有描述符的噪声模式。你可以把它想象成学习语言:直接背一本字典(146个词)很难,但先掌握十几个主题(如食物、情感、动作),再在每个主题下学习相关词汇,效率会高得多,也更能理解词与词之间的联系。

2.3 技术选型:为什么是XGBoost + SHAP?

在众多机器学习模型中,我们选择了XGBoost作为分类器,并采用SHAP进行事后解释,这背后有充分的考量:

XGBoost的优势

  1. 处理混合类型数据:分子描述符包含连续型(如分子量、极性表面积)和整数型(如氢键供体数)特征,XGBoost能天然处理。
  2. 自动处理缺失值:化学数据集常有不完整的描述符计算,XGBoost有内置的缺失值处理机制。
  3. 正则化与防过拟合:通过reg_lambdamax_depth等参数,能有效控制模型复杂度,对于样本量有限的气味数据至关重要。
  4. 高效与可扩展:相较于深度神经网络,树模型在中等规模数据上训练更快,调参更直观,且通常不需要GPU加速。

SHAP的必要性: 预测准确只是第一步。在科学研究中,我们更关心“为什么”。SHAP值基于博弈论,为每个特征对每个预测样本的贡献提供了统一、可加的解释。它能告诉我们:

  • 全局重要性:哪些分子描述符对整个模型预测最重要?
  • 局部解释:对于某个被预测为“硫磺味”的特定分子,是哪个结构特征起了决定性作用?
  • 方向性影响:某个描述符(如BCUTZ-1h)的值是高还是低,会如何推动模型向某个类别预测?

这种可解释性对于验证模型是否学到了有化学意义的规律,而非数据巧合,是不可或缺的。

3. 数据准备与特征工程实战

3.1 分子描述符计算与筛选

分子结构本身是图形,但大多数传统机器学习模型(包括XGBoost)需要固定长度的数值向量作为输入。因此,我们需要将每个分子的SMILES字符串(一种文本化的分子结构表示)转化为一组分子描述符

工具选择:我们使用了Mordred描述符计算器。它是一个开源Python库,能够从分子结构一次性计算出1800多种描述符,涵盖拓扑、几何、电子、量子化学等多个维度。

计算流程

from mordred import Calculator, descriptors from rdkit import Chem # 初始化计算器,计算所有描述符 calc = Calculator(descriptors) # 假设mols是一个RDKit分子对象列表 mols = [Chem.MolFromSmiles(smi) for smi in smiles_list] # 计算描述符,结果为pandas DataFrame df_descriptors = calc.pandas(mols)

这一步会生成一个巨大的特征矩阵(样本数 × 1800+)。

特征筛选策略: 直接使用所有描述符会导致维度灾难和过拟合。我们采用了严格的筛选流程:

  1. 移除零方差特征:删除在所有样本上取值完全一样的描述符(对模型无意义)。
  2. ANOVA F检验:针对每个气味类别(原始146类),计算每个描述符与该类别的F值,选取对每个类别区分度最高的特征。这一步初步保留了与气味最可能相关的特征池。
  3. 递归特征消除:使用一个随机森林模型作为评估器,结合置换特征重要性方法,递归地移除最不重要的特征,直到剩下一个最优的特征子集。

最终特征集:经过上述流程,我们从1800多个描述符中筛选出了23个核心描述符。它们主要来自以下几类:

  • BCUT描述符:基于Burden矩阵的特征值,反映分子的形状、极性、电荷分布等信息。例如BCUTZ-1h(加权原子序数的最高特征值)对区分含硫分子至关重要。
  • ETA描述符:基于拓扑距离的电子拓扑描述符。
  • 信息含量描述符:如SIC0(结构信息含量),反映分子中原子的多样性。
  • VSA描述符:基于范德华表面积的描述符,与分子大小和极性相关。
  • 其他拓扑描述符:如nRot(可旋转键数量)、RPCG(相对极性电荷指数)等。

实操心得:特征筛选的陷阱特征筛选必须在训练集上进行,并保存筛选器,再应用到验证集和测试集。绝对不能在合并所有数据后再筛选,否则会导致数据泄露,严重高估模型性能。我们使用sklearnPipeline来封装这一过程,确保流程的严谨性。

3.2 分类法标签转换

这是本项目的关键预处理步骤。我们有两个映射表:专家分类映射表(Table S8)和数据驱动分类映射表(Table S9)。每个映射表定义了146个原始描述符属于16个高级类别中的哪一个。

转换逻辑: 对于一个原始标签为[‘rose’, ‘vanilla’, ‘sulfur’]的分子:

  • 在专家分类法下,‘rose’->‘Flower’‘vanilla’->‘Gourmand’‘sulfur’->‘Sulfur’。因此,该分子的新标签为[‘Flower’, ‘Gourmand’, ‘Sulfur’]
  • 在数据驱动分类法下,需要查Table S9,假设‘rose’->‘C’‘vanilla’->‘E’‘sulfur’->‘N’。新标签为[‘C’, ‘E’, ‘N’]

多标签处理:转换后,每个分子仍然可能对应多个高级类别。我们使用多标签二值化进行处理,为每个分子生成一个长度为16的二进制向量,其中1表示属于该类别。这构成了XGBoost多输出分类的目标变量。

3.3 数据集划分与不平衡处理

我们将5331个分子的数据集按80/20的比例划分为训练集和测试集,并进行了分层抽样,确保每个气味类别(在分类法下)在训练集和测试集中的分布大致相同。

应对类别不平衡:即使在16个类别的层级,不平衡依然存在。我们主要依靠XGBoost的scale_pos_weight参数(在二分类中)或为每个输出类别设置不同的样本权重来缓解。更复杂的策略如SMOTE过采样在分子描述符这种连续高维特征上效果不稳定,我们经过测试后未采用。

4. 模型构建、训练与超参数优化

4.1 XGBoost多输出分类配置

XGBoost原生支持多输出分类(通过multi_strategy=’one_output_per_tree’或使用MultiOutputClassifier包装器)。我们选择后者,因为它更灵活,可以为每个输出类别单独调整权重。

from xgboost import XGBClassifier from sklearn.multioutput import MultiOutputClassifier from sklearn.model_selection import RandomizedSearchCV # 初始化基础XGBoost分类器 base_xgb = XGBClassifier(objective='binary:logistic', eval_metric='logloss', use_label_encoder=False, random_state=42) # 包装为多输出分类器 multi_target_xgb = MultiOutputClassifier(base_xgb, n_jobs=-1)

4.2 贝叶斯超参数优化

超参数对树模型性能影响巨大。我们采用了基于序列模型的贝叶斯优化(通过scikit-optimize库),它比网格搜索或随机搜索更高效。

定义的搜索空间(基于Table S1):

param_space = { 'estimator__n_estimators': [100, 200, 400, 600, 800, 1000, 2000, 4000, 5000, 10000], 'estimator__max_depth': [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 'estimator__learning_rate': [0.0001, 0.001, 0.01, 0.1, 0.2, 0.4, 0.6, 0.8], 'estimator__subsample': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], 'estimator__colsample_bynode': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], 'estimator__reg_lambda': [0.001, 0.01, 0.1, 1, 5, 10, 15, 20, 25], 'estimator__min_child_weight': [1, 50, 100, 150, 200, 250], 'estimator__tree_method': ['approx', 'hist'] }

优化过程与结果: 我们以验证集上的宏平均AUROC作为优化目标。经过多轮迭代,贝叶斯优化找到了一个在验证集上表现优异的参数组合(如表S1所示):

  • n_estimators: 2000
  • max_depth: 3
  • learning_rate: 0.4
  • subsample: 0.9
  • colsample_bynode: 0.9
  • reg_lambda: 5
  • min_child_weight: 1
  • tree_method: ‘hist’

参数选择背后的考量

  • max_depth=3:较浅的树深度表明模型学习的是相对简单的规则,这有助于防止过拟合,并可能意味着气味类别与某些关键描述符之间存在较强的线性或浅层非线性关系。
  • learning_rate=0.4:相对较高的学习率配合大量的树(n_estimators=2000),说明模型需要快速收敛,但通过大量树来细化学习过程。
  • reg_lambda=5:较强的L2正则化,再次印证了防止过拟合在数据集上的重要性。
  • tree_method=’hist’:直方图算法,能加速训练并减少内存使用,特别适合我们这种特征数(23)远小于样本数的场景。

4.3 模型性能评估

我们使用宏平均AUROC、F1分数、精确率和召回率来全面评估模型。宏平均意味着对每个类别的指标先单独计算,再取平均,这能平等对待所有类别,避免被大类别主导。

核心发现(详见表S6, S7):

  • 数据驱动分类法 vs. 专家分类法:数据驱动分类法在几乎所有指标上都略胜一筹(平均AUROC: 0.698 vs 0.684)。这表明基于数据内在统计规律构建的分类法,可能比人类专家基于概念构建的分类法,更有利于机器学习模型学习。
  • 分类法 vs. 原始描述符:与直接预测146个原始描述符的基线模型相比,两种分类法都带来了显著的性能提升(AUROC从~0.59提升至~0.69)。这强有力地证明了分类法作为数据增强策略的有效性。
  • 最佳与最差类别:在两个分类法中,“硫磺”类(Sulfur/N)和“鲜味”类(Savory/I)都是表现最好的类别(AUROC > 0.75)。而表现较差的类别通常是那些样本量少或气味定义模糊的类别,如专家法中的“水生调”(Aquatic)和数据驱动法中的“P”类(浆果类)。

5. 模型可解释性深度剖析:SHAP值揭示结构-气味密码

训练出一个好模型只是成功了一半。接下来,我们使用SHAP分析来解读模型,探寻分子结构如何编码气味信息。这是整个项目最富洞察力的部分。

5.1 全局特征重要性:什么决定了分子的气味家族?

我们首先计算了所有样本的SHAP值,并汇总得到每个特征对每个气味类别的平均绝对贡献。

关键发现

  • BCUTZ-1h是全局王者:这个描述符在多个类别中都具有极高的SHAP值。它本质上是Burden矩阵按原子序数加权的最高特征值。化学意义:它强烈关联于分子中最重原子的原子序数。例如,对于含硫(S,原子序数16)的分子,BCUTZ-1h值会显著升高。因此,它成为预测“硫磺”类气味的决定性特征��
  • SIC0(结构信息含量)的独特角色:这个描述符衡量分子中原子的多样性。高SIC0值意味着分子由多种不同类型的原子以不同连接方式构成,结构复杂。SHAP分析显示,SIC0值强烈推动模型预测“鲜味”类,却抑制对“硫磺”类的预测。这揭示了“鲜味”和“硫磺”气味在分子结构复杂性上的微妙差异:具有复杂、多样原子构成的分子更可能呈现鲜味(如某些含氮杂环化合物),而典型的硫磺味分子可能结构相对简单(如硫醇、硫醚)。
  • 其他重要描述符BCUTare-1l(加权原子极化率的特征值)、Xpc-4dv(4阶路径聚类信息指数)等也在不同类别中扮演重要角色,通常与分子的尺寸、形状和电子分布有关。

5.2 局部解释:以“硫磺”和“鲜味”为例

让我们深入两个表现最好的类别,看看SHAP如何提供类特异的解释。

“硫磺”类(专家法Sulfur / 数据驱动法N)

  • 主导特征BCUTZ-1h一骑绝尘。SHAP依赖图显示,当BCUTZ-1h值接近16时,其对“硫磺”类的SHAP贡献值急剧正向增加。这完美符合化学直觉:原子序数16对应硫元素。
  • 分类法间的差异:在数据驱动分类法中,BCUTZ-1h的影响力比在专家分类法中更为集中和突出。这可能意味着数据驱动法下的“硫磺”类所包含的描述符集合更纯粹、更紧密地与含硫结构相关;而专家法下的“硫磺”类可能还包含了一些在感知上相关但结构上不那么特异的描述符(如“发酵味”),从而稀释了BCUTZ-1h的绝对重要性。

“鲜味”类(专家法Savory / 数据驱动法I)

  • 主导特征SIC0取代BCUTZ-1h成为最重要的特征。高SIC0值(结构复杂)强烈指向“鲜味”。
  • 与“硫磺”类的对比:这正是SHAP分析的魅力所在。它告诉我们,虽然BCUTZ-1h对两者都重要,但模型区分“硫磺”和“鲜味”的关键在于SIC0。一个具有中等BCUTZ-1h值(可能来自其他重原子)但SIC0很高的分子,更可能被预测为“鲜味”而非“硫磺”。这为理解这两种有时在感官描述上接近的气味(都可能有“咸鲜”、“含硫”感)提供了分子结构层面的量化区分。

5.3 置换特征重要性验证

为了确保SHAP结果的稳健性,我们还计算了置换特征重要性。PFI通过随机打乱某个特征在测试集上的值,观察模型性能(如AUROC)的下降程度来衡量其重要性。

结果一致性:PFI分析(图S15, S16)确认了BCUTZ-1hBCUTare-1l等特征是全局最重要的。这与SHAP的全局分析结论一致,增强了我们对于这些关键描述符信心的信心。但PFI无法提供SHAP那样的方向性(正负影响)和样本级解释。

6. 案例研究:梨子香精分子的分类预测

为了测试模型的实用性和泛化能力,我们选取了5种在香水中常用的梨子香精分子作为外部测试案例。

测试分子:包括Poirenate, Pearadise, Helvetolide, Quinceester等。预测任务:使用训练好的两个XGBoost模型(分别基于专家分类法和数据驱动分类法)预测这些分子所属的高级气味类别。

预测结果与分析(对应原文Table 4): 所有5个分子都被两个模型正确地预测为含有“果香”类别(专家法:Fruity; 数据驱动法:J)。其中一些分子还被预测出“青香”(Green)、“花香”(Flower)等辅助类别,这与调香师对这些分子的实际描述是吻合的。

案例启示

  1. 模型泛化性:模型能够对训练集中未出现过的、但结构相似的商业香精分子做出合理预测,说明它学到的是普适的结构-气味规律,而非单纯记忆。
  2. 分类法的实用性:即使模型预测的是高级类别(如“果香”),而非具体描述符(如“梨子”),这在实际应用中仍有很高价值。在香水设计的早期阶段,快速筛选出具有“果香”骨架的分子,可以极大地缩小候选范围。
  3. 可解释性的应用:我们可以进一步查看这些梨子香精分子的SHAP值。例如,可能会发现高Xpc-4dv(与分子分支度相关)和特定的VSA_EState描述符(与极性相关)是它们被归为“果香”的关键。这为设计新的果香分子提供了具体的设计思路。

7. 常见问题、挑战与未来展望

7.1 项目实施中的关键挑战与解决方案

  1. 数据质量与一致性:气味数据来自多个数据库,描述符的标注存在主观性和不一致性。
    • 解决方案:进行了严格的数据清洗,统一了描述符名称,并剔除了标注信息过少或冲突严重的分子。最终构建了一个相对干净、统一的分子气味数据集(MMD)。
  2. 多标签与类别不平衡:如前所述,这是本领域的核心难题。
    • 解决方案:采用分类法进行标签压缩是治本之策。此外,在XGBoost中为少数类别设置更高的样本权重,并在评估时使用宏平均指标,以公平看待所有类别。
  3. 分子表示的局限性:我们使用的是2D分子描述符,丢失了分子的三维构象信息。而气味分子与嗅觉受体的结合是三维空间中的事件。
    • 现状与妥协:目前公开的大规模气味数据集大多只提供2D结构(SMILES)。使用2D描述符是权衡数据可用性与计算复杂度的结果。尽管有局限,但结果表明2D信息已经能捕捉到显著的结构-气味关联。

7.2 模型局限性与未来改进方向

  1. 迈向3D与图神经网络:未来的方向必然是整合3D分子结构信息。方法包括使用分子动力学模拟采样多个构象,或直接使用图神经网络(如FragNet, KerGNNs)。GNN能直接处理分子图,自动学习空间和拓扑特征,潜力巨大。我们的分类法可以作为先验知识融入GNN的损失函数或架构设计中,引导其学习更有层次的气味表示。
  2. 浓度依赖性与混合物:当前模型预测的是单一气味分子在“典型”浓度下的气味。现实中,气味感知强烈依赖于浓度,且我们闻到的基本都是复杂混合物。构建包含浓度信息和混合物标签的数据集,是下一代模型必须攻克的堡垒。
  3. 对映异构体与手性:许多气味分子具有手性中心,其对映异构体气味可能截然不同(如左旋香芹酮是留兰香味,右旋是葛缕子味)。当前2D描述符无法区分手性。未来需要能够处理三维手性信息的描述符或模型。
  4. 从分类到生成:当前工作是“分析式”的(给定结构预测气味)。更激动人心的方向是“生成式”的:给定一个想要的气味描述(如“阳光下的海风”),让AI生成可能具有该气味的分子结构。这需要结合生成模型(如VAE, GAN)和我们这里训练的预测模型。

7.3 给实践者的建议

如果你打算在自己的研究中复现或拓展类似工作,以下几点经验可能对你有帮助:

  • 从公开数据集开始:Pyrfume项目等开源倡议正在整合多个气味数据集,是很好的起点。
  • 重视特征工程,但不必沉迷:Mordred描述符是一个强大的起点。但与其手动筛选,不如结合领域知识(如关注含硫、含氮官能团相关的描述符)和自动筛选方法(如我们使用的递归特征消除)。
  • 可解释性不是可选项:尤其是在科学探索中,一个无法解释的“黑箱”模型价值有限。务必把SHAP、LIME等可解释性工具作为你工作流的标准组成部分。
  • 分类法是有效的“降维”策略:当你的标签空间复杂、稀疏时,考虑引入某种形式的层次结构或分组,无论是基于专家知识还是数据聚类,这很可能带来性能提升和更清晰的��释。

这个项目向我们展示,通过结合严谨的数据处理、恰当的机器学习模型和深入的可解释性分析,我们能够从纷繁复杂的分子结构与主观气味描述中,提炼出稳定、可量化、具有化学意义的规律。它不仅是向“数字化嗅觉”迈出的坚实一步,也为如何将领域知识(分类法)与数据驱动方法(机器学习)相结合,解决复杂的化学生物学问题,提供了一个精彩的范例。

http://www.gsyq.cn/news/1376302.html

相关文章:

  • Unity ML-Agents环境安装避坑指南:Python、TensorFlow、Barracuda版本协同拓扑
  • ProChart深度解析:Unity运行时数据可视化中间件架构与工程实践
  • Centos 7/8 实战:将官网deb包转为rpm安装搜狗拼音,我的踩坑记录与完整命令
  • 保姆级教程:在CentOS 7/8上从源码编译安装ndctl和ipmctl(附常见编译错误解决)
  • 3分钟搞定网易云音乐NCM解密:终极免费转换工具使用指南
  • 时间序列去噪实战:手把手教你用Python SSA算法分离信号与噪声(含窗口长度L选择技巧)
  • BFloat16浮点格式与SME指令集在深度学习中的应用
  • XUnity.AutoTranslator:打破语言障碍,让Unity游戏实时翻译变得简单
  • iOS砸壳与反编译实战:从FairPlay解密到Swift逆向分析
  • 智能识别告警系统完整方案
  • AI写论文神器合集!4款AI论文写作工具,解决你的论文烦恼!
  • 3分钟快速解密网易云音乐NCM文件:免费工具完整使用指南
  • 如何3分钟完成飞书文档批量导出:完整指南与实战教程
  • 为啥年纪轻轻就膝关节痛?中医妙招来揭秘!
  • JMeter实战:从接口测试到性能基线的全链路压测指南
  • 基于MLP误差预测的自适应多尺度模拟:原理、实现与应用
  • Propius:面向协同机器学习的异构边缘资源管理平台架构解析
  • 机器学习在金融风控中的应用:随机森林与SVM银行破产预测对比
  • 2026年全国现烤烘焙连锁品牌排行榜:最新权威排名与专业指南。
  • 终极Minecraft NBT数据编辑指南:NBTExplorer完全解析
  • 调试项目上只更换镜头,主要影响哪些效果
  • DeepSeek 的上下文缓存是什么?它和程序里的 Redis 缓存一样吗?
  • QMCDecode:解锁QQ音乐加密格式,实现音频自由播放的本地解密工具
  • # AI零代码应用生成平台项目实训(七)——图片收集并发优化与子图实战
  • Claude Code SubAgents 配置实战:4个现成配置,复制就能用
  • 科学机器学习评估框架CTF4Science:主流模型在混沌系统预测中的性能剖析
  • Tushare金融数据 API 平台
  • 联邦学习与知识图谱融合:破解罕见儿科疾病数据孤岛与隐私难题
  • 工业智能化的时序选型指南:当数据底座遇见机器学习
  • 终极指南:如何在Windows系统中使用ViGEmBus实现游戏控制器虚拟化