数据转换

用于获取、划分和标注数据以及通用转换的函数

获取、划分和标注

对于大多数数据源创建,我们需要函数来获取项目列表,将其划分为训练/验证集,并进行标注。fastai 提供了函数来简化这些步骤(尤其是与 fastai.data.blocks 结合使用时)。

获取

首先,我们将查看那些用于获取项目列表(通常是文件名)的函数。

在本页的示例/测试中,我们将使用小型 MNIST(MNIST 的一个子集,仅包含 73 两类)。

path = untar_data(URLs.MNIST_TINY)
(path/'train').ls()
100.54% [344064/342207 00:00<00:00]
(#2) [Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/3')]

来源

get_files

 get_files (path, extensions=None, recurse=True, folders=None,
            followlinks=True)

获取 path 下的所有文件,可选择包含特定 extensions、可选地进行 recurse(递归),如果指定了,则仅在 folders 中查找。

这是从磁盘获取大量文件名的最通用方法。如果您传递 extensions(包括 .),则返回的文件名将按该列表过滤。除非您传递 recurse,否则仅包含 path 中的直接文件,启用 recurse 后,也会递归搜索所有子文件夹。folders 是一个可选的目录列表,用于限制搜索范围。

t3 = get_files(path/'train'/'3', extensions='.png', recurse=False)
t7 = get_files(path/'train'/'7', extensions='.png', recurse=False)
t  = get_files(path/'train', extensions='.png', recurse=True)
test_eq(len(t), len(t3)+len(t7))
test_eq(len(get_files(path/'train'/'3', extensions='.jpg', recurse=False)),0)
test_eq(len(t), len(get_files(path, extensions='.png', recurse=True, folders='train')))
t
(#709) [Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9243.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9519.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/7534.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9082.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8377.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/994.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8559.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8217.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8571.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/8954.png')...]

创建具有自定义行为的函数通常很有用。fastai.data 通常使用命名为 CamelCase 动词且以 er 结尾的函数来创建这些函数。FileGetter 就是这种函数创建器的一个简单示例。


来源

FileGetter

 FileGetter (suf='', extensions=None, recurse=True, folders=None)

创建一个 get_files 偏函数,用于搜索路径后缀 suf,如果指定了,则仅在 folders 中搜索,并传递参数 args。

fpng = FileGetter(extensions='.png', recurse=False)
test_eq(len(t7), len(fpng(path/'train'/'7')))
test_eq(len(t), len(fpng(path/'train', recurse=True)))
fpng_r = FileGetter(extensions='.png', recurse=True)
test_eq(len(t), len(fpng_r(path/'train')))

来源

get_image_files

 get_image_files (path, recurse=True, folders=None)

递归获取 path 中的图像文件,如果指定了,则仅在 folders 中查找。

这只是简单地调用 get_files 并传入标准图像扩展名列表。

test_eq(len(t), len(get_image_files(path, recurse=True, folders='train')))

来源

ImageGetter

 ImageGetter (suf='', recurse=True, folders=None)

创建一个 get_image_files 偏函数,用于搜索后缀 suf 并传递 kwargs,如果指定了,则仅在 folders 中查找。

FileGetter 相同,但针对图像扩展名。

test_eq(len(get_files(path/'train', extensions='.png', recurse=True, folders='3')),
        len(ImageGetter(   'train',                    recurse=True, folders='3')(path)))

来源

get_text_files

 get_text_files (path, recurse=True, folders=None)

递归获取 path 中的文本文件,如果指定了,则仅在 folders 中查找。


来源

ItemGetter

 ItemGetter (i)

创建一个合适的转换,该转换应用 itemgetter(i)(即使在元组上)。

test_eq(ItemGetter(1)((1,2,3)),  2)
test_eq(ItemGetter(1)(L(1,2,3)), 2)
test_eq(ItemGetter(1)([1,2,3]),  2)
test_eq(ItemGetter(1)(np.array([1,2,3])),  2)

来源

AttrGetter

 AttrGetter (nm, default=None)

创建一个合适的转换,该转换应用 attrgetter(nm)(即使在元组上)。

test_eq(AttrGetter('shape')(torch.randn([4,5])), [4,5])
test_eq(AttrGetter('shape', [0])([4,5]), [0])

划分

下一组函数用于将数据划分为训练集和验证集。这些函数返回两个列表——分别用于训练集和验证集的索引列表或掩码列表。


来源

RandomSplitter

 RandomSplitter (valid_pct=0.2, seed=None)

创建一个函数,用于根据 valid_pct 随机地将 items 划分为训练/验证集。

def _test_splitter(f, items=None):
    "A basic set of condition a splitter must pass"
    items = ifnone(items, range_of(30))
    trn,val = f(items)
    assert 0<len(trn)<len(items)
    assert all(o not in val for o in trn)
    test_eq(len(trn), len(items)-len(val))
    # test random seed consistency
    test_eq(f(items)[0], trn)
    return trn, val
_test_splitter(RandomSplitter(seed=42))
((#24) [10,18,16,23,28,26,20,7,21,22...], (#6) [12,0,6,25,8,15])

使用 scikit-learn 的 train_test_split 函数。这允许以分层方式(根据 'labels' 的分布均匀地)划分项目。


来源

TrainTestSplitter

 TrainTestSplitter (test_size=0.2, random_state=None, stratify=None,
                    train_size=None, shuffle=True)

使用 sklearn train_test_split 工具将 items 划分为随机的训练和测试子集。

src = list(range(30))
labels = [0] * 20 + [1] * 10
test_size = 0.2

f = TrainTestSplitter(test_size=test_size, random_state=42, stratify=labels)
trn,val = _test_splitter(f, items=src)

# test labels distribution consistency
# there should be test_size % of zeroes and ones respectively in the validation set
test_eq(len([t for t in val if t < 20]) / 20, test_size)
test_eq(len([t for t in val if t > 20]) / 10, test_size)

来源

IndexSplitter

 IndexSplitter (valid_idx)

划分 items,使 val_idx 位于验证集中,其余位于训练集中。

items = 'a,b,c,d,e,f,g,h,i,j'.split(',')  #to make obvious that splits indexes and not items.
splitter = IndexSplitter([3,7,9])

_test_splitter(splitter, items)
test_eq(splitter(items),[[0,1,2,4,5,6,8],[3,7,9]])

来源

EndSplitter

 EndSplitter (valid_pct=0.2, valid_last=True)

创建一个函数,用于根据 valid_pctitems 划分为训练/验证集,如果 valid_last 为 True,则验证集位于末尾,否则位于开头。对有序数据有用。

items = range_of(10)

splitter_last = EndSplitter(valid_last=True)
_test_splitter(splitter_last)
test_eq(splitter_last(items), ([0,1,2,3,4,5,6,7], [8,9]))

splitter_start = EndSplitter(valid_last=False)
_test_splitter(splitter_start)
test_eq(splitter_start(items), ([2,3,4,5,6,7,8,9], [0,1]))

来源

GrandparentSplitter

 GrandparentSplitter (train_name='train', valid_name='valid')

根据祖父文件夹名称(train_namevalid_name)划分 items

fnames = [path/'train/3/9932.png', path/'valid/7/7189.png',
          path/'valid/7/7320.png', path/'train/7/9833.png',
          path/'train/3/7666.png', path/'valid/3/925.png',
          path/'train/7/724.png', path/'valid/3/93055.png']
splitter = GrandparentSplitter()
_test_splitter(splitter, items=fnames)
test_eq(splitter(fnames),[[0,3,4,6],[1,2,5,7]])
fnames2 = fnames + [path/'test/3/4256.png', path/'test/7/2345.png', path/'valid/7/6467.png']
splitter = GrandparentSplitter(train_name=('train', 'valid'), valid_name='test')
_test_splitter(splitter, items=fnames2)
test_eq(splitter(fnames2),[[0,3,4,6,1,2,5,7,10],[8,9]])

来源

FuncSplitter

 FuncSplitter (func)

根据 func 的结果划分 itemsTrue 表示验证集,False 表示训练集)。

splitter = FuncSplitter(lambda o: Path(o).parent.parent.name == 'valid')
_test_splitter(splitter, fnames)
test_eq(splitter(fnames),[[0,3,4,6],[1,2,5,7]])

来源

MaskSplitter

 MaskSplitter (mask)

根据 mask 的值划分 items

items = list(range(6))
splitter = MaskSplitter([True,False,False,True,False,True])
_test_splitter(splitter, items)
test_eq(splitter(items),[[1,2,4],[0,3,5]])

来源

FileSplitter

 FileSplitter (fname)

通过提供文件 fname 划分 items(该文件包含用换行符分隔的验证集项目名称)。

with tempfile.TemporaryDirectory() as d:
    fname = Path(d)/'valid.txt'
    fname.write_text('\n'.join([Path(fnames[i]).name for i in [1,3,4]]))
    splitter = FileSplitter(fname)
    _test_splitter(splitter, fnames)
    test_eq(splitter(fnames),[[0,2,5,6,7],[1,3,4]])

来源

ColSplitter

 ColSplitter (col='is_valid', on=None)

根据 col 中的值划分 items(假定 items 是一个 dataframe)。

df = pd.DataFrame({'a': [0,1,2,3,4], 'b': [True,False,True,True,False]})
splits = ColSplitter('b')(df)
test_eq(splits, [[1,4], [0,2,3]])
# Works with strings or index
splits = ColSplitter(1)(df)
test_eq(splits, [[1,4], [0,2,3]])
# does not get confused if the type of 'is_valid' is integer, but it meant to be a yes/no
df = pd.DataFrame({'a': [0,1,2,3,4], 'is_valid': [1,0,1,1,0]})
splits_by_int = ColSplitter('is_valid')(df)
test_eq(splits_by_int, [[1,4], [0,2,3]])
# optionally pass a specific value to split on
df = pd.DataFrame({'a': [0,1,2,3,4,5], 'b': [1,2,3,1,2,3]})
splits_on_val = ColSplitter('b', 3)(df)
test_eq(splits_on_val, [[0,1,3,4], [2,5]])
# or multiple values
splits_on_val = ColSplitter('b', [2,3])(df)
test_eq(splits_on_val, [[0,3], [1,2,4,5]])

来源

RandomSubsetSplitter

 RandomSubsetSplitter (train_sz, valid_sz, seed=None)

splits 中随机抽取子集,大小分别为 train_szvalid_sz

items = list(range(100))
valid_idx = list(np.arange(70,100))
splitter = RandomSubsetSplitter(0.3, 0.1)
splits = RandomSubsetSplitter(0.3, 0.1)(items)
test_eq(len(splits[0]), 30)
test_eq(len(splits[1]), 10)

标注

最后一组函数用于对单个数据项进行标注


来源

parent_label

 parent_label (o)

使用父文件夹名称标注 item

请注意,parent_label 没有可自定义的参数,因此它不返回一个函数——您可以直接使用它。

test_eq(parent_label(fnames[0]), '3')
test_eq(parent_label("fastai_dev/dev/data/mnist_tiny/train/3/9932.png"), '3')
[parent_label(o) for o in fnames]
['3', '7', '7', '7', '3', '3', '7', '3']

来源

RegexLabeller

 RegexLabeller (pat, match=False)

使用正则表达式 pat 标注 item

RegexLabeller 是一个非常灵活的函数,因为它能处理对字符串化项目的任何正则表达式搜索。传递 match=True 可使用 re.match(即仅检查字符串开头),否则使用 re.search(默认)。

例如,这里有一个示例复现了之前的 parent_label 结果。

f = RegexLabeller(fr'{posixpath.sep}(\d){posixpath.sep}')
test_eq(f(fnames[0]), '3')
[f(o) for o in fnames]
['3', '7', '7', '7', '3', '3', '7', '3']
f = RegexLabeller(fr'{posixpath.sep}(\d){posixpath.sep}')
a1 = Path(fnames[0]).as_posix()
test_eq(f(a1), '3')
[f(o) for o in fnames]
['3', '7', '7', '7', '3', '3', '7', '3']
f = RegexLabeller(r'(\d*)', match=True)
test_eq(f(fnames[0].name), '9932')

来源

ColReader

 ColReader (cols, pref='', suff='', label_delim=None)

读取 row 中的 cols,可选择添加前缀 pref 和后缀 suff

cols 可以是列名列表或索引列表(或两者的混合)。如果传递了 label_delim,则使用它分割结果。

df = pd.DataFrame({'a': 'a b c d'.split(), 'b': ['1 2', '0', '', '1 2 3']})
f = ColReader('a', pref='0', suff='1')
test_eq([f(o) for o in df.itertuples()], '0a1 0b1 0c1 0d1'.split())

f = ColReader('b', label_delim=' ')
test_eq([f(o) for o in df.itertuples()], [['1', '2'], ['0'], [], ['1', '2', '3']])

df['a1'] = df['a']
f = ColReader(['a', 'a1'], pref='0', suff='1')
test_eq([f(o) for o in df.itertuples()], [L('0a1', '0a1'), L('0b1', '0b1'), L('0c1', '0c1'), L('0d1', '0d1')])

df = pd.DataFrame({'a': [L(0,1), L(2,3,4), L(5,6,7)]})
f = ColReader('a')
test_eq([f(o) for o in df.itertuples()], [L(0,1), L(2,3,4), L(5,6,7)])

df['name'] = df['a']
f = ColReader('name')
test_eq([f(df.iloc[0,:])], [L(0,1)])

df['mask'] = df['a']
f = ColReader('mask')
test_eq([f(o) for o in df.itertuples()], [L(0,1), L(2,3,4), L(5,6,7)])
test_eq([f(df.iloc[0,:])], [L(0,1)])

来源

CategoryMap

 CategoryMap (col, sort=True, add_na=False, strict=False)

类别集合,在 o2i 中包含反向映射。

t = CategoryMap([4,2,3,4])
test_eq(t, [2,3,4])
test_eq(t.o2i, {2:0,3:1,4:2})
test_eq(t.map_objs([2,3]), [0,1])
test_eq(t.map_ids([0,1]), [2,3])
test_fail(lambda: t.o2i['unseen label'])
t = CategoryMap([4,2,3,4], add_na=True)
test_eq(t, ['#na#',2,3,4])
test_eq(t.o2i, {'#na#':0,2:1,3:2,4:3})
t = CategoryMap(pd.Series([4,2,3,4]), sort=False)
test_eq(t, [4,2,3])
test_eq(t.o2i, {4:0,2:1,3:2})
col = pd.Series(pd.Categorical(['M','H','L','M'], categories=['H','M','L'], ordered=True))
t = CategoryMap(col)
test_eq(t, ['H','M','L'])
test_eq(t.o2i, {'H':0,'M':1,'L':2})
col = pd.Series(pd.Categorical(['M','H','M'], categories=['H','M','L'], ordered=True))
t = CategoryMap(col, strict=True)
test_eq(t, ['H','M'])
test_eq(t.o2i, {'H':0,'M':1})

来源

Categorize

 Categorize (vocab=None, sort=True, add_na=False)

将类别字符串可逆转换为 vocab ID。


来源

Category

*str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

从给定对象创建新的字符串对象。如果指定了 encoding 或 errors,则对象必须暴露一个数据缓冲区,该缓冲区将使用给定的编码和错误处理器进行解码。否则,返回 object.__str__()(如果已定义)或 repr(object) 的结果。encoding 默认为 sys.getdefaultencoding()。errors 默认为 ‘strict’。*

cat = Categorize()
tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])
test_eq(cat.vocab, ['cat', 'dog'])
test_eq(cat('cat'), 0)
test_eq(cat.decode(1), 'dog')
test_stdout(lambda: show_at(tds,2), 'cat')
test_fail(lambda: cat('bird'))
cat = Categorize(add_na=True)
tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])
test_eq(cat.vocab, ['#na#', 'cat', 'dog'])
test_eq(cat('cat'), 1)
test_eq(cat.decode(2), 'dog')
test_stdout(lambda: show_at(tds,2), 'cat')
cat = Categorize(vocab=['dog', 'cat'], sort=False, add_na=True)
tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])
test_eq(cat.vocab, ['#na#', 'dog', 'cat'])
test_eq(cat('dog'), 1)
test_eq(cat.decode(2), 'cat')
test_stdout(lambda: show_at(tds,2), 'cat')

