编码器—解码器(seq2seq)

当输入和输出都是不定长序列时(比如机器翻译),我们可以使用编码器—解码器(encoder-decoder) 或者seq2seq模型。这两个模型本质上都用到了两个循环神经网络,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。

以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列,例如

英语输入:“They”、“are”、“watching”、“.”

法语输出:“Ils”、“regardent”、“.”

下图描述了使用编码器—解码器将上述英语句子翻译成法语句子的一种方法。在训练数据集中,我们可以在每个句子后附上特殊符号“<eos>”(end of sequence)以表示序列的终止。编码器每个时间步的输入依次为英语句子中的单词、标点和特殊符号“<eos>”。下图中使用了编码器在最终时间步的隐藏状态作为输入句子的表征或编码信息。解码器在各个时间步中使用输入句子的编码信息和上个时间步的输出以及隐藏状态作为输入。我们希望解码器在各个时间步能正确依次输出翻译后的法语单词、标点和特殊符号”<eos>”。需要注意的是,解码器在最初时间步的输入用到了一个表示序列开始的特殊符号”<bos>”(beginning of sequence)。

使用编码器—解码器将句子由英语翻译成法语。编码器和解码器分别为循环神经网络

编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量c,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。

根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率,并得到该输出序列的损失,在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。

个人理解:

最后decoder每一步输出的是一个字典大小的概率值向量,分别表示这一步输出所有值的概率,一般取最大的值作为输出。

字典大小为4

因此那么在模型预测的时候就需要进行搜索,选择不同的搜索方式决定每一时间步的输出是字典里的那个值,选择不同的值会影响下一时间步的输出概率。

模型预测 Model Prediction

为了搜索该条件概率最大的输出序列,一种方法是穷举所有可能输出序列的条件概率,并输出条件概率最大的序列。我们将该序列称为最优序列,并将这种搜索方法称为穷举搜索 (exhaustive search)。

贪婪搜索 Greedy Search

我们还可以使用贪婪搜索 (greedy search) 。也就是说,对于输出序列任一时间步 [公式] ,从 [公式] 个词中搜索出输出词

[公式]

且一旦搜索出 “<eos>” 符号即完成输出序列。贪婪搜索的计算开销是 [公式] 。它比起穷举搜索的计算开销显著下降。例如,当 [公式] 且 [公式] 时,我们只需评估 [公式] 个序列。

下面我们来看一个例子。假设输出词典里面有 “A”、“B”、“C”和 “<eos>” 这四个词。下图中每个时间步下的四个数字分别代表了该时间步生成 “A”、“B”、“C”和 “<eos>” 这四个词的条件概率。在每个时间步,贪婪搜索选取生成条件概率最大的词。因此,将生成序列 “ABC<eos>” 。该输出序列的条件概率是 [公式] 。

束搜索 Beam Search

束搜索 (beam search) 是比贪婪搜索更加广义的搜索算法。它有一个束宽 (beam size) 超参数。我们将它设为 [公式] 。在时间步1时,选取当前时间步生成条件概率最大的 [公式] 个词,分别组成 [公式] 个候选输出序列的首词。在之后的每个时间步,基于上个时间步的 [公式] 个候选输出序列,从 [公式] 个可能的输出序列中选取生成条件概率最大的 [公式] 个,作为该时间步的候选输出序列。

最终,我们在各个时间步的候选输出序列中筛选出包含特殊符号 “<eos>” 的序列,并将它们中所有特殊符号 “<eos>” 后面的子序列舍弃,得到最终候选输出序列。在这些最终候选输出序列中,取以下分数最高的序列作为输出序列:

[公式]
[公式]

其中 [公式] 为最终候选序列长度, [公式] 一般可选为0.75。分母上的 是为了惩罚较长序列在以上分数中较多的对数相加项。分析可得,束搜索的计算开销为 [公式] 。这介于穷举搜索和贪婪搜索的计算开销之间。

  • 预测不定长序列的方法包括贪婪搜索、穷举搜索和束搜索。
  • 束搜索通过灵活的束宽来权衡计算开销和搜索质量。

