数据分布差异鉴定指南

分析两个数据集(如 HelpSteer2 和 StackExchange)的分布差异性,需要从多个维度系统拆解它们在数据特征、内容属性、结构模式等方面的不同。

一、基本统计特征分布

从最基础的数值特征入手,反映数据集的 “规模” 和 “形态” 差异:
  1. 样本数量与规模分布
    • 总样本量:两个数据集的样本总数是否在同一量级(如 StackExchange 可能远大于 HelpSteer2)?
    • 子集合分布:若数据集包含子类别(如按领域、任务类型划分),各子集合的样本占比是否不同?(例如:StackExchange 可能按 “编程”“数学”“生活” 等领域划分,而 HelpSteer2 可能按 “求助类型” 划分,子集合的比例差异会直接影响分布)。
  2. 文本长度分布
    • 单个样本的文本长度(如字符数、词数、句子数):绘制长度直方图,对比两者的均值、中位数、标准差、极值(是否存在长尾分布?哪个数据集的文本更长 / 更短?)。
    • 若为对话 / 多轮数据:对比轮次分布(平均轮次、最大轮次),例如 StackExchange 多为 “单轮问答”,而 HelpSteer2 可能包含更多 “多轮对话”。

二、内容主题与领域分布

核心是分析两个数据集的 “内容聚焦方向” 差异:
  1. 主题领域划分
    • 手动或自动标注核心主题(如技术、教育、生活、情感、学术等),统计各主题在两个数据集中的占比(例如:StackExchange 以 “技术问答” 为主,HelpSteer2 可能更偏向 “日常求助” 或 “特定场景对话”)。
    • 用主题模型(如 LDA、BERTopic)挖掘潜在主题,对比主题数量、主题权重(哪个主题在 A 中更突出,在 B 中几乎不存在?)。
  2. 主题细粒度差异
    • 同一宏观主题下的细分方向:例如同属 “技术领域”,StackExchange 可能更多是 “代码调试”“工具使用”,而 HelpSteer2 可能是 “技术产品操作求助”。

三、语言风格与结构分布

反映数据集的 “表达形式” 差异:
  1. 语言正式度与口语化程度
    • 统计口语化词汇(如缩写、俚语、感叹词)的频率:StackExchange 作为社区问答,可能更偏向 “半正式”(需清晰表达问题),而 HelpSteer2 若为 “生活帮助对话”,可能更口语化。
    • 句式复杂度:平均句长、从句数量、被动语态占比(正式文本被动语态更多)。
  2. 文本结构模式
    • 问答结构:StackExchange 可能有明确的 “问题 - 回答” 配对(甚至带 “最佳答案” 标记),而 HelpSteer2 若为对话数据,可能是 “求助 - 回应 - 追问” 的流式结构。
    • 上下文依赖:样本是否依赖历史上下文(如多轮对话中,当前句子是否需要前序内容才能理解)?StackExchange 单轮问答的上下文依赖弱,而 HelpSteer2 可能更强。

四、实体与词汇分布

从 “微观词汇 / 实体” 层面看内容构成的差异:
  1. 词汇分布
    • 词频与词表差异:对比高频词表(前 100/1000 词),是否存在显著不同的核心词汇(如 StackExchange 可能高频出现 “问题”“解决方案”“代码”,HelpSteer2 可能高频出现 “怎么办”“帮我”“步骤”)。
    • 词汇多样性:用 “词汇丰富度指数”(如 TTR 类型 - token 比)衡量,哪个数据集的词汇更单一或更丰富?
  2. 实体类型分布
    • 统计专有名词(实体)的类型与频率:如 StackExchange 可能包含大量 “技术产品名”“编程语言名”,HelpSteer2 可能包含更多 “日常物品”“场景名”(如 “手机”“厨房”)。
    • 用命名实体识别(NER)工具标注实体(如人物、地点、组织、技术术语),对比实体类型的占比差异。

五、任务 / 功能类型分布

从 “数据的功能意图” 角度分析差异:
  1. 任务类型划分
    • 样本的核心意图:例如 StackExchange 的样本多为 “事实性问答”“问题解决”“知识科普”,而 HelpSteer2 可能更多是 “建议求助”“流程指导”“情感支持”。
    • 统计各任务类型的占比(如 “事实查询” 在 A 中占 60%,在 B 中占 20%)。
  2. 难度 / 复杂度分布
    • 文本难度:用可读性指数(如 Flesch-Kincaid 年级值)衡量,哪个数据集的文本更简单 / 更复杂?
    • 问题复杂度:例如 StackExchange 的技术问题可能包含 “多条件约束”,而 HelpSteer2 的求助可能更 “直接”。

