前言
我是跟着李沐的动手学深度学习v2
视频学习深度学习的,光看不做假把式,所以在学习完第七章-现代卷积神经网络
之后,参加了一次李沐发布的Kaggle竞赛。自己动手,从组织数据集开始,到训练,再到推理,把整个流程走了一遍,收获颇丰,感觉学到了很多东西,(完整代码:https://www.kaggle.com/code/leyoubaloy/competition-classifyleaves/notebook?scriptVersionId=114980812 )现总结如下:
组织数据集
我相信很多小伙伴跟我一样,学习深度学习不是为了在这个领域做出创新性的贡献,而是仅仅会用它解决实际问题即可,但是在用别人的模型的过程中卡在了第一步——组织数据集。
先搞明白两个类,Dataset和Dataloader(需要有python面向对象知识基础)。Dataset是对整体数据集的一个抽象,它定义了数据的读取方法等属性,而Dataloader是用于训练和、评估和测试的迭代器,它能控制batch_size,shuffle等属性。Dataset代码的编写是很灵活的,针对不同的数据集有不同的写法,想要会用深度学习一定先学会写Dataset类。
class MyDataset(Dataset):
def __init__(self, mode='train', valid_ratio=0.2, resize_height=256, resize_width=256):
self.resize_height = resize_height
self.resize_width = resize_width
self.mode = mode
if mode == 'train':
data = pd.read_csv('/kaggle/input/classify-leaves/train.csv')
self.train_len = int(data.shape[0] * (1 - valid_ratio))
self.train_image = np.asarray(data.loc[:self.train_len, 'image'])
self.train_label = np.asarray(data.loc[:self.train_len, 'label'])
self.image_arr = self.train_image
self.label_arr = self.train_label
elif mode == 'valid':
data = pd.read_csv('/kaggle/input/classify-leaves/train.csv')
self.train_len = int(data.shape[0] * (1 - valid_ratio))
self.valid_image = np.asarray(data.loc[self.train_len:, 'image'])
self.valid_label = np.asarray(data.loc[self.train_len:, 'label'])
self.image_arr = self.valid_image
self.label_arr = self.valid_label
elif mode == 'test':
data = pd.read_csv('/kaggle/input/classify-leaves/test.csv')
self.test_image = np.asarray(data.loc[:, 'image'])
self.image_arr = self.test_image
self._len = len(self.image_arr)
def __getitem__(self, index):
image_path = self.image_arr[index]
image = Image.open(os.path.join('/kaggle/input/classify-leaves/', image_path))
if self.mode == 'train':
transform = transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
else:
transform = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
image = transform(image)
if self.mode == 'test':
return image
label = self.label_arr[index]
label_num = label2num[label]
return image, label_num
def __len__(self):
return self._len
这次竞赛等数据集是一个树叶图像分类数据集,在我的代码中定义了一个MyDataset类,在init方法中,我用pandas读取图像列表为一个Series。在getitem方法中,传入一个参数index,用Image库读取第index个图像,再用transforms对它做一些预处理(包括resize、裁切、转化成tensor、标准化),使其能够被输入到神经网络。
有三种模式,训练、验证和测试。训练和验证模式的数据是从训练集中划分的,是有标签的,而测试集是没有标签的,需要我们训练完推理得到答案,上传答案。在训练模式下,随机裁切图像即可,但是验证和测试模式最好用中心裁切,使待测试的图像尽可能完整的输入到网络中。训练和验证模式的getitem返回的是tensor和label,而测试模式返回的是tensor。
然后就可以实例化,得到train_dataset,valid_dataset,test_dataset。
train_dataset = MyDataset(mode='train')
valid_dataset = MyDataset(mode='valid')
test_dataset = MyDataset(mode='test')
最后用dataset生成dataloader,这个就很简单了,传几个参数就可以。batch_size是每个batch的训练样本个数,shuffle是否打乱数据,num_worker是读取数据所用的进程数,因为读取数据通常比较花时间,所以有时候需要用不止一个进程来读取数据。对于训练和验证数据,需要打乱,对于测试数据就不需要打乱了。
train_loader = DataLoader(train_dataset, batch_size=10, shuffle=True, num_workers=2)
valid_loader = DataLoader(valid_dataset, batch_size=10, shuffle=True, num_workers=2)
test_loader = DataLoader(test_dataset, batch_size=10, shuffle=False, num_workers=2)
label的格式
这个问题困扰了我比较久。这是一个176类的分类问题,模型输出的是有176个元素的向量,可以理解为向量中的每个元素表示该样本属于此类的概率,所以我们选择最大的元素值的index即可,也就是argmax。
按理来说,我们的label的格式也应该是one-hot编码,然鹅这样太占内存了,所以,在pytorch中规定我们的label格式是index,即只有一个数字,表示所属类的下标。
现实中的label往往是字符串,要先对其编码,用两个字典就可以了,num2label和label2num。
labels = train['label'].unique()
label2num = {}
for idx,label in enumerate(labels):
label2num[label] = idx
print('label2num:',label2num)
num2label = { a : b for b, a in label2num.items()}
print('num2label:',num2label)
网络调整
网络需要改的地方是输入size和输出size,在d2l第七章的教学中mnist数据集是黑白的,输入只有一个通道,而我们的数据集是彩色的,有三个通道,所以要改一下。mnist的输出是10类,而我们的输出有176类,这个也要改一下。
损失函数
训练过程是一个epoch一个epoch进行训练的,所以网络的输入输出都是是若干个样本,损失也是若干个样本一起计算一次损失。分类任务常用的是交叉熵损失,即nn.CronssEntropyLoss(),它的输入是y_hat和y_real,注意哟这两个格式是不一样的,y_hat是模型的输出,类似于one-hot编码,而y_real只是一个index。
关于这个损失函数,最好理解一下,上百度搜一下它是怎么计算类别的损失的。
如何推理
验证精度和推理测试集的时候都需要用到推理,推理也是分epoch进行的,推理时要把自动梯度关掉。在训练的代码中有一个细节是torch.max()函数不仅返回的是最大值,还有它的index,我们所需要的主要是它的index。
net.load_state_dict(torch.load('./net.ckpt'))
net.eval()
test_labels = []
with torch.no_grad():
for test_x in test_loader:
test_x = test_x.to(device)
output = net(test_x)
for label in torch.max(output, 1)[1]:
# print('label:',)
test_labels.append(num2label[label.item()])
总结
很多看似简单的代码其实包含了很多理论知识,可以花个一两天的时间把这些代码好好理一理,观察一下每行代码的输出形式是什么样的。