结合代码手把手自学深度学习(一)
最近在学习某一篇论文中涉及到的对抗学习领域自适应,涉及到特征提取器、分类器和域鉴别器的代码编写。作为一个正在学习深度学习代码的小白想试着记录并分析一下,所以有了此文。
最近在学习某一篇论文中涉及到的对抗学习领域自适应,涉及到特征提取器、分类器和域鉴别器的代码编写。
故涉及代码如下:
class FeatureExtractor(nn.Module):
def __init__(self):
super(FeatureExtractor, self).__init__()
self.conv1 = nn.Conv1d(in_channels=1, out_channels=64, kernel_size=3, padding=1)
self.conv2 = nn.Conv1d(in_channels=64, out_channels=128, kernel_size=3, padding=1)
self.pool = nn.AdaptiveAvgPool1d(output_size=50)
self.fc = nn.Linear(128 * 50, 256) # 最终将输出映射到256维特征向量
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = self.pool(x)
x = x.view(x.size(0), -1) # Flatten
x = F.relu(self.fc(x))
return x
作为一个正在学习深度学习代码的小白想试着记录并分析一下,所以有了此文。
ps:第二篇已发布《结合代码手把手自学深度学习(二)》https://blog.csdn.net/weixin_55646552/article/details/142406980?spm=1001.2014.3001.5502
1 函数
1.1 __init__ 方法
这是构造函数,用于定义模型的层。
- self.conv1 = nn.Conv1d(in_channels=1, out_channels=64, kernel_size=3, padding=1)
- 一维卷积层:它接收一维信号输入(如时间序列或文本等)。
in_channels=1:输入信号的通道数是1(表示单通道,如单变量的时间序列)。out_channels=64:卷积操作后输出64个特征映射(即通道数为64)。kernel_size=3:卷积核大小为3(即滤波器长度为3)。padding=1:表示在输入数据的两端各填充一个0,这样卷积后的输出大小与输入相同。
- self.conv2 = nn.Conv1d(in_channels=64, out_channels=128, kernel_size=3, padding=1)
- 第二个一维卷积层:类似于
conv1,但这次输入通道是64,输出通道是128。它进一步从conv1输出的64个特征映射中提取更高层次的特征。
- self.pool = nn.AdaptiveAvgPool1d(output_size=50)
- 自适应平均池化层:它将输入的每个通道缩放到长度为50的输出,无论输入的实际长度是多少。池化的目的是减少特征的长度,同时保留重要信息。
- self.fc = nn.Linear(128 * 50, 256)
- 全连接层:它接收前面池化层的输出,大小是
128 * 50,并将其映射到256维的特征向量。这里,128是通道数,50是池化后每个通道的长度。
1.2 forward 方法
这是前向传播的定义,描述了数据如何流经网络。
- x = F.relu(self.conv1(x))
- 应用第一个卷积层并激活:输入
x通过第一个卷积层conv1,然后应用ReLU激活函数,使输出中负数变为0,从而引入非线性。
- x = F.relu(self.conv2(x))
- 应用第二个卷积层并激活:第一个卷积层的输出经过第二个卷积层
conv2,再次通过ReLU激活。
- x = self.pool(x)
- 池化操作:对卷积后的结果应用池化层,将每个通道的特征长度缩减到50。
- x = x.view(x.size(0), -1)
- 展平:将池化后的输出展平,以便输入到全连接层中。
x.size(0)表示批次大小,-1表示让PyTorch自动计算展平后的长度。
- x = F.relu(self.fc(x))
- 全连接层并激活:展平后的特征向量通过全连接层
fc,然后应用ReLU激活,输出最终的256维特征向量。
其中F 是 PyTorch 中的 torch.nn.functional,即函数式接口模块。这个模块包含了一些常用的神经网络操作,如激活函数、损失函数、池化操作等。这些操作和层不同,属于“函数式”操作,它们不会存储权重参数。
完整的前向传播流程如下:
- 输入经过第一个卷积层
conv1处理,输出一个64通道的特征图。通过F.relu进行非线性激活。 - 激活后的输出再经过第二个卷积层
conv2处理,生成128通道的特征图,同样经过F.relu激活。 - 输出再经过自适应池化层
pool,将特征图缩放到固定长度50。 - 将池化后的特征图展平(即打平成一维),以便输入到全连接层。
- 最后,通过全连接层
fc,并进行 ReLU 激活,输出一个256维的特征向量。
1.3 返回值
前向传播后,返回的是一个256维的特征向量,用于后续任务,比如分类或回归。
2 逐层分析
2.1 一维卷积神经网络CNN
-
输入:卷积层的输入通常是形状为
(batch_size, in_channels, length)的张量,代表一个批次的数据。比如在代码中,in_channels=1,即每个输入是单通道的一维数据(比如一条时间序列),length表示该序列的长度。 -
卷积核(滤波器):卷积层会学习多个卷积核,每个卷积核用于提取局部特征。比如在
conv1层中,参数是:
self.conv1 = nn.Conv1d(in_channels=1, out_channels=64, kernel_size=3, padding=1)
- 输入通道数
in_channels=1:代表每个输入数据的维度。比如如果输入是一个单变量的时间序列,那么就只有1个通道。 - 输出通道数
out_channels=64:表示该卷积层会学习64个不同的卷积核,每个卷积核提取一种不同的特征。所以,输出将有64个通道,或者叫64个特征图。 - 卷积核大小
kernel_size=3:卷积核会在输入的每个局部3个数据点上滑动,提取局部特征。 - 填充
padding=1:在卷积操作之前,输入的两边会各加1个0,这样使得卷积前后输入输出的长度保持不变。
如果输入的长度是 L,经过卷积层后的输出长度 L_out 计算公式为:

在conv2中:
self.conv2 = nn.Conv1d(in_channels=64, out_channels=128, kernel_size=3, padding=1)
- 输入通道数
in_channels=64:该层的输入通道数是64,接收conv1的输出。 - 输出通道数
out_channels=128:该层学习128个卷积核,生成128个特征图。 - 卷积核大小
kernel_size=3和 填充padding=1的意义与conv1相同。输出的长度同样保持不变。
卷积计算过程:
-
卷积核滑动:卷积核从输入的序列开始,按一定的步幅(stride,默认是1)滑动,每次将卷积核的值与序列中的局部值相乘并求和。
-
生成特征图(feature map):每个卷积核通过滑动卷积生成一张特征图,特征图的大小取决于输入的长度、卷积核大小、步幅和填充方式。
举个例子,假设输入是一条长度为5的时间序列 x = [x1, x2, x3, x4, x5],卷积核大小为 3,不考虑步幅和填充:
- 卷积核
w = [w1, w2, w3]首先会与输入的前3个数据点相乘并求和:y1 = w1*x1 + w2*x2 + w3*x3 - 然后卷积核会移动到下一个位置,计算
y2 = w1*x2 + w2*x3 + w3*x4 - 重复这个过程,直到遍历整个输入,生成一个特征图
y = [y1, y2, y3]
卷积核的设定与学习过程
-
卷积核的初始设定:
- 当模型初始化时,卷积核的权重(即每个卷积核中的数值)通常是随机初始化的。比如,使用一些方法(如 Xavier 初始化、He 初始化等)来设定这些权重的初值。
- 这些卷积核在初始化时可能还没有任何实际意义,它们只是随机值。
-
卷积核的学习过程:
- 在训练过程中,模型会通过前向传播计算输出,并与实际标签计算损失(如交叉熵损失、均方误差等)。
- 然后,通过反向传播,损失函数会根据梯度更新模型中的参数,这包括卷积层中的卷积核的权重。
- 每次训练迭代,卷积核的权重都会被更新,以便它们能够更好地提取输入数据中的特征。最终,这些卷积核会学习到适合当前任务的特定模式或特征。
每个卷积核生成一个特征图
在模型中,第一层卷积有64个卷积核,每个卷积核都是独立学习的,因此每个卷积核会提取输入数据中的不同特征,并生成一个相应的特征图。例如:
- 一个卷积核可能会学习到检测输入中的某种局部模式,比如边缘、线条、特定频率成分等。
- 不同卷积核可能会学习到不同的模式,比如检测曲线、某种变化趋势等。
经过64个卷积核的卷积操作后,输入数据会产生64个不同的特征图,每个特征图反映了输入数据中的某一类局部特征。
2.2 激活函数与非线性
在卷积之后,通常会添加一个激活函数(如ReLU)来引入非线性。你代码中的激活部分是:
x = F.relu(self.conv1(x))
ReLU (Rectified Linear Unit) 激活函数将负值截断为0,正值保持不变。它帮助网络能够学习更复杂的非线性特征。
2.3 池化层
池化层是卷积神经网络中常用的操作之一,其主要目的是降低数据的维度,减少特征图的大小,从而减少计算量,同时保留重要的特征。池化层可以帮助模型更加高效地学习特征,同时具有一定的抗噪能力。
代码中的池化层使用的是自适应平均池化(Adaptive Average Pooling),这是一个特殊的池化层。与普通的池化层不同,自适应池化层的输出大小是固定的,无论输入的大小是多少。它通过自动调整池化窗口的大小和步幅,使得输出特征图的大小为指定的尺寸。
自适应池化的具体含义
-
自适应池化(Adaptive Pooling):自适应池化的主要功能是确保特征图的输出大小符合预期的尺寸,即使输入特征图的大小不同。你只需要指定输出的尺寸,网络会自动调整池化窗口的大小和步幅,确保输出的尺寸是你指定的
output_size。 -
平均池化(Average Pooling):平均池化会对池化窗口中的所有值求平均。这意味着,它不是只保留局部最大值,而是考虑池化窗口内所有值的整体趋势,从而保留了更多的全局信息。与最大池化(Max Pooling)相比,平均池化更平滑,不会只聚焦于局部的极端值。
为什么固定 output_size=50?
在代码中,自适应平均池化的 output_size=50 意味着,经过池化后,特征图的长度会被缩减到 50,无论输入特征图的长度是多少。这样做有几个好处:
- 控制输出的维度:不管输入特征图的长度是多少,自适应池化确保了输出的长度始终为
50。这使得后续的全连接层能够处理固定大小的输入,方便网络的设计。 - 减少计算量:通过缩小特征图的尺寸,池化层降低了后续全连接层的输入维度,从而减少了计算量和模型的参数量。
池化层与卷积层的结合
池化层通常放置在卷积层之后,因为卷积层生成的特征图可能仍然比较大,通过池化层可以缩小特征图的大小,降低后续计算的复杂度,同时保留最重要的特征。
在模型中,两层卷积之后进行了自适应平均池化:
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = self.pool(x) # 池化操作
- 第一层卷积:提取输入数据的初级特征,并生成64个特征图。
- 第二层卷积:进一步提取更高层次的特征,并生成128个特征图。
- 池化层:将128个特征图的长度缩小为固定的
50,从而减少数据的维度,并为后续的全连接层提供一个固定大小的输入。
池化层的类型
常见的池化层类型包括:
- 最大池化(Max Pooling):取池化窗口中的最大值,通常用于提取最显著的特征。
- 平均池化(Average Pooling):取池化窗口中的平均值,通常用于平滑特征图。
- 自适应池化(Adaptive Pooling):可以根据输出大小自适应调整池化窗口,无论输入大小如何,输出都是固定的大小。
为什么选择平均池化而不是最大池化?
在模型中,使用了平均自适应池化,而不是最大池化。两者的主要区别在于它们保留信息的方式:
- 最大池化(Max Pooling):只保留局部区域内的最大值,适合保留强烈的局部特征或显著模式,但可能忽略一些细节。
- 平均池化(Average Pooling):保留池化区域的所有信息的平均值,适合平滑处理,保留全局趋势,适合提取细节不那么明显的特征。
选择平均池化的原因是:希望网络能够保留特征图中的更多细节信息,而不是只聚焦于局部最大值。对于某些任务(如信号处理或平滑变化的时间序列数据),平均池化比最大池化更有利于保留全局特征。
2.4 全连接层
view 是 PyTorch 中的一个函数,类似于 reshape,用于改变张量的形状,而不会改变其数据的存储方式。其作用是重新排列张量的维度,以便在后续的层中使用。
view 的参数含义:
x.size(0):表示张量的第一个维度的大小,即batch_size,它保持不变。x.size(0)就是当前x的第一个维度大小,即批量的数量。-1:-1是一种特殊的标记,表示我们希望 PyTorch 自动计算这一维度的大小,以确保总元素数量保持一致。它会根据张量的总大小和其他维度来推断出适当的大小。换句话说,-1可以自动填充合适的值,使张量的总元素数量不变
例子:
-
假设池化后,
x的形状是(batch_size, 128, 50),即有batch_size个样本,每个样本有128个特征图,每个特征图的长度为50。 - 展平操作的结果就是将每个样本的特征图(
128 x 50)展开成一维向量,即将(128, 50)变成128 * 50 = 6400。这样,x的形状就从(batch_size, 128, 50)变成了(batch_size, 6400),每个样本变成了一个6400维的向量。x = x.view(batch_size, 6400)由于
batch_size是变化的,因此我们使用x.size(0)来保持批量大小不变,-1表示自动计算这个展平后的维度(这里是6400)。
为什么需要展平?
展平操作是为了将卷积层和池化层输出的多维特征图转换为一维向量,以便传递给后续的全连接层(Fully Connected Layer)。全连接层只接受一维向量作为输入,因此必须将三维或四维的张量展平。
具体原因:
- 卷积层和池化层的输出是三维张量(通常是
(batch_size, 通道数, 特征图大小)),而全连接层需要一维的输入(即(batch_size, 输入向量长度))。 - 展平操作将多维特征图展平成一维向量,这样全连接层可以将展平后的数据作为输入进行处理。
全连接层如何降维?
以该模型为例,输入向量的维度为 6400,全连接层将它降维为 256 维。这里的关键在于权重矩阵和偏置向量。
在全连接层中,输入向量 x 和输出向量 y 之间的关系为:
其中:
x是输入向量,形状为(6400)。W是权重矩阵,形状为(256, 6400),表示有 256 个输出节点,每个输出节点与输入的 6400 个特征都有连接。b是偏置向量,形状为(256),表示每个输出节点有一个独立的偏置值。y是输出向量,形状为(256),即将输入的 6400 维数据压缩为 256 维数据。
具体操作步骤:
-
线性变换:每个输出节点计算输入向量的加权和:

