上一節中,我們使用autograd的包來定義模型並求導。本節中,我們將使用torch.nn包來構建神經網絡。
一個nn.Module包含各個層和一個forward(input)方法,該方法返回output.
上圖是一個簡單的前饋神經網絡。它接受一個輸入。然后一層接着一層地傳遞。最后輸出計算的結果。
神經網絡模型的訓練過程
神經網絡的典型訓練過程如下:
- 定義包含一些可學習的參數(或者叫做權重)的神經網絡模型。
- 在數據集上迭代。
- 通過神經網絡處理輸入。
- 計算損失函數(輸出結果和正確值的差值大小)。
- 將梯度反向傳播回網絡的權重等參數。
- 更新網絡的權重。主要使用以下的更新原則:weight = weight - learining_rate*gradient.
完整的代碼如下(跟官方比,做了詳細的中文注釋):
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data.dataloader as Data
import torchvision #torchvision模塊包括了一些圖像數據集,如MNIST,cifar10等
import matplotlib.pyplot as plt
# 1 准備數據
# 所有的torchvision.datasets數據集的類都是torch.utils.data.Dataset的子類,實現了__getitem__和__len__方法。故可傳遞給torch.utils.data.DataLoader來加載
# 創建用於Train的數據集,若root目錄無數據集,則Download;若root目錄有數據集,則從PIL圖像數據轉換為Tensor
train_data = torchvision.datasets.MNIST(root='./mnist',train=True,transform=torchvision.transforms.ToTensor(),download=True)
test_data = torchvision.datasets.MNIST(root='./mnist',train=False,transform=torchvision.transforms.ToTensor())
print("train_data:",train_data.data.size())
print("train_labels:",train_data.targets.size())
print("test_data:",test_data.data.size())
# 根據數據集創建響應的dataLoader
# shuffle(bool, 可選) – 如果每一個epoch內要打亂數據,就設置為True(默認:False)
train_loader = Data.DataLoader(dataset=train_data, batch_size=50, shuffle=True)
test_loader = Data.DataLoader(dataset=test_data, batch_size=500, shuffle=False)
# 2 創建模型
class CNN(nn.Module): # 定義了一個類,名字叫CNN
#注意: 在模型中必須要定義 `forward` 函數,`backward` 函數(用來計算梯度)會被`autograd`自動創建。 可以在 `forward` 函數中使用任何針對 `Tensor` 的操作。
def __init__(self): # 每個類都必須有的構造函數,用來初始化該類
super(CNN, self).__init__() # 先調用父類的構造函數
# 1 input image channel, 6 output channels, 5x5 square convolution
# kernel
# 本函數配置了卷積層和全連接層的維度
# Conv2d(in_cahnnels, out_channels, kernel_size, stride, padding=0 ,...)
self.conv1 = nn.Conv2d(1, 16, 5, 1, 2) # 卷積層1: 二維卷積層, 1x28x28,16x28x28, 卷積核大小為5x5
self.conv2 = nn.Conv2d(16, 32, 5, 1, 2) # 卷積層2: 二維卷積層, 16x14x14,32x14x14, 卷積核大小為5x5
# an affine(仿射) operation: y = Wx + b # 全連接層1: 線性層, 輸入維度32x7x7,輸出維度128
self.fc1 = nn.Linear(32 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10) # 全連接層2: 線性層, 輸入維度128,輸出維度10
def forward(self, x): #定義了forward函數
# Max pooling over a (2, 2) window
conv1_out = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # 先卷積,再池化
# If the size is a square you can only specify a single number
conv2_out = F.max_pool2d(F.relu(self.conv2(conv1_out)), 2) # 再卷積,再池化
res = conv2_out.view(conv2_out.size(0), -1) # 將conv3_out展開成一維(扁平化)
fc1_out = F.relu(self.fc1(res)) # 全連接1
out = self.fc2(fc1_out) # 全連接2
#return out
return F.log_softmax(out),fc1_out #返回softmax后的Tensor,以及倒數第二層的Tensor(以進行低維Tensor的可視化)
cnn = CNN() #新建了一個CNN對象,其實是一系列的函數/方法的集合
cnn = cnn.cuda() #*.cuda()將模型的所有參數和緩存移動到GPU
print(cnn)
def plot_with_labels(lowDWeights, labels):
plt.cla() #clear當前活動的坐標軸
X, Y = lowDWeights[:, 0], lowDWeights[:, 1] #把Tensor的第1列和第2列,也就是TSNE之后的前兩個特征提取出來,作為X,Y
for x, y, s in zip(X, Y, labels):
c = cm.rainbow(int(255 * s / 9));
#plt.text(x, y, s, backgroundcolor=c, fontsize=9)
plt.text(x, y, str(s),color=c,fontdict={'weight': 'bold', 'size': 9}) #在指定位置放置文本
plt.xlim(X.min(), X.max());
plt.ylim(Y.min(), Y.max());
plt.title('Visualize last layer');
plt.show();
plt.pause(0.01)
# 3 定義損失函數-這里默認是交叉熵函數
loss_func = torch.nn.CrossEntropyLoss()
# 4 初始化:優化器
optimizer = optim.Adam(cnn.parameters(), lr=0.01) #list(cnn.parameters())會給出一個參數列表,記錄了所有訓練參數(W和b)的數據
# optimizer =optim.Adam([ {'params': cnn.conv1.weight}, {'params': cnn.conv1.bias, 'lr': 0.002,'weight_decay': 0 },
# {'params': cnn.conv2.weight}, {'params': cnn.conv2.bias, 'lr': 0.002,'weight_decay': 0 },
# {'params': cnn.fc1.weight}, {'params': cnn.fc1.bias, 'lr': 0.002,'weight_decay': 0 },
# {'params': cnn.fc2.weight}, {'params': cnn.fc2.bias, 'lr': 0.002,'weight_decay': 0 },
# {'params': cnn.conv3.weight}, {'params': cnn.conv3.bias, 'lr': 0.002,'weight_decay': 0 },
# {'params': cnn.conv4.weight}, {'params': cnn.conv4.bias, 'lr': 0.002,'weight_decay': 0 },
# {'params': cnn.conv5.weight}, {'params': cnn.conv5.bias, 'lr': 0.002,'weight_decay': 0 },], lr=0.001, weight_decay=0.0001)
# 5 訓練:
def train(epoch):
print('epoch {}'.format(epoch))
#直接初始化為0的是標量,tensor調用item()將返回標量值
train_loss = 0
train_acc = 0
#step是enumerate()函數自帶的索引,從0開始
for step, (batch_x, batch_y) in enumerate(train_loader):
# 把batch_x和batth_y移動到GPU
batch_x = batch_x.cuda()
batch_y = batch_y.cuda()
# 正向傳播
out,_ = cnn(batch_x)
loss = loss_func(out, batch_y)
train_loss += loss.item()
# torch.max(tensor,dim:int):tensor找到第dim維度(第0維度是數據下標)上的最大值
# return: 第一個Tensor是該維度的最大值,第二個Tensor是最大值相應的下標
pred = torch.max(out, 1)[1]
# 直接對邏輯量進行sum,將返回True的個數
train_correct = (pred == batch_y).sum()
train_acc += train_correct.item()
if step % 20 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, step * len(batch_x), len(train_loader.dataset),100. * step / len(train_loader), loss.item()))
#反向傳播
optimizer.zero_grad() # 所有參數的梯度清零
loss.backward() #即反向傳播求梯度
optimizer.step() #調用optimizer進行梯度下降更新參數
print('Train Loss: {:.6f}, Acc: {:.6f}'.format(train_loss / (len(train_data)), train_acc / (len(train_data))))
from matplotlib import cm
try:
from sklearn.manifold import TSNE; HAS_SK = True
except:
HAS_SK = False; print('Please install sklearn for layer visualization')
# 6 准確率
def test():
cnn.eval()
eval_loss = 0
eval_acc = 0
# 打開imshow()交互模式:更新圖像后直接執行以后的代碼,不阻塞在plt.show()
plt.ion()
#無需反向傳播計算梯度,不需要進行求導運算
with torch.no_grad():
for step, (batch_x, batch_y) in enumerate(test_loader):
batch_x = batch_x.cuda()
batch_y = batch_y.cuda()
out,last_layer = cnn(batch_x)
loss = loss_func(out, batch_y)
#loss = += F.nll_loss(out, batch_y, size_average=False).item()
eval_loss += loss.item()
pred = torch.max(out, 1)[1]
num_correct = (pred == batch_y).sum()
eval_acc += num_correct.item()
#若需繪圖,將下面代碼塊注釋去掉
if step % 100 == 0:
#t-SNE 是一種非線性降維算法,非常適用於高維數據降維到2維或者3維,進行可視化
tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
#最多只畫500個點
plot_only = 500
#fit_transform函數把last_layer的Tensor降低至2個特征量,即3個維度(2個維度的坐標系)
low_dim_embs = tsne.fit_transform(last_layer.cpu().data.numpy()[:plot_only, :])
labels = batch_y.cpu().numpy()[:plot_only]
plot_with_labels(low_dim_embs, labels)
#若需繪圖,將上面代碼塊注釋去掉
print('Test Loss: {:.6f}, Accuracy: {}/{} ({:.2f}%'.format(eval_loss / (len(test_data)),eval_acc, len(test_data) ,100.*eval_acc / (len(test_data))))
plt.ioff()
# 共訓練/測試 20輪
# 每輪訓練整個數據集1遍,每輪有len(dataset)/batch_size次訓練
# 每次訓練要訓練batch_size個數據
# 每個batch的數據,第一個維度是數據的下標:0,1,2,...,batch_size-1
for epoch in range(1, 21):
train(epoch)
test()
除了上面代碼段中的實現,你可能還需要了解以下知識:
損失函數與autograd反向傳播
一個損失函數需要一對輸入:模型輸出和目標,然后計算一個值來評估輸出距離目標有多遠。
根據之前學習的理論,一般是計算y和y'的交叉熵.
例如,對於線性擬合案例,損失函數就是均方誤差,即MSE.
output = net(input)
target = torch.randn(10) # a dummy target, for example
target = target.view(1, -1) # make it the same shape as output
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
將輸出:
tensor(1.3389, grad_fn=<MseLossBackward>)
現在,如果你跟隨損失到反向傳播路徑,可以使用它的 .grad_fn 屬性,你將會看到一個這樣的計算圖:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
所以,當我們調用 loss.backward(),整個圖都會微分,而且所有的在圖中的requires_grad=True 的張量將會讓他們的 grad 張量累計梯度。
為了演示,我們將跟隨以下步驟來跟蹤反向傳播。
print(loss.grad_fn) # MSELoss
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU
輸出:
<MseLossBackward object at 0x7fab77615278>
<AddmmBackward object at 0x7fab77615940>
<AccumulateGrad object at 0x7fab77615940>
為了實現反向傳播損失,我們所有需要做的事情僅僅是使用 loss.backward()。你需要清空現存的梯度,要不梯度將會和現存的梯度累計到一起。
現在我們調用 loss.backward()
,然后看一下 con1
的偏置項在反向傳播之前和之后的變化。
net.zero_grad() # zeroes the gradient buffers of all parameters,清空現存的梯度
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
輸出:
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([-0.0054, 0.0011, 0.0012, 0.0148, -0.0186, 0.0087])
上面介紹了如何使用損失函數。
更新神經網絡參數
最簡單的更新規則就是隨機梯度下降:weight = weight - learning_rate * gradient
。
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
復雜的神經網絡參數可這樣使用:
import torch.optim as optim
# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)
# in your training loop:
optimizer.zero_grad() # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # Does the update
t-SNE:數據可視化
本節摘自 這里。
數據降維與可視化——t-SNE
t-SNE 是目前來說效果最好的數據降維與可視化方法,但是它的缺點也很明顯,比如:占內存大,運行時間長。但是,當我們想要對高維數據進行分類,又不清楚這個數據集有沒有很好的可分性(即同類之間間隔小,異類之間間隔大),可以通過 t-SNE 投影到 2 維或者 3 維的空間中觀察一下。如果在低維空間中具有可分性,則數據是可分的;如果在高維空間中不具有可分性,可能是數據不可分,也可能僅僅是因為不能投影到低維空間。
t-distributed Stochastic Neighbor Embedding(t-SNE)
t-SNE(TSNE)將數據點之間的相似度轉換為概率。原始空間中的相似度由高斯聯合概率表示,嵌入空間的相似度由“學生 t 分布”表示。
雖然 Isomap,LLE 和 variants 等數據降維和可視化方法,更適合展開單個連續的低維的 manifold。但如果要准確的可視化樣本間的相似度關系,如對於下圖所示的 S 曲線(不同顏色的圖像表示不同類別的數據),t-SNE 表現更好。因為t-SNE 主要是關注數據的局部結構。
通過原始空間和嵌入空間的聯合概率的 Kullback-Leibler(KL)散度來評估可視化效果的好壞,也就是說用有關 KL 散度的函數作為 loss 函數,然后通過梯度下降最小化 loss 函數,最終獲得收斂結果。
使用 t-SNE 的缺點大概是:
- t-SNE 的計算復雜度很高,在數百萬個樣本數據集中可能需要幾個小時,而 PCA 可以在幾秒鍾或幾分鍾內完成
- Barnes-Hut t-SNE 方法(下面講)限於二維或三維嵌入。
- 算法是隨機的,具有不同種子的多次實驗可以產生不同的結果。雖然選擇 loss 最小的結果就行,但可能需要多次實驗以選擇超參數。
- 全局結構未明確保留。這個問題可以通過 PCA 初始化點(使用init ='pca')來緩解。
其參數及描述如下:
n_components int, 默認為 2,嵌入空間的維度(嵌入空間的意思就是結果空間)
perplexity float, 默認為 30,數據集越大,需要參數值越大,建議值位 5-50
early_exaggeration float, 默認為 12.0,控制原始空間中的自然集群在嵌入式空間中的緊密程度以及它們之間的空間。 對於較大的值,嵌入式空間中自然群集之間的空間將更大。 再次,這個參數的選擇不是很關鍵。 如果在初始優化期間成本函數增加,則可能是該參數值過高。
learning_rate float, default:200.0, 學習率,建議取值為 10.0-1000.0
n_iter int, default:1000, 最大迭代次數
n_iter_without_progress int, default:300, 另一種形式的最大迭代次數,必須是 50 的倍數
min_grad_norm float, default:1e-7, 如果梯度低於該值,則停止算法
metric string or callable, 精確度的計量方式
init string or numpy array, default:”random”, 可以是’random’, ‘pca’或者一個 numpy 數組(shape=(n_samples, n_components)。
verbose int, default:0, 訓練過程是否可視
random_state int, RandomState instance or None, default:None,控制隨機數的生成
method string, default:’barnes_hut’, 對於大數據集用默認值,對於小數據集用’exact’
angle float, default:0.5, 只有method='barnes_hut'時可用
其具有的屬性有:
attributes description
embedding_ 嵌入向量
kl_divergence 最后的 KL 散度
n_iter_ 迭代的次數
其Method有:
Methods description
fit 將 X 投影到一個嵌入空間
fit_transform 將 X 投影到一個嵌入空間並返回轉換結果
get_params 獲取 t-SNE 的參數
set_params 設置 t-SNE 的參數
下列代碼詳細解釋了MNIST數據集可進行的可視化:
# coding='utf-8'
"""t-SNE 對手寫數字進行可視化"""
from time import time
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.manifold import TSNE
def get_data():
digits = datasets.load_digits(n_class=6)
data = digits.data
label = digits.target
n_samples, n_features = data.shape
return data, label, n_samples, n_features
def plot_embedding(data, label, title):
x_min, x_max = np.min(data, 0), np.max(data, 0)
data = (data - x_min) / (x_max - x_min)
fig = plt.figure()
ax = plt.subplot(111)
for i in range(data.shape[0]):
plt.text(data[i, 0], data[i, 1], str(label[i]),
color=plt.cm.Set1(label[i] / 10.),
fontdict={'weight': 'bold', 'size': 9})
plt.xticks([])
plt.yticks([])
plt.title(title)
return fig
def main():
data, label, n_samples, n_features = get_data()
print('Computing t-SNE embedding')
tsne = TSNE(n_components=2, init='pca', random_state=0)
t0 = time()
result = tsne.fit_transform(data)
fig = plot_embedding(result, label,
't-SNE embedding of the digits (time %.2fs)'
% (time() - t0))
plt.show(fig)
if __name__ == '__main__':
main()