线性代数在AI中的应用:数据表示、降维

一句话概述

线性代数是AI的"底层语言"——无论是一张图片、一段文本还是一个用户画像,在AI眼中统统都是向量与矩阵;而降维则是AI处理海量数据时的"压缩术",用更少的维度保留最核心的信息,让模型跑得快、学得好。从数据表示到特征提取,从词向量嵌入到PCA降维,线性代数贯穿了AI从数据输入到模型输出的每一个环节。

💡 核心要点:①数据在AI中以向量/矩阵/张量形式存在,万物皆可数值化;②词向量将语言映射为数学空间中的点,使语义计算成为可能;③PCA通过寻找方差最大的方向实现降维,是最经典的线性降维方法;④降维的本质是在信息损失与计算效率之间寻找最优平衡点。

教学与演示

一、数据表示——万物皆向量

是什么(定义):数据表示(Data Representation)是指将现实世界中的各种类型的数据——图像、文本、音频、用户行为等——转换为计算机可以处理的数值形式。在线性代数框架下,最基本的数据表示单位是向量(一维数组)和矩阵(二维数组),更高维的数据则用张量(Tensor)来表示。

大白话 想象你是一个只懂数字的AI,看到一张猫的图片、听到一首歌、读到一句话,你什么都"看不懂"。数据表示就是把这些五花八门的东西翻译成一串数字,让AI能"读"懂。就像翻译官把中文翻成英文一样,数据表示把"现实世界"翻译成"数字世界"。

为什么(原理):AI模型本质上是数学函数,数学函数只能处理数值。因此,所有输入AI的数据都必须先数值化。线性代数提供了向量、矩阵、张量这套优雅的数学工具,既能统一表示各种数据,又能利用矩阵运算的高效性进行批量处理。GPU之所以能加速AI训练,正是因为矩阵运算是高度可并行的。

怎么做(实现)

import numpy as np

# ==================== 图像的矩阵表示 ====================
# 一张 3x3 的灰度图像,每个像素值在 0~255 之间
# 0 表示黑色,255 表示白色,中间值表示灰色
gray_image = np.array([
    [  0, 128, 255],   # 第一行像素:黑、灰、白
    [ 64, 192,  32],   # 第二行像素:深灰、浅灰、深色
    [200, 100, 150]    # 第三行像素:浅灰、中灰、灰
], dtype=np.float32)   # 使用 float32 类型,AI 中常用

print("灰度图像矩阵形状:", gray_image.shape)  # (3, 3) 表示 3行3列
print("灰度图像矩阵:\n", gray_image)

# ==================== 彩色图像的张量表示 ====================
# 彩色图像有 RGB 三个通道,所以是三维张量
# 形状为 (高, 宽, 3),3 代表 R/G/B 三个颜色通道
rgb_image = np.array([
    [[255, 0, 0], [0, 255, 0], [0, 0, 255]],       # 第一行:红、绿、蓝
    [[255, 255, 0], [255, 0, 255], [0, 255, 255]],  # 第二行:黄、品红、青
    [[128, 128, 128], [255, 255, 255], [0, 0, 0]]   # 第三行:灰、白、黑
], dtype=np.float32)

print("\n彩色图像张量形状:", rgb_image.shape)  # (3, 3, 3) 三维张量
print("第一个像素的RGB值:", rgb_image[0, 0])  # [255, 0, 0] 红色像素

# ==================== 文本的向量表示 ====================
# 最简单的方式:词袋模型(Bag of Words)
# 假设词汇表只有 5 个词:["我", "爱", "AI", "学习", "编程"]
vocabulary = ["我", "爱", "AI", "学习", "编程"]

# 句子 "我爱AI" 的向量表示
sentence1 = "我爱AI"
# 统计每个词出现的次数,生成向量
vec1 = np.array([1, 1, 1, 0, 0], dtype=np.float32)  # 我1次、爱1次、AI1次、学习0次、编程0次

# 句子 "我爱学习编程" 的向量表示
sentence2 = "我爱学习编程"
vec2 = np.array([1, 1, 0, 1, 1], dtype=np.float32)  # 我1次、爱1次、AI0次、学习1次、编程1次

print("\n句子1向量:", vec1)
print("句子2向量:", vec2)

# ==================== 用户画像的特征向量 ====================
# 一个用户可以用多个特征来描述
# 特征:[年龄归一化, 月消费额归一化, 登录频率归一化, 购买次数归一化]
user_a = np.array([0.25, 0.80, 0.60, 0.45], dtype=np.float32)  # 年轻、高消费、中等活跃
user_b = np.array([0.70, 0.30, 0.90, 0.85], dtype=np.float32)  # 年长、低消费、高活跃

print("\n用户A特征向量:", user_a)
print("用户B特征向量:", user_b)

# ==================== 计算两个用户的相似度(余弦相似度) ====================
# 余弦相似度衡量两个向量方向的相似程度,值域 [-1, 1]
# 1 表示完全相同方向,0 表示正交(无关),-1 表示完全相反
def cosine_similarity(a, b):
    """计算两个向量的余弦相似度"""
    # np.dot 计算点积,np.linalg.norm 计算向量的模(长度)
    dot_product = np.dot(a, b)           # 点积:对应位置相乘再求和
    norm_a = np.linalg.norm(a)           # 向量 a 的长度
    norm_b = np.linalg.norm(b)           # 向量 b 的长度
    similarity = dot_product / (norm_a * norm_b)  # 余弦相似度公式
    return similarity

sim = cosine_similarity(user_a, user_b)
print(f"\n用户A与B的余弦相似度: {sim:.4f}")
print("(值越接近1,两个用户越相似)")
向量的余弦相似度 cos(θ) = (a·b) / (|a|·|b|) = Σ a_i b_i / (√(Σ a_i²) · √(Σ b_i²))
大白话 分子是两个向量的点积,即对应分量相乘再求和,衡量它们在同一方向上的"重叠程度"
大白话 分母是两个向量的模长之积,即各自长度的乘积,用于归一化
大白话 归一化的作用是使结果落在 [-1, 1] 之间,消除向量大小对相似度的影响
大白话 当结果为 1 时表示两向量方向完全相同,为 -1 时表示方向完全相反
大白话 当结果为 0 时表示两向量正交,即彼此无关、互相独立
大白话 余弦相似度只关心方向,不关心大小,适合衡量"趋势是否一致"
大白话 在AI中,余弦相似度广泛用于推荐系统和语义搜索,衡量用户偏好或文本语义的相似程度