六、相关性分析(补充维度)

相关性分析用于衡量两个数据集在上述维度上的 “重叠程度”,是分布差异的 “量化补充”:
  1. 主题相关性
    • 计算两个数据集的主题分布的余弦相似度或 Jaccard 系数:若某主题在两者中均高频出现,说明该主题相关性高;若几乎无重叠,说明主题差异大。
  2. 词汇 / 实体相关性
    • 对比高频词表的交集比例:若重叠词占比低,说明词汇分布差异大;反之则相似。
  3. 统计分布相关性
    • 对文本长度、轮次等数值特征,用皮尔逊 / 斯皮尔曼相关系数衡量分布趋势的相似性(如均呈右偏分布,但偏度不同)。

总结

分布差异性分析的核心是 “拆解维度→量化特征→对比形状”,而相关性分析是其中用于衡量 “重叠程度” 的工具。通过上述维度,可系统回答:两个数据集在 “说什么”“怎么说”“为了什么目的说” 等方面有何不同,从而为后续建模(如迁移学习、数据增强)提供依据(例如:若主题差异大,模型可能需要针对性适配)。 

代码实现:

import json
import csv
import os
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer
from scipy.stats import describe


# 下载NLTK资源(新增:添加punkt_tab的下载)
import nltk
nltk.download('punkt')
nltk.download('punkt_tab')  # 关键:解决缺失punkt_tab的错误
nltk.download('stopwords')
nltk.download('wordnet')
import matplotlib.pyplot as plt
plt.rcParams['font.family'] ='sans-serif'
plt.rcParams['font.sans-serif'] = ['SimSun']
plt.rcParams['axes.unicode_minus'] = False

def convert_helpsteer_to_reward_format(input_path, output_path):
    """将HelpSteer2的原始preference.jsonl转换为prompt\tchosen\trejected格式的TSV文件"""
    with open(input_path, "r", encoding="utf-8") as fin, \
         open(output_path, "w", encoding="utf-8", newline="") as fout:

        tsv_writer = csv.writer(fout, delimiter='\t')

        for line_num, line in enumerate(fin, 1):
            try:
                data = json.loads(line)
                prompt = data["prompt"]
                if data["preference_strength"] > 0:
                    chosen = data["response_2"]
                    rejected = data["response_1"]
                else:
                    chosen = data["response_1"]
                    rejected = data["response_2"]
                tsv_writer.writerow([prompt, chosen, rejected])
            except Exception as e:
                print(f"处理第{line_num}行失败: {e},已跳过")

    print(f"转换完成,输出文件: {output_path}")


def load_dataset(file_path, dataset_name):
    """加载数据集(手动解析避免EOF inside string错误)"""
    data = []
    error_lines = []

    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
        reader = csv.reader(
            f,
            delimiter='\t',
            quoting=csv.QUOTE_NONE,
            escapechar=None
        )

        for line_num, row in enumerate(reader, 1):
            try:
                if len(row) != 3:
                    raise ValueError(f"列数错误(预期3列,实际{len(row)}列)")
                
                prompt = re.sub(r'[\r\n]+', ' ', row[0].strip())
                chosen = re.sub(r'[\r\n]+', ' ', row[1].strip())
                rejected = re.sub(r'[\r\n]+', ' ', row[2].strip())
                
                if not (prompt and chosen and rejected):
                    raise ValueError("存在空值字段")
                
                data.append({
                    'prompt': prompt,
                    'chosen': chosen,
                    'rejected': rejected
                })

            except Exception as e:
                error_lines.append(f"行号 {line_num}:{str(e)}")
                continue

    df = pd.DataFrame(data)
    
    print(f"加载{dataset_name}完成:")
    print(f"  有效样本量:{len(df)}")
    print(f"  总行数:{line_num},跳过错误行:{len(error_lines)}")
    if error_lines:
        print(f"  前5个错误示例:{error_lines[:5]}")
    
    return df


def preprocess_text(text):
    """文本预处理:去特殊字符、分词、去停用词"""
    text = str(text).lower()
    text = re.sub(r'[^a-zA-Z0-9\s]', '', text)
    tokens = word_tokenize(text)
    stop_words = set(stopwords.words('english'))
    lemmatizer = WordNetLemmatizer()
    tokens = [lemmatizer.lemmatize(token) for token in tokens if token not in stop_words]
    return ' '.join(tokens)


