导入库方面略过不提
第一部分:简易日期处理模块
def processdate(date):
date_num = (int(date[:4]) - 2014)*12 + (int(date[4:6])-5)
return date_num
processdate
函数
这个函数的作用是将输入的日期字符串转化为一个整数。它将日期中的年份和月份信息分别转化为从2014年开始的月数。例如,对于输入 '201806'
,函数会返回 (2018-2014)*12 + (6-5) = 48 + 1 = 49
。
日期从2014年5月开始,所以这个模块会计算出距离2014年5月过去了多少个月。
第二部分:数据集
class houseDataset(Dataset):
def __init__(self, path, mode="train", feature_dim=5):
with open(path,'r') as f:
csv_data = list(csv.reader(f))
for index in range(1, len(csv_data)):
csv_data[index][1] = processdate(csv_data[index][1]) //日期处理
csv_data[index][2] = str(eval(csv_data[index][2])) //这里的数据是价格,转为字符串形式
x = np.delete(np.array(csv_data)[1:].astype(float), [0, 2, 16], axis=1)
//这里处理了特征项。去掉了第一行也就是特征的名称,然后去掉了0列(id)以及2列(price,实际上是每一组x对应的target)以及16列(zipcode,邮政编码。由于难以处理且在特征中无不可替代性而直接删去)。处理完后将这些数据转换为np的数组并转为浮点形式。
y = np.array(csv_data)[1:, 2].astype(float)/10**6
//如上所述,第二列实际上是我们的target。取出target后除以10^6,将价格转为一个较小的数方便对比。
self.x = torch.tensor(x)
self.y = torch.tensor(y)
self.x = (self.x - self.x.mean(dim=0,keepdim=True)) / self.x.std(dim=0,keepdim=True) //特征归一化
print('Finished reading the {} set of house Dataset ({} samples found)'
.format(mode, len(self.x)))
def __getitem__(self, item):
return self.x[item].float(), self.y[item]
def __len__(self):
return len(self.x)
houseDataset
类
这个类用于从CSV文件中加载和处理数据。其中:
__init__
方法首先读取CSV文件中的数据,并逐行进行处理。对于每行的第二个元素(索引为1的元素,即日期),使用 processdate
函数进行处理;对于每行的第三个元素,使用 eval
函数进行处理(注意这里使用 eval
可能存在安全风险,如果数据源不可靠的话)。然后,该方法创建输入 x
和目标输出 y
,并对输入数据进行标准化处理。
__getitem__
方法用于返回给定索引处的数据样本。
__len__
方法返回数据集的长度。
笔记:数据处理逻辑已于代码注释给出,init函数实际上完成了数据集的导入、处理,并且将处理完的x和y作为数据集类型的全局变量供其它函数使用。getitem和len函数非常简单,实现的就是返回对应xy和数据集长度。
第三部分:用于预测的神经网络模型
class myNet(nn.Module):
def __init__(self,inDim):
super(myNet,self).__init__()//使用super修改nn.Module的初始化,这个步骤在数据集中就不用实现。
self.fc1 = nn.Linear(inDim, 64)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(64, 1)
# self.fc3 = nn.Linear(256,256)
# self.fc4 = nn.Linear(inDim,1) /*inDim是你的特征维数,比如说你有5个特征,那么inDim就该输入为5。这里自定义了这个预测模型的网络层数,在未注释的版本中是将输入维度经过全连接层fc1,变化为64维的输出,此输出经过relu函数的非线性变换后再进入全连接层fc2,将64维降为1维即可输出。如果你有需要,可以像注释的两行一样自定义更多层数*/
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
# x = self.relu(x)
#
# x = self.fc3(x)
# x = self.relu(x)
#
# x = self.fc4(x)
# x = self.relu(x)
if len(x.size()) > 1:
return x.squeeze(1) //将经过网络的x降为一维,方便与y作对比。
else:
return x
myNet
类
这是一个用于房价预测的神经网络模型。其中:
__init__
方法定义了网络的结构。该网络有两个全连接层(fc1
和 fc2
),并在两层之间使用ReLU激活函数。
forward
方法定义了数据在网络中的前向传播过程。输入数据首先通过第一个全连接层 fc1
,然后经过ReLU激活函数,再通过第二个全连接层 fc2
。如果输出的维度大于1,就使用 squeeze
方法去掉维度为1的维度。
注意:
代码中有一些被注释掉的代码,这些代码可能是用于测试或调试的,但在当前的实现中并未使用。 在houseDataset
类中,数据被标准化(即进行了Z-score归一化),这是一种常见的数据预处理方法,有助于模型更好地学习和收敛。
神经网络模型 myNet
的结构相对简单,可能适合处理一些较为简单的房价预测任务。如果需要处理更复杂的任务,可能需要增加更多的层或调整网络结构。
第四部分:训练与验证
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)
plt_train_loss = []
plt_val_loss = []
val_rel = []
min_val_loss = 100000
for i in range(epoch):
start_time = time.time()
model.train()
train_loss = 0.0
val_loss = 0.0 //记录时间,进入训练模式并初始化训练损失和验证损失。
for data in trainloader:
optimizer.zero_grad()
x, target = data[0].to(device), data[1].to(torch.float32).to(device)
pred = model(x)
bat_loss = loss(pred, target, model)
bat_loss.backward()
optimizer.step()
train_loss += bat_loss.detach().cpu().item()
//在一个epoch中,每组数据首先清零上一组遗留的梯度,然后将x和y放到机器上。
//接下来在model中传入特征x,计算预测值pred并计算每一个batch的损失。计算完成后将损失反向传播,并使用优化器更新模型。最后累加单个批次的损失,先将这个批次的loss取下并用item取值。
plt_train_loss. append(train_loss/trainloader.dataset.__len__())
//累加每个批次的损失,除以一个批次的训练集长度得平均损失。
model.eval()
with torch.no_grad():
for data in valloader:
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, model)
val_loss += val_bat_loss
val_rel.append(val_pred)
if val_loss < min_val_loss:
torch.save(model, save_)
//验证模式与训练模式大同小异。重要区别在于,验证时由于数据集不可用于改进模型,因此应该在无梯度计算下运行,同时也不需要反向传播算法。如果本次验证集的损失小于曾经记录的最小损失,更新这个值并保存这次训练的模型。
plt_val_loss. append(val_loss/valloader.dataset.__len__())
print('[%03d/%03d] %2.2f sec(s) TrainLoss : %3.6f | valLoss: %3.6f' % \
(i, epoch, time.time()-start_time, plt_train_loss[-1], plt_val_loss[-1])
)
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title('loss')
plt.legend(['train', 'val'])
plt.show()
//打印训练成果和损失图
这段代码实现了一个基本的训练和验证过程,用于训练一个神经网络模型(由参数`model`提供)。它使用给定的训练数据加载器(`trainloader`)和验证数据加载器(`valloader`)来分别加载训练数据和验证数据。以下是代码的主要功能和步骤:
1. **设置模型和设备**:首先,它将模型发送到指定的设备(通常是GPU,如果可用)。
model = model.to(device)
2. **初始化损失列表**:初始化三个列表来存储训练损失、验证损失和验证预测。
plt_train_loss = []
plt_val_loss = []
val_rel = []
3. **开始训练循环**:对于每一个epoch(由参数`epoch`提供),代码执行以下操作:
* **重置损失**:在开始每个epoch之前,重置训练损失和验证损失。
python`train_loss = 0.0
val_loss = 0.0`
* **训练阶段**:设置模型为训练模式,并对`trainloader`中的每个数据批次执行以下操作:
+ 清零梯度。
+ 将数据和目标加载到设备上。
+ 通过模型进行前向传播以获取预测。
+ 计算损失。
+ 反向传播损失。
+ 更新模型权重。
+ 累加训练损失。
* **计算平均训练损失**:在每个epoch结束时,计算平均训练损失并存储在`plt_train_loss`列表中。
python`plt_train_loss.append(train_loss/trainloader.dataset.__len__())
* **验证阶段**:设置模型为评估模式,并对`valloader`中的每个数据批次执行以下操作:
+ 将验证数据加载到设备上。
+ 通过模型进行前向传播以获取预测。
+ 计算验证损失。
+ 累加验证损失。
+ 存储验证预测。
* **检查并保存最佳模型**:如果当前验证损失低于之前的最小验证损失,则保存模型。
python`if val_loss < min_val_loss:
torch.save(model, save_)`
* **计算平均验证损失**:在每个epoch结束时,计算平均验证损失并存储在`plt_val_loss`列表中。
plt_val_loss.append(val_loss/valloader.dataset.__len__())
* **打印训练和验证损失**:打印每个epoch的训练损失、验证损失和训练时间。
4. **绘制损失图**:在训练结束后,使用matplotlib绘制训练和验证损失的图。
```python
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title('loss')
plt.legend(['train', 'val'])
plt.show()
```
总的来说,这段代码实现了神经网络模型的训练、验证和损失的可视化。它使用训练数据来更新模型权重,并使用验证数据来评估模型的性能。在每个epoch结束时,它保存了平均训练损失和平均验证损失,并在训练结束后绘制了损失图。
第五部分:事前准备
device = 'cuda' if torch.cuda.is_available() else 'cpu'
train_path = 'kc_house_data.csv'
house_data = pd.read_csv(train_path)
print(house_data.head())
house_data.info()
print(house_data.describe())
house_dataset = houseDataset(train_path)
train_set, val_set = random_split(house_dataset,[int(len(house_dataset)*0.8),len(house_dataset)- int(len(house_dataset)*0.8)])
print(train_set.indices)
简单点讲,完成了数据集的导入,并且将内容划分为训练集和验证集。做完这些后,打印了数据集的一些初始状态。
第六部分:损失函数
def mseLoss(pred, target, model):
loss = nn.MSELoss(reduction='mean')
''' 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 = mseLoss
Q:发散:关于L2正则化与L1正则化?
A:L2正则化在机器学习和统计建模中起着关键的作用,尤其在控制模型的复杂度和提高泛化能力方面。具体来说,L2正则化有以下几个主要作用:
减少过拟合:过拟合是当模型在训练数据上表现非常好,但在新数据或测试数据上表现较差时发生的现象。L2正则化通过约束模型参数的大小,使其不会过于复杂,从而减少了过拟合的风险。这有助于模型更好地泛化到未见过的数据上。 控制模型复杂度:L2正则化通过在损失函数中添加模型参数的L2范数(即参数平方和的平方根),使得在优化过程中,参数值会倾向于变得更小。这有助于降低模型的复杂度,因为模型不再依赖于某些特定的、可能只在训练数据中出现的特征。那么,为什么L2正则化会起效呢?这主要是基于以下几个原因:
奥卡姆剃刀原则:这个原则强调在多个可能的解释中,应选择最简单的那个。在机器学习中,这通常意味着更简单的模型(即参数值更小的模型)更可能是正确的模型。L2正则化通过鼓励参数值变小,从而遵循了这一原则。 数值稳定性:较小的参数值通常意味着模型对输入数据的微小变化不那么敏感。这有助于提高模型的数值稳定性,减少因数据微小变化而导致的模型输出的大幅波动。 权重衰减:L2正则化也被称为权重衰减,因为它在每次更新参数时都会减去一个与参数值成正比的项。这会导致参数值逐渐减小,从而降低模型的复杂度。总的来说,L2正则化通过约束模型参数的大小,使其更加简单和稳定,从而提高模型的泛化能力。这是L2正则化在机器学习和统计建模中广泛应用的原因。
L1正则化是另一种在机器学习中常用的正则化方法,它与L2正则化在目标、方式和效果上都有所不同。
L1正则化是在损失函数的基础上,加上所有参数的绝对值之和(乘以一个常数),用于惩罚参数过大,促使模型更加稀疏化。它的主要目标是使模型参数尽量接近0,甚至变为0,从而达到特征选择的效果。在一些需要特征选择的问题上,L1正则化表现得更加优秀。然而,由于L1正则化的导数在0时不可导,使得优化问题更加复杂。
与L2正则化相比,L1正则化有以下几个主要区别:
稀疏性:L1正则化倾向于产生稀疏模型,将不重要的特征的权重压缩为零,从而可以作为特征选择的手段。而L2正则化则不会将权重完全压缩为零,而是将它们压缩到一个小的范围内,所有特征都会有不为零的权重。 解的少数性:在某些情况下,L1正则化可能存在多个解。而L2正则化通常具有少数解,尤其是当特征数量少于样本数量时。 计算复杂性:由于L1正则化倾向于产生稀疏模型,在某些算法中可能更难优化。而L2正则化的梯度是连续的,数学上更易处理和优化。 对于异常值的鲁棒性:L1正则化对异常值更鲁棒,因为它不会过分地惩罚大的权重。此外,L1正则化和L2正则化在模型预测准确性方面也有所不同。L1正则化通过将参数压缩到极小值,可以抑制对数据的过拟合,因此会大大提升模型的泛化能力,但可能会使模型的预测准确率下降。而L2正则化不会完全抑制模型参数的系数,因此可能会减少模型的泛化能力,但有助于提升模型的预测准确性。
总的来说,L1正则化和L2正则化都是控制模型复杂度和防止过拟合的有效手段,但它们在实现方式、效果和应用场景上有所区别。选择哪种正则化方法需要根据具体的问题和实验结果来决定。
一些个人理解:两种正则化方法都试图降低参数大小。这种方法能够避免过拟合的原因是当你的模型出现过拟合时,它通常尝试模拟*所有*出现的特征点与标签的关系。我们知道,有时你的训练集中会出现明显过于偏移正常值的异常点。没有过拟合的函数将不会太受到此类异常点的影响,然而过拟合的函数由于其试图模拟所有点位,因此会导致模拟的结果异常的向此类异常点偏移,从而导致一些不应被考虑的特征点在模型训练中却占据了很大权重。
落实到参数时,理解这样一个场景:过拟合的模型由于过于复杂并试图模拟所有点位,因此其参数值将会异常偏大,导致他们对于异常特征点位更加敏感。较大的w与较大的x相乘,结果自然会对模型训练造成很大影响。正则化就是试图减少这种情况的发生。
再顺便一提:文心一言讲到L1正则化还可以用于选择最佳特征,这种特性是在模型的训练过程中自然而然的进行的。与此同时,还有一种用于选择最佳特征的方法SKB(Selective K-Best),可以在进行训练之前人为的选出最佳特征。如果你的数据集比较复杂,而你希望训练结果是一个相对稀疏、易于理解的模型,你可以选择L1正则化;如果你的数据集相对简单,或者你有自己的偏好,希望从一些特定角度理解特征和y之间的关系,可以考虑SKB。
第七部分:尾声
config = {
'n_epochs': 50, # maximum number of epochs
'batch_size': 25, # mini-batch size for dataloader
'optimizer': 'SGD', # optimization algorithm (optimizer in torch.optim)
'optim_hparas': { # hyper-parameters for the optimizer (depends on which optimizer you are using)
'lr': 0.0001, # learning rate of SGD
'momentum': 0.9 # momentum for SGD
},
'early_stop': 200, # early stopping epochs (the number epochs since your model's last improvement)
'save_path': 'model_save/model.pth', # your model will be saved here
}
model = myNet(18).to(device)
# optimizer = optim.SGD(model.parameters(), lr=0.03,momentum=0.9)
optimizer = torch.optim.Adam(model.parameters(),
lr=0.03,
weight_decay=0.001) #换一个优化器
trainloader = DataLoader(train_set,batch_size=config['batch_size'], shuffle=True)
valloader = DataLoader(val_set,batch_size=config['batch_size'], shuffle=True)
train_val(model, trainloader,valloader,optimizer, loss, config['n_epochs'], device,save_=config['save_path'])
这部分就是config的设置以及优化器的设置,然后进行一个训练的跑。