1. 项目概述与核心挑战

在软件工程领域,需求工程是决定项目成败的基石。一个清晰、完整且分类明确的需求文档,是后续设计、开发和测试工作的蓝图。然而,现实中的需求文档往往是自然语言文本的集合,动辄数百条,人工逐条分类不仅耗时耗力,而且容易因主观判断产生不一致。过去十几年,机器学习技术被引入来解决这个问题,目标是训练一个模型,让它能像经验丰富的分析师一样,自动将一条条需求语句归类到“功能性需求”、“性能需求”、“安全性需求”等类别中。

听起来很美好,对吧?但真正动手做过的同行都知道,这里头有两个“老大难”问题,就像两座大山一样横在面前,让很多先进的模型也栽了跟头。第一个是 类别不平衡 。想想看,一个典型的软件项目里,“功能性需求”的数量可能占到了70%以上,而像“可维护性”、“可移植性”这类非功能性需求可能只占个位数百分比。用这样头重脚轻的数据去训练模型,模型自然会倾向于把所有东西都预测为“功能性需求”,因为这样它的准确率看起来最高,但对少数类别的识别能力几乎为零。

第二个是 高维小样本 问题。我们把每条需求文本转换成机器能理解的数字特征(比如词袋模型),特征维度(词汇表大小)轻松上千甚至上万。但我们的训练样本(已标注的需求条目)可能只有几百条。这就好比你要用几百个点去描绘一个上万维空间中的复杂边界,模型极其容易“过拟合”——它在训练集上表现完美,但遇到新的、没见过的需求时就懵了。更糟糕的是,高维空间会放大类别不平衡的影响,让少数类样本在浩瀚的特征海洋里更加“孤立无援”。

我见过不少团队尝试用传统的过采样(给少数类“造”数据)或降维方法,效果总是不尽人意。过采样造出来的数据可能很“假”,反而误导模型;而降维如果只依赖词频(比如TF-IDF),可能会丢掉那些低频但关键的专业术语。正是在这种背景下,我们团队提出了 HC4RC 方法。它不是某个单一算法的魔改,而是一套组合拳,核心思路是: 不从“词”的表面频率入手,而从“语义”的深层结构去抓特征;不硬刚一个不平衡的多分类问题,而是把它巧妙地拆解成几个相对平衡的子问题 。下面,我就把这套方法的里里外外、实操细节以及我们踩过的坑,毫无保留地分享出来。

2. HC4RC方法的三重核心设计解析

HC4RC的完整流程可以看作一个精密的处理流水线,它主要由三个创新性的技术模块串联而成:基于语义角色的特征选择、数据集分解和层次分类。这三个模块并非孤立,而是环环相扣,共同应对前述的挑战。

2.1 语义角色特征选择:从“词频”到“语义”的降维革命

传统的文本特征选择,无论是N-gram还是TF-IDF,其本质都是基于统计学上的词频或共现。这在通用文本上或许有效,但在领域特定的需求文本中,问题很大。例如,“系统应在95%的情况下保证响应时间小于2秒”这条性能需求。关键词“95%”和“2秒”的出现频率可能极低,但它们对分类的决定性作用远超高频词“系统”或“保证”。基于词频的方法很可能将这些关键数字过滤掉。

我们的SR4FS技术跳出了词频的框架,转向了语言学中的 语义角色 理论。简单说,我们不再关心一个词出现了多少次,而是关心这个词在句子中“扮演什么角色”。这就像分析一个剧本,我们不统计演员名字出现了几次,而是看谁是“主角”(施事者)、干了什么“动作”、动作的“对象”是什么、为了什么“目标”、以何种“方式”、达到什么“度量”。

