Extending FCM
Contents
Extending FCM#
FCM models can be extended in different ways to overcome its shortcomings, one of which is Interval-Valued Fuzzy Cognitive Maps (IVFCMs) proposed by Hajek and Prochazka 1. This tutorial introduces an example of modifying the library to fit the FCM model used. This will be done by extending upon the previous insurgency model.
Since the node values and the edge weights for the IVFCMS are defined as intervals, the initial values and edge weights will be initiated as followed:
import networkx as nx
import cuda-hybrid as ch
def create_insurgency_fcm():
# there are two data about each edge, lower and upper bounds
FCM = nx.read_edgelist('insurgency_simple.txt', nodetype=str,
data=([('lower', float), ('upper', float)]), create_using=nx.DiGraph())
# Each node interval will use an initial random value which then be used to create an interval
# and the results will be bounded between [0,1]
val = np.random.random()
FCM.nodes["EconomicDevelopment"]["val"] = [max(0, val - 0.02), min(1, val + 0.02)]
val = np.random.random()
FCM.nodes["Rebelliousness"]["val"] = [max(0, val - 0.03), min(1, val + 0.03)]
val = np.random.random()
FCM.nodes["AbilityOfInsurgentsToControlThePopulation"]["val"] = [max(0, val - 0.04), min(1, val + 0.04)]
return FCM
# Create the ABM/FCM hybrid model using the n
G = nx.G = nx.newman_watts_strogatz_graph(2000, 2, 2)
for agent in G.nodes():
G.nodes[agent]["FCM"] = create_insurgency_fcm()
hm = ch.HybridModel(G)
For this model, node values are randomly generated but for others, there might be certain rules that must be followed. For simplicity, no such rules are proposed for this model.
Since the node values are now intervals, there are a few changes that need to be made to the library. In the original HybridModel
, there are three 2D numpy arrays: node_val
, node_future_val
, and FCM_adj
, which will need to be changed to a 3D array to accommodate the intervals. In transformNetwork
method, replace:
self.FCM_adj = nx.to_numpy_array(G.nodes[list(G.nodes())[0]]["FCM"], dtype=np.float32)
with:
# Get the size of the FCM adjecency matrix, which will be a 3D array, with the last dimension set as 2
# for lower and upper bounds
fcm_edges = len(G.nodes[list(G.nodes())[0]]["FCM"].nodes())
self.FCM_adj = np.zeros((fcm_edges, fcm_edges, 2), dtype=np.float32)
# Get the edge list from the graph and populate the adjacency matrix. If there is no edge between
# two nodes, the interval will be [0. 0.]
edge_lst = G.nodes[list(G.nodes())[0]]["FCM"].edges.data()
for edge in edge_lst:
fr = self.fcm_labels[edge[0]]
to = self.fcm_labels[edge[1]]
weight = edge[2]
self.FCM_adj[fr][to][0] = weight["lower"]
self.FCM_adj[fr][to][1] = weight["upper"]
and:
# store the node values and future node values that will serve as a buffer
fcm_edges = len(G.nodes[list(G.nodes())[0]]["FCM"].nodes())
abm_edges = len(G.nodes())
self.node_val = np.zeros((abm_edges, fcm_edges), dtype=np.float32)
self.node_future_val = np.zeros((abm_edges, fcm_edges), dtype=np.float32)
# create a nested loop of the FCM node attribute and the value
for i, node in enumerate(G.nodes(data="FCM")):
for j, fcm_node in enumerate(node[1].nodes(data="val")):
# store this value
self.node_val[i][j] = fcm_node[1]
self.node_future_val[i][j] = fcm_node[1]
with:
# store the node values and future node values that will serve as a buffer
fcm_edges = len(G.nodes[list(G.nodes())[0]]["FCM"].nodes())
abm_edges = len(G.nodes())
# Added a third dimensions of 2 for the intervals
self.node_val = np.zeros((abm_edges, fcm_edges, 2), dtype=np.float32)
self.node_future_val = np.zeros((abm_edges, fcm_edges, 2), dtype=np.float32)
# create a nested loop of the FCM node attribute and the value
for i, node in enumerate(G.nodes(data="FCM")):
for j, fcm_node in enumerate(node[1].nodes(data="val")):
# store this value
self.node_val[i][j][0] = fcm_node[1][0]
self.node_val[i][j][1] = fcm_node[1][1]
self.node_future_val[i][j][0] = fcm_node[1][0]
self.node_future_val[i][j][1] = fcm_node[1][1]
Another place that needs to be modified is the runFCMCUDA_comm
function(or runFCMCUDA
if run without communities or runFCM
if run serially). The rules for adding and multiplying intervals can be found in the paper 1. Since the total input to a node will now have bounds, the following code:
# loop through the concept nodes in the FCM
for concept in range(FCM_adj.shape[0]):
weightSum = 0
# loop through the edge values
for edge in range(FCM_adj.shape[1]):
weightSum += FCM_adj[edge][concept] * node_val[agent][edge]
# Apply tanh if out of range and buffer the new value
num = node_val[agent][concept] + weightSum
if num > 1 or num < 0:
num = math.tanh(num)
node_future_val[agent][concept] = num
# check if all focus concepts are stable
all_stable = True
for i in range(len(focus)):
if abs(node_future_val[agent][focus[i]] - node_val[agent][focus[i]]) > threshold[i]:
all_stable = False
break
#break if all stable
if all_stable:
break
can be changed to:
# loop through the concept nodes in the FCM
for concept in range(FCM_adj.shape[0]):
# Replace weightSum with net_lower and net_upper since lists can not be create on GPU
net_lower = 0
net_upper = 0
# Save the current lower and upper bounds of the node to reference later
node_lower = node_val[agent][concept][0]
node_upper = node_val[agent][concept][1]
# loop through the edge values
for edge in range(FCM_adj.shape[1]):
# multiply inter node value intervals with the edge weight interval
# using the multiplication rule
lower = FCM_adj[edge][concept][0] * node_val[agent][edge][0]
upper = max(FCM_adj[edge][concept][0] * node_val[agent][edge][1],
FCM_adj[edge][concept][1] * node_val[agent][edge][0])
# add the current input to the node to the net input using the
# addition rule
net_lower = min(lower + net_upper,
upper + net_lower)
net_upper = upper + net_upper
# Calculate the new values using the addition rule and and apply sigmoid function
# to both bounds
node_lower = min(node_lower + net_upper,
node_upper + net_lower)
node_future_val[agent][concept][0] = 1 / (1 + math.exp(node_lower))
node_upper = node_upper + net_upper
node_future_val[agent][concept][0] = 1 / (1 + math.exp(node_lower))
# the threshold check was not mentioned in the paper so that part was removed, though the parameter threshold is still kept for minimal changes
The last function that needs attention is loadNewValuesCUDA_comm
. Since there are now two dimensions for each node value, the following line:
node_val[agent_idx][concept] = node_future_val[agent_idx][concept]
can be replaced with:
node_val[agent_idx][concept][0] = node_future_val[agent_idx][concept][0]
node_val[agent_idx][concept][1] = node_future_val[agent_idx][concept][1]
Running the simulation with communities#
The last thing that is needed for this to work is the interaction function. In order to simplify the interaction function and introduce running with communities to accommodate more agents, the interaction function will be run in serial while the FCMs will be run in parallel. There are only a few differences between this interaction function and the interaction function in the serial insurgency model because of broadcasting in numpy and will be noted with comments.
def econ_influence(val, influencing):
threshold = 10
impact = 5
avg = 0.0
for num in influencing:
avg += num
lowerThresh = 1 - threshold / 100.0
upperThresh = 1 + threshold / 100.0
result = val
# avg > val * upperThresh will result in a boolean array that compare every value along the axis
# so using np.all will check if all values in the avg array pass the threshold
if np.all(avg > val * upperThresh):
result += val * impact / 100.0
elif np.all(avg < val * lowerThresh):
result -= val * impact / 100.0
return result
def insurgency_influence(influencedVal, influencing):
# nothing needs to be changed in this function
rate = 0.1
result = influencedVal
for num in influencing:
result -= rate * num
return result
# nothing needs to be changed in this function
def insurgency_interact(hm):
if hm.ABM_adj.shape[0] <= 1:
return
# loop through each agent
for agent in range(hm.ABM_adj.shape[0]):
# grab the neighbors
friends = hm.get_neighbors(agent)
# get the numeric index for EconomicDevelopment and AbilityOfInsurgentsToControlThePopulation
econIdx = hm.fcm_labels["EconomicDevelopment"]
insurgeIdx = hm.fcm_labels["AbilityOfInsurgentsToControlThePopulation"]
econList = []
insurgeList = []
# get the list of values for all the neighbors for the two concepts
for friend in friends:
econList.append(hm.node_val[friend][econIdx])
insurgeList.append(hm.node_val[friend][insurgeIdx])
# agents now influence each other
hm.node_future_val[agent][econIdx] = econ_influence(
hm.node_val[agent][econIdx],
econList
)
hm.node_future_val[agent][econIdx] = insurgency_influence(
hm.node_future_val[agent][econIdx],
insurgeList
)
The simulation then can be run using the run_parallel
and setting with_community
to True
along with providing a community algorithm.
hm.run_parallel(["Rebelliousness"], [0.05], 10, insurgency_interact, [hm], 20,
True, nx.algorithms.community.greedy_modularity_communities)
The results are displayed below:
{'Rebelliousness': array([0.40102673, 0.5344537 ], dtype=float32)}
References