Python实战经历总结-2
2017-10-10
TF/Keras学习中遇到的Python技巧总结
原创文章,转载请注明:转自Luozm's Blogpython 关于大文件的读写
最近在做项目时遇到训练集过大的情况,无法直接读入内存,而使用keras的fit_generator()
感觉也遇到了IO瓶颈。
于是我想把验证集从训练集中分离出来,每次只把验证集读取进内存,节省一定的时间。在这个过程中遇到了一系列问题,记录下来以备查找。
1. 读取
备注: Pandas.DataFrame
是一个很好用的数据结构,但是在读取大文件时请小心,不然容易造成悲剧。
我遇到的问题是:训练集是一个62G的BSON
文件,需要根据索引从中找出验证集中数据的位置,读取出来并写入到一个独立文件中,以备后续使用。
1.1 使用DataFrame
逐行储存
我首先想到的方法就是使用DataFrame
格式,直接把每个读到的数据逐行存进df里。
示例代码如下:
import os
from tqdm import *
import bson
import pandas as pd
# Input data files are available in the "../input/" directory.
data_dir = "../input/"
train_bson_path = os.path.join(data_dir, "train.bson")
# First load the lookup tables from the CSV files.
train_offsets_df = pd.read_csv("train_offsets.csv", index_col=0) # index and features of every instance in the training set file
val_images_df = pd.read_csv("val_images.csv", index_col=0) # index of which instance is belong to validation set
num_val_images = len(val_images_df)
train_bson_file = open(train_bson_path, "rb") # open training set file
val_full_dataset = pd.DataFrame(columns=["x", "y"]) # create df to save val set
with tqdm(total=num_val_images) as pbar: # show estimited time and progress
for c, d in enumerate(val_images_df.itertuples()):
offset_row = train_offsets_df.loc[d[1]] # find the corresponding location
# Read this product's data from the BSON file.
train_bson_file.seek(offset_row["offset"]) # find where to start reading (random read)
item_data = train_bson_file.read(offset_row["length"])
# Grab the image from the product.
item = bson.BSON.decode(item_data)
bson_img = item["imgs"][d[3]]["picture"]
val_full_dataset.loc[c] = {"x": bson_img, "y": d[2]} # save into the dataframe
pbar.update()
代码可以运行,可是在实际运行中,数据的读取速度急速下降,从最开始的700/s下降到100/s,甚至还在继续下降。这段简单的代码实际运行了25个小时。
经过一系列测试发现,代码的瓶颈在于val_full_dataset.loc[c] = {"x": bson_img, "y": d[2]}
,重点是这个df.loc[]
会随着df的变长而越来越慢。
接下来我又测试了df.append()
,得到了与loc
相似的结论,都说明了它们的运行速度与df本身的长度有关。所以在定长的df中使用loc
是正确的。
1.2 使用list
逐行储存并转化成DataFrame
经过一系列的测试,我发现还是直接使用Python自带的list
数据结构能够解决这个问题。
对上述代码进行一些微小的调整:
val_full_dataset = pd.DataFrame(columns=["x", "y"])
——>val_full_dataset = []
val_full_dataset.loc[c] = {"x": bson_img, "y": d[2]}
——>val_full_dataset.append({"x": bson_img, "y": d[2]})
从Creating Pandas DataFrames from Lists and Dictionaries中发现,实际上我们生成的这个由dict
组成的list
是可以直接转换成DataFrame
的:
- 最后把生成的
list
转换成DataFrame:val_full_dataset_df = pd.DataFrame(val_full_dataset)
在测试中,经过这一点微小的改动,我们程序的运行时间从25个小时缩短到了两分钟!!
2. 写入(保存)
在读取并成功保存到DataFrame
之后,我们需要把它写入到文件中。
官方文档: IO Tools (Text, CSV, HDF5, …)
从文档中发现,df
可以直接写成三种文件类型:text
、binary
、SQL
。由于我们的文件很大,SQL
我不太熟悉,因此选用binary
文件进行储存。
2.1 HDF5
在这些二进制文件中,我最先尝试了hdf5
。
在使用前需要首先安装依赖包(PyTables):pip3 install tables
- 简单写入:
df.to_hdf("file_name","key")
,key
是储存到文件中的一个键值 - 简单读取:
df = pd.read_hdf("file_name","key")
例如:val_full_dataset_df.to_hdf("val_dataset.h5", "df")
但是在实际运行中报错:OverflowError: value too large to convert to int
,经测试发现可能是由于文件过大导致,写成较小文件时正常。
2.2 pickle
经过list
的启发,我想使用Python自带的pickle
格式试一试。
- 简单写入:
df.to_pickle('val_dataset.pkl')
- 简单读取:
df = pd.read_pickle('val_dataset.pkl')
经过测试,使用pickle
格式写入文件用时20s左右,而读取文件仅仅使用2.2s,,并且保留原BSON
文件大小,完全满足要求。
默认的pickle
格式还有压缩选项:If ‘infer’(by default), then use gzip, bz2, zip, or xz if filename ends in ‘.gz’, ‘.bz2’, ‘.zip’, or ‘.xz’, respectively.
可是经过测试,gz
也会出现Overflow
问题,zip
报错;bz2
和xz
使用正常,但是压缩速度很慢,如果不需要保留空间则不用使用。