我们定义了六个最贴合需求陈述的语义角色:

  1. 施事者 :动作的发起者,通常是句子的主语,如“ 系统 应发送邮件”。
  2. 动作 :核心行为,由动词承担,如“系统应 发送 邮件”。
  3. 主题 :动作的直接承受者或对象,通常是直接宾语,如“系统应发送一封 验证邮件 ”。
  4. 目标 :动作的间接对象或最终目的,通常是间接宾语或介词宾语,如“发送给 用户 ”。
  5. 方式 :描述动作发生的方式或条件,通常由形容词、副词或介词短语体现,如“ 以加密方式 发送”。
  6. 度量 :对动作或状态的量化描述,通常是数字、百分比或程度副词,如“ 95% 的可用性”、“响应时间 小于2秒 ”。

为什么是这六个角色? 因为它们几乎能完整回答需求分析中的核心问题: 谁(施事者),做什么(动作),对什么(主题),为谁/何目的(目标),如何做(方式),做多少(度量) 。功能性需求更关注“施事者-动作-主题”这个主干(谁对什么做了什么),而非功能性需求则更依赖于“方式”和“度量”来体现质量属性(如何做,做到多好)。

实操中,我们利用现成的NLP工具(如spaCy)来自动化识别这些角色。过程分两步:

  1. 句法分析 :对每条需求进行词性标注、依存句法分析和命名实体识别。
  2. 角色映射 :根据预定义的规则(例如,如果某个词是主动词的主语,则映射为“施事者”;如果是数字或百分比实体,则映射为“度量”),将句法成分映射到上述六个语义角色上。

注意 :完全依赖自动化工具会有误差。特别是命名实体识别,对“2秒”、“98%”这类领域相关实体的识别可能不准。因此, 我们强烈建议在关键项目或标注数据较少时,加入一道人工校验工序 。检查并修正自动标注的结果,虽然增加了初期工作量,但能极大提升特征质量,这个时间投资是值得的。

通过SR4FS,我们将每条需求从成千上万个稀疏的词特征,压缩到仅由这六个语义角色所承载的、稠密的、富含语义信息的特征集合上。这直接、有效地打击了“高维”问题,并且因为特征基于语义而非词频,它对数据量的依赖更小,缓解了“小样本”问题。

2.2 数据集分解:化整为零的平衡策略

面对一个严重倾斜的数据集(例如,10个类别,其中1个类占60%样本),直接训练一个多分类器是不明智的。数据集分解的思路是: 我们不直接处理这个不平衡的多分类问题,而是先把它转化成一个相对平衡的“二分类”问题

具体操作步骤如下:

  1. 排序 :将所有类别按样本数量从多到少排序。
  2. 划分多数类集 :从样本最多的类别开始,将其所有样本标记为“Maj”(多数类集),然后加入次多类别的样本,持续这个过程,直到“Maj”集合中的总样本数 超过或等于 整个训练集样本数的一半。此时,这些被选中的类别构成了“多数类子集”。
  3. 剩余即少数类集 :所有未被选入的类别,其样本被统一标记为“Min”(少数类集),形成“少数类子集”。

这个过程的关键在于,它不是一个物理上的硬拆分,而是一种 软标签 。原始数据集仍然完整,但每条需求都多了一个“Maj/Min”的层级标签。通过这种划分,我们得到了两个内部类别数量差异仍然存在、但 集合间样本总量大致平衡 的子问题。原来那个困难的“1对多”不平衡问题,变成了一个相对简单的“多数类集 vs 少数类集”的二分类问题,以及两个内部相对更容易处理的子多分类问题。

2.3 层次分类:构建分类决策树

有了分解后的数据集和对应的层级标签,我们就可以构建一个两层的分类器层次结构,这很像一个决策树:

  1. 根分类器 :训练一个 二分类器 。它的任务很简单:给定一条需求,判断它属于“多数类集”还是“少数类集”。这个分类器只使用我们之前提取的语义角色特征。
  2. 叶分类器
    • 如果根分类器判定为“多数类集”,则该需求被送入 多数类多分类器 。这个分类器负责在多数类子集包含的那些具体类别(如“功能性”、“性能”、“安全性”)中进行精细分类。
    • 如果判定为“少数类集”,则送入 少数类多分类器 。这个分类器负责在剩余的那些低频类别(如“可维护性”、“可移植性”、“合规性”)中进行分类。

