提克破事水


  • Home

  • About

  • Tags

  • Categories

  • Archives

  • Search

初识CRF

Posted on 2023-03-29 | In 深度学习 |

序列标注

CRF通常用于序列标注的场景,旨在为输入序列的每一个位置都赋予一个标签,假设模型的输入为Q,输出目标是一个序列a_1,a_2,\dots,a_n,按照通常的建模逻辑,我们希望目标输出序列的概率最大,即:
P(a_1,a_2,\dots,a_n|Q)
不论是传统方法还是深度学习方法,直接对完整序列建模是比较困难的,因此我们通常会使用一些假设来简化它,比如直接使用朴素假设,即可得到
P(a_1,a_2,\dots,a_n|Q)=P(a_1|Q)P(a_2|Q) \dots P(a_n|Q)
此时的Q不一定是原始输入,也可能是LSTM等网络的输出,我们可以认为LSTM的输出中已经具备了序列的特征信息,而且还可以在此基础之上假设输出序列之间互不相关,于是就可以得到:
P(a_1|Q) = P(a_1|q_1,q_2,\dots,q_n) = P(a_1|q_1)
P(a_2|Q) = P(a_2|q_1,q_2,\dots,q_n) = P(a_2|q_2)
P(a_3|Q) = P(a_1|q_1,q_2,\dots,q_n) = P(a_3|q_3)
…
P(a_n|Q) = P(a_1|q_1,q_2,\dots,q_n) = P(a_n|q_n)
从而
P(a_1|q_1,q_2,\dots,q_n|Q) = P(a_1|q_1)P(a_2|q_2)\dotsP(a_n|q_n)
这样就得到了最常用的方案,逐位输出概率值最大的标签。

条件随机场

在使用逐位输出概率值最大的标签的方法进行序列标注时,如果我们使用BIOES对实体信息进行标注,可能会出现BBB这样序列,但这个序列违反了我们的解码规则,B后应当只能为I或者E。之所以会出现这个问题,是因为我们假设输出序列之间互不相关,只要引入上下文信息,就可以解决这个问题。按照以往的方式,通常会建立一个转移矩阵,把不合理的转移概率设为0,如P(b|b)=0,然后通过动态规划保证得到合理的序列。
P(a_1,a_2,\dots,a_n|Q) = P(a_1|Q)P(a_2|a_1)P(a_3|a_1,a_2) \dots P(a_n|a_1,a_2,\dots,a_(n-1))=P(a_1|Q)P(a_2|a_1,Q) \dots P(a_n|a_(n-1),Q)
通过使用上述方法建立输出之间的联系,其中每一项都是转移概率。而CRF的做法是,假设序列之间存在关系f(x,y;Q),然后直接令
P(a_1|q_1,q_2,\dots,q_n|Q) = \frac{1}{Z} exp (\sum_k f(a_(k-1),a_k;Q))
其中Z是归一化因子,与前者相比,P(a_k|a_(k-1))是有概率意义的(条件概率),而单项的e^(f(a_(k-1),a_k;Q))/Z是没有概率意义额,所以CRF是更一般的形式。

梯度累积

Posted on 2023-03-29 | In 深度学习 |

Batch size

Batch size大小对训练过程的收敛性,以及训练模型的最终准确性具有关键影响。
Batch size较大时可能使模型陷入局部最优,
小的Batch size会导致模型收敛速度慢,由于数据中必定存在噪音,当batch size越小,来自于噪音的梯度就越多,梯度估计的准确度就越低。也就是说,较小的batch size可能会使学习过程波动性更大,延长算法收敛的时间。

训练模型过程中,batch size越大对GPU的内存容量要求越高。训练过程中,显存中包含以下内容:

  • 模型参数:模型中的权重和偏差
  • 优化器变量:优化器算法所需要的变量
  • 中间计算变量:网络模型计算产生的中间值会产生临时的内存,如每层激活的输出
  • 工作区Workspace:计算过程中产生的中间变量,如D=A+B/C中B/C的结果
    因此,Batch size越大,训练神经网络需要的样本越多,导致需要存储在AI芯片内存变量激增,则对GPU的显存要求越高。

