미연시리뷰

머신러닝 cll - forward backwrad

두원공대88학번뚜뚜 2020. 11. 13. 18:20
/*
*	Multi-Layer Perceptron example
*/
#include "mlp.h"
#include <mnist.h>

int num_of_epoch = 2;

int main() {
	int num_of_train, num_of_valid;
	MNIST* valid = ReadMNIST("./mnist/t10k-images.idx3-ubyte", "./mnist/t10k-labels.idx1-ubyte", &num_of_valid, 0); // validation data
	MNIST* train = ReadMNIST("./mnist/train-images.idx3-ubyte", "./mnist/train-labels.idx1-ubyte", &num_of_train, 0); // training data
	//입력층 784=28*28
	//은닉층 100
	//출력층 10 개인 MLP를 생성
	int layers[] = { 28 * 28, 100, 10 };
	Network network = CreateNetwork(layers, sizeof(layers) / sizeof(int));

	num_of_train /= 10;				//계산시간 관계로 실습 진행시에는 1/10값을 사용해서 Accuracy가 올라가는지 확인. 실습 검사시에는 이 문장 주석처리 후 실행 결과 확인.

	float learning_rate = 0.03F;
	for (int e = 1; e <= num_of_epoch; e++) {
		int answer = 0;
		for (int j = 0; j < num_of_train; j++) {
			int a = Forward(&network, train[j].image);
			if (a == train[j].label) {
				answer++;
			}
			Backward(&network, train[j].label, learning_rate);
		}
		printf("epoch = %d\ttrain : %f\t", e, (float)answer / num_of_train);
		answer = 0;
		for (int j = 0; j < num_of_valid; j++) {
			int a = Forward(&network, valid[j].image);
			if (a == valid[j].label) {
				answer++;
			}
		}
		printf("valid : %f\n", (float)answer / num_of_valid);
	}
	return 0;
}​
#ifndef MLP_H
#define MLP_H
#include<stdio.h>
#include<stdlib.h>
#include<float.h>
#include<math.h>
//#include<openblas/cblas.h>		//openblas는 선택입니다. cblas_sgemm을 사용할수 없다면 아래의 my_sgemm을 사용할 수 있다.
#ifndef CBLAS_H
#define CblasRowMajor 0
#define CblasNoTrans 111
#define CblasTrans 112	//112는 openblas의 CblasTrans의 상수
#endif
#ifndef MAX
#  define MAX(a,b)  ((a) < (b) ? (b) : (a))
#endif
#ifndef MIN
#  define MIN(a,b)  ((a) > (b) ? (b) : (a))
#endif
#ifndef __cplusplus
typedef struct FCLayer FCLayer;
typedef struct Network Network;
#endif

//밑의 구조체들이 노드(혹은 뉴런)
struct FCLayer {		// Fully connected layer
	int n;				// 뉴런의 개수
	float* w;			// 가중치 , [이전레이어의 크기] x [현재레이어의 크기] 의 2차원 행렬                         (실제로는 1차원)
	float* neuron;	    // 뉴런
	float* error;		// 오차
};
struct Network {
	FCLayer* layers;	//레이어들의 배열
	int depth;			//레이어의 개수
};
inline Network CreateNetwork(int* size_of_layers, int num_of_layers) {
	/*
	*	@TODO
	*	1. num_of_layers 만큼 레이어를 생성
	*	2. Network::depth는 num_of_layers와 같다
	*	3. 각 Layer마다 뉴런의 개수를 size_of_layers 배열을 이용해 설정
	*	4. 각 Layer의 error와 neuron을 할당
	*	5. Layer의 w를 이전레이어의크기x현재레이어의크기 의 2차원 배열로 할당
	*	6. 할당한 w를 [-1,1]의 실수 난수 값으로 초기화
	*/
	Network network;
	network.layers = (FCLayer*)calloc(num_of_layers, sizeof(FCLayer));
	network.depth = num_of_layers; //2

	for (int i = 0; i < num_of_layers; i++) {
		network.layers[i].n = size_of_layers[i];
		network.layers[i].error = (float*)calloc(size_of_layers[i], sizeof(float));
		if (i != 0) {	//첫번째 레이어는 가중치와 뉴런값이 없음.
			network.layers[i].w = (float*)calloc(size_of_layers[i - 1] * size_of_layers[i], sizeof(float));
			network.layers[i].neuron = (float*)calloc(size_of_layers[i], sizeof(float));
			for (int j = 0; j < size_of_layers[i - 1] * size_of_layers[i]; j++) {
				network.layers[i].w[j] = (float)rand() / RAND_MAX * 2 - 1.0F;	//[-1,1] 로 초기화
			}
		}
	}
	return network;
}
/*
M*K행렬 A와 K*N행렬 B를 곱해서 AB (M*N)을 만들어 놓고, 최종적으로 C에다가 alpha*AB + beta*C값을 덮어 씌우는 모양입니다.
나머지 변수들에 대해선 아래에 자세히 설명이 되어 있습니다.
*/
inline void my_sgemm(int major, int transA, int transB, int M, int N, int K, float alpha, float* A, int lda, float* B, int ldb, float beta, float* C, int ldc) {
	//aAB+bC=C
	for (int m = 0; m < M; m++) {
		for (int n = 0; n < N; n++) {
			float c = C[m*ldc + n];
			C[m*ldc + n] = 0.0F;
			for (int k = 0; k < K; k++) {
				float a, b;
				a = transA == CblasTrans ? A[k*lda + m] : A[m*lda + k];
				b = transB == CblasTrans ? B[n*ldb + k] : B[k*ldb + n];
				C[m*ldc + n] += a*b;
			}
			C[m*ldc + n] = alpha*C[m*ldc + n] + beta*c;
		}
	}
}
 
