1. 项目概述与核心挑战

在软件工程的需求工程阶段,我们常常需要处理海量的、以自然语言形式编写的需求文档。一个典型的挑战是,如何自动地将这些需求条目准确地分类到不同的类别中,例如功能性需求(FR)和非功能性需求(NFR),并进一步细分NFR为安全性、可用性、性能等子类。这本质上是一个文本多分类问题。然而,当我们尝试用机器学习方法来解决时,会立刻撞上两堵坚实的“墙”:类别不平衡和高维小样本数据。

想象一下,你手头有一个包含近千条需求的数据集,但其中超过一半是“功能性需求”,而像“可移植性”这样的类别可能只有区区十几条。这就是类别不平衡——模型很容易学会去预测那个最大的类别,而对“小众”类别视而不见,导致在实际应用中,那些至关重要的非功能性需求(如安全、性能)被错误分类或忽略。与此同时,为了将文本转化为机器可理解的特征,我们通常会使用词袋模型或N-gram等方法,这很容易生成数千甚至上万个特征维度。但我们的样本量(需求条目数)可能只有几百个,形成了“维度诅咒”——特征数量远大于样本数量,即高维小样本问题。在这种场景下,大多数传统机器学习模型会陷入过拟合的泥潭,泛化能力急剧下降。

过去几年,社区提出了不少方案。比如K&M方法,它采用N-gram特征和混合特征选择,并结合过采样与欠采样来应对不平衡。还有基于BERT的NoRBERT方法,凭借预训练语言模型的强大表征能力取得了领先效果。以及从医学图像领域借鉴来的Yin方法,它试图通过分解多数类来进行特征选择。但这些方法各有局限:传统方法在HDLSS问题上捉襟见肘;深度学习方法虽然强大,但对小类别的识别依然不稳定,且计算开销巨大。

正是在这样的背景下,我们提出了HC4RC。它不是另一个“魔改”的模型,而是一个系统性的解决方案框架。其核心思想很直观:既然问题复杂,我们就分层处理、分而治之。通过 语义角色特征选择 ,我们不是盲目地使用所有词汇,而是聚焦于那些在句子中承担核心语义角色(如动作、客体、属性)的词语,这相当于为模型配备了一个“语法过滤器”,从源头降低噪声和维度。通过 数据集分解 ,我们将棘手的多分类问题,转化为一系列更易处理的二分类或更小规模的多分类问题,有效缓解了全局的不平衡。最后,通过 层次分类 策略,我们模仿人类专家的决策过程,先判断大类(如FR/NFR),再细分小类,让模型的学习过程更有条理。下面,我将带你深入这个框架的每一个细节,并分享我们在实现和调优过程中的实战经验。

2. HC4RC方法深度解析:三层架构拆解

HC4RC方法的有效性,源于其精心设计的三层架构。每一层都针对一个特定问题,三层叠加,形成了强大的协同效应。理解每一层的设计动机和实现细节,是掌握该方法的关键。

2.1 第一层:语义角色特征选择——从“词海”到“精兵”

文本分类的第一步是特征工程。传统方法如TF-IDF或词袋模型,会将每个不同的词或N-gram都视为一个特征。对于一个专业的需求文档库,这会产生一个极其稀疏且高维的特征空间,其中充斥着大量对分类无益的噪声词(如冠词、介词、泛泛的动词)。

为什么是语义角色? 语义角色标注旨在回答“谁对谁做了什么”的问题。在一个需求语句中,例如“系统 1秒内响应用户 登录请求 ”,核心的语义信息承载在特定的角色上:

  • 谓词(Predicate) : “响应”——表达了系统需要执行的核心动作。
  • 施事(Agent) : “系统”——动作的执行者。
  • 受事(Patient) : “登录请求”——动作的承受者。
  • 时间(Time) : “在1秒内”——定义了动作的时间属性,这强烈暗示了性能需求。

我们的假设是:承载这些核心语义角色的词语,比句子中的其他词语(如“应”、“的”)包含更强的类别区分信号。提取这些角色对应的词语作为特征,相当于做了一次基于语言学知识的强特征降维。