如何使用较大的batch size

为解决GPU显存限制的问题,将运行大Batch size的一种方法是将Sample的Batch拆分为更小的Batch,称之为Mini-Batch,可以通过两种方式使用Mini-batch来完成模型训练任务:数据并行和梯度累积。

  • 数据并行:在多个GPU上训练模型,每个GPU上都有一份Mini-batch数据,在Epoch结束时梯度求和并更新网络参数。
  • 梯度累积:按顺序执行Mini-Batch,同时对梯度进行累积,累积的结果在Mini-Batch计算后求平均更新模型变量。
    两者的区别在于数据并行使用的是多卡环境,当仅有一块GPU的时候可以使用梯度累积。

梯度累计原理

训练时,数据输入模型,中间层对数据进行相应计算,最终输出预测值,通过损失函数计算每个样本的损失值。通过反向传播,计算损失值相对于模型参数的梯度,最后将这些梯度信息用于网络模型中的参数进行更新。以SGD为例,利用loss函数来更新模型参数:
\theta_i = \theta_(i-1) - lr \times grad_i
其中\theta是网络模型中的可训练参数,lr为学习率,grad是参数相对于损失函数的梯度。这种更新模型参数的方式可以理解为“实时的”,计算完成后当即更新模型参数。但梯度累积并不会及时更新网络参数,而是在计算过程中累积计算时得到的梯度,最后统一使用累积的梯度来对参数进行更新。
比如说,现在要训练某神经网络,将数据分为若干Batch,但由于硬件环境受限无法直接将Batch输入模型,所以使用梯度累积的方式训练网络模型。将每个Batch分为若干mini-batch,每个mini-batch计算完毕后记录梯度信息,当前batch都计算完毕后,使用Batch中所有Mini-batch的梯度信息来对模型中的参数进行更新。算法上等价于没有切分的batch的训练方式。
在实际工程当中,关于调参和算法上需要注意两天:

  • 一定条件下,batch size越大训练效果越好,梯度累积则模拟了batch size增大而效果。如果accumulation steps为4,则相当于将batch size 增大4倍,此时需要把学习率适当调大。
  • Batch size放大4倍时和真实batch的数据分布并不完全相同,4倍batch的BN计算出来的方差和均值与原始batch的方差和均值不太相同,因此有些实现中会使用Group Norm来代替Batch Norm

代码实现

正常batch训练模型的过时如下所示。

1
2
3
4
5
6
7
8
9
for i, (images, labels) in enumerate(train_data):
# 1. forwared 前向计算
outputs = model(images)
loss = criterion(outputs, labels)

# 2. backward 反向传播计算梯度
optimizer.zero_grad()
loss.backward()
optimizer.step()

即输入一个batch的数据,计算一次梯度,当即更新换一次网络,使用梯度累的实现方式如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 梯度累加参数
accumulation_steps = 4
for i, (images, labels) in enumerate(train_data):
# 1. forwared 前向计算
outputs = model(imgaes)
loss = criterion(outputs, labels)

# 2.1 loss regularization loss正则化
loss += loss / accumulation_steps

# 2.2 backward propagation 反向传播计算梯度
loss.backward()

# 3. update parameters of net
if ((i+1) % accumulation)==0:
# optimizer the net
optimizer.step()
optimizer.zero_grad() # reset grdient

DataParallel

