C++个人模拟神经细胞相互作用

为美好的世界献上开源。

2023年九月底到国庆假期写的,程序比较简陋和概括,而且突触是双向传递的,只包括神经元的突触联系以及绝对不应期,但是有2000个神经元,适合作为多对象交互的实战案例,也适合Easy X入门,大致还是可以拿出来看一下的,而且效果比较漂亮。

程序其实是我在知乎上发的这篇文章用的,

https://zhuanlan.zhihu.com/p/659709309

完整代码在最后。先看下效果,看看是不是自己想要的。

绝对不应期信号抵消效应-2000神经元-5绝对不应期2.1代

这个程序的重点是在计算机算力有限的情况下,保证程序正常且快速地运行,重点在于算法优化。

首先,神经元,或者说以后遇到的其它程序的对象,都一定是只需要和程序内有限的其它对象交互,看似是2000个神经元,但每个神经元的“感受野”都只有周边的一些神经元,所以如果能把每个神经元的运算压缩到与几个神经元交互的话,程序的计算量就会非常小。

一个神经元到底和哪些神经元交互,当然是看距离,在一定范围内的神经元才能交互,所以神经元需要判断2000个神经元哪些在自己范围之内,但是如果每次迭代都要判断一遍,那么计算量会是惊人的2000^2次,且这是一个循环中的过程,要是每个循环都要来这么2000^2下,这就不太好了吧,反正我没试过,我写其它程序的时候遇到过这种情况,情况不太好。

但是解决方法也非常简单,既然神经元位置不变,那么每次判断的结果也是一样的,既然如此,为什么还要每次循环的时候重新判断一遍呢,所以直接给每个神经元装上一个用于记忆的数组,记住哪些神经元要和自己交互,轮到自己的时候去查看一下就好了。

神经元对象的代码:

struct Neure
{
    short synnum[20] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };//突触编号
    short synnumber = 0;//突触个数
    double a = 5;       //兴奋参数
    short slp_time = 5;    //绝对不应期时间参数
    short x;            //坐标
    short y;
    double a_next = 0;    //兴奋参数缓存
};

//神经元数组
Neure n[2000];

上面的最后一行代码,神经元实例化是用一个结构形式的数组来实现的,和int a[10];这种是完全一样的道理,这样每个神经元就都有了编号,上面神经元结构的第一行,神经元突触编号“short synnum[20]”就是用来存储与自己交互的其它神经元的编号,突触个数short synnumber是和上面的突触编号配合使用的,到时候遍历次数就是这个数,后面除了坐标以外的其它参数都和绝对不应期有关,而这就是另一个重点了。

不应期,用人话说就是神经元被激发后,在短时间内不能再次被激发,跟其它不应期也是一样的,本来很简单,但是现在有个问题,在之前我写的《用C++、Easy X模拟三体运动教程》中也出现过这种问题,当时没提那个关键词,缓存,因为那个程序里也没有标准的缓存,现在专门说一说。

在神经元这里,不应期的现象在上面那个代码块里面是用兴奋参数double a来实现的,a随每次循环迭代递增,比如,当a大于等于10的时候,就处于可激发状态,当a被激发,a就回到0这个值,然后跟着循环次数继续递增,就会造成10帧的不应期。

但是如果只有一个兴奋参数,那么问题来了,在这一帧大循环内,有一个神经元的编号是100,兴奋性为9,前0到99在计算的时候,该神经元都处于不应期内,它自己是不能激发别人的,别人也不能激发它,但是现在轮到它计算了,因为按照算法,兴奋性要跟着大循环递增,它先算了下自己的兴奋性,竟然变成10了,处于兴奋状态,它又可以被别人激发和把别人激发了,所以对于101到1999编号的神经元,它又是在不应期以外的正常神经元。

这是怎么回事呢,非常简单,提前改变了自己的兴奋性,它一个人的兴奋性a的值必须要对于所有人都一样,且按照神经元的运行规律,神经元的兴奋性其实是取决于上一帧的,即此时此帧的兴奋性取决于上一帧是否被激发或是否a=9,所以要把上一帧的数据存储起来,拿来计算这一帧的兴奋性,然后在所有的神经元相互作用之前,就把神经元的兴奋性a值替换成新的。

吗?

当然不是的,我是说不必存储上一帧的所有信息,那个在所有交互之前刷新数据是必要的,既然反正都是要用上一帧算出来个参数,那为什么要保留整个上一帧,为什么不在上一帧的大循环里面顺便就算好各自的兴奋性,然后开个内存先存起来,下一帧再用不就行了吗?