这个层次结构的精妙之处在于,它将一个复杂的、不平衡的任务,分解成了三个更简单、更平衡的子任务。根分类器像一个“路由员”,先把需求分流到不同的“车间”;“车间”内的分类器则专心处理自己范围内类别分布相对均衡的任务。这样做,每个分类器都不需要直面全局的严重不平衡,从而更容易学到有效的分类边界。

3. 从理论到实践:HC4RC的实现与调优细节

纸上谈兵终觉浅,绝知此事要躬行。有了清晰的设计思路,接下来就是具体的工程实现。这里我分享我们的技术栈、关键步骤的实现代码片段,以及那些在论文里可能一笔带过、但却至关重要的调参和优化经验。

3.1 技术栈与预处理流水线

我们选择Python作为实现语言,因为它拥有丰富且成熟的机器学习与NLP生态。核心库包括:

  • spaCy :用于完成词性标注、依存句法分析和命名实体识别。它的精度和效率在工业界得到了广泛验证。
  • scikit-learn :用于构建和训练分类器(如SVM、随机森林),以及处理特征向量化、模型评估等。
  • NLTK :辅助进行文本预处理,如词形还原。

文本预处理 是第一步,也是保证后续步骤质量的基础。我们的标准化流水线如下:

  1. 分词 :将句子拆分成单词或标点符号单元。
  2. 转为小写 :消除大小写带来的差异。
  3. 词形还原 :将单词还原为其词典原形(如“running” -> “run”),这比词干提取更准确。
  4. 移除停用词和短词 :移除“the”,“a”,“is”等无实义的停用词,以及长度小于3的字符(通常噪音较多)。
import spacy
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords
import string

nlp = spacy.load(‘en_core_web_sm’)
lemmatizer = WordNetLemmatizer()
stop_words = set(stopwords.words(‘english’))

def preprocess_text(text):
    # 使用spacy进行分词和词性标注
    doc = nlp(text)
    processed_tokens = []
    for token in doc:
        # 转为小写,进行词形还原,移除停用词、标点和短词
        lemma = lemmatizer.lemmatize(token.text.lower())
        if (lemma not in stop_words and
            lemma not in string.punctuation and
            len(lemma) > 2):
            processed_tokens.append(lemma)
    return ‘ ‘.join(processed_tokens)

# 示例
raw_req = “The system shall encrypt user passwords before storing them in the database.”
clean_req = preprocess_text(raw_req) # 输出:’system shall encrypt user password before store database’

3.2 语义角色特征提取的实现

这是HC4RC的核心。我们基于spaCy的解析结果,编写规则来提取六大语义角色。

def extract_semantic_roles(doc):
    roles = {‘Agent’: [], ‘Action’: [], ‘Theme’: [], ‘Goal’: [], ‘Manner’: [], ‘Measure’: []}
    for token in doc:
        # 1. 施事者 (Agent): 通常是主动词的主语
        if token.dep_ == ‘nsubj’ or token.dep_ == ‘nsubjpass’:
            roles[‘Agent’].append(token.text)
        # 2. 动作 (Action): 动词
        elif token.pos_ == ‘VERB’:
            roles[‘Action’].append(token.lemma_) # 使用词元
        # 3. 主题 (Theme): 通常是直接宾语(dobj)
        elif token.dep_ == ‘dobj’:
            roles[‘Theme’].append(token.text)
        # 4. 目标 (Goal): 间接宾语(dative)或介词宾语(pobj),且介词是’to’, ‘for’等
        elif token.dep_ == ‘dative’ or (token.dep_ == ‘pobj’ and token.head.text.lower() in [‘to’, ‘for’]):
            roles[‘Goal’].append(token.text)
        # 5. 方式 (Manner): 副词(ADV)、形容词(ADJ)或特定介词短语
        elif token.pos_ in [‘ADV’, ‘ADJ’]:
            # 简单起见,这里将副词和形容词都归为方式,实际可根据更细的依存关系区分
            roles[‘Manner’].append(token.text)
        elif token.dep_ == ‘prep’ and token.text.lower() in [‘by’, ‘with’, ‘using’, ‘via’]:
            # 提取整个介词短语作为方式
            phrase = ‘ ‘.join([child.text for child in token.subtree])
            roles[‘Manner’].append(phrase)
        # 6. 度量 (Measure): 数字、百分比或数量词
        elif token.ent_type_ in [‘CARDINAL’, ‘PERCENT’, ‘QUANTITY’]:
            roles[‘Measure’].append(token.text)
        elif token.text in [‘fully’, ‘completely’, ‘partially’]: # 一些程度副词
            roles[‘Measure’].append(token.text)
    # 将每个角色的词列表合并成字符串,作为该角色的特征值
    for role in roles:
        roles[role] = ‘ ‘.join(roles[role]) if roles[role] else ‘NONE’
    return roles

