3
0

提高篇:大数据集分析预处理

2026-05-15
2026-05-15
提高篇:大数据集分析预处理

第0章 前言

0.1 为什么需要数据预处理?

在数学建模和数据分析竞赛中,原始数据往往是"脏"的:缺失值、异常值、高维稀疏特征、类别不平衡……这些都会让后续的建模过程举步维艰。

数据预处理是连接原始数据与有效模型之间的桥梁。 它不是建模的附属步骤,而是决定模型上限的关键环节——正如一句老话所说:

Garbage in, garbage out.

本教程以经典的 Adult/Census Income 数据集为载体,系统地演示一套从原始表格到高质量特征向量的完整预处理流水线。这套方法不仅适用于竞赛数据,也可以推广到电商交易分析、日志挖掘、用户行为分析等场景。

0.2 数据集介绍

Adult 数据集 提取自 1994 年美国人口普查数据库,包含 48,842 条记录(训练集 32,561 条,测试集 16,281 条)。每条记录有 14 个属性:

属性

类型

说明

示例值

age

连续

年龄

39

workclass

离散

工作类别

Private, Self-emp-not-inc, Federal-gov...

fnlwgt

连续

最终权重(抽样权重)

77516

education

离散

教育程度

Bachelors, HS-grad, Masters...

education-num

连续

受教育年限

13

marital-status

离散

婚姻状况

Never-married, Married-civ-spouse...

occupation

离散

职业

Adm-clerical, Exec-managerial...

relationship

离散

家庭关系

Husband, Wife, Own-child...

race

离散

种族

White, Black, Asian-Pac-Islander...

sex

离散

性别

Male, Female

capital-gain

连续

资本收益

2174

capital-loss

连续

资本损失

0

hours-per-week

连续

每周工作小时数

40

native-country

离散

原籍国家

United-States, China, India...

预测任务:判断个人年收入是否超过 50K(二分类问题)。

ch00_data_overview.png

从上图可以看到:

  • workclass 以 Private 为主(约 2.2 万人)

  • education 中 HS-grad(高中毕业)和 Some-college 占比最高

  • occupation 中 Prof-specialty、Exec-managerial 和 Craft-repair 是前三大类

  • native-country 极度不均衡,United-States 占绝大多数

0.3 技术路线图

本教程的预处理流水线分为六个阶段,如下图所示:

ch00_pipeline.png

各阶段的核心目标:

阶段

方法

目标

数据预处理

离散化 + 序列化

将表格数据转化为"事务-项"格式

频繁项集挖掘

Apriori / FP-Growth

发现高频属性组合,过滤低频噪声

嵌入学习

Word2Vec / GloVe / FastText

将离散符号映射为稠密低维向量

降维可视化

t-SNE / UMAP

将嵌入降至 2D,直观验证嵌入质量

深度降维

自编码器

进一步压缩特征,提升泛化能力

下游任务

分类 / 聚类

验证预处理效果

0.4 核心思路:把表格当文本处理

传统统计学方法(如逻辑回归、决策树)直接处理表格数据。本教程采用一种创新的交叉视角

将每一行数据视为一个"句子",将每个属性值视为一个"词"。

例如,第 1 行数据:

39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, ...

转化为 token 序列后变为:

[age:Young, workclass:State-gov, education:Bachelors, marital:Never-married, occupation:Adm-clerical, ...]

这样做的好处:

  1. 频繁项集挖掘可以发现哪些属性值经常一起出现(如 "Bachelors + Prof-specialty + >50K")

  2. 嵌入学习可以捕获属性值之间的语义关系(如 "Bachelors" 和 "Masters" 的向量会很接近)

  3. 降维可视化可以直观展示高维特征空间的结构

0.5 环境准备

本教程使用 Python 3.13,主要依赖以下库:

pandas>=2.0        # 数据处理
numpy>=1.24        # 数值计算
matplotlib>=3.7    # 可视化
mlxtend>=0.23      # Apriori / FP-Growth
gensim>=4.3        # Word2Vec / FastText / GloVe
scikit-learn>=1.3  # t-SNE / 评估指标
umap-learn>=0.5    # UMAP 降维
torch>=2.0         # 自编码器

0.6 本节小结

  • Adult 数据集包含 32,561 条训练记录、15 个属性(14 个特征 + 1 个标签)

  • 核心思路是将每行数据序列化为 token 序列,从而应用 NLP 方法

  • 预处理流水线:数据清洗 → 频繁项集 → 嵌入学习 → 降维可视化 → 深度降维 → 下游验证

  • 下一章将详细讲解数据预处理的具体操作

第1章 数据预处理与序列化

1.1 加载数据

Adult 数据集分为训练集和测试集两个文件。我们首先将它们合并,并统一处理:

import pandas as pd

cols = ['age','workclass','fnlwgt','education','education_num','marital_status',
        'occupation','relationship','race','sex','capital_gain','capital_loss',
        'hours_per_week','native_country','income']

df_train = pd.read_csv('adult.data', names=cols, na_values=' ?', skipinitialspace=True)
df_test = pd.read_csv('adult.test', names=cols, na_values=' ?', skipinitialspace=True, skiprows=1)

# 清理测试集 income 尾部的 '.'(原始数据格式问题)
df_test['income'] = df_test['income'].str.strip('.')

# 合并
df_all = pd.concat([df_train, df_test], ignore_index=True)
print(f"合并后: {df_all.shape[0]} 行")
合并后: 48842 行

1.2 缺失值处理

原始数据中用 ' ?' 表示缺失值。pd.read_csvna_values=' ?' 参数已将其转换为 NaN

print(df_all.isnull().sum())
age               0
workclass        1836
fnlwgt              0
education           0
education_num       0
marital_status      0
occupation       1843
relationship        0
race                0
sex                 0
capital_gain        0
capital_loss        0
hours_per_week      0
native_country    583
income              0

缺失集中在 workclass(1,836 条)、occupation(1,843 条)和 native_country(583 条)。对于频繁项集挖掘,缺失值没有语义意义,直接删除这些行:

df_clean = df_all.dropna(subset=['workclass', 'occupation', 'native_country']).reset_index(drop=True)
print(f"清洗后: {df_clean.shape[0]} 行(删除 {len(df_all) - len(df_clean)} 条)")
清洗后: 46043 行(删除 2799 条)

1.3 连续值离散化

连续变量无法直接用于频繁项集挖掘(每个值只出现一次)。我们需要将连续变量分箱(binning)为离散的类别标签。

1.3.1 离散化策略选择

变量

策略

分箱依据

箱数

age

业务规则

生命周期阶段

6

hours-per-week

业务规则

工作强度分级

5

capital-gain

业务规则 + 阈值

有无收益 + 金额分级

4

capital-loss

业务规则 + 阈值

有无损失 + 金额分级

4

fnlwgt

等频分箱

四分位数

4

education-num

与 education 合并

冗余,不单独使用

1.3.2 实现代码

import pandas as pd

# age 分箱
def age_bin(x):
    if x < 25:   return 'Young'
    elif x < 35: return 'Adult_25-34'
    elif x < 45: return 'Adult_35-44'
    elif x < 55: return 'Middle_45-54'
    elif x < 65: return 'Senior_55-64'
    else:        return 'Elderly_65+'

# hours-per-week 分箱
def hours_bin(x):
    if x < 20:  return 'Hours_PartTime'
    elif x < 40: return 'Hours_20-39'
    elif x == 40: return 'Hours_FullTime'
    elif x < 50: return 'Hours_41-49'
    else:         return 'Hours_Overtime'

# capital-gain / capital-loss 分箱
def capital_bin(x, name):
    if x == 0:      return f'{name}_None'
    elif x < 3000:  return f'{name}_Low'
    elif x < 7000:  return f'{name}_Mid'
    else:           return f'{name}_High'

df = df_clean.copy()
df['age_binned'] = df['age'].map(age_bin)
df['hours_binned'] = df['hours_per_week'].map(hours_bin)
df['capital_gain_binned'] = df['capital_gain'].map(lambda x: capital_bin(x, 'CapGain'))
df['capital_loss_binned'] = df['capital_loss'].map(lambda x: capital_bin(x, 'CapLoss'))

# fnlwgt 等频四分位
_, fnlwgt_bins = pd.qcut(df['fnlwgt'], q=4, duplicates='drop', retbins=True)
df['fnlwgt_binned'] = pd.cut(df['fnlwgt'], bins=fnlwgt_bins,
                              labels=['Fnlwgt_Q1','Fnlwgt_Q2','Fnlwgt_Q3','Fnlwgt_Q4'])

1.3.3 离散化效果对比

ch01_discretization.png

上图展示了三个关键变量离散化前后的对比:

  • age:原始分布呈右偏(25-45 岁为主),离散化为 6 个年龄段后,分布形态基本保持

  • hours-per-week:在 40 处有极强的峰值(标准全职),离散化单独保留了这一箱

  • capital-gain:绝大多数人为 0(91.7%),离散化后用 CapGain_None 捕获这一特征,仅对非零值进一步分级

为什么要保留 "0 值" 作为一个独立箱?

对于 capital-gain 和 capital-loss,0 值本身就是最强的信号——"没有投资收益/损失" 这一信息在频繁项集挖掘中极为有用。

1.4 序列化:构建事务数据

现在我们将每一行转换为 token 序列,每个 token 的格式为 前缀:值

token_cols = ['workclass', 'education', 'marital_status', 'occupation',
              'relationship', 'race', 'sex', 'age_binned', 'hours_binned',
              'capital_gain_binned', 'capital_loss_binned', 'native_country', 'income']

prefix_map = {
    'workclass': 'WC', 'education': 'EDU', 'marital_status': 'MS',
    'occupation': 'OCC', 'relationship': 'REL', 'race': 'RACE',
    'sex': 'SEX', 'age_binned': 'AGE', 'hours_binned': 'HRS',
    'capital_gain_binned': 'CG', 'capital_loss_binned': 'CL',
    'native_country': 'NC', 'income': 'INC'
}

transactions = []
for _, row in df.iterrows():
    tokens = [f"{prefix_map[c]}:{row[c]}" for c in token_cols]
    transactions.append(tokens)

# 查看第一条
print(transactions[0])
['WC:Private', 'EDU:Bachelors', 'MS:Never-married', 'OCC:Adm-clerical',
 'REL:Not-in-family', 'RACE:White', 'SEX:Male', 'AGE:Adult_35-44',
 'HRS:Hours_FullTime', 'CG:CapGain_None', 'CL:CapLoss_None',
 'NC:United-States', 'INC:<=50K']

每条事务固定包含 13 个 token(12 个特征 + 1 个标签)。

1.4.1 Token 词频统计

from collections import Counter

all_tokens = [t for tx in transactions for t in tx]
token_counts = Counter(all_tokens).most_common(30)
ch01_token_freq.png

词频图揭示了几个重要特征:

  1. INC:<=50K 出现 37,155 次(红色条),远超 INC:>50K 的 11,687 次——类别不平衡明显

  2. WC:Private 最高频,对应私营部门占绝对主导

  3. CG:CapGain_None 极高频,印证了绝大多数人没有资本收益

  4. 国家属性中 NC:United-States 占绝对主导,其他国家的 token 频率都很低

1.4.2 事务长度分布

ch01_transaction_len.png

由于每行固定选取 13 个属性,所有事务长度一致(均值 = 13.0)。在后续章节使用 Apriori/FP-Growth 时,这意味着每个事务的挖掘空间是可预期的。

1.5 序列化结果保存

为了方便后续章节复用,将序列化后的事务保存为文本文件:

# 保存为每行一个事务的格式(空格分隔 token)
with open('transactions.txt', 'w') as f:
    for tx in transactions:
        f.write(' '.join(tx) + '\n')

print(f"已保存 {len(transactions)} 条事务到 transactions.txt")

1.6 本节小结

  • 缺失值处理:删除含 '?' 的行(2,799 条),保留 46,043 条

  • 离散化策略:连续变量按业务规则或等频分箱,避免随意切分

  • 序列化格式前缀:值 的 token 格式,每行 13 个 token

  • 类别不平衡<=50K 占比约 76%,后续分析需要注意

  • 下一篇将基于序列化后的事务数据,使用 Apriori 和 FP-Growth 挖掘频繁项集