//딥러닝에서는 ReLU를 많이 사용하나, 
//간단한 실습이니 Sigmoid를 사용
inline float Sigmoid(float x) {
	return 1.0F / (1.0F + expf(-x));
}
inline float Sigmoid_Derivative(float x) {
	return x*(1.0F - x);
}
inline int Forward(Network* network, float* input) {
	/*
	*	@TODO
	*	1. 첫번째 레이어의 neuron 값은 input 
	*	2. 순방향으로 레이어를 순회하면서 Neuron의 값들을 계산
	*	3. 현재 레이어의 뉴런의 값은 이전 레이어의 뉴런과 가중치의 행렬곱으로 표기할 수 있다
	*	@example
	*	[100][784] 행렬인 W와 [784][1] 행렬인 이전 뉴런값을 행렬곱하여 [100][1]인 현재 뉴런의 값을 계산할 수 있다.
	*	@gemm references
	*		gemm은 A,B,C가 행렬 a,b가 스칼라 값일때 aAB+bC를 계산합니다. 즉 A,B,C가 모두 입력 행렬 이고 C는 출력 행렬
	*		major : 행렬이 RowMajor인지 ColMajor 인지 결정합니다. C언어는 RowMajor 이므로 CblasRowMajor를 입력한다.
	*		transA : A행렬을 전치행렬로 사용할 것인지, 전치행렬이면 CblasTrans, 그대로 사용할 것이면 CblasNoTrans를 입력 한다
	*		transB : B행렬을 전치행렬로 사용할 것인지
	*		M,N,K : [M][K] 크기의 행렬과 [K][N]의 행렬 을 곱해 [M][N]의 C행렬을 생성
	*		alpha : aAB+bC 를 계산할때 곱해지는 a(스칼라) 값입니다. 단순히 AB 를 계산하고 싶으면 alpha=1.0, beta를 0.0 으로 입력
	*		A : 입력행렬 A
	*		lda : A행렬의 2번째 차원의 값. 5x4을 1차원으로 표기한 행렬의 i,j 접근은 A[i*4+j] 로 표기한다. 이때 4가 lda 
	*		B : 입력행렬 B
	*		ldb : B행렬의 2번째 차원의 값
	*		beta : b의 값
	*		C : 입출력 행렬 C
	*		ldc : C행렬의 2번째 차원의 값
	*/

	network->layers[0].neuron = input;					
	for (int i = 1; i < network->depth; i++) {
		//현재 레이어의 뉴런의 값은 이전 레이어의 뉴런과 가중치의 계산으로 나온다. aAB+bC
		my_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans
					, network->layers[i].n      // M
					, 1                         // N
					, network->layers[i - 1].n  // K
					, 1.0F    // alpha
					, network->layers[i].w, network->layers[i - 1].n    // A, lda
					, network->layers[i - 1].neuron, 1      // B, ldb
					, 0.0F    // beta
					, network->layers[i].neuron, 1);        // C, ldc
		for (int j = 0; j < network->layers[i].n; j++) {
			network->layers[i].neuron[j] = Sigmoid(network->layers[i].neuron[j]);
		}
	}

	int a = 0;
	float max_value = network->layers [ network->depth - 1 ].neuron [ 0 ];

	// ADD YOUR CODE HERE. max_value가 최종층
	for (int i = 1; i < network->layers[network->depth - 1].n; i++) {
		if (network->layers[network->depth - 1].neuron[i] > max_value) {
			max_value = network->layers[network->depth - 1].neuron[i];
			a = i;
		}
	}

	return a;
}
inline void Backward(Network* network, int label, float learning_rate) {
	/*
	*	@TODO
	*	error와 Gradient를 계산
	*	최종층에서의 에러는 one-hot vector화 된 결과와 출력 결과를 뺸 값
	*/
	//Calculate last layer's error
	for ( int i = 0; i < network->layers [ network->depth - 1 ].n; i++ ) {
		network->layers [ network->depth - 1 ].error [ i ] = 0.0F - network->layers [ network->depth - 1 ].neuron [ i ];
	}
	network->layers [ network->depth - 1 ].error [ label ] = 1.0F - network->layers [ network->depth - 1 ].neuron [ label ];
	/*
	*	@TODO
	*	역으로 순회하면서 각 레이어의 오차를 계산한다.
	*	이전 레이어의 오차는 현재 레이어의 가중치(전치)와, 현재 레이어의 오차를 이용해 계산한다.
	*	@example
	*	[100][10] 의 전치된 가중치와 [10][1]의 오차를 행렬곱 하여 [100][1] 의 이전 레이어의 오차를 계산할 수 있다.
	*	@other
	*	사실 입력층의 오차는 계산할 필요가 없습니다. 다만 추후에 Convolution을 앞에 붙이면 입력층의 오차가 필요하다.
	*	따라서 입력층의 오차도 미리 계산해 둔다.
	*/

	//Calculate other layer's error
	for ( int i = network->depth - 1; i > 0; i-- ) {
		my_sgemm(CblasRowMajor, CblasTrans, CblasNoTrans  //ORDER
					, network->layers [ i - 1 ].n, 1, network->layers [ i ].n // M N K
					, 1.0F													 // alpha
					, network->layers[i].w, network->layers [i - 1].n		// A, lda
					, network->layers[i].error, 1								// B, ldb
					, 0.0F													// beta
					, network->layers [ i - 1 ].error, 1);					// C, ldc
	}
	/*
	*	@TODO
	*	가중치를 갱신하기 위해서는 Gradient와, 이전 레이어의 출력값이 필요하다.
	*	Gradient를 계산하기 위해서는 현재 레이어의 오차와, 현재 레이어의 뉴런값의 미분값이 필요하다.
	*	단순히 오차와 뉴런의 미분값을 곱하여(스칼라곱) Gradient를 계산할 수 있다.
	*	@Warning
	*	Gradient를 계산할때 오차를 저장하는 변수 error에 덮어쓰기 하지 마시오.
	*	(Convolution에 역전파할때도 마찬가지로 오차만을 전파하기 위함)
	*	@hint
	*	계산된 Gradient 역시 [10][1] 행렬 
	*	[10][1] Gradient와 [1][100]의 이전레이어의 뉴런값을 행렬곱하여 [10][100]의 업데이트될 가중치를 계산할 수 있다.
	*	행렬곱으로 나온 [10][100]에 learning_rate를 곱해 기존 Weight에 더하면 그 것이 가중치의 갱신이다.
	*	@tip
	*	gemm의 alpha와 beta를 사용하여 한번에 learning_rate를 곱해서 더하는 효과를 볼 수 있다.
	*/
	
	//Update weights
	for ( int i = network->depth - 1; i >= 1; i-- ) {
		float* Gradient = ( float* ) calloc(network->layers [ i ].n, sizeof(float));
		for ( int j = 0; j < network->layers [ i ].n; j++ ) {
			//에러와 미분값을 곱하여(스칼라곱) gradient 값을 계산
			Gradient[j] = network->layers[i].error[j] * Sigmoid(network->layers[i].neuron[j])/*ADD YOUR CODE HERE*/;
		}
		my_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans
					, network->layers [ i ].n, network->layers [ i - 1 ].n, 1		// M N K
					, learning_rate/*ADD YOUR CODE HERE*/							// alpha = 스카랄 = learning rate
					, Gradient/*ADD YOUR CODE HERE*/, 1									// A, lda
					, network->layers[i-1].neuron/*ADD YOUR CODE HERE*/, 1									// B, ldb
					, 1.0F															// beta
					, network->layers [ i ].w, network->layers [ i - 1 ].n);        // C, ldc = 레이어 개수
		free(Gradient);
	}
}

