上一篇文章介绍了一个最基础的神经网络需要怎么构成,这一篇文章主要介绍的是如何用pytorch框架去实现这一个过程,并且还附有李宏毅老师作业HW1的代码分析。
PyTorch与TensorFlow都是深度学习的框架,他们两个有相似的地方,都是现在十分常用的两个框架。
那他们两个有什么区别呢?下面一张表格可以给出一个总括:
框架 | PyTorch | TensorFlow |
---|---|---|
实现方式 | 命令式编程 | 符号式编程 |
图的定义 | 动态图 | 静态图 |
效率 | 偏低 | 偏高 |
难度 | 偏低 | 偏高 |
所谓命令式编程,也就是说直观的去编写代码,并不需要把整个代码框架搭建好后再输入数据进行计算,十分的直观,非常易于调试。而符号式编程则需要先把代码的框架逻辑搭好,最后再输入数据,实现代码,这种编程不可交互,但是效率十分的高。
为完成深度学习的基础代码我们需要引入一些库来帮助我们去完成
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
Dataset负责告诉程序数据在哪,每个索引所对应的数据是什么,用于存放样本和预测值。
DataLoader则是用于对数据进行分组,对偌大的数据集中随机抽取出大小为设定值为batch的个数,重新分为一组,然后形成一个新的数据集。
可以看到,本来一个大的数据集被拆分成多个数据集的合并。
其中的shuffle参数用于控制是否随机抽取,如果设置为False,则每次抽取组成的新数据集是一样的
class MyDataset(Dataset):
def __init__(self):
self.data=... //这里用于读入数据集并且标记索引
def __getitem__(self, index):
return self.data[index] //这里用于查找相应索引的数据
def __len__(self):
return len(self.data) //返回数据集的总长度
dataset=MyDataset(file)
dataloader=DataLoader(dataset,batch_size=4,shuffle=True)
在构造完数据集后,我们还需要知道如何进行梯度下降的计算,这是LossFunction最优化的核心部分。
首先我们得知道一个很重要的参数:require_grad
一个tensor设置require_grad为True的情况下,才会对这个tensor以及由这个tensor计算出来的其他tensor求导,并将导数值存在tensor的grad属性中,便于优化器来更新参数
然后我们要知道另外一个函数:backward()
这个函数用于求导,但是他接受的必须是一个标量,以下面例子为由,如果z不是标量,会令该函数报错,这时候需要在backward()里面设定矢量的参数,作为矢量的权值,例如z.backward(torch.tensor([2.,2.,2.]))这样就可以为一个三元的矢量求导。但是每次求导完后,需要清零optimizer.zero_grad(),否则会继续接着上一次继续求导
我们举一个简单的例子:
x=torch.tensor([[1.,0.],[-1.,1.],require_grad=True)
z=x.pow(2).sum()
z.backward()
print(x.grad)
我们知道神经网络是一个连接层减少的一个过程,最后的结果只会输出一层,所以我们构造神经网络也要按着这个逻辑去进行构造。
首先我们得了解一个重要函数:nn.Sequential()
这个函数是一个容器,用于搭建神经网络的模块被按照被传入构造器的顺序添加到nn.Sequential()容器中,利用nn.Sequential()搭建好模型架构,模型前向传播时调用forward()方法,模型接收的输入首先被传入nn.Sequential()包含的第一个网络模块中。然后,第一个网络模块的输出传入第二个网络模块作为输入,按照顺序依次计算并传播,直到nn.Sequential()里的最后一个模块输出结果。
class MyModel(nn.Module):
def _init_(self):
super(MyModel,self)._init_()
self.net = nn.Sequential(
nn.Linear(10,32), //L1
nn.Sigmoid(), //L2
nn.Linear(32,1) //L3
)
def forward(self,x):
return self.net(x) //等价于输出L1(x),L2(L1),L3(L2)
接下来我们需要构造我们的LossFunction了,一般可以选择MSE和交叉熵
//Mean Squared Error (for regression tasks)
criterion = nn.MSELoss()
//Cross Entropy (for classification tasks)
criterion = nn.CrossEntropyLoss()
loss = criterion(model_output, expected_value)
然后我们需要实现最佳化,也就是调参,这里我们选择SGD
torch.optim.SGD(model.parameters(), lr, momentum = 0)
对于每次调参都必须要进行梯度清零optimizer.zero_grad(),梯度下降求导loss.backward(),还有调整参数optimizer.step()。
//part1 准备工作
dataset = MyDataset(File)
tr_set = DataLoader(dataset,16,shuffle=True)
model = Mymodel().to(device) //选择cpu还是gpu
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameter(),0.1)
//part2 训练模型
for epoch in range(n_epochs):
model.train()
for x,y in tr_set:
optimizer.zero_grad()
x, y = x.to(device), y.to(device)
pred = model(x)
loss = criterion(pred,y)
loss.backward()
optimizer.step()
//part3 验证模型,便于检测模型是否有过拟合,从而调整超参数,优化训练而添加的网络层会被关闭,从而使得评估时不会发生偏移。
model.eval() //进入验证模式
total_loss = 0
for x, y in dv_set:
x, y = x.to(device), y.to(device)
with torch.no_grad(): //用于屏蔽梯度下降,强制不进行梯度下降的计算,也不会生成动图,从而避免过多的训练
pred = model(x)
loss = criterion(pred,y)
total_loss += loss.cpu().item() * len(x)
avg_loss = total_loss / len(dv_set.dataset)
//part4 测试模型,加入测试集,看看结果
model.eval()
preds=[]
for x in tt_set:
x=x.to(device)
with torch.no_grad():
pred=model(x)
preds.append(pred.cpu())
//part5 保存模型和加载模型(可注释掉)
torch.save(model.state_dict(), path)
ckpt = torch.load(path)
model.load_state_dict(ckpt)
使用相同的网络结构,即使用的学习率,迭代次数,batch size 都是一样,但最后跑出来的结果却不尽相同,这个问题在CPU中是不会出现的,但是如果在GPU上跑,是有可能出现这个问题,这是因为GPU会提供了多核并行计算的基础结构,调用额外的随机源,使得结果不能准确再现。所以固定我们的种子函数就十分的重要了。
torch.backends.cudnn.deterministic
将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。
torch.backends.cudnn.benchmark
这个参数会让程序花费一定的时间,为整个网络的每个卷积层搜寻最适合它的算法,从而实现网络的加速,但是一般PyTorch是默认使用cuDnn加速的,所以这个参数一般设置为False。并且该参数有一个致命的问题,如果神经网络的结构是动态变化的,则每次都需要进行预优化,选出最合适的算法,这样就大大降低了效率。
torch.manual_seed(seed)
这个是一个产生随机数的种子库,调用后,后面使用torch.rand(1)后就会依循该种子库去生成我们的随机数。
def same_seed(seed):
'''Fixes random number generator seeds for reproducibility.'''
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
我们生成随机数的最主要目的是分割我们的数据集,以达到获取训练集,验证集的效果,这边选择存入三个参数数据集,验证值的比率,还有种子标号。
random_split(dataset, lengths, generator=
这个函数需要我们提供数据集dataset,分割的长度length,以列表储存,还有随机种子的生成器torch.Generator(),就能帮助我们自动分割了,并且分割出来的数据类型是np.array型
def train_valid_split(data_set, valid_ratio, seed):
'''Split provided training data into training set and validation set'''
valid_set_size = int(valid_ratio * len(data_set))
train_set_size = len(data_set) - valid_set_size
train_set, valid_set = random_split(data_set, [train_set_size, valid_set_size], generator=torch.Generator().manual_seed(seed))
return np.array(train_set), np.array(valid_set)
预测函数与上面基础代码类似,这里就不作阐述
def predict(test_loader, model, device):
model.eval() # Set your model to evaluation mode.
preds = []
for x in tqdm(test_loader):
x = x.to(device)
with torch.no_grad():
pred = model(x)
preds.append(pred.detach().cpu())
preds = torch.cat(preds, dim=0).numpy()
return preds
这里前面也已经阐述,这里就不过多阐述
class COVID19Dataset(Dataset):
'''
x: Features.
y: Targets, if none, do prediction.
'''
def __init__(self, x, y=None):
if y is None:
self.y = y
else:
self.y = torch.FloatTensor(y)
self.x = torch.FloatTensor(x)
def __getitem__(self, idx):
if self.y is None:
return self.x[idx]
else:
return self.x[idx], self.y[idx]
def __len__(self):
return len(self.x)
class My_Model(nn.Module):
def __init__(self, input_dim):
super(My_Model, self).__init__()
# TODO: modify model's structure, be aware of dimensions.
self.layers = nn.Sequential(
nn.Linear(input_dim, 16),
nn.ReLU(),
nn.Linear(16, 8),
nn.ReLU(),
nn.Linear(8, 1)
)
def forward(self, x):
x = self.layers(x)
x = x.squeeze(1) # (B, 1) -> (B)
return x
由于数据中有很多特征,所以我们也可以减少特征值去进行训练,避免过拟合
这里需要注意[:, -1]是指最后一列的所有元素,而[:, :-1]是到最后一列的所有元素
def select_feat(train_data, valid_data, test_data, select_all=True):
'''Selects useful features to perform regression'''
y_train, y_valid = train_data[:, -1], valid_data[:, -1]
raw_x_train, raw_x_valid, raw_x_test = train_data[:, :-1], valid_data[:, :-1], test_data
if select_all:
feat_idx = list(range(raw_x_train.shape[1]))
else:
feat_idx = [0, 1, 2, 3, 4] # TODO: Select suitable feature columns.
return raw_x_train[:, feat_idx], raw_x_valid[:, feat_idx], raw_x_test[:, feat_idx], y_train, y_valid
训练模型其实包括训练模型与验证模型两个方面。训练模型需要首先配置lossfunction跟optimizer,然后我们就可以按照之前的基础代码进行训练了,并且记录下每次训练的loss值,然后计算平均loss值,之后我们就需要进行验证,看看是否有过拟合,验证步骤与训练步骤其实十分相像。我们的这个模型还提供提前终止训练,如果多次都未能达到减少loss时,为了提高训练的效率,我们会选择终止训练,并且作出提醒。
tqdm(iterable)
这是一个进度条函数,只要在里面放入可迭代的对象,就可以显示进度条
.set_description(f’Epoch [{epoch+1}/{n_epochs}]')
.set_postfix({‘loss’: loss.detach().item()})
.set_description可以在进度条前面加上一个描述,.set_postfix则可以在进度条的后面加上一串描述,像这样子:
SummaryWriter()
用于建造一个tensorboard对象,后面方便调用tensorboard的函数实现可视化。
SummaryWriter().add_scalar(‘Loss/train’, mean_train_loss, step)
这是在tensorboard中创建图表,第一个参数是图表的名称,第二个是y轴数据,第三个参数是x轴数据
def trainer(train_loader, valid_loader, model, config, device):
criterion = nn.MSELoss(reduction='mean') # Define your loss function, do not modify this.
# Define your optimization algorithm.
# TODO: Please check https://pytorch.org/docs/stable/optim.html to get more available algorithms.
# TODO: L2 regularization (optimizer(weight decay...) or implement by your self).
optimizer = torch.optim.SGD(model.parameters(), lr=config['learning_rate'], momentum=0.9)
writer = SummaryWriter() # Writer of tensoboard.
if not os.path.isdir('./models'):
os.mkdir('./models') # Create directory of saving models.
n_epochs, best_loss, step, early_stop_count = config['n_epochs'], math.inf, 0, 0
for epoch in range(n_epochs):
model.train() # Set your model to train mode.
loss_record = []
# tqdm is a package to visualize your training progress.
train_pbar = tqdm(train_loader, position=0, leave=True)
for x, y in train_pbar:
optimizer.zero_grad() # Set gradient to zero.
x, y = x.to(device), y.to(device) # Move your data to device.
pred = model(x)
loss = criterion(pred, y)
loss.backward() # Compute gradient(backpropagation).
optimizer.step() # Update parameters.
step += 1
loss_record.append(loss.detach().item())
# Display current epoch number and loss on tqdm progress bar.
train_pbar.set_description(f'Epoch [{epoch+1}/{n_epochs}]')
train_pbar.set_postfix({'loss': loss.detach().item()})
mean_train_loss = sum(loss_record)/len(loss_record)
writer.add_scalar('Loss/train', mean_train_loss, step)
model.eval() # Set your model to evaluation mode.
loss_record = []
for x, y in valid_loader:
x, y = x.to(device), y.to(device)
with torch.no_grad():
pred = model(x)
loss = criterion(pred, y)
loss_record.append(loss.item())
mean_valid_loss = sum(loss_record)/len(loss_record)
print(f'Epoch [{epoch+1}/{n_epochs}]: Train loss: {mean_train_loss:.4f}, Valid loss: {mean_valid_loss:.4f}')
writer.add_scalar('Loss/valid', mean_valid_loss, step)
if mean_valid_loss < best_loss:
best_loss = mean_valid_loss
torch.save(model.state_dict(), config['save_path']) # Save your best model
print('Saving model with loss {:.3f}...'.format(best_loss))
early_stop_count = 0
else:
early_stop_count += 1
if early_stop_count >= config['early_stop']:
print('\nModel is not improving, so we halt the training session.')
return
我们需要配置一些超参数和普通的参数,为了方便查看就利用一个容器将其包裹起来。
device = 'cuda' if torch.cuda.is_available() else 'cpu'
config = {
'seed': 5201314, # Your seed number, you can pick your lucky number. :)
'select_all': True, # Whether to use all features.
'valid_ratio': 0.2, # validation_size = train_size * valid_ratio
'n_epochs': 3000, # Number of epochs.
'batch_size': 256,
'learning_rate': 1e-5,
'early_stop': 400, # If model has not improved for this many consecutive epochs, stop training.
'save_path': './models/model.ckpt' # Your model will be saved here.
}
首先,我们需要先导入训练集covid.train.csv和测试集covid.test.csv,然后利用随机种子对训练集进行分割,然后选择是否需要全部的特征值,之后就可以covid19的dataset封装好他们,再使用DataLoader进行小批量分割处理得到最后的DataLoader
# Set seed for reproducibility
same_seed(config['seed'])
# train_data size: 2699 x 118 (id + 37 states + 16 features x 5 days)
# test_data size: 1078 x 117 (without last day's positive rate)
train_data, test_data = pd.read_csv('./covid.train.csv').values, pd.read_csv('./covid.test.csv').values
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed'])
# Print out the data size.
print(f"""train_data size: {train_data.shape}
valid_data size: {valid_data.shape}
test_data size: {test_data.shape}""")
# Select features
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all'])
# Print out the number of features.
print(f'number of features: {x_train.shape[1]}')
train_dataset, valid_dataset, test_dataset = COVID19Dataset(x_train, y_train), \
COVID19Dataset(x_valid, y_valid), \
COVID19Dataset(x_test)
# Pytorch data loader loads pytorch dataset into batches.
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True)
model = My_Model(input_dim=x_train.shape[1]).to(device) # put your model and data on the same computation device.
trainer(train_loader, valid_loader, model, config, device)
def save_pred(preds, file):
''' Save predictions to specified file '''
with open(file, 'w') as fp:
writer = csv.writer(fp)
writer.writerow(['id', 'tested_positive'])
for i, p in enumerate(preds):
writer.writerow([i, p])
model = My_Model(input_dim=x_train.shape[1]).to(device)
model.load_state_dict(torch.load(config['save_path']))
preds = predict(test_loader, model, device)
save_pred(preds, 'pred.csv')
本篇文章介绍了李宏毅的深度学习入门编程代码以及第一个作业的源码分析,希望能让大家真正的入门深度学习,下面附有一张思维导图以便记忆。