OSCR

Remembering the "when": Hebbian memory models for the time of past events.

Code ↔ Paper

1 match between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 1 match
  1. [1] § 2. Results › 2.4. Examples of episodic-like memory systems › 2.4.6. A what-where-when memory system for food caching animals. ↔ scripts/sims.jl, lines 597–675 · score 0.56 · cache locations, caching events, probed, food, simulations, reward

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Julia · 794 lines · 33 KB · no license · 1 match

  1. using NeuralProcessingOfTime: similarity
  2. using NeuralProcessingOfTime
  3. using DataFrames, PGFPlotsX, Statistics, ColorSchemes, Distributions
  4. import NeuralProcessingOfTime: age_at_similarity
  5. import NeuralProcessingOfTime: Token, Color, Shape, age_at_similarity, IdPlus, Clamp, Shifter
  6. import NeuralProcessingOfTime: Neurons, Distributed, Connection, TokenSensor, Hebbian,
  7. One2OneConnection, All2AllConnection, HebbianLatentStateDecay, Brain, RewardSensor,
  8. RewardModulatedHebbian, FixedMicroSteps, OnlyActiveAtStep, DelayedHebbian,
  9. Additive, IncrementPost, All2FirstConnection, SparseRandomConnection,
  10. All2FirstOfKindConnection
  11. import NeuralProcessingOfTime: sense!, micro_step!, update!, propagate!, activity,
  12. setactivity!, heaviside, step!, action!, RandomInFan
  13. import NeuralProcessingOfTime: black, red, green, blue, orange, pink, yellow,
  14. shapeless, triangle, square, pentagon, circle, halfcircle, quartercircle
  15. const colors = ColorSchemes.Johnson
  16. using Random
  17. const BASEPATH = joinpath(@__DIR__, "..")
  18. const DOCPATH = joinpath(BASEPATH, "doc")
  19. const DATAPATH = joinpath(BASEPATH, "data")
  20. using Serialization, CodecZstd
  21. function csave(f, d)
  22. open(f, "w") do fd
  23. stream = ZstdCompressorStream(fd)
  24. serialize(stream, d)
  25. close(stream)
  26. end
  27. end
  28. function cload(f)
  29. open(f, "r") do fd
  30. stream = ZstdDecompressorStream(fd)
  31. res = deserialize(stream)
  32. close(stream)
  33. res
  34. end
  35. end
  36. function reward_task1(history, query, action)
  37. age = age_at_similarity(history, query, threshold = 1)
  38. age != 3 && return 0.
  39. action == 1 && return -1.
  40. return 1.
  41. end
  42. function reward_task2(history, query, action)
  43. age = age_at_similarity(history, query, threshold = 1)
  44. (age ∉ (2, 3) || query.c ∉ (blue, red)) && return 0.
  45. if age == 2
  46. query.c == blue && action == 2 && return 1.
  47. query.c == red && action == 1 && return 1.
  48. elseif age == 3
  49. query.c == blue && action == 1 && return 1.
  50. query.c == red && action == 2 && return 1.
  51. end
  52. return -1.
  53. end
  54. function reward_task3(history, query, action)
  55. age = age_at_similarity(history, query, threshold = 1)
  56. (age === nothing || age > 5 || query.s === circle) && return 0.
  57. ((age ≤ 2 && action == 1) ||
  58. (age > 2 && action == 2)) && return 1.
  59. return -1.
  60. end
  61. function stimuli_task1(; test_interval = rand(1:5), version = 1)
  62. tokens = [Token(red, triangle),
  63. Token(blue, square),
  64. Token(green, circle),
  65. Token(yellow, pentagon),
  66. Token(orange, halfcircle)]
  67. sequence = [tokens[1:3]; tokens[1]]
  68. if test_interval == 1
  69. append!(sequence, [tokens[4], tokens[4]])
  70. elseif test_interval == 2
  71. push!(sequence, tokens[3])
  72. elseif test_interval == 3
  73. if version == 1
  74. push!(sequence, tokens[2])
  75. else
  76. append!(sequence, [tokens[4], tokens[3]])
  77. end
  78. elseif test_interval == 4
  79. append!(sequence, [tokens[4], tokens[2]])
  80. elseif test_interval == 5
  81. append!(sequence, [tokens[4:5]; tokens[2]])
  82. end
  83. sequence
  84. end
  85. function stimuli_task2(; trials = 50, inter_trial = 5)
  86. tokens = [Token(red, triangle),
  87. Token(black, circle),
  88. Token(blue, square)]
  89. sequence = Token{Color, Shape}[]
  90. for t in 1:trials
  91. tok = iseven(t) ? tokens[1] : tokens[3]
  92. for filler in (1, 2)
  93. push!(sequence, tok)
  94. for _ in 1:filler
  95. push!(sequence, tokens[2])
  96. end
  97. push!(sequence, tok)
  98. for _ in 1:inter_trial # inter trial interval
  99. push!(sequence, tokens[2])
  100. end
  101. end
  102. end
  103. sequence
  104. end
  105. function stimuli_task3(; trials = 50,
  106. max_age = 5,
  107. inter_trial = max_age,
  108. max_length = 10*trials,
  109. rng = Random.GLOBAL_RNG)
  110. tokens = [Token(red, triangle),
  111. Token(black, circle)
  112. ]
  113. sequence = Token{Color, Shape}[]
  114. for _ in 1:trials
  115. length(sequence) == max_length && return sequence
  116. push!(sequence, tokens[1])
  117. RI = rand(rng, 0:max_age-1)
  118. for _ in 1:RI
  119. length(sequence) == max_length && return sequence
  120. push!(sequence, tokens[2])
  121. end
  122. push!(sequence, tokens[1])
  123. for _ in 1:inter_trial
  124. push!(sequence, tokens[2])
  125. end
  126. end
  127. for _ in 1:max_length - length(sequence)
  128. push!(sequence, tokens[2])
  129. end
  130. sequence
  131. end
  132. struct RandomBrain end
  133. step!(::RandomBrain, ::Any, ::Any; kwargs...) = rand((1, 2))
  134. mutable struct OptimalLearner{S}
  135. history::Vector{Token}
  136. STM::S
  137. Q::Dict{S, Float64}
  138. end
  139. function OptimalLearner(; only_age = false,
  140. sinit = only_age ? (1, 1) : (Token(black, square), 1, 1))
  141. OptimalLearner(Token[],
  142. sinit,
  143. Dict{typeof(sinit), Float64}())
  144. end
  145. query(::OptimalLearner{<:Tuple{<:Token,Int,Int}}, token, age, a) = (token, age, a)
  146. query(::OptimalLearner{Tuple{Int,Int}}, token, age, a) = (age, a)
  147. function step!(o::OptimalLearner, token, reward; kwargs...)
  148. age = age_at_similarity(o.history, token, threshold = 1)
  149. if age === nothing
  150. age = -1
  151. end
  152. push!(o.history, token)
  153. o.Q[o.STM] = reward
  154. qs = [get(o.Q, query(o, token, age, a), 0.) for a in 1:2]
  155. a = rand(findall(==(maximum(qs)), qs))
  156. o.STM = query(o, token, age, a)
  157. return a
  158. end
  159. mutable struct Tracker1
  160. a::Int
  161. r::Vector{Float64}
  162. end
  163. Tracker1() = Tracker1(0, [])
  164. function (t::Tracker1)(a, r)
  165. t.a = a
  166. r != 0 && push!(t.r, r)
  167. end
  168. struct TrackAll
  169. a::Vector{Int}
  170. r::Vector{Float64}
  171. end
  172. TrackAll() = TrackAll([], [])
  173. (t::TrackAll)(a, r) = (push!(t.a, a); push!(t.r, r))
  174. function run_task1!(brain, history, tracker = Tracker1(); callback = () -> nothing)
  175. t = run_task!(brain, history, reward_task1, tracker; callback)
  176. t.a, t.r
  177. end
  178. function run_task2!(brain, history, tracker = TrackAll(); callback = () -> nothing)
  179. run_task!(brain, history, reward_task2, tracker; callback)
  180. end
  181. function run_task3!(brain, history, tracker = TrackAll(); callback = () -> nothing)
  182. run_task!(brain, history, reward_task3, tracker; callback)
  183. end
  184. function run_task!(brain, history, reward, tracker; callback = () -> nothing, greedy = false)
  185. r = 0.
  186. for i in eachindex(history)
  187. a = step!(brain, history[i], r; callback, greedy)
  188. r = reward(history[1:i-1], history[i], a)
  189. callback() == "x" && break
  190. tracker(a, r)
  191. end
  192. tracker
  193. end
  194. # HebbianLatentStateDecay
  195. function initial_weights(n1, n2, w1, w2; only_first = false, with_bias = false)
  196. [only_first && j < length(n2) ? 0. : a == 1 ? w1 : w2 for a in 1:length(n1), j in 1:length(n2) + with_bias]
  197. end
  198. function hebbian_latent_state_decay_brain(; winit = [.9, .2],
  199. only_first = false,
  200. η = 1.,
  201. act = IdPlus(),
  202. clipper = Clamp())
  203. actuators = Neurons("actuators", Distributed(x = zeros(2)), act)
  204. token_sensor = TokenSensor()
  205. reward_sensor = RewardSensor()
  206. intermediate = Neurons("intermediate", Distributed(length(token_sensor.neurons)),
  207. heaviside)
  208. content = Neurons("content", Distributed(length(token_sensor.neurons)), heaviside)
  209. tag = Neurons("tag", Distributed(6), heaviside)
  210. stepper = FixedMicroSteps(N = 3)
  211. connections = (
  212. One2OneConnection(pre = token_sensor.neurons,
  213. post = intermediate),
  214. One2OneConnection(pre = token_sensor.neurons,
  215. post = content),
  216. All2AllConnection(pre = token_sensor.neurons,
  217. post = tag),
  218. Connection(plasticity = (DelayedHebbian(pre = intermediate,
  219. post = content), ),
  220. pre = intermediate,
  221. post = content),
  222. Connection(plasticity = (HebbianLatentStateDecay(pre = intermediate,
  223. post = tag,
  224. kind = Additive(1/3)),
  225. ),
  226. pre = intermediate,
  227. post = tag),
  228. Connection(plasticity = (RewardModulatedHebbian(; pre = tag,
  229. post = actuators,
  230. η, clipper,
  231. reward_sensor),),
  232. pre = tag, post = actuators,
  233. modulator = OnlyActiveAtStep(3, stepper),
  234. w = initial_weights(actuators, tag, winit[1], winit[2]; only_first)
  235. ),
  236. Connection(plasticity = (RewardModulatedHebbian(; pre = content,
  237. post = actuators,
  238. η, clipper,
  239. reward_sensor),),
  240. pre = content, post = actuators,
  241. modulator = OnlyActiveAtStep(3, stepper),
  242. w = initial_weights(actuators, content, .5, .5; only_first)
  243. )
  244. )
  245. Brain(; reward_sensor, token_sensor, connections, actuators,
  246. is_micro_step! = stepper)
  247. end
  248. function increment_post_brain(; winit = [.9, .2], only_first = false, η = 1., act = IdPlus(), clipper = Clamp())
  249. actuators = Neurons("actuators", Distributed(x = zeros(2)), act)
  250. token_sensor = TokenSensor()
  251. reward_sensor = RewardSensor()
  252. intermediate = Neurons("intermediate", Distributed(length(token_sensor.neurons)),
  253. heaviside)
  254. content = Neurons("content", Distributed(length(token_sensor.neurons)), heaviside)
  255. tag = Neurons("tag", Distributed(6), heaviside)
  256. stepper = FixedMicroSteps(N = 3)
  257. connections = (
  258. One2OneConnection(pre = token_sensor.neurons,
  259. post = intermediate),
  260. One2OneConnection(pre = token_sensor.neurons,
  261. post = content),
  262. All2FirstConnection(pre = token_sensor.neurons,
  263. post = tag),
  264. Connection(plasticity = (DelayedHebbian(pre = intermediate,
  265. post = content), ),
  266. pre = intermediate,
  267. post = content),
  268. Connection(plasticity = (IncrementPost(stepper, 6),
  269. DelayedHebbian(pre = intermediate, post = tag)
  270. ),
  271. pre = intermediate,
  272. post = tag),
  273. Connection(plasticity = (RewardModulatedHebbian(; pre = tag,
  274. post = actuators,
  275. η, clipper,
  276. reward_sensor),),
  277. pre = tag, post = actuators,
  278. modulator = OnlyActiveAtStep(3, stepper),
  279. w = initial_weights(actuators, tag, winit[1], winit[2]; only_first)
  280. ),
  281. Connection(plasticity = (RewardModulatedHebbian(; pre = content,
  282. post = actuators,
  283. η, clipper,
  284. reward_sensor),),
  285. pre = content, post = actuators,
  286. modulator = OnlyActiveAtStep(3, stepper),
  287. w = initial_weights(actuators, content, winit[1], winit[2]; only_first)
  288. ),
  289. )
  290. Brain(; reward_sensor, token_sensor, connections, actuators, is_micro_step! = stepper)
  291. end
  292. function chrono_brain(; winit = [.9, .2], only_first = false, η = 1., act = IdPlus(), clipper = Clamp())
  293. actuators = Neurons("actuators", Distributed(x = zeros(2)), act)
  294. token_sensor = TokenSensor()
  295. reward_sensor = RewardSensor()
  296. intermediate = Neurons("intermediate", Distributed(length(token_sensor.neurons)),
  297. heaviside)
  298. chrono = Neurons("chrono", Distributed(6*length(token_sensor.neurons)), heaviside)
  299. stepper = FixedMicroSteps(N = 3)
  300. connections = (
  301. One2OneConnection(pre = token_sensor.neurons,
  302. post = intermediate),
  303. All2FirstOfKindConnection(pre = token_sensor.neurons,
  304. post = chrono,
  305. off = 6),
  306. Connection(plasticity = (IncrementPost(stepper, 6),
  307. DelayedHebbian(pre = intermediate, post = chrono)
  308. ),
  309. pre = intermediate,
  310. post = chrono),
  311. Connection(plasticity = (RewardModulatedHebbian(; pre = chrono,
  312. post = actuators,
  313. η, clipper,
  314. reward_sensor),),
  315. pre = chrono, post = actuators,
  316. modulator = OnlyActiveAtStep(3, stepper),
  317. w = initial_weights(actuators, chrono, winit[1], winit[2]; only_first)
  318. )
  319. )
  320. Brain(; reward_sensor, token_sensor, connections, actuators, is_micro_step! = stepper)
  321. end
  322. function sparse_random_brain(; winit = [.9, .2], only_first = false, η = 1., η_bias = nothing,
  323. act = IdPlus(), clipper = Clamp(),
  324. n_intermediate = 50, n_content = 200, n_action = 2,
  325. latent_state_increment_factor = 6,
  326. n_color = length(instances(NeuralProcessingOfTime.Color)),
  327. n_shape = length(instances(NeuralProcessingOfTime.Shape)),
  328. sparsity = 1/(n_color+n_shape-2),
  329. max_fanin = n_color+n_shape-2,
  330. min_fanin = 1,
  331. with_readout_bias = true,
  332. latent_state_distribution = Uniform(0, latent_state_increment_factor),
  333. in_weights = nothing,
  334. content_activation = x -> heaviside(x - 1.5))
  335. actuators = Neurons("actuators", Distributed(x = zeros(n_action)), act)
  336. token_sensor = TokenSensor(; n_color, n_shape)
  337. reward_sensor = RewardSensor()
  338. intermediate = Neurons("intermediate", Distributed(n_intermediate), heaviside)
  339. content = Neurons("content", Distributed(n_content), content_activation)
  340. stepper = FixedMicroSteps(N = 3)
  341. connections = (
  342. SparseRandomConnection(; pre = token_sensor.neurons,
  343. post = intermediate,
  344. sparsity),
  345. SparseRandomConnection(pre = token_sensor.neurons,
  346. post = content,
  347. weights = in_weights,
  348. sparsity = RandomInFan(min = min_fanin, max = max_fanin)),
  349. Connection(plasticity = (HebbianLatentStateDecay(pre = intermediate,
  350. post = content,
  351. kind = Additive(1/3),
  352. latent_state_increment = hcat(fill(rand(latent_state_distribution, length(content)), length(intermediate))...)
  353. ), ),
  354. pre = intermediate,
  355. post = content),
  356. Connection(plasticity = (RewardModulatedHebbian(; pre = content,
  357. post = actuators,
  358. with_bias = with_readout_bias,
  359. η, η_bias, clipper,
  360. reward_sensor),),
  361. pre = content, post = actuators,
  362. with_bias = with_readout_bias,
  363. modulator = OnlyActiveAtStep(3, stepper),
  364. w = initial_weights(actuators, content, winit[1], winit[2]; only_first, with_bias = with_readout_bias)
  365. )
  366. )
  367. Brain(; reward_sensor, token_sensor, connections, actuators, is_micro_step! = stepper)
  368. end
  369. function run_sims(; brain, version = 1, n = 10^4)
  370. vcat([DataFrame(test_interval = i,
  371. action = run_task1!(brain(),
  372. stimuli_task1(; test_interval = i, version)))
  373. for _ in 1:n, i = 1:5]...)
  374. end
  375. function analyse_results(results)
  376. combine(groupby(results, :test_interval),
  377. :action => (x -> mean(first.(x) .== 2)) => :act2,
  378. :action => (x -> mean(first.(filter(x -> x[2][1] == 1, x)) .== 2)) => :act2_given_posrew1,
  379. :action => (x -> mean(first.(filter(x -> x[2][1] == -1, x)) .== 2)) => :act2_given_negrew1,
  380. :action => (x -> mean(first.(last.(x)))) => :rew1,
  381. :action => (x -> mean(last.(last.(x)))) => :rew2,
  382. )
  383. end
  384. function action_prior(; winit = [.9, .2],
  385. brain = increment_post_brain(; η = 0., winit),
  386. n = 10^5)
  387. as = [step!.(Ref(brain), stimuli_task1(test_interval = rand(1:5)), 0)[end] for _ in 1:n]
  388. mean(as .== 2)
  389. end
  390. # TODO: What happens without Shifter? Results should remain the same.
  391. learning_modes = Dict(:simple => (act = identity,),
  392. :linear => (act = IdPlus(1.),),
  393. :exp => (act = exp, clipper = Shifter()))
  394. brains = Dict(:state_decay => hebbian_latent_state_decay_brain,
  395. :increment => increment_post_brain,
  396. :sparse => sparse_random_brain,
  397. :chrono => chrono_brain,
  398. )
  399. push!(PGFPlotsX.CUSTOM_PREAMBLE, read(joinpath(DOCPATH, "modelnames.tex"), String))
  400. function modelnames(s; extra = Dict())
  401. haskey(extra, s) && return extra[s]
  402. spl = split("$s", '_')
  403. name = if spl[1] == "sparse"
  404. # "Sparse-Random-Pruning"
  405. raw"\sparse{}"
  406. elseif spl[1] == "increment"
  407. # "Representational-Drift"
  408. raw"\incrementmodel{}"
  409. elseif spl[1] == "state"
  410. # "Organized-Pruning"
  411. raw"\statedecay{}"
  412. elseif spl[1] == "chrono"
  413. # "Chronological-Organization"
  414. raw"\chrono{}"
  415. end
  416. name # * " ($(spl[end]))"
  417. end
  418. function styles(s)
  419. spl = split("$s", '_')
  420. color = if spl[1] == "sparse"
  421. colors[5]
  422. elseif spl[1] == "increment"
  423. colors[3]
  424. elseif spl[1] == "state"
  425. colors[1]
  426. elseif spl[1] == "chrono"
  427. colors[4]
  428. else
  429. "black"
  430. end
  431. linestyle = if spl[end] == "exp"
  432. "solid"
  433. elseif spl[end] == "simple"
  434. "solid"
  435. elseif spl[end] == "linear"
  436. "dotted"
  437. else
  438. "solid"
  439. end
  440. st = @pgf {color = color}
  441. st[linestyle] = nothing
  442. st
  443. end
  444. ###
  445. ### TASK 1
  446. ###
  447. """
  448. These learning rates have approximately the best final performance in task2 and task3
  449. among `η in (.005, .01, .02, .05, .1, .2, .5, 1., 2.)`.
  450. """
  451. function learning_rate(b)
  452. b == :chrono && return 1.0
  453. b == :increment && return 0.5
  454. b == :sparse && return .02
  455. b == :state_decay && return .2
  456. error()
  457. end
  458. res1 = Dict()
  459. for (nb, b) in brains
  460. for (nl, l) in learning_modes
  461. id = Symbol(nb, "_", nl)
  462. # nb == :sparse || continue
  463. nl == :exp || continue
  464. winit = [.5, .5]
  465. η = learning_rate(nb)
  466. @show id
  467. brain = () -> b(; η, winit, l...)
  468. res1[id] = run_sims(; brain)
  469. end
  470. end
  471. res1[:ap] = action_prior(winit = [.5, .5])
  472. csave(joinpath(DATAPATH, "task1.dat.zstd"), res1)
  473. res1 = cload(joinpath(DATAPATH, "task1.dat.zstd"))
  474. ks = sort(collect(filter(x -> x != :ap && (false || split("$x", '_')[end] == "exp"), keys(res1))))
  475. f1 = @pgf PGFPlotsX.Axis({xlabel = raw"$\Delta t_\mathrm{test}$",
  476. ymin = .4, ymax = .75, xtick = 1:5,
  477. xmin = .7, xmax = 5.3,
  478. legend_pos = "outer north east",
  479. ylabel = raw"probability of action $a_2$"},
  480. [Plot({mark = "*", styles(k)...},
  481. Coordinates(1:5, analyse_results(res1[k]).act2))
  482. for k in ks]...,
  483. Plot({black, dotted}, Expression("$(res1[:ap])")),
  484. PGFPlotsX.Legend([modelnames.(ks);
  485. "before learning"])
  486. )
  487. pgfsave(joinpath(DOCPATH, "sim1.tikz"), f1)
  488. ###
  489. ### TASK 2
  490. ###
  491. res2 = Dict()
  492. res2[:optimal] = mean([run_task2!(OptimalLearner(), stimuli_task2(trials = 400), Tracker1()).r for _ in 1:10^2])
  493. for (nb, b) in brains
  494. for (nl, l) in learning_modes
  495. nl == :exp || continue
  496. id = Symbol(nb, "_", nl)
  497. η = learning_rate(nb)
  498. N = 10^2
  499. @show id
  500. res2[id] = mean([run_task2!(b(; η, winit = [.5, .5], l...), stimuli_task2(trials = 400), Tracker1()).r for _ in 1:N])
  501. end
  502. end
  503. csave(joinpath(DATAPATH, "task2.dat.zstd"), res2)
  504. res2 = cload(joinpath(DATAPATH, "task2.dat.zstd"))
  505. session_average(x, l = 4) = [mean(x[(i-1)*l+1:i*l]) for i in 1:length(x)÷l]
  506. ks = sort(collect(filter(x -> x != :optimal && split("$x", '_')[end] == "exp", keys(res2))))
  507. # ks = best_of_kind.(Ref(res2), ["increment_exp", "sparse_exp", "state_decay_exp", "chrono_exp"], session_average) |> sort
  508. f2 = @pgf PGFPlotsX.Axis({legend_pos = "outer north east", xlabel = "session",
  509. ylabel = "expected reward"},
  510. [Plot({styles(k)...},
  511. Coordinates(1:200, session_average(res2[k])))
  512. for k in ks]...,
  513. Plot({dotted}, Coordinates(1:200, session_average(res2[:optimal]))),
  514. Plot({dotted, orange}, Coordinates(1:200, fill(0.5, 200))),
  515. PGFPlotsX.Legend([modelnames.(ks); "optimal"; "best linear"
  516. ])
  517. )
  518. pgfsave(joinpath(DOCPATH, "sim2.tikz"), f2)
  519. ###
  520. ### TASK 3
  521. ###
  522. res3 = Dict()
  523. res3[:optimal] = mean([run_task3!(OptimalLearner(), stimuli_task3(trials = 100), Tracker1()).r for _ in 1:10^4])
  524. for (nb, b) in brains
  525. for (nl, l) in learning_modes
  526. id = Symbol(nb, "_", nl)
  527. nl == :exp || continue
  528. η = learning_rate(nb)
  529. N = 10^3
  530. @show id
  531. res3[id] = mean([run_task3!(b(; η, winit = [.5, .5], l...), stimuli_task3(trials = 100), Tracker1()).r for _ in 1:N])
  532. end
  533. end
  534. csave(joinpath(DATAPATH, "task3.dat.zstd"), res3)
  535. res3 = cload(joinpath(DATAPATH, "task3.dat.zstd"))
  536. function best_of_kind(res, key, f = identity)
  537. ks = filter(x -> match(Regex(key), String(x)) !== nothing, keys(res))
  538. argmax(k -> f(res[k])[end], ks)
  539. end
  540. ks = sort(collect(filter(x -> x != :optimal && split("$x", '_')[end] == "exp", keys(res3))))
  541. f3 = @pgf PGFPlotsX.Axis({legend_pos = "outer north east", xlabel = "trial",
  542. ylabel = "expected reward"},
  543. [Plot({styles(k)...},
  544. Coordinates(1:100, res3[k])) for k in ks]...,
  545. Plot({dotted}, Coordinates(1:100, res3[:optimal])),
  546. PGFPlotsX.Legend([modelnames.(ks); "optimal"])
  547. )
  548. pgfsave(joinpath(DOCPATH, "sim3.tikz"), f3)
  549. ###
  550. ### Food Caching Simulation
  551. ###
  552. function stimuli_foodcaching(; T = 20_000, n_color = 10_000, n_shape = 10)
  553. sequence = Token{Int,Int}[]
  554. locations = randperm(n_color-1)
  555. cache_event_counter = 1
  556. for t in 1:T
  557. if length(sequence) > 10 && rand() > 1/3 # probe
  558. # power law sampling of past locations
  559. recall_weight = 1 ./ sqrt.((length(sequence):-1:1))
  560. c = 0
  561. while true
  562. idx = NeuralProcessingOfTime.wsample(recall_weight)
  563. token = sequence[idx]
  564. if token.s > 0 # look only at cache locations
  565. c = token.c
  566. break
  567. end
  568. end
  569. token = Token(c, 0)
  570. else
  571. token = Token(locations[cache_event_counter], rand(1:n_shape-1))
  572. cache_event_counter += 1
  573. end
  574. push!(sequence, token)
  575. end
  576. sequence
  577. end
  578. function optimal_action_foodcaching(history, query)
  579. query.s != 0 && return 2
  580. age = age_at_similarity(history, query, threshold = 1, similarity = NeuralProcessingOfTime.csimilarity)
  581. token = history[end-age+1]
  582. 2-Int(age < (token.s == 0 ? 0 : token.s == 1 ? 10. : token.s == 2 ? 20. : 160.))
  583. end
  584. function optimal_action_oneage_foodcaching(history, query, refage)
  585. query.s != 0 && return 2
  586. age = age_at_similarity(history, query, threshold = 1, similarity = NeuralProcessingOfTime.csimilarity)
  587. token = history[end-age+1]
  588. 2-Int(age < (token.s == 0 ? 0 : refage))
  589. end
  590. function reward_foodcaching(history, query, action)
  591. (query.s != 0 || action == 0 || action == 2) && return 0.
  592. age = age_at_similarity(history, query, threshold = 1, similarity = NeuralProcessingOfTime.csimilarity)
  593. token = history[end-age+1]
  594. float(age < (token.s == 0 ? 0 : token.s == 1 ? 10. : token.s == 2 ? 20. : 160.))-.2
  595. end
  596. function foodcaching_rewards(stimuli, policy)
  597. [reward_foodcaching(stimuli[1:t-1], stimuli[t],
  598. policy(stimuli[1:t-1], stimuli[t]))
  599. for t in eachindex(stimuli)]
  600. end
  601. function special_connections!(bigbrain; n_color, n_shape, only_shape_content = false, ks = [10, 20, 160])
  602. for i in 1:n_color-1, j in 1:2
  603. bigbrain.connections[1].idxs[2*(i-1) + j] = [i]
  604. end
  605. if only_shape_content
  606. for j in 1:n_shape-1, k in eachindex(ks)
  607. post_idx = length(ks)*(j-1) + k
  608. bigbrain.connections[2].idxs[post_idx] = [n_color-1 + j]
  609. bigbrain.connections[3].plasticity[1].latent_state_increment[post_idx, :] .= ks[k]
  610. bigbrain.connections[4].w[1, post_idx] = j == 1 ? .4 : j == 2 ? .9 : 1.2
  611. bigbrain.connections[4].w[2, post_idx] = 0
  612. end
  613. else
  614. for i in 1:n_color-1, j in 1:n_shape-1, k in eachindex(ks)
  615. post_idx = length(ks)*(i-1) + length(ks)*(n_color-1)*(j-1) + k
  616. bigbrain.connections[2].idxs[post_idx] = [i, n_color-1 + j]
  617. bigbrain.connections[3].plasticity[1].latent_state_increment[post_idx, :] .= ks[k]
  618. bigbrain.connections[4].w[1, post_idx] = j == 1 ? .4 : j == 2 ? .9 : 1.2
  619. bigbrain.connections[4].w[2, post_idx] = 0
  620. end
  621. end
  622. # bias
  623. bigbrain.connections[4].w[1, end] = 0.
  624. bigbrain.connections[4].w[2, end] = 1.
  625. bigbrain
  626. end
  627. Random.seed!(1221)
  628. n_color = 1000
  629. n_shape = 4
  630. T = 2*n_color
  631. bigbrain = sparse_random_brain(; act = exp, clipper = identity, n_intermediate = 5*(n_color-1), n_content = 20*(n_shape-1), latent_state_increment_factor = 200, n_color, n_shape, η = .02, η_bias = 0, winit = [1/10, 0], max_fanin = 3, min_fanin = 1, sparsity = 1/n_color, in_weights = [fill(1/(n_color-1), n_color - 1); fill(2/(n_shape-1), n_shape - 1)], content_activation = heaviside)
  632. bigbrain.connections[4].w[:, end] .= [0, 1]
  633. results = []
  634. baseline_policies = (best = optimal_action_foodcaching,
  635. worst = (h, ht) -> optimal_action_foodcaching(h, ht) == 1 ? 2 : 1,
  636. random = (_, _) -> rand(1:2),
  637. all1 = (_, _) -> 1,
  638. all2 = (_, _) -> 2,
  639. oneage1 = (h, ht) -> optimal_action_oneage_foodcaching(h, ht, 10),
  640. oneage2 = (h, ht) -> optimal_action_oneage_foodcaching(h, ht, 20),
  641. oneage3 = (h, ht) -> optimal_action_oneage_foodcaching(h, ht, 160))
  642. Random.seed!(20)
  643. for epoch in 1:30
  644. stimuli = stimuli_foodcaching(; T, n_color, n_shape);
  645. baselines = map(p -> foodcaching_rewards(stimuli, p), baseline_policies)
  646. @time res = run_task!(bigbrain, stimuli, reward_foodcaching, TrackAll(), greedy = true)
  647. @show epoch map(sum, baselines) sum(res.r)
  648. push!(results, (; stimuli, baselines, res))
  649. end
  650. csave(joinpath(DATAPATH, "foodcaching.dat.zstd"), results)
  651. Random.seed!(1221)
  652. n_color = 1000
  653. n_shape = 4
  654. T = 2*n_color
  655. bigbrain = sparse_random_brain(; act = exp, clipper = identity, n_intermediate = 2*(n_color-1), n_content = 3*(n_shape-1), latent_state_increment_factor = 200, n_color, n_shape, η = .02, η_bias = 0., winit = [.5, .5], max_fanin = 3, min_fanin = 2, sparsity = 2/n_color, in_weights = [fill(1/(n_color-1), n_color - 1); fill(2/(n_shape-1), n_shape - 1)], content_activation = heaviside)
  656. special_connections!(bigbrain; n_color, n_shape, only_shape_content = true)
  657. bigbrain.connections[4].w[1, 1:end-1] .= 1.01
  658. bigbrain.connections[4].w[2, 1:end-1] .= 0
  659. results2 = []
  660. Random.seed!(20)
  661. for epoch in 1:30
  662. stimuli = stimuli_bigtask(; T, n_color, n_shape);
  663. baselines = map(p -> bigtask_rewards(stimuli, p), baseline_policies)
  664. @time res = run_task!(bigbrain, stimuli, reward_bigtask, TrackAll(), greedy = true)
  665. @show epoch map(sum, baselines) sum(res.r)
  666. push!(results2, (; stimuli, baselines, res))
  667. end
  668. csave(joinpath(DATAPATH, "foodcaching2.dat.zstd"), results2)
  669. function result_dataframe(results, baseline_policies)
  670. resultdf = vcat([DataFrame(map(p -> foodcaching_rewards(r.stimuli, p),
  671. baseline_policies))
  672. for r in results]...)
  673. resultdf.model = vcat(getproperty.(getproperty.(results, :res), :r)...)
  674. test = vcat([[t.s == 0 for t in r.stimuli] for r in results]...)
  675. resultdf = resultdf[test, :]
  676. end
  677. resultdf1 = result_dataframe(results, baseline_policies)
  678. resultdf2 = result_dataframe(results2, baseline_policies)
  679. struct Smoother
  680. weights::Vector{Float64}
  681. center::Int
  682. stride::Int
  683. end
  684. Smoother(; σ = 500, b = 8, stride = σ÷10) = Smoother([exp(-d^2/σ^2) for d in -b*σ:b*σ], b * σ, stride)
  685. (s::Smoother)(x) = s.(Ref(x), 1:s.stride:length(x))
  686. function (s::Smoother)(x, i)
  687. n = 0.
  688. y = 0.
  689. ws, c = s.weights, s.center
  690. for j in max(1, i - c):min(length(x), i + c)
  691. w = ws[j - i + c + 1]
  692. y += w * x[j]
  693. n += w
  694. end
  695. y/n
  696. end
  697. struct Scaler
  698. min::Vector{Float64}
  699. max::Vector{Float64}
  700. end
  701. (s::Scaler)(x) = @. (x - s.min) / (s.max - s.min)
  702. smoother = Smoother(σ = 500)
  703. scaler = Scaler(smoother(resultdf2.random), smoother(resultdf2.best))
  704. ks = setdiff(names(resultdf1), ["worst", "all2"])
  705. modelname = Dict("best" => "optimal",
  706. "random" => "uniform random",
  707. "all1" => "always retrieve",
  708. "all2" => "never retrieve",
  709. "oneage1" => "retrieve if age \$\\leq\$ 10 steps",
  710. "oneage2" => "retrieve if age \$\\leq\$ 20 steps",
  711. "oneage3" => "retrieve if age \$\\leq\$ 160 steps",
  712. "model" => modelnames("chrono"),
  713. "model2" => modelnames("sparse"))
  714. cs = Dict("best" => @pgf({black, dotted, thick}),
  715. "worst" => @pgf({black, dotted}),
  716. "random" => @pgf({black,dashed}),
  717. "all1" => @pgf({green!50!black}),
  718. "all2" => @pgf({blue,dashed}),
  719. "oneage1" => @pgf({green!50!black,dashed}),
  720. "oneage2" => @pgf({green!50!black,"dash dot"}),
  721. "oneage3" => @pgf({green!50!black,dotted}),
  722. "model" => styles("chrono"),
  723. "model2" => styles("sparse"),
  724. )
  725. f4 = @pgf PGFPlotsX.Axis({legend_pos = "outer north east", xlabel = "retrieval test event",
  726. ylabel = "average normalized reward", ymin = -.1, "very thick"},
  727. Plot(cs["model2"], Coordinates(1:smoother.stride:nrow(resultdf1),
  728. scaler(smoother(resultdf1.model)))),
  729. [Plot(cs[k],
  730. Coordinates(1:smoother.stride:nrow(resultdf1), scaler(smoother(getproperty(resultdf2, k))))) for k in reverse(ks)]...,
  731. PGFPlotsX.Legend(map(x -> modelname[x], reverse([ks; "model2"])))
  732. )
  733. pgfsave(joinpath(DOCPATH, "sim_caching.tikz"), f4)