什么用(应用):数据表示是AI的第一步,决定了模型能"看到"什么信息。在推荐系统中,用户和商品都用特征向量表示,通过计算向量相似度来做推荐;在图像识别中,图片被表示为像素矩阵送入卷积神经网络;在自然语言处理中,文本被表示为词向量序列送入Transformer。好的数据表示能让模型事半功倍。

哪些坑(缺点):简单的数据表示方法(如词袋模型)会丢失大量信息——词序、语义、上下文全部丢失;高分辨率图像直接展平为向量会导致维度爆炸(一张 1000×1000 的彩色图片展平后是 300万维向量);不同类型的数据需要不同的表示策略,没有"万能"的表示方法。


二、词向量与嵌入——语言的数学化

是什么(定义):词向量(Word Embedding)是一种将词语映射为低维稠密实数向量的技术。与词袋模型的高维稀疏表示不同,词向量将每个词映射为例如 100维 或 300维 的稠密向量,使得语义相近的词在向量空间中距离也相近。嵌入(Embedding)是更广义的概念,指将任何离散对象映射到连续向量空间的过程。

大白话 想象每个词都有自己的"身份证号",但不是随便编的号——意思相近的词,号码也相近。比如"国王"和"女王"的号码很接近,"猫"和"狗"的号码也比较接近,但"猫"和"汽车"的号码就差很远。这样AI就能通过比较号码(向量)来判断词语之间的关系了。

为什么(原理):词向量的核心思想来自分布式假设(Distributional Hypothesis):上下文相似的词,语义也相似。Word2Vec 等算法通过让神经网络预测上下文词来学习词向量,训练过程中,语义相近的词自然获得了相近的向量表示。这种稠密低维表示相比词袋模型有三大优势:(1) 维度低,计算高效;(2) 捕获语义关系;(3) 泛化能力强。

怎么做(实现)

import numpy as np

# ==================== 模拟词向量 ====================
# 实际中词向量由 Word2Vec/GloVe 等算法在大语料上训练得到
# 这里我们手动构造一个简化的 3 维词向量空间来理解概念
# 三个维度分别代表:[皇权程度, 性别男性度, 年龄成年度]

word_vectors = {
    "国王": np.array([0.95, 0.90, 0.85]),   # 高皇权、男性、成年
    "女王": np.array([0.95, 0.10, 0.85]),   # 高皇权、女性、成年
    "男人": np.array([0.10, 0.90, 0.80]),   # 低皇权、男性、成年
    "女人": np.array([0.10, 0.10, 0.80]),   # 低皇权、女性、成年
    "王子": np.array([0.60, 0.85, 0.30]),   # 中皇权、男性、年轻
    "公主": np.array([0.60, 0.15, 0.30]),   # 中皇权、女性、年轻
    "猫":   np.array([0.01, 0.50, 0.20]),   # 无皇权、性别中性、年轻
    "苹果": np.array([0.00, 0.00, 0.10]),   # 无皇权、无性别、无年龄
}

# ==================== 词向量的神奇运算:语义加减法 ====================
# 经典例子:国王 - 男人 + 女人 ≈ 女王
# 这说明词向量空间中存在"方向"的概念
# "男性→女性"是一个方向,"国王→女王"也沿着这个方向

result = word_vectors["国王"] - word_vectors["男人"] + word_vectors["女人"]
print("国王 - 男人 + 女人 =", result.round(2))

# 计算结果与"女王"的相似度
def cosine_similarity(a, b):
    """计算余弦相似度"""
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# 找出与计算结果最相似的词
best_word = None
best_sim = -1
for word, vec in word_vectors.items():
    sim = cosine_similarity(result, vec)
    if sim > best_sim:
        best_sim = sim
        best_word = word

print(f"最接近的词是: '{best_word}',相似度: {best_sim:.4f}")

# ==================== 词语相似度矩阵 ====================
# 计算所有词对之间的余弦相似度
words = list(word_vectors.keys())
n = len(words)
sim_matrix = np.zeros((n, n))

for i in range(n):
    for j in range(n):
        sim_matrix[i][j] = cosine_similarity(word_vectors[words[i]], word_vectors[words[j]])

print("\n词语相似度矩阵(部分):")
print(f"{'':>6}", end="")
for w in words[:5]:
    print(f"{w:>8}", end="")
print()
for i in range(5):
    print(f"{words[i]:>6}", end="")
    for j in range(5):
        print(f"{sim_matrix[i][j]:>8.2f}", end="")
    print()

# ==================== 嵌入在AI中的实际应用 ====================
# 1. 文本分类:将整段文本的词向量平均,得到文本向量,送入分类器
# 2. 语义搜索:将查询和文档都转为向量,用余弦相似度匹配
# 3. 推荐系统:将用户行为和物品都嵌入到同一向量空间

# 示例:用词向量平均得到句子向量
def sentence_vector(sentence_words, word_vec_dict):
    """将句子中所有词的向量取平均,得到句子向量"""
    vectors = []
    for w in sentence_words:
        if w in word_vec_dict:
            vectors.append(word_vec_dict[w])
    if len(vectors) == 0:
        return np.zeros(3)  # 如果没有匹配的词,返回零向量
    return np.mean(vectors, axis=0)  # 取平均值

# 两个句子的语义相似度
sentence1_words = ["国王", "女王"]
sentence2_words = ["王子", "公主"]

sv1 = sentence_vector(sentence1_words, word_vectors)
sv2 = sentence_vector(sentence2_words, word_vectors)

