0%

机器学习02:西瓜书第三章 线性模型

线性回归

主要 follw 教程:https://datawhale.feishu.cn/docs/doccndJC2sbSfdziNcahCYCx70W#

机器学习三要素

  1. 模型:根据具体问题,确定假设空间
  2. 策略:根据评价标准,确定选取最优模型的策略(通常会产出一个“损失函数”)
  3. 算法:求解损失函数,确定最优模型

一元线性回归

以一元线性回归为例:求解发际线高度x和计算机水平y的关系。

  1. 模型:根据经验(或观察数据形态),呈线性关系,所以假设空间 $f(x)=wx+b$,而不是曲线关系。

    • 一元线性回归表达式:$ y = wx + b $
  2. 策略:所有点距离拟合的直线垂直距离最小,即均方误差小,最小二乘法。或者使用极大似然估计,假设$y=wx+b+\epsilon$,对$\epsilon$误差建模,能得出同样策略。

    • 损失函数为均方误差(最小二乘法):

  3. 算法:可以证明 $E_{(w, b)}$是凸函数(可以求2阶偏导数,证明Hessian矩阵半正定),所以能求出闭式解。(但机器学习算法通常没有闭式解,就要用梯度下降法、牛顿法近似求解)

    • 令一阶偏导等于0,可以求出闭式解(此处省略推导):

代码实现

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 numpy as np
import matplotlib.pyplot as plt

def load_data(n, w_true, b_true):
X = np.arange(n)
eps = -0.5 * b_true + np.random.random(n) * b_true
Y = w_true * X + eps
return X, Y

def closed_form_solution(X,Y):
# 参考闭式解公式
X_bar = X.sum() / X.size
Y_bar = Y.sum() / Y.size
w = Y.dot(X - X_bar) / (X.dot(X) - (X.sum()) ** 2 / X.size)
b = Y_bar - w * X_bar
return w, b

X, Y = load_data(20, 3, 15)
w, b = closed_form_solution(X, Y)
Y_predict = X * w + b

# 画图
plt.scatter(X, Y, label="原始数据", c='b', alpha=0.5)
plt.plot(X, Y_predict, label="回归线", c='r')
plt.title('一元线性回归')
plt.xlabel("X")
plt.ylabel("Y")
plt.legend()
plt.show()

结果可视化:

1
2
3
4
5
6
7
8
# 画图
plt.scatter(X, Y, label="原始数据", c='b', alpha=0.5)
plt.plot(X, Y_predict, label="回归线", c='r')
plt.title('一元线性回归')
plt.xlabel("X")
plt.ylabel("Y")
plt.legend()
plt.show()

image-20221219202638629

多元线性回归

与一元线性回归相比,$\boldsymbol{x}$维度变高,写成向量形式:

把$\boldsymbol{w}$和$b$合并成$\boldsymbol{\hat w}$:

故多元线性回归的均方误差可以写成:

其中:

所以可以把$E_{\hat{\boldsymbol{w}}}$向量化为:$E_{\hat{\boldsymbol{w}}}=(\boldsymbol{y}-\mathbf{X} \hat{\boldsymbol{w}})^{\mathrm{T}}(\boldsymbol{y}-\mathbf{X} \hat{\boldsymbol{w}})$

同样可以证明$E_{\hat{\boldsymbol{w}}}$是凸函数(求二阶偏导证明hessian矩阵半正定),$E_{\hat{\boldsymbol{w}}}$对$\hat{\boldsymbol{w}}$求一阶偏导:

其中用到的矩阵微分公式:$\frac{\partial \boldsymbol{x}^{\mathrm{T}} \boldsymbol{a}}{\partial \boldsymbol{x}}=\frac{\partial \boldsymbol{a}^{\mathrm{T}} \boldsymbol{x}}{\partial \boldsymbol{x}}=\boldsymbol{a}, \frac{\partial \boldsymbol{x}^{\mathrm{T}} \mathbf{A} \boldsymbol{x}}{\partial \boldsymbol{x}}=\left(\mathbf{A}+\mathbf{A}^{\mathrm{T}}\right) \boldsymbol{x},\frac{\partial \mathbf{A} \boldsymbol{x}}{\partial \boldsymbol{x}}=\mathbf{A}^{\mathrm{T}}$

