머신러닝 cll - forward backwrad
/*
* 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), 박스를 치는 알고리즘