sim = cosine_similarity(sv1, sv2)
print(f"\n'国王女王' 与 '王子公主' 的语义相似度: {sim:.4f}")
print("(都是皇室成员,语义相近,相似度高)")
Word2Vec Skip-gram 目标函数 L = Σ_{t=1}^{T} Σ_{-c≤j≤c, j≠0} log P(w_{t+j} | w_t)
大白话 外层求和遍历语料库中的每个位置 t,从 1 到 T,T 是语料库长度
大白话 内层求和遍历中心词 w_t 周围的上下文窗口,范围是 -c 到 c(不含 0)
大白话 c 是窗口大小,控制上下文的范围,c 越大捕获的上下文信息越丰富
大白话 P(w_{t+j} | w_t) 是给定中心词预测上下文词出现的条件概率
大白话 取对数 log 是将连乘转化为连加,避免数值下溢,同时便于梯度计算
大白话 整体目标是最大化这个对数似然,即让模型尽可能准确地预测上下文
大白话 训练过程中,语义相近的词因为共享相似的上下文,会获得相近的向量表示
大白话 在AI中,Skip-gram 是 Word2Vec 的核心架构之一,是现代词嵌入和语义理解的基础

什么用(应用):词向量是现代NLP的基石。在机器翻译中,不同语言的词向量空间可以对齐,实现跨语言翻译;在搜索引擎中,查询词和文档都转为向量,通过相似度匹配实现语义搜索(而非关键词匹配);在推荐系统中,用户和物品都可以嵌入到同一向量空间,实现个性化推荐。大语言模型(如GPT)的输入层本质上也是一个嵌入层。

哪些坑(缺点):传统词向量(如Word2Vec)是一词一向量,无法区分多义词(如"苹果"可以是水果也可以是公司);词向量的质量高度依赖训练语料的规模和质量;维度选择需要权衡——太低丢失信息,太高增加计算量;词向量训练是静态的,不会根据上下文动态变化(BERT等模型解决了这个问题)。


三、主成分分析(PCA)——最经典的降维

是什么(定义):主成分分析(Principal Component Analysis,PCA)是一种无监督的线性降维方法。它通过正交变换,将可能相关的高维变量转换为线性不相关的低维变量,这些新的变量称为主成分。主成分按方差从大到小排列——第一个主成分方向是数据方差最大的方向,第二个主成分方向是与第一个正交且方差次大的方向,以此类推。

大白话 想象你拍了一张正面照和一张侧面照来描述一个人的长相。如果这个人特别"长"(比如很高),那正面照就包含了最多的信息——PCA就是自动找出"信息量最大"的那个观察角度。它把数据沿着信息最丰富的方向重新"摆好",然后你可以只保留信息最多的几个方向,丢掉信息少的,就实现了降维。

为什么(原理):PCA的数学原理基于协方差矩阵的特征值分解。数据的协方差矩阵反映了各维度之间的相关性,对协方差矩阵做特征值分解,特征值最大的方向就是方差最大的方向(即信息最丰富的方向)。选择前k个最大特征值对应的特征向量作为新坐标轴,就完成了降维。降维后的数据保留了原始数据中尽可能多的方差(信息)。

怎么做(实现)

import numpy as np

# ==================== PCA 的完整实现步骤 ====================

# 第一步:生成模拟数据(2维数据,方便可视化理解)
np.random.seed(42)  # 设置随机种子,确保结果可复现

# 生成一组沿着某个方向分布的 2D 数据点
n_samples = 100  # 样本数量
# 原始数据沿 45 度方向分布,有一定的散布
mean = [0, 0]  # 均值
# 协方差矩阵:控制数据的分布方向和散布程度
# [[3, 2], [2, 3]] 表示 x 和 y 正相关,数据沿 45 度方向拉伸
cov = [[3, 2], [2, 3]]
X = np.random.multivariate_normal(mean, cov, n_samples)  # 生成多元正态分布数据

print("原始数据形状:", X.shape)  # (100, 2) 100个样本,2个特征
print("原始数据前5个样本:\n", X[:5].round(2))

# 第二步:数据中心化(减去均值)
# PCA 要求先对每个特征减去其均值,使数据中心在原点
X_mean = np.mean(X, axis=0)  # 计算每个特征的均值,axis=0 按列求均值
X_centered = X - X_mean       # 每个样本减去均值

print("\n数据中心化后均值:", X_centered.mean(axis=0).round(10))  # 应该接近 [0, 0]

# 第三步:计算协方差矩阵
# 协方差矩阵衡量各特征之间的线性相关性
# 对角线元素是各特征的方差,非对角线元素是特征间的协方差
cov_matrix = np.cov(X_centered, rowvar=False)  # rowvar=False 表示每列是一个特征

print("\n协方差矩阵:\n", cov_matrix.round(4))

# 第四步:对协方差矩阵做特征值分解
# 特征值表示该方向上的方差大小(信息量)
# 特征向量表示该主成分的方向
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)  # eigh 适用于对称矩阵

# 按特征值从大到小排序
idx = np.argsort(eigenvalues)[::-1]  # 降序排列的索引
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

print("\n特征值(从大到小):", eigenvalues.round(4))
print("特征向量(每列是一个主成分方向):\n", eigenvectors.round(4))

# 第五步:计算方差解释比例
# 每个主成分解释了多少比例的总方差
explained_variance_ratio = eigenvalues / np.sum(eigenvalues)
print("\n各主成分的方差解释比例:", explained_variance_ratio.round(4))
print(f"第一主成分解释了 {explained_variance_ratio[0]*100:.1f}% 的方差")
print(f"第二主成分解释了 {explained_variance_ratio[1]*100:.1f}% 的方差")

# 第六步:选择主成分并投影(降维)
# 如果我们降到 1 维,只保留第一主成分
n_components = 1  # 目标维度
W = eigenvectors[:, :n_components]  # 取前 n_components 个特征向量组成投影矩阵

print(f"\n投影矩阵 W 形状: {W.shape}")  # (2, 1)
print("投影矩阵 W:\n", W.round(4))

# 将原始数据投影到低维空间
X_pca = X_centered @ W  # 矩阵乘法完成投影

print(f"\n降维后数据形状: {X_pca.shape}")  # (100, 1)
print("降维后数据前5个样本:\n", X_pca[:5].round(4))

