情之所起

说起香港,脑海中闪过的第一个念头就是《重庆森林》——重庆大厦里奔走的林青霞、趴在半山扶梯上窥看编号633住处的王菲和她哼唱的《California Dreamin’》。终于要去见识这个憧憬太久的目的地了啊。

阅读全文 »

PriorityQueue的使用场景

1
2
3
4
5
Queue<Integer> queue1 = new PriorityQueue<>();

Queue<Integer> queue2 = new PriorityQueue<>(Comparator.comparingInt(o -> o));

Queue<Integer> queue3 = new PriorityQueue<>(Comparator.comparingInt(o -> -o));

Java中的PriorityQueue常用于实现小顶堆、大顶堆。由上述代码可见,通常有默认无参构造函数、传入Comparator对象等方式的实现。


困惑

从构造函数层面,完全不清楚默认构造函数的排序规则,也完全不明白Comparator实现的comparingInt方法对应的排序规则

根据网上绝大多数文章的描述,都没有解释或者只是说明了对应的排序规则,但是不知道为什么是对应升序或降序。通过阅读源码,才得到准确的认知。


阅读源码

PriorityQueue构造函数、增删元素

image-20240417200527177

可以看到,无参构造函数和指定了Comparator的构造函数实际都是指向PriorityQueue(int initialCapacity, Comparator<? super E> comparator)构造函数。而comparator发挥作用的时候,体现在PriorityQueue增删元素的方法中,以offer方法为例:

image-20240417202910975

根据构造函数是否传入了Comparator对象,元素的比较分别执行siftUpUsingComparatorsiftUpComparable方法,两个方法的逻辑一致,只是Comparator通常作为外部排序工具,Comparable作为对象的内部比较方式。

  • 修改范围Comparable 通常作为对象的内部比较方式,适合单一的自然排序;而 Comparator 作为外部工具,可以定义多种排序方式。
  • 灵活性Comparator 更加灵活,允许不同的排序策略,且可以在不修改原有类的情况下实现。
  • 应用场景:如果排序方式只有一种,或者是对象的自然顺序,推荐使用 Comparable;如果需要多种排序方式,或者不希望修改原有类的结构,推荐使用 Comparator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static <T> void siftUpUsingComparator(
int k, T x, Object[] es, Comparator<? super T> cmp) {
while (k > 0) {
// 无符号右移,找到父节点索引下标
int parent = (k - 1) >>> 1;
Object e = es[parent];
// 将当前元素和父节点进行比较,结果>=0 跳出循环
if (cmp.compare(x, (T) e) >= 0)
break;
// < 0,则交换当前元素和父节点的位置
es[k] = e;
k = parent;
}
// 将当前元素x放入到最终确定的位置
es[k] = x;
}

可知,cmp.compare()方法的返回值< 0时,当前元素与父节点交换位置siftUpComparable方法也是相同,是在不传入Comparator对象也能进行大小比较的前提下,通过ComparablecompareTo()方法进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = es[parent];
if (key.compareTo((T) e) >= 0)
break;
es[k] = e;
k = parent;
}
es[k] = key;
}

Comparator.comparingInt()

1
2
3
4
5
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}

Comparator.comparingInt()的代码可知,它接收一个ToIntFunction作为参数。
o -> o是一个lambda表达式,将元素本身作为整数值返回,实现了小顶堆的效果

1
2

new PriorityQueue<>(Comparator.comparingInt(o -> o));

总结

PriorityQueue的排序规则,依赖于cmp.compare()的具体实现。cmp.compare()方法的返回值< 0时,当前元素与父节点交换位置

数值大小进行直接比较的自然排序为例,数值较小的元素总是会成为父节点,最终形成小顶堆的效果。

数据操作 + 数据预处理

N维数组

N维数组是机器学习和神经网络的主要数据结构。

  1. 0维 标量

    1.0

    表示一个类别

  2. 维 向量

    [1.0, 2.7, 3.4]

    表示一个特征向量

  3. 2维 矩阵

    [[1.0, 2.7, 3.4]

    [5.0, 0.2, 4.6]

    [4.3, 8.5, 0.2]]

    表示一个样本—特征矩阵

  4. 3维

    [ [ [0.1, 2.7, 3.4]

    ​ [5.0, 0.2, 4.6]

    ​ [4.3, 8.5, 0.2] ]

    [ [3.2, 5.7, 3.4]

    [5.4, 6.2, 3.2]

    [4.1, 3.5, 6.2] ] ]

    如RGB图片(w * H * channels )

  5. 4维

    一个RGB图片的批量 (批量大小 * 宽 * 高 * 通道)

  6. 5维

    一个视频批量 (批量大小 * 时间 * 宽 * 高 * 通道)


数据操作

1
import torch

张量(tensor)表示一个数值组成的数组,这个数据可能有多个维度。

  • shape属性:访问张量的形状。
  • numel()函数:访问张量中元素的总数。
  • reshape()函数:改变张量的形状而不改变元素数量和元素值。
1
2
3
x = torch.arange(12)
x.shape
x.numel()

reshape()


  • torch.zeros() 使用全0填充指定形状的张量
  • torch.ones() 使用全1填充指定形状的张量
  • torch.tensor() 为张量中的每个元素赋予确定值
1
2
3
torch.zeros((234))

torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

  • 常见的标准运算符(+-*/和求幂**)都可以被升级为按元素运算

    1
    2
    3
    x = torch.tensor([1.0, 2, 4, 8])
    y = torch.tensor([2, 2, 2, 2])
    x + y, x - y, x * y, x / y, x**y

    image-20240131222451661


  • torch.cat() 连结张量

    image-20240201160930619


  • 通过逻辑运算符构建二元张量

    image-20240201161036811


  • x.sum() 对张量中所有元素进行求和,会产生只有一个元素的张量

  • 即使形状不同,仍然可以通过调用广播机制维度的尺寸要么相等,要么其中一个维度为1)来执行按元素操作。

    image-20240201162656769


  • 可以用[-1]选择最后一个元素

  • 可以用[1:3]选择第二个和第三个元素

  • 可以指定索引将元素写入矩阵

    image-20240201163611717

  • 为多个元素赋值相同的元素,只需要索引所有元素,为其赋值。

    image-20240201163810436


  • Numpy类型的张量转换为pytorch类型的张量

    1
    2
    A = X.numpy()
    B = torch.tensor(A)
  • 将大小为1的张量转换为Python标量

    1
    2
    a = torch.tensor([3.5])
    a.item(), float(a), int(a)

数据预处理

  1. 创建人工数据集,并存储在csv文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import os
    # 创建 ../data文件夹
    os.makedirs(os.path.join('..', 'data'), exist_ok=True)
    # CSV 文件的路径 ../data/house_tiny.csv
    data_file = os.path.join('..', 'data', 'house_tiny.csv')

    with open(data_file, 'w') as f:
    f.write('NumRooms,Alley,Price\n') # 列名
    f.write('NA,Pave,127500\n') # 每行表示一个数据样本
    f.write('2,NA,106000\n')
    f.write('4,NA,178100\n')
    f.write('NA,NA,140000\n')
  2. 从csv文件中加载原始数据集

    image-20240201213545427

  3. 为了处理缺失的数据,典型的方法包括插值删除

    下例通过插入数值,消除缺失的数值。

    1
    2
    3
    4
    inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
    # 用 非空数据的平均值 填充NaN
    inputs = inputs.fillna(inputs.mean(numeric_only=True))
    inputs

    image-20240201224423732

  4. 对于类别值或离散值,将NaN视为一个类别。

    1
    2
    3
    4
    5
    6
    7
    # 独热编码(将分类变量转换为二进制向量)
    # 每个分类变量的每个可能取值都被编码为一个二进制特征
    # 当分类变量有多个不同取值时,会生成更多的列。这可能会导致维度灾难(curse of dimensionality)的问题
    inputs = pd.get_dummies(inputs, dummy_na=True)
    # 将inputs中的所有值转换为数值类型(True/False --> 1/0)
    inputs *= 1
    inputs

    image-20240201234546437

  5. 将dataframe转换为张量

    1
    2
    3
    4
    5
    6
    7
    import torch

    # 把所有条目都是数值类型的 dataframe格式的inputs和outputs
    # 转换为张量格式
    X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)

    X, y

    image-20240201235240914


线性代数

标量

  • 简单操作
    • $c = a + b$
    • $c = a \cdot b$
    • $c = \sin a$
  • 长度
    • $|a|=\begin{cases} a, & \text{if } a > 0 \ -a, & \text{if } x \leq 0 \end{cases}$
    • $|a+b| \leq |a| + |b|$
    • $|a\cdot b|=|a| \cdot |b|$

标量由只有一个元素的张量表示。

1
2
3
4
5
6
import torch

x = torch.tensor([3.0])
y = torch.tensor([2.0])

x + y, x - y, x * y, x / y, x**y

