1维卷积
文本 [B,C,L]
2维卷积
[B,C,W,H]
C
C:是特征维数,是通道channel ,一个特征向量有多少个维度,用多少个数表示一个特征向量 data.shape= [B,C,L] data[0]为一个元素 用多少个维度/什么样的shape去表示一个业务元素/自然元素 - 文本,一句话L个单词 - 图形,[w,h]个像素 - 这是shape,是形状, 每个业务元素/自然元素 再赋予不同的含义 - 比如,图像的像素,可以用一个向量去表示,特征维度为3,特征数为3 - 每个单词也可以用一个向量去表示
卷积计算的是特征维度,是所有数据shape上特征维度的线性组合,因此不改变shape
图像,以像素为自然元素,每个像素使用RGB三色表示 - [w,h]个像素形成一个图像,每个像素使用三个数来表示 - 用卷积计算,其数据shape为[B,C,W,H] = [B,3,W,H] 文本 - 一句本有L个单词,每个单词用embedding_dim个数来表示 - 用卷积计算,其数据shape为[B,C,N] = [B,embedding_dim,N] 交易 - 库表中的一行交易有N个列,每个列有一个业务含义,可用一个向量来表示 - 通常这个向量就是一个数, - 用卷积计算,其数据shape为[B,C,N] = [B,1,N]
nn.Conv1d( in_channels: int, out_channels: int, kernel_size: Union[int, Tuple[int]], stride: Union[int, Tuple[int]] = 1, padding: Union[str, int, Tuple[int]] = 0, dilation: Union[int, Tuple[int]] = 1, groups: int = 1, bias: bool = True, padding_mode: str = 'zeros', device=None, dtype=None, ) -> None 重要参数 in_channels: int, out_channels: int, kernel_size: Union[int, Tuple[int]], stride: Union[int, Tuple[int]] = 1, padding: Union[str, int, Tuple[int]] = 0, |
import torch from torch import nn X = torch.randn(3,1,7) conv1 = nn.Conv1d(in_channels=1,out_channels=8,kernel_size=2) conv1(X).shape # torch.Size([3, 8, 6]) 特征的线性变换 特征从1变为8,从8个维度去看这份数据,就产生了8个结果 特征shape从7变了6,少了1 stride: Union[int, Tuple[int]] = 1, padding: Union[str, int, Tuple[int]] = 0, 省略的参数,stride默认为1,padding为0(即不补) 此处K=2 (7 +2*0 - 2 )/1 + 1 = 6 即 (x +2*0 - 2 )/1 + 1 = x - 1 卷积后通常会跟一个maxpool来改变特征图,如果整张特征图取一个特征的话,就是 nn.MaxPool1d(kernel_size=x-1) |
|
|
|
官方API
https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html#torch.nn.Conv2d
调用示例
import torch from torch import nn torch.manual_seed(73) conv2d = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=(3, 3), padding=(1, 1), stride=(1, 1)) k=3,p=1,s=1这种 3 1 1的设计可保证特征图形状不变 (L + 2*1 - 3 )/ 1 +1 = L -K是减掉了第0步,即移动之前占的那个位置,所以最后+1,把这个补回来
pytorch图像输入的格式[N,C,H,W]
# [N, C, H, W] # N:batch_size # C:通道数/特征层 # H:高度 # W:宽度 X = torch.randn(32, 3, 512, 256) X.shape torch.Size([32, 3, 512, 256])
卷积变换的是特征层数(前提是311设计)
比如,最初一个像素特征有3个维数,即RGB三色, 可以使用卷积将这个像素的特征层数提升到32,即使用32个维度的数据来表示这个像素 conv2d(X).shape torch.Size([32, 32, 512, 256]) out_channels=32,因此得到32个新的特征层, 每个特征层都是原来3个特征层的整合,是对原3层特征图整体数据 某个维度的 提取/一次计算 因为提取了32次,所以才得到32个特征层
批次的维度永远不变
一个样本对应一个结果,永远是一对一,所以批次的维度在计算的过程中永远不变 conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1, stride=1) imgs = torch.randn(64, 3, 512, 512) conv1(imgs).shape torch.Size([64, 32, 512, 512])
非311的设计就会改变特征图shape
conv2 = nn.Conv2d(in_channels=5, out_channels=30, kernel_size=(3, 5), padding=1, stride=2) imgs = torch.randn(32, 5, 5, 5) conv2(imgs).shape torch.Size([32, 30, 3, 2]) 5 + 2*1 - 3 = 4; 4 /2 = 2; 2+1=3 5 + 2*1 - 5 = 2; 2 /2 = 1; 1+1=2
参数:偏置b
b = conv2.state_dict()["bias"] b tensor([ 1.1633e-02, 8.4127e-02, -2.0785e-02, 3.5110e-02, 1.0957e-01, 8.1714e-02, 7.5002e-02, -1.0953e-01, 3.1369e-02, -3.4423e-02, 1.9286e-02, -9.6399e-02, 4.3017e-02, -3.2373e-02, 1.2796e-02, 8.5092e-02, 7.9171e-02, 5.7777e-02, -9.4634e-02, 1.0043e-01, -6.0957e-02, 4.0399e-02, -2.4609e-05, -8.7420e-02, -9.3181e-03, -6.8756e-02, 9.5733e-02, -4.2755e-02, 5.5806e-02, 1.0345e-01]) b是一个向量,维数有30个, 卷积输出了30次,每次计算对应一个偏置, 毕竟每次输出对应的维度不同,偏置通常也不会一样 人各有志,世界上没有相同的两片树叶, 肯定是因为有不一样的因素在影响,每个树叶变化的方向会有所差异
参数w
w = conv2.state_dict()["weight"] w.shape torch.Size([30, 5, 3, 5]) 30是批次 5是特征层数,是原数据的特征层数,因为w是要 与"原数据"进行运算的 [3,5]是w的shape,原数据核kernel_size=(3, 5),所以w.shape=[3,5], 注意这里不是矩阵乘法,是按位相乘再相加, 即卷积的最小计算单元是按位相乘再相加
新维度上的一个数值outj = 各个特征层C上的“参数矩阵与对应核矩阵相乘再相加”之后的总和 + 偏置j
Cout 输出的维度 N 批次 Ni 批次中第i个样本 Cin 输入的维度,特征层,比如图像中的RGB三色,就是3,每个颜色使用相同的一个参数矩阵,三层共享一个偏置 Cout 输出的维度 bias 每个输出对应一个偏置
卷积计算公式数据及参数准备,参数就借用一下nn.Conv2d随机生成的参数,关键看计算过程
import torch from torch import nn torch.manual_seed(73) X = torch.randn(1, 3, 3, 3) conv2d = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=(3, 3), padding=(1, 1), stride=(1, 1)) w=conv2d.state_dict()['weight'] b=conv2d.state_dict()['bias'] print(w.shape, b.shape) # torch.Size([2, 3, 3, 3]) torch.Size([2])
卷积计算过程分析
torch.sum(X[0,0,:3,:3]*w[0,0,:,:]) + torch.sum(X[0,1,:3,:3]*w[0,1,:,:]) + torch.sum(X[0,2,:3,:3]*w[0,2,:,:]) + b[0] X[0,0,:3,:3] 取第1个特征层上前三行与前三列,取出一个kernel_size=(3, 3)的[3,3]子矩阵, 如此才能与[3,3]的参数矩阵w,按位相乘再相加, 公式中那个大大的星号,代表的是按位相乘,sum将它们相加 X[0,0,:3,:3] 虽然是原始数据左上角第一个[3,3]的子矩阵, 但,这是没考虑补0的情况... 上下,左右,各补0,就是 padding=(1, 1)之后的结果是这样的 3+1+1=5 x = X[0,0] a=torch.cat([torch.zeros(1,3),x,torch.zeros(1,3)],dim=0) a tensor([[ 0.0000, 0.0000, 0.0000], [ 0.3408, 0.2297, 1.7066], [ 1.3925, -0.2963, -0.0565], [-0.6886, -1.2959, 1.1781], [ 0.0000, 0.0000, 0.0000]]) a=torch.cat([torch.zeros(5,1),a,torch.zeros(5,1)],dim=1) a tensor([[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], [ 0.0000, 0.3408, 0.2297, 1.7066, 0.0000], [ 0.0000, 1.3925, -0.2963, -0.0565, 0.0000], [ 0.0000, -0.6886, -1.2959, 1.1781, 0.0000], [ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]]) X[0,0] tensor([[ 0.3408, 0.2297, 1.7066], [ 1.3925, -0.2963, -0.0565], [-0.6886, -1.2959, 1.1781]]) x = X[0,0]只是取了X数据3个特征层的第1层,回到开头那个计算公式, 原始数据x相当于补完0后最中间的那个数据,所以 torch.sum(X[0,0,:3,:3]*w[0,0,:,:]) + torch.sum(X[0,1,:3,:3]*w[0,1,:,:]) + torch.sum(X[0,2,:3,:3]*w[0,2,:,:]) + b[0] tensor(-0.6611) 的计算结果就是卷积计算[3,3]矩阵最中心位置的数值
验证
out=conv2d(X) out.shape torch.Size([1, 2, 3, 3]) 1 批次的维度 2 输出维度为2 [3,3] 特征图shape,311核设计不改变原数据特征图shape out tensor([[[[ 0.3094, -0.0677, -0.1076], [ 0.2938, -0.6611, -0.1007], [ 0.3736, -0.3332, -0.4082]], [[-0.2423, -0.2159, -0.4177], [ 0.8199, -0.6038, -0.5338], [-0.0077, 0.3984, 0.2478]]]], grad_fn=<ConvolutionBackward0>) w.shape torch.Size([2, 3, 3, 3]) 2 输出维度,一个维度或者说一个新的层面,有一组参数矩阵;有几组输出就会有几组参数矩阵,每个输出都有一个参数矩阵 3 原数据特征层数,一层对应一组参数矩阵, 这里一组有3个[3,3]的参数矩阵,原数据每层对应一个[3,3]参数矩阵,3层共用一个偏置 [3,3]一个2维参数矩阵的shape 这里Cout(输出的维度)= 2 ,所以out有两个矩阵, 其中第1个矩阵就是参数矩阵[0,:,:,:]与原数据计算的结果 最中间的数据-0.6611,与公式计算结果相符 至此,验证完毕,虽然只验证了一个数,其计算过程都是一样的
要点
一个n维向量有n维,这好像是废话... 这个n维,就是特征层,就是特征维度, 特征,特征层,这么抽象的事物,就是n维向量的n个维度, 每个维度上就都一个shape,只不过向量的shape为1, 如果shape高于1就是向量组/矩阵了,初学者应该以向量组的角度去看更容易理解一些 以RGB三色图像为例,再回忆一下上面的计算,从向量的角度看过去, 一个图像有shape[H,W]个3维向量组成,一个向量就是一个(R,G,B) 真正计算的最小单位是 sum(向量[R,G,B]*参数W[w1,w2,w3]) + 偏置b = 一个新的维度 细节在于卷积一次 计算一个核的(比如K=[3,3])个向量 一个图像有shape[H,W]个3维向量,格式就变成了 [C,H,W],再加上批次的维度就是[B,C,H,W] 实际上还是将一个n维向量转换到,或者是变换出 1,2,...,m个新维度,形成一个m维度向量 重点在于维度的变换,这也是后来的卷积使用311核,只变换维度,不改变shape的原因 就是为了强调,卷积的设计 为的就是/目的就是/现在专于 维度的变换(维度变换在AI角度就是提取特征)
最后再来看看这张广为流传的卷积计算图
输出特征数:每一个输出都是一次卷积计算的结果 一次卷积计算涉及参数量:输入特征数*卷积核大小 + 1个偏置 总参数量 = 输出特征数*(输入特征数*卷积核大小 + 1)
nn.Linear api
from torch import nn nn.Linear(in_features=13, out_features=2) Linear(in_features=13, out_features=2, bias=True)
nn.Linear 公式
b=line.state_dict()['bias'] b tensor([0.0154, 0.0998]) 一个新的特征层上配一个单独的偏置 这里有输出二个层面,就有2个偏置 每个偏置与原数据所有特征层(向量的所有维度)进行加法运算 这与卷积一样,或者说卷层就是复杂点的全连接
卷积
提取特征,就是那个n维向量的维数,每一维都对应一个特征图(shape) - 通常是由少变多,比如RGB最开始有三个特征图,变32,64,256 - 多变少的工作后续由全连接完成
池化层
裁减特征图(舍弃不重要,保留重要特征) - 每一层上的特征图变小了,信息变少了 - 不是哪里都可以用的(它舍弃的信息有点多,有的场景效果不好),最终效果好不好要实践一下
批归一化BN
这里的归一化指(x-E)/std 按特征层对批数据进行归一化, 一个特征层/一个维度的数据进行归一化,不跨层,不跨特征 比如RGB,R颜色不会跟G颜色数据混合计算,各算各的 增加收敛速度
激活函数
ReLU PReLU 激活函数是非线性的,有可能提升模型拟合能力的上限(也有可能降低 )
dropout
dropout有可能增加模型的容错能力,让模型变得健壮
损失函数
分类问题 交叉熵 连续问题 MSE
优化器
SGD :随机梯度下降 Adam :自适应,开始下降的快,当损失函数变小时,下降的步长变小,下降速度变慢
全连接
分类使用全连接