实操要点与工具选择: 我们使用斯坦福大学的CoreNLP工具包进行语义角色标注。这个过程大致如下:

  1. 句子分割与分词 :将每条需求文本分割成句子并进行分词。
  2. 词性标注与句法分析 :标注每个词的词性,并构建句法依存树。
  3. 语义角色标注 :基于句法树,识别出句子中的谓词以及与之相关的语义角色论元。

注意 :SRL工具并非100%准确,尤其在需求文本这种有时不太符合标准语法规范的领域。在实践中,我们发现对长句、被动语态或省略句的处理会有误差。一个实用的技巧是,在预处理阶段,可以尝试对需求语句进行简单的句式规范化(例如,将被动语态转为主动),这能在一定程度上提升SRL的准确性。

提取出角色后,我们不是简单地使用角色标签,而是提取填充这些角色的 词语序列 。例如,对于“时间”角色,我们提取“1秒内”这个词。然后,我们将所有需求中提取到的、属于核心语义角色(我们通常关注谓词、受事、时间、地点等)的词语,构成一个精简的词典。最终的特征向量,就是基于这个精简词典生成的TF-IDF向量。

带来的好处:

  • 维度大幅降低 :特征维度从原始文本的2000+,可能降至几百甚至更少,直接缓解了HDLSS问题。
  • 特征可解释性增强 :特征不再是孤立的词,而是带有语义功能的词,便于我们理解模型为何做出某个决策。
  • 噪声抑制 :过滤了停用词和无关词汇,让模型更关注实质性内容。

2.2 第二层:数据集分解——化整为零的战术

面对一个包含12个类别且严重不平衡的数据集,直接训练一个12分类器是困难的。数据集分解的核心思想是“分解问题,降低难度”。我们不是一次性区分所有类别,而是通过巧妙的分解,创造出一系列更平衡、更简单的子问题。

HC4RC的分解策略: 我们采用了一种基于类别相似性的分解策略。首先,分析所有类别的需求描述,计算它们在高维空间(经过SRL特征选择后)的相似度(如余弦相似度)。然后,将语义上相近的类别聚合在一起。例如,“性能”(PE)、“可维护性”(MN)、“可用性”(US)可能被分到一组,而“安全性”(SE)、“法律性”(L)、“可移植性”(PO)被分到另一组。

具体分解流程如下:

  1. 将原始的12类数据集,根据相似度聚类成K个组(例如K=4)。每个组内的类别数相对较少,且样本量相对平衡。
  2. 训练一个“元分类器”,其任务是判断一个新需求属于这K个组中的哪一个。这是一个K分类问题,但比原始的12分类要简单,且组间不平衡程度可能更低。
  3. 对于每一个组,我们为其内部包含的类别,单独训练一个细粒度的分类器。

这样,对于一个新需求,分类流程变为:先由元分类器判断它属于哪个组,再由该组对应的细粒度分类器给出最终类别标签。这本质上是一个两层的层次分类结构。

与Yin方法的本质区别: 这里必须澄清,我们的数据集分解与Yin方法中的 类别分解 有根本不同。Yin方法是针对二分类问题,将多数类样本通过聚类等方法分解成多个“伪子类”,目的是为了在特征选择阶段创造更平衡的对比环境。而我们的数据集分解是针对多分类问题,将整个数据集(所有类别)按语义划分为子集,目的是降低单个分类器的决策复杂度和平滑类间分布。这是解决多分类不平衡的一种宏观策略,而非微观的特征选择技巧。

2.3 第三层:层次分类——模仿人类的决策逻辑

层次分类是数据集分解的自然延伸,它为我们提供了一个清晰、高效的推理框架。其结构与我们分解后的数据集完美契合。

层次分类器构建:

  1. 根节点分类器 :对应上述的“元分类器”,负责进行粗粒度分组(例如,判断需求属于“质量属性组”、“约束组”还是“功能组”)。
  2. 叶节点分类器 :每个分组对应一个叶节点分类器,负责该组内的细粒度类别划分。