向量

  • 简单操作

    • $c = a + b \text{ where } c_i = a_i + b_i$
    • $c = \alpha \cdot b \text{ where } c_i = \alpha b_i$ (α是标量
    • $c = \sin a \text{ where } c_i = \sin a_i$
  • 长度

    • $||a||2=\sqrt{\sum\limits{i=1}^ma_i^2}$
    • $||a||\geq 0 \text{ for all } a$
    • $||a+b|| \leq ||a|| + ||b||$
    • $||\alpha\cdot b||=|\alpha| \cdot ||b||$ (α是标量
  • 点乘

    $a^Tb = \sum\limits_ia_ib_i$

    如果a与b正交,则$a^Tb = 0$

1
2
3
4
5
6
7
# 可以把向量视为标量值组成的列表
x = torch.arange(4)

# 张量的长度
len(x)
# 只有一个轴的张量,形状只有一个元素。
x.shape # torch.Size([4])

矩阵

  • 简单操作

    • $C = A + B \text{ where }C_{ij}=A_{ij}+B_{ij}$
    • $C=\alpha\cdot B \text{ where } C_{ij}=\alpha B_{ij}$
    • $C=\sin A \text{ where } C_{ij} = \sin A_{ij}$
  • 乘法(矩阵 x 向量

    $C = Ab \text{ where } C_i=\sum\limits_jA_{ij}b_j$ (矩阵A的每一行和向量b做内积)

    image-20240202081512116

  • 乘法(矩阵 x 矩阵)

    $C = AB \text{ where } C_{ik}=\sum\limits_jA_{ij}B_{jk}$

  • 范数(norm)

    F范数:把所有元素的模取平方和,再开方。

    $||A||2=\sqrt{|A{11}|^2+… + |A_{nn}|^2}=\sqrt{\sum\limits_{i,j}|a_{ij}|^2}$

    1
    torch.norm(torch.ones((4, 9)))

    image-20240203005800817

    平方范数$L_2$,


特殊矩阵

  • 对称和反对称矩阵

    • $A_{ij}=A_{ji}$
    • $A_{ij}=-A_{ji}$
  • 正定矩阵

    $x$为任意向量,$x^TAx \geq 0$ ,就称A为正定矩阵。

  • 正交矩阵

    • 所有行/列都是单位向量,且两两正交
    • 可以写成$AA^T=E$ (对角线全为1的单位矩阵)
  • 置换矩阵


  • 特征向量

    不被矩阵改变方向的向量,称为特征向量。对称矩阵总能找到特征向量。

    $Ax = \lambda x$


代码举例

通过指定两个分量$m$和$n$来创建一个形状为$m\times n$的矩阵。

1
2
3
4
A = torch.arange(20).reshape(5, 4)

# 得到矩阵的转置
A.T

哈达玛积 $\odot$

两个矩阵的按元素乘法称为哈达玛积$\odot$,不常用。

1
2
3
4
5
6
7
import torch

A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
# 通过分配新内存,将A的一个副本分配给B
B = A.clone()

A * B

image-20240202224121178

1
2
3
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape

image-20240202224400195


按指定轴求和

1
2
A = torch.arange(20 * 2).reshape(2, 5, 4)
A

image-20240203000156266

  • 针对第1个维度求和

    1
    2
    A_sum_axis0 = A.sum(axis = 0)
    A.shape, A_sum_axis0.shape, A_sum_axis0

    image-20240203000257268

  • 针对第2个维度求和

    1
    2
    A_sum_axis1 = A.sum(axis = 1)
    A.shape, A_sum_axis1.shape, A_sum_axis1

    image-20240203000435931

  • 针对1、2两个维度求和

    1
    A.sum(axis = [0, 1])

    image-20240203000524694

  • 针对第1个维度计算累加和

    1
    A.cumsum(axis = 0)

    image-20240203003928627


平均值

  • A.mean()
  • A.mean(axis=0)

计算总和或均值时 保持轴数不变

1
2
3
4
A = torch.arange(20 * 2.).reshape(2, 5, 4)

# 保持第2维的存在(只有1行),对于广播机制是一个很好的操作
A.sum(axis=1, keepdims=True)

image-20240203003249355


点积 $\cdot$ (点乘、内积)

点积是相同位置的按元素乘积的

1
2
3
x = torch.arange(4.)
y = torch.ones(4, dtype=torch.float32)
x, y, torch.dot(x, y)

image-20240203004516282

可以通过执行按元素乘法(哈达玛积$\odot$),然后求和来表示两个向量的点积。

1
torch.sum(x * y)

矩阵向量积

1
A.shape, x.shape, torch.mv(A, x)

image-20240203005305511


矩阵乘法

1
2
B = torch.ones(4, 3)
torch.mm(A, B)

image-20240203005453450


梯度

梯度是导数在向量上的拓展,指向值变化最大的方向

  1. y是标量,x是向量

    image-20240203010527048

    x是列向量,求导之后,会变成行向量

    image-20240203010607386

    image-20240203010856496

  2. y是向量,x是标量

    image-20240204155019314

    y是列向量,求导之后,还是列向量

    image-20240204155124157

  3. y和x都是向量

    求导之后,是一个矩阵

    image-20240204155431906


自动求导

自动求导计算一个函数在指定值上的导数。自动求导有两种模式:

  1. 正向累积

    $\frac{\partial y}{\partial x}=\frac{\partial y}{\partial u_n}(\frac{\partial u_n}{\partial u_{n-1}}(\dots(\frac{\partial u_2}{\partial u_1}\frac{\partial u_1}{\partial x})))$

  2. 反向累积(反向传递)

    $\frac{\partial y}{\partial x}=(((\frac{\partial y}{\partial u_n}\frac{\partial u_n}{\partial u_{n-1}})\dots)\frac{\partial u_2}{\partial u_1})\frac{\partial u_1}{\partial x}$

反向累积求导的复杂度

  • 时间复杂度 $O(n)$,n是操作子个数

    通常和正向累积代价类似。

  • 空间复杂度$O(n)$

    需要存储正向计算的所有中间结果。正向累积的空间复杂度是$O(1)$


对函数$y = 2x^Tx$ 关于列向量x求导

1
2
3
4
5
6
7
8
9
10
11
import torch

x = torch.arange(4., requires_grad=True)
x, x.grad # 默认值是None

y = 2 * torch.dot(x, x)

# 通过调用反向传播函数,自动计算y关于x每个分量的梯度。
# 执行backward前,y必须是个标量
y.backward()
x.grad

重新计算梯度时,要清除之前的值

如继续对函数$y = \sum\limits_{i=1}^nx_i$关于列向量x求导。

1
2
3
4
5
# 默认情况下,pytorch会累积梯度,需要清除之前的值
x.grad.zero_()
y = x.sum() # y = x_1 + x_2 + ... + x_n
y.backward()
x.grad

将某些计算移动到记录的计算图之外

1
2
3
4
5
6
7
8
9
x.grad.zero_()
y = x * x
# 使u不是一个关于x的函数,而是看做一个常数
u = y.detach()
# 会被看做 常数 * x
z = u * x

z.sum().backward()
x.grad

image-20240204192352450

1
2
3
4
x.grad.zero_()
y.sum().backward()

x.grad

image-20240204192422844


即使构建函数的计算图需要通过Python控制流,仍然可以计算得到变量的梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def f(a):
b = a * 2
# F范数:把所有元素的模取平方和,再开方。
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

# 随机数,size为空,即标量
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()

a.grad == d / a

线性回归 + 基础优化算法

线性回归模型

  • 给定n维输入$\mathbf{x} = [x_1, x_2, \dots, x_n]^T$

  • 线性回归模型有一个n维权重$\mathbf{w}$和一个标量偏差b

    $\mathbf{w}=[w_1, w_2, \dots, w_n]^T, \ b$

  • 输出是输入的加权和

    $y = w_1x_1 +w_2x_2 +\dots+w_nx_n+b$

    向量版本:$y=\langle\mathbf{w},\mathbf{x}\rangle+b$

线性回归模型可以看做单层神经网络,有显式解。


衡量预估质量

使用平方损失(均方损失)衡量预测值和真实值的差异。

$\mathscr{l}(y,\hat{y})=\frac{1}{2}(y-\hat{y})^2$ ($\frac{1}{2}$是为了求导时,方便消去)


参数学习

  • 训练损失

    $\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)=\frac{1}{2n}\sum\limits_{i=1}^{n}(y_i-\langle\mathbf{x}_i,\mathbf{w}\rangle-b)^2$

  • 通过最小化损失函数来学习参数

    $\mathbf{w}^*,\mathbf{b}^*=\arg\min\limits_{\mathbf{w},b}\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)$

均方误差(Mean Squared Error, MSE)是一种常用的损失函数,用于衡量预测值与真实值之间的差异:

$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$

1
2
3
from torch import nn

loss = nn.MSELoss()

基础优化方法

梯度下降

当一个模型没有显式解时,如何操作?

  1. 挑选一个初始值$\mathbf{w}_0$

  2. 重复迭代参数 t = 1, 2, 3

    $\mathbf{w}t= \mathbf{w}{t-1} -\eta \frac{\partial \ell}{\partial \mathbf{w}_{t-1}}$

    $\frac{\partial \ell}{\partial \mathbf{w}{t-1}}$:损失函数$\ell$关于$\mathbf{w}{t-1}$的梯度

    • 沿梯度方向将增加损失函数值
    • 学习率$\eta$:步长超参数(需要人为指定的值)。学习率不能太小,也不能太大

在实际中,很少直接使用梯度下降,最长使用的形式是小批量随机梯度下降

  • 在整个训练集上算梯度成本高昂

    一个深度神经网络模型可能需要数分钟至数小时

  • 可以随机采样 b 个样本$i_1, i_2, …, i_b$来近似损失
    $\frac{1}{b}\sum\limits_{i\in I_b}\ell(\mathbf{x}_i, y_i, \mathbf{w})$

b是批量大小,另一个重要的超参数。


  • 梯度下降通过不断沿着反梯度方向更新参数求解
  • 小批量随机梯度下降是深度学习默认的求解算法
  • 两个重要的超参数批量大小学习率

pytorch实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import torch
import random
from torch.utils import data

def synthetic_data(w, b, num_examples):
"""生成 y = Xw + b + 噪音。"""
# 从均值为0,标准差为1的正态分布中随机采样 形状为(num_examples, len(w)) 的张量x
x = torch.normal(0, 1, (num_examples, len(w)))
# torch.matmul:进行矩阵乘法
y = torch.matmul(x, w) + b
# 添加噪音,从均值为0,标准差为0.01的正态分布中随机采样
y += torch.normal(0, 0.01, y.shape)
# -1:表示根据张量的总元素数量自动确定有多少行
# 1:表示每行的大小为 1。
return x, y.reshape((-1, 1))


true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

调用框架中现有的api来读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
def load_array(data_arrays, batch_size, is_train=True):
"""构造一个pytorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
# 按batch_size来随机读取样本
return data.DataLoader(dataset, batch_size, shuffle=is_train)


batch_size = 10
data_iter = load_array((features, labels), batch_size)

# iter() 将可迭代对象data_iter转换为迭代器对象,
# next() 函数从迭代器中获取下一个元素。
next(iter(data_iter))

使用框架的预定义好的层

1
2
3
4
5
from torch import nn

# Linear 线性回归(全连接层),输入维度是2,输出是1
# Sequential 看作 神经网络层的list
net = nn.Sequential(nn.Linear(2, 1))

初始化模型参数

1
2
3
4
# normal_ 对权重参数的数据进行正态分布初始化,均值为 0,标准差为 0.01
net[0].weight.data.normal_(0, 0.01)
# bias 偏差
net[0].bias.data.fill_(0)

计算均方误差使用MSELoss

$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$

1
loss = nn.MSELoss()

实例化SGD实例

随机梯度下降(SGD, Stochastic Gradient Descent)

1
2
# torch.optim.SGD是一个优化器类
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
num_epochs = 3
for epoch in range(num_epochs):
# 遍历训练数据集的批次
for X, y in data_iter:
# 计算预测值,计算预测值与真实标签之间的损失
l = loss(net(X), y)
# 梯度清零
trainer.zero_grad()
# 反向传播,计算损失函数关于模型参数的梯度s
l.backward()
# 根据计算得到的梯度值和SGD的规则 更新模型参数
trainer.step()
# 对 所有训练数据 进行预测,计算损失值
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')

Softmax回归

Softmax回归其实是一个分类问题。

  • 回归估计一个连续值
    • 跟真实值的区别作为损失
  • 分类预测一个离散类别
    • 通常多个输出
    • 输出i是预测为第i类的置信度

Softmax函数

$softmax(o)=\hat{y}$

$softmax(o)_i = \hat{y}_i=\frac{\exp(o_i)}{\sum_j\exp(o_j)}$

  • $softmax(o)_i$ 是模型输出 o 经过 softmax 函数后的概率分布的第 i 个元素。
  • $\sum_j\exp(o_j)$:所有向量指数的和

使用Softmax函数得到每个类的预测置信度


交叉熵

交叉熵常用来衡量两个概率的区别:$H(p,q)=\sum\limits_i -p_i\log (q_i)$

将它作为损失:
$\ell(y,\hat{y})= -\sum\limits_i y_i\log \hat{y}_i = -\log \hat{y}_y$

将每个类别的真实标签的概率 $y_i$ 乘以预测概率 $\hat{y}_i$ 的负对数,并将所有类别的结果求和。交叉熵衡量了预测概率分布 $\hat{y}_i$ 在真实概率分布 $y_i$ 上的不确定性和差异

交叉熵损失函数对模型输出 o 求梯度,即真实概率和预测概率的区别
$\partial_{o_i}\ell(y, \hat{y}) = softmax(o)_i - y_i$


损失函数

均方损失 $L_2$ Loss

$\ell(y,y^{‘})=\frac{1}{2}(y-y^{‘})^2$


绝对值损失 $L_1$ Loss

$\ell(y, y^{‘})=|y-y^{‘}|$

优化到末期时,就不那么稳定


Softmax回归的从零开始实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import torch
from IPython import display

def get_dataloader_workers():
"""使用4个进程来读取的数据"""
return 4


def load_data_fashion_mnist(batch_size, resize=None):
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
# 接受一个可选参数resize,用来将图像大小调整为另一种形状。
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)

# 这个函数返回训练集和验证集的数据迭代器。
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))


batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
  • softmax的输入需要是一个向量,展平每个图像,将它们视为长度为784(1*28*28)的向量。
  • 因为我们的数据集有10个类别,所以网络输出维度为10
1
2
3
4
5
6
num_inputs = 1 * 28 * 28
num_outputs = 10

# 从均值为0,标准差为0.01的正态分布中随机采样
w = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

实现Softmax回归模型

对于一个矩阵来说,就是按行来做softmax。

$softmax(X){ij}=\frac{\exp (X{ij})}{\sum_{k}\exp (X_{ik})}$

1
2
3
4
5
6
7
8
9
10
11
12
def softmax(X):
# 对每个元素做指数计算
X_exp = torch.exp(X)
# 按照维度为1(行)进行求和
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 应用了广播机制

def net(X):
# torch.matmul:进行矩阵乘法
# -1:表示根据张量的总元素数量自动确定有多少行
# 最终得到1个 所有的元素值>0,行和为1的输出
return softmax(torch.matmul(X.reshape((-1, w.shape[0])), w) + b)

实现交叉熵损失函数

1
2
3
4
def cross_entropy(y_hat, y):
# log实际是ln
# len() 能获取矩阵的行数
return -torch.log(y_hat[range(len(y_hat)), y])

将预测类别与真实y元素进行比较

1
2
3
4
5
6
7
8
9
10
11
def accuracy(y_hat, y):
"""计算预测正确的数量"""
# y_hat 是一个二维张量且有多个列(即表示多类别分类任务的预测结果)
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
# argmax 函数用于在给定轴上找到张量中最大元素的索引
# 使用 argmax 函数沿着列的维度(每行)计算出每个样本的预测标签
y_hat = y_hat.argmax(axis=1)
# y.dtype 获取 y 张量的数据类型,使用 y_hat.type(y.dtype) 将 y_hat 的数据类型转换为与 y 相同的类型。
cmp = y_hat.type(y.dtype) == y
# 计算布尔张量 cmp 中值为 True 的元素的数量,并将结果转换为浮点数。返回预测正确的数量。
return float(cmp.type(y.dtype).sum())

评估准确率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Accumulator:
"""For accumulating sums over `n` variables."""

def __init__(self, n):
# 使用列表推导式创建了一个名为 data 的列表,初始化为长度为 n 的全零列表。
self.data = [0.0] * n

def add(self, *args):
# zip 函数将 data 列表与参数 args 逐个配对,并使用列表推导式将它们相加
self.data = [a + float(b) for a, b in zip(self.data, args)]

def reset(self):
# 将 data 列表的每个元素都设置为 0.0。
self.data = [0.0] * len(self.data)

def __getitem__(self, idx):
return self.data[idx]


def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
# 将模型设置为评估模式(不计算梯度)
net.eval()
# 正确预测数、预测总数
metric = Accumulator(2)
for X, y in data_iter:
# accuracy 预测正确的个数
# `numel()`函数:访问张量中元素的总数。
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]

# 未经训练时的准确率
evaluate_accuracy(net, test_iter)

训练的一个迭代周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 训练模型一个迭代周期
def train_epoch(net, train_iter, loss, updater):
if isinstance(net, torch.nn.Module):
net.train()
metric = Accumulator(3)
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y)
# 如果updater是pytorch的optimizer
if isinstance(updater, torch.optim.Optimizer):
# 使用pytorch内置的优化器,将梯度设为0
updater.zero_grad()
# 计算梯度
l.mean().backward()
# 对参数进行一次更新
updater.step()
else:
l.sum().backward()
updater(X.shape[0])
# 训练损失总和,正确的分类数,样本数 放入累加器
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# loss的累加 / 样本数,分类正确的样本数 / 样本数(训练损失和训练精度)
return metric[0] / metric[2], metric[1] / metric[2]

定义一个在动画中绘制数据的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from matplotlib_inline.backend_inline import set_matplotlib_formats


class Animator:
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
set_matplotlib_formats('svg')
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts

def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)

训练函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def train(net, train_iter, test_iter, loss, num_epochs, updater):
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
# 将当前训练轮次的指标(训练损失、训练准确率和测试准确率)添加到 animator 对象中,用于可视化展示。
# 如果 train_metrics 的值是 (0.2, 0.8),而 test_acc 的值是 0.75,那么 train_metrics + (test_acc,) 的结果将是 (0.2, 0.8, 0.75)。
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
# 检查最后一轮训练的损失是否小于 0.5,如果不满足条件,则抛出异常。
assert train_loss < 0.5, train_loss
# 检查最后一轮训练的准确率是否在 0.7 到 1 之间,如果不满足条件,则抛出异常。
assert 1 >= train_acc > 0.7, train_acc
# 检查最后一轮测试的准确率是否在 0.7 到 1 之间,如果不满足条件,则抛出异常。
assert 1 >= test_acc > 0.7, test_acc


小批量随机梯度下降来优化模型的损失函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def sgd(params, lr, batch_size):
"""随机梯度下降(Stochastic Gradient Descent)
小批量梯度下降"""
# 更新时不参与梯度计算
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()


lr = 0.1


def updater(batch_size):
return sgd([w, b], lr, batch_size)

训练10个迭代周期,对图像进行分类预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
num_epochs = 10
train(net, train_iter, test_iter, cross_entropy, num_epochs, updater)


def get_fashion_mnist_labels(labels):
"""返回Fashion-MNIST数据集的文本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]