# 第七步:重建(从低维回到高维,会有信息损失)
X_reconstructed = X_pca @ W.T + X_mean  # 反投影并加回均值
reconstruction_error = np.mean((X - X_reconstructed) ** 2)  # 均方重建误差
print(f"\n重建均方误差: {reconstruction_error:.4f}")
print("(降到1维时,丢失了第二主成分的信息,所以有重建误差)")
PCA投影公式 Z = X_centered · W
大白话 X_centered 是中心化后的数据矩阵,形状为 (n_samples, n_features),每列均值为 0
大白话 中心化的目的是让数据的"重心"落在原点,使 PCA 找到的主成分过原点
大白话 W 是投影矩阵,由前 k 个特征向量按列组成,形状为 (n_features, k)
大白话 每个特征向量代表一个主成分方向,是数据方差最大的正交方向
大白话 矩阵乘法 X_centered · W 本质上是将数据沿各主成分方向做正交投影
大白话 Z 是降维后的数据,形状为 (n_samples, k),k 是目标维度
大白话 Z 的每一行是原始样本在 k 个主成分方向上的坐标,即低维表示
大白话 在AI中,PCA 投影广泛用于数据压缩和特征降维,在保留主要信息的同时大幅减少计算量
协方差矩阵 C = (1/(n−1)) X_centered^T · X_centered
大白话 协方差矩阵 C 的形状为 (n_features, n_features),是一个方阵
大白话 公式中的 1/(n-1) 是无偏估计的修正系数,n 是样本数
大白话 X_centered^T · X_centered 是中心化数据矩阵的转置与自身的乘积
大白话 对角线元素 C_ii 是第 i 个特征的方差,反映该特征的离散程度
大白话 非对角线元素 C_ij 是第 i 和第 j 个特征的协方差,反映它们的线性相关性
大白话 C_ij > 0 表示两特征正相关,C_ij < 0 表示负相关,C_ij = 0 表示无线性关系
大白话 协方差矩阵是对称半正定矩阵,其特征值都是非负的
大白话 在AI中,协方差矩阵是 PCA 降维的数学核心,特征值分解后取最大特征值对应的方向即为主成分

什么用(应用):PCA在AI中有广泛应用:(1) 数据可视化——将高维数据降到2D/3D以便人类观察;(2) 噪声过滤——丢弃方差小的主成分往往也去掉了噪声;(3) 特征压缩——在训练模型前先降维,减少计算量和过拟合风险;(4) 人脸识别——Eigenface方法用PCA提取人脸的主要变化方向;(5) 金融分析——用PCA提取股票市场的主要波动模式。

哪些坑(缺点):PCA只能捕获线性关系,对于非线性结构的数据(如瑞士卷形状),PCA降维效果很差;主成分的可解释性可能较差——新坐标轴是原始特征的线性组合,往往难以给出直观的物理含义;PCA对数据的尺度敏感,使用前必须标准化;降维会丢失信息,选择保留多少主成分需要权衡。


四、降维的直觉——从高维到低维的投影

是什么(定义):降维(Dimensionality Reduction)是将高维数据映射到低维空间的过程,同时尽可能保留原始数据中的重要信息。从几何角度看,降维就是"投影"——想象用手电筒照射一个三维物体,在墙上投下的二维影子就是降维的结果。好的降维方法就像选了一个最佳的照射角度,让影子尽可能保留原物的关键特征。

大白话 你站在一个雕塑前面拍照,雕塑是3D的,照片是2D的——这就是降维。如果你从正面拍,能看清脸部细节;如果从头顶往下拍,只能看到一个轮廓。选好拍摄角度,让2D照片尽可能多地保留3D雕塑的信息,这就是降维要解决的问题。

为什么(原理):降维的必要性来自"维度灾难"(Curse of Dimensionality):随着维度增加,数据在高维空间中变得越来越稀疏,模型需要指数级更多的数据才能学到有效模式。同时,高维数据中往往存在大量冗余——许多特征之间高度相关,真正独立的"信息维度"远少于特征数量。降维通过去除冗余和噪声,让模型聚焦于核心信息。

怎么做(实现)

import numpy as np

# ==================== 维度灾难的直观演示 ====================
# 在高维空间中,随机点之间的距离趋于相同,"近邻"概念失效

def avg_pairwise_distance(n_dims, n_points=1000, n_trials=5):
    """计算 n_dims 维空间中随机点对的平均距离"""
    distances = []
    for _ in range(n_trials):
        # 在 [0,1]^n_dims 超立方体中随机生成点
        points = np.random.rand(n_points, n_dims)
        # 计算所有点对之间的欧氏距离(采样部分点对以节省计算)
        for i in range(min(100, n_points)):
            for j in range(i+1, min(100, n_points)):
                dist = np.linalg.norm(points[i] - points[j])  # 欧氏距离
                distances.append(dist)
    return np.mean(distances), np.std(distances)

# 不同维度下的平均距离和标准差
print("维度 | 平均距离 | 距离标准差 | 标准差/均值")
print("-" * 55)
for dim in [2, 5, 10, 50, 100]:
    avg, std = avg_pairwise_distance(dim, n_points=500, n_trials=3)
    ratio = std / avg if avg > 0 else 0
    print(f"{dim:>4} | {avg:>8.4f} | {std:>9.4f} | {ratio:.4f}")

print("\n观察:随着维度增加,标准差/均值比越来越小")
print("这意味着高维空间中所有点对距离趋于相同,'近邻'概念失效!")

# ==================== 降维保留信息的衡量 ====================
# 用"重建误差"衡量降维损失了多少信息

np.random.seed(42)
# 生成 50 维数据,但只有 5 个维度真正有信息
n_samples = 200
n_features = 50
n_informative = 5  # 真正有信息的维度数

# 构造数据:前5维有较大方差,后45维只有微小噪声
informative_part = np.random.randn(n_samples, n_informative) * 10  # 信号强
noise_part = np.random.randn(n_samples, n_features - n_informative) * 0.1  # 噪声弱
X_high = np.hstack([informative_part, noise_part])  # 拼接成 50 维数据

# 中心化
X_centered = X_high - X_high.mean(axis=0)

# 计算协方差矩阵和特征值
cov_mat = np.cov(X_centered, rowvar=False)
eigenvalues, eigenvectors = np.linalg.eigh(cov_mat)
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]

# 计算累积方差解释比例
cumulative_ratio = np.cumsum(eigenvalues) / np.sum(eigenvalues)

print("\n\n累积方差解释比例(前10个主成分):")
for k in range(10):
    print(f"  前 {k+1} 个主成分: {cumulative_ratio[k]*100:.2f}%")