来源

MultiCategorize

 MultiCategorize (vocab=None, add_na=False)

将多类别字符串可逆转换为 vocab ID。


来源

MultiCategory

 MultiCategory (items=None, *rest, use_list=False, match=None)

行为类似于 items 列表,但也可以使用索引列表或掩码进行索引。

cat = MultiCategorize()
tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], tfms=[cat])
test_eq(tds[3][0], TensorMultiCategory([]))
test_eq(cat.vocab, ['a', 'b', 'c'])
test_eq(cat(['a', 'c']), tensor([0,2]))
test_eq(cat([]), tensor([]))
test_eq(cat.decode([1]), ['b'])
test_eq(cat.decode([0,2]), ['a', 'c'])
test_stdout(lambda: show_at(tds,2), 'a;c')

# if vocab supplied, ensure it maintains its order (i.e., it doesn't sort)
cat = MultiCategorize(vocab=['z', 'y', 'x'])
test_eq(cat.vocab, ['z','y','x'])

test_fail(lambda: cat('bird'))

来源

OneHotEncode

 OneHotEncode (c=None)

对目标进行 One-hot 编码。

可与 MultiCategorize 结合使用,如果您已有 One-hot 编码的目标,也可单独使用(在这种情况下,传递一个 vocab 进行解码,并将 do_encode 设置为 False)。