优势分析:

  • 错误隔离 :如果一个叶节点分类器(例如负责“性能”、“可用性”的那个)犯了错误,这个错误只会影响该组内的类别,不会污染其他完全不相干的类别(如“安全性”)。在扁平的多分类中,一个类的错误会影响所有类的决策边界。
  • 针对性优化 :我们可以为每个叶节点分类器独立地选择模型、调整参数、进行特征工程。例如,对于“安全性”相关的类别,我们可能会加入一些安全领域特定的关键词特征。
  • 效率提升 :在预测时,只需要运行一个根分类器和其中一个叶分类器,计算量通常小于运行一个庞大的、包含所有类别的单一分类器。

分类器模型选择: 在HC4RC中,我们选择了线性支持向量机作为所有分类器(根节点和叶节点)的基础模型。选择SVM的原因在于:

  1. 高维空间有效性 :SVM,特别是线性SVM,在处理高维数据时理论上有良好保障,即使在小样本情况下,通过最大化间隔,也能获得较好的泛化能力。
  2. 可解释性相对较好 :通过分析SVM的权重向量,我们可以了解哪些语义角色特征对区分某个类别最重要。
  3. 计算效率 :相比深度学习模型,线性SVM的训练和预测速度更快,这对于需要快速迭代和部署的场景是优势。

当然,SVM的性能严重依赖于核函数和参数(如惩罚系数C)。我们使用网格搜索(GridSearchCV)对每个分类器独立进行参数调优,以确保其在各自子问题上的最佳性能。

3. 从理论到实践:HC4RC完整实现流程

理解了核心思想后,我们来看如何一步步实现HC4RC。这里以PROMISE-exp数据集为例,展示从数据准备到模型评估的全过程。

3.1 数据准备与预处理

我们使用的PROMISE-exp数据集包含969条需求,分为12个类别。数据预处理是后续所有步骤的基础,其质量直接影响最终效果。

步骤1:数据清洗

  • 去除无关字符 :清理HTML标签、特殊符号、多余空格和换行符。
  • 文本规范化 :将文本转换为小写。对于英文需求,这一步是标准操作。
  • 处理缩写和同义词 :需求文档中常有缩写。我们建立了一个小的领域词典,将常见缩写(如“UI”替换为“User Interface”)和同义词(如“shall”, “must”, “should”归一化)进行标准化。这一步能显著减少特征稀疏性。

步骤2:语义角色标注与特征提取 这是HC4RC区别于其他方法的关键步骤。

import stanza # 或使用Stanford CoreNLP的Python接口
# 初始化Stanza pipeline,包含SRL功能
nlp = stanza.Pipeline('en', processors='tokenize,pos,lemma,depparse,srl')
def extract_srl_features(text):
    doc = nlp(text)
    srl_features = []
    for sentence in doc.sentences:
        for frame in sentence.frames: # frame即谓词-论元结构
            predicate = frame.predicate.text
            srl_features.append(f"PRED_{predicate}")
            for arg in frame.arguments:
                # 我们关注核心论元,如ARG0, ARG1, ARG-TMP等
                if arg.role in ['ARG0', 'ARG1', 'ARG2', 'ARGM-TMP', 'ARGM-LOC']:
                    arg_text = ' '.join([word.text for word in arg.words])
                    srl_features.append(f"{arg.role}_{arg_text}")
    # 返回去重后的特征词列表
    return list(set(srl_features))
# 对数据集中所有需求文本应用此函数,收集所有独特的SRL特征词
all_srl_tokens = []
for req in requirements:
    all_srl_tokens.extend(extract_srl_features(req['text']))
# 构建基于SRL特征的词汇表
from sklearn.feature_extraction.text import CountVectorizer
srl_vectorizer = CountVectorizer(vocabulary=set(all_srl_tokens), tokenizer=lambda x: x.split(), lowercase=False)
X_srl = srl_vectorizer.fit_transform([‘ ‘.join(extract_srl_features(r[‘text’])) for r in requirements])

这段代码演示了核心过程。最终, X_srl 就是一个基于语义角色特征的需求-特征矩阵,其维度远小于基于所有N-gram的特征矩阵。

步骤3:数据集分解 基于SRL特征矩阵,我们计算类别间的相似度(如使用各类别中心向量的余弦相似度),并进行层次聚类(Hierarchical Clustering)或K-Means聚类,将12个类别划分为若干组。

