1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C++標準ライブラリでNeural Network

Last updated at Posted at 2020-05-04

C++の標準ライブラリで、手書き数字データセットを分類できるニューラルネットワークを作成し、手書き数字文字認識及びアヤメの品種推定を行いました。
精度が90%程で、手書き数字認識の割には、精度が低いかなと思うので、どこかで間違えている気がします。間違いを見つけてくださった方は、コメントいただけると幸いです。

コードはVisual Studioのプロジェクトとして、GitHubにアップしてあります。

以下、序盤は手書き数字分類に関してです。

実行の様子

精度に関して

数字認識の場合、ノード数が160を超えるあたりからあまり精度に変化が見られなくなりました。また、epoch数に関しても、3を超えたあたりから、乱高下を繰り返すようになりました。

機能ごとに、何をしているのか追ってみます。

行列演算用のクラス

ニューラルネットワークを作成するにあたり、行列演算を行うことは必須だと考え、行列演算を行うためのクラスを定義しました。
今回は、次のような機能を持つクラスを定義しました。

メソッド 役割
matrix() 初期化関数。1×1行列を作成する
matrix(int nn,int mm) 初期化関数。n×m行列を作成する
matrix(matrix* a) 行列を複製する
void newmat(int nn, int mm) nn×mm行列を作成する
void copy(matrix* a) 行列をコピーする
void put(int i, int j, matrixelement e) i,jにeを代入する
matrixelement get(int i, int j) i,jの要素を取得する
void random_m05to05() すべての要素に-0.5~0.5のランダムな値を代入する
bool plus(matrix a) 引数として与えられた行列を加算する
bool minus(matrix a) 引数として与えられた行列をマイナスする
bool dot(matrix a, matrix b) 行列の掛け算をする
void output() 行列の値を標準出力へ出力する
void sigmoid() すべての要素をシグモイド関数にかけた結果に置き換える
void t_set(matrix* a) 引数の行列を転置して代入
void scalar(matrixelement k) すべての要素をkでスカラー倍する
void scalar_plus(matrixelement k) すべての要素にすべてkを加算する
void scalar_division(matrixelement k) すべての要素をスカラーで除算する
bool multiplication(matrix* a) 引数として与えられた行列を、対応する要素に掛け合わせる
void clear() 行列用メモリを解放する

for文など、基本的な文法だけで実装することができました。

class matrix {
private:
public:
	// Matrix body
	matrixelement* mat;

	// Matrix size
	int n, m;

	// Init matrix
	matrix() {
		n = m = 1;
		mat = new matrixelement[n * m];
		memset(mat, 0, sizeof(matrixelement) * n * m);
	}
	matrix(int nn, int mm) {
		mat = new matrixelement[nn * mm];
		memset(mat, 0, sizeof(matrixelement) * nn * mm);
		n = nn;
		m = mm;
	}
	matrix(matrix* a) {
		mat = new matrixelement[a->n * a->m];
		n = a->n;
		m = a->m;
		memcpy(mat, a->mat, m * n * sizeof(matrixelement));
	}
	void newmat(int nn, int mm) {
		n = nn;
		m = mm;
		mat = new matrixelement[n * m];
		memset(mat, 0, sizeof(matrixelement) * n * m);
	}
	void copy(matrix* a) {
		n = a->n;
		m = a->m;
		delete[] mat;
		mat = new matrixelement[a->n * a->m];
		memcpy(mat, a->mat, m * n * sizeof(matrixelement));
	}
	void put(int i, int j, matrixelement e) {
		if (i >= n || j >= m || i < 0 || j < 0) return;
		mat[i * m + j] = e;
	}
	matrixelement get(int i, int j) {
		if (i >= n || j >= m || i < 0 || j < 0) return -1;
		return mat[i * m + j];
	}
	void random_m05to05() {
		std::random_device rnd;
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] = (matrixelement)(rnd() % RAND) / (matrixelement)RAND - 0.5f;
	}
	bool plus(matrix a) {
		if (a.n != n || a.m != m) return false;
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] += a.mat[i * m + j];
		return true;
	}
	bool minus(matrix a) {
		if (a.n != n || a.m != m) return false;
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] -= a.mat[i * m + j];
		return true;
	}
	bool dot(matrix a, matrix b) {
		if (a.m != b.n) return false;
		n = a.n;
		m = b.m;
		delete[] mat;
		mat = new matrixelement[n * m];
		memset(mat, 0, sizeof(matrixelement) * n * m);
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				for (int k = 0; k < a.m; k++) {
					put(i, j, get(i, j) + a.get(i, k) * b.get(k, j));
				}
			}
		}
		return true;
	}
	void output() {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				printf("[%f]	", get(i, j));
			}
			cout << "\n";
		}
		cout << "\n";
	}
	void sigmoid() {
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] = sigmoidfunction(mat[i * m + j]);
	}
	void t_set(matrix* a) {
		n = a->m;
		m = a->n;
		delete[] mat;
		mat = new matrixelement[n * m];
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] = a->mat[j * a->m + i];
	}
	void scalar(matrixelement k) {
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] *= k;
	}
	void scalar_plus(matrixelement k) {
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] += k;
	}
	void scalar_division(matrixelement k) {
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] /= k;
	}
	bool multiplication(matrix* a) {
		if (n != a->n) return false;
		if (m != a->m) return false;
		for (int i = 0; i < n; i++)
			for (int j = 0; j < m; j++)
				mat[i * m + j] *= a->mat[i * m + j];
	}
	void clear() {
		delete[] mat;
	}
};

