You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于DNN的多标签文本预测:置信度分数实现技术问询

解决多标签文本分类的置信度阈值与模型适配问题

这问题我之前做文本多标签分类项目时刚好碰到过,咱们一步步拆解解决:

一、先修正模型的核心问题:从单标签到多标签的输出层调整

你之前用softmax激活函数是单标签分类的思路——它会让所有标签的概率和为1,隐含了「标签互斥」的前提,但你的场景是文本可以对应多个主题,完全不互斥。所以必须做两个关键改动:

  • 输出层激活函数换成sigmoid:每个输出神经元独立输出0~1之间的概率值,代表该文本属于对应主题的置信度,彼此互不影响。
  • 损失函数换成binary_crossentropy:因为每个主题相当于一个独立的二分类问题(是/不是该主题),交叉熵要按二分类来计算,而不是多分类的categorical_crossentropy

用tflearn的代码示例:

# 替换原来的softmax输出层
net = tflearn.fully_connected(net, num_topics, activation='sigmoid')
# 替换损失函数
net = tflearn.regression(net, optimizer='adam', loss='binary_crossentropy')
model = tflearn.DNN(net)

二、先把非规范化数据转成多标签格式

你的数据是一个id对应多行(一个文本对应多个主题),必须先转换成「一行文本对应一个多标签矩阵」的格式,才能喂给多标签模型:

import pandas as pd
import numpy as np

# 读取原始数据
df = pd.read_csv("your_data.csv")

# 1. 建立所有主题的映射关系
all_topics = df["Topic"].unique()
topic2idx = {t:i for i,t in enumerate(all_topics)}
num_topics = len(all_topics)

# 2. 按id分组,合并同一文本的所有主题
grouped_df = df.groupby("id").agg({
    "Text": "first",  # 同一个id的Text重复,取第一个即可
    "Topic": lambda x: [topic2idx[t] for t in x]
}).reset_index()

# 3. 生成多标签的one-hot矩阵
labels = []
for topic_indices in grouped_df["Topic"]:
    label = np.zeros(num_topics, dtype=int)
    label[topic_indices] = 1
    labels.append(label)

# 现在 grouped_df["Text"] 是输入文本,labels 是对应的多标签矩阵

三、合理选择置信度阈值(拒绝盲目设定)

这是你最关心的问题,这里有几种实用的、数据驱动的方法:

1. 基于验证集的全局最优阈值

用预留的验证集,遍历不同阈值,找到能让模型F1分数(多标签分类的核心评估指标)最高的阈值:

from sklearn.metrics import f1_score

# 假设 val_features 是验证集特征,val_labels 是验证集多标签矩阵
val_probs = model.predict(val_features)

best_threshold = 0.5
best_f1 = 0.0

# 遍历0.1到0.9的阈值,步长0.05
for threshold in np.arange(0.1, 0.9, 0.05):
    # 把概率转为标签:大于阈值标记为1,否则0
    pred_labels = (val_probs > threshold).astype(int)
    # 计算macro-F1(对每个标签的F1取平均,适合样本不平衡的情况)
    current_f1 = f1_score(val_labels, pred_labels, average="macro")
    if current_f1 > best_f1:
        best_f1 = current_f1
        best_threshold = threshold

print(f"找到最优阈值: {round(best_threshold,2)},对应macro-F1: {round(best_f1,4)}")

2. 类别自适应阈值

如果不同主题的样本量差异很大(比如有的主题只有几十个样本,有的有几千个),全局阈值可能不够精准。可以给每个主题单独计算最优阈值:

topic_thresholds = {}

for topic_idx in range(num_topics):
    best_t_f1 = 0.0
    best_t_thresh = 0.5
    # 提取该主题的真实标签和预测概率
    true_labels = val_labels[:, topic_idx]
    pred_probs = val_probs[:, topic_idx]
    # 遍历阈值找最优
    for threshold in np.arange(0.1, 0.9, 0.05):
        pred_t = (pred_probs > threshold).astype(int)
        f1 = f1_score(true_labels, pred_t)
        if f1 > best_t_f1:
            best_t_f1 = f1
            best_t_thresh = threshold
    topic_thresholds[all_topics[topic_idx]] = best_t_thresh

# 预测时对每个主题用对应的阈值

3. 基于样本的动态阈值

如果你的文本对应的主题数量波动不大,可以用「取每个样本预测概率前k个标签」的方式,k设为训练集中文本的平均主题数量。比如训练集平均每个文本对应2个主题,那预测时就取该样本置信度最高的前2个主题。

四、预测阶段的落地

用训练好的模型得到每个主题的置信度后,用上面选好的阈值筛选即可:

# 假设 test_features 是预处理后的测试文本特征
test_probs = model.predict(test_features)

# 用全局最优阈值的情况
predicted_topics = []
for prob in test_probs:
    # 找到置信度大于阈值的主题索引
    topic_ids = np.where(prob > best_threshold)[0]
    # 转成原始主题名称
    topics = [all_topics[idx] for idx in topic_ids]
    predicted_topics.append(topics)

额外小建议

  • 特征工程:Bag of Words可以换成TF-IDF,能降低高频无意义词的权重;如果数据量足够,试试用预训练词嵌入(比如Word2Vec、GloVe)作为输入,比BoW更能捕捉语义信息。
  • 模型选型:除了DNN,也可以试试LightGBM/XGBoost的多标签版本,或者用sklearn的MultiOutputClassifier包装单分类模型,有时候训练速度更快、效果也不错。

内容的提问来源于stack exchange,提问作者Kertis van Kertis

火山引擎 最新活动