06.深度学习

发布于 26 天前  85 次阅读


目录

一、PyTorch

(一)张量的创建

1.概念

  • 在PyTorch中,所有数据被封装为张量参与运算
  • 张量是指一个同类型数据的多维矩阵,默认类型为float32
  • 在PyTorch中对张量的运算是通过类实现的,类中封装了对张量进行运算的一些函数

2.基本创建

  • 通过tensor函数进行创建
    • 适用于已有具体数据,根据数据创建张量
        #1.创建标量
        data = torch.tensor(10)
        print(data)
        #2.使用NumPy数组创建
        data = np.random.randn(2,3)
        data = torch.tensor(data)
        print(data)
        #3.使用list创建
        data = [[10.,20.,30.],[10.,20.,30.]]
        data = torch.tensor(data)
        print(data)
  • 通过Tensor类进行创建
    • 适用于根据形状创建张量,也可以用于指定数据创建张量
        #1.根据形状创建张量
        data = torch.Tensor(2,3)
        print(data)
        #2.根据数据创建张量
        data = torch.Tensor([2,3])
        print(data)
  • 通过具体类别函数创建
    • 适用于创建指定类型的张量
        data = torch.IntTensor(2,3)#默认int32
        print(data)
        """
        ShortTensor:int16
        LongTensor:int64
        FloatTensor:float32
        """
    
        #若传入数据类型不匹配,会发生类型转换

3.创建线性张量

  • 指定步长
      #1.指定步长
      """
      torch.arange()
      :param1:开始值
      :param2:结束值
      :param3:步长
      :return:张量
      """
      data = torch.arange(0,10,3)
      print(data)
  • 指定区间指定元素个数
      #2.在指定区间中指定元素个数
      """
      torch.linspace()
      :param1:开始值
      :param2:结束值
      :param3:元素个数
      :return:张量
      """
      data = torch.linspace(0,10,5)
      print(data)

4.创建随机张量

  • 随机创建
      #1.创建随机张量
      """
      torch.randn()
      :param1:行数
      :param2:列数
      :return:张量
      """
      data = torch.randn(2,3)
      print(data)
      #2.固定随机数种子
      torch.random.manual_seed(0)#固定随机数种子为0
    
      #3.int类型等其他类型示例
      """
      torch.randint()
      :param1:起始点
      :param2:结束点
      :param3:形状
      :return: 返回int类型的随机张量
      """
      data = torch.randint(0,10,[3,3])

5.创建指定值张量

  • 创建全0张量
    • 指定形状
        #1.指定形状
        """
        torch.zeros()
        :param1:行数
        :param2:列数
        :return:全0张量
        """
        data = torch.zeros(2,3)
        print(data)
    • 模仿样板张量
        #2.根据其他张量形状创建
        """
        torch.zeros_like()
        :param:样板张量
        :return:同形状的全0张量
        """
        data2 = torch.zeros_like(data)
        print(data2)
  • 创建全1张量
    • 指定形状
        #1.指定形状
        """
        torch.ones()
        :param1:行数
        :param2:列数
        :return:全1张量
        """
        data = torch.ones(2,3)
        print(data)
    • 模仿样板张量
        #2.根据其他张量形状创建
        """
        torch.ones_like()
        :param:样板张量
        :return:同形状的全1张量
        """
        data2 = torch.ones_like(data)
        print(data2)
  • 创建指定值的张量
    • 指定形状
        #1.指定值和形状
        """
        torch.full()
        :param1:传入形状信息,使用列表或元组
        :param2:传入指定值
        :return: 指定值和形状的张量
        """
        data = torch.full([2,3],2)
        print(data)
    • 模仿样板张量
        #2.根据其他张量形状创建
        """
        torch.full()
        :param1:传入形状信息,使用样板张量
        :param2:传入指定值
        :return: 指定值和形状的张量
        """
        data2 = torch.full_like(data,2)
        print(data2)

(二)张量的元素类型转换

1.type函数

        data = torch.zeros(2,3)
        print(data.dtype)#此时类型为float32
        """
        data.type()
        :param1:具体的一个数据类型的Tensor类
        :return:新的指定类型的张量
        """
        data = data.type(torch.DoubleTensor)
        print(data.dtype)

2.具体类型函数

        data = torch.zeros(2,3)
        print(data.dtype)#此时类型为float32
        """
        data.double()
        :return:double类型的新张量
        如果要转换为其他类型,可以调用相应的具体类型函数
        """
        data = data.double()
        print(data.dtype)

(三)张量的计算

1.张量的基本运算

  • 生成新张量
      #1.生成新张量
      data = torch.randint(0,10,[3,3])
      print(data)
      """
      data.add()
      :param:数值
      :return:将data加上该数值后,返回新张量
      """
      data = data.add(10)
      print(data)
      """
      data.sub():减法
      data.mul():乘法
      data.div():除法
      data.neg():取反
      """
  • 作用于原张量
      #2.作用在原张量
      data = torch.randint(0,10,[3,3])
      print(data)
      """
      data.add_()
      :param:数值
      :return:将data加上该数值后,返回原张量
      """
      data.add_(10)#注意:带了下划线,其他的同理
      print(data)

2.阿达玛积

  • 概念:阿达玛积是指矩阵对应位置的元素相乘
  • 使用mul函数
      #1.使用mul函数
      data1 = torch.tensor([[6,1],[2,3]])
      data2 = torch.tensor([[2,4],[6,8]])
      """
      data.mul()
      :param:指定相乘的第二个矩阵
      :return:两个矩阵的阿达玛积,返回新张量
      """
      data = data1.mul(data2)
      print(data)
  • 使用*运算符
      #2.使用*运算符
      data1 = torch.tensor([[6,1],[2,3]])
      data2 = torch.tensor([[2,4],[6,8]])
      data = data1*data2
      print(data)

3.点积运算

  • 概念:线性代数中的矩阵相乘运算,符合线性代数矩阵相乘的要求
  • 使用@运算符
      #1.使用@运算符
      data1 = torch.tensor([[6,1],[2,3]])
      data2 = torch.tensor([[2,4],[6,8]])
      data = data1@data2
      print(data)
  • 使用mm函数
      #2.使用mm函数
      """
      要求矩阵的维数都是二维(行,列)
      也就是说,没有批次这个维数,只进行两个矩阵的相乘,对比bmm函数
      并不代表限制矩阵的维数
      """
      data1 = torch.tensor([[6,1],[2,3]])
      data2 = torch.tensor([[2,4],[6,8]])
      """
      torch.mm()
      :param1:左侧的矩阵
      :param2;右侧的矩阵
      :return:相乘后的新矩阵
      """
      data = torch.mm(data1,data2)
      print(data)
  • 使用bmm函数
      #3.使用bmm函数
      """
      要求矩阵的维数都是三维(批次,行,列)
      第一个维度:表示批次
      例如批次为n,可以理解为列表,列表中存有n个矩阵
      两个列表中对应位置的矩阵相乘,返回n个结果
      第二个维度:行数
      第三个维度:列数
      """
      data1 = torch.randn(3,4,5)
      data2 = torch.randn(3,5,3)
      """
      torch.bmm()
      :param1:左侧的矩阵
      :param2;右侧的矩阵
      :return:相乘后的新矩阵
      """
      data = torch.bmm(data1,data2)
      print(data)
  • 使用matmul函数
      #4.使用matmul函数
      """
      适用于多种维数
      torch.matmul()
      :param1:左侧矩阵
      :param2:右侧矩阵
      :return:相乘后的新矩阵
      """
      #二维
      data1 = torch.randn(4,5)
      data2 = torch.randn(5,3)
      data = torch.matmul(data1,data2)
      print(data)
      #三维
      data3 = torch.randn(3,4,5)
      data4 = torch.randn(3,5,3)
      data = torch.matmul(data3,data4)
      print(data)
      #维数不同的
      data1 = torch.randn(4, 5)#在运算时会视为3个相同矩阵,批次为3
      data3 = torch.randn(3, 5, 5)
      data = torch.matmul(data1,data3)
      print(data)