注意力机制

在普通的编码器-解码器模型中,有一个很大的局限性。那就是上下文变量对于 Decoding 阶段每个时间步都是一样的,这可能是模型性能的一个瓶颈。我们希望不同时间步的解码能够依赖于与之更相关的上下文信息,换句话说,Decoding 往往并不需要整个输入序列的信息,而是要有所侧重。于是,Bengio 团队的 Bahdanau 在 2014年首次在编码器-解码器模型中引入了注意力机制 (Attention Mechanism):

注意力机制通过注意力汇聚将查询(自主性提示)和键(非自主性提示)结合在一起,实现对值(感官输入)的选择倾向

动机 Motivation

以英语-法语翻译为例,给定一对英语输入序列 “They”、“are”、“watching”、“.” 和法语输出序列 “Ils”、“regardent”、“.”。解码器可以在输出序列的时间步1使用更集中编码了 “They”、“are” 信息的上下文变量来生成 “Ils”,在时间步2使用更集中编码了 “watching” 信息的上下文变量来生成“regardent”,在时间步3使用更集中编码了 “.” 信息的上下文变量来生成 “.”。这看上去就像是在解码器的每一时间步对输入序列中不同时间步编码的信息分配不同的注意力。这也是注意力机制的由来。它最早由 Bahanau 等人提出。

仍然以循环神经网络为例,注意力机制通过对编码器所有时间步的隐藏状态做加权平均来得到背景变量。解码器在每一时间步调整这些权重,即注意力权重,从而能够在不同时间步分别关注输入序列中的不同部分并编码进相应时间步的背景变量。

我们先描述第一个关键点,即计算背景变量。下图描绘了注意力机制如何为解码器在时间步2计算背景变量。首先,函数a根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。

本质上,注意力机制能够为表征中较有价值的部分分配较多的计算资源。这个有趣的想法自提出后得到了快速发展,特别是启发了依靠注意力机制来编码输入序列并解码出输出序列的变换器(Transformer)模型的设计 [2]。变换器抛弃了卷积神经网络和循环神经网络的架构。它在计算效率上比基于循环神经网络的编码器—解码器模型通常更具明显优势。含注意力机制的变换器的编码结构在后来的BERT预训练模型中得以应用并令后者大放异彩:微调后的模型在多达11项自然语言处理任务中取得了当时最先进的结果 [3]。不久后,同样是基于变换器设计的GPT-2模型于新收集的语料数据集预训练后,在7个未参与训练的语言模型数据集上均取得了当时最先进的结果 [4]。除了自然语言处理领域,注意力机制还被广泛用于图像分类、自动图像描述、唇语解读以及语音识别

评价机器翻译结果

评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

总结 Conclusions

让我们回顾一下带注意力机制的编码器-解码器的整个设计:

  1. Encoder 总结输入序列的信息,得到上下文变量 [公式]
  2. Decoder 将上下文变量 [公式] 中的信息解码生成输出序列
  3. 设计 [公式] 函数
  4. 计算当前时间步的隐藏状态 [公式]
  5. 计算当前时间步的解码器输出概率 [公式]
  6. 得到输出序列的联合概率 [公式] 并最大化
  7. 根据 MLE,就是最小化联合概率的负对数
  8. 得到 loss function
  9. 用优化方法降低 loss,学习模型参数
  10. 为了避免相同的上下文变量对模型性能的限制,给编码器-解码器模型加入了注意力机制。

Convolutional Neural Networks for Sentence Classification

https://arxiv.org/abs/1408.5882

github实现

https://github.com/yoonkim/CNN_sentence

https://github.com/Cheneng/TextCNN

对于文本分类,我们能不能用CNN来做,用某种模型初始化,进而做fine-tune呢?答案是肯定的,用于文本分析的CNN—TextCNN。

text-cnn用于情感分类:

与二维卷积层一样,一维卷积层使用一维的互相关运算。在一维互相关运算中,卷积窗口从输入数组的最左方开始,按从左往右的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。