#endif

cnn - 네트워크
node로 그물처럼 구성돼서
cnn: 각 레이어의 노드가 wieght를 가짐
weight는 각 수치를 가진 채로 예를 들어 이미지의 특정 중요부분을 강조
wieght가 적당히 들어있어야 output이 잘 나옴
output이 잘 나오려면 --> wieght의 변경 필수(by 학습)
이 학습 --> 원하는 결과 위한 pattern 익히게

how? forward 연산 + backward 연산
이번 실습에서 한 것: 네트워크로 노드가 짜여진 상황에서 ward 2개 연산 함수가
이미 있으니, 여기서 적당한 인자만 넘겨본 것

imagenet - 이미지 label화시킨 1400여만장의 사진 사이트

레이어 뉴런 역전파알고리즘
성능측전 기준 중 loss. 이를 낮추는 게 중요
보폭 - learning rate. network가 이 보폭을 기반으로 증감을 감지, 점차 가장 낮은
곳으로 가려 함
만일 계속 올라가는 데 지치고 그냥 내려갔는데, 실제로 더 낮은 곳이 있다면?
gradient값을 시그마를 넣었지만, 이젠 gradient를 고정된 valuer가 아닌, 가속도와
확률을 넣음(stochastic). 
sigmoid함수의 도함수 역시 사용. 
overpitting(포화상태) : weigth를바꿔 산을 넘어야 하는데, 이를 넘어가지 못함?
머신러닝-이를 넘어가는 게 목표

ImageRecognition (==classification, 분류)
objectDetection - recognition 뒤, ssd란 알고리즘 추가(single shot detection)
분류된 것에 대해, 이미지가 어딨다는 걸 알아내어(by sift), 박스를 치는 알고리즘