Pytorch教程

Pytorch 神經(jīng)網(wǎng)絡(luò)基礎(chǔ)

1.1 Pytorch & Numpy

1.1.1 用Torch還是Numpy

Torch 自稱為神經(jīng)網(wǎng)絡(luò)界的 Numpy, 因?yàn)樗軐?torch 產(chǎn)生的 tensor 放在 GPU 中加速運(yùn)算 (前提是你有合適的 GPU), 就像 Numpy 會(huì)把 array 放在 CPU 中加速運(yùn)算. 所以神經(jīng)網(wǎng)絡(luò)的話, 當(dāng)然是用 Torch 的 tensor 形式數(shù)據(jù)最好咯. 就像 Tensorflow 當(dāng)中的 tensor 一樣.

當(dāng)然, 我們對(duì) Numpy 還是愛不釋手的, 因?yàn)槲覀兲?xí)慣 numpy 的形式了. 不過 torch 看出來我們的喜愛, 他把 torch 做的和 numpy 能很好的兼容. 比如這樣就能自由地轉(zhuǎn)換 numpy array 和 torch tensor 了:
numpy array轉(zhuǎn)換成 torch tensor:torch.from_numpy(np_data)
torch tensor轉(zhuǎn)換成 numpy array:torch_data.numpy()

import torch
import numpy as np

np_data = np.arange(6).reshape((2, 3))
torch_data = torch.from_numpy(np_data)
tensor2array = torch_data.numpy()
print(
    '\nnumpy array:', np_data,          # [[0 1 2], [3 4 5]]
    '\ntorch tensor:', torch_data,      #  0  1  2 \n 3  4  5    [torch.LongTensor of size 2x3]
    '\ntensor to array:', tensor2array, # [[0 1 2], [3 4 5]]
)

1.1.2 Torch 中的數(shù)學(xué)運(yùn)算

其實(shí) torch 中 tensor 的運(yùn)算和 numpy array 的如出一轍, 我們就以對(duì)比的形式來看. 如果想了解 torch 中其它更多有用的運(yùn)算符, 可以參考Pytorch中文手冊(cè).

  • abs 絕對(duì)值計(jì)算
data = [-1, -2, 1, 2]
tensor = torch.FloatTensor(data)  # 轉(zhuǎn)換成32位浮點(diǎn) tensor
print(
    '\nabs',
    '\nnumpy: ', np.abs(data),          # [1 2 1 2]
    '\ntorch: ', torch.abs(tensor)      # [1 2 1 2]
)
  • sin 三角函數(shù)
print(
    '\nsin',
    '\nnumpy: ', np.sin(data),      # [-0.84147098 -0.90929743  0.84147098  0.90929743]
    '\ntorch: ', torch.sin(tensor)  # [-0.8415 -0.9093  0.8415  0.9093]
)
  • mean 均值
print(
    '\nmean',
    '\nnumpy: ', np.mean(data),         # 0.0
    '\ntorch: ', torch.mean(tensor)     # 0.0
)

除了簡(jiǎn)單的計(jì)算, 矩陣運(yùn)算才是神經(jīng)網(wǎng)絡(luò)中最重要的部分. 所以我們展示下矩陣的乘法. 注意一下包含了一個(gè) numpy 中可行, 但是 torch 中不可行的方式.

  • matrix multiplication 矩陣點(diǎn)乘
# matrix multiplication 矩陣點(diǎn)乘
data = [[1,2], [3,4]]
tensor = torch.FloatTensor(data)  # 轉(zhuǎn)換成32位浮點(diǎn) tensor
# correct method
print(
    '\nmatrix multiplication (matmul)',
    '\nnumpy: ', np.matmul(data, data),     # [[7, 10], [15, 22]]
    '\ntorch: ', torch.mm(tensor, tensor)   # [[7, 10], [15, 22]]
)

# !!!!  下面是錯(cuò)誤的方法 !!!!
data = np.array(data)
print(
    '\nmatrix multiplication (dot)',
    '\nnumpy: ', data.dot(data),        # [[7, 10], [15, 22]] 在numpy 中可行
    '\ntorch: ', tensor.dot(tensor)     # torch.dot只能處理一維數(shù)組
)

1.2 變量 Variable

1.2.1 什么是Variable

在 Torch 中的 Variable 就是一個(gè)存放會(huì)變化的值的地理位置. 里面的值會(huì)不停的變化. 就像一個(gè)裝雞蛋的籃子, 雞蛋數(shù)會(huì)不停變動(dòng). 那誰是里面的雞蛋呢, 自然就是 Torch 的 Tensor 咯. 如果用一個(gè) Variable 進(jìn)行計(jì)算, 那返回的也是一個(gè)同類型的 Variable.

我們定義一個(gè) Variable:

import torch
from torch.autograd import Variable # torch 中 Variable 模塊

# 先生雞蛋
tensor = torch.FloatTensor([[1,2],[3,4]])
# 把雞蛋放到籃子里, requires_grad是參不參與誤差反向傳播, 要不要計(jì)算梯度
variable = Variable(tensor, requires_grad=True)

print(tensor)
"""
 1  2
 3  4
[torch.FloatTensor of size 2x2]
"""

print(variable)
"""
Variable containing:
 1  2
 3  4
[torch.FloatTensor of size 2x2]
"""

1.2.2 Variable 計(jì)算 梯度

我們?cè)賹?duì)比一下 tensor 的計(jì)算和 variable 的計(jì)算.

t_out = torch.mean(tensor*tensor)       # x^2
v_out = torch.mean(variable*variable)   # x^2
print(t_out)
print(v_out)    # 7.5