多输入通道的一维互相关运算也与多输入通道的二维互相关运算类似:在每个通道上,将核与相应的输入做一维互相关运算,并将通道之间的结果相加得到输出结果。

由二维互相关运算的定义可知,多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。

类似地,我们有一维池化层。textCNN中使用的时序最大池化(max-over-time pooling)层实际上对应一维全局最大池化层:假设输入包含多个通道,各通道由不同时间步上的数值组成,各通道的输出即该通道所有时间步中最大的数值。因此,时序最大池化层的输入在各个通道上的时间步数可以不同。

简单来说,时序最大池化层就是沿着时序方向进行最大池化。

textCNN模型主要使用了一维卷积层和时序最大池化层。假设输入的文本序列由n个词组成,每个词用d维的词向量表示。那么输入样本的宽为n,高为1,输入通道数为d。textCNN的计算主要分为以下几步。(输入通道就是每个词的d为维度表示,宽就是时序长度)

词用d维的词向量表示 :一般使用词嵌入模型word2vec.

  1. 定义多个一维卷积核,并使用这些卷积核对输入分别做卷积计算。宽度不同的卷积核可能会捕捉到不同个数的相邻词的相关性。
  2. 对输出的所有通道分别做时序最大池化,再将这些通道的池化输出值连结为向量。
  3. 通过全连接层将连结后的向量变换为有关各类别的输出。这一步可以使用丢弃层应对过拟合。

下图用一个例子解释了textCNN的设计。这里的输入是一个有11个词的句子,每个词用6维词向量表示。因此输入序列的宽为11,输入通道数为6。给定2个一维卷积核,核宽分别为2和4,输出通道数分别设为4和5。因此,一维卷积计算后,4个输出通道的宽为11−2+1=10,而其他5个通道的宽为11−4+1=8。尽管每个通道的宽不同,我们依然可以对各个通道做时序最大池化,并将9个通道的池化输出连结成一个9维向量。最终,使用全连接将9维向量变换为2维输出,即正面情感和负面情感的预测。

Dive-into-DL-PyTorch
pytorch代码实现:
https://github.com/chenpaopao/TextCNN

总结:

  • 可以使用一维卷积来表征时序数据。
  • 多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。
  • 时序最大池化层的输入在各个通道上的时间步数可以不同。
  • textCNN主要使用了一维卷积层和时序最大池化层。
https://www.pexels.com/zh-cn/photo/977739/

数据扩充和增广

chenpaopao

最近在学习 torch,对于图像数据的预处理, torchvision 提供了torchvision.transforms 模块,用于预处理。

  1. 1. 裁剪——Crop 中心裁剪:transforms.CenterCrop 随机裁剪:transforms.RandomCrop 随机长宽比裁剪:transforms.RandomResizedCrop 上下左右中心裁剪:transforms.FiveCrop 上下左右中心裁剪后翻转,transforms.TenCrop
  2. 2. 翻转和旋转——Flip and Rotation 依概率p水平翻转:transforms.RandomHorizontalFlip(p=0.5) 依概率p垂直翻转:transforms.RandomVerticalFlip(p=0.5) 随机旋转:transforms.RandomRotation
  3. 3. 图像变换 resize:transforms.Resize 标准化:transforms.Normalize 转为tensor,并归一化至[0-1]:transforms.ToTensor 填充:transforms.Pad 修改亮度、对比度和饱和度:transforms.ColorJitter 转灰度图:transforms.Grayscale 线性变换:transforms.LinearTransformation() 仿射变换:transforms.RandomAffine 依概率p转为灰度图:transforms.RandomGrayscale 将数据转换为PILImage:transforms.ToPILImage transforms.Lambda:Apply a user-defined lambda as a transform.
  4. 4. 对transforms操作,使数据增强更灵活 transforms.RandomChoice(transforms), 从给定的一系列transforms中选一个进行操作 transforms.RandomApply(transforms, p=0.5),给一个transform加上概率,依概率进行操作 transforms.RandomOrder,将transforms中的操作随机打乱