sims.jl at commit e6c09f2, no license · at the source

Overview

  1. School of Computer and Communication Science, École Polytechnique Fédérale de Lausanne, Lausanne, Switzerland
  2. Brain Mind Institute, School of Life Science, École Polytechnique Fédérale de Lausanne, Lausanne, Switzerland
Journal: PLoS computational biology, volume 22, issue 7, article e1014575
Dates: received 23 June 2025; accepted 14 July 2026; published online 28 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pcbi.1014575 · PMID 42520083 · PMCID PMC13436827 · OpenAlex W4310968086
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: computational modeling (no new data) (modality), human (organism)
Methods: Statistics
MeSH: Memory*, Models, Neurological*, Animals, Computational Biology, Computer Simulation, Humans, Nerve Net, Neuronal Plasticity (* major topic)
Journal subjects: Biology and Life Sciences, Cell Biology, Cellular Types, Animal Cells, Neurons, Neuroscience, Cellular Neuroscience, Cognitive Science, Cognition, Memory, Learning and Memory, Memory Recall, Cognitive Psychology, Learning, Psychology, Social Sciences, Synaptic Plasticity, Developmental Neuroscience, Neuronal Plasticity, Computational Biology, Computational Neuroscience, Coding Mechanisms, Anatomy, Nervous System, Synapses, Medicine and Health Sciences, Physiology, Electrophysiology, Neurophysiology
Topic: Memory and Neural Mechanisms (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 118 references in the paper

Abstract

Humans and animals can remember how long ago specific events happened. Little is known about the neural mechanisms that enable remembering the “when” of memories stored for long durations in the episodic memory system – in contrast to interval-timing on the order of seconds and minutes. Based on a systematic exploration of neural coding, association and retrieval schemes, we develop model classes that span the space of possible mechanisms for the reconstruction of the time of past events. In concrete examples we show how network architecture, Hebbian plasticity, synaptic pruning or systems consolidation allow the retrieval of the time of past events. In a simulation, we demonstrate how these mechanisms would enable food-caching animals such as corvids to remember what they cached, where, and how long ago. To dissociate different hypotheses, we propose three kinds of novel, non-verbal experiments that can be run with humans and animals. Our simulations predict the experimental results for different classes of models. Our study shows that remembering the “when” can be implemented by many biologically plausible mechanisms and that carefully designed experiments are needed to pin down the actual neural implementation of the memory for the time of past events in different species.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 1 match between paragraphs and lines of code.

jbrea/RememberingTheWhen.jl

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: e6c09f28fb90f7f236e2358241bd74a8a47534fb, 1 May 2026
Languages: Julia (3)
Size: 7 files, 3 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, environment (Project.toml, scripts/Manifest.toml, scripts/Project.toml), tests
Not found: license file, CITATION.cff, continuous integration, documentation
Tools: DataFrames.jl (1 file), Distributions.jl (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
4 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 3 scripts, each with its path and the digest of its content;
  • 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability

The code for all simulations is available at https://github.com/jbrea/RememberingTheWhen.jl.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Authors: added Alireza Modirshanechi (0000-0002-7870-8602); Georgios Iatropoulos (0000-0001-6978-311X); Wulfram Gerstner (0000-0002-4344-2189); removed Alireza Modirshanechi; Georgios Iatropoulos; Wulfram Gerstner

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 8 MeSH terms, 1 funder, 106 references.

Cite

This paper

Brea, J., Modirshanechi, A., Iatropoulos, G., & Gerstner, W. (2026). Remembering the "when": Hebbian memory models for the time of past events. PLoS computational biology, 22(7), e1014575. https://doi.org/10.1371/journal.pcbi.1014575

BibTeX

@article{brea2026remembering,
author = {Brea, Johanni and Modirshanechi, Alireza and Iatropoulos, Georgios and Gerstner, Wulfram},
title = {{Remembering the "when": Hebbian memory models for the time of past events}},
journal = {PLoS computational biology},
year = {2026},
month = jul,
volume = {22},
number = {7},
pages = {e1014575},
publisher = {PLOS},
issn = {1553-734X},
doi = {10.1371/journal.pcbi.1014575},
url = {https://doi.org/10.1371/journal.pcbi.1014575},
pmid = {42520083},
pmcid = {PMC13436827}
}

RIS

TY - JOUR
AU - Brea, Johanni
AU - Modirshanechi, Alireza
AU - Iatropoulos, Georgios
AU - Gerstner, Wulfram
TI - Remembering the "when": Hebbian memory models for the time of past events
T2 - PLoS computational biology
J2 - PLoS Comput Biol
PY - 2026
DA - 2026/07/28
VL - 22
IS - 7
SP - e1014575
SN - 1553-734X
PB - PLOS
DO - 10.1371/journal.pcbi.1014575
UR - https://doi.org/10.1371/journal.pcbi.1014575
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pcbi.1014575",
"type": "article-journal",
"title": "Remembering the \"when\": Hebbian memory models for the time of past events",
"container-title": "PLoS computational biology",
"author": [
{
"family": "Brea",
"given": "Johanni"
},
{
"family": "Modirshanechi",
"given": "Alireza"
},
{
"family": "Iatropoulos",
"given": "Georgios"
},
{
"family": "Gerstner",
"given": "Wulfram"
}
],
"container-title-short": "PLoS Comput Biol",
"volume": "22",
"issue": "7",
"page": "e1014575",
"DOI": "10.1371/journal.pcbi.1014575",
"PMID": "42520083",
"PMCID": "PMC13436827",
"ISSN": "1553-734X",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pcbi.1014575",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
28
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pbio.3003759 [code]
Neural population dynamics and temporal context cells in macaque medial parietal cortex support temporal order memory.
Journal: PLoS biology
In common: 9 references
[2] doi:10.1016/j.isci.2026.117391
Context-dependent abstraction of temporal relationships during naturalistic episodic memory.
Journal: iScience
In common: 8 references
[3] doi:10.1371/journal.pcbi.1013235 [code]
Population sparseness determines strength of Hebbian plasticity for maximal memory lifetime in associative networks.
Journal: PLoS computational biology
In common: computational modeling (no new data), 7 references
[4] doi:10.1038/s41586-026-10537-0 [code]
Sparse-to-dense coding transformation between hippocampal areas CA3 and CA1.
Journal: Nature
In common: 7 references
[5] doi:10.1093/pnasnexus/pgag213 [code]
Two-factor synaptic plasticity enables memory consolidation during neuronal burst firing.
Journal: PNAS nexus
In common: DataFrames.jl, Distributions.jl, 2 references
[6] doi:10.7554/elife.107273
Neural representation of time across complementary reference frames.
Journal: eLife
In common: 5 references
[7] doi:10.1038/s41467-026-73999-w [code]
Independence and coherence in temporal sequence computation across the fronto-parietal network.
Journal: Nature communications
In common: 5 references
[8] doi:10.1038/s41467-026-74460-8 [code]
Spike-based alignment learning solves the weight transport problem.
Journal: Nature communications
In common: computational modeling (no new data), 4 references
[9] doi:10.1038/s41540-026-00749-5 [code]
A novel approach to quantify out-of-distribution uncertainty in Neural and Universal Differential Equations.
Journal: NPJ systems biology and applications
In common: DataFrames.jl, Distributions.jl, computational modeling (no new data)
[10] doi:10.1073/pnas.2603114123 [code]
The human hippocampus can pattern separate memories by meaning.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: 4 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.