神经网络项目流程
一、数据预处理
- 清洗数据:处理缺失值、异常值、无用数据和噪声。
- 数据标注:对于监督学习任务,需要人工标注数据,为每个样本提供正确的标签。
- 数据转换:将数据转换为适合模型输入的格式,如归一化、标准化、编码(例如独热编码)等。
- 数据集划分:将数据集分为训练集、验证集和测试集。
- 处理特征x和标签y:在已经处理好的数据中,提取出输入输出。测试集无标签。
二、模型设计:
- 根据问题的性质设计神经网络的架构,包括选择层的类型(如卷积层、全连接层、循环层等)、激活函数、损失函数和优化器。
- 确定超参数,如学习率、批次大小、迭代次数等。
三、模型训练
- 使用训练集数据训练模型。通常使用梯度下降算法,涉及到前向传播、计算损失、反向传播(梯度回传)和参数更新等步骤。
- 使用验证集数据监控模型的性能,调整超参数,防止过拟合。
分批训练:在训练集中取一批批的数据,每一批数据都加权平均计算出一个loss,并不断通过将梯度下降等方法训练更新数据,训练完一轮训练集后将数据在验证集验证效果,同样也是在验证集中取一批批数据验证,完成完一轮的训练和验证和,进行下一轮的训练,重复上述操作
四、模型评估与优化
- 使用测试集评估模型的最终性能。这通常是在模型训练完成后进行的,以确保评估结果的公正性。
- 计算各种评估指标,如准确率、召回率、F1分数、均方误差等。
- 根据评估结果对模型进行优化,可能包括调整网络架构、超参数调整、使用正则化技术等。
- 可能需要多次迭代训练和评估过程,直到达到满意的性能。
数据集的分类
-
训练集(Training Set):
- 训练集是用来训练模型的数据。在这部分数据上,模型会进行参数的学习和调整。通过不断地迭代和优化,模型试图学习到数据中的模式和规律。
- 在实际操作中,训练集通常占据了数据集的大部分,例如70%或80%。
-
验证集(Validation Set):
- 验证集用于在训练过程中评估模型的性能。在整个模型开发过程中都会被使用。每当研究者对模型进行调整(如更改超参数、添加或移除特征等),以及选择最佳的模型结构或特征。
- 验证集不参与训练过程,它独立于训练集,用于防止模型过拟合(overfitting),即模型在训练数据上表现很好,但在未见过的数据上表现不佳。
- 验证集通常占据数据集的一部分,例如10%到20%。
-
测试集(Test Set):
- 测试集用于最终评估模型的泛化能力。在模型训练和验证完成后,使用测试集来测试模型在完全未知数据上的表现。
- 测试集应该被严格保留,直到所有的模型选择和训练过程结束。这样可以确保测试结果的公正性和准确性。
- 测试集通常占据剩余的数据,例如10%到20%。
ps:验证集是训练完一轮就进行验证模型效果的,也是对模型不可见的数据(但是一般是自己从训练集中取一小部分当验证集),用于防止过拟合等情况。而测试集在整个模型开发过程中应该是完全隔离的(过程中完全看不到),直到模型的所有训练和验证过程结束,再进行测试,用于最终评估模型的泛化能力。它代表了模型在实际应用中可能遇到的全新数据。测试集的结果通常用来发布模型的性能报告或作为模型选择的最终依据
分批训练数据优劣
一次取合适大小的一批数据(称为一个批次或batch)进行训练的做法是为了在计算效率和内存限制之间取得平衡。
如果一批训练的数据太大,虽然梯度下降计算的更稳定,但是可能超过内存限制,并且,这种稳定性有时也可能导致梯度估计过于平滑,使得模型在参数空间中的搜索变得过于保守。这可能导致模型陷入局部最小值,而不是全局最小值,从而错过了最优解。
如果一批训练的数据太小,由于每次迭代处理的数据量较少,小批次可能导致训练过程中的计算效率降低。在某些情况下,这可能意味着需要更多的迭代次数来完成训练,从而增加了总体的训练时间。且内存的利用率也较低。
在训练集中取一批批的数据,每一批数据都计算出一个loss,并不断通过将梯度下降等方法训练更新数据,直到训练完一轮训练集。
新冠病毒人数预测实战
背景
数据集 Dataset
数据集构成
dataset类由三个函数构成:
-
初始化__init__(self, *args, **kwargs)
:- 这是类的构造函数,用于初始化数据集对象。在这个函数中,你可以加载数据、设置数据预处理步骤、定义数据集的参数等。
- 这个方法会在创建数据集对象时被调用。
-
取数据__getitem__(self, index)
:- 这个方法用于获取数据集中指定索引位置的单个样本。当数据集被迭代时,每次迭代都会调用这个方法来获取下一个样本。
- 在这个方法中,你需要根据传入的索引
index
返回对应的数据样本,这通常包括特征数据和标签(如果有的话)。
-
返回数据集长度__len__(self)
:- 这个方法返回数据集中样本的数量。它通常在需要知道数据集大小的情况下被调用,例如在设置批处理大小或迭代次数时。
- 在这个方法中,你应该返回数据集中样本的总数。
本数据集采用的是独热编码,下面是独热编码的概念:
独热编码 one-hot
独热编码(One-Hot Encoding)是一种将分类变量转换为机器学习模型可以理解的格式的方法。在独热编码中,每个类别都被表示为一个二进制向量,向量的长度等于类别的总数。向量中的每个元素对应一个类别,如果该类别是当前样本的类别,则对应位置的值为1,否则为0。
假设我们有一个4个样本和3个类别的数据集,其中第一行表示类别,第一列表示样本数量。我们可以这样表示:
类别: A, B, C
样本数量: 1, 2, 1
现在,我们用独热编码来表示每个样本。首先,我们需要为每个类别创建一个独热向量。因为我们有3个类别,所以每个独热向量的长度是3。
对于样本1(假设它属于类别A),独热编码如下:
样本1 (A): [1, 0, 0]
对于样本2(假设它属于类别B),独热编码如下:
样本2 (B): [0, 1, 0]
对于样本3(假设它属于类别C),独热编码如下:
样本3 (C): [0, 0, 1]
对于样本4(假设它属于类别B),独热编码如下:
样本4 (B): [0, 1, 0]
现在,我们可以将这些独热编码组合成一个4x3的矩阵,其中每一行代表一个样本,每一列代表一个类别的独热编码:
样本 | 类别A | 类别B | 类别C |
---|---|---|---|
样本1 | 1 | 0 | 0 |
样本2 | 0 | 1 | 0 |
样本3 | 0 | 0 | 1 |
样本4 | 0 | 1 | 0 |
在这个例子中,我们可以看到,每个样本都被表示为一个长度为3的二进制向量,向量中的1表示样本所属的类别。这样,即使是非数值的数据,也可以被机器学习模型所理解和处理。
新冠项目的数据集就是采用的独热编码。一共90多个特征(90多列),前40列表示40个州,该列下方为1的表示此行样本数据是来自该州的(独热编码),一共2700行,即2700个样本(每一个州取了六七十个样本),后面50多列是不同的特征(如戴口罩的人数,感染人数等),其中训练集train.csv的数据中包括三列tested_positive(感染人数),也就是三天的感染人数,而测试集只有两列tested_positive,即需要根据模型去推算第三天的感染人数。即在训练集中,最后一列tested_positive相当于标签y,而其他特征是输入x。
数据集类代码(取数据集)
class covidDataset(Dataset):
def __init__(self, file_path, mode, dim=4, all_feature=False):
with open(file_path, "r") as f:
csv_data = list(csv.reader(f))
data = np.array(csv_data[1:]) # 将第一行,第一列去掉(特征的名称是无用数据)
# 根据数据集的模式(训练或验证)创建一个索引列表。
if mode == "train": # 训练数据逢5选4, 记录他们的所在行
indices = [i for i in range(len(data)) if i % 5 !=0] # 1,2,3,4, 6,7,8,9
elif mode == "val": # 验证数据逢5选1, 记录他们的所在列
indices = [i for i in range(len(data)) if i % 5 ==0]
if all_feature:
col_idx = [i for i in range(0,93)] # 若全选,则选中所有列。
else:
_, col_idx = get_feature_importance(data[:,1:-1], data[:,-1], k=dim,column =csv_data[0][1:-1]) # 选重要的dim列。
# 处理特征和标签数据。对于测试模式,只处理特征数据(不包括标签),对于训练和验证模式,选择指定的行和列
if mode == "test":
x = data[:, 1:].astype(float) # 测试集没标签,取第二列开始后面的数据(第一列没用),并转为float
x = torch.tensor(x[:, col_idx]) # col_idx表示了选取的列,转为张量
else:
x = data[indices, 1:-1].astype(float)
x = torch.tensor(x[:, col_idx])
y = data[indices, -1].astype(float) # 训练接和验证集有标签,取最后一列的数据为标签,并转为float
self.y = torch.tensor(y) # 转为张量
self.x = (x-x.mean(dim=0,keepdim=True))/x.std(dim=0,keepdim=True) # 对数据进行列归一化 0正太分布
self.mode = mode # 表示当前数据集的模式
def __getitem__(self, item):
if self.mode == "test":
return self.x[item].float() # 测试集没标签。 注意data要转为模型需要的float32型
else: # 否则要返回带标签y的数据
return self.x[item].float(), self.y[item].float()
def __len__(self):
return len(self.x) # 返回数据长度
定义模型 Model
模型类一般包含两个函数:
__init__
初始化
- 目的:初始化模型的权重和偏置,以及定义模型的结构。
- 参数:通常包括输入数据的维度、隐藏层的维度、输出层的维度等。
forward
前向传播
- 目的:定义数据在模型中的前向传播过程,即如何将输入数据转换为输出。
- 参数:通常是一个输入张量(例如,一个批次的图像或文本数据)。
全连接神经网络重点关注两件事情:维度变化Linear() 和 参数量。
这里第一层网络维度是 Linear(93,128),参数量为93*128(w)+128(b)
第二层网络维度是 Linear(128,1),参数量为1*128(w)+1(b)
# 模型
class myNet(nn.Module):
# 初始化模型
def __init__(self, inDim):
super(myNet,self).__init__()
# 定义了两层全连接神经网络 维度 dim -> 128(relu) -> 1
self.fc1 = nn.Linear(inDim, 128) # 全连接
self.relu = nn.ReLU() # 定义激活函数relu(在正数部分输出值本身,在负数部分输出0)
# self.fc3 = nn.Linear(128, 128)
self.fc2 = nn.Linear(128,1) # 全连接
# 前向传播过程
def forward(self, x):
x = self.fc1(x) # 通过第一个全连接层fc1
x = self.relu(x)
# x = self.fc3(x)
x = self.fc2(x)
if len(x.size()) > 1:
return x.squeeze(1)
else:
return x
模型训练函数(包含验证)
# 训练模型
def train_val(model, trainloader, valloader,optimizer, loss, epoch, device, save_):
# trainloader = DataLoader(trainset,batch_size=batch,shuffle=True)
# valloader = DataLoader(valset,batch_size=batch,shuffle=True)
model = model.to(device) # 模型和数据 ,要在一个设备上。 cpu - gpu
plt_train_loss = [] # 记录各种数据集的loss变化
plt_val_loss = []
val_rel = []
min_val_loss = 100000 # 记录训练验证loss,用此变量保存最小loss值,即最优模型的loss
for i in range(epoch): # 训练epoch 轮
start_time = time.time() # 记录开始时间
model.train() # 模型设置为训练状态 (可能测试状态关闭)
train_loss = 0.0 # 一批数据的loss之和,后面要除以数据长度的
val_loss = 0.0
for data in trainloader: # 从训练集取一个batch的数据,这里数据生成器不用自己写了,直接导包就行
optimizer.zero_grad() # 梯度清0
x, target = data[0].to(device), data[1].to(device) # 将数据放到设备上
pred = model(x) # 用模型预测数据
bat_loss = loss(pred, target) # 计算loss(单个)
bat_loss.backward() # 梯度回传, 反向传播。
optimizer.step() # 用优化器更新模型。 轮到SGD出手了
train_loss += bat_loss.detach().cpu().item() # 记录一批loss和,.cpu().item() 是为了把张量以数据取出来
plt_train_loss. append(train_loss/trainloader.dataset.__len__()) # 记录loss到列表。注意是平均的loss ,因此要除以数据集长度。
# 验证模型(基本上把train换为val就行)
model.eval() # 训练完一批,开始验证,模型设置为验证状态
with torch.no_grad(): # 模型不再计算梯度(如果不加这句,只要经过模型就会计算梯度)
for data in valloader: # 从验证集取一个batch的数据
val_x , val_target = data[0].to(device), data[1].to(device) # 将数据放到设备上
val_pred = model(val_x) # 用模型预测数据
val_bat_loss = loss(val_pred, val_target) # 计算loss
val_loss += val_bat_loss.detach().cpu().item() # 计算loss
val_rel.append(val_pred) # 记录预测结果
if val_loss < min_val_loss:
torch.save(model, save_) # 如果loss比之前的最小值小,说明模型更优,保存这个模型
plt_val_loss.append(val_loss/valloader.dataset.__len__()) # 记录loss到列表。注意是平均的loss ,因此要除以数据集长度。
#
print('[%03d/%03d] %2.2f sec(s) TrainLoss : %.6f | valLoss: %.6f' % \
(i, epoch, time.time()-start_time, plt_train_loss[-1], plt_val_loss[-1])
) # 打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数,后面可以对应。[-1]表示最后一列,即loss的最新值
# print('[%03d/%03d] %2.2f sec(s) TrainLoss : %3.6f | valLoss: %.6f' % \
# (i, epoch, time.time()-start_time, 2210.2255411, plt_val_loss[-1])
# ) #打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。
plt.plot(plt_train_loss) # 画图, 向图中放入训练loss数据
plt.plot(plt_val_loss) # 画图, 向图中放入训练loss数据
plt.title('loss') # 画图, 标题
plt.legend(['train', 'val']) # 画图, 图例
plt.show() # 画图, 展示
测试模型(最终评估)
将测试集通过模型得出最终的结果
# 模型评估(测试集通过模型测试最终结果到 pred.csv)
def evaluate(model_path, testset, rel_path ,device):
model = torch.load(model_path).to(device) # 模型放到设备上。 加载模型
testloader = DataLoader(testset, batch_size=1, shuffle=False) # 将验证数据放入loader 验证时, 一般batch为1
val_rel = []
model.eval() # 模型设置为验证状态
with torch.no_grad(): # 模型不再计算梯度
for data in testloader: # 从测试集取一个batch的数据
x = data.to(device) # 将数据放到设备上
pred = model(x) # 用模型预测数据
val_rel.append(pred.item()) # 记录预测结果
print(val_rel) # 打印预测结果
with open(rel_path, 'w') as f: # 打开保存的文件
csv_writer = csv.writer(f) # 初始化一个写文件器 writer
csv_writer.writerow(['id', 'tested_positive']) # 在第一行写上 “id” 和 “tested_positive”
for i in range(len(testset)): # 把测试结果的每一行放入输出的excel表中。
csv_writer.writerow([str(i),str(val_rel[i])])
print("rel已经保存到"+ rel_path)
正则化 regularization
正则化原理的核心思想是在模型训练过程中,通过在损失函数中添加一个额外的惩罚项(正则项)来限制模型的复杂度。这样做的目的是防止模型过度拟合训练数据,从而提高模型在未见过的测试数据上的泛化能力。
公式中参数前面的正则化系数也是一种超参数(很小)
原理:不论哪种正则化,都是在原本的loss后面多加了一个关于参数的正则项,我们训练模型就是为了让loss减小,可是常常由于加入了过多的参数量虽然使得loss变小了,但是模型过于复杂出现过拟合。
通过正则化在loss的表达式中加入关于参数的正则项后,这样一来就不仅要考虑loss值,还要考虑正则项也不能太大,即限制了参数量不能太大(模型不能太复杂),从而防止过拟合。
def mseLoss(pred, target, model):
loss = nn.MSELoss(reduction='mean') # 原来的loss(平方差损失)
''' Calculate loss '''
regularization_loss = 0 # 正则项
for param in model.parameters():
# TODO: you may implement L1/L2 regularization here
# 使用L2正则项
# regularization_loss += torch.sum(abs(param))
regularization_loss += torch.sum(param ** 2) # 计算所有参数平方
return loss(pred, target) + 0.00075 * regularization_loss # 在原来的loss后加正则项,返回新的损失。
loss = mseLoss