参考B站视频使用更佳:《PyTorch深度学习实践》完结合集
个人博客:https://tianjuewudi.gitee.io/
如果代码有格式不对的地方请参考原文:https://tianjuewudi.gitee.io/2021/05/13/pytorch-jiao-xue-ji-shi-li/#toc-heading-10
PyTorch是一个开源的Python机器学习库,基于Torch,用于自然语言处理等应用程序。它是一个基于Python的可续计算包,提供两个高级功能:1、具有强大的GPU加速的张量计算(如NumPy)。2、包含自动求导系统的深度神经网络。
PyTorch和TensorFlow作对比,PyTorch开发商是Facebook,TensorFlow是Google。PyTorch的接口有Python和C++,而TensorFlow接口有Python,C++,JavaScript,Swift。PyTorch调试较简单,TensorFlow在2.0以上版本调试较简单。PyTorch主要用于研究,TensorFlow主要用于生产
张量(Tensor)相当于一个矩阵,它可以是比二维更高的。Tensor的目的是能够创造更高维度的矩阵、向量。举个简单的例子,彩色图像文件(RGB)一般都会处理成3-d tensor,每个2d array中的element表示一个像素,R代表Red,G代表Green,B代表Blue。
具体可参考文档:https://pytorch.org/docs/stable/tensors.html
建立Tensor的方法:
#直接把List放进去
x = torch.tensor([[1,-1],[-1,1]])
#把numpy矩阵放进from_numpy函数中
x = torch.from_numpy(np.array([[1,-1],[-1,1]]))
#产生一个全零tensor(二维)
x = torch.zeros([2,2])
#产生一个全一tensor(三维)
x = torch.ones([1,2,5])
操作Tensor的方法:
#看Tensor每一维的元素个数
x.shape
#把某一维去掉(这里去掉第一维,只有在这一维只有一个元素的情况下降维且不改变原数据)
x = x.squeeze(0)
#在某个位置新加入一个维度,元素个数为1,不改变原数据
x = x.unsqueeze(1)
#把两个维度对调,当只有两维的矩阵对调时即求转置
x = x.transpose(0,1)
#把几个矩阵的某一维度元素拼接在一起,前提是其他维度元素个数相同(下面拼成2*6*3的Tensor)
x = torch.zeros([2,1,3])
y = torch.zeros([2,3,3])
z = torch.zeros([2,2,3])
w = torch.cat([x,y,z],dim = 1)
#常规运算
z = x + y
y = x.pow(2)
#x矩阵中所有元素求和
y = x.sum()
#x矩阵中每一列元素相加
y = x.sum(axis=0)
#x矩阵中每一行元素相加
y = x.sum(axis=1)
#求每一列平均值(二维)(axis=1求行的平均值)
y = x.mean(axis = 0)
#重新塑造3*4矩阵(元素总个数必须一致,否则报错)
x = x.reshape(3,4)
#重新塑造2*2矩阵(元素总个数不用一致,可以只截取前面的一部分)
x = x.resize_(2, 2)
求梯度即求z对x矩阵的导数,结果是对x的各个元素求导,也是一个矩阵。其中z为x各个元素之和。
#requires_grad=True表示需要计算梯度
x = torch.tensor([[1.,0.],[-1.,1.]],requires_grad=True)
#Loss计算公式(构建计算图),这是前馈过程Forward
z = x.pow(2).sum()
#反向传播backward,计算各个梯度
z.backward()
#读出x的梯度
x.grad
注意tensor在进行运算时会构建计算图,之后.backward()之后这个图会从内存中释放。但是,不要在后面直接用张量来计算存标量数据,防止产生向量图,而是把标量取出来计算,应当使用.item()来取出数据或.data更新权重。最后要用到.grad.data.zero_()来对梯度进行清零,否则下一次计算会一直累加,一个简单的例子:
Dataset需要调用到Dataloader里面。
shuffle的意思是每次读数据的顺序是乱的,Testing的时候应使其固定,否则结果会有误差。
初始的方法可以参考我的另一篇文章《Python神经网络编程》。
layer = torch.nn.Linear(32,64) #建立输入32节点,输出64节点的一部分神经网络
layer.weight.shape #输出为torch.Size([64,32])
layer.bias.shape #输出为torch.Size([64])
#激活函数
nn.Sigmoid()
nn.ReLU()
建立神经网络的具体例子:
class LinearModel(torch.nn.Module):
#初始化函数
def __init__(self):
#调用父类的__init__()函数,必用
super(LinearModel,self).__init__()
#构建1输入1输出的线性层,即y=wx+b,可以设置bias=True/False
self.linear = torch.nn.Linear(1,1)
#必须定义的函数,后面直接model(x)可以直接算出估计值y
#这是因为这个函数是放在python的__call__()函数中的
def forward(self,x):
#函数重写,计算y=wx+b
y_pred = self.linear(x)
return y_pred
model = LinearModel() #model是callable可调用的,直接调用例:model(x)使用的是forward函数
criterion = torch.nn.MSELoss(size_average = False)
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01,momentum = 0)
MSELoss即预测的数值和真实值之差求平方然后加和,后面的参数决定是否求平均。SGD即随机梯度下降,后面的第一个参数是传入需要进行训练优化的参数,直接用model.parameters可以直接把模型中定义的所以参数都加入训练中,lr是学习因子,决定学习速率,第三个参数是冲量。用这些参数构建了优化器之后,我们之后可以直接用这个封装好的optimizer对象对整个模型进行优化。
训练过程如下:
for epoch in range(100):
#计算y的预测值
y_pred = model(x_data)
#用上面定义好的损失函数对象传入预测值和真实值来计算Loss值
loss = criterion(y_pred,y_data)
#打印Loss时自动调用__str__()函数,因此不会产生计算图
print(epoch,loss)
#梯度清零
optimizer.zero_grad()
#反向传播,计算梯度
loss.backward()
#对所有我们传入的参数进行梯度更新
optimizer.step()
# 打印权重和偏置值,.item()函数把矩阵转换为数值
print('w=',model.linear.weight.item())
print('b=',model.linear.bias.item())
# 计算预测输出值
x_test = torch.Tensor([[4.0]])
y_test = model(x_test)
print('y_pred = ',y_test.data)
import torch
x_data = torch.Tensor([[1.0],[2.0],[3.0]])
y_data = torch.Tensor([[2.0],[4.0],[6.0]])
class LinearModel(torch.nn.Module):
def __init__(self):
super(LinearModel,self).__init__()
self.linear = torch.nn.Linear(1,1)
def forward(self,x):
y_pred = self.linear(x)
return y_predmodel = LinearModel()
criterion = torch.nn.MSELoss(size_average = False)
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01,momentum = 0)
for epoch in range(100):
y_pred = model(x_data)
loss = criterion(y_pred,y_data)
print(epoch,loss)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('w=',model.linear.weight.item())
print('b=',model.linear.bias.item())
x_test = torch.Tensor([[4.0]])
y_test = model(x_test)
print('y_pred = ',y_test.data)
除了SGD优化器,Pytorch还提供了很多优化器。可以试一试它们的效果
这是一个分类问题并非字面意义的回归问题。在做分类问题的时候,把神经网络输出的数值转化为分类的方法是计算每一个分类的概率,最后决定的分类是概率最大的一项。
Mnist数据集是手写数字的一个数据集,是最基础的数据集之一,可以用来测量各个学习器的性能指标。
Pytorch配套有torchvision的工具包,里面有一个模块可以提供数据集,常用的数据集在里面都有。在运行程序的时候会自动下载。第一个参数是存放的文件夹的位置,第二个参数设定是训练集还是测试集,第三个参数是是否自动下载。如果数据集已经存在则不会重新下载。
import torchvision
train_set = torchvision.datasets.MNIST(root='../dataset/mnist',train=True,download = True)
test_set = torchvision.datasets.MNIST(root='../dataset/mnist',train=False,download = True)
除了这个MNIST数据集,还有分类动物的CIFAR10数据集。
为了找一个函数把实数空间的值映射到0到1的区间内代表概率,因此需要在线性模型后加入Sigmoid函数,这个叫做Logistic函数。 它的导数类似于正态分布函数。
交叉熵损失函数公式(二分类):
L = ∑ i − [ y l o g y ^ + ( 1 − y ) l o g ( 1 − y ^ ) ] L = \sum_i - [ylog\hat{y} + (1-y)log(1-\hat{y})] L=i∑−[ylogy^+(1−y)log(1−y^)]
首先L一定是正数。其中 y ^ \hat{y} y^和y都是0到1的值,为了明确分类y不是0就是1。当y=0时, y ^ \hat{y} y^尽可能趋近于0,才能使得L最小;当y=1时, y ^ \hat{y} y^需要尽可能趋近于0。这样预测值y才能趋近于真实值。
编程上的改动:
搭建模型的函数中(一层线性+SIgmoid):
import torch.nn.functional as F #载入函数包
class LogisticRegressionModel(torch.nn.Module):
def __init__(self):
super(LogisticRegressionModel,self).__init__()
self.linear = torch.nn.Linear(1,1)
def forward(self):
y_pred = F.sigmoid(self.linear(x))
return y_pred
最优化:
criterion = torch.nn.BCELoss(size_average=False) #此处求不求均值会影响后面学习率的设置,不求均值学习率须设小一些
最终结构:
import torchimport torch.nn.functional as F
x_data = torch.Tensor([[1.0],[2.0],[3.0]])
y_data = torch.Tensor([[0.0],[0.0],[1.0]])
class LogisticRegressionModel(torch.nn.Module):
def __init__(self):
super(LogisticRegressionModel,self).__init__()
self.linear = torch.nn.Linear(1,1)
def forward(self,x):
y_pred = F.sigmoid(self.linear(x))
return y_predmodel = LogisticRegressionModel()
criterion = torch.nn.BCELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01,momentum = 0)
for epoch in range(100):
y_pred = model(x_data)
loss = criterion(y_pred,y_data)
print(epoch,loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
绘图:
import numpy as npimport matplotlib.pyplot as plt
#x的范围0到10,取200个点
x = np.linspace(0,10,200)
#把x变成200行1列的矩阵(张量)
x_t = torch.Tensor(x).view((200,1))
#输出预测值y的矩阵
y_t = model(x_t)y = y_t.data.numpy()
#打印图表
plt.plot(x,y)plt.plot([0,10],[0.5,0.5],c='r')
plt.xlabel('Hours')
plt.ylabel('Probability of Pass')
plt.grid()
plt.show()
上面的程序都是基于单输入的,下面讲解多输入。多输入时输出变成
y ^ = σ ( ∑ i w i x i + b ) \hat{y} = \sigma (\sum_i w_i x_i +b) y^=σ(i∑wixi+b)
这样我们就可以利用矩阵运算这种并行计算方式大大提高运算速度,也增加程序可读性。
读取数据:
import numpy as np
#读取文件,第一个参数是文件名,也可以是压缩包,第二个参数是数据分隔符,第三个是数据类型,通常是float32
xy = np.loadtxt('diabetes.csv',delimiter=',',dtype=np.float32)
x_data = torch.from_numpy(xy{
:,:-1})
y_data = torch.from_numpy(xy[:,[-1]]) #用中括号是因为要用矩阵形式而非向量
定义模型:
import torch
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()
self.linear1 = torch.nn.Linear(8,6)
self.linear2 = torch.nn.Linear(6,4)
self.linear3 = torch.nn.Linear(4,1)
self.sigmoid = torch.nn.Sigmoid() #没有参数,只需要一个来构建计算图,这里可以改变激活函数
def forward(self,x):
x = self.sigmoid(self.linear1(x))
x = self.sigmoid(self.linear2(x))
x = self.sigmoid(self.linear3(x))
return xmodel = Model()
优化器:
criterion = torch.nn.BCELoss(size_average = True)
optimizer = torch.optim.SGD(model.parameters(),lr = 0.1)
训练:
for epoch in range(100): #这里没有使用Mini-Bash进行训练,而是全部数据一次训练完成,后面会用到DataLoader进行Mini-Batch的训练
y_pred = model(x_data)
loss = criterion(y_pred,y_data)
print(epoch,loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
可以尝试不同的激活函数,例如除了Sigmoid常用的是ReLU,对应的代码是torch.nn.ReLU()。它对应小于0的输入输出为0,大于0的则直接输出对应值。注意最后的输出层不能用ReLU,否则计算Loss的时候有可能因为ln0而出错。
为了训练时能够跨越鞍点达到全局最优,我们需要分堆进行训练,也就是Mini-Batch的训练。其中一个Epoch代表所有的样本都进行过一次训练,一个Iteration是一个Batch堆进行一次训练,Batch-Size指的是Batch中的样本数。
首先我们要生成一个DataLoader,其中一个参数就是batch_size,第二个参数是shuffle,即是否每次epoch生成的batch都具有随机性,都有所不同,样本都是随机打乱的。还有一个num_workers参数,决定用几个线程读取数据。
import torch
#Dataset为抽象类,不能实例化,只能继承
from torch.utils.data
import Dataset
#DataLoader帮助我们加载数据
from torch.utils.data
import DataLoader
class DiabetesDataset(Dataset):
def __init__(self):
pass
#这个函数为了让我们能通过dataset[index]把数据拿出来
def __getitem__(self,index):
pass
#这个函数为了让我们能通过len(dataset)返回数据条数
def __len__(self):
pass
dataset = DiabetesDataset()
train_loader = DataLoader(dataset=dataset,batch_size=32,shuffle=True,num_workers=2)
文件不大时可以通过init函数把数据都读到内存中,如果数据文件过大,通常只记载标签,然后在后面再把一个个文件读取进来。
注意:Linux和Windows处理多线程的方式不一样,我们需要将用loader迭代的代码封装到if语句中,否则会报错:
if __name__ == '__main__':
for epoch in range(100):
#enumerate把可迭代对象组合成索引序列,索引从0开始,同时输出索引和值
for i,data in enumerate(train_loader,0): # prepare data
Database数据集实现:
import torch
#Dataset为抽象类,不能实例化,只能继承
from torch.utils.data
import Dataset
#DataLoader帮助我们加载数据
from torch.utils.data
import DataLoader
class DiabetesDataset(Dataset):
def __init__(self,filepath):
xy = np.loadtxt(filepath,delimiter=',',dtype=np.float32)
#shape[0]取出行数,即第一个维度值
self.len = xy.shape[0]
self.x_data = torch.from_numpy(xy[:,:-1])
self.y_data = torch.from_numpy(xy[:,[-1]])
#用中括号是因为要用矩阵形式而非向量
#这个函数为了让我们能通过dataset[index]把数据拿出来
def __getitem__(self,index):
return self.x_data[index],self.y_data[index]
#返回(x,y)形式的元组
#这个函数为了让我们能通过len(dataset)返回数据条数
def __len__(self):
return self.len
dataset = DiabetesDataset('diabetes.csv.gz')
train_loader = DataLoader(dataset=dataset,batch_size=32,shuffle=True,num_workers=2)
训练:
if __name__ == '__main__':
for epoch in range(100): #每次循环一个batch
for i,data in enumerate(train_loader,0):
#取出x,y数据的方法,每次取出的是一个batch的数据,并自行组合为两个Tensor赋给inputs和labels
inputs,label = data
#这个输入x的Tensor传给model进行计算,model会执行矩阵运算计算出y的Tensor
y_pred = model(inputs)
loss = criterion(y_pred,labels)
print(epoch,i,loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
完整代码:
import torch from torch.utils.data
import Datasetfrom torch.utils.data
import DataLoader
class DiabetesDataset(Dataset):
def __init__(self,filepath):
xy = np.loadtxt(filepath,delimiter=',',dtype=np.float32)
self.len = xy.shape[0]
self.x_data = torch.from_numpy(xy[:,:-1])
self.y_data = torch.from_numpy(xy[:,[-1]])
def __getitem__(self,index):
return self.x_data[index],self.y_data[index]
def __len__(self):
return self.len
dataset = DiabetesDataset('diabetes.csv.gz')
train_loader = DataLoader(dataset=dataset,batch_size=32,shuffle=True,num_workers=2)
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()
self.linear1 = torch.nn.Linear(8,6)
self.linear2 = torch.nn.Linear(6,4)
self.linear3 = torch.nn.Linear(4,1)
self.sigmoid = torch.nn.Sigmoid()
def forward(self,x):
x = self.sigmoid(self.linear1(x))
x = self.sigmoid(self.linear2(x))
x = self.sigmoid(self.linear3(x))
return xmodel = Model()
criterion = torch.nn.BCELoss(size_average = True)
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01)
if __name__ == '__main__':
for epoch in range(100):
for i,data in enumerate(train_loader,0):
inputs,labels = data
y_pred = model(inputs)
loss = criterion(y_pred,labels)
print(epoch,i,loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
官方数据集的使用方法:
from torchvision import datasets
#设置测试和训练集,是否下载,是否转换为张量,可以转换为0~1或-1~1
train_dataset = datasets.MNIST(root='../dataset/mnist',train=True,transform = transforms.ToTensor(),download = True)
test_dataset = datasets.MNIST(root='../dataset/mnist',train=False,transform = transforms.ToTensor(),download = True)
#分batch训练,否则内存不够加载所有数据
train_loader = DataLoader(dataset=train_dataset,batch_size=32,shuffle=True,num_workers=2)
test_loader = DataLoader(dataset=test_dataset,batch_size=32,shuffle=False,num_workers=2)
在多分类问题上,于二分类不同的在于,二分类只有一个输出即概率,多分类中如果有10个分类,需要设置10个输出。我们希望输出具有竞争性,且符合分布。这里可以引入Softmax层,满足所有输出大于等于0且相加等于1。Softmax层的公式为:
P ( y = i ) = e Z i ∑ j = 0 k − 1 e Z i P(y=i) = \frac{e^{Z_i}}{\sum_{j=0}^{k-1} e^{Z_i}} P(y=i)=∑j=0k−1eZieZi
其中 Z i Z_i Zi是最后一个线性层的输出。这就是Softmax函数。
对于损失函数,参考二分类:
L = − Y l o g Y ^ L = -Ylog\hat{Y} L=−YlogY^
其中 Y ^ \hat{Y} Y^是真实值输出为1的节点的预测值(概率),Y=1。
算法举例:
import numpy as np
y = np.array([1,0,0])
z = np.array([0.2,0.1,-0.1])
y_pred = np.exp(z) / np.exp(z).sum()
loss = (-y * np.log(y_pred)).sum()
print(loss)
实际运用举例:
import torch
#y必须是长整形的张量,当中存放的是最后真实分类的索引,范围是0到输出节点数-1
y = torch.LongTensor([0])
z = torch.Tensor([0.2,0.1,-0.1])
#交叉熵损失,注意:神经网络最后一层直接是线性层即可
criterion = torch.nn.CrossEntropyLoss()
loss = criterion(z,y)print(loss)
NLLLoss损失函数是CrossEntropyLoss损失函数的最后一步,即Softmax和log之后的,所有项相加,去掉负号,再求均值。
下面是对手写MNIST数据集进行训练的例子,原式方法可以查看我另一篇文章《Python神经网络编程》。
导入数据:
import torch
#这是一个对图像进行原始数据处理的工具
from torchvision
import transforms
from torchvision import datasetsfrom
torch.utils.data
import DataLoader
import torch.nn.functional as F
import torch.optim as optim
准备数据:
神经网络要求输入最好比较小,并且遵从正态分布,因此要先把PIL图片转换成Tensor做归一化处理。在多通道图像中有RGB,因此转换的Tensor是三维,第一维是选择RGB,后面的两维就是整张图片灰度。这里的单通道图片变成的是1 * 28 * 28。这里构建Compose类实例,上面的整个过程可以通过第一个ToTensor对象来实现,后面的Normalize中的两个数是进行数据标准化中常用的量,这里均值用0.1307,标准差用0.3081,这些是对整个样本计算的结果,这样可以把样本映射到(0,1)分布上,便于训练。公式为 P i x e l n o r m = p r x e l o r g i n − m e a n s t d Pixel_{norm} = \frac{prxel_{orgin} - mean}{std} Pixelnorm=stdprxelorgin−mean,mean是均值,std是标准差。用映射后的数据去做训练能够得到更好的训练效果。
batch_size= 64
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,),(0.3081,))])
train_dataset = datasets.MNIST(root='../dataset/mnist',train=True,transform = transform,download = True)
test_dataset = datasets.MNIST(root='../dataset/mnist',train=False,transform = transform,download = True)
train_loader = DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False)
可以用以下程序计算平均值标准差:
#算一下数据的均值和标准差
sum1 = 0
sum2 = 0
for record in training_data_list:
all_values = record.split(',')
inputs = (numpy.asfarray(all_values[1:]) / 255.0)
sum1 += inputs.sum()
sum2 += ((inputs - mean)**2).sum()
mean = sum1 /784 /len(training_data_list)
print("平均值:",mean)
std = (sum2 / 784 /len(training_data_list)) ** 0.5
print("标准差:",std)
模型:
激活函数采用ReLU函数,view函数把Tensor转换为元素总数不变,列数为784的Tensor。
class Net(torch.nn.Module):
def __init__(self):
super(Net,self).__init__()
self.l1 = torch.nn.Linear(784,512)
self.l2 = torch.nn.Linear(512,256)
self.l3 = torch.nn.Linear(256,128)
self.l4 = torch.nn.Linear(128,64)
self.l5 = torch.nn.Linear(64,10)
def forward(self,x):
x = x.view(-1,784)
x = F.relu(self.l1(x))
x = F.relu(self.l2(x))
x = F.relu(self.l3(x))
x = F.relu(self.l4(x))
return self.l5(x)model = Net()
优化器:
损失函数为交叉熵函数,优化器带冲量momentum可以优化训练过程
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01,momentum=0.5)
训练:
def train(epoch):
running_loss = 0.0
for batch_idx,data in enumerate(train_loader,0):
inputs,target = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs,target)
loss.backward()
optimizer.step()
running_loss += loss.item()
if batch_idx % 300 == 299:
print('[%d,%5d] loss: %.3f' % (epoch + 1,batch_idx + 1,running_loss / 300))
running_loss = 0
测试:
def test():
correct = 0
total = 0
with torch.no_grad():
#这部分代码不会计算梯度
for data in test_loader:
images,labels = data
outputs = model(images)
#这里用max函数找输出节点中的最大值(即输出矩阵中每一行的最大值),返回该值和对应下标
_,predicted = torch.max(outputs.data,dim=1)
#labels.size(0)返回行数,也即是样本个数
total += labels.size(0)
#把两个N*1的Tensor做比较相等是1否则是0,把所有结果相加就是正确的个数
correct += (predicted == labels).sum().item()
print('Accuracy on test set: %d %%' % (100 * correct / total))
整体程序:
import torch
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
batch_size= 64
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,),(0.3081,))
])
train_dataset = datasets.MNIST(root='../dataset/mnist',train=True,transform = transform,download = True)
test_dataset = datasets.MNIST(root='../dataset/mnist',train=False,transform = transform,download = True)
train_loader = DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False)
class Net(torch.nn.Module):
def __init__(self):
super(Net,self).__init__()
self.l1 = torch.nn.Linear(784,512)
self.l2 = torch.nn.Linear(512,256)
self.l3 = torch.nn.Linear(256,128)
self.l4 = torch.nn.Linear(128,64)
self.l5 = torch.nn.Linear(64,10)
def forward(self,x):
x = x.view(-1,784)
x = F.relu(self.l1(x))
x = F.relu(self.l2(x))
x = F.relu(self.l3(x))
x = F.relu(self.l4(x))
return self.l5(x)
model = Net()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01,momentum=0.5)
def train(epoch):
running_loss = 0.0
for batch_idx,data in enumerate(train_loader,0):
inputs,target = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs,target)
loss.backward()
optimizer.step()
running_loss += loss.item()
if batch_idx % 300 == 299:
print('[%d,%5d] loss: %.3f' % (epoch + 1,batch_idx + 1,running_loss / 300))
running_loss = 0
def test():
correct = 0
total = 0
with torch.no_grad(): #这部分代码不会计算梯度
for data in test_loader:
images,labels = data
outputs = model(images)
#这里用max函数找输出节点中的最大值(即输出矩阵中每一行的最大值),返回该值和对应下标
_,predicted = torch.max(outputs.data,dim=1)
#labels.size(0)返回行数,也即是样本个数
total += labels.size(0)
#把两个N*1的Tensor做比较相等是1否则是0,把所有结果相加就是正确的个数
correct += (predicted == labels).sum().item()
print('Accuracy on test set: %d %%' % (100 * correct / total))
if __name__ == '__main__':
for epoch in range(10):
train(epoch)
test()
当自己拥有数据集时的根据上面改写的程序:
import torch
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
batch_size = 64
class DiabetesDataset(Dataset):
def __init__(self, filepath):
xy = np.loadtxt(filepath, delimiter=',', dtype=np.float32)
self.len = xy.shape[0]
self.x_data = torch.from_numpy(xy[:, 1:])
self.y_data = torch.from_numpy(xy[:, [0]])
self.y_data = self.y_data .squeeze(1).long()
def __getitem__(self, index):
return self.x_data[index], self.y_data[index]
def __len__(self):
return self.len
train_dataset = DiabetesDataset('mnist_train.csv')
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = DiabetesDataset('mnist_test.csv')
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.l1 = torch.nn.Linear(784, 512)
self.l2 = torch.nn.Linear(512, 256)
self.l3 = torch.nn.Linear(256, 128)
self.l4 = torch.nn.Linear(128, 64)
self.l5 = torch.nn.Linear(64, 10)
def forward(self, x):
x = x.view(-1, 784)
x = F.relu(self.l1(x))
x = F.relu(self.l2(x))
x = F.relu(self.l3(x))
x = F.relu(self.l4(x))
return self.l5(x)
model = Net()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(),lr = 0.01,momentum=0.5)
def train(epoch):
running_loss = 0.0
for batch_idx, data in enumerate(train_loader, 0):
inputs, target = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, target)
loss.backward()
optimizer.step()
running_loss += loss.item()
if batch_idx == 0:
print('The initial loss: %.3f' % running_loss )
if batch_idx % 150 == 149:
print('[%d,%5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
running_loss = 0
def test():
correct = 0
total = 0
with torch.no_grad(): # 这部分代码不会计算梯度
for data in test_loader:
images, labels = data
outputs = model(images)
# 这里用max函数找输出节点中的最大值(即输出矩阵中每一行的最大值),返回该值和对应下标
_, predicted = torch.max(outputs.data, dim=1)
# labels.size(0)返回行数,也即是样本个数
total += labels.size(0)
# 把两个N*1的Tensor做比较相等是1否则是0,把所有结果相加就是正确的个数
correct += (predicted == labels).sum().item()
print("Total:",total)
print("Corrcet:",correct)
print('Accuracy on test set: %.2f %%' % (100 * correct / total))
if __name__ == '__main__':
for epoch in range(20):
train(epoch)
test()
如果把上面的ReLU函数变成Sigmoid函数,则收敛速度会大大降低,ReLU函数能让Loss在20代收敛到0,准确率达到98.33%,Sigmoid函数则500个epoch都不能完全收敛,准确率97.14%。而且Loss的下降速度和batch_size的取值也有很大关系,上面batch_size取值是64能得到很好的收敛速度,不能取得过小,取成10都会使Loss的值出现nan的情况,而且训练速度过慢。因此取大一些是有好处的,取128和256的结果没有太多改变但训练速度大大加快。
全连接的神经网络,意思是网络中用的都是线性层串行的方式连接,如上面学过的内容。这里来探讨处理图像时用到的二维卷积神经网络。上面的手写数字是1*28*28的张量,但是我们强行把它拆成一维来训练了。这里我们建议先通过一个卷积层,保留图像的空间结构。可以先把它5*5卷积成4*24*24的三维张量,然后可以进行2*2的下采样,变成4*12*12,下采样不改变通道数,但改变图像的宽高,这样可以减少数据量,降低运算需求。然后再进行5*5卷积变成8*8*8,进行2*2下采样变成8*4*4,然后按一定顺序把它们展开成一维Tensor,通过全连接层,最终映射到10个输出节点,通过交叉熵损失解决分类问题。前面的卷积下采样的工作称为特征提取(Feature Extraction),后面的全连接网络称为分类器(Classification)。
成像的原理:
利用透镜把一束束光打到光敏电阻上,通过电流的变化可以求得电阻值,从而求得对应的光照强度。一个像素就需要红绿蓝三种不同的光敏传感器,从而得到彩色图像。这就是RGB图片。每个颜色都有0到255的灰度级别,这就是栅格图像。还有一种矢量图像,不能直接捕获大部分靠人工生成。描述的时候按照图片的圆心、直径、边框颜色、填充颜色等,因此放大时也是圆滑的而不是栅格的,因为这就是现画的。
我们每次取一个小区域做卷积,从左到右从上到下依次卷积,每一个小区域都含三通道,最后把每一个区域输出的卷积结果拼到一起。
其中一个区域做3*3的卷积运算过程如下:
首先中间第二列的矩阵是3*3卷积核,RGB每一个通道各有一个卷积核。每个通道从左到右从上到下依次拿出和卷积核大小相同的矩阵,和卷积核做乘法,这个乘法是矩阵中的每个元素和另一个矩阵相同位置的元素相乘,得到9个数,然后把它们相加得到一个数,因此做3*3的卷积可以把原矩阵减小两行两列,5*5的矩阵做3*3的卷积可以得到3*3的矩阵。最后把三个通道卷积出来的矩阵相加,就可以得到输出结果,并使通道数减小到1。相同通道的图像块用的是一个卷积核,这也叫做共享权重的机制。
如果想要得到多个输出通道,把上面的过程再进行重复即可,输出通道有几个,就需要几组卷积核。
因此卷积核是一个四维的张量,形式是:输出通道数 * 输入通道数 * 卷积核宽度 * 卷积核高度。
程序的计算过程:
import torch
in_channels,out_channels = 5,10 #输入输出通道数
width,height = 100,100 #图像大小
kernel_size = 3 #卷积核大小
batch_size = 1
#注意输入是四维的,第一维是第几个batch
inputs = torch.randn(batch_size,in_channels,width,height)
#用Conv2d模块来生成卷积核
conv_layer = torch.nn.Conv2d(in_channels,out_channels,kernel_size=kernel_size)
output = conv_layer(inputs)
print(inputs.shape) #torch.Size([1,5,100,100])
print(output.shape) #torch.Size([1,10,98,98])
print(conv_layer.weight.shape) #torch.Size([10,5,3,3])
输出图像宽高等于输入图像(padding参数的使用)
如果需要输出图像的大小要等于输入图像,我们需要在输入图像外面填充一圈0数据再做卷积,具体的填充层数和卷积核大小有关。
程序:
import torch
in_channels,out_channels = 1,1
width,height = 5,5
kernel_size = 3
batch_size = 1
inputs = torch.randn(batch_size,in_channels,width,height)
#这里多了一个padding也就是填充的层数,bias是是否设置卷积后有偏置值
conv_layer = torch.nn.Conv2d(in_channels,out_channels,kernel_size=kernel_size,padding = 1,bias = False)
kernel = torch.Tensor([1,2,3,4,5,6,7,8,9]).view(1,1,3,3)
#设定卷积核的值
conv_layer.weight.data = kernel.data
output = conv_layer(inputs)
print(inputs)
print(output)
调整步长(stride参数的使用)
卷积是的步长是指,在卷积核对输入的一个通道做卷积时,往左右上下移动的时候都是挑一格来进行。
例如上图的卷积中心就在输入的4,8,7,6四个点处,因此输出的图像是2*2的。可以有效降低图像的数据量。代码只需要添加参数即可:
conv_layer = torch.nn.Conv2d(in_channels,out_channels,kernel_size=kernel_size,stride = 2,bias = False)
用得最多的叫最大池化层(MaxPooling),这个层是没有权重的。
这里使用2*2的MaxPooling是将原来的矩阵数据划分为一个个2*2的区域,然后每个区域取其中最大值,这样可以把数据量减少4倍。
import torch
inputs = torch.randn(batch_size,in_channels,width,height)
maxpooling_layer = torch.nn.MaxPool2d(kernel_size = 2) #kernel_size决定划分的区域是几乘几
output = maxpooling_layer(inputs)
print(inputs)
print(output)
模型:
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10,kernel_size = 5)
self.conv2 = torch.nn.Conv2d(10, 20,kernel_size = 5)
self.pooling = torch.nn.MaxPool2d(2)
self.fc = torch.nn.Linear(320, 10)
def forward(self, x):
#这里拿出第1维的个数即(n,1,28,28)中的第一个元素,是样本一个batch的样本个数
#注意输入数据是四维的
batch_size = x.size(0)
x = F.relu(self.pooling(self.conv1(x)))
x = F.relu(self.pooling(self.conv2(x)))
#把它变成全连接网络batch * 320
x = x.view(batch_size,-1)
x = self.fc(x)
return x
model = Net()
除了模型以外,其他程序都和上一章的多分类问题一致。如果要切换到显卡计算,提高运算速度,程序如下:
#这里cuda:0指用的第一块显卡,把模型的参数缓存都放到cuda里计算
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
还需要把输入和输出迁移到GPU上,注意模型和数据使用同一块显卡:
def train(epoch):
--snip--
inputs,target = data
inputs,target = inputs.to(device),target.to(device)
--snip--
def test(epoch):
--snip--
inputs,target = data
inputs,target = inputs.to(device),target.to(device)
--snip--
最终模型改进:
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10,kernel_size = 5)
self.conv2 = torch.nn.Conv2d(10, 20,kernel_size = 5)
self.pooling = torch.nn.MaxPool2d(2)
self.l1 = torch.nn.Linear(320, 256)
self.l2 = torch.nn.Linear(256, 64)
self.l3 = torch.nn.Linear(64, 10)
def forward(self, x):
#这里拿出第1维的个数即(n,1,28,28)中的第一个元素,是样本一个batch的样本个数
#注意输入数据是四维的
batch_size = x.size(0)
x = F.relu(self.pooling(self.conv1(x)))
x = F.relu(self.pooling(self.conv2(x)))
#把它变成全连接网络batch * 320
x = x.view(batch_size,-1)
x = F.relu(self.l1(x))
x = F.relu(self.l2(x))
x = self.l3(x)
return x
经过改进后,经过30个epoch的训练,MINIST数据集的准确率达到了99.11%!!!这是一个巨大的提升。
如上图就是GoogleNet的结构,由非常多的分支组成,但是其中有迹可循,很多结构都是重复的。我们可以把这种重复的结构封装使用,进行响应特征提取处理,减少代码冗余,这种结构叫做Inception Module。
Inception Module有各种各样的构建方式,由于我们在选择卷积核的时候不知道哪个结构是最优的,因此我们把几个结构拼接到一起,让模型自己选择。
四条路径是四个张量,最后沿着通道拼成一个张量。这就要要求图像和高度一致,经过卷积和下采样都不变化,卷积可以通过设置对应的pedding来保证宽高不变,均值池化(Average Pooling),不用默认的步长,设为Stride = 1即可避免缩小一半宽高,然后再通过pedding进行调整,例如3*3的AveragePooling可以通过Stride = 1,Pedding = 1,然后求得的是九个格子的均值。然后是1*1的卷积:
这个卷积把每个通道的灰度值加权求和,得出的宽高不变,每个通道的卷积代表一个通道色彩在分类中的重要程度。
可以看出在加了一层1*1的卷积层之后,把通道数先降下来,计算结果没变计算量反而减少了一个数量级,因此1*1的卷积对减少计算量有着显著作用,这种计算量的减少意味着我们可以尝试更多地权重组合,做出更加复杂的网络。
InceptionModule程序实现:
这样均值池化和三种卷积核的优化路径都齐了,拼在一起,梯度下降算法会自行选择最为合适的参数进行优化。
class InceptionA(torch.nn.Module):
def __init__(self,in_channels):
super(InceptionA, self).__init__()
self.branch1x1 = nn.Conv2d(in_channels,16,kernel_size=1)
self.branch5x5_1 = nn.Conv2d(in_channels,16,kernel_size=1)
self.branch5x5_2 = nn.Conv2d(16,24,kernel_size=5,padding=2)
self.branch3x3_1 = nn.Conv2d(in_channels,16,kernel_size=1)
self.branch3x3_2 = nn.Conv2d(16,24,kernel_size=3,padding=1)
self.branch3x3_3 = nn.Conv2d(24,24,kernel_size=3,padding=1)
self.branch_pool = nn.Conv2d(in_channels,24,kernel_size=1)
def forward(self, x):
branch1x1 = self.branch1x1(x)
branch5x5 = self.branch5x5_1(x)
branch5x5 = self.branch5x5_2(branch5x5)
branch3x3 = self.branch3x3_1(x)
branch3x3 = self.branch3x3_2(branch3x3)
branch3x3 = self.branch3x3_3(branch3x3)
branch_pool = F.avg_pool2d(x,kernel_size=3,stride=1,padding=1)
branch_pool = self.branch_pool(branch_pool)
outputs = [branch1x1,branch5x5,branch3x3,branch_pool]
return torch.cat(outputs,dim=1)
把四个路径得出来的通道都合在一起,根据梯度下降算法,有利于降低Loss的通道中的值会升高,不利于降低Loss的会降低,这就是四条路径的好处。另外,构造函数中包含了输入通道数,这样就可以适应各种输入。输出通道数是88。
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10,kernel_size = 5)
self.conv2 = torch.nn.Conv2d(88, 20,kernel_size = 5)
self.incep1 = InceptionA(in_channels = 10)
self.incep2 = InceptionA(in_channels = 20)
self.mp = nn.MaxPool2d(2)
self.fc = torch.nn.Linear(1408, 10)
def forward(self, x):
in_size = x.size(0)
#变成10*12*12
x = F.relu(self.mp(self.conv1(x)))
#变成88*12*12
x = self.incep1(x)
#变成20*4*4
x = F.relu(self.mp(self.conv2(x)))
#变成88*4*4
x = self.incep2(x)
#展开成全连接层
x = x.view(in_size,-1)
x = self.fc(x)
return x
换了这个网络之后,正确率是98.9%。。。而上一个CNN是99.1%,因此我们应当选取合适的网络,不是越复杂越好。
注意:如果Loss输出为nan说明训练不收敛,学习率太大导致梯度爆炸。因此需要降低学习率。
如上图所示,这是一个残差网络(ResNet),其存在的意义在于解决梯度消失问题,当网络做得越深,靠近输入端的权重因为求导的链式法则使得其值接近于0,导数权值不更新,这样相当于只设置了后面的层而前面的层失去意义,这就是梯度消失。为了解决这个问题,可以把前面层的输出直接跨越一些层加到后面当中,前提数张量大小须一致,这样求梯度时后面加进来的一项相当于一个较为浅层的神经网络,前面的梯度依旧可以更新。
ResidualBlock的编程实现:
class ResidualBlock(nn.Module):
def __init__(self,channels):
super(ResidualBlock, self).__init__()
self.channels = channels
self.conv1 = torch.nn.Conv2d(channels, channels,kernel_size = 3,padding=1)
self.conv2 = torch.nn.Conv2d(channels, channels,kernel_size = 3,padding=1)
def forward(self, x):
y = F.relu(self.conv1(x))
y = self.conv2(y)
return F.relu(x + y)
整体网络:
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 16,kernel_size = 5)
self.conv2 = torch.nn.Conv2d(16, 32,kernel_size = 5)
self.rblock1 = ResidualBlock(16)
self.rblock2 = ResidualBlock(32)
self.mp = nn.MaxPool2d(2)
self.fc = torch.nn.Linear(512, 10)
def forward(self, x):
in_size = x.size(0)
#变成16*12*12
x = F.relu(self.mp(self.conv1(x)))
#变成16*12*12
x = self.rblock1(x)
#变成32*4*4
x = F.relu(self.mp(self.conv2(x)))
#变成32*4*4
x = self.rblock2(x)
#展开成全连接层
x = x.view(in_size,-1)
x = self.fc(x)
return x
这个网络的效果异常显著,可以将正确率提高到99.2%!!还可以将全连接层多加线性层,可以再次优化网络结构!
我们从以上程序还能学习到的是如果网络结构非常复杂,可以用新的类进行封装。
1、 I d e n t i t y M a p p i n g s i n D e e p R e s i d u a l N e t w o r k s Identity Mappings in Deep Residual Networks IdentityMappingsinDeepResidualNetworks
论文中给了非常多的ResidualBlock的设计。可以自己尝试去实现几种看看效果。
2、 D e s e l y C o n n e c t e d C o n v o l u t i o n a l N e t w o r k s Desely Connected Convolutional Networks DeselyConnectedConvolutionalNetworks
这也是一种值得探究的实现方式。
以前使用的全连接网络叫做Dense或Deep网络,也叫DNN。
现在假如我们要预测明天的天气情况,我们是拿不到明天的温度气压等信息的,我们的输入信息只能是前几天的温度气压等信息,然后输出明天的天气情况,这时候把几天的数据拼接在一起使用全连接网络也可以,但是全连接网络的权重信息是非常多的,比CNN都还要多得多。这是DNN因为输入的每一个节点都和输出节点建立权重,而CNN是因为是卷积核共享权重的机制使得权重数量较少。
因此RNN专门用来处理这种序列模式的数字,我们也要用这种权重共享的概念来减少需要训练的权重数字。如果我们拥有前三天的天气数据,我们假定第一天和第二天有联系,第二天和第三天有联系,也就是说后一天的情况依赖于前一天,也就是说这种带有序列,后者依赖前者的数据是RNN的处理对象。
RNN的本质上是一个线性层,不同于CNN它的权重是共享的,流程可以看右边的图,输入和线性层和前面的先验值进行线性变换,进行输出并且传递到下一层,下一层的x,和这一层的x进行某种融合。第一个先验值可以通过CNN+FC进行生成(例如图像到文本),或者设置全0,注意里面进行运算的RNN Cell是同一个线性层。
上图是CNN Cell的构造,首先先验证 h − h^- h−是h*1的矩阵,这一次的输入是 i * 1的矩阵,输出必须也是h*1的矩阵,因此和先验值相乘的权值矩阵 w 1 w_1 w1是h*h,和输入x相乘的权重矩阵 w 2 w_2 w2是h*i,在进行这样的线性变换后,再通过一个激活函数tanh,输出值
h = t a n h ( w 1 h − + b 1 + w 2 x + b 2 ) = t a n h ( [ w 1 w 2 ] [ b 1 b 2 ] ) h = tanh(w_1 h^- + b_1 + w_2x + b_2) = tanh(\left[ \begin{matrix} w_1 & w_2 \end{matrix} \right] \left[ \begin{matrix} b_1 \\ b_2 \end{matrix} \right]) h=tanh(w1h−+b1+w2x+b2)=tanh([w1w2][b1b2])
不断循环这个过程,就可以不断输出下一天的预测值。
定义RNN:
#input_size是输入的维度i,hidden_size是先验值的维度h
cell = torch.nn.RNNCell(input_size = input_size , hidden_size = hidden_size)
使用RNN:
#第一个是输入的向量,第二个是先验值的向量
hidden = cell(inputs,hidden)
注意:由于数据是分batch进行训练的,一个batch中有多条数据,因此实际上程序中输入的维度应该为batch_size * i,h的维度是batch_size * h。要注意这种数据的构造形式,否则程序会发生错误。
处理RNN时,整个序列构造成:
dataset.shape = (seqLen,batchSize,inputSize)
这种形式,第一维是序列长度,第二个是batch长度,最后第三个才是输入的维度。
举例:
import torch
batch_size = 1
seq_len = 3
input_size = 4
hidden_size = 2
cell = torch.nn.RNNCell(input_size = input_size , hidden_size = hidden_size)
dataset = torch.randn(seq_len,batch_size,input_size)
hidden = torch.zeros(batch_size,hidden_size)
#对dataset进行遍历,这样把数据一组组拿出来,从三维变成二维,就能进行RNNCell的运算
for idx,inputs in enumerate(dataset):
print('=' * 20,idx '=' * 20)
print('input.size:',input.shape)
hidden = cell(inputs,hidden)
print('outputs size:',hidden.shape)
print(hidden)
注意里面的hidden是输出值,是不断迭代变化的,输出后进入下一个RNNCell运算。
在实际应用时如果使用RNN类而不是RNNCell类,还需要在后面加一个num_layers参数,代表RNN向上叠的层数,但不建议太多层因为RNN运算非常耗时。在执行
cell = torch.nn.RNN(input_size = input_size , hidden_size = hidden_size,num_layers = num_layers)
out,hidden = cell(inputs,hidden)
的代码时,这时我们不用自己写循环,只需要传入的inputs是三维seqLen*batchSize*inputSize的,输入的hidden是numLayers*batch*input_size,numLayer是RNN的层数,因为有可能RNN有多层,一个RNNCell就需要输入多个h。然后它会自己进行迭代。其中out的输出是h的序列seqLen*batchSize*hidden_size,而hidden输出的是最后一个h,维度是numLayers*batch*input_size。下面是多层RNN的图。
改进的程序:
import torch
batch_size = 1
seq_len = 3
input_size = 4
hidden_size = 2
num_layers = 1
cell = torch.nn.RNN(input_size = input_size , hidden_size = hidden_size,num_layers = num_layers)
inputs = torch.randn(seq_len,batch_size,input_size)
hidden = torch.zeros(num_layers,batch_size,hidden_size)
out,hidden = cell(inputs,hidden)
print('outputs size:',out.shape)
print("Output:",out)
print('Hidden size:',hidden.shape)
print("Hidden",hidden)
RNN类的其他参数,例如batch_first = true,可以设置batch_size在第一个维度,将它和第一个维度进行交换。这样方便在输出时再接一层线性层。这样设置后注意要把输入的维度进行转置以适应改变。
我们的目标是训练一个循环神经网络来适应一个序列变化规律,例如把hello变成ohlol。
首先,把字符进行向量化。在做自然语言处理时要把字符构造成词典,把每一个分配索引。
分配索引后,每一个字母都可以用一个one-hot向量代表,词典中有几个字母这个向量就有几个元素(这里是四个),然后把这个one-hot向量作为输入input。hello有五个字母,输入序列长度是5。因此输出也是一个四维的one-hot向量。这个向量可以接交叉熵来进行分类。
import torch
input_size = 4
hidden_size = 4
batch_size = 1
#准备数据
idx2char = ['e','h','l','o']
x_data = [1,0,2,2,3]
y_data = [3,1,2,3,2]
#查询one-hot向量
one_hot_lookup = [[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,1]]
#根据数据构建one-hot向量组
x_one_hot = [one_hot_lookup[x] for x in x_data]
inputs = torch.Tensor(x_one_hot).view(-1,batch_size,input_size)
#在直接输入的是标签而非one-hot向量时要用LongTensor否则出错
labels = torch.LongTensor(y_data).view(-1,1)
class Model(torch.nn.Module):
def __init__(self,input_size,hidden_size,batch_size):
super(Model, self).__init__()
self.batch_size = batch_size
self.input_size = input_size
self.hidden_size = hidden_size
self.rnncell = torch.nn.RNNCell(input_size=self.input_size,hidden_size = self.hidden_size)
def forward(self, inputs,hidden):
#注意这只做了一个RNNCell,输入注意是batchSize*inputSize,输出batch_size*hiddenSize
hidden = self.rnncell(inputs,hidden)
return hidden
#这个函数用来生成默认的h0
def init_hidden(self):
return torch.zeros(self.batch_size,self.hidden_size)
net = Model(input_size,hidden_size,batch_size)
criterion = torch.nn.CrossEntropyLoss()
#这里用的是改进的随机梯度下降的优化器
optimizer = torch.optim.Adam(net.parameters(),lr = 0.1)
for epoch in range(15):
loss = 0
optimizer.zero_grad()
hidden = net.init_hidden()
print('Predicted string:',end="")
for inputs,label in zip(inputs,labels):
hidden = net(inputs,hidden)
loss += criterion(hidden,label)
#这里用max函数找输出节点中的最大值(即输出矩阵中每一行的最大值dim=1),列是batch_size,此处仅有一行
#返回该值和对应下标的列表,此处列表元素只有一个
_,idx = hidden.max(dim=1)
print(idx2char[idx.item()],end = "")
loss.backward()
optimizer.step()
print(',Epoch [%d/15] loss=%.4f' % (epoch+1,loss.item()))
而不用RNNCell,而是用RNN程序会简介很多:
import torch
input_size = 4
hidden_size = 4
batch_size = 1
#准备数据
idx2char = ['e','h','l','o']
x_data = [1,0,2,2,3]
y_data = [3,1,2,3,2]
#查询one-hot向量
one_hot_lookup = [[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,1]]
#根据数据构建one-hot向量组
x_one_hot = [one_hot_lookup[x] for x in x_data]
inputs = torch.Tensor(x_one_hot).view(-1,batch_size,input_size)
#输出直接用一维的,因为后面已经将维度合并
labels = torch.LongTensor(y_data)
class Model(torch.nn.Module):
def __init__(self,input_size,hidden_size,batch_size,num_layers = 1):
super(Model, self).__init__()
self.num_layers = num_layers
self.batch_size = batch_size
self.input_size = input_size
self.hidden_size = hidden_size
self.rnn = torch.nn.RNN(input_size=self.input_size,hidden_size = self.hidden_size,num_layers=self.num_layers)
def forward(self, inputs):
hidden = torch.zeros(self.num_layers,self.batch_size,self.hidden_size)
#我们要的是整个序列的输出,而不是最后的输出
out,_ = self.rnn(inputs,hidden)
#batch_size=1,因此用二维即可,好处是用交叉熵方便,输出标签直接是一维的即可
return out.view(-1,self.hidden_size)
net = Model(input_size,hidden_size,batch_size)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(),lr = 0.05)
for epoch in range(15):
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backward()
optimizer.step()
_,idx = outputs.max(dim=1)
#变成numpy数组
idx = idx.data.numpy()
#join把列表中的元素练成一个
print( 'Predicted string:',''.join([idx2char[x] for x in idx]),end = "")
print(',Epoch [%d/15] loss=%.4f' % (epoch+1,loss.item()))
One-hot编码的缺点:维度太高,一旦种类过多将很难训练。且向量过于稀疏,只有一个元素是1。而且是硬编码的,哪个字符对应的编码固定,不是学习出来的。
因此我们考虑的改进方向是:低维、稠密、可学习。一个流行的方法是嵌入层(EMBEDDING)。意思是把高维稀疏的样本映射到低维稠密的空间中中,这就是数据降维。
嵌入层可以把数据降维,也可以升维。上图是一个升维的例子,行数是原来的one-hot向量的维度,列数是转换的新的向量的向量的维度,生成这个矩阵W后,只需要进行查询,one-hot向量哪个元素为1把对应行向量取出来即可,设one-hot向量为A,转换之后向量为E,转换公式为:
E = W T A E = W^TA E=WTA
这样即可升维或降维。
进行降维后,模型改变如下:
模型在输入x的上方加入一个嵌入层进行降维,有时候需要在序列中的每一个输出后面加一个线性层,这是因为输出的维度不一定和分类数量一致,我们可以将输出的维度放大一些增强拟合能力,然后用线性层进行转换,变成分类的维度。需要注意的是Embed的输入层必须是LongTensor类型。
embedding的初始化:
其中前两个参数是必须的,构成转换矩阵的高度和宽度。输出会在原来的Tensor的维度上加上一维表示embedding_dim。
线性层可以是任意维度,输出的维度和输入维度一致,除了最后一个维度之外输出和输入每个维度的元素个数一致。交叉熵的计算同理。
模型程序:
import torch
num_class = 4
input_size = 4
hidden_size = 8
embedding_size = 10
#这里用了两层RNN
num_layers = 2
batch_size = 1
seq_len = 5
#准备数据
idx2char = ['e','h','l','o']
#注意x需要变成二维即(batch,seq_len)
x_data = [[1,0,2,2,3]]
#y是一维的即 batch*seq_len
y_data = [3,1,2,3,2]
#embedding输入长整形,直接是下标的Tensor即可,自动转换成one-hot
inputs = torch.LongTensor(x_data)
labels = torch.LongTensor(y_data)
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.emb = torch.nn.Embedding(input_size,embedding_size)
#这里既然把batch放在第一个,后面就需要按照这个顺序排列
self.rnn = torch.nn.RNN(input_size = embedding_size, hidden_size = hidden_size,num_layers = num_layers,batch_first=True)
self.fc = torch.nn.Linear(hidden_size,num_class)
def forward(self, x):
hidden = torch.zeros(num_layers,x.size(0),hidden_size)
x = self.emb(x)
x,_ = self.rnn(x,hidden)
x = self.fc(x)
return x.view(-1,num_class)
net = Model()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(),lr = 0.05)
for epoch in range(15):
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backward()
optimizer.step()
_,idx = outputs.max(dim=1)
#变成numpy数组
idx = idx.data.numpy()
#join把列表中的元素练成一个
print( 'Predicted string:',''.join([idx2char[x] for x in idx]),end = "")
print(',Epoch [%d/15] loss=%.4f' % (epoch+1,loss.item()))
因为输入的维数较低,所以可以从四维升到十维,这个嵌入层可以调节的,避免了硬编码,梯度下降可以寻找适合的嵌入层参数。输入嵌入层的数据是二维batch_size * seq_len的,输出是三维的 batch_size * seq_len * embedding ,这是由于x中的每一个数都变成了一个10维向量。batch_first=true的变化是inputs和outputs都把batch_size放在第一个维度。注意RNN的序列输出是三维的,model中直接强制变成两位,是因为labels只有一维,只有一个样本,这样就可以做交叉熵,如果有多个batch,则需要labels有二维,输出outputs三维。
LSTM是RNN的变种,该算法运算复杂复杂度高,运算时间长,但效果比RNN好得多。由于独特的设计结构,LSTM适合于处理和预测时间序列中间隔和延迟非常长的重要事件。
https://www.jianshu.com/p/4b4701beba92
GRU是一种基于LSTM和RNN之间的算法,是折中的方案。是LSTM网络的一种效果很好的变体,它较LSTM网络的结构更加简单,而且效果也很好,因此也是当前非常流形的一种网络。GRU既然是LSTM的变体,因此也是可以解决RNN网络中的长依赖问题。
https://zhuanlan.zhihu.com/p/32481747
现在我们需要做一个名字分类,通过人的名字来分辨具体的国别。
网络的结构可以变成如上图所示,我们只需要在最后输出一个国别的分类即可,中间的RNNCell的输出不做要求也无法做要求。这就是处理自然语言的一个方法。
中间我们使用更为优秀的GRU来代替传统的RNN。另外每个人的名字也是长度不一的,我们要根据不同长度进行不同处理。首先是分隔字符,我们要将名字分割成一个个字符做成列表,然后制作词典,我们可以用ASCII码表来作为它的词典,这个词典共128个字符,然后查找每个字符对应的ASCII值来拼成对应的Tensor。刚好Embedding层输入需要的是LongTensor而不是one-hot向量,所以这个可以直接作为输入。然后由于每个人们字符串长度不一,我们统一把它们用零填充成最长字符的长度,这样就可以统一处理了。然后再把分类的国家做成一个词典。
什么是双向循环神经网络
在以往RNN中,后面hidden的输出只包含前面输入的信息,而前面的输出不包含后面的信息。但是在自然语言处理中我们也要考虑后面输入的信息,因为后面将要输入的信息也会对前面造成影响,因此我们需要反向再做一次RNN,然后把它们拼接在一起,拼成一个Tensor。这就是双向循环神经网络。
因此双向循环神经网络输出是最上面的序列,Tensor长度是原来hidden的两倍,而hidden的输出是 [ h N f , h N b ] [h_N^f,h_N^b] [hNf,hNb]。同时出示的hidden也要是这个形式和长度。
数据送入GRU是做的优化
为了提高运行效率,后面填充的0参数是没必要参与运算的,原理是0 embedding转换成的向量都是一致的,我们把原来的输入的名字按照长度从大到小排列经过embedding以后,把0的列去掉,然后打包成一个平面,再保存一个关于每一个名字的长度信息即可。这样保存的信息大大减少,以后根据这个长度信息把数据重新拿出来即可。
import gzip
import csv
import torch
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torch.nn.utils.rnn import pack_padded_sequence
import time
import math
HIDDEN_SIZE = 100
BATCH_SIZE = 256
N_LAYER = 2
N_EPOCHS = 100
N_CHARS = 128
USE_GPU = True
# 数据处理,处理后是未经数据化的和排序的人名,经过数据化按人名对应顺序的的国家
class NameDataset(Dataset):
def __init__(self, is_train_set=True):
filename = 'names_train.csv.gz' if is_train_set else 'names_test.csv.gz'
# 读zip文件的方法
with gzip.open(filename, 'rt') as f:
# csv文件的读取的方法
reader = csv.reader(f)
# csv文件读成一个列表
rows = list(reader)
self.names = [row[0] for row in rows]
self.len = len(self.names)
self.countries = [row[1] for row in rows]
# set把列表取除重复的元素,sorted进行排序,赋给新的变量
self.country_list = list(sorted(set(self.countries)))
# 把上面的列表转换成词典,__getitem__提供索引访问
self.country_dict = self.getCountryDict()
self.country_num = len(self.country_list)
def __getitem__(self, index):
# 注意这里是怎么把国家转换成对应索引的
return self.names[index], self.country_dict[self.countries[index]]
def __len__(self):
return self.len
#建立一个国家的字典,国家名和数字标签对应上
def getCountryDict(self):
# 建立空字典
country_dict = dict()
# enumerate() 函数用于将一个可遍历的数据对象组合为一个索引序列,同时返回数据下标和数据
for idx, country_name in enumerate(self.country_list, 0):
country_dict[country_name] = idx
return country_dict
# 通过国家名返回一个索引
def idx2country(self, index):
return self.country_list[index]
# 返回国家个数
def getCountriesNum(self):
return self.country_num
trainset = NameDataset(is_train_set=True)
trainloader = DataLoader(trainset,batch_size=BATCH_SIZE, shuffle=True)
testset = NameDataset(is_train_set=False)
testloader = DataLoader(testset,batch_size=BATCH_SIZE, shuffle=False)
N_COUNTRY = trainset.getCountriesNum()
# 输入未经数据化的的字母名字Tensor和国家索引Tensor,输出经过转换、填充、排序处理的名字Tensor,名字长度Tensor,国家索引Tensor
def make_tensors(names, countries):
# 由一个个元组组成的列表
sequences_and_lengths = [name2list(name) for name in names]
# 拿出列表中每个元组的第一个元素,即名字ASCII码列表组成新的列表
name_sequences = [sl[0] for sl in sequences_and_lengths]
# 拿出列表中每个元组的第二个元素,即名字长度组成新的列表,注意是LongTensor
seq_lengths = torch.LongTensor([sl[1] for sl in sequences_and_lengths])
# 转换成LongTensor
countries = countries.long()
# 用0填充名字到长度一致
seq_tensor = torch.zeros(len(name_sequences), seq_lengths.max()).long()
for idx, (seq, seq_len) in enumerate(zip(name_sequences, seq_lengths), 0):
seq_tensor[idx, :seq_len] = torch.LongTensor(seq)
# 倒序排序,返回排序好的Tensor以及对应的排完序的索引
seq_lengths, perm_idx = seq_lengths.sort(dim=0, descending=True)
# 用索引对原来的名字和国家序列进行排序,排完序后名字长度就是倒序的了
seq_tensor = seq_tensor[perm_idx]
countries = countries[perm_idx]
# seq_lengths需要放在CPU上否则报错。。
return create_tensor(seq_tensor), seq_lengths, create_tensor(countries)
# 如果使用GPU就把tensor放到GPU上
def create_tensor(tensor):
if USE_GPU:
device = torch.device("cuda:0")
tensor = tensor.to(device)
return tensor
# 把名字转换为ASCII值的列表,返回列表和列表长度的元组
def name2list(name):
arr = [ord(c) for c in name]
return arr, len(arr)
class RNNClassifier(torch.nn.Module):
# bidrectional选择循环神经网络单向还是双向
def __init__(self, input_size, hidden_size, output_size, n_layers=1, bidirectional=True):
super(RNNClassifier, self).__init__()
self.hidden_size = hidden_size
self.n_layers = n_layers
self.n_directions = 2 if bidirectional else 1
self.embedding = torch.nn.Embedding(input_size, hidden_size)
self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers, bidirectional=bidirectional)
self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size)
def _init_hidden(self, batch_size):
hidden = torch.zeros(self.n_layers * self.n_directions,batch_size,self.hidden_size)
return create_tensor(hidden)
# 这里多了一个序列的长度的参数
def forward(self, inputs, seq_lengths):
# 做矩阵的转置,把batch_size * seq_len变成seqLen * batch_size
inputs = inputs.t()
batch_size = inputs.size(1)
hidden = self._init_hidden(batch_size)
embedding = self.embedding(inputs)
# 为了提高运行效率,所做的优化,这是RNN,LSTM,GRU都能接受的输入
gru_input = pack_padded_sequence(embedding, seq_lengths)
# 这里要的输出是hidden
output, hidden = self.gru(gru_input, hidden)
# 如果是双向的,那么就需要把两个hidden连起来作为输出
if self.n_directions == 2:
hidden_cat = torch.cat([hidden[-1], hidden[-2]], dim=1)
else:
hidden_cat = hidden[-1]
fc_output = self.fc(hidden_cat)
return fc_output
def trainModel():
total_loss = 0
for i, (names, countries) in enumerate(trainloader, 1):
inputs, seq_lengths, target = make_tensors(names, countries)
output = classifier(inputs, seq_lengths)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
if i % 10 == 0:
print(f'[{
time_since(start)}] Epoch {
epoch} ', end='')
print(f'[{
i * len(inputs)}/{
len(trainset)}]', end='')
print(f'loss = {
total_loss / (i * len(inputs))}')
return total_loss
def testModel():
correct = 0
total = len(testset)
print("evaluating trained model ...")
# 不求梯度
with torch.no_grad():
for i, (names, countries) in enumerate(testloader, 1):
inputs, seq_lengths, target = make_tensors(names, countries)
output = classifier(inputs, seq_lengths)
# 输出最大值节点的索引是分类国家的索引,keepdim保持原有的维度不变
# 最后有个[1]是因为返回值实际上有两个tansor,第一个存储是值,第二个存储索引,我们只需要索引
pred = output.max(dim=1, keepdim=True)[1]
# view_as把Tensor转变成对应Tensor的形式
# eq看两个Tensor对应的位置是否相当,返回相同形式的Tensor,对应位置相同为True,不相同为FALSE
correct += pred.eq(target.view_as(pred)).sum().item()
percent = '%.2f' % (100 * correct / total)
print(f'Test set: Accuracy {
correct}/{
total} {
percent}%')
return correct / total
# 计算训练时长
def time_since(since):
s = time.time() - since
m = math.floor(s / 60)
s -= m * 60
return '%dm %ds' % (m, s)
if __name__ == '__main__':
# N_CHARS是输入字符的长度,N_CHARS是GRU输出隐藏的维度,N_COUNTRY是具体国家的分类数,N_LAYER是GRU的层数
classifier = RNNClassifier(N_CHARS, HIDDEN_SIZE, N_COUNTRY, N_LAYER)
if USE_GPU:
device = torch.device("cuda:0")
classifier.to(device)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(classifier.parameters(), lr=0.001)
# 记录开始训练的时间
start = time.time()
print("Train for %d epochs..." % N_EPOCHS)
acc_list = []
for epoch in range(1, N_EPOCHS + 1):
trainModel()
acc = testModel()
# 把准确率记录到列表中
acc_list.append(acc)
如果需要绘制Loss变化曲线,需要加上以下代码:
import matplotlib.pyplot as plt
import numpy as np
# arange第一个参数是起点,第二个是重点,第三个是步长
epoch = np.arange(1,len(acc_list) + 1,1)
acc_list = np.array(acc_list)
plt.plot(epoch,acc_list)
plt.xlabel("Epoch")
plt.ylabel('Accuracy')
plt.grid()
plt.show()
注意:模型的输入为(seqLen,batchSize),Embedding的输出为(seqLen,batchSize,hiddenSize)同时也是GRU的输入,GRU的输入输出都和原始RNN相同。
如果训练准确度是最高的,我们需要保存模型,可以用
#保存
torch.save(model,PATH)
#载入
model = torch.load(PATH)
Kaggle上一个数据集:https://www.kaggle.com/c/setiment-analysis-on-movie-reviews/data
这个数据集的训练任务是根据电影评论的文本判断用户对电影的态度。
当掌握了RNN后,我们可以用RNN做很多不同的语言模型,例如作诗的神经网络,只需要输入第一个字,就能自动做出一首诗。
我们首先先做一个关于汉字的词典,还要加一个休止符,这个符号代表诗已经作完了。首先需要大量的诗句进行训练,前面的字作为RNNCell输入,输出必须是下一个字,然后把下一个字再送进RNNCell中反复循环,以此类推。因此,只要有足够的数据,我们就能做各种文本的生成器。
至此深度学习图像识别的基础知识基本讲解完毕,本文是从实践的角度进行讲解。