神经网络初探(BP 算法、手写数字识别)

神经网络的结构就不说了,网上一大堆……

这次手写数字识别采用的是 sigmoid 激活函数和 MSE 损失函数。

虽然网上说这种方式比不上 softmax 激活函数和交叉熵损失函数,后者更适合用于分类,但是实测前者可以达到 97.4% 的准确率,后者本机只能达到 92% 的准确率(固定学习率不加任何优化)。

这里就记录一下计算梯度的方式。

对于最后一层,记 d i d_i di 表示点 i i i 激活之前的值, v i = sigmoid ( d i ) = 1 1 + e − d i v_i=\text{sigmoid}(d_i)=\frac{1}{1+e^{-d_i}} vi=sigmoid(di)=1+edi1,记期望得到的值为 g i g_i gi,那么 MSE 损失函数为 ∑ ( v j − g j ) 2 \sum (v_j-g_j)^2 (vjgj)2,对 v i v_i vi 求偏导,得到的是 2 ( v i − g i ) 2(v_i-g_i) 2(vigi)

根据链式法则,我们要计算 ∂ E t o t ∂ d i \frac{\partial E_{tot}}{\partial d_i} diEtot,则分别计算 ∂ E t o t ∂ v i , ∂ v i ∂ d i \frac{\partial E_{tot}}{\partial v_i},\frac{\partial v_i}{\partial d_i} viEtot,divi

前者就是 2 ( v i − g i ) 2(v_i-g_i) 2(vigi),后者就是 sigmoid 的导数,算一下发现是 v i ( 1 − v i ) v_i(1-v_i) vi(1vi)

考虑从后往前计算,对于点 i i i 的后继点 j j j 都能求出 ∂ E t o t ∂ d j \frac{\partial E_{tot}}{\partial d_j} djEtot 的话,那么我们就能求出 ∂ E t o t ∂ d i \frac{\partial E_{tot}}{\partial d_i} diEtot

考虑先求出 ∂ E t o t ∂ v i = ∑ ∂ E t o t ∂ v j ⋅ w i , j \frac{\partial E_{tot}}{\partial v_i}=\sum \frac{\partial E_{tot}}{\partial v_j} \cdot w_{i,j} viEtot=vjEtotwi,j,其中 w i , j w_{i,j} wi,j 是边权。

一般的,对于同层的点 i , j i,j i,j,我们也有 ∂ E t o t ∂ d i = ∑ ∂ E t o t ∂ v j ⋅ ∂ v j ∂ d i \frac{\partial E_{tot}}{\partial d_i}=\sum \frac{\partial E_{tot}}{\partial v_j} \cdot \frac{\partial v_j}{\partial d_i} diEtot=vjEtotdivj。在 sigmoid 函数的情况下,由于 i ≠ j i \neq j i=j v j v_j vj d i d_i di 无关,因此 ∂ E t o t ∂ d i = ∂ E t o t ∂ v i ⋅ v i ( 1 − v i ) \frac{\partial E_{tot}}{\partial d_i}=\frac{\partial E_{tot}}{\partial v_i} \cdot v_i(1-v_i) diEtot=viEtotvi(1vi)。于是就可以递推出每个点了。

对于边 w i , j w_{i,j} wi,j(从 i i i 连向 j j j),它需要减掉 η v i ∂ E t o t ∂ d j \eta v_i\frac{\partial E_{tot}}{\partial d_j} ηvidjEtot ∂ d j ∂ w i , j = v i \frac{\partial d_j}{\partial w_{i,j}}=v_i wi,jdj=vi)。

对于 softmax 和交叉熵的组合,一定要注意同层的 i ≠ j i \neq j i=j 也是要计算的即可。下面放一下 C++ 实现的板子。

sigmoid+MSE:

mt19937 mt;
double randd() {//[-1,1)
	return 2.0 * mt() / UINT_MAX - 1;
}
double sigmoid(double x) {
	return 1 / (1 + exp(-x));
}
typedef vector<double> VD;
typedef vector<VD> VDD;
typedef vector<VDD> VDDD;

