Repository: chainer/chainer

Issue:
SerialIterator's shuffle does not work under certain batch sizes
When we give `shuffle=True` to `SerialIterator`, re-shuffling after an epoch is skipped if `len(dataset)` is divisible by `batch_size`.

![image](https://cloud.githubusercontent.com/assets/469803/17083083/f55bbc30-51ce-11e6-86e7-66853674b898.png)

https://github.com/pfnet/chainer/blob/master/chainer/iterators/serial_iterator.py#L65

Variable `_order` is never re-shuffled if `rest` > 0 (i.e., `len(dataset)` is divisible by `batch_size`).

(If it is okay, I'm interested in working on this issue at the development meeting tomorrow.)



Discussion:

Resolving patch:
diff --git a/chainer/iterators/serial_iterator.py b/chainer/iterators/serial_iterator.py
--- a/chainer/iterators/serial_iterator.py
+++ b/chainer/iterators/serial_iterator.py
@@ -58,11 +58,12 @@ def __next__(self):
         if i_end >= N:
             if self._repeat:
                 rest = i_end - N
+                if self._order is not None:
+                    numpy.random.shuffle(self._order)
                 if rest > 0:
                     if self._order is None:
                         batch += list(self.dataset[:rest])
                     else:
-                        numpy.random.shuffle(self._order)
                         batch += [self.dataset[index]
                                   for index in self._order[:rest]]
                 self.current_position = rest


Test patch:
diff --git a/tests/chainer_tests/iterators_tests/test_multiprocess_iterator.py b/tests/chainer_tests/iterators_tests/test_multiprocess_iterator.py
--- a/tests/chainer_tests/iterators_tests/test_multiprocess_iterator.py
+++ b/tests/chainer_tests/iterators_tests/test_multiprocess_iterator.py
@@ -57,5 +57,18 @@ def test_iterator_not_repeat_not_even(self):
         self.assertEqual(len(batch3), 1)
         self.assertEqual(sorted(batch1 + batch2 + batch3), dataset)
 
+    def test_iterator_shuffle_divisible(self):
+        dataset = list(range(10))
+        it = iterators.MultiprocessIterator(
+            dataset, 10, n_processes=self.n_processes)
+        self.assertNotEqual(it.next(), it.next())
+
+    def test_iterator_shuffle_nondivisible(self):
+        dataset = list(range(10))
+        it = iterators.MultiprocessIterator(
+            dataset, 3, n_processes=self.n_processes)
+        out = sum([it.next() for _ in range(7)], [])
+        self.assertNotEqual(out[0:10], out[10:20])
+
 
 testing.run_module(__name__, __file__)
diff --git a/tests/chainer_tests/iterators_tests/test_serial_iterator.py b/tests/chainer_tests/iterators_tests/test_serial_iterator.py
--- a/tests/chainer_tests/iterators_tests/test_serial_iterator.py
+++ b/tests/chainer_tests/iterators_tests/test_serial_iterator.py
@@ -108,5 +108,16 @@ def test_iterator_not_repeat_not_even(self):
         self.assertEqual(len(batch3), 1)
         self.assertEqual(sorted(batch1 + batch2 + batch3), dataset)
 
+    def test_iterator_shuffle_divisible(self):
+        dataset = list(range(10))
+        it = iterators.SerialIterator(dataset, 10)
+        self.assertNotEqual(it.next(), it.next())
+
+    def test_iterator_shuffle_nondivisible(self):
+        dataset = list(range(10))
+        it = iterators.SerialIterator(dataset, 3)
+        out = sum([it.next() for _ in range(7)], [])
+        self.assertNotEqual(out[0:10], out[10:20])
+
 
 testing.run_module(__name__, __file__)