1717
1818"""Data iterators for common data formats."""
1919from __future__ import absolute_import
20- from collections import OrderedDict , namedtuple
20+ from collections import namedtuple
2121
2222import sys
2323import ctypes
2424import logging
2525import threading
26- try :
27- import h5py
28- except ImportError :
29- h5py = None
3026import 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
4541class 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
546489class 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
766788class MXDataIter (DataIter ):
767789 """A python wrapper a C++ data iterator.
0 commit comments