这就是缓存存在的意义之一。当然缓存还有其它意义,比如说GPU刷新屏幕,是先计算好屏幕下一帧的画面,边计算边把像素数据存入缓存,然后把缓存里面的画面直接覆盖显示屏直接交互的存储。

具体到代码的编写,原则就是,等号右边只能使用实时参数,等号左边只能计算缓存参数,判断只能调用实时参数。以下是使用缓存的神经元交互函数,

//迭代计算函数
void compute()
{
    for (short i = 0; i < Total; i++)     //i为主角
    {
        n[i].a = n[i].a_next;   //更新兴奋参数
        //n[i].a_next = 0;
    }
    for (short i = 0; i < Total; i++) //i为主角。
                                //等号右边只能使用实时参数
                                //等号左边只能计算缓存参数
                                //判断只能调用实时参数
    {
        for (short q = 0; q < n[i].synnumber; q++)  //计算第q个突触
        {
            if (n[i].slp_time == 5) //绝对不应期=5
            {
                if (n[n[i].synnum[q]].a == 10)
                {
                    n[i].a_next = 10;
                }
            }
        }
            if (n[i].slp_time < 5)  //绝对不应期=5
        {
            n[i].slp_time++;
        }
        if (n[i].a == 10) //绝对不应期,阈值为10
        {
            n[i].a_next = 5;
            n[i].slp_time = 0;
        }
    }
}

到时候在主函数main()里面直接调用就行了,直接写上“compute();”就行了。这就是核心函数,其它配套函数难度不大,不过于解释。

当然,还是要强调一下写大程序长代码的习惯,一个是写好注释,另一个是尽量把主函数里面的细节,以函数的形式给搬到其它地方去,请欣赏主函数,

int main()
{
    Position_Initialization();         //初始化位置
    Synapse_Initialization();         //初始化突触参数
    cout << "输入刷新时间间隔:";
    cin >> dt;
    initgraph(wx, wy);        //窗口调整
    while (true)                //开始实时操作
    {
        n[0].a_next = 10; //一直保持这个神经元兴奋(具体实验目标要改变这一行)
        drawmap();                  //绘图
        compute();                   //迭代
        Sleep(dt);                     //时间间隔停顿
        _getch();            //等待键盘按键按下,用于逐帧查看
    }
}

以上是全部的主函数代码,非常简洁,这次我都不用画流程图来说明,这个程序开发的时间跨度有将近两周半,注释和视图简洁非常关键,而且这样模块化的主函数非常容易改写和找bug,相对比较容易找出到底是哪个子函数出了问题,就像查“ikun”什么意思,字典肯定是先翻到i这里,而不是从头开始一个一个检索。

以下是完整代码。

#include <iostream>
#include<graphics.h>
#include<windows.h>
#include <ctime>
#include <cstdlib>
#include <conio.h>
using namespace std;

short wx = 1000;    //窗口参数
short wy = 600;
short D = 30;       //作用半径
short T = 0;        //帧数
short Total = 2000;
short dt = 1;   //时间间隔

//神经元结构
struct Neure
{
    short synnum[20] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };//突触编号
    //double syn[2][20] = { { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 } ,//突触强度
    //                    { 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 } };//突触强度缓存
    short synnumber = 0;//突触个数
    double a = 5;       //兴奋参数
    short slp_time = 5;    //绝对不应期时间参数
    short x;            //坐标
    short y;
    double a_next = 0;    //兴奋参数缓存
};

//神经元数组
Neure n[2000];

//坐标初始化函数
void Position_Initialization()
{
    srand((unsigned)time(NULL));
    for (short i = 0; i < Total; i++)    //遍历所有神经元
    {
        n[i].x = rand() % 100 / 100.0 * wx;
        n[i].y = rand() % 100 / 100.0 * wy;
    }
}

//突触初始化函数
void Synapse_Initialization()
{
    short q = 0;        //突触编号顺序
    short J = 0;        //加法判断积累参数
    for (short i = 0; i < Total; i++)     //i为主角
    {
        q = 0;
        J = 0;
        n[i].synnumber = 0; //初始化突触个数
        for (short b = 0; b < 20; b++)  //初始化突触编号
        {
            n[i].synnum[b] = 0;
        }

        for (short j = 0; j < Total; j++) 
            //初始化突触个数和记录对象,j为配角
        {
            if (i != j)         //排除主角自己
            {
                if (n[j].x < n[i].x + D)    //加法判断第一步
                {
                    J++;
                }
                if (n[j].x > n[i].x - D)
                {
                    J++;
                }
                if (n[j].y < n[i].y + D)
                {
                    J++;
                }
                if (n[j].y > n[i].y - D)
                {
                    J++;
                }

                if (J == 4)        //加法判断第二步
                {
                    n[i].synnum[q] = j; //记录相互作用神经元编号
                    q++;
                }
                J = 0;
            }
        }
        n[i].synnumber = q; //录入突触个数
    }
}