这里
W_{ij}表示输入x_j与输出y_i之间的权重。 -
降维操作:通过这一步,输入的 6400 维向量被映射到了 256 维的空间。全连接层通过线性组合将原来的高维特征进行压缩,保留了主要特征。
-
激活函数:通常在线性变换后,还会通过激活函数(如
ReLU)增加非线性,使得模型能够拟合更复杂的函数:
激活函数的作用是将线性输出映射为非线性,从而增强模型的表达能力。
x = F.relu(self.fc(x)) # 全连接层之后进行 ReLU 激活此时,
x的形状从(batch_size, 6400)变成了(batch_size, 256),即每个样本的特征被压缩为 256 维。
256 维向量的后续操作
压缩到 256 维的向量之后,这个向量会用于进一步的处理,具体取决于模型的任务。以下是几种常见的操作方式:
a. 分类任务中的后续操作
-
再通过一个全连接层:在很多神经网络中,降维后的向量(256 维)通常会再经过一层或多层全连接层,进一步压缩或转换为最终的输出维度。例如,对于一个 10 类分类问题,最终输出需要变为 10 维的向量。你可能会再加一层全连接层:
self.fc2 = nn.Linear(256, 10)这样,256 维的向量会进一步被压缩成 10 维,作为分类概率的输入。
-
通过 Softmax 输出:如果是分类任务,通常会在最后一层全连接层后接一个
Softmax激活函数,将输出向量转化为类别的概率分布:output = F.softmax(self.fc2(x), dim=1)这会将输出的 10 维向量转换为各类别的概率值,用于分类决策。
b. 回归任务中的后续操作
如果任务是回归任务(预测连续值),则 256 维向量可能直接通过一个全连接层转换为回归值。比如,如果输出是一个标量:
self.fc2 = nn.Linear(256, 1)
在这种情况下,256 维向量直接映射到一个值,用于预测。
c. 生成任务或特征向量的用途
在一些生成任务或特征学习任务中,256 维的向量可能用于后续的生成步骤,或者作为特征嵌入(embedding)向量,用于表示输入数据在低维特征空间中的表示。
ps:第二篇已发布《结合代码手把手自学深度学习(二)》https://blog.csdn.net/weixin_55646552/article/details/142406980?spm=1001.2014.3001.5502
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)