# -*- coding: utf-8 -*-


import random

import numpy as np

import tensorflow as tf


random.seed(777)


# 환경 변수 지정


# 입력값 및 타겟값

data = [

    [[0, 0], [0]],

    [[0, 1], [1]],

    [[1, 0], [1]],

    [[1, 1], [0]]

]


# 실행 횟수(iterations), 학습률(lr), 모멘텀 계수(mo) 설정

iterations=100000

lr=0.1

mo=0.9


# 활성화 함수 - 1. 시그모이드

# 미분할 때와 아닐 때의 각각의 값

def sigmoid(x, derivative=False):

    if (derivative == True):

        return x * (1 - x)

    return 1 / (1 + np.exp(-x))


# 활성화 함수 - 2. tanh

# tanh 함수의 미분은 1 - (활성화 함수 출력의 제곱)

def tanh(x, derivative=False):

    if (derivative == True):

        return 1 - x ** 2

    return np.tanh(x)


# 가중치 배열 만드는 함수

def makeMatrix(i, j, fill=0.0):

    mat = []

    for i in range(i):

        mat.append([fill] * j)

    return mat


# 신경망의 실행

class NeuralNetwork:


    # 초깃값의 지정

    def __init__(self, num_x, num_yh, num_yo, bias=1):


        # 입력값(num_x), 은닉층 초깃값(num_yh), 출력층 초깃값(num_yo), 바이어스

        self.num_x = num_x + bias  # 바이어스는 1로 지정(본문 참조)

        self.num_yh = num_yh

        self.num_yo = num_yo


        # 활성화 함수 초깃값

        self.activation_input = [1.0] * self.num_x

        self.activation_hidden = [1.0] * self.num_yh

        self.activation_out = [1.0] * self.num_yo


        # 가중치 입력 초깃값

        self.weight_in = makeMatrix(self.num_x, self.num_yh)

        for i in range(self.num_x):

            for j in range(self.num_yh):

                self.weight_in[i][j] = random.random()


        # 가중치 출력 초깃값

        self.weight_out = makeMatrix(self.num_yh, self.num_yo)

        for j in range(self.num_yh):

            for k in range(self.num_yo):

                self.weight_out[j][k] = random.random()


        # 모멘텀 SGD를 위한 이전 가중치 초깃값

        self.gradient_in = makeMatrix(self.num_x, self.num_yh)

        self.gradient_out = makeMatrix(self.num_yh, self.num_yo)


    # 업데이트 함수

    def update(self, inputs):


        # 입력 레이어의 활성화 함수

        for i in range(self.num_x - 1):

            self.activation_input[i] = inputs[i]


        # 은닉층의 활성화 함수

        for j in range(self.num_yh):

            sum = 0.0

            for i in range(self.num_x):

                sum = sum + self.activation_input[i] * self.weight_in[i][j]

            # 시그모이드와 tanh 중에서 활성화 함수 선택

            self.activation_hidden[j] = sigmoid(sum, False)


        # 출력층의 활성화 함수

        for k in range(self.num_yo):

            sum = 0.0

            for j in range(self.num_yh):

                sum = sum + self.activation_hidden[j] * self.weight_out[j][k]

            # 시그모이드와 tanh 중에서 활성화 함수 선택

            self.activation_out[k] = sigmoid(sum, False)


        return self.activation_out[:]

    

    # 역전파의 실행

    def backPropagate(self, targets):


        # 델타 출력 계산

        output_deltas = [0.0] * self.num_yo

        for k in range(self.num_yo):

            error = targets[k] - self.activation_out[k]

            # 시그모이드와 tanh 중에서 활성화 함수 선택, 미분 적용

            output_deltas[k] = sigmoid(self.activation_out[k], True) * error


        # 은닉 노드의 오차 함수

        hidden_deltas = [0.0] * self.num_yh

        for j in range(self.num_yh):

            error = 0.0

            for k in range(self.num_yo):

                error = error + output_deltas[k] * self.weight_out[j][k]

                # 시그모이드와 tanh 중에서 활성화 함수 선택, 미분 적용

            hidden_deltas[j] = sigmoid(self.activation_hidden[j], True) * error


        # 출력 가중치 업데이트

        for j in range(self.num_yh):

            for k in range(self.num_yo):

                gradient = output_deltas[k] * self.activation_hidden[j]

                v = mo * self.gradient_in[j][k] - lr * gradient

                self.weight_in[j][k] += v

                self.gradient_out[j][k] = gradient


        # 입력 가중치 업데이트

        for i in range(self.num_x):

            for j in range(self.num_yh):

                gradient = hidden_deltas[j] * self.activation_input[i]

                v = mo*self.gradient_in[i][j] - lr * gradient

                self.weight_in[i][j] += v

                self.gradient_in[i][j] = gradient


        # 오차의 계산(최소 제곱법)

        error = 0.0

        for k in range(len(targets)):

            error = error + 0.5 * (targets[k] - self.activation_out[k]) ** 2

        return error


    # 학습 실행

    def train(self, patterns):

        for i in range(iterations):

            error = 0.0

            for p in patterns:

                inputs = p[0]

                targets = p[1]

                self.update(inputs)

                error = error + self.backPropagate(targets)

            if i % 500 == 0:

                print('error: %-.5f' % error)

    # 결괏값 출력

    def result(self, patterns):

        for p in patterns:

            print('Input: %s, Predict: %s' % (p[0], self.update(p[0])))


if __name__ == '__main__':


    # 두 개의 입력 값, 두 개의 레이어, 하나의 출력 값을 갖도록 설정

    n = NeuralNetwork(2, [3,1,2,3], 1)


    # 학습 실행

    n.train(data)


    # 결괏값 출력

    n.result(data)



# Reference: http://arctrix.com/nas/python/bpnn.py (Neil Schemenauer)





이거 풀어줄분;; 문제 한개만이라도;; 감도 안잡힙니다 ..ㅠㅠ