此外,还提供了 torchvision.transforms.Compose( ),可以同时传递多个函数

mytransform = transforms.Compose([
transforms.ToTensor()
]
)

# torch.utils.data.DataLoader
cifarSet = torchvision.datasets.CIFAR10(root = "../data/cifar/", train= True, download = True, transform = mytransform )
cifarLoader = torch.utils.data.DataLoader(cifarSet, batch_size= 10, shuffle= False, num_workers= 2)
>>> transforms.Compose([ 
>>> transforms.CenterCrop(10),
>>> transforms.PILToTensor(), >>> transforms.ConvertImageDtype(torch.float), >>> ])

作为 Dataset类的参数传递 :

torchvision.datasets.Caltech101(root: strtarget_type: Union[List[str], str] = ‘category’transform: Optional[Callable] = Nonetarget_transform: Optional[Callable] = Nonedownload: bool = False)

或者自定义的类:
(自己实现torchvision.datasets.CIFAR10的功能)

(自己实现torchvision.datasets.CIFAR10的功能)
import os
import torch
import torch.utils.data as data
from PIL import Image

def default_loader(path):
return Image.open(path).convert('RGB')

class myImageFloder(data.Dataset):
def __init__(self, root, label, transform = None, target_transform=None, loader=default_loader):
fh = open(label)
c=0
imgs=[]
class_names=[]
for line in fh.readlines():
if c==0:
class_names=[n.strip() for n in line.rstrip().split('    ')]
else:
cls = line.split()
fn = cls.pop(0)
if os.path.isfile(os.path.join(root, fn)):
imgs.append((fn, tuple([float(v) for v in cls])))
c=c+1
self.root = root
self.imgs = imgs
self.classes = class_names
self.transform = transform
self.target_transform = target_transform
self.loader = loader

def __getitem__(self, index):
fn, label = self.imgs[index]
img = self.loader(os.path.join(self.root, fn))
if self.transform is not None:
img = self.transform(img)
return img, torch.Tensor(label)

def __len__(self):
return len(self.imgs)
def getName(self):
return self.classes

实例化torch.utils.data.DataLoader

mytransform = transforms.Compose([
transforms.ToTensor()
]
)

# torch.utils.data.DataLoader
imgLoader = torch.utils.data.DataLoader(
myFloder.myImageFloder(root = "../data/testImages/images", label = "../data/testImages/test_images.txt", transform = mytransform ),
batch_size= 2, shuffle= False, num_workers= 2)

for i, data in enumerate(imgLoader, 0):
print(data[i][0])
# opencv
img2 = data[i][0].numpy()*255
img2 = img2.astype('uint8')
img2 = np.transpose(img2, (1,2,0))
img2=img2[:,:,::-1]#RGB->BGR
cv2.imshow('img2', img2)
cv2.waitKey()
break

2 使用Python+OpenCV进行数据扩充(适用于目标检测)

https://pythonmana.com/2021/12/202112131040182515.html

下面内容来自

数据扩充是一种增加数据集多样性的技术,无需收集更多真实数据,但仍有助于提高模型精度并防止模型过拟合。

数据扩充方法包括:

  1. 随机裁剪
  2. Cutout
  3. 颜色抖动
  4. 增加噪音
  5. 过滤
import os

import cv2

import numpy as np

import random


def file_lines_to_list(path):

    '''

    ### 在TXT文件里的行转换为列表 ###

    path: 文件路径

    '''

    with open(path) as f:

        content = f.readlines()

    content = [(x.strip()).split() for x in content]

    return content


def get_file_name(path):

    
'''

    ### 获取Filepath的文件名 ###

    path: 文件路径

    '''

    basename = os.path.basename(path)

    onlyname = os.path.splitext(basename)[0]

    return onlyname


def write_anno_to_txt(boxes, filepath):

    
'''

    ### 给TXT文件写注释 ###

    boxes: format [[obj x1 y1 x2 y2],...]

    filepath: 文件路径
    '''

    txt_file = open(filepath, "w")

    for box in boxes:

        print(box[0], int(box[1]), int(box[2]), int(box[3]), int(box[4]), file=txt_file)

    txt_file.close()

