[附测试点分析][拓扑排序]计算图 PTA L3-023

本文介绍了计算图在深度学习中的核心作用,它如何表示复杂的数学表达式,并通过示例解释了如何计算函数值及偏导数。计算图中的节点表示操作,边表示依赖关系,利用拓扑排序可以有效地计算函数值和求解偏导数。文章还回顾了微积分基础知识,如偏导数和链式法则,并提供了计算示例。通过对计算图的遍历,可以动态地计算出每个输入变量的梯度。最后,给出了一个具体的计算图实例,演示了计算过程和结果。
摘要由CSDN通过智能技术生成

“计算图”(computational graph)是现代深度学习系统的基础执行引擎,提供了一种表示任意数学表达式的方法,例如用有向无环图表示的神经网络。 图中的节点表示基本操作或输入变量,边表示节点之间的中间值的依赖性。 例如,下图就是一个函数f(x1​,x2​)=lnx1​+x1​x2​−sinx2​的计算图。

现在给定一个计算图,请你根据所有输入变量计算函数值及其偏导数(即梯度)。 例如,给定输入x1​=2,x2​=5,上述计算图获得函数值 f(2,5)=ln(2)+2×5−sin(5)=11.652;并且根据微分链式法则,上图得到的梯度 ∇f=[∂f/∂x1​,∂f/∂x2​]=[1/x1​+x2​,x1​−cosx2​]=[5.500,1.716]。

知道你已经把微积分忘了,所以这里只要求你处理几个简单的算子:加法、减法、乘法、指数(ex,即编程语言中的 exp(x) 函数)、对数(lnx,即编程语言中的 log(x) 函数)和正弦函数(sinx,即编程语言中的 sin(x) 函数)。

友情提醒:

  • 常数的导数是 0;x 的导数是 1;ex 的导数还是 ex;lnx 的导数是 1/x;sinx 的导数是 cosx。
  • 回顾一下什么是偏导数:在数学中,一个多变量的函数的偏导数,就是它关于其中一个变量的导数而保持其他变量恒定。在上面的例子中,当我们对 x1​ 求偏导数 ∂f/∂x1​ 时,就将 x2​ 当成常数,所以得到 lnx1​ 的导数是 1/x1​,x1​x2​ 的导数是 x2​,sinx2​ 的导数是 0。
  • 回顾一下链式法则:复合函数的导数是构成复合这有限个函数在相应点的导数的乘积,即若有 u=f(y),y=g(x),则 du/dx=du/dy⋅dy/dx。例如对 sin(lnx) 求导,就得到 cos(lnx)⋅(1/x)。

如果你注意观察,可以发现在计算图中,计算函数值是一个从左向右进行的计算,而计算偏导数则正好相反。

输入格式:

输入在第一行给出正整数 N(≤5×10^4),为计算图中的顶点数。

以下 N 行,第 i 行给出第 i 个顶点的信息,其中 i=0,1,⋯,N−1。第一个值是顶点的类型编号,分别为:

  • 0 代表输入变量
  • 1 代表加法,对应 x1​+x2​
  • 2 代表减法,对应 x1​−x2​
  • 3 代表乘法,对应 x1​×x2​
  • 4 代表指数,对应 ex
  • 5 代表对数,对应 lnx
  • 6 代表正弦函数,对应 sinx

对于输入变量,后面会跟它的双精度浮点数值;对于单目算子,后面会跟它对应的单个变量的顶点编号(编号从 0 开始);对于双目算子,后面会跟它对应两个变量的顶点编号。

题目保证只有一个输出顶点(即没有出边的顶点,例如上图最右边的 -),且计算过程不会超过双精度浮点数的计算精度范围。

输出格式:

首先在第一行输出给定计算图的函数值。在第二行顺序输出函数对于每个变量的偏导数的值,其间以一个空格分隔,行首尾不得有多余空格。偏导数的输出顺序与输入变量的出现顺序相同。输出小数点后 3 位。

输入样例:

7
0 2.0
0 5.0
5 0
3 0 1
6 1
1 2 3
2 5 4

输出样例:

11.652
5.500 1.716

题意: 正如题目中所说的,给出一个有向无环图,同时给出若干输入变量,求最终函数值以及各变量的偏导数。

分析: 对于有向无环图很容易想到拓扑排序,对于每个结点都需要开四个数组,一个记录结点操作符,一个记录它上面的数值,还有两个分别记录操作数编号。一个结点的入边只有0、1、2这三种情况,0的时候就是起始的输入变量,1的时候就是单目运算符,2的时候就是双目运算符,对于后面两种结点,它们的数值需要先得到其前驱点的数值才能计算,而拓扑序正是满足了这一点,当某结点加入队列时其前驱上的各种信息都已经获得了,所以可以直接拿来用,比如加法:num[now] = num[a[now]]+num[b[now]],其中a[now]和b[now]分别是now点的两个操作数编号,这其实是有点动态规划的思想在的。