4.指定运算设备

  • 注意点
    • 注意要安装对应GPU版本的PyTorch,安装对应的CUDA
    • 默认将张量创建在CPU控制的内存中,简称创建在CPU上
    • 不同设备上的张量不能直接进行运算
  • 使用cuda函数
      # 1.使用CUDA
      data = torch.randint(0,10,[5,5])
      print(data.device)#打印device属性,表示张量存储的设备,此时是CPU
      """
      data.cuda()
      将数据放GPU上
      :return:打印时返回其在CUDA中的编号(cuda:n,n从0开始,表示编号n的显卡)
      """
      data = data.cuda()
      print(data.device)#此时在GPU上
      """
      data.cpu()
      将数据放CPU上
      :return:打印时显示cpu
      """
      data = data.cpu()
      print(data.device)
  • 直接创建在GPU上
          #2.直接在GPU上创建张量
          """
          在创建时传入device参数
          """
          data = torch.randint(0,10,[5,5],device="cuda:0")
          print(data.device)
  • 使用to方法
          #3.使用to方法 
        data = torch.randint(0,10,[5,5])
          print(data.device)#打印device属性,表示张量存储的设备,此时是CPU
          """
          data.to()
          :param:要移动到的设备
          """
          data = data.to("cuda:0")
          print(data.device)#此时在GPU上
          data = data.to("cpu")
          print(data.device)

(四)张量类型转换

1.张量转换为NumPy数组

  • 使用numpy函数
    #1.使用numpy函数
    data = torch.randint(0,10,[2,2])
    """
    data.numpy()
    :return:将data转换为ndarray类型
    注意:转换前后的对象是共享内存的!
    """
    data_numpy = data.numpy()
    print(type(data))
    print(type(data_numpy))
  • 追加copy函数
    #2.使用copy函数
    data2 = torch.randint(0, 10, [2, 2])
    """
    在numpy函数后面加上copy函数,
    使得转换前后的对象不共享内存空间
    """
    data_numpy2 = data2.numpy().copy()
    print(type(data2))
    print(type(data_numpy2))

2.NumPy数组转换为张量

  • 使用from_numpy
      data_numpy = np.array([1,2,3,4])
      """
      torch.from_numpy()
      :param:numpy数组
      :return:张量
      注意:默认是共享内存的,可以使用copy解决
      """
      data_tensor1 = torch.from_numpy(data_numpy)
      print(type(data_tensor1))
      data_tensor2 = torch.from_numpy(data_numpy.copy())#注意copy的位置在括号内
      print(type(data_tensor2))
  • 使用torch.tensor
      data_numpy = np.array([1, 2, 3, 4])
      """
      直接向torch.tensor中传递numpy数组
      默认不共享内存
      """
      date_tensor = torch.tensor(data_numpy)
      print(type(date_tensor))

3.标量张量与数字的转换

  • 提取单个元素的张量
      data1 = torch.tensor(10)
      data2 = torch.tensor([10])
      data3 = torch.tensor([[10]])
      """
      data.item()
      :return:将tensor中的元素转为标量返回
      注意:tensor中只能有一个元素,维度任意
      """
      print(data1.item())
      print(data2.item())
      print(data3.item())

(五)张量拼接操作

1.torch_cat

    data1 = torch.randint(0,10,[3,4,5])
    data2 = torch.randint(0,10,[3,4,5])
    print(data1)
    print(data2)
    """
    torch.cat()
    :param1:要拼接的张量,使用列表
    :dim:拼接的维数,结合不同维数的作用来考虑
    :return:拼接后的张量

    从这个例子上看,
    维数为0:表示个数进行拼接,所有矩阵放到一个列表里
    维数为1:按行拼接,对应矩阵纵向拼接到一起
    维数为2:按列拼接,对应矩阵横向拼接到一起
    """
    # 1.按照0维度进行拼接
    data = torch.cat([data1,data2],dim=0)
    print(data)
    # 2.按照1维度进行拼接
    data = torch.cat([data1,data2],dim=1)
    print(data)
    # 3.按照2维度进行拼接
    data = torch.cat([data1,data2],dim=2)
    print(data)

2.torch.stack

    data1 = torch.randint(0,10,[4,5])
    data2 = torch.randint(0,10,[4,5])
    print(data1)
    print(data2)
    """
    torch.stack()
    :param1:要拼接的张量,使用列表
    :dim:拼接的维数,结合不同维数的作用来考虑
    :return:拼接后的张量

    从这个例子上看,
    维数为0:表示个数进行拼接,所有矩阵放到一个列表里
    维数为1:按行拼接,对应矩阵的对应行按照行拼接在一起
    维数为2:按列拼接,对应矩阵的对应行按照列拼接在一起
    """
    # 1.按照0维度进行拼接
    data = torch.stack([data1,data2],dim=0)
    print(data)
    # 2.按照1维度进行拼接
    data = torch.stack([data1,data2],dim=1)
    print(data)
    # 3.按照2维度进行拼接
    data = torch.stack([data1,data2],dim=2)
    print(data)

(六)张量索引操作

1.行列索引

    data = torch.randint(0,10,[5,5])
    print(data)
    #1.行索引
    """
    data[n]
    n从0开始,表示第n行
    """
    print(data[0])
    #2.列索引
    """
    data[:,n]
    冒号:表示从...到...,省略前面表示从头部开始,省略后面表示到末尾结束
    逗号:逗号前为行,逗号后为列
    """
    print(data[:,0])
    #3.指定行列
    """
    data[:,:]
    灵活使用冒号,可以得到局部数据
    """
    print(data[0:2,0:3])

2.列表索引

def test02():
    data = torch.randint(0,10,[5,5])
    print(data)
    #列表索引
    """
    data([],[])
    :param1:表示行坐标
    :param1:表示列坐标
    :return:组合对应的行列坐标从矩阵中取值,返回一个列表
    注意:行列坐标个数要相等!
    """
    print(data[[0,2,4],[0,2,4]])

3.布尔索引

    data = torch.randint(0,10,[5,5])
    print(data)
    #布尔索引
    """
    data可以与数字进行比较,
    结果是满足条件的位置为True,不满足的位置为False的张量
    然后使用data[条件]进行索引,对应True的位置进行输出
    返回的是一个一维的张量
    """
    print(data[data>3])
    print(data[data[:,3]>3]) #返回所有行中,编号3的列>3的所有行
    print(data[:,data[3]>3])#返回所有列中,编号3的行>3的所有列

4.多维索引

    data = torch.randint(0,10,[3,5,5])
    print(data)
    #多维索引
    """
    data[]
    我们可以认为多维张量是多个张量的列表
    :param1:表示选择第几个tensor
    :param2:行索引
    :param3:列索引
    """
    print(data[0,:,2:3])

(七)张量形状操作

1.查看张量形状

    data = torch.tensor([[1,2,3],[4,5,6]])
    print(data)
    #1.shape
    """
    data.shape[]
    :param:可选,表示指定维数
    :return:张量的形状
    """
    print(data.shape)
    #2.size
    """
    data.size()
    :param:可选,表示指定维数
    :return:张量的形状
    """
    print(data.size())

2.reshape

    data = torch.tensor([[1,2,3],[4,5,6]])
    print(data)
    """
    data.reshape()
    :param1:转换后张量的行数
    :param2:转换后张量的列数
    :return:转换后的张量
    注意:转换前后的张量元素个数要相等!
    技巧:可以对行列中使用-1来让程序自动计算另一个数值,前提是行列有一个已经被指定
    """
    data_new = data.reshape(3,2)
    print(data_new)
    data_new = data.reshape(3,-1)
    print(data_new)

3.transpose

    data = torch.randint(0,10,[3,4,5])
    print(data)
    #transpose
    """
    torch.transpose()
    :param1:要操作的张量
    :param2:要交换的维度1
    :param3:要交换的维度2
    :return:交换维度1和维度2,返回交换后的张量
    缺点:一次只能交换两个维度
    """
    data_new = torch.transpose(data,0,1)
    print(data_new)

4.permute

    data = torch.randint(0,10,[3,4,5])
    print(data)
    """
    torch.permute()
    :param1:要操作的张量
    :param2:维度交换列表,对应位置交换为对应的维数
    :return:交换后的张量
    可以一次交换多个维度
    """
    data_new = torch.permute(data,[2,0,1])
    #表示将原来的维数顺序(0,1,2)交换为(2,0,1)
    print(data_new)