from sklearn.cluster import AgglomerativeClustering
from sklearn.metrics.pairwise import cosine_similarity
# 计算每个类别的平均特征向量
class_vectors = {}
for label in set(labels):
    class_vectors[label] = X_srl[labels==label].mean(axis=0)
# 构建类别相似度矩阵
labels_list = list(class_vectors.keys())
sim_matrix = cosine_similarity(np.vstack([class_vectors[l] for l in labels_list]))
# 进行层次聚类
clustering = AgglomerativeClustering(n_clusters=4, affinity='precomputed', linkage='average').fit(1 - sim_matrix) # 将相似度转为距离
# 得到每个类别所属的组
group_assignment = dict(zip(labels_list, clustering.labels_))

根据聚类结果,我们将原始数据集拆分成多个子集,并为每个子集创建对应的标签映射(将原始类别标签映射为组内类别标签)。

3.2 模型训练与层次分类器构建

步骤4:训练根节点分类器 使用分解后的组标签作为新的目标变量,训练一个多分类SVM(LinearSVC)。特征使用全局的SRL特征矩阵 X_srl

from sklearn.svm import LinearSVC
from sklearn.model_selection import GridSearchCV
# group_labels 是根据group_assignment转换后的组标签
param_grid = {'C': [0.01, 0.1, 1, 10, 100]}
root_clf = GridSearchCV(LinearSVC(max_iter=10000), param_grid, cv=5, scoring='f1_macro')
root_clf.fit(X_srl_train, group_labels_train)

步骤5:训练叶节点分类器 为每一个组(假设有4个组),提取属于该组的所有训练样本及其原始类别标签。为每个组训练一个独立的SVM多分类器。

leaf_classifiers = {}
for group_id in range(num_groups):
    # 获取属于当前组的样本索引
    group_mask = (group_labels_train == group_id)
    X_group = X_srl_train[group_mask]
    y_group = original_labels_train[group_mask] # 原始细粒度标签
    # 训练该组的分类器
    leaf_clf = GridSearchCV(LinearSVC(max_iter=10000), param_grid, cv=5, scoring='f1_macro')
    leaf_clf.fit(X_group, y_group)
    leaf_classifiers[group_id] = leaf_clf

步骤6:预测流程 对于一条新需求:

  1. 提取其SRL特征,转化为特征向量 x
  2. 使用根分类器预测其所属的组: group = root_clf.predict(x)
  3. 根据预测的组ID,调用对应的叶节点分类器: final_label = leaf_classifiers[group].predict(x)

3.3 评估策略:10折与p折交叉验证

为了全面评估模型的泛化能力,我们采用了两种交叉验证策略:

  • 10折交叉验证 :将969条需求随机打乱,分成10份。轮流将其中1份作为测试集,其余9份作为训练集。这评估了模型对“未见过的需求条目”的泛化能力。
  • p折交叉验证 :根据需求来源的“项目”(文档)进行划分。PROMISE-exp数据集来自47个项目。我们将项目分成10组(每组4-5个项目)。测试时,确保同一个项目的所有需求不会同时出现在训练集和测试集中。这评估了模型对“来自全新项目文档的需求”的泛化能力,更具现实意义,因为实际应用中我们总是要处理新项目的需求。

在代码实现上,我们使用 StratifiedKFold 来确保每一折中各类别的分布与整体数据集大致相同,这对于不平衡数据尤为重要。

from sklearn.model_selection import StratifiedKFold
# 10-fold CV
skf_10fold = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
for train_idx, test_idx in skf_10fold.split(X_srl, original_labels):
    X_train, X_test = X_srl[train_idx], X_srl[test_idx]
    y_train, y_test = original_labels[train_idx], original_labels[test_idx]
    # 在此训练和评估HC4RC模型
# p-fold CV: 需要先根据项目ID对样本进行分组,然后进行分层分组K折
from sklearn.model_selection import GroupKFold
gkf = GroupKFold(n_splits=10)
for train_idx, test_idx in gkf.split(X_srl, original_labels, groups=project_ids):
    # groups参数是每个样本对应的项目ID
    # 后续步骤同上

4. 实验结果深度剖析与横向对比