# 只需5个主成分就能解释几乎全部方差
print(f"\n前5个主成分解释了 {cumulative_ratio[4]*100:.2f}% 的方差")
print("这说明50维数据中只有5个'有效维度',降维到5维几乎不损失信息")

# ==================== 不同降维维度的重建误差 ====================
print("\n不同降维维度的重建误差:")
print("目标维度 | 重建误差(MSE) | 方差解释比例")
print("-" * 50)

for k in [1, 3, 5, 10, 20, 50]:
    W_k = eigenvectors[:, idx][:, :k]  # 取前 k 个特征向量
    X_proj = X_centered @ W_k           # 投影到 k 维
    X_recon = X_proj @ W_k.T + X_high.mean(axis=0)  # 重建
    mse = np.mean((X_high - X_recon) ** 2)  # 均方误差
    var_ratio = cumulative_ratio[k-1]
    print(f"  {k:>4} 维 | {mse:>12.4f} | {var_ratio*100:.2f}%")

什么用(应用):降维的直觉帮助我们在实际问题中做出关键决策:选择多少个主成分?是否需要降维?用线性降维还是非线性降维?理解"投影"和"信息保留"的直觉,能帮助我们判断降维是否合理,以及降维后模型性能下降是否在可接受范围内。在图像压缩、信号处理、基因数据分析等领域,降维都是核心步骤。

哪些坑(缺点):降维并非总是有益——如果原始特征都是独立且重要的,降维反而会丢失关键信息;过度降维(保留太少维度)会导致模型欠拟合;不同降维方法适用于不同数据结构,选错方法可能得到误导性结果;降维后的特征往往难以解释,影响模型的可解释性。


五、线性代数在神经网络中的角色

是什么(定义):神经网络中的几乎每一次计算都可以用线性代数来描述。一个全连接层的前向传播就是矩阵乘法加上偏置:y = Wx + b;反向传播中的梯度计算本质上是链式法则的矩阵形式;批量训练时,多个样本组成矩阵并行计算。线性代数不仅是神经网络的数学语言,更是其高效实现的基石。

大白话 神经网络就像一个超级复杂的"数字加工厂",输入是原料(向量),每层是一个加工站(矩阵乘法+偏移),输出是成品(向量)。整个加工过程就是一连串的矩阵运算。GPU之所以能飞速训练神经网络,就是因为它特别擅长做矩阵乘法——就像一个能同时处理几千条流水线的超级工厂。

为什么(原理):神经网络的核心运算是仿射变换(线性变换 + 平移)和非线性激活函数的交替。线性变换由权重矩阵 W 实现,它定义了输入到输出的映射关系。训练过程就是不断调整 W 中的参数,使得网络输出越来越接近目标。梯度下降法利用矩阵微积分高效计算所有参数的梯度,而批量矩阵运算则充分利用了现代硬件的并行计算能力。

怎么做(实现)

import numpy as np

# ==================== 用numpy实现一个简单的神经网络前向传播 ====================

def relu(x):
    """ReLU 激活函数:max(0, x)"""
    return np.maximum(0, x)

def softmax(x):
    """Softmax 函数:将向量转为概率分布"""
    # 减去最大值防止数值溢出
    exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

# ==================== 网络结构定义 ====================
# 一个简单的 3 层神经网络:输入4维 → 隐藏层8维 → 隐藏层6维 → 输出3维
# 可以理解为:4个特征 → 8个中间表示 → 6个中间表示 → 3个类别概率

np.random.seed(42)

# 第一层权重和偏置:4 → 8
W1 = np.random.randn(4, 8) * 0.1   # 权重矩阵,小随机值初始化
b1 = np.zeros(8)                     # 偏置向量,初始化为0

# 第二层权重和偏置:8 → 6
W2 = np.random.randn(8, 6) * 0.1
b2 = np.zeros(6)

# 第三层权重和偏置:6 → 3
W3 = np.random.randn(6, 3) * 0.1
b3 = np.zeros(3)

print("网络参数形状:")
print(f"  W1: {W1.shape}, b1: {b1.shape}")
print(f"  W2: {W2.shape}, b2: {b2.shape}")
print(f"  W3: {W3.shape}, b3: {b3.shape}")

# ==================== 单样本前向传播 ====================
# 输入一个样本(4维向量)
x_single = np.array([0.5, -0.3, 0.8, 0.1])  # 4个特征值

# 第一层:线性变换 + ReLU激活
z1 = x_single @ W1 + b1     # 线性变换:4维 → 8维
a1 = relu(z1)                # 非线性激活

# 第二层:线性变换 + ReLU激活
z2 = a1 @ W2 + b2           # 线性变换:8维 → 6维
a2 = relu(z2)                # 非线性激活

# 第三层:线性变换 + Softmax激活
z3 = a2 @ W3 + b3           # 线性变换:6维 → 3维
output = softmax(z3)         # 输出概率分布

print(f"\n单样本前向传播:")
print(f"  输入: {x_single}")
print(f"  第一层输出 (z1): {z1.round(3)}")
print(f"  第一层激活 (a1): {a1.round(3)}")
print(f"  第二层输出 (z2): {z2.round(3)}")
print(f"  第二层激活 (a2): {a2.round(3)}")
print(f"  最终输出概率: {output.round(4)}")
print(f"  预测类别: {np.argmax(output)} (概率: {output.max():.4f})")

# ==================== 批量前向传播(矩阵运算的威力) ====================
# 同时处理 32 个样本,利用矩阵乘法的并行性
batch_size = 32
X_batch = np.random.randn(batch_size, 4)  # 32个样本,每个4维

# 批量前向传播:一次矩阵乘法处理所有样本
Z1 = X_batch @ W1 + b1    # (32, 4) @ (4, 8) → (32, 8)
A1 = relu(Z1)

Z2 = A1 @ W2 + b2         # (32, 8) @ (8, 6) → (32, 6)
A2 = relu(Z2)

Z3 = A2 @ W3 + b3         # (32, 6) @ (6, 3) → (32, 3)
outputs = softmax(Z3)