5.view和contiguous

    data = torch.randint(0,10,[4,5])
    print(data)
    """
    data.is_contiguous()
    判断张量是否存储在连续的空间中
    :return:True/False
    """
    print(data.is_contiguous())
    """
    data.view()
    :param1:维度数值
    :param2:维度数值
    :return:交换维度数值后的张量
    注意:view只能用于连续空间存储的张量
    """
    data_new = data.view(5,4)
    print(data_new)

    data_test = torch.transpose(data,1,0)
    #一般来说,经过transpose和permute之后,张量的存储空间不再连续
    print(data_test.is_contiguous())
    #可以使用contiguous()使存储空间连续
    data_new = data_test.contiguous().view(4,5)
    print(data_new)

6.squeeze和unsqueeze

    #1.squeeze
    data = torch.randint(0,10,[1,2,3,4])
    print(data)
    """
    data.squeeze()
    去除维数为1的维度
    :param:可选,指定维度
    :return:去除维度后的张量
    """
    data_new_1 = data.squeeze()
    print(data_new_1)

    #2.unsqueeze
    data = torch.randint(0,10,[3,4])
    print(data)
    """
    data.unsqueeze()
    增加维度,维数为1
    :param:在指定位置添加维度
    :return:添加维度之后的张量
    """
    data_new_2 = data.unsqueeze(0)
    print(data_new_2)

(八)张量运算函数

1.均值

    data = torch.randint(0,10,[2,3],dtype=torch.float64)#生成整数随机数,以float64类型存储
    print(data)
    """
    data.mean()
    :param dim:指定维度,表示按该维度求均值
    :return:将所有元素相加求均值,返回均值,张量类型
    """
    print(data.mean())
    print(data.mean(dim=0))#按行这个维度,表示每一列按行相加求均值
    print(data.mean(dim=1))#按列这个维度,表示每一行按列相加求均值

2.求和

    data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
    print(data)
    """
    data.sum()
    :param dim:指定维度,表示按该维度求和
    :return:将所有元素相加,返回和,张量类型
    """
    print(data.sum())
    print(data.sum(dim=0))#按行这个维度,表示每一列按行相加
    print(data.sum(dim=1))#按列这个维度,表示每一行按列相加

3.次方

    data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
    print(data)
    """
    data.pow()
    :param:将张量的每一个元素进行对应的次方
    :return:平方后的结果,张量类型
    """
    data = data.pow(2)
    print(data)

4.平方根

    data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
    print(data)
    """
    data.sqrt()
    将张量的每一个元素进行开平方根
    :return:平方根后的结果,张量类型
    """
    data = data.sqrt()
    print(data)

5.指数

    data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
    print(data)
    """
    data.exp()
    将张量的每一个元素作为指数计算e^指数的值
    :return:运算后的结果,张量类型
    """
    data = data.exp()
    print(data)

6.对数

    data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
    print(data)
    """
    data.log()
    将张量的每一个元素进行对数运算,默认以e为底
    :return:运算后的结果,张量类型
    """
    data = data.log()
    print(data)
    data = data.log2()#以2为底
    print(data)

(九)梯度运算

1.一元的梯度运算

  • 一元标量梯度运算:类似于一元函数求导,自变量只有一个值
      """
      对于要求导的张量,我们需要设置一些属性:
      requires_gard:True
      dtype:一般设置为浮点数
      """
      x = torch.tensor(10,requires_grad=True,dtype=torch.float64)
      fx = x**2 + 1 #构建计算函数
      #自动微分
      """
      fx.backward()
      对fx进行求导得到导函数
      将x=10代入求值
      """
      fx.backward()
      #访问梯度
      print(x.grad)
  • 一元向量梯度运算:类似于一元函数求导,自变量有多个值
      x = torch.tensor([10,20,30,40],requires_grad=True,dtype=torch.float64)
      fx = x**2 + 1 #此时fx是一个向量
      gx = fx.mean() #此时fx是一个标量
      """
      backward()
      对于向量而言,不能直接使用,需要使用mean或者sum
      mean:求平均损失
      sum:求总损失
      过程:
      1.fx有4个变量,调用mean得到: gx = 1/4 * fx
      2.对gx进行求导,自变量为fx: g'x = 1/4 * f'x
      3.根据复合函数求导规则: g'x = 1/4 * 2x = 1/2 * x
      4.代入每一个x的值即可
      """
      gx.backward()
      print(x.grad)

2.多元的梯度运算

  • 多标量梯度运算:对多元函数求偏导,每个自变量只有一个值
      x1 = torch.tensor(10,requires_grad=True,dtype=torch.float64)
      x2 = torch.tensor(20, requires_grad=True, dtype=torch.float64)
      #构造函数
      fx = x1**2 + x2**2 + 1
      #标量可以直接使用backward
      fx.backward()
      print(x1.grad)
      print(x2.grad)
  • 多变量梯度运算:对多元函数求偏导,每个自变量有多个值
      x1 = torch.tensor([10,20],requires_grad=True,dtype=torch.float64)
      x2 = torch.tensor([30,40], requires_grad=True, dtype=torch.float64)
      #构造函数
      fx = x1**2 + x2**2
      gx = fx.sum()
      gx.backward()
      print(x1.grad)
      print(x2.grad)

(十)控制梯度运算

1.控制自动微分

    x = torch.tensor(10,requires_grad=True,dtype=torch.float64)
    #1.方式一
    """
    with torch.no_grad()
    是一个上下文管理器
    作用是在其中计算得到的张量,其requires_grad=False
    这些张量不会在Tensor上追踪计算历史,不会用于自动微分
    适用于不需要进行梯度运算的张量,减少内存使用和计算时间
    """
    with torch.no_grad():
        y = x**2
    print(y.requires_grad) #此时y的requires_grad为False

    #2.方式二
    """
    @torch.no_grad()
    是一个语法糖
    作用是其修饰的函数,返回值默认的requires_grad=False
    """
    @torch.no_grad()
    def func(x):
        return x**2
    y = func(x)
    print(y.requires_grad)

    #3.方式三
    """
    torch.set_grad_enabled(False)
    进行全局设置,一般不使用
    """
    torch.set_grad_enabled(False)
    y = x**2
    print(y.requires_grad)

2.梯度累计和梯度清零

    x = torch.tensor([10,20,30,40],requires_grad=True,dtype=torch.float64)
    #现象:连续运算时会发现x的梯度值是会将历史的梯度值累加的
    for _ in range(3):
        f1 = x**2
        f2=f1.mean()
        f2.backward()
        print(x.grad)
    #避免累加需要进行梯度清零
    for _ in range(3):
        f1 = x**2
        f2=f1.mean()
        """
        先判断:当梯度不为0时
        执行:使用x.grad.data.zero_()将梯度清零
        """
        if x.grad is not None:
            x.grad.data.zero_()
        f2.backward()
        print(x.grad)

3.简单的梯度优化函数

    x = torch.tensor(10,requires_grad=True,dtype=torch.float64)
    for _ in range(1000):
        fx = x**2
        if x.grad is not None:
            x.grad.data.zero_()
        fx.backward()
        x.data = x.data - 0.001*x.grad
        print("%.10f"%x.data)

(十一)自动微分张量的类型转换

1.现象

对一个设置requires_grad=True的张量使用numpy进行转换时,会出现报错:

RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.

2.解决方法:detach

    x = torch.tensor(10, requires_grad=True, dtype=torch.float64)
    """
    x.detach()
    分离x,产生一个新的张量,与x共享内存
    """
    data_numpy = x.detach().numpy()
    print(data_numpy)

(十二)模型组件

1.损失函数

    #导包
    import torch.nn as nn

    #初始化平方损失函数对象
    """
    nn.MSELoss()
    是一个类,
    类中重写了__call__方法
    因此可以作为一个函数使用
    """
    criterion = nn.MSELoss()
    y_pred = torch.randn(3,5,requires_grad=True)
    y_true = torch.randn(3,5)
    #计算损失
    """
    将该类作为函数使用
    :param1:预测值
    :param2:真实值
    :return:平方损失,tensor类型
    """
    loss = criterion(y_pred,y_true)
    print(loss)