我们将HC4RC与K&M、Yin、NoRBERT三个基线方法在PROMISE-exp数据集上进行了全面对比。评估指标采用了更能反映不平衡数据分类性能的宏平均(Macro-Average)和微平均(Micro-Average)F1分数。

4.1 性能对比:数字背后的故事

从整体宏平均F1分数来看(10折CV),HC4RC达到了0.51,显著高于K&M的0.44和Yin的0.47,但低于NoRBERT的0.57。这个结果清晰地告诉我们:

  1. HC4RC在传统机器学习方法中胜出 :它证明了语义角色特征选择+数据集分解+层次分类这套组合拳,比单纯的N-gram+采样(K&M)或为特征选择而做的类分解(Yin)更有效。
  2. 与深度学习的差距 :NoRBERT基于BERT,拥有数亿参数和在大规模语料上预训练的知识,其强大的上下文表征能力在整体性能上领先是符合预期的。HC4RC作为基于SVM的方法,能达到NoRBERT约90%的宏平均性能,已经是非常有力的结果。

深入小类别表现: 这才是HC4RC的闪光点。观察各个类别的F1分数(见表3),我们发现:

  • 在10折CV中,HC4RC在 可用性(A) 、**可维护性(MN) 法律性(L)**这三个小类别上,F1分数超过了包括NoRBERT在内的所有其他方法。
  • 例如,在**可维护性(MN,仅24个样本) 上,HC4RC的F1为0.41,而NoRBERT仅为0.27。在 法律性(L,仅15个样本)**上,HC4RC为0.54,NoRBERT为0.52。

这说明什么? 即使强大如BERT,在面对极端不平衡的小样本类别时,其性能也会出现显著波动和下降。而HC4RC通过数据集分解和层次分类,为这些小类别创造了更“友好”的竞争环境(在一个更小的、更相关的类别子集中进行分类),从而提升了对它们的识别能力。这对于需求工程至关重要,因为诸如安全、法律合规等小类别需求,其重要性往往不亚于大量的功能性需求。

4.2 效率对比:轻量级模型的优势

除了精度,在实际工程部署中,计算效率和资源消耗同样关键。我们的实验在一台标准笔记本电脑(Intel Core i5, 8GB RAM)上进行,结果对比如下:

方法 执行时间 (10折CV) 内存占用
HC4RC 10.73秒 1.7 GB
K&M 23.70分钟 3.9 GB
Yin 10.52秒 0.75 GB
NoRBERT 1.09小时 5.7 GB

分析:

  • HC4RC效率极高 :其训练预测时间与最简单的Yin方法处于同一量级(10秒级),远快于需要复杂特征工程的K&M(分钟级)和需要巨大计算图的NoRBERT(小时级)。
  • 内存友好 :HC4RC的内存占用远低于NoRBERT,甚至低于K&M。这使得它能够在资源受限的环境(如某些CI/CD流水线或边缘设备)中轻松部署。
  • 实用性凸显 :对于需要快速迭代、频繁重新训练(如需求文档持续更新)的场景,或者对于算力有限的团队,HC4RC提供了一个在精度和效率之间取得优异平衡的选择。

4.3 泛化能力分析:10折 vs. p折

对比10折CV和p折CV的结果,可以洞察模型对不同类型“未知”数据的适应能力。

  • HC4RC表现稳定 :其宏平均F1在10折和p折下分别为0.51和0.50,微平均F1为0.63和0.64。这表明HC4RC对于“全新需求条目”和“来自全新项目的需求”都具有稳健的泛化能力,波动很小。
  • NoRBERT在p折下更强 :NoRBERT在p折下的宏/微平均F1(0.64/0.81)显著高于10折下(0.57/0.76)。这说明BERT模型能够更好地学习到跨项目的、更深层的语义模式,从而在面对全新项目文档时,泛化能力更强。这是预训练模型的一大优势。
  • Yin方法泛化能力不足 :Yin方法在p折下的性能(宏平均F1 0.22)相比10折(0.47)暴跌。这很可能是因为其类分解策略严重依赖于训练数据的内在分布,当数据分布随项目变化时(p折模拟了这种情况),其分解方式失效,导致性能骤降。