# 示例
doc = nlp(“The system must encrypt data with AES-256 algorithm.”)
features = extract_semantic_roles(doc)
print(features)
# 输出: {‘Agent’: ‘system’, ‘Action’: ‘encrypt’, ‘Theme’: ‘data’, ‘Goal’: ‘NONE’,
#        ‘Manner’: ‘with AES-256 algorithm’, ‘Measure’: ‘NONE’}

实操心得 :这里的映射规则是工程上的关键。我们定义的规则是基于通用语法和需求文本特点的启发式规则。 它不可能100%准确 。例如,把形容词都归为“方式”可能过于粗糙,因为有些形容词可能修饰主题(如“敏感数据”)。在实际项目中,你需要根据你的需求语料特点,反复迭代和优化这些规则。一个有效的方法是,随机抽样几百条需求,人工检查自动提取的结果,针对常见的错误模式调整规则。

3.3 数据集分解与层次分类器的训练

特征提取后,每条需求被表示为一个6维的语义角色特征向量(每个角色对应一个字符串)。接下来是构建层次模型。

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report

# 假设我们有以下数据
# X_train_raw: 原始需求文本列表
# y_train_flat: 原始类别标签(如 ‘FR’, ‘PERFORMANCE’, ‘USABILITY’…)
# y_train_hier: 层级标签 (‘maj’ 或 ‘min’),由数据集分解步骤生成

# 步骤1: 为每个语义角色单独拟合TF-IDF向量器
# 这是因为不同角色的词汇空间不同,分开处理更合理
role_names = [‘Agent’, ‘Action’, ‘Theme’, ‘Goal’, ‘Manner’, ‘Measure’]
role_vectorizers = {}
role_features_train = []

for role in role_names:
    # 收集该角色在所有训练样本上的取值
    role_texts = [extract_semantic_roles(nlp(req))[role] for req in X_train_raw]
    vectorizer = TfidfVectorizer(max_features=100) # 限制每角色最大特征数,进一步降维
    vectorizer.fit(role_texts)
    role_vectorizers[role] = vectorizer

# 步骤2: 为每条需求构建最终特征向量(拼接所有角色的TF-IDF向量)
def build_final_feature_vector(raw_text, role_vectorizers):
    doc = nlp(raw_text)
    semantic_features = extract_semantic_roles(doc)
    final_vec = []
    for role in role_names:
        role_text = semantic_features[role]
        vec = role_vectorizers[role].transform([role_text]).toarray().flatten()
        final_vec.extend(vec)
    return final_vec

X_train_final = [build_final_feature_vector(req, role_vectorizers) for req in X_train_raw]

# 步骤3: 训练根分类器(二分类,Maj vs Min)
root_clf = RandomForestClassifier(n_estimators=100, class_weight=‘balanced’)
root_clf.fit(X_train_final, y_train_hier) # y_train_hier 是 ‘maj’/‘min’ 标签

# 步骤4: 分别准备多数类集和少数类集的训练数据
X_train_maj = [X_train_final[i] for i in range(len(X_train_final)) if y_train_hier[i] == ‘maj’]
y_train_maj = [y_train_flat[i] for i in range(len(X_train_flat)) if y_train_hier[i] == ‘maj’] # 具体类别

