I am attempting to make an AI similar to NEAT using C++. However, I have run into a problem with getting the population to work correctly. When I use an array of custom objects, some of the data stored in each of the objects' properties is being corrupted. The data inside each of the connections in connections is set to 0xDDDDDDDD
after the net is assigned. When I test the given properties inside the constructor it seems fine but when it is assigned within the population array (in this case t
) all of the data is set to 0xDDDDDDDD
similarly to if it were deleted.
// neat network.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>
#include <cmath>
class node
{
public:
int id;
double value;
node()
{
id = 0;
value = 0;
}
node(int n)
{
id = n;
value = 0;
}
void apply(double v)
{
value = v;
}
node copy()
{
return node(id);
}
};
class connection
{
public:
double weight;
int in;
int out;
connection(int i, int o, double w=0)
{
in = i;
out = o;
weight = w;
int r = rand();
weight = ((double)(r % 2000) - 1000.0) / 1000.0;
srand(r);
}
connection()
{
in = 0;
out = 0;
weight = 0;
}
void mutate()
{
weight += (double)((rand() % 50 - 2) / 1000);
}
connection copy()
{
connection ret = connection(in, out);
ret.weight = weight;
return ret;
}
};
double sigmoid(double n)
{
return 1 / (exp(-n) + 1);
}
class net
{
public:
double* position;
int quant;
node* nodes;
int* inpernode;
connection* connections;
int connlen;
int inn;
int ut;
net(int ins, int outs, int cn=0)
{
inn = ins;
ut = outs;
quant = ins+outs;
nodes = new node[quant];
position = new double[quant];
connections = new connection[ins*outs];
inpernode = new int[quant];
for (int i = 0; i < quant; i++)
{
nodes[i] = node(i);
if (i < ins)
{
position[i] = 0;
}
else
{
position[i] = 1;
}
inpernode[i] = 0;
}
int c = 0;
connlen = ins * outs;
for (int i = 0; i < ins; i++)
{
for (int j = ins; j < quant; j++)
{
connections[c] = connection(i, j);
//std::cout << connections[c].weight << " ";
inpernode[j]++;
c++;
}
}
//std::cout << connections[0].in << std::endl;
}
net()
{
connections = new connection();
connlen = 0;
inn = 0;
inpernode = new int();
quant = 0;
nodes = new node();
position = new double();
ut = 0;
}
~net()
{
delete[] position;
delete[] nodes;
delete[] connections;
delete[] inpernode;
}
double* apply(double* inputs)
{
bool* finished = new bool[quant];
//std::cout << connections[0].in;
bool* cfinished = new bool[connlen];
int* ndone = new int[quant];
bool alldone=false;
for (int i = 0; i < connlen; i++)
{
cfinished[i] = false;
}
for (int i = 0; i < quant; i++)
{
finished[i] = i<inn;
ndone[i] = 0;
nodes[i].value = 0;
if (i < inn) {
nodes[i].value = inputs[i];
}
}
while (!alldone)
{
alldone = true;
for (int i = 0; i < connlen; i++)
{
//std::cout << quant;
if (finished[connections[i].in] && !cfinished[i])
{
alldone = false;
nodes[connections[i].out].value += connections[i].weight * sigmoid(nodes[connections[i].in].value);
cfinished[i] = true;
ndone[connections[i].out]++;
if (ndone[connections[i].out] == inpernode[connections[i].out])
{
finished[connections[i].out] = true;
}
}
}
}
double* outs = new double[ut];
for (int i = inn; i < inn + ut; i++)
{
outs[i - inn] = sigmoid(nodes[i].value);
}
return outs;
}
net copy()
{
net ret = net(inn, ut);
ret.quant = quant;
ret.connlen = connlen;
for (int i = 0; i < quant; i++)
{
ret.position[i] = position[i];
ret.nodes[i] = nodes[i].copy();
ret.inpernode[i] = inpernode[i];
}
for (int i = 0; i < connlen; i++)
{
ret.connections[i] = connections[i].copy();
}
return ret;
}
net mutate()
{
net ret = copy();
for (int i = 0; i < quant; i++)
{
if (rand() % 20 == 19)
{
connections[i].mutate();
}
if (rand() % 333 == 332)
{
nodes[quant] = node(quant);
int temp = connections[i].out;
connections[i].out = quant;
connections[connlen] = connection(quant, temp);
quant++;
connlen++;
}
}
if (rand() % 33 == 32)
{
bool done = false;
int tries = 200;
while (!done)
{
tries--;
if (tries < 0)
done = true;
int inc = rand() % quant;
if (position[inc] == 1)
continue;
int utc = rand() % quant;
if (position[inc] > position[utc])
continue;
bool found = false;
for (int i = 0; i < connlen; i++)
{
if (connections[i].in == inc && connections[i].out == utc)
found = true;
}
if (!found)
{
connections[connlen] = connection(inc, utc);
connlen++;
done = true;
}
}
}
return ret;
}
};
int main()
{
srand(2002);
/*net test = net(2, 1);
net test2 = net(2, 1);
std::cout << test.connections[0].weight << " " << test.connections[1].weight << std::endl;
std::cout << test2.connections[0].weight << " " << test2.connections[1].weight << std::endl;
double ins[] = { 0,0 };
double* cp = test.apply(ins);
std::cout << cp[0]<<" ";
cp = test2.apply(ins);
std::cout << cp[0];*/
net t[1];
t[0] = net(2, 1);
std::cout << t[0].inn << " " << t[0].ut << std::endl;
std::cout << t[0].connections[0].in << " " << t[0].connections[0].weight << std::endl;
/*net pop[100];
for (int i = 0; i < 100; i++)
{
pop[i] = net(2, 1);
std::cout << pop[i].connections[0].in << " " << pop[i].connections[1].in << " " << std::endl;
}
int* scores = new int[100];
for (int gen = 0; gen < 100; gen++)
{
for (int i = 0; i < 100; i++)
{
scores[i] = 0;
double inp[] = { 0, 0 };
double* t = pop[i].apply(inp);
if (t[0] < .1)
scores[i]++;
inp[1] = 1;
t = pop[i].apply(inp);
if (t[0] > .9)
scores[i]++;
inp[0] = 1;
inp[1] = 0;
t = pop[i].apply(inp);
if (t[0] > .9)
scores[i]++;
inp[1] = 1;
t = pop[i].apply(inp);
if (t[0] < .1)
scores[i]++;
if (scores[i] == 4)
{
std::cout << "Solved" << gen;
return 0;
}
scores[i] -= pop[i].connlen + pop[i].quant;
}
int sum = 0;
for (int i = 0; i < 100; i++)
sum += scores[i];
double avg = (double)(sum / 100);
net newpop[100];
int nplen = 0;
for (int i = 0; i < 100; i++)
{
if (scores[i] > avg)
{
newpop[nplen++] = pop[i];
}
}
for (int i = 0; i < nplen; i++)
{
pop[i] = newpop[i];
}
for (int i = nplen; i < 100; i++)
{
pop[i] = pop[i % nplen].mutate();
}
}*/
return 0;
}
// Run program: Ctrl + F5 or Debug > Start Without Debugging menu
// Debug program: F5 or Debug > Start Debugging menu
// Tips for Getting Started:
// 1. Use the Solution Explorer window to add/manage files
// 2. Use the Team Explorer window to connect to source control
// 3. Use the Output window to see build output and other messages
// 4. Use the Error List window to view errors
// 5. Go to Project > Add New Item to create new code files, or Project > Add Existing Item to add existing code files to the project
// 6. In the future, to open this project again, go to File > Open > Project and select the .sln file
srand
connection(int i, int o, double w=0)
{
in = i;
out = o;
weight = w;
int r = rand();
weight = ((double)(r % 2000) - 1000.0) / 1000.0;
srand(r);
}
The user specified weight is always overwritten with a random value.
You should probably write two different constructors:
connection(int i, int o)
: weight((static_cast<double>(rand() % 2000) - 1000.0) / 1000.0),
in(i), out(o)
{
}
connection(int i, int o, double w) : weight(w), in(i), out(o) {}
Consider that multiple calls to srand
must be avoided.
Also srand
doesn't work the way you seem to think. The initial call to rand
returns a well defined value that is then used as seed: this seed is not random at all so there isn't any randomness in chain of calls to srand
/ rand
.
Moreover intermixed calls to srand
could affect quality of rand
(which already in itself isn't great).
mutate
always returns 0
void mutate()
{
weight += (double)((rand() % 50 - 2) / 1000);
}
it should probably be:
void mutate()
{
weight += (double)((rand() % 50 - 2)) / 1000;
}
~net()
{
delete[] position;
delete[] nodes;
delete[] connections;
delete[] inpernode;
}
This assumes that position
, nodes
, connections
, ipernode
are array of objects but they could be a single object, e.g.:
net()
{
connections = new connection();
inpernode = new int();
nodes = new node();
position = new double();
// ...
}
(see delete vs delete[] operators in C++).
net t[1];
constructs a single net
object calling net()
.
t[0] = net(2, 1);
has (at least) four effects:
net
object (net(2, 1)
);t[0]
object;net(2, 1)
inside t[0]
;net(2, 1)
.Point 2 is problematic (see above: Deallocation).
After point 3 you have:
t[0].connections ----points to----------> [connection1, connection2]
^
|
net(2, 1).connections ----points to------------------+
After point 4:
t[0].connections ----points to----------> [freed memory area]
`net(2, 1)` destroyed
and the same holds for position
and inpernode
.
Now
std::cout << t[0].connections[0].in;
accesses a freed memory area (the 0xDDDDDDDD
value...).
Deletion of t[0]
(at the end of main
) involves the deallocation of an already freed memory block (with an error like double free detected in tcache 2).
At least add a copy assignment operator and a copy constructor (rule of three) to the net
class. Also change net()
to something like:
net()
{
connections = nullptr;
connlen = 0;
inn = 0;
inpernode = nullptr;
quant = 0;
nodes = nullptr;
position = nullptr;
ut = 0;
}
All these headaches occur because of manual memory management. You could easily rewrite the code using std::vector
s and avoid writing any custom copy/move constructors, assignment operators or destructors (rule of zero)... and this is the way to go.
PS I've not checked the net::apply
, net::mutate
, net::copy
member functions.