选择题考点
范数
L2 范数(欧几里得范数)计算公式:
即把每个元素平方后求和,再开平方。
范数(norm) 是衡量一个向量”大小”或”长度”的函数。常见的有:
L0 范数:非零元素的个数
严格说它不是真正的范数,但常用于表示稀疏性。
L1 范数:绝对值之和
也叫曼哈顿距离,常用于 Lasso 正则化,能让参数变稀疏。
L2 范数:平方和开根号
就是欧几里得距离,常用于 Ridge 正则化、权重衰减。
L∞ 范数:最大绝对值
所以 L2 范数只是 Lp 范数中 的特例。
在机器学习中,L1 正则化容易产生稀疏解,L2 正则化让参数整体变小、更平滑,防止过拟合。
正则化
正则化是什么?
正则化就是在模型原来的损失函数上,再加一个”惩罚项”,限制模型参数不要太大、太复杂。
一般形式:
其中 控制正则化强度:
- 越大,惩罚越强,模型越简单;
- 越小,惩罚越弱,模型越接近不加正则化;
- 就是不正则化。
L1 正则化是什么?
L1 正则化也叫 Lasso,惩罚项是权重绝对值之和:
所以目标函数类似:
作用:
- 让很多权重变成 0;
- 可以做 特征选择;
- 适合高维数据,比如特征很多但真正有用的很少。
为什么能让权重变 0?
因为 L1 的约束区域是”菱形”,有尖角,最优解很容易落在坐标轴上。
一旦某个权重落在坐标轴上,它就变成 0。
直观理解
L1 正则化像是告诉模型:
不要用太多特征,能不用就不用。
所以它产生的是 稀疏解。
L2 正则化是什么?
L2 正则化也叫 Ridge 回归,在神经网络里常叫
权重衰减。
它的惩罚项是权重平方和:
目标函数类似:
有时会写成 ,只是为了求导方便。
作用:
- 让所有权重整体变小;
- 不让某个特征权重特别大;
- 降低模型方差,提高泛化能力;
- 缓解过拟合;
- 对病态矩阵问题也有改善。
为什么有效?
L2 正则化相当于给权重加了一个”约束球”,让解不要跑得太远。
它不会像 L1 那样把权重压成 0,而是让权重变得平滑、均匀地小。
直观理解
L2 正则化像是告诉模型:
你可以用很多特征,但每个特征的权重别太夸张。
所以它产生的是 非稀疏的小权重。
L1 和 L2 对比
| 对比项 | L1 正则化 | L2 正则化 |
|---|---|---|
| 别名 | Lasso | Ridge、权重衰减 |
| 惩罚项 | ||
| 权重特点 | 很多变成 0 | 整体变小,但很少为 0 |
| 是否稀疏 | 是 | 否 |
| 特征选择 | 可以 | 不可以 |
| 可导性 | 在 0 处不可导 | 处处可导 |
| 优化 | 常用坐标下降、近端梯度 | 梯度下降、闭式解 |
| 适用场景 | 高维、稀疏特征 | 一般防过拟合、共线性 |
为什么要做正则化?
核心原因:防止过拟合。
模型如果太复杂,参数太大,就会:
- 记住训练数据里的噪声;
- 在新数据上表现很差;
- 对输入的小变化非常敏感。
正则化通过限制参数大小或数量,让模型更简单。
这符合奥卡姆剃刀原则:
能简单解决问题,就不要用复杂模型。
另外,从贝叶斯角度看:
- L2 正则化对应权重服从 高斯先验;
- L1 正则化对应权重服从 拉普拉斯先验。
L2 范数与 L2 正则化的区别
L2 范数:是长度,确实要开根号
L2 正则化:通常用的不是 L2 范数本身,而是 L2 范数的平方
有时写成 ,只是为了求导方便。
tanh 激活函数
定义
tanh 激活函数:
也可以写成:
导数
等价形式:
它的导数范围是:
在 时最大:
当 远离 0 时,导数迅速接近 0。
输入正无穷或负无穷会梯度爆炸吗?
不会梯度爆炸,反而会梯度消失。
因为:
所以:
也就是说:
- 输入正无穷:输出趋近 1,导数趋近 0;
- 输入负无穷:输出趋近 -1,导数趋近 0;
- 在饱和区,梯度几乎为 0。
因此 tanh 本身不会导致梯度爆炸,它的导数最大只有 1,反而容易造成梯度消失。
梯度爆炸通常来自权重矩阵过大、连乘层数太多等原因,而不是 tanh 直接造成的。
值域
也就是值域是 。
它取不到 -1 和 1,只能无限接近。
相比 sigmoid 的值域 ,tanh 是零中心的,输出有正有负,通常更有利于神经网络优化。
会用在 LSTM 中吗?
会,而且 LSTM 中非常常用。
LSTM 里通常有两类激活函数:
- sigmoid:用于门控,比如输入门、遗忘门、输出门,因为门需要输出 0 到 1 之间的值;
- tanh:用于生成候选记忆和输出隐藏状态。
相关选择题:ReLU 换成 tanh
把 ReLU 替换成 tanh,可能会导致什么?
A. 神经元死亡
B. 梯度消失
C. 计算复杂
D. 过拟合
正确答案:B. 梯度消失
| 选项 | 分析 |
|---|---|
| A. 神经元死亡 | 这是 ReLU 的典型问题:负区间梯度为 0,神经元可能永久不更新。tanh 不会”死亡”。 |
对比 ReLU 和 tanh
| 激活函数 | 优点 | 缺点 |
|---|---|---|
| ReLU | 正区间导数恒为 1,缓解梯度消失;计算简单 | 负区间梯度为 0,可能神经元死亡 |
| tanh | 零中心,输出有正有负 | 饱和区导数趋近 0,容易梯度消失;计算稍复杂 |
ReLU 激活函数
ReLU 是什么?
ReLU 的公式非常简单:
也就是说:
- 如果 ,输出就是 ,导数(梯度)是 1;
- 如果 ,输出就是 0,导数(梯度)是 0。
图像上,正半轴是直线 ,负半轴是 。
“负区间梯度为 0”是什么意思?
在神经网络中,一个神经元通常先计算线性部分:
然后经过激活函数:
反向传播时,我们需要计算损失对权重 的梯度。根据链式法则:
其中:
而 ReLU 的导数是:
所以,当 时,梯度为 0。
这意味着:
如果某个神经元的线性输出 总是小于等于 0,那么在反向传播时,这个神经元对权重的梯度永远是 0。
梯度为 0,权重就不会更新:
为什么可能”永久不更新”?
假设某个神经元一开始的权重和偏置不好,导致对于训练集中所有样本,它的线性输出 都是负数。
那么:
- 前向传播:输出永远是 0;
- 反向传播:梯度永远是 0;
- 权重更新:永远不变。
既然权重不变,那么 仍然对所有样本都是负数,输出还是 0,梯度还是 0……
这就形成了一个死循环:
这个神经元就相当于”死掉了”,不再参与学习。这就是”神经元死亡”。
为什么会发生这种情况?
常见原因:
- 学习率太大
一次更新把权重推得太远,导致很多神经元的输入变成负数。 - 初始化不好
权重初始化使得某些神经元一开始就进入负区间。 - 数据分布问题
某些特征总是导致该神经元的加权和为负。
一旦死亡,这个神经元对任何输入都输出 0,相当于从网络中”消失”了。
有什么影响?
- 网络的有效容量变小,因为死掉的神经元不再起作用。
- 模型表达能力下降,可能欠拟合。
- 如果大量神经元死亡,训练会变差。
怎么缓解?
常用的改进版 ReLU:
Leaky ReLU:负区间给一个很小的斜率,比如 0.01
这样负区间梯度不为 0,神经元不容易死。
Parametric ReLU (PReLU):负区间的斜率作为可学习参数。
ELU:负区间平滑,有非零梯度。
小心设置学习率:不要太大。
合适的初始化:如 He 初始化。
总结
“ReLU 的典型问题:负区间梯度为 0,神经元可能永久不更新”意思是:
当 ReLU 神经元的输入为负时,输出为 0,导数也为 0。反向传播时梯度为 0,权重无法更新。如果这个神经元对所有输入都处于负区间,它就会一直输出 0,永远学不到东西,相当于”死亡”。
所以 ReLU 虽然计算简单、能缓解梯度消失,但存在 Dead ReLU 的风险。
学习率与 batch size
学习率 / batch size 越大,泛化性不一定越差。
它和泛化性不是简单的单调关系,而是存在一个”合适范围”。太小或太大都可能不好。
为什么关注 η/B?
SGD 的更新是:
其中:
- :学习率
- :batch size
梯度估计的噪声大小,通常和下面这个量有关:
可以把它理解为 每单位数据的学习步长,也叫 噪声尺度。
- 越大:更新噪声越大,参数跳得越厉害;
- 越小:更新越平滑,越接近全批量梯度下降。
| η/B | 效果 |
|---|---|
| 太小 | 噪声小,易收敛到尖锐极小值,泛化可能差 |
| 适中 | 噪声合适,易找到平坦极小值,泛化较好 |
| 太大 | 训练不稳定,可能发散,泛化差 |
学习率 / batch size 越大,泛化性不一定会越差。
在一定范围内,适当增大反而可能提升泛化;但过大时会导致训练不稳定,泛化变差。
关键是要让
处于合适范围,而不是一味追求大或小。
神经元
在神经网络里,神经元通常指人工神经元,也叫节点、单元。它是网络的基本计算单元。
一句话概括:
单个神经元:多个输入,一个输出;一层神经元:多个输入,多个输出。
归纳偏置
归纳偏置(inductive bias,也叫归纳偏好)是机器学习中的一个核心概念。简单说:
归纳偏置是模型在学习过程中,对”什么样的规律更可能成立”所做出的先验假设或偏好。
它不是从训练数据里学来的,而是模型结构、算法或正则化项本身自带的。
为什么需要归纳偏置?
机器学习的目标是:从有限训练数据中,学到一个能泛化到新数据的规律。
但问题是:
- 给定有限个训练样本,能拟合它们的函数有无数个;
- 这些函数在训练集上表现一样,但在新数据上可能完全不同;
- 模型必须选一个。
靠什么选?就靠归纳偏置。
举个例子:
- 给你几个点,你可以用直线拟合,也可以用复杂曲线穿过每个点;
- 线性回归的归纳偏置是”数据关系大致是线性的”;
- 决策树的归纳偏置是”决策边界是轴对齐的矩形划分”;
- CNN 的归纳偏置是”图像特征具有局部性、平移等变性”。
没有归纳偏置,学习算法就无法从有限数据推广到未知数据。
常见模型的归纳偏置
| 模型 | 归纳偏置 |
|---|---|
| 线性回归 | 输出与输入近似线性关系 |
| 逻辑回归 | 线性决策边界,概率由 sigmoid 给出 |
| 决策树 | 轴对齐划分,局部常量预测 |
| KNN | 相近样本有相近标签,局部平滑 |
| SVM | 最大间隔,偏好简单边界 |
| CNN | 局部连接、权重共享、平移等变性 |
| RNN | 顺序依赖、时间权重共享、平稳性 |
| Transformer | 全局交互、参数共享、弱局部性、无位置编码时置换等变 |
| L2 正则化 | 偏好较小的权重 |
| L1 正则化 | 偏好稀疏权重 |
所以,几乎任何模型都有归纳偏置,区别只是强弱和类型不同。
强归纳偏置 vs 弱归纳偏置
强归纳偏置
模型自带很强的假设,比如 CNN:
- 假设局部性;
- 假设平移不变/等变;
- 参数共享。
优点:
- 数据少时也能学好;
- 训练快;
- 对图像任务很合适。
缺点:
- 如果任务不符合这些假设,性能会受限;
- 不够灵活。
弱归纳偏置
模型假设少,比如标准 Transformer:
- 全局注意力,不假设局部性;
- 不假设顺序,需要位置编码;
- 参数共享,但注意力模式由数据决定。
优点:
- 灵活,表达能力强;
- 数据量大时能学到复杂规律。
缺点:
- 需要更多数据;
- 训练成本高;
- 容易过拟合,需要正则化。
所以之前说”Transformer 归纳偏置弱”,意思是它不像 CNN 那样内置了强烈的局部性和平移不变性,而是更依赖数据和位置编码来学习结构。
一句话:
归纳偏置就是模型自带的”世界观”,决定了它更容易学到什么样的规律。
全 0 初始化
在做图像分类时,如果指的是多层神经网络或卷积神经网络(CNN),把参数全部初始化为 0,通常不能正常训练,或者说训练会失败。
核心原因是:对称性无法被打破。
为什么全 0 初始化不行?
假设一个隐藏层有两个神经元,它们的权重和偏置都初始化为 0:
对于同一个输入 ,两个神经元的输出完全相同:
经过激活函数后:
反向传播时,它们得到的梯度也完全相同:
于是更新后:
仍然完全一样。
这样,这一层的多个神经元永远保持相同,等价于只有一个神经元。网络失去了表达多个不同特征的能力,无法学习复杂的图像特征。
这就是所谓的对称性问题。
C++:struct vs class
在 C++ 中,struct 和 class
几乎完全一样,唯一本质区别是:
默认访问权限和默认继承权限不同。
核心区别:
| 对比项 | struct |
class |
|---|---|---|
| 默认成员访问权限 | public |
private |
| 默认继承权限 | public |
private |
C++:浅拷贝 vs 深拷贝
| 对比项 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制内容 | 只复制成员值 | 复制指针指向的数据 |
| 指针成员 | 两个对象共享同一内存 | 各自独立内存 |
| 风险 | 重复释放、悬空指针 | 安全,但开销大 |
| 默认行为 | 默认拷贝构造是浅拷贝 | 需要自己实现 |
C++:抽象类
含有纯虚函数的类叫抽象类。纯虚函数写法:
virtual void func() = 0;抽象类不能实例化对象,只能作为基类被继承。
class Animal {
public:
virtual void speak() = 0; // 纯虚函数
};
// Animal a; // 错误,不能创建抽象类对象指向抽象类的指针
虽然不能创建抽象类对象,但可以定义指向抽象类的指针或引用,用来实现多态。
class Dog : public Animal {
public:
void speak() override { cout << "汪汪"; }
};
Animal* p = new Dog(); // 基类指针指向派生类对象
p->speak(); // 调用 Dog::speak所以”抽象指针”如果指这个,就是:指向抽象基类的指针,用于多态调用。
C++:虚函数
虚函数是用 virtual
修饰的成员函数,允许在派生类中重写,并通过基类指针或引用调用时,动态绑定到实际对象类型的函数。
class Base {
public:
virtual void show() { cout << "Base"; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived"; }
};
Base* p = new Derived();
p->show(); // 输出 Derived,而不是 Base关键点:
- 实现运行时多态。
- 基类指针指向派生类对象时,调用的是派生类版本。
- 有虚函数表的开销。
- 析构函数常声明为虚函数,防止派生类资源泄漏。
C++:派生类
派生类是通过继承从已有类(基类)创建的新类。
class Base {
public:
int a;
};
class Derived : public Base { // Derived 是派生类
public:
int b;
};派生类:
- 继承基类的成员(公有继承时,public 成员可访问)。
- 可以添加自己的成员。
- 可以重写基类的虚函数。
- 可以有多个基类(多重继承)。
派生类对象包含基类部分和自身部分。
C++:模板
模板是 C++ 的泛型编程工具,让函数或类可以适用于多种数据类型,而不必为每种类型重写代码。
函数模板
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int x = max(3, 5);
double y = max(2.5, 1.8);类模板
template <typename T>
class Box {
T value;
public:
Box(T v) : value(v) {}
T get() { return value; }
};
Box<int> b1(10);
Box<string> b2("hello");模板在编译时实例化,生成对应类型的代码。
C++:智能指针
C++11 中三种主要的智能指针是:
std::unique_ptrstd::shared_ptrstd::weak_ptr
它们都在头文件:
#include <memory>你写的 shared、weak、unique
基本对应这三个。
std::unique_ptr:独占所有权
同一时间只能有一个
unique_ptr拥有某个对象。不能拷贝,只能移动:
std::unique_ptr<int> p1 = std::make_unique<int>(10); // std::unique_ptr<int> p2 = p1; // 错误,不能拷贝 std::unique_ptr<int> p2 = std::move(p1); // 可以移动离开作用域时自动释放对象。
开销小,几乎和裸指针一样。
适合”一个对象只归一个所有者”的场景。
std::shared_ptr:共享所有权
多个
shared_ptr可以指向同一个对象。内部有引用计数,最后一个
shared_ptr销毁时释放对象。std::shared_ptr<int> p1 = std::make_shared<int>(10); std::shared_ptr<int> p2 = p1; // 引用计数变为 2适合多个地方需要共享同一个对象的场景。
注意:引用计数操作是线程安全的,但对象本身的读写不是线程安全的。
可能产生循环引用问题。
std::weak_ptr:弱引用,不增加引用计数
weak_ptr指向由shared_ptr管理的对象,但不增加引用计数。它不影响对象生命周期。
主要用来解决
shared_ptr的循环引用问题。不能直接访问对象,需要先
lock()提升为shared_ptr:std::shared_ptr<int> sp = std::make_shared<int>(10); std::weak_ptr<int> wp = sp; if (auto locked = wp.lock()) { std::cout << *locked << std::endl; } else { std::cout << "对象已释放" << std::endl; }
三者对比
| 智能指针 | 所有权 | 引用计数 | 能否拷贝 | 主要用途 |
|---|---|---|---|---|
unique_ptr |
独占 | 无 | 不能,只能移动 | 独占资源管理 |
shared_ptr |
共享 | 有 | 可以 | 多个所有者共享对象 |
weak_ptr |
不拥有 | 不增加计数 | 可以 | 观察 shared_ptr,解决循环引用 |
C++:new 与堆内存
在 C++ 中,new
用来在堆(heap)上动态分配内存,并返回指向该内存的指针。
如果直接用裸 new
分配,必须自己手动释放,否则会造成内存泄漏。但现代 C++
推荐用智能指针或容器,它们会自动释放。