2.假设函数

    #导包
    import torch.nn as nn

    """
    nn.Linear()
    :param1:输入数据的特征数
    :param2:输出数据的特征数
    :return: Linear对象
    """
    model = nn.Linear(in_features=10,out_features=5)
    #输入数据
    inputs = torch.randn(5,10)
    """
    将该类作为函数使用
    :param:输入数据
    :return:预测值
    """
    y_pred = model(inputs)

3.优化方法

    #导包
    import torch.optim as optim

    model = nn.Linear(in_features=10, out_features=5)
    """
    optim.SGD()
    :param1:传入参数,可以使用model.parameters()
    :param lr:学习率
    """
    optimizer = optim.SGD(model.parameters(),lr=0.001)
    #调用backward函数得到梯度值
    #(省略一些调用逻辑)
    #进行梯度清零可以使用:optimizer.zero_grad()
    #更新模型参数
    optimizer.step()

(十三)数据集对象

1.自定义数据集对象

from torch.utils.data import Dataset

#需要实现以下方法
class Data(Dataset):#继承Dataset
    def __init__:#初始化

    def __len__:#返回长度

    def __getitem__:#索引

2.构建数据加载器

    from torch.utils.data import DataLoader

    #创建数据集
    x = torch.randn(100,8)
    y = torch.randint(0,2,[x.size(0),])
    #1.构建数据类
    dataset = Data(x,y)
    #2.构建数据加载器
    dataloader = DataLoader(dataset,batch_size=4,shuffle=True)

3.简单数据集

    from torch.utils.data import TensorDataset

    x = torch.randn(100,8)
    y = torch.randint(0,2,[x.size(0),])
    dataset = TensorDataset(x,y)
    dataloader = DataLoader(dataset,batch_size=4,shuffle=True)

(十四)模型保存与加载

1.模型对象

class Model(nn.Module):

    def __init__(self,input_size,output_size):
        super(Model,self).__init__()
        self.linear1 = nn.Linear(input_size,output_size*2)
        self.linear2 = nn.Linear(input_size*2,output_size)

    def forward(self,inputs):
        """
        当模型类继承了nn.Model并实现了forward函数
        此时可以将模型对象作为函数使用
        :param inputs:
        :return:
        """
        inputs = self.linear1(inputs)
        output = self.linear2(inputs)
        return output

2.直接存储方式

    #存储
    model = Model(64,10)
    """
    torch.save
    :param1:要存储的模型对象
    :param2:存储的位置
    :param pickle_module:存储方式
    :param pickle_protocol:存储协议
    """
    torch.save(model,"model/model.pth",pickle_module=pickle,pickle_protocol=2)

    #加载
    """
    torch.load
    :param1:加载模型的存储位置
    :param pickle_module:加载方式
    :param map_location:加载到的设备
    """
    model = torch.load("model/model.pth",pickle_module=pickle,map_location="cpu")

3.存储模型的网络参数

    #存储
    #自定义要存储的参数:字典形式
    #选择要存储的参数即可
    save_params = {
        "init_params":{"input_size":64,"output_size":10},
        "acc_score": 0.88,
        "avg_loss": 0.53,
        "item_num": 34,
        "optimizer_params":optimizer.state_dict(),
        "model_params":model.state_dict()
    }
    #存储模型参数
    torch.save(save_params,"model/model.pth")

    #加载
    #从磁盘中加载到内存
    model_params = torch.load("model/model.pth")
    #使用参数初始化模型
    model = Model(model_params["init_params"]["input_size"],model_params["init_params"]["output_size"])
    model.load_state_dict(model_params["model_params"])
    #使用参数初始化优化器
    optimizer = optim.Adam(model.parameters())
    optimizer.load_state_dict(model_params["optimizer_params"])
    #其他参数同理

二、深度学习基础

(一)神经网络简介

graph LR
id0("输入")-->id1(第一层神经元)-->id2(第二层神经元)--"......"-->id3(第n层神经元)-->id4(输出)
  • 我们将输入称为输入层,输出称为输出层,中间余下的层级统称为隐藏层
  • 信息经过输入层进入隐藏层,隐藏层通过既定算法对信息进行处理,然后传递到输出层
  • 每一层由多个神经元组成,同一层的神经元一般来说是没有连接的

(二)激活函数

1.作用

  • 激活函数的作用是为每一层的输出数据进行变换,为其添加非线性因素
  • 这样一来,神经网络就可以拟合出曲线模型,而不是仅仅只能拟合线性模型

2.要求

  • 由于我们在更新网络参数时,使用的是反向传播算法
  • 因此我们要求激活函数可微

3.sigmoid激活函数

$$
f(x)=frac{1}{1+e^{-x}}
$$

  • sigmoid函数将任意输入映射到(0,1)
    • 当输入值在[-6,6]时,输出值才会有明显差异
    • 当输入值在[-3,3]时,输出值才会有较好效果
  • sigmoid函数导数的数值范围是(0,0.25)
    • 当输入值在[-6,6]之外时,导数值接近于0,此时网络参数更新极其缓慢
    • 当输入值趋于0时,导数值趋于0.25
  • sigmoid神经网络在5层之内就会产生梯度消失现象,因此此函数用得不多,一般只用于二分类问题
    • 梯度消失:神经网络类似于多层复合函数,对其求梯度适用链式法则,而多重sigmoid函数复合,因为其范围(0.0.25),所以值会接近于0

4.tanh激活函数

$$
f(x)=frac{1-e^{-2e}}{1+e^{-2e}}
$$

tanh

  • tanh函数将任意输入映射到(-1,1)
    • 当输入值在(-3,3)之外时,会被映射为-1或1
  • tanh函数导数的数值范围是(0,1)
    • 当输入值在(-3,3)之外时,导数值接近于0
  • tanh函数以0为中心,其收敛速度比sigmoid快,但同样存在梯度消失问题
  • 在实际使用时,可以在隐藏层使用tanh函数,在输出层使用sigmoid函数

5.ReLU激活函数

$$
f(x)=max(0,x)
$$

ReLU

  • ReLU函数将负值映射为0,正值保持不变
  • ReLU函数计算简单,能够提供模型的训练效率
    • 但当网络参数采用随机初始化时,可能存在负值的参数,这可能导致正负值保留情况出现预料之外的效果
  • ReLU函数在x>0时不存在饱和问题(梯度趋于0),能够保持梯度不衰减,缓解梯度消失问题
    • 但当训练深入时,可能存在部分输入转变为负数,导致对应权重无法更新,我们称之为神经元死亡
    • 但这种情况不一定是负面的,部分神经元输出为0,可以减少参数间的依存关系,使网络更加稀疏,减小过拟合风险

6.SoftMax激活函数

$$
softmax(z_i)=frac{e^{z_i}}{sum_j e^{z_j}}
$$

  • SoftMax函数适用于多分类问题,是对sigmoid函数在多分类上的推广,目的在于将多分类的结果以概率的形式表示
  • SoftMax函数将输出的分类结果转换为(0,1)之间的概率值,所有结果的概率和为1
    • 简单来说,函数的分子是对某一结果取e的指数,函数的分母是对所有结果取e的指数再求和

7.激活函数的选择

  • 对应隐藏层:
    • 优先考虑ReLU函数及其变体,注意神经元死亡问题
    • 不要使用sigmoid函数,使用tanh函数替代
  • 对于输出层:
    • 二分类问题:sigmoid函数
    • 多分类问题:SoftMax函数
    • 回归问题:identity函数
    • identity:$f(x)=x$

(三)参数初始化

1.初始化参数

  • 一般来说我们需要初始化的参数有:
    • 权重:比较重要,可以使用一些参数初始化方法
    • 偏置:一般为0
  • 一般来说实际开发过程中,PyTorch框架会给我们参数对应的默认值,一般不需要改动,除非模型效果较差的时候考虑改动参数

2.均匀分布初始化

  • 权重参数从区间内均匀取值,一般是在$(-frac{1}{sqrt d},frac{1}{sqrt d})$区间上,d表示每个神经元的输入数量,也就是有多少数据进入一个神经元
    #先随便搞个模型
    linear = nn.Linear(5,3)
    #初始化
    """
    nn中有一个init模块
    从中调用各种初始化方式
    均匀分布就是uniform(带_表示对原数据修改)
    参数是模型的权重linear.weight
    """
    nn.init.uniform_(linear.weight)
    print(linear.weight)