X_train_min = [X_train_final[i] for i in range(len(X_train_final)) if y_train_hier[i] == ‘min’]
y_train_min = [y_train_flat[i] for i in range(len(X_train_flat)) if y_train_hier[i] == ‘min’] # 具体类别

# 步骤5: 训练叶分类器(多分类)
leaf_clf_maj = SVC(kernel=‘linear’, class_weight=‘balanced’, decision_function_shape=‘ovr’)
leaf_clf_maj.fit(X_train_maj, y_train_maj)

leaf_clf_min = RandomForestClassifier(n_estimators=50, class_weight=‘balanced’)
leaf_clf_min.fit(X_train_min, y_train_min)

分类器选型思考 :为什么根分类器用随机森林,而叶分类器用了SVM?这是基于实验的权衡。随机森林对特征尺度和分布不敏感,且能提供特征重要性,适合做初步的“路由”决策。SVM在特征维度不高、样本量相对适中时,对于寻找清晰分类边界有优势,适合在分解后的相对平衡子集上做精细分类。当然, 这没有定论 ,你可以尝试逻辑回归、XGBoost等,关键是通过交叉验证来选择。

3.4 预测流程

训练好模型后,对新需求进行分类是一个层级决策过程:

def hc4rc_predict(new_requirement_text, role_vectorizers, root_clf, leaf_clf_maj, leaf_clf_min):
    # 1. 预处理和特征提取
    new_feature_vec = build_final_feature_vector(preprocess_text(new_requirement_text), role_vectorizers)
    # 2. 根分类器决策
    root_prediction = root_clf.predict([new_feature_vec])[0]
    # 3. 叶分类器决策
    if root_prediction == ‘maj’:
        final_label = leaf_clf_maj.predict([new_feature_vec])[0]
    else:
        final_label = leaf_clf_min.predict([new_feature_vec])[0]
    return final_label

# 示例预测
new_req = “User passwords shall be hashed using bcrypt before persistence.”
predicted_category = hc4rc_predict(new_req, role_vectorizers, root_clf, leaf_clf_maj, leaf_clf_min)
print(f”Predicted category: {predicted_category}”) # 可能输出: ‘SECURITY’

4. 效果评估、对比与实战中的挑战

我们设计实验,将HC4RC与三种有代表性的方法进行了对比:

  1. 基准方法 :使用传统TF-IDF特征 + 标准多分类SVM。这代表了最常用的基线。
  2. K&M方法 :一篇经典论文中提出的方法,它使用了N-gram和句法特征,并针对最主要的类别不平衡问题(可用性 vs 其他)进行了随机过采样和欠采样。
  3. 深度学习基线 :使用预训练语言模型(如BERT)的句向量作为特征,接一个全连接层进行分类。这代表了当前较先进的深度学习方法。

我们在公开的需求数据集(如PROMISE NFR)上进行了测试。评估指标不仅看整体准确率,更关注 宏平均F1分数 ,因为它对少数类的性能更敏感。

实验结果简要总结

  • 在整体准确率上 ,HC4RC与深度学习基线方法相差无几,有时略胜一筹,均显著优于传统TF-IDF和K&M方法。
  • 在宏平均F1分数上 ,HC4RC的表现最为突出。这意味着它对那些样本量少的“冷门”需求类别(如“可移植性”、“合规性”)的识别能力更强。传统方法在这些类别上的F1值常常接近0,而HC4RC能将其提升到可接受的范围内(例如0.4-0.6)。
  • 模型复杂性与可解释性 :HC4RC的训练和预测速度远快于需要微调的大型深度学习模型。更重要的是,基于语义角色的特征具有 极佳的可解释性 。当模型将一个需求分类为“安全性”时,我们可以回溯发现,是因为它提取到了“动作”角色中的“加密”、“哈希”,以及“方式”角色中的“AES-256”、“bcrypt”等关键词。这对于需要向领域专家或客户解释分类结果的场景至关重要。

