数据, 术→技巧

项目实践:正负样本文本的关键词提取

钱魏Way · · 16 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

项目简介:针对一标识的文本信息,抽取文本中的关键词,最后以词云的方式暂时关键词。数据集更有2列:text、flag。其中text是文本内容, flag样本标识(0或1)。

步骤一:对文本内容进行分词处理

这里采用的是结巴分词处理。这里使用了默认的词库,建议使用腾讯AI Lab中文词向量数据 生成自定义词库分词效果更佳。

import pandas as pd
import jieba

df = pd.read_csv("data/text-data.csv")
tokenized_docs = [jieba.lcut(text) for text in df['text']]

这里没有对分词特别注重的另外一个原因是因为第二步的合并高频词对,会减少分词精确性对最终的影响。

步骤二:使用 Phrases 检测并合并高频词对

合并高频的目的主要是为了在过滤停用词前避免丢失语意。

from gensim.models import Phrases
from gensim.models.phrases import Phraser

# 使用 Phrases 检测并合并高频词对
phrase_model = Phrases(tokenized_docs, min_count=5, threshold=0.4, scoring='npmi')
phraser = Phraser(phrase_model)

# 合并词组:将 ['没有', '看见'] → ['没有看见']
merged_docs = [[word.replace('_', '') for word in phraser[doc]] for doc in tokenized_docs]

这段代码的主要功能是使用gensim的Phrases和 Phraser模型,检测和合并文本中的高频词组(短语),将两个或多个词组合成一个词组。在自然语言处理中,某些词组(如“没有看见”)是固定搭配或具有特殊意义的组合,单独拆分为“没有”和“看见”可能会丢失上下文信息。gensim.models.Phrases 提供了一种方法,可以根据统计信息(如词频和词间关系)检测这些词组并将它们合并为一个整体。

构建 Phrases 模型

Phrases会分析输入的 tokenized_docs,统计文档中相邻词对的共现频率。它会检测哪些词对(如“没有 看见”)出现频率较高,并且它们的共现关系超过一定的阈值(由 threshold 和 scoring控制),将这些词对标记为短语。