这样就求出了最终输出的函数值,接下来考虑每个输入变量的偏导数。其实求偏导数思想和上面一样,也是一个动态规划的思想,可以开一个数组p[i][j]表示对于第j个变量遍历到i点时其偏导数值,由于始终是以拓扑序遍历结点,所以到当前结点时其前驱结点的p值都已经获得了,还是以加法为例:p[now][i] = p[a[now]][i]+p[b[now]][i],这样就求得各变量的偏导数了。

最终只需要跑一遍拓扑排序,时间复杂度为O(n)。

下面分析一下测试点:

测试点编号数据描述
0大样例,存在2个输入变量
1存在2个输入变量
2只有1个输入变量
3只有1个输入变量
4只有1个输入变量
5存在200个输入变量

具体代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <queue>
using namespace std;

int op[50005];//操作符 
int a[50005], b[50005];//操作数(如果有的话) 
int in[50005];//入度 
double num[50005];//结点上的值 
vector<double> p[50005];//直接二维数组开不下 
vector<int> to[50005];//邻接表建图 
vector<int> x;//保存输入变量 

signed main(){
	int n;
	cin >> n;
	for(int i = 0; i < n; i++){
		scanf("%d", &op[i]);
		if(op[i] == 0){
			scanf("%lf", &num[i]);
			x.push_back(i);
		}
		if(op[i] == 1)
			scanf("%d%d", &a[i], &b[i]), to[a[i]].push_back(i), to[b[i]].push_back(i), in[i] += 2;
		if(op[i] == 2)
			scanf("%d%d", &a[i], &b[i]), to[a[i]].push_back(i), to[b[i]].push_back(i), in[i] += 2;
		if(op[i] == 3)
			scanf("%d%d", &a[i], &b[i]), to[a[i]].push_back(i), to[b[i]].push_back(i), in[i] += 2;
		if(op[i] == 4)
			scanf("%d", &a[i]), to[a[i]].push_back(i), in[i]++;
		if(op[i] == 5)
			scanf("%d", &a[i]), to[a[i]].push_back(i), in[i]++;
		if(op[i] == 6)
			scanf("%d", &a[i]), to[a[i]].push_back(i), in[i]++;
	}
	for(int i = 0; i < n; i++)
		for(int j = 0; j < x.size(); j++)
			p[i].push_back(0);
	queue<int> q;
	for(int i = 0; i < n; i++)
		if(in[i] == 0)
			q.push(i);
	for(int i = 0; i < x.size(); i++)
		p[x[i]][i] = 1;
	int final;//记录终点 
	while(q.size()){
		int now = q.front();
		q.pop();
		if(op[now] == 1){
			num[now] = num[a[now]]+num[b[now]];
			for(int i = 0; i < x.size(); i++) p[now][i] = p[a[now]][i]+p[b[now]][i];
		}
		if(op[now] == 2){
			num[now] = num[a[now]]-num[b[now]];
			for(int i = 0; i < x.size(); i++) p[now][i] = p[a[now]][i]-p[b[now]][i];
		}
		if(op[now] == 3){
			num[now] = num[a[now]]*num[b[now]];
			for(int i = 0; i < x.size(); i++) p[now][i] = p[a[now]][i]*num[b[now]]+p[b[now]][i]*num[a[now]];
		}
		if(op[now] == 4){
			num[now] = exp(num[a[now]]);
			for(int i = 0; i < x.size(); i++) p[now][i] = num[now]*p[a[now]][i];
		}
		if(op[now] == 5){
			num[now] = log(num[a[now]]);
			for(int i = 0; i < x.size(); i++) p[now][i] = 1/num[a[now]]*p[a[now]][i];
		}
		if(op[now] == 6){
			num[now] = sin(num[a[now]]);
			for(int i = 0; i < x.size(); i++) p[now][i] = cos(num[a[now]])*p[a[now]][i];
		}
		for(int i = 0; i < to[now].size(); i++){
			int v = to[now][i];
			in[v]--;
			if(in[v] == 0) q.push(v);
		}
		final = now;
	}
	printf("%.3f\n", num[final]);
	printf("%.3f", p[final][0]);
	for(int i = 1; i < x.size(); i++)
		printf(" %.3f", p[final][i]);
    return 0;
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值