Skip to content

Commit ca255f0

Browse files
stu1130piyushghai
authored andcommitted
Change the way NDArrayIter handle the last batch (apache#12545)
* 1. move the shuffle to the reset 2. modify the roll_over behavior accordingly * refactor the concat part * refactor the code * implement unit test for last_batch_handle * refactor the getdata part * add docstring and refine the code according to linter * 1. add test case for NDArrayIter_h5py 2. refactor the implementation * update contributions doc * fix wording * update doc for roll_over * 1. add test for second iteration of roll_over 2. add shuffle test case * fix some wording and refine the variables naming * move utility function to new file * move utility function to io_utils.py * change shuffle function name to avoid redefining name * make io as a module * rename the utility functions * disable wildcard-import * fix the algorithm * refactor the code * test the NDArrayIter with different combinations of shuffle=True, data_source type and lables * add edge case of label data for csr NDArrayIter * trigger Travis CI * handle the 'list' of data source * check the list of data source * fix the extra blank * Trigger CI * add _ to the utility functions * Trigger CI * update several test cases * add test case for airbnb * fix the typo * fix wrong labels data shape * switch the order of condition to make more sense
1 parent 71c133d commit ca255f0

4 files changed

Lines changed: 398 additions & 189 deletions

File tree

python/mxnet/io/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
# coding: utf-8
21+
# pylint: disable=wildcard-import
22+
""" Data iterators for common data formats and utility functions."""
23+
from __future__ import absolute_import
24+
25+
from . import io
26+
from .io import *
27+
28+
from . import utils
29+
from .utils import *
Lines changed: 153 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,26 @@
1717

1818
"""Data iterators for common data formats."""
1919
from __future__ import absolute_import
20-
from collections import OrderedDict, namedtuple
20+
from collections import namedtuple
2121

2222
import sys
2323
import ctypes
2424
import logging
2525
import threading
26-
try:
27-
import h5py
28-
except ImportError:
29-
h5py = None
3026
import numpy as np
31-
from .base import _LIB
32-
from .base import c_str_array, mx_uint, py_str
33-
from .base import DataIterHandle, NDArrayHandle
34-
from .base import mx_real_t
35-
from .base import check_call, build_param_doc as _build_param_doc
36-
from .ndarray import NDArray
37-
from .ndarray.sparse import CSRNDArray
38-
from .ndarray.sparse import array as sparse_array
39-
from .ndarray import _ndarray_cls
40-
from .ndarray import array
41-
from .ndarray import concatenate
42-
from .ndarray import arange
43-
from .ndarray.random import shuffle as random_shuffle
27+
28+
from ..base import _LIB
29+
from ..base import c_str_array, mx_uint, py_str
30+
from ..base import DataIterHandle, NDArrayHandle
31+
from ..base import mx_real_t
32+
from ..base import check_call, build_param_doc as _build_param_doc
33+
from ..ndarray import NDArray
34+
from ..ndarray.sparse import CSRNDArray
35+
from ..ndarray import _ndarray_cls
36+
from ..ndarray import array
37+
from ..ndarray import concat
38+
39+
from .utils import _init_data, _has_instance, _getdata_by_idx
4440

4541
class DataDesc(namedtuple('DataDesc', ['name', 'shape'])):
4642
"""DataDesc is used to store name, shape, type and layout
@@ -489,59 +485,6 @@ def getindex(self):
489485
def getpad(self):
490486
return self.current_batch.pad
491487

492-
def _init_data(data, allow_empty, default_name):
493-
"""Convert data into canonical form."""
494-
assert (data is not None) or allow_empty
495-
if data is None:
496-
data = []
497-
498-
if isinstance(data, (np.ndarray, NDArray, h5py.Dataset)
499-
if h5py else (np.ndarray, NDArray)):
500-
data = [data]
501-
if isinstance(data, list):
502-
if not allow_empty:
503-
assert(len(data) > 0)
504-
if len(data) == 1:
505-
data = OrderedDict([(default_name, data[0])]) # pylint: disable=redefined-variable-type
506-
else:
507-
data = OrderedDict( # pylint: disable=redefined-variable-type
508-
[('_%d_%s' % (i, default_name), d) for i, d in enumerate(data)])
509-
if not isinstance(data, dict):
510-
raise TypeError("Input must be NDArray, numpy.ndarray, h5py.Dataset " + \
511-
"a list of them or dict with them as values")
512-
for k, v in data.items():
513-
if not isinstance(v, (NDArray, h5py.Dataset) if h5py else NDArray):
514-
try:
515-
data[k] = array(v)
516-
except:
517-
raise TypeError(("Invalid type '%s' for %s, " % (type(v), k)) + \
518-
"should be NDArray, numpy.ndarray or h5py.Dataset")
519-
520-
return list(sorted(data.items()))
521-
522-
def _has_instance(data, dtype):
523-
"""Return True if ``data`` has instance of ``dtype``.
524-
This function is called after _init_data.
525-
``data`` is a list of (str, NDArray)"""
526-
for item in data:
527-
_, arr = item
528-
if isinstance(arr, dtype):
529-
return True
530-
return False
531-
532-
def _shuffle(data, idx):
533-
"""Shuffle the data."""
534-
shuffle_data = []
535-
536-
for k, v in data:
537-
if (isinstance(v, h5py.Dataset) if h5py else False):
538-
shuffle_data.append((k, v))
539-
elif isinstance(v, CSRNDArray):
540-
shuffle_data.append((k, sparse_array(v.asscipy()[idx], v.context)))
541-
else:
542-
shuffle_data.append((k, array(v.asnumpy()[idx], v.context)))
543-
544-
return shuffle_data
545488

546489
class NDArrayIter(DataIter):
547490
"""Returns an iterator for ``mx.nd.NDArray``, ``numpy.ndarray``, ``h5py.Dataset``
@@ -601,6 +544,22 @@ class NDArrayIter(DataIter):
601544
...
602545
>>> batchidx # Remaining examples are discarded. So, 10/3 batches are created.
603546
3
547+
>>> dataiter = mx.io.NDArrayIter(data, labels, 3, False, last_batch_handle='roll_over')
548+
>>> batchidx = 0
549+
>>> for batch in dataiter:
550+
... batchidx += 1
551+
...
552+
>>> batchidx # Remaining examples are rolled over to the next iteration.
553+
3
554+
>>> dataiter.reset()
555+
>>> dataiter.next().data[0].asnumpy()
556+
[[[ 36. 37.]
557+
[ 38. 39.]]
558+
[[ 0. 1.]
559+
[ 2. 3.]]
560+
[[ 4. 5.]
561+
[ 6. 7.]]]
562+
(3L, 2L, 2L)
604563
605564
`NDArrayIter` also supports multiple input and labels.
606565
@@ -633,8 +592,11 @@ class NDArrayIter(DataIter):
633592
Only supported if no h5py.Dataset inputs are used.
634593
last_batch_handle : str, optional
635594
How to handle the last batch. This parameter can be 'pad', 'discard' or
636-
'roll_over'. 'roll_over' is intended for training and can cause problems
637-
if used for prediction.
595+
'roll_over'.
596+
If 'pad', the last batch will be padded with data starting from the begining
597+
If 'discard', the last batch will be discarded
598+
If 'roll_over', the remaining elements will be rolled over to the next iteration and
599+
note that it is intended for training and can cause problems if used for prediction.
638600
data_name : str, optional
639601
The data name.
640602
label_name : str, optional
@@ -648,33 +610,26 @@ def __init__(self, data, label=None, batch_size=1, shuffle=False,
648610
self.data = _init_data(data, allow_empty=False, default_name=data_name)
649611
self.label = _init_data(label, allow_empty=True, default_name=label_name)
650612

651-
if ((_has_instance(self.data, CSRNDArray) or _has_instance(self.label, CSRNDArray)) and
613+
if ((_has_instance(self.data, CSRNDArray) or
614+
_has_instance(self.label, CSRNDArray)) and
652615
(last_batch_handle != 'discard')):
653616
raise NotImplementedError("`NDArrayIter` only supports ``CSRNDArray``" \
654617
" with `last_batch_handle` set to `discard`.")
655618

656-
# shuffle data
657-
if shuffle:
658-
tmp_idx = arange(self.data[0][1].shape[0], dtype=np.int32)
659-
self.idx = random_shuffle(tmp_idx, out=tmp_idx).asnumpy()
660-
self.data = _shuffle(self.data, self.idx)
661-
self.label = _shuffle(self.label, self.idx)
662-
else:
663-
self.idx = np.arange(self.data[0][1].shape[0])
664-
665-
# batching
666-
if last_batch_handle == 'discard':
667-
new_n = self.data[0][1].shape[0] - self.data[0][1].shape[0] % batch_size
668-
self.idx = self.idx[:new_n]
619+
self.idx = np.arange(self.data[0][1].shape[0])
620+
self.shuffle = shuffle
621+
self.last_batch_handle = last_batch_handle
622+
self.batch_size = batch_size
623+
self.cursor = -self.batch_size
624+
self.num_data = self.idx.shape[0]
625+
# shuffle
626+
self.reset()
669627

670628
self.data_list = [x[1] for x in self.data] + [x[1] for x in self.label]
671629
self.num_source = len(self.data_list)
672-
self.num_data = self.idx.shape[0]
673-
assert self.num_data >= batch_size, \
674-
"batch_size needs to be smaller than data size."
675-
self.cursor = -batch_size
676-
self.batch_size = batch_size
677-
self.last_batch_handle = last_batch_handle
630+
# used for 'roll_over'
631+
self._cache_data = None
632+
self._cache_label = None
678633

679634
@property
680635
def provide_data(self):
@@ -694,74 +649,141 @@ def provide_label(self):
694649

695650
def hard_reset(self):
696651
"""Ignore roll over data and set to start."""
652+
if self.shuffle:
653+
self._shuffle_data()
697654
self.cursor = -self.batch_size
655+
self._cache_data = None
656+
self._cache_label = None
698657

699658
def reset(self):
700-
if self.last_batch_handle == 'roll_over' and self.cursor > self.num_data:
701-
self.cursor = -self.batch_size + (self.cursor%self.num_data)%self.batch_size
659+
"""Resets the iterator to the beginning of the data."""
660+
if self.shuffle:
661+
self._shuffle_data()
662+
# the range below indicate the last batch
663+
if self.last_batch_handle == 'roll_over' and \
664+
self.num_data - self.batch_size < self.cursor < self.num_data:
665+
# (self.cursor - self.num_data) represents the data we have for the last batch
666+
self.cursor = self.cursor - self.num_data - self.batch_size
702667
else:
703668
self.cursor = -self.batch_size
704669

705670
def iter_next(self):
671+
"""Increments the coursor by batch_size for next batch
672+
and check current cursor if it exceed the number of data points."""
706673
self.cursor += self.batch_size
707674
return self.cursor < self.num_data
708675

709676
def next(self):
710-
if self.iter_next():
711-
return DataBatch(data=self.getdata(), label=self.getlabel(), \
712-
pad=self.getpad(), index=None)
713-
else:
677+
"""Returns the next batch of data."""
678+
if not self.iter_next():
714679
raise StopIteration
680+
data = self.getdata()
681+
label = self.getlabel()
682+
# iter should stop when last batch is not complete
683+
if data[0].shape[0] != self.batch_size:
684+
# in this case, cache it for next epoch
685+
self._cache_data = data
686+
self._cache_label = label
687+
raise StopIteration
688+
return DataBatch(data=data, label=label, \
689+
pad=self.getpad(), index=None)
690+
691+
def _getdata(self, data_source, start=None, end=None):
692+
"""Load data from underlying arrays."""
693+
assert start is not None or end is not None, 'should at least specify start or end'
694+
start = start if start is not None else 0
695+
if end is None:
696+
end = data_source[0][1].shape[0] if data_source else 0
697+
s = slice(start, end)
698+
return [
699+
x[1][s]
700+
if isinstance(x[1], (np.ndarray, NDArray)) else
701+
# h5py (only supports indices in increasing order)
702+
array(x[1][sorted(self.idx[s])][[
703+
list(self.idx[s]).index(i)
704+
for i in sorted(self.idx[s])
705+
]]) for x in data_source
706+
]
715707

716-
def _getdata(self, data_source):
717-
"""Load data from underlying arrays, internal use only."""
718-
assert(self.cursor < self.num_data), "DataIter needs reset."
719-
if self.cursor + self.batch_size <= self.num_data:
708+
def _concat(self, first_data, second_data):
709+
"""Helper function to concat two NDArrays."""
710+
assert len(first_data) == len(
711+
second_data), 'data source should contain the same size'
712+
if first_data and second_data:
720713
return [
721-
# np.ndarray or NDArray case
722-
x[1][self.cursor:self.cursor + self.batch_size]
723-
if isinstance(x[1], (np.ndarray, NDArray)) else
724-
# h5py (only supports indices in increasing order)
725-
array(x[1][sorted(self.idx[
726-
self.cursor:self.cursor + self.batch_size])][[
727-
list(self.idx[self.cursor:
728-
self.cursor + self.batch_size]).index(i)
729-
for i in sorted(self.idx[
730-
self.cursor:self.cursor + self.batch_size])
731-
]]) for x in data_source
714+
concat(
715+
first_data[x],
716+
second_data[x],
717+
dim=0
718+
) for x in range(len(first_data))
732719
]
720+
elif (not first_data) and (not second_data):
721+
return []
733722
else:
734-
pad = self.batch_size - self.num_data + self.cursor
735723
return [
736-
# np.ndarray or NDArray case
737-
concatenate([x[1][self.cursor:], x[1][:pad]])
738-
if isinstance(x[1], (np.ndarray, NDArray)) else
739-
# h5py (only supports indices in increasing order)
740-
concatenate([
741-
array(x[1][sorted(self.idx[self.cursor:])][[
742-
list(self.idx[self.cursor:]).index(i)
743-
for i in sorted(self.idx[self.cursor:])
744-
]]),
745-
array(x[1][sorted(self.idx[:pad])][[
746-
list(self.idx[:pad]).index(i)
747-
for i in sorted(self.idx[:pad])
748-
]])
749-
]) for x in data_source
724+
first_data[0] if first_data else second_data[0]
725+
for x in range(len(first_data))
750726
]
751727

728+
def _batchify(self, data_source):
729+
"""Load data from underlying arrays, internal use only."""
730+
assert self.cursor < self.num_data, 'DataIter needs reset.'
731+
# first batch of next epoch with 'roll_over'
732+
if self.last_batch_handle == 'roll_over' and \
733+
-self.batch_size < self.cursor < 0:
734+
assert self._cache_data is not None or self._cache_label is not None, \
735+
'next epoch should have cached data'
736+
cache_data = self._cache_data if self._cache_data is not None else self._cache_label
737+
second_data = self._getdata(
738+
data_source, end=self.cursor + self.batch_size)
739+
if self._cache_data is not None:
740+
self._cache_data = None
741+
else:
742+
self._cache_label = None
743+
return self._concat(cache_data, second_data)
744+
# last batch with 'pad'
745+
elif self.last_batch_handle == 'pad' and \
746+
self.cursor + self.batch_size > self.num_data:
747+
pad = self.batch_size - self.num_data + self.cursor
748+
first_data = self._getdata(data_source, start=self.cursor)
749+
second_data = self._getdata(data_source, end=pad)
750+
return self._concat(first_data, second_data)
751+
# normal case
752+
else:
753+
if self.cursor + self.batch_size < self.num_data:
754+
end_idx = self.cursor + self.batch_size
755+
# get incomplete last batch
756+
else:
757+
end_idx = self.num_data
758+
return self._getdata(data_source, self.cursor, end_idx)
759+
752760
def getdata(self):
753-
return self._getdata(self.data)
761+
"""Get data."""
762+
return self._batchify(self.data)
754763

755764
def getlabel(self):
756-
return self._getdata(self.label)
765+
"""Get label."""
766+
return self._batchify(self.label)
757767

758768
def getpad(self):
769+
"""Get pad value of DataBatch."""
759770
if self.last_batch_handle == 'pad' and \
760771
self.cursor + self.batch_size > self.num_data:
761772
return self.cursor + self.batch_size - self.num_data
773+
# check the first batch
774+
elif self.last_batch_handle == 'roll_over' and \
775+
-self.batch_size < self.cursor < 0:
776+
return -self.cursor
762777
else:
763778
return 0
764779

780+
def _shuffle_data(self):
781+
"""Shuffle the data."""
782+
# shuffle index
783+
np.random.shuffle(self.idx)
784+
# get the data by corresponding index
785+
self.data = _getdata_by_idx(self.data, self.idx)
786+
self.label = _getdata_by_idx(self.label, self.idx)
765787

766788
class MXDataIter(DataIter):
767789
"""A python wrapper a C++ data iterator.

0 commit comments

Comments
 (0)