-
Notifications
You must be signed in to change notification settings - Fork 79
Connections create synapse improvements #746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8b371dc
194363e
06ca0c2
183f30f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,9 +40,7 @@ Connections::Connections(const CellIdx numCells, | |
| void Connections::initialize(CellIdx numCells, Permanence connectedThreshold, bool timeseries) { | ||
| cells_ = vector<CellData>(numCells); | ||
| segments_.clear(); | ||
| destroyedSegments_.clear(); | ||
| synapses_.clear(); | ||
| destroyedSynapses_.clear(); | ||
| potentialSynapsesForPresynapticCell_.clear(); | ||
| connectedSynapsesForPresynapticCell_.clear(); | ||
| potentialSegmentsForPresynapticCell_.clear(); | ||
|
|
@@ -90,17 +88,11 @@ Segment Connections::createSegment(const CellIdx cell, | |
| } | ||
|
|
||
| //proceed to create a new segment | ||
| Segment segment; | ||
| if (!destroyedSegments_.empty() ) { //reuse old, destroyed segs | ||
| segment = destroyedSegments_.back(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replacing the destroyed_ buffers. Makes code less complicated and the performance is the same.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this will never reuse a destroyed segment? IMO: in a typical application, segments should never be destroyed, just created. If the HTM reaches full capacity and needs to make room for new information, then the HTM should remove synapses from a segment instead of removing the segment itself.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes. if you want, I can move this commit to a separete PR to discuss there? You're right we almost never remove segments/synapses (cca 0.X% on hotgym benchmark).
if that is valid requirement, we should remove altogether the method
when is this? When synapses on a segment are full, when synapses on all segments are maxed, ...?
then we'd need to limit this pruning to some reasonable value (keep more synapses than activation threshold), otherwise the seg can never be active again.
|
||
| destroyedSegments_.pop_back(); | ||
| } else { //create a new segment | ||
| NTA_CHECK(segments_.size() < std::numeric_limits<Segment>::max()) << "Add segment failed: Range of Segment (data-type) insufficinet size." | ||
| NTA_CHECK(segments_.size() < std::numeric_limits<Segment>::max()) << "Add segment failed: Range of Segment (data-type) insufficinet size." | ||
| << (size_t)segments_.size() << " < " << (size_t)std::numeric_limits<Segment>::max(); | ||
| segment = static_cast<Segment>(segments_.size()); | ||
| const SegmentData& segmentData = SegmentData(cell, iteration_, nextSegmentOrdinal_++); | ||
| segments_.push_back(segmentData); | ||
| } | ||
| const Segment segment = static_cast<Segment>(segments_.size()); | ||
| const SegmentData& segmentData = SegmentData(cell, iteration_, nextSegmentOrdinal_++); | ||
| segments_.push_back(segmentData); | ||
|
|
||
| CellData &cellData = cells_[cell]; | ||
| cellData.segments.push_back(segment); //assign the new segment to its mother-cell | ||
|
|
@@ -116,17 +108,31 @@ Segment Connections::createSegment(const CellIdx cell, | |
| Synapse Connections::createSynapse(Segment segment, | ||
| CellIdx presynapticCell, | ||
| Permanence permanence) { | ||
|
|
||
| // Skip cells that are already synapsed on by this segment | ||
| // Biological motivation (?): | ||
| // There are structural constraints on the shapes of axons & synapses | ||
| // which prevent a large number duplicate of connections. | ||
| // | ||
| // It's important to prevent cells from growing duplicate synapses onto a segment, | ||
| // because otherwise a strong input would be sampled many times and grow many synapses. | ||
| // That would give such input a stronger connection. | ||
| // Synapses are supposed to have binary effects (0 or 1) but duplicate synapses give | ||
| // them (synapses 0/1) varying levels of strength. | ||
| for (const Synapse& syn : synapsesForSegment(segment)) { | ||
| const CellIdx existingPresynapticCell = dataForSynapse(syn).presynapticCell; //TODO 1; add way to get all presynaptic cells for segment (fast) | ||
| if (presynapticCell == existingPresynapticCell) { | ||
| return syn; //synapse (connecting to this presyn cell) already exists on the segment; don't create a new one, exit early and return the existing | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1/ moving code for avoiding duplicate presynaptic connections on the same segment from TM,
2/ was a bug that it was checked only in TM, is applicable generally and should be part of Connections. |
||
| } | ||
| } //else: the new synapse is not duplicit, so keep creating it. | ||
|
|
||
|
|
||
|
|
||
| // Get an index into the synapses_ list, for the new synapse to reside at. | ||
| Synapse synapse; | ||
| if (!destroyedSynapses_.empty() ) { | ||
| synapse = destroyedSynapses_.back(); | ||
| destroyedSynapses_.pop_back(); | ||
| } else { | ||
| NTA_ASSERT(synapses_.size() < std::numeric_limits<Synapse>::max()) << "Add synapse failed: Range of Synapse (data-type) insufficient size." | ||
| NTA_ASSERT(synapses_.size() < std::numeric_limits<Synapse>::max()) << "Add synapse failed: Range of Synapse (data-type) insufficient size." | ||
| << synapses_.size() << " < " << (size_t)std::numeric_limits<Synapse>::max(); | ||
| synapse = static_cast<Synapse>(synapses_.size()); | ||
| synapses_.emplace_back(SynapseData()); | ||
| } | ||
| const Synapse synapse = static_cast<Synapse>(synapses_.size()); //TODO work on cache locality. Have all Synapse, SynapseData on Segment in continuous mem block ? | ||
| synapses_.emplace_back(SynapseData()); | ||
|
|
||
| // Fill in the new synapse's data | ||
| SynapseData &synapseData = synapses_[synapse]; | ||
|
|
@@ -154,6 +160,7 @@ Synapse Connections::createSynapse(Segment segment, | |
| } | ||
|
|
||
| bool Connections::segmentExists_(const Segment segment) const { | ||
| NTA_CHECK(segment < segments_.size()); | ||
| const SegmentData &segmentData = segments_[segment]; | ||
| const vector<Segment> &segmentsOnCell = cells_[segmentData.cell].segments; | ||
| return (std::find(segmentsOnCell.cbegin(), segmentsOnCell.cend(), segment) != | ||
|
|
@@ -211,7 +218,7 @@ void Connections::destroySegment(const Segment segment) { | |
| NTA_ASSERT(*segmentOnCell == segment); | ||
|
|
||
| cellData.segments.erase(segmentOnCell); | ||
| destroyedSegments_.push_back(segment); | ||
| destroyedSegments_++; | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -261,8 +268,7 @@ void Connections::destroySynapse(const Synapse synapse) { | |
| NTA_ASSERT(*synapseOnSegment == synapse); | ||
|
|
||
| segmentData.synapses.erase(synapseOnSegment); | ||
|
|
||
| destroyedSynapses_.push_back(synapse); | ||
| destroyedSynapses_++; | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -329,19 +335,6 @@ SegmentIdx Connections::idxOnCellForSegment(const Segment segment) const { | |
| } | ||
|
|
||
|
|
||
| void Connections::mapSegmentsToCells(const Segment *segments_begin, | ||
| const Segment *segments_end, | ||
| CellIdx *cells_begin) const { | ||
| CellIdx *out = cells_begin; | ||
|
|
||
| for (auto segment = segments_begin; segment != segments_end; | ||
| ++segment, ++out) { | ||
| NTA_ASSERT(segmentExists_(*segment)); | ||
| *out = segments_[*segment].cell; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| bool Connections::compareSegments(const Segment a, const Segment b) const { | ||
| const SegmentData &aData = segments_[a]; | ||
| const SegmentData &bData = segments_[b]; | ||
|
|
@@ -606,7 +599,6 @@ void Connections::destroyMinPermanenceSynapses( | |
| const vector<CellIdx> &excludeCells) | ||
| { | ||
| NTA_ASSERT( nDestroy >= 0 ); | ||
| if( nDestroy <= 0 ) return; // Nothing to do. | ||
|
|
||
| // Don't destroy any cells that are in excludeCells. | ||
| vector<Synapse> destroyCandidates; | ||
|
|
@@ -703,8 +695,6 @@ std::ostream& operator<< (std::ostream& stream, const Connections& self) | |
| << "%) Saturated (" << (Real) synapsesSaturated / self.numSynapses() << "%)" << std::endl; | ||
| stream << " Synapses pruned (" << (Real) self.prunedSyns_ / self.numSynapses() | ||
| << "%) Segments pruned (" << (Real) self.prunedSegs_ / self.numSegments() << "%)" << std::endl; | ||
| stream << " Buffer for destroyed synapses: " << self.destroyedSynapses_.size() << " \t buffer for destr. segments: " | ||
| << self.destroyedSegments_.size() << std::endl; | ||
|
|
||
| return stream; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -246,13 +246,30 @@ class Connections : public Serializable | |
| const SegmentIdx maxSegmentsPerCell = std::numeric_limits<SegmentIdx>::max()); | ||
|
|
||
| /** | ||
| * Creates a synapse on the specified segment. | ||
| * Creates a synapse on the specified segment that connects to the presynaptic cell. | ||
| * | ||
| * Note 1: If attemping to connect to an already synapsed presynaptic cell, we don't create | ||
| * a duplicit synapse, and just return early with the existing synapse. | ||
| * This has an effect that `connections.synapsesForSegment()` is not ensured to grow (by +1) | ||
| * after calling `createSynapse()` is the method conditionally skips. Users can query this by | ||
| * `connections.numSynapses(segment)`. | ||
| * | ||
| * Explanation: | ||
| * Biological motivation (?): | ||
| * There are structural constraints on the shapes of axons & synapses | ||
| * which prevent a large number duplicate of connections. | ||
| * | ||
| * It's important to prevent cells from growing duplicate synapses onto a segment, | ||
| * because otherwise a strong input would be sampled many times and grow many synapses. | ||
| * That would give such input a stronger connection. | ||
| * Synapses are supposed to have binary effects (0 or 1) but duplicate synapses give | ||
| * them (synapses 0/1) varying levels of strength. | ||
| * | ||
| * @param segment Segment to create synapse on. | ||
| * @param presynapticCell Cell to synapse on. | ||
| * @param permanence Initial permanence of new synapse. | ||
| * | ||
| * @reval Created synapse. | ||
| * @return Created synapse. //TODO consider changing to void, or explain what's returned | ||
| */ | ||
| Synapse createSynapse(const Segment segment, | ||
| const CellIdx presynapticCell, | ||
|
|
@@ -312,6 +329,7 @@ class Connections : public Serializable | |
| * @retval Cell that this segment is on. | ||
| */ | ||
| CellIdx cellForSegment(const Segment segment) const { | ||
| NTA_ASSERT(segmentExists_(segment)); | ||
| return segments_[segment].cell; | ||
| } | ||
|
|
||
|
|
@@ -324,19 +342,6 @@ class Connections : public Serializable | |
| */ | ||
| SegmentIdx idxOnCellForSegment(const Segment segment) const; | ||
|
|
||
| /** | ||
| * Get the cell for each provided segment. | ||
| * | ||
| * @param segments | ||
| * The segments to query | ||
| * | ||
| * @param cells | ||
| * Output array with the same length as 'segments' | ||
| */ | ||
| void mapSegmentsToCells(const Segment *segments_begin, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed unused method, |
||
| const Segment *segments_end, | ||
| CellIdx *cells_begin) const; | ||
|
|
||
| /** | ||
| * Gets the segment that this synapse is on. | ||
| * | ||
|
|
@@ -607,8 +612,8 @@ class Connections : public Serializable | |
| * @retval Number of segments. | ||
| */ | ||
| size_t numSegments() const { | ||
| NTA_ASSERT(segments_.size() >= destroyedSegments_.size()); | ||
| return segments_.size() - destroyedSegments_.size(); } | ||
| NTA_ASSERT(segments_.size() >= destroyedSegments_); | ||
| return segments_.size() - destroyedSegments_; } | ||
|
|
||
| /** | ||
| * Gets the number of segments on a cell. | ||
|
|
@@ -625,8 +630,8 @@ class Connections : public Serializable | |
| * @retval Number of synapses. | ||
| */ | ||
| size_t numSynapses() const { | ||
| NTA_ASSERT(synapses_.size() >= destroyedSynapses_.size()); | ||
| return synapses_.size() - destroyedSynapses_.size(); | ||
| NTA_ASSERT(synapses_.size() >= destroyedSynapses_); | ||
| return synapses_.size() - destroyedSynapses_; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -709,9 +714,9 @@ class Connections : public Serializable | |
| private: | ||
| std::vector<CellData> cells_; | ||
| std::vector<SegmentData> segments_; | ||
| std::vector<Segment> destroyedSegments_; | ||
| Segment destroyedSegments_ = 0; | ||
| std::vector<SynapseData> synapses_; | ||
| std::vector<Synapse> destroyedSynapses_; | ||
| Synapse destroyedSynapses_ = 0; //number of destroyed synapses | ||
| Permanence connectedThreshold_; //TODO make const | ||
| UInt32 iteration_ = 0; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,45 +185,26 @@ void TemporalMemory::growSynapses_( | |
| const Segment& segment, | ||
| const SynapseIdx nDesiredNewSynapses, | ||
| const vector<CellIdx> &prevWinnerCells) { | ||
| // It's possible to optimize this, swapping candidates to the end as | ||
| // they're used. But this is awkward to mimic in other | ||
| // implementations, especially because it requires iterating over | ||
| // the existing synapses in a particular order. | ||
|
|
||
|
|
||
| vector<CellIdx> candidates(prevWinnerCells.begin(), prevWinnerCells.end()); | ||
| NTA_ASSERT(std::is_sorted(candidates.begin(), candidates.end())); | ||
|
|
||
| // Skip cells that are already synapsed on by this segment | ||
| // Biological motivation (?): | ||
| // There are structural constraints on the shapes of axons & synapses | ||
| // which prevent a large number duplicate of connections. | ||
| // | ||
| // It's important to prevent cells from growing duplicate synapses onto a segment, | ||
| // because otherwise a strong input would be sampled many times and grow many synapses. | ||
| // That would give such input a stronger connection. | ||
| // Synapses are supposed to have binary effects (0 or 1) but duplicate synapses give | ||
| // them (synapses 0/1) varying levels of strength. | ||
| for (const Synapse& synapse : connections.synapsesForSegment(segment)) { | ||
| const CellIdx presynapticCell = connections.dataForSynapse(synapse).presynapticCell; | ||
| const auto already = std::lower_bound(candidates.cbegin(), candidates.cend(), presynapticCell); | ||
breznak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (already != candidates.cend() && *already == presynapticCell) { | ||
| candidates.erase(already); | ||
| } | ||
| } | ||
|
|
||
| //figure the number of new synapses to grow | ||
| const size_t nActual = std::min(static_cast<size_t>(nDesiredNewSynapses), candidates.size()); | ||
|
|
||
| // Check if we're going to surpass the maximum number of synapses. | ||
| // ..Check if we're going to surpass the maximum number of synapses. | ||
| Int overrun = static_cast<Int>(connections.numSynapses(segment) + nActual - maxSynapsesPerSegment_); | ||
| if (overrun > 0) { | ||
| connections.destroyMinPermanenceSynapses(segment, static_cast<Int>(overrun), prevWinnerCells); | ||
| connections.destroyMinPermanenceSynapses(segment, static_cast<Int>(overrun), prevWinnerCells); //TODO move this functionality to Connections.createSynapse() | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OT: for next PR |
||
| } | ||
|
|
||
| // Recalculate in case we weren't able to destroy as many synapses as needed. | ||
| // ..Recalculate in case we weren't able to destroy as many synapses as needed. | ||
| const size_t nActualWithMax = std::min(nActual, static_cast<size_t>(maxSynapsesPerSegment_) - connections.numSynapses(segment)); | ||
|
|
||
| // Pick nActual cells randomly. | ||
| for (const auto syn : rng_.sample(candidates, static_cast<UInt>(nActualWithMax))) { | ||
| rng_.shuffle(candidates.begin(), candidates.end()); | ||
| const size_t nDesired = connections.numSynapses(segment) + nActualWithMax; //num synapses on seg after this function (+-), see #COND | ||
| for (const auto syn : candidates) { | ||
| // #COND: this loop finishes two folds: a) we ran out of candidates (above), b) we grew the desired number of new synapses (below) | ||
| if(connections.numSynapses(segment) == nDesired) break; | ||
breznak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| connections.createSynapse(segment, syn, initialPermanence_); //TODO createSynapse consider creating a vector of new synapses at once? | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,6 +120,20 @@ TEST(ConnectionsTest, testCreateSynapse) { | |
| SynapseData synapseData2 = connections.dataForSynapse(synapses[1]); | ||
| ASSERT_EQ(synapseData2.presynapticCell, 150ul); | ||
| ASSERT_NEAR((Permanence)0.48, synapseData2.permanence, htm::Epsilon); | ||
| //TODO add tests for failures | ||
| } | ||
|
|
||
|
|
||
| TEST(ConnectionsTest, testCreateSynapseAvoidDuplicitPresynapticConnections) { | ||
| Connections connections(1024); | ||
| UInt32 cell = 10; | ||
| Segment segment = connections.createSegment(cell); | ||
|
|
||
| connections.createSynapse(segment, 50, 0.34f); | ||
| connections.createSynapse(segment, 51, 0.34f); | ||
| const size_t numSynapses = connections.synapsesForSegment(segment).size(); //created 2 synapses above | ||
| connections.createSynapse(segment, 50, 0.48f); //attempt to create already existing synapse (to presyn cell "50") -> skips as no duplication should happen | ||
| ASSERT_EQ(connections.synapsesForSegment(segment).size(), numSynapses) << "Duplicit synapses should not be created!"; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. test the new "check dupes, skip" behavior of createSynapse. |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -644,22 +658,22 @@ TEST(ConnectionsTest, testBumpSegment) { | |
| } | ||
|
|
||
| /** | ||
| * Test the mapSegmentsToCells method. | ||
| * Test the mapping semgnets to cells by cellForSegment() method. | ||
| */ | ||
| TEST(ConnectionsTest, testMapSegmentsToCells) { | ||
| TEST(ConnectionsTest, testCellForSegment) { | ||
| Connections connections(1024); | ||
|
|
||
| const Segment segment1 = connections.createSegment(42); | ||
| const Segment segment2 = connections.createSegment(42); | ||
| const Segment segment3 = connections.createSegment(43); | ||
|
|
||
| const vector<Segment> segments = {segment1, segment2, segment3, segment1}; | ||
| vector<CellIdx> cells(segments.size()); | ||
|
|
||
| connections.mapSegmentsToCells( | ||
| segments.data(), segments.data() + segments.size(), cells.data()); | ||
|
|
||
| const vector<CellIdx> expected = {42, 42, 43, 42}; | ||
| vector<CellIdx> cells; | ||
|
|
||
| for(auto seg : segments) { | ||
| cells.push_back(connections.cellForSegment(seg)); | ||
| } | ||
| ASSERT_EQ(expected, cells); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.