_tfm = OneHotEncode(c=3)
test_eq(_tfm([0,2]), tensor([1.,0,1]))
test_eq(_tfm.decode(tensor([0,1,1])), [1,2])
tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], [[MultiCategorize(), OneHotEncode()]])
test_eq(tds[1], [tensor([1.,0,0])])
test_eq(tds[3], [tensor([0.,0,0])])
test_eq(tds.decode([tensor([False, True, True])]), [['b','c']])
test_eq(type(tds[1][0]), TensorMultiCategory)
test_stdout(lambda: show_at(tds,2), 'a;c')

来源

EncodedMultiCategorize

 EncodedMultiCategorize (vocab)

将 One-hot 编码的多类别进行转换,使用 vocab 进行解码。

_tfm = EncodedMultiCategorize(vocab=['a', 'b', 'c'])
test_eq(_tfm([1,0,1]), tensor([1., 0., 1.]))
test_eq(type(_tfm([1,0,1])), TensorMultiCategory)
test_eq(_tfm.decode(tensor([False, True, True])), ['b','c'])

_tfm2 = EncodedMultiCategorize(vocab=['c', 'b', 'a'])
test_eq(_tfm2.vocab, ['c', 'b', 'a'])

来源

RegressionSetup

 RegressionSetup (c=None)