到目前為止, 我們看不出什么不同, 但是時(shí)刻記住, Variable 計(jì)算時(shí), 它在背景幕布后面一步步默默地搭建著一個(gè)龐大的系統(tǒng), 叫做計(jì)算圖(computational graph). 這個(gè)圖是用來干嘛的? 原來是將所有的計(jì)算步驟 (節(jié)點(diǎn)) 都連接起來, 最后進(jìn)行誤差反向傳遞的時(shí)候, 一次性將所有 variable 里面的修改幅度 (梯度) 都計(jì)算出來, 而 tensor 就沒有這個(gè)能力啦.

v_out = torch.mean(variable*variable) 就是在計(jì)算圖中添加的一個(gè)計(jì)算步驟, 計(jì)算誤差反向傳遞的時(shí)候有他一份功勞, 我們就來舉個(gè)例子:

v_out.backward()    # 模擬 v_out 的誤差反向傳遞

# 下面兩步看不懂沒關(guān)系, 只要知道 Variable 是計(jì)算圖的一部分, 可以用來傳遞誤差就好.
# v_out = 1/4 * sum(variable*variable) 這是計(jì)算圖中的 v_out 計(jì)算步驟
# 針對(duì)于 v_out 的梯度就是, d(v_out)/d(variable) = 1/4*2*variable = variable/2

print(variable.grad)    # 初始 Variable 的梯度
'''
 0.5000  1.0000
 1.5000  2.0000
'''

1.2.3 獲取 Variable 里面的數(shù)據(jù)

直接print(variable)只會(huì)輸出 Variable 形式的數(shù)據(jù), 在很多時(shí)候是用不了的(比如想要用 plt 畫圖), 所以我們要轉(zhuǎn)換一下, 將它變成 tensor 形式.

print(variable)     #  Variable 形式
"""
Variable containing:
 1  2
 3  4
[torch.FloatTensor of size 2x2]
"""

print(variable.data)    # tensor 形式
"""
 1  2
 3  4
[torch.FloatTensor of size 2x2]
"""

print(variable.data.numpy())    # numpy 形式
"""
[[ 1.  2.]
 [ 3.  4.]]
"""

1.3 Torch中的激勵(lì)函數(shù)

Torch 中的激勵(lì)函數(shù)有很多, 不過我們平時(shí)要用到的就這幾個(gè). relu, sigmoid, tanh, softplus. 那我們就看看他們各自長(zhǎng)什么樣啦.

import torch
import torch.nn.functional as F     # 激勵(lì)函數(shù)都在這
from torch.autograd import Variable

# 做一些假數(shù)據(jù)來觀看圖像
x = torch.linspace(-5, 5, 200)  # x data (tensor), shape=(100, 1)
x = Variable(x)

接著就是做生成不同的激勵(lì)函數(shù)數(shù)據(jù):

x_np = x.data.numpy()   # 換成 numpy array, 出圖時(shí)用

# 幾種常用的 激勵(lì)函數(shù)
y_relu = F.relu(x).data.numpy()
y_sigmoid = F.sigmoid(x).data.numpy()
y_tanh = F.tanh(x).data.numpy()
y_softplus = F.softplus(x).data.numpy()
# y_softmax = F.softmax(x)  softmax 比較特殊, 不能直接顯示, 不過他是關(guān)于概率的, 用于分類

接著我們開始畫圖, 畫圖的代碼也在下面:
教程
import matplotlib.pyplot as plt 

plt.figure(1, figsize=(8, 6))
plt.subplot(221)
plt.plot(x_np, y_relu, c='red', label='relu')
plt.ylim((-1, 5))
plt.legend(loc='best')

plt.subplot(222)
plt.plot(x_np, y_sigmoid, c='red', label='sigmoid')
plt.ylim((-0.2, 1.2))
plt.legend(loc='best')

plt.subplot(223)
plt.plot(x_np, y_tanh, c='red', label='tanh')
plt.ylim((-1.2, 1.2))
plt.legend(loc='best')

plt.subplot(224)
plt.plot(x_np, y_softplus, c='red', label='softplus')
plt.ylim((-0.2, 6))
plt.legend(loc='best')

plt.show()

建造第一個(gè)神經(jīng)網(wǎng)絡(luò)

2.1 關(guān)系擬合

本節(jié)會(huì)來見證神經(jīng)網(wǎng)絡(luò)是如何通過簡(jiǎn)單的形式將一群數(shù)據(jù)用一條線條來表示. 或者說, 是如何在數(shù)據(jù)當(dāng)中找到他們的關(guān)系, 然后用神經(jīng)網(wǎng)絡(luò)模型來建立一個(gè)可以代表他們關(guān)系的線條.

2.1.1 建立數(shù)據(jù)集

我們創(chuàng)建一些假數(shù)據(jù)來模擬真實(shí)的情況. 比如一個(gè)一元二次函數(shù): y = a * x^2 + b, 我們給 y 數(shù)據(jù)加上一點(diǎn)噪聲來更加真實(shí)的展示它.

import torch
from torch.autograd import Variable
import matplotlib.pyplot as plt

x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1)  # x data (tensor), shape=(100, 1)
y = x.pow(2) + 0.2*torch.rand(x.size())                 # noisy y data (tensor), shape=(100, 1)

# 用 Variable 來修飾這些數(shù)據(jù) tensor
x, y = torch.autograd.Variable(x), Variable(y)

# 畫圖
plt.scatter(x.data.numpy(), y.data.numpy())
plt.show()

2.1.2 建立神經(jīng)網(wǎng)絡(luò)

建立一個(gè)神經(jīng)網(wǎng)絡(luò)我們可以直接運(yùn)用 torch 中的體系. 先定義所有的層屬性(__init__()), 然后再一層層搭建(forward(x))層于層的關(guān)系鏈接. 建立關(guān)系的時(shí)候, 我們會(huì)用到激勵(lì)函數(shù).

