文本分類
文本情感分類數(shù)據(jù)集 使用循環(huán)神經(jīng)網(wǎng)絡(luò)進(jìn)行情感分類 使用卷積神經(jīng)網(wǎng)絡(luò)進(jìn)行情感分類
文本分類是自然語言處理的一個(gè)常見任務(wù)陵究,它把一段不定長的文本序列變換為文本的類別。本節(jié)關(guān)注它的一個(gè)子問題:使用文本情感分類來分析文本作者的情緒奥帘。這個(gè)問題也叫情感分析畔乙,并有著廣泛的應(yīng)用。
同搜索近義詞和類比詞一樣翩概,文本分類也屬于詞嵌入的下游應(yīng)用牲距。在本節(jié)中返咱,我們將應(yīng)用預(yù)訓(xùn)練的詞向量和含多個(gè)隱藏層的雙向循環(huán)神經(jīng)網(wǎng)絡(luò)與卷積神經(jīng)網(wǎng)絡(luò),來判斷一段不定長的文本序列中包含的是正面還是負(fù)面的情緒牍鞠。后續(xù)內(nèi)容將從以下幾個(gè)方面展開:
文本情感分類數(shù)據(jù)集
使用循環(huán)神經(jīng)網(wǎng)絡(luò)進(jìn)行情感分類
使用卷積神經(jīng)網(wǎng)絡(luò)進(jìn)行情感分類
import collections
import os
import random
import time
from tqdm import tqdm
import torch
from torch import nn
import torchtext.vocab as Vocab
import torch.utils.data as Data
import torch.nn.functional as F
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
文本情感分類數(shù)據(jù)
我們使用斯坦福的IMDb數(shù)據(jù)集(Stanford’s Large Movie Review Dataset)作為文本情感分類的數(shù)據(jù)集咖摹。
讀取數(shù)據(jù)
數(shù)據(jù)集文件夾結(jié)構(gòu):
| aclImdb_v1
| train
| | pos
| | | 0_9.txt
| | | 1_7.txt
| | | ...
| | neg
| | | 0_3.txt
| | | 1_1.txt
| | ...
| test
| | pos
| | neg
| | ...
| ...
def read_imdb(folder='train', data_root="/home/kesci/input/IMDB2578/aclImdb_v1/aclImdb"):
data = []
for label in ['pos', 'neg']:
folder_name = os.path.join(data_root, folder, label)
for file in tqdm(os.listdir(folder_name)):
with open(os.path.join(folder_name, file), 'rb') as f:
review = f.read().decode('utf-8').replace('\n', '').lower()
data.append([review, 1 if label == 'pos' else 0])
random.shuffle(data)
return data
DATA_ROOT = "/home/kesci/input/IMDB2578/aclImdb_v1/"
data_root = os.path.join(DATA_ROOT, "aclImdb")
train_data, test_data = read_imdb('train', data_root), read_imdb('test', data_root)
# 打印訓(xùn)練數(shù)據(jù)中的前五個(gè)sample
for sample in train_data[:5]:
print(sample[1], '\t', sample[0][:50])
100%|██████████| 12500/12500 [00:00<00:00, 15484.71it/s]
100%|██████████| 12500/12500 [00:00<00:00, 53658.60it/s]
100%|██████████| 12500/12500 [00:00<00:00, 53187.52it/s]
100%|██████████| 12500/12500 [00:00<00:00, 52966.52it/s]
1 i'm 60 years old, a guitarist, (lead/rhythm), and
0 it's the worst movie i've ever seen. the action is
1 i have seen the movie holes and say that it has to
1 i just saw this last night, it was broadcast on th
0 ...well, pop this into the dvd, waste an hour and
預(yù)處理數(shù)據(jù)
讀取數(shù)據(jù)后,我們先根據(jù)文本的格式進(jìn)行單詞的切分难述,再利用 torchtext.vocab.Vocab
創(chuàng)建詞典萤晴。
def get_tokenized_imdb(data):
'''
@params:
data: 數(shù)據(jù)的列表,列表中的每個(gè)元素為 [文本字符串胁后,0/1標(biāo)簽] 二元組
@return: 切分詞后的文本的列表店读,列表中的每個(gè)元素為切分后的詞序列
'''
def tokenizer(text):
return [tok.lower() for tok in text.split(' ')]
return [tokenizer(review) for review, _ in data]
def get_vocab_imdb(data):
'''
@params:
data: 同上
@return: 數(shù)據(jù)集上的詞典,Vocab 的實(shí)例(freqs, stoi, itos)
'''
tokenized_data = get_tokenized_imdb(data)
counter = collections.Counter([tk for st in tokenized_data for tk in st])
return Vocab.Vocab(counter, min_freq=5)
vocab = get_vocab_imdb(train_data)
print('# words in vocab:', len(vocab))
words in vocab: 46152
詞典和詞語的索引創(chuàng)建好后攀芯,就可以將數(shù)據(jù)集的文本從字符串的形式轉(zhuǎn)換為單詞下標(biāo)序列的形式屯断,以待之后的使用。
def preprocess_imdb(data, vocab):
'''
@params:
data: 同上侣诺,原始的讀入數(shù)據(jù)
vocab: 訓(xùn)練集上生成的詞典
@return:
features: 單詞下標(biāo)序列殖演,形狀為 (n, max_l) 的整數(shù)張量
labels: 情感標(biāo)簽,形狀為 (n,) 的0/1整數(shù)張量
'''
max_l = 500 # 將每條評(píng)論通過截?cái)嗷蛘哐a(bǔ)0年鸳,使得長度變成500
def pad(x):
return x[:max_l] if len(x) > max_l else x + [0] * (max_l - len(x))
tokenized_data = get_tokenized_imdb(data)
features = torch.tensor([pad([vocab.stoi[word] for word in words]) for words in tokenized_data])
labels = torch.tensor([score for _, score in data])
return features, labels
創(chuàng)建數(shù)據(jù)迭代器
利用 torch.utils.data.TensorDataset
趴久,可以創(chuàng)建 PyTorch 格式的數(shù)據(jù)集,從而創(chuàng)建數(shù)據(jù)迭代器搔确。
train_set = Data.TensorDataset(*preprocess_imdb(train_data, vocab))
test_set = Data.TensorDataset(*preprocess_imdb(test_data, vocab))
# 上面的代碼等價(jià)于下面的注釋代碼
# train_features, train_labels = preprocess_imdb(train_data, vocab)
# test_features, test_labels = preprocess_imdb(test_data, vocab)
# train_set = Data.TensorDataset(train_features, train_labels)
# test_set = Data.TensorDataset(test_features, test_labels)
# len(train_set) = features.shape[0] or labels.shape[0]
# train_set[index] = (features[index], labels[index])
batch_size = 64
train_iter = Data.DataLoader(train_set, batch_size, shuffle=True)
test_iter = Data.DataLoader(test_set, batch_size)
for X, y in train_iter:
print('X', X.shape, 'y', y.shape)
break
print('#batches:', len(train_iter))
X torch.Size([64, 500]) y torch.Size([64])
batches: 391
使用循環(huán)神經(jīng)網(wǎng)絡(luò)
雙向循環(huán)神經(jīng)網(wǎng)絡(luò)
在“雙向循環(huán)神經(jīng)網(wǎng)絡(luò)”一節(jié)中彼棍,我們介紹了其模型與前向計(jì)算的公式,這里簡單回顧一下:
class BiRNN(nn.Module):
def __init__(self, vocab, embed_size, num_hiddens, num_layers):
'''
@params:
vocab: 在數(shù)據(jù)集上創(chuàng)建的詞典膳算,用于獲取詞典大小
embed_size: 嵌入維度大小
num_hiddens: 隱藏狀態(tài)維度大小
num_layers: 隱藏層個(gè)數(shù)
'''
super(BiRNN, self).__init__()
self.embedding = nn.Embedding(len(vocab), embed_size)
# encoder-decoder framework
# bidirectional設(shè)為True即得到雙向循環(huán)神經(jīng)網(wǎng)絡(luò)
self.encoder = nn.LSTM(input_size=embed_size,
hidden_size=num_hiddens,
num_layers=num_layers,
bidirectional=True)
self.decoder = nn.Linear(4*num_hiddens, 2) # 初始時(shí)間步和最終時(shí)間步的隱藏狀態(tài)作為全連接層輸入
def forward(self, inputs):
'''
@params:
inputs: 詞語下標(biāo)序列滥酥,形狀為 (batch_size, seq_len) 的整數(shù)張量
@return:
outs: 對(duì)文本情感的預(yù)測,形狀為 (batch_size, 2) 的張量
'''
# 因?yàn)長STM需要將序列長度(seq_len)作為第一維畦幢,所以需要將輸入轉(zhuǎn)置
embeddings = self.embedding(inputs.permute(1, 0)) # (seq_len, batch_size, d)
# rnn.LSTM 返回輸出、隱藏狀態(tài)和記憶單元缆蝉,格式如 outputs, (h, c)
outputs, _ = self.encoder(embeddings) # (seq_len, batch_size, 2*h)
encoding = torch.cat((outputs[0], outputs[-1]), -1) # (batch_size, 4*h)
outs = self.decoder(encoding) # (batch_size, 2)
return outs
embed_size, num_hiddens, num_layers = 100, 100, 2
net = BiRNN(vocab, embed_size, num_hiddens, num_layers)
加載預(yù)訓(xùn)練的詞向量
由于預(yù)訓(xùn)練詞向量的詞典及詞語索引與我們使用的數(shù)據(jù)集并不相同宇葱,所以需要根據(jù)目前的詞典及索引的順序來加載預(yù)訓(xùn)練詞向量。
cache_dir = "/home/kesci/input/GloVe6B5429"
glove_vocab = Vocab.GloVe(name='6B', dim=100, cache=cache_dir)
def load_pretrained_embedding(words, pretrained_vocab):
'''
@params:
words: 需要加載詞向量的詞語列表刊头,以 itos (index to string) 的詞典形式給出
pretrained_vocab: 預(yù)訓(xùn)練詞向量
@return:
embed: 加載到的詞向量
'''
embed = torch.zeros(len(words), pretrained_vocab.vectors[0].shape[0]) # 初始化為0
oov_count = 0 # out of vocabulary
for i, word in enumerate(words):
try:
idx = pretrained_vocab.stoi[word]
embed[i, :] = pretrained_vocab.vectors[idx]
except KeyError:
oov_count += 1
if oov_count > 0:
print("There are %d oov words." % oov_count)
return embed
net.embedding.weight.data.copy_(load_pretrained_embedding(vocab.itos, glove_vocab))
net.embedding.weight.requires_grad = False # 直接加載預(yù)訓(xùn)練好的, 所以不需要更新它
訓(xùn)練模型
訓(xùn)練時(shí)可以調(diào)用之前編寫的 train 及 evaluate_accuracy 函數(shù)黍瞧。
def evaluate_accuracy(data_iter, net, device=None):
if device is None and isinstance(net, torch.nn.Module):
device = list(net.parameters())[0].device
acc_sum, n = 0.0, 0
with torch.no_grad():
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval()
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train()
else:
if('is_training' in net.__code__.co_varnames):
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
def train(train_iter, test_iter, net, loss, optimizer, device, num_epochs):
net = net.to(device)
print("training on ", device)
batch_count = 0
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
optimizer.zero_grad()
l.backward()
optimizer.step()
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec'
% (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))
由于嵌入層的參數(shù)是不需要在訓(xùn)練過程中被更新的,所以我們利用 filter 函數(shù)和 lambda 表達(dá)式來過濾掉模型中不需要更新參數(shù)的部分原杂。
lr, num_epochs = 0.01, 5
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=lr)
loss = nn.CrossEntropyLoss()
train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)
training on cpu
epoch 1, loss 0.9336, train acc 0.658, test acc 0.788, time 361.3 sec
epoch 2, loss 0.2986, train acc 0.811, test acc 0.744, time 364.4 sec
epoch 3, loss 0.1692, train acc 0.879, test acc 0.791, time 353.2 sec
epoch 4, loss 0.1331, train acc 0.910, test acc 0.782, time 361.1 sec
epoch 5, loss 0.1177, train acc 0.918, test acc 0.771, time 366.7 sec
注:由于本地CPU上訓(xùn)練時(shí)間過長印颤,故只截取了運(yùn)行的結(jié)果,后同穿肄。大家可以自行在網(wǎng)站上訓(xùn)練年局。
評(píng)價(jià)模型
def predict_sentiment(net, vocab, sentence):
'''
@params:
net: 訓(xùn)練好的模型
vocab: 在該數(shù)據(jù)集上創(chuàng)建的詞典际看,用于將給定的單詞序轉(zhuǎn)換為單詞下標(biāo)的序列,從而輸入模型
sentence: 需要分析情感的文本矢否,以單詞序列的形式給出
@return: 預(yù)測的結(jié)果仲闽,positive 為正面情緒文本,negative 為負(fù)面情緒文本
'''
device = list(net.parameters())[0].device # 讀取模型所在的環(huán)境
sentence = torch.tensor([vocab.stoi[word] for word in sentence], device=device)
label = torch.argmax(net(sentence.view((1, -1))), dim=1)
return 'positive' if label.item() == 1 else 'negative'
predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great'])
out:
'positive'
predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad'])
out:
'positive'
使用卷積神經(jīng)網(wǎng)絡(luò)
一維卷積層
在介紹模型前我們先來解釋一維卷積層的工作原理僵朗。與二維卷積層一樣赖欣,一維卷積層使用一維的互相關(guān)運(yùn)算。在一維互相關(guān)運(yùn)算中验庙,卷積窗口從輸入數(shù)組的最左方開始顶吮,按從左往右的順序,依次在輸入數(shù)組上滑動(dòng)粪薛。當(dāng)卷積窗口滑動(dòng)到某一位置時(shí)悴了,窗口中的輸入子數(shù)組與核數(shù)組按元素相乘并求和,得到輸出數(shù)組中相應(yīng)位置的元素汗菜。如圖所示让禀,輸入是一個(gè)寬為 7 的一維數(shù)組,核數(shù)組的寬為 2陨界⊙沧幔可以看到輸出的寬度為 7?2+1=6,且第一個(gè)元素是由輸入的最左邊的寬為 2 的子數(shù)組與核數(shù)組按元素相乘后再相加得到的:0×1+1×2=2菌瘪。
def corr1d(X, K):
'''
@params:
X: 輸入腮敌,形狀為 (seq_len,) 的張量
K: 卷積核,形狀為 (w,) 的張量
@return:
Y: 輸出俏扩,形狀為 (seq_len - w + 1,) 的張量
'''
w = K.shape[0] # 卷積窗口寬度
Y = torch.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]): # 滑動(dòng)窗口
Y[i] = (X[i: i + w] * K).sum()
return Y
X, K = torch.tensor([0, 1, 2, 3, 4, 5, 6]), torch.tensor([1, 2])
print(corr1d(X, K))
tensor([ 2., 5., 8., 11., 14., 17.])
多輸入通道的一維互相關(guān)運(yùn)算也與多輸入通道的二維互相關(guān)運(yùn)算類似:在每個(gè)通道上糜工,將核與相應(yīng)的輸入做一維互相關(guān)運(yùn)算,并將通道之間的結(jié)果相加得到輸出結(jié)果录淡。下圖展示了含 3 個(gè)輸入通道的一維互相關(guān)運(yùn)算捌木,其中陰影部分為第一個(gè)輸出元素及其計(jì)算所使用的輸入和核數(shù)組元素:0×1+1×2+1×3+2×4+2×(?1)+3×(?3)=2。
def corr1d_multi_in(X, K):
# 首先沿著X和K的通道維遍歷并計(jì)算一維互相關(guān)結(jié)果嫉戚。然后將所有結(jié)果堆疊起來沿第0維累加
return torch.stack([corr1d(x, k) for x, k in zip(X, K)]).sum(dim=0)
# [corr1d(X[i], K[i]) for i in range(X.shape[0])]
X = torch.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = torch.tensor([[1, 2], [3, 4], [-1, -3]])
print(corr1d_multi_in(X, K))
tensor([ 2., 8., 14., 20., 26., 32.])
由二維互相關(guān)運(yùn)算的定義可知刨裆,多輸入通道的一維互相關(guān)運(yùn)算可以看作單輸入通道的二維互相關(guān)運(yùn)算。如圖所示彬檀,我們也可以將圖中多輸入通道的一維互相關(guān)運(yùn)算以等價(jià)的單輸入通道的二維互相關(guān)運(yùn)算呈現(xiàn)帆啃。這里核的高等于輸入的高。圖中的陰影部分為第一個(gè)輸出元素及其計(jì)算所使用的輸入和核數(shù)組元素:2×(?1)+3×(?3)+1×3+2×4+0×1+1×2=2窍帝。
注:反之僅當(dāng)二維卷積核的高度等于輸入的高度時(shí)才成立努潘。
之前的例子中輸出都只有一個(gè)通道。我們?cè)?a target="_blank">“多輸入通道和多輸出通道”一節(jié)中介紹了如何在二維卷積層中指定多個(gè)輸出通道。類似地疯坤,我們也可以在一維卷積層指定多個(gè)輸出通道报慕,從而拓展卷積層中的模型參數(shù)。
時(shí)序最大池化層
類似地贴膘,我們有一維池化層卖子。TextCNN 中使用的時(shí)序最大池化(max-over-time pooling)層實(shí)際上對(duì)應(yīng)一維全局最大池化層:假設(shè)輸入包含多個(gè)通道,各通道由不同時(shí)間步上的數(shù)值組成刑峡,各通道的輸出即該通道所有時(shí)間步中最大的數(shù)值洋闽。因此,時(shí)序最大池化層的輸入在各個(gè)通道上的時(shí)間步數(shù)可以不同突梦。
注:自然語言中還有一些其他的池化操作诫舅,可參考這篇博文。
為提升計(jì)算性能宫患,我們常常將不同長度的時(shí)序樣本組成一個(gè)小批量刊懈,并通過在較短序列后附加特殊字符(如0)令批量中各時(shí)序樣本長度相同。這些人為添加的特殊字符當(dāng)然是無意義的娃闲。由于時(shí)序最大池化的主要目的是抓取時(shí)序中最重要的特征虚汛,它通常能使模型不受人為添加字符的影響。
class GlobalMaxPool1d(nn.Module):
def __init__(self):
super(GlobalMaxPool1d, self).__init__()
def forward(self, x):
'''
@params:
x: 輸入皇帮,形狀為 (batch_size, n_channels, seq_len) 的張量
@return: 時(shí)序最大池化后的結(jié)果卷哩,形狀為 (batch_size, n_channels, 1) 的張量
'''
return F.max_pool1d(x, kernel_size=x.shape[2]) # kenerl_size=seq_len
TextCNN 模型
TextCNN 模型主要使用了一維卷積層和時(shí)序最大池化層。假設(shè)輸入的文本序列由 個(gè)詞組成属拾,每個(gè)詞用 維的詞向量表示将谊。那么輸入樣本的寬為 ,輸入通道數(shù)為 渐白。TextCNN 的計(jì)算主要分為以下幾步尊浓。
定義多個(gè)一維卷積核,并使用這些卷積核對(duì)輸入分別做卷積計(jì)算纯衍。寬度不同的卷積核可能會(huì)捕捉到不同個(gè)數(shù)的相鄰詞的相關(guān)性栋齿。
對(duì)輸出的所有通道分別做時(shí)序最大池化,再將這些通道的池化輸出值連結(jié)為向量襟诸。
通過全連接層將連結(jié)后的向量變換為有關(guān)各類別的輸出瓦堵。這一步可以使用丟棄層應(yīng)對(duì)過擬合。
下圖用一個(gè)例子解釋了 TextCNN 的設(shè)計(jì)励堡。這里的輸入是一個(gè)有 11 個(gè)詞的句子,每個(gè)詞用 6 維詞向量表示堡掏。因此輸入序列的寬為 11应结,輸入通道數(shù)為 6。給定 2 個(gè)一維卷積核,核寬分別為 2 和 4鹅龄,輸出通道數(shù)分別設(shè)為 4 和 5揩慕。因此,一維卷積計(jì)算后扮休,4 個(gè)輸出通道的寬為 11?2+1=10迎卤,而其他 5 個(gè)通道的寬為 11?4+1=8。盡管每個(gè)通道的寬不同玷坠,我們依然可以對(duì)各個(gè)通道做時(shí)序最大池化蜗搔,并將 9 個(gè)通道的池化輸出連結(jié)成一個(gè) 9 維向量。最終八堡,使用全連接將 9 維向量變換為 2 維輸出樟凄,即正面情感和負(fù)面情感的預(yù)測。
下面我們來實(shí)現(xiàn) TextCNN 模型兄渺。與上一節(jié)相比缝龄,除了用一維卷積層替換循環(huán)神經(jīng)網(wǎng)絡(luò)外,這里我們還使用了兩個(gè)嵌入層挂谍,一個(gè)的權(quán)重固定叔壤,另一個(gè)則參與訓(xùn)練。
class TextCNN(nn.Module):
def __init__(self, vocab, embed_size, kernel_sizes, num_channels):
'''
@params:
vocab: 在數(shù)據(jù)集上創(chuàng)建的詞典口叙,用于獲取詞典大小
embed_size: 嵌入維度大小
kernel_sizes: 卷積核大小列表
num_channels: 卷積通道數(shù)列表
'''
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(len(vocab), embed_size) # 參與訓(xùn)練的嵌入層
self.constant_embedding = nn.Embedding(len(vocab), embed_size) # 不參與訓(xùn)練的嵌入層
self.pool = GlobalMaxPool1d() # 時(shí)序最大池化層沒有權(quán)重炼绘,所以可以共用一個(gè)實(shí)例
self.convs = nn.ModuleList() # 創(chuàng)建多個(gè)一維卷積層
for c, k in zip(num_channels, kernel_sizes):
self.convs.append(nn.Conv1d(in_channels = 2*embed_size,
out_channels = c,
kernel_size = k))
self.decoder = nn.Linear(sum(num_channels), 2)
self.dropout = nn.Dropout(0.5) # 丟棄層用于防止過擬合
def forward(self, inputs):
'''
@params:
inputs: 詞語下標(biāo)序列,形狀為 (batch_size, seq_len) 的整數(shù)張量
@return:
outputs: 對(duì)文本情感的預(yù)測庐扫,形狀為 (batch_size, 2) 的張量
'''
embeddings = torch.cat((
self.embedding(inputs),
self.constant_embedding(inputs)), dim=2) # (batch_size, seq_len, 2*embed_size)
# 根據(jù)一維卷積層要求的輸入格式饭望,需要將張量進(jìn)行轉(zhuǎn)置
embeddings = embeddings.permute(0, 2, 1) # (batch_size, 2*embed_size, seq_len)
encoding = torch.cat([
self.pool(F.relu(conv(embeddings))).squeeze(-1) for conv in self.convs], dim=1)
# encoding = []
# for conv in self.convs:
# out = conv(embeddings) # (batch_size, out_channels, seq_len-kernel_size+1)
# out = self.pool(F.relu(out)) # (batch_size, out_channels, 1)
# encoding.append(out.squeeze(-1)) # (batch_size, out_channels)
# encoding = torch.cat(encoding) # (batch_size, out_channels_sum)
# 應(yīng)用丟棄法后使用全連接層得到輸出
outputs = self.decoder(self.dropout(encoding))
return outputs
embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
net = TextCNN(vocab, embed_size, kernel_sizes, nums_channels)
訓(xùn)練并評(píng)價(jià)模型
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=lr)
loss = nn.CrossEntropyLoss()
train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)
training on cpu
epoch 1, loss 0.2317, train acc 0.956, test acc 0.782, time 374.0 sec
epoch 2, loss 0.0527, train acc 0.973, test acc 0.780, time 372.5 sec
epoch 3, loss 0.0211, train acc 0.981, test acc 0.783, time 375.3 sec
epoch 4, loss 0.0119, train acc 0.985, test acc 0.788, time 370.7 sec
epoch 5, loss 0.0078, train acc 0.989, test acc 0.791, time 370.8 sec
training on cuda
epoch 1, loss 0.6314, train acc 0.666, test acc 0.803, time 15.9 sec
epoch 2, loss 0.2416, train acc 0.766, test acc 0.807, time 15.9 sec
epoch 3, loss 0.1330, train acc 0.821, test acc 0.849, time 15.9 sec
epoch 4, loss 0.0825, train acc 0.858, test acc 0.860, time 16.0 sec
epoch 5, loss 0.0494, train acc 0.898, test acc 0.865, time 15.9 sec
predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great'])
'positive'
predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad'])
'negative'
數(shù)據(jù)增強(qiáng)
圖像增廣
在5.6節(jié)(深度卷積神經(jīng)網(wǎng)絡(luò))里我們提到過,大規(guī)模數(shù)據(jù)集是成功應(yīng)用深度神經(jīng)網(wǎng)絡(luò)的前提形庭。圖像增廣(image augmentation)技術(shù)通過對(duì)訓(xùn)練圖像做一系列隨機(jī)改變铅辞,來產(chǎn)生相似但又不同的訓(xùn)練樣本,從而擴(kuò)大訓(xùn)練數(shù)據(jù)集的規(guī)模萨醒。圖像增廣的另一種解釋是斟珊,隨機(jī)改變訓(xùn)練樣本可以降低模型對(duì)某些屬性的依賴,從而提高模型的泛化能力富纸。例如囤踩,我們可以對(duì)圖像進(jìn)行不同方式的裁剪,使感興趣的物體出現(xiàn)在不同位置晓褪,從而減輕模型對(duì)物體出現(xiàn)位置的依賴性堵漱。我們也可以調(diào)整亮度、色彩等因素來降低模型對(duì)色彩的敏感度涣仿∏诼可以說示惊,在當(dāng)年AlexNet的成功中,圖像增廣技術(shù)功不可沒愉镰。本節(jié)我們將討論這個(gè)在計(jì)算機(jī)視覺里被廣泛使用的技術(shù)米罚。
首先,導(dǎo)入實(shí)驗(yàn)所需的包或模塊丈探。
import os
os.listdir("/home/kesci/input/img2083/")
out:
['img']
%matplotlib inline
import os
import time
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torchvision
import sys
from PIL import Image
sys.path.append("/home/kesci/input/")
#置當(dāng)前使用的GPU設(shè)備僅為0號(hào)設(shè)備
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
import d2lzh1981 as d2l
# 定義device录择,是否使用GPU,依據(jù)計(jì)算機(jī)配置自動(dòng)會(huì)選擇
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(torch.__version__)
print(device)
1.3.0
cpu
常用的圖像增廣方法
我們來讀取一張形狀為(高和寬分別為400像素和500像素)的圖像作為實(shí)驗(yàn)的樣例碗降。
d2l.set_figsize()
img = Image.open('/home/kesci/input/img2083/img/cat1.jpg')
d2l.plt.imshow(img)
out:
<matplotlib.image.AxesImage at 0x7f8dae7aa198>
下面定義繪圖函數(shù)show_images隘竭。
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def show_images(imgs, num_rows, num_cols, scale=2):
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
for i in range(num_rows):
for j in range(num_cols):
axes[i][j].imshow(imgs[i * num_cols + j])
axes[i][j].axes.get_xaxis().set_visible(False)
axes[i][j].axes.get_yaxis().set_visible(False)
return axes
大部分圖像增廣方法都有一定的隨機(jī)性。為了方便觀察圖像增廣的效果遗锣,接下來我們定義一個(gè)輔助函數(shù)apply货裹。這個(gè)函數(shù)對(duì)輸入圖像img多次運(yùn)行圖像增廣方法aug并展示所有的結(jié)果。
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]
show_images(Y, num_rows, num_cols, scale)
翻轉(zhuǎn)和裁剪
左右翻轉(zhuǎn)圖像通常不改變物體的類別精偿。它是最早也是最廣泛使用的一種圖像增廣方法弧圆。下面我們通過torchvision.transforms模塊創(chuàng)建RandomHorizontalFlip實(shí)例來實(shí)現(xiàn)一半概率的圖像水平(左右)翻轉(zhuǎn)。
apply(img, torchvision.transforms.RandomHorizontalFlip())
上下翻轉(zhuǎn)不如左右翻轉(zhuǎn)通用笔咽。但是至少對(duì)于樣例圖像搔预,上下翻轉(zhuǎn)不會(huì)造成識(shí)別障礙。下面我們創(chuàng)建RandomVerticalFlip實(shí)例來實(shí)現(xiàn)一半概率的圖像垂直(上下)翻轉(zhuǎn)叶组。
apply(img, torchvision.transforms.RandomVerticalFlip())
在我們使用的樣例圖像里拯田,貓?jiān)趫D像正中間,但一般情況下可能不是這樣甩十。在5.4節(jié)(池化層)里我們解釋了池化層能降低卷積層對(duì)目標(biāo)位置的敏感度船庇。除此之外,我們還可以通過對(duì)圖像隨機(jī)裁剪來讓物體以不同的比例出現(xiàn)在圖像的不同位置侣监,這同樣能夠降低模型對(duì)目標(biāo)位置的敏感性鸭轮。
在下面的代碼里,我們每次隨機(jī)裁剪出一塊面積為原面積10%100%的區(qū)域橄霉,且該區(qū)域的寬和高之比隨機(jī)取自0.52窃爷,然后再將該區(qū)域的寬和高分別縮放到200像素。若無特殊說明姓蜂,本節(jié)a中b和之間的隨機(jī)數(shù)指的是從區(qū)間[a,b]中隨機(jī)均勻采樣所得到的連續(xù)值按厘。
shape_aug = torchvision.transforms.RandomResizedCrop(200, scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)
變化顏色
apply(img, torchvision.transforms.ColorJitter(brightness=0.5, contrast=0, saturation=0, hue=0))
我們也可以隨機(jī)變化圖像的色調(diào)。
apply(img, torchvision.transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0.5))
類似地钱慢,我們也可以隨機(jī)變化圖像的對(duì)比度逮京。
apply(img, torchvision.transforms.ColorJitter(brightness=0, contrast=0.5, saturation=0, hue=0))
我們也可以同時(shí)設(shè)置如何隨機(jī)變化圖像的亮度(brightness)、對(duì)比度(contrast)束莫、飽和度(saturation)和色調(diào)(hue)懒棉。
color_aug = torchvision.transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
疊加多個(gè)圖像增廣方法
實(shí)際應(yīng)用中我們會(huì)將多個(gè)圖像增廣方法疊加使用御吞。我們可以通過Compose實(shí)例將上面定義的多個(gè)圖像增廣方法疊加起來,再應(yīng)用到每張圖像之上漓藕。
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)
使用圖像增廣訓(xùn)練模型
下面我們來看一個(gè)將圖像增廣應(yīng)用在實(shí)際訓(xùn)練中的例子。這里我們使用CIFAR-10數(shù)據(jù)集挟裂,而不是之前我們一直使用的Fashion-MNIST數(shù)據(jù)集享钞。這是因?yàn)镕ashion-MNIST數(shù)據(jù)集中物體的位置和尺寸都已經(jīng)經(jīng)過歸一化處理,而CIFAR-10數(shù)據(jù)集中物體的顏色和大小區(qū)別更加顯著诀蓉。下面展示了CIFAR-10數(shù)據(jù)集中前32張訓(xùn)練圖像栗竖。
CIFAR_ROOT_PATH = '/home/kesci/input/cifar102021'
all_imges = torchvision.datasets.CIFAR10(train=True, root=CIFAR_ROOT_PATH, download = True)
# all_imges的每一個(gè)元素都是(image, label)
show_images([all_imges[i][0] for i in range(32)], 4, 8, scale=0.8);
Files already downloaded and verified
為了在預(yù)測時(shí)得到確定的結(jié)果,我們通常只將圖像增廣應(yīng)用在訓(xùn)練樣本上渠啤,而不在預(yù)測時(shí)使用含隨機(jī)操作的圖像增廣狐肢。在這里我們只使用最簡單的隨機(jī)左右翻轉(zhuǎn)。此外沥曹,我們使用ToTensor將小批量圖像轉(zhuǎn)成PyTorch需要的格式份名,即形狀為(批量大小, 通道數(shù), 高, 寬)、值域在0到1之間且類型為32位浮點(diǎn)數(shù)妓美。
flip_aug = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])
no_aug = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])
接下來我們定義一個(gè)輔助函數(shù)來方便讀取圖像并應(yīng)用圖像增廣僵腺。有關(guān)DataLoader的詳細(xì)介紹,可參考更早的3.5節(jié)圖像分類數(shù)據(jù)集(Fashion-MNIST)壶栋。
num_workers = 0 if sys.platform.startswith('win32') else 4
def load_cifar10(is_train, augs, batch_size, root=CIFAR_ROOT_PATH):
dataset = torchvision.datasets.CIFAR10(root=root, train=is_train, transform=augs, download=False)
return DataLoader(dataset, batch_size=batch_size, shuffle=is_train, num_workers=num_workers)
使用圖像增廣訓(xùn)練模型
我們?cè)贑IFAR-10數(shù)據(jù)集上訓(xùn)練5.11節(jié)(殘差網(wǎng)絡(luò))中介紹的ResNet-18模型辰如。
我們先定義train函數(shù)使用GPU訓(xùn)練并評(píng)價(jià)模型。
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def train(train_iter, test_iter, net, loss, optimizer, device, num_epochs):
net = net.to(device)
print("training on ", device)
batch_count = 0
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
optimizer.zero_grad()
l.backward()
optimizer.step()
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = d2l.evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec'
% (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))
然后就可以定義train_with_data_aug函數(shù)使用圖像增廣來訓(xùn)練模型了贵试。該函數(shù)使用Adam算法作為訓(xùn)練使用的優(yōu)化算法琉兜,然后將圖像增廣應(yīng)用于訓(xùn)練數(shù)據(jù)集之上,最后調(diào)用剛才定義的train函數(shù)訓(xùn)練并評(píng)價(jià)模型毙玻。
%% Below, type any markdown to display in the Graffiti tip. %% Then run this cell to save it. train_iter = load_cifar10(True, train_augs, batch_size) test_iter = load_cifar10(False, test_augs, batch_size)
def train_with_data_aug(train_augs, test_augs, lr=0.001):
batch_size, net = 256, d2l.resnet18(10)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
loss = torch.nn.CrossEntropyLoss()
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
train(train_iter, test_iter, net, loss, optimizer, device, num_epochs=10)
下面使用隨機(jī)左右翻轉(zhuǎn)的圖像增廣來訓(xùn)練模型豌蟋。
train_with_data_aug(flip_aug, no_aug)
training on cpu
epoch 1, loss 1.3790, train acc 0.504, test acc 0.554, time 195.8 sec
epoch 2, loss 0.4992, train acc 0.646, test acc 0.592, time 192.5 sec
epoch 3, loss 0.2821, train acc 0.702, test acc 0.657, time 193.7 sec
epoch 4, loss 0.1859, train acc 0.739, test acc 0.693, time 195.4 sec
epoch 5, loss 0.1349, train acc 0.766, test acc 0.688, time 192.6 sec
epoch 6, loss 0.1022, train acc 0.786, test acc 0.701, time 200.2 sec
epoch 7, loss 0.0797, train acc 0.806, test acc 0.720, time 191.8 sec
epoch 8, loss 0.0633, train acc 0.825, test acc 0.695, time 198.6 sec
epoch 9, loss 0.0524, train acc 0.836, test acc 0.693, time 192.1 sec
epoch 10, loss 0.0437, train acc 0.850, test acc 0.769, time 196.3 sec
模型微調(diào)
微調(diào)
在前面的一些章節(jié)中,我們介紹了如何在只有6萬張圖像的Fashion-MNIST訓(xùn)練數(shù)據(jù)集上訓(xùn)練模型淆珊。我們還描述了學(xué)術(shù)界當(dāng)下使用最廣泛的大規(guī)模圖像數(shù)據(jù)集ImageNet夺饲,它有超過1,000萬的圖像和1,000類的物體。然而施符,我們平常接觸到數(shù)據(jù)集的規(guī)模通常在這兩者之間往声。
假設(shè)我們想從圖像中識(shí)別出不同種類的椅子,然后將購買鏈接推薦給用戶戳吝。一種可能的方法是先找出100種常見的椅子浩销,為每種椅子拍攝1,000張不同角度的圖像,然后在收集到的圖像數(shù)據(jù)集上訓(xùn)練一個(gè)分類模型听哭。這個(gè)椅子數(shù)據(jù)集雖然可能比Fashion-MNIST數(shù)據(jù)集要龐大慢洋,但樣本數(shù)仍然不及ImageNet數(shù)據(jù)集中樣本數(shù)的十分之一塘雳。這可能會(huì)導(dǎo)致適用于ImageNet數(shù)據(jù)集的復(fù)雜模型在這個(gè)椅子數(shù)據(jù)集上過擬合。同時(shí)普筹,因?yàn)閿?shù)據(jù)量有限败明,最終訓(xùn)練得到的模型的精度也可能達(dá)不到實(shí)用的要求。
為了應(yīng)對(duì)上述問題太防,一個(gè)顯而易見的解決辦法是收集更多的數(shù)據(jù)妻顶。然而,收集和標(biāo)注數(shù)據(jù)會(huì)花費(fèi)大量的時(shí)間和資金蜒车。例如讳嘱,為了收集ImageNet數(shù)據(jù)集,研究人員花費(fèi)了數(shù)百萬美元的研究經(jīng)費(fèi)酿愧。雖然目前的數(shù)據(jù)采集成本已降低了不少沥潭,但其成本仍然不可忽略。
另外一種解決辦法是應(yīng)用遷移學(xué)習(xí)(transfer learning)嬉挡,將從源數(shù)據(jù)集學(xué)到的知識(shí)遷移到目標(biāo)數(shù)據(jù)集上钝鸽。例如,雖然ImageNet數(shù)據(jù)集的圖像大多跟椅子無關(guān)庞钢,但在該數(shù)據(jù)集上訓(xùn)練的模型可以抽取較通用的圖像特征寞埠,從而能夠幫助識(shí)別邊緣、紋理焊夸、形狀和物體組成等仁连。這些類似的特征對(duì)于識(shí)別椅子也可能同樣有效。
本節(jié)我們介紹遷移學(xué)習(xí)中的一種常用技術(shù):微調(diào)(fine tuning)阱穗。如圖9.1所示饭冬,微調(diào)由以下4步構(gòu)成。
- 在源數(shù)據(jù)集(如ImageNet數(shù)據(jù)集)上預(yù)訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò)模型揪阶,即源模型昌抠。
- 創(chuàng)建一個(gè)新的神經(jīng)網(wǎng)絡(luò)模型,即目標(biāo)模型鲁僚。它復(fù)制了源模型上除了輸出層外的所有模型設(shè)計(jì)及其參數(shù)炊苫。我們假設(shè)這些模型參數(shù)包含了源數(shù)據(jù)集上學(xué)習(xí)到的知識(shí),且這些知識(shí)同樣適用于目標(biāo)數(shù)據(jù)集冰沙。我們還假設(shè)源模型的輸出層跟源數(shù)據(jù)集的標(biāo)簽緊密相關(guān)侨艾,因此在目標(biāo)模型中不予采用。
- 為目標(biāo)模型添加一個(gè)輸出大小為目標(biāo)數(shù)據(jù)集類別個(gè)數(shù)的輸出層拓挥,并隨機(jī)初始化該層的模型參數(shù)唠梨。
-
在目標(biāo)數(shù)據(jù)集(如椅子數(shù)據(jù)集)上訓(xùn)練目標(biāo)模型。我們將從頭訓(xùn)練輸出層侥啤,而其余層的參數(shù)都是基于源模型的參數(shù)微調(diào)得到的当叭。
當(dāng)目標(biāo)數(shù)據(jù)集遠(yuǎn)小于源數(shù)據(jù)集時(shí)茬故,微調(diào)有助于提升模型的泛化能力。
熱狗識(shí)別
接下來我們來實(shí)踐一個(gè)具體的例子:熱狗識(shí)別蚁鳖。我們將基于一個(gè)小數(shù)據(jù)集對(duì)在ImageNet數(shù)據(jù)集上訓(xùn)練好的ResNet模型進(jìn)行微調(diào)磺芭。該小數(shù)據(jù)集含有數(shù)千張包含熱狗和不包含熱狗的圖像。我們將使用微調(diào)得到的模型來識(shí)別一張圖像中是否包含熱狗醉箕。
首先徘跪,導(dǎo)入實(shí)驗(yàn)所需的包或模塊。torchvision的models
包提供了常用的預(yù)訓(xùn)練模型琅攘。如果希望獲取更多的預(yù)訓(xùn)練模型,可以使用使用pretrained-models.pytorch
倉庫松邪。
%matplotlib inline
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torchvision
from torchvision.datasets import ImageFolder
from torchvision import transforms
from torchvision import models
import os
import sys
sys.path.append("/home/kesci/input/")
import d2lzh1981 as d2l
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
獲取數(shù)據(jù)集
我們使用的熱狗數(shù)據(jù)集(點(diǎn)擊下載)是從網(wǎng)上抓取的坞琴,它含有1400張包含熱狗的正類圖像,和同樣多包含其他食品的負(fù)類圖像逗抑。各類的1000張圖像被用于訓(xùn)練剧辐,其余則用于測試。
我們首先將壓縮后的數(shù)據(jù)集下載到路徑data_dir
之下邮府,然后在該路徑將下載好的數(shù)據(jù)集解壓荧关,得到兩個(gè)文件夾hotdog/train
和hotdog/test
。這兩個(gè)文件夾下面均有hotdog
和not-hotdog
兩個(gè)類別文件夾褂傀,每個(gè)類別文件夾里面是圖像文件忍啤。
import os
os.listdir('/home/kesci/input/resnet185352')
out
['resnet18-5c106cde.pth']
data_dir = '/home/kesci/input/hotdog4014'
os.listdir(os.path.join(data_dir, "hotdog"))
out
['test', 'train']
我們創(chuàng)建兩個(gè)ImageFolder實(shí)例來分別讀取訓(xùn)練數(shù)據(jù)集和測試數(shù)據(jù)集中的所有圖像文件。
train_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/train'))
test_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/test'))
下面畫出前8張正類圖像和最后8張負(fù)類圖像仙辟⊥ǎ可以看到,它們的大小和高寬比各不相同叠国。
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);
在訓(xùn)練時(shí)未檩,我們先從圖像中裁剪出隨機(jī)大小和隨機(jī)高寬比的一塊隨機(jī)區(qū)域,然后將該區(qū)域縮放為高和寬均為224像素的輸入粟焊。測試時(shí)冤狡,我們將圖像的高和寬均縮放為256像素,然后從中裁剪出高和寬均為224像素的中心區(qū)域作為輸入项棠。此外悲雳,我們對(duì)RGB(紅、綠香追、藍(lán))三個(gè)顏色通道的數(shù)值做標(biāo)準(zhǔn)化:每個(gè)數(shù)值減去該通道所有數(shù)值的平均值怜奖,再除以該通道所有數(shù)值的標(biāo)準(zhǔn)差作為輸出。
注: 在使用預(yù)訓(xùn)練模型時(shí)翅阵,一定要和預(yù)訓(xùn)練時(shí)作同樣的預(yù)處理歪玲。 如果你使用的是torchvision的models迁央,那就要求: All pre-trained models expect input images normalized in the same way, i.e. mini-batches of 3-channel RGB images of shape (3 x H x W), where H and W are expected to be at least 224. The images have to be loaded in to a range of [0, 1] and then normalized using mean = [0.485, 0.456, 0.406] and std = [0.229, 0.224, 0.225].
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
train_augs = transforms.Compose([
transforms.RandomResizedCrop(size=224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize
])
test_augs = transforms.Compose([
transforms.Resize(size=256),
transforms.CenterCrop(size=224),
transforms.ToTensor(),
normalize
])
定義和初始化模型
我們使用在ImageNet數(shù)據(jù)集上預(yù)訓(xùn)練的ResNet-18作為源模型。這里指定pretrained=True來自動(dòng)下載并加載預(yù)訓(xùn)練的模型參數(shù)滥崩。在第一次使用時(shí)需要聯(lián)網(wǎng)下載模型參數(shù)岖圈。
pretrained_net = models.resnet18(pretrained=False)
pretrained_net.load_state_dict(torch.load('/home/kesci/input/resnet185352/resnet18-5c106cde.pth'))
out
<All keys matched successfully>
下面打印源模型的成員變量fc。作為一個(gè)全連接層钙皮,它將ResNet最終的全局平均池化層輸出變換成ImageNet數(shù)據(jù)集上1000類的輸出蜂科。
print(pretrained_net.fc)
Linear(in_features=512, out_features=1000, bias=True)
可見此時(shí)pretrained_net最后的輸出個(gè)數(shù)等于目標(biāo)數(shù)據(jù)集的類別數(shù)1000。所以我們應(yīng)該將最后的fc成修改我們需要的輸出類別數(shù):
pretrained_net.fc = nn.Linear(512, 2)
print(pretrained_net.fc)
Linear(in_features=512, out_features=2, bias=True)
此時(shí)短条,pretrained_net的fc層就被隨機(jī)初始化了导匣,但是其他層依然保存著預(yù)訓(xùn)練得到的參數(shù)集侯。由于是在很大的ImageNet數(shù)據(jù)集上預(yù)訓(xùn)練的速妖,所以參數(shù)已經(jīng)足夠好,因此一般只需使用較小的學(xué)習(xí)率來微調(diào)這些參數(shù)碟狞,而fc中的隨機(jī)初始化參數(shù)一般需要更大的學(xué)習(xí)率從頭訓(xùn)練可都。PyTorch可以方便的對(duì)模型的不同部分設(shè)置不同的學(xué)習(xí)參數(shù)缓待,我們?cè)谙旅娲a中將fc的學(xué)習(xí)率設(shè)為已經(jīng)預(yù)訓(xùn)練過的部分的10倍。
output_params = list(map(id, pretrained_net.fc.parameters()))
feature_params = filter(lambda p: id(p) not in output_params, pretrained_net.parameters())
lr = 0.01
optimizer = optim.SGD([{'params': feature_params},
{'params': pretrained_net.fc.parameters(), 'lr': lr * 10}],
lr=lr, weight_decay=0.001)
微調(diào)模型
def train_fine_tuning(net, optimizer, batch_size=128, num_epochs=5):
train_iter = DataLoader(ImageFolder(os.path.join(data_dir, 'hotdog/train'), transform=train_augs),
batch_size, shuffle=True)
test_iter = DataLoader(ImageFolder(os.path.join(data_dir, 'hotdog/test'), transform=test_augs),
batch_size)
loss = torch.nn.CrossEntropyLoss()
d2l.train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)
train_fine_tuning(pretrained_net, optimizer)
training on cpu
epoch 1, loss 3.4516, train acc 0.687, test acc 0.884, time 298.2 sec
epoch 2, loss 0.1550, train acc 0.924, test acc 0.895, time 296.2 sec
epoch 3, loss 0.1028, train acc 0.903, test acc 0.950, time 295.0 sec
epoch 4, loss 0.0495, train acc 0.931, test acc 0.897, time 294.0 sec
epoch 5, loss 0.1454, train acc 0.878, test acc 0.939, time 291.0 sec
作為對(duì)比渠牲,我們定義一個(gè)相同的模型旋炒,但將它的所有模型參數(shù)都初始化為隨機(jī)值。由于整個(gè)模型都需要從頭訓(xùn)練签杈,我們可以使用較大的學(xué)習(xí)率瘫镇。
scratch_net = models.resnet18(pretrained=False, num_classes=2)
lr = 0.1
optimizer = optim.SGD(scratch_net.parameters(), lr=lr, weight_decay=0.001)
train_fine_tuning(scratch_net, optimizer)
training on cpu
epoch 1, loss 2.6391, train acc 0.598, test acc 0.734, time 292.4 sec
epoch 2, loss 0.2703, train acc 0.790, test acc 0.632, time 289.7 sec
epoch 3, loss 0.1584, train acc 0.810, test acc 0.825, time 290.2 sec
epoch 4, loss 0.1177, train acc 0.805, test acc 0.787, time 288.6 sec
epoch 5, loss 0.0782, train acc 0.829, test acc 0.828, time 289.8 sec
輸出:
raining on cuda
epoch 1, loss 2.6686, train acc 0.582, test acc 0.556, time 25.3 sec
epoch 2, loss 0.2434, train acc 0.797, test acc 0.776, time 25.3 sec
epoch 3, loss 0.1251, train acc 0.845, test acc 0.802, time 24.9 sec
epoch 4, loss 0.0958, train acc 0.833, test acc 0.810, time 25.0 sec
epoch 5, loss 0.0757, train acc 0.836, test acc 0.780, time 24.9 sec