赞
踩
最近笔者在做一个kaggle上的树叶分类的题目(https://www.kaggle.com/competitions/classify-leaves),这个题目要求根据一张树叶的图片给出这片树叶的类别,这个题目也是沐神的《动手深度学习》课程里的一个课程竞赛题目。题目的数据集比较大,训练集有18000张224x224的图片,如果再加上测试集,那么一共有27000张图片
一般而言,我们的处理方法是,自定义一个Dataset,然后根据这个Dataset创建DataLoader,然后进行训练
具体而言,定义Dataset有两种方法
在Dataset的构造函数中加载所有图片到显存里,然后get_item函数就只需要从构造函数里构造好的tensor中取出一部分来即可
大致的代码实现如下
from torch.utils import data
import torch
class MyDataset(data.Dataset):
def __init__(self):
self.img=read_all_img()
self.labels=read_labels()
pass
def __getitem__(self, item):
return self.img[item],self.labels[item]
train_iter=data.DataLoader(MyDataset(),batch_size)
这种方式有两个问题
所以这种方式的可行性不是特别强
在初始化Dataset时,不加载具体的图片,而是把图片的路径加载好,在get_item时再去读取具体的图片
也就是下面代码所表示的思路
from torch.utils import data import torch class MyDataset(data.Dataset): def __init__(self,img_paths): self.img_paths=img_paths self.labels=read_labels() pass def __getitem__(self, item): img=read_img(self.img_paths[item]) return img,self.labels[item] train_iter=data.DataLoader(MyDataset(),batch_size)
这种方法的问题就更大了,实测下来的问题就是一个字,慢,而且慢的要死
这其实也是可以预见的,我们一般的训练代码都是长下面这样的
for epoch in range(epochs):
net.train()
for X, y in train_iter:
optimizer.zero_grad()
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
每次循环中,读取到的X,y其实不止包含一张图片,而是包含batch_size张图片,而train_iter,也就是DataLoader,是一次一次地调用Dataset的getitem来获取一张图片的tensor,然后再拼接起来,形成一个batch的tensor
这个过程就涉及到大量的IO操作,是相当花时间的,相当于,每训练一个epoch,就需要经历一次读完所有图片的过程,按照上面测试下来的结果,也就会花将近20分钟的时间在读数据上,这显然是不能接受的
如果从硬件的角度去思考问题,就很容易想到我们在计算机组成原理这门课上学到的一个trick,就是按块传输
就以cache和主存之间的数据交换为例子,同样是把4个字节的数据存入cache,我们有2种方式,一种是把4个字节视为一个块,把整个块的数据存入cache,另一种是先存1个字节,等需要下一个字节时再去主存找,然后存cache。显然,第一种更高效
由此,我们可以大胆的假设,一次性把512张图片读入显存所花费的时间是小于分512次把图片读入显存的时间,至于具体是不是这样,还需要实验来验证
具体的解决思路如下:
(注:关于为什么是512张,是因为实验中batch_size取的是512)
首先要对图片进行预处理,把512张图片里的所有数据都存到一个文件里面
然后重写一个迭代器来替代DataLoader,每次就读出一个文件里的所有数据,然后变成图片tensor的形式,并存入显存,再交给训练模块
项目完整代码已开源至github,具体见文末链接,为了文章观感,就不贴出完整代码了,以下就只贴出关键部分进行分析
def read_img_to_numpy(path):
img = cv2.imread("classify-leaves/"+path)
img = np.concatenate(
(img[:, :, 0].reshape((1, img_size, img_size)), img[:, :, 1].reshape((1, img_size, img_size)),
img[:, :, 2].reshape((1, img_size, img_size))),
axis=0)
return img
接下来就是把图片数据保存到文件里面
由于每张图片都是由3*224*224个无符号整数组成的,每个无符号整数占1个字节,所以很自然的,有一个思路就是把这3*224*224个字节以追加的方式写入文件中
具体代码如下
def append_img_to_file(img_path,file_name):
img = read_img_to_numpy(img_path).reshape((-1))
f = open(file_name, "ab+")
for x in img:
f.write(x.tobytes())
f.close()
def read_all_img_from_file(file_name,device):
size=os.path.getsize(file_name)
f = open(file_name, "rb")
result = f.read(size)
result=torch.frombuffer(result, dtype=torch.uint8).to(device=device, dtype=torch.float32).reshape((-1, 3, img_size, img_size))
return result
class ImageDataLoader: def __init__(self,batch_list,batch_size,device="cpu"): self.batch_list=batch_list self.batch_size=batch_size self.current_batch_index=0 self.device=device # read labels and mapping labels=pickle.load(open("data/labels.dump","rb")) self.labels=torch.tensor(labels,dtype=torch.int64,device=device) self.label_map=pickle.load(open("data/label_map.dump","rb")) def __iter__(self): self.current_batch_index=0 return self def __next__(self): if self.current_batch_index==len(self.batch_list): raise StopIteration # read batch index = self.batch_list[self.current_batch_index] labels = self.labels[index * self.batch_size: min((index + 1) * self.batch_size, len(self.labels))] start_time=time.time() # print(f"Try to read batch {index}") file_name = f"data/batch_{index}.bin" imgs = read_all_img_from_file(file_name,self.device) imgs = torch.tensor(imgs, dtype=torch.float32, device=self.device) # increase index self.current_batch_index += 1 end_time=time.time() delta=end_time-start_time # print(f"Read batch {index} with {len(labels)} samples in {delta} seconds, {len(labels)/delta} samples per second") return imgs, labels
这里打印计时结果的代码注释掉了
这个迭代器的主要思想就是,传入需要读取的batch的下标,然后依序把这些batch给读出来
这里传入的是list而不是起始下标,这样设计主要是为了方便构造k折交叉验证的数据集,因为训练集所涉及的batch的下标往往是不连续的
如果是以一张一张地读的方式,实测下来效率大概是每秒13张图
如果按照上述的批量读取方式,实测效率可达到每秒1100张图,读取一个批量用时不到1秒,可见这种数据读取方式的效率是明显更高的
数据预处理其实也是深度学习中常有的事,其目的是为了缩短训练时间,将小文件转成大文件以节省IO成本,这也是一种很常见的处理方式,比如tensorflow里面的tfrecord
但是这类处理有时候是会有其他的代价,例如本文中的提到的方式,在处理之后文件的大小很明显增加了,光是18000张训练图片就占了大约2628MB的磁盘空间,而原来的27000张图片只占了大约200MB的磁盘空间。推测可能是jpg图片有特殊的压缩技术,不过正好也算是在数据预处理阶段做完了jpg图片“解压缩”的任务
如果需要进一步提高效率,可以考虑使用多线程来读取文件,由于1100张图片每秒的速率已经足够进行训练了,所以笔者也就偷个懒,不再进行深入研究了,感兴趣的读者可以自己动手尝试一下
最后,如果本文的内容有任何错误或疏漏,欢迎大家批评指正,也欢迎大家在评论区里或私信里发表自己的意见,你们的支持是笔者持续创作的最大动力!
项目github地址:https://github.com/QZero233/LeafClassify
(注:这个解法是用了ResNet,目前做出来的最好的准确率是88%,目前正在努力尝试突破到90%,如果大家有想法也欢迎在评论区或私信里探讨)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。