CSP202109-3 脉冲神经网络 题解
既然上次更新了那篇大模拟,再加上又有人催更了(huaji),就来一篇更加猛烈的模拟:脉冲神经网络!
一、题目
话不多说,继续凑字数!(((
题目背景
在本题中,你需要实现一个 SNN(spiking neural network,脉冲神经网络)的模拟器。一个 SNN 由以下几部分组成:
- 神经元:按照一定的公式更新内部状态,接受脉冲并可以发放脉冲
- 脉冲源:在特定的时间发放脉冲
- 突触:连接神经元-神经元或者脉冲源-神经元,负责传递脉冲
题目描述
神经元会按照一定的规则更新自己的内部状态。本题中,我们对时间进行离散化处理,即设置一个时间间隔\(\Delta t\) ,仅考虑时间间隔整数倍的时刻 \(t=k\Delta t(k\in Z^+)\),按照下面的公式,从 \(k-1\) 时刻的取值计算 \(k\) 时刻的变量的取值:
其中 \(v\) 和 \(u\) 是神经元内部的变量,会随着时间而变化,\(a\) 和 \(b\) 是常量,不会随着时间变化;其中 \(I_k\) 表示该神经元在 \(k\) 时刻接受到的所有脉冲输入的强度之和,如果没有接受到脉冲,那么 \(I_k=0\)。当进行上面的计算后,如果满足 \(v_k\ge30\),神经元会发放一个脉冲,脉冲经过突触传播到其他神经元;同时,\(v_k\) 设为 \(c\) 并且 \(u_k\) 设为 \(u_k+d\),其中 \(c\) 和 \(d\) 也是常量。图 1 展示了一个神经元 变量随时间变化的曲线。
图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>\text{myrand()}\) 时,在当前时间刻发放一次脉冲,并通过突触传播到神经元。
进行仿真的时候,已知 0 时间刻各个神经元的状态,从 1 时间刻开始按照上述规则进行计算,直到完成 \(T\) 时刻的计算,再输出 \(T\) 时刻神经元的 \(v\) 值和发放的脉冲次数分别的最小值和最大值。
规定输入数据中结点按如下方式顺序编号:\([0,N-1]\) 为神经元的编号,\([N,N+P-1]\) 为脉冲源的编号。
代码中请使用双精度浮点类型。
输入格式
从标准输入读入数据。
输入的第一行包括四个以空格分隔的正整数\(N\) \(S\) \(P\) \(T\) ,表示一共有 \(N\) 个神经元,\(S\) 个突触和 \(P\) 个脉冲源,输出时间刻 \(T\) 时神经元的 \(v\) 值。
输入的第二行是一个正实数 \(\Delta t\) ,表示时间间隔。
输入接下来的若干行,每行有以空格分隔的一个正整数 \(R_N\) 和六个实数 \(v\) \(u\) \(a\) \(b\) \(c\) \(d\),按顺序每一行对应 \(R_N\) 个具有相同初始状态和常量的神经元:其中 \(v\) \(u\) 表示神经元在时刻 0 时的变量取值;\(a\) \(b\) \(c\) \(d\) 为该神经元微分方程里的四个常量。保证所有的 \(R_N\) 加起来等于 \(N\)。它们从前向后按编号顺序描述神经元,每行对应一段连续编号的神经元的信息。
输入接下来的 \(P\) 行,每行是一个正整数 \(r\),按顺序每一行对应一个脉冲源的 \(r\) 参数。
输入接下来的 \(S\) 行,每行有以空格分隔的两个整数 \(s(0\leq s<N+P)\)、\(t(0\leq 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 个脉冲源,时间间隔 \(\Delta t=0.1\)。唯一的脉冲源通过脉冲强度为 30.0、传播延迟为 2 的突触传播到唯一的神经元。
该样例一共进行 10 个时间步的模拟,随机数生成器生成 10 次随机数如下:
16838
5758
10113
17515
31051
5627
23010
7419
16212
4086
因此唯一的脉冲源在时刻 1-4 和 6-10 发放脉冲。在时间刻从 1 到 10 时,唯一的神经元的 取值分别为:
-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 | \(\leq10^2\) | \(\leq10^2\) | \(\leq10^2\) | \(\leq10^2\) | \(\leq10^2\) | 30 |
2 | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | 40 |
3 | \(\leq10^5\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10\) | 30 |
二、思路
这种恐怖的模拟题,还要处理一个重要至极的事情:图论!
其实,我们只要处理一下脉冲就好了。
订一个vector[i][j],表示在i的时刻所有的脉冲。
别的懒得说了。
三、代码
官方66,运行超时了。(真不要脸,没对还来写题解)
#include<iostream>//头文件emm
#include<vector>
#include<cstring>
using namespace std;
struct edge{//一条边
int d;//D
double w;//w
int to;//连接的那一个点
};
vector<edge> G[2005];//存图
vector<edge> js[100005];//上面写的vector[i][j]
double I[2005];//公式中的I
int fa[2005];//发送多少次脉冲
struct sjy{//神经元
double a,b,c,d;//如果是脉冲源,用a表示r
double v,u;//变量
}th[2005];//th=things,各种东西
int n,s,p,t,rn,acnt=0;//nspt,rn表示R_N,acnt表示R_N之和
double jg;//delta_t
static unsigned long nextf = 1;//因为这个玩意似乎会编译错误,所以改成nextf了
/* RAND_MAX assumed to be 32767 */
int myrand(void) {
nextf = nextf * 1103515245 + 12345;
return((unsigned)(nextf/65536) % 32768);
}
inline void input(){//输入
ios::sync_with_stdio(0);//加速
cin>>n>>s>>p>>t;
cin>>jg;
double v,u,a,b,c,d;
while(acnt<n){//一个小技巧
cin>>rn>>v>>u>>a>>b>>c>>d;
for(int i=1;i<=rn;i++){//每次加上
th[acnt+i].a=a,th[acnt+i].b=b,th[acnt+i].c=c,th[acnt+i].d=d,th[acnt+i].v=v,th[acnt+i].u=u;
}
acnt+=rn;
}
for(int i=1;i<=p;i++) cin>>th[++acnt].a;//r
int from,to,ys;//s,t,D
double w;
for(int i=1;i<=s;i++){
cin>>from>>to>>w>>ys;
G[from+1].push_back({ys,w,to+1});//注意,有向图!
}
}
inline void fasong(int x,int nowt){//发送脉冲
fa[x]++;//发送的多一个
edge u;//表示那边的点
for(int i=0;i<G[x].size();i++){//遍历
u=G[x][i];//找到那个节点
if(nowt+u.d<=t) js[nowt+u.d].push_back(u);//注意,要判断一下是否超时
}
}
inline void chuli(int time){//定时处理
for(int i=0;i<js[time].size();i++){//找每一条
I[js[time][i].to]+=js[time][i].w;//加上I
}
vector<edge>().swap(js[time]);//这是vector一个特殊功能,就是说把一个空vector和我们这里交换
//那么js[time]就会变成空的,而这个新vector就被扔掉了,就完成了清空,可以省空间。
}
int main(){
input();
double newv,newu;
for(int time=1;time<=t;time++){//模拟每一个时刻
memset(I,0,sizeof(I));//初始化
for(int i=n+1;i<=n+p;i++){//检查脉冲源
if(th[i].a>myrand()){
fasong(i,time);
}
}
chuli(time);//计算脉冲
for(int i=1;i<=n;i++){//检查神经元
newv=th[i].v+jg*(0.04*th[i].v*th[i].v+5*th[i].v+140-th[i].u)+I[i];//公式
newu=th[i].u+jg*th[i].a*(th[i].b*th[i].v-th[i].u);
th[i].v=newv,th[i].u=newu;
if(newv>=30){//发送脉冲
fasong(i,time);
th[i].v=th[i].c;
th[i].u+=th[i].d;
}
}
}
double mx1=-0x3f3f3f3f,mn1=0x3f3f3f3f;//注意:答案可能有负数
int mx2=-0x3f3f3f3f,mn2=0x3f3f3f3f;
for(int i=1;i<=n;i++){//比较
mx1=max(mx1,th[i].v);
mn1=min(mn1,th[i].v);
mx2=max(mx2,fa[i]);
mn2=min(mn2,fa[i]);
}
printf("%.3lf %.3lf\n",mn1,mx1);//输出
printf("%d %d\n",mn2,mx2);
return 0;
}
这个纯模拟的题目,比较考脑子和手。
下集预告:DP!