pytorch入门:吴恩达Course 2-改善深层神经网络-week3作业 pytorch版

最后本地测试的图片来源于百度及这位大佬,他写了这份作业的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=1m(y(i)loga[2](i)+(1y(i))log(1a[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教学视频,非常详细。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值