数据并行(DataParallel,DP),用于单机多卡的环境,所有卡都负责计算和训练模型,与此同时,device[0]还负责整合梯度,更新参数。主要过程包含三个部分,如图所示。

  • 各卡分别计算损失和梯度(红色部分)
  • 所有梯度整合到device[0](蓝色部分)
  • device[0]进行参数更新,其他卡拉取device[0]的参数进行更新(绿色部分) 虽然DP只能实现单机训练不能算是严格意义上的分布式训练,但其原理和分布式训练算法里的Parameter Server架构很相近,PS的架构如下。 PS架构由server节点和worker节点组成。server借钱的主要功能是初始化和保存模型参数、接收worker节点计算出的局部梯度、汇总计算全局梯度,并更新模型参数
    worker节点的主要功能是各自保存部分训练数据,初始化模型,从server节点拉取最新的都模型参数,根据训练数据计算局部梯度,上传给server节点。
  • PS架构下的DP,会造成负载不均衡,因为充当server的GPU需要一定的显存用来保存worker节点计算出的局部梯度,另外还需要将更新后的模型参数传递给每个worker,server的带宽会成为通信瓶颈,server与worker之间的通信成本会随着worker数目的增加而线性增加。
    PS的并行梯度下降流程分为4个部分。
  • Task Scheuler:负责加载数据并分发数据至各个worker节点,进行多轮迭代。
  • 在每轮迭代中,worker负责:
  • 初始化:载入数据并从server节点拉取全部模型参数
  • 梯度计算:利用该节点的数据计算梯度,并将梯度传输到server节点
  • Server负责:
  • 汇总梯度
  • 更新参数
    以上就是DP所使用的算法。

pytorch实现DP

面试应问

Posted on 2023-03-27 |

1.薪资构成
2.试用期
试用期多久
试用期是否缴纳五险一金
五险一金缴纳的比例
社保和公积金的缴纳比例
3.入职一个月内签订劳动合同、缴纳社保
4.岗位考核标准
5.薪资发放时间、社保缴纳时间,避免断档。

121. 买卖股票的最佳时机

Posted on 2023-03-27 | In Leetcode题录 |

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

你说这是动态规划吧…没啥问题…你说不是吧…好像也没啥问题…毕竟连个dp的定义都没有…可能我用力过猛了…

1
2
3
4
5
6
7
8
class Solution:
def maxProfit(self, prices: List[int]) -> int:
min_price,max_profit = int(1e9), 0
for i in prices:
min_price = min(min_price,i)
max_profit = max(max_profit,i - min_price)
return max_profit

Coding随手记