再令一阶偏导为零,可以求得闭式解:

代码实现

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
50
import numpy as np
import matplotlib.pyplot as plt

def load_data(n_sample:int, W_true:np.array, b_true):
n_feature = len(W_true)
ones = np.ones(shape=(n_sample,1))
X = np.concatenate([np.random.random((n_sample, n_feature)), ones],axis=1)
eps = -0.5 * b_true + np.random.random(n_sample) * b_true
W_true = np.concatenate([W_true,[1,]],axis=0)
W_true = np.mat(W_true).T
Y = X * W_true + np.mat(eps).T
return X, Y

## 求闭式解
def closed_form_solution(X, Y):
# 用np.mat()转换成matrix以后方便矩阵运算
X = np.mat(X)
Y = np.mat(Y)
XTX = X.T * X
if np.linalg.det(XTX) == 0:
# 行列式为0,不可逆
print('X^T·X 不是满秩矩阵,解不唯一')
return
W = XTX.I * X.T * Y
return W

## 梯度下降法求解
def gradient_descent_trainer(X:np.matrix, Y:np.matrix, W:np.matrix, lr:float):
# 一阶导数
gradient = 2 * X.T * (X * W - Y)
W = W - lr * gradient

return W

def loss_function(X:np.matrix, Y:np.matrix, W:np.matrix):
return float((Y - X * W).T * (Y - X * W))

def gradient_descent_solution(X, Y, epoch, lr):
X = np.mat(X)
Y = np.mat(Y)
W = np.mat(np.random.random((X.shape[1],1)))
lossList = []
for i in range(epoch):
loss = loss_function(X, Y, W)
lossList.append(loss)
if (i+1)%10==0:
print(f"epoch:{i+1} loss:{loss:.4f}")
W = gradient_descent_trainer(X, Y, W, lr)

return W, lossList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
n_sample = 10
W_true = np.array([1,2,3,4,5])
b_true = 1
X,Y = load_data(n_sample, W_true, b_true)

# 求闭式解
W_1 = closed_form_solution(X, Y)
Y_1 = X * W_1

# 梯度下降求解
epoch = 200
lr = 1e-2
W_2, lossList = gradient_descent_solution(X, Y, epoch, lr)
Y_2 = X * W_2

梯度下降 loss 曲线:

image-20221221000604133

结果可视化:

1
2
3
4
5
6
7
8
9
# 画图
plt.scatter(list(range(1,n_sample+1)), Y.tolist(), label="原始数据", c='b', alpha=0.5)
plt.scatter(list(range(1,n_sample+1)), Y_1.tolist(), label="闭式求解数据", c='r', alpha=0.5)
plt.scatter(list(range(1,n_sample+1)), Y_2.tolist(), label="梯度下降求解数据", c='y', alpha=0.5)
plt.title('多元线性回归')
plt.xlabel("X")
plt.ylabel("Y")
plt.legend()
plt.show()

image-20221221000525846

对数几率回归(逻辑回归 LR)

对数几率回归算法的机器学习三要素:

  1. 模型:线性模型,输出值的范围是(0,1),近似阶跃的单调可微函数
  2. 策略:极大似然估计,信息论,殊途同归
  3. 算法:梯度下降,牛顿法

对数几率回归(逻辑回归):用线性模型做二分类任务

用sigmoid函数,将实数域的取值映射到(0,1):

为什么可以用sigmoid?