print(f"\n批量前向传播:")
print(f"  输入批次形状: {X_batch.shape}")
print(f"  输出批次形状: {outputs.shape}")
print(f"  前3个样本的预测类别: {np.argmax(outputs[:3], axis=1)}")
print(f"  GPU可以并行处理这些矩阵运算,这就是深度学习快的原因!")

# ==================== 参数量计算 ====================
total_params = (W1.size + b1.size) + (W2.size + b2.size) + (W3.size + b3.size)
print(f"\n网络总参数量: {total_params}")
print(f"  = (4×8+8) + (8×6+6) + (6×3+3) = {4*8+8} + {8*6+6} + {6*3+3} = {total_params}")
print("  每个参数都是权重矩阵中的一个元素,训练就是优化这些数值")
神经网络前向传播 a^(l) = σ(W^(l) · a^(l−1) + b^(l))
大白话 a^(l) 是第 l 层的激活输出,a^(l-1) 是上一层的输出,作为本层的输入
大白话 W^(l) 是第 l 层的权重矩阵,形状为 (n_l, n_{l-1}),定义了层间连接强度
大白话 b^(l) 是偏置向量,形状为 (n_l,),提供平移能力,使变换不局限于过原点
大白话 W^(l) · a^(l-1) + b^(l) 是仿射变换,即线性变换加平移
大白话 σ 是非线性激活函数(如 ReLU、Sigmoid),没有它多层网络就退化为单层线性变换
大白话 ReLU 函数将负值截断为 0,引入了分段线性的非线性能力
大白话 整个网络就是多个"仿射变换 + 非线性激活"的堆叠,逐层提取更高级的特征
大白话 在AI中,这一公式是所有深度学习模型前向传播的统一表达,从 CNN 到 Transformer 都遵循此结构

什么用(应用):理解线性代数在神经网络中的角色,有助于:(1) 理解模型为什么能学习——权重矩阵定义了变换,训练调整变换;(2) 理解GPU加速的原理——矩阵乘法天然适合并行;(3) 理解模型参数量——权重矩阵的大小决定了模型容量;(4) 理解注意力机制——Transformer的核心就是矩阵运算(QKV点积)。

哪些坑(缺点):纯线性变换(没有激活函数)的多层网络等价于一层——这就是为什么激活函数不可或缺;权重矩阵过大(参数过多)容易过拟合;矩阵乘法的数值稳定性需要注意(梯度爆炸/消失);批量大小受GPU显存限制,需要权衡速度和内存。


六、实战:用numpy实现PCA降维

是什么(定义):本节将前面学到的PCA理论完整实现为一个可用的Python函数,并在真实场景中应用。我们将使用numpy从零实现PCA的每一步:数据标准化、协方差矩阵计算、特征值分解、主成分选择和数据投影,最后用累积方差解释比例来评估降维效果。

大白话 前面我们学了PCA的原理,现在把理论变成代码。就像学了做菜的理论,现在真正下厨。我们会一步步把PCA的数学公式翻译成numpy代码,然后在模拟数据上跑一遍,看看降维到底能压缩多少信息。

怎么做(实现)

import numpy as np

# ==================== 从零实现 PCA 类 ====================

class PCAReducer:
    """用numpy从零实现的PCA降维器"""
    
    def __init__(self, n_components=None, variance_threshold=None):
        """
        初始化PCA降维器
        
        参数:
            n_components: 目标维度数(直接指定保留几个主成分)
            variance_threshold: 方差解释比例阈值(如0.95表示保留95%方差)
        """
        self.n_components = n_components          # 目标维度
        self.variance_threshold = variance_threshold  # 方差阈值
        self.mean_ = None       # 训练数据的均值(用于中心化)
        self.components_ = None  # 主成分方向(特征向量)
        self.eigenvalues_ = None # 特征值
        self.explained_variance_ratio_ = None  # 方差解释比例
        self.n_components_ = None  # 实际保留的主成分数
    
    def fit(self, X):
        """
        在数据上拟合PCA模型
        
        参数:
            X: 数据矩阵,形状 (n_samples, n_features)
        """
        # 第一步:计算并保存均值,用于中心化
        self.mean_ = np.mean(X, axis=0)  # 每个特征的均值
        X_centered = X - self.mean_       # 中心化
        
        # 第二步:计算协方差矩阵
        n_samples = X_centered.shape[0]
        cov_matrix = (X_centered.T @ X_centered) / (n_samples - 1)
        
        # 第三步:特征值分解
        eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
        
        # 第四步:按特征值降序排列
        idx = np.argsort(eigenvalues)[::-1]
        eigenvalues = eigenvalues[idx]
        eigenvectors = eigenvectors[:, idx]
        
        # 第五步:计算方差解释比例
        total_variance = np.sum(eigenvalues)
        self.explained_variance_ratio_ = eigenvalues / total_variance
        
        # 第六步:确定保留的主成分数
        if self.n_components is not None:
            # 直接指定维度数
            self.n_components_ = self.n_components
        elif self.variance_threshold is not None:
            # 根据方差阈值自动确定维度数
            cumulative = np.cumsum(self.explained_variance_ratio_)
            self.n_components_ = np.searchsorted(cumulative, self.variance_threshold) + 1
        else:
            # 默认保留所有主成分
            self.n_components_ = len(eigenvalues)
        
        # 保存结果
        self.eigenvalues_ = eigenvalues
        self.components_ = eigenvectors[:, :self.n_components_]  # 取前k个特征向量
        
        return self
    
    def transform(self, X):
        """
        将数据投影到低维空间
        
        参数:
            X: 数据矩阵,形状 (n_samples, n_features)
        返回:
            降维后的数据,形状 (n_samples, n_components_)
        """
        X_centered = X - self.mean_  # 用训练时的均值中心化
        return X_centered @ self.components_  # 矩阵乘法完成投影
    
    def fit_transform(self, X):
        """拟合并转换"""
        self.fit(X)
        return self.transform(X)
    
    def inverse_transform(self, X_reduced):
        """
        从低维空间重建高维数据
        
        参数:
            X_reduced: 降维后的数据
        返回:
            重建的高维数据(会有信息损失)
        """
        return X_reduced @ self.components_.T + self.mean_
    
    def summary(self):
        """打印PCA结果摘要"""
        print(f"PCA降维摘要:")
        print(f"  原始维度: {self.components_.shape[0]}")
        print(f"  降维后维度: {self.n_components_}")
        print(f"  保留方差比例: {np.sum(self.explained_variance_ratio_[:self.n_components_])*100:.2f}%")
        print(f"\n  各主成分方差解释比例:")
        for i in range(min(self.n_components_, 10)):
            print(f"    PC{i+1}: {self.explained_variance_ratio_[i]*100:.2f}%")

