diff --git a/Scripts/Models (Under Development)/N-back.py b/Scripts/Models (Under Development)/N-back.py index 1c4b97f0ab0..903a82b862a 100644 --- a/Scripts/Models (Under Development)/N-back.py +++ b/Scripts/Models (Under Development)/N-back.py @@ -1,63 +1,100 @@ """ -This implements a model of the `N-back task _` -described in `Beukers et al. `_. The model uses a simple implementation of episodic -memory (i.e., content-retrieval memory) to store previous stimuli and the temporal context in which they occured, -and a feedforward neural network to evaluate whether the current stimulus is a match to the n'th preceding stimulus. +This implements a model of the `N-back task `_ +described in `Beukers et al. (2022) `_. The model uses a simple implementation of episodic +(content-addressable) memory to store previous stimuli and the temporal context in which they occured, +and a feedforward neural network to evaluate whether the current stimulus is a match to the n'th preceding stimulus +(n-back level). This model is an example of proposed interactions between working memory (e.g., in neocortex) and +episodic memory e.g., in hippocampus and/or cerebellum) in the performance of tasks demanding of sequential processing +and control, and along the lines of models emerging machine learning that augment the use of recurrent neural networks +(e.g., long short-term memory mechanisms; LSTMs) for active memory and control with an external memory capable of +rapid storage and content-based retrieval, such as the Neural Turing Machine (NTN; `Graves et al., 2016 +`_), Episodic Planning Networks (EPN; `Ritter et al., 2020 +`_), and Emergent Symbols through Binding Networks (ESBN; `Webb et al., 2021 +`_). + +There are three primary methods in the script: + +* construct_model(args): + takes as arguments parameters used to construct the model; for convenience, defaults are defined below, + (under "Construction parameters") + +* train_network(args) + takes as arguments the feedforward neural network Composition (FFN_COMPOSITION) and number of epochs to train. + Note: learning_rate is set at construction (can specify using LEARNING_RATE under "Training parameters" below). + +* run_model() + takes the context drift rate to be applied on each trial and the number of trials to execute as args, as well as + reporting and animation specifications (see "Execution parameters" below). + +See "Settings for running the script" to specify whether the model is trained and/or executed when the script is run, +and whether a graphic display of the network is generated when it is constructed. TODO: + - from Andre + - network architecture; in particular, size of hidden layer and projection patterns to and from it + - softmax temp on output/decision layer? + - confirm that ReLUs all use 0 thresholds and unit slope + - training: + - confirm learning rate: ?? 0.001 + - epoch: 1 trial per epoch of training + - get empirical stimulus sequences + - put N-back script (with pointer to latest version on PNL) in nback-paper repo - get rid of objective_mechanism (see "VERSION *WITHOUT* ObjectiveMechanism" under control(...) - - from nback-paper: - - get ffn weights? - - why SDIM=20 if it is a one-hot encoding (np.eye), and NSTIM=8? (i.e., SHOULDN'T NUM_STIM == STIM_SIZE)? - - do input layers use logistic (as suggested in figure)? - - construct training set and train in ffn using Autodiff + - pass learning_rate as parameter to train_network() - validate against nback-paper results - - replace get_input_sequence and get_training_inputs with generators passed to nback_model.run() and ffn.learn - - make termination processing part of the Comopsition definition? + - after validation: + - try with STIM_SIZE = NUM_STIMS rather than 20 (as in nback-paper) + - refactor generate_stim_sequence() to use actual empirical stimulus sequences + - replace get_input_sequence and get_training_inputs with generators passed to nback_model.run() and ffn.learn + - make termination processing part of the Composition definition (fix bug) + - fix warnings on run """ from graph_scheduler import * + from psyneulink import * import numpy as np -import itertools - -DISPLAY = False # show visual of model -# REPORTING_OPTIONS = ReportOutput.ON # Console output during run -REPORTING_OPTIONS = ReportOutput.OFF +# Settings for running script: +TRAIN = False +RUN = True +DISPLAY = False # show visual graphic of model # PARAMETERS ------------------------------------------------------------------------------------------------------- -# FROM nback-paper: -# SDIM = 20 -# CDIM = 25 -# indim = 2 * (CDIM + SDIM) -# hiddim = SDIM * 4 -# CONTEXT_DRIFT_RATE=.25 -# CONTEXT_DRIFT_NOISE=.075 -# 'stim_weight':0.05, -# 'smtemp':8, -# HAZARD_RATE=0.04 - -# TEST: -MAX_NBACK_LEVELS = 5 -NBACK_LEVELS = [2,3] -NUM_NBACK_LEVELS = len(NBACK_LEVELS) -# NUM_TASKS=2 # number of different variants of n-back tasks (set sizes) +# Fixed (structural) parameters: +MAX_NBACK_LEVELS = 3 NUM_STIM = 8 # number of different stimuli in stimulus set - QUESTION: WHY ISN"T THIS EQUAL TO STIM_SIZE OR VICE VERSA? -NUM_TRIALS = 48 # number of stimuli presented in a sequence +FFN_TRANSFER_FUNCTION = ReLU + +# Constructor parameters: (values are from nback-paper) STIM_SIZE=20 # length of stimulus vector CONTEXT_SIZE=25 # length of context vector HIDDEN_SIZE=STIM_SIZE*4 # dimension of hidden units in ff -CONTEXT_DRIFT_RATE=.1 # drift rate used for DriftOnASphereIntegrator (function of Context mech) on each trial +NBACK_LEVELS = [2,3] # Currently restricted to these +NUM_NBACK_LEVELS = len(NBACK_LEVELS) CONTEXT_DRIFT_NOISE=0.0 # noise used by DriftOnASphereIntegrator (function of Context mech) -STIM_WEIGHT=.05 # weighting of stimulus field in retrieval from em -CONTEXT_WEIGHT = 1-STIM_WEIGHT # weighting of context field in retrieval from em -SOFT_MAX_TEMP=1/8 # express as gain # precision of retrieval process -HAZARD_RATE=0.04 # rate of re=sampling of em following non-match determination in a pass through ffn +RANDOM_WEIGHTS_INITIALIZATION=RandomMatrix(center=0.0, range=0.1) # Matrix spec used to initialize all Projections +RETRIEVAL_SOFTMAX_TEMP=1/8 # express as gain # precision of retrieval process +RETRIEVAL_HAZARD_RATE=0.04 # rate of re=sampling of em following non-match determination in a pass through ffn +RETRIEVAL_STIM_WEIGHT=.05 # weighting of stimulus field in retrieval from em +RETRIEVAL_CONTEXT_WEIGHT = 1-RETRIEVAL_STIM_WEIGHT # weighting of context field in retrieval from em +DECISION_SOFTMAX_TEMP=1/8 # express as gain # binarity of decision process + +# Training parameters: +NUM_EPOCHS=1000 # nback-paper: 400,000, one trial per epoch +LEARNING_RATE=0.1 # nback-paper: .001 + +# Execution parameters: +CONTEXT_DRIFT_RATE=.1 # drift rate used for DriftOnASphereIntegrator (function of Context mech) on each trial +NUM_TRIALS = 48 # number of stimuli presented in a trial sequence +REPORT_OUTPUT = ReportOutput.OFF # Sets console output during run +REPORT_PROGRESS = ReportProgress.ON # Sets console progress bar during run +ANIMATE = True # {UNIT:EXECUTION_SET} # Specifies whether to generate animation of execution -# MECHANISM AND COMPOSITION NAMES: +# Names of Compositions and Mechanisms: +NBACK_MODEL = "N-Back Model" FFN_COMPOSITION = "WORKING MEMORY (fnn)" FFN_STIMULUS_INPUT = "CURRENT STIMULUS" FFN_CONTEXT_INPUT = "CURRENT CONTEXT" @@ -79,10 +116,11 @@ def construct_model(stim_size = STIM_SIZE, hidden_size = HIDDEN_SIZE, num_nback_levels = NUM_NBACK_LEVELS, context_drift_noise = CONTEXT_DRIFT_NOISE, - retrievel_softmax_temp = SOFT_MAX_TEMP, - retrieval_hazard_rate = HAZARD_RATE, - retrieval_stimulus_weight = STIM_WEIGHT, - context_stimulus_weight = CONTEXT_WEIGHT): + retrievel_softmax_temp = RETRIEVAL_SOFTMAX_TEMP, + retrieval_hazard_rate = RETRIEVAL_HAZARD_RATE, + retrieval_stimulus_weight = RETRIEVAL_STIM_WEIGHT, + retrieval_context_weight = RETRIEVAL_CONTEXT_WEIGHT, + decision_softmax_temp = DECISION_SOFTMAX_TEMP): """Construct nback_model""" # FEED FORWARD NETWORK ----------------------------------------- @@ -90,26 +128,38 @@ def construct_model(stim_size = STIM_SIZE, # inputs: encoding of current stimulus and context, retrieved stimulus and retrieved context, # output: decIsion: match [1,0] or non-match [0,1] # Must be trained to detect match for specified task (1-back, 2-back, etc.) - input_current_stim = TransferMechanism(size=STIM_SIZE, function=Linear, name=FFN_STIMULUS_INPUT) # function=Logistic) - input_current_context = TransferMechanism(size=STIM_SIZE, function=Linear, name=FFN_CONTEXT_INPUT) # function=Logistic) - input_retrieved_stim = TransferMechanism(size=STIM_SIZE, function=Linear, name=FFN_STIMULUS_RETRIEVED) # function=Logistic) - input_retrieved_context = TransferMechanism(size=STIM_SIZE, function=Linear, name=FFN_CONTEXT_RETRIEVED) # function=Logistic) - input_task = TransferMechanism(size=NUM_NBACK_LEVELS, function=Linear, name=FFN_TASK) # function=Logistic) - hidden = TransferMechanism(size=HIDDEN_SIZE, function=Logistic, name=FFN_HIDDEN) - decision = ProcessingMechanism(size=2, name=FFN_OUTPUT) - # TODO: THIS NEEDS TO BE REPLACED BY (OR AT LEAST TRAINED AS) AutodiffComposition - # TRAINING: - # - 50% matches and 50% non-matches - # - all possible stimuli - # - 2back and 3back - # - contexts of various distances - ffn = Composition([{input_current_stim, - input_current_context, - input_retrieved_stim, - input_retrieved_context, - input_task}, - hidden, decision], - name=FFN_COMPOSITION) + input_current_stim = TransferMechanism(name=FFN_STIMULUS_INPUT, + size=stim_size, + function=FFN_TRANSFER_FUNCTION) + input_current_context = TransferMechanism(name=FFN_CONTEXT_INPUT, + size=context_size, + function=FFN_TRANSFER_FUNCTION) + input_retrieved_stim = TransferMechanism(name=FFN_STIMULUS_RETRIEVED, + size=stim_size, + function=FFN_TRANSFER_FUNCTION) + input_retrieved_context = TransferMechanism(name=FFN_CONTEXT_RETRIEVED, + size=context_size, + function=FFN_TRANSFER_FUNCTION) + input_task = TransferMechanism(name=FFN_TASK, + size=num_nback_levels, + function=FFN_TRANSFER_FUNCTION) + hidden = TransferMechanism(name=FFN_HIDDEN, + size=hidden_size, + function=FFN_TRANSFER_FUNCTION) + decision = ProcessingMechanism(name=FFN_OUTPUT, + size=2, function=SoftMax(output=MAX_INDICATOR, + gain=decision_softmax_temp)) + ffn = AutodiffComposition(([{input_current_stim, + input_current_context, + input_retrieved_stim, + input_retrieved_context, + input_task}, + hidden, decision], + RANDOM_WEIGHTS_INITIALIZATION, + ), + name=FFN_COMPOSITION, + learning_rate=LEARNING_RATE + ) # FULL MODEL (Outer Composition, including input, EM and control Mechanisms) ------------------------ @@ -120,11 +170,12 @@ def construct_model(stim_size = STIM_SIZE, context = ProcessingMechanism(name=MODEL_CONTEXT_INPUT, function=DriftOnASphereIntegrator( initializer=np.random.random(CONTEXT_SIZE-1), - noise=CONTEXT_DRIFT_NOISE, + noise=context_drift_noise, dimension=CONTEXT_SIZE)) # Task: task one-hot indicating n-back (1, 2, 3 etc.) - must correspond to what ffn has been trained to do - task = ProcessingMechanism(name=MODEL_TASK_INPUT, size=NUM_NBACK_LEVELS) + task = ProcessingMechanism(name=MODEL_TASK_INPUT, + size=NUM_NBACK_LEVELS) # Episodic Memory: # - entries: stimulus (field[0]) and context (field[1]); randomly initialized @@ -136,10 +187,11 @@ def construct_model(stim_size = STIM_SIZE, SIZE:CONTEXT_SIZE}], function=ContentAddressableMemory( initializer=[[[0]*STIM_SIZE, [0]*CONTEXT_SIZE]], - distance_field_weights=[STIM_WEIGHT, CONTEXT_WEIGHT], + distance_field_weights=[retrieval_stimulus_weight, + retrieval_context_weight], # equidistant_entries_select=NEWEST, selection_function=SoftMax(output=MAX_INDICATOR, - gain=SOFT_MAX_TEMP)), + gain=retrievel_softmax_temp)), ) # Control Mechanism @@ -159,7 +211,8 @@ def construct_model(stim_size = STIM_SIZE, # Outcome=1 if match, else 0 function=lambda x: int(x[0][1]>x[0][0])), # Set ControlSignal for EM[store_prob] - function=lambda outcome: int(bool(outcome) or (np.random.random() > HAZARD_RATE)), + function=lambda outcome: int(bool(outcome) + or (np.random.random() > retrieval_hazard_rate)), # # VERSION *WITHOUT* ObjectiveMechanism: # monitor_for_control=decision, # # Set Evaluate outcome and set ControlSignal for EM[store_prob] @@ -168,12 +221,13 @@ def construct_model(stim_size = STIM_SIZE, # or (np.random.random() > HAZARD_RATE)), control=(STORAGE_PROB, em)) - nback_model = Composition(nodes=[stim, context, task, em, ffn, control], + nback_model = Composition(name=NBACK_MODEL, + nodes=[stim, context, task, em, ffn, control], # # # Terminate trial if value of control is still 1 after first pass through execution # # FIX: STOPS AFTER ~ NUMBER OF TRIALS (?90+); SHOULD BE: NUM_TRIALS*NUM_NBACK_LEVELS + 1 # termination_processing={TimeScale.TRIAL: And(Condition(lambda: control.value), # AfterPass(0, TimeScale.TRIAL))}, - name="N-Back Model") + ) # # Terminate trial if value of control is still 1 after first pass through execution # # FIX: ALL OF THE FOLLOWING STOP AFTER ~ NUMBER OF TRIALS (?90+); SHOULD BE: NUM_TRIALS*NUM_NBACK_LEVELS + 1 # nback_model.scheduler.add_condition(nback_model, And(Condition(lambda: control.value), AfterPass(0, TimeScale.TRIAL))) @@ -193,7 +247,7 @@ def construct_model(stim_size = STIM_SIZE, nback_model.show_graph( # show_cim=True, # show_node_structure=ALL, - # show_dimensions=True) + # show_dimensions=True ) return nback_model @@ -207,15 +261,16 @@ def get_stim_set(num_stim=STIM_SIZE): return np.eye(num_stim) def get_task_input(nback_level): - """Construct input to task Mechanism for a given nback_level, used by run_model() and train_model()""" + """Construct input to task Mechanism for a given nback_level, used by run_model() and train_network()""" task_input = list(np.zeros_like(NBACK_LEVELS)) task_input[nback_level-NBACK_LEVELS[0]] = 1 return task_input -def get_run_inputs(model, nback_level, num_trials): +def get_run_inputs(model, nback_level, context_drift_rate, num_trials): """Construct set of stimulus inputs for run_model()""" - def generate_stim_sequence(nback_level, trial_num, stype=0, num_stim=NUM_STIM, num_trials=NUM_TRIALS): + def generate_stim_sequence(nback_level, trial_num, trial_type=0, num_stim=NUM_STIM, num_trials=NUM_TRIALS): + assert nback_level in {2,3} # At present, only 2- and 3-back levels are supported def gen_subseq_stim(): A = np.random.randint(0,num_stim) @@ -230,67 +285,70 @@ def gen_subseq_stim(): ) return A,B,C,X - def genseqCT(nback_level,trial_num): - assert nback_level in {2,3} - # ABXA / AXA + def generate_match_no_foils_sequence(nback_level,trial_num): + # AXA (2-back) or ABXA (3-back) seq = np.random.randint(0,num_stim,num_trials) A,B,C,X = gen_subseq_stim() # - if nback_level==3: - subseq = [A,B,X,A] - elif nback_level==2: + if nback_level==2: subseq = [A,X,A] + elif nback_level==3: + subseq = [A,B,X,A] seq[trial_num-(nback_level+1):trial_num] = subseq return seq[:trial_num] - def genseqCF(nback_level,trial_num): - # ABXC + def generate_non_match_no_foils_sequence(nback_level,trial_num): + # AXB (2-back) or ABXC (3-back) seq = np.random.randint(0,num_stim,num_trials) A,B,C,X = gen_subseq_stim() # - if nback_level==3: - subseq = [A,B,X,C] - elif nback_level==2: + if nback_level==2: subseq = [A,X,B] + elif nback_level==3: + subseq = [A,B,X,C] seq[trial_num-(nback_level+1):trial_num] = subseq return seq[:trial_num] - def genseqLT(nback_level,trial_num): - # AAXA + def generate_match_with_foil_sequence(nback_level,trial_num): + # AAA (2-back) or AAXA (3-back) seq = np.random.randint(0,num_stim,num_trials) A,B,C,X = gen_subseq_stim() # - if nback_level==3: - subseq = [A,A,X,A] - elif nback_level==2: + if nback_level==2: subseq = [A,A,A] + elif nback_level==3: + subseq = [A,A,X,A] seq[trial_num-(nback_level+1):trial_num] = subseq return seq[:trial_num] - def genseqLF(nback_level,trial_num): - # ABXB + def generate_non_match_with_foil_sequence(nback_level,trial_num): + # XAA (2-back) or ABXB (3-back) seq = np.random.randint(0,num_stim,num_trials) A,B,C,X = gen_subseq_stim() # - if nback_level==3: - subseq = [A,B,X,B] - elif nback_level==2: + if nback_level==2: subseq = [X,A,A] + elif nback_level==3: + subseq = [A,B,X,B] seq[trial_num-(nback_level+1):trial_num] = subseq return seq[:trial_num] - genseqL = [genseqCT,genseqLT,genseqCF,genseqLF] - stim_seq = genseqL[stype](nback_level,trial_num) - # ytarget = [1,1,0,0][stype] + trial_types = [generate_match_no_foils_sequence, + generate_match_with_foil_sequence, + generate_non_match_no_foils_sequence, + generate_non_match_with_foil_sequence] + stim_seq = trial_types[trial_type](nback_level,trial_num) + # ytarget = [1,1,0,0][trial_type] # ctxt = spherical_drift(trial_num) # return stim,ctxt,ytarget return stim_seq - def stim_set_generation(nback_level, num_trials): - stim_sequence = [] - # for seq_int, trial in itertools.product(range(4),np.arange(5,trials)): # This generates all length sequences - for seq_int, trial_num in itertools.product(range(4),[num_trials]): # This generates only longest seq (num_trials) - return stim_sequence.append(generate_stim_sequence(nback_level, trial_num, stype=seq_int, trials=num_trials)) + # def stim_set_generation(nback_level, num_trials): + # stim_sequence = [] + # # for seq_int, trial in itertools.product(range(4),np.arange(5,trials)): # This generates all length sequences + # for trial_type, trial_num in itertools.product(range(4),[num_trials]): # This generates only longest seq ( + # # num_trials) + # return stim_sequence.append(generate_stim_sequence(nback_level, trial_num, trial_type=trial_type, trials=num_trials)) def get_input_sequence(nback_level, num_trials=NUM_TRIALS): """Get sequence of inputs for a run""" @@ -301,11 +359,11 @@ def get_input_sequence(nback_level, num_trials=NUM_TRIALS): return [input_set[trial_seq[i]] for i in range(num_trials)] return {model.nodes[MODEL_STIMULUS_INPUT]: get_input_sequence(nback_level, num_trials), - model.nodes[MODEL_CONTEXT_INPUT]: [[CONTEXT_DRIFT_RATE]]*num_trials, + model.nodes[MODEL_CONTEXT_INPUT]: [[context_drift_rate]]*num_trials, model.nodes[MODEL_TASK_INPUT]: [get_task_input(nback_level)]*num_trials} def get_training_inputs(network, num_epochs, nback_levels): - """Construct set of training stimuli for ffn.learn(), used by train_model() + """Construct set of training stimuli used by ffn.learn() in train_network() Construct one example of each condition: match: stim_current = stim_retrieved and context_current = context_retrieved stim_lure: stim_current = stim_retrieved and context_current != context_retrieved @@ -348,55 +406,85 @@ def get_training_inputs(network, num_epochs, nback_levels): contexts.append(context_fct(CONTEXT_DRIFT_RATE)) # Get current context as one that is next to last from list (leaving last one as potential lure) current_context = contexts.pop(num_nback_levels-1) - context_nback = contexts.pop(0) - context_distractor = contexts[np.random.randint(0,len(contexts))] + # + nback_context = contexts.pop(0) + distractor_context = contexts[np.random.randint(0,len(contexts))] # Assign retrieved stimulus and context accordingly to trial_type for trial_type in trial_types: stim_current.append(current_stim) context_current.append(current_context) + # Assign retrieved stimulus if trial_type in {'match','stim_lure'}: - stim_retrieved.append(stim_current) - else: + stim_retrieved.append(current_stim) + else: # context_lure or non_lure stim_retrieved.append(distractor_stim) + # Assign retrieved context if trial_type in {'match','context_lure'}: - context_retrieved.append(context_nback) - else: - context_retrieved.append(context_distractor) + context_retrieved.append(nback_context) + else: # stimulus_lure or non_lure + context_retrieved.append(distractor_context) + # Assign target if trial_type == 'match': target.append([1,0]) else: target.append([0,1]) current_task.append([task_input]) - training_set = {network.nodes[FFN_STIMULUS_INPUT]: stim_current, - network.nodes[FFN_CONTEXT_INPUT]: context_current, - network.nodes[FFN_STIMULUS_RETRIEVED]: stim_retrieved, - network.nodes[FFN_CONTEXT_RETRIEVED]: context_retrieved, - network.nodes[FFN_TASK]: current_task, - network.nodes[FFN_OUTPUT]: target - } + training_set = {INPUTS: {network.nodes[FFN_STIMULUS_INPUT]: stim_current, + network.nodes[FFN_CONTEXT_INPUT]: context_current, + network.nodes[FFN_STIMULUS_RETRIEVED]: stim_retrieved, + network.nodes[FFN_CONTEXT_RETRIEVED]: context_retrieved, + network.nodes[FFN_TASK]: current_task}, + TARGETS: {network.nodes[FFN_OUTPUT]: target}, + EPOCHS: num_epochs} + return training_set # ======================================== MODEL EXECUTION ============================================================ -def train_model(): - get_training_inputs(num_epochs=1, nback_levels=NBACK_LEVELS) - -def run_model(model, num_trials=NUM_TRIALS, reporting_options=REPORTING_OPTIONS): +def train_network(network, + learning_rate=LEARNING_RATE, + num_epochs=NUM_EPOCHS): + training_set = get_training_inputs(network=network, num_epochs=num_epochs, nback_levels=NBACK_LEVELS) + network.learn(inputs=training_set, + minibatch_size=NUM_TRIALS, + execution_mode=ExecutionMode.LLVMRun) + +def run_model(model, + context_drift_rate=CONTEXT_DRIFT_RATE, + num_trials=NUM_TRIALS, + report_output=REPORT_OUTPUT, + report_progress=REPORT_PROGRESS, + animate=ANIMATE + ): for nback_level in NBACK_LEVELS: - model.run(inputs=get_run_inputs(model, nback_level, num_trials), + model.run(inputs=get_run_inputs(model, nback_level, context_drift_rate, num_trials), # FIX: MOVE THIS TO MODEL CONSTRUCTION ONCE THAT WORKS # Terminate trial if value of control is still 1 after first pass through execution termination_processing={TimeScale.TRIAL: And(Condition(lambda: model.nodes[CONTROLLER].value), AfterPass(0, TimeScale.TRIAL))}, # function arg - report_output=reporting_options) + report_output=report_output, + report_progress=report_progress, + animate=animate + ) # FIX: RESET MEMORY HERE? - print("Number of entries in EM: ", len(model.nodes[EM].memory)) + # print("Number of entries in EM: ", len(model.nodes[EM].memory)) assert len(model.nodes[EM].memory) == NUM_TRIALS*NUM_NBACK_LEVELS + 1 + nback_model = construct_model() -run_model(nback_model) +print('nback_model constructed') +if TRAIN: + print('nback_model training...') + train_network(nback_model.nodes[FFN_COMPOSITION]) + print('nback_model trained') +if RUN: + print('nback_model executing...') + run_model(nback_model) + if REPORT_PROGRESS == ReportProgress.ON: + print('\n') +print(f'nback_model done: {len(nback_model.results)} trials executed') # =========================================================================== diff --git a/docs/source/BeukersNBackModel.rst b/docs/source/BeukersNBackModel.rst new file mode 100644 index 00000000000..ea2a3adf26e --- /dev/null +++ b/docs/source/BeukersNBackModel.rst @@ -0,0 +1,81 @@ + +N-Back Model (Beukers et al., 2022) +================================================================== +`"When Working Memory is Just Working, Not Memory" `_ + +Overview +-------- +This implements a model of the `N-back task `_ +described in `Beukers et al. (2022) `_. The model uses a simple implementation of episodic +memory (EM, as a form of content-retrieval memory) to store previous stimuli along with the temporal context in which +they occured, and a feedforward neural network (FFN)to evaluate whether the current stimulus is a match to the n'th +preceding stimulus (nback-level)retrieved from episodic memory. The temporal context is provided by a randomly +drifting high dimensional vector that maintains a constant norm (i.e., drifts on a sphere). The FFN is +trained, given an n-back level of *n*, to identify when the current stimulus matches one stored in EM +with a temporal context vector that differs by an amount corresponding to *n* time steps of drift. During n-back +performance, the model encodes the current stimulus and temporal context, retrieves an item from EM that matches the +current stimulus, weighted by the similarity of its temporal context vector (i.e., most recent), and then uses the +FFN to evaluate whether it is an n-back match. The model responds "match" if the FFN detects a match; otherwise, it +either responds "non-match" or, with a fixed probability (hazard rate), it uses the current stimulus and temporal +context to retrieve another sample from EM and repeat the evaluation. + +This model is an example of proposed interactions between working memory (e.g., in neocortex) and episodic memory +e.g., in hippocampus and/or cerebellum) in the performance of tasks demanding of sequential processing and control, +and along the lines of models emerging machine learning that augment the use of recurrent neural networks (e.g., long +short-term memory mechanisms; LSTMs) for active memory and control with an external memory capable of rapid storage +and content-based retrieval, such as the Neural Turing Machine (NTN; +`Graves et al., 2016 `_), Episodic Planning Networks (EPN; +`Ritter et al., 2020 `_), and Emergent Symbols through Binding Networks (ESBN; +`Webb et al., 2021 `_). + +The script respectively, to construct, train and run the model: + +* construct_model(args): + takes as arguments parameters used to construct the model; for convenience, defaults are defined toward the top + of the script (see "Construction parameters"). +.. +* train_network(args) + takes as arguments the feedforward neural network Composition (FFN_COMPOSITION) and number of epochs to train. + Note: learning_rate is set at construction (which can be specified using LEARNING_RATE under "Training parameters"). +.. +* run_model() + takes as arguments the drift rate in the temporal context vector to be applied on each trial, + and the number of trials to execute, as well as reporting and animation specifications + (see "Execution parameters"). + +The default parameters are ones that have been fit to empirical data concerning human performance +(taken from `Kane et al., 2007 `_). + + +The Model +--------- + +The models is composed of two `Compositions `: an outer one that contains the full model (nback_model), +and an `AutodiffComposition` (ffn), nested within nback_model (see red box in Figure), that implements the +feedforward neural network (ffn). + +nback_model +~~~~~~~~~~~ + +This contains three input Mechanisms ( + +Both of these are constructed in the construct_model function. +The ffn Composition is trained use + +.. _nback_Fig: + +.. figure:: _static/N-Back_Model_movie.gif + :align: left + :alt: N-Back Model Animation + + +Training +-------- + + +Execution +--------- + + +Script: :download:`N-back.py <../../Scripts/Models (Under Development)/Beukers_N-Back_2022.py>` +.. Script: :download:`N-back.py <../../psyneulink/library/models/Beukers -Back.py>` diff --git a/docs/source/Function.rst b/docs/source/Function.rst index eb23c588103..2dc5b4ca902 100644 --- a/docs/source/Function.rst +++ b/docs/source/Function.rst @@ -12,6 +12,6 @@ Function :maxdepth: 3 .. automodule:: psyneulink.core.components.functions.function - :members: Function_Base, ArgumentTherapy + :members: Function_Base, ArgumentTherapy, RandomMatrix :private-members: :exclude-members: Parameters diff --git a/docs/source/Functions.rst b/docs/source/Functions.rst index 4148855aa37..7388dfd3541 100644 --- a/docs/source/Functions.rst +++ b/docs/source/Functions.rst @@ -9,6 +9,6 @@ Functions UserDefinedFunction .. automodule:: psyneulink.core.components.functions.function - :members: Function_Base, ArgumentTherapy, + :members: Function_Base, ArgumentTherapy, RandomMatrix :private-members: :exclude-members: Parameters \ No newline at end of file diff --git a/docs/source/Models.rst b/docs/source/Models.rst index 8e8eed5db7d..24e4e3889fc 100644 --- a/docs/source/Models.rst +++ b/docs/source/Models.rst @@ -17,3 +17,6 @@ illustrate principles of neural and/or psychological function. • `BotvinickConflictMonitoringModel` • `BustamanteStroopXORLVOCModel` + +• `BeukersNBackModel` + diff --git a/docs/source/_static/N-Back-Model_fig.svg b/docs/source/_static/N-Back-Model_fig.svg new file mode 100644 index 00000000000..836e7b87bc9 --- /dev/null +++ b/docs/source/_static/N-Back-Model_fig.svg @@ -0,0 +1 @@ +N-Back ModelWORKING MEMORY (fnn)TASKCURRENT TASKCONTEXTEPISODIC MEMORY (dict)CURRENT CONTEXTSTIMCURRENT STIMULUSRETRIEVED CONTEXTRETRIEVED STIMULUSREAD/WRITE CONTROLLERHIDDEN LAYEROBJECTIVE MECHANISMDECISION LAYER \ No newline at end of file diff --git a/docs/source/_static/N-Back_Model_movie.gif b/docs/source/_static/N-Back_Model_movie.gif new file mode 100644 index 00000000000..3a11c1f8eeb Binary files /dev/null and b/docs/source/_static/N-Back_Model_movie.gif differ diff --git a/psyneulink/core/components/functions/function.py b/psyneulink/core/components/functions/function.py index 00017dbb734..b5d551ea9e0 100644 --- a/psyneulink/core/components/functions/function.py +++ b/psyneulink/core/components/functions/function.py @@ -166,13 +166,13 @@ from psyneulink.core.globals.registry import register_category from psyneulink.core.globals.utilities import ( convert_to_np_array, get_global_seed, is_instance_or_subclass, object_has_single_value, parameter_spec, parse_valid_identifier, safe_len, - SeededRandomState, contains_type, is_numeric + SeededRandomState, contains_type, is_numeric, random_matrix ) __all__ = [ 'ArgumentTherapy', 'EPSILON', 'Function_Base', 'function_keywords', 'FunctionError', 'FunctionOutputType', 'FunctionRegistry', 'get_param_value_for_function', 'get_param_value_for_keyword', 'is_Function', - 'is_function_type', 'PERTINACITY', 'PROPENSITY' + 'is_function_type', 'PERTINACITY', 'PROPENSITY', 'RandomMatrix' ] EPSILON = np.finfo(float).eps @@ -1201,6 +1201,48 @@ def __init__(self, ) +class RandomMatrix(): + """Function that returns matrix with random elements distributed uniformly around **center** across **range**. + + The **center** and **range** arguments are passed at construction, and used for all subsequent calls. + Once constructed, the function must be called with two floats, **sender_size** and **receiver_size**, + that specify the number of rows and columns of the matrix, respectively. + + Can be used to specify the `matrix ` parameter of a `MappingProjection + `, and to specify a default matrix for Projections in the + construction of a `Pathway` (see `Pathway_Specification_Projections`) or in a call to a Composition's + `add_linear_processing_pathway` method. + + .. technical_note:: + A call to the class calls `random_matrix `, passing **sender_size** and + **receiver_size** to `random_matrix ` as its **num_rows** and **num_cols** + arguments, respectively, and passing the `center `\-0.5 and `range ` + attributes specified at construction to `random_matrix ` as its **offset** + and **scale** arguments, respectively. + + Arguments + ---------- + center : float + specifies the value around which the matrix elements are distributed in all calls to the function. + range : float + specifies range over which all matrix elements are distributed in all calls to the function. + + Attributes + ---------- + center : float + determines the center of the distribution of the matrix elements; + range : float + determines the range of the distribution of the matrix elements; + """ + + def __init__(self, center:float=0.0, range:float=1.0): + self.center=center + self.range=range + + def __call__(self, sender_size:int, receiver_size:int): + return random_matrix(sender_size, receiver_size, offset=self.center - 0.5, scale=self.range) + + def get_matrix(specification, rows=1, cols=1, context=None): """Returns matrix conforming to specification with dimensions = rows x cols or None @@ -1215,6 +1257,7 @@ def get_matrix(specification, rows=1, cols=1, context=None): + INVERSE_HOLLOW_MATRIX: 0's on diagonal, -1's elsewhere (must be square matrix), otherwise generates error + FULL_CONNECTIVITY_MATRIX: all 1's + RANDOM_CONNECTIVITY_MATRIX (random floats uniformly distributed between 0 and 1) + + RandomMatrix (random floats uniformly distributed around a specified center value with a specified range) + 2D list or np.ndarray of numbers Returns 2D array with length=rows in dim 0 and length=cols in dim 1, or none if specification is not recognized @@ -1222,9 +1265,6 @@ def get_matrix(specification, rows=1, cols=1, context=None): # Matrix provided (and validated in _validate_params); convert to array if isinstance(specification, (list, np.matrix)): - # # MODIFIED 4/9/22 OLD: - # return convert_to_np_array(specification) - # MODIFIED 4/9/22 NEW: if is_numeric(specification): return convert_to_np_array(specification) else: @@ -1272,7 +1312,7 @@ def get_matrix(specification, rows=1, cols=1, context=None): return np.random.rand(rows, cols) # Function is specified, so assume it uses random.rand() and call with sender_len and receiver_len - if isinstance(specification, types.FunctionType): + if isinstance(specification, (types.FunctionType, RandomMatrix)): return specification(rows, cols) # (7/12/17 CW) this is a PATCH (like the one in MappingProjection) to allow users to diff --git a/psyneulink/core/components/ports/port.py b/psyneulink/core/components/ports/port.py index d73ab06be0b..5320aabfe4b 100644 --- a/psyneulink/core/components/ports/port.py +++ b/psyneulink/core/components/ports/port.py @@ -779,7 +779,8 @@ def test_multiple_modulatory_projections_with_mech_and_port_Name_specs(self): from psyneulink.core import llvm as pnlvm from psyneulink.core.components.component import ComponentError, DefaultsFlexibility, component_keywords -from psyneulink.core.components.functions.function import Function, get_param_value_for_keyword, is_function_type +from psyneulink.core.components.functions.function import \ + Function, get_param_value_for_keyword, is_function_type, RandomMatrix from psyneulink.core.components.functions.nonstateful.combinationfunctions import CombinationFunction, LinearCombination from psyneulink.core.components.functions.nonstateful.transferfunctions import Linear from psyneulink.core.components.shellclasses import Mechanism, Projection, Port @@ -2953,6 +2954,12 @@ def _parse_port_spec(port_type=None, if isinstance(port_specification, types.FunctionType): port_specification = port_specification() + # RandomMatrix (used for Projection); try to resolve to a matrix + if isinstance(port_specification, RandomMatrix): + rows = len(owner.sender.value) + cols = len(owner.receiver.value) + port_specification = port_specification(rows,cols) + # ModulatorySpecification of some kind if _is_modulatory_spec(port_specification): # If it is a ModulatoryMechanism specification, get its ModulatorySignal class diff --git a/psyneulink/core/components/projections/pathway/mappingprojection.py b/psyneulink/core/components/projections/pathway/mappingprojection.py index 557c1b3dbd4..c6a3871c268 100644 --- a/psyneulink/core/components/projections/pathway/mappingprojection.py +++ b/psyneulink/core/components/projections/pathway/mappingprojection.py @@ -19,7 +19,7 @@ - `MappingProjection_Deferred_Initialization` * `MappingProjection_Structure` - `MappingProjection_Matrix` - - `Mapping_Matrix_ParameterPort` + - `MappingProjection_Matrix_ParameterPort` * `MappingProjection_Execution` - `MappingProjection_Learning` * `MappingProjection_Class_Reference` @@ -98,10 +98,8 @@ ` can be used. .. - * **Random matrix function** (`random_matrix `) -- a convenience function - that provides more flexibility than `RANDOM_CONNECTIVITY_MATRIX`. It generates a random matrix sized for a - **sender** and **receiver**, with random numbers drawn from a uniform distribution within a specified **range** and - with a specified **offset**. + * `RandomMatrix` -- assigns a matrix sized appropriately for the **sender** and **receiver**, with random values + drawn from a uniform distribution with a specified **center** and **range**. .. _MappingProjection_Tuple_Specification: @@ -185,14 +183,14 @@ In addition to its `sender `, `receiver `, and `function `, a MappingProjection has the following characteristic attributes: -.. _Mapping_Matrix: +.. _MappingProjection_Matrix: * `matrix ` parameter - used by the MappingProjection's `function ` to carry out a matrix transformation of its input, that is then provided to its `receiver `. It can be specified in a variety of ways, as described `above `. - .. _Mapping_Matrix_Dimensionality + .. _MappingProjection_Matrix_Dimensionality * **Matrix Dimensionality** -- this must match the dimensionality of the MappingProjection's `sender ` and `receiver `. For a standard 2d "weight" matrix (i.e., @@ -204,7 +202,7 @@ `receiver `'s `variable ` (equal to the dimensionality of the matrix minus its sender dimensionality). -.. _Mapping_Matrix_ParameterPort: +.. _MappingProjection_Matrix_ParameterPort: * *MATRIX* `ParameterPort` - this receives any `LearningProjections ` that are assigned to the MappingProjection (see `MappingProjection_Learning_Specification` above), and updates the current value of the @@ -286,6 +284,7 @@ import copy import numpy as np +from typing import Union from psyneulink.core.components.component import parameter_keywords from psyneulink.core.components.functions.stateful.integratorfunctions import AccumulatorIntegrator @@ -304,7 +303,7 @@ from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel __all__ = [ - 'MappingError', 'MappingProjection', + 'MappingError', 'MappingProjection' ] parameter_keywords.update({MAPPING_PROJECTION}) @@ -355,10 +354,11 @@ class MappingProjection(PathwayProjection_Base): the context in which the Projection is used, or its initialization will be `deferred `. - matrix : list, np.ndarray, np.matrix, function or keyword : default DEFAULT_MATRIX + matrix : list, np.ndarray, np.matrix, function, `RandomMatrix` or keyword : default DEFAULT_MATRIX specifies the matrix used by `function ` (default: `LinearCombination`) to transform the `value ` of the `sender ` into a form suitable - for the `variable ` of its `receiver ` `InputPort`. + for the `variable ` of its `receiver ` `InputPort` + (see `MappingProjection_Matrix_Specification` for additional details). Attributes ---------- diff --git a/psyneulink/core/components/projections/projection.py b/psyneulink/core/components/projections/projection.py index b26b6847cac..796cd14c281 100644 --- a/psyneulink/core/components/projections/projection.py +++ b/psyneulink/core/components/projections/projection.py @@ -106,13 +106,13 @@ * **Keyword** -- creates a default instance of the specified type, which can be any of the following: * *MAPPING_PROJECTION* -- if the `sender ` and/or its `receiver - ` cannot be inferred from the context in which this specification occurs, then its - `initialization is deferred ` until both of those have been - determined (e.g., it is used in the specification of a `pathway ` for a `Process`). For - MappingProjections, a `matrix specification ` can also be used to - specify the projection (see **value** below). - COMMENT: + ` cannot be inferred from the context in which this specification occurs, then + its `initialization is deferred ` until both of those have been + determined (e.g., it is used in the specification of a `Pathway` for a `Composition`). For MappingProjections, + a `matrix specification ` can also be used to specify the Projection + (see **value** below). + COMMENT: * *LEARNING_PROJECTION* (or *LEARNING*) -- this can only be used in the specification of a `MappingProjection` (see `tuple ` format). If the `receiver ` of the MappingProjection projects to a `LearningMechanism` or a `ComparatorMechanism` that projects to one, @@ -122,7 +122,9 @@ `. See `LearningMechanism_Learning_Configurations` for additional details. COMMENT + COMMENT: # FIX 5/8/20 [JDC] ELIMINATE SYSTEM: IS IT TRUE THAT CONTROL SIGNALS ARE AUTOMATICALLY CREATED BY COMPOSITIONS? + COMMENT * *CONTROL_PROJECTION* (or *CONTROL*) -- this can be used when specifying a parameter using the `tuple format `, to create a default `ControlProjection` to the `ParameterPort` for that parameter. If the `Component ` to which the parameter belongs is part of a `Composition`, then a @@ -422,7 +424,8 @@ from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel from psyneulink.core.globals.registry import register_category, remove_instance_from_registry from psyneulink.core.globals.socket import ConnectionInfo -from psyneulink.core.globals.utilities import ContentAddressableList, is_matrix, is_numeric, parse_valid_identifier +from psyneulink.core.globals.utilities import \ + ContentAddressableList, is_matrix, is_numeric, parse_valid_identifier __all__ = [ 'Projection_Base', 'projection_keywords', 'PROJECTION_SPEC_KEYWORDS', diff --git a/psyneulink/core/compositions/composition.py b/psyneulink/core/compositions/composition.py index cd5da94cc78..3a646d39f35 100644 --- a/psyneulink/core/compositions/composition.py +++ b/psyneulink/core/compositions/composition.py @@ -2734,7 +2734,7 @@ def input_function(env, result): from psyneulink.core import llvm as pnlvm from psyneulink.core.components.component import Component, ComponentsMeta from psyneulink.core.components.functions.fitfunctions import make_likelihood_function -from psyneulink.core.components.functions.function import is_function_type +from psyneulink.core.components.functions.function import is_function_type, RandomMatrix from psyneulink.core.components.functions.nonstateful.combinationfunctions import LinearCombination, \ PredictionErrorDeltaFunction from psyneulink.core.components.functions.nonstateful.learningfunctions import \ @@ -5591,6 +5591,7 @@ def add_projection(self, projection=None, sender=None, receiver=None, + default_matrix=None, feedback=False, learning_projection=False, name=None, @@ -5599,7 +5600,9 @@ def add_projection(self, ): """Add **projection** to the Composition. - If **projection** is not specified, create a default `MappingProjection` using **sender** and **receiver**. + If **projection** is not specified, and one does not already exist between **sender** and **receiver** + create a default `MappingProjection` between them, using **default_projection_matrix** if specified + (otherwise default for MappingProjection is used). If **projection** is specified: @@ -5648,15 +5651,23 @@ def add_projection(self, Arguments --------- + projection : Projection, list, array, matrix, RandomMatrix, MATRIX_KEYWORD + the projection to add. + sender : Mechanism, Composition, or OutputPort the sender of **projection**. - projection : Projection, matrix - the projection to add. - receiver : Mechanism, Composition, or InputPort the receiver of **projection**. + default_projection_matrix : list, array, matrix, RandomMatrix, MATRIX_KEYWORD + matrix to use in creating default; overrides default for MappingProjection. + + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use in creating default Projection if none is specifed in **projection** + and one does not already exist between **sender** and **receive** + (see `MappingProjection_Matrix_Specification` for details of specification). + feedback : bool or FEEDBACK : False if False, the Projection is *never* designated as a `feedback Projection `, even if that may have been the default behavior (e.g., @@ -5733,6 +5744,7 @@ def add_projection(self, return self.add_projection(proj_spec, sender=projection.sender, receiver=projection.receiver) # Create Projection if it doesn't exist + projection = projection or default_matrix try: # Note: this does NOT initialize the Projection if it is in deferred_init projection = self._instantiate_projection_from_spec(projection, name) @@ -5918,7 +5930,7 @@ def _instantiate_projection_from_spec(self, projection, sender=None, receiver=No proj_type = projection.pop(PROJECTION_TYPE, None) or MappingProjection params = projection.pop(PROJECTION_PARAMS, None) projection = MappingProjection(params=params) - elif isinstance(projection, (np.ndarray, np.matrix, list)): + elif isinstance(projection, (np.ndarray, np.matrix, list, RandomMatrix)): return MappingProjection(matrix=projection, sender=sender, receiver=receiver, name=name) elif isinstance(projection, str): if projection in MATRIX_KEYWORD_VALUES: @@ -5930,8 +5942,8 @@ def _instantiate_projection_from_spec(self, projection, sender=None, receiver=No elif projection is None: return MappingProjection(sender=sender, receiver=receiver, name=name) elif not isinstance(projection, Projection): - raise CompositionError("Invalid projection ({}) specified for {}. Must be a Projection." - .format(projection, self.name)) + raise CompositionError(f"Invalid projection ({projection}) specified for {self.name}. " + f"Must be a Projection.") return projection def _parse_sender_spec(self, projection, sender): @@ -6384,7 +6396,15 @@ def _parse_pathway(self, pathway, name, pathway_arg_str): if isinstance(pathway, Pathway): # Give precedence to name specified in call to add_linear_processing_pathway pathway_name = name or pathway.name + # MODIFIED 11/3/22 OLD: pathway = pathway.pathway + # # MODIFIED 11/3/22 NEW: + # # If Pathway has default_projection_matrix, use tuple_spec to specify for handling below + # if pathway.default_projection_matrix: + # pathway = (pathway.pathway, pathway. default_projection_matrix) + # else: + # pathway = pathway.pathway + # MODIFIED 11/3/22 END else: pathway_name = name @@ -6557,20 +6577,47 @@ def identify_pway_type_and_parse_tuple_prn(pway, tuple_or_dict_str): pway_type = PROCESSING_PATHWAY if isinstance(pway, set): pway = [pway] - return pway_type, pway, None + return pway_type, pway, None, None elif isinstance(pway, tuple): - pway_type = LEARNING_PATHWAY - if len(pway)!=2: + # FIX: ADD SUPPORT FOR 3-ITEM TUPLE AND SPECIFCATION OF DEFAULT MATRIX HERE 10/29/22 + # # MODIFIED 10/29/22 OLD: + # pway_type = LEARNING_PATHWAY + # if len(pway)!=2: + # raise CompositionError(f"A tuple specified in the {pathways_arg_str}" + # f" has more than two items: {pway}") + # pway, learning_function = pway + # if not (_is_node_spec(pway) or isinstance(pway, (list, Pathway))): + # raise CompositionError(f"The 1st item in {tuple_or_dict_str} specified in the " + # f" {pathways_arg_str} must be a node or a list: {pway}") + # if not (isinstance(learning_function, type) and issubclass(learning_function, LearningFunction)): + # raise CompositionError(f"The 2nd item in {tuple_or_dict_str} specified in the " + # f"{pathways_arg_str} must be a LearningFunction: {learning_function}") + # return pway_type, pway, learning_function + # MODIFIED 10/29/22 NEW: + if len(pway) not in {2,3}: raise CompositionError(f"A tuple specified in the {pathways_arg_str}" - f" has more than two items: {pway}") - pway, learning_function = pway - if not (_is_node_spec(pway) or isinstance(pway, (list, Pathway))): - raise CompositionError(f"The 1st item in {tuple_or_dict_str} specified in the " - f" {pathways_arg_str} must be a node or a list: {pway}") - if not (isinstance(learning_function, type) and issubclass(learning_function, LearningFunction)): - raise CompositionError(f"The 2nd item in {tuple_or_dict_str} specified in the " - f"{pathways_arg_str} must be a LearningFunction: {learning_function}") - return pway_type, pway, learning_function + f" must have either two or three items: {pway}") + pway_type = PROCESSING_PATHWAY + matrix_item = None + learning_function_item = None + for i, item in enumerate(pway): + # Ensure that first item is a Pathway spec + if i==0: + if not (_is_node_spec(item) or isinstance(item, (list, Pathway))): + raise CompositionError(f"The 1st item in {tuple_or_dict_str} specified in the " + f" {pathways_arg_str} must be a node or a list: {pway}") + pathway_item = item + elif (isinstance(item, type) and issubclass(item, LearningFunction)): + pway_type = LEARNING_PATHWAY + learning_function_item = item + elif is_matrix(item): + matrix_item = item + else: + raise CompositionError(f"Bad spec for one of the items in {tuple_or_dict_str} " + f"specified for the {pathways_arg_str}: {item}; " + f"its item(s) must be a matrix specification and/or a LearningFunction") + return pway_type, pathway_item, matrix_item, learning_function_item + # MODIFIED 10/29/22 END else: assert False, f"PROGRAM ERROR: arg to identify_pway_type_and_parse_tuple_prn in {self.name}" \ f"is not a Node, list or tuple: {pway}" @@ -6583,13 +6630,22 @@ def identify_pway_type_and_parse_tuple_prn(pway, tuple_or_dict_str): pway_name = None if isinstance(pathway, Pathway): pway_name = pathway.name + # MODIFIED 11/3/22 OLD: pathway = pathway.pathway + # # MODIFIED 11/3/22 NEW: + # # If Pathway has default_projection_matrix, use tuple_spec to specify for later handling + # if pathway.default_projection_matrix: + # pathway = (pathway.pathway, pathway.default_projection_matrix) + # else: + # pathway = pathway.pathway + # MODIFIED 11/3/22 END if _is_node_spec(pathway) or isinstance(pathway, (list, set, tuple)): if isinstance(pathway, set): bad_entries = [repr(entry) for entry in pathway if not _is_node_spec(entry)] if bad_entries: raise CompositionError(f"{bad_entry_error_msg}{','.join(bad_entries)}") - pway_type, pway, pway_learning_fct = identify_pway_type_and_parse_tuple_prn(pathway, f"a tuple") + pway_type, pway, matrix, pway_learning_fct = identify_pway_type_and_parse_tuple_prn(pathway, + f"the tuple") elif isinstance(pathway, dict): if len(pathway)!=1: raise CompositionError(f"A dict specified in the {pathways_arg_str} " @@ -6599,8 +6655,8 @@ def identify_pway_type_and_parse_tuple_prn(pway, tuple_or_dict_str): raise CompositionError(f"The key in a dict specified in the {pathways_arg_str} must be a str " f"(to be used as its name): {pway_name}.") if _is_node_spec(pway) or isinstance(pway, (list, tuple, Pathway)): - pway_type, pway, pway_learning_fct = identify_pway_type_and_parse_tuple_prn(pway, - f"the value of a dict") + pway_type, pway, matrix, pway_learning_fct = identify_pway_type_and_parse_tuple_prn(pway, + f"the value of a dict") else: raise CompositionError(f"The value in a dict specified in the {pathways_arg_str} must be " f"a pathway specification (Node, list or tuple): {pway}.") @@ -6610,11 +6666,13 @@ def identify_pway_type_and_parse_tuple_prn(pway, tuple_or_dict_str): context.source = ContextFlags.METHOD if pway_type == PROCESSING_PATHWAY: new_pathway = self.add_linear_processing_pathway(pathway=pway, + default_projection_matrix=matrix, name=pway_name, context=context) elif pway_type == LEARNING_PATHWAY: new_pathway = self.add_linear_learning_pathway(pathway=pway, learning_function=pway_learning_fct, + default_projection_matrix=matrix, name=pway_name, context=context) else: @@ -6625,7 +6683,7 @@ def identify_pway_type_and_parse_tuple_prn(pway, tuple_or_dict_str): return added_pathways @handle_external_context() - def add_linear_processing_pathway(self, pathway, name:str=None, context=None, *args): + def add_linear_processing_pathway(self, pathway, default_projection_matrix=None, name:str=None, context=None, *args): """Add sequence of `Nodes ` with optionally intercolated `Projections `. .. _Composition_Add_Linear_Processing_Pathway: @@ -6654,6 +6712,11 @@ def add_linear_processing_pathway(self, pathway, name:str=None, context=None, *a learning-related specifications are ignored, as are its `name ` if the **name** argument of add_linear_processing_pathway is specified. + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification) + name : str species the name used for `Pathway`; supercedes `name ` of `Pathway` object if it is has one. @@ -6769,8 +6832,11 @@ def _get_node_specs_for_entry(entry, include_roles=None, exclude_roles=None): else {pathway[c - 1]}) if all(_is_node_spec(sender) for sender in preceding_entry): senders = _get_node_specs_for_entry(preceding_entry, NodeRole.OUTPUT) - projs = {self.add_projection(sender=s, receiver=r, allow_duplicates=False) + projs = {self.add_projection(sender=s, receiver=r, + default_matrix=default_projection_matrix, + allow_duplicates=False) for r in receivers for s in senders} + # MODIFIED 11/2/22 END if all(projs): projs = projs.pop() if len(projs) == 1 else projs projections.append(projs) @@ -6835,8 +6901,10 @@ def _get_node_specs_for_entry(entry, include_roles=None, exclude_roles=None): # Unpack if tuple spec, and assign feedback (with False as default) default_proj_spec, feedback = (spec if isinstance(spec, tuple) else (spec, False)) # Get all specs other than default_proj_spec - # proj_specs = [proj_spec for proj_spec in all_proj_specs if proj_spec not in possible_default_proj_spec] proj_specs = [proj_spec for proj_spec in all_proj_specs if proj_spec is not spec] + # If default matrix is not specified within the pathway, use default_projection_matrix if specified + if default_proj_spec is None: + default_proj_spec = default_projection_matrix # Collect all Projection specifications (to add to Composition at end) proj_set = [] @@ -7040,6 +7108,7 @@ def handle_duplicates(sender, receiver): pathway = Pathway(pathway=explicit_pathway, composition=self, + # default_projection_matrix=default_projection_matrix, name=pathway_name, context=context) self.pathways.append(pathway) @@ -7060,6 +7129,7 @@ def add_linear_learning_pathway(self, learning_rate:tc.any(int,float)=0.05, error_function=LinearCombination, learning_update:tc.any(bool, tc.enum(ONLINE, AFTER))=AFTER, + default_projection_matrix=None, name:str=None, context=None): """Implement learning pathway (including necessary `learning components `. @@ -7130,6 +7200,11 @@ def add_linear_learning_pathway(self, ` in the pathway, and its `LearningProjection` (see `learning_enabled ` for meaning of values). + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification) + name : str : species the name used for `Pathway`; supercedes `name ` of `Pathway` object if it is has one. @@ -7179,6 +7254,7 @@ def add_linear_learning_pathway(self, loss_function, learning_update, name=pathway_name, + default_projection_matrix=default_projection_matrix, context=context) # If BackPropagation is not specified, then the learning pathway is "one-layered" @@ -7196,6 +7272,7 @@ def add_linear_learning_pathway(self, self._add_required_node_role(output_source, NodeRole.OUTPUT, context) learning_pathway = self.add_linear_processing_pathway(pathway=[input_source, learned_projection, output_source], + default_projection_matrix=default_projection_matrix, name=pathway_name, # context=context) context=context) @@ -7251,6 +7328,7 @@ def add_reinforcement_learning_pathway(self, learning_rate=0.05, error_function=None, learning_update:tc.any(bool, tc.enum(ONLINE, AFTER))=ONLINE, + default_projection_matrix=None, name:str=None): """Convenience method that calls `add_linear_learning_pathway` with **learning_function**=`Reinforcement` @@ -7262,6 +7340,11 @@ def add_reinforcement_learning_pathway(self, specified, that projection is the learned projection. Otherwise, a default MappingProjection is automatically generated for the learned projection. + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification) + learning_rate : float : default 0.05 specifies the `learning_rate ` used for the `ReinforcementLearning` function of the `LearningMechanism` in the **pathway**. @@ -7292,6 +7375,7 @@ def add_reinforcement_learning_pathway(self, learning_function=Reinforcement, error_function=error_function, learning_update=learning_update, + default_projection_matrix=default_projection_matrix, name=name) def add_td_learning_pathway(self, @@ -7299,6 +7383,7 @@ def add_td_learning_pathway(self, learning_rate=0.05, error_function=None, learning_update:tc.any(bool, tc.enum(ONLINE, AFTER))=ONLINE, + default_projection_matrix=None, name:str=None): """Convenience method that calls `add_linear_learning_pathway` with **learning_function**=`TDLearning` @@ -7325,6 +7410,11 @@ def add_td_learning_pathway(self, ` in the pathway, and its `LearningProjection` (see `learning_enabled ` for meaning of values). + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification) + name : str : species the name used for `Pathway`; supercedes `name ` of `Pathway` object if it is has one. @@ -7339,6 +7429,7 @@ def add_td_learning_pathway(self, learning_rate=learning_rate, learning_function=TDLearning, learning_update=learning_update, + default_projection_matrix=default_projection_matrix, name=name) def add_backpropagation_learning_pathway(self, @@ -7347,6 +7438,7 @@ def add_backpropagation_learning_pathway(self, error_function=None, loss_function:tc.enum(MSE,SSE)=MSE, learning_update:tc.optional(tc.any(bool, tc.enum(ONLINE, AFTER)))=AFTER, + default_projection_matrix=None, name:str=None): """Convenience method that calls `add_linear_learning_pathway` with **learning_function**=`Backpropagation` @@ -7376,6 +7468,11 @@ def add_backpropagation_learning_pathway(self, ` in the pathway, and their `LearningProjections ` (see `learning_enabled ` for meaning of values). + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification) + name : str : species the name used for `Pathway`; supercedes `name ` of `Pathway` object if it is has one. @@ -7392,6 +7489,7 @@ def add_backpropagation_learning_pathway(self, loss_function=loss_function, error_function=error_function, learning_update=learning_update, + default_projection_matrix=default_projection_matrix, name=name) # NOTES: @@ -7618,6 +7716,7 @@ def _create_backpropagation_learning_pathway(self, error_function=None, loss_function=MSE, learning_update=AFTER, + default_projection_matrix=None, name=None, context=None): @@ -7631,7 +7730,10 @@ def _create_backpropagation_learning_pathway(self, # Pass ContextFlags.INITIALIZING so that it can be passed on to _analyze_graph() and then # _check_for_projection_assignments() in order to ignore checks for require_projection_in_composition context.string = f"'pathway' arg for add_backpropagation_learning_pathway method of {self.name}" - learning_pathway = self.add_linear_processing_pathway(pathway, name, context) + learning_pathway = self.add_linear_processing_pathway(pathway=pathway, + name=name, + default_projection_matrix=default_projection_matrix, + context=context) processing_pathway = learning_pathway.pathway path_length = len(processing_pathway) @@ -8649,9 +8751,6 @@ def evaluate( buffer_animate_state = self._animate # Run Composition in "SIMULATION" context - # # MODIFIED 3/28/22 NEW: - # context.source = ContextFlags.COMPOSITION - # MODIFIED 3/28/22 END context.add_flag(ContextFlags.SIMULATION_MODE) context.remove_flag(ContextFlags.CONTROL) @@ -9616,14 +9715,15 @@ def run( details and `ReportDevices` for options. animate : dict or bool : default False - specifies use of the `show_graph`show_graph ` method to generate - a gif movie showing the sequence of Components executed in a run (see `example - `). A dict can be specified containing - options to pass to the `show_graph ` method; each key must be a legal - argument for the `show_graph ` method, and its value a specification for that - argument. The entries listed below can also be included in the dict to specify parameters of the - animation. If the **animate** argument is specified simply as `True`, defaults are used for all - arguments of `show_graph ` and the options below: + specifies use of the `show_graph ` method to generate a gif movie showing the + sequence of Components executed in a run (see `example `). + A dict can be specified containing options to pass to the `show_graph ` method in + order to customize the display of the graph in the animation. Each key of the dict must be a legal argument + for the `show_graph ` method, and its value a specification for that argument. + The entries listed below can also be included in the dict to specify parameters of the animation. + If the **animate** argument is specified simply as `True`, defaults are used for all arguments + of `show_graph ` and the options below. See `Animation ` + for additional information. * *UNIT*: *EXECUTION_SET* or *COMPONENT* (default=\\ *EXECUTION_SET*\\ ) -- specifies which Components to treat as active in each call to `show_graph() `. *COMPONENT* generates an @@ -9648,7 +9748,7 @@ def run( * *MOVIE_NAME*: str (default=\\ `name ` + 'movie') -- specifies the name to be used for the movie file; it is automatically appended with '.gif'. - +_ * *SAVE_IMAGES*: bool (default=\\ `False`\\ ) -- specifies whether to save each of the images used to construct the animation in separate gif files, in addition to the file containing the animation. @@ -9660,7 +9760,7 @@ def run( `projection ` in the Composition, if it is not already set. .. note:: - as when setting the `log_condition ` directly, a value of `True` will + As when setting the `log_condition ` directly, a value of `True` will correspond to the `EXECUTION` `LogCondition `. scheduler : Scheduler : default None @@ -9781,6 +9881,8 @@ def run( # Set animation attributes if animate is True: animate = {} + if animate is None: + animate = False self._animate = animate if self._animate is not False: self._set_up_animation(context) @@ -10117,7 +10219,7 @@ def learn( specifies the number of training epochs (that is, repetitions of the batched input set) to run with minibatch_size : int (default=1) - specifies the size of the minibatches to use. The input trials will be batched and ran, after which + specifies the size of the minibatches to use. The input trials will be batched and run, after which learning mechanisms with learning mode TRIAL will update weights randomize_minibatch: bool (default=False) diff --git a/psyneulink/core/compositions/pathway.py b/psyneulink/core/compositions/pathway.py index da18203bc84..98b423201d7 100644 --- a/psyneulink/core/compositions/pathway.py +++ b/psyneulink/core/compositions/pathway.py @@ -115,20 +115,22 @@ *Pathway Projection Specifications* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Where no Projections are specified between entries in the list, default Projections (using a `FULL_CONNECTIVITY_MATRIX`; -see `MappingProjection_Matrix_Specification`) are created from each Node in the first entry, as the sender(s), -to each Node in the second, as receiver(s) (described further `below `). Projections between -Nodes in the two entries can also be specified explicitly, by intercolating a Projection or set of Projections between -the two entries in the list. If the sender and receiver are both a single Mechanism, then a single `MappingProjection` -can be `specified` between them. The same applies if the sender is a `Composition` with -a single `OUTPUT ` Node and/or the receiver is a `Composition` with a single `INPUT ` -Node. If either is a set of Nodes, or is a `nested Composition ` with more than one `INPUT -` or `OUTPUT ` Node, respectively, then a collection of Projections can be specified -between any or all pairs of the Nodes in the set(s) and/or nested Composition(s), using either a set or list of -Projections (order of specification does not matter whether a set or a list is used). The collection can contain -`MappingProjections ` between a specific pairs of Nodes and/or a single default specification -(either a `matrix ` specification or a MappingProjection without any `sender -` or `receiver ` specified). +Where no Projections are specified between entries in the list, default Projections are created (using a +`FULL_CONNECTIVITY_MATRIX`, or the Pathway's `default_projection ` if specified) +from each Node in the first entry, as the sender(s), to each Node in the second, as receiver(s) (described further +`below `). Projections between Nodes in the two entries can also be specified explicitly, by +intercolating a Projection or set of Projections between the two entries in the list. If the sender and receiver are +both a single Mechanism, then a single `MappingProjection` can be `specified` between +them. The same applies if the sender is a `Composition` with a single `OUTPUT ` Node and/or the +receiver is a `Composition` with a single `INPUT ` Node. If either is a set of Nodes, or is a +`nested Composition ` with more than one `INPUT ` or `OUTPUT ` +Node, respectively, then a collection of Projections can be specified between any or all pairs of the Nodes in the +set(s) and/or nested Composition(s), using either a set or list of Projections (order of specification does not matter +whether a set or a list is used). The collection can contain `MappingProjections ` between specific +pairs of Nodes and/or a single default specification (either a `matrix ` specification or a +MappingProjection without any `sender ` or `receiver ` +specified; see MappingProject MappingProjection_Matrix_Specification +). .. _Pathway_Projection_Matrix_Note: @@ -231,9 +233,14 @@ `. Sets can also be used in a list specification (see above; and see `add_linear_processing_pathway ` for additional details). .. - * **2-item tuple**: (Pathway, `LearningFunction`) -- used to specify a `learning Pathway - `; the 1st item must be one of the forms of Pathway specification - described above, and the 2nd item must be a subclass of `LearningFunction`. + .. _Pathway_Specification_Tuple: + + * **2 or 3-item tuple**: (Pathway, , ) -- + used to specify a `learning Pathway ` and/or a matrix to use for any unspecified + Projections (overrides default matrix for `MappingProjection`) if a default projection is not otherwise specified + (see `Pathway_Specification_Projections. The 1st item of the tuple must be one of the forms of Pathway + specification described above. The other items must be a subclass of `LearningFunction` and/or a `matrix + specification `. .. _Pathway_Specification_Multiple: @@ -293,6 +300,9 @@ those `NodeRoles ` is assigned to a corresponding attribute on the Pathway. If the Pathway does not belong to a Composition (i.e., it is a `template `), then these attributes return None. +* `default_projection_matrix ` - matrix used as default for Projections that are + not explicitly specified and for which no default is otherwise specified (see `Pathway_Specification_Projections`). + * `learning_function ` - the LearningFunction assigned to the Pathway if it is a `learning Pathway ` that belongs to a Composition; otherwise it is None. @@ -322,6 +332,7 @@ from psyneulink.core.globals.context import ContextFlags, handle_external_context from psyneulink.core.globals.keywords import \ ANY, CONTEXT, FEEDBACK, MAYBE, NODE, LEARNING_FUNCTION, OBJECTIVE_MECHANISM, PROJECTION, TARGET_MECHANISM +from psyneulink.core.globals.utilities import is_matrix from psyneulink.core.globals.registry import register_category __all__ = [ @@ -416,10 +427,17 @@ class PathwayRole(Enum): class Pathway(object): """ - Pathway( \ - pathway, \ - name=None \ + Pathway( \ + pathway, \ + name=None \ ) + COMMENT: + Pathway( \ + pathway, \ + default_projection_matrix, \ + name=None \ + ) + COMMENT A sequence of `Nodes ` and `Projections ` in a `Composition`, or a template for one that can be assigned to one or more Compositions. @@ -431,8 +449,15 @@ class Pathway(object): specifies list of `Nodes ` and intercolated `Projections ` to be created for the Pathway. + COMMENT: + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + specifies matrix to use for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification) + COMMENT + name : str : default see `name ` - specifies the name of the Pathway; see `name ` for additional information. + specifies the name of the Pathway (see `name ` for additional information). Attributes ---------- @@ -452,6 +477,13 @@ class Pathway(object): Returns an empty list if belongs to a Composition but no `PathwayRoles ` have been assigned, and None if the Pathway is a `tempalte ` (i.e., not assigned to a Composition). + default_projection_matrix : list, array, function, `RandomMatrix` or MATRIX_KEYWORD : default None + matrix used for any unspecified Projections (overrides default matrix for `MappingProjection`) + if a default projection is not otherwise specified (see `Pathway_Specification_Projections`; + see `MappingProjection_Matrix_Specification` for details of specification). A default_projection_matrix + is specified by including it in a tuple specification in the **pathways** argument of the Pathway's + constructor (see `2 or 3-item tuple `). + learning_function : `LearningFunction` or None `LearningFunction` used by `LearningMechanism(s) ` associated with Pathway if it is a `learning pathway `. @@ -500,6 +532,7 @@ class Pathway(object): def __init__( self, pathway:list, + # default_projection_matrix=None, name=None, **kwargs ): @@ -547,6 +580,16 @@ def __init__( self.learning_components = None self.roles = None + # Assign default_projection_matrix attribute + # self.default_projection_matrix = default_projection_matrix + # Parse from tuple spec in **pathway** arg: + self.default_projection_matrix = None + if isinstance(self.pathway, tuple): + for item in self.pathway: + if is_matrix(item): + self.default_projection_matrix = item + assert True + def _assign_roles(self, composition): """Assign `PathwayRoles ` to Pathway based `NodeRoles ` assigned to its `Nodes ` by the **composition** to which it belongs. diff --git a/psyneulink/core/compositions/showgraph.py b/psyneulink/core/compositions/showgraph.py index a4fbe76eb7c..b2d54019bc9 100644 --- a/psyneulink/core/compositions/showgraph.py +++ b/psyneulink/core/compositions/showgraph.py @@ -107,6 +107,22 @@ - `CONTROLLER` : purple - `LEARNING` : orange +.. _ShowGraph_Animation: + +*Animation* +----------- + +An animation can be generated of the execution of a Composition by using the **animate** argument of the Composition's +`run ` method. The animation show a graphical display of the Composition, with each of its +the Components highlighted in the sequence that they are executed. The **animate** can be passed a dict containing +any of the options described above to customize the display, as well as several others used to customize the animation +(see **animate** argument under `run `). + + .. note:: + At present, animation of the Components within a `nested Composition ` is not supported; + the box surrounding the nested Composition is highlighted when it is executed, followed by the next Component(s) + to execute. + .. _ShowGraph_Examples_Visualization: *Examples* @@ -828,7 +844,8 @@ def show_graph(self, show_dimensions, show_projection_labels, show_projections_not_in_composition, - nested_args) + nested_args, + context) # Add cim Components to graph if show_cim if show_cim: @@ -907,7 +924,8 @@ def _assign_processing_components(self, show_dimensions, show_projection_labels, show_projections_not_in_composition, - nested_args): + nested_args, + context): """Assign nodes to graph""" from psyneulink.core.compositions.composition import Composition, NodeRole @@ -922,23 +940,40 @@ def _assign_processing_components(self, COMP_HIERARCHY:comp_hierarchy, # 'composition': rcvr, ENCLOSING_COMP:composition, - NESTING_LEVEL:nesting_level + 1}) + NESTING_LEVEL:nesting_level + 1, + }) # Get subgraph for nested Composition + # # MODIFIED 10/29/22 NEW: FIX: HACK SO NESTED COMPOSITIONS DON'T CRASH ANIMATION (THOUGH STILL NOT SHOWN) + if hasattr(composition, '_animate') and composition._animate is not False: + rcvr._animate = composition._animate + rcvr._set_up_animation(context) + rcvr._animate_num_trials = composition._animate_num_trials + 1 + # MODIFIED 10/29/22 END nested_comp_graph = rcvr._show_graph.show_graph(**nested_args) nested_comp_graph.name = "cluster_" + rcvr.name rcvr_label = rcvr.name + + # Assign color to nested_comp, including highlighting if it is the active_item # if rcvr in composition.get_nodes_by_role(NodeRole.FEEDBACK_SENDER): # nested_comp_graph.attr(color=feedback_color) + # nested_comp_attributes = {"label":rcvr_label} + nested_comp_attributes = {} if rcvr in composition.get_nodes_by_role(NodeRole.INPUT) and \ rcvr in composition.get_nodes_by_role(NodeRole.OUTPUT): - nested_comp_graph.attr(color=self.input_and_output_color) + nested_comp_attributes.update({"color": self.input_and_output_color}) elif rcvr in composition.get_nodes_by_role(NodeRole.INPUT): - nested_comp_graph.attr(color=self.input_color) + nested_comp_attributes.update({"color": self.input_color}) elif rcvr in composition.get_nodes_by_role(NodeRole.PROBE): - nested_comp_graph.attr(color=self.probe_color) + nested_comp_attributes.update({"color": self.probe_color}) elif rcvr in composition.get_nodes_by_role(NodeRole.OUTPUT): - nested_comp_graph.attr(color=self.output_color) + nested_comp_attributes.update({"color": self.output_color}) + if rcvr in active_items: + if self.active_color != BOLD: + nested_comp_attributes.update({"color": self.active_color}) + nested_comp_attributes.update({"penwidth": str(self.default_width + self.active_thicker_by)}) + composition.active_item_rendered = True + nested_comp_graph.attr(**nested_comp_attributes) nested_comp_graph.attr(label=rcvr_label) g.subgraph(nested_comp_graph) @@ -2722,6 +2757,7 @@ def _set_up_animation(self, context): if not isinstance(composition._show_animation, bool): raise ShowGraphError(f"{repr(SHOW)} entry of {repr('animate')} argument for {repr('run')} " f"method of {composition.name} ({composition._show_animation}) must be a boolean.") + elif composition._animate: # composition._animate should now be False or a dict raise ShowGraphError("{} argument for {} method of {} ({}) must be a boolean or " @@ -2737,10 +2773,10 @@ def _animate_execution(self, active_items, context): else: composition._component_animation_execution_count += 1 composition.show_graph(active_items=active_items, - **composition._animate, - output_fmt='gif', - context=context, - ) + **composition._animate, + output_fmt='gif', + context=context, + ) def _generate_gifs(self, G, active_items, context): diff --git a/psyneulink/core/globals/keywords.py b/psyneulink/core/globals/keywords.py index e08ab37b0ca..5bd996546bf 100644 --- a/psyneulink/core/globals/keywords.py +++ b/psyneulink/core/globals/keywords.py @@ -46,9 +46,10 @@ 'DIST_SHAPE', 'DISTANCE_FUNCTION', 'DISTANCE_METRICS', 'DISTRIBUTION_FUNCTION_TYPE', 'DIVISION', 'DRIFT_DIFFUSION_INTEGRATOR_FUNCTION', 'DRIFT_ON_A_SPHERE_INTEGRATOR_FUNCTION', 'DUAL_ADAPTIVE_INTEGRATOR_FUNCTION', 'EFFERENTS', 'EID_SIMULATION', 'EID_FROZEN', 'EITHER', 'ENABLE_CONTROLLER', 'ENABLED', 'ENERGY', 'ENTROPY', - 'EPISODIC_MEMORY_MECHANISM', 'EQUAL', 'ERROR_DERIVATIVE_FUNCTION', 'EUCLIDEAN', 'EVC_MECHANISM', 'EVC_SIMULATION', - 'EXAMPLE_FUNCTION_TYPE', 'EXECUTE_UNTIL_FINISHED', 'EXECUTING', 'EXECUTION', 'EXECUTION_COUNT', 'EXECUTION_ID', - 'EXECUTION_PHASE', 'EXPONENTIAL', 'EXPONENT', 'EXPONENTIAL_DIST_FUNCTION', 'EXPONENTIAL_FUNCTION', 'EXPONENTS', + 'EPISODIC_MEMORY_MECHANISM', 'EPOCHS', 'EQUAL', 'ERROR_DERIVATIVE_FUNCTION', 'EUCLIDEAN', + 'EVC_MECHANISM', 'EVC_SIMULATION', 'EXAMPLE_FUNCTION_TYPE', + 'EXECUTE_UNTIL_FINISHED', 'EXECUTING', 'EXECUTION', 'EXECUTION_COUNT', 'EXECUTION_ID', 'EXECUTION_PHASE', + 'EXPONENTIAL', 'EXPONENT', 'EXPONENTIAL_DIST_FUNCTION', 'EXPONENTIAL_FUNCTION', 'EXPONENTS', 'FEEDBACK', 'FITZHUGHNAGUMO_INTEGRATOR_FUNCTION', 'FINAL', 'FLAGS', 'FULL', 'FULL_CONNECTIVITY_MATRIX', 'FUNCTION', 'FUNCTIONS', 'FUNCTION_COMPONENT_CATEGORY','FUNCTION_CHECK_ARGS', 'FUNCTION_OUTPUT_TYPE', 'FUNCTION_OUTPUT_TYPE_CONVERSION', 'FUNCTION_PARAMS', @@ -111,9 +112,9 @@ 'SEPARATOR_BAR', 'SHADOW_INPUT_NAME', 'SHADOW_INPUTS', 'SIMPLE', 'SIMPLE_INTEGRATOR_FUNCTION', 'SIMULATIONS', 'SINGLETON', 'SIZE', 'SLOPE', 'SOFT_CLAMP', 'SOFTMAX_FUNCTION', 'SOURCE', 'SSE', 'STABILITY_FUNCTION', 'STANDARD_ARGS', 'STANDARD_DEVIATION', 'STANDARD_OUTPUT_PORTS', 'SUBTRACTION', 'SUM', - 'TARGET', 'TARGET_MECHANISM', 'TARGET_LABELS_DICT', 'TERMINAL', 'TERMINATION_MEASURE', 'TERMINATION_THRESHOLD', - 'TERMINATION_COMPARISION_OP', 'TERSE', 'TEXT', 'THRESHOLD', 'TIME', 'TIME_STEP_SIZE', 'TIME_STEPS_DIM', - 'TRAINING_SET', + 'TARGET', 'TARGET_MECHANISM', 'TARGET_LABELS_DICT', 'TERMINAL', 'TARGETS', + 'TERMINATION_MEASURE', 'TERMINATION_THRESHOLD', 'TERMINATION_COMPARISION_OP', 'TERSE', 'TEXT', 'THRESHOLD', + 'TIME', 'TIME_STEP_SIZE', 'TIME_STEPS_DIM', 'TRAINING_SET', 'TRANSFER_FUNCTION_TYPE', 'TRANSFER_MECHANISM', 'TRANSFER_WITH_COSTS_FUNCTION', 'TRIAL', 'TRIALS_DIM', 'UNCHANGED', 'UNIFORM_DIST_FUNCTION', 'USER_DEFINED_FUNCTION', 'USER_DEFINED_FUNCTION_TYPE', @@ -413,6 +414,8 @@ def _is_metric(metric): LEARNING_PATHWAY = "learning_pathway" NODE = 'NODE' INPUTS = 'inputs' +TARGETS = 'targets' +EPOCHS = 'epochs' # Used in show_graph for show_nested NESTED = 'nested' diff --git a/psyneulink/core/globals/utilities.py b/psyneulink/core/globals/utilities.py index b75ae1965c7..0adb9969835 100644 --- a/psyneulink/core/globals/utilities.py +++ b/psyneulink/core/globals/utilities.py @@ -359,7 +359,11 @@ def is_matrix(m): try: return is_matrix(m()) except: - return False + try: + # random_matrix and RandomMatrix are allowable functions, but require num_rows and num_cols parameters + return is_matrix(1,2) + except: + return False return False @@ -498,13 +502,11 @@ def iscompatible(candidate, reference=None, **kargs): if is_matrix_spec(reference): return is_matrix(candidate) - # MODIFIED 10/29/17 NEW: # IMPLEMENTATION NOTE: This allows a number in an ndarray to match a float or int # If both the candidate and reference are either a number or an ndarray of dim 0, consider it a match if ((is_number(candidate) or (isinstance(candidate, np.ndarray) and candidate.ndim == 0)) or (is_number(reference) or (isinstance(reference, np.ndarray) and reference.ndim == 0))): return True - # MODIFIED 10/29/17 END # IMPLEMENTATION NOTE: # modified to allow numeric type mismatches (e.g., int and float; @@ -1037,30 +1039,42 @@ def get_value_from_array(array): :return: """ -def random_matrix(sender, receiver, clip=1, offset=0): +def random_matrix(num_rows, num_cols, offset=0.0, scale=1.0): """Generate a random matrix - Calls np.random.rand to generate a 2d np.array with random values. + Calls np.random.rand to generate a 2d np.array with random values and shape (num_rows, num_cols): + + :math:`matrix = (random[0.0:1.0] + offset) * scale + + With the default values of **offset** and **scale**, values of matrix are floats between 0 and 1. + However, **offset** can be used to center the range on other values (e.g., **offset**=-0.5 centers values on 0), + and **scale** can be used to narrow or widen the range. As a conveniuence the keyword 'ZERO_CENTER' can be used + in place of -.05. Arguments ---------- - sender : int + num_rows : int specifies number of rows. - receiver : int - spcifies number of columns. + num_cols : int + specifies number of columns. - range : int - specifies upper limit (lower limit = 0). + offset : float or 'zero_center' + specifies amount added to each entry of the matrix before it is scaled. - offset : int - specifies amount added to each entry of the matrix. + scale : float + specifies amount by which random value + **offset** is multiplicatively scaled. Returns ------- 2d np.array """ - return (clip * np.random.rand(sender, receiver)) + offset + if isinstance(offset,str): + if offset.upper() == 'ZERO_CENTER': + offset = -0.5 + else: + raise UtilitiesError(f"'offset' arg of random_matrix must be a number of 'zero_center'") + return (np.random.rand(num_rows, num_cols) + offset) * scale def underscore_to_camelCase(item): item = item[1:] diff --git a/psyneulink/library/compositions/autodiffcomposition.py b/psyneulink/library/compositions/autodiffcomposition.py index 28b4e6cf81d..7a001ae3b75 100644 --- a/psyneulink/library/compositions/autodiffcomposition.py +++ b/psyneulink/library/compositions/autodiffcomposition.py @@ -26,10 +26,11 @@ Overview -------- -.. warning:: As of PsyNeuLink 0.7.5, the API for using AutodiffCompositions has been slightly changed! Please see `this link ` for more details! +.. warning:: As of PsyNeuLink 0.7.5, the API for using AutodiffCompositions has been slightly changed! + Please see `this link ` for more details! AutodiffComposition is a subclass of `Composition` used to train feedforward neural network models through integration -with `PyTorch `_, a popular machine learning library, which executes considerably more quickly +with `PyTorch `_, a machine learning library that executes considerably more quickly than using the `standard implementation of learning ` in a Composition, using its `learning methods `. An AutodiffComposition is configured and run similarly to a standard Composition, with some exceptions that are described below. @@ -44,14 +45,16 @@ An AutodiffComposition can be created by calling its constructor, and then adding `Components ` using the standard `Composition methods ` for doing so. The constructor also includes an number of -parameters that are specific to the AutodiffComposition. See the for a list of these parameters. +parameters that are specific to the AutodiffComposition. See `AutodiffComposition_Class_Reference` for a list of +these parameters. .. warning:: Mechanisms or Projections should not be added to or deleted from an AutodiffComposition after it has been run for the first time. Unlike an ordinary Composition, AutodiffComposition does not support this functionality. .. warning:: When comparing models built in PyTorch to those using AutodiffComposition, - the `bias ` parameter of PyTorch modules should be set to `False`, as AutodiffComposition does not currently support trainable biases. + the `bias ` parameter of PyTorch modules + should be set to `False`, as AutodiffComposition does not currently support trainable biases. .. _AutodiffComposition_Execution: @@ -59,7 +62,8 @@ Execution --------- -An AutodiffComposition's `run `, `execute `, and `learn ` methods are the same as for a `Composition`. +An AutodiffComposition's `run `, `execute `, and `learn ` +methods are the same as for a `Composition`. The following is an example showing how to create a simple AutodiffComposition, specify its inputs and targets, and run it with learning enabled and disabled. @@ -224,6 +228,7 @@ class Parameters(Composition.Parameters): # TODO (CW 9/28/18): add compositions to registry so default arg for name is no longer needed @check_user_specified def __init__(self, + pathways=None, learning_rate=None, optimizer_type='sgd', weight_decay=0, @@ -233,7 +238,6 @@ def __init__(self, disable_cuda=True, cuda_index=None, force_no_retain_graph=False, - pathways=None, name="autodiff_composition"): if not torch_available: diff --git a/tests/composition/test_composition.py b/tests/composition/test_composition.py index 01dd5053c73..6377b1555eb 100644 --- a/tests/composition/test_composition.py +++ b/tests/composition/test_composition.py @@ -1012,6 +1012,64 @@ def test_various_pathway_configurations_in_constructor(self, config): assert all(node in comp.get_nodes_by_role(NodeRole.INPUT) for node in {A,C}) assert all(node in comp.get_nodes_by_role(NodeRole.OUTPUT) for node in {B,D}) + config = [ + ('([{A,B,C},D,E],Proj)', 'a'), + ('([{A,B,C},Proj_1,D,E],Proj_2)', 'b'), + ('([{A,B,C},D,Proj_1,E],Proj_2)', 'c'), + ('Pathway(default_matrix)', 'd'), + ('([A,B,C],Proj_2,learning_fct)', 'e'), + ('([A,B,C],Proj_2,learning_fct)', 'f'), + # ('([{A,B,C},D,Proj_1,E],Proj_2,learning_fct)', 'g'), # set spec for Projections + # ('([{A,B,C},D,Proj_1,E],learning_fct,Proj_2)', 'h'), # not yet supported for learning Pathways + ] + @pytest.mark.parametrize('config', config, ids=[x[0] for x in config]) + def test_pathway_tuple_specs(self, config): + A = ProcessingMechanism(name='A') + B = ProcessingMechanism(name='B') + # B_comparator = ComparatorMechanism(name='B COMPARATOR') + C = ProcessingMechanism(name='C') + D = ProcessingMechanism(name='D') + E = ProcessingMechanism(name='E') + F = ProcessingMechanism(name='F') + if config[1]=='a': + comp = Composition(([{A,B,C},D,E],[2.9])) + assert all([p.matrix.base==2.9 for p in D.path_afferents]) + assert E.path_afferents[0].matrix.base==2.9 + if config[1]=='b': + comp = Composition(([{A,B,C},[1.6],D,E],[2.9])) + assert all([p.matrix.base==1.6 for p in D.path_afferents]) + assert E.path_afferents[0].matrix.base==2.9 + if config[1]=='c': + comp = Composition(([{A,B,C},D,[1.6],E],[2.9])) + assert all([p.matrix.base==2.9 for p in D.path_afferents]) + assert E.path_afferents[0].matrix.base==1.6 + if config[1]=='d': + # pway=Pathway([{A,B,C},[1.6],D,E], default_projection_matrix=[2.9]) + pway=Pathway(([{A,B,C},[1.6],D,E], [2.9])) + comp = Composition(pway) + assert all([p.matrix.base==1.6 for p in D.path_afferents]) + assert E.path_afferents[0].matrix.base==2.9 + if config[1]=='e': + comp = Composition(([A,B,C],BackPropagation,[2.9])) + assert B.path_afferents[0].matrix.base==2.9 + assert C.path_afferents[0].matrix.base==2.9 + assert comp.pathways[0].learning_function == BackPropagation + if config[1]=='f': + comp = Composition(([A,B,C],[2.9],BackPropagation)) + assert B.path_afferents[0].matrix.base==2.9 + assert C.path_afferents[0].matrix.base==2.9 + assert comp.pathways[0].learning_function == BackPropagation + if config[1]=='g': + comp = Composition(([{A,B,C},D,[1.6],E],BackPropagation,[2.9])) + assert all([p.matrix.base==2.9 for p in D.path_afferents]) + assert E.path_afferents[0].matrix.base==1.6 + assert comp.pathways[0].learning_function == BackPropagation + if config[1]=='h': + comp = Composition(([{A,B,C},D,[1.6],E],[2.9],BackPropagation)) + assert all([p.matrix.base==2.9 for p in D.path_afferents]) + assert E.path_afferents[0].matrix.base==1.6 + assert comp.pathways[0].learning_function == BackPropagation + def test_add_pathways_bad_arg_error(self): I = InputPort(name='I') c = Composition() @@ -1607,7 +1665,9 @@ def test_composition_learning_pathway_dict_with_no_learning_fct_in_tuple_error(s C = ProcessingMechanism(name='C') with pytest.raises(pnl.CompositionError) as error_text: c = Composition(pathways=[{'P1': ([A,B],C)}]) - assert ("The 2nd item" in str(error_text.value) and "must be a LearningFunction" in str(error_text.value)) + assert ("Bad spec for one of the items in the value of a dict specified for the \'pathways\' arg " + "of the constructor for Composition-0: (ProcessingMechanism C); " + "its item(s) must be a matrix specification and/or a LearningFunction" in str(error_text.value)) class TestProperties: diff --git a/tests/mdf/model_basic.yml b/tests/mdf/model_basic.yml new file mode 100644 index 00000000000..a9b8ad2af8f --- /dev/null +++ b/tests/mdf/model_basic.yml @@ -0,0 +1,313 @@ +comp: + format: ModECI MDF v0.4.3 + generating_application: PsyNeuLink v0.12.1.0+135.g211a8db3af.dirty + graphs: + comp: + metadata: + type: Composition + simulation_results: [] + variable: + - 0 + results: [] + has_initializers: false + retain_old_simulation_data: false + execute_until_finished: true + max_executions_before_finished: 1000 + input_specification: null + node_ordering: + - A + - B + required_node_roles: [] + controller: null + nodes: + A: + metadata: + type: TransferMechanism + termination_measure_value: 0.0 + output_labels_dict: {} + has_initializers: false + max_executions_before_finished: 1000 + variable: + - - 0 + input_labels_dict: {} + input_port_variables: null + execute_until_finished: true + termination_comparison_op: <= + termination_measure: + id: Distance_Function_2_1 + metadata: + type: Distance + enable_output_type_conversion: false + variable: + - - - 0 + - - - 0 + output_type: FunctionOutputType.DEFAULT + has_initializers: false + changes_shape: false + execute_until_finished: true + max_executions_before_finished: 1000 + metric: max_abs_diff + normalize: false + function: distance + args: {} + input_ports: null + termination_threshold: null + integrator_mode: false + on_resume_integrator_mode: current_value + output_ports: + - RESULTS + integrator_function: + id: AdaptiveIntegrator_Function_0 + metadata: + type: AdaptiveIntegrator + has_initializers: true + max_executions_before_finished: 1000 + variable: + - - 0 + output_type: FunctionOutputType.DEFAULT + changes_shape: false + enable_output_type_conversion: false + initializer: + - - 0 + execute_until_finished: true + args: + offset: 0.0 + noise: 0.0 + rate: 0.5 + value: (1 - rate) * previous_value + rate * variable0 + + noise + offset + clip: null + integrator_function_value: + - - 0 + input_ports: + A_InputPort_0: + metadata: + type: InputPort + require_projection_in_composition: true + variable: + - 0 + has_initializers: false + internal_only: false + shadow_inputs: null + execute_until_finished: true + max_executions_before_finished: 1000 + weight: null + projections: null + combine: null + default_input: null + exponent: null + shape: + - 1 + type: int64 + functions: + A_Linear_Function_6: + metadata: + type: Linear + enable_output_type_conversion: true + variable: + - - 0 + output_type: FunctionOutputType.NP_2D_ARRAY + has_initializers: false + changes_shape: false + execute_until_finished: true + max_executions_before_finished: 1000 + bounds: null + function: linear + args: + intercept: 2.0 + slope: 5.0 + variable0: A_InputPort_0 + output_ports: + A_RESULT: + metadata: + type: OutputPort + require_projection_in_composition: true + variable: + - 2.0 + has_initializers: false + execute_until_finished: true + max_executions_before_finished: 1000 + projections: null + value: A_Linear_Function_6 + shape: + - 1 + type: float64 + B: + metadata: + type: TransferMechanism + termination_measure_value: 0.0 + output_labels_dict: {} + has_initializers: false + max_executions_before_finished: 1000 + variable: + - - 0 + input_labels_dict: {} + input_port_variables: null + execute_until_finished: true + termination_comparison_op: <= + termination_measure: + id: Distance_Function_2_3 + metadata: + type: Distance + enable_output_type_conversion: false + variable: + - - - 0 + - - - 0 + output_type: FunctionOutputType.DEFAULT + has_initializers: false + changes_shape: false + execute_until_finished: true + max_executions_before_finished: 1000 + metric: max_abs_diff + normalize: false + function: distance + args: {} + input_ports: null + termination_threshold: null + integrator_mode: false + on_resume_integrator_mode: current_value + output_ports: + - RESULTS + integrator_function: + id: AdaptiveIntegrator_Function_1 + metadata: + type: AdaptiveIntegrator + has_initializers: true + max_executions_before_finished: 1000 + variable: + - - 0 + output_type: FunctionOutputType.DEFAULT + changes_shape: false + enable_output_type_conversion: false + initializer: + - - 0 + execute_until_finished: true + args: + offset: 0.0 + noise: 0.0 + rate: 0.5 + value: (1 - rate) * previous_value + rate * variable0 + + noise + offset + clip: null + integrator_function_value: + - - 0 + input_ports: + B_InputPort_0: + metadata: + type: InputPort + require_projection_in_composition: true + variable: + - 0 + has_initializers: false + internal_only: false + shadow_inputs: null + execute_until_finished: true + max_executions_before_finished: 1000 + weight: null + projections: null + combine: null + default_input: null + exponent: null + shape: + - 1 + type: int64 + functions: + B_Logistic_Function_0: + metadata: + type: Logistic + has_initializers: false + max_executions_before_finished: 1000 + variable: + - - 0 + output_type: FunctionOutputType.NP_2D_ARRAY + changes_shape: false + enable_output_type_conversion: true + execute_until_finished: true + bounds: + - 0 + - 1 + function: logistic + args: + offset: 0.0 + scale: 1.0 + x_0: 0 + bias: 0.0 + gain: 1.0 + variable0: B_InputPort_0 + output_ports: + B_RESULT: + metadata: + type: OutputPort + require_projection_in_composition: true + variable: + - 0.5 + has_initializers: false + execute_until_finished: true + max_executions_before_finished: 1000 + projections: null + value: B_Logistic_Function_0 + shape: + - 1 + type: float64 + edges: + MappingProjection_from_A_RESULT__to_B_InputPort_0_: + sender: A + receiver: B + sender_port: A_RESULT + receiver_port: B_InputPort_0 + metadata: + type: MappingProjection + has_initializers: false + execute_until_finished: true + max_executions_before_finished: 1000 + weight: null + exponent: null + functions: + LinearMatrix_Function_0: + metadata: + type: LinearMatrix + enable_output_type_conversion: false + A: + - 2.0 + output_type: FunctionOutputType.DEFAULT + has_initializers: false + changes_shape: false + execute_until_finished: true + max_executions_before_finished: 1000 + bounds: null + function: onnx::MatMul + args: + B: + - - 1.0 + parameters: + weight: 1 + conditions: + node_specific: + A: + type: EveryNPasses + kwargs: + n: 1 + time_scale: TimeScale.ENVIRONMENT_STATE_UPDATE + B: + type: EveryNCalls + kwargs: + dependency: A + n: 2 + termination: + environment_sequence: + type: AfterNEnvironmentStateUpdates + kwargs: + n: 1 + time_scale: TimeScale.ENVIRONMENT_SEQUENCE + environment_state_update: + type: All + kwargs: + args: + - type: Not + kwargs: + condition: + type: BeforeNCalls + kwargs: + dependency: B + n: 5 + time_scale: TimeScale.ENVIRONMENT_STATE_UPDATE