第2章 频繁项集挖掘:Apriori 与 FP-Growth

2.1 什么是频繁项集?

频繁项集(Frequent Itemset)是指在一组事务数据中,出现频率(支持度)超过设定阈值的项的集合。

以超市购物为例:如果 {牛奶, 面包} 在 30% 的交易中出现,且最小支持度阈值为 20%,则它是一个频繁 2-项集。

在我们的 Adult 数据集中,每个"事务"是一行序列化后的 token 序列:

['WC:Private', 'EDU:Bachelors', 'MS:Never-married', 'OCC:Adm-clerical',
 'REL:Not-in-family', 'RACE:White', 'SEX:Male', 'AGE:Adult_35-44',
 'HRS:Hours_FullTime', 'CG:CapGain_None', 'CL:CapLoss_None',
 'NC:United-States', 'INC:<=50K']

频繁项集挖掘可以发现:

  • 高共现属性组合:如 {EDU:Bachelors, OCC:Prof-specialty, INC:>50K}

  • 低频噪声过滤:支持度低于阈值的项目被自动剔除

2.2 核心概念

概念

定义

示例

项集

一组项的集合

{EDU:Bachelors, INC:>50K}

支持度

包含该项集的事务占比

support(X)=count(X)N\text{support}(X) = \frac{\text{count}(X)}{N}

频繁项集

支持度 ≥ 最小阈值的项集

min_support = 0.03

k-项集

包含 k 个项的项集

{A, B, C} 是 3-项集

置信度

关联规则的条件概率

confidence(AB)=support(AB)support(A)\text{confidence}(A \to B) = \frac{\text{support}(A \cup B)}{\text{support}(A)}

2.3 Apriori 算法

2.3.1 算法原理

Apriori 基于一个反单调性质(Apriori Property):

如果一个项集是频繁的,那么它的所有子集也一定是频繁的。

等价地说:如果一个项集是不频繁的,那么它的所有超集也都是不频繁的。

算法流程:

  1. 扫描数据库,统计每个 1-项集的支持度,筛选出频繁 1-项集 L1

  2. 连接步:用 Lk 自连接生成候选 (k+1)-项集 Ck+1

  3. 剪枝步:利用 Apriori 性质剪去包含不频繁子集的候选

  4. 再次扫描,计算 Ck+1 的支持度,得到 Lk+1

  5. 重复直到 Lk+1 为空

2.3.2 代码实现

from mlxtend.frequent_patterns import apriori
from mlxtend.preprocessing import TransactionEncoder

# 编码为 one-hot 矩阵
te = TransactionEncoder()
te_ary = te.fit(transactions).transform(transactions)
df_onehot = pd.DataFrame(te_ary, columns=te.columns_)

# 运行 Apriori
freq_items = apriori(df_onehot, min_support=0.03,
                     use_colnames=True, max_len=3)
print(f"频繁项集数量: {len(freq_items)}")
print(freq_items.sort_values('support', ascending=False).head(10))
频繁项集数量: 2480

   support                        itemsets
5      0.760            (INC:<=50K)
4      0.239              (INC:>50K)
24     0.239    (CG:CapGain_None, INC:<=50K)
20     0.212       (HRS:Hours_FullTime, INC:<=50K)
8      0.192         (EDU:HS-grad, INC:<=50K)
...

2.4 FP-Growth 算法

2.4.1 算法原理

FP-Growth(Frequent Pattern Growth)解决了 Apriori 需要生成候选集的问题。它通过构建 FP-树(Frequent Pattern Tree)来压缩数据:

  1. 第一次扫描:统计每项频率,按频率降序排列

  2. 第二次扫描:构建 FP-树,每条事务按全局频率顺序插入树中

  3. 挖掘:从 FP-树中递归提取频繁模式,无需生成候选集

FP-树的核心思想:具有共同前缀的事务路径共享树的节点,实现数据压缩。

from mlxtend.frequent_patterns import fpgrowth

freq_items = fpgrowth(df_onehot, min_support=0.03,
                      use_colnames=True, max_len=3)

2.4.2 为什么 FP-Growth 理论上更快?

对比项

Apriori

FP-Growth

扫描次数

每层扫描一次(k 层扫 k 次)

仅扫描 2 次

候选集

需要生成和剪枝

不需要

内存

存储候选集

FP-树压缩存储

最坏情况

指数级候选集爆炸

树退化为候选集枚举

理论上,在海量数据(百万级事务)和低支持度阈值(如 0.001)时,FP-Growth 比 Apriori 快 10~100 倍

2.5 实验对比

2.5.1 运行时间对比

ch02_runtime_comparison.png

实验结果揭示了一个重要的要点:FP-Growth 并非在所有场景下都更快

支持度

Apriori (s)

FP-Growth (s)

倍数

1%

3.097

2.033

1.52x

2%

1.835

1.512

1.21x

3%

1.086

1.183

0.92x

5%

0.549

0.979

0.56x

8%

0.301

0.660

0.46x

10%

0.189

0.506

0.37x

原因分析

  • 高支持度(如 8%)时,候选集数量少,Apriori 的候选生成开销不大,反而 FP-Growth 的树构建开销成为瓶颈

  • 低支持度(如 1%)时,候选集增多,FP-Growth 的优势开始显现

  • 百万级事务 + 低支持度 + 多属性的真实大数据场景中,差距会更加显著

教学提示:算法选择要看数据规模和参数。不能简单地说"FP-Growth 一定比 Apriori 快"。

2.5.2 频繁项集 Top 15

ch02_top_itemsets.png

从 Top 15 项集可以直观看到:

  • 单一项INC:<=50K(76%)和 INC:>50K(23.9%)是最频繁的单 token

  • 2-项集{CG:CapGain_None, INC:<=50K}(23.9%)说明没有资本收益的人群大多数收入 ≤50K

  • 3-项集{HRS:Hours_FullTime, CG:CapGain_None, INC:<=50K} 揭示了"全职工作但无投资收益 → 低收入"的模式

2.5.3 支持度分布

ch02_support_dist.png

