资讯详情

CSP——脉冲神经网络

题目背景

在这个问题中,你需要实现一个问题 SNN(spiking neural network,脉冲神经网络)模拟器。 SNN 由以下部分组成:

  1. 神经元:按一定公式更新内部状态,接受脉冲并释放脉冲
  2. 脉冲源:在特定时间释放脉冲
  3. 突触:连接神经元-神经元或脉冲源-神经元,传递脉冲

题目描述

神经元将按照一定的规则更新其内部状态。在这个问题中,我们离散时间,即设置时间间隔Δt,只考虑时间间隔整数倍的时间t=kΔt(k∈Z ),按以下公式,从k?1时刻值计算k时间变量的值:

vk=vk?1 Δt(0.04vk?12 5vk?1 140?uk?1) Ik

uk=uk?1 Δta(bvk?1?uk?1)

其中v和u神经元内部的变量会随着时间的推移而变化,a和b是常量,不会随着时间变化;其中Ik表示神经元在k如果不接受脉冲,则始终接受的所有脉冲输入的强度之和,那么Ik=0。如果满足上述计算vk≥30.神经元会发出脉冲,脉冲通过突触传播到其他神经元;同时,vk设为c并且uk设为uk d,其中c和d也是常量。图 1 展示一个神经元v曲线随时间变化。

图1: 神经元v曲线随时间变化

突触表示的是神经元-神经元、脉冲源-神经元的连接关系,包含一个入结点和一个出结点(可能出现自环和重边)。当突触的入结点(神经元或脉冲源)在k随时发出脉冲,然后延迟传播D(D>0)一刻之后,也就是k D始终突触的出结点(神经元)将接受强度w的脉冲。

脉冲源在每个时刻以一定的概率发放一个脉冲,为了模拟这个过程,每个脉冲源有一个参数0<r≤以下伪随机函数统一采用32、767:

C 版本:

static unsigned long next = 1;  /* RAND_MAX assumed to be 32767 */ int myrand(void) {     next = next * 1103515245   12345;     return((unsigned)(next/65536) % 32768); }

Python 版本:

next = 1 def myrand():     global next     next = (next * 1103515245   12345) % (2 ** 64)     return (next // 65536) % 32768

Java 版本:

long next = 1; int myrand() {     next = next * 1103515245   12345;     return (int)((Long.divideUnsigned(next, 65536)) % 32768); }

每次按编号顺序从小到大,每个脉冲源调用上述伪随机函数一次,当r>myrand()脉冲在当前时间释放,并通过突触传播到神经元。

模拟时,已知 0 每个神经元的状态从时间刻开始 1 按照上述规则计算时间刻,直到完成T计算时间,然后输出T总是神经元v值和发放的脉冲次数分别的最小值和最大值。

按以下顺序规定输入数据中的结点:[0,N?1]为神经元编号,[N,N P?1]为脉冲源编号。

请在代码中使用双精度浮点类型。

输入格式

读入数据从标准输入。

输入的第一行包括四个空间分隔的正整数NSPT,表示一共有N个神经元,S个突触和P脉冲源,输出时间T时神经元的v值。

输入的第二行是正实数Δt,表示时间间隔。

输入接下来的几行,每行都有空间分隔的正整数RN和六个实数vuabcd,每行按顺序对应RN具有相同初始状态和常量的神经元:vu表示神经元在时刻 0 时间的变量值;abcd神经元微分方程中的四个常量。保证所有的RN加起来等于N。保证所有的RN加起来等于N。它们从前到后按编号顺序描述神经元,每行对应连续编号的神经元信息。

下一步输入P行,每行是正整数r,每行按顺序对应一个脉冲源r参数。

下一步输入S每行有两个空格分隔的整数s(0≤s<N P)、t(0≤t<N)、一个实数w(w≥0)和正整数D,其中s和t分别是入结点和出结点的编号;w和D脉冲强度和传播延迟分别表示。

输出格式

输出到标准输出。

输出有两行,第一行由两行保留 3 位小数的实数组成是所有神经元在任何时候T时变量v最小值和最大值。第二行由两个整数组成,即所有神经元在整个模拟过程中释放脉冲次数的最小值和最大值。

只要按照题目要求正确实现,就不会因为计算精度问题而得到错误的答案。

样例1输入

1 1 1 10 0.1 1 -70.0 -14.0 0.02 0.2 -65.0 2.0 30000 1 0 30.0 2

样例1输出

-35.608 -35.608 2 2 

样例1解释

该样例有 1 个神经元、1 个突触和 1 脉冲源,时间间隔Δt=0.1.脉冲强度是唯一的脉冲源 30.传播延迟为0 2 突触传播到唯一的神经元。

这个例子一起进行 10 随机数生成器生成时间步模拟 10 次随机数如下:

16838 5758 10113 17515 31051 5627 23010 7419 16212 4086

因此,唯一的脉冲源始终存在 1-4 和 6-10 发出脉冲。 1 到 10 时间,唯一的神经元v值分别为:

-70.000 -70.000 -40.000 -8.200 -65.000 -35.404 -32.895 0.181 -65.000 -35.608

神经元在时刻 5 和时刻 9 发放,最后得到v=?35.608。

样例2输入

2 4 2 10 0.1 1 -70.0 -14.0 0.02 0.2 -65.0 2.0 1 -69.0 -13.0 0.04 0.1 -60.0 1.0 30000 20000 2 0 15.0 1 3 1 20.0 1 1 0 10.0 2 0 1 40.0 3

样例2输出

-60.000 -22.092 1 2 

子任务

子任务 T N S P D 分值
1 ≤102 ≤102 ≤102 ≤102 ≤102 30
2 ≤103 ≤103 ≤103 ≤103 ≤103 40
3 ≤105 ≤103 ≤103 ≤103 ≤10 30

看完题目,了解题目的意思,就知道是模拟题。根据题目的意思,一步一步模拟现实就好了,当然要注意一下代码的时间和空间复杂度。先放上AC代码稳定军心。

#include<iostream>
#include<vector>
using namespace std;
static unsigned long Next = 1;//题目的next要改一下,不然会报错 
/* RAND_MAX assumed to be 32767 */
int myrand(void) 
{
    Next = Next * 1103515245 + 12345;
    return((unsigned)(Next/65536) % 32768);
}
struct edg//突触结构体 
{
	int end;
	double w;
	int D;
};
double v[1001];//神经元的状态参数,v,u,a,b,c,d 
double u[1001];
double a[1001], b[1001], c[1001], d[1001]; 
int r[1001]={0};//脉冲源的r参数 
vector<edg>e[2001];//突触,下标为起始发送源,存储的是该发送源能到达的目标神经元 
double ik[1001][1001]={0};//行为时间点,列为目标神经元 
int sum[1001]={0};//神经元的发送次数 
int main()
{
	int n=0, s=0, p=0, T=0;
	scanf("%d%d%d%d", &n, &s, &p, &T);
	double dt=0.0;
	scanf("%lf", &dt);
	int sum_n=0, rn=0, g=0;
	while(sum_n<n)//输入神经元 
	{
		scanf("%d%lf%lf%lf%lf%lf%lf", &rn, &v[g], &u[g], &a[g], &b[g], &c[g], &d[g]);
		g++;
		for(int i=1; i<rn; i++)
		{
			v[g]=v[g-1];
			u[g]=u[g-1];
			a[g]=a[g-1];
			b[g]=b[g-1];
			c[g]=c[g-1];
			d[g]=d[g-1];
			g++;
		}
		sum_n=sum_n+rn;
	}
	for(int i=0; i<p; i++)//输入脉冲源 
	{
		scanf("%d", &r[i]);
	}
	for(int i=0; i<s; i++)//输入突触 
	{
		edg p;
		int start=0;
		scanf("%d%d%lf%d", &start, &p.end, &p.w, &p.D);
		e[start].push_back(p);
	}
	//开始模拟 
	for(int t=1; t<=T; t++)
	{
		int temp_t=t%1000;//T最大为1000,后面的时刻可以把前面用过的t给覆盖掉 
		for(int i=0; i<n; i++)//神经元遍历 
		{
			double v_pre=v[i];
			v[i]=v[i]+dt*(0.04*v[i]*v[i]+5*v[i]+140-u[i])+ik[temp_t][i];
			ik[temp_t][i]=0;//用完之后清零,防止%1000的时候再次读到前面的脉冲 
			u[i]=u[i]+dt*a[i]*(b[i]*v_pre-u[i]);
			if(v[i]>=30)
			{
				sum[i]++;
				v[i]=c[i];
				u[i]=u[i]+d[i];
				for(int j=0; j<e[i].size(); j++)
				{
					ik[(t+e[i][j].D)%1000][e[i][j].end]+=e[i][j].w;
				}	
			}
		}
		for(int i=0; i<p; i++)//脉冲源遍历 
		{
			if(r[i]>myrand())
			{
				for(int j=0; j<e[i+n].size(); j++)
				{
					//脉冲源的编号是在神经元之后,即(i+n)
					ik[(t+e[i+n][j].D)%1000][e[i+n][j].end]+=e[i+n][j].w;
				}		
			}
		}
	}
	double v_min=v[0], v_max=v[0];
	int sum_min=sum[0], sum_max=sum[0];
	for(int i=1; i<n; i++)
	{
		if(v_min>v[i])v_min=v[i];
		if(v_max<v[i])v_max=v[i];
		if(sum_min>sum[i])sum_min=sum[i];
		if(sum_max<sum[i])sum_max=sum[i];
	}
	printf("%.3lf %.3lf\n%d %d", v_min, v_max, sum_min, sum_max);
	return 0;
}

1、为什么v,u,a,b,c,d不直接弄个神经元结构体,而是分别用数组存储?在空间上,结构体存储和各个参数分开存储没有区别,但是在获取数据的方式上略微不同,将导致耗费的时间不同。例如,要获取神经元结构体中的参数v,计算机要先在神经元结构体数组中找到该神经元,然后再在该神经元结构体这块内存中再次寻找参数v(类比间接寻址),而用数组存储不同,计算机直接在数组v中顺序查找,直接获取对应参数v(类比直接寻址)。因为本题卡时长,所以每一步优化都很关键。

2、设立vector<edg> e[2001],类似于邻接表,数组e的下标是发送脉冲的脉冲源或者神经元,每个数组元素都是一个vector,表示该脉冲源或者神经元将影响到哪些神经元以及脉冲w为多大。因为脉冲源和神经元都是是1000,所以数组e要设2001。

3、ik二维数组为什么是行为时间点,列为目标神经元,而不是行为目标神经元,列为时间点?在模拟的时候,最外围for循环是时间,然后依次遍历所有的神经元在该时刻所受的脉冲之和,即ik。我们知道二维数组的写入和读取都是一行一行的执行,先执行完一行再执行下一行,当ik中行为目标神经元,列为时间点,在每一时刻,每次访问神经元对应ik都要一行一行的执行,就会造成时间上的浪费。而行为时间点,列为目标神经元时,当访问该时刻时,一行都是接下来要访问的神经元,所以缩短了访问时间。

4、为什么时间要取模1000?因为直接数组e二维数组开10^5*10^3,

空间复杂度度:(4+8+4)*10^5*10^3=1600MB,超内存了,所以不能开10^5。取模1000,因为D最大是1000,当前面时刻的ik用完之后,可以清零直接覆盖掉,不会产生影响。

其它需要注意的点,注释上也有说明,如果还有不清楚的可以留言哦!

 

标签: edg连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台