Posted on 2023-03-27 | In 常用工具 |
  1. 序列标注任务,或者说是基于bert的任务,尽量手动convert_tokens_to_id,如果使用encode_plus可能在处理数据时产生错误。比如序列标注任务时,在语句中添加的[CLS]和[SEP]后,标签可能会和原句不对齐
  2. tokenizer.tokenize(some_word)返回的结果是列表,可以使用list.extend将返回结果写入tokens : list 中

  1. 变长padding,感觉可以在get_examples阶段进行。读取所有数据后先根据长度排序后再返回。
  2. lens=[所有数据长度],len_index = np.argsort(lens),然后data_list = data_list[index]即可,让-1*lens见鬼去吧。
  3. 在collate_fn之前要先处理好数据长度问题,如果数据长度超过max_length这里也没办法处理。因为到collate_fn的时候已经加好了[CLS]和[SEP],不能使用截断操作了。
  4. TorchCRF的crf的是有mask参数的,mask=attention_mask.byte()就有作用了。

  1. list.extend(tokenizer.unk_token) 会添加’[‘, ‘U’, ‘N’, ‘K’, ‘]’,但是extend([tokenizer.unk_token])则添加的是”[UNK]”

  1. torch.nn.utils.RNN.pack_padded_sequence
    原文链接:https://www.cxyzjd.com/article/kejizuiqianfang/100835528
    假设存在两条数据:
    1
    2
    tensor([[1, 2, 3, 4, 5, 6, 7],
    [2, 3, 4, 5, 6, 7, 0]])
    输入RNN的其实是[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0],最终输入模型的0并非真实数据,若参与运算则会影响模型效果且浪费算力,于是使用torch.utils.nn.RNN.pack_padded_sequence方法(后称“pack”)去除掉输入进模型的padding标记,上述两条数据pack之后得到的结果为:
    1
    2
    3
    4
    PackedSequence(data=tensor([1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7]), batch_sizes=tensor([2, 2, 2, 2, 2, 2, 1]), sorted_indices=None, unsorted_indices=None)
    PackedSequence(data=tensor([3, 4, 4, 5, 5, 6, 6, 7, 7]), batch_sizes=tensor([2, 2, 2, 2, 1]), sorted_indices=None, unsorted_indices=None)
    PackedSequence(data=tensor([5, 6, 6, 7, 7]), batch_sizes=tensor([2, 2, 1]), sorted_indices=None, unsorted_indices=None)
    PackedSequence(data=tensor([7]), batch_sizes=tensor([1]), sorted_indices=None, unsorted_indices=None)
    暂且只看第一条数据,pack后的数据为:[1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7],实际上就是去除掉两条数据中padding标记的结果,RNN通过batch_sizes中的数字决定每次输入模型“几个数”,这里的batch_sizes为tensor([2, 2, 2, 2, 2, 2, 1]),意为连续取6次两位数,在第7次时只取一个数。

  1. Python中的矩阵乘法
    Python中,星乘(*)指两个矩阵对应位置相乘;点乘(.dot)指数学上的矩阵乘法。
    在进行星乘时,Python会对“低维度的矩阵”进行广播操作,使之能与另一矩阵维度匹配,而后进行对应位置相乘。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #假设现有一文本序列
    seq = torch.tensor(np.random.randint(0,2,(batch,seq_len)))
    #以及该序列所对应的词向量矩阵
    vector_mat = torch.tensor(np.random.randn(batch,seq_len,dim))
    #现要对padding的部分做mask
    mask = seq.unsqueeze(-1)
    mask = torch.tensor(mask>torch.tensor(np.array([0])),dtype=torch.float)
    print(seq)
    print("**********")
    print(mask)
    print("**********")
    print(vector_mat * mask)

  1. plt.subplots
    如果想要创建多个子图,可以使用plt.subplots方法创建“画布”。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from matplotlib import pyplot as plt
    #设置一块 2*2 的画布,figsize设置子图?整个画布?的尺寸
    f,ax = plt.subplots(nrows=2,ncols=2,figsize=(18,12))
    #此时ax是一个多维的变量,可以通过flatten变成普通list索引访问
    ax = ax.flatten()
    #假设此时要画3个hist,可是定义的画布是2*2的,也就是可以画4个图,最后一个会显示空白
    ax[0].hist(train_text_lens)
    ax[0].set_title("train_text_lens")
    ax[1].hist(dev_text_lens)
    ax[1].set_title("dev_text_lens")
    ax[2].hist(test_text_lens)
    ax[2].set_title("test_text_lens")
    #想要删除某个子图可以使用plt.delaxes方法
    plt.delaxes(ax[-1])

  1. AutoTokenizer
    项目中AutoTokenizer较为常见,与特定的Tokenizer(类似于BertTokenizer)的区别在于,直接使用特定Tokenizer时,cache路径下不需要包括tokenizer.json文件。但是如果使用AutoTokenizer,cache中需要包含的就不仅仅是vocab、model.bin、config.json三个文件了,还需要一个类似于tokenizer.json的文件。

1
2
import re
re.finditer()#找到字符串中的所有目标字符
  1. json.load()和json.loads()的区别
    json.loads()将字符串读取成Python数据结构,json.load()将文件读取成Python数据结构
    另外,如果键值对中包含整型数据,将其保存为json后,会将所有的数据转化为字符串。可以使用object_hook参数来解决这个问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import json
    path = "something/something.json"

    def jsonKey2int(x):
    if isinstance(x,dict):
    return {int(k) : v for k,v in x.items()}
    return x
    with open(path,encoding="utf-8") as f:
    json.load(f,obgject_hook=jsonKey2int)

  2. pytorch中nn.CrossEntropyLoss()的计算过程中是包含Softmax
    如果在模型forward过程中自己计算softmax然后再使用交叉熵很可能会导致loss不下降
    @生命奇点_ZRY 用交叉熵完成分类任务,线性层的输出可别softmax了喔

  1. 引包
    小项目的工作路径中直接包含若干python文件或包,编写代码时可以直接调用。如果项目规模较大,在工作路径中包含若干子项目时,可能会涉及到不同包下模块的引用,经常会出现无法引用自己写的块的问题。

    –project
    —-main.py
    —-folder1
    – – test1.py
    – – test3.py


