8.文本分类、数据增强与数据微调

文本分类

文本情感分类数据

我们使用斯坦福的IMDb数据集(Stanford’s Large Movie Review Dataset)作为文本情感分类的数据集。

读取数据

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

预处理数据

读取数据后,我们先根据文本的格式进行单词的切分,再利用 torchtext.vocab.Vocab 创建词典。

def get_tokenized_imdb(data):  #分词
    def tokenizer(text):
        return [tok.lower() for tok in text.split(' ')]    
    return [tokenizer(review) for review, _ in data]

def get_vocab_imdb(data):  # 建立词典
    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)

def preprocess_imdb(data, vocab):  # 索引化并截断或补足训练集
    max_l = 500  # 将每条评论通过截断或者补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

创建数据迭代器

利用 torch.utils.data.TensorDataset,可以创建 PyTorch 格式的数据集,从而创建数据迭代器。

train_set = Data.TensorDataset(*preprocess_imdb(train_data, vocab))
test_set = Data.TensorDataset(*preprocess_imdb(test_data, vocab))

batch_size = 64
train_iter = Data.DataLoader(train_set, batch_size, shuffle=True)
test_iter = Data.DataLoader(test_set, batch_size)

使用循环神经网络

双向循环神经网络

在这里插入图片描述 H → t = ϕ ( X t W x h ( f ) + H → t − 1 W h h ( f ) + b h ( f ) ) H ← t = ϕ ( X t W x h ( b ) + H ← t + 1 W h h ( b ) + b h ( b ) ) \begin{aligned} &\overrightarrow{\boldsymbol{H}}_{t}=\phi\left(\boldsymbol{X}_{t} \boldsymbol{W}_{x h}^{(f)}+\overrightarrow{\boldsymbol{H}}_{t-1} \boldsymbol{W}_{h h}^{(f)}+\boldsymbol{b}_{h}^{(f)}\right)\\ &\overleftarrow{\boldsymbol{H}}_{t}=\phi\left(\boldsymbol{X}_{t} \boldsymbol{W}_{x h}^{(b)}+\overleftarrow{\boldsymbol{H}}_{t+1} \boldsymbol{W}_{h h}^{(b)}+\boldsymbol{b}_{h}^{(b)}\right) \end{aligned} H t=ϕ(XtWxh(f)+H t1Whh(f)+bh(f))H t=ϕ(XtWxh(b)+H t+1Whh(b)+bh(b))
其中 X t ∈ R n × d \boldsymbol{X}_t\in\mathbb{R}^{n\times d} XtRn×d为时间步(批量大小为 n n n,输入维度为 d d d), H → t ∈ R n × h , H ← t ∈ R n × h \overrightarrow{\boldsymbol{H}}_{t} \in \mathbb{R}^{n \times h},\overleftarrow{\boldsymbol{H}}_{t} \in \mathbb{R}^{n \times h} H tRn×h,H tRn×h
权重 W x h ( f ) ∈ R d × h , W h h ( f ) ∈ R h × h , W x h ( b ) ∈ R d × h , W h h ( b ) ∈ R h × h \boldsymbol{W}_{x h}^{(f)} \in \mathbb{R}^{d \times h}, \boldsymbol{W}_{h h}^{(f)} \in \mathbb{R}^{h \times h}, \boldsymbol{W}_{x h}^{(b)} \in \mathbb{R}^{d \times h}, \boldsymbol{W}_{h h}^{(b)} \in \mathbb{R}^{h \times h} Wxh(f)Rd×h,Whh(f)Rh×h,Wxh(b)Rd×h,Whh(b)Rh×h和偏差 b h ( f ) ∈ R 1 × h , b h ( b ) ∈ R 1 × h \boldsymbol{b}_{h}^{(f)} \in \mathbb{R}^{1 \times h}, \boldsymbol{b}_{h}^{(b)} \in \mathbb{R}^{1 \times h} bh(f)R1×h,bh(b)R1×h均未模型参数, ϕ \phi ϕ为隐藏层激活函数。
然后我们连结两个方向的隐藏状态 H → t \overrightarrow{\boldsymbol{H}}_{t} H t H ← t \overleftarrow{\boldsymbol{H}}_{t} H t来得到隐藏状态 H t ∈ R n × 2 h \boldsymbol{H}_{t} \in \mathbb{R}^{n \times 2 h} HtRn×2h,并将其输入到输出层。输出层计算输出 O t ∈ R n × q \boldsymbol{O}_{t} \in \mathbb{R}^{n \times q} OtRn×q(输出维度为 q q q):
O t = H t W h q + b q \boldsymbol{O}_{t}=\boldsymbol{H}_{t} \boldsymbol{W}_{h q}+\boldsymbol{b}_{q} Ot=HtWhq+bq
权重 W h q ∈ R 2 h × q \boldsymbol{W}_{h q} \in \mathbb{R}^{2 h \times q} WhqR2h×q,偏差 b q ∈ R 1 × q \boldsymbol{b}_{q} \in \mathbb{R}^{1 \times q} bqR1×q
利用 torch.nn.RNN 或 torch.nn.LSTM 模组,我们可以很方便地实现双向循环神经网络,下面是以 LSTM 为例的代码。