ニューラルネットワークのクラス

行列演算のクラスに機能を追加するのと並行で、ニューラルネットワークのプログラムを書いていきました。

メンバー変数

変数名 役割
int input_nodes 入力層のノード数
int hidden_nodes 隠れ層のノード数
int output_nodes 出力層のノード数
int hidden_layer 隠れ層を複数構成にしようとしていた際の残骸(常に1)
vector w 隠れ層を複数構成にしようとしていた際の残骸(隠れ層の行列が格納される)
matrix wo 出力層の重みを記憶する行列

メンバー関数

メソッド名 役割
void save(string fname) 訓練済みのニューラルネットワークを保存する関数
void load(string fname) 訓練済みのニューラルネットワークを読み込む関数
void init() 初期化関数。設定されたパラメータでネットワークを作成する
void train(matrix* input, matrix* ans, float rate) 入力と出力の行列を指定し、学習する関数
void query(matrix* input, matrix* output) 入力から結果を得る関数

順に見ていきます。

save 関数

save関数では、学習済みネットワークの各ノードでの重みをファイルに書き出します。
各層のサイズを出力した後に、各層の重みを表す行列を書き出します。

void save(string fname) {
	FILE* fp = fopen(fname.c_str(), "wb");
	fwrite(&input_nodes, sizeof(int), 1, fp);
	fwrite(&hidden_nodes, sizeof(int), 1, fp);
	fwrite(&output_nodes, sizeof(int), 1, fp);
	fwrite(&hidden_layer, sizeof(int), 1, fp);
	for (int i = 0; i < w.size(); i++) {
		fwrite(&w[i].n, sizeof(int), 1, fp);
		fwrite(&w[i].m, sizeof(int), 1, fp);
		fwrite(w[i].mat, sizeof(matrixelement), w[i].n * w[i].m, fp);
	}
	fwrite(&wo.n, sizeof(int), 1, fp);
	fwrite(&wo.m, sizeof(int), 1, fp);
	fwrite(wo.mat, sizeof(matrixelement), wo.n * wo.m, fp);
	fclose(fp);
}

load 関数

load関数では、上記save関数で書き出した学習済みネットワークの各ノードでの重みをファイルから読み出します。
各層のサイズを読み込んだ後に、各層の重みを表す行列を読み込みます。
この関数により、前回実行した際の学習結果を再利用することができます。

なお、Load関数に関しては、手書き数字の認識とアヤメの品種推定で、別々の関数を定義しています。下記コードは手書き数字認識のコードであり、アヤメの品種推定は後述しています。