—-folder2
– – test2.py
– – test4.py
最外层文件夹名称project,包含main.py和文件夹folder1、folder2。folder1中包含test1.py和test3.py,folder2中包含test2.py和test4.py。
想要正常import,模块必须在sys.path中能被找到,import的查找顺序为:

  • 内置模块
  • .py文件所在目录(当前工作路径)
  • 环境变量中列出的目录(虚拟环境)
  • pip 或 easy_install安装的包
    可以通过打印sys.path的方式查看当前.py文件中sys.path包含了哪些内容
    1
    2
    3
    import sys
    for i in sys.path:
    print(i)
    假设当前执行test1.py,打印其sys.path,可以发现folder1是在其中的,此时若需要引用folder2中的模块,那必然是会报错的,但是由于Pycharm等IDE可将floder2设置为source root则可以避免报错,使用cmd执行时就会报错。

如何解决?

  • sys.path.append(“..”)
    想要调用父级目录中的模块时,可以将父级目录添加到sys.path中。

    from 父级目录.folder_name import module
    这样就可以通过上述方式引用调用模块。

CNN在NLP中的使用

Posted on 2023-03-27 | In 深度学习 |

滤波器与卷积核

一般在检索卷积神经网络相关资料时都会看到类似的样例图。

这里需要强调的是,滤波器(Filter)和卷积核(Kernel)并非指的是同一样东西,每个通道的数据都需要对应一个卷积核,若干个卷积核总称为滤波器。在图示中,输入图像数据包含3通道,所以对应的卷积核就要包含3个,这3个卷积核组合成为一个滤波器。
滤波器的个数与输出通道数相对应,若假设输入数据通道数为$\bf{C_{in}}$,out_channels为$\bf{N}$,,每个通道需要对应一个卷积核,那么实际上就有$3\bf{N}$个卷积核。以上图为例,模型输入为三通道,对应三个卷积核对每个通道中的数据进行卷积操作,将最终得到的矩阵相加得到最终结果。

Pytorch中的卷积层定义

引包

1
import torch

Conv1d

1
2
3
4
5
6
torch.nn.Conv1d(
in_channels
, out_channels
, kernel_size
, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None
)

Conv2d

1
2
3
4
5
6
torch.nn.Conv2d(
in_channels
, out_channels
, kernel_size
, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None
)

Con1d和Conv2d的函数定义如上所示,in_channels代表着输入数据的通道数,以图像数据为例,如果输入的是3通道,那么in_channels就要为3,输入数据需要与卷积层通道数对齐。out_channels代表滤波器的个数,kernel_size为卷积核的尺寸。kernel_size、dilation、padding可以设定为整型也可以设定为元组。kernel_size输入为整型$\bf{X}$时,卷积核尺寸为$\bf{X}\times\bf{X}$;当输入为(x,y)时,卷积核尺寸即为$\bf{X}\times\bf{Y}$。padding和dilation输入为整型时,意为对行、对列的padding或dilation均相同;当输入为元组时,代表当前卷积层进行padding和dilation时对行、对列设定不同数值。

卷积层的输入

Conv1D与Conv2D的输入略有不同,输入Conv1d的往往为三维数据,即$(\bf{N},\bf{C_{in}},\bf{L})$,输入Conv2d的数据往往为四维,即$(\bf{N},\bf{C_{in}},\bf{H},\bf{W})$。以$(\bf{N},\bf{C_{in}},\bf{L})$为例,$\bf{N}$代表一个batch中有$\bf{N}$条数据,每条数据的长度为$\bf{L}$,数据的通道数为$\bf{C_{in}}$;$(\bf{N},\bf{C_{in}},\bf{H},\bf{W})$中$\bf{N}$和$\bf{C_{in}}$的含义不变,$\bf{H}$和$\bf{W}$分别代表数据的高度和宽度。在自然语言处理相关任务中,输入数据的维度多为$(\bf{N},\bf{L},Embedding_dim)$,并没有“通道”的概念。