3.正态分布初始化

  • 随机初始化从均值为0,标准差为1的高斯分布中取值,一般来说这些值很小
    linear = nn.Linear(5,3)
    #初始化
    """
    normal()
    :param1:输入权重
    :param mean:要指定正态分布的均值
    :param std:要指定正态分布的标准差
    """
    nn.init.normal_(linear.weight,mean=0,std=1)
    print(linear.weight)

4.固定值初始化

  • 没啥好说的...设置一个固定值作为初始化值,其实还有个分类:
    • 全0初始化:固定值为0,一般去初始化偏置,不要初始化权重,权重全0这个神经元就会输出0,导致下一层相关神经元也可能为0
    • 全1初始化:固定值为1
    linear = nn.Linear(5,3)
    #初始化
    """
    constant()
    :param1:输入权重
    :param2:要指定的固定值
    """
    nn.init.constant_(linear.weight,5)
    print(linear.weight)
    """
    zeros()
    全0初始化
    :param:输入权重
    """
    nn.init.zeros_(linear.weight)
    print(linear.weight)
    """
    ones()
    全1初始化
    :param:输入权重
    """
    nn.init.ones_(linear.weight)
    print(linear.weight)

5.kaiming初始化

  • 也称为HE初始化,分为正态分布的HE初始化和均匀分布的HE初始化
    linear = nn.Linear(5,3)
    #正态分布的kaiming初始化
    """
    kaiming_normal()
    :param:输入权重
    """
    nn.init.kaiming_normal_(linear.weight)
    print(linear.weight)
    #均匀分布的kaiming初始化
    """
    kaiming_uniform()
    :param:输入权重
    """
    nn.init.kaiming_uniform_(linear.weight)
    print(linear.weight)

6.xavier初始化

  • 也称为Glorot初始化,基本思路是让每一层的激活值和梯度的方程在传播过程中保持一致,同样分为正态分布的xavier初始化和均匀分布的xavier初始化
    linear = nn.Linear(5,3)
    #正态分布的xavier初始化
    """
    xavier_normal()
    :param:输入权重
    """
    nn.init.xavier_normal_(linear.weight)
    print(linear.weight)
    #均匀分布的xavier初始化
    """
    xavier_uniform()
    :param:输入权重
    """
    nn.init.xavier_uniform_(linear.weight)
    print(linear.weight)

(四)反向传播

1.梯度下降算法

$$
w{new}=w{old}-eta frac{partial E}{partial w}
$$

  • $frac{partial E}{partial w}$:梯度,寻找该点增长最快的方向,它的反向就是下降最快的方向
  • $eta$:学习率,也就是每次运动的一个距离,学习率太大容易错过最优解,学习率太小训练效率很慢,最好随着训练过程动态调整
  • 举个栗子:
    • 有一个二维的函数$y=x^2$,其导数就是$y'=2x$,目的是求最小值的点
    • 初始点为(4,16),学习率为0.25,一次迭代得到$w_{new}=4-0.25(24)=2$,新的点为(2,4)
    • 继续:$w_{new}=2-0.25(22)=2$,新的点为(1,1)
    • 继续:$w_{new}=1-0.25(21)=0.5$,新的点为(0.5,0.25)
    • 继续:$w_{new}=0.5-0.25(20.5)=0.25$,新的点为(0.25,0.0625)
    • 重复不断地逼近(0,0)
    • 对于更高维度的,无非就是对每个参数求偏导,然后对应相减嘛

2.三个重要概念

  • Epoch:指的是对整个模型完成一次学习,称为一个Epoch
    • 以读书举例,表示将一本书读完,多个Epoch就表示反复读这本书多次
  • Batch:指的是对一个子数据集完成一次学习,称为一个Batch
    • 以读书举例,表示一本书我每次读10页,这10页读完就完成一个Batch,多个Batch就表示多次读10页直到把书读完呗
    • Batch的数量,应该是整个模型的大小除以Batch的大小,也就是每次读10页,要多少次能读完,当然最后一次如果小于10页也需要读一次
  • Iteration:指的是使用一个子数据集对参数完成一次更新,称为一个Iteraiton
    • 以读书举例,我在读书之前对整本书做一个笔记,然后每读10页有了新的了解,就对这个笔记进行更改,Interation的个数与Batch相同

3.多种梯度下降算法

不同的梯度下降算法的根本区别就在于Batch的大小不同,也就是每次读的页数不同

  • BGD:Batch的大小是整个样本,也就是每次我都将书读完
  • SGD:Batch的大小是一个样本,也就是每次我都只读一页
  • Mini-Batch:Batch的大小是指定的一个数,就记为n吧,也就是每次我都读n页

4.前向传播和反向传播的概念

  • 前向传播:指的是按照输入层--隐藏层--输出层的顺序进行传播,逐层运算之后得到预测值
  • 反向传播:得到前向传播的预测值之后,与真实值之间的差异结合得到损失函数,由损失函数反向地求各个参数的偏导,就是反向传播

5.反向传播算法

BP算法,又称为误差反向传播算法,用于求解模型的参数梯度,从而使用梯度下降法更新模型参数:

  • 通过正向传播得到预测值,通过损失函数计算预测值和真实值之间的误差
  • 通过反向传播将误差传递给网络中的神经元,对神经元的参数进行调整,从而缩小误差

举个栗子:

graph LR
id0("输入:两个特征")-->id1(神经元A)
id0-->id2(神经元B)
id1-->id3(神经元C)
id1-->id4(神经元D)
id2-->id3
id2-->id4
id3-->id5(输出)
id4-->id5
  • 假设:
    • 输入:i、j
    • 下列运算函数中,x为自变量,k为偏置是常数,x的系数a,b,c,d为权重,也就是要更新的参数
    • 神经元A:运算函数$O_1=a_1 x_1+a_2 x2+k{1}$
    • 神经元B:运算函数$O_2=b_1 x_1+b_2 x2+k{2}$
    • 神经元C:运算函数$O_3=c_1 x_1+c_2 x2+k{3}$
    • 神经元D:运算函数$O_4=d_1 x_1+d_2 x2+k{4}$
    • 假设所有神经元的激活函数是sigmoid,例如神经元A的输出结果$O_1$会经过激活函数sigmoid,有$M_1=sigmoid(O_1)$作为该神经元最终的输出结果
    • 假设最终损失函数为平方损失,用L表示
    • 真实值分别为$t_1$和 $t_2$,为常数
  • 过程:
    • 正向传播:
    • i和j传入神经元A和B,输出$M_1$和$M_2$
    • $M_1$和$M_2$作为新的输入值,传入神经元C和D,输出$M_3$和$M_4$
    • 因为后续没有其他神经元,则$M_3$和$M_4$为最终输出结果,也就是预测值
    • 计算损失函数$L=frac12((M_3-t_1)^2+(M_4-t2)^2)$
    • 反向传播:
    • 我们的目的是使得L的值最小,方式是通过修改参数a,b,c,d
    • 例如更新神经元C中的$c_1$:
      • 将$c_1$视为自变量,用损失函数L对$c_1$求导
      • 分析一下,L是$M_3$和$M_4$的函数,其中$M_4$与$c_1$无关,求导后是0;而$M_3$与$c_1$有关
      • $M_3$是$O_3$的函数,$O_3$是$c_1$的函数
      • 那么本质上,从L到$c_1$是一个复合函数求导,使用链式法则
      • $frac{partial L}{partial c_1}=frac{partial L}{partial M_3}frac{partial M_3}{partial O_3}frac{partial O_3}{partial c_1}$
      • 由此求得$c_1$的梯度,使用梯度下降算法更新$C_1$即可
    • 其他神经元和参数的处理同理,本质上就是进行复合函数求导
    • 需要注意的是在求神经元A和神经元B的参数时,要考虑到其结果是作为输入传入神经元C和D的,因此$M_3$和$M_4$均与之相关
import torch
import torch.nn as nn
import torch.optim as optim