随机裁剪

随机裁剪随机选择一个区域并进行裁剪以生成新的数据样本,裁剪后的区域应具有与原始图像相同的宽高比,以保持对象的形状。

def randomcrop(img, gt_boxes, scale=0.5):

    
'''

    ### 随机裁剪 ###

    img: 图像

    gt_boxes: format [[obj x1 y1 x2 y2],...]

    scale: 裁剪区域百分比
    '''


    # 裁剪

    height, width = int(img.shape[0]*scale), int(img.shape[1]*scale)

    x = random.randint(0, img.shape[1] - int(width))

    y = random.randint(0, img.shape[0] - int(height))

    cropped = img[y:y+height, x:x+width]

    resized = cv2.resize(cropped, (img.shape[1], img.shape[0]))


    # 修改注释

    new_boxes=[]

    for box in gt_boxes:

        obj_name = box[0]

        x1 = int(box[1])

        y1 = int(box[2])

        x2 = int(box[3])

        y2 = int(box[4])

        x1, x2 = x1-x, x2-x

        y1, y2 = y1-y, y2-y

        x1, y1, x2, y2 = x1/scale, y1/scale, x2/scale, y2/scale

        if (x1<img.shape[1] and y1<img.shape[0]) and (x2>0 and y2>0):

            if x1<0: x1=0

            if y1<0: y1=0

            if x2>img.shape[1]: x2=img.shape[1]

            if y2>img.shape[0]: y2=img.shape[0]

            new_boxes.append([obj_name, x1, y1, x2, y2])

    return resized, new_boxes

Cutout

Terrance DeVries和Graham W.Taylor在2017年的论文中介绍了Cutout,它是一种简单的正则化技术,用于在训练过程中随机屏蔽输入的方块区域,可用于提高卷积神经网络的鲁棒性和整体性能。这种方法不仅非常容易实现,而且还表明它可以与现有形式的数据扩充和其他正则化工具结合使用,以进一步提高模型性能。如本文所述,剪切用于提高图像识别(分类)的准确性,因此,如果我们将相同的方案部署到对象检测数据集中,可能会导致丢失对象的问题,尤其是小对象。

剪切输出是新生成的图像,我们不移除对象或更改图像大小,则生成图像的注释与原始图像相同。

def cutout(img, gt_boxes, amount=0.5):

    
'''

    ### Cutout ###

    img: 图像

    gt_boxes: format [[obj x1 y1 x2 y2],...]

    amount: 蒙版数量/对象数量
    '''

    out = img.copy()

    ran_select = random.sample(gt_boxes, round(amount*len(gt_boxes)))


    for box in ran_select:

        x1 = int(box[1])

        y1 = int(box[2])

        x2 = int(box[3])

        y2 = int(box[4])

        mask_w = int((x2 - x1)*0.5)

        mask_h = int((y2 - y1)*0.5)

        mask_x1 = random.randint(x1, x2 - mask_w)

        mask_y1 = random.randint(y1, y2 - mask_h)

        mask_x2 = mask_x1 + mask_w

        mask_y2 = mask_y1 + mask_h

        cv2.rectangle(out, (mask_x1, mask_y1), (mask_x2, mask_y2), (0, 0, 0), thickness=-1)

    return out

颜色抖动

ColorJitter是另一种简单的图像数据增强,我们可以随机改变图像的亮度、对比度和饱和度。我相信这个技术很容易被大多数读者理解。