CNN在NLP中的应用

这里我们先假设一组embedding之后的数据,batch大小为3,批中语句长度为12,每个字的embedding_dim为7,共3个滤波器,最终得到的数据如代码块所示。

1
2
3
4
5
6
7
8
9
import numpy as np
import torch
from copy import deepcopy
import torch.nn.functional as F
batch = 2
seq_len = 12
bert_hid_size = 7
output_channel = 3
sequence = torch.tensor(np.random.rand(batch,seq_len,bert_hid_size),dtype=torch.float)

前面介绍了再Pytorch中Conv2d层的输入是四个维度,所以需要将embedding之后的数据升1维,变为(batch,1,len,embedding_dim)。这时可以有两种做法,一种是直接将“1”看作数据的通道数,另一种是将原始数据转变维度,使用seq_len或embedding_dim作为通道数。直接将“1”看作通道数的实现如以下代码块所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
from copy import deepcopy
conv2d_layer = torch.nn.Conv2d(
in_channels = 1
,out_channels = 3
,kernel_size = 3
)
inputs = deepcopy(sequence).unsqueeze(1)
print(f"inputs.shape:{inputs.shape}")
outputs = conv2d_layer(inputs)
print(f"outputs.shape:{outputs.shape}")

# inputs.shape:torch.Size([2, 1, 12, 7])
# outputs.shape:torch.Size([2, 3, 10, 5])

除此之外,还可以将embedding_dim作为通道数进行卷积操作,实现如以下代码块所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from copy import deepcopy
conv2d_layer = torch.nn.Conv2d(
in_channels = bert_hid_size
,out_channels = 3
,kernel_size = 3
# ,padding = 1
)
inputs = deepcopy(sequence).unsqueeze(1).permute(0,3,1,2)
print(f"inputs.shape:{inputs.shape}")
outputs = conv2d_layer(inputs)
print(f"outputs.shape:{outputs.shape}")

# RuntimeError:Calculated padded input size per channel: (1 x 12). Kernel size: (3 x 3).
# Kernel size can't be greater than actual input size

这里如果没有进行padding操作会报错“其中提到计算出的每个channel输入大小:(1x 12),kernel大小:(3 x3),内核大小不能大于实际输入大小”。这是不是可以说明,将embedding_dim作为通道后,在每个channel中的数据实际上是(batch, 1, 1, seq_len),也就是channel把原来的矩阵“掰碎”了?在原始数据中使用维度为768的向量表示一个token,现在在每个通道中,使用一个维度为1的向量代表一个token,换句话说,卷积核的尺寸设置成3,就已经是3-gram模型了?

在这种情况下,kernel_size设计成(1,seq_len)还是(1,int)?如果设计成(1,3)是对应着3-gram模型吗?
这里如果增加了padding,也是可以运行的,结果为torch.Size([3, 3, 1, 12]),这里每个维度代表的含义?第一个3是batch,第二个3是几个滤波器,12代表seq_len,那最终的1可以理解成将每个token的向量转变成了1个数字吗?

对数据进行padding后,上述报错的代码即可执行,实现效果如下代码块所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from copy import deepcopy
conv2d_layer = torch.nn.Conv2d(
in_channels = bert_hid_size
,out_channels = 3
,kernel_size = 3
,padding = 1
)
inputs = deepcopy(sequence).unsqueeze(1).permute(0,3,1,2)
print(f"inputs.shape:{inputs.shape}")
outputs = conv2d_layer(inputs)
print(f"outputs.shape:{outputs.shape}")

# inputs.shape:torch.Size([2, 7, 1, 12])
# outputs.shape:torch.Size([2, 3, 1, 12])

