# AutoGrad 自动求导机制
import pandas as pd
import torch
import torch.nn as nn
import numpy as np
from torch.autograd import Variable
from torch.utils.data import Dataset, DataLoader
from torch import optim
from torchvision import datasets, transforms
from torch.utils.data.dataloader import default_collate
from torchvision.datasets import ImageFolder
from torchvision.datasets.folder import default_loader


x = torch.randn(3, 4, requires_grad = True)
print(x)

b = torch.randn(3, 4, requires_grad = True)
t = x + b
y = t.sum()
print(y)
y.backward()
print(b.grad)
print(x.requires_grad, b.requires_grad, t.requires_grad )

# 做一个线性回归模型
# 定义训练参数
x_values = [i for i in range(11)]
x_train = np.array(x_values, dtype = np.float32)
x_train = x_train.reshape(-1, 1)
print(x_train.shape)
# 定义结果
y_values = [2 * i + 1 for i in x_values]
y_train = np.array(y_values, dtype = np.float32)
y_train = y_train.reshape(-1, 1)
print(y_train.shape)

class LinearRegressionModel(nn.Module) :
    def __init__ (self, input_dim, output_dim) :
        super(LinearRegressionModel, self).__init__()
        self.linear = nn.Linear(input_dim, output_dim)

    def forward (self, x) :
        out = self.linear(x)
        return out

input_dim = 1
output_dim = 1
model = LinearRegressionModel(input_dim, output_dim)
print(model)

# 指定参数和损失函数
epochs = 1000
learning_rate = 0.01
optimizer = torch.optim.SGD(model.parameters(), lr = learning_rate)
criterion = nn.MSELoss()

# 训练模型
for epoch in range(epochs) :
    epoch += 1
    # 转换成 tensor
    inputs = torch.from_numpy(x_train)
    labels = torch.from_numpy(y_train)

    # 每一次迭代梯度要清零
    optimizer.zero_grad()

    # 前向传播
    outputs = model(inputs)

    # 计算损失
    loss = criterion(outputs, labels)

    # 反向传播
    loss.backward()

    # 更新权重参数
    optimizer.step()

    print('epoch {}, loss {}'.format(epoch, loss.item()))

# 测试模型预测结果
predicted = model(torch.from_numpy(x_train).requires_grad_()).data.numpy()
print(predicted)

# 模型保存与读取
torch.save(model.state_dict(), 'model.pkl')  # 保存的是 w 和 b
model.load_state_dict(torch.load('model.pkl'))

# tensor 常见形式
# scalar  vector  matrix  n-dimensional tenson

# hub 模块 包括所有的现成模型

'''
PyTorch 的 torch.nn 包提供了很多与实现神经网络中的具体功能相关的类,
这些类涵盖了深度神经网络模型在搭建和参数优化过程中的常用内容。

troch.nn.Sequential  是 torch.nn 中的一种序列容器,通过在容器中嵌套各种实现神经网络中具体功能相关的
类,来完成对神经网络模型的搭建,最主要的是,参数会按照我们定义好的序列自动喘息下去。
模块的加入一般由两种方式,一种是在代码中直接嵌套,另一种是以  orderdict  有序字典
的方式进行传入

第一种
hidden_layer = 100
input_data = 1000
outout_data = 10

models = torch.nn.Sequential (
    torch.nn.Linear(input_data, hidden_layer),
    torch.nn.ReLU(),
    torch.nn.Linear(hidden_layer, output_data)
)

第二种
使用 orderdict 有序字典进行传入来搭建的模型

from collections import OrderedDict
modles = torch.nn.Sequential(OrderDict([
    ("Linel", torch.nn.Linear(input_data, hidden_layer)),
    ("ReLU", torch.nn.ReLU()),
    ("Line2", torch.nn.Linear(hidden_layer, output_data))
]))

torch.nn.Linear  用于定义模型的线性层,即完成不同层之间的线性转换,
参数为3, 输入特征束,输出特征束,是否使用偏置

torch.nn.ReLU  属于非线性激活函数,在定义时默认不传入参数
可以选择的有  PReLU  LeakyReLU  Tanh  Sigmoid   Softmax

torch.nn.MSFLoss  使用均方误差函数对损失值进行计算
torch.nn.L1Loss   使用平均绝对无法函数对损失值进行计算
torch.nn.CrossEntropyLoss    计算交叉熵

更新参数
for param in models.paramters() :
    param.data -= param.grad.data * learning_rate
    
    
在 PyTorch 的 troch.optim 包中提供了非常多的可实现参数自动优化的类
比如 SGD  AdaGrad  RMSProp  Adam
optimizer = torch.optim.Adam(models.parameters()m lr = learning_rate)

optimzer.zero_grad 来完成对模型参数梯度的归零
optimzer.step  主要功能是使用计算得到的梯度值对各个节点的参数进行梯度更新
'''