实操心得 :这个对比给了我们一个重要启示。如果你的应用场景是处理 同一领域内、风格类似 的需求文档(例如,都是某公司的移动应用需求),HC4RC是非常可靠且高效的选择。但如果你的场景需要处理 跨多个不同领域、写作风格差异巨大 的需求文档,那么投入资源使用像NoRBERT这样的预训练模型,可能带来更好的泛化收益,尽管需要付出更多的计算成本。

5. 避坑指南与实战经验总结

在复现和改进HC4RC方法的过程中,我们踩过不少坑,也积累了一些宝贵的经验。

5.1 语义角色标注的陷阱与调优

问题1:SRL工具对非规范文本的识别误差 需求文本常包含不完整的句子、大量缩写、领域特定术语。标准SRL工具在这些文本上表现会打折扣。

  • 解决方案 :不要完全依赖自动化工具。可以构建一个小的、领域内的 句式模板库 。例如,对于性能需求,常见模式是“系统应在[时间]内响应[动作]”。可以先通过正则表达式匹配这类模式,提取关键成分,再对剩余文本使用SRL。这种规则与统计相结合的方法(Hybrid Approach)在实践中非常有效。

问题2:特征稀疏与信息损失 经过SRL过滤后,特征维度虽然下降,但某些重要的、但未落在核心语义角色上的词可能会被丢弃。例如,表达程度或约束的副词(如“高度安全”、“必须”)。

  • 解决方案 :采用 特征融合 策略。除了SRL特征,可以额外加入一小组手工设计的 领域关键词特征 句法模式特征 (如是否包含“必须”、“应避免”等情态动词)。在特征向量拼接后,可以再次使用方差过滤或基于模型的特征选择(如SelectFromModel)进行二次降维,以控制总维度。

5.2 数据集分解的策略选择

问题:如何确定最佳分组数量K? K值太小,分解不充分,子分类器内部可能依然不平衡;K值太大,层次结构变得复杂,根分类器的错误会逐级放大。

  • 解决方案 :这是一个需要验证的超参数。我们采用 轮廓系数(Silhouette Score) 分类性能验证 相结合的方法。首先,用不同K值对类别向量进行聚类,计算轮廓系数,选择系数较高的几个K值候选。然后,在这些候选K值下,运行一个简化的HC4RC流程(例如用5折CV),比较其宏平均F1分数。通常,K值在3到6之间是一个合理的探索范围。在我们的实验中,K=4取得了较好的平衡。

5.3 类别极度不平衡的应对

即使经过数据集分解,某些叶节点分类器内部仍可能存在严重不平衡(例如,一个组内有一个大类和一个极小类)。

  • 解决方案 :在 叶节点分类器层级 引入类别权重。在SVM中,可以设置 class_weight='balanced' ,让算法在优化时自动调整损失函数,给予少数类更高的权重。这比在全局应用过采样/欠采样更精细,副作用更小。

5.4 与现有工具链的集成

如何将HC4RC融入实际的需求管理流程?

  • 建议架构 :可以将其封装为一个微服务。输入是纯文本需求条目,输出是预测的类别及置信度。服务内部缓存训练好的根分类器和叶分类器模型,以及SRL特征提取器和向量化器。使用Flask或FastAPI框架可以快速搭建REST API。
  • 持续学习 :当有新的、人工标注好的需求数据积累时,可以定期(如每月)触发模型的增量训练或全量重新训练,使模型能够适应项目用语习惯的变化。

HC4RC为我们提供了一个解决需求多分类中类别不平衡和HDLSS问题的新视角。它没有追求最复杂的模型,而是通过巧妙的特征工程和问题重构,让经典的SVM模型焕发了新的活力。它在小类别识别上的优势、极高的计算效率以及稳定的泛化能力,使其特别适合作为企业级需求管理自动化工具中的核心分类模块。当然,它并非银弹,对于追求极致精度且拥有充足计算资源的场景,基于预训练模型的方法仍是首选。但在精度与效率的权衡中,HC4RC无疑占据了一个极具吸引力的位置。未来的工作可以探索将语义角色特征与深度学习模型(如BERT)的嵌入表示相结合,或者将层次分类结构与神经网络进行端到端的联合训练,或许能开辟出更优的路径。

Logo

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

更多推荐