def analyze_basic_stats(df, dataset_name, save_dir="plots"):
    """分析基础统计特征:长度分布、缺失值等"""
    os.makedirs(save_dir, exist_ok=True)

    # 计算文本长度(词数)
    df['prompt_len'] = df['prompt'].apply(lambda x: len(word_tokenize(str(x))))
    df['chosen_len'] = df['chosen'].apply(lambda x: len(word_tokenize(str(x))))
    df['rejected_len'] = df['rejected'].apply(lambda x: len(word_tokenize(str(x))))

    # 打印统计描述
    print(f"\n--- {dataset_name} 文本长度统计 ---")
    print("prompt长度:", describe(df['prompt_len']))
    print("chosen长度:", describe(df['chosen_len']))
    print("rejected长度:", describe(df['rejected_len']))

    # 绘制长度分布直方图
    plt.figure(figsize=(15, 5))
    for i, col in enumerate(['prompt_len', 'chosen_len', 'rejected_len']):
        plt.subplot(1, 3, i+1)
        sns.histplot(df[col], kde=True, bins=50)
        plt.title(f"{col} distribution ({dataset_name})")
        plt.xlabel("Word Count")
        plt.ylabel("Frequency")
    plt.tight_layout()
    plt.savefig(f"{save_dir}/{dataset_name}_length_dist.png")
    plt.close()

    return df

# ----------------------
# 3. 偏好标签差异分析(保持不变)
# ----------------------
def analyze_preference_diff(df, dataset_name, save_dir="plots"):
    """分析chosen与rejected的差异:长度差、语义相似度"""
    # 长度差(chosen - rejected)
    df['len_diff'] = df['chosen_len'] - df['rejected_len']

    # 语义相似度(用预训练模型计算)
    model = SentenceTransformer('all-MiniLM-L6-v2')
    chosen_emb = model.encode(df['chosen'].tolist(), show_progress_bar=True)
    rejected_emb = model.encode(df['rejected'].tolist(), show_progress_bar=True)
    df['similarity'] = [cosine_similarity([c], [r])[0][0] for c, r in zip(chosen_emb, rejected_emb)]

    # 可视化
    plt.figure(figsize=(12, 5))
    # 长度差分布
    plt.subplot(1, 2, 1)
    sns.histplot(df['len_diff'], kde=True, bins=50)
    plt.axvline(x=0, color='r', linestyle='--', label='0 (equal length)')
    plt.title(f"Chosen-Rejected Length Diff ({dataset_name})")
    plt.xlabel("Length Difference (words)")
    plt.legend()

    # 相似度分布
    plt.subplot(1, 2, 2)
    sns.histplot(df['similarity'], kde=True, bins=50, color='green')
    plt.title(f"Chosen-Rejected Similarity ({dataset_name})")
    plt.xlabel("Cosine Similarity")

    plt.tight_layout()
    plt.savefig(f"{save_dir}/{dataset_name}_preference_diff.png")
    plt.close()

    # 打印统计
    print(f"\n--- {dataset_name} 偏好差异统计 ---")
    print("长度差(chosen - rejected):", describe(df['len_diff']))
    print("语义相似度:", describe(df['similarity']))
    return df


# ----------------------
# 4. 主题分布分析(TF-IDF关键词)(保持不变)
# ----------------------
def analyze_topic_dist(df, dataset_name, top_n=20, save_dir="plots"):
    """用TF-IDF分析主题分布(高频关键词)"""
    # 预处理文本
    df['prompt_clean'] = df['prompt'].apply(preprocess_text)

    # TF-IDF提取关键词
    tfidf = TfidfVectorizer(max_features=1000)
    tfidf_matrix = tfidf.fit_transform(df['prompt_clean'])
    keywords = tfidf.get_feature_names_out()
    tfidf_scores = np.sum(tfidf_matrix.toarray(), axis=0)
    top_keywords = pd.Series(tfidf_scores, index=keywords).nlargest(top_n)

    # 可视化高频关键词
    plt.figure(figsize=(12, 6))
    sns.barplot(x=top_keywords.values, y=top_keywords.index)
    plt.title(f"Top {top_n} Keywords in Prompt ({dataset_name})")
    plt.xlabel("TF-IDF Score Sum")
    plt.tight_layout()
    plt.savefig(f"{save_dir}/{dataset_name}_top_keywords.png")
    plt.close()

    print(f"\n--- {dataset_name} 高频关键词 ---")
    print(top_keywords.index.tolist())
    return top_keywords