支持度分布呈右偏——大量项集的支持度集中在低值区间,只有少数项集出现频率很高。这符合现实数据的长尾分布特征。

2.5.4 项集长度分布

ch02_itemset_length.png

在 min_support=0.03 的阈值下:

  • 1-项集:122 个(所有可能的 token)

  • 2-项集:1,484 个

  • 3-项集:874 个

2-项集数量最多,说明两两属性组合中有较多频繁共现关系。

2.6 关联规则提取

频繁项集本身已经提供了洞察,但可以进一步提取关联规则

from mlxtend.frequent_patterns import association_rules

rules = association_rules(freq_items, metric="confidence",
                          min_threshold=0.5)
rules = rules.sort_values('confidence', ascending=False)
print(rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']].head(10))

高置信度规则示例:

  • {EDU:Prof-school} → {EDU:Prof-school, INC:>50K}:专业学位与高收入强相关

  • {OCC:Prof-specialty, HRS:Hours_FullTime} → {INC:<=50K}{INC:>50K}:专业全职工作的收入分化

提升度(Lift)衡量规则的有效性:

  • Lift > 1:正相关(规则有意义)

  • Lift = 1:独立(无关联)

  • Lift < 1:负相关

2.7 本节小结

  • Apriori 原理简单,适合教学理解和中等规模数据

  • FP-Growth 在低支持度、大规模数据时优势明显,但树构建有固定开销

  • 频繁项集揭示了高共现属性组合,如 "无资本收益 + ≤50K"

  • 关联规则可以从频繁项集中提取,提供 "如果...则..." 的洞察

  • 下一篇将利用频繁项集和原始 token 序列,学习每个 token 的嵌入向量

第3章 嵌入学习:Word2Vec

3.1 为什么需要嵌入?

经过前两章的处理,我们已经将每行数据序列化为 token 序列,并挖掘了频繁项集。但传统统计方法(如逻辑回归、SVM)需要数值特征向量作为输入。

最朴素的做法是 One-Hot 编码:每个 token 用一个长度为 122(词汇表大小)的向量表示,对应位置为 1,其余为 0。

但 One-Hot 有两个致命缺陷:

  1. 高维稀疏:122 维中只有一个 1,信息密度极低

  2. 无语义关系BachelorsMasters 的 One-Hot 向量正交,但它们实际上很相似

嵌入学习(Embedding)将离散符号映射为稠密、低维的实值向量(如 64 维),同时保留符号之间的语义关系。

3.2 Word2Vec 的两种架构

Word2Vec 由 Google 的 Mikolov 等人于 2013 年提出,包含两种架构:

3.2.1 CBOW(Continuous Bag-of-Words)

思想:用上下文词的向量来预测中心词。

对于中心词 wt,其上下文窗口为 {wtc,…,wt−1,wt+1,…,wt+c},CBOW 最大化:

1Tt=1TlogP(wtwtc,,wt+c)\frac{1}{T}\sum_{t=1}^{T} \log P(w_t \mid w_{t-c}, \dots, w_{t+c})

CBOW 对上下文做平均,训练速度快,适合大规模数据。

3.2.2 Skip-gram

思想:用中心词来预测上下文词。

Skip-gram 最大化:

1Tt=1Tcjc,j0logP(wt+jwt)\frac{1}{T}\sum_{t=1}^{T} \sum_{-c \leq j \leq c, j \neq 0} \log P(w_{t+j} \mid w_t)

Skip-gram 更擅长捕获稀有词的表示,因为它为每个上下文-中心词对独立训练。

3.3 代码实现

from gensim.models import Word2Vec

# Skip-gram 模型
model_sg = Word2Vec(
    sentences=transactions,     # 事务列表
    vector_size=64,             # 嵌入维度
    window=5,                   # 上下文窗口大小
    min_count=1,                # 忽略频率 < 1 的词(这里保留所有)
    sg=1,                       # 1=Skip-gram, 0=CBOW
    epochs=20,                  # 训练轮数
    seed=42
)

# 查看词向量
vector = model_sg.wv['EDU:Bachelors']
print(vector.shape)  # (64,)

3.4 最相似词分析

训练完成后,最直观的检查方式是看每个词的"邻居":

ch03_w2v_similar_words.png

从六个查询词的结果可以看到嵌入质量很高:

查询词

最相似

解释

Bachelors

Assoc-acdm, Masters, Some-college

同为高等教育学历,语义高度相关

Prof-specialty

Exec-managerial, Tech-support, Adm-clerical

同类白领职业

>50K

<=50K, Married-civ-spouse, Self-emp-inc

收入标签与婚姻状况、自雇正相关

Private

Self-emp-not-inc, Federal-gov, Local-gov

同为工作部门类别

Adult_25-34

Adult_35-44, Middle_45-54

相邻年龄段最相似,体现连续变化

White

Amer-Indian-Eskimo, Other, Black

种族类别聚类良好

关键观察AGE:Adult_25-34 最相似的是 AGE:Adult_35-44(相似度 0.91),其次是 AGE:Middle_45-54(0.78)——相似度随年龄差递减,这说明 Word2Vec 自动捕获了年龄的顺序关系

3.5 词向量 PCA 可视化

将 64 维词向量用 PCA 降至 2D,直观查看嵌入空间的结构:

ch03_w2v_pca_scatter.png

从 PCA 投影可以看到:

  • EDU(绿色) 聚在左侧,与 OCC(紫色)INC(红色) 有一定距离

  • AGE(橙色) 聚集在上方,与其他属性区分明显

  • RACE(深蓝)SEX(深绿) 点少而密集

  • NC(紫色) 分散广泛,说明国家属性内部差异大

虽然 PCA 仅保留了约 22% 的方差,但已经能看到明显的聚类趋势,说明嵌入学习成功地捕获了语义结构。

3.6 Skip-gram vs CBOW 对比

ch03_w2v_param_comparison.png

对于同一查询词,两种模型给出了相似但不完全相同的结果:

  • Skip-gram 的相似度分数分布更广(区分度更高)

  • CBOW 倾向于给出更"平滑"的相似排序

在本任务中,Skip-gram 略胜一筹——它能更好地捕获类别型 token 之间的细微差异。

3.7 关键参数调优指南

参数

含义

推荐范围

影响

vector_size

嵌入维度

32~128

过小欠拟合,过大过拟合

window

上下文窗口

3~10

大窗口捕获全局共现,小窗口捕获局部关系

min_count

最小词频

1~5

忽略低频词,减少噪声

sg

架构选择

0=CBOW, 1=Skip-gram

Skip-gram 更适合稀有词

epochs

训练轮数

10~50

太少不收敛,太多过拟合

negative

负采样数

5~15

影响训练速度和精度

3.8 本节小结

  • Word2Vec 将 122 维 One-Hot 向量压缩为 64 维稠密向量

  • Skip-gram 和 CBOW 各有优劣,本任务中 Skip-gram 表现略好

  • 嵌入空间自动捕获了语义关系(如相邻年龄段相似度高)

  • 下一篇将介绍 GloVe(全局统计方法)和 FastText(子词方法)作为对比

第4章 嵌入学习:GloVe 与 FastText

4.1 GloVe:全局共现矩阵分解

4.1.1 原理

Word2Vec 通过局部滑动窗口学习词向量,只利用了上下文窗口内的共现信息。GloVe(Global Vectors for Word Representation)则直接使用全局共现矩阵,捕获整个语料库的统计信息。

GloVe 的优化目标是最小化加权最小二乘损失:

J=i,j=1Vf(Xij)(wiTw~j+bi+b~jlogXij)2J = \sum_{i,j=1}^{V} f(X_{ij}) \left(w_i^T \tilde{w}_j + b_i + \tilde{b}_j - \log X_{ij}\right)^2

其中:

  • Xij 是词 i 和词 j 的共现次数

  • wi 是主词向量,w~j 是上下文词向量

  • f(x) 是加权函数,防止高频共现对主导损失:

f(x)={(x/xmax)αif x<xmax1otherwisef(x) = \begin{cases} (x/x_{\max})^{\alpha} & \text{if } x < x_{\max} \\ 1 & \text{otherwise} \end{cases}

通常取 xmax=100α=0.75

4.1.2 共现矩阵可视化

构建 Adult 数据集的全局共现矩阵(窗口=5,距离加权):

ch04_cooccurrence_heatmap.png

从热力图可以直观看到:

  • 对角线方向的高频区块:如 EDU:HS-gradEDU:Some-college 共现频繁(因为它们都是教育属性,在同一"句子"中出现)

  • 收入相关区块WC:PrivateINC:<=50K 共现频繁(左下角亮区)

  • 稀疏区域NC:United-States 与特定职业的共现较少,因为国家属性覆盖太广

4.1.3 PyTorch 实现

import torch
import torch.nn as nn

class GloVeModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim):
        super().__init__()
        self.w = nn.Embedding(vocab_size, embedding_dim)
        self.w_context = nn.Embedding(vocab_size, embedding_dim)
        self.b = nn.Embedding(vocab_size, 1)
        self.b_context = nn.Embedding(vocab_size, 1)

    def forward(self, i, j):
        return (self.w(i) * self.w_context(j)).sum(dim=1) + \
               self.b(i).squeeze() + self.b_context(j).squeeze()

# 训练
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.05)
for epoch in range(30):
    # ... 批量优化
    loss = (weights * (pred - log_values)**2).sum()
    loss.backward()
    optimizer.step()