西瓜书上的解释:$y =\frac{1}{1+e^{- \left( \boldsymbol{w}^{\mathrm{T}} \boldsymbol{x}+b \right)}}$ 可以推导出 $\frac{y}{1-y}= \boldsymbol{w}^{\mathrm{T}} \boldsymbol{x}+b$,若将$y$视作样本$\boldsymbol{x}$作为正例的可能性, 则$1-y$是样本$\boldsymbol{x}$作为负例的可能性,$\frac{y}{1-y}$反应了样本$\boldsymbol{x}$作为正例的相对可能性。

损失函数推导

如果随机变量X只取0和1两个值,并且相应的概率为:

则称随机变量X服从参数为p的伯努利分布

逻辑回归是假设数据服从伯努利分布

在伯努利分布下,假设样本概率为:$f(n)=\left\{\begin{array}{ll}
p, & y=1 \\
1-p, & y=0
\end{array}\right.$,则概率密度函数 $p\left(y_{i} \mid x_i\right)=p^{y_{i}}(1-p)^{1-y_{i}}$,

优化均方误差?

$E_{\hat{\boldsymbol{w}}}=\left(\frac{1}{1+e^{- \left( \boldsymbol{w}^{\mathrm{T}} \boldsymbol{x}+b \right)}}-y\right)^2$,可以证明是非凸的,无法求解

极大似然估计法

所以考虑用极大似然估计求分布:

两边取对数:

其中:

带入$ln(L)$,可得:

因为最大化似然函数就等于最小化:

而$\mathcal{l}\left( \boldsymbol{\beta} \right)$是高阶非凸函数,所以可以用经典的数值优化方法梯度下降法或牛顿法求近似解。$\mathcal{l}\left( \boldsymbol{\beta} \right)$也是模型的损失函数。

为什么不求闭式解?

即然$\mathcal{l}\left( \boldsymbol{\beta} \right)$是高阶非凸的,可以直接解,那为什么不直接按公式求闭式解?

回忆多元线性回归的闭式解:$\hat{\boldsymbol{w}}=\left(\mathbf{X}^{\mathrm{T}} \mathbf{X}\right)^{-1} \mathbf{X}^{\mathrm{T}} \boldsymbol{y}$,这里需要求矩阵的逆,通常情况下都不存在,无法直接求解,甚至要用梯度下降近似求解的方法去求这个逆(回忆矩阵课上学的···),所以不如直接近似求解 $\hat{\boldsymbol{w}}$ 。

最大熵法

前置知识:

  • 自信息 $I(X)=-\log _{b} p(x)$

  • 信息熵 $H(X)=E[I(X)]=-\sum_{x} p(x) \log _{b} p(x)$,是自信息的期望,度量随机变量 $X$ 的不确定性,信息熵越大越不确定

  • 相对熵(KL散度):度量两个分布的差异,典型使用场景是用来度量理想分布$p(x)$和模拟分布$q(x)$ 之间的差异

    • 其中 $-\sum_{x} p(x) \log _{b} q(x)$ 为交叉熵。

    • KL散度前半部分是常量,后半部分是交叉熵,这也是为什么常说逻辑回归的损失函数是交叉熵损失函数。

从机器学习三要素中“策略”的角度来说,与理想分布最接近的模拟分布即为最优分布,因此可以通过最小化相对熵这个策略来求出最优分布。由于理想分布$p(x)$是未知但固定的分布(频率学派的角度),所以KL散度前半部分是常量,那么最小化相对熵就等价于最小化交叉熵。

image-20221220015135440

image-20221220015220368

这边已经等价于用极大似然估计取对数的结果了,后面依然能推导得到公式3.27

代码实现

在 MNIST 数据集上实现逻辑回归二分类,标签0~4分类为0,标签5~9分类为1.

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report

# 整个训练集更新不太合理,mini-batch更新暂时没精力写···所以先实现逐个样本更新的版本
# 部分实现参考 https://github.com/Dod-o/Statistical-Learning-Method_Code
# 使用MNIST数据集
def load_mnist(fileName):
'''
加载Mnist数据集
:param fileName:要加载的数据集路径
:return: list形式的数据集及标记
'''
print('start to read data')
# 存放数据及标记的list
dataArr = []; labelArr = []
# 打开文件
fr = open(fileName, 'r')
# 将文件按行读取
for line in fr.readlines():
# 对每一行数据按切割福','进行切割,返回字段列表
curLine = line.strip().split(',')

# Mnsit有0-9是个标记,由于是二分类任务,所以将>=5的作为1,<5为0
if int(curLine[0]) >= 5:
labelArr.append(1)
else:
labelArr.append(0)
#存放标记
#[int(num) for num in curLine[1:]] -> 遍历每一行中除了以第一个元素(标记)外将所有元素转换成int类型
#[int(num)/255 for num in curLine[1:]] -> 将所有数据除255归一化(非必须步骤,可以不归一化)
dataArr.append([int(num)/255 for num in curLine[1:]])

#返回data和label
return dataArr, labelArr

def loss_fn(X:np.matrix, W:np.matrix, y:np.matrix):
# 每条样本的loss
y = float(y)
eps = 1e-10
y_hat = float(X * W)
return np.log(( 1 + np.exp(y_hat))/np.exp(y * y_hat) + eps)

def lr_forward(x:np.matrix, w:np.matrix) -> float:
z = x * w
return 1/(1 + np.exp(-z))

def gradient_descent_trainer(x:np.matrix, w:np.matrix, y:np.matrix, lr:float) -> np.matrix:
# 逐个样本计算梯度
y = float(y)
z = x * w
gradient = float((-y + np.exp(z)/(1+np.exp(z)))) * x.T
w = w - lr * gradient
return w

def collate_fn(X:list, Y:list):
# 把 wx + b 转化成 beta * x_hat 形式
X = np.mat(X)
Y = np.mat(Y).T
ones = np.ones(shape=(X.shape[0],1))
X = np.concatenate([X, ones],axis=1)
return X, Y

def evaluate(X:np.matrix, w:np.matrix, Y:np.matrix):
n_test = X.shape[0]
y_predList = []
for idx in range(n_test):
y_pred = lr_forward(X[idx],w)
y_predList.append( 1 if y_pred > 0.5 else 0)
y_truth = Y.T.tolist()[0]
report = classification_report(y_true=y_truth, y_pred=y_predList, output_dict=True)
return report['accuracy']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 获取训练集及标签
trainData, trainLabel = load_mnist('Mnist/mnist_train.csv')
X_train, Y_train = collate_fn(trainData, trainLabel)
# 获取测试集及标签
testData, testLabel = load_mnist('Mnist/mnist_test.csv')
X_test, Y_test = collate_fn(testData, testLabel)
# 初始化 w
w = np.random.random((X_train.shape[1],1))
# 样本数
n_train = X_train.shape[0]
n_test = X_test.shape[0]
# 超参数
epoch = 20
lr = 1e-4
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
# 训练及评估
lossTrainList = []
lossTestList = []
accTrainList = []
accTestList = []
for i in range(epoch):
lossTrain4Epoch = 0
lossTest4Epoch = 0

for idx in range(n_train):
y_pred = lr_forward(X_train[idx], w)
# 逐个样本更新w
w = gradient_descent_trainer(X_train[idx], w, Y_train[idx],lr)

# 因为不是batch形式更新, 所以评估起来有些费劲
for idx in range(n_train):
y_pred = lr_forward(X_train[idx], w)
lossTrain = loss_fn(X_train[idx], w, Y_train[idx])
lossTrain4Epoch += lossTrain

for idx in range(n_test):
y_pred = lr_forward(X_test[idx], w)
lossTest = loss_fn(X_test[idx], w, Y_test[idx])
lossTest4Epoch += lossTest

lossTrain4Epoch /= n_train
lossTest4Epoch /= n_test
lossTrainList.append(lossTrain4Epoch)
lossTestList.append(lossTest4Epoch)
# 每个epoch评估
accTrain = evaluate(X_train, w, Y_train)
accTest = evaluate(X_test, w, Y_test)
accTrainList.append(accTrain)
accTestList.append(accTest)
print(f"epoch:{i+1}")
print(f"[Train] acc:{accTrain:.5f} loss:{lossTrain4Epoch:.5f}")
print(f"[Test] acc:{accTest:.5f} loss:{lossTest4Epoch:.5f}")

可视化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def draw_loss(lossTrain:list,lossTest:list):
plt.plot(np.arange(len(lossTrain)), lossTrain, linewidth=1, linestyle="solid", label="train loss")
plt.plot(np.arange(len(lossTest)), lossTrain, linewidth=1, linestyle="solid", label="test loss")
plt.title('梯度下降loss曲线')
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()
plt.show()

def draw_acc(accTrain:list,accTest:list):
plt.plot(np.arange(len(accTrain)), accTrain, linewidth=1, linestyle="solid", label="train acc")
plt.plot(np.arange(len(accTest)), accTest, linewidth=1, linestyle="solid", label="test acc")
plt.title('梯度下降acc曲线')
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.legend()
plt.show()

draw_loss(lossTrainList,lossTestList)
draw_acc(accTrainList,accTestList)

image-20221221000736827image-20221221000753958

为什么要用sigmoid?[1]

Q:为什么 LR 模型要使用 sigmoid 函数?

A:因为bernoulli的指数族分布形式为:

其自然系数 $\eta$ 与 $\mu$ 的关系,也就是inverse parameter mapping 为 $\mu = \frac{1}{1+e^{-\eta}}$

Q:为什么要使用指数族分布?

A:因为指数族分布是给定某些统计量下熵最大的分布,例如伯努利分布就是只有两个取值且给定期望值为 $\mu$ 下的熵最大的分布。

Q:为什么要使用熵最大的分布?

A:最大熵理论

最大熵原理是在1957 年由E.T.Jaynes 提出的,其主要思想是,在只掌握关于未知分布的部分知识时,应该选取符合这些知识但熵值最大的概率分布。因为在这种情况下,符合已知知识的概率分布可能不止一个。我们知道,熵定义的实际上是一个随机变量的不确定性,熵最大的时候,说明随机变量最不确定,换句话说,也就是随机变量最随机,对其行为做准确预测最困难。

从这个意义上讲,那么最大熵原理的实质就是,在已知部分知识的前提下,关于未知分布最合理的推断就是符合已知知识最不确定或最随机的推断,这是我们可以作出的不偏不倚的选择,任何其它的选择都意味着我们增加了其它的约束和假设,这些约束和假设根据我们掌握的信息无法作出。

二分类线性判别分析(LDA)

算法原理

“高内聚、低耦合”的思想

从几何角度,让全体样本沿着一个方向投影(高维投影到一维),经过投影后,使得:

  • 异类样本的中心尽可能远
  • 同类样本的方差尽可能小

损失函数推导

异类样本中心尽可能远

首先是投影,我们假定原来的数据是向量$x$,那么顺着$w$方向的投影就是标量:

经过投影后,异类样本的中心尽可能远:

同类样本的方差尽可能小

我们假设属于两类的试验样本数量分别是$N_0$和$N_1$,那么我们采用方差矩阵${Var}_i$来表征每一个类内的总体分布,这里我们使用了协方差的定义:

使同类样本方差尽可能小:

综合这两点,我们用将这两个值相除来得到我们的损失函数,并最大化这个值:

注意$\mathbf{S}_{w} $和$\mathbf{S}_{b} $ 都是 $d \times d$ 的矩阵

直接对$\boldsymbol{w}$求偏导[2],因为只关心$\boldsymbol{w}$的方向:

image-20221222155347165

于是求得 $\boldsymbol{w}$。

感觉这部分主要是数学,就不撸代码了。

参考

[1]https://www.zhihu.com/question/35322351

[2]https://www.bilibili.com/video/BV1aE411o7qd?p=16&vd_source=a207b39881900fb7c1580eaaa0301073