//依次画出函数
/*
void draw()
{
    for (short i = 0; i < Total; i++)
    {
        circle(n[i].x, n[i].y, n[i].a + 5);
        TCHAR s[6];
        _stprintf_s(s, _T("%d"), i);                //打印编号
        outtextxy(n[i].x, n[i].y, s);
        _stprintf_s(s, _T("%d"), (short)n[i].a);    //打印兴奋参数
        outtextxy(n[i].x + 20, n[i].y, s);
        _stprintf_s(s, _T("%d"), (short)n[i].synnumber);    //打印突触参数
        outtextxy(n[i].x, n[i].y + 15, s);
        _stprintf_s(s, _T("%d"), T);                //打印帧数
        outtextxy(0, 0, s);
    }
}
*/

//迭代计算函数
void compute()
{
    for (short i = 0; i < Total; i++)     //i为主角
    {
        n[i].a = n[i].a_next;   //更新兴奋参数
        //n[i].a_next = 0;
        /*for (short q = 0; q < n[i].synnumber; q++)  //更新突触参数
        {
            n[i].syn[0][q] = n[i].syn[1][q];
        }*/
    }

    for (short i = 0; i < Total; i++) //i为主角。
                                //等号右边只能使用实时参数
                                //等号左边只能计算缓存参数
                                //判断只能调用实时参数
    {
        for (short q = 0; q < n[i].synnumber; q++)  //计算第q个突触
        {
            /*n[i].syn[1][q] = n[i].syn[0][q] + n[n[i].synnum[q]].a * 5;
            //强化参数0.5,遗忘参数0.1
            n[i].a_next = n[i].a + n[n[i].synnum[q]].a * n[i].syn[0][q] * 0.5;
            //平均参数0.5*/
            if (n[i].slp_time == 5) //绝对不应期=5
            {
                if (n[n[i].synnum[q]].a == 10)
                {
                    n[i].a_next = 10;
                }
            }
        }
if (n[i].slp_time < 5)  //绝对不应期=5
        {
            n[i].slp_time++;
        }
        if (n[i].a == 10) //绝对不应期,阈值为10
        {
            n[i].a_next = 5;
            n[i].slp_time = 0;
        }
        
    }
}

//画出神经链路
void drawmap()
{
    for (short i = 0; i < Total; i++)     //i为主角神经元
    {
        for (short q = 0; q < n[i].synnumber; q++)
        {
            line(n[i].x, n[i].y, n[n[i].synnum[q]].x, n[n[i].synnum[q]].y);
        }
        setfillcolor(COLORREF RGB(255-n[i].slp_time * 51, 255-n[i].slp_time * 51, 255-n[i].slp_time * 51));
        fillcircle(n[i].x, n[i].y, 5);
        /*
        TCHAR s[6];
        _stprintf_s(s, _T("%d"), i);                //打印编号
        outtextxy(n[i].x, n[i].y, s);
        _stprintf_s(s, _T("%d"), (short)n[i].a);    //打印兴奋参数
        outtextxy(n[i].x + 20, n[i].y, s);
        _stprintf_s(s, _T("%d"), (short)n[i].synnumber);    //打印突触个数
        outtextxy(n[i].x, n[i].y + 15, s);
        _stprintf_s(s, _T("%d"), (short)n[i].slp_time);    //打印绝对不应期参数
        outtextxy(n[i].x + 20, n[i].y + 15, s);
        _stprintf_s(s, _T("%d"), T);                //打印帧数
        outtextxy(0, 0, s);
        */
    }
}

int main()
{
    Position_Initialization();         //初始化
    Synapse_Initialization();
    cout << "输入刷新时间间隔:";
    cin >> dt;
    initgraph(wx, wy);        //窗口调整
    drawmap(); 
    _getch();   //停顿,检查神经链路
    /*
    n[0].a_next = 10;
    n[1].a_next = 10;
    n[2].a_next = 10;
    n[3].a_next = 10;
    n[4].a_next = 10;
    */
    
    while (true)                //开始实时操作
    {
        
        n[0].a_next = 10;
        //n[1].a_next = 10;
        //n[2].a_next = 10;
        //n[3].a_next = 10;
        //n[4].a_next = 10;
        
        //cleardevice();
        drawmap();
        compute();
        Sleep(dt);
        _getch();            //用于逐帧查看
        T++;
    }
    _getch();
    drawmap();
    _getch();
    return 0;
}