# 最终词向量 = 主词向量 + 上下文词向量
glove_vectors = model.w.weight + model.w_context.weight

4.2 FastText:子词嵌入

4.2.1 原理

Word2Vec 和 GloVe 都把每个词视为不可分割的原子单位。FastText 的创新在于将词拆分为 n-gram 子词

"EDU:Bachelors" → ["ED", "DU", "U:", ":B", "Ba", "ac", "ch", "he", "el", "lo", "or", "rs", ...]

每个 n-gram 有自己的向量,词的最终向量是其所有 n-gram 向量之和。

优势:

  • 处理未登录词(OOV):训练时未出现的 token 可以通过子词推断向量

  • 拼写鲁棒性:拼写错误的词仍能生成有意义的向量

  • 形态学信息:共享前缀/后缀的词会获得相似向量

4.2.2 代码实现

from gensim.models import FastText

model_ft = FastText(
    sentences=transactions,
    vector_size=64,
    window=5,
    min_n=2,      # 最小 n-gram 长度
    max_n=4,      # 最大 n-gram 长度
    epochs=20,
    seed=42
)

4.3 三种方法对比

4.3.1 最相似词对比

ch04_method_comparison.png

对于同一查询词,三种方法给出了相似但不完全相同的 Top 8 结果:

查询词

Word2Vec 特色

GloVe 特色

FastText 特色

Bachelors

偏高等教育类

混合教育和收入

更集中于教育类

Prof-specialty

偏管理类

偏技术类

与 Word2Vec 相似

>50K

互补标签 + 婚姻

偏教育类

偏职业类

Adult_25-34

相邻年龄段

相邻年龄段

与 Word2Vec 高度一致

4.3.2 类内一致性量化对比

为了量化比较三种嵌入的质量,计算同类词之间的平均余弦相似度

类别组

Word2Vec

GloVe

FastText

EDU(教育)

0.780

0.856

0.735

AGE(年龄)

0.861

0.920

0.845

WC(工作部门)

0.810

0.872

0.780

GloVe 在所有类别上类内一致性最高,这是因为全局共现矩阵更好地捕获了同类属性的整体统计关系。Word2Vec 次之,FastText 略低——子词拆分在表格 token 场景中收益有限(因为 token 已经是格式化的 前缀:值,子词拆分的形态学意义不大)。

4.4 方法选择指南

方法

优势

适用场景

Word2Vec (Skip-gram)

捕获稀有词,实现简单

中小规模数据,需要快速原型

Word2Vec (CBOW)

训练速度快

超大规模数据,不关注稀有词

GloVe

全局统计信息,类内一致性好

需要高质量语义的场景

FastText

处理 OOV 和拼写错误

用户查询日志、社交媒体文本

