开始之前,需要了解信息论的一些基本概念。
信息量:一个事件发生的概率越低,信息量越大。怎么理解呢?
从上面的例子中,我们可以得到一个事件的信息可以使用 log 2 n \log_{2}{n} log2n ( n n n是等可能事件的数量)进行表示。一个系统中等可能事件越多( n n n 比较大),一个事件发生的概率就越低,那么信息量自然就大。如果已经得知了一个事件的概率为 p p p,那么信息量又可以表示为:
− log 2 p -\log_{2}{p} −log2p 信息熵:信息熵是对于系统而言的。一个系统中存在各种各样的事件,如何衡量这个系统的总信息量?其实就是对这个系统中的各个事件(假定数量为m)的信息量求期望。也即“m个事件概率与m个事件信息量相乘,再求和”。
H ( P ) = − ∑ i = 1 m p i log 2 p i H(P)=-\sum_{i=1}^{m}p_i\log_{2}{p_i} H(P)=−i=1∑mpilog2pi相对熵(KL散度):现在问题又来了,给定一组事件,事件 i i i在系统P中的概率是 p i p_i pi,在系统Q中的概率是 q i q_i qi,以系统P为基准,如何衡量系统Q相对于系统P的差异?我们可以求出每一个事件在系统Q中相对于系统P的信息量差异,然后在系统P中求期望。这就得到了相对熵。
D K L ( P ∣ ∣ Q ) = ∑ i = 1 m p i ( ( − log 2 q i ) − ( − log i p i ) ) = ∑ i = 1 m p i ( − log 2 q i ) − ∑ i = 1 m p i ( − log 2 p i ) D_{KL}(P||Q)=\sum_{i=1}^mp_i((-\log_2{q_i})-(-\log_i{p_i})) \\ \quad\quad\quad\quad\quad\quad\quad=\sum_{i=1}^mp_i(-\log_2{q_i})-\sum_{i=1}^mp_i(-\log_2{p_i}) DKL(P∣∣Q)=i=1∑mpi((−log2qi)−(−logipi))=i=1∑mpi(−log2qi)−i=1∑mpi(−log2pi)化简之后,可以发现, − ∑ i = 1 m p i ( − log 2 p i ) -\sum_{i=1}^mp_i(-\log_2{p_i}) −∑i=1mpi(−log2pi)这一部分正是系统P的熵,而 ∑ i = 1 m p i ( − log 2 q i ) \sum_{i=1}^mp_i(-\log_2{q_i}) ∑i=1mpi(−log2qi)这一部分我们称为交叉熵。吉布斯不等式证明,系统Q相对于系统P的交叉熵一定会大于或等于系统P的信息熵。
所以,我们只需要把交叉熵减小,就可以减小系统P和系统Q之间的差异!
虽然Softmax回归叫做“回归”,但是它其实是一个用于分类的模型。
回归VS分类
由于分类通常具有多个输出,而且有时候类别并不是数值类型的,可以使用one-hot对其进行编码。比如,有三个类别:猫、狗、鸟,可以编码为:
对于一个具有m个分量的向量 o ⃗ \vec{o} o ,对其使用Softmax函数:
y ^ i = e o i ∑ j = 1 m e o j \hat{y}_i=\frac{e^{o_i}}{\sum_{j=1}^{m}e^{o_j}} y^i=∑j=1meojeoi结果会把这个向量的每一个分量进行归一化(映射到0和1之间),可以把每一个分量都视为概率。
注意:由于这个公式是使用指数进行计算,有时候如果数值太大的情况,会出现指数爆炸的情况,计算机是无法表示这么大的数字的,会出现nan。因此,需要首先对输入的数据集进行预处理。
用于训练和验证的数据集是一个非常经典的数据集——FashionMNIST。这个数据集中的训练集有60000张图片,测试集有10000张图片。图片分为10个类别。
import torch
from torch.utils import data
from torchvision import datasets
from torchvision import transforms
def get_train_test_loader(image_size=28, train_batch_size=10, test_batch_size=10, num_workers=0, is_download=True):
"""
获得训练数据生成器和验证数据生成器,这个数据集总共有10个类别(即10个标签)
:param image_size: 图片的大小,取28
:param train_batch_size: 数据生成器的批量大小
:param num_workers: 数据生成器每次读取时调用的线程数量
:param is_download: 是否要下载数据集(如果还未下载设置为True)
:return: 训练数据生成器和验证数据生成器
"""
data_transform = transforms.Compose([
transforms.Resize(image_size), #设置图片大小
transforms.ToTensor() #转化为tensor张量
])
train_data = datasets.FashionMNIST(root='../data', train=True, download=is_download, transform=data_transform)
test_data = datasets.FashionMNIST(root='../data', train=False, download=is_download, transform=data_transform)
train_loader = data.DataLoader(train_data, batch_size=train_batch_size, shuffle=True, num_workers=num_workers,
drop_last=True)
test_loader = data.DataLoader(test_data, batch_size=test_batch_size, shuffle=False, num_workers=num_workers,
drop_last=True)
return train_loader, test_loader
def softmax(input, w, b):
"""
对于这个网络而言,输入是一个batch_size*784的矩阵(行数是批量大小,共有784个特征),输出要有10列
那么,权重矩阵应该有10次线性组合即10列,10列中的每一列都需要对784个特征进行线性组合,所以有784行,故权重矩阵为784*10
偏差目前是一个列向量
:param input: 网络的输入
:param w: 权重矩阵
:param b: 偏差向量
:return: 输出值是一个batch_size*10的矩阵(每一行加起来都为1)
"""
o = torch.mm(input, w) + b
# 利用softmax公式直接求出
return torch.exp(o) / torch.sum(torch.exp(o), dim=1).reshape(-1, 1)
def cross_entropy_loss(y, y_hat):
"""
交叉熵损失函数
:param y: 真实值,是一个256行向量,类别0~9
:param y_hat: 模型计算出的预测值,是一个256*10的矩阵
:return: 交叉损失函数值
"""
# 求每一个样本(每一行)的预测值相对于真实值的交叉熵
# 这里的求法是非常独特的,巧妙运用了python的语法,y_hat[range(len(y_hat)),y]是指遍历range(len(y_hat))行,每一行取第y个元素
# 看不懂的话,建议再看看交叉熵的公式,深刻理解这个python语法,然后把y(写成one-hot,本质上这个向量也需要是one-hot的
# 形状才能求交叉熵)和y_hat的形状写出来。就很好理解了
return -torch.log(y_hat[range(len(y_hat)), y])
def sgb(w, b, lr, batch_size):
"""参数梯度下降"""
with torch.no_grad():
w -= lr * (w.grad / batch_size)
b -= lr * (b.grad / batch_size)
w.grad.zero_()
b.grad.zero_()
def accuracy(y_hat, y):
"""模型训练完成后,判断预测结果的准确率"""
if y_hat.shape[0] < 2 and y_hat.shape[1] < 2:
raise ValueError("dimesion error")
# 得到预测的y_hat每一行中最大概率所在的索引(索引即类别)
y_hat = y_hat.argmax(axis=1)
# 判断预测类别是否与实际类别相等
judge = y_hat.type(y.dtype) == y
# 现在cmp是一个bool类型的向量,转成0和1,统计1的数量
return float(judge.type(y.dtype).sum()) / len(y)
# 学习率
lr = 0.03
# 批量大小
batch_size = 100
test_batch_size = 10000
# 初始化权重和偏差,权重现在是一个矩阵,每一列都是线性组合的系数。总共有(28*28=784)个特征。此外,标签有10个
w = torch.normal(0, 0.01, (784, 10), requires_grad=True)
b = torch.zeros(10, requires_grad=True)
# 模型
net = softmax
# 损失函数
loss = cross_entropy_loss
# 梯度下降
trainer = sgb
# 获取数据生成器以及数据
train_loader, test_loader = get_train_test_loader(train_batch_size=batch_size, test_batch_size=test_batch_size,
is_download=False)
train_loader_test, _ = get_train_test_loader(train_batch_size=60000, is_download=False)
# 学习代数
num_epoch = 20
# 开始训练
for i in range(num_epoch):
for x, y in train_loader:
# 模型得到预测值
y_hat = net(x.reshape(batch_size, 784), w, b)
# 损失函数
l = loss(y, y_hat).sum()
print(f"\r{batch_size}个批量样本损失为{l}", end="", flush=True)
# 求偏导
l.backward()
# 梯度下降
sgb(w, b, lr, batch_size=batch_size)
with torch.no_grad():
for x, y in train_loader_test:
y_hat = net(x.reshape(len(x), 784), w, b)
l = loss(y, y_hat).sum()
print(f"\n第{i}代,所有训练样本损失为{l}")
print(f"训练集预测正确率:{accuracy(net(x.reshape(len(x), 784), w, b), y)}")
for x, y in test_loader:
print(f"验证集预测正确率:{accuracy(net(x.reshape(len(x), 784), w, b), y)}")
print("=" * 25)
import torch
from torch.utils import data
from torchvision import datasets
from torchvision import transforms
from torch import nn
def get_train_test_loader(image_size=28, train_batch_size=10, test_batch_size=10, num_workers=0, is_download=True):
"""
获得训练数据生成器和验证数据生成器,这个数据集总共有10个类别(即10个标签)
:param image_size: 图片的大小,取28
:param train_batch_size: 数据生成器的批量大小
:param num_workers: 数据生成器每次读取时调用的线程数量
:param is_download: 是否要下载数据集(如果还未下载设置为True)
:return: 训练数据生成器和验证数据生成器
"""
data_transform = transforms.Compose([
# 设置图片大小
transforms.Resize(image_size),
# 转化为tensor张量
transforms.ToTensor()
])
train_data = datasets.FashionMNIST(root='../data', train=True, download=is_download, transform=data_transform)
test_data = datasets.FashionMNIST(root='../data', train=False, download=is_download, transform=data_transform)
train_loader = data.DataLoader(train_data, batch_size=train_batch_size, shuffle=True, num_workers=num_workers,
drop_last=True)
test_loader = data.DataLoader(test_data, batch_size=test_batch_size, shuffle=False, num_workers=num_workers,
drop_last=True)
return train_loader, test_loader
def accuracy(y_hat, y):
"""模型训练完成后,判断预测结果的准确率"""
if y_hat.shape[0] < 2 and y_hat.shape[1] < 2:
raise ValueError("dimesion error")
# 得到预测的y_hat每一行中最大概率所在的索引(索引即类别)
y_hat = y_hat.argmax(axis=1)
# 判断预测类别是否与实际类别相等
judge = y_hat.type(y.dtype) == y
# 现在cmp是一个bool类型的向量,转成0和1,统计1的数量
return float(judge.type(y.dtype).sum()) / len(y)
def init_weights(m):
"""将网络中每一个线性层的所有权重都利用标准差为0.01的正态分布进行初始化,b没有初始化,所以初始为0"""
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
# 学习率
lr = 0.03
# 批量大小
batch_size = 100
test_batch_size = 10000
# 模型(nn.Flatten()是第一层,叫做展平层,会将输入的x的形状batch_size*28*28的矩阵自动reshape成batch_size*784,
# 这样就不用每次都手动对x进行reshape了,第二层是线性层,输入是784个特征,输出是10个向量)
net = nn.Sequential(nn.Flatten(),nn.Linear(784,10))
# apply会将net中的每一层都作为参数进入init_weights进行初始化,当发现是线性层,会对线性层的w自动初始化
net.apply(init_weights)
# 损失函数是交叉熵函数,参数是y_hat和y,注意,会对传入的y_hat先进行一次softmax处理
loss = nn.CrossEntropyLoss()
# 梯度下降
trainer = torch.optim.SGD(net.parameters(), lr=lr)
# 获取数据生成器以及数据
train_loader, test_loader = get_train_test_loader(train_batch_size=batch_size, test_batch_size=test_batch_size,
is_download=False)
train_loader_test, _ = get_train_test_loader(train_batch_size=60000, is_download=False)
# 学习代数
num_epoch = 20
for i in range(num_epoch):
for x, y in train_loader:
# 模型得到预测值
y_hat = net(x)
# 损失函数
l = loss(y_hat,y)
print(f"\r{batch_size}个批量样本损失为{l}", end="", flush=True)
trainer.zero_grad()
# 求偏导
l.backward()
# 梯度下降
trainer.step()
with torch.no_grad():
for x, y in train_loader_test:
y_hat = net(x)
l = loss(y_hat,y)
print(f"\n第{i}代,所有训练样本损失为{l}")
print(f"验证集预测正确率:{accuracy(y_hat,y)}")
for x, y in test_loader:
print(f"测试集预测正确率:{accuracy(net(x), y)}")
print("=" * 25)