def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""绘制图像列表"""
fig_size = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=fig_size)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图片张量
ax.imshow(img.numpy())
else:
# PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes

def predict(net, test_iter, n=6):
"""预测标签"""
for X, y in test_iter:
break
trues = get_fashion_mnist_labels(y)
preds = get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])


predict(net, test_iter)

多层感知机

感知机

给定输入$\mathbf{x}$,权重$\mathbf{w}$,偏移$b$,感知机输出:

$o=\sigma(\langle\mathbf{w},\mathbf{x}\rangle+b)$

$\sigma(x)=\begin{cases}1 \ \ \text{if}\ x > 0\0 \ \ \text{otherwise}\end{cases}$

感知机其实就是二分类的问题。

  • 和线性回归相比,线性回归输出实数
  • 和Softmax回归相比,Softmax是多分类

模型选择

模型容量

模型容量指的是拟合各种函数的能力

  • 低容量的模型难以拟合训练数据
  • 高容量的模型可以记住所有的训练数据。

网络中的网络 NiN

全连接层的问题

卷积层只需要较少的参数,但卷积层之后的第一个全连接层需要的参数量很大

  • 卷积层的参数量: $c_i\times c_o \times k^2$ (输入通道数 x 输出通道数 x 卷积核的高 x 卷积核的宽)
  • 全连接层的参数量:$c_i\times w_i \times h_i \times c_o \times w_o \times h_i$ (输入通道数 x 输入宽度 x 输入高度 x 输出通道数 x 输出宽度 x 输出高度)

参数量大,会占用很多内存、计算带宽、容易过拟合


NiN块

1个卷积层后跟2个1 * 1的卷积层,对每个像素增加了非线性

  • 步幅1,无填充,输出形状跟卷积层输出一样
  • 起到全连接层的作用

NiN架构

  • 无全连接层
  • 交替使用NiN块和步幅为2的最大池化层,逐步减小宽高、增大通道数
  • 最后使用全局平均池化层得到输出,不容易过拟合,更少的参数个数。
    • 输入通道数是类别数

含并行连结的网络 GoogLeNet / Inception V3

Inception块用4条有不同超参数的卷积层和池化层的路来抽取不同的信息,它的一个主要优点是模型参数小,计算复杂度低。

image-20240222140852981

GoogLeNet使用了9个lnception块,是第一个达到上百层的网络,后续有一系列改进。


批量归一化

损失出现在最后,后面的层训练较快。

数据在最底部

  • 底部的层训练较慢
  • 底部层一变化,所有都得跟着变
  • 最后的那些层需要重新学习多次
  • 导致收敛变慢

我们可以在学习底部层的时候避免变化顶部层吗?


  • 固定小批量B里的样本x的均值

    $\mu_B=\frac {1}{|B|}\sum\limits_{x\in B}x_i$

  • 固定小批量B里的样本x的方差

    $\sigma^2_B=\frac{1}{|B|}\sum\limits_{i\in B}(x_i-\mu_B)^2+\epsilon$

    $\epsilon$是为了防止方差为0的一个很小的数。

批量归一化就是通过可学习的参数$\gamma、\beta$得到的结果:

$x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta$


批量归一化层

  • 含有可学习的参数为$\gamma$和$\beta$
  • 作用范围:
    • 全连接层和卷积层输出上,激活函数前
    • 全连接层和卷积层输入上
  • 对全连接层,作用在特征
  • 对于卷积层,作用在通道

批量归一化在做什么

后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度,因此没必要跟丢弃法混合使用。

$x_{i+1}=\gamma\frac{x_i-\hat{\mu}_B}{\hat{\sigma}_B}+\beta$

把$\hat{\mu}_B$和$\hat{\sigma}_B$看作随机偏移随机缩放

批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放。可以加速收敛速度,但一般不改变模型精度


残差网络 ResNet

残差块

  • 串联一个层改变函数类,希望能扩大函数类。
  • 残差块加入快速通道,来得到$f(x)=x+g(x)$的结构

image-20240223134806237

有很多调整变形的排列组合。

有两种残差块:

  1. 高宽减半、通道数翻倍的残差块(步幅stride=2)
  2. 接多个高宽不变的残差块(步幅stride=1)

残差块使得很深的网络更加容易训练,甚至可以训练一千层的网络。


数据增广

数据增强就是在一个已有的数据集中,通过变形数据,使得数据集有更多的多样性,模型繁华性能更好。例如:

  1. 在语言里加入各种不同的背景噪音
  2. 改变图片的形状和颜色

常见的图片的数据增强的形式:

  1. 翻转
  2. 切割
  3. 变色

微调

  • 微调通过使用在大数据上得到的预训练好的模型,来初始化模型权重,来完成提升精度。
  • 预训练模型质量很重要。
  • 微调通常速度更快、精度更高。

序列模型

  • 时序模型中,当前数据跟之前观察到的数据相关
  • 自回归模型使用自身过去数据来预测未来
  • 马尔科夫模型假设当前只跟最近少数数据相关,从而简化模型
  • 潜变量模型使用潜变量来概括历史信息

Redis基础

什么是Redis

Redis是基于C语言开发的NoSQL数据库,它是一种存储KV键值对数据的内存数据库,因此读写速度非常快,被广泛应用于分布式缓存方向。

Redis内置了多种数据类型实现:

  • 5种基础数据类型
    1. String
    2. List
    3. Set
    4. Hash
    5. Zset (有序集合)
  • 3种特殊数据类型
    1. HyperLogLog(基数统计)
    2. Bitmap(位图)
    3. Geospatial (地理位置)

Reids还支持事务、持久化(将内存数据保存在磁盘中,重启时可以再次加载使用)、Lua脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。


Redis为什么速度快?

  1. Redis基于内存,内存的访问速度是磁盘的上千倍。

  2. Redis基于Reactor模式,设计开发了一套高效的事件处理模型,主要是单线程事件循环IO多路复用

    image-20231115104527768

  3. Redis内置了多种优化过后的数据类型/结构实现,性能非常高。


Redis持久化机制

使用缓存的时候,我们经常需要对内存中的数据进行持久化,也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

Redis 支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file, AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

RDB 持久化

Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:

1
2
3
4
5
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令

AOF持久化

与快照持久化相比,AOF (append only file)持久化的实时性更好。Redis 6.0 之后默认开启了AOF持久化,可以通过 appendonly 参数开启:

1
appendonly yes

开启 AOF 持久化后,每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof


Redis线程模型

对于读写命令来说,Redis 一直是单线程模型。在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)


  1. 内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。

    Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间。

  2. 很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。

    如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。


常见的缓存更新策略

Cache Aside Pattern 旁路缓存模式

比较适合读比较多的场景。

Cache Aside Pattern中,服务端需要同时维系数据库和缓存,并且以db为准

  • 步骤:

    1. 更新db

    2. 直接删除cache

      Cache Aside Pattern 旁路缓存模式下,写数据的时候,为什么选择删除cache,而不是更新cache?

      • 对服务端造成资源浪费

        删除cache更直接,如果频繁修改db,就会导致需要频繁更新cache,而cache中的数据可能都没有被访问到。

      • 产生数据不一致问题

        并发场景下,更新cache产生数据不一致问题的概率会更大。

      在写数据的过程中,可以先删除cache,后更新db吗?

      不行,可能会造成数据库和缓存数据不一致

      1. 请求1先把cache中的A数据删除;
      2. 请求2从db中读取数据;
      3. 请求1再把db中的A数据更新。

      在写数据的过程中,先更新db,后删除cache就没问题了吗?

      还是可能出现数据不一致的问题,不过概率很小,因为缓存写入速度比数据库写入速度快很多。

      1. 请求1从db中取数据 (说明此时cache没有缓存该数据)
      2. 请求2更新db中的数据,删除缓存
      3. 请求1将读取的数据写入cache (步骤2很难比步骤3快,因为写数据库一般会先加锁
  • 步骤:

    1. 从cache中读,读取到直接返回
    2. cache中读取不到,就从db读
    3. 把db中读取到的数据放到cache中

Cache Aside Pattern 旁路缓存模式 的缺陷

  1. 首次请求的数据一定不在cache中。

    可以将热点数据提前放入cache。

  2. 如果写操作比较频繁,会导致cache中的数据被频繁删除,影响缓存命中率

    1. 数据库和缓存数据强一致

      加一个锁/分布式锁,更新db的时候,同样更新cache,保证不存在线程安全问题。

    2. 短暂地允许数据库和缓存数据不一致

      更新db的时候,同样更新cache,给缓存加一个比较短的过期时间,即使数据不一致,影响也比较小。


Read/Write Through Pattern 读写穿透模式

读写穿透模式中,服务端把cache视为主要数据存储,从中读取数据并将数据写入其中。cache服务负责将此数据读取和写入db,从而减轻应用程序的职责。

这个模式在平时开发过程中非常少见,大概率是因为Redis没有提供cache将数据写入db的功能。

Read-Through Pattern实际只是在Cache Aside Pattern之上进行了封装。在Cache
Aside Pattern下,发生读请求的时候,如果cache中不存在对应的数据,是由客户端自己负责把数据写入cache,而Read Through Pattern则是cache服务自己来写入缓存的,这对客户端是透明的


Write Behind Pattern 异步缓存写入模式

Write Behind Pattern 异步缓存写入模式 和 Read/Write Through Pattern 读写穿透模式很相似,两者都是由cache服务负责cache和db的读写。

但是读写穿透模式同步更新cache和db,而异步缓存写入模式只是更新缓存,不直接更新db,改为异步批量的方式更新db

这对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新db,但cache服务就挂掉了。

这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少。比如消息队列中消息的异步写入磁盘、MySQL的Innodb Buffer Pool机制都用到了这种策略。

Write Behind Pattern下db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

Spring Boot启动流程

@SpringBootApplication注解

Spring Boot的启动,首先需要一个加了@SpringBootApplication注解的启动类

@SpringBootApplication 注解

这个注解本质上,是由@EnableAutoConfiguration@SpringBootConfiguration@ComponentScan三个注解连起来构成。

  • @EnableAutoConfiguration最为核心

    会导入自动配置AutoConfigurationImportSelector类,这个类会将所有符合条件的@Configuration配置都进行加载。

    @EnableAutoConfiguration 注解
  • @SpringBootConfiguration等同于@Configuration

    将当前类标记为配置类,加载到容器中。

    @SpringBootConfiguration 注解

  • @ComponentScan自动扫描并加载符合条件的Bean


SpringApplication.run方法

image-20231028185647082

在run方法开始执行后,会经历如下4个阶段

  1. 服务构建

    构建SpringApplication本身。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 1. 将资源加载器、主方法类加载至内存中
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

    // 2. 逐一判断对应的服务类是否存在,来确定Web服务的类型。默认是基于Servlet的Web服务,如Tomcat
    this.webApplicationType = WebApplicationType.deduceFromClasspath();

    // 3. 加载初始化类,读取所有"META-INF/spring.factories"文件中的
    // 注册初始化、上下文初始化、监听器这三类配置
    // Spring Boot和Spring Boot Autoconfigure这两个工程中配置了7个上下文初始化和8个监听器
    this.bootstrapRegistryInitializers = new ArrayList<>(
    getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); // 没有默认的注册初始化配置
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

    // 通过StackWalker判断出main方法所在的类(大概率就是启动类本身)
    this.mainApplicationClass = deduceMainApplicationClass();
    }
  2. 环境准备

    SpringApplication.run就是进入环境准备阶段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public ConfigurableApplicationContext run(String... args) {
    // ...

    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    Banner printedBanner = printBanner(environment);
    context = createApplicationContext();
    context.setApplicationStartup(this.applicationStartup);
    prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    refreshContext(context);
    afterRefresh(context, applicationArguments);
    Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
    if (this.logStartupInfo) {
    new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
    }
    listeners.started(context, timeTakenToStartup);
    callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
    if (ex instanceof AbandonedRunException) {
    throw ex;
    }
    handleRunFailure(context, ex, listeners);
    throw new IllegalStateException(ex);
    }
    try {
    if (context.isRunning()) {
    Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
    listeners.ready(context, timeTakenToReady);
    }
    }
    catch (Throwable ex) {
    if (ex instanceof AbandonedRunException) {
    throw ex;
    }
    handleRunFailure(context, ex, null);
    throw new IllegalStateException(ex);
    }
    return context;
    }
  3. 容器创建

  4. 填充容器

概述

jmap命令是一个可以输出所有内存中对象的工具,甚至可以将JVM中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的所有对象的情况(如:产生哪些对象,及其数量)。


命令格式

1
2
3
4
5
6
7
8
 jmap [option] <pid>
(to connect to running process) 连接到正在运行的进程

jmap [option] <executable <core>
(to connect to a core file) 连接到核心文件

jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server) 连接到远程调试服务

基本命令

输出jvm的heap内容到文件

jmap -dump:live,format=b,file=[outputFileName.hprof] [pid]

使用hprof二进制形式输出jvm的heap内容到文件。live子选项是可选的,只输出活的对象到文件。


打印正等候回收的对象的信息

jmap -finalizerinfo [pid]


打印heap的概要信息

JDK 8: jmap -heap [pid]

JDK 9往后:jhsdb jmap --heap --pid [pid]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
root@PC:~# jhsdb jmap --heap --pid 51266
Attaching to process ID 51266, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 17.0.8.1+1-Ubuntu-0ubuntu122.04

using thread-local object allocation.
Garbage-First (G1) GC with 10 thread(s)

Heap Configuration: #堆配置情况,也就是JVM参数配置的结果[平常说的tomcat配置JVM参数,就是在配置这些]
MinHeapFreeRatio = 40 #最小堆可用比例
MaxHeapFreeRatio = 70 #最大堆可用比例
MaxHeapSize = 4169138176 (3976.0MB) #最大堆空间大小
NewSize = 1363144 (1.2999954223632812MB) #新生代分配大小
MaxNewSize = 2499805184 (2384.0MB) #最大新生代分配大小
OldSize = 5452592 (5.1999969482421875MB) #老年代大小
NewRatio = 2 #新生代比例
SurvivorRatio = 8 #新生代与suvivor的比例
MetaspaceSize = 22020096 (21.0MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 2097152 (2.0MB)

Heap Usage: #堆使用情况【堆内存实际的使用情况】
G1 Heap:
regions = 1988
capacity = 4169138176 (3976.0MB)
used = 38374400 (36.5966796875MB)
free = 4130763776 (3939.4033203125MB)
0.9204396299673038% used
G1 Young Generation:
Eden Space:
regions = 3
capacity = 117440512 (112.0MB)
used = 6291456 (6.0MB)
free = 111149056 (106.0MB)
5.357142857142857% used
Survivor Space:
regions = 1
capacity = 2097152 (2.0MB)
used = 2097152 (2.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 16
capacity = 71303168 (68.0MB)
used = 29985792 (28.5966796875MB)
free = 41317376 (39.4033203125MB)
42.05394071691177% used

打印每个class的实例数目、内存占用、类名信息

jmap -histo:live [pid]

OOM的原因

一次性申请过多的对象

更改申请对象的数量。(分页)


内存资源耗尽 未释放

找到未释放的对象进行释放。


本身资源不够

查看堆信息:

JDK 8: jmap -heap [pid]

JDK 11往后:jhsdb jmap --heap --pid [pid]

image-20231024141116327

通过dump定位

系统已经OOM挂掉了

  1. 提前设置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=[指定的存放路径]

    需要保证服务器的硬盘空间够大,因为该设置会记录系统在整个运行过程中所有的对象信息,占用空间。

    发生OOM后,会生成java_pidxxxxx.hprof的heap dump文件。

  2. 使用JProfiler进行查看

    image-20231024162807712
  3. 找到与业务相关的类 - 右键,使用选定对象

    image-20231024163513439
  4. 选择 传入引用,确定。

    image-20231024163605325
  5. 显示到GC根(GC Root)的路径,确定

    image-20231024163702041
  6. 根据显示的线程堆栈,查看代码进行分析。

    image-20231024163834286

系统运行中,还未OOM

方式一 导出dump文件

jmap -dump:format=b,file=[outputFileName.hprof] [pid]

在系统运行阶段导出dump文件,会造成一次FullGC、STW(Stop The World,所有线程中断)。但不导出dump文件的话,时间成本要增高很多


方式二 Arthas

简介

消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通时,将消息转发给相应的应用程序或者服务。

如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。

RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据


Docker下相关命令

  • 下载

    1
    2
    # 要选择带management的版本,带管理页面
    docker pull rabbitmq:3.12-management
  • 创建用户

    1
    2
    3
    4
    rabbitmqctl add_user [username] [password]

    # 赋予管理员权限
    rabbitmqctl set_user_tags [username] administrator

架构

image-20231018100910521


交换机 exchange

发布订阅模式 fanout

发布一次,消费多个。

下载

Apache JMeter - Download Apache JMeter

选择二进制文件包。


Windows下

windows下,解压后运行”\apache-jmeter-5.6.2\bin\jmeter.bat”。

Options- choose language - 选择中文。


image-20231012172118257 image-20231012172258211

image-20231012172436339

image-20231012172550539


Linux下

解压后运行apache-jmeter-5.6.2/bin/jmeter.sh -n -t first.jmx -l result.jtl

  • -n 命令行下执行
  • -t 要运行的测试脚本文件
  • -l 记录结果的文件

查看result.jtl文件,要将文件拷出到windows,在监听器 - 聚合报告 下,打开文件浏览

image-20231013013107361

配置不同用户进行测试

image-20231013121242660

cookie管理器中,通过${}获取CSV中设置的变量值

image-20231013121417139

0%