#自定义网络类需要继承父类nn.Module
class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        self.linear_1 = nn.Linear(in_features=2,out_features=2)#神经元A和B
        self.linear_2 = nn.Linear(in_features=2,out_features=2)#神经元C和D

        #查看默认的参数
        print(self.linear_1.weight.data,self.linear_1.bias.data)#神经元A和B的参数和偏置
        print(self.linear_2.weight.data,self.linear_2.bias.data)#神经元C和D的参数和偏置
        print("-"*10)

    #进行正向传播的函数
    def forward(self,x):
        #第一层
        x=self.linear_1(x)#经过第一层得到输出
        x=torch.sigmoid(x)#经过sigmoid得到最终输出
        #第二层
        x=self.linear_2(x)
        x=torch.sigmoid(x)

        #返回最终输出结果:预测值
        return x

#模拟一次反向传播
def test():
    #输入数据,二维列表表示一批数据
    inputs = torch.tensor([[1,2]],dtype=torch.float)
    #真实值
    target = torch.tensor([[2,5]],dtype=torch.float)
    #初始化网络对象
    net = Net()
    #得到预测值
    output = net(inputs)#因为父类中实现了__call__方法,默认调用了Net中的forward函数
    print(f"预测值:{output}")#打印预测值
    #计算误差:使用平方误差
    loss = torch.sum((output-target)**2) / 2
    print(f"损失值:{loss}")
    #构建优化器
    optimizer = optim.SGD(net.parameters(),lr=0.5)
    #梯度清零
    optimizer.zero_grad()
    #反向传播
    loss.backward()#相当于对所有参数求导,得到梯度值
    #参数更新
    optimizer.step()#使用梯度下降算法更新参数

    #打印每个参数的梯度
    print(net.linear_1.weight.grad.data)
    print(net.linear_2.weight.grad.data)
    print("-"*10)
    #更新后的参数值
    print(net.linear_1.weight.data, net.linear_1.bias.data)  # 神经元A和B的参数和偏置
    print(net.linear_2.weight.data, net.linear_2.bias.data)  # 神经元C和D的参数和偏置

if __name__ == '__main__':
    test()

(五)优化方法

1.现象

对于传统的梯度下降算法,可能会遇到:

  • 梯度值过小,导致参数更新缓慢
  • 碰到鞍点或局部最小值,参数无法继续更新

2.指数移动加权平均

  • 一些概念前提:
    • 算术平均:指的是所有数拥有相同的权重,简单相加之后除以个数,得到的就是算术平均
    • 加权平均:指的是为每个数赋予一定权重,每个数乘以权重再相加,除以个数得到加权平均
    • 移动平均:指的是指定一个数值n,计算邻近的n个数,得到移动平均
  • 结合上面的概念我们得到:
    • 指数移动加权平均:根据距离的远近赋予数不同的权重,一般来说距离越近权重越大,由此得到的加权平均数,我们称之为指数移动加权平均

$$
S_n=begin{cases}Vn,当n=0时 beta S{n-1}+(1-beta)V_n,当nneq0时end{cases}
$$

  • 其中S是指数移动加权平均值
  • n表示距离
  • $beta$表示调节权重,越大则平均数越平缓

随机生成的例子:

指数移动加权平均

import torch
import matplotlib.pyplot as plt

#指数加权平均
def test01():
    _, axes = plt.subplots(1, 3)

    torch.manual_seed(0)
    #随机产生20个数
    noise = torch.randint(0,50,size=[20,],dtype=torch.float)
    print(noise)

    distance = torch.arange(1,21,1)
    print(distance)
    axes[0].scatter(distance,noise)

    #指数移动加权平均
    beta=0.9
    exp_weight_avg = []
    for idx,temp in enumerate(noise,1):
        if idx==1:
            exp_weight_avg.append(temp)
            continue
        new_temp = exp_weight_avg[idx-2]*beta+(1-beta)*temp
        exp_weight_avg.append(new_temp)
    #绘制
    axes[1].plot(distance,exp_weight_avg)
    axes[1].set_title("beta=0.9")

    #指数移动加权平均
    beta=0.5
    exp_weight_avg = []
    for idx,temp in enumerate(noise,1):
        if idx==1:
            exp_weight_avg.append(temp)
            continue
        new_temp = exp_weight_avg[idx-2]*beta+(1-beta)*temp
        exp_weight_avg.append(new_temp)
    #绘制
    axes[2].plot(distance,exp_weight_avg)
    axes[2].set_title("beta=0.5")

    plt.show()

3.Momentum

  • 当参数出现更新缓慢或无法更新的情况时,Momentum通过指数加权平均法,累计历史的梯度值对参数进行更新,可以优化梯度,
  • 动量项的计算方法:
    $$
    vt=beta *v{t-1}+eta*L(w_t)
    $$

    • $v_{t-1}$:表示第t次迭代的动量项
    • $w_t$:表示当前参数
    • $L(w_t)$:表示当前参数的梯度值
    • $beta$:表示权重系数,一般取0.9
    • $eta$:表示学习率
  • 参数的更新方法:
    $$
    w_{t+1}=wt-v{t+1}
    $$
  • 举个栗子:
    • 假设权重$beta = 0.9$,取学习率为0.1,初始化动量项$v_0=0$
    • 第一次梯度值:动量项$v_1=0.9v_0+0.1L(w_0)$,更新参数$w_1=w_0-v_1$
    • 第二次梯度值:动量项$v_2=0.9v_1+0.1L(w_1)$,更新参数$w_2=w_1-v_2$
  • 注意:Momentum并没有对学习率进行优化

4.AdaGrad

  • AdaGrad对不同的参数分量使用不同的学习率,其学习率总体上会逐渐减小
  • 学习率计算公式:
    $$
    eta^*=frac{eta}{sqrt{s}+epsilon}
    $$

    • $eta$:学习率
    • $g$:表示当前的梯度
    • $s$:表示累计的平方梯度,$st=s{t-1}+g{t-1}odot g{t-1}$,其中$odot$表示阿达玛积
    • $epsilon$:表示一个小常数,用于避免分母为0的情况,通常取非常小的数,例如$1^{-9}$
  • 参数更新公式:
    $$
    w_{t+1}=wt-eta^*g{t+1}
    $$
  • 注意:AdaGrad可能导致学习率过早降低到一个较小值,导致模型训练后期学习率较小,不易找到最优解

5.RMSProp

  • RMSProp是对AdaGrad的优化,其使用指数移动加权平均梯度替代了历史梯度平方和,也就是更改了s的求法
  • 学习率计算公式:
    $$
    eta^*=frac{eta}{sqrt{s}+epsilon}
    $$

    • $eta$:学习率
    • $g$:表示当前的梯度
    • $s$:表示累计的平方梯度,$$st=beta s{t-1}+(1-beta)g{t-1}odot g{t-1}$$,其中$odot$表示阿达玛积
    • $epsilon$:表示一个小常数,用于避免分母为0的情况,通常取非常小的数,例如$1^{-9}$
  • 参数更新公式:
    $$
    w_{t+1}=wt-eta^*g{t+1}
    $$

6.Adam

  • Adam是对Momentum和RMSProp的综合,同时使用移动加权平均的梯度和移动加权平均的学习率

(六)正则化

1.作用

  • 正则化的主要作用是缓解过拟合

2.Dropout层

  • Dropout通过减少神经元之间的连接,达到降低网络复杂度的目的
  • 我们对Dropout设置一个概率p,表示丢弃的概率,也就是将张量元素设置为0
  • 为了校正张量元素设置为0带来的影响,会对非0元素进行缩放,缩放比例为$frac{1}{1-p}$
    #初始化Dropout对象
    """
    nn.Dropout
    是一个类,作用是随机丢弃神经元,可以传入参数p
    :param p:表示丢弃神经元的概率,默认为0.5
    """
    dropout = nn.Dropout(p=0.8)
    #初始化随机数据
    data = torch.randint(0,10,size=[8,8],dtype=torch.float)
    print(data)
    #将data数据经过Dropout丢弃
    out = dropout(data)
    print(out)#观察结果会发现出现大量的0,表示神经元被丢弃

(七)批量归一化

1.作用

  • Batch Normalization(批量归一化,简写为BN)的主要作用是控制数据的分布,从而加快网络的收敛
    • 因为我们在输入数据的时候,通常是分多个Batch的,每个Batch内的数据可能是满足不同分布的
    • 例如我们将第一批数据满足的分布称为a,第二批数据满足的分布称为b
    • 当第一批数据进入网络时,模型参数会进行调整以适应a分布
    • 当第二批数据进入网络时,模型参数会进行调整以适应b分布
    • 这就意味着,网络的参数会进行较大幅度的调整,不利于网络的收敛
    • 此时,我们可以通过BN对数据进行标准化,使其分布相对稳定一些,从而加快网络收敛