对于 Adult 数据集(表格序列化数据):

  • GloVe 的类内一致性最好,适合后续的降维可视化

  • FastText 的子词优势不明显,因为 token 已经是结构化格式

  • Word2Vec 作为 Baseline,各方面表现均衡

4.5 本节小结

  • GloVe 通过全局共现矩阵分解学习向量,在类内一致性上优于 Word2Vec

  • FastText 的子词拆分对 NLP 文本效果好,但在表格 token 场景下收益有限

  • 三种方法各有优劣,GloVe 在本任务中表现最佳

  • 下一篇将使用 t-SNE 和 UMAP 将 64 维嵌入降至 2D 进行可视化分析

第5章 高维可视化:t-SNE 与 UMAP

5.1 为什么需要降维可视化?

经过嵌入学习,每个 token 被表示为 64 维的稠密向量。但人类无法直接观察 64 维空间。降维可视化将高维向量投影到 2D 平面,让我们能够:

  • 直观验证嵌入质量:同类 token 是否聚集在一起?

  • 发现离群项:哪些 token 与同类差异很大?

  • 辅助特征选择:哪些属性维度携带了最多的信息?

5.2 t-SNE:保留局部结构

5.2.1 原理

t-SNE(t-Distributed Stochastic Neighbor Embedding)由 Hinton 的学生 van der Maaten 提出,核心思想是保持高维空间中的局部邻域关系

  1. 在高维空间,对每个点 xi,计算其他点 xj 的条件概率 pji(相似度高则概率大):

    pji=exp(xixj2/2σi2)kiexp(xixk2/2σi2)p_{j|i} = \frac{\exp(-\|x_i - x_j\|^2 / 2\sigma_i^2)}{\sum_{k \neq i} \exp(-\|x_i - x_k\|^2 / 2\sigma_i^2)}

  2. 在低维空间,用 t 分布(自由度=1,即 Cauchy 分布)计算类似概率 qij

    qij=(1+yiyj2)1kl(1+ykyl2)1q_{ij} = \frac{(1 + \|y_i - y_j\|^2)^{-1}}{\sum_{k \neq l} (1 + \|y_k - y_l\|^2)^{-1}}

  3. 最小化 KL 散度:

    KL(PQ)=ijpijlogpijqijKL(P \| Q) = \sum_{i \neq j} p_{ij} \log \frac{p_{ij}}{q_{ij}}

    ​​

t 分布的"重尾"特性使得低维空间中距离较远的点会被进一步拉开,从而形成清晰的聚类。

5.2.2 代码实现

from sklearn.manifold import TSNE

tsne = TSNE(
    n_components=2,     # 降至 2D
    perplexity=30,      # 局部邻域大小
    learning_rate='auto',
    init='pca',         # 用 PCA 初始化,加速收敛
    random_state=42,
    max_iter=1000
)
vectors_2d = tsne.fit_transform(vectors)

5.2.3 可视化结果

ch05_tsne_embedding.png

t-SNE 结果清晰地展示了:

  • EDU(绿色) HS-gradSome-collegeBachelorsMasters 形成连续的学历梯度

  • AGE(橙色) YoungAdultMiddleSeniorElderly 呈线性排列

  • WC(蓝色) PrivateSelf-emp 类较近,Federal-govLocal-govState-gov 形成政府类簇

  • INC:>50K(红色)在中央偏右,与 Married-civ-spouseExec-managerial 等近邻

5.3 UMAP:兼顾局部与全局

5.3.1 原理

UMAP(Uniform Manifold Approximation and Projection)基于流形学习和拓扑学理论,与 t-SNE 相比:

特性

t-SNE

UMAP

局部结构

保留极好

保留好

全局结构

差(簇间距离无意义)

好(簇间距离可信)

速度