这里观察卷积后得到的数据维度可以发现,batch、seq_len、以及升维的channel都没有变,原本7维的词向量缩短为3维。
也可以将seq_len作为channel,程序依旧可以运行,实现如以下代码块所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from copy import deepcopy
conv2d_layer = torch.nn.Conv2d(
in_channels = seq_len
,out_channels = 3
,kernel_size = 3
,padding = 1
)
inputs = deepcopy(sequence).unsqueeze(1).permute(0,2,1,3)
print(f"inputs.shape:{inputs.shape}")
outputs = conv2d_layer(inputs)
print(f"outputs.shape:{outputs.shape}")

# inputs.shape:torch.Size([2, 12, 1, 7])
# outputs.shape:torch.Size([2, 3, 1, 7])

这里观察输出结果维度发现,batch、channel、embedding维度没有变,但是原本对应句子长度的数值缩减到3。
torch.nn.Conv1d实现起来与Conv2d差不多,下面两个代码块依次以embedding_dim和seq_len作为通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from copy import deepcopy
conv1d_layer = torch.nn.Conv1d(
in_channels = bert_hid_size
,out_channels = 3
,kernel_size = 3
,padding = 1
)
inputs = deepcopy(sequence).permute(0,2,1)
print(f"inputs.shape:{inputs.shape}")
outputs = conv1d_layer(inputs)
print(f"outputs.shape:{outputs.shape}")

# inputs.shape:torch.Size([2, 7, 12])
# outputs.shape:torch.Size([2, 3, 12])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from copy import deepcopy
conv1d_layer = torch.nn.Conv1d(
in_channels = seq_len
,out_channels = 3
,kernel_size = 3
,padding = 1
)
inputs = deepcopy(sequence)
print(f"inputs.shape:{inputs.shape}")
outputs = conv1d_layer(inputs)
print(f"outputs.shape:{outputs.shape}")

# inputs.shape:torch.Size([2, 12, 7])
# outputs.shape:torch.Size([2, 3, 7])

所以说使用CNN进行NLP相关任务的时候最重要的一个原则,需要沿着embedding_dim那个维度进行卷积,例如输入为(batch,len,dim)时(4维时同理),将dim作为通道数,即(batch,dim,len)每个通道内形状为(1,len)相当于原来的embedding被“降维”成了一个数,此时kernel_size设为(1,3)则是一个3-gram模型。
也有许多博客说Conv1d和Conv2d的区别在于卷积的方向,Conv1D的卷积方向是一维的,只能一个方向,Conv2D的卷积方向是二维的,横着卷完竖着卷。但是在处理文本数据时,理论上应该把每个词的词向量完整的包含在卷积核中,对于一个(batch, len, embedding_dim)的矩阵,卷积核尺寸应该类似于(height, embedding_dim),这时卷积的方向不也是只能向下了嘛?
上述的方法的确可以使用CNN对文本embedding后的矩阵进行运算,但是存在一个比较严重的问题就是模型很难做深。

什么是Bert

Posted on 2023-03-27 | In 深度学习 |

现如今,Bert已成为深度学习领域内耳熟能详的预训练语言模型。预训练语言模型根据训练方式可以分为自编码式语言模型和自回归式语言模型。在介绍他们两个之前先对语言模型进行介绍。

语言模型

语言模型是NLP领域中的基本任务,其任务目标为对文本序列中某个位置的词进行预测。传统语言模型就是从左至右计算每个词出现的概率,在预测时模型依靠其上文信息进行预测,通常使用循环神经网络来实现。计算过程中,依次对文本序列中的向量进行计算。

自回归语言模型

基于自回归的预训练语言模型对文本序列的建模方式与传统语言模型相同,都是根据上文信息或者下文信息预测文本序列中的词,但由于模型只能获取到单向的语义信息,在完成完形填空等涉及到上下文信息的任务时,自回归预训练语言模型表现欠佳。后来出现了ELMo,其采用两个LSTM分别获取文本序列中的顺序和逆序的文本信息,将两者拼接后作为文本的向量表达。

自编码语言模型

除了自回归的方式之外,还有其他对文本序列中的字进行预测的方法。自编码模型采用随机遮盖住文本序列中部分字,在预测过程中,根据上下文来预测被遮蔽的词。与自回归的方式不同,模型在预测时可以自然而然地获取到遮蔽词的上下文信息,但这种方式也存在缺点。比如在实际下游任务当中,并不会出现遮蔽词,因此会导致预训练和下游任务中存在gap的问题。