以上是完整代码,包括调试用的代码。(doge)

以下是感慨。

虽然我不是计算机相关专业的,但是我还是一直对程序抱有一些我也说不出来的东西,像是多了个分身。

程序绝对不是那么简单能写完的,算法也是自己不断琢磨和更迭出来的,上面的算法,已经是第三代的算法了,每一代算法的基本逻辑完全不一样,最开始是没有缓存的,是把神经元按照元素周期表那样摆好,这样就知道神经元只能和编号为n+1、n-1、n+8、n-8的元素,欧布,神经元相互作用,但这太计算机了,神经元是不会自己把自己摆好的,而且每个神经元还不知道有多少突触呢,然后不断否定底层的逻辑,这个否定也不是很容易,但是发现新算法还是非常好的,接受起来虽说并不那么困难,但是……

但是中国近代的新青年们都是从哪儿来的,五四运动的那些学生从哪里来的,这些新式学堂从哪儿来的,还不是清政府那些“老头”主持的洋务运动来的,洋务运动虽然失败了,但是没有洋务运动,就没有整个新民主主义革命啊,没有前三代失败的算法,是不可能有第三代的算法的。

最后就到了第三代的算法,前前后后折腾了两周半,2023年的国庆终于干完了,虽然直到现在我们大学还没开任何有关程序的课。以前觉得代码写多少多少行,多牛牪犇逼啊,现在真正写下来才知道,原来代码越少越好,算法越简单越好,能用十行代码写完程序,比多写一百行代码更厉害,特别是这种搞算法的,三行代码要是能完成相同的功能,那肯定三行代码更厉害,而且真正的代码量在最终的程序里面是体现不出来的,这个程序,算上废弃的前两代的代码,最后程序运行的代码占真正写过的代码的比例,大约不到三分之一,我之前那个三体运动的程序,比例更低,精简的代码背后是更多的代码,在无尽的注释和看不见的岁月中履行了自己的职责,甚至有很多贡献经验的代码是在之前做过的其它程序里面的,代码越少,代码越多。

还有一点啊,那就是自己突然想到一个想法,或者说遇到自己想做的这种杂七杂八的事情,虽然麻烦自己,但是是实打实地锻炼自己,这是财富,别人求都求不来的宝藏,这个东西可以带你学到一系列精彩的东西的,这在大学里面,对于提升自己创新能力和专业能力是跨代的提升,虽然不是很确定,但是这些东西以后应该会成为自己的护城河,找工作和保研什么的,都有帮助。

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
假设两个带点粒子的电量分别为 $q_1$ 和 $q_2$,它们的位置分别为 $(x_1, y_1)$ 和 $(x_2, y_2)$。这里我们使用二维平面直角坐标系来描述它们的位置。 我们可以利用库仑定律来计算它们之间的相互作用力,即: $$F = \frac{1}{4\pi\epsilon_0}\frac{q_1q_2}{r^2}$$ 其中 $F$ 是两个粒子之间的相互作用力,$\epsilon_0$ 是真空中电常数,$r$ 是两个粒子之间的距离。 为了计算 $r$,我们可以使用勾股定理,即: $$r = \sqrt{(x_2-x_1)^2+(y_2-y_1)^2}$$ 将上面的公式代入库仑定律中,我们就可以得到两个带点粒子的相互作用力了。具体实现可以参考以下的 C++ 代码: ```cpp #include <iostream> #include <cmath> using namespace std; const double k = 9e9; // 库仑定律中的比例系数 const double eps0 = 8.85e-12; // 真空中电常数 double calc_force(double q1, double q2, double x1, double y1, double x2, double y2) { double r = sqrt(pow(x2-x1, 2) + pow(y2-y1, 2)); // 计算两个粒子之间的距离 double f = k * q1 * q2 / (4 * M_PI * eps0 * pow(r, 2)); // 应用库仑定律计算相互作用力 return f; } int main() { double q1, q2, x1, y1, x2, y2; cout << "请输入第一个粒子的电量和位置(以空格分隔):" << endl; cin >> q1 >> x1 >> y1; cout << "请输入第二个粒子的电量和位置(以空格分隔):" << endl; cin >> q2 >> x2 >> y2; double f = calc_force(q1, q2, x1, y1, x2, y2); cout << "两个粒子之间的相互作用力为:" << f << endl; return 0; } ``` 注意,上面代码中的 `M_PI` 是 C++ 标准库中定义的圆周率常量,需要包含头文件 `#include <cmath>`。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值