struct NNs {
	VDDD w, dw; VDD v, b, db;
	vector<int> sz;
	int n; double eta;
	
	void init(const vector<int> &s, double _eta = 0.01) {
		eta = _eta;
		sz = s;
		n = sz.size() - 1;
		v.resize(n + 1);
		w.resize(n);
		b.resize(n);
		db.resize(n);
		dw.resize(n);
		for (int i = 0; i <= n; i++)
			v[i].resize(sz[i]);
		for (int i = 0; i < n; i++) {
			w[i].resize(sz[i + 1]);
			dw[i].resize(sz[i + 1]);
			b[i].resize(sz[i + 1]);
			db[i].resize(sz[i + 1]);
			for (int j = 0; j < sz[i + 1]; j++) {
				w[i][j].resize(sz[i]);
				dw[i][j].resize(sz[i]);
				for (int k = 0; k < sz[i]; k++)
					w[i][j][k] = randd() / sz[i] * 2;
				b[i][j] = randd();
			}
		}
	}
	
	VD calc(const VD &s) {
		v[0] = s;
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < sz[i + 1]; j++) {
				double d = b[i][j];
				for (int k = 0; k < sz[i]; k++)
					d += w[i][j][k] * v[i][k];
				v[i + 1][j] = sigmoid(d);
			}
		}
		return v[n];
	}
	
	void back(const VD &g) {
		for (int i = 0; i < sz[n]; i++)
			v[n][i] = v[n][i] * (1 - v[n][i]) * 2 * (v[n][i] - g[i]);
		for (int i = n; i > 0; i--) {
			VD l(sz[i - 1]);
			for (int j = 0; j < sz[i]; j++) {
				for (int k = 0; k < sz[i - 1]; k++) {
					l[k] += v[i][j] * w[i - 1][j][k];
					dw[i - 1][j][k] += v[i][j] * v[i - 1][k];
				}
				db[i - 1][j] += v[i][j];
			}
			for (int j = 0; j < sz[i - 1]; j++)
				v[i - 1][j] = v[i - 1][j] * (1 - v[i - 1][j]) * l[j];
		}
	}
	
	void train(VDD s, VDD g, int B = 1) {
		int m = (int)s.size();
		for (int i = 0; i < m; i++) {
			int t = mt() % (i + 1);
			swap(s[i], s[t]), swap(g[i], g[t]);
		}
		for (int i = 0; i < m; i += B) {
			for (int j = 0; j < n; j++)
			for (int k = 0; k < sz[j + 1]; k++) {
				db[j][k] = 0;
				for (int l = 0; l < sz[j]; l++)
					dw[j][k][l] = 0;
			}
			for (int j = i; j < i + B && j < m; j++) {
				calc(s[j]);
				back(g[j]);
			}
			double coef = eta / min(m - i, B);
			for (int j = 0; j < n; j++)
			for (int k = 0; k < sz[j + 1]; k++) {
				b[j][k] -= db[j][k] * coef;
				for (int l = 0; l < sz[j]; l++)
					w[j][k][l] -= dw[j][k][l] * coef;
			}
		}
	}
	
	void input(FILE *f) {
		fscanf(f, "%d", &n);
		sz.resize(n + 1);
		for (int i = 0; i <= n; i++)
			fscanf(f, "%d", &sz[i]);
		v.resize(n + 1);
		w.resize(n);
		b.resize(n);
		db.resize(n);
		dw.resize(n);
		for (int i = 0; i <= n; i++)
			v[i].resize(sz[i]);
		for (int i = 0; i < n; i++) {
			w[i].resize(sz[i + 1]);
			dw[i].resize(sz[i + 1]);
			b[i].resize(sz[i + 1]);
			db[i].resize(sz[i + 1]);
			for (int j = 0; j < sz[i + 1]; j++) {
				w[i][j].resize(sz[i]);
				dw[i][j].resize(sz[i]);
				for (int k = 0; k < sz[i]; k++)
					fscanf(f, "%lf", &w[i][j][k]);
			}
		}
		for (int i = 0; i < n; i++)
		for (int j = 0; j < sz[i + 1]; j++)
			fscanf(f, "%lf", &b[i][j]);
	}
	
	void output(FILE *f) {
		fprintf(f, "%d\n", n);
		for (int i = 0; i <= n; i++)
			fprintf(f, "%d%c", sz[i], " \n"[i == n]);
		for (int i = 0; i < n; i++)
		for (int j = 0; j < sz[i + 1]; j++)
		for (int k = 0; k < sz[i]; k++)
			fprintf(f, "%.12lf ", w[i][j][k]);
		for (int i = 0; i < n; i++)
		for (int j = 0; j < sz[i + 1]; j++)
			fprintf(f, "%.12lf ", b[i][j]);
	}
} nns;