2.公式

$$
f(x)=gammafrac{x-E(x)}{sqrt{Var(x)+epsilon}}+beta
$$

  • $gamma$、$beta$:可学习参数,相当于对标准化之后的值做一个线性变换
  • $epsilon$:表示一个小常数,用于避免分母为0的情况,通常取非常小的数,例如$1^{-9}$
  • $E(x)$:表示变量的均值,使用的是移动加权方法
  • $Var(x)$;表示变量的方差,使用的是移动加权方法

3.API

    #随机生成数据
    """
    生成的形状是[batch_size,channel,height,width]
    batch_size:该批次中有几个样本
    channel:通道数,可以理解为有几个特征
    height:高,也就是行数
    width:宽,也就是列数
    """
    inputs  = torch.randint(0,10,[1,3,5,5],dtype=torch.float)
    print(inputs)
    print("-"*50)
    #创建BN对象
    """
    nn.BatchNorm2d()
    :param num_features:表示通道数
    :param affine:表示是否携带学习参数,若为否,那么gamma和beta分别为1和0
    :param momentum:调节移动加权平均值的计算,这个用于控制计算均值和方差
    :param eps:表示小常数
    """
    bn = nn.BatchNorm2d(num_features=3,affine=False,momentum=0.1,eps=1e-6)
    output = bn(inputs)
    print(output)

三、卷积神经网络

(一)卷积神经网络简介

  • 卷积神经网络是深度学习在计算机视觉领域的重要成果,是含有卷积层的神经网络,卷积层的作用是用于自动学习和提取图像的特征
  • 我们之前了解到的全连接神经网络,对于图像的处理计算开销大,且难以保留图像的特征,准确率低
  • 卷积神经网络由三部分组成:
    • 卷积层:用于提取图像中的局部特征
    • 池化层:用于降低参数的维度
    • 全连接层:用于输出想要的结果

(二)图像的基本知识

1.像素

  • 像素:图像由像素点组成,每个像素点的范围是[0,255],像素点越大表示该点越亮
    • 注意,仅有像素的图片是黑白的
        #创建了一个200*200的像素值全为0的图像
        img = np.zeros([200,200])
        """
        plt.imshow()
        :param:传入像素点的取值
        :param cmap:用于映射标量数据到颜色,默认值为'viridis',若传入的是RGB类型则忽略此参数
        :param vmin:当使用标量数据且没有显式范数时,vmin 和 vmax 定义颜色图涵盖的数据范围 
        """
        plt.imshow(img,cmap="gray",vmin=0)
        plt.show()
    
        #200*200像素值全为1的图像
        img = np.ones([200,200])
        plt.imshow(img,cmap="gray",vmin=0)
        plt.show()
    
        #随机的200*200
        img = np.random.randint(0,256,size=[200,200])
        plt.imshow(img, cmap="gray", vmin=0)
        plt.show()

2.通道

  • 通道:一般来说,彩色图片是由多张图片叠加而成的,通道就是说由哪些图片叠加而成
    • RGB通道:图像由R、G、B三个通道叠加而成,分别表示红色、绿色、蓝色
    • RGBA通道:相比与RGB通道多添加了一个A通道,表示透明度,其值越小,图像越透明
        """
        plt.imread()
        用于读取图片信息
        :param:图片地址
        :return:ndarray类型的数值
        得到的ndarray,例如(1080, 1920, 3)
        第一个维数表示高度上有多少像素点,即高度上有1080个像素点
        第二个维数表示宽度上有多少像素点,即宽度上有1920个像素点
        第三个维数表示有多少通道,即JPG格式有3个通道(RGB),PNG格式一般有4个通道(RGBA)
        """
        img = plt.imread("data/001.jpg")
        img = np.transpose(img,[2,0,1])#将通道数放到第一个维数上,方便后续遍历
        for channel in img: #展示每个通道的图片
            print(channel)
            plt.imshow(channel)
            plt.show()

(三)卷积层

1.卷积计算

  • 卷积计算:是指通过一个卷积核(也称为滤波器),对原有图像进行计算,得到特征图
  • 过程:
    • 假设一张图片的大小是100*100的,也就是一个100行、100列的一个矩阵,记为A矩阵
    • 卷积核也是一个矩阵,一般是需要学习的,我们假设它是5*5的,也就是一个5行、5列的矩阵,记为B矩阵
    • 我们从A矩阵的左上角开始,将B矩阵按左上角对齐的方式放置到A矩阵上,那么会有5行、5列的元素重合对应
    • 我们将对应元素作阿达玛积,然后相加的得到一个数,作为特征图的第1行第1列的元素值
    • 然后,我们将卷积核水平右移一位,继续作阿达玛积求和,作为特征图的第1行第2列的元素值
    • 以此类推,直到卷积核的最右侧与A矩阵的最右侧重合,得到特征图第1行全部数值
    • 随后,我们将卷积核水平平移回到最左侧的位置,然后下移一位,逐步右移继续计算特征图第2行的数值
    • 以此类推,直到卷积核与A矩阵的右下角重合,得到特征图的全部数值

2.Padding

  • 通过上述的计算过程,我们可以发现,经过计算之后的特征图比原图的尺寸要小
  • 如果我们想要得到一个与原图尺寸相同的特征图,就可以在原图周围添加Padding
  • 所谓的Padding,就是对原图的一个扩展,也就是在矩阵周围添加n圈元素,n就是Padding的值
    • 例如:(其中的0是代表一个元素,没有具体值的含义)
      $$
      left[begin{matrix}0&0&0&0&0&0&0 end{matrix}right] to Padding=1 to left[begin{matrix}0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0end{matrix}right]
      $$

3.Stride

  • 再考虑上面的计算过程,我们一直是通过右移和下移1位来得到特征图的
  • 实际上,我们也可以一次右移和下移多位来得到特征图,移动的位数我们称之为步长,也就是Stride
  • 注意:Stride能够影响计算的次数,以及最终得到的特征图的尺寸

4.多通道卷积计算

  • 实际使用时,我们遇到的图像大多数是多通道的
  • 对于多通道的情况,进行卷积计算的过程也差不多:
    • 我们需要为每一个通道提供一个对应的卷积核
    • 对每一个通道,使用对应的卷积核按照卷积计算的过程得到对应的特征图矩阵
    • 最后将得到的所有特征图矩阵按矩阵方式相加,得到最终的特征图矩阵
    • 当然,这个效果可以等价为每计算完一个对应位置就相加,可以节省一点存储空间

5.多卷积核卷积计算

  • 对于每一个卷积核,我们理解为一种理解图像的角度
  • 那么,对于同一个图像,我们是可以通过多个角度来分析图像的,也就是可以使用多个卷积核进行卷积计算
  • 多卷积核卷积计算的过程:
    • 例如,对于一个图像,我们有两个卷积核
    • 我们分别用两个卷积核与图像做卷积计算,得到两个特征图
    • 我们将两个特征图视为一个图像的两个通道,形成一个结果(一般来说,我们会将这个结果送到下一个卷积层)
  • 对于多通道的原图,过程是类似的:
    • 我们先提供多个对应的卷积核组
    • 每一个卷积核组与原图按照多通道卷积计算的规则得到一个特征图
    • 最后我们将所有得到的特征图作为一个图形的多个通道,形成结果

6.特征图大小

  • 我们知道,特征图的大小与一下参数相关:
    • size:卷积核的尺寸,一般都是奇数的方阵
    • Padding:填充的圈数
    • Stride:步长
  • 设原图大小为$WW$,卷积核大小为$FF$,Padding=P,Stride=S,特征图大小为$N*N$,那么:
    $$
    N=frac{W+2P-F}{S}+1
    $$

    • 先考虑填充情况,填充后的图像尺寸为$(W+2P)*(W+2P)$
    • 卷积核右移和下移的长度就是$(W+2P)-F$,思考卷积核右侧(下侧)与填充图的右侧(下侧)重合移动的距离
    • 用这个距离除以卷积核一次移动的步长,就是移动时单个方向的计算次数,我们知道每次计算形成特征图的一个元素,也就是$frac{W+2P-F}{S}$
    • 别忘了,卷积核第一次重合时也进行了一次计算,也就是移动之前我们也进行了一次计算,因此需要加1
    • 最终得到特征图的大小就是$N=frac{W+2P-F}{S}+1$
    • 对于不是方阵的原图,分别计算长宽即可