Bert的预训练任务

Bert的预训练任务中包含两个任务,一个是遮蔽语言模型(Masked Language Model,MLM),还有一个是下一句预测(Next Sentence Prediction,NSP)。

MLM

在MLM中,随机遮蔽句子中15%的词,让模型对这些遮蔽词进行预测。在大规模预训练语料下,15%也是一个不小的规模,为了防止某些词在下游任务中从未见过,作者做了如下处理:

  • 80%的概率使用“[MASK]”替换原词
  • 10%的概率被替换为其他词
  • 10%的概率原封不动
    使用随机词的原因是避免Transformer记住[MASK]是什么,至于随机词带来的负面影响,所有遮蔽词外的词共享15% * 10% = 1.5%的概率,影响是可以忽略不记的。这样使得Transformer既可以获得全部信息,也不至于让模型获得全量信息。

NSP

选择一些句子对A与B,其中50%的数据B是A的下一条句子,剩余50%的数据B是语料库中随机选择的,学习其中的相关性,添加这样的预训练的目的是目前很多NLP的任务比如QA和NLI都需要理解两个句子之间的关系,从而能让预训练的模型更好的适应这样的任务。

我们首先拿到属于上下文的一对句子,也就是两个句子,之后我们要在这两个句子中加一些特殊的 token:[CLS]上一句话[SEP]下一句话[SEP]。也就是在句子开头加一个 [CLS],在两句话之间和句末加 [SEP],

Token Embedding:就是正常的词向量,即 PyTorch 中的 nn.Embedding()
Segment Embedding:用 embedding 的信息让模型分开上下句,我们给上句的 token 全 0,下句的 token 全 1,让模型得以判断上下句的起止位置
Position Embedding :Transformer 中的不一样,不是三角函数,而是学习出来的。

2.两数相加

Posted on 2023-03-27 | In Leetcode题录 |

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next

class Solution(object):

def addTwoNumbers(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
carry = 0
res = ListNode()
record = res #记录起点
while l1 or l2 :
x = l1.val if l1 else 0
y = l2.val if l2 else 0
carry,sums = (x + y + carry) // 10,(x + y + carry) % 10
res.next = ListNode(sums)
res = res.next
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
if carry != 0:
res.next = ListNode(carry)
return record.next

1.两数之和

Posted on 2023-03-27 | In Leetcode题录 |

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/two-sum

1
2
3
4
5
6
7
8
9
10
11
12
class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
for i in range(len(nums) - 1):
for j in range( (i+1),len(nums)):
num_a,num_b = nums[i],nums[j]
if num_a + num_b == target:
return i,j

1669. 合并两个链表

Posted on 2023-03-27 | In Leetcode题录 |

给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置,请你返回结果链表的头指针。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/merge-in-between-linked-lists

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def mergeInBetween(self, list1, a, b, list2):
"""
:type list1: ListNode
:type a: int
:type b: int
:type list2: ListNode
:rtype: ListNode
"""
#保存链表起始位置
head_node = list1
temp_node = list1
pre_node,post_node = None,None
#根据索引保存欲删除节点的前一个和后一个节点
for i in range(b):
if i == (a-1) :
pre_node = list1
list1 = list1.next
else:
list1 = list1.next
post_node = list1.next
#将list2接入list1
pre_node.next = list2
#从头遍历list1,找到list2的最后一个节点
#这个循环最开始写的是 while temp_node,可是一直无法将后续节点补回list1
#因为需要将最后一个节点的next连接到补回节点中,直接遍历到空节点即使赋值,在原链表中也无法找到后续节点
while temp_node.next:
temp_node = temp_node.next
#将后续节点补入list1
temp_node.next = post_node
return head_node
<i class="fa fa-angle-left"></i>1…345…8<i class="fa fa-angle-right"></i>

77 posts
10 categories
14 tags
© 2024 Antique
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4