4.1 实战中遇到的典型问题与调优技巧

  1. 语义角色提取的边界情况 :自然语言千变万化。例如,“系统需易于使用”中,“易于使用”整体作为形容词短语表“方式”,但依赖解析可能将其拆散。我们的策略是: 以规则为主,辅以一个小型的模式匹配词典 。对于“easy to use”、“user-friendly”这类常见表述,直接进行模式匹配和整体抽取,优先级高于通用语法规则。

  2. 根分类器的错误传播 :如果根分类器将一条本属于少数类集的需求误判到多数类集,那么后续的叶分类器几乎不可能纠正这个错误。为了缓解这个问题,我们 为根分类器设置了预测概率阈值 。例如,只有当“多数类集”的预测概率超过0.7时,才将其路由到多数类分类器;否则,将其路由到少数类分类器。这相当于在根节点增加了一个“不确定”缓冲区,虽然可能增加少数类分类器的负担,但降低了灾难性误判的风险。

  3. 极度稀疏的少数类 :即使经过分解,少数类集中可能仍存在某个类别只有个位数样本。对于这种情况,单靠HC4RC的结构优化是不够的。我们的补充策略是: 为这些“极少数类”建立基于关键词的规则后备 。例如,如果某个类别在历史数据中总是包含“GDPR”、“合规”等词,我们可以添加一条简单规则:若文本中出现这些关键词,则直接分类到该类别。这是一种“模型为主,规则为辅”的混合策略。

  4. 新领域适配 :当将训练好的模型应用于一个全新领域(如从金融软件切换到医疗设备)时,语义角色本身是通用的,但角色下的词汇分布会变化。这时, 不需要重新设计特征 ,只需要用新领域的少量标注数据,重新训练TF-IDF向量器和分类器即可,实现了较好的领域迁移能力。

5. 方法局限性与未来扩展方向

没有任何方法是银弹,HC4RC也不例外。清楚地认识到它的边界,才能更好地应用它。

主要局限性

  • 对句法分析工具的依赖 :SR4FS的准确性受限于底层NLP工具(如spaCy)的精度。对于语法不规范、口语化或非常复杂的长句,角色提取可能出错。
  • 规则维护成本 :虽然核心角色只有六个,但针对特定领域语言的映射规则可能需要微调和维护,这需要一定的语言学或领域知识。
  • 信息损失 :将丰富的文本压缩到六个角色,必然会损失一些细节信息。例如,复杂的逻辑关系(“如果…那么…”)或否定语义(“不应…”)在当前的框架中捕捉不足。

可行的扩展方向

  1. 与深度学习特征融合 :可以将语义角色特征与BERT等模型生成的上下文向量进行融合。语义角色提供结构化的、可解释的强特征,而BERT向量提供深层的语义关联。两者结合,可能达到更好的效果。
  2. 更精细的层次结构 :当前是两层结构。对于类别特别多的场景,可以构建更深的树状层次,例如先区分“功能性/非功能性”,再在“非功能性”下区分“运行期质量/演化期质量”,然后再细分。这需要领域知识的指导。
  3. 主动学习集成 :在标注数据稀缺的场景下,可以结合主动学习。让模型筛选出那些它最“不确定”的需求(例如,根分类器概率接近0.5的,或叶分类器概率分布很平缓的),交给人类专家标注,然后迭代更新模型。这样可以高效地利用有限的标注预算。

回过头看,HC4RC的成功并不在于用了多么高深的算法,而在于它 巧妙地转换了问题的视角 。当别人在纠结如何平衡数据或设计更复杂的网络结构时,我们选择回到需求文本的语义本质,并用层次化的思想分解了任务。这套方法带给我的最大启发是:在解决工程问题时,有时对问题本身的深刻理解和巧妙重构,比一味追求模型的复杂度更有效。它可能不是精度最高的方法,但在准确性、效率、可解释性和数据需求之间,取得了非常好的平衡,非常适合作为工业界需求自动化分类的一个可靠起点。如果你正在受困于混乱的需求文档,不妨试试这套思路,从梳理需求的语义结构开始。

Logo

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

更多推荐