void load(string fname) {
	FILE* fp = fopen(fname.c_str(), "rb");
	fread(&input_nodes, sizeof(int), 1, fp);
	fread(&hidden_nodes, sizeof(int), 1, fp);
	fread(&output_nodes, sizeof(int), 1, fp);
	fread(&hidden_layer, sizeof(int), 1, fp);
	for (int i = 0; i < hidden_layer; i++) {
		w.push_back(matrix());
		fread(&w[i].n, sizeof(int), 1, fp);
		fread(&w[i].m, sizeof(int), 1, fp);
		w[i].newmat(w[i].n, w[i].m);
		fread(w[i].mat, sizeof(matrixelement), w[i].n * w[i].m, fp);
	}
	fread(&wo.n, sizeof(int), 1, fp);
	fread(&wo.m, sizeof(int), 1, fp);
	wo.newmat(wo.n, wo.m);
	fread(wo.mat, sizeof(matrixelement), wo.n * wo.m, fp);
}

init 関数

予め設定されたパラメータを基に、隠れ層のランダムな値での初期化といった処理を行います。

void init() {
	int bef = input_nodes;
	for (int k = 0; k < hidden_layer; k++) {
		w.push_back(matrix());
		w[k].newmat(hidden_nodes, bef);
		bef = hidden_nodes;
		w[k].random_m05to05();
	}
	wo.newmat(output_nodes, hidden_nodes);
	wo.random_m05to05();
}

train 関数

ニューラルネットワークの学習を行う関数です。
入力データと答えのデータ、学習率を指定して、学習を行う関数です。
出力層に対しては、ニューラルネットワークで答えを出した後、正解との差に学習率をかけ、加算していきます。
また、隠れ層に関しては、誤差逆伝搬を用いてエラーを修正していきます。

void train(matrix* input, matrix* ans, float rate) {
	matrix final_outputs;
	vector<matrix> hidden_inputs;
	vector<matrix> hidden_outputs;
	hidden_inputs.push_back(matrix());
	hidden_inputs[0].copy(input);
	for (int i = 0; i < w.size(); i++) {
		hidden_outputs.push_back(matrix());
		hidden_outputs[i].dot(w[i], hidden_inputs[i]);
		hidden_outputs[i].sigmoid();
		hidden_inputs.push_back(matrix());
		hidden_inputs[i + 1].copy(&hidden_outputs[i]);
	}
	final_outputs.dot(wo, hidden_inputs[w.size()]);
	final_outputs.sigmoid();
    
	matrix output_errors(ans);
	output_errors.minus(final_outputs);

	output_errors.multiplication(&final_outputs);
	matrix s(ans->n, ans->m), t, u;
	s.scalar_plus(1.0f);
	s.minus(final_outputs);
	output_errors.multiplication(&s);
	t.t_set(&hidden_outputs[w.size() - 1]);
	u.dot(output_errors, t);

	final_outputs.clear();
	s.clear();

	wo.plus(u);
	t.clear();
	u.clear();

    // Backpropagation
	for (int i = w.size() - 1; i >= 0; i--) {
		matrix hidden_errors;
		matrix wt;
		wt.t_set(&w[i]);
		hidden_errors.dot(wt, output_errors);
		output_errors.copy(&hidden_errors);

		matrix as(hidden_errors.n, hidden_errors.m);
		as.scalar_plus(1.0f);
		as.minus(hidden_outputs[i]);
		matrix at(&hidden_errors);
		at.multiplication(&hidden_outputs[i]);
		at.multiplication(&as);
		matrix au;
		au.t_set(&hidden_inputs[i]);
		matrix av;
		av.dot(au, av);
		av.scalar(rate);

		w[i].plus(av);
		wt.clear();
		as.clear();
		at.clear();
		au.clear();
		av.clear();
	}

	for (int i = 0; i < w.size(); i++) {
		hidden_inputs[i].clear();
		hidden_outputs[i].clear();
	}
	hidden_inputs[w.size()].clear();
	output_errors.clear();
}

query 関数