# ==================== 实战:对高维数据进行PCA降维 ====================
np.random.seed(42)

# 模拟一个用户行为数据集:1000个用户,20个行为特征
n_users = 1000
n_features = 20

# 构造数据:只有5个独立的信息源,其余是它们的线性组合+噪声
n_sources = 5
sources = np.random.randn(n_users, n_sources) * np.array([10, 5, 3, 2, 1])[:, np.newaxis].T

# 随机混合矩阵:将5个源混合成20个特征
mix_matrix = np.random.randn(n_sources, n_features)
X = sources @ mix_matrix + np.random.randn(n_users, n_features) * 0.5  # 加少量噪声

print("=" * 60)
print("实战:用户行为数据的PCA降维")
print("=" * 60)
print(f"\n原始数据: {n_users} 个用户, {n_features} 个行为特征")

# 方式1:指定保留95%的方差
pca_auto = PCAReducer(variance_threshold=0.95)
X_reduced = pca_auto.fit_transform(X)
pca_auto.summary()

# 方式2:指定保留5个主成分
pca_k5 = PCAReducer(n_components=5)
X_reduced_k5 = pca_k5.fit_transform(X)
print(f"\n指定保留5个主成分:")
print(f"  降维后数据形状: {X_reduced_k5.shape}")
print(f"  保留方差比例: {np.sum(pca_k5.explained_variance_ratio_[:5])*100:.2f}%")

# ==================== 评估降维效果 ====================
# 重建误差:降维后重建,与原始数据比较
X_reconstructed = pca_k5.inverse_transform(X_reduced_k5)
mse = np.mean((X - X_reconstructed) ** 2)
total_var = np.mean(X ** 2)
print(f"\n降维效果评估:")
print(f"  重建均方误差(MSE): {mse:.4f}")
print(f"  原始数据总方差: {total_var:.4f}")
print(f"  相对误差: {mse/total_var*100:.2f}%")

# ==================== 数据压缩比 ====================
original_size = n_users * n_features  # 原始数据需要的存储量
compressed_size = n_users * 5 + 5 * n_features + n_features  # 降维数据 + 投影矩阵 + 均值
compression_ratio = original_size / compressed_size
print(f"\n数据压缩:")
print(f"  原始存储量: {original_size} 个数值")
print(f"  压缩后存储量: {compressed_size} 个数值")
print(f"  压缩比: {compression_ratio:.2f}x")
print(f"  (用约 {100/compression_ratio:.0f}% 的空间保留了 {np.sum(pca_k5.explained_variance_ratio_[:5])*100:.1f}% 的信息)")

什么用(应用):掌握PCA的numpy实现,不仅能深入理解降维原理,还能在没有scikit-learn的环境(如Pyodide)中使用。在实际项目中,PCA常用于:(1) 数据预处理——在训练复杂模型前先降维,加速训练;(2) 数据探索——通过主成分分析发现数据中的隐藏结构;(3) 特征工程——将PCA降维后的特征作为新特征加入模型。

哪些坑(缺点):自己实现PCA时容易犯的错误:(1) 忘记中心化——PCA要求零均值数据;(2) 忘记标准化——如果特征量纲不同,应先标准化再PCA;(3) 对测试数据用了不同的均值——必须用训练集的均值来中心化测试数据;(4) 特征值分解用了不稳定的算法——对于大矩阵应使用SVD代替特征值分解。

概念关系图谱

概念 上位概念 核心思想 关键公式/方法 AI应用场景
向量 线性代数基础 有方向和大小的有序数列 v = [v_1, v_2, ..., v_n] 数据表示、特征向量
矩阵 线性代数基础 数值的矩形排列 A ∈ R^(m×n) 图像表示、权重矩阵
张量 线性代数扩展 多维数值数组 T ∈ R^(d_1 × d_2 × ... × d_k) 彩色图像、批量数据
词向量 嵌入技术 将词语映射为稠密向量 Word2Vec, GloVe NLP、语义搜索
嵌入 表示学习 离散对象→连续向量 Embed(x) ∈ R^d 推荐系统、搜索
协方差矩阵 统计学 衡量特征间的线性关系 C = (1/(n−1)) X^T X PCA、特征分析
特征值分解 矩阵分解 找到矩阵的"主方向" Cv = λv PCA、谱聚类
PCA 降维方法 沿方差最大方向投影 Z = XW 数据压缩、可视化
降维 数据预处理 减少维度保留信息 PCA, t-SNE, UMAP 特征工程、去噪
余弦相似度 相似度度量 衡量向量方向相似性 cosθ = (a⋅b)/(|a||b|) 推荐系统、搜索
维度灾难 高维统计 高维数据稀疏性 距离集中现象 决定是否降维

重点答疑

Q1: PCA和特征选择的区别是什么?什么时候用PCA,什么时候用特征选择?

PCA和特征选择都是降维方法,但本质不同。特征选择是从原始特征中挑选子集,保留的是原始特征本身(如从100个特征中选20个),不改变特征的含义。PCA是特征提取,通过线性组合创建全新的特征(主成分),新特征是原始特征的加权混合,含义可能难以解释。

什么时候用PCA:特征之间高度相关(存在冗余);不关心特征的可解释性;需要最大化保留方差(信息);数据维度远大于样本数。

什么时候用特征选择:需要保留特征的可解释性;某些特征明确无用或有害;特征之间相对独立;业务规则要求使用特定特征。

解答 PCA是"创造新特征",特征选择是"挑选旧特征"。需要可解释性时用特征选择,需要信息最大化时用PCA。

Q2: 降维后模型的准确率一定会下降吗?

不一定!降维后模型准确率可能下降,也可能上升,取决于具体情况:

准确率可能上升的情况:原始数据中有大量噪声特征,降维去除了噪声;原始特征高度冗余,降维消除了多重共线性;数据维度远大于样本数(小样本高维问题),降维缓解了过拟合。