参数解释:

  • min_count=5: 词对至少出现 5 次才会被考虑为候选短语。
  • threshold=0.4: 控制短语的合并阈值,分值高于该值的词对才会被合并。
    • 如果threshold 值较高,只有那些强关联的词对才会被合并;如果较低,则更多词对会被合并。
    • 如果希望合并更多短语(宽松的条件),可以将threshold 设置较低(如 1 或 0.5)。
    • 如果希望只合并非常明显的短语(严格的条件),可以将threshold 设置较高(如 10 或更高)。
    • 在使用scoring=’npmi’ 时,threshold`通常设置为 1 到 1.0。
  • scoring=’npmi’:
    • ‘default’或 None: 使用对数似然比(Log Likelihood Ratio, LLR)作为评分方法。
    • ‘npmi’: 使用标准化点互信息(Normalized Pointwise Mutual Information, NPMI)。
    • ‘pmi’: 使用点互信息(Pointwise Mutual Information, PMI)。

构建 Phraser 模型

Phraser 是 Phrases的轻量级版本,适合在大规模文档上快速应用。它将词组检测模型转化为一个可以直接用于处理文档的工具,减少内存占用。

使用 Phraser对 tokenized_docs 中的每个文档进行处理。

  • 对于每个文档,`phraser[doc]` 会将检测到的词组合并为一个词。例如:
    • 输入文档:`[‘没有’, ‘看见’, ‘什么’]`
    • 输出文档:`[‘没有看见’, ‘什么’]`

最终,merged_docs 是一个处理后的文档列表,所有检测到的词组都被合并了。

步骤三:统计词频,人工梳理停用词

先讲统计的词频导出为文件,然后人工进行梳理:

from itertools import chain
from collections import Counter

# merged_docs 是 [[词1, 词2, ...], [词1, 词2, ...], ...] 的嵌套列表,使用 chain 展平
all_words = chain.from_iterable(merged_docs)
word_freq = Counter(all_words)
word_freq_df = pd.DataFrame(word_freq.items(), columns=["word", "freq"]).sort_values(by="freq", ascending=False)
word_freq_df.to_csv("data/word_freq.csv", index=False, encoding="utf-8-sig")

以下为去除停用词等相关的处理流程:

import re

STOPWORDS_PATH = 'dic/stopwords.txt'
with open(STOPWORDS_PATH, 'r', encoding='utf-8') as f:
    stopwords = set(line.strip() for line in f)

def is_valid_word(word, stopwords=None):
    # 去除长度为1的词
    if len(word) <= 1:
        return False
    # 去除纯数字或数字+符号
    if re.fullmatch(r'\d+[\W_]*', word):
        return False
    # 去除停用词
    if word in stopwords:
        return False
    return True
 
df['processed'] = [[word for word in doc if is_valid_word(word, stopwords)] for doc in merged_docs]
df = df[df['processed'].map(len) > 0].copy()  # 检查列表是否非空,如果为空进行过滤。

步骤四:生成文本的TF-IDF特征

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

text_strings = [' '.join(words) for words in df['processed']]
tfidf = TfidfVectorizer(max_features=1500)
tfidf_features = tfidf.fit_transform(text_strings)

TF-IDF 是一种衡量词重要性的指标,能够捕捉文本中词的重要性。使用 TfidfVectorizer 将文本转化为 TF-IDF特征矩阵。

max_features参数:

  • 限制生成的特征数量最多为 1500(只保留 1500 个最重要的词)。
  • 如果文本中词汇量较大,限制特征数量可以减少计算量,同时避免过拟合。
  • 通过设置该参数,最终每个文本将被表示为一个长度为 1500 的特征向量。

步骤五:使用LightGBM对词进行分类训练

具体分类模型如下:

from sklearn.model_selection import train_test_split
import lightgbm as lgb
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
import seaborn as sns
import matplotlib.pyplot as plt

# 解决中文乱码问题
from matplotlib import rcParams
rcParams['font.family'] = 'SimHei'
rcParams['axes.unicode_minus'] = False


X_combined = np.concatenate([tfidf_features.toarray()], axis=1)
y = df['flag']
X_train, X_test, y_train, y_test = train_test_split(X_combined, y, test_size=0.2, random_state=42)

clf = lgb.LGBMClassifier(random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
y_pred_proba = clf.predict_proba(X_test)[:, 1]

print("模型评估结果:")
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(f"精确率: {precision_score(y_test, y_pred):.4f}")
print(f"召回率: {recall_score(y_test, y_pred):.4f}")
print(f"F1 分数: {f1_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")
print("分类报告:")
print(classification_report(y_test, y_pred))

conf_matrix = confusion_matrix(y_test, y_pred)
sns.heatmap(conf_matrix, annot=True, fmt="d", cmap="Blues", xticklabels=["负类", "正类"], yticklabels=["负类", "正类"])
plt.title("混淆矩阵")
plt.xlabel("预测")
plt.ylabel("实际")
plt.tight_layout()
plt.show()

这里没有对模型进行超参数优化,以下是使用optuna优化超参数,结果感觉没有什么提升。

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report
import lightgbm as lgb
import seaborn as sns
import matplotlib.pyplot as plt
import optuna

# 中文显示
from matplotlib import rcParams
rcParams['font.family'] = 'SimHei'
rcParams['axes.unicode_minus'] = False

# 数据准备(你的已有数据)
X_combined = np.concatenate([tfidf_features.toarray()], axis=1)
y = df['flag']
X_train, X_test, y_train, y_test = train_test_split(X_combined, y, test_size=0.2, random_state=42)

# Optuna目标函数
def objective(trial):
    param = {
        'n_estimators': trial.suggest_int('n_estimators', 50, 500),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3),
        'num_leaves': trial.suggest_int('num_leaves', 20, 150),
        'max_depth': trial.suggest_int('max_depth', 3, 15),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'random_state': 42,
        'n_jobs': -1
    }

    clf = lgb.LGBMClassifier(**param)
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    return f1_score(y_test, y_pred)

# 开始搜索
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50, show_progress_bar=True)

# 输出最佳参数
print("最佳参数:", study.best_params)

# 使用最佳参数训练最终模型
best_clf = lgb.LGBMClassifier(**study.best_params, random_state=42)
best_clf.fit(X_train, y_train)
y_pred = best_clf.predict(X_test)
y_pred_proba = best_clf.predict_proba(X_test)[:, 1]

# 模型评估
print("模型评估结果:")
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(f"精确率: {precision_score(y_test, y_pred):.4f}")
print(f"召回率: {recall_score(y_test, y_pred):.4f}")
print(f"F1 分数: {f1_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")
print("分类报告:")
print(classification_report(y_test, y_pred))

# 可视化混淆矩阵
conf_matrix = confusion_matrix(y_test, y_pred)
sns.heatmap(conf_matrix, annot=True, fmt="d", cmap="Blues", xticklabels=["负类", "正类"], yticklabels=["负类", "正类"])
plt.title("混淆矩阵")
plt.xlabel("预测")
plt.ylabel("实际")
plt.tight_layout()
plt.show()

步骤六:根据SHAP值展示TOP关键词

基于 SHAP 分析,将关键词按照平均 SHAP 值分为正向和负向两组并输出,同时过滤掉 SHAP 值在指定范围内的数据。

import shap
from wordcloud import WordCloud

def generate_wordcloud(weights, title, save_path):
    abs_weights = {word: abs(weight) for word, weight in weights.items()}
    wc = WordCloud(font_path='msyh.ttc', width=800, height=600, background_color='white')
    wc.generate_from_frequencies(abs_weights)
    plt.imshow(wc, interpolation='bilinear')
    plt.title(title)
    plt.axis('off')
    plt.tight_layout()
    plt.savefig(f'{save_path}{title}.png')
    plt.close()
    
def show_keywords_by_shap_values(model, X, feature_names, save_path, threshold=(-0.1, 0.1)):
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(X)
    shap_values = shap_values[1] if isinstance(shap_values, list) else shap_values  # 如果是分类问题,选择正类的 SHAP 值
    # 筛选 SHAP 值:将范围在 [-0.1, 0.1] 之间的数据设为 NaN
    filtered_shap_values = np.where((shap_values >= threshold[0]) & (shap_values <= threshold[1]), np.nan, shap_values)
    # 计算每个关键词特征的平均 SHAP 值(忽略 NaN)
    avg_shap_values = np.nanmean(filtered_shap_values, axis=0)
    feature_shap_map = dict(zip(feature_names, avg_shap_values))
    # 分组:正向关键词和负向关键词
    positive_keywords = {k: v for k, v in feature_shap_map.items() if v > 0}
    negative_keywords = {k: v for k, v in feature_shap_map.items() if v < 0}
    # 输出分组结果
    print("正向关键词 (平均 SHAP 值 > 0):")
    for k, v in sorted(positive_keywords.items(), key=lambda item: item[1], reverse=True):
        print(f"{k}: {v:.6f}")
    print("------")
    print("负向关键词 (平均 SHAP 值 < 0):")
    for k, v in sorted(negative_keywords.items(), key=lambda item: item[1]):
        print(f"{k}: {v:.6f}")

    # 生成词云
    generate_wordcloud(positive_keywords, "正向关键词", save_path)
    generate_wordcloud(negative_keywords, "负向关键词 ", save_path)


all_feature_names =  tfidf.get_feature_names_out().tolist()
PLOT_SAVE_PATH = './analysis_results/'
show_keywords_by_shap_values(clf, X_test, all_feature_names, PLOT_SAVE_PATH)

步骤七:模型指标提升,加入其他特征

使用TF-IDF特征的模型性能不太理想,尝试加入更多的特征。这里尝试使用语句的句向量进行优化。

from transformers import AutoTokenizer, AutoModel
import joblib
import torch

BERT_MODEL = 'shibing624/text2vec-base-chinese'
# 加载语义模型
def load_semantic_model(model_name=BERT_MODEL):
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModel.from_pretrained(model_name)
    model.eval()
    return tokenizer, model

tokenizer, semantic_model = load_semantic_model()

# 获取句向量嵌入  
def get_sentence_embeddings(texts, tokenizer, model, batch_size=32, max_length=64):  
    embeddings = []  
    for i in range(0, len(texts), batch_size):  
        batch = texts[i:i + batch_size]  
        inputs = tokenizer(batch, return_tensors="pt", padding=True, truncation=True, max_length=max_length)  
        with torch.no_grad():  
            outputs = model(**inputs)  
            emb = outputs.last_hidden_state[:, 0]  # 取 [CLS] token            embeddings.append(emb)  
    return torch.cat(embeddings).numpy() if embeddings else np.array([])

text_strings = [' '.join(words) for words in df['processed']]
semantic_features = get_sentence_embeddings(text_strings, tokenizer, semantic_model)  
# 处理时间比较长,将运营结果缓存下来
joblib.dump(semantic_features, 'cache/semantic_features.pkl')

X_combined = np.concatenate([semantic_features, tfidf_features.toarray()], axis=1)

其中加载了语意模型shibing624/text2vec-base-chinese。shibing624/text2vec-base-chinese 是一个开源的中文文本嵌入模型,旨在将中文句子或段落转换为高维向量表示(embeddings),以支持语义理解、相似度计算等自然语言处理任务。

模型基本信息

  • 开发者:由开发者shibing624 创建并维护,代码和模型权重已在 GitHub 和 Hugging Face 平台开源。
  • 类型:基于 Transformer 架构的预训练模型,结合后处理池化方法生成句子向量。
  • 语言:专门针对中文文本优化,支持简体、繁体及中英文混合内容。
  • 模型结构:基于类似 BERT 的架构(如BERT-base-Chinese),通过对模型输出的词向量进行均值池化(Mean Pooling)生成句子级表示。

核心功能

  • 文本嵌入:将输入文本转换为固定长度的向量(如 768 维),捕捉语义信息。
  • 语义相似度计算:通过余弦相似度等度量方式,衡量两段文本的相关性。
  • 下游任务支持:可作为特征提取器用于聚类、分类、检索等任务(如问答系统、推荐系统)。

性能表现

以下是 shibing624/text2vec-base-chinese 模型在常见中文语义相似度任务中的性能表现表格(数据基于公开测试结果及模型文档,部分为示例值,实际以官方为准):

数据集 任务类型 指标 text2vec-base-chinese BERT-base-Chinese TF-IDF/Word2Vec SOTA 模型
STS-B (中文) 语义相似度回归 Spearman 相关系数 75.2% 65.3% 58.1% 80.5% (SimBERT)
ATEC 句子对匹配 AUC-ROC 82.4% 78.6% 70.2% 85.0% (ERNIE)
BQ Corpus 语义相似度二分类 Accuracy (Acc) 86.7% 83.1% 76.9% 88.3% (MacBERT)
LCQMC 语义匹配二分类 F1 Score 89.5% 85.0% 79.8% 91.2% (RoBERTa-wwm)
PAWS-X (中文) 释义识别 Accuracy (Acc) 68.9% 63.4% 55.0% 72.1% (DeBERTa)
COLIEE (法律) 法律条文匹配 MAP@5 74.3% 70.5% 62.0% 76.8% (领域微调)

TODO:下次使用SimBERT模型进行实验下,是否效果更佳。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注