'''
Variable  和  Tensor 本质上没有区别,不过 Variable  会被放入一个计算图中,然后进行
前向传播、反向传播、自动求导
torch.autograd.Variable 
Variable(a)
'''

'''
torch.utils.data.Dataset 是代表这一数据的抽象类,可以定义数据类来继承和重写这个抽象类
只需要定义 __len__ 和 __getitem__ 两个函数
'''
class myDataset(Dataset) :
    def __init__(self, csv_file, txt_file, root_dir, other_file):
        self.csv_data = pd.read_csv(csv_file)
        with open(txt_file, 'r') as f:
            data_list = f.readline()
        self.txt_data = data_list
        self.root_dir = root_dir

    def __len__(self):
        return len(self.csv_data)

    def __getitem__(self, idx):
        data = (self.csv_data[idx], self.txt_data[idx])
        return data

# 使用多个线程去读取数据
# torch.utils.data.DataLoader 定义一个新的迭代器
# dataiter = DataLoader(myDataset, bach_size = 32, shuffle = True, collate_fn = default_collate)

# 计算机视觉的数据读取类 ImageFolder
# dset = ImageFolder(root = 'root_path', transform = None, loader = default_loader)


'''
PyTorch 里边编写的神经网络所有的层结构和损失函数都来自 torch.nn
所有模型都继承子 nn.Module
'''

'''
使用 gpu 训练模型
if torch.cuda.is_available():
    model = LinearRegression().cuda()
else:
    model - LinearRegression()    
'''

# 测试结果
# model.eval()  将模型变成测试模式

# torch.cat() 实现 Tensor 的拼装
# torch.squeeze 对数据的维度记性压缩
# torch.unsqueeze 对数据维度进行扩充,在指定维度加上维数

def make_features(x):
    x = x.unsqueeze(1)
    return torch.cat([x ** i for i in range(1, 4)], 1)

W_target = torch.FloatTensor([0.5, 3, 2.4]).unsqueeze(1)
b_target = torch.FloatTensor([0.9])
print('w target : {}, \nb_target : {}'.format(W_target, b_target))

def f(x):
    return x.mm(W_target) + b_target[0]

# 随机得到一些数作为训练集
def get_batch(batch_size = 32):
    random = torch.randn(batch_size)
    x = make_features(random)
    # print('\nx : {} \n shape {}'.format(x, x.shape))
    y = f(x)
    return Variable(x), Variable(y)

print('\nget batch : {}'.format(get_batch()))

# Define model
class poly_model(nn.Module):
    def __init__(self):
        super(poly_model, self).__init__()
        self.poly = nn.Linear(3, 1)

    def forward(self, x):
        out = self.poly(x)
        return out

model = poly_model()

criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 1e-3)

epoch = 0
while True:
    batch_x, batch_y = get_batch()
    output = model(batch_x)
    loss = criterion(output, batch_y)
    print_loss = loss.item()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    epoch += 1
    if print_loss < 1e-3:
        break


# nn.BCELoss 是二分类的损失函数
# 分类的准确性通过 结果 大于 0.5 或小于 0.5

# 多层全连接神经网络实现 MNIST 手写数字分类

class simpleNet(nn.Module):
    def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
        super(simpleNet, self).__init__()
        self.layer1 = nn.Linear(in_dim, n_hidden_1)
        self.layer2 = nn.Linear(n_hidden_1, n_hidden_2)
        self.layer3 = nn.Linear(n_hidden_2, out_dim)

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x

# 添加激活函数
# 改进网络,添加激活函数增加网络的非线性
class Activation_Net(nn.Module):
    def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
        super(Activation_Net, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Linear(in_dim, n_hidden_1),
            nn.ReLU(True)
        )
        self.layer2 = nn.Sequential(
            nn.Linear(n_hidden_1, n_hidden_2),
            nn.ReLU(True)
        )
        self.layer3 = nn.Sequential(
            nn.Linear(n_hidden_2, out_dim)
        )

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x

