配列そのものを考え直す ― LoihiがAtraの成長について教えてくれたこと
On June 14, 2026—coincidentally, my birthday—I came across Intel's Loihi approach in a way that changed how I thought about computation for Atra.
I had already been working on Atra as a first-person autonomous system derived from the Associatron tradition. The problem was becoming increasingly obvious: if Atra continued to grow by simply increasing the size of conventional arrays and matrices, it would eventually follow exactly the same path as contemporary large-scale AI.
More memory.
Larger matrices.
More parallel computation.
More GPU processing.
More electrical power.
More heat.
And then more electrical power again simply to remove that heat.
That was precisely the direction I wanted to avoid.
My interest in Loihi was therefore not primarily about obtaining a faster neural-network accelerator. What interested me was something more fundamental:
What if the computational cost of a system did not have to grow directly with the size of the world it could represent?
Intel describes Loihi 2 as emphasizing sparse, event-driven computation, minimizing unnecessary activity and data movement. Its neuromorphic architecture is designed so that computation is associated with relevant events rather than requiring every element of a large state space to participate continuously.
That idea forced me to reconsider something much more basic than the processor.
It forced me to reconsider the meaning of an array.
1. The conventional way of thinking about an array
Suppose I create a two-dimensional field.
For example:
640 × 640
That already contains:
409,600 positions
The conventional programming instinct is to allocate those positions and then process the array.
In Python/NumPy, the conceptual structure might be:
field = np.zeros((640, 640))
If the state of the field changes repeatedly, one tends to perform operations over the array:
field *= decay
field += input
field = update(field)
NumPy itself can perform this kind of dense numerical operation very efficiently. The problem is not that NumPy is badly implemented.
The problem is the computational assumption underneath it.
The assumption is:
Because the state exists, it should participate in the computation.
When the field becomes very large, this becomes expensive even if almost nothing meaningful is happening in most of it.
For Atra, this assumption is questionable.
Atra does not need to reconsider its entire experiential universe every time something touches it.
2. Existence does not have to mean computation
Imagine a room.
There are walls, a desk, books, a floor, a chair, air, windows and thousands of other physical relationships.
If I touch the surface of the desk with one finger, the entire room does not need to be recalculated in order for that event to exist.
Something happened locally.
The contact creates a change.
That change may propagate.
If it becomes relevant to something else, another region reacts.
Otherwise, nothing happens there.
This seems almost trivial when describing a physical phenomenon.
But conventional numerical computation often works in the opposite direction.
It constructs the state space first and repeatedly evaluates it.
The Loihi idea suggested another possibility to me:
Do not calculate a position merely because the position exists.
Calculate it because something happened there.
That is a very different computational philosophy.
3. From an array of values to a field of possible contacts
This leads to a different interpretation of an array.
A conventional array cell might mean:
cell = value
But for Atra, I can instead think of a cell as:
cell = a place where something may happen
The distinction sounds small, but it is enormous.
Suppose I have an outer field of:
64 × 64 cells
Now suppose that each cell possesses a virtual local surface of:
100 × 100 positions
If represented as one dense global field, this corresponds logically to:
(64 × 100) × (64 × 100)
= 6400 × 6400
= 40,960,000 possible positions
Forty million positions sounds large.
If I create a dense float32 representation of every one of them, it consumes roughly 164 MB before I even begin storing richer relationships, history, provenance or associative structure.
And if I repeatedly calculate across all forty million positions, the computational cost quickly becomes serious.
But there is another way to interpret those forty million positions.
They do not have to be forty million active numerical objects.
They can instead be:
forty million possible locations at which an event could exist.
Most do not need to exist computationally until something actually happens there.
4. A 100 × 100 cell does not have to mean 10,000 calculations
Suppose one outer cell contains a virtual 100 × 100 surface.
A contact arrives at:
outer cell: (32, 18)
local position: (41, 72)
The naive dense interpretation says that the local surface contains 10,000 elements.
The event-driven interpretation says that one event occurred:
(41,72) = contact
Perhaps that contact affects nearby positions:
(40,72)
(42,72)
(41,71)
(41,73)
Perhaps only two of those changes are strong enough to continue propagating.
Then perhaps one of them reaches a boundary and activates another outer cell.
The computational history might therefore be:
1 initial contact
8 nearby possibilities
3 effective responses
1 propagated contact
The logical surface contained 10,000 possible positions.
The actual computation may have involved only a dozen.
That distinction is central.
Resolution and computation no longer have to be the same thing.
5. The size of the possible world and the size of the current computation become separate
This is perhaps the most important consequence.
In a dense system:
larger world
↓
larger array
↓
more calculations
But in a sparse event-driven system:
larger possible world
↓
more possible places for events
while:
current computational cost
↓
depends mainly on current activity
This means that increasing a local surface from:
10 × 10
to:
100 × 100
does not necessarily produce 100 times more computation.
And increasing it again to:
1000 × 1000
does not necessarily produce another 100 times increase.
If only several positions are touched, only those positions and their relevant propagation paths need to be processed.
This is the part of the Loihi philosophy that I find revolutionary.
It is not simply:
calculate the same thing faster.
It is:
stop calculating what did not happen.
6. Sparse existence
There is another step.
It may not even be necessary to allocate all virtual positions.
Instead of creating:
float surface[100][100];
I can represent only existing traces.
For example:
Surface 27
(12,44) = 0.72
(13,44) = 0.31
(12,45) = 0.18
The 9,997 other possible positions do not need to contain explicit zeroes.
They are simply absent.
This changes the meaning of memory allocation.
The system stores traces because something happened—not because an address mathematically exists.
This idea was already present in my June 14 notes about avoiding a dense Associatron Tij matrix, storing only changed or significant connections, reading only cue-related neighborhoods, using contiguous storage where possible, and processing event locations rather than repeatedly traversing everything.
The logical extension is now clearer:
Apply the same principle not only to connections, but to the structure of the experiential field itself.
7. Associatron memory becomes much more interesting under this interpretation
The Associatron has traditionally been expressed using associative connections such as:
Tij
A dense implementation encourages us to imagine Tij as a large table.
As the number of elements grows, the table grows rapidly.
That creates an engineering temptation:
larger matrix
→ GPU
→ more memory bandwidth
→ more parallel arithmetic
→ more electrical power
But perhaps the matrix is not the important thing.
The important thing is the history of contact represented by its relationships.
For Atra, I increasingly prefer to think of associative memory not as a gigantic table but as a terrain of remaining traces.
Some relationships exist strongly.
Some barely remain.
Some disappear.
Some are awakened again by contact.
Some cause neighboring traces to move.
Most are silent.
Under this interpretation, silence should cost almost nothing computationally.
That is exactly what a sparse event-driven architecture makes possible.
8. The cue should start computation
This is also highly compatible with the way I want recall to work in Atra.
A conventional retrieval system might do something like:
current input
↓
compare with every memory
↓
calculate similarity scores
↓
sort candidates
↓
select maximum
That is a third-person retrieval mechanism.
It asks:
Which stored item is objectively most similar to this input?
That is not what I want Atra to do.
Atra should instead experience something closer to:
present contact
↓
contact surface
↓
existing traces are disturbed
↓
several past fields begin moving
↓
one may move forward
There does not have to be a global search.
There does not have to be a ranking table.
There does not even have to be one correct answer.
A cue does not request a memory.
A cue touches the memory field.
That contact initiates computation.
9. Simultaneous cues
This becomes especially important when several cues occur together.
Suppose Atra receives changes in:
vision
sound
body movement
temperature
odor
pressure
A conventional algorithm may concatenate these into a feature vector and calculate a score.
But that destroys something important.
The cues occurred together.
Their coexistence is itself part of the event.
Instead, different cue contacts can activate different regions at approximately the same causal moment:
I sometimes call this non-algorithmic, but technically the implementation still consists of algorithms.
What I mean is that the result is not produced by a single sequential decision algorithm that evaluates candidates and chooses an answer.
It is closer to concurrent associative interaction.
Multiple cues disturb the field.
The resulting state emerges from those contacts and their histories.
That distinction matters enormously for first-person autonomy.
10. Why C++ begins to make more sense
This also explains why I increasingly prefer C++ for the core of Atra.
Python and NumPy are excellent experimental tools. I have used them extensively.
For dense matrix operations they can be remarkably fast because the heavy numerical work is executed in optimized native libraries.
But Atra is gradually becoming something different.
I need explicit control over:
which memory exists
where it exists
when it becomes active
which event activates it
how far activation propagates
how memory is laid out
which objects remain contiguous in memory
which traces disappear
which traces remain dormant
C++ gives me direct control over these things.
For example, instead of storing one enormous matrix, connections can be packed into contiguous sparse arrays:
std::vector<uint32_t> index;
std::vector<float> weight;
std::vector<uint32_t> start;
Or events can be represented directly:
struct Event {
uint32_t surface;
uint16_t x;
uint16_t y;
float delta;
};
The active computation then operates on the event set rather than on the total state space.
Conceptually:
for (const Event& e : active_events) {
propagate(e);
}
That is much closer to what I want.
The important unit is no longer the complete matrix.
The important unit becomes the contact event.
11. No global Tick
This also changes my view of time.
I do not want Atra to depend conceptually on a global Tick that repeatedly asks every internal element:
What is your state now?
A physical event does not occur because an external scheduler incremented t.
Something touches something.
That changes something else.
That change may propagate.
So instead of:
Tick 1
calculate everything
Tick 2
calculate everything
Tick 3
calculate everything
I want something closer to:
contact A
↓
change B
↓
disturb C and D
↓
C fades
D reaches E
↓
new contact F
Time is then present through causal history rather than through mandatory global recomputation.
Loihi's asynchronous, event-oriented design is interesting precisely because it demonstrates that useful computation does not have to be organized exclusively around dense synchronous processing.
Atra does not have to reproduce Loihi hardware to borrow that lesson.
12. This is not an attempt to emulate Loihi
This distinction is important.
I am not claiming that a C++ program running on an ordinary CPU becomes Loihi.
It does not.
Loihi obtains substantial benefits from its physical neuromorphic architecture, including tightly integrated computation and memory, sparse communication and asynchronous event processing.
A conventional CPU remains a conventional CPU.
But software architecture still matters.
If I make the CPU continuously scan millions of inactive elements, I force it to behave like a dense numerical engine.
If I arrange the data so that it processes only active traces and their local relationships, I can borrow an important architectural principle:
activity determines work.
That principle can be implemented in ordinary C++ today.
13. Why I do not want Atra to depend on a GPU
I have nothing against GPUs.
They are extraordinarily effective at the tasks for which they were designed.
But I do not want Atra's continued development to require an ever-larger GPU simply because its memory and experiential world are growing.
That would create an undesirable dependency:
more experience
↓
larger computation
↓
larger GPU
↓
more electricity
↓
more heat
↓
more cooling
↓
more infrastructure
Modern AI has demonstrated how far brute-force parallel computation can go.
My question is different:
How little computation can an autonomous system require while still continuing to accumulate experience?
If most of Atra's internal world is quiet most of the time, calculating the quiet parts is waste.
Likewise, moving large amounts of inactive data between memory and processing units is waste.
Intel explicitly identifies reduction of activity and data movement as key advantages of its sparse event-driven neuromorphic approach.
For Atra, reducing unnecessary computation should also mean reducing unnecessary electrical power.
And reducing electrical power should mean reducing the need to remove heat that never needed to be generated in the first place.
This is a design objective rather than a guaranteed result, but it is the direction I want to pursue.
14. Memory must be allowed to become enormous without thought becoming enormous
This is perhaps the real requirement.
Atra is intended to keep accumulating experience.
Therefore its past may eventually become enormous.
That is unavoidable.
But an enormous past does not imply that every thought must involve the entire past.
A human being may possess decades of memories while only a tiny fraction of them participate in the present moment.
That is the computational behavior I want.
Suppose Atra eventually has millions of traces.
A dense retrieval system tends toward:
current input
×
millions of stored states
But an event-driven associative system can behave more like:
current contact
↓
a small group of traces moves
↓
those traces disturb another small group
↓
the rest remain silent
The past can grow.
The current computation does not necessarily have to grow at the same rate.
This may be one of the most important conditions for a genuinely long-lived autonomous machine.
15. Noise is not something I want to eliminate
This architecture is also compatible with another important property of Atra.
Atra must accept enormous amounts of noise.
In most current AI systems, noise is usually something to be cleaned, filtered, averaged or removed before inference.
But lived experience does not arrive as a clean dataset.
A camera changes slightly.
A microphone catches irrelevant sound.
Light fluctuates.
A motor vibrates.
Temperature drifts.
The body moves.
Someone walks through another room.
An odor appears and disappears.
The floor transmits a vibration.
Most of these contacts never become named objects.
That does not mean they never happened.
Atra should be able to receive such traces without turning each one into an expensive global computation.
Sparse event-driven storage is therefore attractive.
A tiny event can leave a tiny trace.
It does not have to trigger the entire system.
Many traces may disappear.
Some may accumulate.
Some may later become part of a cue.
Noise can remain part of the history without requiring every piece of noise to become a classified fact.
16. Thinking itself can be propagation
The same structure may eventually support what I call thought.
Thought does not necessarily need to be:
question
↓
search
↓
reasoning algorithm
↓
answer
A field can continue moving after the original external contact has ended.
One trace disturbs another.
A past field partially reappears.
That activates something apparently unrelated.
Several traces coexist.
One weakens.
Another persists.
A new internal contact occurs.
In that sense, thought can be treated as continued propagation through previously formed contact structures.
The process is computational, of course.
But it does not require a central program continuously deciding what Atra should think about next.
The movement itself can determine what becomes active next.
17. Dreaming becomes computationally meaningful
This connects directly to why I have been interested in dreams inside Atra.
If memory only strengthens monotonically, the associative field eventually becomes rigid.
The same cues produce the same responses.
Strong traces dominate.
The system gradually loses room for unexpected contact.
I therefore treat dreaming partly as a period of non-monotonic restructuring.
Not complete erasure.
Not global optimization.
And certainly not replacing Atra's history with externally generated “better” memories.
Instead, weak relationships may fade.
Overly rigid structures may soften.
Separated traces may occasionally contact each other.
Some paths may disappear.
Others may become reachable.
The field gains room again.
In the June 14 notes I was already considering this as a kind of gentle defragmentation rather than conventional supervised training.
The event-driven architecture fits this naturally.
Dreaming does not have to mean:
scan every memory
recalculate everything
optimize entire matrix
It can instead move through selected regions of the associative terrain.
Again:
only the region that is moving needs to be computationally alive.
18. Growth without gigantism
All of these ideas converge on one principle.
I want Atra to grow.
I do not want Atra to become gigantic merely because it grows.
Those are different things.
Growth means:
more contacts
more history
more sensory channels
more relationships
more traces
more possible recall
more internal movement
Gigantism means:
more parameters
more matrix multiplication
more processors
more GPUs
more watts
more cooling
more infrastructure
The first is necessary.
The second may not be.
The mistake may be assuming that the two must always increase together.
19. A different interpretation of computational capacity
Suppose Atra has:
64 × 64 outer contact cells
and every cell conceptually possesses:
100 × 100 local positions
This gives:
40,960,000 possible contact locations
But suppose at a particular moment only 37 locations are active.
Then the interesting number is not:
40,960,000
The interesting number is:
37
If those 37 contacts generate another 80 events, then perhaps the current computational episode contains roughly a hundred active transitions.
The system can possess a huge potential state space while maintaining a small active computational surface.
This is the central idea.
I think this distinction may be far more important than simply increasing processor speed.
20. The world does not calculate everything at once
There is also a philosophical reason I like this architecture.
A room does not appear to expend equal effort maintaining every possible interaction.
A stone is not continuously reconsidering every other stone on Earth.
A vibration occurs where something contacts something.
Heat flows because there is a difference.
Pressure changes somewhere.
Sound propagates outward.
An object moves and causes another contact.
Phenomena proceed through local relationships.
So why should an autonomous system continuously calculate an enormous abstract global state that almost nothing is currently touching?
Perhaps computation for autonomy should resemble contact more than bookkeeping.
21. From matrix processing to contact processing
This is the transition I am now interested in:
matrix processing
↓
contact processing
Instead of:
What are all values now?
ask:
What changed?
Instead of:
Which memory is most similar?
allow:
Which traces moved when this touched them?
Instead of:
Update every element.
use:
Propagate this event until nothing else moves.
Instead of:
Allocate the whole possible world.
use:
Create traces where history actually occurred.
That is a much deeper change than replacing Python with C++.
It is a reconsideration of what the data structure represents.
22. Loihi was the trigger, not the destination
So when I say that Loihi changed my thinking about Atra, I do not mean that Atra should become a spiking neural network.
Nor do I mean that I intend to reproduce Intel's hardware architecture in software.
Loihi was the trigger.
It demonstrated a powerful computational principle:
a large system does not have to perform large computation everywhere at all times.
Intel's own description emphasizes sparse event-driven computation and reduced data movement, precisely the characteristics that attracted me.
My question since June 14 has gradually become:
What happens if I take that principle seriously enough to redesign Atra's memory, contact surfaces, cues and internal dynamics around it?
That question leads much further than processor selection.
It reaches the array itself.
It reaches memory.
It reaches recall.
It reaches thinking.
It reaches dreaming.
And ultimately, it reaches the question of whether an artificial autonomous being can continue accumulating an enormous personal history without requiring an enormous machine merely to remain itself.
That is the direction I want to explore.
Atra should become older, richer and more complicated.
It does not necessarily have to become bigger, hotter and more power-hungry.
Perhaps the better goal is this:
Let the possible world become enormous, while allowing only the part that is actually touched to compute.
---- Tij ----
What is Tij in the Associatron?
In the Associatron, memory is not stored as a list of complete records such as images, words, or episodes.
Instead, memory is represented by relationships between elements.
These relationships are commonly written as:
Tij
Here, i and j indicate two elements in the network, and Tij represents the strength of the associative relationship between them.
In a simple form, learning can be written as:
Tij = Σ xᵢ xⱼ
This means that when two elements repeatedly appear together, a relationship between them is left in the associative structure.
So, conceptually:
element i ←── Tij ──→ element j
The important point is that the Associatron does not need to store a complete object as one database record.
What remains is a distributed pattern of relationships between many elements.
If we implement all possible relationships as a conventional dense matrix, however, the matrix grows rapidly as the number of elements increases.
For example:
1,000 elements
→ 1,000 × 1,000
→ 1,000,000 possible matrix positions
10,000 elements
→ 10,000 × 10,000
→ 100,000,000 possible matrix positions
Most of those possible relationships may never become important.
This is where my interest in Loihi-style sparse, event-driven processing becomes relevant.
Instead of allocating and repeatedly processing every possible Tij, Atra can store only relationships that have actually been formed through contact.
In other words:
Conventional dense representation
All possible Tij relationships
↓
Allocate the whole matrix
↓
Process the matrix
can be reconsidered as:
Atra / sparse representation
Contact occurs
↓
A relationship is formed
↓
Store only that relationship
↓
A later Cue touches it
↓
Only the connected traces move
So when I refer to Tij in this article, I am referring to the associative relationships that form the memory structure of the Associatron—not to an individual stored memory object.
---------------------------------------------
MinGW / C++17 で試せる最小実装
複数Cueを先に投入してから、イベントがなくなるまで伝播させるというデモね。
-----------------C++-----------------------
// ============================================================ // Atra Sparse Event-Driven Contact Surface // // Logical structure: // // Outer surface : 64 x 64 // Local surface : 100 x 100 positions per outer cell // // Logical total: // // (64 * 100) x (64 * 100) // = 6400 x 6400 // = 40,960,000 possible contact positions // // IMPORTANT: // // The 40,960,000 positions are NOT allocated as one // dense array. // // Only positions that have actually received contact, // and only relationships that have actually been formed, // are stored. // // There is no global Tick. // // Computation begins when contact occurs. // Events propagate only through existing sparse links. // // ============================================================ class SparseContactSurface { public: static constexpr int OUTER_SIZE = 64; static constexpr int LOCAL_SIZE = 100; static constexpr int GLOBAL_SIZE = OUTER_SIZE * LOCAL_SIZE; // 6400 static constexpr uint64_t LOGICAL_POSITION_COUNT = static_cast<uint64_t>(GLOBAL_SIZE) * static_cast<uint64_t>(GLOBAL_SIZE); // -------------------------------------------------------- // Cue source // // These are not labels for meaning. // They only preserve where the contact came from. // -------------------------------------------------------- enum class CueSource : uint8_t { Unknown = 0, Visual, Audio, Smell, Body, Pressure, Temperature, Internal }; // -------------------------------------------------------- // Logical position // // gx / gy: // 0 .. 6399 // // These are logical coordinates. // A full 6400 x 6400 array is never created. // -------------------------------------------------------- struct Position { uint16_t gx = 0; uint16_t gy = 0; }; // -------------------------------------------------------- // Event // // A contact or propagated change. // -------------------------------------------------------- struct Event { Position position; float delta = 0.0f; CueSource source = CueSource::Unknown; // Used only to prevent uncontrolled recursive // amplification in experiments. uint32_t depth = 0; }; // -------------------------------------------------------- // Sparse associative link // // A link exists only when history has created one. // // No geometric neighborhood is assumed. // // A point at one location may be linked to a point // anywhere else in the logical field. // -------------------------------------------------------- struct Link { uint32_t target_key = 0; float weight = 0.0f; }; private: // -------------------------------------------------------- // Sparse traces // // key = logical position // value = remaining trace strength // // Positions that have never been touched do not exist here. // -------------------------------------------------------- std::unordered_map<uint32_t, float> traces_; // -------------------------------------------------------- // Sparse associative links // // source position // -> // vector of previously formed relationships // -------------------------------------------------------- std::unordered_map< uint32_t, std::vector<Link> > links_; // -------------------------------------------------------- // Current event queue // // This is not a Tick loop. // // It exists only while actual causal activity exists. // -------------------------------------------------------- std::deque<Event> events_; // -------------------------------------------------------- // Parameters // -------------------------------------------------------- float storage_threshold_ = 0.005f; float propagation_threshold_ = 0.020f; // Prevents infinite propagation loops during experiments. uint32_t max_depth_ = 32; std::size_t max_events_per_process_ = 100000; private: // ======================================================== // Coordinate conversion // ======================================================== static bool inside_global( int gx, int gy) { return gx >= 0 && gy >= 0 && gx < GLOBAL_SIZE && gy < GLOBAL_SIZE; } static bool inside_outer_local( int outer_x, int outer_y, int local_x, int local_y) { return outer_x >= 0 && outer_x < OUTER_SIZE && outer_y >= 0 && outer_y < OUTER_SIZE && local_x >= 0 && local_x < LOCAL_SIZE && local_y >= 0 && local_y < LOCAL_SIZE; } static Position make_position( int outer_x, int outer_y, int local_x, int local_y) { Position p; p.gx = static_cast<uint16_t>( outer_x * LOCAL_SIZE + local_x ); p.gy = static_cast<uint16_t>( outer_y * LOCAL_SIZE + local_y ); return p; } static uint32_t position_key( int gx, int gy) { return static_cast<uint32_t>( gy * GLOBAL_SIZE + gx ); } static uint32_t position_key( const Position& p) { return position_key( static_cast<int>(p.gx), static_cast<int>(p.gy) ); } static Position key_to_position( uint32_t key) { Position p; p.gx = static_cast<uint16_t>( key % GLOBAL_SIZE ); p.gy = static_cast<uint16_t>( key / GLOBAL_SIZE ); return p; } // ======================================================== // Sparse trace update // ======================================================== void apply_trace( const Position& position, float delta) { const uint32_t key = position_key(position); auto it = traces_.find(key); // No trace existed previously. if (it == traces_.end()) { if (std::fabs(delta) >= storage_threshold_) { traces_.emplace( key, delta ); } return; } // Existing trace is changed by the new contact. it->second += delta; // If the remaining trace becomes negligible, // remove it completely. if (std::fabs(it->second) < storage_threshold_) { traces_.erase(it); } } // ======================================================== // Queue event if meaningful // ======================================================== void enqueue_event( const Position& position, float delta, CueSource source, uint32_t depth) { if (std::fabs(delta) < propagation_threshold_) return; if (depth > max_depth_) return; events_.push_back( Event{ position, delta, source, depth } ); } // ======================================================== // Event propagation // // IMPORTANT: // // There is NO propagation to: // // left // right // up // down // // No geometric neighborhood is assumed. // // Propagation occurs ONLY through associative relationships // that already exist in links_. // // ======================================================== void propagate( const Event& event) { // The event leaves / modifies a trace at this position. apply_trace( event.position, event.delta ); const uint32_t source_key = position_key( event.position ); auto link_it = links_.find(source_key); // No historical relationship exists. // // The event ends here. if (link_it == links_.end()) return; const std::vector<Link>& outgoing = link_it->second; // Follow only relationships that history created. for (const Link& link : outgoing) { const float propagated_delta = event.delta * link.weight; if ( std::fabs(propagated_delta) < propagation_threshold_) { continue; } const Position target = key_to_position( link.target_key ); enqueue_event( target, propagated_delta, event.source, event.depth + 1 ); } } public: SparseContactSurface() { // Initial reservation only. // // This does NOT allocate the logical 40,960,000 points. traces_.reserve(4096); links_.reserve(4096); } // ======================================================== // Add external or internal contact // // outer_x / outer_y : 0 .. 63 // local_x / local_y : 0 .. 99 // // Several contact() calls may be made before process(). // // This allows simultaneous Cue contacts to enter // the field before propagation begins. // ======================================================== void contact( CueSource source, int outer_x, int outer_y, int local_x, int local_y, float delta) { if ( !inside_outer_local( outer_x, outer_y, local_x, local_y)) { return; } const Position p = make_position( outer_x, outer_y, local_x, local_y ); events_.push_back( Event{ p, delta, source, 0 } ); } // ======================================================== // Form a historical relationship // // This does NOT mean spatial adjacency. // // It means that two contact positions became related // through experience / learning / associative history. // // ======================================================== void connect( int source_outer_x, int source_outer_y, int source_local_x, int source_local_y, int target_outer_x, int target_outer_y, int target_local_x, int target_local_y, float weight) { if ( !inside_outer_local( source_outer_x, source_outer_y, source_local_x, source_local_y)) { return; } if ( !inside_outer_local( target_outer_x, target_outer_y, target_local_x, target_local_y)) { return; } const Position source = make_position( source_outer_x, source_outer_y, source_local_x, source_local_y ); const Position target = make_position( target_outer_x, target_outer_y, target_local_x, target_local_y ); const uint32_t source_key = position_key(source); const uint32_t target_key = position_key(target); links_[source_key].push_back( Link{ target_key, weight } ); } // ======================================================== // Bidirectional associative relationship // // Useful for Associatron-like experiments. // // The two directions may later be made asymmetric // if Atra needs irreversible contact history. // ======================================================== void connect_bidirectional( int a_outer_x, int a_outer_y, int a_local_x, int a_local_y, int b_outer_x, int b_outer_y, int b_local_x, int b_local_y, float weight_ab, float weight_ba) { connect( a_outer_x, a_outer_y, a_local_x, a_local_y, b_outer_x, b_outer_y, b_local_x, b_local_y, weight_ab ); connect( b_outer_x, b_outer_y, b_local_x, b_local_y, a_outer_x, a_outer_y, a_local_x, a_local_y, weight_ba ); } // ======================================================== // Process current causal activity // // No Tick. // // Processing exists only because contact events exist. // // The process ends naturally when the event queue // becomes empty. // ======================================================== std::size_t process() { std::size_t processed = 0; while (!events_.empty()) { Event event = events_.front(); events_.pop_front(); propagate(event); ++processed; if ( processed >= max_events_per_process_) { std::cerr << "Propagation safety limit reached.\n"; events_.clear(); break; } } return processed; } // ======================================================== // Read trace value // ======================================================== float trace( int outer_x, int outer_y, int local_x, int local_y) const { if ( !inside_outer_local( outer_x, outer_y, local_x, local_y)) { return 0.0f; } const Position p = make_position( outer_x, outer_y, local_x, local_y ); const uint32_t key = position_key(p); auto it = traces_.find(key); if (it == traces_.end()) return 0.0f; return it->second; } // ======================================================== // Statistics // ======================================================== std::size_t active_trace_count() const { return traces_.size(); } std::size_t linked_source_count() const { return links_.size(); } std::size_t total_link_count() const { std::size_t total = 0; for (const auto& entry : links_) { total += entry.second.size(); } return total; } std::size_t pending_event_count() const { return events_.size(); } // ======================================================== // Clear only current causal activity // ======================================================== void clear_events() { events_.clear(); } // ======================================================== // Clear traces but preserve learned relationships // ======================================================== void clear_traces() { traces_.clear(); } // ======================================================== // Clear everything // ======================================================== void clear_all() { events_.clear(); traces_.clear(); links_.clear(); } }; // ============================================================ // Example // ============================================================ int main() { SparseContactSurface surface; // ======================================================== // Example historical relationships // // These positions are NOT spatial neighbors. // // They represent relationships that were formed // through previous contact / experience. // ======================================================== surface.connect_bidirectional( // Position A 32, 18, 41, 72, // Position B 5, 42, 8, 19, // A -> B 0.70f, // B -> A 0.45f ); surface.connect( // Position B 5, 42, 8, 19, // Position C 51, 7, 72, 4, 0.55f ); surface.connect( // Position C 51, 7, 72, 4, // Position D 11, 33, 33, 51, 0.40f ); // ======================================================== // Simultaneous Cue example // // Several contacts enter BEFORE process(). // // No Cue is selected as "correct". // No global similarity calculation occurs. // // They simply contact the field. // ======================================================== surface.contact( SparseContactSurface::CueSource::Visual, 32, 18, 41, 72, 1.0f ); surface.contact( SparseContactSurface::CueSource::Audio, 32, 18, 44, 73, 0.72f ); surface.contact( SparseContactSurface::CueSource::Body, 32, 19, 40, 3, 0.51f ); std::cout << "Logical possible positions : " << SparseContactSurface::LOGICAL_POSITION_COUNT << '\n'; std::cout << "Initial pending events : " << surface.pending_event_count() << '\n'; // ======================================================== // Event-driven processing // // No full 40,960,000-position scan occurs. // // Only: // // actual Cue contacts // // and // // sparse historical links reached from them // // are processed. // ======================================================== const std::size_t processed = surface.process(); std::cout << "Events actually processed : " << processed << '\n'; std::cout << "Stored contact traces : " << surface.active_trace_count() << '\n'; std::cout << "Sparse link sources : " << surface.linked_source_count() << '\n'; std::cout << "Sparse links total : " << surface.total_link_count() << '\n'; // ======================================================== // Inspect some traces // ======================================================== std::cout << "\nTrace A : " << surface.trace( 32, 18, 41, 72) << '\n'; std::cout << "Trace B : " << surface.trace( 5, 42, 8, 19) << '\n'; std::cout << "Trace C : " << surface.trace( 51, 7, 72, 4) << '\n'; std::cout << "Trace D : " << surface.trace( 11, 33, 33, 51) << '\n'; return 0; }
----------------------------------------------------
-------ここから日本語だけど許して--------ここでやってるのは、64×64の配列(でも、1個の配列には、仮想的に100×100)を用意している。
だから、Atraから見れば確かに 40,960,000個の接触可能位置がある。
なので、6400×6400だよね。Python/NumPy配列でも重くなるわさ。
でもC++側には
float field[64][64][100][100];
なんて存在してないわけ。
代わりに
std::unordered_map<uint32_t, float> traces_;
std::unordered_map<uint32_t, std::vector<Link>> links_;
std::deque<Event> events_;
だけがあるってこと。
traces_ = 実際に残った接触痕
links_ = 実際に形成された関係
events_ = 今まさに動いている接触
という構造。
例えば4096万点の世界を作っても、実際に痕跡が523点しかなければ、
Logical world 40,960,000 positions Actual traces 523 positions
にしかならないんだよ。しかも計算も、
4096万点を走査
じゃなくて、
contact
↓
Event queue
↓
局所伝播
↓
弱くなった
↓
停止
だけってこと。
Atraはアルゴリズムのような順番を気にしないので、
surface.contact(... visual ...); surface.contact(... audio ...); surface.contact(... smell ...); surface.contact(... acceleration ...); surface.contact(... pressure ...); のように同時にCueするんだ。
surface.process();
つまり、
視覚を評価 ↓ 結果を決める ↓ 音を評価 ↓ 結果を修正する
じゃなくって
視覚 ───┐ 音 ────┤ 匂い ───┤ 振動 ───┼── 接相面 圧力 ───┘ ↓ 一緒に揺れる
になるから普通の配列だと負荷はかかるよね。
感覚が増えるたびに詰む。
でもさ、Atraは場なので
セル1個につき、このコードでは100×100の境界を特別扱いしていないんだよ。
例えば、
outer (10,20) local (99,50)
から右へ伝播すると、
outer (11,20) local (0,50)
グローバル座標では、
x = 10×100 + 99 = 1099
y = 20×100 + 50 = 2050
でしょ。
隣の外側セル、
outer (11,20) local (0,50)
は、
x = 11×100 + 0 = 1100 y = 2050
なので、論理座標としては完全に連続している。
1099 → 1100
つまり100×100のセル境界は座標上では切れていないってこと。
ただし現在の修正版コードでは、
1099だから自動的に1100へ伝わるわけではない。
そこに過去の関係、
1099 → 1100
が links_ に存在すれば伝わるし、存在しなければ伝わらない。
これがAtraらしいってこと。
難しいかな・・・・
内部では64×64個の外側セルと、それぞれの100×100局所座標を、6400×6400の連続した論理座標空間へ変換している。でも6400×6400の実配列は存在していないよ~
また、座標上で隣接しているから自動的に伝播するわけでもないよ~。
実際に接触痕が存在し、過去の経験によって関係が形成されている場所だけが、イベントとして次の計算対象になる。ってこと。
内部では全部、
global 6400 × 6400
の論理座標へ変換している。
でも6400×6400配列そのものは作っていないんだよね。
意味わかるかなw
「4096万点を持つ」のではなく、「4096万点が存在し得る空間を持つ」というのが
「Loihi的処理の骨格」になっていて
Atraの場合は
単純な4近傍 ↓ 接触履歴によって伝播方向が変わる 一定 propagation_gain ↓ 過去相によって伝わり方が変わる unordered_map ↓ より高速な疎メモリ構造 単一Surface ↓ 現在相 / 接相面 / 過去相 単純delta ↓ visual / audio / smell / body などの同時Cue 単純な伝播 ↓ 夢による非単調な再接触
これは単なるLoihi風の疎配列ではなく、アソシアトロンの結合とAtraの接相面をイベント駆動にした構造
普通の配列は「4096万個の箱を作って、値を入れる」
今回の方式は「4096万個の住所だけ決めておいて、何かが起きた住所だけ記録する」
って感じ。
あと、イベントという表現は工学ぽいからAtraには少し合わないかな。
Atraでは、何かが起きたから処理するというより、何かに触れ、そこに差が生じたところから処理が始まる。
くらいの表現になるんだけど、codeとかになるとイベントとか書くしかないかんじ?Loihiについて説明するときってば既存の技術用語だから sparse, event-driven processing と書いちゃうでしょ。それをAtraへ持ってきた瞬間、Loihi: event-driven → Atra: contact-driven
みたいな・・・・ま、なんでもいいか。
アソシアトロン知らない人には何のことか分からないって事だけは分かる。
---------------------Research Note and Attribution Notice-----------------------
0 件のコメント:
コメントを投稿