7.API

  • 卷积层对于输入的数据有形状要求:(BatchSize,Channel,Height,Width)
    • BatchSize:批次
    • Channel:通道数
    • Height:高度
    • Width:宽度
  • 单个卷积核
      #读取图像
      img = plt.imread("data/001.jpg")
      print(img.shape)
      #创建卷积核
      """
      nn.Conv2d()
      常用于图像处理
      :param in_channels:输入图像的通道数
      :param out_channels:输出特征图的通道数,也是卷积核组的数量
      :param kernel_size:卷积核尺寸
      :param stride:步长
      :param padding:填充数
      """
      conv = nn.Conv2d(in_channels=3,out_channels=1,kernel_size=3,stride=1,padding=0)
    
      #处理输入数据,使其满足卷积层要求
      img = torch.tensor(img,dtype=torch.float32).permute(2,0,1)
      #因为只有一个批次,直接在批次处填1即可
      img = img.unsqueeze(0)
      print(img.shape)
    
      #传入卷积层处理
      img = conv(img)
      print(img)
    
      #显示处理后的图像
      img = img.squeeze(0).permute(1,2,0) #将图像的格式改回(Height,Width,Channel)
      plt.imshow(img.detach().numpy())
      plt.show()
  • 多个卷积核
      #读取图像
      img = plt.imread("data/001.jpg")
      #创建卷积核
      conv = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3, stride=1, padding=0) #out_channels改一下就行
      #处理输入数据,使其满足卷积层要求
      img = torch.tensor(img,dtype=torch.float32).permute(2,0,1)
      #因为只有一个批次,直接在批次处填1即可
      img = img.unsqueeze(0)
      #传入卷积层处理
      img = conv(img)
      #显示处理后的图像
      img = img.squeeze(0).permute(1,2,0) #将图像的格式改回(Height,Width,Channel)
      #多通道的图像要分通道展示
      plt.imshow(img[:,:,0].detach().numpy())
      plt.show()
      plt.imshow(img[:, :, 1].detach().numpy())
      plt.show()
      plt.imshow(img[:, :, 2].detach().numpy())
      plt.show()
    

(四)池化层

1.池化计算

  • 池化计算:
    • 与卷积层类似,我们会有一个池化核,也是矩阵,一般来说,池化核的计算是固定的,也就是不进行学习
    • 我们通过与卷积核一样的操作,移动池化核,按照一定的规则进行计算得出数值
  • 计算规则:
    • 最大池化:每次选择池化核范围内最大的数值
    • 平均池化:每次计算池化核范围内数据的均值

2.Stride和Padding

  • 与卷积层的Stride相同,指的是步长
  • 与卷积层的Padding相同,指的是填充圈数,一般默认进行填充的元素为0

3.多通道池化计算

  • 对于多通道数据,池化计算会分别对每个通道进行池化计算
  • 但与卷积层不同,池化计算的结果不会进行合并,会输出与通道数数量相同的多个结果

4.API

  • 卷积层对于输入的数据有形状要求:(BatchSize,Channel,Height,Width)
    • BatchSize:批次
    • Channel:通道数
    • Height:高度
    • Width:宽度
  • 单通道池化
      #创建了一个3*3矩阵
      inputs = torch.randint(0,10,[3,3],dtype=torch.float32)
      print(inputs)
      #处理数据
      inputs = inputs.unsqueeze(0).unsqueeze(0)
    
      #1.最大池化
      max_pool = nn.MaxPool2d(kernel_size=2,stride=1,padding=0)
      output = max_pool(inputs)
      print(output)
    
      #2.平均池化
      avg_pool = nn.AvgPool2d(kernel_size=2,stride=1,padding=0)
      output = avg_pool(inputs)
      print(output)
  • 多通道池化
      #随机构建了输入,通道数为3,3*3的矩阵数据
      inputs = torch.randint(0,10,size=[3,3,3],dtype=torch.float32)
      print(inputs)
    
      inputs.unsqueeze(0)
    
      #1.最大池化
      max_pool = nn.MaxPool2d(kernel_size=2,stride=1,padding=0)
      output = max_pool(inputs)
      print(output)
    
      #2.平均池化
      avg_pool = nn.AvgPool2d(kernel_size=2,stride=1,padding=0)
      output = avg_pool(inputs)
      print(output)

四、循环神经网络

(一)循环神经网络简介

  • 循环神经网络,又称RNN网络,主要用于自然语言处理方向
  • 主要研究目的是通过计算机算法理解自然语言

(二)词嵌入层

1.作用

  • 作用:将文本进行数值化

2.使用过程

  • 过程:根据输入词的数量构建一个词向量矩阵,例如可以对100个词转换为100个向量,每个向量有128个特征,从而构成$100*128$的矩阵
    • 先对输入语料进行分词,为每一个词提供单独的索引,形成词表
    • 构建词嵌入矩阵,词索引对应的向量即为其数值化后的向量
  • 词嵌入层:
    • 词嵌入层的初始化是默认是通过均值为0,标准差为1的正态分布进行的,相当于随机初始化
    • 当输入一个词时,会使用随机的向量来表示该词
    • 该词参与到后续任务,会与目标结果进行对比产生损失
    • 通过反向传播来更新词嵌入层中对于词的表示
    text = "读书不是为了雄辩和驳斥,也不是为了轻信和盲从,而是为了思考和权衡。"

    #1.分词
    words = jieba.lcut(text)
    print(words)
    #2.构建词表
    index_to_word = {} #索引->单词
    word_to_index = {} #单词->索引
    #去重
    words = list(set(words))
    print(words)
    #生成索引
    for idx,word in enumerate(words):
        index_to_word[idx] = word
        word_to_index[word] = idx
    print(index_to_word)
    print(word_to_index)
    #3.构建词嵌入层
    """
    nn.Embedding()
    :param num_embeddings:表示词的个数,即词嵌入矩阵的高度
    :param embedding_dim:表示每个词的特征数,一般来说会给一个比较大的值
    :return:返回Embedding对象
    """
    embed = nn.Embedding(num_embeddings=len(index_to_word),embedding_dim=5)
    #4.将文本转为词向量表示
    for idx in range(len(index_to_word)):
        print(index_to_word[idx])#查看第一个词
        print(embed(torch.tensor(idx)))#查看第一个词对应的向量

(三)循环网络层

1.RNN网络原理

graph LR
id( )--"h0"-->id0("神经元")--"h1"-->id1(神经元)--"h2"-->id2(神经元)
  • 注:以上的神经元表示的是同一个神经元
  • 每一个神经元的工作过程:
    • 初始化隐藏层h0,一般为全0向量
    • 输入数据每次输入一个词进入神经元,神经元处理该词之后输出h1
    • h1和下一个输入再次进入神经元输出h2
    • 每一个h都包含了其之间所有词的计算语义
    • 以此类推,直到某一步神经元输出
  • RNN中可以有多个神经元,神经元的数量会影响输出的数据维度

2.计算公式

$$
ht=tanh((w{input}xt+b{input})+(w{hide}h{t-1}+b_{hide}))
$$

  • tanh:激活函数
  • $w{input},w{hide}$:分别为输入数据$xt$和上一次的隐藏数据$h{t-1}$对应的权重
  • $b{input},b{hide}$:分别为输入数据$xt$和上一次的隐藏数据$h{t-1}$对应的偏置

3.RNN的使用

  • RNN的输入要求:
    • 输入数据维度为(seq_len,batch_size,input_size)
    • seq_len:表示序列的长度,也就是词的数量,数量是根据分词的
    • batch_size:表示一批输入几个数据(句子)
    • input_size:表示输入数据的维度
    • 隐藏层维度为(num_layers,batch_size,hidden_size)
    • num_layers:循环层的层数,也就是有几层神经元

学习是一段漫长的旅途