较慢(O(N2)O(N^2)

快(O(N1.2)O(N^{1.2})

可扩展性

千级

百万级

理论基础

概率分布匹配

流形近似 + 拓扑

UMAP 假设数据均匀分布在黎曼流形上,通过构建高维模糊拓扑结构,在低维空间找到最匹配的拓扑。

5.3.2 代码实现

import umap

umap_model = umap.UMAP(
    n_components=2,
    n_neighbors=15,     # 局部邻域大小(类似 perplexity)
    min_dist=0.1,       # 最小点间距(控制紧密度)
    metric='euclidean',
    random_state=42
)
vectors_2d = umap_model.fit_transform(vectors)

5.3.3 可视化结果

ch05_umap_embedding.png

UMAP 的特点:

  • EDUOCC 的连接更紧密(BachelorsProf-specialty 几乎重叠)

  • AGE 仍保持良好的顺序排列

  • 整体来看,UMAP 的簇间距离更具解释性

5.4 三种降维方法直接对比

ch05_comparison.png

方法

优点

缺点

PCA

极快,线性,方差可解释

无法捕获非线性结构

t-SNE

局部结构极好,聚类清晰

全局结构差,参数敏感

UMAP

兼顾局部和全局,快

对 n_neighbors 敏感

5.5 参数敏感性分析

ch05_param_sensitivity.png

t-SNE 的 perplexity

perplexity 可以理解为"有效邻域大小"。太小会导致碎片化,太大会丢失局部细节。经验值:样本量的 5%~30%。

UMAP 的 n_neighbors

n_neighbors 控制局部与全局的权衡。值越小,越关注局部结构;值越大,越关注全局布局。

5.6 参数选择建议

方法

关键参数

推荐值

调节方向

t-SNE

perplexity

30

样本多 → 增大

t-SNE

learning_rate

'auto'

通常不需要调

t-SNE

max_iter

1000

不够则增加

UMAP

n_neighbors

15

关注局部 → 减小,关注全局 → 增大

UMAP

min_dist

0.1

要紧凑 → 减小,要分散 → 增大

5.7 本节小结

  • t-SNE 擅长局部结构保留,适合查看精细聚类

  • UMAP 兼顾局部和全局,速度更快,可扩展到百万级数据

  • 参数选择影响很大,需要通过敏感性分析找到合适的值

  • 对于表格嵌入数据,UMAP 的全局结构保持能力更有优势

  • 下一篇将使用自编码器对嵌入进行深度降维,从 64 维压缩到 32 维

第6章 深度降维:自编码器

6.1 从线性到非线性降维

PCA、t-SNE、UMAP 都是无参数的降维方法。自编码器(Autoencoder)通过神经网络学习一个参数化的非线性映射:

Encoder: z=fθ(x)Rd\text{Encoder: } z = f_\theta(x) \in \mathbb{R}^d
Decoder: x^=gϕ(z)x\text{Decoder: } \hat{x} = g_\phi(z) \approx x

训练目标是最小化重构误差:

minθ,ϕ1Ni=1Nxigϕ(fθ(xi))2\min_{\theta, \phi} \frac{1}{N} \sum_{i=1}^{N} \|x_i - g_\phi(f_\theta(x_i))\|^2

与 PCA 相比,自编码器可以学习非线性的降维映射,理论上能够捕获更复杂的特征结构。

6.2 网络架构

本教程使用的自编码器结构:

输入 (64D)
    │
    ▼
Linear(64 → h1) + ReLU + BatchNorm
    │
    ▼
Linear(h1 → h2) + ReLU + BatchNorm
    │
    ▼
Linear(h2 → latent_dim)    ← 瓶颈层(编码结果)
    │
    ▼
Linear(latent_dim → h2) + ReLU + BatchNorm
    │
    ▼
Linear(h2 → h1) + ReLU + BatchNorm
    │
    ▼
Linear(h1 → 64)            ← 重构输出

以 64D → 32D 为例:

class Autoencoder(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(64, 96), nn.ReLU(), nn.BatchNorm1d(96),
            nn.Linear(96, 64), nn.ReLU(), nn.BatchNorm1d(64),
            nn.Linear(64, 32),
        )
        self.decoder = nn.Sequential(
            nn.Linear(32, 64), nn.ReLU(), nn.BatchNorm1d(64),
            nn.Linear(64, 96), nn.ReLU(), nn.BatchNorm1d(96),
            nn.Linear(96, 64),
        )

    def forward(self, x):
        return self.decoder(self.encoder(x))

6.3 训练过程

model = Autoencoder().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
criterion = nn.MSELoss()

for epoch in range(500):
    _, decoded = model(batch)
    loss = criterion(decoded, batch)
    loss.backward()
    optimizer.step()

训练损失曲线展示了不同压缩维度下的收敛情况:

ch06_ae_loss.png

从图中可以看到:

  • 64D → 8D:损失最大(0.512),信息损失最多,曲线波动也最大

  • 64D → 16D:损失降至 0.245,收敛平稳

  • 64D → 32D:损失仅 0.078,重构质量很高

  • 64D → 48D:损失最低(0.052),几乎完美重构

6.4 原始 vs 压缩后可视化对比

将原始 64D 向量和不同压缩后的编码结果都用 UMAP 降至 2D,直观对比:

ch06_ae_comparison-vfNy.png

三张图的对比揭示了:

  • 原始 64D:聚类清晰,各类分散良好

  • 压缩至 32D:整体结构与原始非常接近,说明 32D 足以保留大部分信息

  • 压缩至 8D:各类之间开始重叠,EDUOCC 混在一起,信息损失明显

32D 是较好的选择——维度减半但视觉效果几乎没有损失。

6.5 压缩维度敏感性分析

为了量化不同压缩维度的效果,我们比较两个指标:

ch06_dim_sensitivity.png

压缩维度

MSE 重构误差

类内一致性

8D

0.512

0.868

16D

0.245

0.906

32D

0.078

0.925

48D

0.052

0.932

关键发现

  • 重构误差随维度增加单调下降(这是预期的)

  • 类内一致性在 32D 和 48D 之间差异极小(0.925 vs 0.932)

  • 32D 是"甜点"——维度减半,但类内一致性仅下降 0.7%,而重构误差也很低

教学提示:在数学建模竞赛中,如果特征维度较高(如 100+),使用自编码器压缩到 32~64 维可以显著降低后续模型的计算复杂度,同时保持较好的特征质量。

6.6 下游任务验证

压缩后的向量可以直接用于下游任务。以下是一个简化的验证示例:

# 用自编码器的 32D 编码作为特征
with torch.no_grad():
    model.eval()
    encoded_32d, _ = model(X_tensor)

# 可以用于:
# 1. 聚类分析(KMeans)
# 2. 分类任务(将编码作为特征输入分类器)
# 3. 异常检测(重构误差大的样本可能是异常值)

6.7 自编码器 vs 其他降维方法

方法

类型

非线性

可泛化

适用场景

PCA

线性

线性相关、快速基线

t-SNE

非线性

仅可视化

UMAP

非线性

可视化和特征工程

自编码器

非线性

特征压缩、下游任务

自编码器的独特优势

  • 可以泛化到新数据(Encoder 是一个函数,可以直接应用于未见过的样本)

  • 可以与下游模型端到端训练

  • 可以加入各种正则化(如 VAE 的 KL 散度、去噪自编码器的噪声注入)

6.8 本节小结

  • 自编码器通过神经网络学习非线性降维映射

  • 32D 是 Adult 数据集的压缩甜点——维度减半,质量损失极小

  • 与 t-SNE/UMAP 不同,自编码器可以泛化到新样本

  • 重构误差和类内一致性是评估压缩质量的两个关键指标

  • 下一篇将整合所有方法,构建完整的预处理流水线

第7章 总结:完整预处理流水线与速查表

经过前 6 章的学习,我们已经掌握了从原始表格到高质量特征向量的完整预处理流程

7.1 完整流水线代码

以下代码串联了所有步骤,可以直接运行:

# ============================================================
# 大数据集分析预处理 — 完整流水线
# ============================================================
import pandas as pd
import numpy as np
from collections import Counter
from gensim.models import Word2Vec, FastText
from sklearn.manifold import TSNE
import umap
import torch
import torch.nn as nn

# ---------- 步骤1:加载与清洗 ----------
cols = ['age','workclass','fnlwgt','education','education_num',
        'marital_status','occupation','relationship','race','sex',
        'capital_gain','capital_loss','hours_per_week','native_country','income']

df_train = pd.read_csv('adult.data', names=cols, na_values=' ?', skipinitialspace=True)
df_test = pd.read_csv('adult.test', names=cols, na_values=' ?', skipinitialspace=True, skiprows=1)
df_test['income'] = df_test['income'].str.strip('.')
df = pd.concat([df_train, df_test], ignore_index=True)
df = df.dropna(subset=['workclass', 'occupation', 'native_country']).reset_index(drop=True)

# ---------- 步骤2:离散化 ----------
def age_bin(x):
    if x < 25:   return 'Young'
    elif x < 35: return 'Adult_25-34'
    elif x < 45: return 'Adult_35-44'
    elif x < 55: return 'Middle_45-54'
    elif x < 65: return 'Senior_55-64'
    else:        return 'Elderly_65+'

def hours_bin(x):
    if x < 20:   return 'Hours_PartTime'
    elif x < 40: return 'Hours_20-39'
    elif x == 40: return 'Hours_FullTime'
    elif x < 50: return 'Hours_41-49'
    else:        return 'Hours_Overtime'

df['age_binned'] = df['age'].map(age_bin)
df['hours_binned'] = df['hours_per_week'].map(hours_bin)

# ---------- 步骤3:序列化 ----------
prefix_map = {'workclass': 'WC', 'education': 'EDU', 'marital_status': 'MS',
              'occupation': 'OCC', 'relationship': 'REL', 'race': 'RACE',
              'sex': 'SEX', 'age_binned': 'AGE', 'hours_binned': 'HRS',
              'capital_gain_binned': 'CG', 'capital_loss_binned': 'CL',
              'native_country': 'NC', 'income': 'INC'}

token_cols = ['workclass', 'education', 'marital_status', 'occupation',
              'relationship', 'race', 'sex', 'age_binned', 'hours_binned',
              'capital_gain_binned', 'capital_loss_binned', 'native_country', 'income']

transactions = []
for _, row in df.iterrows():
    tokens = [f"{prefix_map[c]}:{row[c]}" for c in token_cols]
    transactions.append(tokens)

# ---------- 步骤4:嵌入学习 ----------
# GloVe(推荐)或 Word2Vec
model_w2v = Word2Vec(sentences=transactions, vector_size=64, window=5,
                     min_count=1, sg=1, epochs=20, seed=42)

# ---------- 步骤5:降维可视化 ----------
# 取 GloVe 词向量
vectors = model_w2v.wv.vectors
words = list(model_w2v.wv.key_to_index.keys())

# UMAP 降维
umap_model = umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1, random_state=42)
vectors_2d = umap_model.fit_transform(vectors)

# 可视化
import matplotlib.pyplot as plt
plt.scatter(vectors_2d[:, 0], vectors_2d[:, 1], alpha=0.5)
plt.show()

# ---------- 步骤6:自编码器压缩 ----------
# (见第6章代码)

7.2 下游任务验证

将预处理后的特征用于收入预测任务,比较三种特征方案:

ch07_downstream_eval.png

特征方案

维度

LogisticRegression

RandomForest

One-Hot

106

85.17%

84.65%

Word2Vec

64

85.88%

84.68%

GloVe

64

85.48%

84.11%

关键发现

  1. Word2Vec 特征略优于 One-Hot:尽管维度更低(64 vs 106),但线性分类器在 Word2Vec 特征上表现更好,说明嵌入捕获了更有用的语义结构

  2. RandomForest 对嵌入的增益不如 LR:树模型本身可以处理离散特征,嵌入对其帮助有限

  3. GloVe 在 LR 上仅次于 Word2Vec:与第 4 章的类内一致性结论一致

重要教学点:信息泄露

如果在计算行向量时不排除 INC:income token,准确率会达到 99.9%——这是因为标签本身被编码进了特征中。这是典型的数据泄露(data leakage)。在真实建模中,标签属性不应参与特征学习。

7.3 方法速查表

4.1 预处理方法

操作

代码

说明

缺失值清洗

df.dropna(subset=[...])

删除含 '?' 的行

连续值离散化

pd.cut() / 自定义函数

按业务规则或等频分箱

序列化

[f"{prefix}:{val}" for ...]

转为 token 序列

One-Hot 编码

pd.get_dummies()

传统表格特征工程

4.2 频繁项集挖掘

方法

代码

适用

Apriori

apriori(df_onehot, min_support=0.03)

中等规模,教学理解

FP-Growth

fpgrowth(df_onehot, min_support=0.03)

大规模,低支持度

关联规则

association_rules(freq_items, metric="confidence")

提取 if-then 规则

4.3 嵌入学习

方法

代码

优势

Word2Vec (SG)

Word2Vec(..., sg=1)

稀有词表示好

Word2Vec (CBOW)

Word2Vec(..., sg=0)

训练快

GloVe

PyTorch 实现(见第4章)

全局统计,类内一致性好

FastText

FastText(..., min_n=2, max_n=4)

处理 OOV

4.4 降维可视化

方法

代码

特点

PCA

PCA(n_components=2)

线性,极快

t-SNE

TSNE(perplexity=30)

局部结构好,参数敏感

UMAP

UMAP(n_neighbors=15)

兼顾局部全局,快

自编码器

PyTorch 实现(见第6章)

非线性,可泛化

7.4 常见问题排查

问题

原因

解决方法

频繁项集太多

支持度太低

提高 min_support

嵌入训练慢

数据量大

减少 epochs 或用 negative 采样

t-SNE 碎片化

perplexity 太小

增大到 30~50

UMAP 聚成一团

n_neighbors 太大

减小到 5~15

自编码器不收敛

学习率太大

降到 1e-3 或加 weight_decay

下游任务准确率低

特征不含标签信息

这是正常的——嵌入只捕获属性关系

7.5 本节小结

  • 完整流水线:数据加载 → 清洗 → 离散化 → 序列化 → 频繁项集 → 嵌入 → 降维 → 下游验证

  • Word2Vec 特征在逻辑回归上达到 85.88% 准确率,略超 One-Hot 的 85.17%

  • 嵌入维度从 64 压缩到 32 时,质量损失仅 0.7%

  • 注意信息泄露:标签 token 不应参与行向量计算

附录:完整代码获取

本教程所有代码均可通过以下链接下载:

Big_data_preprocessing.zip

adult.zip

评论