将目标转换为浮点数的转换。

_tfm = RegressionSetup()
dsets = Datasets([0, 1, 2], RegressionSetup)
test_eq(dsets.c, 1)
test_eq_type(dsets[0], (tensor(0.),))

dsets = Datasets([[0, 1, 2], [3,4,5]], RegressionSetup)
test_eq(dsets.c, 3)
test_eq_type(dsets[0], (tensor([0.,1.,2.]),))

来源

get_c

 get_c (dls)

MNIST 端到端数据集示例

让我们展示如何使用这些函数将 mnist 数据集获取到 Datasets 中。首先,我们获取所有图像。

path = untar_data(URLs.MNIST_TINY)
items = get_image_files(path)

然后,我们根据文件夹将数据划分为训练集和验证集。

splitter = GrandparentSplitter()
splits = splitter(items)
train,valid = (items[i] for i in splits)
train[:3],valid[:3]
((#3) [Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9243.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/9519.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/train/7/7534.png')],
 (#3) [Path('/Users/jhoward/.fastai/data/mnist_tiny/valid/7/9294.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/valid/7/9257.png'),Path('/Users/jhoward/.fastai/data/mnist_tiny/valid/7/8175.png')])

我们的输入是要打开并转换为张量的图像,我们的目标则根据父目录进行标注,并且是类别。

from PIL import Image
def open_img(fn:Path): return Image.open(fn).copy()
def img2tensor(im:Image.Image): return TensorImage(array(im)[None])

tfms = [[open_img, img2tensor],
        [parent_label, Categorize()]]
train_ds = Datasets(train, tfms)
x,y = train_ds[3]
xd,yd = decode_at(train_ds,3)
test_eq(parent_label(train[3]),yd)
test_eq(array(Image.open(train[3])),xd[0].numpy())
ax = show_at(train_ds, 3, cmap="Greys", figsize=(1,1))

assert ax.title.get_text() in ('3','7')
test_fig_exists(ax)

来源

ToTensor

 ToTensor (enc=None, dec=None, split_idx=None, order=None)

将项目转换为适当的张量类。


来源

IntToFloatTensor

 IntToFloatTensor (div=255.0, div_mask=1)

将图像转换为浮点张量,可选择除以 255(例如对于图像)。

t = (TensorImage(tensor(1)),tensor(2).long(),TensorMask(tensor(3)))
tfm = IntToFloatTensor()
ft = tfm(t)
test_eq(ft, [1./255, 2, 3])
test_eq(type(ft[0]), TensorImage)
test_eq(type(ft[2]), TensorMask)
test_eq(ft[0].type(),'torch.FloatTensor')
test_eq(ft[1].type(),'torch.LongTensor')
test_eq(ft[2].type(),'torch.LongTensor')

来源

broadcast_vec

 broadcast_vec (dim, ndim, *t, cuda=True)

通过在前后添加单位轴,使向量可在 dim 维度上广播(总共 ndim 维度)。


来源

归一化

 Normalize (mean=None, std=None, axes=(0, 2, 3))

TensorImage 批次进行归一化/反归一化。

mean,std = [0.5]*3,[0.5]*3
mean,std = broadcast_vec(1, 4, mean, std)
batch_tfms = [IntToFloatTensor(), Normalize.from_stats(mean,std)]
tdl = TfmdDL(train_ds, after_batch=batch_tfms, bs=4, device=default_device())
x,y  = tdl.one_batch()
xd,yd = tdl.decode((x,y))

assert x.type().endswith('.FloatTensor')
test_eq(xd.type(), 'torch.LongTensor')
test_eq(type(x), TensorImage)
test_eq(type(y), TensorCategory)
assert x.mean()<0.0
assert x.std()>0.3
assert 0<xd.float().mean()/255.<1
assert 0<xd.float().std()/255.<0.7
#Just for visuals
from fastai.vision.core import *
tdl.show_batch((x,y))