import torch
import torch.nn.functional as F     # 激勵(lì)函數(shù)都在這

class Net(torch.nn.Module):  # 繼承 torch 的 Module
    def __init__(self, n_feature, n_hidden, n_output):
        super(Net, self).__init__()     # 繼承 __init__ 功能
        # 定義每層用什么樣的形式
        self.hidden = torch.nn.Linear(n_feature, n_hidden)   # 隱藏層線性輸出
        self.predict = torch.nn.Linear(n_hidden, n_output)   # 輸出層線性輸出

    def forward(self, x):   # 這同時(shí)也是 Module 中的 forward 功能
        # 正向傳播輸入值, 神經(jīng)網(wǎng)絡(luò)分析出輸出值
        x = F.relu(self.hidden(x))      # 激勵(lì)函數(shù)(隱藏層的線性值)
        x = self.predict(x)             # 輸出值
        return x

net = Net(n_feature=1, n_hidden=10, n_output=1)

print(net)  # net 的結(jié)構(gòu)
"""
Net (
  (hidden): Linear (1 -> 10)
  (predict): Linear (10 -> 1)
)

2.1.3 訓(xùn)練網(wǎng)絡(luò)

訓(xùn)練的步驟很簡(jiǎn)單, 如下:

# optimizer 是訓(xùn)練的工具
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)  # 傳入 net 的所有參數(shù), 學(xué)習(xí)率
loss_func = torch.nn.MSELoss()      # 預(yù)測(cè)值和真實(shí)值的誤差計(jì)算公式 (均方差)

for t in range(100):
    prediction = net(x)     # 喂給 net 訓(xùn)練數(shù)據(jù) x, 輸出預(yù)測(cè)值

    loss = loss_func(prediction, y)     # 計(jì)算兩者的誤差

    optimizer.zero_grad()   # 清空上一步的殘余更新參數(shù)值
    loss.backward()         # 誤差反向傳播, 計(jì)算參數(shù)更新值
    optimizer.step()        # 將參數(shù)更新值施加到 net 的 parameters 上

2.1.4 可視化訓(xùn)練過程

為了可視化整個(gè)訓(xùn)練的過程, 更好的理解是如何訓(xùn)練, 我們?nèi)缦虏僮?

import matplotlib.pyplot as plt

plt.ion()   # 畫圖
plt.show()

for t in range(100):

    ...
    loss.backward()
    optimizer.step()

    # 接著上面來
    if t % 5 == 0:
        # plot and show learning process
        plt.cla()
        plt.scatter(x.data.numpy(), y.data.numpy())
        plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
        plt.text(0.5, 0, 'Loss=%.4f' % loss.data[0], fontdict={'size': 20, 'color':  'red'})
        plt.pause(0.1)

可視化效果如下:
image

2.2 區(qū)分類型(分類)

這次我們也是用最簡(jiǎn)單的途徑來看看神經(jīng)網(wǎng)絡(luò)是怎么進(jìn)行事物的分類.

2.2.1 建立數(shù)據(jù)集

我們創(chuàng)建一些假數(shù)據(jù)來模擬真實(shí)的情況. 比如兩個(gè)二項(xiàng)分布的數(shù)據(jù), 不過他們的均值都不一樣.

import torch
from torch.autograd import Variable
import matplotlib.pyplot as plt

# 假數(shù)據(jù)
n_data = torch.ones(100, 2)         # 數(shù)據(jù)的基本形態(tài)
x0 = torch.normal(2*n_data, 1)      # 類型0 x data (tensor), shape=(100, 2)
y0 = torch.zeros(100)               # 類型0 y data (tensor), shape=(100, 1)
x1 = torch.normal(-2*n_data, 1)     # 類型1 x data (tensor), shape=(100, 1)
y1 = torch.ones(100)                # 類型1 y data (tensor), shape=(100, 1)

# 注意 x, y 數(shù)據(jù)的數(shù)據(jù)形式是一定要像下面一樣 (torch.cat 是在合并數(shù)據(jù))
x = torch.cat((x0, x1), 0).type(torch.FloatTensor)  # FloatTensor = 32-bit floating
y = torch.cat((y0, y1), ).type(torch.LongTensor)    # LongTensor = 64-bit integer

# torch 只能在 Variable 上訓(xùn)練, 所以把它們變成 Variable
x, y = Variable(x), Variable(y)

# 畫圖
plt.scatter(x.data.numpy(), y.data.numpy())
plt.show()

2.2.2 建立神經(jīng)網(wǎng)絡(luò)

建立一個(gè)神經(jīng)網(wǎng)絡(luò)我們可以直接運(yùn)用 torch 中的體系. 先定義所有的層屬性(init()), 然后再一層層搭建(forward(x))層于層的關(guān)系鏈接. 這個(gè)和我們?cè)谇懊?regression 的時(shí)候的神經(jīng)網(wǎng)絡(luò)基本沒差. 建立關(guān)系的時(shí)候, 我們會(huì)用到激勵(lì)函數(shù)霎肯。

import torch
import torch.nn.functional as F     # 激勵(lì)函數(shù)都在這

class Net(torch.nn.Module):     # 繼承 torch 的 Module
    def __init__(self, n_feature, n_hidden, n_output):
        super(Net, self).__init__()     # 繼承 __init__ 功能
        self.hidden = torch.nn.Linear(n_feature, n_hidden)   # 隱藏層線性輸出
        self.out = torch.nn.Linear(n_hidden, n_output)       # 輸出層線性輸出

    def forward(self, x):
        # 正向傳播輸入值, 神經(jīng)網(wǎng)絡(luò)分析出輸出值
        x = F.relu(self.hidden(x))      # 激勵(lì)函數(shù)(隱藏層的線性值)
        x = self.out(x)                 # 輸出值, 但是這個(gè)不是預(yù)測(cè)值, 預(yù)測(cè)值還需要再另外計(jì)算
        return x

net = Net(n_feature=2, n_hidden=10, n_output=2) # 幾個(gè)類別就幾個(gè) output

print(net)  # net 的結(jié)構(gòu)
"""
Net (
  (hidden): Linear (2 -> 10)
  (out): Linear (10 -> 2)
)
"""

2.2.3 訓(xùn)練網(wǎng)絡(luò)

訓(xùn)練的步驟很簡(jiǎn)單, 如下:

# optimizer 是訓(xùn)練的工具
optimizer = torch.optim.SGD(net.parameters(), lr=0.02)  # 傳入 net 的所有參數(shù), 學(xué)習(xí)率
# 算誤差的時(shí)候, 注意真實(shí)值!不是! one-hot 形式的, 而是1D Tensor, (batch,)
# 但是預(yù)測(cè)值是2D tensor (batch, n_classes)
loss_func = torch.nn.CrossEntropyLoss()

for t in range(100):
    out = net(x)     # 喂給 net 訓(xùn)練數(shù)據(jù) x, 輸出分析值

    loss = loss_func(out, y)     # 計(jì)算兩者的誤差

    optimizer.zero_grad()   # 清空上一步的殘余更新參數(shù)值
    loss.backward()         # 誤差反向傳播, 計(jì)算參數(shù)更新值
    optimizer.step()        # 將參數(shù)更新值施加到 net 的 parameters 上

2.2.4 可視化訓(xùn)練過程

為了可視化整個(gè)訓(xùn)練的過程, 更好的理解是如何訓(xùn)練, 我們?nèi)缦虏僮?

import matplotlib.pyplot as plt

plt.ion()   # 畫圖
plt.show()

for t in range(100):

    ...
    loss.backward()
    optimizer.step()

    # 接著上面來
    if t % 2 == 0:
        plt.cla()
        # 過了一道 softmax 的激勵(lì)函數(shù)后的最大概率才是預(yù)測(cè)值
        prediction = torch.max(F.softmax(out, dim=1), 1)[1]
        pred_y = prediction.data.numpy().squeeze()
        target_y = y.data.numpy()
        plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=pred_y, s=100, lw=0, cmap='RdYlGn')
        accuracy = sum(pred_y == target_y)/200  # 預(yù)測(cè)中有多少和真實(shí)值一樣
        plt.text(1.5, -4, 'Accuracy=%.2f' % accuracy, fontdict={'size': 20, 'color':  'red'})
        plt.pause(0.1)

plt.ioff()  # 停止畫圖
plt.show()

可視化效果如下:
image

2.3 快速搭建法

Torch 中提供了很多方便的途徑, 同樣是神經(jīng)網(wǎng)絡(luò), 能快則快, 我們看看如何用更簡(jiǎn)單的方式搭建同樣的回歸神經(jīng)網(wǎng)絡(luò)。

2.3.1 快速搭建

我們先看看之前寫神經(jīng)網(wǎng)絡(luò)時(shí)用到的步驟. 我們用 net1 代表這種方式搭建的神經(jīng)網(wǎng)絡(luò).

class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden, n_output):
        super(Net, self).__init__()
        self.hidden = torch.nn.Linear(n_feature, n_hidden)
        self.predict = torch.nn.Linear(n_hidden, n_output)

    def forward(self, x):
        x = F.relu(self.hidden(x))
        x = self.predict(x)
        return x

net1 = Net(1, 10, 1)   # 這是我們用這種方式搭建的 net1

我們用 class 繼承了一個(gè) torch 中的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu), 然后對(duì)其進(jìn)行了修改, 不過還有更快的一招, 用一句話就概括了上面所有的內(nèi)容!

net2 = torch.nn.Sequential(
    torch.nn.Linear(1, 10),
    torch.nn.ReLU(),
    torch.nn.Linear(10, 1)
)

我們?cè)賹?duì)比一下兩者的結(jié)構(gòu):

print(net1)
"""
Net(
  (hidden): Linear(in_features=1, out_features=10, bias=True)
  (predict): Linear(in_features=10, out_features=1, bias=True)
)
"""
print(net2)
"""
Sequential(
  (0): Linear(in_features=1, out_features=10, bias=True)
  (1): ReLU()
  (2): Linear(in_features=10, out_features=1, bias=True)
)
"""

我們會(huì)發(fā)現(xiàn) net2 多顯示了一些內(nèi)容, 這是為什么呢? 原來他把激勵(lì)函數(shù)也一同納入進(jìn)去了, 但是 net1 中, 激勵(lì)函數(shù)實(shí)際上是在 forward() 功能中才被調(diào)用的. 這也就說明了, 相比 net2, net1 的好處就是, 你可以根據(jù)你的個(gè)人需要更加個(gè)性化你自己的前向傳播過程, 比如(RNN). 不過如果你不需要七七八八的過程, 相信 net2 這種形式更適合你.

2.4 保存提取

訓(xùn)練好了一個(gè)模型, 我們當(dāng)然想要保存它, 留到下次要用的時(shí)候直接提取直接用, 這就是這節(jié)的內(nèi)容啦. 我們用回歸的神經(jīng)網(wǎng)絡(luò)舉例實(shí)現(xiàn)保存提取.

2.4.1 保存

我們快速地建造數(shù)據(jù), 搭建網(wǎng)絡(luò):

torch.manual_seed(1)    # reproducible

# 假數(shù)據(jù)
x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1)  # x data (tensor), shape=(100, 1)
y = x.pow(2) + 0.2*torch.rand(x.size())  # noisy y data (tensor), shape=(100, 1)
x, y = Variable(x, requires_grad=False), Variable(y, requires_grad=False)


def save():
    # 建網(wǎng)絡(luò)
    net1 = torch.nn.Sequential(
        torch.nn.Linear(1, 10),
        torch.nn.ReLU(),
        torch.nn.Linear(10, 1)
    )
    optimizer = torch.optim.SGD(net1.parameters(), lr=0.5)
    loss_func = torch.nn.MSELoss()

    # 訓(xùn)練
    for t in range(100):
        prediction = net1(x)
        loss = loss_func(prediction, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

接下來我們有兩種途徑來保存:

torch.save(net1, 'net.pkl')  # 保存整個(gè)網(wǎng)絡(luò)
torch.save(net1.state_dict(), 'net_params.pkl')   # 只保存網(wǎng)絡(luò)中的參數(shù) (速度快, 占內(nèi)存少)

2.4.2 提取網(wǎng)絡(luò)

這種方式將會(huì)提取整個(gè)神經(jīng)網(wǎng)絡(luò), 網(wǎng)絡(luò)大的時(shí)候可能會(huì)比較慢.

def restore_net():
    # restore entire net1 to net2
    net2 = torch.load('net.pkl')
    prediction = net2(x)

2.4.3 只提取網(wǎng)絡(luò)參數(shù)

這種方式將會(huì)提取所有的參數(shù), 然后再放到你的新建網(wǎng)絡(luò)中.

def restore_params():
    # 新建 net3
    net3 = torch.nn.Sequential(
        torch.nn.Linear(1, 10),
        torch.nn.ReLU(),
        torch.nn.Linear(10, 1)
    )

    # 將保存的參數(shù)復(fù)制到 net3
    net3.load_state_dict(torch.load('net_params.pkl'))
    prediction = net3(x)

2.4.4 顯示結(jié)果

調(diào)用上面建立的幾個(gè)功能, 然后出圖.

# 保存 net1 (1. 整個(gè)網(wǎng)絡(luò), 2. 只有參數(shù))
save()

# 提取整個(gè)網(wǎng)絡(luò)
restore_net()

# 提取網(wǎng)絡(luò)參數(shù), 復(fù)制到新網(wǎng)絡(luò)
restore_params()
image

這樣我們就能看出三個(gè)網(wǎng)絡(luò)完全一模一樣啦榛斯!

2.5 批訓(xùn)練

Torch 中提供了一種幫你整理你的數(shù)據(jù)結(jié)構(gòu)的好東西, 叫做 DataLoader, 我們能用它來包裝自己的數(shù)據(jù), 進(jìn)行批訓(xùn)練.

2.5.1 DataLoader

DataLoader 是 torch 給你用來包裝你的數(shù)據(jù)的工具. 所以你要講自己的 (numpy array 或其他) 數(shù)據(jù)形式裝換成 Tensor, 然后再放進(jìn)這個(gè)包裝器中. 使用 DataLoader 有什么好處呢? 就是他們幫你有效地迭代數(shù)據(jù), 舉例:

import torch
import torch.utils.data as Data
torch.manual_seed(1)    # reproducible

BATCH_SIZE = 5      # 批訓(xùn)練的數(shù)據(jù)個(gè)數(shù)

x = torch.linspace(1, 10, 10)       # x data (torch tensor)
y = torch.linspace(10, 1, 10)       # y data (torch tensor)

# 先轉(zhuǎn)換成 torch 能識(shí)別的 Dataset
torch_dataset = Data.TensorDataset(data_tensor=x, target_tensor=y)

# 把 dataset 放入 DataLoader
loader = Data.DataLoader(
    dataset=torch_dataset,      # torch TensorDataset format
    batch_size=BATCH_SIZE,      # mini batch size
    shuffle=True,               # 要不要打亂數(shù)據(jù) (打亂比較好)
    num_workers=2,              # 多線程來讀數(shù)據(jù)
)

for epoch in range(3):   # 訓(xùn)練所有!整套!數(shù)據(jù) 3 次
    for step, (batch_x, batch_y) in enumerate(loader):  # 每一步 loader 釋放一小批數(shù)據(jù)用來學(xué)習(xí)
        # 假設(shè)這里就是你訓(xùn)練的地方...

        # 打出來一些數(shù)據(jù)
        print('Epoch: ', epoch, '| Step: ', step, '| batch x: ',
              batch_x.numpy(), '| batch y: ', batch_y.numpy())

"""
Epoch:  0 | Step:  0 | batch x:  [ 6.  7.  2.  3.  1.] | batch y:  [  5.   4.   9.   8.  10.]
Epoch:  0 | Step:  1 | batch x:  [  9.  10.   4.   8.   5.] | batch y:  [ 2.  1.  7.  3.  6.]
Epoch:  1 | Step:  0 | batch x:  [  3.   4.   2.   9.  10.] | batch y:  [ 8.  7.  9.  2.  1.]
Epoch:  1 | Step:  1 | batch x:  [ 1.  7.  8.  5.  6.] | batch y:  [ 10.   4.   3.   6.   5.]
Epoch:  2 | Step:  0 | batch x:  [ 3.  9.  2.  6.  7.] | batch y:  [ 8.  2.  9.  5.  4.]
Epoch:  2 | Step:  1 | batch x:  [ 10.   4.   8.   1.   5.] | batch y:  [  1.   7.   3.  10.   6.]
"""

可以看出, 每步都導(dǎo)出了5個(gè)數(shù)據(jù)進(jìn)行學(xué)習(xí). 然后每個(gè) epoch 的導(dǎo)出數(shù)據(jù)都是先打亂了以后再導(dǎo)出.
真正方便的還不是這點(diǎn). 如果我們改變一下 BATCH_SIZE = 8, 這樣我們就知道, step=0 會(huì)導(dǎo)出8個(gè)數(shù)據(jù), 但是, step=1 時(shí)數(shù)據(jù)庫中的數(shù)據(jù)不夠 8個(gè), 這時(shí)怎么辦呢:

BATCH_SIZE = 8      # 批訓(xùn)練的數(shù)據(jù)個(gè)數(shù)

...

for ...:
    for ...:
        ...
        print('Epoch: ', epoch, '| Step: ', step, '| batch x: ',
              batch_x.numpy(), '| batch y: ', batch_y.numpy())
"""
Epoch:  0 | Step:  0 | batch x:  [  6.   7.   2.   3.   1.   9.  10.   4.] | batch y:  [  5.   4.   9.   8.  10.   2.   1.   7.]
Epoch:  0 | Step:  1 | batch x:  [ 8.  5.] | batch y:  [ 3.  6.]
Epoch:  1 | Step:  0 | batch x:  [  3.   4.   2.   9.  10.   1.   7.   8.] | batch y:  [  8.   7.   9.   2.   1.  10.   4.   3.]
Epoch:  1 | Step:  1 | batch x:  [ 5.  6.] | batch y:  [ 6.  5.]
Epoch:  2 | Step:  0 | batch x:  [  3.   9.   2.   6.   7.  10.   4.   8.] | batch y:  [ 8.  2.  9.  5.  4.  1.  7.  3.]
Epoch:  2 | Step:  1 | batch x:  [ 1.  5.] | batch y:  [ 10.   6.]
"""

這時(shí), 在 step=1 就只給你返回這個(gè) epoch 中剩下的數(shù)據(jù)就好了.

2.6 加速神經(jīng)網(wǎng)絡(luò)訓(xùn)練

加速你的神經(jīng)網(wǎng)絡(luò)訓(xùn)練過程包括以下幾種模式:

  • Stochastic Gradient Descent (SGD)
  • Momentum
  • AdaGrad
  • RMSProp
  • Adam

具體算法講解待后續(xù)補(bǔ)充观游,也可網(wǎng)上搜索相關(guān)資料。

2.7 Optimizer 優(yōu)化器

這節(jié)內(nèi)容主要是用 Torch 實(shí)踐上一小節(jié)中提到的幾種優(yōu)化器驮俗,下圖就是這節(jié)內(nèi)容對(duì)比各種優(yōu)化器的效果:
image

2.7.1 偽數(shù)據(jù)

為了對(duì)比各種優(yōu)化器的效果, 我們需要有一些數(shù)據(jù), 今天我們還是自己編一些偽數(shù)據(jù), 這批數(shù)據(jù)是這樣的:
image

2.7.2 每個(gè)優(yōu)化器優(yōu)化一個(gè)神經(jīng)網(wǎng)絡(luò)

為了對(duì)比每一種優(yōu)化器, 我們給他們各自創(chuàng)建一個(gè)神經(jīng)網(wǎng)絡(luò), 但這個(gè)神經(jīng)網(wǎng)絡(luò)都來自同一個(gè) Net 形式.

# 默認(rèn)的 network 形式
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.hidden = torch.nn.Linear(1, 20)   # hidden layer
        self.predict = torch.nn.Linear(20, 1)   # output layer

    def forward(self, x):
        x = F.relu(self.hidden(x))      # activation function for hidden layer
        x = self.predict(x)             # linear output
        return x

# 為每個(gè)優(yōu)化器創(chuàng)建一個(gè) net
net_SGD         = Net()
net_Momentum    = Net()
net_RMSprop     = Net()
net_Adam        = Net()
nets = [net_SGD, net_Momentum, net_RMSprop, net_Adam]

2.7.3 優(yōu)化器 Optimizer

接下來在創(chuàng)建不同的優(yōu)化器, 用來訓(xùn)練不同的網(wǎng)絡(luò). 并創(chuàng)建一個(gè) loss_func 用來計(jì)算誤差. 我們用幾種常見的優(yōu)化器, SGD, Momentum, RMSprop, Adam.

# different optimizers
opt_SGD         = torch.optim.SGD(net_SGD.parameters(), lr=LR)
opt_Momentum    = torch.optim.SGD(net_Momentum.parameters(), lr=LR, momentum=0.8)
opt_RMSprop     = torch.optim.RMSprop(net_RMSprop.parameters(), lr=LR, alpha=0.9)
opt_Adam        = torch.optim.Adam(net_Adam.parameters(), lr=LR, betas=(0.9, 0.99))
optimizers = [opt_SGD, opt_Momentum, opt_RMSprop, opt_Adam]

loss_func = torch.nn.MSELoss()
losses_his = [[], [], [], []]   # 記錄 training 時(shí)不同神經(jīng)網(wǎng)絡(luò)的 loss

2.7.3 優(yōu)化器 Optimizer

接下來在創(chuàng)建不同的優(yōu)化器, 用來訓(xùn)練不同的網(wǎng)絡(luò). 并創(chuàng)建一個(gè) loss_func 用來計(jì)算誤差. 我們用幾種常見的優(yōu)化器, SGD, Momentum, RMSprop, Adam.

# different optimizers
opt_SGD         = torch.optim.SGD(net_SGD.parameters(), lr=LR)
opt_Momentum    = torch.optim.SGD(net_Momentum.parameters(), lr=LR, momentum=0.8)
opt_RMSprop     = torch.optim.RMSprop(net_RMSprop.parameters(), lr=LR, alpha=0.9)
opt_Adam        = torch.optim.Adam(net_Adam.parameters(), lr=LR, betas=(0.9, 0.99))
optimizers = [opt_SGD, opt_Momentum, opt_RMSprop, opt_Adam]

loss_func = torch.nn.MSELoss()
losses_his = [[], [], [], []]   # 記錄 training 時(shí)不同神經(jīng)網(wǎng)絡(luò)的 loss

2.7.4 訓(xùn)練懂缕、出圖

接下來訓(xùn)練和 loss 畫圖.

# 訓(xùn)練
for epoch in range(EPOCH):
    print('Epoch: ', epoch)
    for step, (batch_x, batch_y) in enumerate(loader):
        b_x = Variable(batch_x)  # 務(wù)必要用 Variable 包一下
        b_y = Variable(batch_y)

        # 對(duì)每個(gè)優(yōu)化器, 優(yōu)化屬于他的神經(jīng)網(wǎng)絡(luò)
        for net, opt, l_his in zip(nets, optimizers, losses_his):
            output = net(b_x)              # get output for every net
            loss = loss_func(output, b_y)  # compute loss for every net
            opt.zero_grad()                # clear gradients for next train
            loss.backward()                # backpropagation, compute gradients
            opt.step()                     # apply gradients
            l_his.append(loss.data[0])     # loss recoder

# 出圖
labels = ['SGD', 'Momentum', 'RMSprop', 'Adam']
for i, l_his in enumerate(losses_his):
    plt.plot(l_his, label=labels[i])
plt.legend(loc='best')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.ylim((0, 0.2))
plt.show()
image

SGD 是最普通的優(yōu)化器, 也可以說沒有加速效果, 而 Momentum 是 SGD 的改良版, 它加入了動(dòng)量原則. 后面的 RMSprop 又是 Momentum 的升級(jí)版. 而 Adam 又是 RMSprop 的升級(jí)版. 不過從這個(gè)結(jié)果中我們看到, Adam 的效果似乎比 RMSprop 要差一點(diǎn). 所以說并不是越先進(jìn)的優(yōu)化器, 結(jié)果越佳. 我們?cè)谧约旱脑囼?yàn)中可以嘗試不同的優(yōu)化器, 找到那個(gè)最適合你數(shù)據(jù)/網(wǎng)絡(luò)的優(yōu)化器.

高級(jí)神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)

3.1 卷積神經(jīng)網(wǎng)絡(luò) CNN

這一節(jié),我們一步一步做一個(gè)分析手寫數(shù)字的 CNN王凑。下面是一個(gè) CNN 最后一層的學(xué)習(xí)過程, 我們先可視化看看:[站外圖片上傳中...(image-f40379-1521963744043)]

3.1.1 MNIST手寫數(shù)據(jù)

import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.utils.data as Data
import torchvision      # 數(shù)據(jù)庫模塊
import matplotlib.pyplot as plt

torch.manual_seed(1)    # reproducible

# Hyper Parameters
EPOCH = 1           # 訓(xùn)練整批數(shù)據(jù)多少次, 為了節(jié)約時(shí)間, 我們只訓(xùn)練一次
BATCH_SIZE = 50
LR = 0.001          # 學(xué)習(xí)率
DOWNLOAD_MNIST = True  # 如果你已經(jīng)下載好了mnist數(shù)據(jù)就寫上 Fasle


# Mnist 手寫數(shù)字
train_data = torchvision.datasets.MNIST(
    root='./mnist/',    # 保存或者提取位置
    train=True,  # this is training data
    transform=torchvision.transforms.ToTensor(),    # 轉(zhuǎn)換 PIL.Image or numpy.ndarray 成
                                                    # torch.FloatTensor (C x H x W), 訓(xùn)練的時(shí)候 normalize 成 [0.0, 1.0] 區(qū)間
    download=DOWNLOAD_MNIST,          # 沒下載就下載, 下載了就不用再下了
)

# plot one example
print(train_data.train_data.size())                 # (60000, 28, 28)
print(train_data.train_labels.size())               # (60000)
plt.imshow(train_data.train_data[0].numpy(), cmap='gray')
plt.title('%i' % train_data.train_labels[0])
plt.show()
image

黑色的地方的值都是0, 白色的地方值大于0.
同樣, 我們除了訓(xùn)練數(shù)據(jù), 還給一些測(cè)試數(shù)據(jù), 測(cè)試看看它有沒有訓(xùn)練好.

test_data = torchvision.datasets.MNIST(root='./mnist/', train=False)

# 批訓(xùn)練 50samples, 1 channel, 28x28 (50, 1, 28, 28)
train_loader = Data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)

# 為了節(jié)約時(shí)間, 我們測(cè)試時(shí)只測(cè)試前2000個(gè)
test_x = Variable(torch.unsqueeze(test_data.test_data, dim=1), volatile=True).type(torch.FloatTensor)[:2000]/255.   # shape from (2000, 28, 28) to (2000, 1, 28, 28), value in range(0,1)
test_y = test_data.test_labels[:2000]

3.1.2 CNN模型

和以前一樣, 我們用一個(gè) class 來建立 CNN 模型. 這個(gè) CNN 整體流程是:

卷積(Conv2d) -> 激勵(lì)函數(shù)(ReLU) -> 池化, 向下采樣 (MaxPooling) -> 再來一遍 -> 展平多維的卷積成的特征圖 -> 接入全連接層 (Linear) -> 輸出

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Sequential(  # input shape (1, 28, 28)
            nn.Conv2d(
                in_channels=1,      # input height
                out_channels=16,    # n_filters
                kernel_size=5,      # filter size
                stride=1,           # filter movement/step
                padding=2,      # 如果想要 con2d 出來的圖片長(zhǎng)寬沒有變化, padding=(kernel_size-1)/2 當(dāng) stride=1
            ),      # output shape (16, 28, 28)
            nn.ReLU(),    # activation
            nn.MaxPool2d(kernel_size=2),    # 在 2x2 空間里向下采樣, output shape (16, 14, 14)
        )
        self.conv2 = nn.Sequential(  # input shape (16, 14, 14)
            nn.Conv2d(16, 32, 5, 1, 2),  # output shape (32, 14, 14)
            nn.ReLU(),  # activation
            nn.MaxPool2d(2),  # output shape (32, 7, 7)
        )
        self.out = nn.Linear(32 * 7 * 7, 10)   # fully connected layer, output 10 classes

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)   # 展平多維的卷積圖成 (batch_size, 32 * 7 * 7)
        output = self.out(x)
        return output

cnn = CNN()
print(cnn)  # net architecture
"""
CNN (
  (conv1): Sequential (
    (0): Conv2d(1, 16, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): ReLU ()
    (2): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
  )
  (conv2): Sequential (
    (0): Conv2d(16, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): ReLU ()
    (2): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
  )
  (out): Linear (1568 -> 10)
)
"""

3.1.3 訓(xùn)練

下面我們開始訓(xùn)練, 將 x y 都用 Variable 包起來, 然后放入 cnn 中計(jì)算 output, 最后再計(jì)算誤差.

optimizer = torch.optim.Adam(cnn.parameters(), lr=LR)   # optimize all cnn parameters
loss_func = nn.CrossEntropyLoss()   # the target label is not one-hotted

# training and testing
for epoch in range(EPOCH):
    for step, (x, y) in enumerate(train_loader):   # 分配 batch data, normalize x when iterate train_loader
        b_x = Variable(x)   # batch x
        b_y = Variable(y)   # batch y

        output = cnn(b_x)               # cnn output
        loss = loss_func(output, b_y)   # cross entropy loss
        optimizer.zero_grad()           # clear gradients for this training step
        loss.backward()                 # backpropagation, compute gradients
        optimizer.step()                # apply gradients

        if step % 50 == 0:
            test_output, last_layer = cnn(test_x)
            pred_y = torch.max(test_output, 1)[1].data.squeeze()
            accuracy = sum(pred_y == test_y) / float(test_y.size(0))
            print('Epoch: ', epoch, '| train loss: %.4f' % loss.data[0], '| test accuracy: %.2f' % accuracy)
            if HAS_SK:
                # Visualization of trained flatten layer (T-SNE)
                tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
                plot_only = 500
                low_dim_embs = tsne.fit_transform(last_layer.data.numpy()[:plot_only, :])
                labels = test_y.numpy()[:plot_only]
                plot_with_labels(low_dim_embs, labels)
"""
...
Epoch:  0 | train loss: 0.0306 | test accuracy: 0.97
Epoch:  0 | train loss: 0.0147 | test accuracy: 0.98
Epoch:  0 | train loss: 0.0427 | test accuracy: 0.98
Epoch:  0 | train loss: 0.0078 | test accuracy: 0.98
"""

最后我們?cè)賮砣?0個(gè)數(shù)據(jù), 看看預(yù)測(cè)的值到底對(duì)不對(duì):

test_output = cnn(test_x[:10])
pred_y = torch.max(test_output, 1)[1].data.numpy().squeeze()
print(pred_y, 'prediction number')
print(test_y[:10].numpy(), 'real number')

"""
[7 2 1 0 4 1 4 9 5 9] prediction number
[7 2 1 0 4 1 4 9 5 9] real number
"""

3.1.4 可視化訓(xùn)練

這是做完視頻后突然想要補(bǔ)充的內(nèi)容, 因?yàn)榭梢暬梢詭椭斫? 所以還是有必要提一下. 可視化的代碼主要是用 matplotlib 和 sklearn 來完成的, 因?yàn)槠渲形覀冇玫搅?T-SNE 的降維手段, 將高維的 CNN 最后一層輸出結(jié)果可視化, 也就是 CNN forward 代碼中的 x = x.view(x.size(0), -1) 這一個(gè)結(jié)果.

可視化的代碼不是重點(diǎn), 我們就直接展示可視化的結(jié)果吧.
image

3.2 循環(huán)神經(jīng)網(wǎng)絡(luò) RNN

高階內(nèi)容

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末搪柑,一起剝皮案震驚了整個(gè)濱河市聋丝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌工碾,老刑警劉巖弱睦,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異渊额,居然都是意外死亡况木,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門旬迹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來火惊,“玉大人,你說我怎么就攤上這事奔垦∫倌停” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵宴倍,是天一觀的道長(zhǎng)张症。 經(jīng)常有香客問我,道長(zhǎng)鸵贬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任脖捻,我火速辦了婚禮阔逼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘地沮。我一直安慰自己嗜浮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布摩疑。 她就那樣靜靜地躺著危融,像睡著了一般。 火紅的嫁衣襯著肌膚如雪雷袋。 梳的紋絲不亂的頭發(fā)上吉殃,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音楷怒,去河邊找鬼蛋勺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鸠删,可吹牛的內(nèi)容都是我干的抱完。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼刃泡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼巧娱!你這毒婦竟也來了碉怔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤禁添,失蹤者是張志新(化名)和其女友劉穎撮胧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體上荡,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡趴樱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酪捡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叁征。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖逛薇,靈堂內(nèi)的尸體忽然破棺而出捺疼,到底是詐尸還是另有隱情,我是刑警寧澤永罚,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布啤呼,位于F島的核電站,受9級(jí)特大地震影響呢袱,放射性物質(zhì)發(fā)生泄漏官扣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一羞福、第九天 我趴在偏房一處隱蔽的房頂上張望惕蹄。 院中可真熱鬧,春花似錦治专、人聲如沸卖陵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泪蔫。三九已至,卻和暖如春喘批,著一層夾襖步出監(jiān)牢的瞬間撩荣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工谤祖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婿滓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓粥喜,卻偏偏與公主長(zhǎng)得像凸主,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子额湘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容