int main() {
	nns.init({2, 2, 2}, 0.1);//size
}

softmax 和交叉熵的只有 calc 函数和 back 函数不同(这里 int 参数直接传入类别编号):

int calc(const VD &s) {
	v[0] = s;
	for (int i = 0; i < n; i++) {
		double a = 0, mx = 0;
		for (int j = 0; j < sz[i + 1]; j++) {
			double d = b[i][j];
			for (int k = 0; k < sz[i]; k++)
				d += w[i][j][k] * v[i][k];
			v[i + 1][j] = d;
			chkmax(mx, d);
		}
		for (int j = 0; j < sz[i + 1]; j++)
			a += (v[i + 1][j] = exp(v[i + 1][j] - mx));
		for (int j = 0; j < sz[i + 1]; j++)
			v[i + 1][j] /= a;
	}
	return max_element(v[n].begin(), v[n].end()) - v[n].begin();
}

void back(int g) {
	for (int i = 0; i < sz[n]; i++)
		v[n][i] = i == g ? v[n][i] - 1 : v[n][i];
	for (int i = n; i > 0; i--) {
		VD l(sz[i - 1]);
		for (int j = 0; j < sz[i]; j++) {
			for (int k = 0; k < sz[i - 1]; k++) {
				l[k] += v[i][j] * w[i - 1][j][k];
				dw[i - 1][j][k] += v[i][j] * v[i - 1][k];
			}
			db[i - 1][j] += v[i][j];
		}
		double s = 0;
		for (int j = 0; j < sz[i - 1]; j++)
			s += v[i - 1][j] * l[j];
		for (int j = 0; j < sz[i - 1]; j++)
			v[i - 1][j] = (l[j] - s) * v[i - 1][j];
	}
}

手写数字识别

上 MNIST 下载了 6w 个训练数据和 1w 个测试数据。

softmax 和交叉熵的表现很不好,不能有隐含层,如果有的话好像效果极差(知道怎么优化的教教本蒟蒻,不胜感激)

没有隐含层的话,学习率大约 0.003~0.005 左右最优,正确率大致可以达到 92% 多一点。

sigmoid 和 MSE 的表现还算不错,含有一层节点数为 64 的看起来挺优的,学习率大致 0.05,迭代 100 次可以达到约 97.4% 的正确率。(MNIST 上的数字有的我都认不得,也不能怪神经网络菜了 2333)

然后大概远古时期有个联考题,大概是给定一条数字(有噪点),要求识别出所有的数字。

在这里插入图片描述
数字还算挺工整的,大概如上图。只有黑白像素图的噪点还是很好去掉的,搜一下连通块大小,如果 ≤ 8 \le 8 8 就直接删掉好了。此外,这个图稍微大了些,缩成 28 × 28 28\times28 28×28 的比较好,于是删掉一些空白行,然后四宫格缩成一个格子即可。然后按照空白截断成若干个单独的数字,每个数字扩展到恰好 28 × 28 28\times28 28×28 的大小。

注意最好把所有数字都放到正中间(即上下空白行一样多,左右空白列一样多),这样可以显著提高神经网络的识别率。

然后经过上述处理之后,大概就长这样了:

在这里插入图片描述
还是相当工整的,比 MNIST 上的数字不知道好看多少倍(雾),所以拿着 2w 个训练数据暴力迭代 50 次,就可以对这些数字达到 100% 正确率的识别了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值