第四章 逻辑回归和多分类逻辑回归
序、机器学习的一般定式
首先,机器学习应当可以实现决策功能;其次,机器学习应该可以辅助开发者进行决策。
一、逻辑回归
(一) 二分问题
让我们举一个医学的例子,假设我们现在面前有一沓病历,每一本都有病人的健康信息,以及病人是否有心脏病的判定。我们的问题是:如何推断一个人有心脏病的概率呢?
在第三章,我们学习了线性分类和线性回归,如图:
接下来的内容如果直接看 PPT 会一脸懵逼,我来做一个省流讲解。
如果要做一个判定函数来判定一个人发生心脏病的概率,最简单的方式自然是 sign function(符号函数),如图所示:
但是,符号函数在梯度计算中存在一系列不方便的地方,所以科学家使用了另外一种我们高考常用的函数来重新拟合概率关系了:
按照惯例,我们就需要使用经典的损失函数去寻找一个好的函数,从而能让这一个函数能够尽量贴近 sign function 的效果。
在这里,我们引入的新函数如图所示:
以下是数学推导:
既然我们已经知道了逻辑损失的形式,那么我们只需要求解条件概率即可。
而任意的 (x[i], y[i]) 都是独立分布的,所以联合概率就相当于每一个条件概率相乘,如图所示:
我们可以根据极大似然估计(Maximum Likelihood Estimation) 来估计出 w 的取值。
最大似然估计(MLE)是一种统计方法,用于对给定数据集的潜在概率分布的参数进行推断。
我们可以利用最大似然估计简化连续乘法的表示形式,如图所示:
和 SVM 类似,我们引入了正则化去简化模型并规避过拟合,如图所示:
让我们对上一节的 SVM 和这一节的逻辑回归做一个对比:
我们可以发现,它们都是有监督的算法,都可以用来解决二分分类的问题
既然我们已经知道了损失函数,要求解 w 的最小值的话,我们只需要求导即可:
(二) 多分类问题引论
此前的研究中,我们只考虑 y[i] 属于 { -1, +1 } 的两种情况,但是万一 y[i] 属于 {0, 1} 或者 y[i] 属于 { 0, +1, ..., K - 1 } 的情况呢?
首先让我们先考虑最简单的情况,如图所示:
我们类比此前的二分问题,也引用这一种估计方法来处理:
我们使用正则化去避免过拟合的情况:
接下来就是大家最熟悉的求导,感兴趣的同学可以自行求解:
二、多分类逻辑回归
在之前的问题中,我们讨论的都是二分问题,那么如何拓展到多分类问题呢?在机器学习中,映射到多个类的概率可以实现“举一反三”的效果:
对于每一个类 j 属于 {0, 1, ..., K - 1},我们会定义一个权重向量 w[j],这个向量和类相对应,m 表示维度, K 表示类别:
这个红色的函数就被叫做 Softmax Function(多分类函数),红色函数的分母是一个归一化的辅助值。
如果我们不采用这种形式,那么 max 不光滑,波动大!
根据矩阵惩罚的相关原理,我们可以把公因数提取出来,如图所示:
三、多分类损失的变体(仅供了解)
(一) Large-Margin Softmax Loss
(二) Angular Softmax Loss(A-Softmax Loss)
(三) 为啥这些多分类损失都用了三角学和几何学的知识啊?推导和一般的 Softmax 有什么区别呢?
第五章 欠拟合、过拟合、交叉验证
序、训练集、测试集和交叉验证集
先回顾一下这三种损失函数,这三种损失函数可是期末的重点!
这就涉及到一个灵魂拷问了:在已知数据集 D 的情况下,我们应该如何评价训练出来的模型效果好不好呢?
在拟合( Regression )问题上,我们可以用中学学过的平均值判定法或者最小方差损失判定法。
但是在很多问题上,不能像线性模型这么直观。这就引入接下来的三种集的概念了。
训练集:用于拟合模型的数据(标签已知) 70%
测试集:用于评估模型的数据(Keep Secret) 30%
比较简单的处理数据集的方式就是将数据拆分成 训练集 和 测试集,比例大改为 7:3,我们只允许使用训练集进行模型的训练,利用训练集训练出的模型经过测试集的测试以后才能得到精准程度的评估。
以下是切分数据集合的代码示例
参考代码来源:machinelearningmastery.com/start-here/
这个博客十分推荐大家一边复习一边去读一读!
from pandas import read_csv
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
# load dataset
url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/housing.csv'
dataframe = read_csv(url, header=None)
data = dataframe.values
# split into inputs and outputs
# 疑问:这段代码我只知道要把最后一列单独提取出来,但具体是啥意思?
'''
X, y = data[:,:-1], data[:, -1]这行代码将数据分为特征(X)和目标变量(y)。data[:,:-1]表示取除了最后一列之外的所有列,这些列是特征;data[:, -1]表示取最后一列,这一列是目标变量,也就是我们要预测的值(在这个案例中是房价)
'''
X, y = data[:,:-1], data[:, -1]
print(X.shape, y.shape)
# split into train test sets
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.33,random_state=1)
print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)
# fit the model
model = RandomForestRegressor(random_state=1)
model.fit(X_train, y_train)
# make predictions
y_hat = model.predict(X_test)
# evaluate predictions
mae = mean_absolute_error(y_test, y_hat)
print('MAE: %.3f' % mae)
后来,学术界认为只有训练集和测试集,就像红绿灯没有黄灯一样。所以学术界又从训练集中割了一块集合,名称交叉验证集,交叉验证集的作用可以让模型的训练有一个比对的样板,让模型变得更好,让我们用大家特别熟悉的高考举个例子:
暂时无法在飞书文档外展示此内容
必考:WHY WE NEED VALIDATION SET ?
为什么机器学习需要交叉验证集?
首先,是三个商业理由
① 我们需要选出最好的模型
② 我们需要评测被选择模型的准确性和强度
③ 我们最好需要评测 ROI of the modeling project(建模项目的投资回报率)
其次,还有三个数据科学理由
① 我们学了这么多模型,不难发现,模型的构建主要是为了减少“损失”或者“偏差”
② 但是。在某种程度上,模型总是会拟合“噪声”以及 “信号”
③ 如果开发者仅仅局限在已知的数据集并且选择“最好”的那一个。那么很有可能就会导致过于乐观的情况出现
为什么高考训练需要大量的模拟考试?
高中三年的训练主要是为了减少在高考的失误率,但是,在某种程度上,我们不可否认的是,除了少数顶尖的名校能用出题人的思维训练极其有潜力的学生,其实很大多数同学复习的时候训练的点会“偏”。而如果我们仅仅局限在本校的考试中表现得好,万一又来一次 2022 年的高考数学全国一,怎么办呢?所以需要有交叉训练集(模考)来点醒同学的训练方向。
一、欠拟合和过拟合
我们老师上课的时候曾经说过:“若无必要,无增其烦。 ” 模型最根本的作用就是获取数据的隐藏趋势。
欠拟合(UnderFitting):模型无法捕捉到数据的底层趋势
过拟合(OverFitting):虽然模型完美拟合数据了,但是模型为了把噪音也纳入进来,导致表现形式很繁琐
如何判定欠拟合和过拟合呢?一张表讲清楚:
| Underfitting(欠拟合) | Overfitting(过拟合) | |
|---|---|---|
| Performance of training-set(训练集的表现) | ERROR IS LARGE | error is relatively small |
| Performance of generalization(通用问题的表现) | ERROR IS LARGE | ERROR IS LARGE |
| Solution(遇到这种情况怎么办?) | ⬆️ Capacity (Complexity) | ⬇️ Capacity (Complexity)⬆️ Training Set |
| Signs(特征) | HIGH BIAS(高偏差) | HIGH VARIANCE(高方差) |
| 大白话 | 训练集表现不佳和真实情况相差相差大 | 模型的错误振荡非常大有时候预测得好,有时候又预测不佳 |
二、偏差-方差权衡(Bias-Variance Trade-off)
对于复杂的模型,过于复杂的模型可以忽略这个模型在未来数据的准确性,这种现象叫做偏差-方差权衡
低偏差:模型在训练集拟合得好
高方差:模型更有可能做出错误的决策
小声 os:在特殊情况下,这种图会失效,但是考纲还是按照 PPT 为准。
想了解更多?请戳这里:mlu-explain.github.io/double-desc…
三、交叉验证
如果我们想减少数据的可变性
◼ 首先,我们可以使用不同分区进行多轮交叉验证
◼ 然后,对所有轮次的结果进行平均
给定条件:从总体 D 中采样的数据 𝑆
我们将 S 拆分成 K 个相等的 disjoint subsets 不相交子集 / 并查集(T[1], ... , T[K])
随后,我们根据以下步骤求解出平均错误率:
如果引入正则化的常数 λ 过大,所有的 θ 都会被惩罚并且变成 0,此时出现欠拟合的情况
如果引入正则化的常数 λ 适中,此时模型拟合效果较好
如果引入正则化的常数 λ 过小,此时出现过拟合的情况
如何选取一个好的 λ 呢?
① 选取一个区间内可能的 λ 的取值,比如说
for λ in range (0.02, 0.26, 0.02):
② 这就出现了 12个 不同的 λ 模型去校验
③ 对于每一个 λ[i],我们需要学习 θ[i],并且计算出 Jcv(θ[i])
④ 选取 使得 Jcv(θ[i]) 最小的 λ
⑤ 最后,我们汇报测试误差 Jtest(θ[i])
如何选取一个好的 λ 呢?(Using K-Fold Cross-Validation)
① 将数据集拆分成训练集、交叉验证集和测试集
② 对于每一个有可能的值 λ,估计错误率
③ 选择 λ 可以让最小平均错误最小
④ 最终评估测试集的效果
引入训练集、测试集、交叉验证集的绩点预测系统
Github 地址:github.com/kanghailong…
import numpy as np
import pandas as pd
import sklearn.datasets as sd
import sklearn.model_selection as sms
import matplotlib.pyplot as plt
import random
# 读取实验训练集和验证集
X_train, y_train = sd.load_svmlight_file('a9a.txt',n_features = 123)
X_valid, y_valid = sd.load_svmlight_file('a9a.t.txt',n_features = 123)
# 将稀疏矩阵转为ndarray类型
X_train = X_train.toarray()#转化为矩阵
X_valid = X_valid.toarray()
y_train = y_train.reshape(len(y_train),1)#转为列向量
y_valid = y_valid.reshape(len(y_valid),1)
#X_train.shape[0]返回数组的大小
X_train = np.concatenate((np.ones((X_train.shape[0],1)), X_train), axis = 1)#给x矩阵多增加1列1,拼接在每一行的末尾
X_valid = np.concatenate((np.ones((X_valid.shape[0],1)), X_valid), axis = 1)
#定义sigmoid函数,传入参数为向量时,对向量的每个参数进行运算后返回一个列表
def sigmoid(z):
return 1 / (1 + np.exp(-z))
#定义 logistic loss 函数
def logistic_loss(X, y ,theta):
hx = sigmoid(X.dot(theta))
cost = np.multiply((1+y), np.log(1+hx)) + np.multiply((1-y), np.log(1-hx))
return -cost.mean()/2
#计算当前 loss
theta = np.zeros((X_train.shape[1],1))
print(logistic_loss(X_train, y_train, theta))
#定义 logistic gradient 函数
def logistic_gradient(X, y, theta):
return X.T.dot(sigmoid(X.dot(theta)) - y)
#定义 logistic score 函数,设置阈值为0.5
def logistic_score(X, y, theta):
hx = sigmoid(X.dot(theta))
hx[hx>=0.5] = 1
hx[hx<0.5] = -1
hx = (hx==y)
return np.mean(hx)
#定义 logistic descent 函数
def logistic_descent(X, y, theta, alpha, num_iters, batch_size, X_valid, y_valid):
loss_train = np.zeros((num_iters,1))
loss_valid = np.zeros((num_iters,1))
data = np.concatenate((y, X), axis=1)
for i in range(num_iters):
sample = np.matrix(random.sample(data.tolist(), batch_size))
grad = logistic_gradient(sample[:,1:125], sample[:,0], theta)#梯度
theta = theta - alpha * grad#更新参数模型
loss_train[i] = logistic_loss(X, y, theta)
loss_valid[i] = logistic_loss(X_valid, y_valid, theta)
return theta, loss_train, loss_valid
#执行梯度下降
#全零初始化
theta = np.zeros((X_train.shape[1],1))
alpha = 0.0001
num_iters = 150
opt_theta, loss_train, Lvalidation = logistic_descent(X_train, y_train, theta, alpha, num_iters, 70, X_valid, y_valid)
#选择合适的阈值,将验证集中计算结果大于阈值的标记为正类,反之为负类,得到的平均值
print(logistic_score(X_valid, y_valid, opt_theta))
iteration = np.arange(0, num_iters, step = 1)
fig, ax = plt.subplots(figsize = (12,8))
ax.set_title('Lvalidation change with iteration')
ax.set_xlabel('iteration')#横坐标
ax.set_ylabel('loss')#纵坐标
plt.plot(iteration, Lvalidation, 'r', label='Validation Set Loss')
# plt.plot(iteration, scores, 'g', label='Score on Validation Set')
plt.legend()
plt.show()
#ques2
#定义 hinge loss 函数
def hinge_loss(X, y, theta, t):
loss = np.maximum(0, 1 - np.multiply(y, X.dot(theta))).mean()
reg = np.multiply(theta,theta).sum() / 2
return t * loss + reg
#计算当前 loss
theta = np.random.random((X_train.shape[1],1))
C = 0.5
print(hinge_loss(X_train, y_train, theta, C))
#定义 hinge gradient 函数
def hinge_gradient(X, y, theta, C):
error = np.maximum(0, 1 - np.multiply(y, X.dot(theta)))
index = np.where(error==0)
x = X.copy()
x[index,:] = 0
grad = theta - C * x.T.dot(y) / len(y)
grad[-1] = grad[-1] - theta[-1]
return grad
#定义 svm decent 函数
def svm_descent(X, y, theta, alpha, num_iters, batch_size, X_valid, y_valid, C):
loss_train = np.zeros((num_iters,1))
loss_valid = np.zeros((num_iters,1))
data = np.concatenate((y, X), axis=1)
for i in range(num_iters):
sample = np.matrix(random.sample(data.tolist(), batch_size))
grad = hinge_gradient(sample[:,1:125], sample[:,0], theta, C)
theta = theta - alpha * grad
loss_train[i] = hinge_loss(X, y, theta, C)
loss_valid[i] = hinge_loss(X_valid, y_valid, theta, C)
return theta, loss_train, loss_valid
#定义 svm score 函数
def svm_score(X, y, theta):
hx = X.dot(theta)
hx[hx>=5] = 1
hx[hx<5] = -1
hx = (hx==y)
return np.mean(hx)
#执行梯度下降
theta = np.random.random((X_train.shape[1],1))
alpha = 0.01
num_iters = 250
opt_theta, loss_train, Lvalidation = svm_descent(X_train, y_train, theta, alpha, num_iters, 70, X_valid, y_valid, C)
#选择合适的阈值,将验证集中计算结果大于阈值的标记为正类,反之为负类,得到的平均值
print(svm_score(X_valid, y_valid, opt_theta))
iteration = np.arange(0, num_iters, step = 1)
fig, ax = plt.subplots(figsize = (12,8))
ax.set_title('Lvalidation change with iteration')
ax.set_xlabel('iteration')
ax.set_ylabel('loss')
plt.plot(iteration, Lvalidation, 'r', label='Validation Set Loss')
plt.legend()
plt.show()
这个绩点预测系统很好,但是这个绩点预测系统忽略了 GPA 和 百分制的换算关系,这种就属于业务逻辑了
利用 Pytorch 解决 Softmax 多分类问题
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
# 定义数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# 加载数据集
train_set = datasets.MNIST('data', train=True, transform=transform, download=True)
test_set = datasets.MNIST('data', train=False, transform=transform, download=True)
# 创建数据加载器
train_loader = torch.utils.data.DataLoader(train_set, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=64, shuffle=False)
# 构建与训练模型
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 128)
self.fc4 = nn.Linear(128, 64)
self.fc5 = nn.Linear(64, 10) # 10个输出类别对应10个数字
self.relu = nn.ReLU()
def forward(self, x):
x = x.view(-1, 784)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.relu(self.fc3(x))
x = self.relu(self.fc4(x))
x = self.fc5(x)
return x
model = Net()
# 损失函数
criterion = nn.CrossEntropyLoss()
# 优化器
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
# 训练函数
def train(epoch):
running_loss = 0.0
for i, data in enumerate(train_loader, 0):
inputs, labels = data
optimizer.zero_grad() # 清零梯度
outputs = model(inputs) # 前向传播
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
running_loss += loss.item()
print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader)}')
# 测试函数
def test(epoch):
correct = 0
total = 0
with torch.no_grad(): # 不计算梯度
for data in test_loader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of the network on the test images: {100 * correct / total}%')
# 训练和测试模型
for epoch in range(2): # 这里只训练2个epoch作为示例
train(epoch)
test(epoch)