class BiRNN(nn.Module):
    def __init__(self, vocab, embed_size, num_hiddens, num_layers):
        super(BiRNN, self).__init__()
        self.embedding = nn.Embedding(len(vocab), embed_size)
        self.encoder = nn.LSTM(input_size=embed_size, 
                                hidden_size=num_hiddens, 
                                num_layers=num_layers,
                                bidirectional=True)  # bidirectional设为True即得到双向循环神经网络
        self.decoder = nn.Linear(4*num_hiddens, 2) # 初始时间步和最终时间步的隐藏状态作为全连接层输入
        
    def forward(self, inputs):
        '''
        @params:
            inputs: 词语下标序列,形状为 (batch_size, seq_len) 的整数张量
        @return:
            outs: 对文本情感的预测,形状为 (batch_size, 2) 的张量
        '''
        # 因为LSTM需要将序列长度(seq_len)作为第一维,所以需要将输入转置
        embeddings = self.embedding(inputs.permute(1, 0)) # (seq_len, batch_size, d)
        # rnn.LSTM 返回输出、隐藏状态和记忆单元,格式如 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

加载预训练的词向量

由于预训练词向量的词典及词语索引与我们使用的数据集并不相同,所以需要根据目前的词典及索引的顺序来加载预训练词向量。

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: 预训练词向量
    @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 # 直接加载预训练好的, 所以不需要更新它

训练模型

训练时可以调用之前编写的 train 及 evaluate_accuracy 函数。

评价模型

def predict_sentiment(net, vocab, sentence):
    device = list(net.parameters())[0].device # 读取模型所在的环境
    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'])

使用卷积神经网络

一维卷积层

在这里插入图片描述

def corr1d(X, K):
    w = K.shape[0] # 卷积窗口宽度
    Y = torch.zeros((X.shape[0] - w + 1))
    for i in range(Y.shape[0]): # 滑动窗口
        Y[i] = (X[i: i + w] * K).sum()
    return Y

在这里插入图片描述

def corr1d_multi_in(X, K):
    # 首先沿着X和K的通道维遍历并计算一维互相关结果。然后将所有结果堆叠起来沿第0维累加
    return torch.stack([corr1d(x, k) for x, k in zip(X, K)]).sum(dim=0)

时序最大池化层

TextCNN 中使用的时序最大池化(max-over-time pooling)层实际上对应一维全局最大池化层:假设输入包含多个通道,各通道由不同时间步上的数值组成,各通道的输出即该通道所有时间步中最大的数值。
因此,时序最大池化层的输入在各个通道上的时间步数可以不同。
但一般来说,为提升计算性能,我们常常将不同长度的时序样本组成一个小批量,并通过在较短序列后附加特殊字符(如0)令批量中各时序样本长度相同。这些人为添加的特殊字符当然是无意义的。由于时序最大池化的主要目的是抓取时序中最重要的特征,它通常能使模型不受人为添加字符的影响。
在这里插入图片描述

class GlobalMaxPool1d(nn.Module):
    def __init__(self):
        super(GlobalMaxPool1d, self).__init__()
    def forward(self, x):
        '''
        @params:
            x: 输入,形状为 (batch_size, n_channels, seq_len) 的张量
        @return: 时序最大池化后的结果,形状为 (batch_size, n_channels, 1) 的张量
        '''
        return F.max_pool1d(x, kernel_size=x.shape[2])  # kenerl_size=seq_len