def colorjitter(img, cj_type="b"):

    
'''

    ### 不同的颜色抖动 ###

    img: 图像

    cj_type: {b: brightness, s: saturation, c: constast}
    '''

    if cj_type == "b":

        # value = random.randint(-50, 50)

        value = np.random.choice(np.array([-50, -40, -30, 30, 40, 50]))

        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

        h, s, v = cv2.split(hsv)

        if value >= 0:

            lim = 255 - value

            v[v > lim] = 255

            v[v <= lim] += value

        else:

            lim = np.absolute(value)

            v[v < lim] = 0

            v[v >= lim] -= np.absolute(value)


        final_hsv = cv2.merge((h, s, v))

        img = cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)

        return img


    elif cj_type == "s":

        # value = random.randint(-50, 50)

        value = np.random.choice(np.array([-50, -40, -30, 30, 40, 50]))

        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

        h, s, v = cv2.split(hsv)

        if value >= 0:

            lim = 255 - value

            s[s > lim] = 255

            s[s <= lim] += value

        else:

            lim = np.absolute(value)

            s[s < lim] = 0

            s[s >= lim] -= np.absolute(value)


        final_hsv = cv2.merge((h, s, v))

        img = cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)

        return img


    elif cj_type == "c":

        brightness = 10

        contrast = random.randint(40, 100)

        dummy = np.int16(img)

        dummy = dummy * (contrast/127+1) - contrast + brightness

        dummy = np.clip(dummy, 0, 255)

        img = np.uint8(dummy)

        return img

增加噪声

在一般意义上,噪声被认为是图像中的一个意外因素,然而,几种类型的噪声(例如高斯噪声、椒盐噪声)可用于数据增强,在深度学习中添加噪声是一种非常简单和有益的数据增强方法。

对于那些无法识别高斯噪声和椒盐噪声之间差异的人,高斯噪声的值范围为0到255,具体取决于配置,因此,在RGB图像中,高斯噪声像素可以是任何颜色。相比之下,椒盐噪波像素只能有两个值0或255,分别对应于黑色(PEPER)或白色(salt)。

def noisy(img, noise_type="gauss"):

    
'''

    ### 添加噪声 ###

    img: 图像

    cj_type: {gauss: gaussian, sp: salt & pepper}
    '''

    if noise_type == "gauss":

        image=img.copy() 

        mean=0

        st=0.7

        gauss = np.random.normal(mean,st,image.shape)

        gauss = gauss.astype('uint8')

        image = cv2.add(image,gauss)

        return image


    elif noise_type == "sp":

        image=img.copy() 

        prob = 0.05

        if len(image.shape) == 2:

            black = 0

            white = 255            

        else:

            colorspace = image.shape[2]

            if colorspace == 3:  # RGB

                black = np.array([0, 0, 0], dtype='uint8')

                white = np.array([255, 255, 255], dtype='uint8')

            else:  # RGBA

                black = np.array([0, 0, 0, 255], dtype='uint8')

                white = np.array([255, 255, 255, 255], dtype='uint8')

        probs = np.random.random(image.shape[:2])

        image[probs < (prob / 2)] = black

        image[probs > 1 - (prob / 2)] = white

        return image

滤波

本文介绍的最后一个数据扩充过程是滤波。与添加噪声类似,滤波也简单且易于实现。实现中使用的三种类型的滤波包括模糊(平均)、高斯和中值。

def filters(img, f_type = "blur"):

    
'''

    ### 滤波 ###

    img: 图像

    f_type: {blur: blur, gaussian: gaussian, median: median}
    '''

    if f_type == "blur":

        image=img.copy()

        fsize = 9

        return cv2.blur(image,(fsize,fsize))


    elif f_type == "gaussian":

        image=img.copy()

        fsize = 9

        return cv2.GaussianBlur(image, (fsize, fsize), 0)


    elif f_type == "median":

        image=img.copy()

        fsize = 9

        return cv2.medianBlur(image, fsize)

上述内容可以在这里找到完整实现

https://github.com/tranleanh/data-augmentation

机器学习-吴恩达

笔记地址

http://www.ai-start.com/ml2014/

github链接:(笔记实现)

https://github.com/fengdu78/Coursera-ML-AndrewNg-Notes  

github链接:(code实现)

https://github.com/fengdu78/Coursera-ML-AndrewNg-Notes/tree/master/code

建议看这个https://github.com/mstampfer/Coursera-Stanford-ML-Python

视频:

https://www.bilibili.com/video/BV164411b7dx?p=1

https://www.coursera.org/learn/machine-learning/home/welcome