最后本地测试的图片来源于百度及这位大佬,他写了这份作业的Tensorflow版https://blog.csdn.net/u013733326/article/details/79971488
tensorflow看起来有点复杂,所以决定入坑pytorch,用了几天觉得pytorch挺香的。如果你知道张量是怎么回事可以跳过前面,直接去看一个神经网络怎么实现。
资料下载
工程文件的【下载地址】,提取码:v1wz
前提
本代码基于pytorch1.4.0版本实现【pytorch官网下载地址】【清华镜像下载方法】
Tensor(张量)是pytorch运算的一种基本数据类型,它与ndarry在numpy中的作用类似,不同的是,它可以使用GPU进行加速,torch提供了这两者自由转换的接口。这个代码没有涉及GPU加速的内容。
1- 导入pytorch库
import numpy as np
from matplotlib import pyplot as plt
from tf_utils import load_dataset
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
# 测试本地图片时使用
import imageio
import cv2
固定全局随机种子
# 固定numpy随机种子
np.random.seed(1)
# 固定torch随机种子
torch.manual_seed(1)
torch有自己独立的随机种子,试过同样用seed1,它和numpy生成的随机数不同。
计算一个平方损失
可以看看pytorch是怎么计算下面这个损失的:
l
o
s
s
=
L
(
y
^
,
y
)
=
(
y
^
(
i
)
−
y
(
i
)
)
2
(1)
loss = \mathcal{L}(\hat{y}, y) = (\hat y^{(i)} - y^{(i)})^2 \tag{1}
loss=L(y^,y)=(y^(i)−y(i))2(1)
y_hat = torch.tensor(36) # torch.Tensor是torch.FloatTensor的简称,可用.int()或.to(int)强制转换为整型
y = torch.tensor(39) # 而torch.tensor根据传入数据的类型推断Tensor的数据类型
loss = (y - y_hat)**2
# 张量转换成np数组
loss = loss.numpy()
print(loss)
执行结果:
9
一个坑:最好给torch.Tensor()传入至少一维的数据,如torch.tensor([36]),否则会出现意想不到的结果,而torch.tensor()没有这个限制。
计算两个张量的对应元素相乘
a = torch.tensor(2)
b = torch.tensor(10)
# 对应元素相乘,使用.mul()或 * ,有broadcast机制
c = torch.mul(a, b).numpy()
print('c=', c)
执行结果:
c = 20
对比:pytorch的元素乘法使用torch.mul()或 * ,numpy使用np.multiply()或 * ,都有广播机制。
拓展:计算两个一维张量的内积 / 计算矩阵乘法
# 张量内积
d = torch.tensor([2, 2])
e = torch.tensor([1, 4])
print('张量内积:', torch.dot(d, e).numpy())
# 矩阵乘法
f = torch.tensor([[1], [4]])
print('矩阵乘积:', torch.matmul(d, f).numpy())
执行结果:
张量内积: 10
矩阵乘积: [10]
对比:np.dot()可以用于矩阵运算,但torch.dot()只能用于一维张量计算内积(用于二维运算则报错),torch中与np.dot()作用相对的其实是torch.matmul()。
1.1 - 线性函数
试试用pytorch实现一个线性函数——𝑌=𝑊𝑋+𝑏 , 𝑊和𝑋是随机矩阵,b是一个随机向量。其中,W的维度是(4,3),X的维度是(3,1),b的维度是(4,1)。
def linear_function():
# X, Y, b都初始化为标准正态分布随机数组
X = torch.from_numpy(np.random.randn(3, 1)) # 由于torch和numpy库的随机种子不同,用np取随机数再转为张量
W = torch.from_numpy(np.random.randn(4, 3)) # 如果用torch,则 torch.randn(4, 3)
b = torch.from_numpy(np.random.randn(4, 1))
Y = torch.add(torch.matmul(W, X), b)
return Y.numpy()
print(linear_function())
执行结果:
[[-2.15657382]
[ 2.95891446]
[-1.08926781]
[-0.84538042]]
1.2 - sigmoid函数
同上,实现一个sigmoid函数试试吧:
def sigmoid(z):
z = torch.tensor(z, dtype=torch.float32) # torch.sigmoid不接受长整型
sig = torch.sigmoid(z)
return sig
print('sigmoid(0)= ', str(sigmoid(0).numpy()))
print('sigmoid(12)= ', str(sigmoid(12).numpy()))
执行结果:
sigmoid(0) = 0.5
sigmoid(12) = 0.9999938
小数位比较长,问题不大。
1.3 - 二值交叉熵
我们可以使用一个内置函数计算二值交叉熵成本,而不需要编写代码来计算这个的函数:
J
=
−
1
m
∑
i
=
1
m
(
y
(
i
)
log
a
[
2
]
(
i
)
+
(
1
−
y
(
i
)
)
log
(
1
−
a
[
2
]
(
i
)
)
)
(2)
J = - \frac{1}{m} \sum_{i = 1}^m \large ( \small y^{(i)} \log a^{ [2] (i)} + (1-y^{(i)})\log (1-a^{ [2] (i)} )\large )\small\tag{2}
J=−m1i=1∑m(y(i)loga[2](i)+(1−y(i))log(1−a[2](i)))(2)
def cost(logits, labels):
# 为了和课堂代码一致,再计算一层sigmoid
input = sigmoid(logits.numpy())
# 计算成本
# BCELoss是基于sigmoid的函数,reduction默认='mean',返回成本均值;reduction='none',则不进行取均值的计算,返回每个样本的成本
loss = nn.BCELoss(reduction='none')
cost = loss(input, labels)
return cost
logits = sigmoid(np.array([0.2, 0.4, 0.7, 0.9]))
labels = torch.Tensor([0, 0, 1, 1])
cost = cost(logits, labels)
print('cost=', cost.numpy())
执行结果:
cost= [1.0053871 1.0366408 0.41385436 0.39956608]
Andrew用tensorflow计算的答案:
有末位差别,可能是框架保留小数的机制不同导致的
1.4 - 独热编码
在torch里似乎不是太用得到,因为计算多分类交叉熵时,torch内置的成本函数会自动将y转换成独热编码,不需要我们手动转换,在后面搭神经网络时可以看到。这里为了完整性还是写了一下。
# 独热编码 torch在计算交叉熵时会自动将label转独热编码
# (不过计算MSELoss时需要手动转换)
def one_hot(label, C):
"""
:param label: 张量,y标签
:param C: int,类别数
:return: 独热矩阵,与y标签数组对应
"""
# 这里的y是一维,只有一个维度,=m
m = labels.shape[0]
# 将独热矩阵初始化为全0,torch.zeros与np.zeros类似,torch建立的是tensor类型,而np建立的是ndarry类型
one_hot_matrix = torch.zeros(size=(C, m))
# 每列为一个样本,历遍列 比如该样本的标签为0,则在第一行标1,其他行标0
for l in range(m):
one_hot_matrix[label[l]][l] = 1
return one_hot_matrix.numpy()
# 测试
labels = np.array([1,2,3,0,2,1])
one_hot = one_hot(labels, C=4)
print('one_hot = ' + str(one_hot))
执行结果:
one_hot = [[0. 0. 0. 1. 0. 0.]
[1. 0. 0. 0. 0. 1.]
[0. 1. 0. 0. 1. 0.]
[0. 0. 1. 0. 0. 0.]]
1.5 - 用数字1来初始化
torch和numpy在创建全0、全1矩阵上的思路是类似的,简单地传入一个形状参数即可,不同的是,np创建的是ndarry类型,torch创建的是tensor类型。
def ones(shape):
ones_ = torch.ones(shape)
return ones_.numpy()
print('ones = ' + str(ones([3])))
一个坑: 在torch中尽量不要让变量和创建的方法用相同的命名,否则可能会出一些意想不到的错误。
执行结果:
ones = [1. 1. 1.]
2 - 使用pytorch构建第一个神经网络
(机翻英语:
一天下午,我们和一些朋友决定教我们的电脑破译手语。我们花了几个小时在白色的墙壁前拍照,于是就有了了以下数据集。现在,你的任务是建立一个算法,使有语音障碍的人与不懂手语的人交流。一天下午,我们和一些朋友决定教我们的电脑破译手语。我们花了几个小时在一堵白墙前拍照,得到了以下数据集。现在你的工作是建立一种算法,帮助语言障碍人士和不懂手语的人进行交流。
训练集:1080张图片(64×64像素),这些符号表示0到5之间的数字(每个数字180张图片)。
测试集:120张图片(64×64像素),这些符号表示0到5之间的数字(每个数字20张图片)。
注意,这是sign数据集的一个子集。完整的数据集包含更多的符号。
下面是每个数字的例子。)
2.1 - 加载数据集
一点说明,torch的数据接口torch.utils.data.DataLoader是使用mini-batch优化方法时一个非常有用的工具,因为后面使用Adam优化器,所以会用到它。它要求传入的x_tensor,y_tensor的第一维度(行)相同,否则报错。由于这个原因,这里预处理后数据集是行样本形式,而不是Tensorflow版的列样本形式。
# 载入数据集
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()
# 可视化一张图片
index = 0
plt.imshow(X_train_orig[index])
plt.show()
print('y = ', np.squeeze(Y_train_orig[:, index]))
# 展平数据集
X_train_flatten = X_train_orig.reshape(X_train_orig.shape[0], -1)
X_test_flatten = X_test_orig.reshape(X_test_orig.shape[0], -1)
# 归一化数据集
X_train = X_train_flatten/255
X_test = X_test_flatten/255
# 转置y
Y_train = Y_train_orig.T
Y_test = Y_test_orig.T
print('number of training examples = ' + str(X_train.shape[1]))
print('number of test examples = ' + str(X_test.shape[1]))
print('X_train shape: ' + str(X_train.shape))
print('Y_train shape: ' + str(Y_train.shape))
print('X_test shape: ' + str(X_test.shape))
print('Y_test shape: ' + str(Y_test.shape))
一张图片可视化的结果:
y = 5
说明这是一个数字5。
维度打印结果:
number of training examples = 12288
number of test examples = 12288
X_train shape: (1080, 12288)
Y_train shape: (1080, 1)
X_test shape: (120, 12288)
Y_test shape: (120, 1)
注意12288来自64×64×3。每个图像是正方形的,64×64像素,3是RGB颜色。这样就保证x,y的行数相同,传入数据接口就不会有问题了。
2.2 - 创建网络结构
这个网络的前向传播的方式为:LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX。在torch中搭建神经网络,只需将前向传播部分放在网络结构中。这里用的是快速搭建法,更一般地会用到module类。
# 创建网络结构
def nerual_net():
# 这里最后一层用的是LogSoftmax,它是Softmax取log的结果
# torch的Softmax->log->NLLLose(接下来计算成本的函数) == tf.nn.softmax_cross_entropy_with_logits == torch.nn.CrossEntropyLoss
net = nn.Sequential(
nn.Linear(12288, 25),
nn.ReLU(),
nn.Linear(25, 12),
nn.ReLU(),
nn.Linear(12, 6),
nn.LogSoftmax(dim=1)
)
return net
# 看一下创建的网络结构
net = nerual_net()
print(net)
执行结果:
Sequential(
(0): Linear(in_features=12288, out_features=25, bias=True)
(1): ReLU()
(2): Linear(in_features=25, out_features=12, bias=True)
(3): ReLU()
(4): Linear(in_features=12, out_features=6, bias=True)
(5): LogSoftmax()
)
0-5表示层索引,如果用module搭和这个相同的网络,这里print出来不同,层会以自己的函数命名,比如ReLU层的索引是ReLU。
2.3 - 初始化参数
这个函数对权重矩阵Wi使用Xaiver初始化,对偏置向量bi使用0初始化。(后面建立模型时未实际用到这个函数,因为未找到的原因,它后期收敛速度不及默认的初始化方法,测试准度63%左右,比Tensorflow版低8%,而默认初始化方法的测试准度达到87.5%)
def initialize_params(net):
for i in range(len(net)):
layer = net[i]
if isinstance(layer, nn.Linear):
# 对Wi使用Xaiver初始化
nn.init.xavier_uniform_(layer.weight, gain=nn.init.calculate_gain('relu'))
# 对bi使用0初始化
nn.init.constant_(layer.bias, 0)
print('W' + str(i//2+1) + ' = ' + str(layer.weight.dtype) + ' ' + str(layer.weight.shape))
print('b' + str(i//2+1) + ' = ' + str(layer.bias.dtype) + ' ' + str(layer.bias.shape))
return net
# 测试初始化参数
initialize_params(net)
执行结果:
W1 = torch.float32 torch.Size([25, 12288])
b1 = torch.float32 torch.Size([25])
W2 = torch.float32 torch.Size([12, 25])
b2 = torch.float32 torch.Size([12])
W3 = torch.float32 torch.Size([6, 12])
b3 = torch.float32 torch.Size([6])
2.4 - 创建数据接口
这个函数将X_train, Y_train打包,可以理解为打包成类似于小批量迭代器的形式,每历遍所有批次再进行下一轮迭代,它都会自动打乱数据集,使每个批次内的样本保持随机。
def data_loader(X_train, Y_train, batch_size=32):
train_db = TensorDataset(torch.from_numpy(X_train).float(), torch.squeeze(torch.from_numpy(Y_train)))
# shuffle=True,则每次历遍批量后重新打乱顺序
train_loader = DataLoader(train_db, batch_size=batch_size, shuffle=True)
return train_loader
在封装的模型中调用这个函数
2.5 - 封装模型
模型运行大概需要5-8分钟。
# 封装模型
def model(X_train, Y_train, X_test, Y_test, lr=0.0001, epochs=1500, batch_size=32, print_cost=True, is_plot=True):
# 载入数据
train_loader = data_loader(X_train, Y_train, batch_size)
# 创建网络结构
net = nerual_net()
# 指定成本函数
cost_func = nn.NLLLoss()
# 指定优化器为Adam
optimizer = torch.optim.Adam(net.parameters(), lr=lr, betas=(0.9, 0.999))
# 保存每次迭代cost的列表
costs = []
# 批次数量
m = X_train.shape[0]
num_batch = m / batch_size
# 参数初始化 torch有默认的初始化方法,这里用Xaiver初始化效果不如默认,也不如tensorflow的示例代码,未找到原因,因此没用Xaiver初始化
# 想实验Xaiver初始化,取消注释下一行即可
# net = initialize_params(net)
# 迭代
for epoch in range(epochs):
# 历遍批次
epoch_cost = 0
for step, (batch_x, batch_y) in enumerate(train_loader):
# 前向传播
output = net(batch_x)
# 计算成本
cost = cost_func(output, torch.squeeze(batch_y))
epoch_cost += cost.data.numpy() / num_batch
# 梯度归零 backward()会在每次调用时累加梯度,不清零会干扰下一个批次计算梯度
optimizer.zero_grad()
# 反向传播
cost.backward()
# 更新参数
optimizer.step()
if print_cost and epoch % 5 == 0:
costs.append(epoch_cost)
if epoch % 100 == 0:
print('Cost after epoch %i : %f' % (epoch, epoch_cost))
# 画学习曲线
if is_plot:
plt.plot(costs)
plt.xlabel('iterations per 5')
plt.ylabel('cost')
plt.show()
# 保存学习后的参数
torch.save(net.state_dict(), 'net_params.pkl')
print('参数已保存到本地pkl文件。')
# # 计算训练集预测的结果
net.load_state_dict(torch.load('net_params.pkl'))
output_train = net(torch.from_numpy(X_train).float())
pred_Y_train = torch.max(output_train, dim=1)[1].data.numpy()
# 计算测试集预测的结果
output_test = net(torch.from_numpy(X_test).float())
pred_Y_test = torch.max(output_test, dim=1)[1].data.numpy()
# 训练集准确率
print('Train Accuracy: %.2f %%' % float(np.sum(np.squeeze(Y_train) == pred_Y_train)/m*100))
# 测试集准确率
print('Test Accuracy: %.2f %%' % float(np.sum(np.squeeze(Y_test) == pred_Y_test)/X_test.shape[0]*100))
return net
执行结果:
Cost after epoch 0 : 1.813178
Cost after epoch 100 : 0.888610
Cost after epoch 200 : 0.574326
Cost after epoch 300 : 0.428579
Cost after epoch 400 : 0.302992
Cost after epoch 500 : 0.207265
Cost after epoch 600 : 0.141452
Cost after epoch 700 : 0.105820
Cost after epoch 800 : 0.053666
Cost after epoch 900 : 0.033947
Cost after epoch 1000 : 0.020257
Cost after epoch 1100 : 0.011837
Cost after epoch 1200 : 0.006753
Cost after epoch 1300 : 0.003172
Cost after epoch 1400 : 0.001771
参数已保存到本地pkl文件。
Train Accuracy: 100.00 %
Test Accuracy: 87.50 %
使用torch默认的初始化方法,训练准确率达到100%,测试准确率达到87.5%,挺不错的结果。如果用Xaiver初始化则收敛得比较慢,原因还没找到,也许以后会回来补充。
(机翻英语:
你的模型看起来足够大,可以很好地适应训练集。然而,考虑到训练和测试准确性之间的差异,您可以尝试添加L2或dropout正则化来减少过拟合。)
2.6 - 测试本地的图片(选做)
用了大佬的自拍的手势图片和不知名的百度图片来做这个测试,出处见文章最开头。
def test_local_picture(img_path, y, trained_net, num_px=64):
# 读取一张图片
image = np.array(imageio.imread(img_path))
# 改变图像至指定尺寸。只保留resize()输出的前3列,前3列为rgb通道数值;第4列为固定值255,去掉
image_cut = cv2.resize(image, dsize=(num_px, num_px))[:, :, :3].reshape(1, -1)
# 归一化图像数据
x_test = image_cut/255
# 预测分类
y_test = np.array(y).reshape(1, 1)
output = trained_net(torch.from_numpy(x_test).float())
y_pred = torch.max(output, dim=1)[1].data.numpy()
y_pred = int(np.squeeze(y_pred))
# 打印结果
print('y = %i, predicted % i. % s' % (y, y_pred, ('Correct.' if y == y_pred else 'Wrong.')))
# 测试不同的数字
trained_net = nerual_net()
trained_net.load_state_dict(torch.load('net_params.pkl'))
IMG_PATH = 'datasets/test_images/%i.png'
for i in range(6):
img_path = IMG_PATH % i
test_local_picture(img_path, i, trained_net)
执行结果:
y = 0, predicted 0. Correct.
y = 1, predicted 1. Correct.
y = 2, predicted 2. Correct.
y = 3, predicted 2. Wrong.
y = 4, predicted 2. Wrong.
y = 5, predicted 5. Correct.
6中4,看起来还有很大的优化空间,3和4的手势图都被模型判断为数字2了
只要有numpy做基础,pytorch还是挺好理解的。希望这篇东西对你也有帮助。另外,推荐B站莫烦的pytorch教学视频,非常详细。