七月在线公开课笔记-全-
七月在线公开课笔记(全)


📘 课程名称:循环神经网络与Pytorch框架(课程编号:P1)
📋 概述
在本节课中,我们将要学习神经网络的基础知识,并深入探讨其在自然语言处理(NLP)中的核心应用。课程将从基础的神经网络概念开始,逐步介绍循环神经网络(RNN)、长短期记忆网络(LSTM)以及卷积神经网络(CNN)的原理与应用。最后,我们将通过一个基于LSTM的文本分类实战项目,将理论知识付诸实践。

🧠 第一部分:神经网络基础

上一节我们介绍了课程的整体框架,本节中我们来看看什么是神经网络。
神经网络是一种模仿人脑神经元连接方式的计算模型。它通过构建多层节点(神经元)之间的连接来学习数据中的复杂模式。
神经网络的基本结构
假设我们要预测深圳的房价,并且认为房价只与房屋面积有关。我们可以建立一个简单的线性关系:Y = WX + B。其中,Y是房价,X是面积,W是权重,B是偏移项。
然而,现实中房价受多种因素影响,如面积、周边配套、房龄等。因此,我们需要一个能处理多个输入特征的模型。如果将每个特征视为一个输入节点,并为其分配一个权重,那么输出Y可以表示为输入向量X与权重向量W的转置相乘,再加上偏移项B。这就是一个最简单的神经网络。
神经网络的层与激活函数
一个基础的神经网络包含输入层、隐藏层和输出层。隐藏层中的每个节点值由前一层所有节点的加权和,经过一个激活函数计算得出。
激活函数(如Sigmoid、ReLU)的作用是引入非线性。如果没有激活函数,无论叠加多少层神经网络,其效果都等价于一个单层线性模型。激活函数使得网络能够学习并模拟复杂的非线性关系。
隐藏层的节点数和层数是需要调整的超参数,这决定了模型的复杂度和学习能力。
神经网络的可视化
我们可以通过在线工具可视化神经网络的训练过程。例如,给定一个由蓝色点和橙色点组成的数据集,我们可以构建一个神经网络来区分它们。通过调整隐藏层的数量和节点数,观察模型如何收敛并形成决策边界。
🔄 第二部分:循环神经网络(RNN)
上一节我们介绍了基础的神经网络,本节中我们来看看专门用于处理序列数据的循环神经网络。
循环神经网络(RNN)通过在网络层中引入循环连接,使其能够处理具有时间或顺序依赖性的数据,如文本、语音等。
RNN的基本原理
在基础神经网络中,隐藏层状态H的计算仅依赖于当前输入X。而在RNN中,H的计算还依赖于上一时刻的隐藏状态H_{t-1}。其核心公式可以表示为:
H_t = σ(U * X_t + W * H_{t-1} + B)
其中,σ是激活函数,U和W是权重矩阵,B是偏移项。权重在时间步之间是共享的。
这种结构使得RNN能够“记住”之前的信息,非常适合处理像文本这样具有顺序性的数据。
RNN的输入输出模式
RNN可以配置成多种输入输出模式,以适应不同的任务:
- 一对一:标准神经网络,用于普通分类或回归。
- 一对多:如根据标题生成文章,属于序列生成任务。
- 多对一:如文本分类,输入整个序列,输出一个类别标签。
- 多对多(异步):如机器翻译,输入和输出序列长度可能不同。
- 多对多(同步):如命名实体识别或词性标注,输入和输出序列长度相同且位置对应。
RNN的梯度消失问题
RNN在训练长序列时,容易遇到梯度消失问题。这是因为在反向传播过程中,梯度需要沿着时间步连续相乘。如果每个时间步的梯度都小于1(例如使用Sigmoid激活函数时),经过多个时间步后,梯度会指数级衰减到接近零,导致较早时间步的参数几乎无法更新,模型难以学习长期依赖关系。
🧠 第三部分:长短期记忆网络(LSTM)
上一节我们了解到RNN存在梯度消失的缺陷,本节中我们来看看为解决此问题而设计的LSTM。
长短期记忆网络(LSTM)是RNN的一种变体,通过引入“门”机制和额外的“细胞状态”,有效地缓解了梯度消失问题,能够学习长期依赖关系。
LSTM的核心结构:门机制
LSTM通过三个门来控制信息的流动:
- 遗忘门:决定从上一细胞状态
C_{t-1}中丢弃哪些信息。它查看H_{t-1}和X_t,输出一个0到1之间的值给C_{t-1}中的每个部分。 - 输入门:决定将哪些新信息存入细胞状态。它包含两部分:一个Sigmoid层决定更新哪些值,一个tanh层创建新的候选值向量
C̃_t。 - 输出门:基于当前的细胞状态
C_t,决定输出什么隐藏状态H_t。
LSTM的工作流程
细胞状态C_t是LSTM的关键,它像一个传送带,在整个链上运行,只有少量的线性交互,信息可以很容易地保持不变地流动。其更新公式体现了“遗忘”和“记忆”的平衡:
C_t = f_t * C_{t-1} + i_t * C̃_t
其中,f_t是遗忘门输出,i_t是输入门输出。
隐藏状态H_t则由输出门和当前细胞状态C_t经过tanh激活后共同决定。
LSTM如何缓解梯度消失
在反向传播时,细胞状态C_t的梯度计算包含一项相加操作(f_t * C_{t-1}的梯度),而非全是连乘。即使其他路径的梯度很小,只要遗忘门f_t的输出不为零,梯度就能得到一定程度的保留,从而缓解了梯度消失。
🎨 第四部分:卷积神经网络(CNN)与文本处理
上一节我们学习了用于序列建模的RNN/LSTM,本节中我们来看看并行能力更强的卷积神经网络如何应用于文本。
卷积神经网络(CNN)以其强大的局部特征提取能力和并行计算效率,在图像处理中取得了巨大成功,同样也能有效地应用于文本数据。
卷积操作
卷积在离散情况下的本质是“先相乘,再求和”。在图像处理中,使用一个卷积核(滤波器)在图像上滑动,每次与覆盖区域的像素值进行对位相乘并求和,从而提取局部特征(如边缘、纹理)。
CNN处理文本
对于文本,我们首先将每个词转换为词向量,形成一个二维矩阵(序列长度 × 词向量维度)。然后,我们定义不同宽度的卷积核(如宽度为2、3、4),其高度必须与词向量维度一致,以确保能处理一个完整的词向量。
卷积核在文本矩阵上滑动,每次覆盖连续的几个词(即n-gram),提取该短语级别的特征。使用多个卷积核可以提取多种特征。为了保持输入输出序列长度一致,可以在文本两端进行填充(Padding)。
池化操作
卷积后得到的特征图可能维度仍然较高。为了得到固定长度的向量表示以进行分类,通常使用池化层(如最大池化)来降维。最大池化即取每个特征通道上的最大值,它能捕捉最显著的特征。


CNN用于文本的优势
与RNN的串行处理不同,CNN可以并行计算文本中所有n-gram的特征,计算效率更高。它擅长提取文本中的局部关键短语特征,在许多文本分类任务上表现优异。
💻 第五部分:实战 - 基于LSTM的文本分类
上一节我们介绍了多种神经网络理论,本节中我们将通过一个实战项目来巩固所学知识。
我们将使用PyTorch框架,构建一个基于LSTM的文本情感二分类模型。




1. 数据加载与预处理
以下是数据处理的关键步骤:
- 读取数据:从本地文件加载文本和对应的情感标签(正面/负面)。
- 构建词典:对训练文本进行分词,统计词频,并为每个词分配一个唯一的ID。词典需包含
[PAD]和[UNK]两个特殊标记。 - 文本转下标:将每条文本的分词结果转换为对应的ID序列。
- 序列填充:为保证批次训练,将所有文本序列填充或截断到相同的固定长度。
- 创建DataLoader:使用PyTorch的
DataLoader将数据集封装,以便按批次(batch)加载数据。
2. 定义LSTM模型
使用PyTorch定义模型需要继承nn.Module类,并实现__init__和forward方法。
import torch.nn as nn





class LSTMModel(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, num_layers):
super(LSTMModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True)
self.fc1 = nn.Linear(hidden_size, 100)
self.fc2 = nn.Linear(100, 2) # 二分类输出
def forward(self, x):
# x: [batch_size, seq_len]
embedded = self.embedding(x) # [batch_size, seq_len, embed_size]
lstm_out, _ = self.lstm(embedded) # lstm_out: [batch_size, seq_len, hidden_size]
# 取最后一个时间步的输出
last_hidden = lstm_out[:, -1, :] # [batch_size, hidden_size]
out = self.fc1(last_hidden)
out = self.fc2(out)
return out





3. 模型训练流程
训练循环遵循以下标准步骤:
- 前向传播:将批次数据
X输入模型,得到预测输出。 - 计算损失:使用交叉熵损失函数计算预测输出与真实标签
Y之间的差异。 - 反向传播:
- 优化器梯度清零(
optimizer.zero_grad())。 - 执行损失的反向传播(
loss.backward()),计算所有参数的梯度。 - 优化器根据梯度更新模型参数(
optimizer.step())。
- 优化器梯度清零(
- 循环迭代:重复以上步骤多个轮次(epoch),直到模型收敛。

通过这个实战,我们完整地体验了使用PyTorch构建、训练一个深度学习模型的过程。










📝 总结
本节课中我们一起学习了神经网络的核心知识体系。我们从最基础的神经网络概念出发,理解了其前向传播、损失计算和通过梯度下降进行反向传播的训练原理。随后,我们深入探讨了适用于序列数据的循环神经网络(RNN),并分析了其梯度消失的问题。为此,我们引入了通过门机制缓解此问题的长短期记忆网络(LSTM)。此外,我们还了解了卷积神经网络(CNN)如何并行地提取文本中的局部特征。最后,通过一个基于PyTorch和LSTM的文本分类实战项目,我们将理论应用于实践,巩固了模型构建、数据处理和训练流程的完整技能。理解RNN/LSTM的梯度消失原因及其解决方案,是深入掌握序列建模的关键。
📚 课程名称:NLP高端就业小班10期 - P2:基于RNN的文本分类与语言模型
📖 课程概述
在本节课中,我们将学习语言模型和文本分类的基本概念,并重点介绍如何使用PyTorch构建循环神经网络(RNN)模型来解决这些问题。课程内容涵盖语言模型的定义、评价方法、RNN及其变体(LSTM、GRU)的原理,以及如何用代码实现和训练这些模型。


🧠 语言模型简介
语言模型的核心任务是计算一个句子出现的概率。这个概率衡量了一句话的合理性。例如,“七月在线是一所好学校”比“七月在线一是学好所效”出现的概率更大。
核心公式
给定一个句子 ( S = w_1, w_2, ..., w_n ),其概率可以通过链式法则计算:
[
P(S) = P(w_1) \cdot P(w_2|w_1) \cdot ... \cdot P(w_n|w_1, w_2, ..., w_)
]
在传统n-gram模型中,我们使用马尔可夫假设,即下一个词只依赖于前n个词。但在神经网络模型中,我们可以用更长的历史信息。
模型评价:困惑度(Perplexity)
困惑度是评价语言模型好坏的标准,计算公式为:
[
\text = 2^
]
其中 ( J ) 是模型在测试集上的平均交叉熵损失。困惑度越低,表示模型越好。
🔄 基于神经网络的语言模型
神经网络语言模型同样基于前面的单词预测下一个单词,但使用神经网络(如RNN)来拟合概率 ( P ),而不是简单地计算频率。


循环神经网络(RNN)基础
一个简单的RNN单元在每个时间步 ( t ) 的计算如下:
[
h_t = \tanh(W_ h_ + W_ x_t)
]
[
\hatt = \text(W h_t)
]
其中:
- ( h_t ) 是当前隐藏状态。
- ( x_t ) 是当前输入(单词的嵌入向量)。
- ( \hat_t ) 是预测的下一个单词的概率分布。
初始隐藏状态 ( h_0 ) 通常初始化为零向量。
训练与损失函数
使用交叉熵损失函数,并对序列中每个预测位置计算损失:
[
\text = -\sum_^ \log P(w_t | w_{<t})
]
优化器常用Adam。
RNN的挑战:梯度消失与爆炸
RNN在训练时,梯度需要通过时间反向传播(Backpropagation Through Time, BPTT)。长序列会导致梯度变得极小(消失)或极大(爆炸)。
解决方法:
- 梯度裁剪(Gradient Clipping):解决梯度爆炸,将梯度范数限制在阈值内。
- 使用LSTM或GRU:更复杂的门控机制,能更好地捕捉长期依赖,缓解梯度消失。
🏗️ 长短时记忆网络(LSTM)与门控循环单元(GRU)
上一节我们介绍了基础RNN及其训练挑战,本节中我们来看看两种更强大的变体。
LSTM(长短时记忆网络)
LSTM通过引入“门”结构(输入门、遗忘门、输出门、细胞状态)来控制信息的流动和记忆,有效缓解了梯度消失问题。PyTorch中实现了标准的LSTM单元。
GRU(门控循环单元)
GRU是LSTM的简化版本,它将遗忘门和输入门合并为“更新门”,并引入了“重置门”。参数更少,训练速度往往更快。
核心总结:
- 实践中,LSTM和GRU比基础RNN更常用。
- LSTM同时传递隐藏状态 ( h_t ) 和细胞状态 ( c_t )。
- 训练时通常仍会使用梯度裁剪作为稳定训练的技巧。
💻 代码实践:构建语言模型
以下是使用PyTorch和torchtext库构建语言模型的关键步骤概述。
1. 数据准备与torchtext
torchtext是一个用于文本处理的库,可以帮助我们加载数据、构建词汇表和数据迭代器。
关键步骤:
- 定义
Field来处理文本(如转为小写)。 - 使用
LanguageModelingDataset加载数据。 - 从训练数据构建词汇表(
build_vocab),并设置最大词汇量和未知词标记。 - 创建
BPTTIterator,它将长文本按固定长度(bptt_len)切分成批次,用于BPTT训练。
2. 定义模型结构
我们定义一个通用的RNN模型类,可以配置为RNN、LSTM或GRU。
import torch.nn as nn
class RNNModel(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, rnn_type='LSTM'):
super(RNNModel, self).__init__()
self.encoder = nn.Embedding(vocab_size, embed_size)
if rnn_type == 'LSTM':
self.rnn = nn.LSTM(embed_size, hidden_size)
# ... 可以添加GRU和RNN选项
self.decoder = nn.Linear(hidden_size, vocab_size)
def forward(self, text, hidden):
embedded = self.encoder(text) # shape: [seq_len, batch, embed_size]
output, hidden = self.rnn(embedded, hidden)
decoded = self.decoder(output.view(-1, output.size(2)))
return decoded.view(output.size(0), output.size(1), decoded.size(1)), hidden
def init_hidden(self, batch_size):
# 初始化隐藏状态,与模型参数在同一设备上
weight = next(self.parameters())
if isinstance(self.rnn, nn.LSTM):
return (weight.new_zeros(1, batch_size, self.hidden_size),
weight.new_zeros(1, batch_size, self.hidden_size))
else:
return weight.new_zeros(1, batch_size, self.hidden_size)
3. 训练循环与技巧
关键训练循环步骤:
- 初始化隐藏状态。
- 获取一个批次的数据(
text和target,target是text向右偏移一位)。 - 前向传播,计算损失。
- 反向传播,进行梯度裁剪。
- 优化器更新参数。
重要技巧:隐藏状态再包装(Repackaging Hidden States)
为了避免在非常长的序列上反向传播导致计算图过大和内存爆炸,我们在每个小批次(bptt_len长度)训练后,将隐藏状态从其计算历史中分离(detach),只保留其数值用于下一个批次的初始化。
def repackage_hidden(h):
if isinstance(h, torch.Tensor):
return h.detach()
else:
return tuple(repackage_hidden(v) for v in h)
4. 模型评估、保存与加载
- 评估:在验证集上计算困惑度,监控模型性能。
- 保存:当验证损失达到新低时,保存模型的
state_dict()。 - 加载:
model.load_state_dict(torch.load(‘model.pth’))。
5. 文本生成
使用训练好的模型,可以从一个种子词开始,递归地预测下一个词来生成文本。采样时可以使用multinomial采样增加多样性,或使用argmax获取确定性结果。
📊 文本分类简介
文本分类是另一个重要的NLP任务,例如情感分析、垃圾邮件识别。其目标是将一段文本分配到一个预定义的类别。
基本思路
无论使用什么模型,核心思路都是将变长的文本编码成一个固定长度的句子向量,然后通过一个分类器(如全连接层)进行分类。
常用方法:
- 词向量平均(Word Average):将句子中所有词的词向量取平均,作为句子表示。简单但有效。
- RNN编码:使用RNN(或LSTM/GRU)处理整个句子,用最后一个隐藏状态作为句子表示。
- 双向RNN(Bi-RNN):分别从前向后和从后向前处理句子,将两个方向的最终隐藏状态拼接,能更好地捕获上下文信息。
- 卷积神经网络(CNN):使用不同尺寸的卷积核在词向量序列上滑动,提取局部特征,然后通过池化层得到句子表示。(下节课重点)
使用torchtext处理分类数据
与语言模型类似,但Field定义可能包含更复杂的分词器(如spaCy),并且需要LabelField来处理标签。可以使用BucketIterator来将长度相似的句子分到同一个批次,以减少填充(Padding)的数量,提升效率。
🎯 课程总结
本节课我们一起学习了:
- 语言模型的定义、评价指标(困惑度)及其数学基础。
- 循环神经网络(RNN) 的原理、训练方法及其梯度问题。
- LSTM和GRU 的结构与优势。
- 使用 PyTorch 和
torchtext库构建、训练、评估语言模型的完整流程,包括数据加载、模型定义、训练技巧(梯度裁剪、隐藏状态再包装)和文本生成。 - 文本分类任务的基本概念和常用模型框架,为下节课深入讲解CNN文本分类打下基础。


通过本课的学习,你应该掌握了使用RNN家族模型处理序列数据的基本能力,并了解了文本分类任务的基本范式。



📘 课程名称:NLP高端就业小班10期 - P3:基于CNN的文本分类
📋 课程概述
在本节课中,我们将学习如何使用卷积神经网络(CNN)来处理自然语言处理(NLP)任务。课程内容分为四个部分:首先,我们将补充讲解Transformer模型的剩余部分;其次,分享一些阅读学术论文的建议;然后,深入探讨CNN的基本概念及其在文本处理中的应用;最后,我们将通过代码实践,复现一个基于CNN的文本分类模型。
🔍 第一部分:Transformer模型补充讲解
上一节我们介绍了Transformer模型的核心组件——自注意力机制和多头注意力。本节中,我们将继续探讨Transformer的其他关键结构,包括残差连接、层归一化以及解码器中的掩码机制。
残差连接(Residual Connection)
残差连接的核心思想是让模型训练变得更加简单。它通过将输入直接添加到输出中,形成一条“捷径”,有助于缓解深层网络中的梯度消失问题。
公式表示:
输出 = F(x) + x
其中,F(x) 是网络层的输出,x 是原始输入。
层归一化(Layer Normalization)
层归一化的目的是将输入数据转化为均值为0、方差为1的分布,从而缓解梯度消失或爆炸的问题。与批归一化不同,层归一化在序列维度上进行归一化,更适合处理变长文本数据。
公式表示:
y = (x - μ) / √(σ² + ε) * γ + β
其中,μ 是均值,σ² 是方差,ε 是一个极小值防止除零,γ 和 β 是可学习的参数。





位置编码(Positional Encoding)
由于Transformer模型是并行处理的,它本身不具备序列的顺序信息。因此,需要通过位置编码将序列的位置信息嵌入到输入中。Transformer使用正弦和余弦函数来计算位置编码。
公式表示:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中,pos 是位置,i 是维度索引,d_model 是模型维度。
解码器中的掩码机制(Mask in Decoder)
在解码器中,为了防止模型在生成当前词时看到未来的信息,需要使用掩码机制。具体做法是在计算注意力权重时,将未来位置的权重置为零。
代码描述:
# 假设 attention_scores 是注意力分数矩阵
# 生成一个下三角矩阵作为掩码
mask = torch.tril(torch.ones(seq_len, seq_len))
masked_scores = attention_scores.masked_fill(mask == 0, -1e9)
📚 第二部分:阅读学术论文的建议
在深入学习NLP的过程中,阅读原论文是理解模型细节和思想的重要途径。以下是一些阅读论文的建议。


如何查找论文
- 关注学术资讯网站,如知乎、公众号、CSDN等。
- 使用专业论文网站,如Paper Weekly、Papers with Code。
- 利用论文管理工具,如ReadPaper,方便管理和阅读论文。



阅读论文的顺序
- 阅读摘要和简介:快速了解论文的主要内容和贡献。
- 重点阅读模型部分:理解模型的结构和核心思想。
- 查看实验部分:关注实验结果和消融实验,了解模型的性能。
- 复现代码:如果有开源代码,可以结合代码进一步理解论文。



复现论文的注意事项
- 复现论文时,可能会发现论文描述与代码实现存在差异。
- 建议先阅读多篇相关论文的代码,积累经验后再进行复现。
- 注意论文中的实验通常基于英文数据集,在中文数据集上的效果可能需要自行验证。












🧠 第三部分:卷积神经网络(CNN)基础
上一节我们介绍了Transformer模型,本节中我们来看看卷积神经网络(CNN)及其在文本处理中的应用。



什么是卷积?
卷积是一种数学运算,用于提取局部特征。在图像处理中,卷积通过滑动窗口对局部区域进行加权求和,从而提取特征。

离散卷积公式:
(f * g)[n] = Σ f[k] * g[n - k]
其中,f 和 g 是两个函数,n 是索引。

卷积神经网络(CNN)
CNN通过卷积核提取输入数据的局部特征,并通过反向传播更新卷积核的权重。在文本处理中,卷积核的第一个维度表示短语的长度,第二个维度与词向量的维度保持一致。
代码描述:
import torch.nn as nn
# 定义一个卷积层
conv_layer = nn.Conv2d(in_channels=1, out_channels=2, kernel_size=(3, embedding_dim))
文本中的卷积操作
在文本处理中,卷积核沿着序列方向滑动,每次提取局部短语的特征。例如,使用3×4的卷积核可以提取三个词组成的短语特征。
填充(Padding)与步长(Stride)
- 填充:在序列两端补零,保持输出序列长度不变。
- 步长:卷积核滑动的步长,影响输出序列的长度。
输出维度计算公式:
输出长度 = (输入长度 + 2 * 填充 - 卷积核大小) / 步长 + 1
多通道与池化(Pooling)
- 多通道:使用多个卷积核提取不同特征。
- 池化:进一步压缩特征,常用最大池化或平均池化。
代码描述:
# 最大池化
pool_layer = nn.MaxPool1d(kernel_size=2)
🛠️ 第四部分:CNN在文本分类中的应用
本节中,我们将介绍TextCNN模型,并使用PyTorch实现一个简单的文本分类任务。
TextCNN模型结构
TextCNN使用多种不同大小的卷积核提取文本特征,然后通过池化层压缩特征,最后通过全连接层进行分类。
模型结构:
- 输入层:文本序列的词向量表示。
- 卷积层:使用多种大小的卷积核提取特征。
- 池化层:对每个卷积核的输出进行最大池化。
- 全连接层:将池化后的特征拼接,并通过全连接层分类。
PyTorch实现TextCNN
以下是TextCNN的PyTorch实现代码:


import torch
import torch.nn as nn




class TextCNN(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.conv1 = nn.Conv2d(1, 2, kernel_size=(2, embedding_dim))
self.conv2 = nn.Conv2d(1, 2, kernel_size=(3, embedding_dim))
self.conv3 = nn.Conv2d(1, 2, kernel_size=(4, embedding_dim))
self.pool = nn.MaxPool1d(kernel_size=2)
self.dropout = nn.Dropout(0.5)
self.fc = nn.Linear(6, 2) # 假设输出为二分类
def forward(self, x):
x = self.embedding(x).unsqueeze(1) # 增加通道维度
out1 = self.conv1(x).squeeze(3)
out2 = self.conv2(x).squeeze(3)
out3 = self.conv3(x).squeeze(3)
out1 = self.pool(out1).squeeze(2)
out2 = self.pool(out2).squeeze(2)
out3 = self.pool(out3).squeeze(2)
out = torch.cat([out1, out2, out3], dim=1)
out = self.dropout(out)
out = self.fc(out)
return out



模型训练与评估
使用上述模型在情感分析数据集上进行训练,可以通过调整超参数(如卷积核数量、学习率等)来优化模型性能。





📝 课程总结
本节课中,我们一起学习了以下内容:
- Transformer模型的补充知识:包括残差连接、层归一化、位置编码和解码器中的掩码机制。
- 阅读学术论文的建议:如何查找、阅读和复现论文。
- 卷积神经网络(CNN)基础:卷积的概念、CNN在文本处理中的应用以及相关操作(如填充、步长、池化)。
- TextCNN模型实现:使用PyTorch实现一个基于CNN的文本分类模型。




通过本节课的学习,希望大家能够掌握CNN在文本处理中的应用,并能够独立实现和优化文本分类模型。




📚 NLP课程笔记:文本相似度计算与文本匹配模型
在本节课中,我们将要学习自然语言处理(NLP)中的一个重要应用——文本相似度计算。我们将从概念、应用场景入手,逐步深入到两种核心的实现方法:基于表示学习的相似度计算和基于匹配模型的相似度计算,并重点讲解ESIM模型的结构与实现。


🎯 第一部分:什么是文本相似度?
文本相似度,顾名思义,就是计算两条文本之间的相似程度。它与文本分类最大的区别在于,文本分类针对单条文本,而文本相似度计算需要同时处理两条文本。
文本相似度在众多场景中都有广泛应用:
- 信息检索:例如搜索引擎,根据用户查询(query)与文档库(documents)的相似度对结果进行排序。
- 问答系统:将用户问题与知识库中的问题进行相似度匹配,返回最相似问题对应的答案。
- 文本聚类:通过计算文本间两两相似度,将语义相近的句子聚集在一起。
在计算相似度时,我们通常采用余弦相似度作为度量标准。其计算公式如下:
similarity = (A · B) / (||A|| * ||B||)
其中,A和B代表两个文本的向量表示。余弦相似度的值域在-1到1之间,对于文本向量,其值大多落在0到1的区间内,数值越接近1,表示相似度越高。
🔍 第二部分:基于表示学习的相似度计算
上一节我们介绍了文本相似度的基本概念,本节中我们来看看第一种实现方法——基于表示学习。其核心思想是:通过一个模型将句子转换为一个固定维度的向量(即句子的Embedding),然后计算这两个向量之间的余弦相似度。





整个流程可以概括为:句子 → 分词 → 词向量 → 句子向量 → 计算余弦相似度。






关键步骤:从词向量到句子向量
模型(如Word2Vec, ELMo, BERT)最初生成的是每个词的向量。为了得到整个句子的向量,我们需要进行聚合。常用方法是加权平均:
Vsentence = α1 * Vword1 + α2 * Vword2 + ... + αn * Vwordn
其中,α代表每个词的权重。一种简单有效的方法是取均值,即令所有权重α = 1/n。




以ELMo模型为例
ELMo是一个基于双层双向LSTM的预训练语言模型。它通过在海量文本上预测下一个词的任务进行无监督预训练,从而学习到丰富的语言表示。
- 结构:输入词向量,经过前向和后向LSTM层,将每一时间步的前后向输出拼接起来,得到该词的上下文相关表示。
- 使用:我们可以直接加载预训练的ELMo权重,输入句子后,对模型输出的所有词向量取平均(或加权平均),即可得到句子的向量表示,进而用于相似度计算。
以下是使用预训练ELMo模型获取句子向量的示例代码框架:
import torch
import jieba
from elmoformanylangs import Embedder
# 初始化模型,指定预训练权重路径
e = Embedder('path/to/your/pretrained/model')
# 句子分词
sentence = "这是一个例句"
tokens = list(jieba.cut(sentence))
# 获取句子中所有词的向量
vectors = e.sents2elmo([tokens])
# 对词向量取平均,得到句子向量
sentence_embedding = torch.mean(torch.tensor(vectors[0]), dim=0)
🤖 第三部分:基于匹配模型的相似度计算
基于表示学习的方法速度快,适合召回阶段。本节我们来看另一种效果通常更优的方法——基于匹配模型。这类模型将两条文本直接输入网络,通过模型内部的复杂交互,最终输出一个相似度分数或分类结果(如同义/不同义)。



匹配模型的结构主要分为两类:
- 双塔结构(外交互):两个文本分别通过一个模型(通常共享权重)编码为向量,再对这两个向量进行交互计算(如余弦相似度、全连接层)得到结果。DSSM是代表性模型。
- 单塔结构(内交互):将两条文本拼接或通过注意力机制在模型内部进行早期交互,再通过深层网络处理。ESIM、BERT等属于此类。



ESIM模型详解
ESIM是一个基于LSTM和注意力机制的经典匹配模型,结构清晰,效果出色。它主要包含四层:







- Input Encoding:使用双向LSTM分别对两个句子进行编码,获取每个词的上下文表示。
- Local Inference Modeling:这是核心层。通过注意力机制计算两个句子词与词之间的相关性(软对齐),并生成加权后的句子表示。具体步骤为:
- 计算句子A和B词向量的点积相似度矩阵E。
- 对E分别按行和列进行softmax归一化,得到注意力权重。
- 用权重对原句子向量进行加权求和,得到新的表示
ã和b̃。 - 将原始表示、新表示以及它们的差和元素积拼接起来,增强交互信息:
m_a = [a, ã, a - ã, a ⊙ ã]。
- Inference Composition:将增强后的表示再次输入双向LSTM,进一步组合信息。
- Pooling & Output:对LSTM的输出分别进行最大池化和平均池化,将结果拼接后通过全连接层和softmax进行分类。






在实现ESIM时需注意维度变化:双向LSTM会使输出维度翻倍;Local Inference层中的拼接会使维度变为4倍。因此,第二层LSTM的输入维度应为 hidden_size * 2 * 4 = hidden_size * 8。




📝 总结与回顾



本节课中我们一起学习了文本相似度计算的核心知识:
- 概念与应用:理解了文本相似度是计算两条文本的语义接近程度,并了解了其在检索、问答、聚类等场景的应用。
- 表示学习方法:掌握了通过模型(如ELMo)将句子编码为向量,再计算余弦相似度的流程。这种方法速度快,常用于召回。
- 匹配模型方法:深入剖析了以ESIM为代表的匹配模型,其通过注意力机制等结构让文本在模型内部进行深度交互,效果更好,常用于精排。
- 实践要点:学习了ESIM模型的结构、注意力机制的计算方式,并了解了模型实现中的关键维度处理。








两种方法常结合使用,形成“召回-排序”的流水线,兼顾效率与效果。理解注意力机制是掌握现代NLP模型(如BERT)的关键。


【七月在线】NLP高端就业训练营10期 - P1:1. 循环神经网络与Pytorch框架 📚
在本节课中,我们将要学习神经网络的基础知识,特别是循环神经网络(RNN)及其变体LSTM,以及如何利用PyTorch框架构建一个文本分类模型。课程内容从基础概念到实战应用,旨在帮助初学者理解核心原理并掌握基本操作。
神经网络基础 🧠
上一节我们介绍了课程概述,本节中我们来看看什么是神经网络。神经网络是一种模仿人脑神经元连接方式的计算模型,用于从数据中学习复杂的模式。
假设我们想构建一个模型来预测深圳的房价。如果只考虑房屋面积这一个特征,价格 Y 和面积 X 之间可能呈现线性关系,即 Y = WX + B。其中 W 是权重,B 是偏移项。
然而,房价通常受多个因素影响,如面积、周边配套、房龄等。每个特征 X_i 都有对应的权重 W_i。此时,价格 Y 可以表示为所有特征加权和加上偏移项:Y = W_1*X_1 + W_2*X_2 + ... + W_n*X_n + B。我们可以将输入特征看作向量 X,权重看作向量 W,则 Y = X * W^T + B。这种映射关系可以看作一个最简单的神经网络。

一个基础的神经网络通常包含输入层、隐藏层和输出层。以下是其关键组成部分:
- 输入层:接收原始特征数据。
- 隐藏层:对输入特征进行交互计算,引入非线性变换。每个节点的值
H由前一层所有节点的加权和,经过一个激活函数得到,例如H = σ(W_1*X_1 + W_2*X_2 + ... + B)。 - 输出层:产生最终的预测结果。
- 激活函数:如Sigmoid、ReLU,用于引入非线性,使网络能够学习更复杂的关系。没有激活函数,多层网络会退化为单层网络。
- 超参数:如隐藏层的层数和节点数,需要根据任务进行调整。

神经网络的可视化可以帮助我们理解其结构和工作原理。通过调整网络层数和节点,模型可以学习对不同数据分布进行分类。
除了基础神经网络,还有许多变体,如卷积神经网络(CNN)和循环神经网络(RNN),它们针对不同类型的数据(如图像、序列)进行了优化。
模型训练:损失函数与梯度下降 ⚙️
上一节我们介绍了神经网络的结构,本节中我们来看看如何训练神经网络,即求解网络中的参数(权重 W 和偏移项 B)。
训练的目标是让模型的预测结果尽可能接近真实值。这个过程主要涉及三个核心步骤:前向传播计算输出、计算损失、通过梯度下降反向传播更新参数。
前向传播与输出
以图像分类为例,模型接收一张图片(转换为数字张量),经过网络层层计算,最终得到一个输出值。对于二分类任务(如判断是否为猫),通常在网络末端使用Sigmoid激活函数。Sigmoid函数将任意输入映射到(0,1)区间,公式为 σ(z) = 1 / (1 + e^{-z})。输出值大于0.5则判定为正类(是猫),小于0.5则判定为负类。
对于多分类或多标签任务,输出层可以设置多个节点,每个节点经过Sigmoid函数后独立判断是否属于某个类别。
损失函数
损失函数衡量模型预测值与真实标签之间的差距。对于二分类问题,常用二元交叉熵损失函数。
给定输入 X,模型预测其为正类的概率为 ŷ。则样本属于其真实标签 Y(0或1)的似然为 ŷ^Y * (1-ŷ)^{(1-Y)}。为了便于计算,我们对其取负对数,得到损失函数:
Loss = - [Y * log(ŷ) + (1-Y) * log(1-ŷ)]
对于包含 M 个样本的数据集,总损失是所有样本损失的平均值:
Total Loss = -(1/M) * Σ [Y_i * log(ŷ_i) + (1-Y_i) * log(1-ŷ_i)]
训练的目标就是最小化这个总损失。
梯度下降与反向传播
梯度下降是用于最小化损失函数、优化模型参数的算法。想象损失函数是一个山谷,我们的目标是找到谷底(最小损失点)。梯度指出了当前点处函数值下降最快的方向。
参数更新公式为:W_new = W_old - η * ∇Loss_W。其中:
η是学习率,控制每次更新的步长。∇Loss_W是损失函数对参数W的梯度(偏导数)。
学习率过大可能导致在最优值附近震荡,难以收敛;学习率过小则收敛速度慢。
反向传播是计算梯度的高效方法。它利用链式法则,从输出层开始,逐层向后计算损失函数对每一层参数的梯度。
整个训练流程是一个循环:
- 前向传播:输入数据,得到预测输出。
- 计算损失:比较预测输出与真实标签,计算损失值。
- 反向传播:计算损失对各个参数的梯度。
- 参数更新:使用梯度下降法更新所有参数。
重复这个过程,直到损失收敛到满意程度或达到预定训练轮数。
循环神经网络 🔄
上一节我们介绍了通用的神经网络训练原理,本节中我们来看看专门用于处理序列数据的循环神经网络。
普通神经网络假设输入之间是独立的,但许多数据(如文本、语音、时间序列)具有顺序依赖关系。RNN通过在其隐藏层中引入“循环”结构来解决这个问题,使其能够保留之前输入的信息。
RNN基本原理
在RNN中,当前时刻的隐藏状态 H_t 不仅取决于当前输入 X_t,还取决于上一时刻的隐藏状态 H_{t-1}。其核心计算公式为:
H_t = σ(U * X_t + W * H_{t-1} + B)
其中:
U是输入权重矩阵。W是循环权重矩阵(连接上一时刻隐藏状态)。B是偏移项。σ是激活函数(如tanh)。
RNN在每个时刻 t 也可以产生一个输出 O_t,例如 O_t = softmax(V * H_t)。关键点在于,参数 U、W、V 在所有时间步是共享的。
这种结构使得RNN天然适合处理文本等序列数据,因为文本中的词序本身就包含了重要信息。
RNN的输入输出模式
RNN可以根据任务需求设计成不同的输入输出结构:
- 一对一:标准神经网络,用于图像分类等。
- 一对多:如根据一张图片生成描述文字(图像标注)。
- 多对一:如文本情感分类,输入整个序列,输出一个情感标签。
- 多对多(异步):如机器翻译,输入完整源语言序列,输出完整目标语言序列。
- 多对多(同步):如序列标注(命名实体识别、词性标注),为输入序列的每个元素都产生一个输出。
RNN的梯度消失问题
虽然RNN适合处理序列,但它存在一个严重问题:梯度消失。在训练深层RNN或处理长序列时,通过时间反向传播的梯度会随着时间步的增加而指数级缩小(当梯度值小于1时)。
原因在于,梯度计算涉及连续多个雅可比矩阵(激活函数导数)的连乘。如果激活函数(如Sigmoid、tanh)的梯度很小,连乘后梯度会迅速趋近于零。这导致模型难以学习到长距离的依赖关系,早期时间步的参数几乎得不到有效更新。
长短期记忆网络 🧠➡️💾
上一节我们指出了RNN的梯度消失问题,本节中我们来看看其改进版本——长短期记忆网络,它能够更好地学习长期依赖。
LSTM通过引入精妙的“门”机制和独立的“细胞状态”来缓解梯度消失问题。细胞状态 C_t 像一条传送带,贯穿整个时间序列,只有少量的线性交互,使得信息可以长时间流动。
LSTM的门机制
LSTM包含三个门,用于控制信息的保留与遗忘:
遗忘门:决定从上一细胞状态
C_{t-1}中丢弃哪些信息。
f_t = σ(W_f · [H_{t-1}, X_t] + B_f)
输出一个0到1之间的向量,1表示“完全保留”,0表示“完全遗忘”。输入门:决定将哪些新信息存入细胞状态。
- 首先,一个Sigmoid层决定更新哪些值:
i_t = σ(W_i · [H_{t-1}, X_t] + B_i) - 然后,一个tanh层创建新的候选值向量:
\tilde{C}_t = tanh(W_C · [H_{t-1}, X_t] + B_C)
- 首先,一个Sigmoid层决定更新哪些值:
更新细胞状态:将旧状态
C_{t-1}按遗忘门比例遗忘,并加上输入门筛选后的新候选值。
C_t = f_t * C_{t-1} + i_t * \tilde{C}_t输出门:基于更新后的细胞状态,决定输出什么隐藏状态
H_t。o_t = σ(W_o · [H_{t-1}, X_t] + B_o)H_t = o_t * tanh(C_t)
为何LSTM能缓解梯度消失?
在反向传播时,细胞状态 C_t 的梯度计算涉及一项 f_t(遗忘门输出)的连加,而非连乘。即使其他路径梯度消失,只要遗忘门 f_t 接近1,梯度就可以通过这条“高速公路”有效地向前传播。这使网络能够学习到长距离的依赖关系。当然,如果 f_t 也很小,问题依然存在,因此LSTM是“缓解”而非“根除”梯度消失。
卷积神经网络在文本中的应用 🎯
上一节我们介绍了用于序列建模的RNN/LSTM,本节中我们来看看另一种常用于文本处理的网络——卷积神经网络,它擅长提取局部特征且易于并行计算。
CNN的核心操作是卷积,它通过一个可学习的滤波器(卷积核)在输入数据上滑动,进行局部特征的抽取。
卷积操作
对于离散数据,卷积本质上是对位相乘再求和。在图像处理中,一个3x3的卷积核在图像上滑动,每次计算覆盖区域内像素值与核权重的点积,生成新的特征图。这个过程可以平滑图像、检测边缘等。
在文本处理中,输入是一个序列,每个词由词向量表示。假设一句话有7个词,每个词向量是4维,则输入是一个 7x4 的矩阵。
文本卷积
我们定义一个卷积核,其宽度必须等于词向量的维度(例如4),以确保能处理完整的词语义。其高度(或称宽度)k 是可以调节的超参数,代表一次查看多少个连续的词(即n-gram特征)。例如 k=3 的卷积核可以提取三元短语的特征。
这个 k x 4 的卷积核在 7x4 的输入矩阵上从上到下滑动,每次计算覆盖的 k 个词向量与核权重的点积,最终输出一个长度为 (7 - k + 1) 的向量。
为了保持输入输出序列长度一致(如在序列标注任务中),可以在序列两端进行填充。
多通道与池化
与图像有RGB三通道类似,我们可以使用多个卷积核(如32个 3x4 的核)来提取不同类型的特征。这样,输入经过卷积后,会得到32个特征图(通道)。
为了将变长的卷积输出转换为固定长度的向量以进行分类,通常使用池化操作,如最大池化。它对每个特征图(通道)取所有位置的最大值,最终得到一个一维向量,再输入全连接层进行分类。
CNN处理文本的优势在于能并行提取多种n-gram特征,且计算效率高。但它对长距离依赖的建模能力通常弱于RNN。
PyTorch实战:基于LSTM的文本分类 🚀


上一节我们学习了多种神经网络理论,本节中我们将利用PyTorch框架,实战构建一个基于LSTM的文本分类模型。
1. 数据加载与预处理
首先,我们需要加载和预处理文本数据。
以下是关键步骤:
- 读取数据:从文件读取文本和对应的情感标签(正面/负面)。
- 构建词典:
- 对所有文本进行分词。
- 统计词频,并按词频降序排序。
- 创建词到索引的映射字典。通常保留前N个高频词,并为未知词和填充符添加特殊标记(如
[UNK],[PAD])。
- 文本转索引:将每条文本的分词结果转换为对应的索引序列。
- 序列填充:为了批量处理,需将不同长度的序列填充到相同长度(如最大长度),短序列补
[PAD],长序列截断。 - 创建DataLoader:将数据封装成PyTorch的
Dataset和DataLoader,以便按批次(batch)加载数据,节省内存。
2. 定义LSTM模型
使用PyTorch定义模型需要继承 nn.Module 类,并实现 __init__ 和 forward 方法。
import torch.nn as nn
class LSTMModel(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, num_layers, num_classes):
super(LSTMModel, self).__init__()
# 词嵌入层
self.embedding = nn.Embedding(vocab_size, embed_size)
# LSTM层
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True)
# 全连接输出层
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
# x shape: (batch_size, seq_length)
embedded = self.embedding(x) # (batch_size, seq_length, embed_size)
# LSTM输出包含 (output, (h_n, c_n))
lstm_out, _ = self.lstm(embedded) # lstm_out shape: (batch_size, seq_length, hidden_size)
# 取最后一个时间步的输出用于分类
last_time_step_out = lstm_out[:, -1, :] # (batch_size, hidden_size)
out = self.fc(last_time_step_out) # (batch_size, num_classes)
return out
__init__:初始化网络层,包括嵌入层、LSTM层和全连接层。forward:定义前向传播路径。输入是单词索引序列,经过嵌入层得到词向量,输入LSTM后取最后一个时间步的隐藏状态,最后通过全连接层得到分类结果。


3. 模型训练


训练循环包括前向传播、损失计算、反向传播和参数更新。
import torch.optim as optim
# 初始化模型、损失函数、优化器
model = LSTMModel(...)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)



# 训练循环
for epoch in range(num_epochs):
for batch_x, batch_y in train_loader: # 从DataLoader取批次数据
# 前向传播
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
# 反向传播与优化
optimizer.zero_grad() # 清空过往梯度
loss.backward() # 反向传播,计算当前梯度
optimizer.step() # 根据梯度更新参数
# 每个epoch后评估模型准确率
# ...




- 优化器:如Adam,是梯度下降算法的增强版,能自适应调整学习率。
zero_grad():在每次反向传播前需清零梯度,否则梯度会累加。backward():自动计算损失相对于所有可训练参数的梯度。step():执行一步参数更新。


运行此训练流程,模型在训练集上的准确率会逐步提升。


总结 📝
本节课中我们一起学习了神经网络的核心知识。




我们首先从基础神经网络入手,理解了其结构、前向传播、以及通过损失函数和梯度下降进行训练的原理。

接着,我们深入探讨了循环神经网络,它通过引入循环结构来处理序列数据,但其存在梯度消失的问题。为此,我们学习了LSTM,它利用门机制和细胞状态有效缓解了此问题,能够学习长期依赖。

此外,我们还了解了卷积神经网络如何应用于文本,通过卷积核提取局部n-gram特征,并通过池化层得到固定长度的表示。




最后,我们进行了PyTorch实战,完整实现了从数据预处理、构建LSTM模型到训练评估的文本分类流程。通过本节课,希望大家能够掌握这些核心神经网络模型的基本概念和在NLP中的典型应用,为后续更深入的学习打下坚实基础。
【七月在线】NLP高端就业训练营10期 - P2:2.基于RNN的文本分类与语言模型 📚
概述
在本节课中,我们将学习语言模型和文本分类的基本概念,并重点介绍如何使用PyTorch构建循环神经网络(RNN)模型来解决这些问题。我们将从语言模型的定义和评价方法开始,逐步深入到RNN、LSTM和GRU的具体实现,最后探讨如何将这些模型应用于文本分类任务。
语言模型介绍
语言模型的核心任务是计算一个句子出现的概率。这个概率衡量了一句话的合理性。例如,“七月在线是一所好学校”比“七月在线一是学好所效”出现的概率更大,正常人更可能说出前者。

语言模型的应用

如果你能知道每句话出现的概率,就可以完成许多任务,例如完形填空或文本生成。因为你可以判断哪些后续文本的概率更高。
链式法则与马尔可夫假设
在构建语言模型时,我们通常遵循链式法则,即一个句子出现的概率等于其中每个词基于前面词出现的条件概率的乘积。
在传统模型中,我们常常使用马尔可夫假设,即下一个词出现的概率只依赖于前面的N个词,忽略更早的历史。这有助于减少模型参数。但在神经网络模型中,我们可以让模型看到更长的历史信息。
语言模型的评价:困惑度(Perplexity)
我们通常使用困惑度来评价一个语言模型的好坏。困惑度的计算公式是句子概率的负N分之一次方。这样做有两个原因:首先,句子概率通常非常小,取负次方可以将其变为较大的数字以便观察;其次,取N分之一次方可以消除句子长度对概率的影响,进行归一化。
困惑度越低,表示模型对句子的预测越好,模型质量越高;困惑度越高,则表示模型越“困惑”,质量越差。
基于神经网络的语言模型
基于神经网络的语言模型,其目标与传统模型一致:根据前面的若干个单词预测下一个单词。关键区别在于,我们使用一个神经网络(而非简单的计数统计)来拟合条件概率P。
在语言模型中,我们经常使用循环神经网络(RNN)来进行预测。
循环神经网络(RNN)的基本架构

RNN的基本思想是维护一个隐藏状态(hidden state),该状态随着序列的输入而更新。在每一个时间步t,模型接收当前输入单词Xt和上一个隐藏状态Ht-1,通过神经网络计算得到当前隐藏状态Ht,再利用Ht预测下一个单词。

一个典型的RNN单元计算公式如下:
Ht = tanh(Whh * Ht-1 + Wxh * Xt + bh)
其中,Whh和Wxh是权重矩阵,bh是偏置项。
初始隐藏状态H0通常初始化为全零向量。
预测输出Yt(即下一个单词的概率分布)通过对隐藏状态Ht进行线性变换得到:
Yt = softmax(Why * Ht + by)
这里,Why将隐藏状态映射到整个词表大小的维度上。
RNN的训练与损失函数
训练RNN时,我们通常使用交叉熵损失函数。需要注意的是,损失是在每一个预测的时间步上计算的。假设我们有一个长度为N的文本,模型需要预测从第2个到第N+1个单词。每一步的预测都会产生一个损失,总损失是这些步骤损失的和。
优化方法通常使用随机梯度下降(SGD)或其变种,如Adam优化器,因为它们通常效果较好。
RNN的挑战:梯度消失与爆炸
训练RNN的一个主要困难是梯度消失和梯度爆炸问题。这是因为在反向传播时,梯度需要沿着时间步连续相乘。当序列很长时,连续的乘法可能导致梯度变得极小(消失)或极大(爆炸)。
为了解决梯度爆炸,可以采用梯度裁剪(gradient clipping)技术,即当梯度范数超过某个阈值时,将其缩放。
为了解决梯度消失,研究者提出了更复杂的循环单元结构。
长短时记忆网络(LSTM)与门控循环单元(GRU)
长短时记忆网络(LSTM)
LSTM通过引入“门”机制和额外的细胞状态(cell state)来更好地控制信息的流动和记忆,有效缓解了梯度消失问题。
一个LSTM单元包含三个门:
- 遗忘门(Forget Gate):决定从细胞状态中丢弃哪些信息。
- 输入门(Input Gate):决定哪些新信息将被存储到细胞状态中。
- 输出门(Output Gate):基于细胞状态,决定输出什么信息到隐藏状态。
LSTM在传递过程中同时维护两个状态:隐藏状态H和细胞状态C。PyTorch中实现的LSTM版本是一个更流行的变体。
门控循环单元(GRU)
GRU是LSTM的一个简化版本,它将遗忘门和输入门合并为一个“更新门”,并取消了细胞状态,只有隐藏状态。GRU的参数更少,训练速度可能更快,但在许多任务上表现与LSTM相当。
模型选择总结
在实际项目中,我们很少使用最基础的RNN,大部分时候会选择LSTM或GRU。LSTM同时传递隐藏状态和细胞状态,而GRU只传递一个隐藏状态。
实践部分:使用PyTorch和TorchText构建语言模型
上一节我们介绍了RNN家族的理论基础,本节中我们来看看如何用代码实现一个语言模型。我们将使用torchtext库来简化文本数据的处理。
环境准备与数据加载
首先,导入必要的库并设置超参数。
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext import data, datasets
# 设置随机种子以保证结果可复现
torch.manual_seed(1234)
# 超参数
BATCH_SIZE = 32
EMBEDDING_SIZE = 100
HIDDEN_SIZE = 100
NUM_EPOCHS = 2
GRADIENT_CLIP = 5.0
LEARNING_RATE = 0.001
接下来,使用torchtext加载和预处理数据。我们使用text8数据集作为示例。
# 定义文本字段,指定预处理(如转为小写)
TEXT = data.Field(lower=True)
# 加载语言模型数据集
train_data, val_data, test_data = datasets.LanguageModelingDataset.splits(
path='.',
train='text8.train.txt',
validation='text8.valid.txt',
test='text8.test.txt',
text_field=TEXT
)
# 构建词表,只保留最高频的50000个词,其余用<unk>表示
TEXT.build_vocab(train_data, max_size=50000)
VOCAB_SIZE = len(TEXT.vocab)
print(f"词表大小: {VOCAB_SIZE}")
构建数据迭代器
我们需要将数据组织成批次(batch)。对于语言模型,我们使用BPTTIterator,它会将长文本序列切割成较短的片段(由bptt_len参数控制),以便进行随时间反向传播。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建数据迭代器
train_iter, val_iter, test_iter = data.BPTTIterator.splits(
(train_data, val_data, test_data),
batch_size=BATCH_SIZE,
bptt_len=50, # 反向传播的时间步长度
device=device,
repeat=False,
shuffle=True
)
定义RNN语言模型
现在,我们来定义模型。这个模型包含一个词嵌入层、一个循环神经网络层(这里以LSTM为例)和一个输出层。
class RNNModel(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size):
super(RNNModel, self).__init__()
self.hidden_size = hidden_size
# 词嵌入层
self.encoder = nn.Embedding(vocab_size, embed_size)
# LSTM层
self.rnn = nn.LSTM(embed_size, hidden_size, batch_first=False)
# 输出层,将隐藏状态映射回词表空间
self.decoder = nn.Linear(hidden_size, vocab_size)
def forward(self, input, hidden):
# input shape: [seq_len, batch_size]
embedded = self.encoder(input) # [seq_len, batch_size, embed_size]
output, hidden = self.rnn(embedded, hidden) # output: [seq_len, batch_size, hidden_size]
# 将输出重塑为二维张量以输入线性层
decoded = self.decoder(output.view(-1, self.hidden_size)) # [seq_len*batch_size, vocab_size]
# 将输出重塑回三维
return decoded.view(output.size(0), output.size(1), decoded.size(-1)), hidden
def init_hidden(self, batch_size):
# 初始化隐藏状态和细胞状态为零
weight = next(self.parameters())
return (weight.new_zeros(1, batch_size, self.hidden_size),
weight.new_zeros(1, batch_size, self.hidden_size))
训练模型
以下是训练循环的关键步骤。注意,由于语言模型的序列连续性,我们需要在批次间传递隐藏状态,但为了避免计算图过长导致内存爆炸,我们使用.detach()来截断历史。
model = RNNModel(VOCAB_SIZE, EMBEDDING_SIZE, HIDDEN_SIZE).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
def repackage_hidden(h):
"""将隐藏状态从计算图中分离,以截断历史。"""
if isinstance(h, torch.Tensor):
return h.detach()
else:
return tuple(repackage_hidden(v) for v in h)
for epoch in range(NUM_EPOCHS):
model.train()
hidden = model.init_hidden(BATCH_SIZE)
for i, batch in enumerate(train_iter):
data, targets = batch.text, batch.target
# 分离隐藏状态,防止梯度爆炸
hidden = repackage_hidden(hidden)
optimizer.zero_grad()
# 前向传播
output, hidden = model(data, hidden)
# 计算损失
loss = criterion(output.view(-1, VOCAB_SIZE), targets.view(-1))
# 反向传播
loss.backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), GRADIENT_CLIP)
optimizer.step()
if i % 100 == 0:
print(f'Epoch: {epoch}, Iteration: {i}, Loss: {loss.item()}')
模型评估与保存
我们可以在验证集上评估模型,并保存效果最好的模型。
def evaluate(model, data_iter):
model.eval()
total_loss = 0
total_count = 0
hidden = model.init_hidden(BATCH_SIZE)
with torch.no_grad():
for batch in data_iter:
data, targets = batch.text, batch.target
output, hidden = model(data, hidden)
hidden = repackage_hidden(hidden)
loss = criterion(output.view(-1, VOCAB_SIZE), targets.view(-1))
total_loss += loss.item() * data.numel()
total_count += data.numel()
model.train()
return total_loss / total_count
best_val_loss = float('inf')
for epoch in range(NUM_EPOCHS):
# ... 训练代码 ...
val_loss = evaluate(model, val_iter)
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), 'best_language_model.pth')
print(f'Best model saved with validation loss: {val_loss}')
文本分类简介
上一节我们详细探讨了用于语言模型的RNN,本节中我们来看看RNN在另一个重要任务——文本分类中的应用。文本分类的目标是给一段文本分配一个预定义的类别标签,例如情感分析(正面/负面)、垃圾邮件识别等。
文本分类的基本思路
无论使用何种模型,文本分类的通用流程是:
- 将文本中的单词转换为词向量。
- 将这些词向量组合成一个能够代表整个句子的向量(句子表示)。
- 将这个句子表示输入到一个分类器(通常是线性层)中得到类别预测。
基于词向量平均的模型
一个简单而有效的基线模型是词向量平均模型。其步骤如下:
- 将句子中的每个单词通过查找表转换为词向量。
- 对所有词向量求平均,得到一个句子向量。
- 将该句子向量输入到一个前馈神经网络中进行分类。
虽然简单,但这个模型在很多任务上表现非常鲁棒。
基于RNN的文本分类模型
RNN天然适合处理序列数据,可以更好地捕捉单词间的顺序和上下文信息。常用的RNN文本分类架构包括:
- 使用最后隐藏状态:将RNN处理完整个句子后的最后一个隐藏状态作为句子表示。缺点是可能遗忘句子开头的长程信息。
- 使用所有隐藏状态的平均(平均池化):对RNN在所有时间步产生的隐藏状态求平均。
- 双向RNN(Bi-RNN):同时运行一个前向RNN和一个后向RNN,然后将两个方向对应时间步的隐藏状态拼接起来。这样每个单词的表示都包含了其左右上下文的信息,最后再通过池化(如平均)得到句子表示。
使用TorchText处理文本分类数据
以下是如何使用torchtext加载IMDB电影评论数据集(一个二分类情感分析数据集)的示例。
from torchtext import data, datasets
import spacy
# 使用spacy进行分词
spacy_en = spacy.load('en_core_web_sm')
def tokenizer(text):
return [tok.text for tok in spacy_en.tokenizer(text)]
# 定义字段
TEXT = data.Field(tokenize=tokenizer, lower=True)
LABEL = data.LabelField(dtype=torch.float)
# 加载IMDB数据集
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
# 将训练集进一步划分为训练集和验证集
train_data, val_data = train_data.split(random_state=random.seed(1234))
# 构建词表,并加载预训练的词向量(如GloVe)
TEXT.build_vocab(train_data, max_size=25000, vectors="glove.6B.100d")
LABEL.build_vocab(train_data)
# 创建迭代器,使用BucketIterator将长度相似的句子放在同一个批次以减少填充
train_iter, val_iter, test_iter = data.BucketIterator.splits(
(train_data, val_data, test_data),
batch_size=BATCH_SIZE,
device=device,
sort_within_batch=True,
sort_key=lambda x: len(x.text)
)
关于BucketIterator:它不会打乱一个句子内部的单词顺序,而是将训练集中所有句子按长度分组,使得同一个批次内的句子长度相近。这能有效减少填充符(<pad>)的数量,提升训练效率和模型效果。对于文本分类任务,不同数据样本之间的顺序无关紧要,因此这种分组是可行的。
总结
本节课我们一起学习了自然语言处理中的两个核心任务:语言模型和文本分类。
我们首先深入探讨了语言模型,理解了其如何计算句子概率以及用困惑度进行评价。然后,我们重点学习了循环神经网络(RNN)及其为解决长程依赖问题而衍生的变体——长短时记忆网络(LSTM)和门控循环单元(GRU)。通过PyTorch代码实践,我们掌握了构建、训练和评估一个RNN语言模型的完整流程,包括使用torchtext处理数据、梯度裁剪、隐藏状态传递与截断等关键技巧。
接着,我们将视角转向文本分类,介绍了其基本思路和常用模型,特别是如何利用RNN来获取更好的句子表示。我们看到了如何使用torchtext便捷地加载和处理像IMDB这样的标准分类数据集。


通过本课的学习,你应该对基于RNN的序列建模有了扎实的理解,并具备了使用PyTorch框架实现相关模型的实践能力。这些知识是构建更复杂NLP应用(如机器翻译、对话系统)的重要基石。



【七月在线】NLP高端就业训练营10期 - P3:3. 基于CNN的文本分类教程 📚
在本节课中,我们将学习如何使用卷积神经网络来处理自然语言处理任务,特别是文本分类。我们将从Transformer模型的补充知识开始,然后介绍如何阅读学术论文,最后深入探讨CNN的原理及其在文本处理中的应用。
第一部分:Transformer模型补充 🧠
上一节我们介绍了Transformer模型的核心组件Self-Attention。本节中,我们来看看Transformer编码器中的其他重要结构。
残差连接与层归一化
在Self-Attention层之后,Transformer编码器会进行残差连接和层归一化操作。
残差连接 的核心思想是让训练变得更加简单。它通过添加一条“捷径”,将层的输入直接加到输出上。这有助于缓解深层网络中的梯度消失问题。
公式表示:
输出 = 层输入 + 层输出
层归一化 的目的是将数据转化为均值为0、方差为1的分布,有助于稳定训练过程,防止梯度消失或爆炸。
公式表示:
LayerNorm(x) = α * ( (x - μ) / √(σ² + ε) ) + β
其中,μ是均值,σ²是方差,ε是一个极小值防止除零,α和β是可学习的参数。



位置编码


由于Transformer是并行处理序列的,它本身不具备序列的顺序信息。因此,需要引入位置编码来为每个词的位置提供信息。
在原始Transformer中,位置编码使用正弦和余弦函数计算:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中,pos是位置,i是维度索引。
在BERT等模型中,位置编码通常通过学习一个嵌入矩阵来获得。
解码器中的掩码机制
在解码器部分,为了防止模型在预测当前词时“看到”未来的词信息(即标签泄漏),引入了掩码机制。
具体做法是,在计算注意力权重矩阵后,将其与一个下三角矩阵(未来位置为0)相乘,从而将未来位置的权重置零。
第二部分:如何阅读学术论文 📄



在深入技术细节之前,掌握高效阅读论文的方法至关重要。以下是阅读NLP领域论文的一些建议。

论文查找与筛选

- 关注经典:初学者应优先阅读领域内的经典论文(如Transformer、TextCNN),打好基础。
- 利用资源:可以使用如 arXiv、Papers with Code、PaperWeekly 等网站或社区来发现和跟踪论文。
- 管理工具:推荐使用 ReadPaper 等在线工具来管理和阅读论文,它能高亮重要内容并提取图表。






阅读顺序与重点





阅读一篇论文时,建议遵循以下顺序:


- 标题与摘要:快速了解论文的核心贡献。
- 引言:理解研究背景和动机。
- 模型部分:这是论文的核心,需仔细理解其创新点与结构。
- 实验部分:关注实验设置、数据集和结果分析,特别是与基线模型的对比。
- 结论:总结工作并展望未来。




对于复现,首先查看作者是否开源了代码。复现时要注意论文描述与代码实现可能存在的差异。


第三部分:卷积神经网络基础 🖼️

上一节我们介绍了处理序列的RNN和Transformer。本节中,我们来看看如何利用CNN来捕捉文本中的局部特征(如短语)。
什么是卷积?
卷积是一种数学运算,在图像处理中常用来提取特征(如边缘)。在离散情况下,可以理解为对局部区域进行加权求和。
文本中的卷积:在文本上应用卷积,意味着使用一个滑动窗口(卷积核)在词向量序列上移动,每次计算窗口内几个连续词的组合特征。
CNN在文本上的应用
以下是CNN处理文本的关键步骤:
- 输入表示:句子被表示为词向量矩阵,形状为
[序列长度, 词向量维度]。 - 卷积操作:使用多个不同宽度的卷积核进行扫描。卷积核的宽度(如2, 3, 4)决定了每次考虑几个词,高度必须与词向量维度相同。
- 填充:为了保持卷积后序列长度不变,通常在序列首尾进行补零操作。
- 多通道:使用多个卷积核可以提取不同方面的特征。
- 池化:常用最大池化,从每个卷积核产生的特征图中提取最重要的值(如全局最大值)。
- 拼接与分类:将所有池化后的特征拼接成一个向量,输入全连接层进行分类。
维度变化公式(假设输入序列长度为 W,卷积核大小为 F,步长为 S,填充为 P):
输出长度 = (W + 2P - F) / S + 1
CNN处理文本的优缺点
- 优点:能有效提取N-gram(短语)特征;模型结构简单,参数较少,训练速度快。
- 缺点:感受野有限,难以捕获长距离依赖关系;本身不具备序列顺序信息,通常需要结合位置编码。
第四部分:CNN文本分类模型实战 ⚙️
理论之后,我们通过一个经典的TextCNN模型来实践文本分类。
TextCNN模型结构
TextCNN是用于文本分类的经典CNN模型,其结构非常简单:
- 词嵌入层将单词转换为向量。
- 使用多个不同尺寸(如2,3,4)的卷积核在嵌入矩阵上进行卷积,提取不同粒度的特征。
- 对每个卷积核的输出进行全局最大池化,得到一个标量。
- 将所有池化结果拼接成一个特征向量。
- 将特征向量输入全连接层,最后通过Softmax得到分类概率。
使用PyTorch实现TextCNN
以下是使用PyTorch框架构建TextCNN模型的核心代码:


import torch
import torch.nn as nn
import torch.nn.functional as F
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_size, num_classes=2):
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
# 定义三种不同宽度的卷积核,每种2个
self.conv1 = nn.Conv2d(1, 2, (2, embed_size)) # 窗口大小为2
self.conv2 = nn.Conv2d(1, 2, (3, embed_size)) # 窗口大小为3
self.conv3 = nn.Conv2d(1, 2, (4, embed_size)) # 窗口大小为4
self.dropout = nn.Dropout(0.5)
# 全连接层:输入特征数 = 卷积核种类数 * 每种核的数量
self.fc = nn.Linear(2*3, num_classes)
def forward(self, x):
# x: [batch_size, seq_len]
x = self.embedding(x) # [batch_size, seq_len, embed_size]
x = x.unsqueeze(1) # 增加通道维 [batch_size, 1, seq_len, embed_size]
# 卷积并激活
x1 = F.relu(self.conv1(x)).squeeze(3) # [batch_size, 2, seq_len-1]
x2 = F.relu(self.conv2(x)).squeeze(3) # [batch_size, 2, seq_len-2]
x3 = F.relu(self.conv3(x)).squeeze(3) # [batch_size, 2, seq_len-3]
# 池化
x1 = F.max_pool1d(x1, x1.size(2)).squeeze(2) # [batch_size, 2]
x2 = F.max_pool1d(x2, x2.size(2)).squeeze(2)
x3 = F.max_pool1d(x3, x3.size(2)).squeeze(2)
# 拼接
x = torch.cat((x1, x2, x3), dim=1) # [batch_size, 6]
x = self.dropout(x)
out = self.fc(x) # [batch_size, num_classes]
return out
代码说明:
nn.Embedding:将单词索引映射为稠密向量。nn.Conv2d:定义二维卷积层。在文本中,我们将其视为一维卷积,因此卷积核高度与词向量维度相同。F.max_pool1d:进行一维最大池化,提取每个特征图的最显著特征。torch.cat:将不同卷积核提取的特征拼接起来。nn.Linear:最后的全连接分类层。


在实际训练中,你需要准备数据集(如IMDB影评)、构建词汇表、设置数据加载器,并定义损失函数和优化器进行训练。




总结 🎯
本节课中我们一起学习了:
- Transformer的补充知识:包括残差连接、层归一化、位置编码以及解码器的掩码机制。
- 阅读学术论文的方法:从查找、筛选到重点阅读模型与实验部分。
- 卷积神经网络的基础:理解了卷积的概念及其在文本处理中的应用方式。
- TextCNN模型实战:掌握了使用PyTorch构建一个用于文本分类的简单CNN模型。



CNN为文本处理提供了一种捕捉局部特征的高效方式,特别适合短文本分类任务。理解其原理并能够动手实现,是构建更复杂NLP模型的重要基础。




【七月在线】NLP高端就业训练营10期 - P4:4.文本相似度计算与文本匹配模型 📚
在本节课中,我们将要学习文本相似度计算与文本匹配模型。我们将从文本相似度的基本概念和应用场景入手,然后深入探讨基于表示学习和基于匹配模型的两种主要计算方法,并重点讲解ESIM模型的原理与实现。
第一部分:什么是文本相似度? 🤔
上一节我们介绍了文本分类,本节中我们来看看文本相似度。文本相似度,顾名思义,就是计算两条文本之间的相似程度。它与文本分类最大的区别在于:文本分类针对单条文本,而文本相似度计算需要同时处理两条文本。


文本相似度在自然语言处理中有着广泛的应用场景。
以下是其主要应用场景:
- 信息检索:例如搜索引擎,用户输入一个查询(query),系统需要从海量文档(document)中找出最相关的结果并排序。文本相似度是排序的核心依据之一。
- 问答系统:系统有一个预先构建的问答对(QA)数据库。当用户提出一个新问题时,系统计算该问题与库中所有问题的相似度,将最相似问题对应的答案返回给用户。
- 文本聚类:将大量文本中语义相近的句子聚集在一起,通常也需要计算两两文本间的相似度。
在计算相似度时,我们需要一个度量标准。对于文本向量,最常用的是余弦相似度。
公式:sim(A, B) = (A · B) / (||A|| * ||B||)
选择余弦相似度的主要原因是其值域在[-1, 1]之间,对于文本向量,其值通常落在[0, 1]区间,便于理解和比较。
第二部分:基于表示学习的文本相似度计算 🧠
上一节我们了解了文本相似度的概念,本节中我们来看看第一种计算方法——基于表示学习。这种方法的核心思想是:通过一个模型将句子转换为一个固定维度的向量(即句子的embedding),然后计算这两个向量之间的余弦相似度。
整个流程可以概括为:
- 输入句子。
- 将句子分词,得到词语序列。
- 将每个词语转换为词向量(word embedding)。
- 将所有词向量聚合(如加权平均),生成句向量(sentence embedding)。
- 计算两个句向量之间的余弦相似度。
其中,第3步使用的模型是关键。我们可以使用简单的Word2Vec,也可以使用更强大的预训练语言模型,如ELMo。
ELMo模型介绍
ELMo(Embeddings from Language Models)是一个基于双层双向LSTM的预训练语言模型。

模型结构:
- 输入是词的embedding。
- 经过一个前向的LSTM层和一个后向的LSTM层(均为两层)。
- 将每个位置上前向和后向LSTM的输出拼接起来,作为该词的上下文相关表示。




预训练任务:ELMo在海量文本上进行无监督训练,任务是预测句子的下一个词。训练完成后,模型权重已经包含了丰富的语言知识。





如何使用ELMo生成句向量:
- 加载预训练的ELMo权重。
- 输入句子,模型输出每个词的上下文相关向量。
- 对这些词向量进行平均(或加权平均)操作,得到句子的向量表示。





以下是使用ELMo生成句子向量的代码示例:
import torch
import jieba
from elmoformanylangs import Embedder
# 初始化模型(需提前下载权重文件)
e = Embedder(‘path/to/your/elmo/model‘)
# 句子分词
sentence = “这是一个例句”
seg_list = jieba.lcut(sentence)
# 生成ELMo向量
# 输出是每个词的向量,形状为 (词数, 向量维度)
vectors = e.sents2elmo([seg_list])
# 对词向量求平均,得到句向量
sentence_embedding = torch.mean(torch.tensor(vectors[0]), dim=0)
通过这种方式,我们就能得到高质量的句子表示,进而计算相似度。
第三部分:基于匹配模型的文本相似度计算 ⚔️
上一节我们介绍了基于表示学习的方法,本节中我们来看看第二种主流方法——基于匹配模型。这种方法不单独生成句向量,而是构建一个模型,直接接收两条文本作为输入,并输出一个相似度分数或分类结果(如是否相似)。
这类模型通常采用“孪生网络”或“双塔结构”,即两个结构相同(通常共享权重)的子网络分别处理两条文本,然后在高层进行交互和判断。
根据交互发生的位置,匹配模型可以分为:
- 外交互:两个子网络独立处理文本,生成各自的向量表示后,再计算相似度或进行分类。
- 内交互:在模型处理过程中,两条文本的表示就进行交互(如通过注意力机制),最后再汇总输出。
经典模型:DSSM 与 ESIM
DSSM(Deep Structured Semantic Model) 是双塔结构的经典代表。每个“塔”由全连接层构成,分别将查询(Query)和文档(Document)映射到同一个语义空间,然后计算其余弦相似度。
ESIM(Enhanced Sequential Inference Model) 是一个效果和效率都不错的模型,它基于LSTM并引入了注意力机制进行内交互。接下来我们将重点解析ESIM模型的结构与实现。
ESIM模型主要包含四层:
- Input Encoding:使用双向LSTM对输入的两条文本分别进行编码。
- Local Inference Modeling:使用注意力机制(Attention)让两条文本的表示进行交互,并生成增强的局部推理信息。
- Inference Composition:再次使用双向LSTM对增强后的信息进行组合编码。
- Output:使用池化(最大池化与平均池化)将变长序列转换为定长向量,然后通过全连接层进行分类。
注意力机制(Attention)核心:
这是ESIM模型的关键。它计算文本A中每个词与文本B中所有词的相似度(通过点积得到权重矩阵),然后对文本B的词向量进行加权求和,得到一个能代表文本A中某个词的“上下文向量”。这个过程让模型能够关注两条文本间相关的部分。

公式(简化):权重矩阵 E = A · B^T, 对E按行做softmax得到归一化权重,然后用该权重对B加权得到A的注意力表示 A_hat = softmax(E) · B。对B也进行类似操作。


ESIM模型代码实现要点


以下是使用PyTorch构建ESIM模型的核心结构代码:


import torch
import torch.nn as nn


class ESIM(nn.Module):
def __init__(self, vocab_size, embedding_size, hidden_size):
super(ESIM, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_size)
# Input Encoding 层 (双向LSTM)
self.lstm1 = nn.LSTM(embedding_size, hidden_size, batch_first=True, bidirectional=True)
# Inference Composition 层 (双向LSTM)
# 注意输入维度:经过Local Inference后,向量维度变为 hidden_size*2 * 4
self.lstm2 = nn.LSTM(hidden_size * 8, hidden_size, batch_first=True, bidirectional=True)
# 分类层
self.fc = nn.Sequential(
nn.Linear(hidden_size * 8, hidden_size),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(hidden_size, 2) # 二分类输出
)
self.dropout = nn.Dropout(0.2)
def forward(self, p, q):
# 1. Embedding
p_emb = self.embedding(p)
q_emb = self.embedding(q)
# 2. Input Encoding
p_enc, _ = self.lstm1(p_emb)
q_enc, _ = self.lstm1(q_emb)
p_enc = self.dropout(p_enc)
q_enc = self.dropout(q_enc)
# 3. Local Inference Modeling (Attention)
e = torch.matmul(p_enc, q_enc.transpose(1, 2)) # 相似度矩阵
p_att = torch.matmul(torch.softmax(e, dim=-1), q_enc) # p基于q的注意力表示
q_att = torch.matmul(torch.softmax(e.transpose(1, 2), dim=-1), p_enc) # q基于p的注意力表示
# 组合局部推理信息
m_p = torch.cat([p_enc, p_att, p_enc - p_att, p_enc * p_att], dim=-1)
m_q = torch.cat([q_enc, q_att, q_enc - q_att, q_enc * q_att], dim=-1)
# 4. Inference Composition
p_compose, _ = self.lstm2(m_p)
q_compose, _ = self.lstm2(m_q)
# 5. Output (Pooling + Classification)
p_avg = torch.mean(p_compose, dim=1)
q_avg = torch.mean(q_compose, dim=1)
p_max, _ = torch.max(p_compose, dim=1)
q_max, _ = torch.max(q_compose, dim=1)
v = torch.cat([p_avg, q_avg, p_max, q_max], dim=-1)
v = self.dropout(v)
out = self.fc(v)
return out




模型训练:训练流程与常规文本分类模型一致,包括数据加载、模型初始化、定义损失函数(如交叉熵损失)和优化器、进行前向/反向传播等步骤。


两种方法的对比与应用






在实际系统中,表示学习方法和匹配模型方法常常结合使用,形成“召回-排序”流水线:
- 召回阶段:使用表示学习方法(如ELMo句向量+余弦相似度)。这种方法速度快,可以快速从海量候选文本中筛选出Top-K个最相关的候选项。
- 排序阶段:使用复杂的匹配模型(如ESIM)对召回阶段得到的少量候选项进行精细排序,得到最终结果。这种方法精度高,但计算量较大。


这种架构兼顾了效率与效果。



总结 🎯
本节课中我们一起学习了文本相似度计算与文本匹配模型。


我们首先明确了文本相似度的定义及其在检索、问答、聚类等场景下的重要作用。接着,我们深入探讨了两种主流的计算方法:基于表示学习的方法(如使用ELMo生成句向量)和基于匹配模型的方法(如ESIM模型)。我们重点剖析了ESIM模型的结构,特别是其核心的注意力机制,并给出了模型实现的关键代码。






理解文本相似度计算是构建许多高级NLP应用(如搜索引擎、智能客服)的基础。希望大家通过本节课的学习,能够掌握文本相似度的核心思想与主流方法,并能够动手实现一个基本的匹配模型。
【七月在线】NLP高端就业训练营10期 - P5:1. 基于Attention机制的Seq2Seq任务 🧠
在本节课中,我们将要学习序列到序列(Seq2Seq)任务中的注意力机制。我们将从经典的编码器-解码器架构入手,分析其局限性,并自然地引出注意力机制的概念。最后,我们会将注意力机制抽象为一个通用的特征提取框架,并了解其核心变体。
1. 序列到序列任务中的编码器-解码器架构 🔄
上一节我们介绍了课程的整体脉络,本节中我们来看看序列到序列任务的基础模型——编码器-解码器架构。
编码器-解码器架构是解决序列到序列问题的经典框架,例如机器翻译。其核心思想是将一个变长的输入序列编码成一个固定长度的向量,再将该向量解码成另一个变长的输出序列。
1.1 网络架构图示
下图展示了标准的编码器-解码器架构:
输入序列 (X1, X2, ..., XT) -> [编码器 RNN] -> 上下文向量 C -> [解码器 RNN] -> 输出序列 (Y1, Y2, ..., YT')
在编码器阶段,一个循环神经网络(如RNN、LSTM或GRU)逐步读取输入序列。最后一个时间步的隐藏状态 h_T 被用作整个输入序列的“总结”,即上下文向量 C。
在解码器阶段,另一个循环神经网络以 C 作为其初始隐藏状态,并逐步生成输出序列。在生成每一个输出 Y_t 时,解码器都会参考上一个时刻的隐藏状态、上一个时刻的输出 Y_{t-1} 以及上下文向量 C。
1.2 数学公式描述
以下是该架构核心部分的数学描述:
编码器(以GRU为例):
对于每个时间步 t(从1到T):
z_t = σ(W_z · [h_{t-1}, x_t])
r_t = σ(W_r · [h_{t-1}, x_t])
h̃_t = tanh(W · [r_t * h_{t-1}, x_t])
h_t = (1 - z_t) * h_{t-1} + z_t * h̃_t
最终,上下文向量 C = h_T。
解码器:
解码器的初始隐藏状态 s_0 = C。
对于每个时间步 t(从1到T‘):
s_t = f(s_{t-1}, y_{t-1}, C) // f是解码器RNN单元(如GRU)的计算过程
P(y_t | y_{<t}, C) = g(s_t, y_{t-1}, C) // g是输出层(如softmax)
其中,f 和 g 都是可学习的函数。
1.3 代码示意
以下是该架构在代码中的核心逻辑示意(使用PyTorch风格):
class Encoder(nn.Module):
def forward(self, x):
# x: [seq_len, batch_size, embed_dim]
outputs, hidden = self.rnn(x) # outputs保存所有时间步的隐藏状态
# 取最后一个时间步的隐藏状态作为上下文向量
context = hidden[-1] # 假设是单层RNN
return context
class Decoder(nn.Module):
def __init__(self, ...):
self.rnn = nn.GRU(...)
self.fc_out = nn.Linear(...)
def forward(self, decoder_input, hidden, context):
# decoder_input: 上一个时间步的输出(或起始符)
# hidden: 上一个时间步的隐藏状态,初始为context
# context: 编码器输出的上下文向量
output, hidden = self.rnn(decoder_input, hidden)
# 在计算输出时,会结合context信息
output = self.fc_out(torch.cat([output, context.unsqueeze(0)], dim=-1))
return output, hidden
1.4 架构的局限性
通过以上分析,我们可以清晰地看到该架构的核心操作:将变长的输入序列压缩成一个定长的向量 C。
这带来了一个根本性问题:C 的容量有限,难以完整存储长序列的所有信息,尤其是序列开头的细节信息在传递到末尾时可能已经丢失或稀释。这被称为“信息瓶颈”问题。
2. 序列到序列任务中的注意力机制 🎯
上一节我们介绍了编码器-解码器架构及其信息瓶颈问题,本节中我们来看看如何通过注意力机制来解决这个问题。
注意力机制的朴素想法是:在解码器生成每一个词时,不应该均等地看待编码器所有时间步的信息,而应该“有侧重地”去查看输入序列的不同部分。解码器在每一步都可以“软搜索”一组与当前生成词最相关的输入位置,并基于这些位置的编码信息来生成当前词。
2.1 带有注意力机制的Seq2Seq架构
下图展示了引入注意力机制后的Seq2Seq模型:
编码器隐藏状态: (h1, h2, ..., hT)
↓ (为解码器每一步计算注意力权重)
解码器步骤t: -- 注意力权重 (α_t1, α_t2, ..., α_tT) --
↓ (加权求和)
上下文向量 C_t = Σ(α_ti * h_i)
↓ (与解码器状态结合)
输出 Y_t
关键改进在于:上下文向量 C 不再是固定的,而是为解码器的每一个时间步 t 动态计算一个专属的 C_t。
2.2 注意力机制的计算步骤
以下是计算动态上下文向量 C_t 的步骤:
第一步:计算注意力得分(Alignment Scores)
对于解码器当前时刻 t 的隐藏状态 s_{t-1} 和编码器所有时刻的隐藏状态 h_i,计算一个得分 e_{ti},表示 h_i 对生成当前词的重要程度。
一种常见的计算方式是:
e_{ti} = v_a^T · tanh(W_a · [s_{t-1}; h_i])
其中 W_a 和 v_a 是可学习的参数,[;] 表示向量拼接。
第二步:计算注意力权重(Attention Weights)
使用softmax函数将所有得分 e_{ti} 归一化为概率分布,即注意力权重 α_{ti}:
α_{ti} = exp(e_{ti}) / Σ_{j=1}^{T} exp(e_{tj})
α_{ti} 表示在生成第 t 个输出词时,编码器第 i 个输入词的关注程度。
第三步:计算上下文向量(Context Vector)
将编码器的所有隐藏状态 h_i 按其对应的注意力权重 α_{ti} 进行加权求和,得到当前时刻的上下文向量 C_t:
C_t = Σ_{i=1}^{T} α_{ti} · h_i
第四步:更新解码器输出
将动态上下文向量 C_t 与解码器当前隐藏状态 s_{t-1} 拼接,一起送入输出层预测当前词 y_t:
s_t = f(s_{t-1}, y_{t-1}, C_t)
P(y_t | ...) = g(s_t, y_{t-1}, C_t)
2.3 代码示意
以下是注意力机制核心计算的代码示意:
class Attention(nn.Module):
def __init__(self, enc_hid_dim, dec_hid_dim):
super().__init__()
self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim) # 对应 W_a
self.v = nn.Linear(dec_hid_dim, 1, bias=False) # 对应 v_a^T
def forward(self, decoder_hidden, encoder_outputs):
# decoder_hidden: [batch_size, dec_hid_dim]
# encoder_outputs: [src_len, batch_size, enc_hid_dim * 2] (双向)
src_len = encoder_outputs.shape[0]
# 重复解码器隐藏状态以匹配编码器输出序列长度
repeated_decoder_hidden = decoder_hidden.unsqueeze(1).repeat(1, src_len, 1)
# 计算能量值 e_{ti}
energy = torch.tanh(self.attn(torch.cat((repeated_decoder_hidden, encoder_outputs.permute(1,0,2)), dim=2)))
attention = self.v(energy).squeeze(2) # [batch_size, src_len]
# 计算注意力权重 α_{ti}
return F.softmax(attention, dim=1)
在解码器中,每一步都需要调用此注意力模块:
# 在解码器每一步中
attention_weights = self.attention(decoder_hidden, encoder_outputs) # [batch_size, src_len]
# encoder_outputs: [src_len, batch_size, hid_dim]
context_vector = torch.bmm(attention_weights.unsqueeze(1), encoder_outputs.permute(1,0,2))
# context_vector: [batch_size, 1, hid_dim]
3. 注意力机制的抽象:通用框架 🧩
上一节我们看到了注意力机制在Seq2Seq中的具体应用,本节中我们将其抽象为一个通用的特征提取框架。
注意力机制的本质可以概括为:根据某些“查询”(Query),在一组“键-值”(Key-Value)对中,有选择地(通过权重)聚合“值”(Value)信息。
3.1 软性注意力(Soft Attention)的一般形式
一个通用的软性注意力机制包含两步:
1. 注意力分布计算
给定查询向量 q 和包含 N 个元素的输入信息 X = {x_1, ..., x_N},计算注意力分布 α_i,表示 x_i 受关注的程度。
α_i = softmax(s(x_i, q))
其中 s(x_i, q) 是打分函数,用于计算 x_i 与 q 的相关性。常见的打分函数有:
- 加性模型:
s(x_i, q) = v^T tanh(W x_i + U q) - 点积模型:
s(x_i, q) = x_i^T q - 缩放点积模型:
s(x_i, q) = (x_i^T q) / √d_k(d_k是x_i和q的维度,用于稳定梯度)
2. 信息聚合(加权平均)
根据注意力分布,对输入信息进行加权求和,得到输出向量。
output = Σ_{i=1}^{N} α_i · x_i
在抽象框架中,x_i 同时充当了计算注意力的“键”和被聚合的“值”。
3.2 键值对注意力(Key-Value Attention)
在更一般的设定中,用于计算注意力的“键”(Key)和用于聚合的“值”(Value)是分离的。输入是一组键值对 (K, V) = {(k_1, v_1), ..., (k_N, v_N)}。
α_i = softmax(s(k_i, q))
output = Σ_{i=1}^{N} α_i · v_i
这种分离使得模型可以学习更灵活的表征,例如,k_i 可以编码“哪些信息是可供检索的”,而 v_i 编码“这些信息的具体内容是什么”。
3.3 自注意力(Self-Attention)与缩放点积注意力
自注意力是一种特殊的键值对注意力,其查询 Q、键 K、值 V 都来自同一个输入序列 X 的线性变换:
Q = X W^Q, K = X W^K, V = X W^V
其中 W^Q, W^K, W^V 是可学习的权重矩阵。
缩放点积注意力(Scaled Dot-Product Attention) 是Transformer模型的核心,其计算公式如下:
Attention(Q, K, V) = softmax( (Q K^T) / √d_k ) V
这里使用了点积作为打分函数,并除以 √d_k 进行缩放,以防止点积结果过大导致softmax梯度消失。
3.4 多头注意力(Multi-Head Attention)
为了增强模型的容量,允许模型在不同的表示子空间里学习相关信息,可以将自注意力机制并行执行多次,即“多头”。
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) W^O
其中 head_i = Attention(Q W_i^Q, K W_i^K, V W_i^V)
每个头都有自己独立的线性变换权重 W_i^Q, W_i^K, W_i^V,最后将所有头的输出拼接起来再做一次线性变换 W^O。
总结 📚
本节课中我们一起学习了注意力机制从具体应用到抽象框架的完整脉络。
- 起点:我们从经典的Seq2Seq编码器-解码器架构出发,分析了其将变长序列压缩为定长向量所导致的“信息瓶颈”问题。
- 引入:为了解决这个问题,我们引入了注意力机制。在Seq2Seq中,它为解码器的每一步动态计算一个上下文向量,该向量是编码器所有隐藏状态的加权和,权重由解码器当前状态与编码器各状态的匹配度决定。
- 抽象:我们将注意力机制抽象为一个通用的特征提取框架。其核心是 “根据查询(Query),有选择地聚合键值对(Key-Value)信息” 。这包含两个步骤:计算注意力分布(通常用softmax归一化打分函数结果)和加权求和。
- 演进:我们进一步了解了其重要变体:
- 键值对注意力:将用于计算注意力的“键”和用于聚合的“值”分离,提供更大灵活性。
- 自注意力:查询、键、值均来自同一输入序列的变换,使序列内部元素能够直接相互关注,并行计算效率高。
- 缩放点积注意力与多头注意力:这是现代Transformer模型的基础。缩放点积注意力是高效的计算方式,多头注意力则允许模型在不同子空间共同关注信息。


注意力机制的本质可以理解为在全连接网络的特征提取基础上,增加了一个动态的、内容相关的权重调制。它不依赖于序列的顺序计算,具有强大的并行能力,并且能够建立序列中任意距离元素之间的直接依赖关系,因此已成为深度学习,尤其是自然语言处理领域最重要的基础模块之一。



【七月在线】NLP高端就业训练营10期 - P6:2.Seq2Seq任务—机器翻译与文本摘要 📚
在本节课中,我们将学习序列到序列(Seq2Seq)任务的两个核心应用:机器翻译和文本摘要。我们将从任务的重要性、评估方法、经典模型架构到最新的研究进展,系统地介绍如何构建和理解这些生成式模型。


概述 🎯
机器翻译和文本摘要是自然语言处理中典型的序列生成任务。它们都遵循编码器-解码器(Encoder-Decoder)的基本范式:模型接收一个输入序列(如源语言句子或长文档),通过编码器将其压缩为上下文表示,再由解码器基于该表示生成目标序列(如翻译后的句子或摘要)。本节课将深入探讨这一范式的演变、核心挑战以及评估方法。
机器翻译的重要性与数据驱动 📈


机器翻译在实际生活中应用广泛,其错误也侧面印证了它的普及程度。如今,绝大多数机器翻译模型都是数据驱动的,这与整个AI领域向机器学习靠拢的趋势一致。
早期的机器翻译系统曾分为两派:一派基于人工定义的语法规则和词典;另一派则主张从数据中学习。在拥有海量数据和强大算力的今天,基于数据的方法已被证明更为有效。
训练数据主要来源于双语对照的语料,例如:
- 新闻网站的多语言版本
- 公司网页的多语言内容
- 法律专利文件
- 电影电视字幕
- 联合国文件
大部分数据是文档级别的对应,需要通过无监督或统计学方法(如句子对齐算法)来提取句子级别的配对。目前,拥有大量专有翻译数据是各大公司的核心竞争力。
机器翻译的评估:BLEU分数 📊


人工评估翻译质量虽然准确,但费时费力。因此,我们需要自动化的评估指标。BLEU(Bilingual Evaluation Understudy)是当前最常用的机器翻译自动评估方法,其核心思想是比较机器翻译输出(candidate)与一个或多个参考翻译(reference)之间的相似度。
BLEU主要基于n-gram精度(precision)。它不仅考虑单个单词(unigram)的匹配,还考虑更长的词组(bigram, trigram等)的匹配,以评估流畅度。通常使用的是BLEU-4,即计算1到4-gram精度的几何平均。


公式示例:
对于一个候选翻译,其n-gram精度计算为:
P_n = (候选翻译中与参考翻译匹配的n-gram数量) / (候选翻译中n-gram的总数量)


为了惩罚过度重复(如“the the the...”),BLEU引入了修正机制(Modified Precision)。最终,BLEU分数是多种n-gram精度的加权几何平均,并乘以简短惩罚因子(Brevity Penalty, BP)。






尽管看似简单,但BLEU分数与人工评估的相关性较高,在实践中被广泛认可。







统计机器翻译与编码器-解码器框架 🔄
机器翻译可以被视为一个“噪声信道”解码问题:假设目标语言句子Y经过一个噪声信道后变成了源语言句子X(即我们观测到的“乱码”),翻译的目标就是找到最有可能产生X的Y。
根据贝叶斯公式,我们寻找:
argmax_Y P(Y|X) = argmax_Y P(X|Y) * P(Y)
其中,P(X|Y)是翻译模型,确保翻译忠实于原文;P(Y)是语言模型,确保翻译结果流畅自然。


现代神经机器翻译模型主要基于编码器-解码器框架。其基本思路是:
- 编码器(如RNN/LSTM/Transformer)将源语言句子
X编码为一个固定维度的上下文向量C(或一组隐藏状态),旨在捕获整个句子的信息。 - 解码器(另一个RNN/LSTM/Transformer)以上下文向量
C为条件,自回归地(即根据已生成的部分)逐个生成目标语言单词Y。


训练时使用交叉熵损失函数,通过梯度下降优化模型参数。




注意力机制的引入 🧠
早期的编码器-解码器模型有一个瓶颈:要求单个上下文向量C编码整个源句的所有信息,这对于长句子来说非常困难。


注意力机制的提出解决了这个问题。它允许解码器在生成每一个目标单词时,动态地“关注”源句子中不同部分对应的编码器隐藏状态,而不是仅仅依赖一个固定的C。
核心计算步骤:
- 计算当前解码器状态与所有编码器隐藏状态的相关性分数(score)。
- 将分数通过softmax函数归一化为权重(attention distribution)。
- 将编码器隐藏状态按权重加权求和,得到当前步的上下文向量
C_t。 - 解码器结合
C_t和自身状态来预测下一个单词。
注意力机制显著提升了长序列翻译的质量,并成为Seq2Seq模型的标配。其计算方式多样,如点积(dot product)、加性(additive)或使用小型神经网络。
从RNN到Transformer的演进 🚀
在Transformer出现之前,基于RNN/LSTM并加入注意力机制的模型是主流。例如,Google的神经机器翻译系统使用了深层的双向LSTM编码器和单向LSTM解码器,并引入了残差连接以稳定训练。
Transformer模型的提出是革命性的。它完全摒弃了循环结构,仅依赖自注意力(Self-Attention) 和前馈神经网络来构建编码器和解码器。
Transformer的优势:
- 并行计算:摆脱了RNN的序列依赖性,训练速度极大提升。
- 长程依赖:自注意力机制能直接建模序列中任意两个位置的关系,缓解了长程依赖问题。
- 强大性能:在机器翻译等任务上取得了当时最好的效果。
如今,Transformer及其变体(如BERT、GPT)已成为NLP的基石模型。
解码策略:束搜索(Beam Search)🔍
在模型训练时,解码器在预测下一个单词时已知真实的之前单词(Teacher Forcing)。但在模型推理(实际翻译)时,模型只能基于自己之前预测的结果来生成下一个词,这可能导致错误累积。
贪婪解码(Greedy Decoding,每步只选概率最高的词)容易陷入局部最优。束搜索是一种常用的折中方案:
算法步骤:
- 设定一个束宽(beam size)
k。 - 在解码的第一步,保留概率最高的
k个候选词。 - 在后续每一步,对于上一步保留的
k个候选序列,每个序列都继续扩展V(词表大小)种可能,得到k * V个新序列。 - 从这
k * V个新序列中,仅保留总体概率最高的k个。 - 重复步骤3-4,直到所有候选序列都生成结束符或达到最大长度。
- 从最终的
k个序列中选出总体概率最高的作为输出。
束搜索比贪婪搜索更可能找到全局更优解,但增大k值并不总是带来更好的结果,且会显著增加计算开销。如何更好地进行解码仍然是一个开放的研究问题。
文本摘要任务概述 📝


文本摘要任务旨在将长文档压缩为保留核心信息的短文本。主要分为两类:
- 抽取式摘要:从原文中直接提取出重要的句子或片段组合成摘要。这通常被建模为句子级别的二分类或序列标注问题。
- 生成式摘要:像翻译一样,根据原文内容重新组织语言生成全新的摘要。这属于典型的Seq2Seq生成任务。

文本摘要的评估:ROUGE分数 📏


与BLEU类似,ROUGE(Recall-Oriented Understudy for Gisting Evaluation)是评估摘要质量的常用自动指标。它主要基于n-gram召回率(recall),关注参考摘要中的信息有多少被系统摘要覆盖。
常用的ROUGE指标包括:
- ROUGE-N:计算系统摘要和参考摘要之间n-gram的召回率。
- ROUGE-L:基于最长公共子序列(LCS)计算,考虑句子结构的相似性。
公式示例(ROUGE-N):
R_n = (系统摘要中与参考摘要匹配的n-gram数量) / (参考摘要中n-gram的总数量)
ROUGE分数越高,通常意味着摘要质量越好。
文本摘要模型实践 🛠️
对于抽取式摘要,可以将其视为句子分类任务。一个有效的方法是使用预训练模型(如BERT)作为文档编码器。通过在每句前添加[CLS]标记,并用这些[CLS]标记的表示来进行句子重要性分类。
对于生成式摘要,可以直接采用类似机器翻译的Seq2Seq框架(如Transformer)。一个重要的改进是引入指针生成器网络。
指针生成器网络的核心思想是解决OOV(未登录词)问题和决定何时“生成”新词、何时从原文“复制”词。
- 生成模式:从固定的词表中生成新词。
- 复制模式:直接从输入原文中复制一个词过来。
模型通过一个“生成概率”p_gen来软切换这两种模式,p_gen由当前解码器状态、注意力分布和输入决定。这大大提高了摘要的准确性和处理罕见词的能力。
此外,覆盖机制被用来防止摘要重复关注原文的同一部分,它通过追踪历史注意力分布的累加和,并在计算当前注意力时将其作为负反馈,从而鼓励模型关注未覆盖的内容。
实用工具与开源项目 🔧
学习和研究相关任务时,可以利用以下优秀开源项目:
- Fairseq:Facebook AI Research开源的序列建模工具包,支持多种Seq2Seq模型。
- Tensor2Tensor / Trax:Google开源的深度学习模型库,包含了Transformer的官方实现及其后续发展。
通过研究和使用这些框架,可以快速复现前沿模型,并将其应用到自己的定制化任务中。
总结 🎓


本节课我们一起深入学习了Seq2Seq任务的两大核心应用。
- 机器翻译:我们从评估方法(BLEU)、经典思想(噪声信道模型)出发,详细讲解了编码器-解码器框架、注意力机制的革新性作用,以及Transformer如何成为当前的主流架构。我们还探讨了实际推理中的解码策略(束搜索)。
- 文本摘要:我们区分了抽取式和生成式摘要,介绍了对应的评估指标(ROUGE)。并重点讲解了如何利用预训练模型(如BERT)进行抽取式摘要,以及生成式摘要中关键的指针生成器网络和覆盖机制。


掌握这些模型的思想、演变和细节,是深入理解并应用现代NLP生成技术的基础。建议结合开源项目和实际数据动手实践,以加深理解。

【七月在线】NLP高端就业训练营10期 - P7:3.基于Luong Attention的机器翻译教程 🧠➡️🗣️
在本节课中,我们将学习如何使用Sequence-to-Sequence模型进行机器翻译,并训练一个简单的聊天机器人。我们将从基础的Encoder-Decoder模型开始,逐步引入Luong Attention机制,以提升翻译效果。课程内容将涵盖模型原理、代码实现以及实际应用。
模型回顾 📚

上一节我们介绍了语言模型的基本概念。本节中,我们来看看用于机器翻译的Sequence-to-Sequence模型。





基础Encoder-Decoder模型
2014年,Cho等人发表了论文《Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation》。该模型的基本思路非常简单。
- 将待翻译的中文句子用 X 表示,目标英文句子用 Y 表示。
- 将中文句子进行词嵌入(Embedding),然后输入一个循环神经网络(RNN,文中使用了GRU)。
- 编码器(Encoder)处理完整个输入序列后,会得到一个向量 c,它包含了整个句子的信息。
- 将这个向量 c 作为初始状态,一步步输入到另一个循环神经网络——解码器(Decoder)中,解码生成英文句子。
模型的训练采用交叉熵损失(Cross-Entropy Loss),与语言模型非常类似,可以看作是一个以向量 c 为条件的语言模型。
引入Attention机制
随后,Bahdanau等人在此基础上加入了Attention机制,显著提升了翻译效果。
- 编码器使用双向循环神经网络,获取每个输入单词更丰富的上下文表示。
- 不再只使用最后一个隐藏状态作为句子表示,而是认为每个隐藏状态都是句子表示的一部分。
- 在解码时,对于当前要生成的单词,计算一个加权和(Weighted Sum)作为当前的上下文向量(Context Vector),并将其与解码器的输入结合。


这个加权和的权重(即Attention分数)通过以下方式计算:
- 计算解码器上一个时刻的隐藏状态 st-1 与编码器所有隐藏状态 hj 的相似度得分 etj。
- 对这些得分进行Softmax归一化,得到权重 αtj。
- 用权重对编码器隐藏状态进行加权求和,得到当前时刻的上下文向量 ct。
公式:
αtj = softmax(etj)
ct = Σj αtj hj


Luong Attention
与Bahdanau的Attention(在计算得分时使用解码器上一时刻的隐藏状态)略有不同,Luong等人提出了另一种非常类似但更高效的Attention机制。
- 得分函数(Scoring Function):Luong Attention介绍了几种计算得分 etj 的方法,例如点积(Dot)、双线性(Bilinear,或称General)和拼接后加线性变换(Concat)。
- 隐藏状态计算:在获得上下文向量 ct 后,Luong Attention将其与解码器当前时刻的隐藏状态 st 拼接,再经过一个线性变换和Tanh激活函数,得到用于预测最终输出的向量。
公式(以双线性评分函数为例):
etj = stT Wa hj
ãt = tanh(Wc [ct; st])
本节我们将使用Luong Attention的双线性(General) 方法来构建我们的机器翻译模型。


代码实现 💻


理解了模型原理后,本节我们来看看如何用PyTorch实现一个基于Luong Attention的机器翻译模型。由于数据集较小,训练出的模型效果可能有限,但足以帮助我们理解整个过程。


环境与数据准备
以下是导入必要库和进行数据预处理的步骤。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn.utils import clip_grad_norm_
import numpy as np
import nltk
nltk.download('punkt') # 用于英文分词
我们使用的数据是中文-英文平行语料,格式为“英文句子[TAB]中文句子”。以下是数据读取和预处理函数:
def read_data(file_path):
data = []
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
parts = line.strip().split('\t')
if len(parts) == 2:
en_sent = ['<bos>'] + nltk.word_tokenize(parts[0].lower()) + ['<eos>']
# 中文按字切分,也可使用jieba分词
cn_sent = ['<bos>'] + [char for char in parts[1]] + ['<eos>']
data.append((en_sent, cn_sent))
return data
train_data = read_data('train.txt')
dev_data = read_data('dev.txt')


构建词表
我们需要为源语言(英文)和目标语言(中文)分别构建词表,将单词映射为索引。
def build_vocab(sentences, max_size=50000):
word_count = {}
for sent in sentences:
for word in sent:
word_count[word] = word_count.get(word, 0) + 1
# 保留最高频的词汇,并加入特殊符号
vocab = ['<pad>', '<unk>', '<bos>', '<eos>']
vocab += [word for word, _ in sorted(word_count.items(), key=lambda x: -x[1])[:max_size-len(vocab)]]
word2idx = {word: idx for idx, word in enumerate(vocab)}
return vocab, word2idx
# 构建词表
en_vocab, en_word2idx = build_vocab([sent for en, cn in train_data for sent in [en]])
cn_vocab, cn_word2idx = build_vocab([sent for en, cn in train_data for sent in [cn]])
数据批处理(Batching)
为了高效训练,我们需要将数据组织成批次(Batch)。一个批次内的句子长度最好相近,因此需要按长度排序。
def get_mini_batch(data, batch_size, shuffle=True):
# 按句子长度排序的索引
sorted_indices = np.argsort([len(sent) for sent, _ in data])
if shuffle:
np.random.shuffle(sorted_indices)
mini_batches = []
for start_idx in range(0, len(data), batch_size):
batch_indices = sorted_indices[start_idx: start_idx + batch_size]
mini_batches.append([data[i] for i in batch_indices])
return mini_batches
定义模型
接下来是模型的核心部分。我们将分别定义编码器、注意力机制和解码器。
1. 编码器(Encoder)
编码器使用双向GRU来获取输入序列的隐藏状态。
class Encoder(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, dropout=0.2):
super(Encoder, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.gru = nn.GRU(embed_size, hidden_size, bidirectional=True, batch_first=True)
self.fc = nn.Linear(hidden_size * 2, hidden_size) # 将双向输出映射到解码器隐藏层大小
self.dropout = nn.Dropout(dropout)
def forward(self, x, lengths):
# x: [batch_size, seq_len]
embedded = self.dropout(self.embedding(x)) # [batch_size, seq_len, embed_size]
packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, lengths, batch_first=True, enforce_sorted=False)
packed_outputs, hidden = self.gru(packed_embedded)
outputs, _ = nn.utils.rnn.pad_packed_sequence(packed_outputs, batch_first=True) # [batch_size, seq_len, hid_dim*2]
# 合并双向的最终隐藏状态
hidden = torch.tanh(self.fc(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1))) # [batch_size, hid_dim]
return outputs, hidden
2. 注意力机制(Luong Attention)
实现Luong的General Attention(双线性评分)。
class Attention(nn.Module):
def __init__(self, encoder_hid_dim, decoder_hid_dim):
super(Attention, self).__init__()
self.attn = nn.Linear(encoder_hid_dim * 2, decoder_hid_dim, bias=False) # 双线性变换矩阵 Wa
def forward(self, decoder_hidden, encoder_outputs, mask):
# decoder_hidden: [batch_size, decoder_hid_dim]
# encoder_outputs: [batch_size, src_len, encoder_hid_dim * 2]
# mask: [batch_size, src_len]
decoder_hidden = decoder_hidden.unsqueeze(1) # [batch_size, 1, decoder_hid_dim]
# 计算注意力得分
energy = torch.bmm(decoder_hidden, self.attn(encoder_outputs).transpose(1, 2)) # [batch_size, 1, src_len]
energy = energy.squeeze(1).masked_fill(mask == 0, -1e10) # 屏蔽填充位置 [batch_size, src_len]
attention = torch.softmax(energy, dim=1) # [batch_size, src_len]
return attention
3. 解码器(Decoder with Attention)


解码器在每一步使用Attention计算出的上下文向量。


class Decoder(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, encoder_hid_dim, dropout=0.2):
super(Decoder, self).__init__()
self.vocab_size = vocab_size
self.attention = Attention(encoder_hid_dim, hidden_size)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.gru = nn.GRU(embed_size + encoder_hid_dim * 2, hidden_size, batch_first=True)
self.fc_out = nn.Linear(embed_size + encoder_hid_dim * 2 + hidden_size, vocab_size)
self.dropout = nn.Dropout(dropout)
def forward(self, y, hidden, encoder_outputs, mask):
# y: [batch_size, trg_len]
# hidden: [batch_size, hid_dim] (初始为编码器最终状态)
# encoder_outputs: [batch_size, src_len, encoder_hid_dim*2]
# mask: [batch_size, src_len]
y = y.unsqueeze(1) if y.dim() == 1 else y # 确保是二维
embedded = self.dropout(self.embedding(y)) # [batch_size, trg_len, embed_size]
outputs = []
for i in range(y.shape[1]):
# 计算当前步的注意力权重和上下文向量
attn_weights = self.attention(hidden, encoder_outputs, mask) # [batch_size, src_len]
context = torch.bmm(attn_weights.unsqueeze(1), encoder_outputs) # [batch_size, 1, encoder_hid_dim*2]
# GRU输入:当前词嵌入 + 上下文向量
gru_input = torch.cat((embedded[:, i:i+1, :], context), dim=2) # [batch_size, 1, embed_size + encoder_hid_dim*2]
output, hidden = self.gru(gru_input, hidden.unsqueeze(0))
hidden = hidden.squeeze(0)
# 预测输出
output = torch.cat((embedded[:, i:i+1, :], context, output), dim=2)
prediction = self.fc_out(output.squeeze(1))
outputs.append(prediction.unsqueeze(1))
outputs = torch.cat(outputs, dim=1) # [batch_size, trg_len, vocab_size]
return outputs, hidden


4. Seq2Seq模型
最后,将编码器和解码器组合成完整的Sequence-to-Sequence模型。


class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder):
super(Seq2Seq, self).__init__()
self.encoder = encoder
self.decoder = decoder
def forward(self, src, src_len, trg, trg_len):
encoder_outputs, hidden = self.encoder(src, src_len)
# 创建源序列的mask
src_mask = (src != 0).float() # 假设0是<pad>的索引
decoder_outputs, _ = self.decoder(trg, hidden, encoder_outputs, src_mask)
return decoder_outputs
def translate(self, src, src_len, max_len=50):
encoder_outputs, hidden = self.encoder(src, src_len)
src_mask = (src != 0).float()
trg_indices = [cn_word2idx['<bos>']]
for _ in range(max_len):
trg_tensor = torch.LongTensor([trg_indices[-1]]).unsqueeze(0).to(src.device)
output, hidden = self.decoder(trg_tensor, hidden, encoder_outputs, src_mask)
next_word = output.argmax(2).item()
trg_indices.append(next_word)
if next_word == cn_word2idx['<eos>']:
break
return trg_indices[1:] # 去掉<bos>



训练与评估

定义好模型后,我们需要编写训练和评估循环。
def train(model, iterator, optimizer, criterion, clip):
model.train()
epoch_loss = 0
for i, batch in enumerate(iterator):
src, src_len, trg, trg_len = process_batch(batch) # 假设此函数处理一个batch的数据
optimizer.zero_grad()
output = model(src, src_len, trg[:, :-1], trg_len-1) # 解码器输入是trg去掉最后一个<eos>
# 计算损失,忽略<pad>
loss = criterion(output.reshape(-1, output.shape[-1]), trg[:, 1:].reshape(-1))
loss.backward()
clip_grad_norm_(model.parameters(), clip)
optimizer.step()
epoch_loss += loss.item()
return epoch_loss / len(iterator)
def evaluate(model, iterator, criterion):
model.eval()
epoch_loss = 0
with torch.no_grad():
for batch in iterator:
src, src_len, trg, trg_len = process_batch(batch)
output = model(src, src_len, trg[:, :-1], trg_len-1)
loss = criterion(output.reshape(-1, output.shape[-1]), trg[:, 1:].reshape(-1))
epoch_loss += loss.item()
return epoch_loss / len(iterator)
模型初始化与训练
现在,我们可以初始化模型并开始训练。
# 超参数
EMBED_SIZE = 256
HIDDEN_SIZE = 512
ENCODER_HID_DIM = HIDDEN_SIZE * 2 # 因为是双向
DROPOUT = 0.2
CLIP = 1.0
LEARNING_RATE = 0.001
N_EPOCHS = 10




# 初始化模型
encoder = Encoder(len(en_vocab), EMBED_SIZE, HIDDEN_SIZE, DROPOUT)
decoder = Decoder(len(cn_vocab), EMBED_SIZE, HIDDEN_SIZE, ENCODER_HID_DIM, DROPOUT)
model = Seq2Seq(encoder, decoder).to(device)
# 定义优化器和损失函数
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
criterion = nn.CrossEntropyLoss(ignore_index=0) # 忽略<pad>索引0
# 训练循环
for epoch in range(N_EPOCHS):
train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
valid_loss = evaluate(model, dev_iterator, criterion)
print(f'Epoch: {epoch+1:02}')
print(f'\tTrain Loss: {train_loss:.3f}')
print(f'\t Val. Loss: {valid_loss:.3f}')



测试翻译


训练完成后,我们可以用模型进行翻译测试。
def translate_sentence(sentence, model, device):
model.eval()
# 预处理输入句子
tokens = ['<bos>'] + nltk.word_tokenize(sentence.lower()) + ['<eos>']
indices = [en_word2idx.get(token, en_word2idx['<unk>']) for token in tokens]
src_tensor = torch.LongTensor(indices).unsqueeze(0).to(device)
src_len = torch.LongTensor([len(indices)]).to(device)
with torch.no_grad():
trg_indices = model.translate(src_tensor, src_len)
trg_tokens = [cn_vocab[i] for i in trg_indices]
return ''.join(trg_tokens) # 中文按字输出,直接拼接
# 示例
test_sentence = "How are you?"
translation = translate_sentence(test_sentence, model, device)
print(f"输入: {test_sentence}")
print(f"翻译: {translation}")



聊天机器人简介 🤖
Sequence-to-Sequence模型也被广泛应用于开放域聊天机器人(Open-Domain Chatbot)的构建。其基本思想是将对话历史作为输入,将生成的回复作为输出。

然而,纯粹的生成式模型在实际产品中面临挑战,


【七月在线】NLP高端就业训练营10期 - P8:4.隐马尔可夫模型原理 📊





概述
在本节课中,我们将要学习隐马尔可夫模型(HMM)的基本原理。HMM是概率图模型中的重要组成部分,尤其在自然语言处理领域有着广泛的应用。我们将从模型定义、基本假设、核心问题及其求解方法等方面,系统地介绍HMM。
模型定义与结构 🏗️
隐马尔可夫模型是关于时序的概率模型。它描述了一个由隐藏的马尔可夫链随机生成不可观测的状态序列,再由各个状态生成一个观测,从而产生观测随机序列的过程。
核心概念与符号
- 状态集合 Q:所有可能状态的集合,
Q = {q1, q2, ..., qN},其中N是状态的数量。 - 观测集合 V:所有可能观测的集合,
V = {v1, v2, ..., vM},其中M是观测的数量。 - 状态序列 I:长度为
T的状态序列,I = (i1, i2, ..., iT),其中it ∈ Q。 - 观测序列 O:长度为
T的观测序列,O = (o1, o2, ..., oT),其中ot ∈ V。状态序列与观测序列长度一致,因为每个状态生成一个观测。 - 状态转移概率矩阵 A:一个
N x N的矩阵,其中元素aij表示在时刻t处于状态qi的条件下,在时刻t+1转移到状态qj的概率。公式为:aij = P(it+1 = qj | it = qi)。 - 观测概率矩阵 B:一个
N x M的矩阵,其中元素bj(k)表示在时刻t处于状态qj的条件下,生成观测vk的概率。公式为:bj(k) = P(ot = vk | it = qj)。 - 初始状态概率向量 π:一个
N维向量,其中元素πi表示时刻t=1处于状态qi的概率。公式为:πi = P(i1 = qi)。
因此,一个隐马尔可夫模型 λ 可以用三元组表示:λ = (A, B, π)。
两个基本假设 🔧
为了简化模型的计算,HMM 引入了两个基本假设。

上一节我们介绍了HMM的模型结构,本节中我们来看看为了便于计算而引入的两个关键假设。
1. 齐次马尔可夫性假设
该假设认为,任意时刻 t 的状态只依赖于其前一时刻 t-1 的状态,而与更早的状态和观测无关。
P(it | i1, o1, ..., it-1, ot-1) = P(it | it-1)
2. 观测独立性假设
该假设认为,任意时刻 t 的观测只依赖于该时刻的状态,而与其他时刻的状态及观测无关。
P(ot | i1, o1, ..., it, ..., iT, oT) = P(ot | it)

HMM解决的三个基本问题 🎯

HMM主要用来解决三类问题,这三类问题构成了HMM应用的核心。

以下是HMM能够解决的三个核心问题:
- 概率计算问题(评估问题)
- 已知:模型
λ = (A, B, π)和观测序列O。 - 求解:计算在该模型下,观测序列
O出现的概率P(O | λ)。这个问题是后续问题的基础,为学习问题和预测问题提供中间计算结果。
- 已知:模型

- 学习问题(参数估计问题)
- 已知:观测序列
O。 - 求解:估计模型参数
λ = (A, B, π),使得该模型下观测序列O的概率P(O | λ)最大。这是一个“无中生有”的过程,即仅从观测数据学习出模型。
- 已知:观测序列

- 预测问题(解码问题)
- 已知:模型
λ = (A, B, π)和观测序列O。 - 求解:求最有可能(概率最大)对应的状态序列
I。这是HMM最核心的应用,例如在词性标注中,观测序列是单词序列,预测出的状态序列就是词性标签序列。
- 已知:模型






问题一:概率计算与前向算法 ➕
我们首先来看概率计算问题。给定模型和观测序列,如何高效计算 P(O | λ)?直接计算复杂度极高,因此引入前向算法。
前向概率定义
定义前向概率 αt(i) 为:在给定模型 λ 的条件下,到时刻 t 的部分观测序列为 o1, o2, ..., ot,且时刻 t 的状态为 qi 的概率。
αt(i) = P(o1, o2, ..., ot, it = qi | λ)
前向算法递推过程
- 初始化:计算第一个时刻的前向概率。
α1(i) = πi * bi(o1), i=1,2,...,N - 递推:对
t = 1, 2, ..., T-1,计算:
αt+1(j) = [ Σ αt(i) * aij ] * bj(ot+1), j=1,2,...,N
其中,Σ表示对i从1到N求和。 - 终止:计算观测序列概率。
P(O | λ) = Σ αT(i), i=1,2,...,N
前向算法通过动态规划,将指数级复杂度的计算降至 O(N²T)。






问题二:学习问题与EM算法 🧠
学习问题是如何仅从观测序列 O 估计出模型参数 λ。由于状态序列 I 是隐藏的,这是一个含有隐变量的参数估计问题,可以使用期望最大化(EM)算法求解。

EM算法框架回顾
EM算法用于求解含有隐变量的概率模型参数估计问题。
- E步(期望步):基于当前参数估计值
λ,计算完全数据(观测O和隐状态I)的对数似然函数关于隐变量条件概率分布的期望,即Q函数:
Q(λ, λ) = Σ P(I | O, λ) * log P(O, I | λ) - M步(最大化步):寻找使Q函数最大化的新参数值
λ:
λ = argmax Q(λ, λ)
在HMM中的应用(Baum-Welch算法)
将HMM的学习问题套入EM框架:
- 完全数据:
(O, I) - Q函数:
Q(λ, λ) = Σ_I P(I | O, λ) log P(O, I | λ) - 通过对Q函数分别关于
π,A,B求极大值(通常借助拉格朗日乘子法处理概率和为1的约束),可以得到参数的重估公式。这些公式可以用一些在概率计算问题中得到的中间概率(如前向概率、后向概率)来表示,从而迭代更新模型参数。

问题三:预测问题与维特比算法 🏆
预测问题是给定模型和观测序列,求最可能的状态序列。这是一个最优路径问题,可以使用维特比算法(Viterbi Algorithm)解决,它是一种动态规划算法。
维特比算法
定义在时刻 t 状态为 i 的所有单个路径 (i1, i2, ..., it) 中概率最大值:
δt(i) = max P(i1, i2, ..., it=qi, o1, o2, ..., ot | λ)
- 初始化:
δ1(i) = πi * bi(o1), i=1,2,...,N
ψ1(i) = 0 - 递推:对
t = 2, 3, ..., T
δt(j) = max [ δt-1(i) * aij ] * bj(ot), i=1,2,...,N
ψt(j) = argmax [ δt-1(i) * aij ], i=1,2,...,N - 终止:
P* = max δT(i), i=1,2,...,N
iT* = argmax δT(i), i=1,2,...,N - 最优路径回溯:对
t = T-1, T-2, ..., 1
it* = ψt+1(it+1*)


最终得到最优状态序列 I* = (i1*, i2*, ..., iT*)。

总结
本节课中我们一起学习了隐马尔可夫模型(HMM)的核心原理。我们从模型的定义和两个基本假设出发,明确了HMM描述的是隐藏状态序列与观测序列之间的生成关系。接着,我们深入探讨了HMM旨在解决的三个基本问题:概率计算、参数学习和状态预测,并分别介绍了对应的前向算法、Baum-Welch算法(EM)和维特比算法。理解HMM为解决序列到序列的映射问题(如词性标注、分词)提供了经典的概率框架,是学习更复杂模型(如条件随机场)的重要基础。
【七月在线】NLP高端就业训练营10期 - P9:6.命名实体识别与CRF应用 🧠
概述
在本节课中,我们将学习序列标注任务及其在自然语言处理(NLP)中的核心应用。我们将首先回顾隐马尔可夫模型(HMM)的基本原理及其如何解决序列标注问题。接着,我们将深入探讨条件随机场(CRF)模型,并重点介绍如何将双向长短期记忆网络(Bi-LSTM)与CRF结合,构建强大的命名实体识别(NER)模型。课程内容旨在让初学者理解核心概念,并通过公式和代码示例加深理解。
第一部分:序列标注与隐马尔可夫模型
1.1 序列标注任务定义
序列标注是一类重要的机器学习任务。给定一个输入序列 X = (x₁, x₂, ..., xₙ),目标是计算出序列中每个元素对应的标签,从而得到一个输出序列 Y = (y₁, y₂, ..., yₙ)。
需要特别注意两点:
- 输入 X 和输出 Y 都是序列,强调元素之间的严格先后顺序,这与无序的数据集集合不同。
- 输入序列 X 和输出序列 Y 中的元素是一一对应的。
在NLP中,输入序列 X 通常是字符序列或词语序列,输出序列 Y 则是对应的标签,如词性(名词、动词等)或分词角色(B, M, E, S)。
示例:中文分词
对于句子“序列标注与中文分词”,其分词标注(BMES)结果为:
- 序列 → B E
- 标注 → B E
- 与 → S
- 中文 → B E
- 分词 → B E
因此,输入是字序列,输出是标签序列[B, E, B, E, S, B, E, B, E]。模型的目标就是学习从 X 到 Y 的映射。
上一节我们明确了序列标注任务的定义,本节中我们来看看解决此类任务的一个经典概率模型——隐马尔可夫模型。
1.2 隐马尔可夫模型(HMM)基础
HMM是一种用于描述含有隐含状态的序列的概率模型。它包含以下核心组件:
- 状态序列 I: 不可见的隐含状态序列,I = (i₁, i₂, ..., iₜ)。每个状态 iₜ 取自状态集合 Q = {q₁, q₂, ..., qₙ}。
- 观测序列 O: 可见的观测序列,O = (o₁, o₂, ..., oₜ)。每个观测 oₜ 取自观测集合 V = {v₁, v₂, ..., vₘ}。
- 状态转移矩阵 A: 一个 N×N 的矩阵,其中元素 aᵢⱼ 表示在时刻 t 处于状态 qᵢ 的条件下,在时刻 t+1 转移到状态 qⱼ 的概率。
- 公式: aᵢⱼ = P(iₜ₊₁ = qⱼ | iₜ = qᵢ)
- 观测概率矩阵 B: 一个 N×M 的矩阵,其中元素 bⱼₖ 表示在时刻 t 处于状态 qⱼ 的条件下,生成观测 vₖ 的概率。
- 公式: bⱼₖ = P(oₜ = vₖ | iₜ = qⱼ)
- 初始状态概率向量 π: 一个 N 维向量,其中元素 πᵢ 表示初始时刻 (t=1) 处于状态 qᵢ 的概率。
- 公式: πᵢ = P(i₁ = qᵢ)


一个HMM模型 λ 由以上三个参数决定:λ = (A, B, π)。
为了简化计算,HMM做了两个重要假设:
- 齐次马尔可夫性假设: 任意时刻 t 的状态只依赖于其前一时刻 t-1 的状态。
- 观测独立性假设: 任意时刻 t 的观测只依赖于该时刻的状态。
1.3 HMM的三个基本问题
HMM主要解决三类问题:
- 概率计算问题(评估): 给定模型 λ 和观测序列 O,计算该观测序列出现的概率 P(O|λ)。(前向-后向算法)
- 学习问题(参数估计): 仅给定观测序列 O,估计模型参数 λ = (A, B, π),使得 P(O|λ) 最大。(Baum-Welch算法,一种EM算法)
- 预测问题(解码): 给定模型 λ 和观测序列 O,求最有可能对应的状态序列 I。(维特比算法)
在序列标注任务中的应用:
将观测序列 O 视为输入文本(可见),状态序列 I 视为标签序列(隐藏)。通过解决学习问题,我们可以从大量标注语料(O, I)中学习出模型 λ(即语言模型)。然后,对于新的输入文本 O‘,通过解决预测问题,即可得到最可能的标签序列 I’。这就完成了从序列到序列的映射。
1.4 HMM代码示例
以下是使用 hmmlearn 库构建一个简单HMM(模拟健康/发烧状态)的示例。
import numpy as np
from hmmlearn import hmm
# 1. 定义状态和观测集合
states = ["Healthy", "Fever"] # 状态集合 Q
observations = ["normal", "cold", "dizzy"] # 观测集合 V
# 2. 定义模型参数
# 初始状态概率 π
start_probability = np.array([0.6, 0.4]) # [健康, 发烧]
# 状态转移矩阵 A
transition_probability = np.array([
[0.7, 0.3], # 健康 -> [健康, 发烧]
[0.4, 0.6] # 发烧 -> [健康, 发烧]
])
# 观测概率矩阵 B
emission_probability = np.array([
[0.5, 0.4, 0.1], # 健康状态下生成 [正常,冷,头晕] 的概率
[0.1, 0.3, 0.6] # 发烧状态下生成 [正常,冷,头晕] 的概率
])
# 3. 创建并训练模型(这里直接传入参数,而非用数据拟合)
model = hmm.CategoricalHMM(n_components=len(states))
model.startprob_ = start_probability
model.transmat_ = transition_probability
model.emissionprob_ = emission_probability
# 4. 解码(预测问题):给定观测序列,求最可能的状态序列
# 将观测值编码为索引
obs_seq = np.array([[0, 2, 1]]).T # 对应 [“normal”, “dizzy”, “cold”]
logprob, state_seq = model.decode(obs_seq, algorithm="viterbi")
print("观测序列:", [observations[i[0]] for i in obs_seq])
print("最可能状态序列:", [states[i] for i in state_seq])
print("序列对数概率:", logprob)
以上我们介绍了HMM模型及其在序列标注中的应用。然而,HMM基于较强的独立性假设,且难以融入复杂的特征。接下来,我们将探讨更强大的序列标注模型——条件随机场。
第二部分:命名实体识别与CRF模型
2.1 从HMM到CRF
HMM是生成式模型,它试图对联合概率 P(O, I) 进行建模。而条件随机场(CRF)是一种判别式模型,它直接对条件概率 P(I | O) 进行建模。这意味着CRF专注于给定输入序列条件下,输出序列的分布,通常能取得更好的性能。
线性链条件随机场(Linear-Chain CRF)是NLP中最常用的形式,其定义如下:
给定输入序列 X = (x₁, x₂, ..., xₙ),输出序列 Y = (y₁, y₂, ..., yₙ) 的条件概率为:
公式:
P(Y | X) = (1 / Z(X)) * exp( Σₜ Σₖ λₖ fₖ(yₜ₋₁, yₜ, X, t) + Σₜ Σₗ μₗ gₗ(yₜ, X, t) )
其中:
- Z(X) 是归一化因子(配分函数),确保所有可能Y序列的概率和为1。
- fₖ 是边特征函数,定义在相邻标签 (yₜ₋₁, yₜ) 上,捕捉标签之间的转移关系(如:前一个词是动词时,当前词是名词的概率)。
- gₗ 是点特征函数,定义在单个标签 yₜ 上,捕捉当前观测与标签的关系(如:当前词是大写时,它是人名起始标签B-PER的概率)。
- λₖ 和 μₗ 是对应特征函数的权重,是模型需要学习的参数。
CRF的优点在于可以灵活地定义大量特征函数,从而充分利用上下文信息。
2.2 双向LSTM-CRF模型
尽管CRF强大,但手工设计特征函数费时费力。深度学习模型,如LSTM,能够自动从数据中学习丰富的特征表示。因此,将两者结合的 Bi-LSTM-CRF 模型成为了序列标注(特别是NER)的强基准模型。
模型结构:
- 嵌入层: 将输入的每个词或字转换为稠密向量。
- Bi-LSTM层: 双向LSTM捕获每个时间步的上下文信息,输出每个词的上下文相关表示。
- CRF层: 接收Bi-LSTM的输出作为“点特征”的分数,同时学习标签之间的转移矩阵(对应“边特征”的分数),共同决定全局最优的标签序列。
核心思想:
- Bi-LSTM为每个位置 t 的单词生成一个分数向量,表示该单词属于每个标签的“置信度”。
- CRF层有一个标签转移矩阵,存储了从标签 i 转移到标签 j 的分数。
- 对于一个候选标签序列 Y,其总分数由两部分组成:所有位置LSTM输出的标签分数之和 + 所有相邻标签的转移分数之和。
- 模型训练目标是最大化正确标签序列的总分数(通过负对数似然损失)。
分数计算示例:
假设标签集为 {O, B-PER, I-PER}。对于句子“Barack Obama”,Bi-LSTM输出每个词的分数,CRF有一个3x3的转移矩阵。
- 序列
[B-PER, I-PER]的分数 = (LSTM给“Barack”的B-PER分) + (LSTM给“Obama”的I-PER分) + (转移矩阵中 B-PER → I-PER 的分)。 - 模型会对比所有可能序列(如
[O, O],[B-PER, O]等)的分数,并通过维特比算法高效地找到分数最高的序列。
2.3 模型实现要点
以下是使用PyTorch框架构建Bi-LSTM-CRF的核心代码逻辑概述:
import torch
import torch.nn as nn
import torch.optim as optim
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
super(BiLSTM_CRF, self).__init__()
self.embedding_dim = embedding_dim
self.hidden_dim = hidden_dim
self.vocab_size = vocab_size
self.tag_to_ix = tag_to_ix
self.tagset_size = len(tag_to_ix)
self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,
num_layers=1, bidirectional=True, batch_first=True)
# 将LSTM输出映射到标签空间
self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)
# CRF参数:转移矩阵,transition_matrix[i, j] 表示从标签j转移到标签i的分数
self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))
# 约束:不可能从其他标签转移到开始标签(START),也不可能从结束标签(STOP)转移到其他标签
self.transitions.data[tag_to_ix[START_TAG], :] = -10000
self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000
def _forward_alg(self, feats):
# 实现前向算法,计算所有可能序列的分数和(即配分函数Z(X))
# ... (省略具体实现)
return torch.logsumexp(init_alphas, dim=1)
def _score_sentence(self, feats, tags):
# 计算给定标签序列tags的分数
# ... (省略具体实现)
return score
def _viterbi_decode(self, feats):
# 维特比解码,寻找分数最高的标签序列
# ... (省略具体实现)
return path_score, best_path
def neg_log_likelihood(self, sentence, tags):
# 计算负对数似然损失
feats = self._get_lstm_features(sentence) # 通过Bi-LSTM获取特征分数
forward_score = self._forward_alg(feats) # 配分函数
gold_score = self._score_sentence(feats, tags) # 正确序列的分数
return forward_score - gold_score # 损失 = log(Z) - S(gold)
def forward(self, sentence):
# 前向传播(解码阶段)
lstm_feats = self._get_lstm_features(sentence)
score, tag_seq = self._viterbi_decode(lstm_feats)
return score, tag_seq
2.4 命名实体识别(NER)任务
命名实体识别是序列标注的一个典型应用,旨在识别文本中具有特定意义的实体,并将其分类到预定义的类别中,如人名(PER)、地名(LOC)、组织名(ORG)等。
常用标签方案: BIO或BIOES
- B-XXX: 实体开头
- I-XXX: 实体内部
- E-XXX: 实体结尾(BIOES方案)
- S-XXX: 单字实体(BIOES方案)
- O: 非实体
示例:
句子: “马云在杭州创立了阿里巴巴集团。”
标注: 马/B-PER 云/E-PER 在/O 杭/B-LOC 州/E-LOC 创/O 立/O 了/O 阿/B-ORG 里/I-ORG 巴/I-ORG 巴/I-ORG 集/I-ORG 团/E-ORG 。/O
Bi-LSTM-CRF模型在此任务上表现优异,因为它既能利用Bi-LSTM捕获“杭州”、“阿里巴巴集团”这类上下文依赖,又能利用CRF施加“B-LOC后面不能接I-PER”之类的标签约束。
总结
本节课我们一起学习了序列标注任务及其在NLP中的核心模型。
- 序列标注: 我们首先明确了序列标注任务的定义,即从输入序列到输出序列的一一映射,并以中文分词和词性标注为例说明了其应用。
- 隐马尔可夫模型(HMM): 作为经典的生成式概率模型,我们学习了HMM的三大组件(A, B, π)和两个基本假设。HMM通过解决学习问题和预测问题,可以完成序列标注。我们通过一个健康监测的代码示例进行了实践。
- 条件随机场(CRF)与Bi-LSTM-CRF: 我们了解到CRF作为判别式模型的优势。重点介绍了将深度学习的特征学习能力(Bi-LSTM)与CRF的序列全局优化能力相结合的Bi-LSTM-CRF模型。该模型通过联合计算LSTM的发射分数和CRF的转移分数,并利用维特比解码,在命名实体识别等任务上取得了显著效果。
- 命名实体识别(NER): 作为序列标注的重要应用,我们介绍了NER的任务目标和常用的标注体系。


从HMM到CRF,再到深度学习与CRF的结合,体现了NLP技术从概率图模型到深度神经网络的发展脉络。理解这些基础模型,对于后续学习更先进的预训练模型(如BERT)及其在NER等任务上的应用至关重要。
机器学习基础课程01:线性回归、逻辑回归与梯度下降 🧠
在本节课中,我们将要学习机器学习中最基础且核心的两个算法:线性回归和逻辑回归。我们将从机器学习的基本概念入手,理解监督学习与无监督学习的区别,然后深入探讨这两个回归类算法的原理、假设函数、损失函数以及如何通过梯度下降进行优化。课程最后,我们会通过代码示例来巩固理解。
机器学习基本概念
在深入学习具体算法之前,我们需要建立一个宏观的机器学习框架。机器学习主要分为两大类:监督学习和无监督学习。
监督学习 (Supervised Learning)
监督学习类似于有参考答案的学习过程。我们拥有输入数据 X 和对应的正确答案(标签)Y。算法的目标是从这些“题目”和“答案”中学习出一个从 X 到 Y 的映射关系 F,以便在面对新的、没有答案的“题目”时,能给出准确的预测。
- 分类问题:输出
Y是离散的类别标签,例如判断邮件是否为垃圾邮件(是/否),或识别图像中的动物种类(猫/狗/鸟)。这相当于做“选择题”。 - 回归问题:输出
Y是连续的数值,例如预测房价、票房或温度。这相当于做“填空题”或“问答题”。
无监督学习 (Unsupervised Learning)
无监督学习则是在没有标签的数据中寻找内在结构和模式。例如,根据用户的购物行为将用户分成不同的群体(聚类),或者发现商品之间的关联规则(如“啤酒与尿布”)。
上一节我们介绍了机器学习的宏观分类,本节中我们来看看监督学习中最基础的两个模型。
线性回归 (Linear Regression) 📈
线性回归用于解决回归问题,其核心思想是假设输入特征 X 和输出 Y 之间存在线性关系。
假设函数 (Hypothesis Function)
我们假设映射关系 F 是一个线性组合:
h_θ(x) = θ₀ + θ₁x₁ + θ₂x₂ + ... + θₙxₙ
其中,θ 是模型需要学习的参数(权重),x 是输入特征。
为了简化表示,我们通常使用向量化形式。令 θ 和 x 都为列向量(并在 x 前添加一个值为1的 x₀ 以包含截距项 θ₀),则假设函数可以写为:
h_θ(x) = θᵀx
损失函数与优化目标
仅仅知道假设函数的形式还不够,我们需要一个标准来衡量一组参数 θ 的好坏。这个标准就是损失函数。
对于线性回归,最常用的损失函数是均方误差:
J(θ) = (1/(2m)) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)²
其中,m 是样本数量。J(θ) 衡量了模型预测值与真实值之间的平均差异(乘以 1/2 是为了后续求导方便,不影响优化方向)。
我们的优化目标就是找到一组参数 θ*,使得损失函数 J(θ) 最小化:
θ* = argmin_θ J(θ)
梯度下降算法 (Gradient Descent)
如何找到使 J(θ) 最小的 θ 呢?一个直观的方法是梯度下降。想象 J(θ) 是一个碗状曲面,我们随机放置一个小球(随机初始化 θ),让它沿着最陡的下坡方向滚动,最终它会到达碗底(最小值点)。
这个“最陡的下坡方向”就是损失函数 J(θ) 在当前 θ 处的负梯度。梯度下降的更新公式为:
θ_j := θ_j - α * (∂J(θ)/∂θ_j)
对于所有参数 j 同时进行更新。其中 α 称为学习率,它控制了每次更新的步长。
- 学习率
α的影响:α太小:收敛速度慢,需要很多次迭代。α太大:可能越过最低点,导致无法收敛甚至发散。α是一个需要手动调节的超参数。
将均方误差的梯度代入,可以得到线性回归的梯度下降更新规则:
θ_j := θ_j - α * (1/m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾) * x_j⁽ⁱ⁾
逻辑回归 (Logistic Regression) 🎯
逻辑回归虽然名字里有“回归”,但它是一个用于解决二分类问题的模型。它的目标是预测一个样本属于正类(如“是垃圾邮件”)的概率。
从线性回归到分类问题
直接使用线性回归的输出(范围是 (-∞, +∞))作为概率并不合适,因为它可能超出 [0, 1] 区间,且对异常值敏感。我们需要一个函数将线性回归的结果映射到 (0, 1) 之间。
Sigmoid 函数
这个函数就是 Sigmoid 函数(或 Logistic 函数):
g(z) = 1 / (1 + e^{-z})
它的值域是 (0, 1),并且具有良好的数学性质,其导数 g'(z) = g(z)(1 - g(z))。
假设函数与决策边界
我们将线性回归的结果 z = θᵀx 送入 Sigmoid 函数,得到逻辑回归的假设函数:
h_θ(x) = g(θᵀx) = 1 / (1 + e^{-θᵀx})
h_θ(x) 的输出可以解释为 y=1 的概率,即 P(y=1|x; θ)。
我们设定一个阈值(通常为0.5):
- 如果
h_θ(x) >= 0.5,预测y=1。 - 如果
h_θ(x) < 0.5,预测y=0。
由于g(z) >= 0.5等价于z >= 0,所以决策边界实际上是由θᵀx = 0这个线性(或经特征变换后的非线性)方程决定的。
损失函数:对数损失
我们不能再用均方误差作为逻辑回归的损失函数,因为它会导致 J(θ) 非凸,存在多个局部最小值,不利于优化。
逻辑回归使用对数损失(或交叉熵损失):
J(θ) = - (1/m) * Σ [ y⁽ⁱ⁾ log(h_θ(x⁽ⁱ⁾)) + (1 - y⁽ⁱ⁾) log(1 - h_θ(x⁽ⁱ⁾)) ]
这个函数是凸函数,保证了梯度下降能找到全局最优解。
其梯度下降更新公式(经过求导)为:
θ_j := θ_j - α * (1/m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾) * x_j⁽ⁱ⁾
形式上与线性回归完全一致,但请注意 h_θ(x) 的定义已变为 Sigmoid 函数。
多分类扩展
逻辑回归本质上是二分类器。对于多分类问题,有两种常用策略:
- 一对多 (One-vs-Rest, OvR):训练
K个分类器(K为类别数),每个分类器判断样本是否属于特定类别。 - 一对一 (One-vs-One, OvO):为每两个类别训练一个分类器,共
K(K-1)/2个,通过投票决定最终类别。
过拟合、欠拟合与正则化 ⚖️
在模型训练中,我们常遇到两个问题:
- 欠拟合:模型过于简单,无法捕捉数据中的基本模式。对应高偏差。
- 过拟合:模型过于复杂,不仅学习了数据中的规律,还“记忆”了噪声和异常值。对应高方差,在新数据上表现差。
解决过拟合的一个核心方法是正则化。它在损失函数中增加一个惩罚项,限制参数 θ 的幅度,迫使模型变得更简单、平滑。
L2 正则化
在损失函数中加入参数 θ 的 L2 范数平方(通常不惩罚截距项 θ₀):
J(θ)_reg = J(θ) + (λ/(2m)) * Σ θ_j²
其中 λ 是正则化参数,也是一个超参数。
λ太大:惩罚过重,可能导致欠拟合。λ太小:惩罚不足,可能无法抑制过拟合。
加入 L2 正则化后,梯度下降的更新公式变为:
θ_j := θ_j - α * [ (1/m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾) * x_j⁽ⁱ⁾ + (λ/m) * θ_j ]
代码实践与工具库
在实际应用中,我们通常使用成熟的机器学习库,而不是从头手写算法。以下是两个推荐的工具库:
- Scikit-learn:Python 中最流行的机器学习库之一,提供了
LinearRegression和LogisticRegression等简洁易用的接口。 - LIBSVM / LIBLINEAR:由台湾大学开发的高效库(C++),Scikit-learn 中的 SVM 和逻辑回归部分对其进行了封装。
核心代码示例(使用 Scikit-learn):
# 线性回归
from sklearn.linear_model import LinearRegression
model_lr = LinearRegression()
model_lr.fit(X_train, y_train) # 拟合数据
predictions = model_lr.predict(X_test) # 预测
# 逻辑回归
from sklearn.linear_model import LogisticRegression
model_logr = LogisticRegression(C=1.0) # C 是正则化强度的倒数,即 C = 1/λ
model_logr.fit(X_train, y_train)
predictions = model_logr.predict(X_test)
probabilities = model_logr.predict_proba(X_test) # 获取预测概率
总结
本节课中我们一起学习了机器学习的基础入门知识:
- 建立了机器学习的地图:理解了监督学习(分类/回归)与无监督学习的区别。
- 掌握了线性回归:学习了其线性假设函数、均方误差损失函数,以及通过梯度下降进行优化的全过程。
- 深入理解了逻辑回归:明白了如何通过 Sigmoid 函数将线性模型用于分类,并学习了其特有的对数损失函数。
- 认识了模型的核心挑战:了解了过拟合与欠拟合的概念,并学会了使用 L2 正则化 来对抗过拟合,提升模型泛化能力。
- 接触了实践工具:了解了使用 Scikit-learn 等库可以快速实现模型,而理解底层原理是调优和解决问题的关键。


逻辑回归因其简单、高效、可解释性强,至今仍是工业界许多场景下的首选基线模型。理解好这两个基础模型,将为后续学习更复杂的算法打下坚实的基础。


【七月在线】机器学习就业训练营16期 - P10:机器学习基本流程、基础模型与sklearn使用教程 📚
在本节课中,我们将学习机器学习的基本流程,并重点介绍如何使用Python的scikit-learn(简称sklearn)库来构建和评估基础模型。课程内容将涵盖从数据预处理到模型训练、验证和调参的全过程。
第一部分:课程概述与实战重要性 🎯


大家好,我是刘老师。在接下来的两周里,我将带领大家学习机器学习的实战部分。今天的课程将重点讲解机器学习的基本流程,并以sklearn库为核心进行演示。
机器学习的学习中,原理与实战同等重要。原理帮助我们理解算法的来龙去脉,而实战则能增加我们对算法的具体理解,积累解决工业级案例的经验,并为后续的项目实践打下基础。
在Python环境下进行机器学习实战,掌握核心库的使用是必备技能。sklearn是其中功能最强大、应用最广泛的机器学习库之一,涵盖了数据预处理、特征工程、模型训练与评估等各个环节。
第二部分:scikit-learn 库介绍 🧰
scikit-learn(sklearn)是Python环境下功能最强大的机器学习库之一。它几乎涵盖了所有常见的机器学习算法,并提供了标准化的实现流程和易于使用的API。

sklearn的核心特点:
- 算法覆盖全面:包含分类、回归、聚类、降维等各类机器学习算法。
- 标准化API:所有模型都遵循
fit、predict、score等统一的接口,易于学习和使用。 - 高效稳定:底层基于
NumPy、SciPy等库,使用C/C++进行优化,保证了计算效率。 - 文档详尽:官方提供了非常清晰的API文档、教程和示例,是学习的绝佳资源。
如何选择算法?
sklearn官网提供了一张非常有用的算法选择指南图。其核心思路是:
- 根据样本数量(是否大于50)判断数据集是否可用。
- 根据任务目标判断是分类、回归、聚类还是降维问题。
- 对于有监督任务(分类/回归),再根据数据特征(如样本量、是否为文本等)进一步选择具体算法。
这张图是一个很好的思维框架,能帮助我们在面对具体问题时快速定位合适的算法。
第三部分:机器学习建模基本流程 🔄
上一部分我们介绍了sklearn库,本节中我们来看看使用它进行机器学习建模的标准流程。
一个完整的机器学习项目通常包含以下步骤:
1. 数据划分
在开始建模前,需要将数据划分为训练集、验证集和测试集。常见的划分方法有:
- 留出法:按固定比例(如8:2)随机划分。
- K折交叉验证:将数据均分为K份,依次用其中一份作为验证集,其余K-1份作为训练集,循环K次。
- 自助采样法:通过有放回采样构建训练集,未采到的样本作为验证集。
在sklearn中,可以使用model_selection.train_test_split进行留出法划分,使用model_selection.KFold或cross_val_score进行交叉验证。
2. 数据预处理与特征工程
原始数据通常不能直接输入模型,需要进行预处理。sklearn的preprocessing模块提供了丰富的工具:
- 标准化/归一化:如
StandardScaler,MinMaxScaler。 - 缺失值填充:如
SimpleImputer。 - 特征编码:对类别型特征进行编码,如
LabelEncoder,OneHotEncoder。
核心概念:fit与transform
fit:计算预处理所需的参数(如均值、方差),并保存状态。transform:使用fit阶段计算好的参数对数据进行转换。- 重要原则:预处理器(如
StandardScaler)只能在训练集上fit一次,然后在训练集和测试集上分别transform,以确保转换标准一致。

3. 模型训练与评估
选择好算法和处理好数据后,就可以进行模型训练。sklearn中所有模型都遵循相似的接口:
from sklearn.xxx import SomeModel
model = SomeModel(hyperparameter=value) # 1. 实例化模型
model.fit(X_train, y_train) # 2. 在训练集上训练
score = model.score(X_test, y_test) # 3. 在测试集上评估
y_pred = model.predict(X_new) # 4. 对新数据进行预测




4. 模型调参与选择
模型有许多超参数需要调整。sklearn的model_selection模块提供了自动调参工具:
- 网格搜索:
GridSearchCV,遍历所有给定的参数组合。 - 随机搜索:
RandomizedSearchCV,在给定的参数分布中随机采样。
5. 误差分析与流程迭代
根据模型在训练集和验证集上的表现,判断问题是欠拟合还是过拟合,并决定下一步优化方向(如获取更多数据、调整模型复杂度、增加正则化等)。
第四部分:sklearn 实战代码示例 💻
前面我们介绍了基本流程,现在让我们通过几个具体的代码示例来加深理解。
示例1:使用KNN进行鸢尾花分类
这是一个完整的迷你工作流示例。
# 导入必要的模块
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
# 1. 加载数据
iris = load_iris()
X, y = iris.data, iris.target
# 2. 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
# 3. 数据预处理:标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 只在训练集上fit
X_test_scaled = scaler.transform(X_test) # 用训练集的参数转换测试集
# 4. 训练模型
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train_scaled, y_train)
# 5. 预测与评估
y_pred = knn.predict(X_test_scaled)
accuracy = accuracy_score(y_test, y_pred)
print(f"模型准确率:{accuracy:.2f}")
示例2:构建Pipeline简化流程
对于包含多个步骤的流程,可以使用Pipeline将其串联,使代码更简洁,并防止数据泄露。
from sklearn.pipeline import make_pipeline
from sklearn.ensemble import RandomForestClassifier



# 创建一个管道:先标准化,再使用随机森林分类
pipeline = make_pipeline(StandardScaler(),
RandomForestClassifier(n_estimators=100, random_state=42))
# 训练和预测只需一步
pipeline.fit(X_train, y_train)
pipeline_score = pipeline.score(X_test, y_test)


示例3:使用交叉验证与网格搜索
from sklearn.model_selection import cross_val_score, GridSearchCV
# 交叉验证评估模型
cv_scores = cross_val_score(knn, X_train_scaled, y_train, cv=5)
print(f"交叉验证平均得分:{cv_scores.mean():.2f}")
# 网格搜索寻找最佳参数
param_grid = {'n_neighbors': [3, 5, 7, 9]}
grid_search = GridSearchCV(KNeighborsClassifier(), param_grid, cv=5)
grid_search.fit(X_train_scaled, y_train)
print(f"最佳参数:{grid_search.best_params_}")
print(f"最佳得分:{grid_search.best_score_:.2f}")
第五部分:学习建议与总结 📝
本节课中,我们一起学习了机器学习的基本流程和scikit-learn库的核心用法。
核心总结:
- 流程是关键:机器学习项目遵循“数据划分 → 预处理 → 模型训练/评估 → 调参优化”的基本流程。
- sklearn是利器:
sklearn提供了标准化、模块化的工具,能高效完成上述所有步骤。掌握fit/transform/predict等核心接口至关重要。 - 原理与实战结合:理解算法原理能帮助我们更好地调参和解决问题,而动手实践则能加深对原理的理解并积累经验。
给初学者的建议:
- 从案例入手:建议从
sklearn官网的examples或经典的案例(如泰坦尼克号生存预测)开始实践,先跑通代码,再深入理解。 - 善用文档:
sklearn官方API文档是最好、最准确的学习资料,遇到问题首先查阅文档。 - 动手练习:机器学习是实践性很强的学科,一定要多写代码,尝试修改参数,观察结果变化。


通过本课程的学习,希望大家能够建立起使用sklearn进行机器学习建模的完整知识框架,并能够将其应用到实际问题中。


🧠 机器学习实战教程 第11课:数据分析与特征工程串讲
在本节课中,我们将系统性地学习数据分析与特征工程的核心流程与实践方法。课程将从一个具体的租房热度预测案例出发,讲解如何从原始数据出发,通过数据理解、特征构建与模型训练,最终完成一个机器学习项目。
🎯 第一部分:问题识别与建模
在开始数据分析之前,我们首先需要对问题进行抽象和建模。这决定了我们最终的解题思路和方法。
机器学习任务通常根据场景进行划分,例如分类、回归、排序或无监督学习。我们可以根据问题的类型(有无标签、标签类型)来定义任务。
- 有监督学习:数据包含标签(label)。根据标签类型可进一步划分:
- 分类问题 (Classification):标签是离散的类别。例如,预测用户是否违约。
- 回归问题 (Regression):标签是连续的数值。例如,预测房屋价格。
- 排序问题 (Ranking):标签是次序关系。
- 无监督学习 (Unsupervised Learning):数据没有标签。例如,聚类(k-means)、降维(PCA)。
此外,数据的类型也决定了适用的算法:
- 结构化数据:表格型数据,适合使用树模型(如 XGBoost, LightGBM)或进行人工特征工程。
- 非结构化数据:如图像、文本、语音,通常更适合深度学习模型。
建模流程并非线性,而是一个需要反复迭代的过程。我们可能在数据理解、预处理、建模、评估等任何环节发现问题并返回调整,直到模型性能达到要求。
在实际项目中,数据处理和特征工程往往占据70%以上的时间,而模型训练和验证可能只占10%。因此,深入理解数据和构建有效特征是至关重要的。
📊 第二部分:数据处理与分析实战
本节我们将以 Kaggle 竞赛数据集 Two Sigma Connect: Rental Listing Inquiries(租房热度预测)为例,进行实际操作。
问题背景与数据理解
该任务是根据房屋信息预测其受欢迎程度(热度),标签分为三类:high,medium,low。这是一个多分类问题,评估指标为 Log Loss。
以下是数据集的主要字段及其含义:
| 字段名 | 类型 | 含义 |
|---|---|---|
bathrooms |
数值 | 卫生间数量 |
bedrooms |
数值 | 卧室数量 |
building_id |
类别 | 建筑物ID |
created |
日期 | 信息发布时间 |
description |
文本 | 房屋描述 |
features |
列表 | 房屋特点标签(如“有电梯”) |
latitude / longitude |
数值 | 经纬度 |
price |
数值 | 价格 |
interest_level |
类别 | 热度标签(目标变量) |
单变量分析
对于单个字段,我们可以通过统计和可视化来理解其分布规律。
对于数值型字段(如 price, bathrooms):
- 统计描述:使用
pandas.DataFrame.describe()计算均值、标准差、分位数等。 - 可视化:绘制直方图 (Histogram) 或 核密度估计图 (KDE) 观察分布。例如,价格数据通常呈现左偏分布,可以通过取对数 (
np.log) 来使其更接近正态分布。 - 箱线图 (Boxplot):用于识别离群点 (Outliers)。其上下界计算公式为:
IQR = Q3 - Q1- 上界 =
Q3 + 1.5 * IQR - 下界 =
Q1 - 1.5 * IQR
对于类别型字段(如 building_id):
- 统计描述:使用
pandas.Series.value_counts()统计各类别出现频次。 - 可视化:当类别数不多时,可使用柱状图 (Bar Chart)。
对于文本/日期型字段:
- 日期字段:可提取年、月、日、小时、是否周末等特征,并绘制时间序列图观察趋势。
- 文本字段:可统计词频,或使用词云 (Word Cloud) 直观展示高频词汇。
通过以上分析,我们可以对数据有初步理解,例如:哪些字段存在缺失值?分布是否异常?哪些字段可能与目标变量相关?

🔧 第三部分:特征工程原理与实践
特征工程是将原始数据转换为更能代表问题本质的特征的过程,是提升模型性能的关键。
1. 类别特征编码
类别特征(字符串)必须转换为数值才能被模型处理。
独热编码 (One-Hot Encoding)
- 方法:为每个类别创建一个新的二进制列。
- 公式:若类别有
K种取值,则转换为K维向量,对应类别位置为1,其余为0。 - 优点:不引入次序关系,适用于无序类别。
- 缺点:当类别数很多时,会导致特征维度爆炸,数据稀疏。
- 代码:
pd.get_dummies(df[‘col‘]) # 或 from sklearn.preprocessing import OneHotEncoder
标签编码 (Label Encoding)
- 方法:为每个类别分配一个唯一的整数ID。
- 优点:不增加维度。
- 缺点:会引入人为的次序关系,可能误导模型。更适合树模型。
- 代码:
df[‘col‘].astype(‘category‘).cat.codes # 或 from sklearn.preprocessing import LabelEncoder
频数编码 (Count / Frequency Encoding)
- 方法:用该类别在训练集中出现的次数或频率来替代类别本身。
- 优点:简单,包含了类别流行度信息。
- 缺点:不同类别可能有相同频数,导致信息混淆。需注意训练集和测试集分布一致。
目标编码 (Target Encoding)
- 方法:用该类别下目标变量的均值来替代类别。例如,
building_id为 “A” 的所有房子,其热度为high的平均概率是0.7,则用0.7替代 “A”。 - 优点:编码值含有与目标变量的直接关联信息,非常强大。
- 缺点:极易导致标签泄露 (Data Leakage) 和过拟合。
- 改进:使用交叉验证进行目标编码,防止泄露。即对第
i折的验证集编码时,仅使用其他折的训练集数据来计算目标均值。
- 方法:用该类别下目标变量的均值来替代类别。例如,
2. 数值特征处理
- 缩放与归一化:将特征值映射到特定区间,有助于模型收敛。
- 最大最小值归一化 (MinMaxScaler):
X_scaled = (X - X.min) / (X.max - X.min) - 标准化 (StandardScaler):
X_scaled = (X - X.mean) / X.std,适用于近似高斯分布的数据。
- 最大最小值归一化 (MinMaxScaler):
- 离散化 (Binning):将连续值分段,转化为有序的类别。例如,将年龄划分为
[0-18, 19-35, 36-60, 60+]。 - 交叉特征:通过已有特征进行加减乘除运算,生成新特征。
- 同类型特征:可做加减。如
total_rooms = bedrooms + bathrooms。 - 不同类型特征:可做乘除。如
price_per_bedroom = price / bedrooms。
- 同类型特征:可做加减。如
3. 时间、文本特征构建
- 时间特征:从日期字段中提取丰富特征,如年份、月份、星期几、是否节假日、距某个日期的天数等。
- 文本特征:常用方法包括词袋模型 (Bag-of-Words) 和 TF-IDF,将文本转换为数值向量。
4. 特征选择与重要性
训练模型后,可以评估特征的重要性。
- 树模型:可通过
model.feature_importances_属性获取基于信息增益或基尼不纯度的特征重要性排序。 - 作用:帮助理解数据,并可用于特征筛选,移除不重要特征以简化模型。
🤖 第四部分:模型训练、验证与集成
数据划分与交叉验证
将数据划分为训练集 (Train Set)、验证集 (Validation Set) 和测试集 (Test Set)。
- 训练集:用于训练模型参数。
- 验证集:用于调整超参数、选择模型,监控训练过程以防过拟合。
- 测试集:用于最终评估模型泛化能力,在最终模型确定前不应使用。
K折交叉验证 (K-Fold CV) 是一种更鲁棒的验证方法。它将训练集均分为K份,每次用其中K-1份训练,剩余1份验证,循环K次,最终取K次验证结果的平均值。这能更稳定地评估模型性能。
过拟合与欠拟合
- 欠拟合:模型在训练集和验证集上表现都差。解决方法:增加模型复杂度、增加特征。
- 过拟合:模型在训练集上表现好,在验证集上表现差。解决方法:获取更多数据、降低模型复杂度、正则化、早停 (Early Stopping)。
早停法:在训练迭代过程中(如深度学习或梯度提升树),监控验证集误差。当验证集误差不再下降反而开始上升时,停止训练,从而避免过拟合。
模型集成:Stacking
Stacking 是一种高级集成技术,将多个基模型(如随机森林、XGBoost)的预测结果作为新特征,输入到一个次级模型(元模型)中进行再次训练。
关键步骤:
- 将训练集分为K折。
- 对于每个基模型,进行K折交叉验证:用K-1折训练模型,预测剩余1折(得到对部分训练集的预测),同时预测整个测试集。
- 将所有基模型对训练集的K折预测结果拼接起来,形成新的训练特征。
- 将所有基模型对测试集的预测结果取平均,形成新的测试特征。
- 用新的训练特征和原始标签训练次级模型(如线性回归),并用其预测新的测试特征,得到最终结果。
这种方法能融合不同模型的优势,通常能提升预测精度。
📝 总结与回顾
本节课我们一起学习了机器学习项目中的两个核心环节:数据分析与特征工程。
- 问题建模:我们首先学习了如何根据标签和数据形式定义机器学习任务(分类、回归等),并理解了结构化与非结构化数据的差异。
- 数据分析:我们以租房数据集为例,演示了如何通过统计描述和可视化(直方图、箱线图、词云等)来理解数据分布、发现异常值、探索特征与目标的关系。
- 特征工程:我们系统介绍了各类特征的处理方法:
- 类别特征:独热编码、标签编码、频数编码、目标编码。
- 数值特征:缩放、离散化、构造交叉特征。
- 时间/文本特征:提取关键信息并向量化。
- 模型实践:我们介绍了数据划分、交叉验证、过拟合应对策略,并简要说明了通过特征重要性进行特征选择,以及使用Stacking集成模型来提升性能。


记住,没有放之四海而皆准的特征工程方法。最佳实践依赖于具体的数据、问题和模型。核心能力在于理解每种方法的原理与适用场景,并在实践中灵活运用和迭代优化。


【七月在线】机器学习就业训练营16期 - P12:图像与文本基础教程

在本节课中,我们将要学习机器学习中两个核心的非结构化数据类型:文本和图像的基础处理方法。课程将分为数据类型介绍、文本数据处理基础、图像数据处理基础以及实践案例四个部分。
第一部分:数据类型介绍
上一节我们介绍了算法工程师的不同方向。本节中,我们来看看数据类型的划分,这是选择合适方法的第一步。
在机器学习任务中,数据通常分为结构化数据和非结构化数据。
- 结构化数据:指表格类型的数据,可以用
pandas库方便地读取和处理。 - 非结构化数据:指不适合用表格表示的数据,主要包括文本、图像和视频。
视频可以看作是图像帧与音频的组合。理解数据类型至关重要,因为即使是相同的任务(如分类),处理结构化数据(如房价预测)和非结构化数据(如文本分类)的方法也截然不同。
第二部分:文本数据处理基础
上一节我们区分了数据类型,本节中我们来看看如何处理文本这类非结构化数据。
文本处理属于自然语言处理(Natural Language Processing, NLP)领域,目标是让机器理解或生成人类语言。NLP任务主要分为两类:
- 自然语言理解(NLU):理解给定文本的核心含义。公式可表示为:
输入文本 -> 输出类别/标签。 - 自然语言生成(NLG):根据输入文本或含义生成新的文本。
NLP非常具有挑战性,因为语言是开放、复杂且依赖上下文的。
以下是NLP中常见的任务类型:
- NLU任务:垃圾邮件识别、情感分析、意图识别(本质是文本分类)、聊天机器人、智能客服、语音识别。
- NLG任务:机器翻译、文本摘要。
处理文本时,不同语言(如中文和英文)有显著区别。中文需要分词,而英文通常以空格分隔单词。这个过程称为 tokenization。
文本特征提取:从词到向量
机器学习的模型通常需要数值输入,因此我们需要将文本转换为数值向量。以下是两种经典方法:
1. 词频向量与N-Gram
CountVectorizer 是一种将文本转换为词频向量的方法。它首先构建一个词汇表,然后将每个句子转换为一个向量,向量的每个位置对应一个单词在该句子中出现的次数。
from sklearn.feature_exture_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)
N-Gram 是一种语言模型,它考虑连续的N个词项(token)作为特征。例如,bigram (N=2) 会考虑“this is”、“is good”这样的组合,能更好地保留词序信息。在 CountVectorizer 中,可以通过 ngram_range 参数设置。
2. TF-IDF向量
TF-IDF 衡量一个词在文档中的重要程度,由两部分组成:
- 词频(TF):词在当前文档中出现的频率。
- 逆文档频率(IDF):词在所有文档中出现的普遍性的倒数。公式为:
IDF(t) = log(总文档数 / 包含词t的文档数)。
TF-IDF值由 TF * IDF 计算得出。一个词的TF高且IDF高(即该词在当前文档常见但在整个语料库中罕见),则其TF-IDF值高,被认为更具代表性。
from sklearn.feature_exture_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)
TfidfVectorizer 一步到位,而 TfidfTransformer 则用于对已有的词频矩阵进行转换。
中文文本处理注意:使用上述方法前,需先对中文进行分词并用空格连接,才能进行有效的特征提取。
文本分类实践
我们以一个真假新闻文本二分类任务为例,展示完整流程:
- 文本预处理:包括统一为小写、去除噪声(如URL)、分词、去除停用词、词形还原等。
- 特征提取:使用
CountVectorizer或TfidfVectorizer将文本转换为特征向量。 - 模型训练与评估:使用逻辑回归、朴素贝叶斯等分类器进行训练,并用交叉验证评估。
实践发现,在此任务中:
- 逻辑回归和朴素贝叶斯模型表现较好。
- 使用
CountVectorizer的特征有时比TfidfVectorizer效果更好,说明IDF项并非总是有效。 - XGBoost等树模型在此类数值特征上表现可能不如线性模型,这体现了模型对数据类型的偏好。
进阶:更现代的方法是使用词嵌入(Word Embedding)将单词表示为稠密向量,这将在后续课程中深入讲解。
第三部分:图像数据处理基础
上一节我们学习了文本处理,本节中我们来看看图像这种视觉数据如何处理。
计算机视觉(Computer Vision, CV)的目标是让机器理解和处理图像/视频。图像在计算机中本质是一个矩阵(或张量),例如一张彩色图像可表示为 高度(H) × 宽度(W) × 通道数(C) 的矩阵,其中通道通常为3(RGB)。
计算机视觉任务与深度学习
CV的核心任务包括:
- 图像分类:识别图像主体类别。
- 目标检测:识别图中多个目标并定位。
- 语义分割:对每个像素进行分类。
深度学习,特别是卷积神经网络(CNN),在CV任务中取得了巨大成功。CNN通过卷积层、池化层等结构,能够自动从浅到深地提取图像的边缘、局部特征乃至全局语义特征。
图像特征提取方法
除了使用深度学习模型端到端地学习,我们也可以手动提取图像特征用于机器学习模型。以下是几种方法:
1. 图像哈希
将图片缩放至固定大小(如8x8),灰度化后根据像素值与均值的比较生成二进制序列,再转换为哈希字符串。相似图片的哈希值也相似。适用于图片去重。
2. 颜色直方图
统计图像中各颜色强度区间的像素数量。它描述了图像的全局颜色分布,适用于颜色相似的图片匹配。距离计算可使用欧氏距离。
3. 关键点检测(如SIFT)
检测图像中梯度变化明显的角点、边缘等局部特征点,并为每个关键点生成一个描述子向量(如128维)。SIFT特征具有尺度、旋转不变性。适用于局部相似性匹配和版权检测。
4. 深度学习特征
使用预训练好的CNN模型(如ResNet),移除最后的分类层,将图像前向传播,提取中间层的输出作为特征向量。这是一种强大的全局特征表示方法,计算特征间的相似度(如余弦相似度)即可衡量图像相似性。
不同特征各有适用场景:哈希用于去重,颜色直方图用于颜色匹配,SIFT用于局部匹配,CNN特征用于通用语义匹配。
图像分类实践
我们以手写数字识别(MNIST数据集)为例,展示两种方法:
1. 使用深度学习框架(Keras)搭建CNN
# 示例代码结构
model = Sequential([
Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
MaxPooling2D((2,2)),
Conv2D(64, (3,3), activation='relu'),
MaxPooling2D((2,2)),
Flatten(),
Dropout(0.5),
Dense(10, activation='softmax')
])
model.compile(...)
model.fit(...)
CNN通过卷积和池化层逐步提取特征,最终实现高精度分类。
2. 使用机器学习库(Scikit-learn)
可以将图像像素矩阵展平为一维向量,作为特征输入到MLPClassifier(全连接网络)或线性分类器中进行训练。但通常效果不如专门设计的CNN。
第四部分:总结与核心要点
本节课中我们一起学习了图像与文本处理的基础知识。
我们来总结一下核心要点:
- 数据驱动方法选择:遇到问题时,首先分析数据类型(结构化/非结构化),这决定了后续的技术选型。不存在适用于所有问题的“银弹”。
- 模型与数据的匹配:不同数据需要不同的建模方法。例如,文本的TF-IDF特征更适合线性模型,而图像分类任务中CNN表现卓越。算法工程师的核心能力之一就是为特定数据匹配合适的模型。
- NLP与CV是重要方向:两者都是机器学习的热门应用领域,拥有各自丰富的任务体系和技术栈。
- 基础理论相通:尽管处理的数据和模型不同,但机器学习的基础是相通的,例如训练集/验证集划分、过拟合判断、评估指标等,这些概念在处理结构化数据时同样适用。

对于初学者,建议从 文本分类 和 图像分类 这两个基础任务入手,掌握 Scikit-learn、Keras/PyTorch 等库的基本使用,并深入理解特征提取的核心思想。通过实践积累,逐步拓展到更复杂的任务中去。


注:本教程根据提供的直播内容整理,保留了原话的核心含义,并按照要求进行了结构化、简化和格式化处理。

【七月在线】机器学习就业训练营16期 - P13:基于SQL的机器学习流程和实践 📊

概述
在本节课中,我们将学习如何在大数据环境下,使用SQL和Spark进行机器学习。课程将涵盖SQL基础、Spark框架介绍、Spark SQL的使用以及基于Spark的机器学习实践案例。我们将通过对比Pandas与SQL/Spark的操作,帮助初学者理解不同工具的应用场景和语法差异。
第一部分:SQL与大数据开发 🗃️
在之前的课程中,我们主要讲解了Python环境下的机器学习流程。然而,SQL在大数据处理中同样扮演着至关重要的角色。本节我们将介绍SQL的基础知识及其在大数据开发岗位中的应用。
SQL是用于访问和操作数据库的标准计算机语言。它可以完成数据的插入、查询、更新、删除以及所有与数据库相关的控制操作。常见的数据库软件如MySQL、SQL Server、Oracle等都使用SQL进行交互。
SQL可以视为计算机领域的基础技能,是大多数计算机专业学生的必修课,也是大数据开发的必备技能。下图展示了编程语言如何通过SQL与数据库进行交互:



HTTP协议发送具体的SQL语句,通过SQL软件与底层数据库进行交互。SQL语言独立于具体的编程语言,但与数据库系统紧密相关。


对于转行的同学,需要了解数据库是用于存储、管理和查询数据的关系系统。常见的关系型数据库有MySQL、SQL Server,此外还有非关系型数据库。数据库能够保证数据稳定存储,支持多用户并发操作,并具备事务回滚等特性。
在之前的课程中,我们使用了Pandas进行数据处理。Pandas的本质操作与SQL有许多相似之处,只是实现方式不同。例如,两者都能完成数据筛选、展示、删除、分组聚合以及多表合并等操作。
有同学可能只知道SQL的增删改查,但不清楚其在实际工作中的应用。这里举一个电商的例子:用户登录时,系统会校验用户名和密码,这些信息通常加密后存储在数据库中,通过SQL查询进行匹配。此外,统计用户年度总消费金额(如支付宝年度账单)也依赖于SQL查询,例如:
SELECT SUM(amount) FROM orders WHERE year = 2020;
在实际工作中,程序员(有时被称为“SQL Boy”)的核心任务之一就是将产品经理的业务需求,转化为具体的SQL查询逻辑。
Pandas与SQL语法对比

以下是Pandas操作与SQL语句的对比示例,帮助大家理解两者的异同。

选择指定列并限制行数:
- SQL语法:
SELECT total_bill, tip, smoker, time FROM tips LIMIT 5; - Pandas语法:
tips[['total_bill', 'tip', 'smoker', 'time']].head(5)
添加条件筛选:
- SQL语法:
SELECT * FROM tips WHERE time = 'Dinner' LIMIT 5; - Pandas语法:
tips[tips['time'] == 'Dinner'].head(5)
多条件筛选:
- SQL语法:
SELECT * FROM tips WHERE time = 'Dinner' AND tip > 5;
分组聚合:
- SQL语法:
SELECT sex, COUNT(*) FROM tips GROUP BY sex; - 说明:
SELECT sex表示选择分组键,COUNT(*)表示统计每个分组的行数。
多列分组聚合:
- SQL语法:
SELECT smoker, day, COUNT(*), AVG(tip) FROM tips GROUP BY smoker, day;
表连接(内连接):
- SQL语法:
SELECT * FROM df1 INNER JOIN df2 ON df1.key = df2.key;
通过对比可以发现,Pandas通常用一个函数实现一个操作,而SQL则用一条语句完成。两者的核心逻辑是相通的。

为何要学习SQL和Spark?
上一节我们对比了Pandas和SQL的语法,本节我们来看看学习这些工具的必要性。

- 基础与面试要求: SQL是基础技能,在算法、数据开发等岗位的面试中经常被考察。
- 大公司技术栈: 大型互联网公司(如阿里)的算法工程师往往在大数据平台(而非单机Python环境)上进行开发,主要使用SQL或Spark等工具处理海量数据。
- 薪资与发展: 大数据开发方向的薪资通常高于普通软件开发,且技能更具竞争力。
大数据开发岗位主要分为三类:
- 大数据开发平台与管理: 如数据仓库工程师、数据运维。
- 大数据平台数据分析、挖掘与机器学习: 即数据开发工程师或算法工程师,这也是本课程关注的重点。
- 大数据分析结果展示: 如数据分析师。




对于希望进入大型公司从事算法或数据开发的同学,掌握SQL和Spark技能是非常必要的。



第二部分:Spark介绍与使用 ⚡
在了解了SQL的重要性后,我们引入一个强大的大数据处理框架——Spark。本节将介绍Spark的基本概念、优势及其核心组件。


Spark是由加州大学伯克利分校开发的基于内存的并行计算框架,是目前流行的大数据计算框架之一。
Spark的主要优点包括:
- 运行速度快: 基于内存计算,速度比Hadoop MapReduce快10倍以上。
- 易用性好: 支持Scala、Java、Python、R等多种语言。
- 生态完善: 支持Spark SQL(交互式查询)、Spark Streaming(流计算)、MLlib(机器学习库)、GraphX(图计算库)等。
- 数据源丰富: 支持从CSV、JSON、MySQL、Hive等多种数据源读取数据。
Spark与Hadoop对比:
- Spark将中间数据放在内存中,效率更高。
- Spark通过RDD概念提供了更好的容错性。
- Spark提供了更丰富的操作算子(如map、filter、reduceByKey等),而Hadoop需要编写复杂的MapReduce程序。
Spark适用场景:
- 复杂的批处理: 处理时间跨度长(数十分钟到数小时)的离线任务,常用于数据报表生成。
- 基于历史数据的交互式查询: 响应时间在十秒到数十分钟之间,类似交互式分析。
- 流式数据处理: 处理源源不断的数据流,响应时间在几百毫秒到数秒之间,适用于实时推荐等场景。第三代框架Flink也专注于流处理。
Spark核心概念:
从Spark 2.0开始,主要使用SparkSession作为统一的入口,它整合了SparkContext、SQLContext、HiveContext等,用于操作RDD、SQL、Streaming等。
创建SparkSession的示例代码如下:
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("MyApp") \
.config("spark.some.config.option", "some-value") \
.getOrCreate()
第三部分:Spark核心概念与操作 🛠️
上一节我们介绍了Spark框架,本节我们将深入其两个核心概念:RDD和DataFrame,并通过代码演示其基本操作。
RDD(弹性分布式数据集)
RDD是Spark最基础的数据抽象,代表一个不可变、可分区的元素集合,可以在集群中并行操作。
RDD的特点:
- 基于内存: 数据可以缓存在内存中,加快迭代计算。
- 容错性: 通过RDD的血缘关系(Lineage)记录转换过程,丢失数据时可自动恢复。
- 惰性计算: RDD的转换操作(如map、filter)是惰性的,只有遇到行动操作(如collect、count)时才会真正执行。这允许Spark进行整体优化。


创建RDD与基础操作:
# 通过并行化集合创建RDD
data = [1, 2, 3, 4, 5]
rdd = spark.sparkContext.parallelize(data, numSlices=4)

# 转换操作:每个元素乘以2(惰性执行)
rdd_double = rdd.map(lambda x: x * 2)
# 行动操作:触发计算并收集结果
print(rdd_double.collect()) # 输出: [2, 4, 6, 8, 10]


# 过滤操作
rdd_filtered = rdd.filter(lambda x: x > 3)
print(rdd_filtered.collect()) # 输出: [4, 5]


# 统计操作
print(rdd.count()) # 计数: 5
print(rdd.mean()) # 平均值: 3.0
WordCount示例(经典案例):
text_rdd = spark.sparkContext.parallelize(["hello spark", "hello world", "hello world"])
word_counts = text_rdd.flatMap(lambda line: line.split(" ")) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda a, b: a + b)
print(word_counts.collect()) # 输出: [('hello', 3), ('spark', 1), ('world', 2)]
DataFrame
DataFrame是以RDD为基础构建的分布式数据表,具有明确的列名和数据类型(Schema),提供了更高级的数据操作接口,类似于Pandas DataFrame或数据库中的表。
创建DataFrame:
# 从JSON文件创建
df = spark.read.json("path/to/people.json")
df.show()
# 输出示例:
# +----+-------+
# | age| name|
# +----+-------+
# |null|Michael|
# | 30| Andy|
# | 19| Justin|
# +----+-------+
# 查看Schema
df.printSchema()
# 输出:
# root
# |-- age: long (nullable = true)
# |-- name: string (nullable = true)
DataFrame基本操作:
# 选择列
df.select("name").show()
df.select(df['name'], df['age'] + 1).show()
# 条件筛选
df.filter(df['age'] > 20).show()
# 分组聚合
df.groupBy("age").count().show()
# 排序
df.sort(df['age'].desc()).show()
RDD与DataFrame互转:
- 反射推断: 通过样例类(Scala)或Row对象(Python)推断Schema。
from pyspark.sql import Row rdd = spark.sparkContext.textFile("people.txt") row_rdd = rdd.map(lambda line: line.split(",")).map(lambda p: Row(name=p[0], age=int(p[1]))) df = spark.createDataFrame(row_rdd) - 编程指定: 明确定义Schema结构。
from pyspark.sql.types import StructType, StructField, StringType, IntegerType schema = StructType([ StructField("name", StringType(), True), StructField("age", IntegerType(), True) ]) df = spark.createDataFrame(row_rdd, schema)
第四部分:Spark SQL与机器学习案例 🤖
掌握了Spark的核心操作后,本节我们将学习如何使用Spark SQL进行数据查询,并完成两个完整的机器学习案例。
Spark SQL
Spark SQL允许用户使用SQL语句来查询DataFrame中的数据,实现了SQL与Spark程序的无缝集成。
使用示例:
# 将DataFrame注册为临时视图
df.createOrReplaceTempView("people")
# 执行SQL查询
sql_df = spark.sql("SELECT name, age FROM people WHERE age > 20")
sql_df.show()
Spark SQL的语法与标准SQL高度相似,支持SELECT、WHERE、GROUP BY、JOIN、ORDER BY等几乎所有常用子句。
机器学习案例一:垃圾邮件分类(文本分类)
本案例演示如何使用Spark MLlib构建一个文本分类管道,识别垃圾邮件。
流程概述:
- 数据读取与预览: 读取包含邮件文本和标签的数据。
- 数据预处理: 包括分词、去除停用词、特征提取(TF-IDF)。
- 标签编码: 将字符串标签转换为数值。
- 构建机器学习管道: 将预处理步骤和模型训练封装。
- 模型训练与评估: 使用朴素贝叶斯算法进行分类。
核心代码片段:
from pyspark.ml import Pipeline
from pyspark.ml.feature import Tokenizer, StopWordsRemover, HashingTF, IDF, StringIndexer
from pyspark.ml.classification import NaiveBayes
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
# 1. 读取数据
df = spark.read.csv("spam_data.csv", inferSchema=True, sep='\t')
df = df.withColumnRenamed("_c0", "label").withColumnRenamed("_c1", "text")


# 2. 定义管道阶段
tokenizer = Tokenizer(inputCol="text", outputCol="words")
remover = StopWordsRemover(inputCol="words", outputCol="filtered_words")
hashing_tf = HashingTF(inputCol="filtered_words", outputCol="raw_features")
idf = IDF(inputCol="raw_features", outputCol="features")
label_indexer = StringIndexer(inputCol="label", outputCol="indexed_label")
# 3. 定义模型
nb = NaiveBayes(labelCol="indexed_label", featuresCol="features")

# 4. 构建管道
pipeline = Pipeline(stages=[label_indexer, tokenizer, remover, hashing_tf, idf, nb])
# 5. 拆分数据集并训练
train_df, test_df = df.randomSplit([0.7, 0.3])
model = pipeline.fit(train_df)

# 6. 预测与评估
predictions = model.transform(test_df)
evaluator = MulticlassClassificationEvaluator(labelCol="indexed_label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print(f"测试集准确率: {accuracy}")




管道优势:
使用Pipeline可以将数据预处理和模型训练等多个步骤封装成一个工作流,代码更简洁,且便于Spark进行整体优化。


机器学习案例二:泰坦尼克号生存预测


本案例使用Spark处理结构化数据,并训练随机森林模型预测乘客生存情况。


流程概述:
- 数据读取与清洗: 处理缺失值,进行特征选择。
- 特征工程: 对类别型特征(如性别)进行标签编码或独热编码。
- 数据集划分。
- 模型训练与评估: 使用随机森林算法。


核心代码片段:
from pyspark.ml.feature import VectorAssembler, StringIndexer
from pyspark.ml.classification import RandomForestClassifier
# 1. 读取数据,选择特征列
df = spark.read.csv("titanic.csv", header=True, inferSchema=True)
selected_df = df.select("Survived", "Pclass", "Sex", "Age", "Fare").na.drop()
# 2. 特征工程:将性别字符串转换为数值
indexer = StringIndexer(inputCol="Sex", outputCol="SexIndex")
df_indexed = indexer.fit(selected_df).transform(selected_df)
# 3. 将特征列组合成特征向量
assembler = VectorAssembler(
inputCols=["Pclass", "SexIndex", "Age", "Fare"],
outputCol="features"
)
df_assembled = assembler.transform(df_indexed).select("Survived", "features")
# 4. 拆分与训练
train_df, test_df = df_assembled.randomSplit([0.7, 0.3])
rf = RandomForestClassifier(labelCol="Survived", featuresCol="features", numTrees=100)
model = rf.fit(train_df)
# 5. 预测与评估
predictions = model.transform(test_df)
# ... 使用评估器计算准确率、AUC等指标


总结与建议 📝
本节课我们一起学习了基于SQL和Spark的机器学习流程。我们从SQL的基础知识及其在大数据开发中的重要性讲起,对比了Pandas与SQL的操作。接着,我们深入介绍了Spark框架,包括其核心概念RDD和DataFrame,并通过代码演示了基本操作。最后,我们完成了两个使用Spark MLlib进行机器学习的实战案例。
核心要点总结:
- SQL是基础: 无论是数据库管理还是大数据查询(Hive SQL),SQL都是必须掌握的技能。
- 工具语法相通: Pandas、SQL和Spark对数据的操作(筛选、聚合、连接)在逻辑上高度相似,主要区别在于语法形式。
- Spark是重要框架: Spark是一个成熟的大数据处理和机器学习框架,对于从事ETL、数据开发或希望进入大公司从事算法工作的同学,掌握Spark非常有价值。
学习建议:
- 本节课只是一个入门引导,要熟练掌握Spark还需要大量的练习和实践。
- 建议按照课程提供的Notebook和速查表,亲自运行和修改代码,加深理解。
- 关注Spark的官方文档和社区资源,如Spark官方教程,以深入学习RDD优化、性能调优等高级主题。


对于有志于在大数据领域发展的同学,扎实的SQL功底和Spark实践能力将成为你求职和工作中强有力的武器。


机器学习高阶实践教程:可视化、特征工程与案例解析 📊
课程概述
在本节课中,我们将深入学习机器学习的高阶实践内容。我们将重点探讨如何通过深入的数据分析(EDA)来理解数据,掌握多种特征筛选方法,并通过具体的金融风控和文本匹配案例,学习如何将理论知识应用于解决实际问题。课程旨在帮助初学者建立从数据理解到模型构建的完整工作流程。
第一部分:探索性数据分析(EDA)🔍
在之前的课程中,我们已经学习了许多机器学习算法和基础实践。然而,要真正解决一个实际问题,我们还需要对数据有更深入的理解。本节课程将重点展开高级实践中的关键步骤:深入的可视化分析与特征筛选。
探索性数据分析(EDA)是数据挖掘和建模过程中持续进行的重要环节。它是理解数据的最佳形式。在许多互联网公司,算法工程师的大量工作并非直接建模,而是构建和准备数据。数据可能原始、非规整或散布在多个表格中。建模前,我们需要将这些数据汇总、构建成规整的表格,这个过程需要业务知识的指导。
数据分析帮助我们理解数据内部的分布规律,从而辅助建模。在具体实践中,数据清洗、处理和探索性分析占据了算法工程师70%以上的工作时间,是建模中最耗费精力也最能挖掘关键信息的环节。不同的人在相同数据和模型下得到不同精度的关键,就在于对数据集的有效理解和处理。
EDA的核心步骤
以下是进行探索性数据分析的标准流程:
- 读取数据并分析数据质量:检查数据集是否规整(如二维表格),是否存在噪音。
- 深入分析每个变量:从整体表格分析转向对每一列(变量)的深入分析。
- 分析变量关键属性:
- 类型:变量的数据类型会影响编码方式。
- 缺失值:检查是否存在缺失值,这在现实中很常见。
- 异常值:对于数值型变量,可通过箱线图等方法找出;对于类别型变量,可检查出现次数过少的类别。
- 重复值:检查列向量中的取值是否完全唯一。
- 分布:分析变量的整体分布情况(是否均匀等)。
- 分析变量与标签的关系:对于一个包含标签的数据集,需要分析每个变量与目标标签之间是否存在逻辑关系或相关性。
- 得出分析结论:基于以上分析,决定变量是否需要转换、筛选或清洗,以及如何编码。
变量与标签的关系分析
在分析变量与标签的关系时,如果从相关性系数发现它们强相关,可以进一步通过统计或可视化进行深入分析。
例如,假设变量X1是类别型(取值为0,1,2),标签Y是数值型(0-100)。若计算发现两者强相关,可以进行分组统计:按X1取值分组,计算每组下标签Y的均值,并进行可视化。这样可以清晰看到X1的不同取值如何影响Y的分布。
类似地,也可以分析变量与变量之间的关系,例如连续型与连续型变量、离散型与离散型变量之间的关系,以及变量分布的正态性等。
EDA的产出与价值
通过完整的EDA,我们可以得出以下关键结论,指导后续建模:
- 模型选择:根据数据特征选择模型。例如,数据中类别型特征多时,树模型较好;数值型特征多时,线性模型或神经网络可能更合适。
- 数据处理策略:确定如何对数据集进行编码、转换和清洗。
- 新特征构建:基于业务逻辑和数据分析,从原始特征中构建新的特征。例如,在房价预测案例中,可以构建“房屋价格与同小区均价差值”等特征。
- 特征有效性验证:通过可视化或统计方法验证新构建的特征与标签的关系,从而确认特征构建思路的有效性。
核心思想:EDA的本质是挖掘数据字段内在的规律和含义。我们需要对单个变量、变量与标签、以及变量之间的关系进行分析,从而将“不会说话”的数据转化为可指导建模的洞察。
数据可视化方法
进行数据分析时,有多种可视化方法可供选择,不同的图表用于体现不同的信息:
- 折线图:用于展示随时间变化的趋势(X轴为时间)。
- 柱状图:用于比较不同类别的数据(X轴为类别)。
- 其他类型:还包括比较分析、分布分析、构成分析、联系分析等图表。
在Python环境中,常用的可视化工具包括Matplotlib、Seaborn,以及支持交互的高阶库如Plotly、Bokeh等。

第二部分:特征筛选方法 🎯

上一节我们介绍了如何通过EDA理解数据,本节我们来看看如何从理解后的数据中筛选出最有效的特征。特征筛选是机器学习中非常关键的环节,对于初学者可能有一定难度。
特征筛选是指从原始特征集中选择一部分特征参与模型训练。例如,从20个特征中只选择10个。筛选后的特征集可能会影响模型精度,这种影响可能是正向的,也可能是负向的。
特征筛选的必要性
在构建特征时,我们可能会衍生出大量新特征(例如通过分组聚合)。然而,特征数量增加并不总能提升模型精度,有时反而会导致精度下降或训练速度变慢。由于计算资源有限,我们需要用有限的特征进行高效训练。


因此,我们需要:1)在构建特征时控制维度;2)如果已有大量特征,则通过筛选方法选择有效子集。从20个特征中选16个,其组合数量(搜索空间)非常大(C(20,16)),因此特征筛选本身是一个具有挑战性的优化问题。

特征筛选的三大类方法

在机器学习教材中,特征筛选方法通常分为三类:
- 过滤式:基于统计信息进行筛选,例如基于方差、信息熵、基尼指数等。
- 嵌入式:基于模型嵌入进行筛选,利用模型训练后输出的特征重要性进行选择。
- 包裹式:通过迭代方法进行筛选,通过判断添加或删除某个特征后模型精度的变化来决定特征去留。
具体的特征筛选技术
在实践中,我们可以使用多种技术来评估特征重要性并进行筛选:
- 基于方差筛选:移除方差过小的特征,因为它们包含的信息量少。
from sklearn.feature_selection import VarianceThreshold selector = VarianceThreshold(threshold=0.01) # 移除方差小于0.01的特征 X_new = selector.fit_transform(X) - 基于相关性筛选:计算特征与标签的相关性(如皮尔逊系数、互信息),保留相关性强的特征。
- 基于模型系数:对于线性模型,特征的权重(系数)大小可以反映其重要性。
- 排列重要性:通过打乱某一特征的值,观察模型精度下降的程度来评估该特征的重要性。下降越多,特征越重要。
- SHAP重要性:一种基于博弈论的特征重要性计算方法,能更精确地反映每个特征对模型输出的贡献。
实战代码示例:特征重要性分析
在Python的scikit-learn库中,可以方便地进行特征筛选。例如,树模型(如随机森林)自带feature_importances_属性来衡量特征重要性。
from sklearn.ensemble import RandomForestClassifier
import numpy as np
# 假设 X_train, y_train 是训练数据和标签
model = RandomForestClassifier()
model.fit(X_train, y_train)
# 获取特征重要性
importances = model.feature_importances_
indices = np.argsort(importances)[::-1] # 按重要性降序排列
# 打印最重要的特征
print("Feature ranking:")
for f in range(X_train.shape[1]):
print(f"{f + 1}. feature {indices[f]} ({importances[indices[f]]})")
通过上述方法,我们可以筛选出重要性最高的特征,移除重要性低的特征,从而简化模型并可能提升性能。
第三部分:机器学习案例实战流程 🚀
前面两节我们分别学习了数据探索和特征筛选,本节我们将这些知识整合到一个完整的机器学习项目流程中。虽然不同数据的建模流程有所差异,但整体可以概括为以下几个步骤:
- 理解任务与背景:明确具体任务、评分标准和时间限制。
- 数据理解与探索:对数据集进行初步分析和EDA,理解数据是否需要处理。
- 特征工程:对数据进行转换、构建新特征,并进行特征筛选。
- 模型构建:选择模型、训练模型、验证模型并进行超参数调优。
- 预测与提交:在测试集上进行预测并输出结果。
结构化数据挖掘(如表格数据)有许多公开比赛案例可供学习。接下来,我们将通过一个具体的金融风控案例来详细讲解。
第四部分:金融风控案例实战 💳
本节我们将通过一个真实的金融风控比赛案例,演示如何将前述流程应用于结构化数据。案例背景是基于消费金融场景,构建风险控制模型来预测用户贷款后是否会违约。这是一个二分类问题,通常使用AUC值进行评估。
数据特点与挑战
该案例数据具有以下难点:
- 高维度:原始特征有几百维。
- 类型混合:包含类别型和数值型特征,量纲不统一。
- 匿名性:字段含义未知,增加了特征构建和验证的难度。
因此,建模的核心在于通过人工特征工程,从数据中找到有效的特征,剔除无效特征,并将数据转换为最有效的表现形式。


实战步骤分解
第一步:缺失值分析
通过可视化训练集和测试集的缺失值比例,可以发现:
- 训练集和测试集缺失情况分布一致,表明数据同分布,这对建模有利。
- 部分特征(尤其是第三方征信特征)缺失严重(比例接近1),可考虑剔除。

第二步:特征相关性分析
将特征按基础信息、通话信息、第三方征信分组,绘制特征间相关性热力图。可以发现:
- 同类特征内部存在强相关性。
- 不同类特征之间相关性较弱。
- 部分特征几乎完全重复。
第三步:特征清洗与筛选
基于以上分析,进行数据清洗:
- 剔除缺失严重的列:缺失比例大于97%的列。
- 剔除低方差列:信息量小的特征。
- 剔除分布不一致的列:在训练集和测试集上取值分布差异过大的特征。


第四步:特征构建与编码
- 地理位置信息提取:从匿名字段中推断并提取省份、城市等信息,进行
Target Encoding(按省份分组计算违约率均值)。 - 连续特征离散化:通过可视化发现某些连续特征可明显分层,将其转换为离散值。
- 构建交叉特征:通过散点图分析,发现某些特征组合能更好区分违约用户。例如,构建特征A与特征B的比值或差值作为新特征。
第五步:迭代优化与结果
通过以上步骤,模型精度得到显著提升:
- 原始533维特征 → 筛选后300+维特征:AUC从0.75提升至0.847。
- 加入特征预处理和编码:AUC继续提升。
- 加入地理位置和交叉特征:AUC进一步提升。
代码结构建议:
将数据处理步骤封装成函数,使代码模块化、清晰易读。主要函数包括:数据读取、预处理(处理缺失、方差、分布)、特征工程(地理位置编码、交叉特征、Target Encoding)、模型训练与验证。
# 示例:数据预处理函数框架
def preprocess_data(train_df, test_df):
# 1. 合并数据,统一处理
combined = pd.concat([train_df, test_df])
# 2. 剔除高缺失率列
combined = drop_high_missing_cols(combined, threshold=0.97)
# 3. 剔除低方差列
combined = drop_low_variance_cols(combined)
# 4. 剔除分布不一致列
combined = drop_inconsistent_cols(combined, train_len=len(train_df))
# 5. 返回处理后的训练集和测试集
return combined.iloc[:len(train_df)], combined.iloc[len(train_df):]
第五部分:文本匹配案例实战 📝
除了结构化数据,我们也简要介绍一个非结构化的文本案例:Quora重复问题检测。任务是判断两个句子是否表达相同含义。
方法一:传统机器学习方法(特征工程 + 树模型)
- 人工特征提取:
- 统计特征:两个句子的共有单词比例、句子长度、单词个数等。
- 文本特征:是否包含问号、特殊符号、数字、大写字母比例等。
- 相似度特征:计算两个句子的TF-IDF向量余弦相似度。
- 建模:将上述特征作为输入,使用XGBoost等树模型进行分类。
方法二:深度学习方法(词向量 + 神经网络)
- 文本预处理:将句子分词,转换为数值序列,并进行填充/截断到相同长度。
- 词向量表示:使用预训练词向量(如GloVe)将每个单词转换为稠密向量。
- 构建神经网络模型:
- 输入:两个句子的词向量序列。
- 特征提取:通过LSTM/GRU层获取句子表示。
- 交互与匹配:计算两个句子表示的差值、点积等,拼接后输入全连接层。
- 输出:通过Sigmoid层进行二分类。
- 优点:深度学习模型能自动学习更复杂的语义特征,通常在此类任务上获得比传统方法更高的精度。
核心对比:
- 方法一:依赖人工特征工程,可解释性强,计算效率高。
- 方法二:依赖数据驱动,自动学习特征,能捕捉复杂模式,精度通常更高。
课程总结 🎓


本节课我们一起学习了机器学习的高阶实践内容:
- 探索性数据分析:我们深入理解了EDA在建模中的核心地位,学习了如何通过质量检查、单变量分析、变量与标签关系分析来挖掘数据洞察,并指导特征工程和模型选择。
- 特征筛选方法:我们掌握了过滤式、嵌入式、包裹式三大类特征筛选方法,并学习了基于方差、相关性、模型重要性、排列重要性等具体技术来优化特征集。
- 完整项目流程:我们梳理了从任务理解、数据探索、特征工程、模型构建到预测的标准化机器学习流程。
- 实战案例解析:
- 通过金融风控案例,我们演练了如何处理高维、匿名表格数据,包括缺失值分析、相关性分析、特征清洗、构建与编码,并看到了每一步对模型精度的提升。
- 通过文本匹配案例,我们对比了基于特征工程的传统方法和基于深度学习的端到端方法,理解了不同数据形态下的建模策略。


希望各位同学结合课后提供的代码,进一步巩固和实践这些内容。机器学习实践能力的提升离不开对数据的深刻理解和反复的动手实验。

机器学习就业训练营 - 第2课:决策树、随机森林与GBDT 🌲


在本节课中,我们将学习一类在工业界和数据科学竞赛中占据重要地位的模型——基于树的模型。我们将从最基础的决策树开始,了解其工作原理、构建方法以及如何用于分类和回归任务。接着,我们将探讨如何通过集成学习的思想,将多棵决策树组合成更强大的模型——随机森林。这些模型以其直观的逻辑、良好的可解释性和对数据预处理要求较低的特点而广受欢迎。




从逻辑回归到决策树 🤔

上一节我们介绍了逻辑回归模型,它通过数学计算 WX + b 并送入 Sigmoid 函数来得到分类概率。这是一种有效的分类方法。



然而,人类在做决策时,往往更倾向于使用一系列直观的规则,例如“如果年龄大于30岁,就不去相亲”。这种基于“如果-那么”规则的决策过程,就是决策树模型的核心思想。决策树模型将复杂的决策过程转化为一棵树形结构,逻辑清晰且易于理解。






决策树模型详解 🌳

决策树是一种基于树结构进行决策的模型。它的核心组成部分如下:
- 内部节点:代表对某个属性(特征)进行判断的条件。
- 分支:代表该属性判断后的一种可能结果(取值)。
- 叶子节点:代表最终的决策结果(预测值)。



决策树的预测过程非常简单:从根节点开始,根据数据属性的取值,沿着满足条件的分支向下移动,直到到达某个叶子节点,该节点的值即为预测结果。



决策树如何“生长”?🌱

决策树的构建是一个“分而治之”的递归过程,核心在于解决两个问题:
- 如何选择当前最重要的划分属性?
- 生长到何时停止?
停止生长的条件




以下是决策树停止生长的三种常见条件:
- 当前节点包含的样本全属于同一个类别:无需再划分。
- 当前属性集为空,或所有样本在所有属性上取值相同:无法找到新的属性进行划分。
- 当前节点包含的样本集合为空:没有样本可供划分。



选择划分属性的准则
为了找到“最重要”的属性,我们需要一个衡量标准。核心思想是:选择一个属性进行划分后,希望数据集的“不纯度”下降得最多。“纯度”越高,意味着该节点样本的类别越一致。





以下是三种常用的不纯度度量及对应的经典算法:




- 信息熵与信息增益 (ID3算法)
- 信息熵:度量样本集合纯度的指标。熵越大,不确定性越高,纯度越低。
- 公式:
Entropy(D) = -Σ (p_k * log₂(p_k)),其中p_k是第k类样本的比例。
- 公式:
- 信息增益:表示使用某个属性
a进行划分后,信息熵减少的程度。ID3算法选择信息增益最大的属性。- 公式:
Gain(D, a) = Entropy(D) - Σ (|D_v|/|D| * Entropy(D_v)),其中D_v是属性a取值为v的样本子集。
- 公式:
- 信息熵:度量样本集合纯度的指标。熵越大,不确定性越高,纯度越低。



- 信息增益率 (C4.5算法)
- 问题:信息增益准则对可取值数目较多的属性有偏好(例如“学号”)。
- 改进:C4.5算法使用信息增益率,它在信息增益的基础上,除以该属性本身的信息熵(称为“固有值”),以消除属性取值数量带来的影响。
- 公式:
Gain_ratio(D, a) = Gain(D, a) / IV(a),其中IV(a) = -Σ (|D_v|/|D| * log₂(|D_v|/|D|))。
- 公式:




- 基尼指数 (CART算法)
- 基尼指数:另一种度量不纯度的指标。直观理解为:从数据集中随机抽取两个样本,其类别标记不一致的概率。基尼指数越小,纯度越高。
- 公式:
Gini(D) = 1 - Σ (p_k²)。
- 公式:
- CART(分类与回归树)算法使用基尼指数,并且它构建的是二叉树。对于每个属性,它会寻找一个最优切分点,将数据分为“是”和“否”两部分。
- 基尼指数:另一种度量不纯度的指标。直观理解为:从数据集中随机抽取两个样本,其类别标记不一致的概率。基尼指数越小,纯度越高。


核心概念关联:通过数学中的泰勒展开可以证明,信息熵和基尼指数在误差允许范围内是等价的,它们衡量的趋势是一致的。

处理连续值与回归问题 📈

连续值处理
决策树本质处理离散属性。对于连续属性(如“年龄”),处理方法是将其离散化。
- 将训练样本在该属性上的取值排序。
- 取任意两个相邻取值的均值作为“候选划分点”。
- 将每个候选划分点(如“年龄 ≤ 22.5?”)视为一个二值离散属性,然后像考察普通离散属性一样,用上述准则(信息增益、基尼指数等)评估其划分效果,选择最优的划分点。



回归任务

决策树同样可以用于回归任务,此时称为回归树。
- 预测方式:每个叶子节点不再输出类别,而是输出一个具体的数值。通常取该叶子节点内所有样本目标值的平均值。
- 划分准则:回归树在划分时,旨在最小化划分后各区域的平方误差和。
- 公式:对于划分产生的
M个区域R1, R2, ..., Rm,其损失函数常定义为RSS = Σ_m Σ_i (y_i - c_m)²,其中c_m是区域Rm的预测值(均值)。
- 公式:对于划分产生的
- 构建方法:采用启发式的、自顶向下的递归二分法。对于每个属性,遍历所有可能的切分点,选择能使
RSS减少最多的属性和切分点进行划分。
防止过拟合:与分类树类似,回归树也需要控制过拟合。常见方法包括:
- 限制树的最大深度 (
max_depth)。 - 限制叶子节点的最少样本数 (
min_samples_leaf)。 - 直接对损失函数加入正则化项,例如:
Loss = RSS + α * (叶子节点数量),其中α是超参数。
集成学习与随机森林 🌲🌳🌲

单棵决策树容易过拟合且不稳定。集成学习通过结合多个学习器来获得更好的性能。

Bagging 思想


Bagging 的核心是“自助采样法”和“投票/平均”。
- 从原始训练集中有放回地随机抽取
n个样本,作为一个子训练集。 - 用该子训练集训练一个基学习器(如决策树)。
- 重复上述步骤
T次,得到T个基学习器。 - 对于分类任务,
T个学习器进行投票;对于回归任务,对T个学习器的输出取平均。


Bagging通过降低对噪声样本和异常数据的敏感性来提高模型的泛化能力。
随机森林



随机森林 是 Bagging 思想的一个扩展和特化,它以决策树为基学习器,并在 Bagging 的样本随机采样基础上,增加了特征随机采样。
- 在构建每棵决策树时,不仅从训练集中随机采样样本,还会从所有特征中随机选取一个特征子集,然后从这个子集中选择最优划分属性。
- 这种“双重随机性”进一步增强了模型的多样性和泛化能力,有效抑制过拟合。

随机森林通常能产生非常平滑的决策边界,对噪声不敏感,是工业界非常强大的工具。



实战案例 💻


案例1:决策树分类

使用决策树预测居民收入是否超过5万美元。

import pandas as pd
from sklearn.preprocessing import OneHotEncoder
from sklearn.tree import DecisionTreeClassifier, plot_tree




# 1. 加载数据
data = pd.read_csv('adult.csv')
features = data[['workclass', 'education', 'marital-status', ...]] # 特征列
labels = data['income'] # 标签列


# 2. 特征处理(将类别特征转为数值,例如独热编码)
encoder = OneHotEncoder()
features_encoded = encoder.fit_transform(features)
# 3. 构建并训练决策树模型
model = DecisionTreeClassifier(criterion='entropy', max_depth=4)
model.fit(features_encoded, labels)







# 4. 可视化决策树
plot_tree(model, filled=True)





案例2:随机森林回归





使用随机森林预测波士顿房价。







from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor






# 1. 加载数据
boston = load_boston()
X, y = boston.data, boston.target







# 2. 构建并训练随机森林回归模型
# n_estimators: 森林中树的数量
# max_features: 每棵树考虑的最大特征数(可以是整数、浮点数或‘auto’等)
model = RandomForestRegressor(n_estimators=15, max_features=0.6)
model.fit(X, y)





# 3. 进行预测
predictions = model.predict(X[:5])
print(predictions)









总结 📚







本节课我们一起学习了机器学习中非常重要的一类模型——树模型。
- 决策树:我们了解了其基于规则决策的直观思想,掌握了其树形结构(节点、分支、叶子)。深入探讨了决策树生长的核心:如何用信息增益、信息增益率、基尼指数等准则选择划分属性,以及何时停止生长。
- 连续值与回归:我们学习了如何将连续属性离散化以供决策树使用,并了解了回归树如何通过最小化平方误差来构建,用于解决回归问题。
- 集成学习:我们认识了Bagging这种通过结合多个弱学习器来提升性能的集成思想。
- 随机森林:作为Bagging与决策树的完美结合,随机森林通过对样本和特征进行双重随机采样,构建多棵差异化的决策树,并通过投票或平均得到最终结果,极大地提升了模型的稳定性和预测精度。

树模型以其强大的表现力和相对简单的预处理流程,成为解决众多分类与回归问题的利器。理解其原理,是灵活应用和调优的基础。

🧠 机器学习就业训练营 - 课程3:支持向量机(SVM)与数据分类
在本节课中,我们将要学习支持向量机(SVM)的核心原理、数学推导及其在数据分类,特别是文本分类中的应用。SVM是一种强大的监督学习算法,尤其擅长处理高维数据和寻找最优分类边界。
📚 课程概述
本节课的主要内容是讲解SVM模型的基本推导和实战应用。我们将从线性可分的数据集入手,逐步深入到线性不可分的情况,并介绍SVM的求解算法。课程将涵盖以下核心知识点:
- 函数间隔与几何间隔
- 最大间隔分类器
- 拉格朗日乘子法与对偶问题
- SMO求解算法
- 核函数与软间隔分类器
- SVM的损失函数视角与多分类扩展
- 文本分类实战
1️⃣ 函数间隔与几何间隔
上一节我们介绍了课程的整体框架,本节中我们来看看SVM最基础的两个概念:函数间隔和几何间隔。
假设我们有一个线性可分的二维数据集,黑点代表正类(+1),空心点代表负类(-1)。我们可以画出无数条直线(超平面) W·X + b = 0 将两类数据分开。在这些线中,哪一条是最好的呢?
函数间隔 衡量了数据点距离分类超平面的“确信度”。对于一个数据点 (X_i, y_i),其函数间隔定义为:
γ̂_i = y_i (W·X_i + b)
其中 y_i ∈ {+1, -1}。引入 y_i 是为了保证间隔始终为非负值(对于正确分类的点)。
然而,函数间隔有一个问题:如果我们成比例地缩放 W 和 b,函数间隔会随之同比例变化,但这并没有改变超平面本身。因此,函数间隔不能绝对地衡量距离。
为此,我们引入 几何间隔,它表示数据点到超平面的真实欧氏距离。几何间隔在函数间隔的基础上,对法向量 W 进行了归一化:
γ_i = y_i ( (W / ||W||)·X_i + b / ||W|| ) = γ̂_i / ||W||
其中 ||W|| 是 W 的 L2 范数。几何间隔不会随 W 和 b 的缩放而改变,是一个绝对的距离度量。
对于一个超平面,我们定义其 最小几何间隔 为所有样本点几何间隔中的最小值。SVM的核心思想就是:寻找那个能使最小几何间隔最大的超平面,即“最大间隔”分类器。
2️⃣ 最大间隔分类器的形式化
上一节我们定义了衡量标准,本节中我们来看看如何将“寻找最大间隔超平面”转化为一个数学优化问题。
我们的目标是:
max_{W, b} γ
subject to y_i (W·X_i + b) ≥ γ, i = 1,..., m
||W|| = 1
这里 γ 是最小几何间隔。约束条件 ||W|| = 1 使得目标函数中的 γ 就是几何间隔。但这个约束条件(等式)使得问题非凸,难以求解。
我们可以通过两步变换来简化问题:
- 用函数间隔表示几何间隔:令
γ = γ̂ / ||W||,并将约束条件两边同除以||W||,从而将||W|| = 1的约束融入到不等式约束中。 - 固定函数间隔:函数间隔
γ̂的缩放不影响解。为了简化,我们可以令γ̂ = 1。此时,最大化γ = 1 / ||W||等价于最小化||W||。为了后续求导方便,我们最小化(1/2)||W||^2。
经过上述变换,原始的“最大间隔”问题等价于以下 凸二次规划问题:
min_{W, b} (1/2) ||W||^2
subject to y_i (W·X_i + b) ≥ 1, i = 1,..., m
这就是 支持向量机(SVM)的基本型。
3️⃣ 拉格朗日乘子法与对偶问题
上一节我们得到了一个带约束的优化问题,本节中我们引入拉格朗日乘子法来求解它。
对于含有等式约束 h_i(W)=0 的优化问题 min f(W),我们引入拉格朗日函数:
L(W, β) = f(W) + Σ β_i h_i(W)
通过令 ∂L/∂W = 0 和 ∂L/∂β = 0 来求解。
对于SVM,我们有不等式约束 g_i(W) = 1 - y_i(W·X_i + b) ≤ 0。我们需要引入 广义拉格朗日函数:
L(W, b, α) = (1/2)||W||^2 + Σ_{i=1}^m α_i [1 - y_i(W·X_i + b)]
其中 α_i ≥ 0 是拉格朗日乘子。
根据拉格朗日对偶性,原始问题 min_{W,b} max_{α≥0} L(W, b, α) 可以转化为其对偶问题 max_{α≥0} min_{W,b} L(W, b, α),这往往更易求解。当满足 KKT条件 时,对偶问题的最优解即是原始问题的最优解。
KKT条件包括:
∂L/∂W = 0,∂L/∂b = 0α_i ≥ 01 - y_i(W·X_i + b) ≤ 0α_i [1 - y_i(W·X_i + b)] = 0(互补松弛条件)
最后一个条件至关重要:它意味着对于大多数样本,α_i = 0;只有当样本点满足 y_i(W·X_i + b) = 1,即恰好位于间隔边界上时,α_i 才可能大于0。这些点就是 支持向量,它们决定了最终的分类超平面。
4️⃣ SVM对偶问题的求解
上一节我们得到了对偶问题,本节中我们具体求解它,并得到SVM的最终形式。
首先,固定 α,对 L(W, b, α) 关于 W 和 b 求偏导并令其为零:
∂L/∂W = 0 ⇒ W = Σ_{i=1}^m α_i y_i X_i
∂L/∂b = 0 ⇒ Σ_{i=1}^m α_i y_i = 0
将这两个结果代回拉格朗日函数,消去 W 和 b,得到 只关于 α 的对偶问题:
max_α Σ_{i=1}^m α_i - (1/2) Σ_{i=1}^m Σ_{j=1}^m α_i α_j y_i y_j X_i·X_j
subject to Σ_{i=1}^m α_i y_i = 0, α_i ≥ 0, i=1,...,m
求解出 α 后,我们可以得到模型参数:
W* = Σ_{i=1}^m α_i y_i X_i
b* 可以通过任意一个支持向量 (X_s, y_s) 计算:b* = y_s - W*·X_s
最终的决策函数为:
f(X) = sign(W*·X + b*) = sign( Σ_{i=1}^m α_i y_i X_i·X + b* )
重要观察:
- 模型参数
W和b仅由支持向量(α_i > 0的样本)决定。 - 决策函数依赖于支持向量与输入样本的内积
X_i·X。
5️⃣ SMO算法简介
上一节我们得到了对偶问题的形式,本节中我们简要介绍求解该问题的SMO算法。
对偶问题是一个二次规划问题。当样本量很大时,通用的QP求解器效率低下。序列最小最优化(SMO) 算法是一种高效的启发式算法。
SMO算法的基本思想源自 坐标上升法:每次只优化一个变量,固定其他变量。但由于对偶问题中有约束 Σ α_i y_i = 0,单独更新一个 α_i 会破坏约束。
因此,SMO算法每次选择两个变量 α_i 和 α_j 进行优化,固定其他变量。这样,由约束 α_i y_i + α_j y_j = constant,我们可以用 α_i 表示 α_j,从而将问题转化为单变量优化问题,该问题有解析解。迭代重复以下步骤直至收敛:
以下是SMO算法的主要步骤:
- 启发式选择一对需要更新的拉格朗日乘子
α_i和α_j。 - 固定其他乘子,解析求解关于这两个乘子的二次规划子问题,更新
α_i和α_j。 - 根据更新后的
α更新模型参数b和误差缓存。 - 检查是否满足收敛条件(如KKT条件在容忍度内),若不满足则返回步骤1。
6️⃣ 核函数:处理线性不可分数据
上一节我们讨论了线性可分情况,本节中我们来看看SVM如何处理线性不可分的数据。
对于在原始特征空间中线性不可分的数据,我们可以将其映射到一个更高维的特征空间,使其在新空间中线性可分。设映射函数为 φ(X),则决策函数变为:
f(X) = sign( Σ α_i y_i φ(X_i)·φ(X) + b )
直接计算高维空间的内积 φ(X_i)·φ(X) 可能非常困难(维度灾难)。核函数 技巧巧妙地解决了这个问题。我们定义一个核函数 K(X_i, X_j) = φ(X_i)·φ(X_j),它直接在原始空间计算,结果等于映射后空间的内积。这样,我们无需显式地定义映射 φ,也无需计算高维向量。
决策函数可重写为:
f(X) = sign( Σ α_i y_i K(X_i, X) + b )
常用的核函数包括:
- 线性核:
K(X_i, X_j) = X_i·X_j - 多项式核:
K(X_i, X_j) = (X_i·X_j + c)^d - 高斯核(RBF核):
K(X_i, X_j) = exp(-γ ||X_i - X_j||^2) - Sigmoid核:
K(X_i, X_j) = tanh(β X_i·X_j + θ)
7️⃣ 软间隔分类器:容忍噪声与异常点


上一节我们通过升维解决不可分问题,本节中我们介绍另一种更常用的方法——软间隔,它允许一些样本点不满足约束。
即使使用了核函数,数据中也可能存在噪声或特异点,导致严格线性不可分。软间隔SVM 通过引入 松弛变量 ξ_i ≥ 0 来允许一些样本点犯错(位于间隔之内甚至被误分类)。优化目标变为:
min_{W,b,ξ} (1/2)||W||^2 + C Σ_{i=1}^m ξ_i
subject to y_i(W·X_i + b) ≥ 1 - ξ_i, ξ_i ≥ 0, i=1,...,m
其中 C > 0 是一个惩罚参数,控制对误分类的惩罚力度。C 越大,对误分类的容忍度越低,模型越倾向于过拟合;C 越小,则容忍度越高,模型越简单,可能欠拟合。
同样地,可以推导出其拉格朗日对偶形式,与硬间隔SVM的对偶形式非常相似,只是约束条件变为 0 ≤ α_i ≤ C。KKT条件中的互补松弛条件也相应变化。
8️⃣ 合页损失函数视角
上一节我们从优化角度定义了软间隔,本节中我们从损失函数的角度重新理解SVM。

SVM的优化目标 (1/2)||W||^2 + C Σ ξ_i 可以等价地写为经验风险最小化的形式:
min_{W,b} Σ_{i=1}^m [1 - y_i(W·X_i + b)]_+ + λ ||W||^2
其中 [z]_+ = max(0, z) 称为 合页损失(Hinge Loss),λ 是正则化系数。
合页损失函数的特点是:当样本被正确分类且函数间隔大于1时,损失为0;否则,损失线性增长。这与逻辑回归的交叉熵损失、感知机的0-1损失都不同。0-1损失难以优化,合页损失是其一个凸上界,且比交叉熵损失更关注于那些“难分”的样本(即间隔边界附近的样本)。
9️⃣ SVM的多分类扩展
上一节我们讨论的都是二分类SVM,本节中我们简要介绍如何将其扩展到多分类问题。
SVM本质上是二分类器。处理多分类(K类)问题常用以下策略:
- 一对一(One-vs-One):为每两个类别训练一个SVM分类器,共需
K(K-1)/2个分类器。预测时采用投票法。 - 一对多(One-vs-Rest):为每个类别训练一个SVM,将其与其他所有类别区分开,共需
K个分类器。预测时选择决策函数值最大的类别。 - 层次SVM:通过构建一个二叉树结构,将多分类问题分解为一系列二分类问题。
在实际应用中,一对一和一对多策略最为常用,许多SVM库(如LIBSVM)都直接提供了多分类的实现。

🔟 实战:SVM用于文本分类
前面我们完成了SVM的理论学习,本节中我们进行实战,将SVM应用于文本分类任务。
文本分类是SVM的传统优势领域。基本流程如下:

以下是文本分类的主要步骤:
- 分词:对于中文文本,首先需要使用分词工具(如结巴分词)将句子切分成词语序列。
- 特征提取与选择:
- 去除停用词(如“的”、“了”等无实义词)。
- 构建词表,将每个词映射为一个ID。
- 将每篇文档表示为向量,常用 词袋模型(Bag-of-Words) 或 TF-IDF 加权。
- 可以进行特征选择(如卡方检验、信息增益)以降低维度。
- 模型训练与评估:使用SVM库(如Python的
sklearn.svm或专业的LIBSVM)在向量化后的数据上训练模型,并进行交叉验证评估。
实战经验分享:在一个33类的新闻文本分类比赛中,通过以下调优提升了效果:
- 使用更细粒度的分词。
- 将词频特征替换为TF-IDF权重。
- 精心调整SVM的核函数、惩罚参数C等。
- 对不平衡类别采用分层采样或调整类别权重。
- 最终,特征工程的精细程度(如引入n-gram特征)对结果影响巨大。
作业:使用LIBSVM或scikit-learn的SVM模块,完成提供的新闻文本数据集的分类任务,并尝试调整参数以达到最佳性能。
📝 课程总结
本节课中我们一起学习了支持向量机(SVM)的完整知识体系:
- 核心思想:寻找最大几何间隔的分类超平面,以获得强泛化能力。
- 数学推导:从函数间隔、几何间隔出发,形式化为凸二次规划问题,并通过拉格朗日对偶性转化为更易求解的对偶问题。理解 支持向量 和 KKT条件 是关键。
- 求解算法:了解了高效的SMO算法原理。
- 关键扩展:
- 核函数:通过隐式的高维映射处理线性不可分数据,避免了维度灾难。
- 软间隔:引入松弛变量和惩罚参数C,使模型能容忍噪声和异常点,控制模型复杂度。
- 多分类:掌握了一对一和一对多等扩展策略。
- 实战应用:掌握了将SVM应用于文本分类的完整流程,包括文本预处理、特征工程和模型调优。


SVM以其坚实的数学基础和良好的性能,在机器学习发展史上占有重要地位。虽然深度学习在许多领域取得了领先,但理解SVM的原理对于构建完整的机器学习知识体系至关重要。希望本课程能帮助大家打下坚实的基础。
机器学习就业训练营 - 课程4:特征工程实战教程 🛠️
在本节课中,我们将要学习机器学习中一个至关重要但常被忽视的环节——特征工程。我们将探讨如何将原始、杂乱的数据,通过一系列处理步骤,转化为机器学习模型能够有效学习的“语言”。特征工程的质量,往往直接决定了模型效果的上限。
概述:什么是特征工程?
特征工程,英文称为 Feature Engineering。它指的是从原始数据中抽取对预测结果有用的信息,并将其处理成机器学习算法能更好发挥作用的表达形态。
人工智能远没有想象的那么智能,它需要大量“人工”的环节。你不能简单地将原始数据扔给模型就期望得到好结果。原始数据可能杂乱无章,包含许多计算机无法直接理解的信息(如文本、类别)。特征工程的核心,就是结合计算机知识和领域专业知识,将这些信息转化为有效的数值表达。
一个常见的误解是,模型越复杂、越“高级”,效果就越好。实际上,在工业界,我们更青睐简单、可控、可解释性好的模型(如逻辑回归)。通过精心设计的特征工程,简单模型的效果完全可以媲美甚至超越复杂模型。数据科学家大约60%-70%的时间会花在数据处理和特征工程上,而模型调优只占较少部分,这足以说明其重要性。
上一节我们介绍了特征工程的核心概念与重要性,本节中我们来看看面对原始数据时,我们需要进行哪些准备工作。
数据采集与初步考量
在特征工程开始之前,首先需要获取数据。虽然数据采集通常不由机器学习工程师直接完成,但我们需要思考:哪些数据对预测结果有帮助?这些数据能否采集到?
例如,在电商推荐场景中,商品在搜索结果列表中的“位置”是一个强特征。但在构建线上推荐模型时,你无法提前知道一个商品会被排在哪个位置,因此这个特征在训练时可用,在线上实时预测时却不可用。我们必须考虑特征的线上可获取性和计算效率。
以下是在设计特征时可以从几个维度切入思考的例子(以预测用户对课程的兴趣为例):
- 机构维度:课程提供方的口碑、历史表现。
- 讲师维度:讲师风格、声音、受欢迎程度。
- 用户维度:用户历史学习记录、兴趣偏好。
- 课程维度:内容难度、展示形式、开课时间。
- 环境维度:同时参与课程的其他学员情况。
数据格式化后,并非所有采集到的数据都有用。原始数据中可能包含噪声或错误信息。机器学习模型没有区分能力,它会学习你提供给它的所有信息,包括噪声,这就是“垃圾进,垃圾出”。因此,数据清洗是特征工程的第一步,目的是剔除不可靠的数据,提升“原材料”的质量。
数据清洗与采样处理
数据清洗的目标是识别并处理数据中的噪声、错误和不一致。异常点(脏数据)的处理至关重要,有时仅仅清理掉明显的脏数据就能让模型排名大幅提升。
以下是识别和处理脏数据的一些方法:
- 业务逻辑判断:识别明显不符合常理的数据,例如身高超过3米的人,或单月消费额异常高的用户。
- 统计方法:对于连续值,可以使用分位数进行“掐头去尾”。例如,剔除99%分位数以上和1%分位数以下的极端值。
除了清洗,另一项关键处理是针对样本不均衡问题的数据采样。在许多分类问题中(如疾病诊断、电商购买),正负样本数量可能相差悬殊。如果直接用不均衡数据训练,模型可能会偏向多数类。
以下是解决样本不均衡的几种方法:
- 欠采样:从多数类中随机抽取一部分样本,使其数量与少数类接近。
- 过采样:增加少数类样本的数量。除了简单复制,还可以使用如SMOTE算法,通过在特征空间中对少数类样本进行插值来生成新样本。
- 分层采样:在划分训练集/测试集时,保持每个数据子集中不同类别(或重要维度,如性别)的比例与原始数据集一致。
- 修改损失函数:在模型训练时,为不同类别的样本损失赋予不同的权重,降低多数类的影响。
上一节我们处理了数据层面的问题,本节中我们来看看如何针对不同类型的数据进行特征转换和抽取。
特征处理:数值型数据
数值型数据指年龄、价格等连续值。对它们的处理旨在让模型更高效地学习。
幅度缩放
当不同特征的数值范围差异巨大时(如“房间数”范围0-5,“均价”范围0-50000),某些模型(如逻辑回归、神经网络)的学习过程会变得困难。缩放可以将不同特征调整到相近的幅度范围内。
常用方法:
- 标准化:对每一列特征进行处理。
新值 = (原值 - 该列均值) / 该列标准差。此方法使数据符合标准正态分布。 - 最大最小值缩放:
新值 = (原值 - 该列最小值) / (该列最大值 - 该列最小值)。此方法将数据缩放到[0, 1]区间。
统计特征
除了原始值,还可以基于统计信息构造新特征。例如,在房价预测中,可以构造“该地区房价均价”、“该地区房价最高/最低价”、“该地区房价的25%/75%分位数”等特征。
离散化
离散化,也称分箱或分桶,是将连续值划分为多个区间段,并将每个区间作为一个新的类别特征。
为什么需要离散化?
考虑“是否让座”的场景,影响因素“年龄”与结果并非单调关系(需要照顾老人和小孩)。如果直接将年龄作为连续特征送入逻辑回归,模型无法同时捕捉两头的趋势。将年龄离散化为[儿童, 青年, 老年]几个区间后,模型可以为每个区间学习独立的权重,从而灵活建模。
如何确定分箱边界?
- 等距切分:按数值范围均匀划分。例如,价格每50元一段。缺点:可能无法适应数据的不均匀分布。
- 等频切分:按样本数量均匀划分,使得每个箱内的样本数大致相同。通常使用分位数来确定边界。
上一节我们处理了数值型数据,本节中我们来看看计算机更难以理解的类别型和时间型数据。
特征处理:类别型与时间型数据
类别型数据
类别型数据如口红色号、衣服尺码、星期几,计算机无法直接理解,必须转换为数值。
1. 独热编码
这是最常用的方法。为每个类别取值创建一个新的二值特征列。
- 原始数据:颜色 [红, 绿, 蓝, 黄]
- 独热编码后:
- 红 → [1, 0, 0, 0]
- 绿 → [0, 1, 0, 0]
- 蓝 → [0, 0, 1, 0]
- 黄 → [0, 0, 0, 1]
为什么不直接用1,2,3,4标签编码? 因为标签编码会引入不应存在的大小顺序关系(例如,模型可能错误地认为“黄色(4)”比“红色(1)”更重要)。
2. 哈希技巧
当类别取值极多(如词表)时,独热编码维度会爆炸。哈希技巧可以将高维稀疏表示压缩到低维。例如,在文本分类中,可以统计一篇文章的词落在预定义的“财经”、“体育”、“政治”等词表中的数量,用这个计数向量作为特征。
3. 基于统计的映射
通过统计类别与其他特征的关联来构造新特征。例如,统计“性别”分组下,“爱好”为足球、散步、看电视剧的占比。然后将这个占比向量(如男性:[0.67, 0.33, 0.0])作为新特征附加到对应性别的样本上。
时间型数据
时间戳是宝贵的信息源,既可以作为连续值,也可以离散化后作为类别值。
可抽取的特征包括:
- 离散特征:小时、星期几、月份、季度、是否节假日、距离特定节日(如双十一)的天数。
- 连续特征:页面浏览时长、两次行为的时间间隔、用户连续登录天数。
- 历史统计特征:过去7天的平均活跃度、上周同一时间的销量。
在电商比赛中,经常需要结合日历,挖掘节假日、促销周期与用户行为的关系,这些时间特征往往非常有效。
特征处理:文本型数据与特征构造
文本型数据
计算机无法理解文本,必须将其向量化。
1. 词袋模型
构建一个词表,文本用一个大向量表示,向量每一维对应一个词,出现则为1(或词频),否则为0。
- 缺点:完全丢失了词序信息。“李雷喜欢韩梅梅”和“韩梅梅喜欢李雷”的表示相同。
2. N-gram模型
为了捕捉词序,可以同时考虑相邻词的组合。例如,2-gram会提取“李雷 喜欢”、“喜欢 韩梅梅”作为特征。这在一定程度上保留了局部词序信息。
3. TF-IDF
这是一种加权统计特征,用于评估一个词对于一份文档的重要程度。
- 公式:
TF-IDF(t, d) = TF(t, d) * IDF(t) - 词频:
TF(t, d) = (词t在文档d中出现的次数) / (文档d中所有词的总数) - 逆文档频率:
IDF(t) = log(总文档数 / (包含词t的文档数 + 1)) - 核心思想:一个词在当前文档中出现次数越多(TF高),且在全体文档中出现越少(IDF高),则它对当前文档越重要。
特征构造:统计与组合
除了转换现有特征,还可以基于业务理解构造全新的特征。
统计特征:
- 比值型:商品的好/中/差评比例,用户消费超过品类平均水平的百分比。
- 排名型:商品销量在同类中的排名,学生成绩在班级的百分位。
- 趋势型:商品昨日销量与前日销量的涨幅。
特征组合:
- 人工组合:将两个或多个特征交互,生成新的特征。例如,“用户ID” + “商品类别”组合成一个新特征,只有当特定用户浏览特定类别时,该特征才为1。
- 基于模型组合:利用树模型(如GBDT)来发现有效的特征组合。树模型分裂节点时使用的条件(如“性别=男”且“城市=上海”),本身就是特征的组合,可以将这些组合路径作为新的特征使用。
通过以上方法,我们可以从原始数据中构造出大量特征。但特征并非越多越好,接下来我们需要进行特征选择。
特征选择
当特征维度很高时,会消耗大量计算资源,且可能包含冗余或无关特征,甚至对模型产生负面影响。特征选择的目标是筛选出最重要的特征子集。
特征选择 vs. 降维:
- 特征选择:从原始特征中挑选子集,特征含义保持不变。
- 降维:通过变换将高维特征映射到低维空间(如PCA),新特征失去了原始含义。
特征选择方法:
1. 过滤法
单独评估每个特征与目标变量的相关性(如计算相关系数),选择相关性最高的特征。
- 优点:计算快。
- 缺点:未考虑特征间的相互作用。
2. 包裹法
将特征选择看作一个特征子集搜索问题。常用递归特征消除:
* 用全部特征训练一个能提供特征重要度的模型(如随机森林)。
* 剔除最不重要的特征。
* 用剩余特征重新训练模型,评估性能。
* 重复上述过程,直到性能显著下降。
3. 嵌入法
利用模型训练过程本身来进行特征选择。最典型的是使用L1正则化的线性模型(如Lasso回归、线性SVM)。
- 原理:L1正则化具有稀疏化效应,会使许多不重要的特征的权重变为零。训练完成后,权重非零的特征即被选中。
- 适用场景:特别适用于高维稀疏特征(如经过独热编码后的特征)。
实战案例与总结
本节课中我们一起学习了特征工程的完整流程:从数据采集清洗,到针对数值、类别、时间、文本等各类数据的处理方法,再到特征构造与选择。
我们通过两个案例加深理解:
- 共享单车需求预测:演示了如何从日期时间戳中提取小时、星期几、月份等特征,并进行初步分析。
- 探索性数据分析:展示了一个完整的EDA流程,包括数据概览、缺失值和异常值分析、特征分布可视化、特征工程、以及初步建模。这强调了基于数据洞察来指导特征工程的重要性。
特征工程是连接数据和模型的桥梁,是机器学习项目成败的关键。它没有固定公式,依赖于对数据的深刻理解、业务知识的积累以及不断的实验和迭代。掌握好特征工程,你就能让简单的模型发挥出强大的力量。


核心公式与代码片段回顾:
- 标准化公式:
X_scaled = (X - mean(X)) / std(X) - 离散化(等频分箱)代码示意:
import pandas as pd # 使用qcut进行等频分箱 data['age_bin'] = pd.qcut(data['age'], q=5, labels=False) - 独热编码代码示意:
import pandas as pd # 使用get_dummies进行独热编码 encoded_df = pd.get_dummies(data, columns=['color']) - TF-IDF公式:
TF-IDF(t, d) = (count(t in d) / len(d)) * log(N / (df(t) + 1)) - 基于L1正则化的特征选择代码示意:
from sklearn.svm import LinearSVC from sklearn.feature_selection import SelectFromModel lsvc = LinearSVC(C=0.01, penalty="l1", dual=False).fit(X, y) model = SelectFromModel(lsvc, prefit=True) X_new = model.transform(X)


🧠 机器学习基础与广义线性模型速讲(课程P5)
在本节课中,我们将学习机器学习的基本概念、核心术语,并深入理解广义线性模型家族中的两个基础但至关重要的模型:线性回归与逻辑斯蒂回归。掌握这些内容是后续学习更复杂模型(如神经网络、集成学习等)的基石。
📚 第一部分:机器学习基础与术语

上一节我们介绍了课程的整体安排和学习脉络。本节中,我们来看看机器学习领域必须掌握的核心术语和基本概念,这是理解所有后续模型的前提。




基本术语定义



以下是理解机器学习数据与任务的基础术语:
- 属性/特征:描述事物在特定方面表现或性质的事项。在结构化数据(如表格)中,通常对应表格的列。例如,在学生信息表中,“姓名”、“身高”、“数学成绩”都是属性。
- 属性值:属性上的具体取值。例如,属性“性别”的取值可以是“男”或“女”。
- 属性空间/输入空间:由某个属性所有可能取值构成的集合。例如,如果“成绩等级”属性只取“优”、“良”、“中”,则其属性空间为 {优, 良, 中}。
- 记录/样本/实例:对一个具体事物的属性描述,由属性向量表示。在表格中,通常对应行。一条记录
x_i可以表示为:
x_i = (x_i^{(1)}, x_i^{(2)}, ..., x_i^{(n)})^T
其中,x_i^{(j)}表示第i条记录的第j个属性值,上标T表示转置(在机器学习中,向量默认为列向量)。 - 标记/标签:描述事物某个特性或结果的事项,通常是模型要预测的目标。例如,在学生表中,“是否为三好学生”这一列就是标记。
- 标记空间/输出空间:标记所有可能取值的集合。例如,二分类问题的标记空间为 {0, 1}。
- 样例:由一条记录及其对应的标记组成的对,表示为
(x_i, y_i)。 - 数据集:记录的集合或样例的集合。根据是否包含标记,可分为:
- 无监督学习数据集:仅包含记录(属性)的集合。
- 有监督学习数据集:包含样例(记录-标记对)的集合。根据标记的类型,有监督学习任务可进一步分为:
- 回归问题:标记为连续值。
- 分类问题:标记为离散值。其中,标记只有两个取值的称为二分类问题,多于两个的称为多分类问题。
- 数据划分:为了有效评估模型,通常将数据集划分为:
- 训练集:用于训练模型的数据集。
- 验证集:在训练过程中用于选择模型超参数(如学习率)的数据集。
- 测试集:在模型训练完成后,用于最终评估模型泛化性能的数据集。
假设空间与学习目标
上一节我们明确了数据的形式。本节中,我们来看看机器学习的核心目标:从数据中学习一个“映射”或“规律”。
机器学习的目标是学习一个从输入空间 X 到输出空间 Y 的映射。这个映射可以有两种表现形式:
- 决策函数(非概率模型):形式为
Y = f(X),直接给出预测值。 - 条件概率分布(概率模型):形式为
P(Y|X),给出在已知输入X的条件下,输出Y取各种值的概率。
所有可能的映射 f 或条件概率分布 P 构成的集合,称为假设空间 F。学习的过程,就是在假设空间 F 中,根据一定的策略,寻找最优的映射 f* 或分布 P*。
在实际模型中,映射 f 或分布 P 通常由一个参数向量 w 决定(例如,直线 y=ax+b 由参数 a, b 决定)。因此,寻找最优映射的问题,常常转化为在参数空间中寻找最优参数 w* 的问题。

策略:损失函数与风险最小化



为了在假设空间中寻找最优模型,我们需要一个衡量模型“好坏”的标准。这就是损失函数和风险最小化策略。




损失函数 L(Y, f(X)) 度量模型预测值 f(X) 与真实值 Y 之间的差异(错误程度)。值越小,预测越准确。常见的损失函数有:


- 0-1损失函数:
L(Y, f(X)) = I(Y ≠ f(X)),其中I(·)为指示函数。仅判断对错,不度量误差大小。 - 平方损失函数:
L(Y, f(X)) = (Y - f(X))^2。最常用于回归问题,连续可导,能度量误差程度。 - 绝对损失函数:
L(Y, f(X)) = |Y - f(X)|。 - 对数损失函数:
-log P(Y|X)。常用于概率模型。

有了损失函数,就可以定义在训练集 D 上的平均损失,即经验风险:
R_emp(f) = 1/N * Σ_{i=1}^{N} L(y_i, f(x_i))
一个直观的策略是选择使经验风险最小的模型,即经验风险最小化:
f* = arg min_{f ∈ F} R_emp(f)







过拟合与结构风险最小化


然而,单纯追求经验风险最小化会导致一个严重问题:过拟合。即模型在训练集上表现极好(损失很小),但在未知的测试数据上表现很差。这通常是因为模型过于复杂,“死记硬背”了训练数据中的噪声和特例。
为了解决过拟合,需要在经验风险上增加一个表示模型复杂度的正则化项 J(f),构成结构风险:
R_srm(f) = R_emp(f) + λJ(f)
其中 λ ≥ 0 是权衡经验风险与模型复杂度的系数。

新的策略变为结构风险最小化:
f* = arg min_{f ∈ F} R_srm(f)

常见的正则化项有:
- L2正则化(岭回归):
J(f) = 1/2 ||w||_2^2。倾向于让参数w的各分量值都比较小。 - L1正则化(LASSO回归):
J(f) = ||w||_1。倾向于产生稀疏参数,即让部分参数为0。





优化算法




确定了风险最小化的策略后,寻找最优参数 w* 的过程就是一个优化问题。当无法直接求解解析解时,常用梯度下降等迭代方法。



基本思想是:随机初始化参数 w0,然后沿损失函数关于 w 的负梯度方向(函数下降最快的方向)更新参数:
w_{t+1} = w_t - η * ∇ R(w_t)
其中,η 称为学习率,是一个需要设定的超参数,控制每次更新的步长。这个过程反复迭代,直至收敛。


性能评估
模型训练完成后,需要评估其性能。


- 训练误差:模型在训练集上的平均损失。
- 测试误差:模型在测试集上的平均损失。测试误差更能反映模型的泛化能力。
- 分类问题评估指标:对于二分类问题,常使用混淆矩阵衍生出的指标:
- 精确率/查准率 (Precision):
P = TP / (TP + FP)。预测为正的样本中,真正为正的比例。 - 召回率/查全率 (Recall):
R = TP / (TP + FN)。所有真实为正的样本中,被正确预测出来的比例。 - F1值:
F1 = 2 * P * R / (P + R)。精确率和召回率的调和平均。
- 精确率/查准率 (Precision):



📈 第二部分:线性回归模型


上一节我们建立了机器学习的基础框架。本节中,我们应用这个框架来学习第一个具体模型:线性回归。



模型定义



给定数据集 D = {(x_1, y_1), (x_2, y_2), ..., (x_N, y_N)},其中 x_i ∈ R^n 为n维特征向量,y_i ∈ R 为连续值标记。多元线性回归模型试图学得一个线性函数:
f(x_i) = w^T x_i + b
来近似表示 y_i。其中 w = (w_1, w_2, ..., w_n)^T 为权重向量,b 为偏置项。

为简化形式,令增广权重向量 ŵ = (w^T, b)^T,增广特征向量 x̂_i = (x_i^T, 1)^T,则模型可写为:
f(x̂_i) = ŵ^T x̂_i
策略与求解(经验风险最小化)



采用平方损失函数,则经验风险为:
R_emp(ŵ) = 1/N * Σ_{i=1}^{N} (y_i - ŵ^T x̂_i)^2






我们的目标是找到使 R_emp(ŵ) 最小的 ŵ*。这可以通过令损失函数对 ŵ 的导数为零,直接求得解析解(最小二乘解):
ŵ* = (X^T X)^{-1} X^T y
其中,X 是由所有 x̂_i^T 堆叠而成的设计矩阵,y 是由所有 y_i 堆叠而成的向量。

更多时候,我们使用梯度下降法求数值解。平方损失关于 ŵ 的梯度为:
∇ R_emp(ŵ) = -2/N * Σ_{i=1}^{N} (y_i - ŵ^T x̂_i) x̂_i
然后按照 ŵ_{t+1} = ŵ_t - η * ∇ R_emp(ŵ_t) 进行迭代更新。

结构风险最小化版:岭回归
为防止过拟合,在线性回归的经验风险中加入L2正则化项,得到岭回归的目标函数:
R_srm(ŵ) = 1/N * Σ_{i=1}^{N} (y_i - ŵ^T x̂_i)^2 + λ ||ŵ||_2^2
其梯度为:
∇ R_srm(ŵ) = -2/N * Σ_{i=1}^{N} (y_i - ŵ^T x̂_i) x̂_i + 2λŵ
同样可以用梯度下降法求解。
🔮 第三部分:逻辑斯蒂回归模型





上一节我们学习了用于回归问题的线性模型。本节中,我们通过对线性回归进行一个巧妙的变换,将其改造成用于分类问题的逻辑斯蒂回归模型。


从回归到分类:Sigmoid函数
逻辑斯蒂回归虽然名字叫“回归”,但解决的是二分类问题。其核心思想是:在线性回归模型 z = w^T x + b 的输出上,套用一个 Sigmoid函数,将连续值 z 映射到 (0, 1) 区间,并将其解释为样本属于正类的概率。
Sigmoid函数定义为:
σ(z) = 1 / (1 + e^{-z})
其函数图像呈S形,值域为 (0, 1),且 σ'(z) = σ(z)(1 - σ(z))。
模型定义


逻辑斯蒂回归模型将线性回归的输出 z 作为Sigmoid函数的输入:
P(Y=1 | x) = σ(w^T x + b) = 1 / (1 + e^{-(w^T x + b)})
P(Y=0 | x) = 1 - P(Y=1 | x)
其中,P(Y=1 | x) 表示在给定特征 x 的条件下,样本属于类别1的概率。
策略与求解(极大似然估计)



对于概率模型,我们通常采用极大似然估计策略。对于整个数据集 D,其似然函数为所有样本预测概率的连乘。为方便计算,取对数得到对数似然函数:
L(w) = Σ_{i=1}^{N} [y_i log P(Y=1|x_i) + (1-y_i) log P(Y=0|x_i)]
将概率表达式代入,并利用 y_i 取0或1的特性,可以简化为:
L(w) = Σ_{i=1}^{N} [y_i (w^T x_i) - log(1 + e^{w^T x_i})]


我们的目标是最大化对数似然函数 L(w)。这等价于最小化负对数似然损失。对 L(w) 关于 w 求导,得到梯度:
∇ L(w) = Σ_{i=1}^{N} (y_i - σ(w^T x_i)) x_i


由于此梯度没有解析解,我们使用梯度上升法(因为这里是求极大值)进行迭代求解:
w_{t+1} = w_t + η * ∇ L(w_t)
注意,这里的更新是“加”梯度,因为是最大化目标函数。
核心关联
逻辑斯蒂回归可以看作是线性回归 + Sigmoid激活函数。这个结构正是现代神经网络中单个神经元(感知机)的基本组成。因此,理解逻辑斯蒂回归是理解复杂神经网络的基础。











本节课中我们一起学习了机器学习的基础术语、核心框架(假设空间、风险最小化),并深入剖析了广义线性模型家族的两个代表:用于回归的线性回归和用于分类的逻辑斯蒂回归。我们看到了如何通过定义模型、制定策略(损失函数)、应用优化算法来求解模型,也理解了过拟合问题及正则化的解决方法。这些概念和方法是构建所有机器学习模型的通用语言和工具箱,务必牢固掌握。
机器学习课程笔记 - 第6课:决策树与Boosting模型融合精髓 🎄⚡
在本节课中,我们将要学习机器学习中非常重要的一类模型——决策树,以及基于决策树的集成学习方法——Boosting。我们将从决策树的基本原理出发,逐步深入到其核心算法和模型融合的精髓。
一、决策树模型概述
决策树模型是一类以树形结构为基础的机器学习模型。它通过模拟人类决策过程,依据特征的重要性排序,对数据进行层层划分,最终做出判断或预测。
决策树模型主要解决分类问题,但经过改造后也可用于回归任务。其核心在于对特征进行顺序性考察,即先判断哪个特征,再判断哪个特征。
上一节我们回顾了机器学习的基本术语,本节中我们来看看决策树如何将这些概念应用于实际建模。
二、决策树的核心步骤
构建一棵决策树通常包含三个核心步骤:
- 特征选择:确定使用哪个特征来对数据进行划分。
- 决策树生成:根据特征选择的结果,递归地构建树形结构。
- 决策树修剪:对生成的树进行简化,防止过拟合。
接下来,我们将逐一深入探讨这三个步骤。
三、特征选择:信息增益与熵
特征选择需要有一个量化的依据来判断哪个特征更重要。这里我们引入信息论中的两个核心概念:熵和信息增益。
3.1 熵:不确定性的度量
熵(Entropy)表示随机变量的不确定性。不确定性越大,熵值越高;确定性越强,熵值越低。
对于一个离散随机变量 (X),其取值为 ({x_1, x_2, ..., x_n}),对应的概率为 (P(X=x_i)=p_i),则 (X) 的熵 (H(X)) 定义为:
[
H(X) = -\sum_^ p_i \log p_i
]
理解示例:抛一枚均匀硬币,正反面概率各为1/2,不确定性最大,熵值最高。如果一枚硬币总是正面朝上,则结果是确定的,熵值为0。
3.2 条件熵与信息增益
条件熵 (H(Y|X)) 表示在已知随机变量 (X) 的条件下,随机变量 (Y) 的不确定性。
信息增益 (g(D, A)) 则表示特征 (A) 为数据集 (D) 带来的信息量,它是熵与条件熵的差值:
[
g(D, A) = H(D) - H(D|A)
]
其中:
- (H(D)):数据集 (D) 的熵(基于类别标签计算)。
- (H(D|A)):特征 (A) 给定的条件下,数据集 (D) 的条件熵。
核心思想:信息增益越大,意味着使用特征 (A) 进行划分后,数据集的不确定性减少得越多,因此该特征越重要。
信息增益的计算分为三步:
- 计算数据集 (D) 的熵 (H(D))。
- 计算特征 (A) 给定下的条件熵 (H(D|A))。
- 求差值得到信息增益 (g(D, A))。
上一节我们介绍了熵和信息增益的概念,本节中我们来看看如何具体计算它们以进行特征选择。
四、决策树生成算法
有了特征选择的依据,我们就可以递归地生成决策树。以下是两种经典算法。
4.1 ID3算法
ID3算法使用信息增益作为特征选择准则。
以下是ID3算法的核心步骤:
- 若当前数据集中所有实例属于同一类别,则构建单节点树,以此类别作为标记。
- 若特征集合为空,则构建单节点树,以数据集中实例数最多的类别作为标记。
- 否则,计算所有特征的信息增益,选择信息增益最大的特征 (A_g)。
- 如果 (A_g) 的信息增益小于预设阈值,则构建单节点树,以数据集中实例数最多的类别作为标记。
- 否则,依据特征 (A_g) 的每一个可能取值,将数据集划分为若干子集。
- 对每个子集,以剩余特征集合递归调用步骤1-5,构建子树。
4.2 C4.5算法
C4.5算法是ID3的改进,它使用信息增益比作为特征选择准则,以修正信息增益偏向于选择取值较多的特征的问题。
信息增益比 (g_R(D, A)) 定义为:
[
g_R(D, A) = \frac{g(D, A)}{H_A(D)}
]
其中 (H_A(D)) 是数据集 (D) 关于特征 (A) 的熵(这次是基于特征 (A) 的取值计算)。
C4.5的生成过程与ID3完全相同,只是将特征选择准则从信息增益换成了信息增益比。
五、决策树的剪枝
决策树生成算法可能产生过于复杂的树,导致过拟合。剪枝通过简化树结构来提升模型的泛化能力。
剪枝通常通过极小化决策树的整体损失函数来实现。损失函数 (C_\alpha(T)) 定义如下:
[
C_\alpha(T) = \sum_^{|T|} N_t H_t(T) + \alpha |T|
]
其中:
- (|T|):树 (T) 的叶节点个数,衡量模型复杂度。
- (N_t):第 (t) 个叶节点的样本数。
- (H_t(T)):第 (t) 个叶节点上的经验熵。
- (\alpha):权衡拟合程度与模型复杂度的参数。
剪枝算法:从生成的树 (T) 开始,递归地从叶节点向上回缩。比较回缩前((T_B))和回缩后((T_A))两棵树的损失函数值 (C_\alpha(T_B)) 和 (C_\alpha(T_A))。如果 (C_\alpha(T_A) \leq C_\alpha(T_B)),则进行剪枝(用父节点代替该子节点)。持续此过程,直到不能继续提升为止。
六、CART树:分类与回归树
CART(Classification And Regression Tree)模型是另一类广泛使用的决策树,它既可以做分类也可以做回归。
6.1 CART回归树
CART回归树将输入空间划分为 (M) 个单元 (R_1, R_2, ..., R_M),每个单元有一个固定的输出值 (c_m)。模型表示为:
[
f(x) = \sum_^ c_m I(x \in R_m)
]
其中 (I(\cdot)) 是指示函数。
生成关键:如何划分空间?对于每个特征 (j) 和其取值 (s),定义两个区域:
[
R_1(j, s) = {x | x^{(j)} \leq s} \quad 和 \quad R_2(j, s) = {x | x^{(j)} > s}
]
然后寻找最优的切分变量 (j) 和切分点 (s),使得两个区域内的样本输出值 (y_i) 与该区域输出值 (c)(通常取区域内 (y_i) 的均值)的误差平方和最小:
[
\min_{j, s} \left[ \min_ \sum_{x_i \in R_1(j,s)} (y_i - c_1)^2 + \min_ \sum_{x_i \in R_2(j,s)} (y_i - c_2)^2 \right]
]
递归地对两个子区域重复此过程,即可构建回归树。
6.2 CART分类树
CART分类树在生成时,使用基尼指数(Gini Index)代替信息增益作为特征选择准则。基尼指数同样表示数据的不纯度。
对于数据集 (D),其基尼值定义为:
[
\text(D) = 1 - \sum_ \left( \frac{|C_k|}{|D|} \right)2
]
特征 (A) 下集合 (D) 的基尼指数定义为:
[
\text{Gini_index}(D, A) = \sum_ \frac{|Dv|}{|D|} \text(D^v)
]
选择使基尼指数最小的特征进行划分。
七、集成学习与Boosting
集成学习通过结合多个性能较弱的“基学习器”来构建一个更强、更稳定的模型。Boosting是其中一种主流方法,其核心思想是“三个臭皮匠,顶个诸葛亮”。
7.1 加法模型与前向分步算法
Boosting通常表示为加法模型:
[
f(x) = \sum_^ \beta_m b(x; \gamma_m)
]
其中 (b(x; \gamma_m)) 是基学习器,(\beta_m) 是系数。
直接优化整个加法模型很复杂,因此采用前向分步算法:每一步只学习一个基学习器及其系数,逐步逼近优化目标。
算法步骤:
- 初始化 (f_0(x) = 0)。
- 对 (m = 1, 2, ..., M):
- 求解当前步的最优基学习器 (\beta_m) 和 (b(x; \gamma_m)),使得损失函数最小:
[
(\beta_m, \gamma_m) = \arg \min_{\beta, \gamma} \sum_^ L(y_i, f_(x_i) + \beta b(x_i; \gamma))
] - 更新模型:(f_m(x) = f_(x) + \beta_m b(x; \gamma_m))。
- 求解当前步的最优基学习器 (\beta_m) 和 (b(x; \gamma_m)),使得损失函数最小:
- 得到最终加法模型 (f(x) = f_M(x))。
7.2 提升决策树(BDT)
以决策树为基学习器的Boosting方法称为提升决策树(Boosting Decision Tree, BDT)。模型形式为:
[
f_M(x) = \sum_^ T(x; \Theta_m)
]
这里简化了系数 (\beta_m),通常认为每棵树权重相同。
采用前向分步算法,第 (m) 步的优化目标是:
[
\hat{\Theta}m = \arg \min{\Theta_m} \sum_^ L(y_i, f_(x_i) + T(x_i; \Theta_m))
]
7.3 关键结论:拟合残差
当损失函数 (L) 为平方损失 (L(y, f(x)) = (y - f(x))^2) 时,上述优化问题有直观解释。
第 (m) 步的损失函数可写为:
[
\sum_^ [y_i - f_(x_i) - T(x_i; \Theta_m)]2 = \sum_ [r_{i,m} - T(x_i; \Theta_m)]^2
]
其中 (r_{i,m} = y_i - f_(x_i)) 是当前模型 (f_(x)) 在样本 (i) 上的残差。
核心洞察:在平方损失下,前向分步算法每一步训练的新决策树 (T(x; \Theta_m)),其学习目标不再是原始输出 (y),而是上一轮模型预测的残差 (r_{i,m})。即,新树专门用于修正之前所有树累积的误差。
这就像学生整理“错题本”,每次只专注于之前做错的题目进行强化学习,从而逐步提升整体成绩。
课程总结
本节课中我们一起学习了决策树模型与Boosting模型融合的精髓。
- 决策树基础:了解决策树通过特征选择、树生成和剪枝三个步骤构建,其本质是对输入空间的划分。
- 特征选择:掌握了信息增益、信息增益比和基尼指数等核心概念,它们为选择划分特征提供了量化依据。
- 经典算法:学习了ID3、C4.5和CART算法,理解了它们分别适用于不同场景(分类/回归)和使用不同划分准则。
- 模型融合:引入了集成学习的Boosting思想,理解了加法模型和前向分步算法如何将弱学习器组合成强学习器。
- 提升决策树:掌握了以决策树为基学习器的Boosting方法,并得出了在平方损失下,Boosting过程是不断拟合残差的重要结论。


这些内容是理解后续更强大的集成模型(如GBDT、XGBoost)的坚实基础。


【七月在线】机器学习就业训练营16期 - P7:朴素贝叶斯与SVM模型精髓速讲 📚
在本节课中,我们将要学习两个在机器学习中至关重要的模型:支持向量机(SVM)和朴素贝叶斯。我们将从SVM的硬间隔、软间隔、核技巧到其优化算法SMO,系统地理解其原理。随后,我们将探讨朴素贝叶斯模型,学习其基于概率的分类思想以及相关的核心数学规则。课程内容旨在为初学者构建清晰的知识框架。
第一部分:支持向量机(SVM)模型精髓 🧠
上一节我们介绍了机器学习模型的基本分类,本节中我们来看看一个经典且强大的分类模型——支持向量机。
1. 硬间隔支持向量机(线性可分)
支持向量机最初要解决的是线性可分的二分类问题。其核心思想是找到一个最优的分离超平面,使得两类样本点距离该平面的“间隔”最大化。
问题定义与数据集
我们拥有的数据集是典型的监督学习数据集:
- 输入:
T = {(x1, y1), (x2, y2), ..., (xn, yn)} - 特征向量:
xi ∈ R^n,是一个n维向量。 - 标签:
yi ∈ {+1, -1},代表两个类别(注意,SVM中通常使用+1和-1,而非0和1)。
分离超平面与决策函数
目标是学到一个分类超平面:
w* · x + b* = 0
以及相应的分类决策函数:
f(x) = sign(w* · x + b*)
其中,w*是权重向量,b*是偏置,sign(·)是符号函数。
函数间隔与几何间隔
为了量化“间隔”,我们引入两个核心概念:
以下是关于样本点的间隔定义:
- 函数间隔:
γ̂_i = yi (w · xi + b) - 几何间隔:
γ_i = yi (w · xi + b) / ||w||,其物理意义是样本点到超平面的几何距离。
对于整个训练集,我们关心的是所有样本点中间隔最小的那个:
- 数据集函数间隔:
γ̂ = min_i γ̂_i - 数据集几何间隔:
γ = min_i γ_i
两者关系为:γ = γ̂ / ||w||
间隔最大化
SVM的核心目标是最大化几何间隔γ。这意味着我们要让离超平面最近的那些样本点(即支持向量)尽可能远离它。这等价于求解以下带约束的最优化问题:
max γ
subject to: yi (w · xi + b) / ||w|| >= γ, i=1,2,...,n
问题转化与求解
通过一系列数学转化(包括利用函数间隔的相对性,将其固定为1),上述问题可以等价为更易求解的凸二次规划问题:
min (1/2) ||w||^2
subject to: yi (w · xi + b) >= 1, i=1,2,...,n
我们使用拉格朗日乘子法求解此约束优化问题:
- 构建拉格朗日函数:引入拉格朗日乘子αi >= 0。
L(w, b, α) = (1/2)||w||^2 - Σ α_i [y_i (w·x_i + b) - 1] - 求极小值:分别对
w和b求偏导并令其为零。∂L/∂w = 0 => w = Σ α_i y_i x_i∂L/∂b = 0 => Σ α_i y_i = 0
- 求极大值:将上述结果代回拉格朗日函数,问题转化为关于α的对偶问题:
max Σ α_i - (1/2) Σ Σ α_i α_j y_i y_j (x_i · x_j)
subject to: Σ α_i y_i = 0, α_i >= 0
最终,最优超平面参数为:
w* = Σ α_i* y_i x_ib*可通过支持向量(对应α_i* > 0的样本)计算得到。
2. 软间隔支持向量机(线性不可分)
硬间隔要求数据严格线性可分,这在实际中往往不成立。软间隔SVM通过引入松弛变量ξ_i,允许一些样本点被错误分类或落入间隔带内,从而处理近似线性可分的数据。
其优化问题变为:
min (1/2) ||w||^2 + C Σ ξ_i
subject to: y_i (w · x_i + b) >= 1 - ξ_i, ξ_i >= 0
其中,C > 0是惩罚参数,控制对误分类的惩罚力度。求解过程与硬间隔类似,同样会转化为对偶问题求解。
3. 非线性支持向量机与核技巧 🌀
上一节我们介绍了处理近似线性问题的软间隔,本节中我们来看看如何处理完全非线性可分的问题。
对于在原始特征空间中线性不可分的数据,我们可以将其映射到一个更高维的特征空间,使其在新空间中线性可分。核技巧的精妙之处在于,我们不需要显式地定义这个映射函数Φ(x),只需要一个核函数K(x, z)。
核函数定义了原始空间中两个向量的某种相似度,它等价于它们在映射后空间中的内积:
K(x_i, x_j) = Φ(x_i) · Φ(x_j)
这样,在对偶问题的目标函数中,我们只需要将内积(x_i · x_j)替换为核函数K(x_i, x_j)即可:
max Σ α_i - (1/2) Σ Σ α_i α_j y_i y_j K(x_i, x_j)
常用的核函数包括:
- 多项式核函数:
K(x, z) = (x · z + 1)^p - 高斯核函数(RBF核):
K(x, z) = exp(-γ ||x - z||^2)
4. 序列最小最优化算法(SMO)
求解SVM最终的对偶问题是一个二次规划问题。当样本量很大时,通用求解器效率低下。SMO算法是一种高效的启发式算法,其基本思想是:
- 每次只选择两个拉格朗日乘子
α_i和α_j进行优化,固定其他乘子。 - 由于存在约束
Σ α_i y_i = 0,两个变量之间存在线性关系,因此二次规划问题可以解析求解。 - 不断选择新的乘子对进行优化,直至收敛。
第二部分:朴素贝叶斯模型 🎲
上一节我们学习了基于间隔最大化的SVM,本节中我们来看看一个基于概率统计的经典分类模型——朴素贝叶斯。
1. 基本概率规则
在深入模型前,需要掌握两个贯穿概率图模型的基础规则:
以下是两条核心的概率运算规则:
- 加和(边际化)规则:
P(X) = Σ_Y P(X, Y)- 用于从联合概率中消除变量,得到边际概率。
- 乘积(链式)规则:
P(X, Y) = P(X|Y) P(Y)- 用于将联合概率分解为条件概率和边际概率的乘积。
贝叶斯定理是乘积规则的一个直接推论:
P(Y|X) = P(X|Y) P(Y) / P(X)
其中,P(Y|X)称为后验概率,P(X|Y)称为似然,P(Y)称为先验概率。
2. 朴素贝叶斯模型原理
朴素贝叶斯模型基于贝叶斯定理,并做了一个强大的条件独立性假设:在给定类别标签Y的条件下,所有特征X1, X2, ..., Xn之间相互独立。
这意味着:
P(X1, X2, ..., Xn | Y=ck) = Π_j P(Xj | Y=ck)
对于一个分类问题,给定输入x,我们计算它属于每个类别ck的后验概率:
P(Y=ck | X=x) = [P(Y=ck) Π_j P(Xj=xj | Y=ck)] / P(X=x)
由于对于所有类别,分母P(X=x)相同,因此我们只需比较分子的大小。决策规则就是选择使分子最大的那个类别:
y = argmax_{ck} P(Y=ck) Π_j P(Xj=xj | Y=ck)
3. 参数估计
模型中的参数(先验概率P(Y=ck)和条件概率P(Xj=ajl | Y=ck))需要通过训练数据来估计。
以下是两种常用的参数估计方法:
- 极大似然估计:直接用训练数据中的频率来估计概率。
P(Y=ck) = (Σ_i I(yi=ck)) / NP(Xj=ajl | Y=ck) = (Σ_i I(xi_j=ajl, yi=ck)) / (Σ_i I(yi=ck))- 其中
I(·)是指示函数。
- 贝叶斯估计(拉普拉斯平滑):为了解决极大似然估计中可能出现的概率为0的情况(当某个特征值在某个类别下未出现时),在分子分母上加上平滑项。
P_λ(Xj=ajl | Y=ck) = (Σ_i I(xi_j=ajl, yi=ck) + λ) / (Σ_i I(yi=ck) + Sj λ)- 当λ=1时,称为拉普拉斯平滑。
总结 📝
本节课中我们一起学习了支持向量机和朴素贝叶斯两个核心模型。
对于支持向量机,我们理解了其追求最大间隔的核心思想,掌握了从硬间隔(线性可分)到软间隔(引入松弛变量处理噪声)再到非线性(通过核技巧映射到高维空间)的演进逻辑。我们也了解了其求解最终依赖于求解一个凸二次规划问题,并知道了SMO这一高效算法。
对于朴素贝叶斯,我们学习了其基于贝叶斯定理和条件独立性假设的概率框架。它通过计算后验概率进行分类,模型参数可以通过极大似然估计或贝叶斯估计从数据中学习。其模型简单,计算高效,常作为文本分类等任务的基线模型。


理解这两个模型,不仅掌握了两种重要的机器学习范式(几何间隔最大化与概率生成模型),也为后续学习更复杂的模型(如基于概率图模型的HMM、CRF)打下了坚实的基础。

机器学习就业训练营 - 课程16:XGBoost精讲 🚀
概述
在本节课中,我们将深入学习极限梯度提升树(XGBoost)模型。XGBoost是一种高效、强大的机器学习算法,在各类数据科学竞赛和实际应用中表现出色。我们将从其发展脉络讲起,理解其核心原理,并学习如何应用它。
1. 模型发展脉络回顾 📈
上一节我们介绍了集成学习的基础概念。本节中,我们来看看XGBoost是如何从一系列前序模型发展而来的。
XGBoost并非凭空产生,它是在一系列成熟模型基础上的改进与集成。其发展路径清晰可循:
- 决策树:作为基础模型,用于对输入空间进行划分和预测。
- Boosting集成方法:通过加法模型和前向分步算法,将多个弱学习器组合成强学习器。
- 提升决策树(BDT):以决策树为基模型的Boosting方法。
- 梯度提升决策树(GBDT):在BDT的基础上,使用损失函数的负梯度来近似残差,进行模型拟合。
- 极限梯度提升决策树(XGBoost):在GBDT的基础上,进行了两项核心改进:引入正则化项和使用二阶梯度信息,从而在精度和速度上获得显著提升。
这个过程体现了机器学习模型“站在巨人肩膀上”不断优化的特点。
2. 核心原理:从GBDT到XGBoost 🔬
上一节我们回顾了模型的发展史。本节中我们重点剖析XGBoost在GBDT基础上所做的关键改进。
2.1 改进一:引入正则化目标函数
GBDT模型主要关注经验风险最小化,容易导致过拟合。XGBoost在目标函数中显式地加入了正则化项,以控制模型复杂度。
模型定义:
在XGBoost中,单棵决策树模型定义为:
f(x) = w_{q(x)}
其中:
q(x)是一个将样本x映射到对应叶子节点索引的函数。w是一个向量,其元素w_j表示第j个叶子节点的得分(输出值)。
正则化目标函数:
对于包含 K 棵树的加法模型,其预测输出为 ŷ_i = Σ_{k=1}^{K} f_k(x_i)。XGBoost的目标函数为:
Obj(Θ) = Σ_{i=1}^{n} l(y_i, ŷ_i) + Σ_{k=1}^{K} Ω(f_k)
其中:
Σ l(y_i, ŷ_i)是传统的损失函数项(如平方损失、对数损失),衡量模型预测误差。Σ Ω(f_k)是正则化项,用于惩罚模型复杂度。其具体形式为:
Ω(f) = γT + (1/2)λ Σ_{j=1}^{T} w_j^2T是当前树的叶子节点个数。叶子越多,模型越复杂。γ和λ是超参数,控制正则化的强度。Σ w_j^2惩罚叶子节点得分过大,防止模型对个别样本过度响应。
这个设计使得优化过程同时兼顾“拟合数据”和“保持模型简洁”,有效缓解过拟合。
2.2 改进二:利用二阶泰勒展开
GBDT在每一轮迭代中,使用损失函数的一阶梯度(负梯度)来拟合新树。XGBoost进一步使用了二阶梯度信息,使每一步的优化方向更精确。
推导过程:
假设我们正在进行第 t 轮迭代,已经拥有前 t-1 棵树的模型 ŷ_i^{(t-1)},现在要学习第 t 棵树 f_t。目标函数为:
Obj^{(t)} = Σ_{i=1}^{n} l(y_i, ŷ_i^{(t-1)} + f_t(x_i)) + Ω(f_t) + constant
在 ŷ_i^{(t-1)} 处对损失函数 l 进行二阶泰勒展开:
l(y_i, ŷ_i^{(t-1)} + f_t(x_i)) ≈ l(y_i, ŷ_i^{(t-1)}) + g_i f_t(x_i) + (1/2) h_i f_t^2(x_i)
其中:
g_i = ∂l(y_i, ŷ_i^{(t-1)}) / ∂ŷ_i^{(t-1)}是一阶梯度。h_i = ∂²l(y_i, ŷ_i^{(t-1)}) / ∂(ŷ_i^{(t-1)})²是二阶梯度。
代入目标函数并忽略常数项,我们得到第 t 轮的简化优化目标:
Obj^{(t)} ≈ Σ_{i=1}^{n} [g_i f_t(x_i) + (1/2) h_i f_t^2(x_i)] + Ω(f_t)
2.3 求解最优树结构
将正则化项 Ω(f_t) = γT + (1/2)λ Σ_{j=1}^{T} w_j^2 和树模型定义 f_t(x_i) = w_{q(x_i)} 代入上述简化目标。
关键转换:
将按样本求和 Σ_{i=1}^{n} 转换为按叶子节点求和 Σ_{j=1}^{T} Σ_{i ∈ I_j},其中 I_j = {i | q(x_i) = j} 是被划分到第 j 个叶子节点的样本索引集合。
转换后,目标函数变为:
Obj^{(t)} ≈ Σ_{j=1}^{T} [ (Σ_{i ∈ I_j} g_i) w_j + (1/2)(Σ_{i ∈ I_j} h_i + λ) w_j^2 ] + γT
这是一个关于叶子节点得分 w_j 的二次函数。对于每个叶子 j,令其导数为零,可以解得最优叶子权重为:
w_j^* = - (Σ_{i ∈ I_j} g_i) / (Σ_{i ∈ I_j} h_i + λ)
将其代回目标函数,得到当树结构固定(即 q(x) 和 I_j 固定)时,最小的目标函数值为:
Obj^* = - (1/2) Σ_{j=1}^{T} [ (Σ_{i ∈ I_j} g_i)² / (Σ_{i ∈ I_j} h_i + λ) ] + γT
分裂依据:
这个值 Obj^* 可以用来衡量一棵树结构的好坏。XGBoost在构建树时,通过贪心算法寻找最佳分裂点。对于任何候选分裂,将样本分为左子树 I_L 和右子树 I_R,分裂后的目标函数增益为:
Gain = (1/2) [ (Σ_{i ∈ I_L} g_i)²/(Σ_{i ∈ I_L} h_i+λ) + (Σ_{i ∈ I_R} g_i)²/(Σ_{i ∈ I_R} h_i+λ) - (Σ_{i ∈ I} g_i)²/(Σ_{i ∈ I} h_i+λ) ] - γ
其中 I = I_L ∪ I_R。我们选择使 Gain 最大的特征和特征值进行分裂。Gain 计算了分裂后损失函数的减少量,如果 Gain 为负或小于阈值,则停止分裂。
3. XGBoost应用指南 🛠️
理解了核心原理后,本节我们来看看如何在实际中使用XGBoost。
3.1 主要参数解析
XGBoost参数丰富,合理调参对性能至关重要。主要参数可分为三类:
以下是通用参数:
booster:选择基学习器,默认为gbtree(树模型),也可选gblinear(线性模型)。silent:是否静默模式,默认为0(输出运行信息)。nthread:运行线程数,默认为最大可用线程数。
以下是提升器参数(针对树模型):
eta(learning_rate):学习率或步长收缩因子,默认为0.3。控制每棵树对最终结果的贡献,较小值需要更多树。gamma:节点分裂所需的最小损失减少量,默认为0。用于控制树的分裂,值越大算法越保守。max_depth:树的最大深度,默认为6。控制模型复杂度,防止过拟合。min_child_weight:叶子节点中样本权重和(即Hessian和)的最小值,默认为1。值越大算法越保守。subsample:训练每棵树时使用的样本比例,默认为1。可防止过拟合。colsample_bytree:训练每棵树时使用的特征比例,默认为1。lambda(reg_lambda):L2正则化项的权重系数,默认为1。alpha(reg_alpha):L1正则化项的权重系数,默认为0。
以下是学习任务参数:
objective:定义学习任务和损失函数。例如:reg:squarederror:平方损失回归。binary:logistic:二分类逻辑回归,输出概率。multi:softmax:多分类,输出类别。
eval_metric:验证数据的评估指标,如rmse,mae,logloss,error等。
3.2 基本使用示例
以下是一个简单的二分类任务示例代码框架:
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 1. 准备数据
# X, y 为特征和标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 2. 转换为DMatrix格式(XGBoost高效数据结构)
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
# 3. 设置参数
params = {
'booster': 'gbtree',
'objective': 'binary:logistic', # 二分类逻辑回归
'eval_metric': 'logloss',
'eta': 0.1,
'max_depth': 6,
'subsample': 0.8,
'colsample_bytree': 0.8,
'seed': 42
}
# 4. 训练模型
num_rounds = 100
bst = xgb.train(params, dtrain, num_rounds, evals=[(dtest, 'eval')], early_stopping_rounds=10)
# 5. 预测
# 预测概率
y_pred_proba = bst.predict(dtest)
# 转换为类别(阈值0.5)
y_pred = (y_pred_proba > 0.5).astype(int)
# 6. 评估
accuracy = accuracy_score(y_test, y_pred)
print(f"测试集准确率: {accuracy:.4f}")
# 7. 特征重要性
importance = bst.get_score(importance_type='weight')
print("特征重要性:", importance)
3.3 与LightGBM的简单对比
LightGBM是微软开发的另一个高效的梯度提升框架。以下是两者的简要对比:
- 速度:LightGBM通常训练速度更快,内存消耗更低,尤其在大数据集上。
- 准确性:两者在多数数据集上准确率相当,具体表现取决于数据和参数。
- 分裂策略:XGBoost使用按层生长的决策树(level-wise),而LightGBM使用按叶子生长的决策树(leaf-wise),后者在降低损失方面更有效,但可能生长出更深的树。
- 实践建议:在实际项目中,可以两者都尝试,选择在验证集上表现更好的模型。LightGBM在处理大规模数据时优势明显。
总结 🎯
本节课我们一起学习了XGBoost模型。我们从其发展脉络开始,理解了它作为GBDT的增强版本,核心贡献在于引入正则化项控制复杂度以及利用损失函数的二阶梯度信息使优化更精确。我们详细推导了其目标函数、最优解形式以及基于增益的分裂准则。最后,我们介绍了XGBoost的主要参数、基本使用方法,并简要对比了其与LightGBM的特点。


XGBoost因其出色的性能和速度,已成为机器学习工具箱中的必备利器。掌握其原理并能熟练应用,对于解决实际的回归、分类等问题至关重要。
课程名称:机器学习就业训练营 - P9:隐马尔可夫模型(HMM)要点教程
概述 📖
在本节课中,我们将系统地学习概率图模型中的核心模型之一——隐马尔可夫模型。我们将从模型的基本定义出发,逐步深入到其解决的三大核心问题:概率计算、学习算法和预测算法。课程内容将力求简单直白,确保初学者能够理解每一步的逻辑和推导过程。
一、模型定义 🧱
上一节我们介绍了本课程的整体脉络。本节中,我们来看看隐马尔可夫模型(HMM)是如何被精确定义的。
HMM主要用于解决序列到序列的标注任务,例如自然语言处理中的词性标注。给定一个单词序列(如“隐马尔可夫/模型”),模型的目标是推断出最可能的词性序列(如“名词/名词”)。
核心概念与集合
首先,我们需要定义两个基础集合:
- 状态集合 Q:包含所有可能的隐藏状态。
Q = {q1, q2, ..., qN},共有 N 种状态。 - 观测集合 V:包含所有可能的观测值。
V = {v1, v2, ..., vM},共有 M 种观测。
这两个集合的元素本身没有顺序,它们为后续的序列提供“词汇表”。
序列定义
基于上述集合,我们定义两个有顺序的序列:
- 状态序列 I:由状态集合中的元素按时间顺序组成。
I = (i1, i2, ..., iT),其中it ∈ Q。 - 观测序列 O:由观测集合中的元素按时间顺序组成。
O = (o1, o2, ..., oT),其中ot ∈ V。
在HMM中,观测序列由隐藏的状态序列决定。
模型参数(λ)
一个完整的HMM由以下三个参数决定,我们通常用 λ = (A, B, π) 表示:
状态转移概率矩阵 A:一个
N x N的矩阵,描述隐藏状态之间的转移概率。- 公式:
A = [aij],其中aij = P(it+1 = qj | it = qi)。它表示在时刻t处于状态qi的条件下,在时刻t+1转移到状态qj的概率。
- 公式:
观测概率矩阵 B:一个
N x M的矩阵,描述从隐藏状态生成观测值的概率。- 公式:
B = [bj(k)],其中bj(k) = P(ot = vk | it = qj)。它表示在时刻t处于状态qj的条件下,生成观测vk的概率。
- 公式:
初始状态概率向量 π:一个长度为
N的向量,描述初始时刻(t=1)各个状态出现的概率。- 公式:
π = (πi),其中πi = P(i1 = qi)。
- 公式:
模型图示与生成过程
HMM的生成过程可以形象地理解为:
- 根据
π选择初始状态i1。 - 根据状态
i1和矩阵B生成第一个观测o1。 - 根据状态
i1和矩阵A转移到下一个状态i2。 - 重复步骤2和3,依次生成
o2,i3,o3, ...,直到生成长度为T的观测序列。
两个基本假设
为了简化模型计算,HMM做出两个关键假设:
齐次马尔可夫性假设:当前时刻的隐藏状态只依赖于前一时刻的隐藏状态,与其他时刻的状态和观测无关。
- 公式:
P(it | i1, o1, ..., it-1, ot-1) = P(it | it-1)
- 公式:
观测独立性假设:当前时刻的观测只依赖于当前时刻的隐藏状态,与其他时刻的状态和观测无关。
- 公式:
P(ot | i1, o1, ..., it, ot-1) = P(ot | it)
- 公式:
二、概率计算问题 🧮
上一节我们明确了HMM的构成。本节中,我们来看看HMM面临的第一个核心问题:概率计算。即,在已知模型参数 λ = (A, B, π) 和观测序列 O 的情况下,如何计算该观测序列出现的概率 P(O|λ)。
直接计算 P(O|λ) 需要穷举所有可能的状态序列 I,计算量巨大。因此,我们引入前向算法,这是一种基于动态规划的高效算法。
前向概率定义
我们定义前向概率 αt(i):
- 公式:
αt(i) = P(o1, o2, ..., ot, it = qi | λ) - 含义:在给定模型
λ下,到时刻t为止的观测序列为(o1, o2, ..., ot),且时刻t的状态为qi的联合概率。
前向算法递推过程
前向算法的核心在于建立 αt(i) 的递推关系。
初始化(t=1):
- 计算在初始时刻,状态为
qi并生成观测o1的概率。 - 公式:
α1(i) = πi * bi(o1),i = 1, 2, ..., N
- 计算在初始时刻,状态为
递推(对 t = 1, 2, ..., T-1):
- 要计算
αt+1(j),可以考虑所有在时刻t可能的状态i。 - 路径是:时刻
t处于状态i(αt(i)),从i转移到j(aij),并在t+1时刻由状态j生成观测ot+1(bj(ot+1))。 - 公式:
αt+1(j) = [ Σ (i=1 to N) αt(i) * aij ] * bj(ot+1),j = 1, 2, ..., N
- 要计算
终止:
- 最终,观测序列
O的概率等于所有在时刻T以状态i结束的前向概率之和。 - 公式:
P(O|λ) = Σ (i=1 to N) αT(i)
- 最终,观测序列
通过这种递推方式,我们将指数级复杂度的计算降低到了 O(N²T) 级别。
三、学习算法问题 🤖
上一节我们解决了在已知模型下评估观测序列的问题。本节中,我们来看看更具挑战性的第二个问题:学习算法。即,如何仅通过一个观测序列 O,来估计出最可能产生该序列的模型参数 λ = (A, B, π)。
这个问题中,状态序列 I 是未知的(隐变量),因此我们需要使用EM算法(期望最大化算法)来解决这类含有隐变量的参数估计问题。
EM算法框架
EM算法用于求解不完全数据(只有观测 Y)的模型参数 θ。在HMM中:
- 观测变量
Y对应观测序列O。 - 隐变量
Z对应状态序列I。 - 参数
θ对应λ = (A, B, π)。
EM算法通过迭代两步求解:
E步(期望步):基于当前参数估计
λ,计算完全数据(O, I)的对数似然函数log P(O, I | λ)关于隐变量I的条件期望。这个期望函数称为 Q函数。- 公式:
Q(λ, λ’) = Σ_I P(I | O, λ’) * log P(O, I | λ) - 其中
λ’是上一轮迭代的参数。
- 公式:
M步(最大化步):寻找能使Q函数最大化的新参数
λ。- 公式:
λ = argmax_λ Q(λ, λ’)
- 公式:
HMM参数估计(Baum-Welch算法)
将EM算法应用于HMM,就是著名的Baum-Welch算法。经过推导,M步的参数更新公式具有直观的统计意义:
以下是参数估计的直观解释(具体推导涉及前向概率 α 和后向概率 β 等中间量):
- 初始状态概率 πi:估计为所有样本序列中,初始状态为
qi的期望频率。 - 状态转移概率 aij:估计为从状态
qi转移到qj的期望次数,除以从状态qi转移出去的总期望次数。 - 观测概率 bj(k):估计为在状态
qj下生成观测vk的期望次数,除以处于状态qj的总期望次数。
算法步骤:
- 初始化模型参数
λ(0) = (A, B, π)。 - 迭代执行:
- E步:基于当前
λ,利用前向-后向算法计算所需的各种期望统计量(如状态占用期望、转移期望等)。 - M步:利用E步计算出的统计量,根据上述直观公式更新
A,B,π,得到λ(new)。
- E步:基于当前
- 重复步骤2,直到参数
λ收敛。
四、预测算法问题(解码问题) 🔍
上一节我们学习了如何从数据中学习模型。本节中,我们来看最后一个问题:预测(解码)算法。即,在已知模型 λ 和观测序列 O 的情况下,寻找最有可能产生该观测序列的隐藏状态序列 I*。
这个问题由维特比算法解决,它是一种基于动态规划的求最优路径算法。
维特比算法
定义在时刻 t 状态为 i 的所有单条路径 (i1, i2, ..., it) 中,概率最大的那条路径的概率值为 δt(i):
- 公式:
δt(i) = max_(i1, i2, ..., it-1) P(i1, i2, ..., it=i, o1, o2, ..., ot | λ)
同时,我们需要一个变量 ψt(i) 来记录该最优路径上前一个时刻(t-1)的状态是什么。
算法递推过程:
初始化:
δ1(i) = πi * bi(o1),i = 1, 2, ..., Nψ1(i) = 0
递推(对 t = 2, 3, ..., T):
- 对每个状态
j,找到使δt-1(i) * aij最大的前一状态i。 δt(j) = max_(1≤i≤N) [δt-1(i) * aij] * bj(ot)ψt(j) = argmax_(1≤i≤N) [δt-1(i) * aij]
- 对每个状态
终止:
- 最优路径概率
P* = max_(1≤i≤N) δT(i) - 最优路径终点
iT* = argmax_(1≤i≤N) δT(i)
- 最优路径概率
路径回溯(对 t = T-1, T-2, ..., 1):
- 根据
ψ数组向前回溯,得到完整的最优状态序列。 it* = ψt+1(it+1*)
- 根据
维特比算法与前向算法结构相似,但将“求和”操作替换为“取最大值”操作,从而找到概率最大的路径(状态序列)。
总结 🎯
本节课中,我们一起学习了隐马尔可夫模型的核心内容。
- 模型定义:我们明确了HMM是一个由初始概率
π、状态转移矩阵A和观测概率矩阵B构成的三元组λ,它用于描述一个双重随机过程(隐藏状态链和观测链)。 - 三大问题:
- 概率计算:使用前向算法高效计算
P(O|λ)。 - 学习算法:在状态序列未知的情况下,使用EM算法(具体为Baum-Welch算法)从观测序列
O中学习出最优模型参数λ。 - 预测算法:使用维特比算法进行解码,找到最有可能产生观测序列
O的隐藏状态序列I。
- 概率计算:使用前向算法高效计算
- 核心工具:整个推导和计算过程依赖于概率图模型的基本概率法则(加法和乘法规则)以及HMM的两个基本假设(齐次马尔可夫性和观测独立性)。


HMM是理解概率图模型和序列建模的重要基础。虽然当前深度学习模型在很多序列任务上表现卓越,但HMM的理论价值及其与复杂模型的融合潜力依然不可忽视。
🎓 课程1447-P1:行人重识别(ReID)背景与基线方法
在本节课中,我们将要学习行人重识别(ReID)项目的基础知识。我们将从项目背景、核心概念讲起,并初步搭建一个用于ReID任务的基线模型框架。
📖 概述:什么是行人重识别?
行人重识别,简称ReID,全称为Re-identification。本质上,它是一种目标重识别技术。该技术主要分为两类:行人重识别和车辆重识别。本课程将以行人重识别作为主要讲解对象,其涉及的技术框架和代码逻辑同样适用于车辆重识别。
这项技术出现的背景是智慧城市、智慧交通和智慧安防的发展需求。传统的基于高清“枪机”的人脸识别技术存在局限性,例如对拍摄角度(如正脸)要求严格,一旦无法捕捉清晰人脸,识别就会失效。而ReID技术则通过识别行人的整体外观(如穿着、体型)来进行身份辨别,与人脸识别技术形成互补,共同构建更完善的监控识别系统。
ReID的核心问题定义:给定一个查询图像(Query),以及一个包含大量带身份标签图像的大型数据库(Gallery),目标是找到查询图像在数据库中的匹配项,从而确定查询图像中行人的身份。
核心思路:ReID本质上是一个识别问题。但由于直接进行分类识别存在困难,我们转而采用图像检索的技术手段来达到身份识别的目的。即,通过计算查询图像与图库中所有图像的相似度,按相似度排序,最相似的图像即被认为是同一个人。
🎯 ReID的价值与应用场景
ReID技术的主要价值在于能够进行轨迹的追踪和串联。

在大型商场(Shopping Mall)等场景中,通常会部署多个摄像头。每个摄像头下的检测追踪算法会给经过的行人分配一个临时ID。然而,同一个人在不同摄像头下会被赋予不同的ID,这就无法形成完整的行动轨迹。ReID的作用就是将不同摄像头下属于同一个人的所有临时ID关联起来,合并为同一个身份。

这种轨迹串联能力具有巨大的商业价值。例如,在智慧商场中,通过分析顾客的逛店轨迹、停留时长,可以构建精准的用户画像,实现个性化推荐和营销。在安防领域,公安部门可以借助ReID技术,结合“技战法”(总结的破案规律),高效追踪嫌疑人轨迹,即使其进行了伪装(如遮挡面部)。
⚙️ ReID的技术挑战与数据准备
ReID在实际应用中面临诸多挑战,包括行人姿态变化、遮挡(背包、打伞)、衣着变化(季节、款式)、光照条件差异等。这些因素都增加了图像匹配的难度。

为了研究和评估ReID算法,业界有公认的基准数据集。对于行人ReID,有三大经典数据集:Market1501、DukeMTMC-reID和CUHK03。本课程将重点使用 Market1501 数据集。对于车辆ReID,则有 VeRi-776 等数据集。这些数据集通常已对车牌等隐私信息进行了处理。

🧠 ReID系统的基本框架
一个典型的ReID系统框架包含以下步骤:
- 检测与追踪:在每个摄像头视频流中,运行目标检测(如YOLO)与多目标追踪算法,获取行人的边界框和临时ID轨迹片段。
- 特征提取:对检测到的每个行人图像,使用深度学习模型(如CNN)提取一个固定维度的特征向量。这个向量应能表征行人的外观信息。
- 特征匹配/检索:当有一个查询图像(Query)时,同样提取其特征向量,然后计算它与图库(Gallery)中所有特征向量的相似度(如余弦相似度)。
- 身份关联:根据相似度排序,返回最匹配的结果,从而将查询图像的身份与图库中已知身份关联起来。


核心公式:相似度计算可表示为 sim(Q, G_i) = f(Q) · f(G_i),其中 f(·) 是特征提取函数,· 表示向量点积。我们的目标就是训练一个好的特征提取模型 f。
💻 动手实践:搭建ReID基线模型

上一节我们介绍了ReID的系统框架,本节中我们来看看如何用代码实现一个最基础的基线模型。我们将使用Market1501数据集和MobileNet网络。

首先,我们需要准备数据和环境。



import os
import numpy as np
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
import tensorflow as tf
from tensorflow.keras import layers, models
from tensorflow.keras.applications import MobileNet

# 配置参数
IMG_HEIGHT, IMG_WIDTH = 64, 64
BATCH_SIZE = 128
EPOCHS = 100
LEARNING_RATE = 0.01
DATA_ROOT = ‘./datasets/market1501/bounding_box_train‘ # 请根据实际路径修改
# 1. 加载并解析图像路径与标签
image_paths = []
person_ids = []
for img_name in os.listdir(DATA_ROOT):
if img_name.endswith(‘.jpg‘):
img_path = os.path.join(DATA_ROOT, img_name)
# 从文件名解析身份ID,例如 “0002_C1S1_000451_03.jpg” 中的 ‘0002‘
pid = img_name.split(‘_‘)[0]
image_paths.append(img_path)
person_ids.append(pid)
# 2. 将字符串ID编码为整数
le = LabelEncoder()
person_ids_encoded = le.fit_transform(person_ids)
num_classes = len(le.classes_)
print(f“Number of person IDs: {num_classes}“)
print(f“Total images: {len(image_paths)}“)
# 3. 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(
image_paths, person_ids_encoded,
test_size=0.2, random_state=42, shuffle=True
)
print(f“Training samples: {len(X_train)}, Validation samples: {len(X_val)}“)
接下来,我们构建模型。我们将使用在ImageNet上预训练的MobileNet作为特征提取器,并在其顶部添加一个全连接层用于分类。
# 4. 构建模型
def create_model(num_classes):
# 加载预训练的MobileNet,不包括顶部分类层
base_model = MobileNet(
include_top=False,
weights=‘imagenet‘,
input_shape=(IMG_HEIGHT, IMG_WIDTH, 3),
pooling=‘avg‘ # 全局平均池化,将特征图转换为特征向量
)
# 冻结基座模型的前面一些层(可选,用于微调)
# base_model.trainable = False
# 在基础模型上添加新的分类头
x = base_model.output
x = layers.Dense(512, activation=‘relu‘)(x)
x = layers.Dropout(0.5)(x)
predictions = layers.Dense(num_classes, activation=‘softmax‘)(x)
# 构建完整模型
model = models.Model(inputs=base_model.input, outputs=predictions)
return model
model = create_model(num_classes)
model.summary() # 打印模型结构
# 5. 编译模型
model.compile(
optimizer=tf.keras.optimizers.SGD(learning_rate=LEARNING_RATE, momentum=0.9),
loss=‘sparse_categorical_crossentropy‘,
metrics=[‘accuracy‘]
)
以上代码搭建了一个ReID基线模型的核心结构。我们使用了预训练的MobileNet来提取具有判别性的特征,并通过一个全连接层学习如何根据这些特征区分不同身份的行人。在模型训练时,我们使用交叉熵损失函数来优化分类任务。
需要注意的是,这是一个非常基础的分类范式的ReID模型。在实际推理时,我们会丢弃最后的分类层,仅使用MobileNet提取的特征向量进行相似度匹配。更先进的ReID方法会使用度量学习(如三元组损失)、局部特征对齐等策略。
📝 总结
本节课中我们一起学习了行人重识别(ReID)的基础知识。
我们首先了解了ReID技术的产生背景及其在智慧安防、商业分析中的核心价值——轨迹追踪与串联。然后,我们明确了ReID的核心思路:通过图像检索的技术手段,解决行人身份识别的问题。
接着,我们探讨了ReID面临的技术挑战,并介绍了常用的基准数据集(如Market1501)。最后,我们进入实践环节,使用TensorFlow/Keras搭建了一个基于MobileNet和分类损失的基础ReID模型框架。这个框架包括了数据加载、标签编码、模型构建和编译等关键步骤。




在接下来的课程中,我们将进一步完善这个流程,实现数据加载器、模型训练与评估,并最终完成一个可以提取特征并进行图像检索的完整ReID系统。
机器学习集训营第15期 - P10:特征工程实战指南 🛠️
在本节课中,我们将深入学习机器学习中至关重要的一环——特征工程。我们将探讨如何从原始数据中提取、清洗、构造和选择对模型预测最有用的信息,从而显著提升模型性能。
概述:为什么特征工程如此重要?🤔
从本次课开始,大家会陆续接触到更贴近实际算法应用的内容。在未来的面试或工作中,除了模型理论,面试官同样会非常关注你处理实际数据的感知和应用能力。这种能力并非简单的参数调整,而是对整个数据驱动方案效果的深刻影响。
我的同事在校招面试中最爱问的一个问题就是:“你知道哪些数据处理和特征工程的方法?” 这几乎是面试中的必问题目。今天,我将为大家讲解其中的核心操作,并引导大家思考:为什么需要这些处理?哪些模型需要它们?以及它们会带来什么样的影响?
特征工程,即 Feature Engineering,在市面上很少有专门的书籍讲解,但它却是实战中的核心。大家需要更多地关注“为什么要做这个处理”以及“处理后的数据会呈现什么形态”。
一、 特征工程的定义与意义 🎯
特征指的是从原始数据中抽取出来的、对预测结果有用的信息。人工智能远没有想象中那么智能,它需要大量的人工环节。原始数据往往杂乱无章,且包含许多计算机无法直接理解的形式(如文本、类别)。
特征工程就是结合计算机知识和特定领域的专业知识,将原始数据处理成能让机器学习算法发挥更好作用的数据表达形态。它是一个偏工程和实践的过程,核心在于对数据的理解和思考。
在工业界,我们更青睐简单但效果好的模型。这类模型可控性高、线上复杂度低、可解释性强。如果我们能抽取足够好的特征,简单的模型(如逻辑回归)效果不一定比复杂的深度学习模型差。
一个优秀的数据驱动方案需要三部分技能:
- 开发技能:能够写代码实现想法和分析数据。
- 机器学习理论技能:理解模型原理,指导调参。
- 领域知识:对业务场景的深刻理解,这是特征工程的基础。
核心公式:最终模型效果 = 数据质量(特征工程) + 模型选择 + 调优
二、 特征工程的工作量占比 ⏳
在互联网公司或数据科学比赛中,大约 60%到70% 的时间会花在数据处理和特征工程上,只有约30%的时间花在模型上。因为模型的套路相对固定,而前期对数据的理解程度和特征表达的有效性,将极大程度地决定最终结果的排名上限。
一个有效的特征抽取可能带来百分位级别的提升,而单个模型的调参优化通常只在千分位波动。因此,当你的排名还在几百名时,应重点关注数据和场景;当进入前20名冲刺冠军时,才需要深入研究模型融合等技巧。
三、 数据采集与清洗 🧹
数据采集
虽然数据采集通常不由算法工程师直接完成,但我们需要思考:哪些数据对结果预估有帮助?这些数据能否采集到?
例如,在电商推荐场景中,商品在搜索结果中的“位置”特征对点击率预估非常有效。但在线上实时预估时,我们无法提前知道商品会被排在哪个位置,因此这个特征在模型训练时可用,上线时却无法获取。我们需要考虑特征的线上可获取性和计算效率。
数据清洗:垃圾进,垃圾出 🗑️
机器学习模型无法区分数据的好坏,它会学习你给它的所有信息,包括噪声和错误。因此,数据清洗至关重要,它基本决定了模型效果的上限。
脏数据示例:
- 身高超过3米的人。
- 一个月购买生活用品花费10万元的用户。
处理方法:
- 基于业务常识的直接过滤:剔除明显不可能的数据。
- 基于统计值的过滤(如分位数):例如,剔除年龄在99%分位数以上或1%分位数以下的极端值。在Pandas中,可以使用
DataFrame.quantile()函数计算分位数。
代码示例:基于分位数去除异常值
import pandas as pd
# 假设 df 是包含‘age’列的DataFrame
q_low = df[“age”].quantile(0.01) # 1%分位数
q_high = df[“age”].quantile(0.99) # 99%分位数
df_filtered = df[(df[“age”] >= q_low) & (df[“age”] <= q_high)]
四、 处理不均衡数据 ⚖️
在分类问题中,正负样本数量可能严重不均衡(如疾病患者 vs 健康人、购买用户 vs 浏览用户)。如果直接使用原始数据,模型可能会偏向多数类。
常用处理方法:
以下是几种解决样本不均衡的策略:
- 采样法
- 欠采样:从多数类中随机抽取一部分样本,使其与少数类数量接近。
- 过采样:对少数类样本进行复制或生成新样本。对于图像数据,可以通过旋转、平移等方式生成新样本;对于一般数据,可以使用 SMOTE 算法在特征空间内生成合成样本。
- 修改损失函数:在计算损失时,给少数类样本更高的权重,让模型更关注它们。例如,在逻辑回归的对数损失函数中为不同类别添加权重系数。
- 采集更多数据:尽可能获取更多的少数类样本数据。
五、 数值型特征处理 🔢
数值型特征是连续的,如年龄、价格、房龄。
1. 幅度缩放
当不同特征的数值范围差异巨大时(如房间数量:0-5,房屋均价:0-50000),对于逻辑回归、神经网络等模型,这会导致优化困难(损失函数等高线图变得又尖又窄)。我们需要将特征缩放到相似的幅度范围内。
- 归一化:对行进行处理,将样本向量转化为“单位向量”。
x_normalized = x / sqrt(x1^2 + x2^2 + ... + xn^2) - 标准化:对列进行处理,使其均值为0,标准差为1。
x_standardized = (x - mean(column)) / std(column)
在 Scikit-learn 中,使用StandardScaler。
2. 统计值特征
除了原始值,还可以生成统计特征,如:均价、最高价、最低价、25%/75%分位数价格等。可以使用 Pandas 的 describe() 或 quantile() 函数。
3. 离散化(分箱/分桶)📦
将连续值分段,每段作为一个新的特征。这是特征工程中极其重要的操作,尤其在面试中常被问到。
为什么需要离散化?
考虑“是否让座”的场景,影响因素“年龄”与结果并非单调关系(需要照顾老人和小孩)。如果直接用连续年龄送入逻辑回归,模型权重W只能是正或负,无法同时照顾两头。
解决方法:将年龄离散化为三列:“年龄<6岁”、“6岁≤年龄≤60岁”、“年龄>60岁”。这样模型可以为不同年龄段学习独立的权重,解决了非单调性问题。
离散化方法:
- 等距切分:按值域均匀分段。问题:数据分布可能不均匀,导致某些桶内数据很少。
- 等频切分:按样本数量均匀分段,使每个桶内样本数大致相同。更常用。
在 Pandas 中,等距切分用pd.cut(),等频切分用pd.qcut()。
核心要点:需要离散化的是逻辑回归、神经网络这类模型。树模型(如决策树、随机森林)天生就能处理连续值并产生分裂规则,因此不需要提前做离散化。
六、 类别型特征处理 🏷️
类别型特征是离散的,计算机无法理解,如口红色号、衣服尺码、星期几。
1. 标签编码
用数字直接映射类别,如{红:1, 绿:2, 蓝:3}。
问题:引入了不应存在的顺序关系(如蓝色“大于”绿色),可能误导模型。
2. 独热向量编码
为每个类别创建一个新的二值特征列。例如,颜色有红、绿、蓝、黄四种,就创建四列:is_red, is_green, is_blue, is_yellow。样本属于哪个颜色,对应列即为1,其余为0。
这种方法消除了顺序性,使每个类别平等。在 Pandas 中,使用 pd.get_dummies()。
3. 哈希技巧与统计编码
- 文本的词袋模型:将文本表示为一个大向量,每个维度对应一个词,出现则为1,否则为0。这丢失了词序信息。
- N-Gram:为了捕捉词序,可以同时记录单个词和连续出现的词对(bigram)、三元组(trigram)等。
- TF-IDF:更高级的文本特征,不仅考虑词频,还考虑词的逆文档频率,以衡量词对文档的重要性。
- 基于统计的编码:例如,统计“男性”和“女性”用户中“爱好”的分布比例(如:男性中2/3爱足球,1/3爱散步,0爱看剧),然后将这个分布向量作为新的特征拼接到对应性别的样本上。
七、 时间型特征处理 ⏰
时间可以当作连续值(如浏览时长、距离上次购买的天数),也可以离散化。
离散化思路:
- 一天中的时间段(如上班高峰、午休、夜晚)。
- 一周中的星期几(工作日 vs 周末)。
- 一年中的第几周(可能与特定节日相关)。
- 是否节假日,距离大型促销日(如双十一)的天数。
时间戳可以通过编程提取出年、月、日、小时、星期几等组件,这些组件通常需要作为类别型特征进一步处理(如独热编码或离散化)。
八、 特征组合与交互 🤝
单一特征可能不够,组合特征能捕捉更复杂的模式。
- 简单组合:例如,将“用户ID”和“商品类别ID”组合成一个新特征,只有当该用户浏览该类别商品时,此特征才为1。
- 基于模型组合:使用决策树、GBDT、随机森林等模型先对数据训练。树模型产生的每一条从根到叶子的路径,本质上就是一系列特征的组合条件。我们可以将这些路径或子路径作为新的组合特征。这种方法比人工拍脑袋组合更有效。
九、 特征选择 🎯
当特征维度非常高时(例如通过特征工程产出数千维),需要选择最重要的特征,以降低计算成本、防止过拟合、去除冗余。
特征选择与降维不同:特征选择是剔除不相关的特征列;降维是将高维特征映射到低维空间,试图保留大部分信息。
1. 过滤式
独立评估每个特征与目标变量的相关性(如皮尔逊相关系数),选择相关性最高的特征。
缺点:未考虑特征之间的相互作用。
Scikit-learn工具:SelectKBest, SelectPercentile
2. 包裹式
将特征选择看作一个特征子集搜索问题。典型方法是递归特征消除。
步骤:
1. 用全部特征训练一个能提供特征重要度的模型(如逻辑回归、随机森林)。
2. 剔除最不重要的特征(如后5%)。
3. 用剩下的特征重新训练模型,评估性能。
4. 重复2-3步,直到性能显著下降。
Scikit-learn工具:RFE (Recursive Feature Elimination)
3. 嵌入式
利用模型训练过程本身进行特征选择。最典型的是使用线性模型(逻辑回归、线性SVM)结合L1正则化。
原理:L1正则化具有稀疏化效应,会使许多不重要的特征的权重直接变为0。训练完成后,权重非零的特征即被选中。
适用场景:特别适用于高维稀疏特征(如经过独热编码后的特征)。对于稠密特征效果一般。
Scikit-learn工具:使用带 penalty=‘l1’ 的 LogisticRegression 或 LinearSVC,并结合 SelectFromModel。
十、 实战案例与数据分析流程 📊
特征工程离不开探索性数据分析。通过可视化(如箱线图发现异常值、折线图观察时间趋势、热力图查看特征相关性)和统计分析,我们能深刻理解数据分布,从而指导特征构造、清洗和选择。
一个完整的流程通常包括:
- 数据加载与概览。
- 缺失值分析与处理。
- 异常值检测与处理。
- 单变量与多变量分析(分布、相关性)。
- 特征构造与变换。
- 特征选择。
- 模型训练与评估。
总结 📝
本节课我们一起深入学习了机器学习的核心实战环节——特征工程。我们明确了它的定义与重要性,了解了数据清洗和样本均衡处理的必要性,并系统性地掌握了针对数值型、类别型、时间型特征的各种处理方法,如离散化、独热编码、TF-IDF等。我们还学习了如何通过特征组合创造更强大的特征,以及如何利用过滤式、包裹式、嵌入式三种方法进行特征选择,以优化模型效率与性能。


记住,好的特征工程能让简单的模型发挥出强大的威力。它需要你对数据充满好奇,对业务深入理解,并熟练运用各种数据处理工具。希望大家能将今天所学应用到实际项目和比赛中,不断积累经验。


🧠 机器学习基础与广义线性模型速讲(课程编号:1447-P11)
在本节课中,我们将学习机器学习的基本概念、术语,并深入理解广义线性模型家族中的两个核心成员:线性回归与逻辑斯蒂回归。我们将从最基础的数据表示开始,逐步构建模型,并理解其背后的优化逻辑。
📚 第一部分:机器学习基础术语


机器学习是利用经验(数据),通过计算构建模型,以改善系统自身性能(预测能力)的过程。理解以下基础术语是后续学习的基石。




属性、记录与标记



- 属性(特征):描述事物在特定方面表现或性质的事项。在结构化数据(如表格)中,通常对应表格的列。例如,在学生表中,“学号”、“姓名”、“身高”都是属性。
- 记录(样本/实例):一个具体事物的属性描述,由属性向量表示。在表格中,通常对应表格的行。例如,学生表中关于“张三”的所有信息构成一条记录。
- 标记(标签):描述事物某个特性或结果的事项,通常是结论性的数据。例如,在学生表中,“是否为三好学生”这一列就是标记。标记也对应表格中的一列。
空间与数据集
- 属性空间(输入空间):某个属性所有可能取值构成的集合。
- 标记空间(输出空间):标记所有可能取值构成的集合。
- 样例:由一条记录及其对应的标记组成的对,表示为
(x, y)。 - 数据集:记录的集合称为无监督学习数据集;样例的集合称为有监督学习数据集。根据标记的取值类型,有监督学习可进一步分为:
- 回归问题:标记为连续值。
- 分类问题:标记为离散值。其中,标记只有两个取值的称为二分类问题,多于两个的称为多分类问题。
假设空间与模型
机器学习的目标是学习一个从输入空间 X 到输出空间 Y 的映射 f。所有可能的映射 f 构成的集合称为假设空间或模型空间 F。
模型通常有两种表示形式:
- 决策函数(非概率模型):
Y = f(X),直接给出预测值。 - 条件概率分布(概率模型):
P(Y|X),给出预测为不同取值的概率。
在实际中,模型 f 通常由一个参数向量 w 决定。因此,寻找最优模型 f* 的问题,常常转化为在参数空间中寻找最优参数 w* 的问题。



⚖️ 第二部分:策略、损失函数与优化

上一节我们介绍了模型空间的概念,本节中我们来看看如何从众多可能的模型中选出“最好”的那一个。这需要定义“好”的标准,即策略。



损失函数与经验风险


损失函数 L(Y, f(X)) 用于度量模型一次预测的错误程度。值越大,错误越严重。

以下是几种常见的损失函数:

- 0-1损失:预测错误记为1,正确记为0。
L(Y, f(X)) = I(Y ≠ f(X)) - 平方损失:常用于回归问题。
L(Y, f(X)) = (Y - f(X))^2 - 绝对损失:
L(Y, f(X)) = |Y - f(X)|








对于一个包含 N 个样例的数据集 D,模型 f 的经验风险(平均损失)定义为:
R_emp(f) = (1/N) * Σ_{i=1}^{N} L(y_i, f(x_i))
一个直观的策略是经验风险最小化(ERM),即选择使经验风险最小的模型:
f* = arg min_{f ∈ F} R_emp(f)
过拟合与结构风险最小化


然而,经验风险最小化策略可能导致过拟合:模型在训练数据上表现很好(经验风险小),但在未知数据(测试集)上表现很差。这通常是因为模型过于复杂,学习了数据中的噪声而非规律。
为了解决过拟合,我们引入结构风险最小化(SRM)策略。它在经验风险的基础上,增加一个表示模型复杂度的正则化项 J(f):
R_srm(f) = R_emp(f) + λ * J(f)
其中 λ 是权衡两项重要性的系数。






常见的正则化项有:
- L2正则化(岭回归):
J(f) = (1/2) * ||w||_2^2 - L1正则化(LASSO):
J(f) = ||w||_1






最优模型变为:f* = arg min_{f ∈ F} R_srm(f)
优化算法


寻找使结构风险最小的参数 w* 是一个优化问题。当目标函数是凸函数时,常用梯度下降法进行迭代求解。


其核心迭代公式为:
w_{new} = w_{old} - α * (∂R_srm(w) / ∂w)
其中 α 是学习率(步长),∂R_srm(w) / ∂w 是梯度。我们沿着梯度的负方向(函数下降最快的方向)更新参数,直至收敛。
📈 第三部分:线性回归模型




现在,让我们将上述理论应用于第一个具体模型——线性回归。线性回归用于解决回归问题,其思想是找到属性(特征)的线性组合来预测目标值。


模型定义
给定数据集 D = {(x1, y1), (x2, y2), ..., (xN, yN)},其中 xi 是 n 维向量,yi 是连续实值。
多元线性回归模型定义为:
f(x) = w^T * x + b
其中 w 是权重向量,b 是偏置项。




为简化表示,我们进行增广:
令 ŵ = (w^T, b)^T,x̂ = (x^T, 1)^T,则模型简化为:
f(x̂) = ŵ^T * x̂
参数估计(学习)



我们采用平方损失和经验风险最小化策略。
经验风险为:
R_emp(ŵ) = (1/N) * Σ_{i=1}^{N} (y_i - ŵ^T * x̂_i)^2




求解最优参数 ŵ* 有两种主要方法:



- 最小二乘法(解析解):令梯度为零,直接求解方程。可得:
ŵ* = (X̂^T * X̂)^{-1} * X̂^T * Y
其中X̂是由所有x̂_i^T堆叠而成的设计矩阵,Y是所有y_i组成的向量。

- 梯度下降法(迭代解):更通用的方法。计算梯度:
∂R_emp(ŵ) / ∂ŵ = -(2/N) * Σ_{i=1}^{N} (y_i - ŵ^T * x̂_i) * x̂_i
然后使用迭代公式ŵ_{new} = ŵ_{old} - α * (∂R_emp(ŵ) / ∂ŵ)进行更新。
为了防止过拟合,可以在损失中加入正则化项,例如L2正则化,此时模型称为岭回归。
🔮 第四部分:逻辑斯蒂回归模型




上一节我们学习了用于回归的线性模型,本节中我们来看看如何将其改造用于分类任务,这就是逻辑斯蒂回归。




从回归到分类
线性回归的输出是连续实值 z = w^T * x + b,而二分类任务的输出是离散的 {0, 1}。我们需要一个函数将 z 映射到 (0, 1) 区间,并将其解释为概率。这个函数就是Sigmoid函数(或称逻辑函数):
σ(z) = 1 / (1 + e^{-z})
其函数图像呈S形,将整个实数轴压缩到 (0, 1) 之间。
模型定义
逻辑斯蒂回归模型定义为:
P(Y=1 | x) = σ(w^T * x + b) = 1 / (1 + e^{-(w^T * x + b)})
P(Y=0 | x) = 1 - P(Y=1 | x)
这样,我们就得到了一个基于输入 x 的条件概率分布模型。


参数估计(学习)—— 最大似然估计
对于分类问题,我们使用对数似然函数作为优化的目标(可理解为损失函数的相反数)。对于数据集 D,其对数似然函数为:
L(w) = Σ_{i=1}^{N} [y_i log P(Y=1|x_i) + (1-y_i) log P(Y=0|x_i)]
我们的目标是最大化这个似然函数(即最大似然估计,MLE)。


将概率表达式代入并化简,可以得到:
L(w) = Σ_{i=1}^{N} [y_i (w^T * x_i) - log(1 + e^{w^T * x_i})]



最大化 L(w) 等价于最小化 -L(w)。对 -L(w) 求关于 w 的梯度,得到:
∂(-L(w)) / ∂w = - Σ_{i=1}^{N} [y_i - σ(w^T * x_i)] * x_i


然后,我们同样可以使用梯度下降法(或专用于此问题的牛顿法等)来迭代求解最优参数 w*:
w_{new} = w_{old} - α * (∂(-L(w)) / ∂w)
核心关联:逻辑斯蒂回归可以看作是在线性回归的输出 z 上套了一个Sigmoid激活函数,从而将回归值转化为概率,用于分类。因此,线性回归是逻辑斯蒂回归的基础。
🎯 总结







本节课中我们一起学习了:
- 机器学习基础:明确了属性、记录、标记、数据集、假设空间等核心术语,理解了经验风险最小化与结构风险最小化策略,以及过拟合的概念。
- 线性回归:掌握了用于回归问题的线性模型定义,以及通过最小二乘法或梯度下降法进行参数估计的过程。
- 逻辑斯蒂回归:理解了如何通过Sigmoid函数将线性回归模型转化为分类模型,并学会了使用最大似然估计和梯度下降法来求解参数。




线性回归和逻辑斯蒂回归是广义线性模型家族的代表,也是许多复杂模型(如神经网络)的基础构件。理解它们的原理和关联,对于后续深入学习至关重要。


课程12:NLP模型构建与意图识别 🧠
在本节课中,我们将学习如何从零开始构建一个NLP模型,并重点探讨用于意图识别的文本分类模型。课程内容分为三个主要部分:NLP模型的基础构建流程、经典的文本分类模型(特别是TextCNN),以及Transformer模型的核心概念——自注意力机制。
第一部分:如何从零搭建NLP模型 🏗️
上一节我们介绍了智能问答系统的整体框架。本节中,我们来看看构建一个NLP深度学习模型需要哪些基本组件和步骤。
一个NLP模型的输入通常需要经过精心设计,主要包括以下几个方面:
1. 输入形式:字向量 vs. 词向量

模型的输入需要将文本转换为数值化的向量表示,即嵌入(Embedding)。这主要分为两种形式:

- 字向量(Char Embedding):将句子按字进行划分。
- 优势:词典规模小(例如,中文常用字约2万个),内存占用少,不易出现未登录词(OOV)问题。
- 劣势:语义表达能力通常弱于词向量。
- 词向量(Word Embedding):将句子按词进行划分。
- 优势:语义表达能力更强,因为词通常由多个字组成,承载了更丰富的语义信息。
- 劣势:受分词工具效果影响大;词典规模庞大(可能达10万甚至20万词),易导致OOV问题,且One-Hot编码会消耗大量显存。



在实际应用中,可以同时使用字向量和词向量,例如通过两个独立的嵌入层分别处理,然后将它们的输出进行融合(如相加或拼接)。

2. 嵌入向量的初始化方式
在构建模型的嵌入层时,有三种常见的初始化方式:
- 动态字/词向量:使用预训练好的向量(如Word2Vec)初始化嵌入矩阵,并在模型反向传播(BP)过程中更新该矩阵。
- 静态字/词向量:同样使用预训练向量初始化嵌入矩阵,但在BP过程中不更新该矩阵,只更新模型后续层的参数。
- 随机字/词向量:不进行预训练,直接随机初始化嵌入矩阵,并在BP过程中更新它。
3. 引入先验特征


除了文本本身的向量表示,我们还可以加入人工提取的特征,这类似于机器学习中的特征工程。


以下是几个例子:
- 在文本匹配任务中,可以加入两个句子的共有词数量、文本长度差、是否包含特定关键词等特征。
- 在情感分析任务中,可以加入是否出现强烈情感倾向词等特征。


这些特征可以作为额外的输入,与文本向量拼接后一同输入模型。
4. 构建词典


构建模型的第一步是创建词典,它将字或词映射到唯一的索引ID。流程如下:
- 确定输入形式:明确使用分词还是分字,并确保训练和预测时使用相同的分词器。
- 读取语料与分词:加载所有训练文本,并进行分词/分字处理。
- 统计词频与排序:统计每个词的出现频率,并按照词频从高到低排序。
- 添加特殊标记:在词典开头加入必要的特殊标记符。
- 生成词典文件:将高频词(例如,词频大于5)及其索引写入文件。
词典中通常包含以下四个关键的特殊标记位:
[PAD]:填充标记。用于将不同长度的句子补齐到同一固定长度,以便批量处理。[UNK]:未知词标记。用于替换那些未出现在词典中的词。[CLS]:起始标记。在如BERT等模型中用于表示序列的开始。[SEP]:分隔/终止标记。用于分隔句子或表示序列结束。
5. 统一输入长度
为了进行批量训练,需要将所有句子处理成相同长度。假设最大序列长度设为 max_len。
- 短句填充:对于长度小于
max_len的句子,在其末尾(或开头)添加[PAD]标记直至达到max_len。 - 长句截断:对于长度超过
max_len的句子,进行截断。可以截取前max_len个词、后max_len个词,或前后各取一部分进行拼接。
6. 使用数据生成器
当语料数据量很大时,一次性加载所有数据可能耗尽内存。因此,我们使用数据生成器(Data Generator)进行小批量(batch)加载。
在PyTorch中,这通过组合 torch.utils.data.Dataset 和 torch.utils.data.DataLoader 来实现。Dataset 负责封装数据和标签,DataLoader 负责按指定批量大小生成数据批次。
7. 使用PyTorch构建模型
在PyTorch中构建深度学习模型非常直观,主要分为两步:
- 自定义模型类:创建一个继承自
nn.Module的类。 - 重写两个方法:
__init__方法(构造方法):在此初始化模型参数,并定义模型所需要的所有网络层(如嵌入层、卷积层、全连接层等)。forward方法(前向传播方法):在此定义数据从输入到输出的完整计算流程,即如何将__init__中定义的层连接起来。
模型构建公式示例:
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self, vocab_size, embed_size):
super(MyModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.fc = nn.Linear(embed_size, num_classes)
def forward(self, x):
x = self.embedding(x) # 将词索引转换为向量
x = x.mean(dim=1) # 简单的池化:沿序列维度取平均
output = self.fc(x) # 分类层
return output
第二部分:常见的文本分类模型 📚




上一节我们介绍了NLP模型的基础构建模块。本节中,我们来看看如何将这些模块应用于具体的任务——意图识别,这本质上是一个文本分类问题。



我们将介绍五种常见的文本分类模型,它们各有适用场景。




模型概览






以下是五种文本分类模型的简要对比:
- FastText:结构极简,适合类别区分明显的短文本分类。
- TextCNN:利用多尺寸卷积核捕捉不同粒度的特征,适合短文本分类,计算效率高。
- HAN (Hierarchical Attention Network):采用层次化注意力机制,特别适合处理长文本(如文档)。
- BiLSTM + Attention:结合双向LSTM和注意力机制,是一个通用性较强的模型,对长短文本都有效。
- BERT微调:使用强大的预训练语言模型进行微调,通常能取得最佳效果,但计算成本较高。




在实际工作中,建议先从简单的模型(如TextCNN)开始尝试,若效果不满足要求再考虑使用更复杂的模型(如BERT)。



重点模型:TextCNN详解


TextCNN是处理短文本分类的高效模型。其核心思想是使用多个不同宽度(即不同词数)的卷积核,来并行地提取句子中不同局部范围的关联特征,这类似于N-gram模型从不同粒度(如字、词、短语)捕捉信息。
模型结构拆解:
- 嵌入层:输入句子被转换为词向量矩阵,形状为
[序列长度, 嵌入维度],可视为一张“特征图”。 - 卷积层:使用多个不同尺寸的卷积核(例如,宽度分别为2, 3, 4,高度等于嵌入维度)在词向量矩阵上进行一维卷积操作。每个尺寸的卷积核可以有多个,以提取更丰富的特征。
- 卷积操作:卷积核在矩阵上滑动,每次计算其覆盖区域内向量的点积之和,生成一个特征值。
- 池化层:对每个卷积核产生的特征序列进行最大池化(Max Pooling),即只保留最重要的一个特征值。
- 拼接与分类:将所有池化后的特征值拼接成一个长向量,然后通过全连接层(+ Softmax/Sigmoid)输出分类结果。
TextCNN前向传播流程公式化描述:
输入: [batch_size, seq_len]
嵌入层: -> [batch_size, seq_len, embed_dim]
卷积层(多个核): -> 多个 [batch_size, out_channels, new_seq_len]
池化层(最大池化): -> 多个 [batch_size, out_channels]
拼接: -> [batch_size, total_out_channels]
全连接层: -> [batch_size, num_classes]




代码实践:构建TextCNN模型


以下是使用PyTorch实现TextCNN的关键代码片段:
import torch
import torch.nn as nn
import torch.nn.functional as F

class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_size, num_classes, kernel_sizes=[3,4,5], num_filters=100):
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
# 多个并行的一维卷积层
self.convs = nn.ModuleList([
nn.Conv2d(1, num_filters, (k, embed_size)) for k in kernel_sizes
])
self.dropout = nn.Dropout(0.5)
self.fc = nn.Linear(len(kernel_sizes) * num_filters, num_classes)
def forward(self, x):
x = self.embedding(x) # [batch, seq_len, embed_size]
x = x.unsqueeze(1) # 增加通道维 [batch, 1, seq_len, embed_size]
# 对每个卷积核进行卷积和池化
conv_outputs = []
for conv in self.convs:
conv_out = F.relu(conv(x)).squeeze(3) # [batch, num_filters, seq_len-k+1]
pool_out = F.max_pool1d(conv_out, conv_out.size(2)).squeeze(2) # [batch, num_filters]
conv_outputs.append(pool_out)
# 拼接所有特征
x = torch.cat(conv_outputs, dim=1) # [batch, len(kernel_sizes)*num_filters]
x = self.dropout(x)
logits = self.fc(x) # [batch, num_classes]
return logits
模型训练与集成


训练过程包括:加载数据、定义损失函数和优化器、进行前向/反向传播、模型验证与保存。训练完成后,将预测函数集成到智能问答的主流程中,即可实现意图识别功能。

# 意图识别集成示例
def intent_recognition(user_input):
# 使用训练好的TextCNN模型进行预测
prob = textcnn_model.predict(user_input)
if prob > 0.5:
return "闲聊", prob
else:
return "专业问题", prob

第三部分:Transformer模型核心——自注意力机制 ⚡
上一节我们介绍了用于分类的CNN模型。本节中,我们来看看NLP领域里程碑式的模型——Transformer,并深入理解其核心组件:自注意力机制。
Transformer模型同样遵循编码器-解码器(Encoder-Decoder)架构,但其完全基于注意力机制,摒弃了循环神经网络(RNN)。
自注意力机制详解
自注意力(Self-Attention)的核心思想是:让句子中的每个词都与该句子中的所有词进行交互,从而动态地计算每个词相对于其他所有词的“重要性”权重,并基于这些权重对词向量进行加权求和,得到新的、蕴含了全局上下文信息的词表示。
计算步骤:
- 生成Q, K, V:对于输入序列的每个词向量,通过三个不同的权重矩阵(
W_Q,W_K,W_V)线性变换,得到对应的查询向量(Query)、键向量(Key)和值向量(Value)。通常,初始时Q, K, V就是输入词向量本身。 - 计算注意力分数:计算Query与所有Key的点积,得到每个词对其他词的原始注意力分数。这衡量了词与词之间的相关性。
- 缩放与归一化:将注意力分数除以
sqrt(d_k)(d_k是Key向量的维度),以防止点积结果过大导致Softmax梯度消失。然后应用Softmax函数,将分数归一化为概率分布(总和为1),即得到最终的注意力权重。 - 加权求和:将注意力权重与对应的Value向量相乘并求和,得到该词新的向量表示。
自注意力公式:
Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) ) * V



多头注意力机制
单一的注意力头可能只能捕捉到一种模式的依赖关系。为了增强模型的能力,Transformer使用了多头注意力(Multi-Head Attention)。
其做法是:
- 将Q, K, V通过多组不同的权重矩阵投影到多个子空间(即多个“头”)。
- 在每个头上独立地执行自注意力计算。
- 将所有头的输出拼接起来,再通过一个线性层进行融合和降维。
这样,模型可以并行地从不同子空间(例如,语法、语义、指代等不同方面)学习信息。
多头注意力公式:
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) * W_O
其中 head_i = Attention(Q * W_Q_i, K * W_K_i, V * W_V_i)
在模型中的应用
在Transformer的编码器中,多头自注意力层是其核心。它使模型能够无视距离地捕捉序列中任意两个位置之间的依赖关系,解决了RNN长距离依赖捕捉困难的问题。这种机制同样可以替换之前提到的HAN或BiLSTM+Attention模型中的注意力层,往往能取得更好的效果。
总结 📝
本节课中我们一起学习了构建NLP深度学习模型的完整流程,从输入处理(字/词向量、词典构建、长度统一)到使用PyTorch搭建模型。我们重点剖析了TextCNN这一高效的短文本分类模型,并通过代码实践将其应用于意图识别任务。最后,我们探讨了Transformer模型的核心——自注意力机制的原理,理解了其如何通过动态加权聚合全局信息。




通过本课的学习,你应该能够独立实现一个基础的文本分类模型,并理解现代NLP模型中最重要的注意力机制的基本思想。请务必动手完成代码实践,这是掌握这些知识的关键。






课程13:智能问答机器人中的闲聊技术进阶 🧠🤖
概述
在本节课中,我们将深入学习智能问答机器人中闲聊模块的进阶技术。课程将分为三个核心部分:首先,我们将探讨更精确的文本相似度计算模型,以优化检索式问答的准确性;其次,我们将对上节课介绍的Transformer模型进行补充讲解;最后,我们将介绍如何利用BERT等预训练模型生成高质量的句子向量(Embedding),以替代传统的词向量方法。
第一部分:文本相似度计算模型 📊
上一节我们介绍了基于词向量的基础检索方法。本节中,我们来看看如何通过更精细的文本相似度模型来提升答案选择的准确率。
在实际应用中,当用户提问时,我们通常有海量的候选问题库。直接使用精细模型对全部候选进行相似度计算会非常耗时。因此,工业界常采用“召回-精排”的两阶段策略:
- 召回阶段:使用快速但相对粗糙的方法(如词向量余弦相似度)从海量问题中筛选出Top K个最相关的候选。
- 精排阶段:使用更复杂、更精确的文本相似度模型,对这K个候选进行精细打分,选出最终答案。
文本相似度模型的核心任务是判断两个句子是否为同义句。这类模型通常采用孪生网络结构。

孪生网络结构
孪生网络包含两个结构相同、权重共享的子网络。每个子网络处理一个输入句子,输出一个句子向量。然后,模型会计算这两个向量的某种关系(如拼接、相减等),最后通过一个分类层(如Sigmoid)输出一个0到1之间的相似度分数。




公式表示如下:
- 对于句子A和B,分别通过共享权重的编码器
Encoder:
vector_A = Encoder(A)
vector_B = Encoder(B) - 计算交互特征,例如:
[vector_A, vector_B, vector_A - vector_B, vector_A * vector_B] - 将交互特征送入分类层,得到相似度分数:
similarity_score = Classifier(interaction_features)


我们将重点介绍一个效果不错且结构清晰的模型:ESIM。


ESIM模型详解
ESIM模型结构清晰,速度快,适合工业部署。其核心结构可分为四层:




1. 输入编码层
- 作用:将输入的句子转换为初步的上下文感知向量。
- 结构:词嵌入层 + 双向LSTM。
- 输入:句子的词索引序列。
- 输出:每个时间步的隐藏状态序列。




2. 局部推理建模层
- 作用:捕捉两个句子间的交互信息。
- 核心操作:使用注意力机制计算两个句子向量序列间的软对齐。
- 计算注意力权重矩阵
E:E = softmax(A * B^T),其中A、B分别为两个句子的向量序列。 - 使用权重矩阵对原序列进行加权,得到新的对齐表示
A'和B'。
- 计算注意力权重矩阵
- 增强交互:将原始表示与对齐表示进行组合(如拼接、相减、点乘),生成丰富的交互特征向量。

3. 推理组合层
- 作用:进一步融合局部推理信息。
- 结构:另一个双向LSTM,用于处理上一步得到的增强交互特征。



4. 预测层
- 作用:聚合信息并输出最终相似度。
- 操作:
- 池化:对LSTM最后一个时间步的输出序列,在时间维度上进行最大池化和平均池化。
- 分类:将池化后的向量拼接,送入全连接层和Sigmoid函数,输出0-1之间的相似度值。










第二部分:Transformer模型补充 🔄
上一节我们介绍了Transformer的Encoder部分。本节中,我们来看看完整的结构以及其他关键组件。




Encoder的完整结构
除了上节课讲的多头自注意力机制,Encoder层还包含以下重要部分:
1. 位置编码
- 问题:Transformer不像RNN那样按顺序处理输入,因此本身不具备位置信息。
- 解决方案:为每个词的位置生成一个独特的向量,并与词嵌入向量相加。
- 公式:在原始Transformer中,使用正弦和余弦函数生成固定位置编码:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中pos是位置,i是维度索引。
2. 残差连接与层归一化
- 残差连接:将某一层的输入直接加到该层的输出上。这有助于缓解深层网络中的梯度消失问题,使训练更稳定。公式为:
Output = Layer(x) + x。 - 层归一化:对单个样本的所有特征进行归一化(均值为0,方差为1),并引入可学习的缩放和平移参数。这有助于稳定训练过程,防止激活值进入饱和区。
- 公式:
LN(x) = γ * ( (x - μ) / √(σ^2 + ε) ) + β
其中μ是均值,σ^2是方差,γ和β是可学习参数,ε是极小值防止除零。
- 公式:



3. 前馈神经网络
- 结构:两个线性变换中间夹一个ReLU激活函数。
- 公式:
FFN(x) = max(0, xW1 + b1)W2 + b2
BERT:基于Transformer的预训练模型
BERT的本质是一个多层Transformer Encoder的堆叠(Base版12层,Large版24层)。它通过在大规模无标注语料上进行预训练,学习通用的语言表示。
BERT的输入表示
BERT的输入是三个嵌入向量的和:
- 词嵌入:每个词的向量表示。
- 段嵌入:用于区分句子对中的两个句子(如[0,0,...]代表第一句,[1,1,...]代表第二句)。
- 位置嵌入:BERT使用可学习的位置嵌入矩阵,而非Transformer的固定三角函数式编码。
BERT的预训练任务
- 掩码语言模型:随机遮盖输入中15%的单词,让模型根据上下文预测被遮盖的词。为了解决预训练与微调时的不匹配,被遮盖的词80%替换为
[MASK],10%替换为随机词,10%保持不变。 - 下一句预测:给定两个句子A和B,判断B是否是A的下一句。这帮助模型理解句子间关系。
第三部分:基于BERT的句子向量生成 🧬
之前我们更多使用静态词向量。本节中,我们来看看如何利用先进的预训练模型生成更强大的句子向量。
直接使用BERT的[CLS]标记的输出或对词向量取平均作为句子向量,在相似度计算任务上效果可能不理想。原因在于:
- 空间非标准正交:余弦相似度在标准正交基下才有明确的几何意义,而BERT的输出空间未必满足此条件。
- 词频影响分布:高频词的向量倾向于聚集在原点附近,低频词则分散在远处。直接平均会受此分布影响,不能很好表征句子整体语义。
解决方案:Sentence-BERT
一种有效的监督学习方法是通过孪生网络/三元组网络结构对BERT进行微调。
模型结构:
- 将两个句子分别输入同一个BERT模型。
- 获取各自的句子表示向量(如对BERT输出做池化)。
- 计算两个向量
u和v的余弦相似度。 - 使用真实标签(1表示相似,0表示不相似)与预测相似度计算损失(如均方误差),并反向传播更新BERT参数。





损失函数示例(均方误差):
Loss = (cosine_similarity(u, v) - label)^2
经过在句子对匹配数据集上微调后,BERT生成的句子向量在语义相似度计算任务上表现显著提升。




总结 🎯





本节课我们一起学习了智能问答机器人中闲聊技术的三个进阶方向:
- 文本相似度模型:深入理解了ESIM这一高效的孪生网络模型,它通过LSTM和注意力机制实现精细的句子匹配,可用于“精排”阶段。
- Transformer补充:完整了解了Transformer Encoder的残差连接、层归一化等组件,并掌握了BERT预训练模型的基本原理和输入结构。
- BERT句子向量:认识到直接使用BERT输出的局限性,并学习了通过Sentence-BERT等有监督方法微调BERT以获得高质量句子向量的思路。
实践建议:
- 尝试实现ESIM模型,并将其集成到你的问答系统中,替换掉简单的余弦相似度排序。
- 探索使用Sentence-BERT或类似方法生成句子向量,替代项目中的词向量平均方法,以提升召回效果。


通过掌握这些技术,你将能够构建更准确、更强大的智能对话系统。
课程14:决策树与Boosting模型融合精髓速讲 🌳➡️🚀
在本节课中,我们将要学习机器学习中两大核心内容:决策树模型与基于决策树的集成学习(Boosting)。我们将从决策树的基本原理出发,理解其如何通过树形结构进行决策,并探讨如何通过集成多个“弱”决策树模型来构建一个强大的“强”模型。
一、 决策树模型概述
决策树模型是一类以树形结构为基础的机器学习模型,主要用于解决分类问题,经过改造后也可用于回归任务。其核心思想是模仿人类做决策的过程:根据特征的重要性顺序,依次对数据进行判断,最终到达一个结论(叶子节点)。
上一节我们回顾了机器学习的基本概念,本节中我们来看看如何用树形结构来构建模型。
决策树模型的构建主要包含三个关键步骤:
- 特征选择:决定使用哪个特征来对数据进行划分。
- 决策树生成:根据特征选择的结果,递归地构建树形结构。
- 决策树剪枝:对生成的树进行简化,防止模型过于复杂(过拟合)。
二、 特征选择:信息论基础
为了对特征进行重要性排序,我们需要一个量化的标准。这里引入了信息论中的概念。
2.1 熵(Entropy)
熵用来度量一个随机变量的不确定性。不确定性越大,熵值越高。
公式:
假设随机变量 (X) 有 (n) 个可能的取值,其概率分布为 (P(X=x_i)=p_i),则 (X) 的熵 (H(X)) 定义为:
[
H(X) = -\sum_^ p_i \log p_i
]
理解:例如,抛一枚均匀硬币,正反面概率各为1/2,不确定性最大,熵值最高。如果一枚硬币总是正面朝上,则结果是确定的,熵值为0。
2.2 条件熵(Conditional Entropy)
条件熵表示在已知一个随机变量 (X) 的条件下,另一个随机变量 (Y) 的不确定性。
公式:
[
H(Y|X) = \sum_^ p_i H(Y|X=x_i)
]
其中 (p_i = P(X=x_i))。
理解:例如,迎面走来两个人,如果你认识其中一人((X) 确定),那么你对另一人((Y))的不确定性就会降低。
2.3 信息增益(Information Gain)
信息增益表示由于特征 (X) 的确定,使得类别 (Y) 的不确定性减少的程度。信息增益越大,说明该特征对分类的贡献越大。
公式:
特征 (A) 对训练数据集 (D) 的信息增益 (g(D, A)) 定义为:
[
g(D, A) = H(D) - H(D|A)
]
即,数据集 (D) 的熵减去特征 (A) 给定条件下 (D) 的条件熵。
计算步骤:
- 计算数据集 (D) 的熵 (H(D))。
- 计算特征 (A) 给定条件下数据集 (D) 的条件熵 (H(D|A))。
- 两者相减得到信息增益 (g(D, A))。
三、 决策树生成算法
有了特征选择的依据(如信息增益),就可以递归地构建决策树。以下是经典的ID3算法流程。
算法输入:训练数据集 (D),特征集 (A),阈值 (\epsilon)。
算法输出:决策树 (T)。
以下是ID3算法的核心步骤:
- 检查终止条件1:若 (D) 中所有实例都属于同一类 (C_k),则 (T) 为单节点树,并将类 (C_k) 作为该节点的类标记,返回 (T)。
- 检查终止条件2:若特征集 (A) 为空,则 (T) 为单节点树,并将 (D) 中实例数最多的类 (C_k) 作为该节点的类标记,返回 (T)。
- 选择最优特征:否则,计算 (A) 中每个特征对 (D) 的信息增益,选择信息增益最大的特征 (A_g)。
- 检查终止条件3:如果 (A_g) 的信息增益小于阈值 (\epsilon),则 (T) 为单节点树,并将 (D) 中实例数最多的类 (C_k) 作为该节点的类标记,返回 (T)。
- 划分数据集:否则,对 (A_g) 的每一个可能取值 (a_i),将 (D) 分割为若干非空子集 (D_i),将 (D_i) 中实例数最多的类作为标记,构建子节点。
- 递归构建:对第 (i) 个子节点,以 (D_i) 为训练集,以 (A - {A_g}) 为特征集,递归地调用步骤(1)-(5),得到子树 (T_i),返回 (T_i)。
说明:C4.5算法与ID3流程基本相同,唯一区别在于它使用信息增益比(Information Gain Ratio)而非信息增益作为特征选择标准,以校正信息增益对取值数目较多的特征的偏好。
四、 决策树剪枝
决策树生成算法可能产生过于复杂的树,导致过拟合。剪枝通过简化树结构来提高泛化能力。
剪枝通常通过最小化决策树整体的损失函数来实现。
损失函数定义:
设树 (T) 的叶节点个数为 (|T|),叶节点 (t) 有 (N_t) 个样本点,其中属于类 (k) 的样本点有 (N_) 个,(H_t(T)) 为叶节点 (t) 上的经验熵,(\alpha \geq 0) 为参数。
决策树 (T) 的损失函数 (C_\alpha(T)) 定义为:
[
C_\alpha(T) = \sum_^{|T|} N_t H_t(T) + \alpha |T|
]
其中,(H_t(T) = -\sum_k \frac{N_} \log \frac{N_})。
理解:损失函数由两部分组成:
- (\sum N_t H_t(T)):模型对训练数据的拟合程度。
- (\alpha |T|):模型复杂度惩罚项。(\alpha) 越大,树结构越简单。
剪枝算法:
输入:生成算法产生的整个树 (T),参数 (\alpha)。
输出:修剪后的子树 (T_\alpha)。
- 计算每个节点的经验熵。
- 递归地从树的叶节点向上回缩。
- 设一组叶节点回缩到其父节点之前与之后的整体树分别为 (T_B) 与 (T_A),其对应的损失函数值分别是 (C_\alpha(T_B)) 与 (C_\alpha(T_A))。
- 如果 (C_\alpha(T_A) \leq C_\alpha(T_B)),则进行剪枝,将父节点变为新的叶节点。
- 返回(2),直至不能继续为止。
五、 CART树:分类与回归
CART(Classification and Regression Trees)树是另一类广泛使用的决策树,可用于分类和回归。
5.1 回归树
回归树将输入空间划分为 (M) 个单元 (R_1, R_2, ..., R_M),每个单元有一个固定的输出值 (c_m)。
回归树模型 (f(x)) 表示为:
[
f(x) = \sum_^ c_m I(x \in R_m)
]
其中 (I) 是指示函数。
生成算法核心:如何选择最优切分特征 (j) 和切分点 (s)?
遍历所有特征和所有可能的切分点,选择使以下平方误差最小的 ((j, s)) 对:
[
\min_{j, s} \left[ \min_ \sum_{x_i \in R_1(j,s)} (y_i - c_1)^2 + \min_ \sum_{x_i \in R_2(j,s)} (y_i - c_2)^2 \right]
]
其中,(R_1) 和 (R_2) 是根据 ((j, s)) 划分的两个区域,(c_1) 和 (c_2) 分别是这两个区域内所有样本输出值的平均值。
5.2 分类树
CART分类树使用基尼指数(Gini Index) 作为特征选择标准。
数据集 (D) 的基尼指数定义为:
[
\text(D) = 1 - \sum_^ \left( \frac{|C_k|}{|D|} \right)^2
]
特征 (A) 条件下集合 (D) 的基尼指数定义为:
[
\text(D, A) = \frac{|D_1|}{|D|} \text(D_1) + \frac{|D_2|}{|D|} \text(D_2)
]
选择使基尼指数最小的特征及其切分点作为最优划分属性。
六、 集成学习与Boosting
集成学习通过结合多个学习器来获得比单一学习器更优越的性能。“三个臭皮匠,顶个诸葛亮”是其核心思想。Boosting是集成学习的主要流派之一。
6.1 加法模型(Additive Model)
Boosting方法可以表示为加法模型:
[
f(x) = \sum_^ \beta_m b(x; \gamma_m)
]
其中:
- (b(x; \gamma_m)) 是基学习器(如决策树)。
- (\beta_m) 是基学习器的系数。
- (\gamma_m) 是基学习器的参数。
6.2 前向分步算法(Forward Stagewise Algorithm)
直接优化整个加法模型是复杂的。前向分步算法采用贪心策略,每一步只学习一个基学习器及其系数。
算法流程:
- 初始化 (f_0(x) = 0)。
- 对 (m = 1, 2, ..., M):
- 极小化损失函数 (L),求解参数 (\beta_m, \gamma_m):
[
(\beta_m, \gamma_m) = \arg \min_{\beta, \gamma} \sum_^ L(y_i, f_(x_i) + \beta b(x_i; \gamma))
] - 更新模型:(f_m(x) = f_(x) + \beta_m b(x; \gamma_m))。
- 极小化损失函数 (L),求解参数 (\beta_m, \gamma_m):
- 得到加法模型:(f(x) = f_M(x))。
理解:每一步都在当前模型 (f_(x)) 的基础上,增加一个新的基学习器,来拟合之前模型未能完美解释的部分(残差)。
6.3 提升决策树(Boosting Decision Tree)
以决策树为基学习器的Boosting方法称为提升决策树(BDT)。模型为:
[
f_M(x) = \sum_^ T(x; \Theta_m)
]
这里通常假设每棵树的权重相同((\beta_m = 1))。
当损失函数为平方误差时,前向分步算法的每一步变得非常直观:
[
L(y, f_(x) + T_m(x)) = [y - f_(x) - T_m(x)]^2 = [r - T_m(x)]^2
]
其中 (r = y - f_(x)) 是当前模型的残差。
重要结论:在平方损失下,每一步需要拟合的基学习器 (T_m(x)),其目标不再是原始输出 (y),而是当前模型 (f_(x)) 的预测残差 (r)。这使得Boosting能够通过不断修正错误来提升模型性能。
总结
本节课中我们一起学习了决策树与Boosting模型融合的核心精髓。
- 决策树:我们学习了ID3、C4.5和CART树,理解了特征选择(信息增益、信息增益比、基尼指数)、树的生成与剪枝过程。决策树的本质是对输入空间的划分。
- Boosting集成:我们掌握了加法模型和前向分步算法的思想,了解了如何通过集成多个弱学习器(特别是决策树)来构建强学习器。关键点在于,Boosting通过不断拟合当前模型的残差来逐步提升性能。


这些内容是理解后续更强大的集成模型(如GBDT、XGBoost)的坚实基础。下一节课,我们将深入探讨梯度提升决策树(GBDT)的详细原理。




课程 1447-七月在线-机器学习集训营15期 - P15:08-NLP-4-智能问答机器人项目的部署、总结 📚🤖


在本节课中,我们将学习智能问答机器人项目的最后一部分,包括生成式闲聊模型的核心原理、项目整合与部署方法,并对整个项目进行总结。
第一部分:Sequence-to-Sequence 模型详解 🧠
上一节我们介绍了检索式问答模型。本节中,我们来看看用于生成式闲聊的 Sequence-to-Sequence 模型。
Sequence-to-Sequence 模型用于对序列的 item 进行建模。例如,文本数据由一个个词或字组成,这些词或字被称为 item。模型接收一个输入序列,并输出一个目标序列。
该模型的核心结构分为两个部分:编码器(Encoder)和解码器(Decoder)。
- 编码器:处理输入序列。它将输入值经过一系列特征提取,得到一个上下文向量(context vector)。
- 解码器:接收编码器输出的上下文向量,并逐个生成新的输出序列。
上下文向量的维度取决于编码器的输出。如果编码器使用 RNN 结构,其隐藏状态(hidden state)的维度就是上下文向量的维度。
Sequence-to-Sequence 模型属于多模态模型,可以处理不同类型的数据转换,例如:
- 文本到文本:机器翻译
- 图片到文本:看图说话
- 音频到文本:语音识别
- 文本到图片:根据描述生成图片
训练过程与预测过程的区别
我们以中英翻译为例,说明训练和预测的区别。
训练过程:
模型已知输入(如中文句子)和输出(如英文句子)。在训练时,解码器的输入会在目标句子前添加一个起始符(如 <S>),并让输出与输入错开一个位置。模型的目标是预测出偏移后的序列,包括最后的结束符(如 </S>)。


预测过程:
模型只有编码器的输入。解码器首先接收一个起始符和上下文向量,预测出第一个词。然后,将这个预测出的词作为下一个时间步的输入,依次循环,直到模型预测出结束符,整个过程结束。
核心公式:
在训练阶段,我们希望最大化目标序列的条件概率:
P(y1, y2, ..., yT | x1, x2, ..., xS)
其中 x 是输入序列,y 是目标序列。模型通过编码器-解码器结构来学习这个分布。


关键点:必须区分训练和预测过程。训练时解码器有完整的输入(起始符+目标序列),而预测时解码器只能依赖已生成的词和起始符进行自回归生成。
第二部分:基于 GPT 的闲聊模型 🤖💬
理解了基础的 Seq2Seq 模型后,本节我们来看看基于 Transformer 解码器的 GPT 模型如何用于闲聊。
GPT 模型与之前介绍的 BERT 模型(属于 Transformer 编码器)不同。GPT 的核心是使用了 Masked Multi-Head Attention 的 Transformer 解码器。
Masked Multi-Head Attention 的作用
在标准的 Attention 中,每个词可以关注到序列中所有词的信息。但在文本生成任务中,当前时刻的预测只能依赖于已生成的上文,不能“偷看”未来的词,否则会造成标签泄漏。
Masked Multi-Head Attention 通过一个掩码矩阵来解决这个问题。该矩阵将当前词之后所有位置的注意力权重设置为零,从而确保模型在生成每个词时,只关注它前面的词。
实现思路:
生成一个下三角矩阵(对角线及左侧为1,右侧为0),与计算出的注意力权重矩阵进行对位相乘,将“未来”位置的权重归零。
GPT 与 BERT 的区别




- BERT:使用 Transformer 编码器,通过掩码随机单词进行训练,旨在理解上下文。
- GPT:使用 Masked Transformer 解码器,通过给定上文预测下一个词进行训练,旨在生成文本。
GPT 是一个自回归语言模型,给定一个起始符,它可以续写出一段文本。
DialogGPT:用于对话的 GPT
DialogGPT 的思路是将多轮历史对话拼接起来,作为模型的输入,让模型生成下一句回复。
训练:使用大量的对话数据,将历史对话和回复拼接后输入 GPT 模型进行训练。
预测:将当前对话历史输入模型,模型会生成一个回复。然后将这个回复加入历史,继续下一轮生成。
对于资源有限的开发者,可以使用开源的预训练 DialogGPT 模型,无需从头训练。
文本生成的解码策略
模型输出时,会对词典做 softmax 得到每个词的概率。如何根据概率选择输出的词,有不同的解码策略。
1. Greedy Search(贪心搜索)
每次选择当前概率最高的词。
- 缺点:可能不是全局最优解,容易生成重复、枯燥的文本。

2. Beam Search(束搜索)
每次保留概率最高的 top-k 个候选序列(k 为束宽),在下一步中为这些候选序列分别扩展,始终保留整体概率最高的 k 个序列。
- 优点:比贪心搜索更可能找到全局更优解。
- 缺点:计算量更大;在闲聊等任务中可能过于刻板,缺乏多样性。
3. 采样(Sampling)
为了增加生成文本的多样性,可以采用基于概率的采样。
- Top-k Sampling:从概率最高的 k 个词中,根据其概率分布进行采样。
- Top-p Sampling (Nucleus Sampling):从累积概率达到 p 的最小词集合中,根据其概率分布进行采样。
在闲聊任务中,通常使用采样策略来获得更有趣、更多样的回复。


第三部分:项目整合与部署 🚀
前面我们分别完成了分类、检索和闲聊模块。本节中,我们来看看如何将它们整合成一个完整的流程,并进行简单的部署。
项目流程整合
完整的智能问答流程如下:
- 意图识别:用户问题输入后,首先用分类模型判断是“闲聊”还是“专业问题”。
- 闲聊处理:若为闲聊,则调用 DialogGPT 模型生成回复。
- 专业问题处理:若为专业问题,进入检索式流程:
- 召回:使用词向量计算余弦相似度,从问答库中召回最相似的 Top-K 个问题。
- 精排:将召回的 K 个问题与用户问题配对,输入 ESIM 等深度匹配模型进行精细排序,选出最相似的问题。
- 返回答案:返回精排后最相似问题对应的答案。



以下是核心代码逻辑的示意:
# 伪代码示意
user_input = get_user_input()
intent = intent_classifier.predict(user_input)
if intent == "闲聊":
response = dialog_gpt.chat(user_input, history)
else:
# 检索式问答
recalled_questions = retrieve_topk_with_word2vec(user_input, k=10)
best_match_index = esim_model.rank(user_input, recalled_questions)
response = answer_library[best_match_index]
return response
项目部署简介
一个简单的服务化部署方案是:Flask + Gunicorn + Docker。
- Flask:一个轻量级 Python Web 框架,用于快速构建提供问答接口的 API 服务。
- Gunicorn:一个 Python WSGI HTTP 服务器。使用多进程/多 worker 模式,可以提高服务的并发处理能力,避免请求排队。
- Docker:容器化技术。将应用程序及其所有依赖项打包成一个镜像,可以在任何支持 Docker 的环境中一键部署和运行,保证了环境的一致性,极大简化了多服务器部署的流程。

Flask API 示例:
from flask import Flask, request
from your_qa_pipeline import qa_pipeline
app = Flask(__name__)
@app.route('/qa', methods=['GET'])
def answer_question():
user_text = request.args.get('text', '')
answer = qa_pipeline(user_text) # 调用整合好的问答流程
return answer


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
运行后,可通过访问 http://服务器IP:5000/qa?text=你的问题 来获取答案。




第四部分:项目总结与展望 🎯


本节课中我们一起学习了智能问答机器人项目的收官部分。





我们回顾了整个项目的架构:通过意图识别分流,闲聊场景使用生成式模型(如 GPT),专业问答场景使用检索式模型(召回+精排)。我们深入探讨了 Seq2Seq 和 GPT 模型的原理,以及文本生成的解码策略。最后,我们将所有模块整合,并介绍了使用 Flask 进行服务化部署的基本方法。





项目优化方向


这个项目提供了一个完整的基线系统,在实际应用中还有许多优化空间:
- 意图识别:可以识别更多意图(如问候、查询、投诉等),并可尝试 BERT、TextCNN 等更强大的分类模型。
- 检索模块:
- 召回:将简单的词向量替换为 Sentence-BERT 等句子向量模型,召回效果会更好。
- 精排:可以尝试 BERT Cross-Encoder、SimCSE 等更先进的语义匹配模型。
- 生成模块:可以尝试 T5、BART 等其他先进的生成模型,或在自己的领域数据上对 DialogGPT 进行微调。
- 部署:考虑性能、监控、日志、负载均衡等生产级需求。
给算法工程师的建议
- 复现能力:能够阅读并复现论文是核心能力。可以从阅读开源代码开始,结合论文理解原理。
- 持续学习:AI 领域发展迅速,需要持续关注前沿技术(如大模型、提示工程等),并思考如何应用于业务。
- 业务结合:理解业务需求比单纯追求模型复杂度更重要。最好的模型不一定是最复杂的,而是最适合当前业务场景和数据条件的。


希望本课程能帮助你建立起构建一个完整 AI 项目的思维框架和实践能力。祝你在学习和工作的道路上不断进步!
🛍️ 课程名称:移动电商商品推荐算法实战 - 第1课:商品推荐系统介绍与环境搭建






在本节课中,我们将学习移动电商商品推荐系统的基础知识,了解我们将要完成的核心任务,并完成必要的环境配置。

📋 项目概述与任务介绍
本项目是移动电商商品推荐算法的实战。推荐系统对用户推荐商品的质量好坏,直接影响了电商平台的成交额与发展。对于阿里巴巴、京东、拼多多等电商平台而言,个性化的流量分发(即推荐系统)是公司算法团队的核心工作。
在本项目中,我们将使用阿里巴巴淘宝电商的公开数据集,根据用户在商品全集上的移动端行为数据以及百万级别的商品信息,构建一个推荐模型,以预估用户在接下来一天对商品子集的购买行为。
我们将涉及多种模型,包括协同过滤、逻辑回归、FM、GBDT以及深度神经网络模型。后续课程会详细回顾这些模型的原理、公式及代码实现。
🎯 课程主要内容与安排
上一节我们介绍了项目的整体目标,本节中我们来看看课程的具体安排。
整个项目主要包含以下内容:
- 电商商品推荐任务、数据集及评估指标介绍。
- 环境配置与安装。
- 特征处理与特征构建。
- 基于规则的预估方法。
- 基于K-Means、逻辑回归、GBDT等模型的实战。
- 深度神经网络推荐算法(Wide & Deep, NFM)实战。
- 推荐系统整体架构扩展介绍。
以下是课程分阶段的详细任务安排:
- 第一阶段:介绍项目任务书、淘宝App数据集、天池大数据比赛平台、赛题与评估指标。
- 第二阶段:环境配置、任务分析理解、特征处理与构建、训练/测试样本处理、基于规则的预估方法实现与提交。
- 第三阶段:协同过滤、K-Means聚类与采样、逻辑回归模型、GBDT模型的介绍与实战,并进行模型预估结果的提交评测。
- 第四阶段:深度神经网络推荐算法,重点介绍Wide & Deep和NFM模型及其代码实现。
- 第五阶段:从工程角度扩展介绍推荐系统的整体架构。
📊 数据集与任务详解
上一节我们了解了课程安排,本节中我们深入看看我们将要使用的数据集和具体任务。
我们将主要使用“天池新人实战赛”中的“移动推荐算法”数据集。该比赛持续在线,便于提交评测。数据集包含两部分:
- 用户行为表 (
user_behavior):记录了用户在商品全集上的移动端行为。 - 商品子集表 (
item_subset):列出了需要预测的商品集合。

以下是用户行为表的核心字段说明:

user_id:用户唯一标识(脱敏后)。item_id:商品唯一标识(脱敏后)。behavior_type:用户行为类型。1=浏览,2=收藏,3=加购物车,4=购买。item_category:商品所属类目。time:行为发生时间(精确到小时)。


我们的核心任务:利用用户在11月18日至12月18日期间(约一个月)的行为数据(表1),预测他们在12月19日会购买商品子集(表2)中的哪些商品。


提交格式:预测结果需要提交一个包含两列的文件,格式为 user_id, item_id,每一行代表预测该用户将在目标日购买该商品。
⚖️ 评估指标:F1 Score
为了评价模型预测结果的好坏,我们需要理解评估指标。本任务采用精确度、召回率以及F1值作为评估指标。
- 精确度:预测正确的购买数量占所有预测购买数量的比例。
公式:Precision = |Prediction Set ∩ Reference Set| / |Prediction Set| - 召回率:预测正确的购买数量占所有真实购买数量的比例。
公式:Recall = |Prediction Set ∩ Reference Set| / |Reference Set| - F1值:精确度和召回率的调和平均数,用于综合评估模型性能。
公式:F1 = 2 * Precision * Recall / (Precision + Recall)
F1值是本项目唯一的最终评测标准。在天池平台的排行榜上,队伍将依据F1值从高到低进行排名。



💻 环境配置与课后任务




上一节我们明确了任务和评估标准,本节中我们来看看如何搭建实践环境并布置课后任务。

我们将使用Python作为主要开发语言,需要安装以下关键包:
pandas,numpy:用于数据处理和科学计算。scikit-learn:用于传统机器学习模型。tensorflow:用于第四阶段的深度学习模型(可稍后安装)。
环境配置建议:推荐使用Anaconda来管理Python环境,它预装了数据科学所需的许多包,包括pandas, numpy和scikit-learn,可以简化安装过程。
以下是本节课后需要完成的任务:
- 下载数据集:注册并登录天池平台,报名“移动推荐算法”比赛,下载数据集并查看数据内容。
- 配置开发环境:安装Anaconda(或配置好Python环境),并确保
pandas,numpy,scikit-learn可用。 - 阅读论文:学有余力者,建议阅读谷歌的经典论文《Wide & Deep Learning for Recommender Systems》,有助于深入理解工业级推荐系统。
🏗️ 扩展知识:推荐系统架构概览
在深入代码之前,了解工业界推荐系统的工作流程很有帮助。通常,一个完整的推荐系统Pipeline包含三个主要部分:

- 数据生成:利用用户行为日志和商品信息,构建模型训练所需的样本和特征。
- 模型训练:使用训练数据训练推荐模型。实践中常采用增量训练,即用前一天的模型参数初始化当天的训练,以提高效率和质量。训练后需进行模型验证。
- 模型服务:将验证通过的模型部署到线上服务端,供推荐引擎实时调用进行打分和排序。



在线推荐流程可简化为“召回”与“排序”两步:先从海量商品中快速检索出少量相关商品(召回),再使用复杂模型对这些商品进行精确打分排序(排序)。本项目重点聚焦于“排序”阶段的模型构建。
📝 总结与建议
本节课中我们一起学习了移动电商推荐系统项目的基本轮廓。我们明确了预测用户购买行为的核心任务,了解了所使用的数据集结构和评估指标F1 Score,并布置了环境配置的课后作业。

对于有志于成为算法工程师的同学,请记住两点核心:深入理解算法模型原理与深刻理解业务需求。工程能力(如编程语言、框架使用)是实现想法的工具,“会用即可”,应将更多精力投入到算法和业务本身,打好数学和机器学习基础。




下节课我们将开始进行具体的代码实战,请大家务必完成课后准备任务。

机器学习集训营第15期 - P17:朴素贝叶斯与SVM模型精髓速讲 📚
在本节课中,我们将学习两个在机器学习领域至关重要的模型:支持向量机(SVM)和朴素贝叶斯。我们将从SVM的硬间隔、软间隔、核技巧等核心概念讲起,然后探讨朴素贝叶斯模型的基本原理、条件独立性假设及其应用。课程内容力求简单直白,让初学者能够轻松理解。
概述 📖
本节课我们将深入探讨支持向量机(SVM)和朴素贝叶斯模型。SVM是一种强大的分类模型,尤其擅长处理高维数据和非线性问题。朴素贝叶斯则是一种基于概率论的分类方法,简单高效,常用于文本分类等场景。我们将从基本原理出发,逐步讲解它们的核心思想、数学推导以及实际应用。
第一部分:支持向量机(SVM)模型精髓 🧠
上一节我们介绍了决策树模型,本节中我们来看看支持向量机(SVM)。SVM在深度学习兴起之前,是机器学习领域最重要的模型之一。它不仅在理论上具有重要意义,在实际工作中也常作为性能评估的基准线。
1. 硬间隔支持向量机(线性可分)
首先,我们需要明确SVM要解决的问题。给定一个线性可分的数据集 T = {(x1, y1), (x2, y2), ..., (xn, yn)},其中 xi ∈ R^n 是特征向量,yi ∈ {+1, -1} 是类别标签。SVM的目标是找到一个最优的超平面 w·x + b = 0,将两类样本分开,并且使得两类样本到该超平面的间隔最大化。
核心概念:函数间隔与几何间隔
- 函数间隔:对于样本点
(xi, yi),其函数间隔定义为γ̂_i = yi(w·xi + b)。它表示分类预测的确信度。 - 几何间隔:对于样本点
(xi, yi),其几何间隔定义为γ_i = yi(w·xi + b) / ||w||。它表示该点到超平面的实际欧氏距离。
优化目标
SVM的核心思想是间隔最大化,即找到使所有样本点中最小的几何间隔尽可能大的超平面。这可以转化为以下凸二次规划问题:
min_(w,b) (1/2) * ||w||^2
s.t. yi(w·xi + b) >= 1, i = 1, 2, ..., n
这里,||w||^2 的最小化等价于几何间隔 (1/||w||) 的最大化。约束条件 yi(w·xi + b) >= 1 确保了所有样本点的函数间隔至少为1(通过缩放 w 和 b 总可以做到)。
求解方法:拉格朗日乘子法
为了求解上述带约束的优化问题,我们引入拉格朗日乘子 αi >= 0,构建拉格朗日函数:
L(w, b, α) = (1/2)||w||^2 - Σ_{i=1}^{n} αi [yi(w·xi + b) - 1]
通过求解其对 w 和 b 的偏导并令其为零,我们可以将原问题转化为其对偶问题:
max_α Σ_{i=1}^{n} αi - (1/2) Σ_{i=1}^{n} Σ_{j=1}^{n} αi αj yi yj (xi·xj)
s.t. Σ_{i=1}^{n} αi yi = 0, αi >= 0
求解这个对偶问题(通常使用SMO算法)得到最优的 α*,进而可以计算出最优的超平面参数:
w* = Σ_{i=1}^{n} αi* yi xi
b* = yj - Σ_{i=1}^{n} αi* yi (xi·xj) (对于任意支持向量 `xj`)
最终的分类决策函数为:
f(x) = sign(w*·x + b*)
2. 软间隔支持向量机(线性不可分)
在实际问题中,数据往往不是严格线性可分的,或者存在噪声。硬间隔SVM会因此失效。为此,我们引入软间隔概念,允许一些样本点不满足严格的间隔约束。
核心修改:引入松弛变量
我们为每个样本点引入一个松弛变量 ξi >= 0,并修改优化问题:
min_(w,b,ξ) (1/2) * ||w||^2 + C * Σ_{i=1}^{n} ξi
s.t. yi(w·xi + b) >= 1 - ξi, i = 1, 2, ..., n
ξi >= 0
其中,C > 0 是一个惩罚参数,用于控制对误分类的惩罚力度。C 越大,对误分类的容忍度越低,模型越倾向于将所有样本正确分类(可能过拟合);C 越小,则允许更多的误分类(可能欠拟合)。
求解思路
软间隔SVM的求解过程与硬间隔类似,同样使用拉格朗日乘子法转化为对偶问题求解,只是拉格朗日乘子 αi 多了一个上界约束 αi <= C。
3. 非线性支持向量机与核技巧 🌀
上一节我们介绍了如何处理近似线性可分的数据,本节中我们来看看如何用SVM处理完全非线性可分的数据。核技巧是SVM处理非线性问题的关键。
核心思想:映射到高维空间
对于在原始特征空间中线性不可分的数据,我们可以通过一个映射函数 φ(x) 将其映射到一个更高维(甚至是无穷维)的特征空间。在这个新空间中,数据可能变得线性可分。然后,我们在高维空间中应用线性SVM。
核技巧的妙处
直接计算高维空间中的内积 φ(xi)·φ(xj) 可能非常复杂甚至不可行。核技巧指出,存在一个核函数 K(xi, xj),它能在原始空间中直接计算,其结果等于映射后高维空间中的内积,即:
K(xi, xj) = φ(xi)·φ(xj)
这样,我们无需知道映射函数 φ(x) 的具体形式,也无需在高维空间中进行复杂计算,只需在原始空间中计算核函数即可。SVM的对偶问题中只涉及样本间的内积 (xi·xj),因此只需将其替换为核函数 K(xi, xj):
max_α Σ_{i=1}^{n} αi - (1/2) Σ_{i=1}^{n} Σ_{j=1}^{n} αi αj yi yj K(xi, xj)
s.t. Σ_{i=1}^{n} αi yi = 0, 0 <= αi <= C
最终的决策函数变为:
f(x) = sign( Σ_{i=1}^{n} αi* yi K(xi, x) + b* )
常用核函数
以下是两种最常用的核函数:
- 多项式核函数:
K(x, z) = (x·z + c)^d,其中d为多项式次数。 - 高斯核函数(RBF核):
K(x, z) = exp(-γ * ||x - z||^2),其中γ为参数,控制高斯函数的宽度。
4. 序列最小优化(SMO)算法 ⚙️
SMO算法是用于高效求解SVM对偶问题的一种常用方法。其基本思想是:每次只选择两个拉格朗日乘子 αi 和 αj 进行优化,固定其他所有乘子。由于存在约束 Σ αi yi = 0,两个变量之间具有线性关系,因此每次优化实际上是一个单变量的二次规划问题,可以解析求解。通过反复选择并优化变量对,直至满足收敛条件。
第二部分:朴素贝叶斯模型精髓 📊
在了解了基于间隔最大化的SVM后,我们转向另一种基于概率论的经典分类模型——朴素贝叶斯。它的核心是贝叶斯定理和特征条件独立性假设。
1. 概率基础:加法规则与乘法规则
在深入朴素贝叶斯之前,需要回顾两个基本的概率规则,它们贯穿于所有概率图模型的推导。
以下是两个核心规则:
- 加法(求和)规则:
P(X) = Σ_Y P(X, Y)。边缘概率可以通过对联合概率求和得到。 - 乘法(乘积)规则:
P(X, Y) = P(X|Y) * P(Y) = P(Y|X) * P(X)。联合概率可以分解为条件概率和边缘概率的乘积。
2. 朴素贝叶斯原理
朴素贝叶斯模型用于解决分类问题。给定训练数据集 T = {(x1, y1), ..., (xn, yn)},其中 xi 是n维特征向量,yi ∈ {c1, c2, ..., cK} 是类别标签。
核心假设:条件独立性
这是“朴素”一词的由来。该假设认为,在给定类别 Y = ck 的条件下,所有特征 X1, X2, ..., Xn 是相互独立的。即:
P(X1=x1, X2=x2, ..., Xn=xn | Y=ck) = Π_{j=1}^{n} P(Xj=xj | Y=ck)
这个假设极大地简化了计算,但现实中特征之间往往存在关联,这也是模型性能可能受限的原因。
模型推导:基于贝叶斯定理
我们的目标是计算给定特征 x 后,它属于各个类别 ck 的后验概率 P(Y=ck | X=x),并选择概率最大的类别作为预测结果。
根据贝叶斯定理和乘法规则:
P(Y=ck | X=x) = P(X=x | Y=ck) * P(Y=ck) / P(X=x)
利用条件独立性假设:
P(Y=ck | X=x) = [ Π_{j=1}^{n} P(Xj=xj | Y=ck) ] * P(Y=ck) / P(X=x)
对于同一个输入 x,分母 P(X=x) 是相同的,因此比较后验概率时只需比较分子部分。朴素贝叶斯分类器表示为:
y = argmax_{ck} [ P(Y=ck) * Π_{j=1}^{n} P(Xj=xj | Y=ck) ]
3. 参数估计:极大似然估计
在训练阶段,我们需要从数据中估计先验概率 P(Y=ck) 和条件概率 P(Xj=ajl | Y=ck)(其中 ajl 是第 j 个特征的第 l 个可能取值)。
以下是参数估计的方法:
- 先验概率:
P(Y=ck) = (Σ_{i=1}^{N} I(yi = ck)) / N,即数据集中类别为ck的样本所占比例。 - 条件概率(离散特征):
P(Xj=ajl | Y=ck) = (Σ_{i=1}^{N} I(xi_j = ajl, yi = ck)) / (Σ_{i=1}^{N} I(yi = ck)),即在类别为ck的样本中,第j个特征取值为ajl的样本所占比例。
4. 贝叶斯估计与拉普拉斯平滑
当使用极大似然估计时,如果某个特征值在某个类别下从未出现,会导致其条件概率为0,进而使得整个后验概率为0(无论其他特征多么明显)。为了克服这个问题,我们采用贝叶斯估计(拉普拉斯平滑)。
具体做法是在计数时加上一个平滑项 λ(通常 λ=1):
P_λ(Xj=ajl | Y=ck) = (Σ_{i=1}^{N} I(xi_j = ajl, yi = ck) + λ) / (Σ_{i=1}^{N} I(yi = ck) + Sj * λ)
其中 Sj 是第 j 个特征可能取值的个数。同样,先验概率也可以进行平滑:
P_λ(Y=ck) = (Σ_{i=1}^{N} I(yi = ck) + λ) / (N + K * λ)
当 λ=0 时,即为极大似然估计;λ=1 时,称为拉普拉斯平滑,可以有效避免概率为0的情况。
总结 🎯
本节课中我们一起学习了支持向量机(SVM)和朴素贝叶斯两个核心的机器学习模型。
对于SVM,我们掌握了其从硬间隔(处理线性可分数据)到软间隔(引入松弛变量处理近似线性可分和噪声)再到非线性(通过核技巧映射到高维空间)的完整演进思路。理解了其间隔最大化的核心思想,以及通过拉格朗日乘子法转化为对偶问题求解的数学框架。
对于朴素贝叶斯,我们理解了其基于贝叶斯定理和特征条件独立性假设的基本原理。掌握了如何使用极大似然估计进行参数学习,以及通过拉普拉斯平滑来避免零概率问题。


这两个模型分别代表了判别式模型(SVM,直接学习决策边界)和生成式模型(朴素贝叶斯,学习数据生成机制)的重要思想,是机器学习知识体系中的基石。理解它们,将为后续学习更复杂的模型(如HMM、CRF等)打下坚实的基础。


课程1447-P18:XGBoost精讲教程 🚀
在本节课中,我们将要学习极限梯度提升(XGBoost)模型。XGBoost是一种在梯度提升决策树(GBDT)基础上,通过引入正则化和二阶梯度信息进行重大改进的集成学习模型。它以计算准确、速度快而著称,在各类数据科学竞赛和实际工程中应用广泛。

1. 模型发展脉络回顾 📈
上一节我们介绍了提升方法(Boosting)和提升决策树(BDT)。本节中,我们来看看这些基础如何演进到梯度提升决策树(GBDT),并最终形成XGBoost。
XGBoost并非深度神经网络模型,它基于决策树和集成学习思想,在模型结构和优化策略上与传统深度学习不同。



2. 提升方法与BDT回顾
提升方法的核心是加法模型和前向分布算法。
加法模型:将一系列基模型 \(b(x; \gamma_m)\) 与其权重系数 \(\beta_m\) 累加,得到最终模型 \(f(x)\)。
\(f(x) = \sum_{m=1}^{M} \beta_m b(x; \gamma_m)\)
优化目标是在整个加法模型上最小化损失函数。前向分布算法:一种优化加法模型的策略。它从前向后,每一步只学习一个基函数及其系数,逐步逼近优化目标。
\(f_m(x) = f_{m-1}(x) + \beta_m b(x; \gamma_m)\)
每一步的优化目标是:
\((\beta_m, \gamma_m) = \arg\min_{\beta, \gamma} \sum_{i=1}^{N} L(y_i, f_{m-1}(x_i) + \beta b(x_i; \gamma))\)
提升决策树(BDT) 是以决策树为基模型的提升方法。其模型形式为:
\(f_M(x) = \sum_{m=1}^{M} T(x; \Theta_m)\)
其中,\(T(x; \Theta_m)\) 表示第 \(m\) 棵决策树。
当损失函数为平方损失时,BDT的学习过程有一个直观解释:每一步构建一棵新的决策树,去拟合当前模型预测值与真实值之间的残差。
3. 梯度提升决策树(GBDT)🔍
GBDT在BDT的基础上进行了关键改进:它使用损失函数的负梯度作为残差的近似值,来拟合每一棵新的回归树。这使得GBDT可以适用于各种损失函数,而不仅限于平方损失。
GBDT的算法步骤如下:
- 初始化模型,例如使 \(f_0(x)\) 为一个常数。
- 对于 \(m = 1\) 到 \(M\)(\(M\) 为树的数量):
- 对每个样本 \(i\),计算负梯度(即伪残差):
\(r_{mi} = -\left[\frac{\partial L(y_i, f(x_i))}{\partial f(x_i)}\right]_{f(x)=f_{m-1}(x)}\) - 使用所有样本 \({(x_i, r_{mi})}\) 拟合一棵新的回归树 \(T_m(x)\),其叶子节点区域为 \(R_{mj}, j=1,2,...,J_m\)。
- 对每个叶子区域 \(R_{mj}\),计算最佳拟合值 \(c_{mj}\),通常是通过最小化该区域内样本的损失来确定。
- 更新模型:\(f_m(x) = f_{m-1}(x) + \sum_{j=1}^{J_m} c_{mj} I(x \in R_{mj})\)。
- 对每个样本 \(i\),计算负梯度(即伪残差):
- 得到最终的提升树模型:\(f_M(x)\)。






核心理解:GBDT可以看作是在函数空间中进行梯度下降。每一步增加一棵树,相当于沿着损失函数对当前模型的负梯度方向前进,从而逐步降低总损失。




4. XGBoost的核心原理 ⚙️
XGBoost在GBDT的基础上主要做了两点核心改进:引入正则化和使用二阶梯度信息。
4.1 模型定义与正则化目标函数
在XGBoost中,单棵决策树被定义为:
\(f_t(x) = w_{q(x)}, \quad w \in R^T, \quad q: R^d \to \{1,2,...,T\}\)
其中:
- \(q(x)\) 函数将输入样本 \(x\) 映射到树的某个叶子节点索引(共 \(T\) 个叶子)。
- \(w\) 是叶子节点的权重向量,\(w_j\) 表示第 \(j\) 个叶子节点的得分。
- 因此,\(f_t(x)\) 的输出就是样本 \(x\) 所在叶子节点的权重 \(w_{q(x)}\)。
对于包含 \(K\) 棵树的加法模型,预测输出为:
\(\hat{y}_i = \phi(x_i) = \sum_{k=1}^{K} f_k(x_i)\)
XGBoost的目标函数由损失函数和正则化项组成:
\(Obj(\Theta) = \sum_{i=1}^{n} l(y_i, \hat{y}_i) + \sum_{k=1}^{K} \Omega(f_k)\)
其中,正则化项 \(\Omega(f)\) 定义为:
\(\Omega(f) = \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2\)
这里:
- \(T\) 是树的叶子节点个数,控制模型复杂度。
- \(\sum w_j^2\) 是叶子节点权重的L2范数平方,防止权重过大。
- \(\gamma\) 和 \(\lambda\) 是控制正则化强度的超参数。
4.2 二阶泰勒展开与目标函数化简
在第 \(t\) 步迭代时,我们的目标是学习第 \(t\) 棵树 \(f_t\)。目标函数可以写为:
\(Obj^{(t)} = \sum_{i=1}^{n} l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t) + constant\)
为了优化这个目标,XGBoost在 \(\hat{y}_i^{(t-1)}\) 处对损失函数 \(l\) 进行二阶泰勒展开。定义:
\(g_i = \frac{\partial l(y_i, \hat{y}_i^{(t-1)})}{\partial \hat{y}_i^{(t-1)}}, \quad h_i = \frac{\partial^2 l(y_i, \hat{y}_i^{(t-1)})}{\partial (\hat{y}_i^{(t-1)})^2}\)
即 \(g_i\) 和 \(h_i\) 分别是一阶和二阶梯度。



经过泰勒展开并忽略常数项后,第 \(t\) 步的目标函数近似为:
\(Obj^{(t)} \approx \sum_{i=1}^{n} [g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i)] + \Omega(f_t)\)
4.3 统一求和与最优解

由于 \(f_t(x_i) = w_{q(x_i)}\),我们可以将按样本 \(i\) 的求和,转换为按叶子节点 \(j\) 的求和。定义叶子节点 \(j\) 上的样本集合为 \(I_j = \{ i | q(x_i) = j \}\)。



将目标函数按叶子节点重新组织:
$$ \begin
Obj^{(t)} &\approx \sum_ [(\sum_{i \in I_j} g_i) w_j + \frac{1}{2} (\sum_{i \in I_j} h_i + \lambda) w_j2] + \gamma T \
&= \sum_ [G_j w_j + \frac{1}{2} (H_j + \lambda) w_j2] + \gamma T
\end $$
其中,\(G_j = \sum_{i \in I_j} g_i\),\(H_j = \sum_{i \in I_j} h_i\)。

这是一个关于 \(w_j\) 的二次函数。对于每个叶子节点 \(j\),可以求出其最优权重 \(w_j^*\):
\(w_j^* = -\frac{G_j}{H_j + \lambda}\)

将 \(w_j^*\) 代回目标函数,得到当前树结构 \(q\) 下的最小目标函数值(即结构分数):
\(Obj^* = -\frac{1}{2} \sum_{j=1}^{T} \frac{G_j^2}{H_j + \lambda} + \gamma T\)
4.4 树结构的生成与分裂依据
\(Obj^*\) 的值越小,说明当前树结构越好。XGBoost使用贪心算法来生成树:从根节点开始,迭代地选择最佳分裂点。
对于一次候选分裂,将节点上的样本分裂到左子树 \(I_L\) 和右子树 \(I_R\)。分裂后的目标函数值减少量(即增益)为:
\(Gain = \frac{1}{2} \left[ \frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{(G_L+G_R)^2}{H_L+H_R + \lambda} \right] - \gamma\)
其中,\(G_L, H_L\) 和 \(G_R, H_R\) 分别是左右子节点的一阶、二阶梯度之和。
分裂准则:选择使 \(Gain\) 最大的特征和特征值进行分裂。如果最大 \(Gain\) 小于0(或小于某个阈值),则停止分裂。这里的 \(\gamma\) 起到了预剪枝的作用。


5. XGBoost应用与参数调优 🛠️




5.1 主要参数类别

以下是XGBoost中需要调节的主要参数:


通用参数
booster: 选择基础模型,gbtree(树模型)或gblinear(线性模型)。nthread: 并行线程数。verbosity: 打印信息的详细程度。
提升器参数(对于树模型)
eta(learning_rate): 学习率,缩小每棵树的贡献,防止过拟合。gamma: 节点分裂所需的最小损失减少量(即正则项 \(\gamma\)),用于控制树的分裂。max_depth: 树的最大深度。min_child_weight: 叶子节点中样本权重和(即 \(H_j\))的最小值,用于控制过拟合。subsample: 训练每棵树时使用的样本子集比例。colsample_bytree: 训练每棵树时使用的特征子集比例。lambda(reg_lambda): L2正则化权重 \(\lambda\)。alpha(reg_alpha): L1正则化权重。


- 学习任务参数
objective: 定义学习任务和损失函数,如reg:squarederror(回归),binary:logistic(二分类),multi:softmax(多分类)。eval_metric: 评估指标,如rmse,logloss,error。
5.2 基本使用示例


以下是一个简单的二分类示例代码框架:
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 假设 X, y 是你的数据和标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 转换为DMatrix格式,XGBoost高效的数据结构
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
# 设置参数
params = {
'booster': 'gbtree',
'objective': 'binary:logistic', # 输出概率的二分类
'eval_metric': 'logloss',
'eta': 0.1,
'max_depth': 6,
'min_child_weight': 1,
'gamma': 0,
'subsample': 0.8,
'colsample_bytree': 0.8,
'lambda': 1,
'alpha': 0,
'seed': 42
}


# 训练模型
num_rounds = 100
bst = xgb.train(params, dtrain, num_rounds, evals=[(dtest, 'test')], early_stopping_rounds=10)

# 预测(输出概率)
y_pred_proba = bst.predict(dtest)
# 将概率转换为类别
y_pred = (y_pred_proba > 0.5).astype(int)


# 评估准确率
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f}")


5.3 与LightGBM的简单对比
LightGBM是微软开发的另一个高效的梯度提升框架。它与XGBoost的主要区别在于树的生长策略:
- XGBoost:使用按层生长(Level-wise) 的策略,同一层的所有节点都进行分裂。
- LightGBM:使用按叶子生长(Leaf-wise) 的策略,每次从当前所有叶子中,选择增益最大的一个进行分裂。




这种差异使得LightGBM通常在训练速度和内存消耗上更有优势,尤其是在数据量大或特征维度高时。但在小数据集上,XGBoost可能因其更细致的分裂控制而表现更稳定。在实际项目中,两者都值得尝试。
总结 📝




本节课中我们一起学习了XGBoost模型。我们从提升方法和BDT出发,回顾了GBDT利用负梯度拟合残差的思想。然后,我们深入探讨了XGBoost的两大核心改进:








- 引入正则化:在目标函数中加入控制叶子节点数(\(\gamma T\))和叶子权重(\(\frac{1}{2}\lambda \sum w_j^2\))的正则项,有效防止过拟合。
- 利用二阶梯度:通过二阶泰勒展开近似损失函数,在优化时同时利用一阶梯度(\(g_i\))和二阶梯度(\(h_i\))信息,使得优化更精确、收敛更快。




我们推导了在给定树结构下,叶子节点最优权重的计算公式 \(w_j^* = -\frac{G_j}{H_j + \lambda}\),以及衡量树结构好坏的结构分数 \(Obj^*\)。树的分裂依据正是基于分裂前后结构分数的变化(增益 \(Gain\))来决定的。


最后,我们简要介绍了XGBoost的主要参数、基本使用方法,并将其与LightGBM进行了对比。XGBoost因其出色的性能、灵活性和可解释性,至今仍是结构化数据建模中最强大的工具之一。
机器学习课程 P19:隐马尔可夫模型(HMM)详解 🧠
在本节课中,我们将系统性地学习概率图模型中的核心模型之一——隐马尔可夫模型(HMM)。我们将从模型定义出发,逐步深入到其解决的三大核心问题:概率计算、学习算法和预测算法。课程内容力求简单直白,并辅以公式和图示,帮助初学者建立清晰的理解。
概述 📋
隐马尔可夫模型(HMM)是一种用于处理序列到序列映射任务的概率图模型,尤其在自然语言处理(如词性标注)中应用广泛。本节课我们将学习HMM的完整知识体系,包括其数学定义、核心假设以及解决实际问题的算法流程。
一、模型定义与基本概念 🏗️
上一节我们概述了HMM的应用场景。本节中,我们来看看HMM的严格数学定义和构成要素。
HMM旨在描述由隐藏的状态序列生成可观测的观测序列的过程。为了形式化地定义它,我们首先需要明确两个集合和两个序列。
两个核心集合
以下是定义HMM所需的基础集合:
- 状态集合 Q:包含所有可能的隐藏状态。
Q = {q1, q2, ..., qN},共有N种状态。 - 观测集合 V:包含所有可能的观测值。
V = {v1, v2, ..., vM},共有M种观测。
注意:集合内的元素是无序的,它们仅表示所有可能的取值。
两个关键序列
基于上述集合,我们可以定义两个在时间上有序的序列:
- 状态序列 I:由状态集合中的元素按时间顺序构成。
I = (i1, i2, ..., iT),其中T为序列长度,每个it∈ Q。 - 观测序列 O:由观测集合中的元素按时间顺序构成。
O = (o1, o2, ..., oT),每个ot∈ V。
核心关系:在HMM中,观测序列由状态序列决定。每个时刻t的观测值
ot只由该时刻的状态it生成。
模型的概率参数
HMM的概率生成过程由以下三个参数决定:
初始状态概率向量 π:
- 定义:
πi = P(i1 = qi),表示初始时刻(t=1)处于状态qi的概率。 - 形式:
π = (π1, π2, ..., πN),满足Σπi = 1。
- 定义:
状态转移概率矩阵 A:
- 定义:
A = [aij]N×N,其中aij = P(it+1 = qj | it = qi)。 - 含义:表示在时刻t处于状态qi的条件下,下一时刻t+1转移到状态qj的概率。
- 定义:
观测概率矩阵 B:
- 定义:
B = [bj(k)]N×M,其中bj(k) = P(ot = vk | it = qj)。 - 含义:表示在时刻t处于状态qj的条件下,生成观测值vk的概率。
- 定义:
模型的形式化定义
一个隐马尔可夫模型λ由上述三元组完全确定:
λ = (A, B, π)
模型图示与生成过程
根据以上定义,HMM的生成过程可以图示如下,这有助于直观理解:
i1 -> i2 -> i3 -> ... -> iT
↓ ↓ ↓ ↓
o1 o2 o3 ... oT
生成算法:
- 根据初始概率向量π生成初始状态i1。
- 根据观测概率矩阵B,由状态i1生成观测o1。
- 根据状态转移矩阵A,由状态i1生成下一个状态i2。
- 重复步骤2-3,依次生成(o2, i3), (o3, i4), ..., (oT, iT+1),直到生成长度为T的观测序列。
二、两个基本假设 🔧
为了简化模型的计算,HMM建立在两个重要的假设之上。
1. 齐次马尔可夫性假设
该假设规定了状态序列内部的依赖关系。
- 内容:任意时刻t的状态
it只依赖于其前一时刻的状态it-1,而与更早的状态及所有观测无关。 - 公式:
P(it | i1, o1, ..., it-1, ot-1) = P(it | it-1)
2. 观测独立性假设
该假设规定了状态与观测之间的依赖关系。
- 内容:任意时刻t的观测
ot只依赖于该时刻的状态it,而与其他时刻的状态及观测无关。 - 公式:
P(ot | i1, o1, ..., it, ..., iT, oT) = P(ot | it)
这两个假设极大地降低了模型的复杂度,是后续所有推导和计算的基础。
三、HMM解决的三大问题 ⚙️
在定义了模型之后,HMM主要被用来解决三类问题。
问题1:概率计算问题(Evaluation)
- 已知:模型参数
λ = (A, B, π)和观测序列O = (o1, o2, ..., oT)。 - 求解:计算在该模型下,观测序列O出现的概率
P(O | λ)。 - 意义:评估模型与观测数据的匹配程度。例如,给定多个模型,选择
P(O | λ)最大的那个。 - 算法:前向算法或后向算法。这里我们重点介绍前向算法。
前向算法详解
前向算法通过引入“前向概率”来高效计算 P(O | λ)。
定义前向概率 αt(i):
αt(i) = P(o1, o2, ..., ot, it = qi | λ)
它表示在模型λ下,到时刻t为止的观测序列为(o1,...,ot),且时刻t的状态为qi的联合概率。递推计算:
- 初始化(t=1):
α1(i) = πi * bi(o1),i = 1, 2, ..., N
即用初始概率和第一个观测的生成概率计算。 - 递推(对 t = 1, 2, ..., T-1):
αt+1(j) = [Σ αt(i) * aij] * bj(ot+1),j = 1, 2, ..., N
解释:时刻t+1处于状态j的前向概率 = 【所有可能从t时刻状态i转移到j的概率之和】 × 【在状态j下生成观测ot+1的概率】。 - 终止:
P(O | λ) = Σ αT(i),i = 1, 2, ..., N
因为αT(i)已包含了整个观测序列O和最终状态为qi的概率,对所有最终状态求和即得整个序列的概率。
- 初始化(t=1):
前向算法将直接计算的指数级复杂度降低到了 O(N²T) 级别。
问题2:学习问题(Learning)
- 已知:观测序列
O = (o1, o2, ..., oT)。 - 求解:估计模型参数
λ = (A, B, π),使得该模型下观测序列O的概率P(O | λ)最大。 - 意义:从数据中自动学习模型参数,是一个无监督学习过程。
- 挑战:状态序列I是未知的(隐变量),使得问题变得复杂。
- 算法:Baum-Welch算法,它是期望最大化算法在HMM中的具体应用。
EM算法与Baum-Welch算法
由于状态序列I是隐变量,我们需要使用EM算法来求解。
- E步(求期望):基于当前参数估计λ,计算完全数据(O, I)的对数似然函数
log P(O, I | λ)关于隐变量I的条件期望,即Q函数:
Q(λ, λ’) = Σ_I P(I | O, λ’) * log P(O, I | λ)
其中λ’是上一轮迭代的参数。 - M步(最大化):最大化Q函数,得到新的参数估计λ:
λ = argmax_λ Q(λ, λ’)
在HMM的特定形式下,通过推导,M步的参数更新有闭式解:
- 初始概率:
πi* = γ1(i),即初始时刻处于状态i的期望概率。 - 转移概率:
aij* = Σ ξt(i, j) / Σ γt(i)。分子是从i转移到j的期望次数,分母是处于状态i的期望次数。 - 观测概率:
bj(k)* = Σ γt(j) / Σ γt(j)。分子是在状态j下观测到vk的期望次数,分母是处于状态j的期望次数。
其中,γt(i) = P(it = qi | O, λ)(给定观测下t时刻状态为i的概率)和 ξt(i, j) = P(it = qi, it+1 = qj | O, λ) 是可以通过前向-后向算法计算得到的中间量。
Baum-Welch算法就是不断迭代E步和M步,直至参数收敛。
问题3:预测问题(Decoding)
- 已知:模型参数
λ = (A, B, π)和观测序列O = (o1, o2, ..., oT)。 - 求解:寻找一个**最优的状态序列 I* = (i1*, i2*, ..., iT*) **,使得条件概率
P(I | O, λ)最大。 - 意义:揭示最可能产生观测序列的隐藏状态路径。例如,在词性标注中,根据句子(观测)推测每个词的词性(状态)。
- 算法:维特比算法,一种基于动态规划的最大化路径搜索算法。
维特比算法详解
维特比算法定义了两个变量:
- 路径概率 δt(i):在时刻t,所有以状态qi结尾的局部路径(i1, i2, ..., it=qi)中,概率最大的那条路径的概率值。
δt(i) = max_{i1, i2, ..., it-1} P(i1, i2, ..., it=qi, o1, o2, ..., ot | λ) - 路径回溯 ψt(i):记录使δt(i)达到最大的时刻t-1的状态编号。
算法步骤:
- 初始化:
δ1(i) = πi * bi(o1),ψ1(i) = 0,i = 1, 2, ..., N - 递推(对 t = 2, 3, ..., T):
δt(j) = max_{1≤i≤N} [δt-1(i) * aij] * bj(ot)
ψt(j) = argmax_{1≤i≤N} [δt-1(i) * aij] - 终止:
P* = max_{1≤i≤N} δT(i)(最大路径概率)
iT* = argmax_{1≤i≤N} δT(i)(最优路径终点) - 路径回溯(对 t = T-1, T-2, ..., 1):
it* = ψt+1(it+1*)
通过ψ数组从终点反向回溯,即可得到完整的最优状态序列I*。
维特比算法与前向算法结构相似,但将求和Σ换为取最大值max,并增加了记录回溯指针的步骤。
四、EM算法补充说明 🔄
在学习问题中,我们提到了EM算法。这里对其核心思想做简要补充。
EM算法用于解决含有隐变量的概率模型参数估计问题。其核心思想是:由于直接最大化观测数据的似然 P(O | λ) 困难,转而构建一个易于优化的“下界”函数(Q函数),并通过迭代不断抬高这个下界,从而间接使原似然函数增大。
- E步(Expectation):基于当前参数λ’,计算隐变量的后验分布
P(I | O, λ’),进而计算完全数据的对数似然log P(O, I | λ)在该后验分布下的期望,即Q函数。 - M步(Maximization):寻找使Q函数最大化的新参数λ。
对于HMM,E步需要计算 γt(i) 和 ξt(i, j),这由前向-后向算法完成;M步则根据前面给出的公式更新A, B, π。
总结 🎯
本节课中,我们一起学习了隐马尔可夫模型(HMM)的核心内容:
- 模型定义:HMM是一个三元组
λ = (A, B, π),描述了隐藏状态序列生成观测序列的概率过程。 - 基本假设:齐次马尔可夫性假设和观测独立性假设是模型简化的基石。
- 三大问题与算法:
- 概率计算:使用前向算法高效计算
P(O | λ)。 - 参数学习:在状态序列未知时,使用基于EM算法的Baum-Welch算法从观测数据中学习模型参数。
- 序列预测:使用维特比算法动态规划求解最可能的状态序列。
- 概率计算:使用前向算法高效计算
- 关键思想:HMM通过引入隐藏状态来建模序列数据的内部结构,其相关算法(前向、维特比、EM)是动态规划和迭代优化思想的经典体现。


HMM作为概率图模型的基础,其思想和算法在语音识别、自然语言处理、生物信息学等领域仍有重要应用,也是理解更复杂模型(如条件随机场CRF)的坚实基础。
🧠 机器学习基础与回归算法(课程编号:1447-P2-1)
在本节课中,我们将学习机器学习的基本概念,并深入探讨两种基础的回归类算法:线性回归和逻辑回归。我们将从宏观框架入手,理解监督学习与无监督学习的区别,然后聚焦于算法的核心思想、数学表达、优化方法以及如何避免过拟合。课程内容力求简单直白,适合初学者理解。
📚 机器学习基本概念
在深入学习具体算法之前,我们需要建立一个宏观的机器学习知识框架。整个机器学习领域,从解决问题的角度,主要可以分为监督学习和无监督学习两大类。
监督学习类似于有参考答案的学习过程。你拥有大量的“题目”(数据X)和对应的“标准答案”(标签Y)。你的目标是学习一个从X到Y的映射函数F,使得在面对新的、没有答案的“题目”时,也能给出正确的“答案”。我们本节课要讲的线性回归和逻辑回归都属于监督学习。
无监督学习则是在没有“标准答案”的情况下,从数据中挖掘规律和模式。例如,电商平台拥有海量的用户行为数据,但并没有给用户打上“爱买口红”或“爱买电子产品”的标签。通过无监督学习(如聚类算法),我们可以自动发现用户群体之间的相似性,从而进行用户分群。
此外,监督学习可以根据输出结果Y的类型,进一步细分为分类问题和回归问题。
- 分类问题:输出是离散的类别标签,类似于做“选择题”。例如,判断一封邮件是否为垃圾邮件(是/否),或识别一张图片中的动物是猫、狗还是其他。
- 回归问题:输出是连续的数值,类似于做“填空题”或“问答题”。例如,预测一套房子的具体价格,或预测一部电影的票房。
理解这些基本概念,有助于我们将后续学到的每一个算法,准确地归入这个知识地图的相应位置。
📈 线性回归
上一节我们介绍了机器学习的基本框架,本节我们来看看监督学习中的第一个具体算法:线性回归。线性回归用于解决回归问题,其核心思想是假设输入特征和输出结果之间存在线性关系。
核心思想与模型表示
线性回归模型试图学习一个线性函数,来拟合输入特征X和连续值输出Y之间的关系。其假设函数可以表示为:
h_θ(x) = θ₀ + θ₁x₁ + θ₂x₂ + ... + θₙxₙ
为了简化书写,我们通常使用向量化的形式:
h_θ(x) = θᵀx
其中,θ 是包含所有参数 (θ₀, θ₁, ..., θₙ) 的列向量,x 是包含所有特征 (1, x₁, ..., xₙ) 的列向量(x₀=1 对应截距项 θ₀)。我们的目标就是找到一组最优的参数 θ。
损失函数与优化
我们已经知道了模型的表达形式,但如何找到最优的 θ 呢?这就需要定义一个衡量模型预测好坏的准则——损失函数。对于线性回归,最常用的损失函数是均方误差:
J(θ) = (1/2m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)²
其中,m 是样本数量。J(θ) 衡量了模型在所有训练样本上的预测值与真实值之间的平均差异的平方。我们的优化目标就是最小化这个损失函数 J(θ)。
那么,如何最小化 J(θ) 呢?一个直观的方法是梯度下降。想象你在一个碗状曲面的顶部放一个小球,小球会沿着最陡的方向向下滚,最终停留在碗底(最低点)。梯度下降就是模拟这个过程:
- 随机初始化参数
θ(相当于随意放置小球)。 - 计算损失函数
J(θ)在当前θ处的梯度(即最陡的下降方向)。 - 沿着负梯度方向更新参数:
θ := θ - α * ∇J(θ)。 - 重复步骤2和3,直到损失函数收敛到最小值。
其中,α 称为学习率,它控制了每次更新的步长。步长太大会导致震荡甚至无法收敛;步长太小则收敛速度过慢。
以下是梯度下降算法的关键步骤:
- 初始化参数:为
θ设置初始值(例如全零)。 - 计算梯度:根据当前
θ计算损失函数的梯度。 - 更新参数:按照公式
θ := θ - α * ∇J(θ)更新参数。 - 迭代:重复计算梯度和更新参数,直到满足停止条件(如损失变化很小或达到最大迭代次数)。
⚖️ 过拟合、欠拟合与正则化
在模型训练过程中,我们可能会遇到两种不良状态:欠拟合和过拟合。
- 欠拟合:模型过于简单,无法捕捉数据中的基本规律。就像能力不足的学生,即使非常努力,也无法学好复杂的知识。表现在损失函数上,就是在训练集和测试集上的误差都很大。
- 过拟合:模型过于复杂,不仅学习了数据中的规律,还“记忆”了训练数据中的噪声和随机波动。就像记忆力超群但不求甚解的学生,能完美复述做过的题目,但遇到新题就束手无策。表现在损失函数上,就是在训练集上误差很小,但在测试集上误差很大。
过拟合是机器学习中更常见且棘手的问题。为了解决过拟合,我们引入正则化技术。其核心思想是在损失函数中增加一个对模型参数 θ 的惩罚项,限制参数值的大小,从而降低模型的复杂度,使其更加平滑。
以线性回归为例,加入L2正则化后的损失函数变为:
J(θ) = (1/2m) * [ Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)² + λ Σ θⱼ² ]
其中,λ 是正则化参数,控制惩罚的力度。λ 太大,模型会过于简单导致欠拟合;λ 太小,则约束力不足,可能仍会过拟合。λ 是一个需要根据实际数据调整的超参数。
🔮 逻辑回归
上一节我们学习了用于预测连续值的线性回归,本节我们来看一个用于解决分类问题的强大算法——逻辑回归。尽管名字中有“回归”,但逻辑回归是经典的分类模型。
从线性回归到分类
如果我们尝试直接用线性回归的直线来划分两类样本点(例如肿瘤良性/恶性),会面临两个问题:
- 对异常值敏感,容易被个别极端样本带偏决策边界。
- 线性回归的输出范围是
(-∞, +∞),而分类需要的是一个[0, 1]之间的概率值。
Sigmoid函数与模型表示
为了解决上述问题,逻辑回归在线性回归的输出上套用了一个 Sigmoid函数(也叫Logistic函数):
g(z) = 1 / (1 + e^{-z})
这个函数的神奇之处在于,它可以将任何实数 z 映射到 (0, 1) 区间,完美地表示概率。当 z 为0时,g(z)=0.5;z 趋向正无穷时,g(z) 趋向1;z 趋向负无穷时,g(z) 趋向0。
因此,逻辑回归的假设函数为:
h_θ(x) = g(θᵀx) = 1 / (1 + e^{-θᵀx})
h_θ(x) 的输出被解释为“样本属于正类(例如恶性肿瘤)的概率”。
损失函数与优化
逻辑回归不能使用线性回归的均方误差作为损失函数,因为其损失函数会是非凸的,导致优化困难。逻辑回归使用对数损失函数(Log Loss),也称为交叉熵损失:
J(θ) = - (1/m) * Σ [ y⁽ⁱ⁾ log(h_θ(x⁽ⁱ⁾)) + (1 - y⁽ⁱ⁾) log(1 - h_θ(x⁽ⁱ⁾)) ]
这个损失函数的含义是:对于正样本 (y=1),我们希望预测概率 h_θ(x) 越大越好(-log(h_θ(x)) 越小越好);对于负样本 (y=0),我们希望预测概率 h_θ(x) 越小越好(-log(1 - h_θ(x)) 越小越好)。该函数是凸函数,同样可以使用梯度下降法进行优化。
同样,为了防止过拟合,也可以在损失函数中加入L2正则化项。
多分类扩展
基本的逻辑回归是二分类模型。对于多分类问题,有两种常见的策略:
- 一对多:为每个类别训练一个二分类器,判断样本是“属于该类”还是“不属于该类”。预测时,选择输出概率最高的类别。
- 一对一:为每两个类别组合训练一个二分类器。预测时,让新样本经过所有分类器投票,得票最多的类别获胜。
💡 总结与应用
本节课中我们一起学习了机器学习的基础框架和两个核心的回归类算法。
我们首先建立了机器学习的宏观视图,区分了监督学习、无监督学习,以及监督学习下的分类与回归任务。然后,我们深入探讨了线性回归,学习了其模型表示、均方误差损失函数以及通过梯度下降进行优化的全过程。接着,我们认识了模型训练中的常见问题——欠拟合与过拟合,并引入了正则化作为控制模型复杂度、缓解过拟合的关键技术。
最后,我们学习了逻辑回归。虽然名为回归,但它实质是一个强大的分类模型。我们了解了如何通过Sigmoid函数将线性输出转化为概率,以及为何要使用对数损失函数,并简要了解了其扩展到多分类问题的方法。


逻辑回归在工业界有着“一招LR打天下”的说法,这得益于其模型简单、训练高效、可解释性强(可以通过参数大小判断特征重要性)以及输出概率的天然优势。它是许多复杂系统优秀的基线模型。理解这些基础算法,是通往更复杂模型(如神经网络、集成模型)的坚实阶梯。

课程 1447-P3:行人重识别(ReID)项目实战:训练与评测流程 🚶♂️🔍
概述
在本节课中,我们将学习如何构建并运行一个完整的行人重识别(ReID)项目。我们将从回顾ReID任务的核心概念开始,然后一步步实现一个用于特征提取的卷积神经网络(CNN)的训练流程,包括数据加载、模型构建、训练循环以及数据增强等关键环节。
回顾:ReID任务的核心本质
上一节我们介绍了行人重识别(ReID)任务的主要背景和应用场景。本节中,我们来深入理解其技术本质。
ReID任务看似是一个识别问题,但其本质是通过图像检索的技术手段来实现识别目的。具体来说,给定一张查询(query)图片,系统需要在数据库(gallery)中搜索并返回相关的图片样本。
为了实现检索,核心在于获取图像的特征表达。我们将每张图片通过一个特征提取模块(如CNN)转换为一个特征向量。之后,识别问题就转化为了计算查询图片特征向量与数据库图片特征向量之间的相似度。相似度计算通常使用向量内积等度量方式。
公式:相似度计算
相似度 = query特征向量 · gallery特征向量
因此,我们的核心目标就变成了:训练一个能够提取高质量特征向量的CNN模型。
从分类到特征提取:模型构建思路
既然目标是训练一个特征提取器,一个直观的思路是利用分类任务进行训练。在ReID数据集中,每个行人的ID可以视为一个独立的类别。通过训练一个CNN分类器(例如,预测图片属于哪个ID),我们可以让模型学会区分不同行人的视觉特征。
训练完成后,移除模型最后的分类层(softmax层),保留下来的网络部分(如最后的全连接层或全局池化层输出)就可以作为输入图片的特征向量。
这里有一个至关重要的概念:测试集中的ID不需要出现在训练集中。这是因为我们的模型是一个通用的特征提取器,而不是一个封闭集的分类器。模型在训练时学习了如何提取有区分度的特征,在遇到新的ID时,它依然能提取出有效的特征向量,然后通过特征比对(检索)的方式在底库中找到最相似的样本,从而完成识别。这保证了模型的泛化能力。
项目代码结构梳理
以下是实现训练流程的核心步骤,我们将逐一构建:
- 数据准备与加载:解析图像路径和ID标签,并划分为训练集和验证集。
- 模型定义:选择一个CNN骨干网络(如MobileNetV2),并修改其输出层以适应我们的ID分类任务。
- 生成器(Generator)编写:实现一个数据生成器,用于批量从硬盘加载图像数据到内存/显存,并支持数据增强。
- 训练循环配置:设置优化器、损失函数、回调函数(如模型检查点),并启动训练过程。
核心模块实现详解
1. 数据加载与预处理
首先,我们需要加载数据。数据通常以图像文件列表和对应的标签列表形式组织。
代码:数据路径与标签解析示例
# image_path_list 示例
# [‘/home/data/person_001_001.jpg‘, ‘/home/data/person_001_002.jpg‘, ...]
# image_label_list 示例 (原始ID,如字符串)
# [‘person_001‘, ‘person_001‘, ...]
from sklearn.preprocessing import LabelEncoder
# 使用LabelEncoder将字符串ID转换为连续的整数标签,例如 0, 1, 2, ...
label_encoder = LabelEncoder()
encoded_labels = label_encoder.fit_transform(original_id_list)
接下来,使用 train_test_split 将数据划分为训练集和验证集,注意设置 random_state 以确保结果可复现。
2. 模型构建
我们以MobileNetV2为例,利用其在ImageNet上的预训练权重,并替换最后的分类层。
代码:构建ReID特征提取模型
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.layers import Dense, GlobalMaxPooling2D
from tensorflow.keras.models import Model
# 加载预训练骨干网络,不包括顶部分类层
base_model = MobileNetV2(weights=‘imagenet‘, include_top=False, pooling=‘max‘)
# 添加新的分类层,num_classes 是训练集中行人ID的总数
x = base_model.output
predictions = Dense(num_classes, activation=‘softmax‘)(x)
model = Model(inputs=base_model.input, outputs=predictions)
3. 实现数据生成器 (Generator)
由于数据量可能很大,我们需要一个生成器来逐批加载数据。这个生成器需要兼容训练(需要标签)和推理/评估(仅需图像)两种模式。
以下是生成器函数的核心逻辑介绍:
关键参数:
image_path_list: 图像路径列表。image_label_list: 对应的标签列表(评估时可设为None)。batch_size: 批大小。augment: 是否进行数据增强。target_size: 图像统一调整的尺寸。
内部流程:
- 如果
shuffle为真,则同步打乱路径和标签列表。 - 使用
while True循环持续产出数据批次。 - 在循环内,计算当前批次的起始和结束索引。
- 调用
load_image_batch函数加载一个批次的图像和标签。 - 如果
augment为真,对该批次图像应用数据增强变换(如使用albumentations库)。 - 对图像进行归一化处理(如除以255,并减去ImageNet均值除以标准差)。
- 使用
yield返回处理后的批次数据 (x_batch,y_batch) 或仅x_batch。
代码:load_image_batch 函数核心片段
def load_image_batch(path_list, label_list, target_size):
batch_size = len(path_list)
# 初始化图像批次数组
x_batch = np.zeros((batch_size, *target_size, 3))
# 初始化标签批次数组 (one-hot编码)
if label_list is not None:
y_batch = np.zeros((batch_size, num_classes))
for i in range(batch_size):
img_path = path_list[i]
# 使用OpenCV读取图像
img = cv2.imread(img_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转换通道顺序
img = cv2.resize(img, target_size)
x_batch[i] = img
if label_list is not None:
label = label_list[i]
y_batch[i, label] = 1 # one-hot编码
if label_list is not None:
return x_batch, y_batch
else:
return x_batch
4. 配置与启动训练
准备好生成器和模型后,我们需要编译模型,并配置训练参数。
代码:模型编译与训练配置
from tensorflow.keras.callbacks import ModelCheckpoint
# 编译模型,指定损失函数和评估指标
model.compile(optimizer=‘adam‘,
loss=‘categorical_crossentropy‘,
metrics=[‘accuracy‘])
# 设置模型检查点回调,仅保存验证集上性能最好的模型
checkpoint_callback = ModelCheckpoint(
filepath=‘best_model.h5‘,
monitor=‘val_accuracy‘,
verbose=1,
save_best_only=True,
mode=‘max‘
)
# 计算每轮的步数 (steps_per_epoch)
train_steps = len(train_image_paths) // batch_size
val_steps = len(val_image_paths) // batch_size
# 启动训练
history = model.fit(
train_generator,
steps_per_epoch=train_steps,
validation_data=val_generator,
validation_steps=val_steps,
epochs=100,
callbacks=[checkpoint_callback],
shuffle=True
)
总结
本节课中,我们一起学习了行人重识别(ReID)项目训练流程的完整实现。我们从理解ReID作为图像检索任务的本质出发,明确了训练一个通用特征提取器的目标。随后,我们详细拆解了代码实现的关键部分:
- 数据准备:解析ID标签并编码,划分数据集。
- 模型构建:基于预训练CNN骨干网络,改造为适用于ReID分类任务的模型。
- 数据流水线:实现了支持批量加载、数据增强和归一化的数据生成器,这是处理大规模数据集的关键。
- 训练流程:配置了损失函数、优化器以及模型保存回调,启动了模型的训练过程。


通过本课的学习,你已经掌握了搭建一个ReID模型训练框架的核心技能。下节课,我们将重点学习如何使用训练好的模型进行特征提取,并完成对模型性能的评测。


📊 课程1447-P4:数据分析与特征工程串讲
在本节课中,我们将要学习数据分析与特征工程的核心概念与实践方法。我们将从理解问题与数据开始,逐步深入到数据清洗、特征构建、模型训练与验证的全过程,并通过一个具体的租房热度预测案例来巩固所学知识。
🎯 第一部分:问题识别与数据理解
在开始数据分析之前,我们首先需要对问题进行抽象和建模。这决定了我们最终的解题方法和流程。
机器学习任务可以根据场景进行划分,例如分类、回归、排序或无监督学习。划分的依据通常是数据中是否存在标签(label),以及标签的类型。
- 有监督学习:数据带有标签。
- 分类问题:标签是离散的类别(例如:预测用户是否违约)。
- 回归问题:标签是连续的数值(例如:预测房屋价格)。
- 排序问题:标签是次序关系。
- 无监督学习:数据没有标签。
- 典型任务包括降维(如PCA)和聚类(如K-Means)。
此外,数据的类型也决定了适用的算法。
- 结构化数据:以表格形式存储,适合使用XGBoost、LightGBM等树模型。
- 非结构化数据:如图片、文本、语音,适合使用深度学习模型。
在实际项目中,解决问题的流程并非线性的“瀑布式”开发,而是一个需要不断迭代和回溯的循环过程。我们可能在数据清洗、特征工程、模型训练等任何环节发现问题,并返回之前的步骤进行调整。
核心公式:机器学习流程 ≈ 70% 数据处理 + 10% 模型训练 + 20% 迭代调优
🔍 第二部分:数据处理与分析实践
上一节我们介绍了如何从宏观上理解问题和数据,本节中我们来看看如何对具体的数据集进行探索性分析。
算法工程师70%以上的时间都花在与数据处理相关的工作上,例如数据清洗、查询和分析。模型训练本身所占时间相对较少。
我们将以“Two Sigma Connect: 租房热度预测”比赛数据集为例。该任务是根据房屋信息(如价格、地理位置、描述等)预测其受欢迎程度(热度),属于一个多分类问题(热度分为高、中、低三类)。
以下是该数据集包含的部分字段及其类型:
bathrooms: 数值型,卫生间数量。bedrooms: 数值型,卧室数量。building_id: 类别型,建筑物ID。created: 日期型,信息发布时间。description: 文本型,房屋描述。features: 列表型,房屋特点标签(如“可养宠物”、“有电梯”)。latitude/longitude: 数值型,经纬度。price: 数值型,价格。interest_level: 类别型,标签(高/中/低)。
在进行数据分析时,我们通常从整体分布和统计量入手。
数值型字段分析
对于数值型字段(如price, bathrooms),我们可以:
- 计算描述性统计:使用
pandas.DataFrame.describe()获取最小值、最大值、均值、分位数等。 - 绘制分布图:使用直方图(Histogram)或核密度估计图(KDE)观察数据分布形态。例如,价格数据通常呈现严重的左偏分布。
- 识别异常值:使用箱线图(Box Plot)。箱线图基于分位数(Q1, Q3)和四分位距(IQR)来定义数据的合理范围,超出上下界的数据点可被视为异常值。
代码示例:绘制价格分布与箱线图
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 加载数据
df = pd.read_json('train.json')
# 价格分布
sns.histplot(df['price'], kde=True)
plt.title('Price Distribution')
plt.show()
# 价格箱线图
sns.boxplot(x=df['price'])
plt.title('Price Boxplot')
plt.show()
类别型与文本型字段分析
对于类别型字段(如building_id, manager_id)或文本型字段(如description):
- 统计频次:使用
pandas.Series.value_counts()统计每个取值出现的次数。 - 可视化:
- 对于取值空间不大的类别字段,可使用柱状图。
- 对于文本字段,可以生成词云(Word Cloud)来展示高频词汇。
日期型字段分析
对于日期型字段(如created),可以提取年、月、日、星期几等信息,并绘制时间序列图,观察发布数量的周期性规律。

通过以上分析,我们可以对数据有直观的理解,并初步判断哪些字段可能与目标变量(房屋热度)强相关,例如价格、位置、中介ID等。
🛠️ 第三部分:特征工程原理与实践
在完成初步的数据分析后,我们需要将原始数据转化为机器学习模型能够更好理解的特征。这个过程就是特征工程。
类别特征编码
类别特征(如国家、城市)是字符串类型,必须转换为数值才能被模型处理。以下是几种常见的编码方式:
1. 独热编码 (One-Hot Encoding)
- 方法:为每个类别创建一个新的二进制列。
- 公式:如果类别有K种取值,则生成一个K维向量,对应类别的位置为1,其余为0。
- 优点:编码简单,适用于无序类别。
- 缺点:当类别取值很多时,会导致特征维度爆炸,数据变得非常稀疏。
- 适用模型:线性模型。
2. 标签编码 (Label Encoding)
- 方法:为每个类别分配一个唯一的整数ID(如0,1,2,...)。
- 优点:不增加特征维度。
- 缺点:会引入人为的次序关系,可能误导模型。更适合树模型。
- 适用模型:树模型(如决策树、随机森林)。
3. 频次编码 (Count / Frequency Encoding)
- 方法:用该类别在训练集中出现的次数(或频率)来代替类别本身。
- 优点:简单,包含了类别流行度信息。
- 缺点:如果训练集和测试集分布不一致,编码可能失效。
4. 目标编码 (Target Encoding)
- 方法:用该类别下目标变量的均值(或其他统计量)来编码。
- 公式:
编码值 = mean(目标变量 | 类别) - 优点:编码值具有明确的统计意义,模型容易学习。
- 缺点:极易造成标签信息泄露(Data Leakage),必须谨慎使用,通常需配合交叉验证。
代码示例:几种编码的实现
import pandas as pd
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
# 示例数据
data = {'country': ['China', 'USA', 'UK', 'China', 'Japan']}
df = pd.DataFrame(data)
# 1. Label Encoding
le = LabelEncoder()
df['country_label'] = le.fit_transform(df['country'])
# 2. One-Hot Encoding
df_onehot = pd.get_dummies(df['country'], prefix='country')
# 或者使用 sklearn
# ohe = OneHotEncoder(sparse=False)
# encoded = ohe.fit_transform(df[['country']])
数值特征处理
数值特征(如年龄、收入)也需要处理,以提升模型性能。
缩放与归一化:
- 最大最小值归一化 (MinMaxScaler):将数据缩放到[0, 1]区间。
x_scaled = (x - min) / (max - min) - 标准化 (StandardScaler):将数据转换为均值为0,标准差为1。
x_scaled = (x - mean) / std - 适用场景:归一化适用于分布范围有限的数据;标准化适用于近似正态分布的数据。
- 最大最小值归一化 (MinMaxScaler):将数据缩放到[0, 1]区间。
离散化 (Binning):将连续值分段,转化为有序的类别。例如,将年龄分为“少年”、“青年”、“中年”、“老年”。这需要人工根据业务知识来定义分箱边界。
交叉特征:通过已有特征进行组合生成新特征。
- 加减:
总房间数 = 卧室数 + 卫生间数 - 乘除:
房间均价 = 总价 / 总房间数 - 聚合特征:例如,统计同一卧室数量下的平均价格,然后计算当前价格与该平均价格的比值。
- 加减:
日期与文本特征提取
- 日期特征:从日期字段中提取年、月、日、星期几、是否节假日、是否工作日等。
- 文本特征:可以从描述文本中提取长度、单词数、情感倾向等。更复杂的方法如TF-IDF将在后续课程介绍。
需要警惕的特征:数据泄露 (Data Leakage)
数据泄露特征是指那些在训练时包含未来或目标信息,但在实际预测时无法获取的特征。例如,在按时间排序的数据中,如果用“记录创建时间”来预测“热度”,而数据恰好是按热度高低顺序录入的,那么“创建时间”就成为了一个泄露特征。这类特征必须被识别并移除。
⚙️ 第四部分:模型训练、验证与代码实践
特征工程完成后,我们就可以进入模型训练阶段。本节中我们来看看如何划分数据、训练模型并防止过拟合。
数据集划分与交叉验证
为了客观评估模型性能,我们需要将数据划分为三部分:
- 训练集 (Training Set):用于训练模型参数。
- 验证集 (Validation Set):用于在训练过程中调整超参数、选择模型,监控是否过拟合。
- 测试集 (Test Set):用于最终评估模型的泛化能力,在全部调优完成后只使用一次。
K折交叉验证 (K-Fold Cross Validation) 是一种更充分利用数据、稳定评估模型的方法。它将训练集均匀分成K份,每次用其中K-1份训练,用剩下的1份验证,重复K次,最后取K次验证结果的平均值作为模型性能的估计。
过拟合与欠拟合
- 欠拟合:模型在训练集和验证集上表现都差。解决方法:增加模型复杂度、增加特征。
- 过拟合:模型在训练集上表现很好,但在验证集上表现差。解决方法:获取更多数据、降低模型复杂度、正则化、早停(Early Stopping)。
早停法:在训练迭代过程中(如深度学习或梯度提升树),持续监控模型在验证集上的性能。当验证集性能不再提升甚至开始下降时,停止训练。这是防止过拟合的有效手段。
案例实践:租房热度预测流程
以下结合“Two Sigma”案例,简述特征工程与建模流程:
- 数据读取与探索:使用Pandas读取JSON数据,进行初步的统计分析。
- 特征构建:
- 统计特征:从
photos,features列表字段中提取长度(照片数量、标签数量)。 - 文本特征:计算
description字段的单词数量。 - 日期特征:从
created中提取年、月、日、小时等。 - 交叉特征:创建
房间总数、每间房均价等。 - 目标编码:对
manager_id、building_id等高频类别字段,采用五折交叉验证下的目标编码,防止数据泄露。
- 统计特征:从
- 编码与归一化:对剩余的类别特征进行标签编码,对数值特征进行标准化。
- 模型训练与验证:
- 使用
KFold进行五折交叉验证。 - 在每一折中,用训练部分拟合模型(如XGBoost),在验证部分评估。
- 五折训练完成后,得到五个模型,对测试集的预测结果取平均,作为最终预测。
- 使用
- 模型集成(进阶):可以采用堆叠法。即用多个不同的基模型(如随机森林、XGBoost)进行第一层预测,然后将它们的预测结果作为新特征,训练一个第二层的“元模型”来融合结果,往往能提升性能。
核心思想:特征工程与模型选择紧密相关。树模型对数值缩放不敏感,能较好处理类别特征;而线性模型则对归一化和独热编码依赖更强。
📝 总结与回顾
本节课中我们一起学习了数据分析与特征工程的完整链路:
- 问题抽象:首先明确任务是分类、回归还是其他类型,并理解数据是结构化还是非结构化。
- 数据分析:通过统计描述和可视化(直方图、箱线图、词云等)理解每个字段的分布、识别异常值、探索字段与标签的潜在关系。
- 特征工程:这是提升模型性能的关键。我们学习了针对类别特征(独热、标签、目标编码)、数值特征(缩放、离散化、交叉)以及日期文本特征的处理方法,并强调了警惕数据泄露的重要性。
- 模型训练与验证:介绍了数据集划分、交叉验证、过拟合/欠拟合的概念及应对策略(如早停法),并通过案例简述了从特征构建到模型集成的实践流程。


记住,没有放之四海而皆准的特征工程方法。最佳实践来源于对业务问题的深刻理解、对数据分布的细致分析,以及通过实验不断迭代优化。希望本教程能帮助你建立起数据分析与特征工程的系统性思维。

机器学习集训营第15期 - P5:决策树、随机森林与GBDT 🌲


在本节课中,我们将学习一类在工业界和数据科学竞赛中极为重要的模型——树模型。我们将从基础的决策树开始,了解其工作原理和构建方法,然后探讨如何通过集成学习构建更强大的模型,如随机森林。这些模型以其直观的逻辑、强大的预测能力和对数据预处理要求较低的特点而广受欢迎。


从逻辑回归到决策树


上一节我们介绍了逻辑回归模型,它通过线性组合特征并经过Sigmoid函数转换来得到分类概率。其核心公式为:

P = σ(WX + b)

其中,σ代表Sigmoid函数。

然而,人类的决策过程往往更直观,更像是一系列“如果...那么...”的规则判断。例如,决定是否去相亲,可能会基于一系列条件:如果年龄 > 30岁,则不去;否则,如果长相不佳,则不去;否则,如果收入高,则去... 这种基于规则链的决策方式,就是决策树模型的直观体现。



决策树模型是“透明”的,其决策逻辑清晰可见,具有很强的可解释性。


决策树模型详解



决策树是一种基于树结构进行决策的模型。其基本构成如下:
- 内部节点:代表对某个属性(特征)进行判断。
- 分支:代表该属性判断的一种可能结果(取值)。
- 叶子节点:代表最终的决策结果(分类标签或回归值)。





预测时,只需从根节点开始,根据样本的属性值选择分支向下遍历,直到到达某个叶子节点,即可得到预测结果。

决策树的核心问题:生长与停止
构建决策树需要解决两个核心问题:如何生长(选择划分属性)和何时停止。
以下是决策树停止生长的三种条件:
- 当前节点包含的样本全属于同一个类别:无需再划分。
- 当前属性集为空,或所有样本在所有属性上取值相同:无法找到有效的属性进行进一步划分。
- 当前节点包含的样本集合为空:无法划分。


如何选择最优划分属性

决策树生长的关键在于,在每个节点上选择哪个属性进行划分能最大程度地提升模型的“纯度”。我们引入信息熵来衡量样本集合的不纯度。


信息熵公式:
Ent(D) = - Σ (pk * log₂(pk))
其中,pk是当前样本集合D中第k类样本所占的比例。Ent(D)的值越小,则D的纯度越高。




我们希望划分后数据的不纯度降低。因此,可以用信息增益来衡量某个属性a进行划分所带来的“纯度提升”:




信息增益公式:
Gain(D, a) = Ent(D) - Σ (|Dᵛ| / |D|) * Ent(Dᵛ)
其中,v是属性a的取值,Dᵛ是D中在属性a上取值为v的样本子集。我们选择信息增益最大的属性作为当前节点的划分属性。




基于信息增益的决策树算法称为ID3。




决策树的演变:ID3、C4.5与CART
ID3算法倾向于选择取值数目较多的属性(如“学号”),这可能导致过拟合。C4.5算法使用信息增益率来改进这一点,它通过除以属性a本身的“固有值”(属性熵)来抵消属性取值数目带来的影响。

信息增益率公式:
Gain_ratio(D, a) = Gain(D, a) / IV(a)
IV(a) = - Σ (|Dᵛ| / |D|) * log₂(|Dᵛ| / |D|)
另一种广泛使用的算法是CART,它使用基尼系数来衡量不纯度,并且每次都生成二叉树。

基尼系数公式:
Gini(D) = 1 - Σ (pk²)
基尼系数反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。Gini(D)越小,数据集D的纯度越高。

有趣的是,通过对信息熵的泰勒展开进行一阶近似,可以发现信息熵和基尼系数在趋势上是一致的。



处理连续值特征
决策树同样可以处理连续值特征(如年龄)。处理方法是将连续值离散化。具体步骤为:
- 将样本在该特征上的取值排序。
- 考察每两个相邻取值的中点,作为候选划分点。
- 将每个候选划分点(如“年龄是否<22.5”)视为一个二值离散属性。
- 使用与离散属性相同的方法(计算信息增益/增益率/基尼系数)来评估这些候选划分点,选择最优的一个。
这样,连续的年龄特征就被转化为了一系列二值判断,决策树通过多次判断自然可以划分出“24岁到28岁”这样的区间。
回归树
决策树不仅可以用于分类,也可用于回归问题,此时称为回归树。与分类树预测类别不同,回归树预测的是连续值。


回归树的核心思想是:将特征空间划分为若干个互不重叠的区域(R1, R2, ..., Rm),并在每个区域上有一个固定的输出值c_m。对于回归问题,通常取该区域内所有样本目标值的均值作为c_m。

划分准则:通过递归二叉分裂,寻找最优的划分属性和划分点,使得划分后各区域的残差平方和最小。

RSS公式:
RSS = Σ ( Σ (y_i - c_m)² )
其中,外层求和遍历所有区域,内层求和遍历该区域内的所有样本。


为了防止过拟合(如将每个样本点都划分到单独区域),可以在损失函数中加入对模型复杂度的惩罚项,例如叶子节点数量的正则项。
集成学习与随机森林 🌳



单棵决策树可能不稳定,容易过拟合。集成学习通过构建并结合多个学习器来完成学习任务,可以获得比单一学习器更优越的性能。

Bagging思想



Bagging是并行式集成学习的代表方法,其核心是自助采样法。
- 从原始样本集中随机抽取(有放回)一个子集,用于训练一个基学习器。
- 重复上述过程T次,得到T个基学习器。
- 对于分类任务,采用投票法结合T个学习器的结果;对于回归任务,采用平均法。
Bagging通过样本扰动增加了基学习器之间的多样性,从而降低了整体模型的方差。


随机森林
随机森林是Bagging的一个扩展变体。它在以决策树为基学习器构建Bagging集成的基础上,进一步在决策树的训练过程中引入了属性扰动。
- 传统决策树在选择划分属性时,是从当前节点的所有属性集合中选择一个最优属性。
- 随机森林则先从该属性集合中随机选择一个包含k个属性的子集,然后再从这个子集中选择最优属性。参数k控制了随机性的程度。


因此,随机森林的基学习器的多样性不仅来自样本扰动,还来自属性扰动,这使得最终集成的泛化性能进一步提升。随机森林通常能产生非常平滑的决策边界,对噪声数据有很好的鲁棒性。


案例实践


案例一:决策树分类


我们使用一个经典数据集,根据人口普查信息(工作类型、教育程度、婚姻状况等)预测个人年收入是否超过5万美元。





以下是核心代码步骤:
# 1. 导入库与数据
import pandas as pd
from sklearn import preprocessing
from sklearn.tree import DecisionTreeClassifier, export_graphviz
data = pd.read_csv(‘decision_tree.csv‘)







# 2. 准备特征X和标签y
feature_names = [‘workclass‘, ‘education‘, ‘marital-status‘, ...]
X = data[feature_names]
y = data[‘income‘]

# 3. 特征工程:将类别特征转换为数值(如独热编码)
# ... (使用preprocessing进行处理)







# 4. 构建并训练决策树模型
clf = DecisionTreeClassifier(criterion=‘entropy‘, max_depth=4)
clf.fit(X_train, y_train)








# 5. 可视化决策树
export_graphviz(clf, out_file=‘tree.dot‘, feature_names=feature_names, class_names=[‘<=50K‘, ‘>50K‘], filled=True)



案例二:随机森林回归








我们使用波士顿房价数据集,根据房屋的各种特征(犯罪率、房间数等)预测其价格。





以下是核心代码步骤:
# 1. 导入库与数据
from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
boston = load_boston()
X, y = boston.data, boston.target






# 2. 构建并训练随机森林回归模型
# n_estimators: 森林中树的数量
# max_features: 寻找最佳分割时考虑的特征数(可设为特征总数的百分比,如0.6)
regr = RandomForestRegressor(n_estimators=15, max_features=0.6, random_state=0)
regr.fit(X, y)




# 3. 进行预测
print(regr.predict(X[:5]))








总结

本节课我们一起学习了树模型的核心知识。我们从决策树的基本概念出发,探讨了其构建过程(如何选择划分属性、何时停止生长),并介绍了三种主要算法:ID3、C4.5和CART。接着,我们将决策树扩展到回归问题,了解了回归树的工作原理。最后,我们学习了集成学习中的Bagging思想以及以其为基础的强大模型——随机森林,它通过结合多棵决策树并引入样本和特征的双重随机性,极大地提升了模型的泛化能力和鲁棒性。树模型以其直观、强大且易于使用的特点,成为机器学习工具箱中不可或缺的利器。


机器学习集训营第15期 - P6:图像与文本基础教程 📚
概述
在本节课中,我们将学习机器学习中处理非结构化数据的两大核心领域:文本和图像的基础知识。我们将了解它们的数据特点、常见的处理方法、特征提取技术,并通过实践案例加深理解。
第一部分:数据类型介绍 📊
在上一节课程中,我们介绍了结构化数据。本节中,我们来看看非结构化数据。
算法工程师的岗位通常与特定任务方向相关,例如推荐系统、计算机视觉(CV)或自然语言处理(NLP)。不同类型的数据需要不同的技能和方法来解决。
数据主要分为结构化数据和非结构化数据。
- 结构化数据:指表格类型的数据,可以使用Pandas等库方便地读取和处理。
- 非结构化的数据:指不适合用表格形式表示的数据,主要包括文本、图像和视频。
视频可以看作是图像帧与音频的组合。理解不同数据类型的差异是选择正确解决方法的第一步。例如,虽然图片分类和文本分类都是分类任务,但由于数据类型不同,其模型和侧重点也不同。
第二部分:文本数据处理基础 📝
上一部分我们区分了数据类型,本节中我们来看看如何处理文本数据。
自然语言处理(NLP)旨在让机器理解人类的自然语言。互联网上超过80%的信息以文本形式存在。NLP主要分为两大方向:
- 自然语言理解(NLU):理解给定文本的核心含义。
- 自然语言生成(NLG):根据理解的含义组织语言生成新的文本。
NLP任务非常复杂,因为语言是开放、多变且依赖上下文的。
以下是NLP中常见的任务类型:
- NLU任务:垃圾邮件识别、情感分析、意图识别(本质是文本分类)。
- NLG任务:聊天机器人、机器翻译。
处理文本时,一个核心挑战是文本本质上是不定长的单词列表,而机器学习模型通常需要固定长度的数值输入。
文本特征提取:从词频到TF-IDF
以下是两种基础的文本特征提取方法。
1. 词频向量化(CountVectorizer)
该方法统计每个单词在文档中出现的次数,将文本转换为一个固定长度的向量。向量的每个位置对应一个单词,值表示该单词在文档中出现的频率。
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)
2. TF-IDF向量化
TF-IDF 衡量一个单词在文档中的重要程度,由两部分组成:
- 词频(TF):单词在当前文档中出现的频率。
- 逆文档频率(IDF):单词在所有文档中出现的频率的倒数,用于降低常见词的重要性。
TF-IDF = TF * IDF
在scikit-learn中,可以方便地使用TfidfVectorizer。
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)
注意:中文文本需要先进行分词,再用空格连接,才能使用上述基于空格分隔的方法。
实践:文本分类流程
一个完整的文本分类流程通常包括以下步骤:
- 文本预处理:统一大小写、去除噪声、分词、去除停用词等。
- 特征提取与表示:使用如
CountVectorizer或TfidfVectorizer将文本转换为数值特征。 - 分类器训练:使用逻辑回归、朴素贝叶斯或XGBoost等模型进行分类。
不同的模型对数据有不同的偏好。例如,树模型(如XGBoost)在处理数值型特征(如TF-IDF)时可能不如线性模型(如逻辑回归)或朴素贝叶斯表现好。
第三部分:图像数据处理基础 🖼️
上一节我们介绍了文本处理,本节中我们来看看图像处理。
计算机视觉(CV)的目标是让机器理解和处理图像与视频。图像在计算机中本质上是一个三维矩阵(高度 × 宽度 × 通道数)。
图像特征提取方法
图像特征提取旨在从高维的像素矩阵中提取有意义的、低维度的信息。
以下是几种常见的图像特征提取方法:
- 图像哈希:将图片内容缩放到小尺寸(如8x8),根据像素值与均值的比较生成二进制序列,再转换为哈希字符串。适用于图片去重。
- 颜色直方图:统计图像中各颜色(或灰度)强度值的像素数量。这是一种全局特征,用于衡量图像在颜色空间上的整体相似性。
- 关键点检测(如SIFT):检测图像中梯度变化明显的局部特征点(如角点、边缘),并生成描述该点周围区域的特征向量。SIFT特征具有尺度不变性和旋转不变性。
- 卷积神经网络(CNN)特征:使用预训练的深度学习模型(如ResNet)提取图像的深层特征。这些特征通常具有强大的表征能力。
实践:图像分类案例
我们以手写数字识别(MNIST数据集)为例,展示两种方法:
1. 使用深度学习框架(Keras)搭建CNN模型
# 示例代码结构
model = Sequential([
Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
MaxPooling2D((2,2)),
Conv2D(64, (3,3), activation='relu'),
MaxPooling2D((2,2)),
Flatten(),
Dense(64, activation='relu'),
Dense(10, activation='softmax')
])
model.compile(...)
model.fit(...)
2. 使用Scikit-learn的MLP分类器
from sklearn.neural_network import MLPClassifier
mlp = MLPClassifier()
mlp.fit(X_train, y_train)
score = mlp.score(X_test, y_test)
通常情况下,精心设计的CNN模型在图像分类任务上的准确率会高于传统的全连接网络(MLP)或线性模型。
第四部分:核心要点总结 🎯
本节课中我们一起学习了图像与文本处理的基础知识。
- 数据驱动方法选择:没有放之四海而皆准的“银弹”。处理不同类型的数据(结构化/非结构化,文本/图像)需要选择不同的预处理方法和模型。
- 模型与数据的匹配:算法工程师的核心能力之一是针对具体数据构建合适的模型。不同模型对数据特征有不同的偏好。
- 基础相通性:尽管NLP和CV的模型和技术细节不同,但机器学习的核心基础是相通的,例如数据集划分、过拟合判断、模型评估等。
- 学习路径建议:对于初学者,建议从基础任务入手,如文本分类或图像分类,并熟练掌握
scikit-learn、Keras/PyTorch等核心工具库的使用。



希望本教程能帮助你建立起对非结构化数据处理的基本认识,为后续深入学习NLP和CV打下坚实的基础。


📚 课程名称:1447-七月在线-机器学习集训营15期 - P7:3-SVM与数据分类
🎯 概述
在本节课中,我们将要学习支持向量机(SVM)模型。主要内容包括SVM模型的基本推导、在文本分类上的实战应用。在模型推导过程中,我们将讲解线性可分分类器、线性不可分数据的处理、SVM的求解算法(如SMO算法),并串联相关的数学知识。
📖 1. 函数间隔与几何间隔
首先,我们介绍函数间隔和几何间隔的概念。假设有一个二维数据集,黑点代表正类,空心点代表负类。该数据集是线性可分的,因为可以画一条直线将两类完全分开。
这条直线的方程是 W·X + B = 0。在直线上的点代入方程后结果为0,直线下方的点结果小于0,直线上方的点结果大于0。
对于一个线性可分的数据集,可以画出无数条直线将其分开,但哪一条是最好的呢?我们需要一个衡量标准。
1.1 函数间隔
对于一个数据点,其函数间隔定义为:
[
\hat{\gamma}_i = y_i (W \cdot x_i + B)
]
其中,( y_i ) 是类别标签(+1 或 -1)。函数间隔的几何意义是该点到直线的带符号距离。
1.2 几何间隔
函数间隔会随着 ( W ) 和 ( B ) 的等比例缩放而改变,不是一个绝对的度量。因此,我们引入几何间隔:
[
\gamma_i = \frac{y_i (W \cdot x_i + B)}{||W||}
]
几何间隔代表了点到直线的垂直距离,是一个绝对度量。
🧮 2. 最大间隔分类器
上一节我们介绍了间隔的概念,本节中我们来看看如何找到“最好”的分类直线。
最好的分类直线是使所有数据点的最小几何间隔最大的那条直线。这被称为最大间隔分类器。
我们的优化目标可以形式化为:
[
\max_{W, B} \gamma
]
[
\text y_i (W \cdot x_i + B) \geq \gamma, \quad i = 1, \ldots, m
]
[
||W|| = 1
]
为了简化问题,我们可以进行如下变换:
- 令函数间隔 (\hat{\gamma} = 1)。因为 ( W ) 和 ( B ) 可以等比例缩放,这总是可以做到的。
- 最大化 ( 1 / ||W|| ) 等价于最小化 ( \frac{1}{2} ||W||^2 )。加上 ( \frac{1}{2} ) 是为了后续求导方便。
最终,问题转化为一个带约束的优化问题:
[
\min_{W, B} \frac{1}{2} ||W||^2
]
[
\text y_i (W \cdot x_i + B) \geq 1, \quad i = 1, \ldots, m
]
🧠 3. 拉格朗日乘子法与对偶问题
上一节我们将问题转化为一个带不等式约束的优化问题。本节中,我们引入拉格朗日乘子法来求解它。
3.1 拉格朗日函数
对于优化问题:
[
\min f(w) \quad \text g_i(w) \leq 0, \quad h_j(w) = 0
]
我们构造拉格朗日函数:
[
L(w, \alpha, \beta) = f(w) + \sum_i \alpha_i g_i(w) + \sum_j \beta_j h_j(w)
]
其中,( \alpha_i \geq 0 )。
3.2 原始问题与对偶问题
原始问题可以表述为极小极大问题:
[
p^* = \min_w \max_{\alpha \geq 0, \beta} L(w, \alpha, \beta)
]
其对偶问题是极大极小问题:
[
d^* = \max_{\alpha \geq 0, \beta} \min_w L(w, \alpha, \beta)
]
通常,( d^* \leq p^* )。在满足一定条件(如Slater条件)时,强对偶成立,即 ( d^* = p^* )。
3.3 KKT条件
当强对偶成立时,最优解必须满足KKT条件(Karush-Kuhn-Tucker条件):
- 稳定性条件:( \nabla_w L = 0 )
- 原始可行性:( g_i(w) \leq 0, \quad h_j(w) = 0 )
- 对偶可行性:( \alpha_i \geq 0 )
- 互补松弛条件:( \alpha_i g_i(w) = 0 )
⚙️ 4. SVM的求解:SMO算法
上一节我们通过拉格朗日法得到了SVM的对偶问题。本节中,我们来看如何求解这个对偶问题。
将最大间隔分类器的原始问题代入拉格朗日函数,得到其对偶形式:
[
\max_{\alpha} \sum_^m \alpha_i - \frac{1}{2} \sum_^m \sum_^m \alpha_i \alpha_j y_i y_j (x_i \cdot x_j)
]
[
\text \sum_^m \alpha_i y_i = 0, \quad \alpha_i \geq 0, \quad i = 1, \ldots, m
]
这是一个二次规划问题。由于约束条件 ( \sum \alpha_i y_i = 0 ) 的存在,不能单独优化每个 ( \alpha_i )。
4.1 SMO算法思想
SMO(Sequential Minimal Optimization)算法的核心思想是:每次只选择两个变量 ( \alpha_i, \alpha_j ) 进行优化,固定其他变量。由于存在线性约束 ( \sum \alpha_i y_i = 0 ),优化两个变量可以保持约束成立。
以下是SMO算法的简化流程:
- 选择一对需要更新的变量 ( \alpha_i ) 和 ( \alpha_j )。
- 固定其他 ( \alpha ),仅对 ( \alpha_i ) 和 ( \alpha_j ) 进行优化,求解使目标函数最大化的值。
- 重复以上步骤,直到收敛。
🔄 5. 处理线性不可分:核技巧
前面我们假设数据是线性可分的。本节中,我们来看看当数据线性不可分时,SVM如何处理。
基本思想是:将数据从原始空间映射到一个更高维的特征空间,使其在新空间中线性可分。
设映射函数为 ( \phi(x) ),则在新空间中的分类模型为:
[
f(x) = W \cdot \phi(x) + B
]
对应的对偶问题中,内积 ( x_i \cdot x_j ) 被替换为 ( \phi(x_i) \cdot \phi(x_j) )。
5.1 核函数
直接计算高维空间的内积 ( \phi(x_i) \cdot \phi(x_j) ) 可能计算量很大。核函数 ( K(x_i, x_j) ) 定义了在原始空间中直接计算这个内积的方法:
[
K(x_i, x_j) = \phi(x_i) \cdot \phi(x_j)
]
这样,我们无需显式地计算映射 ( \phi ),也无需知道 ( \phi ) 的具体形式。
5.2 常用核函数
以下是几种常用的核函数:
- 线性核:( K(x, z) = x \cdot z )
- 多项式核:( K(x, z) = (x \cdot z + c)^d )
- 高斯核(RBF核):( K(x, z) = \exp(-\frac{||x - z||2}{2\sigma2}) )
- Sigmoid核:( K(x, z) = \tanh(\beta x \cdot z + \theta) )
🛡️ 6. 处理线性不可分:软间隔分类器
即使使用了核技巧映射到高维空间,数据中仍可能存在噪声或异常点,导致严格线性可分不可行。本节介绍软间隔分类器。
软间隔分类器允许一些样本不满足约束条件 ( y_i (W \cdot x_i + B) \geq 1 \),但对这些“违规”的样本施加惩罚。
优化目标修改为:
[
\min_{W, B, \xi} \frac{1}{2} ||W||^2 + C \sum_^m \xi_i
]
[
\text y_i (W \cdot x_i + B) \geq 1 - \xi_i, \quad \xi_i \geq 0, \quad i = 1, \ldots, m
]
其中,( \xi_i ) 是松弛变量,衡量第 ( i ) 个样本违反间隔的程度;( C > 0 ) 是惩罚参数,控制对误分类的惩罚力度。
引入拉格朗日乘子后,其对偶问题与硬间隔形式类似,只是约束变为 ( 0 \leq \alpha_i \leq C )。
📉 7. 合页损失函数视角
除了间隔最大化的视角,SVM还可以从损失函数最小化的角度来理解。


SVM的优化目标等价于最小化以下合页损失函数(Hinge Loss):
[
\min_{W, B} \sum_^m \max(0, 1 - y_i (W \cdot x_i + B)) + \lambda ||W||^2
]
其中,( \lambda ) 是正则化参数。
合页损失函数的特点是:当样本被正确分类且函数间隔大于1时,损失为0;否则,损失线性增长。
🏷️ 8. SVM与多分类问题
标准的SVM是二分类器。本节中,我们看看如何将其扩展到多分类问题。主要有三种策略:
- 一对多(One-vs-Rest, OvR):为每个类别训练一个二分类器,将该类与其他所有类区分。需要训练 ( K ) 个分类器。
- 一对一(One-vs-One, OvO):为每两个类别训练一个二分类器。需要训练 ( \frac{K(K-1)}{2} ) 个分类器,预测时通过投票决定最终类别。
- 层次SVM:构建一个二叉树结构,每个节点是一个二分类SVM,将类别集不断二分。

💻 9. 实战:SVM用于文本分类
理论部分已经讲解完毕,本节我们将进行实战,使用SVM解决一个文本分类问题。
我们将使用一个新闻分类数据集,包含10个类别(如体育、政治等)。基本流程如下:
9.1 文本处理流程
以下是文本分类的主要步骤:
- 分词:将中文文本切分成词语序列(使用结巴分词等工具)。
- 特征筛选:
- 去除停用词(如“的”、“了”等无实义词)。
- 根据词频等信息筛选重要特征。
- 文本向量化:将分词后的文本表示为数值向量。常用方法有:
- 词袋模型(Bag-of-Words):统计每个词出现的次数。
- TF-IDF:衡量词语的重要性。
- 训练SVM模型:将向量化后的数据输入SVM进行训练。
- 预测与评估:使用训练好的模型对新文本进行分类,并评估准确率。
9.2 代码实现要点
我们将使用 libsvm 库来实现SVM。以下是一些关键步骤的伪代码示意:

# 1. 读取数据,分词
import jieba
def segment_text(input_file, output_file):
with open(input_file, 'r', encoding='utf-8') as fin, \
open(output_file, 'w', encoding='utf-8') as fout:
for line in fin:
label, text = line.strip().split('\t', 1)
words = ' '.join(jieba.cut(text)) # 分词并用空格连接
fout.write(f"{label}\t{words}\n")
# 2. 构建词表,并将文本转化为特征向量(如TF-IDF)
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(max_features=5000) # 选择最重要的5000个特征
X_train = vectorizer.fit_transform(train_texts) # 训练集文本列表
X_test = vectorizer.transform(test_texts) # 测试集文本列表
# 3. 使用SVM训练
from sklearn.svm import SVC
svm_model = SVC(kernel='linear', C=1.0) # 线性核,惩罚参数C=1
svm_model.fit(X_train, y_train)

# 4. 预测与评估
from sklearn.metrics import accuracy_score
y_pred = svm_model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"测试集准确率: {accuracy:.4f}")
作业:请使用Python代码,基于提供的数据集,实现完整的SVM文本分类流程,并尝试调整参数(如核函数、惩罚系数C)以达到92%-94%的分类准确率。
📝 总结
本节课中,我们一起学习了支持向量机(SVM)的核心内容:
- 核心思想:寻找最大几何间隔的超平面来进行分类。
- 数学推导:从函数间隔、几何间隔出发,通过拉格朗日乘子法将原始问题转化为对偶问题求解。
- 求解算法:介绍了用于高效求解SVM对偶问题的SMO算法。
- 关键扩展:
- 核技巧:通过核函数隐式地将数据映射到高维空间,以处理线性不可分数据。
- 软间隔:引入松弛变量和惩罚项,允许存在误分类,增强模型鲁棒性。
- 多分类策略:了解了一对多、一对一等多分类方法。
- 实战应用:完成了SVM在文本分类任务上的完整流程,包括分词、特征提取、模型训练与评估。


SVM是一个理论优美、应用广泛的经典机器学习算法。虽然深度学习如今盛行,但理解SVM的数学原理和思想,对于构建坚实的机器学习基础至关重要。

课程 1447-七月在线-机器学习集训营15期 - P8:04-CV-4-行人重识别项目(ReID)模型优化迭代及总结 🚶♂️➡️🚶♀️
在本节课中,我们将学习如何对行人重识别(ReID)模型进行评估,并引入一种强大的优化技术——三元组损失(Triplet Loss)来提升模型性能。我们将从评估流程的代码实现开始,逐步深入到Triplet Loss的原理、实现及其在ReID任务中的应用。
模型评估流程回顾
上一节我们完成了模型训练,本节中我们来看看如何对训练好的模型进行评估。评估的核心目标是:给定一个查询图像(query),从图库(gallery)中检索出属于同一行人的图像。
评估流程的代码逻辑如下:
- 加载模型与数据:加载训练好的模型,并读取测试集的query和gallery图像。注意,测试集的图像ID与训练集不重合。
- 构建特征提取模型:原始模型输出是分类概率。为了进行图像检索,我们需要一个输出为特征向量(feature)的模型。可以通过
model.get_layer方法获取中间层的输出,并以此构建新的特征提取模型。 - 生成特征向量:使用构建好的特征提取模型,分别对query和gallery图像进行预测,得到两组特征向量。通常会对这些特征进行归一化(L2 Normalize)处理。
- 计算相似度矩阵:通过矩阵乘法计算query特征与gallery特征之间的相似度矩阵。
similarity_matrix = np.dot(query_features, gallery_features.T) - 排序与匹配:对相似度矩阵的每一行(对应一个query)进行排序,取相似度最高的gallery图像ID作为预测结果。
- 计算准确率:将预测的ID与query图像的真实ID进行比对,计算Top-1准确率等指标。
以下是评估流程中几个关键步骤的代码片段:


# 构建特征提取模型
feature_extractor = Model(inputs=model.input, outputs=model.get_layer('target_layer_name').output)
feature_extractor.compile(optimizer='adam', loss='categorical_crossentropy') # optimizer仅用于编译,实际不使用
# 预测特征
query_features = feature_extractor.predict(query_generator)
query_features = normalize(query_features, axis=1) # L2归一化
# 计算相似度并排序
similarity_matrix = np.dot(query_features, gallery_features.T)
# 按相似度从大到小排序,获取索引
sorted_indices = np.argsort(-similarity_matrix, axis=1)
# 取每个query最相似的gallery索引(即第一列)
top1_pred_indices = sorted_indices[:, 0]
模型优化:引入三元组损失(Triplet Loss)
评估完成后,我们思考如何提升模型性能。本节将介绍一种常用于图像检索任务的损失函数——三元组损失(Triplet Loss)。
三元组损失的核心思想
三元组损失旨在学习一个特征嵌入空间(Embedding Space),使得在这个空间中:
- 相同类别的样本(正样本对)距离更近。
- 不同类别的样本(负样本对)距离更远。
具体而言,它每次处理一个三元组(Triplet) 样本:
- Anchor(锚点):作为参照的样本。
- Positive(正样本):与Anchor属于同一类别的样本。
- Negative(负样本):与Anchor属于不同类别的样本。
目标是让Anchor与Positive之间的距离,小于Anchor与Negative之间的距离,并且要至少小于一个边界值(margin)。
用公式表示如下:
Loss = max( d(A, P) - d(A, N) + margin, 0 )
其中,d() 表示距离函数(如欧氏距离),margin 是一个大于0的常数。
困难样本挖掘(Hard Triplet Mining)
简单地随机采样三元组效率可能不高。困难样本挖掘是指有选择地构建更有挑战性的三元组,以促使模型学习到更具判别力的特征。
在ReID任务中:
- Anchor 和 Positive:选择同一行人ID的不同图像。
- Negative:选择不同行人ID的图像。而“困难”的负样本,是指那些与Anchor在特征空间上容易混淆的不同ID样本(例如,穿着颜色、款式相似但ID不同的行人图像)。
三元组损失的代码实现
以下是三元组损失函数的一个基本实现示例:
import tensorflow as tf
from tensorflow.keras import backend as K
def triplet_loss(y_true, y_pred, margin=0.3):
"""
y_pred: 模型输出的特征向量,形状为 (batch_size * 3, embedding_dim)。
每连续三个样本依次为:Anchor, Positive, Negative。
"""
embedding_size = K.int_shape(y_pred)[-1]
# 将预测值重塑为 (batch_size, 3, embedding_dim)
y_pred = K.reshape(y_pred, (-1, 3, embedding_size))
# L2归一化
y_pred = K.l2_normalize(y_pred, axis=-1)
# 分割出A, P, N
anchor = y_pred[:, 0, :]
positive = y_pred[:, 1, :]
negative = y_pred[:, 2, :]
# 计算距离
pos_dist = K.sum(K.square(anchor - positive), axis=-1)
neg_dist = K.sum(K.square(anchor - negative), axis=-1)
# 计算损失
basic_loss = pos_dist - neg_dist + margin
loss = K.maximum(basic_loss, 0.0)
return K.mean(loss)
在模型中使用三元组损失
为了结合分类损失和三元组损失,我们采用多任务学习(Multi-task Learning) 框架。模型将有两个输出分支:
- 一个分支输出特征向量,用于计算三元组损失。
- 另一个分支输出分类概率,用于计算传统的交叉熵损失。
模型结构代码示例如下:
from tensorflow.keras.layers import Input, Dense, Lambda, GlobalAveragePooling2D
from tensorflow.keras.models import Model
# ... 假设 base_model 是特征提取骨干网络 ...
x = base_model.output
x = GlobalAveragePooling2D()(x)
# 分支1:用于Triplet Loss的特征输出(L2归一化)
triplet_feature = Lambda(lambda v: tf.math.l2_normalize(v, axis=1), name='triplet_feature')(x)
# 分支2:用于分类的输出
classification_output = Dense(num_classes, activation='softmax', name='classification')(x)
# 定义多输出模型
model = Model(inputs=base_model.input, outputs=[triplet_feature, classification_output])
相应地,在编译模型时,需要为两个输出指定各自的损失函数:
# 可以使用现成的Triplet Loss实现,例如 `tensorflow_addons.losses.TripletSemiHardLoss`
import tensorflow_addons as tfa
model.compile(
optimizer='adam',
loss={
'triplet_feature': tfa.losses.TripletSemiHardLoss(), # 三元组损失
'classification': 'categorical_crossentropy' # 分类损失
},
loss_weights={'triplet_feature': 0.5, 'classification': 0.5} # 可为不同损失设置权重
)
适配数据生成器(Data Generator)
由于模型现在需要两种监督信号(用于Triplet Loss的三元组和用于分类的标签),我们必须修改数据生成器。
新的生成器需要按批次生成三元组数据 (anchor, positive, negative) 及其对应的分类标签。一种常见的采样策略是 PK采样:
- P:每个批次采样P个不同的行人ID。
- K:对每个ID,随机采样K张图像。
- 批次大小则为
P * K。然后在这个批次内构造三元组。
数据生成器的输出需要与模型定义的两个输出严格对应。


学习率调度策略
最后,我们介绍一个训练技巧:学习率调度(Learning Rate Schedule)。它可以自动在训练到特定轮次时降低学习率,而无需手动干预。
以下是使用Keras回调函数实现学习率调度的示例:
from tensorflow.keras.callbacks import LearningRateScheduler, ModelCheckpoint
def lr_schedule(epoch):
"""在第40轮和70轮将学习率乘以0.1"""
initial_lr = 0.001
if epoch < 40:
return initial_lr
elif epoch < 70:
return initial_lr * 0.1
else:
return initial_lr * 0.01


lr_scheduler = LearningRateScheduler(lr_schedule)
checkpoint = ModelCheckpoint('best_model.h5', monitor='val_loss', save_best_only=True)
# 在 model.fit 的 callbacks 中同时加入
model.fit(..., callbacks=[checkpoint, lr_scheduler])
课程总结
本节课中我们一起学习了行人重识别(ReID)项目的两个关键部分:
- 模型评估:我们梳理了从加载模型、提取特征、计算相似度到最终匹配评估的完整流程,并理解了其背后的检索逻辑。
- 模型优化:我们深入探讨了三元组损失(Triplet Loss) 的原理,它通过拉近正样本对、推远负样本对的方式来学习更好的特征表示。我们介绍了困难样本挖掘的重要性,并展示了如何通过多任务学习将三元组损失与分类损失结合。最后,为了适配新的模型和损失函数,我们讲解了如何修改数据生成器,并介绍了学习率自动调度的技巧。


通过本课的学习,你不仅掌握了ReID项目的评估方法,还获得了提升模型性能的重要工具和思路。理解数据、模型与损失函数之间的协同设计,是解决复杂计算机视觉任务的关键。


🚀 机器学习集训营15期 - P9:基于SQL的机器学习流程和实践

在本节课中,我们将学习如何在大数据环境下,使用SQL和Spark进行机器学习。我们将从SQL基础开始,逐步深入到Spark的核心概念和实际操作,并通过具体的机器学习案例来巩固所学知识。
📚 第一部分:SQL基础与大数据开发
SQL是用于访问和操作数据库的标准计算机语言。它能够完成数据的插入、查询、更新和删除等所有与数据库相关的操作。常见的数据库软件如MySQL、SQL Server和Oracle都使用SQL进行交互。
SQL是计算机专业的基础技能,也是大数据开发的必备技能。它独立于编程语言,与数据库紧密相关。学习SQL成本低,简单易懂,建议没有基础的同学花一天时间系统学习。




SQL与Pandas的对比
SQL和Pandas在数据处理上有许多相似之处,只是实现方式不同。以下是两者的对比:
- 数据选择:
- SQL:
SELECT * FROM tips LIMIT 5; - Pandas:
tips[['total_bill', 'tip', 'smoker', 'time']].head(5)
- SQL:
- 条件筛选:
- SQL:
SELECT * FROM tips WHERE time = 'Dinner' LIMIT 5; - Pandas:
tips[tips['time'] == 'Dinner'].head(5)
- SQL:
- 分组聚合:
- SQL:
SELECT sex, COUNT(*) FROM tips GROUP BY sex; - Pandas:
tips.groupby('sex').size()
- SQL:
- 表连接:
- SQL:
SELECT * FROM df1 INNER JOIN df2 ON df1.key = df2.key; - Pandas:
pd.merge(df1, df2, on='key')
- SQL:
大数据开发岗位


大数据开发主要分为三类岗位:
- 大数据开发平台与管理:如数据仓库工程师、数据运维工程师。
- 大数据平台的数据分析、挖掘与机器学习:如数据开发工程师、算法工程师。
- 大数据分析结果展示:如数据分析师。
我们重点关注第二个方向,即大数据平台下的机器学习和数据应用技能。掌握SQL和Spark对于应聘大公司的算法工程师或数据开发工程师岗位至关重要。
⚡ 第二部分:Spark介绍与使用
上一节我们介绍了SQL的基础知识,本节中我们来看看大数据处理框架Spark。Spark是加州大学伯克利分校开发的一个基于内存的并行计算框架,是目前流行的大数据处理工具之一。

Spark的优势

- 运行速度快:基于内存计算,速度比Hadoop快10倍以上。
- 易用性好:支持Scala、Java、Python和R等多种语言。
- 生态完善:支持Spark SQL(交互式查询)、Spark Streaming(流处理)、MLlib(机器学习库)和GraphX(图计算库)。
- 数据源丰富:可以从CSV、JSON、MySQL、Hive等多种数据源读取数据。



Spark的核心概念:RDD



RDD是Spark最基础的数据抽象,代表一个不可变、可分区的数据集合。Spark通过构建有向无环图来优化RDD的操作流程,实现高效的数据处理。


创建RDD示例:
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("example").getOrCreate()
sc = spark.sparkContext
data = sc.parallelize(range(14), 4) # 创建RDD,分为4个分区
squared_rdd = data.map(lambda x: x*x) # 对每个元素进行平方操作(惰性计算)
print(squared_rdd.collect()) # 触发实际计算并收集结果


Spark的核心概念:DataFrame
DataFrame是一种以命名列方式组织的分布式数据集,类似于关系型数据库中的表或Pandas中的DataFrame。它提供了丰富的操作接口,并且底层由RDD实现。
创建与操作DataFrame示例:
# 从JSON文件创建DataFrame
df = spark.read.json("people.json")
df.show()
# 查看结构
df.printSchema()
# 选择列
df.select("name").show()
df.select(df['name'], df['age'] + 1).show()
# 条件筛选
df.filter(df['age'] > 21).show()
# 分组聚合
df.groupBy("age").count().show()
🔧 第三部分:Spark SQL使用案例
了解了RDD和DataFrame的基本操作后,本节我们来看看如何使用更接近SQL语法的Spark SQL进行数据查询和处理。
使用Spark SQL查询
首先需要将DataFrame注册为一个临时视图,然后即可使用SQL语句进行查询。


# 将DataFrame注册为临时视图
df.createOrReplaceTempView("people")
# 使用Spark SQL查询
spark.sql("SELECT * FROM people").show()
spark.sql("SELECT name FROM people WHERE age > 20").show()






数据转换:RDD与DataFrame的互操作


在实际工作中,经常需要在RDD和DataFrame之间进行转换。
1. 通过反射推断Schema(推荐):
这种方法通过样本数据自动推断数据类型。
from pyspark.sql import Row
# 从文本文件创建RDD
lines = sc.textFile("people.txt")
parts = lines.map(lambda l: l.split(","))
# 将RDD映射为Row对象
people_rdd = parts.map(lambda p: Row(name=p[0], age=int(p[1])))
# 通过反射创建DataFrame
people_df = spark.createDataFrame(people_rdd)
people_df.show()
2. 通过编程方式指定Schema:
这种方法需要显式定义数据结构,适用于已知Schema的情况。
from pyspark.sql.types import StructType, StructField, StringType, IntegerType
# 定义Schema
schema = StructType([
StructField("name", StringType(), True),
StructField("age", IntegerType(), True)
])
# 将RDD转换为Row对象,并与Schema结合创建DataFrame
row_rdd = parts.map(lambda p: Row(p[0], int(p[1])))
people_df2 = spark.createDataFrame(row_rdd, schema)
people_df2.show()
🤖 第四部分:Spark机器学习案例
最后,我们将运用前面所学的知识,在Spark平台上实现完整的机器学习流程。Spark MLlib库提供了常见的机器学习算法。
案例一:垃圾邮件分类(文本分类)
本案例演示如何使用Spark MLlib的Pipeline进行文本处理和朴素贝叶斯分类。
核心流程:
- 数据读取与预览:读取CSV格式的邮件数据。
- 特征工程:使用Tokenizer分词、StopWordsRemover去除停用词、CountVectorizer计算词频、IDF计算逆文档频率。
- 标签编码:使用StringIndexer将字符串标签(如“spam”, “ham”)转换为数值。
- 构建Pipeline:将上述步骤和朴素贝叶斯分类器串联。
- 训练与评估:划分训练集和测试集,训练模型并评估准确率。
from pyspark.ml import Pipeline
from pyspark.ml.feature import Tokenizer, StopWordsRemover, CountVectorizer, IDF, StringIndexer
from pyspark.ml.classification import NaiveBayes
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
# 1. 读取数据
df = spark.read.csv("spam.csv", inferSchema=True, sep='\t')
df = df.withColumnRenamed("_c0", "class").withColumnRenamed("_c1", "text")
# 2. 定义Pipeline阶段
tokenizer = Tokenizer(inputCol="text", outputCol="words")
remover = StopWordsRemover(inputCol="words", outputCol="filtered")
cv = CountVectorizer(inputCol="filtered", outputCol="rawFeatures")
idf = IDF(inputCol="rawFeatures", outputCol="features")
label_indexer = StringIndexer(inputCol="class", outputCol="label")
nb = NaiveBayes()
# 3. 构建Pipeline
pipeline = Pipeline(stages=[label_indexer, tokenizer, remover, cv, idf, nb])
# 4. 划分数据集并训练
train_df, test_df = df.randomSplit([0.7, 0.3], seed=42)
model = pipeline.fit(train_df)
predictions = model.transform(test_df)
# 5. 评估
evaluator = MulticlassClassificationEvaluator(labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print(f"测试集准确率: {accuracy}")


案例二:泰坦尼克号生存预测

本案例演示如何不使用Pipeline,而是手动进行数据预处理,并训练一个随机森林模型。
核心步骤:
- 数据清洗:处理缺失值,进行类型转换。
- 特征编码:对分类变量(如“性别”)进行标签编码或独热编码。
- 特征组装:将多个特征列组合成一个特征向量。
- 模型训练与预测:使用随机森林分类器。



from pyspark.ml.feature import VectorAssembler, StringIndexer
from pyspark.ml.classification import RandomForestClassifier





# 1. 读取并选择特征列
df = spark.read.csv("titanic.csv", header=True, inferSchema=True)
df = df.select("Survived", "Pclass", "Sex", "Age", "Fare")



# 2. 处理缺失值并转换类型
df = df.dropna()
df = df.withColumn("Age", df["Age"].cast("float"))



# 3. 对“Sex”列进行标签编码
indexer = StringIndexer(inputCol="Sex", outputCol="SexIndex")
df_indexed = indexer.fit(df).transform(df)
# 4. 组装特征向量
assembler = VectorAssembler(inputCols=["Pclass", "SexIndex", "Age", "Fare"], outputCol="features")
df_assembled = assembler.transform(df_indexed)
# 5. 划分数据集,训练随机森林模型
train_df, test_df = df_assembled.randomSplit([0.7, 0.3])
rf = RandomForestClassifier(labelCol="Survived", featuresCol="features")
model = rf.fit(train_df)
predictions = model.transform(test_df)
predictions.select("Survived", "prediction", "probability").show(10)
📝 总结


本节课我们一起学习了基于SQL和Spark的机器学习全流程。
- SQL是基石:作为数据查询和管理的标准语言,是从事数据相关工作的基础技能。
- Spark是强大工具:作为成熟的大数据处理和机器学习框架,Spark(特别是其PySpark API)能够高效处理海量数据,并内置了丰富的机器学习算法。
- 核心概念:我们理解了RDD(弹性分布式数据集)和DataFrame这两种核心数据抽象,以及如何通过Spark SQL用熟悉的语法操作数据。
- 完整流程:通过垃圾邮件分类和泰坦尼克号预测两个案例,我们实践了从数据读取、清洗、特征工程到模型训练、评估的完整机器学习流程。


对于希望在大数据平台下进行算法开发的工程师来说,掌握SQL和Spark是至关重要的。建议大家在课后积极动手实践,巩固本节课的知识点。


📚 Lookalike相似人群拓展项目实战 - P1
在本节课中,我们将学习推荐广告领域中的一个经典业务——Lookalike相似人群拓展。我们将从赛题背景出发,逐步了解数据、构建特征、选择模型,并最终完成模型融合。本教程旨在让初学者能够清晰理解整个流程。

🎯 赛题背景与业务理解

Lookalike相似人群拓展业务源于2018年的腾讯广告算法大赛。该赛题非常贴合真实业务,是推荐广告系统中的经典问题。其核心目标是:基于客户提供的一小部分高质量“种子人群”,通过建模方式,在全量用户中寻找与种子人群高度相似的“扩展人群”。
上一节我们介绍了课程概述,本节中我们来看看具体的业务场景。
业务场景流程如下:
- 种子人群:客户上传指定高质量人群包。
- 特征提取:从种子人群中挖掘显著的画像特征(如兴趣、行为)。
- 人群匹配:基于提取的特征,从全量活跃用户中寻找相似人群。
- 广告投放:向扩展人群展示相关广告,观察其点击或转化行为。
业务价值体现在:
- 更好触达意向用户。
- 获得更高的互动转化可能性。
- 找到潜在目标人群,助力用户增长(拉新)。
本次赛题对真实业务进行了简化。我们无需从全量库中召回用户,数据已直接提供了用户(UID)和广告(AID)的配对关系。我们的任务是预测给定的“用户-广告”对是否会发生点击(或转化),这本质上可以转化为一个点击率(CTR)预估问题。
📊 探索性数据分析

在开始建模前,我们必须先理解数据。探索性数据分析帮助我们了解数据的结构、规模和分布。


本次竞赛数据均为脱敏处理,时间范围为30天(但不包含具体时间戳)。数据分为四个部分:
- 训练集:包含
UID(用户ID)、AID(广告ID)和label(是否点击/转化)。 - 测试集:包含
UID和AID,需要预测其label的概率。 - 用户特征:包含用户的基础属性(如年龄、性别)以及多值兴趣特征(如
interest1, interest2)。 - 广告特征:包含广告的相关属性(如广告主ID、素材ID、广告类别等)。


关键发现:
- 训练集中用户ID(UID)的重复度很低,因此UID本身不能直接作为特征使用,否则会导致严重的过拟合和冷启动问题。但可以围绕UID构造统计特征。
- 测试集与训练集中的广告ID(AID)集合完全一致。
- 用户特征中的兴趣字段(如
interest1)是多值特征,即一个字段内包含多个用逗号分隔的ID,需要特殊处理。
🔧 特征工程
特征工程是提升模型性能的关键。我们将围绕 UID 和 AID 构造丰富的特征。


以下是构造特征的主要方法:
1. 多值特征处理
对于 interest1 这类多值特征,常用处理方法有:
- 展开为稀疏向量:使用
CountVectorizer或TfidfVectorizer。例如,interest1中的不同ID可以视为“词”,整个字段视为“文档”,进行词频或TF-IDF统计。# 示例:使用 CountVectorizer from sklearn.feature_extraction.text import CountVectorizer vectorizer = CountVectorizer(tokenizer=lambda x: x.split(',')) interest_features = vectorizer.fit_transform(user_df['interest1']) - 降维:由于展开后维度可能极高,可以使用
PCA、NMF或SVD进行降维。
2. 基础特征
直接使用原始特征,如用户的年龄、性别,广告的素材大小、类别ID等,作为类别型或数值型特征。
3. 统计特征
- 计数特征:例如,统计每个
AID被点击的总次数 (count)。 - 唯一值计数:例如,统计每个用户
UID有多少个不同的兴趣ID (nunique),反映兴趣广度。 - 点击统计:例如,统计每个用户的历史总点击次数,反映活跃度。
4. 比例特征
- 目标编码:计算某个类别(如
AID)下label的平均值(转化率)。这是强特征,但需注意数据泄露问题。- 解决方法:使用五折交叉验证的方式在训练集上构造,即用其他四折的数据统计当前折的转化率。对于测试集,则用全部训练数据统计。
- 平滑处理:对于出现次数少的类别,其统计值不可靠,需引入平滑(如贝叶斯平滑),公式可参考:
其中 α 和 β 为超参数。平滑后的转化率 = (点击次数 + α) / (展示次数 + α + β)
5. 交叉组合特征
- 类别特征交叉:将两个或多个类别特征组合,形成更细粒度的特征,如“性别_年龄段”。
- 转化率交叉特征:将不同维度统计的转化率进行组合或比较。
🤖 CTR模型建模
我们将问题定义为CTR预估,因此可以使用经典的CTR模型。
1. 树模型
- LightGBM / XGBoost:处理结构化数据的利器,性能稳定且高效。
- LightGBM优势:采用直方图算法和叶子节点分裂策略,训练速度更快,内存消耗更低。
- XGBoost优势:采用预排序算法,精度可能略高,但效率较低。
2. 因子分解机及其变种
- FM:通过隐向量内积建模二阶特征交叉,公式为:
其中ŷ = w0 + Σ wi*xi + Σ Σ <vi, vj> * xi * xj<vi, vj>是特征i和j的隐向量内积。 - FFM:在FM基础上引入“域”的概念,同一特征在与不同域的特征交叉时使用不同的隐向量,能更好地建模特征交叉,但参数更多。
- DeepFM / NFM 等:将FM与深度神经网络结合,能够以端到端的方式同时学习低阶和高阶特征交叉。
⚙️ 训练与验证策略
为了保证线上线下的一致性,验证策略至关重要。
1. 按AID划分验证集
由于测试集中每个AID是独立的,最合理的验证方式是按照AID进行分层划分。例如,随机抽取20%的AID对应的所有样本作为验证集,剩余80%作为训练集。这能最大程度模拟测试集的数据分布。
2. 五折交叉验证
在训练阶段,对80%的训练数据再进行五折交叉验证,用于调参和获得稳定的模型输出。五折产生的五个模型可以用于后续的集成。
🧩 模型融合
模型融合是提升最终成绩的重要手段,其核心思想是集成多个差异化的模型,以降低方差,提高泛化能力。
差异来源主要包括:
- 模型差异:使用不同原理的模型,如 LightGBM, FFM, DeepFM。
- 特征差异:使用不同的特征子集或特征构造方法。
- 样本差异:通过Bagging或交叉验证产生不同的训练子集。
常用融合方法:
- 加权平均:对多个模型的预测概率进行加权平均。
- Stacking:
- 将第一层多个模型的预测输出(在验证集和测试集上)作为新的特征。
- 将这些新特征与原始特征拼接,训练第二层模型(通常为简单的线性模型或树模型)。
- 这是一个强大的集成方法,但需小心过拟合。
实战技巧:在比赛中,优胜方案通常构建了复杂的多层级联模型。例如,将LightGBM、FFM等模型的预测结果作为特征,输入到下一阶段的XGBoost或神经网络中,并进行多次堆叠。
📝 总结与代码建议


本节课我们一起学习了Lookalike相似人群拓展项目的完整实战流程:
- 理解业务:将人群扩展问题转化为CTR预估问题。
- 分析数据:通过EDA了解数据结构和关键点(如多值特征、UID不可用)。
- 构造特征:重点处理多值特征、构建统计特征、比例特征(目标编码)和交叉特征。
- 选择模型:应用树模型和深度CTR模型进行建模。
- 设计验证:按AID划分验证集,保证线上线下一致性。
- 模型融合:通过加权平均、Stacking等方法集成多个模型,提升最终效果。
建议:
- 对于此类经典赛题,强烈建议复现top选手的开源代码,学习他们的特征工程和模型集成技巧。
- 在实践中,特征的有效性需要通过模型线下验证来最终判断,理论上有解释性的特征不一定带来效果提升。
- 本赛题简化了真实业务(如一对多关系、召回阶段),真实场景会更复杂。


通过本课程的学习,你应该对如何解决一个Lookalike或CTR预估问题有了系统的认识。接下来,可以尝试在具体数据集上实践这些步骤,并深入阅读FM、FFM等模型的原理细节。








🧠 深度学习集训营 P1:深入理解CNN、RNN和LSTM








在本节课中,我们将学习如何从数学原理出发,深入理解全连接神经网络(FNN)、循环神经网络(RNN)和长短期记忆网络(LSTM)的核心机制。我们将通过将复杂的数学公式转化为直观的图示,并最终抽象为向量化表示,来掌握这些模型的本质。课程的重点在于掌握一套分析模型的方法论,而不仅仅是记住公式。







📘 概述:从公式到图,再到向量化




理解深度学习模型的关键在于建立不同抽象层次之间的联系。我们将遵循一个清晰的路径:首先,从最底层的标量数学公式开始,理解每一个计算细节;然后,将这些公式映射为结构化的图示,使计算流程可视化;最后,将图示抽象为更高层次的向量/矩阵表示,这是现代深度学习框架实现的基础。掌握这套方法,你将能剖析任何复杂的神经网络结构。
第一部分:全连接神经网络(FNN)的深入剖析
上一节概述了我们分析模型的整体思路,本节中我们来看看如何将这套方法应用于最基础的全连接神经网络。
1.1 模型输入与数据表示
首先,我们需要明确模型的输入。假设我们有一个分类任务的数据集。
以下是数据集中一个样本的数学表示:
- 特征向量
x: 这是一个n维的列向量,属于实数空间R^n。x = [x1, x2, ..., xn]^T。在图中,它对应输入层的n个节点。 - 标签
y: 这是一个标量,表示类别。在代码实现中,我们通常将其转换为m维的 one-hot 编码向量,其中m是类别总数。这样,输出层就有m个节点。
核心概念:输入 x 是向量,输出 y 在计算中也被处理为向量(one-hot 编码)。区分标量和向量是理解后续公式的关键。
1.2 前向传播:从标量公式到网络图示
前向传播定义了数据如何从输入层流经网络得到预测输出。我们以一个三层网络(输入、隐藏、输出)为例,将其拆解为线性部分和非线性部分。
第一层(输入层)

第一层很简单,只是接收输入数据。
公式表示:
a^(1) = x
这意味着将输入向量 x 的值赋给第一层的激活值 a^(1)。上标 (1) 表示第一层。
图示映射:
在图上,这对应于 n 个输入节点,每个节点的值就是 x 向量的一个分量 x_i。
上一节我们介绍了输入层的简单映射,本节中我们来看看包含计算的隐藏层。
第二层(隐藏层)


隐藏层开始进行真正的计算,包括线性加权求和与非线性激活。
线性部分 z^(2):
z_j^(2) = Σ_k ( w_(jk)^(2) * a_k^(1) ) + b_j^(2)
其中,j 是隐藏层神经元的索引(j=1 to p),k 是输入层神经元的索引(k=1 to n)。w_(jk)^(2) 是连接输入层第 k 个神经元和隐藏层第 j 个神经元的权重。注意下标顺序:w_(jk) 表示“目标层j ← 来源层k”。
非线性部分 a^(2):
a_j^(2) = σ(z_j^(2))
这里 σ 是激活函数,例如 Sigmoid 函数 σ(z) = 1 / (1 + e^(-z))。
图示映射:
- 在图上画出
p个代表z_j^(2)的节点。 - 对于每个
z_j^(2),画出从所有a_k^(1)指向它的连接线,线上标注权重w_(jk)^(2)。 - 为每个
z_j^(2)添加偏置b_j^(2)。 - 从每个
z_j^(2)画出一个箭头指向a_j^(2),表示应用激活函数σ。
通过这种方式,我们可以将第二层的两行数学公式完全转化为一张局部的网络图。


理解了隐藏层的图示化方法后,输出层的处理方式是完全类似的。
第三层(输出层)及预测输出

第三层(输出层)的计算逻辑与第二层相同,只是维度变化。

线性部分 z^(3):
z_j^(3) = Σ_k ( w_(jk)^(3) * a_k^(2) ) + b_j^(3)
j=1 to m(输出类别数),k=1 to p(隐藏层神经元数)。
非线性部分 a^(3):
a_j^(3) = σ(z_j^(3))
预测输出 ŷ:
ŷ_j = a_j^(3)
预测输出 ŷ 直接等于输出层的激活值 a^(3),它是一个 m 维向量,可以解释为属于各个类别的概率。
图示映射:
重复第二层的绘图过程,将层标改为 (3),并将 a_j^(3) 节点标记为 ŷ_j。
至此,我们完成了从输入 x 到预测输出 ŷ 的完整前向计算图示。图中的每一个符号都严格对应数学公式中的一个变量。
1.3 损失函数与训练目标
得到预测值后,我们需要衡量它与真实值的差距。



公式表示(以均方误差为例):
L(x) = 1/2 * Σ_j ( (ŷ_j - y_j)^2 )
其中 y_j 是真实标签的 one-hot 编码向量的第 j 个分量。

我们的训练目标就是通过调整网络中的所有权重 w 和偏置 b,最小化整个训练集上的总损失。
1.4 误差反向传播:链式法则的图示化应用
反向传播的目的是计算损失函数对每个参数(w, b)的梯度,以便用梯度下降法更新它们。其核心数学工具是链式法则。
关键中间量:误差 δ
为了计算方便,我们定义每一层(第 l 层)的误差 δ^(l):
δ^(l) = ∂L / ∂z^(l)
即损失对当前层线性部分 z 的偏导。它是一个非常有用的中间量。
为什么需要 δ? 它本身没有直接的物理意义,但作为计算参数梯度的“跳板”,可以简化公式推导和代码实现。
输出层误差 δ^(3) 的计算
以第三层(输出层)为例,计算 δ_j^(3) = ∂L / ∂z_j^(3)。
根据图示,L 到 z_j^(3) 不是直接相连,中间经过了 a_j^(3)(即 ŷ_j)。因此,应用链式法则:
δ_j^(3) = (∂L / ∂a_j^(3)) * (∂a_j^(3) / ∂z_j^(3))
- 第一项
∂L / ∂a_j^(3):将均方误差公式和ŷ_j = a_j^(3)代入,可得(a_j^(3) - y_j)。 - 第二项
∂a_j^(3) / ∂z_j^(3):这就是激活函数σ在z_j^(3)处的导数,记作σ‘(z_j^(3))。

所以,δ_j^(3) = (a_j^(3) - y_j) * σ‘(z_j^(3))。
图示帮助:在图上,这清晰地对应着从损失 L 反向经过 ŷ_j 节点,再到达 z_j^(3) 节点的路径。

隐藏层误差 δ^(2) 的计算
计算第二层的误差 δ_j^(2) = ∂L / ∂z_j^(2) 更复杂一些,因为 L 到 z_j^(2) 的路径更长。
观察图示,从 z_j^(2) 到 L 的路径有两条主要分支:它先影响本层的 a_j^(2),而 a_j^(2) 会影响下一层所有的 z_k^(3)(k=1 to m)。因此,需要用到全导数公式:当一个变量影响多个后续变量时,对其求导需将所有路径的导数相加。

应用链式法则和全导数公式:
δ_j^(2) = Σ_k [ (∂L / ∂z_k^(3)) * (∂z_k^(3) / ∂a_j^(2)) ] * (∂a_j^(2) / ∂z_j^(2))
- 第一部分
∂L / ∂z_k^(3):这正是我们刚算出的δ_k^(3)。 - 第二部分
∂z_k^(3) / ∂a_j^(2):根据z_k^(3)的公式,这项等于权重w_(kj)^(3)。注意下标:这里变成了w_(kj),因为反向传播时,来源层(2)和目标层(3)的下标顺序与正向传播相反。 - 第三部分
∂a_j^(2) / ∂z_j^(2):同样是激活函数的导数σ‘(z_j^(2))。
所以,δ_j^(2) = σ‘(z_j^(2)) * Σ_k ( δ_k^(3) * w_(kj)^(3) )。
图示帮助:在图上,这对应从 z_j^(2) 出发,经过 a_j^(2),然后分散到所有 z_k^(3),最后汇聚到损失 L 的所有可能路径的导数之和。
计算参数梯度
有了各层的误差 δ,计算权重和偏置的梯度就非常简单了。
对于偏置 b:
∂L / ∂b_j^(l) = δ_j^(l)
因为根据 z 的公式,∂z_j^(l) / ∂b_j^(l) = 1。

对于权重 w:
∂L / ∂w_(jk)^(l) = a_k^(l-1) * δ_j^(l)
因为 ∂z_j^(l) / ∂w_(jk)^(l) = a_k^(l-1)。
算法总结:
- 前向传播,计算所有
a和z。 - 计算输出层误差
δ^(L)。 - 反向逐层计算
δ^(l)。 - 利用
δ^(l)计算各层参数的梯度∂L/∂w和∂L/∂b。 - 使用梯度下降更新参数:
w = w - η * ∂L/∂w,b = b - η * ∂L/∂b。
1.5 向量化表示:从繁琐到简洁
上述标量形式虽然细致,但过于繁琐。在实际的算法描述和代码实现中,我们使用向量和矩阵进行表示,极大提升了简洁性和计算效率。
- 激活值:
a^(l)表示第l层所有神经元的激活值向量。 - 权重:
W^(l)表示连接第l-1层到第l层的权重矩阵。W^(l)的第j行第k列元素就是w_(jk)^(l)。 - 线性计算:
z^(l) = W^(l) * a^(l-1) + b^(l)。这里*表示矩阵乘法。 - 激活函数:
a^(l) = σ(z^(l)),这里的σ是逐元素(element-wise)运算。 - 误差:
δ^(l) = ( (W^(l+1))^T * δ^(l+1) ) ⊙ σ‘(z^(l))。其中⊙表示逐元素相乘,^T表示矩阵转置。 - 梯度:
∇_(b^(l)) L = δ^(l)∇_(W^(l)) L = δ^(l) * (a^(l-1))^T
抽象图示:在向量化视角下,一个复杂的全连接网络可以简化为:
x (输入向量) → a (隐藏向量) → ŷ (输出向量)
其中每个箭头都包含了矩阵乘法、向量加法和非线性激活这一整套计算。这种高度抽象的视图是理解更复杂模型(如RNN)的基础。
第二部分:从FNN到RNN与LSTM
上一节我们详细拆解了全连接神经网络,并学会了用向量化视角来抽象它。本节中我们来看看如何利用这种抽象,自然地过渡到循环神经网络。
2.1 循环神经网络(RNN)的直觉
观察FNN的向量化抽象图:x -> a -> ŷ。如果我们让隐藏状态 a 不仅依赖于当前输入 x,还依赖于上一时刻的隐藏状态 a_(t-1),就引入了“记忆”能力,形成了循环。
核心思想:在RNN中,网络在处理序列的每个时间步 t 时,都会接收当前输入 x_t 和上一时刻的隐藏状态 h_(t-1)(通常用 h 代替 a 表示RNN的隐藏状态),并产生当前输出 y_t 和传递给下一时刻的隐藏状态 h_t。
2.2 RNN的数学与向量化表示
公式表示:
- 隐藏状态更新:
h_t = σ_h ( W_h * x_t + U_h * h_(t-1) + b_h )W_h: 输入x_t的权重矩阵。U_h: 上一时刻隐藏状态h_(t-1)的权重矩阵。这就是循环连接的核心。σ_h: 隐藏层的激活函数(如tanh)。
- 输出计算:
y_t = σ_y ( V * h_t + c )V: 隐藏状态到输出的权重矩阵。σ_y: 输出层的激活函数(如Softmax用于分类)。
图示展开:将循环网络按时间步展开,就得到一个由多个共享参数的FNN模块串联而成的深层网络。这清晰地揭示了RNN如何传递历史信息。
2.3 RNN的挑战:梯度消失与爆炸
将RNN沿时间展开后,在反向传播时,梯度需要沿着时间步反向流动。计算损失 L 对早期时间步参数(如 U_h)的梯度时,会涉及多次矩阵 U_h 的连乘。
公式示意:∂h_t / ∂h_k ≈ Π_(i=k+1)^t ( ∂h_i / ∂h_(i-1) ) ≈ Π_(i=k+1)^t ( U_h^T * diag(σ_h‘(z_i)) )
- 如果
U_h的谱范数(主要特征值)大于1,连乘会导致梯度呈指数级增长,即梯度爆炸,模型无法稳定训练。 - 如果小于1,连乘会导致梯度呈指数级衰减到0,即梯度消失,模型无法学习到长距离的依赖关系。
2.4 长短期记忆网络(LSTM)
为了解决RNN的长期依赖问题,LSTM引入了“门控机制”和“细胞状态” C_t。
核心概念:
- 细胞状态
C_t:一条贯穿时间的“高速公路”,用于保存长期记忆。它通过线性操作(主要是逐元素乘加)传递,减轻了梯度消失。 - 门控:三个门(遗忘门
f_t、输入门i_t、输出门o_t)控制信息的流动。每个门都是一个由Sigmoid激活的神经网络层,输出0到1之间的值,表示“允许通过的比例”。
LSTM单元的计算步骤(向量化形式):
- 遗忘门:决定从细胞状态中丢弃什么信息。
f_t = σ(W_f * [h_(t-1), x_t] + b_f) - 输入门:决定哪些新信息存入细胞状态。
i_t = σ(W_i * [h_(t-1), x_t] + b_i)Ĉ_t = tanh(W_C * [h_(t-1), x_t] + b_C)(候选值)
- 更新细胞状态:
C_t = f_t ⊙ C_(t-1) + i_t ⊙ Ĉ_t。这是LSTM的核心,结合了遗忘和新增。 - 输出门:基于更新后的细胞状态,决定输出什么。
o_t = σ(W_o * [h_(t-1), x_t] + b_o)h_t = o_t ⊙ tanh(C_t)
通过这种设计,LSTM能够有选择地保留长期信息,并有效地在长序列中传播梯度,从而显著提升了处理长期依赖的能力。
🎯 总结
本节课中我们一起学习了深度学习中分析模型的系统性方法:


- 标量化理解:从最底层的数学公式出发,明确每一个变量(标量/向量/矩阵)的含义。
- 图示化映射:将公式转化为网络结构图,利用图示直观理解前向计算和反向传播中数据的流动与链式法则的应用。
- 向量化抽象:将繁琐的标量计算提升为简洁的向量/矩阵运算,这是理解现代深度学习模型和框架实现的关键。
- 模型演进:应用这套方法,我们看到了如何从全连接神经网络(FNN)自然地抽象出其核心结构,并通过添加循环连接得到循环神经网络(RN

🧠 七月在线-深度学习集训营 第三期[2022] - P2:神经网络压缩技术:工业界业务上线部署的大杀器!

在本节课中,我们将要学习神经网络压缩技术。这是工业界将模型部署到线上、移动端等资源受限环境时的关键技术。我们将从两个主要方向展开:一是设计轻量化的网络结构(如MobileNet),二是对已有大模型进行压缩(如剪枝和知识蒸馏)。

🎯 课程概述与动机
上一节我们介绍了卷积神经网络的基础结构。本节中,我们来看看为什么以及如何对神经网络进行压缩。


在计算机视觉领域,像VGGNet、ResNet这样的大模型参数量巨大(可达数百兆)。在GPU上训练尚可,但在线上部署(尤其是移动端或大规模CPU集群部署)时,会面临计算速度慢、内存占用高、功耗大等问题。
通常有两种解决方案:
- 从头开始设计一个更小、更高效的网络结构。
- 将一个训练好的大模型进行压缩,得到一个小模型。

本节课将围绕这两个主题展开。


📱 轻量化网络结构:MobileNet
在实际生产环境中,尤其是在移动设备上进行图像分类、分割、检测等任务时,轻量化网络结构被大量使用。近年来最具代表性的就是MobileNet系列。
选择MobileNet讲解,不仅因为其经典且被广泛应用,更因为其核心设计思想具有代表性,理解了它就能轻松理解其他轻量级网络。

回顾标准卷积操作

在具体介绍MobileNet之前,我们先回顾一下昨天学习的标准卷积操作。

对于一个输入为3通道的图片,如果我们使用2个3x3的卷积核(filter),那么每个卷积核实际上是一个3x3x3的三维张量。卷积操作是每个三维卷积核在输入的三维张量上滑动,进行点乘求和,最终每个卷积核输出一个二维的特征图。因此,输出通道数等于卷积核的个数。

代码描述一个标准卷积层:
# 假设输入尺寸为 [batch, height, width, in_channels]
# 使用2个3x3的卷积核,输出通道数为2
standard_conv = tf.keras.layers.Conv2D(filters=2, kernel_size=(3, 3), padding='same')

🔍 深度可分离卷积 (Depthwise Separable Convolution)

MobileNet等轻量网络的核心是一种称为深度可分离卷积的操作,它将标准卷积分解为两个步骤:深度卷积 和 逐点卷积。

1. 深度卷积 (Depthwise Convolution)

深度卷积的核心思想是:每个输入通道单独使用一个二维卷积核进行卷积。
- 输入有M个通道,就准备M个二维卷积核(例如3x3)。
- 第m个卷积核只与输入的第m个通道进行二维卷积,生成一个二维特征图。
- 将所有M个通道卷积得到的结果在通道维度上拼接起来,就得到了深度卷积的输出。输出通道数等于输入通道数。

这个过程没有进行通道间的信息融合。


2. 逐点卷积 (Pointwise Convolution)


逐点卷积用于弥补深度卷积的不足,其本质就是 1x1卷积。
- 它使用1x1大小的卷积核,但其“深度”等于深度卷积输出的通道数(即M)。
- 1x1卷积的作用是跨通道地融合特征,并可以自由地改变输出通道数(例如变为N)。

将深度卷积和逐点卷积串联起来,就构成了深度可分离卷积,它能以更少的参数和计算量近似标准卷积的效果。



公式对比计算量:
假设输入特征图尺寸为 (D_F \times D_F),输入通道数为 (M),输出通道数为 (N),卷积核尺寸为 (D_K \times D_K)。
- 标准卷积计算量: (D_K \cdot D_K \cdot M \cdot N \cdot D_F \cdot D_F)
- 深度可分离卷积计算量: (D_K \cdot D_K \cdot M \cdot D_F \cdot D_F + M \cdot N \cdot D_F \cdot D_F)

计算量降低比例约为: (\frac{1} + \frac{1}{D_K^2})。当使用3x3卷积且N较大时,计算量可降至原来的1/9左右。


📊 MobileNet V1 结构

MobileNet V1 的基本结构就是将传统卷积网络中的标准卷积层,几乎全部替换为深度可分离卷积层。每个深度可分离卷积模块通常包含:深度卷积 → Batch Normalization → ReLU6 → 逐点卷积 → Batch Normalization → ReLU6。


此外,MobileNet V1 引入了两个超参数来灵活控制模型的“瘦身”程度:
- 宽度乘数 α: 用于等比例减少每一层的通道数,让网络更“瘦”。
- 分辨率乘数 ρ: 用于降低输入图像的分辨率。

通过调整这两个参数,可以在准确率和模型大小/速度之间进行权衡。




🚀 MobileNet V2 的改进


MobileNet V2 在 V1 的基础上做了两大改进,进一步提升了性能:





- 引入残差连接: 借鉴ResNet的思想,加入了跳跃连接,有助于缓解深层网络的梯度消失问题,让训练更稳定。
- 倒残差结构: 与V1的“深度卷积->逐点卷积”不同,V2的基本模块变为:
- 扩展层: 一个1x1卷积,先提升通道维度(例如扩展6倍),在高维空间进行特征变换。
- 深度卷积: 在扩展后的高维特征上进行深度卷积。
- 投影层: 再用一个1x1卷积降低通道维度,同时与输入通过残差连接相加。



这种“先升维后降维”的结构,配合残差连接,使得信息流动更高效,在保持轻量化的同时获得了更强的表达能力。





✂️ 神经网络压缩技术



上一节我们介绍了如何设计轻量网络。本节中,我们来看看如何对已经训练好的大模型进行“减肥”压缩。



1. 网络剪枝

网络剪枝的核心思想是:移除网络中不重要的参数或结构。
- 权重剪枝: 将权重矩阵中绝对值小于某个阈值的权重置零,形成稀疏矩阵。
- 通道/滤波器剪枝: 直接移除整个输出通道值很小的滤波器,并同步移除下一层中对应的输入通道。



剪枝的一般流程是迭代进行的:
- 训练一个大型模型。
- 根据某种准则(如权重绝对值大小)剪枝。
- 对剪枝后的模型进行微调,恢复性能。
- 评估性能,若可接受则重复步骤2-3。



2. 知识蒸馏

知识蒸馏是一种“模型压缩”的思想,其核心是:让一个小的学生模型去学习一个大的教师模型的“知识”。


这里的“知识”主要指教师模型输出的概率分布(软标签),而不仅仅是真实的硬标签(one-hot向量)。教师模型输出的概率分布包含了丰富的类别间相似性信息(例如,“猫”和“狗”的概率可能都高于“汽车”),这些信息有助于学生模型更好地学习。



实现步骤:
- 训练一个或多个强大的教师模型。
- 固定教师模型,用小模型作为学生模型。
- 设计损失函数:学生模型的输出既要拟合教师模型的软标签(使用带温度参数T的Softmax产生),也要尽可能拟合真实的硬标签。
- 训练学生模型。
关键点: 学生模型拟合教师软标签时,使用的通常是均方误差等回归损失,而不是交叉熵分类损失。


知识蒸馏的一个巧妙应用是:当缺乏标注数据时,可以利用业界公开的、性能优秀的API输出作为教师模型的软标签,来训练自己的小模型,从而快速构建业务能力。
📝 课程总结



本节课我们一起学习了神经网络压缩这一工业界部署的关键技术。




我们首先探讨了其动机:大模型在部署时面临速度、存储和功耗的挑战。接着,我们从两个路径学习了解决方案:
设计轻量网络: 重点讲解了MobileNet系列。其核心是深度可分离卷积,它将标准卷积分解为深度卷积和逐点卷积,大幅减少了计算量和参数量。我们分析了MobileNet V1的基本结构和超参数,以及V2引入的倒残差结构和残差连接带来的改进。
压缩已有模型: 介绍了两种主流技术。
- 网络剪枝: 通过移除不重要的权重或滤波器来稀疏化模型。
- 知识蒸馏: 训练一个学生模型来模仿教师模型的行为和输出分布,从而将大模型的知识“蒸馏”到小模型中。

掌握这些技术,能够帮助我们在实际项目中,根据资源约束和性能要求,灵活地选择或打造合适的模型,使其成功部署上线。
七月在线-深度学习集训营 第三期[2022] - P3:深度学习在物体检测中的应用(下)🚀
概述
在本节课中,我们将要学习物体检测领域中的高级网络结构与特征提取方法。我们将重点探讨如何解决检测算法在不同大小物体上表现不一致的问题,并介绍无需预设锚框(Anchor-Free)的检测新范式。此外,我们还将分享几个能显著提升检测性能的实用训练技巧。
第一部分:特征金字塔网络(FPN)与三叉戟网络(TridentNet)
上一节我们介绍了单阶段(One-Stage)检测算法,如YOLO和SSD。本节中我们来看看如何通过改进特征提取结构,来更好地处理多尺度物体检测问题。
1.1 多尺度检测的挑战与早期方案
在物体检测中,一个核心挑战是让同一套算法能有效检测不同大小的物体。卷积神经网络(CNN)的下采样特性导致深层特征图感受野大,适合检测大物体,但会丢失小物体的细节信息。
早期解决多尺度问题的思路主要有两种:
- 图像金字塔:将输入图像缩放成多个尺寸,分别送入网络检测。这种方法计算和内存开销大,不切实际。
- 单一特征图预测:如Faster R-CNN,只在网络最后一层特征图上进行预测。这种方式受限于单一尺度的感受野,难以兼顾所有大小的物体。
1.2 特征金字塔网络(FPN)的核心思想
FPN提出了一种高效构建特征金字塔的方法,它包含两条路径:
- 自底向上路径(Bottom-up):即常规CNN的前向传播过程,随着网络加深,特征图分辨率降低,语义信息增强。
- 自顶向下路径(Top-down):对深层、低分辨率的特征图进行上采样,使其与浅层、高分辨率的特征图尺寸对齐。
- 横向连接(Lateral Connection):将上采样后的深层特征与对应的浅层特征(通常经过1x1卷积降维后)进行逐元素相加,融合高层的语义信息和低层的细节信息。融合后的特征再经过一个3x3卷积来消除上一步融合可能带来的混叠效应。
公式/代码描述:
对于第 l 层(如 l=4),其融合过程可表示为:
P_l = Conv3x3( Conv1x1(C_l) + Upsample(P_{l+1}) )
其中 C_l 是自底向上路径第 l 层的特征,P_{l+1} 是上一层(更深层)融合后的特征。
1.3 FPN与检测框架的融合
FPN本身是一种特征提取器,可以无缝集成到如Faster R-CNN等两阶段检测器中。在RPN(区域提议网络)阶段,FPN在不同层级的特征图(P2, P3, P4, P5...)上分别定义不同尺度的锚框(Anchor),让不同大小的物体在最适合的层级被提议出来。
一个有趣的现象是,集成了FPN的Faster R-CNN推理速度有时反而比原始版本更快。这主要有两个原因:
- 更轻量的RPN头:FPN的RPN设计通常更简洁。
- 非极大值抑制(NMS)的并行化:由于不同层级的提议框(Proposal)尺寸差异大,重叠可能性小,因此可以对不同层级的提议框并行进行NMS,大大降低了计算复杂度(从O(N²)降至约O((N/k)² * k),其中k为金字塔层数)。
1.4 三叉戟网络(TridentNet)
TridentNet是2019年提出的一个更简洁优雅的多尺度检测网络。其灵感来源于一个实验观察:在特征图上使用不同膨胀率(Dilation Rate)的空洞卷积进行预测时,膨胀率越大(感受野越大),对大物体的检测效果越好;膨胀率越小,对小物体的检测效果越好。
基于此,TridentNet设计了并行的三个分支,每个分支使用相同的网络权重,但具有不同的空洞卷积膨胀率(例如1,2,3),分别专注于检测小、中、大物体。最后将三个分支的预测结果合并。
核心优势:结构统一、设计直观,并且在保持高性能的同时,可以通过只使用中间分支(对应中等大小物体)来平衡精度与速度,适用于实际业务部署。
第二部分:Anchor-Free检测方法(CSP与CenterNet)
上一节我们介绍了通过改进特征金字塔来处理多尺度问题。本节中我们来看看一种更根本的思路:抛弃预设锚框(Anchor),直接预测目标的关键点。
2.1 Anchor-Based方法的局限
以锚框为基础的检测器(如Faster R-CNN, SSD, YOLOv3)需要精心设计锚框的尺寸、长宽比和数量。这引入了大量超参数,调优复杂,且锚框与真实物体的匹配策略(如IoU阈值)对性能影响敏感。
2.2 CSP:行人检测中的Anchor-Free方案
CSP(Center and Scale Prediction)专为行人检测设计,思路极其直接:
- 中心点预测:网络直接预测行人边界框的中心点位置。这被建模为一个热图(Heatmap),真实中心点位置为峰值。
- 尺度预测:在中心点对应的位置上,网络预测行人的高度(或高度和宽度)。由于行人宽高比相对稳定,仅预测高度通常已足够。
训练挑战与解决方案:
- 正负样本极端不平衡:一张图中只有少数几个正样本(中心点),其余全是负样本。直接训练网络难以收敛。
- 解决方案:
- 高斯掩码(Gaussian Mask):不以非0即1的硬标签作为监督,而是在真实中心点周围生成一个2D高斯分布作为软标签。这样,靠近中心点的位置也有较高的监督信号,缓解了正负样本的尖锐矛盾。
- Focal Loss:进一步使用Focal Loss来降低大量简单负样本在训练中的权重,使网络更专注于学习困难样本。
损失函数(示意):
中心点预测的损失借鉴了Focal Loss的形式:
L_k = -1/N * Σ[ (1 - Y_hat)^α * (Y)^β * log(Y_hat) ]
其中 Y 是高斯软标签,Y_hat 是预测值,α 和 β 是超参数,用于调节难易样本和正负样本的权重。
2.3 CenterNet:通用的Anchor-Free检测器
CenterNet将CSP的思想推广到通用物体检测(如COCO数据集)。其核心与CSP一致:将物体检测转化为对中心点、尺寸(宽高)和偏移量的回归问题。它同样使用高斯掩码和修改版的Focal Loss进行训练。


优势:
- 结构简单:无需非极大值抑制(NMS)后处理(因为每个物体理论上只由一个中心点预测),Pipeline更简洁。
- 易于扩展:同样的框架可以轻松扩展到人体姿态估计、3D检测等任务。


第三部分:物体检测实用训练技巧 🛠️
掌握了核心网络结构后,本节我们来看看几个能显著提升模型性能的训练技巧。这些技巧简单有效,在多个检测框架上都能带来稳定提升。
以下是三个关键技巧:
MixUp 数据增强
- 原理:将两张训练图像以一定比例混合,同时其标签也以相同比例混合。例如:
新图像 = λ * 图像A + (1-λ) * 图像B,新标签 = λ * 标签A + (1-λ) * 标签B。λ从Beta分布中采样。 - 作用:鼓励模型在训练数据之间进行线性行为,提高泛化能力和鲁棒性。在检测任务中,λ的分布通常与图像分类任务不同,需要调整。
- 原理:将两张训练图像以一定比例混合,同时其标签也以相同比例混合。例如:
标签平滑(Label Smoothing)
- 原理:修改分类任务中常用的one-hot硬标签。例如,对于正样本,将标签“1”改为“0.9”,并将剩余的“0.1”均匀分配给其他类别(或将背景类视为一个特殊类别)。
- 作用:防止模型对训练标签过于自信,减轻过拟合,尤其能缓解数据集标签可能存在错误(噪声)带来的影响。
- 代码描述:
# 原始one-hot标签: [0, 0, 1, 0] # 平滑后 (epsilon=0.1): # smoothed_label = (1 - epsilon) * one_hot + epsilon / K # 结果: [0.025, 0.025, 0.925, 0.025] (K=4)
余弦学习率衰减(Cosine Learning Rate Decay)
- 原理:学习率随训练周期(epoch)的变化遵循余弦函数曲线,从初始值缓慢下降到0。
- 作用:相比传统的阶梯式下降(Step Decay),余弦衰减过程更加平滑,让模型在训练末期也能进行更精细的权重更新,通常能带来更好的收敛效果和最终精度。
总结
本节课中我们一起学习了物体检测领域的进阶内容:
- 多尺度特征融合:我们深入探讨了FPN通过自顶向下和横向连接构建特征金字塔的机制,以及TridentNet利用不同膨胀率的并行分支来优雅解决多尺度检测问题的思路。
- Anchor-Free检测范式:我们分析了CSP和CenterNet如何摒弃复杂的锚框设计,直接预测目标的中心点和尺寸,并通过高斯掩码和Focal Loss解决了训练中的样本不平衡问题。
- 实用训练技巧:我们介绍了MixUp、标签平滑和余弦学习率衰减这三个简单却高效的技巧,它们能显著提升检测模型的最终性能。


理解这些现代检测网络的核心思想与技巧,将帮助你更好地应对实际项目中复杂的检测需求,并具备跟进最新研究进展的能力。


🧠 七月在线-深度学习集训营 第三期[2022] - P4:当下最好的语言模型BERT介绍
在本节课中,我们将要学习当下效果卓越的语言模型BERT。课程将从Attention机制开始,逐步深入到Transformer的核心结构,并最终详细解析BERT模型的原理、预训练任务以及如何在实际任务中进行微调。我们将确保内容简单直白,让初学者能够跟上。
🔍 第一部分:Attention机制回顾
上一节我们介绍了课程的整体安排,本节中我们来看看Attention机制。对于序列生成类任务,如机器翻译、文本摘要,通常采用Sequence-to-Sequence模型。
Sequence-to-Sequence模型包含编码器(Encoder)和解码器(Decoder)两部分。编码器将输入序列编码为一个上下文向量(Context),解码器则根据该向量生成输出序列。然而,该模型有一个致命缺点:当输入序列过长时,靠前的信息容易丢失。
为了解决长序列信息丢失的问题,Attention机制应运而生。Attention不是模型,而是一种机制。它模仿人类阅读时的行为:在回答问题时,只关注文章中相关的段落,而非通读全文。
在引入Attention的Sequence-to-Sequence模型中,编码器会输出所有时间步的隐藏状态,而不仅仅是最末的上下文向量。解码器的每个时间步会计算一个权重分布,对编码器的所有隐藏状态进行加权求和,从而动态地聚焦于输入序列的不同部分。
以下是Attention机制的核心思想,可以概括为一个加权求和的过程:
公式: Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V
其中,Q(Query)代表解码器的当前状态,K(Key)和V(Value)通常代表编码器的所有隐藏状态。通过计算Q和K的相似度得到权重,再对V进行加权求和。




🏗️ 第二部分:Transformer核心结构
理解了Attention机制后,本节我们来看看Transformer模型。Transformer完全基于Attention机制构建,是BERT模型的核心。


Transformer同样采用编码器-解码器架构。其编码器由N个相同的层堆叠而成,每层包含两个子层:
- 多头自注意力机制(Multi-Head Self-Attention)
- 前馈神经网络(Feed-Forward Network)


每个子层后都接有残差连接(Residual Connection)和层归一化(Layer Normalization),这有助于缓解梯度消失和过拟合。
解码器结构类似,但在其第一个多头注意力子层中加入了掩码(Mask),以防止在训练时“看到”未来的信息,确保自回归生成的正确性。


2.1 Scaled Dot-Product Attention
Transformer中使用的Attention变体是Scaled Dot-Product Attention。其计算公式如下:
公式: Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V
与简单的点积注意力(Dot-Product Attention)相比,它多了一个缩放因子 1 / sqrt(d_k)。d_k 是Key的维度。加入缩放因子是为了防止点积结果方差过大,导致经过Softmax函数后梯度太小,影响模型训练。
2.2 多头注意力机制(Multi-Head Attention)


单一的Attention机制可能只关注到一种模式的信息。为了让模型能够同时关注来自不同表示子空间的信息,Transformer引入了多头注意力机制。
其做法是:
- 将
Q,K,V通过不同的线性变换矩阵投影多次,得到多组Q,K,V。 - 对每一组分别进行Scaled Dot-Product Attention计算,得到多个输出。
- 将多个输出拼接起来,再通过一个线性变换层进行整合和降维。




代码示意:
# 伪代码示意多头注意力
def multi_head_attention(Q, K, V, num_heads):
# 1. 线性投影,分割成多头
Q_proj = split_heads(linear(Q), num_heads)
K_proj = split_heads(linear(K), num_heads)
V_proj = split_heads(linear(V), num_heads)
# 2. 对每个头计算注意力
head_outputs = []
for i in range(num_heads):
attn_output = scaled_dot_product_attention(Q_proj[i], K_proj[i], V_proj[i])
head_outputs.append(attn_output)
# 3. 合并多头输出并线性变换
concat_output = concatenate(head_outputs)
output = linear(concat_output)
return output
在Transformer编码器中,使用的是自注意力(Self-Attention),即Q, K, V均来自同一个输入序列,用于捕捉序列内部的依赖关系。
🤖 第三部分:BERT模型详解
掌握了Transformer的编码器后,本节我们来学习BERT模型。BERT的全称是Bidirectional Encoder Representations from Transformers,即双向Transformer编码器表示。
BERT的本质是一个多层Transformer编码器的堆叠。它有两种规格:
- BERT-base: 12层Transformer,隐藏层维度768,12个注意力头。
- BERT-large: 24层Transformer,隐藏层维度1024,16个注意力头。
BERT是一个预训练模型,其应用分为两个阶段:预训练(Pre-training) 和 微调(Fine-tuning)。

3.1 BERT的预训练任务


BERT通过两个无监督任务在海量文本上进行预训练,从而学习通用的语言表示。

任务一:掩码语言模型(Masked Language Model, MLM)
类似于完形填空。随机掩盖输入序列中15%的Token,然后让模型预测被掩盖的Token。为了防止[MASK]标记在微调阶段从未出现,BERT采用以下策略:
- 80%的概率替换为
[MASK] - 10%的概率替换为随机Token
- 10%的概率保持不变


任务二:下一句预测(Next Sentence Prediction, NSP)
判断句子B是否是句子A的下一句。训练数据中,50%的句子B是A的实际下一句(正例),50%是随机选取的(负例)。这个任务帮助模型理解句子间关系。
BERT的输入由三部分嵌入相加而成:
- Token Embedding: 词嵌入。
- Segment Embedding: 段落嵌入,用于区分两个句子(如句子A全为0,句子B全为1)。
- Position Embedding: 位置嵌入,为序列提供位置信息。
输入序列以[CLS]标记开头,句子间用[SEP]标记分隔。[CLS]标记的最终输出常被用作整个序列的聚合表示,用于分类任务。







🛠️ 第四部分:BERT实战——文本相似度微调
理论部分我们已经介绍了BERT的原理,本节中我们来看看如何在实际任务中微调BERT。我们将以“文本相似度判断”任务为例。


微调的本质是在预训练好的BERT模型基础上,针对特定任务和数据集,对模型参数进行小幅调整。




以下是微调BERT的主要步骤:


- 准备数据:准备训练集、验证集和测试集,格式通常为
(句子A,句子B,标签)。 - 定义数据处理器:需要继承
DataProcessor类,并实现四个方法:获取训练集、验证集、测试集和标签列表。核心是将数据转换为InputExample对象。 - 修改运行脚本:BERT官方提供了
run_classifier.py等脚本。我们需要在其中注册自定义的数据处理器,并通过命令行参数指定任务名称。 - 执行训练:通过命令行运行脚本,指定必要的参数,如预训练模型路径、数据目录、输出目录等。
关键代码片段示例(自定义DataProcessor):
class MyProcessor(DataProcessor):
def get_train_examples(self, data_dir):
# 读取你的训练数据文件,例如TSV格式
df = pd.read_csv(os.path.join(data_dir, "train.tsv"), sep='\t')
examples = []
for (i, row) in df.iterrows():
guid = f"train-{i}"
text_a = row['sentence1']
text_b = row['sentence2']
# 注意:标签需要转换为字符串类型
label = str(row['label'])
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
# 类似地实现 get_dev_examples, get_test_examples, get_labels 方法
在运行微调后,模型会在验证集上评估性能(如准确率),并将微调后的模型保存到指定目录,供后续预测使用。








🚀 第五部分:BERT的演进与总结



在BERT之后,研究者们提出了诸多改进模型,如:
- GPT系列:使用Transformer解码器,采用单向语言模型进行预训练。
- ERNIE(百度):在MLM任务中掩盖整个实体或短语,而非单个字词,以增强语义理解。
- XLNet:采用排列语言模型,克服了BERT中
[MASK]标记在预训练与微调时不一致的问题,并引入了Transformer-XL以处理更长序列。 - RoBERTa:主要改进了BERT的训练策略,如使用更大批次、更多数据、动态掩码等,去除了NSP任务。



尽管新模型层出不穷,但由于生态、中文支持及性价比等因素,BERT及其变体仍然是工业界最常用的预训练模型之一。





本节课总结





本节课我们一起学习了从Attention到Transformer,再到BERT的完整知识体系:
- Attention机制是一种加权求和机制,解决了长序列信息丢失问题。
- Transformer是基于Attention的编码器-解码器模型,其核心是多头自注意力机制。
- BERT是双向Transformer编码器的堆叠,通过MLM和NSP任务进行预训练,学习通用的语言表示。
- 微调(Fine-tuning) 是将预训练BERT模型适配到特定下游任务的关键步骤,我们以文本相似度任务为例进行了实战讲解。




理解Transformer的结构是掌握BERT的基础,而熟练进行微调则是将BERT应用于实际项目的关键。建议大家课后多复习,并动手完成微调实践。
🧠 七月在线-深度学习集训营 第三期[2022] - P5:生成式对抗网络(GAN)教程
在本节课中,我们将要学习生成式对抗网络(GAN)的核心原理、数学基础、训练挑战及其重要变体WGAN。课程内容将从基础概念入手,逐步深入到模型优化与实践技巧,旨在让初学者能够清晰理解GAN的工作机制。
📚 预备知识:KL散度与JS散度
在正式讲解生成式对抗网络之前,我们需要先理解两个衡量数据分布差异的重要概念:KL散度和JS散度。
KL散度(Kullback-Leibler Divergence)
KL散度用于衡量两个概率分布之间的差异。给定两个分布 ( P(x) ) 和 ( Q(x) ),KL散度的定义如下:
对于连续变量:
[
D_(P \parallel Q) = \int P(x) \log \frac{P(x)}{Q(x)} , dx
]
对于离散变量:
[
D_(P \parallel Q) = \sum_ P(x) \log \frac{P(x)}{Q(x)}
]
KL散度具有非对称性,即 ( D_(P \parallel Q) \neq D_(Q \parallel P) )。当两个分布完全相同时,KL散度达到最小值0。
JS散度(Jensen-Shannon Divergence)
JS散度建立在KL散度的基础之上,通过计算两个分布与其平均分布之间的KL散度的均值来定义,具有对称性。
[
D_(P \parallel Q) = \frac{1}{2} D_(P \parallel M) + \frac{1}{2} D_(Q \parallel M)
]
其中 ( M = \frac{P + Q}{2} )。
JS散度的值域在0到1之间,当两个分布完全相同时为0,完全不同时为1。
🤖 生成式对抗网络(GAN)核心框架
上一节我们介绍了衡量分布差异的工具,本节中我们来看看如何利用这种思想构建一个“对抗”的网络。
生成式对抗网络由两个核心组件构成:生成器(Generator) 和 判别器(Discriminator)。
- 判别器(D): 如同一个“验钞机”,其目标是准确区分输入数据是来自真实分布 ( P_(x) ) 的“真钞”,还是来自生成器分布的“假钞”。对于真实数据,它应输出高概率(接近1);对于生成数据,应输出低概率(接近0)。
- 生成器(G): 如同一个“造假者”,其目标是生成足以“以假乱真”的数据,骗过判别器。它接收一个随机噪声向量 ( z )(通常来自简单分布如均匀分布或高斯分布),并输出一个伪造的数据样本 ( G(z) )。
🎯 GAN的数学目标
GAN的训练过程是一个极小极大博弈(Minimax Game)。其目标函数 ( V(D, G) ) 定义如下:
[
\min_G \max_D V(D, G) = \mathbb{x \sim P(x)}[\log D(x)] + \mathbb_{z \sim P_z(z)}[\log (1 - D(G(z)))]
]
- 判别器(D)的目标(内层max): 最大化这个函数。即,最大化对真实数据的判别概率 ( \log D(x) ),同时最大化对生成数据的判别错误概率 ( \log (1 - D(G(z))) )(即认为它是假的)。
- 生成器(G)的目标(外层min): 最小化这个函数。即,希望生成的数据 ( G(z) ) 能被判别器误判为真,也就是最小化 ( \log (1 - D(G(z))) ),等价于最大化 ( D(G(z)) )。
🔍 最优判别器与纳什均衡
理论分析表明,对于固定的生成器 ( G \,最优的判别器 ( D^_G(x) ) 为:
[
D^G(x) = \frac{P(x)}{P_(x) + P_G(x)}
]
其中 ( P_G(x) ) 是生成器产生的数据分布。
当生成器完美地学习了真实数据分布,即 ( P_G = P_ ) 时,最优判别器对任何输入都只能给出 ( D^*_G(x) = \frac{1}{2} ),意味着它无法区分真假,达到了纳什均衡点。此时,生成器生成的样本与真实样本在分布上已无区别。
⚠️ GAN训练中的挑战
尽管GAN思想巧妙,但在实际训练中面临诸多挑战。
以下是GAN训练中常见的几个问题:
- 梯度消失(Gradient Vanishing): 当判别器过于强大时,它对生成样本的梯度会非常小,导致生成器无法获得有效的更新信号。
- 模式崩溃(Mode Collapse): 生成器倾向于生成多样性有限的、安全的样本,而无法覆盖真实数据分布的所有模式(例如,只生成某一种人脸,而不生成其他类型)。
- 评估困难: 原始的GAN损失函数无法直接反映生成样本的质量,通常需要人工观察生成结果来评估训练进度。
- 分布不重叠问题: 真实数据分布 ( P_ ) 和生成器分布 ( P_G ) 通常都是高维空间中的低维流形,它们很可能没有交集或交集测度为零。此时,JS散度会是一个常数(例如 (\log 2)),导致梯度为零,训练停滞。
🚀 WGAN:使用Wasserstein距离改进GAN
为了克服原始GAN的缺陷,Wasserstein GAN(WGAN)被提出。其核心思想是使用Wasserstein距离(又称推土机距离,EM距离) 替代JS散度来衡量分布差异。
🌊 Wasserstein距离
Wasserstein距离直观上理解为:将一个分布 ( P ) 的“土堆”搬动成另一个分布 ( Q ) 的形状所需的最小“工作量”。它即使在不重叠的分布之间也能提供平滑的梯度。
根据对偶性,Wasserstein距离可以转化为一个最大化问题:
[
W(P_, P_G) = \sup_{| f |L \leq 1} \left[ \mathbb{x \sim P_}[f(x)] - \mathbb_{x \sim P_G}[f(x)] \right]
]
其中,( f ) 是一个满足 1-Lipschitz连续性 的函数(即其梯度的绝对值几乎处处不超过1)。
🛠️ WGAN的实现改动
在WGAN中,我们用一组参数 ( w )(通过神经网络实现)来逼近函数 ( f ),并称之为“批评器(Critic)”。相比原始GAN的判别器(输出0/1概率),批评器输出一个实数分数。
以下是WGAN相对于原始GAN的主要改动:
- 移除判别器最后一层的Sigmoid激活函数,使批评器输出一个无界的分数。
- 不再使用基于对数(log)的损失函数,损失函数直接采用Wasserstein距离的对偶形式。
- 对批评器的权重进行梯度裁剪(Weight Clipping),例如限制在 ([-c, c]) 区间内,以近似满足Lipschitz约束。
- 推荐使用RMSProp或SGD等优化器,而非基于动量的Adam,以获得更稳定的训练。
✨ WGAN-GP:梯度惩罚(Gradient Penalty)
权重裁剪是一种简单但可能影响网络表达能力的强制约束。WGAN-GP提出了更优雅的梯度惩罚(Gradient Penalty) 方法。
其核心思想是:直接强制批评器对随机采样的数据点 ( \hat ) 的梯度范数接近1。具体做法是在损失函数中添加一项惩罚项:
[
\lambda \cdot \mathbb{\hat \sim P{\hat}} \left[ (| \nabla_{\hat} D(\hat) |_2 - 1)^2 \right]
]
其中 ( \hat ) 是真实样本和生成样本连线上的随机插值点。这种方法能更稳定地训练出性能更好的WGAN。
📝 总结
本节课中我们一起学习了生成式对抗网络(GAN)的核心内容:
- 我们首先了解了衡量分布差异的KL散度和JS散度。
- 然后深入探讨了GAN的基本框架,包括生成器与判别器的对抗博弈过程及其数学目标函数。
- 我们分析了原始GAN在训练中面临的主要挑战,如梯度消失、模式崩溃和评估困难。
- 最后,我们介绍了WGAN及其改进版本WGAN-GP,它们通过使用Wasserstein距离和梯度惩罚技巧,有效解决了原始GAN的梯度问题,使训练更加稳定。


生成式对抗网络是生成模型领域的里程碑,理解其原理和变体对于深入掌握现代深度学习至关重要。


七月在线-深度学习集训营 第三期[2022] - P6:深度学习在推荐系统里的应用场景 📺
在本节课中,我们将学习如何将深度学习的经典模型(如DNN、CNN、RNN、自编码器等)应用于推荐系统。我们将从最直接的模型迁移开始,逐步深入到为推荐系统专门设计的网络结构,并了解注意力机制和强化学习等前沿思想在推荐领域的应用。
第一部分:经典深度学习模型在推荐中的直接应用 🧩
上一节我们介绍了课程概述,本节中我们来看看如何将已有的深度学习模型直接应用到推荐系统数据上。这些应用方式通常不改变模型结构,仅将推荐数据(用户特征和物品特征)作为输入。
1. 深度神经网络(DNN)的直接应用
这是最直接的应用方式。模型将推荐数据分为用户特征和物品特征,分别进行嵌入(Embedding)后,连接一个多层全连接神经网络。
核心思想:用户特征向量 + 物品特征向量 -> 多层DNN -> 预测输出
一个著名的组合模型是 Wide & Deep Learning。它将简单的线性模型(Wide部分)和深度神经网络(Deep部分)并行结合。
- Wide部分:相当于一个逻辑回归(LR)模型,直接连接特征到输出,记忆性强,擅长捕捉明确的用户偏好。
- Deep部分:即上述的DNN,通过多层非线性变换,泛化能力强,能发现用户潜在的、不易直接观察的兴趣。
该模型通过并行结构,同时利用了LR的记忆性和DNN的泛化能力。


2. 卷积神经网络(CNN)的直接应用
在推荐场景中,CNN常被用作特征提取器。例如,在文本推荐中,用户和物品的原始特征(如文本描述)可以分别通过一个CNN来提取高级特征向量。
核心思想:用户文本 -> CNN -> 用户隐向量 与 物品文本 -> CNN -> 物品隐向量,然后计算两个隐向量的相似度(如内积)作为预测值。
3. 循环神经网络(RNN)的直接应用
RNN是序列模型,天然适合处理用户行为序列。我们可以将用户的购买或观看历史视为一个序列。
核心思想:将用户视为“句子”,将用户历史中的物品视为“词”。这样,根据用户过去的物品序列预测下一个物品,就等同于RNN中根据上文预测下一个词的任务。
数据处理示例:
# 用户A的历史行为序列: [物品1, 物品3, 物品7]
# 这可以转化为RNN的训练样本:输入 [物品1, 物品3],输出标签为 物品7
4. 自编码器(AutoEncoder)的直接应用

自编码器通过学习输入的重构来获取数据的隐含表示。在推荐中,一种经典用法是利用用户-物品评分矩阵。




核心思想:
- 输入是用户对所有物品的评分构成的稀疏向量。
- 通过编码器降维得到用户的隐向量(代表用户兴趣)。
- 通过解码器重构评分向量。
- 训练目标是让重构向量接近原始输入。


预测方法:输入时,用户未评分的物品位置为0(缺失)。训练好的模型在输出层会对所有物品生成预测评分,这些预测值即可用于推荐。

更复杂的模型会对用户和物品的特征分别进行自编码,得到两者的隐向量,再计算相似度。




第二部分:推荐系统特有的网络结构——交叉层 ⚙️



上一节我们介绍了经典模型的直接应用,本节中我们来看看为推荐系统定制的核心网络结构:交叉层。其核心思想是显式地建模特征之间的交互,尤其是用户特征与物品特征之间的交互。




1. 因子分解机(FM)与 DeepFM

FM是推荐系统的里程碑模型,它在线性模型的基础上增加了特征交叉项。
FM公式:
ŷ = w₀ + Σ w_i x_i + Σ Σ <v_i, v_j> x_i x_j
w₀ + Σ w_i x_i:线性部分,对应记忆性。Σ Σ <v_i, v_j> x_i x_j:交叉部分,特征i和j的交互权重由它们的隐向量内积<v_i, v_j>决定,对应泛化性。
DeepFM模型将FM与DNN以并行(左右)结构结合:
- 左侧FM部分:直接对输入特征进行一阶和二阶交叉。
- 右侧Deep部分:对嵌入后的特征进行深度非线性变换。
- 两部分输出相加得到最终预测。


FM交叉项的优化计算:
直接计算二阶交叉复杂度高(O(n²))。利用数学推导可优化为线性复杂度(O(n)):
ΣΣ <v_i, v_j> x_i x_j = 0.5 * Σ ( (Σ v_i x_i)² - Σ (v_i x_i)² )
这在实现上非常高效。

2. 深度交叉网络(DCN)


DCN探索了FM与DNN的另一种组合方式:堆叠(上下)结构。
- 底层:一个标准的DNN,对特征进行深度变换。
- 顶层:一个交叉层,对DNN输出的高阶隐向量进行特征交叉(如内积)。
这种结构旨在先通过DNN学习复杂的特征表示,再在其基础上进行特征交叉。

3. 更复杂的交叉:外积与CrossNet
一些模型认为特征隐向量不同维度之间也需要交叉。
- 内积(如FM):用户隐向量和物品隐向量对应维度相乘后求和。假设隐向量维度代表“兴趣维度”(如动作、喜剧),内积是同维度兴趣的匹配。
- 外积:计算用户隐向量和物品隐向量的外积,得到一个矩阵。这个矩阵包含了所有维度之间的两两交叉。为了便于后续处理,通常会通过一个权重向量将其压缩回原维度。
- CrossNet:将这种外积交叉做成了可堆叠的层,每一层输入和输出维度相同,可以像DNN一样多层堆叠,进行非常深度的特征交叉。


注意:虽然这类模型在结构上有创新,但过于复杂的交叉在实际场景中不一定带来稳定提升,其可解释性也较弱。
4. 端到端训练思想
现代推荐模型强调端到端训练。目标是尽可能减少人工特征工程,让模型从最原始的特征(如用户ID、物品ID、文本、图像)开始学习。
实现方式:在输入端,针对不同特征类型使用相应的子网络(如文本用CNN/RNN,图像用CNN)进行初步特征提取,然后将提取到的特征统一送入推荐主网络(如包含交叉层的网络)进行训练。
第三部分:前沿进展:注意力与强化学习 🔮
上一节我们介绍了推荐系统的核心交叉结构,本节中我们来看看两个前沿方向如何提升推荐效果。
1. 注意力机制(Attention)的应用
注意力机制让模型能够动态关注输入中最重要的部分。


在FM中引入注意力(AFM):
在FM的二阶交叉项中,不同的特征交叉对最终预测的贡献不同。注意力机制为每一个交叉项<v_i, v_j>学习一个权重a_ij。
- 直观理解:对于喜欢动作片的用户,“用户-动作”特征和“电影-动作”特征的交叉权重会很大;而对于喜欢喜剧的用户,则是“喜剧”相关的交叉权重更大。模型学会了动态分配注意力。
协同注意力网络:
用于利用用户和物品的评论信息。模型分两层筛选信息:
- 评论级注意力:从用户的所有历史评论和物品的所有历史评论中,找出彼此最相关、最重要的一对评论。
- 词级注意力:在筛选出的关键评论中,进一步找出最重要的关键词。
- 最终基于这些关键信息进行预测。这模拟了人类做决策时,先关注整体评价,再聚焦于细节的过程。
2. 强化学习(RL)的应用


将推荐过程视为智能体(推荐系统)与环境(用户)的序贯决策过程。
- 状态(State):当前用户画像、上下文、历史交互等。
- 动作(Action):推荐给用户哪个或哪些物品。
- 奖励(Reward):用户的正向反馈(如点击、购买、停留时长)和负向反馈(如忽略、快速离开、差评)。
优势:
- 考虑长期收益:不同于传统模型只优化下一次点击(即时奖励),强化学习可以优化用户的长期满意度(累计奖励),避免“标题党”等短期行为。
- 处理动态兴趣:能更好地建模用户兴趣随时间的变化。
- 无需精确标签:只需要一个能评价推荐动作好坏的“奖励信号”,而不需要“点击=1,不点击=0”这样的绝对标签,可以融入更复杂的业务指标。
总结 📝
本节课中我们一起学习了深度学习在推荐系统中的应用全景:
- 直接迁移:我们可以将DNN、CNN、RNN、自编码器等模型直接或稍加组合后用于推荐,处理用户和物品的特征及序列数据。
- 核心结构:交叉层是为推荐定制的核心,以FM为代表,显式建模特征交互。DeepFM、DCN等模型是FM与DNN的不同组合范式。
- 前沿方向:注意力机制让模型能聚焦关键信息;强化学习则将推荐建模为序贯决策问题,以优化长期收益为目标。

推荐系统是一个快速发展的领域,其核心是在理解用户和物品的基础上,有效匹配两者信息。深度学习通过强大的表示学习和复杂的交互建模能力,正不断推动推荐技术向前发展。

七月在线—算法coding公开课 - P1:实战动态规划(直播coding) 🚀
在本节课中,我们将通过三个具体的编程例题,深入学习动态规划(Dynamic Programming, DP)的实战应用。我们将从问题分析入手,推导出状态转移方程,并最终实现代码,同时探讨空间优化的技巧。
例一:最小路径和 📉
给定一个 M 行 N 列的二维矩阵,每个元素为非负整数。从左上角出发,每次只能向右或向下移动,最终到达右下角。目标是找到一条路径,使得路径上经过的所有数字之和最小。
问题分析与思路
最直接的思路是枚举所有可能的路径。从左上角到右下角需要走 M + N - 2 步,其中向下 M - 1 步,向右 N - 1 步。路径总数为组合数 C(M+N-2, M-1)。当 M 和 N 较大时(例如各为50),这个数字非常庞大,枚举法在实际中不可行。
因此,我们采用动态规划方法。
动态规划解法
我们定义状态 dp[i][j] 为:从左上角 (0, 0) 走到位置 (i, j) 的最小路径和。
要到达 (i, j),只能从正上方 (i-1, j) 向下走一步,或者从左方 (i, j-1) 向右走一步。因此,状态转移方程为:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
以下是初始条件的设置:
dp[0][0] = grid[0][0],因为起点就是其本身的值。- 对于第一行
(i=0, j>0),只能从左方来:dp[0][j] = dp[0][j-1] + grid[0][j]。 - 对于第一列
(j=0, i>0),只能从上方来:dp[i][0] = dp[i-1][0] + grid[i][0]。
最终答案即为 dp[M-1][N-1]。该算法的时间复杂度和空间复杂度均为 O(M * N)。
空间优化
观察状态转移方程,dp[i][j] 的值仅依赖于 dp[i-1][j] 和 dp[i][j-1]。我们可以使用一个一维数组 dp 进行滚动更新。在按行遍历时,dp[j] 在更新前存储的是上一行的值 dp[i-1][j],更新后存储的是当前行的值 dp[i][j]。优化后的空间复杂度为 O(N)。
贪心算法反例 ❌
贪心策略(每次都选择相邻格子中数字较小的方向前进)在此问题上不成立。例如,对于矩阵 [[1, 2], [100, 100]],贪心会选择 1 -> 100 -> 100,和为201。而最优路径是 1 -> 2 -> 100,和为103。
代码实现
以下是空间优化后的代码实现:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<int> dp(n, 0);
dp[0] = grid[0][0];
for (int j = 1; j < n; ++j) {
dp[j] = dp[j-1] + grid[0][j];
}
for (int i = 1; i < m; ++i) {
dp[0] += grid[i][0];
for (int j = 1; j < n; ++j) {
dp[j] = min(dp[j], dp[j-1]) + grid[i][j];
}
}
return dp[n-1];
}
上一节我们介绍了如何用动态规划解决网格路径问题,本节中我们来看看另一个经典问题:如何在一个数列中寻找和最大的连续子数组。
例二:最大子数组和 📈
给定一个整数数组,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
多种解法思路
- 暴力枚举(O(N³)):枚举所有子数组的起点和终点,再循环求和。
- 优化枚举(O(N²)):枚举起点,在枚举终点的同时累加和,避免内层循环。
- 分治法(O(N log N)):将数组分为两半,最大子数组和可能出现在左半部分、右半部分或跨越中点。递归求解。
- 动态规划(O(N)):这是最高效的方法。
动态规划解法
定义状态 dp[i] 为:以第 i 个元素 结尾 的最大子数组和。
对于 dp[i],我们有两种选择:
- 只包含当前元素
nums[i],子数组和为nums[i]。 - 将当前元素接在以
nums[i-1]结尾的最大子数组之后,子数组和为dp[i-1] + nums[i]。
我们需要在这两种方案中取最大值。因此,状态转移方程为:
dp[i] = max(nums[i], dp[i-1] + nums[i])
初始条件为 dp[0] = nums[0]。最终答案并非 dp[n-1],而是所有 dp[i] 中的最大值,因为最大子数组可能以任意位置结尾。
空间优化
由于 dp[i] 只依赖于 dp[i-1],我们可以用一个变量 curr_max 来代替整个 dp 数组,在遍历过程中不断更新。空间复杂度优化至 O(1)。
另一种线性思路:前缀和
定义前缀和 sum[i] 为 nums[0] 到 nums[i] 的和(sum[-1] = 0)。子数组 nums[i..j] 的和等于 sum[j] - sum[i-1]。
对于固定的 j,要使 sum[j] - sum[i-1] 最大,就需要 sum[i-1] 最小。因此,在遍历数组计算 sum[j] 时,同时维护一个 min_sum 记录已遍历前缀和的最小值,用 sum[j] - min_sum 更新答案即可。
代码实现
以下是动态规划(空间优化版)和前缀和两种方法的实现:
动态规划(O(1)空间):
int maxSubArray(vector<int>& nums) {
int curr_max = nums[0];
int global_max = nums[0];
for (int i = 1; i < nums.size(); ++i) {
curr_max = max(nums[i], curr_max + nums[i]);
global_max = max(global_max, curr_max);
}
return global_max;
}
前缀和法:
int maxSubArray(vector<int>& nums) {
int sum = 0;
int min_sum = 0; // 对应 sum[-1] = 0
int ans = nums[0];
for (int num : nums) {
sum += num;
ans = max(ans, sum - min_sum);
min_sum = min(min_sum, sum);
}
return ans;
}
解决了数组上的动态规划问题后,我们进入一个更复杂的领域:字符串编辑。如何衡量两个字符串的相似度,并计算将它们变得相同所需的最少操作次数?
例三:编辑距离 ✏️
给定两个单词 word1 和 word2,计算将 word1 转换成 word2 所使用的最少操作数。操作包括:插入一个字符、删除一个字符、替换一个字符。
问题转化:字符串对齐
我们可以将编辑操作转化为一个“字符串对齐”问题。在两个字符串中插入一种特殊的“空白”字符(如 -),使它们长度相等,然后进行对齐。
- 如果对齐的两个字符相同,不产生代价。
- 如果对齐的两个字符不同,则产生
1点代价(代表替换操作)。 - 如果
word1的字符与“空白”对齐,产生1点代价(代表删除操作)。 - 如果
word2的字符与“空白”对齐,产生1点代价(代表插入操作)。
最小编辑距离就等价于寻找一种对齐方式,使得总代价最小。
动态规划解法
定义状态 dp[i][j] 为:将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数。
我们考虑如何得到 dp[i][j]:
- 替换或匹配:让
word1[i-1]和word2[j-1]对齐。- 如果它们相等,无需额外操作,代价为
dp[i-1][j-1]。 - 如果不等,需要一次替换操作,代价为
dp[i-1][j-1] + 1。
- 如果它们相等,无需额外操作,代价为
- 删除:让
word1[i-1]与一个“空白”对齐。这意味着word1的前i-1位已经和word2的前j位对齐,现在删除word1[i-1]。代价为dp[i-1][j] + 1。 - 插入:让
word2[j-1]与一个“空白”对齐。这意味着word1的前i位已经和word2的前j-1位对齐,现在插入word2[j-1]。代价为dp[i][j-1] + 1。
我们需要从这三种可能的“最后一步操作”中选择代价最小的一个。因此,状态转移方程为:
dp[i][j] = min(dp[i-1][j-1] + cost, dp[i-1][j] + 1, dp[i][j-1] + 1)
其中,cost = 0 若 word1[i-1] == word2[j-1],否则 cost = 1。
初始条件:
dp[0][j] = j:将空字符串变为word2的前j个字符,需要j次插入。dp[i][0] = i:将word1的前i个字符变为空字符串,需要i次删除。
最终答案为 dp[m][n],其中 m 和 n 分别为两个单词的长度。时间复杂度和空间复杂度均为 O(m * n)。
空间优化
状态 dp[i][j] 依赖于其左方 dp[i][j-1]、上方 dp[i-1][j] 和左上方 dp[i-1][j-1] 的值。我们可以使用一维数组进行优化,但需要额外一个变量来保存被覆盖掉的 dp[i-1][j-1] 的值。
代码实现
以下是空间优化后的代码实现:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
vector<int> dp(n + 1, 0);
for (int j = 0; j <= n; ++j) dp[j] = j; // 初始化第一行
for (int i = 1; i <= m; ++i) {
int prev = dp[0]; // 保存左上角的值 dp[i-1][j-1]
dp[0] = i; // 更新当前行的第一列 dp[i][0]
for (int j = 1; j <= n; ++j) {
int temp = dp[j]; // 保存旧的 dp[j],即下一轮的 dp[i-1][j-1]
if (word1[i-1] == word2[j-1]) {
dp[j] = prev;
} else {
dp[j] = min(prev, min(dp[j], dp[j-1])) + 1;
}
prev = temp; // 为下一列更新 prev
}
}
return dp[n];
}
总结与思考 🎯
本节课中我们一起学习了动态规划的实战应用,通过三个由浅入深的例题掌握了其核心思想与解题步骤:
- 定义状态:用
dp[i]或dp[i][j]等形式清晰地表示子问题的解。 - 建立状态转移方程:找出状态之间的关系,这是动态规划的核心。通常需要枚举“最后一步”或“当前决策”的所有可能情况。
- 确定初始条件(边界):这是递推的起点,必须仔细考虑。
- 计算顺序与答案:确定填表顺序,并找到最终答案对应的状态。
- 空间优化:观察状态依赖关系,常常可以通过滚动数组将空间复杂度降低一维。
动态规划的本质是一种高效的递推,它通过存储子问题的解避免了重复计算。要熟练掌握动态规划,关键在于多练习、多思考,培养对问题状态定义和转移的直觉。
推荐练习(LeetCode题号):
-
- 爬楼梯
-
- 买卖股票的最佳时机
-
- 打家劫舍
-
- 零钱兑换
-
- 最长公共子序列
-
- 最长递增子序列


希望本教程能帮助你更好地理解和运用动态规划这一强大的算法工具。


七月在线—算法coding公开课 - P2:排序查找实战 🎯
在本节课中,我们将通过三个实战例题,深入探讨排序与查找算法的核心思想与代码实现。我们将学习如何利用矩阵的有序性进行高效查找,如何合并两个有序数组并寻找中位数,以及如何使用“循环不变式”思想解决经典的荷兰国旗问题。课程将结合算法思路讲解与现场代码编写,帮助初学者理解并掌握这些关键技巧。
例题一:杨氏矩阵查找 🔍
所谓杨氏矩阵,是指矩阵的每一行、每一列都满足单调递增的性质。我们的任务是在这样的矩阵中,查找一个给定的目标值 target 是否出现。
方法一:行列缩减法

上一节我们介绍了问题背景,本节中我们来看看第一种解法。该方法的核心思想是:每次尝试删掉矩阵的一行或一列,并保证目标值 target 不在被删除的行列中,从而将问题规模缩小。
我们从矩阵的右上角(第一行最后一列)开始比较。设当前位置的值为 x,目标值为 target:
- 如果
x < target,根据杨氏矩阵的性质,第一行所有元素都小于target。因此可以安全地删除第一行。 - 如果
x > target,则最后一列所有元素都大于target。因此可以安全地删除最后一列。 - 如果
x == target,则查找成功。
每次操作后,我们始终关注剩余子矩阵的右上角元素。该算法的时间复杂度为 O(m + n),其中 m 为行数,n 为列数。
以下是该方法的代码实现:
def searchMatrix(matrix, target):
if not matrix:
return False
m, n = len(matrix), len(matrix[0])
row, col = 0, n - 1 # 起始位置:右上角
while row < m and col >= 0:
if matrix[row][col] == target:
return True
elif matrix[row][col] < target:
row += 1 # 删除当前行
else:
col -= 1 # 删除当前列
return False
方法二:中心点分治法
接下来,我们看看利用分治思想的第二种方法。我们取矩阵的中心点 x,并与目标值 target 比较。
- 如果
x < target,则中心点左上方的子矩阵A中所有值均小于target,可删除。 - 如果
x > target,则中心点右下方的子矩阵D中所有值均大于target,可删除。
删除一部分后,问题被分解为在剩余的几个子矩阵中递归查找。该方法的时间复杂度分析较为复杂,介于 O(√(mn)) 到 O(mn) 之间。
方法三:中线二分法
最后,我们介绍一种更高效的分治法。我们在矩阵的中间一行进行二分查找,找到最后一个小于等于 target 的值 x1,以及紧邻其后第一个大于 target 的值 x2。
- 以
x1为右下角的子矩阵A中所有值均小于target。 - 以
x2为左上角的子矩阵D中所有值均大于target。

因此,我们可以同时删除 A 和 D 两个子矩阵,仅在剩余的 B 和 C 两个子矩阵中递归查找。该方法的时间复杂度可优化至 O(√(m*n))。
以下是利用中线二分法的代码框架:
def searchMatrix(matrix, target):
def binary_search_row(arr, left, right, target):
# 在数组arr的[left, right]区间内,二分查找最后一个<=target的索引
while left <= right:
mid = (left + right) // 2
if arr[mid] <= target:
left = mid + 1
else:
right = mid - 1
return right # 返回最后一个<=target的索引
def search_sub(matrix, x1, y1, x2, y2, target):
# 在子矩阵(左上角[x1,y1], 右下角[x2,y2])中查找
if x1 > x2 or y1 > y2:
return False
mid_row = (x1 + x2) // 2
# 在中间行二分查找临界点
col_idx = binary_search_row(matrix[mid_row], y1, y2, target)
# 检查是否找到目标
if col_idx >= y1 and matrix[mid_row][col_idx] == target:
return True
# 递归搜索剩余的两个子矩阵
return (search_sub(matrix, x1, col_idx + 1, mid_row - 1, y2, target) or
search_sub(matrix, mid_row + 1, y1, x2, col_idx, target))
# 主函数调用
if not matrix:
return False
return search_sub(matrix, 0, 0, len(matrix)-1, len(matrix[0])-1, target)

例题二:两个有序数组的中位数 📊
现在,我们来看一个更复杂的问题:如何找到两个有序数组合并后的中位数。这可以泛化为寻找两个有序数组的第 k 小元素问题。
方法一:归并法
最直观的方法是模拟归并排序的合并过程,从两个数组头部开始比较,取出前 k 个元素,第 k 个即为所求。此方法时间复杂度为 O(k),在最坏情况下(k 接近 m+n)为 O(m+n)。
以下是归并法的核心代码逻辑:
def findKthSortedArrays(A, B, k):
i, j = 0, 0
while i < len(A) and j < len(B):
k -= 1
if A[i] < B[j]:
if k == 0:
return A[i]
i += 1
else:
if k == 0:
return B[j]
j += 1
# 某个数组已耗尽
if i < len(A):
return A[i + k - 1]
else:
return B[j + k - 1]
方法二:分治法(高效版)
为了达到对数级复杂度,我们需要更巧妙的分治策略。假设我们要从数组 A 和 B 中找第 k 小的数。
- 我们尝试从较短的数组
A中取前pa = min(k//2, len(A))个元素,从B中取前pb = k - pa个元素。 - 比较
A[pa-1]和B[pb-1]:- 如果
A[pa-1] < B[pb-1]:说明A中这pa个元素一定属于合并后的前k小元素,且第k小的数不可能在A的这前pa个元素中(因为它们太小了),也不可能在B的第pb个元素之后(因为光靠A的这pa个和B的前pb个已经凑够k个候选了)。因此,我们可以丢弃A的前pa个元素和B的pb之后的所有元素,然后在剩余部分中寻找第(k - pa)小的数。 - 如果
A[pa-1] >= B[pb-1]:情况对称,丢弃B的前pb个元素和A的pa之后的所有元素,在剩余部分寻找第(k - pb)小的数。
- 如果
每次递归,k 的值至少减少一半,因此时间复杂度为 O(log k),对于中位数问题即为 O(log(m+n))。
以下是分治法的实现:
def findKth(A, B, k):
# 保证A是较短的数组
if len(A) > len(B):
A, B = B, A
if not A:
return B[k-1]
if k == 1:
return min(A[0], B[0])
pa = min(k // 2, len(A))
pb = k - pa
if A[pa-1] < B[pb-1]:
# 丢弃A的前pa个,B的pb之后的部分
return findKth(A[pa:], B[:pb], k - pa)
else:
# 丢弃B的前pb个,A的pa之后的部分
return findKth(A[:pa], B[pb:], k - pb)

例题三:荷兰国旗问题 🏳️🌈
荷兰国旗问题要求我们将一个仅包含 0, 1, 2 的数组,通过交换操作,排序成 0 在前,1 在中,2 在后的形式。
循环不变式解法
解决此问题的经典方法是使用三指针,并维护一个“循环不变式”来保证算法的正确性。我们定义三个指针:
i:指向下一个0应该放置的位置([0, i-1]区间全是0)。j:指向下一个2应该放置的位置([j, n-1]区间全是2)。k:当前遍历的指针([i, k-1]区间全是1,[k, j-1]是待处理区间)。
循环不变式:在遍历过程中,始终保证:
[0, i-1]全是0。[i, k-1]全是1。[k, j-1]是待处理的未知区域。[j, n-1]全是2。
算法过程如下,遍历指针 k 从 0 到 j-1:
- 若
nums[k] == 0:交换nums[k]和nums[i],然后i++,k++。因为交换后nums[k]变成了1(来自原nums[i]),符合区间定义。 - 若
nums[k] == 1:k++即可。 - 若
nums[k] == 2:交换nums[k]和nums[j-1],然后j--。注意,此时k不能增加,因为从后面交换过来的元素是未处理的,需要在下一次循环中判断。
该算法只需一次遍历,时间复杂度为 O(n),空间复杂度为 O(1)。

以下是代码实现:
def sortColors(nums):
i, k, j = 0, 0, len(nums)
while k < j:
if nums[k] == 0:
nums[i], nums[k] = nums[k], nums[i]
i += 1
k += 1
elif nums[k] == 1:
k += 1
else: # nums[k] == 2
j -= 1
nums[k], nums[j] = nums[j], nums[k]
# k 不自增,因为交换过来的 nums[j] 尚未处理

总结 📝
本节课中我们一起学习了排序与查找的三大实战案例:
- 杨氏矩阵查找:我们探讨了利用矩阵有序性的三种策略——行列缩减、中心分治和中线二分,理解了如何将二维查找问题化归为一维或更小的子问题。
- 两个有序数组的中位数:我们将其抽象为寻找第 k 小元素问题,并对比了直观的归并法与高效的分治法。分治法的核心在于通过比较“试探点”,安全地丢弃不可能包含目标的大量子数组。
- 荷兰国旗问题:我们学习了“循环不变式”这一重要的算法证明与设计思想,并利用三指针一次扫描解决了三分类排序问题,这同时也是快速排序中“三路划分”的核心思想。


查找的本质在于利用数据的有序性,通过分治(如二分)快速缩小搜索范围。排序则涉及多种策略,本节课涉及的划分思想是快速排序等高效算法的基础。掌握这些核心思想,是理解和设计更复杂算法的关键。


七月在线—算法coding公开课 - P3:链表实战(直播coding) 🧩
在本节课中,我们将学习链表相关的几个经典问题及其解决方案。课程将涵盖链表是否有环的判断、奇偶节点分离、链表排序以及有序链表去重等核心算法。我们将通过分析问题、推导数学原理,并最终用代码实现,帮助初学者掌握链表操作的关键技巧。
1. 链表是否有环 🔄
上一节我们介绍了课程概述,本节中我们来看看第一个经典问题:如何判断一个单链表是否有环,并找出环的起点。
普通单链表的最后一个节点指向空。如果链表有环,则最后一个节点会指向前面的某个节点。环的形状可能像数字“6”,即前面有一条“尾巴”(线段),后面连接一个环。
最直接的方法是使用一个集合(如 set 或 map)记录遍历过的节点。遍历链表时,检查当前节点是否已在集合中。如果出现过,则说明有环,且该节点即为环的起点。
以下是使用集合方法的代码示例:
def hasCycle(head):
visited = set()
while head:
if head in visited:
return head # 找到环的起点
visited.add(head)
head = head.next
return None # 无环
此方法的时间复杂度为 O(N),空间复杂度也为 O(N),因为需要额外的存储空间。
更优的方法是使用双指针(快慢指针)。定义两个指针 slow 和 fast,slow 每次移动一步,fast 每次移动两步。如果链表有环,它们最终会相遇。
数学推导:
假设链表“尾巴”部分有 B 个节点,环部分有 A 个节点。当 slow 进入环时,fast 已在环中,设其领先 slow C 步(C < A)。由于 fast 速度是 slow 的两倍,它们之间的距离每步减少 1。因此,经过 A - C 步后,两者相遇。
第一次相遇后,将 slow 移回链表头部,然后 slow 和 fast 都以每次一步的速度移动。当它们再次相遇时,相遇点即为环的起点。
以下是双指针方法的代码实现:
def detectCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast: # 第一次相遇
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow # 环的起点
return None # 无环
此方法的时间复杂度为 O(N),空间复杂度为 O(1)。

2. 奇偶节点分离 ⚖️
上一节我们学习了如何检测链表中的环,本节中我们来看看如何将链表中的奇数位置节点和偶数位置节点分离,并重新排列。
问题要求将奇数位置节点按原顺序排在前面,偶数位置节点按原顺序排在后面。关键思路是创建两个新链表,分别存储奇数位置节点和偶数位置节点,最后将偶数链表连接到奇数链表之后。

以下是实现步骤:

- 初始化四个指针:
odd_head,odd_tail,even_head,even_tail,分别表示奇数链表和偶数链表的头尾。 - 遍历原链表,根据节点位置(索引)将其添加到对应的链表中。
- 处理边界情况,如链表为空或只有一个节点。
- 连接奇数链表尾部与偶数链表头部,并确保新链表的最后一个节点指向
None。

以下是代码实现:
def oddEvenList(head):
if not head:
return None
odd_head = odd_tail = None
even_head = even_tail = None
index = 1
current = head
while current:
if index % 2 == 1: # 奇数位置
if odd_tail:
odd_tail.next = current
odd_tail = current
else:
odd_head = odd_tail = current
else: # 偶数位置
if even_tail:
even_tail.next = current
even_tail = current
else:
even_head = even_tail = current
current = current.next
index += 1
# 连接两个链表
if odd_tail:
odd_tail.next = even_head
else:
odd_head = even_head
# 确保链表尾部指向 None
if even_tail:
even_tail.next = None
return odd_head
此方法的时间复杂度为 O(N),空间复杂度为 O(1)。
3. 链表排序 📊

上一节我们完成了奇偶节点的分离,本节中我们来看看如何对链表进行排序,要求时间复杂度为 O(N log N),空间复杂度为 O(1)。

由于链表不支持随机访问,归并排序是更适合的选择。递归实现归并排序的步骤如下:
- 找到链表的中点,使用快慢指针法。
- 递归地对前半部分和后半部分进行排序。
- 合并两个已排序的链表。


以下是递归实现归并排序的代码:

def sortList(head):
# 边界条件:链表为空或只有一个节点
if not head or not head.next:
return head
# 使用快慢指针找到链表中点
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 分割链表
mid = slow.next
slow.next = None
# 递归排序
left = sortList(head)
right = sortList(mid)
# 合并两个有序链表
return merge(left, right)
def merge(l1, l2):
dummy = ListNode(0)
tail = dummy
while l1 and l2:
if l1.val < l2.val:
tail.next = l1
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
# 处理剩余节点
tail.next = l1 if l1 else l2
return dummy.next
此方法的时间复杂度为 O(N log N),空间复杂度为 O(log N)(递归栈空间)。若要求严格 O(1) 空间,需使用非递归实现。
4. 有序链表去重 🧹

上一节我们学习了链表的排序,本节中我们来看看如何对有序链表进行去重。根据要求不同,去重分为两种:删除所有重复节点(保留唯一值)和保留重复节点中的一个。


4.1 删除所有重复节点
以下是删除所有重复节点的实现步骤:

- 创建一个虚拟头节点,简化边界处理。
- 遍历链表,如果当前节点与下一个节点值相同,则跳过所有重复节点。
- 将非重复节点连接到新链表中。
以下是代码实现:
def deleteDuplicatesAll(head):
dummy = ListNode(0)
dummy.next = head
prev = dummy
while head:
if head.next and head.val == head.next.val:
# 跳过所有重复节点
while head.next and head.val == head.next.val:
head = head.next
prev.next = head.next
else:
prev = prev.next
head = head.next
return dummy.next
4.2 保留重复节点中的一个
以下是保留重复节点中的一个的实现步骤:

- 遍历链表,如果当前节点与下一个节点值相同,则跳过重复节点,只保留一个。
- 直接修改原链表。
以下是代码实现:
def deleteDuplicatesOne(head):
current = head
while current and current.next:
if current.val == current.next.val:
current.next = current.next.next
else:
current = current.next
return head
这两种方法的时间复杂度均为 O(N),空间复杂度为 O(1)。
总结 📝
本节课中我们一起学习了链表相关的四个核心问题:
- 链表是否有环:通过双指针法(快慢指针)可以在 O(N) 时间和 O(1) 空间内检测环并找到环的起点。
- 奇偶节点分离:通过创建两个子链表并重新连接,可以在 O(N) 时间内完成节点重排。
- 链表排序:使用归并排序可以在 O(N log N) 时间内对链表进行排序,递归实现较为简洁。
- 有序链表去重:根据需求选择删除所有重复节点或保留一个,均可在 O(N) 时间内完成。


链表操作的关键在于正确处理指针的指向和边界条件(如头节点、尾节点和空值)。通过本节课的学习,希望大家能够掌握链表问题的基本解决思路,并能够灵活应用到其他类似问题中。


七月在线—算法coding公开课 - P4:树实战 🌳
在本节课中,我们将学习树这种数据结构的基础知识、核心性质,并通过解决几个LeetCode上的经典题目来实践树的遍历、递归应用以及字典树(Trie)的使用。
树的定义与分类 📚
树是一种特殊的图。图由节点和边构成。树需要满足三个条件:它是一个无向图、无环图且连通图。这三个条件缺一不可。
树可以分为两类:
- 有根树:存在一个特殊节点称为根。定义了根后,节点间便有了父子关系。
- 无根树:没有特殊根节点的树。
在计算机科学中,二叉树尤为重要,例如二叉搜索树。普通树的研究则更多在图论领域。
树的核心性质 🔑
以下是树的一些关键性质:
- 节点与边的关系:一棵有
N个节点的树,恰好有N-1条边。 - 叶子节点存在性:当
N >= 2时,树中至少存在两个度为1的节点(叶子节点)。 - 路径唯一性:树中任意两个节点之间有且仅有一条简单路径。
- 最小连通图:树是使所有
N个节点连通所需边数最少的图。这意味着树中的每条边都是“割边”,移除任何一条边都会破坏连通性。 - 成环性:在树中添加任意一条新边,都会形成一个环。
上一节我们介绍了树的基本概念,本节中我们来看看如何应用这些知识解决实际问题。
例题一:验证二叉树的前序序列化 (LeetCode 331) 🔍

问题概述:给定一个用逗号分隔的字符串,表示二叉树的前序遍历序列(空节点用 # 表示),判断该序列是否是一个合法的二叉树前序序列。
核心思路:我们无需重建整棵树。可以将问题转化为“槽位”的分配与消耗。每个非空节点会消耗父节点的一个槽位,并为自己创造两个新的槽位(用于连接左右孩子);每个空节点(#)只消耗父节点的一个槽位,不创造新槽位。
开始时,我们假设存在一个虚拟根节点,它提供1个初始槽位用于放置真正的根节点。然后遍历序列:
- 无论遇到什么节点,首先需要消耗一个槽位。如果此时槽位不足,则序列非法。
- 如果节点非空(不是
#),则在消耗一个槽位后,增加两个新槽位。 - 如果节点是空节点(
#),则只消耗槽位,不增加。
遍历结束后,合法的序列必须恰好用完所有槽位(槽位数为0)。
以下是实现该思路的代码:

bool isValidSerialization(string preorder) {
// 将逗号替换为空格,便于使用字符串流处理
for (char &c : preorder) if (c == ',') c = ' ';
stringstream ss(preorder);
string node;
int slots = 1; // 初始槽位数(虚拟根提供)
while (ss >> node) {
// 消耗一个槽位
slots--;
if (slots < 0) return false; // 槽位不足,非法
// 若非空节点,增加两个槽位
if (node != "#") slots += 2;
}
// 遍历结束,槽位必须恰好为0
return slots == 0;
}

例题二:递归框架应用(三题合一) 🔄
接下来,我们通过三个紧密相关的问题来理解处理二叉树的递归框架。这三个问题分别是:判断两棵树是否相同(LeetCode 100)、求二叉树的最大深度(LeetCode 104)和最小深度(LeetCode 111)。
它们的核心都是递归地处理根节点、左子树和右子树。
1. 相同的树 (LeetCode 100)
判断以 p 和 q 为根的两棵二叉树是否完全相同。
递归逻辑:两棵树相同,当且仅当它们的根节点值相同,并且左子树相同、右子树也相同。
bool isSameTree(TreeNode* p, TreeNode* q) {
if (!p && !q) return true; // 都为空
if (!p || !q) return false; // 一个空一个非空
if (p->val != q->val) return false; // 根节点值不同
// 递归判断左右子树
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
2. 二叉树的最大深度 (LeetCode 104)
计算二叉树的最大深度(根节点到最远叶子节点的最长路径上的节点数)。
递归逻辑:空树深度为0。非空树的最大深度等于其左右子树最大深度的较大值加1(加1代表根节点自身)。


int maxDepth(TreeNode* root) {
if (!root) return 0;
return 1 + max(maxDepth(root->left), maxDepth(root->right));
}
3. 二叉树的最小深度 (LeetCode 111)
计算二叉树的最小深度(根节点到最近叶子节点的最短路径上的节点数)。
递归逻辑:需要特别注意,最小深度的终点必须是叶子节点(左右孩子都为空)。
- 如果当前节点为空,返回0。
- 如果左右子树均存在,最小深度为左右子树最小深度的较小值加1。
- 如果只有一棵子树存在,最小深度为该子树的最小深度加1(因为路径必须到达叶子,不能在半路停止)。
- 如果当前节点就是叶子节点,返回1。
int minDepth(TreeNode* root) {
if (!root) return 0;
if (root->left && root->right) {
return 1 + min(minDepth(root->left), minDepth(root->right));
} else if (root->left) {
return 1 + minDepth(root->left);
} else if (root->right) {
return 1 + minDepth(root->right);
} else {
return 1; // 叶子节点
}
}

例题三:实现 Trie (前缀树/字典树) (LeetCode 208) 📖
Trie 概述:Trie(发音同 “try”)是一种用于高效存储和检索字符串数据集的前缀树。对于只包含小写字母的单词,可以将其视为一棵26叉树。每个节点包含一个长度为26的子节点指针数组和一个布尔标记,表示从根到该节点的路径是否构成一个完整单词。
核心操作:
- 插入:从根开始,沿着单词每个字符对应的分支向下走。如果路径不存在则创建新节点。遍历完单词后,在最后一个节点标记为单词结束。
- 搜索:从根开始,沿着单词每个字符对应的分支向下走。如果中途路径断开,或遍历完后当前节点未标记为单词结束,则返回
false。 - 前缀搜索:与搜索类似,但只需检查路径是否存在,无需检查节点是否标记为单词结束。
以下是Trie的实现:
class TrieNode {
public:
vector<TrieNode*> children;
bool isEnd;
TrieNode() : children(26, nullptr), isEnd(false) {}
};
class Trie {
private:
TrieNode* root;
public:
Trie() {
root = new TrieNode();
}
void insert(string word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) {
node->children[idx] = new TrieNode();
}
node = node->children[idx];
}
node->isEnd = true;
}
bool search(string word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) return false;
node = node->children[idx];
}
return node->isEnd;
}
bool startsWith(string prefix) {
TrieNode* node = root;
for (char c : prefix) {
int idx = c - 'a';
if (!node->children[idx]) return false;
node = node->children[idx];
}
return true; // 只要路径存在即可
}
};
总结 📝

本节课中我们一起学习了树结构的实战应用。
- 理解树的遍历:通过验证前序序列化问题,我们掌握了如何在不显式建树的情况下,利用遍历顺序的性质(如槽位计算)解决问题。
- 掌握递归框架:通过判断树相同、求最大深度和最小深度这三个问题,我们深入理解了处理二叉树问题的通用递归范式——分解为根、左子树、右子树的子问题。
- 学习使用 Trie:我们实现了字典树(Trie),理解了其高效处理字符串前缀相关查询的原理,并掌握了插入、搜索和前缀搜索三个核心操作的代码实现。


树是算法中极其重要的数据结构,熟练掌握其遍历、递归操作以及高级变种(如Trie),是解决更复杂问题的基础。


七月在线—算法coding公开课 - P5:数组实战(直播coding) 🎯
在本节课中,我们将通过三个LeetCode上的经典问题,深入探讨数组相关的算法实战技巧。我们将学习如何利用归并排序、分桶、二进制运算、二分查找以及动态规划等多种方法来解决数组问题,并体会“一题多解”的思维魅力。


概述 📋
数组是算法中最基础的数据结构之一。本节课我们将聚焦于数组的实战应用,通过解决三个具体问题,学习如何将理论知识转化为代码。我们将重点关注算法的时间与空间复杂度优化,以及不同解法的核心思想。
问题一:计算右侧小于当前元素的个数 🔢
LeetCode第315题。给定一个整数数组,要求返回一个新数组,新数组的每个元素表示原数组中该位置元素右侧比它小的元素个数。例如,输入 [5, 2, 6, 1],输出应为 [2, 1, 1, 0]。
这个问题本质上是计算每个元素的“逆序数”。我们将介绍三种不同的解法。
解法一:归并排序法
上一节我们介绍了问题,本节中我们来看看如何使用经典的归并排序算法来解决它。关键在于,我们不能直接对原数组排序,否则会丢失元素的原下标信息。因此,我们改为对下标数组进行排序。


以下是实现步骤:
- 定义一个递归函数
mergeSort,用于对下标数组indices在区间[left, right]内进行归并排序。 - 在归并过程中,当合并两个已排序的子数组时,如果左侧子数组的当前元素大于右侧子数组的当前元素,则意味着左侧当前元素大于右侧当前元素及其之后的所有元素。此时,需要更新答案数组
answer。 - 注意处理元素相等的情况,应优先移动右侧指针,以保证计数的正确性。
核心代码逻辑如下:
void mergeSort(vector<int>& nums, vector<int>& indices, vector<int>& answer, int left, int right) {
if (left >= right) return;
int mid = left + (right - left) / 2;
mergeSort(nums, indices, answer, left, mid);
mergeSort(nums, indices, answer, mid + 1, right);
// 合并并统计逆序数
// ... 具体合并逻辑
}
这种方法的时间复杂度为 O(n log n),空间复杂度为 O(n)。
解法二:离散化 + 分桶法
归并排序的代码量较大。本节我们来看看一种更直观的暴力优化方法——离散化加分桶。


首先进行离散化:将原数组中的数字映射到 0 到 n-1 的范围内,忽略绝对大小,只保留相对顺序。这可以通过排序实现,时间复杂度为 O(n log n)。
接着进行分桶:我们将数值范围 [0, n-1] 分成大小为 bucket_size = sqrt(n) 的桶。然后从右向左遍历数组:
- 对于当前数
x,比它小的数由两部分组成:- 所有编号比
x所在桶编号小的桶中的数。我们可以直接累加这些桶的计数,复杂度为 O(√n)。 - 与
x在同一个桶中,但数值比x小的数。我们需要遍历这个桶内的元素进行统计,复杂度也是 O(√n)。
- 所有编号比
- 统计完毕后,将当前数
x加入对应的桶和计数数组中。
由于每个数处理一次的复杂度为 O(√n),总时间复杂度为 O(n√n)。虽然比归并排序慢,但代码简洁,且易于理解。
解法三:离散化 + 二进制索引法


本节我们来看一种利用二进制位运算的巧妙方法,它也能达到 O(n log n) 的时间复杂度。
离散化步骤同上。核心思想是:对于任意一个数 x,比它小的数可以按二进制位分类。我们从最低位开始,将 x 右移。每当遇到 x 的某一位是 1 时,我们就将这一位变为 0,然后查看以这个新数为前缀的数出现了多少次(这些数都比 x 小)。
我们维护一个计数数组 count。从右向左遍历原数组(映射后的):
- 初始化答案
ans[i] = 0。 - 对于当前数
x,只要x > 0,就循环:- 如果
x的最低位是1,则ans[i] += count[x ^ 1](x^1即把最低位1变为0)。 - 将
count[x]++。 - 将
x右移一位 (x >>= 1)。
- 如果
- 循环结束后,
ans[i]即为右侧小于nums[i]的元素个数。


这种方法代码量小,且效率高。
问题二:寻找重复数 🔍
LeetCode第287题。给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包含 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。要求不能更改原数组,只能使用 O(1) 的额外空间,且时间复杂度小于 O(n²)。
解法一:二进制位运算法


上一节我们处理了逆序数问题,本节中我们来看看如何利用二进制位来寻找重复数。由于数字范围是 1 到 n,这相当于天然做好了离散化。
思路是逐位确定重复数的二进制位。对于第 i 位:
- 计算
1到n这n个数中,第i位为1的数的个数,记为cnt_expected。 - 计算数组
nums中(共n+1个数),第i位为1的数的个数,记为cnt_actual。 - 如果
cnt_actual > cnt_expected,说明重复的数在第i位上是1,否则是0。
通过遍历所有二进制位(最多 log n 位),即可拼凑出重复的数。每次统计需要遍历整个数组,因此总时间复杂度为 O(n log n)。


解法二:二分查找法
除了位运算,我们还可以使用二分查找的思想。虽然数组无序,但我们可以二分数值的范围。
设当前查找的数值范围为 [left, right],计算中点 mid。统计原数组 nums 中,值在 [left, mid] 区间内的元素个数 count。
- 如果
count > (mid - left + 1),说明重复的数一定在[left, mid]这个区间内(因为区间内应有的数字数量是mid-left+1,现在多出来了)。 - 否则,重复的数在
[mid+1, right]区间内。


每次二分都将数值范围缩小一半,每次统计需要遍历整个数组,因此时间复杂度也是 O(n log n)。这种方法同样满足所有限制条件。
问题三:戳气球 🎈
LeetCode第312题。有 n 个气球,编号为 0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。求所能获得硬币的最大数量。


解法:动态规划法

前两个问题我们关注了数组与序的关系,本节我们来看一个数组上的动态规划问题。戳气球的难点在于,每戳破一个气球,左右气球的相邻关系就会发生变化。
我们换个角度思考:最后被戳破的气球是哪个? 假设最后戳破的气球是 k,那么在戳破 k 之前,区间 [0, k-1] 和 [k+1, n-1] 的气球都已经被戳破了,并且这两个子问题相互独立。戳破 k 时,它的左右邻居就是原数组的 nums[-1](边界外,视为1)和 nums[n](边界外,视为1),或者说是其外部相邻的、尚未被戳破的“虚拟气球”。
因此,我们可以定义动态规划状态:
dp[i][j] 表示戳破开区间 (i, j)(即不戳 i 和 j)内的所有气球,能获得的最大硬币数。这里 i 和 j 是边界,可以理解为 nums[-1] 和 nums[n]。
状态转移方程:我们枚举区间 (i, j) 内最后一个被戳破的气球 k(i < k < j)。
- 戳破
k时,区间(i, k)和(k, j)内的气球已经全部被戳破,所以k的左右邻居就是i和j。 - 因此,戳破
k获得的硬币为nums[i] * nums[k] * nums[j]。 - 总收益为:
dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]。
我们需要取所有可能的 k 中的最大值:
dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]),其中 i < k < j。
初始条件:当 i + 1 >= j 时,开区间内没有气球,dp[i][j] = 0。
最终答案:dp[0][n+1],这里我们在原数组首尾添加值为 1 的虚拟气球。

我们需要按区间长度从小到大的顺序来计算 dp 值。时间复杂度为 O(n³),空间复杂度为 O(n²)。
核心代码结构如下:
int maxCoins(vector<int>& nums) {
int n = nums.size();
vector<int> balloons(n + 2, 1);
for (int i = 0; i < n; i++) balloons[i + 1] = nums[i];
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
for (int len = 2; len <= n + 1; len++) { // 区间长度
for (int i = 0; i + len <= n + 1; i++) { // 区间起点
int j = i + len;
for (int k = i + 1; k < j; k++) { // 枚举最后戳破的气球k
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + balloons[i] * balloons[k] * balloons[j]);
}
}
}
return dp[0][n + 1];
}
总结 🏁


本节课我们一起学习了三个数组相关的经典算法问题:
- 计算右侧小于当前元素的个数:我们掌握了归并排序、离散化分桶以及二进制索引三种解法,理解了如何利用“序”和二进制位来高效统计逆序对。
- 寻找重复数:在严格的限制条件下,我们运用二进制位统计和二分查找值域两种 O(n log n) 的方法找到了重复数,避免了修改数组和使用额外空间。
- 戳气球:我们学习了如何将复杂的交互性问题转化为动态规划模型,通过定义“最后戳破的气球”这一状态,巧妙地处理了状态转移的依赖关系。

数组作为基础数据结构,其相关算法博大精深。解决数组问题不仅需要熟悉排序、查找、二分等基础操作,更需要灵活运用位运算、分治、动态规划等高级技巧,并时常进行“一题多解”的思考与练习。希望本课的内容能为大家的算法学习之路提供有益的启发。


七月在线—算法coding公开课 - P6:图搜索实战(直播coding) 🧭
在本节课中,我们将学习图搜索算法的实战应用。图搜索是深度优先搜索(DFS)和广度优先搜索(BFS)的统称。我们将通过几个经典例题,学习如何将实际问题抽象为图搜索问题,并分别用DFS和BFS进行求解。
课程简介 📖
上一节我们介绍了图搜索的基本概念,本节中我们来看看如何将其应用于实战。
图的核心是节点和边。节点代表元素,边代表元素之间的关系。在题目中,图通常是隐式的,需要我们自行发现并构建。
图搜索相关的题型主要分为以下几大类:

以下是常见的图搜索题型:
- 求连通分量:相对简单和单一。
- 求任意一组解:与判断是否有解等价。
- 枚举求全部解:找出所有可能的解。
- 求满足特定要求的解:介于求任意解和求全部解之间。推荐在搜索过程中直接维护解的合法性,这比先求全部解再过滤更高效。
例题一:括号生成 🧩
题目:LeetCode 22题。给定数字 N,生成所有由 N 对括号组成的有效组合。
问题分析:这是一个求全部解的问题。关键在于定义“合法”:在任何位置,左括号的数量不能少于右括号的数量。
图的构建:
- 节点:用
(X, Y)表示,其中X是当前左括号数,Y是当前右括号数。为保证合法性,始终有X >= Y。 - 边:从节点
(X, Y)出发,可以添加一个左括号到达(X+1, Y),或者(当X > Y时)添加一个右括号到达(X, Y+1)。 - 目标:寻找所有从起点
(0, 0)到终点(N, N)的路径,每条路径对应一个有效括号序列。
DFS解法

DFS的思路是递归探索,每次在当前部分解的基础上进行修改。
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> answer;
dfs(answer, "", 0, 0, n);
return answer;
}
void dfs(vector<string>& ans, string str, int x, int y, int n) {
if (y == n) { // 找到一组解
ans.push_back(str);
return;
}
if (x < n) { // 可以加左括号
dfs(ans, str + '(', x + 1, y, n);
}
if (x > y) { // 可以加右括号
dfs(ans, str + ')', x, y + 1, n);
}
}
};
BFS解法

BFS需要借助队列,并记录每个状态对应的部分解,空间消耗通常更大。
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> answer;
if (n == 0) {
answer.push_back("");
return answer;
}
// 队列元素:当前左括号数x,右括号数y,部分解str
queue<tuple<int, int, string>> q;
q.push({0, 0, ""});
while (!q.empty()) {
auto [x, y, str] = q.front();
q.pop();
if (x < n) { // 扩展左括号
q.push({x + 1, y, str + '('});
}
if (x > y) { // 扩展右括号
string newStr = str + ')';
if (y + 1 == n) { // 找到一个完整解
answer.push_back(newStr);
} else {
q.push({x, y + 1, newStr});
}
}
}
return answer;
}
};
小结:对于求全部解的问题,DFS代码通常更简洁。BFS需要显式维护队列和状态,代码较长。
例题二:岛屿数量 🏝️
题目:LeetCode 200题。给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿由水平或垂直方向上相邻的陆地连接形成。
问题分析:这是一个经典的求连通分量问题。

图的构建:
- 节点:每个值为
'1'的网格位置(i, j)。 - 边:如果两个
'1'位置在水平或垂直方向相邻,则它们之间有一条边。 - 目标:找出图中连通分量的数量。关键在于标记访问过的节点(判重),避免重复访问。
DFS解法
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty()) return 0;
int m = grid.size(), n = grid[0].size();
int count = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
++count; // 找到一个连通分量
}
}
}
return count;
}
void dfs(vector<vector<char>>& grid, int x, int y) {
// 边界检查及合法性检查
if (x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size() || grid[x][y] != '1') {
return;
}
grid[x][y] = '0'; // 标记为已访问
// 递归搜索四个方向
dfs(grid, x + 1, y);
dfs(grid, x - 1, y);
dfs(grid, x, y + 1);
dfs(grid, x, y - 1);
}
};
BFS解法
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty()) return 0;
int m = grid.size(), n = grid[0].size();
int count = 0;
// 方向数组:上下左右
vector<pair<int, int>> directions = {{1,0}, {-1,0}, {0,1}, {0,-1}};
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
++count;
queue<pair<int, int>> q;
q.push({i, j});
grid[i][j] = '0'; // 标记起点
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (auto& dir : directions) {
int nx = x + dir.first;
int ny = y + dir.second;
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == '1') {
grid[nx][ny] = '0'; // 标记为已访问
q.push({nx, ny});
}
}
}
}
}
}
return count;
}
};

小结:对于连通分量问题,DFS和BFS思想一致,都是从一个起点出发,标记所有能到达的点。DFS实现更简洁。
例题三:单词拆分 🔗
题目:LeetCode 139题。给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判断 s 是否可以被空格拆分为一个或多个字典中的单词。
问题分析:这是一个判断是否有解(或求一组解)的问题。
图的构建:
- 节点:整数
i,表示字符串s的前i个字符(前缀)。 - 边:如果从位置
i开始,截取的一个子串是字典中的单词,那么就有一条从节点i到节点i+单词长度的边。 - 目标:判断是否存在一条从节点
0到节点s.length()的路径。同样需要判重,避免对同一节点重复搜索。
DFS解法
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> dict(wordDict.begin(), wordDict.end());
vector<bool> visited(s.length(), false);
return dfs(s, 0, dict, visited);
}
bool dfs(const string& s, int start, const unordered_set<string>& dict, vector<bool>& visited) {
if (start >= s.length()) return true; // 成功匹配完整个字符串
if (visited[start]) return false; // 该起点已搜索过且失败
visited[start] = true;
for (int end = start + 1; end <= s.length(); ++end) {
string sub = s.substr(start, end - start);
if (dict.find(sub) != dict.end() && dfs(s, end, dict, visited)) {
return true;
}
}
return false;
}
};
BFS解法

class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> dict(wordDict.begin(), wordDict.end());
vector<bool> visited(s.length(), false);
queue<int> q;
q.push(0);
while (!q.empty()) {
int start = q.front();
q.pop();
if (!visited[start]) {
for (int end = start + 1; end <= s.length(); ++end) {
string sub = s.substr(start, end - start);
if (dict.find(sub) != dict.end()) {
if (end == s.length()) return true;
q.push(end);
}
}
visited[start] = true;
}
}
return false;
}
};
动态规划思路

此问题也可用类似动态规划的“打表”思路解决,状态 dp[i] 表示前 i 个字符能否被拆分。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> dict(wordDict.begin(), wordDict.end());
vector<bool> dp(s.length() + 1, false);
dp[0] = true; // 空串可以被拆分
for (int i = 1; i <= s.length(); ++i) {
for (int j = 0; j < i; ++j) {
// 如果前j个字符可拆分,且子串s[j, i)在字典中,则前i个字符可拆分
if (dp[j] && dict.find(s.substr(j, i - j)) != dict.end()) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
};
小结:单词拆分问题展示了图搜索与动态规划之间的联系。DFS/BFS更直观,而DP方法在某些情况下可能更高效。
例题四:组合总和 🎯
题目:LeetCode 39题 & 40题。给定一个无重复元素的整数数组 candidates 和一个目标数 target,找出所有和为目标数的组合。39题中数字可无限重复选取;40题中每个数字在每个组合中只能使用一次。
问题分析:这是求满足条件的所有解的问题,通常使用DFS(回溯)更为方便。
图的构建(以39题为例):
- 节点:当前的和
sum以及当前考虑的数字下标index。 - 边:选择当前数字(
sum增加,index可能不变或变化)或不选择当前数字(index增加)。 - 目标:找到所有从起点
(0, 0)出发,到达sum == target的路径。
解法(LeetCode 39,数字可重复)
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> answer;
vector<int> path;
sort(candidates.begin(), candidates.end()); // 排序便于处理
dfs(candidates, target, 0, 0, path, answer);
return answer;
}
void dfs(vector<int>& nums, int target, int index, int sum,
vector<int>& path, vector<vector<int>>& ans) {
if (sum > target) return; // 剪枝
if (sum == target) {
ans.push_back(path);
return;
}
for (int i = index; i < nums.size(); ++i) {
path.push_back(nums[i]);
dfs(nums, target, i, sum + nums[i], path, ans); // 注意i不变,可重复选取
path.pop_back(); // 回溯
}
}
};
解法(LeetCode 40,数字不可重复)
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> answer;
vector<int> path;
sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0, 0, path, answer);
return answer;
}
void dfs(vector<int>& nums, int target, int index, int sum,
vector<int>& path, vector<vector<int>>& ans) {
if (sum > target) return;
if (sum == target) {
ans.push_back(path);
return;
}
for (int i = index; i < nums.size(); ++i) {
// 关键:跳过同一层中相同的元素,避免重复组合
if (i > index && nums[i] == nums[i-1]) continue;
path.push_back(nums[i]);
dfs(nums, target, i + 1, sum + nums[i], path, ans); // i+1,不可重复选取
path.pop_back();
}
}
};

小结:对于求所有组合的问题,DFS回溯是天然的工具。通过排序和跳过特定条件(如重复元素),可以有效去重和剪枝。
总结与思考 🧠
本节课中我们一起学习了图搜索算法的实战应用。
- 隐式图的构建:关键是识别问题中的“状态”(节点)和“状态转移”(边)。
- 问题类型与算法选择:
- 连通分量 / 判断有解:DFS和BFS均可,实现难度相近。
- 求任意一组解:DFS通常更简单。
- 求全部解 / 满足条件的解:强烈推荐DFS(回溯)。它通过在单个解路径上修改和回溯来探索所有可能,代码简洁。BFS需要存储大量中间状态,且记录路径复杂。
- 求最优解(如最短路径):BFS更具优势,因为它按层搜索,首次到达目标即为最短。
- 核心技巧:
- 状态标记(判重):防止在图中绕圈,对于DFS/BFS都至关重要。
- 剪枝:在搜索过程中提前排除无效分支,提升效率。
- 路径记录:DFS回溯时自然记录;BFS需要额外维护前驱信息。
延伸思考:LeetCode 77/78/90(子集问题)、51/52(N皇后问题)都可以套用DFS回溯的框架。对于组合总和问题,当 target 较小时,也可以用动态规划的思路求解,但记录具体组合方案依然需要类似回溯的方法。


希望本课程能帮助你更好地理解和运用图搜索算法。


七月在线—算法coding公开课 - P7:字符串实战(直播coding) 🧵
在本节课中,我们将学习四个典型的字符串处理问题。我们将从简单的滑动窗口开始,逐步深入到使用栈和动态规划解决更复杂的问题。课程内容旨在帮助初学者理解核心概念,并通过代码示例掌握解题思路。
概述 📋

本节课我们将要学习四个字符串实战问题:
- 无重复字符的最长子串(滑动窗口)
- 去除重复字母(贪心/栈)
- 翻转与交错字符串(栈的扩展应用)
- 最长有效括号(动态规划/BFS)

这些问题难度递增,涵盖了字符串处理中的多种核心技巧。

问题一:无重复字符的最长子串 🪟


上一节我们介绍了课程概述,本节中我们来看看第一个问题:给定一个字符串,返回其不包含重复字符的最长子串的长度。
核心思路是使用滑动窗口(或双指针)技术。我们动态维护一个窗口 [i, j),保证窗口内的字符不重复。当窗口满足条件时,向右移动 j 以扩大窗口;当 j 指向的字符在窗口内已存在时,则向右移动 i 以缩小窗口,直到窗口内再次无重复字符。
以下是实现该算法的关键步骤:

- 使用一个布尔数组
have[256]来记录当前窗口内有哪些字符。 - 初始化
i = 0,j = 0,answer = 0。 - 循环执行以下操作:
- 当
j未越界且s[j]不在当前窗口内时,将s[j]加入窗口(have[s[j]] = true),并更新answer,然后j++。 - 否则,将
s[i]移出窗口(have[s[i]] = false),然后i++。
- 当
- 循环结束后,返回
answer。
int lengthOfLongestSubstring(string s) {
int answer = 0;
bool have[256] = {false};
int i = 0, j = 0;
int n = s.length();
while (true) {
if (j < n && !have[s[j]]) {
have[s[j]] = true;
answer = max(answer, j - i + 1);
j++;
} else {
if (j >= n) break;
while (s[i] != s[j]) {
have[s[i]] = false;
i++;
}
have[s[i]] = false;
i++;
}
}
return answer;
}


问题二:去除重复字母 🔤
上一节我们介绍了滑动窗口,本节中我们来看看一个去重问题:给定一个仅包含小写字母的字符串,去除字符串中的重复字母,使得每个字母只出现一次,并且返回结果的字典序最小。


核心思路是使用栈来构建结果。我们希望栈底(结果字符串的开头)的字母尽可能小。遍历字符串,对于每个字符 c:
- 如果
c已经在栈中,则跳过。 - 如果
c不在栈中,则检查栈顶元素t:- 如果
t > c且t在后续字符串中还会出现,那么弹出t(因为后面还有机会把它加回来,并且用c开头能得到更小的字典序)。 - 重复此过程,直到栈顶元素不满足弹出条件。
- 如果
- 将
c压入栈中。
以下是实现该算法的关键数据结构与步骤:
- 使用一个整型数组
count[26]记录每个字符在剩余字符串中出现的次数。 - 使用一个布尔数组
inStack[26]记录字符是否已在栈中。 - 使用一个栈
stk来构建结果。 - 遍历字符串,对于每个字符
c:- 将
count[c]减1。 - 如果
inStack[c]为真,跳过。 - 否则,当栈非空,且栈顶字符
top > c,且count[top] > 0(后面还有)时,弹出栈顶,并设置inStack[top] = false。 - 将
c压栈,并设置inStack[c] = true。
- 将
- 遍历结束后,将栈中字符从底到顶拼接成字符串返回。
string removeDuplicateLetters(string s) {
vector<int> count(26, 0);
vector<bool> inStack(26, false);
stack<char> stk;
for (char c : s) count[c - 'a']++;
for (char c : s) {
count[c - 'a']--;
if (inStack[c - 'a']) continue;
while (!stk.empty() && stk.top() > c && count[stk.top() - 'a'] > 0) {
inStack[stk.top() - 'a'] = false;
stk.pop();
}
stk.push(c);
inStack[c - 'a'] = true;
}
string ans;
while (!stk.empty()) {
ans = stk.top() + ans;
stk.pop();
}
return ans;
}
问题三:翻转与交错字符串 🔄


上一节我们学习了用栈去除重复字母,本节中我们来看一个类似但更复杂的问题。给定一个字符串 S',已知它是原字符串 S 的翻转串 reverse(S) 和乱序串 shuffle(S) 交错合并(merge)的结果。要求找出字典序最小的原字符串 S。
这个问题可以转化为:将 S' 翻转后,得到的新字符串是 S 和 shuffle(S) 的 merge 结果。因此,我们需要从翻转后的字符串中,为每个字母恰好挑选出一半(因为 S 和 shuffle(S) 各包含一半的字母),并使得挑选出的序列(即 S)字典序最小。
思路与问题二完全一致,只是弹出栈顶的条件需要改变:只有当栈顶字符 t > c,并且剩余字符串中字符 t 的数量加上已挑选的 t 的数量,仍然大于等于我们最终需要挑选的 t 的数量时,才能弹出 t。这保证了弹出后我们仍有足够的机会在后面挑选到所需的 t。
以下是该算法的核心修改部分(假设 need[c] 是字符 c 最终需要的数量,have[c] 是当前已挑选的数量,left[c] 是剩余字符串中字符 c 的数量):

// 弹出栈顶的条件变为:
while (!stk.empty() && stk.top() > c && have[stk.top()] + left[stk.top()] >= need[stk.top()]) {
have[stk.top()]--;
inStack[stk.top()] = false;
stk.pop();
}
问题四:最长有效括号 🧱
上一节我们解决了基于栈的挑选问题,本节我们来看一个动态规划问题:给定一个可能包含无效字符的括号序列,通过删除最少的括号,使其变为有效的括号序列,并返回所有可能的最长有效序列。
我们可以使用动态规划。定义 dp[i][j] 表示考虑原字符串前 i 个字符,且左括号比右括号多 j 个时,能构成的最长有效子序列的长度(j 必须非负)。状态转移需要考虑第 i 个字符是左括号、右括号还是其他字符。
以下是状态转移方程的核心思想:


- 如果
s[i]是'(':我们可以选择不取它,则dp[i][j] = dp[i-1][j];或者选择取它,则dp[i][j] = dp[i-1][j-1] + 1(前提是j>0)。 - 如果
s[i]是')':我们可以选择不取它,则dp[i][j] = dp[i-1][j];或者选择取它,则dp[i][j] = dp[i-1][j+1] + 1。 - 如果
s[i]是其他字符:只能选择取它,则dp[i][j] = dp[i-1][j] + 1。
为了得到所有具体方案,我们可以在 dp 数组中存储集合,而不仅仅是长度。每个状态 (i, j) 对应的集合,存储所有能达到该状态且长度为 dp[i][j] 的字符串。在状态转移时,需要比较不同前驱状态带来的字符串集合,选择最优(更长)的,或者合并长度相等的。
由于空间优化和集合操作较为复杂,此处不展开完整代码,但核心是上述动态规划思想配合集合存储路径。
总结 🎯


本节课中我们一起学习了四个字符串处理问题:
- 滑动窗口用于解决无重复字符最长子串问题,时间复杂度为
O(n)。 - 单调栈用于解决字典序最小的去重问题(问题二)及其变种(问题三),核心在于设定合理的弹出条件。
- 动态规划用于解决最长有效括号序列问题,通过定义状态
dp[i][j]并存储路径集合来找到所有最优解。


字符串题目变化多端,但滑动窗口、栈和动态规划是解决许多中等难度问题的有力工具。希望本课程能帮助你建立解决此类问题的基本思路。

人工智能—Kaggle实战公开课(七月在线出品) - P1:机器学习到底应用在哪些酷炫领域? 🚀

在本节课中,我们将要学习机器学习解决实际问题的完整流程,并了解机器学习在众多领域的广泛应用。课程将首先介绍一个通用的建模流程,然后讲解核心工具的使用,最后概览机器学习在各个行业中的具体应用场景。
课程概述与目标
首先感谢大家参加本次Kaggle比赛实战课程。从今天开始,我们将陆续看到机器学习建模解决各种问题的完整流程。课程将涵盖数据比赛和工业界对应的应用。
本节课作为开篇,旨在介绍机器学习解决实际问题的流程、建模与优化方式,以及可能用到的工具和模板。掌握工具后,后续应用会相对顺畅。
由于不清楚每位同学的背景和对机器学习的熟悉程度,今天的内容对部分同学可能已有所了解,对部分同学可能稍显深入。这没有关系,因为本节课主要目标是带大家熟悉流程和工具。许多内容可能需要课后消化,课上只需建立整体认知即可。
从下节课开始,嘉浩老师和我将带大家探讨具体场景下的机器学习应用和各类Kaggle比赛。希望大家在每节课后,都能在案例中找到与课程对应的知识点。本节课提到的完整工作流程,在后续学习比赛或工业应用时都会遇到。实际应用中,不一定涵盖所有环节,可能在特征选择或模型处理等环节有更深入的工作。
如果大家对今天的内容有任何不明白的地方,可以随时提问。
机器学习应用全流程解析 🔄
本节我们将首先了解使用机器学习解决问题(无论是参加比赛还是解决工业界实际问题)的大致总体流程。我们将根据这个流程,逐一介绍每个环节中可以调用的库和工具。
我们将使用一个在机器学习领域广泛使用的库——Scikit-learn。我们将按照流程,介绍其中涉及数据处理、特征工程、模型选择等环节可调用的类或函数。
流程主要涉及以下五个环节:
- 数据处理
- 特征工程
- 模型选择与超参数优化
- 模型评估与分析
- 模型融合
以下是每个环节的简要说明:
- 数据处理:涉及数据清洗、缺失值处理、数据转换等预处理工作。
- 特征工程:通过创建、转换、选择特征,以更好地表示数据,提升模型性能。
- 模型选择与超参数优化:选择合适的算法模型,并寻找最优的超参数组合。模型性能的差异往往源于超参数的选择,例如决策树的数量、深度等。
- 模型评估与分析:使用合适的评估标准来衡量模型效果的好坏。
- 模型融合:结合多个模型的预测结果,以提升最终表现的常用策略。
我们将顺着这五个环节,带大家了解Scikit-learn这个工具包中哪些部分是可用的。相关链接已在PPT中提供。
理解评估标准:Kaggle Wiki的重要性 📚
接下来,我们将带大家浏览Kaggle的Wiki页面。了解历史比赛情况非常有用。更好的方式是,先了解历史上是否存在与你当前要解决的问题类似的问题。
这有几重含义,其中很重要的一点是:做任何工作都需要一个评判标准。标准的不同会直接影响最终的效果。如果你的优化是针对准确率进行的,但任务的评估标准是F1分数或AUC,那么即使准确率很高,最终排名或分数也可能不理想。
因此,你需要了解问题的评判标准是什么。Kaggle体系中有许多评判标准。例如,对于回归问题,有均方误差等标准;对于分类问题,又有其他不同的标准。这些标准的衡量方式各不相同。
如果不了解评估标准,直接以你认为的分类准确率去优化,结果可能并不理想。例如,一些企业举办的比赛,其评判标准可能与常见标准不同。了解并针对目标评估标准进行优化,才能在该体系下取得好成绩。
核心工具:XGBoost与自定义目标函数 ⚙️
以XGBoost为例,它支持用户自定义目标函数。这意味着你可以根据比赛特定的评估标准来设定优化目标。但自定义目标函数有一些要求,后续课程会详细说明。
了解最终的评判目标,并针对该目标进行优化,是取得好成绩的关键。
案例演示与工业界应用概览 🌐
最后,我们会简要过一下案例。这些案例的目的是展示完整流程,以及Scikit-learn和XGBoost等工具的基本调用方法。案例讲解并非本节课的核心内容。
机器学习在工业界有广泛的应用。本节课将涵盖其中大部分应用领域:
- 经济金融:如股市预测、房价预测等与资金密切相关的领域。
- 能源行业:如电力需求预测、异常用电检测等,传统行业正通过数据驱动方法优化资源配置。
- 互联网应用:
- 自然语言处理:涉及文本分类、排序、主题模型、文本相似度计算等。
- 广告点击率预测:直接影响Google、Facebook及国内BAT等公司营收的核心问题。
- 零售与电商:
- 销量预测:帮助优化库存管理,例如在特定节日预测商品需求。
- 推荐系统:根据用户历史行为进行商品推荐,有效提升转化率和营收。
- 深度学习:目前主要在图像领域落地较多,相关比赛基本由深度学习模型主导。在自然语言处理等领域,深度学习也在快速发展。
- 其他领域:
- 气象预测:如短时精准天气预报应用。
- 社交网络分析:从微博、微信等社交数据中挖掘有价值的信息。
列出这些方向,是为了让大家了解,无论是目前在该领域工作,还是未来希望进入该领域,你所涉及的工作大多可以归入上述应用范畴。我们的课程也是按照这些方向来组织的。

总结
本节课我们一起学习了机器学习解决实际问题的基本流程,认识了Scikit-learn等核心工具,并重点强调了理解问题评估标准的重要性。最后,我们概览了机器学习在经济、能源、互联网、零售、深度学习等多个领域的广泛应用场景。希望大家对机器学习的全貌有了初步的认识,为后续的实战课程打下基础。






人工智能—Kaggle实战公开课(七月在线出品) - P10:征服Kaggle大数据竞赛 🚀

在本节课中,我们将要学习什么是Kaggle大数据竞赛,为什么要参加以及如何参加。我们将通过五个核心主题,系统地了解Kaggle的竞赛模式、数据处理、平台选择、项目流程以及参赛的意义,帮助初学者迈出征服Kaggle的第一步。

什么是Kaggle竞赛?🎯
Kaggle是一个进行模型预测的平台。在其官方网站上,会发布各种各样的问题以及相应的数据集。参赛者可以以个人或团队的形式,利用自己的知识和开发工具来参加比赛。最终的目标是构建一个预测模型来解决提出的问题。
Kaggle也是一个非常有影响力的大型比赛平台,最初专注于数据科学研究。它在全球200多个国家拥有超过60万的数据科学研究者。许多大公司如Google、Facebook、Amazon、Zillow、Lyft和Uber都与Kaggle合作。平台上涌现的优秀算法最终会应用于工业界和学术界。
除了奖金,更多参赛者会利用在Kaggle上取得的成绩为自己的履历增色,从而找到理想的工作。Kaggle平台本身也提供求职渠道。
Kaggle的趣味性在于其“实时排名”系统,它能激发强烈的竞争意识。通过组队合作,可以与众多高手相互讨论,在论坛上学习他们的方案,快速提升自己。它也是一个氛围友好的同行交流平台。对于取得优异成绩(如前三名)的选手,不仅有丰厚的奖金,还有很大机会获得知名公司的工作机会。
Kaggle的官方博客(Kaggle Blog)内容非常丰富,主要包括五类:数据科学新闻、Kaggle相关新闻、如何参赛的教程,以及最重要的——获胜者访谈。Kaggle通常会对每场比赛的前三名进行采访,分享他们的心得体会和解决问题的步骤。大多数获胜者也愿意公开自己的源代码。多研究获胜者的经验能有效提升自身能力。
如何报名与参赛?📝
进入Kaggle官网,选择一个竞赛(例如一个关于Twitter情感信息提取的比赛)。第一步是注册账号,可以使用Google账号或邮箱注册。完成注册后,点击“Join Competition”即可参赛。
之后,你需要进行建模、编写代码、多次调参。完成模型后,提交你的预测结果(Submit Prediction)。提交模型并非一锤子买卖,你可以每天优化模型并重新提交,以刷新实时排名。


排名高的选手(通常是前三名)会获得奖金。例如,Twitter情感分析比赛的总奖金为15,000美元,会按名次分配给前三名。在Kaggle上,这并非最丰厚的奖金,还有更多奖金更高的比赛。
Kaggle的数据与排名机制 📊
在Kaggle比赛中,主要有三种数据:
- 训练集:用于模型训练。
- 测试集:分为公开测试集和私有测试集。
参赛流程通常是:
- 在训练集上构建并训练模型。
- 在公开测试集上进行预测并提交模型,系统会根据此生成一个公开排行榜。
- 比赛结束后,系统会在私有测试集上重新测试所有模型,生成最终排行榜。这个基于私有测试集的排名才是你的最终成绩。
因此,比赛期间的公开排行榜仅供参考,并不代表最终结果。
克服过拟合:交叉验证技巧
在以往经验中,有些队伍在公开排行榜上排名靠前,但最终成绩却不理想。为了克服这种现象,我们主要采用交叉验证的方法。
具体步骤如下:
- 将训练集随机分成K等份(例如3份),每一份称为一个折。
- 每次使用其中K-1份数据训练模型,并在剩余的那1份数据上进行验证。
- 循环这个过程,确保每一份数据都充当过一次验证集。
- 最后,综合所有折上的验证结果来评估模型性能。
核心建议是:请更多相信你自己通过交叉验证得到的模型性能,而不要过度关注公开排行榜的排名。
两个实用小技巧:
- 如果数据集较小,通常越简单的代码效果越好。
- 如果数据集非常大,则应更多关注快速迭代。
一个实际例子是Zillow房地产评估预测比赛。在最终私有排行榜上,有些队伍排名大幅上升(如从第10名升至第1名),而有些则下降(如从第1名跌出奖金区)。这往往是因为后者过度迎合公开测试集,导致了过拟合。因此,相信自己的交叉验证结果至关重要。


参赛工具与平台选择 💻

在Kaggle比赛中,主要使用两种编程语言:Python和R。选择你更熟悉的语言即可,两者没有优劣之分。
Kaggle平台提供了名为Kaggle Kernels的在线编程环境,它基于Jupyter Notebook,并预装了大量的常用库,如NumPy、Pandas、Scikit-learn、OpenCV、PyTorch等,方便用户直接在线编写和测试代码。





你可以在Kernel中创建自己的笔记本,并选择配置(如是否使用GPU/TPU)来加速训练过程。
如何选择合适的比赛类型?🏆
Kaggle的比赛类型主要分为两大类:常见类型和不常见类型。
常见竞赛类型
Featured:
- 特点:专注于机器学习,通常由大公司提出与其业务相关的预测问题,并提供丰厚的奖金(最高可达100万美元)。
- 举例:Allstate保险公司销售预测、Wikipedia负面信息分类、Zillow房屋价格预测。
- 适合人群:所有水平的参赛者,是提升能力、争取高额奖金的好机会。
Research:
- 特点:侧重于学术研究问题,通常是实验性质的。
- 举例:Google地标识别、白细胞识别、Wikipedia大规模文本分类。
- 适合人群:对学术研究感兴趣者,这类比赛通常没有奖金,但能提供解决问题和展示能力的机会。
Getting Started:
- 特点:专门为新手设计,题目长期开放,是很好的学习途径。
- 举例:经典的泰坦尼克号生存预测、房价预测。
- 适合人群:强烈推荐初学者从此类比赛开始,以了解基本套路和建立信心。此类比赛没有奖金,且提交结果只在排行榜上保留两个月。
Playground:
- 特点:趣味性较强,同样适合新手,有些会提供小额奖金或荣誉。
- 举例:猫狗图像分类、树叶分类、纽约出租车路径规划。
- 适合人群:初学者用于练手和娱乐。
不常见竞赛类型
Recruitment:
- 特点:公司为招聘举办的比赛,相当于一次求职筛选。
- 举例:Walmart库存预测招聘、Airbnb新用户订房预测招聘。
Annual:
- 特点:Kaggle每年举办两次的赛事(三月机器学习赛和圣诞优化赛),并非严肃的商业或研究比赛。
Limited:
- 特点:限制参与的比赛,要么完全私有,要么需要收到邀请才能参加。通常是行业大师的专属领域。
比赛题目领域与参赛建议
Kaggle比赛题目主要涵盖四大领域:数据挖掘、计算机视觉、自然语言处理和优化问题。
参赛建议:
- 初学者:从Getting Started或Playground比赛开始。
- 有经验者:挑战Featured或Research比赛以争取奖金或深入研究。
- 领域专注:建议专注于你感兴趣或未来职业发展方向的领域(如计算机视觉)。同类型比赛的解题思路有共通之处,熟能生巧。
- 细分领域:在选定大领域后,可进一步选择细分领域(如计算机视觉下的生物医学图像分析)进行深入挖掘。
- 寻求指导:如果自学规划比赛路径有困难,可以寻找有经验的导师或课程进行系统学习。


解决一个Kaggle项目的通用流程 🔄
本节中,我们来看看如何系统性地解决一个Kaggle项目。通常可以分为以下四个步骤:
第一步:数据分析与预处理
这是至关重要的一步,为后续所有工作打下基础。
- 数据预处理:清洗数据,去除噪声、冗余信息和异常值。对数据进行规范化等操作,使其更规整。
- 数据可视化:使用图表工具来验证预处理效果并理解数据分布。常用工具包括:
- 箱形图:展示数据的最小值、第一四分位数、中位数、第三四分位数和最大值,以及异常值。
- 散点图、直方图等。
- 对于图像数据,可以使用Matplotlib或OpenCV进行可视化。
第二步:特征工程
在理解数据的基础上,进行特征处理与选择。
- 特征处理:根据领域知识创建、转换或组合特征。
- 特征选择:目标是筛选出对预测最有效的特征,这可以降低计算复杂度,并可能提升模型性能。
第三步:建模与验证
这是将想法付诸实践的核心环节。
- 建立基线模型:首先构建一个简单的模型,让流程先运行起来。
- 交叉验证:使用第一步中提到的交叉验证方法,在训练集上评估模型性能。
- 理解评估标准:明确比赛使用的评估指标(如准确率、均方误差等)。
- 调参优化:调整模型参数以提升性能。
- 尝试不同模型:不要局限于最初的想法,多尝试不同的算法和模型架构,可能会有意外收获。
- 社区交流:多参与Kaggle论坛的讨论,学习他人的思路和技巧。
再次强调:更多地相信你自己交叉验证的结果,而非公开排行榜的暂时排名。
第四步:提交与迭代
提交不是终点,而是迭代循环的一部分。
- 多次提交:Kaggle允许每天多次提交。你可以根据每次提交后的排名反馈,不断优化模型。
- 循环改进:根据与排行榜其他模型的对比,返回去修改模型,再次提交。
- 运气:有时,好运气也是实力的一部分。
参加Kaggle比赛的意义与影响 🌟
聊完技术流程,我们最后来看看参加Kaggle比赛的深远影响。


- 赢得奖金:比赛奖金通常以美元结算,是一笔实实在在的丰厚奖励。
- 提升能力:实战是提升编程、建模和解决问题能力的最佳途径。
- 助力求职:一份优秀的Kaggle比赛成绩是求职时极具分量的筹码,能显著增加获得心仪工作的机会。
- 拓展人脉:在Kaggle上,你可以结识众多志同道合的高手,进入专业圈子,这对个人成长和职业发展都大有裨益。
- 建立个人品牌:通过比赛获得好成绩,能够提升你在领域内的知名度。无论是在求职、申请研究生还是与业界交流时,一个响亮的Kaggle排名都是强有力的背书。




本节课中,我们一起学习了Kaggle竞赛的基本全貌:从平台介绍、报名方式、数据排名机制,到比赛类型选择、项目实战流程,以及参赛的多元价值。希望这份指南能帮助你自信地开启你的Kaggle征服之旅。记住,从简单的比赛开始,不断学习,持续迭代,你也能在数据科学的舞台上取得亮眼的成绩。

人工智能—Kaggle实战公开课(七月在线出品) - P2:零基础入门Kaggle竞赛 🚀

概述


在本节课中,我们将要学习什么是数据竞赛,特别是Kaggle竞赛。我们将介绍Kaggle平台的常见赛题类型、完整的解题流程与技巧,并通过具体案例展示如何解决不同类型的竞赛问题。课程最后,我们将探讨如何在一周内快速上手Kaggle,并为你提供后续的学习路径建议。
第一部分:数据竞赛介绍 🏆
上一节我们概述了课程内容,本节中我们来看看什么是数据竞赛。
什么是数据竞赛
数据竞赛是以工业或学术问题为导向,聚合广泛跨学科人才参与,利用算法模型探索解决方案的研发模式。
数据竞赛有两个核心要点:
- 它是一种众包的竞赛方式,对参赛人员几乎没有门槛限制。
- 每一场竞赛都有明确的业务背景和特定的待解决问题。
数据竞赛与传统的英语、作文或数学建模竞赛不同。它以具体数据为比赛内容,采用定量的评分机制(例如,通过提交预测文件计算与真实结果的差异得分),并且支持交互式的多次提交与实时反馈。

数据竞赛的意义与发展

数据竞赛与大数据时代紧密相连。一个著名的例子是ImageNet数据集竞赛。该竞赛任务是对图片进行1000个类别的分类,训练集包含1300万张图片。从2013年到2017年,参赛模型的分类精度从71%提升至97%以上。
公式示例:模型精度提升
模型精度 = (正确预测的样本数 / 总样本数) * 100%
ImageNet竞赛成为了深度学习时代的引爆点,并推动了计算机视觉乃至整个AI领域的发展。如今,像ImageNet、LFW(人脸识别)、COCO(物体检测)这样的权威数据集,已成为相关领域算法验证的统一标准。
Kaggle平台简介

Kaggle是全球最大的数据科学竞赛平台,每年举办数十场竞赛,以算法赛和可视化比赛为主。它在2018年被谷歌收购,拥有完善的平台机制,涵盖赛题介绍、数据分析、评分排名到最终方案分享的全流程。
一个典型的Kaggle比赛页面包含以下部分:
- Overview: 赛题背景与评测信息。
- Data: 比赛数据集。
- Notebooks: 在线编程环境(支持CPU/GPU)。
- Discussion: 参赛者讨论区。
- Leaderboard: 实时排名榜。


第二部分:Kaggle常见赛题类型 📊
了解了数据竞赛的基本概念后,本节中我们来看看Kaggle平台上都有哪些类型的比赛。


Kaggle赛题可以根据数据形态进行分类,主要分为以下几类:
以下是常见的赛题数据分类及其占比(基于历史数据):
- 结构化数据挖掘(占比约58%):数据以表格形式存在,例如CSV文件。
- 图像赛题(占比约28%):处理图片数据,如图像分类、物体检测。
- 文本赛题:处理自然语言文本,如情感分析、文本分类。
- 语音赛题:处理音频数据,如语音识别、音频分类。

此外,Kaggle平台也根据比赛目的和难度进行了划分:
- Getting Started: 入门级比赛,难度最低。
- Playground: 练习级比赛,难度简单。
- Featured / Research: 正式或研究型比赛,难度较高。
- Recruitment / Annual / Invitational / InClass: 招聘赛、年度赛、邀请赛、课堂赛等特殊类型。

比赛还可以按阶段分为单阶段赛、两阶段赛和代码竞赛。代码竞赛要求所有提交必须通过Kaggle Notebook完成,无法本地提交结果。
第三部分:Kaggle解题流程与技巧 🛠️
认识了赛题类型,本节中我们来看看解决一个Kaggle比赛的具体流程是什么。
我们以一场正在进行中的入门赛 “Real or Not? NLP with Disaster Tweets” 为例。该赛题是一个文本二分类任务,目标是判断一条推特推文是否是关于真实灾难的新闻。
标准解题流程
当确定要参加一个比赛后,通常遵循以下步骤:
赛题理解
- 阅读赛题背景,明确需要解决的任务。
- 了解评分方法和比赛时间线。
数据分析
- 探索数据分布规律,理解每个字段的含义。
- 结合业务背景加深对数据的理解。
- 进行必要的预处理,如清理表情符号、特殊字符等。
特征工程
- 特征转换:将原始数据转换为模型可识别的格式。
- 特征构建:从现有特征中衍生出新特征。
- 特征选择:筛选出对模型预测最有用的特征。
模型构建
- 选择适合的模型(如逻辑回归、树模型、神经网络)。
- 进行模型的训练、验证和调参。
结果提交与迭代
- 使用模型对测试集进行预测并提交结果。
- 根据得分反馈,可能需要进行多模型集成。
代码示例:使用Scikit-learn进行逻辑回归训练
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer
# 1. 特征转换:将文本转换为TF-IDF向量
vectorizer = TfidfVectorizer()
X_train = vectorizer.fit_transform(train_texts)
# 2. 模型训练
model = LogisticRegression()
model.fit(X_train, train_labels)
# 3. 预测
X_test = vectorizer.transform(test_texts)
predictions = model.predict(X_test)

需要强调的是,这五个步骤并非线性,而是一个需要反复迭代的过程。如果提交后得分不理想,需要回溯检查数据分析是否到位、特征工程是否有效、模型参数是否需要调整。

针对本NLP赛题的解决思路
对于这场推特文本分类比赛,一个可能的模型迭代路径如下:
- 基线模型:使用 TF-IDF 提取文本特征,然后用 SVM 进行分类。
- 进阶模型1:使用 FastText 训练词向量,再输入分类器。
- 进阶模型2:使用 Word2Vec/GloVe 词向量,结合 TextCNN 等神经网络。
- 进阶模型3:使用预训练的 BERT 等Transformer模型进行微调。
- 集成模型:将BERT模型的输出与其他统计特征结合,使用树模型进行集成预测。
这个迭代过程体现了从简单到复杂、不断优化精度的竞赛思路。对于非结构化数据(如图像、文本),深度学习模型因其强大的表征能力,有时可以减少对复杂特征工程的依赖。

第四部分:Kaggle比赛案例串讲 📈
上一节我们以文本赛题为例讲解了流程,本节中我们通过更多案例来看看其他类型赛题如何解决。
案例一:图像分类赛 - Planet: Understanding the Amazon from Space
- 背景:根据卫星图像,判断地表天气和土地覆盖类型(如雨林、河流)。
- 任务:多标签图像分类。
- 思路:典型的图像分类问题,可使用预训练的CNN模型(如ResNet, DenseNet)进行微调。
- 难点:
- 类别分布极度不均衡。
- 图像受天气(如雾)影响大,可考虑图像去雾或数据增强。
案例二:语音分类赛 - Freesound Audio Tagging
- 背景:对音频片段进行多标签分类。
- 任务:音频分类。
- 思路:提取音频特征(如梅尔频谱图),使用深度学习模型(如CNN)进行分类。
- 难点:
- 标签不均衡。
- 是代码竞赛,必须在Kaggle Notebook中完成。
- 可使用 Mixup 数据增强技术,在数值空间混合两个音频样本以生成新样本,有效处理不均衡问题。
案例三:结构化数据赛 - Two Sigma Connect: Rental Listing Inquiries
- 背景:根据纽约租房信息,预测房源的热度(受欢迎程度)。
- 任务:结构化数据分类/回归。
- 思路:对房屋属性(卧室数、价格、位置等)、经纪人信息进行多维度统计和聚合,构建特征,使用树模型或梯度提升模型。
- 难点:
- 数据虽为表格,但字段类型多样,包含数值、类别、文本、日期甚至图片,需要综合运用多种数据处理技能。
- 特征工程是关键,需要大量且有效的统计特征。
通过这些案例可以看出,赛题的数据类型决定了特征工程和模型的选择方向。图像赛题常用CNN,文本赛题常用词向量+RNN/Transformer,结构化赛题则依赖特征工程+树模型。


第五部分:如何一周快速上手Kaggle 🚀
学习了这么多赛题和案例,本节中我们来看看如何高效地开始你的Kaggle之旅。

学习建议
如果你有以下动机,Kaggle是绝佳的学习平台:
- 想用机器学习/深度学习解决实际问题。
- 想将算法模型应用于具体场景。
- 想学习Python数据科学库(如Pandas, Scikit-learn, PyTorch/TensorFlow)。

我们推荐两种学习模式:
- 模式一(理论优先):先系统学习Python、机器学习、深度学习理论,再参加比赛。
- 模式二(实践驱动):直接选择一场入门赛,边做边学,遇到问题再针对性学习。
强烈推荐模式二。这种方式目标明确,学习的知识点能立即应用,能快速提升动手能力,入门门槛更低。通过一个完整的比赛项目,你可以在短时间内串联起数据读取、分析、建模、调参的全流程。


课程推荐

为了帮助你更系统地入门和进阶,我们七月在线提供了两门相关课程:


- 《从头到尾带打Kaggle比赛》
- 特点:零基础入门,详细讲解一个完整比赛(如Two Sigma租房预测)。
- 内容:从Python库、数据分析、特征工程讲到模型训练与提交。
- 周期:6次直播,约3周。

- 《Kaggle实战项目班》
- 特点:进阶实战,覆盖多类型赛题。
- 内容:讲解9-10个不同比赛案例,包括结构化、图像、文本赛题,分析优胜方案思路。
- 周期:12次直播,约4-6周。

你可以根据自身基础和学习目标选择合适的课程。





总结
本节课中我们一起学习了数据竞赛和Kaggle平台的方方面面。我们从数据竞赛的定义和意义讲起,介绍了Kaggle丰富的赛题类型。然后,我们深入剖析了一个标准化的Kaggle解题流程,并通过文本、图像、语音和结构化数据等多个生动案例,展示了不同赛题的解决思路与挑战。最后,我们为你提供了快速上手Kaggle的实践性建议和后续学习路径。



记住,学习Kaggle最好的方式就是动手实践。选择一场感兴趣的入门赛,勇敢地迈出第一步,在解决实际问题的过程中,你的数据科学能力将得到飞速成长。祝你竞赛顺利!


人工智能—Kaggle实战公开课(七月在线出品) - P3:Kaggle Expedia酒店推荐比赛 🏨


在本节课中,我们将学习如何解决Kaggle上的Expedia酒店推荐比赛。这是一个典型的推荐系统问题,我们将从理解问题背景开始,逐步进行数据探索、特征工程、模型构建与融合,最终生成可提交的预测结果。整个过程将涉及多个模型和技巧,旨在为大家展示一个完整的数据科学竞赛解决方案。
问题背景与设定
本节我们将介绍本次比赛的具体任务、数据概况以及评估标准。
Expedia是一个在线旅游服务平台,类似于国内的携程。当用户计划旅行时,需要在海量酒店中做出选择,这通常是一个耗时且困难的过程。为了帮助用户简化决策,Expedia希望利用历史数据构建一个推荐系统。

然而,直接推荐具体的酒店非常困难,因为用户的目的地信息通常只到城市级别。因此,比赛方将所有的酒店划分成了100个簇(cluster)。这些簇是根据酒店的历史价格、用户评分、地理位置(如相对于城市中心的位置)等多种属性划分的。例如,经济型连锁酒店可能被分到同一个簇中。


我们的任务不再是预测具体的酒店ID,而是预测用户最有可能预订的酒店簇(cluster)。比赛提供了从2013年到2015年的用户行为数据,其中2013-2014年的数据作为训练集,2015年的数据作为测试集。

需要注意的是,训练集是从全量数据中随机采样得到的子集,这意味着其数据分布可能与全量数据不完全一致。因此,在建模时需要警惕过拟合(overfitting)问题。

数据文件说明
比赛提供了多个数据文件:
- train.csv: 训练数据集,包含用户历史行为记录。
- test.csv: 测试数据集,我们需要为其预测酒店簇。
- destinations.csv: 包含酒店相关属性(features)的文件。出于隐私保护,这些属性已被哈希处理(hash),我们得到的是匿名化的特征(D1-D149列),而非明文信息。
- sample_submission.csv: 提交结果的格式示例。


评估标准
比赛采用 MAP@5 (Mean Average Precision at 5)作为评估指标。其含义是:系统为用户推荐5个酒店簇,只要这5个推荐中包含用户实际预订的簇,即视为本次推荐成功。这模拟了现实场景中,用户通常只浏览搜索结果第一页(例如显示5个结果)的行为。我们的目标是尽可能提高这个指标。

提交文件需要为测试集中的每个user_id和srch_id组合,预测5个酒店簇ID,格式如下:
id, hotel_cluster
1, 99 31 75 20 1
2, 65 12 98 46 77
...

整体解决方案图示
上一节我们介绍了问题的背景,本节我们来看看解决这个问题的整体思路和流程。
我们将构建一个包含多个模型的混合(Blending)解决方案。核心流程如下图所示:

整个流程可以概括为以下步骤:
- 数据准备与探索:加载并理解训练集和测试集。
- 利用数据泄露(Data Leakage):我们发现部分测试集数据在训练集中有完全匹配的记录。对于这部分数据(约占总测试集的1/3),我们可以直接“查表”得到准确答案。
- 多模型训练:对剩余的数据,我们将使用四类不同的模型进行训练和预测:
- 随机森林(Random Forest)
- 梯度提升决策树(GBDT)
- 随机梯度下降分类器(SGDClassifier)
- 伯努利朴素贝叶斯(Bernoulli Naive Bayes)
- 模型混合(Model Blending):将上述四个模型的预测结果进行加权平均,得到最终的预测概率。
- 结果合并与提交:将“数据泄露”部分的直接答案与模型混合的预测结果合并,整理成比赛要求的提交格式。
接下来,我们将逐一深入每个环节。
核心步骤详解
1. 处理数据泄露(Data Leakage)
在数据竞赛中,有时测试集数据会在训练集中“泄露”出来,即存在完全相同的记录。直接利用这部分信息可以显著提升分数。我们的分析发现,本次比赛约有1/3的测试数据存在这种情况。
以下是处理此问题的核心思路:
- 比对测试集与训练集,找出完全匹配的记录(基于用户、搜索、酒店等关键字段)。
- 对于这些匹配的记录,直接使用训练集中对应的
hotel_cluster作为预测结果。 - 这部分“标准答案”将作为最终提交结果的一部分。
2. 多模型构建与训练
对于不存在数据泄露的测试数据,我们需要依靠模型进行预测。我们选择了四种各具特色的模型,并对数据和特征进行了差异化处理以适应不同模型的特点。
模型一:随机森林(Random Forest)
随机森林是一种基于Bagging思想的集成学习模型,通过构建多棵决策树并综合其结果来提高泛化能力。
关键处理:
- 数据使用:仅使用训练集中
is_booking标签为1(即实际发生预订)的记录进行训练。这基于一个假设:预订行为比点击行为更能反映用户的真实偏好。 - 大数据处理:由于数据量巨大(数千万条),我们使用Pandas的
chunksize参数分块读取数据(例如每次20万条),以避免内存溢出。
# 示例:分块读取数据
chunk_size = 200000
for chunk in pd.read_csv(‘train.csv‘, chunksize=chunk_size):
# 处理每个数据块
process(chunk)
- 特征:包含了哈希处理后的目的地特征(
destinations.csv中的D1-D149列)。树模型能够较好地处理这类数值型特征。
模型二:梯度提升决策树(GBDT)
GBDT是一种基于Boosting思想的集成学习模型,通过串行地训练一系列弱学习器(通常是决策树),每一棵树都致力于纠正前一棵树的错误。
关键处理:
- 与随机森林类似,仅使用预订数据进行训练。
- 同样采用分块读取策略处理大数据。
- 使用包含目的地哈希特征的完整特征集。
模型三:随机梯度下降分类器(SGDClassifier)
SGDClassifier是一个使用随机梯度下降算法进行优化的线性分类器,适合在线学习或处理大规模数据。
关键处理:
- 数据使用:使用全部训练数据(包括点击和预订记录)。因为线性模型可能从更广泛的行为模式中学习。
- 特征工程:不直接使用哈希化的目的地特征(D1-D149)。因为原始的哈希值作为类别型变量,如果不进行适当的编码(如One-Hot),不适合直接输入线性模型。我们主要使用其他结构化特征。
- 模型支持
partial_fit方法,可以分批次训练,非常适合处理无法一次性装入内存的大数据。

模型四:伯努利朴素贝叶斯(Bernoulli Naive Bayes)
朴素贝叶斯是一种基于贝叶斯定理的简单概率分类器,它假设特征之间相互独立。伯努利版本适用于二值化特征。

关键处理:
- 数据使用:使用全部训练数据。
- 特征工程:与SGDClassifier类似,不使用目的地哈希特征。需要对特征进行二值化处理,以符合伯努利分布的要求。
3. 模型混合(Model Blending)
上一节我们介绍了四个独立的模型,本节我们来看看如何将它们的结果有效地结合起来。我们采用模型混合(Blending) 而非更复杂的堆叠(Stacking)方法,以降低过拟合风险。
混合的基本思想是对不同模型的预测结果进行加权平均。例如:
最终预测概率 = w1 * RF预测概率 + w2 * GBDT预测概率 + w3 * SGD预测概率 + w4 * NB预测概率
其中,权重w1, w2, w3, w4可以通过在验证集上的表现来调整,也可以简单设为平均值。我们根据各个模型在本地验证集上的MAP@5得分来分配权重,表现更好的模型获得更高的权重。



4. 生成最终提交结果

这是流程的最后一步。我们将两部分预测结果整合:
- 直接答案:从数据泄露中获得的、约占总测试集1/3的准确预测。
- 模型预测:对剩余2/3的测试数据,使用四个模型混合后得到的预测结果(取概率最高的5个簇)。
将这两部分按测试集原始顺序合并,就得到了完整的hotel_cluster推荐列表,将其格式化为与sample_submission.csv相同的结构,即可提交至Kaggle平台。

总结与要点回顾

本节课中,我们一起学习了Kaggle Expedia酒店推荐比赛的完整解决方案。我们来回顾一下核心要点:





- 问题转化:比赛将复杂的酒店推荐问题简化为预测100个酒店簇的任务,并使用MAP@5作为评估指标。
- 数据洞察:我们发现了数据泄露现象,并巧妙地利用这部分“标准答案”来提升成绩。
- 大数据处理:面对数千万级别的数据,我们采用了分块读取(chunksize) 和在线学习(partial_fit) 等技术来克服内存限制。
- 差异化建模:我们根据模型特性,对数据和特征进行了差异化处理:
- 树模型(RF, GBDT):使用预订数据和目的地哈希特征。
- 线性/概率模型(SGD, NB):使用全量数据,并避免使用原始哈希特征。
- 模型融合:通过加权混合(Blending) 的方式,融合了随机森林、GBDT、SGDClassifier和朴素贝叶斯四个模型的优势,得到了更鲁棒的预测结果。
- 完整流程:我们实践了从数据探索、特征工程、模型训练与调优、模型融合到结果提交的完整数据科学竞赛流程。



这个案例表明,一个成功的竞赛解决方案往往不是单一模型的胜利,而是对数据的深刻理解、合理的特征工程、恰当的模型选择以及有效的模型融合策略共同作用的结果。希望本教程能帮助你更好地理解如何系统性地解决一个真实的数据科学问题。
人工智能—Kaggle实战公开课(七月在线出品) - P4:Kaggle IEEE-CIS 欺诈检测二分类大赛思路分享 🎯
概述
在本节课中,我们将要学习如何应对一个典型的二分类问题——Kaggle IEEE-CIS 欺诈检测大赛。这个比赛的数据集来自一家银行,主要包含信用卡交易数据,好坏用户的比例大约是29:1。我们将从数据探索、性能优化、建立基线模型、分析模型问题,到实施特征工程,系统地分享整个比赛的解决思路和实战技巧。
比赛背景与数据介绍
上一节我们介绍了课程概述,本节中我们来看看比赛的具体背景和数据情况。
这个比赛的数据结构相对简单,主要包含两张表:
- 主表(交易表):存放用户的交易卡信息。
- 附表(身份表):存放用户使用设备的相关信息。
比赛的一个优点是,官方在讨论区对所有字段的含义进行了说明。例如:
TransactionDT:日期时间。TransactionAMT:交易金额。ProductCD:交易产品类型的编码。card1到card6:用户的支付卡相关信息。P_emaildomain和R_emaildomain:卡使用者的邮件信息。addr1和addr2:支付卡对应的地址。
整体解题思路
了解了数据背景后,本节中我们来看看解决这类比赛的一般性流程化思路。
以下是打比赛的常见步骤:
- 理解比赛:阅读比赛背景介绍和评分方式。本比赛字段含义大部分公开,属于“半匿名”比赛。
- 数据探索(EDA):通过可视化和计算统计值来熟悉数据,寻找异常。例如,查看类别特征的分布、训练集与测试集的差异等。
- 性能优化:对Pandas内存占用和代码执行速度进行优化,以节省时间尝试更多方案。
- 建立基线模型:在完成初步特征工程后,建立一个最基本的模型作为后续改进的基准。
- 确定交叉验证策略:对于本比赛这种有时间序列特性的问题,需要谨慎选择CV策略(如
KFold比StratifiedKFold更合适)。 - 针对性特征工程:根据模型表现,进行特征衍生、转换或选择。
- 模型集成:在特征工程难以突破时,考虑使用集成或堆叠方法上分。
数据探索分析实战
上一节我们介绍了整体思路,本节中我们进入实战,看看如何进行具体的数据探索分析。
首先读取数据并查看概况。
import pandas as pd
train_transaction = pd.read_csv('train_transaction.csv')
train_identity = pd.read_csv('train_identity.csv')
需要注意,CSV中的一些类别特征(如card1, card2)是以整数或浮点数形式存储的,需要手动标注出来,否则模型可能将其误判为连续特征。
由于只有两张表,这里直接将副表合并到主表。一个小技巧是在副表中添加一个had_id字段,用以标识该用户是否在副表中出现。
train_identity['had_id'] = 1
data = train_transaction.merge(train_identity, on='TransactionID', how='left')
data['had_id'].fillna(0, inplace=True)


然后进行基本的统计描述。由于字段众多,建议将结果保存到本地文件查看。接着是可视化工
作,以下是对几个关键特征的探索:
TransactionDT:可视化发现它是一个表示时间的连续特征。但训练集和测试集的数据范围完全不同,因此这个特征无法直接用于建模,后续需要排除。TransactionAMT:通过箱线图发现训练集和测试集分布有差异。对比正负样本发现,正常用户中有大额交易,而所有欺诈用户都倾向于小额交易。这个特征具有区分度。ProductCD:这是一个低基数类别特征,只有五个类别。可视化显示其在好坏客户间的分布差异较大,是一个重要特征。card1-card6:这些是高基数类别特征。例如card1有13553个不同类别。需要检查训练集和测试集中类别出现的差异。card4,card6:这些特征包含文本信息(如visa,mastercard),可以结合先验知识进行挖掘(例如,不同类型卡的违约率可能不同)。P_emaildomain,R_emaildomain:包含丰富文本信息的类别特征。可以进行合并处理(如gmail.com和gmail合并),或衍生新特征(如“是否使用匿名邮箱”)。C系列特征:技术类特征,大部分值集中在某个区间。D系列特征:时间间隔类特征,在不同样本类型上分布差异明显。M系列特征:布尔型特征(T/F),只有两类,无需复杂处理。V系列特征:数量众多,高度冗余,后续需要做相关性分析。ID系列特征:ID_01到ID_11是连续型,ID_12到ID_38是类别型。对于高基数类别特征,需要进行编码处理。
相关性分析与特征处理


完成了初步的数据探索,本节中我们来看看如何处理特征间的相关性,并优化内存。
首先,对于V特征,官方说明它们是已知特征的衍生,可能冗余。因此可以进行严格的相关性分析并删除高相关特征(阈值设为0.75),这不仅能节约内存,还能提高模型泛化性能。
注意:计算相关性前,必须按缺失值情况对特征分组。因为pandas.corr()只计算两个特征均非缺失的样本,如果公共样本很少,计算结果会失真。
以下是分组和删除高相关特征的示例代码:
# 根据缺失值情况分组
def get_missing_groups(df):
missing_stats = df.isnull().sum()
groups = {}
for col in df.columns:
missing_count = missing_stats[col]
if missing_count not in groups:
groups[missing_count] = []
groups[missing_count].append(col)
return groups
# 计算组内相关性并删除高相关特征
def remove_high_corr_in_group(df, group_cols, threshold=0.75):
corr_matrix = df[group_cols].corr(method='spearman').abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
to_drop = [column for column in upper.columns if any(upper[column] > threshold)]
# 删除时,保留取值数量更多的特征
for col in to_drop:
if df[col].nunique() < df[其他高相关特征].nunique(): # 伪代码,需具体实现
df.drop(col, axis=1, inplace=True)
return df
对于其他特征(如C特征),不建议使用如此严格的阈值,通常只删除相关性为0.99或1的完全冗余特征。
建立基线模型与交叉验证策略
处理完相关性后,本节中我们来建立基线模型并确定合适的交叉验证策略。
首先,固定所有随机种子,确保结果可复现。
import numpy as np, random, os
def seed_everything(seed=42):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
seed_everything()
然后进行内存优化,将类别特征进行标签编码,并使用工具函数降低数值型特征的内存占用。
# 标签编码
from sklearn.preprocessing import LabelEncoder
for col in categorical_cols:
data[col] = data[col].fillna(-1)
le = LabelEncoder()
data[col] = le.fit_transform(data[col].astype(str))
# 内存优化函数
def reduce_mem_usage(df):
# ... 具体实现:根据数值范围调整数据类型
return df
data = reduce_mem_usage(data)
接下来确定交叉验证策略。由于本比赛数据具有时间性,测试了多种CV方式:
StratifiedKFold:可能导致时间泄露,线上分数较低。KFold:线上分数相对较高。GroupKFold:分数与KFold接近。TimeSeriesSplit:线上分数很差。
因此,后续选择KFold作为CV策略。建立初始的LightGBM基线模型后,发现线下AUC约为0.92,线上约为0.93,表明线下验证与线上得分差异不大,CV策略合理。
模型分析与过拟合问题诊断
建立了基线模型后,本节中我们深入分析模型存在的问题。
查看训练日志发现,模型过拟合严重:训练集AUC接近1,但验证集AUC只有0.9左右,泛化误差高达7个点。尝试调整参数(如降低num_leaves、增加min_child_samples)来增强模型约束,但效果有限。泛化误差很难单纯通过调参从7个点缩小到2-3个点。
因此,需要从其他角度解决过拟合问题。过拟合的本质是训练集和测试集的数据分布存在差异。针对表格数据,主要从特征工程的角度入手。
这里引入对抗性验证来检测特征分布偏移:
- 将训练集标签设为1,测试集标签设为0。
- 训练一个分类器(如LightGBM)来区分样本来自训练集还是测试集。
- 如果AUC很高(例如>0.6),说明两组数据分布不同。通过特征重要性找出导致差异的特征。
首次对抗性验证发现,只有TransactionDT的特征重要性极高,其他都为0。这与EDA结论一致,TransactionDT是一个偏移严重的特征。删除它后再次进行对抗性验证,其他偏移特征(如ID_31, card1, D15)的重要性才显现出来。


特征偏移的检测与处理
上一节我们使用对抗性验证找到了偏移特征,本节中我们介绍另一种更精细的偏移检测方法——“Quasi”验证法,并讨论如何处理这些偏移特征。
“Quasi”验证法(源自冠军解决方案)思路如下:
- 单独抽取每一个特征。
- 仅用该特征训练一个模型并进行交叉验证。
- 计算该特征在训练折和验证折上的平均AUC差值(
delta_AUC = train_AUC - val_AUC)。
delta_AUC越大,说明该特征带来的泛化误差可能越大。
通过这种方法,可以更细致地评估每个特征的偏移程度。排名靠前的偏移特征大多是类别特征(如card1, addr1)和TransactionAMT。
处理特征偏移的思路:
- 类别特征:
- 直接删除:可能损失信息(如删除
card1会导致分数下降)。 - 转换为连续值:在本比赛中,将类别特征进行标签编码后当作连续值使用,效果很好,显著降低了泛化误差。
- 特征编码/嵌入:如频率编码、目标编码。
- 直接删除:可能损失信息(如删除
- 连续特征:
- 对数变换/分箱:尝试对
TransactionAMT做对数变换,但效果不明显。
- 对数变换/分箱:尝试对


实践尝试:
- 将类别特征转为连续值,并删除
Quasi验证中val_AUC低于0.5的“无用”特征。结果:线上分数提升约0.0002。 - 仅将类别特征转为连续值,不删除任何特征。结果:线上分数提升约0.0011,效果更好。这是因为转换后,虽然某些特征(如
card1)的区分度(train_AUC)略有下降,但其泛化误差(delta_AUC)大幅降低,整体收益为正。




特征工程实战
解决了特征偏移问题,模型分数有了初步提升。本节中我们开始进行系统的特征工程来进一步提高性能。
特征工程遵循“衍生-评估”的迭代流程。首先从最重要的特征入手(参考基线模型的特征重要性)。
1. 重要类别特征的编码
对特征重要性排名靠前的类别特征(如card1, addr1)进行频率编码。
for col in ['card1', 'addr1']:
freq_enc = data[col].value_counts(normalize=True)
data[col + '_freq'] = data[col].map(freq_enc)
衍生新特征后,需进行相关性分析,删除与原始特征或其他新特征高相关的特征。然后将保留的新特征加入数据集,运行交叉验证评估效果。频率编码带来了显著的分数提升。
2. 类别特征之间的组合
对重要的类别特征进行两两组合和三三组合,生成新的交叉特征。这会产生大量新特征。
同样需要进行相关性分析,并通过网格搜索确定最佳的相关性删除阈值(例如,本案例中阈值设为0.989时效果最好)。组合特征进一步提升了模型性能。
3. 类别特征与连续特征的聚合
对重要的类别特征和连续特征进行groupby聚合操作(如求均值、标准差、中位数等)。例如,card1和TransactionAMT的聚合特征,逻辑上可以理解为“每种卡类型的平均消费金额”,这类特征往往包含有价值的信息。
聚合会产生大量特征,需要处理常值特征和缺失值,并进行严格的相关性分析。通过测试不同相关性阈值对分数的影响,找到最优阈值。
4. 其他技巧
- 用负值填充缺失值:对于树模型,用-999等负值填充缺失值,可能让模型在分裂时考虑缺失样本,有时能缓解过拟合。但本案例中尝试后分数下降。
- 挖掘“Magic Feature”:冠军方案中通过深入的业务理解,构建了“用户唯一标识(UID)”特征(结合
card1,addr1, 交易开始时间等),这是一个强特征,带来了巨大的分数提升。这依赖于对问题的深刻洞察,而非通用套路。 - 交易金额小数部分:有方案将
TransactionAMT的小数部分单独作为一个特征,认为其可能代表不同的交易地区。
完成几轮特征工程后,再进行一次超参数调优(使用贝叶斯优化),分数还能得到小幅提升。


关键问题总结与优化技巧
我们一起走完了从数据探索到特征工程的完整流程。本节中我们对课程中的关键问题和实用技巧进行总结。
一、 类别不平衡问题
本比赛正负样本比为29:1,但排名靠前的方案均未使用重采样等方法。因为类别不平衡的本质问题往往不是比例悬殊,而是类间重叠、小类样本子分布复杂等。当少数类样本绝对数量足够时(本例中欺诈样本不少),模型可以学习到其模式,无需特殊处理。
二、 特征偏移问题
这是导致过拟合的核心。对抗性验证和“Quasi”验证是有效的检测工具。处理方法因特征类型而异,没有普适方案,需要结合业务理解进行尝试。
三、 性能优化技巧
- 内存优化:使用
reduce_mem_usage类函数优化数据类型。 - 代码提速:使用
numba加速关键计算(如AUC),使用多进程并行。 - 提前剪枝:在特征工程前,删除常值特征、ID类特征等。
四、 特征选择方法
- 过滤式:如相关性分析。缺点是指标与模型目标可能脱节。
- 包裹式:如递归特征消除(RFE)。效果较好但计算成本高。
- 嵌入式:如模型的特征重要性。但传统
feature_importance有缺陷:1) 强特征会掩盖其他特征;2) 只反映对训练集拟合的贡献,不反映泛化能力。 - 改进方法:
permutation importance(打乱特征值看分数下降)、shap value能更好地评估特征对模型预测的真实贡献。
五、 实用工具与模板
- 固定随机种子:确保实验可复现。
- 超参数优化模板:推荐使用贝叶斯优化(如
hyperopt库),比网格/随机搜索更高效。 - 项目目录模板:规范化的目录结构(如
/data,/features,/models)能极大提升工作效率。 - Kaggle工具函数集:积累常用的数据读取、内存优化、特征编码、验证函数等,形成自己的工具箱。


总结


在本节课中,我们一起学习了如何系统性地解决一个Kaggle二分类比赛。我们从理解数据和比赛背景出发,通过EDA熟悉数据,然后建立基线模型并诊断出过拟合问题。利用对抗性验证等方法,我们定位了特征偏移这一根本原因,并针对性地处理了类别和连续特征。随后,我们通过多轮特征工程(编码、组合、聚合)稳步提升模型性能。最后,我们总结了类别不平衡、特征偏移、性能优化和特征选择等核心问题的理解与应对策略,并分享了一系列提升效率的实战工具和模板。希望这套从分析到解决问题的框架,能对大家未来的数据科学项目有所启发和帮助。
人工智能—Kaggle实战公开课(七月在线出品) - P5:Kaggle金融比赛实战:房价预测 🏠

在本节课中,我们将学习如何运用机器学习知识,在Kaggle平台上完成一个经典的房价预测竞赛项目。我们将从数据导入开始,逐步讲解数据预处理、特征工程、模型构建与集成,最终提交预测结果。

概述
Kaggle是一个知名的数据科学竞赛平台,国内类似的平台有天池。公司会在这些平台上发布数据集和问题,参赛者需要构建模型来解决问题并提交预测结果。本节课我们将以Kaggle上的“房价预测”竞赛为例,使用一个包含美国房屋多种特征(如房屋等级、占地面积、邻里状况等)的数据集,目标是预测房屋的最终售价。
我们将使用Pandas进行数据处理,并运用决策树、集成学习等模型来完成预测。
数据导入与初步观察
首先,我们需要导入必要的Python库并加载数据。在数据竞赛中,使用Pandas库处理数据是一种高效且通用的做法。
以下是导入库和读取数据的代码:
import pandas as pd
# 读取训练数据和测试数据
train = pd.read_csv('../input/train.csv')
test = pd.read_csv('../input/test.csv')
读取数据后,我们通常需要查看数据的前几行,以了解数据的结构和内容。
# 查看数据前五行
print(train.head())
通过观察,我们可能会发现数据中存在缺失值(显示为NaN)。这提示我们在后续步骤中需要进行数据清洗。
数据预处理
数据预处理是建模前至关重要的一步,目的是将原始数据转换为适合模型训练的格式。本节我们将介绍几个关键的预处理步骤。
目标变量平滑化
在回归问题中,如果目标变量(这里是房价SalePrice)的分布严重偏斜,可能会影响模型的拟合效果。我们通常希望目标变量的分布更接近正态分布。
一种常见的处理方法是使用对数变换。我们采用log(1 + x)的形式,其中的+1是为了防止出现对数为0或负数的情况。
# 对训练数据的目标变量进行log(1+x)变换
train["SalePrice"] = np.log1p(train["SalePrice"])
这个变换可以使目标变量的分布更加平滑,有利于模型学习。
合并训练集与测试集
为了确保训练集和测试集经历完全相同的预处理流程(如填充缺失值、特征编码等),我们先将它们合并。
# 合并数据集,同时标记数据来源
all_data = pd.concat((train.loc[:, 'MSSubClass':'SaleCondition'],
test.loc[:, 'MSSubClass':'SaleCondition']))
合并后,我们可以统一对all_data进行预处理,处理完成后再分割回训练集和测试集。
特征工程
特征工程是指将原始数据转换为更能代表问题本质的特征的过程。以下是两个主要方面。
分类变量处理
数据集中有些列是分类变量(如房屋等级MSSubClass),但它们以数字形式存储。数字本身带有大小关系,但这可能并不符合分类变量的真实含义。
首先,我们将这些列的数据类型转换为字符串,明确其分类属性。
# 将MSSubClass列转换为字符串类型
all_data['MSSubClass'] = all_data['MSSubClass'].astype(str)
接着,对于所有分类变量,我们使用独热编码将其转换为数值形式。独热编码为每个类别创建一个新的二进制列(0或1),从而消除数字间的虚假顺序关系。
# 对分类变量进行独热编码
all_data = pd.get_dummies(all_data)
数值变量处理
对于数值型变量,我们主要处理两个问题:缺失值和数据尺度。
1. 填充缺失值
对于缺失的数值,我们通常用平均值、中位数或0来填充。具体方法需参考竞赛说明。这里我们简单地用该列的平均值填充。
# 用每列的平均值填充缺失值
all_data = all_data.fillna(all_data.mean())
2. 数值标准化
为了消除不同特征之间量纲和数值范围的差异,加速模型收敛,我们通常对数值特征进行标准化处理。标准化公式为:
(x - mean(x)) / std(x)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
# 对数值列进行标准化
numeric_cols = all_data.select_dtypes(include=[np.number]).columns
all_data[numeric_cols] = scaler.fit_transform(all_data[numeric_cols])
标准化后,所有数值特征的均值约为0,标准差约为1。
模型构建与评估
预处理完成后,我们将数据分割回训练集和测试集,并开始构建预测模型。
数据分割
首先,从合并的数据中分离出训练集和测试集。
# 分割数据
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y_train = train.SalePrice
尝试基础模型
我们首先尝试两个基础模型:岭回归和随机森林,并使用交叉验证来评估它们的性能。
1. 岭回归
我们使用交叉验证来寻找岭回归的最佳正则化参数alpha。
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
# 测试不同的alpha值
alphas = [0.05, 0.1, 0.3, 1, 3, 5, 10, 15, 30, 50, 75]
cv_ridge = [cross_val_score(Ridge(alpha=alpha), X_train, y_train, cv=5).mean()
for alpha in alphas]
通过绘制alpha与交叉验证得分的关系图,我们可以找到使模型性能最优的alpha值(例如,可能在10到20之间)。
2. 随机森林
同样,我们对随机森林模型进行调参,例如调整树的数量n_estimators和每棵树考虑的最大特征比例max_features。
from sklearn.ensemble import RandomForestRegressor
# 测试不同的max_features值
max_features = [0.1, 0.3, 0.5, 0.7, 0.9, 0.99]
cv_rf = [cross_val_score(RandomForestRegressor(n_estimators=200, max_features=mf),
X_train, y_train, cv=5).mean()
for mf in max_features]
通过交叉验证,我们可以确定随机森林的最佳参数(例如,max_features可能在0.3左右)。
模型集成
上一节我们介绍了两个基础模型及其调参方法。本节中,我们来看看如何通过集成学习结合这两个模型的优势,以获得更稳健、更准确的预测。
集成学习的核心思想是“三个臭皮匠,顶个诸葛亮”。我们将训练好的岭回归模型和随机森林模型的预测结果进行平均,作为最终的预测值。
# 使用最佳参数训练模型
ridge_model = Ridge(alpha=15).fit(X_train, y_train)
rf_model = RandomForestRegressor(n_estimators=200, max_features=0.3).fit(X_train, y_train)
# 获取两个模型的预测结果(注意:预测值经过了log变换)
ridge_pred = ridge_model.predict(X_test)
rf_pred = rf_model.predict(X_test)
# 对预测结果进行平均集成
final_pred = (ridge_pred + rf_pred) / 2
由于我们对目标变量SalePrice进行了log(1+x)变换,现在需要将集成后的预测值转换回原始尺度。
# 将预测值从对数尺度转换回原始房价尺度
final_pred = np.expm1(final_pred)
生成提交文件
最后,我们需要按照Kaggle要求的格式生成提交文件。
# 创建提交文件
submission = pd.DataFrame({'Id': test.Id, 'SalePrice': final_pred})
submission.to_csv('submission.csv', index=False)
将生成的submission.csv文件上传至Kaggle,即可查看模型在测试集上的得分。

总结
本节课中,我们一起学习了如何完整地参与一个Kaggle房价预测竞赛。我们从数据导入和观察开始,进行了包括目标变量平滑化、分类变量编码、缺失值填充和数值标准化在内的数据预处理与特征工程。然后,我们构建并调优了岭回归和随机森林两个基础模型,最后通过简单的平均法集成这两个模型,得到了最终的预测结果并生成了提交文件。

这个过程展示了将机器学习理论知识应用于实际问题的完整流程,特别是集成学习如何有效提升模型性能。希望这个案例能帮助你更好地理解数据竞赛的实战步骤。
人工智能—Kaggle实战公开课(七月在线出品) - P6:ML必备技巧:模型分析与模型融合 🧩
在本节课中,我们将要学习机器学习流程中至关重要的一步:模型分析与模型融合。当你已经完成了特征工程、模型选择与调优,并拥有了一个或多个表现不错的模型后,下一步就是通过模型融合技术来进一步提升预测性能。
模型融合概述
上一节我们介绍了如何通过调优来获得一个不错的模型。本节中我们来看看如何将多个模型结合起来,以获得更稳定、更强大的预测效果。模型融合主要有三种常见方式。
以下是三种主要的模型融合方法:
- Bagging:旨在解决模型过拟合(Overfitting)问题。
- Stacking:通过组合多个模型的预测结果作为新特征,来训练一个“元模型”。
- Boosting:一种逐步增强的集成方法,如AdaBoost或梯度提升决策树。
Bagging:降低方差,缓解过拟合
Bagging的核心思想是通过构建多个相互独立的基学习器,并对它们的结果进行综合(如投票或平均),来平滑单个模型可能产生的噪声或异常,从而降低整体模型的方差,缓解过拟合。
其过程可以概括为:
- 训练:从原始数据集中有放回地随机抽取多个子集,每个子集用于训练一个基学习器。
- 预测:对于分类任务,综合所有基学习器的预测结果进行投票;对于回归任务,则对所有结果求平均。
在Scikit-learn中,你可以方便地使用 BaggingClassifier 或 BaggingRegressor。
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
# 使用决策树作为基分类器,构建一个包含100个估计器的Bagging集成模型
bagging_model = BaggingClassifier(
base_estimator=DecisionTreeClassifier(),
n_estimators=100
)
Stacking与Blending:元模型的力量
上一节我们介绍了Bagging,它是一种并行集成的思想。本节中我们来看看Stacking,它是一种层次化的模型融合策略。
Stacking的做法是:首先用原始数据训练多个不同的基学习器(第一层),然后将这些基学习器的预测结果作为新的特征,输入给另一个模型(第二层,或称元模型)进行训练,由元模型做出最终预测。
一个简化的理解是:神经网络的多层结构就在做类似的事情,每一层的输出作为下一层的输入。
Blending 可以看作是Stacking的一种简化或特例。它通常使用一个简单的线性模型(如线性回归)作为元模型,对第一层各个模型的预测结果进行加权平均。
例如,在房价预测回归问题中,三个基学习器分别给出预测值 Y1, Y2, Y3。Blending会学习一组权重 (w1, w2, w3),使得线性组合 w1*Y1 + w2*Y2 + w3*Y3 最接近真实房价。表现好的基学习器会获得更大的权重。
注:Scikit-learn没有直接提供Stacking/Blending的接口,但实现起来并不复杂。通常需要手动获取第一层模型的预测输出,并将其作为特征训练第二层模型。
Boosting:从错误中学习
Boosting与Bagging的并行思想不同,它是一种串行的集成方法。其核心在于让后续的模型重点关注之前模型预测错误的样本,通过不断修正错误来提升整体性能。
这个过程类似于学生做练习题:先做一遍,把做错的题目标记出来并给予更多重视,然后重点练习这些错题,如此反复,直到掌握所有题目。
以下是两种经典的Boosting算法:
- AdaBoost:通过调整训练样本的权重,让后续模型更关注被前序模型分错的样本。
- 梯度提升决策树(GBDT/Gradient Boosting):通过在损失函数(Loss Function)的梯度下降方向上构建新的模型,来不断减少残差。
在Scikit-learn中,你可以找到对应的实现:
from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier
# AdaBoost分类器
ada_model = AdaBoostClassifier()
# 梯度提升分类器
gbdt_model = GradientBoostingClassifier()
提示:对于GBDT,在实际应用中(尤其是大数据集),更推荐使用 XGBoost、LightGBM 或 CatBoost 等优化过的库,它们比Scikit-learn中的实现速度更快、功能更强大。
总结与工具回顾
本节课中我们一起学习了三种核心的模型融合技术:用于降低方差的Bagging,层次化组合模型的Stacking/Blending,以及从错误中逐步增强的Boosting。
回顾整个机器学习流程,在Scikit-learn中我们可以找到大部分所需的工具:
- 数据预处理:
sklearn.preprocessing和sklearn.impute。 - 特征工程与选择:
sklearn.feature_selection。 - 模型训练与评估:各种分类器、回归器及评估指标。
- 模型诊断:通过学习曲线分析模型状态。
- 模型融合:
sklearn.ensemble模块提供了Bagging、AdaBoost、GBDT、随机森林等集成方法。


通过合理运用这些技巧,你可以系统地构建出更强健、更准确的机器学习模型,从而在Kaggle等数据科学竞赛中取得更好的成绩。
人工智能—Kaggle实战公开课(七月在线出品) - P7:Spark MLlib 与 Pipeline API 🚀

在本节课中,我们将学习如何利用 Spark MLlib 及其 Pipeline API 来处理海量数据,从而避免因采样而丢失信息。我们将重点介绍数据处理流程、特征工程以及如何在分布式环境中构建和评估模型。

大规模数据处理需求
当面对海量数据时,我们不想进行下采样或大量采样,以免丢失数据信息。这时,我们需要借助大规模计算工具,例如 Spark。
Spark 包含一个名为 MLlib 的组件,专门用于机器学习建模。除了 Spark SQL、Streaming 和 GraphX 用于其他数据处理任务(如流处理或 HDFS 上的 Hive 数据操作),真正的建模工作主要在 MLlib 中完成。
要使用 Spark MLlib,需要熟悉一个概念:ML Pipeline。它提供了一个流程化的框架来组织机器学习任务。
Spark ML Pipeline 核心组件
Spark ML Pipeline 的核心组件与常见的数据科学工具类似,主要包括:
- DataFrame:类似于 Pandas 中的 DataFrame,是 Spark SQL 中处理结构化数据的主要数据结构。
- Transformer:用于对 DataFrame 进行转换,例如数据预处理和特征工程,输入一个 DataFrame 并输出一个新的 DataFrame。
- Estimator:用于拟合模型,从数据中学习参数。
- Parameter:用于向模型或转换器传递参数。
- Evaluator:用于评估模型性能。
一个典型的 Pipeline 流程可以概括为:加载数据 → 抽取特征 → 训练模型 → 评估模型。
Pipeline 工作流程详解
以下是 Spark MLlib 中一个典型的 Pipeline 工作流程,我们将逐步解析每个步骤。
1. 数据读取与初步处理
首先,我们需要将数据读入 Spark。以下是一个使用 PySpark 读取 CSV 文件的示例代码,因为 PySpark 的语法对 Python 用户更友好。
# 示例:读取CSV数据并创建DataFrame
from pyspark.sql import SparkSession
from pyspark.sql import Row
spark = SparkSession.builder.appName("MLExample").getOrCreate()
def parse_line(line):
# 假设CSV文件以逗号分隔
parts = line.split(',')
# 假设第一列是标签,后续列是特征
label = int(parts[0])
int_features = [int(x) for x in parts[1:5]] # 假设第2-5列是整数特征
cat_features = parts[5:] # 假设第6列及之后是类别特征
return Row(label=label, int_features=int_features, cat_features=cat_features)
# 读取文本文件并应用处理函数
lines = spark.sparkContext.textFile("data.csv")
parsed_data = lines.map(parse_line)
df = spark.createDataFrame(parsed_data)
df.show()
这段代码将 CSV 文件的每一行进行分割,并构建一个包含标签、整数特征和类别特征的 DataFrame。
2. 特征工程:类别特征编码
在机器学习中,类别特征(如“A”、“B”、“C”)需要转换为数值形式。一个常见的方法是 One-Hot Encoding。
在 Spark 中,这个过程通常分为两步:
- StringIndexer:为每个类别分配一个索引编号。Spark 会统计类别出现的频率,并按照频率从高到低分配索引(0, 1, 2...)。
- OneHotEncoder:根据索引,将类别转换为稀疏向量表示。
例如,对于类别值 [A, A, C, B, C]:
- StringIndexer 统计后,A 出现2次,C 出现2次,B 出现1次。因此索引可能为:A->0, C->1, B->2。
- OneHotEncoder 将其转换为稀疏向量。例如,A 转换为
(3, [0], [1.0]),表示一个长度为3的向量,在第0个位置值为1。
核心概念公式:
对于一个有 k 个类别的特征,One-Hot Encoding 将其转换为一个 k 维向量,其中只有对应类别索引的位置为1,其余为0。
3. 特征组装与模型训练
处理完所有特征列(例如,对多个类别列进行编码)后,我们需要将它们组合成一个最终的特征向量。这可以通过 VectorAssembler 完成。
接着,就可以将组装好的特征向量输入到分类器(如逻辑回归)中进行训练。
以下是一个简化的 Pipeline 构建示例:
from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer, OneHotEncoder, VectorAssembler
from pyspark.ml.classification import LogisticRegression
# 1. 定义StringIndexer
indexer = StringIndexer(inputCol="cat_features", outputCol="cat_indexed")
# 2. 定义OneHotEncoder
encoder = OneHotEncoder(inputCol="cat_indexed", outputCol="cat_encoded")
# 3. 定义VectorAssembler,组合所有特征
assembler = VectorAssembler(
inputCols=["int_features", "cat_encoded"],
outputCol="features"
)
# 4. 定义逻辑回归模型
lr = LogisticRegression(featuresCol="features", labelCol="label")
# 5. 构建Pipeline
pipeline = Pipeline(stages=[indexer, encoder, assembler, lr])
# 6. 训练模型
model = pipeline.fit(df)

4. 模型评估与工业界实践

模型训练完成后,需要进行评估。在点击率预估等任务中,AUC 是一个关键指标,它是 ROC 曲线 下的面积。
此外,工业界在处理极高维特征时,常使用 特征哈希 等技术进行降维,以节省存储和计算资源。
一个完整的工业级 Pipeline 还包括:
- 将数据分割为训练集和验证集。
- 使用交叉验证进行超参数调优。
- 绘制 ROC 曲线并计算 AUC 值。
- 对模型进行持久化保存和加载。
总结
本节课我们一起学习了如何利用 Spark MLlib 和 Pipeline API 处理大规模数据。我们介绍了 Pipeline 的核心概念,包括 DataFrame、Transformer 和 Estimator。通过具体的代码示例,我们了解了如何读取数据、对类别特征进行 StringIndexer 和 OneHotEncoder 编码、使用 VectorAssembler 组合特征,以及最终构建和训练逻辑回归模型。


Spark MLlib 的 Pipeline 设计思想与 Scikit-learn 相似,但能够在分布式集群上运行,适合处理海量数据。对于熟悉 Python 的同学,PySpark 提供了便捷的接口。掌握这些工具,能够帮助你在 Kaggle 竞赛或工业界实践中,更高效地利用全量数据构建稳健的机器学习模型。

人工智能Kaggle实战公开课(七月在线出品) - P8:特征工程之非标准数据的处理 🛠️
在本节课中,我们将要学习如何处理非标准数据,将其转化为机器学习模型能够处理的标准化特征形式。我们将探讨文本、图片和视频等不同类型数据的处理方法。
概述
在之前的课程中,我们介绍了特征工程的基本概念,即处理那些已经是高维向量形式 X = [特征1, 特征2, ..., 特征N] 但不够“干净”的数据。然而,现实世界中的数据往往并非一开始就是这种规整的格式。
本节我们将关注那些更原始、非标准的数据,例如一句话、一张图片或一段视频。我们的核心任务是通过一系列步骤,将这些数据“数字化”和“向量化”,最终转化为模型可用的特征矩阵。
从非标准数据到特征向量
现实中的数据并非生来就是整齐划一的高维特征。我们需要通过降维、特征提取或数字化表达等方式,将原始数据转化为 X = [特征1, 特征2, ..., 特征N] 的形式。
这个过程可以宽泛地理解为更复杂的特征工程。与处理已结构化数据不同,这里我们面对的是数据本身形态的转换。
文本数据的处理 📝
文本数据,例如“hello world”,本身不是能被计算机直接按列处理的格式。我们需要借助自然语言处理技术将其转化为数字向量。
以下是几种常见的文本向量化方法:
- 词袋模型:统计每个单词在文本中出现的次数,转化为一个由0和1(或词频)组成的向量。例如,“hello”出现记为1,未出现的词记为0。
- 词频-逆文档频率:不仅考虑词频,还考虑词语在整个文档集合中的重要性。
- 词嵌入:例如 Word2Vec,通过语义网络将单词表达为一个固定维度的稠密向量,如
[0.1, 0.43, -0.28, ...]。
核心思想是,我们通过自定义的规则或模型,将文本数据转化为一个数字化的向量。
图片数据的处理 🖼️
图片数据本身是以RGB像素点阵构成的矩阵,这已经是一种数字化的表达形式,可以直接作为模型的输入。
然而,图片处理仍有其特殊性。例如,我们可能只想提取图片中“狗头”的部分,这就涉及到特征区域提取。更重要的是,原始的像素矩阵虽然数字化,但每个像素值本身并无明确语义。
通常,我们会使用卷积神经网络 等算法,从原始像素矩阵中提取出有意义的深层特征。这些提取出的特征值,才是真正意义上的、可用于后续机器学习模型(如分类器)的特征。关于CNN的详细内容,我们会在后续的深度学习课程中深入讲解。
视频数据的处理 🎬

视频数据的维度比图片更高,处理起来也更为复杂。基本的思路是将其分解为更易处理的低维组件。
处理视频数据通常遵循以下步骤:
- 将视频流分离为音轨和视频轨。
- 将视频轨按帧分解为一系列图片,每张图片可按上述图片处理方法转化为矩阵。
- 音轨可以转化为声波波形(一系列振幅数值),或通过语音识别技术转化为文本,再按文本处理方法处理。
通过这种层层降维的方式,最终将所有信息都转化为数字化的向量表达形式。
案例与后续课程安排
本节课提到的文本分类具体案例(如Top News分类),将在下一堂专门讲解自然语言处理的课程中详细展开,这样更符合学习逻辑。课件已提前上传,供大家预习。

关于图片处理及卷积神经网络的应用,我们计划在第五或第六课,即深度学习部分进行系统讲解。
总结
本节课我们一起学习了特征工程中处理非标准数据的关键思想。我们了解到,无论是文本、图片还是视频,都可以通过降维和数字化表达的技术,将其转化为机器学习模型所需的标准化特征向量。文本常用词袋模型或词嵌入,图片本身是矩阵但需CNN提取特征,视频则可分解为图片和音频再分别处理。掌握这些预处理方法,是解决现实世界复杂机器学习问题的重要第一步。

注:课程相关资料与代码将会上传。感谢大家的学习。
人工智能—Kaggle实战公开课(七月在线出品) - P9:以鲸鱼识别为例,利用深度学习解决Kaggle竞赛中的图像分类问题 🐋
在本节课中,我们将学习如何利用深度学习解决Kaggle竞赛中的图像分类问题,并以鲸鱼识别竞赛为例,详细讲解从基线模型到高级技巧的完整流程。我们将看到如何将复杂的计算机视觉任务分解为一系列可解决的机器学习子问题。
注意力机制的原理与应用
上一节我们介绍了深度学习的基础模型,本节中我们来看看如何将不同模型组合以解决更复杂的问题,例如图像描述生成中的注意力机制。
我们有一个目标:输入一张图片,输出描述该图片的文字序列,并希望模型在生成每个词时,能“注意”到图像中特定的区域。我们拥有CNN和RNN模型。
CNN的核心作用是将一张图像转换成一个张量表示。例如,VGG网络可以将图像转换为一个4096维的向量。这个表示包含了图像的多个实例信息,即张量中的每一个“小柱子”都对应着原始图像中的一个特定区域。从机器学习的角度看,这是实现注意力的关键。
注意力机制正是利用了CNN输出的这种张量表示。我们可以将这个张量中的每一个元素(每一个“框框”)视为图像的一个局部区域特征。然后,我们使用RNN来生成描述文字。在RNN生成每一个词时,我们对这些局部特征进行加权求和(通过一个softmax回归来决定权重),权重的大小就代表了模型对该区域的“注意力”程度。
因此,整个过程可以概括为:利用CNN提取的局部特征表示,结合RNN的序列生成能力,通过softmax动态地为每个生成步骤分配注意力权重。
共享语义空间:结合图像与文本
上一节我们介绍了如何让模型“注意”图像局部,本节中我们来看看如何融合图像和文本的语义信息,构建一个共享的表示空间。
在传统的图像分类任务中,我们有一个图像X和一个标签Y。标签通常被编码为one-hot向量,例如[0,0,1,0,...,0]。然而,这种表示忽略了标签词(如“cat”、“dog”)本身丰富的语义信息。
在自然语言处理中,Word2Vec等技术可以将词语嵌入到一个连续的向量空间中,其表示能力远超one-hot编码。因此,一个更圆满的思路是:我们是否可以将图像和文本标签都映射到同一个语义空间(共享空间)中,将分类任务转化为回归或度量学习任务?
从数学角度描述,我们学习两个映射函数:
f_image(X):将图像X嵌入到共享空间。f_label(Y):将文本标签Y(通过Word2Vec初始化)也嵌入到同一个共享空间。
我们的目标是,通过监督学习,让带有“猫”标签的图片f_image(X_cat),在共享空间中与标签“猫”的向量f_label(“cat”)距离尽可能近。

模型结构示例:
一个图像输入CNN(如VGG),经过几层全连接后,不再输出one-hot向量,而是输出一个与Word2Vec维度相同的向量。网络的训练目标就是让这个输出向量接近对应标签的Word2Vec向量。其中,倒数第二层的激活值就可以被视为图像在共享空间中的嵌入表示。
这种做法属于多模态学习,它能够利用文本的语义信息来辅助图像表示的学习,或者进行图像与文本的共同训练。
Kaggle鲸鱼识别竞赛实战
前面我们探讨了一些高级概念,现在让我们进入实战环节,看看如何将这些思想应用于具体的Kaggle竞赛——鲸鱼识别。
任务概述与基线模型
本次竞赛的任务是:给定超过4000张在海上拍摄的鲸鱼照片,要求对一张新照片进行分类,判断它属于哪一种鲸鱼。这是一个细粒度图像分类问题。
训练集有4237张图片,对应427种鲸鱼。但数据分布极不均衡,有些类别的鲸鱼在训练集中仅出现了一次,这是后续需要处理的技巧。
拿到这个问题,最基础的基线方法是:
- 对图像进行缩放和裁剪等预处理。
- 将其输入一个预训练的卷积神经网络(如VGGNet)中。
- 替换并重新训练网络的最后一层,以输出鲸鱼种类的分类结果。


在深度学习中,建议采用敏捷开发的方式:先从最简单的基线模型开始,逐步迭代改进。如果基线模型准确率已经很高(例如99%以上),则无需复杂操作。但通常情况并非如此。
问题分析与改进一:定位器
当我们对基线模型进行分析(例如通过可视化卷积层激活)时,可能会发现一个问题:分类器决策所依赖的特征可能并非鲸鱼头部,而是海面上的波纹或背景。这显然不是我们想要的。
因此,第一个改进思路是:我们能否先定位出鲸鱼的头部,只将头部区域裁剪出来,再送入分类网络?这样可以让模型更关注主体目标。
以下是实现定位器的思路:
我们想要的是图像中鲸鱼头部的边界框,这是一个目标检测问题。但我们掌握的机器学习工具主要是分类器和回归器。一个简单直接的方法是:将边界框的预测转化为回归问题。
- 人工标注一部分训练图片的鲸鱼头部边界框(即四个坐标值)。
- 训练一个回归模型(可以在CNN基础上接一个回归层),直接预测边界框的坐标。
- 可以使用R-CNN或Faster R-CNN等更高级的目标检测方法,但核心思想一致:将检测任务转化为坐标回归。
改进二:对齐器
即使用定位器裁剪出头部后,还可能存在一个问题:鲸鱼头部的角度各异(朝左、朝右、倾斜)。对于神经网络来说,它可能将不同角度误认为是不同类别的特征。
因此,第二个改进技巧是对齐:将所有鲸鱼头部图像旋转到统一的标准角度(如正面朝左)。
以下是实现对齐器的思路:
我们想要的是找到头部的关键点(例如头部前端和颈部的一个点),然后根据这两点连线计算旋转角度。这又是一个关键点检测问题。
- 我们将其再次转化为回归问题:人工标注这些关键点的坐标。
- 训练一个回归模型来预测这些关键点的位置。
- 根据预测出的关键点,计算所需旋转角度,对图像进行仿射变换,实现对齐。
通过“定位 → 对齐 → 分类”这一流程,我们可以显著提升模型在细粒度分类任务上的性能。这个过程清晰地展示了如何将一个复杂的现实问题(鲸鱼识别),分解为多个可解的机器学习子问题(分类、回归),并逐步优化。
课程总结


本节课中我们一起学习了如何系统性地解决Kaggle图像分类竞赛。我们从注意力机制和共享语义空间的理论出发,理解了模型融合与多模态学习的思路。接着,我们以鲸鱼识别竞赛为案例,实战演练了从构建基线模型开始,通过分析模型缺陷,逐步引入定位器和对齐器来优化性能的完整流程。关键在于掌握将复杂计算机视觉任务(如检测、对齐)分解并转化为基本的分类或回归问题的思想。
🐍 人工智能—Python AI公开课(七月在线出品) - P1:三节课上手Python第一节
在本节课中,我们将要学习Python的入门知识。课程主要包含四个目标:了解Python的简介与应用、搭建Python开发环境、学习使用Jupyter Notebook工具,以及掌握Python的基础语法和操作。我们将从零开始,手把手带你进入Python的世界。
🎯 课程目标与概述
本节课主要包含四个目标。
第一个目标是介绍Python语言的简介和它的具体应用。
第二个目标是手把手带领大家搭建Python的开发环境。
第三个目标是在环境搭建完成后,学习如何使用主要的开发工具Jupyter Notebook。
第四个目标是学习Python的一些基础操作,包括简单的语法、常用操作符以及循环分支判断结构。
今天的内容比较丰富,但我们会确保讲解清晰,让每位同学都能跟上。
📚 Python简介与应用
上一节我们概述了课程目标,本节中我们来看看Python语言的背景和特点。
Python语言的作者是Guido van Rossum,一位荷兰的工程师。Python这个名字来源于他喜欢的一个喜剧团体“Monty Python”。
Python并非横空出世,它的前身是ABC语言。ABC语言是一个旨在服务于非专业程序员的语言项目。虽然ABC项目后来停止了,但Python从中汲取灵感并发展起来。
Python诞生于上世纪90年代,已经不是一个年轻的语言。
关于Python版本,初学者无需过度纠结。如果你现在开始学习,请直接从Python 3.x开始,可以是3.5或3.6。不必为版本的兼容性担心。


Python拥有许多特色,其中最重要的特色是简单易学。相对于其他编程语言,Python的学习难度较低。


Python的设计初衷是让程序员从底层硬件、操作系统等细节中抽离出来,专注于解决问题。





除了简单,Python流行的另一个关键原因是它拥有强大的第三方库支持。



以下是Python的一些主要应用领域:
- Web开发: 可以使用Django、Flask等框架。
- 科学计算: 拥有NumPy、SciPy等库进行矩阵运算和信号分析。
- 数据可视化: 可以使用Matplotlib、Seaborn等库。
- 深度学习: 拥有TensorFlow、PyTorch等强大的库支持。
- 图形界面开发: 也有相应的GUI工具库。



Python可以看作是一个“万能”的语言,其强大的应用生态支撑了它的高流行度。





⚙️ Python环境搭建



上一节我们介绍了Python语言本身,本节中我们来看看如何搭建它的运行环境。



Python是一门解释型语言。我们写的代码需要一个解释器来执行。因此,我们需要安装Python环境。



安装Python环境有多种方法。第一种方法是访问Python官网(python.org)下载安装包。对于Windows系统很方便,Linux系统通常自带Python环境。





但是,不推荐大家直接从官网安装。原因有两个:
- 官网环境只是一个基础解释器。当你需要用到第三方库(如做图像处理的PIL库或做深度学习的TensorFlow)时,需要手动安装,可能会遇到版本兼容和依赖问题,耗费大量时间。
- 同时管理Python 2.x和3.x多个版本的环境会比较麻烦,容易混淆。


我们推荐安装Anaconda环境。Anaconda是一个集成了Python和大量常用科学计算库的发行版。


以下是安装步骤:
- 从清华大学镜像站等渠道下载Anaconda安装包(推荐下载Python 3.5或3.6版本,64位系统选择x86_64版本)。
- 运行安装程序,在安装过程中,所有提示选项都勾选上,进行“傻瓜式”安装即可。



Anaconda体积较大(约500MB),但它集成了大量库,避免了单独安装库的麻烦,极大地提高了学习和开发效率。

🖥️ Jupyter Notebook使用


上一节我们完成了环境搭建,本节中我们来看看核心开发工具如何使用。



Python代码通常有两种运行方式。第一种是命令行方式。安装Anaconda后,打开命令提示符(CMD),输入python并回车,即可进入Python交互环境。在此环境下可以输入代码并立即看到结果,但修改和编写较长的代码不太方便。



我们主要学习第二种方式:使用Jupyter Notebook。这是一个基于Web的交互式计算环境。



首先,在命令提示符(CMD)中,输入jupyter notebook并回车。这个命令会启动一个本地服务器。


启动完成后,它会自动在浏览器中打开一个页面,地址通常是localhost:8888。这个页面就是Jupyter Notebook的主界面。




在Notebook主界面,点击“New”按钮,选择“Python 3”,即可创建一个新的Notebook文件。



Notebook中的代码编写在“单元格”(Cell)中。在单元格中输入代码,例如:
A = 1
B = "China"
print(A, B)
要运行单元格中的代码,可以点击工具栏上的“运行”按钮,或使用快捷键Ctrl + Enter。运行后,结果会直接显示在单元格下方。

Notebook的优势在于可以随时修改单元格中的代码并重新运行,非常适合学习和探索。
如果需要了解Notebook的更多操作(如合并单元格、切换模式等),可以在Notebook界面中按Esc键退出编辑状态,然后按H键,即可查看快捷键帮助。


🧠 Python基础操作



上一节我们学会了使用开发工具,本节中我们开始学习Python语言本身的基础知识。
由于时间关系,这里只为大家梳理一个清晰的脉络。如果你有其他语言基础,会很容易理解;如果没有,按照这个路径学习也能快速上手。
基础语法
注释: 在代码前添加#号,该行代码就不会被执行,用于解释说明。
# 这是公开课的演示代码
A = 1




代码换行: 如果一行代码很长,可以使用反斜杠\进行换行。
total = item_one + \
item_two + \
item_three

代码块与缩进: Python使用冒号:和缩进来区分代码块,而不是花括号{}。缩进是Python语法的重要组成部分。
def add(a, b): # 函数定义,冒号表示代码块开始
return a + b # 缩进表示属于函数内部的代码
if a > b: # 条件判断,冒号表示代码块开始
print("a is larger") # 缩进表示属于if内部的代码
模块导入: 使用import关键字导入第三方库或模块。
import numpy as np # 导入NumPy库,并简称为np
data = np.random.randn(4, 4) # 使用np生成一个4x4的随机数数组
print(data)
标识符与操作符
标识符: 变量、函数等的名称。命名规则包括不能以数字开头,只能包含字母、数字和下划线。
var1 = “正确”
1var = “错误” # 语法错误,不能以数字开头
操作符: Python包含多种操作符。
- 比较操作符:
>, <, ==, !=等。print(1 > 2) # 输出 False print(1 < 2) # 输出 True - 赋值操作符:
=。a = 3 a = b = c = 5 # 多重赋值 - 交换赋值: Python可以便捷地交换变量值。
a, b = b, a # 交换a和b的值

循环与条件判断

for循环: 遍历一个序列(如列表、字符串)。
for i in range(5): # 循环5次,i从0到4
print("第", i+1, "次循环")
while循环: 在条件为真时重复执行代码块。
n = 0
while n < 5:
print("n =", n)
n = n + 1 # 或 n += 1


基础数据结构
Python有几种基本数据结构:
- 数值:
a = 10 - 字符串:
b = "hello" - 列表:
c = [1, 2, 3, 'a', 'b'],有序,可修改。 - 元组:
d = (1, 2, 3, 'a'),有序,不可修改。 - 字典:
e = {'name': 'Alice', 'age': 25},键值对集合。
序列访问: 对于字符串、列表等序列,可以通过索引和切片访问元素。
- 索引访问: 从0开始计数。
s = "China" print(s[0]) # 输出 'C' - 切片访问:
[起始索引:结束索引],包含起始,不包含结束。s = "China" print(s[0:3]) # 输出 'Chi' print(s[:3]) # 输出 'Chi',起始默认为0 print(s[2:]) # 输出 'ina',结束默认为末尾
📝 课后练习


本节课的最后,我们留一个小练习来巩固所学知识。



题目:回文判断
回文是指正读和反读都相同的字符串,例如 “61816”、“level”、“上海自来水来自海上”。
请你编写一个Python程序,判断用户输入的字符串是否是回文。
提示:可以利用字符串的切片操作(例如 s[::-1] 可以得到字符串s的倒序)来简化判断。




🎓 课程总结

本节课中我们一起学习了Python的入门知识。
我们首先了解了Python的简介、特色和广泛的应用领域。然后,我们手把手搭建了推荐的Anaconda开发环境,并学习了如何使用强大的Jupyter Notebook工具进行编码和探索。最后,我们快速浏览了Python的基础语法、操作符、流程控制语句和数据结构,并通过一个回文判断练习让大家动手实践。

Python学习既简单也困难。简单在于它的入门门槛相对较低;困难在于任何技能的掌握都需要持之以恒的练习。记住“用以致学”,将学到的知识立即付诸实践,是最高效的学习方法。希望你能完成课后练习,这是我们今天课程最重要的成果。



期待在下一节公开课中与你再次相见!
🧠 人工智能公开课 P10:大学生AI成长计划
在本节课中,我们将要学习人工智能的基本概念、行业现状、项目流程以及成为一名AI工程师所需的核心技能。课程内容分为四个主要部分:对AI行业的思考、AI项目的基本流程、需要掌握的核心技能,以及七月在线提供的学习路径和资源。
🤔 第一部分:对AI行业的思考
人工智能的声音已逐步变得普遍理性,越来越多的AI创业项目已经开始下沉到应用层面,例如金融、电商、教育等领域。与此同时,AI技术型人才也同样着手于探究AI更深层次的技术难题。未来,这方面的人才会越来越多,对应的市场机会也会越来越大。
对于AI的发展及其对社会和行业的影响,我们可以从以下六个方面进行思考:
以下是六个核心思考维度:
- What:人工智能到底是什么?它的整个发展和经历是一个什么样的过程?
- Why:为什么要有这样的人工智能?
- Where:现在人工智能在各个应用场景下达到了一个什么样的水平?
- How:我们怎么去实现它?实现后有哪些成果?
- When:人工智能什么时候才能走进我们的生活,产生落地的价值?
- Who:在人工智能的浪潮里,我们充当什么样的角色?
人工智能的定义与发展
在讲人工智能之前,可以回想一下人类的智能是如何发展而来的。人类的智能是通过迭代进化产生的。人工智能时代的产生也有其迭代顺序:从能源时代,到电子时代,再到互联网时代,最后进入人工智能时代。
今天的人工智能依托于计算机及其背后的计算能力,并需要互联网和移动互联网所带来的大量数据作为支撑。人工智能可以被视为第四次工业革命,其终极目的是将人类从繁重的脑力劳动中解放出来。
人工智能的定义是利用数字计算机或数字计算机控制的机器,模拟、延伸和扩展人的智能,包括感知环境等,以获取并使用知识的系统。人工智能领域的研究包括机器人、语音识别、图像识别、自然语言处理和专家系统等。
人工智能的三个阶段
一般根据内容的难易程度,将人工智能分为三个阶段:运算智能、感知智能和认知智能。
- 运算智能:主要作用是能存会算,既能存储也可以进行大量运算。例如,阿尔法围棋(AlphaGo)本质上属于运算智能,它解决了在有限任务空间内的问题。
- 感知智能:主要表现是能听会说、能看会认。生活中常见的智能音箱、智能手环、无人机自主飞行、无人驾驶等设备都具有识别和感知能力。
- 认知智能:这是终极目标,即能够理解并且会思考。自然语言处理是人工智能皇冠上的明珠,但实现起来非常困难,因为语言本身含有多重含义,需要常识、知识、多模态等技术作为支撑。
人工智能的魅力在于其持续向前进化和无成本复制的能力。机器的性能可以持续优化,而一旦研发成功,其边际复制成本极低。
强人工智能与弱人工智能
在讨论如何实现人工智能时,需要区分两个重要概念:强人工智能和弱人工智能。
- 强人工智能:指机器能像人一样推理和思考,具有自我意识。这属于前瞻性研究。
- 弱人工智能:指专注于完成特定任务的智能系统。目前我们处于这个阶段,其主流技术路线是基于大数据和深度学习框架。
实现路径主要依赖于深度学习算法、算力(计算能力)和大数据这三者的结合。
人工智能的落地与挑战
人工智能要走进生活并产生价值,需要经历从理论、技术、原型、产品到商品、商业的漫长链条。在这个过程中,挑战非常大。公众对人工智能的期望很高,但对它的容错率却很低。例如,翻译系统或无人驾驶一旦出现小问题,就会被无限放大。
目前已经落地的人工智能应用包括人脸识别、智能音箱、无人车、病理检测、智能红绿灯和天气预报等。
在人工智能的浪潮中,像七月在线这样的教育培训公司,定位是培养AI人才,助力AI产业发展。
上一节我们探讨了AI行业的宏观图景,接下来我们将视角转向具体实践,看看一个AI项目是如何从想法变为现实的。
🔄 第二部分:AI项目的基本流程
企业中完成一个AI项目的大致流程可以分为五个步骤。需要注意的是,这只是一个通用框架,并非所有项目都严格遵循。
以下是AI项目的五个核心步骤:
- 提取问题:找到出发点,明确需求,并将实际问题抽象成数学问题(如分类、回归、聚类)。
- 获取数据:通过爬虫、购买等方式获取原始数据,并将其分为训练集和测试集。数据决定了机器学习结果的上限。
- 特征工程:包含特征构建、特征提取和特征选择。实际工作中大部分时间都花在这个阶段,目的是从原始数据中提炼出对模型有用的信息。
- 建模:核心环节包括训练、诊断、优化、验证和模型融合。
- 训练:使用算法在数据上学习。
- 诊断:判断模型是否过拟合或欠拟合,常用方法有交叉验证、绘制学习曲线等。
- 优化:调整模型参数以提高性能。诊断与优化是一个反复迭代的过程。
- 验证:使用测试数据集验证模型的有效性,分析误差来源。
- 融合:将多个模型或特征工程的前后端结合,以提升算法精确度。
- 上线部署:将模型集成到工程系统中。线上效果需综合准确度、误差、运行速度、资源消耗和稳定性等多个因素来判断。

了解了项目流程后,我们就有了建造AI大厦的“施工蓝图”。那么,作为工程师,我们需要准备哪些“建筑材料”和“工具”呢?


⚙️ 第三部分:需要掌握的核心技能
掌握基本流程后,就像大楼有了框架,接下来需要填充内容。我们可以根据七月在线的技能图谱(知识树)来规划学习路径,该图谱涵盖从入门到高级的各个阶段。
入门基础
以下是入门阶段需要掌握的基础技能:
- 数学基础:微积分、概率论、线性代数、图论。这些是理工科必修课程。
- 编程语言:首选Python,因为它简单且拥有丰富的数据处理库(如NumPy, Pandas, Matplotlib)。
- 数据结构与算法:理解堆、栈、树、图、递归、排序(冒泡、选择)等常见数据结构和算法。
初级阶段
初级阶段的核心是数据分析,需要敏锐的数据洞察力。
- 数据获取:使用Python进行网络爬虫(如Scrapy框架、requests库)。
- 数据处理:进行数据清洗、分组、合并等操作,为特征工程做准备。
- 基础建模:开始接触模型训练与选择。
中级阶段
中级阶段侧重于数据建模,学习主流的机器学习算法。
- 监督学习:逻辑回归、决策树、朴素贝叶斯、支持向量机(SVM)等。
- 无监督学习:聚类等算法。
- 进阶领域:集成学习、深度学习、强化学习、迁移学习、生成对抗网络(GAN)等。学习顺序建议从机器学习到深度学习,再逐步深入。


高级阶段




高级阶段是应用阶段,将所学知识用于解决实际问题。
- 应用方向:无人驾驶、自然语言处理(聊天机器人)、语音识别、图像处理、推荐系统、金融风控、计算广告、无人机自主飞行等。
- 工具与框架:直接使用成熟的框架如TensorFlow、PyTorch。
- 实践与提升:参与Kaggle等竞赛、阅读和解读前沿论文,以保持技术敏感度。

技能图谱为我们指明了学习方向。对于大学生而言,如何系统性地迈出第一步呢?七月在线为此设计了专门的学习计划。
🚀 第四部分:走进七月在线
七月在线是一家教育培训公司,目标是培养百万AI人才,助力AI产业发展。针对大学生群体,推出了“大学生AI成长计划”。
大学生AI成长计划课程
该课程面向高年级本科生或即将毕业的研究生,采用录播加直播的方式,包含在线视频、在线实训和在线直播三种形式。




课程规划分为七个阶段:
- Python入门:掌握Python语言基础。
- 数据结构与算法:学习栈、队列、哈希表等,为数据分析打基础。
- 数据分析:重点学习NumPy、Pandas、Matplotlib,通过大量案例掌握数据处理与可视化。
- 机器学习原理:学习线性回归、决策树、随机森林、SVM等基础算法原理。
- 机器学习实战:通过Kaggle比赛等案例,将原理应用于实践。
- 深度学习原理与实战:学习卷积神经网络(CNN)、循环神经网络(RNN)等,并实战搭建电影推荐网站项目。
- 进阶与就业:学完后可向更高阶的集训营或就业班靠拢。就业班需通过笔试面试,并提供就业保障。



平台资源


七月在线平台还提供丰富的辅助资源:
- 题库:包含4000多道来自大厂的面试真题(如机器学习330+道),可在线练习并查看解析。
- 在线编程:提供类似LeetCode的编程题目。
- 竞赛平台:举办手写体识别、文本情感分类等竞赛,供学员实践。
- 社区:学员可以交流学习经验,讨论问题。
📝 总结


本节课我们一起学习了人工智能的基本概念、发展历程和三个阶段(运算、感知、认知)。我们探讨了AI项目的标准流程:从问题定义、数据获取、特征工程、建模调优到上线部署。接着,我们梳理了成为AI工程师所需的技能树,包括数学、编程、数据结构、机器学习、深度学习以及各领域的应用。最后,我们介绍了七月在线为大学生量身定制的“AI成长计划”课程及其丰富的学习资源。人工智能行业空间广阔,找准定位并系统学习,将有助于我们在这个持续发展的时代中把握机会。


🐍 人工智能—Python AI公开课(七月在线出品) - P2:三节课上手Python第二节


在本节课中,我们将要学习Python编程中两个核心概念:函数与面向对象编程。它们是构建复杂程序、实现代码复用和结构化的基石。我们将通过具体的代码示例,帮助你理解如何创建和使用函数,以及如何运用面向对象的思想来设计程序。





🔄 复习与课程目标

上一节我们介绍了Python基础、Anaconda环境配置、Jupyter Notebook的使用以及Python的基本数据类型。本节中,我们来看看本节课的两个核心目标。


本节课的目标是承前启后:
- 了解函数的意义,掌握函数的创建及使用,包括不定长参数。
- 理解面向对象的基本思想,理解类和对象的关系,了解类的结构和成员,并能根据需求创建包含相应成员的类。
📦 第一部分:函数



函数是编程中用于封装代码块、实现特定功能的基本单元。它接收输入(参数),经过处理,然后返回输出(结果)。

函数的创建与结构


在Python中,使用 def 关键字来定义函数。

def my_add(a, b):
c = a + b
return c





def:定义函数的关键字。my_add:函数名。(a, b):形参列表,定义函数接收的参数。::冒号表示函数头的结束。c = a + b和return c:函数体,包含具体的执行逻辑和返回语句。






调用函数时,需要传入实际的参数(实参):
result = my_add(4, 7) # 实参为4和7
print(result) # 输出:11

函数体是执行具体操作的地方。return语句用于指定函数的返回值。如果没有return语句,函数默认返回 None。
print() 和 return 的区别在于:print() 是将内容输出到屏幕,而 return 是将结果返回给调用者。一个函数可以有多个 print(),但通常只有一个 return 值(或元组等复合结构)作为最终输出。


实战:实现斐波那契数列函数
斐波那契数列的特点是:从第三项开始,每一项都等于前两项之和(例如:0, 1, 1, 2, 3, 5, 8...)。


以下是实现该数列的函数:
def fib(num):
l1 = [0, 1] # 初始化数列前两项
for i in range(num - 2): # 循环生成后续项
l1.append(sum(l1[-2:])) # 将最后两项的和追加到列表
return l1


# 调用函数,生成前8个斐波那契数
print(fib(8)) # 输出:[0, 1, 1, 2, 3, 5, 8, 13]
这个函数接收一个参数 num,返回一个长度为 num 的斐波那契数列列表。它演示了如何将过程化的循环逻辑封装在一个函数中,方便重复调用。


不定长参数
有时我们希望函数能接受任意数量的参数。这时可以使用不定长参数 *args。
def my_add(*args):
return sum(args)
print(my_add(3, 5, 7)) # 输出:15
print(my_add(1, 2, 3, 4, 5)) # 输出:15
print(my_add()) # 输出:0

*args 会将所有传入的位置参数打包成一个元组,在函数体内可以遍历或直接使用。

🧠 第二部分:面向对象编程(OOP)
面向对象编程是一种将数据和操作数据的方法组合成“对象”的编程范式。Python中“一切皆对象”。
核心概念:类与对象


- 类:是对具有相同属性和行为的一类事物的抽象描述,相当于蓝图或图纸。例如,“学生”是一个类。
- 对象:是类的一个具体实例,是根据类创建出来的实体。例如,名为“张三”的学生是一个对象。
关系:类是抽象的模板,对象是根据模板创建的具体实体。
类的创建与成员

使用 class 关键字定义类。类包含属性(变量)和方法(函数)。

以下是一个“学生类”的设计示例,要求能记录学生信息、考试成绩、毕业状态,并能统计整体学生情况。
class Student:
# 类属性:所有对象共享
total = 0 # 学生总人数
name_list = [] # 所有学生姓名列表
graduated_count = 0 # 已毕业学生人数
graduate_standard = 1000 # 毕业所需总分
def __init__(self, name, age, gender):
# 实例属性:每个对象独有的数据
self._name = name # 姓名(私有属性)
self._age = age # 年龄
self._gender = gender # 性别
self._score = 0 # 个人总分,初始为0
# 更新类属性
Student.total += 1
Student.name_list.append(name)
# 实例方法:对象的行为
def exam(self, score):
"""考试方法,成绩合格则累加分数"""
if score < 60:
return "Sorry, you didn't pass."
else:
self._score += score
# 检查是否达到毕业标准
if self._score >= Student.graduate_standard:
Student.graduated_count += 1
return f"Score added. Current total: {self._score}"
def check_score(self):
"""查询个人当前总分"""
return self._score
# 类方法:与类相关,而非特定实例
@classmethod
def check_graduated(cls):
"""查询已毕业学生人数"""
return cls.graduated_count
@classmethod
def check_all_students(cls):
"""查询所有学生姓名"""
return cls.name_list
代码解析:
__init__方法:构造方法,在创建对象时自动调用,用于初始化对象的属性。self代表对象实例本身。- 实例属性:如
self._name,以self.开头,属于每个对象独有的数据。属性名前加单下划线_是一种约定,暗示其为“私有”(非强制),建议不直接从外部访问。 - 类属性:如
total,在类内部直接定义,属于类本身,所有对象共享。 - 实例方法:第一个参数必须是
self,代表调用该方法的对象。如exam,check_score。 - 类方法:使用
@classmethod装饰器,第一个参数必须是cls,代表类本身。如check_graduated,check_all_students。
使用类创建对象
# 创建两个学生对象
david = Student("David", 33, "Male")
jack = Student("Jack", 23, "Male")

# 调用实例方法:David参加考试
print(david.exam(80)) # Score added. Current total: 80
print(david.exam(90)) # Score added. Current total: 170

# 查询David的分数
print(david.check_score()) # 170
# 调用类方法:查询整体情况
print(Student.check_all_students()) # ['David', 'Jack']
print(Student.check_graduated()) # 0 (尚未有人毕业)
通过这个例子,你可以看到:
david和jack是两个独立的对象,拥有各自的分数。- 类属性
total和name_list记录了所有学生的整体信息。 - 当某个学生的分数达到
graduate_standard时,类属性graduated_count会自动更新。
面向对象编程通过这种方式,将数据(属性)和操作数据的方法(方法)捆绑在一起,使代码结构更清晰,更易于管理和扩展。
📝 总结
本节课中我们一起学习了Python中两个至关重要的概念。

首先,我们深入探讨了函数。我们学习了如何使用 def 关键字定义函数,理解了形参、实参、函数体和返回值的含义,并通过斐波那契数列的例子实践了函数的封装与调用。我们还简要介绍了不定长参数 *args 的用法。


接着,我们进入了面向对象编程的世界。我们理解了“类”作为蓝图和“对象”作为实例的关系。通过设计一个功能完整的“学生类”,我们实践了如何定义类属性、实例属性、实例方法以及类方法。我们看到了 self 和 cls 参数的作用,并初步了解了封装的思想。



掌握函数和面向对象是编写结构化、可复用Python代码的关键。建议你根据课堂示例和留下的作业(如“创建素数查找函数”、“设计公司类”)进行练习,这是巩固知识的最佳途径。



下节课,我们将学习用于科学计算的 NumPy 库和用于数据分析的 Pandas 库,进入数据处理的实际应用阶段。


人工智能—Python AI公开课(七月在线出品) - P3:三节课上手Python第三节 🧮
在本节课中,我们将要学习Python科学计算的核心库NumPy和Pandas的简介,并重点使用NumPy来计算一个Softmax函数。我们将从NumPy的基础概念讲起,逐步深入到数组操作和运算,最后通过一个实践项目来巩固所学知识。
课程回顾与概述
上一节我们介绍了Python的函数与面向对象编程,理解了如何通过封装来提高代码的复用性和可维护性。本节中,我们来看看Python在科学计算领域的强大工具。
本节课是“三节课上手Python”系列的最后一节,主题是“Python与科学计算”。我们将重点介绍NumPy库,它是一个专为高效数学运算而设计的库,解决了Python原生列表在数值计算上能力偏弱的问题。同时,我们也会简要提及Pandas库,它用于内存中的表格数据处理。由于时间关系,我们将主要精力放在NumPy上,掌握它之后,学习Pandas将会更加容易。
第一部分:NumPy库简介与导入
NumPy是“Numerical Python”的缩写,它是Python科学计算的基础包,提供了一个强大的N维数组对象ndarray,以及大量的数学函数。


要使用NumPy,我们首先需要导入它。通常,我们为其设置一个简短的别名np,以方便后续调用。
import numpy as np
导入成功后,我们就可以使用np来调用NumPy的所有功能了。
第二部分:为什么需要NumPy?🤔
在深入NumPy之前,让我们思考一个简单的问题:如何给一个Python列表中的每个元素都加1?
使用原生Python列表,这个过程比较繁琐:
L1 = [1, 2, 3]
# 直接加1会报错
# L1 + 1
# 需要循环遍历
L2 = []
for i in L1:
L2.append(i + 1)
print(L2) # 输出:[2, 3, 4]
如果列表是多维的,代码会变得更加复杂。而使用NumPy,这一切变得非常简单:
import numpy as np
L1_np = np.array([1, 2, 3])
result = L1_np + 1
print(result) # 输出:[2 3 4]
# 同样支持乘法等运算
result_mul = L1_np * 4
print(result_mul) # 输出:[ 4 8 12]
NumPy的ndarray对象支持这种“广播”式的元素级运算,极大地简化了代码。
第三部分:构建NumPy数组(ndarray)
ndarray是NumPy的核心对象,代表N维数组。N代表维度(Dimension),D就是维(Dimension),所以ndarray即N维数组。
以下是构建ndarray的几种常见方法:
1. 从Python列表或元组创建
这是最直接的方式,np.array()函数可以将序列对象转换为ndarray。
# 一维数组
arr1d = np.array([1, 2, 3, 4, 5])
print(arr1d)
# 二维数组
arr2d = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10]])
print(arr2d)
2. 使用NumPy内置函数快速创建
NumPy提供了多种快速创建特殊数组的函数。
# 生成指定形状的随机数组
arr_random = np.random.rand(2, 5) # 2行5列,元素在[0,1)区间均匀分布
print(arr_random)
# 生成序列数组,类似range
arr_range = np.arange(10) # 生成0到9的数组
print(arr_range)


# 生成线性间隔数组
arr_linspace = np.linspace(1, 10, 100) # 在1到10之间生成100个等间隔的数
print(arr_linspace)
# 生成全零数组
arr_zeros = np.zeros((4, 5)) # 4行5列的全0数组
print(arr_zeros)
# 生成全一数组
arr_ones = np.ones((3, 4)) # 3行4列的全1数组
print(arr_ones)
# 生成指定值的数组
arr_full = np.full((2, 3), 7) # 2行3列,所有元素为7
print(arr_full)
第四部分:ndarray的基本属性
每个ndarray对象都有一些重要的属性,用于描述其状态。
以下是ndarray的几个核心属性:
shape: 返回一个元组,表示数组在每个维度上的大小(形状)。例如,(2, 5)表示2行5列。ndim: 返回数组的维数(维度数量)。例如,二维数组的ndim为2。size: 返回数组中元素的总数。它等于shape中各维度大小的乘积。dtype: 返回数组中元素的数据类型,如int64,float64。
让我们通过代码查看这些属性:
arr = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10]])
print(“数组形状 (shape):”, arr.shape) # 输出:(2, 5)
print(“数组维度 (ndim):”, arr.ndim) # 输出:2
print(“元素总数 (size):”, arr.size) # 输出:10
print(“数据类型 (dtype):”, arr.dtype) # 输出:int64 (取决于系统)
其中,shape和ndim是最常用和最重要的两个属性。


第五部分:ndarray的索引与切片
理解了数组的构建和属性后,我们需要学习如何访问和修改其中的数据。NumPy的索引和切片语法与Python列表类似,但可以扩展到多维。
1. 基本索引与切片
对于一维数组,其用法与列表完全一致。

arr = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print(arr[2]) # 输出:2 (索引)
print(arr[2:5]) # 输出:[2 3 4] (切片)
print(arr[:5]) # 输出:[0 1 2 3 4]
print(arr[5:]) # 输出:[5 6 7 8 9]

对于多维数组,使用逗号,分隔不同维度的索引。
arr_2d = np.arange(15).reshape(3, 5) # 将0-14重组成3行5列
print(arr_2d)
# 输出:
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]]



# 访问单个元素:第1行,第2列(索引从0开始)
print(arr_2d[1, 2]) # 输出:7
# 切片:第1行到最后一行,所有列
print(arr_2d[1:, :])
# 输出:
# [[ 5 6 7 8 9]
# [10 11 12 13 14]]
# 切片:所有行,第2列到第4列
print(arr_2d[:, 2:4])
# 输出:
# [[ 2 3]
# [ 7 8]
# [12 13]]
2. 花式索引 (Fancy Indexing)
除了连续的切片,还可以传入一个索引列表来访问不连续的位置。
arr_2d = np.arange(15).reshape(3, 5)
# 访问第0行和第2行,第1列和第3列
print(arr_2d[[0, 2], :][:, [1, 3]])
# 更直接的写法:
print(arr_2d[[0, 2]][:, [1, 3]])
# 或者使用 np.ix_ 函数更清晰(可选)
# 输出:
# [[ 1 3]
# [11 13]]
3. 布尔索引
通过布尔数组来索引,常用于条件筛选。
arr = np.array([1, 2, 3, 4, 5])
# 选出大于2的元素
mask = arr > 2
print(arr[mask]) # 输出:[3 4 5]
掌握了访问方法,赋值操作自然也就明白了,只需在等号左边指定索引位置即可。

arr_2d[0, 0] = 99
print(arr_2d[0, 0]) # 输出:99

第六部分:ndarray的维度操作
有时我们需要改变数组的形状,NumPy提供了灵活的方法。
1. reshape方法
reshape方法返回一个指定形状的新数组视图,不改变原数组的数据。
arr = np.arange(30) # 一维,30个元素
arr_reshaped = arr.reshape(5, 6) # 变为5行6列的二维数组
print(arr_reshaped.shape) # 输出:(5, 6)
print(arr.shape) # 输出:(30,) 原数组未变
2. resize方法或直接修改shape属性
直接修改数组的shape属性或使用resize方法会改变原数组。
arr = np.arange(30)
arr.shape = (5, 6) # 直接修改shape属性
print(arr.shape) # 输出:(5, 6)

# 或者使用resize
arr = np.arange(30)
arr.resize((5, 6))
print(arr.shape) # 输出:(5, 6)

3. flatten或ravel方法
将多维数组“展平”为一维数组。flatten返回拷贝,ravel返回视图。
arr_2d = np.arange(12).reshape(3, 4)
arr_flat = arr_2d.flatten()
print(arr_flat) # 输出:[ 0 1 2 3 4 5 6 7 8 9 10 11]
第七部分:ndarray的运算


NumPy的强大之处在于其高效的数组运算能力。

1. 元素级运算(广播机制)
数组与标量之间的运算,会被广播到数组的每个元素上。
arr = np.array([1, 2, 3])
print(arr + 1) # 输出:[2 3 4]
print(arr * 2) # 输出:[2 4 6]
print(arr ** 2) # 输出:[1 4 9]
两个形状相同的数组之间的运算,也是对应位置的元素进行运算。

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(a + b) # 输出:[5 7 9]
print(a * b) # 输出:[4 10 18]

2. 矩阵乘法(点积)
使用np.dot()函数或@运算符进行矩阵乘法。这要求第一个数组的列数等于第二个数组的行数。
A = np.random.rand(5, 3) # 5行3列
B = np.random.rand(3, 2) # 3行2列
C = np.dot(A, B) # 结果C的形状为 (5, 2)
# 或者使用 @ 运算符
C = A @ B
print(C.shape) # 输出:(5, 2)
3. 通用函数(ufunc)
NumPy提供了许多对数组执行元素级运算的通用函数,如np.sin, np.exp, np.log等。

arr = np.array([0, np.pi/2, np.pi])
print(np.sin(arr)) # 输出:[0.0000000e+00 1.0000000e+00 1.2246468e-16]

4. 聚合函数
沿指定轴(维度)进行统计计算,如求和、求均值、最大值、最小值等。

arr = np.arange(15).reshape(3, 5)
print(arr)
# 输出:
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]]

# 全局求和
print(np.sum(arr)) # 输出:105
# 沿轴0求和(跨行,即每列求和)
print(np.sum(arr, axis=0)) # 输出:[15 18 21 24 27]



# 沿轴1求和(跨列,即每行求和)
print(np.sum(arr, axis=1)) # 输出:[10 35 60]

# 求最大值
print(np.max(arr)) # 输出:14
print(np.max(arr, axis=0)) # 输出:[10 11 12 13 14]

参数axis是关键:axis=0表示沿着第0轴(行方向)操作,结果数组的该维度会被“压缩”掉;axis=1表示沿着第1轴(列方向)操作。
第八部分:实战练习——实现Softmax函数
现在,让我们运用所学的NumPy知识来实现一个机器学习中常用的函数——Softmax。Softmax函数可以将一组任意实数转换为一组概率分布,所有输出值在(0,1)之间,且和为1。
Softmax的公式为:
S(x_i) = exp(x_i) / Σ(exp(x_j)),其中j遍历所有元素。

以下是实现步骤:

- 给定一个输入数组(可能包含正负数)。
- 对每个元素求指数(
np.exp),确保所有值为正。 - 计算所有指数值的和。
- 将每个指数值除以总和,得到概率。
def softmax(x):
# 防止数值溢出,进行稳定性处理:减去最大值
x_exp = np.exp(x - np.max(x))
return x_exp / np.sum(x_exp)
# 测试
x = np.array([1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0])
probabilities = softmax(x)
print(“输入:”, x)
print(“Softmax概率:”, probabilities)
print(“概率和:”, np.sum(probabilities)) # 应非常接近1.0


# 也可以处理二维数组(如多分类问题中每个样本的得分)
scores = np.random.randn(3, 5) # 3个样本,5个类别得分
probs = softmax(scores) # 对每一行(axis=1)计算softmax
print(“\n样本得分矩阵形状:”, scores.shape)
print(“Softmax概率矩阵形状:”, probs.shape)
print(“每行概率和:”, np.sum(probs, axis=1)) # 每行和都应接近1
这个练习综合运用了数组创建、数学运算、广播机制和聚合函数,是检验NumPy掌握程度的绝佳方式。
课程总结与作业 🎯
本节课中我们一起学习了Python科学计算的核心库NumPy。我们从理解为什么需要NumPy开始,逐步掌握了ndarray数组的构建、属性查看、索引切片、维度变换以及各种数学运算。最后,我们通过实现Softmax函数将理论知识应用于实践。


本节课核心要点总结:
- NumPy的
ndarray对象是高效数值计算的基础。 - 掌握数组的
shape、ndim、size等属性。 - 熟练使用索引和切片访问、修改多维数组数据。
- 理解并运用
reshape、flatten进行维度操作。 - 掌握元素级运算、矩阵乘法和沿轴聚合计算。
- 理解广播机制,它使数组与标量或不同形状数组间的运算变得简单。
课后作业:
请独立完成一个Softmax函数的实现,并对其进行测试。
- 实现函数
softmax(x),能正确处理一维和二维输入。 - 对二维输入,确保对每一行独立计算概率分布(即
axis=1)。 - 生成一些随机测试数据(包括正负数),验证你的函数输出概率和为1。

通过完成这个作业,你将能巩固本节课关于NumPy数组操作和运算的所有关键知识。科学计算是数据科学和人工智能的基石,熟练掌握NumPy将为你的后续学习铺平道路。


教程内容根据“人工智能—Python AI公开课(七月在线出品)- P3:三节课上手Python第三节”视频内容整理翻译而成。
人工智能—Python AI公开课(七月在线出品) - P4:瑞士军刀pandas之数据分类 📊
在本节课中,我们将要学习pandas库中三个强大的数据处理操作:groupby、aggregate和transform。我们将通过具体的例子,学习如何对数据进行分类、汇总和逐行转换,从而更深入地理解和分析数据。
概述
在前面的课程中,我们已经初步了解了pandas的核心数据结构:一维的Series和二维的DataFrame。DataFrame是我们进行数据分析的主要工具。本节课将围绕DataFrame展开,重点介绍如何对数据进行分组、聚合和转换操作。
创建示例数据
在开始学习具体操作之前,我们首先需要创建一个示例数据集。假设我们有一个名为“七月在线”的公司,我们需要一张表格来记录每位员工的年度薪水和奖金流水。
以下是创建该数据集的代码:
import pandas as pd
import numpy as np
# 创建一个包含员工信息的DataFrame
salaries = pd.DataFrame({
'name': ['July', 'July', 'Han', 'Han', 'July', 'Zewei', 'Zewei', 'Zewei', 'July', 'Han'],
'year': [2016, 2016, 2016, 2017, 2017, 2016, 2017, 2017, 2016, 2017],
'salary': [80000, 82000, 70000, 72000, 85000, 90000, 95000, 92000, 81000, 73000],
'bonus': [5000, 5500, 4500, 4800, 6000, 8000, 8500, 8200, 5200, 4700]
})
数据分组:groupby
上一节我们创建了示例数据,本节中我们来看看如何使用groupby对数据进行分类。groupby操作的核心思想是按照一个或多个列的值,将数据分成不同的组。
基本分组操作
以下是如何按照员工姓名(name列)进行分组:
grouped_by_name = salaries.groupby('name')
print(type(grouped_by_name))
执行上述代码后,你会得到一个DataFrameGroupBy对象。这个对象本身并不直接显示数据,但它已经将数据按照姓名分成了不同的组。
理解groupby对象
groupby对象内部存储了分组信息。我们可以通过.groups属性查看每个组包含哪些数据行(通过索引标识)。
print(grouped_by_name.groups)
输出结果会显示每个姓名对应的数据行索引,这有助于我们理解groupby是如何工作的:它遍历DataFrame,根据指定列的值将行索引归类到不同的组中。
数据聚合:aggregate
在将数据分组之后,我们通常希望对每个组进行汇总计算,这就是聚合(aggregate)操作。聚合操作可以与groupby无缝衔接。
基本聚合操作
以下是对分组后的数据求和:
# 对每个组的所有数值列求和
sum_result = salaries.groupby('name').sum()
print(sum_result)
# 只对指定的列(salary和bonus)求和
sum_specific = salaries.groupby('name')[['salary', 'bonus']].sum()
print(sum_specific)
控制排序和多种聚合函数
默认情况下,groupby的结果会按分组键排序。你可以通过sort=False参数禁用排序。
# 不排序的分组求和
sum_no_sort = salaries.groupby('name', sort=False).sum()
print(sum_no_sort)
aggregate(或简写为agg)方法可以接受一个函数,对每个组应用该函数。你甚至可以传递一个函数列表,一次性计算多个聚合指标。
以下是常用的聚合函数示例:
grouped = salaries.groupby('name')
# 计算每个组的大小(记录数)
print(grouped.size())
# 计算平均值
print(grouped.mean())
# 计算中位数
print(grouped.median())
# 计算标准差
print(grouped.std())
# 生成描述性统计摘要
print(grouped.describe())
你还可以一次性计算多个聚合指标:
# 同时计算最小值、标准差和总和
multi_agg = salaries.groupby('name').agg([np.min, np.std, np.sum])
print(multi_agg)
遍历与选择分组
有时我们需要手动检查或处理每个分组。groupby对象支持迭代,并且可以按组名选择特定的组。
以下是遍历分组的方法:
# 遍历每个分组
for name, group in salaries.groupby('name'):
print(f"Group name: {name}")
print(group)
print(f"Type of group: {type(group)}")
print("-" * 20)
你也可以通过组名直接获取特定的分组:
# 获取名为‘Han’的分组数据
han_group = salaries.groupby('name').get_group('Han')
print(han_group)
多列分组
groupby不仅支持单列分组,也支持基于多列的组合进行分组。这在分析多维数据时非常有用。
以下是如何按照姓名和年份进行分组:
# 按照姓名和年份分组
grouped_multi = salaries.groupby(['name', 'year'])
# 对分组后的数据求和
sum_multi = grouped_multi.sum()
print(sum_multi)
数据转换:transform
上一节我们介绍了如何对分组数据进行聚合汇总,本节中我们来看看transform操作。transform会对DataFrame中的每一行数据应用一个函数,通常与groupby结合使用,在组内进行标准化或计算相对值。
transform的基本用法
假设我们想计算每位员工薪水相对于其所在年份平均薪水的Z-score(标准分数)。这需要先按年份分组,然后在组内进行转换。
首先,我们加载一个更复杂的数据集(英伟达股票数据)来演示:
# 读取股票数据,并将日期列解析为时间索引
nvda = pd.read_csv('data/NVDA.csv', index_col=0, parse_dates=['Date'])
nvda.index = pd.to_datetime(nvda.index)
接下来,定义一个按年份分组的键,并计算Z-score:
# 定义一个函数,返回每个日期对应的年份
key = lambda x: x.year
# 定义Z-score转换函数
z_score = lambda x: (x - x.mean()) / x.std()
# 按年份分组,并对‘Adj Close’列进行Z-score转换
transformed = nvda.groupby(key)['Adj Close'].transform(z_score)
print(transformed.head())
结合groupby与transform的更多例子
transform非常灵活。例如,我们可以计算每年股价的波动范围(最高价-最低价),并将这个值赋给该年的每一天。
# 计算每年股价的范围(最大值-最小值)
price_range = lambda x: x.max() - x.min()
yearly_range = nvda.groupby(key)['Adj Close'].transform(price_range)
print(yearly_range.head())
数据可视化初探
为了更直观地展示数据,我们可以使用pandas内置的绘图功能进行简单的可视化。这有助于我们快速理解数据的分布和趋势。
首先,确保在Jupyter Notebook中启用内联绘图:
%matplotlib inline
然后,我们可以绘制股票调整后的收盘价:
# 绘制英伟达调整后的收盘价走势图
nvda['Adj Close'].plot(grid=True, figsize=(10, 6))
我们还可以将原始数据与转换后的数据(如Z-score)进行对比绘图:
# 创建一个包含原始数据和转换后数据的DataFrame用于对比
compare = pd.DataFrame({
'Original': nvda['Adj Close'],
'Transformed (Z-score)': transformed
})
compare.plot(grid=True, figsize=(12, 8))
实战练习:创建月K线图
最后,我们通过一个综合练习来巩固所学知识:将每日股票数据聚合为月度数据,并绘制月K线图。
以下是实现步骤:
# 1. 按年份和月份分组,并取每月最后一天的收盘价
monthly_nvda = nvda.groupby([lambda x: x.year, lambda x: x.month]).last()
# 2. 提取调整后的收盘价
monthly_adj_close = monthly_nvda['Adj Close']
# 3. 为月度数据创建更友好的时间索引(如‘1999-01’)
new_index = pd.PeriodIndex([f'{i[0]}-{i[1]:02d}' for i in monthly_adj_close.index], freq='M')
monthly_adj_close.index = new_index
# 4. 绘制月度收盘价走势图
monthly_adj_close.plot(grid=True, figsize=(12, 6))
总结
本节课中我们一起学习了pandas中三个核心的数据处理操作:
groupby:用于根据一个或多个列的值对数据进行分类。aggregate(或agg):用于对分组后的数据进行汇总计算,如求和、求平均值、计算标准差等。transform:用于对DataFrame的每一行数据进行转换,常与groupby结合在组内进行标准化等操作。


我们通过员工薪水数据和股票数据的例子,详细讲解了这些方法的用法和组合技巧。掌握这些操作,你将能更高效地对数据进行分类、汇总和转换,为后续更复杂的数据分析任务打下坚实的基础。
人工智能—Python AI公开课(七月在线出品) - P5:瑞士军刀pandas之数据整合 📊
在本节课中,我们将要学习如何使用pandas库进行数据整合,这是数据分析中连接和组合多个数据集的关键技能。我们将重点介绍三个核心函数:concat、merge和join,并通过实际代码演示它们的使用方法。
导入库与创建示例数据
首先,我们需要导入必要的库并创建一些示例数据框(DataFrame)用于演示。
import numpy as np
import pandas as pd
我们创建三个简单的DataFrame,分别代表不同城市的一些数据。
# 创建第一个DataFrame
df1 = pd.DataFrame({
‘apartments‘: [50000, 60000],
‘car‘: [300000, 350000]
}, index=[‘上海‘, ‘北京‘])
# 创建第二个DataFrame
df2 = pd.DataFrame({
‘apartments‘: [45000, 55000],
‘car‘: [280000, 320000]
}, index=[‘广州‘, ‘深圳‘])
# 创建第三个DataFrame
df3 = pd.DataFrame({
‘apartments‘: [40000],
‘car‘: [250000]
}, index=[‘杭州‘])
使用concat进行数据拼接
concat函数主要用于沿特定轴(行或列)拼接多个pandas对象。
按行拼接(纵向拼接)
默认情况下,concat按行(axis=0)进行拼接,即将数据框一个接一个地堆叠起来。
result = pd.concat([df1, df2, df3])
print(result)
执行上述代码后,df1、df2和df3会按顺序纵向拼接成一个大的DataFrame。
指定拼接键(keys)
我们可以为每个被拼接的部分指定一个键(key),这会在结果中创建一个多级索引(MultiIndex)。
result_with_keys = pd.concat([df1, df2, df3], keys=[‘x‘, ‘y‘, ‘z‘])
print(result_with_keys)
现在,结果的第一层索引是我们指定的键(‘x‘, ‘y‘, ‘z‘),第二层是原始索引。我们可以通过.loc方法访问特定部分。
# 访问键为 ‘y‘ 的部分,即 df2
df2_extracted = result_with_keys.loc[‘y‘]
print(df2_extracted)
按列拼接(横向拼接)
通过设置参数axis=1,我们可以进行横向拼接,即按列合并。
首先,我们创建一个包含薪水信息的新DataFrame。
df4 = pd.DataFrame({
‘salary‘: [15000, 18000, 16000, 14000, 13000]
}, index=[‘苏州‘, ‘北京‘, ‘上海‘, ‘广州‘, ‘天津‘])
现在,我们将result(之前的纵向拼接结果)与df4进行横向拼接。
result_horizontal = pd.concat([result, df4], axis=1)
print(result_horizontal)
观察结果,只有索引匹配的城市(如‘上海‘、‘北京‘)的数据会被对齐,不匹配的索引位置会用NaN(空值)填充。
控制拼接方式:inner join
默认的拼接是outer join,保留所有索引。我们可以通过join参数指定为inner,只保留双方都匹配的索引。
result_inner = pd.concat([result, df4], axis=1, join=‘inner‘)
print(result_inner)
这样,结果中只包含‘北京‘、‘上海‘、‘广州‘这三个在两个DataFrame中都存在的城市。
指定拼接轴(join_axes)
我们还可以指定以哪个DataFrame的索引为准进行拼接。这个功能在较新版本的pandas中已被弃用,更推荐使用merge或join方法。
# 以 result 的索引为准(保留所有result中的行)
result_left = pd.concat([result, df4], axis=1, join_axes=[result.index])
print(result_left)
# 以 df4 的索引为准(保留所有df4中的行)
result_right = pd.concat([result, df4], axis=1, join_axes=[df4.index])
print(result_right)
使用append方法
append是concat在行方向(axis=0)上的一个简便方法,用于在DataFrame末尾添加行。
# 将 df2 追加到 df1 的末尾
appended_df = df1.append(df2)
print(appended_df)
# 同时追加多个DataFrame
appended_multiple = df1.append([df2, df3])
print(appended_multiple)
拼接Series与DataFrame
concat也可以用于拼接Series和DataFrame。
# 创建一个Series
s1 = pd.Series([60, 50], index=[‘上海‘, ‘北京‘], name=‘meal‘)
print(s1)
# 将Series与DataFrame按列拼接
combined = pd.concat([df1, s1], axis=1)
print(combined)
如果Series的索引能与DataFrame的列名匹配,也可以按行追加。
# 创建一个索引为列名的Series
s2 = pd.Series([18000, 12000], index=[‘apartments‘, ‘car‘], name=‘厦门‘)
print(s2)
# 将Series追加为一行
df_with_series_row = df1.append(s2)
print(df_with_series_row)
上一节我们介绍了使用concat进行各种拼接操作,本节中我们来看看功能更强大的merge函数。
使用merge进行数据合并
merge函数用于基于一个或多个键(key)将两个DataFrame的行连接起来,它比concat更灵活,不局限于索引对齐。
首先,我们调整一下之前数据的结构,将索引重置为普通列,以便演示基于列的合并。
# 重置 result 的索引,并将列重命名为 ‘cities‘
result_reset = result.reset_index().rename(columns={‘index‘: ‘cities‘})
print(result_reset)
# 同样处理 df4
df4_reset = df4.reset_index().rename(columns={‘index‘: ‘cities‘})
print(df4_reset)
现在,我们使用merge函数,基于‘cities‘列合并这两个DataFrame。
# 基于 ‘cities‘ 列进行合并,默认是 inner join
merged_inner = pd.merge(result_reset, df4_reset, on=‘cities‘)
print(merged_inner)
指定合并方式(how参数)
merge的how参数用于指定合并类型。
以下是几种常见的合并方式:
inner:只保留两个DataFrame中键匹配的行(交集)。outer:保留所有行,不匹配的位置用NaN填充(并集)。left:以左边DataFrame的键为准,保留所有左表的行。right:以右边DataFrame的键为准,保留所有右表的行。
# 使用 outer join
merged_outer = pd.merge(result_reset, df4_reset, on=‘cities‘, how=‘outer‘)
print(merged_outer)
# 使用 left join (以 result_reset 为主)
merged_left = pd.merge(result_reset, df4_reset, on=‘cities‘, how=‘left‘)
print(merged_left)
# 使用 right join (以 df4_reset 为主)
merged_right = pd.merge(result_reset, df4_reset, on=‘cities‘, how=‘right‘)
print(merged_right)
了解了基于列的合并后,我们来看看如何基于索引进行合并,这时join函数就派上用场了。
使用join进行索引合并
join方法是merge的一个便捷用法,专门用于基于索引合并DataFrame。
首先,我们将DataFrame的索引设置回‘cities‘。
# 将 ‘cities‘ 列重新设置为 df1 的索引
df1_indexed = df1.copy()
# df1 原本就有城市索引,这里确认一下
# 将 ‘cities‘ 列重新设置为 df4 的索引
df4_indexed = df4_reset.set_index(‘cities‘)
print(df4_indexed)
现在,使用join方法基于索引进行合并。
# 默认是 left join,以调用者(df1)的索引为准
joined_default = df1.join(df4_indexed)
print(joined_default)
同样,join方法也支持how参数来指定合并类型。
# 使用 outer join
joined_outer = df1.join(df4_indexed, how=‘outer‘)
print(joined_outer)
# 使用 right join (以 df4_indexed 的索引为准)
joined_right = df1.join(df4_indexed, how=‘right‘)
print(joined_right)
实际上,merge函数也可以通过指定left_index和right_index参数来实现基于索引的合并。
# 使用 merge 实现基于索引的 outer join
merged_by_index = pd.merge(df1, df4_indexed, left_index=True, right_index=True, how=‘outer‘)
print(merged_by_index)
可以看到,concat、merge和join三者功能有重叠,许多操作可以通过不同方法实现。掌握它们各自的特点和适用场景是关键。
实战项目:整合与可视化多只股票数据
最后,我们将运用今天所学的数据整合知识,完成一个小型实战项目:下载并整合多只股票的历史数据,然后进行可视化比较。
以下是实现步骤:
- 读取股票数据:我们从本地CSV文件读取Google、Apple和Microsoft的股票历史数据。
- 数据清洗:处理数据中的异常值(例如,Apple数据中的字符串‘null‘)。
- 数据整合:使用
concat函数将三只股票的调整后收盘价合并到一个DataFrame中。 - 数据筛选与可视化:筛选出Google上市后的数据,并绘制在一张图中进行比较。
import matplotlib.pyplot as plt
# 确保图表能内嵌显示在Notebook中
%matplotlib inline
# 1. 读取数据
google = pd.read_csv(‘data/goog.csv‘, index_col=0, parse_dates=[‘Date‘])
apple = pd.read_csv(‘data/aapl.csv‘, index_col=0, parse_dates=[‘Date‘])
microsoft = pd.read_csv(‘data/msft.csv‘, index_col=0, parse_dates=[‘Date‘])
# 2. 数据清洗 (处理Apple数据中的 ‘null‘ 字符串)
# 将 ‘null‘ 替换为真正的NaN
apple[‘Adj Close‘] = apple[‘Adj Close‘].replace(‘null‘, np.nan)
# 将列转换为浮点数类型,无法转换的(NaN)保持原样
apple[‘Adj Close‘] = pd.to_numeric(apple[‘Adj Close‘], errors=‘coerce‘)
# 使用前向填充法填充NaN值
apple[‘Adj Close‘].fillna(method=‘ffill‘, inplace=True)
# 3. 提取需要的列并重命名
goog_adj_close = google[[‘Adj Close‘]].copy()
goog_adj_close.columns = [‘GOOG‘]
aapl_adj_close = apple[[‘Adj Close‘]].copy()
aapl_adj_close.columns = [‘AAPL‘]
msft_adj_close = microsoft[[‘Adj Close‘]].copy()
msft_adj_close.columns = [‘MSFT‘]
# 使用 concat 进行横向合并
stocks = pd.concat([aapl_adj_close, goog_adj_close, msft_adj_close], axis=1)
print(stocks.head())
# 4. 筛选数据(取Google上市后的数据)
# 找到GOOG列第一个非NaN值的索引日期
start_date = stocks[‘GOOG‘].first_valid_index()
# 筛选该日期之后的数据
stocks_filtered = stocks.loc[stocks.index >= start_date]
# 5. 绘制图表
plt.figure(figsize=(12, 6))
stocks_filtered.plot(grid=True)
plt.title(‘Stock Price Comparison (AAPL, GOOG, MSFT)‘)
plt.ylabel(‘Adjusted Close Price‘)
plt.xlabel(‘Date‘)
plt.show()
运行以上代码,我们将得到一张从2004年Google上市至今,三家公司股价走势的对比图。通过这个项目,我们实践了数据读取、清洗、整合(使用concat)和可视化的完整流程。
总结
本节课中我们一起学习了pandas中数据整合的三大核心工具。
concat:主要用于沿轴(行或列)简单堆叠多个DataFrame或Series,操作直观,适用于结构相似的数据集拼接。merge:功能强大的合并函数,基于一个或多个键(列)连接行,支持多种连接类型(inner, outer, left, right),适用于数据库风格的合并操作。join:merge的便捷方法,专门用于基于索引进行合并,语法更简洁。
我们还通过一个股票数据分析的实战项目,综合运用了concat等方法,将多个数据源整合后进行可视化分析。这些数据整合技能是进行复杂数据分析的基础,希望同学们能多加练习,熟练掌握。
提示:要深入了解这些函数的全部参数和高级用法,建议查阅官方文档(例如,搜索“pandas concat”、“pandas merge”)。


好,那我们今天的课就讲到这里。😊
人工智能—Python AI公开课(七月在线出品) - P6:Seaborn可视化数据分析 📊
在本节课中,我们将要学习如何使用Seaborn库进行高级的数据可视化,特别是针对双变量分布的分析。我们将探索如何直观地展示两个变量之间的关系,并学习处理不同数据量场景下的绘图技巧。
双变量分布分析 🔍
上一节我们介绍了单变量分布的绘制方法。本节中我们来看看如何分析两个变量的联合分布。如果仅使用基础方法,绘制两个变量的分布会有些麻烦。
Seaborn库提供了一个非常棒的功能来处理这个问题,它叫做“双变量分布”。更棒的是,如果你使用Seaborn中的jointplot函数来绘制双变量分布,可以直接传入一个DataFrame数据框,并指定两个列名。
以下是生成示例数据并绘制双变量分布图的代码:
import seaborn as sns
import pandas as pd
import numpy as np
# 生成示例数据
mean = [0, 0]
cov = [[1, 0.5], [0.5, 1]]
data = np.random.multivariate_normal(mean, cov, 200)
df = pd.DataFrame(data, columns=[‘X‘, ‘Y‘])
# 绘制双变量分布图
sns.jointplot(x=‘X‘, y=‘Y‘, data=df)
jointplot函数可以分别绘制两个变量的分布图,一个横向,一个纵向。你可以通过交换x和y参数的顺序来旋转图形。
散点图与相关性分析 📈
上节课我们还讲解了散点图。散点图通常用于分析两个变量之间的相关性。

jointplot默认就会绘制一个散点图,展示X和Y的分布关系,同时将X和Y各自的直方图画在两侧。
但是,散点图在处理大量数据点时存在局限性。例如,当你有20万甚至200万个数据点时,绘制散点图会非常耗时,并且最终图形会因点过于密集而难以辨认。

六边形分箱图 🐝
为了解决大数据量下的可视化问题,Seaborn提供了“六边形分箱图”。它不直接绘制每一个点,而是统计每个六边形区域内数据点的数量,并用颜色的深浅来表示数量的多少。
以下是绘制六边形分箱图的代码:
sns.jointplot(x=‘X‘, y=‘Y‘, data=df, kind=‘hex‘)
颜色越深,表示该区域的数据点越集中。通过结合两侧的分布图,你同样可以清晰地判断两个变量之间的相关性。例如,如果数据点集中分布在一条斜线附近,则说明两个变量相关性较强。


核密度估计图 🗺️
如果你希望得到一个更平滑、更连续的分布估计,可以使用“核密度估计图”。它类似于地图上的等高线,密度越高的地方颜色越深。
以下是绘制核密度估计图的代码:




sns.jointplot(x=‘X‘, y=‘Y‘, data=df, kind=‘kde‘)
此时,图形两侧的直方图也会被替换为平滑的核密度估计曲线。这幅图能让你更直观地理解数据的联合概率分布。
增强可视化效果 ✨


Seaborn允许你以多种方式增强可视化效果,以便更清楚地了解数据分布。

你可以将原始数据点(实例)标记在图形边缘,作为分布的辅助参考。这类似于单变量分布图中的rug参数。

# 绘制核密度估计图,并在边缘标注数据点
sns.kdeplot(df[‘X‘], df[‘Y‘])
sns.rugplot(df[‘X‘], color=“g“, axis=‘x‘)
sns.rugplot(df[‘Y‘], color=“r“, axis=‘y‘)
你还可以调整图形的样式,例如将背景设置为黑色,用亮度来体现密度。
# 设置黑色背景并绘制核密度估计图
sns.set_style(“dark“)
sns.kdeplot(df[‘X‘], df[‘Y‘], shade=True, cmap=“Reds“)
此外,你甚至可以将散点与核密度估计图叠加,更清晰地看到数据点的原始位置与密度分布的关系。但请注意,在数据量极大时,应避免叠加散点图,以免影响性能。
成对关系图 🤝
在数据分析中,我们经常需要了解数据集中多个数值变量两两之间的关系。Seaborn的pairplot函数完美地解决了这个问题,它无需你手动编写循环。
pairplot函数接受一个包含多列数值数据的DataFrame,并绘制出所有数值列两两之间的散点图,以及对角线上每个变量自身的分布直方图。
以下是使用pairplot的示例代码:
# 使用鸢尾花数据集
iris = sns.load_dataset(‘iris‘)
sns.pairplot(iris)
- 对角线上的图是每个变量自身的分布(直方图)。
- 非对角线上的图是任意两个变量之间的散点图,用于观察相关性。
除了基本的散点图矩阵,你还可以指定kind=‘kde‘参数,绘制出所有变量对的核密度估计图,得到一个更平滑、连续的视图。
sns.pairplot(iris, kind=‘kde‘)
pairplot会智能地处理图形,只计算一半的矩阵,然后通过镜像展示完整结果,并不会重复计算以节省时间。


本节课中我们一起学习了Seaborn中用于分析双变量及多变量分布的高级工具。我们掌握了:
- 使用
jointplot分析两个变量的联合分布,包括散点图、六边形分箱图和核密度估计图。 - 理解了不同绘图方法(离散 vs. 连续)适用于不同的数据量和分析需求。
- 学会了使用
pairplot快速可视化数据集中所有数值变量对之间的关系。
记住,可视化的核心目的是帮助我们理解数据的分布和变量间的关联。选择哪种绘图方式取决于你的数据特点和分析习惯。


人工智能—Python AI公开课(七月在线出品) - P7:如何从Python起步学习AI 🚀

在本节课中,我们将要学习人工智能(AI)的基本概念、发展历程,以及如何规划一条从Python编程开始,系统学习AI的路径。课程内容将涵盖机器学习的核心思想、分类方法,并为你构建一个清晰的学习框架。
人工智能的发展背景 📜


人工智能并非一个全新的概念。它在上世纪50至60年代就已提出,并经历了两次发展高峰。我们目前正处于以2006年深度学习理论提出、2013年在图像识别领域取得突破为标志的第三波发展浪潮中。
这次浪潮的兴起,不仅得益于理论的进步,更依赖于硬件计算资源(如GPU)的飞速发展和大数据平台的成熟。计算能力与数据量的同步提升,共同推动了人工智能技术在社会和工业界的广泛应用。
人工智能的发展路径 🛤️


人工智能的终极目标是让机器在能力上接近并超越人类。过去的计算机擅长存储和计算,但在处理人类轻而易举的任务(如识别面孔)时却非常困难。在当前的第三波浪潮中,计算机开始在这些方面发力。
机器学习已经深度渗透到我们的日常生活中。例如,支付宝的面部识别、网易云音乐的歌曲推荐、电商平台的商品推荐等,都应用了机器学习技术。
什么是机器学习? 🤔
我们可以从人类学习经验的角度来理解机器学习。人类通过总结规律来学习,例如“瑞雪兆丰年”或“朝霞不出门,晚霞行千里”。这些谚语就是经验的总结。
机器学习让计算机也能做类似的事情。它从数据中学习一套规律,以完成我们设定的目标。这个过程可以量化成一个学习模型。例如,预测明年收成,我们可以将“是否下雪”、“雪量”等作为数据特征,将“是否丰收”作为目标,让机器找出它们之间的关系。
在人类经验中,智慧通过“阅历”(如“吃亏是福”)来传递;在机器中,智慧则体现为学习到的权重值和数字。调整这些权重,就是机器积累“经验”的过程。

人工智能、机器学习与深度学习的关系 🎯

这几个概念是包含关系:
- 人工智能:一个非常宽泛的领域。
- 机器学习:实现人工智能的一种核心方法。
- 深度学习:是机器学习中“神经网络”方法的一个强大分支。因其在图像、语音、自然语言处理等领域的卓越表现,而被单独强调。
简单来说,深度学习是机器学习的一种,而机器学习是实现人工智能的一种方式。
机器学习的分类 📊


机器学习主要分为三大类:监督学习、无监督学习和强化学习。本节我们重点介绍前两类。
监督学习


监督学习是最常用的方法。它的核心思想是:我们提供带有“标准答案”的数据,让机器学习数据与答案之间的关系。

上一节我们介绍了机器学习的思想,本节中我们来看看监督学习的具体例子。
以“瑞雪兆丰年”为例,构建一个监督学习模型:
- 训练数据:收集1980年至2017年每年的冬季数据(特征:是否下雪、雪量等)和次年收成数据(标签:是否丰收,可用0/1表示)。
- 学习过程:使用一个机器学习算法(如逻辑回归),学习特征(X)与标签(Y)之间的关系,即求解一个方程式
Y = f(X)。求得的方程系数就是权重值,即机器学到的“经验”。 - 预测:将2017年的冬季数据输入学习好的模型,即可预测2018年的收成。
监督学习主要用于两类问题:
- 分类:预测目标变量是离散值(如猫/狗/猪,是否丰收)。
- 回归:预测目标变量是连续值(如房价、销售额)。
以下是监督学习的核心流程公式化表示:
给定训练数据集 {(x1, y1), (x2, y2), ..., (xn, yn)}
目标:学习一个映射函数 f: X -> Y
使得对于新数据 x_new,能预测 y_new = f(x_new)
无监督学习

与监督学习不同,无监督学习的数据没有“标准答案”。它的目标是发现数据内部隐藏的结构或关系。

无监督学习主要有两类应用:
- 聚类:将相似的数据自动分组。例如,分析电商用户行为数据,自动发现喜欢同一类音乐或商品的用户群体。著名的“啤酒与尿布”案例就是聚类分析的结果。
- 降维:将高维数据压缩到低维空间,便于分析和可视化,同时尽可能保留数据的主要结构。常用方法有主成分分析(PCA)。
机器学习常见算法简介 ⚙️
以下是几种常见的机器学习算法:
- 决策树:模拟人类做决策的过程,通过一系列判断规则对数据进行分类。
- 回归(线性/逻辑回归):寻找特征与连续目标(线性回归)或分类概率(逻辑回归)之间的线性关系。
- 支持向量机:通过核函数将数据映射到高维空间,以找到最佳分割超平面,尤其擅长处理线性不可分问题。
- 神经网络/深度学习:模拟人脑神经元网络。输入层与权重矩阵相连,通过多层计算(隐藏层)后得到输出。层数加深即为“深度”学习。
- 朴素贝叶斯:基于特征相互独立的假设,利用贝叶斯定理计算样本属于某个类别的概率。常用于垃圾邮件过滤。


AI学习路径规划 🗺️
了解了AI的基本概念后,我们来看看如何系统性地学习。一个科学的学习路径可以避免盲目和跨阶段学习,提升效率。以下是一个分为四个阶段的学习规划:
第一阶段:Python编程基础、算法与数据结构
这是计算机科学的基础。Python是当前AI领域最主流的编程语言,语法简洁,且有强大的第三方库生态支持(如NumPy, Pandas, Scikit-learn, TensorFlow)。同时,掌握基本算法和数据结构是通过技术面试的必备条件。
第二阶段:数理统计基础

这是很多开发者的知识短板,却是理解机器学习算法原理的基石。例如,理解线性回归中的损失函数优化(梯度下降),就需要微积分和求导知识。扎实的数学基础能让你不仅知其然,更知其所以然。

第三阶段:机器学习算法
此阶段需要深入学习和实践各种核心算法,如逻辑回归、决策树、SVM、聚类等。不仅要会用库(如Scikit-learn)调用,更要理解其数学模型、损失函数和调优方法。
第四阶段:实战应用
将前三个阶段的知识融会贯通,通过完整的项目来检验学习成果。只有亲手完成一个项目,才能将分散的知识点串联起来,形成解决实际问题的能力,这也是求职和转行时的重要凭证。



本节课中我们一起学习了人工智能的概况、机器学习的核心思想与分类,并规划了一条从Python入门到AI实战的系统学习路径。记住,学习AI是一个循序渐进的过程,希望你能有一个坚实的起点,并最终通过项目实践收获丰硕的成果。
人工智能—Python AI公开课(七月在线出品) - P8:1小时带你熟练Python基础语法 🐍
在本节课中,我们将要学习Python的基础语法,包括注释、标识符、关键字、运算符、控制语句以及核心的数据结构。内容设计力求简单直白,让初学者能够轻松跟上。
概述 📋
Python是一门简洁而强大的编程语言。掌握其基础语法是进行后续人工智能和数据分析学习的基石。本节课程将系统性地介绍Python的核心语法元素,并通过实例帮助大家理解。
一、注释 📝
注释在每种编程语言中都有。它的作用主要是方便后期的代码阅读和维护。
注释在程序中不参与解释运行,也不会被输出。例如:
# 这是一行注释,说明下面代码的功能
print("hello world")
运行上述代码,只会输出 hello world,而注释内容不会被显示。它仅起到辅助说明的作用,在后期代码维护时非常重要。如果没有注释,可能连开发者自己都会忘记代码的意图。
二、标识符 🏷️
上一节我们介绍了注释,本节中我们来看看标识符。
标识符就是一个名字,由开发人员在程序中自定义的一些符号和名称构成。例如变量名、函数名等,都可以称为标识符。
标识符的规则分为两大类:固定规则和行业规则。
固定规则
标识符由字母、下划线和数字组成。这三者可以任意组合,但数字不能作为开头。
以下是标识符规则的一些例子(√ 表示合法,× 表示不合法):
myVariable(√)_private(√)var123(√)123var(×,数字开头)my-var(×,包含横线)my.var(×,包含点号)
行业规则(命名规范)
在实际开发中,需要遵循一些命名规范,通常在项目开始前团队会统一约定。
以下是常见的行业命名规范:
- 见名知意:尽量让标识符的名字直观反映其用途,例如
name表示姓名,student表示学生。 - 驼峰命名法:
- 小驼峰:第一个单词小写,后面每个单词首字母大写,例如
myFirstName。 - 大驼峰:每个单词的首字母都大写,例如
MyFirstName。
- 小驼峰:第一个单词小写,后面每个单词首字母大写,例如
- 下划线命名法:用下划线连接单词,例如
my_first_name。这是目前Python社区比较流行的方式。
三、关键字 🔑
关键字是一些具有特殊功能的标识符。我们只需要了解两点:哪些是关键字,以及关键字不能用作自定义的标识符。
例如,pass, and, or, not, True, False 等都是Python的关键字。在定义变量或函数名时,应避免使用这些关键字,否则会引起歧义或错误。
查看Python中所有关键字的方法如下:
方法一:在终端中查看
# 进入Python交互环境后执行
import keyword
print(keyword.kwlist)
方法二:在代码文件中查看
import keyword
print(keyword.kwlist)
四、运算符 ⚙️
运算符用于执行程序代码运算。Python中常见的运算符有六类:算术运算符、赋值运算符、复合赋值运算符、比较运算符、逻辑运算符和位运算符。
1. 算术运算符
进行基本的数学运算。
+(加),-(减),*(乘),/(除)%(取余),//(取整),**(幂)
2. 赋值运算符
将右侧的值赋给左侧的变量。
=(赋值)
number = 10 # 将整数10赋值给变量number
3. 复合赋值运算符
将算术运算符和赋值运算符结合起来。
+=,-=,*=,/=,%=,//=,**=
c += a # 等价于 c = c + a
4. 比较运算符
比较两个值,返回布尔值(True 或 False)。
==(等于),!=(不等于)>(大于),<(小于)>=(大于等于),<=(小于等于)
注意:一个等号
=是赋值,两个等号==才是比较是否相等。
5. 逻辑运算符
用于连接多个条件,也返回布尔值。
and(与):两个条件都为真时返回真。or(或):至少一个条件为真时返回真。not(非):对条件取反。
注意:Python中使用关键字
and,or,not作为逻辑运算符,而不是符号&&,||,!。
6. 位运算符
直接对整数的二进制位进行操作,常用于底层优化。
&(按位与),|(按位或),^(按位异或)~(按位取反),<<(左移),>>(右移)
a = 60 # 二进制:0011 1100
b = 13 # 二进制:0000 1101
print(a & b) # 结果为 12 (二进制:0000 1100)
五、控制语句 🎮
控制语句用于控制程序的执行流程,主要包括条件判断和循环。
1. 条件判断 (if语句)
条件判断用于在满足特定条件时才执行某些代码。
Python中if语句有四种基本格式:
格式一:简单的if
if 条件:
# 条件成立时执行的代码
格式二:if...else
if 条件:
# 条件成立时执行的代码
else:
# 条件不成立时执行的代码
格式三:if...elif...else (多条件)
if 条件1:
# 条件1成立时执行
elif 条件2:
# 条件2成立时执行
else:
# 以上条件都不成立时执行
格式四:嵌套if
可以在一个if代码块中嵌入另一个if语句。
示例:判断年龄
age = 25
if age >= 18:
print("你已经成年了。")
2. 循环
循环用于重复执行某段代码。Python主要有两种循环:while 循环和 for 循环。
while循环
while 循环在条件为真时重复执行代码块。
# 计算1到100的和
i = 1
sum = 0
while i <= 100:
sum += i
i += 1
print(sum) # 输出 5050
for循环
for 循环常用于遍历序列(如字符串、列表等)。
# 遍历字符串
name = "julyedu.com"
for char in name:
print(char)
循环控制关键字
break:立即终止整个循环。continue:跳过本次循环的剩余语句,直接进入下一次循环。
# break 示例
for char in "julyedu.com":
if char == 'd':
break
print(char) # 只打印出 julye
# continue 示例
for char in "julyedu.com":
if char == 'd':
continue
print(char) # 跳过 'd',打印其他所有字符
注意:
break和continue只对所在的最内层循环起作用。编写循环时,务必确保有明确的退出条件,避免死循环。
六、数据结构 🗃️
Python内置了六种核心数据结构:数字(Number)、字符串(String)、列表(List)、元组(Tuple)、字典(Dictionary)、集合(Set)。
1. 数字 (Number)
用于存储数值,分为四类:
int(整数), 如10float(浮点数), 如5.5bool(布尔), 如True,Falsecomplex(复数), 如4+3j
使用 type() 函数可以查看变量的数据类型:
a = 20
b = 5.5
c = True
d = 4+3j
print(type(a)) # <class 'int'>
print(type(b)) # <class 'float'>
print(type(c)) # <class 'bool'>
print(type(d)) # <class 'complex'>
2. 字符串 (String)
字符串是由单引号或双引号括起来的字符序列。
my_str = "Hello, World!"
字符串操作:
- 下标索引:从0开始,通过
字符串名[索引]访问特定字符。 - 切片:用于截取字符串的一部分,语法为
[起始:结束:步长],遵循“左闭右开”原则。
name = "ABCDEF"
print(name[0]) # 输出 'A'
print(name[0:3]) # 输出 'ABC' (索引0,1,2)
print(name[::2]) # 输出 'ACE' (从头到尾,步长为2)
print(name[::-1]) # 输出 'FEDCBA' (反转字符串)
3. 列表 (List)
列表是用方括号 [] 括起来的、可变的、有序的元素集合,可以存储不同类型的数据。
my_list = ['小王', '小张', 1, True]
列表的常见操作(增删改查):
# 增加元素
my_list.append('新元素') # 在末尾添加
my_list.insert(1, '插入元素') # 在指定索引前插入
my_list.extend([7,8,9]) # 合并另一个列表
# 删除元素
del my_list[0] # 删除指定索引的元素
my_list.pop() # 删除并返回末尾元素
my_list.remove('小张') # 删除第一个匹配的元素
# 修改元素
my_list[0] = '修改后的值'
# 查询与排序
if '小王' in my_list:
print("存在")
my_list.sort() # 升序排序
my_list.reverse() # 反转列表
4. 字典 (Dictionary)
字典是用花括号 {} 括起来的、可变的、由键值对(key:value)组成的集合。键必须是不可变类型。
student = {'name': '小明', 'id': 100, 'sex': '男'}
字典的常见操作:
# 增/改:通过键直接赋值
student['age'] = 20 # 新增键值对
student['id'] = 200 # 修改已有键的值
# 删
del student['sex'] # 删除指定键值对
student.clear() # 清空字典
# 查
print(student['name']) # 直接通过键访问,键不存在会报错
print(student.get('name')) # 使用get方法,键不存在返回None
print(student.keys()) # 获取所有键
print(student.values()) # 获取所有值
print(student.items()) # 获取所有键值对(元组形式)
5. 元组 (Tuple)
元组是用圆括号 () 括起来的、不可变的、有序的元素集合。一旦创建,其元素不能修改。
my_tuple = (1, 2, 3, 'go', True)
元组操作:
print(my_tuple[0]) # 通过索引访问,输出 1
# my_tuple[0] = 100 # 错误!元组元素不可修改
del my_tuple # 可以删除整个元组
new_tuple = my_tuple + (4, 5) # 元组可以连接
6. 集合 (Set)
集合是用花括号 {} 或 set() 函数创建的、无序的、元素不重复的集合。常用于去重和关系测试。
my_set = {1, 2, 3, 3, 2} # 实际存储为 {1, 2, 3}
集合的常见操作:
# 增加元素
my_set.add(4)
my_set.update([5, 6, 7])
# 删除元素
my_set.remove(3) # 移除元素,不存在则报错
my_set.discard(10) # 移除元素,不存在也不报错
# 集合运算
set_a = {1, 2, 3}
set_b = {3, 4, 5}
print(set_a & set_b) # 交集 {3}
print(set_a | set_b) # 并集 {1, 2, 3, 4, 5}
print(set_a - set_b) # 差集 (在a中但不在b中) {1, 2}
总结 🎉
本节课中我们一起学习了Python的基础语法核心内容。我们从最基础的注释和标识符讲起,明确了代码书写规范。然后深入了解了关键字、各类运算符的用法。接着,我们掌握了控制程序流程的两种重要结构:条件判断(if)和循环(while, for)。最后,我们系统学习了Python的六大内置数据结构:数字、字符串、列表、元组、字典和集合,并了解了它们的基本特性和常用操作。


这些知识是编写任何Python程序的基石。理解并熟练运用它们,将为后续学习更高级的主题(如函数、面向对象编程、数据分析与人工智能库)打下坚实的基础。建议大家多动手练习,通过代码来巩固理解。
人工智能—Python AI公开课(七月在线出品) - P9:Pandas初步:Series与DataFrame操作 🐼
在本节课中,我们将要学习Pandas库的两个核心数据结构:Series和DataFrame。我们将从如何创建它们开始,逐步学习如何从中选择数据、进行赋值以及执行基本的数据运算。通过本教程,你将能够使用Pandas进行初步的数据操作。
导入必要的库
要使用Pandas,我们首先需要导入它。同时,我们也会导入NumPy库,因为Pandas的许多操作都构建在NumPy之上,并且后续可能会用到NumPy的功能。
import pandas as pd
import numpy as np
现在,我们已经导入了Pandas和NumPy这两个库,这是我们今天工作的基础。
认识Series数据结构 📊
Series是Pandas中的一维数据结构。在Python中,一维数据结构通常是一个列表(list)。Series可以看作是对列表的一种封装,它为每个数据元素添加了一个索引。
创建Series
以下是创建Series的几种方法。
方法一:使用列表创建
我们可以直接从一个列表创建一个Series。Pandas会自动为列表中的每个元素分配一个从0开始的整数索引。
my_list = [‘a‘, ‘b‘, 1, 2, 3]
print(type(my_list)) # 输出:<class ‘list‘>
my_series = pd.Series(my_list)
print(my_series)
输出结果中,列表被封装成了一个Series,每个元素都带有一个索引,数据类型为object,因为列表中混合了字符串和数字。
方法二:自定义索引
我们也可以在创建Series时,通过index参数自定义索引。
my_series_custom = pd.Series(my_list, index=[‘A‘, ‘B‘, ‘C‘, ‘D‘, ‘E‘])
print(my_series_custom)
print(type(my_series_custom))
现在,Series的索引变成了我们指定的[‘A‘, ‘B‘, ‘C‘, ‘D‘, ‘E‘]。
方法三:使用字典创建
由于Series本质上是一个键值对结构,我们也可以直接用一个字典来创建它。字典的键(key)会自动成为Series的索引。
city_prices = {‘Beijing‘: 80000, ‘Guangzhou‘: 30000, ‘Hangzhou‘: 20000, ‘Shanghai‘: 70000, ‘Suzhou‘: np.nan}
apartments = pd.Series(city_prices)
print(apartments)
print(type(apartments))
这样,我们就用字典创建了一个表示城市房价的Series。
从Series中选择数据
创建了Series之后,我们需要知道如何从中提取我们需要的数据。
通过索引选择单个值
我们可以像访问字典一样,通过索引键来获取对应的值。
print(apartments[‘Hangzhou‘]) # 输出:20000.0
通过索引列表选择多个值
我们可以传入一个索引列表,来获取多个值,返回的结果仍然是一个Series。
selected_cities = apartments[[‘Hangzhou‘, ‘Beijing‘, ‘Shenzhen‘]]
print(selected_cities)
print(type(selected_cities))
使用布尔索引进行条件筛选
布尔索引允许我们根据条件来筛选数据。例如,我们想筛选出所有房价低于5万元的城市。
# 首先,创建一个布尔序列
condition = apartments < 50000
print(condition)
# 然后,使用这个布尔序列来筛选数据
cheap_cities = apartments[condition]
print(cheap_cities)
apartments < 50000这个操作会返回一个布尔类型的Series,其中满足条件的索引对应True,否则为False。然后我们可以用这个布尔Series来索引原数据。
为Series中的数据赋值
对Series中的元素进行赋值非常直接,方法与读取数据类似。
为单个元素赋值
apartments[‘Shenzhen‘] = 55000
print(apartments[‘Shenzhen‘])
使用布尔索引为多个元素赋值
我们也可以利用布尔索引,一次性修改多个满足条件的值。
apartments[apartments < 50000] = 40000
print(apartments)
现在,所有房价低于5万元的城市都被设置成了4万元。
Series的基本数据运算
Series支持许多基本的数学运算,这些运算通常是向量化的,会作用在每一个元素上。
# 所有值除以2
print(apartments / 2)
# 所有值乘以2
print(apartments * 2)
# 计算平方,可以使用NumPy的函数
print(np.square(apartments))
# 或者使用Python的幂运算符
print(apartments ** 2)
Series之间的运算
我们还可以对两个Series进行运算。需要注意的是,运算会基于索引进行对齐。只有两个Series中都存在的索引,才会进行运算;不存在的索引,结果会显示为NaN(Not a Number)。
# 假设我们定义了另一个Series表示城市购车成本
car_costs = pd.Series({‘Beijing‘: 100000, ‘Guangzhou‘: 80000, ‘Shanghai‘: 120000, ‘Shenzhen‘: 90000})
# 计算在每个城市购买100平米房子和一辆车的总成本
total_cost = car_costs + apartments * 100
print(total_cost)
结果中,只有Beijing, Guangzhou, Shanghai, Shenzhen这四个在两个Series中都存在的城市有计算结果,其他城市的结果为NaN。
处理缺失数据(NaN)
在实际数据中,经常会出现缺失值,Pandas用NaN表示。Pandas提供了一些方法来检查和操作缺失值。
检查索引是否存在
print(‘Hangzhou‘ in apartments) # 输出:True
print(‘Hangzhou‘ in car_costs) # 输出:False
检查数据是否为NaN
isna()方法返回一个布尔Series,指示哪些元素是NaN。notna()则相反。
print(apartments.isna()) # 显示哪些是NaN
print(apartments.notna()) # 显示哪些不是NaN
我们可以利用这些布尔Series进行筛选。
# 筛选出非空值
valid_prices = apartments[apartments.notna()]
print(valid_prices)
# 筛选出空值(NaN)
missing_prices = apartments[apartments.isna()]
print(missing_prices)
还有其他等价的写法,例如apartments[apartments.isna() == False]也能筛选出非空值。
上一节我们详细介绍了Series的创建、选择、赋值和运算。接下来,我们将目光转向更强大的二维数据结构——DataFrame。
认识DataFrame数据结构 📈
DataFrame是Pandas中最重要的数据结构,它是一个二维的、表格型的数据结构,可以看作是由多个Series按列组合而成,类似于Excel表格或SQL数据库中的表。
创建DataFrame
创建DataFrame有多种方式,最常用的是通过字典来创建。
使用字典创建
字典的每个键值对代表一列数据,键是列名,值是一个列表,代表该列的所有数据。
data = {
‘city‘: [‘Beijing‘, ‘Shanghai‘, ‘Guangzhou‘, ‘Shenzhen‘, ‘Hangzhou‘, ‘Suzhou‘],
‘year‘: [2010, 2010, 2010, 2010, 2010, 2010],
‘population‘: [1961, 2302, 1270, 1036, 870, 534] # 单位:万,示例数据
}
frame_one = pd.DataFrame(data)
print(frame_one)
print(type(frame_one))
生成的DataFrame有行(自动生成的索引0,1,2...)和列(我们定义的city, year, population)。
指定列的顺序
在创建时,我们可以通过columns参数指定列的顺序。
frame_two = pd.DataFrame(data, columns=[‘year‘, ‘city‘, ‘population‘])
print(frame_two)
如果指定的列名在数据中不存在,Pandas会自动创建该列,并用NaN填充。
frame_three = pd.DataFrame(data, columns=[‘year‘, ‘city‘, ‘population‘, ‘debt‘])
print(frame_three)
指定行的索引
类似Series,我们也可以通过index参数为DataFrame指定自定义的行索引。
frame_four = pd.DataFrame(data, index=[‘one‘, ‘two‘, ‘three‘, ‘four‘, ‘five‘, ‘six‘])
print(frame_four)
从DataFrame中选择数据
DataFrame的数据选择更加灵活,因为我们需要同时考虑行和列。
选择单列数据
选择单列数据有两种常用方法,返回的是一个Series。
# 方法一:使用点号(属性访问方式),列名必须是有效的Python变量名
city_series = frame_two.city
print(city_series)
print(type(city_series)) # 输出:<class ‘pandas.core.series.Series‘>
# 方法二:使用方括号(字典访问方式)
city_series_alt = frame_two[‘city‘]
print(city_series_alt)
选择多列数据
要选择多列,需要传入一个列名的列表,返回的结果是一个新的DataFrame。

sub_frame = frame_two[[‘city‘, ‘population‘]]
print(sub_frame)
print(type(sub_frame)) # 输出:<class ‘pandas.core.frame.DataFrame‘>

本节课中我们一起学习了Pandas库的两个基石:Series和DataFrame。我们掌握了如何创建它们,如何通过索引、布尔索引等方式灵活地选择和筛选数据,以及如何进行基本的数据运算和缺失值处理。DataFrame作为二维表格,其数据选择方式更为丰富,我们学习了如何选取单列和多列数据。这些是进行数据分析和处理的第一步,熟练掌握它们将为后续更复杂的数据操作打下坚实的基础。
人工智能—推荐系统公开课(七月在线出品) - P1:从零开始写代码:使用MovieLens的电影评分数据实践FM 🎬








在本节课中,我们将要学习如何从零开始,使用Python代码实现一个基础的因子分解机(FM)模型。我们将使用MovieLens的电影评分数据集,通过实践来理解FM模型的核心思想、参数更新过程以及如何评估模型效果。






概述





我们将从下载和读取数据集开始,逐步构建一个简化的FM模型。这个模型将专注于用户ID和电影ID这两个特征,通过它们的隐向量内积来预测评分。我们会定义损失函数,实现梯度下降算法进行训练,并最终在训练集和测试集上评估模型的性能。







数据准备与读取


首先,我们需要获取并理解数据。我们使用MovieLens数据集中的一个评分文件。该文件的结构很简单,每一行包含三个字段:用户ID、电影ID和评分(1-5分),以及一个时间戳。





为了简化模型,我们只使用评分数据。以下是读取数据文件的函数实现:


def load_data(filepath):
data = []
with open(filepath, 'r') as f:
for line in f:
# 去除首尾空格,并按双冒号分割
parts = line.strip().split('::')
user_id = int(parts[0])
movie_id = int(parts[1])
rating = float(parts[2])
# 将三元组(用户ID, 电影ID, 评分)加入列表
data.append((user_id, movie_id, rating))
return data

运行这个函数后,我们可以打印前几项数据来确认读取是否正确。


定义模型参数与预测函数


上一节我们介绍了如何读取数据,本节中我们来看看如何定义模型的核心参数和预测函数。

我们首先定义几个超参数:
M: 用户数量(约6000)N: 电影数量(约3000)K: 隐向量的维度,这是一个需要通过交叉验证来选择的超参数。

我们使用numpy来存储和计算参数。用户隐向量矩阵 U 的形状是 M x K,电影隐向量矩阵 V 的形状是 N x K。我们使用高斯分布进行随机初始化。




import numpy as np




M = 6000
N = 3000
K = 5
reg = 0.01 # 正则化系数
step = 0.001 # 梯度下降步长




# 初始化参数矩阵
U = np.random.randn(M, K)
V = np.random.randn(N, K)





接下来,我们定义预测函数。为了专注于理解FM的二次项交互,我们暂时忽略线性项和常数项。在我们的简化场景中,只有用户ID和电影ID两个特征是非零的,因此FM的复杂求和公式退化为这两个特征对应隐向量的内积。





预测用户 i 对电影 j 的评分公式为:
prediction = np.dot(U[i], V[j])





对应的Python函数如下:




def predict(i, j):
"""预测用户i对电影j的评分"""
return np.dot(U[i], V[j])
定义损失函数与梯度
我们的目标是预测评分,这是一个回归问题,因此使用均方误差(MSE)作为损失函数,并加入L2正则化项以防止过拟合。


单个样本 (i, j, r) 的损失函数 L 定义为:
L = (prediction - r)^2 + reg * (||U[i]||^2 + ||V[j]||^2)





其中 r 是真实评分,prediction 是模型预测值。





为了使用梯度下降法优化参数,我们需要计算损失函数对参数 U[i] 和 V[j] 的梯度。






以下是梯度的计算公式:
- 对
U[i]的梯度g_i:g_i = 2 * (prediction - r) * V[j] + 2 * reg * U[i] - 对
V[j]的梯度g_j:g_j = 2 * (prediction - r) * U[i] + 2 * reg * V[j]






实现模型训练过程







有了梯度的计算公式,我们就可以实现训练函数了。我们将采用随机梯度下降(SGD)的方法,遍历数据集中的每一个样本,并根据计算出的梯度更新对应的参数。





以下是训练一个轮次(epoch)的代码:





def train_one_epoch(data):
"""遍历数据集一次,更新参数"""
for i, j, r in data:
p = predict(i, j) # 预测值
# 计算梯度
grad_u = 2 * (p - r) * V[j] + 2 * reg * U[i]
grad_v = 2 * (p - r) * U[i] + 2 * reg * V[j]
# 梯度下降更新参数
U[i] -= step * grad_u
V[j] -= step * grad_v




模型评估与效果验证






训练过程中,我们需要监控模型在训练集和测试集上的表现,以判断模型是否学习有效以及是否出现过拟合。我们实现两个辅助函数:一个用于在给定数据集上生成所有预测值,另一个用于计算均方根误差(RMSE)。



以下是相关函数:





def make_predictions(data):
"""为给定数据集生成预测列表"""
preds = []
for i, j, r in data:
preds.append(predict(i, j))
return preds






def rmse(data, predictions):
"""计算真实评分和预测评分之间的RMSE"""
total_error = 0.0
count = 0
for idx, (i, j, r) in enumerate(data):
p = predictions[idx]
total_error += (p - r) ** 2
count += 1
return np.sqrt(total_error / count)




为了进行可靠的评估,我们需要将数据集划分为训练集和测试集。例如,我们可以将前80万条数据作为训练集,后20万条数据作为测试集。





# 假设 data 是加载的全部数据
split_idx = 800000
train_data = data[:split_idx]
test_data = data[split_idx:]





# 训练前,计算初始RMSE
init_train_preds = make_predictions(train_data)
init_test_preds = make_predictions(test_data)
print(f"初始训练集RMSE: {rmse(train_data, init_train_preds):.4f}")
print(f"初始测试集RMSE: {rmse(test_data, init_test_preds):.4f}")






# 进行多轮训练
num_epochs = 10
for epoch in range(num_epochs):
train_one_epoch(train_data)
train_preds = make_predictions(train_data)
test_preds = make_predictions(test_data)
print(f"Epoch {epoch+1}: 训练集RMSE: {rmse(train_data, train_preds):.4f}, 测试集RMSE: {rmse(test_data, test_preds):.4f}")






超参数调试与分析


在运行上述代码后,我们可能会发现模型表现不佳或训练不稳定。这时就需要调整超参数。以下是一些关键的调试点:



- 参数初始化:如果初始RMSE很大(例如接近4),说明随机初始化的隐向量内积范围与真实评分(1-5)不匹配。可以尝试调整初始化尺度。
- 学习率(步长):步长
step过大可能导致梯度更新震荡甚至数值溢出;过小则会导致收敛缓慢。需要尝试不同的值。 - 隐向量维度 K:
K值增大模型容量,可能提高拟合能力但也更容易过拟合;K值减小则相反。需要通过交叉验证选择。 - 正则化系数 reg:
reg用于控制模型复杂度。将其设为0可以移除正则化,可能使训练集误差更小但测试集误差变大(过拟合);增大reg可以抑制过拟合。

在实践中,MovieLens数据集质量较高,简单的FM模型也能取得不错的效果,可能不容易观察到明显的过拟合现象。但通过调整这些参数,你可以更深入地理解它们对模型行为的影响。


总结
本节课中我们一起学习了如何从零开始实现一个用于评分预测的因子分解机(FM)模型。我们从数据读取开始,定义了模型的参数和预测函数,推导了损失函数及其梯度,并实现了梯度下降训练流程。最后,我们建立了模型评估方法,并讨论了超参数调试的重要性。


我们实现的这个简化版FM,实际上等价于经典的SVD矩阵分解模型。它揭示了如何通过用户和物品的隐向量交互来捕捉复杂的特征关系。作为扩展,你可以尝试为模型添加线性项和常数项,或者融入更多的用户和电影特征,使其成为一个更完整的FM模型。


人工智能—推荐系统公开课(七月在线出品) - P10:CTR预估模型演变教程 📈
在本节课中,我们将要学习CTR(点击率)预估模型的演变历程。我们将从早期的简单线性模型开始,逐步深入到结合深度学习和注意力机制的复杂模型,了解其发展脉络、核心思想以及不同阶段模型的特点。


概述
CTR预估是推荐系统、在线广告等领域的核心技术,其目标在于预测用户点击某个物品(如商品、广告)的概率。模型的发展经历了从依赖人工特征工程到模型自动学习高阶交叉特征的演进过程。理解这一演变过程,有助于我们把握推荐系统技术的核心发展方向。
第一阶段:发展初期(人工特征 + 线性模型)
在深度学习发展相对缓慢的初期,CTR预估主要依赖于大量的人工构造特征和简单的线性模型,如逻辑回归(LR)。
这个阶段的核心思想是,由于模型本身(如LR)的学习能力有限,无法自动学习特征间的交互关系,因此需要算法工程师手动构造各种可能的特征组合,以帮助模型捕捉更多信息。
以下是该阶段的主要特点:

- 模型简单:主要使用逻辑回归等线性模型,复杂度低,但学习能力有限。
- 特征工程繁重:需要人工构造大量特征,包括一阶特征、二阶及更高阶的交叉特征、统计特征等。
- 特征处理关键:连续特征需要归一化或分桶;类别特征需要转换为One-Hot编码,这可能导致特征维度极高(如千万维)。
- 流程固定:经过复杂的特征构造和处理后,将特征输入LR模型,通过Sigmoid函数得到最终的点击率分数。

第二阶段:加速发展期(自动特征交叉)
随着业务发展,完全依赖人工构造特征变得效率低下且可能考虑不全。此阶段开始借助模型本身的能力来自动进行特征交叉,代表模型有因子分解机(FM)、域感知因子分解机(FFM)以及梯度提升树(GBDT)与LR的结合。
上一节我们介绍了完全依赖人工特征工程的阶段,本节中我们来看看如何利用模型自动学习特征交叉。
因子分解机(FM)与域感知因子分解机(FFM)
FM模型在线性模型的基础上,增加了特征二阶交叉项,能够有效捕获特征间的两两交互作用。
FM模型公式:
ŷ = w₀ + Σᵢ wᵢ xᵢ + Σᵢ Σⱼ<ᵢ ⟨vᵢ, vⱼ⟩ xᵢ xⱼ
其中,⟨vᵢ, vⱼ⟩ 代表特征i和特征j的隐向量内积,用于建模二阶交叉。
FFM在FM的基础上引入了“域”的概念,认为特征在与不同域的特征交叉时,其重要性(权重)应该是不同的,从而进行了更精细的建模。
FFM模型核心思想:
特征i在与属于域fⱼ的特征j交叉时,使用特定的隐向量 vᵢ, fⱼ,而非FM中固定的 vᵢ。这增加了模型的表达能力,但也显著提高了计算复杂度和参数量。
GBDT + LR 模型

该模型利用GBDT的非线性能力来自动进行特征组合与筛选,并将组合结果作为新的离散特征输入给LR模型。
以下是其工作原理:
- GBDT生成特征:样本输入GBDT(多棵树),最终会落到每棵树的某个叶子节点上。将样本在所有树上的叶子节点位置进行One-Hot编码,形成一个高维稀疏的01向量。
- LR进行分类:将这个由GBDT生成的01向量作为新的特征,输入到LR模型中进行最终的CTR预估。
这种结构尝试将树模型的强特征组合能力与线性模型的高效性相结合。
第三阶段:深度学习发展阶段
深度学习为CTR预估带来了革命性变化。模型不再局限于二阶交叉,可以学习更复杂的高阶非线性关系,并且能够自然地处理稀疏特征、序列特征等。

上一节我们介绍了利用传统机器学习模型进行自动特征交叉的方法,本节中我们进入当前的主流——深度学习模型阶段。
核心变化与方向
深度学习阶段的模型演变主要围绕以下几个方向展开:
- Embedding化:将高维稀疏的类别特征通过Embedding层映射为低维稠密向量,这是深度学习处理推荐问题的基石。
- 高阶特征交叉:设计各种网络结构(如Deep & Cross Network)来自动学习显式或隐式的高阶特征组合。
- 用户兴趣建模:引入注意力(Attention)、序列模型(LSTM/GRU)、Transformer等结构,从用户历史行为序列中动态捕捉用户兴趣及其演化。
经典模型结构
以下是几种具有代表性的深度学习CTR模型结构:
1. Wide & Deep / DeepFM
这类模型通常采用并行的双路结构。
- Wide & Deep:Wide部分(LR)负责记忆(memorization),Deep部分(深度神经网络)负责泛化(generalization)。
- DeepFM:用FM替换了Wide部分,使其能自动学习二阶特征交叉,并与Deep部分共享Embedding输入。
2. Deep & Cross Network (DCN)
DCN专注于进行高效的高阶特征交叉。其Cross Network通过特殊的层叠公式,在每一层都与输入特征进行交叉,从而构造有限阶数的显式特征交叉。


3. 深度兴趣网络(DIN)与深度兴趣进化网络(DIEN)
这类模型专注于用户兴趣建模。
- DIN:引入注意力机制,根据候选商品自适应地计算用户历史行为中每个商品的重要性权重,然后加权求和得到用户兴趣表示。
- DIEN:在DIN基础上,进一步使用GRU等序列模型来模拟用户兴趣随时间的演化过程,捕捉兴趣的动态变化。
4. Behavior Sequence Transformer (BST)
BST利用Transformer架构来建模用户行为序列。通过Transformer中的自注意力机制,可以更好地捕捉行为序列中长距离的依赖关系,并理解不同行为之间的复杂交互。
前沿发展与挑战


当前,CTR预估模型仍在快速发展,趋势包括:
- 多任务学习:如MMOE、PLE模型,同时优化点击率、转化率等多个目标。
- 实时性:模型更新频率从天级别向小时级、分钟级演进(Online Learning)。
- 预训练大模型:探索将BERT等预训练大模型思想应用于推荐(如BERT4Rec)。
- 图神经网络:结合知识图谱(KG)解决冷启动等问题。

这些发展使得模型越来越复杂,但也对工程实现、线上服务性能(如要求推理在毫秒级完成)提出了严峻挑战。
总结


本节课我们一起学习了CTR预估模型的演变历程。我们从依赖人工特征工程的线性模型出发,经历了借助FM、GBDT等模型进行自动特征交叉的阶段,最终进入到以深度学习为核心的现代模型阶段。现代模型通过Embedding、复杂网络结构(如DCN、Transformer)和注意力机制,能够自动学习高阶特征交互并动态捕捉用户兴趣。理解这一演变路径,不仅有助于我们掌握现有技术,更能为我们紧跟前沿(如多任务学习、大模型推荐)奠定坚实的基础。在实际工作中,模型选择需综合考虑业务场景、数据特点、性能要求与可解释性等因素。

人工智能—推荐系统公开课(七月在线出品) - P11:手把手带你挖掘电商用户行为 👨💻



在本节课中,我们将学习如何对一个电商平台的用户行为数据进行完整的分析和挖掘。我们将遵循数据挖掘的标准流程,从目标定义、数据探索、特征工程到模型构建与评估,一步步完成一个预测用户购买行为的项目。


概述:数据挖掘的基本流程 📊
上一节我们介绍了课程背景,本节中我们来看看数据挖掘的标准流程。数据挖掘是从大量数据中总结泛化规律的过程,其核心流程通常包括以下几个步骤:
- 目标定义:明确本次数据挖掘要解决的具体业务问题。
- 数据采集:从业务系统中抽取与目标相关的样本数据子集。
- 数据整理:对数据进行探索性分析、清洗和预处理。
- 模型构建:根据问题类型选择合适的算法构建预测模型。
- 模型评价:使用预定的标准评估模型性能,并进行优化。
- 模型发布:将模型部署上线,并持续监控和维护。
这个流程并非单向的,在实际工作中可能需要反复迭代和探索。
项目背景与目标 🎯
上一节我们梳理了通用流程,本节中我们来看看具体的项目背景。电商平台竞争激烈,掌握用户行为、实现精准营销至关重要。本项目基于一份真实的电商用户行为数据集,包含用户的点击、收藏、加购、购买四种行为。
我们的挖掘目标是:预测在下一个时间日期,用户对特定商品子集的购买情况。这是一个典型的二分类预测问题(买或不买)。
模型的评估标准采用 F1分数,它是精确率(Precision)和召回率(Recall)的调和平均数,能综合衡量模型的性能。其公式如下:
F1 = 2 * (Precision * Recall) / (Precision + Recall)
数据采集与整理 🧹
明确了目标后,我们开始处理数据。首先使用Pandas读取数据文件,并查看数据的基本概况。
import pandas as pd
df = pd.read_csv('user.csv')
df.head()
df.info()
数据包含用户ID、商品ID、行为类型、商品类目、时间戳等字段。其中“地理位置”字段存在大量空值且对目标预测帮助不大,我们将其删除。
df = df.drop(columns=['地理位置'], axis=1) # axis=1表示按列删除
检查缺失值后发现数据质量很好,没有缺失。接着,我们将用数字编码的“行为类型”转换为可读的标签,并将“时间戳”拆分为“日期”、“具体时间”和“星期几”,以便进行更细致的时间维度分析。
# 行为类型映射
behavior_map = {1: ‘点击‘, 2: ‘收藏‘, 3: ‘加购‘, 4: ‘购买‘}
df[‘行为类型‘] = df[‘行为类型‘].map(behavior_map)
# 拆分时间戳
df[‘日期‘] = df[‘时间戳‘].apply(lambda x: x.split(‘ ‘)[0])
df[‘具体时间‘] = df[‘时间戳‘].apply(lambda x: x.split(‘ ‘)[1])
df[‘星期几‘] = pd.to_datetime(df[‘日期‘]).dt.dayofweek
数据分析与可视化 📈
数据预处理完成后,我们进入探索性数据分析阶段,从多个维度洞察用户行为。
以下是几个关键的分析方向:
- 用户流量分析:分析页面浏览量(PV)和独立访客数(UV)随时间(如按日、按月)的变化趋势。例如,通过图表可以发现“双十二”活动期间PV和UV出现显著峰值。
- 用户消费行为分析:
- 时间习惯:分析用户一天中不同时间段的活跃度。通常会发现用户活跃高峰与休息时间(如晚间)重合。
- 行为转化:绘制从“点击”到“加购/收藏”,再到“购买”的转化漏斗图。通常点击到购买的转化率较低。
- 复购分析:统计在一个月内购买超过一次的用户比例。
- 商品销售分析:统计热销商品、分析商品销售分布等。
- 用户价值分析:使用RFM模型或其他方法对用户进行分类(如高价值用户、发展期用户、保持用户、流失用户),并制定差异化运营策略。
这些分析主要通过Pandas进行数据聚合和统计,并借助Matplotlib或Seaborn库进行可视化呈现。
模型构建与评估 🤖
数据分析为我们提供了深刻的业务洞察,本节中我们基于这些洞察来构建预测模型。整个过程分为三步:构建数据集、特征工程和模型训练。
1. 构建数据集
首先需要划分训练集和测试集,通常按8:2的比例划分。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(features, label, test_size=0.2, random_state=42)
2. 特征工程
特征质量直接决定模型效果。我们可以从原始数据中构建多种类型的特征:
- 统计类特征:用户历史购买次数、点击次数、最近一次购买时间间隔等。
- 比率类特征:用户购买点击比、加购点击比等。
- 类别特征:商品类目、用户年龄段(需从其他数据衍生)等。
- 交叉特征:用户对特定商品类目的偏好程度。

3. 模型选择与训练
我们选择两个经典模型进行尝试和对比:逻辑回归(LR)和梯度提升树(GBDT)。



from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
model_lr = LogisticRegression()
model_gbdt = GradientBoostingClassifier()
model_lr.fit(X_train, y_train)
model_gbdt.fit(X_train, y_train)
4. 模型评估
使用之前定义的F1分数对两个模型在测试集上的表现进行评估。
from sklearn.metrics import f1_score
y_pred_lr = model_lr.predict(X_test)
y_pred_gbdt = model_gbdt.predict(X_test)


f1_lr = f1_score(y_test, y_pred_lr)
f1_gbdt = f1_score(y_test, y_pred_gbdt)
print(f“逻辑回归 F1分数: {f1_lr:.4f}“)
print(f“GBDT F1分数: {f1_gbdt:.4f}“)
根据F1分数选择性能更优的模型,并可以进一步进行模型调优。

总结 🎓


本节课中我们一起学习了电商用户行为数据挖掘的完整流程。我们从定义预测目标开始,经历了数据读取、清洗、探索性分析,并基于业务理解构建特征,最终使用机器学习模型进行预测和评估。



整个项目的核心在于:理解业务逻辑比单纯调用模型代码更重要。数据挖掘是一个业务驱动、反复迭代的过程,扎实的数据分析能力和清晰的业务思维是成功的关键。希望本教程能帮助你建立起数据挖掘的系统性认知。

人工智能—推荐系统公开课(七月在线出品) - P12:排序算法发展趋势 📈

在本节课中,我们将学习点击率预估模型的最新发展趋势。点击率预估是推荐系统中的核心环节,负责对召回的商品进行精准排序,以提升用户体验和平台收益。
概述
点击率预估不仅指点击行为的预测,也泛指转化率、下载率等类似场景的预估。虽然具体场景的决策点和数据稀疏程度不同,但核心模型与算法是相通的。当前,CTR预估的发展主要围绕四个方向展开:特征交互组合、特征抽取优化、多模态融合以及长短期兴趣建模。我们将逐一探讨这些方向的核心思想与代表性工作。
特征交互组合 🤝
上一节我们概述了CTR预估的四个发展方向,本节中我们来看看第一个方向:特征交互组合。该方向关注模型如何捕捉特征间深层、复杂的交互信息,从早期的简单模型到如今的高阶交叉模型,其演进体现了对特征关系理解的深化。
以下是两组代表性的特征交互模型:
- 浅层交互模型:如FM、FFM。这类模型主要捕捉二阶特征交叉。FM为所有特征域交叉分配相同权重,而FFM进一步细化了不同特征域间交叉的权重差异。
- 深层交互模型:如DeepFM、DCN。这类模型通常结合深度神经网络来捕捉隐式高阶特征交互。例如,DCN通过其交叉网络显式地进行特征交叉,交叉阶数可人为设定,其核心操作遵循公式:x_{l+1} = x_0 * x_l^T * w_l + b_l + x_l。
目前,基于特征交互的模型大致可分为三类:
- 聚合用户历史行为序列:通过注意力机制等加权方式,聚合用户行为序列信息,使其与候选商品进行交互,例如经典的DIN模型。
- 基于图结构的方法:将特征视为图节点,通过图神经网络进行信息传播与聚合,例如GCN、GraphSAGE等模型在图推荐中的应用。
- 显式特征组合:直接建模特征间的显式交叉,如前述的FM、DeepFM、DCN等模型。
以阿里的CAN模型为例,其核心是Co-Action Unit。该单元将用户历史行为序列中的每个商品与目标商品进行交互,同时也将用户属性(如年龄)与目标商品交互。其内部是一个多层感知机,通过将用户和商品的特征向量拆分并逐层交互,最终输出交叉信息,再与深度网络部分结合,共同进行CTR预估。
特征抽取优化 🔍
在了解了特征如何交互后,我们来看看如何从原始数据中更有效地抽取特征。不同的特征抽取器适用于不同的输入类型,能提取出不同层次的信息。
以下是几种核心的特征抽取器及其应用:
- Transformer:核心是Multi-Head Self-Attention机制。它能并行处理序列中所有元素间的交互,捕捉全局依赖关系。其基本操作包括多头注意力计算和前馈神经网络。
- RNN:适合处理序列数据,通过隐藏状态传递历史信息,最终输出对整个序列的表示,但属于串行计算。
- CNN:可通过一维卷积从序列或文本中提取局部特征模式。
具体到CTR模型,BST模型利用Transformer层来处理用户行为序列与目标商品,捕捉其间的交互信息。而BERT4Rec模型则将NLP中的BERT架构引入推荐,用于序列推荐,它能进行双向的上下文编码,比传统的单向RNN更能充分理解用户行为序列的上下文信息。不过,考虑到线上推理的耗时,工业界通常会采用层数较浅的Transformer变体。
多模态融合 🖼️📝

现实世界中的信息是多元的。本节我们探讨如何将文本、图像、视频等多模态信息融合到推荐模型中,以更全面地理解用户和商品。

多模态信息主要包含以下几类:

- 人工特征:用户/商品ID、统计特征等。
- 文本特征:搜索Query、商品标题、描述、评论等。
- 用户行为特征:用户点击、购买序列,社交关系图等。
- 图像/视频特征:商品主图、宣传视频帧等。
- 音频特征:音乐、语音描述等。

以图像特征为例,京东的CSCCN模型研究了如何将商品图像信息有效引入CTR预估。与离线预训练图像特征再接入模型不同,CSCCN将CNN结构嵌入到端到端的CTR模型中进行联合训练,使图像特征的优化目标与点击率目标一致,避免了目标偏差。
另一种思路是利用知识图谱。例如,通过图注意力网络聚合知识图谱中实体的多跳关联属性信息,更新实体嵌入,再输入推荐模型。这种方法能有效缓解新品(冷启动)推荐问题,因为可以通过其品牌、类别等属性信息进行推断。


长短期兴趣建模 ⏳⚡
用户的兴趣是动态变化的,既有稳定的长期偏好,也有易变的短期意图。本节我们学习如何对这两种兴趣分别进行建模。
兴趣建模的演进路径如下:
- Pooling/DIN:早期采用平均池化或最大池化聚合用户行为序列,视为一视同仁。DIN引入注意力机制,根据候选商品动态加权历史行为,但未考虑兴趣随时间演化的趋势。
- DIEN:在DIN基础上,引入GRU等序列模型来模拟用户兴趣的演化过程,捕捉兴趣的动态变化。
- MIMN:首次将用户行为序列建模长度扩展到千级别,并将其编码到固定大小的记忆网络中,但可能引入大量噪声行为信息。
- SIM:通过“搜索”机制(如软搜索)从超长行为序列中检索出与当前候选商品最相关的部分行为子序列,再进行建模,有效过滤了噪声,聚焦于长期兴趣中的关键信息。
总结


本节课我们一起学习了CTR预估模型的四大发展趋势:特征交互组合致力于挖掘特征间深层次关系;特征抽取优化利用更强大的网络结构(如Transformer)从原始数据中提取有效信息;多模态融合将图像、文本等多源信息纳入模型,提供更丰富的决策依据;长短期兴趣建模区分并分别建模用户稳定和动态变化的兴趣。理解这些趋势,有助于把握推荐系统前沿,并设计出更强大的排序模型。


人工智能—推荐系统公开课(七月在线出品) - P13:【公开课】快速入门推荐系统串讲

概述
在本节课中,我们将快速入门推荐系统,了解其核心流程与关键技术。课程将围绕推荐系统的两大核心模块——召回与排序展开,介绍其基本概念、经典算法与发展脉络,帮助初学者建立对推荐系统的整体认知。

推荐系统介绍


推荐系统是当前互联网产品中的核心组成部分,广泛应用于抖音、京东、YouTube等平台。它通过分析用户的历史行为数据,理解用户兴趣,从而为用户精准推送内容。对于大型互联网公司而言,推荐系统是重要的收入来源,其性能的微小提升都可能带来巨大的商业价值。

推荐系统的整体工作流程通常包含以下几个步骤:

- 数据:系统的基础是用户与平台产生的各种交互日志数据,例如浏览、点击、加购、购买等行为。
- 召回:从海量(如百万或上亿)的商品或内容库中,快速筛选出数百或数千个可能与用户相关的候选集。
- 排序:对召回得到的候选集,利用更复杂的模型和丰富的特征进行精细打分与排序,预测用户对每个候选项目的偏好概率。
- 重排与展示:对排序后的结果进行多样性、新鲜度等策略调整,最终组合成展示给用户的列表。
召回阶段处理数据量巨大,要求速度快、模型相对简单。排序阶段则使用更复杂的模型和更多特征进行精准预测。

召回模块


上一节我们介绍了推荐系统的整体流程,本节中我们来看看流程的第一步——召回模块。召回的核心任务是从全量物品库中快速缩小筛选范围,其设计理念类似于“盲人摸象”,需要从多个角度、采用多种策略来覆盖用户可能感兴趣的不同方面,避免兴趣单一化。
评估召回效果时,除了准确度,还需考虑覆盖度、多样性、时效性等指标,并通过离线评估(如损失函数、覆盖度)和线上测试(如点击率CTR)来综合衡量。

以下是召回模块中一些经典的方法与模型:

1. 协同过滤
协同过滤通过分析用户与物品之间的相似性进行推荐。其核心公式是计算用户或物品之间的相似度。例如,用户协同过滤的相似度计算可表示为:
sim(u, v) = cosine(user_embedding_u, user_embedding_v)
若用户A与用户B相似,且用户B喜欢物品I,则可将物品I推荐给用户A。
- UserCF:核心是找到相似的用户。适用于社交属性强的平台(如微信视频号),将好友喜欢的内容推荐给用户。
- ItemCF:核心是找到相似的物品。适用于物品属性稳定的场景(如电商),根据用户历史喜欢的物品,推荐与之相似的物品。

2. 关联规则召回
通过挖掘用户行为序列中物品之间的关联关系进行召回。例如,用户购买了啤酒后经常购买尿布,则两者具有强关联性。关联性的打分会考虑时间衰减和方向性(如购买手机后买手机壳是强正向关联)。
3. 向量化召回
将用户和物品映射到向量空间,通过计算向量相似度进行召回。

- Youtube DNN:经典的双塔模型。离线训练模型,得到所有视频的向量并存储;线上实时计算用户向量,通过近似最近邻搜索快速找到最相似的视频。
- DSSM:深度语义匹配模型,同样采用双塔结构,分别学习用户侧(Query)和物品侧(Document)的向量表示,通过计算余弦相似度或内积进行匹配。
- MIND(多兴趣网络):为了解决用户兴趣多样性的问题,利用胶囊网络从用户行为序列中提取多个兴趣向量,每个兴趣向量可以独立进行召回。
4. 基于图的召回
将用户和物品视为图中的节点,交互行为视为边,构建异构图。通过随机游走生成序列,再使用Word2Vec等方法学习节点(即用户或物品)的向量表示。EGES 算法是该方向的经典模型,它通过引入物品的边信息(如品类、品牌)来增强向量表示,缓解冷启动问题。

5. 基于知识图谱的召回
利用知识图谱中丰富的实体属性和关系进行召回。特别适用于解决新品冷启动和时效性热点问题。例如,新上市的手机在历史行为数据中很少,但通过知识图谱可以关联到其品牌、品类等已有信息,从而进行有效推荐。

排序模块

在召回模块为我们筛选出候选集之后,排序模块的任务是对这些候选进行精准打分与排序。排序模型的发展经历了从人工特征工程到自动化、深度学习化的演进。

以下是排序模型发展的几个关键阶段及其代表模型:

1. 发展初期:人工特征 + 线性模型
早期主要依赖人工构建的大量特征,结合逻辑回归等线性模型进行预测。模型公式简单,但特征工程负担重,且难以捕捉复杂的特征交叉关系。

2. 加速发展:自动化特征交叉模型
模型开始具备自动学习特征交叉的能力。
- FM(因子分解机):在LR的基础上增加了二阶特征交叉项,能自动学习特征间的组合关系。其预测公式为:
ŷ = w0 + Σwi*xi + ΣΣ <vi, vj> * xi * xj - FFM(场感知因子分解机):在FM基础上引入“场”的概念,同一特征在与不同场的特征交叉时使用不同的隐向量,建模更细致,但参数量大。
- GBDT+LR:Facebook提出的经典模型。利用GBDT进行自动特征组合与转换,将样本落在每棵树的叶子节点编号构成新的离散特征向量,再输入LR进行训练。这相当于利用树模型进行了高阶特征交叉。


3. 深度发展:深度学习模型
利用深度神经网络强大的非线性拟合能力进行排序。

- Wide & Deep:谷歌提出的模型,兼顾记忆与泛化。“Wide”部分(线性模型)记忆历史数据中的频繁模式,“Deep”部分(深度神经网络)泛化到新的特征组合。
- DeepFM:华为提出的模型,用FM替换了Wide & Deep中的Wide部分。FM层可以低阶特征交叉,Deep层进行高阶特征交叉,两者共享输入,无需精细的特征工程。
- DIN(深度兴趣网络):阿里妈妈提出的模型,针对用户历史行为序列,引入注意力机制。模型会动态计算用户历史行为中每个物品与当前候选广告的相关性权重,从而形成与当前候选相关的用户兴趣表示。
- DIEN(深度兴趣进化网络):在DIN的基础上,进一步使用序列模型(如GRU)来模拟用户兴趣随时间的变化过程,能更好地捕捉兴趣的动态演进。

总结与思考
本节课中我们一起学习了推荐系统的核心框架与关键技术。

召回模块的核心在于多路与多样。它从海量信息中快速初筛,并需要从热度、协同过滤、向量化、知识图谱等多个角度设计召回策略,以确保候选集的覆盖度和多样性。其发展路径是从简单规则到复杂模型,从通用化召回走向深度个性化召回。
排序模块的核心在于精准与智能。它利用丰富的特征和复杂的模型(从线性模型、树模型到深度模型)对候选进行精细打分。模型演进的核心方向是更好地自动化学习特征交叉,以及更精准地建模用户兴趣(尤其是动态的兴趣序列)。


召回与排序相辅相成,共同决定了推荐系统的最终效果。一个高效的召回能为排序提供优质的候选,而一个精准的排序能将召回的价值最大化。掌握这两大模块的原理与实现,是构建现代推荐系统的基础。





(注:课程中涉及的奖品领取、课件获取等内容已按您的要求省略,仅保留技术教程部分。)


🧠 人工智能—推荐系统公开课(P14):深度学习与电商推荐系统
在本节课中,我们将要学习深度学习的基础知识,并了解如何将其应用于电商推荐系统的构建。课程内容分为三部分:深度学习基础、推荐系统介绍以及一个基于淘宝数据集的电商推荐系统实战案例。
🧱 第一部分:深度学习基础
上一节我们介绍了课程的整体安排,本节中我们来看看深度学习的基础概念。
深度学习是机器学习的一个特定分支。它与传统机器学习算法(如线性模型、树模型、KNN、SVM等)是并列关系,而非对立关系。深度学习的核心特点在于其网络结构模拟了人脑的神经元组织。
深度学习有两个主要特点:
- 端到端(End-to-End)过程:模型直接从输入得到输出,中间无需复杂的人工特征工程。
- 有向计算图:网络由节点(计算过程)和边(数据流向)组成,可以形成复杂的、甚至带环的结构。
一个典型的深度学习网络包含输入层、隐含层和输出层。其中,隐含层可以是多层。层与层之间如果所有节点都相互连接,则称为全连接层(Fully Connected Layer)。由多个全连接层组成的网络称为多层感知机(MLP)。
在计算上,深度学习模型常利用矩阵乘法进行高效并行计算。例如,一个全连接层的计算可以表示为:
输出 = 激活函数(输入矩阵 × 权重矩阵 + 偏置向量)
深度学习特别适合处理非结构化数据(如图片、文本)的端到端任务。以下是几个例子:
- 输入图片,输出标签:人脸识别、车牌识别、动物分类、红绿灯检测。
- 输入文本,输出文本:机器翻译、对话机器人。
🔍 第二部分:推荐系统介绍
了解了深度学习的基础后,本节我们来看看推荐系统的基本概念及其在电商领域的应用。
推荐系统的核心目标是建立用户(User)与商品(Item)之间的连接,预测用户可能感兴趣或购买的商品。其方法主要分为两类:
- 基于机器学习的推荐:依赖人工特征工程,从数据中提取刻画用户行为和商品属性的特征,再使用机器学习模型进行预测。
- 基于深度学习的推荐:利用深度学习模型自动学习用户和商品的表示(嵌入),并完成匹配预测。
在电商场景中,基于机器学习的方法需要人工构建大量特征,例如:
以下是基于用户行为可以构建的部分特征示例:
- 用户浏览不同商品的心智指数(衡量偏好)。
- 用户浏览最多商品的品牌转化率。
- 用户最后一次操作的类型(如点击、收藏)。
- 用户活跃天数、历史购买次数、复购行为。
- 用户加购次数、注册时间等。
而基于深度学习的方法则更为简洁,通常采用深度协同过滤等模型。其核心思想是将用户ID和商品ID通过嵌入层(Embedding Layer) 映射为稠密向量,然后将这些向量拼接或交互,最后通过全连接层等网络结构预测用户对商品的偏好得分(如点击率、购买概率)。
🛒 第三部分:电商推荐系统实战
前面我们介绍了推荐系统的两种思路,本节我们将使用深度学习模型,在真实的淘宝用户行为数据集上构建一个推荐系统实战案例。
我们使用的数据集记录了2017年11月25日至12月3日间约100万用户的行为,每条数据包含:用户ID、商品ID、商品类目ID、行为类型(pv浏览、buy购买、cart加购、fav收藏)和时间戳。
数据准备与探索
首先,我们读取数据并进行初步分析,例如将时间戳转换为日期,统计每日行为量,分析不同商品的转化率(购买次数/浏览次数)等。
定义预测任务
我们定义一个具体的推荐任务:预测用户是否会购买某个商品。这是一个二分类问题。
- 正样本:用户-商品组合中,用户购买了该商品。
- 负样本:用户-商品组合中,用户未购买该商品(可从用户未交互的商品中采样)。
构建深度学习模型
我们构建一个简单的深度推荐模型,其结构如下:
- 输入层:接收用户ID、商品ID、商品类目ID。
- 嵌入层(Embedding Layer):将高维稀疏的ID特征映射为低维稠密向量(例如32维)。公式上,这相当于一个查找表:
E(user_id) -> user_embedding。 - 拼接层:将用户嵌入向量、商品嵌入向量、类目嵌入向量拼接成一个长向量。
- 全连接层(Fully Connected Layers):接收拼接后的向量,通过一层或多层非线性变换学习高级特征。
- 输出层:通过一个神经元(使用Sigmoid激活函数)输出一个0到1之间的值,表示用户购买该商品的预测概率。
模型使用二元交叉熵损失(BCELoss)进行训练。训练完成后,对于待推荐的商品列表,我们可以用该模型为用户可能购买的每个商品打分,并按分数从高到低进行推荐。
模型训练与总结




通过准备正负样本数据集,我们可以对该模型进行训练。训练过程与传统深度学习模型一致,包括前向传播、损失计算、反向传播和参数更新。



本节课中我们一起学习了深度学习的基础、推荐系统的原理,并实战演练了如何使用深度学习模型构建一个电商推荐系统。我们看到了深度学习如何通过嵌入技术自动学习用户和商品的表示,从而避免了繁琐的人工特征工程,为实现高效的个性化推荐提供了强大的工具。




注:本教程根据公开课内容整理,侧重核心思路与流程阐述。完整代码、数据集及详细实现需参考相关课程资料。
人工智能—推荐系统公开课(七月在线出品) - P15:22.3.31深入浅出推荐系统
概述
在本节课中,我们将要学习推荐系统的核心模块与经典算法。课程将围绕召回、排序、重排三大模块展开,介绍其基本概念、经典策略与模型演变,帮助初学者对推荐系统建立一个整体性的认识。
推荐系统简介
推荐系统是人工智能领域的热门方向,与搜索、广告并称为“搜广推”。其核心目标是提升用户体验,通过分析用户行为数据(如点击、浏览、购买),挖掘用户的潜在兴趣,从而进行个性化商品或内容推荐。
一个典型的电商推荐系统流程如下:首先从日志中提取用户行为数据,然后利用召回、排序、重排技术,最终挖掘出用户可能感兴趣的商品。
召回模块
上一节我们介绍了推荐系统的整体流程,本节中我们来看看召回模块。召回的核心任务是从海量商品池(如上亿商品)中快速筛选出用户可能感兴趣的几百或几千个商品,以缩小后续排序模块的搜索范围。
召回策略通常采用“盲人摸象”的思路,即通过多种策略(模型)从不同角度捕捉用户兴趣,因为单一模型难以覆盖所有可能性。评估召回效果时,不仅看准确度,还需考虑覆盖度、多样性、时效性等因素。
以下是几种经典的召回策略:
1. 协同过滤
协同过滤同时利用用户和物品之间的相似性进行推荐。它分为基于用户的协同过滤和基于物品的协同过滤。
- 基于用户的协同过滤:找到与目标用户相似的用户,将该相似用户喜欢的物品推荐给目标用户。其核心是计算用户之间的相似度,例如使用余弦相似度公式:
similarity(A, B) = (A·B) / (||A|| * ||B||)。 - 基于物品的协同过滤:找到与目标物品相似的物品,将这些相似物品推荐给喜欢目标物品的用户。其核心是计算物品之间的相似度。
2. 关联规则召回
该策略通过挖掘商品之间的关联关系进行推荐。例如,经典案例“啤酒与尿布”,即购买啤酒的用户很可能也会购买尿布。
计算商品关联性时,可考虑购买序列中的距离和先后关系。一个简化的关联分数计算公式可以是:score(i1, i2) = base^{distance} * θ,其中base是距离衰减系数,θ在正向关联(先i1后i2)时为1,逆向时为小于1的系数。
3. 向量召回
向量召回通过模型得到用户和物品的嵌入向量,然后通过近似最近邻搜索快速找到与用户向量最相似的物品向量。
- 单向量召回:如YouTube DNN模型,为每个用户生成一个代表其整体兴趣的向量。线上服务时,实时计算用户向量,并通过Faiss等库检索相似物品。
- 多向量召回:如MIND模型,使用胶囊网络为每个用户生成多个兴趣向量,以表征其多样的兴趣意图,每个兴趣向量可独立召回一部分物品。
4. 图嵌入召回
基于图结构的学习方法,如DeepWalk、Node2Vec,通过随机游走生成物品序列,再利用Word2Vec思想学习物品的嵌入向量,从而捕捉物品在图结构中的相似性。
5. 知识图谱召回
利用知识图谱中实体(如商品、品牌、品类)的丰富属性和关系进行推荐,有助于解决新品冷启动、实现精准搭配(如手机与手机壳)等问题。
排序模块
上一节我们介绍了如何从海量商品中召回候选集,本节中我们来看看排序模块。排序模块的任务是对召回得到的几百个商品进行精准打分排序,需要构建复杂的特征和模型。
排序模型的发展大致分为三个阶段:
- 发展初期(2010年前):人工特征工程 + 线性模型(如LR)。
- 加速发展期(2010-2015年):自动特征交叉 + 线性模型,代表模型有FM(因子分解机)、FFM(场感知因子分解机)、GBDT+LR。
- 深度发展期(2016年至今):深度学习模型广泛应用,模型兼具记忆与泛化能力。
以下是几个核心的深度学习排序模型:
1. Wide & Deep
由Google提出,模型结构包含两部分:
- Wide部分:线性模型,处理稀疏特征,负责记忆。
- Deep部分:深度神经网络,处理稠密嵌入特征,负责泛化。
两部分结果相加后通过激活函数输出。
2. DeepFM
由华为提出,是对Wide & Deep的改进,用FM替换了Wide部分,能自动进行二阶特征交叉,结构更统一。
3. DIN(深度兴趣网络)
由阿里巴巴提出,针对用户历史行为序列,引入了注意力机制。它计算候选商品与历史行为商品之间的相关性权重,对历史行为进行加权求和,从而动态表征用户兴趣。
注意力权重的计算可简化为一个小型神经网络:attention_score = f(v_item, v_hist, v_item - v_hist, v_item * v_hist)。
4. DIEN(深度兴趣进化网络)
在DIN的基础上,使用GRU序列模型对用户历史兴趣的演化过程进行建模,能更好地捕捉兴趣的动态变化。



重排模块

排序模块输出了精准排序的列表,但直接展示给用户可能体验不佳。重排模块在排序结果之上,基于业务规则和用户体验进行最终调整。

以下是常见的重排策略:
- 打散:避免同一品类或相似商品连续出现,增加结果多样性。
- 去重:去除重复或高度相似的商品。
- 新鲜度:适当插入新品或热点内容。
- 业务规则:如价格带控制、品牌露出、库存过滤等。
- 上下文适配:根据用户实时场景(如时间、地点)微调。
总结



本节课我们一起学习了推荐系统的核心架构。我们从召回模块开始,了解了如何从海量商品中快速初筛,介绍了协同过滤、关联规则、向量召回、图嵌入和知识图谱等多种策略。接着,我们深入排序模块,回顾了从线性模型到深度学习模型(如Wide & Deep, DeepFM, DIN)的演进,这些模型负责对候选集进行精准打分。最后,我们简要介绍了重排模块,它通过一系列策略优化最终展示列表,以提升用户体验和业务指标。推荐系统是一个融合了算法、工程和业务理解的综合领域,希望本课能为你提供一个清晰的入门指引。



人工智能—推荐系统公开课(七月在线出品) - P16:快速入门推荐系统串讲 🚀
在本节课中,我们将要学习推荐系统的核心概念与整体架构。推荐系统是互联网应用中至关重要的技术,它帮助我们从海量数据中筛选出用户可能感兴趣的内容,如商品、视频或文章。我们将围绕召回、排序和重排这三个核心模块展开介绍,了解它们如何协同工作,最终为用户提供个性化的推荐结果。

推荐系统概述与架构 📊
推荐系统随着互联网数据量的增长而变得日益重要。其核心挑战在于如何从海量数据中快速找到用户感兴趣的内容。这项技术直接影响着许多公司的收入,例如在电商或广告场景中,精准的推荐能显著提升用户的点击率和转化率。
一个典型的推荐系统,例如电商推荐系统,其工作流程始于用户行为日志。这些日志记录了用户的浏览、点击、加购、购买等行为。系统从这些数据中提取用户兴趣,例如,用户将商品加入购物车是一个强烈的兴趣信号。
基于这些数据,推荐系统技术开始发挥作用。整个过程主要分为三个阶段:
- 召回:从拥有数百万甚至上亿商品的物料池中,快速筛选出几百或几千个用户可能感兴趣的商品。
- 排序:对召回得到的少量商品,使用更复杂的模型和丰富的特征进行精准打分和排序,以提升推荐的准确性。
- 重排:在最终展示前,对排序结果进行策略调整,例如过滤已购商品、增加推荐多样性,以优化用户体验。
最终,系统将处理好的商品展示在平台的各个推荐位上。
召回与排序:两大核心阶段 ⚙️

上一节我们介绍了推荐系统的整体架构,本节中我们来看看其核心的两个阶段:召回与排序。
召回阶段处理的是海量数据,其核心目标是“快”和“全”。它需要在毫秒级时间内,从庞大的商品池中初步筛选出用户可能感兴趣的一批商品。因此,召回模型通常相对简单,使用的特征也较少。

排序阶段则处理召回阶段筛选出的少量商品(例如几百个)。此时,核心目标是“准”。为了追求更高的准确性,排序模型可以设计得非常复杂,并引入大量特征。从早期的逻辑回归(LR)模型,到深度神经网络(DNN),再到结合Transformer等复杂结构,排序模型在不断演进。
简而言之,召回是“大海捞针”,快速找到可能的候选集;排序是“精挑细选”,对候选集进行精准打分和排序。

深入召回阶段:缩小搜索范围 🎯
上一节我们区分了召回与排序,本节中我们深入探讨召回阶段的具体目标与方法。

召回的核心任务是缩小搜索范围,即从几百万商品中筛选出几百个。更重要的是,它需要通过“局部拟合整体”,尽可能涵盖用户所有可能感兴趣的内容。由于用户行为数据可能稀疏或兴趣多样,单一召回策略往往不够。
因此,工业界通常采用多路召回策略,每一路从不同角度(如热门商品、相似用户喜好、关联商品等)进行筛选,最后将各路结果整合。这就像“盲人摸象”,每一路召回摸到“大象”的一部分,整合起来才能更接近全貌。
评估召回效果时,除了点击率(CTR)、转化率(CVR)等指标,在内容推荐场景还需考虑用户停留时长、转发、关注等互动指标。离线评估常用损失函数或人工评估,线上则通过A/B测试观察核心业务指标的变化。

以下是几种经典的召回方法:
协同过滤(Collaborative Filtering)

协同过滤利用用户(User)和物品(Item)之间的相似性进行推荐。其核心思想是“物以类聚,人以群分”。例如,如果用户A和用户B相似,且用户B喜欢了物品I,那么系统可以向用户A推荐物品I。
我们可以构建一个用户-物品评分矩阵。对于缺失值,简单的用0填充可能并不合理(未交互不代表不喜欢),实践中会采用均值或其他方法进行填充。计算用户相似度常用余弦相似度公式:

相似度计算公式:sim(A, B) = (A·B) / (||A|| * ||B||)
协同过滤主要分为两类:
- 基于用户的协同过滤(User-CF):更强调社交属性,根据相似用户的行为进行推荐,适合发现热点和趋势。
- 基于物品的协同过滤(Item-CF):基于物品本身的相似度,由于物品属性相对稳定,该方法在用户兴趣变化较慢的场景(如电商)中效果稳定。

关联规则召回
关联规则召回属于协同过滤的一种,其核心是挖掘商品之间的关联关系。经典案例是“啤酒与尿布”。系统根据用户历史交互序列,统计商品A出现后商品B出现的概率,从而建立关联。
实践中,关联的强度(分数)会受到方向性和时间距离的影响:
- 方向性:用户先买A再买B(正向)与先买B再买A(逆向),其关联强度可能不同。
- 时间距离:购买A和B的时间间隔越近,通常认为关联性越强。

我们可以设计一个公式来综合计算关联分数,例如:分数 = 基础关联度 * 方向系数 * 时间衰减系数。根据用户最近交互的商品,找出关联分数高的商品进行推荐。

向量化召回(Embedding-Based Retrieval)

向量化召回是将用户和物品映射到同一个低维向量空间,通过计算向量相似度来寻找候选物品。其流程通常分为离线和在线两部分:
- 离线:使用模型(如双塔DNN)分别计算所有物品的向量(Embedding)并存储。
- 在线:实时计算用户的向量,然后通过近似最近邻搜索(ANN)算法,快速从海量物品向量中找出与用户向量最相似的Top-K个物品。
双塔模型代码示意:
# 用户塔
user_embedding = user_tower(user_features)
# 物品塔
item_embedding = item_tower(item_features)
# 计算相似度(如点积)
similarity_score = tf.reduce_sum(user_embedding * item_embedding, axis=1)

YouTube在2016年提出的深度神经网络推荐模型是这一方向的经典工作。它同样采用类似结构,并利用ANN进行高效检索。
深度结构化语义模型(DSSM)
DSSM最初用于搜索引擎,现也应用于推荐。它将查询(Query,可视为用户特征)和文档(Document,可视为物品特征)分别通过深度网络映射为向量,然后计算两者的相似度(如余弦相似度或点积),最终通过Softmax输出概率。

多兴趣向量召回(MIND)

用户兴趣往往是多元的。MIND模型通过胶囊网络(Capsule Network)为每个用户生成多个兴趣向量(即多个Embedding),而不仅仅是一个。每个兴趣向量可以独立进行向量召回,最后将结果融合,从而更好地捕捉和满足用户的多样化兴趣。

图神经网络召回

图召回通过构建用户-物品交互图来利用丰富的结构信息。例如,DeepWalk方法:
- 构图:根据用户行为序列构建物品之间的转移关系图。
- 随机游走:在图上进行随机游走,生成类似于文本的节点序列。
- 向量化:使用Word2vec等算法,将节点序列转化为每个物品的向量表示。

更复杂的方法如Node2vec,在游走时结合了广度优先搜索(BFS)和深度优先搜索(DFS)的策略。这类方法能有效挖掘物品间的高阶关联。

EGES(Enhanced Graph Embedding with Side Information)
EGES针对冷启动问题(新品或行为稀疏的商品),在基础的图嵌入模型上加入了边信息(Side Information)。除了商品ID,它还考虑商品的品类、品牌等属性ID,并将这些属性的Embedding进行加权平均,得到商品的最终向量表示,从而丰富商品的表征。

知识图谱召回

基于商品知识图谱的召回,能够有效建模新品、时尚热点和时效活动。通过构建包含商品属性、类别、品牌等实体及其关系的知识图谱,系统可以挖掘深层次的语义关联,实现精准搭配推荐或解决冷启动问题。
构建知识图谱涉及商品多维度信息抽取、用户-商品关系建模等。基于图谱,可以预测用户兴趣演变、实现精准复购周期预测等。

深入排序阶段:精准打分与排序 🏆
上一节我们详细探讨了多种召回策略,本节中我们进入排序阶段。排序阶段的目标是对召回得到的几百个候选物品,利用复杂模型和丰富特征,进行精准的点击率(CTR)、转化率(CVR)等预测,并据此进行最终排序。

排序模块的核心是特征工程和模型。下面我们重点回顾排序模型的演进历程。
模型演进史

- 人工特征 + 线性模型(LR)时代:早期(如2010年前后)主要依赖专家构建海量人工特征,结合简单的逻辑回归(LR)模型。特征工程的质量至关重要。
- 自动特征交叉 + 线性模型时代(2010-2015):为了更精细地捕捉特征交互,出现了能自动进行特征交叉的模型。
- 因子分解机(FM):通过为每个特征学习一个隐向量,实现特征间的二阶交互。
- 梯度提升树(GBDT):通过树的分裂过程天然实现特征交叉。常将GBDT的叶子节点输出作为新特征,输入到LR模型中(GBDT+LR范式)。
- 深度模型崛起时代(2016至今):深度学习被引入,模型结构变得复杂。
- Wide & Deep:谷歌提出,结合了“宽部分”(记忆历史特征共现)和“深部分”(泛化新特征组合)。
- DeepFM:华为提出,在Wide & Deep基础上,用FM层替换Wide部分,能同时学习低阶和高阶特征交互。
- xDeepFM:提出压缩交互网络(CIN),显式地学习高阶特征交互。
- DIN(深度兴趣网络):阿里提出,针对用户历史行为序列,引入注意力(Attention)机制,动态评估不同历史行为对当前候选广告的重要性。
- DIEN(深度兴趣进化网络):在DIN基础上,引入GRU等序列模型来模拟用户兴趣随时间的变化过程。

目前,排序模型的发展趋势是更深入地建模用户行为序列,并与其他领域(如图神经网络、强化学习)的技术相结合。

重排阶段:优化最终体验 ✨
排序之后、展示之前,还有一个重要的重排阶段。重排的目标不是进一步预测用户偏好,而是对排序列表进行策略调整,以优化整体用户体验和平台指标。
以下是常见的重排策略:
- 去重与过滤:过滤掉用户已购买的非快消品、加入黑名单的商品、重复的相似商品(如图片相似、同SPU)。
- 多样性打散:避免同一屏推荐结果过于同质化。可以按照商品类目、品牌、价格段等维度进行打散,保证结果多样性。
- 业务规则干预:根据业务需求调整展示比例,例如不同推荐栏目的内容占比控制、提升用户收藏/加购商品的排名、对近期已购商品进行沉底等。
- 探索与利用:适当插入一些新内容或热门内容,探索用户潜在兴趣。



重排是连接算法与产品体验的关键环节,需要综合考虑算法分数和产品策略。
课程总结与展望 📚

本节课中,我们一起学习了推荐系统的核心框架与关键技术。我们从推荐系统的商业价值与挑战出发,详细剖析了召回、排序、重排三大核心模块。
在召回部分,我们介绍了从经典的协同过滤、关联规则,到现代的向量化召回、多兴趣召回、图神经网络召回等多种技术,它们从不同角度“大海捞针”,快速筛选候选集。在排序部分,我们回顾了模型从线性模型、因子分解机到深度兴趣网络的演进历程,看到了模型如何变得越来越复杂以追求极致的精准度。最后,我们了解了重排阶段如何通过策略调整,使推荐结果更加多样、合理,提升用户体验。



推荐系统是一个庞大且快速发展的领域,本节课只是一个入门串讲。要深入掌握工业级推荐系统的构建,还需要学习更具体的模型细节、特征工程方法、大数据平台工具以及项目实战经验。希望本次课程能为你打开推荐系统的大门,指引进一步学习的方向。


人工智能—推荐系统公开课(七月在线出品) - P2:大数据到推荐算法工程师的成长之路 🚀

概述

在本节课中,我们将跟随一位从大数据工程师成功转型为推荐算法工程师的讲师的经历,学习其成长路径、学习方法、项目实践以及面试经验。课程内容涵盖推荐系统的核心概念、技术栈构成、项目实战细节以及求职面试的关键要点。
个人转型经历
我于2015年毕业,最初从事Java Web开发工作。半年后,我决定转行,开始自学大数据技术,并找到了一份大数据相关的工作。在工作中,我接触了ETL和数据仓库,但始终不清楚这些数据的最终用途。



后来,我接触到机器学习和推荐算法,并决定从大数据领域转向算法领域。起初,我通过自学啃书,但效率低下且知识不成体系。最终,我选择报名七月在线的课程,系统性地构建了自己的知识架构。
学习方法与知识构建

由于我有数学专业背景,学习数学基础部分相对较快。七月在线的课程讲解非常到位,细节清晰。为了掌握知识,我采取了以下方法:

- 反复观看视频,每个视频都看了好几遍。
- 用笔记本记录所有知识点,并手动推导一遍。
- 动手实践,用Python实现核心算法,例如决策树和逻辑回归(LR)。


决策树的核心思想是递归,其伪代码如下:
def build_tree(data):
if 满足终止条件:
return 叶子节点
else:
选择最佳特征进行分割
左子树 = build_tree(左子集)
右子树 = build_tree(右子集)
return 决策节点(特征, 左子树, 右子树)

我认为编码能力和对原理的理解比掌握模型的数量更重要。
推荐系统项目实战:Park电影推荐系统
得益于之前的大数据经验,我在搭建系统环境时比较顺利。我在阿里云租用了三台服务器来搭建整个架构。


离线推荐:基于ALS的协同过滤
项目使用了Spark MLlib中的**交替最小二乘法(ALS)**进行矩阵分解,得到物品(item)之间的相似度矩阵。



矩阵分解公式可以简化为:R ≈ P * Q^T,其中R是用户-物品评分矩阵,P是用户隐因子矩阵,Q是物品隐因子矩阵。
计算出的相似度矩阵被缓存起来,供线上实时推荐使用。

实时推荐
实时推荐的核心是维护一个推荐列表堆(Heap)。当用户点击一个物品时,系统会根据该物品与用户历史行为物品的相似度进行计算,并实时更新这个推荐列表。因此,用户的推荐列表是动态变化的。
基于统计的召回策略
除了协同过滤,项目还实现了基于统计特征的召回策略,例如“近期热门”。其核心是根据时间窗口(如按年月划分)对物品进行分组,然后根据评分进行加权排序。
基于内容的召回策略
项目还实现了基于电影名称的推荐,使用了TF-IDF算法将电影名称转化为向量,然后计算电影之间的余弦相似度,最后进行推荐。
TF-IDF计算公式:TF-IDF(t, d) = TF(t, d) * IDF(t),其中TF是词频,IDF是逆文档频率。
无论是基于评分、统计还是内容,推荐系统的核心任务之一就是计算相似度。
面试经验与知识深度
在面试中,仅掌握ALS一种召回算法是不够的。面试官可能会深入询问以下问题:
- 多种召回算法:如Item-CF, User-CF, LFM(隐语义模型)的原理与区别。
- 冷启动问题:如何为新用户或新物品进行推荐。
- 数据倾斜与性能:当物品数量极大时,如何维护和优化相似度矩阵的计算与存储。一个可行的思路是结合物品类别信息,使用倒排索引或聚类方法,先筛选出具有潜在相似性的物品对,再进行精细计算。
- 公式推导:可能会要求手推LFM等模型的公式。
排序模型实践
在排序阶段,我实践了多种模型,并观察了模型融合的效果:
- 逻辑回归(LR):使用基础特征时,AUC(模型评价指标)一般。
- LR + 特征组合:加入组合特征后,AUC有明显提升。
- 梯度提升树(如XGBoost/LightGBM):AUC进一步提升。
- GBDT + LR:进行模型融合,效果通常优于单一模型。
- 深度学习模型:在数据量充足时,其效果通常优于传统的机器学习模型组合。
最终效果排序大致为:深度学习 > GBDT+LR > GBDT > LR。

技术广度与深度建议
推荐算法工程师不仅需要算法深度,还需要相关的技术广度来构建完整的系统。
- 大数据基础:必须熟悉大数据生态,因为训练数据通常来自日志,代码运行在集群上。需要了解Hadoop、Spark、Kafka等组件的原理和使用。
- 数据倾斜处理:需要理解其原理(如Shuffle阶段Key分布不均),并知道解决方案(如使用Combiner预聚合、加盐打散Key等)。
- 编程与数据结构:算法工程师的核心竞争力之一是扎实的编程能力和数据结构基础。面试中常要求手写代码,如链表、排序、堆栈等,并能分析时间/空间复杂度。
- 系统架构:形成自己的技术知识体系,能够清晰地描述推荐系统的整体架构(从数据采集、ETL、模型训练到线上服务)。
当前工作与扩展
目前我在公司从事与外卖推荐及物流调度相关的算法工作。例如,使用层次聚类和凸包扫描算法(Graham Scan)来解决商圈划分和骑手调度路径优化问题。这类问题大量运用了数据结构和几何计算知识。
凸包扫描算法核心:通过计算向量的叉积来判断点的转向,从而逐步构建凸包。
总结


本节课我们一起学习了从大数据转型到推荐算法工程师的完整路径。关键点包括:
- 系统学习:通过体系化课程(如七月在线)构建知识框架,并辅以反复学习和手动推导。
- 项目驱动:动手实现一个完整的推荐系统项目,涵盖离线/实时推荐、多种召回与排序策略。
- 深入原理:不仅要会用,还要理解多种召回算法(ALS, Item-CF, LFM等)和排序模型(LR, GBDT, 深度学习)的原理与差异。
- 拓宽视野:掌握必要的大数据技能(处理数据倾斜、集群原理)和扎实的编程与数据结构基础。
- 面试准备:形成自己的知识体系,能够清晰阐述项目细节、技术选型和解决方案。

对于有志于进入推荐系统领域的同学,建议聚焦推荐算法本身,同时打好计算机基础,并积极进行项目实践,这样就能在求职市场中具备较强的竞争力。

🛒 人工智能—推荐系统公开课(七月在线出品) - P3:带你实战电商平台的推荐系统
在本节课中,我们将学习如何从零开始,使用一个真实的电商平台日志数据来训练一个推荐系统模型。我们将重点关注数据处理、模型训练、特征工程以及调参过程中的常见陷阱和最佳实践。



📊 课程概述

本节课的核心目标是使用 libFM 模型对电商用户行为日志进行训练,构建一个点击率预估模型。我们将从原始数据出发,逐步完成数据预处理、特征编码、模型训练和效果评估的全过程。过程中会特别强调数据处理的正确逻辑和模型理解的深度,避免初学者常犯的错误。




🔍 数据与问题定义



我们拥有的数据是电商平台的用户行为日志,主要包含以下字段:时间戳、用户ID (user_id)、商品ID (item_id)、商品类目ID (category_id) 以及行为类型(如浏览 view 和交易 transaction)。我们将交易行为视为正样本,浏览行为视为负样本。

我们的任务是:仅使用 user_id 和 item_id 这两个ID类特征,训练一个 libFM 模型来预测用户对商品发生交易行为的概率。
首先,我们面临一个基础但关键的问题:直接将 user_id 和 item_id 两列数据抽取出来,丢给 libFM 模型进行训练,这样做是否合理?




⚠️ 核心陷阱:特征ID冲突


直接将 user_id 和 item_id 作为特征输入模型存在一个严重问题:ID冲突。
libFM 模型会将所有输入的特征视为同一空间下的ID进行处理。如果 user_id 和 item_id 的取值有重叠(例如,存在一个ID既是某个用户的ID,又是某个商品的ID),那么模型就会错误地将它们视为同一个特征,学习同一个隐向量。这显然不符合业务逻辑,因为用户和商品是完全不同的实体。
解决方案:我们必须对来自不同域(Field)的ID进行全局唯一的重编码(Re-encoding)。例如,为所有 user_id 加上前缀 “U_”,为所有 item_id 加上前缀 “I_”,确保它们在特征空间中是独一无二的。
# 示例:全局唯一编码逻辑
feature_dict = {}
current_id = 0
def get_or_add_id(feature_value, prefix):
global current_id
key = f"{prefix}_{feature_value}"
if key not in feature_dict:
feature_dict[key] = current_id
current_id += 1
return feature_dict[key]
# 对每条样本中的 user_id 和 item_id 进行编码
encoded_user_id = get_or_add_id(raw_user_id, "U")
encoded_item_id = get_or_add_id(raw_item_id, "I")
上一节我们明确了数据输入前的关键处理步骤,本节中我们来看看如何构建训练流程并评估模型。
🚀 模型训练与初步评估
在完成正确的特征编码后,我们可以开始训练模型。我们使用 libFM 进行训练,初始参数设置如下:
- 学习率 (
learn_rate): 0.001 - 正则化参数 (
regularization): 0.01 - 隐向量维度 (
dimension): 8 - 迭代轮数 (
iteration): 30
训练完成后,我们首先观察模型的准确率(Accuracy)。然而,在正负样本极不均衡(如点击率仅为千分之八)的场景下,准确率是一个不可靠的指标。模型只需将所有样本预测为负类,就能获得很高的准确率,但这没有任何意义。
正确的评估指标:我们应该使用 AUC (Area Under Curve) 来评估模型的排序能力。AUC衡量的是模型将正样本排在负样本前面的概率,对样本比例不敏感。
我们首次训练得到的AUC为 0.5。这是一个危险的信号,因为AUC=0.5等同于随机猜测,说明模型完全没有学到任何规律。


🔧 调试与问题排查
当遇到AUC=0.5时,我们的第一反应不应该是怀疑模型能力或盲目增加特征,而应该系统性地排查流程中的Bug。
以下是标准的排查思路:
- 检查数据:确认训练集和测试集是否正确分离,是否有数据泄露。
- 检查特征:确认特征编码是否正确,特别是之前提到的ID冲突问题是否已解决。
- 检查代码:确认数据读取、格式转换、模型调用流程是否有误。
- 检查配置:确认模型参数是否在合理范围内。
在我们的案例中,问题出在使用了错误的测试集文件。修正后,重新评估得到的AUC上升至 0.8046。这验证了流程的正确性。
⚙️ 模型调参实践
得到一个baseline后,我们可以尝试通过调参来提升模型效果。调参需要理解每个参数的意义:
- 学习率 (
learn_rate):控制参数更新的步长。太大可能导致无法收敛到最优点,太小则收敛过慢。我们从0.001尝试调大到0.01,发现AUC下降;调小到0.0005,AUC有轻微提升。 - 迭代轮数 (
iteration):增加迭代轮数让模型有更多机会学习。我们尝试了60轮和100轮,发现AUC反而略有下降,说明30轮已足够收敛。 - 隐向量维度 (
dimension):增加维度可以增强模型的表达能力。我们将维度从8增加到16,AUC从0.8046提升到了 0.805,获得了微小但稳定的提升。
重要认知:在工业界,通常不会进行耗时的网格搜索(Grid Search),而是依靠经验(Experience)进行快速调试。参数调优的收益往往是边际递减的,尤其是在千分位、万分位上的波动。


🧩 特征工程尝试:加入类目特征
为了进一步提升模型,我们考虑引入新的特征:商品类目ID (category_id)。我们希望模型能学习到“用户对某类商品的偏好”。
然而,加入新特征需要格外小心:
- 唯一编码:
category_id也需要进行全局唯一编码(例如,加上前缀“C_”),避免与用户ID、商品ID冲突。 - 数据关联:需要根据
item_id去关联对应的category_id。注意,一个商品可能属于多个类目(一对多关系),处理代码需要能容纳这种关系。 - 模型特性:
libFM是一个不考虑特征顺序的模型,因此我们不需要(也无法)利用category_id出现的时间序列信息。
加入 category_id 特征后,模型AUC显著下降至0.7左右。这说明新增的特征并没有带来正向收益,反而可能因为引入了噪声或与现有特征存在不利的交互,导致模型效果变差。
💡 核心总结与进阶思考
本节课我们一起学习了电商推荐系统实战的完整流程:


- 数据预处理是基石:正确处理特征ID的唯一性是模型生效的前提,否则一切后续工作都是徒劳。
- 选择正确的评估指标:在类别不平衡的场景下,AUC比准确率更能反映模型的排序能力。
- 系统化调试:当模型效果不佳(如AUC=0.5)时,应优先排查数据、特征、代码流程中的错误。
- 理解调参的意义:调参是在模型结构和特征确定后,寻找最优解的过程。理解参数对模型的影响比盲目搜索更重要。
- 特征工程的双刃剑:不是所有特征都是有益的。增加特征可能带来负面影响,需要仔细评估和调试。特征的有效性往往与模型特性紧密相关。
对于本案例的进一步优化,最大的潜力可能在于:
- 统计特征离散化:例如,计算“用户的历史点击率”、“商品的热度”等统计值,并将其离散化(分桶)后作为新的特征加入模型。这类特征往往能提供强大的信息。
- 深入理解模型:
libFM本身已经在进行特征间的二阶交互(通过隐向量内积)。所谓的“特征交叉”已由模型自动完成。盲目地手动构造交叉特征可能事倍功半。



最终,推荐算法工程师的核心能力在于:对业务的理解、对数据的敏感、对模型原理的深刻认知,以及通过“Just Do It”的实践精神去不断试错和积累经验。本节课演示的每一个“坑”和解决方案,都是实践中宝贵的经验。





人工智能—推荐系统公开课(七月在线出品) - P4:电商推荐系统架构与模型 📚

在本节课中,我们将要学习电商推荐系统的核心架构与模型。我们将从推荐系统的一般流程开始,深入探讨排序和召回两大核心模块的工作原理,并了解线上AB实验平台如何科学地评估模型效果。课程内容力求简单直白,适合初学者理解。
一、推荐系统的一般架构与流程 🔄
上一节我们介绍了课程概述,本节中我们来看看推荐系统的基础架构。
推荐系统最经典的算法是协同过滤,它分为基于用户的协同过滤和基于物品的协同过滤。其核心思想是构造一个用户-物品的交互矩阵。矩阵的每一行代表一个用户,每一列代表一个商品。如果用户购买或点击了某商品,则标记为1,否则标记为0。
公式: 用户-物品交互矩阵 R,其中 R[u][i] = 1 表示用户 u 对物品 i 有正向行为。
基于此矩阵,可以计算用户或物品之间的相似度,从而进行推荐。然而,现代电商推荐系统已较少直接使用协同过滤,更多采用基于学习的排序方法。
电商推荐与新闻推荐存在差异。新闻推荐主要优化点击率、停留时长和用户留存,推荐内容是文章或视频。电商推荐的核心优化目标是销售额和成交转化,推荐内容主要是商品。
一个典型的用户交互流程是:用户打开应用,推荐引擎在几百毫秒内返回结果,用户产生点击、加购等行为,这些行为被记录到日志服务器中,用于离线构建用户画像和训练模型。
推荐系统的核心流程通常包括召回、过滤、排序、再过滤和调整几个步骤。其中,召回和排序是算法最核心的部分。
整个系统的流水线可以概括为:用户请求到达推荐引擎,引擎结合用户特征、商品特征和实时日志,通过召回和排序模型产生结果,再经过AB实验分流后返回给用户。离线部分则负责日志处理、特征工程和模型训练。
二、排序模块详解 🥇
上一节我们介绍了推荐系统的整体流程,本节中我们重点看看排序模块是如何工作的。
在电商平台中,目前最通用的排序模型结构是深度神经网络。模型底层是输入层,会对稀疏特征进行嵌入编码。
代码: 对用户ID这类稀疏特征进行嵌入操作。
# 假设 user_id 是一个稀疏的整数索引
user_embedding = tf.keras.layers.Embedding(input_dim=num_users, output_dim=32)(user_id_input)
对于列表型特征,如用户标签,通常会对多个标签的嵌入向量进行平均或求和操作,以得到一个固定维度的向量。所有特征的嵌入向量会被拼接起来,作为DNN模型的输入。实数值特征则通常直接拼接,不做嵌入。
拼接后的向量会经过若干层全连接层,使用ReLU或Sigmoid等激活函数。模型训练通常是一个二分类任务,例如预测用户是否会点击商品。
公式: 模型输出为用户点击商品的概率 P(click=1 | user, item)。
这种DNN模型的参数量主要集中在嵌入层,因为用户和商品的数量可能达到亿级。模型训练和上线部署都较为耗时。
在线服务时,排序服务通常被单独部署。推荐主引擎将用户上下文信息发送给排序服务,排序服务加载训练好的模型进行实时打分。从模型训练到线上部署,中间需要严格的质量验证流程,以确保新模型不会引起线上故障。
为了捕捉用户实时兴趣和解决商品冷启动问题,系统还需要支持特征和模型的实时更新。这通过处理实时数据流,进行增量训练和模型更新来实现。
然而,仅按点击率排序是不够的,它可能面临三个问题:
- 推荐结果缺乏多样性。
- 需要平衡多个优化目标。
- 在线计算资源有限,无法对所有候选商品打分。
以下是针对这些问题的一些解决方案:
解决多样性问题
一种常见方法是在排序分数中引入多样性分数。
公式: 最终分数 = α * 相关性分数 + (1 - α) * 新颖性分数
其中,新颖性分数可以通过计算当前候选商品与已推荐商品集合的KL散度来衡量。算法可以采用贪心策略,逐步选择能使总体多样性分数最大的商品。
解决多目标优化问题
一个直接的实践方案是为每个目标单独训练模型,例如点击率模型、转化率模型。
公式: 综合分数 = w1 * CTR_score + w2 * CVR_score + ...
最后对各个模型的输出进行加权融合。虽然学术界有更复杂的多任务学习模型,但在工业界,加权融合仍是常见且有效的做法。
解决计算资源有限问题
采用多轮排序的漏斗机制。首先从亿级商品库中通过召回策略筛选出万级候选商品,然后经过“粗排”模型筛选出千级,再经过“精排”模型筛选出百级,最后进行重排得到最终结果。越靠后的环节,模型可以越复杂,对效果的影响也越直接。
三、召回模块详解 🎣
上一节我们深入探讨了排序,本节中我们来看看如何从海量商品中筛选出候选集,即召回模块。
召回的目标是从上亿的商品库中,快速筛选出几千个可能相关的候选商品。其核心是建立高效的索引。
常见的召回策略包括:
- 基于物品的协同过滤
- 热门商品/类目召回
- 实时促销商品召回
召回结果依然数量庞大,因此通常会用轻量级模型进行第一轮“粗排”,例如逻辑回归或特征较少的GBDT模型,以控制计算延迟。
除了基于索引的召回,另一种常见方法是使用嵌入向量的K近邻搜索。这涉及到三个关键问题:
- 如何生成用户和商品的嵌入向量。
- 如何定义向量间的距离。
- 线上如何快速检索Top-K近邻。
对于问题三,直接计算用户向量与所有商品向量的距离是不现实的。工业界采用近似最近邻搜索算法,例如对商品向量进行聚类,先计算用户向量与聚类中心的距离,再在最近的中心内搜索,从而大幅提升检索效率。
四、AB实验平台 ⚖️


上一节我们介绍了召回策略,本节中我们了解一下如何科学地评估不同策略或模型的效果,即AB实验。

在线上同时进行多个实验时,如果简单地将流量均匀分桶,每个实验获得的流量会很少,导致结论置信度低。因此,引入了分层实验的概念。

核心思想是将系统划分为多个正交的层,例如召回层、排序层、业务策略层。每一层都拥有100%的流量,可以独立进行实验。通过不同的哈希函数将用户请求分配到不同层的不同实验中,保证了层与层之间的实验互不干扰。
这样做的优点是每个实验都能获得充足的流量进行快速迭代。关键在于必须严格保证各层之间的正交性,避免实验结论相互污染。
五、课程总结与进阶学习 🚀
本节课中我们一起学习了电商推荐系统的核心知识。
我们首先回顾了推荐系统的一般架构与流程,理解了电商推荐的特点。然后,我们深入分析了排序模块,学习了DNN模型的结构、线上服务流程,以及如何解决多样性、多目标优化和计算资源限制等实际问题。接着,我们探讨了召回模块的角色与常见策略,包括索引召回和嵌入向量召回。最后,我们介绍了AB实验平台的分层设计原理,这是科学评估模型效果、进行快速迭代的基石。



由于公开课时间有限,许多细节无法展开。如果您希望深入了解工业界真实场景下的推荐系统实战,包括使用企业真实数据集进行项目演练,可以关注《推荐系统高级实训营》课程。该课程涵盖了从特征工程、深度排序模型、在线学习到多目标排序等前沿技术,并提供多个基于京东等电商平台真实数据的实战项目。



注: 课程中提到的福利活动(如领取优惠券)请以官方最新信息为准。本教程内容根据公开课内容整理,聚焦于技术知识本身。


人工智能—推荐系统公开课(七月在线出品) - P5:工业界的推荐系统解密 🧠

在本节课中,我们将要学习工业界推荐系统的核心架构与实践流程。课程将分为四个主要部分:推荐系统的网络结构、特征与特征工程、召回策略以及精排模型。我们将深入探讨每个环节在真实业务场景中的应用,并介绍相关的核心概念与技术栈。

推荐系统的整体架构 🏗️


首先,我们来了解一下一个工业级推荐系统的整体架构。下图展示了一个典型的推荐系统分层结构,它反映了公司内部推荐系统的运作情况。

整个架构可以分为几层,我们依次来看。


最底层是与离线数据相关的数据存储层。在工业界,数据通常不会以简单的CSV或TXT文件存储,而是有成熟的数据体系。源头通常是工程系统记录的日志,然后通过中间层和业务层进行消费。这涉及到许多技术栈,例如日志落盘、实时处理框架(如Kafka这类消息队列机制)等。
对于实时计算,业界常使用如Storm、Spark Streaming、Flink等实时计算框架。离线数据则通常存储在HDFS文件系统或Hive表中。模型的训练和预测也涉及上下游依赖,例如特征来源、标签来源以及打分环节(如召回和精排)。这些都是推荐系统架构中的重要组成部分。




最上层是业务层。推荐系统在业务中有多种应用场景,例如在电商平台购物后,结果页会展示相似商品。这些场景基本都遵循引擎层、计算层、模型服务层和业务层的分层架构,这在各大公司中大同小异。

架构图中还有一列是监控层。因为推荐系统与用户体验息息相关,模型更新时需要监控其是否符合业务预期(业务监控),以及服务的调用量、请求TPS等系统指标(系统监控)。这些都是系统稳定运行的重要保障。



关于推荐系统方向的工程师角色,一般分为三类:平台研发工程师、AI系统架构师和算法工程师。平台研发负责业务系统(如展示推荐结果的页面系统);AI系统架构负责模型服务部署、分布式计算引擎(如MapReduce、K8S)等;算法工程师则负责模型研发、数据分析等。这三类角色在工作中需要紧密协作。

特征与特征工程 🔧

上一节我们介绍了推荐系统的整体架构,本节中我们来看看推荐系统的基石——特征与特征工程。首先需要明确什么是Item(物品)。



Item的定义


Item通常指需要被打分的个体。在搜索中,它是等待被搜索的个体;在推荐中,它是等待被推荐的个体。但在实际业务中,情况往往更复杂。例如,同一款耐克鞋在不同店铺(Shop ID不同)出售,是视为不同的Item还是同一个Item?这涉及到如何利用Shop ID、Category ID等信息对Item进行精确刻画。推荐系统的目标是为不同用户推荐最合适的Item,因此对Item的刻画越精确越好。

Item特征


以下是Item常用的几类特征。


1. Item的类别特征
这类信息比较直观,例如Shop ID、Category ID等。
2. Item的统计类特征
这类特征基于用户行为计算。首先介绍两个概念:
- 曝光:向用户展示了一个Item。
- 点击:用户点击了被展示的Item。
在二分类任务中,常将“曝光未点击”标记为label=0,“曝光且点击”标记为label=1。
基于此,可以构造统计特征:
- 曝光次数
- 点击次数
- 点击率:
CTR = 点击次数 / 曝光次数。CTR是模型预估的核心目标之一,将其作为特征也有助于模型学习。

仅有这三个统计量可能不够,我们常将其扩展为滑动窗口统计。例如:
- 最近1小时的曝光次数
- 最近3小时的曝光次数
- 最近12小时的曝光次数
这些数据会随时间窗口向前滚动更新,这类特征也常被称为实时特征。
离散特征与连续特征
区分这两种特征很重要,因为它们在表达意义和模型适用性上有所不同。


- 离散特征:取值是有限的、分离的。例如:站点、性别。
- 连续特征:取值是平滑、连续的。例如:CTR。


有些特征介于两者之间,例如年龄。它既可以作为连续特征(值就是年龄数字),也可以作为离散特征。将连续特征转为离散特征的过程称为分桶。例如,将年龄每10岁分为一组(11-20岁,21-30岁等)。从离散特征到连续特征的转化,则通常通过Embedding的方式实现。
选择离散还是连续特征,常与模型特性相关:
- LR等线性模型:偏好离散特征。因为LR对特征值波动敏感,离散化可以降低特征间变化的相互影响。
- GBDT等树模型:偏好连续特征。树模型在分裂时需要搜索,对连续特征搜索空间的管理(如取百分位)比处理庞大的离散空间更高效。


关于分桶的方法,主要有两种:
- 人工分桶:基于业务理解(如按年龄段)或统计信息(如按分位数)进行划分。
- 自动分桶:例如使用GBDT+LR模型。GBDT模型在构建树的过程中,每一次特征分裂都隐含了一种分桶逻辑。将样本最终落入的叶子节点进行编码,就形成了一种新的离散特征表示,这本质上也是一种自动分桶。
用户画像 👤




用户画像是用户建模的基础。常见的用户特征包括:
- 基本特征:年龄、性别等。
- 社会身份类特征:职业、教育背景等。
- 用户生命周期类特征:与具体场景强相关,例如在App中的活跃度(日活、月活)。不同业务场景(如会员页和普通页)可能需要不同的生命周期建模。
- 类目偏好特征:用户通常不会显式声明喜好,因此这类特征多通过用户行为(如购买、浏览记录)建模得出,例如计算购买食品类商品占总消费的比例。对于不活跃的用户,也可能通过模型预测其偏好。
特征挖掘依赖于对业务目标和埋点数据的深刻理解。算法工程师需要深入业务,提出数据埋点需求,并设计合理的建模目标(Label)。例如,优化日活(DAU)不能简单用“今日是否登录”来建模,而可能需要将其转化为“用户是否使用优惠券”等更具体、可建模的业务问题。




User-to-Item特征
这类特征是用户和物品的交叉特征。假设有用户U1、U2和物品I1、I2,一个U2I特征可以是“U1是否浏览过I1”。每个用户对每个物品都可以构造这样的特征,例如浏览次数、滑动窗口内的浏览统计等。

U2I特征与普通的特征交叉(如用户年龄与收入交叉)有所区别,它更强调用户与物品之间的直接关联,通常与场景的信号更强。


召回策略 🔍
上一节我们探讨了特征工程,本节中我们来看看推荐流程的第一步——召回。召回的目标是从海量(例如亿级)的候选Item池中,快速筛选出相对较少的(例如千级)Item集合,送给后续的精排环节。


召回环节的特点是:
- 复杂度低:模型相对简单。
- 速度快:需要快速响应。
- 特征少:使用的特征相对精简。
- 规则与模型结合:常结合业务规则和简单模型。
精排则是对召回得到的千级Item进行精确打分和排序,最终选出Top结果(如10个)。精排模型可以非常复杂(如深度神经网络),使用的特征维度也可能极高(上亿维)。



在召回模型中,上下文特征是一类特殊特征。它指的是在系统实时服务时才能获取的信息,例如当前时间、用户设备类型(iOS/Android)。这与实时特征(如实时CTR)不同,实时特征是系统计算好的,而上下文特征与当前请求的上下文状态强相关。


协同过滤
协同过滤是推荐系统的经典方法,主要有三种类型:
- 基于用户的协同过滤:计算用户之间的相似度。如果用户A和用户B相似,那么B喜欢的物品可以推荐给A。
- 基于物品的协同过滤:计算物品之间的相似度。通过用户历史喜欢的物品,找到与这些物品相似的其他物品进行推荐。
- 基于模型的协同过滤:这里我们重点介绍SVD和SVD++。

SVD(奇异值分解)




SVD是一种矩阵分解技术。在推荐场景中,我们有一个用户-物品评分矩阵Y,其中有很多缺失值(用户未评分)。我们的目标是补全这些缺失值,高分值即表示用户可能喜欢。
传统SVD要求矩阵是稠密的,因此对于稀疏的评分矩阵,直接补零再进行分解效果不佳。于是提出了适用于推荐系统的SVD模型。
我们定义模型为:Y_hat = V_u · V_i,其中V_u和V_i分别是用户和物品的向量表示(即Embedding)。我们的目标是最小化以下损失函数:
L = Σ (Y - Y_hat)^2 + λ(||V_u||^2 + ||V_i||^2)
其中,第一项是均方误差,第二项是L2正则项,用于防止过拟合。V_u和V_i可以通过梯度下降法进行优化。
Embedding的数学概念是将一个高维稀疏向量空间X,通过映射函数F,压缩到一个低维稠密向量空间Y。压缩后的维度(Embedding Size)是一个超参数,类似于神经网络中每层的大小。
SVD++




SVD++在SVD的基础上,进一步利用了用户的历史行为信息。它不仅使用用户向量V_u,还引入了用户评分过的所有物品的集合,用这些物品的向量Q_j之和来增强用户表示。


因此,SVD++的用户表示为:V_u + (Σ Q_j) / sqrt(N_u),其中N_u是用户评分的物品数量,除以sqrt(N_u)是一种归一化操作,以平衡不同用户行为数量级的差异。这样,模型同时考虑了用户的直接偏好(V_u)和其历史行为隐含的偏好(Σ Q_j)。



精排模型 🥇



在召回阶段筛选出候选集后,精排阶段负责进行精确排序。本节我们将介绍两个重要的深度学习精排模型。
Wide & Deep 模型



Wide & Deep模型巧妙地将线性模型与深度神经网络结合。
- Wide部分:通常是一个线性模型(如LR),负责记忆(memorization),学习特征中特定、精确的规则。
- Deep部分:是一个深度前馈神经网络,负责泛化(generalization),通过多层网络对特征进行高级抽象,学习更通用的模式。

两部分联合训练,最终输出相结合。如果将Wide部分替换为FM(Factorization Machines),则模型变为DeepFM。Wide & Deep模型是许多深度学习推荐模型的起点,它奠定了同时利用记忆与泛化能力的框架。



ESMM 模型


ESMM是阿里巴巴提出的解决CVR预估难题的模型。在电商场景中,用户行为漏斗是:曝光 → 点击 → 转化(购买)。我们分别关注:
- CTR:点击率
- CVR:点击后转化率
- CTCVR:曝光后转化率



CVR预估的难点在于,正样本(点击后转化)非常稀疏。ESMM通过多任务学习巧妙地解决了这个问题。


模型包含两个子网络:
- CTR预测塔
- CVR预测塔
它们共享底层特征输入。模型的损失函数由两部分组成:
- CTR任务的损失(使用全部曝光样本)。
- CTCVR任务的损失(
CTCVR = CTR * CVR,也使用全部曝光样本)。

通过这种方式,CVR塔虽然只输出CVR,但其训练受到了全部曝光样本(通过CTCVR任务)的间接监督,缓解了样本稀疏问题。线上预测时,我们使用CVR塔的预测值。

ESMM是一个根据具体业务场景(样本稀疏)设计网络结构的优秀范例。类似的还有DIN(Deep Interest Network)等模型。算法工程师需要在经典模型基础上,结合业务理解进行创新。
总结 📝
本节课我们一起学习了工业界推荐系统的核心知识。我们从宏观架构入手,了解了分层设计的系统全貌。然后深入探讨了特征工程,包括Item特征、用户画像、特征类型及处理方式。接着,我们分析了召回阶段的作用与经典协同过滤算法(SVD, SVD++)。最后,我们介绍了精排阶段的深度学习模型,如Wide & Deep和解决实际业务难题的ESMM模型。

希望本课程能帮助你建立起对工业级推荐系统的整体认知,并为深入该领域的学习和实践打下基础。
人工智能—推荐系统公开课(P6):基于内容和协同过滤的推荐系统 🧠
在本节课中,我们将学习推荐系统中两种经典且重要的方法:基于内容的推荐和协同过滤推荐。我们将了解它们的基本原理、适用场景以及如何实现。
概述 📋
推荐系统是现代互联网应用的核心功能之一。本节课将重点介绍两种基础的推荐算法:基于内容的推荐和协同过滤推荐。我们将探讨它们如何工作、各自的优缺点以及在实际场景中的应用。
一、基于内容的推荐 📚
上一节我们概述了课程内容,本节中我们来看看第一种推荐方法:基于内容的推荐。
基于内容的推荐方法被称为 content based recommendation。这种方法有其独特的作用。推荐系统面临一个常见挑战,即“冷启动”问题。如果一个新平台没有大量用户历史行为数据,许多高级算法将无法有效工作。然而,基于内容的算法受此影响较小。
这种方法通常用于与文本相关的产品推荐。它完全基于用户喜欢的物品的属性进行推荐。因此,需要额外分析物品的内容。例如,分析一本书的章节、主题或作者风格,并为其添加标签。
其核心思想是:无需考虑用户之间的关联,只需考虑用户与当前商品之间的匹配程度。
核心概念与实现
以下是基于内容的推荐系统构建步骤:
构建物品特征向量:为每个待推荐的内容(如新闻、书籍)构建一份特征资料。常用方法是 TF-IDF。
- TF (Term Frequency):词在当前文档中出现的频率。频率越高,重要性可能越高。
- IDF (Inverse Document Frequency):词在所有文档中出现的频率的倒数。在所有文档中出现越频繁,其区分度越低,重要性可能越低。
- 通过TF-IDF,可以为文档中的每个词计算一个权重,从而将文档表示为一个权重向量。
# 伪代码示例:使用TF-IDF构建文档向量 from sklearn.feature_extraction.text import TfidfVectorizer documents = [“文档1内容”, “文档2内容”, ...] vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform(documents) # 得到文档-词权重矩阵构建用户偏好向量:根据用户历史浏览或喜欢的多个文档,计算这些文档向量的平均值或通过类似TF-IDF的方法,得到一个代表用户偏好的权重向量。
计算相似度并推荐:获得物品向量和用户向量后,计算它们之间的相似度。常用方法是余弦相似度。
- 公式:
cosine_similarity(A, B) = (A·B) / (||A|| * ||B||) - 值越接近1,表示方向越一致,相似度越高。
- 对所有候选物品计算与用户偏好向量的相似度,选取相似度最高的物品进行推荐。
- 公式:
示例说明
假设一个用户喜欢书籍 《Building Data Mining Applications for CRM》。系统会:
- 为所有书籍(包括用户喜欢的这本)的标题构建TF-IDF向量。
- 将用户喜欢的这本书的向量作为其偏好向量。
- 计算其他书籍向量与该偏好向量的余弦相似度。
- 推荐相似度最高的前三本书。
二、协同过滤推荐 🤝
上一节我们介绍了基于内容的推荐,本节中我们来看看另一种主流方法:协同过滤。
协同过滤是一种基于邻域的算法。其核心思想是“物以类聚,人以群分”。如果你想知道某部电影是否好看,可以询问兴趣相投的朋友的意见。协同过滤正是基于这种逻辑。
它主要分为两种模式:
1. 基于用户的协同过滤
基于用户的协同过滤的核心是找到与目标用户兴趣相似的其他用户(邻居),然后根据这些邻居的喜好来预测目标用户的喜好。
工作流程如下:
- 找到与目标用户有共同评分行为的其他用户。
- 计算目标用户与这些用户之间的相似度(如余弦相似度、皮尔逊相关系数)。
- 选取最相似的K个用户作为“邻居”。
- 根据邻居们对某个物品的评分,进行加权平均,预测目标用户对该物品的评分。
- 公式(加权平均):
预测评分 = sum(邻居相似度 * 邻居对该物品评分) / sum(邻居相似度)
- 公式(加权平均):
2. 基于物品的协同过滤
基于物品的协同过滤的核心是计算物品之间的相似度。如果用户喜欢物品A,而物品B与A非常相似,那么用户也可能喜欢物品B。
工作流程如下:
- 构建用户-物品评分矩阵(一个稀疏矩阵)。
- 计算物品之间的相似度。通常只考虑同时对两个物品都有评分的用户向量。
- 常用相似度度量:调整余弦相似度、皮尔逊相关系数。
- 皮尔逊相关系数公式(简化理解):先减去各自评分的均值,再计算余弦相似度。
- 对于目标用户未评分的物品,找出与该物品最相似的K个物品(这些物品用户已评分)。
- 根据这K个相似物品的评分和相似度,加权预测目标用户对当前物品的评分。
相似度/距离度量
在协同过滤中,衡量用户或物品之间的相似性至关重要。以下是几种常见方法:
- 欧氏距离:衡量空间中的直线距离。距离越小,相似度越高。
- 公式:
distance = sqrt(sum((xi - yi)^2))
- 公式:
- 杰卡德相似系数:适用于只有交互行为(如点击、购买)而无具体评分的场景。衡量集合的交集与并集的比例。
- 公式:
J(A,B) = |A∩B| / |A∪B|
- 公式:
- 余弦相似度:衡量两个向量在方向上的差异,忽略长度。常用于文本和评分向量。
- 公式:
cosine_sim(A,B) = (A·B) / (||A|| * ||B||)
- 公式:
- 皮尔逊相关系数:衡量两个变量之间的线性相关性。在计算相似度前会先减去各自的平均值,能消除用户评分尺度不一的影响。
基于物品的协同过滤实例
假设有一个用户-电影评分矩阵,我们想预测用户5对电影1的评分。
- 计算电影1与其他所有电影的相似度(例如使用皮尔逊相关系数)。
- 发现与电影1最相似的两部电影是电影3和电影6,相似度分别为0.41和0.59。
- 用户5对电影3评分为2,对电影6评分为3。
- 预测用户5对电影1的评分 =
(0.41*2 + 0.59*3) / (0.41+0.59) = 2.6
通过这种方式,可以填充评分矩阵中的空白值,从而为用户生成推荐列表。
总结 🎯
本节课中我们一起学习了推荐系统的两种基础而强大的算法。
- 基于内容的推荐:通过分析物品本身的属性/特征,为用户匹配与其历史喜好相似的物品。它不受“冷启动”问题严重困扰,特别适合文本、音乐等特征易于提取的领域。其核心是TF-IDF特征提取和余弦相似度计算。
- 协同过滤推荐:利用群体智慧进行推荐,分为基于用户和基于物品两种。
- 基于用户的协同过滤:找到相似用户,根据他们的喜好做推荐。关键是计算用户相似度。
- 基于物品的协同过滤:找到相似物品,根据用户的历史喜好做推荐。关键是计算物品相似度。
- 两者都依赖于用户-物品评分矩阵,并使用各种相似度度量方法(如余弦相似度、皮尔逊相关系数)进行计算。


这两种方法奠定了现代推荐系统的基础,理解它们对于学习更复杂的混合推荐、深度学习推荐模型至关重要。


人工智能—推荐系统公开课(七月在线出品) - P7:图神经网络在推荐广告场景中的应用 📊

在本节课中,我们将学习图神经网络在推荐和广告场景中的应用。课程内容分为三大部分:首先介绍图神经网络的基础概念,然后探讨其在推荐广告中的具体应用案例,最后分析图神经网络在工业界落地所需的必要组件。

第一部分:图神经网络介绍 🧠

图是一种非常常用的数据结构。典型的例子包括社交网络、用户-商品图、蛋白质结构以及交通路网和知识图谱。这些都可以描述成图结构。甚至规则的网格数据,也是图的一种特殊形式。因此,图是一个非常值得研究的领域。
接下来,我们看看图的研究一般分为哪几种。

以下是图研究的几种主要分类:
- 经典图算法:例如路径搜索、二分图匹配。这些算法在百度地图或高德地图的路径规划,以及滴滴的分单算法中都有应用。
- 概率图模型:例如条件随机场,在自然语言处理领域应用广泛。
- 图神经网络:主要包括图嵌入和GCN,这是我们今天介绍的主题。知识图谱相关内容不在此次讨论范围内。
图神经网络从2016年开始成为一个非常火热的研究方向。这主要有几个原因:首先是深度学习在CV和NLP等规则网格数据领域的成功,促使人们希望将其拓展到不规则的图数据上;其次是图神经网络拥有广泛的使用场景,许多问题都可以建模成图问题。


这里附上一张CV领域的全景图。展示这张图有两个目的:第一,说明GNN的出发点源于CNN在视觉领域和深度学习在NLP领域的成功,是一次成功的迁移;第二,提醒大家,CV、NLP、推荐广告等领域的算法内核其实是一致的,底层原理相通,并且发展有先后顺序。通常,CV领先NLP一到两年,NLP又领先推荐一到两年。因此,从事推荐的同学可以多关注NLP和CV的最新发展,以获得启发。
第二部分:图嵌入详解 🧩
上一节我们介绍了图神经网络概览,本节中我们来看看图嵌入的具体方法。
什么是嵌入?
嵌入在数学上是一个函数,将一个空间映射到另一个空间。通常情况下,是从高维抽象空间映射到低维具象空间。嵌入一般是稠密的分布式表示,这与One-Hot编码相对应。

为什么会出现嵌入这个概念呢?原因有三点:第一,抽象的事物本应有一个低维的表示;第二,计算机更善于处理低维的信息;第三,为了解决One-Hot编码的问题。One-Hot编码的问题在于其维度随物料数量线性增长,并且无法表示词语之间的相似关系。
现在,嵌入技术已经非常成熟,从Word2Vec开始,到Doc2Vec、Node2Vec,以及我们今天要讲的图嵌入。
图嵌入全景图

图嵌入从2016年发展至今,已经衍生出许多分支。我们今天只介绍一些特别重要、必须掌握的方法。
DeepWalk
DeepWalk是图嵌入的“始祖”,其思想非常简单,纯粹借鉴了Word2Vec。想象一下,如果有一个图要做图嵌入,参照Word2Vec还缺什么?Word2Vec有几个要素:词表(对应于图中的节点)、句子(需要从图中构造)和语料库(一堆句子)。DeepWalk就是解决了如何从图上构造这三个要素的问题,其方法就是随机游走。思想非常简单:在一个领域初始阶段,做出贡献相对容易。
具体做法是:给定一个带权图,从一个节点开始,按照边权定义的概率进行随机游走,生成序列。然后并行地进行多次随机游走,得到一批句子(序列),最后按照Word2Vec的方法进行优化即可。
LINE算法
LINE算法在工业界和学术界都应用得非常广泛。其建模思路与DeepWalk已完全不同,不再借鉴Word2Vec。它的思想是定义了两种相似度:一阶相似度和二阶相似度。
- 一阶相似度:由边权决定。边权越大,认为两个节点的相似度越大。
- 二阶相似度:指两个节点邻居的重合度。如果两个节点的邻居集合高度相似,则认为它们的二阶相似度高。

优化目标是让嵌入后的节点相似度分布与定义的一阶/二阶相似度分布尽可能接近。这里引出一个思考题:如何衡量两个分布的差异?常用的方法是交叉熵损失,或更广义的散度。
这里需要注意,一阶相似度和二阶相似度不能直接放在同一个损失函数中相加优化。因为对于某些节点对,一阶相似度可能要求它们远离,而二阶相似度可能要求它们接近,这会导致模型无法学习。通常的做法是分别优化,得到两个图嵌入,然后通过拼接、平均或求和等方式融合。
由此可以得到一个启发:既然可以考虑一阶、二阶相似度,那么是否可以扩展到三阶、四阶相似度呢?答案是完全可以。

Node2Vec算法

Node2Vec也是一个工业界特别常用的算法,其思想来源仍然是Word2Vec,并且改进了DeepWalk。


我们知道DeepWalk的随机游走可以理解为一种深度优先搜索。而LINE算法优化一阶相似度时,可以理解为一种广度优先搜索,它假设与谁边权越大就越相似。Node2Vec则定义了一个二阶的随机游走,平衡了BFS和DFS。
它通过两个参数p和q来控制游走策略:
- 参数p:控制回头率。p越大,越不回头,一直向前走(倾向DFS);p越小,回头概率越大。
- 参数q:控制BFS与DFS的平衡。q=1时,不做区分;q很大时,倾向于BFS(探索直接邻居);q很小时,倾向于DFS(走向远方)。


采样到序列后,就和DeepWalk一样,用Word2Vec的方法优化即可。Node2Vec在广告的Lookalike(相似人群扩展)等场景有应用。

struc2vec算法
前面我们介绍了内容相似性(相邻节点更相似)和结构相似性(在网络中结构地位相似的节点更相似)。DFS能在一定程度上解决结构相似性问题,但如果图很大,节点相隔很远,DFS可能游走不到。struc2vec通过分层的方式定义了一种结构相似性。
其核心思想是:比较两个节点是否相似,先比较它们的一阶邻居是否相似;如果相似,再比较二阶邻居;以此类推。它通过比较节点邻居的度序列(有序列表)的相似度来实现。然后,它会构建一个分层的图,每一层代表一种尺度下的结构相似性,层与层之间通过定义的权重连接。

为什么在风控场景下,struc2vec相对于Node2Vec有提升?最本质的原因是风控场景更需要结构相似性。例如,在支付宝的关系网络中,一个普通用户还款能力强,不代表另一个普通用户还款能力也强;但两个体量相似的商家,它们的还款或借贷能力可能更相似。Node2Vec通过平衡DFS和BFS采样,无法有效获得长距离的结构相似性,而struc2vec可以。
另外,注意到优化目标中的归一化项Z_k。在Word2Vec中,处理Z通常使用负采样或层次Softmax。这里为什么可以用层次Softmax?因为在Word2Vec中,词频不均匀,构建的哈夫曼树不平衡;而在图嵌入中,每个节点出现的“词频”可以视为1,构建的哈夫曼树是平衡的,因此可以使用层次Softmax。

GraphSAGE算法
struc2vec介绍了结构相似性的概念。而GraphSAGE是在推荐系统广告大规模落地中一个非常犀利的武器。

这里先介绍两个概念:归纳式学习和直推式学习。
- 直推式学习:之前介绍的算法都属于此类。给定一个图,学习图中每个节点的嵌入,并存储起来。当图发生变化时,需要重新学习。
- 归纳式学习:GraphSAGE是代表。它既学习节点的嵌入,也学习如何得到这个嵌入的方法论。当图发生变化或出现新节点时,可以通过学到的聚合函数,根据新节点的邻居迅速生成其嵌入。这在推荐系统中非常重要,因为推荐系统的图通常巨大且变化飞快。
GraphSAGE算法分为两步:采样和聚合。
- 采样:对于每个中心节点,采样其一定数量的一阶邻居,然后对每个邻居再采样其一定数量的邻居(二阶),以此类推。
- 聚合:将采样到的邻居信息通过一个聚合函数,逐层聚合到中心节点上,最终得到该节点的嵌入。聚合函数可以是均值池化、最大池化或LSTM等。
GraphSAGE后面我们会介绍其在推荐中的一个重要应用——PinSAGE算法。
GraphWave算法

最后简单提一下GraphWave,这也是一个基于结构相似性假设的算法,但它是完全无监督的,不需要任何先验知识,只需要一个图,通过一些方式就能得到节点的嵌入函数,速度快且效果不错。

图嵌入方法总结
图嵌入最主要的问题是如何选择模型。实际上,选择什么样的模型一定要与你的实际问题相关。最重要的是构图,在图神经网络领域,图往往不是天然形成的,即使像社交网络这样的天然图,也需要进行精炼,例如对边权进行先验加工。在电商领域,一定要把图构好,这是后续所有工作的最关键基础。


第三部分:图卷积网络简介 ⚙️




图卷积网络涉及更多图谱理论背景,在此不做深入介绍,只做类比:
- 图嵌入 类比于 NLP 中的 Word2Vec。
- 图卷积网络 类比于 CV 中的 CNN。

图嵌入一般对应于两阶段建模,即先用某种算法得到图嵌入,再将其作为特征赋能给上游任务。GCN 则通常是端到端的,可以直接做分类等任务。GCN 也可以生成节点嵌入,供上层任务使用。广义上,图嵌入属于 GCN 的一种。图嵌入通常是浅层网络,参数少;而 GCN 可以做得非常深。但需要注意的是,基于谱理论的 GCN 需要在全图上进行卷积,计算性能较慢,目前在工业界大规模落地的场景还不多。







第四部分:图神经网络在推荐广告中的应用案例 🚀



上一部分我们介绍了理论基础,现在进入实践部分,看看图神经网络在推荐广告中的具体应用案例。

应用范式





图神经网络在推荐广告中的应用主要有两大范式:端到端和两阶段。
- 端到端:图神经网络作为模型的一部分,与其他特征处理模型结合,共同进行训练和预测。
- 两阶段:先使用图神经网络预训练得到图嵌入,然后将这些嵌入作为特征,输入到主模型中进行训练。
目前工业界成熟的应用以端到端为主。两阶段建模存在模型更新冲突等问题,应用相对较少。

案例一:GCMC(图卷积矩阵补全)
这是一个比较朴素的方法。其思想很简单:将推荐问题建模为矩阵补全问题,用户-物品交互矩阵中的已知项是真实值,未知项需要预测。做法是构建一个用户-物品二分图,然后对这个图做图嵌入或图卷积,得到用户和物品的嵌入,最后通过计算用户和物品嵌入的相似度来预测链接(即评分)。这篇文章使用的GCN是在全图上操作的,计算开销大,工程复杂性高,不具备落地性,但可以帮助理解GCN在召回中是如何应用的。

案例二:PinSAGE(基于GraphSAGE的大规模落地)
这是基于GraphSAGE的一个成功落地实践,由Pinterest和斯坦福大学共同研究,在工业界的推荐广告场景中被广泛沿用。其业务场景是图片推荐,可以理解为图片版的“抖音”。用户可以将图片收藏到画板中,如果两个图片位于同一个画板,则认为它们之间存在关联,由此构建一个图片-画板二分图。
构图思考:电商场景如何构图?可以只构建商品图(同构图),或用户-商品交互图(异构图)。新闻推荐也可以类似地构建用户-新闻的交互图。

PinSAGE算法:其图神经网络模型基于GraphSAGE,但做了采样和聚合上的优化。采样时不是随机采样,而是基于重要性采样。训练采用有监督方式,正样本是用户短时间内连续点击的图片对,使用带间隔的排序损失进行优化。
工程优化:这篇论文包含大量工程实践,非常值得一读。
- 重要性采样:不是基于随机游走,而是基于边权的重要性进行采样。
- Warm-up:在大批次训练时使用,防止训练初期跑偏。
- 分布式异步训练:利用多GPU并行计算梯度,异步更新。
- 困难负样本挖掘:不仅使用随机负样本,还加入难以区分的负样本,提升模型区分能力。但困难负样本的比例需要逐步增加,如果一开始全部使用最难样本,模型可能无法收敛。
- 服务端优化:使用MapReduce预先计算所有节点的嵌入,避免线上服务时重复计算,然后进行近似最近邻检索。


案例三:淘宝的图嵌入实践


淘宝的实践展示了电商场景如何构图以及图嵌入的演进。
- 早期(2016-17):基于用户会话构建商品序列图,然后使用DeepWalk等算法学习商品嵌入,用于多路召回。
- 升级版:不仅考虑商品实体,还加入了商品的附加信息(Side Information),如品牌、价格、类别等。它采用了端到端范式中“GNN作为主模型”的方式,将商品属性也作为图中的节点,通过注意力机制区分不同属性的重要性,共同学习得到最终的嵌入。
两阶段建模的应用
两阶段建模在工业界也有应用,但公开案例较少。通常适用于拥有多产品线的大公司。例如,阿里可以用全站数据(淘宝、天猫、1688等)构建一个全局图,训练一个通用的图嵌入模型。这个模型学到的用户和商品嵌入,可以赋能给各个独立产品的推荐系统,实现知识的迁移。
第五部分:图神经网络工业落地必要组件 🛠️
最后,我们介绍图神经网络在工业界落地时需要的必要组件。对于大公司倾向于自研,中小公司可使用开源软件进行二次开发。
除了算法,还需要解决许多工程问题:图太大如何存储?如何实现高性能的图操作(如采样)?图神经网络如何与深度学习框架深度结合?
以阿里的高性能图学习平台Euler为例,其架构通常包含以下几层:
- 图存储与分布式引擎:底层是图的分布式存储。存储方式有两种:按点分区和按边分区。按点分区方便并行采样,但可能受热点节点影响;按边分区则能避免数据倾斜问题。
- 图查询语言:提供类SQL的图查询语言,用户编写查询后,底层会进行优化并执行。
- 消息传递抽象层:将图神经网络的核心操作抽象为消息传递、聚合和更新三个步骤,并提供基础算子。
- 算法层:实现常用的图算法,供用户快速调用。
高性能采样实现:在异构图(多种边类型)上,需要支持按不同类型、不同权重进行采样。通常的作法是存储每个节点的邻居列表、边类型、边权以及边权的前缀和。采样时,根据要采样的边类型和随机数,通过二分查找快速定位到具体的邻居节点,从而实现高效采样。
课程总结 📝
本节课我们一起学习了图神经网络在推荐广告场景中的应用。我们从图神经网络的基础概念讲起,详细介绍了多种图嵌入方法及其特点。然后,通过PinSAGE、淘宝等案例,深入分析了图神经网络在工业界的实际应用范式与优化技巧。最后,探讨了图神经网络落地所必需的工程组件,如图存储、高性能采样等。

希望本课程能帮助你建立起对图神经网络在推荐广告领域应用的系统性认识。图神经网络方兴未艾,在构图、算法、工程等方面仍有广阔的探索空间。


人工智能—推荐系统公开课(七月在线出品) - P8:推荐系统算法技术发展(各模型的演进) 📈
在本节课中,我们将要学习推荐系统核心算法技术的发展脉络,重点聚焦于召回与排序两大模块的演进历程。我们将从最基础的协同过滤开始,逐步深入到当前主流的深度学习模型,了解每个阶段的核心思想、代表性模型及其解决的问题。
概述
推荐系统在我们的数字生活中无处不在,无论是观看视频还是在线购物,我们所接触的内容大多由推荐系统生成。对于亚马逊、京东、字节跳动等平台而言,推荐和广告系统的收益占比极高。推荐系统主要基于用户的行为数据,学习其兴趣偏好,进而进行个性化推荐。
其核心流程通常分为两步:召回和排序。召回阶段负责从海量商品(如千万甚至上亿级别)中快速筛选出数百个候选物品,模型相对简单,注重效率。排序阶段则对召回结果进行精细排序,使用更复杂的模型和丰富的特征,以选出最符合用户兴趣的Top-N个物品进行最终推荐。部分场景下还会进行重排序以进一步提升效果。
第一部分:召回模块的发展
上一节我们概述了推荐系统的整体流程,本节中我们来看看召回模块的具体算法是如何演进的。召回的目标是缩小搜索范围,从全量物品中快速筛选出用户可能感兴趣的候选集。
1. 协同过滤(Collaborative Filtering)
协同过滤是推荐系统最经典的算法之一,其核心思想是利用用户或物品之间的相似性进行推荐。它主要分为两类:基于用户的协同过滤和基于物品的协同过滤。
基于用户的协同过滤(UserCF):寻找兴趣相似的用户,将相似用户喜欢的物品推荐给目标用户。它更适用于社交性强、用户兴趣相对稳定的场景,如新闻推荐。
公式:计算用户相似度常用余弦相似度。例如,用户A和用户B的向量分别为 Va 和 Vb,其相似度为:
sim(A, B) = (Va · Vb) / (||Va|| * ||Vb||)
基于物品的协同过滤(ItemCF):寻找物品之间的相似性,将与用户历史喜欢物品相似的物品推荐给用户。它更适用于用户兴趣变化快、物品相对稳定的场景,如电商推荐。
2. 关联规则召回
关联规则召回基于用户的行为序列,挖掘物品之间的共现关系。其核心思想是,在同一行为窗口(如同一购物车、同一浏览会话)内出现的物品具有关联性。
核心概念:关联强度不仅取决于是否共现,还受到共现距离的影响。距离越近的物品,关联性越强。
公式:通常会给关联强度一个基础分(如0.8),并根据共现距离进行衰减。例如,物品i和j的关联分数可定义为:score(i, j) = base_score ^ |pos(i) - pos(j)|,其中 pos 表示物品在序列中的位置。
以下是关联召回的基本步骤:
- 从用户历史行为序列中,滑动窗口提取物品对。
- 根据上述公式计算每对物品的关联分数。
- 对于目标用户,聚合其历史物品的所有关联物品,并按分数排序。
- 剔除用户已交互过的物品,取Top-N作为召回结果。
3. 单向量召回(Single Embedding)
单向量召回的核心思想是为每个用户和物品学习一个稠密的向量表示(Embedding),通过向量相似度(如余弦相似度)进行快速匹配。线上服务时,使用近似最近邻搜索(如Faiss库)加速检索。
经典模型:YouTube DNN
该模型是业界标杆。它将用户观看历史、搜索历史等特征通过Embedding层和池化层(如平均池化)转化为固定长度的向量,再经过多层全连接网络,最终输出用户向量。物品向量则通过Softmax层的权重得到。由于物品数量巨大,训练时会采用负采样技术。
经典模型:双塔模型(DSSM)
双塔模型最初用于语义匹配,后被引入推荐系统。它包含两个独立的“塔”式神经网络,分别处理用户特征(如历史行为、人口属性)和物品特征。两个塔的输出向量进行点积或余弦相似度计算,得到匹配分数。线上服务时,同样预存物品向量,通过向量检索进行召回。
4. 多向量召回(Multi-Embedding)
单向量召回假设用户兴趣是单一的,但实际用户兴趣往往是多元的。用一个向量表示所有兴趣,可能会被高频兴趣主导,导致推荐多样性不足。
经典模型:MIND(Multi-Interest Network with Dynamic Routing)
MIND模型使用胶囊网络(Capsule Network)为用户生成多个兴趣向量。其关键结构是动态路由层,它能够根据当前候选物品,自适应地激活和组合用户的不同兴趣胶囊,从而生成更具针对性的用户表示。这更好地建模了用户的多方面兴趣。
5. 图嵌入召回(Graph Embedding)
图嵌入召回将用户行为序列构建成图结构,利用图算法学习物品的Embedding。它能捕获物品之间复杂、高阶的关联关系。
经典算法:DeepWalk
DeepWalk通过随机游走(Random Walk)在用户-物品交互图上生成物品序列,然后将这些序列视为“句子”,使用Word2Vec中的Skip-gram模型来学习物品的向量表示。游走时可以设置边权(如点击、购买赋予不同权重)来影响游走路径。
经典算法:Node2Vec
Node2Vec是DeepWalk的扩展,它通过调整游走策略,在广度优先搜索(BFS)和深度优先搜索(DFS)之间取得平衡。BFS倾向于学习结构的相似性(同质性),DFS倾向于学习内容的相似性(结构性),从而得到更丰富的节点表示。
6. 增强图嵌入与知识图谱召回
基础的图嵌入仅使用了物品ID信息,对于新品或冷门物品(冷启动问题)效果不佳。


经典模型:EGES(Enhanced Graph Embedding with Side Information)
EGES模型在DeepWalk的基础上,引入了物品的边信息(Side Information),如类别、品牌、价格等。它为每种边信息也学习一个Embedding,然后通过注意力机制加权融合物品ID的Embedding和所有边信息的Embedding,生成最终的物品向量。这有效缓解了冷启动问题。
知识图谱召回
知识图谱召回利用结构化的知识库(包含商品、类别、属性、品牌等实体及它们之间的关系)进行推荐。它可以实现更精准的搭配推荐(如手机配手机壳),并能结合时效性热点(如利用历史爆款信息预测新品潜力)。通过在图谱上推理,可以挖掘用户深层的兴趣概念,而不仅仅是具体的商品ID。
第二部分:排序模块的发展
上一节我们介绍了召回模块如何从海量数据中快速筛选候选集,本节中我们来看看排序模块如何对这些候选进行精细打分与排序。排序阶段是推荐系统的“精加工”环节,模型复杂,特征工程至关重要。
1. 人工特征与线性模型时代(2010年前)
这个阶段的核心是特征工程。模型本身(如逻辑回归LR)相对简单,效果严重依赖于人工构建的特征质量。
特征处理方式:
- 类别特征:先进行Label Encoding(转换为0,1,2,...),再进行One-Hot Encoding,转化为稀疏向量。
- 连续特征:进行归一化(如Min-Max Scaling),以消除量纲影响。也可进行分桶(Binning),将其转化为类别特征,便于后续交叉。
- 特征交叉:人工构造二阶、三阶特征组合(如“性别_年龄_职业”),以捕捉特征间的交互作用。
- 专家特征:基于业务理解构造的特征(如复购周期、购买力)。
模型通常采用逻辑回归(LR),其输出是一个0到1之间的概率值,表示用户点击或转化的可能性。
公式:P(y=1|x) = 1 / (1 + exp(-(w0 + Σ wi*xi))),其中 xi 是特征,wi 是权重。
2. 自动特征交叉与非线性模型发展期(2010-2015)
此阶段,模型开始具备自动学习特征交叉的能力,减轻了对人工特征工程的依赖。
因子分解机(FM)
FM在LR的基础上,增加了自动学习二阶特征交叉的能力。它通过为每个特征学习一个隐向量,通过隐向量的内积来建模特征交叉的权重。
公式:y(x) = w0 + Σ wi*xi + Σ Σ <vi, vj> * xi * xj。FM的参数复杂度是线性的,计算高效。
场感知因子分解机(FFM)
FFM是FM的改进,引入了“场”的概念。同一个特征在与不同特征域的特征进行交叉时,会使用不同的隐向量,建模更加精细。
梯度提升树(GBDT)+ LR
Facebook提出的经典模型。先用GBDT对原始特征进行自动组合与筛选,GBDT每棵树的叶子节点对应一种特征组合。然后将样本落入的叶子节点进行One-Hot编码,生成新的高维稀疏特征向量,再输入给LR进行训练。GBDT负责进行高阶特征交叉,LR负责最终拟合。
3. 深度学习时代(2015年至今)
深度学习模型通过多层神经网络自动学习高阶、非线性的特征交互,成为当前的主流。
模型演进的核心方向:
- 离散特征Embedding化:将高维稀疏的类别特征映射为低维稠密向量,作为模型输入。
- 显式与隐式特征交叉:
- 显式交叉:如PNN(Product-based Neural Network)在Embedding层后显式地引入内积/外积操作来捕获二阶交互。
- 隐式交叉:通过多层全连接网络(DNN)自动学习高阶特征交互。
- 记忆与泛化结合:
- 记忆:通过线性模型(Wide部分)记忆历史数据中频繁出现的特征模式。
- 泛化:通过深度网络(Deep部分)泛化到未曾出现过的特征组合。
- 引入注意力机制:对用户历史行为序列中的不同物品赋予不同的权重,动态响应当前候选物品。
经典模型:Wide & Deep
谷歌提出的模型,结构简单有效。Wide部分是线性模型(如LR),用于记忆;Deep部分是DNN,用于泛化。两部分联合训练。
# 伪代码概念
final_logits = linear_layer(wide_features) + dnn_layer(deep_features)
output = sigmoid(final_logits)
经典模型:DeepFM
DeepFM用FM替换了Wide & Deep中的Wide部分。FM负责显式地学习二阶特征交叉,DNN负责学习高阶隐式交叉。两者共享Embedding层输入,端到端训练。
经典模型:DIN(Deep Interest Network)
DIN针对用户历史行为序列设计。传统方法(如平均池化)平等对待所有历史行为。DIN引入了注意力机制,根据当前候选物品,动态计算用户历史行为中每个物品的权重,从而生成与当前候选相关的用户兴趣表示。
核心思想:用户兴趣是多样的,且针对不同的候选物品,其兴趣的侧重点应不同。
多任务学习
在实际业务中,我们往往需要同时优化多个目标,如点击率(CTR)和转化率(CVR)。多任务学习模型通过共享底层特征和网络参数,同时学习多个相关任务,能有效利用数据,缓解数据稀疏问题,并防止模型在单一任务上过拟合。
第三部分:总结与展望
本节课中我们一起学习了推荐系统召回与排序两大核心模块的技术演进史。
召回模块的发展从基于规则的协同过滤和关联召回,发展到基于表示学习的单向量/多向量召回、图嵌入召回,再到融合丰富信息的增强图嵌入与知识图谱召回。其趋势是:从统计规则走向表示学习,从单一兴趣建模走向多元兴趣与复杂关系建模,并不断融合更多辅助信息以解决冷启动等问题。
排序模块的发展从依赖人工特征工程的线性模型,演进到具备自动特征交叉能力的因子分解机和树模型,最终进入以深度学习为主导的时期。深度学习模型通过Embedding、复杂网络结构(如注意力机制、序列建模)、以及多任务学习等技术,极大地提升了模型的表达能力和效果。




未来趋势包括但不限于:强化学习与推荐的结合以实现更优的长期收益;更强大的序列建模(如Transformer)在推荐中的应用;以及多模态信息(图像、文本、视频)的深度融合。

总而言之,推荐系统算法的发展是一个不断吸收机器学习、深度学习、自然语言处理、图计算等领域最新成果的过程,其核心目标始终是更精准、更高效、更个性化地连接用户与内容。
人工智能—推荐系统公开课(七月在线出品) - P9:推荐系统导论:从推荐算法到案例应用
概述
在本节课中,我们将要学习推荐系统的基本概念、核心算法及其评估方法。课程将从推荐系统的存在意义讲起,介绍其基本结构和评估准则,然后深入讲解一系列在工业界非常实用的推荐算法,包括内容推荐、协同过滤、矩阵分解以及基于用户行为序列的Word2Vec应用。最后,我们会通过简单的代码案例来理解部分算法的实现。
为什么需要推荐系统?
互联网数据量爆炸式增长带来了信息过载问题。一个人每天接触到的文字、声音和图像信息量巨大,各类平台每天新增的内容也远超个人的处理能力。为了解决用户在海量信息中快速找到所需内容的问题,推荐系统应运而生。
早期的解决方案是分类导航(如雅虎)和搜索引擎,它们需要用户主动提供关键词。然而,人们越来越希望系统能主动发现其兴趣和需求,甚至带来惊喜(Surprise)。推荐系统正是为了满足这种“被动获取个性化信息”的需求而存在。
对于用户而言,推荐系统能帮助发现新鲜事物、辅助决策、节省时间。对于商家而言,它能提供更个性化的服务,提高用户粘性和信任度,并直接带来显著的营收增长。例如,Netflix三分之二的电影观看源于推荐,亚马逊35%的营业额来自推荐,今日头条半数以上的点击也来源于推荐。
推荐系统是什么?
通俗理解
推荐系统会根据用户的历史行为、社交关系、兴趣点以及当前所处的上下文环境,来判断用户的当前需求和兴趣点,并将合适的商品或内容推荐给用户。
- 历史行为:是判断用户兴趣的主要数据来源,例如阅读过的新闻、购买过的商品。
- 社交关系:当用户历史行为数据不足(冷启动问题)时,可以通过其社交关系来推测其可能喜好。
- 兴趣点:可通过用户注册信息或历史行为挖掘获得。
- 上下文环境:指用户当前的情境或浏览内容,对于提升推荐精准度至关重要。例如,即使用户历史喜欢衬衫,但若当前一直在浏览牛仔裤,则推荐牛仔裤可能更有效。
数学定义
用数学语言更精确地描述,推荐系统要做的是:
给定全体用户集合 C 和全体商品(或内容)集合 S,以及一个评估函数 U。对于任意一个用户 c ∈ C,推荐系统需要遍历所有商品 s ∈ S,计算评估函数 U(c, s) 的值,然后选择使该函数值最大的商品 s 推荐给用户 c。
用公式表示核心决策过程为:
s' = argmax_{s ∈ S} U(c, s)
其中,U(c, s) 衡量了将商品 s 推荐给用户 c 的效用或得分。
推荐系统的基本结构
一个典型的推荐系统结构包含线下(Offline)和线上(Online)两部分。
- 线下部分(Offline):
- 数据处理:包括数据清洗、特征提取等,为模型准备干净、有效的输入数据。
- 模型训练:使用处理后的数据训练推荐模型,目标是优化准确度等指标。
- 线上部分(Online):
- 模型加载:将训练好的模型部署到线上环境。
- 上下文感知:结合用户当前的实时行为和环境信息。
- 生成推荐:利用模型和上下文信息,实时生成推荐结果并返回给用户。
线下部分的核心是模型准确度,线上部分则对速度和实时性有极高要求。
如何评估推荐系统?
评估推荐系统好坏需要多方面的标准,而不仅仅是准确度。
1. 准确度(针对打分系统)
对于允许用户显式评分(如5星评分)的系统,常用均方根误差(RMSE)和平均绝对误差(MAE)来衡量预测评分与实际评分的差距。
- RMSE(均方根误差):
其中,RMSE = sqrt( (1/|T|) * Σ_{(u,i)∈T} (r_{ui} - r̂_{ui})^2 )r_{ui}是用户 u 对商品 i 的实际评分,r̂_{ui}是预测评分,T 是测试集。 - MAE(平均绝对误差):
MAE = (1/|T|) * Σ_{(u,i)∈T} |r_{ui} - r̂_{ui}|
2. 准确率与召回率(针对Top-N推荐)
对于隐式反馈(如点击/购买)系统,用户只有“感兴趣”或“不感兴趣”的行为。常用准确率(Precision)和召回率(Recall)来评估。
- 准确率:推荐的商品中,用户真正感兴趣的比例。
Precision = |推荐集 ∩ 用户真实感兴趣集| / |推荐集| - 召回率:用户真正感兴趣的商品中,被系统推荐出来的比例。
Recall = |推荐集 ∩ 用户真实感兴趣集| / |用户真实感兴趣集|
3. 覆盖率
覆盖率衡量推荐系统能够发掘长尾商品、避免马太效应(热门商品越来越热)的能力。
- 简单覆盖率:被推荐过的商品种类数占总商品种类数的比例。
Coverage = |被推荐过的商品集合| / |总商品集合| - 信息熵覆盖率:考虑商品被推荐次数的分布均匀性,分布越均匀,覆盖率质量越高。
其中,H = -Σ_{i=1}^{n} p_i log p_ip_i是商品 i 被推荐的概率(次数占比)。
4. 多样性
多样性指推荐列表内商品之间的不相似性。高的多样性可以增加用户的选择空间,可能提升购买转化率。
Diversity = 1 - ( Σ_{i∈R, j∈R, i≠j} sim(i, j) ) / (0.5 * |R| * (|R|-1) )
其中,sim(i, j) 是商品 i 和 j 的相似度,R 是推荐列表。需要对所有用户的多样性求平均。
5. 其他指标
- 惊喜度:推荐用户不知道但会很喜欢的内容的能力,能极大提高用户粘性。
- 新颖度:推荐结果的新鲜程度,避免重复推荐。
- 信任度:提供推荐理由(如“你的朋友也喜欢”),能增加用户对推荐结果的接受度。
- 实时性:对于新闻等场景尤为重要。
经典推荐算法
本节我们将介绍一系列经典的推荐算法,这些算法在Netflix推荐大赛中获奖团队均有使用,在工业界非常实用。
1. 基于内容的推荐
该方法基于用户过去喜欢的商品(Item)的属性,推荐与之内容相似的其他商品。
核心思想:
- 为每个待推荐的商品建立一份内容资料(Profile),通常使用TF-IDF等方法将文本内容转化为特征向量。
- 为用户建立一份资料,基于其历史喜欢商品的内容特征向量聚合(如平均)而成。
- 计算待推荐商品与用户资料向量的相似度(如余弦相似度),按相似度高低进行推荐。
优点:不需要其他用户的行为数据,能解决新商品的冷启动问题。
缺点:依赖内容特征挖掘,推荐结果多样性可能受限,难以产生惊喜。
2. 协同过滤
协同过滤是应用最广泛的推荐算法之一,它仅使用用户-商品的交互数据(评分、点击等),而不需要商品内容信息。
User-Based 协同过滤
核心思想:找到与目标用户兴趣相似的用户群体(邻居),然后将邻居喜欢的商品推荐给目标用户。
- 计算用户相似度:利用用户-商品评分矩阵,计算目标用户与其他用户的相似度(如余弦相似度、皮尔逊相关系数)。皮尔逊相关系数通过减去用户平均分来消除用户打分严格度差异的影响。
- 生成推荐:根据相似用户的评分,加权预测目标用户对未评分商品的兴趣。
Item-Based 协同过滤
核心思想:找到与目标商品相似的商品集合,然后将这些相似商品推荐给喜欢目标商品的用户。
- 计算商品相似度:利用用户-商品评分矩阵,计算商品之间的相似度。
- 生成推荐:根据用户历史喜欢的商品,找出其相似商品进行推荐。
工业界更常用Item-Based CF的原因:
- 商品数量通常远小于用户数量,计算和存储相似度矩阵更高效。
- 商品相似度相对稳定,而用户兴趣可能随时间变化。
协同过滤的优缺点:
- 优点:不需要领域知识,仅凭用户行为就能产生推荐;在用户行为丰富时准确度高。
- 缺点:
- 冷启动问题:新用户或新商品缺少足够交互数据。
- 稀疏性问题:用户-商品矩阵非常稀疏时,难以找到可靠关联。
- 同款问题:无法自动关联不同ID的同款商品。
3. 隐语义模型(矩阵分解)
为了克服协同过滤的稀疏性问题,隐语义模型被提出。它通过降维技术来发现用户和商品背后隐藏的“因子”。
核心思想:将用户-商品评分矩阵 R (m×n) 分解为两个低维矩阵的乘积:
R ≈ P * Q^T
其中,P (m×k) 是用户-隐因子矩阵,表示用户对各个隐因子的兴趣程度;Q (n×k) 是商品-隐因子矩阵,表示商品在各个隐因子上的强度。k 是隐因子的数量,远小于 m 和 n。
优化目标:最小化预测评分与实际评分的差异,并加入正则化项防止过拟合。
min_{P,Q} Σ_{(u,i)∈已知评分} (r_{ui} - p_u · q_i^T)^2 + λ(||p_u||^2 + ||q_i||^2)
通常使用随机梯度下降来求解 P 和 Q。
进阶:引入偏置项,考虑全局平均分、用户偏置、商品偏置,使模型更精准:
预测评分 = μ + b_u + b_i + p_u · q_i^T
其中,μ 是全局平均分,b_u 是用户偏置,b_i 是商品偏置。
4. 基于用户行为序列的建模(Word2Vec思想的应用)
将用户在会话中的商品点击序列类比为自然语言中的句子,将每个商品视为一个“词”。应用 Word2Vec 中的 Skip-gram 等模型进行训练,可以得到每个商品的向量表示(Embedding)。
核心思想:在序列中相邻的商品,其向量在嵌入空间中也应该接近。这捕捉了商品之间的“上下文”关联,这种关联基于用户的实际行为模式,而非商品本身的属性。
作用:这种方法能有效挖掘商品间的深层关联,补充协同过滤在覆盖率上的不足,尤其擅长发现行为上的搭配关系。
案例与代码示例
上一节我们介绍了多种推荐算法的原理,本节中我们来看看简单的代码实现,以加深理解。
以下是两个简化示例:


示例1:User-Based 协同过滤(Python手写示例)


这个示例演示了如何计算用户相似度并做出推荐。
# 计算用户相似度(以欧氏距离为例)
def euclidean_distance(rating1, rating2):
distance = 0
for key in rating1:
if key in rating2:
distance += (rating1[key] - rating2[key]) ** 2
return distance ** 0.5





# 寻找最近邻用户
def find_nearest_neighbor(username, users):
distances = []
for user in users:
if user != username:
distance = euclidean_distance(users[user], users[username])
distances.append((distance, user))
distances.sort() # 按距离排序
return distances
# 生成推荐
def recommend(username, users):
nearest = find_nearest_neighbor(username, users)[0][1] # 取最近邻
recommendations = []
neighbor_ratings = users[nearest]
user_ratings = users[username]
for item in neighbor_ratings:
if item not in user_ratings:
recommendations.append((item, neighbor_ratings[item]))
return sorted(recommendations, key=lambda x: x[1], reverse=True) # 按评分排序
# 示例数据
users = {
"小明": {"电影A": 5, "电影B": 3, "电影C": 4},
"小红": {"电影A": 3, "电影B": 4, "电影D": 5},
"小刚": {"电影B": 2, "电影C": 5, "电影D": 3},
}
print(recommend("小明", users))
示例2:矩阵分解(Python手写SGD示例)
这个示例演示了如何使用梯度下降进行简单的矩阵分解。
import numpy as np
def matrix_factorization(R, P, Q, K, steps=5000, alpha=0.0002, beta=0.02):
Q = Q.T
for step in range(steps):
for i in range(len(R)):
for j in range(len(R[i])):
if R[i][j] > 0: # 只对已知评分进行训练
eij = R[i][j] - np.dot(P[i,:], Q[:,j])
for k in range(K):
P[i][k] = P[i][k] + alpha * (2 * eij * Q[k][j] - beta * P[i][k])
Q[k][j] = Q[k][j] + alpha * (2 * eij * P[i][k] - beta * Q[k][j])
# 计算总误差
e = 0
for i in range(len(R)):
for j in range(len(R[i])):
if R[i][j] > 0:
e = e + (R[i][j] - np.dot(P[i,:], Q[:,j])) ** 2
for k in range(K):
e = e + (beta/2) * (P[i][k]**2 + Q[k][j]**2)
if e < 0.001:
break
return P, Q.T
# 示例:一个4x4的评分矩阵,0表示未评分
R = np.array([
[5, 3, 0, 1],
[4, 0, 0, 1],
[1, 1, 0, 5],
[1, 0, 0, 4],
[0, 1, 5, 4],
])
N, M = R.shape
K = 2 # 隐因子数量
P = np.random.rand(N, K)
Q = np.random.rand(M, K)
P_new, Q_new = matrix_factorization(R, P, Q, K)
# 重构的评分矩阵
R_pred = np.dot(P_new, Q_new.T)
print(R_pred)
运行后,R_pred矩阵中原本为0的位置会被填上预测的分数。
总结
本节课中我们一起学习了推荐系统的核心知识。我们从推荐系统存在的必要性讲起,理解了其数学定义和基本架构。重点探讨了如何从准确度、召回率、覆盖率、多样性等多维度评估一个推荐系统。
随后,我们深入讲解了四大类经典且实用的推荐算法:基于内容的推荐、协同过滤(User-Based与Item-Based)、隐语义模型(矩阵分解)以及基于Word2Vec思想的用户行为序列建模。我们分析了它们的原理、优缺点及适用场景,并通过简单的代码示例演示了协同过滤和矩阵分解的基本实现。


推荐系统是一个复杂的工程系统,在实际应用中通常需要融合多种算法,并结合产品设计、实时计算、A/B测试等,才能达到最佳效果。希望本课程为你打开了推荐系统的大门。
人工智能—机器学习中的数学(七月在线出品) - P1:Taylor展式与拟牛顿 🧮
在本节课中,我们将要学习泰勒展式(Taylor Expansion)及其在机器学习中的重要应用,特别是拟牛顿法(Quasi-Newton Method)。我们将从泰勒展式的定义出发,探讨其在函数近似计算、基尼系数解释以及牛顿法中的应用,并最终理解如何利用泰勒展式推导出高效的优化算法。
泰勒展式的定义与公式 📐
上一节我们介绍了课程概述,本节中我们来看看泰勒展式的核心定义。
如果给定一个函数 ( f(x) ),它在某一点 ( x_0 ) 处具有直到 ( n ) 阶的导数,那么 ( f(x) ) 就可以在 ( x_0 ) 处进行 ( n ) 阶泰勒展开,得到如下公式:
[
f(x) = f(x_0) + f'(x_0)(x - x_0) + \frac{f''(x_0)}{2!}(x - x_0)2 + \cdots + \frac{f{(n)}(x_0)}{n!}(x - x_0)^n + R_n(x)
]
这个公式的含义是:函数值 ( f(x_0) ) 加上一阶导数乘以差值 ( (x - x_0) ),再加上二阶导数乘以差值平方并除以 ( 2! ),以此类推。( R_n(x) ) 是余项。
如果我们令 ( x_0 = 0 ),在原点处进行泰勒展开,得到的式子就是麦克劳林公式(Maclaurin Series)。因此,麦克劳林公式是泰勒展式在原点展开的特例。
泰勒展式的应用:函数近似计算 🔢
理解了泰勒展式的定义后,本节中我们来看看如何利用它进行函数近似计算。
利用泰勒展式,我们可以方便地计算一些初等函数的值。例如,对 ( \sin(x) ) 在原点展开,由于其导数的周期性,我们可以得到其展开式。取前若干项的和,就可以作为 ( \sin(x) ) 在某个弧度值下的近似。
另一个例子是计算 ( ex )。在原点处展开 ( ex ) 得到:
[
ex = 1 + x + \frac{x2}{2!} + \frac{x^3}{3!} + \cdots
]
令 ( x = 1 ),则可以得到自然常数 ( e ) 的定义:
[
e = \sum_^{\infty} \frac{1}{i!}
]
在实践中,为了计算如 ( e^{100} ) 这样的大数,直接使用原点处的泰勒展开误差较大。一种改进思路是将指数 ( x ) 分解为 ( x = k \cdot \ln(2) + r ),其中 ( k ) 是整数,( r ) 是小数。这样,( ex = 2k \cdot er )。由于 ( 2k ) 易算,而 ( r ) 是一个接近0的小数,我们可以用泰勒展开精确计算 ( e^r )。一些编程语言正是利用这种机制来计算指数运算。
泰勒展式的应用:解释基尼系数 📊
上一节我们看到了泰勒展式在数值计算中的应用,本节中我们来看看它在解释机器学习概念——基尼系数(Gini Index)中的作用。
基尼系数在决策树和随机森林中用于衡量数据的不纯度,其定义如下(对于分类问题):
[
\text = \sum_ p_k (1 - p_k)
]
其中 ( p_k ) 是样本属于第 ( k ) 类的概率。
这个定义看似奇怪,但可以用泰勒展式来解释。考虑函数 ( f(x) = -\ln(x) ),我们在 ( x = 1 ) 处对其进行一阶泰勒展开(忽略高阶项):
[
f(x) \approx 1 - x
]
信息熵(Entropy)的定义是 ( -\sum p_k \ln(p_k) ),它衡量不确定性。如果我们用 ( 1 - p_k ) 来近似 ( -\ln(p_k) ),那么熵的近似就变成了:
[
\text \approx \sum p_k (1 - p_k)
]
这正是基尼系数的形式(可能相差一个常数因子)。因此,基尼系数可以看作是熵在 ( p=1 ) 处的一阶泰勒近似。这使得它在实践中可以作为分类误差率的一个有效近似,用于决策树中特征选择和分割点的计算。
从泰勒展式到牛顿法 ⚙️
了解了基尼系数的解释后,我们进入本节课的核心部分:如何从泰勒展式推导出强大的优化算法。
首先,我们看一个具体问题:如何计算一个正数 ( a ) 的平方根?定义函数 ( f(x) = x^2 - a ),求根问题转化为求 ( f(x)=0 ) 的解。
我们在当前估计值 ( x_0 ) 处对 ( f(x) ) 进行一阶泰勒展开(忽略高阶项):
[
f(x) \approx f(x_0) + f'(x_0)(x - x_0)
]
令近似值等于0(( f(x)=0 )),并假设 ( f'(x_0) \neq 0 ),可以解出 ( x ):
[
x \approx x_0 - \frac{f(x_0)}{f'(x_0)}
]
对于 ( f(x) = x^2 - a ),有 ( f'(x) = 2x )。代入上式得到迭代公式:
[
x_{n+1} = x_n - \frac{x_n^2 - a}{2x_n} = \frac{1}{2}(x_n + \frac)
]
这就是计算平方根的牛顿迭代法。通过不断迭代,当 ( x_n ) 与 ( x_ ) 的差距足够小时,我们就得到了 ( \sqrt ) 的近似值。
以下是该算法的核心Python代码示例:
def sqrt_newton(a):
x = a / 2.0 # 初始估计值
while True:
x_next = 0.5 * (x + a / x)
if abs(x_next - x) < 1e-10: # 收敛条件
break
x = x_next
return x
这个算法通常只需很少的迭代次数(如5-6次)就能得到高精度的结果。
梯度下降算法 📉
在深入牛顿法之前,我们需要先理解机器学习中更基础的优化算法:梯度下降(Gradient Descent)。
假设我们有一个关于参数 ( \theta ) 的目标函数(损失函数)( J(\theta) ),例如线性回归中的最小二乘损失。我们的目标是找到使 ( J(\theta) ) 最小化的 ( \theta^* )。
梯度下降算法的思想是:从一个初始估计 ( \theta_0 ) 开始,沿着目标函数负梯度的方向(即下降最快的方向)不断更新参数:
[
\theta_{t+1} = \theta_t - \alpha \cdot \nabla J(\theta_t)
]
其中,( \alpha ) 是学习率(步长),( \nabla J(\theta_t) ) 是 ( \theta_t ) 处的梯度。通过反复迭代,参数会(期望)收敛到一个局部极小值。取正梯度方向则是梯度上升算法,用于寻找最大值。
牛顿法 🚀
上一节我们介绍了梯度下降,它只用了一阶导数信息。本节中我们利用泰勒展式引入使用二阶导数的牛顿法,以期获得更快的收敛速度。
对目标函数 ( f(x) ) 在点 ( x_k ) 处进行二阶泰勒展开:
[
f(x) \approx f(x_k) + f'(x_k)(x - x_k) + \frac{1}{2} f''(x_k)(x - x_k)^2
]
我们将右边记作 ( s(x) )。为了找到 ( f(x) ) 的极值点(驻点),我们令 ( s'(x) = 0 ):
[
s'(x) = f'(x_k) + f''(x_k)(x - x_k) = 0
]
假设 ( f''(x_k) \neq 0 ),可以解出 ( x ):
[
x = x_k - \frac{f'(x_k)}{f''(x_k)}
]
这就得到了牛顿法的迭代公式。与梯度下降用一次函数(切线)近似不同,牛顿法用一个二次函数(抛物线)去近似原函数,并直接跳到该二次函数的极值点。因此,在目标函数性质较好(如接近二次)且初始点合适时,牛顿法具有二阶收敛性,速度更快。
将一维情况推广到多维(参数为向量 ( \theta )),一阶导数 ( f' ) 变为梯度向量 ( g ),二阶导数 ( f'' ) 变为海森矩阵(Hessian Matrix)( H )。牛顿法的迭代公式变为:
[
\theta_{t+1} = \theta_t - H^{-1}(\theta_t) \cdot g(\theta_t)
]
拟牛顿法 🤖
虽然牛顿法收敛快,但它存在几个问题:
- 需要计算海森矩阵 ( H ) 及其逆矩阵 ( H^{-1} ),计算成本高。
- 要求海森矩阵正定,否则搜索方向可能错误(甚至指向上升方向)。
- 对初始点要求较高。
为了解决这些问题,拟牛顿法(Quasi-Newton Method)被提出。其核心思想是:不直接计算海森矩阵,而是用一个正定矩阵 ( B_t ) 来近似 ( H ),或用 ( C_t ) 近似 ( H^{-1} ),并通过迭代不断更新这个近似矩阵。
拟牛顿法需要满足“拟牛顿条件”(或“割线条件”)。在点 ( \theta_{t+1} ) 处,我们期望近似矩阵满足:
[
C_{t+1} (g_{t+1} - g_t) \approx \theta_{t+1} - \theta_t
]
令 ( \delta = \theta_{t+1} - \theta_t ), ( \gamma = g_{t+1} - g_t ),则条件简化为:
[
C_{t+1} \gamma = \delta
]
不同的拟牛顿算法在于如何更新 ( C_t ) 以满足这个条件。以下是两个著名的方案:
DFP算法 (Davidon–Fletcher–Powell):
通过低秩矩阵更新来迭代 ( C_t ),其更新公式具有特定形式,并通过待定系数法确定参数,最终得到迭代式。
BFGS算法 (Broyden–Fletcher–Goldfarb–Shanno):
这是目前最流行、效果最好的拟牛顿算法之一。它直接对海森矩阵的逆 ( H^{-1} ) 进行更新,公式略有不同但思想类似。BFGS通常比DFP性能更稳定。
在BFGS算法中,我们用 ( B_t ) 近似海森矩阵 ( H_t ),其更新公式为:
[
B_{t+1} = B_t + \frac{\gamma_t \gamma_tT}{\gamma_tT \delta_t} - \frac{B_t \delta_t \delta_tT B_t}{\delta_tT B_t \delta_t}
]
对应的,用于迭代的参数更新公式为 ( \theta_{t+1} = \theta_t - B_t^{-1} g_t ),但实践中我们通过数学变换避免直接求逆,而是维护 ( C_t \approx H_t^{-1} ) 并进行更新。
拟牛顿法结合了梯度下降的简单和牛顿法的快速收敛性。在实际的机器学习模型(如逻辑回归)训练中,采用BFGS等拟牛顿法往往比标准梯度下降收敛所需迭代次数少一个数量级。
对于特征维度 ( n ) 非常大的问题,存储和更新 ( n \times n ) 的矩阵 ( C_t ) 开销巨大。此时可以使用L-BFGS算法(Limited-memory BFGS),它只保存最近 ( m ) 次(( m ) 通常很小,如10)的 ( \delta ) 和 ( \gamma ) 向量来近似计算更新方向,大大节省了内存。
总结与展望 🎯
本节课中我们一起学习了泰勒展式及其在机器学习中的一系列深刻应用。
我们首先学习了泰勒展式的定义,它是用多项式逼近复杂函数的强大工具。接着,我们看到了它在数值计算(如指数函数、平方根计算)中的直接应用。然后,我们探讨了如何用一阶泰勒展式来解释决策树中使用的基尼系数,将其与信息熵联系起来。
课程的核心部分是从泰勒展式推导出优化算法。我们从一阶展开得到了梯度下降法的基础思想,从二阶展开推导出了收敛更快的牛顿法。为了克服牛顿法计算成本高和对海森矩阵正定要求的缺点,我们深入介绍了拟牛顿法(特别是DFP和BFGS算法),它们通过迭代近似海森矩阵或其逆矩阵,在保证较快收敛的同时,更适用于实际的机器学习优化问题。
泰勒展式是连接数学分析与机器学习算法的桥梁。理解它不仅能帮助我们掌握这些算法的来源,也为改进和发明新算法提供了思路。在后续的机器学习课程中,我们还会遇到更多基于泰勒展式或其思想的技术,例如自适应学习率方法、以及更深入的经济学与社会学中基尼系数的计算等。


注:本教程根据提供的视频内容整理,力求准确传达原意。更深入的实践与理论细节可在七月在线平台(julyedu.com)的社区中进一步探讨。



人工智能—机器学习中的数学(七月在线出品) - P10:四个基本的子空间 🧮

在本节课中,我们将要学习线性代数中一个核心且美妙的概念:矩阵的四个基本子空间。理解这四个子空间及其相互关系,是掌握线性方程组、矩阵分解等高级主题的基石。我们将通过直观的几何图像和简单的例子,让初学者也能轻松理解这些抽象概念。


列空间(Column Space)🚀
上一节我们介绍了线性组合的概念,本节中我们来看看由矩阵的列向量所张成的空间。
列空间,顾名思义,是由矩阵所有列向量的线性组合构成的空间。对于一个矩阵 A,其列空间是所有形如 Y = AX 的向量 Y 的集合,其中 X 可以取任意实数值。这本质上就是矩阵 A 的列向量张成(span)的空间。


如果矩阵 A 的列向量线性无关,那么这些列向量本身就是其列空间的一组“基”。基是一组“恰到好处”的向量:它们线性无关,并且能够张满整个子空间。多一个向量就会线性相关,少一个向量则无法张满该空间。

以下是关于基和列空间的关键点:
- 基是子空间中最大的线性无关向量组。
- 一个子空间可以有不同的基,但所有基包含的向量数量(即空间的维数)是相同的。
- 列空间是 R^m 的一个子空间(假设 A 是 m×n 矩阵)。
为了更直观地理解,考虑一个例子:假设矩阵 A 有两列,分别是向量 (0,3,3) 和 (1,4,2)。这两个向量的所有线性组合构成了一个平面。这个平面是三维空间 R^3 的一个子集,因此被称为 R^3 的一个子空间。子空间必须包含零点(当线性组合系数全为0时)。
零空间(Null Space)🎯

接下来,我们探讨与列空间紧密相关的另一个子空间:零空间。

零空间是满足方程 AX = 0 的所有解向量 X 的集合。注意,零空间是 R^n 的一个子空间(A 是 m×n 矩阵),而不是 R^m 的子空间。

让我们通过一个具体例子来理解。假设有一个2×4的矩阵 A,其零空间的解由两个线性无关的向量构成。这两个向量就是零空间的一组基。零空间中的所有向量(即 AX=0 的所有解),都可以表示为这两个基向量的线性组合,这个组合本身也构成了 R^4 的一个子空间。

简单来说,零空间就是齐次线性方程组 AX=0 的所有解构成的空间。



行空间(Row Space)与左零空间(Left Null Space)🔀





除了列空间和零空间,还有两个重要的子空间:行空间和左零空间。它们让线性代数的理论结构变得更加完整和对称。





行空间是所有行向量的线性组合构成的空间。我们可以通过矩阵转置来理解:行空间就是 A^T 的列空间。

左零空间则定义为满足 A^T Y = 0 的所有向量 Y 的集合。将其转置,得到 Y^T A = 0,这意味着向量 Y(在左边)与 A 的每一个列向量都垂直(内积为0),因此得名“左”零空间。左零空间是 R^m 的一个子空间。

四大子空间的关系图 🗺️


理解了每个子空间的定义后,最关键的是掌握它们之间的美妙关系。这是线性代数中最核心的一幅图景。




以下是四个基本子空间的关系总结:
- 列空间 C(A): A 的列向量生成的空间,是 R^m 的子空间,维数等于矩阵 A 的秩 r。
- 零空间 N(A): AX=0 的解空间,是 R^n 的子空间,维数等于 n - r。
- 行空间 C(A^T): A 的行向量生成的空间,是 R^n 的子空间,维数也等于秩 r。(这解释了“行秩=列秩”)
- 左零空间 N(A^T): A^T Y=0 的解空间,是 R^m 的子空间,维数等于 m - r。


它们之间存在两对正交补关系:
- 在 R^n 中: 行空间 C(A^T) 与零空间 N(A) 互为正交补。这意味着行空间中的任何一个向量都与零空间中的任何一个向量垂直,并且它们的直和构成了整个 R^n 空间。
- 在 R^m 中: 列空间 C(A) 与左零空间 N(A^T) 互为正交补。这意味着列空间中的任何一个向量都与左零空间中的任何一个向量垂直,并且它们的直和构成了整个 R^m 空间。




“正交补”不仅要求垂直,还要求两个子空间唯一的交集是零向量。例如,三维空间中,一个过原点的平面(列空间)和一条过原点且垂直于该平面的直线(左零空间)就互为正交补,它们共同填满了整个三维空间。




子空间视角下的方程解 🧩


最后,我们利用子空间的观点,重新审视线性方程组 AX = B 的解。
AX = B 有解,当且仅当向量 B 位于矩阵 A 的列空间之内。如果 B 不在列空间中,则方程无解。





当方程有解时,解的结构可以清晰地用子空间表示:
- 特解: 找到任意一个满足 AX_p = B 的特解 X_p。
- 通解: 方程的所有解 X 可以表示为:X = X_p + X_n,其中 X_n 是零空间 N(A) 中的任意向量。




这是因为 A(X_p + X_n) = AX_p + AX_n = B + 0 = B。零空间的存在决定了方程解的个数:如果零空间维数为0(即只有零向量),则方程有唯一解;如果零空间维数大于0,则方程有无穷多解。






本节课中我们一起学习了矩阵的四个基本子空间:列空间、零空间、行空间和左零空间。我们不仅了解了它们的定义,更重要的是掌握了它们之间互为正交补的优美关系,以及如何用子空间的观点来理解线性方程组解的存在性和结构。这幅“四大子空间”的图景是贯穿线性代数许多核心概念的线索,请务必仔细体会。
人工智能—机器学习中的数学(七月在线出品) - P11:随机梯度下降算法综述 📚
概述
在本节课中,我们将学习随机梯度下降算法的核心原理、其面临的主要挑战,以及一系列旨在解决这些挑战的改进算法。我们将从基础的梯度下降法出发,逐步深入到各种变体,并理解它们的设计思想和适用场景。
第一节:梯度下降法简介 📉
梯度下降法是机器学习中用于优化模型参数的核心方法。其基本思想是:通过计算目标函数在当前参数点处的梯度(即函数值上升最快的方向),然后沿着梯度的反方向更新参数,以期望找到函数的极小值点。
具体更新公式如下:
[
\theta_{t+1} = \theta_t - \eta \cdot \nabla J(\theta_t)
]
其中,(\theta) 是模型参数,(\eta) 是学习率,(\nabla J(\theta_t)) 是目标函数 (J) 在点 (\theta_t) 处的梯度。
该方法包含两个核心步骤:
- 计算梯度:对目标函数求导,得到下降方向。
- 选择学习率:决定沿着该方向前进的步长。
然而,这种方法在实践中面临两大主要困难:
- 梯度计算开销大:当训练样本数量巨大时,计算所有样本的梯度之和非常耗时。
- 学习率难以选择:学习率过大可能导致震荡不收敛,过小则收敛缓慢,且通常需要针对具体问题手动调整。
第二节:从梯度下降到随机梯度下降 🔄
上一节我们介绍了梯度下降法及其挑战,本节中我们来看看如何通过引入“随机性”来应对第一个挑战——梯度计算开销过大。
随机梯度下降(SGD)
为了解决全量梯度计算慢的问题,随机梯度下降法在每次参数更新时,只随机使用一个训练样本来计算梯度。其更新公式为:
[
\theta_{t+1} = \theta_t - \eta \cdot \nabla J(\theta_t; x_i, y_i)
]
其中,((x_i, y_i)) 是随机选取的单个样本。
优点:
- 计算高效:避免了冗余计算,大大加快了每次迭代的速度。
- 可能跳出局部极小值:由于梯度的随机性,算法有机会跳出较差的局部最优点。
缺点:
- 更新方向震荡大:单个样本的梯度不能代表整体数据的梯度方向,导致参数更新路径不稳定,收敛过程波动剧烈。
小批量随机梯度下降(Mini-batch SGD)
为了在计算效率和稳定性之间取得平衡,最常用的方法是小批量随机梯度下降。它每次随机选取一小批(例如32、64、128个)样本计算梯度。
[
\theta_{t+1} = \theta_t - \eta \cdot \frac{1} \sum_^ \nabla J(\theta_t; x_i, y_i)
]
其中,(m) 是小批量的大小。
优点:
- 计算仍高效:可以利用现代计算库(如NumPy)的向量化操作,并行计算小批量梯度。
- 更新更稳定:小批量梯度的方差比单样本小,收敛路径更平滑。
- 成为实际标准:在深度学习领域,通常所说的“SGD”默认指的就是Mini-batch SGD。
实现细节:
以下是关于小批量处理的一些常见做法:
- 将整个训练集划分为若干个小批量。
- 遍历所有小批量完成一次训练称为一个“epoch”。
- 在每个epoch开始前,对训练数据进行洗牌(Shuffle),然后重新划分小批量,这有助于避免数据顺序对训练结果产生潜在影响。
第三节:随机梯度下降面临的挑战 ⚠️
虽然随机梯度下降法解决了计算效率问题,但它(以及所有梯度下降类方法)仍然面临一系列核心挑战,这些挑战催生了后续的各种改进算法。
- 病态曲率与震荡:在高维非凸优化中,损失函数的曲面可能在某些方向非常陡峭,而在另一些方向相对平坦(如峡谷地形)。标准的SGD会在陡峭方向来回震荡,导致在平坦的主下降方向上进展缓慢。
- 学习率调度困难:虽然可以通过预先设定规则(如随时间衰减)来降低学习率以促进收敛,但如何为不同数据集自动设计合适的衰减策略仍是一个难题。
- 参数更新频率不均:对于稀疏数据,某些特征很少出现。如果对所有参数使用相同的、且逐渐衰减的学习率,这些稀疏特征对应的参数可能得不到充分更新。
- 陷入鞍点:在高维空间中,鞍点(某些方向是极小值,某些方向是极大值)比局部极小值更常见。在鞍点附近梯度很小,SGD容易停滞不前。
第四节:应对挑战的改进算法 🛠️
上一节我们总结了SGD的主要挑战,本节中我们将逐一介绍为解决这些挑战而设计的经典改进算法。每种算法都针对特定问题提供了直观的解决方案。
1. 动量法(Momentum)—— 缓解震荡
动量法受物理学启发,旨在解决在病态曲率区域(如峡谷地形)的震荡问题。它引入了“速度”变量,让参数更新不仅考虑当前梯度,还积累之前更新的方向,从而在主要下降方向上获得加速,并抑制垂直方向的摆动。
核心公式:
[
\begin
v_t &= \gamma v_ + \eta \nabla J(\theta_t) \
\theta_{t+1} &= \theta_t - v_t
\end
]
其中,(v_t) 是当前速度,(\gamma) 是动量系数(通常取0.9),用于衰减历史速度。
物理类比:想象推一个重球下山。它有惯性(动量),不会因为路面的小坑洼而剧烈转向,能更平稳地沿着山谷向下滚动。
2. Nesterov 加速梯度(NAG)—— 更聪明的动量
动量法的一个问题是,当球滚到谷底时,积累的动量可能让它冲上对面的山坡。NAG对此做了“前瞻性”改进:它先根据累积动量向前看一步,在那个“未来”的位置计算梯度,然后用这个梯度来修正当前的更新方向。
核心公式:
[
\begin
v_t &= \gamma v_ + \eta \nabla J(\theta_t - \gamma v_) \
\theta_{t+1} &= \theta_t - v_t
\end
]
直观理解:这好比在滚球时,先看看如果按当前动量滚下去,前面路况如何(坡度怎样),然后提前调整发力,起到“刹车”或“转向”的作用,使收敛更稳定。
3. Adagrad —— 自适应学习率(针对稀疏特征)
Adagrad 旨在为每个参数自动适应不同的学习率,特别适合处理稀疏数据。它为每个参数维护一个历史梯度平方的累积和。更新频繁的参数,累积和大,学习率会自动减小;更新稀少的参数,累积和小,学习率相对较大。
核心公式:
[
\begin
G_{t, ii} &= G_{t-1, ii} + (\nabla J(\theta_t)i)^2 \
\theta{t+1, i} &= \theta_{t, i} - \frac{\eta}{\sqrt{G_{t, ii} + \epsilon}} \cdot \nabla J(\theta_t)i
\end
]
其中,(G_t) 是一个对角矩阵,其元素 (G{t, ii}) 是参数 (\theta_i) 历史梯度平方的累积。
优点:无需手动调整学习率衰减,且对稀疏特征友好。
缺点:随着训练进行,分母中的累积和会单调递增,导致学习率过早、过度衰减,可能使训练提前终止。
4. RMSprop —— 解决 Adagrad 学习率急剧衰减
RMSprop 改进了 Adagrad,将历史梯度平方的累积和改为指数移动平均,从而解决了学习率单调下降过快的问题。它更关注近期梯度的变化。
核心公式:
[
\begin
E[g2]_t &= \beta E[g2] + (1-\beta)(\nabla J(\theta_t))^2 \
\theta{t+1} &= \theta_t - \frac{\eta}{\sqrt{E[g2]_t + \epsilon}} \cdot \nabla J(\theta_t)
\end
]
其中,(E[g2]_t) 是梯度平方的指数移动平均,(\beta) 是衰减率(通常为0.9)。
5. Adam —— 结合动量与自适应学习率
Adam(Adaptive Moment Estimation)可以说是当前最流行、默认推荐的优化器。它同时结合了动量法(一阶矩估计)和RMSprop(二阶矩估计)的优点,即同时考虑梯度方向的历史信息和梯度大小的历史信息。
核心公式:
[
\begin
m_t &= \beta_1 m_ + (1-\beta_1) \nabla J(\theta_t) \quad &\text{(一阶矩,有偏估计)} \
v_t &= \beta_2 v_ + (1-\beta_2) (\nabla J(\theta_t))2 \quad &\text{(二阶矩,有偏估计)} \
\hat_t &= \frac{1 - \beta_1t} \quad &\text{(修正一阶矩)} \
\hatt &= \frac{1 - \beta_2^t} \quad &\text{(修正二阶矩)} \
\theta{t+1} &= \theta_t - \frac{\eta}{\sqrt{\hat_t} + \epsilon} \hat_t
\end
]
其中,(\beta_1, \beta_2) 通常取0.9和0.999。对 (m_t) 和 (v_t) 进行偏差修正是为了在训练初期使其估计更准确。
优点:通常收敛速度快,对超参数选择相对鲁棒,是许多场景下的“默认”选择。
第五节:其他优化技巧 💡
除了修改参数更新规则的核心算法外,还有一些重要的技巧可以提升SGD的训练效果。
以下是几种常用的辅助优化技巧:
- 数据洗牌(Shuffling):在每个训练周期(epoch)开始前,随机打乱训练数据顺序,有助于模型学习更通用的模式,避免因数据顺序带来的偏差。
- 批规范化(Batch Normalization):对每一层神经网络的输入进行标准化处理(减均值、除标准差),可以稳定网络的训练过程,允许使用更高的学习率,并具有一定的正则化效果。
- 早停(Early Stopping):在训练过程中持续监控模型在验证集上的性能。当验证集误差不再下降甚至开始上升时,即使训练误差还在下降,也停止训练,以防止过拟合。
- 梯度噪声(Gradient Noise):在梯度中加入少量随机噪声。这有助于模型跳出较差的局部极小值或鞍点,增加找到更好解的可能性。噪声的幅度通常随训练进行而衰减。
第六节:如何选择优化算法? 🤔
面对众多优化算法,初学者可能会感到困惑。以下是一些指导原则:
- 理解问题特性:如果你的数据稀疏,考虑自适应学习率算法(Adagrad, Adadelta, RMSprop, Adam)。如果你的优化地形非常复杂(崎岖),考虑使用带动量的方法(Momentum, NAG, Adam)。
- Adam 作为强力的默认选择:在大多数情况下,Adam 优化器因其结合了动量和自适应学习率的优点,往往能提供快速且稳定的收敛,是一个非常好的起点。
- 经典的 SGD + Momentum 仍有价值:虽然 Adam 很流行,但一些研究表明,精心调参的 SGD with Momentum 有时能达到更好的最终精度,尽管其收敛可能需要更长时间。
- 实践是检验真理的唯一标准:对于你的特定任务和数据集,最好的方法是通过实验(例如,在验证集上比较收敛速度和最终性能)来选择优化器。可以先用 Adam 快速得到一个基准,再尝试其他算法看是否有提升。
总结
本节课中,我们一起学习了随机梯度下降算法的演进之路。
我们从最基础的梯度下降法出发,认识了其计算瓶颈。
随后引入了随机性,诞生了随机梯度下降(SGD)及其更实用的变体小批量SGD,解决了效率问题。
接着,我们探讨了SGD面临的四大核心挑战:病态曲率震荡、学习率调度难、稀疏参数更新不均以及鞍点问题。
针对这些挑战,我们深入讲解了一系列改进算法:用动量法(Momentum) 抑制震荡,用Nesterov加速梯度实现更智能的动量,用Adagrad为稀疏特征自适应学习率,用RMSprop改进其衰减过程,最终学习了集大成的Adam算法。
此外,我们还了解了一些重要的辅助技巧,如数据洗牌、批规范化和早停。
最后,我们讨论了算法选择的策略,建议将Adam作为默认的强力选择,并根据具体问题特性进行调整和实验。



通过本教程,希望你不仅掌握了这些算法的公式,更理解了它们背后要解决的实际问题,从而能在实践中做出更明智的选择。

人工智能—机器学习中的数学(七月在线出品) - P12:微积分和梯度 🧮
概述
在本节课中,我们将学习微积分和梯度在机器学习中的核心作用。课程将从极限、导数、微分等基础概念出发,逐步深入到梯度、凸函数以及重要的不等式,并展示它们如何应用于机器学习问题的分析与求解。
极限与重要极限
上一节我们介绍了课程的整体框架,本节中我们来看看微积分的基础——极限。
我们从一个简单的级数问题开始:零的阶乘分之一加上一的阶乘分之一,一直加到N的阶乘分之一,当N趋向无穷大时,这个和S是收敛的。S的值是多少呢?我们将通过微积分的方法来求解。
首先,我们给出一个直观的定理:两边夹定理。如果在点X0的某个邻域内,函数F(X)有定义,并且满足 G(X) ≤ F(X) ≤ H(X)。同时,已知G(X)和H(X)在X趋向于X0时的极限都是A。那么,F(X)的极限也是A。
我们直接应用两边夹定理来解决一个问题。例如,给定一个单位圆,圆心为O,半径OA长度为1。取任意角度X,线段CB的长度是sin X,弧AB的长度是X,线段AD的长度是tan X。显然,CB < AB,而直线段AB小于弧AB,因此有 sin X < X。同样,通过比较三角形OAD的面积和扇形OAB的面积,可以推导出 X < tan X。这在X的邻域内是正确的。
我们在不等式两边同时除以 sin X,得到:
1 < X / sin X < 1 / cos X
稍作整理,得到:
cos X < sin X / X < 1
当X趋近于0时,cos X的极限是1,右边的极限也是1。因此,根据两边夹定理,sin X / X 在X趋近于0时的极限就是1。这是一个利用两边夹定理的简单结论。
这个公式揭示了三角函数与多项式之间的极限关系,我们可以利用它解决许多问题。
自然底数 e 的引入
上一节我们利用极限解决了一个三角问题,本节中我们来看看另一个重要的极限,它引出了自然底数e。
考虑函数 Y = logₐ X。当底数a取不同值(如2, 3, 1.5)时,可以画出对应的对数曲线。所有曲线都经过点(1, 0)。在这一点,不同函数的斜率不同。我们能否找到一个底数a,使得函数在X=1处的斜率恰好为1呢?
我们来看如何解决这个问题。假设要找的底数是a,记函数为 F(X) = logₐ X。在X=1处,割线的斜率公式为 (F(1+ΔX) - F(1)) / ΔX。当ΔX趋近于0时,这就是导数。代入函数得到:
[logₐ (1+ΔX) - logₐ 1] / ΔX = logₐ (1+ΔX) / ΔX
根据对数运算法则,两个对数的差等于其商的对数。我们想让这个导数等于1,即:
logₐ (1+ΔX) / ΔX = 1
这意味着 (1+ΔX)^(1/ΔX) = a。
因此,我们想探讨的是当ΔX趋近于0时,(1+ΔX)^(1/ΔX) 的极限是什么。事实上,这个极限就是自然底数e。
我们构造一个数列:X_N = (1 + 1/N)^N。利用牛顿二项式定理将其展开:
X_N = Σ_{k=0}^{N} C_N^k * (1/N)^k
其中 C_N^k = N! / (k! (N-k)!)。展开并化简后,可以证明这个数列是单调递增且有上界(例如小于3)的。根据单调有界数列必有极限的定理,该数列的极限存在,我们将其记为e。
我们证明了当N是自然数时极限存在。对于任意实数x,我们总可以找到整数N,使得 N ≤ x ≤ N+1。利用类似的两边夹定理,可以证明当x趋向无穷大时,(1+1/x)^x 的极限也是e。
因此,e可以看作是无穷级数的和:
e = 1/0! + 1/1! + 1/2! + ... + 1/n! + ... (当 n → ∞)
导数与微分
上一节我们引入了自然底数e,本节中我们正式学习导数和微分。
导数可以直观地理解为曲线的斜率,它表征了函数值变化的快慢。导数本身也可以求导,得到二阶导数。二阶导数反映了斜率变化的快慢,即函数的凹凸性。在物理中,对于运动轨迹,加速度方向总指向轨迹凹的一侧。二阶导连续的函数通常被称为“光顺”的,这在凸优化中是一个重要概念。
根据前面关于e的极限,我们可以得到函数 F(X) = ln X(即以e为底的对数)在X=1处的导数恰好为1。进而推导出 (ln X)' = 1/X。结合换底公式、反函数求导等工具,可以得到其他基本初等函数的导数公式。
以下是基本导数公式:
(C)' = 0(C为常数)(x^a)' = a * x^(a-1)(a^x)' = a^x * ln a(e^x)' = e^x(log_a x)' = 1/(x ln a)(ln x)' = 1/x(sin x)' = cos x(cos x)' = -sin x(tan x)' = sec^2 x
导数的运算法则包括:
- 加法法则:
(u + v)' = u' + v' - 乘法法则:
(u * v)' = u' * v + u * v'
我们重点关注乘法法则。对 (u * v)' = u' * v + u * v' 两边同时积分,得到:
∫ u * v' dx = u * v - ∫ u' * v dx
这就是分部积分法。例如,求 ∫ ln x dx,可以令 u = ln x, v' = 1, 则 u' = 1/x, v = x。代入公式:
∫ ln x dx = x * ln x - ∫ x * (1/x) dx = x ln x - x + C
微分的应用:幂指函数与阶乘估计
上一节我们学习了导数和微分的基本法则,本节中我们看看微分的一些具体应用。
1. 幂指函数求极值
考虑函数 F(X) = X^X (X > 0)。这是一个底数和指数都包含变量的幂指函数。我们想求其最小值。
解决思路是两边取对数:令 t = X^X,则 ln t = X ln X。两边对X求导:
(1/t) * t' = ln X + 1
因此,t' = t * (ln X + 1) = X^X * (ln X + 1)。
令导数 t' = 0 求驻点。由于 X^X > 0,只需 ln X + 1 = 0,解得 X = e^(-1)。
分析函数性质可知,该函数先减后增,因此在 X = 1/e 处取得全局最小值。代入原函数得最小值为 e^(-1/e)。
2. 阶乘的对数规模估计
N! 在N很大时增长极快。对其取对数 ln(N!),它的增长规模如何呢?
ln(N!) = ln1 + ln2 + ... + lnN。
这个和可以近似看作函数 y = ln x 从1到N的积分。
ln(N!) ≈ ∫_1^N ln x dx
利用分部积分法求解该积分:
∫ ln x dx = x ln x - x
因此,ln(N!) ≈ N ln N - N + 1。当N很大时,主要增长项为 N ln N。所以我们说 ln(N!) 的增长规模是 O(N log N)。
多元函数与梯度
上一节我们讨论了一元函数的应用,本节中我们将概念扩展到多元函数。
对于二元函数 Z = F(X, Y), 在点 P(X0, Y0) 处可微。我们不仅可以求偏导数,还可以求沿某一方向L的方向导数。假设方向L与X轴正方向的夹角为 φ,则方向导数为:
∂F/∂L = (∂F/∂X) * cosφ + (∂F/∂Y) * sinφ
这可以写成向量点乘的形式:[∂F/∂X, ∂F/∂Y] · [cosφ, sinφ]^T。
左边向量只与函数F在点P的性质有关,右边向量只与方向L有关。当方向向量 [cosφ, sinφ] 与梯度向量 [∂F/∂X, ∂F/∂Y] 方向一致时,点乘值(即方向导数)最大。这意味着函数在该点沿梯度方向变化最快。
梯度记作 ∇F 或 grad F:
∇F = (∂F/∂X, ∂F/∂Y)
在机器学习中,我们常沿着梯度的反方向(负梯度方向)更新参数,以寻找损失函数的最小值,这就是梯度下降法的基本原理。
凸函数与 Jensen 不等式
上一节我们介绍了梯度,本节中我们学习机器学习优化中另一个核心概念:凸函数。
凸函数定义:函数F的定义域是凸集,对于定义域内任意两点X, Y和任意常数 θ ∈ [0, 1], 满足:
F(θX + (1-θ)Y) ≤ θF(X) + (1-θ)F(Y)
几何意义是:函数图像上任意两点的连线(割线)总在函数图像的上方。
在机器学习领域,像 Y = X^2 这样的函数被称为凸函数(开口向上),而 Y = -X^2 被称为凹函数。这与某些数学教材的称呼可能相反。
一阶条件:如果F是一阶可微的,则F是凸函数等价于:
F(Y) ≥ F(X) + ∇F(X)^T (Y - X), 对所有X, Y在定义域内成立。
几何意义是:函数图像在任何一点处的切线都在图像的下方。这可以看作函数的一个全局下界估计。
二阶条件:如果F是二阶可微的,则F是凸函数等价于其海森矩阵(二阶导矩阵)是半正定的(对于一元函数,即二阶导 ≥ 0)。例如,Y = X^2 的二阶导为2 > 0,所以它是凸函数。
常见的凸函数有:
- 指数函数:
e^(aX)(a为任意实数) - 幂函数:
X^a(当 a ≥ 1 或 a ≤ 0) - 负对数函数:
-log X - 仿射函数:
AX + b
Jensen不等式:凸函数定义的一个直接推广。对于凸函数F,有:
F(Σ θ_i X_i) ≤ Σ θ_i F(X_i), 其中 θ_i ≥ 0 且 Σ θ_i = 1。
如果我们将 θ_i 视为概率,X_i 视为随机变量取值,那么不等式可以写成期望的形式:
F(E[X]) ≤ E[F(X)]
其中E表示数学期望。这对于连续型随机变量同样成立。
Jensen不等式非常强大,许多经典不等式都可以视为它的特例。
以下是两个应用示例:
- 算术-几何平均不等式:对于正数a, b, 有
(a+b)/2 ≥ √(ab)。
证明:取凸函数F(X) = -ln X, 并令θ = 1/2,X1 = a,X2 = b, 代入Jensen不等式即可得证。 - KL散度的非负性:KL散度(相对熵)用于衡量两个概率分布P和Q的差异,定义为:
D_KL(P||Q) = Σ P(x) ln (P(x)/Q(x))
可以证明D_KL(P||Q) ≥ 0。
证明:将-ln x视为凸函数,将Q(x)/P(x)视为随机变量(在分布P下),应用Jensen不等式:
D_KL(P||Q) = E_P[-ln (Q(x)/P(x))] ≥ -ln (E_P[Q(x)/P(x)]) = -ln(1) = 0
总结
本节课我们一起学习了微积分和梯度在机器学习中的核心知识。
我们首先回顾了极限和两边夹定理,并由此引出了重要的自然底数e。接着,我们学习了导数与微分的概念、公式及其运算法则,并通过幂指函数求极值和阶乘规模估计展示了微分的应用。然后,我们将概念扩展到多元函数,引入了方向导数和梯度的概念,并解释了梯度方向是函数值变化最快的方向。最后,我们深入探讨了凸函数的定义、性质以及至关重要的Jensen不等式,并展示了其在证明算术-几何平均不等式和KL散度非负性中的应用。


以机器学习应用为目的来学习这些数学知识,会发现它们并非难以掌握。许多复杂的结论都可以通过基本的定义和定理一步步推导出来。理解这些概念背后的几何直观和物理意义,对于构建机器学习模型和设计优化算法至关重要。关于凸优化更深入的内容,我们将在后续课程中详细探讨。
人工智能—机器学习中的数学(七月在线出品) - P13:协方差 📊
在本节课中,我们将要学习概率论与统计学中的一个核心概念——协方差。协方差是衡量两个随机变量之间线性关系强度和方向的重要工具,也是理解后续更复杂概念(如相关系数、协方差矩阵)的基础。
协方差的定义与计算
上一节我们介绍了随机变量的期望,本节中我们来看看如何衡量两个变量之间的联动关系。
两个随机变量X和Y的协方差定义为:它们各自与其期望的偏差的乘积的期望。用公式表示如下:
Cov(X, Y) = E[(X - E[X])(Y - E[Y])]
根据定义,协方差具有对称性,即 Cov(X, Y) = Cov(Y, X)。
此外,协方差还有一个更常用的计算公式,它等于两个变量乘积的期望减去它们各自期望的乘积:
Cov(X, Y) = E[XY] - E[X]E[Y]
独立、不相关与协方差的关系
以下是关于独立与不相关性的重要结论:
- 独立性与协方差:如果随机变量X和Y相互独立,那么
E[XY] = E[X]E[Y]。根据协方差的计算公式,可以立即得出Cov(X, Y) = 0。 - 不相关的定义:如果两个随机变量的协方差为零,即
Cov(X, Y) = 0,我们称X和Y“不相关”。 - 两者的关系:独立性是一个更强的条件,它必然导致不相关(协方差为零)。但反之不成立,协方差为零(不相关)不能推出两个变量相互独立。不相关仅仅意味着变量之间没有线性关系,但它们可能存在其他非线性关系(如二次方、三角函数关系等)。
协方差的意义与上界
协方差度量了两个随机变量变化趋势的一致性。
- Cov(X, Y) > 0:表示X和Y的变化趋势相同,即一个变量增大时,另一个也倾向于增大。
- Cov(X, Y) < 0:表示X和Y的变化趋势相反,即一个变量增大时,另一个倾向于减小。
- Cov(X, Y) = 0:表示X和Y之间没有线性趋势关系。
那么,协方差的大小是否有上限呢?答案是肯定的。对于方差分别为 σ₁² 和 σ₂² 的随机变量X和Y,其协方差满足柯西-施瓦茨不等式:
|Cov(X, Y)| ≤ σ₁σ₂
当且仅当X和Y之间存在严格的线性关系(即 X = aY + b)时,等号成立。这个上界定理引出了一个更标准化的度量——相关系数。
相关系数:标准化的协方差
为了消除量纲影响,更纯粹地衡量线性相关程度,我们定义相关系数 ρ(也称为皮尔逊相关系数):
ρ = Cov(X, Y) / (σ₁σ₂)
根据协方差的上界定理,相关系数满足 -1 ≤ ρ ≤ 1。
- ρ = 1:完全正相关,X和Y存在严格的递增线性关系。
- ρ = -1:完全负相关,X和Y存在严格的递减线性关系。
- ρ = 0:不相关,无线性关系。
因此,相关系数可以看作是“标准化”后的协方差。关于协方差为零(不相关)的结论,同样适用于相关系数为零的情况。
一个特例:对于服从二维正态分布的随机变量(X, Y),“不相关”与“独立”是等价的。这是因为二维正态分布的概率密度函数中的参数ρ就是它们的相关系数,当ρ=0时,联合密度函数恰好等于两个边缘密度函数的乘积。
从两个变量到多个变量:协方差矩阵
上一节我们讨论了两个随机变量的情况,本节中我们来看看如何将其推广到多个随机变量。
假设有n个随机变量 X₁, X₂, ..., Xₙ,构成一个随机向量。我们可以计算任意两个变量 Xᵢ 和 Xⱼ 之间的协方差 Cov(Xᵢ, Xⱼ)。将这些协方差按顺序排列,就构成了一个 n × n 的协方差矩阵 C:
C = [Cov(Xᵢ, Xⱼ)], 其中 i, j = 1, 2, ..., n
由于 Cov(Xᵢ, Xⱼ) = Cov(Xⱼ, Xᵢ),协方差矩阵 C 是一个对称矩阵。
协方差矩阵有直观的矩阵运算表示形式。假设我们对每个随机变量进行了m次采样,并将去均值后的数据排列成矩阵 X(每列代表一个变量,每行代表一次观测),那么协方差矩阵可以近似计算为:
C ≈ (1/m) * XᵀX
这个形式在后续的主成分分析(PCA)等算法中非常有用。
思考:协方差矩阵是对称阵,对称阵的特征向量是正交的。这个性质将对称阵与正交变换联系起来,为数据降维(如PCA)提供了数学基础。
总结
本节课中我们一起学习了协方差及其相关概念。
- 我们首先学习了协方差的定义和两种计算公式,它衡量了两个随机变量变化的协同趋势。
- 我们明确了独立与不相关的区别与联系:独立必不相关,但不相关不一定独立。不相关特指没有线性关系。
- 我们了解了协方差存在上界,并由此引出了标准化的相关系数,其取值范围在[-1, 1]之间,能更好地度量线性相关的强度和方向。
- 最后,我们将概念从两个变量推广到多个变量,引入了协方差矩阵的概念。协方差矩阵是一个对称矩阵,概括了随机向量中所有变量两两之间的线性关系,是多元统计分析的核心工具之一。


理解协方差是掌握许多机器学习算法(如线性回归、主成分分析、高斯分布)的关键第一步。
人工智能—机器学习中的数学(七月在线出品) - P14:中心极限定理 📊
在本节课中,我们将要学习概率论中两个极其重要的定理:大数定律与中心极限定理。它们为统计学和机器学习中的许多方法提供了坚实的理论基础。
概述
切比雪夫不等式揭示了方差的物理意义:方差越小,随机变量取值集中在期望附近的概率越大。这个不等式是证明大数定律的关键工具。
上一节我们介绍了方差与期望的关系,本节中我们来看看如何从切比雪夫不等式出发,理解随机现象的长期稳定性与分布规律。
大数定律
大数定律描述了当独立重复试验次数足够多时,随机事件发生的频率会稳定地趋近于其概率。
设 X1, X2, ..., Xn 是相互独立且具有相同期望 μ 和方差 σ² 的随机变量。定义 Yn = (X1 + X2 + ... + Xn) / n。
根据切比雪夫不等式,可以证明当 n 趋向于无穷大时,Yn 以概率 1 收敛于期望 μ。公式表示为:
P( lim_{n→∞} Yn = μ ) = 1
这意味着,尽管单个随机变量的方差可能很大,但将大量独立同分布的随机变量取平均后,其结果会稳定在期望值附近。
以下是关于大数定律的几个要点:
- 频率与概率的关系:对于一个事件
A,其发生概率为p。在n次独立重复试验中,事件A发生的次数记为NA,则频率NA/n以概率1收敛于概率p。这几乎为概率提供了操作性的定义,即概率是频率的稳定值。 - 实践应用:大数定律是许多机器学习参数估计方法(如正态分布的参数估计、贝叶斯分类中的先验学习等)的理论依据,它使得我们能够用观测到的数据(频率)去推断未知的参数(概率)。
中心极限定理
中心极限定理则解释了为什么许多自然和社会现象都近似服从正态分布。
设 X1, X2, ..., Xn 是相互独立且具有相同期望 μ 和方差 σ² 的随机变量。考虑它们的和 Sn = X1 + X2 + ... + Xn。
中心极限定理指出,当 n 足够大时,标准化后的和 (Sn - nμ) / (√n * σ) 的分布会趋近于标准正态分布 N(0, 1)。其和 Sn 本身则近似服从正态分布 N(nμ, nσ²)。
以下是中心极限定理的核心思想与应用场景:
- 现象解释:如果一个结果是由大量微小、独立的随机因素共同作用产生的,那么这个结果的分布往往近似于正态分布。
- 实例说明:
- 城市用电量:可以看作是大量用户独立用电量的总和,因此近似服从正态分布。
- 测量误差:由许多无法控制的微小因素(如环境、仪器波动)综合导致,通常服从正态分布。
- 学生成绩:受智力、努力程度、临场状态等多种独立因素影响,一个班级的成绩分布通常也近似正态。如果严重偏离,可能暗示存在异常(如大面积作弊或试题设计不当)。
- 实验验证:下图展示了中心极限定理的模拟实验。从一个均匀分布中多次抽样并计算均值,这些均值的分布会呈现出正态分布的形状。







总结
本节课中我们一起学习了概率论的两大基石:大数定律与中心极限定理。

- 大数定律保证了在长期或大量的重复中,随机事件的频率会稳定在其概率附近,这为用样本推断总体提供了理论支持。
- 中心极限定理则揭示了无论原始随机变量服从什么分布,只要独立同分布且数量足够多,其和的标准化形式就会趋近于正态分布。这解释了正态分布在现实世界中的普遍性,并为许多统计推断方法(如线性回归中的最小二乘法)奠定了理论基础。


理解这两个定理,对于深入掌握机器学习和数据分析中的统计思想至关重要。

人工智能—机器学习中的数学(七月在线出品) - P15:重新理解矩阵 🧮
在本节课中,我们将从一个全新的视角重新理解矩阵方程 AX = B。我们将探讨其行视图与列视图的几何意义,并引出线性代数中的核心概念,如线性相关、线性无关、基与子空间。随后,我们将深入探讨矩阵分解,包括特征值分解与奇异值分解,并揭示它们之间的内在联系,最终构建一个完整的知识框架。
线性代数的基本知识
上一节我们概述了课程目标,本节中我们来看看线性代数的一些基本符号和概念,为后续内容打下基础。
以下是本节课将使用的主要数学符号表:
- R^n:n维实向量空间。
- R^(m×n):m行n列的实矩阵集合。
- A^T:矩阵A的转置。
- det(A):矩阵A的行列式。
- C(A):矩阵A的列空间。
- N(A):矩阵A的零空间(核空间)。
- A^(-1):矩阵A的逆矩阵。
- diag(v):将向量v转化为对角矩阵。
- tr(A):矩阵A的迹(对角线元素之和)。
- A^H:矩阵A的共轭转置(本节课仅讨论实数,可暂时视同A^T)。
- rank(A):矩阵A的秩。
说明:红色框标注的内容代表最重要的定理或概念,绿色框标注的则是帮助理解的示例。
重新理解 AX = B
我们从一个最基础的矩阵方程开始。考虑方程 AX = B,其中 A 是一个矩阵,X 和 B 是向量。教科书通常从行列式讲起,但我们将从更直观的几何视角——行视图和列视图——来切入。
行视图:方程组的交点
对于方程 AX = B,行视图将其理解为一系列线性方程的交集。
例如,给定矩阵和向量:
A = [[2, -1],
[1, 1]]
X = [x, y]^T
B = [1, 5]^T
方程 AX = B 等价于方程组:
2x - y = 1
x + y = 5
在二维空间中,每个方程代表一条直线。方程有解,意味着这两条直线相交于一点 (x=2, y=3)。
推广到三维,例如一个 3x3 的系统,每个方程代表一个平面。三个平面相交于一点,即该方程组的解。更高维的情况可以类推,每个方程定义一个“超平面”,解就是所有超平面的交点。
这个视角与机器学习中的“超平面”概念紧密相关,例如,约束
A^T X = b就定义了一个超平面。
列视图:向量的线性组合
现在,我们从列的角度审视同一个方程 AX = B。我们可以将矩阵A按列分块:
A = [a1, a2]
那么方程 AX = B 可以重写为:
x * a1 + y * a2 = B
这意味着,结果向量 B 是矩阵 A 各列向量的一个线性组合,组合系数就是向量 X 中的元素。
对于上面的例子:
A的列向量: a1 = [2, 1]^T, a2 = [-1, 1]^T
B = [1, 5]^T
解 X = [2, 3]^T 意味着:
2 * [2, 1]^T + 3 * [-1, 1]^T = [1, 5]^T
从几何上看,我们将向量 a1 拉伸2倍,将向量 a2 拉伸3倍,然后通过向量加法(平行四边形法则),恰好得到了向量 B。
行视图与列视图的联系与区别:
- 行视图关注方程描述的几何对象(直线、平面、超平面)如何相交。
- 列视图关注矩阵的列向量如何通过伸缩与相加来合成目标向量。
- 两者是同一数学事实的两种几何表现,但列视图更贴近线性代数“线性组合”的核心思想。
线性相关与线性无关
从列视图的讨论中,我们自然引出一个问题:是否任意向量 B 都能被一组给定的列向量组合出来?这直接关系到“线性相关”与“线性无关”的概念。
定义
给定一组向量 {v1, v2, ..., vn}:
- 线性相关:存在一组不全为零的标量
c1, c2, ..., cn,使得:
c1*v1 + c2*v2 + ... + cn*vn = 0
这意味着至少有一个向量可以被其他向量线性表示。 - 线性无关:只有当所有标量
c1, c2, ..., cn全为零时,上式才成立。即,没有任何一个向量可以表示为其他向量的线性组合。
示例与理解
考虑矩阵 A = [[1, 2, 3], [0, 1, 1], [1, 3, 4]] 的列向量:
- 列向量线性相关:因为
-1*[1,0,1]^T + (-1)*[2,1,3]^T + 1*[3,1,4]^T = [0,0,0]^T。我们发现第三列是前两列之和,它是“多余”的。 - 列向量线性无关:对于矩阵
A = [[1, 0, 0], [0, 1, 0], [0, 0, 1]](单位阵),其列向量是线性无关的。你无法找到非零系数使它们的线性组合为零向量。
与方程 AX=0 的联系:
方程 AX = 0 称为齐次方程。将其写作列向量形式:
x1*a1 + x2*a2 + ... + xn*an = 0
- 如果
A的列向量线性无关,那么上式成立当且仅当x1 = x2 = ... = xn = 0。此时AX=0只有零解,矩阵A是可逆的(对于方阵而言)。 - 如果
A的列向量线性相关,那么存在非零向量X使得AX=0。这意味着A的列空间无法充满整个空间,A不可逆。
基与子空间
理解了线性无关,我们就可以定义“基”和“子空间”这两个核心概念。
- 基:向量空间
V中一组线性无关的向量,并且它们能够张成(即通过线性组合表示)整个空间V。空间V的维数等于基中向量的个数。 - 子空间:一个向量空间的子集,如果它自身也满足向量空间的公理(对加法和数乘封闭),则称为子空间。矩阵
A的列空间C(A)(所有列向量的线性组合构成的集合)和零空间N(A)(所有满足AX=0的向量X的集合)是两个最重要的子空间。
矩阵分解:特征值分解 (EVD)
对于方阵,一种重要的分解方式是特征值分解。
定义与公式
若 n×n 方阵 A 有 n 个线性无关的特征向量,则可被分解为:
A = P * Λ * P^(-1)
其中:
P是由A的n个线性无关的特征向量组成的矩阵。Λ是由对应的特征值构成的对角矩阵,Λ = diag(λ1, λ2, ..., λn)。
特殊情形:对称矩阵
当 A 是实对称矩阵(A = A^T)时,情况更优美:
- 其特征值都是实数。
- 不同特征值对应的特征向量相互正交。
- 它可以被正交对角化:
A = Q * Λ * Q^T
其中Q是由单位正交特征向量组成的正交矩阵(Q^T * Q = I,即Q^(-1) = Q^T)。
对称矩阵的特征值分解是理解二次型 X^T A X 的基础,在优化问题(如判断凸性)中至关重要。
矩阵分解:奇异值分解 (SVD)
特征值分解只适用于方阵。对于任意 m×n 的矩阵 A,我们有一种“万能”的分解方法——奇异值分解。
定义与公式
任意实矩阵 A (m×n) 都可以分解为:
A = U * Σ * V^T
其中:
U是一个m×m的正交矩阵,其列向量称为左奇异向量,构成A * A^T的特征向量基。V是一个n×n的正交矩阵,其列向量称为右奇异向量,构成A^T * A的特征向量基。Σ是一个m×n的矩形对角矩阵,其对角线上的非负元素称为奇异值,通常按降序排列σ1 ≥ σ2 ≥ ... ≥ σr > 0,r = rank(A)。
SVD 与子空间的联系
SVD 完美地揭示了矩阵的四大基本子空间:
U的前r列:张成列空间C(A)。U的后m-r列:张成左零空间N(A^T)。V的前r列:张成行空间C(A^T)。V的后n-r列:张成零空间N(A)。
SVD 与 EVD 的关系
- 对于对称矩阵
A,其 SVD 与特征值分解本质相同,奇异值就是特征值的绝对值。 A^T * A的特征值是A的奇异值的平方,其特征向量组成了V。A * A^T的特征值也是A的奇异值的平方,其特征向量组成了U。
知识框架总结
本节课中,我们一起学习了如何从多个角度理解线性代数:
- 起点:我们从矩阵方程
AX = B出发,建立了行视图(方程交点)和列视图(向量线性组合)的几何直观。 - 核心概念:由列视图引出了线性相关/无关、基和子空间(列空间、零空间)这些构建线性代数大厦的基石。
- 矩阵分解:
- 对于方阵,我们学习了特征值分解 (EVD),特别是对称矩阵的优美性质。
- 对于任意矩阵,我们介绍了强大的奇异值分解 (SVD),它统一了四大子空间,并广泛应用于数据降维(PCA)、推荐系统、图像压缩等领域。
- 内在联系:SVD 是 EVD 的推广,它们通过矩阵
A^T A和A A^T相联系。所有这些概念都围绕着矩阵A及其基本子空间展开,形成了一个连贯的知识网络。


理解这个框架后,你在学习更具体的线性代数细节或应用机器学习算法时,就能清楚地知道每个数学工具在整个知识体系中的位置和作用。

人工智能—机器学习中的数学(七月在线出品) - P16:牛顿法与梯度下降法 🧠
在本节课中,我们将要学习两种在机器学习和优化问题中至关重要的算法:牛顿法与梯度下降法。我们将探讨它们的数学原理、核心思想、各自的优缺点以及它们之间的联系。这两种算法的本质都是通过“逼近”来寻找函数的最优解。
背景与问题定义
上一节我们介绍了微积分中的逼近思想。本节中我们来看看如何将这种思想应用于实际的优化问题。
很多机器学习或统计学习问题,最终都会转化为一个优化问题。例如,训练一个模型通常意味着最小化某个损失函数或代价函数。在本课程范围内,我们考虑的函数都是可微分的。
优化的核心问题是:对于一个可微分的函数,如何找到它的极值点?极值点分为全局极小值和局部极小值。
- 全局极小值:对于定义域内的任意点 ( x ),其函数值 ( f(x) ) 都大于等于 ( f(x^) ),则 ( x^ ) 是全局极小值点。
- 局部极小值:存在一个正数 ( \delta ),只要点 ( x ) 与 ( x^* ) 的距离小于 ( \delta ),就有 ( f(x^) \leq f(x) ),则 ( x^ ) 是局部极小值点。
无论是全局还是局部极值点,对于可微函数,在该点处的导数(一元函数)或梯度(多元函数)一定为零。我们将利用这个条件来设计寻找极值点的算法。
算法概述与比较
在深入细节之前,我们先对牛顿法和梯度下降法做一个整体的比较,了解它们的适用范围和特点。
以下是两种算法的核心对比:
- 优化类型:两者都是局部优化算法。因为它们都基于在某一点附近的逼近,所以只能找到初始点附近的局部极值,而非全局极值。
- 数学原理:
- 牛顿法使用二阶逼近(泰勒展开到二阶),利用了函数的曲率信息。
- 梯度下降法使用一阶逼近(泰勒展开到一阶),只利用了函数的斜率信息。
- 收敛速度:牛顿法通常比梯度下降法收敛速度更快,因为它使用了更多信息,能以更少的迭代步数达到较好的精度。
- 计算成本:牛顿法需要计算并求逆Hessian矩阵(二阶导数的矩阵),计算量大且复杂。梯度下降法只需计算梯度,每一步的计算更简单。
- 初始点与稳定性:两者都需要一个初始点。牛顿法对初始点更敏感,可能收敛到极大值点或鞍点。梯度下降法(因其“下降”特性)通常不会找到极大值,但若步长设置不当,也可能在极小值点附近震荡。
牛顿法详解 🔨
上一节我们概述了两种算法。本节中我们深入探讨牛顿法的具体原理。
牛顿法的核心思想是:在当前位置 ( x_0 ) 处,用原函数 ( f(x) ) 的二阶泰勒展开来逼近它,然后直接求解这个二次逼近函数的极值点,以此作为对原函数极值点的估计。
对于一个一元函数 ( f(x) ),在点 ( x_0 ) 处的二阶泰勒展开为:
[
f(x) \approx f(x_0) + f'(x_0)(x - x_0) + \frac{1}{2}f''(x_0)(x - x_0)^2
]
我们将右边除余项外的部分记为一个二次函数 ( g(x) )。我们知道二次函数 ( g(x) ) 的极值点在其导数为零处,即:
[
g'(x) = f'(x_0) + f''(x_0)(x - x_0) = 0
]
解这个方程,得到:
[
x = x_0 - \frac{f'(x_0)}{f''(x_0)}
]
我们将这个 ( x ) 记为 ( x_1 ),作为对 ( f(x) ) 极值点的第一次估计。然后以 ( x_1 ) 为新的起点,重复这个过程,就得到了牛顿法的迭代公式:
[
x_ = x_ - \frac{f'(x_)}{f''(x_)}
]
对于多元函数 ( f(\mathbf) ),其中 ( \mathbf ) 是一个向量,公式需要推广。我们用梯度 ( \nabla f ) 代替一阶导数,用 Hessian 矩阵 ( H(f) ) 代替二阶导数。Hessian 矩阵 ( H ) 的元素是函数的二阶偏导数:
[
H_ = \frac{\partial2 f}{\partial x_i \partial x_j}
]
多元情况下的牛顿法迭代公式为:
[
\mathbf = \mathbf - H(f)(\mathbf_){-1} \nabla f(\mathbf)
]
这里 ( H(f)(\mathbf)^{-1} ) 表示在点 ( \mathbf_ ) 处的 Hessian 矩阵的逆。
梯度下降法详解 📉
了解了利用二阶信息的牛顿法后,本节我们来看看只利用一阶信息的梯度下降法。
梯度下降法的思想更直观:既然梯度 ( \nabla f ) 的方向是函数值上升最快的方向,那么它的反方向 ( -\nabla f ) 就是函数值下降最快的方向。我们沿着这个方向走一小步,就能让函数值减小。
对于多元函数 ( f(\mathbf) ),在点 ( \mathbf_0 ) 处的一阶泰勒展开为:
[
f(\mathbf) \approx f(\mathbf_0) + \nabla f(\mathbf_0)^T (\mathbf - \mathbf_0)
]
记右边为线性函数 ( g(\mathbf) )。线性函数没有极值点,但它指明了变化的方向。为了使 ( f(\mathbf) ) 减小,我们应该让 ( \mathbf ) 朝着 ( -\nabla f(\mathbf_0) ) 的方向移动。但移动多远呢?一阶逼近无法告诉我们,因此我们需要手动设定一个步长 ( \alpha )(学习率)。
由此得到梯度下降法的迭代公式:
[
\mathbf = \mathbf - \alpha \nabla f(\mathbf_)
]
示例:考虑函数 ( f(x, y) = x2 + y2 ),在点 ( (1, 1) ) 处。
- 梯度 ( \nabla f = (2x, 2y) ),在 ( (1,1) ) 处为 ( (2, 2) )。
- 负梯度方向为 ( (-2, -2) )。
- 若选择学习率 ( \alpha = 0.1 ),则下一个点为:
[
(x_1, y_1) = (1, 1) - 0.1 * (2, 2) = (0.8, 0.8)
]
可以验证 ( f(0.8,0.8) = 1.28 < f(1,1) = 2 ),函数值确实下降了。
梯度下降法因为只使用了梯度信息,不知道每一步应该走多远(需要调参),所以收敛速度通常慢于牛顿法。
核心概念辨析与常见问题
在学习了两种算法的原理后,我们来澄清一些关键概念并解答常见疑问。
以下是关于梯度和算法选择的要点:
- 梯度与导数的关系:对于一元函数,梯度就是导数,是一个标量。对于多元函数,梯度是所有一阶偏导数构成的向量,它指向函数值增长最快的方向。
- 梯度下降的方向:梯度是上升最快的方向,因此 负梯度方向 才是下降最快的方向。这就是迭代公式中带有负号的原因。
- Hessian矩阵不可逆怎么办:这在牛顿法中是一个实际问题。如果Hessian矩阵奇异(不可逆),则无法计算迭代步长。这通常意味着初始点选择不当,或者需要采用改进的牛顿法(如拟牛顿法)。
- 如何选择初始点和学习率:初始点的选择依赖于具体问题,有时可以采用网格搜索等启发式方法。学习率 ( \alpha ) 是梯度下降法的关键超参数,太小会导致收敛慢,太大会导致震荡甚至发散,需要仔细调整。
- 如何找到全局最小值:标准的牛顿法和梯度下降法都是局部优化算法,无法保证找到全局最小值。解决全局优化问题需要更复杂的方法,如随机初始化多次运行、模拟退火、遗传算法等。
- 牛顿法与梯度下降法如何选择:没有绝对答案。如果计算Hessian矩阵及其逆的代价可以接受,且希望快速收敛,牛顿法可能是好选择。如果数据维度很高,计算Hessian矩阵太昂贵,或者问题非常大规模,梯度下降法及其变种(如随机梯度下降)更为实用。
总结与回顾
本节课中我们一起学习了机器学习和优化中两个基础而重要的算法:牛顿法与梯度下降法。
我们首先明确了优化问题的背景,即寻找函数的极值点。接着,我们对比了两种算法的特性:牛顿法使用二阶逼近,收敛快但计算量大;梯度下降法使用一阶逼近,计算简单但收敛慢。两者都是局部优化方法,效果依赖于初始点。
然后,我们深入推导了牛顿法的迭代公式,它通过不断用二次函数逼近并求解其极值来寻找原函数的极值点。对于多元函数,这涉及梯度向量和Hessian矩阵。

之后,我们探讨了梯度下降法,其核心是沿着当前点负梯度的方向,以一个固定步长(学习率)前进,逐步逼近极小值点。
最后,我们辨析了梯度等核心概念,并讨论了算法在实际应用中可能遇到的问题,如初始点选择、全局优化困境以及算法选择策略。

理解这两种算法的原理和权衡,是深入学习更高级优化技术(如共轭梯度法、拟牛顿法、Adam等)的重要基础。
人工智能—机器学习中的数学(七月在线出品) - P17:偏差方差均衡和模型选择 📊
概述
在本节课中,我们将要学习机器学习中一个核心概念:偏差-方差均衡。我们将探讨偏差和方差的定义、它们如何影响模型性能,以及如何通过模型选择和正则化来平衡这两者,从而构建泛化能力更强的模型。
基础知识回顾
在深入探讨偏差-方差均衡之前,我们需要回顾几个核心的基础概念,包括线性回归、正态分布和最大似然估计。这些概念是理解后续内容的关键。
线性回归
线性回归是回归问题中最简单的模型。我们假设观测到的数据为 (X1, Y1), (X2, Y2), ..., (Xn, Yn),其中 X 是特征向量,Y 是连续的标签值。
模型的目标是找到一个线性函数 f(X) = X * θ 来预测 Y。我们通过最小化预测值 Y_hat 与真实值 Y 之间的均方误差来求解参数 θ。
目标函数(均方误差):
L(θ) = (1/n) * Σ (Y_i - X_i * θ)^2
通过最小化 L(θ),我们可以得到参数 θ 的解析解:
θ_hat = (X^T * X)^(-1) * X^T * Y
正态分布
正态分布是一种常见的连续概率分布。对于一个正态分布 N(μ, σ^2),μ 是期望(均值),σ^2 是方差。
概率密度函数:
f(x) = (1 / √(2πσ^2)) * exp(-(x-μ)^2 / (2σ^2))
期望 μ 描述了分布的中心位置,方差 σ^2 描述了数据围绕均值的离散程度。方差越大,分布越“宽”;方差越小,分布越“尖”。
最大似然估计
最大似然估计是一种参数估计方法,其核心思想是:在已知观测数据的情况下,选择能使这些数据出现概率最大的参数值。
对于线性回归,假设误差服从正态分布,那么最小化均方误差等价于进行最大似然估计。


什么是偏差和方差?
上一节我们回顾了基础知识,本节中我们来看看偏差和方差的正式定义。理解这两个概念是掌握偏差-方差均衡的前提。
我们假设存在一个真实的参数 θ,而我们通过模型从数据中估计出的参数是 θ_hat。由于数据是随机采样得到的,因此 θ_hat 也是一个随机变量。
偏差:衡量的是估计值的期望与真实值之间的差距。
Bias(θ_hat) = E[θ_hat] - θ偏差反映了模型本身的系统性误差。偏差高,意味着模型过于简单,无法捕捉数据中的真实规律(欠拟合)。
方差:衡量的是估计值自身的离散程度,即其波动性。
Variance(θ_hat) = E[(θ_hat - E[θ_hat])^2]方差反映了模型对训练数据中随机噪声的敏感程度。方差高,意味着模型过于复杂,过度拟合了训练数据中的细节和噪声(过拟合)。
一个关键的理解:这里的随机性来源于数据。因为我们用于训练模型的数据只是从真实数据分布中随机抽取的一个样本,所以基于不同数据样本训练出的模型参数 θ_hat 会有所不同,因此我们可以讨论它的期望和方差。
偏差-方差均衡
理解了偏差和方差的定义后,本节我们来看看它们如何共同决定模型的总体误差,以及为什么需要在两者之间进行权衡。
模型的总体误差(期望泛化误差)可以分解为偏差、方差和一个不可约的噪声项。对于平方误差损失,其分解公式如下:
Error = E[(Y - f_hat(X))^2] = Bias(f_hat)^2 + Variance(f_hat) + Noise

从这个公式可以看出:
- 偏差的平方:模型预测值与真实值平均差异的平方。
- 方差:模型预测值自身的波动范围。
- 噪声:数据中固有的、无法被任何模型消除的随机误差。



偏差-方差均衡的核心思想:在模型复杂度和泛化能力之间取得平衡。
- 简单模型(如线性回归):通常具有高偏差、低方差。模型不够灵活,可能无法拟合数据的真实结构(欠拟合),但对数据扰动不敏感,表现稳定。
- 复杂模型(如高阶多项式、深度神经网络):通常具有低偏差、高方差。模型非常灵活,可以几乎完美拟合训练数据,但过度学习了数据中的噪声,导致在新数据上表现不稳定(过拟合)。


我们的目标是找到一个“甜蜜点”,使得偏差和方差之和最小,从而获得最佳的泛化性能。
应用:模型选择与正则化
上一节我们介绍了偏差-方差均衡的理论,本节中我们来看看它在实践中的两个主要应用:模型选择和正则化。
模型选择
模型选择的核心是找到那个使偏差和方差之和最小的模型复杂度。以下是一些常见的方法:
以下是几种模型选择策略:
- 交叉验证:将数据分为训练集和验证集(或K折),在训练集上训练不同复杂度的模型,在验证集上评估其性能,选择性能最好的模型。这直接评估了模型在未见数据上的表现(泛化误差)。
- 信息准则:如AIC(赤池信息准则)和BIC(贝叶斯信息准则)。它们在模型的对数似然值上加上一个与模型参数个数成正比的惩罚项,平衡拟合优度和模型复杂度。选择AIC或BIC值最小的模型。
(其中AIC = -2 * log(L) + 2 * k BIC = -2 * log(L) + log(n) * kL是似然函数值,k是参数个数,n是样本数)
正则化
正则化是一种在模型训练过程中显式地控制模型复杂度、降低方差的技术。它通过在损失函数中增加一个对模型参数的惩罚项来实现。
最常见的两种正则化范数:
- L2正则化(岭回归):惩罚项是参数向量的L2范数平方。它倾向于让所有参数值都较小且分布均匀,但通常不会将参数压缩至零。
损失函数 = 原始损失 + λ * Σ(θ_i^2) - L1正则化(LASSO回归):惩罚项是参数向量的L1范数。它倾向于产生稀疏解,即会将一些不重要的特征对应的参数压缩至零,因此也具有特征选择的功能。
损失函数 = 原始损失 + λ * Σ|θ_i|
参数 λ 控制正则化的强度:
λ越大,惩罚越重,模型参数值被限制得越小,模型越简单(高偏差,低方差)。λ越小,惩罚越轻,模型越倾向于拟合训练数据(低偏差,高方差)。


通过调整 λ,我们实际上是在偏差和方差之间进行权衡,寻找最优的平衡点。选择 λ 的过程本身也是一个模型选择问题,通常通过交叉验证来完成。
总结
本节课中我们一起学习了机器学习中的核心概念——偏差-方差均衡。
我们首先回顾了线性回归和正态分布等基础知识。然后,我们明确了偏差和方差的定义:偏差源于模型本身的错误假设,方差源于模型对训练数据随机波动的过度敏感。接着,我们看到了模型的总体误差可以分解为偏差、方差和噪声之和,这揭示了模型复杂度与泛化能力之间需要权衡的本质。


最后,我们探讨了这一理论的两个重要实践应用:通过交叉验证或信息准则进行模型选择,以及通过L1/L2正则化在训练中直接控制模型复杂度。理解并应用偏差-方差均衡,是构建稳健、高性能机器学习模型的关键。
人工智能—机器学习中的数学(七月在线出品) - P18:随机梯度下降法的困难与变种 🧠
概述
在本节课中,我们将要学习梯度下降法在机器学习应用中遇到的两个主要困难,以及工程师们为解决这些困难而提出的一系列改进算法,特别是随机梯度下降法及其各种变种。
梯度下降法的两个主要困难
上一节我们介绍了梯度下降法的基本原理。然而,在具体的工程实践中,标准的梯度下降法会遇到两个显著的困难。
困难一:梯度计算开销大
在机器学习中,目标函数通常不是由一个简单的公式给出。它通常是大量样本损失函数的求和。对这样的函数求梯度,需要对每个样本的损失函数分别求导后再求和。当样本量巨大时,这个过程会非常耗时耗力。
困难二:学习率难以选择
学习率的选择是一个棘手的问题。学习率设置过小,会导致算法收敛速度极慢。学习率设置过大,则可能导致算法在最优解附近震荡,甚至无法收敛。对于新问题,通常需要反复试验来寻找合适的学习率,这非常耗费时间。
随机梯度下降法 (Stochastic Gradient Descent, SGD)
为了解决梯度计算开销大的问题,工程师们提出了随机梯度下降法。这是目前工程上使用最广泛的梯度下降类优化算法。
基本思想
既然对所有样本计算梯度太慢,那么每次只用一个样本计算梯度。最原始的随机梯度下降法正是这样做的:每次迭代时,随机选取一个样本计算梯度并更新参数。
以下是其核心更新公式的简化表示:
θ = θ - η * ∇J_i(θ)
其中,∇J_i(θ) 是第 i 个样本的损失函数梯度,η 是学习率。
优点与缺点
以下是随机梯度下降法的主要特点:
- 优点1:计算高效:每次迭代只计算一个样本的梯度,大大减少了计算量。
- 优点2:可能跳出局部极小值:由于梯度估计带有随机噪声,这增加了算法跳出局部极小值的潜力。
- 缺点:路径震荡:单个样本的梯度方向不能代表整体数据的梯度方向,导致优化路径非常嘈杂、震荡剧烈,可能影响收敛。
为了应对路径震荡的问题,在实践中通常会采用一个策略:逐渐缩小学习率。开始时使用一个较大的学习率进行快速探索,随后逐渐减小学习率,使算法最终能够稳定收敛。
小批量随机梯度下降法 (Mini-batch SGD)
只使用一个样本虽然快,但梯度估计的噪声太大,不够稳定。因此,更常用的改进版本是小批量随机梯度下降法。
核心做法
每次梯度计算时,不使用全部样本,也不只使用一个样本,而是随机抽取一小批样本(例如100个)来计算梯度的平均值,并用此平均值来更新参数。
其更新公式可表示为:
θ = θ - η * (1/m) * Σ ∇J_k(θ)
其中,m 是小批量的大小,Σ ∇J_k(θ) 是该批样本梯度的总和。
为何成为主流
以下是它成为主流方法的原因:
- 平衡效率与稳定性:它比标准梯度下降法计算更快,比原始SGD的梯度估计更稳定。
- 硬件友好:小批量的计算可以很好地利用现代计算库(如针对矩阵运算优化的库)进行并行加速。
- 术语惯例:在深度学习领域,通常所说的“SGD”指的就是小批量随机梯度下降法。
关于“随机”二字的含义:它主要指在每轮训练开始前,会将训练数据集随机打乱顺序,然后按顺序依次抽取小批量。这确保了每个样本都有机会被使用,且顺序是随机的。
标准梯度下降法的其他局限
随机梯度下降法主要解决了计算开销的问题,但对于学习率选择困难以及某些复杂函数地形下的优化问题,仍然存在局限。
问题一:在“峡谷”地形中表现不佳
考虑一种像狭窄倾斜峡谷的地形。整体下降方向是沿着峡谷走向,但峡谷两侧的壁非常陡峭。梯度下降法在局部点计算梯度时,陡峭侧壁的方向分量可能很大,导致更新路径在峡谷两侧来回震荡,像“之”字形前进,整体收敛速度很慢。
问题二:学习率对所有参数“一视同仁”
在复杂模型中,不同参数的重要性、更新频率可能不同。理想情况下,对于不常更新的参数,一旦有机会更新,应该用较大的学习率;对于频繁更新的参数,则应用较小的学习率进行精细调整。固定的全局学习率无法做到这一点。
随机梯度下降法的改进变种
为了解决上述问题,研究者们提出了多种改进算法。
1. 动量法 (Momentum)
动量法旨在解决“峡谷”地形中的震荡问题。它的思想类似于物理学中的动量:在更新时不仅考虑当前的梯度,还会保留一部分上一次的更新方向。这有助于抵消不同方向上的震荡,使优化方向更加一致地沿着峡谷走向前进。
2. AdaGrad / RMSProp
这类方法旨在解决参数自适应学习率的问题。其核心思想是:对于历史上更新频繁(梯度平方和较大)的参数,给予较小的学习率;对于更新不频繁(梯度平方和较小)的参数,给予较大的学习率。 这样每个参数都拥有了自己的、随时间调整的学习率。
3. Adam (Adaptive Moment Estimation)
Adam方法结合了动量法和自适应学习率方法的优点。它既像动量法一样记录梯度的一阶矩(均值)来保持方向,又像RMSProp一样记录梯度的二阶矩(未中心化的方差)来为每个参数调整学习率。因此,它能同时应对崎岖地形和参数学习率自适应的问题。
如何选择优化算法
了解不同算法的设计目的,能帮助我们做出更合适的选择。以下是一个简单的选择思路:
- 如果你的模型训练相对平稳,没有明显的“峡谷”地形问题,可以优先选择RMSProp或Adam,它们能自适应调整学习率。
- 如果你的优化问题地形复杂,存在大量震荡,可以尝试使用动量法。
- 如果你不确定问题的特性,或者希望有一个表现稳健的默认选择,Adam优化器通常是一个不错的起点,因为它结合了多种优点。
在实际应用中,主流深度学习框架(如TensorFlow, PyTorch)都内置了这些优化器,我们只需在配置模型时选择即可。


总结
本节课中我们一起学习了梯度下降法在工程实践中的两大困难:巨大的梯度计算开销和难以选择的学习率。为了克服这些困难,我们介绍了随机梯度下降法(SGD)及其更实用的版本小批量随机梯度下降法,它们通过减少每次迭代的计算量来提升效率。此外,我们还探讨了标准方法在复杂优化地形中的局限,并介绍了几种重要的改进变种:动量法用于减少震荡、AdaGrad/RMSProp用于为每个参数自适应学习率,以及综合二者优点的Adam算法。理解这些算法的核心思想,将帮助你在实际项目中更有效地选择和运用优化工具。

人工智能—机器学习中的数学(七月在线出品) - P2:半小时梳理凸优化

📚 概述
在本节课中,我们将共同讨论凸优化相关的问题。主要内容将从凸集、凸函数和凸优化这三个核心方面展开。通过本节课的学习,你将理解凸优化的基本概念、性质以及其在机器学习中的重要性。
🧱 第一部分:凸集
上一节我们介绍了课程的整体结构,本节中我们来看看凸集。凸集是凸优化的基础,理解凸集有助于我们后续理解凸函数和凸优化问题。
凸集的定义
如果一个集合 C 满足:对于集合内任意两点,连接这两点的线段上的所有点也都在该集合内,那么这个集合就是一个凸集。
用数学公式描述如下:
对于任意 ( x_1, x_2 \in C ) 和任意 ( \theta \in [0, 1] ),都有:
[
\theta x_1 + (1 - \theta) x_2 \in C
]
以下是凸集与非凸集的例子:
- 一个任意的凸多边形是一个凸集。
- 一条线段是一个凸集。
- 一个内部有凹陷的扇形区域不是一个凸集。
- 一个边界有缺失的多边形不是一个凸集。
超平面与半空间
超平面和半空间是构建更复杂凸集(如多面体)的基本单元。
- 超平面:由方程 ( a^T x = b ) 定义的所有点 ( x ) 的集合,其中 ( a ) 是一个非零向量。
- 半空间:由不等式 ( aT x \leq b ) 或 ( aT x \geq b ) 定义的所有点 ( x ) 的集合。
在二维空间中,超平面是一条直线,( a ) 是其法线方向。半空间则是这条直线某一侧的所有点。
多面体
多面体是由有限个线性不等式和等式定义的集合。其标准形式为:
[
\begin
Ax \preceq b \
Cx = d
\end
]
其中 ( A ) 和 ( C ) 是矩阵,( b ) 和 ( d ) 是向量。满足这些条件的 ( x ) 的集合就是一个多面体。
多面体是凸集。在有些文献中,有界的多面体也被称为多胞形。
保持凸性的运算
某些运算作用于凸集后,结果仍然是凸集,这些运算称为保凸运算。
以下是几种重要的保凸运算:
- 交集:任意多个凸集的交集仍然是凸集。
- 仿射变换:若 ( f(x) = Ax + b ) 是一个仿射变换(( A ) 是矩阵,( b ) 是向量),则凸集 ( S ) 在 ( f ) 下的像 ( f(S) ) 是凸集;反之,若 ( f(S) ) 是凸集,则原像 ( S ) 也是凸集。
- 透视变换:函数 ( P: \mathbb^{n+1} \rightarrow \mathbb^n ) 定义为 ( P(z, t) = z/t )(其中 ( t > 0 ))。凸集在透视变换下的像仍然是凸集。
- 投射函数(线性分式变换):这是仿射函数与透视函数的复合,形式为 ( f(x) = (Ax + b) / (cT x + d) ),其定义域为 ( {x | cT x + d > 0} )。投射函数也是保凸运算。
📈 第二部分:凸函数
在理解了凸集之后,本节我们来看看凸函数。凸函数在优化问题中具有极好的性质,例如局部最小值就是全局最小值。
凸函数的定义
一个函数 ( f: \mathbb^n \rightarrow \mathbb ) 是凸函数,如果其定义域 ( \text f ) 是凸集,且对于所有 ( x, y \in \text f ) 和 ( \theta \in [0, 1] ),满足:
[
f(\theta x + (1-\theta) y) \leq \theta f(x) + (1-\theta) f(y)
]
这个不等式的几何意义是:函数图像上任意两点间的线段(割线)位于函数图像的上方。
凸函数的一阶条件
如果函数 ( f ) 一阶可微,那么 ( f ) 是凸函数当且仅当其定义域是凸集,且对于所有 ( x, y \in \text f ),满足:
[
f(y) \geq f(x) + \nabla f(x)^T (y - x)
]
这个不等式的意义是:函数在任何一点的一阶泰勒展开式(即该点处的切线或切平面)是函数的一个全局下估计。
凸函数的二阶条件
如果函数 ( f ) 二阶可微,那么 ( f ) 是凸函数当且仅当其定义域是凸集,且其 Hessian 矩阵是半正定的:
[
\nabla^2 f(x) \succeq 0
]
对于一元函数,这等价于二阶导数 ( f''(x) \geq 0 )。
上镜图
函数 ( f ) 的上镜图定义为:
[
\text f = { (x, t) | x \in \text f, f(x) \leq t }
]
一个函数是凸函数,当且仅当它的上镜图是凸集。这个性质将凸函数与凸集紧密联系了起来。
Jensen 不等式
凸函数的定义可以推广到多个点和连续的情形,这就是 Jensen 不等式。
离散形式:若 ( f ) 是凸函数,则
[
f(\sum_^k \theta_i x_i) \leq \sum_^k \theta_i f(x_i)
]
其中 ( \theta_i \geq 0 ) 且 ( \sum_^k \theta_i = 1 )。
连续/期望形式:若 ( f ) 是凸函数,则
[
f(\mathbb[x]) \leq \mathbb[f(x)]
]
Jensen 不等式是证明许多其他不等式(如算术-几何平均不等式、KL散度非负性)的基础工具。
保持函数凸性的运算
与凸集类似,对凸函数进行某些运算后,结果仍然是凸函数。
以下是几种保持凸性的运算:
- 非负加权和:若 ( f_1, ..., f_k ) 是凸函数,( \omega_1, ..., \omega_k \geq 0 ),则 ( f = \sum_^k \omega_i f_i ) 是凸函数。
- 与仿射函数复合:若 ( f ) 是凸函数,则 ( g(x) = f(Ax + b) ) 也是凸函数。
- 逐点取最大值:若 ( f_1, ..., f_k ) 是凸函数,则 ( f(x) = \max{f_1(x), ..., f_k(x)} ) 是凸函数。
- 逐点上确界:若对于每个 ( y \in \mathcal ),函数 ( f(x, y) ) 关于 ( x ) 是凸的,则函数 ( g(x) = \sup_{y \in \mathcal} f(x, y) ) 关于 ( x ) 是凸的。
逐点取最大值运算的凸性,可以直观理解为:若干条直线(凸函数)在每一点处取最高的那条,形成的“屋顶”形状仍然是凸的。
⚙️ 第三部分:凸优化
掌握了凸集和凸函数,我们现在可以正式进入凸优化部分。凸优化问题因其良好的性质,是机器学习中许多算法的基础。
优化问题的一般形式
一个优化问题通常可以写成如下形式:
[
\begin
& \underset{\text}
& & f_0(x) \
& \text
& & f_i(x) \leq 0, ; i = 1, \ldots, m \
& & & h_j(x) = 0, ; j = 1, \ldots, p
\end
]
其中 ( x \in \mathbb^n ) 是优化变量,( f_0 ) 是目标函数,( f_i ) 是不等式约束函数,( h_j ) 是等式约束函数。
凸优化问题的定义
如果上述一般形式中的目标函数 ( f_0 ) 和所有不等式约束函数 ( f_i ) 都是凸函数,并且所有等式约束函数 ( h_j ) 都是仿射函数(即 ( h_j(x) = a_j^T x - b_j )),那么该问题就是一个凸优化问题。
凸优化问题的关键性质是:其可行域是凸集,并且任何局部最优解都是全局最优解。
拉格朗日对偶
对于不一定为凸的一般优化问题,我们可以通过拉格朗日对偶将其转化为一个凸优化问题(对偶问题)。
首先,为原问题构造拉格朗日函数,引入拉格朗日乘子 ( \lambda_i )(对应不等式约束)和 ( \nu_j )(对应等式约束):
[
L(x, \lambda, \nu) = f_0(x) + \sum_^m \lambda_i f_i(x) + \sum_^p \nu_j h_j(x)
]
其中要求 ( \lambda_i \geq 0 )。
然后,定义拉格朗日对偶函数 ( g ) 为拉格朗日函数关于 ( x ) 的下确界:
[
g(\lambda, \nu) = \inf_{x \in \mathcal} L(x, \lambda, \nu)
]
对偶函数 ( g(\lambda, \nu) ) 具有一个非常重要的性质:无论原问题是否为凸,对偶函数 ( g ) 总是凹函数。这是因为对于固定的 ( x ),( L(x, \lambda, \nu) ) 是关于 ( (\lambda, \nu) ) 的仿射函数,而一系列仿射函数逐点取下确界的结果是凹函数。
对偶函数给出了原问题最优值 ( p^* ) 的一个下界:对于任意 ( \lambda \succeq 0 ) 和 ( \nu ),有 ( g(\lambda, \nu) \leq p^* )。
对偶问题与强对偶性
为了得到最紧的下界,我们求解对偶问题,即最大化对偶函数:
[
\begin
& \underset{\lambda, \nu}{\text}
& & g(\lambda, \nu) \
& \text
& & \lambda \succeq 0
\end
]
由于 ( g ) 是凹函数,且约束是简单的非负约束,因此对偶问题总是一个凸优化问题。
设对偶问题的最优值为 ( d^* )。根据定义,总有 ( d^* \leq p^* ),这个性质称为弱对偶性。如果等号成立,即 ( d^* = p^* ),则称强对偶性成立。
KKT 条件
当原问题是凸优化问题且满足某些约束规范(如 Slater 条件)时,强对偶性通常成立。此时,原问题最优解 ( x^* ) 和对偶问题最优解 ( \lambda^, \nu^ ) 必须满足一组称为 Karush-Kuhn-Tucker (KKT) 条件 的方程。
KKT 条件包括:
- 原始可行性:( f_i(x^) \leq 0, \quad h_j(x^) = 0 )
- 对偶可行性:( \lambda_i^* \geq 0 )
- 互补松弛条件:( \lambda_i^* f_i(x^*) = 0 )
- 梯度为零:( \nabla f_0(x^) + \sum_m \lambda_i \nabla f_i(x^) + \sum_p \nu_j \nabla h_j(x^*) = 0 )
对于凸优化问题,如果强对偶性成立,那么满足 KKT 条件的点就是原问题和对偶问题的最优解。
实例:最小二乘问题的对偶
考虑一个受线性等式约束的最小二乘问题:
[
\begin
& \underset{\text}
& & xT x \
& \text
& & Ax = b
\end
]
其拉格朗日函数为 ( L(x, \nu) = xT x + \nuT (Ax - b) )。通过对 ( x ) 求导并令其为零,可以求出对偶函数 ( g(\nu) ),再最大化 ( g(\nu) ) 得到对偶解。最终可以验证,通过求解这个对偶凸优化问题得到的最优解 ( x* ),与直接求解原问题得到的结果一致,这证明了该问题具有强对偶性。
🎯 总结
本节课中,我们一起学习了凸优化的核心内容。
我们首先从凸集开始,理解了其定义、相关概念(如超平面、半空间、多面体)以及保持凸性的运算(交集、仿射变换等)。
接着,我们深入探讨了凸函数,学习了其定义、一阶和二阶判定条件、上镜图概念、Jensen不等式以及保持函数凸性的运算(如加权和、与仿射函数复合、逐点取最大值)。
最后,我们将前两部分知识应用于凸优化。我们定义了凸优化问题,并介绍了处理更一般优化问题的强大工具——拉格朗日对偶。通过构建对偶问题,我们总能得到一个凸优化问题,并在满足强对偶性的情况下,利用KKT条件来求解原问题的最优解。
凸优化为机器学习中的许多模型(如支持向量机、逻辑回归)提供了坚实的理论基础和高效的求解思路。





人工智能—机器学习中的数学(七月在线出品) - P3:概率计算与拒绝采样 🎲
在本节课中,我们将要学习概率论中的两个核心概念:事件独立性的判断与拒绝采样方法。我们将从一个有趣的“机场会面”问题入手,探讨如何用几何概型解决概率计算,并深入理解如何利用一个已知的随机数生成器来构造另一个随机数生成器。
回顾:机场会面问题 ✈️

上一节我们介绍了如何用分段积分计算概率,本节中我们来看看如何用更直观的几何概型来解决一个经典问题。



问题描述如下:A国和B国的元首约定在晚上8点到0点之间于机场交换文件。A国飞机先到会等待1小时,B国飞机先到会等待2小时。假设两架飞机在8点到0点间到达的时刻服从均匀分布,且行动相互独立。求他们能成功交换文件的概率。

我们可以将A、B到达的时刻分别设为随机变量X和Y。由于时间区间是8点到0点,为方便计算,我们可以将其平移到0到4小时。那么,成功交换的条件可以表示为:
- 当A先到(X < Y)时,需满足
Y - X <= 1。 - 当B先到(Y < X)时,需满足
X - Y <= 2。
同时,X和Y都必须落在区间[0, 4]内。

几何概型解法 📐
我们可以建立一个平面直角坐标系,横轴为X(A到达时刻),纵轴为Y(B到达时刻)。总的基本事件区域是边长为4的正方形,其面积为 4 * 4 = 16。

在这个正方形内,满足交换条件的区域由以下三条直线围成:
Y = X(对角线)Y = X + 1(A等待线)Y = X - 2(B等待线)



阴影区域(即满足 |X-Y| <= 1 或 |X-Y| <= 2 且在正方形内的部分)的面积与正方形总面积之比,即为所求概率。通过计算两个空白三角形的面积,再用总面积减去它们,可以方便地得到阴影面积,最终算出概率值。
核心公式:
P(成功交换) = 阴影区域面积 / 正方形总面积
从随机到随机:拒绝采样法 🎯
解决了会面问题,我们来看另一个经典问题:如何利用一个已知的均匀随机数生成器,构造另一个我们需要的均匀随机数生成器。
具体问题是:假设有一个函数 rand7(),可以均匀地返回1到7之间的整数。要求仅使用 rand7() 来构造一个函数 rand10(),使其能均匀地返回1到10之间的整数。

一个直观但错误的想法是:将两次 rand7() 的结果相加或相减。例如 rand7() + rand7() - 4,其取值范围是1到10,但其分布不再是均匀的。因为两个均匀分布的和的分布会趋向于正态分布(中心极限定理)。
正确的构造方法:拒绝采样
以下是构造 rand10() 的正确步骤:
扩大采样空间:利用
rand7()构造一个更大范围的均匀分布。我们可以将两次调用rand7()的结果看作一个两位的7进制数。- 令
a = rand7() - 1,得到均匀的[0, 6]。 - 令
b = rand7() - 1,得到另一个均匀的[0, 6]。 - 计算
num = a * 7 + b。这样,num可以均匀地覆盖0到48(即7*7 - 1)这49个整数。
- 令
拒绝非均匀部分:我们的目标是1到10,即10个均匀的数。49不是10的整数倍,所以不能直接映射。我们采用“拒绝采样”策略:只接受
0到39这40个数字(因为40是10的整数倍),拒绝40到48这9个数字。- 如果
num >= 40,则本次采样无效,重新执行步骤1。 - 如果
num < 40,则进入步骤3。
- 如果
均匀映射到目标区间:对于被接受的
num(取值范围0~39),我们通过num % 10 + 1将其均匀映射到[1, 10]区间。
代码描述:
def rand10():
while True:
a = rand7() - 1 # [0, 6]
b = rand7() - 1 # [0, 6]
num = a * 7 + b # [0, 48]
if num < 40: # 拒绝采样,只保留[0, 39]
return num % 10 + 1 # 映射到[1, 10]
这种方法保证了 rand10() 返回每个数字的概率严格相等,均为 1/10。其采样效率(接受率)为 40/49。

事件的独立性与信息度量 ⚖️
在解决了具体的概率计算和采样问题后,我们需要回到理论层面,理解事件间的关系。

事件独立的定义
对于两个事件A和B,如果它们满足以下公式,则称A和B相互独立:
P(A ∩ B) = P(A) * P(B)
这个公式的含义是,事件A和B同时发生的概率,等于它们各自发生概率的乘积。根据条件概率公式 P(A|B) = P(A∩B) / P(B),当A和B独立时,可以推导出 P(A|B) = P(A)。这意味着,知道B是否发生,完全不影响A发生的概率。在实践中,我们常常根据事件的实际意义(如两次独立的实验)来判断其独立性,而非直接计算上述公式。
如何度量事件间的信息关联?
一个自然的想法是:如果两个事件独立,它们之间“共享的信息量”应该为0。那么,如何量化这种“共享的信息量”呢?
以下是几种思路:
- 基于条件概率的差值:可以定义为
P(A) - P(A|B)或P(A|B) - P(A)。当独立时,差值为0。但这种定义可能不对称。 - 基于概率比的对数:更常见的思路是观察比值
P(A|B) / P(A)。当独立时,比值为1。对其取对数log[ P(A|B) / P(A) ],独立时结果即为0。这个量在信息论中与“互信息”的概念紧密相关,它对称地度量了两个变量之间共享的信息。

总结 📝
本节课中我们一起学习了:
- 几何概型在概率计算中的应用:通过将“机场会面”问题转化为二维平面上的面积计算,我们得到了一个直观且高效的解法。
- 拒绝采样法:掌握了如何利用一个基础的均匀随机源(
rand7()),通过“采样-拒绝-映射”的步骤,构造出另一个均匀随机生成器(rand10())。这是蒙特卡洛方法中的重要基础。 - 事件的独立性:从数学定义(
P(AB)=P(A)P(B))和直观意义(一个事件的发生不影响另一个)两个角度理解了独立性。 - 信息关联的度量:初步探讨了如何量化两个事件之间的信息关联,引出了基于概率比的对数思想,为后续学习信息论概念(如互信息)做了铺垫。

这些内容将概率论的基本思想与解决实际问题的技巧相结合,是理解更复杂机器学习算法中随机过程的基础。
人工智能—机器学习中的数学(七月在线出品) - P4:概率论基础
在本节课中,我们将要学习概率论的基础知识,这是理解机器学习模型背后原理的基石。我们将从概率的基本定义出发,探讨概率密度、累积分布等核心概念,并通过实例理解条件概率、贝叶斯公式等重要思想。
概率的基本认识 🎲
如果一个事件X发生的可能性是一定会发生,我们记作1。如果一定不会发生,则记作0。因此,概率可以看作是关于事件X的一个函数,记作 P(X)。这个概率值一定是从0到1的,可以取0,也可以取1。
但是需要注意,反过来并不总是成立。如果一个事件一定不发生,其概率一定是0,这句话没错。然而,如果一个事件发生的概率为0,并不意味着这个事件一定不会发生。例如,在一个桌面上投针,投之前针尖落在桌面任意一个特定点的概率为0(因为点的面积除以桌面面积是0),但投掷后,针尖总会落在某个具体的点上,这个事件就发生了。
同理,概率为1也并不意味着事件一定发生。例如,模拟退火算法以概率1收敛于全局最优解,但并不意味着它一定会收敛到全局最优解。
离散与连续概率 📊
如果X是一个离散的随机变量,那么 P(X = x0) 表示X取特定值x0的概率。如果X是连续的,我们通常使用概率密度函数来描述。概率密度函数是累积分布函数的导数。
在计算机科学和机器学习的视角下,离散情况的求和(Σ)与连续情况的积分(∫)本质是相同的,只是处理的对象不同。
累积分布函数 📈
给定概率分布后,我们可以计算累积概率分布函数(CDF)。对于随机变量X,其累积分布函数 F(x0) 定义为 P(X ≤ x0)。这个函数是单调非减的。当x0取定义域的最小值时,F(x0)为0;当x0取最大值时,F(x0)为1。
反过来思考,如果一个函数 y = F(x) 的值域是[0, 1]并且是单调递增的,那么它可以被看作是某个随机变量的累积分布函数。对其求导即可得到概率密度函数(PDF)。逻辑回归等模型正是基于这个思想构建的。
- 概率密度函数 简称 PDF。
- 累积分布函数 简称 CDF。
古典概型实例 🧮
上一节我们介绍了概率的基本概念,本节中我们来看看如何应用这些概念解决实际问题。以下是几个古典概型的例子。
例1:生日悖论
将n个人随机分配到N=365天(生日)中,计算至少有两人生日相同的概率。所有基本事件数为 N^n。有效事件(所有人生日都不同)数为排列数 P(N, n)。因此,至少两人生日相同的概率为 1 - P(N, n) / N^n。当人数达到50时,这个概率高达约97%,这与直觉相悖,故称“生日悖论”。
例2:麻将无将概率
标准麻将136张牌,分为34种牌,每种4张。庄家起手摸14张牌,计算没有“将”(两张相同的牌)的概率。
- 基本事件总数:从136张中选14张,即 C(136, 14)。
- 有效事件数:先从34种牌中选出14种,然后在这14种牌中各选1张,即 C(34, 14) * 4^14。
两者相除即可得到概率,计算结果约为0.012,意味着打约80次牌可能会遇到一次起手无将的情况。
例3:装箱问题
将15件商品(12正品,3次品)随机放入3个箱子,每箱5件。求每个箱子恰好有1件次品的概率。
- 基本事件总数:15件商品分成3组(5,5,5),方法数为 15! / (5!5!5!)。
- 有效事件数:先将3件次品分别放入3个箱子(3!种方法),再将12件正品放入3个箱子,每箱4件,方法数为 12! / (4!4!4!)。
概率为两者相乘再除以基本事件总数。此问题可推广为:将N个物品分成k组,各组数量为n1, n2, ..., nk的分组方法总数为 N! / (n1! n2! ... nk!)。
条件概率与贝叶斯公式 🔄
现在我们从单一事件的概率转向事件之间的关系。条件概率描述在已知一个事件发生的条件下,另一个事件发生的概率。
给定事件B发生的条件下,事件A发生的条件概率定义为:P(A|B) = P(A∩B) / P(B)。
利用条件概率和全概率公式,我们可以推导出贝叶斯公式:
P(Bi|A) = [P(A|Bi) * P(Bi)] / Σ_j [P(A|Bj) * P(Bj)]
贝叶斯公式的意义在于,它允许我们根据观察到的结果(A)来更新对原因(Bi)的信念。它在一定程度上“颠倒”了因果,我们只关心事件之间的关联强度,而不预设绝对的因果关系。
实例:校准枪支
有8支步枪,5支校准过,3支未校准。射手用校准过的枪中靶概率为0.8,用未校准的枪中靶概率为0.3。随机选一支枪射击并中靶,问这支枪是校准过的概率。
- 设事件B1:枪已校准,P(B1)=5/8。
- 设事件B2:枪未校准,P(B2)=3/8。
- 设事件A:中靶。P(A|B1)=0.8, P(A|B2)=0.3。
求P(B1|A)。直接代入贝叶斯公式即可计算。
基于对概率中“参数”是否固定的不同看法,形成了频率学派和贝叶斯学派。两者无高低之分,都是认识世界的工具。在大数据时代,频率学派的方法展现了强大的生命力。
常见概率分布 📉
上一节我们讨论了概率的运算规则,本节中我们来看看几种在机器学习中至关重要的概率分布。以下是几个核心分布及其关键特性。
离散分布
- 伯努利分布:单次试验,成功概率为p。期望E=p,方差Var=p(1-p)。
- 二项分布:n次独立伯努利试验的成功次数。期望E=np,方差Var=np(1-p)。
- 泊松分布:描述单位时间内随机事件发生次数的概率分布。其概率质量函数为 P(X=k) = (λk * e{-λ}) / k!。可以通过泰勒展开式 eλ = Σ (λk / k!) 推导出来。期望和方差均为λ。
连续分布
- 均匀分布:在区间[a, b]上概率密度恒定。期望E=(a+b)/2,方差Var=(b-a)²/12。
- 指数分布:具有无记忆性,即 P(X>s+t | X>s) = P(X>t)。常用于描述寿命或等待时间。
- 正态(高斯)分布:最重要的连续分布,概率密度函数为 f(x) = (1 / √(2πσ²)) * exp(-(x-μ)²/(2σ²))。期望为μ,方差为σ²。其多元形式在机器学习中广泛应用。
指数族分布 🌟
一个深刻的见解是,许多常见分布(伯努利、高斯、泊松等)都可以统一写成指数族分布的形式:
P(y; η) = b(y) * exp(η^T * T(y) - a(η))
其中,η是自然参数,T(y)是充分统计量,a(η)是对数配分函数。
例如,伯努利分布可以写成此形式,其连接函数(自然参数η与均值μ的关系函数)恰好是Logistic函数(或称Sigmoid函数):
μ = 1 / (1 + e^{-η})
这个函数的曲线是S型,值域(0,1),单调递增,非常适合表示概率。它也是逻辑回归模型的核心以及神经网络中常用的激活函数。其导数有一个优美的形式:f'(x) = f(x) * (1 - f(x))。
高斯分布也属于指数族。指数族分布的统一形式为构建广义线性模型(GLM)提供了理论基础。
扩展知识:伽马分布与思考题 💡
伽马分布
伽马分布是另一种有用的连续分布,其概率密度函数涉及伽马函数 Γ(α)。伽马函数是阶乘在实数域上的推广,Γ(n) = (n-1)! 对于正整数n成立。一个有趣的结果是 Γ(1/2) = √π。伽马分布在主题模型(如LDA)中有所涉及。
思考题
- 几何概型题:两架飞机分别从机场东西两条跑道独立起飞,各自需要10分钟准备时间,但只能在准备完成后立即起飞。如果它们准备完成的时间点在某个小时内均匀随机,求它们不会相撞(即起飞时间间隔大于2分钟)的概率。
- 采样题:已知一个可以生成随机数0和1的伯努利发生器,其生成1的概率为p(p未知)。如何利用它构造一个生成0和1概率各为1/2的公平伯努利发生器?
推荐阅读与总结 📚
推荐资料
- Andrew Ng的《机器学习》课程讲义:内容通俗,与工程结合紧密。
- 《Pattern Recognition and Machine Learning》(PRML)、《Machine Learning: A Probabilistic Perspective》(MLAPP)、《统计学习方法》:更深入的教材。
- 任何经典的《高等数学》、《数学分析》和《概率论与数理统计》教材,用于巩固基础。


总结
本节课中我们一起学习了概率论的核心基础。我们从概率的定义出发,区分了概率为0与不可能事件的区别。接着,我们探讨了离散与连续随机变量的描述方式,以及累积分布函数的意义。通过生日悖论、麻将等实例,我们实践了古典概型的计算。然后,我们深入学习了条件概率和强大的贝叶斯公式,并了解了频率学派与贝叶斯学派的区别。我们回顾了几种关键的概率分布(伯努利、二项、泊松、均匀、指数、正态),并揭示了它们都可以纳入指数族分布的统一框架,其中Logistic函数的导出尤为关键。最后,我们简要介绍了伽马分布并留下了两个值得深思的问题。掌握这些概率论知识,将为后续学习机器学习算法打下坚实的数学基础。
人工智能—机器学习中的数学(七月在线出品) - P5:极大似然估计 📊
在本节课中,我们将要学习机器学习中一个非常重要的参数估计方法——极大似然估计。我们将从贝叶斯公式出发,理解其核心思想,并通过具体的例子来掌握其应用方法。
从贝叶斯公式到极大似然估计 🔄
上一节我们介绍了参数估计的基本概念,本节中我们来看看如何通过贝叶斯公式引出极大似然估计的思想。
我们考察这样一个事情。假定我们回想贝叶斯公式。在给定条件 D 的时候,计算事件 A 的概率。这个公式我们已经讲过了。
现在我们把 D 看作是已知的样本,这是条件。那么就是说给定了样本之后,计算分布的参数。我们这样来看贝叶斯公式。那就是在给定样本的情况下,看看哪一组参数取得的概率最大。
我们就认为哪一组参数是最有可能的,最应该被我们估计的那个值。根据贝叶斯公式,既然是这个东西,我们来观察一下这个事情。假定对于样本 D 而言,它可能得出一个结论 A1,可能得出一个结论 A2,最终可能得到 AN。假定可能有 N 种可能。也就是我想估计一下,在样本 D 这个数据给定的时候,看看哪一个 Ai 它的概率最大。
那么我就是想计算一下这个值的最大值是什么。它取最大值的时候,看哪个 i 能够符合我的最大值。根据贝叶斯公式,这个东西直接带入贝叶斯公式,就是它。注意,上面这个东西跟 Ai 是有关的。底下这个 P(D) 是我们样本本身发生的概率。样本都给你了,样本发生的概率是一个常数。所以对它而言是个常数,我们不要这个分母了。只剩下了这样一个东西。
如果我们进一步假定,原始的在没有给定数据之前,结论 A1, A2, ..., AN 他们是等概率出现的,或者是近似等概率出现的,大体上差不多。也就是没有任何相应信息的情况下,P(Ai) 大体是相等的,我们把它再给扔了。
那么把它扔了之后,就得到这样一个东西。这是什么呢?我们把中间这个过程全部忽略掉,左边是这个,右边是这个,也就是写成第二行的式子。大家发现问题了没有?我们本来要做的事情是看一下在样本给定的时候,看看哪一个结论是最大的。但是我们在实践上,在社会上都会发生的一个思想是,我们反过来看,看看哪一个参数能够使得这个数据最大可能地发生。我们就把那样子的一个参数看作是我们最有可能估计的值。
这个东西如果给它讲故事的话,相当于颠倒黑白,互为因果,把因和果给倒过来了。倒过来做的事情对不对呢?但其实我们利用贝叶斯公式真的是可以解释的。有些时候颠倒因果是有道理的,因为贝叶斯公式是成立的,我们只能承认它。这就是关于这个事情。那我们就利用这一点,不再去看哪一个数据给定的时候,哪个参数可能值最大,而是看一下哪一个参数能够使得这个数据产生的概率最大。这个就是极大似然估计的基本想法。
极大似然估计的数学定义 📐
上一节我们介绍了极大似然估计的思想来源,本节中我们来看看它的具体数学定义。
我们假定一个总体的分布是长这样子的,θ 是我们不知道但想知道的一个参数。X 是我们的基本事件,我们的研究对象。
这里面的 X1, X2, ..., Xn 是我们通过这个总体采样得到的一些样本,n 个样本。首先这 n 个样本是来自于同样一个分布的。假定他们是独立的,也就是独立同分布的。
既然他们是独立的,对于 X1 而言,它发生的概率就是给定参数时 X1 的概率。X2 就是 X2 的概率。如果他们是独立的,独立意味着如果 X 和 Y 的联合概率可以写成各自概率的乘积。我们这里不是 X 和 Y,而是 X1, X2, ..., Xn,这里边就是 Xi 的值各自乘起来,就是这样一个式子。无非就是把它做了 n 个变化而已。这个参数我们假定有 k 个。
这个东西是它就写成这样一个东西了。这是什么?这个是我们样本发生的概率。第一个样本拿到的概率是这个,第二个样本拿到的概率是这个,到第 n 个也是这个,乘起来。这其实是我们拿到这个样本的概率。既然发生了这么一个事情,那就是像那个样子发生了。把这个东西说得文雅一点,就是“似然”。像什么什么的样子,似然嘛。因此这个东西是个似然函数,记作 L。它表示的是我们样本发生的概率。
另外我们可以想象到的是,这里边拿到手的这 n 个样本,其实已经是放在这里我们能够看见的东西了。我们看不见的东西是 θ1, θ2, ..., θk。我们转一个视角来看,把这样一个似然函数看作是关于未知参数 θ1 到 θk 的一个函数也是可以的。因为样本虽然用 X 表示,它已经是给我们的了,它已经采样得到了,θ 是未知的。所以这个似然函数我们看作这个东西关于 θ 的函数,这是一个似然。
下面的工作就是我们去求某一个 θ,使得这个似然函数能够概率取最大。那就是我们的极大似然估计。这个也就是刚才说的,看看哪一个参数能够使得这个 D 概率最大,不就是这个事情吗?
在实践当中,我们由于求导的需要,往往是对似然函数先取对数,得到对数似然函数。对这个对数似然函数求导数,然后得到若干个方程,然后让它求驻点,往往求的驻点就是极大值。就这么做法。所以我们先对它取对数,取对数之后,这个大 L 变成了这个小 l。我们一般用小 l 来表达,这个东西是它。
然后分别对 θ1, θ2, ..., θk 求偏导,得到这个东西,这其实是个方程组,解方程就是了。
核心公式:似然函数与对数似然函数
- 似然函数:
L(θ) = ∏_{i=1}^{n} P(x_i | θ) - 对数似然函数:
l(θ) = log L(θ) = ∑_{i=1}^{n} log P(x_i | θ)
应用示例:抛硬币实验 🪙
上一节我们定义了极大似然估计的数学形式,本节中我们通过一个简单的例子来看看它是如何工作的。
我们已经说了,本质就是去找出与样本的分布最接近的那个分布值。举个例子,比方说我们抛了10次硬币。如果大家没听懂,继续听例子就懂了。
我们假设抛个硬币,抛10次。第一次是个正,第二次又是个正,第三次是个反,第四次是个正等等,抛了10次。我们拿到了这10次的抛币结果。我们现在来假设 p 是每次抛硬币结果为正的概率。我们现在想来估计一下这个 p 等于几。这个 p 是我们未知的,其实就是那个二项分布的唯一那个参数。我们想估计这个 p 等于几,怎么做呢?
第一次是正,这个正它发生的概率就是 p。第二次是正,发生的概率就是 p。第三次是反,所以它的发生概率是 1-p。第四次是正,那就是 p。每一个都这么写出来,其实就是一个 p 的7次方乘以 (1-p) 的3次方。这个东西相当于是关于 p 的一个未知的函数,这个函数记作 L(p)。其实这个东西就是那个大 L。我们现在想来求这个大 L 的极大值是什么?谁能够使得大 L 取极大值,谁就是我要估计的那个 p 的最优的那个值。p 的那个最优值其实是等于0.7的。请问怎么做的?
我们现在把这个东西做一个理论化的说法,到底为什么能够算出0.7呢?其实很简单。
以下是抛硬币实验的极大似然估计推导步骤:
- 定义问题:抛硬币进行了
N次实验,有n次朝上,有N-n次朝下。假定朝上的概率是p。 - 写出似然函数:给定
p的时候,观测到n次朝上的概率(似然函数)是:L(p) = C(N, n) * p^n * (1-p)^(N-n)。其中C(N, n)是组合数。 - 取对数得到对数似然:
l(p) = log L(p) = log C(N, n) + n*log(p) + (N-n)*log(1-p)。记作关于p的函数h(p)。 - 对参数求导:对
l(p)关于p求导:dl/dp = n/p - (N-n)/(1-p)。 - 令导数为零求解:令
dl/dp = 0,解得p = n / N。
这个结论跟我们的直观想象没有任何区别。你进行 N 次实验,有 n 次朝上,你不说这一套理论,问个小学生,他也会用 n/N 来估计朝上的概率。我们现在利用极大似然估计得到了一个结论,并且这个结论跟我们的实际是没有矛盾的。说明在一定意义上,极大似然估计是对的。我们不能说它一定对,总之,我们通过这个假定,给出了一个最终的结论,这个结论跟我们的直观是相符的,是能够解释的。所以我们整个的推理没有发生大的偏差。
应用示例:正态分布的参数估计 📈
上一节我们通过抛硬币的例子熟悉了极大似然估计的流程,本节中我们来看一个更常见的例子——估计正态分布的参数。
给你 X1, X2, ..., Xn 这 n 个样本,那么假定它来自于高斯分布,你能够估计这个高斯分布的均值和方差吗?还记得之前我们用矩估计给出了期望跟方差的结论。我们现在用极大似然估计再来算算到底结论是什么。
以下是正态分布参数估计的步骤:
- 写出概率密度函数:高斯分布的概率密度函数是:
f(x|μ, σ^2) = (1/√(2πσ^2)) * exp(-(x-μ)^2/(2σ^2))。 - 构建似然函数:把样本
xi带入,让i从1到n乘起来,得到似然函数:L(μ, σ^2) = ∏_{i=1}^{n} f(x_i | μ, σ^2)。 - 取对数得到对数似然:
l(μ, σ^2) = log L(μ, σ^2) = ∑_{i=1}^{n} log f(x_i | μ, σ^2) = -n/2 * log(2πσ^2) - 1/(2σ^2) * ∑_{i=1}^{n} (x_i - μ)^2。 - 分别对参数求偏导:
- 对
μ求偏导:∂l/∂μ = (1/σ^2) * ∑_{i=1}^{n} (x_i - μ) - 对
σ^2求偏导:∂l/∂(σ^2) = -n/(2σ^2) + 1/(2σ^4) * ∑_{i=1}^{n} (x_i - μ)^2
- 对
- 令偏导为零并求解:
- 令
∂l/∂μ = 0,解得μ = (1/n) * ∑_{i=1}^{n} x_i。 - 令
∂l/∂(σ^2) = 0,并将μ的估计值代入,解得σ^2 = (1/n) * ∑_{i=1}^{n} (x_i - μ)^2。
- 令
这个结论很漂亮。这是什么?这就是我们样本的均值,就是总体的均值的估计。样本的(伪)方差,就是总体的方差的估计。这个结论跟我们刚才矩估计的那个结论是完全一致的,没有区别。二者虽然更多的统计学家是把方差定义做 1/(n-1) 的,但是这个极大似然估计和矩估计,他们都指向了一个除以 n 的结论。所以说除 n 是有一定的意义的,不是就真错了。除 n 是有意义的。我有时候把它叫伪方差。因为经典统计还是除 n-1。这个结论我们后面在谈到EM算法、高斯混合模型时仍然会用到。
极大似然估计的思考与扩展 💡
上一节我们完成了对正态分布参数的估计,本节中我们通过一个例子来思考极大似然估计的局限性,并引出其扩展。
既然你刚才给了我这个极大似然估计结论,我就跟上这个结论给你做一点点的思考。我到底看看你到底跟实际符不符合,我们倒一个例子出来。
比如说我们的校门口去统计一段时间出入门口的男生跟女生的数目。我们记作 N_boy 跟 N_girl 两个记号。用这个东西来去估算男女生的比例。根据刚才我们极大似然估计抛硬币那个例子,那么就是一个是朝上,一个是朝下嘛,所有的值是 N = N_boy + N_girl。所以这个结论很容易求,一个是 boy 的概率 P_boy = N_boy / N,一个是 girl 的概率 P_girl = N_girl / N。
我现在统计了一段时间,发现出来了有4位女生跟1位男生。带入这个公式,我说该校的女生比例是 4/5=80%。这样对吗?很显然,这样做是不合理的。因为一个学校的人数是很多的,但是你只拿到了4个女生跟1个男生,你就得到一个80%的结论,似乎有点不太地道。
那我们可不可以做一点点的修正呢?比如说我这里边让分子加上一个数,让分母也加上一个数。比如让 N_boy 和 N_girl 各自加上 5,那么 P_boy = (1+5)/(5+10)=6/15=40%,P_girl = (4+5)/(5+10)=9/15=60%。似乎比刚才的80%更靠谱。我们不能说40%跟60%也是对的,但是似乎比这个更合理一点。
现在问题就是:
- 你为什么知道要加上一个数呢?这个加有道理吗?
- 你要加的话,你应该加几呢?
先回答第二个问题。如果要是加,我们承认它的话,加几这个东西是一个超参数,是无法通过我们的样本就能够估计出来的。我们需要做一个交叉验证才可以。这个加几,我是自己把它随便试出来的一个数,我觉得这样还不错,然后就给出了一个这个东西。没有什么更多的结论,这个例子是我构造的。
然后这是第一个。第二个呢就是加这个东西,其实意味着我们没有完全取信于极大似然估计的这个结论,而是做了一个变化。这个变化来自于哪儿呢?来自于前边,我们把它再倒回去,看看我们最开始讲极大似然估计到底是怎么说的。
我们说,这个里边如果在假定 P(Ai) 的概率近似相等的时候,就可以推导出来极大似然估计是正确的。那如果这个 P(Ai) 近似不相等呢?如果 P(Ai) 服从某一个分布呢?比如说后面这个例子,我如果假定它的参数是服从伽马分布的,或者是多元的就服从狄利克雷分布的,它就可以加上一个数来去得到结论了。这个就是极大似然估计,把这个东西加上一个先验,就得到了极大后验概率估计的原因。这是一个非常有趣的概念。如果大家了解过LDA主题模型应该清楚了,那个LDA其实就这么干的,它就是加了一个超参数。
所以,我一直在强调,我们的前几次数学课,真的不是复习数学而已。只是我们用机器学习的眼光来看待数学,来看看它到底里边有什么跟我们相关的事情。
总结 🎯
本节课中我们一起学习了极大似然估计。
我们首先从贝叶斯公式出发,理解了“哪个参数最可能产生观测到的数据”这一核心思想,并将其形式化为求似然函数最大值的问题。我们学习了似然函数和对数似然函数的定义。
接着,我们通过抛硬币和正态分布参数估计两个经典例子,一步步演示了如何应用极大似然估计:
- 写出似然函数
L(θ)。 - 取对数简化计算,得到
l(θ)。 - 对参数
θ求(偏)导。 - 令导数为零,解出参数的估计值。
最后,我们通过一个校门口统计性别的简单例子,探讨了极大似然估计在样本量很小时可能不够稳健,并引出了可以通过加入先验信息(如狄利克雷分布)进行平滑的极大后验概率估计,这为后续学习更复杂的模型(如LDA)打下了基础。


极大似然估计是连接概率论与统计推断的桥梁,是机器学习中许多模型(如逻辑回归、高斯混合模型)进行参数学习的理论基础,务必掌握其思想与计算方法。

人工智能—机器学习中的数学(七月在线出品) - P6:矩估计

📚 课程概述
在本节课中,我们将要学习参数估计的一种重要方法——矩估计。我们将理解总体与样本的区别,学习如何利用样本数据计算统计量,并最终通过建立样本矩与总体矩的等式关系来估计未知的总体参数。
🎼 样本统计量
上一节我们介绍了总体的矩(如期望、方差),本节中我们来看看如何从样本数据中计算类似的统计量。
假设我们有一组样本数据:X1, X2, ..., XN。我们可以基于这些样本值计算以下统计量:
- 样本均值:将所有样本值相加后除以样本数量
N。- 公式:
样本均值 = (X1 + X2 + ... + XN) / N
- 公式:
- 样本方差:将每个样本值与样本均值的差进行平方,求和后再除以
N-1。- 公式:
样本方差 = [(X1 - 均值)^2 + (X2 - 均值)^2 + ... + (XN - 均值)^2] / (N-1)
- 公式:
核心概念区分:
- 总体矩:基于已知的概率分布(概率密度函数)计算出的理论值。
- 样本统计量:基于实际观测到的样本数据计算出的数值。
关于样本方差除以 N-1 而非 N,是为了保证估计的无偏性。在实践中,有时也会使用除以 N 的版本,这被称为伪方差。
📊 样本矩的定义
仿照总体矩的定义,我们可以定义样本的矩。
以下是样本矩的定义:
- 样本的K阶原点矩:样本值的K次幂之和除以
N。- 公式:
A_k = (X1^k + X2^k + ... + XN^k) / N
- 公式:
- 样本的K阶中心矩:样本值减去样本均值后的K次幂之和除以
N。- 公式:
B_k = [(X1 - 均值)^k + (X2 - 均值)^k + ... + (XN - 均值)^k] / N
- 公式:
根据定义:
- 一阶原点矩
A_1就是样本均值。 - 二阶中心矩
B_2(若除以N)就是样本伪方差;若除以N-1则是样本方差。
🔗 矩估计的核心思想
现在,我们来看看如何利用样本矩来估计总体参数。这是矩估计方法的核心。
我们假设总体服从一个由参数 θ 决定的分布。θ 是未知的,但客观存在。例如:
- 高斯分布:
θ = (μ, σ) - 泊松分布:
θ = λ - 均匀分布:
θ = (a, b)
我们从总体中独立同分布地抽取了样本 X1, X2, ..., XN。
矩估计的思路:
- 利用样本,我们可以计算出各阶样本矩(如
A_1,A_2,B_2等),这些是已知的数值。 - 总体的各阶矩(如期望
E(X)、方差D(X)等)是未知参数θ的函数。 - 令样本矩等于对应的总体矩,建立关于未知参数
θ的方程组。 - 解这个方程组,即可得到参数
θ的估计值θ_hat。
简单来说:用样本计算出的“平均特征”(矩)去匹配理论分布的“平均特征”,从而反推出分布的参数。
🧮 矩估计的应用实例
上一节我们介绍了矩估计的原理,本节中我们通过具体例子来看看如何应用。
实例一:估计正态分布的参数
假设总体服从正态分布 N(μ, σ^2),参数 μ 和 σ^2 未知。
- 总体一阶矩(期望):
E(X) = μ - 总体二阶原点矩:
E(X^2) = D(X) + [E(X)]^2 = σ^2 + μ^2
根据矩估计法,令样本矩等于总体矩:
样本均值 A_1 = μ样本二阶原点矩 A_2 = σ^2 + μ^2
解此方程组,得到参数的矩估计量:
μ_hat = A_1 = 样本均值σ^2_hat = A_2 - (A_1)^2 = 样本二阶原点矩 - (样本均值)^2
可以发现,σ^2_hat 正是之前定义的样本伪方差。
矩估计结论:对于任何分布,均可用样本均值估计总体期望,用样本伪方差估计总体方差。

实例二:估计均匀分布的参数
假设总体服从区间 [a, b] 上的均匀分布,参数 a 和 b 未知。
已知均匀分布的性质:
- 总体期望:
E(X) = (a + b) / 2 - 总体方差:
D(X) = (b - a)^2 / 12
令样本矩等于总体矩:
样本均值 = (a + b) / 2样本伪方差 = (b - a)^2 / 12
设 μ_hat 为样本均值,σ_hat^2 为样本伪方差。解方程组可得:
a_hat = μ_hat - √(3 * σ_hat^2)b_hat = μ_hat + √(3 * σ_hat^2)
这样,我们就通过样本数据估计出了均匀分布的区间范围 [a_hat, b_hat]。

📝 课程总结
本节课中我们一起学习了参数估计的矩估计法。


我们首先区分了总体矩与样本统计量,然后定义了样本原点矩和样本中心矩。矩估计的核心思想是建立样本矩等于总体矩的方程,通过求解方程组来估计未知的总体参数。我们以正态分布和均匀分布为例,演示了矩估计的具体应用步骤。矩估计原理直观、计算简单,是参数估计的一种基本而重要的方法。
人工智能—机器学习中的数学(七月在线出品) - P7:矩阵基础综述 📊
在本节课中,我们将要学习矩阵和线性代数的一些核心基础知识。理解这些概念对于后续学习图像处理、机器学习等复杂算法至关重要。我们将从矩阵的基本概念出发,逐步深入到线性方程组、子空间以及特征分解,旨在帮助大家建立对矩阵的深刻理解,而不仅仅是停留在表面计算。
矩阵的本质与视角 🔍
上一节我们介绍了课程的目标和重要性,本节中我们来看看矩阵到底是什么。矩阵不仅仅是数字的排列,它可以从多个层面进行理解。
一个矩阵,例如一个4x3的矩阵,通常被简单地视为用方括号括起来的一组数字。然而,这种理解是浅显的。更深入地,我们可以通过线性方程组 AX = B 来理解矩阵。
对于这个方程组,存在三种不同层次的理解:
- 行视角(几何交点):将每个方程视为一条直线(或平面),方程组的解就是这些直线(或平面)的交点。这是初中数学的层面。
- 列视角(线性组合):将矩阵
A的每一列视为一个向量。AX的结果B就是这些列向量的线性组合,组合系数由向量X提供。这是理解矩阵乘法的关键视角之一。 - 子空间视角:所有可能的线性组合
AX(即所有可能的B)构成了一个向量空间,称为矩阵A的列空间。这是最高层次的理解,将矩阵与向量空间理论联系起来。
矩阵乘法的四种表示形式 ✖️
理解了矩阵的多种视角后,我们来看看矩阵乘法的具体表示。矩阵乘法 C = A * B 有四种等价的表示形式,这有助于我们从不同角度理解运算的本质。
以下是矩阵乘法的四种表示形式:
- 内积表示:
C的第i行第j列元素c_ij,等于A的第i行与B的第j列的内积。这是教科书中最常见的形式。 - 列表示:
C的每一列,都是矩阵A的所有列以B的对应列为系数的线性组合。即C的第j列 =A * (B的第j列)。 - 行表示:
C的每一行,都是矩阵B的所有行以A的对应行为系数的线性组合。即C的第i行 =(A的第i行) * B。 - 外积表示:
C可以表示为一系列秩为1的矩阵之和。具体地,C = Σ (A的第k列) * (B的第k行转置)。这揭示了矩阵乘法可以分解为多个简单矩阵的和。
线性代数基础概念回顾 📐
矩阵运算建立在坚实的线性代数基础之上。本节我们将回顾向量组的线性相关性、张成空间等核心概念,这些是理解矩阵子空间的基石。
一组向量 {a1, a2, ..., an} 是线性无关的,当且仅当方程 c1*a1 + c2*a2 + ... + cn*an = 0 的唯一解是所有系数 c1, c2, ..., cn 都为零。这意味着没有一个向量可以表示为其他向量的线性组合。反之,则是线性相关。
向量组 {a1, a2, ..., an} 的张成空间,记作 Span{a1, ..., an},是指由这些向量的所有可能线性组合构成的集合。这个集合满足子空间的两个条件:
- 对加法封闭:如果
u和v在子空间中,则u + v也在。 - 对数乘封闭:如果
u在子空间中,c是任意标量,则c * u也在。
任何子空间都必须包含零向量。
矩阵的四个基本子空间 🧩
从子空间的角度理解矩阵是线性代数的精髓。任何一个 m x n 的矩阵 A 都关联着四个重要的子空间,它们揭示了矩阵 A 的结构和性质。
以下是矩阵 A 的四个基本子空间:
- 列空间:记作
C(A)。由矩阵A的所有列向量张成的子空间,是R^m的子空间。AX = b有解,当且仅当向量b位于A的列空间中。 - 零空间:记作
N(A)。由所有满足AX = 0的解向量x构成的子空间,是R^n的子空间。 - 行空间:记作
C(A^T)。由矩阵A的所有行向量(即A^T的列向量)张成的子空间,是R^n的子空间。 - 左零空间:记作
N(A^T)。由所有满足A^T y = 0的解向量y构成的子空间,是R^m的子空间。
这四个子空间之间存在优美的正交关系:行空间与零空间在 R^n 中正交,列空间与左零空间在 R^m 中正交。并且它们的维数满足:
dim(C(A)) = dim(C(A^T)) = rank(A)(秩)dim(N(A)) = n - rank(A)dim(N(A^T)) = m - rank(A)
矩阵的秩与线性方程组的解 🧮
矩阵的秩是连接子空间维数与方程组解的存在性的桥梁。理解了秩,就能清晰地判断 AX = b 的解的情况。
矩阵 A 的秩是其列(或行)向量中最大线性无关组的向量个数,也就是其列空间的维数。矩阵的秩决定了线性方程组 AX = b 的解的性质:
以下是不同情况下 AX = b 的解的总结:
- 方阵且满秩:当
A是n x n方阵且rank(A) = n时,A可逆。对于任意b,方程有唯一解x = A^{-1}b。 - “胖”矩阵:当
A是m x n矩阵且rank(A) = m < n(行满秩)时,方程AX = b要么无解,要么有无穷多解。解存在的条件是b必须位于A的列空间内。 - “瘦”矩阵:当
A是m x n矩阵且rank(A) = n < m(列满秩)时,方程AX = b可能有唯一解,也可能无解。当b不在列空间内时无解,此时引入最小二乘法,寻找一个x使得||Ax - b||^2最小,这对应于将b投影到列空间上。
特征值与特征分解 🔬
特征值与特征分解是分析矩阵,尤其是对称矩阵的强大工具。它不仅在理论分析中重要,在计算矩阵高次幂等实际问题中也非常高效。
对于方阵 A,如果存在标量 λ 和非零向量 v,使得 Av = λv 成立,则称 λ 为 A 的特征值,v 为对应的特征向量。几何上,这意味着矩阵 A 对向量 v 的作用仅仅是缩放(系数为 λ),而不改变其方向。
特征值通过求解特征方程 det(A - λI) = 0 得到。如果 A 是 n x n 矩阵且有 n 个线性无关的特征向量 v1, v2, ..., vn,对应的特征值为 λ1, λ2, ..., λn,那么 A 可以进行特征分解(也称对角化):
A = V Λ V^{-1}
其中 V 是以特征向量为列的矩阵,Λ 是以特征值为对角元素的对角矩阵。
这个分解的威力在于,例如计算 A 的 k 次幂变得非常简单:A^k = V Λ^k V^{-1},而 Λ^k 只需将对角元素各自取 k 次幂。
特别地,对于实对称矩阵 A,其特征值都是实数,且不同特征值对应的特征向量相互正交。因此,其特征分解可以写成更优美的形式:
A = U Λ U^T
其中 U 是正交矩阵(U^T U = I),其列是 A 的单位正交特征向量。此时,矩阵的列空间和零空间可以直接与 U 的列向量联系起来:列空间由对应非零特征值的特征向量张成,零空间由对应零特征值的特征向量张成。
总结 📝
本节课中我们一起学习了矩阵与线性代数的核心基础。我们从矩阵的三种理解视角(行、列、子空间)出发,深入探讨了矩阵乘法的四种表示形式。我们重点回顾并构建了以四个基本子空间为核心的理论框架,理解了列空间、零空间、行空间和左零空间的定义、相互关系及其维数定理。在此基础上,我们分析了矩阵的秩如何决定线性方程组 AX = b 解的存在性与唯一性,并引出了最小二乘的概念。最后,我们学习了特征值与特征分解,特别是实对称矩阵的优美性质,并将其与子空间理论联系起来。


掌握这些基础概念,是从更高维度理解后续机器学习、图像处理中复杂矩阵运算和模型的关键。希望大家能通过子空间的观点重新审视矩阵,将其视为构建向量空间的工具,而不仅仅是数字的表格。

人工智能—机器学习中的数学(七月在线出品) - P8:偏度和峰度 📊


在本节课中,我们将要学习两个重要的统计量:偏度和峰度。它们是期望和方差的延伸,能够帮助我们更深入地理解数据分布的形状特征,例如分布的不对称性和陡峭程度。

矩的概念 📐

上一节我们介绍了期望和方差,本节中我们来看看更一般的“矩”的概念。矩的概念来源于物理学中的力矩,在统计学中,它被用来描述随机变量的分布特征。
对于一个随机变量X,我们定义两种矩:
- K阶原点矩:定义为随机变量X的K次幂的期望,即 E[X^K]。
- K阶中心矩:定义为随机变量X减去其期望后的K次幂的期望,即 E[(X - E)^K]。

从这个定义出发,我们可以发现:
- 期望(一阶矩)其实就是 K=1时的一阶原点矩。
- 方差(二阶矩)其实就是 K=2时的二阶中心矩。
因此,期望和方差是矩的特例。我们还可以构造更高阶的矩来刻画分布的其他特性。

偏度:衡量分布的不对称性 ⚖️
偏度用于衡量概率分布的不对称性,即数据围绕其均值(期望)的对称程度。

以下是偏度的几种情况:
- 偏度为负(左偏):分布的左尾较长,数据集中在右侧。均值通常位于众数左侧。
- 偏度为零:分布大致对称,数据均匀分布在均值两侧(但不一定完全对称)。
- 偏度为正(右偏):分布的右尾较长,数据集中在左侧。均值通常位于众数右侧。

偏度的计算公式如下,它基于三阶中心矩,并进行了标准化以消除量纲影响:
偏度 = E[(X - μ)3] / σ3
其中,μ是期望,σ是标准差。
在实践中,为了计算方便,我们常使用以下等价公式:
偏度 = (E[X3] - 3μE[X2] + 2μ3) / σ3

峰度:衡量分布的陡峭程度 🏔️


峰度用于衡量概率分布的陡峭程度,即数据在均值附近的集中程度和尾部的厚度。
以下是峰度的几种情况(通常指超额峰度,即原始峰度减3):
- 峰度 > 0(尖峰):分布比正态分布更陡峭,尾部更厚。
- 峰度 = 0:分布的陡峭程度与正态分布相同。
- 峰度 < 0(低峰):分布比正态分布更平缓,尾部更薄。
峰度的计算公式基于四阶中心矩:
峰度 = E[(X - μ)4] / σ4
由于正态分布的峰度值为3,为了便于比较,我们常使用“超额峰度”:
超额峰度 = E[(X - μ)4] / σ4 - 3
这样,正态分布的超额峰度就为0。
实例分析:不同分布的偏度与峰度 🔬
为了直观理解偏度和峰度,我们通过代码生成几种不同的数据分布并进行分析。

以下是生成和分析数据的核心步骤:
- 生成数据:标准正态分布、方差放大的正态分布、截断的正态分布(取右半部分)、均匀分布。
- 计算统计量:分别计算每个数据集的均值、标准差、偏度和峰度。
- 可视化比较:绘制各数据分布的直方图,并标注计算出的统计量。
通过对比分析,我们可以观察到:
- 标准正态分布及其方差放大版:偏度接近0,峰度接近3(超额峰度接近0),说明它们是对称且陡峭程度与正态分布一致的。
- 截断的正态分布(右半部分):偏度为正,峰度大于3,说明分布右偏且更加陡峭。
- 均匀分布:偏度接近0,峰度远小于3(超额峰度为负),说明分布对称但非常平缓。
这个实验表明,偏度和峰度需要结合其他统计量(如方差)一起分析,才能全面描述数据的分布形态。例如,方差不同的两个正态分布可能有相同的峰度。


切比雪夫不等式:方差的实际意义 📉
最后,我们探讨一个与方差紧密相关的重要不等式——切比雪夫不等式。它揭示了方差的物理意义:方差越小,随机变量的取值越集中在期望附近。
对于任意随机变量X(期望为μ,方差为σ²)和任意正数ε,切比雪夫不等式指出:
P(|X - μ| ≥ ε) ≤ σ² / ε²
其等价形式为:
P(|X - μ| < ε) ≥ 1 - σ² / ε²
这个不等式说明,X的取值落在期望μ附近ε范围内的概率至少为 1 - σ²/ε²。方差σ²越小,这个概率的下界就越大,意味着X越可能集中在期望附近。

总结 📝

本节课中我们一起学习了:
- 矩的概念:理解了K阶原点矩和中心矩的定义,并认识到期望和方差是矩的特例。
- 偏度:学习了如何用三阶中心矩衡量分布的不对称性(左偏、对称、右偏)。
- 峰度:学习了如何用四阶中心矩衡量分布的陡峭程度(尖峰、常峰、低峰),并引入了超额峰度的概念。
- 实例分析:通过编程实践,观察了不同分布下偏度和峰度的表现,加深了对这两个统计量的理解。
- 切比雪夫不等式:了解了该不等式的含义,它从概率上严格阐述了方差越小,数据越集中在期望附近的这一直观认识。

掌握偏度和峰度,能帮助我们在进行数据分析时,超越均值和方差,对数据分布的形状有更精细的把握。
人工智能—机器学习中的数学(七月在线出品) - P9:期望和方差 📊
在本节课中,我们将学习概率论中的两个核心概念:期望与方差。我们将从定义出发,理解其直观含义,并通过具体的面试题来应用这些知识,最后总结方差与标准差的关系。
期望的定义与性质 📈
上一节我们介绍了概率分布的基本概念,本节中我们来看看如何描述一个随机变量的“平均水平”,这就是期望。
期望是概率加权下的平均值。对于一个离散型随机变量 (X),其期望 (E(X)) 定义为:
[
E(X) = \sum_ x_i p_i
]
其中 (x_i) 是 (X) 可能的取值,(p_i) 是 (X) 取值为 (x_i) 的概率。
对于一个连续型随机变量,其期望定义为:
[
E(X) = \int_{-\infty}^{\infty} x f(x) dx
]
其中 (f(x)) 是 (X) 的概率密度函数。加和与积分在本质上是相同的操作。
我们可以将期望理解为用概率作为权重的均值。如果去掉概率权重 (p_i),对所有取值简单平均,就是普通的算术平均;加上 (p_i),就是以概率为权重的加权平均。
根据期望的定义,我们可以推导出一些无条件成立的公式。
以下是期望的几个基本性质:
- 线性性质:若 (a) 和 (b) 是常数,则 (E(aX + b) = aE(X) + b)。证明可以直接由定义得出。
- 可加性:对于任意两个随机变量 (X) 和 (Y),有 (E(X + Y) = E(X) + E(Y))。这个性质无条件成立,无论 (X) 与 (Y) 是否独立。
- 独立变量的乘积:如果随机变量 (X) 和 (Y) 相互独立,那么有 (E(XY) = E(X)E(Y))。需要注意的是,这个性质是有条件成立的(需要独立性)。由 (E(XY) = E(X)E(Y)) 可以推出 (X) 与 (Y) 不相关,但反之不成立,即“不相关”不能推出“独立”。
期望的应用:一道面试题解析 💡
上一节我们介绍了期望的基本性质,本节中我们来看看如何利用这些性质解决一个实际问题。
题目如下:给定100个数,分别是1, 2, 3, ..., 99 和 2015。从这100个数中任意选取若干个数(可以全选、全不选或选一部分),对选出的所有数进行按位异或(XOR)操作,求这个异或结果的期望值。
首先,我们需要理解“异或”操作。对于单个比特位,异或的规则是:相同为0,不同为1。也可以理解为不考虑进位的二进制加法。一个关键特性是:奇数个1进行异或,结果为1;偶数个1进行异或,结果为0。
解题思路是按位计算。因为期望具有可加性,整个数的异或期望等于其每一位的异或期望之和。2015是这100个数中最大的,其二进制形式决定了我们只需要考虑11个比特位(因为 (2^{10}=1024, 2^{11}=2048))。
我们考察第 (i) 位(记该位的值为随机变量 (X_i))。在这100个数的第 (i) 位上,假设有 (N) 个1和 (M) 个0。题目条件保证了对于这11位中的任何一位,(N \ge 1)(即至少有一个1)。
对于某一次抽样,我们在第 (i) 位上抽到了 (k) 个1。根据异或特性,只有当 (k) 为奇数时,该位的异或结果 (X_i) 才为1。因此,计算 (X_i) 的期望,等价于计算抽到奇数个1的概率。
以下是计算该概率的过程:
- 从 (N) 个1中抽取 (k) 个((k) 为奇数),取法有 (\sum_{k\ odd} C_N^k) 种。
- 对于 (M) 个0,可以任意选取(每个0都可选可不选),因此有 (2^M) 种取法。
- 总的可能选取方式(从 (N+M) 个数中任选若干)有 (2^{N+M}) 种。
- 因此,抽到奇数个1的概率为:(\frac{(\sum_{k\ odd} C_Nk) \cdot 2M}{2^{N+M}} = \frac{\sum_{k\ odd} C_Nk}{2N})。
根据二项式定理的性质,奇数项系数之和等于偶数项系数之和,都等于 (2^)。因此,上述概率为:
[
\frac{2^}{2^N} = \frac{1}{2}
]
这意味着,对于任何 (N \ge 1) 的位,其异或结果为1的概率恒为 (1/2),即 (E(X_i) = 1/2)。
现在计算总的期望 (E(X))。设第 (i) 位的权重为 (2i),则:
[
E(X) = E(\sum_{10} 2i X_i) = \sum_{10} 2i E(X_i) = \sum_{10} 2i \cdot \frac{1}{2} = \frac{1}{2} \sum_{10} 2i
]
这是一个等比数列求和,其和为 (2{11} - 1 = 2047)。因此,最终期望为:
[
E(X) = \frac{2047}{2} = 1023.5
]
通过程序模拟抽样验证,实验结果与理论值高度吻合。
思考:如果将题目中的2015换成1024(二进制为10000000000),期望值会改变吗?答案是会的。因为对于最高位(第10位),1024提供了唯一的1,即 (N=1),公式仍然适用,期望为 (1/2)。但对于其他某些低位,可能所有数在该位上都是0(即 (N=0)),那么抽到奇数个1的概率为0,该位的期望就是0。因此需要重新逐位计算。
方差与标准差 📊
上一节我们利用期望解决了问题,本节中我们来看看如何度量随机变量的波动程度,这就是方差。
方差衡量的是随机变量取值与其期望的偏离程度的平均值。随机变量 (X) 的方差 (Var(X)) 定义为:
[
Var(X) = E[(X - E(X))^2]
]
即“偏差平方”的期望。
根据期望的性质,可以推导出方差的一个常用计算公式:
[
Var(X) = E(X^2) - [E(X)]^2
]
一个随机变量的方差等于其平方的期望减去其期望的平方。
方差具有以下性质:
- 常数的方差:常数的方差为0。
- 缩放性质:(Var(aX + b) = a2 Var(X)),其中 (a, b) 为常数。方差对平移不敏感(加 (b) 无影响),对缩放敏感(乘 (a) 则方差变为 (a2) 倍)。
- 独立变量的和:如果随机变量 (X) 和 (Y) 相互独立,那么 (Var(X + Y) = Var(X) + Var(Y))。注意,这个性质的前提是 (X) 与 (Y) 独立。
直观上理解,如果所有数据都紧密聚集在期望值附近,则方差小;如果数据分布分散,波动大,则方差大。
方差的算术平方根称为标准差,记作 (\sigma_X):
[
\sigma_X = \sqrt{Var(X)}
]
标准差与原始随机变量具有相同的量纲,在实际中更常被用来表示数据的离散程度。
总结 🎯
本节课中我们一起学习了概率论的核心工具——期望与方差。
- 期望描述了随机变量平均可能取到的值,是概率加权下的均值。它具有线性和可加性。
- 我们通过一道复杂的异或期望面试题,实践了按位计算和利用期望可加性的解题思路。
- 方差描述了随机变量围绕其期望的波动或离散程度。方差小意味着数据集中,方差大意味着数据分散。
- 标准差是方差的平方根,与数据单位一致,便于直接比较。


理解并熟练运用期望与方差,是学习概率论、统计学以及后续机器学习算法的重要基础。
人工智能—机器学习公开课(七月在线出品) - P1:机器学习从业者在公司都做些啥 👨💻
概述
在本节课中,我们将了解机器学习从业者在互联网公司的实际工作内容。课程将澄清一些常见的误解,并详细介绍从业者日常工作的核心组成部分。
工作内容剖析
我不太清楚在座的这些同学有多少是实际在互联网公司从事相关工作的同学。
如果你从事的是数据相关的工作,并且与机器学习团队的同事交流过,你应该知道大部分互联网公司的机器学习工作并不像大家想象的那样,是去研究各种各样高大上的模型。
当然,像百度的IDL或者滴滴的研究院等大公司的研究院里,确实存在一些相对偏研究性质的岗位。
但是,大部分实际落地的应用,并非专注于研究诸如深度学习或多层神经网络这类复杂模型。
大部分情况下,你是在和数据打交道。
时间分配与核心任务
我们今天提到的这些内容,是大部分时间可能会花费的地方。
我们简单估算一下,可能有70%的时间是在处理数据,后面的30%的时间会用于建模、模型状态评估、模型融合等工作。
大部分复杂模型的算法精进,都是由一些数学科班出身的数据科学家,或者顶级实验室(如CMU)的同学在跟进。
大部分人只是将这些现成的算法包拿过来使用。
既然大家都在使用这些算法包,谁能用得更好?这在很大程度上取决于我们接下来要提到的、非常通用的数据处理能力。
数据处理与工程技能
大部分时候,你会处理各种各样的数据相关任务。
有很多同学会问,做机器学习是否需要具备Hadoop或Spark这类大数据处理框架的知识或背景。
实际上,如果你真的进入这样的团队,你一定会具备这个技能。
这些技能本身并不太难,只是因为数据量大到单机无法处理,所以你必然需要掌握在大规模数据上进行处理的相关方法。
例如,你可能需要编写一些MapReduce任务。
如果你对此不熟悉,但熟悉SQL,你也可以编写类似Hive SQL的查询,来完成数据仓库中各种“搬砖”性质的工作,我们称之为ETL(抽取、转换、加载)。
数据清洗与特征工程
你会花费大量时间进行各种各样的数据清洗工作。
因为你拿到手的数据,不一定可信,也不一定是按照真实分布展现给你的。
所以你需要处理数据中的离群点、缺失值等问题,确保拿到手的数据是可信的。
之后,会有一些工作专注于分析业务、寻找特征。
即使在技术非常精湛的团队,例如阿里的团队或百度做广告的团队,也确实有同学专门负责特征工程相关的工作。
我们组之前就有同学专门研究特征的组合、变换和映射,以探索是否能带来实际效果的提升。
模型选择:简单与复杂
学习了诸如GBDT、SVM(带有RBF核或多项式核)等算法后,你可能会觉得像逻辑回归这样简单的算法不太想用,而更倾向于使用高级算法。
但我要告诉大家,实际上,如果你去观察像阿里或百度这样真正核心的、使用机器学习的部门,它们一定会有一个逻辑回归模型作为基线模型。
因为这个模型非常可控。
我们组之前也上线过一些深度学习模型。
但正如我们后面会讲到的,深度学习模型是一个“黑盒”,它能产生好的结果,但一旦它表现不佳或出现问题,你很难定位原因——究竟是哪些样本导致了它做出这样的判定,或是哪部分特征出了问题。
我们需要有一个模型能够稳住当前的性能,使其不至于太差,这时使用的就是逻辑回归。
SVM模型严格意义上来说是这样的:我的理解是,SVM在小型数据集上可以取得非常好的效果,通过各种核函数映射能获得优秀表现。
但在特别大的数据集上,你很少会看到它被广泛使用,例如像电商这种一天能产生数亿数据的场景。
总结
本节课中,我们一起学习了机器学习从业者在公司的实际工作内容。
我们了解到,大部分时间(约70%)花在了数据处理、清洗和特征工程上,而非研究复杂模型。

掌握大数据处理工具(如Hadoop/Spark)和扎实的数据处理能力是必备技能。
在模型选择上,简单、可控的模型(如逻辑回归)常被用作重要的基线模型,而复杂模型(如深度学习)虽然强大,但也存在可解释性差的挑战。

理解这些实际工作流程,有助于大家建立对机器学习职业更清晰、更务实的认识。
人工智能—机器学习公开课(七月在线出品) - P10:机器学习完成数据科学比赛案例精讲 🎯
在本节课中,我们将学习如何运用机器学习技术解决一个实际的数据科学问题:大学生助学金精准资助预测。我们将通过一个真实的比赛案例,从数据理解、特征工程、模型构建到结果优化,完整地走一遍数据科学比赛的流程。
背景介绍
近期有新闻报道,中国科学技术大学尝试利用大数据方法辅助大学生助学金的发放。其思路是统计学生在食堂的每餐消费金额,例如男生每餐消费低于4元,则可能被识别为贫困生。但这并非纯自动化方法,仍需人工介入核实。
随着校园一卡通的普及,学生的消费、借阅、门禁等行为数据被集中记录。这些数据涵盖了学生的生活轨迹,理论上可以用于推断其生活水准,从而实现助学金的精准、个性化资助。
本节课的案例正是基于这一愿景。数据来源于企业与数据科学平台联合发起的一场比赛,旨在利用校园大数据预测学生获得助学金的情况。
数据说明
本案例利用2014年9月至2015年9月的数据,预测学生在2015年获得助学金的情况。数据分为训练集和测试集,每组约1万名学生记录,且ID无交集。
数据主要包括以下几类(均已脱敏处理):
- 图书馆借阅数据 (
borrow_train.txt/borrow_test.txt)- 字段:学生ID、借阅日期、图书名称、图书编号。
- 部分图书编号缺失。
- 一卡通消费数据 (
card_train.txt/card_test.txt)- 字段:学生ID、消费类型、消费地点、消费方式、消费时间、消费金额、卡内剩余金额。
- 宿舍门禁数据 (
dorm_train.txt/dorm_test.txt)- 字段:学生ID、进出时间。
- 图书馆门禁数据 (
library_train.txt/library_test.txt)- 字段:学生ID、进出时间。
- 学生成绩数据 (
score.txt)- 字段:学生ID、学院编号、成绩排名。
- 助学金标签数据 (
subsidy_train.txt)- 字段:学生ID、助学金金额(0元、1000元、1500元、2000元)。此标签由辅导员等人根据多种因素人工评定得出。
所有数据表可通过“学生ID”这一关键字段进行关联。
核心挑战与思路
这是一个典型的多分类且样本不均衡的问题。从标签分布看,绝大多数学生(约86%)未获得助学金。
数据科学比赛是一个需要长期迭代和分析的过程,不同于即时编程竞赛。其核心流程包括:数据探索、特征构造、模型预测、分析bad case、补充特征、再次迭代。特征工程的质量往往比模型选择更能决定成绩的上限。
上一节我们介绍了案例背景和数据概况,本节中我们来看看如何从最基础的模型开始,一步步构建解决方案。
基础建模流程演示
我们将从简单的逻辑回归模型开始,逐步尝试更复杂的模型,包括集成学习和神经网络。请注意,以下演示侧重于流程,特征工程较为基础。
第一步:数据读取与初步清洗
我们首先读取一卡通消费数据和成绩数据,并进行初步处理。
import pandas as pd
# 1. 读取一卡通数据
card_columns = ['sid', 'consume_type', 'consume_location', 'consume_category', 'time', 'amount', 'remainder']
card_train = pd.read_csv('card_train.txt', names=card_columns)
card_test = pd.read_csv('card_test.txt', names=card_columns)
# 合并训练集和测试集,便于统一处理
card_all = pd.concat([card_train, card_test], ignore_index=True)
# 2. 读取成绩数据
score = pd.read_csv('score.txt', names=['sid', 'dept_id', 'rank'])
# 3. 处理缺失值(示例:粗暴地用众数‘食堂’填充消费地点缺失值)
card_all['consume_location'].fillna('食堂', inplace=True)
第二步:特征构造
基于直观理解,学生的经济消费状况是判断其是否需要助学金的重要依据。我们构造以下基础特征:
- 学生总消费金额:按学生ID分组,对消费金额求和。
- 分消费类别的金额统计:按学生ID和消费方式(如食堂、超市、洗衣房)分组,统计在各处的消费总额。缺失类别填充为0。
- 成绩排名归一化:按学院分组,对成绩排名进行标准化处理,以消除学院间差异。
# 1. 计算总消费
total_consume = card_all.groupby('sid')['amount'].sum().reset_index()
total_consume.columns = ['sid', 'total_amount']
# 2. 计算分消费类别的金额(以消费方式‘consume_category’为例)
category_consume = card_all.groupby(['sid', 'consume_category'])['amount'].sum().unstack(fill_value=0)
category_consume.columns = ['category_' + str(col) for col in category_consume.columns]
# 3. 成绩排名归一化(按学院)
score['rank_normalized'] = score.groupby('dept_id')['rank'].transform(lambda x: (x - x.mean()) / x.std())
# 4. 合并特征
features = pd.merge(total_consume, category_consume, on='sid', how='left')
features = pd.merge(features, score[['sid', 'rank_normalized']], on='sid', how='left')
features.fillna(0, inplace=True) # 填充合并产生的缺失值
第三步:数据分割与预处理
将处理好的特征与标签数据关联,并分割为训练集和测试集。
# 读取标签
label = pd.read_csv('subsidy_train.txt', names=['sid', 'subsidy'])
# 关联特征与标签
data = pd.merge(features, label, on='sid', how='inner') # 训练集取交集
X_train = data.drop(['sid', 'subsidy'], axis=1)
y_train = data['subsidy']
# 对于测试集,只取特征
test_data = features[~features['sid'].isin(label['sid'])] # 假设测试集ID不在训练标签中
X_test = test_data.drop(['sid'], axis=1)
# 对于线性模型和神经网络,需要进行特征缩放(例如归一化)
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
第四步:模型构建与评估
我们将尝试多种模型。注意:由于样本不均衡,评估指标不能使用准确率(Accuracy),而应使用F1-score(特别是加权平均F1)等。
1. 逻辑回归 (Logistic Regression)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import f1_score, classification_report
# 由于是多分类,使用 multinomial
lr = LogisticRegression(multi_class='multinomial', solver='lbfgs', max_iter=1000)
param_grid = {'C': [0.01, 0.1, 1, 10]}
grid_search = GridSearchCV(lr, param_grid, cv=5, scoring='f1_weighted')
grid_search.fit(X_train_scaled, y_train)
print(f"Best LR F1: {grid_search.best_score_:.4f}")
y_pred = grid_search.predict(X_test_scaled)
2. 随机森林 (Random Forest)
树模型通常不需要特征缩放。
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier()
param_grid_rf = {
'n_estimators': [50, 200, 500],
'max_depth': [5, 10, None]
}
grid_search_rf = GridSearchCV(rf, param_grid_rf, cv=5, scoring='f1_weighted')
grid_search_rf.fit(X_train, y_train) # 使用未缩放的X_train
print(f"Best RF F1: {grid_search_rf.best_score_:.4f}")
3. 梯度提升树 (GBDT - XGBoost示例)
from xgboost import XGBClassifier
xgb = XGBClassifier(eval_metric='mlogloss', use_label_encoder=False)
param_grid_xgb = {
'n_estimators': [100, 200],
'learning_rate': [0.01, 0.05, 0.1],
'max_depth': [3, 5]
}
grid_search_xgb = GridSearchCV(xgb, param_grid_xgb, cv=5, scoring='f1_weighted')
grid_search_xgb.fit(X_train, y_train)
print(f"Best XGB F1: {grid_search_xgb.best_score_:.4f}")
4. 浅层神经网络 (TensorFlow/Keras示例)
神经网络必须使用缩放后的特征。
import tensorflow as tf
from tensorflow import keras
model = keras.Sequential([
keras.layers.Dense(12, activation='relu', input_shape=(X_train_scaled.shape[1],)),
keras.layers.Dense(8, activation='relu'),
keras.layers.Dense(4, activation='softmax') # 4个输出类别
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 注意:为处理不均衡,可以在fit中设置class_weight
model.fit(X_train_scaled, y_train, epochs=50, batch_size=32, validation_split=0.2, verbose=0)
# 预测需要取概率最大的类别
y_pred_proba = model.predict(X_test_scaled)
y_pred_nn = y_pred_proba.argmax(axis=1)
通过以上流程,我们完成了从数据到基础模型的构建。然而,正如之前所述,基础特征下的模型性能(F1分数)可能并不理想。接下来,我们来看看冠军团队是如何通过精湛的特征工程大幅提升模型表现的。
冠军方案特征工程精粹
该比赛冠军团队的成绩远超第二名,其成功的关键在于极其深入和创造性的特征工程。他们的工作流程可以概括为:地毯式特征抽取 → 多人脑暴设计 → 多轮迭代优化。
以下是他们部分特征构造的思路,极具启发性:
第一轮:基础特征(约200个特征)
- 图书馆借阅:是否借书、借书总数、借阅考研/编程/托福等特定类型书籍的次数。
- 门禁数据:不同时间段(如早晨、晚上、周末)进出图书馆/宿舍的次数、在馆/在舍天数、最早/最晚离开时间。
第二轮:统计与维度切分特征(增至约500个特征)
- 消费地点分析:统计消费金额最高的前N个地点、最常去的N个地点。
- 消费时间分析:按24小时、是否节假日/周末/寒暑假、三餐时间等维度切分,统计各时段消费次数与金额。
- 消费模式:针对12种消费方式(食堂、超市、洗衣等),分别统计次数、金额、频率。
第三轮:高级组合与统计特征(增至约1151个特征)
- 比率特征:
总消费金额 / 活跃天数、某时段消费金额 / 活跃天数。 - 交叉特征:
本学院消费排名 * 成绩排名。 - 行为序列特征:
过完年后的第一笔消费日期(反映返校早晚)。 - 统计特征:
日均消费在0-10元区间的天数、单笔消费最高金额、消费金额的方差等。
第四轮:特征选择与优化(约1200个特征)
- 使用随机森林评估特征重要性。
- 通过实验验证(删除特征后看模型在验证集上的表现)来筛选特征。
特征重要性洞察
冠军团队发现,最重要的特征并非直观的消费总额,而是:
- 是否曾更换校园卡(可能反映了卡片丢失或损坏情况,间接关联经济习惯)。
- 日均低消费(0-10元)的频率。
- 早晨(7-8点)的平均消费额(反映作息规律性)。
- 消费排名与成绩排名的组合特征。
此外,他们还针对样本不均衡问题,采用了分层抽样构建交叉验证集,并为不同类别设置权重。在模型上,他们主要使用了GBDT(如LightGBM)和随机森林,并通过多模型集成(投票/堆叠) 来进一步提升效果。
总结与启示
本节课中我们一起学习了如何用机器学习解决“大学生助学金预测”这一数据科学比赛案例。
我们首先了解了案例背景与数据构成,然后演示了从数据清洗、基础特征构造到应用逻辑回归、随机森林、GBDT及神经网络进行建模的完整流程。最后,我们深入剖析了冠军团队的解决方案,其核心启示在于:
- 特征工程是灵魂:在结构化数据比赛中,对业务的深刻理解与创造性的特征构造,其价值远大于选择复杂的模型。冠军团队通过上千个精心设计的特征显著提升了预测能力。
- 样本不均衡需妥善处理:必须使用合适的评估指标(如F1-score),并采用分层抽样、类别权重调整等方法。
- 迭代与分析是关键:数据科学比赛是不断“假设-验证-改进”的循环过程,需要耐心分析bad case,持续优化特征。
- 模型融合锦上添花:在拥有强特征的基础上,模型集成可以进一步提升性能。


希望本课程能帮助你理解数据科学比赛的基本方法论,并激发你在特征工程上的创造力。记住,从业务中来,到业务中去,是解决此类问题的根本。


人工智能—机器学习公开课(P11):机器学习项目实施方法论 🧠

在本节课中,我们将要学习机器学习项目的实施方法论。随着机器学习技术的成熟,其在项目中的应用越来越广泛。然而,技术的应用效果会因人员素质、项目情况和约束条件的不同而产生巨大差异。因此,建立一套规范化的实施步骤和方法论至关重要。本节课将借鉴软件工程的思想,探讨如何将工程化的方法应用于机器学习项目,以确保项目高效、规范地推进。

一、机器学习项目的定义与本质
上一节我们介绍了课程的主题,本节中我们来看看如何定义和理解机器学习项目。
机器学习项目是在有限的资源(如时间、人员)约束下,为了满足客户的业务分析需求,综合运用机器学习技术,分析挖掘数据中蕴含的规律,为商业决策提供支撑的过程。
核心公式可以概括为:
项目目标 = 在约束条件下,使用机器学习技术分析数据,以支撑业务决策
这个定义强调了几个关键点:
- 有限资源约束:项目必须在给定的时间、人力等条件下完成。
- 服务业务需求:最终目标是满足客户的业务分析需求,技术是服务于此目标的手段。
- 数据驱动:核心工作是分析数据、挖掘规律。
- 提供决策支撑:成果是为商业或业务决策提供依据,而非替代决策本身。
项目的核心挑战:跨领域映射
机器学习项目实施的核心困难在于需要在两个不同的问题域之间进行工作与映射。
- 业务问题域:你需要深入理解所要解决的具体业务问题(例如,气象领域的延伸期预报)。必须搞懂该领域的业务逻辑、现有技术方法、面临的困难及未来方向。
- 技术能力域:你需要判断现有的机器学习技术、模型和方法能否以及如何解决抽象出来的业务问题。
关键映射过程:
业务问题 -> 抽象 -> 技术问题(如回归、分类、聚类)
这就要求项目人员(尤其是算法应用工程师)不仅是技术专家,还需要具备快速学习新业务领域知识的能力,成为能够沟通业务与技术的复合型人才。对业务领域保持敬畏之心,在充分理解问题之前,避免做出过度承诺。
项目中的角色与利益相关方
在项目推进中,理解各方角色有助于更好地协调与沟通。
乙方(项目承接方)常见角色:
- 算法应用工程师:核心角色,负责应用现有算法构建模型。
- 数据工程师:负责协调、获取、整理和提供高质量的数据。
- 开发工程师:负责将训练好的模型集成到业务系统中上线。
- 数据科学家:凭借经验,指导业务问题抽象、模型选型和实验方向。
- 项目经理/产品经理:负责项目整体管理、协调各方、控制需求、推进落地。目前该角色在机器学习项目中非常稀缺,是算法工程师一个重要的成长方向。
甲方(客户方)常见角色:
- 项目决策者:高层领导,决定项目做与否及最终验收。
- 业务专家:精通具体业务,为决策者提供专业建议,是评估项目成果的关键人物。
- 业务工程师:一线业务人员,掌握具体的业务细节和技巧。
- 数据工程师:管理业务系统数据,是获取数据的主要接口人。
处理与不同角色的关系,特别是管理甲方期望并有效沟通,是项目成功的重要因素。


二、核心方法论:CRISP-DM模型 🔄
上一节我们分析了项目的定义与角色,本节中我们来看看一个可供借鉴的成熟方法论。
目前,机器学习领域缺乏像软件工程中“敏捷开发”那样被广泛认可的标准方法论。一个可行的借鉴是来自数据挖掘领域的 CRISP-DM(跨行业数据挖掘标准流程)。考虑到数据挖掘本质上是机器学习技术在商业分析领域的应用,其方法论具有很高的参考价值。
CRISP-DM模型具有行业无关性、工具无关性和应用无关性,是一个抽象层次较高的通用流程。
以下是该模型的核心阶段图示及详解:

各阶段详解
以下是CRISP-DM模型六个阶段的核心工作:
1. 商业理解
这是项目的起点,也是决定性的环节。目标是深入理解业务背景、目标和需求。你需要:
- 向业务专家学习或研读领域文献,使自己能用“行话”沟通。
- 准确定义商业目标,评估项目环境、资源、约束与风险。
- 输出:明确的项目目标文档,获得甲方确认。
2. 数据理解
在业务理解基础上,聚焦支持业务的数据。
- 收集数据,识别数据源。
- 探索数据,理解每个字段的业务含义。
- 评估数据质量,发现数据问题。
- 输出:数据探索报告,与甲方确认数据现状。
3. 数据准备
从原始数据构造出最终用于建模的数据集。
- 进行数据清洗、转换、集成。
- 构造特征工程,生成衍生变量。
- 输出:干净、可用于建模的数据集。
4. 建模
应用各种机器学习算法构建模型。
- 选择建模技术(如回归、分类、聚类)。
- 进行模型训练、调参和优化。
- 输出:训练好的模型及其技术评估报告(如准确率、F1值)。
5. 评估
此阶段评估并非技术指标评估,而是业务价值评估。 核心是将模型结果转化为业务语言,让业务专家理解和认可其价值。
- 评估模型结果是否满足商业目标。
- 与业务专家沟通,解释模型逻辑和结论(可解释性至关重要,有时需为此牺牲部分精度)。
- 若未通过业务评估,则需返回前面阶段(商业理解、数据理解等)进行迭代。
- 输出:由业务专家认可的项目结论报告。

6. 部署
将评估通过的模型投入实际使用。
- 将模型集成到现有业务流程或系统中。
- 生成最终报告,总结项目。
- 规划模型监控与维护,为下一轮迭代做准备。
文档化与沟通
方法论的有效执行离不开严格的文档化和有效沟通。
- 每个阶段都应有明确的输入和输出文档。例如,商业理解阶段的需求文档,数据理解阶段的数据质量报告。
- 文档是与甲方确认需求、划定责任、管理变更的重要依据。
- 在“评估”阶段,沟通技巧至关重要。你需要用业务语言(如规则、概率)而非技术术语(如神经网络层数)向业务专家解释模型。

业务问题的抽象映射
将模糊的业务需求准确抽象为可解决的技术问题,是方法论成功的关键。以下是一些常见的映射:
- 数据描述/总结 → 基本统计分析(同比、环比、百分比)。
- 用户/市场细分 → 聚类分析(如K-Means)。注意,甲方常称之为“分类”,但技术上可能是无监督的聚类。
- 预测趋势 → 回归分析(如线性回归、时间序列预测)。
- 发现关联 → 关联规则分析(如Apriori算法)。
- 规则归纳 → 分类模型(如决策树),其产生的规则易于解释。
三、案例实践:延伸期降水预报项目 🌧️
上一节我们介绍了理论方法论,本节中我们通过一个真实案例来看其具体应用。
项目背景:为某省气象部门构建“延伸期”(未来10-30天)降水预报模型。
1. 商业理解
- 行动:通过查阅专业书籍、文献,向气象专家请教,深入理解“延伸期降水预报”的业务定义、现有方法和难点。
- 发现:现有主流方法采用“基于主成分分析的多元滞后回归模型”,本质是一个线性回归模型。其存在非线性拟合能力不足、未利用空间特征、所用气象特征单一等问题。
- 结论:业务上存在改进空间,技术上我们具备用更复杂模型(非线性、多特征)尝试的能力。
2. 数据理解与准备
- 行动:分析气象站点数据(空间分布、时间序列)、探空气象数据(多高度层特征)等。
- 发现:站点空间分布较均匀,适合提取空间特征;历史数据连续,适合提取时间特征;可用特征远多于文献所用。
- 准备:将站点数据网格化(视为图像),准备多时间尺度的序列切片,整合多维气象特征。

3. 建模
- 技术方案:
- 使用卷积神经网络(CNN) 提取网格数据的空间特征。
- 使用循环神经网络(RNN) 提取不同时间切片(近期、中期、远期)的时间特征。
- 将其他气象特征输入全连接神经网络。
- 对多个子模型的输出进行集成学习(如加权平均)。
- 代码示意(核心思路):
# 伪代码,展示多模型融合思路 spatial_feat = CNN_Net(grid_data) temporal_feat = RNN_Net(time_series_data) other_feat = Dense_Net(other_features) combined_feat = concatenate([spatial_feat, temporal_feat, other_feat]) final_output = Final_Dense_Net(combined_feat) # 训练并融合多个此类模型
4. 评估与部署
- 行动:将技术方案和初步结果向气象业务专家汇报,重点解释我们如何改进了传统方法的不足(引入了空间、时间特征,使用了非线性模型)。
- 结果:虽然模型绝对精度仍有提升空间,但业务专家认可该技术思路的价值和先进性,认为这是未来改进的正确方向。项目通过评审。
- 部署:使用Docker容器化技术,将多个子模型部署在云平台上,便于集成和扩展。

四、总结与建议 📝
本节课中我们一起学习了机器学习项目的实施方法论。
我们首先明确了机器学习项目的定义:在资源约束下,利用机器学习技术分析数据以支撑业务决策。其核心挑战在于完成业务问题域到技术能力域的映射,这要求项目人员成为复合型人才。
接着,我们详细介绍了可借鉴的 CRISP-DM 方法论,它包含六个阶段:
- 商业理解(奠定基础)
- 数据理解(熟悉原料)
- 数据准备(加工原料)
- 建模(核心生产)
- 评估(业务验收——最关键也最易被忽视)
- 部署(交付上线)



我们强调了文档化和以业务语言沟通的重要性,特别是在“评估”阶段,模型的可解释性往往比单纯的精度提升更重要。

最后,通过一个气象预报的案例,我们看到了该方法论从业务理解、问题抽象、技术方案设计到业务沟通的全过程应用。

给初学者的建议:
- 摆正位置:技术是支撑,业务是目标。保持谦逊,深入学习业务知识。
- 重视流程:遵循规范的方法论步骤,避免盲目跳入模型构建。
- 学会沟通:练习将复杂的技术结论转化为业务专家能听懂的语言。
- 拓展视野:在精通技术的同时,有意识地培养项目协调、管理和沟通能力,向数据科学家或项目经理等复合角色发展。
机器学习项目实施既是科学,也是艺术。希望这套方法论能为你提供一张有价值的“地图”,帮助你在实际项目的复杂地形中更稳健地前行。




人工智能—机器学习公开课(七月在线出品) - P12:机器学习中的贝叶斯方法——以曲线拟合为例
在本节课中,我们将要学习如何将概率论中非常重要的贝叶斯方法引入到机器学习领域。我们将以多项式曲线拟合为例,展示如何将传统的机器学习模型改造为概率模型,从而获得更好的可解释性和应用价值。
📚 第一部分:概率的基本规则与贝叶斯定理
上一节我们介绍了课程的目的和意义,本节中我们来看看概率论的基础。理解概率的基本规则是掌握贝叶斯方法的关键。现代概率论建立在几条公理之上,其中最核心的是加法规则和乘法规则。
加法规则
加法规则建立了一个随机变量X的边缘分布与两个随机变量X、Y的联合分布之间的关系。其公式如下:
P(X) = Σ_Y P(X, Y)
这个公式的含义是:当我们对联合概率 P(X, Y) 中的随机变量Y取遍所有可能的值并求和时,就得到了X的边缘概率 P(X)。换句话说,通过累加使一个随机变量“不起作用”,联合概率就退化为边缘概率。
乘法规则
乘法规则建立了联合概率与条件概率、边缘概率之间的关系。其公式如下:
P(X, Y) = P(Y | X) * P(X)
这个公式的含义是:两个随机变量X和Y的联合概率,可以拆分为“在已知X的条件下Y发生的概率”与“X发生的概率”的乘积。这为我们拆分复杂的联合概率提供了工具。
掌握了这两条规则,就可以推导出更复杂的概率模型。
贝叶斯定理
贝叶斯定理可以直接从乘法规则及其对称性推导出来。首先,根据乘法规则和对称性 P(X, Y) = P(Y, X),我们可以得到:
P(Y | X) * P(X) = P(X | Y) * P(Y)
对这个等式进行变换,就得到了贝叶斯定理的标准形式:
P(Y | X) = [P(X | Y) * P(Y)] / P(X)
在机器学习中,我们通常将 X 视为输入数据,Y 视为输出或模型参数。因此,条件概率 P(Y | X) 可以被视为一个概率模型:给定输入X,输出Y取某个值的概率。
为了更好地理解贝叶斯定理,我们对其组成部分进行命名:
- 后验概率
P(Y | X):在观察到证据X之后,我们对Y的信念(概率)。 - 似然函数
P(X | Y):在假设Y为真的情况下,观察到X的可能性。 - 先验概率
P(Y):在观察到任何证据X之前,我们对Y的初始信念。 - 证据因子
P(X):观察到证据X的边际概率,也称为归一化因子。
贝叶斯定理的核心思想是动态更新:我们从一个关于Y的初始信念(先验)开始,当获得新的证据X后,通过结合似然函数来更新我们的信念,从而得到后验概率。这个过程可以迭代进行,用当前的后验作为新的先验,随着证据的不断积累,我们对Y的认识会越来越准确。
此外,由于证据因子 P(X) 对于不同的Y值通常是相同的(它是一个归一化常数,确保所有后验概率之和为1),我们在比较后验概率大小时常常忽略它,使用正比关系:
P(Y | X) ∝ P(X | Y) * P(Y)
即,后验概率正比于似然函数与先验概率的乘积。
🔍 第二部分:多项式曲线拟合的机器学习方法
上一节我们介绍了概率论的基础和贝叶斯定理,本节中我们来看看一个传统的机器学习问题——多项式曲线拟合,并思考其局限性。
假设我们有一个包含N个观测值的训练集:输入 X = {x1, x2, ..., xN} 和对应的目标值 T = {t1, t2, ..., tN}(这里T即标签Y)。我们的目标是学习一个模型来拟合这些数据。
我们使用一个M阶多项式作为模型:
y(x, w) = w0 + w1*x + w2*x^2 + ... + wM*x^M = Σ_{j=0}^{M} w_j * x^j
其中,w = (w0, w1, ..., wM)^T 是多项式系数向量,是我们需要求解的未知参数。
为了求解最优的 w,我们定义一个损失函数(如平方损失)和一个代价函数(如经验风险)。经验风险最小化定义为:
最小化:E(w) = 1/2 * Σ_{n=1}^{N} {y(x_n, w) - t_n}^2
通过最小化 E(w),我们可以得到一组确定的系数 w。为了缓解过拟合,通常会加入正则化项(如L2正则),转化为结构风险最小化:
最小化:Ẽ(w) = 1/2 * Σ_{n=1}^{N} {y(x_n, w) - t_n}^2 + λ/2 * ||w||^2
这种方法得到的模型,对于新的输入 x0,会输出一个确定的预测值 y(x0, w)。然而,这种“确切的结论”在实际应用中(如医疗诊断)可能显得过于生硬,缺乏对预测不确定性的度量。
🧩 第三部分:多项式曲线拟合的贝叶斯方法
上一节我们回顾了传统的曲线拟合方法,本节中我们来看看如何用贝叶斯方法对其进行改造,使其输出概率化的结果。改造分为两步:最大似然方法和最大后验方法。
第一步:最大似然方法
首先,我们将模型的输出从一个确定值改为一个概率分布。我们假设,对于给定的输入 x,其目标值 t 服从一个高斯分布,该分布的均值是我们的多项式预测值 y(x, w),精度(方差的倒数)为 β。
p(t | x, w, β) = N(t | y(x, w), β^{-1})
这样,模型就变成了一个概率模型:p(t | x, w, β)。给定 x,它给出了 t 取各个值的概率。
接下来,我们定义似然函数。假设数据点独立同分布,整个数据集的似然函数是每个数据点似然的乘积:
p(T | X, w, β) = Π_{n=1}^{N} N(t_n | y(x_n, w), β^{-1})
为了方便求解,我们通常最大化对数似然函数。通过推导可以发现,在平方损失的定义下,最大化(对数)似然函数等价于最小化经验风险(即平方和误差)。因此,我们可以用同样的方法求出使似然最大的 w_ML(最大似然解)。
此外,我们还可以求出精度参数 β 的最大似然解 β_ML。这样,我们就得到了一个完整的概率模型:对于新输入 x0,预测值 y(x0, w_ML) 是高斯分布的均值,同时我们还知道这个预测的方差 β_ML^{-1}。这比单纯给出一个确定值提供了更多信息(不确定性度量)。
第二步:最大后验方法
最大似然方法只对输出 t 进行了概率化,但模型参数 w 仍然是确定值。在实际应用中,系数 w 的大小代表了对应特征(如 x, x^2)的重要性,对其不确定性进行度量也很有价值。
为此,我们进一步对参数 w 引入先验分布。假设其先验也是一个零均值的高斯分布:
p(w | α) = N(w | 0, α^{-1} I)
这里 α 是控制先验分布方差的超参数。零均值的假设符合“奥卡姆剃刀”原理:在没有先验知识时,倾向于简单的模型(系数值小)。
根据贝叶斯定理,参数 w 的后验概率正比于似然函数与先验概率的乘积:
p(w | X, T, α, β) ∝ p(T | X, w, β) * p(w | α)
我们的目标是找到使后验概率最大的 w,即最大后验估计。通过最大化后验概率(等价于最小化负对数后验),并进行推导,可以发现一个关键的结论:最大化后验概率等价于最小化带有L2正则项的结构风险。其中,正则化系数 λ 与先验精度 α 和似然精度 β 有关。
因此,通过最大后验方法,我们不仅得到了参数 w_MAP(最大后验解)和 β,更重要的是,我们得到了 w 的一个概率分布(后验分布)。我们可以给出系数 w 最可能的值(均值 w_MAP),同时给出其不确定性(方差)。这在业务解释上极具价值,例如可以判断哪些特征因子是显著重要的。
💻 第四部分:代码实现与示例
上一节我们从理论上介绍了贝叶斯方法在曲线拟合中的应用,本节中我们通过一个简单的代码示例来直观感受一下。
以下是一个使用贝叶斯方法进行多项式回归的简要流程和关键点:
- 生成数据:我们以正弦函数
sin(2πx)为基础,加入高斯噪声来模拟真实数据。 - 特征处理:将输入
x转换为多项式特征向量[1, x, x^2, ..., x^M]。 - 设置先验:设定参数
w的先验分布N(0, α^{-1}I)和噪声精度β的初始值。 - 计算后验:利用贝叶斯定理的推导结果,将问题转化为求解一个线性方程组,计算后验分布
p(w | X, T)的均值和方差。这可以使用numpy.linalg中的函数高效完成。 - 预测与绘图:对于新的输入,预测输出不再是单个值,而是一个高斯分布。我们可以画出预测均值曲线以及均值±标准差的范围(通常表示不确定性区间)。
在结果图中,我们可以看到:
- 绿色曲线是真实的正弦函数。
- 蓝色点是带有噪声的训练数据。
- 红色曲线是模型预测的均值。
- 红色阴影区域表示预测的不确定性范围(如±1标准差)。
可以看到,在数据点密集的区域,预测不确定性小;在数据点稀疏的区域(如两端),预测不确定性变大。这正是概率模型带来的优势:它不仅告诉我们“预测值是多少”,还告诉我们“这个预测有多可靠”。
📝 总结
本节课中我们一起学习了如何将贝叶斯方法融入机器学习。
我们首先回顾了概率论的两条基本规则——加法规则和乘法规则,并由此推导出机器学习的核心概率工具:贝叶斯定理。我们强调要动态地理解贝叶斯定理,即用新证据不断更新信念的过程。
接着,我们以多项式曲线拟合为例,展示了改造传统模型的两种贝叶斯方法:
- 最大似然方法:将模型输出改为概率分布,为预测值提供不确定性度量。
- 最大后验方法:进一步为模型参数引入先验分布,不仅得到概率化的预测,还得到概率化的参数估计,增强了模型的可解释性。
关键的洞见在于:经验风险最小化等价于最大似然估计,结构风险最小化(L2正则)等价于最大后验估计。这桥接了传统优化观点与贝叶斯概率观点。


最后,通过代码示例,我们直观看到了贝叶斯回归如何给出预测及其不确定性区间。掌握贝叶斯方法,对于理解复杂概率模型、构建可解释的AI系统以及未来与深度学习融合,都具有重要意义。

人工智能—机器学习公开课(七月在线出品) - P13:极大似然估计 📊
在本节课中,我们将要学习参数估计的一种核心方法——极大似然估计。我们将从贝叶斯公式出发,理解其思想来源,并通过具体例子掌握其计算过程,最终推导出正态分布参数的极大似然估计结果。
从贝叶斯公式到估计思想 🤔
上一节我们介绍了矩估计,本节中我们来看看另一种更常用的参数估计方法。
给定样本数据 ( D ),在 ( D ) 发生的条件下,结论 ( A ) 发生的概率可以写成贝叶斯公式的形式:
[
P(A|D) = \frac{P(D|A) P(A)}{P(D)}
]
其中,( P(D) ) 是归一化因子,可以忽略。如果我们假设各种可能结论 ( A_i ) 的先验概率 ( P(A_i) ) 相等或近似,那么 ( P(A|D) ) 就正比于 ( P(D|A) )。
换句话说,我们想做的事是:给定这组样本数据 ( D ) 后,看哪个参数 ( A_i ) 对应的 ( P(D|A_i) ) 概率值最大,我们就认为总体最有可能取那个参数。即,哪个参数能使观测到的数据出现的可能性最大,我们就取谁。这种“像那个样子”的可能性在文言文中称为“似然”,让似然达到极大的估计方法就是极大似然估计。
似然函数的定义与构建 🔨
上一节我们介绍了极大似然估计的思想,本节中我们来看看如何将其数学化。
设总体分布为 ( f(x; \theta) ),其中 ( \theta ) 是未知参数。( X_1, X_2, ..., X_n ) 是从该总体中独立抽取的样本。由于样本独立同分布,其联合概率密度(或概率质量)函数为各样本点概率的乘积:
[
L(\theta) = L(\theta; X_1, ..., X_n) = \prod_^ f(X_i; \theta)
]
这个关于参数 ( \theta ) 的函数 ( L(\theta) ) 就称为似然函数。在样本 ( X_i ) 已知的情况下,似然函数只与未知参数 ( \theta ) 有关。我们的目标是找到能使 ( L(\theta) ) 取得极大值的 ( \hat{\theta} ),即:
[
\hat{\theta} = \arg \max{\theta} L(\theta)
]
这种方法就是极大似然估计。
一个具体例子:估计硬币正面概率 🪙
理解了似然函数的概念后,我们通过一个简单例子来掌握计算过程。
假设抛一枚硬币10次,得到结果:正,正,反,正,正,正,反,反,正,正。设每次抛掷得到正面的概率为未知参数 ( p )。
以下是构建似然函数并求解的步骤:
- 每次抛掷是独立的伯努利试验。出现正面的概率为 ( p ),出现反面的概率为 ( 1-p )。
- 根据观测序列,其似然函数为:
[
L(p) = p \times p \times (1-p) \times p \times p \times p \times (1-p) \times (1-p) \times p \times p = p^7 (1-p)^3
] - 直接对 ( L(p) ) 求导找极值较复杂,通常对其取自然对数,得到对数似然函数 ( l(p) = \ln L(p) )。
[
l(p) = 7\ln p + 3\ln(1-p)
] - 对 ( l(p) ) 关于 ( p ) 求导并令其为零:
[
\frac{dl(p)} = \frac{7} - \frac{3}{1-p} = 0
] - 解方程得到 ( p = 0.7 )。可以验证该点为极大值点。
因此,硬币正面概率 ( p ) 的极大似然估计值为 ( \hat = 0.7 )。
核心应用:正态分布的参数估计 📈
上一节我们通过一个离散例子熟悉了流程,本节中我们来看一个极其重要的连续分布例子——估计正态分布的参数。
假设样本 ( X_1, X_2, ..., X_n ) 来自正态分布 ( N(\mu, \sigma2) ),其中均值 ( \mu ) 和方差 ( \sigma2 ) 未知。我们的目标是利用极大似然估计求出 ( \hat{\mu} ) 和 ( \hat{\sigma}^2 )。
以下是详细的推导过程:
- 写出似然函数:正态分布的概率密度函数为 ( f(x; \mu, \sigma2) = \frac{1}{\sqrt{2\pi\sigma2}} \exp\left(-\frac{(x-\mu)2}{2\sigma2}\right) )。因此似然函数为:
[
L(\mu, \sigma2) = \prod_ \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(X_i-\mu)2}{2\sigma2}\right)
] - 取对数得到对数似然函数:
[
l(\mu, \sigma2) = \ln L(\mu, \sigma2) = -\frac{2}\ln(2\pi) - \frac{2}\ln(\sigma2) - \frac{1}{2\sigma2}\sum_^(X_i-\mu)^2
] - 分别对 ( \mu ) 和 ( \sigma^2 ) 求偏导并令为零:
- 对 ( \mu ) 求偏导:
[
\frac{\partial l}{\partial \mu} = \frac{1}{\sigma2}\sum_(X_i-\mu) = 0 \quad \Rightarrow \quad \sum_(X_i-\mu)=0
]
解得:
[
\hat{\mu} = \frac{1}\sum_ X_i
] - 对 ( \sigma2 ) 求偏导(将 ( \sigma2 ) 视为整体):
[
\frac{\partial l}{\partial \sigma2} = -\frac{2\sigma2} + \frac{1}{2(\sigma^2)2}\sum_(X_i-\mu)^2 = 0
]
将 ( \hat{\mu} ) 代入,解得:
[
\hat{\sigma}2 = \frac{1}\sum_ (X_i - \hat{\mu})^2
]
- 对 ( \mu ) 求偏导:
结论:对于正态分布,其均值 ( \mu ) 的极大似然估计是样本均值,方差 ( \sigma^2 ) 的极大似然估计是样本二阶中心矩(即未修正的样本方差)。这个结果直观且与矩估计的结果在均值上一致,但在方差上有所不同(矩估计的样本方差分母是 ( n-1 ))。
总结与思考 🎯
本节课中我们一起学习了参数估计的核心方法——极大似然估计。
- 我们从贝叶斯公式出发,理解了“选择使观测数据出现概率最大的参数”这一核心思想。
- 我们定义了似然函数 ( L(\theta) ),并通过取对数简化求导过程。
- 我们通过估计硬币正面概率的例子,掌握了极大似然估计的基本计算步骤。
- 最后,我们完整推导了正态分布参数的极大似然估计结果,得到了 ( \hat{\mu} = \bar ) 和 ( \hat{\sigma}^2 = \frac{1}\sum (X_i - \bar)^2 ) 的重要结论。


极大似然估计是机器学习中许多模型(如逻辑回归、高斯混合模型)的基础。其思想虽然简单,但威力强大,是连接概率模型与观测数据的关键桥梁。
人工智能—机器学习公开课(七月在线出品) - P14:来自工业界的经验分享:机器学习基本流程 👨💻
在本节课中,我们将要学习机器学习在工业界应用的基本流程、核心概念以及一些实用的经验分享。我们将从宏观的机器学习分类讲起,深入到监督学习的具体应用,并探讨算法工程师在实际工作中需要具备的核心能力。
机器学习概述与分类 🤖
上一节我们介绍了课程的整体目标,本节中我们来看看机器学习的宏观分类。
在2016年之前,机器学习主要分为两大类:非监督学习和监督学习。
- 非监督学习:例如聚类算法,包括K-Means(硬聚类)和高斯混合模型(软聚类)。但在工业界实际应用中,这类工作相对较少。
- 监督学习:这是工业界应用最广泛的部分。例如,广告点击率预估模型、商品购买预测模型等,都属于有标签数据的监督学习问题。
2016年之后,强化学习开始兴起。它通过智能体与环境的交互进行学习,门槛较高,目前在工业界落地相对困难。
对于初学者和大多数实际工作而言,学习的重点应集中在监督学习上。
监督学习的工业应用实例 🎯
上一节我们了解了监督学习的重要性,本节中我们来看看它在工业界的具体应用。
一个典型的例子是点击率预估模型。其流程可以概括为:
用户行为数据 -> 特征工程 -> 模型训练(如DNN, DeepFM) -> 点击概率预估 -> 排序 -> 展示给用户
以下是监督学习中两个常见的任务:
- 点击率模型:预测用户点击某个广告或商品的概率。数据相对稠密,容易获取。
- 转化率模型:预测用户点击后是否会产生购买等转化行为。数据更稀疏,难度更大。
另一个重要应用是搜索推荐中的相关性模型。例如,用户搜索“七月在线”,系统需要从海量候选结果中筛选出最相关的内容。这里的挑战在于,相关性的标签数据无法像点击行为那样自动获取,通常需要通过人工标注(众包) 来获得。
算法工程师的核心能力与避坑指南 ⚙️
上一节我们看了具体的应用,本节中我们来探讨从事这项工作需要具备哪些核心能力。
在工业界从事机器学习,定义和理解业务问题的能力比推导复杂公式或调参更为核心。技术指标(如AUC)的提升必须与业务指标的提升对齐,否则工作价值会大打折扣。
除了扎实的理论基础,算法工程师必须具备强大的自主学习能力。这包括:
- 持续阅读顶会论文:关注NeurIPS, KDD, ICML, CVPR, ACL等顶级会议,跟踪前沿技术。
- 掌握核心工具:不仅要会用Scikit-learn这类封装好的库快速上手,更要深入掌握像TensorFlow或PyTorch这样的深度学习框架,以便灵活定制模型结构和损失函数来解决特定业务问题。
关于工具和模型的选择,有以下经验分享:
- Scikit-learn:适合快速入门和原型验证,封装了大多数经典算法。
- TensorFlow/PyTorch:是进行深度学习研究和解决复杂工业问题的必备工具,提供了极大的灵活性。
- 关于模型:像SVM这类模型在当前的工业界主流场景中已较少使用。逻辑回归也被更强大的模型所取代。当前的主流是树模型(如XGBoost, LightGBM)和深度学习模型。
总结 📝


本节课中我们一起学习了机器学习在工业界的基本面貌。我们了解到监督学习是当前应用的核心,并通过点击率预估和相关性模型等例子看到了理论如何落地。更重要的是,我们认识到一个合格的算法工程师,其核心竞争力在于将业务问题转化为机器学习问题的能力、技术指标与业务效果结合的意识以及通过阅读论文和掌握核心工具来实现持续自我迭代的自主学习能力。希望这些来自工业界的经验能帮助大家在学习和职业道路上方向更清晰。
人工智能—机器学习公开课(七月在线出品) - P15:老冯经典之作:纯白板手推SVM 🧠
在本节课中,我们将要学习支持向量机(SVM)的核心思想与数学推导过程。SVM不仅是机器学习中一个强大的“开箱即用”工具,其背后的数学原理和推导过程本身也具有极高的价值。我们将从最基础的直觉出发,一步步推导出SVM的优化目标,并揭示其背后关键的“核方法”思想。
概述:什么是SVM?🤔
SVM是一种用于分类(和回归)的监督学习模型。它的核心思想非常直观:在众多能将正负样本分开的决策边界中,寻找一个“最宽”的边界。这个“宽度”被称为“间隔”(Margin)。SVM认为,间隔最大的决策边界具有最好的鲁棒性,对噪声的容忍度更高。
上一节我们介绍了SVM的基本目标和直觉,本节中我们来看看如何用数学语言精确地描述这个目标。
第一步:用数学描述决策边界 📐
假设我们有一个线性可分的二分类问题,样本特征为向量 x,标签 y ∈ {+1, -1}。我们的目标是找到一个线性决策函数。
决策规则可以描述为:对于一个新样本点 u,如果满足以下条件,则预测为正类(+1)。
决策规则: w · u + b ≥ 0 => 预测为 +1
其中 w 是决策边界的法向量,b 是偏置项。我们的任务就是确定 w 和 b。
在训练集中,因为我们知道每个样本的真实标签,我们可以提出更强的要求。我们希望所有正样本离决策边界足够“远”,所有负样本也离决策边界足够“远”。为了数学上的方便,我们将这个“足够远”的距离定义为1。
以下是训练集中的约束条件:
- 对于正样本(y_i = +1):w · x_i + b ≥ 1
- 对于负样本(y_i = -1):w · x_i + b ≤ -1
我们可以利用标签 y_i 将这两个不等式巧妙地合并成一个统一的约束条件:
统一约束: y_i (w · x_i + b) ≥ 1, 对于所有训练样本 i
这个公式是我们推导的基石。
第二步:定义并计算“间隔”(Margin) 📏
“间隔”是指决策边界到其两侧最近样本的垂直距离之和,也就是分类“道路”的宽度。
假设在决策边界两侧各有一个最近的样本点 x_+(正类侧)和 x_-(负类侧),它们都恰好落在间隔边界上,即满足 y_i (w · x_i + b) = 1。那么,间隔的宽度可以通过向量 (x_+ - x_-) 在法向量 w 方向上的投影来计算。
间隔宽度公式推导如下:
间隔宽度 = (x_+ - x_-) · (w / ||w||)
由于 x_+ 和 x_- 满足边界条件,我们可以代入计算:
∵ y_+ = +1, ∴ w · x_+ + b = 1 => w · x_+ = 1 - b
∵ y_- = -1, ∴ w · x_- + b = -1 => w · x_- = -1 - b
将上述关系代入间隔宽度公式:
间隔宽度 = [(1 - b) - (-1 - b)] / ||w|| = 2 / ||w||
这是一个关键的发现:最大化间隔等价于最小化法向量 w 的模长 ||w||。
第三步:构建优化问题 ⚙️
根据上面的推导,我们的目标变得清晰:在满足所有训练样本约束的前提下,让间隔 2 / ||w|| 最大。
这等价于以下优化问题:
最小化: (1/2) ||w||^2
约束条件: y_i (w · x_i + b) ≥ 1, 对于所有 i
这里将目标函数写为 (1/2) ||w||^2 是为了后续求导的数学方便,它与最小化 ||w|| 是等价的。
第四步:引入拉格朗日乘子法 🔗
这是一个带约束的优化问题,我们使用拉格朗日乘子法将其转化为无约束问题。为每个约束引入一个非负的拉格朗日乘子 α_i ≥ 0。
构建拉格朗日函数 L(w, b, α):
L(w, b, α) = (1/2) ||w||^2 - Σ_i α_i [ y_i (w · x_i + b) - 1 ]
接下来,我们分别对 w 和 b 求偏导数,并令其为零。
对 w 求偏导:
∂L/∂w = w - Σ_i α_i y_i x_i = 0
=> w = Σ_i α_i y_i x_i
这个结果非常美妙:最优的决策边界法向量 w,是训练样本 x_i 的线性组合!
对 b 求偏导:
∂L/∂b = - Σ_i α_i y_i = 0
=> Σ_i α_i y_i = 0
第五步:对偶问题与关键发现 💎
我们将上面得到的两个关系式代回拉格朗日函数 L。经过一系列代入和化简(详细过程见课程推导),L 函数可以完全用拉格朗日乘子 α 和样本数据表示,并且消去了原始参数 w 和 b。
最终得到所谓的对偶问题:
最大化: L(α) = Σ_i α_i - (1/2) Σ_i Σ_j α_i α_j y_i y_j (x_i · x_j)
约束条件: α_i ≥ 0, 且 Σ_i α_i y_i = 0
观察这个目标函数 L(α),它揭示了一个至关重要的现象:优化过程以及最终模型,只依赖于训练样本两两之间的点积 (x_i · x_j),而不直接依赖于样本 x_i 本身的具体值。
同时,我们最初的决策规则也可以重写:
决策规则: Σ_i α_i y_i (x_i · u) + b ≥ 0 => 预测为 +1
这意味着,无论是训练还是预测,我们只需要计算样本之间的点积。
第六步:支持向量与核方法 🌟
求解上述对偶问题后,大部分 α_i 会等于0。那些 α_i > 0 所对应的训练样本 x_i,被称为支持向量。它们就是位于间隔边界上的点,决定了最终的决策边界。
核方法(Kernel Method)的诞生:由于模型只依赖于点积 (x_i · x_j),我们可以利用一个巧妙的“核技巧”。如果原始数据线性不可分,我们可以通过一个映射函数 Φ 将其映射到更高维的特征空间,使其在那个空间里线性可分。在高维空间中的计算可能非常复杂,但核技巧允许我们直接计算映射后向量的点积,而无需显式地进行映射:
核函数: K(x_i, x_j) = Φ(x_i) · Φ(x_j)
例如,使用高斯核 K(x_i, x_j) = exp(-γ ||x_i - x_j||^2),等价于将数据映射到了一个无限维的空间。这样,我们就能在原始空间中处理非线性决策边界。核方法的思想不仅限于SVM,也可以应用到线性回归、岭回归等模型中,形成它们的“核化”版本。
总结 🎯
本节课中我们一起学习了支持向量机的核心推导过程:
- 核心思想:寻找最大间隔的决策边界以获得最佳鲁棒性。
- 数学建模:将最大化间隔问题转化为最小化
||w||^2的带约束优化问题。 - 拉格朗日对偶:通过引入拉格朗日乘子,将原问题转化为对偶问题,并发现最优的 w 是训练样本的线性组合。
- 关键洞察:对偶问题的目标函数和决策规则只依赖于样本间的点积。
- 核方法:基于点积这一特性,通过引入核函数,可以隐式地将数据映射到高维空间,从而高效地处理非线性问题。


SVM的推导过程完美体现了统计学习理论、凸优化和泛函分析的结合,其价值与模型性能同等重要。理解这一推导,是灵活应用和扩展SVM乃至其他核方法模型的基础。

人工智能—机器学习公开课(七月在线出品) - P16:模型选择 🧠
在本节课中,我们将要学习机器学习中一个至关重要的环节:模型选择。我们将从两个层面来理解它:一是如何根据问题类型和数据特点选择最合适的算法模型;二是在选定模型后,如何通过调整其参数来获得最佳性能。理解并掌握模型选择的方法,是构建有效机器学习解决方案的关键一步。
模型选择的第一种含义:选择算法模型
上一节我们介绍了模型选择的概念,本节中我们来看看它的第一种常见含义:如何根据具体问题和数据,从众多机器学习算法中选择一个合适的模型。
通常,没有一种模型是万能的,能适用于所有场景。模型的选择往往依赖于特定的数据特征和问题背景。不过,存在一些通用的准则可以帮助我们进行初步筛选。
以下是基于 scikit-learn 库的一个常用模型选择流程图,它为我们提供了一个大致的决策路径:
判断样本量:首先评估你的数据样本量是否充足。机器学习通常需要大量样本来训练出具有良好泛化能力的模型。
- 如果样本量非常少,建议要么采集更多数据,要么考虑使用人工规则而非机器学习模型。
区分问题类型:在样本量充足的前提下,确定你要解决的问题是连续值预测(回归问题)还是离散类别判定(分类问题)。
- 回归问题:例如预测房价、股价。
- 分类问题:例如判定交易是否欺诈、用户是否会点击广告。
针对分类问题的路径选择:
- 如果样本量不是特别大,可以考虑线性支持向量机(Linear SVC)或逻辑回归(Logistic Regression)。
- 如果数据是文本类型,朴素贝叶斯(Naive Bayes)是一个经典且有效的选择。
- 如果样本量非常大,使用传统SVM可能训练缓慢,此时可考虑使用随机梯度下降(SGD)相关的分类器。
针对回归问题的路径选择:
- 如果样本量适中,可以考虑线性回归、支持向量回归(SVR)或梯度提升回归树(GBRT)等。
- 如果样本量非常大,同样需要考虑使用随机梯度下降(SGD)来优化训练过程。
- 如果数据维度非常高,通常需要先进行降维处理(如使用PCA算法),以节省计算资源。
无监督学习场景:如果你的数据没有标签(即不知道目标值
y),目标是发现数据内在的结构或关联,则应使用聚类算法(如K-Means)。
注意:此流程图是一个通用的参考指南。在实际应用中,根据具体数据和特征工程的结果,可能需要进行调整。例如,分类问题在维度很高时同样可能需要先进行降维。
模型选择的第二种含义:选择模型参数
上一节我们讨论了如何选择算法,本节中我们来看看模型选择的第二种含义:在选定某个具体模型后,如何为其选择最优的超参数。
即使确定了使用某个算法(例如多项式回归),模型内部仍有参数需要确定。以多项式回归为例,其核心参数是多项式的最高次数 n。
- 如果
n=0,模型退化为常数函数y = c,无法拟合复杂数据。 - 如果
n=1,模型为线性函数y = kx + b,可能无法很好地拟合非线性关系。 - 如果
n=3,模型为三次函数y = ax³ + bx² + cx + d,拟合能力更强。 - 如果
n=9,模型为九次函数,它可能在训练数据上拟合得非常好,甚至穿过每一个点,但这容易导致过拟合,即在新的、未见过的数据上表现很差。
因此,我们需要一种方法来评估不同参数下模型的好坏,并选择泛化能力最强的那个。这就是交叉验证的目的。
交叉验证:模型参数选择的利器
为了解决参数选择问题,我们引入交叉验证方法。标准的做法是将数据集分为三部分,而不仅仅是训练集和测试集。
以下是数据集的典型划分方式及其作用:
- 训练集:用于构建和训练模型。
- 交叉验证集:用于进行模型选择和参数调优。
- 测试集:仅在最终模型确定后,用于评估模型的泛化性能。
最经典的交叉验证方法是 K折交叉验证。其流程如下:
假设我们进行5折交叉验证(K=5):
- 首先,将训练集随机、均匀地分成5个子集(称为5折)。
- 然后进行5轮训练与验证。在每一轮中:
- 选取其中一折作为交叉验证集。
- 使用剩余的4折数据作为训练集来训练模型。
- 用训练好的模型在当轮的交叉验证集上评估性能(如计算准确率)。
- 最后,计算这5轮评估结果的平均值,作为该组参数下模型性能的估计。

通过这种方式,我们充分利用了有限的训练数据,减少了因单次数据划分不合理而带来的偏差,从而更可靠地比较不同参数组合的优劣。从所有参数组合中,我们选择在交叉验证集上平均性能最好的那一组。
最终,我们使用从未参与过模型选择和训练的测试集,对选定参数的模型进行最终的性能评估。
课程总结

本节课中我们一起学习了机器学习中的模型选择。我们首先从宏观层面了解了如何根据问题类型(回归/分类)、数据规模和数据形式(如文本)来选择合适的算法模型。接着,我们深入到微观层面,探讨了在选定算法后,如何通过交叉验证这一关键技术来为模型选择最优的超参数,并介绍了K折交叉验证的标准流程。理解这两个层面的模型选择,是构建稳健、高效机器学习模型的基础。
人工智能—机器学习公开课(七月在线出品) - P17:谱聚类
📚 课程概述
在本节课中,我们将要学习一种基于图论的聚类方法——谱聚类。我们将从“谱”的概念讲起,逐步介绍如何构建相似度图、计算拉普拉斯矩阵,并最终利用特征向量完成聚类。整个过程将结合公式和代码进行说明,力求简单直白,让初学者能够看懂。
🎼 什么是“谱”?
所谓“谱”,指的是一个方阵所有特征值的全体。对于一个线性算子(矩阵)A,其作用 A × x 仍然是一个线性变换。这个矩阵的“谱”就是它的特征值集合。
其中,最大的特征值被称为该谱的“半径”。在计算中,我们常常通过计算 ATA 这个方阵的最大特征值来求得谱半径。
🧩 谱聚类的基本思想
上一节我们介绍了“谱”的概念,本节中我们来看看什么是谱聚类。
谱聚类本质上是一种基于图论的聚类方法。其核心流程可以概括为以下三步:
- 根据样本数据构建一张图(相似度图)。
- 计算该图的拉普拉斯矩阵。
- 对该矩阵进行特征分解,利用得到的特征向量对样本进行聚类。
接下来,我们将详细展开每一步。
🗺️ 第一步:构建相似度图
给定 N 个样本点 X1 到 XN,我们需要构建一个图来表示它们之间的关系。以下是构建相似度图的关键步骤和常见方法。
相似度矩阵 W
首先,我们需要计算任意两点之间的相似度 sij。相似度与距离成反比,值越大表示两点越相似。由此,我们得到一个 N×N 的相似度矩阵 W。
注意:我们通常强制规定 W 矩阵对角线上的元素 Wii = 0(即自己与自己的相似度为0)。这样做是为了方便后续计算度矩阵。
相似度度量函数
实践中,常用的相似度度量函数是高斯核函数(Radial Basis Function, RBF),其公式如下:
sij = exp(-||xi - xj||² / (2σ²))
其中,σ 是一个带宽参数,控制函数的衰减速度。
图的稀疏化
得到稠密的相似度矩阵后,我们通常需要将其稀疏化,即只保留重要的连接。以下是三种常见方法:
- ε-邻近图:设定一个阈值 ε。如果 sij < ε,则将 Wij 置为0;否则保留。阈值 ε 可以选取所有相似度的均值或图的最小生成树中最大边的权重。
- k-近邻图:对于每个点 i,只保留与它相似度最高的 k 个点的连接,其他连接置0。注意,这种方式构建的图可能不是对称的。
- 互k-近邻图:在k-近邻图的基础上,要求连接是双向的。即点 i 和点 j 必须互为对方的k近邻之一,才保留连接。
实践建议:如果不确定如何选择,通常直接使用 k-近邻图 即可。
🔢 第二步:计算拉普拉斯矩阵
在构建好图(即得到矩阵 W)之后,我们需要计算其拉普拉斯矩阵。这是谱聚类的核心。
度矩阵 D
首先计算度矩阵 D。它是一个对角矩阵,对角线上的元素 Dii 表示第 i 个节点的“度”,即与该节点相连的所有边的权重之和:
Dii = Σj=1N Wij
拉普拉斯矩阵 L 及其变种
最基本的拉普拉斯矩阵是非规范化拉普拉斯矩阵,定义为:
L = D - W
这个矩阵 L 具有一些重要性质:它是对称且半正定的。这意味着它的所有特征值都是非负实数,并且至少有一个特征值为0。
此外,还有两种常用的拉普拉斯矩阵变种:
- 对称拉普拉斯矩阵:Lsym = D-1/2 L D-1/2 = I - D-1/2 W D-1/2
- 随机游走拉普拉斯矩阵:Lrw = D-1 L = I - D-1 W
随机游走拉普拉斯矩阵 的名称来源于其概率解释。D-1W 的每一行元素之和为1,可以看作一个转移概率矩阵,描述了在图上随机游走的模型。
实践建议:如果不知道选择哪一种,优先选择随机游走拉普拉斯矩阵。
🧮 第三步:特征分解与聚类
本节我们将看到如何利用拉普拉斯矩阵的特征向量来完成最终的聚类。
算法步骤
假设我们希望将数据聚成 K 类,以下是谱聚类的标准步骤:
- 计算相似度矩阵 W 和度矩阵 D。
- 计算拉普拉斯矩阵 L(或 Lsym、Lrw)。
- 计算 L 的前 K 个最小的特征值及其对应的特征向量 u1, u2, ..., uK。
- 每个特征向量是 N 维的(N 为样本数)。
- 将这 K 个特征向量按列排列,形成一个 N×K 的矩阵 U:U = [u1 u2 ... uK]。
- 将矩阵 U 的每一行看作一个新的 K 维空间中的样本点 yi(i = 1, ..., N)。
- 对这 N 个新的样本点 {yi} 使用传统的 K-Means 算法进行聚类,得到 K 个簇。
- 将 K-Means 输出的簇标签映射回原始样本点,即完成谱聚类。
物理意义:这个过程可以看作一种降维。我们通过提取拉普拉斯矩阵的前K个特征向量,将原始数据转换到一个新的、更能体现其内在结构的低维空间(K维),然后在这个空间中进行聚类。
如何选择聚类数目 K?
如果事先不知道 K 值,可以通过观察拉普拉斯矩阵的特征值来估计。将特征值从小到大排列,寻找相邻特征值之间“跳跃”最大的位置,跳跃点之前的特征值个数可以作为 K 的参考值。
💻 谱聚类代码实践
理论清晰后,实现起来就非常简单了。以下是谱聚类(以随机游走拉普拉斯矩阵为例)的核心代码逻辑概述:
# 伪代码流程
1. 输入:数据点 X,聚类数 K,近邻数 k
2. 计算相似度矩阵 W (使用k-近邻+高斯核)
3. 计算度矩阵 D (D_ii = sum_j W_ij)
4. 计算拉普拉斯矩阵 L_rw = I - D^(-1) * W
5. 计算 L_rw 的前 K 个最小特征值对应的特征向量,构成矩阵 U (N x K)
6. 将 U 的每一行作为新特征 y_i
7. 对 {y_i} 使用 K-Means 算法,聚类成 K 类
8. 输出聚类结果
可以看到,谱聚类的代码实现主要依赖于矩阵运算和特征值求解,聚类部分直接调用成熟的 K-Means 算法。
⚠️ 注意事项与失败案例
谱聚类并非万能,其效果严重依赖于参数的选择(如高斯核的带宽 σ、近邻数 k、聚类数 K)。参数选择不当会导致聚类失败。
以下是几种常见的失败情况:
- 带宽 σ 过大或过小:会导致相似度度量失真,可能将所有点聚成一类,或产生不合理的分割。
- 近邻数 k 选择不当:可能破坏数据的局部结构,导致无法正确识别簇。
- 聚类数 K 选择错误:显然会得到不符合数据真实分布的聚类结果。
因此,在实际应用中,调参是一个必要且重要的步骤。
📝 课程总结
本节课中,我们一起学习了谱聚类这一强大的基于图论的聚类方法。
我们首先了解了“谱”的概念,然后逐步学习了谱聚类的三个核心步骤:
- 建图:通过计算样本间相似度(如使用高斯核),并利用k-近邻等方法构建稀疏的相似度图。
- 计算拉普拉斯矩阵:介绍了度矩阵 D、非规范化拉普拉斯矩阵 L = D - W 及其两种常用变体(对称型和随机游走型)。
- 特征分解与聚类:通过计算拉普拉斯矩阵前K个最小特征值对应的特征向量,将原始数据映射到新空间,再用K-Means算法完成聚类。
我们还讨论了如何自适应选择聚类数目 K,并简要看了代码实现逻辑。最后,我们指出了谱聚类对参数敏感的特点,需要通过调参来获得最佳效果。


谱聚类与主成分分析在思想上有异曲同工之妙,都是通过提取矩阵的特征向量来发现数据的主要结构。掌握谱聚类,为你处理复杂形状的数据聚类问题提供了一个有力的工具。
人工智能—机器学习公开课(七月在线出品) - P18:世界杯数据分析案例 🏆
在本节课中,我们将学习如何利用Python进行一场真实世界杯比赛的数据分析。我们将从原始数据出发,通过数据清洗、特征工程、统计分析和可视化,一步步揭示比赛背后的故事。
概述与数据准备
上一节我们介绍了数据分析的基本流程,本节中我们来看看如何将其应用到一个具体的体育赛事案例中。
首先,我们需要准备好分析所需的数据和工具。数据来源于2014年世界杯决赛(德国 vs 阿根廷)的JSON格式比赛记录。我们需要将其转换为更易处理的DataFrame格式。
以下是分析前需要完成的准备工作:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
同时,为了更直观地展示球场上的事件,我们预先封装了两个自定义函数:
draw_pitch(): 用于绘制一个标准足球场的背景图,包括禁区、中场线等。draw_event(): 用于在球场图上用箭头等形式绘制具体事件(如传球、射门)。
数据预处理与特征工程
在开始分析之前,我们需要对原始数据进行处理,并创建一些有助于分析的新特征。
我们从CSV文件中读取比赛数据,并基于足球场的实际尺寸进行坐标缩放,确保可视化时的比例正确。
以下是我们在原始数据基础上创建的一些核心特征:
distance: 计算事件发生位置到球门的距离。公式为distance = sqrt(x**2 + y**2)。to_box/from_box: 计算事件发生位置到己方或对方禁区的距离。on_offense: 判断球队是否处于进攻阶段。这是一个二值特征,当球员位置越过中场线进入对方半场时,值为1(进攻),否则为0(防守)。
这些特征将帮助我们量化球队的攻势、控球优势以及威胁程度。
上半场比赛分析
现在,让我们聚焦于上半场的比赛情况。我们首先通过可视化来直观感受双方的攻防态势。
我们绘制了上半场的攻势分布图。图中,纵坐标大于0的区域代表德国队的进攻时段,小于0的区域代表阿根廷队的进攻时段,并用箭头标记了所有射门事件。

从图中可以清晰地看到,上半场蓝色区域(德国进攻)占据了绝对主导,表明德国队大部分时间都将比赛压制在阿根廷的半场。
为了用数据证实这一观察,我们进行以下统计:
以下是德国与阿根廷在上半场的关键数据对比:

- 进攻时长占比:对
on_offense特征求平均值,得到各队处于进攻状态的概率。- 德国队:61.6%
- 阿根廷队:27.3%
- 传球成功率:对传球事件的
outcome(成功为1,失败为0)求平均值。- 德国队:83.5%
- 阿根廷队:69.4%
数据证实了德国队在上半场拥有显著的控球和进攻优势。
我们还可以通过绘制传球事件图来获得更细致的洞察。下图展示了上半场所有的传球,箭头方向代表传球方向。

从图中可见,绝大多数传球(箭头)都发生在阿根廷的半场(右侧),且德国队有多次传球深入阿根廷禁区,而阿根廷队则很难将球传入德国队的危险区域。
关键事件深度分析:克拉默受伤的影响
数据分析不仅在于描述现象,更在于挖掘原因。上半场中段,德国队球员克拉默受伤,但直到约12分钟后才被换下。我们通过数据来探究这期间的影响。
我们提取了克拉默受伤前后个人事件的数据表,并绘制了其活动统计柱状图。



数据显示,在受伤后的12分钟里,克拉默仅有一次传球且以丢失球权告终,其活动频率急剧下降。这导致德国队在该时段内相当于少打一人,攻势受挫,这与整体攻势图中出现的短暂“低谷期”相吻合。


下半场及加时赛分析
接下来,我们以同样的方法分析下半场和加时赛。
下半场的攻势分布图显示,双方态势趋于均衡,阿根廷队有了更多攻入德国半场的机会。

统计数据也支持这一观察:
- 进攻时长占比:德国 59.1%,阿根廷 40.9%。
- 传球成功率:德国 83.0%,阿根廷 81.1%。
进入加时赛后,局势又有新的变化。下图展示了加时赛阶段的传球与射门事件。




分析发现,德国队在加时赛进球后,明显减少了进攻尝试和向禁区的传球,转而采取控制节奏、消耗时间的策略。而阿根廷队虽奋力反扑,但仅有的两次射门(图中红色箭头)均来自禁区外,未能构成绝对威胁,最终未能扳平比分。
球员个人表现统计

最后,我们可以对球员的个人表现进行汇总分析。



以下是生成球员数据统计表的方法:


- 按球员姓名对数据进行分组 (
groupby)。 - 聚合计算每个球员的总事件数、传球次数、成功率、射门次数等指标。
- 将结果以表格形式呈现,便于横向比较各球员的贡献和效率。

这种分析可以帮助识别关键球员、评估战术执行效果,或为未来的比赛部署提供参考。

总结
本节课中我们一起学习了如何对一个完整的体育赛事数据集进行端到端的分析。
我们从数据预处理和特征工程开始,创建了用于量化比赛态势的特征。接着,我们通过绘制攻势时序图和事件散点图进行可视化探索,直观揭示了德国队在上半场的压倒性优势。然后,我们利用分组统计验证了视觉观察,并通过深挖关键事件(球员受伤)的数据,展示了如何结合背景知识进行归因分析。最后,我们将分析方法应用于比赛的其他阶段,并扩展到球员个人表现的评估。


这个案例表明,数据分析不仅仅是冰冷的数字,结合领域知识和恰当的可视化,它能够生动地讲述数据背后的故事,为决策提供有力支持。

人工智能—机器学习公开课(P19):特征处理与特征选择 🧠
在本节课中,我们将要学习特征工程中的核心环节:特征处理与特征选择。我们将探讨如何从原始数据中提取、构造和筛选出对模型最有价值的特征,以提升机器学习模型的性能与效率。
概述 📋
特征工程是机器学习项目成功的关键。原始数据通常不能直接用于模型训练,需要通过一系列处理转化为模型能够理解的有效特征。本节课将系统介绍统计特征的构造、组合特征的生成以及特征选择的方法。
上一节我们介绍了特征的基本概念与类型,本节中我们来看看如何具体处理和选择这些特征。
统计特征 📊
统计特征是在各类数据竞赛和工业界业务中贴合度非常高的特征。它们通过对原始数据进行统计计算得到,能够有效反映数据的内在规律。
以下是几种常见的统计特征类型:
- 加减平均:例如,用户购买商品价格高于全体用户平均价格的差值,可以衡量其消费能力。
- 分位数:例如,商品价格处于售出商品价格序列的某个分位点,可以体现购买力。
20%分位数意味着20%的商品价格低于此值。 - 排序型:例如,商品在类别中的热度排名。
- 比例型:例如,用户某项行为数占总行为数的比例。
实例分析:电商推荐大赛
在一个电商推荐算法大赛中,参赛者基于用户行为表和商品属性表构造了大量有效的统计特征。
以下是他们构造的部分特征示例:
- 用户维度统计特征:例如,
用户购物车购买转化率、用户日均行为数。 - 商品维度统计特征:例如,
商品热度(点击/收藏/加购/购买次数)、商品曝光购买转化率。 - 时间窗口统计特征:例如,
最近7天用户行为数与平均行为数的比值、商品近期销量变化趋势。 - 比值型特征:例如,
用户对某品牌购买数除以收藏数、商品行为数除以同类商品平均行为数。
这些特征从连续值、离散值、时间维度等多个角度对数据进行刻画,为模型提供了丰富的信息。
组合特征 🔗
单一特征有时信息有限,将多个特征组合起来能产生更强的表达能力。组合特征主要分为两类。
1. 拼接型组合特征
这是最简单直接的方式,尤其在逻辑回归等线性模型中使用。它将两个或多个离散特征的值直接拼接,作为一个新的特征。
例如,将用户ID和商品品类ID拼接:
特征 = 用户ID_品类ID
如果用户10001购买了女士裙装,则该特征值为1,否则为0。模型学习到的权重直接反映了用户对该品类组合的偏好。
2. 基于模型的特征组合
以Facebook提出的GBDT + LR方法为例。首先使用梯度提升树模型自动进行特征组合与筛选。
# 概念性伪代码
# GBDT模型会生成多棵决策树
tree_path = GBDT.predict_path(X)
# 每一条从根到叶子的路径代表一种特征组合规则
# 例如:规则 = (性别=男) AND (城市=上海) AND (设备=手机)
# 将该规则编码为一个新特征
new_feature = encode(tree_path)
# 将所有新特征输入逻辑回归模型
lr_model.fit(new_feature, y)
GBDT模型学习到的每一条决策路径,都代表了一种有效的特征交叉组合。将这些路径作为新的离散特征输入逻辑回归,能显著提升模型效果。
特征选择 🎯
特征过多会导致计算效率下降、模型复杂度过高,并可能引入噪声。特征选择的目标是从原始特征集中筛选出一个最优子集。
特征选择 vs. 降维
- 特征选择:从N个特征中挑选出K个特征,不改变特征原始含义。例如,从50个特征中选出30个最重要的。
- 降维:通过数学变换将N个特征转换为M个新特征,通常会改变原始特征的含义和解释性。例如主成分分析。
特征选择方法
1. 过滤法
过滤法独立于任何机器学习模型,它根据特征的统计指标(如与目标变量的相关性)进行排序和选择。
以下是两种常用的过滤法函数:
- SelectKBest:选择与目标变量相关性最高的K个特征。
from sklearn.feature_selection import SelectKBest selector = SelectKBest(k=2) X_new = selector.fit_transform(X, y) - SelectPercentile:选择与目标变量相关性最高的前百分之N的特征。
from sklearn.feature_selection import SelectPercentile selector = SelectPercentile(percentile=50) # 选择前50% X_new = selector.fit_transform(X, y)
优点:计算快,易于理解。
缺点:未考虑特征间的相互关系,可能漏选有组合价值的特征。
2. 包裹法
包裹法将特征选择过程与模型训练相结合,通过评估不同特征子集对模型性能的影响来进行选择。递归特征消除是常用方法。
递归特征消除步骤:
- 用全部特征训练模型。
- 根据模型系数(如线性模型的权重)排序,移除权重最小的一个或一组特征。
- 用剩余的特征重复步骤1和2。
- 直到特征数量达到预设值,或模型性能出现显著下降。
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
selector = RFE(estimator=model, n_features_to_select=5)
X_new = selector.fit_transform(X, y)
优点:考虑特征组合效应,选出的特征子集通常性能更优。
缺点:计算开销大,尤其对于特征数量多的情况。
3. 嵌入法
嵌入法将特征选择作为模型训练过程的一部分。最常见的是在线性模型中使用L1正则化。
- L1正则化:在损失函数中加入模型权重的绝对值之和作为惩罚项。这会导致不重要的特征对应的权重被压缩为精确的0,从而实现特征选择。
损失函数 = 原始损失 + λ * Σ|权重| - L2正则化:惩罚项是权重的平方和。它会使权重整体缩小,但很少恰好为0。
from sklearn.feature_selection import SelectFromModel
from sklearn.svm import LinearSVC
# 使用带有L1正则化的线性模型作为基评估器
lsvc = LinearSVC(C=0.01, penalty="l1", dual=False).fit(X, y)
model = SelectFromModel(lsvc, prefit=True)
X_new = model.transform(X)
优点:在模型训练的同时完成特征选择,效率高于包裹法。
缺点:依赖于特定的模型(通常是线性模型)。
总结 🎓
本节课中我们一起学习了特征处理与特征选择的核心内容。
我们首先介绍了如何从业务角度构造统计特征,包括均值、分位数、比值等,这些是构建有效特征的基础。接着,我们探讨了组合特征的威力,无论是简单的特征拼接还是利用GBDT等模型自动发现特征交叉,都能显著提升模型表现。
最后,我们深入讲解了特征选择的三大方法:过滤法快速但粗糙,包裹法精确但耗时,嵌入法(特别是结合L1正则化)则在效率和效果之间取得了很好的平衡,是工业界常用的方法。


特征工程是一门艺术,需要结合领域知识、数据洞察和反复实验。掌握这些方法,能帮助你从数据中提炼出黄金,为构建强大的机器学习模型奠定坚实的基础。

人工智能—机器学习公开课(七月在线出品) - P2:18分钟理解EM算法

在本节课中,我们将要学习EM算法的核心思想与推导过程。EM算法是一种用于处理含有隐变量的概率模型参数估计的有效方法。我们将从基本概念出发,逐步推导其数学原理,并最终理解其迭代框架。
🎼 EM算法的基本思路
上一节我们介绍了EM算法要解决的问题。本节中我们来看看它的基本思路。
EM算法处理的是含有隐变量 Z 的模型参数估计问题。例如,在身高数据中,X 是观测到的身高,Z 是未观测到的性别。我们的目标是找到模型参数 θ,使得观测数据 X 的似然函数 P(X|θ) 最大。
由于直接最大化包含隐变量的似然函数 P(X|θ) 很困难,EM算法采用了一种迭代优化的策略。其核心思想是:如果能找到一个函数,它是原对数似然函数的下界,并且这个下界函数更容易求极大值,那么通过不断优化这个下界函数,就能间接地提升原对数似然函数的值,最终收敛到一个局部最优解。
📈 数学推导:构建下界函数
上一节我们了解了EM算法的优化思路。本节中我们通过数学推导来具体实现它。
首先,我们写出包含 M 个独立样本的对数似然函数:
L(θ) = Σ_{i=1}^{M} log P(x_i | θ)
由于 P(x_i | θ) 是边缘分布,我们引入隐变量 Z 将其写成联合分布的形式:
P(x_i | θ) = Σ_{z} P(x_i, z | θ)
因此,对数似然函数变为:
L(θ) = Σ_{i=1}^{M} log [ Σ_{z} P(x_i, z | θ) ]
直接优化这个式子很困难。EM算法的关键一步是为第 i 个样本的隐变量 z 引入一个分布 Q_i(z)。我们可以在对数内部乘除 Q_i(z):
L(θ) = Σ_{i=1}^{M} log [ Σ_{z} Q_i(z) * (P(x_i, z | θ) / Q_i(z)) ]
现在,我们将 Σ_{z} Q_i(z) * [P(x_i, z | θ) / Q_i(z)] 看作随机变量 P/Q 在分布 Q_i(z) 下的期望。根据Jensen不等式,由于 log 函数是凹函数,我们有:
log( E[·] ) ≥ E[ log(·) ]
应用此不等式:
L(θ) = Σ_{i=1}^{M} log( E_{z~Q_i}[P(x_i, z | θ) / Q_i(z)] )
≥ Σ_{i=1}^{M} E_{z~Q_i}[ log( P(x_i, z | θ) / Q_i(z) ) ]
= Σ_{i=1}^{M} Σ_{z} Q_i(z) log( P(x_i, z | θ) / Q_i(z) )
我们定义这个下界函数为 J(Q, θ):
J(Q, θ) = Σ_{i=1}^{M} Σ_{z} Q_i(z) log( P(x_i, z | θ) ) - Σ_{i=1}^{M} Σ_{z} Q_i(z) log( Q_i(z) )
这样,我们就成功地将难以优化的 L(θ) 转换为了相对容易处理的下界函数 J(Q, θ)。
🔍 何时取等号与Q的选择
上一节我们构建了下界函数 J(Q, θ)。本节中我们探讨如何选择分布 Q 以使这个下界尽可能紧。
根据Jensen不等式,等号成立的条件是随机变量 P(x_i, z | θ) / Q_i(z) 为常数。这意味着:
P(x_i, z | θ) / Q_i(z) = C (C 为常数)
由此可得 Q_i(z) 与 P(x_i, z | θ) 成正比。同时,Q_i(z) 是一个概率分布,需要满足 Σ_{z} Q_i(z) = 1。结合这两个条件,我们可以解出 Q_i(z):
Q_i(z) = P(x_i, z | θ) / Σ_{z} P(x_i, z | θ) = P(x_i, z | θ) / P(x_i | θ) = P(z | x_i, θ)
这个结果具有清晰的统计意义:最优的 Q_i(z) 就是在给定观测样本 x_i 和当前参数 θ 的条件下,隐变量 z 的后验概率分布。
因此,EM算法中的 E步(期望步) 就是固定参数 θ,计算隐变量的后验分布 Q_i(z) = P(z | x_i, θ)。
🔄 EM算法的完整迭代框架
上一节我们确定了E步中 Q 函数的选择。本节中我们来看完整的EM算法迭代流程。
以下是EM算法的标准步骤:
1. 初始化参数 θ
随机或根据先验知识初始化模型参数 θ^{old}。
2. E步(Expectation)
固定参数 θ^{old},对于每一个样本 i,计算隐变量 z 的后验分布:
Q_i(z) = P(z | x_i, θ^{old})
这步计算的是在当前参数下,每个观测数据对应的隐变量的“责任”或“期望”。
3. M步(Maximization)
固定分布 Q_i(z),最大化下界函数 J(Q, θ) 以更新参数:
θ^{new} = argmax_{θ} J(Q, θ) = argmax_{θ} Σ_{i=1}^{M} Σ_{z} Q_i(z) log P(x_i, z | θ)
注意,在最大化时,J(Q, θ) 中与 θ 无关的项 -ΣΣ Q_i(z) log Q_i(z) 可以忽略。M步的目标是找到使“完全数据” (x, z) 的期望对数似然最大的新参数。
4. 检查收敛
比较 θ^{new} 与 θ^{old},或者比较对数似然函数 L(θ) 的变化。如果未收敛,则令 θ^{old} = θ^{new},返回第2步(E步)继续迭代。
🏁 总结
本节课中我们一起学习了EM算法的核心原理。
- 问题:EM算法用于解决含有隐变量的概率模型参数估计问题。
- 思路:通过引入隐变量的分布
Q,构建原对数似然函数的一个下界J(Q, θ),然后通过交替优化Q(E步) 和θ(M步) 来间接优化原目标。 - E步:固定
θ,计算隐变量的后验分布Q(z) = P(z|x, θ),这使下界J(Q, θ)紧贴原函数。 - M步:固定
Q,最大化下界函数J(Q, θ)来更新参数θ。 - 流程:初始化参数后,循环执行E步和M步,直至收敛。
EM算法通过这种巧妙的“坐标上升”法,将复杂的含隐变量问题分解为两个可求解的子步骤,是机器学习中一个非常重要且强大的工具。





人工智能—机器学习公开课(七月在线出品) - P20:微分流形在机器学习中的应用 👨🏫
在本节课中,我们将要学习微分流形在机器学习中的应用。主要内容包括流行学习的基本概念、传统方法,以及从拓扑学角度理解深度学习的工作原理与局限性。
第一部分:线性降维方法回顾 📉
上一节我们介绍了课程概述,本节中我们来看看最常见的线性降维方法。
我们通常输入的数据是每个数据点包含多个特征(features)的向量。输入的数据矩阵通常有N个数据点,每个数据点是一个高维向量。主成分分析(PCA)是常用的降维方法。
PCA的核心思想是利用数据的协方差矩阵来寻找数据中最重要的方向(主成分)。它将每个高维数据点投影到选出的前K个主成分所张成的低维子空间上,从而得到降维后的数据表示。
以下是PCA的简要步骤:
- 计算数据集的协方差矩阵。
- 计算协方差矩阵的特征值和特征向量。
- 选取前K个最大特征值对应的特征向量作为主成分。
- 将原始数据投影到这K个主成分上,得到降维后的数据。
然而,PCA是一种线性方法,它难以揭示数据中可能存在的非线性结构。
第二部分:流行学习简介 🌀
上一节我们介绍了线性降维的局限性,本节中我们来看看非线性降维方法——流行学习。
流行学习基于一个核心假设:我们观测到的高维数据点,实际上集中在一个嵌入在高维空间中的、维数更低的子流形上。这个“流行假设”认为,数据的内在结构是低维的。
例如,手写数字的所有可能图像构成了所有可能图像空间中的一个子集,这个子集更接近于一个流形,而非随机散点。流行学习的目标就是揭示数据背后的这个低维流形结构。
等距映射(Isomap)
第一个要介绍的方法是等距映射(Isomap)。它的核心思想是利用流形上的测地距离(即流形表面两点间的最短路径长度),而非高维空间中的欧氏距离,来重构流形结构。
以下是Isomap算法的流程:
- 构建邻域图:对每个数据点,找到其邻近点(例如K近邻或ε-邻域),并在邻近点之间连接边,边的权重为欧氏距离,从而形成一个邻域图。
- 计算测地距离:在邻域图上,使用图论中的最短路径算法(如Dijkstra算法)计算图中任意两点间的最短路径长度,以此作为测地距离的近似。
- 多维缩放(MDS):将上一步得到的测地距离矩阵作为输入,应用MDS算法,寻找一个低维嵌入,使得嵌入空间中的点间欧氏距离尽可能接近输入的测地距离。
Isomap通过局部邻域信息来估计全局的流形结构,但最后一步的MDS计算复杂度较高。
局部线性嵌入(LLE)
第二个方法是局部线性嵌入(LLE)。它的思想与Isomap不同,不是基于距离,而是基于局部线性关系。
LLE假设流形在局部是近似线性的,即一个数据点可以由其邻近点的线性组合来重构。算法目标是保持这种局部线性重构关系在降维后不变。
以下是LLE算法的核心步骤:
- 寻找邻域:与Isomap类似,为每个数据点确定其K个最近邻。
- 计算局部重构权值矩阵:对每个点,计算其如何由其邻域点线性表出的权值系数,使得重构误差最小。这通过求解一个局部最小二乘问题完成。
- 计算低维嵌入:在低维空间中寻找一组点,使得它们能用同样的权值系数从其低维邻域点重构出来,即保持第二步计算出的权值矩阵不变。

拉普拉斯特征映射(Laplacian Eigenmaps)

第三个方法是拉普拉斯特征映射。它从另一个角度出发,基于流形上的拉普拉斯算子的特征函数来寻找低维表示。
其核心思想是:在降维后,希望保留数据点之间的局部邻近关系。关系紧密的点在低维空间中也应该靠近。
以下是该方法的简要过程:
- 构建邻域图:同样先构建一个邻域图(如K近邻图)。
- 构建图拉普拉斯矩阵:根据邻域图构建拉普拉斯矩阵。
- 特征值分解:求解广义特征值问题,选取最小的几个非零特征值对应的特征向量,这些特征向量就构成了数据在低维空间中的坐标。
第三部分:深度学习与拓扑学视角 🔗
上一节我们介绍了多种流行学习方法,本节中我们来看看如何从拓扑学角度理解深度学习。
深度学习模型,如多层感知机(MLP),通过叠加“线性变换+非线性激活函数”的层来工作。一个没有隐藏层的单层网络等价于一个线性分类器。
当我们增加网络层数(深度)时,模型引入了非线性,从而能够学习更复杂的决策边界。从拓扑视角看,深度网络的前几层可以被视为对输入空间进行一系列“同胚变换”(连续且可逆的形变,如拉伸、压缩、弯曲,但不撕裂或粘连)。
这种变换的目的是将原本在输入空间中线性不可分的不同类别数据所对应的流形,逐步“解套”或展开,使得在最后一个隐藏层的输出空间中,这些类别变得线性可分。然后,输出层只需一个简单的线性分类器即可完成完美分类。
然而,这种基于窄宽度网络的同胚变换方法存在根本性局限。对于某些拓扑结构复杂的数据(例如一个点被一个环包围,或两个类别像纽结一样纠缠在一起),无论如何进行同胚变换,都无法将它们变为线性可分。


解决此局限性的一个方法是增加网络的宽度。通过将数据映射到更高维的空间(例如从二维映射到三维),有可能找到一个新的视角,使得原本纠缠的类别变得线性可分。




开放性问题与总结 💡
本节课中我们一起学习了微分流形在机器学习中的应用。我们回顾了线性降维的不足,介绍了流行学习的核心思想与几种经典方法(Isomap, LLE, Laplacian Eigenmaps)。接着,我们从拓扑学角度探讨了深度学习的工作原理及其在窄宽度下的局限性。
这引发出一些开放性问题:
- 能否结合流行学习来改进现有的深度学习架构,使其更高效或更容易训练?
- 能否借助流行学习来增强深度学习模型的可解释性,例如理解生成模型的内部表示?
- 能否利用强大的深度学习模型来改进流行学习的效果?
- 基于这些理解,我们能否设计出全新的、更强大的机器学习模型架构?

对这些问题的探索,将有助于我们更深入地理解智能的本质,并推动人工智能技术的进一步发展。

人工智能—机器学习公开课(七月在线出品) - P21:用pandas完成机器学习数据预处理与特征工程 🛠️
在本节课中,我们将要学习如何使用pandas库进行数据预处理与特征工程,重点掌握数据的分组与合并操作。这些操作是数据分析和机器学习流程中整理、汇总数据的关键步骤。
数据分组与合并 📊
上一节我们介绍了数据的基本操作,本节中我们来看看如何对数据进行分组与合并。分组与合并是针对包含多行多列的数据表进行的操作。
你可以简单地理解为,当我们有一个很大的数据表格,数据量过多时,可以先将数据拆分成组进行处理,这类似于SQL中的GROUP BY操作。处理完成后,最终需要将结果合并成一个新的DataFrame。
以下是创建一个示例DataFrame的代码:
import pandas as pd
data = {
'name': ['老王', '老王', '老王', '老王', '老宋', '老宋', '老宋', '容妹'],
'year': [2016, 2016, 2017, 2017, 2016, 2016, 2017, 2017],
'salary': [10000, 12000, 11000, 13000, 9000, 9500, 10000, 15000],
'bonus': [2000, 2500, 2200, 2300, 1800, 1900, 2000, 3000]
}
df = pd.DataFrame(data)
print(df)
当我们拿到这样的数据时,通常需要进行统计。例如,我们想计算“老王”在2016年和2017年的总工资。这时,我们可以先按姓名进行分组。
使用groupby进行分组
分组操作非常简单,直接调用groupby()函数即可。如果你了解SQL,groupby相当于对某一列进行汇总。
以下是按name列进行分组的代码:
grouped = df.groupby('name')
print(type(grouped)) # 输出:<class 'pandas.core.groupby.generic.DataFrameGroupBy'>
这行代码创建了一个DataFrameGroupBy对象,它将相同姓名的数据聚合在一起,不同姓名的数据分开。单纯的分组对象没有太多意义,我们需要在此基础上进行后续操作。
分组后的聚合操作
分组后,我们可以进行各种聚合计算,例如求和。
以下是按姓名分组后求和的代码:
sum_result = grouped.sum()
print(sum_result)
调用sum()方法会对分组后各列的数据进行累加。但请注意,像year这样的列进行累加可能没有实际意义,此处仅为演示。
groupby函数内部有许多参数。例如,默认情况下分组后会排序,你可以通过设置sort=False来取消排序。
除了sum(),还可以使用aggregate()函数(或其简写agg())进行聚合,这个函数功能非常强大。
以下是使用aggregate()进行求和的代码:
sum_result_agg = grouped.aggregate('sum')
print(sum_result_agg)
aggregate()的第一个参数func非常灵活,可以是函数名、字符串、列表或字典。例如,传入字符串'sum'与直接调用sum()函数效果相同。
以下是使用不同聚合函数的示例:
# 求平均值
mean_result = grouped.aggregate(np.mean)
print(mean_result)
# 求标准差
std_result = grouped.aggregate(np.std)
print(std_result)
查看分组详情
我们可以查看分组对象的一些属性来了解数据。
以下是查看分组详情的方法:
# 查看每个姓名对应的原始行索引位置
print(grouped.groups)
# 查看有多少个不同的组(即有多少个不同的姓名)
print(len(grouped))
describe()方法可以快速提供每个组的描述性统计摘要,这在数据分析初期非常有用。
以下是使用describe()的代码:
desc_result = grouped.describe()
print(desc_result)
describe()会显示计数(count)、平均值(mean)、标准差(std)、最小值(min)、最大值(max)以及四分位数(25%, 50%, 75%)。
按多列分组与迭代分组对象
分组不仅可以基于单列,还可以基于多列。
以下是按name和year两列进行分组的代码:
grouped_multi = df.groupby(['name', 'year'])
sum_multi = grouped_multi.sum()
print(sum_multi)
我们也可以迭代分组对象,对每个组进行单独处理。
以下是迭代分组对象的代码:
for name, group in df.groupby('name'):
print(f"姓名: {name}")
print(group)
print("-" * 20)
通过get_group()方法可以获取指定的组,每个组本身都是一个DataFrame。
以下是获取特定组的代码:
laosong_group = grouped.get_group('老宋')
print(laosong_group)
print(type(laosong_group)) # 输出:<class 'pandas.core.frame.DataFrame'>
对特定列进行聚合操作
我们可以选择只对DataFrame中的特定列进行聚合操作。
以下是对salary和bonus列求和的代码:
agg_specific = grouped.agg({'salary': 'sum', 'bonus': 'sum'})
print(agg_specific)
如果想在结果中保留year列(但不聚合),可以使用匿名函数。
以下是保留year列第一项的代码:
agg_with_year = grouped.agg({
'salary': 'sum',
'bonus': 'sum',
'year': lambda x: x.iloc[0] # 取该组第一个年份
})
print(agg_with_year)
transform 操作
transform方法非常灵活,它返回一个与原始数据形状相同的对象,对每个组内的所有记录应用相同的转换规则。
假设我们有一个股票数据集nvda.csv,包含日期(Date)、开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close)等。
以下是读取数据并按年份计算平均收盘价的代码:
nvda_df = pd.read_csv('nvda.csv', index_col='Date', parse_dates=['Date'])
nvda_df['Year'] = nvda_df.index.year
yearly_avg = nvda_df.groupby('Year')['Close'].mean()
print(yearly_avg)
使用transform可以计算每个数据点与其所在年份平均值的Z-score(标准化分数)。
Z-score的公式为:
[
Z = \frac{x - \mu}{\sigma}
]
其中,(x)是原始值,(\mu)是组内均值,(\sigma)是组内标准差。
以下是使用transform计算Z-score的代码:
def z_score(x):
return (x - x.mean()) / x.std()
nvda_df['Close_Z'] = nvda_df.groupby('Year')['Close'].transform(z_score)
print(nvda_df[['Close', 'Close_Z']].head())
apply是一个更通用的方法,自由度很高,但transform在组内转换时更常用。
filter 操作
filter方法用于过滤分组。它根据自定义的条件判断是否保留整个组。
以下是一个简单的过滤示例,保留总和大于4的组:
s = pd.Series([1, 2, 2, 3, 4, 4, 5])
grouped_s = s.groupby(s)
filtered = grouped_s.filter(lambda x: x.sum() > 4)
print(filtered)
对于DataFrame,我们可以根据组的某些属性进行过滤。例如,过滤出股票数据中年平均收盘价超过100的年份的所有数据。
以下是过滤股票数据的代码:
def filter_func(group):
# 判断该年份的平均收盘价是否大于100
return group['Close'].mean() > 100

filtered_df = nvda_df.groupby('Year').filter(filter_func)
print(filtered_df['Year'].unique()) # 打印出哪些年份被保留了
总结 📝
本节课中我们一起学习了pandas中数据分组与合并的核心操作。

我们掌握了如何使用groupby将数据拆分成组,这是“拆分”步骤。接着,我们学习了多种“应用”操作:
aggregate/agg: 用于对组进行聚合计算(如求和、求平均)。transform: 用于对组内每个元素进行转换,并保持原始形状。filter: 用于根据条件过滤整个组。


最后,pandas会自动将处理后的结果“组合”起来,形成新的数据结构。理解并熟练运用“拆分-应用-组合”这一模式,是进行高效数据预处理和特征工程的关键。
人工智能—机器学习公开课(七月在线出品) - P22:最大熵模型原理推导
📚 课程概述
在本节课中,我们将要学习最大熵模型的基本原理和数学推导过程。最大熵模型是一种基于最大熵原理的概率模型,它在满足给定约束条件的模型集合中,选择熵最大的模型作为最优模型。我们将从基本概念出发,逐步推导出其数学形式,并理解其背后的优化思想。
🔗 模型关联与预备知识
最大熵模型在模型关联图中占有特定位置,主要用于后续条件随机场(CRF)模型的学习问题。在开始之前,我们需要回顾两个重要的概率计算规则。
加法规则与乘法规则
以下是两个在概率计算中频繁使用的基础规则。
- 加法规则:边缘概率与联合概率的关系。公式为
P(X) = Σ_Y P(X, Y)。它有两个使用方向:一是将边缘概率展开为联合概率的求和形式;二是将联合概率通过求和“还原”为边缘概率。 - 乘法规则:联合概率与条件概率、边缘概率的关系。公式为
P(X, Y) = P(Y|X) * P(X)。它同样有两个使用方向:一是将联合概率分解为条件概率和边缘概率的乘积;二是将条件概率和边缘概率的乘积“吸收”为联合概率。
🧠 最大熵原理与模型定义
上一节我们介绍了基础的概率规则,本节中我们来看看最大熵模型的核心思想——最大熵原理。
最大熵原理是一种方法论或价值观,它为如何选择“最优”概率模型提供了指导原则。该原理认为:在所有满足已知约束条件的概率模型中,熵最大的模型是最好的模型。
熵的定义
在信息论中,对于一个离散随机变量 X,其概率分布为 P(X),该分布的熵 H(P) 定义为:
H(P) = - Σ_X P(x) log P(x)
熵的取值范围是 0 ≤ H(P) ≤ log |X|,其中 |X| 是 X 可能取值的个数。当 X 在所有取值上等概率分布时,熵取得最大值 log |X|,此时随机变量的不确定性最大。
最大熵模型的形式化
有了熵的概念,我们可以形式化地定义最大熵模型。
- 输入与输出:设
X和Y分别代表输入和输出的集合。 - 模型形式:模型以条件概率
P(Y|X)的形式给出,表示在给定输入X的条件下,输出Y的概率。 - 训练数据:设有训练数据集
T = {(x1, y1), (x2, y2), ..., (xN, yN)}。 - 经验分布:我们可以从数据集中得到经验(频率)分布。
- 联合经验分布:
˜P(X=x, Y=y) = count(x, y) / N - 边缘经验分布:
˜P(X=x) = count(x) / N
- 联合经验分布:
- 特征函数:特征函数
f(x, y)是一个二值函数,用于描述x和y之间满足的某一事实。如果满足,则f(x, y)=1;否则为0。 - 约束条件——期望相等:如果模型能够从训练数据中学习到信息,那么特征函数
f关于模型分布P(Y|X)和经验分布˜P(X)的期望,应该等于f关于经验联合分布˜P(X, Y)的期望。- 模型期望:
E_P(f) = Σ_{x,y} ˜P(x) P(y|x) f(x, y) - 经验期望:
E_˜P(f) = Σ_{x,y} ˜P(x, y) f(x, y) - 约束条件:
E_P(f) = E_˜P(f)
- 模型期望:
通常我们有 n 个特征函数 f_i (i=1,2,...,n),每个特征函数都对应一个上述的期望相等约束。
⚖️ 约束优化问题
上一节我们定义了模型和约束条件,本节中我们来看看如何将最大熵模型表述为一个约束优化问题。
所有满足这 n 个约束条件的条件概率分布 P(Y|X) 构成一个集合,记作 C。集合 C 中的模型通常不止一个。
根据最大熵原理,我们从集合 C 中选择熵最大的模型作为最优模型。条件分布的熵定义为:
H(P) = - Σ_{x,y} ˜P(x) P(y|x) log P(y|x)
因此,最大熵模型等价于求解以下带约束的最优化问题:
最大化:H(P) = - Σ_{x,y} ˜P(x) P(y|x) log P(y|x)
约束条件:
E_P(f_i) = E_˜P(f_i), i=1,2,...,nΣ_y P(y|x) = 1(概率归一化条件)
为了方便求解,我们通常将最大化问题转化为等价的最小化问题,即最小化 -H(P)。
🧮 拉格朗日乘子法求解
面对带约束的优化问题,我们使用拉格朗日乘子法进行求解。其步骤分为三步:构建拉格朗日函数、求极小、求极大。
第一步:构建拉格朗日函数
我们为 n 个特征函数约束和1个概率归一化约束分别引入拉格朗日乘子 w_1, w_2, ..., w_n 和 w_0。
拉格朗日函数 L(P, w) 定义为原目标函数 -H(P) 加上所有约束项(乘以各自的乘子):
L(P, w) = -H(P) + w_0 (1 - Σ_y P(y|x)) + Σ_{i=1}^n w_i (E_˜P(f_i) - E_P(f_i))
将 H(P) 和期望展开后,得到具体表达式。
第二步:对模型 P 求极小
我们将拉格朗日函数对 P(y|x) 求偏导,并令其等于 0。
∂L / ∂P(y|x) = 0
经过详细的求导和化简(过程涉及对数求导和乘法规则),我们可以解出最优条件概率分布 P_w(y|x) 具有如下形式:
P_w(y|x) = exp( Σ_{i=1}^n w_i f_i(x, y) ) / Z_w(x)
其中,Z_w(x) = Σ_y exp( Σ_{i=1}^n w_i f_i(x, y) ) 是归一化因子,称为配分函数。
这个形式非常重要,它表明最大熵模型是指数族分布的一种。
第三步:对乘子 w 求极大
将第二步得到的最优 P_w(y|x) 代回拉格朗日函数 L(P, w),此时函数仅是关于 w 的函数,记作 Ψ(w)。
Ψ(w) = L(P_w, w)
原问题转化为求解 Ψ(w) 的极大值:
max_w Ψ(w)
求解此极大化问题,即可得到最终的模型参数 w*。通常使用改进的迭代尺度法(IIS)或拟牛顿法等数值优化方法进行求解。
🔄 与极大似然估计的等价性
一个关键的结论是:对偶函数 Ψ(w) 的极大化,等价于最大熵模型的极大似然估计。
证明思路是构造模型 P_w(y|x) 的对数似然函数 L_˜P(P_w),并证明 Ψ(w) = L_˜P(P_w)。
对数似然函数为:
L_˜P(P_w) = Σ_{x,y} ˜P(x, y) log P_w(y|x)
通过代入 P_w(y|x) 的表达式并进行推导,可以证明 Ψ(w) 与 L_˜P(P_w) 具有完全相同的数学形式。


因此,求解最大熵模型参数 w 的问题,最终转化为求解该模型在训练数据上的极大似然估计问题。这为使用成熟的优化算法(如IIS)提供了理论依据。
📝 课程总结
本节课中我们一起学习了最大熵模型的完整原理推导。
- 核心思想:我们首先学习了最大熵原理,即在所有满足已知约束的模型中,选择不确定性最大(熵最大)的模型作为最优模型。
- 问题形式化:我们将该思想形式化为一个带约束(特征函数期望相等)的优化问题。
- 求解过程:通过拉格朗日乘子法,我们将约束优化问题转化为先对模型求极小、再对乘子求极大的对偶问题。求解极小值得到了模型优美的指数形式
P_w(y|x) = exp( Σ w_i f_i ) / Z_w(x)。 - 最终求解:极大化对偶函数
Ψ(w)以获得参数w,并证明了该过程等价于对模型的极大似然估计,从而可以通过IIS等优化算法求解。


最大熵模型为概率模型学习提供了一个清晰的原则性框架,也是理解更复杂模型(如条件随机场)的重要基础。其推导过程中综合运用了概率论、信息论和最优化理论,是机器学习中一个经典而优美的模型。


人工智能—机器学习公开课(七月在线出品) - P23:机器学习中的几点注意事项——以HMM及CRF模型为例

📘 课程概述
在本节课中,我们将探讨机器学习学习过程中的几个关键注意事项。我们将以隐马尔可夫模型(HMM)和条件随机场(CRF)为例,但重点不在于讲解模型本身,而在于分享学习复杂模型时的方法论。课程内容将分为三个部分:符号与图形表示、概率模型与基本规则、以及模型间的脉络关系。


🧮 第一部分:符号表示与图形表示
在机器学习中,随着模型越来越复杂,我们需要使用大量无歧义的数学符号来表示计算逻辑。同时,将复杂的符号逻辑映射为直观的图形表示,能帮助我们更好地理解模型。
符号表示的重要性
符号表示即模型的数学化、函数化表示形式。每个数学符号都有其明确的标准含义,对其理解不能出错。数学本身要求无歧义性,但不同教材或场景下,同一符号的使用可能存在细微差别,这会给学习者带来困惑。
以下是解决此矛盾的一个关键方法:
查看符号表:许多经典教材会在书的前面或后面附上符号表,说明本书中使用的每个符号的具体含义。建议在学习一本新专著时,先查看其符号表,即使99%的符号与你之前的理解一致,那1%的不同也可能导致后续学习的重大困惑。
例如,在李航老师的《统计学习方法》中,实数集 R 使用了黑体 R 表示,这与一些材料中使用普通 R 的惯例不同。向量表示也需注意:在该书中,默认所有向量为列向量,但为书写方便,常以行向量加转置 x^T 的形式表示。
图形表示的辅助作用
图形表示能将抽象的数学逻辑形象化。以隐马尔可夫模型(HMM)为例,其模型参数 λ 由三元组 (A, B, π) 组成,分别代表状态转移概率矩阵、观测概率矩阵和初始状态概率向量。
状态转移矩阵 A 是一个 N×N 的矩阵,元素 a_ 表示在时刻 t 处于状态 q_i 的条件下,在时刻 t+1 转移到状态 q_j 的概率,即:
P(i_{t+1} = q_j | i_t = q_i) = a_{ij}
仅从数学公式理解可能不够直观。我们可以用图形表示:用两个相邻的圆圈分别代表时刻 t 的状态 i_t 和时刻 t+1 的状态 i_{t+1},用箭头表示转移关系,并在箭头上标注概率 a_。这样,复杂的条件概率关系就变成了直观的前后状态跳转图。
同样,观测概率矩阵 B 和初始概率 π 也可以在图中找到对应位置。将数学符号与图形元素一一对应,能极大地降低理解复杂模型的难度。对于条件随机场(CRF),图形表示也能清晰地区分条件(输入 X)和输出(随机变量序列 Y),以及节点特征和边特征。
核心要点:面对复杂模型,先厘清数学符号的含义,再尝试将其转化为图形表示,建立“数形结合”的理解,是高效学习的关键。
🎲 第二部分:概率模型与基本规则
上一节我们介绍了如何通过符号和图形理解模型结构。本节中,我们来看看另一种重要的模型表示形式——概率模型,以及支撑其推导的基本概率规则。


理解概率模型
我们最容易理解的模型是函数模型:y = f(x),给定输入 x,直接得到输出 y。
概率模型则不同,它通常以条件概率的形式表示:P(Y | X)。给定输入 X,模型输出的是关于所有可能输出 Y 的一个概率分布,而不是一个确定值。例如,在分类任务中,对于输入 X,模型会给出它属于每个类别的概率 P(Y=类别1 | X), P(Y=类别2 | X)... 我们最终选择概率最大的类别作为预测结果。
因此,概率模型的假设空间可以定义为条件概率的集合:F = { P_θ(Y | X) },其中 θ 是参数向量。我们的学习目标就是在参数空间中找到最优的 θ。
两条基本概率规则
复杂的概率模型推导,如HMM中的前向概率计算,看似繁琐,实则反复依赖于两条最基本的概率规则:加法规则和乘法规则。
加法规则(求和规则):
P(X) = Σ_Y P(X, Y)
该规则用于边缘化(消去)随机变量。它允许我们将联合概率P(X, Y)中对 Y 的所有可能取值求和,从而得到只关于 X 的边缘概率P(X)。乘法规则(乘积规则):
P(X, Y) = P(Y | X) P(X)
该规则将联合概率分解为条件概率和边缘概率的乘积。这是贝叶斯定理的基础。
在HMM前向概率的推导中,每一步的变换几乎都是这两条规则的应用。例如,从 α_t(i) 推导 α_{t+1}(j) 时,会先引入新的状态变量(使用加法规则的思想,对上一时刻所有状态求和),再将联合概率分解为条件概率和边缘概率的乘积(使用乘法规则)。
核心要点:概率模型的核心是条件概率 P(Y|X)。再复杂的概率图模型推导,本质上都是加法规则和乘法规则在不同条件下的灵活运用。掌握这两条规则,是理解概率模型推导的钥匙。
🔗 第三部分:模型间的脉络关系
前面我们讨论了理解单个模型的方法。本节我们来看看不同模型之间的关系。机器学习中的模型并非孤立存在,它们之间存在清晰的发展脉络和演进关系。
理解模型间的脉络关系,能将看似繁杂的模型体系串联起来,降低学习复杂度。当你遇到一个新模型时,可以思考:它的前身是什么?它解决了原有模型的哪些不足?做了哪些改进?

以下是两个发展脉络的示例:

示例一:从线性回归到深度神经网络
- 起点:线性回归
y = Wx + b,是最基础的回归模型。 - 关键演变:在线性回归的输出上增加一个 Sigmoid 函数,将输出压缩到 (0,1) 区间,就得到了逻辑回归,用于二分类。逻辑回归单元可以看作一个“神经元”。
- 结构扩展:将多个神经元并排,形成一层;将多层神经元堆叠,前一层输出作为后一层输入,就构成了基本的前馈神经网络。
- 领域特化:在神经网络基础上,引入卷积操作和池化,形成了卷积神经网络(CNN),专精于图像处理;引入循环连接,形成了循环神经网络(RNN),专精于序列数据。这些构成了深度学习的基础。
示例二:从决策树到XGBoost
- 起点:决策树(如ID3, C4.5, CART),是一种基础的非线性模型。
- 集成学习:将多个决策树模型组合起来,就进入了集成学习领域。通过Bagging策略得到了随机森林;通过Boosting策略(如AdaBoost)顺序地训练多个弱学习器(通常是决策树桩)。
- 梯度提升:Gradient Boosting Decision Tree (GBDT) 使用梯度下降的思想来指导Boosting过程,用决策树来拟合当前模型的负梯度(残差)。
- 工程优化:XGBoost在GBDT的基础上,引入了二阶导数信息(Hessian矩阵)进行更精确的优化,并加入了正则化项来控制模型复杂度,同时在工程实现上做了大量优化,成为非常强大的模型。
核心要点:学习新模型时,主动探寻其与前序模型的关系。了解一个模型是在什么背景下、针对什么痛点、从哪个基础模型改进而来,能帮助你更深刻地理解其设计思想,并将知识融会贯通。
📝 课程总结
本节课我们一起学习了机器学习学习过程中的三个重要注意事项:


- 符号与图形结合:重视教材中的符号表,确保对数学符号的理解准确无误。学会将复杂的符号逻辑转化为直观的图形表示,建立数形结合的理解方式。
- 概率模型与基本规则:理解概率模型以条件概率
P(Y|X)为核心的本质。掌握加法规则和乘法规则这两条基本概率工具,它们是解析复杂概率模型推导的基石。 - 模型的脉络关系:认识到模型之间存在发展演进关系。学习新模型时,尝试将其置于模型发展的脉络中,理解其来源、改进与创新,从而构建系统化的知识体系。


希望这些方法能帮助大家在机器学习的道路上更高效地学习和探索。


人工智能—机器学习公开课(七月在线出品) - P24:时间序列分类实战:心电图疾病识别 📈
在本节课中,我们将要学习时间序列数据的基本概念、特征工程方法,以及如何构建一个用于心电图疾病识别的分类模型。我们将从基础理论出发,逐步过渡到代码实践,确保初学者能够理解并掌握核心内容。
第一部分:时间序列数据介绍 📊
上一节我们介绍了课程的整体安排,本节中我们来看看什么是时间序列数据。
时间序列数据是数据挖掘领域中非常重要的一类数据,它无处不在。例如,经济领域的股票、黄金价格走势,电商领域的用户每日浏览量,工业领域的传感器温度记录等,都属于时间序列。
时间序列数据通常由两列组成:一列是时间戳(time),另一列是该时刻对应的值(value)。其特点是数据点按时间顺序排列,并且记录通常具有等间隔性(例如每秒、每天记录一次)。
时间序列数据与结构化数据的区别:
- 结构化数据:以行(样本)和列(特征)的二维形式组织,样本之间通常是独立的。
- 时间序列数据:数据点之间存在时间上的先后次序和依赖关系,整体构成一个序列(
sequence)。
时间序列主要有两大用途:
- 挖掘内在规律:分析序列的周期、趋势、变化幅度等。
- 预测与监控:基于历史数据预测未来走势,或监控数据是否出现异常(例如工业设备温度异常飙升)。
一个时间序列通常可以分解为以下三项:
- 趋势项(Trend):描述序列长期的、缓慢的变化方向。
- 季节项/周期项(Seasonal):描述序列中固定周期的波动。
- 残差项(Residual):去除趋势和周期后剩余的随机波动,可视为噪音。
根据这些成分的组合方式,可以构建两种基础模型:
- 加法模型:
序列值 = 趋势项 + 季节项 + 残差项。适用于季节性和残差与趋势强弱无关的情况。 - 乘法模型:
序列值 = 趋势项 × 季节项 × 残差项。适用于季节性波动幅度随趋势水平变化的情况。
第二部分:时间序列的特征工程 ⚙️
理解了时间序列的构成后,本节中我们来看看如何从中提取有用的特征,以便用于机器学习模型。
由于不同时间序列样本的长度可能不一致,而大多数机器学习模型要求输入维度固定,因此需要进行特征工程。以下是几种常见的特征提取方法:
以下是基于日期时间的特征:
可以直接从时间戳中提取丰富的信息,例如年份、月份、日、周数、星期几、小时、分钟等。在Python中,利用datetime模块可以轻松提取这些特征。
以下是滞后特征(Lag Features):
将历史时刻的值作为当前时刻的特征。例如,lag1表示上一时刻的值,lag2表示上上时刻的值。这类似于一个滑动的历史窗口。
以下是滑动窗口聚合特征(Rolling Window Statistics):
对某个固定时间窗口内的值进行统计计算,如计算平均值、最大值、最小值、标准差等。例如,计算最近7个时间点的值的移动平均。
以下是扩展窗口聚合特征(Expanding Window Statistics):
计算从序列起点开始到当前时刻所有数据的累计统计值,如累计平均值。
以下是对比特征:
将当前值与历史同期值(例如昨天同一时间、上周同一时间)进行比较,计算差值或比值。
第三部分:时间序列的分类与回归模型 🤖
提取了特征之后,本节中我们来看看有哪些模型可以用于时间序列任务。
时间序列分类:给定一个序列,判断其属于哪个类别(例如,心电图正常或异常)。主要方法有:
- 基于序列距离:计算新序列与各类别代表序列的距离(如欧氏距离、DTW动态时间规整距离),距离最近的类别即为预测结果。
- 基于特征的传统机器学习:先提取序列的统计特征(如均值、方差、波动性),然后将这些特征输入到分类模型(如逻辑回归、随机森林)中进行训练。
- 基于深度学习:直接将原始序列输入深度学习模型(如一维卷积神经网络
1D-CNN)进行端到端的特征学习和分类。这种方法通常能获得较高的精度。

时间序列回归(预测):基于历史序列预测未来值。常用方法包括:
- 滑动平均模型(MA)、自回归模型(AR)
- 整合移动平均自回归模型(ARIMA)
- Facebook 提出的 Prophet 模型
- 深度学习模型,如循环神经网络(RNN)、长短时记忆网络(LSTM)以及 Seq2Seq 模型
第四部分:实战案例:心电图疾病识别 🫀
理论铺垫完毕,本节中我们将运用所学知识,实战构建一个心电图分类模型。
问题背景:心电图是诊断心脏健康状况的重要指标。本案例目标是构建一个机器学习模型,自动对心电图序列进行分类,判断其是否正常或属于何种异常类型。

以下是实战步骤:

- 数据读取与探索:使用
pandas和numpy读取数据。数据集中,每一行代表一个心电图序列,每一列代表一个时间点的测量值。所有序列已被填充或截断为相同长度(如188个时间点)。检查数据是否存在缺失值。 - 数据预处理:观察数据标签分布。由于“正常”(类别0)的样本可能远多于其他类别,为避免模型偏差,可以对多数类样本进行下采样,使各类别样本量相对平衡。
- 数据可视化:绘制不同类别的心电图序列,直观感受其形态差异(如波形的光滑度、振幅范围等)。
- 划分数据集:将数据集按比例(如9:1)划分为训练集和验证集。
- 构建深度学习模型:本例采用一维卷积神经网络(
1D-CNN)作为特征提取器,后接全连接层进行分类。
模型的前几层# 示例模型结构(使用Keras) model = Sequential() model.add(Conv1D(filters=32, kernel_size=3, activation='relu', input_shape=(sequence_length, 1))) model.add(Conv1D(filters=64, kernel_size=3, activation='relu')) model.add(MaxPooling1D(pool_size=2)) model.add(Flatten()) model.add(Dense(100, activation='relu')) model.add(Dense(num_classes, activation='softmax')) # num_classes为类别数量Conv1D负责自动提取序列的局部特征,Flatten和Dense层负责将这些特征映射到具体的类别。 - 模型训练与评估:使用分类交叉熵损失函数和Adam优化器编译模型,在训练集上进行训练。训练完成后,在验证集上评估模型性能,查看准确率和混淆矩阵。
通过以上步骤,我们能够构建一个高精度的心电图分类模型,验证集准确率可达96%以上,这展示了深度学习在时间序列分类任务上的强大能力。



对于序列长度不一致的通用处理方法:
- 填充(Padding):将所有序列填充到同一长度(如用0填充尾部)。
- 截断/分块(Truncating/Segmentation):将长序列截断为固定长度,或将长序列通过滑动窗口划分为多个等长的短序列。





总结 📝



本节课中我们一起学习了时间序列分析的基础知识。我们从时间序列的定义、与结构化数据的区别讲起,介绍了其分解方法(趋势、季节、残差)和模型(加法、乘法)。接着,我们深入探讨了时间序列的特征工程,包括日期特征、滞后特征、窗口统计特征等。然后,我们概述了用于时间序列的分类和回归模型。最后,通过一个心电图疾病识别的实战案例,完整演示了如何使用一维卷积神经网络(1D-CNN)对时间序列数据进行分类,涵盖了数据读取、预处理、模型构建、训练和评估的全流程。希望本课程能帮助你入门时间序列分析,并应用于实际项目中。
人工智能—机器学习公开课(七月在线出品) - P25:数据挖掘与机器学习基础 🎯
课程概述
在本节课中,我们将要学习数据挖掘与机器学习的基础知识。课程将涵盖从数据处理工具Pandas的使用,到机器学习的基本概念、常见模型,并通过一个实际案例演示完整的建模流程。本课程面向初学者,力求内容简单直白,帮助大家建立对数据科学领域的初步认识。
第一部分:数据处理工具Pandas 📊
在数据科学领域,Python因其强大的第三方库生态而成为首选语言。与C语言等需要手动编写大量代码处理文件不同,Python的库可以极大简化工作。
Pandas是Python环境下进行数据分析和统计的核心库。它非常适合处理结构化数据,即由行和列组成的二维表格数据,类似于Excel表格。
在Pandas中,DataFrame对象代表一个表格。Column Names是列名,Index是行索引,行列交叉处是具体的取值。Pandas可以高效处理包含数百万行的大型数据集,支持快速加载、索引和数据转换。
以下是Pandas的一些基础操作演示:
1. 创建序列与表格
Pandas可以方便地从Python字典创建数据表格。
import pandas as pd
# 从字典创建DataFrame
df = pd.DataFrame({
‘A‘: [1, 2, 3, 4],
‘B‘: [5, 6, 7, 8],
‘C‘: [9, 10, 11, 12]
})
2. 数据选择与赋值
可以像索引矩阵一样,通过条件选择数据并进行赋值。
# 选择A列大于2的行,并将其B列赋值为-1
df.loc[df[‘A‘] > 2, ‘B‘] = -1
3. 分组聚合操作
groupby功能可以方便地对数据进行分组统计,例如计算每个类别的平均值。
# 按‘animal‘列分组,并计算‘age‘列的平均值
grouped = df.groupby(‘animal‘)[‘age‘].mean()
掌握Pandas是进行数据挖掘的第一步,它为我们后续的数据分析和模型训练奠定了基础。
第二部分:数据挖掘与机器学习流程 🔄
上一节我们介绍了数据处理工具,本节中我们来看看如何将处理好的数据用于挖掘知识。
数据挖掘是使用机器学习、统计和数据库方法从数据中挖掘模式和知识的过程。它更关注整个分析流程。机器学习是人工智能的一个分支,更侧重于具体的建模算法,即如何通过模型解决问题。数据挖掘常常借助机器学习模型来完成具体任务。
数据挖掘涉及的主要步骤包括:数据读取、数据预处理、模型训练、模型推断和结果可视化。
数据挖掘任务主要分为以下几类:
- 异常检测:识别数据中的异常点。
- 关联分析:发现数据项之间的关联规则。
- 聚类:将相似样本自动分组,无需预先标签。
- 分类:预测样本的离散类别标签(例如:是否患病)。
- 回归:预测连续的数值输出(例如:房屋价格)。
其中,分类和回归是监督学习中最常见的任务,区别在于预测的输出Y是类别型还是数值型。
一个完整的机器学习建模流程通常遵循以下步骤:
- 数据预处理:清洗和准备原始数据。
- 特征编码与工程:将数据转换为模型可接受的特征。
- 模型训练与验证:使用训练数据训练模型,并用验证集评估。
- 模型预测与评价:使用测试集进行最终预测,并分析模型性能。
第三部分:常见机器学习模型 🤖
了解了流程后,我们需要认识流程中核心的“工具”——机器学习模型。以下是几类基础且重要的模型。
1. 线性模型
线性模型试图用线性关系来拟合输入X和输出Y。其基本形式可表示为:
Y = WX + b
其中,W是权重,b是偏置。当X为多维向量时(例如房屋面积和楼层),W也相应为多维向量。逻辑回归是线性模型在分类问题上的一个扩展。
2. 贝叶斯模型
贝叶斯模型基于贝叶斯定理,利用先验概率和条件概率进行计算。其核心思想是通过计算在给定输入X的条件下,输出为某个类别C的后验概率来进行分类:
P(C|X) = P(X|C) * P(C) / P(X)
3. 决策树模型
决策树模拟人类决策过程,通过一系列的判断规则(树的分支)对数据进行划分,最终到达叶子节点得到预测结果。它直观易懂,适合处理具有清晰逻辑规则的问题。
4. 支持向量机
SVM的目标是找到一个最优的决策边界(超平面),使得不同类别的样本之间的间隔最大化。这个边界位于两类样本的“最中间”,从而期望在新的数据上具有更好的泛化能力。
5. 深度学习模型
深度学习模型受生物神经网络启发,由多层神经元连接构成。它包含输入层、隐藏层和输出层,能够自动学习数据的多层次抽象特征,在图像、语音等领域表现卓越。
学习机器学习,本质就是学习每类算法的原理、应用场景及其优缺点。
第四部分:实战案例:泰坦尼克号生存预测 🚢
理论需要结合实践。本节我们将通过一个经典案例——泰坦尼克号乘客生存预测,来串联之前所学的知识。
首先,我们使用Pandas读取数据并进行探索性数据分析。例如,我们可以分析不同性别乘客的生存率:
import pandas as pd
import matplotlib.pyplot as plt
# 分组统计并可视化
survival_rate_by_sex = df.groupby(‘Sex‘)[‘Survived‘].mean()
survival_rate_by_sex.plot(kind=‘bar‘)
plt.show()
分析发现,女性乘客的生存率显著高于男性,这与历史背景相符。
接着进行特征工程。例如,从乘客姓名中提取称呼(如Mr., Miss., Dr.)作为一个新特征,这可能包含社会地位、婚姻状况等信息。
然后,进入建模阶段。我们使用scikit-learn库,它可以极大地简化建模过程。以下是使用逻辑回归模型的示例:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
# 实例化并训练模型
model = LogisticRegression()
model.fit(X_train, y_train)
# 预测并评估
predictions = model.predict(X_val)
accuracy = (predictions == y_val).mean()
print(f“模型准确率: {accuracy:.2%}“)
我们还可以轻松尝试其他模型,如高斯朴素贝叶斯、SVM、决策树、K近邻等,并比较它们的性能。
整个实战代码遵循了标准流程:数据预处理、特征编码、模型训练验证和评价。完整的代码将在课程群中提供。
课程总结与互动环节 🎁
本节课中我们一起学习了数据挖掘与机器学习的基础。我们从数据处理利器Pandas入手,了解了数据挖掘的基本流程和常见任务,认识了线性模型、贝叶斯模型、决策树、SVM和深度学习等核心模型,最后通过泰坦尼克号案例完成了从数据分析到模型训练的完整实践。
互动问题:Pandas最适合对什么类型的数据集进行操作?
(答案:结构化数据或表格型数据)

对于初学者,建议的学习路径是:首先掌握Python基础,然后学习Pandas、NumPy、Matplotlib和Scikit-learn等核心库的使用。最好的学习方式是动手实践,通过具体的项目和案例来巩固知识。


如果想继续深入学习,可以关注系统的机器学习课程,从原理到实战,再到项目落地,进行体系化学习。代码和更多学习资料将在课程群中分享。





请注意:本教程根据提供的直播转录内容整理,删除了语气词,并按照要求进行了结构化、口语化处理及格式调整。核心概念已用加粗或代码块标示。
人工智能—机器学习公开课(七月在线出品) - P26:数据挖掘与机器学习进阶
📚 课程概述
在本节课中,我们将要学习数据挖掘与机器学习的进阶知识。我们将深入探讨如何处理不同类型的数据,特别是类别型和数值型特征,并了解集成学习的基本思想。通过本课,你将掌握在拥有数据的情况下,如何对数据集进行处理以获得更好的模型精度。
🗂️ 数据的类型
上一节我们介绍了机器学习的基本流程,本节中我们来看看数据本身。在现实生活和工业应用中,数据是多种多样的。
最常见的数据类型是结构化数据。结构化数据在英文中常被称为“tabular data”,其形式与我们上节课学习的pandas DataFrame类似。它有清晰的行和列结构。
- 行:通常代表一个具体的样本。
- 列:代表描述样本的字段或特征。
例如,一个关于NBA球员的数据集,每一行是一个球员,每一列是球员的球队、号码、位置、年龄等信息。
除了结构化数据,还有半结构化数据和非结构化数据。
- 半结构化数据:如XML、JSON、电子邮件或网页数据。它们具有一定的结构(如字段),但可能不规整,不同样本的字段数量可能不同。
- 非结构化数据:如音频、视频和文本。这类数据没有固定的格式,例如图片大小不一,文本长短不同。
一个关键点是:所有机器学习模型的输入格式都必须是规整的。这意味着每个输入样本的特征维度必须相同。例如,一个设计为接收两个特征(X1, X2)的线性回归模型,无法直接处理包含三个特征(X1, X2, X3)的样本。
🏷️ 类别型特征的处理
接下来,我们聚焦于特征处理。对于初学者,可以首先关注三类特征:类别型、数值型、日期或时间型。本节我们先讲解类别型特征。
类别型特征在我们的数据集中非常常见。例如,个人信息中的性别、城市、民族等字段都是类别型。它们通常以字符串形式存储,例如“男”、“女”、“北京”。
类别型特征可以进一步细分为有序类别和无序类别。
- 有序类别:类别之间存在大小或次序关系。例如,情感极性(正面、中性、负面)或教育程度(高中、本科、硕士、博士)。
- 无序类别:类别之间是平等的,没有次序关系。例如,动物种类(猫、狗、鸟)或颜色(红、白、黑)。
区分有序和无序很重要,因为这将影响我们后续选择哪种编码方式。
为什么需要处理类别型特征?
主要有两个原因:
- 所有机器学习模型的输入都需要是数值类型。模型本质是数学计算,无法直接处理字符串。
- 类别型特征容易引入高基数问题。如果一个类别特征的取值非常多(例如“用户ID”),就会产生大量离散值,给模型带来挑战。此外,类别特征的缺失值也很难填充。
以下是几种常见的类别型特征编码方法:
1. 独热编码


独热编码(One-Hot Encoding)是将一个类别特征转换为一个长度等于其取值空间大小K的向量。在这个向量中,只有对应原始类别的位置为1,其余位置均为0。
公式:如果某个特征的取值空间为 {硕士, 本科, 博士},那么:
硕士编码为[1, 0, 0]本科编码为[0, 1, 0]博士编码为[0, 0, 1]
优点:能够简单有效地将类别特征转化为数值形式,且没有引入人为的次序关系。
缺点:会增加数据维度(一列变K列),可能导致维度爆炸和特征稀疏(矩阵中充满0)。
实现方法:可以使用pandas的 get_dummies() 函数或scikit-learn的 OneHotEncoder。
2. 标签编码
标签编码(Label Encoding)是使用一个具体的数字来替代原始的字符串。
公式:例如,对国家特征进行编码:中国 → 0, 美国 → 1, 英国 → 2, 日本 → 3。
优点:没有增加数据维度。
缺点:引入了数字的大小关系(例如0<1<2<3),这可能误导模型。因此,它仅适用于有序类别或高基数且无需考虑次序的类别。
实现方法:可以使用pandas的 factorize() 方法或scikit-learn的 LabelEncoder。
3. 顺序编码
顺序编码(Ordinal Encoding)是手动根据已知的类别顺序进行编码。
公式:例如,已知教育程度的有序关系为 本科 < 硕士 < 博士,则可以编码为:本科 → 1, 硕士 → 2, 博士 → 3。
优点:保留了类别间的次序信息,且没有增加维度。
缺点:需要人工先验知识来定义顺序。对于未在编码字典中出现的新类别不友好。
实现方法:通常通过Python字典(dict)建立映射关系来实现。
4. 计数编码
计数编码(Count Encoding)不是用数字代替类别,而是用该类别在整个数据集中出现的次数(或频率)来编码。
公式:例如,在数据集中“中国”出现了50次,则所有“中国”样本的该特征值都编码为50。
优点:简单,没有增加维度,并且从全局视角引入了分布信息。
缺点:编码结果严重依赖于当前数据集。如果数据分布有偏或存在误差,编码也会不准确。
🔢 数值型特征的处理
上一节我们介绍了类别型特征的编码,本节中我们来看看数值型特征。数值型特征同样常见,如年龄、成绩、经纬度等。
数值型特征的特点是:
- 有大小关系:例如20岁大于19岁。
- 可进行数学运算:例如可以计算年龄差,这个差值是有意义的。
数值型特征容易出现异常值和离群点。
- 异常值:明显错误的值,例如在年龄列中出现1000岁。
- 离群点:虽然可能不是错误,但远离主体分布的值,例如在一群18-30岁的学生中,出现一个5岁的样本。
为什么需要处理数值型特征?
对数值特征进行缩放(归一化/标准化)是常见的预处理步骤,主要原因是为了优化模型训练。
考虑一个线性模型:y = w1*x1 + w2*x2 + b
如果特征x1的范围是[0, 1],而x2的范围是[0, 100],那么权重w2的微小变化对结果y的影响将远大于w1。这会导致优化路径变得曲折(像椭圆),收敛缓慢且不稳定。
如果将特征缩放至相近的范围(例如都在[0,1]或均值为0、方差为1),优化路径会更接近圆形,训练会更稳定高效。
常见的数值特征处理方法
以下是两种最常用的缩放方法:
1. 最小-最大归一化
将特征缩放到一个固定的范围,通常是[0, 1]。
公式:X_scaled = (X - X_min) / (X_max - X_min)
优点:保留了原始数据的分布形状,适用于非高斯分布的数据。
缺点:对异常值非常敏感,因为异常值通常是最大值或最小值。
2. 标准化
将特征缩放为均值为0,标准差为1。
公式:X_scaled = (X - μ) / σ (其中μ是均值,σ是标准差)
优点:适用于假设数据呈高斯分布的场景,缩放后数据更接近标准正态分布。
缺点:计算均值和方差需要遍历全部数据。
🤝 集成学习简介
在学习了特征处理之后,我们简要了解一下提升模型性能的另一种强大思想——集成学习。

集成学习的核心思想是:结合多个模型的预测结果,以获得比单一模型更好的性能。俗话说“三个臭皮匠,顶个诸葛亮”,这就是集成思想的体现。

集成学习的主要分支
以下是两种主流的集成学习方法:
1. Bagging
Bagging的核心是民主集中。它并行训练多个相互独立的模型(通常是同质模型),每个模型在训练时使用从原始数据集中有放回抽样得到的子集。最终预测时,对于分类任务采用投票法,对于回归任务采用平均法。
关键:模型之间的差异性至关重要。通过行采样和列采样来制造不同的训练数据,是引入差异性的常用手段。
代表算法:随机森林。
2. Boosting
Boosting的核心是串行增强。它按顺序训练一系列模型,每一个新模型都更加关注前一个模型预测错误的样本。通过不断迭代修正错误,逐步提升整体性能。
关键:后一个模型学习的是前一个模型的“残差”或“不足”。
代表算法:AdaBoost, Gradient Boosting, XGBoost, LightGBM。
💻 实践与总结



代码实践要点
- 特征编码:可以使用pandas和scikit-learn库轻松实现One-Hot Encoding、Label Encoding等。
- 集成模型:scikit-learn的
ensemble模块提供了随机森林、梯度提升树等算法的实现。训练后还可以查看模型的特征重要性,了解哪些特征对预测贡献最大。 - 特征工程:除了编码,还可以构建新特征。例如,有了房屋总价和卧室数量,可以创建“每卧室均价”这一新特征,这可能蕴含更有价值的信息。

课程总结
本节课中我们一起学习了数据挖掘与机器学习的进阶知识:
- 数据类型:了解了结构化、半结构化和非结构化数据的区别,并明确了机器学习模型需要规整的数值输入。
- 特征处理:
- 类别型特征:学习了独热编码、标签编码、顺序编码和计数编码的原理、优缺点及适用场景。
- 数值型特征:理解了进行归一化或标准化的必要性,以及最小-最大归一化和标准化两种方法。
- 集成学习:介绍了Bagging和Boosting两种核心的集成思想,它们通过结合多个模型来提升预测性能。
- 学习路径建议:机器学习的学习可以遵循“理论 → 应用与优缺点 → 项目实战”的路径,并按照“提出问题 → 准备数据 → 分析 → 建模 → 得出结论”的流程进行实践。


记住,深度学习是机器学习的一个子领域,两者并非对立。打好机器学习的基础,对后续学习深度学习大有裨益。

人工智能—机器学习公开课(七月在线出品) - P27:手把手带你挖掘电商用户行为



📚 课程概述


在本节课中,我们将学习如何对一个电商用户行为数据集进行完整的数据挖掘。我们将从数据挖掘的基本流程入手,逐步完成数据分析、特征工程、模型构建与评估,最终实现用户购买行为的预测。通过这个实战项目,你将掌握数据挖掘的核心思路和标准工作流。
🔄 数据挖掘基本流程
上一节我们介绍了课程目标,本节中我们来看看数据挖掘的标准流程。数据挖掘是从大量数据中提取有价值信息和泛化规律的过程,其核心流程通常包含以下六个步骤:
- 目标定义:明确本次数据挖掘要解决的具体业务问题。
- 数据采集:从业务系统中抽取与目标相关的样本数据子集。
- 数据整理:对数据进行探索性分析、清洗和预处理,保证数据质量。
- 模型构建:根据问题类型选择合适的算法,构建预测或分析模型。
- 模型评价:使用预定的评估标准对模型性能进行衡量和优化。
- 模型发布:将验证通过的模型部署上线,并进行后续监控与维护。
在实际工业场景中,这个过程往往是反复迭代的,需要根据中间结果不断调整前面的步骤。
🛒 项目背景与目标定义
上一节我们梳理了数据挖掘的通用流程,本节中我们将其应用于具体的电商场景。
项目背景
在电商平台竞争激烈的背景下,除了提升商品质量与价格优势,深入理解用户行为、实现精准营销至关重要。数据挖掘可以帮助企业从用户行为数据(如点击、收藏、加购、购买)中提取商业价值,优化运营策略。
挖掘目标
我们使用一个包含2000多万条用户行为记录的数据集。数据字段包括用户ID (user_id)、商品ID (item_id)、行为类型 (behavior_type)、商品类目 (item_category) 和时间戳 (time)。
我们的核心目标是:预测在下一个时间日期,用户对特定商品子集是否会进行购买。这本质上是一个二分类预测问题(买/不买)。
评估标准
我们采用 F1 Score 作为模型评估的核心指标,它综合了精确率(Precision)和召回率(Recall)。
F1 Score 计算公式:
F1 = 2 * (Precision * Recall) / (Precision + Recall)
🧹 数据采集与整理
上一节我们明确了要解决的问题,本节中我们开始处理数据。数据整理是数据挖掘中耗时最多、也最为关键的环节之一。
数据加载与初步观察
我们使用 pandas 库读取数据,并通过 head()、info()、describe() 等方法了解数据概况,包括数据量、字段类型、是否存在缺失值等。
数据清洗
- 删除无关字段:例如“地理位置”字段多为空值或加密数据,实用价值低,可直接删除。
df = df.drop(columns=[‘geo_location’], axis=1) # axis=1 表示按列删除 - 处理缺失值:本数据集较为完整,无缺失值。若存在缺失,常用处理方法包括删除(缺失比例低时)或填充(如均值、中位数、前后值)。
- 数据转换:将用户行为类型 (
behavior_type) 的数字编码(1,2,3,4)转换为更具可读性的标签(‘pv’, ‘fav’, ‘cart’, ‘buy’)。behavior_map = {1: ‘pv’, 2: ‘fav’, 3: ‘cart’, 4: ‘buy’} df[‘behavior_type’] = df[‘behavior_type’].map(behavior_map) - 时间字段处理:将时间戳字段拆分为日期 (
date)、小时 (hour) 和星期几 (weekday),便于按不同时间维度进行分析。df[‘date’] = pd.to_datetime(df[‘time’]).dt.date df[‘hour’] = pd.to_datetime(df[‘time’]).dt.hour df[‘weekday’] = pd.to_datetime(df[‘time’]).dt.weekday
📊 数据分析与可视化
数据经过清洗后,本节中我们对其进行多角度的探索性分析,以洞察用户行为规律。
以下是几个核心的分析方向:
1. 用户流量分析
- 月度PV/UV趋势:分析页面访问量(PV)和独立访客数(UV)在一个月内的变化。可以发现,在“双十二”活动期间,PV和UV出现显著峰值,说明活动引流效果显著。
- 日均访问量:计算日均PV和UV,了解平台日常流量水平。
- 流量增长率:计算日环比增长率,监控流量健康度。
2. 用户消费行为分析
- 用户行为习惯:分析一天24小时内,四种用户行为的分布。通常可以发现,用户活跃高峰与休息时间(如午休、晚间)重合,此时是营销的黄金时段。
- 用户行为转化分析:计算从点击(
pv)到其他行为(fav,cart,buy)的转化率,并绘制漏斗图。通常点击到购买的转化率最低,这指明了优化方向。 - 复购率分析:统计在一个月内购买次数大于1次的用户比例,衡量用户忠诚度。
- 购买路径分析:统计用户常见的购买路径,如“
pv -> buy”、“pv -> cart -> buy”等。
3. 商品销售情况分析
- 商品销售总量:统计被购买过的商品总数。
- 热销商品:找出销量最高的商品,用于优化库存和选品。
- 商品复售率:统计被重复购买(销售次数>1)的商品比例。
4. 用户价值分析(RFM模型)
根据用户的最近购买时间(Recency)、购买频率(Frequency)、购买金额(Monetary,本例中可用购买次数替代)对用户进行分群,例如:
- 重要价值用户:最近买、买得多、买得频。
- 重要发展用户:最近买,但次数少,有潜力。
- 重要保持用户:买得多且频,但最近没来。
- 重要挽留用户:曾经价值高,但很久没来。
通过可视化(如饼图)展示各类用户占比,为差异化营销提供依据。
🤖 模型构建与评估




上一节我们通过分析获得了业务洞见,本节中我们利用这些洞见来构建预测模型。
1. 数据集划分
将数据集划分为训练集、验证集和测试集,常用比例为 8:1:1。使用 sklearn 的 train_test_split 函数。
from sklearn.model_selection import train_test_split
X_train, X_temp, y_train, y_temp = train_test_split(features, label, test_size=0.2, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
2. 特征工程
基于之前的分析,构建有意义的特征。以下是特征构建的几种思路:
- 统计类特征:用户历史购买次数、点击次数、加购次数等。
- 比率类特征:用户购买点击比、加购点击比等。
- 时间类特征:用户最近一次购买时间距今天数、活跃时间段等。
- 商品类特征:商品历史被购买次数、被点击次数等。
3. 模型选择与训练
选择适合二分类问题的模型进行训练和对比。本节课示例中选用两种经典模型:
- 逻辑回归 (LR):简单、可解释性强。
- 梯度提升树 (GBDT):如XGBoost、LightGBM,通常能取得更好的性能。
from sklearn.linear_model import LogisticRegression from lightgbm import LGBMClassifier model_lr = LogisticRegression() model_lgb = LGBMClassifier() model_lr.fit(X_train, y_train) model_lgb.fit(X_train, y_train)
4. 模型评估与优化
使用预留的验证集和测试集,以 F1 Score 为核心指标评估模型性能。可以尝试:
- 调整模型参数:进行网格搜索或随机搜索。
- 特征选择/组合:优化特征集。
- 模型融合:结合多个模型的预测结果。



最终,根据测试集上的F1 Score选择最佳模型,并对预测结果进行排序,输出最可能购买的用户-商品对列表。






🎯 课程总结


在本节课中,我们一起学习了数据挖掘的完整流程,并实战演练了电商用户行为挖掘项目。我们从目标定义出发,经历了数据采集与整理、深入的数据分析与可视化,最终完成了模型构建与评估。


核心收获在于理解:数据挖掘不仅是调用算法模型,更是一个以业务目标为导向、以数据为基础、需要反复迭代优化的系统工程。其中,业务理解、数据清洗和特征工程往往比模型选择更为关键。



希望本教程能帮助你建立起数据挖掘的基本框架,并为你的后续学习与实践打下坚实基础。


人工智能—机器学习公开课(七月在线出品) - P28:4.14【公开课】企业风险预测:特征工程与树模型实战
在本节课中,我们将学习如何利用特征工程和树模型解决一个实际问题——企业风险预测。我们将从结构化数据的基础知识开始,逐步深入到特征工程的核心概念,并讲解树模型的原理与实战应用,最后通过一个企业非法集资风险预测的案例,将所学知识融会贯通。
第一部分:结构化数据集基础
上一节我们介绍了课程的整体目标,本节中我们来看看什么是结构化数据。
结构化数据是指以非常规整的形式存储的数据集,最典型的形式是二维表格。这种形式类似于常见的数据库或Excel表格。
结构化数据由行和列组成。每一行代表一个样本(例如,一个学生、一家企业),每一列代表一个字段或特征(例如,年龄、性别、注册资本)。行与列的交叉位置,就是这个样本在该特征下的具体取值。


在机器学习中,许多基础算法都适用于处理结构化数据,例如预测用户是否违约、预测房价、识别道路行人等。选择合适算法的关键在于理解问题的类型(如分类或回归)以及数据的特性。
与结构化数据相对的是半结构化数据(如JSON格式)和非结构化数据(如图片、音频、文本)。这三类数据的主要区别在于数据是否规整,即每个样本是否能用一行规整的数据来表示。
一个完整的数据挖掘或建模流程通常包含以下步骤:
- 识别问题
- 理解数据
- 数据预处理
- 模型建模
- 模型评估
每一部分的侧重点不同。例如,在理解数据阶段,我们更关注数据本身的规律及其与标签的关联。
第二部分:特征工程入门
理解了数据的基本结构后,本节我们将探讨如何对原始数据进行改造,使其更适合模型学习,这个过程就是特征工程。
在实际建模中,我们往往不是改进模型本身,而是改进输入模型的数据。原始数据中的字段可能包含多种类型(数值型、字符串型、日期型),但机器学习模型通常只能处理数值型输入。因此,我们需要将原始字段转换为模型可用的特征。


原始数据经过预处理和特征工程后,才能得到最终可送入模型的特征。字段不一定是特征,有些原始字段在建模时可能不需要。


以下是数据处理的一些常见思路:
- 如果原始字段是数值类型,通常可以直接使用。
- 如果原始字段是字符串类型,则需要考虑进行编码,例如独热编码。
类别特征的处理
类别特征是指取值空间有限的离散特征,例如性别、城市、民族。这类特征通常以字符串形式存储,但无法直接参与模型计算,必须进行编码。
类别特征可分为两类:
- 有序类别:取值之间有大小或次序关系,例如年级(一年级、二年级)、情感程度(开心、非常开心)。
- 无序类别:取值之间相互平等,没有大小关系,例如动物种类、颜色。
处理类别特征时,需要根据其是有序还是无序来选择合适的编码方式。
以下是两种常见的类别特征编码方法:
1. 独热编码
独热编码将具有K个类别的特征转换为K个二进制特征。每个类别对应一个特征,属于该类别则标记为1,否则为0。
- 优点:简单有效,能平等对待所有类别,不引入人为的大小关系。
- 缺点:会增加特征维度,可能导致维度爆炸和特征稀疏。
- 实现:可使用Pandas的
get_dummies函数或Scikit-learn的OneHotEncoder。
2. 标签编码
标签编码为每个类别分配一个唯一的整数ID。
- 优点:简单,不增加特征维度。
- 缺点:引入了人为的整数大小关系,可能误导模型(例如,将“中国”编码为0,“美国”编码为1,模型可能误认为0<1有意义)。
- 实现:可使用Pandas的
factorize函数或Scikit-learn的LabelEncoder。
对于有序类别特征,也可以根据业务逻辑进行手动映射编码(例如,将“一年经验”映射为1,“两年经验”映射为2)。
第三部分:树模型原理与使用
完成了特征工程,我们得到了可供模型使用的数据。本节我们将介绍在本案例中表现优异的模型——树模型。
树模型是一种基础而强大的机器学习模型,其决策逻辑类似于 if-else 判断。满足某个条件则走左边分支,不满足则走右边分支,最终到达叶子节点得到预测结果。

单个树模型可以作为一个基础学习器。我们可以通过集成学习的方法,将多个基础学习器组合成更强大的模型。

集成学习:Bagging 与 Boosting
Bagging
Bagging(自助聚合)采用并行训练的思路,类似于“民主投票”。
- 流程:从原始数据集中有放回地采样出多个子集,分别训练多个模型,然后将这些模型的预测结果进行投票(分类)或平均(回归)。
- 优点:可以有效降低模型的方差,提高稳定性。
- 关键:需要保证基模型之间的多样性。可以通过对数据行(样本)和列(特征)进行随机采样,或调整模型超参数来引入多样性。
- 典型代表:随机森林。
Boosting
Boosting采用串行训练的思路,类似于“精益求精”。
- 流程:依次训练多个模型,每个新模型都更关注前序模型预测错误的样本,通过不断修正错误来提升整体性能。
- 优点:可以有效降低模型的偏差。
- 关键:如何基于上一轮模型的结果调整学习重点。常见方法有调整样本权重或直接拟合上一轮模型的残差。
- 典型代表:GBDT, XGBoost, LightGBM。
高阶树模型(如LightGBM)的使用
像LightGBM这样的高阶模型,在工程实现上做了大量优化(如直方图算法加速分裂、特征捆绑等),使其速度更快、内存更省且性能更好。
学习使用一个树模型,通常需要掌握以下步骤:
- 读取数据集
- 训练模型
- 保存与加载模型
- 评估模型与计算特征重要性
- 调整超参数
- 模型部署
以LightGBM为例,它提供两种接口:
- 原生接口:
lgb.train - Scikit-learn API接口:
LGBMClassifier/LGBMRegressor
使用Scikit-learn接口可以更方便地利用其丰富的工具链,如网格搜索调参。
第四部分:企业风险预测实战
前面我们学习了特征工程和树模型的理论,现在我们将它们应用于一个真实案例:预测企业是否存在非法集资风险。
案例背景与数据
我们拥有约25000家企业的信息作为训练集,约15000家作为测试集。目标是根据企业信息判断其是否有非法集资风险。
数据的一个关键特点是多表结构。企业信息分散在多个表格中:
base_info: 企业基本信息(经营范围、行业类型、注册资本等)annual_report_info: 企业年报信息(员工数、经营状态等)tax_info: 企业纳税信息change_info: 企业变更信息news_info: 企业新闻舆论信息- ... 以及其他表格(商标、判决文书等)
实战流程
第一步:多表数据读取与初步分析
分别读取每一张表格。对于每张表,进行以下操作:
- 查看数据概览,了解字段含义和类型。
- 分析缺失值情况,可视化缺失值比例,考虑剔除缺失率过高的列。
- 分析字段的取值空间。
第二步:单表特征工程与聚合
由于某些表(如年报、纳税表)包含同一企业的多条记录,我们需要先进行单表内的特征聚合,将多行数据合并为一行。
- 分组聚合:使用
groupby操作,按企业ID分组,然后使用agg函数进行统计(如求和、均值、最大值、最小值、种类数等)。# 示例:对纳税信息按企业ID聚合,统计纳税总额、最大单笔纳税额等 tax_agg = tax_df.groupby('id').agg({ 'tax_amount': ['sum', 'max', 'min', 'mean'], 'tax_type': ['nunique'] }) - 特征提取:从原始字段中提取新特征,例如从新闻日期中提取年份、月份,或计算最近新闻的时间间隔。
第三步:多表合并
将所有经过处理(清洗、聚合、特征提取)的表格,通过企业ID(主键)合并成一张宽表。
- 这张宽表的每一行代表一家企业的所有整合信息。
- 合并后的宽表即为最终用于建模的特征数据集。
第四步:模型训练与预测
- 数据准备:将合并后的训练集拆分为特征
X和标签y。 - 使用树模型:本例中使用LightGBM。
- 交叉验证:采用K折交叉验证(如10折)来训练模型,以获得更稳健的评估结果,并配合早停机制防止过拟合。
- 预测与集成:让交叉验证产生的多个模型分别对测试集进行预测,然后将这些预测结果取平均,作为最终的预测结果,这通常能提升预测的稳定性。


案例难点与总结

本案例的核心难点在于处理多表关联数据。解决方案是“先聚合,再合并”。必须先对包含多条记录的表格进行聚合,将其转换为“一个企业一行记录”的格式,才能与其他表格正确合并。


整个流程可以概括为:多表读取 → 单表分析与聚合 → 多表合并 → 特征工程 → 模型训练与评估。成功合并出一张高质量的宽表是后续建模成功的基础。


总结
本节课我们一起学习了企业风险预测的完整流程。
- 我们首先了解了结构化数据是机器学习中最常见的数据形式。
- 然后深入学习了特征工程,特别是如何处理类别特征,包括独热编码和标签编码。
- 接着探讨了树模型的原理,包括基础的决策树以及通过Bagging和Boosting衍生的集成模型,并简要介绍了LightGBM的使用。
- 最后,通过一个企业非法集资风险预测的实战案例,我们将理论应用于实践。案例重点演示了如何处理多表数据、进行特征聚合与合并,并利用树模型完成分类预测任务。


希望本课程能帮助你理解从原始数据到机器学习预测的完整链路,并掌握相关的核心技能。

人工智能—机器学习公开课(七月在线出品) - P3:K-means聚类 👨🏫
在本节课中,我们将要学习一种经典且应用广泛的聚类算法——K-means。我们将从聚类的基本概念入手,探讨如何度量样本间的相似性,并详细讲解K-means算法的原理、步骤、优缺点及其实现。
概述:什么是聚类?🤔
聚类是一种无监督学习方法。它的目标是对大量没有标注的数据集,按照数据内在的相似性,将其划分成若干个类别。划分的结果需要使得类别内部的数据相似度较大,而类别之间的数据相似度较小。
因此,聚类涉及两个核心问题:第一,如何定义数据之间的“相似性”?第二,如何确定类别的数目(K值)?
第一部分:如何计算相似度?📏
上一节我们介绍了聚类的目标,本节中我们来看看如何度量数据点之间的相似性。在聚类中,我们通常用“距离”来度量“不相似度”。两个样本越相似,它们的距离就越近;反之,距离就越远。所以,距离和相似度是同一个问题的两个方面。
以下是几种常见的距离或相似度度量方法:
1. 闵可夫斯基距离
这是欧式距离的推广。给定两个n维向量X和Y,其距离公式为:
distance(X, Y) = (∑|Xi - Yi|^p)^(1/p)
当 p=2 时,即为经典的欧式距离。当 p=1 时,称为曼哈顿距离。当 p 趋近于无穷大时,距离由差值最大的维度决定。
2. 杰卡德相似系数
适用于集合数据。对于集合A和B,其相似度定义为它们交集与并集元素数量的比值:
J(A, B) = |A ∩ B| / |A ∪ B|
该度量在推荐系统中应用广泛。
3. 余弦相似度
通过计算两个向量夹角的余弦值来衡量其方向上的相似性,常用于文本数据:
cosine_similarity(X, Y) = (X·Y) / (||X|| * ||Y||)
4. 皮尔逊相关系数
衡量两个随机变量之间的线性相关程度,其值在-1到1之间。公式为相关系数的标准定义。
5. KL散度(相对熵)
用于衡量两个概率分布P和Q之间的差异,公式为:
KL(P||Q) = ∑ P(i) * log(P(i)/Q(i))
KL散度是非对称的。基于它可以构造对称的距离度量,例如海林格距离。
在文本处理中,我们常将文档转化为高维向量(如词袋模型),然后使用余弦相似度进行计算,这实质上等价于对去均值后的向量求相关系数。
第二部分:K-means聚类算法详解 ⚙️
了解了如何度量相似度后,我们正式进入K-means算法。K-means的核心思想是贪心迭代,通过不断优化样本点与聚类中心的隶属关系来达到聚类目的。
算法形式化描述
给定包含N个对象的数据集,目标是将其划分为K个簇(K个类别)。每个簇至少包含一个对象,且每个对象属于且仅属于一个簇。
算法步骤
以下是K-means算法的具体步骤:
- 初始化:随机选择K个数据点作为初始的聚类中心(质心)。
- 分配阶段:对于数据集中的每一个样本点,计算它与K个质心的距离(通常用欧式距离),并将其分配给距离最近的质心所属的簇。
- 更新阶段:所有样本点分配完毕后,重新计算每个簇的质心。新的质心是该簇所有样本点的均值。
- 迭代:重复步骤2和步骤3,直到满足终止条件(例如质心位置不再发生显著变化,或达到预设的最大迭代次数)。
代码实现示意
以下是一个简化的K-means算法核心步骤的伪代码描述:
# 假设 data 是样本数据,k 是预设的簇数量
centroids = 随机选择k个样本点作为初始质心
for _ in range(最大迭代次数):
clusters = 为每个样本点分配最近的质心,形成k个簇
new_centroids = []
for cluster in clusters:
new_centroid = 计算cluster中所有点的均值
new_centroids.append(new_centroid)
if 质心变化很小:
break
centroids = new_centroids
第三部分:K-means的特点与挑战 🎯
上一节我们介绍了K-means的算法流程,本节中我们来看看它的优缺点以及实际应用中需要注意的问题。
优点
- 简单高效:原理和实现都相对简单,对于大数据集也有较好的伸缩性。
- 适合凸形分布:当簇的形状近似球形(类似高斯分布)时,效果很好。
缺点与挑战
- 需要预先指定K值:这是聚类中最棘手的问题之一。通常需要基于业务先验知识,或通过肘部法则、轮廓系数等方法辅助选择。
- 对初始值敏感:不同的初始质心可能导致不同的聚类结果。常用解决方案是多次运行算法,选择最优结果。
- 对噪声和离群点敏感:由于使用均值更新质心,离群点会显著拉偏质心的位置。改进方法之一是使用K-medoids(以中位数代替均值)。
- 不适合非凸形状簇:对于流形、环形等复杂形状的簇,K-means的划分效果往往不理想。
改进策略:二分K-means
为了缓解对初始值的敏感性问题,可以采用二分K-means算法。其基本思路是:先将所有点视为一个簇,然后选择误差最大的簇进行二分。之后,选择两个簇合并以使总体误差最小,如此反复,直到达到指定的K值。
总结 📚

本节课中我们一起学习了K-means聚类算法。我们从聚类的定义和相似度度量出发,详细讲解了K-means的迭代过程与实现原理。我们认识到,K-means因其简洁高效成为最基础的聚类方法,但它对K值选择、初始值和数据分布形态有特定要求。理解这些特点,能帮助我们在实践中更好地应用和改进该算法,例如通过二分K-means或转向更适合复杂形状的谱聚类等算法。
人工智能—机器学习公开课(七月在线出品) - P4:SVM数据试验 🧪
在本节课中,我们将通过一系列可视化实验,深入理解支持向量机(SVM)的核心概念,包括支持向量、核函数以及关键参数 C 和 σ 的影响。我们将从简单例子开始,逐步观察不同设置下决策边界和模型行为的变化。
1. 支持向量与拉格朗日乘子 α
上一节我们介绍了SVM的理论基础,本节中我们来看看拉格朗日乘子 α 在实际数据点上的表现。α 的值决定了哪些样本是支持向量。
我们首先观察一个线性可分的简单例子。根据KKT条件,α 的值被限制在 0 和参数 C 之间。
- α = 0: 对应的样本点被正确分类,且位于间隔边界之外。
- 0 < α < C: 对应的样本点位于间隔边界上,是支持向量。
- α = C: 对应的样本点可能被错误分类,或者恰好位于间隔边界上但被惩罚。
以下是我们在实验中观察到的 α 值与样本位置的关系:
- 位于间隔边界之外的样本,其
α值接近或等于0。 - 位于间隔边界上的样本,其
α值在0和C之间。 - 被错误分类的样本,其
α值等于C。
2. 线性核与参数 C 的影响
接下来,我们使用线性核函数,并探讨惩罚参数 C 的作用。C 控制着模型对分类错误的容忍度。
我们通过增大 C 值进行实验。观察发现:
- C 值增大: 间隔(Margin)的宽度会变窄。模型会尽可能减少分类错误,导致决策边界更贴近那些可能被误分的样本点。
- C 值减小: 间隔的宽度会变宽。模型对分类错误的惩罚变小,允许更多的样本点被误分,以获得一个更“宽松”的决策边界。
核心公式: C 是软间隔SVM优化目标中的正则化参数。
min 1/2 ||w||^2 + C * Σξ_i
3. 高斯核(RBF核)与参数 σ
现在,我们换用高斯核(RBF核)来处理非线性数据。高斯核引入了一个新参数 σ(或 γ,γ = 1/(2σ^2)),它控制了单个样本的影响范围。
我们通过调整 σ 值进行实验:
- σ 值很大: 高斯“钟形”曲线很平缓。每个样本点的影响范围很广,导致决策边界更加平滑,甚至接近线性。模型偏向于欠拟合。
- σ 值很小: 高斯“钟形”曲线很尖锐。每个样本点只影响其周围极小区域。决策边界会变得非常复杂曲折,试图穿过所有样本点。模型偏向于过拟合。
核心公式: 高斯核函数。
K(x_i, x_j) = exp(-||x_i - x_j||^2 / (2 * σ^2))
4. C 与 σ 的协同作用与模型选择
在实验中,我们发现 C 和 σ 是紧密相关的。
σ控制着决策边界的弯曲程度和复杂度。C控制着模型对个别样本的重视程度,影响间隔宽度。
正是因为这两个参数相互影响,且在高维空间中无法直观可视化,所以在实际工程中,我们必须采用系统化的方法进行参数选择。
以下是常用的参数寻优方法:
- 网格搜索(Grid Search): 为
C和σ设定一个候选值范围,遍历所有组合,通过交叉验证选择最佳组合。 - 随机搜索(Random Search): 在指定的参数分布中随机采样组合,相比网格搜索有时效率更高。
5. 高斯核的强大能力与过拟合风险
我们通过构造一个复杂的“棋盘格”数据分布来展示高斯核的强大拟合能力。即使面对如此复杂的模式,高斯核也能找到决策边界将其分开。
这证明了高斯核对应的特征映射是无限维的,因此具有极强的表达能力。然而,能力越强,风险也越高:
- 优点: 可以建模非常复杂的非线性关系。
- 缺点: 极易导致过拟合,即模型在训练集上表现完美,但在未知数据上泛化能力很差。
因此,在使用高斯核时务必谨慎,必须配合严格的验证和正则化(通过 C 和 σ 控制)。
6. Kaggle竞赛简介
最后,我们简要介绍Kaggle。Kaggle是一个著名的数据科学竞赛平台。
以下是关于Kaggle的几个关键点:
- 它汇集了来自全球的数据科学家和机器学习爱好者。
- 平台提供多种类型的数据集(不仅仅是商业数据),涵盖分类、回归、计算机视觉、自然语言处理等任务。
- 在竞赛中取得高排名(如前10%)具有相当高的难度,是检验和实践机器学习技能的绝佳场所。
- 鼓励初学者前往探索和尝试,在实践中学习成长。


本节课总结:
本节课我们一起通过可视化实验,深入观察了SVM的核心机制。我们验证了拉格朗日乘子 α 与支持向量的关系,比较了线性核与高斯核的区别,并详细分析了惩罚参数 C 和高斯核带宽 σ 对模型性能(间隔宽度、边界形状、过拟合风险)的深刻影响。最后,我们认识到强大的高斯核需要谨慎使用,并介绍了Kaggle作为实践平台。理解这些概念对于正确应用和调优SVM模型至关重要。
人工智能—机器学习公开课(七月在线出品) - P5:半小时Torch教程 🚀
在本节课中,我们将要学习一个非常流行的机器学习框架——Torch。我们将了解它是什么、为什么值得推荐、如何安装使用,以及它与其他框架相比的特点。
概述:什么是Torch?🤔

Torch是一个科学计算框架。它的官方定义是一个面向机器学习的现代工具和框架。我们可以简单地将其理解为类似于MATLAB的一套面向机器学习的工具。

Torch之所以被称为“现代”,是因为它很好地支持了GPU以及CUDA、OpenCL等后端计算库。这使得它非常适合在当前环境下用于学习和进入深度学习领域。
Torch的核心是一个提供了N维数组(称为张量,即Tensor)的库。这个核心就是tensor库。围绕这个张量库,Torch提供了诸如索引、切片、转置等各种矩阵变换操作,以及线性代数算法,例如矩阵乘法、求逆等。

在这个核心的张量库之上,Torch提供了大量与机器学习相关的算法包,例如神经网络、数值优化等。因此,Torch是一个完整的、类似于MATLAB的工具和框架。
安装Torch 💻


学习一个框架的第一步是安装它。Torch的安装过程非常简单。

以下是安装步骤:
- 访问Torch官方网站:
torch.ch。 - 点击“Get Started”。
- 目前Torch主要支持OS X和Ubuntu Linux系统,暂时没有官方的Windows支持。但这通常不是大问题,可以使用Linux虚拟机或服务器。
- 安装过程只需执行三个命令。如果你的Ubuntu机器上没有安装Git,需要先安装Git。后续工作会通过脚本自动完成。
- 在一台全新的Ubuntu机器上,整个安装过程可能需要近半个小时,因为它会下载和编译许多开发工具。
- 如果你想支持GPU开发,需要在安装Torch之前,从NVIDIA官网下载并安装CUDA开发工具。CUDA的安装可能会遇到一些与显卡驱动相关的问题,但通常可以通过搜索引擎找到解决方案。
安装完成后,我们可以开始探索Torch。

Torch的核心工具与特性 🛠️


上一节我们介绍了如何安装Torch,本节中我们来看看Torch提供哪些核心工具和特性,使其易于使用和学习。

交互式命令行工具

安装好Torch后,我们可以运行th命令启动一个程序。这是一个基于命令行的交互式工具,类似于一个增强的Lua环境。



为什么说它提供了类似MATLAB的工具?这是Torch提供的第一个工具——交互式命令行工具。在这个环境下,输入torch后按Tab键,它会提示有206种可能,这些都是Torch内置的张量库函数和功能模块。


这种交互式环境非常方便,你可以无需编写完整程序,就能快速验证和学习库函数的使用。
示例:创建张量与运算
-- 创建一个3x3的随机张量
A = torch.rand(3, 3)
print(A)
-- 对A的每个元素进行指数运算
B = torch.exp(A)
print(B)

示例:神经网络模块交互
-- 加载神经网络库
require ‘nn’



-- 创建一个简单的卷积层
convLayer = nn.SpatialConvolutionMM(3, 3, 3, 3, 1, 1)
-- 创建一个随机图像张量(假设为3通道,16x16像素)
image = torch.rand(3, 16, 16)


-- 执行前向传播计算
imageB = convLayer:forward(image)
print(imageB)
正是因为有这样的交互工具,我们可以即时查看变量、函数输出,方便调试和学习神经网络等模块的使用。这与TensorFlow等基于符号计算的框架不同,后者调试起来可能没那么直观。

可视化工具
Torch除了交互式命令行,还提供了一些可视化工具。


首先,它集成了gnuplot库用于绘图。

示例:使用gnuplot绘图
require ‘gnuplot’
x = torch.rand(100)
gnuplot.plot(x)
这类似于MATLAB中的plot命令。

其次,Torch还提供了一个基于Web的、更强大的可视化工具——itorch notebook。这是一个类似Jupyter Notebook的环境,可以在浏览器中执行代码并可视化结果。

示例:在itorch中显示图像
require ‘image’
img = image.lena()
itorch.image(img)
包管理工具与丰富的第三方库
Torch另一个强大的特点是其包管理工具luarocks。你可以通过它轻松安装、更新、卸载各种Lua语言的第三方软件包。

Torch本身及其神经网络包、机器学习包都是通过luarocks管理的。由于Torch是一个相对闭环的生态系统,主要维护自身的兼容性,因此在其上安装绝大多数开发包都比较顺利,较少遇到兼容性问题。

示例:查看已安装包
luarocks list
Torch集成了许多有趣的库,例如OpenCV。因为Lua语言非常容易作为“胶水”集成C语言开发的库,这是Torch选择Lua的重要原因之一。
示例:一个集成了OpenCV的人脸识别Demo
(该Demo可以实时打开摄像头,识别人脸并预测性别和年龄,展示了Torch与第三方库结合的能力。)

所有Torch相关的开发资源、第三方库和文档入口,都可以在其官方网站的“Cheat Sheet”页面上找到。

为什么选择Torch进行学习?📚
前面我们介绍了Torch易用的工具链,本节我们来看看Torch在学习和实践方面的优势。
Torch拥有大量优秀的开源项目和学习案例,这对于初学者入门深度学习非常有帮助。
以下是几个著名的Torch开源项目示例:
- waifu2x:一个在动漫界非常著名的项目,使用深度学习实现智能图像放大,能有效去除放大后的毛刺。在GitHub上有超过5000星。
- Neural Style:图像风格迁移项目。可以将著名画作的风格融合到普通照片中,生成具有艺术效果的图片。在GitHub上有超过6000星。
- NeuralTalk2:图像描述生成项目。输入一张图片,神经网络可以输出对图片内容的文字描述。在GitHub上有超过2000星。
- char-rnn:基于字符的循环神经网络(RNN)语言模型。可以用于生成文本,例如模仿莎士比亚文风、维基百科文章甚至Linux内核代码。该项目展示了Torch在自然语言处理(NLP)领域的应用能力。

通过这些优秀且有趣的开源项目,初学者可以迅速接触到深度学习在不同领域(如图像、文本)的应用,并参考其代码进行学习。

Torch在学术界和工业界也被广泛使用。其主要维护者和贡献者包括Facebook AI Research (FAIR) 和 Google DeepMind。这些顶尖研究机构的支持,确保了Torch能够紧跟深度学习领域的最新进展。例如,针对ResNet(残差网络)、GAN(生成对抗网络)等最新模型,社区都能很快给出Torch的实现。
Torch的总结与对比 ⚖️
本节课我们一起学习了Torch框架。最后,我们来总结并客观评价一下Torch。

Torch的优势:
- 适合初学者:交互式环境和丰富的学习资源使其非常适合深度学习入门。
- 工具链完整:提供交互工具、可视化工具和包管理工具,体验流畅。
- 设计直观:基于“层”(Layer)的神经网络构建方式,易于理解和调试。
- 社区活跃:拥有大量高质量的开源项目,得到FAIR、DeepMind等机构支持。
Torch的不足与对比:
- 语言相对小众:Torch基于Lua语言,而当前深度学习社区更主流的是Python(如TensorFlow, PyTorch)。
- 与符号计算框架的差异:Torch和Caffe属于“命令式”(Imperative)或“动态图”框架。而像TensorFlow、Theano则是“声明式”或“符号计算”(Symbolic)框架,它们先定义计算图,再编译执行。

示例:TensorFlow的符号计算风格(对比)
import tensorflow as tf




# 定义符号(占位符)
x = tf.placeholder(tf.float32, shape=[None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))




# 定义计算图
y = tf.nn.softmax(tf.matmul(x, W) + b)



# ... 之后需要创建会话(Session)来运行这个计算图
这种“先定义,后执行”的符号计算理念,与Torch“定义即执行”的方式不同。


学习建议:
建议初学者可以通过Torch直观地入门深度学习,掌握基本概念。之后,可以再学习TensorFlow这类基于符号计算的框架。理解和掌握这两种不同风格的框架,会对深度学习工具有更全面和深入的理解。


课程总结 🎯


在本节课中,我们一起学习了:
- Torch是什么:一个面向机器学习的科学计算框架,核心是张量库。
- 如何安装Torch:过程简单,主要支持Linux/OS X系统。
- Torch的核心工具:交互式命令行、可视化工具(gnuplot, itorch)、包管理工具(luarocks)。
- Torch的学习优势:拥有大量有趣且高质量的开源项目,便于学习和实践。
- Torch的定位:非常适合初学者入门,其直观的设计有助于理解深度学习概念。同时,我们也了解了它与TensorFlow等符号计算框架的主要区别。


希望通过本教程,你能对Torch框架有一个清晰的初步认识,并能够开始你的深度学习探索之旅。

人工智能—机器学习公开课(七月在线出品) - P6:标签传递算法 📤
在本节课中,我们将要学习一种名为“标签传递算法”的半监督学习方法。该算法适用于数据集中只有少量样本带有标签,而大多数样本没有标签的场景。其核心思想是将已标记样本的标签信息,通过概率传递的方式扩散到未标记的样本上,从而完成对所有样本的分类。
算法背景与应用场景
上一节我们介绍了无监督分类。在实际问题中,有时只有部分样本带有标记,而大多数样本没有标记,这种场景被称为半监督学习。标签传递算法是处理此类问题最简单、最实用的思路之一。
该算法在实践中应用广泛。例如,对微博数据进行聚类,或对电影网站的用户评论进行情感分类,都可以使用这种方法。
算法核心思想
标签传递算法的想法很直观:将那些已标记样本的标签,通过一定的概率传递给那些没有标记的样本。最终,所有样本都会获得一个标签概率值,从而完成分类操作。
算法步骤详解
以下是标签传递算法的具体实现步骤。
数据与相似度矩阵
给定一个数据集data,我们假设前l个样本是已标记的,后面的样本是未标记的。首先,需要计算一个转移概率矩阵P。这个矩阵的计算方式与之前课程中提到的拉普拉斯矩阵构建过程几乎完全相同,它衡量的是样本i和样本j之间的相似度。公式/代码示意:
P[i][j] = similarity(data[i], data[j])初始化与迭代
计算出样本数目和维度后,设定一个迭代次数(例如100次)。然后,对每一个未标记的样本(从第l个到最后一个),计算它应该从哪个邻居样本那里获得标签。标签传递函数
定义一个函数,用于决定将哪个邻居的标签传递给当前样本。其核心逻辑是:对于当前样本i,根据其与所有邻居的转移概率,随机选择一个邻居j。如果邻居j本身有标签(标签值不为零),则将这个标签传递给样本i。如何随机选择:这类似于根据不同的权重随机选择一首歌。我们根据转移概率计算出一个累积概率分布,然后生成一个0到1之间的随机数,看这个随机数落在哪个概率区间,就选择对应的邻居。
代码逻辑示意:
cumulative_prob = calculate_cumulative_prob(P[i]) rand_num = random() selected_neighbor = find_interval(cumulative_prob, rand_num) if label[selected_neighbor] > 0: label[i] = label[selected_neighbor]迭代可视化
标签传递是一个迭代扩散的过程。初始时,只有少数点有颜色(标签)。经过一次迭代后,标签开始向邻近点传播。迭代20次、30次、40次后,标签会逐渐扩散到整个数据集,最终所有样本都会获得一个标签。该算法代码简单,时间复杂度低,因此在实践中非常有用。
算法的影响因素与局限性
虽然算法简单有效,但其结果会受到参数设置的显著影响,主要是带宽和邻域的选择。
以下是通过两个环形数据叠加的例子,说明参数设置不当可能带来的问题。
- 邻域过小:如果设置的邻域特别小,可能导致标签无法在全局有效传递。例如,一个环全部被标记为蓝色,另一个环全部被标记为红色,而无法识别出它们其实是两个独立的结构。
- 邻域过大:如果设置的邻域特别大,又可能导致标签过度平滑。例如,在两个环的交界处会产生模糊的过渡地带,无法形成清晰的分类边界。
对于某些复杂结构的数据(如嵌套的环),单纯调整带宽和邻域参数可能无法得到理想的结果。因为从不同角度看,分类方式可能都有其合理性,这时就需要结合其他方法或更复杂的模型来处理。
参考文献
本课程中提到的方法,特别是之前介绍的“基于密度最大值距离”的算法,有对应的学术论文可供深入阅读。第一篇参考文献尤为重要,大家可以通过搜索引擎直接找到并下载其PDF版本。


本节课中,我们一起学习了标签传递算法。我们了解了它作为半监督学习方法的背景与核心思想,即通过概率扩散将少量标签传递至整个数据集。我们逐步拆解了算法的实现步骤,并讨论了带宽和邻域参数对结果的影响及其局限性。这是一个原理直观、实现简单且在实践中有广泛应用的高效算法。



人工智能—机器学习公开课(七月在线出品) - P7:分布式机器学习系统的设计与实现

概述
在本节课中,我们将要学习分布式机器学习系统的设计与实现。我们将从大数据时代对机器学习提出的挑战出发,探讨为何需要分布式系统,并沿着技术演进的脉络,从MapReduce、Spark到Parameter Server、数据流框架,系统地介绍分布式机器学习的核心范式、设计哲学与关键技术,特别是如何高效地进行模型同步。


大数据时代的机器学习挑战
随着互联网的快速发展,我们已进入大数据时代。例如,Facebook每分钟处理近240万用户的分享,YouTube每分钟上传72小时的视频数据。这些海量数据使得机器学习算法能够发挥前所未有的效果。
然而,大数据和复杂模型也带来了巨大挑战。例如,在单张NVIDIA GPU上训练一个56层的ResNet网络在ImageNet数据集上达到收敛,可能需要长达14天的时间。这对于需要频繁调参的模型开发流程是不可接受的。
另一个挑战是模型规模巨大。例如,LDA主题模型可能需要处理10^11量级的参数,模型大小超过400GB。在线广告点击率预估中使用的FFM模型,其大小经常超过1TB,训练数据更是达到PB级别。
这些挑战表明,使用单台计算机处理工业级机器学习任务已不再可能,分布式训练已成为处理大规模机器学习问题的先决条件。
分布式机器学习的三个层次
学习分布式机器学习,我们可以从三个层次入手:
- 机器学习模型与优化方法:从数学理论层面理解模型。
- 分布式机器学习范式:理解系统设计所基于的计算模式。
- 系统设计与实现:了解具体系统的设计哲学与实现细节。
我们将沿着这个思路,为大家梳理分布式机器学习的知识体系。
机器学习模型基础
对于一个机器学习模型,我们主要关注六个方面:
- 模型形式:模型是如何定义的。
- 预测/推断:模型如何对新数据进行预测。
- 评估指标:如何衡量模型的好坏。
- 训练损失:用于优化模型的目标函数。
- 正则化:如何防止模型过拟合。
- 优化算法:如何最小化训练损失。
以线性模型为例,其预测是将权重向量 w 与特征向量 x 做点积:y_pred = w^T * x。对于深度学习模型,虽然表达能力更强,但训练所需的计算力也大得多。
优化算法经历了漫长演变。以2010年为界,之前的算法多为确定性算法,每一步优化都精确计算;之后的算法则多为随机算法,如随机梯度下降,它更适合大数据场景,因为不需要在每一步都遍历全部数据。
核心的优化算法随机梯度下降的更新公式为:
w_{t+1} = w_t - η * ∇L(w_t)
其中 η 是学习率,∇L(w_t) 是损失函数在 w_t 处的梯度。


分布式机器学习核心:模型同步
分布式机器学习的核心思想是将任务分发给多台机器协同训练。这主要涉及两种并行模式:
- 数据并行:将训练数据划分到不同机器,每台机器拥有完整的模型副本,独立计算梯度,然后同步梯度以更新全局模型。
- 模型并行:将模型本身划分到不同机器,每台机器负责模型的一部分,通常需要处理同一份数据,并在机器间同步中间结果。
无论哪种模式,核心挑战都在于如何高效地进行模型同步。因为网络通信速度远慢于CPU/GPU计算速度,同步可能成为系统瓶颈。
系统架构演进
分布式机器学习系统的架构经历了一系列演进:
1. MapReduce 时代
MapReduce 是早期处理大数据的主要范式。在机器学习任务中,Map阶段计算梯度,输出 (feature_id, gradient) 键值对;Reduce阶段将相同feature_id的梯度相加。
然而,MapReduce 不适合迭代式机器学习任务。因为每一轮迭代都需要启动新的MapReduce作业,从磁盘重复加载数据和模型,I/O开销巨大,成为主要性能瓶颈。
2. Spark 的改进
Spark 的核心改进是将数据持久化在内存中,避免了每轮迭代的磁盘I/O,性能提升显著。
但是,Spark 采用同步计算和广播模型更新的方式。当模型非常大时,Driver节点广播模型会带来巨大的网络带宽压力。同时,同步计算要求所有Worker同时完成计算,速度受限于最慢的节点,资源利用率低。
3. Parameter Server 架构
为了解决Spark的瓶颈,Parameter Server架构被提出。它将模型参数集中存储在若干Server节点上,Worker节点异步地从Server拉取参数、计算梯度,再推送给Server更新。
这种异步执行提高了资源利用率,因为Worker无需相互等待。为了在收敛速度和系统效率间取得平衡,提出了SSP协议,允许最快的Worker比最慢的Worker最多快B步,B是一个可设置的边界值。

Parameter Server提供了get和add的编程抽象。Worker通过get(key)获取参数,计算后通过add(key, delta)推送更新。


4. 数据流框架与深度学习

对于深度学习,像TensorFlow、MXNet这样的数据流框架提供了更直观的编程模型。它们能自动处理并行,例如通过计算与通信的重叠来隐藏通信开销。
一个关键优化是梯度压缩,用以减少通信量:
- 1-bit量化:将梯度正负值分别用其均值替代,然后用1个比特表示正负。
- 2-bit量化:设置阈值,将梯度值量化为
{-a, 0, +a}三种,用2个比特表示。
为防止精度损失,采用了残差累积技术,将本轮未发送完的梯度残差累加到下一轮。


5. All-Reduce 与弹性同步


All-Reduce是一种集体通信操作,所有节点平等参与,最终每个节点都获得完整的聚合结果。它非常适合深度学习的数据并行同步,能充分利用网络带宽。
经典的All-Reduce算法如Ring All-Reduce,通过将数据分块并在节点间形成流水线进行传递和累加,高效完成同步。
为了进一步优化,提出了弹性All-Reduce。它允许在模型未完全同步前就开始下一轮计算,通过设置一个边界值M来控制同步程度,在延迟和带宽利用率之间进行权衡。
总结
本节课我们一起学习了分布式机器学习系统的设计与实现。我们从大数据带来的挑战出发,理解了分布式训练的必要性。随后,我们探讨了分布式机器学习的核心——模型同步,并沿着技术发展脉络,系统学习了从MapReduce、Spark到Parameter Server、数据流框架以及All-Reduce等核心架构的演进过程、设计原理与优缺点。
关键要点包括:
- 分布式机器学习解决的核心问题是大规模数据与模型下的高效训练。
- 模型同步是系统设计的关键挑战。
- 系统演进围绕提高资源利用率和降低通信开销展开。
- 现代深度学习框架通过计算-通信重叠和梯度压缩等技术来优化性能。


要深入这个领域,建议阅读优秀开源系统(如MXNet、XGBoost)的代码,并理解其背后的设计哲学。分布式机器学习是一个将理论、算法与系统工程紧密结合的领域,需要持续的学习和实践。

人工智能—机器学习公开课(七月在线出品) - P8:感性理解GMM 🧠
概述
在本节课中,我们将要学习高斯混合模型(GMM)及其核心求解算法——期望最大化(EM)算法。我们将从一个简单的例子出发,逐步理解如何从“不完全数据”中估计模型参数,并最终掌握EM算法的核心思想和计算步骤。
回顾:极大似然估计
上一节我们介绍了高斯混合模型的基本概念,本节中我们来看看其参数估计的核心思想——极大似然估计。
首先,我们回忆一下极大似然估计。举个例子,假设有10个硬币,我们抛硬币10次,记录结果为“正正反正正正反反正正”。我们想估计硬币正面朝上的概率P。
我们假定每次抛硬币结果为正的概率是P。由于每次抛硬币是独立的,看到这10次结果的概率就是每次结果概率的乘积。其中有7次正,3次反,因此总概率为 P^7 * (1-P)^3。
我们把这个总概率记作关于P的函数。极大似然估计的思想是:既然我们观测到了这个结果,那么就认为这个结果出现的可能性最大。因此,我们需要找到使这个概率函数值最大的P。
为了求解最大值,我们对函数取对数(因为乘积取对数后变为求和,便于求导),然后对P求导并令其为零,最终可以计算出P=0.7。
扩展到高斯分布
理解了离散分布的估计后,我们来看看连续分布的情况。
现在假设我们有N个样本 X1, X2, ..., XN,它们来自一个高斯分布总体。该分布的均值μ和标准差σ是未知的。我们需要根据这N个样本值来估计参数μ和σ。
我们仍然使用极大似然估计。首先写出高斯分布的概率密度函数。给定μ和σ时,样本Xi的概率密度是已知的。将所有样本的概率密度乘起来,就得到了似然函数。
为了求极值,我们同样先对似然函数取对数,得到对数似然函数。然后分别对μ和σ求偏导,并令偏导数为零,即可解出μ和σ的估计值。
计算后我们发现:
- 均值μ的估计值是样本的均值。
- 方差σ²的估计值是样本的“伪方差”(即除以N,而非N-1)。
这个结论与我们的直观是吻合的。
引入新问题:混合高斯模型
上一节我们处理了单一分布的情况,本节中我们来看看更复杂的场景——数据来自多个分布的混合。
现在考虑一个新问题:我们测量了1万名志愿者的身高,得到了1万个身高数据。这些志愿者中有男有女。假设男性的身高服从一个高斯分布(均值为μ1,标准差为σ1),女性的身高服从另一个高斯分布(均值为μ2,标准差为σ2)。
如果我们知道每个志愿者的性别,那么分别用男性数据和女性数据,就能直接套用上一节的公式估计出两组参数。但问题在于,我们只有身高数据,没有性别数据。这种数据被称为“不完全数据”。
我们的目标变成了:仅凭身高数据,估计出μ1, σ1, μ2, σ2,以及样本属于男性或女性的先验概率(记作π1和π2)。这就是高斯混合模型要解决的问题,而解决它的手段就是EM算法。
形式化地描述:假设观测数据X是由K个高斯分布混合而成。每个样本Xi来自第k个高斯分布的概率是πk。第k个高斯分布的均值为μk,标准差为σk。我们观测到了M个样本,需要估计所有πk, μk, σk。
EM算法的核心思想
面对不完全数据,直接建立似然函数会非常复杂(因为对数内部有求和),难以求解。EM算法采用了一种分两步走的迭代策略。
它的核心思想是:既然我们不知道每个样本具体属于哪个分布,那就先“猜”一个。
第一步(E步):计算“属于”每个分布的概率
首先,我们随机初始化所有参数(πk, μk, σk)。对于每一个样本Xi,我们计算它“属于”第k个分布的概率γik。这个概率可以通过以下方式理解:
- 分子:样本Xi来自第k个分布的可能性(πk)乘以在该分布下出现Xi的概率密度。
- 分母:对所有的K个分布做上述计算并求和,目的是进行归一化,使得对于同一个样本Xi,所有γik加起来等于1。
公式:
γik = (πk * N(Xi | μk, σk)) / (∑(j=1 to K) πj * N(Xi | μj, σj))
这个γik就是整个EM算法中最关键的桥梁。
第二步(M步):更新分布参数
有了每个样本属于各个分布的“概率权重”γik后,我们就可以更新参数了。思路是:把每个样本Xi按照权重γik“拆分”到各个分布中去。
例如,一个身高1.9m的样本,计算出的γi1(男)=0.9,γi2(女)=0.1。那么在更新男性分布参数时,这个样本就以0.9的权重参与计算;更新女性分布时,以0.1的权重参与计算。
以下是更新公式的直观理解:
- 更新πk:所有样本属于第k类的权重之和,除以总样本数。
πk_new = (∑γik) / N - 更新μk:用权重γik加权后,所有样本的均值。
μk_new = (∑(γik * Xi)) / (∑γik) - 更新σk:用权重γik加权后,所有样本的(伪)方差。
σk_new = sqrt( (∑γik * (Xi - μk_new)²) / (∑γik) )
迭代过程
用M步得到的新参数(πk_new, μk_new, σk_new),替换掉旧的参数,然后回到E步重新计算γik。如此反复迭代,直到参数不再发生显著变化,算法收敛。
算法特点与注意事项
在应用EM算法时,有以下几点需要注意:
- 局部最优:EM算法只能保证收敛到局部最优解,而非全局最优。初始参数(“猜”的起点)的选择会影响最终结果。糟糕的初值可能导致得到不理想的模型。
- 收敛条件:通常当参数的变化小于某个阈值,或似然函数值不再明显增加时,停止迭代。
- 应用范围:我们讨论的是各组分均为高斯分布的混合模型。若组分是其他分布(如泊松分布),则更新公式会不同,但EM算法的两步走框架依然适用。
总结
本节课中我们一起学习了高斯混合模型及其求解算法——EM算法。
我们首先从简单的极大似然估计出发,理解了参数估计的基本思想。然后,我们将问题扩展到数据来源不明(不完全数据)的混合高斯模型。为了求解这个复杂问题,我们引入了EM算法,其核心是通过迭代的“猜测-更新”过程来逼近最优参数:
- E步(期望步):基于当前参数,计算每个样本属于各个分布的概率权重γik。
- M步(最大化步):利用计算出的权重γik,像处理“完全数据”一样,更新每个高斯分布的参数(πk, μk, σk)。


这个算法直观而强大,是无监督学习中处理隐变量问题的经典方法。
🧠 人工智能—机器学习公开课(七月在线出品) - P9:机器学习模型发展脉络——以感知机模型为例


📚 课程概述
在本节课中,我们将一起探讨机器学习模型发展的内在逻辑与脉络。我们将以感知机模型为例,详细分析其如何从线性回归模型发展而来,以及它如何进一步演变为支持向量机等更复杂的模型。通过本节课的学习,你将理解如何构建自己的知识体系,并掌握学习复杂模型的有效方法。
🎯 第一部分:学习目标与核心内容
上一节我们介绍了课程的整体安排,本节中我们来看看学习机器学习需要达成的具体目标。根据往期学员的反馈和行业需求,我们的学习应聚焦于以下三个核心方面。
1. 理论基础部分
理论基础是面试和工作中的考察重点。你需要掌握模型的基本原理、数学推导以及相关概念。
以下是理论基础部分需要掌握的核心内容:
- 数学基础:根据个人情况查漏补缺,重点包括数理逻辑、微积分、线性代数、概率论与统计、信息论和最优化方法。
- 机器学习模型:掌握传统机器学习模型,如线性回归、逻辑回归、支持向量机、决策树、贝叶斯方法等,并理解它们之间的联系。
- 深度学习模型:掌握以神经网络为基础的深度学习模型,如CNN、RNN、LSTM、Attention、Transformer等,并了解其在CV、NLP等方向的应用发展。
- 分布式机器学习:了解模型并行、数据并行等分布式计算的基本原理,这对处理大规模数据和复杂模型很重要。
2. 编程实践部分
理论学习必须通过编程实践来巩固和验证。这部分的核心是“调包调参”,但前提是理解背后的原理。
以下是编程实践部分需要掌握的核心技能:
- 掌握核心工具包:熟练使用
scikit-learn,TensorFlow,PyTorch等机器学习框架。 - 精通Python:Python是目前该领域的首选编程语言。
- 重视数据可视化:可视化是向业务专家展示结果、进行有效沟通的关键环节。
3. 工程项目部分
理论知识最终要服务于解决实际业务问题。完整的项目经验能让你深刻理解从问题定义到模型部署的全流程。

我们的课程会通过完整的项目案例,带你体验实际工程项目的流程,包括与业务专家的沟通、需求分析、方案设计、结果汇报等。

总结:明确你当前的基础水平(理论、编程),认清学习目标(掌握上述三部分内容),中间的过程就是我们的课程与你的努力。
🗺️ 第二部分:模型发展脉络与知识体系构建
上一节我们明确了学习目标,本节中我们来看看如何系统性地学习众多模型。机器学习模型并非孤立存在,它们之间存在清晰的发展脉络和逻辑关系。构建知识体系的关键在于理清这些脉络。
以下是一个简化的模型发展脉络示例,帮助你理解模型间的关联:
- 线性回归 → 逻辑回归/感知机:在线性回归模型
y = WX + b的基础上,通过引入不同的激活函数(如Sigmoid、Sign),分别衍生出用于分类的逻辑回归和感知机模型。 - 感知机 → 支持向量机(SVM):感知机只要求分类正确,解不唯一。SVM在此基础上,进一步追求“分类间隔最大化”,从而得到更鲁棒的解。
- 感知机 → 人工神经网络:多层感知机(MLP)即最基本的前馈神经网络。通过堆叠感知机层并使用反向传播算法,构成了深度学习的基础。
- 决策树(ID3 → C4.5 → CART):这是一个模型自我迭代完善的典型脉络,针对信息增益、连续值处理、回归问题等缺陷进行逐步改进。
- 决策树 → 集成方法(Bagging → Random Forest, Boosting → GBDT/XGBoost):单一模型能力有限,集成学习通过组合多个弱模型来构建强模型。
- 贝叶斯定理 → 朴素贝叶斯 → 隐马尔可夫模型(HMM)-> 条件随机场(CRF):这是一个以概率图模型为核心的发展脉络,广泛应用于序列标注等问题。
核心建议:学习新模型时,要主动思考“它从何而来”(解决了之前模型的什么不足)、“到何处去”(它自身存在什么局限,后续模型如何改进)。推荐使用《机器学习》(西瓜书)和《统计学习方法》作为核心教材,构建你的知识框架。


🔍 第三部分:案例深入——感知机模型的发展脉络


上一节我们宏观地看了模型发展的图谱,本节中我们以感知机模型为具体案例,深入剖析其来龙去脉。
1. 感知机模型的来源:从线性回归到二分类
感知机的核心思想很简单:它在线性回归模型的基础上,增加了一个符号函数,从而将连续的预测值转换为离散的类别标签。
- 线性回归:模型为
f(x) = W·X + b,完成从特征空间R^n到实数值R的映射。 - 感知机:模型为
f(x) = sign(W·X + b),其中sign是符号函数(正数输出+1,负数输出-1)。这完成了从特征空间R^n到二值类别{+1, -1}的映射。


公式描述:
sign(x) = { +1, if x >= 0; -1, if x < 0 }
过渡:由此可见,感知机利用线性回归模型 W·X + b 计算出一个“得分”,然后根据得分的正负来决定类别。这就是从回归模型衍生出分类模型的经典思路。
2. 感知机的学习策略:定义损失函数
模型定义好后,我们需要一个标准来衡量模型的好坏,并据此改进模型参数(W, b)。感知机采用的策略是:只关注被错误分类的样本。
- 几何直观:在二维平面上,感知机试图找到一条直线
W·X + b = 0(称为分离超平面)将两类点分开。 - 误分类点:对于误分类的点
(x_i, y_i),其满足-y_i * (W·x_i + b) > 0。这个式子很重要,它统一了正负样本被分错的情况。 - 距离度量:点到超平面的距离公式为
|W·x_i + b| / ||W||。结合误分类点的性质,可以去掉绝对值,得到误分类点到超平面的距离为-y_i (W·x_i + b) / ||W||。 - 损失函数:将所有误分类点
M的距离求和,并忽略分母||W||(通过系数缩放技巧,不影响优化方向),就得到了感知机的损失函数:


公式描述:
L(W, b) = - Σ_{x_i ∈ M} y_i (W·x_i + b)




过渡:这个损失函数的意义很直观:如果没有误分类点(M为空集),损失为0;误分类点越多、离超平面越远,损失值就越大。我们的目标就是最小化这个损失函数。



3. 感知机的局限与SVM的诞生


感知机采用梯度下降法最小化上述损失函数,但存在一个明显问题:解不唯一。只要能将训练数据完全分开,任何一条这样的分隔线都是感知机的解。
- 问题所在:如下图所示,多条直线都能完美分类,但它们的“稳健性”不同。有的线离某些样本点太近,对于新数据的分类可能不可靠。
![]()
- SVM的改进思想:支持向量机(SVM)在感知机的基础上,提出了更高的要求:不仅要分类正确,还要让离分隔线最近的那些点(支持向量)尽可能远。这被称为“最大化分类间隔”。
- 从感知机到SVM:感知机的目标是“没有误分类点”,而SVM的目标是“在保证没有误分类点的前提下,最大化间隔”。这通过引入函数间隔和几何间隔的概念,并将问题转化为一个带约束的凸二次规划问题来实现。
过渡:因此,SVM可以看作是感知机的一种“加强版”,它通过增加一个优化目标(最大化间隔),解决了感知机解不唯一、泛化能力可能不强的问题,从而获得了更优的分类器。


📝 课程总结
在本节课中,我们一起学习了以下核心内容:

- 明确了学习目标:机器学习的学习应围绕理论基础、编程实践和工程项目三个核心维度展开,缺一不可。
- 构建了知识体系:模型学习不能“只见树木,不见森林”。我们梳理了以线性回归-感知机-SVM/神经网络、决策树-集成学习、贝叶斯-概率图模型为代表的发展脉络,强调在学习中要主动建立模型间的联系。
- 深入剖析了案例:以感知机模型为例,我们详细分析了:
- 其来源:如何从线性回归模型通过引入符号函数演变而来。
- 其原理:如何定义误分类点并构建损失函数
L(W, b) = - Σ y_i (W·x_i + b)。 - 其发展:如何因其“解不唯一”的局限性,自然引出了以“最大化间隔”为目标的支持向量机(SVM)。


希望本节课能帮助你建立起机器学习模型学习的宏观视角和有效方法。记住,理解脉络、打通原理与代码、通过项目实践深化理解,是学好机器学习的关键。

人工智能—深度学习公开课(七月在线出品) - P1:CNN训练注意事项 🧠
在本节课中,我们将要学习卷积神经网络训练中一个至关重要的环节:权重初始化。我们将探讨为什么初始化如此重要,以及不当的初始化方法如何导致训练失败。课程将介绍几种主流的初始化策略,并解释其背后的原理,帮助初学者理解如何为深度神经网络设置一个良好的起点。
早期深度神经网络的困境 😰
上一节我们介绍了CNN的基本概念,本节中我们来看看训练深度网络时遇到的一个经典难题。
早期神经网络的发展曾一度停滞,直到2012年AlexNet的出现才取得突破。AlexNet之后,各种深度神经网络模型如雨后春笋般涌现。这种爆炸式增长的一个关键原因是,AlexNet为后续模型的训练提供了一个良好的初始值。
在AlexNet的7层架构时代,训练更深层次的网络就变得异常困难。网络经常在训练中途“崩溃”。具体表现为:梯度在反向传播中消失,或者前向运算中的权重W全部变为零。例如,在激活层之后,输出可能全部变为零,或者反向传播回来的梯度变为零。这导致网络无法学习到任何有效信息。
深度网络训练失败的核心原因在于其脆弱性。如果初始权重设置不当,网络将无法进行有效的学习。因此,权重初始化成为了一个极其重要的研究课题。近年来的许多论文都在专门探讨如何初始化权重,以增强网络的鲁棒性,使其不会因为输入数据的微小变化而在训练中失败。
为什么不能将所有权重初始化为零? 🚫
以下是关于全零初始化问题的分析。
在深度神经网络中,如果将所有权重W初始化为0,会导致一个严重问题:对称性。
第二层神经网络的输入,是由第一层的输出与权重W做线性组合得到的。当所有权重W都为零时,无论上一层的输出是什么,下一层所有神经元的输入都将变得完全相同。这种对称性会逐层传递。
在卷积神经网络中,我们希望每个滤波器能关注图像的不同特征,如颜色或纹理。对称的初始化使得所有神经元都在学习完全相同的东西,无法分化出不同的滤波器。无论是前向传播还是使用反向传播算法求梯度,所有参数的变化都会一模一样。这违背了我们希望神经网络具有非对称性的初衷,导致模型无法有效学习。
因此,将所有权重初始化为相同的值(包括全零)是不可取的。
尝试一:使用小随机数初始化 🔢
既然全零初始化不行,一个自然的想法是使用很小的随机数来初始化权重。
最常见的方法是从一个均值为0、方差很小(例如0.01)的高斯分布中随机采样数值来初始化权重W。在代码中,可以使用NumPy实现:
W = np.random.randn(fan_in, fan_out) * 0.01
在这种方法中,我们通常希望权重中正数和负数的数量大致各占一半,以避免网络在初始化时就向某一侧过度倾斜,从而保持初始的非对称性。
然而,这种方法仅适用于层次不深的神经网络。如果只有一两个隐藏层,这种初始化方式通常能正常工作。但对于更深的网络,它仍然会导致问题。
深度网络中的激活值分析 📊
为了验证上述初始化方法在深层次网络中的效果,我们进行了一个实验。
我们构建了一个10层、每层500个神经元的深度神经网络,并使用tanh作为激活函数。我们监控网络在前向传播过程中,每一层激活输出的均值和方差。
实验发现,当使用小随机数初始化时:
- 第一层:激活输出的均值约为-0.000117,方差为0.21,表现正常。
- 第二层:均值急剧减小到约0.000001,方差也变得非常小。
- 后续层:均值和方差都迅速趋近于零。
这意味着,在深层次网络中,信号经过几层传递后,激活值几乎全部变为零。输出没有波动,导致反向传播时梯度消失,网络无法学习。因此,过小的初始化数值会导致激活值消失。
那么,使用较大的随机数初始化是否可行呢?实验表明,这会导致另一个极端问题:激活值饱和。对于tanh或sigmoid这类激活函数,过大的输入会使输出趋近于±1,进入梯度近乎为零的饱和区,同样阻碍了训练。
解决方案:Xavier/Glorot 初始化 ⚖️
上一节我们看到,过大或过小的初始化值都会有问题。本节介绍一种经典的解决方案。
为了解决上述问题,2010年的一篇论文提出了 Xavier初始化(也称Glorot初始化)。其核心思想是:保持每一层激活输出的方差与输入方差大致相同,从而避免信号在传播过程中过度放大或缩小。
具体公式为:从均值为0、方差为 Var(W) = 1 / fan_in 的高斯分布中采样来初始化权重W。其中,fan_in是该层的输入神经元数量。
在代码中通常实现为:
W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)
其数学推导基于一个假设:希望该层输出的方差 Var(y) 与该层输入的方差 Var(x) 相等。经过推导(涉及线性变换和独立假设),可以得出当 Var(W) = 1 / fan_in 时,能近似满足这一条件。这有助于在深度网络中维持激活值的稳定分布。
然而,Xavier初始化是针对tanh或sigmoid这类对称的饱和激活函数设计的。
针对ReLU的改进:He初始化 🔧
当更高效的ReLU激活函数成为主流后,人们发现Xavier初始化对其效果不佳。
ReLU函数会将所有负输入置零,这改变了输出的分布。使用Xavier初始化后,在深层次网络中,激活输出的方差仍然会逐渐衰减。
因此,在2015年,何恺明等人提出了专门针对ReLU及其变体的 He初始化。其方法非常简单,仅将公式中的分母做了调整:
W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in / 2)
# 或者等价于:W = np.random.randn(fan_in, fan_out) * np.sqrt(2. / fan_in)
实验表明,使用np.sqrt(2. / fan_in)作为缩放因子,能够使ReLU网络在各层的激活输出保持稳定的方差,从而确保训练过程正常进行。
在Batch Normalization等技术普及之前,手动调整初始化策略(如Xavier或He初始化)是训练深度网络的关键技巧。它们需要大量的实验来验证其有效性。
现代方法:Batch Normalization 🚀
手动初始化策略虽然有效,但仍需谨慎调优。现代深度学习提供了一个更强大的工具来自动稳定训练过程:批归一化。
Batch Normalization(BN)层的提出,极大地缓解了对精细初始化策略的依赖。BN层在网络中自动完成以下工作:对每一层的输入进行归一化处理(减去均值,除以标准差),然后通过可学习的参数γ和β进行缩放和偏移。
这个过程能确保无论输入分布如何变化,传递到下一层的值都能保持相对稳定的分布。因此,即使初始化不那么完美,BN也能帮助网络更稳定、更快地收敛。在当今大多数深度网络中,结合He初始化和Batch Normalization已成为标准做法。
总结 📝
本节课中我们一起学习了卷积神经网络训练中权重初始化的关键知识。
我们首先探讨了错误初始化(如全零初始化)导致的对称性问题,这会使网络无法学习。接着,我们分析了使用过小或过大随机数初始化在深度网络中引发的激活值消失或饱和现象。然后,我们介绍了两种经典的手动初始化策略:适用于tanh/sigmoid的Xavier初始化和适用于ReLU的He初始化,它们通过数学推导来维持层间激活值的稳定分布。最后,我们提到了现代深度学习中广泛使用的Batch Normalization技术,它能自动标准化层输入,降低了对初始化策略的敏感度,使深度网络训练更加鲁棒。



理解权重初始化的原理,是构建和训练有效深度学习模型的重要基础。

人工智能—深度学习公开课(七月在线出品) - P2:CNN之卷积计算层 修订版 🧠
在本节课中,我们将要学习卷积神经网络(CNN)的核心组成部分——卷积计算层。我们将通过直观的比喻和清晰的步骤,理解卷积层如何工作,以及它如何通过参数共享等机制,极大地减少神经网络的参数量,同时保持强大的特征提取能力。
概述:卷积层的直观理解 🎯
上一节我们介绍了神经网络的基本概念,本节中我们来看看卷积层。卷积层是CNN中至关重要的一层,它能够将网络的参数量极大地降低。为了便于理解,我们可以将神经元想象成一个个“小朋友”,每个小朋友都有自己的“世界观”(即权重参数),他们负责观察和理解输入的数据。
图像数据具有一个关键特性:局部关联性。一个像素点的值,与其相邻像素点的关联性最强,而与图像另一角的像素点关联性很弱。因此,让一个“小朋友”一次性看完整张巨大的图像(例如32x32像素)是困难且低效的。卷积层采用了一种巧妙的方法来解决这个问题。
核心概念与计算过程 🔍
滑动窗口与参数共享
卷积层的工作方式,是让每个“小朋友”(神经元)通过一个固定大小的“滑动窗口”来观察图像。这个窗口也称为感受野。
以下是卷积操作的关键步骤:
- 分配滑动窗口:例如,我们为每个小朋友分配一个3x3的窗口。这样,他每次只需要关注9个像素点,压力就小了很多。
- 滑动覆盖全图:为了让小朋友看完整个图像,这个窗口会从图像的左上角开始,先向右滑动,看完一行后,再向下移动,继续向右滑动,如此反复,直到覆盖整张图像。
- 保持“世界观”一致:最关键的一点在于,每个小朋友在滑动窗口观察图像的不同部分时,他用来理解和判断的“世界观”(即那9个权重参数W)是固定不变的。这被称为参数共享。
- 这意味着,对于同一个小朋友(神经元),无论他的窗口滑动到哪里,他都用同一套标准(同一组W)去分析看到的那一小块图像。
- 不同的“小朋友”(不同的神经元)则拥有各自不同的“世界观”(不同的W组),这样他们才能从不同角度解读图像,产生思维的碰撞。
通过这种方式,一个神经元的参数量就从需要连接整张图像(如32x32=1024个)减少到了只连接一个窗口(如3x3=9个),参数量实现了大幅下降。
卷积运算详解
那么,每个小朋友在窗口固定于某一位置时,具体是如何计算的呢?运算过程非常简单,就是点乘求和。
假设我们有一个3x3的数据窗口(矩阵),和一个3x3的权重矩阵(即该小朋友的“世界观”W)。计算步骤如下:
- 将数据矩阵和权重矩阵在相同位置上的数值相乘。
- 将所有乘积结果相加,得到一个数值。

这个数值就是该小朋友对这个特定图像块的理解结果,也称为特征值。用公式可以表示为:
输出特征值 = Σ (数据矩阵[i,j] * 权重矩阵[i,j]) (有时还会加上一个偏置项b)

这个运算在信号处理领域被称为卷积,这也是卷积神经网络名称的由来。
重要概念解析 📚
理解了基本操作后,我们还需要明确几个关键术语。
深度(Depth)

深度指的是卷积层中神经元的数量,也就是“小朋友”的数量。它决定了下一层特征图的“厚度”。因为我们需要多个神经元从不同角度观察图像,综合他们的意见才能得到更全面的理解。深度也对应着滤波器(Filter) 的数量,每个滤波器代表一个神经元及其固定的一组权重。




步长(Stride)





步长定义了滑动窗口每次移动的距离。例如,步长为2意味着窗口每次向右或向下移动2个像素。步长影响输出特征图的大小,步长越大,输出特征图尺寸越小。




零填充(Zero Padding)



当滑动窗口的尺寸和步长设置使得窗口无法恰好从图像一边滑到另一边时,我们可以在图像边缘外围填充一圈(或多圈)0。这样做有两个好处:
- 控制输出特征图的尺寸。
- 让窗口能更多地覆盖到图像边缘的像素信息。





多通道卷积示例 🖼️





实际的彩色图像通常有红(R)、绿(G)、蓝(B)三个颜色通道。因此,输入是一个三维数据(如32x32x3)。卷积层如何处理这种情况呢?




每个“小朋友”(神经元)在观察时,必须同时看三个通道的信息。因此,他的“世界观”(权重W)也会扩展为三维。例如,对于一个3x3的窗口,他的权重会是一个3x3x3的立方体。





计算时,他在每个颜色通道上,用对应的3x3权重矩阵与图像数据做点乘求和,最后将三个通道的计算结果再加起来(并加上偏置),最终得到一个特征值。这个过程确保了神经元能综合所有颜色信息进行判断。




本节总结 📝

本节课中我们一起学习了卷积神经网络的核心——卷积计算层。
- 我们首先通过“小朋友看图像”的比喻,理解了卷积层利用滑动窗口和参数共享机制来高效处理图像局部信息,并大幅降低参数量。
- 接着,我们详细讲解了卷积的点乘求和计算过程。
- 然后,我们明确了深度(神经元数量)、步长(滑动距离)、零填充(边缘处理) 这三个关键概念。
- 最后,我们了解了如何处理具有多个通道(如RGB)的输入图像。

卷积层通过固定的、可学习的滤波器(权重模板)在图像上滑动并计算,提取出基础的特征图(Feature Map),为后续更深层的网络理解图像内容奠定了坚实的基础。正是这种设计,使得CNN在图像识别等领域取得了巨大成功。

人工智能—深度学习公开课(七月在线出品) - P3:NeuralStyle艺术化图片(学梵高作画背后的原理) 🎨
在本节课中,我们将要学习Neural Style Transfer(神经风格迁移)的原理。这是一种能够将著名画作(如梵高、毕加索的作品)的艺术风格,迁移到普通照片上的技术。我们将深入探讨其背后的核心概念、损失函数的构成以及训练过程。
概述
Neural Style Transfer技术源于2015年的一篇论文。它的目标非常明确:输入一张原始照片和一张艺术风格图像(如大师的画作),生成一张既保留原始照片内容,又具有艺术风格的新图像。
核心原理:两种损失函数
上一节我们介绍了Neural Style Transfer的目标,本节中我们来看看它是如何通过数学方法实现这一目标的。整个过程的核心在于定义并优化两个关键的损失函数。
所有机器学习问题,尤其是有监督学习,通常都需要定义一个损失函数(Loss Function)或代价函数(Cost Function),并通过不断最小化这个函数来优化模型。Neural Style Transfer也不例外,但它同时优化两个相互“竞争”的损失。

以下是构成总损失函数的两个核心部分:
- 内容损失
- 目标:衡量生成图像与原始内容图像在视觉内容上的差异度。
- 作用:确保生成图不会偏离原始照片的主体内容。


- 风格损失
- 目标:衡量生成图像与风格图像在艺术风格上的差异度。
- 作用:促使生成图学习并模仿艺术画作的笔触、色彩搭配等风格特征。
在训练过程中,这两个损失函数会不断“打架”。当生成图在内容上非常接近原图时,往往难以学到抽象的艺术风格;反之,当风格模仿得很像时,内容的可理解性又会下降。因此,我们需要一个总损失函数来平衡这两者。
总损失函数的公式可以表示为:
总损失 = α * 内容损失 + β * 风格损失
其中,α 和 β 是两个超参数,分别控制内容保真度和风格迁移强度的权重。
损失函数的计算细节
理解了总体的损失框架后,我们来看看这两个损失具体是如何计算的。这需要借助一个预训练好的卷积神经网络(如VGG)来提取图像特征。
内容损失的计算
内容损失的计算相对直观。它的核心思想是:比较两幅图像在神经网络同一中间层输出的特征图(Feature Map)的差异。
假设我们使用VGG网络的第5个卷积层。对于一幅输入图像,该层会输出一个维度为 C x H x W 的特征张量,其中:
- C 代表通道数(即特征图数量,例如512)。
- H 和 W 代表特征图的高度和宽度(例如64x64)。
计算步骤如下:
- 将原始内容图像输入网络,提取第L层的特征图,记为
F^L。 - 将当前生成的图像输入同一网络,提取第L层的特征图,记为
P^L。 - 计算这两个特征图之间每个位置(像素点)的差值,并求其平方和。
内容损失的公式为:
L_content = 1/2 * Σ (F^L - P^L)^2
这个公式本质上是一种L2损失,它迫使生成图像的特征图在数值上接近内容图像的特征图。
风格损失的计算
风格损失的计算是Neural Style Transfer算法的创新之处。它使用Gram矩阵来捕捉图像的风格信息。
Gram矩阵的计算方法如下:
- 对于同一幅图像在某一层(如第L层)的特征图(维度
C x H x W),我们将其视为C个大小为H x W的矩阵。 - 计算这
C个特征图中,每两个特征图之间的相关性。具体做法是:将两个特征图(例如第i个和第j个)展平为向量,并计算它们的点积(内积)。 - 对所有
C x C个组合进行上述计算,得到一个C x C的矩阵,这就是Gram矩阵G^L。
Gram矩阵的物理意义:特征图中的每个通道可以理解为捕捉了图像的某种纹理或模式。Gram矩阵中元素 G_{ij} 的值,反映了第 i 种纹理模式与第 j 种纹理模式在整幅图像中同时出现的程度。这种不同特征之间的相关性,被证明能有效表达图像的风格。
计算步骤如下:
- 计算风格图像在第L层的Gram矩阵,记为
A^L。 - 计算生成图像在第L层的Gram矩阵,记为
G^L。 - 计算这两个Gram矩阵之间的L2损失(类似于内容损失的计算)。
风格损失的公式为:
L_style = Σ (w_l * ||A^L - G^L||^2)
其中,w_l 是不同层损失的权重,通常可以对多层(如VGG的多个卷积层)的损失进行加权求和,以捕捉不同尺度的风格特征。
模型的训练过程
明确了损失函数如何计算后,本节我们来看看Neural Style Transfer独特的训练过程。它与常规的神经网络训练有显著不同。
在传统的图像分类任务中,我们固定输入图像,通过反向传播来调整网络的权重(W和B),使网络输出更接近标签。
而Neural Style Transfer的训练过程则相反:
- 固定网络权重:我们使用一个预训练好的VGG网络(如VGG19),并冻结其所有权重。这个网络在此任务中仅用作一个强大的“特征提取器”。
- 优化输入图像:我们将待生成的图像本身作为可优化的变量(通常初始化为白噪声或内容图像的副本)。
- 前向传播与损失计算:在每次迭代中,将内容图像、风格图像和当前生成图像分别输入固定的VGG网络,根据前面介绍的方法计算内容损失和风格损失,并得到总损失。
- 反向传播与更新:关键的一步来了。我们计算总损失关于生成图像像素值(X)的梯度,而不是关于网络权重的梯度。然后使用梯度下降法(如Adam优化器)直接更新生成图像的像素值。
这个过程可以概括为:我们不是在训练一个网络,而是在“训练”或“优化”一张图片,使其在VGG特征空间中,同时接近内容图像的内容和风格图像的风格。
代码实现一览
为了帮助大家理解,我们可以简要看一下TensorFlow版本的代码核心逻辑。以下是关键步骤的示意:
# 1. 加载预训练的VGG模型,并冻结权重
vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
vgg.trainable = False
# 2. 定义内容损失函数
def content_loss(base_content, target):
return tf.reduce_mean(tf.square(base_content - target))
# 3. 定义Gram矩阵计算和风格损失函数
def gram_matrix(input_tensor):
result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
input_shape = tf.shape(input_tensor)
num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
return result / num_locations
def style_loss(base_style, gram_target):
gram_style = gram_matrix(base_style)
return tf.reduce_mean(tf.square(gram_style - gram_target))
# 4. 提取特征并计算总损失
# 获取VGG特定层的输出作为特征
content_features = vgg(content_image)['block5_conv2']
style_features = vgg(style_image)['block1_conv1', 'block2_conv1', ...] # 通常取多层
generated_features = vgg(generated_image)
# 计算损失
c_loss = content_loss(content_features, generated_features['block5_conv2'])
s_loss = 0
for layer_name in style_layers:
s_loss += style_loss(generated_features[layer_name], style_features[layer_name])
total_loss = alpha * c_loss + beta * s_loss
# 5. 优化生成图像
optimizer = tf.optimizers.Adam(learning_rate=0.02)
gradients = tape.gradient(total_loss, generated_image)
optimizer.apply_gradients([(gradients, generated_image)])
效果与参数调整
通过调整总损失函数中的超参数 α 和 β,我们可以控制生成图像的倾向:
- α 值相对较大(β较小):生成图像更注重保留原始照片的内容,风格化效果较弱。
- β 值相对较大(α较小):生成图像更注重模仿艺术风格,内容可能变得抽象甚至难以辨认。
- α 与 β 平衡:在内容和风格之间取得一个较好的折中,这是最常见的使用方式。
总结


本节课中我们一起学习了Neural Style Transfer(神经风格迁移)的原理。我们从其目标出发,深入探讨了内容损失和风格损失这两个核心概念及其计算方法,特别是通过Gram矩阵来量化图像风格的创新思想。我们还了解了其独特的训练过程——固定预训练网络权重,转而优化生成图像本身的像素值。最后,通过代码概览和参数调整分析,我们掌握了如何在实际中运用这一技术。希望本教程能帮助你理解这项有趣且强大的图像生成技术背后的奥秘。
人工智能—深度学习公开课(七月在线出品) - P4:TensorFlow推荐系统 🎬
概述
在本节课中,我们将学习如何使用TensorFlow构建一个基于矩阵分解的推荐系统。我们将从数据处理开始,逐步搭建模型,并进行训练和评估。整个过程将使用MovieLens数据集作为示例,但您可以将此模板应用于其他格式相似的数据。
数据处理 📊
上一节我们介绍了课程目标,本节中我们来看看如何准备数据。推荐系统的数据通常包含用户、物品和评分信息。我们的目标是将其处理成适合神经网络训练的格式。
首先,我们需要读取并处理数据。数据格式通常包括用户ID、物品ID、评分和时间戳。
以下是数据读取与处理函数的核心步骤:
def read_data_and_process(filname):
# 指定列名
col_names = ["user", "item", "rate", "st"]
# 使用pandas读取CSV文件,以制表符分隔,无表头
df = pd.read_csv(filname, sep="\t", header=None, names=col_names, engine='python')
# 将用户ID和物品ID转换为从0开始的索引
df["user"] -= 1
df["item"] -= 1
# 转换数据类型
df["user"] = df["user"].astype(np.int32)
df["item"] = df["item"].astype(np.int32)
df["rate"] = df["rate"].astype(np.float32)
return df
接下来,我们需要为模型训练生成批次数据。神经网络通常以批次为单位进行训练,这有助于提高训练效率和模型泛化能力。
以下是用于随机产出训练批次的迭代器类:
class ShuffleDataIterator:
def __init__(self, inputs, batch_size=10):
self.inputs = inputs
self.batch_size = batch_size
self.num_cols = len(self.inputs)
self.len = len(self.inputs[0])
self.inputs = np.transpose(np.vstack([np.array(self.inputs[i]) for i in range(self.num_cols)]))
def __next__(self):
# 从所有样本中随机抽取一个批次的下标
indices = np.random.randint(self.len, size=self.batch_size)
# 根据下标抽取对应的样本数据
batch = self.inputs[indices, :]
return np.hsplit(batch, self.num_cols)
对于模型评估,我们需要顺序产出数据,以确保测试集中的每个样本都被评估到。
以下是用于顺序产出评估批次的迭代器类:
class OneEpochDataIterator(ShuffleDataIterator):
def __init__(self, inputs, batch_size=10):
super(OneEpochDataIterator, self).__init__(inputs, batch_size=batch_size)
self.num_batches = self.len // self.batch_size
if self.len % self.batch_size != 0:
self.num_batches += 1
self.batch_idx = 0
def __next__(self):
start = self.batch_idx * self.batch_size
end = min(start + self.batch_size, self.len)
self.batch_idx += 1
if self.batch_idx == self.num_batches:
self.batch_idx = 0
batch = self.inputs[start:end, :]
return np.hsplit(batch, self.num_cols)
模型搭建 🧱
上一节我们处理好了数据,本节中我们来看看如何搭建推荐模型。我们将使用矩阵分解方法,其核心思想是将用户和物品映射到低维向量空间,并通过向量内积来预测评分。
预测评分的公式如下:
预测评分 = 全局偏置 + 用户偏置 + 物品偏置 + (用户向量 · 物品向量)
以下是使用TensorFlow搭建该模型的函数:
def inference_svd(user_batch, item_batch, user_num, item_num, dim=5, device="/cpu:0"):
with tf.device(device):
# 初始化偏置项
global_bias = tf.get_variable("global_bias", shape=[])
w_bias_user = tf.get_variable("embd_bias_user", shape=[user_num])
w_bias_item = tf.get_variable("embd_bias_item", shape=[item_num])
# 查找用户和物品的偏置向量
bias_user = tf.nn.embedding_lookup(w_bias_user, user_batch, name="bias_user")
bias_item = tf.nn.embedding_lookup(w_bias_item, item_batch, name="bias_item")
# 初始化用户和物品的嵌入向量
w_user = tf.get_variable("embd_user", shape=[user_num, dim])
w_item = tf.get_variable("embd_item", shape=[item_num, dim])
# 查找用户和物品的嵌入向量
embd_user = tf.nn.embedding_lookup(w_user, user_batch, name="embedding_user")
embd_item = tf.nn.embedding_lookup(w_item, item_batch, name="embedding_item")
# 计算预测评分
infer = tf.reduce_sum(tf.multiply(embd_user, embd_item), 1)
infer = tf.add(infer, global_bias)
infer = tf.add(infer, bias_user)
infer = tf.add(infer, bias_item, name="svd_inference")
# 添加L2正则化项
regularizer = tf.add(tf.nn.l2_loss(embd_user), tf.nn.l2_loss(embd_item), name="svd_regularizer")
return infer, regularizer
模型搭建完成后,我们需要定义损失函数和优化器来训练模型。
以下是定义损失函数和优化器的步骤:
def optimization(infer, regularizer, rate_batch, learning_rate=0.001, reg=0.1, device="/cpu:0"):
with tf.device(device):
# 计算L2损失
cost_l2 = tf.nn.l2_loss(tf.subtract(infer, rate_batch))
# 总损失 = L2损失 + 正则化项
penalty = tf.constant(reg, dtype=tf.float32, shape=[], name="l2")
cost = tf.add(cost_l2, tf.multiply(regularizer, penalty))
# 使用Adam优化器最小化损失
train_op = tf.train.AdamOptimizer(learning_rate).minimize(cost)
return cost, train_op
模型训练与评估 🚀
上一节我们搭建好了模型,本节中我们来看看如何在实际数据上进行训练和评估。我们将设置训练参数,读取数据,并启动训练循环。
以下是训练过程的核心步骤:
- 设置超参数,如批次大小、向量维度、迭代轮数等。
- 读取训练集和测试集数据。
- 使用之前定义的迭代器产出批次数据。
- 在TensorFlow会话中运行训练循环,更新模型参数。
- 定期在测试集上评估模型性能。
训练过程中,我们会监控训练损失和验证损失的变化,以判断模型是否收敛。
总结
本节课中我们一起学习了如何使用TensorFlow构建一个简单的推荐系统。我们从数据处理开始,学会了如何读取和批次化数据。然后,我们基于矩阵分解公式搭建了神经网络模型,并定义了损失函数和优化器。最后,我们在实际数据集上完成了模型的训练与评估。


这个模板的核心在于将用户和物品表示为低维向量,并通过向量运算预测评分。您可以将此框架应用于其他具有类似格式的数据集,只需调整数据读取部分即可。通过本课的学习,您应该对使用深度学习框架构建推荐系统有了初步的实践认识。

人工智能—深度学习公开课(七月在线出品) - P5:基于序列到序列的深度学习机器翻译系统 🧠➡️🗣️
在本节课中,我们将要学习如何构建一个基于序列到序列(Seq2Seq)架构的深度学习机器翻译系统。我们将从数据处理开始,一步步搭建模型,并完成训练和评估的整个流程。
概述 📋
机器翻译是自然语言处理中的核心任务之一。本节课将指导你实现一个基础的神经机器翻译模型。我们将遵循一个清晰的工程流程:首先进行数据预处理,然后构建编码器-解码器模型,接着训练模型,最后评估其翻译效果。
第一步:数据预处理与读取 📥
上一节我们介绍了课程目标,本节中我们来看看如何准备数据。数据处理的第一步是将原始文本数据读入程序并进行初步清洗。
我们首先需要从文件中读取中英文句子对。数据文件的格式是每行包含一句英文和一句中文,中间用制表符(\t)分隔。
以下是读取数据的步骤:



- 打开数据文件。
- 逐行读取,并使用制表符分割每一行,得到英文和中文句子。
- 将句子分别存入英文列表和中文列表。




def load_data(in_file):
english = []
chinese = []
with open(in_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
parts = line.split('\t')
if len(parts) == 2:
english.append(parts[0])
chinese.append(parts[1])
return english, chinese


第二步:文本分词与特殊标记 ✂️



读取原始字符串后,我们需要将句子分解成单词或字符的序列。对于英文,我们使用NLTK库进行分词;对于中文,我们采用按字符分割的简单方式。此外,为了告知模型句子的开始和结束,我们需要在每个序列的开头和结尾添加特殊标记:BOS(Begin Of Sentence)和EOS(End Of Sentence)。
以下是分词和添加标记的步骤:


- 对英文句子使用
word_tokenize进行分词。 - 对中文句子按字符进行分割。
- 在每个序列的开头添加
BOS标记,结尾添加EOS标记。
from nltk.tokenize import word_tokenize
def tokenize_and_tag(english_sentences, chinese_sentences):
tokenized_en = []
tokenized_cn = []
for en_sent, cn_sent in zip(english_sentences, chinese_sentences):
# 英文分词
en_words = ['BOS'] + word_tokenize(en_sent.lower()) + ['EOS']
tokenized_en.append(en_words)
# 中文按字符分割
cn_chars = ['BOS'] + list(cn_sent) + ['EOS']
tokenized_cn.append(cn_chars)
return tokenized_en, tokenized_cn
第三步:构建词汇表与数字编码 🔢
模型无法直接处理单词,因此我们需要将单词映射为数字。这需要为英文和中文分别构建一个词汇表(字典)。词汇表记录了每个单词对应的唯一ID。对于训练集中未出现过的单词,我们使用一个特殊的UNK(Unknown)标记来表示。
以下是构建词汇表和编码的步骤:
- 统计所有训练数据中单词的出现频率。
- 选择出现频率最高的前N个单词构成词汇表,并为每个单词分配一个ID(从1开始)。
- 将
UNK标记的ID设为0。 - 使用构建好的词汇表,将分词后的句子序列转换为对应的数字ID序列。
from collections import Counter

def build_dictionary(tokenized_sentences, max_words=50000):
word_counts = Counter()
for sentence in tokenized_sentences:
for word in sentence:
word_counts[word] += 1
# 选择最常见的单词
most_common_words = word_counts.most_common(max_words)
# 构建单词到ID的映射
word_to_id = {'UNK': 0}
for idx, (word, _) in enumerate(most_common_words):
word_to_id[word] = idx + 1 # ID从1开始
return word_to_id, len(most_common_words)

def encode_sentences(tokenized_sentences, word_to_id):
encoded = []
for sentence in tokenized_sentences:
encoded_sent = [word_to_id.get(word, 0) for word in sentence] # 未知词映射到0
encoded.append(encoded_sent)
return encoded
第四步:数据排序与批处理准备 ⚙️
为了提升训练效率,我们通常将数据按序列长度进行排序。这样,在组成批次(Batch)进行训练时,同一个批次内的句子长度相近,可以减少因填充(Padding)带来的计算浪费。
我们只需根据英文句子的长度(或中文句子长度)对所有句子对进行排序即可。
def sort_data_by_length(encoded_en, encoded_cn):
# 根据英文句子长度创建索引
paired_data = list(zip(encoded_en, encoded_cn))
paired_data.sort(key=lambda x: len(x[0]))
sorted_en, sorted_cn = zip(*paired_data)
return list(sorted_en), list(sorted_cn)
总结 🎯


本节课中我们一起学习了构建神经机器翻译系统的数据预处理全流程。我们完成了从读取原始文本、分词、添加特殊标记,到构建词汇表、将文本转换为数字ID,以及为高效训练进行数据排序的关键步骤。这些步骤是为后续模型搭建和训练奠定坚实的数据基础。下一节,我们将利用处理好的数据,开始构建编码器-解码器模型结构。

人工智能—计算广告公开课(七月在线出品) - P1:CTR预估技术的演变 🎯
概述
在本节课中,我们将学习点击率预估技术的演变历史。我们将从最基础的逻辑回归模型开始,逐步深入到深度学习模型,并探讨在线学习、强化学习等前沿技术。课程将穿插讲解机器学习相关技巧与实践经验,旨在帮助初学者理解CTR预估的核心概念与技术脉络。
第一章:逻辑回归 📈
上一节我们概述了课程内容,本节中我们来看看CTR预估技术中最基础且经典的模型——逻辑回归。
什么是CTR预估?
CTR预估的核心任务是通过用户、物品和上下文信息来预测用户点击某个物品的概率。其数学表示如下:
公式:Y = f(U, I, C)
其中,U 代表用户,I 代表物品,C 代表上下文。模型的目标是利用这组三元组特征来预估最终的点击结果 Y。
逻辑回归模型的三要素
学习一个模型,我们需要从三个方面入手:模型本身、损失函数和优化算法。这不仅适用于逻辑回归,也适用于其他机器学习模型。


1. 模型
逻辑回归模型的基本形式是一个线性函数外加一个Sigmoid变换,将输出映射到0到1之间,代表点击概率。
公式:f(x) = σ(β^T x)
其中,σ 是Sigmoid函数,β 是模型参数,x 是特征向量。
2. 损失函数
损失函数定义了模型“好坏”的标准。逻辑回归通常使用交叉熵作为损失函数。
公式:L = -[y log(ŷ) + (1-y) log(1-ŷ)]
其中,y 是真实标签,ŷ 是模型预测值。
3. 优化算法
优化算法决定了如何更新模型参数以使损失函数最小化。常见算法包括随机梯度下降和FTRL。
代码示例(SGD核心思想):
# 伪代码
for epoch in range(num_epochs):
for x, y in dataset:
gradient = compute_gradient(loss_function, x, y, params)
params = params - learning_rate * gradient
逻辑回归的特性与特征工程
逻辑回归属于广义线性模型家族。这类模型对连续特征和特征量纲不一致问题比较敏感。因此,特征工程至关重要,主要包括以下两点:
以下是特征工程的两个核心方向:
- 特征离散化:将连续特征分桶,可以解决特征敏感性、量纲不一致问题,并能实现风险均摊,降低异常值的影响。
- 特征组合:将原始的一阶特征进行组合,生成二阶、三阶等高阶交叉特征,能显著提升模型表达能力。
传统的特征工程依赖人工经验,但存在上限。因此,自动特征组合技术应运而生。
GBDT + LR:自动特征工程的典范
GBDT+LR模型巧妙地结合了树模型和线性模型,实现了自动特征离散化与组合。
流程简述:
- 首先,用GBDT模型在训练集上训练多棵决策树。
- 然后将每个样本输入到训练好的GBDT中,样本会落入每棵树的某个叶子节点。
- 接着,将样本在所有树上的叶子节点位置进行One-Hot编码,生成一个高维稀疏的离散特征向量。
- 最后,将这个稀疏特征向量作为逻辑回归模型的输入进行训练。
为什么有效?
- 对GBDT:它擅长处理连续特征,通过树的分裂过程自动进行了特征组合与离散化。
- 对LR:LR模型擅长处理GBDT输出的离散化特征。两者结合,GBDT充当了强大的特征转换器,LR则负责最终的精准预测。
第二章:深度模型 🧠
上一节我们介绍了基于特征工程的经典逻辑回归模型,本节中我们来看看如何利用深度学习模型来进一步提升CTR预估的性能。
深度学习为何有效?

深度学习近几年在CTR预估中广泛应用,主要得益于两点:
- 数据量的爆发:深度学习模型参数多、VC维高,需要海量数据才能充分训练,避免欠拟合。
- 算力的提升:GPU、TPU等专用硬件的发展,使得训练复杂的深度神经网络成为可能。

深度CTR模型的两条演进路线
深度CTR模型的发展主要沿两条主线展开:
- 模型复杂化路线:从FM模型出发,不断引入更复杂的网络结构。
- 业务场景化路线:从Embedding+MLP基础架构出发,结合具体业务场景进行优化。
起点一:因子分解机
FM模型通过矩阵分解技术,解决了特征稀疏场景下的二阶特征组合问题。
公式:ŷ = w₀ + Σ w_i x_i + Σ Σ <v_i, v_j> x_i x_j
其中,<v_i, v_j> 代表特征向量的内积,通过低维稠密向量 v 的学习来刻画特征间交互。
从神经网络视角看,FM可以理解为对离散特征进行嵌入,然后对嵌入向量进行内积交互。
核心概念:Embedding
Embedding的本质是一种映射,将高维稀疏的离散特征向量,转换为低维稠密的连续特征向量。
- 数学上:是从一个向量空间到另一个向量空间的映射。
- 神经网络上:对应网络中的一层权重矩阵。
- 特征工程上:是特征的一种新的、更高效的表示方式。
起点二:Embedding + MLP
这是深度CTR模型最基础的架构。先将所有特征通过Embedding层映射为稠密向量,然后拼接起来,输入到多层感知机中进行深层特征交互与预测。
里程碑:Wide & Deep 模型
Google提出的Wide & Deep模型是CTR预估领域的里程碑。它并行了两个部分:
- Wide部分:简单的线性模型,处理稀疏特征,擅长记忆。
- Deep部分:深度神经网络,处理稠密特征,擅长泛化。
两部分联合训练,兼具记忆与泛化能力。该框架简单有效,易于扩展,后续很多模型都是在其基础上的变体。
前沿模型探索
近年来,业界提出了许多针对特定业务场景的深度模型:
- DIN:引入注意力机制,用于处理用户历史行为序列,更好地捕捉与当前候选商品相关的行为。
- ESMM:通过多任务学习,联合训练CTR和CVR任务,解决了CVR任务样本稀疏的难题。
第三章:在线学习与强化学习 ⚡
上一节我们探讨了复杂的深度模型,本节中我们来看看如何让模型“动起来”,实现实时更新与智能决策。
模型提升的关键
在实践中,提升模型效果主要依赖三个方面:
- 业务理解:准确地将业务问题抽象为数学模型。
- 特征工程:挖掘更精准的特征,包括实时特征。
- 特征与模型实时化:即在线学习与强化学习。

在线学习

在线学习意味着模型能够实时地利用新产生的数据更新自身。
在线学习的价值:
- 引入实时信息:如当前时间、用户实时行为。
- 快速学习新特征:新广告上线后能快速积累有效特征。
- 更新统计信息:使上下文统计特征更准确。
在线学习的挑战:
- 流式计算引擎:需要高性能、高可用的实时数据处理平台。
- 样本处理:涉及时间窗口、样本拼接、过滤与采样等。
- 更新与评估逻辑:模型如何在线更新,以及如何在线评估效果。
- 优化算法:需采用适合在线场景的算法,如FTRL。

强化学习

强化学习让智能体通过与环境的动态交互来学习最优策略,更贴近人类学习方式。


在广告中的应用:
- 多臂老丨虎丨机:解决探索与利用的平衡,适用于简单选择场景。
- 上下文老丨虎丨机:在考虑用户上下文信息的情况下进行决策。
- 深度强化学习:结合深度学习的强大感知能力,处理更复杂的状态和动作空间,是未来的重要方向。
第四章:建模技巧与总结 🛠️
在本节课的最后,我们简要探讨一些CTR预估中的实用建模技巧,并对全课内容进行总结。
常见问题与技巧
在CTR预估的实践中,会遇到许多具体问题:
以下是几个典型的建模挑战:

- 冷启动问题:新广告或新用户缺乏历史数据。在线学习可以部分缓解,但非万能。
- 召回与精排的平衡:召回阶段追求性能与覆盖率,精排阶段追求精准度,需要权衡。
- 评估指标:AUC并非永远是最佳指标。在关注精确率或召回率的场景下,需选择更合适的评估体系。
- 平滑技术:对于数据稀疏的样本(如新广告),其统计指标(如点击率)不可靠,需要进行平滑处理。
- 平台利益平衡:在计算广告体系中,需要设计机制平衡用户体验、广告主利益和平台收益。

总结
本节课我们一起学习了CTR预估技术的完整演变历程:

- 逻辑回归:作为基石,我们理解了模型、损失函数、优化算法三位一体的建模方法论,以及特征工程(离散化、组合)的核心重要性。
- 深度模型:我们从FM和Embedding+MLP出发,看到了模型如何通过复杂化和业务场景化两条路径演进,最终形成了如Wide & Deep这样兼具记忆与泛化能力的强大架构。
- 在线学习与强化学习:我们探讨了让模型实时进化以及与环境智能交互的前沿技术,这是提升模型效果和适应性的关键。
- 建模技巧:我们认识到,除了模型本身,对业务的理解、对数据的处理以及对系统各方面的权衡,同样是成功构建CTR预估系统的关键。


CTR预估技术仍在快速发展,希望本课程能为你打开这扇大门,助你在计算广告与推荐系统的领域继续深入探索。
🧠 人工智能—计算广告公开课(七月在线出品) - P2:计算广告的市场与技术
📖 课程概述
在本节课中,我们将要学习计算广告的核心概念、市场背景与技术体系。我们将从在线广告的基本定义出发,逐步深入探讨计算广告的三种主要产品形态(合约广告、竞价广告、程序化交易广告),并了解其背后的技术架构、核心算法(如点击率预估)以及在线分配等关键问题。
🏛️ 第一部分:计算广告的产品与发展简史

计算广告是一种在线广告形式。首先需要了解什么是在线广告。在线广告,顾名思义,就是网络广告或互联网广告,直接在媒体上投放广告。

虽然“在线广告”这个名字可能有些陌生,但它的表现形式大家应该很熟悉。例如,在浏览网站时,底部出现的淘宝推广链接;或者在淘宝、京东购物时,看到的站内推荐链接。这些广告都是计算广告,也就是在线广告。再比如,使用百度或谷歌搜索引擎时,搜索结果最上方经常出现的广告。这些都属于互联网广告。
接下来,我们讨论在线广告与传统广告的不同。传统广告类似于街上发传单或在电视上播放的广告,我们称之为传统广告。在线广告则基于互联网技术,在过去一二十年里逐渐衍生和发展出一套新体系。计算广告是在线广告基础上的进一步发展,通过将机器学习、计算等技术应用到广告领域,为平台方和广告主带来更多收益。
这里提到了两个关键名词:平台方和广告主。广告主是投放广告的人,平台方是提供广告投放渠道的地方。例如,百度上展示了一家医院的广告,那么这家医院就是广告主,百度就是平台方。平台方有时也称为媒体。此外,还有用户,即看广告的人。我们自己就是用户。这样就形成了计算广告的三方体系:平台方、广告主和用户。本课程将主要在平台方和广告主之间展开讨论。
本公开课主要从四个方面分享计算广告的技术:
- 计算广告的产品和发展简史:讲解合约广告、竞价广告和程序化交易广告的特点、涉及的技术以及发展历程。
- 计算广告的系统架构:讨论计算广告的整体系统架构(系统架构大图)以及涉及的具体技术和中间件。
- 计算广告的点击率预估:讨论CTR预估的基本技术,从传统机器学习算法和深度学习算法中各挑例子进行讲解。CTR预估是目前竞价广告和程序化广告中最重要的技术,也是机器学习发挥作用最大的地方。
- 基于合约广告的在线分配:讲解在线应用、实时技术以及最优化技术。
📜 合约广告
首先介绍的是合约广告。顾名思义,合约广告是广告主与平台签订合约。合约内容一般是:广告主在平台的特定位置展示其广告创意。典型的例子是视频广告,例如优酷在百度上投放推广视频。通常,合约会以展示次数作为最终计费条件。
广告可以分为三个阶段:曝光、点击和转化。
- 曝光:广告投放在平台上,被展示出来,就是一次曝光。
- 点击:用户对广告感兴趣并点击进去。
- 转化:用户点击后,在广告主网站发生购买或其他行为,成为网站用户。
合约广告一般是针对曝光进行计费的,即只要展示了广告就计费。这种付费方式称为CPM。CPM的意义在于关注长期价值和流量价值。例如,优酷在百度投广告,更看重的是通过百度吸引更多用户进入优酷网站浏览,产生流量效应,而不是用户具体看了什么内容。因此,当点击收益不大,而更考虑长期价值和流量价值时,通常会使用合约广告。


合约广告的投放方式通常涉及以下需求:
- 受众定向:广告主会圈定一部分目标人群进行投放,这部分技术称为受众定向。广告主会告诉平台他们想圈定哪类人群。
- 投放:将广告投放到具体每个人的视角上,确保有人能看到。
- 担保式投送:广告主可能对广告投放的数量或其他指标有要求,例如保证至少达到1000万次的投放量。这引出了合约广告的技术难点。

合约广告的技术难点在于:
- 在线分配:平台流量很难精确预估。平台需要实时、动态地分配流量给不同的广告主,以满足他们的担保要求,同时保持整体均衡。例如,前一个小时投放过多,下一个小时就需要减少投放。
- 受限最优化:当多个广告主竞争同一人群的流量时,如何保证每个广告主都能达到满足的量级,并使整体效果最优?这是一个受限最优化问题,通常与在线分配结合解决。业界常用运筹学、线性规划、二次优化等技术进行计算。
🎯 受众定向小知识
广告主如何进行受众定向?广告主通常拥有用户信息,这些信息可能是自己总结的,也可能是平台方提供的(在合约广告中通常是平台方提供)。典型的受众定向方法包括:
- 地域定向(如只投给北京市的人)
- 人群属性定向(如只投给青年人)
- 频道定向
- 上下文定向
- 行为定向
- 精准位置定向
- 重定向
这里重点解释两个概念:
- 上下文定向:根据当前系统请求的状态来决定广告投放给谁的过滤条件。这是一个动态标签,例如根据用户实时的地理位置信息决策。
- 重定向:广告主对曾经在其平台有过行为(如购买)的用户进行重点投放,希望他们再次光顾。
受众定向依赖于标签体系。在合约广告中,通常由广告主维护。标签体系一般有两种:
- 结构化标签:具有上下级从属关系的标签。
- 平行化标签:各个标签之间没有严格的从属或互斥关系。
合约广告更倾向于使用具有上下级关系的结构化标签。
总结:合约广告的核心是按曝光计费,原因在于考虑长期和流量价值。其难点在于在线分配和受限最优化问题。
⚖️ 竞价广告
随着市场精细化程度提高、需要更精准的定向标签、以及广告主数量不断膨胀,广告技术从合约广告进化到了竞价广告。与合约广告最大的区别在于,竞价广告不再保证流量,而是保护单位流量的成本,更关注引流效果。此时,计费方式从按曝光计费转变为按点击计费,即CPC。
典型的例子是搜索广告,例如百度搜索结果上方的广告。搜索广告是竞价广告的典型代表,因为用户搜索时目的性强,平台能结合用户信息和广告主标签进行更精准的投放。
竞价广告的具体需求包括:
- 精准人群定向:通过用户信息、上下文特征、广告主沉淀的用户画像等进行精准投放。
- 竞价机制:由于广告主数量多且不保量,平台需要决定广告的排序。常用的排序公式是 ECPM = CTR × Bid。
- CTR 是点击率,由平台方预估。
- Bid 是广告主愿意出的价格,由广告主设定。
平台的目标是最大化ECPM。广告主之间因此存在竞价。目前应用最广的竞价机制是广义第二高价。例如,两个广告主竞价,一个出价100元,一个出价200元。出价200元的广告主获胜,但他实际支付的价格是第二高的出价(即100元)加上一个很小的单位(如0.01元)。
竞价广告的难点在于平台方的CTR预估。CTR预估的准确性直接关系到平台的收益(ECPM),因此是机器学习应用最广泛的领域之一。
🔄 程序化交易广告(实时竞价)
从竞价广告中又发现了新的问题:出价逻辑过于封闭,无法实时调整,也无法自主选择流量。例如,广告主可能发现给北京人投100元不合适,希望根据不同人的情况动态调整出价。这就产生了实时竞价。
实时竞价的产品形式是:每次广告展示时都实时出价。与传统竞价广告不同,在实时竞价中,平台方(SSP)会将竞价请求发送给广告交易平台(ADX),ADX再向各个需求方平台(DSP,即广告主端)发起实时竞价。每个DSP根据当前广告位信息,实时决定是否出价以及出价多少,然后返回给ADX和SSP,最终由平台方决定展示哪个广告。

实时竞价的需求同样是精准人群定向,但竞价机制变成了实时竞价。计费方式也是CPC。其难点在于需求方的精准定价,即广告主(DSP)需要更精准地预估自己的转化率和出价。


🏢 需求方层级组织

需求方的广告组织一般分为三个层级:
- 广告计划:一次广告的合同,例如推广“步步高手机”这个产品。
- 广告组:具体的投放策略,例如针对年轻人或在百度平台投放。
- 广告创意:具体的广告内容,如针对男性和女性设计的不同图片、标题、文案等。

🔍 再谈搜索广告
搜索广告是最常见的竞价广告形式。
- 查询扩展:平台会根据搜索关键词进行扩展匹配,如精准匹配、短语匹配、模糊匹配、否定匹配等。
- 广告位:一般分为北区(搜索结果上方)、南区(搜索结果下方)和东区(搜索结果右侧)。北区价格通常最高。
- ECPM计算:依然是 CTR × Bid。
📊 竞价广告 vs. 合约广告
- 更精细的人群定向:竞价广告可以利用平台方更精细的用户数据(如兴趣标签),而不仅是广告主提供的粗略标签。
- 中小广告主成为主流:竞价广告降低了投放门槛。
- 数据为核心的产品体系:CTR预估完全由平台方通过数据分析、模型优化来主导。
📚 相关术语详解


- RTB:实时竞价。
- ADX:广告交易平台,连接SSP和多个DSP。
- DSP:需求方平台,代表广告主利益,进行精准出价。
- SSP:供给方平台,代表媒体(平台方)利益,管理广告位。
- 重定向技术:根据用户在网站的行为(浏览、下单)进行定向投放,是计算广告中的重要技术。
- DMP:数据管理平台,管理人群标签等数据。

🗺️ 广告演变图总结
广告发展脉络可概括为:线下广告 → 合约广告(展示量担保)→ 精准定向广告 → 竞价广告。同时,搜索广告的蓬勃发展也推动了上下文广告的发展,最终汇入一般竞价广告,并进一步实时化,发展为程序化交易广告(实时竞价广告)。
🏗️ 第二部分:计算广告的系统架构
计算广告的系统架构通常包含以下几个核心部分:
- 分布式计算平台:负责核心计算任务,如点击率建模、在线流量分配等。
- 广告投放机:线上服务系统,处理广告请求,负责广告的召回、排序、ECPM计算和收益管理。
- 召回:从海量广告中快速筛选出少量候选广告。分为业务召回(根据规则过滤)和算法召回(如向量召回)。
- 排序:对召回结果进行精细排序,常用深度学习模型。之后可能还有基于收益管理和在线分配的重排序环节。
- 数据高速公路:负责实时将日志数据推送到流计算平台,并打通离线和在线数据,提供一致的开发体系。
- 流式计算平台:处理实时数据,包括实时受众定向、实时点击反馈、构造实时特征,以及计费、反作弊等业务逻辑。
相关软件与中间件包括:Hadoop(大数据存储计算)、Kafka(日志消息队列)、Spark(分布式计算引擎)、Storm/Flink(流计算引擎)、Redis(缓存)、Elasticsearch(搜索)等。
📈 第三部分:计算广告的点击率预估
CTR预估是互联网公司进行流量分配的核心依据,对于平台、用户和广告主三方都至关重要。它直接关系到平台的收入,因此是机器学习最重要的应用领域之一。


CTR预估问题可以形式化为:给定用户U、物品(广告)I、上下文C,预测点击结果Y(0或1)。特征来自离线特征(用户画像)和实时特征(如近期点击率),标签来自系统日志。
🤖 传统机器学习算法:逻辑回归
逻辑回归是广义线性模型,适用于离散特征。其模型公式为:
P(Y=1) = σ(β^T X)
其中σ是Sigmoid函数,将线性结果映射到(0,1)区间。
逻辑回归模型简单,但需要复杂的特征工程,如特征离散化、二阶特征组合等。也有自动特征组合方法,如GBDT+LR。逻辑回归是计算广告领域最基础且重要的模型之一。


🧠 新一代算法:深度学习(以Wide & Deep为例)


Wide & Deep模型由谷歌提出,采用神经网络并联联合训练的思路。
- Wide部分:类似逻辑回归,处理稀疏的离散特征,擅长记忆(捕捉特性)。
- Deep部分:使用深度神经网络(MLP),处理稠密特征,擅长泛化(捕捉共性)。
该模型能同时捕捉共性和特性,结构上是Deep部分多层堆叠后,与Wide部分并联输出。
⚡ 在线学习
在线学习旨在实现模型和特征的实时更新。
- 模型实时化:在线系统可以实时用新数据更新模型,而非天/周级别的离线更新。
- 特征实时化:例如,统计广告最近1小时、3小时等的点击量(滑窗统计)。如何精准、高效地计算这些实时特征是一个重要课题。
在线学习涉及的技术包括:
- 流计算引擎(Spark Streaming, Storm, Flink)。
- 合理的样本拼接与时间窗口设定。
- 在线样本的过滤与采样。
- 适配的优化算法(如Google的FTRL),注重稀疏性,防止模型膨胀和过拟合。

🧮 第四部分:基于合约广告的在线分配


回顾合约广告,其核心问题是在线分配和受限最优化。

流量预测是在线分配的基础。问题定义是:给定标签组合和ECPM阈值,估算流量。实践中常用基于时间序列的短期预估(如根据过去15分钟预测未来15分钟),以指导下一个时间段的流量分配。这涉及流量反向检索(根据用户标签查找可投广告)和频次控制(通过服务端或客户端统计控制广告展示频率)。
在线分配问题的数学定义是优化一个目标函数(如总收益),并满足一系列约束条件。主要分为两类:
- 担保问题:保证广告主的流量不低于承诺值
D_a。 - 有预算的出价问题:广告主的消耗不超过其总预算
B_a。
实用优化算法包括线性规划、二次规划等。通过拉格朗日乘子法和KKT条件可以求解其对偶问题,有时对偶问题更易求解。此外,还有基于对偶的紧凑分配方案,以及通过重排序技术进行实时调整和补偿。
🎯 课程总结
本节课我们一起学习了计算广告的市场与技术全景。
- 我们首先了解了在线广告与计算广告的基本概念,以及平台方、广告主、用户三方体系。
- 接着,我们深入探讨了计算广告的三种主要产品形态:合约广告(按曝光计费,重在担保和长期价值)、竞价广告(按点击计费,核心是CTR预估和竞价机制)和程序化交易广告/实时竞价(实时出价,难点在需求方精准定价)。
- 然后,我们剖析了计算广告的系统架构,包括分布式计算、广告投放机、数据流水线和流式计算平台。
- 之后,我们重点讲解了点击率预估这一核心技术,从传统的逻辑回归到深度学习的Wide & Deep模型,并介绍了在线学习的概念与技术。
- 最后,我们回归到合约广告的在线分配问题,探讨了其数学定义和实用的优化算法。


希望本课程能帮助你建立起对计算广告领域的基本认知框架。
人工智能—计算广告公开课(七月在线出品) - P3:在线广告的CTR预估 📈
在本节课中,我们将要学习在线广告中一个核心且极具商业价值的技术方向——点击率预估。我们将从背景介绍开始,逐步深入到具体的模型应用,包括逻辑回归、梯度提升树以及因子分解机等,并了解如何在实际工业场景中构建和优化CTR预估系统。
背景介绍:为什么CTR预估如此重要? 💰
上一节我们介绍了课程的整体框架,本节中我们来看看CTR预估的背景和重要性。


对于搜索引擎、电商平台、社交媒体等互联网公司而言,广告是主要的营收来源。例如,Google、百度、Facebook、腾讯等巨头的核心部门都需要解决广告精准投放的问题。
在线广告有多种形式,例如根据用户行为的定向广告,或结合上下文(如地理位置)的广告。广告系统需要平衡平台、广告主和用户三方的利益,这比单纯的推荐系统更为复杂。
广告的收费模式主要有三种:CPM(按千次展示付费)、CPC(按点击付费)和CPA(按行为付费)。目前主流的模式是CPC。在这种模式下,平台收入直接与广告的点击次数挂钩。因此,点击率 就变得至关重要。
点击率 的计算公式为:
[
CTR = \frac{\text{点击次数}}{\text{曝光次数}}
]
准确预估CTR,意味着能将更相关、用户更可能点击的广告展示出来。这不仅能提升平台收入,也能减少对用户体验的干扰。由于这个问题极具商业价值且技术挑战大,Kaggle等平台曾多次举办相关的公开竞赛。
核心挑战与解决方案概览 🧩

上一节我们了解了CTR预估的商业背景,本节中我们来看看解决此问题的技术路径和常用模型。

CTR预估本质上是一个二分类问题(点击/未点击),但要求模型能输出点击的概率值,以便对多个广告候选进行排序。


以下是工业界CTR预估模型发展的一个简要脉络:


- 逻辑回归: 作为最基础的基线模型,因其简单、高效且可解释性强,被广泛使用。
- 梯度提升树: 例如GBDT,常被用来进行特征组合与变换,其输出可作为特征输入给逻辑回归模型,以提升效果。
- 因子分解机: 包括FM及其改进版FFM,能有效建模特征之间的交互,在CTR预估任务上通常能取得比逻辑回归更好的效果。
- 深度学习模型: 随着深度学习的发展,出现了如 Wide & Deep、DeepFM 等模型,尝试结合传统模型与深度神经网络的优势,进一步提升预估性能。

接下来,我们将重点剖析最常用且作为基线系统的逻辑回归模型,及其在Spark MLlib中的实现流程。

实战:基于Spark MLlib的逻辑回归CTR预估 🔧

上一节我们概述了各类模型,本节中我们深入看看如何用逻辑回归构建一个实际的CTR预估流程。我们将以一个Kaggle展示广告竞赛的数据为例。
竞赛数据通常包含数十个特征字段,大致分为两类:
- 数值型特征: 例如用户历史行为计数。
- 类别型特征: 例如用户ID、广告类别ID等,这些数据通常经过脱敏处理。
我们的目标是:给定特征 X,预测用户点击广告的概率 P(Y=1|X)。
整个工作流程可以概括为以下几个步骤:
第一步:数据加载与解析
首先,我们需要将原始数据加载到Spark中,并解析出标签列 Y(点击与否)和特征列 X。
# 伪代码示例:解析数据行
def parse_line(line):
fields = line.split(' ')
label = int(fields[0]) # 第一列为标签
features = [process(field) for field in fields[1:]] # 处理后续特征列
return LabeledPoint(label, features)
第二步:特征工程——处理类别型特征


类别型特征不能直接输入模型,需要进行编码。最常用的方法是 独热编码。

以下是处理流程:
- 建立索引: 为每个类别型特征的每个可能值建立一个唯一的整数索引。
- 编码转换: 根据索引,将每个类别值转换为一个稀疏向量。例如,特征“城市”有3个值(北京、上海、深圳),索引后,北京可能表示为
[1, 0, 0]。 - 特征组合: 将所有处理后的数值型特征和编码后的类别型特征向量拼接成一条最终的特征向量。



第三步:训练逻辑回归模型
将处理好的特征向量和标签输入逻辑回归模型进行训练。逻辑回归使用Sigmoid函数将线性组合的结果映射为概率。
逻辑回归的预测公式为:
[
P(Y=1|X) = \frac{1}{1 + e^{-(w^T X + b)}}
]
其中,(w) 是权重向量,(b) 是偏置项,(X) 是特征向量。
第四步:模型评估与超参数调优
我们需要使用交叉验证等方法来评估模型性能(常用AUC指标)并选择最优的超参数(如正则化系数)。
# 伪代码示例:网格搜索与交叉验证
param_grid = ParamGridBuilder() \
.addGrid(lr.regParam, [0.01, 0.1, 1.0]) \
.build()
crossval = CrossValidator(estimator=lr,
estimatorParamMaps=param_grid,
evaluator=BinaryClassificationEvaluator(),
numFolds=5)

第五步:模型预测

对新来的广告请求数据,重复第二步的特征处理流程(使用训练时生成的索引映射关系),然后使用训练好的模型进行概率预测。
逻辑回归实践中的关键点与挑战 ⚠️
上一节我们走通了LR模型的构建流程,本节中我们来看看在实际应用中会遇到哪些具体挑战。
虽然逻辑回归模型简单,但在工业级大数据场景下应用,仍需注意以下几点:
- 特征重要性: LR模型的效果严重依赖于特征的质量。如何结合业务知识构造有效的特征(例如交叉特征、统计特征)是关键。
- 特征稀疏性: 独热编码会产生极高维度的稀疏特征向量,需要高效的稀疏矩阵计算支持。
- 模型校准: LR输出的概率值需要尽可能接近真实的点击频率,这关系到后续的排序和计费。
- 在线学习: 为了快速适应数据分布的变化,系统可能需要支持模型的在线更新。
正是这些挑战,推动了GBDT+LR、FM等更复杂模型的应用,它们能自动进行特征组合,在一定程度上缓解了人工特征工程的负担。
总结与展望 🚀
本节课中我们一起学习了在线广告CTR预估的核心知识。
我们从CTR预估巨大的商业价值出发,理解了它为何是互联网公司的核心技术。随后,我们梳理了从逻辑回归、梯度提升树到因子分解机和深度学习模型的技术演进路线。最后,我们详细拆解了如何使用Spark MLlib构建一个基于逻辑回归的CTR预估基线系统,并探讨了实际工程中的关键点。


尽管逻辑回归作为基线系统仍被广泛使用,但工业界正在不断探索和应用FM、FFM以及Wide & Deep、DeepFM等深度学习模型,以期在复杂的特征交互中捕捉更深层的模式,进一步提升预估准确性。掌握从传统模型到深度学习模型的演进思路与实践方法,是深入计算广告领域的重要一步。

人工智能—计算广告公开课(七月在线出品) - P4:Lookalike相似人群拓展项目实战 🎯

在本节课中,我们将学习Lookalike相似人群拓展项目。这是一个在推荐和广告领域非常经典的业务场景,源于2018年的腾讯广告算法大赛。从2017年到2020年,腾讯大赛的赛题都非常贴合真实业务,极具学习和研究价值。通过对赛题的复盘和拆解,我们可以深入理解解决方案的思路,学习如何进行特征工程、挖掘强信息,以及应用哪些模型来解决真实场景下的问题。

本节课将从五个部分展开分享:赛题背景、探索性数据分析、特征工程、CTR模型建模以及最后的模型融合。

1. 赛题背景 📋
首先,我们来了解Lookalike的具体业务场景。Lookalike即相似人群扩展。客户会上传一部分指定的高质量人群,这部分人群被称为“种子人群”。业务的目标是依托于种子人群,从中找到一些显著的画像特征,然后在大盘活跃用户中寻找与种子人群高度相似的“扩展人群”。这个过程需要依靠建模,而非人工判断。
具体流程是:我们拥有种子人群以及向他们投放的广告。根据种子人群对广告的点击(正向样本)与不点击(负向样本)行为进行建模,从而提取出这群人的共同偏好和兴趣。然后,我们从人群库中向其他用户推送同类广告,观察他们是否发生点击或转化。如果发生,则认为这些用户与种子人群具有相似的兴趣,可以归为扩展的相似人群。
Lookalike主要有三个业务作用:
- 更好触达意向用户:扩展原有人群,覆盖更多具有相同意向的用户。
- 更高的互动转化可能性:相似人群更有可能对广告产生互动和转化。
- 找到潜在目标人群帮助拉新:这是用户增长策略中的重要环节,可以帮助唤醒成熟用户或吸引新用户。
整个业务流程分为三步:
- 上传种子用户。
- 基于用户画像提取用户特征。
- 扩大受众,展示相关广告。
本赛题对此流程进行了简化。我们不需要从全量用户中寻找,数据已经提供了候选用户(UID)和对应的广告(AID)。我们的任务是预测给定的“用户-广告”对是否会发生转化(如点击),这本质上可以转化为一个点击率(CTR)预测问题。
2. 赛题任务与数据 🗂️

上一节我们介绍了业务背景,本节中我们来看看具体的赛题任务和数据构成。


赛题任务:本赛题提供了几百个种子人群(即种子包),每个种子人群对应一类广告(AID)。选手需要根据提供的训练数据,预测测试集中给定的“用户-广告”对是否属于该种子包(即用户是否会点击该广告)。预测结果需要以概率形式(如0.76)提交。


评价指标:采用按种子包加权的AUC(Area Under Curve)进行评估。这是因为每个种子包对应的广告特性不同,用户行为分布也不同。为每个种子包单独计算AUC后再进行加权平均(类似GAUC的思想),能更公平、准确地评估模型针对不同广告的排序能力。
数据构成:数据已做脱敏处理,时间范围为30天(但未提供具体时间戳)。数据分为四个部分:
- 训练集:包含
AID(广告ID)、UID(用户ID)和label(是否点击/转化)。 - 测试集:包含
AID和UID,需要预测label的概率。 - 用户特征:每个用户ID对应一系列特征,包括年龄、性别等单值特征,以及兴趣、关键词等多值特征(特征值以逗号分隔,如“125,8”)。
- 广告特征:每个广告ID对应的特征,如广告主ID、素材ID、广告类别等。
我们的特征工程将主要围绕AID和UID展开。
3. 探索性数据分析 🔍


在开始构建模型之前,我们必须先理解数据。探索性数据分析(EDA)是至关重要的第一步,它能帮助我们了解数据规模、类型、分布及潜在问题。
我们对数据进行了基本统计:
- 训练集:用户ID(UID)的唯一值约有780多万个,广告ID(AID)有173个。UID的重复率很低。
- 测试集:用户ID的唯一值约有200多万个,与训练集的用户重合度很低。广告ID同样为173个,与训练集完全一致。
关键发现:
- 由于用户ID在训练集和测试集之间重复很少,且新用户ID会不断出现,因此不能直接将UID作为模型特征,否则会导致严重的冷启动问题,模型无法泛化。但可以围绕UID构造统计特征。
- 广告ID(AID)在训练和测试中完全一致,可以直接使用或基于其构造特征。
通过EDA,我们对数据有了初步认识,接下来就可以着手进行特征工程了。
4. 特征工程 ⚙️
特征工程是模型效果的关键。本节我们将介绍针对本赛题的各种特征构造方法。
以下是构造特征的主要思路:
1. 多值特征处理
用户特征中的兴趣、关键词等是多值特征。处理方法包括:
- 展开为稀疏向量:使用
CountVectorizer或TfidfVectorizer。例如,对于兴趣“125,8”,可以将其视为文本,转换为词频或TF-IDF向量。# 示例:使用 CountVectorizer from sklearn.feature_extraction.text import CountVectorizer vectorizer = CountVectorizer(tokenizer=lambda x: x.split(',')) X = vectorizer.fit_transform(user_interests) - 降维:展开后维度可能极高,可以使用
PCA、NMF或SVD进行降维,得到稠密、低维的特征表示。
2. 基础特征
直接使用原始的单值特征,如年龄、性别、广告尺寸等,作为类别特征或数值特征。
3. 统计特征
- 计数(Count):例如,用户历史点击广告的总次数、广告被点击的总次数。
- 唯一值计数(Unique Count):例如,用户兴趣列表中不同兴趣的个数,反映兴趣广度。
- 点击统计:用户或广告的点击次数,反映活跃度。
- 长度特征:多值特征列表的长度。
4. 比例特征
也称为目标编码(Target Encoding),能刻画用户对特定属性的偏好强度。
- 公式:
Retail(UID, AID) = 用户UID点击广告AID的次数 / 用户UID的总点击次数 - 注意:为防止数据泄露,需使用交叉验证的方式计算;对于稀疏行为,需加入贝叶斯平滑。
5. 交叉组合特征
将不同特征组合,得到更细粒度的信息。
- 示例:将“性别”和“年龄段”组合成新特征“性别_年龄段”,能更精细地刻画用户群体。
5. CTR模型建模 🤖
特征准备就绪后,我们进入模型构建阶段。本赛题可用的模型非常多样。
1. 树模型(LightGBM / XGBoost)
这类模型是处理结构化数据的利器,稳定且强大。
- LightGBM:采用叶子节点(leaf-wise)分裂和直方图算法,训练效率高,能避免过拟合。
- XGBoost:采用层级(level-wise)分裂和预排序算法,精度高但效率相对较低。
2. 因子分解机及其变种
- FM(Factorization Machines):擅长捕捉二阶特征交叉。其核心公式为:
ŷ = w0 + Σwi*xi + ΣΣ<vi, vj>*xi*xj
其中,<vi, vj>是特征i和j的隐向量内积。 - FFM(Field-aware FM):在FM基础上引入“域”的概念。同一个特征在与不同域的特征交叉时,使用不同的隐向量,更贴合实际(例如,“男性”与“篮球”交叉和与“化妆品”交叉的重要性不同)。
- NFFM/DeepFM:将FM与深度神经网络结合,FM部分负责显式低阶特征交叉,DNN部分负责隐式高阶特征交叉,建模能力更强。
3. 验证策略
为保证线上线下一致性,我们采用了特殊的验证方式:
- 首先,按广告ID(AID)将训练集切分出20%作为离线验证集,其构成方式与测试集一致。
- 然后,在剩余的80%数据上,再进行5折交叉验证来训练模型和调参。
这种方式虽然损失了部分训练数据,但能最大程度模拟测试集环境,确保模型稳定性。
6. 模型融合 🧩
当单一模型达到瓶颈时,模型融合是提升效果的最终手段。融合的核心在于利用模型的差异性。
融合的理论基础:
- 特征差异:使用不同的特征子集训练模型。
- 样本差异:通过采样(Bagging)使模型看到不同的数据。
- 模型差异:使用不同类型的模型(如树模型、FFM、DNN)。
常用融合方法:
- 加权平均/投票:对多个模型的预测结果进行加权平均或投票。
- Stacking:
- 第一层:用多个基模型(如LGB, XGB, FFM)对训练集进行K折交叉验证预测,得到一组新的特征(即每个样本的K个预测值)。
- 第二层:将这些新特征与原始特征拼接,训练一个元模型(如线性回归、浅层树模型)进行最终预测。
- 这种方法能有效融合不同模型的“知识”。
高级技巧:
- 可以构建多层级、多链路的复杂Stacking结构。
- 对于类别特征,在输入树模型前可进行计数编码、目标编码等;在输入神经网络前则通过嵌入层(Embedding)转化为稠密向量。


总结 📝
本节课我们一起学习了Lookalike相似人群拓展项目的完整实战流程。
我们从理解业务背景出发,明确了将业务问题转化为CTR预测问题的思路。接着,通过探索性数据分析深入了解了数据构成和关键点。然后,我们重点讲解了特征工程,包括多值特征处理、统计特征、比例特征和交叉特征的构造方法。在模型建模部分,我们介绍了适用于本场景的树模型、因子分解机及其变种模型。最后,我们探讨了通过模型融合来进一步提升效果的策略与技巧。


这个赛题是计算广告领域的经典案例,其中涉及的特征处理、模型选择和融合思路,对于从事推荐、广告、用户增长等相关工作的同学具有很高的参考价值。建议课后复现相关代码,并研究其他优秀选手的开源方案,以加深理解。
人工智能—计算广告公开课(七月在线出品) - P5:带你从头到尾实战广告转化率预估 🎯
在本节课中,我们将学习广告转化率预估的核心概念、技术流程,并以2017年腾讯社交广告大赛为例,深入解析从数据处理、特征工程到模型构建与融合的完整实战方案。课程最后,我们还将拓展介绍多目标学习在解决转化率预估难题上的应用。
第一部分:转化率预估背景知识
互联网广告的商业模式主要基于几种出价和计费方式。
- CPM:按千次展现扣费。
- CPC:按点击扣费。
- CPA:按转化行为(如下载、购买)扣费。
- OCPC:基于预估转化率进行智能出价,按CPC扣费。
典型的互联网广告转化漏斗是:曝光 → 点击 → 转化(如访问、咨询、下载、购买)。CPC优化点击层,而OCPC和CPA则需要参考下层的转化效果。
因此,转化率预估是CPA和OCPC等模式的关键技术。其核心是给定一个广告展示对象,通过统计或建模方法预估其转化概率。
以下是几个核心指标的定义:
- 点击率:
CTR = 点击次数 / 展现次数 - 转化率:
CVR = 转化次数 / 点击次数 - 点击转化率:
CTCVR = 转化次数 / 展现次数 = CTR * CVR
本次课程聚焦于CVR预估,即预测用户点击广告后发生转化行为(如APP安装、商品购买)的概率。
该任务的特点在于数据海量,且转化行为数据相对于点击数据更为稀疏。如何在海量数据中设计有效特征并解决数据稀疏性问题,是本次介绍的重点。
第二部分:腾讯社交广告转化率预估赛题解析


上一节我们介绍了转化率预估的基本概念,本节中我们来看看一个具体的实战案例:2017年腾讯社交广告转化率预估大赛。
该赛题使用了腾讯社交平台(如朋友圈、QQ空间)的真实广告日志数据。提供了30天的APP安装流水,以及第17至30天共14天的广告点击日志作为训练集。目标是利用这些数据,预测第31天广告被点击后发生APP激活(即转化)的概率。
评估指标为二分类任务常用的对数损失函数 Log Loss。
数据包含以下信息:
- 用户信息:用户ID、年龄、性别、教育程度等。
- 广告信息:APP ID、广告主ID、广告素材ID、推广计划ID、APP类别等。
- 上下文信息:广告位位置、网络类型、点击时间等。
解题的一般思路流程为:数据分析 → 数据去噪 → 特征工程 → 模型构建与融合。
数据去噪
由于转化行为链路较长,数据可能存在回流延迟。针对此问题的去噪方法包括:
- 删除每个APP在最后一次转化发生之后的所有点击日志数据。
- 对于发生重大更新的APP,若其前后转化率差异巨大,则删除更新前的历史数据。
特征工程
特征工程是机器学习任务的上限。对于本赛题,特征主要包括以下几类:
以下是转化率相关特征,旨在从不同维度挖掘历史转化信息:
- APP历史转化率
- 广告位历史转化率
- 用户历史转化率
- 用户-APP类别组合转化率
以下是点击相关特征,通过统计分析发现点击模式与转化率存在关联:
- 用户/APP/广告位在不同时间窗口(如分钟、小时、天)内的点击次数统计
- 用户对特定APP的点击次数
- 注意:构造特征时需严防数据泄露,只能使用当前样本时间点之前的历史数据。
以下是安装行为相关特征:
- 用户上次安装APP的时间
- 点击与上次安装的时间差
- 用户近期安装APP的数量
- 用户连续安装的APP ID组合
以下是时间相关特征:
- 将一天24小时划分为48个半小时时段,作为类别特征。
第三部分:模型构建与融合
在完成了数据清洗和特征构造之后,本节我们来看看如何利用模型进行学习和预测。
常用的模型包括传统的梯度提升决策树 GBDT,以及深度学习模型如 Wide & Deep、PNN 和 NFM 等。本次冠军方案的一个创新点是使用了 NFFM 模型。
NFFM 模型结构解析:
- 输入特征首先进行 One-Hot 编码,得到高维稀疏特征向量。
- 通过一个 Embedding 层(可视为一个查找表
lookup table)将每个稀疏特征映射为低维稠密向量。假设有N个特征,每个特征映射为K维向量,则得到N * K的稠密矩阵。 - 模型左半部分是一个简单的线性模型(如逻辑回归),直接对原始稀疏特征进行加权求和。
- 模型右半部分是核心创新:对
Embedding得到的稠密向量进行两两特征间的 逐元素乘积 操作,得到特征交叉信息。这与 FM 模型思想一致,但 NFFM 进一步将交叉后的结果输入到一个多层全连接神经网络中进行高阶特征组合。 - 最终,将线性部分的输出和神经网络部分的输出合并,通过 Sigmoid 函数输出最终的转化概率。
单模型 NFFM 即可达到线上第三名的成绩。
模型融合
为了进一步提升效果,可以采用模型融合策略。冠军方案融合了8个模型:
- 特征:39个简单特征 + 49个复杂特征。
- 模型:1个 GBDT,1个 WDL,2个 PNN,4个 NFFM。
- 方法:对8个模型的预测结果进行 加权平均。
模型融合相比单模型带来了约1%的性能提升。
第四部分:拓展:多目标学习与ESMM模型
前面介绍的方案是在点击样本空间内预估CVR。然而,工业界实践中有两个固有难题:样本选择偏差和数据稀疏性。
样本选择偏差:训练时我们使用点击日志(点击后的样本),但线上预估时需要对所有曝光样本进行推断。这两个样本空间存在差异。
数据稀疏性:转化样本数量远少于点击样本,导致模型训练不充分。
为了解决这些问题,阿里提出了 ESMM 模型,它利用多任务学习在整个曝光样本空间进行建模。
用户行为遵循序列决策模式:曝光 → 点击 → 转化。根据概率公式,有:
P(转化|曝光) = P(点击|曝光) * P(转化|点击)
即 CTCVR = CTR * CVR
ESMM模型结构包含两个主要任务:
- 主任务:预估
P(转化|点击),即 CVR。 - 辅助任务:预估
P(点击|曝光),即 CTR。
模型通过将 CTR 和 CVR 的预估结果相乘,得到 CTCVR 的预估值,并利用 CTCVR 和 CTR 的观测数据(来自全量曝光样本)来联合训练网络。
ESMM模型的两个关键设计:
- 共享Embedding层:CVR网络和CTR网络共享底层的特征Embedding参数。这大大减少了参数量,并使得CVR任务能够利用CTR任务从海量曝光数据中学到的特征表示,缓解数据稀疏问题。
- 全样本空间训练:通过引入CTR作为辅助任务,模型能够利用全量曝光数据进行训练,使训练空间和线上推断空间保持一致,解决了样本选择偏差问题。
实验表明,ESMM模型在CVR预估任务上优于传统方法。
总结与课程回顾
本节课中我们一起学习了广告转化率预估的完整流程。
我们从基础概念出发,了解了CTR、CVR、CTCVR等核心指标以及转化率预估的商业应用场景。接着,我们深入分析了2017年腾讯社交广告大赛的实战案例,涵盖了从数据去噪、特征工程(包括转化率、点击、安装、时间等特征),到复杂模型NFFM的构建原理,以及通过模型融合提升效果的策略。
最后,我们探讨了传统CVR预估的局限性,并介绍了利用多任务学习的ESMM模型如何通过共享参数和全空间训练,有效解决样本选择偏差和数据稀疏性两大挑战。


希望本课程能帮助你建立起对广告转化率预估系统性、实战性的理解。





人工智能—计算广告公开课(七月在线出品) - P6:基于电商广告的点击率预估实战 📊



在本节课中,我们将要学习计算广告领域一个核心且实际的问题——点击率(CTR)与转化率(CVR)预估。我们将以腾讯社交广告的真实比赛数据为例,从问题定义、数据理解到构建基线模型,完整地走一遍实战流程。
问题定义与背景 🌐

上一节我们介绍了课程的整体框架,本节中我们来看看点击率预估问题的具体设定。
计算广告是互联网公司重要的商业模式之一,其核心目标是在给定用户和上下文的情况下,精准预估用户对某条广告的交互概率。主要的优化指标包括曝光、点击和转化。
- 点击率(CTR):用户看到广告后点击的概率,公式为
P(click=1 | ad, user, context)。 - 转化率(CVR):用户点击广告后完成目标行为(如下载、购买)的概率,公式为
P(conversion=1 | click=1, ad, user, context)。
本节课聚焦的腾讯算法大赛题目,即是一个典型的CVR预估问题:预测移动App广告被点击后,用户激活该App的概率。这本质上是一个二分类问题,但模型需要输出一个概率值,而不仅仅是0或1的类别标签。
数据理解与探索 📁
所有数据挖掘问题都是数据驱动的。理解数据是建模的第一步。腾讯提供的数据来源于其社交广告系统“广点通”连续两周的采样日志。
数据主要从三个维度进行描述,对应CVR预估公式中的条件:
- 广告(Ad)特征:描述广告本身的信息。
- 用户(User)特征:描述用户属性的信息。
- 上下文(Context)特征:描述本次广告展示场景的信息。
以下是数据文件的简要说明:
train.csv/test.csv: 核心样本文件,包含每条广告展示的记录ID、标签(是否转化)、点击时间等,并通过creative_id等字段关联其他特征表。ad.csv: 广告特征表,包含广告创意ID、广告主、推广计划、App信息等。user.csv: 用户特征表,包含用户ID、人口统计学信息(年龄、性别等)、地理位置、App安装列表等。position.csv: 上下文特征表,包含广告位信息、网络类型、运营商等。
由于隐私和安全考虑,所有数据均已进行脱敏和哈希处理。原始数据被拆分到多个文件中,因此在实际建模前,我们需要像在数据库中一样,根据关键字段(如creative_id, user_id)将这些表连接(JOIN)起来,形成完整的训练和测试数据集。
基线模型构建:统计方法 📈



在开始复杂的机器学习建模之前,建立一个简单的基线(Baseline)模型是标准流程。这有助于我们快速了解问题的难度,并作为后续模型效果的对比基准。


腾讯官方提供的第一个基线模型是基于广告(Ad)的后验统计方法。其思路非常简单:忽略用户和上下文信息,只统计每个广告的历史转化率,并将此比率作为对该广告未来曝光转化率的预测值。



以下是该模型的核心步骤:






- 数据合并:将训练数据
train.csv与广告特征ad.csv通过creative_id进行关联。 - 分组统计:按照
app_id(或creative_id)对训练集进行分组,计算每个组内标签(label)的均值。由于标签是0或1,这个均值就是该广告历史转化率的统计值。# 伪代码示例 train_data = pd.merge(train, ad, on='creative_id') # 按app_id分组,计算转化率的均值 ctr_by_app = train_data.groupby('app_id')['label'].mean().reset_index() - 预测:对于测试集中的每条记录,根据其
app_id从上述统计结果中查找对应的转化率作为预测值。
这种方法非常直观,但显然忽略了用户个体差异和展示场景的影响。官方结果显示,该统计基线模型的LogLoss评估指标约为0.10988。


基线模型构建:逻辑回归方法 🤖
上一节我们介绍了基于统计的基线模型,本节中我们来看看如何使用经典的机器学习模型——逻辑回归(Logistic Regression)来构建一个更强的基线。
逻辑回归是CTR/CVR预估领域最基础且常用的模型之一,即使引入了深度学习,它仍然因其可解释性和稳定性被广泛使用。
以下是基于逻辑回归的基线模型构建流程:


- 数据准备与合并:同样,需要将训练集、测试集与必要的特征表(如
ad.csv)进行合并,形成包含所有原始特征的数据集。 - 特征工程:这是将原始数据转化为模型可接受输入的关键步骤。
- 识别特征类型:数据中包含数值型特征和类别型特征(ID类特征,如
creative_id,app_id,position_id等)。 - 类别特征编码:对于类别型特征,不能直接将其数值作为输入,因为数字间的大小关系无意义。必须进行独热编码(One-Hot Encoding)。
# 使用sklearn的OneHotEncoder进行编码 from sklearn.preprocessing import OneHotEncoder encoder = OneHotEncoder() # 假设category_features是类别型特征列 X_train_encoded = encoder.fit_transform(train_data[category_features]) - 处理高维稀疏性:独热编码后,特征维度会急剧膨胀。为了高效存储和计算,我们使用稀疏矩阵(Sparse Matrix)来存储这些特征,其中大量元素为0,仅存储非零元素的位置和值。
- 识别特征类型:数据中包含数值型特征和类别型特征(ID类特征,如
- 模型训练与评估:
- 将编码后的稀疏特征矩阵和标签输入逻辑回归模型进行训练。
- 使用
predict_proba方法获取测试样本属于正类(转化)的概率值。 - 官方结果显示,此逻辑回归基线模型的LogLoss约为0.10743,优于纯统计方法。
# 核心代码结构示意
from sklearn.linear_model import LogisticRegression
# 假设 X_train_sparse 是经过独热编码的稀疏训练特征,y_train 是标签
lr_model = LogisticRegression()
lr_model.fit(X_train_sparse, y_train)
# 预测概率
y_pred_proba = lr_model.predict_proba(X_test_sparse)[:, 1]
总结与回顾 🎯
本节课中我们一起学习了电商广告点击率预估的实战入门。我们从计算广告的业务背景出发,明确了CTR和CVR预估的定义与重要性。随后,我们深入分析了腾讯公开赛的数据结构,理解了广告、用户和上下文三类特征。
在模型部分,我们循序渐进地构建了两个基线模型:
- 基于广告的统计模型:简单直接,作为效果的下限参考。
- 基于逻辑回归的机器学习模型:引入了特征工程(特别是独热编码和稀疏矩阵处理),能够初步利用更多特征信息,取得了更好的效果。

这两个基线模型为我们后续尝试更复杂的模型(如梯度提升树、深度神经网络)奠定了坚实的基础。记住,良好的特征工程和合理的基线模型是解决任何数据科学问题的成功开端。
🧠 人工智能—计算广告公开课(七月在线出品) - P7:计算广告的发展和相关技术
在本节课中,我们将要学习计算广告的核心知识。课程将从四个方面展开:计算广告的产品发展简史、系统架构、竞价广告点击率预估以及基于合约广告的在线分配。我们将深入浅出地探讨这些主题,帮助初学者理解计算广告领域的核心概念和技术。
📜 第一部分:计算广告的产品发展简史

上一节我们介绍了课程的整体框架,本节中我们来看看计算广告是如何一步步发展演变的。



计算广告由“计算”和“广告”两部分组成。“广告”的含义众所周知,而“计算”则指利用数学和数据技术,实现广告的精准投放,从而获得比传统广告更大的收益。
在线广告是指在网络和互联网流媒体上投放的广告,例如百度、知乎等网站上的广告。与传统广告不同,在线广告在发展中形成了特定模式,其特点是以人群为投放目标,以产品为技术导向。
广告的本质是实现商业信息的传播。
合约广告
合约广告是最容易理解的形式。其产品形式是在互联网上展示广告创意,例如视频广告。
以下是合约广告的具体需求:
- 受众定向:根据用户属性(如性别、地域)进行广告推送。
- 广告投放:系统根据受众标签进行广告替换和展示。
- 担保式投送:媒体向广告主保证一定的广告曝光量。
合约广告的付费方式是 CPM,即按千次展示付费。其难点在于,当多个广告主对不同标签组合的人群有保量需求时,如何进行复杂的在线分配和约束优化。
竞价广告
竞价广告比合约广告更加精细化。其发展源于市场精细化、定向标签更精准以及广告主数量膨胀。
竞价广告的产品形式是供给方不再保证曝光量,而是保证单位流量的成本,即按点击付费。搜索广告是典型例子。
以下是竞价广告的具体需求:
- 精准人群定向:使用更精细、静态的用户特征进行刻画。
- 竞价机制:通过预估点击率和出价计算期望收益,以最大化收益为目标。


竞价广告的付费方式是 CPC,即按点击付费,通常采用广义第二高价机制。其核心难点在于 CTR预估 的准确性。
程序化交易广告(实时竞价)
程序化交易广告的核心是实时竞价机制。它解决了广告主希望根据实时数据(如用户转化价值)动态调整出价和选择流量的需求。
其产品形式是每次广告展示都进行实时出价。流程通常为用户访问媒体,媒体通过广告交易平台向多个需求方平台询价,各平台根据自身数据实时出价,价高者获得展示机会。
程序化交易广告的付费方式回归到 CPM。其难点在于需求方需要进行精准的实时定价。
广告层级组织与相关概念
一个广告活动通常包含以下层级组织:广告计划、广告组和广告创意。
此外,还需要了解一些核心概念:
- ADX:广告交易平台,类似二级市场。
- DSP:需求方平台,代表广告主利益。
- SSP:供给方平台,代表媒体利益。
- DMP:数据管理平台,负责标签生产与管理。
- RTB:实时竞价。
- 重定向:根据用户历史行为进行再次定向投放。
🏗️ 第二部分:计算广告的系统架构
上一节我们回顾了计算广告的产品演进,本节中我们来看看支撑这些产品的技术系统架构。
计算广告的系统架构通常分为四个部分:在线服务、数据高速公路、分布式计算和流式计算。
在线服务
在线服务是响应用户请求的核心。其主要功能包括:
- 生成会话日志。
- 进行受众定向和行为打标。
- 调用CTR预估模型进行打分。
- 集成商业智能模块。
在线服务负责串联整个线上系统,处理高并发请求,并完成广告的召回、排序和重排序。
数据高速公路
数据高速公路负责日志的收集、管理和分发。其主要功能包括:
- 准实时地将日志推送到其他平台。
- 连接在线和离线系统,供批处理和流处理消费。
常用的相关技术包括 Kafka 和 Flume。
流式计算平台

流式计算平台用于处理实时任务。其主要应用包括:
- 实时受众定向。
- 实时点击反馈统计。
- 计费和反作弊。


常用的流式计算引擎有 Storm、Spark Streaming 和 Flink。

批处理计算平台

批处理计算平台用于离线分析。其主要任务包括:


- 数据分析与BI报表。
- 特征工程与模型训练。


常用的技术包括 Hadoop、HDFS 和 Hive。此外,Redis、Lucene 和 Elasticsearch 也常用于缓存和索引。
🔮 第三部分:竞价广告点击率预估
上一节我们介绍了计算广告的系统骨架,本节中我们深入其中最核心的算法部分——点击率预估。
CTR预估对于提升广告收益至关重要。为了提升预估准确率,业界不断涌现新的方法。
预估方法与技术
CTR预估的技术演进主要分为几个方向:
- 传统机器学习算法:如逻辑回归。
- 新一代机器学习算法:如基于深度学习的DNN、Wide & Deep等模型。
- 更前沿的算法:如在线学习和强化学习。
- 模型与特征融合。
- 优化算法:如SGD、Adam、FTRL等。
CTR预估的数学表示是通过用户、广告和上下文特征来预测点击概率 y = f(user, item, context)。


逻辑回归与特征工程
逻辑回归是CTR预估的基础模型。使用时常需要注意:
- 特征离散化有助于风险均摊和模型鲁棒性。
- 特征组合包括人工组合和自动组合(如使用GBDT+LR)。
Wide & Deep 模型
Wide & Deep 模型综合了记忆和泛化的优势:
- Wide部分:擅长记忆特定、稀疏的特征组合。
- Deep部分:擅长泛化,学习深层特征表示。
模型输入可以是稀疏特征或稠密特征,但需要进行适当处理以适应两部分。
在线学习
在线学习使模型能实时更新,适应数据分布的变化。其实现依赖于:
- 实时计算引擎提供实时特征和数据。
- 时间窗口、样本过滤与拼接等技术。
- 适配的优化算法,如专为稀疏性设计的FTRL。
⚖️ 第四部分:基于合约广告的在线分配
在本课程的最后一部分,我们将探讨合约广告中的一个经典问题——在线分配。
合约广告需要满足广告主的保量需求,同时最大化平台收益,这引出了一个约束优化问题。
流量预测与频次控制

在线分配的基础是流量预测,即预估不同标签组合下的流量供给。常用方法是基于时间序列的短期预估。
频次控制是为了避免用户对同一广告感到疲劳,可通过客户端或服务端策略实现。

在线分配问题建模
在线分配问题可以形式化为一个约束优化问题。其核心约束包括:
- 供给方约束:每个展示机会分配给各广告主的比例之和不超过1。
- 需求方约束:
- 担保问题:保证每个广告主获得约定的最低流量。
- 预算问题:每个广告主的消耗不超过其预算。
解决方案
解决上述优化问题的方法包括:
- 线性规划与二次规划。
- 对偶问题求解:利用拉格朗日乘子法将原问题转化为对偶问题求解,并设计紧凑的分配方案。
- 在线实时调整与补偿:根据全天流量完成情况,动态调整后续时间段的分配策略。
📝 总结


本节课我们一起学习了计算广告的核心知识。我们从计算广告的产品发展简史开始,了解了从合约广告、竞价广告到程序化交易广告的演进过程及其核心概念。接着,我们探讨了支撑这些广告产品的复杂系统架构。然后,我们深入研究了竞价广告的核心——点击率预估的各种模型与技术。最后,我们分析了合约广告中的在线分配问题及其解决方案。希望本课程能帮助你建立起对计算广告领域的整体认知。
人工智能—计算广告公开课(七月在线出品) - P8:计算广告的核心:CTR预估 📈
在本节课中,我们将要学习计算广告领域的核心技术之一:点击率预估。我们将从问题背景出发,了解其重要性,并梳理其核心算法的发展脉络,最后通过一个经典比赛案例来巩固所学知识。
什么是CTR预估?🎯
CTR的全称是Click Through Rate,中文称为点击率。这个问题最初来源于互联网广告,具体来说是搜索引擎中的搜索广告。当用户在搜索引擎中输入关键词时,系统会展示广告。CTR预估就是预测用户点击某个广告的概率。
CTR预估技术之所以重要,被称为“镶嵌在互联网技术上的明珠”,是因为它与搜索引擎公司的核心商业模式和商业收入强相关。这项技术的发展历史几乎伴随着整个互联网技术的发展。
CTR预估的应用场景 📱
CTR预估技术有两大经典应用场景:广告和推荐。
以下是几个典型的产品案例:
- 搜索引擎广告:例如百度和Google的搜索广告,这是其最核心的商业模式。
- 电商广告:例如阿里巴巴的广告系统,在淘宝搜索结果中融入广告商品。
- 信息流推荐:例如今日头条,其信息流内容推荐的核心技术之一就是CTR预估。
此外,任何标签为“是”或“否”(即0或1)的二分类问题,例如广告转化率预估、信用卡欺诈交易识别等,都可以借鉴CTR预估的思路来解决。
机器学习问题建模 🧠
上一节我们了解了CTR预估的应用场景,本节中我们来看看如何将“用户是否会点击广告”这个商业问题,形式化为一个标准的机器学习问题。
这个过程遵循经典的机器学习建模流程:
- 问题定义:预估广告是否被点击是一个二分类问题。标签Y为0(未点击)或1(点击)。
- 特征提取:从数据中抽取特征,表示为X。
- 模型假设:假设标签Y和特征X之间存在一个函数关系H(即我们的算法模型)。模型H带有参数θ。
- 损失函数:训练时需要最小化的目标,对于二分类问题,通常使用交叉熵损失函数。
- 模型训练:通过优化算法(如梯度下降)寻找最优参数θ,使损失函数最小。
- 离线评估:在测试集上评估模型性能,常用指标包括AUC(衡量排序能力)和LogLoss(衡量预估误差)。
- 线上验证:将模型部署到线上进行A/B测试,观察业务指标(如线上CTR)是否提升。
不同的CTR预估算法,本质上对应着不同的模型假设H。
核心算法发展历程 🚀
在明确了问题建模方式后,我们来详细看看CTR预估领域常用的核心算法及其发展过程。这些算法在工业界被广泛使用。
以下是算法演进的主要阶段:
1. 逻辑回归
逻辑回归是CTR预估最经典、应用历史最长的算法之一。其模型假设H是特征X的线性加权和,再经过Sigmoid函数变换,得到点击概率。
公式:P(Y=1|X) = sigmoid(W^T * X)
它的优点是模型简单、可解释性强、易于大规模分布式实现,并支持在线学习。缺点是模型表达能力有限。
2. 因子分解机
为了提升模型表达能力,因子分解机系列算法被提出。它们的核心思想是引入特征之间的交互组合。
- FM:在逻辑回归的基础上,增加了特征两两组合的隐向量内积项,以建模特征间的交互关系。
- FFM:在FM的基础上引入了“域”的概念,同一个特征对于不同域的特征交互使用不同的隐向量,使得特征组合更加精细。
3. 在线学习与FTRL
之前的算法多属于批量学习,即每次训练需要使用全部历史数据,计算开销大。在线学习则支持用最新数据增量更新模型,学习更快,能及时适应数据变化。
FTRL是一种优秀的在线学习优化算法,它通过理论上的Regret分析,保证了在线学习的效果能够逼近批量学习的最优解。FTRL可用于优化逻辑回归、FM等模型。
4. 树模型与集成学习
以GBDT为代表的树模型是另一条技术路线。它通过集成多棵决策树来构建强大的预测模型。
代表工具:XGBoost, LightGBM。
其优点是对连续值特征处理友好、模型鲁棒性强、在小数据量上效果往往优于线性模型。缺点是大规模分布式实现相对复杂,模型结构更新较慢。
5. 深度模型与混合模型
随着深度学习兴起,CTR预估进入了深度模型时代。最具代表性的是Google提出的Wide & Deep模型。
- Wide部分:线性模型(如逻辑回归),负责“记忆”,擅长处理大量稀疏的ID类特征,学习历史数据中频繁出现的模式。
- Deep部分:深度神经网络,负责“泛化”,通过 embedding 和深层网络,学习特征之间的深层、非线性关系,能更好地处理未见过的特征组合。
这种混合架构巧妙地平衡了模型的记忆能力和泛化能力。
6. 特征工程自动化
Facebook提出的GBDT+LR模型,旨在用模型自动完成特征工程。其思路是先用GBDT模型对原始特征进行转换,将每棵树的叶子节点编号作为新的离散特征,再输入给LR模型。这相当于利用树模型自动进行了特征离散化、组合与筛选。
常用工具与资源 🛠️
了解算法后,我们需要掌握实现它们的工具。以下是一些常用工具:
线性模型/FM模型:
- LibLinear / Scikit-learn:单机实现。
- Spark MLlib:分布式实现。
- LibFM / xLearn:专门针对FM/FFM的高效实现。
树模型:
- XGBoost
- LightGBM
深度模型:
- TensorFlow:提供了Wide & Deep的官方教程。
- DeepCTR:一个集成了众多深度CTR模型(如DeepFM, xDeepFM等)的开源项目,方便研究和实验。
实战练习:经典CTR预估比赛 🏆
理论需要结合实践。本节我们将通过一个经典的CTR预估比赛——Criteo Display Advertising Challenge,来巩固所学知识。
这是一个非常贴近真实广告业务场景的比赛,数据质量高,特征具有代表性。冠军解决方案中包含了大量特征工程的技巧和模型融合的智慧。
学习建议:
- 复现冠军方案:仔细研究冠军分享的技术报告和代码,尝试使用LibFM等工具复现其核心特征工程和模型,理解每一步的动机。
- 尝试新算法:用课程中学到的GBDT、Wide & Deep等新模型在该数据集上重新实验,看看能否超越几年前的冠军成绩。
- 深入思考:在实践过程中,思考特征构建、模型选择背后的原因,这是提升机器学习实战能力的关键。
通过这个完整的练习,你将深刻理解CTR预估问题的核心挑战和解决方案。
总结 📝


本节课我们一起学习了计算广告的核心——CTR预估技术。我们从其商业背景和重要性讲起,了解了如何将点击率预测问题建模为机器学习中的二分类问题。随后,我们系统梳理了从经典的逻辑回归、因子分解机,到在线学习FTRL,再到树模型GBDT,以及当前主流的深度混合模型(如Wide & Deep)的算法发展历程。最后,我们介绍了相关的实用工具,并通过一个经典比赛案例指出了将理论付诸实践的最佳路径。掌握CTR预估,不仅是进入计算广告领域的钥匙,也是理解大规模机器学习应用的一个绝佳范例。
人工智能—计算广告公开课(七月在线出品) - P9:计算广告及搜索广告入门 🎯
在本节课中,我们将要学习计算广告的基础知识,包括互联网广告的商业模式、核心的拍卖与竞价排序机制,以及成为一名广告算法工程师所需的关键技能。
什么是互联网广告?💻
上一节我们介绍了课程概述,本节中我们来看看什么是互联网广告。互联网广告随着技术发展,其形态也在不断演变。
在PC门户网站时代,如雅虎、搜狐、新浪、网易等网站,广告主要以大型横幅广告(Banner)的形式出现。这种广告售卖媒体资源的方式,与传统的电视广告和报纸广告较为相似。
随后进入PC搜索引擎时代,以谷歌、百度为代表,广告主通过购买关键词进行广告投放。当用户搜索特定关键词时,相关广告会通过竞价排名机制获得展现机会。
在移动互联网时代,广告形式变得更加多样化。以下是几种主要类型:
- 移动社交广告:出现在微信朋友圈、QQ空间等社交平台。
- 移动媒体广告:出现在今日头条、抖音等信息流和短视频平台。
- 电商推广广告:出现在淘宝、京东等电商平台,用于推广商品。
为什么互联网公司热衷广告?💰
互联网公司热衷于广告业务的原因非常简单:广告是其主要收入来源。例如,谷歌母公司Alphabet约84%的营收来自广告,百度约82%的营收来自广告。这也解释了为何广告算法工程师的薪酬颇具竞争力。
互联网广告的商业模式 🏷️
互联网广告主要有三种出价与计费模式:
- CPM:按千次展现收费。广告每展示一千次,广告主即支付一次费用。
- CPC:按点击收费。这是目前流行的一种方式,只有当用户点击了广告链接后,广告主才会被扣费。
- CPA:按行动成本收费。这是一种按广告投放实际效果计费的方式,例如按照有效的用户注册或激活行为进行计费,而不限制广告的投放量。



广告拍卖机制 ⚖️
上一节我们了解了广告的商业模式,本节中我们来看看广告资源是如何通过拍卖机制售卖的。
常见的拍卖方式有多种。例如,公开增价拍卖是电视上常见的形式,参与者依次加价,直到无人再加价时成交。而荷兰式拍卖则是公开减价拍卖,拍卖师从一个高价开始逐步降价,直到有参与者接受当前价格。
在互联网广告中,普遍采用的是第二价格密封拍卖。要理解它,首先需要了解密封拍卖,即每个参与者不知道其他人的出价,例如招投标。在第一价格密封拍卖中,出价最高者获胜,并按自己的出价结算。而在第二价格密封拍卖中,出价最高者同样获胜,但结算时按出价第二高的价格支付。
为什么互联网广告选择第二价格密封拍卖呢?我们可以通过一个简单的博弈模型来分析。在第一价格拍卖中,广告主的收益是其对广告的认可价值减去自己的出价。这会导致广告主有不断试探性降低出价的动力,使得整个竞价系统不稳定。而在第二价格拍卖中,无论广告主如何调整出价,其最优策略始终是“诚实出价”,即出价等于自己对广告的认可价值。这使得竞价系统更加稳定,广告主无需过度关注他人的出价策略。
广告竞价排序机制 📊
在实际场景中,通常有多个广告主竞争同一个广告展示位。这时,系统需要通过竞价排序机制来决定展示哪个广告。
由于广告主可能采用不同的出价模式,系统需要将它们统一到同一个维度进行比较,这个维度就是eCPM。eCPM代表预估的千次展现收益。
以下是不同出价模式折算为eCPM的公式:
- CPM出价:
eCPM = CPM - CPC出价:
eCPM = CPC * pCTR * 1000 - CPA出价:
eCPM = CPA * pCTR * pCVR * 1000
其中,pCTR是预估点击率,pCVR是预估转化率。
假设有三个广告主竞争同一个广告位:
- 广告主A(汽车品牌)采用CPM出价40元。
- 广告主B(餐饮优惠券)采用CPC出价1元,其广告的
pCTR为6%。 - 广告主C(手游下载)采用CPA出价10元,其广告的
pCTR为10%,pCVR为5%。
计算他们的eCPM:
- A: 40元
- B:
1 * 0.06 * 1000 = 60元 - C:
10 * 0.10 * 0.05 * 1000 = 50元
因此,广告主B胜出。但根据第二价格拍卖规则,B的实际扣费会按照第二高的eCPM,即广告主C的50元进行结算。
从这个过程可以看出,预估点击率和预估转化率是广告排序算法中的核心环节。
广告算法工程师的核心技能 🛠️
上一节我们介绍了竞价排序机制,本节中我们来看看支撑这套系统运转的算法工程师需要哪些技能。
广告界有一个著名的“哥德巴赫猜想”:广告主有一半的广告费被浪费了,但不知道是哪一半。广告算法工程师的核心任务之一就是通过技术手段减少这种浪费。
一个典型的广告投放流程涉及用户画像、广告召回、竞价排序、展现与转化跟踪等环节。这其中的技术挑战包括复杂的定向规则、海量的用户与广告数据、高并发的实时请求以及极低的延迟要求。
广告算法涉及的技术点非常广泛,主要包括以下几类:
1. 点击率/转化率预估
这是一个经典的机器学习二分类问题。系统需要预测在特定场景下,某条广告被用户点击或发生转化的概率。使用的特征包括场景特征、广告特征、媒体特征和用户特征。模型从传统的逻辑回归、GBDT,到复杂的深度模型如Wide & Deep、DIN、DIEN等都有应用。提升预估准确性哪怕一个百分点,都可能为大型平台带来每日数百万的收入增长。
2. 自然语言处理技术
在搜索广告等领域应用广泛。主要包括:
- 查询改写:将用户搜索词映射到更规范或更易召回广告的形式。
- 查询分析与召回:理解查询意图并召回相关广告。
- 语义相关性:计算查询与广告之间的语义匹配度。
- 创意生成:自动生成广告标题或文本摘要。

3. 计算机视觉技术
主要用于广告创意环节。例如:
- 图像分类与质量评估:判断图片是否吸引点击且符合法规。
- 智能创意:自动从网页中抓取并组合图文生成广告。
- 图像特征提取:将图像转化为特征向量,作为CTR预估模型的一维特征,常用ResNet、VGG等模型。
4. 机制设计与博弈论
这是计算广告区别于一般机器学习任务的独特领域。例如:
- 智能调价:如何根据广告主的目标自动优化其出价。
- 流量分配:在合约广告中,如何在不同时间段合理分配广告库存。
- 强化学习应用:模拟广告主的出价行为,优化整体拍卖效果。
5. 人群定向与扩展
精准找到目标用户是广告效果的关键。除了基础属性定向,还包括:
- 兴趣定向:根据用户历史行为挖掘其商业兴趣。
- 消费能力定向。
- 相似人群扩展:基于社交网络等图数据,找到与种子用户相似的人群,常用Graph Embedding技术。
对于广告算法工程师而言,需要了解的技术面较广,但通常只需在某一两个领域有深入专长即可,例如精通CTR预估、NLP或机制设计中的任一方向。
总结 📝


本节课中我们一起学习了计算广告的基础入门知识。我们首先认识了互联网广告的形态与商业模式,理解了CPM、CPC、CPA等核心概念。接着,我们深入探讨了支撑互联网广告运行的第二价格密封拍卖机制及其稳定性原理,并学习了如何通过eCPM将不同出价模式的广告放在同一维度进行竞价排序。最后,我们概述了成为一名广告算法工程师可能涉及的五大类核心技术:点击率预估、自然语言处理、计算机视觉、机制设计以及人群定向。这些技术共同构成了现代计算广告系统的基石。

🎓 人工智能—计算机视觉CV公开课(七月在线出品) - P1:2小时杀入视觉赛题前10%




在本节公开课中,我们将学习如何快速入门计算机视觉竞赛,掌握核心解题思路,并了解如何通过系统学习提升在Kaggle等平台上的竞赛成绩。课程内容轻松易懂,旨在帮助初学者在两小时内掌握关键知识点。
📊 第一部分:Kaggle竞赛简介
Kaggle是一个被谷歌收购的全球性数据科学竞赛平台。它以具体问题为导向,聚合跨学科人才,利用数据研发算法模型,探索解决方案。
数据竞赛是一种众包模式。参赛者在规定时间内提交算法模型,平台通过定量和交互式的评分机制进行排名。这种竞赛通常面向公众开放,具有明确的业务背景,旨在解决工业或学术界的实际问题。


与传统的学科竞赛相比,数据竞赛具有以下特点:
- 内容不同:关注数据挖掘、机器学习算法及相关业务,而非特定学科知识点。
- 反馈机制:支持每日多次提交并即时获得评分反馈,而非“一锤定音”。
- 评分方式:采用精确的定量打分(如准确率到小数点后多位)。
- 交互性:提交代码或模型后,系统自动评估并反馈结果,更符合数据挖掘的实际流程。
Kaggle平台为每个比赛提供了完整的页面,包含赛题介绍、数据集、Notebook(在线编程环境)、讨论区和排行榜,是目前最成熟、含金量最高的竞赛平台之一。
竞赛可以根据任务或数据类型进行划分:
- 按任务:可分为结构化数据(如金融风控)、计算机视觉(如图像分类)、自然语言处理(如文本分类)、语音处理等。
- 按数据:可分为结构化数据(表格数据)和非结构化数据(图像、文本、语音)。近年来,非结构化数据相关的竞赛比例显著上升。
本节课我们将聚焦于计算机视觉(CV) 领域的赛题。
👁️ 第二部分:计算机视觉入门指南
计算机视觉是让机器获得“看”和理解能力的一门科学。对于计算机而言,一张图像本质是一个三维张量(矩阵),例如 H x W x C(高度 x 宽度 x 通道数)。然而,人眼并非逐像素理解,而是感知语义信息,这之间存在“语义鸿沟”。
目前的技术尚无法让机器像人一样通用地理解图像。现有的计算机视觉算法多是针对特定任务(如人脸识别、车牌识别)分别建模,属于“狭义人工智能”。
计算机视觉应用广泛,如安防、智慧城市、自动驾驶等。但现有算法在复杂场景(如小目标、遮挡、环境干扰)下仍难以达到100%的精度,存在提升空间。
计算机视觉学习路线建议

以下是系统学习计算机视觉的五阶段路线:
- 计算机视觉基础:学习数字图像处理、颜色空间、特征提取(如边缘、角点)等。
- 机器学习与深度学习基础:掌握机器学习原理、神经网络,特别是卷积神经网络(CNN)的原理与实战。
- 深度学习进阶:了解CNN模型发展脉络(如AlexNet, VGG, ResNet),学习大规模图像检索、行人重识别、目标检测基础等。
- 目标跟踪与语义分割:掌握相关算法与应用。
- 视觉项目实战:进行实际项目演练,可能涉及强化学习、模型部署等。

计算机视觉的核心任务


常见的计算机视觉任务包括:
- 图像分类:识别图像主体类别(如动物分类)。
- 物体检测:定位并识别图像中的单个或多个物体(如行人检测、车牌识别)。
- 实例分割:在物体检测基础上,精确分割出每个物体的像素轮廓。
这些任务是许多高级应用(如自动驾驶中的交通标志识别、安防中的人脸检测)的基础。
深度学习与卷积神经网络
深度学习是机器学习的一个分支,在计算机视觉中占据主导地位,主要因为:
- 精度更高:在多数任务上超越传统机器学习方法。
- 适用性广:特别擅长处理图像、语音等非结构化数据。
- 可迁移性强:在一个数据集上预训练的模型,可以较容易地迁移到新任务上。
深度学习是一种“表征学习”,通过多层网络自动学习数据的层次化特征。CNN因其能有效提取图像的局部特征(如边缘、纹理),成为计算机视觉的首选网络结构。

深度学习 vs. 传统机器学习流程对比:
- 传统机器学习:
输入 -> (人工)特征提取 -> 模型 -> 输出 - 深度学习:
输入 -> 端到端网络(自动特征提取+模型) -> 输出

深度学习能自动提取特征,但需要精心设计网络结构,并常需配合数据增强和正则化方法来防止过拟合。
🧩 第三部分:视觉赛题案例解析 - ImageNet图像分类
ImageNet是一个包含千万级图像、1000个类别的经典数据集,极大推动了深度学习在CV领域的发展。在ImageNet上预训练的模型,其学到的特征(如浅层网络的边缘、纹理信息)具有通用性。
迁移学习的价值
我们可以利用在ImageNet上预训练的模型,通过迁移学习快速适应新任务:
- 复用参数:将预训练模型的参数作为新任务模型的初始化。
- 微调:使用新任务的数据对模型参数进行微调。
迁移学习能显著减少新任务所需的数据量、缩短训练时间、并提供更好的参数起点,从而提高模型精度。
构建图像分类模型的流程
- 确定网络结构:选择或设计CNN网络,如ResNet, EfficientNet等。
- 确定数据增强方法:对训练图像进行变换(如翻转、旋转、裁剪、加噪),以增加数据多样性并防止过拟合。需注意,增强方法需符合任务逻辑(如手写数字可水平翻转,但交通标志可能不行)。
- 确定训练方法:使用随机梯度下降等优化算法,结合反向传播进行训练。现代框架(如TensorFlow, PyTorch)已封装了大部分计算细节。
🏆 第四部分:Kaggle视觉竞赛实战案例
案例一:谷歌涂鸦识别挑战
赛题:对全球用户手绘的涂鸦(共5000万张,340类)进行分类。
难点:数据量巨大(远超ImageNet),图像尺寸大(512x512),训练周期长。
解题核心思路:
- 数据转换:将存储鼠标轨迹的JSON数据绘制成图像。为适配三通道预训练模型,可在不同通道绘制不同粗细的笔画,以编码更多信息。
- 模型选择:在模型复杂度(精度)与训练时间之间权衡,可选择MobileNet等轻量模型起步。
- 渐进式训练:由于直接训练大尺寸图像困难,采用 “逐步放大”策略:
- 先在缩小的图像(如64x64)上训练模型至收敛。
- 然后将图像尺寸放大一倍(如128x128),加载上一步训练好的权重进行微调。
- 重复此过程,直至达到原始尺寸(512x512)。这比直接训练大尺寸图像更高效且精度更高。
- 高级技巧:使用MixUp、CutOut等数据增强方法;利用测试集类别分布均匀的特点进行后处理(如类别平均)。
案例二:孟加拉语手写字符识别
赛题:识别孟加拉语手写字符,每个字符有三个独立标签:字根(G, 168类)、元音(V, 11类)、辅音(C, 7类)。这是一个多标签分类问题。
难点:测试集中出现了训练集未出现过的G-V-C组合(约200种),即出现了“新字符”,容易导致模型在B榜(私有榜)上表现不佳。
解题核心思路:
- 思路一:未知样本检测与分治
- 使用ArcFace等损失函数训练一个模型,使同类特征紧凑、异类特征分离。
- 计算训练集中所有已知字符的特征中心。
- 对测试样本,计算其与所有已知特征中心的距离。若距离过远,则判定为“未知字符”。
- 对“已知字符”和“未知字符”分别用不同策略的模型进行预测(如欠拟合模型独立预测G/V/C,过拟合模型学习组合关系)。
- 思路二:合成未知样本
- 利用孟加拉语的字体文件(TTF/TTC),生成训练集中缺失的G-V-C组合字符图像。
- 使用风格迁移模型,将这些印刷体字符转换为手写风格。
- 将合成数据加入训练集,让模型直接学习所有可能的字符。
案例三:谷歌地标图像检索
赛题:构建地标检索系统,即“以图搜图”,找出包含相同地标的图片。
核心概念:图像检索的本质是特征提取与相似度匹配。
基本流程:
- 特征提取:使用CNN模型(如ResNet)提取图像的特征向量(Feature Map)。
- 特征池化:使用池化层(如GeM, Generalized Mean Pooling)将特征图聚合为一个固定长度的特征向量。池化方式对检索性能影响很大(Max Pooling利于保留关键点)。
- 检索:计算查询图像的特征向量与图像库中所有特征向量的相似度(如余弦相似度),返回最相似的图像。
提分技巧:
- 测试时数据增强:对查询图像进行多次裁剪,分别检索后融合结果。
- 查询扩展:将初次检索到的Top-K结果的特征进行平均,用这个平均特征再次检索,以提升召回率。
- 索引加速:对于大规模图像库,需使用PCA降维、聚类索引等方法加速检索过程。
📝 第五部分:总结与展望
本节课程总结
- 深度学习是基础:掌握深度学习,特别是CNN的原理与技巧,是解决视觉赛题的必要条件。
- 具体问题具体分析:不同赛题的数据增强方法、训练策略可能截然不同,需灵活应对。
- 模型诊断指南:遵循一个系统化的模型诊断流程(如下图),能高效定位问题并找到改进方向。
- 训练集误差大 → 使用更强大的模型。
- 验证集误差大(训练集误差小)-> 增加正则化(数据增强、Dropout等)。
- 公开榜(A榜)误差大 → 检查训练集与测试集分布是否一致,尝试数据合成或领域适应。
- 私有榜(B榜)误差大(A榜误差小)-> 可能过拟合了A榜,需减少针对A榜的调参,增强模型泛化能力。
未来展望
- 竞赛趋势:视觉竞赛将越来越多,且难度和业务贴近度会不断提升。
- 赛题类型:单纯的图像分类赛题将减少,物体检测、语义分割、多模态(图像+文本)等更复杂的赛题将成为主流。

通过系统学习(如参加专业的CV就业班)和竞赛实战,可以快速提升在计算机视觉领域的技术实力和竞赛水平。希望本课程能为大家的CV学习之路提供一个清晰的起点。










🎯 人工智能—计算机视觉CV公开课 P10:物体分割Mask RCNN理论与实战
在本节课中,我们将要学习物体分割(Instance Segmentation)的核心概念,特别是基于Mask RCNN的方法。我们将从卷积神经网络(CNN)的基础理论回顾开始,逐步深入到目标检测(RCNN系列)的演进,最终详细讲解Mask RCNN的原理、改进点及其实际应用。课程内容力求简单直白,让初学者能够理解。
📚 概述:视觉感知的基本问题
视觉感知主要解决四个基本问题:
- 分类:识别图像中的内容是什么。
- 检测:确定目标物体的位置(边界框)并识别其类别。
- 语义分割:对图像中的每个像素进行分类,但不区分同一类别的不同个体(例如,将所有人分割为“人”这个类别)。
- 实例分割/物体分割:在检测的基础上,精确分割出每个物体的轮廓,并能区分同一种类的不同个体(例如,区分图像中的每一个人)。
本节课的重点是实例分割,我们将以Mask RCNN作为核心模型进行讲解。
🧠 第一部分:卷积神经网络(CNN)理论回顾
上一节我们概述了视觉任务,本节中我们来看看实现这些任务的基础技术——卷积神经网络。理解CNN是理解后续所有高级模型的关键。
CNN并非全新理论,其概念早在1998年的LeNet-5中就已提出。如今的流行得益于数据量的积累和算力的提升,使其在视觉任务中大放异彩。
一个典型的CNN结构包含以下层:
- 卷积层:使用卷积核在图像上滑动,进行特征提取。
- 下采样层(池化层):降低特征图尺寸,增强特征不变性。
- 全连接层:将提取的特征用于最终分类或回归。
核心操作与数学表达
以下是CNN中的核心概念:

- 卷积操作:本质上是矩阵运算,其数学表达为加权求和。
公式:输出 = Σ (输入特征 Xi * 卷积核权重 Wi) + 偏置 b - 激活函数:在卷积后引入非线性变换,如ReLU、Sigmoid。
- 损失函数:衡量模型预测与真实标签的差距。对于回归问题,常用均方误差(MSE)。
公式:L = 1/N * Σ (预测值 - 真实值)^2 - 反向传播(BP)与梯度下降:通过链式法则计算损失函数对网络权重的梯度,并利用梯度下降法更新权重,使损失最小化。这是训练神经网络的核心算法。

简单来说,CNN通过多层卷积和非线性变换,从图像中提取出高层语义特征,最终通过这些特征完成分类等任务。
🔍 第二部分:目标检测(Object Detection)演进之路
在回顾了CNN的基础后,我们进入目标检测领域。本节我们将看到检测模型如何从RCNN一步步演进到更高效的Faster RCNN。
RCNN:区域提议与卷积特征提取
RCNN的思路直观但效率较低:
- 生成候选区域:使用传统方法(如Selective Search)在图像中生成约2000个可能包含物体的区域(Region of Interest, ROI)。
- 特征提取:将每个ROI区域缩放(Warp)到统一尺寸,分别输入CNN提取特征。
- 分类与回归:对提取的特征,使用支持向量机(SVM)进行分类,并使用边界框回归(Bounding Box Regression)精修位置。
RCNN的主要问题:对大量重叠的ROI进行独立的CNN前向传播,计算冗余严重;区域提议阶段耗时。
Fast RCNN:共享卷积计算
Fast RCNN针对RCNN的计算冗余进行了关键改进:
- 整体特征提取:先对整个输入图像进行一次CNN前向传播,得到整张图的特征图(Feature Map)。
- 区域映射:将Selective Search生成的ROI映射到特征图上对应的区域。
- ROI池化:通过ROI池化层,将不同尺寸的ROI特征转换为固定尺寸的特征。
- 分类与回归:将固定尺寸的特征送入全连接层,同时完成分类和边界框回归。
改进效果:大幅减少了卷积计算量,提升了效率。但区域提议仍依赖传统方法。
Faster RCNN:端到端的区域提议
Faster RCNN的核心创新是将区域提议也集成到神经网络中,实现端到端训练。
- 区域提议网络(RPN):在主干网络提取的特征图上,使用一组预先定义好的、不同尺寸和长宽比的“锚框”(Anchor)进行滑动。RPN网络判断每个锚框内是否包含物体(二分类),并初步调整锚框位置。
- 特征共享:RPN和后续的检测头(Fast RCNN部分)共享同一个特征图。
- 两阶段训练:通常先训练RPN生成提议区域,再固定RPN训练检测头。也可进行交替训练。
核心优势:区域提议过程也由可学习的神经网络完成,速度更快,且提议质量更高。
🎭 第三部分:实例分割与Mask RCNN详解
理解了目标检测的框架后,我们终于可以进入本节课的核心——实例分割模型Mask RCNN。它是在Faster RCNN框架上的自然延伸。
Mask RCNN:Faster RCNN + FCN
用一句话概括:Mask RCNN = Faster RCNN + 全卷积网络(FCN)。
它在Faster RCNN的两个输出分支(分类分支、边界框回归分支)基础上,增加了第三个分支:Mask预测分支。
- 该分支是一个小型FCN,对每个ROI区域进行像素级的二分类(判断每个像素是否属于该ROI所检测到的物体类别)。
- 它利用分类分支提供的类别信息,只为对应的类别生成掩码(Mask),最终输出一个二值掩码图。
关键技术:特征金字塔网络(FPN)
物体尺度变化是视觉任务的一大挑战。CNN深层特征语义信息强但分辨率低,对小物体不敏感;浅层特征分辨率高但语义信息弱。
FPN通过自顶向下路径和横向连接,将深层的强语义特征与浅层的高分辨率特征融合,构建了具有丰富多尺度信息的特征金字塔。Mask RCNN常采用FPN作为主干网络,显著提升了对不同尺度物体的检测与分割精度。
关键技术:ROIAlign
在Faster RCNN的ROI池化中,存在两次量化取整操作(将ROI坐标映射到特征图,再将特征图网格划分),这会导致像素级任务(如分割)出现偏差。
ROIAlign 取消了量化操作,使用双线性插值方法,精确计算每个池化采样点的特征值。这避免了量化误差,使特征图与原始图像像素对齐更精确,对分割任务至关重要。

损失函数

Mask RCNN的损失函数由三部分组成:
公式:L = L_class + L_box + L_mask
L_class:分类损失。L_box:边界框回归损失。L_mask:掩码预测损失(对每个ROI,在特定类别上计算平均二值交叉熵损失)。


💻 第四部分:Mask RCNN实战指南
理论最终需要付诸实践。本节将简要介绍如何运行和训练一个Mask RCNN模型。
目前有多种框架实现了Mask RCNN,其中Facebook开源的PyTorch版本较为流行且易于使用。
环境安装与Demo运行

以下是关键的实践步骤:

- 安装:按照官方README安装依赖。注意PyTorch和Torchvision的版本需与CUDA版本匹配。
- 运行Demo:项目提供了Webcam实时演示和Jupyter Notebook示例。若无摄像头,可修改代码读取静态图片进行推理。
- 查看结果:Demo会展示实例分割(以及可选的人体关键点检测)的可视化效果。
训练自定义数据集
若要训练自己的数据,需进行以下配置:
- 数据准备:使用标注工具(如LabelMe)标注图像,生成包含多边形轮廓的JSON文件。需将数据转换为模型要求的格式(如COCO格式)。
- 修改配置:
- 在配置文件中指定自定义数据集的路径和类别数。
- 修改模型配置(如
configs/下的yaml文件),指定主干网络、是否从预训练模型微调等。 - 调整超参数(如学习率)。
- 开始训练:执行训练脚本,指定配置文件路径。


通过以上步骤,即可在自定义数据集上训练Mask RCNN模型。

📝 总结


本节课中我们一起学习了实例分割的核心模型Mask RCNN。我们从CNN的基础理论出发,回顾了目标检测从RCNN、Fast RCNN到Faster RCNN的演进过程,理解了多任务学习、特征共享、锚框机制等核心思想。在此基础上,我们深入剖析了Mask RCNN如何通过增加掩码分支、引入FPN和ROIAlign等关键技术,在Faster RCNN的框架上实现了精准的像素级实例分割。最后,我们简要了解了其开源实现的用法。希望本课程能帮助你建立起对Mask RCNN从理论到实践的完整认知。




🎓 人工智能—计算机视觉CV公开课(P11):计算机视觉面试求职经验分享


在本节课中,我们将跟随一位优秀学员的分享,系统性地了解计算机视觉算法工程师的求职面试经验。课程将涵盖岗位派别区分、面试核心套路、常见面试题解析、必备学习资料以及个人心得体会,旨在帮助初学者建立清晰的求职认知和学习路径。
🧩 第一部分:CV算法工程师的派别之分
在计算机视觉领域,算法工程师的岗位通常可以分为两大派别:算法派和工程派。理解这两者的区别,对于规划职业路径和准备面试至关重要。
上一节我们介绍了课程概述,本节中我们来看看算法工程师的具体分类。


算法派工程师更侧重于模型结构的研究、算法的优化与迭代。他们需要深厚的理论基础和数学功底,日常工作包括研读论文、复现代码,并对业务场景的数据分布进行深刻理解。


工程派工程师则更偏向于业务实现和工程落地。他们的核心要求是能够高效地编写代码、训练模型和调整参数,以支撑具体的产品需求。


以下是两种派别的核心对比:
算法派:
- 工作重心:模型结构研究、算法优化。
- 能力要求:深厚的理论基础、数学功底、论文阅读与复现能力。
- 面试考察:80% 为基础理论、项目经历、模型设计;20% 为代码能力。
- 典型公司:大型互联网公司(如BAT)、资金充裕的独角兽公司。
工程派:
- 工作重心:业务支撑、工程实现。
- 能力要求:强大的编码能力、项目实现经验。
- 面试考察:80% 为实现方案、项目细节、代码编写;20% 为基础理论。
- 典型公司:初创公司、刚转型的中小型公司。
选择哪条路径,取决于你的兴趣和技能倾向。如果你热爱钻研模型原理,算法派是更好的选择;如果你更享受用代码解决实际问题,工程派可能更适合你。
🎯 第二部分:CV算法工程师的面试套路
了解了岗位分类后,我们来看看面试中常见的流程和问题。社招面试通常遵循一套相对固定的“套路”,掌握这些可以帮助你更好地准备。
以下是面试中常见的几大环节:
- 自我介绍:简洁明了,突出与岗位相关的项目经验,避免提及无关信息(如非相关专业背景)。
- 项目背景:阐述为什么在该项目中选择使用深度学习,重点说明数据量级(建议不低于20万)和数据分布。
- 模型选择:解释为什么选择某个特定模型(如 Inception V3, ResNet),需要结合准确率、运算速度、稳定性以及行业标杆(如谷歌的模型)来回答。
- 模型结构:深入阐述核心模型结构(如 Inception Module, 残差块),最好能动手画出来。这是考察理论基础的重点。
- 优化与思考:面试官会考察你是否对模型有过深度思考和优化尝试。这反映了你的研究潜力和对问题的理解深度。
- 数据与实现:说明数据来源、处理方式以及数据量。可以将数据处理等非核心工作“甩锅”给其他团队,聚焦于算法设计本身。
- 部署与问题:描述模型上线过程中遇到的问题(如小目标检测不准)及解决方案(如数据重新标注、数据扩增)。要承认问题的存在,并体现代码能力。
- 离职原因:从行业、企业、个人发展三个维度立体地回答,展现你的宏观思考能力,避免单纯抱怨“钱少”或“干得不爽”。
- 笔试与代码:可能会考察手写分类模型(如MNIST)、排序查找算法等。核心数据结构是重点。
在回答问题时,要主动引导面试官,展现你对问题的深度思考,而不仅仅是机械地回答。
❓ 第三部分:常见的CV面试题解析
面试中会遇到各种技术问题,以下是一些高频考点,掌握它们能让你在面试中更加从容。
上一节我们梳理了面试流程,本节中我们聚焦于具体的技术问题。
以下是部分核心面试题及准备要点:
- 模型原理题:
- 题目:介绍 Inception V3 和 ResNet 的核心结构。
- 准备:必须能默写画出 Inception Module 和残差连接结构,并理解其设计初衷(解决梯度消失、提升特征提取能力等)。
- 基础概念题:
- 题目:什么是BN层(Batch Normalization)?它有什么作用?
- 准备:理解其公式
BN(x) = γ * (x - μ) / √(σ² + ε) + β,以及它能加速训练、缓解梯度消失/爆炸的作用。
- 经典模型题:
- 题目:简述Faster R-CNN的原理。
- 准备:掌握R-CNN系列(R-CNN, Fast R-CNN, Faster R-CNN)的演进过程,核心是理解RPN(Region Proposal Network)和端到端训练。
- 数学推导题:
- 题目:手推反向传播(Backpropagation)公式。
- 准备:这是考察数学功底的必考题。需要熟练掌握链式法则,并能推导具体网络层中权重(W)和偏置(b)的梯度更新公式。
- 代码实践题:
- 题目:手写一个简单的图像分类模型(如用PyTorch实现LeNet对MNIST分类)。
- 准备:熟悉深度学习框架(PyTorch/TensorFlow)的基本API,能够不依赖参考独立完成数据加载、模型定义、训练循环等代码。


对于目标检测模型(如YOLO系列),要能说清各版本的改进点。对于优化器,要理解SGD(随机梯度下降)的核心价值——它将复杂的图像问题转化为可优化的数学问题,是深度学习发展的基石。
📚 第四部分:核心论文与学习资料
要成为一名合格的CV算法工程师,持续学习论文和积累代码实践是必不可少的。以下是一些推荐的学习资料。

以下是提升理论和技术水平的关键资源:

- 必读论文与资料:
- 基础论文:ResNet, Inception系列,Faster R-CNN,YOLO系列,Transformer in CV(如ViT)。
- 中文解读:寻找优秀的中英文对照论文解读博客或专栏,帮助降低阅读门槛。
- “花书”:《深度学习》(Deep Learning) by Ian Goodfellow。建议通读以建立宏观知识体系。
- 代码与框架:
- 框架教程:PyTorch官方教程、TensorFlow指南。
- 实战代码:GitHub上的热门项目复现(如mmdetection, detectron2)。
- 模型转换:了解ONNX等工具,实现不同框架间模型的转换与部署。
- 数学基础:
- 核心科目:线性代数、概率论与数理统计、微积分。
- 学习建议:如果感到吃力,可以观看考研数学课程或专业培训机构的数学课,目标是能看懂论文中的数学符号和推导逻辑。
- 面试准备:
- 算法题:在LeetCode上练习基本的排序、查找、动态规划问题。
- 项目复盘:深入思考自己做过的项目,从模型选择到问题解决的每一个环节,准备好“为什么”。
记住,学习是一个量变到质变的过程。初期看不懂论文很正常,坚持阅读和思考,积累到一定阶段自然会豁然开朗。
💡 第五部分:心得体会与未来展望
最后,分享一些关于学习心态、职业发展和行业前景的个人思考。


本节课我们一起学习了面试的方方面面,最后我们来谈谈一些更深层次的体会。
- 学习心态:直面“绝望之谷”
在学习过程中,几乎每个人都会经历“邓宁-克鲁格效应”中的“绝望之谷”——发现自己什么都不懂。这恰恰是你真正入门和开始深入思考的标志,千万不要在此刻放弃。 坚持过去,就会进入开悟爬升期。 - 核心竞争力:算法 > 工程
算法工程师的核心价值在于算法设计能力和模型迭代优化能力,而非单纯的代码量。要花70%的精力钻研理论、论文和数学基础,用30%的精力提升代码实践。深入理解“为什么模型要这样设计”比“如何调包跑通模型”更重要。 - 未来方向
- 深度强化学习:让机器自动设计网络结构可能是未来的重要方向。
- GAN的应用:在图像生成、数据增强等领域有广阔前景。
- 模型结构探索:在深度探索趋于饱和后,模型宽度(Width)的研究值得关注。
- 业务与数据思维
技术最终服务于业务场景。要对业务有深刻理解,并对数据分布保持敏感。未来,将是数据科学家和算法工程师驱动产品迭代的时代。 - 求职建议
- 尽早行动:不要等“完全准备好”再去面试,通过面试查漏补缺是高效的学习方式。
- 针对性准备:根据目标公司的派别(算法/工程)调整简历和面试策略。
- 保持良好心态:求职有运气成分,一次失败不代表能力不行。保持清淡饮食,规律作息,以积极状态应对。
📝 总结
在本节课中,我们一起学习了计算机视觉算法工程师求职的完整攻略:
- 明确派别:区分了算法派(重研究)和工程派(重实现),帮助你定位发展方向。
- 掌握套路:梳理了社招面试的九大常见环节,让你对面试过程心中有数。
- 攻克试题:解析了模型原理、数学推导、代码实践等高频面试题。
- 积累资料:推荐了论文、代码、数学等核心学习资源,提供了学习路径。
- 调整心态:分享了面对学习困难、构建核心竞争力以及规划职业未来的心得体会。


记住,成为一名优秀的CV算法工程师是一场马拉松。它需要你扎实数学基础,保持英语阅读能力,深入理解模型本质,并始终对业务和数据保持好奇。这条路虽然漫长,但前景光明。祝大家都能在CV领域找到自己心仪的工作,实现个人价值!


人工智能—计算机视觉CV公开课(七月在线出品) - P12:目标检测中的图像识别与定位问题 👁️🗺️
在本节课中,我们将要学习计算机视觉中的一个核心任务:目标检测。我们将从基础的图像识别与定位问题入手,探讨如何让计算机不仅能识别出图像中的物体是什么,还能精确地定位出它的位置。课程将涵盖从简单回归方法到滑动窗口检测等早期经典思路,为理解现代目标检测算法打下基础。

课程概述与物体检测简介 🎯
物体检测是计算机视觉中一个至关重要且应用广泛的领域。近年来,随着自动驾驶等技术的兴起,该领域受到了极大关注。在自动驾驶场景中,系统不仅需要识别摄像头捕捉画面中的物体(如车辆、行人),还需要精确定位它们的位置,以便进行避障和路径规划。这就是一个典型的目标检测任务。

本节课将介绍目标检测中几个经典且仍在使用的稳健方法。主要内容分为两部分:首先,我们将探讨目标检测的基础——图像识别与定位问题;其次,我们会简要提及迁移学习的概念。在目标检测部分,我们将沿着从R-CNN到Fast R-CNN,再到Faster R-CNN的发展脉络进行讲解。当然,后续还有R-FCN等其他方法,但本节课将聚焦于奠定基础的核心思想。

深度网络与图像分类的进步 🏗️
在深入目标检测之前,我们需要回顾一下图像分类的背景。对于卷积神经网络而言,业界有一个普遍共识:“更深更好”。这意味着,在处理像ImageNet这样丰富多样的现实世界图像分类任务时,只要网络能够被成功训练,其性能通常会随着网络深度的增加而提升。
这背后的原因是,模型的容量(即复杂度)越大,理论上其拟合能力越强。虽然大容量模型在小数据集上容易过拟合,但由于ImageNet等数据集规模巨大,类别繁多,更深的网络能够从中提取更多有效信息,从而持续提升性能。因此,从早期的LeNet、AlexNet,到后来的VGG、GoogLeNet(Inception系列),再到ResNet(甚至训练了上千层的网络),网络的深度不断增加,图像分类的错误率也随之稳步下降。

图像相关任务的分类 📸
上一节我们回顾了图像分类的进步,本节中我们来看看图像领域更广泛的任务类型。图像相关的任务可以粗略地分为以下几类,主要依据图像中物体的数量和要求输出的精细程度进行区分:


以下是四种主要的图像任务类型:
- 单物体识别:也称为图像分类。任务是识别图像中的主体内容是什么,输出是一个类别标签(例如,“猫”或“狗”)。
- 单物体识别与定位:不仅需要识别主体内容,还需要输出一个边界框来定位该物体在图像中的位置。输出包括类别和框的坐标(如左上角点
(x, y)和宽高(w, h))。 - 多物体检测:图像中可能存在多个物体。任务是将所有物体都找出来,并为每个物体给出一个合适的矩形边界框及其类别。这就是我们常说的目标检测。
- 实例分割:这是更精细的任务。不仅需要检测出每个物体,还需要精确地描绘出每个物体的像素级轮廓,而不仅仅是用矩形框框出来。

本节课的内容将集中在前三个部分。实例分割与我们将要讨论的方法有很强的关联性,但属于更高级的主题。
识别与定位任务的定义 🎯

明确了任务类型后,我们来具体定义“识别与定位”这个任务。在单纯的图像分类任务中,输入是一张图片,输出是C个类别的标签,评估标准是准确率。
而定位任务则要求输出一个边界框来标识物体的位置。这个框通常可以用一个点(如左上角(x, y))加上宽度(w)和高度(h)来确定,即输出四个连续的数值。评估定位好坏的标准常采用“交并比”(IoU),它计算预测框与真实标注框的交集面积与并集面积的比值,比值越高说明定位越准。

当把识别和定位结合起来,就是“识别与定位”任务:模型需要同时输出物体的类别和其边界框的坐标。

思路一:直接回归边界框 📏

面对定位问题,最直观的想法之一是将它视为一个回归问题。既然输出是四个连续数值(x, y, w, h),那么很自然地想到,可以设计一个损失函数,让网络预测的这四个值与真实值越接近越好。

一个常用的损失函数是L2损失(欧氏距离)。其公式为:
Loss = (x - x')² + (y - y')² + (w - w')² + (h - h')²
我们的目标就是通过训练,最小化这个损失。


那么,如何构建这样的网络呢?通常,我们会利用在大规模数据集(如ImageNet)上预训练好的图像分类网络(如VGG、ResNet)。这些网络的前面部分可以看作一个强大的“特征抽取器”,后面部分则是“分类器”。
具体做法如下:
- 我们保留预训练网络的特征抽取部分,并将其参数固定。
- 移除原网络的分类器头部。
- 接上一个新的“回归头部”。这个头部通常由全连接层构成,负责输出四个数值。
- 使用L2损失和反向传播算法(如SGD)来训练这个新接上的回归头部。

在预测时,我们可以将不同的“头部”连接到同一个特征抽取器后面,以完成不同任务。例如,连接一个“分类头部”用于识别物体类别,连接一个“回归头部”用于预测边界框位置。

关于回归模块应该加在网络的哪个位置,早期研究中有不同尝试。例如,在VGG网络上,可以接在最后一个卷积层之后;而在R-CNN等方法中,则接在最后的全连接层之后。

思路延伸:对主体进行更细致的定位 🧩



上一节我们介绍了用回归直接预测一个边界框,本节中我们来看看能否对物体进行更细致的定位。例如,我们不仅想定位一只猫,还想定位出它的左耳、右耳、眼睛和鼻子的位置。
最早尝试这个思路的论文之一是Google在CVPR 2014上发表的《DeepPose: Human Pose Estimation via Deep Neural Networks》。它的核心思想是:预先定义好每一类物体(如人)由K个关键部件组成(例如人的关节)。

网络的任务就从输出4个值,变为输出K × 4个值,分别对应每个部件的位置。对于人体姿态估计,网络会输出一系列关节点的坐标,这些点首尾相连就能形成火柴人式的姿态轮廓。这样,我们就能判断出人是站立、坐下等不同姿势。
这种方法假设物体的组成部分是固定且顺序已知的,因此不需要额外机制来区分哪四个数对应哪个部件。

思路二:滑动窗口与分类 🔍
除了直接回归,还有另一种思路来解决定位问题。既然卷积神经网络可以判断一张图片属于某个类别的概率,那么我们可以尝试用不同大小、不同位置的“框”去截取图像,然后将每个框内的图像送入网络进行分类。
以下是该思路的基本步骤:
- 生成候选框:在原始图像上,以不同尺度、不同位置滑动一个固定大小的窗口,生成大量候选图像区域。
- 分类与调整:将每个候选区域送入一个卷积神经网络。这个网络有两个输出头:
- 分类头:输出该区域属于各个类别的概率。
- 回归头:输出一个微调向量
(Δx, Δy, Δw, Δh),用于对当前候选框的位置进行精细调整。
- 合并与筛选:根据分类得分的高低,对所有候选框进行筛选,并应用非极大值抑制等后处理技术,合并重叠的框,得到最终的检测结果。


例如,在一张猫的图片上,滑动窗口在猫身体大部分区域时,分类头会给出较高的“猫”的概率,同时回归头会输出如何将这个窗口调整到更贴合猫的边界。

全卷积网络与计算优化 ⚡


滑动窗口方法的一个明显缺点是计算量巨大,因为每个候选区域都需要独立通过整个卷积网络进行前向传播。为了优化效率,研究者们引入了全卷积网络的概念。

传统的分类网络末尾通常有全连接层,其输入尺寸是固定的。而全卷积网络将最后的全连接层也替换为卷积层(通常是1x1卷积)。1x1卷积的作用类似于全连接,可以对特征进行加权组合,同时又能保持输入输出的空间维度。
这样做的好处是:
- 参数量大幅减少:1x1卷积的参数远少于全连接层。
- 支持任意尺寸输入:全卷积网络可以接受任意尺寸的输入图像,并输出对应尺寸的特征图。
- 计算共享:这是最关键的优势。我们可以将整张图像一次性输入全卷积网络,得到一张密集的“特征图”。当我们需要评估某个滑动窗口时,无需重新计算,只需从这张特征图上提取对应区域的特征即可,极大地共享了计算。

例如,一个网络结构可能变为:输入图像 → 卷积特征提取 → 1x1卷积(4096维)-> 1x1卷积(1024维)-> 两个并行的1x1卷积头(一个输出1000类分类得分,一个输出4*1000个回归值)。通过这种方式,实现了高效的滑动窗口检测。



课程总结 📚
本节课中,我们一起学习了目标检测的基础——图像识别与定位问题。我们从最简单的单物体场景出发,探讨了两种核心思路:

- 直接回归法:将边界框坐标视为回归目标,通过设计回归头部和L2损失函数来训练网络直接预测位置。这种方法思路直接,但可能难以处理复杂情况。
- 滑动窗口法:通过生成大量候选区域,对每个区域进行分类和边界框微调。为了提升效率,引入了全卷积网络来共享计算,使得一次性处理整张图像并评估所有窗口成为可能。


这些早期方法为后续更强大的目标检测框架(如R-CNN系列、YOLO、SSD等)奠定了重要的思想基础。理解这些基础,有助于我们更好地掌握现代目标检测技术的精髓。



人工智能—计算机视觉CV公开课(七月在线出品) - P13:Yolo后时代之目标检测的发展方向 🎯
在本节课中,我们将要学习目标检测领域在YOLO之父“退役”后的发展动态。我们将以YOLOv4和YOLOv5的出现为引,探讨目标检测算法的核心改进,并展望该领域未来的两个重要发展方向。

YOLO的传承与演变 🏔️

上一节我们介绍了目标检测的基本概念,本节中我们来看看YOLO系列的发展历程。YOLO(You Only Look Once)是目标检测领域一座无法绕过的高山。在YOLO出现之前,目标检测主要以R-CNN家族为代表的两阶段方法为主。YOLO的出现标志着一阶段实时目标检测的真正开端。
2020年2月,YOLO之父宣布因伦理原因退出计算机视觉研究领域。此后不久,由俄罗斯学者和中国台湾学者联合推出了YOLOv4。约两个月后,另一位长期维护YOLO社区的大神推出了基于PyTorch的YOLOv5。YOLOv5在工业界表现优异,但未发表正式论文。因此,本节课我们将主要围绕有详细论文支撑的YOLOv4展开,其论文也被称为“目标检测调参手册”。
目标检测器的通用框架 🏗️
无论是单阶段还是两阶段检测器,只要是基于锚框(Anchor)的方法,都遵循一个通用框架。该框架主要包含四个部分。
以下是目标检测器的主要组成部分:
- 输入(Input):模型的输入,通常是图像或视频帧。
- 骨干网络(Backbone):用于提取图像特征的卷积神经网络(CNN),如VGG、ResNet、Darknet53等。
- 颈部(Neck):连接骨干网络和预测头的部分,用于融合和增强特征,例如FPN、PAN结构。
- 预测头(Head):负责进行最终的分类和边界框回归预测。
YOLOv4的整体结构也严格遵循了这个框架,并在每个部分都进行了创新性的改进。
YOLOv4 核心技术解析 ⚙️
接下来,我们将逐一解析YOLOv4在框架各个部分所采用的核心技术。
1. 输入端改进:马赛克数据增强
在训练阶段,YOLOv4对输入端进行了重要改进,主要采用了马赛克(Mosaic)数据增强技术。该技术借鉴了2019年的CutMix,但将拼接的图片数量从2张增加到了4张。
以下是马赛克数据增强的步骤:
- 随机缩放4张原始图片。
- 随机裁剪这4张图片。
- 将这4张处理后的图片随机排布拼接成一张新的训练图片。
使用马赛克数据增强主要有两个优点:
- 丰富数据集:特别是增加了大量小目标样本,提升了模型对小目标的检测能力。
- 优化GPU使用:一次性计算4张图片的数据,可以在较小的批量(Batch Size)下高效利用GPU。
这里引出了如何定义目标大小的问题。在COCO数据集中,通常这样划分:
- 小目标:边界框面积 < (32 \times 32) 像素
- 中目标:(32 \times 32) ≤ 边界框面积 < (96 \times 96) 像素
- 大目标:边界框面积 ≥ (96 \times 96) 像素
2. 骨干网络改进:CSPDarknet53
YOLOv4的骨干网络采用了CSPDarknet53。它是在YOLOv3的Darknet53基础上,引入了CSPNet(Cross Stage Partial Network,跨阶段局部网络) 的设计思想。
CSPNet的主要思想是将特征图在通道维度上分成两部分,一部分进行复杂的卷积变换,另一部分直接保留(捷径连接),最后再将两部分合并。这种方法能有效缓解梯度信息重复的问题。
以下是CSPNet的三个优点:
- 增强了CNN的学习能力,同时在轻量化时保持准确性。
- 降低了计算瓶颈。
- 降低了内存成本。
此外,YOLOv4在骨干网络中还用DropBlock 替代了传统的Dropout。Dropout随机丢弃单个神经元,而在卷积层中,由于池化层的存在,这种随机丢弃可能效果不佳。DropBlock改为丢弃整个局部区域块,能更有效地防止卷积网络过拟合。
3. 颈部改进:SPP与PAN
在颈部,YOLOv4集成了SPP(Spatial Pyramid Pooling,空间金字塔池化) 模块和PAN(Path Aggregation Network,路径聚合网络) 结构。
- SPP模块:解决了输入图像必须缩放到统一尺寸的问题。它通过不同尺度的池化层(如
4x4,2x2,1x1),将任意大小的特征图转换为固定长度的特征向量(例如16+4+1=21维)。这使得网络可以接受任意尺寸的输入。 - PAN结构:在FPN(特征金字塔网络)自顶向下传达强语义特征的基础上,增加了一个自底向上的路径,用于传达强定位特征。YOLOv4中的PAN将特征融合方式从相加(Addition)改为拼接(Concatenation),保留了更多的特征信息。

4. 预测头与损失函数改进:CIoU Loss
预测头部分,YOLOv4沿用了YOLOv3的设计,但改进了边界框回归的损失函数。目标检测的核心是优化边界框的预测,其关键在于损失函数,而损失函数的基础是IoU(交并比)。
原始IoU Loss存在两个问题:1)当预测框与真实框不相交时,IoU为0,无法优化;2)当相交情况不同但IoU相同时,无法区分哪种情况更好。
为了解决这些问题,研究者们提出了一系列改进:
- GIoU Loss:在IoU基础上,考虑最小外接矩形,缓解了不相交时的优化问题。
- DIoU Loss:在IoU基础上,同时考虑重叠面积和两个框中心点的距离。
- CIoU Loss:YOLOv4采用的损失函数。它在DIoU的基础上,额外考虑了边界框的长宽比一致性。
CIoU Loss的公式如下:
[
\mathcal_ = 1 - IoU + \frac{\rho2(b, b)}{c^2} + \alpha v
]
其中,(v) 衡量长宽比的相似性,(\alpha) 是权重系数。CIoU能更全面地引导边界框回归。
此外,YOLOv4在预测后处理中,将传统的NMS(非极大值抑制)替换为DIoU-NMS。传统NMS仅根据分类分数抑制重叠框,当两个目标靠得很近时,容易错误地抑制其中一个。DIoU-NMS在考虑分数的同时,也考虑框之间的中心点距离,能更好地区分相邻目标。
目标检测的未来方向 🚀
在YOLO系列性能不断提升之后,目标检测领域未来将向何处发展?我们认为有两个重要方向值得关注。
方向一:无锚框(Anchor-Free)目标检测
基于锚框的方法需要预设大量锚框,其中包含大量负样本(不包含目标的框),计算效率有提升空间。无锚框方法摒弃了锚框,转而预测目标的关键点。
以下是两种经典的无锚框方法:
- CornerNet:通过检测目标框的左上角和右下角两个关键点来确定边界框。它使用嵌入向量(Embedding)来匹配属于同一物体的角点对。
- CenterNet:在CornerNet的基础上,额外预测目标的中心点。通过判断中心点是否落在预测框的中心区域内,来辅助匹配和过滤误检,提高了精度。
方向二:模型压缩与轻量化
将大型、高精度的模型(如YOLOv5)部署到算力有限的移动端或嵌入式设备(如手机、无人机)时,需要进行模型压缩,在精度和效率之间取得平衡。

以下是几种常见的模型压缩技术:
- 网络剪枝:识别并移除网络中冗余的神经元或连接(“南郭先生”)。例如,MobileNet系列网络通过深度可分离卷积等设计,大幅减少了参数量和计算量。
- 知识蒸馏:用一个大型“教师网络”指导一个小型“学生网络”进行训练,让学生网络模仿教师网络的行为。
- 参数量化:将模型参数从高精度浮点数(如32位)转换为低精度数据(如8位整数),减少存储和计算开销。
- 动态计算:让模型根据当前设备资源(如电量)动态调整计算复杂度,在资源紧张时以较低精度运行。

总结 📝
本节课中我们一起学习了YOLO后时代目标检测的发展。我们从YOLOv4的论文入手,详细分析了它在输入端(马赛克数据增强)、骨干网络(CSPDarknet53、DropBlock)、颈部(SPP、PAN)以及预测头(CIoU Loss, DIoU-NMS)等方面的创新。这些改进共同推动了目标检测性能的边界。


最后,我们展望了目标检测未来的两个重要趋势:无锚框(Anchor-Free)方法和模型轻量化压缩技术。前者试图从根本上改变检测范式,后者则致力于让先进的检测算法能落地于实际应用场景。希望本课程能帮助你更好地理解目标检测领域的现状与未来。

人工智能—计算机视觉CV公开课(P14):电商图像检索原理与实践 🖼️🔍

在本节课中,我们将要学习电商领域下图像检索的核心原理与实践过程。我们将从图像特征提取的基础知识开始,逐步深入到具体的检索流程、先进的深度学习方法以及检索加速技术。
第一部分:图像特征提取
图像是一种非结构化的数据,其尺寸和内容多变。为了进行有效的图像检索,我们需要找到一种方法来描述图像,并将其编码成固定维度的特征向量,以便后续计算相似度。
图像特征主要分为两类:全局特征和局部特征。
全局特征与局部特征
- 全局特征:关注图像的整体信息,通常将整张图像编码成一个固定长度的向量(例如,512维)。这使得相似度计算非常直接。
- 局部特征:关注图像的细节信息,通常提取图像中的多个关键点(Key Points),每个关键点用一个向量(例如,128维)描述。因此,一张图像的特征可能是一个
N x 128的矩阵,其中N(关键点数量)是不固定的。
上一节我们介绍了特征的基本分类,本节中我们来看看图像检索任务的具体类型,以及如何选择特征。
图像检索任务类型与特征选择
图像检索任务可分为:
- 相似图像检索:找到与查询图像内容类似(如同类商品)的图像。
- 相同图像检索:找到与查询图像完全一致或几乎一致的图像(如版权检索)。
以下是特征选择的一般建议:
- 全局特征(如CNN特征) 更适合相似图像检索和分类相关任务(如图像分类、语义分割)。
- 局部特征(如SIFT关键点) 更适合相同图像检索,因为它能精确匹配图像的局部细节,对形变、旋转有较好的鲁棒性。
对于初学者,学习图像检索可以遵循以下路径:
- 学习各类图像特征(哈希、直方图、关键点、CNN)的原理与优缺点。
- 理解图像检索与图像分类任务的区别与联系。
- 掌握如何将不定长的局部特征编码为定长的全局特征。
- 了解如何用CNN网络提取局部特征的前沿工作。
接下来,我们将详细介绍几种常见的特征提取方法。
传统特征提取方法
以下是几种经典的特征提取方法:
1. 图像哈希值
图像哈希(如aHash, pHash, dHash)通过缩放图像、灰度化、计算像素差异等步骤,生成一个固定长度(如64位)的字符串或二进制序列来表示图像。
- 优点:计算快,存储方便,可用于快速去重。
- 缺点:对图像内容变化(如颜色、旋转)敏感,且存在不同图像哈希值相同的碰撞可能。
2. 颜色直方图
颜色直方图统计图像中不同颜色值的像素分布情况。
- 优点:对图像的旋转、平移变化不敏感。
- 缺点:对颜色变化非常敏感,且丢失了颜色的空间分布信息。
3. 关键点特征(如SIFT, ORB)
提取图像中的角点、边缘等显著位置作为关键点,并为每个关键点计算一个描述子向量。
- 优点:对旋转、缩放、亮度变化具有很好的鲁棒性,适合精确匹配。
- 缺点:计算速度相对较慢;容易受到图像中文字等强边缘信息的干扰。
从局部特征到全局特征:词袋模型
由于局部特征(N x 128 矩阵)的长度 N 不固定,我们需要将其编码为固定维度的全局特征。常用方法是词袋模型。
步骤如下:
- 提取特征:从所有训练图片中提取局部特征,得到一个大的特征池(例如,
M x 128矩阵)。 - 生成视觉词典:对特征池进行聚类(如K-Means),聚类中心称为“视觉单词”。假设我们设定聚类数为
K,则得到K x 128的视觉词典。 - 图片编码:对于一张新图片,提取其局部特征(
N x 128)。对于每个局部特征,在视觉词典中找到最近的视觉单词。最终,图片被表示为一个K维的直方图向量,每个维度值是该视觉单词出现的频率。
公式化表示:一张图片 I 的词袋向量 V 可以通过以下方式计算:
V_i = count( nearest_visual_word(feature_j) == i ), 其中 i 从 1 到 K,feature_j 是图片的局部特征。
第二部分:图像检索流程
基于内容的图像检索通用流程如下:
- 特征提取:对图像库中的所有图像提取特征(全局特征或编码后的局部特征),并存储到特征数据库中。
- 查询处理:对用户输入的查询图像,提取相同的特征。
- 相似度计算:计算查询图像的特征与特征库中所有特征之间的相似度(如余弦相似度、欧氏距离)。
- 结果排序与返回:根据相似度对库中图像进行排序,返回最相似的若干张图像。
在深度学习中,我们通常使用卷积神经网络来提取强大的全局特征。
基于CNN的特征提取
一个典型的图像分类CNN网络结构为:卷积层 -> 池化层 -> 全连接层 -> Softmax。
- 卷积层:提取图像的局部特征。
- 池化层:对特征进行降维,并一定程度上消除特征的位置信息,增强模型的平移不变性。常见的池化方法有:
- 最大池化(Max Pooling):保留最显著的特征。
- 平均池化(Average Pooling):保留平均特征。
- 广义均值池化(GeM Pooling)、注意力池化(如R-MAC) 等更高级的方法。
对于图像检索,我们通常去掉最后的全连接层和Softmax,直接使用卷积层和池化层输出的特征作为图像的全局描述子。
第三部分:ArcFace原理与实践 👤➡️📏
在开集检索场景(测试集类别可能不在训练集中出现)下,传统的Softmax分类损失并不直接优化特征之间的可分性。ArcFace是一种先进的损失函数,它直接在角度空间最大化类间间隔,从而学习到更具判别力的特征。
ArcFace原理
ArcFace对Softmax损失进行了改进:
- 对权重和特征向量进行L2归一化,使预测仅依赖于特征与权重之间的角度
θ。 - 在目标类别的角度
θ上增加一个附加角度间隔m,使得同类特征更加紧凑,不同类特征更加分离。
其损失函数核心公式可以简化为:
L = -log( e^(s·cos(θ_yi + m)) / (e^(s·cos(θ_yi + m)) + Σ e^(s·cos(θ_j)) ) )
其中,s 是缩放因子,θ_yi 是特征与它真实类别权重向量的角度,m 是附加的角度间隔。
ArcFace实践:电商图像聚类比赛
我们以一个电商图像聚类比赛为例进行实践。任务是将测试集的图像聚类到训练集已有的类别中,这是一个典型的开集检索问题。
代码流程概述:
- 数据准备:划分训练集和验证集,确保类别不交叉。
- 模型定义:使用预训练的CNN(如EfficientNet)作为特征提取器,移除其分类头,接上ArcFace层。
- 训练:使用ArcFace损失函数和交叉熵损失进行训练。
- 特征提取与聚类:
- 对测试集图像提取特征。
- 计算特征间的余弦相似度矩阵。
- 设定一个动态阈值,将相似度高于该阈值的图像对归为同一类,完成聚类。
# 伪代码示例:计算相似度并聚类
features = model.extract_features(test_images) # 形状: [N, D]
similarity_matrix = cosine_similarity(features, features) # 形状: [N, N]
# 使用阈值进行聚类...
第四部分:图像检索加速方法 ⚡
当图像库规模巨大(如百万、千万级)时,直接计算查询特征与所有库特征的相似度(暴力搜索)效率极低。以下是几种加速方法:
聚类索引:
- 先对特征库中的所有特征进行粗聚类(如分成1000类)。
- 检索时,先确定查询特征所属的粗类别,然后仅在该类别内进行精细搜索。这大大缩小了搜索范围。
乘积量化:
- 将高维特征向量切分成多个子向量。
- 对每个子向量空间独立进行聚类,生成子码本。
- 图像特征用其各子向量对应的聚类中心ID(码字)串联起来表示,这相当于一个压缩的编码。
- 检索时,通过计算查询编码与库中编码的近似距离来加速。这种方法在压缩率和检索精度间取得了很好的平衡。
近似最近邻搜索:
- 使用诸如 HNSW 的图索引方法。
- 预先构建一个特征相似图,图中相近的节点代表特征相似。
- 检索时,从图中的一个点出发,沿着边快速导航到最近邻区域,避免了全局比较。
总结
本节课中我们一起学习了电商图像检索的核心知识。我们从图像特征提取的基础开始,区分了全局特征与局部特征及其适用场景。接着,我们梳理了标准的图像检索流程,并介绍了如何利用CNN提取深度特征。然后,我们深入探讨了针对开集检索的ArcFace损失函数的原理,并通过一个实践案例展示了其应用。最后,我们介绍了面对海量数据时的图像检索加速方法,如聚类索引、乘积量化和HNSW。


通过本课程,你应该对构建一个基本的图像检索系统有了清晰的认识,并了解了如何利用先进技术提升检索效果与效率。

人工智能—计算机视觉CV公开课(七月在线出品) - P15:从零实现人脸关键点模型的训练和部署 🎯
概述
在本节课中,我们将学习如何从零开始构建、训练并部署一个用于人脸关键点检测的深度学习模型。我们将从深度学习的基础概念讲起,逐步深入到模型搭建、训练流程,最后完成一个可以实时运行的人脸关键点检测应用。

第一部分:深度学习介绍 🤖
深度学习是包含多层神经元结构的一种机器学习模型,其整体结构与人脑神经元的结构有相似之处。深度学习是机器学习的一个分支,属于人工智能领域的一部分。
上一节我们介绍了课程的整体安排,本节中我们来看看深度学习的核心定义。
人工智能、机器学习与深度学习的关系
最外层是人工智能,里面是机器学习,再里面是深度学习。深度学习是机器学习的一种算法或一个分支。机器学习领域还包括其他模型,如树模型、线性模型(逻辑回归、SVM)、无监督模型(KNN)等。
深度学习的特点
深度学习的特点是端到端的。端到端指的是从输入到输出的过程,即 inputs -> model -> outputs,中间直接通过模型完成一次性计算。深度学习是一个有效的计算图,每个节点代表一个具体的计算过程,节点之间具有有向的数据流向。
深度学习模型通常包含输入层、隐含层和输出层。输入层将原始数据输入到节点,隐含层是中间层,输出层得到最终的输出结果,可以是类别的概率值或回归的数值结果。
下图展示了一个典型的全连接网络。什么是全连接网络呢?假如这是第一层、第二层、第三层,层与层之间具有有向的数据流向。第二层节点的输出作为第三层节点的整体输入,本质上是一种映射关系。
全连接层的计算可以用矩阵乘法方便地表示。例如,单个神经元的计算为:
z = x1*w1 + x2*w2 + x3*w3 + b
output = activation_function(z)
其中,activation_function 可以是 sigmoid 等函数。
深度学习的应用场景
深度学习非常适合处理那些“对人很简单,但对机器很难”的任务,即人类能直观完成但难以用传统方法提取特征的任务。例如:
- CV方向:人脸识别、车辆检测、动物识别、红绿灯检测。
- NLP方向:文本翻译、客服对话机器人。
这些任务通常是有监督场景,且很难进行人工特征工程。
第二部分:模型搭建基础 🧱
上一节我们介绍了深度学习是什么,本节中我们来看看如何搭建一个深度学习模型。
在搭建一个网络模型时,本质是由输入层、隐含层和输出层这三类层组成。如果搭建的是卷积神经网络,通常遵循以下步骤:
- 确定输入和输出,即确定网络模型的输入维度和输出维度。
- 确定隐含层。例如使用卷积层时,需要确定通道数量、卷积核大小、步长和填充等参数。
- 确定全连接层的维度等。
理解“层”的概念
学习深度学习框架,本质是学习各种“层”的使用。常见的层包括:
- 激活函数层:如
sigmoid,ReLU。 - 损失函数层:如
BCE(二分类交叉熵),CE(多分类交叉熵),MSE(均方误差),MAE(平均绝对误差)。 - 卷积层:如 1D、2D、3D卷积,转置卷积。
- 全连接层
- 规范化层
卷积层详解
卷积层的作用类似于数字图像处理中的滤波器。它通过一个滤波器(卷积核)对输入的二维数据进行滤波,得到输出结果。
以下是卷积操作的核心参数:
- 卷积核大小:滤波器的尺寸。
- 步长:每次滑动时移动的像素个数。
- 填充:在输入数据四周填充常数(通常为0)。
输出维度计算公式:
output_size = (N + 2P - F) / S + 1
其中:
N:输入尺寸P:填充尺寸F:卷积核大小S:步长
示例:输入维度为 7x7,卷积核 3x3,填充为1,步长为1。
output_size = (7 + 2*1 - 3) / 1 + 1 = 7
对于多通道输入(如RGB彩色图片为3通道),卷积核也需要具有相应的通道数。计算时,每个输入通道与卷积核的对应通道进行计算,所有结果求和再加上偏置,得到一个输出值。多个卷积核会产生多通道输出。
卷积神经网络
卷积神经网络由卷积层、激活函数、池化层和全连接层有效组合而成。其结构类似于人的视网膜,逐步提取特征并汇聚信息。
典型的卷积神经网络流程是:输入数据 → 卷积层(特征提取)-> 池化层(降维)-> 展平为向量 → 全连接层 → 输出。
第三部分:模型训练与超参数 ⚙️
上一节我们介绍了如何搭建模型,本节中我们来看看如何训练模型以及其中的关键概念。
参数与超参数
- 参数:模型能够从训练数据集中学习并调整的数值,例如权重
W。 - 超参数:不能从数据中学习,需要人工设置的数值,例如网络结构本身、学习率等。
模型由 数据 和 超参数 共同决定。
以下是深度学习中常见的超参数:
- 学习率
- 损失函数的超参数
- 批次大小
- Dropout 比率
- 模型深度
- 卷积核尺寸
选择超参数时,可以关注哪些能提高模型的建模能力,哪些能缓解过拟合。
学习率的作用
学习率决定了每次参数更新的步长。
- 学习率过小:更新幅度太小,需要很多步才能收敛,训练缓慢。
- 学习率过大:更新幅度太大,可能越过最优点,导致损失函数震荡,无法收敛。
模型的训练过程
模型训练是一个迭代过程,核心步骤包括正向传播、损失计算、反向传播和参数更新。
训练流程伪代码:
for epoch in range(num_epochs): # 遍历整个数据集多次
for batch in data_loader: # 遍历批次数据
# 1. 正向传播
predictions = model(batch.data)
# 2. 计算损失
loss = loss_function(predictions, batch.labels)
# 3. 反向传播 (计算梯度)
loss.backward()
# 4. 参数更新 (使用优化器,如SGD)
optimizer.step()
optimizer.zero_grad() # 清零梯度,为下一批次准备


为什么要使用批次?
使用所有样本(批量梯度下降)计算准确但耗时,且缺乏随机性。使用单个样本(随机梯度下降)梯度噪声大,不稳定。使用小批次是折衷方案,既能保证一定的计算效率,又能引入有益的随机性,有助于模型泛化。
梯度下降 通过计算损失函数对参数的偏导,沿着使损失降低最快的方向更新参数。公式为:
参数 = 参数 - 学习率 * 梯度
第四部分:代码实践 💻
上一节我们介绍了模型的训练理论,本节中我们通过代码进行实践。我们推荐使用 PyTorch 框架。
PyTorch 基础
PyTorch 的张量(Tensor)是多维矩阵,可以从 Python 列表或 NumPy 数组创建。它支持 GPU 加速,能高效处理线性代数运算。
自动求导是 PyTorch 的核心特性之一,可以自动计算梯度。
案例1:线性回归拟合
我们创建一个带噪声的线性数据 y = -3x + 4 + noise,然后用一个线性模型 y_pred = w*x + b 去拟合。
以下是核心训练循环:
# 初始化参数
w = torch.tensor([1.0], requires_grad=True)
b = torch.tensor([1.0], requires_grad=True)
# 定义优化器
optimizer = torch.optim.SGD([w, b], lr=learning_rate)
for epoch in range(num_epochs):
# 正向传播
y_pred = w * x_data + b
# 计算损失
loss = F.mse_loss(y_pred, y_data)
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
optimizer.zero_grad()
通过调整学习率,可以观察损失收敛情况。
案例2:实现全连接层
全连接层本质是矩阵乘法 output = input @ W + b。可以手动实现,也可以直接使用 torch.nn.Linear。
案例3:实现卷积层与卷积神经网络
卷积操作本质是滑动窗口的乘加运算。可以手动实现,也可以使用 torch.nn.Conv2d。
一个简单的卷积神经网络定义如下:
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5) # 输入通道1,输出通道6,卷积核5x5
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5) # 输入通道6,输出通道16,卷积核5x5
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 展平后输入全连接层
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = torch.flatten(x, 1) # 展平
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
核心案例:人脸关键点检测 👁️👃👄
任务:输入一张人脸图片,输出左眼、右眼、鼻子、嘴巴四个关键点的坐标(共8个值)。


模型构建:使用一个简单的卷积神经网络,最后通过全连接层输出8个坐标值。需要对坐标标签进行归一化(缩放到0-1之间)。





训练流程:
- 加载数据,创建
DataLoader。 - 定义模型、损失函数(如
MSELoss)和优化器。 - 运行训练循环(正向传播、计算损失、反向传播、参数更新)。
部署与推理:
- 训练完成后保存模型权重。
- 在部署时,加载模型权重。
- 使用 OpenCV 捕获摄像头视频流,对每一帧进行人脸检测。
- 对检测到的人脸区域,预处理后输入模型进行正向传播(推理),得到关键点坐标。
- 将关键点绘制在原始图像上并显示。


一个简单的推理代码片段:
# 加载模型
model = FaceKeypointModel()
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()
# 处理单张人脸图像
with torch.no_grad():
predictions = model(processed_face_tensor)
keypoints = predictions.numpy().reshape(-1, 2) # 转换为坐标点
# 将归一化的坐标映射回原图尺寸并绘制


总结 🎓
本节课我们一起学习了深度学习的核心概念、模型搭建基础、训练流程与超参数调整,并通过 PyTorch 代码实践了线性回归、全连接网络、卷积网络,最终完成了一个从零开始训练并部署的人脸关键点检测项目。深度学习的学习需要理论与实践相结合,建议在理解原理的基础上,多动手编写和运行代码。





人工智能—计算机视觉CV公开课(七月在线出品) - P16:时间序列实战:心电图疾病监测 📈
在本节课中,我们将要学习时间序列分析的基础知识、特征提取方法、分类与回归模型,并通过一个心电图疾病识别的实战案例,掌握如何应用深度学习模型解决实际问题。
时间序列介绍 📅
时间序列数据在我们的日常生活中非常常见。这类数据的特点是按照时间维度有规律地进行采集和存储,最终形成一个数据序列,而不是单个数值点。
时间序列数据具有规律性,例如每分钟采集一次股票价格。如果数据是无规律采集的,则不能称为时间序列。下图展示了一个典型的按分钟级别采集的时间序列走势图,其数值会随时间推移而波动。




时间序列的组成与分解 🔍
上一节我们介绍了时间序列的基本概念,本节中我们来看看时间序列通常由哪些部分组成。
一个时间序列通常可以分解为以下三项:
- 趋势项:描述序列长期的规律,可以是增加、减少或保持不变。
- 季节项:序列在特定周期(如一天、一周、一年)内呈现的规律性波动。
- 残差项:在剔除趋势和季节因素后,剩余的随机波动。
原始时间序列可以看作是这三项的组合。根据组合方式的不同,主要有两种模型:
加法模型:当季节和残差与趋势无关时,序列由三者相加得到。
序列 = 趋势 + 季节 + 残差




乘法模型:当季节波动的幅度与趋势水平相关时,序列由三者相乘得到。
序列 = 趋势 * 季节 * 残差

代码实践:构建与分解序列 💻


理解了理论后,我们通过代码来实践如何构建和分解时间序列。
以下是构建加法模型时间序列的示例代码:
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# 创建时间轴
time = np.arange(1, 51)
# 创建趋势项(线性增长)
trend = time * 2.75
# 创建季节项(正弦波动)
seasonal = 10 + np.sin(time * 10)
# 创建残差项(随机噪声)
residual = np.random.normal(size=50) * 10
# 组合成加法模型序列
additive_ts = trend + seasonal + residual


接下来,我们可以使用 seasonal_decompose 函数对构建好的序列进行分解,直观地观察趋势、季节和残差项。
时间序列的特征提取 🛠️

要对时间序列进行建模,特征提取是关键一步。我们可以从多个角度提取有价值的特征。




以下是基于日期时间的特征提取方法:
我们可以从时间戳中提取出年、月、日、小时、分钟、星期几、是一年中的第几天等信息,这些特征常能反映周期性规律。
另一种重要的特征是滞后特征。它的核心思想是利用历史数据作为当前时刻的特征。
例如,lag1 表示用前一时刻的值作为当前特征,lag2 表示用前两个时刻的值,以此类推。在Pandas中,这可以通过 shift() 操作轻松实现。

此外,我们还可以计算滚动窗口特征。
例如,计算窗口大小为7的滚动平均值,即用最近7个时刻的均值作为一个特征。这有助于平滑短期波动,捕捉中期趋势。与之相对的是扩展窗口特征,它计算从序列起点到当前时刻所有数据的累积统计值(如累积均值)。






时间序列的分类与回归模型 🤖


提取特征后,我们就可以构建模型了。时间序列任务主要分为分类和回归两大类。




时间序列分类:目标是识别一个序列所属的类别(如心电图是否异常)。常用方法有:
- 基于距离的方法(如KNN),使用DTW(动态时间规整)等算法计算序列相似度。
- 提取手工特征(如上节所述),然后使用传统机器学习模型(如随机森林)进行分类。
- 使用端到端的深度学习模型,如一维卷积神经网络(1D CNN),自动学习特征并分类。




时间序列回归/预测:目标是预测序列未来的值。常用方法有:
- 简单方法,如使用历史数据的滑动平均值作为预测值。
- 经典统计模型,如ARIMA、Prophet。
- 深度学习模型,如RNN、LSTM、DeepAR等,它们能捕捉复杂的时序依赖关系。


例如,我们可以使用GluonTS库中的DeepAR模型进行时间序列预测,该模型会输出预测值及相应的置信区间。



实战案例:心电图疾病识别 ❤️






现在,我们将应用所学知识,解决一个实际问题:心电图疾病识别。心电图信号是典型的时间序列数据。




我们的目标是构建一个分类模型,根据一段心电图信号判断其对应的疾病类别。数据集中包含5个类别,且存在类别不均衡的问题。





以下是解决此问题的核心步骤:
- 数据预处理:读取数据,进行下采样以缓解类别不均衡,并将数据集划分为训练集和验证集。
- 数据格式化:将数据转换为适合卷积网络输入的三维格式
(样本数, 序列长度, 1)。 - 构建1D CNN模型:我们搭建一个包含三个一维卷积层、一个池化层和三个全连接层的网络。模型输入为187维的心电信号序列。
model = Sequential([ Conv1D(filters=32, kernel_size=3, activation='relu', input_shape=(187, 1)), Conv1D(filters=32, kernel_size=3, activation='relu'), Conv1D(filters=32, kernel_size=3, activation='relu'), GlobalAveragePooling1D(), Dense(64, activation='relu'), Dense(32, activation='relu'), Dense(5, activation='softmax') # 5个类别 ]) - 模型训练与评估:使用分类交叉熵损失函数训练模型。经过少量轮次的训练,模型在验证集上即可达到很高的准确率。通过混淆矩阵可以进一步观察模型在各个类别上的分类效果。





总结 📝



本节课中我们一起学习了时间序列分析的全流程。我们从时间序列的定义和组成(趋势、季节、残差)入手,学习了如何通过代码构建和分解序列。接着,探讨了关键的特征提取技术,包括日期特征、滞后特征和滚动窗口特征。然后,我们介绍了用于时间序列分类和回归的各类模型。最后,通过心电图疾病识别的实战案例,完整演示了如何使用一维卷积神经网络对时间序列数据进行分类建模。希望本教程能帮助你入门时间序列分析,并将其应用于更多实际场景。

🧠 人工智能—计算机视觉CV公开课 P17:语义分割实战:人脸抠图PS


在本节课中,我们将要学习语义分割的基本概念,并动手实践一个具体应用:使用U-Net模型实现人脸抠图。我们将从深度学习的基础介绍开始,逐步深入到语义分割的原理,最后通过代码实战完成一个人脸分割项目。
🏢 第一部分:七月在线介绍
七月在线是一家成立于2015年的职业教育在线平台,专注于人工智能时代的人才培养和企业服务。平台不仅面向个人学习者,提供从入门到提升的全方位课程,也为包括中国联通、国家电网等在内的众多企业提供培训服务。
🤖 第二部分:深度学习介绍
上一节我们介绍了课程提供方,本节中我们来看看什么是深度学习。
深度学习是机器学习的一个子领域,它使用包含多层神经元的网络结构,这种结构类似于人脑的神经元连接。深度学习是一种端到端的训练过程,即模型直接从输入映射到输出,中间没有其他人工处理环节。
在建模时,可以将网络结构视为一个计算图,图中的节点代表具体的计算操作。一个典型的神经网络包含输入层、隐含层和输出层。
- 输入层:接收原始数据,可以是多维数据甚至矩阵。
- 隐含层:进行中间计算,可以有多层。
- 输出层:输出最终结果。
网络中的每个神经元本质上是一个计算单元。对于一个全连接层,每个神经元都会接收上一层的所有输入。计算过程可以表示为:
公式: 输出 = 激活函数( (输入1 * 权重1) + (输入2 * 权重2) + ... + (输入n * 权重n) + 偏置 )
用矩阵形式可以更高效地表示整个层的计算:
公式: 输出 = 激活函数( 输入矩阵 · 权重矩阵 + 偏置向量 )
深度学习非常适合处理图像、文本等非结构化数据,在人脸识别、机器翻译、自动驾驶等领域有广泛应用。
🎯 第三部分:语义分割理想
上一节我们介绍了深度学习的基础,本节中我们来看看计算机视觉中的一项重要任务——语义分割。


语义分割本质上是对图像中的每一个像素进行分类。这与普通的图像分类不同,图像分类是给整张图片分配一个标签,而语义分割需要输出一个与输入图像尺寸相同的矩阵,矩阵中每个位置的值代表该像素所属的类别。
核心概念对比:
- 图像分类:输入图像,输出一个类别标签(或类别概率向量)。
- 语义分割:输入维度为
H × W × C的图像,输出一个H × W × N的矩阵,其中N是类别数,输出矩阵的每个点都是一个类别概率向量。
例如,输入一张256x256的RGB图片(3通道),要对5个类别进行分割,那么模型输出的维度就是 256 × 256 × 5。
语义分割的标注工作通常比较繁琐,需要使用工具对每个像素进行标注。常见的数据集有PASCAL VOC、Cityscapes等。
🧩 第四部分:U-Net模型架构
上一节我们明确了语义分割的任务定义,本节中我们来看看完成该任务的经典模型——U-Net。
U-Net是一种专为语义分割设计的卷积神经网络,其结构呈对称的“U”形,主要由编码器(下采样)和解码器(上采样)两部分组成。
模型特点:
- 编码器:通过卷积和池化层逐步提取图像特征,缩小空间尺寸,增加通道数,以捕获高级语义信息。
- 解码器:通过转置卷积或上采样操作逐步恢复图像的空间尺寸,减少通道数,以生成精细的分割图。
- 跳跃连接:将编码器相应阶段的高分辨率特征图与解码器特征图进行拼接。这有助于解码器在恢复细节时利用编码器捕获的浅层局部信息(如边缘、纹理),从而提升分割边界的精度。
U-Net的这种设计使其在医学图像分割等需要精确轮廓的任务中表现出色。









💻 第五部分:人脸PS实战



上一节我们了解了U-Net的原理,本节中我们将使用U-Net完成人脸抠图的实战项目。我们的目标是:输入一张人像照片,输出一个掩码(Mask),将人物前景与背景分离。


数据准备
我们使用的数据集包含人像图片和对应的二值掩码标签(人物区域为1,背景为0)。为了统一输入尺寸和加速训练,我们将所有图片和标签缩放至128x128像素。




问题: 输入图片的维度是多少?输出掩码的维度是多少?
答案: 输入维度是 (128, 128, 3)。输出是二分类掩码,维度为 (128, 128, 1)。


以下是构建数据生成器(Data Generator)的关键步骤:
# 示例代码:数据预处理与生成器构建
def load_and_preprocess(image_path, mask_path):
# 读取图像和掩码
img = cv2.imread(image_path)
mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 读取为单通道灰度图
# 缩放到统一尺寸
img = cv2.resize(img, (128, 128))
mask = cv2.resize(mask, (128, 128))
# 归一化
img = img / 255.0
mask = mask / 255.0
mask = np.expand_dims(mask, axis=-1) # 增加通道维度,变为 (128,128,1)
return img, mask

模型构建




我们使用TensorFlow 2.0/Keras 来构建U-Net模型。以下是模型的核心结构:



# 示例代码:U-Net模型构建(简化版)
def unet_model(input_size=(128, 128, 3)):
inputs = Input(input_size)
# 编码器部分 (下采样)
c1 = Conv2D(64, 3, activation='relu', padding='same')(inputs)
p1 = MaxPooling2D((2, 2))(c1)
# ... 更多卷积和池化层 ...
# 解码器部分 (上采样)
u6 = Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same')(...)
u6 = concatenate([u6, c2]) # 跳跃连接,拼接对应编码器层的特征
c6 = Conv2D(64, 3, activation='relu', padding='same')(u6)
# ... 更多转置卷积和卷积层 ...
# 输出层
outputs = Conv2D(1, 1, activation='sigmoid')(...) # 单通道,sigmoid激活用于二分类
model = Model(inputs=[inputs], outputs=[outputs])
return model




模型训练与预测


我们使用二元交叉熵损失函数和Adam优化器来训练模型。




训练完成后,对新的图像进行预测。由于训练时输入是128x128,预测后需要将输出的掩码重新缩放到原始图像尺寸,再与原始图像结合,实现抠图效果。
验证集的作用:验证集用于在训练过程中监控模型是否出现过拟合,并作为调整超参数(如学习率、迭代次数)的依据。



📝 总结





本节课中我们一起学习了:
- 深度学习的基本概念,理解了神经网络尤其是全连接层的工作原理。
- 语义分割的任务定义,明确了其与图像分类的区别,即对图像进行像素级分类。
- U-Net模型架构,了解了其编码器-解码器结构以及跳跃连接的重要性。
- 人脸抠图实战,从数据准备、模型构建、训练到预测,完整实现了一个基于U-Net的语义分割应用。



通过本课程,你应该已经掌握了使用深度学习模型解决语义分割问题的基础流程和关键思想。

人工智能—计算机视觉CV公开课(七月在线出品) - P18:基于Pytorch实战:手部姿势识别 🖐️

在本节课中,我们将要学习如何利用深度学习模型进行手部姿势识别。课程将首先介绍一个高效的网络结构——EfficientNet,并探讨其背后的AutoML(自动机器学习)思想。随后,我们将通过一个实际的手势分类案例,演示如何使用PyTorch框架和EfficientNet模型来构建和训练一个手部姿态识别系统。
概述:EfficientNet与AutoML 🤖
上一节我们介绍了课程主题,本节中我们来看看EfficientNet网络及其背后的AutoML理念。EfficientNet是继ResNet之后,在深度学习中又一个非常重要的网络结构。它在模型参数量和运行速度方面都远超之前的网络。
EfficientNet在设计网络结构时,引入了AutoML的搜索过程。这意味着网络结构的设计并非完全依赖人工,而是通过算法自动搜索得到更优的配置。
那么,什么是AutoML呢?在具体的建模过程中,我们通常需要进行模型调参和优化等步骤。然而,这些步骤并非全部需要算法工程师手动参与。AutoML的目标是让机器自动完成部分或全部流程。
一个完整的数据挖掘或建模工作通常包含以下步骤:
- 数据预处理
- 特征提取
- 模型训练
- 模型选择
- 模型预测
在上述流程中,Machine Learning Framework(机器学习框架)部分的工作,很大程度上可以由机器自动完成。
AutoML能做什么?🔧
AutoML能够完成的任务范围很广,主要包括:
- 自动数据分析
- 自动特征工程
- 自动特征缩放
- 自动正则化
- 自动特征选择
- 自动模型选择
- 自动超参数优化
其中,最核心的方向有三个:
- 自动特征产生/构造
- 网络结构搜索(NAS)
- 模型超参数调优
自动特征产生
在特征工程中,自动特征挖掘是指从已有数据集中自动发现并构造新特征。
以下是两类主要方法:
1. 基于神经网络的方法
此方法构建一个神经网络,让网络自动完成特征提取和特征交叉。例如,在网络中加入嵌入层(Embedding Layer)对输入特征进行编码,然后让特征在模型内部自动进行高阶组合。
2. 基于搜索的方法
此方法将特征组合视为一个搜索问题。假设原始有A、B、C、D四个特征,目标是找到能带来最大精度收益的特征组合(如A+B,或A+B+D等)。这个过程需要对特征空间进行搜索和评估,以确定最有效的特征子集。
网络结构搜索(NAS)
网络结构搜索是AutoML中目前落地较多的方向。传统神经网络结构需要人工设计,过程繁琐且依赖专家知识。NAS的目标是通过算法自动搜索出最优的网络结构。
NAS的核心包含三个部分:
- 搜索空间:定义网络结构的基本组成元素(如卷积核大小、池化方式、连接方式等)。
- 搜索策略:采用何种算法在搜索空间中寻找最优结构,例如基于强化学习或基于梯度的方法。
- 性能评估:如何快速、准确地评估搜索到的网络结构的性能。
由于网络结构的组合可能性极多,NAS通常需要巨大的计算量。
一种常见的可微分搜索方法是DARTS。它将网络结构表示为一个有向无环图,图中节点代表特征,边代表可能的操作(如卷积、池化)。DARTS为每条边赋予一个可训练的权重参数,通过梯度下降来优化这些权重,最终“软化”地选择出最优的网络连接和操作。其优化目标可表示为以下公式:
min_α L_val(ω*(α), α)
s.t. ω*(α) = argmin_ω L_train(ω, α)
其中,α代表网络结构参数,ω代表网络权重参数。目标是在验证集上取得最小损失,同时网络权重需要在训练集上最优。
对于想实践AutoML的开发者,可以使用一些开源库,例如基于TensorFlow的AutoKeras。它可以用非常简洁的代码自动搜索适合特定数据集(如图像分类、文本情感分析)的网络结构。
EfficientNet:复合缩放策略 📈
上一节我们了解了AutoML和NAS,本节中我们来看看EfficientNet如何运用这些思想。EfficientNet的网络设计内在包含了AutoML的思路。
在图像分类领域,ImageNet是常用的评估数据集。评价一个模型的好坏,通常从准确率、参数量和计算量三个维度考量。理想模型是参数量少、计算量低,同时准确率高。
研究发现,提升模型精度主要可以通过三种方式:
- 增加网络深度(
depth) - 增加网络宽度(
width,即通道数) - 增加输入图像分辨率(
resolution)
单独增加任何一项都能提升精度,但也会增加计算量。EfficientNet的洞察在于:平衡地缩放深度、宽度和分辨率三个维度,可以获得更好的精度-效率权衡。
因此,EfficientNet将问题形式化为一个约束优化问题:在给定计算资源(FLOPS)预算下,最大化模型精度。它提出了一个复合缩放策略,使用一个复合系数φ来统一缩放三个维度:
depth: d = α^φ
width: w = β^φ
resolution: r = γ^φ
s.t. α · β² · γ² ≈ 2
α ≥ 1, β ≥ 1, γ ≥ 1


其中,α, β, γ是通过网格搜索确定的基础放大系数。约束条件 α · β² · γ² ≈ 2 是为了确保FLOPS大约增加 2^φ 倍。


EfficientNet的设计流程分为两步:
- 固定
φ=1,通过网格搜索确定最优的α, β, γ,得到基础网络 EfficientNet-B0。 - 使用不同的
φ值(如1, 2, 3...),在B0的基础上进行复合缩放,得到 EfficientNet-B1 到 B7 等一系列模型。
通过这种系统化的缩放方法,EfficientNet家族模型在准确率和效率上都达到了当时(2019年)的领先水平。
实战:基于PyTorch的手势识别 ✋



上一节我们学习了EfficientNet的原理,本节中我们来看看如何将其应用于实际任务——手势识别。

我们将使用一个包含10类手势(如数字1-5、不同拇指朝向等)的数据集。任务目标是对输入的手部图片进行分类。


以下是实现步骤的核心代码概述:


1. 导入库与定义模型
import torch
import torchvision.models as models
# 加载预训练的EfficientNet-B0,并修改其分类头以适应我们的10分类任务
model = models.efficientnet_b0(pretrained=True)
model.classifier[1] = torch.nn.Linear(model.classifier[1].in_features, 10) # 10个手势类别

2. 加载与预处理数据
from torchvision import transforms, datasets
# 定义数据预处理(调整大小、归一化等)
data_transforms = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# 加载数据集
train_dataset = datasets.ImageFolder('path/to/train_data', transform=data_transforms)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)




3. 定义损失函数与优化器
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)


4. 训练模型
for epoch in range(num_epochs):
for images, labels in train_loader:
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
5. 模型评估与预测
训练完成后,可以在验证集上评估模型精度,并对新图片进行预测。


关于更复杂的手势识别
需要注意的是,上述案例是简单的图像分类。对于需要识别手部关键点(如指尖、关节)的更复杂任务(例如手语识别或精细的手势控制),则需要使用姿态估计模型。这类模型(如Google的MediaPipe)会输出手部的骨骼关键点坐标,而非简单的类别标签。工业界在部署时,往往会选择更轻量、高效的关键点检测模型。

总结 🎯


本节课中我们一起学习了以下内容:
- AutoML的核心思想:让机器自动完成特征工程、模型选择和超参数优化等流程,其中网络结构搜索是一个重要方向。
- EfficientNet的设计精髓:通过复合缩放策略,平衡地增加网络深度、宽度和输入分辨率,从而在提升精度的同时高效利用计算资源。
- 实战手势识别:我们演示了如何使用PyTorch和预训练的EfficientNet模型,快速构建一个手势图像分类器。对于更复杂的、基于关键点的手势识别任务,则需要采用姿态估计模型。


希望本教程能帮助你理解EfficientNet和AutoML的概念,并为你实现自己的计算机视觉项目提供基础。

人工智能—计算机视觉CV公开课(P19):从零实现人脸关键点模型的训练和部署 🎯
在本节课中,我们将学习如何从零开始构建、训练并部署一个用于人脸关键点检测的深度学习模型。我们将从深度学习的基础概念讲起,逐步深入到模型搭建、训练流程,并最终完成一个可运行的实时检测应用。

第一部分:深度学习介绍 🤖
深度学习是包含多层神经元结构的一种机器学习模型,其整体结构与人脑神经元网络有相似之处。深度学习是机器学习的一个分支,属于人工智能领域的一部分。
理解深度学习,需要明确它与机器学习、人工智能的关系。人工智能的范围最广,包含了机器学习。机器学习则包含了深度学习以及其他多种模型(如决策树、支持向量机等)。深度学习是机器学习中一类以神经网络为基础的算法。
深度学习的一个核心特点是“端到端”(End-to-End)学习。这意味着模型直接从原始输入(如图片)学习,并输出最终结果(如分类标签),中间无需复杂的人工特征工程。模型能够自动从数据中学习并提取有用的特征。
深度学习模型可以看作一个有效的计算图。图中的每个节点代表一个计算单元(神经元),节点之间的有向连接代表数据的流动方向。一个典型的全连接网络包含输入层、隐含层和输出层。输入层接收原始数据,隐含层进行特征变换和提取,输出层产生最终预测结果。
第二部分:模型搭建基础 🧱
上一节我们介绍了深度学习的基本概念,本节中我们来看看如何搭建一个基础的神经网络模型。
在搭建卷积神经网络时,通常遵循以下步骤:
- 确定模型的输入和输出维度。
- 设计隐含层结构,例如确定卷积层的通道数、卷积核大小、步长和填充等参数。
深度学习框架(如PyTorch)将各种功能封装为不同的“层”。常见的层类型包括:
- 全连接层:进行线性变换。
- 卷积层:提取空间特征,是计算机视觉的核心。
- 激活函数层:如ReLU、Sigmoid,引入非线性。
- 池化层:进行下采样,降低数据维度。
- 损失函数层:如交叉熵(Cross-Entropy)、均方误差(MSE),用于衡量预测误差。
卷积层详解
卷积层是卷积神经网络的核心。其操作类似于数字图像处理中的滤波器。对于一个二维输入,卷积核在输入数据上滑动,进行局部区域的加权求和计算。
卷积操作涉及几个关键参数:
- 卷积核大小:决定感受野的大小。
- 步长:卷积核每次滑动的像素数。
- 填充:在输入数据边缘添加的像素(常为0),用于控制输出尺寸。
输出特征图尺寸的计算公式为:
输出尺寸 = (输入尺寸 + 2 * 填充 - 卷积核尺寸) / 步长 + 1
对于彩色图像等多通道输入,卷积核也需要具备相应的通道数。每个通道分别与输入的对应通道进行卷积,最后将所有通道的结果相加,并加上偏置项,得到一个输出通道的值。使用多个卷积核,即可得到多通道的输出特征图。
一个典型的卷积神经网络由卷积层、激活函数层、池化层和全连接层组合而成。网络前部通过卷积和池化逐步提取和压缩特征,最后通过全连接层将特征映射到最终的输出(如分类结果)。
第三部分:模型参数与训练过程 🔄
搭建好模型结构后,我们需要通过训练来确定模型内部的参数。首先,要区分参数和超参数。
参数是模型内部可以通过训练数据自动学习和调整的变量,例如神经网络中的权重和偏置。
超参数是在训练开始前需要人工设定的变量,它们控制着训练过程本身,例如学习率、批大小、网络层数、卷积核尺寸等。模型的结构本身也是一个超参数。
超参数的选择至关重要。以学习率为例:
- 学习率过小,参数更新缓慢,训练效率低下。
- 学习率过大,参数更新步伐太大,可能导致损失函数在最优值附近震荡,无法收敛。
模型的训练是一个迭代优化过程,主要包含两个步骤:正向传播和反向传播。
- 正向传播:输入数据通过网络层层计算,得到预测输出。
- 计算损失:使用损失函数比较预测输出与真实标签的差异。
- 反向传播:计算损失函数相对于每个模型参数的梯度(偏导数)。这指明了参数调整的方向。
- 参数更新:使用优化算法(如随机梯度下降SGD),沿着梯度反方向更新参数,以减小损失。更新公式为:
新参数 = 旧参数 - 学习率 * 梯度
训练通常不会使用全部数据一次性更新,而是采用小批量梯度下降。将数据集分成多个小批次,每次使用一个批次进行正向和反向传播并更新参数。这样做既提高了计算效率,又因批次的随机性有助于模型跳出局部最优。
完整的训练流程是一个双重循环:
- 外层循环遍历整个数据集多次(每个遍历称为一个“轮次”或Epoch)。
- 内层循环遍历数据集中的所有小批次(Batch)。
第四部分:代码实践与案例 🖥️
理论需要实践来巩固。我们将使用PyTorch框架进行代码实践。PyTorch的张量(Tensor)与NumPy数组类似,但支持GPU加速和自动求导,非常适合深度学习。
1. PyTorch基础与线性回归
首先,我们创建一个简单的线性回归任务来演示训练流程。
import torch
# 1. 准备数据
x = torch.linspace(0, 10, 100).reshape(-1, 1)
y = -3 * x + 4 + torch.randint(-2, 3, (100, 1)).float()
# 2. 初始化参数
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
# 3. 定义损失函数和优化器
criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD([w, b], lr=0.05)
# 4. 训练循环
for epoch in range(20):
y_pred = x * w + b # 正向传播
loss = criterion(y_pred, y) # 计算损失
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播
optimizer.step() # 更新参数
print(f'Epoch {epoch}, Loss: {loss.item()}')
2. 构建卷积神经网络进行人脸关键点检测
接下来,我们构建一个CNN模型来检测人脸关键点(如眼睛、鼻子、嘴角的坐标)。
import torch.nn as nn
class FaceKeypointCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv_layers = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), # 输入1通道,输出6通道
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, kernel_size=5), # 输入6通道,输出16通道
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(16, 32, kernel_size=3),# 输入16通道,输出32通道
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
self.fc_layers = nn.Sequential(
nn.Flatten(), # 将特征图展平为向量
nn.Linear(32 * 5 * 5, 256), # 全连接层
nn.ReLU(),
nn.Linear(256, 8) # 输出8个坐标值 (x1,y1,x2,y2,...)
)
def forward(self, x):
x = self.conv_layers(x)
x = self.fc_layers(x)
return x
# 实例化模型、定义损失和优化器
model = FaceKeypointCNN()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters())
训练过程与线性回归类似,但数据加载更复杂。我们需要使用DataLoader来批量加载图像和标签数据,并进行归一化等预处理。
3. 模型部署与应用
训练完成后,我们可以保存模型权重,并将其部署到实际应用中。

以下是部署的核心步骤:
- 加载模型:加载训练好的权重文件。
- 图像预处理:对摄像头捕获的每一帧图像,使用OpenCV进行人脸检测和裁剪,然后调整为模型所需的输入尺寸并归一化。
- 模型推理:将处理后的图像输入模型,进行正向传播得到关键点坐标。
- 结果可视化:将预测的关键点绘制回原始图像帧上。






import cv2
# 加载训练好的模型
model.load_state_dict(torch.load('face_keypoint_model.pth'))
model.eval() # 设置为评估模式
# 初始化摄像头
cap = cv2.VideoCapture(0)
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
while True:
ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
for (x, y, w, h) in faces:
# 裁剪人脸区域并预处理
face_roi = gray[y:y+h, x:x+w]
face_resized = cv2.resize(face_roi, (96, 96))
face_tensor = torch.from_numpy(face_resized).float().unsqueeze(0).unsqueeze(0) / 255.0
# 模型推理
with torch.no_grad():
keypoints = model(face_tensor).view(-1, 2).numpy() * 96
# 将关键点坐标映射回原图
for kp in keypoints:
px, py = int(x + kp[0] * w / 96), int(y + kp[1] * h / 96)
cv2.circle(frame, (px, py), 3, (0, 255, 0), -1)
cv2.imshow('Face Keypoint Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()


总结 📝
本节课中我们一起学习了深度学习的完整流程:
- 理解基础:明确了深度学习是机器学习的一个分支,其核心是端到端的特征学习和多层神经网络计算。
- 搭建模型:学习了卷积神经网络的结构,包括卷积层、池化层、全连接层的作用和参数计算。
- 训练模型:掌握了模型训练的核心循环:正向传播、计算损失、反向传播和参数更新,并理解了超参数(如学习率)的重要性。
- 实践与部署:使用PyTorch框架实现了从简单的线性回归到复杂的人脸关键点检测CNN模型的代码,并完成了从训练到实时摄像头部署的全过程。


通过这个从理论到实践、从训练到部署的完整案例,希望你对如何构建一个深度学习项目有了清晰的认识。持续学习和动手实践是掌握深度学习的关键。



🎬 人工智能—计算机视觉CV公开课(七月在线出品) - P2:工业级的视频版权检测算法


在本节课中,我们将学习工业级的视频版权检测算法。这是一个针对特定场景的完整解决方案,我们将从任务定义、基础知识讲起,逐步深入到图像和视频版权检测的核心算法,并最终了解如何将其应用于实际比赛和工业场景。
📋 第一部分:视频版权检测任务定义
上一节我们介绍了课程的整体结构,本节中我们来看看视频版权检测任务的具体定义。



视频版权检测,学术上称为 Video Duplicate Detection。随着移动互联网和智能手机的普及,短视频已成为重要的信息媒介,同时也带来了大量的视频侵权行为。本课程主要从视觉角度解决版权检测问题,不涉及音频部分。
任务可以抽象为:给定一个待检测的短视频(称为 Query),我们需要在一个视频库(包含许多 Refer 视频)中,找到它可能侵权的原始长视频。侵权的视频可能经过多种复杂变换,例如:
- 插入模板、文字、Logo、水印或小动画。
- 进行降质处理,如模糊、丢帧、调整对比度或分辨率。
- 进行裁剪、混剪(视频拼接)、画中画、刚体变换、速度变换等综合操作。


这些变换是视频版权检测的主要难点。目前已有多个学术数据集来研究此问题,例如 CC_WEB_VIDEO, FIVR, VCDB 以及抖音发布的 SVD 数据集。该领域仍是一个活跃的研究方向。


🧠 第二部分:图像特征基础知识
理解了任务背景后,我们需要掌握一些基础知识。本节将介绍图像的两类核心特征:局部特征和全局特征。


图像特征是我们比较和检索内容的基础。主要分为两类:


1. 图像局部特征
局部特征关注图像内的关键点,如角点或极值点(例如 SIFT、ORB 算法提取的点)。其特点是:
- 特征点数量(N)不固定,随图像内容变化。
- 关注局部信息,具有尺度不变性,对旋转、颜色变化相对鲁棒。
- 缺点:易受图像中文字等强边缘信息干扰。

2. 图像全局特征
全局特征关注图像的整体统计信息或语义特征(例如颜色直方图或 CNN 提取的特征)。其特点是:
- 特征维度是固定的(例如一个 512 维的向量)。
- 关注全局信息,但通常对图像的尺度变化(如旋转)比较敏感。

简单对比:局部特征是一个 N x 128 的矩阵(N 不固定),而全局特征是一个 1 x 512 的向量。

🖼️ 第三部分:图像版权检测算法
掌握了图像特征后,我们可以将其应用于具体问题。本节将探讨如何利用这些特征进行图像版权检测。
图像版权检测(Image Copyright Detection)是判断两张图片是否包含相同像素区域的相似图像检索任务。判定标准可以是:是否包含相同物体、像素相似度是否高、或可匹配的关键点是否多。


一个基础的解决思路是 “词袋”模型。以下是其核心流程:
- 提取局部特征:从所有图片中提取关键点(如 SIFT 特征)。
- 特征聚类:将所有图片的关键点特征进行聚类,形成一部“视觉词典”。
- 特征映射:将每张图片的局部特征映射到这部词典上,统计每个“视觉单词”出现的频率,从而将每张图片表示为一个固定维度的向量(词袋向量)。
- 相似度计算:通过计算两个词袋向量的相似度(如余弦相似度)来判断图像是否相似。若相似度高于阈值(如 0.9),则认为两张图片相同。
公式:图像 I 的词袋向量表示为:V_I = [w1, w2, ..., wk],其中 wi 是第 i 个视觉单词在该图像中出现的频率或权重。


🎥 第四部分:视频版权检测算法
将图像检测扩展到视频领域是核心目标。本节我们将学习如何将视频版权检测问题分解并解决。
我们以 CCF BDCI 2019 年的“爱奇艺视频版权检测”比赛为例。任务要求:根据 Query 侵权短视频,找到对应的 Refer 长视频,并精确定位两者在时间轴上的对应片段(即找出 Query 的开始/结束时间 Qs, Qe 和 Refer 的开始/结束时间 Rs, Re)。
我们可以将此任务分解为两个子任务:
- 视频对应关系:找到 Query 视频对应的 Refer 视频(视频检索任务)。
- 时间轴对应关系:精确定位时间片段。

步骤一:视频抽帧
首先需要将视频转换为图像序列。常用方法有:
- 关键帧抽取:抽取视频压缩中的 I 帧(信息量最大,最清晰)。
- 场景转换帧抽取:通过计算帧间差异(如颜色直方图差异)来检测镜头切换点。
- 均匀抽帧:按固定时间间隔抽帧。
在版权检测任务中,抽取 I 帧 通常最有效,因其数量少、信息密度高。
步骤二:图像特征提取
对抽出的关键帧提取特征。可以使用:
- 全局特征:如用预训练的 CNN 模型(ResNet, VGG)提取特征。在池化层选择上,最大池化(Max Pooling) 通常比平均池化更有效,因其能保留更突出的特征。
- 局部特征:如 SIFT 或基于深度学习的局部特征(如 DELF),后者兼具 CNN 的强表征能力和局部特征的尺度不变性。
步骤三:解决子任务一(视频检索)
- 对 Query 和 Refer 视频都抽取关键帧并提取特征(如 ResNet18 的卷积层特征),并进行 L2 归一化。
- 将 Query 的每一关键帧特征,与 Refer 库中所有关键帧特征进行相似度计算(如余弦相似度),保留相似度最高的 Top-K 个结果。
- 统计这 Top-K 个结果中,来自同一个 Refer 视频的出现次数。出现次数最多、且平均相似度最高的那个 Refer 视频,即被认为是 Query 对应的视频。

步骤四:解决子任务二(时间轴对应)
在找到视频对应关系后,利用已匹配上的关键帧对来推算时间轴。基于一些先验知识(如视频速度比通常为 1:1,裁剪拼接后的起止帧通常是关键帧),可以进行推算:
假设已知 Query 的第 N-1 帧匹配 Refer 的第 M-1 帧,Query 的第 N 帧匹配 Refer 的第 M 帧。
若速度比为 1:1,则可以推断 Query 中第 N-2 帧在 Refer 中的对应位置约为 M-1 - (Q[N-1]的时间 - Q[N-2]的时间)。
通过这种向前/向后推导,并结合多个匹配对进行验证,可以估算出 Qs, Qe, Rs, Re。


其他优秀方案亮点
- 第一名方案:使用 VGG 浅层卷积特征(保留更多像素细节)结合 R-MAC 池化(一种区域聚合池化,对裁剪更鲁棒)。
- 第二名方案:采用 深度局部特征,在 CNN 特征图上提取关键点,兼具高精度和尺度不变性,且计算效率高。
工程优化:特征检索与压缩
当视频库很大时,需要对海量帧特征进行快速检索。常用方法有:
- 暴力检索 (KNN):精度高,适合数据量小、要求 100% 召回率的场景。
- 近似最近邻搜索 (ANN):如 HNSW、PQ 量化等方法,牺牲少量精度以换取大幅速度提升,适合千万甚至上亿级别的大规模检索。
🔍 第五部分:扩展:图像与视频检索


最后,我们简要扩展一下更广义的检索任务。视频版权检测属于“以图搜图”或“以视频搜视频”的范畴。除此之外,还有:
- 类别检索:根据视频类别搜索。
- 文本检索:根据文本描述搜索视频。这是一个前沿方向,通常通过将文本和图像映射到同一个特征空间(例如使用 Word2Vec 和 CNN),然后在该空间内进行最近邻搜索来实现。

📝 总结


本节课中我们一起学习了工业级视频版权检测算法的完整流程。我们从任务定义出发,学习了图像局部与全局特征的基础知识,并了解了如何利用词袋模型进行图像版权检测。接着,我们深入探讨了视频版权检测的核心,将其分解为视频检索和时间轴定位两个子任务,详细讲解了抽帧、特征提取、匹配和推算的具体步骤。最后,我们还了解了特征检索的工程优化方法以及其他图像视频检索的扩展方向。




希望本教程能帮助你理解视频版权检测的核心思想与实现路径。



人工智能—计算机视觉CV公开课(P20):项目实战-精灵宝可梦识别 🎮
在本节课中,我们将学习计算机视觉领域的核心任务,包括如何完成图像特征提取、使用无监督模型K-means进行聚类,以及使用卷积神经网络(CNN)完成图像分类。课程内容将从基础概念到代码实战,帮助初学者建立完整的知识体系。
第一部分:图像特征提取 🖼️

图像是一种典型的非结构化数据。在处理图像数据集时,我们需要对图像进行特征提取,即通过一组具体的特征来描述图像。这涉及到对图像特征的有效编码,并且编码方式通常与基础数据强相关。



对于图像而言,我们需要关注两个核心问题:图像特征如何提取,以及图像特征如何与具体任务相结合。对于初学者,图像分类和图像检索是较为适合入门的学习任务。


图像分类和检索的本质,都是通过提取图像特征,然后完成特征到目标(类别或相似图像)的映射。
然而,图像本身千变万化。即使是同一物体(如蒙娜丽莎),在不同角度、光照、遮挡等条件下拍摄的照片也可能差异巨大。因此,我们需要能够应对这些变化的特征提取方法。


图像特征类型

图像特征主要可以分为两类:全局特征和局部特征。
- 全局特征:从整体角度关注图像的宏观信息。
- 局部特征:关注图像中细节的关键点。
提取特征后,我们可以将其应用于图像检索任务,例如寻找相似或相同的图像。例如,我们可以提取图像的深度学习特征(全局)或关键点匹配特征(局部)来完成检索。

以下是常见的图像特征提取方法:


- 图片哈希值
- 描述:将图片转换为一个定长的字符串(哈希值),类似于MD5。常用于快速查找重复图片。
- 过程:首先将图片缩放到小尺寸(如8x8),然后基于像素值(例如判断是否大于128)生成一个布尔矩阵,最后转换为十六进制字符串。
- 优点:生成定长字符串,便于数据库存储和比较,计算资源消耗小。
- 缺点:对图片内容的任何微小改动(如亮度、裁剪)都可能导致哈希值发生巨大变化,且不同内容的图片可能产生相同的哈希值(哈希碰撞)。



- 颜色直方图
- 描述:统计图像中不同颜色值出现的频率分布。它是一种全局特征。
- 过程:分别统计RGB三个通道(或其他颜色空间)中,每个颜色值(0-255)对应的像素点个数,并绘制成分布曲线。
- 优点:不关心像素位置,对图像的旋转和平移不敏感。
- 缺点:对颜色变化非常敏感,且丢失了所有的空间位置信息。




上一节我们介绍了图像特征提取的基本概念和方法,本节中我们来看看如何使用无监督学习模型对数据进行聚类。


第二部分:无监督模型K-means聚类 🔍

K-means是一种在日常生活和数据分析中非常普及且有效的无监督聚类算法。它根据数据本身的相似性,将数据点划分为不同的组(簇)。
K-means算法步骤
K-means的聚类过程遵循以下步骤:
- 初始化:随机选择k个数据点作为初始的聚类中心。
- 分配簇:计算每个数据点到所有聚类中心的距离,并将其分配到距离最近的聚类中心所在的簇。
- 更新中心:重新计算每个簇中所有数据点的平均值,将该平均值作为新的聚类中心。
- 迭代:重复步骤2和步骤3,直到聚类中心不再发生变化或达到预设的迭代次数。


公式: 数据点 \(x_i\) 到聚类中心 \(\mu_j\) 的距离通常使用欧氏距离计算:
\(d(x_i, \mu_j) = \sqrt{\sum_{d=1}^{D}(x_{i,d} - \mu_{j,d})^2}\)
其中,\(D\) 是特征维度。
K-means是一种无监督算法,不需要数据标签。但需要预先指定超参数k(聚类数量)。除了K-means,还有基于密度的DBSCAN等其他聚类算法。

如何选择K值?——肘部法则

K值的选择至关重要。常用的方法是“肘部法则”(Elbow Method)。


过程:
- 尝试不同的k值(例如从2到20)。
- 对每个k值运行K-means算法,并计算所有样本到其所属簇中心的距离平方和(SSE,或称惯性)。
- 绘制k值与对应SSE的关系曲线。
- 观察曲线,寻找“肘点”——即SSE下降速度突然变缓的点,该点对应的k值通常是一个较好的选择。
我们已经学习了特征提取和聚类,接下来我们将进入本节课的核心部分:使用卷积神经网络进行图像分类。
第三部分:CNN图像分类 🧠

深度学习,特别是卷积神经网络(CNN),在图像领域取得了巨大成功。CNN通过堆叠多层结构(卷积层、池化层等)来提取图像特征并完成建模。

卷积神经网络基础
- 卷积层:核心操作,使用卷积核在输入图像上滑动,进行局部特征提取。
- 计算:卷积核与输入图像的局部区域进行元素级相乘后求和。
- 作用:可以视为一种参数共享、稀疏连接的全连接层,能有效提取图像的边缘、纹理等特征。
- 多通道卷积:可以处理多通道输入(如RGB三通道),每个卷积核也有对应的通道数,分别与输入通道卷积后求和。
- 池化层:通常跟在卷积层后,用于降维和保留主要特征,增强模型鲁棒性。
- 最大池化:取池化窗口内的最大值。
- 平均池化:取池化窗口内的平均值。
- 网络结构:典型的CNN由“卷积层 → 激活函数 → 池化层”重复堆叠,最后连接全连接层输出分类结果。


在实践中,我们常使用预训练模型(如ResNet、VGG)作为特征提取器或微调的基础,这能节省训练时间并提升性能。
使用PyTorch实战CNN分类

以下是使用PyTorch框架进行精灵宝可梦分类的关键步骤概述:

- 数据准备:
- 读取数据集,每个类别的图片放在一个文件夹下,文件夹名即为标签。
- 使用
LabelEncoder将字符串标签转换为整数。 - 划分训练集和验证集(例如5000张训练,1000张验证)。
- 创建自定义
Dataset和DataLoader,实现数据的批量加载、图像缩放和归一化。



模型定义:
- 加载预训练的ResNet-18模型。
- 修改其最后的全连接层,使其输出维度等于我们的类别数(例如150类)。
import torchvision.models as models model = models.resnet18(pretrained=True) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 150) # 150个类别

训练与验证:
- 训练函数:包含前向传播、损失计算、反向传播和参数优化。
- 验证函数:仅包含前向传播,用于评估模型在验证集上的性能。
- 定义损失函数(如交叉熵损失)和优化器(如Adam)。
- 循环多个轮次(Epoch),每轮在训练集上训练,并定期在验证集上测试,保存最佳模型。
模型评估:
- 训练过程中,观察训练集和验证集的准确率变化。一个健康的模型,两者应同步上升并最终趋于稳定。
- 最终模型在验证集上的准确率是衡量其性能的关键指标。

知识点总结与展望 📚

本节课中我们一起学习了计算机视觉入门的关键三步:


- 图像特征提取:学习了全局特征(如颜色直方图)和局部特征的概念,以及图片哈希值这一具体方法。
- 无监督聚类:掌握了K-means算法的原理、步骤以及如何使用肘部法则选择最佳K值。
- CNN图像分类:理解了卷积神经网络的基本组件(卷积层、池化层),并完成了从数据准备、模型构建到训练评估的完整实战流程。


CNN是计算机视觉的基石,其本质是强大的特征提取器和模式识别器。要进一步深入学习,可以关注以下方向:
- CNN在其他任务上的应用,如图像分割、目标检测、姿态估计等。
- 如何将CNN与循环神经网络(RNN/LSTM)结合,处理视频或序列图像数据。
- 更先进的网络架构(如ResNet, Transformer)及其原理。



希望本教程能帮助你顺利踏入计算机视觉的大门!
人工智能—计算机视觉CV公开课(七月在线出品) - P21:22.3.10 实时情绪识别实战-模型训练与部署
概述 📋
在本节课中,我们将学习深度学习的基础概念,并通过一个实时情绪识别的实战项目,了解从模型搭建、训练到部署的完整流程。课程将涵盖深度学习与机器学习的区别、神经网络的基本原理、使用PyTorch框架进行模型训练,以及如何将训练好的模型应用于实际场景。
第一部分:深度学习介绍 🧠
深度学习的定义与范围
深度学习是一种包含多层神经元结构的计算方法,其结构类似于人脑的神经元网络。我们关注的重点是模型从输入到输出的整体网络结构。
在学习深度学习时,不可避免地会接触到机器学习。深度学习是机器学习的一个分支,特别是基于神经元计算的分支。如果将人工智能、机器学习和深度学习的关系用一个维恩图表示,人工智能是最大的范围,机器学习包含在其中,而深度学习则是机器学习的一部分。
对于初学者而言,建议先学习机器学习,再学习深度学习。许多深度学习课程也会先讲解机器学习的基础知识,因此无需担心知识割裂。
深度学习与机器学习的区别
机器学习包含许多非深度学习的方法,例如树模型、线性模型(如逻辑回归、LASSO)以及集成学习等。这些方法都是机器学习的重要组成部分。因此,不能将深度学习与机器学习等同或对立起来。
两者的学习方法也有较大不同:
- 机器学习:更关注模型原理、模型构造过程以及模型的泛化能力(即精度评价)。
- 深度学习:更关注网络结构本身以及网络的优化训练过程。
深度学习的特点
- 端到端计算:深度学习可以自动进行特征工程。传统的机器学习流程是:数据 → 特征工程 → 模型 → 预测。而深度学习是端到端的,即:输入 → 深度学习模型 → 输出。模型会自动学习所需的特征。
- 有向计算图:深度学习的网络结构可以用有向图表示。节点代表具体的计算过程,边代表数据的流向。





第二部分:神经网络基础 ⚙️


全连接网络
一个典型的全连接网络由输入层、隐藏层和输出层组成。隐藏层可以有多层。
- 输入层:接收原始输入数据。
- 隐藏层:接收前一层的输入,进行计算后输出给下一层。
- 输出层:输出最终结果。
在全连接网络中,每一层的所有节点都与下一层的所有节点相连。层内的节点之间没有连接。
神经元与矩阵计算
单个神经元的计算过程可以看作是输入与权重的线性加权求和,再加上偏置,最后通过一个激活函数得到输出。
用公式表示如下:
输出 = 激活函数( (X1 * W1 + X2 * W2 + ... + Xn * Wn) + B )
这个计算过程本质上就是矩阵乘法。因此,全连接层的计算可以通过矩阵乘法来实现。理解这个计算过程至关重要,因为这不仅是使用深度学习框架的基础,也是未来可能需要自定义网络层所必备的知识。
深度学习的应用场景
深度学习非常适合处理非结构化数据,例如:
- 图像类:人脸识别、车牌识别、动物识别、红绿灯检测。
- 文本类:机器翻译、客服对话机器人。
相比之下,结构化数据(如表格数据)更适合用传统的机器学习方法处理。

第三部分:手动实现与模型训练 🔧
手动实现全连接层
我们可以使用矩阵乘法来手动模拟一个全连接层。以手写数字数据集MNIST为例,其输入是28x28的灰度图。首先需要将二维图像数据展平为一维向量(784维)。
以下是定义两层全连接网络参数的核心思想:
- 第一层:输入维度784,隐藏层维度256。这对应一个
784x256的权重矩阵W1和一个256维的偏置向量B1。 - 第二层:输入维度256,输出维度10(对应10个数字类别)。这对应一个
256x10的权重矩阵W2和一个10维的偏置向量B2。
训练时,进行前向传播计算:
输出 = ((输入 * W1 + B1) * W2 + B2)
这里的乘法均为矩阵乘法。
模型训练与梯度下降
训练深度学习模型需要一个目标函数(损失函数)来衡量预测值与真实标签的差异。然后通过优化算法(如随机梯度下降)来更新模型参数,以最小化损失。
随机梯度下降的步骤如下:
- 初始化模型参数
W。 - 重复以下过程:
- 计算当前参数下损失函数的梯度(偏导数)。
- 沿着梯度的反方向更新参数:
W = W - 学习率 * 梯度。
梯度指示了损失函数增长最快的方向,因此反向更新可以使损失下降。

使用PyTorch自动求导

在实际中,我们使用PyTorch等框架,它们可以自动计算梯度。例如:
import torch
x = torch.tensor([[1, 2], [3, 4]], requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()
out.backward() # 自动计算out对x的梯度
print(x.grad) # 查看梯度值
backward() 方法会自动计算并累积梯度。在参数更新前,通常需要用 optimizer.zero_grad() 清空上一轮的梯度,防止累积。
线性回归示例
以下是一个使用PyTorch实现线性回归的简化流程:
- 生成模拟数据(如
Y = -3X + 4加噪声)。 - 定义参数
W和B,并设置为需要梯度。 - 定义损失函数(如均方误差MSE)。
- 在循环中:
- 前向传播计算预测值:
预测 = X * W + B - 计算损失:
loss = MSE(预测, 真实Y) - 反向传播计算梯度:
loss.backward() - 使用优化器更新参数:
optimizer.step() - 清空梯度:
optimizer.zero_grad()
- 前向传播计算预测值:
学习率是一个重要的超参数。过小会导致收敛慢,过大会导致在最优解附近震荡。


第四部分:卷积神经网络与情绪识别实战 🎭

上一节我们介绍了全连接网络和训练基础,本节我们将使用更强大的卷积神经网络(CNN)来完成情绪识别任务。

卷积神经网络简介
卷积神经网络仿生了生物的视觉机制。其核心是使用卷积层自动提取图像的特征,再通过池化层和全连接层得到最终输出。CNN的关键在于使用卷积核(滤波器)在图像的局部区域进行计算,从而捕获空间特征。
实战步骤分解
对于初学者,完成一个深度学习项目通常会在数据读取这一步遇到困难。整个流程可分为三步:
- 读取与处理数据
- 定义模型
- 训练模型
1. 数据读取与处理
对于按文件夹分类存储的图像数据集(例如,每个子文件夹代表一种情绪类别),可以使用PyTorch的 ImageFolder 来方便地加载。
同时,需要定义 transforms 来进行数据转换和增强,例如调整尺寸、转为灰度图、随机翻转、旋转等,这有助于提升模型的泛化能力。
from torchvision import datasets, transforms
# 定义数据转换(包括增强)
transform = transforms.Compose([
transforms.Grayscale(),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])


# 使用ImageFolder加载数据集
train_dataset = datasets.ImageFolder(root='path/to/train_data', transform=transform)
# 创建DataLoader,用于批量读取数据
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)
2. 定义情绪识别CNN模型

模型定义通常继承自 torch.nn.Module,并重写 __init__(初始化层)和 forward(定义前向传播逻辑)两个函数。
一个简单的情绪识别CNN模型结构如下:
- 前端:由多个卷积块(卷积层 + 批归一化层 + 激活函数 + 池化层)堆叠而成,用于特征提取。
- 后端:将提取的特征展平后,接入全连接层,最终输出对应各类情绪的概率。
在PyTorch中,可以直接使用 nn.Conv2d, nn.Linear 等预定义层,无需手动创建权重矩阵。

3. 训练模型

训练循环的代码与之前的线性回归示例结构类似,但使用了更便捷的优化器:
model = EmotionCNN() # 实例化模型
criterion = nn.CrossEntropyLoss() # 定义损失函数(分类任务常用交叉熵损失)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 定义优化器



for epoch in range(num_epochs):
for images, labels in train_loader:
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播与优化
optimizer.zero_grad() # 清空梯度
loss.backward() # 计算梯度
optimizer.step() # 更新参数
optimizer.step() 会根据计算出的梯度自动更新模型中的所有参数。

4. 模型部署

模型部署的本质就是进行前向传播。以实时摄像头情绪识别为例:
- 使用人脸检测算法(如OpenCV的Haar级联或Dlib)从视频流中检测并截取人脸区域。
- 将人脸区域图像进行与训练时相同的预处理(如缩放、归一化)。
- 将处理后的图像输入到训练好的模型中,进行前向传播,得到情绪分类结果。
- 将结果可视化在视频画面上。



课程总结 🎯



本节课我们一起学习了深度学习的核心知识:
- 概念基础:理解了深度学习与机器学习的关系与区别,以及深度学习端到端和有向计算图的特点。
- 网络原理:学习了全连接网络和神经元的基础计算,认识到矩阵乘法是实现神经网络的基础。
- 训练过程:掌握了模型训练的核心——通过损失函数和梯度下降算法优化参数,并使用PyTorch框架简化了梯度计算和参数更新流程。
- 项目实战:通过情绪识别项目,串联了从数据加载、CNN模型构建、训练循环到模型部署的完整深度学习应用流程。



希望本教程能帮助你打下坚实的深度学习基础,并具备动手实现一个完整CV项目的能力。




注:教程中涉及的代码框架、数据集处理及模型部署细节,可根据提供的配套代码进行进一步实践。


人工智能—计算机视觉CV公开课(七月在线出品) - P22:凭什么,Transformer在CV领域这么火 🚀
在本节课中,我们将要学习Transformer模型为何能在计算机视觉领域取得巨大成功。我们将回顾其起源,探讨其核心思想,并重点分析Vision Transformer及其重要变体Swin Transformer的工作原理和优势。
课程概述 📖
Transformer模型最初在自然语言处理领域大放异彩,但如今已成功进军计算机视觉领域,并展现出强大的潜力。本节课将解析Transformer的核心机制,并探讨其如何克服传统视觉模型的局限,最终成为CV领域的重要基石。
Transformer的起源与核心思想
上一节我们概述了课程内容,本节中我们来看看Transformer的起源。
Transformer并非起源于计算机视觉领域。它大约在2020年左右在自然语言处理领域大放异彩。在Transformer之前,NLP领域主要依赖循环神经网络。
例如,在处理一段包含代词的长文本时,人类能轻松理解代词所指代的对象,即使它们相隔很远。但对于RNN模型来说,捕捉这种长距离依赖关系非常困难。
Transformer的出现解决了这个问题。其核心思想是注意力机制,它允许序列中的每个元素(如单词)关注序列中所有其他元素,从而建立全局依赖关系。
Transformer的基本结构遵循编码器-解码器范式。其核心公式是多头自注意力机制:
# 多头注意力机制的简化表示
Attention(Q, K, V) = softmax(Q * K^T / sqrt(d_k)) * V
其中,Q(查询)、K(键)、V(值)是由输入序列通过线性变换得到的矩阵。d_k是键向量的维度。
Transformer进军计算机视觉的挑战
上一节我们介绍了Transformer在NLP中的成功,本节中我们来看看它最初应用于CV时面临的挑战。
将Transformer直接应用于图像处理存在两大主要障碍:
- 输入形式不同:NLP的输入是离散的、具有高级语义的单词序列(Token)。而图像的输入是连续的、低级的像素点网格。
- 序列长度与计算量:NLP的序列长度相对较短(如128或256个词)。而图像的像素点数量极其庞大(如224x224=50176),直接对每个像素点应用自注意力机制会导致计算复杂度呈平方级增长,难以承受。
Vision Transformer:破局之道
面对上述挑战,研究者提出了Vision Transformer。
ViT的核心创新在于改变了输入形式。它不再将图像视为像素网格,而是将其分割成固定大小的图像块(Patch)。
以下是ViT处理图像的步骤:
- 图像分块:将输入图像分割成N个大小为PxP的Patch。
- 线性投影:将每个Patch展平为一个向量,并通过一个可学习的线性层映射到Transformer的嵌入维度。
- 添加位置编码:为每个Patch向量添加位置编码,以保留其在原始图像中的空间信息。
- 输入Transformer:将得到的Patch序列(加上一个特殊的[CLS]分类令牌)输入标准的Transformer编码器。
通过这种方式,图像被转化为一个序列,从而能够利用Transformer强大的全局建模能力。
Swin Transformer:更高效的视觉Transformer
上一节我们介绍了ViT如何将Transformer引入CV,本节中我们来看看一个更高效的变体——Swin Transformer。
Swin Transformer在2021年ICCV会议上获得了最佳论文奖。它旨在设计一个通用的视觉骨干网络,并解决ViT的两个问题:计算复杂度高以及缺乏像CNN那样的层次化特征表示。
Swin Transformer的核心创新是滑动窗口和层级设计。
以下是Swin Transformer的关键特性:
- 层级特征图:像CNN一样,Swin Transformer构建了层次化的特征图。随着网络加深,特征图尺寸减小,通道数增加,从而捕获不同尺度的信息。
- 基于窗口的自注意力:为了降低计算复杂度,Swin Transformer将自注意力计算限制在每个不重叠的局部窗口内。这大大减少了计算量。
- 移位窗口:为了在保持计算效率的同时实现跨窗口连接,Swin Transformer在连续的Transformer块中交替使用规则窗口划分和移位窗口划分。移位窗口机制允许前一层的非相邻窗口在下一层进行交互,从而实现了全局建模能力。
基于窗口的自注意力计算复杂度公式为:
Ω(MSA) = 4hwC² + 2(hw)²C
而基于滑动窗口的自注意力计算复杂度为:
Ω(W-MSA) = 4hwC² + 2M²hwC
其中,h和w是特征图大小,C是通道数,M是窗口大小。当M固定时(如7),复杂度从与hw的平方相关变为线性相关,显著提升了效率。
总结与展望
本节课中我们一起学习了Transformer在计算机视觉领域兴起的原因和关键技术。
我们从Transformer在NLP中的起源讲起,理解了其核心的注意力机制。随后,我们探讨了将其应用于图像所面临的输入形式和计算复杂度挑战。Vision Transformer通过将图像分块转化为序列,巧妙地解决了第一个挑战。而Swin Transformer则通过引入滑动窗口和层级结构,在保持强大建模能力的同时,显著降低了计算成本,使其成为更实用的视觉骨干网络。
Transformer的成功不仅体现在图像分类上,更在目标检测、分割等下游任务中展现出强大潜力,其设计思想正在重塑计算机视觉的研究范式。



本教程根据七月在线CV公开课P22内容整理,聚焦于技术原理讲解,已省略课程相关的推广与互动信息。


人工智能—计算机视觉CV公开课(七月在线出品) - P3:计算机视觉技术跨平台应用实践 🚀
在本节课中,我们将学习计算机视觉技术如何从理论走向实际产品,涵盖大规模后台服务构建、移动端模型部署以及AI在教育场景中的应用实践。我们将探讨从模型选择、系统设计到具体落地所面临的挑战与解决方案。


课程概述 📋
本次分享将围绕计算机视觉技术的跨平台应用展开。内容不专注于单一问题或方法,而是涉及多个方面,包括如何构建大规模后台云服务、如何在移动端开发和部署计算机视觉模型,以及如何将AI技术应用到教育场景中。课程将结合行业现状、讲者自身及同事的工作经验,讨论实际产品中遇到的问题和挑战。
一、 图像理解与卷积神经网络发展
对于互联网公司及许多行业,我们拥有海量的图片和视频数据。从这些数据出发,我们常常希望回答一些关于数据的问题。例如,一张图片是照片还是素描?包含人还是物体?物体看起来是什么样子?在做什么事情?是室内还是室外场景?是风景名胜吗?包含文本信息吗?文本是什么?同时,很多产品也关心图片是否含有不良内容,以便从互联网平台过滤掉。
这里会用到很多AI技术,特别是卷积神经网络,用于图片分类和物体检测。
卷积神经网络发展简史
大家应该对卷积神经网络的发展历史相当熟悉,这里可以快速总结一下。
- LeNet (1998): 第一个成功应用的卷积神经网络,由Yann LeCun开发。它是一个只有5层的简单网络,包含卷积层、下采样层和全连接层,输出是数字0到9的类别标签。该算法曾运行在ATM自动取款机上。
- AlexNet (2012): 第一个大规模卷积神经网络,赢得了2012年ImageNet图片识别比赛冠军,并大幅领先传统方法。它有5个卷积层和3个全连接层,使用ReLU做非线性激活。与之前算法相比,精度提高了20%以上。
- VGGNet (2014): 由牛津大学提出。与AlexNet相比,网络深度从8层提高到了16层或19层。它叠加了多个3x3的卷积层。将多个3x3卷积层叠加,其感受野等价于更大的卷积核(如3个3x3等价于1个7x7),同时网络更深,非线性运算更多,参数更少,更适合图片分类。
- GoogLeNet (2014): 赢得了当年ImageNet比赛冠军。它是一种更深的网络结构(22层),使用了称为Inception的模块。Inception模块包含多个分支(1x1, 3x3, 5x5卷积),通过并行连接输出,并使用1x1的瓶颈层降低维度。与AlexNet和VGG相比,参数量大幅减少(仅500万参数,比AlexNet小12倍),效果也有提升。
- ResNet (2015): 由何恺明提出。研究发现,简单地叠加更多层数只会使模型性能变差,主要原因是梯度消失。ResNet设计了残差网络结构,引入了跨层连接,使网络不再直接拟合输出Y,而是拟合残差Y-X。这使得网络更容易训练,深度可达150层甚至1000层。ResNet赢得了2015年ImageNet和COCO竞赛冠军,也是2016年CVPR最佳论文。
模型对比分析
我们可以将上述模型进行对比。在一幅图中,Y轴表示图像识别精度,X轴表示运行模型时的实际计算量(单位:10亿次加乘运算),圆圈面积表示模型大小(参数量)。

- AlexNet 位于左下角,参数量大,计算量小,精度相对较低。
- Inception 和 ResNet 位于图的上方,精度高,模型大小和运算量适中。
- VGG-16/19 位于图的最右侧,模型非常大,计算量巨大,精度优于AlexNet但不及Inception和ResNet。
ResNet的改进工作

ResNet之后有两个重要的改进工作,效果很好且易于使用。
- DenseNet (2017 CVPR最佳论文): 在层与层之间增加了更多连接。每一层的输入是前面所有层的输出,而不仅仅是前一层的输出。DenseNet采用模块化设计,只有模块内部的层才密集连接。这种设计使得信息流动更高效,在同样精度下,DenseNet可以使用更少的参数和计算量。
- ResNeXt: 采用分组卷积的方式。它将卷积操作分成多个组(例如将128个通道分为32个组),增加了神经元的数量(宽度)以提高模型精度,但由于分组,计算量与原始的ResNet模块保持一致。
上一节我们回顾了图像分类的主流模型发展,本节中我们来看看用于物体检测的常见方法。
二、 物体检测方法

物体检测与图片分类不同。对于图片分类,我们只需要给出一个类别标签(例如,这是一张猫的图片)。对于物体检测,除了要知道图片中有猫,还需要知道猫在图片中的具体位置,通常用一个矩形框(Bounding Box)圈出。一张图片中可能包含多个属于相同或不同类别的物体,都需要用不同的矩形框标出。

传统方法与挑战
传统的物体检测方法(如人脸检测)通常采用滑动窗口法。给定一张图片,我们选取一个窗口,提取特征并与模型比较。为了检测出图片中所有可能位置、大小的人脸,需要设置各种尺寸、长宽比的窗口在图片上滑动,对每个窗口提取特征并进行匹配。这种方法计算开销巨大。

当使用深度学习模型时,如果直接对每个窗口提取深度特征,计算量将无法承受。因此,后续很多检测方法的核心都是设计模型以减少检测所需的计算量。
以下是几种重要的物体检测方法:
- OverFeat (2013): 充分利用卷积特征,只运行一次模型即可实现与滑动窗口等价的效果。方法直观:首先将分类模型(如AlexNet)中的全连接层全部替换为卷积层,使网络可以接受任意尺寸的输入。最终输出是一个二维的特征图,该图上每个点对应原始输入的一个矩形框区域和一个类别预测向量。通过找到得分高于阈值的类别,即可得到对应的物体位置。这种方法受限于卷积的填充(Padding)和池化(Pooling)操作,精度会受到影响。
- R-CNN (Region-based CNN): 首先使用选择性搜索(Selective Search)等算法从图片中预选出约2000个可能包含物体的候选窗口(Region Proposal),大大减少了窗口数量。然后对每个候选窗口区域运行一个CNN模型进行分类。这比滑动窗口法计算量小,但仍需对每个候选区域单独运行CNN,计算量依然很大。
- Fast R-CNN: 针对R-CNN的改进。它先对整个输入图片运行一次CNN,提取完整的特征图。然后,将选择性搜索得到的候选区域映射到该特征图上,并在特征图上提取每个区域的特征(通过RoI Pooling层)。这样,基础的CNN特征提取只需计算一次,大大降低了计算量。
- Faster R-CNN: 由任少卿、何恺明、孙剑等人提出。它将选择候选窗口的算法和检测算法合并到一个模型中。模型有两个分支:一个分支是区域提议网络(RPN),从CNN特征图中预测出候选窗口;另一个分支则利用这些候选窗口从特征图中提取特征,并进行分类和边框回归。这种端到端的设计进一步提高了性能。
- SSD (Single Shot MultiBox Detector, 2016): 由刘伟博士提出。它完全放弃了预选候选窗口的步骤,是一个单一的网络。SSD允许从CNN网络的不同层输出中直接预测物体检测结果,底层特征用于预测小物体,上层特征用于预测大物体。其结构清晰简洁,应用广泛。
- YOLO系列: 与SSD相比,YOLO专门设计了网络结构来优化检测速度和精度,包括YOLO v2 (YOLO9000)、YOLO v3等。
了解了学术界和工业界采用的分类与检测方法后,我们来看看当我们将这些技术用于构建实际产品时会遇到哪些新问题。
三、 从模型到产品:实际问题与挑战
当我们确定了分类方法(如ResNet),并可以利用开源框架和公开数据集训练模型后,下一步想将其做成产品时,会遇到许多新问题。
以下是构建图像理解产品时可能遇到的一些关键问题:
- 数据与类别定义: 需要判定哪些类别?这些类别如何精确定义?例如,判断“苹果”时,完整的红苹果、绿苹果、切开的苹果、苹果块、苹果沙拉、结满苹果的树,是否都符合产品所需的“苹果”定义?这是实际工作中首先要花费大量时间解决的问题。
- 数据需求量: 训练模型达到所需精度需要多少数据?例如,训练“苹果”类别可能需要5000张图片,这是一个合理的数字吗?
- 数据搜集途径: 如何搜集训练和测试图片?搜集的数据必须尽可能接近线上产品实际面对的数据分布。如果只搜集素描苹果图片,模型对真实苹果照片的性能就会很差。
- 高质量标注: 如何获得高质量的标注结果?标注是体力劳动,存在疲劳和不认真的情况。如何确保标注数据的可靠性和精度?
- 数据不平衡: 如何处理不平衡数据?某些类别(如“苹果”)图片容易搜集,数量巨大;而稀有类别(如某种蜥蜴)图片很难找到。在训练中如何处理这种类别间数据量差异巨大的情况?
- 长尾类别: 如何处理出现频率极低、难以搜集数据的类别?
- 类别重叠: 如何处理两个类别有重叠的情况?例如,某些样本可能同时属于两个类别。此时,使用简单的Softmax(N选一)结构可能无法得到理想结果。
- 增量学习: 如果已经训练好一个包含1000个类别的模型,现在需要增加一个新类别,是应该从头开始重新训练,还是在已有模型基础上进行扩展?

面对这些挑战,一个稳健的后台服务架构至关重要。接下来,我们看看如何部署一个大规模的图像理解服务。
四、 大规模图像理解服务架构
这里主要介绍两种服务部署方案:同步服务和异步服务。
同步服务架构
这是一种实时服务模式。用户发送包含图片的请求到后台。后台将图片存入数据仓库,同时向图像理解服务集群发起一个同步RPC请求。负载均衡器从集群中(例如1000台服务器)选择一台机器,将请求分发过去。该机器运行训练好的CNN模型,将结果存入数据库并返回。
这种架构需要考虑的问题包括:
- 超时设置: 运行CNN模型耗时较长(CPU机器可能每秒仅处理几个请求),需要设置合理的超时时间。
- 负载均衡策略: 当单机处理能力有限时,简单的随机选择可能无法平衡负载。需要设计适合长处理任务的负载均衡策略。
- 跨区域部署: 如果服务器部署在不同区域或大洲,如何进行跨区域的负载均衡。
同步方案结构简单,但存在资源利用率问题。线上请求通常有高峰和低谷。如果按峰值配置服务器,在低谷时会有大量服务器闲置,导致利用率低下。
异步服务架构
为了解决资源利用率问题,可以采用异步方案。用户请求不再直接发给后台服务,而是先放入一个队列。后台服务器在处理完当前任务后,主动从队列中拉取下一个请求。可以设置多个优先级队列,优先处理高优先级任务。
这种方案的好处是无需按峰值配置资源,可以用更少的机器提供服务。但它不是实时模式,而是异步处理模式。
在实际场景中,通常需要同时满足同步和异步的需求,因此会采用混合部署模型。


介绍完服务架构,我们来看一个具体的例子:如何设计和搭建一个自助式的AI模型训练与部署平台。


五、 自助式AI平台实践:以Facebook的Lumos为例


这种自助式平台(在Facebook内部称为Lumos)每天处理数十亿张图片。它帮助产品团队描述图片内容、为视障人士提供语音描述、筛选值得纪念的老照片/视频、提升图片/视频搜索效果以及过滤不良内容。


平台的核心价值在于,当平台搭建完成后,算法团队无需为每个产品需求单独训练模型,产品团队或客户可以直接在平台上使用自己的数据训练所需模型。

平台工作原理
以图像识别为例,平台后端可能运行多种主流模型(如ResNet)。训练一个ResNet模型通常需要数小时甚至数天。如果每个应用场景都单独训练模型,请求会越来越多,算法团队将无法承受。
解决方案是:
- 使用海量数据(千万或亿级别)预先训练一个强大的主模型。
- 当有新训练任务时,不是从头训练,而是利用主模型提供的图像表征(特征)进行迁移学习。
用户可以在平台上选择从主模型的哪一层提取特征:
- 选择较前的层: 特征更偏向图像底层信息,后续需要添加较大的模型进行训练,训练相对不便,但针对特定场景可能获得更高精度。
- 选择最后的层(如倒数第二层): 特征已经是高维语义信息,后续可能只需加一个线性分类器。训练负担极轻(几分钟即可完成),但性能可能因未针对数据优化而受影响。
这本质上是精度与速度/便利性的权衡。
平台使用演示
以下是一个用户使用该平台训练“人骑马”分类模型的简化流程:
- 创建模型: 用户创建一个名为“人骑马”的新模型。
- 搜集数据: 从社交平台根据标签搜索相关图片。
- 数据聚类与标注: 系统对搜索到的图片进行聚类,将相似图片分组。用户只需对聚类中心进行标注(如“正样本:人骑马”、“负样本:只有马”),大大提升了标注效率。
- 提交训练: 用户选择特征层(例如最后一层),提交训练任务。由于模型简单,训练在一分钟内完成。
- 评估与迭代: 系统展示模型在测试集上的性能(如准确率、ROC曲线)。用户可以将模型部署到线上测试,将识别错误的样本加入训练集,重新训练以提升模型性能。
- 搜集负样本: 为了提升模型区分度,可以专门搜索“马”的标签,将这些图片作为负样本加入训练。
- 模型上线: 设定合适阈值后,将模型部署上线,与其他视觉模型一起处理海量图片。

之前我们介绍了后台云服务,接下来我们看看如何将视觉模型部署到资源受限的移动端。

六、 移动端计算机视觉模型部署

在手机上运行神经网络模型与在GPU服务器上有很大不同,受限于CPU、内存和功耗。近年来大部分深度学习研究集中在提高模型精度,这些模型并非为手机设计,无法直接移植。

例如,ResNet-50有50层卷积和全连接层,对一张图片需要进行数十亿次运算,其大小和计算量都无法在手机上运行。因此,我们需要专门为移动端设计小而高效的网络结构,在速度和精度间取得平衡。

高效的移动端网络结构

以下是几种专为移动端设计的高效模型:
- SqueezeNet (2016): 包含称为“Fire Module”的结构,由1x1和3x3卷积组成。模型大小仅为AlexNet的1/50,但能达到相近的精度。
- MobileNet (Google): 使用深度可分离卷积,将标准卷积分解为:
- 逐通道卷积: 对每个输入通道单独进行卷积。
- 逐点卷积: 使用1x1卷积核融合各通道信息。
这种方法能极大减少计算量和模型大小。标准卷积计算量涉及6个维度参数,逐通道卷积减少到5个,逐点卷积减少到4个。将二者结合,计算量远小于标准卷积。
- ShuffleNet (旷视科技): 使用分组卷积,并通过“通道重排”操作来促进组间信息交流,防止分组导致的信息隔离,从而在保持小模型的同时获得良好效果。
后续还有MobileNet V2, ShuffleNet V2等一系列改进工作。



模型压缩技术





除了设计小模型,还可以对已有大模型进行压缩。

- 网络剪枝: 移除网络中不重要的连接。例如,首先训练一个完整网络,然后将接近于零的权重剪掉,再对剪枝后的网络进行微调,通过迭代得到既保持精度又大幅减少参数量的模型。
- 量化: 将网络权重从32位浮点数转换为8位整数。虽然会损失一定精度,但通过聚类、码本等技术,许多模型在8位量化后仍能保持近似原始精度。量化能将模型大小压缩至原来的1/4。
- 结合使用: 可以同时使用剪枝和量化,在实验中可将模型压缩至原来的1/8且精度不变。
移动端推理框架与实践
在实际产品中,可以使用专为移动端优化的框架,如Caffe2。Caffe2的安装包小于1MB,非常轻量。在手机端,模型可以选择在CPU、GPU或DSP上运行。
- CPU: 可使用NNPACK等库加速。
- GPU (iOS): 使用Apple的Metal Performance Shaders。
- GPU/DSP (Android, 高通芯片): 使用高通提供的Snapdragon Neural Processing Engine (SNPE)。
使用DSP进行推理能耗非常低,适合长时间运行神经网络模型(如监控、持续预测)。
移动端应用实例
以下是两个在手机上实时运行的计算机视觉应用例子:
- 实时风格迁移: 基于Justin Johnson等人2016年的方法(Image Transform Net),通过模型和底层工程优化,在手机上对720P图片实现了接近实时的艺术风格转换。
- 实时人体姿态估计: 检测人体关键点(如眼睛、肩膀、手肘、手腕)来描述姿态。采用基于Mask R-CNN(何恺明,2017)的网络结构,它是一个通用框架,可同时完成物体检测、特征提取和分割。通过SNPE优化,在三星Galaxy S7等设备上实现了实时姿态估计。


最后,我们将视角转向一个特定的垂直领域,看看计算机视觉技术如何与教育场景深度融合。
七、 AI在教育行业的应用实践

教育行业的核心痛点在于优质教育资源稀缺。AI技术可以融入传统教育场景,提升教学效率和效果。
AI技术在教育中的应用方向

- 计算机视觉: 人脸分析、人体姿态与动作分析、手势识别、OCR自动判题/阅卷。
- 语音技术: 语音识别、语音合成、基于对话的语音评测(尤其用于非母语学生的语言练习)。
- 自然语言处理: 语义理解、作文自动评分。
- 数据挖掘: 个性化学习路径推荐、自适应练习设计。
智慧课堂:重新定义课堂40分钟


目标是将AI技术融入课堂互动,为每个学生提供个性化的关注和互动,并提供




人工智能—计算机视觉CV公开课(七月在线出品) - P4:计算机视觉漫谈 🧠


在本节课中,我们将要学习计算机视觉领域的三个核心话题:目标检测、关键点检测以及Kaggle竞赛。我们将从基本概念入手,逐步深入到核心思想、算法流派和实际应用,帮助初学者建立对计算机视觉领域的宏观认识。
目标检测 🔍
上一节我们介绍了课程概述,本节中我们来看看第一个核心话题:目标检测。
目标检测,英文名为Object Detection。在深入探讨目标检测之前,我们通常会从目标识别开始。这是因为目标识别所使用的卷积神经网络(CNN)是目标检测的基础网络。目标识别的目的是从图像中提取大量特征,并解决一个分类问题。例如,给定一张图像,经过卷积神经网络处理后,它会告诉我们图像中有什么,比如是猫、狗还是小孩。
对于目标检测来说,我们除了想知道图像中有什么(分类问题),还想知道目标在哪里(定位问题)。例如,一张图像中有一只狗和一个小孩。目标识别只会输出“狗”和“小孩”,而目标检测则会用矩形框将狗和小孩圈出,并给出对应的标签。







目标检测的基本过程







以下是目标检测的一个基本过程描述:

- 输入与骨干网络:输入一张图像,经过一系列卷积层、池化层和全连接层(即骨干网络)进行特征提取。
- 输出定义:对于目标检测,网络的输出Y通常包含多个值。以检测“狗”为例:
Pc:一个值,表示当前区域是否有我们感兴趣的目标(即是否为背景)。Pc=1表示有目标,Pc=0表示是背景。bx, by, bw, bh:四个值,用于定义边界框(Bounding Box)。(bx, by)是矩形框中心点的坐标,bw和bh分别是框的宽度和高度。C1, C2, C3...:多个值,表示目标属于哪个类别(如狗、猫、人等)的概率。
- 损失函数:深度学习的核心是找到网络参数,使网络的预测输出
Y无限接近真实标注值Y_hat(Ground Truth)。这通过最小化损失函数L来实现。损失函数L可以定义为预测值与真实值之差的平方和。具体而言:- 如果
Pc=0(背景),则只计算Pc的预测误差。 - 如果
Pc=1(有目标),则需要计算Pc、边界框参数(bx, by, bw, bh)以及类别概率(C1, C2, C3...)所有项的预测误差。
- 如果


交并比(IoU)与非极大值抑制(NMS)

在实际检测中,我们可能会用滑动窗口生成大量候选框,其中很多框都会覆盖到目标。为了选出最准确的框,我们引入交并比(Intersection over Union, IoU)的概念。



公式:
IoU = (预测框 ∩ 真实框) / (预测框 ∪ 真实框)

IoU值衡量了预测框与真实框的重叠程度,值越大表示两者越接近。在得到多个高IoU值的候选框后,我们使用非极大值抑制(Non-Maximum Suppression, NMS)算法来剔除冗余框,只保留最有可能的一个。
目标检测的应用
目标检测技术已广泛应用于各个领域:
- 车牌识别:自动定位并识别车辆牌照上的字符。
- 无人驾驶:检测车辆、行人、交通标志等,用于路径规划和避障。
- 图像搜索:识别照片中的主体建筑或物体,并进行网络搜索。
- 工业自动化:如配送车导航、机械臂抓取定位等。
- 医疗影像:辅助医生定位疑似病灶区域。
自2013年R-CNN提出以来,目标检测技术取得了长足发展。从早期的R-CNN到最新的YOLOv4,其性能和速度都有了天壤之别的提升。
目标检测的算法流派 🧬

上一节我们了解了目标检测的基本概念和应用,本节中我们来看看其背后的主要算法流派。

目前主流的目标检测算法可以根据是否使用“锚框”(Anchor Box)分为两大流派:锚框派(Anchor-based)和无锚框派(Anchor-free)。




锚框派(Anchor-based)

锚框派是目前比较主流的分支,它又可以根据检测流程分为两类:

- 两阶段(Two-stage)检测器:如R-CNN系列(Fast R-CNN, Faster R-CNN, Mask R-CNN)。其流程分为两步:
- 首先使用骨干网络提取图像特征。
- 然后,第一阶段(如RPN,Region Proposal Network)生成一系列可能包含目标的“候选区域”(Proposals)。第二阶段对这些候选区域进行精细的分类和边界框回归。
- 单阶段(One-stage)检测器:如SSD、YOLO系列。其思想是在特征图上直接进行密集抽样和预测,一步完成分类和回归,因此速度通常更快。
锚框(Anchor) 是预先定义好的一组具有不同尺度和长宽比的矩形框。它们被放置在特征图的每个像素点上,作为检测的参考基准。网络的任务是判断每个锚框内是否有目标,并对锚框的位置和大小进行微调(回归),使其更匹配真实目标框。
以Mask R-CNN为例,它是一个强大的两阶段检测器,不仅可以进行目标检测,还能完成实例分割和关键点检测。它在Faster R-CNN的基础上增加了一个掩码(Mask)分支,用于预测目标的精确像素级轮廓。

无锚框派(Anchor-free)



无锚框派最早出现在2016年的CornerNet中。其核心思想是不依赖预定义的锚框,而是通过直接预测目标的一些关键点来构成边界框。



- CornerNet:通过检测目标的左上角和右下角两个角点来形成边界框。网络会为每个角点预测一个热力图(Heatmap,表示该点是角点的概率)和一个嵌入向量(Embedding,用于匹配属于同一目标的角点对)。
- CenterNet:在CornerNet的基础上,额外预测目标的中心点。通过判断中心点是否落在由角点构成的边界框中心区域内,来过滤掉错误的检测框,提高了准确性。



无锚框方法简化了检测流程,避免了对锚框超参数(如数量、尺度和比例)的依赖,在某些场景下表现更优。



关键点检测 👤


上一节我们探讨了目标检测的算法,本节中我们来看看一个更细粒度的视觉任务:关键点检测。
关键点检测是目标检测的延伸,其目的不仅是找到目标,还要精确定位目标上的一些具有特定意义的点。它可以分为四大类:

- 人体骨骼点检测(Human Pose Estimation)
- 人脸特征点检测(Facial Landmark Detection)
- 手部关节点检测(Hand Pose Estimation)
- 物体关键点检测(Object Keypoint Detection)

人体骨骼点检测

这是关键点检测中最早也是最大的分支。例如,COCO数据集定义了人体的17个骨骼点(如脚踝、膝盖、手腕、肘部、肩膀、眼睛、耳朵等)。通过检测这些点的位置和连接关系,可以分析人的姿态和行为。

人体骨骼点检测主要有两种思路:

- 自顶向下(Top-down):先使用目标检测器找出图像中所有的人,然后在每个检测到的人体区域内部进行关键点检测。例如Mask R-CNN就可以扩展用于关键点检测,在其掩码分支的基础上,将输出改为关键点的热力图。
- 自底向上(Bottom-up):先检测出图像中所有的关键点,然后再通过聚类或匹配算法将这些点分组,关联到不同的个体上。经典的OpenPose算法就采用这种思路,它同时预测关键点的热力图和点与点之间的关联度(Part Affinity Fields),最后进行组合。


关键点检测的应用
关键点检测技术有非常广泛的应用:






- 无人超市:通过追踪顾客手部、身体的关键点,判断其拿取、放回商品的动作,实现自动结算。
- 体育分析:如NBA的Home Court应用,通过手机摄像头分析球员的投篮姿势、动作次数等。
- 人机交互与VR:捕捉人体动作,实现更自然的交互体验。
- 行为识别:基于姿态序列分析人的行为。
- 人脸比对与特效:人脸关键点检测可用于人脸识别、解锁、美颜、虚拟贴纸等。








Kaggle竞赛 🏆

上一节我们学习了关键点检测,本节中我们来看看如何通过实践平台提升技能——Kaggle竞赛。


Kaggle是一个全球知名的数据科学和机器学习竞赛平台。公司或组织可以在Kaggle上发布问题、提供数据集,全世界的数据科学家和爱好者可以参与竞赛,提交解决方案模型,争夺排名和奖金。


为什么要参加Kaggle?
- 丰厚的奖金:许多竞赛,特别是“Featured”类别的商业竞赛,提供高额美元奖金。
- 提升知识与技能:在解决实际问题的过程中,你会深入理解模型、调参技巧和性能优化方法。
- 积累项目经验与声誉:在Kaggle上取得好名次(即使是前10%)是简历上极具说服力的亮点,能极大提升求职竞争力。
- 社区与交流:Kaggle拥有活跃的社区,你可以学习顶级选手的解决方案(Kernels),与全球高手交流。


竞赛类型

Kaggle竞赛主要有以下几种类型:

- Featured:最常见的类型,由企业发布的以商业目标为导向的竞赛,奖金丰厚。
- Research:以研究探索为目的的竞赛,通常没有奖金,旨在解决前沿学术或实际问题。
- Getting Started (Playground):为初学者设计的入门级竞赛,问题相对经典简单,适合练手。
- Recruitment:企业为招聘人才而设立的竞赛。
- Limited Participation:邀请制的大师赛,仅限受邀请者参加。


如何开始?

对于初学者,建议从“Getting Started”或“Playground”竞赛入手,熟悉Kaggle的比赛流程、数据格式和提交方式。专注于计算机视觉相关的比赛题目,积累经验。如果自学遇到困难,可以寻求有经验的导师或课程指导,系统学习竞赛技巧和套路。
总结 📚
本节课我们一起学习了计算机视觉领域的三个重要方向。
我们首先深入探讨了目标检测,理解了其如何同时解决分类(是什么)和定位(在哪里)的问题,介绍了其核心概念如边界框、损失函数、IoU以及锚框派与无锚框派两大算法流派。
接着,我们学习了关键点检测,认识到这是对目标检测的细化,用于精确定位目标上的特征点,并了解了其在人体姿态估计、人脸识别、无人超市等领域的广泛应用。


最后,我们介绍了Kaggle竞赛这个宝贵的实践平台,明白了参与竞赛对于提升技能、积累项目经验和增强求职竞争力的重要意义。

希望本教程能帮助你建立起对计算机视觉这些核心话题的基本认识。如果你想更深入地学习理论并动手实践,可以关注相关的专业课程,通过项目实战来巩固和提升你的能力。



🎯 人工智能—计算机视觉CV公开课(七月在线出品) - P5:目标检测这些年的那些事

在本节课中,我们将要学习目标检测技术在过去几年的发展历程。我们将从骨架网络的演变开始,了解目标检测的基础。接着,我们会探讨目标检测的三大主流分支及其经典框架。最后,我们会分析近期大热的YOLOv4,了解其成功的原因。
🦴 第一部分:骨架网络的演变
目标检测的演变,很大程度上依赖于其骨架网络的进步。骨架网络通常指用于提取图像特征的卷积神经网络。
上一节我们介绍了课程概述,本节中我们来看看目标检测的基石——卷积神经网络是如何演进的。
卷积神经网络与目标识别
卷积神经网络主要解决目标识别问题,即图像分类问题。例如,区分台式机、笔记本电脑和平板电脑都属于电脑大类,但需要根据大小、形状等特征细分为不同小类。
计算机通过寻找目标的特征来进行识别。常见的特征包括:
- 颜色特征:例如用RGB(红、绿、蓝)通道表示图像,每个通道取值范围为0-255或0-1。
- 形状特征:例如图像中的线条、角点等结构。
- 纹理特征:例如图像中重复的图案,如六边形网格。
CNN之所以适用于图像识别,是因为它能在图像的不同区域发现相同的特征。例如,识别不同鸟类时,都可以用专门的神经元来检测“鸟嘴”这一特征,并且这些神经元可以共享参数。
从AlexNet到ResNet:深度与梯度消失
随着网络层数加深,模型识别能力似乎会增强。从AlexNet(8层)到VGG(19层),再到GoogLeNet(22层),模型的错误率在不断下降。


然而,无限制地增加网络深度会导致梯度消失问题,即在反向传播过程中,梯度变得极小,使得网络无法有效学习。


ResNet(残差网络)通过引入残差连接 解决了这个问题。其核心思想是在进行非线性变换前,将输入直接加到输出上。公式可以表示为:
输出 = F(x) + x
其中,x是输入,F(x)是网络学习到的残差映射。这种方式确保了梯度能够有效传播,即使网络很深(如152层),也能有效训练。
DenseNet与CSPNet:更高效的特征传递
在ResNet之后,DenseNet提出了更密集的连接方式。它不仅将前一层的输出引到后一层,还将前面所有层的特征图都连接到后面每一层的输入。这通过拼接(Concatenation) 操作实现,而非像素相加。
DenseNet能更有效地利用特征,并在一定程度上减少参数量。
CSPNet则是一种网络设计思想,而非单一网络。它针对如DenseNet等网络进行改进。其核心思想是将输入特征图分割(Partition) 为两部分:一部分直接传递到网络末端,另一部分进行密集的卷积计算。最后再将两部分特征融合。
CSPNet这样做的好处是增加了梯度路径的多样性,同时减少了计算量。
🔍 第二部分:目标检测的主流分支

在了解了骨架网络的发展后,我们来看看目标检测本身是如何演进的。目标检测不仅要识别图像中的物体是什么(分类),还要找出它们的具体位置(定位)。
目标检测的核心思想
目标检测的输出通常是一个包含多个信息的向量。例如,对于一张图,我们不仅要知道有没有“狗”,还要用一个边界框(Bounding Box) 框出狗的位置,并用坐标(Bx, By, Bw, Bh)表示这个框的中心点和宽高。

整个过程的优化目标是最小化损失函数(Loss Function),即让网络预测的边界框和类别无限接近人工标注的真实值。
评估预测框好坏的一个关键指标是交并比(IoU),其公式为:
IoU = 交集面积 / 并集面积
IoU值越高,说明预测框与真实框重叠越好。我们通常设置一个阈值(如0.5),只保留IoU高于阈值的预测框,并采用非极大值抑制(NMS) 等方法筛选出最终的最佳预测框。
应用领域


目标检测技术已广泛应用于多个领域:
- 车牌识别:成熟的传统CV与深度学习结合的应用。
- 自动驾驶:检测车辆、行人、交通标志等。
- 安防监控:行人检测、人流统计、逃犯追踪。
- 图像搜索:通过拍照识别建筑物或地标。
- 工业自动化:物流分拣机器人、机械臂抓取等。
- 医疗影像:在CT等影像中定位病灶区域。
三大算法流派
目标检测算法主要分为三大流派:
- 两阶段检测器(Two-Stage):如Faster R-CNN。先由区域提议网络(RPN)生成可能存在目标的候选区域(Region Proposals),再对这些区域进行分类和边框回归。精度高,速度相对慢。
- 一阶段检测器(One-Stage):如YOLO、SSD。将目标检测视为回归问题,直接在图像上预测边界框和类别。速度快,精度通常略低于两阶段方法。
- Anchor-Free检测器:如CornerNet、CenterNet。摒弃了预设锚框(Anchor)的设计,通过预测目标的关键点(如角点、中心点)来构成边界框。是近年来的研究热点。
两阶段检测器代表:Mask R-CNN
Mask R-CNN是在Faster R-CNN基础上的扩展,增加了一个掩码分支(Mask Branch),用于进行实例分割——不仅框出物体,还能精确勾勒出物体的像素级轮廓。
其骨干网络常采用特征金字塔网络(FPN),通过融合深层的高语义特征和浅层的高分辨率特征,来同时检测大目标和小目标。
在RPN阶段,会使用预定义的锚框(Anchor) 来在图像上滑动,初步判断每个位置是否有目标(二分类)以及初步的边框位置(回归)。这些锚框的大小和长宽比通常通过对训练数据集中所有真实框进行K-Means聚类 来确定。


一阶段检测器代表:SSD
SSD的核心思想非常直接:特征提取 + 多尺度检测。它直接在VGG等骨干网络提取的不同层特征图上,使用小卷积核进行密集预测。
例如,在某个38x38的特征图上,每个单元格(Cell)会基于多个预定义锚框预测21个类别的分数(20个目标类+1个背景类)和4个边框坐标偏移量。SSD在多个不同尺度的特征图上进行预测,浅层特征图负责检测小物体,深层特征图负责检测大物体。

Anchor-Free检测器代表:CornerNet 与 CenterNet



CornerNet 放弃了锚框,改为预测目标框的左上角和右下角两个关键点。对于每个角点,网络不仅预测其位置的热力图(Heatmap),还预测一个嵌入向量(Embedding)。通过计算两个角点嵌入向量的相似度,将属于同一个目标的角点配对,从而形成边界框。
CenterNet 则进一步提出,只预测角点可能无法感知目标内部信息。因此,它在预测角点的同时,额外预测一个中心点。如果预测的边界框的中心点也落在目标区域内,则认为该预测更可靠。CenterNet将CornerNet的单阶段结构改进为了两阶段结构。
⚡ 第三部分:YOLOv4 为什么这么“狂”?
最后,我们来聊聊现象级的YOLOv4。在学术界,YOLOv4被广泛认可为YOLO系列的当前最新里程碑版本。
YOLOv4的贡献更像是一份详尽的“调参手册”和“技巧宝典”。其论文系统性地总结了大量能提升CNN性能的“免费赠品(Bag of Freebies)”和“特别技巧(Bag of Specials)”。
- Bag of Freebies:指那些仅增加训练成本而不影响推理速度的方法,如数据增强(CutMix, Mosaic)、正则化(DropBlock)、损失函数设计(CIoU Loss)等。
- Bag of Specials:指那些轻微增加推理成本但能显著提升精度的方法,如特殊的注意力模块、激活函数(Mish)、后处理方法(DIoU-NMS)等。
YOLOv4的核心结构由三部分组成:
- 骨干网络(Backbone):采用CSPDarknet53,结合了CSPNet的思想和Darknet架构。
- 颈部网络(Neck):采用SPP(空间金字塔池化)和PANet(路径聚合网络),用于增强特征融合。
- 检测头(Head):沿用YOLOv3的检测头。

YOLOv4并非在算法结构上有颠覆性创新,而是通过精妙的工程集成与实验验证,将当时各种最有效的技巧融合到一个框架中,从而在速度和精度上达到了极佳的平衡,落地应用能力非常强。


📚 总结
本节课中我们一起学习了目标检测技术的发展脉络。
我们首先回顾了作为目标检测基础的骨架网络,从AlexNet、VGG到ResNet、DenseNet和CSPNet,看到了网络如何通过结构创新解决深度带来的梯度消失问题,并实现更高效的特征传递。

接着,我们深入探讨了目标检测的三大主流分支:追求高精度的两阶段检测器(如Mask R-CNN),追求高效率的一阶段检测器(如SSD),以及摒弃锚框、思路新颖的Anchor-Free检测器(如CornerNet, CenterNet)。我们了解了它们各自的核心思想、工作流程和代表性框架。
最后,我们分析了YOLOv4成功的原因。它通过系统性地集成大量经过验证的训练技巧和网络模块,在工程实践上达到了新的高度,成为目标检测领域一个非常实用和强大的工具。


希望本教程能帮助你勾勒出目标检测领域的演进画卷,并为你的进一步学习打下基础。



人工智能—计算机视觉CV公开课(七月在线出品) - P6:人体姿态估计的前世今生 👤➡️🦴



在本节课中,我们将要学习人体姿态估计这一计算机视觉基础且应用广泛的任务。我们将从问题定义出发,了解其核心概念、发展历程、常用数据集与评估方法,并探讨其在实际场景中的应用与挑战。
1. 人体姿态估计的问题定义
人体姿态估计是计算机视觉中的一个基础问题,其核心任务是从图像或视频中识别并定位出人体的关键骨骼点。
从名称中的“估计”一词可以看出,它与分类或检测任务不同,其目标是预测关键点的具体位置。例如,从右侧的骨骼图可以看出,定义某些关键点(如运动员的右肩)本身就可能存在模糊性或难度。
因此,精细的人体姿态估计是一项具有挑战性的任务。
根据数据的不同,人体姿态估计可以划分为以下几类:
- 单人姿态估计:图像中仅包含一个人。
- 多人姿态估计:图像中包含多个人。
- 人体姿态追踪:在视频序列中追踪人体的姿态。
- 3D人体姿态估计:预测人体关键点在三维空间中的坐标。


现实生活中,2D的单人或多人姿态估计更为常见。该技术已广泛应用于各类场景,例如抖音等应用中的舞蹈特效和动作打分。
姿态估计不仅限于2D图片,还可以扩展到视频帧的姿态追踪以及3D姿态预测。3D姿态估计在游戏建模、AR/VR等领域有重要应用。因此,人体姿态估计在计算机视觉、工业界和游戏领域都有着广泛的应用前景。
然而,人体姿态估计并非一个简单的问题,它面临诸多挑战:
- 关键点重叠:多个人体之间或人体自身各部分(如交叉的腿、重叠的肩膀)的关键点可能相互遮挡。
- 视角多变:人体姿态是三维空间的运动在二维平面的投影,拍摄角度(仰视、俯视、侧面)的多样性增加了识别难度。
- 外观干扰:衣物颜色可能与背景相似,影响模型对边界的判断。
- 部分可见:人体可能被遮挡,只露出一部分肢体,导致关键点检测不全。
- 环境影响:光照条件、拍摄角度等物理因素会影响图像信号,进而影响模型性能。
现实中的姿态估计通常处理较为规整的正面视角,但实际应用场景要复杂得多。

2. 常见数据集与评测方法

人体姿态估计的发展高度依赖于大规模、高质量的数据集。数据量的多少直接影响模型的精度。例如,COCO、MPII等大型数据集对推动该领域发展起到了关键作用。
同时,业界也有一系列标准化的评测指标来衡量模型性能:
- PCK:正确估计的关键点比例。它是一个较早的指标,计算预测关键点与真实关键点之间的欧氏距离,并通过设定阈值来判断是否正确。虽然较老,但在工程项目中作为衡量标准仍很方便。
- AP (Average Precision):平均精度,是当前主流的评测指标,尤其在学术论文中。其计算方式因任务而异:
- 单人姿态估计:针对单张图片中的单个人体,计算预测关键点与真实关键点之间的相似度(常用OKS指标),然后计算AP。
- 多人姿态估计:需要先检测出多个人体,再为每个人体计算AP,最后对所有人体或所有图片的AP进行平均,得到mAP。
目前,人体姿态估计领域常用的一些数据集包括:
- MPII:主流的单人人体关键点检测数据集,包含约16个关键点,样本数近2.5万。该数据集目前已被广泛研究,精度很高,适合初学者入门。
- COCO:主流的多人人体关键点检测数据集,包含17个关键点,样本量超过30万。它是评测多人姿态估计性能的重要基准。
- AI Challenger:由创新工场在2017-2018年举办的比赛数据集,样本量约38万,包含14个关键点,曾是一个经典的大规模数据集。
- Human3.6M:大型3D人体姿态估计数据集,包含360万个姿势和对应的视频帧,由11位演员从4个视角拍摄15天得到,数据量达数十GB。
3. 姿态估计模型的发展脉络
人体姿态估计模型的发展与深度学习的演进密切相关。在深度学习兴起之前,主要处理单人姿态估计,采用手工特征(如SIFT、HOG)结合浅层模型以及关键点间的空间关系进行建模。
深度学习,特别是卷积神经网络(CNN),极大地推动了姿态估计的发展。自2014年起,基于CNN的姿态估计方法开始涌现。MPII数据集的引入为模型训练提供了充足的数据支持。
其发展脉络大致可分为几个阶段:
- 2014-2015年:主流方法是对关键点坐标进行直接回归。
- 2015-2017年:方法演变为热力图回归与坐标回归相结合。
- 2018年至今:研究重点转向多人姿态估计。
坐标点回归的代表工作是 DeepPose。它是首个使用CNN进行姿态估计的方法,将关键点的坐标位置作为网络直接回归的目标,即让网络输出具体的坐标值。
热力图回归的代表模型是 CPM。它不再直接回归坐标,而是预测一个表示关键点可能出现位置的“热力图”。热力图上的每个像素值表示该位置是关键点的概率。这种方法类似于分类任务中的软标签,预测的是一个分布,通常能使模型收敛更快。CPM 通过多阶段网络结构,在不同尺度上预测热力图,融合后得到最终的关键点位置,其精度优于直接回归坐标的方法。
后续的许多工作(如结合热力图与偏移量预测)都是在此基础上的改进,以处理人体尺度变化、关键点间关系等复杂情况。
当前,姿态估计模型主要分为两大技术路线:
- 单人姿态估计:如
CPM及其后续著名的OpenPose。 - 多人姿态估计:又可细分为:
- 自上而下:先检测图像中所有的人体边界框,再在每个框内进行单人姿态估计。
- 自下而上:先检测图像中所有的关键点,再将关键点聚类、组合成不同的人体实例。
目前,自上而下的方法更为常见。例如2018年的 RMPE 等论文,借鉴了目标检测的思想,通过人体检测框来辅助姿态估计,取得了很好的效果。后续还有 CPN、HRNet 等更先进的模型不断刷新性能纪录。


4. 姿态估计的应用与落地
人体姿态估计是计算机视觉中非常容易落地的领域之一,其应用场景极为广泛:
- 动作识别与评分:分析运动员、舞者的姿态,进行动作打分或规范性评估。
- 安防监控:检测危险行为,如打架、摔倒、闯入禁区等。
- 游戏与交互:驱动VR/AR虚拟形象,实现体感游戏、舞蹈机联动等。
- 时尚与电商:检测服饰的关键点(衣领、袖口、裙摆等),用于服装检索、虚拟试衣、时尚分析。这不仅限于人体,也可用于宠物、特定物体等任何有关键点定义的对象。
姿态估计的落地仍需解决一些实际问题:
- 行为细粒度判断:例如,如何区分“握手”和“打架”?这需要结合时序信息分析关键点的运动模式。
- 数据扩增:对于姿态估计任务,简单的图像翻转、平移可能导致关键点标注失效或产生歧义,需要设计针对性的扩增策略。
- 与相关任务对比:与人脸关键点检测相比,人体姿态估计的关键点分布更稀疏、空间关系更复杂,且常面临严重遮挡,因此难度通常更大。
- 任务结合:姿态估计常与行人重识别、行为识别等任务结合,形成更复杂的应用系统。
5. 总结与课程介绍
本节课我们一起学习了人体姿态估计的完整图景。我们从问题定义入手,理解了其核心任务与挑战。接着,介绍了支撑该领域发展的常见数据集和评测指标。然后,梳理了模型发展的主要脉络,从坐标回归到热力图回归,从单人估计到多人估计。最后,探讨了姿态估计在多个行业的广泛应用和落地思考。
人体姿态估计是一个基础、重要且充满活力的研究方向,掌握它不仅有助于理解计算机视觉的核心技术,也能为解决众多实际问题提供有力工具。
(以下为课程推广内容)
如果您希望更深入地学习人体姿态估计及计算机视觉的其他核心技术与项目实战,可以关注我们的《计算机视觉就业班》。该课程不仅包含《人体关键点提取》等项目实战,还系统覆盖图像处理、传统视觉、深度学习、目标检测、语义分割、三维重建等核心知识点与6大实战项目,旨在帮助学员构建扎实的CV知识体系并积累项目经验,助力求职就业。
课程将于近期开班,现在报名可享专属优惠。感兴趣的同学可以点击页面上的“立即报名”咨询详情。今天的课件资料也会分享在直播群中,欢迎领取。



本节课到此结束,谢谢大家!





人工智能—计算机视觉CV公开课(七月在线出品) - P7:深度计算机视觉简介



在本节课中,我们将要学习深度计算机视觉的基础知识。课程将围绕四个核心话题展开:什么是深度学习、目标识别与检测、人体关键点检测以及图像对抗生成。我们将以简单直白的方式,帮助初学者理解这些复杂概念。


🧠 什么是深度学习?
本节中,我们将探讨深度学习的定义、其与人工智能和机器学习的关系,并介绍其核心的三步流程。
深度学习是人工智能的一个分支。在人工智能出现之前,传统的软件或算法是基于一套预设的规则来运行的。人工智能则是从海量数据中归纳知识,它只关注现象,不关心内在原因,其基础是统计学。


机器学习是人工智能的一个子领域。它的基本思路是:首先将现实问题抽象为数学问题,然后让机器去学习并解决这个数学问题,最后将解决方案反馈回现实世界。
深度学习是机器学习的一个分支。在传统机器学习中,特征主要依赖人工提取。而在深度学习中,特征由机器自动提取。深度学习模型就像一个黑箱,输入数据(如图片、声音),经过模型内部复杂的自主学习过程,最终得到输出结果(如图片内容、声音含义)。
以下是人工智能、机器学习和深度学习的关系总结:
- 人工智能:让机器模仿人类的特征或行为。
- 机器学习:让机器自主地学习人类的方法。
- 深度学习:让机器自主思考和琢磨,是机器学习的一个分支。
深度学习有其优点和缺点。






以下是深度学习的优点:
- 学习能力强:无需人工干预特征提取。
- 覆盖范围广,适应性好:应用领域广泛,对同类新数据适应性强。
- 数据驱动,上限高:性能随着数据量的增加而提升。
- 可移植性好:通过迁移学习,可以将一个模型学到的知识应用到相关任务中。
以下是深度学习的缺点:
- 计算量大,硬件需求高:通常需要高性能显卡进行训练。
- 模型设计复杂:随着任务变难,模型结构越来越复杂。
- 调参困难,容易“跑偏”:不同的参数会导致结果差异很大,需要经验和技巧。
深度学习的发展经历了起伏。其概念在上世纪50年代就已出现,但在1966年至1997年间经历了“AI寒冬”,主要受限于当时的计算硬件能力。随着芯片和计算机性能的飞速发展,深度学习才重新崛起并成为全民性话题。


关于人工智能是否会取代人类工作,通常具有以下特征的工作容易被取代:
- 工作所需的决策信息量很小。
- 决策过程逻辑简单。
- 能够独立完成,无需合作。
- 重复性高。
而需要强社交能力、创造力或复杂感知操作能力的工作则很难被取代。
现在,我们来回答“如何做深度学习”这个问题。深度学习可以看作是在寻找一个函数。例如,语音识别是寻找一个将音频转换为文本的函数;图像识别是寻找一个将图片转换为其内容描述的函数。
我们可以把这个函数集合视为一个黑箱。黑箱里包含许多不同的函数。我们通过训练数据(即人为标记好正确答案的数据)来评估这些函数的好坏,并从中选择出最佳的函数。
因此,深度学习可以总结为简单的三步曲:
- 定义一个黑箱,里面包含许多可能的函数。
- 让机器自主判断这些函数结果的好坏。
- 找到一个最优解,即输入能通过该函数得到我们想要的输出。
简而言之,就像“把大象关进冰箱”一样,深度学习也就三步:定义模型、训练评估、得到最优模型。
深度学习在计算机视觉领域有广泛应用,例如人脸识别、目标检测、人体姿态估计、视频生成、图像翻译等。
🔍 目标识别与检测
上一节我们介绍了深度学习的基本概念,本节中我们来看看它在计算机视觉中的两个具体任务:目标识别与目标检测。
目标识别本质上是一个分类问题。例如,识别出图像中的物体是台式机、笔记本电脑还是平板电脑。计算机会通过分析图像的特征来进行分类。

以下是计算机用于图像分析的几种常见特征:
- 颜色特征:例如RGB颜色模型,通过红、绿、蓝三原色不同比例的混合来表示所有颜色。
- 形状特征:例如图像中的线条、角点或更复杂的结构。
- 纹理特征:例如图像中重复的图案,如蜂巢状或叶片状纹理。


在目标识别中,卷积神经网络(CNN)是核心工具。CNN可以理解为一个包含输入层、输出层和多个隐藏层的黑箱。输入是图片,输出是分类结果(如图片内容)。隐藏层由卷积层和池化层等组成。

卷积层的工作方式是使用过滤器(或称为卷积核)在图像上滑动,进行卷积运算以提取局部特征。公式可以简化为:卷积结果 = Σ(图像局部像素值 × 过滤器对应权重)。为了处理图像边缘和调整扫描速度,还会用到填充(Padding)和步长(Stride)。




池化层(如最大池化)的作用是降低数据维度,同时保留最显著的特征。它会在一个小区域内(如2x2)只保留最大值。


经过多层卷积和池化后,特征会从低级的边缘、纹理逐渐组合成高级的、人眼可识别的形状。最后通过全连接层将特征图“拉平”,并映射到最终的分类结果上。


CNN的结构在不断进化。从AlexNet(8层)到VGG(19层),再到ResNet(152层),网络层数增加,识别错误率下降。但并非层数越多越好,过深的网络会遇到梯度消失或爆炸问题。现代框架(如TensorFlow, PyTorch)让搭建这些复杂模型变得容易。
一个有趣的应用是风格迁移(Deep Dream),它使用两个CNN分别学习照片的内容和名画的风格,然后合成一张具有名画风格的新照片。



目标识别解决了“是什么”的问题,而目标检测还要解决“在哪里”的问题。它需要在识别物体的同时,用矩形框标出物体的位置。
传统目标检测方法效率低下。现代深度学习方法主要分为两大流派:


两阶段检测(Two-Stage):
- 首先生成一系列可能包含物体的候选区域。
- 然后对这些候选区域进行分类和位置微调。
代表算法有R-CNN家族(如Fast R-CNN)。
单阶段检测(One-Stage):
- 直接预测物体的类别和位置,不再单独生成候选区域,速度更快。
- 通常使用锚框(Anchor Box)作为参考。
代表算法有YOLO系列。
目标检测的应用非常广泛,包括车牌识别、自动驾驶中的车辆与行人检测、工业质检、医疗影像分析等。
🕺 人体关键点检测
在学习了目标识别与检测后,本节我们关注一个更细粒度的视觉任务:人体关键点检测,也称为人体姿态估计。
人体关键点检测的作用是在图像上定位人体关键关节(如眼睛、肩膀、手肘、膝盖)的位置,并将它们按照人体结构连接起来,形成骨骼图。

进行关键点检测需要带有精确标注的数据集,例如COCO数据集,它包含了大量标注了17个人体关键点的图像。标注工具可以帮助我们创建自己的数据集。



该领域主要有两种技术路线:
自顶向下(Top-Down):
- 先用目标检测算法找出图像中所有的人。
- 然后在每个人的边界框内,单独进行关键点检测(这本身也是一个分类问题)。
代表算法是Mask R-CNN。它在Faster R-CNN的基础上增加了一个掩码分支,用于实例分割。在关键点检测任务中,它将掩码预测改为对17个关键点的热力图预测。热力图(Heatmap)上最亮的点表示该位置是特定关键点的概率最高。





自底向上(Bottom-Up):
- 首先检测出图像中所有类型的关键点(如所有左手腕、所有右膝盖)。
- 然后将这些点通过关联度计算,聚类组合成属于不同人的完整骨骼。
代表算法是OpenPose。它同时预测关键点的热力图和部分亲和力场(PAF)。PAF描述了关键点之间的关联向量。通过计算点与点之间的关联度,可以将属于同一个人的关键点正确连接起来。





人体关键点检测有众多应用,例如:
- 无人超市:通过分析顾客的骨骼动作,判断其拿取、放回商品的行为,实现自动结算。
- 体育分析:用于运动员动作捕捉、技术分析和训练指导。
- 人机交互:手势识别、虚拟试衣、动画驱动等。


🎨 图像对抗与生成


最后,我们来探讨计算机视觉中一个充满创造性的领域:图像对抗生成网络(GAN)。



GAN的主要应用包括:生成逼真的卡通人物头像、进行图像风格迁移、模拟人脸年龄变化、根据文字描述生成图像、为商品生成多角度展示图等。其核心思想包含“对抗”和“生成”两个部分。
GAN的基本结构包含两个神经网络:
- 生成器(G):目标是生成以假乱真的图片。
- 判别器(D):目标是准确区分输入图片是真实的还是生成器伪造的。
两者相互对抗、共同进步,形成一个动态的“博弈”过程。生成器试图“欺骗”判别器,而判别器则努力提升自己的“鉴别”能力。
具体训练过程是一个迭代的两步走算法:
- 固定生成器G,训练判别器D。用真实图片(标签1)和生成器生成的假图片(标签0)训练D,目标是让D对真实图片打分高,对假图片打分低。损失函数可表示为:
L_D = -[log(D(x)) + log(1 - D(G(z)))],其中x是真实图片,z是随机噪声。 - 固定判别器D,训练生成器G。目标是将随机噪声z输入G,生成的图片G(z)能让D给出高分(即误认为是真图)。损失函数可表示为:
L_G = -log(D(G(z)))。
通过反复迭代这两步,生成器和判别器的能力都会不断增强,最终生成器能产出非常逼真的图像。
GAN有很多变种,如CycleGAN(用于风格迁移)、StyleGAN(生成高质量人脸)等。研究者们将各种GAN模型汇总成了“GAN Zoo”。
📚 课程总结




本节课中我们一起学习了深度计算机视觉的四个核心话题。



我们首先了解了深度学习是什么,它作为人工智能的一个分支,通过“定义模型、训练评估、得到最优解”三步曲来解决问题。接着,我们深入探讨了目标识别(分类)与目标检测(分类+定位)的原理与经典算法(如CNN、YOLO、R-CNN)。然后,我们学习了人体关键点检测的两种方法(自顶向下和自底向上)及其广泛应用。最后,我们探索了生成对抗网络(GAN)的奇妙世界,理解了生成器与判别器相互对抗的训练机制,以及其在图像生成和编辑方面的强大能力。




希望本教程能帮助你建立起对深度计算机视觉的初步认识。





人工智能—计算机视觉CV公开课(七月在线出品) - P8:双语公开课《解密GNN》 🧠



在本节课中,我们将要学习图神经网络的基本概念、核心原理以及一个重要的变体——图注意力网络。我们将从图的基本定义开始,逐步深入到图卷积网络的工作原理,最后简要了解图注意力网络的运作方式。
图神经网络简介 📊
首先,我们需要明确几个基本问题:什么是图?什么是神经网络?什么是图神经网络?
图在计算机科学中通常对应英文单词 graph。它由两部分组成:节点 和 边。节点代表对象或实体,边代表对象之间的关系。
用数学公式可以表示为:
G = (V, E)
其中,G 代表图,V 代表节点集合,E 代表边集合。
图有多种分类方式:
- 无向图 vs 有向图:无向图的边没有方向,表示双向关系;有向图的边有箭头,表示单向关系。
- 循环图 vs 非循环图:循环图中存在路径能让信息从某个节点出发最终回到自身;非循环图则没有这种路径。
- 连通图 vs 非连通图:连通图中所有节点都直接或间接相连;非连通图中存在孤立的节点或子图。
图在现实世界中有广泛应用,例如社交网络(用户是节点,关注关系是边)、推荐系统(用户和商品是节点,购买行为是边)等。图神经网络的目标,就是利用深度学习的思想,直接在这些图结构的数据上进行学习和预测。

图卷积神经网络 🔄


上一节我们介绍了图的基本概念,本节中我们来看看图神经网络的一个核心模型——图卷积神经网络。
图卷积神经网络是一种直接在图上操作的神经网络,它利用图的结构信息来解决节点分类等问题。其核心思想是:每个节点从其所有邻居节点(包括自身)处聚合特征信息。


这与卷积神经网络的思想类似,都是聚合邻域信息。主要区别在于,CNN处理的是规整的网格数据(如图像像素),而GCN处理的是不规则的图结构。GCN通过引入邻接矩阵 A 来显式地表达图的结构。
一个简化的GCN层操作可以表示为:
H^{(l+1)} = σ(Ã H^{(l)} W^{(l)})
其中:
- H^{(l)} 是第 l 层的节点特征矩阵。
- Ã 是经过自循环和归一化处理后的邻接矩阵(具体为 Ã = D^{-1/2} A D^{-1/2},A是邻接矩阵,D是度矩阵)。
- W^{(l)} 是可学习的权重矩阵。
- σ 是非线性激活函数。
以下是GCN工作流程的关键步骤:
- 构建邻接矩阵 A:这是一个 N×N 的矩阵(N为节点数),如果节点 i 和 j 之间有边相连,则 A_ = 1,否则为 0。
- 计算度矩阵 D:这是一个对角矩阵,对角线上的值 D_ 表示节点 i 的度(即相连的边数)。
- 添加自循环与归一化:为了在信息聚合时包含节点自身的特征,并为连接数不同的节点进行归一化,我们计算 Ã = D^{-1/2} (A + I) D^{-1/2},其中 I 是单位矩阵。
- 特征传播与变换:将归一化的邻接矩阵 Ã 与节点特征矩阵 H 相乘,实现邻居信息的聚合,再通过可学习的权重矩阵 W 进行线性变换,最后经过激活函数。

通过堆叠多个GCN层,每个节点可以聚合到来自多跳邻居的信息,从而获得更丰富的特征表示。
图注意力网络 🎯
前面我们介绍了GCN,它平等地对待所有邻居节点。但在现实中,不同邻居的重要性往往不同。本节中我们来看看引入了注意力机制的图神经网络——图注意力网络。
图注意力网络的核心是为图中的每条边分配一个可学习的注意力权重,从而让节点在聚合邻居信息时能够“关注”更重要的邻居。
以下是GAT单头注意力层的主要计算步骤:

- 线性变换:对每个节点的输入特征 h_i 进行线性变换。z_i = W h_i
- 计算注意力系数:计算节点 i 对其邻居 j 的原始注意力分数。e_ = a([z_i || z_j]),其中
a是一个单层前馈神经网络,||表示拼接操作。 - 归一化注意力系数:使用 softmax 函数对某个节点 i 的所有邻居 j 的注意力系数进行归一化,得到最终的注意力权重 α_。α_ = softmax_j(e_)
- 聚合邻居信息:使用归一化后的注意力权重对邻居节点的变换后特征进行加权求和,并应用非线性激活函数。h'i = σ( Σ{j∈N(i)} α_ z_j )
- 多头注意力:为了稳定学习过程并捕获不同方面的信息,GAT通常使用多头注意力。即独立执行 K 次上述步骤,然后将得到的 K 个特征向量拼接或求平均作为最终输出。h'i = ||^K σ( Σ_{j∈N(i)} α_k Wk h_j )

通过注意力机制,GAT能够为图中不同的连接分配不同的重要性,从而更灵活、更强大地对图结构数据进行建模。
总结 📝

本节课中我们一起学习了图神经网络的基础知识。我们从图的基本定义出发,了解了节点、边、邻接矩阵等核心概念。接着,我们深入探讨了图卷积神经网络的原理,明白了它如何通过归一化的邻接矩阵来聚合邻居信息。最后,我们简要介绍了图注意力网络,它通过引入注意力机制,使模型能够区分不同邻居的重要性。



图神经网络为我们处理非欧几里得空间的结构化数据(如社交网络、分子结构、知识图谱)提供了强大的工具。希望本课程能帮助你打开图神经网络世界的大门。




🖼️ 人工智能—计算机视觉CV公开课(P9):图像搜索与分类


在本节课中,我们将要学习图像搜索与分类的核心概念,涵盖卷积神经网络、目标检测、变分自编码器以及车辆重识别等关键主题。我们将从基础原理入手,逐步深入到实际应用,确保初学者能够轻松理解。
🧠 第一部分:卷积神经网络(CNN)
上一节我们介绍了课程概述,本节中我们来看看卷积神经网络(CNN)。CNN是深度学习在计算机视觉领域的基石,主要用于解决目标识别或分类问题。

什么是卷积神经网络?

卷积神经网络主要解决目标识别问题。目标识别是指当看到一张照片时,能够识别出照片的主体是什么,例如区分照片中是猫、狗、建筑还是人。这个问题统称为分类问题。
机器如何进行图像分类?
人类通过寻找图像的相同点和不同点(如大小、形状、材质)来分类。机器则依靠寻找图像的特征来进行判别。特征在英文中称为“feature”。
以下是机器识别图像时依赖的主要特征类型:
- 颜色特征:计算机将颜色用数值表示,常见的方法如RGB(红、绿、蓝)模型,通过三维数值组合成各种颜色。
- 形状特征:例如图像中的线条、角、不规则结构等。
- 纹理特征:图像中大量重复的图案模式。
机器通过摄像头捕捉图像,在图像上找到这些特征,并将低级特征组合成高级特征,最终完成分类。
CNN的基本结构
一个典型的CNN由三部分构成:输入端、输出端和中间的隐藏层。
- 输入端:输入一张图片。
- 输出端:输出分类结果,例如有3类电脑(笔记本电脑、台式机、平板电脑),则输出端对应3个节点。
- 隐藏层:位于输入和输出之间,其层数可以人为定义。隐藏层的主要目标是找出图像的各种特征。越靠近输入端的层,找到的特征越低级(如边缘、角点);越靠近输出端的层,找到的特征越高级(如物体部件、整体形状)。
卷积层与卷积核
CNN的核心组件是卷积层,其核心操作是卷积,依赖于卷积核(也称为过滤器)。
- 卷积核:是一个小矩阵(如3x3x3),其作用是像过滤器一样在图像上滑动,搜索特定的模式(如斜线、竖线、圆角)。不同的卷积核用于检测不同的特征。
- 卷积操作:卷积核在输入图像上从左到右、从上到下滑动。在每个位置,卷积核与对应的图像区域进行元素相乘并求和,得到一个数值,所有数值组成一个新的矩阵,称为特征图。
在卷积操作中,有两个重要概念:
- 填充:在图像外围填充像素(通常为0),以确保卷积核能扫描到图像边缘的每一个像素。
- 步长:卷积核每次滑动的像素距离。步长越大,扫描速度越快,但可能忽略细节;步长越小,特征提取越精细,但计算量越大。

池化层

在卷积层之后,通常会连接一个池化层。

- 作用:对卷积层输出的特征图进行下采样,减少数据量,同时保留主要特征,并增强模型的平移不变性。
- 常见操作:最大池化(取区域内最大值)或平均池化(取区域内平均值)。例如,将一个4x4的区域分成4个2x2的小块,对每个小块取最大值,最终得到一个2x2的输出。
卷积层和池化层可以组合成一个基本的隐藏层模块,并在网络中重复多次。


从特征到分类


经过多层卷积和池化后,网络会输出一个三维特征图。为了进行分类,需要将这个三维特征图“拉平”成一维向量,然后连接一个或多个全连接层。
- 全连接层:将前面提取的所有特征进行综合,最终映射到输出层的各个类别节点上。
- 输出与训练:输出层每个节点代表一个类别的概率。训练网络时,我们输入已知标签的图片(如猫的图片,标签为“猫”),通过调整网络内部参数,使得网络对于猫图片的输出中“猫”对应的概率最高。训练完成后,网络便能对新的、未见过的图片进行分类。
CNN的发展与应用
CNN自提出以来,网络层数不断加深,性能持续提升,例如AlexNet、VGG、GoogLeNet、ResNet等。如今,我们通常使用深度学习框架来构建CNN,无需手动设计每一层和过滤器。
CNN的应用非常广泛,包括:
- 目标识别(如人脸识别、Face ID)
- 目标检测(如交通监控、无人驾驶)
- 图像生成(如风格迁移,将普通照片转化为梵高画作风格)
- 医学图像分析等。










🔍 第二部分:目标检测与定位
上一节我们介绍了用于图像分类的CNN,本节中我们来看看目标检测。目标检测在分类的基础上,增加了定位任务,即不仅要识别出物体是什么,还要用边界框标出它在图像中的位置。
从分类到检测

目标识别是一个分类问题,例如判断图像中是狗还是婴儿。目标检测则是一个“分类+定位”问题,需要在图像中找到目标并用矩形框圈出,例如在一张有狗和婴儿的图片中,分别用框标出狗和婴儿的位置。
目标检测的主要方法
传统方法(如滑动窗口)效率低下。现代基于深度学习的目标检测方法主要分为两大流派:
- 两阶段检测:首先生成一系列可能包含目标的候选区域,然后对这些候选区域进行分类和精确边框回归。精度通常较高,但速度较慢。
- 代表算法:R-CNN系列、Fast R-CNN、Faster R-CNN、Mask R-CNN。
- 一阶段检测:直接在图像的不同位置进行密集抽样和预测,一步到位地输出分类和边框信息。速度非常快,但精度传统上略低于两阶段方法。
- 代表算法:YOLO系列、SSD、RetinaNet、CornerNet。


近年来,一些新算法(如CornerNet-Lite)致力于结合两者的优点,在保持高速度的同时提升精度。
目标检测的应用
目标检测技术已广泛应用于各个领域:
- 车牌识别:自动定位并识别车牌号码。
- 智能交通:检测车辆、行人,分析车流轨迹,进行碰撞预警。
- 行人检测:公共安全监控。
- 图像搜索:通过拍摄物体搜索相关信息。
- 机器人:帮助机器人定位和抓取物体。
- 工业自动化:机械臂的视觉引导。
- 医疗影像:辅助医生定位病变区域。
🎨 第三部分:变分自编码器(VAE)
上一节我们讨论了如何检测和定位图像中的物体,本节中我们来看看如何生成图像。变分自编码器是一种强大的生成模型。
从自编码器到变分自编码器

- 自编码器:一种神经网络,目标是学习输入数据的压缩表示(编码),并能从这个压缩表示中重建出原始数据(解码)。训练目标是让输出尽可能接近输入,相当于一个高效的数据压缩和重建工具。
- 变分自编码器的改进:标准自编码器学到的编码空间不规则,难以用于生成新数据。VAE对其进行了关键改进:
- 约束编码:强迫编码器产生的潜在变量服从一个简单的先验分布(通常是标准高斯分布)。
- 采样生成:从这个分布中采样,然后将采样点输入解码器,即可生成新的图像。
- 平衡优化:通过一个损失函数同时优化两项:重建损失(生成图像与原始图像的差异)和KL散度损失(潜在变量分布与标准高斯分布的差异)。这使得VAE既能生成多样化的新图像,又能保证生成图像的质量和合理性。

VAE使我们能够通过操纵潜在空间中的点,来生成具有特定特征的、全新的图像。

🚗 第四部分:车辆重识别
上一节我们介绍了生成图像的VAE,本节我们来看一个具体的应用难题——车辆重识别。车辆重识别是指在不同的时间、不同的摄像头视角下,重新识别并跟踪同一辆车辆。
什么是车辆重识别?
已知在某一时刻、某一地点出现过的目标车辆,经过一段时间后,在可能覆盖的区域(如城市道路网络)中,重新定位并跟踪到这辆车。
车辆重识别的流程与挑战
一个典型的车辆重识别系统通常包含以下步骤:
- 确立假设空间:根据车辆最大可能行驶速度,划定一个合理的圆形搜索区域。
- 缩小搜索空间:结合地图信息,过滤掉非道路区域(如湖泊、学校),极大缩小搜索范围。
- 筛选候选数据:召回该区域内所有摄像头在相关时间段抓拍到的车辆图像。
- 确定可疑车辆:在候选车辆中,通过比对找到目标车辆。
这个过程面临诸多挑战:
- 视角差异:同一车辆在不同角度拍摄的照片差异很大。
- 摄像头差异:不同摄像头的分辨率、光照条件不同。
- 车牌信息缺失或模糊:车牌被遮挡、污损,或高速运动导致图像模糊。
- 行驶路线不确定性:交通状况、天气等因素可能改变车辆既定路线。


关键技术:如何应对模糊车牌?




当车牌清晰时,直接进行字符识别即可匹配。但当车牌模糊时,可以采用“验证”而非“识别”的策略:
- 使用目标检测模型截取出候选车辆的车牌区域。
- 利用孪生神经网络来比较两个车牌区域的相似度。
- 孪生神经网络由两个结构、参数完全相同的子网络组成。
- 输入是两张车牌图片,分别通过子网络提取特征编码。
- 计算两个特征编码之间的相似度(如欧氏距离)。
- 相似度越高,说明是同一车牌的可能性越大。这样就不需要精确识别出模糊的车牌字符,只需判断两个车牌是否相似。



📚 课程总结

本节课中我们一起学习了计算机视觉中图像搜索与分类的四个核心主题:
- 卷积神经网络:作为计算机视觉的基石,CNN通过卷积、池化等操作自动提取图像特征,完成高效的图像分类任务。
- 目标检测与定位:在分类基础上发展而来,能够定位图像中多个物体的位置,主要分为精度高的两阶段方法和速度快的一阶段方法。
- 变分自编码器:一种生成模型,通过编码-解码结构和潜在空间约束,能够学习数据分布并生成新的图像样本。
- 车辆重识别:一个综合性的应用实例,结合了时空信息、外观过滤、车牌验证(尤其是应对模糊情况的孪生网络技术)来解决跨摄像头、跨时间的车辆追踪难题。




这些技术构成了现代图像搜索、分类与理解系统的基础,并广泛应用于安防、交通、医疗、娱乐等众多领域。希望本教程能帮助你建立起对计算机视觉这些核心概念的初步理解。
人工智能—面试求职公开课(七月在线出品) - P1:O(N)时间解决的面试题(上) 🧠
在本节课中,我们将学习一系列可以在 O(N) 时间复杂度内解决的经典面试题。理解并掌握这些线性时间算法,对于应对技术面试至关重要。我们将从基础概念入手,通过六个具体的例题,深入探讨如何设计并优化线性时间算法。
课程简介 📖
首先,我们需要明确 O(N) 中的 N 具体指代什么。N 代表问题的输入规模。例如,如果输入是一个图,我们需要明确 N 是指节点数还是边数。对于一个稠密图,边数可能是节点数的平方级别。因此,当我们说算法是 O(N) 时,必须清楚这个线性是相对于哪种输入规模而言的。
在方法上,O(N) 最直观的理解是扫描一遍输入,例如一个循环 N 次的简单遍历。但线性算法远不止于此,常见的形式还包括:
- 双指针扫描:从两端向中间或同向移动。
- 看似嵌套但总次数为 O(N) 的循环:关键在于内层循环的变量不减小,总循环次数与 N 成线性关系。
- 利用数据结构的单调性:如单调队列或单调栈,可以将复杂问题转化为线性时间可解的问题。
接下来,我们将通过具体问题来实践这些思想。
例题一:名人问题(社会名流问题) 👑
问题描述:给定一个 N x N 的矩阵 M,M[i][j] = 1 表示人物 i 认识人物 j,M[i][j] = 0 则表示不认识。矩阵不一定对称(即 i 认识 j 不代表 j 认识 i)。名人的定义是:他不认识任何其他人,但其他所有人都认识他。请找出所有名人。

关键洞察:一个群体中最多只能有一个名人。可以通过两两比较来排除不可能的人选。

O(N²) 的朴素算法:对于每个人 i,检查是否所有 j 都满足 (i 不认识 j) 且 (j 认识 i)。这需要两层循环。
O(N) 的优化算法:我们可以利用一次遍历找出可能的候选人。
- 初始化候选人
cand = 0。 - 遍历其他人
i(从 1 到 N-1):- 如果
cand认识i,则cand不可能是名人,将cand更新为i。 - 如果
cand不认识i,则i不可能是名人,继续检查下一个i。
- 如果
- 遍历结束后,
cand是唯一可能的候选人。最后,我们需要验证cand是否真的满足名人条件(即他不认识任何人,且所有人都认识他)。这个验证过程也是 O(N)。
算法实现(O(1) 空间):
def findCelebrity(M):
n = len(M)
cand = 0
# 第一遍扫描,找出候选人
for i in range(1, n):
if M[cand][i]: # cand 认识 i,cand 不是名人
cand = i
# 第二遍扫描,验证候选人
for i in range(n):
if i != cand and (M[cand][i] or not M[i][cand]):
return -1 # cand 不是名人
return cand
算法分析:时间复杂度为 O(N),空间复杂度为 O(1)。我们通过巧妙的比较和排除,将问题规模线性减小。
例题二:接雨水问题(LeetCode 42) 💧

问题描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
核心思路:对于第 i 个位置,它能接到的雨水量取决于它左边最高柱子和右边最高柱子中较矮的那个,再减去它自身的高度。公式为:
water[i] = min(left_max[i], right_max[i]) - height[i]
O(N) 算法步骤:
- 计算前缀最大值数组
left_max,其中left_max[i]表示位置i及其左边柱子的最大高度。 - 计算后缀最大值数组
right_max,其中right_max[i]表示位置i及其右边柱子的最大高度。 - 遍历每个位置
i,累加min(left_max[i], right_max[i]) - height[i]。
算法实现:
def trap(height):
if not height:
return 0
n = len(height)
left_max = [0] * n
right_max = [0] * n
ans = 0
# 计算前缀最大值
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i-1], height[i])
# 计算后缀最大值
right_max[n-1] = height[n-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], height[i])
# 计算总雨水量
for i in range(n):
ans += min(left_max[i], right_max[i]) - height[i]
return ans
算法分析:我们通过三次线性扫描,分别计算前缀最大值、后缀最大值和最终结果,总时间复杂度为 O(N),空间复杂度为 O(N)(用于存储两个最大值数组)。此方法直观地应用了动态规划的思想。
例题三:盛最多水的容器(LeetCode 11) 🏺
问题描述:给定一个长度为 n 的整数数组 height,代表 n 条垂直线的高度。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
核心思路:容器的盛水量由**两条线的距离(底边)和两条线中较短者的高度(高)**决定,即面积 S = min(height[i], height[j]) * (j - i)。
O(N²) 的朴素算法:枚举所有可能的 (i, j) 组合。
O(N) 的双指针算法:
- 初始化指针
i = 0,j = n - 1,以及最大面积max_area = 0。 - 当
i < j时循环:- 计算当前面积
area = min(height[i], height[j]) * (j - i),并更新max_area。 - 移动高度较小的指针:如果
height[i] < height[j],则i += 1;否则j -= 1。
- 计算当前面积
算法正确性证明(简要):关键在于,每次移动高度较小的指针,是在舍弃不可能成为更优解的情况。因为容器的盛水量受限于较短边,固定短边并移动长边,宽度在减小,面积只会更小。因此,只有移动短边,才有可能在后续遇到更高的边,从而获得更大的面积。这个策略保证了最优解一定会被遍历到。
算法实现:
def maxArea(height):
i, j = 0, len(height) - 1
max_area = 0
while i < j:
h = min(height[i], height[j])
max_area = max(max_area, h * (j - i))
if height[i] < height[j]:
i += 1
else:
j -= 1
return max_area
算法分析:指针 i 和 j 总共移动 n-1 次,时间复杂度为 O(N),空间复杂度为 O(1)。这是一个利用问题特性进行贪心选择的经典例子。
例题四:最大间距问题 📏
问题描述:给定一个无序数组,找出数组在排序后,相邻元素之间最大的差值。要求在线性时间和空间内完成。

挑战:直接排序需要 O(N log N) 时间。我们需要一个 O(N) 的算法。
O(N) 的桶排序思想算法:
- 找出数组中的最大值
max_val和最小值min_val。 - 计算桶的容量
bucket_size = max(1, (max_val - min_val) // (n - 1))和桶的数量bucket_num = (max_val - min_val) // bucket_size + 1。 - 创建
bucket_num个桶,每个桶只记录该桶内的最大值和最小值。 - 遍历数组,将每个元素放入对应的桶中(
index = (num - min_val) // bucket_size),并更新该桶的最大最小值。 - 遍历所有桶,最大间距只可能出现在前一个非空桶的最大值和后一个非空桶的最小值之间。

算法实现(核心逻辑):
def maximumGap(nums):
if len(nums) < 2:
return 0
min_val, max_val = min(nums), max(nums)
n = len(nums)
bucket_size = max(1, (max_val - min_val) // (n - 1))
bucket_num = (max_val - min_val) // bucket_size + 1
buckets = [[float('inf'), float('-inf')] for _ in range(bucket_num)]
for num in nums:
idx = (num - min_val) // bucket_size
buckets[idx][0] = min(buckets[idx][0], num)
buckets[idx][1] = max(buckets[idx][1], num)
max_gap = 0
prev_max = min_val
for i in range(bucket_num):
if buckets[i][0] == float('inf'): # 空桶
continue
max_gap = max(max_gap, buckets[i][0] - prev_max)
prev_max = buckets[i][1]
return max_gap
算法分析:此算法基于桶排序,但无需对桶内元素完全排序。我们利用了“最大间距一定不小于 (max_val - min_val) / (n-1)”这一特性来设置桶的大小,从而确保最大间距不会出现在桶内,只可能出现在桶间。时间复杂度为 O(N),空间复杂度为 O(N)。
例题五:连续子数组:0 和 1 个数相等的最长子串 🔢
问题描述:给定一个二进制数组(只包含 0 和 1),找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
核心转化:将问题中的 0 视为 -1,1 视为 1。那么,“子数组中 0 和 1 数量相等” 就等价于 “子数组的和为 0”。进一步,子数组 nums[i:j] 的和为 0 等价于前缀和 prefix_sum[j] 等于前缀和 prefix_sum[i-1]。

O(N) 的哈希表算法:
- 使用一个变量
cur_sum记录当前的前缀和(初始为0),一个哈希表map记录每个前缀和第一次出现的下标(初始{0: -1},表示前缀和为0在索引-1处出现,用于处理从开头开始的子数组)。 - 遍历数组:
- 更新当前前缀和:
cur_sum += 1 if nums[i]==1 else -1。 - 如果
cur_sum在哈希表中出现过(设第一次出现在index),那么从index+1到i的子数组和为0,长度为i - index。 - 如果
cur_sum没出现过,则将其值cur_sum和当前索引i存入哈希表。
- 更新当前前缀和:
- 遍历过程中记录找到的最大长度。
算法实现:
def findMaxLength(nums):
sum_index_map = {0: -1}
cur_sum = 0
max_len = 0
for i, num in enumerate(nums):
cur_sum += 1 if num == 1 else -1
if cur_sum in sum_index_map:
max_len = max(max_len, i - sum_index_map[cur_sum])
else:
sum_index_map[cur_sum] = i
return max_len
算法分析:通过将前缀和作为键存入哈希表,我们可以在 O(1) 时间内查询到某个和是否出现过,从而将寻找相等前缀和的时间复杂度从 O(N log N)(排序)降低到 O(N)。总时间复杂度为 O(N),空间复杂度为 O(N)(最坏情况下需要存储 N 个不同的前缀和)。
例题六:行降序 01 矩阵中 1 最多的行 🔍

问题描述:给定一个 N x N 的 01 矩阵,其中每一行都是降序排列(即先出现连续的 1,后出现连续的 0)。找出 1 的数量最多的那一行,并返回其 1 的个数。要求时间复杂度 O(N)。
关键观察:由于矩阵是 N x N 的,且每行有序,一个 O(N log N) 的算法是对每行进行二分查找,找到第一个 0 的位置。但我们可以利用全局信息做得更好。
O(N) 的走楼梯算法:
- 初始化答案
max_ones = 0,以及列指针col = 0。 - 从第一行 (
row = 0) 开始遍历:- 在当前行,从当前列
col开始向右移动,只要matrix[row][col] == 1,就增加col和max_ones的计数。 - 当遇到
matrix[row][col] == 0或col == N时,停止向右。此时max_ones记录了到当前行为止看到的最大 1 的数量,col指向了第一个 0 出现的位置(或末尾)。 - 移动到下一行 (
row += 1)。因为下一行的col列及之后的位置,如果还是 1,那么该行的 1 可能更多;如果是 0,则该行不可能超过当前的max_ones,列指针col无需回退。
- 在当前行,从当前列
算法分析:列指针 col 在整个过程中只增不减,从 0 最多增加到 N。行指针 row 从 0 遍历到 N-1。因此总操作次数约为 2N,时间复杂度为 O(N),空间复杂度为 O(1)。这个算法巧妙地利用了矩阵的行有序性和问题的目标,避免了不必要的重复检查。
总结与延伸 🎯

本节课我们一起学习了六个可以在 O(N) 线性时间内解决的面试算法问题。我们回顾一下核心要点:

- 明确问题规模 N:始终要清楚算法复杂度中的 N 具体指代什么(如节点数、边数、数组长度)。
- 掌握经典模式:
- 双指针/滑动窗口:用于子数组、链表等问题(例题三、六)。
- 前缀和/前缀最值:用于快速计算区间属性和比较(例题二、五)。
- 哈希表优化查找:将查找时间从 O(log N) 或 O(N) 降至 O(1)(例题五)。
- 桶排序思想:用于在特定条件下实现线性排序或统计(例题四)。
- 利用单调性:通过排除不可能的解来缩小搜索空间(例题一、三、六)。
- 复杂度分析本质:不要简单地数循环嵌套层数,而要分析基本操作的总执行次数与输入规模 N 的关系。
线性时间算法在面试中极为常见,除了本节课的例题,还有许多其他经典问题,例如:
- 链表相关(环检测、相交节点、反转等)
- 二叉树遍历(前序、中序、后序、层序)
- 使用 Partition 操作的问题(快速选择、颜色分类)
- 使用单调栈/队列的问题(柱状图中最大矩形、滑动窗口最大值)
要想熟练掌握,多思考、多练习、多总结是关键。希望本课程能帮助你建立起对 O(N) 算法的感性认识和理性分析框架,在面试中更加游刃有余。


版权说明:本课程内容基于《人工智能—面试求职公开课(七月在线出品)》P1 部分进行翻译、整理和再创作。核心算法思想与案例归原课程所有。
人工智能—面试求职公开课(七月在线出品) - P10:图与树的面试题精讲 🧠🌳
在本节课中,我们将要学习图与树相关的核心面试题。课程将从图论简介开始,梳理常见考点,并通过一系列由浅入深的例题,讲解如何运用递归等核心思想解决二叉树和图论问题。最后,我们将对关键知识点进行总结。
图论简介 📊
图是一种抽象的数学结构,由节点和边组成。根据边是否有方向,图可分为有向图和无向图。
在计算机数据结构中,一种特殊的图是树。树是一种无环的无向图。其中,有根树(如并查集)和二叉树(特别是二叉搜索树)是经典结构。堆也是一种特殊的二叉树,常用于堆排序算法。
面试题总体分析 📝
对于图论,面试常考察以下方面:
- 连通性:判断两点间是否有路径、求连通分量等。
- 割点与割边:删除该点或边会使图由连通变为不连通。
- 最小生成树:如Kruskal算法。
- 最短路问题:包括单源最短路、任意两点间最短路等。
- 搜索算法:广度优先搜索(BFS)和深度优先搜索(DFS)。
- 欧拉回路与哈密顿回路:概念性考察。
- 拓扑排序:相对简单。
对于树,常考察以下方面:
- 定义与判断:判断是否为二叉树、二叉搜索树、平衡二叉树等。
- 属性计算:求树的最大/最小高度(深度)。
- 最近公共祖先(LCA):求两个节点的最近公共祖先。
例题精讲 🧩
上一节我们梳理了图与树的核心考点,本节中我们将通过具体例题来深入理解解题思路。所有例题将围绕一个统一的递归框架展开,难度逐步递增。
例一:由遍历序列构造二叉树 🔨
LeetCode 105题。给定二叉树的前序遍历和中序遍历序列,构造出原始的二叉树。
核心思路:
前序遍历的第一个节点是根节点。在中序遍历序列中找到这个根节点,其左侧序列即为左子树的中序遍历,右侧为右子树的中序遍历。根据左右子树的长度,同样可以将前序遍历序列分割为左子树和右子树的前序遍历序列。这样就得到了两个子问题,递归构造左右子树即可。
代码框架:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) return nullptr;
TreeNode* root = new TreeNode(preorder[0]); // 根节点
// 在中序序列中找到根节点的位置
auto it = find(inorder.begin(), inorder.end(), preorder[0]);
int leftSize = it - inorder.begin();
// 递归构造左右子树
vector<int> leftPre(preorder.begin()+1, preorder.begin()+1+leftSize);
vector<int> leftIn(inorder.begin(), it);
vector<int> rightPre(preorder.begin()+1+leftSize, preorder.end());
vector<int> rightIn(it+1, inorder.end());
root->left = buildTree(leftPre, leftIn);
root->right = buildTree(rightPre, rightIn);
return root;
}
这个过程本质上是前序遍历的递归:先构造根节点,再构造左子树,最后构造右子树。
思考题:LeetCode 106题,给定后序遍历和中序遍历序列构造二叉树,思路完全一致。
例二:二叉树问题的递归框架 🔄
从例一可以看出,二叉树问题大多可以用递归解决。递归的核心在于处理根节点、左子树、右子树三部分,根据处理顺序的不同,对应了抽象意义上的前序、中序、后序遍历。
以下是几个可用此框架解决的经典问题:
1. 二叉树中的最大路径和 (LeetCode 124)
问题:二叉树每个节点有一个整数值,求路径(节点序列)上各节点值之和的最大值。路径至少包含一个节点,且不一定经过根节点。
思路:对于每个节点,计算从该节点向下延伸(向叶子方向)的最大路径和。全局最大路径和可能出现在:1) 左子树向下延伸的路径;2) 右子树向下延伸的路径;3) 从左子树经过根节点再到右子树的“V”形路径。这是一个后序遍历的过程(先算左右,再算根)。
int maxPathSum(TreeNode* root) {
int result = INT_MIN;
helper(root, result);
return result;
}
int helper(TreeNode* root, int &result) {
if (!root) return 0;
int left = max(0, helper(root->left, result)); // 只取正值
int right = max(0, helper(root->right, result));
// 计算经过当前根节点的“V”形路径和,更新全局最大值
result = max(result, left + right + root->val);
// 返回从当前节点向下延伸的最大路径和(只能选择一边)
return max(left, right) + root->val;
}
2. 二叉树的最小深度 (LeetCode 111)
问题:从根节点到最近叶子节点的最短路径上的节点数量。
思路:需要确保终点是叶子节点。递归计算左右子树的最小深度,但需处理子树为空的情况。
int minDepth(TreeNode* root) {
if (!root) return 0;
if (!root->left && !root->right) return 1;
int min_depth = INT_MAX;
if (root->left) {
min_depth = min(min_depth, minDepth(root->left));
}
if (root->right) {
min_depth = min(min_depth, minDepth(root->right));
}
return min_depth + 1;
}
3. 平衡二叉树的判断 (LeetCode 110)
问题:判断二叉树是否是高度平衡的(每个节点的左右子树高度差不超过1)。
思路:在递归计算节点高度的同时,判断其左右子树是否平衡。可视为一种后序遍历。
bool isBalanced(TreeNode* root) {
bool balanced = true;
height(root, balanced);
return balanced;
}
int height(TreeNode* root, bool &balanced) {
if (!root || !balanced) return 0;
int left_h = height(root->left, balanced);
int right_h = height(root->right, balanced);
if (abs(left_h - right_h) > 1) balanced = false;
return max(left_h, right_h) + 1;
}
4. 二叉树的最大深度 (LeetCode 104)
问题:求二叉树的最大深度(高度)。
思路:根据定义直接递归。空树深度为0,否则为左右子树最大深度加1。
int maxDepth(TreeNode* root) {
return root ? 1 + max(maxDepth(root->left), maxDepth(root->right)) : 0;
}
5. 相同的树 (LeetCode 100)
问题:判断两棵二叉树是否完全相同。
思路:递归判断根节点值是否相等,且左右子树是否分别相同。
bool isSameTree(TreeNode* p, TreeNode* q) {
if (!p && !q) return true;
if (!p || !q) return false;
return (p->val == q->val) && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
6. 对称二叉树 (LeetCode 101)
问题:判断二叉树是否镜像对称。
思路:转化为判断两棵树(原树的左右子树)是否镜像。镜像判断:根节点值相等,且树A的左子树与树B的右子树镜像,树A的右子树与树B的左子树镜像。
bool isSymmetric(TreeNode* root) {
return isMirror(root, root);
}
bool isMirror(TreeNode* t1, TreeNode* t2) {
if (!t1 && !t2) return true;
if (!t1 || !t2) return false;
return (t1->val == t2->val) && isMirror(t1->left, t2->right) && isMirror(t1->right, t2->left);
}
7. 验证二叉搜索树 (LeetCode 98)
问题:判断二叉树是否是有效的二叉搜索树(BST)。
思路:BST的中序遍历序列是严格递增的。进行中序遍历,并记录上一个访问节点的值,比较当前节点值是否大于上一个值。
bool isValidBST(TreeNode* root) {
TreeNode* prev = nullptr;
return inorder(root, prev);
}
bool inorder(TreeNode* node, TreeNode* &prev) {
if (!node) return true;
if (!inorder(node->left, prev)) return false;
if (prev && node->val <= prev->val) return false;
prev = node;
return inorder(node->right, prev);
}
例三:二叉树与链表的转换 🔗
上一节我们介绍了二叉树问题的通用递归框架,本节中我们来看看二叉树与链表相互转换这一特殊应用。
1. 二叉树展开为链表 (LeetCode 114)
问题:将二叉树按前序遍历顺序展开为一个只有右子树的单链表(原地修改)。
思路:递归处理。将左子树展开后接到根的右侧,再将原右子树展开后接到当前链表的末尾。
void flatten(TreeNode* root) {
if (!root) return;
flatten(root->left);
flatten(root->right);
TreeNode* temp = root->right;
root->right = root->left;
root->left = nullptr;
// 找到当前右子树(即原左子树)的末尾
TreeNode* cur = root;
while (cur->right) cur = cur->right;
cur->right = temp; // 接上原右子树
}
2. 有序链表转换二叉搜索树 (LeetCode 109)
问题:给定一个单链表,元素按升序排列,将其转换为高度平衡的二叉搜索树。
思路(O(n log n)):每次用快慢指针找到链表中点作为根节点,递归构造左右子树。由于链表不能随机访问,找中点需要O(n)时间。
优化思路(O(n)):利用中序遍历的顺序性。先计算链表长度,然后递归构建。在递归过程中,通过引用传递链表节点指针,使其自然按中序顺序前进。
TreeNode* sortedListToBST(ListNode* head) {
int len = 0;
ListNode* cur = head;
while (cur) { len++; cur = cur->next; }
return build(head, 0, len - 1);
}
TreeNode* build(ListNode* &head, int start, int end) {
if (start > end) return nullptr;
int mid = start + (end - start) / 2;
TreeNode* left = build(head, start, mid - 1); // 构建左子树,head会移动
TreeNode* root = new TreeNode(head->val); // 当前head即为中点
head = head->next;
root->left = left;
root->right = build(head, mid + 1, end); // 构建右子树
return root;
}
思考题:LeetCode 108题,将有序数组转换为平衡二叉搜索树,由于数组支持随机访问,实现更为简单。
例四:图的深拷贝(克隆图) 🧬
LeetCode 133题(中等)。给定一个无向连通图的节点,返回该图的深拷贝。图中每个节点包含其邻居列表。图可能包含自环和重复边。
核心思路:由于图可能有环,直接递归复制会导致无限循环或重复创建。使用深度优先搜索(DFS) 配合哈希映射(Map) 来记录原节点与克隆节点的对应关系。
- 从给定节点开始DFS。
- 如果当前节点已在Map中,直接返回其克隆节点。
- 否则,创建当前节点的克隆体,并立即放入Map(防止后续递归又创建它)。
- 递归克隆所有邻居节点,并添加到克隆节点的邻居列表中。
unordered_map<Node*, Node*> visited;
Node* cloneGraph(Node* node) {
if (!node) return nullptr;
if (visited.find(node) != visited.end()) {
return visited[node]; // 已克隆,直接返回
}
Node* cloneNode = new Node(node->val); // 创建克隆节点
visited[node] = cloneNode; // 关键:先放入映射,再处理邻居
for (Node* neighbor : node->neighbors) {
cloneNode->neighbors.push_back(cloneGraph(neighbor));
}
return cloneNode;
}
例五:棋盘上的路径问题(转化为欧拉路) ♟️
这是一个较难的问题。给定一个矩形棋盘和一些必须经过的指定点。从任意起点出发,移动规则:每次只能水平或竖直移动任意格,但移动方向必须与上一次垂直(即不能连续两次同向移动)。每个指定点只能经过一次。判断是否存在这样的路径。
问题转化:
- 这看起来像一个哈密顿路径问题(访问所有节点各一次),是NP-Hard问题,直接求解困难。
- 巧妙转化:将每个点的行坐标(X) 和列坐标(Y) 视为两类节点。每次移动(水平或垂直)相当于在X节点和Y节点之间连一条边。
- 要求遍历所有指定点,等价于要遍历所有这些X-Y边各一次,且节点(坐标值)可重复访问。
- 这就转化为了判断无向图是否存在欧拉路径(一笔画) 的问题。欧拉路径存在性有多项式时间判定准则:对于无向图,当且仅当奇度顶点(连接边数为奇数的顶点)的个数为0或2时,存在欧拉路径。
解题步骤:
- 统计所有指定点的行坐标和列坐标,构建X节点集和Y节点集。
- 每个指定点对应一条连接其X节点和Y节点的边。
- 计算所有X节点和Y节点的度(连接的边数)。
- 统计奇度节点的数量。若为0或2,则存在满足条件的路径;否则不存在。
思考题:密码锁问题(Google面试题)
一个4位密码锁,状态从ABCD可以变为BCDE,操作是:移除最高位A,剩余位左移(BCD变成新状态的高三位),然后在最低位填入任意数字E(0-9)。问:是否存在一种操作序列,能遍历所有0000到9999的一万个状态各恰好一次?
提示:构建一个图,节点是三位数字(000-999),共1000个节点。如果节点ABC的后两位BC与节点BCD的前两位BC相同,则它们之间有一条有向边,这条边恰好对应了一个四位数状态ABCD。遍历所有边各一次,即欧拉回路问题。可以证明该图存在欧拉回路。
课程总结 📚
本节课我们一起学习了图与树在面试中的核心考题与解题思路。
- 理解递归:递归是解决树形结构问题的利器。核心在于定义好子问题(通常是左右子树),并在递归调用中处理好根节点。
- 树的遍历:前序、中序、后序遍历不仅是基础操作,其递归思想更是解决复杂树问题的框架。同时需掌握非递归实现。
- 其他重要问题:
- 最近公共祖先(LCA):有离线算法(如Tarjan)和在线算法(如倍增法)。
- 隐式图搜索:许多问题(如例五)需要自己构建图模型,再应用BFS/DFS。这涉及到连通分量、强连通分量的求解。
- 拓扑排序:用于有向无环图(DAG)的节点排序,教材中有标准算法。
- 图论建模:最难的问题往往在于将实际问题抽象为合适的图论模型(如哈密顿路转化为欧拉路),这需要敏锐的洞察力和练习。


通过本课的学习,希望大家能够掌握图与树相关问题的核心解题模式,并具备将复杂问题转化为已知模型的能力。

人工智能—面试求职公开课(七月在线出品) - P11:助力春招
📚 课程概述
在本节课中,我们将学习一系列在算法面试中频繁出现的经典题目及其解题思路。课程内容涵盖概率计算、数据结构操作和逻辑推理等多个方面,旨在帮助初学者掌握核心概念和解题技巧。
🎲 第一题:古典概型与概率计算
上一节我们介绍了课程的整体安排,本节中我们来看看一个典型的概率计算问题。
题目描述:现有面值分别为5元、10元、20元的纸币,数量分别为6张、5张、4张。从中任意抽取4张纸币,要求每种面值至少有一张。求满足此条件的概率。
解题思路:
根据古典概型,概率等于有效事件数目除以基本事件总数。
- 基本事件总数:从15张纸币中任取4张的所有组合数。公式为:
C(15, 4) - 有效事件数目:抽取的4张纸币中,每种面值至少一张。由于只有4张纸币,必然有一种面值取了两张,另外两种各取一张。以下是所有可能情况:
- 5元取两张,10元和20元各取一张:
C(6, 2) * C(5, 1) * C(4, 1) - 10元取两张,5元和20元各取一张:
C(5, 2) * C(6, 1) * C(4, 1) - 20元取两张,5元和10元各取一张:
C(4, 2) * C(6, 1) * C(5, 1)
将三种情况的组合数相加,即得到有效事件总数。
- 5元取两张,10元和20元各取一张:
最终概率为:P = (有效事件总数) / (基本事件总数)。
核心要点:遇到复杂概率问题时,将事件分解为互斥的简单情况,分别计算后求和,是常用的解题策略。
📊 第二题:寻找绝对众数
在掌握了基础的概率计算后,我们进入数据结构与算法的领域。本节中我们来看看如何高效地寻找数组中的“绝对众数”。
概念定义:
- 众数:在一组数据中出现次数最多的数值。
- 绝对众数:在一组长度为N的数据中,出现次数大于 N/2 的数值。绝对众数如果存在,则必然是唯一的。
题目要求:给定一个存在绝对众数的整数数组,设计一个时间复杂度为O(N),空间复杂度为O(1)的算法来找出这个绝对众数。
算法思路(Boyer-Moore投票算法):
算法的核心思想是“对消”。由于绝对众数出现的次数超过总数的一半,那么即使它与其他所有不同的数进行一对一的“对消”,最后剩下的也必然是它。
- 初始化候选值
candidate和计数器count = 0。 - 遍历数组中的每个数字
num:- 如果
count == 0,则将当前num设为candidate。 - 如果
num == candidate,则count++。 - 如果
num != candidate,则count--(模拟一次对消)。
- 如果
- 遍历结束后,
candidate即为可能的绝对众数。由于题目保证存在,可直接返回。若不保证存在,则需要再次遍历验证其出现次数是否大于 N/2。
代码示例:
int majorityElement(int* nums, int numsSize) {
int candidate = 0;
int count = 0;
for (int i = 0; i < numsSize; i++) {
if (count == 0) {
candidate = nums[i];
count = 1;
} else if (nums[i] == candidate) {
count++;
} else {
count--;
}
}
// 题目假设存在绝对众数,否则需要二次验证
// int verifyCount = 0;
// for (int i = 0; i < numsSize; i++) if (nums[i] == candidate) verifyCount++;
// return (verifyCount > numsSize / 2) ? candidate : -1; // 假设-1表示不存在
return candidate;
}
算法扩展:此算法思想可以推广到寻找出现次数超过 N/K 的所有元素的问题,此时需要维护 K-1 个候选值。
🔗 第三题:链表的部分反转
链表操作是面试中的常客,它考察的是对指针的掌控能力和编码基本功。本节我们学习链表的部分反转。
题目描述:给定一个单链表和两个整数 m 和 n(1 ≤ m ≤ n ≤ 链表长度),要求反转从第 m 个节点到第 n 个节点的子链表。要求原地反转,且只遍历一次。
解题思路(头插法):
- 创建一个虚拟头节点
dummyHead,其next指向原链表头。这样可以统一处理,避免对头节点的特殊判断。 - 找到第 m-1 个节点
pre,即需要反转部分的前一个节点。 - 定义两个指针
cur和next。初始时,cur指向第 m 个节点(即pre->next)。 - 循环执行 n-m 次,将
cur的下一个节点next插入到pre之后:cur->next = next->next;// cur 跳过 nextnext->next = pre->next;// next 插入到 pre 之后pre->next = next;// 更新 pre 的链接
- 返回
dummyHead->next。
图解过程(以链表 1->2->3->4->5, m=2, n=4 为例):
初始:dummy->1->2->3->4->5, pre=1, cur=2
第一步:将3插入到1之后:dummy->1->3->2->4->5, cur=2, next=4
第二步:将4插入到1之后:dummy->1->4->3->2->5
完成。
核心要点:头插法是原地反转链表的有效技巧,关键在于维护好 pre、cur、next 三个指针的关系。
🧩 第四题:逻辑推理与“金钗赛诗”
算法面试不仅考察代码能力,也考察逻辑思维。本节我们通过一个改编自Google面试题的逻辑问题来锻炼推理能力。
问题背景(金钗赛诗会):
十二金钗每人写了一首诗,但随机拿错了别人的诗。她们只能通过与“李纨”交换来换回自己的诗(即任何两人不能直接交换,必须经由李纨中转)。给定每个人当前手持的诗的作者,求最少需要多少次交换,能使所有人拿回自己的诗。
问题抽象:
将十二个人和十二首诗看作一个排列。每个人指向他手中诗的作者。这个排列会形成若干个环(包括自环)。例如,A拿了B的诗,B拿了C的诗,C拿了A的诗,三人形成一个环。
- 包含李纨的环:假设环中有 k 个人(含李纨)。通过 k-1 次交换(李纨依次与环中其他人交换),即可让所有人归位。
- 不包含李纨的环:假设环中有 k 个人。需要先将其中一人与李纨交换(1次),将李纨引入环,此时环变为 k+1 人且包含李纨,再通过 (k+1)-1 = k 次交换使该环所有人归位,最后李纨再与最初交换那人换回(1次)。总计 k+2 次。
算法步骤:
- 遍历所有人,标记已访问。
- 对每个未访问的人,沿着“手持诗的作者”指针向下走,直到回到起点,形成一个环,并统计环的大小
cycleSize。 - 如果环的大小为1(自环),无需交换,贡献为0。
- 如果环中包含李纨,该环的交换次数贡献为
cycleSize - 1。 - 如果环中不包含李纨,该环的交换次数贡献为
cycleSize + 1。 - 将所有环的贡献值相加,即为最少交换次数。
核心要点:将实际问题抽象为图论中的环检测问题,并分析不同环结构下的最优交换策略。
🤔 第五题:猜数字游戏与推理树
另一个锻炼逻辑思维的经典题目是“猜数字”游戏。这类问题通常没有固定算法,但可以通过构建“推理树”来辅助分析。
题目描述:A和B额头上各贴一个正整数,两数相差1。他们只能看到对方的数。对话如下:
A:“我不知道我的数是多少。”
B:“我也不知道我的数是多少。”
A:“现在我知道了。”
B:“我也知道了。”
请问A和B头上的数字各是多少?
推理思路:
- 从最小正整数开始推理。如果B看到A的数是1,那么B立刻可以知道自己的数是2(因为正整数且差1,不能是0)。所以,如果A是1,第一句“我不知道”就不成立。因此A不是1。
- 同理,如果A看到B的数是1,A也能立刻知道自己是2。所以,如果B是1,那么A在第一句就不会说“我不知道”。因此,在A说“我不知道”时,B就知道自己不是1。
- 继续向后模拟。当A看到B是2时,A会想:“我可能是1或3。如果我是1,B会看到1,根据步骤1,B会立刻知道自己是2并说出来。但B第一句说的是‘不知道’,所以我不能是1。因此,我一定是3。”
- 通过这种递归式的“如果我是X,对方会如何推理”的思路,可以最终推出唯一解。构建一棵“推理树”可以帮助清晰地展现所有可能性和被排除的分支。
答案:A是3,B是2。(或对称情况A是2,B是3,取决于谁先说话)
核心要点:解决此类问题需要仔细分析每一句话给对方带来的信息增量,并进行递归推理。
🦘 第六题:跳跃游戏(Jump Game)的最小步数
最后,我们来看一个融合了贪心、广度优先搜索(BFS)和动态规划(DP)思想的经典题目。
题目描述:给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个下标。假设你总是可以到达数组末尾。
示例:nums = [2,3,1,1,4]
从下标0(值2)出发,可以跳到下标1或2。最优路径是跳1步到下标1(值3),再跳3步到达下标4。最少需要2步。
解题思路(贪心法):
我们不需要精确地模拟每一步跳到哪个位置,而是维护当前能够到达的边界。
- 初始化
steps = 0(跳跃次数),curEnd = 0(当前步数下能到达的最远位置),curFarthest = 0(从当前区间内所有点出发,能到达的最远位置)。 - 遍历数组(除了最后一个元素):
- 更新
curFarthest = max(curFarthest, i + nums[i])。 - 如果遍历到
i == curEnd,说明已经走完了上一跳所能覆盖的所有范围,必须进行下一次跳跃:steps++curEnd = curFarthest// 将下一次跳跃的覆盖范围更新为当前能到达的最远点
- 更新
- 返回
steps。
代码示例:
int jump(int* nums, int numsSize) {
int steps = 0;
int curEnd = 0;
int curFarthest = 0;
for (int i = 0; i < numsSize - 1; i++) { // 注意不用走到最后一个元素
curFarthest = (curFarthest > i + nums[i]) ? curFarthest : i + nums[i];
if (i == curEnd) {
steps++;
curEnd = curFarthest;
if (curEnd >= numsSize - 1) break; // 提前结束
}
}
return steps;
}
算法视角:
- 贪心:每一步都在当前可达范围内选择能跳最远的那个点作为下一跳的起跳点之一(隐式选择)。
- BFS:可以将每次
curEnd的更新看作是一层遍历,steps就是层数,求的是从起点到终点的最短路径层数。 - DP:可以定义状态 dp[i] 为到达 i 的最小步数,但有更优的贪心解法。
时间复杂度:O(N),因为每个元素只被访问一次。
🎯 课程总结
本节课我们一起学习了六类在算法面试中常见的题目:
- 古典概型计算:通过分情况讨论,将复杂事件分解求和。
- 寻找绝对众数:利用Boyer-Moore投票算法,以O(N)时间和O(1)空间高效解决。
- 链表部分反转:使用虚拟头节点和头插法,统一逻辑,清晰编码。
- 逻辑推理(金钗赛诗):将交换问题抽象为环分析,找到最优策略。
- 逻辑推理(猜数字):通过分析对话的信息增量,进行递归推理。
- 跳跃游戏:运用贪心思想,维护当前可达边界,以最小步数到达终点。


掌握这些题目的核心思想与解题技巧,能够帮助你在面试中更加从容地应对算法挑战。记住,理解思路比死记代码更为重要。

人工智能—面试求职公开课(七月在线出品) - P12:字符串高频面试题精讲 📝
在本节课中,我们将学习字符串相关的高频面试题。课程将从字符串的基本概念讲起,梳理常见题型,并通过五个典型例题深入讲解解题思路与技巧,最后进行总结。
字符串简介
字符串通常被视为字符数组。不同编程语言对字符串的处理方式有所不同。
在Java中,String是内置的不可变类。如需修改字符串,可使用StringBuffer、StringBuilder类,或使用toCharArray()方法将其转换为字符数组再操作。
在C++中,标准库提供了std::string类,它是可变的字符串。也可以直接使用char数组,两者在操作上差别不大,因为可以通过中括号[]直接访问和修改特定位置的字符。
在C语言中,只有字符数组,没有内置的字符串类。
以下是三个值得注意的要点:
- C++字符串连接符的复杂度:C++中,使用加号
+进行字符串连接的复杂度通常被认为是线性的。在循环中频繁使用字符串连接可能导致复杂度退化为O(n²)。例如,构建一个从a到z的字符串,若在循环中不断使用+,复杂度约为26²;若使用字符数组预先赋值,则复杂度为26。 - C++的
substr函数:substr函数的第二个参数是子串的长度,而非终点索引。这与Java的substring函数(参数为起点和终点,左闭右开区间)行为不同。 - 字符范围与统计:统计字符串中各字符出现次数时,常直接将字符作为数组下标。在C/C++中,
char通常为8位(ASCII码,范围0-255),数组大小需256。在Java中,char为16位Unicode编码(范围0-65535),数组大小需65536。
字符串面试题总体分析
字符串与数组密切相关,涉及数组的题目常可转化为字符串问题。字符串面试题主要分为以下几类:
- 简单操作:包括插入、删除、修改字符,以及字符串旋转等。
- 规则判断:判断字符串是否符合特定规则,如实现
atoi(字符串转整数)、判断是否为合法浮点数、罗马数字与阿拉伯数字转换等。 - 数字运算:用字符串模拟大数(超出内置整数类型范围)的加、减、乘、除运算,例如二进制加法。
- 排序交换:涉及数组的排序与交换操作,典型如快速排序中的
partition过程。 - 字符计数:统计字符串中各类字符的出现次数,常用于解决变位词判断等问题。变位词是指字母组成相同但顺序不同的单词,如
abc和bac。 - 匹配:包括正则表达式匹配、子串匹配等。子串匹配的暴力解法复杂度较高,KMP算法是经典的高效解法,还可用于判断字符串是否为周期串。
- 动态规划:常见问题有最长公共子序列(LCS)、最长公共子串、编辑距离、最长回文子串等。
- 搜索:字符串可视为字符数组,因此可进行排列、组合等搜索操作,单词变换也是典型的搜索问题。
例题精讲
上一节我们梳理了字符串面试题的常见类型,本节中我们通过具体例题来深入理解其中的核心思想。
例题一:01串排序 🔢
题目:给定一个只包含0和1的字符串,只能通过交换任意两个位置的操作对其进行排序,问至少需要交换多少次才能使其有序(所有0在前,1在后)。
思路:此问题本质是快速排序中的partition过程。我们使用双指针法,一个指针i从左向右扫描找到第一个1,另一个指针j从右向左扫描找到第一个0,然后交换它们,并记录交换次数。
代码:
int minSwaps(string s) {
int n = s.length();
int i = 0, j = n - 1;
int count = 0;
while (i < j) {
while (i < j && s[i] == '0') i++;
while (i < j && s[j] == '1') j--;
if (i < j) {
count++;
i++;
j--;
}
}
return count;
}
复杂度分析:指针i只增不减,j只减不增,总共遍历字符串一次,时间复杂度为O(n)。
例题二:字符的替换与复制 📋
题目:给定一个足够大的字符数组,要求删除其中所有的a,并复制其中所有的b。
思路:
- 删除
a:使用双指针,一个指针i遍历原数组,另一个指针n指向新数组的当前位置。当s[i]不是a时,将其复制到s[n],然后n++。遍历完成后,s[0..n-1]即为删除所有a后的字符串。 - 复制
b:先统计b的个数countB。新字符串长度为n + countB。从后向前(倒着)复制字符:设置指针i指向新字符串末尾,j指向旧字符串末尾。将s[j]复制到s[i],如果s[j]是b,则再复制一个b。这样可以避免覆盖尚未处理的字符。
倒着复制的核心思想:当需要在数组中部插入元素时,从后向前处理可以确保源数据不会被目标数据覆盖,这是memcpy等函数处理内存重叠区域时的经典策略。
思考题:如何将字符串中的空格替换为%20?同样可以采用倒着复制的策略。
例题三:星号与数字的重排 ✨
题目:给定一个只包含*和数字的字符串,要求将*移到前面,数字移到后面。有两种要求:
- 不要求保持数字的相对顺序。
- 要求保持数字的相对顺序。
思路:
- 方法一(不保持顺序):使用快排
partition思想。维护循环不变式:[0, i-1]全是*,[i, j-1]全是数字,[j, n-1]是未处理字符。遍历时,遇到*就与s[i]交换,并移动i。此方法通过交换实现,但会改变数字顺序。 - 方法二(保持顺序):使用倒着复制的思想。从后向前遍历,将数字依次放到数组尾部,最后将数组前部剩余位置填充为
*。此方法保持了数字顺序,但不是纯粹的交换操作。
面试提示:遇到此类题目,应主动询问面试官是否需要保持数字顺序,以及是否允许使用非交换操作。
例题四:子串变位词(滑动窗口) 🪟
题目:给定两个字符串A和B,判断B是否是A的某个子串的变位词。
思路:采用滑动窗口结合字符计数的方法。假设字符串只包含小写字母。
- 统计
B中每个字母的出现次数,存入数组count(长度26)。同时维护变量nonZero,记录count中非零元素的个数。 - 在
A上设置一个长度为len(B)的滑动窗口。初始时,将A的前len(B)个字符纳入窗口,更新count数组(减去窗口中字符的计数),并相应更新nonZero。若此时nonZero为0,则找到变位词。 - 滑动窗口:窗口向右移动一位时,需要“移除”原窗口最左字符(在
count中加回其计数),“加入”新窗口最右字符(在count中减去其计数),并更新nonZero。每次移动后检查nonZero是否为0。
代码关键:
vector<int> count(26, 0);
int nonZero = 0;
// 1. 初始化B的计数
for (char c : B) {
int idx = c - 'a';
if (count[idx] == 0) nonZero++;
count[idx]++;
if (count[idx] == 0) nonZero--; // 此情况本例不会出现
}
// 2. 初始化A的第一个窗口
for (int i = 0; i < B.length(); i++) {
int idx = A[i] - 'a';
count[idx]--;
if (count[idx] == 0) nonZero--;
else if (count[idx] == -1) nonZero++; // 从0变为-1
}
if (nonZero == 0) return true;
// 3. 滑动窗口
for (int i = B.length(); i < A.length(); i++) {
int left_idx = A[i - B.length()] - 'a'; // 移除左端字符
count[left_idx]++;
if (count[left_idx] == 0) nonZero--;
else if (count[left_idx] == 1) nonZero++;
int right_idx = A[i] - 'a'; // 加入右端字符
count[right_idx]--;
if (count[right_idx] == 0) nonZero--;
else if (count[right_idx] == -1) nonZero++;
if (nonZero == 0) return true;
}
return false;
复杂度:窗口每次滑动是O(1)操作,整体时间复杂度为O(n)。
思考题:LeetCode第3题“无重复字符的最长子串”,同样可以使用滑动窗口配合字符计数来解决。
例题五:翻转句子中的单词 🔄
题目:翻转句子中单词的顺序,但保持每个单词内部的字符顺序不变。例如,"I am a student" 翻转为 "student a am I"。
思路:借助一个辅助函数reverse(s, i, j),用于翻转字符串s中从索引i到j(闭区间)的子串。
- 翻转整个句子:
reverse(s, 0, n-1)。此时单词顺序和每个单词的字母顺序都被翻转。 - 识别每个单词的边界(空格分隔),对每个单词再次调用
reverse函数翻转回来。
核心技巧:通过两次翻转(整体翻转+局部翻转)达到目的,时间复杂度为O(n)。
扩展:字符串循环移位:将字符串ABCD左移2位得到CDAB。
- 翻转前
m位:AB→BA - 翻转后
n-m位:CD→DC - 整体字符串变为
BADC - 翻转整个字符串:
BADC→CDAB
同样运用了翻转的思想。
课程总结 🎯
本节课我们一起学习了字符串处理中的高频面试题。
我们首先理解了“原地操作”(in-place)的含义,它通常指利用输入数据自身的空间完成操作,而不一定严格是O(1)的额外空间。相关的例题包括字符串循环移位和快排partition过程。
我们重点掌握了滑动窗口技巧,它能够以O(n)的时间复杂度高效解决子串匹配、变位词判断等问题,其空间复杂度通常为O(1)(这里指字符集大小是常数)。
此外,课程还提及了其他重要题型:
- 规则判断类(如
atoi):考察细致的编程实现能力。 - 匹配类:笔试面试中通常允许使用暴力解法,直接实现KMP等复杂算法的情况较少。
- 动态规划类(如最长回文子串):有经典的线性算法,但实现难度较高。


希望本课程能帮助大家更好地理解和应对字符串相关的面试挑战。

人工智能—面试求职公开课(七月在线出品) - P13:半小时掌握DP的基本套路 🚀

在本节课中,我们将要学习动态规划(Dynamic Programming,简称DP)的核心思想与基本实现套路。动态规划是算法面试中的高频考点,其本质并不复杂。我们将通过一个具体的例题,从递归解法入手,逐步引入缓存优化,最终掌握动态规划的通用解题思路。
课程概述 📖
动态规划在大学算法课中可能较少涉及,但却是公司面试的常考内容。这并非因为其难度,而是因为其核心思想——“加缓存”——在实际的大规模系统开发中至关重要。本节课的目标是让大家能够独立写出动态规划的代码。
学习本课的前置技能是理解递归和基本的深度优先搜索(回溯法)。动态规划的本质可以理解为 递归 + 缓存。
从递归到动态规划:以“打家劫舍”为例
我们以力扣(LeetCode)第198题“打家劫舍”作为示例。题目描述如下:你是一个专业的小偷,计划偷窃一条街上的房屋。每间房内都藏有一定的现金。唯一的约束是:如果两间相邻的房屋在同一晚上被偷,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
第一步:递归(暴力搜索)思路
面对此题,第一反应不应是动态规划,而应是尝试所有可能的偷窃方案,即求所有满足“不相邻”条件的房屋子集。这自然引向深度优先搜索(DFS)。
定义搜索过程(状态):我们定义一个递归函数,其返回值是从第 i 个房屋开始偷窃,能获得的最大金额。参数 i 标识了当前要处理的“子问题”起点,这是区分不同任务的关键。
拆解问题(决策):对于第 i 个房屋,我们有两种选择:
- 偷:获得
nums[i]的金额,但下一间房i+1不能偷,因此下一个子问题从i+2开始。 - 不偷:获得
0金额,下一间房i+1可以偷,因此下一个子问题从i+1开始。
原问题(从 i 开始)的解,就是这两种决策结果的最大值。这体现了 最优子结构:子问题的最优解能构成原问题的最优解。
边界条件:当 i 超过数组末尾时,无可偷房屋,返回 0。
根据以上分析,我们可以写出递归代码框架:
def rob(nums, i):
if i >= len(nums): # 边界条件
return 0
# 决策:偷 vs 不偷
steal = nums[i] + rob(nums, i+2)
not_steal = rob(nums, i+1)
return max(steal, not_steal)
然而,直接提交此递归解法会超时。为什么呢?
第二步:识别重叠子问题与加缓存
我们画出上述递归的调用树(以 i=0 为例):
rob(0)
/ \
偷(0) / \ 不偷(0)
/ \
rob(2) rob(1)
/ \ / \
rob(4) rob(3) rob(3) rob(2)
可以清晰地看到,rob(2)、rob(3) 等函数被重复计算了多次。这就是 重叠子问题。重复计算导致时间复杂度呈指数级增长。
解决方案:加缓存(记忆化搜索)。我们用一个字典(Map)来存储已经计算过的子问题结果(i → 最大金额)。在递归函数开始时,先检查缓存中是否有答案;在函数返回前,将计算结果存入缓存。
memo = {}
def rob(nums, i):
if i >= len(nums):
return 0
if i in memo: # 取缓存
return memo[i]
# 计算
steal = nums[i] + rob(nums, i+2)
not_steal = rob(nums, i+1)
res = max(steal, not_steal)
memo[i] = res # 存缓存
return res
加上缓存后,每个子问题只计算一次,时间和空间复杂度都优化为 O(n)。这种方法称为 记忆化搜索(Memoization),是自顶向下的动态规划。
第三步:总结动态规划的特征与套路
通过上面的例子,我们可以总结出动规问题的共性:
- 问题类型:通常是求最优化问题(最大、最小、最长、计数等)。
- 问题性质:问题是离散的,状态可以用有限个参数(如整数索引)描述。
- 最优子结构:问题的最优解可以由其子问题的最优解推导出来(
N的解可由N-1或N-2等的解推导)。 - 重叠子问题:递归算法会反复求解相同的子问题。
动态规划的基本步骤:
- 定义状态:用一组参数明确地表示一个子问题。例如
dp[i]表示从第i间房开始偷能获得的最大金额。 - 确定状态转移方程:找出状态之间的关系。例如
dp[i] = max(nums[i] + dp[i+2], dp[i+1])。 - 确定初始(边界)条件:例如
dp[n] = 0(n为房屋总数,超出范围)。 - 计算顺序:自底向上(从边界往目标状态递推)或自顶向下(记忆化搜索)。
- 空间优化(可选):根据状态转移方程,有时可以压缩存储空间。
对于“打家劫舍”问题,自底向上的递推写法如下:
def rob(nums):
n = len(nums)
if n == 0:
return 0
# dp[i] 表示从第 i 间房开始(0-indexed)偷,能获得的最大金额
dp = [0] * (n + 2) # 多分配两位,方便处理 i+2 的边界
for i in range(n-1, -1, -1): # 从后往前递推
dp[i] = max(nums[i] + dp[i+2], dp[i+1])
return dp[0]
观察发现,dp[i] 只依赖于 dp[i+1] 和 dp[i+2],因此可以进一步优化空间:
def rob(nums):
prev, curr = 0, 0 # 分别代表 dp[i+2], dp[i+1]
for num in nums:
# 计算当前的 dp[i]
new = max(num + prev, curr)
prev, curr = curr, new # 滚动更新
return curr
本节课总结 🎯

本节课我们一起学习了动态规划的基本套路:
- 核心思想:动态规划 = 递归 + 缓存,其精髓在于用空间(存储子问题解)换取时间(避免重复计算)。
- 解题起点:面对最优化问题时,先尝试用递归(DFS)暴力搜索的思路去定义状态和决策。
- 优化关键:识别递归树中的重叠子问题,通过加缓存(记忆化) 来优化。
- 实现形式:可以采用自顶向下的记忆化搜索,也可以转化为自底向上的递推(迭代) 写法。
- 适用特征:问题具有离散状态、最优子结构和重叠子问题。


记住,掌握“定义状态”和“写出状态转移方程”是解决动态规划问题的关键。从递归思考入手,再优化为动规,是一条清晰可靠的路径。
人工智能面试求职公开课(七月在线出品) - P14:面对失败如何继续坚持? 🧭
概述
在本节课中,我们将跟随一位从Java开发成功转型为算法工程师的分享者,学习其转行的心路历程、系统学习算法的方法、面试求职的核心技巧以及工作后的真实感受。课程旨在为初学者和转行者提供一份实用的行动指南。
一、 为什么要转行做算法? 💰
上一节我们概述了课程内容,本节中我们来看看分享者转行的核心动机。
分享者最初是Java开发,后转向大数据开发,最终因机器学习火热而决定转行算法。其转行的最主要原因是追求高薪。算法工程师的薪资普遍较高,且当时行业门槛相对开发岗位而言并非高不可攀,对于希望突破薪资瓶颈的开发者是一个机会。
此外,算法领域聚集了许多聪明且毕业于名校的人才,整体行业缺口较大,公司招聘需求旺盛。从政府、企业到高校都在积极推动AI发展,创造了大量入门级算法岗位。
核心观点:转行算法的主要驱动力是薪资高、行业趋势好以及人才缺口大。
二、 如何系统学习算法? 📚
了解了转行动机后,本节我们重点探讨如何有效地学习算法知识。以下是几个关键的学习方向。
1. 夯实代码能力:数据结构与LeetCode刷题
代码能力是面试的第一道门槛。许多公司面试首先会考察LeetCode题目,如果代码写不好,第一印象会大打折扣。
建议:
- 养成每日刷题的习惯。
- 重点掌握剑指Offer中的题目。
- 在LeetCode上练习简单题20-30道,中等题10道,并尝试理解少量难题。
- 理解算法原理,避免死记硬背代码。
2. 深入理解算法理论
学习不能只停留在表面。对于每个算法,不仅要会推导,更要理解其背后的设计思想。
学习方法:
- 多问为什么:例如,逻辑回归为什么用L1正则?L1和L2正则的区别是什么?
- 对比与思考:比较不同模型(如LR vs XGBoost)的优劣、适用场景及过拟合倾向。
- 掌握核心推导:能手推关键算法(如SVM、XGBoost)的公式,并理解其每一步的意义。
3. 积累项目与实践经验
理论知识需要项目来落地。对于缺乏工作经验的求职者,项目经验尤为重要。
获取经验的途径:
- 参加算法比赛:在Kaggle、天池等平台参赛,好的名次是简历上的亮点。
- 动手跑项目代码:学习课程后,务必亲自运行和修改项目代码。
- 深入业务场景:思考如何将算法应用到具体业务中,并解决可能出现的Bad Case。
4. 培养解决实际问题的思路
面试官非常看重候选人主动思考和解决问题的能力。
考察点:
- 能否清晰描述项目背景、所用模型及取得的性能指标。
- 能否识别项目中的Bad Case,并提出系统性的优化思路。
- 能否阐述选择某个模型的原因,以及后续的迭代方向。
核心公式:成功的学习 = 扎实的代码基础 + 深入的算法理解 + 丰富的项目实践 + 解决问题的思路
三、 工作后的感受与建议 💼
掌握了学习方法后,我们来看看实际工作中的挑战与收获。
- 学习永无止境:工作中所需的知识远超算法本身,还包括对数据的深刻理解、工程化能力以及业务知识。
- 理论与实践的鸿沟:很多理论上有效的模型(如TF-IDF, Word2Vec)在实际业务中可能效果不佳,需要通过实验不断调整和优化。
- 抗压与加班:互联网行业节奏快,需求紧急,加班是常态,需要具备良好的抗压能力和高效的时间管理能力。
- 算法与工程的结合:算法工程师的“工程”更多指特征工程、样本处理等数据层面的工作,与传统的业务开发有所区别。
四、 求职面试的实战技巧 🎯
理论学习最终要服务于求职。本节汇总关键的面试技巧。
关于简历
- 突出亮点:深入掌握1-2个算法,形成自己的知识深度,这比泛泛而谈更有吸引力。
- 项目经验:如果没有公司项目,可以用比赛经历或从技术公众号、论坛上研究的开源项目框架来充实简历。
- 诚实为上:只写自己熟悉的内容,避免被深入问到时无法回答。
关于面试
- 自信表达:面试时不要胆怯,清晰的表达能力和强大的内心同样被考察。
- 坚持薪酬预期:认定合理的薪资范围后要自信坚持,不要轻易妥协。
- 展现思考过程:回答问题时,可以先从数学或业务角度阐述思路,再给出解决方案,这能展现你的思维能力。
- 多投多面:不要因几次失败而气馁。年底(金三银四之外)可能是求职的好时机,因为招聘方也有KPI压力,可能会放宽要求。
关于刷题
以下是推荐的刷题策略:
- 基础必备:刷完《剑指Offer》所有题目,对不熟的题目反复练习。
- 进阶提升:在LeetCode上按难度梯度刷题(简单→中等→困难)。
- 核心算法:务必掌握堆排序、快速排序、二分查找、动态规划(如背包问题) 等经典算法。
代码示例(快速排序思想):
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
五、 常见问题答疑 ❓
本节针对分享中提到的常见疑问进行集中解答。
Q:非硕士/博士,本科能找到算法工作吗?
A:可以。学历是门槛之一,但能力更重要。需用扎实的代码能力、项目经验和面试表现来弥补。Q:转行没有实际项目经验怎么办?
A:参加算法比赛是最佳途径之一。比赛经历完全可以作为项目经验写在简历上。Q:面试会问很偏的算法吗(如图优化)?
A:通常不会。面试问题更贴近实际业务应用,如特征工程、模型选择、Bad Case解决思路等。Q:大数据背景转推荐算法有何建议?
A:非常有优势。需熟悉Hive、Spark等大数据工具,并学习如何将算法(如XGBoost的Java包)与大数据平台结合。Q:工作中Python和Java哪个用的多?
A:训练模型、数据分析多用Python;工程上线、服务部署多用Java。需要根据场景灵活掌握。
总结
本节课中,我们一起学习了从开发转型算法工程师的完整路径。核心在于:明确转行目标(高薪与趋势),构建系统学习体系(代码、理论、项目、思路),掌握求职面试技巧(简历、表达、刷题),并做好迎接工作挑战(持续学习、抗压) 的心理准备。记住,找工作是一个不断尝试和积累信心的过程,多投递、多面试、多总结,你一定能找到心仪的岗位。祝大家求职顺利!



给弟们再见。


人工智能—面试求职公开课(七月在线出品) - P15:面试绝技 📝
在本节课中,我们将要学习如何为技术岗位的校招面试做准备。课程内容涵盖简历撰写、算法刷题、面试技巧以及知识广度积累等多个方面,旨在帮助应届生提升面试成功率。
概述
本节课将系统性地讲解技术面试的完整流程与核心技巧。我们将从简历准备开始,逐步深入到算法实践、面试沟通以及职业规划,为你提供一套实用的求职策略。
简历准备:你的第一张名片
上一节我们介绍了课程的整体框架,本节中我们来看看如何打造一份出色的简历。简历是面试官对你的第一印象,至关重要。
一份优秀的简历应该简洁、重点突出。面试官通常只会花很短的时间浏览,因此需要将最核心的信息放在最前面。
以下是撰写简历时需要关注的核心要点:
- 内容精炼:简历应控制在一页A4纸内,中文和英文各一份。字号不宜过小,避免内容过于拥挤。
- 突出亮点:按重要性排序,应优先展示实习经验、学校和专业成绩、在校项目经历。
- 实习经验:优质的实习经历是强有力的背书,可能让你免去笔试或电话面试环节。
- 专业成绩:对于没有实习经验的同学,优异的专业成绩(如专业排名)是证明学习能力的重要依据。实践表明,高分高能是普遍现象。
- 项目经历:可以展示课程大作业或个人项目,说明用到的算法和解决的问题。
- 其他信息:除非是学生会主席/副主席等能体现管理能力的职位,或外貌特别出众,否则类似学生会普通成员、寝室长等经历无需写入。高中学校除非是顶尖名校,否则也无需提及。
- 学历权重:在计算机领域,硕士学历相对于本科并无绝对优势,公司更看重实际能力和项目经验。强大的自学能力是关键。
一份专业的英文简历同样重要,建议请英语专业的同学帮忙审阅或利用网络资源(如Bing搜索)学习地道的表达方式。
算法刷题:从思路到代码的跨越
准备好简历后,下一步是夯实技术基础,而算法刷题是其中不可或缺的一环。很多同学懂思路,但无法流畅地转化为无bug的代码,这需要通过大量练习来解决。
刷题的目的不仅是做出题目,更是为了深入理解数据结构和语言特性,并培养良好的工程习惯。
以下是刷题的具体建议与步骤:
- 平台选择:推荐使用 LeetCode 或 LintCode。
- 语言选择:首选 Python 或 C++。Java 也是互联网公司的常用语言。面试时一般不对语言做硬性要求,但需保证熟练度。
- 练习方法:
- 遇到难题时,不要死磕,应主动搜索答案。
- 理解答案后,自己动手复现并调试通过。
- 间隔一段时间后,尝试不看答案独立完成。
- 目标设定:
- 对于 BAT 普通部门,能独立在20-30分钟内解决中等难度题目即可。
- 对于 微软亚洲研究院、阿里核心算法团队等,需要能应对 Hard 难度的题目。
- 代码质量:刷题时需注意代码风格、可读性和可维护性。要思考资源管理(如C++中的内存泄露)、异常处理等工程问题。
- 心理建设:起步阶段可能很慢,这是正常过程。坚持练习,速度和能力都会提升。
仅仅做出题目并不够,在工程实践中,代码的可维护性与团队协作效率同样重要。

面试实战:沟通与编程技巧

当简历和刷题准备就绪,真正的挑战在于面试现场的发挥。本节我们将探讨面试中的沟通与白板编程技巧。
面试是综合能力的考察,良好的沟通和严谨的编程习惯能为你大大加分。
自我介绍
自我介绍是固定环节,需要提前准备并反复练习。
- 时间控制:时长控制在5分钟左右。
- 内容重点:言简意赅,突出个人优势、技术强项和做过的“牛事”(如主导项目架构)。
- 表达方式:放慢语速,说好普通话。清晰的表达是重要的职场竞争力。
白板编程
白板编程是技术面试的核心,考察临场问题解决能力。
以下是白板编程的标准化流程:

- 确认题意:不要立即动笔。先与面试官反复确认题目要求、输入输出和边界条件,即使题目看似熟悉也要谨慎。
- 阐述思路:先口头或书写伪代码,说明解题思路。即使不是最优解,也应先给出一个可行的基础方案。
- 优化迭代:面试官通常会追问优化空间。此时可结合提示,讨论如何改进算法(如时间/空间复杂度)。
- 编写代码:在思路清晰的基础上编写代码。注意代码结构清晰。
- 检查边界:完成代码后,主动列举测试用例,覆盖各种边界情况(如空输入、单个元素、已排序/逆序等)。
- 讨论测试:向面试官说明你的测试用例设计思路。在实际工作中,编写测试代码是开发的重要部分,这项技能会为你加分。
关键禁忌:切忌依赖在线判题系统的反馈来反复修改代码。在白板面试中,你需要具备“人肉调试”的能力,在心中模拟代码执行过程,争取一次写对。
开放性问题与知识广度
顺利通过编程考核后,面试官可能会探讨一些开放性问题,这考验你的知识储备和思维深度。




技术之路并非只有编码,对行业和技术的宏观思考同样重要。开放性问题没有标准答案,旨在考察你的学习能力和思维广度。
应对开放性问题的策略如下:
- 日常积累:不要局限于教科书。多关注行业动态、技术博客(如阮一峰、左耳朵耗子)、框架优缺点(如Spring, 微服务)、技术争议帖。
- 逻辑自洽:回答时不怕说错,但要保证逻辑严密,能自圆其说。例如,被问到如何设计一个IM系统,即使方案不实用,但只要你能清晰论证其优劣,并关联到已知技术(如点对点连接 vs 服务器中转,区块链的去中心化思想),就能体现思考能力。
- 深入本质:关注技术演变的本质原因,理解不同技术方案背后的权衡(如工程效率 vs 执行效率)。
职业规划与其他建议
最后,面试官可能会询问你的职业规划。这是一个表达个人志向的机会。
职业规划问题没有标准答案,但态度要诚恳积极。



- 如实回答:无论是想走技术专家路线还是管理路线,都可以直接表达。
- 展示规划:可以简要说明你计划如何通过学习技术和积累经验来实现目标。
- 展现热情:表达对技术和行业的热情,但避免说“我进来以后会努力学习”之类的话,这暗示你入职前准备不足。公司希望你入职即能产生价值。






此外,还有一些重要的补充建议:
- 经典教材:阅读英文原版技术书籍(如《Thinking in C++》),通常比中文译本更清晰。
- 技术博客:定期阅读业内大牛的博客(如韩小阳的机器学习博客,左耳朵耗子的技术博客)以拓宽视野。
- 个人品牌:将学习总结、项目代码整理到 GitHub 或个人博客上,这会是简历上的亮点。
- 语言态度:不要有强烈的语言偏好,应表达“公司需要什么,我就学什么”的积极态度。
- 忌讳之言:切勿在面试测试开发岗时说“我代码写得不好,所以来做测试”。测试开发岗同样需要扎实的编码能力。
总结

本节课中我们一起学习了技术校招面试的完整攻略。我们从简历撰写开始,明确了如何打造简洁有力的个人名片;接着深入算法刷题,掌握了从理解到熟练的练习方法;然后进入面试实战,学习了自我介绍、白板编程的沟通与解题技巧;最后探讨了如何应对开放性问题并规划职业发展。

记住,面试是双向选择的过程。充分准备,展示真实的自己,同时保持开放学习的心态,是成功的关键。祝大家在即将到来的面试季中收获理想的Offer!

人工智能面试求职公开课(七月在线出品) - P16:应届毕业生的面试历程 🎓
在本节课中,我们将跟随一位从机械专业成功转型算法岗的应届毕业生的视角,回顾他在秋招过程中的真实经历、遇到的挑战以及总结出的宝贵经验。课程将涵盖从前期准备、面试实战到心态调整的全过程,旨在为有志于进入人工智能领域的同学提供一份实用的求职指南。
概述
本节课将分享一位非科班背景学生的算法岗求职历程。内容主要包括自我介绍与转行契机、秋招的整体感悟、具体公司的面试经验复盘、以及针对求职准备和心态调整的实用建议。我们将重点关注数据结构与算法能力在面试中的核心地位,以及如何有效避坑。
个人背景与转行契机
我目前就读于某C9高校的机械专业硕士。在转向算法岗位之前,我仅会一点简单的C++,对数据结构和机器学习基础几乎一无所知。
最初我尝试自学,看过“西瓜书”和“花书”,但效果不佳,前前后后折腾了近两个月,感到非常迷茫。一个偶然的机会,我了解到七月在线的课程。当时我的一位同学报了推荐系统班,反馈老师讲得不错。考虑到当时已是四五月份,秋招临近,时间紧迫,我决定报名试一试。
在七月在线的学习对我帮助很大。在后续面试中,我明显感觉到很多被问及的机器学习和深度学习基础知识,都是课程中详细讲解过的。例如,面试OPPO时,面试官要求我徒手推导SVM,从最初的分类思想到最后求解最宽分界面的完整过程,包括SMO算法,这些在课程中都有细致深入的讲解。
秋招整体感悟与核心建议
上一节我们了解了转行的起点,本节中我们来看看我对整个秋招过程的总结。我的核心感悟是:技术实力是根本。如果基础不扎实,找工作会非常辛苦。作为非科班转行的学生,我深刻体会到“菜是原罪”。因此,在面试前不要抱有任何侥幸心理,必须夯实基础。
对于想转算法岗的同学,我认为最重要的两点是:
- 掌握机器学习和深度学习的项目经验或基础知识。
- 打好计算机基础,特别是数据结构和算法,并精通一门编程语言(如C++或Python)。
以下是具体的建议列表:
- 夯实基础:必须静下心来沉淀自己。对于算法岗,数据结构(如栈、队列、链表、二叉树)必须掌握得非常清楚。很多公司都有手撕代码环节,这是第一道关卡。
- 及时获取招聘信息:实力是一部分,机遇同样重要。要主动关注各公司的招聘动态,充分利用学校、七月在线平台、已工作学长学姐等渠道的内推资源,掌握信息差。
- 重视实习经历:实习经历非常重要。我曾拿到华为的实习Offer,但因导师不同意未能成行,这在秋招中让我感觉比较吃亏。有大厂实习经历对后续找工作帮助巨大。
- 把握招聘节奏:秋招通常分为提前批(6-8月)和正式批(8-10月)。提前批可能“神仙打架”,但面试难度有时相对较低;正式批岗位更多,竞争也更激烈。之后还有一些银行、中小公司的招聘。无论如何,提升自身硬实力始终是第一要务。
具体公司面试经验分享
在分享了整体建议后,本节我将复盘几个具体公司的面试经历,希望能给大家带来更直观的参考。
我主要分享四家公司的面试情况。
1. 云从科技
我面试了云从科技,共经历了两轮技术面和一轮HR面。我遗憾地在第二轮总监面(主管面)的手写代码题上失利。
面试过程回顾:
- 一面:面试官首先让我详细介绍项目,并针对项目细节问得非常深入,包括模型参数、过拟合、梯度爆炸、Batch Normalization等。随后问了一个机器学习问题:如何区分聚类是否完成。幸运的是,这个问题在七月在线的课程中由小杨老师讲解过,我得以顺利回答。最后是一道算法题,类似于桶排序,我完成了。
- 面试技巧:一面后,我主动询问了面试官的工作方向,并请他给我一些建议。从他的反馈中,我能感觉到面试情况还不错。他提到学校项目与企业项目的区别,这让我心里有了底。果然,当天下午就收到了二面通知。
- 二面:面试官较为高冷,让我讲完项目后,问了几个问题,如是否了解TensorFlow底层框架。由于我是非科班,对此了解不深。最后是一道算法题,题目本身有一定难度,我未能给出最优解。
2. OPPO
OPPO的面试问得极其细致。
面试过程回顾:
面试官在项目提问后,开始考察我对经典神经网络架构的了解,从AlexNet问到ResNet等。我按照发展脉络逐个讲解,面试官表示认可。然而,在手写代码环节,我遇到一道环形链表插入节点的题目,没有在规定时间内写完。
面试后我同样请求建议,面试官说:“如果你做CV,基础知识没问题了,但数据结构和代码能力还有欠缺。” 我立刻明白这次面试失败了。总结来说,对于招聘硕士,公司非常看重动手能力和工程实现能力,代码能力是核心考察点。
3. 中兴通讯
我最终选择了中兴,部分原因是工作地点在西安。面试共三面。
面试过程回顾:
- 一面:依然是项目介绍,面试官问了一些PyTorch相关的问题。最后是一道堆排序的算法题,我用伪代码阐述了思路。
- 二面:聊得比较轻松,面试官主要看中我简历上的论文和专利。
- 三面:主要是谈薪资。
从这三家公司的经历可以看出,所有公司无一例外都会考察数据结构和手写代码能力。
4. 中国移动
中国移动的招聘流程启动很晚(10月底11月),因为我已拿到其他Offer且地点在上海,最终没有选择。
面试中踩过的“坑”与重要提醒
上一节我们复盘了具体面试,本节我们来看看我在过程中踩过的一些“坑”,希望大家能引以为戒。
- 笔试时间冲突:我曾同时参加苏宁和顺丰的笔试,两者时间重叠。我没注意到顺丰的笔试可以顺延半小时,结果两边都没做好。建议:仔细阅读笔试通知,如有冲突,尝试协调或优先准备更有把握的。
- 忽视基础:我最初自学时,只关注机器学习算法,对数据结构几乎不懂。上了七月在线的数据结构和刷题班后,才系统补上。深刻教训:自信心不能建立在薄弱的基础上,必须补齐所有短板。
- 非科班转型的路径:对于非科班同学,短期内冲击阿里、腾讯等大厂算法岗可能较难。可以考虑先加入中等规模的公司积累经验和项目,后续再寻求跳槽机会。
学习资源与技能准备建议
基于我的经验,我强烈推荐以下学习资源和准备重点:
以下是推荐的课程与学习资料:
- 陈小云《数据结构与算法》:B站上有免费课程,讲解得非常清晰,强烈推荐。
- 七月在线《面试求职第四期》及配套刷题班:与上述课程配套练习,效果很好。
- 《剑指Offer》:针对C++/Java,是经典的面试算法题集。
- LeetCode:必须持续刷题,很多面试题都源于此。
以下是针对算法岗(尤其是CV方向)的具体技能准备建议:
- 计算机基础:数据结构和算法是重中之重。常见考点包括:二叉树遍历、链表翻转、快速排序/归并排序等。
- 编程语言:Python或C至少精通一门。如果做CV,Python基本够用;掌握C会是加分项。
- CV方向知识:必须熟悉经典神经网络架构(如AlexNet, VGG, GoogLeNet, ResNet等),了解它们的关键创新点和成功原因。
- 项目与简历:简历上的项目必须了如指掌,对用到的算法要能透彻推导和讲解,否则容易被怀疑真实性。
心态调整与最终建议
求职是一场持久战,心态至关重要。
以下是一些心态调整的建议:
- 避免情绪大起大落:不要因为一个Offer而骄傲,也不要因连续被拒而沮丧。保持自信,平稳的心态有助于在后续面试中正常发挥。
- 持续学习:无论秋招结果如何,都是一个过程。重要的是保持学习,不断提升自己。“塞翁失马,焉知非福”。
- 关于报班:可以理性评估投入产出比。报班可能用50-100小时达到自学200小时的效果,省下的时间可以用来做其他提升。投资自己永远是值得的,但报了班就一定要努力学,否则就是浪费。
总结与答疑摘要
本节课中,我们一起学习了一位非科班同学的算法岗求职全历程。我们总结了以下核心要点:
- 硬实力是根基:数据结构和算法能力是面试的“敲门砖”和“试金石”,必须高度重视。
- 信息与机遇:积极获取招聘信息,争取实习机会,把握招聘节奏。
- 实战经验:面试中项目讲解要深入,手写代码需熟练,并学会从面试官反馈中获取信息。
- 资源与准备:善用优质学习资源,系统性地补足知识体系,针对目标岗位进行专项准备。
- 心态决定状态:保持平稳自信的心态,将求职视为长期成长过程的一部分。
答疑摘要:
- 岗位选择:目前CV岗位多但竞争者也多,门槛被抬高;NLP和推荐系统岗位相对竞争稍小。可以根据兴趣和竞争情况选择方向。
- 语言选择:Python对于算法岗基本够用,也有同学仅凭Python找到NLP工作。掌握C++会是优势。
- 非科班路径:非科班转型需要付出更多努力,重点补齐计算机基础,可以考虑“先入行再提升”的策略。


希望我的这些经验和教训能帮助大家在求职路上走得更加顺利。

人工智能—面试求职公开课(七月在线出品) - P2:O(N)时间解决的面试题(中) 🧠
在本节课中,我们将继续学习如何在O(N)时间复杂度内解决一系列经典的面试算法问题。课程将从组合数学、巧妙证明、动态规划等多个角度展开,帮助大家树立“计数不等于枚举”的理念,并掌握处理复杂问题的简化思路。
课程概述 📋
本节课主要涵盖以下内容:组合数学中的下一个/上一个排列问题;通过数学证明巧妙解决问题;区分枚举与计数的不同;以及一个较难的最大子数组变种问题的动态规划解法。我们将通过7个例题来具体阐述这些概念。
1. 下一个排列 🔄
上一节我们介绍了O(N)时间算法的总体思路,本节中我们首先来看一个具体的字典序问题:找到给定排列的下一个排列。
核心概念:给定一个排列,我们希望找到在字典序中刚好比它大的下一个排列。例如,12345的下一个排列是12354。
算法思路:可以概括为“二找一交换一翻转”。
- 找X:从右向左找到第一个满足
A[X] < A[X+1]的位置X。这意味着A[X]右边的序列B是降序的。 - 找Y:在
B中从右向左找到第一个大于A[X]的数A[Y](即大于A[X]的最小值)。 - 交换:交换
A[X]和A[Y]。 - 翻转:将
X位置之后的序列(即B)进行翻转(反转),使其变为升序。
以下是算法实现的伪代码描述:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int x = -1;
// 找X
for (int i = n - 2; i >= 0; --i) {
if (nums[i] < nums[i + 1]) {
x = i;
break;
}
}
if (x < 0) {
// 已是最大排列,翻转得到最小排列
reverse(nums.begin(), nums.end());
return;
}
int y = -1;
// 找Y
for (int i = n - 1; i > x; --i) {
if (nums[i] > nums[x]) {
y = i;
break;
}
}
// 交换
swap(nums[x], nums[y]);
// 翻转
reverse(nums.begin() + x + 1, nums.end());
}
算法优点:时间复杂度为O(N),且能正确处理包含重复元素的序列。上一个排列的算法与此对称,只需修改比较符号的方向。
2. 分割01串问题 ✂️
接下来,我们看一个需要利用数学性质巧妙解决的问题。
问题描述:给定一个长度为 4N 的01串,其中恰好有 2N 个0和 2N 个1。你可以将其切割成若干段后重新拼接,目标是使拼接后的两部分各自恰好包含 N 个0和 N 个1,并且要求切割的段数最少。
核心思路:答案只能是2或3。无需真正找到切割点,只需分析第一个长度为 2N 的窗口。
- 定义函数
F(k)为从位置k开始长度为2N的子串中,0的个数 - 1的个数。 - 计算
F(0)。如果F(0) == 0,则直接从中间(2N)处切开即可(答案为2)。 - 如果
F(0) != 0,则由于F(0) + F(2N) = 0且F(k)每次变化为±2,在0到2N之间必然存在某个位置y使得F(y) = 0。此时切三段:[0, y-1],[y, y+2N-1],[y+2N, 4N-1],将首尾两段拼为一部分,中间段为另一部分(答案为3)。
以下是判断最少段数的伪代码:
def min_cuts(s):
n = len(s) // 4 # 原题中的N
diff = 0
for i in range(2 * n):
if s[i] == '0':
diff += 1
else:
diff -= 1
if diff == 0:
return 2 # 切一刀,成两段
else:
return 3 # 切两刀,成三段
3. 平衡分割点问题 ⚖️
现在,我们分析一个通过等式推导即可解决的问题,无需复杂计算。
问题描述:给定长度为 N 的正整数数组 A 和一个特定值 X,求一个位置 M (0 ≤ M ≤ N),使得 A[0..M-1] 中等于 X 的元素个数,等于 A[M..N-1] 中不等于 X 的元素个数。
推导与解法:
设数组中 X 的总个数为 countX。
设前 M 个元素中有 y 个 X。
则后 N-M 个元素中,非 X 的个数为 (N - M) - (countX - y)。
根据要求:y = (N - M) - (countX - y)。
化简得:M = N - countX。
因此,解法非常简单:只需遍历数组一次,统计 X 出现的次数 countX,答案 M 即为 N - countX。时间复杂度O(N),空间复杂度O(1)。
思考题:10个硬币,4正6反,在不能看的情况下,如何通过翻面操作将它们分成两组,使得两组正面朝上的硬币数相同?提示:将硬币分成6个和4个两组,并将第二组的4个硬币全部翻面。
4. 统计子序列 “PAT” 的数量 🔢
从这个问题开始,我们引入“计数”与“枚举”的区别。直接枚举所有“PAT”子序列需要O(N³)时间,而计数可以在O(N)内完成。
问题描述:给定一个由字母 P、A、T 组成的字符串,计算有多少个子序列是“PAT”。子序列不要求连续。
计数策略:
我们维护三个变量:
countP: 到目前为止出现的P的个数。countPA: 到目前为止能形成的PA序列的个数(每个A可以和它之前的所有P形成PA)。countPAT: 到目前为止能形成的PAT序列的个数(每个T可以和它之前的所有PA形成PAT)。
遍历字符串:
- 遇到
P:countP++。 - 遇到
A:countPA += countP(这个A可以和前面所有的P组成新的PA)。 - 遇到
T:countPAT += countPA(这个T可以和前面所有的PA组成新的PAT)。
最终 countPAT 即为答案。时间复杂度O(N),空间复杂度O(1)。
def countPAT(s):
countP = countPA = countPAT = 0
for ch in s:
if ch == 'P':
countP += 1
elif ch == 'A':
countPA += countP
else: # ch == 'T'
countPAT += countPA
return countPAT
扩展思考:LeetCode 115题 “Distinct Subsequences” 是此问题的泛化形式。
5. 最小平均值子数组 📉
这个问题展示了如何通过分析问题性质,将搜索空间从O(N²)缩小到O(N)。
问题描述:在给定数组中,寻找一个长度至少为2的子数组,使其平均值最小。输出该子数组的起始位置(若有多个,取起始位置最小的)。
关键洞察:长度至少为2的最优解(平均值最小),其长度要么是2,要么是3。
- 假设存在一个长度大于3的最优解,我们可以将其划分为若干个长度为2或3的片段。根据平均值最小性质,这些片段的平均值必须相等,否则就会有一个片段比最优解平均值更小,矛盾。因此,最优解的平均值一定等于某个长度为2或3的子数组的平均值。
解法:只需遍历数组,计算所有长度为2和长度为3的子数组的平均值,记录平均值最小的子数组起点即可。比较时可用乘法避免浮点数精度问题:比较 (sum2 * 3) 和 (sum3 * 2)。
def minAvgSlice(A):
n = len(A)
min_avg_value = float('inf')
min_index = 0
# 检查长度为2的子数组
for i in range(n - 1):
avg = (A[i] + A[i+1]) / 2.0
if avg < min_avg_value or (avg == min_avg_value and i < min_index):
min_avg_value = avg
min_index = i
# 检查长度为3的子数组
for i in range(n - 2):
avg = (A[i] + A[i+1] + A[i+2]) / 3.0
if avg < min_avg_value or (avg == min_avg_value and i < min_index):
min_avg_value = avg
min_index = i
return min_index
6. 环形数组的最大子数组和 🌀
这是经典最大子数组和问题(Kadane算法)的一个变体,数组是环形的(首尾相连)。
问题分析:环形数组的最大子数组和有两种情况:
- 最大子数组没有跨越数组首尾(即位于数组中间)。这种情况直接用普通Kadane算法求解。
- 最大子数组跨越了数组首尾(由开头的一段和结尾的一段组成)。这种情况等价于数组总和减去中间那段最小子数组和。
解法:
- 用Kadane算法求一次不跨越边界的最大子数组和
max1。 - 求数组总和
total。 - 用Kadane算法求一次最小子数组和
min2(或对数组取反后求最大子数组和)。 - 如果
total - min2 > max1且min2对应的子数组不是整个数组(避免全为负数时误判),则跨越边界的和为total - min2。 - 最终结果为
max(max1, total - min2)。
def maxSubarraySumCircular(A):
def kadane(arr):
max_ending_here = max_so_far = arr[0]
for x in arr[1:]:
max_ending_here = max(x, max_ending_here + x)
max_so_far = max(max_so_far, max_ending_here)
return max_so_far
n = len(A)
# 情况1:不跨越边界
max_kadane = kadane(A)
# 情况2:跨越边界
total = sum(A)
# 求最小子数组和:对数组取反后求最大子数组和,再取反
max_wrap = total + kadane([-x for x in A]) # 相当于 total - min_subarray
# 特殊情况处理:如果所有数都是负数,kadane(A)就是最大值,max_wrap会是0(total - total)
if max_wrap == 0:
return max_kadane
return max(max_kadane, max_wrap)
7. 至多交换一次的最大子数组和 🔀
这是本节课最难的一个问题,需要精巧的动态规划定义。
问题描述:允许交换数组中任意两个元素一次(或零次),求交换后可能的最大子数组和。
动态规划定义:
F[i]:在子数组A[0..i]中,一段以A[i]结尾的子数组 与 一个独立的孤立点(不与那段子数组重叠) 两者之和的最大值。这段子数组可以为空,孤立点可以是0..i中任意一个点。G[i]:以A[i]开头的最大子数组和(经典Kadane算法从右向左的版本)。
状态转移:
F[i] = max(F[i-1] + A[i], max(A[0..i]))- 第一种情况:延续
F[i-1]中的那段子数组,把A[i]加进去,孤立点不变。 - 第二种情况:那段子数组为空,只取
A[0..i]中的最大值作为孤立点。
- 第一种情况:延续
G[i] = max(A[i], A[i] + G[i+1])
最终答案的构成:考虑交换的元素对 (A[j], A[i]),其中 j < i。
- 如果我们交换
A[j]和A[i],并且最终的最大子数组包含了被换过来的A[j](现在在位置i),那么这个子数组的和可以表示为:G[i] - A[i] + F[i-1]。G[i] - A[i]:是以i开头(但去除了原来的A[i])的子数组和。F[i-1]:包含了i之前的一个孤立点(即被换走的A[j])和可能的一段子数组。
- 此外,还要考虑不交换的情况(普通最大子数组和)以及
j > i的情况(可通过数组翻转再计算一次来处理)。
def maxSumWithOneSwap(A):
n = len(A)
if n == 0:
return 0
# 从左到右计算F[i]和G[i],并记录普通最大子数组和
F = [0] * n
G = [0] * n
F[0] = A[0]
cur_max = A[0] # 记录A[0..i]的最大值,用于计算F[i]
overall_max = A[0] # 普通最大子数组和
max_val = A[0] # A[0..i]的最大值
for i in range(1, n):
# 计算G[i] (需要先算,因为用到了G[i-1]其实是i-1开头的)
# 但更简单的方式:先正向算F和普通最大和,再反向算G
pass # 此处省略详细实现,展示思路
# 反向计算G[i]
G[n-1] = A[n-1]
for i in range(n-2, -1, -1):
G[i] = max(A[i], A[i] + G[i+1])
# 组合答案
ans = overall_max # 不交换
for i in range(1, n):
# 交换一个j < i 的元素
ans = max(ans, G[i] - A[i] + F[i-1])
# 处理 j > i 的情况:翻转数组再算一次
# ...
return ans
核心:理解 F[i] 的定义,它为我们提供了在 i 位置之前可用于交换的“最佳孤立点”信息。
课程总结 🎯
本节课我们一起学习了7个可以在O(N)时间内解决的面试算法题:
- 下一个排列:通过“二找一交换一翻转”的固定流程解决。
- 分割01串:利用数学性质将答案简化为2或3,避免复杂搜索。
- 平衡分割点:通过等式推导得到简洁公式。
- 统计子序列“PAT”:展示了“计数”优于“枚举”的思想,用动态计数替代暴力搜索。
- 最小平均值子数组:通过问题分析,将搜索范围缩小到长度为2和3的子数组。
- 环形最大子数组和:将问题分解为不跨越和跨越首尾两种情况处理。
- 至多交换一次的最大子数组和:利用巧妙的动态规划定义
F[i]和G[i]解决问题。


解决问题的关键在于深入分析问题性质、寻找规律、定义合适的状态,而不是急于编写暴力代码。多思考、多练习,才能灵活运用这些技巧。下节课我们将继续探讨更多与序列相关的问题。
人工智能—面试求职公开课(七月在线出品) - P3:O(N)时间解决的面试题(下) 🚀
在本节课中,我们将学习一系列可以在 O(N) 线性时间复杂度内解决的经典面试题。我们将对这些问题进行分类汇总,并深入讲解其中一些核心问题的解决思路和代码实现。
例题汇总 📚
上一节我们介绍了O(N)时间复杂度的概念,本节中我们来看看一系列具体的面试题目。我将它们分成了10个类别,每个类别包含多个相关问题。
1. 最大子数组问题
以下是和最大子数组相关的两个核心问题。
1.1 最大子数组和
这是LeetCode第53题,也是动态规划中的经典问题。目标是找到一个连续子数组,使其和最大。
方法一:利用最小前缀和
前缀和是从第一项开始连续相加的和。例如,空数组、A[1]、A[1]+A[2] 都是前缀和。以 A[i] 结尾的最大子数组和,等于 A[1] 加到 A[i] 的前缀和减去之前最小的一个前缀和。因为被减数固定时,减数越小,差越大。我们可以线性时间计算前缀和并维护最小前缀和。
方法二:动态规划
记录以每个位置 i 结尾的最大子数组和 dp[i]。状态转移方程为:
dp[i] = max(dp[i-1] + A[i], A[i])
即,要么延续之前的子数组,要么自己单独作为一个新子数组。
1.2 最大子数组乘积
这是LeetCode第152题,与最大子数组和类似,但需要考虑正负数的影响。
由于两个负数相乘可得正数,我们需要同时记录以当前位置结尾的最大乘积和最小乘积。状态转移时,需要考虑当前数 A[i] 的正负性。
以下是核心代码逻辑:
def maxProduct(nums):
if not nums:
return 0
max_ending_here = min_ending_here = global_max = nums[0]
for i in range(1, len(nums)):
temp_min = min_ending_here
min_ending_here = min(nums[i], nums[i] * min_ending_here, nums[i] * max_ending_here)
max_ending_here = max(nums[i], nums[i] * temp_min, nums[i] * max_ending_here)
global_max = max(global_max, max_ending_here)
return global_max
2. 循环移位与翻转 🔄
给定一个长度为N的数组,将其循环右移M位。直接移动的复杂度是O(N*M)。一个经典的O(N)方法是利用三次翻转。
算法步骤:
- 翻转前M个元素。
- 翻转后N-M个元素。
- 翻转整个数组。
例如,数组 [1,2,3,4,5], M=2:
- 翻转前2位:
[2,1,3,4,5] - 翻转后3位:
[2,1,5,4,3] - 翻转整个数组:
[3,4,5,1,2]
翻转函数可以这样实现:
def reverse(arr, left, right):
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
总体时间复杂度为O(2N),即O(N)。
3. Partition相关算法 🎯
Partition是快速排序的核心步骤,可以解决多种问题。
3.1 荷兰国旗问题 (LeetCode 75)
对只包含0,1,2的数组进行原地排序。使用三个指针将数组分为 <1, =1, >1 三部分。
3.2 奇偶/正负分开
将数组按奇偶性或正负性分开,是经典的两种分类Partition。
3.3 第一个缺失的正整数 (LeetCode 41)
给定一个未排序的整数数组,找出其中没有出现的最小的正整数。可以通过交换,将数值 i 放到数组索引 i-1 的位置上,然后再次遍历查找。
3.4 第K大/小数 & 最大的K个数
- 第K大数:可以利用改进的快速选择算法,通过“五数取中”选择pivot,并将数组分为
<pivot,=pivot,>pivot三段来递归求解,平均O(N)。 - 最大的K个数:使用一个大小为K的最小堆。遍历数组,维护堆中始终是已看到的最大的K个数。时间复杂度为O(N log K),实现简单,是面试中的推荐写法。
4. 众数与出现频率问题 📊
4.1 寻找众数
众数定义为出现次数超过一半的数。使用Boyer-Moore投票算法,可以在O(N)时间和O(1)空间内解决。
4.2 推广:出现次数大于 N/K 的数
寻找所有出现次数超过 N/K 的元素。可以使用一个能容纳 (K-1) 个候选者的“计数器”(如哈希表)。原理是:如果有K个不同的数,同时消去它们,不会影响那些频率超过 N/K 的数。当K为常数时,算法复杂度为O(N)。
5. 单调栈与队列 📉
5.1 单调栈求最大矩形面积 (LeetCode 84)
给定一个柱状图,求能勾勒出的最大矩形面积。核心思想:当一根柱子入栈时,其左边界确定;出栈时,其右边界确定。从而可以计算以该柱子为高的最大矩形面积。每根柱子只入栈出栈一次,时间复杂度O(N)。
5.2 单调队列求滑动窗口最值 (LeetCode 239)
维护一个双端队列,队头始终是当前窗口的最大值(或最小值)。新元素入队时,将队尾所有小于它的元素弹出,以保证队列的单调递减性。同时,需要检查队头元素是否已滑出窗口。每个元素入队出队各一次,总体O(N)。
一个极致的应用是:求有多少个子数组,其最大值与最小值之差 ⇐ K。使用两个单调队列分别维护窗口内的最大值和最小值,利用滑动窗口思想,左右指针只增不减,可在O(N)时间内解决。
6. 树相关问题 🌳
对于有N个节点的树(包括二叉树),其边数为N-1,因此许多遍历和查询操作可以在O(N)时间内完成。
- 树的高度/深度
- 判断二叉树是否对称/平衡
- 二叉树的最小/最大深度
- 求所有和为指定值的路径
- 二叉树与双向链表转换
- 二叉树的前序、中序、后序遍历
一个经典技巧:求树的直径
树的直径是树中任意两个节点之间的最长路径。一个巧妙的O(N)算法是:
- 从任意节点
u出发,进行DFS/BFS,找到距离它最远的节点x。 - 从节点
x出发,再次进行DFS/BFS,找到距离它最远的节点y。
路径x-y就是树的一条直径。本质是两次DFS/BFS。
7. 滑动窗口(双指针)问题 🪟
这类问题通常要求找到一个满足特定条件的连续子数组(子串)。
7.1 长度最小的子数组 (LeetCode 209)
给定一个正整数数组和一个目标值S,找出总和 >= S 的长度最小的连续子数组。
由于数组元素全为正数,具有单调性:如果一个窗口的和太小,扩大它(右移右指针)和可能增大;如果和已经 >=S,缩小它(右移左指针)可能找到更短的合格窗口。左右指针只增不减,时间复杂度O(N)。
7.2 最小覆盖子串 (LeetCode 76) & 无重复字符的最长子串 (LeetCode 3)
- 76题:在字符串S中找到一个最短的子串,包含字符串T的所有字符。维护一个滑动窗口,用哈希表记录窗口内字符与目标字符的差值。
- 3题:找到一个不包含重复字符的最长子串。同样使用滑动窗口,当窗口内出现重复字符时,移动左指针直到重复消除。
它们的共同点是:根据窗口是否满足条件,同步地向右移动左右指针。
8. 链表相关问题 ⛓️
链表是一种线性结构,许多操作可以在一次遍历(O(N))内完成。
- 求长度、翻转链表:基础操作。包括指定区间翻转(92)、K个一组翻转(25)。
- 插入删除节点:涉及指针操作,如删除倒数第N个节点(19)。
- 特殊链表:复制带随机指针的链表(138)。
- 链表相交:判断两个链表是否相交,并找到交点(160)。
- 链表环:判断链表是否有环(141),并找到环的起点(142)。
- 回文链表:可以找到链表中点,翻转后半部分,与前半部分比较(O(1)空间,但会改变链表结构)。
9. 哈希表妙用 🔑
9.1 两数之和 (LeetCode 1)
在数组中找出两个数,使它们的和等于目标值。最直接的方法是使用哈希表。遍历数组,对于每个数 nums[i],在哈希表中查找 target - nums[i]。若存在,则找到答案;否则将 nums[i] 及其索引存入哈希表。时间复杂度O(N)。
9.2 谷歌面试题:最少排序次数
给定一个1到N的排列,每次操作可将一个数移到末尾,问至少操作几次能将其排序。
关键思路:寻找从1开始,在原始排列中按顺序连续出现的最长前缀。例如排列 [3,1,5,4,2],从1开始能找到 [1,2] 是连续出现的。那么,我们只需要将剩余的数字(本例中是3,5,4)依次移到末尾即可。操作次数 = N - 最长连续前缀的长度。只需扫描一遍数组,O(N)时间。
9.3 摩根斯坦利赛题变种
允许将数字移动到开头或末尾。目标是找到中间一段最长的、在原始排列中已经按顺序排列的子序列(不一定从1开始)。我们可以用动态规划解决:
- 令
dp[x]表示从数值x开始,在数组中出现的最长连续序列长度(即x, x+1, x+2...连续出现)。 - 从后往前遍历数组,如果
A[i]+1在之后出现,则dp[A[i]] = dp[A[i]+1] + 1。 - 最后,找到最大的
dp值M,这意味着有一段长度为M的连续数字已经有序。我们只需将其他N-M个数字移动到两端即可。时间复杂度O(N),空间复杂度O(N)。
总结 🎉
本节课我们一起学习了多种可以在 O(N) 线性时间复杂度内解决的面试算法题,涵盖了数组、字符串、链表、树、滑动窗口、单调数据结构、哈希表等多个领域和技巧。
核心要点回顾:
- 前缀和与差分:用于快速计算子数组和。
- 双指针/滑动窗口:利用问题单调性,使左右指针单向移动。
- Partition思想:不仅是排序,也是分类和选择问题的利器。
- 单调栈/队列:高效维护区间最值信息。
- 哈希表:以空间换时间,实现快速查找。
- 树的遍历:许多树问题本质是遍历的变体。
- 链表操作:理解指针的指向是解决链表问题的关键。
- 问题转化:将复杂问题(如最少排序次数)转化为寻找特定模式(如最长连续序列)。


O(N)算法通常意味着高效和优雅,是面试中的重点。理解这些问题的本质和通用模式,并勤加练习,是掌握它们的最佳途径。


人工智能—面试求职公开课(七月在线出品) - P4:阿里巴巴面试题精讲 🎯

在本节课中,我们将学习如何应对阿里巴巴等大型科技公司的技术面试。课程将精选七道典型面试题,涵盖概率、智力题、数据结构与算法以及系统设计等多个方面,帮助你掌握解题思路与核心技巧。
关于面试的观点 🤔

首先,我们来谈谈关于面试的一些观点。各大公司是否有自己的专属题库并不重要,因为题目来源通常是共享的,可能来自员工出题或网络搜集。因此,一道题目可能被多家公司使用,无需为其打上特定公司的标签。
笔试和面试存在显著区别。笔试通常以试卷形式进行,要求独立完成,题型多样但程序题较少,且缺乏与考官的即时交流。面试则强调面对面沟通,通常要求编写完整代码,并有机会向面试官阐述思路,这是展示逻辑思维和表达能力的关键环节。

笔试与面试题的特点 📝
接下来,我们看看笔试和面试题目的特点。以阿里巴巴为例,其题目非常重视计算机科学的基础知识。

笔试题型广泛,涉及操作系统(如缓存、LRU算法、进程线程、死锁)、计算机网络(如TCP三次握手、URL访问流程)等。数据结构与算法始终是考察重点,包括排序、二分查找、树与图等。此外,也可能出现数学概率题或智力题。
面试则更侧重于交流与实际问题解决能力,有时会包含开放性的系统设计问题。


精选例题讲解

下面,我们将深入讲解七道精选例题,它们分别代表了不同的考察方向。
例题一:球队强强对话(概率)⚽


问题:8支球队随机抽签两两配对。已知其中有3支强队,问出现“强强对话”(即两支强队被分到同一对)的概率是多少?
解题思路:这是一个组合数学问题。首先计算8支球队两两配对的总方案数。我们可以依次为球队选择对手,总方案数为 7 * 5 * 3 * 1 = 105。

接着,计算没有强强对话(即每个强队都与弱队配对)的方案数。从5支弱队中选出3支与强队配对,并考虑顺序,方案数为 P(5,3) = 5*4*3 = 60。
因此,强强对话的方案数为 105 - 60 = 45。最终概率为 45 / 105 = 3/7。

总结:此类概率题通常通过计算满足条件与总情况的方案数,运用组合数学公式求解。
例题二:选包抽球(后验概率)🎲

问题:有两个包,甲包有8红球2蓝球,乙包有8蓝球2红球。先抛硬币决定选哪个包,然后从选中包中有放回地取球11次,结果恰好是7红4蓝。问最初选中甲包的概率是多少?
解题思路:这是一个典型的后验概率问题,需使用贝叶斯公式。
定义事件:
- A: 选中甲包
- B: 选中乙包
- C: 11次取球结果为7红4蓝

已知先验概率 P(A) = P(B) = 0.5。
在已知选中甲包的条件下,事件C发生的概率服从二项分布:P(C|A) = C(11,4) * (0.8)^7 * (0.2)^4。
同理,P(C|B) = C(11,4) * (0.2)^7 * (0.8)^4。
所求概率为 P(A|C)。根据贝叶斯公式:
P(A|C) = [P(C|A) * P(A)] / [P(C|A)*P(A) + P(C|B)*P(B)]
代入已知数值即可计算出结果。

总结:对于已知结果反推原因的条件概率问题,贝叶斯公式是核心工具。
例题三:黑板上的数字(智力题)🧠
问题:黑板上写有1到50。每次操作擦去两个数,写上它们的差(绝对值)。经过49次操作后只剩一个数,问这个数可能是什么?为什么?
解题思路:关键在于寻找不变量。考虑剩余数字中奇数的个数的奇偶性。
每次操作有三种情况:
- 擦去两个奇数:差为偶数,奇数个数减少2。
- 擦去两个偶数:差为偶数,奇数个数不变。
- 擦去一奇一偶:差为奇数,奇数个数不变。
因此,奇数个数的奇偶性始终保持不变。初始有25个奇数(奇数个),故最终剩下的数只能是奇数(奇数个奇数不可能通过每次减2变为0)。

范围上,结果显然在0到50之间。通过构造法(例如,若想得到1,则保留1和2,将其余数字配对相消)可以证明,1到49之间的所有奇数都可能出现。
答案:最终结果可能是1到49之间的任意奇数。

上一节我们探讨了数学和智力问题,本节开始,我们将重点转向数据结构与算法,这是技术面试的核心。
例题四:最大间距(线性时间与空间)📊
问题:给定一个无序的实数数组,要求在线性时间和线性空间复杂度内,找出数组排序后相邻元素之间最大的差值。

解题思路:直接排序需要 O(n log n) 时间。要达到 O(n),需使用桶排序的思想。
- 遍历数组,找到最小值
min和最大值max。 - 将数值范围
[min, max)平均分成n+1个桶(区间),每个桶的宽度为bucketSize = (max - min) / (n+1)。 - 将每个数放入对应的桶中,每个桶只记录桶内元素的最小值和最大值。
- 根据鸽巢原理,
n个数放入n+1个桶,至少有一个空桶。最大间距一定不会出现在同一个桶内部,因为空桶的存在保证了其相邻非空桶之间最小值与最大值的差至少为一个桶的宽度。 - 因此,只需遍历所有桶,用当前桶的最小值减去前一个非空桶的最大值,并记录最大值,即为答案。
代码关键步骤:
def maximumGap(nums):
if len(nums) < 2:
return 0
n = len(nums)
min_val, max_val = min(nums), max(nums)
if min_val == max_val:
return 0
bucket_size = max(1, (max_val - min_val) // (n + 1))
bucket_count = (max_val - min_val) // bucket_size + 1
buckets_min = [float('inf')] * bucket_count
buckets_max = [float('-inf')] * bucket_count
for num in nums:
idx = (num - min_val) // bucket_size
buckets_min[idx] = min(buckets_min[idx], num)
buckets_max[idx] = max(buckets_max[idx], num)
max_gap = 0
prev_max = buckets_max[0]
for i in range(1, bucket_count):
if buckets_min[i] == float('inf'): # 空桶
continue
max_gap = max(max_gap, buckets_min[i] - prev_max)
prev_max = buckets_max[i]
return max_gap

例题五:流式数据随机采样(蓄水池抽样)🎣
问题:有一个数据流(只能通过 get_next() 依次访问元素,无法预知总数 n),要求设计一个算法,等概率地随机返回流中的一个元素。空间复杂度需为 O(1)。
解题思路:使用蓄水池抽样算法。算法步骤如下:
- 初始化结果
result,计数器i = 0。 - 当读到一个新元素
x时:i += 1- 以
1/i的概率用x替换当前的result。 - 以
(i-1)/i的概率保持result不变。
- 数据流结束后,返回
result。

证明:对于第 i 个元素,它被选中并最终保留的概率是:
(1/i) * [i/(i+1)] * [(i+1)/(i+2)] * ... * [(n-1)/n] = 1/n
因此,每个元素被选中的概率都是 1/n。
伪代码:
result = None
i = 0
while (x = get_next()) is not None:
i += 1
if random.randint(1, i) == 1: # 以 1/i 的概率
result = x
return result
扩展:此算法可推广到随机抽取 k 个样本的情况。
例题六:查找 AI = I 的下标 🔍

问题:给定一个严格递增的整数数组 A,寻找是否存在下标 i 使得 A[i] == i。要求分析时间复杂度。
解题思路:构造新数组 B,其中 B[i] = A[i] - i。
由于 A 严格递增且为整数,A[i+1] - A[i] >= 1,因此 B[i+1] - B[i] = (A[i+1] - (i+1)) - (A[i] - i) = A[i+1] - A[i] - 1 >= 0。
所以 B 是一个非严格递增数组。问题转化为在 B 中查找值 0。
这可以通过标准的二分查找在 O(log n) 时间内解决。注意,我们无需显式构造 B 数组,在二分查找过程中动态计算 A[mid] - mid 的值即可。




例题七:成绩查询与排名系统设计(开放问题)💻

问题:设计一个系统,存储百万条学生成绩记录(ID,科目A/B/C成绩)。需求:(1) 快速查询指定ID的各科成绩;(2) ID范围变为0-10亿且不连续时如何应对;(3) 支持按总分或单科成绩排名。
思路分析:
这是一个开放的系统设计问题,以下提供一些思考方向:
- ID连续时:可直接用数组存储,将ID偏移后作为索引。百万条记录,每科成绩用1字节存储(0-100),总内存消耗约3MB,效率极高。

ID不连续且范围大时:
- 核心:需要将稀疏的、大范围的ID映射到紧凑的存储索引。
- 方法一(内存哈希):使用哈希表(如开放寻址或链地址法)。预估存储空间,例如设计一个大小为1200万的哈希数组来映射100万条记录,结合成绩存储,总内存可能在几十MB量级,需评估是否可接受。
- 方法二(磁盘哈希):若内存受限,可将哈希表和部分数据放在磁盘上,通过文件IO进行查找,但速度会慢于纯内存方案。
支持排名:
- 排序:需要对百万级数据进行排序。内存充足时可使用快速排序;考虑到成绩范围很小(0-100或总分0-300),使用计数排序是更优选择,时间复杂度可达 O(n)。
- 外部排序:如果数据量极大无法全部装入内存,需使用外部归并排序:将数据分块读入内存排序后写回临时文件,再对这些有序文件进行多路归并。
- 存储排名:排序后,可将排名作为额外字段存入记录中。更新时可能需要全量重排或使用增量更新策略。
优化思路:
- 数据压缩:成绩值域小,可使用更小的数据类型(如
uint8)存储。 - 索引结构:为常用的查询字段(如总分)建立索引。
- 数据压缩:成绩值域小,可使用更小的数据类型(如
课程总结 📚

本节课我们一起学习了应对技术面试的多种策略与经典题型。
我们了解到,笔试注重广泛的基础知识考察,而面试则更侧重于交流能力、思维过程以及解决开放性问题的能力。在面试中,应主动与面试官沟通思路,对问题进行合理假设和简化。
通过七道例题,我们掌握了:
- 利用组合数学解决概率问题。
- 运用贝叶斯公式处理后验概率。
- 通过寻找不变量解决智力题。
- 使用桶排序思想在线性时间内求解最大间距问题。
- 应用蓄水池抽样算法处理流式数据的随机采样。
- 通过问题转换与二分查找解决特定条件的查找问题。
- 从多个角度思考系统设计问题,权衡空间、时间与实现复杂度。


希望本课程能帮助你更好地准备技术面试,提升解题能力。
人工智能—面试求职公开课(七月在线出品) - P5:曹博:我的北美求职心得
🎯 课程概述
在本节课中,我们将学习北美科技公司求职的完整流程、核心挑战与准备策略。课程内容基于讲师的亲身经历,旨在为有志于赴美工作的同学提供一份实用的指南。
📋 求职一般流程
上一节我们介绍了课程概述,本节中我们来看看北美求职的一般流程。该流程与国内求职有相似之处,但存在一些关键差异。
以下是主要的求职渠道:
- LinkedIn:平台上有许多招聘人员(Recruiter),可以主动联系他们或被他们联系,通过站内信或电话沟通后投递简历。
- 直接投递:通过公司官网直接申请职位,但成功率通常较低,因为候选人众多,简历容易石沉大海。
- 内推:通过已在目标公司工作的朋友、学长等进行内部推荐。公司对内部推荐的重视程度较高,获得面试的机会也更大。
- 比赛:通过参加如LeetCode、Codeforces、TopCoder等平台上的编程比赛,如果获得优异名次(如前十、前二十),公司可能会主动联系。
在获得面试机会后,流程通常如下:
- HR初步沟通:HR会进行约10到20分钟的电话沟通,了解你的背景、项目经验、技术栈和职业意向,并介绍公司概况。
- 电话面试:通常有1到3轮,在线上进行。
- 现场面试:需要前往美国公司所在地进行,通常有5到6轮,从早晨持续到晚上。中午可能与工程师共进午餐,了解公司文化。
- 发放Offer:面试通过后,公司会发放录用通知。
- 办理签证:公司协助办理美国工作签证(主要是H-1B)。
- 入职:通常在10月1日之后。
从电话面试到现场面试的通过率相对较高,但从现场面试到最终获得Offer的难度则大很多。
🏢 可投递的公司列表
了解了流程后,我们来看看哪些公司值得关注。由于签证办理对公司是项负担,因此并非所有美国公司都愿意从海外招聘。
以下是部分已知从中国招聘的知名公司:
- FLAG:这是一个流传的说法,指代四家巨头公司:
- F - Facebook (Meta)
- L - LinkedIn
- A - Amazon
- G - Google
- Microsoft (微软)
- Twitter (推特)
- Storm8 (一家游戏公司)
- Apple (苹果)
- Pocket Gems (一家手游公司)
- Tango (一款类似微信的通讯工具)
- Thinx (总部在加拿大,美国有分部)
建议优先考虑FLAG、微软、推特等规模较大、有成熟海外招聘流程的公司。
😫 公司招聘的烦恼
为什么公司从海外招聘如此谨慎?本节我们来分析公司方的考量。
公司招聘海外员工面临以下主要烦恼:
- 人才供给:美国本土毕业生数量充足,但平均水平可能不及来自中国、印度等地的顶尖候选人。公司为追求更高水平人才,才愿意承担额外成本从海外招聘。
- 面试协调成本:安排跨时区的电话面试本身就更耗时。安排现场面试则需承担机票、酒店住宿等费用,成本显著高于招聘本土候选人。
- 签证与等待周期:工作签证(H-1B)实行抽签制,每年4月1日提交申请,10月1日生效。这意味着从发出Offer到员工入职,至少有半年等待期,无法满足紧急的招聘需求。
- 提高招聘标准:正因为招聘海外员工成本高、周期长,公司对其面试要求也水涨船高,期望招到更优秀的人才。
这些因素共同导致了海外求职面试难度大、不确定性高。
🛂 签证问题详解
签证是海外求职的核心挑战之一。本节我们详细解读美国工作签证H-1B。
H-1B签证核心信息:
- 性质:非移民签证,与雇主绑定。必须由雇主为你申请。
- 有效期:首次有效期一般为3年,可延期3年,最长总计6年。
- 绿卡转换:在H-1B期间,公司通常会为员工申请绿卡(移民签证)。在申请进入一定阶段(如I-140批准后),即使绿卡未最终获批,H-1B身份也可无限期延期。
签证流程与抽签:
- 提交申请:每年4月1日,雇主将申请材料提交至美国移民局(USCIS)。
- 抽签:移民局每年发放约65,000个H-1B签证名额。若申请人数超额,则通过电脑随机抽签决定。
- 硕士及以上学历优待:拥有美国硕士或博士学位的申请人,享有20,000个优先名额(包含在65,000个内),中签率更高。
- 结果与生效:抽签结果通常在5-6月公布。中签者需准备领事馆面签,通过后签证于当年10月1日生效。
因此,最理想的求职节奏是在每年4月1日前拿到Offer,以便赶上当年的抽签。对于留学生,毕业后可申请OPT(Optional Practical Training)临时工作许可,作为等待H-1B抽签期间的合法工作身份。
📝 面试题型与准备方法
面对高标准的面试,该如何准备?本节我们将面试内容分为技术与非技术两方面进行梳理。
非技术面试准备:
以下是常见的非技术问题类型:
- 自我介绍与项目经历:清晰阐述你的背景和做过的项目。
- 行为问题:考察团队协作与解决问题的方式。例如:“如果同事进度慢影响团队,你会如何处理?”
- 开放性问题:考察产品思维与批判性思考。例如:“你认为我们产品有哪些可以改进的地方?”
- 文化契合度:了解你是否能融入团队和公司文化。
技术面试准备:
技术面试的形式多样,主要包括:
- 在线编程测试:在HackerRank或Codility等平台完成,通常有倒计时。
# 示例:一个简单的OA题目框架 def solve(input_data): # 你的解题逻辑 result = ... return result - 电话面试:主要考察编程和算法,难度通常不会太高,但受限于沟通形式。
- 现场面试:考察范围最广,可能包括:
- 编程与算法:核心考察内容。
- 系统设计:设计一个分布式系统,如短链接服务、售票系统等。考察工程综合能力。
- 逻辑/数学题:考察思维敏捷度。
- 测试与调试:如何测试一个功能或排查系统问题。
🔍 面试例题选讲
为了让大家有更直观的感受,本节我们选取几个不同类型的面试题进行简要讲解。
1. 逻辑题:100把锁
100把锁编号1-100,初始锁着。第一个人把所有锁打开;第二个人将编号为2的倍数的锁状态反转(即关上);第i个人将编号为i的倍数的锁状态反转。问第100人操作后,哪些锁是开着的?
关键:每把锁被操作的次数等于其编号的约数个数。只有约数个数为奇数的锁,最终状态是开的。而只有完全平方数的约数个数是奇数。
2. 概率题:蚂蚁碰撞
三只蚂蚁位于等边三角形三个顶点,各自随机选择一条边沿顺时针或逆时针爬行,速度相同。问它们不会相互碰撞的概率是多少?
关键:所有蚂蚁同向(全顺时针或全逆时针)则不会碰撞。每只蚂蚁有2种方向选择,总共有2^3 = 8种情况。因此不碰撞概率为2/8 = 1/4。
3. 算法题:水流问题(太平洋大西洋水流问题)
给定一个矩阵,左边界和上边界邻接“太平洋”,右边界和下边界邻接“大西洋”。每个格子有高度,水只能向不高(或相等)于当前格子的相邻格子流动。找出所有能同时流到太平洋和大西洋的格子。
关键:从两个海洋的边界分别进行DFS或BFS逆向搜索(从低往高找),标记能到达的格子。最后取两个标记集合的交集。# 思路伪代码 def pacificAtlantic(heights): # 初始化两个访问矩阵 can_reach_p = dfs_from_pacific_border(heights) can_reach_a = dfs_from_atlantic_border(heights) # 找到两个矩阵都为True的坐标 return [(i, j) for i in range(rows) for j in range(cols) if can_reach_p[i][j] and can_reach_a[i][j]]
4. 系统设计题:短链接系统
如何设计一个像微博或Twitter那样的短链接生成服务?
考察点:哈希函数设计、长链接到短码的映射、避免哈希冲突、高并发下的性能、分布式系统下的唯一ID生成、数据存储与缓存策略等。
🎓 课程总结
本节课中,我们一起学习了北美求职的完整图景。
我们首先梳理了从投递简历到最终入职的一般流程,强调了内推的重要性。接着,列出了部分对海外候选人开放的公司列表。然后,深入分析了公司招聘海外员工的烦恼与成本,这解释了面试高要求的原因。签证(H-1B) 部分我们详细讲解了其抽签机制和漫长周期,这是海外求职最大的不确定性之一。
在面试准备部分,我们将其分为技术面和非技术面,并列举了编程、算法、系统设计、行为问题等多种题型。通过几个例题,我们直观感受了面试的考察方向。
最后,需要认识到北美求职是实力与运气的结合。面试本身具有随机性,签证抽签更是无法控制。因此,保持良好心态,将每次面试视为锻炼,不断积累经验至关重要。即使实力出众,也需要一定的运气才能成功。
希望这份心得能帮助你在求职路上少走弯路。祝你好运!




人工智能—面试求职公开课(七月在线出品) - P6:概率面试题精讲 🎲
在本节课中,我们将要学习概率论在算法面试中的核心应用。课程将从概率的基本概念入手,逐步深入到随机数生成、采样算法等经典面试题,并通过多个例题帮助大家掌握解题思路。
课程简介 📋
概率相关的面试题主要考察以下几个范围:
- 对独立事件的理解。
- 古典概率,即通过计数(排列组合)做除法。
- 条件概率,即给定一个事件成立,求另一个事件的概率。
- 期望的计算。
- 随机数的产生和利用(采样)。
前四个知识点主要出现在笔试的选择题、填空题和计算题中。最后一个知识点“随机数的产生和利用”则更常见于面试中,也可能出现在笔试的大题里。
预备知识:关于随机性 💡
在深入具体题目之前,我们先了解一些关于随机性的背景知识。
随机数的产生在计算机科学中是一个深刻的课题。一个著名的计算机科学家在其著作《编程珠玑》中用了大量篇幅讨论如何产生随机数。我们只需有一个感性认识:随机性和不可预测性存在差异。
这个差异在密码学中尤为明显。例如,固定一个整数 M,对所有自然数 N 计算 N % M(取余数),结果在 0 到 M-1 之间是均匀分布的。虽然它具有一定的随机性,但在密码学中通常不被采用,因为它是可预测的(具有周期性)。
因此,在面试中如果考察构造随机数发生器,题目通常会假设存在一个基础的随机数发生器,而不会要求我们凭空构造。
关于期望的计算,一个常见技巧是将其转化为方程组来求解。例如,对于状态 A,其期望 E(A) 可以表示为 E(A) = 1 + Σ [P(从A到邻居状态) * E(邻居状态)]。通过为每个状态建立类似的方程,我们可以形成一个多元一次方程组,从而解出所有状态的期望值。
例题精讲 📚
上一节我们介绍了概率面试题的考察范围和预备知识,本节中我们来看看具体的例题。

例题1:理解独立性

这个例题旨在帮助大家精确理解“独立”的概念。
假设 X1, X2 是两个二元随机变量,即它们以各50%的概率取值为0或1。定义 X3 = X1 XOR X2(异或运算:相同为0,不同为1)。
直觉上,X3 与 X1、X2 似乎因为定义式而相关。但实际上,X3 与 X1 以及 X3 与 X2 都是相互独立的。
我们可以枚举验证:
- 当
(X1, X2)为(0,0)时,X3=0 - 当
(X1, X2)为(0,1)时,X3=1 - 当
(X1, X2)为(1,0)时,X3=1 - 当
(X1, X2)为(1,1)时,X3=0
观察可知,无论 X1 是0还是1,X3 都有一半的概率是0,一半的概率是1。根据独立的定义——P(A∩B) = P(A)P(B)——可以验证它们满足独立性。
关键点:判断独立性不能凭直觉,必须依据数学定义。
例题2:构造随机数发生器
这是一个非常经典的面试题。题目通常会给定一个随机数发生器,要求构造另一个。
题目:给定一个均匀生成 [1, 7] 范围内整数的随机函数 rand7(),请构造一个均匀生成 [1, 10] 范围内整数的随机函数 rand10()。
核心思路:我们可以丢弃(拒绝)不想要的采样结果,但必须保证在接受的样本中,每个结果都是等概率出现的。
以下是两种方法:
方法一(利用二进制):
- 调用
rand7(),如果结果是7则重试,直到得到[1, 6]范围内的数。此时,奇数和偶数各三个,等概率。 - 将奇数映射为1,偶数映射为0,我们就得到了一个均匀的
rand2(),即等概率产生0和1。 - 调用4次
rand2(),拼接成一个4位二进制数,可以等概率生成[0, 15]共16个数。 - 如果生成的数在
[1, 10]范围内,则接受;否则(即0或11-15),拒绝并重试。
# 伪代码示意
def rand10():
while True:
# 步骤1,2: 生成一个均匀的0/1
def rand2():
while True:
num = rand7()
if num != 7:
return num % 2 # 奇数得1,偶数得0
# 步骤3: 生成0-15的随机数
num = 0
for i in range(4):
num = (num << 1) | rand2()
# 步骤4: 拒绝采样
if 1 <= num <= 10:
return num
方法二(利用七进制,更高效):
- 将
rand7()减1,得到均匀的[0, 6]发生器。 - 将其视为7进制。调用两次,分别作为高位
a和低位b,可以等概率生成[0, 48](即7*7-1)共49个数。 - 我们拒绝
[40, 48]这9个数。此时剩下的[0, 39]共40个数,每个数的个位数(0-9)都出现了4次,因此是均匀的。 - 将得到的数取个位并加1,即得到
[1, 10]。
# 伪代码示意
def rand10():
while True:
a = rand7() - 1 # 高位
b = rand7() - 1 # 低位
num = a * 7 + b # 均匀生成 0~48
if num < 40: # 拒绝40~48,保留0~39
return (num % 10) + 1 # 均匀生成1~10
方法对比:方法二丢弃的样本更少,因此期望的调用次数更少,效率更高。
一个通用结论:如果一次实验成功的概率是 p,那么直到成功所需的期望实验次数是 1/p。
例题3:用不均匀硬币构造均匀硬币
题目:给定一个随机函数 randP(),它以概率 p 产生0,以概率 1-p 产生1(0<p<1)。请构造一个等概率产生0和1的随机函数。
思路:利用两次独立调用 randP() 的结果。
- 连续调用两次,结果只有四种可能:
00,01,10,11。 - 其中,
01和10出现的概率是相等的,都是p*(1-p)。 - 因此,我们可以丢弃
00和11的情况。当得到01时输出0,得到10时输出1。
def randFair():
while True:
a = randP()
b = randP()
if a == 0 and b == 1:
return 0
if a == 1 and b == 0:
return 1
# 其他情况(a==b)则重试
例题4:连续随机变量的概率计算
这是一道出现在笔试中的选择题,考察将概率问题转化为几何问题。
题目:设随机变量 X 在 [0, a] 上均匀分布,Y 在 [0, b] 上均匀分布,且相互独立。给定 z,求 P(X + Y <= z)。
解法:
(X, Y)的所有可能取值构成一个面积为a * b的矩形。- 直线
X + Y = z将这个矩形分为两部分。 P(X + Y <= z)就等于直线下方区域面积与矩形总面积之比。- 需要根据
z相对于a和b的大小进行分类讨论:- 当
0 <= z <= min(a, b)时,区域是三角形,面积为z^2 / 2。 - 当
min(a, b) < z <= max(a, b)时,区域是一个五边形。 - 当
max(a, b) < z <= a+b时,区域是矩形减去一个小三角形。 - 当
z > a+b时,概率为1;当z < 0时,概率为0。
- 当
例题5:水库采样算法
这是本节课的重点之一。它用于解决数据流中的等概率随机采样问题。
问题:有一个长度很大(甚至未知)的数据流,我们只能顺序访问一次。请设计一个算法,从中等概率地随机选取 k 个元素。
算法步骤(水库采样):
- 初始化一个大小为
k的数组reservoir。 - 对于数据流中第
i个元素(i从1开始计数):- 如果
i <= k,直接放入reservoir[i-1]。 - 如果
i > k,随机生成一个[0, i-1)范围内的整数j。 - 如果
j < k,则用第i个元素替换reservoir[j]。
- 如果
算法证明:
目标是证明,在处理完所有 N 个元素后,每个元素被留在 reservoir 中的概率都是 k/N。
- 对于前
k个元素:它们一开始被选入。第i (i<=k)个元素要最终留下,必须在后续所有(i+1)到N次替换中都不被换出。这个概率是(k/(k+1)) * ((k+1)/(k+2)) * ... * ((N-1)/N) = k/N。 - 对于第
i (i>k)个元素:它被选入reservoir的概率是k/i。选入后,要最终留下,必须在后续所有替换中不被换出,概率为(i/(i+1)) * ... * ((N-1)/N) = i/N。因此,最终留下的概率是(k/i) * (i/N) = k/N。
优点:无需预先知道数据流的总长度 N,空间复杂度仅为 O(k)。
应用:从大文件中随机读取一行(k=1的特殊情况)、从未知长度链表中随机选取节点等。
例题6:随机重排数组
问题:如何均匀随机地打乱一个数组(即生成其所有 n! 个排列中的一个)?
经典算法(Fisher-Yates Shuffle):
- 初始化数组,
arr[i] = i(或待打乱的原始数据)。 - 遍历下标
i从0到n-2:- 生成一个
[i, n-1]范围内的随机整数j。 - 交换
arr[i]和arr[j]。
- 生成一个
import random
def shuffle(arr):
n = len(arr)
for i in range(n-1): # 最后一个元素无需交换
j = random.randint(i, n-1) # 生成[i, n-1]的随机整数
arr[i], arr[j] = arr[j], arr[i]
关键:每次交换时,随机范围是从当前下标 i 开始,而不是从0开始。这保证了每个排列出现的概率相等。
例题7:加权随机采样
这是水库采样的进阶问题,在实际推荐系统中常有应用。
问题:给定 n 个元素和对应的权重 w_i(假设为整数)。要求随机抽取一个元素,且元素被抽中的概率与其权重成正比。
方法一:按权重复制(朴素)
将每个元素 i 复制 w_i 份,得到一个扩大的列表,然后在这个列表上进行普通的等概率采样(如水库采样)。该方法简单直观,但当权重很大时非常浪费空间。
方法二:前缀和+二分查找
- 计算所有权重的总和
total_weight。 - 计算每个元素的前缀和,形成一个累积分布区间。例如,权重为
[3, 2, 6],则区间为[0,3),[3,5),[5,11)。 - 生成一个
[0, total_weight)之间的随机数r。 - 使用二分查找找到
r所在的区间,对应的元素即为被选中的元素。
该方法省空间,但需要二分查找,时间复杂度为O(log n)。
方法三:概率选择法
这是一种更巧妙的算法,尤其适合权重差异大的情况。
- 以
1/n的概率随机选择一个元素作为“候选”。 - 对于这个候选元素
i,我们以概率p = w_i / w_max(或其他特定形式)来决定是否接受它。w_max是最大权重。 - 如果接受,则返回该元素;否则,拒绝并回到步骤1重试。
原理分析:
设我们采用 p = w_i / w_max。那么在一次循环中,选中元素 i 并接受的概率为 (1/n) * (w_i / w_max)。这是一个几何分布的过程。最终,元素 i 被选中的概率与 w_i 成正比。此方法的期望循环次数与 n 和权重分布有关,通常优于朴素方法。
应用:根据歌曲的评分(权重)进行随机推荐,评分越高的歌曲被推荐的概率越大。
课程总结 🎯
本节课我们一起学习了概率论在算法面试中的核心应用。
- 采样算法是重中之重,尤其是水库采样算法,它优雅地解决了数据流中的等概率随机采样问题。其扩展形式如加权采样(例题7)也有很强的实用价值。
- 概率与算法的结合无处不在。例如快速排序中随机选择pivot来避免最坏情况,将期望时间复杂度降至
O(n log n);哈希函数的设计中利用随机性来减少冲突;以及在随机算法中,通过多次运行一个成功概率不高的算法,来极高概率地获得正确解(例如运行30次,失败概率可降至约10亿分之一)。 - 随机数构造是经典面试题,解题关键在于利用已有的随机源,通过“拒绝采样”和“数值进制转换”等技巧,在保证均匀性的前提下构造目标随机数发生器。
- 理解独立性的严格数学定义,并能将其与直觉区分,是解决基础概率题的关键。


希望本课程能帮助大家更好地应对面试中涉及概率与统计的挑战。
人工智能—面试求职公开课(七月在线出品) - P7:谷歌面试题精讲 🎯
在本节课中,我们将学习如何应对技术面试,特别是通过精讲六道经典的谷歌面试题,从简单到复杂,涵盖数学、动态规划、组合数学、图论和位运算等多个领域。我们将深入分析每道题的解题思路,并学习如何与面试官进行有效沟通。
面试心得交流 🤝
上一节我们介绍了课程的整体安排,本节中我们来看看面试的一些通用心得。
关于面试,有人会问各个公司是否有自己的面试题库。这个问题意义不大。即使公司有题库,题目来源一方面是员工原创,但相对较少。另一方面,他们也通过网络搜罗题目。因此,即使公司有自己的题库,不同公司题库的重合度也会很大。所以,关注特定公司的面试题意义不大,应该关注所有公司的面试题,因为它们交集非常大。
笔试和面试不同。笔试是面对卷子埋头做题,没有交流。面试则是展现自我和思路的过程,需要与面试官交流。很多面试书籍都强调不要把面试当成笔试。面试官希望并需要你与他交流,这样才能展现你的思路。
面试的关键点并非解决所有问题,而是给面试官留下良好印象。解题是留下好印象的重要条件,但光解题不够。你需要与他交流。即使有些问题不能完全解决,把想到的思路说出来也比什么都不说要好。总的来说,你需要向面试官展现一个良好的个人印象。
六道谷歌面试题精讲 🧩
以下是本节课的重点,我们将讲解六道从简单到困难的谷歌面试题。
例题一:求和问题
给定一个数 N,求不超过 N 的所有能被 3 或者 5 整除的数的和。例如,N=9,能被 3 整除的数是 3, 6, 9;能被 5 整除的数是 5。和为 23。
这是一个数学问题。我们可以枚举能被 3 整除的数,其项数为 N/3(向下取整)。同理,能被 5 整除的数的项数为 N/5。这两个序列有重复,重复的数是既能被 3 整除又能被 5 整除的数,即 15 的倍数,项数为 N/15。
这三个都是等差数列。关键是对等差数列求和,公式为 (首项 + 尾项) * 项数 / 2。最终答案为能被 3 整除的数的和加上能被 5 整除的数的和,减去能被 15 整除的数的和。
在面试中,我们需要与面试官交流 N 的范围,确认是否会超过 int 类型,是否需要使用 long long。因为等差数列的和是 N^2 级别的,很可能超过 int。通常面试官会告知无需考虑,但主动交流是必要的。
核心代码非常简单:
int sumDivisibleBy(int n, int divisor) {
int count = n / divisor;
return divisor * count * (count + 1) / 2;
}
// 最终结果: sumDivisibleBy(N, 3) + sumDivisibleBy(N, 5) - sumDivisibleBy(N, 15)
例题二:合法字符串计数
如果一个字符串只包含 A、B、C 三个字母,并且任意相邻三个字母不全相同(即非法),问长度为 N 的合法字符串有多少个。
这可以用类似动态规划的递推思路解决。定义两个状态:
dp[i][0]: 长度为i,最后两位不同的合法串数量。dp[i][1]: 长度为i,最后两位相同的合法串数量。
以下是状态转移的推导:
- 对于
dp[i][0](最后两位不同):- 如果前
i-1位最后两位不同(状态0),最后一位可以加一个与这两个字母都不同的字母,有1种选择;或者加一个与倒数第二位相同的字母(此时最后两位变为相同,状态变为1),有1种选择。所以贡献为dp[i-1][0]。 - 如果前
i-1位最后两位相同(状态1),最后一位必须加一个与倒数第一位不同的字母(以保持最后两位不同),有2种选择。所以贡献为2 * dp[i-1][1]。 - 因此,
dp[i][0] = dp[i-1][0] + 2 * dp[i-1][1]。
- 如果前
- 对于
dp[i][1](最后两位相同):- 只能由前
i-1位最后两位不同(状态0)的情况,最后一位加上一个与倒数第一位相同的字母得到,有1种选择。所以dp[i][1] = dp[i-1][0]。
- 只能由前
初始状态:dp[1][0] = 3 (A, B, C),dp[1][1] = 0。
最终答案为 dp[N][0] + dp[N][1]。
由于 dp[i] 只依赖于 dp[i-1],可以进行空间优化。时间复杂度为 O(N)。对于更大的 N,可以使用矩阵快速幂优化到 O(log N)。
例题三:最少交换次数
给定一个包含 0 到 N-1 的排列的数组,只允许将任意一个数与 0 交换。问至少需要交换多少次,才能将数组排序(即 i 在 A[i] 的位置)。
这个问题涉及组合数学中的“圈”概念。一个排列可以划分为若干个不相交的环(圈)。例如,0 在位置1,1 在位置2,2 在位置0,这就构成了一个长度为3的圈。
关于交换次数的结论:
- 对于一个长度为
m且包含0的圈,最少需要m-1次交换(只与0交换)使其所有元素归位。 - 对于一个长度为
m且不包含0的圈,需要先将0交换进这个圈(1次),然后进行m-1次交换使元素归位,最后再将0交换出去(实际上归位后0自然在正确位置,但总计需要m+1次交换)。
因此,算法步骤如下:
- 找出数组中所有的圈。
- 对每个圈,如果包含0,答案增加
(圈长度 - 1);否则,答案增加(圈长度 + 1)。
寻找圈可以通过遍历数组并标记已访问的元素来实现,时间复杂度为 O(N)。
例题四:最少移动次数
给定一个 1 到 N 的排列,每次操作可以将任意一个数移到序列末尾。问至少需要几次操作能使序列有序(升序)。
关键思路是寻找最长前缀有序子序列。即从序列开头开始,找到最长的连续上升子序列,且其值是从1开始连续递增的。
例如,序列 [3, 1, 2, 4, 5]。从开头找:1不在开头,但我们可以发现,如果保留最终在正确位置上的元素,它们必须是 1, 2, ... 这样连续出现在原序列中的。我们从值1开始检查:
- 1在原序列中。
- 2在1的后面。
- 3应该在2的后面,但原序列中3在1的前面,因此有序链在3处断开。
因此,最长的可保留前缀是 [1, 2]。从第一个不满足条件的值(即3)开始,后面的所有数(3, 4, 5)都需要被移动到末尾。所以操作次数为 N - 2 = 3。
算法只需扫描一次数组,时间复杂度为 O(N)。
例题五:拆墙迷宫(BFS扩展)
在一个矩阵迷宫中,S 是起点,E 是终点,. 是路,# 是墙。第一问:从 S 到 E 的最短路径步数(经典BFS)。第二问:如果最多可以拆掉 K 面墙(K=3),求最短路径步数。
第一问是标准的广度优先搜索(BFS)。
第二问的暴力方法是枚举所有可能的 K 面墙的组合,对每种组合进行BFS,复杂度极高(O((N^2)^K * N^2)),不可接受。
一个巧妙的建图方法:构建一个 (K+1) 层的立体图。
- 每一层都是原始迷宫的复制。
- 层内的移动规则与普通BFS相同(只能走到非墙位置)。
- 层与层之间的边表示“拆墙”操作:如果当前层某个位置
(x, y)的邻居是墙,那么可以从当前层的(x, y)点,建立一条指向下一层对应墙位置(x', y')的边。这表示拆掉了这面墙,进入了下一层。 - 起点位于第0层的
S,终点可以是任何一层的E。
在这个 (K+1) 层的图上进行BFS,找到从起点层 S 到任意终点层 E 的最短路径。路径长度就是最少步数,而跨层的次数就代表了拆墙的数量。这种方法将复杂度降到了 O(K * N^2)。
例题六:最大长度乘积(位运算)
给定一个单词字典,找到两个单词,使得它们不包含相同的字母,并且它们的长度乘积最大。允许预处理字典。
这是一个开放性问题,需要与面试官交流思路。
基础方法(暴力枚举):
将每个单词转换成一个26位的位掩码(bitmask),如果单词包含某个字母,则对应位为1。对于两个单词的掩码 mask1 和 mask2,如果 (mask1 & mask2) == 0,则说明它们没有公共字母。
然后枚举所有单词对 (i, j),检查掩码并计算长度乘积,取最大值。时间复杂度为 O(N^2 * L),其中 L 为单词平均长度。当 N 很大时(如百万级)不可行。
优化方法(预处理+位运算DP):
- 预处理:计算每个单词的位掩码
mask和长度len。 - 状态定义:令
dp[mask]表示所有字母集合是mask的子集的单词中,最大的长度值。例如,mask=0111(表示字母a,b,c),那么dp[mask]可能是单词 “ab”(掩码0011)、“ac”(掩码0101)或 “abc”(掩码0111)的长度最大值。 - 状态转移:为了计算
dp[mask],我们可以用其子集来更新。一种高效的方式是:- 首先,用每个单词自身的掩码
w_mask更新dp[w_mask] = max(dp[w_mask], len)。 - 然后,对于每个
mask,枚举其二进制位,将某一位的1变为0得到sub_mask,用dp[sub_mask]来更新dp[mask]:dp[mask] = max(dp[mask], dp[sub_mask])。这样,dp[mask]最终就存储了其所有子集掩码对应的最大长度。 - 这个动态规划的过程时间复杂度为
O(26 * 2^26)。
- 首先,用每个单词自身的掩码
- 求解答案:遍历每个单词
word,其掩码为w_mask,长度为len。与它无公共字母的单词,其掩码other_mask必须满足(w_mask & other_mask) == 0,即other_mask是w_mask的补集complement的子集。而dp[complement]正好存储了这样的单词的最大长度。因此,候选乘积为len * dp[complement]。遍历所有单词取最大值即可。 - 复杂度:预处理
O(N*L),DPO(26 * 2^26),求解O(N)。空间复杂度O(2^26)。
这个方法利用位运算和状态压缩,将问题转化为了对子集最优值的查询,适合字典很大但字母集有限(26个)的场景。
课程总结 📚
本节课中我们一起学习了技术面试的沟通技巧和六道谷歌经典面试题。
首先,我们再次强调一定要和面试官积极交流,不要把面试当成笔试,要展现思考过程。即使问题没有完全解决,交流也能留下好印象。
其次,我们通过六道例题涵盖了多种解题技巧:
- 数学求和:利用等差数列公式,并注意数据范围。
- 递推/动态规划:解决计数问题,定义合适的状态。
- 组合数学与环:理解排列的环结构,分析最优操作。
- 贪心思维:寻找最长可保留序列以最小化操作。
- 图论建图:通过升维(多层图)将复杂条件转化为标准BFS。
- 位运算与状态压缩:利用位掩码表示集合,并通过动态规划预处理子集最优解,以优化枚举。
谷歌以及很多公司的面试题并没有固定套路,题目往往很灵活。多总结、多思考、多归纳,才能培养出解决问题的“感觉”。希望本课的内容能对大家的面试准备有所帮助。祝大家好运!





人工智能—面试求职公开课(七月在线出品) - P8:链表面试题精讲 📚
在本节课中,我们将学习链表相关的核心面试题。链表是数据结构的基础,其面试题虽然难度不高,但细节繁多,需要扎实的基本功和清晰的思路。我们将从链表的基本操作讲起,逐步深入到几个经典的面试问题,帮助你掌握解题技巧。
链表简介 🔗
链表是一种常见的数据结构。它的一个特点是,每个元素只能通过指针来连接下一个元素。这里指的是单链表。我们不能在常数时间内访问第K个元素,这和数组不一样。数组可以直接通过下标访问第K个元素。链表只能从表头开始一个一个找过去。
链表的分类包括单链表和双链表。单链表只有指向下一个节点的指针。双链表有一个指向前一个节点的指针和一个指向后一个节点的指针。
另一个分类是循环链表。它和单链表、双链表本身并不是独立的分类,而是有交集。它指的是首尾相接,把最后一个元素和第一个元素连起来形成一个圈,这就是循环链表。
在编程语言中,Java有LinkedList,C++的STL里有list。C语言没什么好说的,只能通过指针来实现链表,这是最基本的一种陈述。
面试题总体分析 📝
链表的面试题都不是很难,但是很繁杂。主要涉及链表的基本操作,包括插入、删除、翻转。翻转有多种形式:一种是从头到尾都翻转过来;第二种是给定一段区间,例如第M个节点到第N个节点,把这段翻转过来,其他地方不动;还有一种是把链表分成每K个一组,每组翻转一次。总的来说都是翻转。
还有涉及链表排序的,比较经典的是归并排序。这里提到了,还有快排的partition。
还有一种类型是复杂链表的复制,后面有例题。再后面是链表,这里指的是单链表,判断是否有环。如果有环,环的起点在哪里?环的长度有多长?后面有个例题会说明。
链表的倒数第K个节点,其实可以通过快慢指针找到。只要快指针和慢指针相差K个节点,这样扫描一遍就能找到倒数第K个节点。当然也可以先统计出链表的长度,然后再找第K个节点,都比较简单。
还有一类是随机返回链表的一个节点,要等概率的。这其实不属于链表的范围,它属于随机采样的范围,只是从全面性的角度把它列出来。
还有一个是链表和其他数据结构,主要指的是二叉树。二叉树和双向链表之间可以相互转换。这个本来举了一个例题,后来因为时间关系删掉了,会放在“树和图”那一章里面讲。
例题一:链表的插入与删除 ✏️
上一节我们介绍了链表的基本概念和常见面试题型。本节中,我们来看看最基础的操作:链表的插入与删除。
链表的插入与删除是最基本的操作。这里只列出来让大家熟悉一下基本操作。我们讲最简单的类型,就是单链表。
遇到链表的问题,建议先在纸上画一下要做哪些操作,一画就明了了。我们需要考虑哪些指针需要修改。显然,这个节点之前的那个节点,也就是前驱节点的next指针要改变,因为它要指向新节点。新节点自己的next指针肯定也要变,要指向原来前驱节点的next。所以我们至少要改变两个指针。
插入一般要找到插入之前的那个节点,也就是前驱。有什么特殊情况呢?在head之前插入。这里其实还包括head等于空的情况,head就是表头。如果表头等于空,这个链表就没有东西,那我们插入就只剩下一个东西了。这种情况其实和在head之前插入可以统一起来。
我们直接node就作为新插入的节点。假设我们已经构造好了,我们执行node.next = head和head = node。其实这两步就完成了插入。这个特殊情况其实就是说插入之后,表头变了。
大部分情况都是在中间插入,插入之后,表头不变。因为我们链表的问题一般都是写一个函数,传进去一个表头,传回来还是一个表头。因为这个链表会有变化,我们只有通过表头来访问链表。所以链表的问题都是传入一个表头,传出也是一个表头,表头非常关键。所以我们要考虑得非常清楚。
一般情况就是这个样子。我刚才说了,改变两个指针。改变新节点的指针,改变前驱节点的next指针。注意这两个顺序,因为我要把新节点的next赋值为前驱节点的next,所以我必须要先改变新节点的next,再改变前驱的next。所以这个比较简单。
删除其实差不多。哪些指针要修改?其实就是前驱的next,把它跳过去就可以了。把它指向它的next的next就可以了。特殊情况是表头会变,因为可能把表头删了。
如果链表里面只有一个元素的话,删了之后它就是null,因为temp = head.next就是null。我们在p后面删除的话,怎么删呢?先把p的下一个节点记下来,然后把p.next赋成temp.next,再把temp,也就是原来p的下一个节点,也就是要删的那个节点的空间释放掉。所以删除也就这么几步,非常简单。
以下是几个思考题:
- 刚才讲的是最简单的情况,就是单向链表。双向链表怎么插入、怎么删除?
- 另一个是循环有序的链表。就是这个链表本身,例如
1->2->3->4->5,这是一个有序的,并且它把5的next指向1了,就是在一个圈里面。那么要插入一个节点,其实还是有点困难的。例如插入的节点有重复值,或者插入的是最大的、最小的等等,插入的位置怎么选取,要把它想好了。不过个人建议是先把它断开,插好之后再把它最后一个节点指到第一个节点上,这样不容易错。但其实有很多不用断开它,加了很多条件判断的方法也可以做。这个留给大家思考。
这里想提一下所谓的lazy delete,就是懒删除。假设我们要删除node这个节点,这里要注意,node不是最后一个节点。那么我们找到node,我们现在就在node这个节点上,我们要删除它。这个在单链表里面一般情况是无法实现的,因为我们必须在它之前的那个节点位置才能把它删除。那现在我已经到了这个节点,我怎么把它删除呢?
有一个比较懒的办法,就是我把node的下一个节点复制过来。我这里假设这个node只有一个字段,就是val,只有一个整数值。我把下一个节点的内容信息都复制过来。那这样的话,node本身和node下一个节点长得都一样了。那么我再把node.next删掉。实际上这并不是真正的删除node,我真正删除的其实是node的下一个节点,我只是用node把它的下一个节点复制了一遍,再把下一个节点删除。所以这是一种删除方式。
例题二:单链表的翻转 🔄
上一节我们讨论了链表的插入与删除。本节中,我们来看看另一个基础操作:单链表的翻转。
单链表的翻转非常简单,我就快速地讲一下。前两个例题都是帮大家理解链表最基本的操作。
翻转其实就是把当前节点拿过来作为已经翻转结果的表头就可以了。因为我们可以改变链表的顺序,类似于一个栈。假设我前面已经翻好了,我把这个节点拿过来作为新的结果的表头,那么前面这些节点都已经翻好了。
我们看一下,非常简单。result就是我翻好的这个链表的结果。那么我先要保存当前这个节点的下一个节点,因为我下一次循环就要翻这个节点了。那么我现在要翻的就是head这个节点。head节点的next等于我之前翻好的那个链表的表头,那么表头变一下,就是结果的表头变一下,那么head变成它原来的下一个节点。所以这个顺序很重要,主要强调一下顺序。并且这里面head为空的情况,其实都已经在这个里面隐含着了。因为head为空result肯定为空,并且翻转之后的最后一个节点的next肯定也是空,因为第一个节点就把head.next = result了。
还有一个就是顺序要注意,先把这个节点的下一个节点保存下来才能改变它,不然链表就断掉了。前两个例题都非常简单。
以下是思考题:
- 如何翻转一部分链表?例如LeetCode第92题,就是翻转第M个元素到第N个元素之间的那一段。其实我们需要把它从第M个元素之前断开,第N个元素之后断开,把M到N翻转了,翻转之后还得再连回来。连回来的时候,前面那部分就是第M个元素之前的那部分的最后一个元素我们必须得有,因为我们要从最后一个元素把那个翻转那部分接回来。后面那部分第N个元素之后那部分的第一个元素我们也必须有,还得接回来。还有很多特殊情况需要处理,例如
M等于1,也就是链表第M个元素之前没有东西怎么办?第N个元素后面没有东西怎么办?所以很多特殊情况需要考虑,尽管这几步说起来非常简单。 - 另一个是刚才说的LeetCode第25题,每K个元素翻一次,一样的。同样我们把前面翻转好的部分都保存好,现在要翻转这K个,把它单独截出来,翻转之后再把它连上来。那后面没有处理的,还要再继续处理。还有如果最后有余数不足K怎么处理?所以有很多细节的问题,说起来都不太难。这个留作思考题。
例题三:判断链表是否有环并找出环起点 🎯
上一节我们学习了链表的翻转。本节中,我们来看一个经典问题:判断链表是否有环,并找出环的起点。
第三个例题其实比较经典。它是说给你一个单链表,问这个链表是否有环。其实就是说这个链表可能建错了,我最后一个节点的next不是空,而是它指向了前面的某一个元素。这样的话,链表就转起来了。如果沿着链表走,就会转起来,没有头没有尾,就是从头直接走,走到一个节点,永远走不到尾。
我们怎么判断这种情况呢?这个是LeetCode上141、142题。141题和142题有一个区别在于,有一个题是说只要判断有没有环就行了,返回一个布尔值true/false。那另外一个题是说如果有环的话,把环的起点找出来,就是环的起点是哪个节点。当然这个环可能不是从开头就产生的,有可能是一个类似于阿拉伯数字6的这种形状,就是它开始有一段,然后才有一个环。所以环的起点也需要求。
我们先来看一下如何找环。最直接的办法就是我们用一个set存放每个节点的地址。注意set里面存放的元素必须是有序的,这个序不一定是大小关系,你需要给它定一个序,所以并不是任何的object都可以放到set里面。我们需要给它规定一个顺序。那地址就具备这种顺序。其实地址我们可以把它理解为整数,尽管这种理解可能不是十分正确,但是其实真正意义上地址它就是一个整数,只不过这个整数我们不能做传统的加减运算。
最直接的方法,我们就用一个set把经过的节点都存下来就好了。我们就这样沿着头指针走。如果找到了之前经过的一个节点,那么它显然就是走环绕回来的,就有环,否则就没有环。这是一个方法。但它额外用了一个set,主要是空间复杂度有一些,时间复杂度也有,因为set里面查找是O(log n)的。当然我们把它改成哈希的话,就可以做到期望的时间是O(n)的,但问题在于还是用了额外的空间。
有没有不用set的办法呢?我们来看一下。我们用两个指针,p1和p2。p1每次走一步,p2每次走两步。如果有圈,它们一定会相遇。请大家思考一下,为什么一定会相遇?如果相遇的时候,我们如何找到交点?
我定义一些变量。圈长是N。链表起点到圈的起点的距离,也就是那个“柄”的长度,是A。p1到起点的时候,因为p1是每次走一步的那个指针,它到了圈的起点的时候,p2肯定比它进圈早,因为它走得快。p2在圈的X位置,这个X是指距离圈起点的位置。
刚才说了,p1到圈起点后,p2已经走了X步了,在圈里面。那么他们相距其实是N - X步,因为在圈里面两个方向,一边是X,那边就是N - X,圈总长度是N。那么他们N - X步肯定会相遇。这是一个追击问题,因为每走一步距离缩小一,p2走得快,每走一步,他走2,p1走1,他们相距N - X,所以走N - X步肯定相遇。
假设我们相遇的时候,相遇点到圈起点的距离是B。那么p1走的距离实际上是A + B。其实p1走的肯定小于一个圈,因为p1在圈里面只走了N - X步,圈的长度是N,所以p1肯定没有绕过一圈在圈里面。所以它走的距离就是A + B,因为这个相遇点距离圈起点是B,前面那个柄的长度是A,所以p1走的是A + B。
那p2走的是什么呢?p2其实在圈里面可能绕了很多圈了,可能绕了K圈了。这里其实p2不一定比p1只多走了一圈,它可能比p1在里面已经绕了K圈了。这是在p1进入圈之前,p2早就进入圈了。如果这个柄比较长,圈比较短的话,p2在这个时候已经在圈里面绕了很多圈了。但无论如何它绕了K圈。那么他走了就是A + B + K * N,因为N是圈长,K是一个整数。
右边是怎么回事?右边是刚才说了,p2走的是p1的二倍,p2的速度是p1的二倍,所以p1走A + B,p2显然走了2*(A+B),所以这就有了这么一个等式:A + B + K * N = 2 * (A + B)。
这个等式对我们来讲有什么用呢?把A + B消掉,就是A + B = K * N。这就说明A + B,也就是p1走的距离,或者说柄的那段长度再加上圈的起点到这个相遇点的那个距离,正好是圈长的整数倍。
那这个有什么意思呢?我们在想,我们现在假设两个点已经相遇了,他们相遇的时候,距离圈起点的距离是B。那么我们把p1拉回起点,这个不是圈的起点,拉回链表的起点,就是第一个节点。把p2不动,因为它就在距离B的位置。那么我们再走。这时候走并不是一个一步一个两步了,每个都走一步,就是p1走一步,p2也走一步,然后p1再走一步,p2再走一步,就这样一步一步走。
这样走A步之后,我们看一下出现什么情况。因为我刚才说A的定义是起点到圈的起点的那个长度,也就是那个柄的长度。所以A步之后,p1肯定到了圈的起点,因为这个A就是这么定义的。p2呢?p2在B的位置走的,在距离圈起点B的位置走的,因为相遇点是距离圈的起点是B。它走A步之后,我们有这个式子A + B = K * N,它走A步之后,算上B的那段,恰好也是圈长的整数倍。所以p2也到了圈的起点。这时候p1、p2又相遇了。
所以我们在第一次相遇之后,把p1拉回起点,然后两个指针再一步一步地走,而不是一个一步一个两步地走。走A步之后再次相遇的时候,我们就找到圈的起点了。找到圈的起点,再找圈长就容易了。因为我们顺着那个圈再走一圈,例如p1不动,p2再走,再相遇的时候,记一个数,那个长度就是圈的长度。所以关键是如何找到圈的起点,圈长是好办的。
我们来看一下这有个示意图。这个就是我说的。这个是入口,就是圈的起点,这个是链表的起点。p1进入入口的时候,p2是在这里的。然后呢,第一次相遇点就是这个红的位置,实际上就是说刚才说的这段距离是A,红的这段距离是B。p1走的是A + B,p2走的是可能是好多圈,但也到B了。但无论如何,这个A加上B的长度,是这个圈长的若干倍,这个刚才已经证明了。
那现在如果他们在第一次在这相遇了,相遇之后,我把p1拉回来,p2从这走。因为他相当于p2先走了B,在圈里面先走了B。那么p1走A的时候,到圈的起点,p2走了也走了一个A,他走了一个A,算上之前那个已经在圈里的B,他肯定也走到了圈的起点。这个就是A + B是圈长的整数倍的应用。他们在同时到相遇的时候,恰好到了圈的起点,我们就找到圈的起点了。那这是一个证明。
那么大家可以看一下我的代码。其实代码写起来非常简单。
首先,两个都是头指针,这步就是一个走一步,一个走两步的过程。因为p2快,所以我没有必要判断p1是不是空。注意我这里混用了那个NULL空值和0。如果p2是空或者p2的next是空,就说明这个链表是有限长的,因为肯定已经到表尾了。那否则的话我p2就走两步,p1走一步,直到他们相遇。如果推出这个循环,显然就是有环了。如果在这返回就是没有环。
那有环之后,就是我刚才说的,把p1拉回起点,然后再一步一步走,这个是p1走一步,p2走一步。实际上这个推出来之后,这个p2就在圈的那个刚才我说的B的那个位置,p1拉回来,然后他们在同时走A步,走A步的时候会同时到达圈的起点。这时候p1、p2相同了,相同的时候返回哪一个都可以,都是起点。这个就是第三题,我觉得是比较经典的一个问题。
例题四:两个单链表的交点 🔀
上一节我们解决了链表环的问题。本节中,我们来看一个相对简单的问题:寻找两个单链表的交点。


第四题其实我觉得比第三题简单很多。可以看一下,在两个单链表里面找到它们的

人工智能—面试求职公开课(七月在线出品) - P9:探秘校招笔试面试
在本节课中,我们将一起探讨当前人工智能领域校招笔试与面试的常见题型、解题思路以及背后的核心知识。课程内容涵盖概率论、算法、逻辑推理等多个方面,旨在帮助初学者理解题目本质,掌握解题方法。
概述:校招笔试面试的趋势与考察重点
我的总体体会是,现在的笔试和面试题目越来越难。除了考察编程语言、数据结构、数据库、操作系统、计算机网络这些传统内容,还增加了许多算法、逻辑以及数学方面的题目。
数学方面的题目,大家要尤其重视概率论这个方面的内容。概率论、数理统计这方面的内容大家尤其需要重视。此外,机器学习的题目也做了一些增加,难度也有所提升。贝叶斯属于机器学习的内容。
我个人觉得,当然你也可以把它归类到数据挖掘,意思差不多。概率论这方面,可能更多考察的就是大家在概率论和数理统计那门课程一般的学习过程中,本科生或研究生的教材内容。我一般是感觉是这样。只不过在面试里面会大量充斥着一些关于算法和逻辑的题目。
大家有时候会有疑问,在以后的工作中,算法和逻辑题真的会用吗?我觉得这个事情,咱们可以先讨论两句。事实上,算法题目分两种。有一类工种真的会用到算法,但并不是所有内容都需要它。尤其对于校招来讲,公司没有一个更好的尺子来判断你的能力,他总要筛选一些人,选择他觉得合适的。在这些基本内容以外,他觉得考察算法非常接近于他所想要的人才,就会拿这个东西作为一个衡量标准。
另外,即使是突击学的算法,如果你学得还不错,通过了面试官和公司的考核,他觉得OK,那么你在学习其他内容的时候,也应该是一个非常好的苗子。基于这个考虑,所以好多公司在做笔试面试题目时会增加算法题目和逻辑题目。我想他的原因可能是这样,这是我个人的一个想法。
对了,还有就是做广告了。大概在10月11号,每周六、周日下午2点到4点,是我们即将开始的10月算法班。还有一个9月机器学习班,它的时间是10月11号开始,每周六和周日的晚上7点到9点。我们的9月算法班和10月算法班,其实开始时间都是10月11号。
我大概罗列了一下算法班的大纲以及机器学习班。因为机器学习班有24课,内容实在很多,我就随便截了一下后面的那几个。大家有兴趣可以看一下。关于报名,直接访问我们的官网 julyedu.com。打开这个官网之后,就能看到报名链接以及一些相关内容。julyapp.com 是附属于官网之下的一个专门为手机APP做的链接。在这里面大家可以发现我们的APP有安卓版和iOS版,可以在里面下载。julyedu.com,大家打开这个就好了。里面有论坛、社区,还有一些课程和相关内容。
我大概贴了一下某些公司今年的校招题目。我们随便看一个,比方说第一个为例。在一个8列乘6行的矩阵中,从A点移动到B点的过程之中一共有多少种不同的走法?要求是只能够向上和向右走,不能够走回头路,并且不能够经过P这个点。这种题目,我觉得难度应该算是中档,不算难,但还是需要稍微考虑一下。像这些题目,等会儿会跟大家一个一个去梳理,看看到底怎么做。
有这种题目,我觉得其实是个算法题。而第二个求期望的题目,其实是考察概率论和数理统计方面的内容。第三个是一个典型的逻辑题目,需要知道怎么样去推导推理。这个题目,你说它是算法也行,说它是推理也可以。就是说取这100个数,它们可能的绝对值的差的和,最大是多少。像这个题目显然是一个比较怪异的概率题目。这个题目其实有一个更有趣的问题叫“三门问题”,大家有兴趣可以在我们的7月算法APP上或者一些网站上去搜一下这个问题,都是一些很有趣的关于概率的、并且经常考的内容。第四个看起来像是一个推荐系统方面的内容,我们待会看看它到底想考察什么,其实没有那么难。我们就不再一个个过了,只是跟大家看一下今天要聊的一些相关题目。
第一节:概率问题与几何解法
我们先看刚才说的那个所谓的推荐系统里面的一个小东西。它先介绍一个背景,商品推荐的场景。如果你是过分地聚焦在商品推荐上,其实往往会损害到购物体验。有时候系统会选择一定程度的随机性来给用户增加一些惊喜感,惊喜度也是推荐系统需要考察的一个度量标志。
假设在某个推荐系统里,想去计算A和B这两个商品与当前访问用户的匹配情况。假设用户来了,并且算出来了用户和A商品的匹配度是0.8,和B商品的匹配度是0.2。这是我们经过其他相应知识算出来的。然后,系统随机地给A商品生成一个从0到0.8的均匀分布的得分,给B商品生成一个从0到0.2的均匀分布的得分。让你算一下,B的最终得分会大于A的最终得分的概率是多少?
大家先把题目读一下。这个题目很长,但考察内容看起来像是推荐系统、向机器学习,但其实后面是让你算概率。这个题目大家有感觉吗?怎么做呢?
是这样,我们可以在这里算面积就可以了。怎么算呢?因为涉及A和B两个商品,人类最习惯的是二维的东西。现在它给定的就是算两个商品之间的关系,二元关系是很有趣的关系。A是从0到0.8的一个均匀分布,B是一个0到0.2的均匀分布。因此我画出横轴A和纵轴B,各自组成两个轴。A从0到0.8变化,B从0到0.2变化,因此它们总体的变化域就是从0到0.8和从0到0.2这么一个矩形。现在都是均匀分布的,在二维上也是均匀分布的。
现在让我们算的是B的概率大于A的概率。那很显然,画一条A等于B的斜线。位于三角形区域的点,比方说红色点,大家会发现这时候B的值是这么高,如果鼠标在这儿的话,A是这么大,所以说B是大于A的。所以三角形这块面积是满足B大于A这个情况的。一共满足条件就这么大,总共的是矩形面积。算一下三角形面积,算下矩形面积,一除就好了。
蓝色三角形面积我算成0.02,矩形整个的是0.2乘0.8等于0.16,一除等于0.125,也就是八分之一。这个题目就做完了。
像这种题目,往往遇到了这种算概率,并且是二元的事情,你其实直接画个面积一下子就出来了,没必要去真正地推导,推导的话反而麻烦了。
第二节:约会问题与概率计算
为了让大家能够去理解这个事情,咱再找一道题目。这个题目也是今年的一个实际的面试题。大家思考一下,假定A、B两个国家元首,他们相约在首都机场晚上8点到晚上12点之间去交换一个重要的文件。如果A国的飞机先到,那么A国的元首会等一个小时。如果B国的飞机先到,那么B国的元首会等待两个小时。现在假定这两架飞机都是从20点到24点均匀降落在机场的,它的降落时间分布是均匀分布的。算一下,最终能够在20点到24点完成文件交换的概率是多大?这里加一个小提示,假定交换文件本身是不需要时间的,就他们俩如果能碰面,就算交换成功了。
这个题目我没有给大家给出解析,大家可以思考一下怎么做。我们的APP上是有的,有兴趣我可以让大家先想想。在今天,比方说假定一个半小时之后结束的话,最后我把答案贴出来。大家可以想想,都是一样的一个套路。这个题目比刚才那个题目难度要略高一点,略难一点点,就是它的画图要比它麻烦一点点。
第三节:进制转换问题
我们来看第二道题目。给定一个公式是 84 * 148 = ECA8,如果成立的话,请问这个公式采用的是几进制?
这种题目有一个放之四海而皆准的标准解法。假定它是一个X进制的,那么84就是 8*X + 4,148就是 1*X^2 + 4*X + 8。ECA8,显然B是11,A是10,所以是 E*X^3 + C*X^2 + A*X + 8,即 14*X^3 + 12*X^2 + 10*X + 8。左边式子相乘等于右边式子,化简一下就出来了。这个式子不可能等于零,因为X是大于零的整数,这里面只有X等于12是可以的。这种做法是完全最标准的。大家直接遇到这种题目时,如果上来硬算,直接这么算,一点问题都没有。这是第一个常规做法。
第二个启发式做法,计算尾数就够了。事实上,如果在十进制体系下表述的话,左侧的尾数是4,右侧的个位数是8。4乘8是32,右侧个位数是8,这个32和8的差是24。那24的差既然是24,它的进制必然是24的一个约数。这里面只有12是24的约数。因为你这个24一定要能够整除它的进制。这样子也可以搞定。这种做法更简单。
我说明两点:第一,我们用十进制来做这个事情,仅仅是个技术习惯。第二,第二种做法可以作为第一种解法的一个辅助手段,来相互验证一下是不是做对了。


第四节:构造法与贪心策略


这个题目稍微有点麻烦,它是一个N个数的差,我把它叫做“绝差之和”问题。题目是这样的:从1到100这100个数任意的排列,形成一个环。要求求两个相邻数的差的绝对值求和,最大是多少?当然有些选项,大家看看哪个答案对。
首先,题目本身没问题吧?从1到100连完做成个环,后面数减前面那个数取绝对值,然后把这个绝对值加起来,问这个绝对值的和最大能够达到多少。
我个人理解这个题目,用构造法是最合适的。他这里让我们算的是相邻两个数的差的绝对值。那这样的话,相邻两个数的差,这里从1到50,从51到100分两部分。我把1到50叫小数,51到100叫大数,这是我个人的定义,为了表示方便。我们把这个相邻元素的差的绝对值简称叫“绝差之和”。

我们看看怎么样去做。现在给定一个贪心的思路:为了让绝差之和最大,我们就应该先验地知道,要避免把大数跟大数放在一块。你俩大数放在一块儿的话,抵消掉好多部分。因此我就考虑把大数和小数间隔着排。就是1, 100, 2, 99, 3, 98, ..., 50, 51。这个序列就是我想要的绝差之和最大的那个序列。
为什么呢?我们需要给出一个证明。我们可以想想,如果序列里面两个小数X和Y进行交换,或者是两个大数A和B进行交换,或者是大数和小数进行交换,它都能得到一个我们想要的结论,或许就能搞定了。因为就这三种情况,除此以外没别的情况。我把所有情况都分析完不就完了吗?
- 小数和小数换:假设用小数X和小数Y来换。交换之前和交换之后的绝差之和是一样的。
- 大数和大数换:假设用大数A和大数B来换。交换之前和交换之后的绝差之和也是一样的。
- 大数和小数换:假设把大数A和小数Z做交换。交换之后会发现,绝差之和会变小。
因此,交换了还不如不交换,交换的话会导致绝差变小。而刚才前两种情况,交换的话相当于没有任何效果。因此,相当于我就证明了原始的这种间隔状态是能够使得绝差之和最大的。
而这种原始状态,1, 100, 2, 99, 3, 98...这种情况,它的绝差是多少呢?后者减前者:100减1是99,99减2是97,98减3是95,...,51减50是1,然后1和51的差是50。这个数我们加起来很方便得到5000。因为1到100加起来是5050,这样就少了50,所以是5000。
第二,从刚才的小小交换和大大交换会知道,最终的这个绝差最大序列不是唯一的。你可能构造一个这样的序列,我也可能构造一个别的,仍然是最大的。但是跑不出去,我通过小小和大大做交换,能够交换成跟你一样的。
第五节:动态规划与棋盘路径
这是一个非常经典的题目,经典到几乎任何一本书都会谈到。给定一个M乘N的矩阵,矩阵里面每一个位置都是一个非负整数。在左上角放个机器人,这个机器人每次只能够向右和向下走,不能够往回走。最终会走到右下角。请问从左上角走到右下角,经过的最小路径和是什么?
先把题目的审题验过了,题目有问题吗?
有朋友谈到了动态规划(DP),组合排列,深度优先搜索。深度优先搜索有点过分了,因为时间复杂度有点高。贪心和动态规划,本质可以看的是一码事。这个题目没必要上A*算法。
我们看这个题目怎么做。既然它要从左上走到右下,我们就来考察某一个点它的情况。首先我们知道,在走的方向决定下,同一格子不能走两次。如果在红色这个点的时候,那么它上一次要么来自上方这个绿色格子,要么来自于左侧这个绿色格子,除此以外没有别的情况。
我们就用一个 dp[x][y] 来表示它位于(x,y)这个点的时候,它的最短路径和。那么,dp[x-1][y] 是它位于左边这个点的最短路径和,dp[x][y-1] 是它位于上面这个点的最短路径和。那么,这两个最短路径和,它们各自加上 a[x][y](当前点的值)走到当前这个红色点,这两个值谁小,我就取谁,就是能够走到红色这个点的最短的那个路径。当然里面可以把公因式 a[x][y] 提出来。
这是普通一个点上的事情。第二,如果假定它位于第一行呢?它如果位于第一行,比如绿色这个点的位置,它只能够从左到右依次加上来。因此第一行的值就是第一行这些数的累加。第一列的点,只能是从上到下依次累加得到。所以第一行第一列我单独处理。而中间某一个点,我用一个最小值(谁小我算谁的)加上去,最后就能得到 dp[m][n] 那个值,就是走到右下角的时候,它的最短路径和的值。
这个题目咱的解析就算是说完了。
动态规划的深入分析
下面我们对这个题目做一点更进一步的分析。
- 最大路径问题:如果题目里把求最小路径变成求最大路径,其实上面所有的分析过程就把里面的
min变成max,所有的都不变。所以这个题目相当于蕴含着两个对偶的问题,本质一样。 - 空间优化(滚动数组):通过刚才的写法,其实可以写出更规则化的状态转移方程。如果这么写状态转移方程,空间复杂度是
O(m*n)。但是如果你用滚动数组的方式来降维,一行一行地去滚动,空间复杂度就能降到O(min(m, n))。这是空间复杂度从大体上O(n^2)降到了O(n)。 - 与组合数的关系:我们现在假定用
dp[x][y]去表示,如果位于(x,y)这个点的时候,它一共有多少种可行的路径。那么显然dp[x][y]就是dp[x-1][y]和dp[x][y-1]的可行路径加和。这个方程可以推导出组合数公式C(m+n, n)。因此,这个格子取数/走棋盘问题也蕴含着组合数公式,可以说它真的是一个排列组合相关的题目。
路径计数问题的变体
我们把这个题目再琢磨一下,变成例4.1。给定一个8列6行的棋盘,从A点移动到B点,只能够向右和向上走,要求是不能走回头路,并且不能经过点P。那么一共有多少种可行的走法?
如果不经过P的话,怎么做呢?从A到B一共需要往右走7步,向上走5步,因此可行走法有 C(12, 5) 种。但是从A到P呢?向右走5步,向上走3步就到P了,因此是 C(8, 3) 种走法。而P到B呢?向右走2步,向上走2步,C(4, 2) 是6种走法。那么从A到B经过P有多少种呢?C(8, 3) * C(4, 2),这是一个乘法原理,得到336种。而一共多少种呢?C(12, 5) 是792种,792减336就是456。这就是这道题的答案,从A到B一共需要456种。
这些东西都是在我们7月算法APP里面得到的一些内容。只是今天时间稍微宽裕一点,所以我就把这个题目展开,从走棋盘,从各种各样有意义的东西,把它聊出来。
另外,可以根据刚才的说法,其实如果一共有一种,每一个就这么写就完了。比方说这个10怎么得来的,有左边这个值,下面这个值加起来。70怎么得来的,左边这个值下面这个值加起来,只不过P点的值是0。比方说98怎么得到,70加28嘛,我们总能把它算出来也是这个数。如果大家不想这么算,并且你非常任性的话,可以这么玩一下,其实答案肯定是一样的。
第六节:逻辑推理与技能匹配
这个题目叫做“寻找程序员”。我们有A、B、C、D四个人去应聘一个程序员职位。职务的要求是:要Java熟练,要懂数据库,要会Web,此外还需要有C++。谁满足的条件越多,就雇佣谁。并且我们先验地知道,这四个要求(Java、数据库、Web、C++)中,每两个技能组合恰好有一个人是满足的。并且我们知道了一些知识:A和B会Java,B和C会Web,C和D懂数据库,D有C++经验。但是其他的,比方说A是不是懂Web不知道。现在通过这四个已知的条件,以及“两两组合只有一人满足”,最后推一下A、B、C、D各自会哪些技能,并且最后告诉我们应该雇佣谁。
这个题目是实际面试的时候出的一道题目,我把它变成了那种表述方式。咱先把这个题目本身技术层面先解决清楚。
这个题目怎么做呢?我个人的解法是这样:


- 整理信息:把信息整理出来,就是A会Java,B会Java跟Web,C会数据库跟Web,D会数据库和C++。把人和技能列成表格,把各自会什么打上勾,空格处是未知的。并且他告诉我们两两组合只有一人满足。一共有4种技能,两种组合,一共就6种情况:Java+数据库,Java+Web,Java+C++,数据库+Web,数据库+C++,Web+C++。这6种组合,我把它命名作甲、乙、丙、丁、戊、己这么6个条件,它恰好只有一人满足。并且我们知道Java和Web是B满足的,数据库和Web是C
论文公开课(P1):随机梯度下降算法综述 📚
在本节课中,我们将学习随机梯度下降算法的基本原理、面临的挑战以及其各种改进变种。我们将跟随一篇综述性论文的结构,系统地了解不同优化算法的设计思想、优劣以及适用场景,旨在帮助大家在实践中更合理地选择和使用梯度下降类算法。
第一节:梯度下降法简介 📉
在机器学习中,训练模型通常涉及定义一个损失函数 ( J(\theta) ),其中 ( \theta ) 是模型参数。我们的目标是调整参数 ( \theta ),使损失函数 ( J(\theta) ) 最小化。梯度下降法是最基础的优化方法。
梯度下降法的核心思想是:在参数空间的当前点 ( \theta_t ),计算损失函数的梯度 ( \nabla_\theta J(\theta_t) )。梯度方向是函数值增加最快的方向,因此,我们沿着梯度的反方向更新参数,以期望函数值减小。参数更新公式如下:
[
\theta_{t+1} = \theta_t - \eta \cdot \nabla_\theta J(\theta_t)
]
其中,( \eta ) 称为学习率,它控制着每次更新的步长。
然而,标准的梯度下降法存在两个主要问题:
- 梯度计算开销大:在机器学习中,损失函数通常是所有样本损失的和,即 ( J(\theta) = \sum_^ J_i(\theta; x_i) )。计算完整梯度需要对所有样本求导并求和,当样本量 ( n ) 很大时(例如数百万),计算非常耗时。
- 学习率选择困难:学习率 ( \eta ) 需要手动设定。如果设置过大,可能导致算法在最优解附近震荡甚至发散;如果设置过小,则收敛速度会非常缓慢。
第二节:随机梯度下降法(SGD)及其变种 🔄
为了解决标准梯度下降法计算开销大的问题,引入了随机梯度下降法。
随机梯度下降法(SGD)
SGD的核心改进是:每次更新时,只使用一个随机样本来计算梯度估计,并用这个估计来更新参数。更新公式变为:
[
\theta_{t+1} = \theta_t - \eta \cdot \nabla_\theta J_i(\theta_t)
]
其中,( i ) 是在每次迭代中随机选取的样本索引。
优点:
- 计算高效:每次迭代只计算一个样本的梯度,大大加快了单次迭代的速度。
- 可能逃离局部极小值:由于梯度的随机性,算法具有一定的“噪音”,这有助于跳出一些较差的局部极小点。
缺点:
- 更新方向方差大:单个样本的梯度不能代表整体数据的梯度,导致参数更新路径震荡剧烈,收敛不稳定。
小批量随机梯度下降法(Mini-batch SGD)
为了在计算效率和稳定性之间取得平衡,最常用的方法是小批量随机梯度下降法。每次更新时,随机抽取一小批样本(例如32、64、128个)来计算梯度的平均值。
[
\theta_{t+1} = \theta_t - \eta \cdot \frac{1} \sum_^ \nabla_\theta J_i(\theta_t)
]
其中,( m ) 是小批量的大小。
优点:
- 计算与稳定性平衡:相比SGD,梯度估计更稳定;相比全批量梯度下降,计算量更小。
- 利于硬件并行:小批量计算可以很好地利用现代计算硬件的并行能力(如GPU)。
注意:在实践中,当人们提到“SGD”时,通常指的就是Mini-batch SGD。在每个训练周期(Epoch)结束后,通常会将整个数据集打乱(Shuffle),然后重新划分小批量,以消除样本顺序可能带来的偏差。
第三节:SGD面临的挑战与问题 ⚠️
尽管SGD及其变种被广泛应用,但它们仍然面临一些核心挑战:
- 病态曲率与震荡:损失函数的曲面可能在某些方向非常陡峭,而在另一些方向相对平坦(例如狭长的山谷地形)。SGD在陡峭方向会大幅震荡,而在平坦方向进展缓慢,导致收敛速度慢。
- 学习率调优困难:虽然可以通过预设一个衰减计划(如随时间步衰减)来使学习率变小以促进收敛,但如何为不同数据集自动设定合适的衰减策略仍然是个难题。
- 稀疏特征与不同频率的参数更新:对于出现频率差异很大的特征(例如文本中的罕见词和常见词),统一的学习率是不公平的。频繁出现的特征希望用较小的学习率精细调整,而罕见特征则希望用较大的学习率在出现时快速学习。
- 陷入鞍点:在高维非凸优化问题中(如神经网络),鞍点(某些方向是极小值,某些方向是极大值的点)比局部极小值更常见。在鞍点附近,梯度很小,SGD会停滞不前。
第四节:SGD的改进算法 🛠️
为了解决上述问题,研究者提出了多种改进算法。上一节我们介绍了SGD的基本形式及其挑战,本节中我们来看看一系列旨在解决这些问题的著名优化器。
1. 动量法(Momentum)💨
动量法旨在解决病态曲率导致的震荡问题。其灵感来源于物理学:将参数更新过程视为一个有质量的球在损失函数曲面上滚动。动量会累积之前更新的方向,使当前更新不仅受当前梯度影响,也受历史更新方向的影响。
更新公式:
[
\begin
v_t &= \gamma v_ + \eta \nabla_\theta J(\theta_t) \
\theta_{t+1} &= \theta_t - v_t
\end
]
其中,( v_t ) 是当前速度,( \gamma ) 是动量系数(通常取0.9),用于衰减历史速度。
作用:在梯度方向一致的方向(如山谷底部),动量会不断累积,加速下降;在梯度方向变化剧烈的方向(如山谷两侧),动量会相互抵消,抑制震荡。
2. Nesterov 加速梯度法(NAG)🚀
NAG是对动量法的一个改进。动量法在当前位置计算梯度并加上动量。而NAG具有“前瞻性”:它先根据累积的动量向前走一步,在那个“未来”的位置计算梯度,然后进行校正。
更新公式:
[
\begin
v_t &= \gamma v_ + \eta \nabla_\theta J(\theta_t - \gamma v_) \
\theta_{t+1} &= \theta_t - v_t
\end
]
作用:这种“向前看”的梯度计算,使得算法在接近最低点时能提前感知到坡度的变化,从而及时减速,避免在谷底过度震荡,提高了稳定性。
3. Adagrad 自适应学习率算法 📉
Adagrad 旨在解决稀疏特征和自动学习率衰减的问题。它为每个参数维护一个累积平方梯度,并据此为每个参数自适应地调整学习率。
更新公式:
[
\begin
G_{t, ii} &= G_{t-1, ii} + (\nabla_{\theta_i} J(\theta_t))^2 \
\theta_{t+1, i} &= \theta_{t, i} - \frac{\eta}{\sqrt{G_{t, ii} + \epsilon}} \cdot \nabla_{\theta_i} J(\theta_t)
\end
]
其中,( G_t ) 是一个对角矩阵,其对角线元素 ( G_{t, ii} ) 是参数 ( \theta_i ) 历史梯度平方的累积和。( \epsilon ) 是一个小常数,防止除零。
作用:
- 参数特异性:对于更新频繁的参数(梯度平方和大),分母大,有效学习率小;对于更新稀疏的参数(梯度平方和小),分母小,有效学习率大。这非常适用于稀疏数据。
- 自动衰减:随着训练进行,( G_t ) 单调递增,导致所有参数的学习率都在自动衰减。
缺点:由于 ( G_t ) 持续累积,学习率会变得过小,可能导致训练提前终止。
4. Adadelta / RMSprop 算法 🔄
为了改进 Adagrad 学习率急剧下降的问题,Adadelta 和 RMSprop 不再累积全部历史平方梯度,而是使用指数移动平均来关注最近一段时间的梯度规模。
RMSprop 更新公式:
[
\begin
E[g2]_t &= \beta E[g2] + (1-\beta)(\nabla\theta J(\theta_t))2 \
\theta_{t+1} &= \theta_t - \frac{\eta}{\sqrt{E[g2]t + \epsilon}} \cdot \nabla\theta J(\theta_t)
\end
]
其中,( \beta ) 是衰减率(通常为0.9),( E[g^2]_t ) 是平方梯度的指数移动平均。
作用:解决了 Adagrad 学习率单调下降至零的问题,使得学习率能够适应非平稳目标,在训练后期也能有更新。
5. Adam 自适应矩估计算法 ⚡
Adam 可以说是目前最流行、默认推荐的优化器。它结合了动量法和自适应学习率的优点。它同时计算梯度的一阶矩(均值,提供动量)和二阶矩(未中心化的方差,提供自适应学习率),并进行偏差校正。
更新公式:
[
\begin
m_t &= \beta_1 m_ + (1-\beta_1) \nabla_\theta J(\theta_t) \quad &\text{(一阶矩估计)} \
v_t &= \beta_2 v_ + (1-\beta_2) (\nabla_\theta J(\theta_t))2 \quad &\text{(二阶矩估计)} \
\hat_t &= \frac{1 - \beta_1t} \quad &\text{(偏差校正)} \
\hatt &= \frac{1 - \beta_2^t} \quad &\text{(偏差校正)} \
\theta{t+1} &= \theta_t - \frac{\eta}{\sqrt{\hat_t} + \epsilon} \hat_t
\end
]
其中,( \beta_1, \beta_2 ) 通常分别取0.9和0.999。
优点:
- 兼具动量的加速效果和自适应学习率对稀疏梯度、不同曲率的适应性。
- 通常收敛速度快,且对超参数(除了学习率)相对不敏感。
如何选择优化器?
- 对于稀疏数据,可使用自适应学习率算法(Adagrad, Adadelta, RMSprop, Adam)。
- 如果数据分布相对均匀,动量法或NAG通常表现良好。
- Adam 因其鲁棒性和优秀的综合性能,常被作为默认选择。当不确定用哪种时,可以先尝试Adam。
第五节:其他优化策略 🧩
除了修改参数更新规则,还有一些外围策略可以提升SGD的训练效果。
以下是几种常见的策略:
- Shuffling(数据打乱):在每个训练周期开始时,随机打乱训练数据顺序,可以防止模型因数据顺序而产生偏差,提升泛化能力。
- Batch Normalization(批标准化):对每一层神经网络的输入进行标准化处理(减去均值,除以标准差),可以稳定网络的训练过程,允许使用更高的学习率,并具有一定的正则化效果。
- Early Stopping(早停):在验证集误差不再下降甚至开始上升时,提前终止训练。这是防止过拟合最简单有效的方法之一。
- Gradient Noise(梯度噪声):在梯度中加入少量高斯噪声。公式为:( g_t = \nabla_\theta J(\theta_t) + \mathcal(0, \sigma_t2) )。噪声可以帮助模型逃离局部极小值或鞍点,其中噪声方差 ( \sigma_t2 ) 通常随时间衰减。
总结 📝
本节课中我们一起学习了随机梯度下降算法的核心脉络:
- 我们从最基础的梯度下降法出发,认识了其计算开销大和学习率难调的问题。
- 引入了随机梯度下降(SGD) 及其主流变种小批量SGD,以牺牲一定稳定性为代价换取了计算效率的巨大提升。
- 深入分析了SGD面临的四大挑战:病态曲率震荡、学习率调优、稀疏参数更新和鞍点问题。
- 系统学习了一系列改进算法:Momentum/NAG 通过引入“惯性”缓解震荡;Adagrad/Adadelta/RMSprop 通过自适应学习率处理稀疏性和自动衰减;Adam 综合了动量与自适应学习的优势,成为当前最流行的选择。
- 最后,了解了一些辅助优化策略,如数据打乱、批标准化、早停和添加梯度噪声。



理解这些算法的原理和适用场景,能帮助我们在实际项目中不再是“黑箱”调用优化器,而是能够根据具体问题(如数据稀疏性、模型曲面特性)做出更明智、更有效的选择。
论文公开课(七月在线出品) - P2:深度学习中的归一化
概述
在本节课中,我们将一起探讨深度学习模型中的归一化技术。我们将以“批量归一化”这一经典方法作为基准,并引入最新的“自归一化神经网络”思想,通过对比这两篇文章,系统地梳理归一化在深度学习中的作用、原理与实现方式。课程旨在让初学者能够理解为什么需要归一化,以及它是如何解决训练过程中的关键问题的。
第一部分:深度模型与激活函数回顾
上一节我们介绍了课程的整体脉络,本节中我们首先回顾一下深度学习模型的基本结构。
一个深度模型,例如多层感知机(MLP),其核心目的是通过组合多个简单的函数来逼近一个复杂的未知函数。机器学习本质上就是在学习一个从输入到输出的映射函数。
模型的基本形式
对于一个简单的两层网络,其计算过程可以表示为:
公式:
y = g( W2 * h + b2 )
h = g( W1 * x + b1 )
其中,x 是输入,h 是隐藏层的输出,y 是最终输出。W1, b1, W2, b2 是需要学习的参数,而 g(·) 是一个非线性函数,我们称之为激活函数。
激活函数的作用
激活函数的引入至关重要。如果每一层的变换都是线性的,那么无论叠加多少层,整个网络仍然等价于一个线性变换,无法拟合复杂的非线性关系。因此,激活函数为模型引入了非线性,使其能够学习更复杂的模式。
然而,激活函数的选择也会带来一些挑战。
第二部分:梯度消失问题
上一节我们了解了激活函数的基本作用,本节中我们来看看它可能引发的一个关键问题:梯度消失。
在训练深度神经网络时,我们通常使用基于梯度的优化方法(如随机梯度下降)。梯度通过链式法则从输出层反向传播到输入层。
公式(链式法则简化示意):
∂Loss/∂W1 = (∂Loss/∂y) * (∂y/∂h) * (∂h/∂W1)
如果链式中的任何一项(特别是激活函数的导数)非常小,那么连乘之后,传递到较浅层的梯度就会变得极其微小,接近于零。这就是梯度消失现象。此时,网络浅层的参数几乎得不到更新,导致训练停滞。
常用激活函数及其导数
以下是导致梯度消失问题的具体分析:
Sigmoid 函数:
σ(x) = 1 / (1 + e^{-x})- 其导数
σ'(x) = σ(x)(1 - σ(x))。 - 当输入
x的绝对值较大时,函数值趋于0或1,导数则趋近于0。这意味着只要输入落在两端的“饱和区”,该神经元的梯度就会消失。
- 其导数
Tanh 函数:
tanh(x)- 与 Sigmoid 类似,在两端也存在饱和区,导数会趋近于0。
ReLU 函数:
ReLU(x) = max(0, x)- 在
x > 0时,导数为1,缓解了梯度消失。 - 但在
x < 0时,导数为0。如果大量神经元输出为负,会导致“神经元死亡”,同样阻碍梯度传播。
- 在
梯度消失使得深度网络难以训练。为了解决这个问题,研究者们一方面设计了新的激活函数(如 Leaky ReLU, ELU),另一方面则从数据分布的角度提出了更根本的解决方案——归一化。
第三部分:批量归一化(Batch Normalization)
上一节我们分析了梯度消失的根源,本节中我们介绍一种广泛应用的解决方案:批量归一化。
批量归一化的核心思想是:在每一层的输入传递到激活函数之前,先对其进行标准化处理,使其保持稳定的分布(例如均值为0,方差为1)。这样可以减少内部协变量偏移,让每一层的输入都落在激活函数梯度较大的区域,从而加速训练并缓解梯度消失。
批量归一化的操作步骤
对于一个 mini-batch 的输入 B = {x_1, ..., x_m},批量归一化进行如下变换:
公式:
- 计算 mini-batch 的均值:
μ_B = (1/m) Σ_{i=1}^{m} x_i - 计算 mini-batch 的方差:
σ_B² = (1/m) Σ_{i=1}^{m} (x_i - μ_B)² - 归一化:
x̂_i = (x_i - μ_B) / √(σ_B² + ε),ε是一个很小的常数,用于数值稳定性。 - 缩放与偏移:
y_i = γ * x̂_i + β
其中,γ 和 β 是可学习的参数。步骤4至关重要,它恢复了网络的表达能力。如果归一化后数据应该被平移或缩放,网络可以通过学习 γ 和 β 来适应,而不是被强制固定在标准分布上。
批量归一化的优点
- 允许使用更高的学习率:稳定的输入分布降低了训练对参数初始化和学习率的敏感性。
- 减少对Dropout的依赖:BN本身具有一定的正则化效果。
- 加速模型收敛:缓解了梯度消失/爆炸问题。
批量归一化已成为训练深度神经网络的标配组件。然而,它需要计算 mini-batch 的统计量,这在 batch size 较小或动态网络结构中可能受限。
第四部分:自归一化神经网络(Self-Normalizing Neural Networks, SNNs)
上一节我们介绍了需要外部干预的批量归一化,本节中我们来看一种更“智能”的方法:让网络自己实现归一化。
自归一化神经网络的核心创新在于设计了一个特殊的激活函数——缩放指数线性单元(SELU),并证明了在使用该函数且满足一定条件(如权重初始化、网络宽度)时,网络具有“自归一化”的属性。
SELU 激活函数
公式:
SELU(x) = λ * { x (if x > 0), α * (e^x - 1) (if x ≤ 0) }
其中 λ ≈ 1.0507,α ≈ 1.6733 是精心推导出的固定值。与 ELU 相比,SELU 在正区间的斜率 λ > 1。
自归一化属性的直观理解
自归一化属性是指:当网络使用 SELU 激活函数并正确初始化时,每一层输出的均值会向0收敛,方差会向1收敛,即使没有显式的批量归一化层。
其数学证明非常复杂,但思想可以概括为:
- 利用中心极限定理:当网络较宽时,某一神经元的输入是许多随机变量的加权和,其分布接近高斯分布。
- 构造映射不动点:SELU 函数被设计为,当输入是均值为0、方差为1的高斯分布时,输出也保持相同的均值和方差(这是一个“不动点”)。
- 证明收敛性:进一步证明,该映射是一个“压缩映射”。这意味着,即使初始输入的分布偏离了标准高斯,经过多层这样的变换后,其分布也会被拉回那个不动点附近。
SNNs 的意义与局限
- 意义:提供了一种无需额外归一化层就能稳定训练深度网络的方法,简化了网络结构,在 batch size 为1时也能工作。
- 局限:理论保证依赖于网络宽度、特定的权重初始化(如 LeCun normal)等条件。对于卷积网络等结构较窄的网络,其效果可能不如批量归一化稳定。
总结
本节课中我们一起学习了深度学习中的归一化技术。
我们首先回顾了深度模型的基本结构和激活函数的作用,并指出了传统激活函数可能导致的梯度消失问题。接着,我们深入分析了批量归一化(BN) 这一经典方法,它通过显式地标准化每一层的输入来稳定分布,从而加速训练并缓解梯度问题。最后,我们探讨了最新的自归一化神经网络(SNN) 思想,它通过精心设计的 SELU 激活函数,让网络自身具备了稳定内部数据分布的能力。
这两种思路代表了解决深度网络训练难题的不同路径:BN 是强大的工程化解决方案,而 SNN 则展示了通过理论推导改进基础组件的潜力。理解它们有助于我们更深入地把握深度学习模型训练的动力学,并在实践中根据任务需求选择合适的工具。


注:本教程根据七月在线论文公开课第二讲内容整理,主要参考了《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》和《Self-Normalizing Neural Networks》两篇论文的思想。

论文公开课(七月在线出品) - P3:AlphaGo Zero背后的算法

📚 课程概述
在本节课中,我们将学习AlphaGo Zero背后的核心算法。我们将从强化学习的基本框架入手,分析早期AlphaGo如何利用人类知识,并重点探讨AlphaGo Zero如何摒弃人类棋谱,通过自我对弈实现超越。课程将结合两篇关键论文,深入浅出地解释深度模型与蒙特卡洛树搜索的结合,以及这种“专家迭代”训练范式的原理。
第一部分:动态规划与强化学习
上一节我们介绍了课程的整体目标,本节中我们来看看解决此类问题的通用框架:动态规划与强化学习。
强化学习和动态规划的目标是一致的:设计一种算法,使其能基于环境状态采取行动,以最大化长期收益。这里的“环境”在围棋中就是棋盘上的棋子布局,“行动”就是落子位置,“收益”就是最终赢得比赛。
两者的核心区别在于对“环境变化规律”的认知:
- 动态规划:要求环境的转移规律是确定且已知的。
- 强化学习:不要求预先知道环境的转移规律,适用范围更广,但也更困难。
我们可以通过动态规划这个简化模型,来帮助理解强化学习以及AlphaGo的核心思想。
🤖 强化学习的基本模型
强化学习的基本思路可以概括为“智能体(Agent)与环境(Environment)的交互学习”。以下是其数学描述(马尔可夫决策过程):
- 状态集合 (S):所有可能的环境状态。例如围棋中所有可能的棋盘局面。
- 行动集合 (A):在给定状态下,智能体可以采取的所有行动。例如在某个棋盘局面下所有合法的落子点。
- 奖励函数 (R):在状态
s下采取行动a后获得的即时奖励或惩罚。 - 转移函数 (T):在状态
s下采取行动a后,转移到新状态s'的概率分布。这是动态规划与强化学习的关键区别点。 - 策略 (π):最终的学习目标,一个从状态到行动选择的映射函数。即给定任何局面,都知道最优的落子位置。
🎲 举例说明:21点游戏
为了具体化这些概念,我们以21点游戏为例。
- 状态空间:玩家手牌点数(P)和庄家明牌点数(D)的组合。
- 行动空间:玩家可以选择“要牌”或“停牌”。
- 奖励函数:游戏结束时,获胜得+1,落败得-1。注意:并非每一步都有奖励,只有终局才有。因此,另一种设计思路是将“获胜概率的增加值”作为每一步的奖励。
- 转移函数:玩家行动后,庄家会按照固定规则(点数小于17必须抽牌,否则停牌)行动,其抽牌的概率分布是已知的。
对于21点,由于其状态和行动空间有限,且转移规则固定,我们可以用动态规划计算出最优策略表。这个表告诉我们,在任何(P, D)的组合下,玩家选择“要牌”还是“停牌”的期望收益最高。
然而,围棋的挑战在于:
- 状态空间巨大:可能局面数量远超宇宙原子数,无法列出完整的价值表。
- 转移函数未知:你落子后,对手会如何应对是未知的,且对手的策略并非固定公开的规则。
因此,围棋属于典型的强化学习问题,且是其中非常困难的一类。
第二部分:AlphaGo的解决方案与核心困难
面对围棋的巨大挑战,AlphaGo的解决方案核心是两件事:
- 使用深度模型来逼近复杂的价值函数和策略函数,以解决状态空间过多的问题。
- 预测对手的落子概率分布,以应对转移函数未知的问题。
对于第二点“预测落子分布”,AlphaGo(初代)和AlphaGo Zero采取了截然不同的路径。
👥 AlphaGo(初代):模仿学习与混合策略
AlphaGo初代的核心思想是模仿学习,即从人类高手棋谱中学习。它的训练和决策融合了三种策略:
以下是三种策略的来源与结合方式:
监督学习策略网络 (SL Policy Network):
- 目标:直接模仿人类高手在特定局面下的落子。
- 训练数据:数百万人类棋谱。
- 效果:对人类高手走法的预测准确率达到57%。仅凭此无法战胜顶尖棋手。
强化学习策略网络 (RL Policy Network):
- 目标:优化胜率,而非模仿。
- 训练方式:让上面的SL策略网络自我对弈,根据对弈结果调整网络参数,使其朝着赢棋的方向进化。
- 效果:性能与SL网络相当或略差,但目标更直接(赢棋)。
快速走子网络 (Fast Rollout Policy):
- 目标:速度极快、精度较低的走子预测,用于蒙特卡洛模拟。
- 特点:比策略网络快数千倍,但准确率仅24%。
- 用途:在树搜索中进行快速胜负推演。
价值网络 (Value Network):
- 目标:直接评估当前局面的胜率。
- 训练数据:使用强化学习策略网络自我对弈产生的3000万局棋,每局只随机采样一个局面,以避免过拟合。
- 用途:在搜索树中替代随机模拟到底,提供更稳定的局面评估。
最终决策:AlphaGo将以上策略通过蒙特卡洛树搜索 (MCTS) 框架结合起来。在树搜索中,它综合使用策略网络(提供优先落子点)、快速走子(进行快速模拟)和价值网络(评估叶节点),共同决定最终落子。实验表明,任何单一策略都无法达到职业水平,而三者结合则能超越人类顶尖棋手。
第三部分:AlphaGo Zero——无需人类知识的突破
上一节我们看到了AlphaGo如何依赖人类知识,本节中我们来看看革命性的AlphaGo Zero如何摆脱这一依赖。
AlphaGo Zero的核心是**“专家迭代”** 框架,它仅用一个神经网络,并通过自我对弈和树搜索进行训练。
🧠 核心架构:统一的神经网络
AlphaGo Zero使用一个深度残差神经网络 f_θ。该网络接收棋盘状态 s 作为输入,同时输出两个结果:
- 落子概率向量
p:代表在状态s下,选择每一个合法落子位置a的概率。p = Pr(a|s) - 局面价值标量
v:代表从状态s出发,当前玩家的预期胜率。v ≈ E[最终胜者 | s]
这个网络统一了AlphaGo初代中的策略网络和价值网络。
🔄 训练过程:自我对弈与专家迭代
训练是一个不断循环的过程,每一步都包含三个关键阶段:
自我对弈(生成棋谱):
- 使用当前神经网络
f_θ,基于蒙特卡洛树搜索 (MCTS) 的引导进行自我对弈。 - 关键点:落子不是直接采用神经网络输出的概率
p,而是采用MCTS搜索后计算出的改进后的概率分布π。π比原始的p更强,因为它包含了通过搜索发现的更优变化。 - 对弈直至终局,产生胜负结果
z(赢为+1,输为-1)。
- 使用当前神经网络
神经网络训练(学习棋谱):
- 利用自我对弈产生的数据
(s, π, z)来更新神经网络参数θ。 - 训练目标是最小化以下损失函数
l:
l = (z - v)^2 - π^T log p + c ||θ||^2 - 损失函数的三部分:
(z - v)^2:价值损失。让神经网络预测的胜率v接近实际对弈结果z。- π^T log p:策略损失。让神经网络输出的落子概率p接近MCTS搜索出的更强分布π(交叉熵)。c ||θ||^2:L2正则化项。防止过拟合。
- 利用自我对弈产生的数据
迭代优化:
- 用新训练好的神经网络
f_θ替换旧的,回到第1步开始新一轮自我对弈。 - 随着迭代进行,神经网络和MCTS相互促进:更好的网络指导更高效的搜索,更高效的搜索产生更高质量的训练数据来改进网络。
- 用新训练好的神经网络
🌳 蒙特卡洛树搜索 (MCTS) 在AlphaGo Zero中的角色
MCTS是“专家迭代”中产生更强策略 π 的关键。其过程可以概括为选择、扩展、评估、回溯四个步骤,在搜索中为每条边 (s, a) 维护:
- 访问次数
N(s, a):该边被访问的次数。 - 行动价值
Q(s, a):该边带来的平均胜率估计。 - 先验概率
P(s, a):由神经网络f_θ给出的原始落子概率。
搜索时的选择公式(UCT算法的变体):
在节点 s 选择行动 a 时,最大化以下置信上限:
U(s, a) = Q(s, a) + c_{puct} * P(s, a) * sqrt(∑_b N(s, b)) / (1 + N(s, a))
- 第一项
Q(s, a):利用。倾向于选择历史胜率高的行动。 - 第二项:探索。倾向于选择先验概率
P高但访问次数N少的行动。c_{puct}是控制探索程度的常数。
搜索结束后,根节点各行动的访问次数分布 N(s, a) 被归一化,即作为改进后的策略 π 用于训练神经网络。
📊 总结与对比
本节课中我们一起学习了从AlphaGo到AlphaGo Zero的算法演进:
| 特性 | AlphaGo (初代) | AlphaGo Zero |
|---|---|---|
| 人类知识 | 依赖。使用人类棋谱训练监督学习策略网络。 | 无需。完全从随机初始化开始自我学习。 |
| 网络结构 | 分离。独立的策略网络(SL/RL)和价值网络。 | 统一。一个神经网络同时输出策略 p 和价值 v。 |
| 快速走子 | 需要。使用快速但低精度的策略进行蒙特卡洛模拟。 | 无需。价值网络 v 直接评估叶节点,更高效准确。 |
| 训练数据 | 人类棋谱 + 自我对弈数据。 | 纯自我对弈数据。 |
| 核心方法 | 模仿学习 + 蒙特卡洛树搜索 + 价值网络混合。 | 专家迭代:神经网络与蒙特卡洛树搜索紧密耦合,相互增强。 |
| 性能与效率 | 需要大量计算资源和时间训练,已超越人类。 | 更高效、更强大。使用更少的计算资源,在更短时间内达到更高水平。 |
AlphaGo Zero的成功表明:
- 对于围棋这类问题,无需人类先验知识,纯自我对弈的强化学习可以找到甚至超越人类认知的策略。
- 深度神经网络是逼近复杂策略和价值函数的强大工具。
- 蒙特卡洛树搜索不仅可用于决策,还能与神经网络结合,生成高质量的训练目标,形成强大的“规划-学习”循环。
这一框架为许多其他具有明确规则但策略空间复杂的领域(如其他棋类、电子游戏、算法设计等)提供了新的解决思路。







浙公网安备 33010602011771号