# 添加批标准化
# 最后一个加快收敛速度的方法 ---- 批标准化
class Batch_Net(nn.Module):
    def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
        super(Batch_Net, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Linear(in_dim, n_hidden_1),
            nn.BatchNorm1d(n_hidden_1),
            nn.ReLU(True)
        )
        self.layer2 = nn.Sequential(
            nn.Linear(n_hidden_1, n_hidden_2),
            nn.BatchNorm1d(n_hidden_2),
            nn.ReLU(True)
        )
        self.layer3 = nn.Sequential(
            nn.Linear(n_hidden_2, out_dim)
        )

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x
# 批标准化一般放在全连接层的后面,非线性(激活函数)的前面

# 训练网络
# MNIST 数据集是一个手写字体数据集,包含 0 - 9 这 10 个数字,
# 其中 55000 张训练集,10000 张测试集,5000 张验证集,
# 图片大小是 28 x 28 的灰度图

# 定义一些超参数
# 超参数 Hyperparmeters
batch_size = 64
learning_rate = 1e-3
num_epoches = 20

# 数据预处理,
# torchvision.transforms 提供了很多图片预处理的方法,
# transform.ToTensor()  将图片转换成 PyTorch 中处理的对象 Tensor,在转换过程自动将图片标准化,
#   也就是说 Tensor 的范围是 0 - 1
# transforms.Normalize()  需要传入两个参数,第一个参数是均值,第二个参数是方法,
#   做的处理就是 减均值,再除以方差,用均值和标准差归一化张量图像,将 0 - 1 变换成 -1 - 1
# trochvision.transforms 是 pytorch 中的图像预处理包,一般用 Compose 把多个步骤
#   整合到一起
# 图片是灰度图,所以只有一个通道,如果是彩色图像,有三个通道,
# transforms.Normalize([a, b, c], [d, e, f]) 来表示每个通道对应的均值和方法

data_tf = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize([0.5], [0.5])] # image = (image - mean) / std  -->  -1 ~ 1
)

# 读取数据集
# 下载训练集 MNIST 手写数字训练集
# 通过 PyTorch 的内置函数 torchvision.datasets.MNIST 导入数据集,传入数据预处理,
# 接着使用 torch.utils.data.DataLoader 建立一个数据迭代器,传入数据集和 batch_size,
# 通过 shuffle = True 来表示每次迭代数据的时候是否将数据打乱
train_dataset = datasets.MNIST(
    root = './data',
    train = True,
    transform = data_tf,
    download = True
)
test_dataset = datasets.MNIST(
    root = './data',
    train = False,
    transform = data_tf
)
train_loader = DataLoader(train_dataset, batch_size = batch_size, shuffle = True)
test_loader = DataLoader(test_dataset, batch_size = batch_size, shuffle = False)

# 导入网络,定义损失函数和优化方法
# model = simpleNet(28 * 28, 300, 100, 10)
# model = Activation_Net(28 * 28, 300, 100, 10)
model = Batch_Net(28 * 28, 3000, 1000, 10)
if torch.cuda.is_available():
    model = model.cuda()

criterion = nn.CrossEntropyLoss()  # 交叉熵损失函数
optimizer = optim.SGD(model.parameters(), lr = learning_rate)

# 训练网络
# for epoch in range(epochs):
#     epoch += 1


# 训练模型
for epoch in range(num_epoches) :
    epoch += 1
    for train_data in train_loader:
        img, label = train_data
        img = img.view(img.size(0), -1)
        if torch.cuda.is_available():
            img = Variable(img).cuda()
            label = Variable(label).cuda()
        else:
            img = Variable(img)
            label = Variable(label)

        optimizer.zero_grad()
        outputs = model(img)
        loss = criterion(outputs, label)
        loss.backward()
        optimizer.step()

    print('epoch {}, loss {}'.format(epoch, loss.item()))


# model.eval()
# eval_loss = 0
# eval_ace = 0
# for data in test_loader:
#     img, label = data
#     img = img.view(img.size(0), -1)
#     if torch.cuda.is_available() :
# 工口可 = Tariable (img, volatile=True) . cuda ()
# label = Variable (label, volatile=True) .cuda ()
# 10 else:
# 11 img = Variabel (iπ volatile=True)
# 12 label =飞Tariable (label, volat le=True)
# 13 out = model (img)
# 14 loss = criterion(out, label)
# 15 eval loss += loss.data[O] * label.size(O)
# 16 _, pred = torch.max(out, 1)
# 11 num_correct = (pred == label) .sum()
# 1s eval ace += num correct.data[O]
# 19 print (’Test Loss: {: 6f} , Ace: {: .6 剖’. forrr t(
# 20 eval loss I (len (test dataset)) ,
# 21 val ace I (len(test dataset))))