# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load in 

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import cv2
import torch
from torch.nn import functional
from torch.autograd import Variable
from torch.utils.data import Dataset, DataLoader

# Input data files are available in the "../input/" directory.
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# Any results you write to the current directory are saved as output.

train_data_file = "/kaggle/input/digit-recognizer/train.csv"
test_data_file = "/kaggle/input/digit-recognizer/test.csv"


def load_train_data():
    col_list = []
    for idx in range(784):
        col_list.append("pixel" + str(idx))
    data = pd.read_csv(train_data_file, sep=",")
    x_train = np.uint8(data[col_list].values)
    y_train = data["label"].values
    return x_train, y_train


def load_test_data():
    data = pd.read_csv(test_data_file, sep=",")
    return np.uint8(data.values)

def sum_list(li_number):
    sum_number = 0
    for num in li_number:
        sum_number += num
    return sum_number


class TrainDataset(Dataset):
    def __init__(self):
        data_x, data_y = load_train_data()
        self.train_data = data_x
        self.train_label = data_y

    def __getitem__(self, item):
        out_data = np.reshape(self.train_data[item, :], [28, 28])
        ret, out_data = cv2.threshold(out_data, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
        out_data = cv2.bitwise_not(out_data)
        out_data = np.array([out_data / 255.0])
        out_label = np.array(self.train_label[item])
        return Variable(torch.from_numpy(np.float32(out_data))), Variable(torch.from_numpy(out_label))

    def __len__(self):
        return self.train_data.shape[0]


class TestDataset(Dataset):
    def __init__(self):
        data_x = load_test_data()
        self.data = data_x

    def __getitem__(self, item):
        out_data = np.reshape(self.data[item, :], [28, 28])
        ret, out_data = cv2.threshold(out_data, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
        out_data = cv2.bitwise_not(out_data)
        out_data = np.array([out_data / 255.0])
        return Variable(torch.from_numpy(np.float32(out_data)))

    def __len__(self):
        return self.data.shape[0]


class KaggleCNN(torch.nn.Module):
    def __init__(self):
        super(KaggleCNN, self).__init__()
        self.conv_l1 = torch.nn.Sequential(
            # output: 64 * 28 *28
            torch.nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, padding=2),
            torch.nn.BatchNorm2d(num_features=16),
            torch.nn.ReLU(),
            torch.nn.Conv2d(in_channels=16, out_channels=16, kernel_size=5, padding=2),
            torch.nn.BatchNorm2d(num_features=16),
            torch.nn.ReLU(),
            # output: 64 * 14 * 14
            torch.nn.MaxPool2d(kernel_size=2)
        )
        self.conv_l2 = torch.nn.Sequential(
            # output: 128 * 14 * 14
            torch.nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, padding=2),
            torch.nn.BatchNorm2d(num_features=32),
            torch.nn.ReLU(),
            torch.nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, padding=2),
            torch.nn.BatchNorm2d(num_features=32),
            torch.nn.ReLU(),
            # output: 128 * 7 * 7
            torch.nn.MaxPool2d(kernel_size=2)
        )
        self.linear_l1 = torch.nn.Linear(32 * 7 * 7, 10)
        self.log_softmax = torch.nn.LogSoftmax(dim=1)

    def forward(self, x):
        x = self.conv_l1(x)
        x = self.conv_l2(x)
        x = x.view(x.size(0), -1)
        x = self.linear_l1(x)
        x = self.log_softmax(x)
        return x


def build_first_nn(device):
    net = KaggleCNN().to(device)
    return net


def save_result(y, filename=None):
    if filename is None:
        return False
    fid = open(filename, "w")
    fid.writelines("ImageId,Label\n")
    for idx in range(y.shape[0]):
        new_line = "{},{}\n".format(idx+1, y[idx])
        fid.writelines(new_line)
    fid.close()


def main():
    train_dataset = TrainDataset()
    train_loader = DataLoader(dataset=train_dataset, batch_size=2048, shuffle=True)
    if torch.cuda.is_available():
        device = torch.device("cuda")
    else:
        device = torch.device("cpu")
    net = build_first_nn(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
    print(net)

    loss_record = []
    early_stop_count = 0
    for i in range(10000):
        net.train()
        loss_val = 0
        for j, data in enumerate(train_loader):
            train_x, train_y = data
            optimizer.zero_grad()
            output = net(train_x.to(device))
            loss = functional.nll_loss(output, train_y.to(device))
            loss_val += loss.data.cpu()
            # print("{}:{}:{}".format(i, j, loss.data.cpu()))
            loss.backward()
            optimizer.step()
        print("{}:{}".format(i, loss_val))
        if i > 10:
            loss_record.append(loss_val)
            if len(loss_record) > 10:
                del(loss_record[0])
                if loss_val > (sum_list(loss_record) / len(loss_record)):
                    early_stop_count += 1
                    print("Early stop ready: count=%d, current_loss=%.8f, average=%.8f" %
                          (early_stop_count, loss_val, (sum_list(loss_record) / len(loss_record))))
                if early_stop_count > 1 and loss_val < (sum_list(loss_record) / len(loss_record)):
                    print("Early stop done: current_loss=%.8f, average=%.8f" %
                          (loss_val, (sum_list(loss_record) / len(loss_record))))
                    break

    test_dataset = TestDataset()
    test_loader = DataLoader(dataset=test_dataset, batch_size=2048)
    result_y = None
    net.eval()
    for i, data in enumerate(test_loader):
        tensor_x = data.to(device)
        predict_y = np.argmax(net(tensor_x).cpu().detach().numpy(), axis=1)
        if result_y is None:
            result_y = predict_y
        else:
            result_y = np.concatenate((result_y, predict_y), axis=0)

    print("Save result to ./result.csv")
    save_result(result_y, "./result.csv")

main()