学習済みニューラルネットワークで、入力に対する出力を得ます。

	void query(matrix* input, matrix* output) {
		matrix x(input);
		for (int i = 0; i < w.size(); i++) {
			matrix o;
			o.dot(w[i], x);
			o.sigmoid();
			x.copy(&o);
			o.clear();
		}
		output->dot(wo, x);
		output->sigmoid();
		x.clear();
	}
};

ニューラルネットに関するプログラムは、ここまでです。

データの読み込み学習や入力などを行う部分

続いて、入出力などを行う処理を書いていきました。
手順としては

  1. 入力数や出力数など、ニューラルネットワークの設定
  2. 学習済みネットワークの読み込み
  3. テストデータによるネットワークの評価

という手順で行っています。

また、defineでモードを切り替えられるようにしています。

define 内容
MODE_USE_TRAINED_DATA 学習済みネットワークを利用する
MODE_OUTPUT_ON テストデータの分類時に、詳細を表示する
void loadfile(string fname, vector<matrix>* inputs, vector<matrix>* outputs) {
	// Load file
	FILE* fp = NULL;
	fp = fopen(fname.c_str(), "r");

	while (!feof(fp))
	{
		char buf[1024 * 5];
		fgets(buf, 1024 * 5, fp);
		CellsTy cells = cells_split(buf, ',');
		if (cells.size() < 785) continue;

		matrix input(784, 1), output(10, 1);
		for (int i = 0; i < 784; i++) {
			input.put(i, 0, atof(cells[i + 1].c_str()));
		}
		input.scalar_division(255.0f);
		input.scalar(0.999f);

		output.scalar_plus(ZERO);
		output.put(atoi(cells[0].c_str()), 0, ONE);
		inputs->push_back(input);
		outputs->push_back(output);
//		output.output();
	}
	fclose(fp);
}

//#define MODE_USE_TRAINED_DATA
//#define MODE_OUTPUT_ON

int main() {

	// Setting neural network
	NeuralNetwork nn;
	nn.input_nodes = 784;
	nn.hidden_nodes = 200;
	nn.hidden_layer = 1;
	nn.output_nodes = 10;
	nn.learning_rate = 0.1;
	int epochs = 10;

	vector<matrix> inputs;
	vector<matrix> outputs;

	nn.init();

    // Load or training network 
#ifdef MODE_USE_TRAINED_DATA
	nn.load("w");
#else
	loadfile("./sample_data/mnist_train_small.csv", &inputs, &outputs);
	cout << "Start training" << endl;
	for (int i = 0; i < epochs; i++) {
		cout << "epoch:	" << i << endl;
		for (int j = 0; j < inputs.size(); j++) {
			nn.train(&inputs[j], &outputs[j], 0.1);
		}
	}
	cout << "Training Done !" << endl;
	nn.save("w");
	cout << "Saved." << endl;
#endif

    // Test network
	inputs.clear();
	outputs.clear();

	loadfile("./sample_data/mnist_test.csv", &inputs, &outputs);

	int cou = 0;
	int sum = 0;
	for (int j = 0; j < inputs.size(); j++) {
		int ans = 0;
		for (int i = 1; i < 10; i++) {
			if (outputs[j].get(i, 0) > outputs[j].get(ans, 0)) ans = i;
		}

		nn.query(&inputs[j], &outputs[j]);
		int mindex = 0;
		for (int i = 1; i < 10; i++) {
			if (outputs[j].get(i, 0) > outputs[j].get(mindex, 0)) mindex = i;
		}
#ifdef MODE_OUTPUT_ON
		outputs[j].output();
		cout << "Answer:" << ans << endl;
		if (ans == mindex) cout << "success" << endl;
		else cout << "failed" << endl;
#endif
		if (ans == mindex) cou++;
		sum++;
	}
	cout << "Correct answer:" << cou << endl;
	cout << "Fialed answer:" << sum - cou << endl;
	cout << "Success rate:" << (float)cou / (float)(sum) << endl;
}

アヤメの品種推定

ソースコードを若干変更することで、アヤメの品種推定もできるようになりました。
アヤメの品種推定は、精度91%程でした。

前処理

Pandasのテスト用データとしてGithubに存在するCSVファイルを、ランダムに並び替えた後、学習用データ7割とテスト用データ3割に分割しました。