准确率可能下降的情况:降维过度,丢失了关键判别信息;数据本身维度不高且特征都重要;降维方法不适合数据结构(如对非线性数据用PCA)。

解答 降维不是简单的"信息丢失",而是"去芜存菁"。好的降维去除噪声和冗余,反而可能提升模型性能。关键在于选择合适的降维维度和方法。

Q3: 为什么PCA之前要标准化数据?不标准化会怎样?

PCA寻找方差最大的方向,而方差的量纲与原始特征的量纲直接相关。如果不标准化,量纲大的特征会主导PCA的结果。

例如:一个数据集有两个特征——身高(厘米,值在150-190之间,方差约100)和考试成绩(0-100分,方差约400)。不标准化就做PCA,第一主成分几乎完全由考试成绩决定,因为它的方差更大。但这并不意味着考试成绩更"重要"——只是它的数值范围更大。

标准化的方法:将每个特征减去均值再除以标准差,使所有特征的方差都为1,让PCA公平对待每个特征。

解答 不标准化时,量纲大的特征会"绑架"PCA结果。标准化让所有特征站在同一起跑线上,PCA才能找到真正有意义的方向。

Q4: 词向量中"国王-男人+女人≈女王"是怎么做到的?这真的有意义吗?

这个经典例子展示了词向量空间中的"语义方向"概念。在训练过程中,"国王"和"男人"经常出现在相似的上下文中(区别只在于"皇权"这个维度),"女王"和"女人"也是如此。因此,"国王→女王"的向量和"男人→女人"的向量大致平行,都指向"男性→女性"这个语义方向。

数学上:v_国王 - v_男人 ≈ v_女王 - v_女人,所以 v_国王 - v_男人 + v_女人 ≈ v_女王。

但这有意义吗?有,但要谨慎:(1) 它证明了词向量确实捕获了某种语义关系;(2) 它不是完美的,结果向量通常只是"最接近女王"而非"精确等于女王";(3) 这种线性关系在更复杂的语义关系上不一定成立;(4) 存在性别偏见等问题——"医生-男人+女人"可能得到"护士"而非"女医生"。

解答 词向量的语义加减法证明了向量空间中存在有意义的"方向",但它是近似的而非精确的,且可能携带训练数据中的偏见。

Q5: 神经网络中为什么必须要有非线性激活函数?全用线性变换不行吗?

如果全用线性变换(没有激活函数),无论网络有多少层,最终都等价于一个单层线性变换。数学证明:

假设两层线性变换:y = W_2(W_1 x + b_1) + b_2

展开得:y = W_2 W_1 x + W_2 b_1 + b_2

令 W' = W_2 W_1,b' = W_2 b_1 + b_2,则 y = W' x + b'

这就是一个单层线性变换!多层线性网络等价于一层线性网络,无法学习非线性模式(如XOR问题)。非线性激活函数(ReLU、Sigmoid等)打破了这种等价性,使网络能够拟合任意复杂的函数。

解答 没有激活函数,多层网络退化为单层,只能学线性关系。激活函数是网络的"非线性引擎",让深度学习成为可能。

Q6: 协方差矩阵的特征值和特征向量在PCA中分别代表什么含义?

特征值代表该主成分方向上的方差大小,也就是该方向上包含的"信息量"。特征值越大,说明数据在该方向上越分散,包含的信息越多。特征值为0的方向,数据完全没有变化(所有点重合),不包含任何信息。

特征向量代表主成分的方向,即数据在新坐标系中的轴方向。每个特征向量是一个单位向量,指向数据方差最大的方向(在已排除之前主成分的条件下)。特征向量的每个分量表示对应原始特征在该主成分中的权重(贡献度)。

例如,如果第一特征向量是 [0.7, 0.7](近似值),说明第一主成分大致沿着45度方向,两个原始特征对第一主成分的贡献差不多。如果第一特征向量是 [0.98, 0.14],说明第一主成分几乎完全由第一个特征决定。

解答 特征值="这条轴上有多少信息",特征向量="这条轴指向哪个方向"。PCA按信息量从大到小排列轴,保留信息最多的几条轴就完成了降维。

章节单词汇总

英文音标术语/释义
vector /ˈvektər/ 向量
matrix /ˈmeɪtrɪks/ 矩阵
tensor /ˈtensər/ 张量
dimension /daɪˈmenʃn/ 维度
dimensionality reduction /daɪˌmenʃəˈnæləti rɪˈdʌkʃn/ 降维
principal component analysis /ˈprɪnsəpl kəmˈpoʊnənt əˈnæləsɪs/ 主成分分析
covariance /koʊˈveriəns/ 协方差
eigenvalue /ˈaɪɡənˌvæljuː/ 特征值
eigenvector /ˈaɪɡənˌvektər/ 特征向量
embedding /emˈbedɪŋ/ 嵌入
word embedding /wɜːrd emˈbedɪŋ/ 词向量/词嵌入
projection /prəˈdʒekʃn/ 投影
variance /ˈveriəns/ 方差
cosine similarity /ˈkoʊsaɪn ˌsɪməˈlærəti/ 余弦相似度
dot product /dɑːt ˈprɑːdʌkt/ 点积
orthogonal /ɔːrˈθɑːɡənl/ 正交
feature extraction /ˈfiːtʃər ɪkˈstrækʃn/ 特征提取
curse of dimensionality /kɜːrs əv daɪˌmenʃəˈnæləti/ 维度灾难
activation function /ˌæktɪˈveɪʃn ˈfʌŋkʃn/ 激活函数
forward propagation /ˈfɔːrwərd ˌprɑːpəˈɡeɪʃn/ 前向传播
reconstruction error /ˌriːkənˈstrʌkʃn ˈerər/ 重建误差
standardization /ˌstændərdaɪˈzeɪʃn/ 标准化
cumulative /ˈkjuːmjəleɪtɪv/ 累积的
sparse /spɑːrs/ 稀疏的
dense /dens/ 稠密的

面试练习

单选题 在PCA中,第一主成分的方向是什么?
A数据均值最大的方向
B数据方差最大的方向
C数据均值最小的方向
D数据方差最小的方向