Skip to content
This repository has been archived by the owner on Sep 1, 2023. It is now read-only.

Commit

Permalink
Merge pull request #882 from chetan51/issue-837
Browse files Browse the repository at this point in the history
Updated mapPotential to support N-dimensional topology (and other improvements)
  • Loading branch information
scottpurdy committed Jul 3, 2014
2 parents 5cc0bfe + 2e2d161 commit eabf538
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .nupic_modules
@@ -1,3 +1,3 @@
# Default nupic.core dependencies (override in optional .nupic_config)
NUPIC_CORE_REMOTE = 'git://github.com/numenta/nupic.core.git'
NUPIC_CORE_COMMITISH = '0a86854fce9352ec6b3a0f6a2ab1339f83d3ab2c'
NUPIC_CORE_COMMITISH = 'd697bf1a1ea62036fe94d8b65878debc87767ce3'
95 changes: 57 additions & 38 deletions nupic/research/spatial_pooler.py
Expand Up @@ -192,16 +192,18 @@ def __init__(self,
spVerbosity: spVerbosity level: 0, 1, 2, or 3
"""
# Verify input is valid
inputDimensions = numpy.array(inputDimensions)
columnDimensions = numpy.array(columnDimensions)
inputDimensions = numpy.array(inputDimensions, ndmin=1)
columnDimensions = numpy.array(columnDimensions, ndmin=1)
numColumns = columnDimensions.prod()
numInputs = inputDimensions.prod()

assert(numColumns > 0)
assert(numInputs > 0)
assert(inputDimensions.size == columnDimensions.size)
assert numColumns > 0, "No columns specified"
assert numInputs > 0, "No inputs specified"
assert inputDimensions.size == columnDimensions.size, (
"Input dimensions must match column dimensions")
assert (numActiveColumnsPerInhArea > 0 or
(localAreaDensity > 0 and localAreaDensity <= 0.5))
(localAreaDensity > 0 and localAreaDensity <= 0.5)), (
"Inhibition parameters are invalid")

self._seed(seed)

Expand Down Expand Up @@ -230,7 +232,8 @@ def __init__(self,
self._synPermMin = 0.0
self._synPermMax = 1.0
self._synPermTrimThreshold = synPermActiveInc / 2.0
assert(self._synPermTrimThreshold < self._synPermConnected)
assert (self._synPermTrimThreshold < self._synPermConnected), (
"synPermTrimThreshold must be less than synPermConnected")
self._updatePeriod = 50
initConnectedPct = 0.5

Expand Down Expand Up @@ -922,10 +925,6 @@ def _avgConnectedSpanForColumnND(self, index):
and connectivity matrices.
"""
dimensions = self._inputDimensions
bounds = numpy.cumprod(numpy.append([1], dimensions[::-1][:-1]))[::-1]
def toCoords(index):
return (index / bounds) % dimensions

connected = self._connectedSynapses.getRow(index).nonzero()[0]
if connected.size == 0:
return 0
Expand All @@ -934,8 +933,8 @@ def toCoords(index):
maxCoord.fill(-1)
minCoord.fill(max(self._inputDimensions))
for i in connected:
maxCoord = numpy.maximum(maxCoord, toCoords(i))
minCoord = numpy.minimum(minCoord, toCoords(i))
maxCoord = numpy.maximum(maxCoord, numpy.unravel_index(i, dimensions))
minCoord = numpy.minimum(minCoord, numpy.unravel_index(i, dimensions))
return numpy.average(maxCoord - minCoord + 1)


Expand Down Expand Up @@ -1120,6 +1119,37 @@ def _initPermanence(self, potential, connectedPct):
return perm


def _mapColumn(self, index):
"""
Maps a column to its respective input index, keeping to the topology of
the region. It takes the index of the column as an argument and determines
what is the index of the flattened input vector that is to be the center of
the column's potential pool. It distributes the columns over the inputs
uniformly. The return value is an integer representing the index of the
input bit. Examples of the expected output of this method:
* If the topology is one dimensional, and the column index is 0, this
method will return the input index 0. If the column index is 1, and there
are 3 columns over 7 inputs, this method will return the input index 3.
* If the topology is two dimensional, with column dimensions [3, 5] and
input dimensions [7, 11], and the column index is 3, the method
returns input index 8.
Parameters:
----------------------------
index: The index identifying a column in the permanence, potential
and connectivity matrices.
wrapAround: A boolean value indicating that boundaries should be
ignored.
"""
columnCoords = numpy.unravel_index(index, self._columnDimensions)
columnCoords = numpy.array(columnCoords, dtype=realDType)
ratios = columnCoords / numpy.maximum((self._columnDimensions - 1), 1)
inputCoords = (self._inputDimensions - 1) * ratios
inputCoords = inputCoords.astype(int)
inputIndex = numpy.ravel_multi_index(inputCoords, self._inputDimensions)
return inputIndex


def _mapPotential(self, index, wrapAround=False):
"""
Maps a column to its input bits. This method encapsulates the topology of
Expand All @@ -1146,21 +1176,18 @@ def _mapPotential(self, index, wrapAround=False):
index: The index identifying a column in the permanence, potential
and connectivity matrices.
wrapAround: A boolean value indicating that boundaries should be
region boundaries ignored.
ignored.
"""
# Distribute column over inputs uniformly
ratio = float(index) / max((self._numColumns - 1), 1)
index = int((self._numInputs - 1) * ratio)
index = self._mapColumn(index)
indices = self._getNeighborsND(index,
self._inputDimensions,
self._potentialRadius,
wrapAround=wrapAround)
indices.append(index)
indices = numpy.array(indices)

indices = numpy.array(range(2*self._potentialRadius+1))
indices += index
indices -= self._potentialRadius
if wrapAround:
indices %= self._numInputs
else:
indices = indices[
numpy.logical_and(indices >= 0, indices < self._numInputs)]
indices = numpy.array(list(set(indices)))
# TODO: See https://github.com/numenta/nupic.core/issues/128
indices.sort()

# Select a subset of the receptive field to serve as the
# the potential pool
Expand Down Expand Up @@ -1503,15 +1530,8 @@ def _getNeighborsND(columnIndex, dimensions, radius, wrapAround=False):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
"""
assert(dimensions.size > 0)
bounds = numpy.cumprod(numpy.append([1], dimensions[::-1][:-1]))[::-1]

def toCoords(index):
return (index / bounds) % dimensions

def toIndex(coords):
return numpy.dot(bounds, coords)

columnCoords = toCoords(columnIndex)
columnCoords = numpy.unravel_index(columnIndex, dimensions)
rangeND = []
for i in xrange(dimensions.size):
if wrapAround:
Expand All @@ -1523,12 +1543,11 @@ def toIndex(coords):
curRange = curRange[
numpy.logical_and(curRange >= 0, curRange < dimensions[i])]

rangeND.append(curRange)
rangeND.append(numpy.unique(curRange))

neighbors = [toIndex(numpy.array(coord)) for coord in
neighbors = [numpy.ravel_multi_index(coord, dimensions) for coord in
itertools.product(*rangeND)]
neighbors = list(set(neighbors) - set([columnIndex]))
assert(neighbors)
neighbors.remove(columnIndex)
return neighbors


Expand Down
101 changes: 95 additions & 6 deletions tests/unit/py2/nupic/research/spatial_pooler_unit_test.py
Expand Up @@ -146,10 +146,10 @@ def testExactOutput(self):
Previously output varied between platforms (OSX/Linux etc)
'''

expectedOutput = [10, 29, 110, 114, 210, 221, 253, 260, 289, 340, 393, 408,
473, 503, 534, 639, 680, 712, 739, 791, 905, 912, 961,
1048, 1086, 1131, 1173, 1210, 1223, 1261, 1276, 1285,
1302, 1617, 1679, 1712, 1721, 1780, 1920, 1951]
expectedOutput = [10, 29, 110, 114, 210, 221, 253, 260, 289, 340, 393,
408, 473, 503, 534, 639, 680, 712, 739, 791, 905, 912,
961, 1048, 1086, 1131, 1173, 1210, 1223, 1261, 1276,
1285, 1302, 1617, 1679, 1712, 1721, 1780, 1920, 1951]

sp = SpatialPooler(
inputDimensions = [1,188],
Expand Down Expand Up @@ -229,6 +229,56 @@ def testStripNeverLearned(self):
self.assertListEqual(trueStripped, list(stripped))


def testMapColumn(self):
params = self._params.copy()

# Test 1D
params.update({
"columnDimensions": [4],
"inputDimensions": [10]
})
sp = SpatialPooler(**params)

self.assertEqual(sp._mapColumn(0), 0)
self.assertEqual(sp._mapColumn(1), 3)
self.assertEqual(sp._mapColumn(2), 6)
self.assertEqual(sp._mapColumn(3), 9)

# Test 1D with same dimensions of columns and inputs
params.update({
"columnDimensions": [4],
"inputDimensions": [4]
})
sp = SpatialPooler(**params)

self.assertEqual(sp._mapColumn(0), 0)
self.assertEqual(sp._mapColumn(1), 1)
self.assertEqual(sp._mapColumn(2), 2)
self.assertEqual(sp._mapColumn(3), 3)

# Test 1D with dimensions of length 1
params.update({
"columnDimensions": [1],
"inputDimensions": [1]
})
sp = SpatialPooler(**params)

self.assertEqual(sp._mapColumn(0), 0)

# Test 2D
params.update({
"columnDimensions": [12, 4],
"inputDimensions": [20, 10]
})
sp = SpatialPooler(**params)

self.assertEqual(sp._mapColumn(0), 0)
self.assertEqual(sp._mapColumn(4), 10)
self.assertEqual(sp._mapColumn(5), 13)
self.assertEqual(sp._mapColumn(7), 19)
self.assertEqual(sp._mapColumn(47), 199)


def testMapPotential1D(self):
params = self._params.copy()
params.update({
Expand All @@ -246,7 +296,7 @@ def testMapPotential1D(self):
self.assertListEqual(mask.tolist(), expectedMask)

expectedMask = [0, 0, 0, 0, 1, 1, 1, 1, 1, 0]
mask = sp._mapPotential(2, wrapAround=False);
mask = sp._mapPotential(2, wrapAround=False)
self.assertListEqual(mask.tolist(), expectedMask)

# Test with wrapAround and potentialPct = 1
Expand All @@ -258,7 +308,7 @@ def testMapPotential1D(self):
self.assertListEqual(mask.tolist(), expectedMask)

expectedMask = [1, 1, 0, 0, 0, 0, 0, 1, 1, 1]
mask = sp._mapPotential(3, wrapAround=True);
mask = sp._mapPotential(3, wrapAround=True)
self.assertListEqual(mask.tolist(), expectedMask)

# Test with potentialPct < 1
Expand All @@ -272,6 +322,45 @@ def testMapPotential1D(self):
self.assertListEqual(unionMask.tolist(), supersetMask.tolist())


def testMapPotential2D(self):
params = self._params.copy()
params.update({
"columnDimensions": [2, 4],
"inputDimensions": [5, 10],
"potentialRadius": 1,
"potentialPct": 1
})

# Test without wrapAround
sp = SpatialPooler(**params)

trueIndicies = [0, 10,
1, 11]
mask = sp._mapPotential(0, wrapAround=False)
self.assertSetEqual(set(numpy.flatnonzero(mask).tolist()), set(trueIndicies))

trueIndicies = [5, 15,
6, 16,
7, 17]
mask = sp._mapPotential(2, wrapAround=False)
self.assertSetEqual(set(numpy.flatnonzero(mask).tolist()), set(trueIndicies))

# Test with wrapAround
sp = SpatialPooler(**params)

trueIndicies = [49, 9, 19,
40, 0, 10,
41, 1, 11]
mask = sp._mapPotential(0, wrapAround=True)
self.assertSetEqual(set(numpy.flatnonzero(mask).tolist()), set(trueIndicies))

trueIndicies = [48, 8, 18,
49, 9, 19,
40, 0, 10]
mask = sp._mapPotential(3, wrapAround=True)
self.assertSetEqual(set(numpy.flatnonzero(mask).tolist()), set(trueIndicies))


def testMapPotential1Column1Input(self):
params = self._params.copy()
params.update({
Expand Down

0 comments on commit eabf538

Please sign in to comment.