TextCNN 模型

1、定义多个一维卷积核,并使用这些卷积核对输入分别做卷积计算。宽度不同的卷积核可能会捕捉到不同个数的相邻词的相关性。
2.对输出的所有通道分别做时序最大池化,再将这些通道的池化输出值连结为向量。
3.通过全连接层将连结后的向量变换为有关各类别的输出。这一步可以使用丢弃层应对过拟合。
在这里插入图片描述
与上一个模型相比,TextCNN 模型除了用一维卷积层替换循环神经网络外,还使用了两个嵌入层,一个的权重固定,另一个则参与训练。

class TextCNN(nn.Module):
    def __init__(self, vocab, embed_size, kernel_sizes, num_channels):
        super(TextCNN, self).__init__()
        self.embedding = nn.Embedding(len(vocab), embed_size) # 参与训练的嵌入层
        self.constant_embedding = nn.Embedding(len(vocab), embed_size) # 不参与训练的嵌入层
        
        self.pool = GlobalMaxPool1d() # 时序最大池化层没有权重,所以可以共用一个实例
        self.convs = nn.ModuleList()  # 创建多个一维卷积层
        for c, k in zip(num_channels, kernel_sizes):
            self.convs.append(nn.Conv1d(in_channels = 2*embed_size,  # 注意用的是一维卷积conv1d
                                        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: 词语下标序列,形状为 (batch_size, seq_len) 的整数张量
        @return:
            outputs: 对文本情感的预测,形状为 (batch_size, 2) 的张量
        '''
        embeddings = torch.cat((
            self.embedding(inputs), 
            self.constant_embedding(inputs)), dim=2) # (batch_size, seq_len, 2*embed_size)
        # 根据一维卷积层要求的输入格式,需要将张量进行转置 2*embed_size相当于通道数
        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)
        
        # 应用丢弃法后使用全连接层得到输出
        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)

训练并评价模型

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)

数据增强

图像增广

1.图像增广(image augmentation)技术通过对训练图像做一系列随机改变,来产生相似但又不同的训练样本,从而扩大训练数据集的规模。
2.图像增广的另一种解释是,随机改变训练样本可以降低模型对某些属性的依赖,从而提高模型的泛化能力。

常用的图像增广方法

torchvision.transforms.RandomHorizontalFlip()  # 左右翻转
torchvision.transforms.RandomVerticalFlip()  # 上下翻转
torchvision.transforms.Resize(size=256),
torchvision.transforms.CenterCrop(size=224),
shape_aug = torchvision.transforms.RandomResizedCrop(200, scale=(0.1, 1), ratio=(0.5, 2)  #长宽比裁剪与缩放
color_aug = torchvision.transforms.ColorJitter(brightness=0.5, contrast=0, saturation=0, hue=0)  # 变化颜色

叠加多个图像增广方法

augs = torchvision.transforms.Compose([
    torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])

使用图像增广训练模型

flip_aug = torchvision.transforms.Compose([
     torchvision.transforms.RandomHorizontalFlip(),
     torchvision.transforms.ToTensor()])
no_aug = torchvision.transforms.Compose([
     torchvision.transforms.ToTensor()])

数据微调

获取数据集

import os
data_dir = '/home/kesci/input/hotdog4014'
os.listdir(os.path.join(data_dir, "hotdog"))
train_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/train'))
test_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/test'))
hotdogs = [train_imgs[i][0] for i in range(8)]  # ImageFolder返回的结果
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
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
    ])

定义和初始化模型

我们使用在ImageNet数据集上预训练的ResNet-18作为源模型。
这里指定pretrained=True来自动下载并加载预训练的模型参数,在第一次使用时需要联网下载模型参数。

pretrained_net = models.resnet18(pretrained=False)
pretrained_net.load_state_dict(torch.load('/home/kesci/input/resnet185352/resnet18-5c106cde.pth'))
pretrained_net.fc = nn.Linear(512, 2)  # 将全连接层改为输出2类,判别是否是hotdog
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)  #输出参数的lr是特征参数的10倍
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值