import pandas as pd
import numpy as np

df = pd.read_csv('iris.csv')
df = df.sample(frac=1)

df = df.replace('Iris-setosa', '0')
df = df.replace('Iris-versicolor', '1')
df = df.replace('Iris-virginica', '2')

df.insert(0, 'IrisName', df['Name'])

ratio = 0.7
p = int(ratio * len(df))
df1 = df.iloc[:p, :]
df2 = df.iloc[p:, :]

df1.to_csv("iris_train.csv", index=False, header=None)
df2.to_csv("iris_test.csv", index=False, header=None)

変更部分のコード

void loadfile(string fname, vector<matrix>* inputs, vector<matrix>* outputs,int input_nodesize,int output_nodesize) {
	// Load file
	FILE* fp = NULL;
	fp = fopen(fname.c_str(), "r");

	while (!feof(fp))
	{
		char buf[1024 * 5];
		fgets(buf, 1024 * 5, fp);
		CellsTy cells = cells_split(buf, ',');
		if (cells.size() < input_nodesize+1) continue;

		matrix input(input_nodesize, 1), output(output_nodesize, 1);
		for (int i = 0; i < input_nodesize; i++) {
			input.put(i, 0, atof(cells[i + 1].c_str()));
		}
//		input.scalar_division(255.0f);
//		input.scalar(0.999f);

		output.scalar_plus(ZERO);
		output.put(atoi(cells[0].c_str()), 0, ONE);
		inputs->push_back(input);
		outputs->push_back(output);
	}
	fclose(fp);
}

//#define MODE_USE_TRAINED_DATA
//#define MODE_OUTPUT_ON

int main() {

	// Setting neural network
	NeuralNetwork nn;
	nn.input_nodes = 4;
	nn.hidden_nodes = 84;
	nn.hidden_layer = 1;
	nn.output_nodes = 3;
	int epochs = 200;

	vector<matrix> inputs;
	vector<matrix> outputs;

	nn.init();

	// Load or training network 
#ifdef MODE_USE_TRAINED_DATA
	nn.load("w");
#else
	loadfile("./sample_data/ayame.csv", &inputs, &outputs, nn.input_nodes, nn.output_nodes);
	cout << "Start training" << endl;
	for (int i = 0; i < epochs; i++) {
		cout << "epoch:	" << i << endl;
		for (int j = 0; j < inputs.size(); j++) {
			nn.train(&inputs[j], &outputs[j], 0.01);
		}
	}
	cout << "Training Done !" << endl;
	nn.save("w");
	cout << "Saved." << endl;
#endif

	// Test network
	inputs.clear();
	outputs.clear();

	loadfile("./sample_data/ayame_test.csv", &inputs, &outputs, nn.input_nodes, nn.output_nodes);

	int cou = 0;
	int sum = 0;
	for (int j = 0; j < inputs.size(); j++) {
		int ans = 0;
		for (int i = 1; i < nn.output_nodes; i++) {
			if (outputs[j].get(i, 0) > outputs[j].get(ans, 0)) ans = i;
		}

		nn.query(&inputs[j], &outputs[j]);
		int mindex = 0;
		for (int i = 1; i < nn.output_nodes; i++) {
			if (outputs[j].get(i, 0) > outputs[j].get(mindex, 0)) mindex = i;
		}
#ifdef MODE_OUTPUT_ON
		outputs[j].output();
		cout << "Answer:" << ans << endl;
		if (ans == mindex) cout << "success" << endl;
		else cout << "failed" << endl;
#endif
		if (ans == mindex) cou++;
		sum++;
	}
	cout << "Correct answer:" << cou << endl;
	cout << "Fialed answer:" << sum - cou << endl;
	cout << "Success rate:" << (float)cou / (float)(sum) << endl;
}

ソースコードの全体像

最初に記した通り、GitHubにアップしていますので、併せて参照してください。

読み込み部分を変更することで、Neural Network部分に関しては変更を加えずとも

  • 手書き数字認識
  • スイセンの品種分類

が9割ほどの精度でできたので、他のデータに関しても試せればと思います。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?