# ----------------------
# 5. 跨数据集差异对比(保持不变)
# ----------------------
def compare_datasets(df1, df2, name1, name2, save_dir="plots"):
    """对比两个数据集的核心差异"""
    # 1. 长度分布对比
    plt.figure(figsize=(15, 5))
    for i, col in enumerate(['prompt_len', 'chosen_len', 'rejected_len']):
        plt.subplot(1, 3, i+1)
        sns.kdeplot(df1[col], label=name1, fill=True)
        sns.kdeplot(df2[col], label=name2, fill=True)
        plt.title(f"{col} Distribution")
        plt.xlabel("Word Count")
        plt.legend()
    plt.tight_layout()
    plt.savefig(f"{save_dir}/length_compare.png")
    plt.close()

    # 2. 偏好差异对比(长度差、相似度)
    plt.figure(figsize=(12, 5))
    # 长度差对比
    plt.subplot(1, 2, 1)
    sns.kdeplot(df1['len_diff'], label=name1, fill=True)
    sns.kdeplot(df2['len_diff'], label=name2, fill=True)
    plt.axvline(x=0, color='r', linestyle='--')
    plt.title("Chosen-Rejected Length Diff Comparison")
    plt.xlabel("Length Difference")
    plt.legend()

    # 相似度对比
    plt.subplot(1, 2, 2)
    sns.kdeplot(df1['similarity'], label=name1, fill=True)
    sns.kdeplot(df2['similarity'], label=name2, fill=True)
    plt.title("Chosen-Rejected Similarity Comparison")
    plt.xlabel("Cosine Similarity")
    plt.legend()

    plt.tight_layout()
    plt.savefig(f"{save_dir}/preference_compare.png")
    plt.close()
    print("\n跨数据集对比图表已保存")



def main():
    # 1. 加载数据(替换为实际路径)
    stackexchange_path = "/data/team/lmq/prefenceModel/stack_exchange/data/reward/merged_reward_test.csv"
    helpsteer2_path = "/data/team/lmq/prefenceModel/HelpSteer2/preference/helpsteer_reward_data_test.csv"

    df_stack = load_dataset(stackexchange_path, "StackExchange")
    df_help = load_dataset(helpsteer2_path, "HelpSteer2")

    # 2. 基础统计分析
    df_stack = analyze_basic_stats(df_stack, "StackExchange")
    df_help = analyze_basic_stats(df_help, "HelpSteer2")

    
    # 3. 偏好标签差异分析
    df_stack = analyze_preference_diff(df_stack, "StackExchange")
    df_help = analyze_preference_diff(df_help, "HelpSteer2")

    # 4. 主题分布分析
    keywords_stack = analyze_topic_dist(df_stack, "StackExchange")
    keywords_help = analyze_topic_dist(df_help, "HelpSteer2")

    # 5. 跨数据集对比
    compare_datasets(df_stack, df_help, "StackExchange", "HelpSteer2")

    # 6. 计算主题相关性(关键词重叠度)
    common_keywords = set(keywords_stack.index) & set(keywords_help.index)
    print(f"\n--- 主题相关性分析 ---")
    print(f"共同高频关键词数量:{len(common_keywords)}")
    print(f"共同关键词:{list(common_keywords)[:10]}...")
    print(f"主题重叠度:{len(common_keywords)/len(set(keywords_stack.index)|set(keywords_help.index)):.2f}")

if __name__ == "__main__":
    main()

代码功能说明

  1. 数据加载:针对 StackExchange 的prompt\tchosen\trejected格式读取数据,支持 HelpSteer2 的同类格式(需确保路径正确)。
  2. 基础统计:分析 prompt/chosen/rejected 的长度分布(词数),通过直方图展示两个数据集的文本冗长程度差异。
  3. 偏好差异:计算 chosen 与 rejected 的长度差(反映 “回答是否越长越受偏好”)和语义相似度(反映 “偏好强度”—— 差异大则相似度低)。
  4. 主题分布:用 TF-IDF 提取高频关键词,对比两个数据集的核心主题(如 StackExchange 可能更多出现 “code”“error”,HelpSteer2 可能更多出现 “help”“task”)。
  5. 跨数据集对比:通过核密度图对比长度分布、偏好差异,并计算主题重叠度(相关性指标)。

结果解读方向

  • 长度分布:若 StackExchange 的 chosen 普遍比 rejected 长,而 HelpSteer2 中两者长度差异小,说明奖励模型从前者可能学到 “偏好更长回答”,从后者学到 “长度不是关键”。
  • 主题重叠度:若共同关键词少(如重叠度 < 0.3),说明主题差异大,训练奖励模型时需注意领域适配。
  • 偏好强度:若 StackExchange 的 chosen 与 rejected 语义相似度低(差异明显),而 HelpSteer2 相似度高(细微差异),说明前者更适合训练 “明确偏好”,后者适合 “精细偏好”。

6e224f3139f5aa5edd8e05e475d7fa18

 

458a75010e2c5b1a75ce1e969867d4db

 

posted on 2025-07-28 15:42  limingqi  阅读(107)  评论(0)    收藏  举报

导航