小乔学算法
使用图像分割技术实现视频特效
一、前言在许多视频对话软件中,都可以选择视频的背景。其原理就是将人像抠出来,把非人像部分替换。而大多数软件是切换图片背景,或者是动图背景。利用图像分割技术,可以实现更复杂的背景替换,最终结果就像电影特效。许多特效拍摄会使用绿幕背景,而图像分割技术可以在不使用绿幕的情况下达到类似的效果,不过相较绿幕要差一些。今天我们要做的就是使用开源的人像分割项目来完成视频背景替换的操作。我们会在原有项目的基础上继续开发。二、项目介绍2.1 项目运行今天我们要使用的是一个视频转绿幕的项目。项目地址:github.com/PeterL1n/Ro…首先需要下载项目:git clone https://github.com/PeterL1n/RobustVideoMatting.git下载完成后,安装对应的依赖:cd RobustVideoMatting
pip install -r requirements_inference.txt然后下载对应的模型:github.com/PeterL1n/Ro…可以选择rvm_mobilenetv3.pth、rvm_resnet50.pth其中一个,放在项目目录下。然后在项目下创建一个Python文件,写入下面的代码:import torch
from model import MattingNetwork
from inference import convert_video
# 选择mobilenetv3或resnet50,对应前面下载的两个模型
model = MattingNetwork('mobilenetv3').eval().cuda() # or "resnet50"
model.load_state_dict(torch.load('rvm_mobilenetv3.pth'))
convert_video(
model, # 模型
input_source='input.mp4', # 视频或者图片目录
output_type='video', # 选"video"或"png_sequence"
output_composition='com.mp4', # 输出路径
output_alpha="pha.mp4" # 预测的alpha通道
output_video_mbps=4,
downsample_ratio=None,
seq_chunk=12,
)运行上面代码就可以完成视频抠人像的操作,输出的com.mp4是一个绿幕视频,pha.mp4是一个黑白视频(人像为白,背景为黑)。下面是一些例子:2.2 定义图片抠图和视频抠图函数在项目中,没有提供自己抠图的代码,所以我们自己编写对应的代码。项目中model.MattingNetwork就是一个人像分割网络,我们调用它的前向传播方法,输入图片,它就会返回抠取的alpha通道,代码如下:import cv2
import torch
from PIL import Image
from torchvision.transforms import transforms
from model import MattingNetwork
device = "cuda" if torch.cuda.is_available() else "cpu"
# 加载模型
segmentor = MattingNetwork('resnet50').eval().cuda()
segmentor.load_state_dict(torch.load('rvm_resnet50.pth'))
def human_segment(model, image):
src = (transforms.PILToTensor()(image) / 255.)[None]
src = src.to(device)
# 抠图
with torch.no_grad():
fgr, pha, *rec = model(src)
segmented = torch.cat([src.cpu(), pha.cpu()], dim=1).squeeze(0).permute(1, 2, 0).numpy()
segmented = cv2.normalize(segmented, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
return Image.fromarray(segmented)
human_segment(segmentor, Image.open('xscn.jpg')).show()现在只需要调用human_segment函数就能实现抠图操作了。而抠视频的操作也非常简单,在项目下提供了下面的代码:from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor
from inference_utils import VideoReader, VideoWriter
reader = VideoReader('input.mp4', transform=ToTensor())
writer = VideoWriter('output.mp4', frame_rate=30)
bgr = torch.tensor([.47, 1, .6]).view(3, 1, 1).cuda() # Green background.
rec = [None] * 4 # Initial recurrent states.
downsample_ratio = 0.25 # Adjust based on your video.
with torch.no_grad():
for src in DataLoader(reader): # RGB tensor normalized to 0 ~ 1.
fgr, pha, *rec = model(src.cuda(), *rec, downsample_ratio) # Cycle the recurrent states.
com = fgr * pha + bgr * (1 - pha) # Composite to green background.
writer.write(com) # Write frame.我们也可以自己编写代码用于视频转换,这里的操作就是逐帧读取,然后调用human_segment函数。这里写一个示例代码:capture = cv2.VideoCapture("input.mp4")
while True:
ret, frame = capture.read()
if not ret:
break
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
result = human_segment(segmentor, Image.fromarray(image))
result = cv2.cvtColor(np.array(result), cv2.COLOR_RGB2BGR)
cv2.imshow("result", result)
cv2.waitKey(10)
cv2.destroyAllWindows()不过上述代码显示时无法显示透明效果。三、视频背景切换视频背景切换的思路可以分为下面几个步骤:读取人像视频帧和背景视频帧对每一帧进行抠图把抠出来的人像与背景混合把混合结果写入视频下面我们一步步来实现。其中1、2步我们已经实现了,下面要做的就是3、4两个步骤。3.1 png图片切换背景第三步可以看做是给png图片换背景的操作,可以把这一步实现为一个函数,代码如下:from PIL import Image
def change_background(image, background):
w, h = image.size
background = background.resize((w, h))
background.paste(image, (0, 0), image)
return background其中image和background都是Pillow图片。3.2 写入视频最后就是写入视频的操作了,这个可以用OpenCV实现,代码如下:# 读取人像视频和背景视频
capture = cv2.VideoCapture("input.mp4")
capture_background = cv2.VideoCapture('background.mp4')
# 获取画面大小
fps = capture.get(cv2.CAP_PROP_FPS)
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
size = (width, height)
# 写入视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('output.mp4', fourcc, fps, size)
frames = min(capture.get(cv2.CAP_PROP_FRAME_COUNT), capture_background.get(cv2.CAP_PROP_FRAME_COUNT))
bar = tqdm(total=frames)
while True:
ret1, frame1 = capture.read()
ret2, frame2 = capture_background.read()
# 如果有一个视频结束了,则结束
if not ret1 or not ret2:
break
image = cv2.cvtColor(frame1, cv2.COLOR_BGR2RGB)
segmented = human_segment(segmentor, Image.fromarray(image))
background = Image.fromarray(cv2.cvtColor(frame2, cv2.COLOR_BGR2RGB))
changed = change_background(segmented, background)
changed = cv2.cvtColor(np.array(changed), cv2.COLOR_RGB2BGR)
out.write(changed)
bar.update(1)
out.release()上面的代码就是完成了本章开始提到的四个步骤,最后会输出一个视频。在代码中,读取了人像和背景视频,而且不要求两个视频长度一样。程序会自动选取较短的视频作为输出视频的时长。在代码中有一部分BGR转RBG、cv转Pillow的操作,这部分代码还能做一些改进。
小乔学算法
低成本实现人脸生成--使用AutoEncoder网络
一、前言AutoEncoder(AE)中文叫自编码器,是一种介于监督学习和无监督学习之间的模型。其学习方式现在被称为自监督学习,类似于Bert模型,它的训练需要标签数据,但是这个标签数据是自己生成的,因此叫做自监督学习。AutoEncoder可以实现很多有用的事情,比如数据降维、图像降噪、风格迁移等。今天我们用AutoEncoder实现人脸生成的例子,这个实际上不是非常常见的例子。二、AutoEncoderAutoEncoder的网络结构非常简单,由Encoder和Decoder两部分组成,Encoder对数据不断降维,而Decoder对降维后的数据不断升维。最后的希望Encoder的输入和Decoder的输出越接近越好。如下图:根据问题的不同Encoder和Decoder的结构也可以自由更改。比如针对简单的问题,Encoder和Decoder可以用全连接实现,逐层降维而后逐层升维。如果处理图像数据,可以用卷积神经网络来实现Encoder和Decoder。在本文中,我们处理的是人脸图像,因此选取卷积神经网络实现Encoder和Decoder。三、AutoEncoder的实现在开始生成人脸之前,需要用人脸数据训练一个AutoEncoder网络。这里使用CelebA数据集作为训练数据,数据下载地址为:mmlab.ie.cuhk.edu.hk/projects/Ce… 。选择In The Wild Images即可,不过默认使用的是Google云盘,如果不能访问可以选择百度网盘下载:pan.baidu.com/s/1eSNpdRG?at=1678847322615#list/path=%2F 。3.1 模块导入这里使用PyTorch来实现,首先看看需要用到的模块:import torch
import numpy as np
from torch import nn
from PIL import Image
from torch import optim
from torchvision.utils import make_grid
from torch.utils.data import DataLoader, Dataset
import matplotlib.pyplot as plt其中make_grid的作用是把多张图片合并成一张,方便后续显示。3.2 加载数据为了方便数据读取,我们实现一个FaceDataset类,实现__getitem__和__len__方法,代码如下:class FaceDataset(Dataset):
def __init__(self, data_dir="/home/zack/Files/datasets/img_align_celeba", image_size=64):
self.image_size = image_size
# 图像预处理
self.trans = transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 保持所有图片的路径
self.image_paths = []
# 读取根目录,把所有图片路径放入image_paths
for root, dirs, files in os.walk(data_dir):
for file in files:
self.image_paths.append(os.path.join(root, file))
def __getitem__(self, item):
# 读取图片,并预处理
return self.trans(Image.open(self.image_paths[item]))
def __len__(self):
return len(self.image_paths)有了上面的Dataset我们就可以获取DataLoader对象了:dataset = FaceDataset()
# 创建DataLoader对象,设置batch_size为64
dataloader = DataLoader(dataset, 64)我们可以测试一下:for data in dataloader:
print(data.shape)
break
# 输出
# torch.Size([64, 3, 64, 64])下一步就是构建网络了。3.3 构建模型这里使用卷积神经网络实现Encoder和Decoder,代码如下:class FaceAutoEncoder(nn.Module):
def __init__(self, encoded_dim=1024):
super(FaceAutoEncoder, self).__init__()
# [b, 3, 64, 64] --> [b, encoded_dim, 1, 1]
self.encoder = nn.Sequential(
nn.Conv2d(3, 64, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64 * 2, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64 * 4, 64 * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64 * 8, encoded_dim, 4, 1, 0, bias=False),
nn.LeakyReLU(0.2, inplace=True)
)
# [b, encoded_dim, 1, 1] - > [b, 3, 64, 64]
self.decoder = nn.Sequential(
nn.ConvTranspose2d(encoded_dim, 64 * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(64 * 8),
nn.ReLU(True),
nn.ConvTranspose2d(64 * 8, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4),
nn.ReLU(True),
nn.ConvTranspose2d(64 * 4, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2),
nn.ReLU(True),
nn.ConvTranspose2d(64 * 2, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=True),
nn.Tanh()
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return xEncoder部分主体为卷积层,同设置stride为2达到缩小特征图的效果,因此省去了pooling层。Pytorch处理图像时,四维数据的各个维度分别表示batch_size、通道数、高、宽。Encoder的输入形状为(batch_size * 3 * 64 * 64),输出形状为(batch_size * encoded_dim * 1 * 1),其中encoded_dim为图像编码后的维度。Decoder部分和Encoder类似,把卷积改为转置卷积,不断减少通道数,扩大特征图尺寸。Decoder输入形状为(batch_size * encoded_dim * 1 * 1),正好和Encoder输出一致,输出形状为(batch_size * 3 * 64 * 64),正好和Encoder的输入一致。因为图像预处理后的范围存在负值,因此输出层使用Tanh而不是Sigmoid。3.4 模型训练下面就可以开始训练了,在加载数据是,我们的Dataset对象只返回了一个值,这个值既是特征值也是目标值,因此叫自监督学习。代码如下:# 使用gpu
device = "cuda:0" if torch.cuda.is_available() else "cpu"
# 超参数
lr = 0.0001
batch_size = 64
image_size = 64
encoded_dim = 1024
epochs = 20
# 加载数据
dataset = FaceDataset(image_size=image_size)
dataloader = DataLoader(dataset, batch_size)
# 构建模型
model = FaceAutoEncoder(encoded_dim=encoded)
# 定义loss和优化器
optimizer = optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss()
# 训练
model.to(device)
if not model.training:
model.train()
for epoch in range(epochs):
for idx, data in enumerate(dataloader):
data = data.to(device)
# 目标值和特征值为同一批数据
target = data
# 正向传播
output = model(data)
loss = criterion(output, target)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# evaluate
if idx % 300 == 0:
with torch.no_grad():
# 编码解码
outputs = model(data)
# 讲解码后的图片按8行8列组成一张
outputs = make_grid(outputs.cpu(), normalize=True)
# pytorch中的图像是通道在第一维,这里把通道放到最后一个维度
outputs = outputs.numpy().transpose((1, 2, 0))
plt.imshow(outputs)
plt.show()
print("epoch: %s, train_loss: %.4f" % (
epoch, loss.item()
))
# 每个epoch都保存一次模型
torch.save(model.state_dict(), 'face_auto_encoder.pth')训练完成后得到效果如下图,从左到右训练轮次依次增加。最后一张图可以明显看到人脸情况,说明我们的AutoEncoder能很好的学习如何编码一张人脸。不过现在我们还不是在生成人脸,而是在对人脸图片编码和解码。下面我们来看看如何用AutoEncoder生成人脸。四、AutoEncoder生成人脸AutoEncoder分为Encoder和Decoder两个部分,Encoder可以把人脸图像转换成1024维的向量,而Decoder可以由一个1024维的向量生成一个人脸图像。由此,可以把生成人脸的问题看做是获取一个1024维向量的问题。4.1 使用随机向量现在我们做一个测试,如果给Decoder一个随机的1024维向量,能否给我们返回一个人脸图像,我们用下面代码进行测试:zdim = 1024
model = FaceAutoEncoder(encoded_dim=zdim)
model.load_state_dict(torch.load('face_auto_encoder.pth'))
model.eval()
# 生成符合标准整体分布的数据
z1 = torch.randn(64, 1024, 1, 1)
# 生成符合均分分布的数据
z2 = torch.rand(64, 1024, 1, 1)
z = [z1, z2]
with torch.no_grad():
for encoded in z:
# 使用解码器生成人脸
outputs = model.decoder(encoded)
outputs = torch.clamp(outputs, 0, 255)
grid = make_grid(outputs).numpy().transpose((1, 2, 0))
plt.imshow(grid)
plt.show()下面是得到的结果,左边是用正太分布的向量生成的,右边是用均匀分布的向量生成的,完全看不出来人脸。4.2 估计人脸向量的分布现在我们做一个假设,即假设人脸向量符合一个多元正太分布,此时我们只需要求出这个分布的均值和协方差矩阵就可以得到这个分布。得到这个分布后,就可以用这个分布采样人脸向量。此时采样到的人脸向量很有可能可以解码出一个人脸。那么均值和协方差应该怎么求呢?一种非常简单的办法就是通过统计得到,代码如下:zdim = 1024
# 加载数据
dataloader = DataLoader(
FaceDataset(image_size=64),
batch_size=128
)
# 构建模型
model = FaceAutoEncoder(encoded_dim=zdim)
model.load_state_dict(torch.load('face_auto_encoder.pth'))
# 生成mean和cov
mean = np.zeros((zdim,), dtype=np.float32)
cov = np.zeros((zdim, zdim), dtype=np.float32)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.eval()
model = model.to(device)
with torch.no_grad():
for idx, data in enumerate(dataloader):
data = data.to(device)
# 对人脸数据进行编码
encoded = model.encoder(data).view(128, -1)
# 求人脸向量的均值并累加
mean += encoded.mean(axis=0).cpu().numpy()
# 求人脸向量的协方差矩阵并累加
cov += np.cov(encoded.cpu().numpy().T)
if idx % 50 == 0:
print(f"\ridx: {idx}/{len(dataloader)}", end="")
# 归一化
mean /= (idx + 1)
cov /= (idx + 1)
np.savez('face_distribution.npz', mean, cov)这里可以用np.cov求协方差矩阵,最后把均值和协方差矩阵保存成一个npz文件。4.3 生成人脸现在我们以及估计出了人脸向量对应的正太分布,可以用np.random.multivariate_normal来对这个正太分布进行采样,然后把采样结果交给Decoder部分。具体代码如下:zdim = 1024
model = FaceAutoEncoder(encoded_dim=zdim)
model.load_state_dict(torch.load('face_auto_encoder.pth'))
model.eval()
# 加载人脸分布
distribution = np.load('face_distribution.npz')
mean = distribution['arr_0']
cov = distribution['arr_1']
# 生成编码向量
batch_size = 64
z = np.random.multivariate_normal(
mean,
cov,
batch_size
).astype(np.float32)
# 解码
with torch.no_grad():
encoded = torch.from_numpy(z).view(batch_size, zdim, 1, 1)
outputs = model.decoder(encoded)
# 把像素值映射到0-255
outputs = torch.clamp(outputs, 0, 255)
grid = make_grid(outputs).numpy().transpose((1, 2, 0))
plt.imshow(grid)
plt.show()下面是生成的一部分人脸:虽然效果不是那么好,但是也生成了人脸。不过这个效果还可以继续改进,在后续的文章中再继续给大家分享。
小乔学算法
如何打造自己的视觉版GPT
一、前言自ChatGPT出现后,Yann LeCun就一直抨击GPT无法成为AGI的最终形态,其原因在于文本只是人类智能的一种载体,有许多东西是文本不能表示的,也无法从GPT的训练中理解,Yann LeCun认为视觉大模型是一种更有前景的方向。如今GPT4V、MiniGPT5等模型已经具备了视觉能力,基于这种视觉能力,做出来非常多有趣是应用。比如:AI游戏解说、AI赛事解说、TinyBot等。不过视觉比文本要更加昂贵,以GPT4V为例,理解一个一分钟的视频需要十几到几十美元不等。今天我们要做的就是打造一个低成本的视觉版GPT,这里的低成本不仅体现在价钱,还体现在硬件资源。二、实现原理让GPT理解图像的方式有两种,分别是端到端的和非端到端的。首先是第一种非端到端的,这种方式的想法很简单,就是利用图像描述模型生成图像的描述,然后利用Prompt工程实现图像理解。我们只需要使用Image Captioning相关的网络生成一个图像描述即可。而端到端的网络则是输入图片和问题,直接返回回答。在这个过程中,需要用到两个Transformer。首先需要对输入的问题进行编码,可以使用基础的Transformer Encoder做到。为了方便Decoder处理,我们可以用一个ViT(Vision Transformer)对图像进行编码。ViT和传统的Transformer非常相似,只不过把原本输入的token变成了图片的patch。最后图像、文本被编码成768的向量,再把向量拼接,传递给Tranformer Decoder生成回答。第一种方式可以理解为GPT本身是一个盲人,但是有人在给他描述场景。而第二种方式则是GPT本身就能看到东西。在理解能力上,第二种实现要更加准确,不过资源的消耗是巨大的。因此本文使用第一种实现方式。三、基于YOLO的图像描述3.1 yolo生成描述一种简单的思路是使用YOLO、ResNet等网络来生成描述。这种描述通常比较机械,但是非常直观。这里我们以YOLO为例。首先需要使用yolo来完成目标检测的操作,这里需要使用:pip install ultralytics安装必要的库。然后是目标检测,代码如下:from ultralytics import YOLO
# 从huggingface加载模型
model = YOLO('ultralyticsplus/yolov8s')
image = 'https://github.com/ultralytics/yolov5/raw/master/data/images/zidane.jpg'
# 预测
results = model.predict(image)
# 绘制框
render = render_result(model=model, image=image, result=results[0])
render.show()使用yolo我们可以得到两个信息,第一个是图像中有什么物体,一个是物体所在的区域。根据这两个信息,我们可以使用一套模板当做图像描述,比如:图片左上角(右下角)有一个猫(狗、人)在图像中可以检测到多个物体,一个物体生成一句描述,最后合起来就是图像的最终描述了。生成描述的代码如下:from collections import Counter
from ultralyticsplus import YOLO
def location_to_description(yolo_results):
counter = Counter()
for category, box in zip(yolo_results[0].boxes.cls.cpu().numpy(), yolo_results[0].boxes.xywhn.cpu().numpy()):
x, y, w, h = box
if x < 0.33 and y < 0.33:
location = "upper left area"
elif x > 0.66 and y > 0.66:
location = "lower right area"
elif x < 0.33 and y > 0.66:
location = "lower left area"
elif x > 0.66 and y < 0.33:
location = "upper right area"
elif 0.33 < x < 0.66 < y:
location = "bottom center area"
elif 0.66 > x > 0.33 > y:
location = "top center area"
else:
location = "center area"
counter.update((f"There is ?? {yolo.model.names[int(category)]} located in the {location}.",))
description = ""
for obj in counter.most_common():
tmp, count = obj
if count > 1:
tmp = tmp.replace("is", "are")
tmp = tmp.replace("??", str(count))
description += tmp + "\n"
return descriptionlocation_to_description函数接收yolo的输出,并返回一段描述,比如用下面的图片测试:测试代码如下:if __name__ == '__main__':
yolo = YOLO('ultralyticsplus/yolov8s')
results = yolo.predict("dongwu.jpg")
print(location_to_description(results))得到如下输出:There are 2 elephant located in the upper left area.
There are 2 elephant located in the center area.
There are 2 elephant located in the top center area.因为是用规则生成的描述,难免会有一些语法问题,不过这个可以交给LLM自己解决。3.2 图文问答接下来就是把描述接入LLM了,这里选择开源的Llama2模型,使用Llamacpp部署。我们可以用fastapi写一个简单的接口,也可以用llama-cpp-python提供的api,这里选择前者。代码如下:import uvicorn
from llama_cpp import Llama
from fastapi import FastAPI, Request
app = FastAPI()
llm = Llama(
model_path=r'G:\models\llama2\llama-2-13b-chat-q4\ggml-model-q4_0.gguf',
n_ctx=2048
)
@app.post("/chat")
async def chat(request: Request):
global llm
jdata = await request.json()
prompt = jdata['prompt']
return llm(prompt, stop=['Human'])
if __name__ == '__main__':
uvicorn.run(app, host='127.0.0.1', port=8000, workers=1)这里把model_path设置成自己的模型位置即可。运行后就可以用post访问http://127.0.0.1:8000/chat 了,调用接口的代码如下:async def chat(prompt):
async with aiohttp.ClientSession() as session:
async with session.post('http://127.0.0.1:8000/chat', json={'prompt': prompt}) as response:
response = await response.json()
return response['choices'][0]['text']我们只需传入prompt即可。接下来就是界面的搭建,这里选择streamlit,代码如下:import aiohttp
import asyncio
from io import BytesIO
from PIL import Image
import streamlit as st
from ultralyticsplus import YOLO
prompt = ""
# 加载历史消息
messages = st.session_state.get('history_chat')
if not messages:
messages = []
# 加载yolo
yolo = st.session_state.get('yolo')
if not yolo:
yolo = YOLO('ultralyticsplus/yolov8s')
st.session_state['yolo'] = yolo
# 界面
st.title("图文对话")
if file := st.file_uploader(label="请上传图片"):
image = Image.open(BytesIO(file.getvalue()))
st.sidebar.image(image)
results = yolo.predict(image)
description = location_to_description(results)
st.sidebar.write(description)
prompt += (
"System: You need to answer the questions based on the description of the picture given below."
"If the description has nothing to do with the question, "
"you should just answer using your own language abilities."
"Do not imagine non-existent facts.\n\n"
f"Description: {description}."
)
for role, text in messages:
st.chat_message(role).write(text)
if message := st.chat_input("请输入问题:"):
messages.append(['user', message])
prompt += (
f"\n\nHuman: {message}. \n\nAssistant: "
)
st.chat_message('user').write(message)
response = asyncio.run(chat(prompt))
messages.append(['assistant', response])
st.chat_message('assistant').write(response)
st.session_state['history_chat'] = messages运行界面后就可以上传图片进行对话了。四、基于BLIP的图像描述在这种方法中,上面的大部分代码都可以复用,我们只需要重写一个生成描述的方法即可。代码如下:# 导入新模块
import torch
from transformers import BlipProcessor, BlipForConditionalGeneration
prompt = ""
# 加载历史消息
messages = st.session_state.get('history_chat')
if not messages:
messages = []
# 把加载yolo改成加载blip
model_path = r"G:\huggingface\hub\models--Salesforce--blip-image-captioning-large"
processor = st.session_state.get('processor')
if not processor:
processor = BlipProcessor.from_pretrained(model_path)
blip = st.session_state.get('blip')
if not blip:
blip = BlipForConditionalGeneration.from_pretrained(model_path,
torch_dtype=torch.float16).to("cuda")
# 界面
st.title("图文对话")
if file := st.file_uploader(label="请上传图片"):
image = Image.open(BytesIO(file.getvalue()))
st.sidebar.image(image)
# 使用blip生成描述
text = "a photography of"
inputs = processor(image, text, return_tensors="pt").to("cuda", torch.float16)
out = blip.generate(**inputs)
description = processor.decode(out[0], skip_special_tokens=True)
st.sidebar.write(description)
prompt += (
"System: You need to answer the questions based on the description of the picture given below."
"If the description has nothing to do with the question, "
"you should just answer using your own language abilities."
"Do not imagine non-existent facts.\n\n"
f"Description: {description}."
)
for role, text in messages:
st.chat_message(role).write(text)
if message := st.chat_input("请输入问题:"):
messages.append(['user', message])
prompt += (
f"\n\nHuman: {message}. \n\nAssistant: "
)
st.chat_message('user').write(message)
response = asyncio.run(chat(prompt))
messages.append(['assistant', response])
st.chat_message('assistant').write(response)
st.session_state['history_chat'] = messages上述代码有三处修改。第一处是导包,这里把yolo去掉了,导入了BLIP。第二处则是加载模型,同样把yolo换成了BLIP。第三处则是生成描述,这里去掉了location_to_description函数,换成了使用blip生成图像描述。其余部分维持原样,此时我们再来运行会发现结果比原本要更自然。五、总结本文使用了一种类似于文档问答的方式实现了图文QA的操作。这种方法不需要额外的训练,可以在极低的成本下实现,但是效果也取决于描述的质量,对于某些细节的把握并不是那么准确。读者可以尝试更多生成描述的方法以提升问答的准确性。
小乔学算法
基于LSTM的文本情感分析(Keras版)
一、前言文本情感分析是自然语言处理中非常基本的任务,我们生活中有很多都是属于这一任务。比如购物网站的好评、差评,垃圾邮件过滤、垃圾短信过滤等。文本情感分析的实现方法也是多种多样的,可以使用传统的朴素贝叶斯、决策树,也可以使用基于深度学习的CNN、RNN等。本文使用IMDB电影评论数据集,基于RNN网络来实现文本情感分析。二、数据处理2.1 数据预览首先需要下载对应的数据:ai.stanford.edu/~amaas/data…。点击下图位置:数据解压后得到下面的目录结构:- aclImdb
- test
- neg
- pos
- labeledBow.feat
- urls_neg.txt
- urls_pos.txt
- train
- neg
- pos这是一个电影影评数据集,neg中包含的评论是评分较低的评论,而pos中包含的是评分较高的评论。我们需要的数据分别是test里面的neg和pos,以及train里面的neg和pos(neg表示negative,pos表示positive)。下面我们开始处理。2.2 导入模块在开始写代码之前需要先导入相关模块:import os
import re
import string
import numpy as np
from tensorflow.keras import layers
from tensorflow.keras.models import Model
我的环境是tensorflow2.7,部分版本的tensorflow导入方式如下:from keras import layers
from keras.models import Model
可以根据自己环境自行替换。2.3 数据读取这里定义一个函数读取评论文件:def load_data(data_dir=r'/home/zack/Files/datasets/aclImdb/train'):
"""
data_dir:train的目录或test的目录
输出:
X:评论的字符串列表
y:标签列表(0,1)
"""
classes = ['pos', 'neg']
X, y = [], []
for idx, cls in enumerate(classes):
# 拼接某个类别的目录
cls_path = os.path.join(data_dir, cls)
for file in os.listdir(cls_path):
# 拼接单个文件的目录
file_path = os.path.join(cls_path, file)
with open(file_path, encoding='utf-8') as f:
X.append(f.read().strip())
y.append(idx)
return X, np.array(y)上述函数会得到两个列表,便于我们后面处理。2.4 构建词表在我们获取评论文本后,我们需要构建词表。即统计所有出现的词,给每个词一个编号(也可以统计一部分,多余的用unk表示)。这一步会得到一个词到id的映射和id到词的映射,具体代码如下:def build_vocabulary(sentences):
"""
sentences:文本列表
输出:
word2idx:词到id的映射
idx2word:id到词的映射
"""
word2idx = {}
idx2word = {}
# 获取标点符号及空字符
punctuations = string.punctuation + "\t\n "
for sentence in sentences:
# 分词
words = re.split(f'[{punctuations}]', sentence.lower())
for word in words:
# 如果是新词
if word not in word2idx:
word2idx[word] = len(word2idx)
idx2word[len(word2idx) - 1] = word
return word2idx, idx2word有了上面的两个映射后,我们就可以将句子转换成id序列,也可以把id序列转换成句子,在本案例中只需要前者。2.5 单词标记化(tokenize)因为我们模型需要固定长度的数据,因此在标记化时我们对句子长度进行限制:def tokenize(sentences, max_len=300):
"""
sentences:文本列表
tokens:标记化后的id矩阵,形状为(句子数量, 句子长度)
"""
# 生产一个形状为(句子数量, 句子长度)的矩阵,默认用空字符的id填充,类型必须为int
tokens = np.full((len(sentences), max_len),
fill_value=word2idx[''], dtype=np.int32)
punctuations = string.punctuation + "\t\n "
for row, sentence in enumerate(sentences):
# 分词
words = re.split(f'[{punctuations}]', sentence.lower())
for col, word in enumerate(words):
if col >= max_len:
break
# 把第row个句子的第col个词转成id
tokens[row, col] = word2idx.get(word, word2idx[''])
return tokens使用该函数就可以将句子列表转换成ndarray了。三、构建模型并训练3.1 构建模型这里使用RNN来实现,模型结构如下图:下面我们用程序实现这个模型:def build_model():
vocab_size = len(word2idx)
# 构建模型
inputs = layers.Input(shape=max_len)
x = layers.Embedding(vocab_size, embedding_dim)(inputs)
x = layers.LSTM(64)(x)
outputs = layers.Dense(1, activation='sigmoid')(x)
model = Model(inputs, outputs)
return model
这里需要注意下面几个地方:Embedding层的输入是(batch_size,max_len),输出是(batch_size,max_len,embedding_dim)LSTM层的输入是(batch_size,max_len,embedding_dim),输出是(batch_size,units),units就是LSTM创建时传入的值。3.2 训练模型下面就可以使用前面实现好的几个方法开始训练模型了,代码如下:# 超参数
max_len = 200
batch_size = 64
embedding_dim = 256
# 加载数据
X_train, y_train = load_data()
X_test, y_test = load_data('/home/zack/Files/datasets/aclImdb/test')
X = X_train + X_test
word2idx, idx2word = build_vocabulary(X)
X_train = tokenize(X_train, max_len=max_len)
X_test = tokenize(X_test, max_len=max_len)
# 构建模型
model = build_model()
model.summary()
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(
X_train, y_train,
batch_size=batch_size,
epochs=20,
validation_data=[X_test, y_test],
)经过20个epoch的训练后,训练集准确率可以达到99%,而验证集准确率在80%左右,模型有一定程度的过拟合,可以通过修改模型结构或调节超参数来进行优化。比如修改max_len的大小、使用预训练的词嵌入、修改RNN中units的大小、修改embedding_dim的大小等。还可以添加BatchNormalization、Dropout层。四、使用模型模型训练好后,可以用predict来预测,predict的输入和embedding层的输入是一样的:while True:
sentence = input("请输入句子:")
tokenized = tokenize([sentence], max_len)
output = model.predict(tokenized)
print('消极' if output[0][0] >= 0.5 else '积极')下面是一些测试结果:请输入句子:this is a bad movie
消极
请输入句子:this is a good movie
积极
请输入句子:i like this movie very much
积极
请输入句子:i hate this movie very much
积极
请输入句子:i will never see this movie again
积极效果不是特别理想,因为训练样本通常为长文本,而现在测试的是短文本。
小乔学算法
基于的Transformer文本情感分析(Keras版)
一、前言从2017年起,RNN系列网络逐渐被一个叫Transformer的网络替代,发展到现在Transformer已经成为自然语言处理中主流的模型了,而且由Transformer引来了一股大语言模型热潮。从Bert到GPT3,再到如今的ChatGPT。Transformer实现了人类难以想象的功能,而且仍在不停发展。本文将基于Transformer的Encoder部分,实现文本情感分析任务。二、数据处理数据处理可以参考上一篇基于LSTM的文本情感分析(Keras版)的代码,本文使用另一种简单的方法实现。2.1 数据预览首先需要下载对应的数据:ai.stanford.edu/~amaas/data…。点击下图位置:数据解压后得到下面的目录结构:数据解压后得到下面的目录结构:- aclImdb
- test
- neg
- pos
- labeledBow.feat
- urls_neg.txt
- urls_pos.txt
- train
- neg
- pos
这是一个电影影评数据集,neg中包含的评论是评分较低的评论,而pos中包含的是评分较高的评论。我们需要的数据分别是test里面的neg和pos,以及train里面的neg和pos(neg表示negative,pos表示positive)。下面我们开始处理。2.2 导入模块在开始写代码之前需要先导入相关模块:import os
import keras
import tensorflow as tf
from keras import layers我的环境是tensorflow2.7,部分版本的tensorflow导入方式不同,可以根据自己环境自行替换。2.3 读取数据这里定义一个函数读取评论文件:def load_data(data_dir=r'/home/zack/Files/datasets/aclImdb/train'):
"""
data_dir:train的目录或test的目录
输出:
X:评论的字符串列表
y:标签列表(0,1)
"""
classes = ['pos', 'neg']
X, y = [], []
for idx, cls in enumerate(classes):
# 拼接某个类别的目录
cls_path = os.path.join(data_dir, cls)
for file in os.listdir(cls_path):
# 拼接单个文件的目录
file_path = os.path.join(cls_path, file)
with open(file_path, encoding='utf-8') as f:
X.append(f.read().strip())
y.append(idx)
return X, np.array(y)上述函数会得到两个列表,便于我们后面处理。2.4 构建词表及tokenize前面部分的处理和之前的文章一样,而构建词表和tokenize的操作则用keras的api来实现。代码如下:X, y = load_data()
vectorization = TextVectorization(max_tokens=vocab_size, output_sequence_length=seq_len)
# 构建词表
vectorization.adapt(X)
# tokenize
X = vectorization(X)其中adapt方法接收的是句子列表,调用adapt方法后keras会帮我们构建词表,而后用vectorization(X)可以讲句子列表转换成词id列表。三、构建模型这里使用Transformer的Encoder部分作为我们网络主干。我们需要实现两个部分,分别是PositionalEmbedding、TransformerEncoder,并将两个部分组成情感分类模型。3.1 TransformerEncoder来简单介绍一下Transformer,这里粗略看一下Transformer的各个部件。Transformer结构如下图:其中左半部分是Encoder,右半部分是Decoder,我们要实现的就是Encoder部分。我们从低向上来看一看Decoder部分。(1)Input Embedding和Positional EncodingTransformer的输入是一个id列表,形状为batch_size × sequence_len,输入首先会经过一个简单的Embedding层(Input Embedding)得到一个形状为batch_size × sequence_len × embed_dim,我们称为te。te里面包含了sequence_len个词的嵌入,其中te的第一个嵌入会与向量pe[0]相加,te的第二个嵌入会与向量t[1]相加,依次类推。因此pe的形状应该为sequence_len × embed_dim,pe里面就包含了位置信息。在原始论文中pe有固定公式获得,位置信息固定后pe就固定了,而本文在实现时用一个叫Positional Embedding方式替代它,实现代码如下:class PositionalEmbedding(layers.Layer):
def __init__(self, input_size, seq_len, embed_dim):
super(PositionalEmbedding, self).__init__()
self.seq_len = seq_len
# 词嵌入
self.tokens_embedding = layers.Embedding(input_size, embed_dim)
# 位置嵌入
self.positions_embedding = layers.Embedding(seq_len, embed_dim)
def call(self, inputs, *args, **kwargs):
# 生成位置id
positions = tf.range(0, self.seq_len, dtype='int32')
te = self.tokens_embedding(inputs)
pe = self.positions_embedding(positions)
return te + pe这里使用了和词嵌入类似的思想,让网络自己学习位置信息。(2)Multi-Head AttentionMulti-Head Attention可以认为是对一个序列做了多次Self-Attention,然后把每次Self-Attention的结构拼接起来。在Keras和Pytorch中都有对应的实现,这里我们看看应该如何使用。在创建MultiHeadAttention层时,需要指定头的数量以及key的维数,在正向传播时,如果传入两个相同的序列那么就是在做Self-Attention,代码如下:from keras import layers
import tensorflow as tf
# 形状为batch_size × sequence_len × embed_dim
X = tf.random.uniform((3, 10, 5))
mta = layers.MultiHeadAttention(4, 10)
out = mta(X, X)
# 输出:(3, 10, 5)
print(out.shape)从代码可以看出MultiHeadAttention的输入与输出形状一致。(3)Add & Norm在经过Attention后,我们把Attention的输入和Attention的输出都放入了一个叫Add & Norm的模块中。这里其实就是把两者相加,而后经过LayerNormalization,其结构如下图:把词嵌入x1、x2输入Attention得到z1、z2,然后把x1、x2组成矩阵X,z1、z2组成矩阵Z,计算LayerNorm(X+Z),输入下一层,代码实现如下:# 定义层
mta = layers.MultiHeadAttention(4, 10)
ln = layers.LayerNormalization()
# 正向传播
X = tf.random.uniform((3, 10, 5))
Z = mta(X, X)
out = ln(X+Z)
# 输出 (3, 10, 5)
print(out.shape)
(4)Feed ForwardFeed Forward就是简单的全连接层,不过这里是对单个向量进行全连接,即z1-zn每个向量都单独经过Linear层。另外Feed Forward层有两层全连接,先放大再缩小,代码如下:import keras
from keras import layers
import tensorflow as tf
mta = layers.MultiHeadAttention(4, 10)
ln = layers.LayerNormalization()
# Feed Forward层
ff = keras.Sequential([
layers.Dense(10, activation='relu'),
layers.Dense(5)
])
X = tf.random.uniform((3, 10, 5))
Z = mta(X, X)
Z = ln(X+Z)
out = ff(Z)
# 输出 (3, 10, 5)
print(out.shape)
到此我们就把Encoder的各个部件说明了。下面来实现TransformerEncoder层。3.2 Encoder的实现现在我们把上面各个部分写成一个TransformerEncoder类,这里不包含PositionalEmbedding,代码如下:class TransformerEncoder(layers.Layer):
def __init__(self, embed_dim, hidden_dim, num_heads, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
# Multi-Head Attention层
self.attention = layers.MultiHeadAttention(
num_heads=num_heads, key_dim=embed_dim
)
# Feed Forward层
self.feed_forward = keras.Sequential([
layers.Dense(hidden_dim, activation='relu'),
layers.Dense(embed_dim)
])
# layernorm层
self.layernorm1 = layers.LayerNormalization()
self.layernorm2 = layers.LayerNormalization()
def call(self, inputs, *args, **kwargs):
# 计算Self-Attention
attention_output = self.attention(inputs, inputs)
# 进行第一个Layer & Norm
ff_input = self.layernorm1(inputs + attention_output)
# Feed Forward
ff_output = self.feed_forward(ff_input)
# 进行第二个Layer & Norm
outputs = self.layernorm2(ff_input + ff_output)
return outputs
现在我们实现的这个TransformerEncoder它接收一个batch_size × sequence_len × embed_dim的张量,输出一个形状一样的张量。如果要用于情感分析,我们可以在输出后面拼接全局平均池化和全连接层。3.3 分类模型下面我们使用前面的PositionalEmbedding和TransformerEncoder实现我们的文本分类网络,代码如下:# 超参数
vocab_size = 20000
seq_len = 180
batch_size = 64
hidden_size = 1024
embed_dim = 256
num_heads = 8
# 加载数据
X_train, y_train = load_data()
X_test, y_test = load_data(r'/home/zack/Files/datasets/aclImdb/test')
vectorization = layers.TextVectorization(
max_tokens=vocab_size,
output_sequence_length=seq_len,
pad_to_max_tokens=True
)
vectorization.adapt(X_train)
X_train = vectorization(X_train)
X_test = vectorization(X_test)
# 构建模型
inputs = layers.Input(shape=(seq_len,))
x = PositionalEmbedding(vocab_size, seq_len, embed_dim)(inputs)
x = TransformerEncoder(embed_dim, hidden_size, num_heads)(x)
x = layers.GlobalAveragePooling1D()(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation='sigmoid')(x)
model = keras.Model(inputs, outputs)
# 训练
model.compile(loss='binary_crossentropy', metrics=['accuracy'])
model.fit(
X_train,
y_train,
epochs=20,
batch_size=batch_size,
validation_data=[X_test, y_test]
)
最终训练后在测试集上准确率在85%左右。
小乔学算法
如何去除图片马赛克?
一、前言对图像不了解的人时常妄想去除马赛克是可以实现的,严格意义来说这确实是无法实现的。而深度学习是出现,让去除马赛克成为可能。为了理解去除马赛克有多难,我们需要知道马赛克是什么。观感上,马赛克就是方块感。当我们观察图像像素时, 马赛克表现为下图的情况:原图右下角有十字,而添加马赛克后右下角一片都变成了同一像素,如果我们没保留原图,那么我们无法还原,也不知道是否还原了原图。因为原图已经被破坏了,这也是为什么马赛克是不可修复的。那神经网络又是如何让修复成为可能呢?其实无论什么方式的修复,都是一种估计,而不是真正的修复。神经网络去除马赛克的操作其实是生成马赛克那部分内容,然后替代马赛克,从而达到修复的效果。这种修复并不是还原,而是想象。假如我们对一张人脸打了马赛克,神经网络可以去除马赛克,但是去除后的人脸不再是原来那个人了。二、实现原理2.1 自编码器图像修复的方法有很多,比如自编码器。自编码器是一种自监督模型,结构简单,不需要人为打标,收敛迅速。其结构如图:编码器部分就是用于下采样的卷积网络,编码器会把图片编码成一个向量,而解码器则利用转置卷积把编码向量上采样成和原图大小一致的图片,最后我们把原图和生成结果的MSE作为损失函数进行优化。当模型训练好后,就可以用编码器对图片进行编码。2.2 自编码器去除马赛克那自编码器和去除马赛克有什么联系呢?其实非常简单,就是原本我们是输入原图,期望解码器能输出原图。这是出于我们希望模型学习如何编码图片的原图。而现在我们想要模型去除马赛克,此时我们要做的就是把马赛克图片作为输入,而原图作为输出,这样来训练就可以达到去除马赛克的效果了:关于关于这种实现可以参考:juejin.cn/post/721068…2.3 自编码器的问题自编码器有个很明显的问题,就是图片经过编码器后会损失信息,而解码器的结果自然也会存在一些问题。这样既达不到去除马赛克的功能,连还原的原图都有一些模糊。这里可以利用FPN的思想来改进,当自编码器加入FPN后,就得到了UNet网络结构。2.4 UNet网络UNet结构和自编码器类似,是一个先下再上的结构。和自编码器不同的时,UNet会利用编码器的每个输出,将各个输出与解码器的输入进行concatenate,这样就能更好地保留原图信息。其结构如下图:UNet原本是用于图像分割的网络,这里我们用它来去除马赛克。在UNet中,有几个部分我们分别来看看。2.4.1 ConvBlock在UNet中,有大量连续卷积的操作,这里我们作为一个Block(蓝色箭头),它可以实现为一个层,用PyTorch实现如下:class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU()
)
def forward(self, inputs):
return self.model(inputs)这里其实就是两次卷积操作,这里的目的是提取当前感受野的特征。2.4.2 ConvDown经过连续卷积后,会使用卷积网络对图片进行下采样,这里把stride设置为2即可让图片缩小为原来的1/2。我们同样可以实现为层:class ConvDown(nn.Module):
def __init__(self, channels):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(channels, channels, 3, 2, 1),
nn.BatchNorm2d(channels),
nn.ReLU()
)
def forward(self, inputs):
return self.model(inputs)这里只有一个卷积,而且stride被设置为了2。2.4.3 ConvUp接下来是解码器部分,这里多了一个上采用的操作,我们可以用转置卷积完成,代码如下:class ConvUp(nn.Module):
def __init__(self, channels):
super().__init__()
self.model = nn.Sequential(
nn.ConvTranspose2d(channels, channels // 2, 2, 2),
nn.BatchNorm2d(channels // 2),
nn.ReLU()
)
def forward(self, inputs):
return self.model(inputs)上面是层可以把图片尺寸扩大为2倍,同时把特征图数量缩小到1/2。这里缩小特征图的操作是为了concatenate操作,后面详细说。三、完整实现首先,导入需要用的模块:import os
import random
import torch
from torch import nn
from torch import optim
from torch.utils import data
import matplotlib.pyplot as plt
from torchvision import transforms
from torchvision.transforms import ToTensor
from PIL import Image, ImageDraw, ImageFilter
from torchvision.utils import make_grid下面开始具体实现。3.1 创建Dataset首先创建本次任务需要的数据集,分布大致相同的图片即可,代码如下:class ReConstructionDataset(data.Dataset):
def __init__(self, data_dir=r"G:/datasets/lbxx", image_size=64):
self.image_size = image_size
# 图像预处理
self.trans = transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
# transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 保持所有图片的路径
self.image_paths = []
# 读取根目录,把所有图片路径放入image_paths
for root, dirs, files in os.walk(data_dir):
for file in files:
self.image_paths.append(os.path.join(root, file))
def __getitem__(self, item):
# 读取图片,并预处理
image = Image.open(self.image_paths[item])
return self.trans(self.create_blur(image)), self.trans(image)
def __len__(self):
return len(self.image_paths)
@staticmethod
def create_blur(image, return_mask=False, box_size=200):
mask = Image.new('L', image.size, 255)
draw = ImageDraw.Draw(mask)
upper_left_corner = (random.randint(0, image.size[0] - box_size), random.randint(0, image.size[1] - box_size))
lower_right_corner = (upper_left_corner[0] + box_size, upper_left_corner[1] + box_size)
draw.rectangle([lower_right_corner, upper_left_corner], fill=0)
masked_image = Image.composite(image, image.filter(ImageFilter.GaussianBlur(15)), mask)
if return_mask:
return masked_image, mask
else:
return masked_imageDataset的实现与以往基本一致,实现init、getitem、len方法,这里我们还实现了一个create_blur方法,该方法用于生成矩形马赛克(实际上是高斯模糊)。下面是create_blur方法生成的图片:3.2 网络构建这里我们需要使用前面的几个子单元,先实现编码器,代码如下:class UNetEncoder(nn.Module):
def __init__(self):
super().__init__()
self.blk0 = ConvBlock(3, 64)
self.down0 = ConvDown(64)
self.blk1 = ConvBlock(64, 128)
self.down1 = ConvDown(128)
self.blk2 = ConvBlock(128, 256)
self.down2 = ConvDown(256)
self.blk3 = ConvBlock(256, 512)
self.down3 = ConvDown(512)
self.blk4 = ConvBlock(512, 1024)
def forward(self, inputs):
f0 = self.blk0(inputs)
d0 = self.down0(f0)
f1 = self.blk1(d0)
d1 = self.down1(f1)
f2 = self.blk2(d1)
d2 = self.down2(f2)
f3 = self.blk3(d2)
d3 = self.down3(f3)
f4 = self.blk4(d3)
return f0, f1, f2, f3, f4这里就是ConvBlok和ConvDown的n次组合,最终会得到一个1024×4×4的特征图。在forward中,我们返回了5个ConvBlok返回的结果,因为在解码器中我们需要全部使用。接下来是解码器部分,这里与编码器相反,代码如下:class UNetDecoder(nn.Module):
def __init__(self):
super().__init__()
self.up3 = ConvUp(1024)
self.blk3 = ConvBlock(1024, 512)
self.up2 = ConvUp(512)
self.blk2 = ConvBlock(512, 256)
self.up1 = ConvUp(256)
self.blk1 = ConvBlock(256, 128)
self.up0 = ConvUp(128)
self.blk0 = ConvBlock(128, 64)
self.last_conv = nn.Conv2d(64, 3, 3, 1, 1)
def forward(self, inputs):
f0, f1, f2, f3, f4 = inputs
u3 = self.up3(f4)
df2 = self.blk3(torch.concat((f3, u3), dim=1))
u2 = self.up2(df2)
df1 = self.blk2(torch.concat((f2, u2), dim=1))
u1 = self.up1(df1)
df0 = self.blk1(torch.concat((f1, u1), dim=1))
u0 = self.up0(df0)
f = self.blk0(torch.concat((f0, u0), dim=1))
return torch.tanh(self.last_conv(f))解码器的inputs为编码器的5组特征图,在forward时需要与上采样结果concatenate。最后,整个网络组合起来,代码如下:class ReConstructionNetwork(nn.Module):
def __init__(self):
super().__init__()
self.encoder = UNetEncoder()
self.decoder = UNetDecoder()
def forward(self, inputs):
fs = self.encoder(inputs)
return self.decoder(fs)3.3 网络训练现在各个部分都完成了,可以开始训练网络:device = "cuda" if torch.cuda.is_available() else "cpu"
def train(model, dataloader, optimizer, criterion, epochs):
model = model.to(device)
for epoch in range(epochs):
for iter, (masked_images, images) in enumerate(dataloader):
masked_images, images = masked_images.to(device), images.to(device)
outputs = model(masked_images)
loss = criterion(outputs, images)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (iter + 1) % 100 == 1:
print("epoch: %s, iter: %s, loss: %s" % (epoch + 1, iter + 1, loss.item()))
with torch.no_grad():
outputs = make_grid(outputs)
img = outputs.cpu().numpy().transpose(1, 2, 0)
plt.imshow(img)
plt.show()
torch.save(model.state_dict(), '../outputs/reconstruction.pth')
if __name__ == '__main__':
dataloader = data.DataLoader(ReConstructionDataset(r"G:\datasets\lbxx"), 64)
unet = ReConstructionNetwork()
optimizer = optim.Adam(auto_encoder.parameters(), lr=0.0002)
criterion = nn.MSELoss()
train(unet, dataloader, optimizer, criterion, 20)训练完成后,就可以用来去除马赛克了,代码如下:
dataloader = data.DataLoader(ReConstructionDataset(r"G:\datasets\lbxx"), 64, shuffle=True)
unet = ReConstructionNetwork().to(device)
unet.load_state_dict(torch.load('../outputs/reconstruction.pth'))
for masked_images, images in dataloader:
masked_images, images = masked_images.to(device), images.to(device)
with torch.no_grad():
outputs = unet(masked_images)
outputs = torch.concatenate([images, masked_images, outputs], dim=-1)
outputs = make_grid(outputs)
img = outputs.cpu().numpy().transpose(1, 2, 0)
img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
Image.fromarray(img).show()
下面是生成结果。左侧为原图,中间为添加马赛克后的图片,右侧则是去除马赛克后的结果:整体来说效果比较不错。本文的方法不只可以用来去除马赛克,还可以完成图像重构。比如老化的图片、被墨汁污染的图片等,都可以用本文的方法完成重构。
小乔学算法
如何攻击神经网络?人工智能VS人工智障
一、前言自2012年起,人工智能快速发展,频繁出现在大众视野。从Alpha GO到ChatGPT,人工智能已成为不可阻挡的发展趋势。但是由于神经学习的黑盒性质,导致神经网络难以解释,且难以控制。即使像ChatGPT这种强大的模型,在联网的情况下也会出现一些低级错误。神经网络出错让人很难琢磨,比如人脸检测有时会检测出和人脸毫无相关的人脸(对人而言)。ChatGPT也会回答一些毫无头绪的答案,比如GPT3.5当遇到问题“2022飞洒发生范德萨分”时,会出现短路情况。又或是李世石的“神之一手”,都是神经网络难以琢磨的表现。今天的主题并非讨论为什么会出现这些情况,而是讨论如何创造这些情况,也就是攻击神经网络。看完今天的内容,相信大家对神经网络的智能会有新的认识。二、网络训练现在不管是什么网络,几乎用的都是梯度下降算法。首先需要定义一个网络,这里用y=f(θ;x)表示,其中θ是网络的权重。θ可选的值有无穷种可能,但是只有少数θ可以得到比较好的结果。为了评估θ的好坏,可以定义一个损失函数loss=L(f(θ;x), target),其中target是真实值。现在只需要找一组让loss最小的θ就能完成训练。但是f(θ;x)是一个非常复杂的函数,L(f(θ;x), target)则更为复杂,无法直接给出解析解,所以需要使用迭代算法求解θ。深度学习中用的就是梯度下降算法,梯度下降算法的表达式如下:θ = θ − η ∂L∂θ\theta = \theta - \eta \frac{\partial L}{\partial \theta}θ = θ − η ∂θ∂L其中η是用来调节更新幅度的参数,叫学习率。当loss比较小时,网络可以正确预测结果。而攻击也是围绕梯度和loss来的。攻击网络就是生成一个对抗样本,让这个样本输入网络后得到一个较大的loss。或者让对抗样本与假真实值有较小的loss。三、对抗攻击(Adversarial Attack)攻击神经网络的方式有很多,基于不同的先验知识可以分为黑盒攻击和白盒攻击。基于不同的目的,可以分为源/目标误分类、针对性误分类、误分类、置信度降低。其中误分类攻击目的最简单,就是让模型分类错误,这也是本文要实现的一种攻击。其中白盒攻击比较简单,在白盒攻击中,我们对模型了如指掌。我们知道网络的每一处细节,也可以拿到网络进行推理和梯度回传。在白盒攻击中,可以通过梯度信息来生成对抗样本。训练的过程中我们的目的是降低loss,而对抗的过程则是增加loss。当生成的对抗样本计算出较大loss时,网络会有较大概率分类错误,这样就达到了欺骗网络的目的。而黑盒攻击要更为复杂,黑盒攻击假设我们不知道网络的详细信息,网络结构、网络权重,但是我们可以使用这个网络。我们知道网络输入什么,以及当前输入对应的输出。这种情况下,要攻击神经网络会比较复杂。已经上线的网络通常都属于黑盒情况,在对抗样本提出后,大家并不认为在黑盒情况下能有正确攻击网络。而GAN的作者Goodfellow则发现情况并非如此。黑盒攻击可以用集成学习的方式来实现,在本文不会详细介绍。本文主要针对白盒攻击进行讨论。四、Fast Gradient Sign Attack实现攻击的方式也是多种多样的,本文使用一种名为Fast Gradient Sign Attack(FGSA)的攻击方式,这种方式利用梯度信息对输入进修改,来达到攻击的目的。在前面已经提到了,模型的训练是使用梯度下降算法实现的。这里需要注意两个点,一个是更新方向,一个是更新参数。在训练过程中,我们的目的是minimize L(f(θ;x), target),并且是找一组最优的θ。由此可以知道我们要更新的参数是θ,并且更新方向是梯度的反方向。攻击模型的目的则不同,首先讨论误分类的情况。在误分类的情况中,我们的目的是生成对抗样本,使模型分类错误,此时我们的目的是让L(f(θ;x), target)比较大。这里我们要找的是对抗样本,因此更新的参数是x,并且方向是梯度方向。那么生成对抗样本的操作可以用下面公式表示:adversarialX = x + ϵ ∂L∂ xadversarialX = x + \epsilon \frac{\partial L}{\partial x}adversarialX = x + ϵ ∂ x∂L在FGSA中,不考虑梯度大小的问题,只关注梯度方向。因此FGSA中应该用下面公式表示:adversarialX = x + ϵ sign(∂L∂ x)adversarialX = x + \epsilon sign(\frac{\partial L}{\partial x})adversarialX = x + ϵ sign(∂ x∂L)其中sign是符号函数,会返回梯度的正负号。四、代码实现接下来我们用代码来实现FGSA攻击,这里使用白盒攻击。所以需要先实现一个网络,这里以手写数字为例。4.1 手写数字识别白盒攻击的特点是我们知道网络的全部细节,因此我们自己实现一个网络,这个网络的所有细节我们都可以知道。网络可以自由设计,此处我们选择用一个两层的卷积神经网络,训练代码如下:import torch
from torch import nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
from collections import OrderedDict
device = "cuda" if torch.cuda.is_available() else "cpu"
# 超参数
epochs = 10
batch_size = 64
lr = 0.001
# 1、加载数据
train_dataset = datasets.MNIST('./', True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size)
# 2、构建模型
class DigitalNet(nn.Module):
def __init__(self):
super(DigitalNet, self).__init__()
self.model = nn.Sequential(OrderedDict({
"conv1": nn.Conv2d(1, 6, 5),
"relu1": nn.ReLU(),
"pool1": nn.MaxPool2d(2),
"conv2": nn.Conv2d(6, 16, 5),
"relu2": nn.ReLU(),
"pool2": nn.MaxPool2d(2),
"flatten": nn.Flatten(),
"fc1": nn.Linear(4 * 4 * 16, 128),
"relu3": nn.ReLU(),
"fc2": nn.Linear(128, 10),
}))
def forward(self, inputs):
return self.model(inputs)
# 3、定义loss
loss_fn = nn.CrossEntropyLoss()
# 4、定义优化器
model = DigitalNet().to(device)
optimizer = optim.Adam(model.parameters(), lr)
# 5、训练
for epoch in range(epochs):
for image, target in train_loader:
image, target = image.to(device), target.to(device)
# 正向传播
output = model(image)
loss = loss_fn(output, target)
model.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
print(f'epoch: {epoch+1}, loss: {loss.item()}')
torch.save(model.state_dict(), 'digital.pth')这里为了方便,省略了测试相关代码,准确率的计算也省去了。代码运行完成后,可以得到一个digital.pth文件,这个就是模型文件。后续生成对抗样本需要使用到这个文件。4.2 FGSA得到模型后,我们就可以开始生成对抗样本了。这里使用FGSA方法,在前面我们推导出FGSA的表达式为:adversarialX = x + ϵ sign(∂L∂ x)adversarialX = x + \epsilon sign(\frac{\partial L}{\partial x})adversarialX = x + ϵ sign(∂ x∂L)现在只需要用代码把这个函数实现即可,这个函数有两个输入,分别是输入x和x的梯度。该函数的操作可以分为下面几步:获取梯度方向代入上述公式得到对抗样本代码如下:def fgsa_attack(x, epsilon, x_grad):
# 获取x梯度方向
sign_grad = x_grad.sign()
# 更新x,让x往梯度方向更新
adversarial_x = x + epsilon * sign_grad
# 把结果映射到0-1之间
adversarial_x = torch.clamp(adversarial_x, 0, 1)
return adversarial_x其中x是我们已有的数据,epsilon是超参数,需要我们自己设置,x_grad是x的梯度信息,这个还没有获取。接下来要做的就是拿到x_grad,即求损失函数对x的导数。默认情况下x是不会求导的,因此需要设置x自动求导,只需要下面一句即可:x.requires_grad = True而后要做的就是计算loss,反向传播即可。调用loss.backward()方法后,张量中就存储了梯度信息,而x的梯度可以通过下面方式获取:x_grad = x.grad.data这样fgsa_attack需要的值我们都有了,接下来就可以生成对抗样本了。攻击网络的完整代码如下:import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, utils
from torchvision.transforms import ToTensor
from collections import OrderedDict
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
# 超参数
epochs = 10
batch_size = 64
lr = 0.001
# 1、加载数据
train_dataset = datasets.MNIST('./', True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size)
loss_fn = nn.CrossEntropyLoss()
# 加载模型
model = DigitalNet()
model.load_state_dict(torch.load('digital.pth'))
for image, target in train_loader:
# 设置输入自动求导
image.requires_grad = True
output = model(image)
loss = loss_fn(output, target)
model.zero_grad()
loss.backward()
# loss对image的梯度
image_grad = image.grad.data
# 对image进行修改
adversarial_x = fgsa_attack(image, .15, image_grad)
# 对攻击数据预测
output = model(adversarial_x)
grid = utils.make_grid(adversarial_x, normalize=True)
with torch.no_grad():
grid = grid.cpu().numpy().transpose((1, 2, 0))
print(output.argmax(dim=1).cpu().numpy().reshape((8, 8)))
plt.imshow(grid)
plt.show()
break这里测试了64张图像,下面是带有攻击性的输入图像:对人来说,这副图像依旧是原来的数字,但是对神经网络来说并非如此了,下面的矩阵是各个图像对应的预测结果:[[2 9 3 8 8 9 8 8]
[3 3 0 8 8 8 8 8]
[8 7 3 2 9 5 8 8]
[3 3 8 3 7 2 7 7]
[9 7 0 2 3 0 2 9]
[8 3 5 8 8 8 8 8]
[5 0 5 0 5 3 8 7]
[5 8 9 8 2 7 3 5]]4.3 分类成指定类别在前面的程序中,我们只要求生成数据,让网络错误分类。在一些场景下,我们需要生成数据,让网络分类成指定类别,比如想欺骗人脸识别,就需要生成可以让网络识别为某人的数据。这个应该如何实现呢?其实非常简单,错误分类的操作就是改变输入,让输入网梯度方向更新,此时loss会增加,从而达到错误分类的效果。错误分类成某个类别则不太一样,比如现在想生成数据,让模型错误分类成数字1,我们要做的是让loss_fn(output, 1) 变小,因此需要修改两个地方:目标值改为1(具体类别)数据往梯度反方向更新下面把fgsa_attack函数修改为如下:def fgsm_attack(x, epsilon, x_grad):
# 获取梯度的反方向
sign_grad = -x_grad.sign()
# 让输入添加梯度信息,即让输入添加能让loss减小的信息
adversarial_x = x + epsilon * sign_grad
# 把结果映射到0-1之间
adversarial_x = torch.clamp(adversarial_x, 0, 1)
return adversarial_x把攻击的代码修改为:import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, utils
from torchvision.transforms import ToTensor
from collections import OrderedDict
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
# 超参数
epochs = 10
batch_size = 64
lr = 0.001
# 1、加载数据
train_dataset = datasets.MNIST('./', True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size)
loss_fn = nn.CrossEntropyLoss()
# 加载模型
model = DigitalNet()
model.load_state_dict(torch.load('digital.pth'))
for image, target in train_loader:
# 设置输入自动求导
image.requires_grad = True
output = model(image)
# 把目标值修改为1
target[::] = 1
loss = loss_fn(output, target)
model.zero_grad()
loss.backward()
# loss对image的梯度
image_grad = image.grad.data
# 对image进行修改
adversarial_x = fgsa_attack(image, .2, image_grad)
# 对攻击数据预测
output = model(adversarial_x)
grid = utils.make_grid(adversarial_x, normalize=True)
with torch.no_grad():
grid = grid.cpu().numpy().transpose((1, 2, 0))
print(output.argmax(dim=1).cpu().numpy().reshape((8, 8)))
plt.imshow(grid)
plt.show()
break这里做的就是把目标值改为了1,并且调整了fgsa_attack的epsilon值,得到的攻击图像如下:模型对图像的预测结果为:[[3 0 1 1 4 8 1 1]
[1 1 1 1 3 6 1 9]
[0 1 1 1 1 1 1 1]
[1 1 8 1 0 1 1 1]
[5 1 1 1 1 0 1 1]
[1 1 1 1 3 4 8 1]
[1 1 1 9 0 8 4 1]
[0 4 1 1 9 1 5 9]]虽然结果并非全为1,但是预测结果为1的数量远多于真实为1的数量,这表明此次攻击是成功的。五、总结神经网络虽然非常强大,但是对神经网络的理解仍是一个待解决的问题。由于神经网络非常庞大,我们难以把握每一个细节,很难确定网络如何推理出结果,正因为此,一个看似训练良好的模型在应用的实际任务时会出现很多离奇现象。只有理解这些离奇现象为何会发生,才能更好地理解模型,并改进模型。因为现在大多数网络都是使用梯度下降来更新模型,因此梯度是攻击网络的一个很好的突破点。在上面对网络进行了两种攻击,看似都非常有效。但是白盒攻击的前提是我们能够知道网络具体结构,对网络有完全的控制能力,但是在实际情况中这并不常见,因此也不用过于担心自己的网络会被攻击。
小乔学算法
AutoEncoder实现人脸渐变
一、前言在上一篇博客:juejin.cn/post/721068…, 分享了如何用AutoEncoder生成人脸,在本篇博客,依然围绕人脸来讨论。如何从一张人脸自然地变换到另一张人脸,或者从年轻慢慢变老,使用AutoEncoder可以很容易实现这样的功能。二、实现原理AutoEncoder的目的是对输入进行编码,希望这个编码能够很好的还原原有数据。当AutoEncoder训练完成后,我们可以对任意的图像进行编码,这样就可以用一个低维的向量表示图片。那这个低维向量有什么作用呢?假设我们使用AutoEncoder把图像编码成1024维的向量,通过修改该向量,就可以达到修改图像的效果,但是我们现在的问题是如何修改向量。对于用人脸数据训练的AutoEncoder,如果我们胡乱修改编码向量,会导致Decoder解码的内容不像人脸。在今天的例子中,需要实现人脸的渐变效果。我们可以把人脸的渐变转换成向量的渐变,使用插值算法可以很简单的实现向量渐变。比如人脸A的向量为z1,人脸B的向量为z2,此时在z1和z2之间插值4个向量,然后把这几个向量用Decoder解码,得到的效果就是人脸渐变效果。比如下面的图片,左上角和右下角的图像是用真实图像的向量解码出来的,而中间的图像则是通过插值算法生成的向量得到的图。即使性别不同也可以很自然的变换。三、插值算法插值算法有很多,这里选择最简单的线性插值。就是让向量z1每一个都等距离变换,直到变换成向量z2。插值的代码非常简单,具体如下:def interpolate(x1, x2, num):
"""
x1:起始向量
x2:结束向量
num:最后需要得到的向量个数
"""
result = torch.zeros((num, *x1.shape))
step = (x2 - x1) / (num - 1)
for i in range(num):
result[i] = x1 + step * i
return result这里可以简单测试一下,代码如下:x1 = torch.Tensor([1, 0])
x2 = torch.Tensor([0, 1])
out = interpolate(x1, x2, 5)
print(out)输出结果如下:tensor([[1.0000, 0.0000],
[0.7500, 0.2500],
[0.5000, 0.5000],
[0.2500, 0.7500],
[0.0000, 1.0000]])可以看到,向量x1慢慢变成了向量x2。四、代码实现接下来就来实现人脸渐变的效果。这里需要用到之前训练的AutoEncoder模型,具体参考博客juejin.cn/post/721068…。 加载训练好的模型,配合插值算法实现人脸渐变。import torch
from torch.utils.data import DataLoader
from torchvision.utils import make_grid
import matplotlib.pyplot as plt
# 加载数据集
dataloader = DataLoader(
FaceDataset(data_dir='/home/zack/Files/datasets/img_align_celeba', image_size=64),
batch_size=2
)
model = FaceAutoEncoder()
model.load_state_dict(torch.load('../outputs/face_auto_encoder.pth'))
model.eval()
with torch.no_grad():
for idx, data in enumerate(dataloader):
# 对人脸编码
encoded1 = model.encoder(data[0].reshape((1, 3, 64, 64)))
encoded2 = model.encoder(data[1].reshape((1, 3, 64, 64)))
# 对人脸编码进行插值
encoded = interpolate(encoded1[0], encoded2[0], 64)
# 解码成人脸
outputs = model.decoder(encoded).reshape((64, 3, 64, 64))
outputs = make_grid(outputs, normalize=True)
plt.imshow(outputs.numpy().transpose((1, 2, 0)))
plt.show()其中FaceAutoEncoder和FaceDataset是上一篇博客实现的两个类。下面是几组生成效果:在测试中发现,使用训练数据中的图片可以得到比较好的效果,而使用额外图片效果则差一点。
小乔学算法
简单场景的图像分割算法
一、前言图像分割是计算机视觉中非常重要且基本的任务,在需要应用中都需要使用到图像分割算法。包括自动驾驶、修图、电影特效等。现在有许多成熟的图像分割算法,对于一些简单图像可以使用传统图像处理方法完成分割,而一些复杂场景则需要使用基于深度学习的方法。本文要介绍的是一种机器学习算法的分割方案,即使用KMeans算法完成图像分割。二、KMeans聚类2.1 聚类聚类是一种无监督学习算法,常见的有KMeans、DBSCAN、层次聚类等。聚类的思想就是根据样本的相似度,把某些相似的样本归为一个簇,最终样本会被分成n个簇。在某些算法中,也存在一些无法归为任何一个簇的样本点,这种点被称为离群点,或者异常点。在聚类算法中,距离(相似度)的度量是一个重要问题,通常我们使用几何距离作为度量依据,其计算如下:2.2 KMeans算法KMeans是一个比较简单的聚类算法,其步骤如下:确定簇的个数k,随机初始化k个簇中心计算所有样本与k个簇中心的距离,把样本分配给离它最近的簇中心根据当前已有的簇,选择簇的中心作为新的簇中心重复2-4步骤,直到3中簇中心不再更新KMeans算法可以用下图表示:2.3 KMeans的实现在sklearn模块中,提供了KMeans的实现,我们可以直接调用。具体代码如下:from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
plt.style.use("ggplot")
# 生成数据
X, y = make_blobs(300, 2, centers=3)
# 构建模型
kmeans = KMeans(n_clusters=3)
# 训练
kmeans.fit(X)
# 预测
preds = kmeans.predict(X)
# 绘制结果
plt.subplot(121)
plt.title("origin")
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.subplot(122)
plt.title("cluster")
plt.scatter(X[:, 0], X[:, 1], c=preds)
plt.show()绘制结果如下:其中左边是原本的类别情况,右边是聚类结果。在聚类完成后,可以获取各个簇中心,代码如下:kmeans.cluster_centers_在后面我们会用到簇中心。三、图像分割图像分割就是将图像按照一些要求分割成不同的部分。比如人像分割就是把背景和人像分割开,又或者语义分割是将不同含义的内容分割开。图像分割可以被认为是对图片的每个像素进行分类。这一点与前面的聚类有一些相似的地方。在聚类中,我们是把每个样本归为一个簇,如果把前景、背景各看作一个簇,那么聚类就可以看作是把前面和背景分割的操作。现在的问题就是如何把图片作为输入。我们可以把图片的每个像素看作一个样本,把图片从h×w×c(高、宽、通道数)转换成size×c,其中c为通道数,即颜色相关的维度。接下来只需要把图片输入聚类算法即可,分割的代码如下:import cv2
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 读取图片
origin = cv2.imread('img.png')
h, w, c = origin.shape
origin = cv2.cvtColor(origin, cv2.COLOR_BGR2RGB)
img = origin.copy().reshape(-1, c)
# 构建模型
kmeans = KMeans(n_clusters=2)
# 训练
kmeans.fit(img)
# 预测
preds = kmeans.predict(img)
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.title("origin")
plt.imshow(origin)
plt.subplot(122)
plt.title("cluster")
plt.imshow(preds.reshape((h, w)))
plt.show()上面代码我们把图片的像素作为样本,进行聚类,这样我们就可以根据像素的相似度进行图片的分割操作了。下面是分割结果:其中左边是原图,由花和叶组成,可以看作前景和背景。右边是分割结果,把两个部分分割开来了。因为我们使用的是KMeans算法,因此可以得到两个簇的簇中心,并且在这个例子中我们使用颜色作为聚类的样本,因此可以想到簇中心其实就是各个簇的平均颜色。于是我们使用簇中心作为区域的颜色对上面的结果重新绘制,代码如下:# 预测
preds = kmeans.predict(img)
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.title("origin")
plt.imshow(origin)
plt.subplot(122)
plt.title("cluster")
result = np.zeros_like(origin)
result[(preds == 0).reshape(h, w), :] = kmeans.cluster_centers_[0]
result[(preds != 0).reshape(h, w), :] = kmeans.cluster_centers_[1]
plt.imshow(result)
plt.show()这里是修改预测部分的代码,生成结果如下:图片被正确分割开来。四、总结图像分割算法有很多,聚类是比较简单的一种。在本文的例子中,我们使用颜色作为分割依据,这种方法有几个确定。比如需要提前确定分割的数量、复杂场景的分割效果很差等。另外这种方式还有一个非常问题,就是在分割时没有考虑位置信息。在前面的例子中,每个像素都是一个样本点,而实际在分割是周围像素对分割的决定也起了非常重要作用,而这是上述算法欠缺的。一直想法是混入位置信息,这里留给读者自己思考。另外一种方法则是使用卷积神经网络,这样就可以很好解决这一问题。后续我们会讨论更复杂的图像分割实现。
小乔学算法
如何欺骗神经网络
一、前言虽然人工智能技术已经能完成各种超出想象的事情,但是人工智能仍卸不掉人工智障的标签。即使强如ChatGPT也会犯一些非常弱智的错误。网上流传的关于人工智能的表情包,比如下面这个:又或者下面这张:这里问题现在还不能完全避免,这也不是我们今天的主题。我们今天的主题是如何找到这类可以让AI犯错的例子,或者说如何创造出这种例子。这种技术被称为Adversarial Attack(对抗攻击)。二、Adversarial Attack攻击神经网络的方式有很多,基于不同的先验知识可以分为黑盒攻击和白盒攻击。其中黑盒攻击假定我们不知道网络结构、网络权重,只知道网络的输入输出。而白盒攻击假定我们对模型了如指掌,我们知道网络的结构、网络权重、网络输入输出等。基于不同的目的,可以分为源/目标误分类、针对性误分类、误分类、置信度降低。其中误分类攻击目的最简单,就是让模型分类错误,这也是本文要实现的一种攻击。三、Fast Gradient Sign Attack实现攻击的方式也是多种多样的,本文使用一种名为Fast Gradient Sign Attack(FGSA)的攻击方式,这种方式利用梯度信息对输入进修改,来达到攻击的目的。3.1 模型的训练要理解FGSA我们需要知道模型的训练方式,我们可以把模型看做一个函数Fθ(X),其中θ是模型的参数,另外可以用损失函数L(X,Y;θ)来表示模型的好坏。在训练的过程中,可以让参数θ延梯度gθ的反方向更新:θ-=ηgθ。此时我们的目的是希望模型能得到一个比较低的损失值。3.2 攻击模型攻击模型利用的同样是梯度,在训练的时候我们希望模型得到好的结果,因此我们更新模型的参数,并且是沿着loss降低的方向更新。而攻击模型的目的是希望得到一个让模型出错的输入,因此我们更新输入信息,并且沿着loss上升的方向更新。四、代码实现接下来我们用代码来实现FGSA攻击,这里使用白盒攻击。所以需要先实现一个网络,这里以手写数字为例。4.1 手写数字识别白盒攻击的特点是我们知道网络的全部细节,因此我们自己实现一个网络,这个网络的所有细节我们都可以知道。代码如下:import torch
import torch.nn as nn
from torch import optim
from torchvision.transforms import ToTensor
from torchvision.datasets import MNIST
from torch.utils.data.dataloader import DataLoader
class DigitalRecognition(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
# 加载数据
train_dataset = MNIST("./", True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, 64)
# 构建模型
model = DigitalRecognition()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.002)
# 训练
for epoch in range(10):
for data, target in train_loader:
# forward
pred = model(data)
loss = criterion(pred, target)
optimizer.zero_grad()
# backward
loss.backward()
optimizer.step()
print(f"epoch: {epoch}, loss: {loss.item()}")
torch.save(model.state_dict(), 'digital.pth')这里为了方便,省略了测试相关代码,准确率的计算也省去了。代码运行完成后,可以得到一个digital.pth文件。训练部分的代码我们可以是任意的,只需要得到一个模型即可。4.2 FGSA得到模型后,我们就可以开始生成攻击数据了。我们编写一个函数,这个函数输入模型的输入X,以及loss对X的梯度,得到一个带有攻击性的输入,代码如下:def fgsm_attack(image, epsilon, data_grad):
"""
image: 需要更新的输入
epsilon:更新程度,epsilon越大图像变化越大,攻击越有效
data_grad:输入对应的梯度,即loss对image的导数
"""
# 获取梯度的正负信息
sign_data_grad = data_grad.sign()
# 让输入添加梯度信息,即让输入添加能让loss增大的信息
perturbed_image = image + epsilon * sign_data_grad
# 把结果映射到0-1之间
perturbed_image = torch.clamp(perturbed_image, 0, 1)
return perturbed_image其中image是我们已有的数据,epsilon是超参数,我们只需要得到image对应的梯度就可以实现攻击了。求image梯度的代码如下:import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import utils
from torchvision.transforms import ToTensor
from torchvision.datasets import MNIST
from torch.utils.data.dataloader import DataLoader
import matplotlib.pyplot as plt
def fgsm_attack(image, epsilon, data_grad):
pass
# 加载数据
train_dataset = MNIST("./", True, ToTensor(), download=True)
train_loader = DataLoader(train_dataset, 64)
model = DigitalRecognition()
model.load_state_dict(torch.load('digital.pth'))
model.eval()
for data, target in train_loader:
# 设置输入自动求导
data.requires_grad = True
output = model(data)
loss = F.nll_loss(output, target)
model.zero_grad()
loss.backward()
# loss对data的梯度
data_grad = data.grad.data
# 对data进行修改
perturbed_data = fgsm_attack(data, .15, data_grad)
# 对攻击数据预测
output = model(perturbed_data)
grid = utils.make_grid(perturbed_data, normalize=True)
with torch.no_grad():
grid = grid.cpu().numpy().transpose((1, 2, 0))
print(output.argmax(dim=1).cpu().numpy().reshape((8, 8)))
plt.imshow(grid)
plt.show()
break在调用loss.backward()后,pytorch帮我们自动求了loss对data的导数,然后调用fgsm_attack,把输入和梯度传进去对数据添加攻击信息。下面是带有攻击性的输入图像:下面的矩阵是各个图像对应的预测结果:[[3 0 3 3 4 8 8 9]
[3 2 9 2 8 8 3 3]
[8 3 3 8 3 0 4 3]
[2 3 8 3 8 2 8 8]
[3 2 8 2 8 0 6 4]
[9 3 5 1 8 5 0 8]
[1 9 8 0 5 7 8 7]
[0 4 8 2 0 8 8 9]]
添加攻击信息后的图像对人来说很容易辨别,但是网络无法正确预测。从结果来看我们达到了攻击的目的。
小乔学算法
LoRA原理与实现--PyTorch自己搭建LoRA模型
一、前言在AIGC领域频繁出现着一个特殊名词“LoRA”,听上去有点像人名,但是这是一种模型训练的方法。LoRA全称Low-Rank Adaptation of Large Language Models,中文叫做大语言模型的低阶适应。如今在stable diffusion中用地非常频繁。由于大语言模型的参数量巨大,许多大公司都需要训练数月,由此提出了各种资源消耗较小的训练方法,LoRA就是其中一种。本文将详细介绍LoRA的原理,并使用PyTorch实现小模型的LoRA训练。二、模型训练现在大多数模型训练都是采用梯度下降算法。梯度下降算法可以分为下面4个步骤:正向传播计算损失值反向传播计算梯度利用梯度更新参数重复1、2、3的步骤,直到获取较小的损失以线性模型为例,模型参数为W,输入输出为x、y,损失函数以均方误差为例。那么各个步骤的计算如下,首先是正向传播,对于线性模型来说就是做一个矩阵乘法:在求出损失后,可以计算L对W的梯度,得到dW:dW是一个矩阵,它会指向L上升最快的方向,但是我们的目的是让L下降,因此让W减去dW。为了调整更新的步伐,还会乘上一个学习率η,计算如下:最后一直重复即刻。上述三个步骤的伪代码如下:# 4、重复1、2、3
for i in range(10000):
# 1、正向传播计算损失
L = MSE(Wx, y)
# 2、反向传播计算梯度
dW = gradient(L, W)
# 3、利用梯度更新参数
W -= lr * dW在更新完成后,得到新的参数W'。此时我们使用模型预测时,计算如下:三、引入LoRA我们可以来思考一下W和W'之间的关系。W通常指基础模型的参数,而W'是在基础模型的基础上,经过几次矩阵加减得到的。假设在训练的过程中更新了10次,每次的dW分别为dW1、dW2、....、dW10,那么完整的更新过程可以写为一次运算:其中dW是一个形状与W'一致的矩阵。我们把-ηdW写成矩阵R,那么更新后的参数就是:此时训练的过程就被简化为原矩阵加上另一个矩阵R。但是求解矩阵R并没有更简单,而且也没有节约资源,此时就引出LoRA了这一思想。一个训练充分的矩阵,通常是满秩或者基本满足秩的,即矩阵中没有一列是多余的。在论文《Scaling Laws for Neural Language Model》中提出了数据集与参数大小之间的关系,满足该关系且训练良好,得到的模型是基本满秩的。在微调模型时,我们会选取一个底模,该底模就是基本满秩的。而更新矩阵R秩的情况是如何的呢?我们假定R矩阵是一个低秩矩阵,低秩矩阵有许多重复的列,因此可以分解为两个更小的矩阵。假如W的形状为m×n,那么A的形状也是m×n,我们把矩阵R分解为AB(其中A形状为m×r,B形状为r×N),r通常会选取一个远小于m、n的值,如图所示:将低秩矩阵分解为两个矩阵几点好处,首先是参数量明显减少。假设R矩阵的形状为100×100,那么R的参数量为10000。当我们选取秩为10时,此时矩阵A的形状为100×10,矩阵B的形状为10×100,此时参数量为2000,比R矩阵少了80%。而且由于R是低秩矩阵,所以在训练充分的情况下,A和B矩阵可以达到R的效果。这里的矩阵AB就是我们常说的LoRA模型。在引入LoRA后,我们的预测需要将x分别输入W和AB,此时预测的计算为:在预测时会比原始模型稍慢,但是在大模型中基本感觉不到差异。四、实战为了把握各个细节,这里不使用大模型作为lora的实战,而是选择使用vgg19这种小型网络来训练lora模型。导入需要用到的模块:import os
import torch
from torch import optim, nn
from PIL import Image
from torch.utils import data
from torchvision import models
from torchvision.transforms import transforms4.1 数据集准备这里使用vgg19在imagenet上的预训练权重作为底模,因此需要准备分类数据集。为了方便,这里只准备了一个类别,且只准备了5张图片,图片在项目下的data/goldfish下:在imagenet中包含了goldfish类别,但是这里选取的是插画版的goldfish,经过测试,预训练模型不能将上述图片正确分类。我们的目的就是训练LoRA,让模型正确分类。我们创建一个LoraDataset:transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
class LoraDataset(data.Dataset):
def __init__(self, data_path="datas"):
categories = models.VGG19_Weights.IMAGENET1K_V1.value.meta["categories"]
self.files = []
self.labels = []
for dir in os.listdir(data_path):
dirname = os.path.join(data_path, dir)
for file in os.listdir(dirname):
self.files.append(os.path.join(dirname, file))
self.labels.append(categories.index(dir))
def __getitem__(self, item):
image = Image.open(self.files[item]).convert("RGB")
label = torch.zeros(1000, dtype=torch.float64)
label[self.labels[item]] = 1.
return transform(image), label
def __len__(self):
return len(self.files)4.2 创建LoRA模型我们把LoRA封装成一个层,LoRA中只有两个需要训练的矩阵,LoRA的代码如下:class Lora(nn.Module):
def __init__(self, m, n, rank=10):
super().__init__()
self.m = m
self.A = nn.Parameter(torch.randn(m, rank))
self.B = nn.Parameter(torch.zeros(rank, n))
def forward(self, inputs):
inputs = inputs.view(-1, self.m)
return torch.mm(torch.mm(inputs, self.A), self.B)其中m是输入的大小,n是输出的大小,rank是秩的大小,我们可以设置一个较小的值。在权重初始化时,我们把A用高斯噪声初始化,而B用0矩阵初始化,这样的目的是保证从底模开始训练。因为AB是0矩阵,所以初始状态下,LoRA不起作用。4.3 设置超参数并训练接下来就是训练了,这里和PyTorch常规训练代码基本一致,先看代码:# 加载底模和lora
vgg19 = models.vgg19(models.VGG19_Weights.IMAGENET1K_V1)
for params in vgg19.parameters():
params.requires_grad = False
vgg19.eval()
lora = Lora(224 * 224 * 3, 1000)
# 加载数据
lora_loader = data.DataLoader(LoraDataset(), batch_size=batch_size, shuffle=True)
# 加载优化器
optimizer = optim.Adam(lora.parameters(), lr=lr)
# 定义损失
loss_fn = nn.CrossEntropyLoss()
# 训练
for epoch in range(epochs):
for image, label in lora_loader:
# 正向传播
pred = vgg19(image) + lora(image)
loss = loss_fn(pred, label)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
optimizer.zero_grad()
print(f"loss: {loss.item()}")这里有两点需要注意,第一点是我们把vgg19的权重设置为不可训练,这和迁移学习很像,但其实是不一样的。第二点则是正向传播时,我们使用了下面代码:pred = vgg19(image) + lora(image)4.4 测试下面来简单测试一下:# 测试
for image, _ in lora_loader:
pred = vgg19(image) + lora(image)
idx = torch.argmax(pred, dim=1).item()
category = models.VGG19_Weights.IMAGENET1K_V1.value.meta["categories"][idx]
print(category)
torch.save(lora.state_dict(), 'lora.pth')输出结果如下:goldfish
goldfish
goldfish
goldfish
goldfish基本预测正确了,不过这个测试结果并不能说明什么。最后我们保存了一个5M的LoRA模型,相比vgg19的几十M算是非常小了。五、总结LoRA是针对大模型的一种高效的训练方法,而本文则将LoRA使用在小型的分类网络中,旨在让读者更清晰认识LoRA的详细实现(同时也因为跑不动大模型)。限于数据量,对LoRA的精度效率等问题没有详细讨论,读者可以参考相关资料深入了解。
小乔学算法
基于Transformer的图像分类网络Vit
一、前言长久以来,CNN一直是CV领域最受欢迎的网络,在NLP也有CNN的一席之地。限于CNN上下文的能力,RNN系列网络在长文本任务中要比CNN更受欢迎,但是RNN系列网络也一直存在性能问题。2017年的一篇论文《Attention is all you need》提出了Transformer架构,Transformer的出现打破了RNN的绝对优势,Transformer在NLP领域取得了不菲的成绩。Transformer出来不久,就有许多关于Transformer与CNN相关的讨论。本文我们要讨论的就是Transformer在CV领域的应用,我们要实现论文《An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》中提出的Vit(Vision Transformer)的网络。二、Transformer在开始介绍Vit之前,我们来简单看看Transformer网络。其结构如下图:在Vit中我们只需要用到Encoder部分,此时和之前情感分析的例子,详见:基于Transformer的文本情感分析。此时Transformer可以分为三个部分:Input、Attention、分类网络,我们一一介绍。2.1 Input在Input的部分,原始的Transformer需要输入词的id,然后经过Embedding层,并加上Positional Encoding。此过程可以描述成下面的伪代码:embedded = embedding(idxes)
embedded = embedded + positioal_encoding而图像的处理则和句子不一样,在Vit中,图像会被分为多个patch,每个patch会被看作是原本的一个token,如下图所示:而Positional Encoding也被替换成了Positional Embedding。原本的Positional Encoding是由三角函数生成的固定值,而Positional Embedding则是和普通Embedding类似的一种可供学习的位置嵌入,只不过在Positional Embedding中输入的id变为的固定值(第一个patch的位置id为0,第二个为1,...)。另外,在原Transformer中是Embedding结果与Positional Encoding相加,而在Vit中是patch经过线性层后与Positional Embedding相加。为了方便后续使用,我们可以封装一个PatchEncoder,用于处理输入(分patch并加上位置嵌入)。代码如下:class Patches(layers.Layer):
def __init__(self, patch_size):
super().__init__()
self.patch_size = patch_size
def call(self, images):
batch_size = tf.shape(images)[0]
patches = tf.image.extract_patches(
images=images,
sizes=[1, self.patch_size, self.patch_size, 1],
strides=[1, self.patch_size, self.patch_size, 1],
rates=[1, 1, 1, 1],
padding="VALID",
)
patch_dims = patches.shape[-1]
patches = tf.reshape(patches, [batch_size, -1, patch_dims])
return patches
class PatchEncoder(layers.Layer):
def __init__(self, num_patches, projection_dim):
super().__init__()
self.num_patches = num_patches
self.projection = layers.Dense(units=projection_dim)
self.position_embedding = layers.Embedding(
input_dim=num_patches, output_dim=projection_dim
)
def call(self, patch):
positions = tf.range(start=0, limit=self.num_patches, delta=1)
encoded = self.projection(patch) + self.position_embedding(positions)
return encoded在后续构建网络时需要使用到PatchEncoder。2.2 AttentionVit中的self-attention与Transformer中的self-attention是一样的,这里不详细赘述,其代码实现如下:def mlp(x, hidden_units, dropout_rate):
for units in hidden_units:
x = layers.Dense(units, activation=tf.nn.gelu)(x)
x = layers.Dropout(dropout_rate)(x)
return x
# 输入图片信息
x1 = layers.LayerNormalization(epsilon=1e-6)(encoded_patches)
# 计算self-attention
attention_output = layers.MultiHeadAttention(
num_heads=num_heads, key_dim=projection_dim, dropout=0.1
)(x1, x1)
# 残差连接
x2 = layers.Add()([attention_output, encoded_patches])
# layernorm
x3 = layers.LayerNormalization(epsilon=1e-6)(x2)
# 线性层
x3 = mlp(x3, hidden_units=transformer_units, dropout_rate=0.1)
# 残差连接
encoded_patches = layers.Add()([x3, x2])在实际训练时,这部分会重复多次。在self-attention中,会提取图片的关键信息,将注意力集中在对分类起决定作用的图像区域。2.3 分类网络分类网络用来做最后的分类工作,由全连接和残差连接构成,其结构与attention部分非常类似,具体代码如下:# 展开
representation = layers.LayerNormalization(epsilon=1e-6)(encoded_patches)
representation = layers.Flatten()(representation)
representation = layers.Dropout(0.5)(representation)
# 全连接
features = mlp(representation, hidden_units=mlp_head_units, dropout_rate=0.5)
# 分类
logits = layers.Dense(num_classes)(features)logits的结果就是类别分数,后续会与真实标签计算crossentropy。本文假定读者已经对Transformer有些许了解,因此省去部分细节。下面就使用Vit网络来完成一个实际的任务。三、使用Vit网络进行图像分类本文使用cifar100数据集训练Vit网络,首先导入需要用的模块:import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import tensorflow_addons as tfa3.1 准备数据这里使用keras直接加载cifar100的数据:num_classes = 100
input_shape = (32, 32, 3)
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar100.load_data()
print(f"x_train shape: {x_train.shape} - y_train shape: {y_train.shape}")
print(f"x_test shape: {x_test.shape} - y_test shape: {y_test.shape}")输出结果如下:x_train shape: (50000, 32, 32, 3) - y_train shape: (50000, 1)
x_test shape: (10000, 32, 32, 3) - y_test shape: (10000, 1)共计60000张图片。3.2 配置超参数下面配置训练需要用到的一些参数:learning_rate = 0.001
# 权重衰减系数
weight_decay = 0.0001
batch_size = 256
num_epochs = 100
image_size = 72
# 单个patch的尺寸
patch_size = 6
num_patches = (image_size // patch_size) ** 2
projection_dim = 64
num_heads = 4
# attention的神经元数量
transformer_units = [
projection_dim * 2,
projection_dim,
]
transformer_layers = 8
# 分类网络的神经元数量
mlp_head_units = [2048, 1024]3.3 数据增强为了提高泛化能力,可以添加数据增强的操作,代码如下:# 数据增强层
data_augmentation = keras.Sequential(
[
layers.Normalization(),
layers.Resizing(image_size, image_size),
layers.RandomFlip("horizontal"),
layers.RandomRotation(factor=0.02),
layers.RandomZoom(
height_factor=0.2, width_factor=0.2
),
],
name="data_augmentation",
)
# 对训练数据进行数据正确
data_augmentation.layers[0].adapt(x_train)3.4 构建Vit模型下面我们使用前面三部分的代码创建Vit模型,代码如下:def create_vit_classifier():
inputs = layers.Input(shape=input_shape)
augmented = data_augmentation(inputs)
patches = Patches(patch_size)(augmented)
encoded_patches = PatchEncoder(num_patches, projection_dim)(patches)
# 重复多次attention
for _ in range(transformer_layers):
x1 = layers.LayerNormalization(epsilon=1e-6)(encoded_patches)
attention_output = layers.MultiHeadAttention(
num_heads=num_heads, key_dim=projection_dim, dropout=0.1
)(x1, x1)
x2 = layers.Add()([attention_output, encoded_patches])
x3 = layers.LayerNormalization(epsilon=1e-6)(x2)
x3 = mlp(x3, hidden_units=transformer_units, dropout_rate=0.1)
encoded_patches = layers.Add()([x3, x2])
# 全连接
representation = layers.LayerNormalization(epsilon=1e-6)(encoded_patches)
representation = layers.Flatten()(representation)
representation = layers.Dropout(0.5)(representation)
features = mlp(representation, hidden_units=mlp_head_units, dropout_rate=0.5)
logits = layers.Dense(num_classes)(features)
model = keras.Model(inputs=inputs, outputs=logits)
return model3.5 训练Vit训练的代码非常简单,代码如下:vit = create_vit_classifier()
vit.compile(
'adam',
# 因为模型输出的结果没有经过softmax,因此需要设置参数from_logits=True
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['acc']
)
vit.fit(
x_train, y_train,
batch_size=batch_size,
epochs=num_epochs,
validation_data=[x_test, y_test]
)训练完成后准确率在70%左右,这比CNN表现稍差。四、总结在使用Vit实验后,发现结果并没有比CNN好。那么是不是Transformer就不适合应用在CV领域呢?答案是否定的。相比传统的CNN,vit的参数量更大,训练时间也更长。在数据量比较小时,Transformer会欠拟合,此时CNN依旧是最佳选择。而数据量较大时,CNN将到达性能瓶颈,此时可以考虑使用Vit网络,或许可以得到更好的结果。
小乔学算法
生成梵高风格的图片
一、前言在以前,AI被认为是与艺术无关,AI也无法创作艺术作品。而GAN家族、风格迁移、Diffusion模型的出现,让AI也能创作有艺术风格的图像。其中最简单的就是风格迁移。风格迁移是一种把图像A的风格迁移到图像B的一种算法,利用该算法可以将“梵高的风格”迁移到其它任意图片上。今天我们要分享的就是风格迁移的实现。二、风格迁移2.1 风格迁移介绍风格迁移是一种将目标图像的风格转移到特定图像的算法。在该算法中,有两个输入图像,一个图像负责贡献内容,一个图像负责贡献风格。在风格迁移中,一个重要的问题是如何定义风格。风格比较抽象,在某些例子中可以很容易理解。比如梵高的《星空》、莫奈的《印象 日出》,虽然无法用文字描述,但是我们可以感觉到其中的风格。在后面我们会用特征图解释风格的具体含义。在风格迁移里面,我们要做的就是把一幅不属于梵高的作品,变得像梵高的作品;一幅不像莫奈的作品,变得像莫奈的作品。比如下面的例子:2.2 实现原理2.2.1 特征图假如输入风格图像A和内容图像B,那么我们希望输出图像C的风格与A相似,内容与B相似。这里的相似需要有一个衡量的依据,一个简单的想法就是像素值。不过这个明显是不可行的,不管是内容还是风格,用像素值作为衡量依据都会导致泛化能力非常差,因此需要改用其它方法。在深度学习中,我们喜欢用卷积神经网络提取的特征图来作为衡量依据。特征图有许多良好的性质,比如对位置不敏感、多尺度特征。VGG是比较常用的一种卷积神经网络,在本例中我们使用卷积神经网络来提取图片特征。下图是VGG19的网络结构:在VGG19中,有5组卷积神经网络。左边的卷积层提取纹理、边缘等低级特征,越往右提取的特征越抽象。2.2.2 内容损失风格迁移任务不同于其它网络,在以往任务中,我们做的是准备数据,更新网络权重。而在本例中,我们要做的是利用卷积提取图片特征,然后通过内容相似度、风格相似度的信息来修改输入的图片。所以我们需要有一个函数用来评估内容相似度,这里我们称为内容损失。内容是图片整体的信息,比较抽象,需要在较右的卷积核才能提取,这里选择conv4_1的输出作为图片内容。这一层右N(512)个特征图,因此把内容损失定义为:其中f为向量化后的特征图,D为特征图的大小,这里就是计算两个图片的特征图的均方误差。2.2.3 风格损失风格损失则更复杂一点。风格不只反映了图片的纹理,也反映了图像的整体风格。因此在考虑风格损失的时候,需要考虑各层特征图的内容。通常用同一层不同特征图的Gram矩阵来表示。Gram矩阵定义为:N个特征图,将特征图向量化后记为f,则第i和j个向量的内积就是Gram矩阵的第(i, j)个元素:此时某一层的风格损失可以定义为:因为风格损失需要考虑多层,因此整体的风格损失可以定义为:其值为各层特征图的加权和。最后我们可以用Ls和Lc的加权和作为最终的损失:三、代码实现下面使用PyTorch实现迁移学习。3.1 加载图片首先模型需要三个输入,分别是内容图片、风格图片、结果图片。结果图片是用来更新的图片,我们可以用随机噪声来生成,也可以用内容图片来代替,加载图片的代码如下:import cv2
import torch
import torchvision.models as models
import torch.nn.functional as F
import torch.nn as nn
from PIL import Image
from torchvision.transforms import Compose, ToTensor, Resize
transform = Compose([
Resize((512, 512)),
ToTensor(),
])
def load_images(content_path, style_path):
content_img = Image.open(content_path)
image_size = content_img.size
content_img = transform(content_img).unsqueeze(0).cuda()
style_img = Image.open(style_path)
style_img = transform(style_img).unsqueeze(0).cuda()
var_img = content_img.clone()
var_img.requires_grad = True
return content_img, style_img, var_img, image_size
content_img, style_img, var_img, image_size = load_images('content.jpeg', 'style.png')这里就是读取了两个图片,并复制了内容图片,另外我们将结果图片var_img设置为计算梯度,因为后续我们需要根据梯度更新该图片。3.2 提取特征提取特征我们使用的是预训练的vgg19网络,而且不需要更新网络权重,另外我们提取的特征包括用于计算内容损失和风格损失的,为了方便,这里构造一个类用于提取特征:# 加载预训练模型,使其不计算梯度
model = models.vgg19(pretrained=True).cuda()
batch_size = 1
for params in model.parameters():
params.requires_grad = False
model.eval()
# 用于归一化和反归一化
mu = torch.Tensor([0.485, 0.456, 0.406]).unsqueeze(-1).unsqueeze(-1).cuda()
std = torch.Tensor([0.229, 0.224, 0.225]).unsqueeze(-1).unsqueeze(-1).cuda()
unnormalize = lambda x: x * std + mu
normalize = lambda x: (x - mu) / std
# 构造层用于提取特征
class FeatureExtractor(nn.Module):
def __init__(self, model):
super().__init__()
self.module = model.features.cuda().eval()
self.con_layers = [22]
self.sty_layers = [1, 6, 11, 20, 29]
for name, layer in self.module.named_children():
if isinstance(layer, nn.MaxPool2d):
self.module[int(name)] = nn.AvgPool2d(kernel_size=2, stride=2)
def forward(self, tensor: torch.Tensor) -> dict:
sty_feat_maps = []
con_feat_maps = []
x = normalize(tensor)
for name, layer in self.module.named_children():
x = layer(x)
if int(name) in self.con_layers: con_feat_maps.append(x)
if int(name) in self.sty_layers: sty_feat_maps.append(x)
return {"content_features": con_feat_maps, "style_features": sty_feat_maps}
model = FeatureExtractor(model)
style_target = model(style_img)["style_features"]
content_target = model(content_img)["content_features"]这里提取的两个target就是我们需要特征图。3.3 Gram矩阵对于风格损失,我们还需要计算grim矩阵。即计算风格特征图两两之间的点积,代码如下:gram_target = []
for i in range(len(style_target)):
b, c, h, w = style_target [i].size()
tensor_ = style_target[i].view(b * c, h * w)
gram_i = torch.mm(tensor_, tensor_.t()).div(b * c * h * w)
gram_target.append(gram_i)gram_target里面就存储了Gram矩阵的内容。3.4 更新图片最后我们要做的就是定义训练的过程,以往我们训练是更新网络权重,今天我们训练是更新结果图片,也就是var_img,代码与以往更新基本一致:optimizer = torch.optim.Adam([var_img], lr=0.01, betas=(0.9, 0.999), eps=1e-8)
lam1 = 1e-3
lam2 = 1e7
lam3 = 5e-3
for itera in range(20001):
optimizer.zero_grad()
output = model(var_img)
sty_output = output["style_features"]
con_output = output["content_features"]
con_loss = torch.tensor([0]).cuda().float()
# 计算内容图片和结果图像的内容损失
for i in range(len(con_output)):
con_loss = con_loss + F.mse_loss(con_output[i], con_target[i])
# 计算风格图片和结果图像的风格损失
sty_loss = torch.tensor([0]).cuda().float()
for i in range(len(sty_output)):
b, c, h, w = sty_output[i].size()
tensor_ = sty_output[i].view(b * c, h * w)
gram_i = torch.mm(tensor_, tensor_.t()).div(b * c * h * w)
sty_loss = sty_loss + F.mse_loss(gram_i, gram_target[i])
b, c, h, w = style_img.size()
TV_loss = (torch.sum(torch.abs(style_img[:, :, :, :-1] - style_img[:, :, :, 1:])) +
torch.sum(torch.abs(style_img[:, :, :-1, :] - style_img[:, :, 1:, :]))) / (b * c * h * w)
loss = con_loss * lam1 + sty_loss * lam2 + TV_loss * lam3
loss.backward()
var_img.data.clamp_(0, 1)
optimizer.step()
if itera % 100 == 0:
print('itera: %d, con_loss: %.4f, sty_loss: %.4f, TV_loss: %.4f' % (itera,
con_loss.item() * lam1,
sty_loss.item() * lam2,
TV_loss.item() * lam3), '\n\t total loss:',
loss.item())
print('var_img mean:%.4f, std:%.4f' % (var_img.mean().item(), var_img.std().item()))
if itera % 1000 == 0:
save_img = var_img.clone()
save_img = torch.clamp(save_img, 0, 1)
save_img = save_img[0].permute(1, 2, 0).data.cpu().numpy() * 255
save_img = save_img[..., ::-1].astype('uint8')
save_img = cv2.resize(save_img, image_size)
cv2.imwrite('outputs/transfer%d.jpg' % itera, save_img)最后我们运行即可。下面是一些效果图片:从左到右依次是风格图像,内容图像,迭代1000次后的结果图像,迭代3000次后的结果图像。从结果来看,我们确实完成了风格迁移。
小乔学算法
如何解释图像识别的过程?深度剖析卷积神经网络
一、前言图像识别方法有很多,包括传统的逻辑回归、Adaboost、卷积神经网络、Transformer等。现代图像识别算法的准确率已经超过人类的水平了,但是关于这些算法如何完成图像识别却仍是一个问题。关于如何解释一个算法的研究被称为可解释的机器学习,本文将选取逻辑回归和卷积神经网络两个算法,来研究图像识别的原理。二、逻辑回归首先来聊聊逻辑回归算法,逻辑回归是一个非常简单的线性模型,在某些简单任务中非常高效。而且可解释性也非常强,因此先讨论逻辑回归图像识别的原理。2.1 逻辑回归原理逻辑回归是线性回归加上sigmoid函数,其表达式如下:y=11+e−(WXT+b)y=\frac{1}{1+e^{-(WX^T+b)}}y=1+e−(WXT+b)1其中X、W是向量。其中y的取值会在0-1之间,当y大于等于0.5时,X会被判断为类别1,否则判断为类别0。逻辑回归的训练就是要找到最优的W和b。对于图片分类来说,X就是图片,并且要求是一维向量(不考虑mini batch)。但是图片本身是二维或者三维的,因此逻辑回归在进行图片分类时,会把图片转换成一维向量。这么做有个明显问题,就是没有考虑图片的空间信息。这点会在卷积神经网络中改进。2.2 逻辑回归的实现在sklearn中提供了逻辑回归的实现,使用下面代码可以完成逻辑回归。from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_digits
X, y = load_digits(n_class=2, return_X_y=True)
lr = LogisticRegression()
lr.fit(X, y)这里使用8×8的数字0和数字1的图片作为训练集,训练逻辑回归模型。在训练完成后,可以通过下面的属性获取W和b:print(lr.coef_.shape)
print(lr.intercept_.shape)
# 输出
# (1, 64)
# (1,)W的数量和图片像素数量是一样的。2.3 逻辑回归图片分类原理在逻辑回归中,起至关重要的是权重W。它与像素一一对应,表示了某个像素在分类时起何种作用。数字1对应类别1,数字0对应类别0。如果想正确分类,那么0对应区域应该会有负权重,这样才能保证遇到0时输出结果会偏小。而1对应区域应该会有较大的权重,这样才能保证遇到1时输出较大的结果。这里以一个比数字识别更简单的问题来举例,两类数据如下:两类数据有个明显区别,及类别0的白色区域(物体)集中在右上角,而类别1的白色区域集中在左下角。如果下给你正确分类,那么权重的大致分布如下:即右上角权重偏大,左下角权重偏小。权重本身是一维的,但是我们可以把它reshape成和图片一样的形状,上面的图片就是这么得到的。现在我们对lr.coef_来reshape并展示,代码如下:img = lr.coef_.reshape((8, 8))
plt.imshow(img)
plt.show()展示结果如下:从结果来看外面有一圈暗色,即低权重,这部分是数字0的分布区域。而中间有一束亮色,即高权重,这部分是数字1的分布区域。当1和0分布区域重合时,会得到接近0的权重。三、卷积神经网络3.1 卷积神经网络相比逻辑回归,卷积神经网络要复杂得多。解释起来也要更麻烦,但是还是有办法解释的。首先来看看卷积神经网络的原理。卷积神经网络的原理就是提取特征,然后分类。后面的分类与逻辑回归是非常像的,这里着重解释特征提取。一次卷积操作可以分为下面几个步骤:Padding卷积Pooling下面详细解释每一步。3.1.1 Padding因为卷积会导致图片缩小,因此需要在图片外圈填充一圈0,这一操作叫做padding。其操作为:原图形状为5×5,填充后形状为7×7。3.1.2 卷积第二步则是卷积操作,也是卷积神经网络的关键。卷积操作需要有一个卷积核,卷积核是一个数字矩阵。在数字图像处理中,卷积核是人为设计的,而在卷积神经网络中,卷积核是学习而来的。卷积的具体操作为将卷积核与图片左上角区域做点积,得到第一个结果。操作如下图:而后将区域往右移动一(由stride参数决定)个像素,同样计算点积,得到第二个结果:后续则是移动区域,重复上述操作,最后得到卷积结果,即特征图。结果如下:特征图时一个十字,观察原图(Padding前的图)可以发现以(3,2)和(4,3)为中心的区域是十字形,而特征图中这两处正好是5(最大值)。其实这就是卷积的作用,卷积结果越大,说明在某个区域与卷积核越像。在图片中,包含大量特征,因此实际情况中会使用大量的卷积核。3.1.3 PoolingPooling有许多种,这里以MaxPooling为例。其操作就是选取指定区域,然后把区域最大值作为结果。Pooling并非必要操作,而是一种节约资源的举措。下面是具体操作:对比Pooling前后的特征图,Pooling前可以找到某一特征的准确位置,而Pooling后可以找到某一特征的大致位置。3.2 感受野卷积核的大小通常为3×3、5×5、7×7这种小尺寸,能够提取的特征也是非常小的。那么卷积神经网络是如何知道图片中是否存在一个复杂物体的呢?这就要通过卷积层的堆叠了。我们在原图上进行卷积时,提取的是微小的特征,也就是纹理、边缘等特征。这些特征无法获取图像的宏观信息,因此需要站在更广的范围观察图片。即对Pooling后的图片进行卷积。以3×3的卷积、2×2的Pooling为例,第一次卷积只能捕抓3×3范围的特征,而Pooling后的特征图一个像素代表原图2×2的区域,因此第二次卷积能捕抓6×6范围的特征。也就是说第二次卷积有更大的感受野。3.3 卷积识别数字下面用一个简单的例子来解释卷积神经网络如何识别数字。这里以数字1和2为例,下面是两个图片:首先我们设计第一层卷积的卷积核,下面是根据图片设计的几个卷积核:使用上面5个卷积核对原图进行卷积,各自可以得到5个特征图。下面是数字1卷积后的结果:然后我们观察数字1图片中各个特征的位置,比如特征1(第一个卷积核对应的特征)出现在靠近左上角位置。特征二则出现在靠近中间位置,依次往后推到,由此我们可以设计第二层卷积的卷积核。第二层卷积核的通道数需要与特征图数量一致,为了简单,第二层我们先设计一个卷积核。具体如下:这里是一个由5通道的卷积核。每个通道表示图片中某个位置有何种特征,多种特征组合起来就成了一个数字了。对于数字2来说,情况也是类似的。首先使用第一层卷积对图片进行卷积操作,得到与图片1不同的特征图。结果如下:由此设计出第二层的第二个卷积核:通过同样的方式就可以知道图片中是否有数字二了。那么网络如何区分数字1和数字二呢?这个非常简单,第二层我们设计了两个卷积核,如果不做padding,那么第二层会输出两个向量。比如输入图片1,会得到下面的结果:[[2, 6, 3, 4, 4],
[4, 0, 3, 3, 4]]而输入图片2,则会得到:[[2, 4, 3, 3, 4],
[4, 0, 3, 4, 4]]其中第一个向量是为数字1设计的,粗略来看,第一个向量的元素和越大则图片越像数字1,第二个向量的元素和越大则图片越像数字2。不过这并非绝对的。在实作时,我们会在后面拼接全连接。而逻辑回归可以看作是一个简单的全连接,因此网络后面部分可以用逻辑回归的方式进行解读。通过上面的例子,我们详细剖析了多层卷积是如何工作的。在上面的基础上我们可以继续扩展,当卷积层数更高时,我们可以识别猫狗这种更为复杂的特征。
小乔学算法
如何实现以图搜图
一、前言在许多搜索引擎中,都内置了以图搜图的功能。以图搜图功能,可以极大简化搜索工作。今天要做的就是实现一个以图搜图引擎。我们先来讨论一下以图搜图的难点,首当其冲的就是如何对比图片的相似度?怎么样的图片才叫相似?人可以一眼判断,但是计算机却不一样。图片以数字矩阵的形式存在,而相似度的比较也是比较矩阵的相似度。但是这有一些问题。第二个问题就是大小问题,图片的大小通常是不一样的,而不同大小的矩阵也无法比较相似度。不过这个很好解决,直接修改图片尺寸即可。第三个问题则是像素包含的信息非常有限,无法表达抽象信息。比如画风、物体、色调等。根据上面描述,我们现在要解决两个问题:用什么信息替换像素信息、怎么计算相似度。下面一一解决。在开始前,我们先实现一个简易的以图搜图功能。二、简易以图搜图实现2.1 如何计算相似度首先来讨论一下直接使用像素作为图像的表示,此时我们应该如何完成以图搜图的操作。一个非常简单的想法就是直接计算两个图片的几何距离,假如我们目标图片为target,图库中的图片为source,几何距离的计算如下:distance = sum[(target−source)2]distance = \sqrt{sum[(target - source)^2]}distance = sum[(target−source)2]然后把距离最小的n个图片作为搜索结果。这个方法看起来不可靠,但是实际使用时也会有不错的结果。如果图库图片本身不是非常复杂,比如动漫头像,那么这种方式非常简单有效,而其它情况下结果会比较不稳定。2.2 基于几何距离的图片搜索基于几何距离的图片搜索实现步骤如下:把图片修改到同一尺寸,如果尺寸不同则无法计算几何距离选定一个图片作为目标图片,即待搜索图片遍历图库,计算几何距离,并记录到列表对列表排序,获取几何距离最小的n张图片这里使用蜡笔小新的图片作为图库进行搜索,下面是图片的一些示例:部分图片有类似的风格,我们希望能根据一张图片找到类似风格的图片。实现代码如下:import os
import cv2
import random
import numpy as np
base_path = r"G:\datasets\lbxx"
# 获取所有图片路径
files = [os.path.join(base_path, file) for file in os.listdir(base_path)]
# 选取一张图片作为目标图片
target_path = random.choice(files)
target = cv2.imread(target_path)
h, w, _ = target.shape
distances = []
# 遍历图库
for file in files:
# 读取图片,转换成与目标图片同一尺寸
source = cv2.imread(file)
if not isinstance(source, np.ndarray):
continue
source = cv2.resize(source, (w, h))
# 计算几何距离,并加入列表,这里没有开方
distance = ((target - source) ** 2).sum()
distances.append((file, distance))
# 找到相似度前5的图片,这里拿了6个,第一个是原图
distances = sorted(distances, key=lambda x: x[-1])[:6]
imgs = list(map(lambda x: cv2.imread(x[0]), distances))
result = np.hstack(imgs)
cv2.imwrite("result.jpg", result)下面是一些比较好搜索结果,其中最左边是target,其余10张为搜索结果。如果换成猫狗图片,下面是一些搜索结果:2.3 存在的问题上面的实现存在两个问题,其一是像素并不能表示图像的深层含义。搜索结果中经常会返回颜色相似的图片。第二个则是计算复杂度的问题,如果图片大小未224×224,那么图片有150528个像素,计算几何距离会比较耗时。而且在搜索时,需要遍历整个图库,当图库数量较大时,计算量将不可忍受。因此需要对上面的方法进行一些改进。三、改进一,用特征代替像素3.1 图像特征在表示图片时,就是从基本的像素到手工特征再到深度学习特征。相比之下,用卷积神经网络提取的图像特征有几个有点,具体如下:具有很强的泛化能力,提取的特征受角度、位置、亮度等的影响会像素和手工特征。较少的维度,使用ResNet50提取224×224图片的特征时,会返回一个7×7×2048的张量,这比像素数量要少许多。具有抽象性,相比前面两种,卷积神经网络提取的特征具有抽象性。比如关于图片中类别的信息,这是前面两种无法达到的效果。在本文我们会使用ResNet50来提取图片特征。3.2 Embedding的妙用使用ResNet50提取的特征也可以被称为Embedding,也可以简单理解为图向量。Embedding近几年在人工智能领域发挥了巨大潜力,尤其在自然语言处理领域。3.2.1 关系可视化早期Embedding主要用于词向量,通过word2vec把单词转换成向量,然后就可以完成一些奇妙的操作。比如单词之间关系的可视化,比如下面这张图:在图片中可视化了:mother、father、car、auto、cat、tiger六个单词,从图可以明显看出mother、father比较近;car、auto比较近;cat、tiger比较近,这些都与我们常识相符。3.2.2 关系运算我们希望训练良好的Embedding每一个维度都有一个具体的含义,比如第一维表示词性,第二维表示情感,其余各个维度都有具体含义。如果能达到这个效果,或者达到近似效果,那么就可以使用向量的计算来计算单词之间的关系。比如“妈妈-女性+男性≈爸爸”,或者“国王-男性+女性≈皇后”。比如以往要搜索“物理学界的贝多芬是谁”可能得到非常奇怪的结果,但是如果把这个问题转换成“贝多芬-音乐界+物理学界≈?”,这样问题就简单多了。3.2.3 聚类当我们可以用Embedding表示图片和文字时,就可以使用聚类算法完成图片或文字的自动分组。在许多手机的相册中,就有自动图片归类的功能。聚类还可以加速搜索的操作,这点会在后面详细说。3.3 以图搜图改进下面使用图像特征来代替像素改进以图搜图,代码如下:import os
import cv2
import random
import numpy as np
from keras.api.keras.applications.resnet50 import ResNet50
from keras.api.keras.applications.resnet50 import preprocess_input
w, h = 224, 224
# 加载模型
encoder = ResNet50(include_top=False)
base_path = r"G:\datasets\lbxx"
files = [os.path.join(base_path, file) for file in os.listdir(base_path)]
target_path = random.choice(files)
target = cv2.resize(cv2.imread(target_path), (w, h))
# 提取图片特征
target = encoder(preprocess_input(target[None]))
distances = []
for file in files:
source = cv2.imread(file)
if not isinstance(source, np.ndarray):
continue
# 读取图片,提取图片特征
source = cv2.resize(source, (w, h))
source = encoder(preprocess_input(source[None]))
distance = np.sum((target - source) ** 2)
distances.append((file, distance))
# 找到相似度前5的图片,这里拿了6个,第一个是原图
distances = sorted(distances, key=lambda x: x[-1])[:6]
imgs = list(map(lambda x: cv2.imread(x[0]), distances))
result = np.hstack(imgs)
cv2.imwrite("result.jpg", result)这里使用在imagenet上预训练的ResNet50作为特征提取网络,提取的关键操作如下:加载模型# 加载ResNet50的卷积层,舍弃全连接部分
encoder = ResNet50(include_top=False)图片预处理# 把图片转换成224×224,并使用ResNet50内置的预处理方法处理
target = cv2.resize(cv2.imread(target_path), (w, h))
target = preprocess_input(target[None])提取特征# 使用ResNet40网络提取特征
target = encoder(preprocess_input(target)下面是改进后的搜索结果:四、改进二,使用聚类改进搜索速度4.1 实现原理在前面的例子中,我们都是使用线性搜索的方式,此时需要遍历所有图片。搜索复杂度为O(n),通常可以用树结构来存储待搜索的内容,从而把复杂度降低到O(logn)。这里我们使用更简单的方法,即聚类。首先我们要做的就是对图片的特征进行聚类,聚成c个簇,每个簇都会对应一个簇中心。簇中心可以认为是一个簇中的平均结构,同一簇中的样本相似度会比较高。在完成聚类后,我们可以拿到target图片的向量,在c个簇中心中查找target与哪个簇最接近。然后再到当前簇中线性查找最相似的几个图片。4.2 代码实现代码实现分为下面几个步骤:把图片转换成向量这部分代码和前面基本一样,不过这次为了速度快,我们把图像特征存储到embeddings.pkl文件:import os
import cv2
import pickle
import numpy as np
import tensorflow as tf
from keras.api.keras.applications.resnet50 import ResNet50
from keras.api.keras.applications.resnet50 import preprocess_input
w, h = 224, 224
# 加载模型
encoder = ResNet50(include_top=False)
base_path = r"G:\datasets\lbxx"
# 获取所有图片路径
files = [os.path.join(base_path, file) for file in os.listdir(base_path)]
# 将图片转换成向量
embeddings = []
for file in files:
# 读取图片,转换成与目标图片同一尺寸
source = cv2.imread(file)
if not isinstance(source, np.ndarray):
continue
source = cv2.resize(source, (w, h))
embedding = encoder(preprocess_input(source[None]))
embeddings.append({
"filepath": file,
"embedding": tf.reshape(embedding, (-1,))
})
with open('embeddings.pkl', 'wb') as f:
pickle.dump(embeddings, f)对所有向量进行聚类操作这里可以使用sklearn完成:from sklearn.cluster import KMeans
with open('embeddings.pkl', 'rb') as f:
embeddings = pickle.load(f)
X = [item['embedding'] for item in embeddings]
kmeans = KMeans(n_clusters=500)
kmeans.fit(X)
preds = kmeans.predict(X)
for item, pred in zip(embeddings, preds):
item['cluster'] = pred
joblib.dump(kmeans, 'kmeans.pkl')
with open('embeddings.pkl', 'wb') as f:
pickle.dump(embeddings, f)如果图片数量比较多的话,这部分操作会比较耗时。然后调用kmeans.predict方法就可以知道某个图片属于哪个簇,这个也可以事先存储。找到输入图片最近的簇中心在训练完成后,就可以拿到所有簇中心:kmeans.cluster_centers_现在要做的就是找到与输入图片最近的簇中心,这个和前面的搜索一样:# 查找最近的簇
closet_cluster = 0
closet_distance = sys.float_info.max
for idx, center in enumerate(centers):
distance = np.sum((target.numpy() - center) ** 2)
if distance < closet_distance:
closet_distance = distance
closet_cluster = idx在当前簇中查找图片这个和前面也是基本一样的:distances = []
for item in embeddings:
if not item['cluster'] == closet_cluster:
continue
embedding = item['embedding']
distance = np.sum((target - embedding) ** 2)
distances.append((item['filepath'], distance))
# 对距离进行排序
distances = sorted(distances, key=lambda x: x[-1])[:11]
imgs = list(map(lambda x: cv2.imread(x[0]), distances))
result = np.hstack(imgs)
cv2.imwrite("result.jpg", result)下面是一些搜索结果:效果还是不错的,而且这次搜索速度快了许多。不过在编码上这种方式比较繁琐,为了让代码更简洁,下面引入向量数据库。五、向量数据库5.1 向量数据库向量数据库和传统数据库不太一样,可以在数据库中存储向量字段,然后完成向量相似度检索。使用向量数据库可以很方便实现上面的检索功能,而且性能方面会比前面更佳。向量数据库与传统数据库有很多相似的地方,在关系型数据库中,数据库分为连接、数据库、表、对象。在向量数据库中分别对应连接、数据库、集合、数据。集合中,可以添加embedding类型的字段,该字段可以用于向量检索。5.2 Milvus向量数据库的使用下面简单说一下Milvus向量数据库的使用,首先需要安装Milvus,执行下面两条执行即可:wget https://github.com/milvus-io/milvus/releases/download/v2.2.11/milvus-standalone-docker-compose.yml -O docker-compose.yml
sudo docker-compose up -d下载完成后,需要连接数据库,代码如下:from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection, utility
connections.connect(host='127.0.0.1', port='19530')然后创建集合:def create_milvus_collection(collection_name, dim):
if utility.has_collection(collection_name):
utility.drop_collection(collection_name)
fields = [
FieldSchema(name='id', dtype=DataType.INT64, descrition='ids', max_length=500, is_primary=True,
auto_id=True),
FieldSchema(name='filepath', dtype=DataType.VARCHAR, description='filepath', max_length=512),
FieldSchema(name='embedding', dtype=DataType.FLOAT_VECTOR, descrition='embedding vectors', dim=dim),
]
schema = CollectionSchema(fields=fields, description='reverse image search')
collection = Collection(name=collection_name, schema=schema)
# create IVF_FLAT index for collection.
index_params = {
'metric_type': 'L2',
'index_type': "IVF_FLAT",
'params': {"nlist": 2048}
}
collection.create_index(field_name="embedding", index_params=index_params)
return collection
collection = create_milvus_collection('images', 2048)其中create_milvus_collection的第二个参数是embedding的维度,这里传入图片特征的维度。然后把图片特征存储到向量数据库中,这里需要注意维度不能超过32768,但是ResNet50返回的维度超过了这个限制,为此可以用PCA降维或者采用其它方法获取图片embedding。import pickle
from sklearn.decomposition import PCA
with open('embeddings.pkl', 'rb') as f:
embeddings = pickle.load(f)
X = [item['embedding'] for item in embeddings]
pca = PCA(n_components=2048)
X = pca.fit_transform(X)
for item, vec in zip(embeddings, X):
item['embedding'] = vec
with open('embeddings.pkl', 'wb') as f:
pickle.dump(embeddings, f)
with open('pca.pkl', 'wb') as f:
pickle.dump(pca, f)这样就可以插入数据了,代码如下:index_params = {
"metric_type": "L2",
"index_type": "IVF_FLAT",
"params": {"nlist": 1024}
}
with open('embeddings.pkl', 'rb') as f:
embeddings = pickle.load(f)
base_path = r"G:\datasets\lbxx"
# 获取所有图片路径
files = [os.path.join(base_path, file) for file in os.listdir(base_path)]
for item in embeddings:
collection.insert([
[item['filepath']],
[item['embedding']]
])现在如果想要搜索图片,只需要下面几行代码即可:import os
import cv2
import joblib
import random
import numpy as np
import tensorflow as tf
from PIL import Image
from keras.api.keras.applications.resnet50 import ResNet50
from keras.api.keras.applications.resnet50 import preprocess_input
from pymilvus import connections, Collection
pca = joblib.load('pca.pkl')
w, h = 224, 224
encoder = ResNet50(include_top=False)
base_path = r"G:\datasets\lbxx"
files = [os.path.join(base_path, file) for file in os.listdir(base_path)]
target_path = random.choice(files)
target = cv2.resize(cv2.imread(target_path), (w, h))
target = encoder(preprocess_input(target[None]))
target = tf.reshape(target, (1, -1))
target = pca.transform(target)
# 连接数据库,加载images集合
connections.connect(host='127.0.0.1', port='19530')
collection = Collection(name='images')
search_params = {"metric_type": "L2", "params": {"nprobe": 10}, "offset": 5}
collection.load()
# 在数据库中搜索
results = collection.search(
data=[target[0]],
anns_field='embedding',
param=search_params,
output_fields=['filepath'],
limit=10,
consistency_level="Strong"
)
collection.release()
images = []
for result in results[0]:
entity = result.entity
filepath = entity.get('filepath')
image = cv2.resize(cv2.imread(filepath), (w, h))
images.append(np.array(image))
result = np.hstack(images)
cv2.imwrite("result.jpg", result)下面是一些搜索结果,整体来看还是非常不错的,不过由于降维的关系,搜索效果可能或略差于前面,但是整体效率要高许多。六、总结本文我们分享了以图搜图的功能。主要思想就是将图片转换成向量表示,然后利用相似度计算,在图库中查找与之最接近的图片。最开始使用线性搜索的方式,此时查找效率最低。而后使用聚类进行改进,把把图片分成多个簇,把查找分为查找簇和查找最近图片两个步骤,可以大大提高查找效率。改进后代码变得比较繁琐,于是引入向量数据库,使用向量数据库完成检索功能。这样就完成了整个程序的编写。
小乔学算法
如何实现文字搜图
一、前言上一篇介绍以图搜图的实现:juejin.cn/post/725585…,我们利用了卷积神经网络提取特征,然后对比特征相似度,并使用向量数据库加快查找。本文我们将介绍根据文本搜索图片的实现。首先需要知道根据文本搜索图片具体是什么问题,这里可以有两个层面。第一个则是图片中包含的文本内容,这个可以用OCR识别提取出来。第二个则是深层次的对图片描述的文本,比如红色的狗、跑步的猪、骑猪的人。这些都是对图片内容的描述,相比之下第二种要复杂得多。二、OCR+文字搜图OCR是指光学字符识别,也就是我们常说的文字识别。OCR的实现方式是多样的,这里使用Tesseract或者各种神经网络。OCR不是文本重点,因此这里只简单介绍其使用。详情可见:juejin.cn/post/696437…OCR+文字搜图的原理非常简单,就是先识别文字,然后根据文字模糊查询找到相关图片即可。为了方便查询,这里需要使用数据库。2.1 文字识别使用pytesseract模块可以很方便实现OCR,具体代码如下:import os, cv2
import numpy as np
import pytesseract
from tqdm import tqdm
from PIL import Image
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
base_path = "G:\datasets\emoji"
files = [os.path.join(base_path, file) for file in os.listdir(base_path) if file.endswith(".jpg")]
for file in files:
try:
image = Image.open(file)
string = pytesseract.image_to_string(image, lang='chi_sim')
print(file, ":", string.strip())
except Exception as e:
pass其中string就是识别到的文本内容。pytesseract中也提供了批量识别的接口,因为这里存在一些错误图片,因此这里不适用批量接口。2.2 存储数据库为了方便查询,可以把图片路径和图片中包含的文本内容存储到数据库中。这里使用sqlalchemy+sqlite,代码如下:from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String
class Base(DeclarativeBase):
pass
class ImageInformation(Base):
__tablename__ = "image_information"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
filepath: Mapped[str] = mapped_column(String(255))
content: Mapped[str] = mapped_column(String(255))
def __repr__(self) -> str:
return f"User(id={self.id!r}, filepath={self.filepath!r}, content={self.content!r})"
engine = create_engine("sqlite:///image_search.db", echo=False)
Base.metadata.create_all(engine)其中ImageInformation类对应我们需要创建的数据库表。创建好后,识别图片的文字,然后存储到图片数据库中:base_path = "G:\datasets\emoji"
files = [os.path.join(base_path, file) for file in os.listdir(base_path) if file.endswith(".jpg")]
bar = tqdm(total=len(files))
for file in files:
try:
# 识别文字
image = Image.open(file)
string = pytesseract.image_to_string(image, lang='chi_sim').strip()
file = file[:255] if len(file) > 255 else file
string = string[:255] if len(string) > 255 else string
# 存储数据库
with Session(engine) as session:
info = ImageInformation(filepath=file, content=string)
session.add_all([info])
session.commit()
except Exception as e:
pass
bar.update(1)这个过程会比较久。2.3 根据文字搜索图片完成上面的存储操作后,就可以开始根据文字查找图片了。这里只需要使用简单的数据库查询操作即可完成,代码如下,我们先把你好作为输入文本:keyword = '你好'
w, h = 224, 224
with Session(engine) as session:
stmt = select(ImageInformation).where(ImageInformation.content.contains(keyword)).limit(8)
images = [cv2.resize(cv2.imread(ii.filepath), (w, h)) for ii in session.scalars(stmt)]
if len(images) > 0:
result = np.hstack(images)
cv2.imwrite("result.jpg", result)
else:
print("没有找到结果")下面是查询到的结果图片:如果关键词改为喜欢,得到结果如下:经过测试,发现在一些短文本搜索中,这种方式比较奏效,但是在长文本则经常搜索不到结果。一种改进方式是不存储文本本身,而是使用Bert等模型把文本转换成Embedding,然后存储Embedding。这样我们就不能再使用sqlite了,而需要使用向量数据库。三、基于Transformer的改进在前面的例子中,搜索结果非常依赖字符串匹配。比如查找鸡,只有图片中有鸡字才会被搜索到,而与坤相关的图片则查找不到。为此我们用Transformer对上面进行改进,主要思路就是先识别文字,然后把文字交给文本编码器,转换成Embedding,然后在查找时查找输入文本和Embedding的相似度,这样就可以缓解上述问题。3.1 创建数据库这里我们还是使用向量数据库,向量数据库有很多选择,这里使用Milvus数据库,具体使用可以参考:milvus.io/docs/instal…首先创建数据库和集合:from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection, utility
# 创建数据库
connections.connect(host='127.0.0.1', port='19530')
def create_milvus_collection(collection_name, dim):
if utility.has_collection(collection_name):
utility.drop_collection(collection_name)
fields = [
FieldSchema(name='id', dtype=DataType.INT64, descrition='ids', max_length=500, is_primary=True,
auto_id=True),
FieldSchema(name='filepath', dtype=DataType.VARCHAR, description='filepath', max_length=512),
FieldSchema(name='embedding', dtype=DataType.FLOAT_VECTOR, descrition='embedding vectors', dim=dim),
]
schema = CollectionSchema(fields=fields, description='reverse image search')
collection = Collection(name=collection_name, schema=schema)
# create IVF_FLAT index for collection.
index_params = {
'metric_type': 'L2',
'index_type': "IVF_FLAT",
'params': {"nlist": 2048}
}
collection.create_index(field_name="embedding", index_params=index_params)
return collection
collection = create_milvus_collection('image_information', 768)
集合中主要有filepath和embedding两个字段。其中embedding有768个维度,这是由Transformer决定的,我这里选择的Transformer输出768个维度,因此这里填768。3.2 text2vec创建完成后,就是读取图片、识别文字、文字编码、存入数据库。其中文字编码可以使用Transformers模块或者text2vec完成,这里使用text2vec,其操作如下:from text2vec import SentenceModel
model = SentenceModel('shibing624/text2vec-base-chinese')
embeddings = model.encode(['不要温顺地走进那个良夜'])
print(embeddings.shape)在创建SentenceModel时传入对应的模型,然后调用model.encode方法即可。输出如下结果:(1, 768)其余操作则不详细解释,具体代码如下:from text2vec import SentenceModel
model = SentenceModel('shibing624/text2vec-base-chinese', device="cuda")
base_path = "G:\datasets\emoji"
files = [os.path.join(base_path, file) for file in os.listdir(base_path) if file.endswith(".jpg")]
bar = tqdm(total=len(files))
for idx, file in enumerate(files):
try:
image = Image.open(file)
string = pytesseract.image_to_string(image, lang='chi_sim').strip()
embedding = model.encode([string])[0]
collection.insert([
[file],
[embedding]
])
except Exception as e:
pass
bar.update(1)3.3 根据文字搜索图片在插入数据后,直接使用数据库的查询操作即可完成搜索操作,具体代码如下:import cv2
import numpy as np
from text2vec import SentenceModel
from pymilvus import connections, Collection
# 加载模型
model = SentenceModel('shibing624/text2vec-base-chinese', device="cuda")
# 连接数据库,加载集合
connections.connect(host='127.0.0.1', port='19530')
collection = Collection(name='image_information')
search_params = {"metric_type": "L2", "params": {"nprobe": 10}, "offset": 5}
collection.load()
# 用来查询的文本
keyword = "今天不开心"
embedding = model.encode([keyword])
print(embedding.shape)
# 在数据库中搜索
results = collection.search(
data=[embedding[0]],
anns_field='embedding',
param=search_params,
output_fields=['filepath'],
limit=10,
consistency_level="Strong"
)
collection.release()
# 展示查询结果
w, h = 224, 224
images = []
for result in results[0]:
entity = result.entity
filepath = entity.get('filepath')
image = cv2.resize(cv2.imread(filepath), (w, h))
images.append(np.array(image))
result = np.hstack(images)
cv2.imwrite("result.jpg", result)向量数据库在查询时,可以根据向量的相似度返回查询结果。在前面我们存储了句向量,所以我们可以把查询文本转换成句向量,然后利用向量数据库的查询功能,查找相似结果。在上面代码中,我们查询“今天不开心”,这次不再是字符串层面的查询,而是句子含义层面的查询,因此可以查询的不包含这些字符的图片,下面是查询结果:把关键词修改为“我想吃饭”后得到下面的结果:整体效果还是非常不错的。不过前面的结果是建立在能在图片中识别到文本的情况下,如果是我们随手拍的照片,那么就不能使用上面的方式来实现文字搜索图片。四、基于图片含义的文字搜图在多模态领域有许多组合模型,而我们需要的就是Image-to-Text类模型。如果要手工给图片添加画面描述会非常麻烦,因此我们选择使用Image-to-Text模型完成自动识别。4.1 实现原理基于图片含义的文字搜图的实现与前面基于OCR的类似,只不过需要把OCR修改为Image Captioning网络。在前面我们的流程是:读取图片OCR识别把识别结果转换成向量存入数据库现在只需要把第二步修改为使用Image Captioning生成图片描述即可。后面部分则是完全一致的。4.2 Image Captioning像这类输入图片,输出画面描述的任务叫做Image Captioning,用于这一任务的模型非常多。包括CNN+LSTM,Vit等都可以实现Image Captioning。两者都是一个Encoder-Decoder架构,使用CNN、Vit作为图片Encoder,将图片转换成特征图或者特征向量。然后把Encoder的输出作为Decoder的输入,并输入,然后依次生成图片描述。以Vit为例,其结构如图:Vit其实就是一个为图片设计的Transformer架构,在某些细节上为图片做了一些修改。4.3 根据图片含义搜索图片首先我们可以使用和前面相同的方式创建数据库,这里不再重复,我们复用前面的数据库image_information。然后需要修改插入数据的代码,首先来创建一个函数加载模型,并创建一个函数用于将图片转换成文本向量,代码如下:import os
import torch
from tqdm import tqdm
from PIL import Image
from text2vec import SentenceModel
from transformers import VisionEncoderDecoderModel, ViTImageProcessor, AutoTokenizer
from pymilvus import Collection
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_model():
"""
加载需要使用到的模型
"""
sentence_model = SentenceModel('shibing624/text2vec-base-chinese', device="cuda")
model = VisionEncoderDecoderModel.from_pretrained("bipin/image-caption-generator")
image_processor = ViTImageProcessor.from_pretrained("bipin/image-caption-generator")
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model.to(device)
return sentence_model, model, image_processor, tokenizer
def get_embedding(filepath):
"""
输入图片路径,将图片转成描述向量
"""
pixel_values = image_processor(images=[Image.open(filepath)], return_tensors="pt").pixel_values.to(device)
output_ids = model.generate(pixel_values, num_beams=4, max_length=128)
pred = tokenizer.decode(output_ids[0], skip_special_tokens=True)
return sentence_model.encode(pred)后续只需要调用get_embedding函数就可以完成图片到向量的转换。接下来就是修改插入数据的代码,具体如下:connections.connect(host='127.0.0.1', port='19530')
collection = Collection("image_information")
collection.load()
sentence_model, model, image_processor, tokenizer = load_model()
base_path = "G:\datasets\people"
files = [os.path.join(base_path, file) for file in os.listdir(base_path)]
bar = tqdm(total=len(files))
for idx, file in enumerate(files):
try:
embedding = get_embedding(file)
collection.insert([
[file],
[embedding]
])
except Exception as e:
pass
bar.update(1)最后则是搜图操作了,这个和前面是完全一样的:search_params = {"metric_type": "L2", "params": {"nprobe": 10}, "offset": 5}
# 用来查询的文本
keyword = "girl"
embedding = sentence_model.encode([keyword])
# 在数据库中搜索
results = collection.search(
data=[embedding[0]],
anns_field='embedding',
param=search_params,
output_fields=['filepath'],
limit=10,
consistency_level="Strong"
)
collection.release()
# 展示查询结果
w, h = 224, 224
images = []
for result in results[0]:
entity = result.entity
filepath = entity.get('filepath')
image = cv2.resize(cv2.imread(filepath), (w, h))
images.append(np.array(image))
result = np.hstack(images)
cv2.imwrite("result.jpg", result)因为这里选择的Image Captioning模型输出为英文,因此这里把英文作为关键字。这里关键字为"girl",下面是搜索结果:因为数据库中还存储了之前的表情包,因此表情包中关于与"girl"有关的表情包也搜索出来了,比如"娘们"、"女人"等。如果把关键字改为"smile girl",搜索结果如下:如果图片数量足够,则可以得到一个比较好的搜索结果。上面的结果还可以有一些改进,在Image Captioning步骤,我们只生成了一个描述。在很多情况下,这个描述不一定准确,比如下面的图片:可以描述为“拿着话筒的姑娘”、“一个姑娘在微笑”或者“一个拿着话筒的姑娘在微笑”。因此我们可以生成多个描述,存入数据库,这样在查找时结果可以更准确。可以通过修改temperature参数来生成不同的描述:output_ids = model.generate(pixel_values, num_beams=4, max_length=128, temperature=0.8)当temperature小于1时,生成结果带有随机性。temperature越小,结果越随机。
小乔学算法
Amazon SageMaker使用自编码器完成人脸生成
一、前言最近受邀参与了亚马逊云科技【云上探索实验室】活动,体验了一下Amazon SageMaker平台,训练了一个人脸的自编码器。对比本地的训练时间,速度提升比较明显。本文将围绕Amazon SageMaker和自编码器进行。自编码器是一个非常简单的网络,早在上世纪90年代就提出了自编码器的概念。当时使用受限的玻尔兹曼机分层训练,在硬件强大的今天可以实现端到端的训练。自编码器有许多变种,比如变分自编码器、去噪自编码器、正则自编码器等。由于自编码器采用的是自监督学习,因此使用自编码器可以使用较低成本得到较好的效果。今天我们将训练一个自编码器。自编码器训练完成后可以进行各种有趣的实验,像编辑一个人的表情、让人脸A渐渐变成人脸B、让一个人从小慢慢变老、生成人脸等。在本文我们会做一部分实验,实现人脸渐变和生成人脸的操作。1.1 什么是Amazon SageMakerAmazon SageMaker是一个完全托管的机器学习服务平台,包含了机器学习的各个流程,从标注到部署。开发人员可以快速构建模型并训练,还可以部署到托管环境。Amazon SageMaker提供了Jupyter笔记本,而且可以执行各种流行框架,不止是MXNet,还可以使用PyTorch、TensorFlow等主流框架。1.2 有什么特别的在数据标注时,Ground Truth可以用来团队标注,在标注一定数据后,Ground Truth可以自动标注,当对标注不确定时才会让人工进行标注。Amazon SageMaker提供了数据存储,模型部署等服务,完成这些操作都可以一键式完成。同时Amazon SageMaker提供了许多高阶API,可以使用Amazon Auto Pilot自动训练模型与调优模型。同时还可以对模型的情况进行监控,以便更好地改善模型。二、机器学习流程2.1 机器学习整体流程在开始使用Amazon SageMaker完成机器学习任务前,先熟悉一下机器学习的流程。机器学习的流程分为一下几个步骤:获取数据数据清洗将数据处理成模型的输入训练模型评估模型部署模型监控、收集数据、评估模型上述步骤整体是一条直线,但是经常会回到前面几个步骤重新往下执行。如下图所示:(1)数据处理1-3为数据处理部分,主要是数据从无到有、从杂乱到规范,在此步骤中,我们会用到诸如爬虫、正则、归一化、标准化等技术。处理后的数据通常表示为一个张量(多维数组),不同类型的数据的形状有所不同。图像数据通常为n_sample × channel × height × width,各维度含义分别为样本数、通道数、高、宽,有时候通道数会放在最后一个维度。表格数据被处理为n_sample × n_feature,分别为样本数、特征数。文本数据则是n_sample × sequence_len × 1,分别为样本数和文本长度。还有时间序列、视频、立体图片等数据都会处理成固定形状的张量。数据处理成张量后还可以做一些特征工程,比如特征选择、标准化、归一化等。这一步操作有利于模型训练。(2)训练模型4-5为训练评估部分,在处理好数据后就可以开始训练模型了。训练模型需要我们确定几样东西,分别是模型结构(选取上面算法)、对应的超参数、优化器等。在此过程通常会训练多个模型,选取一个最优模型作为最终模型。为了选择较优的超参数,会使用到网格搜索交叉验证。在sklearn模块中,提供了对应的实现。SageMaker中也有类似更强的功能,后面有具体体验。(3)部署及维护当选取一个最优模型后,既可以将模型部署上线,模型的部署可以作为API、Web、小程序、APP等。在部署上线后,模型可能会出现各种问题,也会慢慢落后,因此还需要对模型进行监控维护。在上线后,可以继续收集一些用户授权的数据,然后把这些数据重复之前的步骤,对模型进行迭代优化。2.2 SageMaker中机器学习流程SageMaker中机器学习流程于上面一致,下面我们来实际看看各个步骤如何操作。(1)数据处理Amazon SageMaker中可以创建Jupyter Notebook,创建的Notebook可以执行pip、wget等指令。我们可以使用以往使用的所有方式处理数据,也可以使用SageMaker自带的SDK。在SageMaker中有一个sklearn子模块,可以用来处理数据。SKLearnProcessor可以执行sklearn脚本来处理数据,首先需要创建SKLearnProcessor对象,然后调用run方法对数据进行处理,示例代码如下:from sagemaker import get_execution_role
from sagemaker.sklearn.processing import SKLearnProcessor
from sagemaker.processing import ProcessingInput, ProcessingOutput
role = get_execution_role()
sklearn_processor = SKLearnProcessor(
framework_version="0.20.0", role=role, instance_type="ml.t3.medium", instance_count=1
)
sklearn_processor.run(
code="preprocessing.py",
inputs=[
ProcessingInput(source="s3://your-bucket/path/to/your/data", destination="/opt/ml/processing/input"),
],
outputs=[
ProcessingOutput(output_name="train_data", source="/opt/ml/processing/train"),
ProcessingOutput(output_name="test_data", source="/opt/ml/processing/test"),
],
arguments=["--train-test-split-ratio", "0.2"],
)
preprocessing_job_description = sklearn_processor.jobs[-1].describe()在创建调用run方法时,需要先创建好用于处理数据的preprocessing.py文件,对应code参数。preprocessing.py命令行参数由arguments给出。preprocessing.py可以用sklearn来做具体处理。处理完成后,处理结果等信息保存在preprocessing_job_description中,可以通过preprocessing_job_description['Outputs']拿到处理结果。(2)训练模型模型的训练与处理数据一样,需要准备好对应的训练脚本train.py。处理好数据并准备好train.py脚本后,使用下面的代码就可以进行训练了,这里的实例类型要和数据处理的实例类型一致。from sagemaker.sklearn.estimator import SKLearn
sklearn = SKLearn(
entry_point="train.py", framework_version="0.20.0", instance_type="ml.t3.medium", role=role
)
sklearn.fit({"train": preprocessed_training_data})
training_job_description = sklearn.jobs[-1].describe()
model_data_s3_uri = "{}{}/{}".format(
training_job_description["OutputDataConfig"]["S3OutputPath"],
training_job_description["TrainingJobName"],
"output/model.tar.gz",
)评估模型的代码也是一样的风格:sklearn_processor.run(
code="evaluation.py",
inputs=[
ProcessingInput(source=model_data_s3_uri, destination="/opt/ml/processing/model"),
ProcessingInput(source=preprocessed_test_data, destination="/opt/ml/processing/test"),
],
outputs=[ProcessingOutput(output_name="evaluation", source="/opt/ml/processing/evaluation")],
)
evaluation_job_description = sklearn_processor.jobs[-1].describe()(3)部署构建和训练模型后,您可以将模型部署至终端节点,以中获取预测推理结果。部署使用下面代码即可完成:predictor = estimator.deploy(initial_instance_count=1, instance_type="ml.m4.xlarge")
可以根据任务要求选择实例类型。三、自编码器下面使用SageMaker完成自编码相关的实验。3.1 自编码器介绍自编码器是一个非常简单网络,通常由编码器和解码器两个部分组成。编码器解码器的结构可以用全连接,也可以用卷积,或者其它一些网络。在早期自编码器的编码器解码器需要分开训练,而现在通常是端到端的训练。编码器部分会将输入逐步降维,最后得到一个固定长度的向量,这个向量可以作为输入数据的编码。解码器部分接收编码器的输出,结果解码器会得到一个形状与编码器输入一样的数据。自编码器训练的目的就是输出与数据尽可能接近。整体上看自编码器使用的是监督学习方法,但是目标值和特征值是一样的,像这种标签由数据自身给出的学习方法又被称为自监督学习。自编码器结构简单,但是有一些非常好的性质。比如训练简单,不需要人工标记数据等。使用自编码器可以对数据进行降维,创建新数据等。假设对人脸图像数据进行编码,编码器会得到1024维的向量。那么编码器得到的这个向量每一个维度可能代表着一种特征,比如第n个维度可能代表表情、第k个维度可能代表性别等。如果能知道这些信息,就可以做一些有趣的事情。3.2 环境准备这里使用Amazon SageMaker的笔记本实例进行实验,创建笔记本实例,创建时使用默认选项即可。在创建时需要记住使用的实例类型,后续训练需要对应正确的类型。创建好环境后,可以在笔记本中运行下面代码获取当前角色以及S3桶:import sagemaker
import os
sess = sagemaker.Session()
role = sagemaker.get_execution_role()
bucket = sagemaker_session.default_bucket()下面就可以开始数据的准备了。3.3 数据处理这里使用PyTorch完成数据的处理和训练,下面是需要用到的一些模块:from torch import nn
from torch import optim
from torch.utils.data import Dataset, DataLoader
from torchvision import datasets
from torchvision import transforms本实验使用的是CelebA Dataset,可以在mmlab.ie.cuhk.edu.hk/projects/Ce…下载。数据集里面包含了10万修正好的人脸图片。将图片下载到电脑上后,再把数据上传到笔记本中。为了方便加载,创建一个Dataset类,完成数据的加载:class FaceDataset(Dataset):
def __init__(self, data_dir="./datasets/img_align_celeba", image_size=64):
self.image_size = image_size
self.trans = transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
self.image_paths = []
if os.path.isdir(data_dir):
for root, dirs, files in os.walk(data_dir):
for file in files:
self.image_paths.append(os.path.join(root, file))
elif os.path.isfile(data_dir):
with open(data_dir, encoding='utf-8') as f:
self.image_paths = f.read().strip().split()
def __getitem__(self, item):
return self.trans(Image.open(self.image_paths[item]))
def __len__(self):
return len(self.image_paths)下面创建DataLoader用于加载数据:dataset = FaceDataset(data_dir="./datasets/img_align_celeba")
dataloader = DataLoader(dataset, 64)另外,可以通过下面代码将数据上传至S3桶:inputs = sagemaker_session.upload_data(path="datasets/img_align_celeba", bucket=bucket, key_prefix='sagemaker/img_align_celeba')数据准备好后,需要编写模型以及训练脚本。3.2 模型训练本文以人脸数据训练一个自编码器,而后用这个自编码器做一些其它事情,首先训练一个自编码器。网络结构由卷积和转置卷积组成,代码如下:class FaceAutoEncoder(nn.Module):
def __init__(self, encoded_dim=1024):
super(FaceAutoEncoder, self).__init__()
# [b, 3, 64, 64] --> [b, 1024, 1, 1]
self.encoder = nn.Sequential(
nn.Conv2d(3, 64, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64 * 2, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64 * 4, 64 * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64 * 8, encoded_dim, 4, 1, 0, bias=False),
nn.LeakyReLU(0.2, inplace=True)
)
# [b, 1024, 1, 1] - > [b, 3, 64, 64]
self.decoder = nn.Sequential(
nn.ConvTranspose2d(encoded_dim, 64 * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(64 * 8),
nn.ReLU(True),
nn.ConvTranspose2d(64 * 8, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4),
nn.ReLU(True),
nn.ConvTranspose2d(64 * 4, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2),
nn.ReLU(True),
nn.ConvTranspose2d(64 * 2, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=True),
nn.Tanh()
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x
编码器部分将64×64的图片转换成1024维的向量,而解码器则是利用转置卷积将1024维的向量转换成64×64的图像。现在我们的目标是模型输入与数据越接近越好,所有可以用均方误差作为损失函数,同时输入和目标为同一批数据。下面编写一个训练脚本autoencoder.py,代码如下:def train(args):
device = "cuda" if torch.cuda.is_available() else "cpu"
dataset = FaceDataset()
dataloader = DataLoader(dataset, 64)
model = FaceAutoEncoder().to(device)
optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=0.9)
for epoch in range(1, args.epochs + 1):
model.train()
for batch_idx, data in enumerate(dataloader, 1):
data = data.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.mse_loss(output, data)
loss.backward()
optimizer.step()
save_model(model, args.model_dir)
def model_fn(model_dir):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = torch.nn.DataParallel(AutoEncoder())
with open(os.path.join(model_dir, "model.pth"), "rb") as f:
model.load_state_dict(torch.load(f))
return model.to(device)
def save_model(model, model_dir):
path = os.path.join(model_dir, "model.pth")
torch.save(model.decoder.cpu().state_dict(), path)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--epochs",
type=int,
default=10,
metavar="N",
help="number of epochs to train (default: 10)",
)
parser.add_argument(
"--lr", type=float, default=0.01, metavar="LR", help="learning rate (default: 0.01)"
)
train(parser.parse_args())
我们可以使用argparse设置一些参数。脚本准备好后,可以开始进行训练,因为使用的是PyTorch,所以要改用sagemaker.pytorch中的PyTorch来进行训练,代码如下:from sagemaker.pytorch import PyTorch
estimator = PyTorch(
entry_point="autoencoder.py",
role=role,
py_version="py38",
framework_version="1.11.0",
instance_count=1,
instance_type="ml.c5.2xlarge",
hyperparameters={"epochs": 4},
)
estimator.fit({"training": inputs})其中entry_point就是前面的训练脚本。然后等待模型训练,训练完成后可以得到模型文件model.pth,这里只包含decoder部分。下面是输出的结果:训练完成后使用下面一句就可以对模型进行部署:predictor = estimator.deploy(initial_instance_count=1, instance_type="ml.m4.xlarge")不过自编码器本身只是还原原有数据而已,要生成人脸需要使用Decoder部分,进行推理。四、使用自编码器实现人脸渐变4.1 人脸渐变原理在前面提到自编码器的一个优点就是可以把对输入的操作改为对编码的操作,现在我们训练了一个人脸的自编码。假设人脸A被编码成z1,人脸B被编码成z2,现在想让人脸由A到B渐变。现在可以把这个问题转换为向量z1和z2之间的渐变,向量的渐变可以直接使用插值算法,我们在两个向量见插入n个向量,再把这些向量输入解码器,得到的人脸图像就是介于A和B之间的人脸。现在人脸渐变就变为了插值。4.2 人脸渐变的实现首先实现插值算法,插值的实现很简单,具体代码如下:def interpolate(x1, x2, num):
result = torch.zeros((num, *x1.shape))
step = (x2 - x1) / (num - 1)
for i in range(num):
result[i] = x1 + step * i
return result上面函数输入两个长度一样的向量,输出num个向量。这num个向量将作为Decoder的输入。接下来使用Decoder部分进行推理:# 加载数据集
dataloader = DataLoader(
FaceDataset(data_dir='/home/zack/Files/datasets/img_align_celeba', image_size=64),
batch_size=2
)
model = FaceAutoEncoder()
model.load_state_dict(torch.load('../outputs/face_auto_encoder.pth'))
model.eval()
with torch.no_grad():
for idx, data in enumerate(dataloader):
# 对人脸编码
encoded1 = model.encoder(data[0].reshape((1, 3, 64, 64)))
encoded2 = model.encoder(data[1].reshape((1, 3, 64, 64)))
# 对人脸编码进行插值
encoded = interpolate(encoded1[0], encoded2[0], 64)
# 解码成人脸
outputs = model.decoder(encoded).reshape((64, 3, 64, 64))
outputs = make_grid(outputs, normalize=True)
plt.imshow(outputs.numpy().transpose((1, 2, 0)))
plt.show()下面是实现的效果:可以看到人脸转变的很自然。现在可以将这一部分部署,部署代码如下:from sagemaker.pytorch import PyTorchModel
pytorch = PyTorchModel(
model_data=model_data,
role=role,
entry_point="inference.py",
source_dir="code",
framework_version="1.3.1",
py_version="py3",
sagemaker_session=sagemaker_session,
)
predictor = pytorch.deploy(
initial_instance_count=1,
instance_type='ml.m5.large',
endpoint_name=endpoint_name,
wait=True,
)
其中inference.py是推理脚本,具体代码如下:def predict_fn(input_data, model):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
input_data = input_data.to(device)
with torch.no_grad():
inputs = interpolate(input_data[:, 0], input_data[:, 1])
return model.decoder.forward(inputs)
def model_fn(model_dir="model.pth"):
loaded_model = torch.jit.load(model_dir, map_location=torch.device("cpu"))
loaded_model = loaded_model.eval()
return loaded_model
然后使用predictor.predict传入2 × 3 × 64 × 64的张量就可以完成模型的推理,返回插值后的人脸图像。五、自编码器生成人脸自编码器除了实现人脸渐变外,还可以用来生成人脸,关键点依然在编码部分。5.1 人脸分布自编码器生成人脸的原理比较简单,在训练自编码器时,把人脸编码成一个长度为1024维的向量。现在我们假设人脸服从高斯分布,如果能求出均值和方差,就可以知道这个高斯分布的具体样子。在知道高斯分布的具体表达式后,就可以对从中采样人脸向量,把这个向量交给decoder就可以生成人脸。均值和方差可以用统计的方式获取,具体代码如下,把结果保存为一个npz文件:mean = np.zeros((zdim,), dtype=np.float32)
cov = np.zeros((zdim, zdim), dtype=np.float32)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.eval()
model = model.to(device)
with torch.no_grad():
for idx, data in enumerate(dataloader):
try:
data = data.to(device)
encoded = model.encoder(data).view(128, -1)
mean += encoded.mean(axis=0).cpu().numpy()
cov += np.cov(encoded.cpu().numpy().T)
if idx % 50 == 0:
print(f"\ridx: {idx}/{len(dataloader)}", end="")
except Exception as e:
pass
mean /= (idx + 1)
cov /= (idx + 1)
np.savez('face_distribution.npz', mean, cov)5.2 生成人脸生成人脸的操作就是从前面的高斯分布中进行采样,然后把采样的向量交给decoder进行编码即可。具体代码:# 加载人脸分布
distribution = np.load('face_distribution.npz')
mean = distribution['arr_0']
cov = distribution['arr_1']
# 生成编码向量
batch_size = 64
z = np.random.multivariate_normal(
mean,
cov,
batch_size
).astype(np.float32)
# 解码
with torch.no_grad():
encoded = torch.from_numpy(z).view(batch_size, 1024, 1, 1)
outputs = model.decoder(encoded)
outputs = torch.clamp(outputs, 0, 255)
grid = make_grid(outputs).numpy().transpose((1, 2, 0))
plt.imshow(grid)
plt.show()下面是生成的一些人脸,五官部分可以看的很清楚,但是背景部分有一些模糊。现在前面Amazon SageMaker部署的代码可以复用,只需要修改推理的代码即可。把inference.py中predict_fn函数修改成下面的样子:def predict_fn(input_data, model):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
z = torch.randn(1, 1024, 1, 1).to(device)
with torch.no_grad():
return model.decoder.forward(z)5.3 生成相关的模型在前面,我们使用Amazon SageMaker,围绕自编码器实现了人脸渐变和人脸生成的实验。自编码器并非主流的生成模型,对于图像生成,现在更流行Stable Diffusion模型,相比之下Stable Diffusion的能力更强,生成的图像更逼真。Amazon官方给出了Stable Diffusion相关实验:catalog.us-east-1.prod.workshops.aws/workshops/3…。六、总结6.1 算法总结自编码器的结构非常简单,训练起来相比GAN等要更容易,但是功能特别强大。在前面的实验中,我们都是把对结果的调整转为对编码的处理。在自编码器中有一个非常理想的设想,就是每个维度控制一个特征,但是实际上没有这么简单。自编码器还有一些其它变形,比如变分自编码器,ALAE等,可以实现更加强大的功能,甚至可以媲美StyleGAN,生成逼真的网络。除了直接使用自编码器,在其它网络中也可以插入与自编码器类似的结构,比如Stable Diffusion中就有Unet网络,也是一种编码解码结构。6.2 Amazon SageMaker总结用于机器学习的平台有很多,Amazon SageMaker是比较全面,且面向应用的一个。在Amazon SageMaker中包括了机器学习的各个流程,在以往Python中的开发习惯可以在Amazon SageMaker中完全适用,另外Amazon SageMaker提供了更高阶的API,可以让用户更专注于算法的研究。Amazon SageMaker支持Sklearn、PyTorch、TensorFlow、Hugging Face等,对这些主流模块和框架都有相应的封装。另外Amazon SageMaker提供了非常便捷的部署方式。为了方便训练模型,Amazon SageMaker中提供了Amazon AutoPilot可以自动对各种模型以及各组超参数进行搜索,训练最优模型。
小乔学算法
深度学习实战
深度学习实战文章,分享各种有趣的深度学习例子。
小乔学算法
大语言模型之LlaMA系列-LlaMA 2及LlaMA_chat(下)
接 Llama系列-Llama 2及LLaMA_chat(上)多转一致性的系统消息 - System Message for Multi-Turn Consistency在对话设置中,某些指示应适用于所有对话轮次。 例如,简洁地响应,或"充当"某个公众人物。当我们向Llama 2-Chat提供此类指示时,后续应响应始终遵守约束。然而,我们最初的RLHF模型往往会在几轮对话后忘记最初的指令,如图9(左)所示。为了解决这个问题,我们提出了Ghost Attention(GAtt),这是一种受上下文蒸馏启发的非常简单的方法,它对微调数据进行破解(hacks),以帮助注意力集中在多阶段过程中。如图9(右)所示,GAtt能控制对多轮对话,如图(9)右所示。GAtt方法假设我们有一组两人之间的多轮对话的数据集(例如,一个用户一和个助理),其中用户信息为u开头,助理回应的消息为a开头, n代表轮次, 对话内容为[u1,ai,...,un,an]。 然后我们再定义一个作用于整个对话的指令(例如,指令可以是"act as"),并将该指令与对话集融合。下一步,我们使用最新的RLHF模型从这个合成的对话集中进行采样。 我们现在就有一个上下文对话和样本,可采用类似于拒绝采样的过程对模型进行微调。 我们可以在除了第一回合之后移除了,而不是用指令来扩充所有的上下文对话,但这会导致系统消息(即最后一个回合之前的所有中间辅助消息)和我们的样本在训练时间上的不匹配。为了解决这个可能会影响到训练的问题,我们只需将前几回合的token的损失设置为0,这些token包括辅助消息。为了这些训练的指令,我们创建一些合成约束条件来进行采样:爱好(比如"你喜欢网球"),语言(比如"讲法语"),或公众人物(比如“扮演拿破伦”)。 这些兴趣爱好与公众人物列表 我们要求Llama 2-Chat来生成,这样可以指令和模型知识间的不匹配(例如:要求模型去扮演在训练时不存在的人物或兴趣). 为了让指令更加的复杂和多样式,我们通过随机组合上述约束来构建最终指令。在为训练数据构建最终的系统统消息时,我们也会有一半的时间去修改原始指令,使期不要那么冗长,例如:”从现在开始总是充当拿破仑“将改为"身份:拿破仑."。这些步骤会生成一个SFT数据集,用于微调Llama-2-chat.GAtt评估我们RLHF V3之后开始使用Gatt。 我们的一份定量分析报告指明GAtt可以让多轮对话在达到最大上下文长度之前将对话持续20+轮,还能保证一致性。 图28我们展示了推理时Gatt训练时不使用约束的情况,从图中可以看出,模型一致性保持的不错。为了描述GAtt在微调期间如何重塑注意力,图10展示了我们模型的最大注意力激动情况,其中图的左侧对应的系统消息(例如“扮演奥斯卡·王尔德”),左图为没用GAtt模型的情况,右图为使用GAtt模型的史。我们从图可以看到,与没有Gatt(左)的模型相比,配备了GAtt的模型(右)在对话的大部分时间里保持了对系统消息的最大注意力激活。尽管他很实用,但当前的GAtt实现是vanilla且该技术的更多开发和迭代可能对该模型更有益。例如,我们可以教模型去在对话期间过程中通过在微调过程中集成这些数据去改变系统消息。RLHF结果Model-Based Evaluation评估LLMs是一个具有挑战性的开放研究问题。虽然人类评估是一个黄金标准,但也可能因为各种HCI因素而变的复杂,且并不总是可扩展的。为了在每 在从RLHF-V1到V5的每次迭代的几个消融中选择性能最好的模型, 我们首先从最新的奖励模型中观察到奖励的改进,以节省成本同时提高迭代速度。我们后来通过人工评估验证了主要模板的版本。基于模型的评估能走多远? 为了衡量我们奖励模型的鲁棒性,我们收集了一组安全性与有用性并存的提示测试性, 要求三位标注者根据7-point Likert方法值(越高越好)来判断答案的质量。由此,我们观察到我们的奖励模型总体上与人类偏好是对齐的,如下图(图29)。这证实了使用我们的奖励作为逐点指标的相关性,尽管我们接受了成对排名损失的训练。尽管如此,正如Goodhart定律所指出的,当措施变成目标时,它就不再是一个好的措施。为了保证我们的措施与人类偏好没有偏离,我们还用了一种更通用的奖励,在不同的开源奖励模型集进行训练。我们还没观察到任何的偏离,因此我我们假设迭代模型更新可能有助于防止这种情况的发生。最后一个验证步骤,是确保我们的新模型与前一个模型之间不存在回归, 我们在下一个标注迭代中使用这两个模型进行采样。这样我们可以在新提示上“免费"进行模型比较,还有助于增加采样的多样性。模型进展。 下图(图11) 展示了我们针对SFT和RLHF不同版本版本的进展,该图示是由我们内部的安全性与有用性的奖励模型测量所得。在这些评估集中,RLHF-V3后我们在SFT和RLHF上都胜过了ChatGPT(无害性与有用性都超过了50%)。尽管前面提到了使用我们奖励作为逐点衡量标准(point-wise metric)的相关性,但可以说,它可能偏向于Llama 2-Chat。因此为了比较的公平性,我们使用GPT-4额外计算最终结果,用于评估模型优劣。ChatGPT和Llama 2-Chat的输出顺序 GPT-4的提示为了避免任何偏差,ChatGPT和Llama 2-Chat输出在GPT-4提示下出现的顺序是随机且交替的,这样可以避免任何偏差。正如所预期的那样,尽管我们最新的Llama 2-Chat获得了超过60%的胜率,但支持Llama 2-Chat的胜率并不那么明显。 这个提示分别对应于1586和584个安全性和有用性验证集。Human Evaluation人类评估通常被视为评判自然语言生成模型(包括对话类模型)的黄金标准。 为了评估主要模型版本的质量,我们要求人类评估员对其安全性与有效性进行评分。我们采用超过4000个单轮或多轮的提示就Llama 2-Chat与其他开源的模型(Falcon, MPT MosaicML NLP Team et al. (2023), Vicuna Chiang et al. (2023), as well as closed-source models (ChatGPT (OpenAI, 2023) and PaLM Anil et al. (2023))做了比较,其中ChatGPT我们使用了gpt-3.5-turbo-0301型号的模板, PaLM我们使用了chat-bison-001型号的模型,每一个模板的人类评估所使用的提示数量见表Table 32. 更多的理论细节见附录的A.3.7章节。下面的章节,我们只展示有用性结果,而安全性相关的结构将在安全性的章节中阐述(Section 4.4).结论。 从图12(上图)可以看出,Llama 2-Chat在单轮与多轮提示中都明显优于其他开源模型。特别是Llama 2-Chat 7B模型在提示上要超过MPT-7B-chat的60%。Llama 2-Chat 34B与同等尺寸的Vicuna-33B和Falcon 40B型号相比,总体胜率超过75%。最大参数的Llama 2-Chat模型与ChatGPT很具竞争力的。Llama 2-Chat 70B模型相对于ChatGPT的胜率36%,平局率为1.5%。 在我们的提示集中Lama 2-Chat 70B模型要PaLM bison Chat模型好很多。更多结果和分析见第A.3.7节。Inter-Rater Reliability (内部评测方法的可靠性,简称IRR)。在人类评估中,三个不同的标注为每一个模型生成比较提供独立的评估。从数据质量的角度来看,高IRR分值(接近1.0)常能被视为更好,然后,上下文很重要。高度主观的任务,比如评估LLM生成的总体有用性,通常比更客观的标记任务会有较低的IRR分值。针对这些任务的公共基准测试集相对要少,所以我们认为分享我们的分析结果有利于研究界。我们使用Gwet的AC1/2统计方法来评测IRR,因为我们发现他是不同测量场景中最稳定的指标。在使用7-point Likert方法评估模型有用性任务时,根据不同的模型,Gwet的AC1/2统计得分在 0.37 到 0.55之间变化。 在获胜负相近的模型比较(比如Llama 2-Chat-70B-chat和ChatGPT比较)中其分值处于低分区。而在胜负相差较明显的两个模型(比如: Llama 2-Chat-34b-chat和Falcon-40b-instruct比较中,其分值处于高分区。人类评估局限制。 尽管我们结果表示表明,Llama 2-Chat在人类评估方面与ChatGPT不相上下,但值得注意是事,人类评估有一些局限制:按学院和研究标准,我们的4k的提示词集已足够大了。然而,他并不覆盖查模型在真实使用的使用场景,后者需要更额外更多的用例。提示的多样性是影响结果的另外一个因素。例如,我们的提示集并不包含任何与编码或推理相关的提示。我们仅评估多轮对话的最后一轮的结果。 一个更有意思的评估是要求模型去完成一个任务,并对模型在整个多轮对话的整体体验进行评估。生成模型的人类评估本质上是主观的且有噪声的。因此,对不同提示词集合或不同指令的评估可能会导致不同的结果安全性安全预训练重要的是要了解预训练数据中的内容,这不仅可以提高透明度,还可以阐明潜在问题(如潜在的偏见)的根本原因。这将能告知(如果有的话)下游需要考虑哪些缓解措施,同时帮助指导适当的模型使用。在本节中,我们分析了预训练数据的语言分布、人类学特征和和有害内容。我们还展示了在现有安全基准上测试预训练模型的结果完成一次“负责任”的预训练需要采取哪些步骤。 对于训练中使用的每个数据集,我们遵循了Meta的标准隐私策略和法律审查流程。 在训练中,我们没有使用Meta用户的私有数据。我们排除来自某些已知包含大量个人信息的网站的数据。我们尽最大努力高效的训练模型,以减少预训练期间的碳足迹(第 2.2.1 节)。尽可能的共享Meta的模型以减少重复造轮子。也没有对数据集进行额外的过滤,这样可以使得Llama 2的适用于更多的任务(例如,它可以更好地用于仇恨言论分类),同时避免有时因过度清理而导致的意外统计数据擦除的可能。 更重要的是,这将使得Llama 2-Chat在安全微调时能用更少样本进行更有效地泛化。最后,在使用和后续开发Llama 2模型时应当非常谨慎,充分注意到安全性。人类表征:代词. 模型生成中的偏差可能是从训练数据本身继承而来的。 例如,Bailey等人的研究表明,在大量文本语料库中,"people"在上下文中常常与代表"男性"的单词相似。Ganesh等人研究表明 就公平性指标而言,模型在公平性指示上的表现,可能高度的依赖于模型训练时如何对待小规模人口群体(representing underrepresented demographic groups). 在英语训练语料库中,我们对最常见的英语代词的频率做了计算,见表9a. 我们观察到,比'SHE','He'这个代词在文档中的比例通常更高(过高),与此相似,在描述大小的模型预训练数据集的代词中,也有类似的现象。这可能意味着模型在预训练过程中对提到She代词的上下文学习较少,因此可能会更倾向于生成He相关的内容。人类表征:身份. 我们还通过 测量HolisticBias数据集中人口特征术语的使用率,分析了预训练数据中不同人口群体的身份指代词。我们对预训练语料库中每个术语的使用频率进行了计算。 当归纳总结为5维(宗教、性别、国籍、种族和民族以及性取向),见表9b中显示每个维度中排名前5的术语。在前5维术语中,我们删除了一些术语,例如"直"(注:性取向)、“白”和“黑”(注:肤色),因为这些术语在除了人类表征的其他领域也有着频繁的使用(例如,作为基本颜色术语)。我们还对列表进行重复数据删除,删除了和"性别"和"性与性取向"有关的术语。对于性别和性,虽然很少用到"她"这个代词,但是"女性"一词经常出现。这可能意味着,虽然有关"她"这个代词的上下文较少,但有关"女性"的评论更多,这可能反映了这些术语的语言标记性差异。对于性取向,排名前五的术语均与LGBTQ+ 的身份相关。对于国籍、种族和民族以及宗教,我们观察到这些数据更偏向于西方世界的文化。例如,69.4%的参考文献中提到了"美国"一词,"欧洲人"一词比其他种族和民族出现得更普遍,"基督教"是出现最多的宗教,其次是"天主教"和"犹太教"。数据毒性(Toxicity). 我们使用了在ToxiGen数据集上微调的使用HateBERT分类器构建语料库,测量预训练语料中英语部分的有害言论的出现概率。我们针对文档的每一行进行打分,平均后作为文档的分值。图13显示了从总体语料中随机抽样10%的条件下的打分分布情况,其中约0.2%文档得分>=0.5,这意味着预训练数据中存在少量有害内容。语言识别. 虽然我们的预训练数据主要是英语,但也包括少量其他语言的内容。表10显示了我们语料库中语言的分布(统计抽样子集为总语料库的0.005%)。我们使用fastText语言识别工具,并设置了0.5阈值的语言检测。以英语为主的训练语料库意味着该模型可能不适合在其他语言中使用预训练模型的安全基准评估。我们根据三个主流的自动化评估基准(Benchmarks) 评估离Llama 2 的安全能力,其中涉及大模型的预训练中的三个关键维度。Truthfulness, referring to whether a language model produces known falsehoods due to misconceptions or false beliefs. We employ TruthfulQA (Lin et al., 2021) to measure how well our LLMs can generate reliable outputs that agree with factuality and common sense.Toxicity, defined as the tendency of a language model to generate toxic, rude, adversarial, or implicitly hateful content. We choose ToxiGen (Hartvigsen et al., 2022) to measure the amount of generation of toxic language and hate speech across different groups.Bias, defined as how model generations reproduce existing stereotypical social biases. We use BOLD (Dhamala et al., 2021) to study how the sentiment in model generations may vary with demographic attributes.We compare the performance of Llama 2 with Llama 1 (Touvron et al., 2023), Falcon (Almazrouei et al., 2023), and MPT (MosaicML NLP Team et al., 2023) in Table 11. For decoding, we set temperature to 0.1 and use nucleus sampling (Holtzman et al., 2020) with top-p set to 0.9. For TruthfulQA, we present the percentage of generations that are both truthful and informative (the higher, the better). For ToxiGen, we present the percentage of generations that are deemed toxic by the metric (the lower, the better). Detailed descriptions of the benchmarks and metrics can be found in Appendix A.4.7. When compared to Llama 1-7B, Llama 2-7B demonstrates a 21.37% increase in truthfulness and informativeness and a 7.61% decrease in toxicity. We also observe an increase in toxicity in the pretrained 13B and 70B Llama 2, which may result from larger pretraining data or a different dataset mix. Some have postulated the existence of a relationship between pretraining dataset size and downstream model toxicity or bias (Bender et al., 2021b), but empirical work to validate this claim is still ongoing (Dodge et al., 2021; Smith and Williams, 2021; Tal et al., 2022), and further evidence from up-to-date models is still needed。In Appendix A.4.7, we present bias metrics, such as how the sentiment of model generations varies with demographic attributes. We note an increase in positive sentiment overall for many of the groups using BOLD prompts. More detailed results split by different demographic groups can be found in Appendix A.4.8。Llama 2 does not outperform other models on toxicity metrics, and we speculate that this may be because we refrained from aggressively filtering the pretraining data. Recall that leaving pretraining data unfiltered may enable base models tuned to perform well on more downstream tasks (including hate speech detection), and it carries less risk of accidentally filtering out some demographic groups. We observe that models trained from less aggressively filtered pretraining data also required fewer examples to achieve reasonable safety-alignment. We reiterate that this motivated choice does imply that additional safety mitigations should be applied before deployment of base Llama 2 models。Benchmarks give a summary view of model capabilities and behaviors that allow us to understand general patterns in the model, but they do not provide a fully comprehensive view of the impact the model may have on people or real-world outcomes; that would require study of end-to-end product deployments. Further testing and mitigation should be done to understand bias and other social issues for the specific context in which a system may be deployed. For this, it may be necessary to test beyond the groups available in the BOLD dataset (race, religion, and gender). As LLMs are integrated and deployed, we look forward to continuing research that will amplify their potential for positive impact on these important social issues。结论在这项研究中,我们介绍了Llama 2系列模型的预训练和微调模型,其规模从70亿到700亿个参数不等。这些模型已经证明了它们与现有的开源语言模型相比的竞争力,并且在评估数据集上表现出了等同于某些专有模型(闭源模型)的能力,虽然Llama 2仍然落后于一些模型,如GPT-4。我们详细阐述了实现这些模型所应用的方法和技术,重点强调其同时具备有用性和安全性。为了更有意义地为社会做出贡献并促进研究,我们负有责任地开放了Llama 2和Llama 2-Chat的访问权限,作为持续致力于透明度和安全性的一部分,我们计划在未来的工作中进一步改进Llama 2-Chat。以下是笔者添加的总结Llama 2 总结沿用了Llama 1的设计与架构:RoPE、RMSNorm、SwiGLU+AdamWLlama 2采用了Llama 1中的大部分预训练设置和模型架构,包括标准Transformer架构、使用RMSNorm的预归一化、SwiGLU激活函数和旋转位置嵌入(RoPE)。采用AdamW 优化器进行训练,其中β1=0.9,β2=0.95,eps=10−5β_1= 0.9,β_2 = 0.95,eps = 10^{−5}β1=0.9,β2=0.95,eps=10−5。同时使用余弦(consin)学习率表, 预热2000 步,并最终将学习率衰减到了峰值学习率的10%。Meta在其研究超级集群(Research Super Cluster, RSC)以及内部生产集群上都对模型进行了预训练。Llama 2的预训练与微调Llama 2的数据量比Llama 1多了40%,上下文长度增加了一倍(上下文长度高达 4096)。Llama 2模型是在2万亿个标记上进行训练的,Llama-2-chat模型还额外训练了超过100万个新的人类标注。上下文长度高达4096(是 Llama 1的两倍)高质量SFTRLHF对齐(PPO+Rejection Sampling fine-tuning 近邻策略和拒绝采样微调):Llama-2-chat使用从人类反馈中进行的强化学习来确保安全和有用。Llama 2的安全性通过三个常用基准评估Llama 2的安全性:采用 TruthfulQA 基准评估真实性; 采用ToxiGen基准评估毒性,采用BOLD基准评估偏见。Meta在安全微调中使用监督安全微调、安全RLHF、安全上下文蒸馏。附录(待续)各种型号Llama2模型地址中文 Llama2-Chinese-7b-Chat: huggingface.co/FlagAlpha/L… Atom Atom-7B: huggingface.co/FlagAlpha/A… Atom-7B-Chat: huggingface.co/FlagAlpha/A… hf Llama-2-7b-hf: huggingface.co/meta-llama/… Llama-2-70b-hf: huggingface.co/meta-llama/… Llama-2-13b-hf: huggingface.co/meta-llama/… Llama-2-13b-chat-hf: huggingface.co/meta-llama/… Llama-2-70b-chat-hf: huggingface.co/meta-llama/… Llama-2-7b-chat-hf: huggingface.co/meta-llama/… base Llama-2-7b: huggingface.co/meta-llama/… Llama-2-13b: huggingface.co/meta-llama/… Llama-2-70b: huggingface.co/meta-llama/… char Llama-2-7b-chat: huggingface.co/meta-llama/… Llama-2-13b-chat: huggingface.co/meta-llama/… Llama-2-70b-chat: huggingface.co/meta-llama/…术语Red Teaming: : 红蓝对抗, 在大模型开发的诸多环节中,Red Teaming是一个关键过程,它通过模拟真实世界,来测试AI模型的潜在漏洞、偏见和弱点,以确认模型性能足够可靠。PPO(Proximal Polic Optimization),梯度策略的一种改进算法, 详细)RMS Norm(均方根误差)是一种常见的数学和统计学概念,用于衡量预测值和真实值之间的平均误差。Cosine Learning Rate Decay : 余弦学习率衰减MPT: MosaicML预训练模型系列Ghost Attention: Gatt - 最初的RLHF模型在几轮对话后忘记最初的指令,下图(左)所示。为了解决这些限制,提出Ghost Attention方法(Gatt,其实是一个训练trick)来增强模型对指令的遵从。bootstrap: bootstrap 是从数据样本中估算数量的一种强大的统计方法。例如平均数。你从数据中抽取大量样本,计算平均值,然后平均所有的平均值以便更好的估计真实的平均值,可看看(bagging)。温度参数(temperature parameter),在人工智能领领域,温度参数是指在生成式模型中使用的一种技术,可以用于控制生成结果的多样性和随机性HCI: 人机交互(HCI)是一个多学科的实践,注重于用户(人)和计算机之间的交互以及计算机交互界面的设计。Inter-Rater Reliability(IRR)系数,两名或更多的评分者之间对同一组受试者评分的可信度(variation)。
小乔学算法
AI的任务引导 - 提示工程
指令 + 背景 + 任务 + 输出引导例1 - 网红技术文章指令:帮我写一篇引爆'掘金'的技术文章背景:应对职业PUA要求:字数2000字以内,开头分析现状并提出问题,内容按序层层递进,结尾升华引起共鸣输出引导:请提供一篇符合上述要求的技术文章例2 - 演讲稿指令:帮我写一篇演讲稿背景:演讲主题是xxx,内容”总-分-总”结构。主要包含的内容要点是“..”要求:内容不超过1000字以内,对于具体的论证细节要求有对应的数据支持,以增加可信度输出引导:请提供一篇符合上述要求的演讲稿例3 - 减肥计划指令:如保进行科学减肥背景:我不太能管的住嘴。每天应该如何吃,如何运动,如何坚持?要求:身高xxx,体重xxx,预计3个月减到xx斤。目前的饮食习惯是xxx。输出引导:请提供一份完整的计划表给我,包括每日的饮食建议,每周运动建议和运动项目。让我能够科学的减肥。标签:ChatGPT
小乔学算法
机器学习之模型训练概念
Learning Rate - 学习率学习率(Learning Rate,LR)决定了模型参数的更新幅度,学习率越高,模型参数更新越激进,即相同 Loss 对模型参数产生的调整幅度越大,反之越越小。如果学习率太小,会导致网络loss下降非常慢;如果学习率太大,那么参数更新的幅度就非常大,产生振荡,导致网络收敛到局部最优点,或者 loss 不降反增。Batch SizeBatch size表示1次传递给模型用以训练的(样本)数据量。比如总共有1000个数据,如果设置batch_size=100,那么模型首次会使用数据集中的1~100个来训练模型,训练结束更新权重,再使用第101-200的个数据训练以此类推。Batch size越大,模型一次处理的数据量越大,能够更快的运行完一个Epoch, 反之运行完一个Epoch越慢。epoch:表示模型训练过程中数据集的完整遍历次数由于模型一次是根据一个Batch Size的数据计算Loss,然后更新模型参数,如果Batchsize过小,单个Batch 可能与整个数据的分布有较大差异,会带来较大的噪声,导致模型难以收敛。与此同时,Batch size 越大,模型单个 Step 加载的数据量越大,对于GPU显存的占用也越大,当 GPU 显存不够充足的情况下,较大的 Batch size 会导致 OOM,因此,需要针对实际的硬件情况,设置合理的 Batch size 取值。在合理范围内,更大的 Batch size 能够:• 提高内存利用率,提高并行化效率;• 一个 Epoch 所需的迭代次数变少,减少训练时间;• 梯度计算更加稳定,训练曲线更平滑,下降方向更准,能够取得更好的效果;对于传统模型,在较多场景中,较小的 Batch size 能够取得更好的模型性能;对于大模型,往往更大的 Batch size 能够取得更好的性能。激活函数激活函数用途线性函数是一次函数的别称,则非线性函数即函数图像不是一条直线的函数。非线性函数包括指数函数、幂函数、对数函数、多项式函数等等基本初等函数以及他们组成的复合函数。激活函数是多层神经网络的基础,保证多层网络不退化成线性网络。线性模型的表达能力不够,激活函数使得神经网络可以逼近其他的任何非线性函数,这样可以使得神经网络应用到更多非线性模型中。常见激活函数sigmoidsoftmax & tanhReLUSwish目前大模型在使用Swish。损失函数损失函数(loss function)就是用来度量模型的预测值f(x)与真实值Y的差异程度(损失值)的运算函数,它是一个非负实值函数。损失函数仅用于模型训练阶段,得到损失值后,通过反向传播来更新参数,从而降低预测值与真实值之间的损失值,从而提升模型性能。整个模型训练的过程,就是在通过不断更新参数,使得损失函数不断逼近全局最优点(全局最小值) 。不同类型的任务会定义不同的损失函数,例如回归任务重的MAE、MSE,分类任务中的交叉熵损失等损失函数分类MSE & MAE交叉熵损失 - 经典且常用交叉熵损失函数与激活函数之使用总结对于不同的分类任务,交叉熵损失函数使用不同的激活函数(sigmoid/softmax)获得概率输出:二分类使用sigmoid和softmax均可,注意在二分类中,Sigmoid函数,我们可以当作成它是对一个类别的“建模”,另一个相对的类别就直接通过1减去得到。而softmax函数,是对两个类别建模,同样的,得到两个类别的概率之和是1。单标签多分类交叉熵损失函数使用softmax获取概率输出(互斥输出)。多标签多分类交叉熵损失函数使用sigmoid获取概率输出。
小乔学算法
Transformer模型-6-Encoder
Encoder是6层结构,每层内部结构相同,都由Multi-Head Attention和Feed Forward组成,而这两层后都带有有一个Add&Norm层,Add&Norm层由 Add 和 Norm 两部分组成, 如下:输入Inputs见Transformer模型-4-Inputs-笔记多头注意力见Transformer模型-5-MultiHead Attention-笔记Add & NormAdd&Norm层由Add和Norm两部分组成,是Transformer的常有层,用于在多头自注意力机制和前馈神经网络之间添加残差连接和归一化操作。Add指X+MultiHeadAttention(X),是一种残差连接。Norm是Layer Normalization。这个层是将前一层的输出与前一层的输入相加,并进行归一化,以便更好地传递信息和控制梯度。其作用可以总结为在保持信息流畅性的同时,避免梯度消失或爆炸的问题,从而提高模型的训练效率和性能。Add&Norm层主要完成以下几件事情:残差连接:将前一层的输出与前一层的输入相加,得到一个残差向量。归一化:对残差向量进行归一化,以便更好地传递信息和控制梯度。归一化可以采用不同的方法,如Layer Normalization或Batch Normalization。线性变换:对归一化后的向量进行线性变换,以便更好地适应下一层的输入。Add什么是残差连接什么是残差连接呢?残差连接就是把网络的输入和输出相加,得到网络的输出为F(x)+xF(x)+xF(x)+x。分析在网络结构比较深的时候,网络梯度反向传播更新参数时,容易造成梯度消失的问题,但是如果每层的输出都加上一个x的时候,就变成了F(x)+x,对x求导结果为1,所以就相当于每一层求导时都加上了一个常数项'1',这样就有效解决了梯度消失问题。Transformer中的残差连接在Transformer中,数据过Attention层和FFN层后,都会经过一个Add & Norm处理。其中Add为residule block(残差模块) ,数据在这里进行residule connection(残差连接)残差连接的图表如下所示:Encoder架构图残差链接方式Add是一种残差连接,用于缓解梯度消失,这一概念在ResNet中被提出: Add可以让反向传播过程中,有一路的梯度不会经过梯度F(x) 计算(如上右图公式中的第一个F(x)),直接经过后续的处理(传播), 能够保存更多的梯度信息。有了Add(残差连接)我们可以将网络做的更深。Norm什么是NormalizationNorm即为Normalization(标准化)模块,就是把输入数据X,在输送给神经元之前先对其进行平移和伸缩变换,将X的分布规范化成在固定区间范围的标准分布,简单的说就是 将数据统一到固定区间内。变化框架Transformer中NormTransformer中采用的是Layer Normalization(层标准化)方式。Encoder架构图数学公式Add的结果经过LN进行层归一化:Feed Forword - 前馈神经网络什么是前馈神经网络前馈神经网络(Feedforward Neural Network, FNN) 是最早发明的简单人工神经网络。在前馈神经网络中,各神经元分别属于不同的层,每一层的神经元可以接收前一层神经元的信号,并产生信号输出到下一层。第0层称为输入层,最后一层称为输出层,其他中间层称为隐藏层。整个网络中无反馈,信号从输入层向输出层单向传播,可用一个有向无环图表示。Transformer中的前馈神经网络Feed Forward 层是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数:
小乔学算法
Transformer模型-1-概述、核心部件及应用场景
Transformer概述什么是TransformerTransformer模型是由谷歌公司提出的一种基于自注意力机制的神经网络模型,用于处理序列数据。相比于传统的循环神经网络模型,Transformer模型具有更好的并行性能和更短的训练时间,因此在自然语言处理领域中得到了广泛应用。《Attention Is All You Need》在自然语言处理中,序列数据的输入包括一系列文本、语音信号、图像或视频等。传统的循环神经网络(RNN)模型已经在这些任务中取得了很好的效果,但是该模型存在着两个主要问题:一是难以并行计算,二是难以捕捉长距离依赖关系。为了解决这些问题,Transformer模型应运而生。作为一种基于自注意力机制的神经网络模型,Transformer模型能够对序列中的每个元素进行全局建模,并在各个元素之间建立联系。与循环神经网络模型相比,Transformer模型具有更好的并行性能和更短的训练时间。Transformer核心部件Transformer模型中包含了多层encoder和decoder每一层都由多个注意力机制模块和前馈神经网络模块组成。encoder用于将输入序列编码成一个高维特征向量表示,decoder则用于将该向量表示解码成目标序列。在Transformer模型中,还使用了残差连接和层归一化等技术来加速模型收敛和提高模型性能。Transformer模型的核心是自注意力机制(Self-Attention Mechanism)其作用是为每个输入序列中的每个位置分配一个权重,然后将这些加权的位置向量作为输出。自注意力机制的计算过程包括三个步骤:计算注意力权重:计算每个位置与其他位置之间的注意力权重,即每个位置对其他位置的重要性。计算加权和:将每个位置向量与注意力权重相乘,然后将它们相加,得到加权和向量。线性变换:对加权和向量进行线性变换,得到最终的输出向量。通过不断堆叠多个自注意力层和前馈神经网络层,可以构建出Transformer模型。对于Transformer模型的训练通常采用无监督的方式进行预训练,然后再进行有监督的微调。在预训练过程中,通常采用自编码器或者掩码语言模型等方式进行训练,目标是学习输入序列的表示。在微调过程中,通常采用有监督的方式进行训练,例如在机器翻译任务中,使用平行语料进行训练,目标是学习将输入序列映射到目标序列的映射关系。Transformer模型应用领域Transformer模型是一种基于注意力机制的神经网络架构,最初被提出用于自然语言处理任务中的序列到序列学习。随着时间的推移,Transformer模型被应用于各种不同的领域,如下所示:自然语言处理自然语言处理是指将人类语言转换为计算机可以理解的形式,以便计算机能够处理和理解语言。Transformer模型在自然语言处理领域有许多应用案例。以下是一些例子:文本分类:Transformer模型可以对文本进行分类,例如将电子邮件分类为垃圾邮件或非垃圾邮件。在这种情况下,Transformer模型可以将文本作为输入,然后输出类别标签。机器翻译:Transformer模型可以将一种语言的文本翻译成另一种语言的文本。在这种情况下,Transformer模型可以将源语言的文本作为输入,然后输出目标语言的文本。命名实体识别:Transformer模型可以识别文本中的命名实体,例如人名、地名、组织名称等。在这种情况下,Transformer模型可以将文本作为输入,然后输出命名实体的类型和位置。情感分析:Transformer模型可以对文本进行情感分析,例如判断一篇文章是积极的还是消极的。在这种情况下,Transformer模型可以将文本作为输入,然后输出情感极性。语音识别语音识别是指将人类语音转换为计算机可以理解的形式,以便计算机能够处理和理解语音。一些最新的研究表明,基于Transformer的语音识别系统已经取得了与传统的循环神经网络(RNN)和卷积神经网络(CNN)相媲美的性能。下面是一些Transformer模型在语音识别领域的应用案例:语音识别:Transformer模型可以对语音信号进行识别,例如将语音转换为文本。在这种情况下,Transformer模型可以将语音信号作为输入,然后输出文本结果。语音合成:Transformer模型可以将文本转换为语音信号。在这种情况下,Transformer模型可以将文本作为输入,然后输出语音信号。说话人识别:Transformer模型可以识别不同说话者的语音信号。在这种情况下,Transformer模型可以将语音信号作为输入,然后输出说话者的身份。声纹识别:Transformer模型可以对声音信号进行识别,例如将声音转换为特征向量。在这种情况下,Transformer模型可以将声音信号作为输入,然后输出特征向量。这些应用案例只是Transformer模型在语音识别领域中的一部分应用。由于Transformer模型具有处理变长序列数据的能力和更好的性能,因此在语音识别领域中得到了广泛的应用。计算机视觉计算机视觉是指让计算机理解和分析图像和视频。Transformer模型在计算机视觉领域也有广泛应用。以下是一些例子:图像分类:Transformer模型可以对图像进行分类,例如将图像分类为不同的物体或场景。在这种情况下,Transformer模型可以将图像作为输入,然后输出类别标签。目标检测:Transformer模型可以检测图像中的物体,并将它们分割出来。在这种情况下,Transformer模型可以将图像作为输入,然后输出物体的位置和大小。图像生成:Transformer模型可以生成新的图像,例如生成一张艺术作品或者修改一张图像。在这种情况下,Transformer模型可以将图像作为输入,然后输出新的图像。这些应用案例只是Transformer模型在计算机视觉领域中的一部分应用。由于Transformer模型具有处理变长序列数据的能力和更好的性能,因此在计算机视觉领域中得到了广泛的应用。强化学习Transformer模型在强化学习领域的应用主要是应用于策略学习和值函数近似。强化学习是指让机器在与环境互动的过程中,通过试错来学习最优的行为策略。在强化学习中,模型需要通过学习状态转移概率,来预测下一个状态和奖励,从而实现增强学习。Transformer模型可以通过多头注意力机制来处理多个输入序列,并将它们融合成一个输出序列。在强化学习中,Transformer模型可以将当前状态作为输入,然后输出一个行动策略。具体而言,Transformer模型可以学习到状态转移概率函数,使得在当前状态下,选择行动后可以获得最大的奖励。Transformer模型还可以用于值函数近似。值函数是指在给定状态下,执行一个特定行动所能获得的期望奖励。在强化学习中,值函数通常是通过蒙特卡罗方法来估计的。而Transformer模型可以通过学习值函数来近似这些值,从而提高强化学习的效率和精度。Transformer模型已经被广泛应用于自然语言处理、语音识别、计算机视觉和强化学习等领域,并且在这些领域中都取得了显著的成果。它的广泛应用前景表明,Transformer模型在未来的人工智能领域中将扮演着越来越重要的角色。总体来说,Transformer模型是一种高效、灵活、易于实现的神经网络模型,其在自然语言处理领域中发挥着越来越重要的作用。随着深度学习技术的不断发展,Transformer模型必将在未来的自然语言处理领域中发挥越来越重要的作用。Transformer模型的优缺点Transformer模型的优点更好的并行性能:Transformer模型能够在所有位置同时计算,从而充分利用GPU并行计算的优势,加速了模型的训练和推理过程。能够处理长序列:传统的循环神经网络模型在处理长序列时容易出现梯度消失和梯度爆炸的问题,而Transformer模型使用了自注意力机制,能够同时考虑所有位置的信息,从而更好地处理长序列。更好的性能表现:Transformer模型在自然语言处理领域中已经取得了很多重要的研究成果,比如在机器翻译、文本生成、语言模型等任务中都取得了很好的效果。Transformer模型的缺点对于小数据集,Transformer模型的表现可能会不如传统的循环神经网络模型,因为它需要更大的数据集来训练。Transformer模型的计算复杂度较高,需要更多的计算资源,比如GPU等。Transformer模型的可解释性不如传统的循环神经网络模型,因为它使用了自注意力机制,难以解释每个位置的重要性。笔者学习笔记记录[参考]aistudio.baidu.com/projectdeta…
小乔学算法
大语言模型之ICL(上下文学习) - In-Context Learning Creates Tas
本文译自 《In-Context Learning Creates Task Vectors》 —— 论文中的作者也在用LLaMA模型,笔者自我感觉拉近和世界顶级人才的距离,哈哈 内容较长,如想看结论直接看 摘要、介绍与结论几个章节即可,看细节请看目录索引。 经验风险最小化 (Empirical Risk Minimization ERM): 这也是理论... 水平有限,敬请勘误摘要在大语言模型(LLMs)中的上下文学习(In-Context Learning,ICL) 成为一种强大的新学习范式(learning paradigm),然而我们对它的底层机制仍不够明确清晰。尤其是将其映射到传统的机器学习框架 就很具挑战性,其中我们使用 训练集S 在特定的假设类别中去寻找一个最佳拟合 函数f(x) 。我们发现,ICL可以学习到的函数通常具有非常简单的结构:他们直接表现近似于Transformer架构的LLMs,仅有的输入是 查询x 和 由训练集计算而得的单个'任务向量(task vector)', 因此 ICL可以看成是将 训练集S 压缩成一个单个任务向量(task vector) θ(S),然后利用该任务向量来调控Transformer以生成输出。为了验证上述观点,我们进行了一系列的综合实验,涵盖各种模型和任务。原始信息论文:In-Context Learning Creates Task Vectors作者:Roee Hendel(Tel Aviv University), Mor Geva(Google DeepMind), Amir Globerson(Tel Aviv University, Google)地址:arxiv.org/pdf/2310.15…代码:github.com,atroeehendel/icl_task_ve….介绍什么是In Context Learning (ICL)近年为大模型飞速发展,它的显著特点是可以从少量的示例集合(demonstrations)中就学到新规则。例如,我们向模型输入苹果->红色, 青柠->绿色 , 玉米 -> 就得到玉米对应的黄色输出。上述过程至少涉及LLM的'ICL'与'Promot'的两大主题。 好像整篇就上述这段话有用,其他用途不大的感觉啊,太理论了,可花了时间不啥得删啊。上述例子中模型仅基于两个例子就可学会了目标映射关系,这种能力我们称之为上下文学习 InContext Learning (ICL)。 ICL已经被广泛应用且效果显著。ICL如此神奇,人们开始探寻ICL背后潜在的机制,即模式内部是实现通过 示例集S 和查询 x 来生成所需要的输出?Figure 1: ICL as learning in a Hypothesis Class(是ICL在假设类中的学习过程)我们通过使用上图所示方法来处理该问题。在ICL中,我们给LLM一个含有特定任务的示例集S 提示(prompt) 和一个查询x,这个模型为 查询x 产生了输出, 如该示例中的输出'Yellow'。我们发现其内部的处理过程可以分解为两个部分(如上图所示): 第一部分是学习算法(learning algorithm) ', 用于计算 未知查询向量θ(S)θ(S)θ(S),该学习算法我们称之为 在假设类中函数参数,上图中的蓝色部分。第二部分是由θ定义的规则在查询x上的应用,我们用fff表示,该规则不直接依赖于 示例集’S', 如上图所示的黄色区域。ICL的预测函数ICL的预测函数是T([S,x])T([S, x])T([S,x]) , 其中T是自回归的语言模型(auto-regressive transformer), S表示用作ICL输入的训练示例集,x是查询参数, ICL根据输入x得到最终输出。而[S, x]表示为ICL对x和S串联后的输出。因此,在一般情况下,该预测函数可以是对S和x进行运算以产生输出的任意函数,这包括"非参数(non-parametric)"方法,诸如 最近邻法(nearest-neighbor)。ICL解决了什么问题来自统计学习理论的假定类概念。 在学习理论的表示中,通常我们将假定类看成H,H的每个元素都是函数H(x;θ)H(x;θ)H(x;θ), 表示为对输入x进行参数为向量θ 运算。 例如,如果x∈Rdx ∈ R^dx∈Rd ,那么假定类H 就是线性分类器(linear classifier)的集合, h(x;θ)=θ⋅xh(x; θ) = θ·xh(x;θ)=θ⋅x, θ为系数向量,输入为输入。学习算法在探索一个元素h, 且 h∈Hh ∈ H h∈H,该h可以更好的适应训练集,也就是所所谓的 经验风险最小化(Empirical Risk Minimization ERM)。ICL是否以这种方式执执目前并不十分清楚,最近已有机构正在探寻该问题。例如:我们从头开始训练一个语言模型(Transformer)并在上下文中以线性回归方法执行, 这种新兴的学习方法类似于梯度下降法(Stochastic Gradient Descent SGD)。 然而对于要执行更多复杂任务的自然语言任务的LLMs来说,其假设空间可能是什么还不是特别明确。在本论文中,我们证实了,在许多任务中,LLM的ICL都可以工作在假设空间中。给定一个训练集S,模型将其映射为任务向量θ(S),该向量表示为训练集S中映射/规则的描述。即给定模型T和一个向量θ,我们可以构造出一个用于完成指定任务的新函数f(x;θ)f(x; θ)f(x;θ)。该函数f近似于原始模型,直接应用于输入x,无需示例集合直接由θθθ激活, 如下图。Figure 2: Separating A and f. (分离A和f) 该图在文章的讲到具体章节时还贴了一张, 主要是为了查看方便,在此多贴一张我们的观点也与软提示有关,因为这两种方法都会针对特定任务调整转换器的功能。然而,在ICL中,任务向量是在前向传播中计算的,而不是经过微调。论文贡献我们的贡献包括:我们提出一种基于假设类的ICL机制, 并利用公开可用的大模型进行了一系列的不同任务试验以此来验证我们观点可靠性我们的研究进一步加深了对ICL的理解,可能对LLM执行特定任务的具有实际意义。ICL框架ICL的假设空间观点 - A Hypothesis Class View of ICL受学习理论的假设类观点的启动, 我们的主要目标是理解ICl是否将一个示例集S映射到一个关于输入x(Query x)的函数及该映射是如何产生的。我们特别探寻了ICL是否将 示例集S 转化为 一个θ —— 某个特定假设空间内函数的"参数"。实验结果的确证明了 ICL是运行在假设空间上的。理论框架 - Theoretical Framework假设类 - A Proposed Hypothesis Class如上图(Figure 2)所示框架,根据A和f的不同选择,假设类会有许多可能的实现。我们将描述重点在以Transfomer框架为基础的实现上。首先我们以(Figure 1)所示的方式来设置ICL, 其中输入一个x(i.e., Corn)外加一个 → 符号。 学习过程我们分为两个部分:基于训练集S的参数向量x,并将由该参数向量定义规则应用于查询x。前L层计算得到的 A 和 → 符号负责更新参数向量 θ ,然后用参数向量 θ 和查询x作为剩下的层的输入并产生输出。上上图(Figure 1).解决示例集S和查询x 在transformer中的任务层都可见的问题.任务与模型 - Tasks and Models任务:我们一共准备了18项目任务,这些任务一共分为4类:算法、翻译、语言和知识。 为了简单起来,我们限制其为单个token输出。 上表1展示了这些任务中有代表性的任务情况。更多的试验数据见论文原文模型:我们使用了多个大语言模型: LLaMA 7B, 13B, and 30B(Touvron et al., 2023), GPT-J 6B (Wang and Komatsuzaki, 2021), and Pythia a 2.8B, 6.9B, and 12B (Biderman et al., 2023)。探寻L层 - Finding L在第二章节我们在描述其内部机制时,提到了一个自由参数 —— L层,该层作为A的结束与f的开始。我们使用用(A,f)(A, f)(A,f)实现对L的不同选择,并通过评估以找到最佳层数。更多的显示见论文原文。图3展示了不同参数的LLaMA模型上,针对L层的不同选择其开发集的准确度。有趣的是,所有的模型在相似的中间层都展示了一个相似的性能峰值,无关模型的参数与层数的多少。基于假设的预测的准确度 - Accuracy of Hypothesis Based Prediction上图显示了每个模型在这3个过程中所有任务的平均精度。完整结果原论文更详细的数据分析及其A.2-表6数据。一切结果表示,我们提出 对A和f的分离为ICL提供了更好的执行过程。任务向量的鲁棒性 - Robustness of Task Vectors在我们的设置场景下,θ是来自于 示例集S 和 虚拟x'(dummy query x′)。 检查θ对输入变量的鲁棒性(稳定性)是一个必要事情。正常情况下,如果他表示任务,他应该在不同的S与x′值间保持稳定。为了做上述鲁棒性的测试,我们使用了LLaMA 7B的模型为每一个任务生成50个不同的S和x′的任务向量, 并且进行了如下分析。Geometry of θFigure 5是一个任务向量的t-SNE图, A t-SNE降维图 展示了任务向量形成不同的簇,每个簇包含单个任务的任务向量。论文中的图9将进一步显示了相同类别的任务间的接近性。Variability of θ下图是一个展示任务内部及任务间的距离的直方图。 可以看出同一个任务内与不同任务间的距离更靠近一些。这表明θ在任务中是稳定的,不受x′或S的高度影响。θ补丁的优势 - Dominance of θ Patching在第三章节,我们讨论了阻止f直接访问S示例集。然后,在ICL期间一个常规的前向传播过程,最后一个token是可以关注到S的。 这里我们验证了这种情况的存在, f主要使用任务向量θ且不直接访问示例集S。 最后我们使用了一对名为A和B的任务,他们共享了输入空间但有不同的输出。我们首先使用了“Regular"的前向传播,其中我们为模型提供了任务A的示例集S(我们把它表示为SA), 以验证模型可以使用ICl执行该任务。然后我们又进行了"Conflicting"的前向传播, 仍然是SA作为模型任务的数据集, 同时注入θ。For more details, refer to Fig. 6 in §A.1.上表2, 这个"Regular"的前向传播中在任务A中表现了很高的精度,然而这个“Conflicting”的前向传播产在任务B中产生了高精度,该任务对应于注入了向量θ。这意味道着这个任务主要依赖于θ,而忽略了为任务A的示例集S。 我们注意到任务B的准确度较低,可能与图6(Figure 6)的性能下降有关,可能进一步受到S存在的影响。对θ的解析 - Interpreting θ学习到了向量θ直接观地捉了关于示例集S所展示的任务信息。这里我们提供了支持这一解析的证明数据。由于向量θ是transformer的中间隐藏状态,我们可以使用词汇投影法(vocabulary projection method,nostalgebraist,2020;Dar et al. ,2022) 。即,我们检查由隐藏状态引起的分布在词汇表上的顶层token。下表展示了 LLsMA 13B下三个任务的顶层token.更多的请看 论文附 A 中的表7.在多种情况下,我们观察到能直接描述任务的token。而更重要的是,这些术语从未明确出现在上下文中。例如,在从法译英的任务中,我们观察到诸如“英语”和“翻译”之类的token。这支持了我们的观点,即θ携带了关于任务的重要、非琐碎的语义信息(θ carries significant, non-trivial semantic information about the task)。结论 Conclusions本文通过对LLM中ICl的探索,我们为ICL学习机制的供了新的视角。 我们展示了一个简单而优雅的结构:ICL通过将一个给定的训练集压缩为一个单任务向量来发挥作用,用来指导transformer根据给定的查询x去成最优输出。我们的工作为LLM如何执行ICL过程提供了理论阐述,由此我们预测,未来的工作可能会侧重在任务向量如何构建以及如何使用他来评估输出上。术语中英对照参考见原文论文
小乔学算法
Python+PyTorch+Anaconda安装配置
主流配置:Anaconda+Pycharm+PyTorchPython如果之前没python,无需专门安装,conda在创建pytorch环境时会c一起安装。查看python版本python -v
#-
# Python 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)] on win32
Anaconda安装安装了Anaconda就将大部分的Pytorch功能安装了,同时安装了虚拟环境conda。 官网:www.anaconda.com/ 中文:anaconda.org.cn/ 仓库:repo.anaconda.com/ 所有版本的Anaconda: repo.anaconda.com/archive/下载 - for windows安装指南: anaconda.org.cn/anaconda/in… 下载: www.anaconda.com/download#do…安装笔者下载的版本是“Anaconda3-2023.09-0-Windows-x86_64.exe”, 点击安装即可(过程略)。从文件名看py对应的版本 : Anaconda3... 后的‘3’代表对应的python的大版本是3,Anaconda2... 则python2。验证搜索 Anaconda Powershell Prompt ,打开对话框,如果出现如下界面,则说明安装成功。(base) PS C:\Users\carmen-x13>
# base代表是 "基础环境",后面会用到conda虚拟环境conda可完成pytorch与python的多版本切换。conda随Anaconda一起安装。PyTorch安装显卡配置基本信息深度学习离不开显卡,现在一些tensflow、pytorch等都只支持NVIDA的显卡,但是否有显卡对于学习pytorch并没有影响,前者只是起到训练加速的作用。显卡配置主要涉及以下内容驱动CUDA ToolkitCUDA Toolkit 现在随着pytorch一起安装,需要手工检查的是CUDA驱动安装的情况检查显卡安装情况For windows :任务管理器 - 性能 -GPU 0 : 如果能正常显示型号,意味着显卡驱动正式安装了先可查看当前主机是硬件型号与查看GPU对应的型号其他查看本机显卡驱动名的方法: 通过“软件管理、鲁大师” 或 打开''设备管理器'': Intel(R) UHD Graphics 或任务管理器: 右上角 dxdiag再进入 www.geforce.cn/hardware/te…查看 driver版本后面在安装pytorch时选CUDA版本时用会出 运行如下命令-nvidia-smi CUDA对驱动版本有要求,CUDA9.x 只支持驱动版本>396.26升级驱动版本利用电脑管理,或进入英伟 达的驱动官网,下载对应的驱动pytorch安装进入Anaconda Powershell Prompt命令提示行创建环境conda create -n pytorch python=3.11.0
激活环境conda activate pytorch
查看当前环境已安装的包与pytorchpip list
pip list | findstr pytorch
没有,则需要安装pytorchpytorch配置官网:pytorch.org 源码:github.com/pytorch/pyt…1、访问官网首页,找到 "INSTALL PYTORCH" 楼层,根据以下信息选择对应的版本,上述图片选择如下PyTorch Build:选 StablePackage: 选conda(在windows推荐使用conda, linux推荐pip)Language: 选PythonCompute Platform:CUDA 12.2 Relase 与 download: docs.nvidia.com/cuda/cuda-t…2、打开Anaconda Powershell Prompts 窗口,输入上述图片中 Running this Common 对应的指令点 install previous versions of PyTorch 可看到看有的安装包#没有NVIDIA运行下方命令
#conda install pytorch torchvision torchaudio cpuonly -c pytorch
conda install pytorch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 cpuonly -c pytorch
# 如下条信息安装结束
# Downloading and Extracting Packages
# ................
#Preparing transaction: done
#Verifying transaction: done
#Executing transaction: done
# 如果之前就已安装,会提示'# All requested packages already installed'
3、查看安装情况pip list |findstr torch
# 如下信息说明安装上了
# torch 2.1.0
# torchaudio 2.1.0
# torchvision 0.16.0
也可手工下载 cudatoolkit-x.x.x.bz与pytorch-x.x.x-......bz 复制到初始安装Anaconda的安装目录 C:\Users\[用户]\anaconda3\pkgs下并解压,再运行。验证安装python # 进入python环凌晨
>> import torch # 如果该步骤没有报错(或不输出任何信息)说明安装成功
>> import torchvision
>> torch.cuda.is_available() # 如果返回True意味着torch是否可使用GPU,输出为False - 集成显卡 - ComputerPlatform选CPU时也是。
Flase
本地这装pytorchconda install --use-local xxx
进入pytorch.org,在首页的“INSTALL PYTORCH"中 点击 “ install previous versions of PyTorch”找到以前的版本,找到以下两个包对应的合适版本。用conda install --use-local xxx进行安装。Pycharm安装 (略)
小乔学算法
大模型之基准测试集(Benchmark)-给通义千问2.0做测评的10个权威测基准测评集
引言在今年(2023)云栖大会上,阿里云正式发布千亿级参数大模型通义千问2.0。据现场介绍,在10个权威测评中,通义千问2.0综合性能超过GPT-3.5,正在加速追赶GPT-4。以下是通义千问在MMLU、C-Eval、GSM8K、HumanEval、MATH等10个主流Benchmark测评集上的表现:上图可以看出通义千问2.0的得分整体超越META的Llama-2-70B,相比OpenAI的Chat-3.5是九胜一负,相比GPT-4则是四胜六负,与GPT-4的差距进一步缩小 (新闻来自新浪财经)。那么问题来了,上图中Benchmark测评集分别是什么?侧重点在哪些方面?基准测评集介绍CMMLUCMMLU是针对中国背景下的大型语言模型的知识和推理能力的评测,由MBZUAI、上海交通大学、微软亚洲研究院共同推出,包含67个主题,专门用于评估语言模型在中文语境下的知识和推理能力。CMMLU是一个涵盖自然科学、社会科学、工程和人文学科等多个学科的综合性中国基准。是国内两大权威评测之一。论文:CMMLU: Measuring massive multitask language understanding in Chinese数据、代码与最新榜单:github.com/haonan-li/C…MMLUMMLU(Massive Multitask Language Understanding,大规模多任务语言理解)是一个由Hendrycks等人在《Measuring Massive Multitask Language Understanding》中提出的新基准,旨在通过仅在零样本和少样本设置下评估模型来衡量预训练。官网: paperswithcode.com/dataset/mml…论文: MEASURING MASSIVE MULTITASK LANGUAGE UNDERSTANDING大模型排行榜: paperswithcode.com/sota/multi-…C-EvaC-Eval是由清华大学、上海交通大学和爱丁堡大学合作构建的综合性考试评测集,覆盖52个学科,是目前权威的中文AI大模型评测榜单之一。是国内两大权威评测之一。C-Eval是全面的中文基础模型评估套件,涵盖了52个不同学科的13948个多项选择题,分为四个难度级别。论文:C-Eval: A Multi-Level Multi-Discipline Chinese Evaluation Suite for Foundation Models官网:cevalbenchmark.com/网址:github.com/hkust-nlp/c…排行:浏览GSM8KGSM8K是由OpenAI发布的大模型数学推理能力评测基准。一个由8.5K高质量的语言多样化的小学数学单词问题组成的数据集(其中7.5K训练集,1K测试集)。这些问题都是由人类写手创造的。每个问题需要2-8步推理来求解,主要是使用基本的算术运算(+-/*)进行一连串的基本计算,以得出最终答案。GSM8K是两大知名数学推理基准之一,该项测试在2021年10月份发布,至今仍然是非常困难的一种测试基准。提出背景:像GPT-3这样的大型语言模型有许多令人印象深刻的技能,包括模仿许多写作风格的能力,以及广泛的事实知识。但GPT难以完成需要精确多步骤推理的任务,比如解决小学数学单词问题。为了匹配人类在复杂逻辑领域中的表现,OpenAI使用验证器在许多解决方案中选择了最好的GSM8K, 他们收集了新的GSM8K数据集来评估其方法,并发布该数据集以促进研究。论文:Training Verifiers to Solve Math Word Problems项目:github.com/openai/grad…博客:openai.com/research/so…Gaokao-BenchGAOKAO-bench是一个以中国中考试题为数据集,评估大型语言模型的语言理解和逻辑推理能力的评估框架,收集了2010-2022年全国高考卷的题目, 包含1781道选择题、218道填空题和812道解答题。同时评测分为两部分,自动化评测的客观题部分和依赖于专家打分的主观题部分,这两部分结果构成了最终的分数。所有过程的数据和结果都是公开的。官网:github.com/OpenLMLab/G…论文:Evaluating the Performance of Large Language Models on GAOKAO BenchmarkAGIEval微软发布的大模型基础能力评测基准,在2023年4月推出,主要评测大模型在人类认知和解决问题的一般能力,涵盖全球20种面向普通人类考生的官方、公共和高标准录取和资格考试,包含中英文数据。因此,该测试更加倾向于人类考试结果,涵盖了中英文。论文:AGIEval: A Human-Centric Benchmark for Evaluating Foundation Models数据:github.com/microsoft/A…MATHMATH 数学领域的推理和解决问题能力测试, 是UC Berkeley提出的一个用于评估机器学习模型的数学问题解决能力的数据集。MATH与GSM8K类似,但是包含了12500道高中数学竞赛题,每道题都有详细的步骤化解法,可用于教模型生成答案推导和解释。MATH数据集目前对现有模型仍非常具挑战性。MATH是两大知名数学推理基准之一。项目地址:github.com/hendrycks/m…论文:Measuring Mathematical Problem Solving With the MATH DatasetBBHBIG bench hard(BBH) 基准,通过选择大语言模型表现出比人类更差性能的具有挑战性的任务,专注于研究大语言模型目前无法解决的任务。BIG-bench Hard是BIG-bench的一个仅包含目前模型表现无法超过人类的任务子集。BIG-bench 是一个协作基准,旨在从各个方面调查现有的大语言模型。它包括204项任务,涵盖了广泛的主题,包括语言学、儿童发展、数学、常识推理、生物学、物理学、社会偏见、软件开发等。通过缩放模型大小,大语言模型甚至可以在BIG-bench上65%的任务中,在少样本设置下的平均人类表现论文:Challenging BIG-Bench Tasks and Whether Chain-of-Thought Can Solve Themgithub: github.com/suzgunmirac…HumanEval它用于测量从文档字符串合成程序的功能正确性。它由164个原始编程问题组成,评估语言理解、算法和简单数学,其中一些问题与简单的软件面试问题相当。论文: arxiv.org/abs/2107.03…github: github.com/openai/huma…MBPP该基准测试由大约1000个众包Python编程问题组成,旨在由入门级程序员解决,涵盖编程基础知识、标准库功能等。每个问题都由任务描述、代码解决方案和3个自动化测试用例组成。主要反映大模型的代码理解和生成任务能力。论文:Program Synthesis with Large Language Modelsgithub: github.com/.../mbpp附录榜单UC伯克利主导的「LLM排位赛」LMSYS Org是UC伯克利(University of California,Berkeley)的研究人员发起的一个大语言模型版排位赛!顾名思义,就是让一群大语言模型随机进行battle,并根据它们的Elo得分进行排名。官网:lmsys.org/projects/大语言模型的在线试用与评测:chat.lmsys.org/该排位赛使用MT-bench作为聊天机器人评估基准。创始人之一盛颖是之前爆火的、可以在单GPU上可以跑175B模型推理的系统FlexGen的一作,目前已获8k星,她是斯坦福大学计算机科学系的博士生。另外两位是Lianmin Zheng和Hao Zhang。AlpacaEvalgithub: github.com/tatsu-lab/a…榜单:Alpaca Eval LeaderboardOpenCompass官网:opencompass.org.cn榜单:opencompass.org.cn/leaderboard…MT-BenchMT-Bench是一个经过精心设计的基准测试,包含80个高质量的多轮问题。8个主要的类别:写作、角色扮演、提取、推理、数学、编程、知识I(科学技术工程数学)和知识II(人文社科)。其中,每个类别有10个多轮问题,总共160个问题。下图是LMSYS Org上的2023年榜单上的雷达图:项目说明如下:Writing - 写作Humanities - 人类行业Roleplay - 角色扮演STEM - 理工科任务Reasoning - 推理任务Extraction - 提取(蒸馏)Math - 数学任务Coding - 代码任务MathVistaMathVista由微软发布的全新多模态数学推理基准数据集,同时提供了一份涵盖 112 页的详细评测报告,专注于大型多模态模型的数学推理表现。这一基准测试对于目前最先进的模型,如 GPT-4V,来说也是一项挑战,显示了这些模型在多模态数学问题解决方面的局限性。论文:arxiv.org/abs/2310.02…项目:mathvista.github.io/HF数据集:huggingface.co/datasets/AI…数据可视化:mathvista.github.io/#visualizat…Leaderboard:mathvista.github.io/#leaderboar…评测综述的论文:大型语言模型评估综述论文:A Survey on Evaluation of Large Language Models欢迎提供更多的[参考]blog.csdn.net/qq_18846849…baijiahao.baidu.com/s?id=178244…zhuanlan.zhihu.com/p/643086466…opencompass.org.cn/ability
小乔学算法
PyTorch入门备忘-1- 用Dataset自建数据集-及Jupyter与Pycharm简易入门
随手手册,希望对初学者有用,更适用于主技术栈非python,偶尔需要用python和pytorch的JYM的入门速查。想从事AI方向的JYM,是绕不开Python和Pytorch,对于有经验的程序员来说,看如下三文,入门很是OK了,当然在此前需要看看python的基础语法、数据类型等,但对于有经验的程序员来说这费不用多少时间了,语法都类似。Python+PyTorch+Anaconda安装配置PyTorch + Anaconda3 + Pycharm 入门工程LLM工具:Pycharm、PythonConsole及Jupyter比较与Jupyter安装与入门 —— 看比较部分即可入门之后就到了具体学习,看视频 (文章底部有参考链接,并非广告,这个作者我也不认识,该文章与之相对是和者的学习总结)。 有了一遍视频学习之后,来一个速度手册, 便于快速恢复。 —— 这是笔者针对目前编程语言众多,各自占不同山头,程序员得知道多种语言技能的处理办法。笔者在这类文章里贴出的代码会尽可能提供全注解,便于复查时快速恢复记忆中的代码片段、py语法结构等。PyTorchPyTorch是一个开源的Python机器学习库,基于Torch,用于自然语言处理等应用程序。2017年1月,由Facebook人工智能研究院(FAIR)基于Torch推出了PyTorch。它是一个基于Python的可续计算包,提供两个高级功能: 具有强大的GPU加速的张量计算(如NumPy)。 包含自动求导系统的深度神经网络。用Dataset自建数据集Dataset:pytorch提供一种方式去获取数据及其方式。流程:先按规则组织数据集,再创建一个继承Dataset的类,来引用该数据集。有关如何组织数据集可参考“附-数据集”部分内容; 目前笔者在想的是如何与有无监督学习关联起来。数据处理介绍数据集从杂乱无章的数据到神经网络训练可用的数据之间需要经过Dataset和DataLoader两个过程:Dataset:提供一种方式去获取数据及labelDataLoader: 对各种数据进行打包成数据集并编号,比如送去神经网络之前要按batchsize对数据进行打包再送入, 或对数据进行压缩等,为后面的网络或数据训练等操作做准备用Jupyter查看dataset的帮助用Jupyter查看帮助-段落清晰#1. 启动
jupyter node
#2. 打开浏览器
http://localhost:8888/
3. 点击右侧 [new] --> Python3 (ipykernel)
4. 在浏览器的控制台里输入
Dataset ??
# 或 ```help(Dataset) ```
输出实战代码步骤1. 创建工程创建一个conda虚拟环境的工程,见PyTorch + Anaconda3 + Pycharm 入门工程步骤2. 准备数据将数据复制一工程下通过该数所集,我们知道:该数据是用于区别蚂蚁和蜜蜂的二分类数据集数据是按"文件夹即标签"的形式组织的以什么样的方式准备数据,见本章节的数据集步骤3. 代码安装图片依赖包pip install pillow
pip install Image
# 也可以用cv2: pip install opencv-python
图片依赖包的介绍 CV2和PIL opencv原视频因为网络问题采用了PIL,笔者试了安装上了 pip install opencv-python代码tip: 在编码过程中,如果需要对数据或路做测试,可以在pycharm中打开python console控制台操作,pycharm会根据输入自动显示对应类或包的属性方法也可以查看变量的返回值等,见 pycharm和Python常用操作 将鼠标放在pycharm的对应文件夹,用"CTRL+C”就可以将文件夹名称复制下来,配置数据路径时会用到
# 路径
import os.path
# 引入Dataset
from torch.utils.data import Dataset
# 图像操作库
from PIL import Image
# import cv2
# 继承:Dataset
class MyData(Dataset):
# 实始化函数
def __init__(self, root_dir, label_dir):
"""
根目录:root_dir
用目录标签:label_dir
"""
# 创建时给赋值的变量为全局变量 self.xxx
self.root_dir = root_dir
self.label_dir = label_dir
self.path = os.path.join(self.root_dir, self.label_dir)
self.img_path = os.listdir(self.path)
pass
def __getitem__(self, idx):
"""
返回一张图片
:param idx: 图片所有位置index 从0开始
:return: image对象与标签名
"""
# 得到文件名
img_name = self.img_path(idx)
img_item_path = os.path.join(self.root_dir, self.label_dir, img_name)
img = Image.open(img_item_path)
label = self.label_dir
return img, label
def __len__(self):
"""
数据集长度
:return: 长度
"""
return len(self.img_path)
if __name__ == '__main__':
# 指定数据文件夹
root_dir = "data/train"
# 标签文件夹
ant_label_dir = "ants"
bees_label_dir = "bees"
# 某一种签检集
ants_dataset = MyData(root_dir, ant_label_dir)
bees_dataset = MyData(root_dir, bees_label_dir)
# 标签合集 - 拼接数据集
train_data = ants_dataset + bees_dataset
# 打印数据集长度
print(len(train_data))
输出结果此种操作和java完全不同Pycharm和Python常用操作Dir与Helpdir()函数,能让我们知道工具箱以及工具箱中的分隔区有什么东西help()函数,能让我们知道每个工具是如何使用的,工具的使用方法dir(pytorch)
dir(pythorch.xxx)
help(Dataset)
常用操作得到图片相对地址注释快捷 CTRL + / 注释打开对就的帮助 将鼠标移入import 对应的包包, 按住CTRL点鼠标,会打开SummaryWriter的帮助文档Python控制台输入指令,右侧自动显示类的属性、方法或数据from PIL import Image
img_path = "data/train/ants/0013035.jpg"
img = Image.open(img_path)
img.show("my")
img.show()
dir_path = "data/train/ants"
import os
img_path_list = os.listdir(dir_path)
可以将整个程序块复制到控制台,模拟程序代码进行测试虚拟环境-CondaConda是运行在Windows、macOS和Linux上的开源软件包管理系统和环境管理系统。Anaconda 主流的开源Python发行平台, 安装Anacoda会自动安装condaMiniconda是一个免费的Conda最小安装程序。它是Anaconda的一个小型的引导版本,只包含conda、Python及它们所依赖的包和少量其他有用的包,比如pip、zlib和其他一些包。PY的虚拟环境起到不同项目包版本隔离等的作用,现主流的虚拟环境有:pipenvcondapoetryvirtualenv...python第三方库www.360doc.com/content/22/…常用库OpenCVOpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效 —— 由一系列C函数和少量 C++类构成,提供了Python、Ruby、MATLAB、java、cuda等语言的接口和机器学习的基础算法调用,实现了图像处理和计算机视觉方面的很多通用算法...(CV:Computer Vision,计算机视觉) 。blog.csdn.net/weixin_4110…安装的时候是 opencv_python,但在导入的时候采用 import cv2。# 方法一:直接安装
pip install opencv-python
# 方法二 -- 官网下载安装.whl文件
# https://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv
pip install opencv_python‑3.x.x‑cp36‑cp36m‑win_amd64.whl
# 方法三 -- conda环境安装:
conda install opencv-python
PILPythonWare公司提供了免费的图像处理工具包 PIL (Python Image Library), 该软件包提供了基本的图像处理功能,如:改变图像大小,旋转图像,图像格式转换,色场空间转换,图像增强,直方图处理,插值和滤波等等...blog.csdn.net/yohnyang/ar… Python图像处理库安装pip install pillow
pip install Image
使用from PIL import Image ##调用库
...
im = Image.open("E:\mywife.jpg") ##文件存在的路径
im.show()
PillowPillow是PIL的替代版本,PIL 软件包提供了基本的图像处理功能,如:改变图像大小,旋转图像,图像格式转换,色场空间转换,图像增强,直方图处理,插值和滤波等等。Pillow为了解决PIL的两个问题:不兼容setuptools 问题太多,更新太慢数据集总结三种组织方式:文件夹名即为标签名按训练所需图片与训练所需标签分类存储文件名即为标签名以下数据集展示的图片来自对教学的视频 规则也是笔者边学边记,如还有其他规则可以探讨文件夹名即为标签名文件夹内的内容文件夹展示形式图片文件夹人容标签文件夹内容按训练所需图片与训练所需标签分类存储将数据集用另外一种表示,直接指明文件夹对应的数据类型。适用于一种物种有较多数据类型的形式。文件夹形式图片文件夹标签文件夹标签内文件格式注意:图片文件夹中的文件名称与标签文件夹的名称有对应关系文件名即为标签名[参考]PyTorch深度学习快速入门教程 www.oschina.net/p/pil?hmsr=…blog.csdn.net/m0_56729804…blog.csdn.net/m0_51864191…
小乔学算法
机器学习之基础名词解释
在学习机器学习与深度学习之前、和AI相关的技术伙伴交流时,我们必须能听懂或看懂相关的名词:样本样本是指一条数据。为深度学习训练模型用的,可以是已标注的也可以是未标注的数据,来源可以是线上的也可以是线下的。特征特征是指:被观测对象中可测量特性,例如西瓜的颜色、瓜蒂、纹路、敲击声等;特征向量用一个 d 维向量表征一个样本的所有或部分特征;标签(label)/真实值样本特征对应的真实类型或者真实取值,即正确答案;数据集(dataset)多条样本组成的集合,是样本的集合。一般用于机器学习的数据集会分为:训练集、评估集、测试型。训练集(train)已指定用于模型训练的数据集;评估集(eval)用于在训练过程中周期性评估模型效果的数据集合;测试集(test)用于在训练完成后评估最终模型效果的数据集合;模型可以从数据中学习到的,可以实现特定功能/映射的函数;误差/损失样本真实值与预测值之间的误差;预测值样本输入模型后输出的结果;模型训练使用训练数据集对模型参数进行迭代更新的过程;模型收敛任意输入样本对应的预测结果与真实标签之间的误差稳定;模型评估使用测试数据和评估指标对训练完成的模型的效果进行评估的过程;模型推理/预测使用训练好的模型对数据进行预测的过程;模型部署使用服务加载训练好的模型,对外提供推理服务;
小乔学算法
Transformer模型-5-Multi-Head Attention
上图红色圈中的部分为 Multi-Head Attention,是由多个Self-Attention组成的,虽然Encoder与Decoder中都有Multi-Head Attention,但他们略有区别。Encoder block包含一个 Multi-Head Attention, 而Decoder block包含两个 Multi-Head Attention。Decoder block包含两个 Multi-Head Attention,其中第一层的多头注意力用到Masked,第二层其数据组成则是由 Encoder输出数据的3/4直接送入( Encoder输出的另外1/4数据入了Add&Norm层) 再加上 由Decoder的Outputs进入的经Token Embedding和Position Enbedding计算后得的向量,经过第一层多头注意力后的数据注意力Attention函数可以描述为将query和一组key-value对映射到输出,其中query、key、value和输出都是向量。 输出为value的加权和,其中分配给每个value的权重通过query与相应key的兼容函数来计算。—— 来自(www.yiyibooks.cn/yiyibooks/A…)QKV的理解在《Attention is all you need》论文中首次提出Transformer时构建了三个辅助向量QKV。而所谓QKV也就是Q(Query 查询),K(Key 键值),V(Value 实际的特征信息)。 他们本质上是代表了三个独立的矩阵,都是我们原本的序列X做了不同的线性变换之后的结果,都可以作为X的代表。简单的可理解成:什么是多头注意力所谓多头,是分别将线性的变换之后的QKV切分为H份,然后对每一份进行后续的self-attention操作。最后再连接并做线性回归产生输出。如下图:观察上图的多头注意力结构的中间的Scaled Dot-Product Attention(点积自注意力),我们可以把拆为理解为高维向量被拆分为H分低维向量,并在H个低维空间里求解各自的self-Attention。多头注意力的理解代码层面: 把原始的维度切成H份,假如h=8(切成8份),每份则为512/8=64。在每个64维做相关度(即相乘)计算原理层面: 把原来在一个高维空间里衡量一个文本的任意两个字之间的相关度,变成了在8维空间里去分别衡量任意两个字的相关度的变化工作原理QKV获取经过position embedding 与 word embedding 计算后得到的向量x进入Encoder,经过3次线性变化之后,得到QKV,如下图:获取每个字都形成512/H维度的信息一个完整的数据矩阵, 经过计算得到QKV之后,分别获取三个(batch_size)维向量矩阵(下图右侧第一列),再次水平切分矩阵为(seq_len * 512维的信息) 数据(如下图右侧第二列)按,按字按维度切成H份(如最右侧的第三列)。再然后,计算一个字的维度。即将一个字分成H分,每一份为512/H维信息,那么一个完整的字一共有512/H维度的信息(transformer中h=8)。在H维空间中矩阵拆变如下:[batch-size、seq-len、dim]
[batch_size, seq_len, h, dim/h]
---举例---
[1024, 5, 512 ]
[1024, 5, 8, 512/8]
在H个子维度里计算任意两个字的盯关度每个字的第1头和其他字的第一头分别相乘比如 "我“ 字,按h=8拆分,得64(512/8) 维的信息。接下来再把我字的第一个64维度的信息分别和其他字的第一个64维的信息进行向量相乘。如果相乘的结果越大代表两个向量相似度接高,越小两个向量的相似度越低。如下图,右侧说明一个序列的第1个字的64维分别与其他字的第一个64维的向量相乘(对于我要吃汉堡,一共是5个字,对应右侧的5个图,从左到向,从上一下:我 想 吃 汉 堡)。在H个不同的(512/H)维计算相关度经过上图的拆分,由原来在512的大的维度空间里计算相似度,变成了在H个不同的(512/H) 维的子空间里分别去计算任意两个字的相似(关)度。比如:原来只进行1次的512 * 512的向量相乘 现在变成进行8次的64 * 64这样的向量相乘,即把原来的高维空间映射成了8个不同的64维的子空间,在每字64维的子空间里,分别去衡量这一序列字词之任意两个字之间的相似度,进而提升模型的表达能力。再进行组合成为一个512维的矩阵当拆分计算完成即相乘之后,再连接并做线性回归产生输出, 形成如下右图所示的矩阵,第一个格子的相似关是Q∗KtQ* KtQ∗Kt,其他格子也是...即再聚合起来(线性变化)成为一个512维的矩阵。多头注意力公式自注意力-Self-Attention从多头注意力的结构可以看到由H份组成的"Scaled Dot-Product Attention",称为点积注意力层 或 自注意力(self-attention)。输入由query、dk 维的key和dv维的value的组成,用dk相除query、d_k 维的key和d_v维的value的组成,用\sqrt{d_k}相除query、dk 维的key和dv维的value的组成,用dk相除,然后应用一个softmax函数以获得值的权重。结构如下:上图是 Self-Attention 的结构,在计算的时候需要用到矩阵QKV。其中,Self-Attention接收的是输向量x组成的矩阵X,或者上一个Encoder block的输出。经过三次线性变化得到的QKV。Q与KtK^tKt经过MatMul,生成相似度矩阵。对相似度矩阵每个元素除以 dk\sqrt{d_k}dk ,其中dkd_kdk为K的维度大小。这个除法被称为Scale。 当dkd_kdk很大时,Q*KTK^TKT的乘法结果方法变大, 进行Scale可使方差变小,训练时梯度更新更稳定。Mask是个要选环节,在机器翻译等自然语言处理任务中经常使用的环节。在机器翻译等NLP场景中,每个样本句子的长短不同,对于句子结束之后的位置,无需参与相似度的计算,否则影响Softmax的计算结果。softmax是个激活函数,在没有Mask时,softmax只起到归一化的作用。自注意力机制将一个单词与句子中的所有单词联系起来,从而提取每个词的更多信息。注意力公式[参考]www.yiyibooks.cn/yiyibooks/A…luweikxy.gitbook.io/machine-lea…标签:深度学习LLM人工智能
小乔学算法
PyTorch + Anaconda3 + Pycharm 入门工程
安装 Python+PyTorch+Anaconda安装配置新建项目打开pycharm,New Project -> PurePython如果在Previously configured interpreter --> Interpreter的下拉框中是否存在"C:\Users[user]\anaconda3\envs\pytorch"
如果存在,选择该环境,点”创建“即可。如果不存在,则按如下方式创建pytorch环境pycharm中pytorch环境创建点上图中Previously configured interpreter --> Interpreterd右则的 “Add Interpreter"按钮,进入创建一个环境。选Conda Environment,并在右则的Location输入pytorch,点”OK“按钮完成环境创建导入pytorch包如果不正常导入,后续的一些操作会失败如果上图中不存在pytorch,点”+“号添加pytorch验证配置情况进入PythonConsole,输入 >> import torch # 如果该步骤没有报错(或不输出任何信息)说明安装成功
>> import torchvision
>> torch.cuda.is_available() # 如果返回True意味着torch是否可使用GPU,输出为False - 集成显卡 - ComputerPlatform选CPU时也是。
Flase
虚拟环境激活情况上图是激活的状态,如果没有激活或没有进入torch,则需要Anconda Powershell Promots下运行conda activate torch激活创建fist_demo.py文件 print("Start")
a = 'hello world'
b = 2019
c = a + str(b)
print(c)
为first_demo.py配置编译环境点"Edit Configrations"进入的对话框下拉框 中的文件指向first_demo,点 绿色三角按钮,运行。输出如下为正常参考:www.bilibili.com/video/BV1hE…
小乔学算法
TypeScript实现图的遍历
前言有一个图,我们想访问它的所有顶点,就称为图的遍历。遍历图有两种方法:广度优先搜索和深度有优先搜索。图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通。本文将详解图的两种遍历并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。写在前面本文重点讲解图遍历的实现,对图和图两种遍历方式的概念不了解的开发者请移步我的另外几篇文章。图的认识 | 深度优先搜索的理解与简单实现 | 广度优先搜索的理解与简单实现图遍历思想图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。完全探索一个顶点,表示我们已经查看了该顶点的每一条边,对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问的顶点列表中。为了保证算法的效率,务必访问每个顶点最多两次,连通图中每条边和顶点都会被访问到。广度优先搜索算法和深度优先搜索算法基本上是相同的,唯一不同之处在于待访问顶点列表的数据结构。深度优先搜索采用栈来存储待访问顶点广度优先搜索采用队列来存储待访问顶点我们用三种颜色描述各个顶点的访问状态。白色:标识这个顶点还没被访问灰色:标识这个顶点被访问过,但未被探索过黑色:标识这个顶点被访问过且被完全探索过我们需要实现一个辅助方法,用于初始化每个顶点的颜色。这个辅助方法实现也简单,参数传一个顶点列表,函数内部声明一个颜色对象,遍历顶点列表,将每个顶点的值作为颜色对象的key,颜色对象的value为白色。最后返回这个颜色对象。广度优先搜索接下来我们来分析下广度优先搜索如何实现。实现思路广度优先搜索算法会从指定的一个顶点开始遍历图,先访问其所有的临点,一层一层的访问。从一个顶点v开始进行广度优先搜索的实现思路如下:声明一个函数breadthFirstSearch,该函数接收三个参数:要进行遍历的图、开始顶点、回调函数获取参数图(graph)的所有顶点和邻接表,将获取到的顶点初始化为白色,声明一个变量(color)接收初始化后的顶点对象创建一个队列(queue),将开始顶点(startVertex)入队如果队列非空,则执行以下步骤当邻接表中所有的顶点都被标识为灰色后,标识u顶点已被完全探索,将其标识为黑色如果参数回调函数(callback)存在,则执行回调函数实现代码上面我们分析了广度优先搜索的实现思路,我们将上述思路转换为代码。/**
* 广度优先搜索
* @param graph 需要进行搜索的图
* @param startVertex 开始顶点
* @param callback 得到每个节点后的回调函数
*/
export const breadthFirstSearch = (graph: Graph, startVertex: string | number, callback: (val: string | number) => void): void => {
// 获取图的所有顶点
const vertices = graph.getVertices();
// 获取图的临接表
const adjList = graph.getAdjList();
// 将顶点进行初始化
const color = initializeColor(vertices);
// 实例化一个队列
const queue = new Queue();
// 将开始顶点入队
queue.enqueue(startVertex);
// 如果队列不为空就继续执行
while (!queue.isEmpty()) {
// 取出队列里存储的顶点u
const u = queue.dequeue();
// 获取取出顶点的临接表
const neighbors = <(string | number)[]>adjList.get(u);
// 将顶点列表里的u标识为已被访问但未被探索
color[u] = Colors.GERY;
// 遍历当前取出顶点的临接表
for (let i = 0; i < neighbors.length; i++) {
// 获取临接表中的每个顶点w
const w = neighbors[i];
// 如果w没被访问过
if (color[w] === Colors.WHITE) {
// 标识w为已被访问但未被探索
color[w] = Colors.GERY;
// 将w加入队列
queue.enqueue(w);
}
}
// 此时u顶点与其相邻顶点已经被探索,将u标识为已被访问且被完全探索
color[u] = Colors.BLACK;
// 执行回调函数
if (callback) {
callback(u);
}
}
};
完整代码请移步:Graph.ts编写测试代码接下来,我们通过一个例子来测试下上述代码是否正确执行。如下图所示,我们使用广度优先搜索访问图中的所有顶点。构建图并建立顶点与顶点之间的关系let graph = new Graph();
let vertices = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
for (let i = 0; i < vertices.length; i++) {
graph.addVertex(vertices[i]);
}
graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("A", "D");
graph.addEdge("C", "D");
graph.addEdge("C", "G");
graph.addEdge("D", "G");
graph.addEdge("D", "H");
graph.addEdge("B", "E");
graph.addEdge("B", "F");
graph.addEdge("E", "I");
创建回调函数,执行广度优先搜索函数const printVertices = (val) => {
console.log(val);
};
breadthFirstSearch(graph, vertices[0], printVertices);
求最短路径上面我们学习了广度优先搜索的基本原理,我们还可以用该算法做更多的事情,而不只是输出被访问节点的顺序。例如,给定一个图G和源顶点v,找出每个顶点u和v之间的最短路径的距离(以边的数量计)对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。所以,可以用广度优先算法来解决这个问题。我们修改上面实现的广度优先算法,让其返回如下信息:从v到u的距离distances[u]前溯点predecessors[u],用来推导出从v到其他每个顶点u的最短路径接下来我们来分析下如何修改算法来返回我们需要的信息。声明两个对象distances与predecessors分别用来存储距离和前溯点遍历所有顶点,将每个顶点的距离初始化为0,将每个顶点的前溯点初始化为null遍历邻接表时,发现顶点u的临点w时,通过给distances[u]的值+1来增加v和w之间的距离,设置w的前溯点值为u最后,返回distances对象和predecessors对象代码实现如下:/**
* 广度优先搜索优化
* @param graph 要进行遍历的图
* @param startVertex 开始顶点
* @constructor
*/
export const BFS = (
graph: Graph,
startVertex: string | number
): { distances: { [key: string]: string | number }; predecessors: { [key: string]: string | number | null } } => {
// 获取图的所有顶点
const vertices = <(string | number)[]>graph.getVertices();
// 获取图的临接表
const adjList = graph.getAdjList();
// 初始化顶点颜色
const color = initializeColor(vertices);
// 创建一个队列
const queue = new Queue();
// 存储每个顶点的距离
const distances: { [key: string]: string | number } = {};
// 存储前溯点
const predecessors: { [key: string]: string | null | number } = {};
// 顶点入队
queue.enqueue(startVertex);
// 遍历所有顶点
for (let i = 0; i < vertices.length; i++) {
// 用0来初始化每个顶点的距离
distances[vertices[i]] = 0;
// 用null来初始化每个顶点的前溯点
predecessors[vertices[i]] = null;
}
while (!queue.isEmpty()) {
// 获取队首顶点u
const u = queue.dequeue();
// 获取u的临接表
const neighbors = <(string | number)[]>adjList.get(u);
// u标识为已访问但未被探索状态
color[u] = Colors.GERY;
// 遍历u的临接表
for (let i = 0; i < neighbors.length; i++) {
// 获取临接表中遍历到的顶点w
const w = neighbors[i];
// 如果顶点w未被访问
if (color[w] === Colors.WHITE) {
// 标识顶点w为已访问但为被探索
color[w] = Colors.GERY;
// 给u顶点加1来增加v和w之间的距离(u是w的前溯点)
distances[w] = <number>distances[u] + 1;
// 发现顶点u的邻点w时,则设置w的前溯点值为u
predecessors[w] = u;
// w入栈
queue.enqueue(w);
}
}
}
return {
distances,
predecessors
};
};
接下来我们测试下,优化后的方法是否正常执行。const shortestPaths = BFS(graph, vertices[0]);
console.log(shortestPaths);
执行上述代码后,我们会得到如下图所示的结果。解释如下:# 距离
distances: { A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2, I: 3 }
顶点A到A的距离为0
顶点A到B的距离为1
顶点A到C的距离为1
顶点A到D的距离为1
顶点A到E的距离为2
顶点A到F的距离为2
顶点A到G的距离为2
顶点A到I的距离为3
# 前溯点
predecessors: { A: null, B: "A", C: "A", D: "A", E: "B", F: "B", G: "C", H: "D", I: "E" }
顶点A的前溯点为null
顶点B的前溯点为A
顶点C的前溯点为A
顶点D的前溯点为A
顶点E的前溯点为B
顶点F的前溯点为为B
顶点G的前溯点为C
顶点H的前溯点为D
顶点I的前溯点为E
上面我们拿到了A到每个顶点距离和每个顶点的前溯点,接下来我们就可以根据距离和前溯点来求最短距离了。遍历除过源顶点外的顶点列表将源顶点入栈声明变量s用于存储最短路径,依次取出栈中的元素,将其用-拼接打印s/**
通过前溯点列表获取顶点A到其他顶点的路径
*/
// 用顶点A作为源顶点
const fromVertex = vertices[0];
// 遍历除过源顶点外的顶点
for (let i = 1; i < vertices.length; i++) {
// 获取A抵达的顶点
const toVertex = vertices[i];
// 创建一个栈来存储路径值
const path = new Stack();
// 追溯toVertex到fromVertex的路径,变量v赋值为其前溯点的值
for (let v = toVertex; v !== fromVertex; v = shortestPaths.predecessors[v]) {
// v入栈
path.push(v);
}
// 源顶点入栈
path.push(fromVertex);
let s = path.pop();
while (!path.isEmpty()) {
s += " - " + path.pop();
}
console.log(s);
}
完整代码请移步: GraphTest.js执行结果如下深度优先搜索深度优先搜索算法将会从一个指定的顶点开始遍历图,沿着路径查找,直至这条这条路径的最后一个顶点被访问,接着原路回退并探索下一条路径。如下图所示实现思路深度优先搜索不需要一个源顶点,在深度优先算法中,若图中顶点v未访问,则访问该顶点v。要访问顶点v,实现思路如下。声明一个函数depthFirstSearch,该函数接收2个参数:要进行遍历的图、回调函数获取图(graph)的顶点以及临接表,将获取到的顶点初始化为白色,用一个变量color来存储初始化后的顶点遍历所有顶点,如果当前遍历到的顶点未被访问就递归访问其顶点递归访问顶点的实现思路如下。声明一个函数depthFirstSearchVisit,该函数接收4个参数:要访问的顶点、颜色对象、图的临接表、回调函数首先,将要访问的顶点u标识为已发现状态执行回调函数获取u的临接表,遍历临接表最后,遍历结束u已经被完全探索,将其标识为黑色。实现代码接下来我们将上述思路转换为代码。/**
* 深度优先搜索
* @param graph 要进行遍历的图
* @param callback 回调函数
*/
export const depthFirstSearch = (graph: Graph, callback: (val: string | number) => void): void => {
// 获取图的顶点
const vertices = graph.getVertices();
// 获取图的临接表
const adjList = graph.getAdjList();
// 初始化顶点
const color = initializeColor(vertices);
for (let i = 0; i < vertices.length; i++) {
// 如果当前顶点未被访问
if (color[vertices[i]] === Colors.WHITE) {
// 调用递归函数进行递归访问
depthFirstSearchVisit(vertices[i], color, adjList, callback);
}
}
};
/**
* 递归访问顶点
* @param u 要访问的顶点
* @param color 颜色对象
* @param adjList 图的临接表
* @param callback 回调函数
*/
const depthFirstSearchVisit = (
u: string | number,
color: { [p: string]: number },
adjList: Dictionary<string | number, (string | number)[]>,
callback: (val: string | number) => void
) => {
// 顶点u访问后,标识为已访问但未被探索状态
color[u] = Colors.GERY;
// 执行回调函数
if (callback) {
callback(u);
}
// 获取当前顶点的临接表
const neighbors = <string | number[]>adjList.get(u);
// 遍历临接表
for (let i = 0; i < neighbors.length; i++) {
// 获取临接表的每个顶点w
const w = neighbors[i];
// 如果w未被访问则以w为顶点进行递归访问
if (color[w] === Colors.WHITE) {
depthFirstSearchVisit(w, color, adjList, callback);
}
}
// u标识为已被完全探索
color[u] = Colors.BLACK;
};
完整代码请移步:Graph.ts编写测试代码接下来我们用广度优先搜索的例子来测试下上述代码是否都正确执行。console.log("深度优先搜索节点访问顺序");
depthFirstSearch(graph, printVertices);
拓扑排序上面我们学习了深度优先搜索的基本原理,我们可以用深度优先搜索做很多事情,而不只是输出顶点的被访问顺序。例如,给定一个图G,我们希望深度优先算法遍历图G的所有顶点,构建“森林”以及一组源顶点,并输出两个数组:发现时间和完成探索时间。我们修改深度优先搜索算法,让其实现返回以下信息。顶点u的发现时间d[u]当顶点u被标注为黑色时,u的完成探索时间f[u]顶点u的前溯点p[u]接下来我们分析下,如果通过修改代码让其返回我们需要的信息。声明3个对象d、f、p分别存储顶点的发现时间、完成探索时间、前溯点声明time对象,用于记录每个顶点的访问时间遍历顶点,将d、f、的每个顶点赋值为0,将p每个顶点的前溯点赋值为null修改递归访问函数。递归访问函数需要多传4个参数,d、f、p、time当顶点被发现时,记录当前顶点u的初次发现时间,获取time的时间进行+1当临接点w未被访问时存储w的前溯点顶点u被完全访问后记录顶点的完成访问时间代码实现如下/**
* 优化后的深度优先搜索
* @param graph 要进行搜索的图
* @constructor
*/
export const DFS = (
graph: Graph
): { predecessors: { [key: string]: string | number | null }; discovery: { [key: string]: string | number }; finished: { [key: string]: string | number } } => {
const vertices = <(number | string)[]>graph.getVertices();
const adjList = graph.getAdjList();
const color = initializeColor(vertices);
const d: { [key: string]: string | number } = {};
const f: { [key: string]: string | number } = {};
const p: { [key: string]: string | number | null } = {};
const time: { [key: string]: number } = { count: 0 };
for (let i = 0; i < vertices.length; i++) {
f[vertices[i]] = 0;
d[vertices[i]] = 0;
p[vertices[i]] = null;
}
for (let i = 0; i < vertices.length; i++) {
if (color[vertices[i]] === Colors.WHITE) {
// 从i顶点开始递归访问
DFSVisit(vertices[i], color, d, f, p, time, adjList);
}
}
return {
discovery: d,
finished: f,
predecessors: p
};
};
/**
* 递归访问顶点
* @param u 要访问的顶点
* @param color 颜色对象
* @param d 顶点初次发现时间
* @param f 顶点完成访问时间
* @param p 前溯点
* @param time 初始时间
* @param adjList 临接表
* @constructor
*/
const DFSVisit = (
u: string | number,
color: { [p: string]: number },
d: { [key: string]: string | number },
f: { [key: string]: string | number },
p: { [key: string]: string | number | null },
time: { [key: string]: number },
adjList: Dictionary<string | number, (string | number)[]>
) => {
// 顶点u被发现但未被探索
color[u] = Colors.GERY;
// 记录顶点u的初次发现时间
d[u] = ++time["count"];
// 获取顶点u的临接表
const neighbors = <(string | number)[]>adjList.get(u);
for (let i = 0; i < neighbors.length; i++) {
// 获取w临接点
const w = neighbors[i];
// 如果w未被访问,存储w的前溯点,以w未顶点继续递归访问
if (color[w] === Colors.WHITE) {
p[w] = u;
DFSVisit(w, color, d, f, p, time, adjList);
}
}
// 顶点被完全访问
color[u] = Colors.BLACK;
// 记录顶点完成访问时间
f[u] = ++time["count"];
};
接下来我们测试下优化后的方法是否能正常执行。console.log("优化后的深度优先搜索");
console.log(DFS(graph));
执行上述代码后,我们会得到如下图所示的结果。解释如下顶点A发现时间是1,完成访问的时间是18
顶点B的发现时间是2,完成访问的时间是9
顶点C的发现时间是10,完成访问的时间17
顶点D的发现时间是11,完成访问的时间是16
顶点E的发现时间是3,完成访问的时间6
顶点F的发现时间是7,完成访问的时间8
...
顶点I的发现时间是4,完成访问的时间是5
上述执行结果也可以用下图来描述我们通过一个例子来讲解拓扑排序,如下图所示,假定每个顶点都是一个需要去执行的任务。上图是一个有向图,意味着任务的执行是有顺序的。例如任务F不能在任务A之前执行,这个图没有环,意味着这张图是一个无环图,因此我们可以称上图为有向无环图。当我们优化深度优先搜索算法后,拿到了我们需要的数据,就可以根据这些数据,稍作调整就能完成拓扑排序了。构建一个有向图,将顶点依次加入图中建立每个顶点之间的连接,执行优化后的深度优先搜索算法,获取其返回的数据result获取result中的完成访问时间fTimes声明变量s,用于存储拓扑排序最终的路径遍历顶点列表打印拼接好的字符串s实现代码如下// 实现拓扑排序
graph = new Graph(true);
vertices = ["A", "B", "C", "D", "E", "F"];
for (let i = 0; i < vertices.length; i++) {
graph.addVertex(vertices[i]);
}
graph.addEdge("A", "C");
graph.addEdge("A", "D");
graph.addEdge("B", "D");
graph.addEdge("B", "E");
graph.addEdge("C", "F");
graph.addEdge("F", "E");
const result = DFS(graph);
console.log("拓扑排序");
const fTimes = result.finished;
let s = "";
for (let count = 0; count < vertices.length; count++) {
let max = 0;
let maxName = null;
for (let i = 0; i < vertices.length; i++) {
if (fTimes[vertices[i]] > max) {
max = fTimes[vertices[i]];
maxName = vertices[i];
}
}
s += maxName + " - ";
delete fTimes[maxName];
}
console.log(s);
完整代码请移步: GraphTest.js上述代码的执行结果如下图所示
小乔学算法
TypeScript实现图
前言图是一个非线性数据结构,本文将讲解图的基本运用,将图巧妙运用,并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。写在前面本文着重讲解图的实现思路,对图的基础概念不了解的开发者,请移步我的另一篇文章:图的认识。实现思路图是网络结构的抽象模型,图是由一组边连接的顶点。任何二元关系都可以用图来表示,例如:社交网络、道路、航班以及通信。基本概念一个图G = (V, E)由以下元素组成。V:一组顶点E:一组边,连接V中的顶点下图描述了一个图。通过上图我们来讲解下图的一些术语。相邻顶点,即由一条边连接在一起的顶点。如上图所示,A和B是相邻的,A和D是相邻的,A和C是相邻的,A和E不是相邻的。度,即一个顶点与其相邻顶点的数量,如上图所示,A和其他三个顶点相连接,因此A的度为3;E和其他两个顶点相连,因此E的度为2。路径,即顶点v1,v2,...,vn的一个连续序列,其中v1和vn+1是相邻的。如上图所示,包含路径A B E I和A C D G简单路径,即不包含重复的顶点。如上图所示,ADG就是一条简单路径,除去最后一个顶点,因为它与A是同一个顶点。环,它也是一个简单路径,如上图所示,A D C A,最后一个顶点重新回到A。有向图与无向图图可以是无向(没有方向)的或是有向(有向图)的。上面我们画的是无向图,下图描述了一个有向图。强连通,即图中每两个顶点间在双向上都存在路径。如上图所示,C和D就是强连通的,而A和B不是强联通的。加权,如果给图上每条边都标上权重,那么这个图就是一个加权图,否则就是不加权的,加权图如下所示。图的表示图可以用多种数据结构来表示,不存在绝对正确的方式。图的正确表示法取决于待解决的问题和图的类型。邻接矩阵图最常见的实现是邻接矩阵,每个节点都和一个种整数相关联,该整数将作为数组的索引。我们可以用一个二维数组来表示顶点之间的的连接。如果索引为i的节点和索引为j的节点相邻,则 array[i][j] = 1,否则 array[i][j] = 0,如下图所示不是强联通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻顶点,我们也不得不迭代一整行。邻接矩阵表示法不够好的一个理由是:图中顶点的数量可能会改变,而二维数组不太灵活。临接表我们可以使用临接表这种动态数据结构来表示图,临接表由图中每个顶点的相邻顶点列表所组成。我们可以使用数组、链表、散列表或字典来表示相邻顶点列表,如下图所示描述了临接表这种数据结构。临接表对大多数问题来说是比较好的选择,以上两种表示法都很有用,他们有着不同的性质(例如,要找出v和w是否相邻,使用邻接矩阵会比较快)。关联矩阵我们还可以使用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所示,使用二维数组来表示两者之间的连通性,如果顶点v是边e的入射点,则 array[v][e] = 1;否则, array[v][e] = 0。关联矩阵通常用于边的数量比顶点多的情况,以节省空间和内存。使用临接表实现图我们选用临接表来表示图,接下来我们来分析下如何来实现图。创建图所需的基础变量创建Grap类,构造器接收一个参数用于判断图是否有向,默认情况图是无向的。类内部,声明一个数组用来存储图中所有顶点的名字(vertices),声明一个字典来存储临接表(adjList)。字典会使用顶点的名字作为键,邻接顶点列表作为值。实现图所需的两种方法接下来我们需要实现两个方法:一个用来向图中添加一个新的顶点,另一个用来添加顶点之间的边。向图中添加顶点(addVertex)addVertex方法接收一个参数:要添加的顶点(v)首先,判断要添加的顶点是否在图(顶点列表)中如果不存在,将该顶点添加到顶点列表中在临接表中设置顶点v作为键,对应的字典值为一个空数组向图中添加边(addEdge)addEdge方法接收两个参数: 要进行连接的两个顶点(v,w)添加顶点前,验证要添加的两个顶点是否在图中,如果不存在则需要先调用addVertex方法将其添加到图中获取顶点v的临接表,将w添加进v的临接表中,这样我们就得到了一条来自顶点v到顶点w的边如果是无向图则需要添加一条自w到v的边实现图的获取方法上面我们实现了向图中插入值,我们还需要获取图中的值以及将图转换成比较友好的字符串。获取图的顶点列表(getVertices)直接返回vertices即可获取图的临接表(getAdjList)直接返回adjList即可将图转换为字符串(toString)首先,遍历图的所有顶点,将顶点的名字加入字符串中然后,获取当前遍历到顶点的临接表然后,遍历获取到的临接表,将临街表中的每个顶点加入到字符串中最后,临接表遍历完成后向字符串中添加一个换行符实现代码前面我们分析了图的实现思路,接下来我们将上述思路转换为代码。新建Graph.ts文件,创建类以及实现图所需要的变量。export default class Graph {
// 存储图的顶点
private vertices: (number | string)[] = [];
// 存储临接表
private adjList: Dictionary<string | number, (string | number)[]> = new Dictionary();
constructor(private isDirected: boolean = false) {}
}
实现添加顶点和添加边的方法 // 添加顶点
addVertex(v: string | number): void {
// 顶点不存在于图中
if (!this.vertices.includes(v)) {
// 将该顶点添加到顶点列表中
this.vertices.push(v);
// 在临接表中设置顶点v作为键,对应的字典值为一个空数组
this.adjList.set(v, []);
}
}
// 添加线,连接顶点
addEdge(v: string | number, w: string | number): void {
// 添加顶点之前需要验证顶点v和w是否在图中,不存在就追加
if (!this.adjList.get(v)) {
this.addVertex(v);
}
if (!this.adjList.get(w)) {
this.addVertex(w);
}
// 将w加入到v的临接表中,我们就得到了一条来自顶点v到顶点w的边
this.adjList.get(v)?.push(w);
if (!this.isDirected) {
// 如果是无向图则需要添加一条自w到v的边
this.adjList.get(w)?.push(v);
}
}
实现图的获取方法 // 获取顶点列表
getVertices(): (string | number)[] {
return this.vertices;
}
// 获取临接表
getAdjList(): Dictionary<string | number, (string | number)[]> {
return this.adjList;
}
// 将图转为字符串
toString(): string {
let s = "";
for (let i = 0; i < this.vertices.length; i++) {
s += `${this.vertices[i]} -> `;
const neighbors = <Array<string | number>>this.adjList.get(this.vertices[i]);
for (let j = 0; j < neighbors.length; j++) {
s += `${neighbors[j]} `;
}
s += "\n";
}
return s;
}
完整代码请移步:Graph.ts编写测试代码接下来我们通过一个例子来测试下我们上述图的实现是否正确,我们还是以基本概念中所用的图为例。为了方便起见,我们创建了一个数组,这个数组包含了图中的所有顶点,我们遍历数组,将数组中的每个顶点添加进我们的图中。const graph = new Graph();
const vertices = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
for (let i = 0; i < vertices.length; i++) {
graph.addVertex(vertices[i]);
}
然后,调用addEdge方法添加我们想要的边。graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("A", "D");
graph.addEdge("C", "D");
graph.addEdge("C", "G");
graph.addEdge("D", "G");
graph.addEdge("D", "H");
graph.addEdge("B", "E");
graph.addEdge("B", "F");
graph.addEdge("E", "I");
最后,调用toString方法将图转换为字符串,将其打印到控制台。console.log("图的关系对应如下");
console.log(graph.toString());
运行结果如下,我们发现图的对应关系与基本概念中的示例图一样。完整代码请移步:GraphTest.js图的遍历与树结构类似,我们可以访问图的所有节点,有两种算法可以实现对图进行遍历:广度优先搜索和深度优先搜索。图遍历可以用来寻找特定的顶点或寻找连个顶点之间的路径,检查图是否联通,检查图是否含有环。
小乔学算法
对语言大模型的现状总结与趋势
本文是对《对语言大模型的若干观察和思考》等网文总结ChatGPT与LLM技术现状LLM的主要手段模型:Transformer拥有强大的表示能力,能对具有组合性(compositinality)的语言进行很好的表示和学习。预训练(pre-training):使用大规模文本数据进行语言建模(language modeling),学习进行的是数据压缩,也就是单词序列的生成概率最大化或预测误差最小化。监督微调 SFT(supervised fine tunning):学习的是输入到输出的映射,X→YX→YX→Y, 或者是输入到输出的映射及产出过程 X,C1⋯,Cn→YX, C_1⋯,C_n→YX,C1⋯,Cn→Y,学习到模型的基本行为。这里,C1⋯,CnC_1⋯,C_nC1⋯,Cn 代表思维链。基于人类反馈的强化学习 RLHF(reinforcement learning from human feedback):根据人的反馈,调整模型的整体行为。LLM 核心竞争力ChatGPT 和 GPT4 相比传统的深度学习技术,如 BERT,主要是在智能性和通用性上取得了巨大突破。具备语言、知识、简单推理能力,能够很好地近似人的智能行为。不需要标注数据就可以在不同领域完成不同任务,也就是进行零样本或小样本学习LLM 带来的巨大进步。究其原因:一是使用大数据大模型大算力 规模带来了质的变化。 ChatGPT 有 175B 参数,300B 的 token 做训练。而之前的模型参数规模超过 1B 的都不多。二是 Open AI 开发出了一套调教大模型的方法,包括基本步骤、技巧和工程实现 利用语言建模的机制将人的知识和能力输入给大模型。大规模系统的工程实现和模型的调教方法成了 Open AI 的核心竞争力。LLM 的优点和局限LLM 已经非常强大。但也有大家指出的明显需要解决的问题:1. 如何优化模型,也就是降低训练和使用成本,同时扩大可处理问题的规模。2. 如何保证模型生成内容的真实性,也就是避免幻觉。3. 如何构建可信赖大模型,也就是保证模型生成结果的有用性,安全性等。LLM 重要研究课题LLM 的优化LLM 的真实性可信赖 LLM 与 AI 伦理LLM 的理论多模态大模型LLM + 逻辑推理智能体(agent)面向未来,多模态大模型、LLM+ 逻辑推理、智能体等都是重要的研究课题,尤其是多模态大模型、LLM+ 逻辑推理。LLM 的统一实现LLM 实现所有自然语言处理任务目前为止,自然语言处理有六个大的任务,包括分类、匹配、标注和语义分析、序列生成、序列到序列、序贯决策。分类:从文字序列到标签的映射,如文本分类。匹配:文字序列与文字序列的匹配,如搜索、阅读理解。标注和语义分析:文字序列到标签序列或结构表示的映射,如分词、词性标注、句法分析。序列生成:文字序列的生成,也就是基于语言模型的生成。序列到序列(seq2seq):文字序列到文字序列的转化,如机器翻译、生成式对话、摘要。序贯决策:基于已有的文字序列产生新的文字序列,如多轮对话。前三个是语言理解任务,后三个是语言生成任务。理解任务的输出是类别标签等,可以认为是心智语言的表示。所有的任务都可以用序列到序列 seq2seq 模型实现。语言理解是自然语言到心智语言的 seq2seq。语言生成是心智语言到自然语言的 seq2seq。语言转换是一种自然语言到另一种自然语言的转换。多模态大模型多模态大模型指的是将文本、图像、视频、音频等多模态信息联合起来进行训练的模型。代表性的MLLM分为4种主要类型: 多模态指令调整(MIT) 多模态上下文学习(M-ICL) 多模态思想链(M-CoT) LLM辅助视觉推理(LAVR) 前三个构成了MLLM的基本原理,而最后一个是以LLM为核心的多模态系统。但前三种技术也都是是相对独立的,并且可以组合使用。多模态处理应该是 LLM 之后未来人工智能发展的重要方向。多模态研究最近也有很多进展。比如,视觉语言模型(vision language model)方面,Open AI 开发的 CLIP 模型是视觉语言对齐上最有代表性的模型。字节跳动也开发了 X-VLM 模型,在细粒度的多模态理解任务上有最好的表现 。LLM 与数学能力数学能力包括几种能力,有逻辑推理、算术计算、代数计算、几何概念理解等。人的数学解题有两种机制,分别使用心理学称作的系统 1 和系统 2,进行快的思维(基于死记硬背)和慢的思维(进行深入思考)。用LLM直接解题,对应着系统 1。 用 LLM 产生心智语言,在心智语言的基础上进行解题,对应着系统 2。LLM 本身具备类推推理(analogical reasoning)的能力,但不具备逻辑推理(logical reasoning)的能力(逻辑推理是指基于三段论的推理)。因此,LLM 可以做一些简单的数学计算、数学解题。对比于人,相当于用死记硬背的方法做数学。虽然 GPT4 展现出了非常强的数学解题能力,求解复杂的数学问题应该还需要其他机制。附录《对语言大模型的若干观察和思考》主要观点ChatGPT 的突破主要在于规模带来的质变和模型调教方式的发明。LLM 融合了实现人工智能的三条路径。LLM 的开发需要结合第三者体验和第一者体验。LLM 能近似生成心智语言。LLM 需要与多模态大模型结合,以产生对世界的认识。LLM 本身不具备逻辑推理能力,需要在其基础上增加推理能力。Transformers语言模型不仅仅是一个神经网络。现代语言模型包含各种组件或块,通常由不同的神经网络组成,每个组件或块都设计用于执行特定任务并具有专门的体系结构。「几乎所有当前的 LM 都基于一种特别成功的架构选择,那就是Transformer」。从自然语言处理 (NLP) 领域开始,Transformers 已经彻底改变了几乎所有应用 AI 领域,因为它们能够高效地一次处理大量数据(并行化)而不是顺序处理,这一特性允许在更大的数据集上进行训练 数据集比以前的现有架构。在文本数据上,Transformers 被证明非常擅长执行某种形式的自然语言上下文理解,这使它们成为当今大多数NLP任务的标准选择。两个组成部分是成功的关键:注意力机制和词嵌入。RLHF三步骤RLHF用于训练ChatGPT,OpenAI通过三步过程微调 ChatGPT:初初步 有一批通过工人标注与OpenAI的API请求由取的数据构建成的训练数据集。 然后使用该数据集以监督方式微调预训练模型,生成监督微调 (SFT) 模型。第二步 围绕偏好排序。标注者(或注释者)的任务是对多个 SFT 模型输出进行投票,从而创建一个由比较数据组成的新数据集。第三步 及应用强化学习通过奖励模型向 SFT 模型传授人类偏好策略,基本上如上一节所述。 SFT 模型通过奖励模型进行微调。 结果就是所谓的政策模型。参考字节跳动李航:对语言大模型的若干观察和思考大型自然语言模型(LLM)发展与关键技术
小乔学算法
机器学习的两种典型任务 - 分类与回归
分类和回归是机器学习的两种典型的任务,本文半分别对这两个任务做阐述。首先我们先流程下机器学习过程 -> 准备数据 - 构建模型 - 训练模型 - 预测结果详见 机器学习的任务流程分类任务分类是一种预测模型,用于将输入数据划分到预定义的类别中,其通过学习样本数据的特征和标签之间的关系,建立一个决策边界或者分类规则来进行分类预测。简单理解就是,分类任务是对”离散值"进行预测,根据每个样本的值/特征预测该样本属于类型A、类型B还是类型C,例如情感分类,内容审核,相当于学习了一个分类边界(决策边界),用分类边界把不同类别的数据区分开来。按输出类别(标签)不同,可以分为二分类(Binary Classification)、多分类(Multi-Class Classification)、多标签分类(Multi-Label Classification)。二分类、多分类与多标签二分类通过训练集(x1,y1),(x1,y1)...(xn,y1n)(x_1,y_1),(x_1,y_1)...(x_n,y_1n)(x1,y1),(x1,y1)...(xn,y1n)进行学习,建立一个从输入空间X到输出空间Y的映射 f:X->Y. 其中Y={-1,+1} 或 {0,1} 。比如:新闻可以分为体育、非体育等两个类别,这就是一个典型的二分类任务。假设1代表正类,0代表负类,二分类模型可以定义如下:P = (Y=1|x} = f(x) P = (Y=0|x} = f(x)其中 Y ∈ {0,1} 是输出,x为输入。二分类算法:传统二分类算法思想LR逻辑回归算法概率划分SVM支持向量机空间划分K邻近算法距离划分决策树信息量划分朴素贝叶斯条件概率公式FM因子分解机概率划分常用的二分类评估指标:准确率、召回率、F1。可见机器学习之模型评估指标多分类通过训练集(x1,y1),(x1,y1)...(xn,y1n)(x_1,y_1),(x_1,y_1)...(x_n,y_1n)(x1,y1),(x1,y1)...(xn,y1n)进行学习,建立一个从输入空间X到输出空间Y的映射 f:X->Y. 其中Y > 2。比如:新闻可以分为体育、财经、其它等三个类别,这就是一个典型的多分类任务。假设A代表A类,B代表B类...,N代表N类。多分类模型定义如下: P = (Y= A|x} = f(x) P = (Y= B|x} = f(x)... P = (Y= N|x} = f(x)其中 Y ∈ {A,B...N} 是输出,x为输入。多分类算法:传统二分类算法思想K邻近算法距离划分朴素贝叶斯条件概率公式决策树信息量划分RF随机森林信息量划分GBDT梯度提升树信息量划分XGBoost信息量划分多分类评估方法:Micro F1、Macro F1。可见机器学习之模型评估指标一些算法只支持完成二分类的任务,但有一些算法天然可以完成多分类任务。 多分类的任务可以转换成二分类的任务,多标签分类假设X=R4X=R^4X=R4代表d维的示例空间,Y = {y1,y2,...yqy_1,y_2,...y_qy1,y2,...yq}代表包含q个类的标记空间,给定多标记训练集D = {(xix_ixi,yiy_iyi)},其中 xi∈Xx_i ∈ X xi∈X 为 d维的属性向性(xi1,xi2,...,xid)r(x_i1,x_i2,...,x_id)^r(xi1,xi2,...,xid)r,而 Y∈YY ∈ Y Y∈Y 为与 xi x_ixi对应的类别标记,学习系统的任务是从中学习得到一个多标记分类器h:x−>2Yh:x -> 2^Yh:x−>2Y。基于此,对于任一示例 x ∈ X,分类器预测隶属于该示例的类别标记集合为 h(x) ∈ Y。简单的说:一个实例,在某个时刻,可能有多个标签来描述它。如下图,这个电影被标记了“战争”,“普通话”,“华语”这几个标签。多标签算法:方法策略算法问题转换一阶策略Binary Relevance二阶策略Calibrated Label Ranking高阶策徊Random k-labelsets算法适应一阶策略ML - kNN二阶策略Rank - kNN高阶策徊LEAD多标签评估:基于样本角度评估指标分类Subset AccuracyHamming Loss基于样本角度评估指标分类Bmacro<br>BmicroB_macro<br>B_microBmacro<br>Bmicro排序AUCmacro<br>AUCmicroAUC_macro<br>AUC_microAUCmacro<br>AUCmicro三种分类对比假设输出二分类二元独立二选一多分类多元独立多选一多标签多样性多选多分类工具Scikit-learnFastTextBERT...典型应用分类任务一直都是机器学习的基础任务,已经被广泛应用在新闻分类、情感分类、主题分类、图片分类、视频分类、广告过滤,内容审核,评论分析,问题对答等NLP、数据挖掘、推荐系统、广告系统等领域。回归任务设样本集S={s1,s2,…,sms_1,s_2,…,s_ms1,s2,…,sm}包含m个样本,样本s_i=(xi,yix_i,y_ixi,yi)包括一个实例x_i和一个实数标签值y_i,实例由n维特征向量表示,即x_i=(xi(1),xi(2),…,xi(n))(x_i^(1),x_i^(2),…,x_i^(n))(xi(1),xi(2),…,xi(n))。 回归任务可分为学习过程和预测过程。简单的说: 回归任务是对“连续值”进行预测,根据每个样本的值/特征预测该样本的具体数值,例如房价预测,股票预测,销售额等,相当于学习到了这一组数据背后的分布,能够根据数据的输入预测该数据的取值。常见的回归算法贝叶斯线性回归百分比回归核岭回归支撑向量回归分位数回归回归树级联相关分组方法数据处理多元自适应回归样条多线性插值...评估算法回归任务的评估算法: 机器学习之模型评估指标回归工具Scikit-learnBERT...典型应用回归算法经常用于预测,用一个连续的函数去预测未来和未知。趋势预估:传染病学金融:分析与量化投资的系统性风险经济:预测消费支出、固定资产投资支出、持有流动资产需求等环境科学:...与分类任务的区别根本区别在于输出为一个度量空间:f(x)→Y,xεA,yεBf(x) → Y,x ε A , y ε Bf(x)→Y,xεA,yεB对于分类问题,目的是寻找决策边界,其输出边界空间B不是度量空间,即“定性”。也就是说,在分类问题中,只有分类“正确”与“错误”之分,至于分类到了类别A还是类别B,没有分别,都是错误的数量+1。对于回归问题,目的寻找最优拟合, 其输出边界空间B是一个度量空间,即“定量”问题。通过度量空间衡量训练预测值与真实值之间的“误差大小”。当真实值为10时,预测值为5时,误差为5,预测值为8时,误差值为2。【参考】北大公开课-人工智能基础 56 机器学习的任务之回归二分类、多分类、多标签分类的基础、原理、算法和工具
小乔学算法
深度学习经典模型之RNN_LSTM_Seq2Seq
RNN什么是RNN循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络(recursive neural network)。RNN是一种特殊的神经网络结构, 它是根据人的认知是基于过往的经验和记忆这一观点提出的. 它与DNN,CNN不同的是: 它不仅考虑前一时刻的输入,而且赋予了网络对前面的内容的一种记忆功能.RNN之所以称为循环神经网路,即一个序列当前的输出与前面的输出也有关。具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,即隐藏层之间的节点不再无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。RNN时间线展开图及公式RNN 参考人类思考的方式,按时间顺序将文本(字或词)输入至模型里处理,比如“我想吃汉堡“前一时间输入的信息会通过上图中的中间层传递给下一时刻,参与下一时间输出的计算。RNN应用领域RNN的应用领域有很多, 可以说只要考虑时间先后顺序的问题都可以使用RNN来解决.这里主要说一下几个常见的应用领域:自然语言处理(NLP): 主要有视频处理, 文本生成, 语言模型, 图像处理机器翻译, 机器写小说语音识别图像描述生成文本相似度计算推荐(音乐推荐、网易考拉商品推荐、Youtube视频推荐等新的应用领域).RNN发展代表性RNN及变体基本RNN:循环网络的基本构成LSTM:突破性进展的长短期记忆网络GRU:新式的Cell模块单元NTM:更大记忆体模型的探索RNN存在的 知期记忆 问题RNN 跟传统神经网络最大的区别在于每次都会将前一次的输出结果,带到下一次的隐藏层中,一起训练。也就是说,RNN 前面所有的输入都会对未来的输出产生影响。如下图所示,RNN 中短期的记忆影响较大(如橙色区域),但是长期的记忆影响就很小(如黑色和绿色区域),这就是 RNN 存在的短期记忆问题。RNN无法并行训练,导致训练成本较高。LSTM - Long Short-Term Memory - 接替RNN长短期记忆网络 在Bert出来之前,LP领域的任务都 不管是文本分类或NER(实体提取) 等都是用LSTM来完成。什么是LSTM由于RNN也有梯度消失的问题,因此很难处理长序列的数据。LSTM是对RNN的改进的特例(Long Short-Term Memory),它可以避免常规RNN的梯度消失,因此在工业界得到了广泛的应用。Long Short Term Memory networks(以下简称LSTMs),一种特殊的RNN网络,该网络设计出来是为了解决长依赖问题。该网络由 Hochreiter & Schmidhuber (1997)引入,并有许多人对其进行了改进和普及。他们的工作被用来解决了各种各样的问题,直到目前还被广泛应用。整体表征图上图 左侧为t−1t-1t−1的时间步,中间为ttt时刻的时间步,中侧为t+1t+1t+1时刻的时间步。 α (sigmoid):x?x_?x?不论取何况,所有的值都会被压缩0 10~10 1之间,当xix_ixi取值越大,α越接近于1,反之接近于0。所有循环神经网络都具有神经网络的重复模块链的形式。在标准的RNN中,该重复模块将具有非常简单的结构,例如单个tanh层。LSTMs也具有这种链式结构,但它的重复单元不同于标准RNN网络里的单元只有一个网络层,它的内部有四个网络层。LSTM核心思路单元状态LSTMs的核心是单元状态(Cell State),用贯穿单元的水平线表示。单元状态有点像传送带。它沿着整个链一直走,只有一些微小的线性相互作用。信息很容易在不改变的情况下流动。单元状态如下图所示。LSTM确实有能力将信息移除或添加到单元状态,并由称为gates的结构小心地进行调节。门控机制如下图,三个α (sigmoid)即对应三个门,从左到右分别是: 遗忘门,输入门,输出门门是一种选择性地让信息通过的方式。它们由一个Sigmod网络层和一个点乘运算组成。因为sigmoid层的输出是0-1的值,这代表有多少信息能够流过sigmoid层。0表示都不能通过,1表示都能通过。三个门的表达式:遗忘门遗忘门 是上一个隐层信息 ht−1h_{t-1}ht−1 和 当前输入的信息xix_ixi息整合(组合成一个非线性的函数)后输入到α里。如果 遗忘门 认为该数据需要丢弃,那么α数值贴近于0,相关于衰减。 越接近于1,数据保留更多,衰减更少。输入门输入门依然是上一时刻的信息ht−1h_{t-1}ht−1与当前输入的信息xix_ixi组合后,输入到α里。输出门Seq2Seq什么是Seq2Seqseq2seq是是一种循环神经网络RNN的变种,包含编码器(Encoder)和解码器(Decoder)两个部门,也称为 Encoder-Decoder 模型。Seq2Seq结构seq2seq是”Sequence to Sequence”的简写,seq2seq模型的核心就是编码器(Encoder)和解码器(Decoder)组成的。hαh_αhα 隐藏层信息 x1,x2,x3,x4x_1,x_2,x_3,x_4x1,x2,x3,x4: 输入值 h1,h2,h3,h4h_1,h_2,h_3,h_4h1,h2,h3,h4:输入值经过encoder + hαh_αhα 整后后得到的值 h0‘h^`_0h0‘是decoder的隐藏层信息,上下文向量c在seq2seq不再被当成RNN的初始隐藏层,而是当成每一个神经元的输入(每个神经都拥有一个输入)Seq2Seq的数据流程在Encoder中,“欢迎/来/北京”这些词转换成词向量,也就是Embedding,我们用 $v_i$来表示,与上一时刻的隐状态 $h_{t-1}$按照时间顺序进行输入,每一个时刻输出一个隐状态$h_{t}$ ,我们可以用函数$f$ 表达RNN隐藏层的变换:$h_t=f(v_i,h_{t-1})$ 。假设有t个词,最终通过Encoder自定义函数$q$ 将各时刻的隐状态变换为向量 $c=q(h_0,h_1,...,h_{t})$,这个 $c$就相当于从“欢迎/来/北京”这几个单词中提炼出来的大概意思一样,包含了这句话的含义。Decoder的每一时刻的输入为Eecoder输出的$c$ 和Decoder前一时刻解码的输出$s_{t-1}$ ,还有前一时刻预测的词的向量$E_{t-1}$ (如果是预测第一个词的话,此时输入的词向量为“GO”的词向量,标志着解码的开始),我们可以用函数g 表达解码器隐藏层变换: $s_i=g(c,s_{t-1},E_{t-1})$。直到解码解出“ EOS”,标志着解码的结束。Seq2Seq Attention模型在 Seq2Seq 模型,Encoder 总是将源句子的所有信息编码到一个固定长度的上下文向量 c中,然后在 Decoder 解码的过程中向量 c 都是不变的。这存在着不少缺陷: 对于比较长的句子,很难用一个定长的向量 c 完全表示其意义。 RNN 存在长序列梯度消失的问题,只使用最后一个神经元得到的向量 c 效果不理想。 与人类的注意力方式不同,即人类在阅读文章的时候,会把注意力放在当前的句子上。Attention 即注意力机制,是一种将模型的注意力放在当前翻译单词上的一种机制。使用了 Attention 后,Decoder 的输入就不是固定的上下文向量 c了,而是会根据当前翻译的信息,计算当前的 c。Seq2Seq 是自然语言处理中的一种重要模型,通过在seq2seq结构中加入Attention机制,使seq2seq的性能大大提升,现在seq2seq被广泛地用于机器翻译、对话生成、文本摘人、人体姿态序列生成等各种任务上,并取得了非常好的效果。公式RNN的attention都是基于decoder中的目标词和encoder的序列中的每一个词计算点积(或者其他的计算方式,如MLP,conv等都可以),然后softmax得到一个概率分布,也就是attention的权值。然后对encoder的序列中的每个词对应的向量做加权和得到最终的attention的结果。具体的如下图:对于 Decoder 的每一步解码 i ,都有一个输入Ci,对输入序列所有隐层信息 h1 ,h2 ,...,hTx进行加权求和。相当于在预测下一个词时,会把输入序列的隐层信息都看一遍,决定预测预测当前词语输入序列的哪些词最相关。[参考]一文看尽RNN(循环神经网络)Seq2Seq模型介绍
小乔学算法
大语言模型实战预习资料汇总-Python+Pytorch+机器学习+深度学习
在学习大模型之前希望有python与NLP相关经验的算法工程师,如果之前不是从事相关领域工作的小伙伴,那么我们可能需要做好基础准备。Python基础主要是在帮助学习者快速掌握Python编程语法、了解Python开发常见概念和工具。定位于让其他语言开发者在较短时间内掌握Python开发的最少必要知识。一般要求学会包括如下内容:python环境搭建Python的基本语法函数读写文件异常处理面向对象模块和import、包、标准库第三方库等内容。教材列表官方教材中文教程python 3 教程机器学习基础主要包括机器学习的一系列基础概念,保证对机器学习的基本概念有一个初步的认知, 预习材料中,掌握这部分的内容尤其重要。机器学习从入门到精通材《机器学习从入门到精通材》★吴恩达机器学习课程 2019《吴恩达机器学习课程 2019》 ★极为经典的一套入门课程,大家如果有富裕时间,可以完整看一遍,相信你会受益匪浅机器学习资源汇总Pytorch基础Pytorch框架,国内应用最多的深度学习框架,最好能掌握它的基础使用方法。刚开始基础语法即可。★ Pytorch 快速入门教程《Pytorch 快速入门教程》以下信息在视频下方的文本说明里就有 对应代码: github.com/xiaotudui/P… 蚂蚁蜜蜂/练手数据集 pan.baidu.com/s/1jZoTmoFz… 密码: 5suq 课程资源:pan.baidu.com/s/1CvTIjuXT… 提取码:jnnp 如果没有Nvidia显卡,torch.cuda.is_available()就是False,是正确的。 即便没有显卡,也是可以往后面学习的。官方文档《官方文档》深度学习基础同 "机器学习"一样,深度学习部分要尽量多花时间去学习相关的基础知识。深度学习入门教程《深度学习入门教程》★吴恩达深度学习课程《吴恩达深度学习课程》推荐阅读★西瓜书周志华老师的《机器学习》 —— 俗称《西瓜书》李航《统计学习方式》Hulu《百面机器学习》Transformer论文逐段精读Transformer论文逐段精读【论文精读】动手学深度学习综合视频课跟李沐学AI《跟李沐学AI》
小乔学算法
PyTorch入门之Tensor综合-含操作/运算、机器学习的关系、稠密张量与稀疏张量的定义等
Tensor的理解数学中有标量、向量和矩阵的概念,它们的维度分别是0、1、2。其中:标量可以看成的一个数字,1,标量中元素的位置固定。向量可以看成是一维表格,向量中元素的位置需要通过其索引确定,表示为矩阵可以看成是二维表格,矩阵中的元素位置需要通过其行号和列号确定,表示为:张量(Tensor) 可以视为矩阵的扩展,可以用于表示无穷维度的数据如果我们用标量、向量或矩阵描述一个事物时,该事物最多可用 [H,W]的维度表示。在现实与客观世界中,我们经常会碰到的物体的维度可能会更高维度,很难通过向量或矩阵来描述,这时我们就需要张量。也就是说,我们可以通过张量来描述任意维度的物体 H * W * C,其中C为C维的特征图(特征图是深度学习中的一个概念),除外我们还可以用Tensor描述更高维度的物体:H * W * C* D,其中D为未知的更高维空间。总之,张量是对于标量、向量、矩阵之上进行更加泛化的定义。标量可以看成是0阶的张量,向量是1阶段张量,矩阵是2阶的张量,除了0,1,2阶的张量之外,还有3阶、4阶、5阶..n阶的张量。引申到深度学习,其 数据输入的维度是不确定的(可以是任意维度),这时就需要采用一个更加广泛的概念去描述这些量,Tensor就可以更便利解决该问题的。Tensor的基本概念其中标量是0维的张量,向量是1维的张量,矩阵是2维的张量。举例:如下所示,左侧为一个长方体某一切面的矩阵(二阶张量),该矩阵包含N*M个元素,其中每一个元素是一个标量,每一张都是一个向量。一个物体有N个切面,将C 个 N * M 拼接到一起,就会得到一个三阶的张量C*N*M用于表示长方体(如下右图)在用张量描述物体时,我们需要确定这个张量具体是一个什么样的量,用变量或常量来描述。Tensor机器学习的关系完整的机器学习的任务,会涉及到样本、模型等元素(如下所示)。对于样本(机器学习中用到的数据)我们就可通过Tensor来对其进行描述。比如一条语音数据,我们可有能会采用向量来进行描述,该向量就是1阶的张量。此时向量描述的是语音数据被采样后在当前时刻的声音特征,图形化之后可能为波行。而对于灰度图,我们通常采用矩阵描述(二阶Tensor),表示成H*W,彩色图则会描述成[H * W * C],为一个三阶Tensor,其中C=3。而模型(模型分为有参数模型与无参数模型),有参数模型一般被描述为 Y = WX + b函数,其中X是指样本,W,B是指参数。当W与B未知的情况下为变量,该变量也是通过Tensor来表示,Y为最后的标签,而标签在进行数字化时也会通过Tensor来对其进行描述。样本标签与属性关系可描述为y=f(x),其中x为属性(样本),f为模型。结论:Tensor可以用来描述机器学习过程中的样本或模型。Tensor的类型张量(Tensor)是Pytorch库中的基本数据类型,在 Pytorch中各种基本数字类型都有其对应的Tensor类型(但在Pytorch中没有内嵌的字符串类型)。类型列表Tensor的每种类型分别有对应CPU和GPU版本。加粗部分为常用类型。Tensor默认的数据类型FloatTensor。-数据类型torchCPU TensorGPU Tensor32-bit float point32 bit 浮点torch.float32 or torch.floattorch.FloatTensortorch.cuda.FloatTensor64-bit float point64 bit 浮点torch.float64 or torch.doubletorch.DoubleTensortorch.cuda.DoubleTensor16-bit float point16 bit 半精度浮点torch.float16 or torch.halftorch.HalfTensortorch.cuda.HalfTensor8-bit integer(unsigned)8 bit 无符号整形(0~255)****torch.uint8torch.ByteTensortorch.cuda.ByteTensor8-bit integer(signed)8 bit 有符号整形(-128~127)torch.int8torch.CharTensortorch.cuda.CharTensor16-bit integer(signed)16 bit 有符号整形torch.int16 or torch.shorttorch.ShortTensortorch.cuda.ShortTensor32-bit integer(signed)32 bit 有符号整形torch.int32 or torch.inttorch.IntTensortorch.cuda.IntTensor64-bit integer(signed)64 bit 有符号整形torch.int64 or torch.longtorch.LongTensortorch.cuda LongTensorBoolean布尔torch.booltorch.BooleanTensortorch.cuda.BooleanTensor常见类型操作将普通张量类型转化为GPU张量类型的方法: 普通张量变量名.cuda(), 返回一个GPU张量的引用。Tensor默认的数据类型默认的Tensor是FloatTensor(如果默认类型为GPU tensor,则所有操作都将在GPU上进行)。设置默认的数据类型: torch.set_default_tensor_type(类型名)增强学习中使用DoubleTensor的使用会更多将张量转换为其他数据类型查看张量数据类型的方法type方法:使用张量名.type()可以查看张量的具体类型。isinstance:isinstance(torch.randn(2,3),torch.FloatTensor),返回布尔值。生成元素数据类型指定的张量浮点型张量tensor.FloatTensor(标量/列表/numpy数组) # 生成元素均为单精度浮点型的张量
tensor.DoubleTensor(标量/列表/numpy数组) # 生成元素均为双精度浮点型的张量
tensor.HalfTensor(标量/列表/numpy数组) # 生成元素均为半精度浮点型的张量
整型张量tensor.IntTensor(标量/列表/numpy数组) # 生成元素均为基本整型的张量
tensor.ShortTensor(标量/列表/numpy数组) # 生成元素均为短整型的张量
tensor.LongTensor(标量/列表/numpy数组) # 生成元素均为长整型的张量
布尔型张量tensor.BoolTensor(标量/列表/numpy数组) # 生成元素均为布尔类型的张量
Tensor的创建创建函数函数功能备注Tensor(*size)基础构造函数size: 直接根据形状定义Tensor, 例:torch.tensor(标量列表),Tensor(data)类似np.arraydata: 使用数据直接初始化, 例:torch.from_numpy(numpy数组)*ones(size)全1Tensor常用结构:全部为1的张量*zeros(size)全0Tensor常用结构:全部为0的常量*eye(size)对角线为1,其他为0常用结构:对角线为1,其他为0arange(s,e,step)从s到e,步长为step从s到e, 中间的间隔为step,即步长linspace(s,e,steps)从s到e,均匀切分成steps份从s到e, 均匀切分成steps份rand/randn(*size)均匀/标准分布size: 根据形状定义Tensor, 值为随机赋值,均匀/标准分布的随机采样normal(mean,std)/uniform (from,to)正态分布/均匀分布满足正态分布或均匀分布的randperm(m)随机排列对一个序列进行随机排列empty(*size)生成不经过元素初始化的指定形状的张量rand_like(tensor)生成指定填充值的指定形状的张量full(*size)指定值的Tensor编程实例import numpy as np
import torch
def printTensor(tensor):
print(tensor)
print("numel =", tensor.numel()) # 输出9
print("dim =", tensor.dim()) # 输出2
print("type =", tensor.type()) # 输出2
'''常用的Tensor定义'''
print("--------------as_tensor--------------")
shape = [[2, 3], [4, 5], [6, 7]]
tensor = torch.as_tensor(shape)
printTensor(tensor)
print("--------------Tensor(* Size)--------------")
shape = [(2, 3), (4, 5), [6, 7]]
tensor1 = torch.Tensor(shape)
printTensor(tensor1)
print("--------------numpy.array()--------------")
data_array = np.array([[2, 3], [4, 5], [6, 7]])
tensor = torch.tensor(data_array)
printTensor(tensor)
# 创建一个3行3列的张量
print("--------------torch.ones--------------")
tensor = torch.ones((3, 3))
printTensor(tensor)
print("--------------torch.zeros--------------")
tensor = torch.zeros(2, 2)
printTensor(tensor)
print("--------------torch.zeros_like--------------")
tensorlike = torch.zeros_like(tensor1)
printTensor(tensorlike)
print("--------------torch.randn--------------")
tensor = torch.randn(2, 2)
printTensor(tensor)
'''正态分布'''
print(u"--------------torch.normal mean为均值,std为标准差-1--------------")
# std=5组不同的正诚分布,5组都是随机的标准差和 mean =0
tensor = torch.normal(mean=0.0, std=torch.rand(5))
printTensor(tensor)
print(u"--------------torch.normal mean为均值,std为标准差-2--------------")
# std=5组不同的正诚分布,5组都是随机的标准差和随机的mean
tensor = torch.normal(mean=torch.rand(5), std=torch.rand(5))
printTensor(tensor)
print(u"--------------torch.uniform_--------------")
tensor = torch.Tensor(4, 2).uniform_()
printTensor(tensor)
'''定义一个序列'''
print(u"--------------torch.arange--------------")
# 定义一个序列, 步长为2, 最后10不包含在序列中
tensor = torch.arange(0, 10, 2)
printTensor(tensor)
print(u"--------------torch.linspace--------------")
# 等间节切分,5为个数,11为范围,0为起始值
tensor = torch.linspace(0, 11, 5)
printTensor(tensor)
Tensor的属性每一个Tensor有torch.dtype、torch.device、torch.layout三种属性torch.device 标识了torch.Tensor对象在创建之后所存储在的设备名称torch.layout表示torch.Tensor内存布局的对象torch.dtype: 在使用tensor函数创建tensor张量对象时还可以使用dtype参数指定数据类型 torch.device: 张量所创建的数据,到底应该存储在哪个设备上,CPU或GPU, GPU是通过CUDA来表示,多个则用cuda:0, cuda:1,以此类推 torch.layout: 张量的排布方式,对应到内存中连续的区别。稠密或稀疏的方式。稠密的张量稠密的张量定义方法import torch
# GPU
# GPU则代码改为: dev = torch.device("cuda:0")
dev = torch.device("cpu")
a = torch.tensor([1,2,3], dtype=torch.float32,device=dev)
print(a)
稀疏的张量稀疏或低秩是机器学习中两个很重要的概念,描述了当前数据是否满足某种性质稀疏表达了当前数据中非0元素的个数,非0元素的个数越少,说明越稀疏。如果全部为0则说明最稀疏。低秩描述了数据本身的关联性,也是线性代码中的一个概念。秩从线性相关的角度来看,主要是描述了当前矩阵中的向量间线性可表示的关系。稀疏的张量在机器学习中的优势从模型角度:能够使模型变的非常简单。对于有参数的模型,如果参数中0的个数非常多,意味着可以对模型进行简化。即参数为0的项(item) 是可以减掉的,因为0乘以任何数都等于0。这样参数个数变少,意味着模型变的更简单,对于参数稀疏化的约束在机器学习中是一个非常重要的性质。这是我们从机器学习模型的角度上介绍稀疏的意义。从数据角度,通过对数据进行稀疏化的表示,可以减少数据在内存中的开销。假设存在一个100*100的矩阵,哪果用稠密方式表示数据,则需要100**100单位的空间,而如果用稀疏张量表示,我们只要记住非0元素的坐标即可。torch.sparse_coo_tensorPyTorch中用torch.sparse_coo_tensor 表示稀疏矩阵。使用torch.sparse_coo_tensor可以方便地将稀疏矩阵转换为PyTorch张量,并进行各种操作。同时,由于只存储了非零元素的位置和值,因此可以节省大量的内存空间。名称是 'coo'代表非零元素的坐标。即coo类型: coo类型表示了非零元素的坐标形式torch.sparse_coo_tensor参数说明:indices: 一个二维的LongTensor, 表示非零元素在原矩阵中的位置,其形状为(N, 2),其中N为非零元素个数。values: 一个一维的Tensor, 表示非零元素的值,其形状为(N,)。size: 一个元组,表示输出张量的形状,例如(M, N)。torch.sparse_coo_tensor参数解说:indices: 表示非零元素在原矩阵中的位置,即哪些位置是非零的。它的形状为(N, 2), 其中N为非零元素个数。每个元素包含两个值,分别表示该非零元素在行和列上的位置。values: 表示非零元素的值,即这些位置上的数值。它的形状为(N,)size: 表示输出张量的形状,即输出张量的行数和列数。例如,如果输入矩阵是一个4x5的矩阵,但是只有第2行和第4行、第3列和第5列上的元素是非零的,那么输出张量的形状就是(2, 2)。torch.sparse_coo_tensor用法import torch
dev = torch.device("cpu")
# 定义长度分别为3个长度的坐标: [0,2] [1,0] [1,2],
# indices = torch.tensor([[0, 1, 2], [0, 1, 2]]) 会保存数据落地对角线上
indices = torch.tensor([[0, 1, 1], [2, 0, 2]])
# 以上3组从坐标对应的三个非0元素 3,4,5
values = torch.tensor([3, 4, 5], dtype=torch.float32)
# 原张量的形状是一个2,4的tensor, 如果用稠密方式打印,打看到一个2,4的变量
x = torch.sparse_coo_tensor(indices, values, [3, 3], device=dev, dtype=torch.float32)
x_to_dense = x.to_dense()
print("--------sparse_coo_tensor------------")
print(x)
print("--------x_to_dense------------")
print(x_to_dense)
控制台输出例:将数据落地对角线上indices = torch.tensor([[0, 1, 2], [0, 1, 2]])
values = torch.tensor([1,2,3], dtype=torch.float32)
Tensor的算术运算四则运算加减乘除矩阵运算其他运算torch.pow - 幂运算torch.exp(input, out=None) - e指数,注意只支持浮点型torch.sqrt(input, out=None) - 开方torch.log(input, out=None) - 对数运算,以e为底ceil/round/floor/trunc - 取整/四舍五入/下取整/只保留整数部分 - 如torch.ceil(input, out=None)clamp(input, min, max) - 超过min和max部分截断 - torch.clamp(input, min, max, out=None)torch.abs(input, out=None)- 求绝对值...加减法运算import torch
a = torch.rand(2, 3)
b = torch.rand(2, 3)
c = a + b # a - b ,
print(c)
c = torch.add(a, b) # 减法sub
print(c)
print(a.add(b)) # 减法sub,
# a.add_(b) # sub_(...), 带下划线的方式,运算后将结果同时赋给a。加减乘除等运算中含有下划线"_"的规则都一样。
print(a)
减法:- / ...sub() / ..sub_() 乘法: * / c = torch.mul(a,b) /a.mul(b) /a.mul_(b)哈达玛积哈达玛积(element wise,对应元素相乘) - mul乘法 如果一个tensor是shape是2 * 2 的话,则另外一个tensor也是(2 * 2), 这样保证所有元素都保证每个元素都可以被相乘。c = a * b
c = torch.mul(a, b) # a和b的shape要一样的
a.mul(b)
a.mul_(b) # 同时将结果赋给a
矩阵的乘法二维矩阵的乘法运算二维矩阵的乘法运算包括torch.mm(),torch.matmul(),@规则a * b 时,即两个矩阵相乘,m * n, n * p, 一定要保证 两个矩阵的'n'是相同的。a = torch.ones(2,1)
b = torch.ones(1,2)
print(torch.mm(a,b))
print(torch.matmul(a,b))
print(a @ b)
print(a.matmul(b))
print(a.mm(b))
高维矩阵的乘法运算对于高维的Tensor(dim>2),假如矩阵的size=(a1,a2,m,n) ,我们要保证除最后两维m,n之外的前几维的值保持一致, 最后两维的规则同二维矩阵,即n相同 。就像矩阵的索引一样并且运算操只有`torch.matmul()同样是除了矩阵内的数值之外,要保证维度的每个元素都可以被计算。 mm()与matmul()不存在下划线类的计算.。如下所示:n相同,a,b相同。a = torch.ones(a, b, m, n)
b = torch.ones(a, b, n, p)
举例:a = torch.ones(1, 2, 4, 5)
b = torch.ones(1, 2, 5, 3)
print(a.matmul(b))
print(torch.matmul(a, b))
矩阵的乘除与逆运算幂运算a = torch.full((2, 3), fill_value=2)
print(torch.pow(a, 2))
print(a.pow(2))
print(a ** 2)
print(a.pow_(2))
指数运算函数y=a^x(a>0且a≠1) 叫做指数函数,a是常数,x是自变量,定义域为R,值域为(0,+∞)。要求:a^x前的系数必须是数1,自变量x必须在指数的位置上,且不能是x的其他表达式,否则就不是指数函数。a>1时,则指数函数单调递增;若0<a<1,则为单调递减的。对于a不大于0的情况,函数的定义域不连续,不考虑; a等于0函数无意义一般也不考虑。指数函数恒过(0,1)点,即水平直线y=1是从递减到递增的一个过渡位置。指数函数是非奇非偶函数。...指数函数应用到自然常数e上写为exp(x),现常写为e^x(表示为x=lny),其图像是单调递增,n∈R,y>0,与y轴相交于(0,1)点。,图像位于X轴上方,第二象限无限接近X轴。e的x次方x = torch.full((2, 3), fill_value=2
print(torch.exp(x))
其他(略)开方运算a = torch.full((2, 3), fill_value=4)
print(a.sqrt())
# ----返回值-----
tensor([[2., 2., 2.],
[2., 2., 2.]])
对数运算对数源于指数,是指数函数反函数 因为:N = ax 所以:x = log(aN)如果 N=a^x(a>0,a≠1),即a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作:x=log(aN) 其中,a叫做对数的底数,N叫做真数,x叫做 “以a为底N的对数”。对数运算规则 数据增长率怎么算 - 对数是用来衡量增长率的: 对数函数中,x是自变量,y是因变量,当x大于1时,对数函数是增函数,所以取对数就是增长率。a = torch.full((2, 3), fill_value=4)
print(torch.log2(a))
print(torch.log10(a))
print(torch.log(a))
Tensor的更多操作维度调整查看维度:torch.shape变换维度:torch.reshape([d0,d1,d2])unsqueeze和squeeze两个tensor合并- `torch.cat(tensors, dim=0, out=None)`
- `torch.stack(tensors)`合并时候新建一个维度
与numpy互换不重复造论子,请看 pytorch和numpy的互转
小乔学算法
队列与双端队列的实现
前言队列作为一种数据结构,在现实生活中它可应用于电影院、自助餐厅等场合,排在第一个的人会先接受服务。在计算机应用领域里,多个文档的打印就是一个队列,排在第一的文档会先执行打印操作。本文将用TypeScript实现队列与双端队列这两种数据结构,并用其解决计算机科学领域中的两道经典题,欢迎各位感兴趣的开发者阅读本文。队列的实现本文主要讲解队列用代码的实现,如果对队列这种数据结构不了解的开发者可以移步我的另一篇文章:栈与队列实现思路队列遵循先进先出(FIFO)原则,根据队列的特性,我们可以知道要实现一个队列必须具备以下方法:入队,将一个新元素加入队列中(往对象中添加一个key)出队,将队首的元素取出(根据key来获取),并返回队首的元素。判断队列是否为空,判断队列中的元素数量是否为0(队列大小 - 队首元素位置)队首元素,获取当前队列的队首元素并返回。队列大小,获取队列中的元素数量并返回(队列大小 - 队首元素位置)。清空队列,删除队列中的所有元素。(初始化队列的内部变量)。队列内所有元素,将队列中的元素用逗号拼接成字符串并返回(遍历队列中的元素)。实现代码有了思路,我们就可以编码了。接下来,我们将上述实现思路转换为代码:新建一个Queue.ts文件使用接口声明队列内部对象类型interface QueueObj {
[propName: number] : any;
}
在构造器中声明并初始化队列需要的三个变量:队列大小、队首元素、队列对象private count: number;
private lowestCount: number;
private items: QueueObj;
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
实现入队函数(enqueue),参数为任意类型的数据,将队列的大小作为队列对象的key。enqueue(item: any) {
// 队列的末尾添加元素: 将队列的大小作为key
this.items[this.count] = item;
this.count++;
}
实现出队函数(dequeue),存储队首元素,移除队列对象中的队首元素key,队首元素位置自增。dequeue() {
if(this.isEmpty()){
return undefined;
}
const result = this.items[this.lowestCount];
// 删除队首元素
delete this.items[this.lowestCount];
// 队首元素自增
this.lowestCount++;
return result;
}
判断队列是否为空(isEmpty),判断当前队列大小 - 当前队首元素位置是否等于0isEmpty() {
return this.count - this.lowestCount === 0;
}
获取队首元素(peek),以当前队首元素位置为key获取队列队对象中的值并返回。peek() {
if(this.isEmpty()){
return undefined;
}
return this.items[this.lowestCount];
}
获取队列大小(size),当前队列大小 - 队首元素位置size() {
return this.count - this.lowestCount;
}
清空队列(clear),初始化构造器中的三个变量。clear() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
获取队列中的所有元素,遍历对象中的元素,用逗号拼接成字符串并返回。toString() {
if(this.isEmpty()){
return "";
}
let objString = `${this.items[this.lowestCount]}`;
for (let i = this.lowestCount + 1; i < this.count; i++){
objString = `${objString},${this.items[i]}`;
}
return objString;
}
完整代码请移步:Queue.ts编写测试代码队列实现后,接下来我们来测试下队列中的方法是否能正常运行。// 队列执行测试
import Queue from "./lib/Queue.ts";
const queue = new Queue();
// 入队
queue.enqueue("队列中的第一条数据");
queue.enqueue("队列中的第二条数据");
queue.enqueue("队列中的第三条数据");
// 出队
queue.dequeue();
// 获取队首元素
console.log(queue.peek());
// 获取队列大小
console.log(queue.size());
// 获取队列中的所有元素
console.log(queue.toString());
// 判断队列中是否有数据
console.log(queue.isEmpty());
// 清空队列
queue.clear();
执行结果如下:双端队列双端队列是一种允许我们同时从前端和后端添加和移除元素的特殊队列。双端队列同时遵守了先进先出和后进先出的原则,所以可以说它是一种把队列和栈相结合的一种数据结构。现实中用到双端队列的例子有很多,例如电影院、餐厅排队的队伍。排队买电影票的人当他买到电影票后,离开了队伍,此时他想咨询一些其他小问题时,他可以直接去队伍的最前面咨询问题。排在队伍后面的人,临时有其他事情无法买票,他就会从队伍的末尾离开。在计算机科学中,存储一系列的撤销操作就用到了双端队列,每当用户在软件中进行了一个操作,该操作就会被存储在一个双端队列中,当用户点撤销操作时,该操作会从队列的末尾弹出,在进行了预先定义的一定数量的操作后,最下执行的操作就会从队首移除。实现思路双端队列相比队列多了两端都可以出入元素,因此普通队列中的获取队列大小、清空队列、队列判空、获取队列中的所有元素这些方法同样存在于双端队列中且实现代码与之相同。由于双端队列两端都可以出入元素,那么我们需要实现以下函数:队首添加元素,添加元素时需要判断队列是否为空,以及队首元素是否为0。队尾添加元素,等同于队列的入队操作。获取队首元素,等同于队列的获取队首元素获取队尾元素,等同于栈的获取栈顶操作。删除队首元素,等同于出队操作。删除队尾元素,等同与出战操作。观察上述我们要实现的函数后,我们发现双端队列除了队首添加元素与之前我们实现的差别很大,其他的函数我们之前都已经实现过了,所以此处仅讲解队首添加元素的实现。想了解其他函数的具体实现请移步我的另一篇文章:数组实现栈与对象实现栈的区别队首添加元素的实现思路如下:如果队列为空,直接调用队尾添加元素函数。如果队首元素key大于0,则需要将当前队首元素key-1,然后将当前元素放入队列中。如果队首元素key等于0,则需要将队列中的元素整体向后移动一位,空出0号key出来,队首元素重新赋值为0,然后将当前元素放入0号key中。实现代码接下来,我们将上述思路转换为代码。新建一个Deque.ts文件声明双端队列内部对象的类型interface DequeObj {
[propName: number]: any;
}
在构造器中声明双端队列需要用到的变量并初始化private count: number;
private lowestCount: number;
private items: DequeObj;
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
实现队首添加元素函数addFront(item: any) {
if (this.isEmpty()){
this.addBack(item);
}else if(this.lowestCount > 0){
// 队首元素大于0
this.lowestCount--;
this.items[this.lowestCount] = item;
}else{
// 队首元素为0,我们需要将将队列里的0号key空出来,其他数据整体向后移动一位。
for (let i = this.count; i > 0; i--){
this.items[i] = this.items[i - 1];
}
// 队列长度自增
this.count++;
// 队首元素设为0
this.lowestCount = 0;
// 为队首的0号key添加当前元素
this.items[0] = item;
}
}
完整代码请移步:Deque.ts编写测试代码// 双端队列测试
import Deque from "./lib/Deque.ts";
const deque = new Deque();
// 队列为空的情况下,往队首添加元素
deque.addFront("队首添加元素");
console.log(deque.peekFront());
// 队尾添加元素
deque.addBack("队尾添加元素");
console.log(deque.peekBack());
// 队首元素等于0的情况往队首添加元素
deque.addFront("队首元素等于0添加元素");
console.log(deque.peekFront());
// 队首元素大于0,往队首添加元素
deque.removeFront();
deque.addFront("队首元素大于0添加元素");
console.log(deque.peekFront());
// 获取队列大小
console.log("队列大小:",deque.size())
// 队列末尾添加元素
deque.addBack("队列末尾添加元素")
// 获取队列中的所有元素
console.log("队列中的所有元素: ",deque.toString())
// 移除队首元素
deque.removeFront();
// 移除队尾元素
deque.removeBack();
// 获取队首元素
console.log("队首元素: ",deque.peekFront());
// 获取队尾元素
console.log("队尾元素: ",deque.peekBack());
执行结果如下:解决问题上面我们实现了队列和双端队列,接下来我们就通过两个例子来理解这两种数据结构。队列实现击鼓传花游戏由于队列经常被应用在计算机领域和我们的现实生活中,就出现了一些队列的修改版本。例如循环队列,我们通过实现一个击鼓传花游戏来理解循环队列。游戏规则击鼓传花游戏的规则如下:一群人围成一个圆圈,放一朵花在任意一个人手里。开始打鼓,拿到花的人会把花尽快的传给旁边的人。鼓声停止,这个时候花在谁手里,谁就退出圆圈,结束游戏。重复这个过程,直至只剩下一个人,这个人就是游戏的获胜者。实现思路知道游戏规则后,我们来捋一下实现思路:声明一个函数,参数为:参与人员数组,多少次为一轮函数内部实例化一个队列,声明淘汰人员列表变量。将参与人员入队(参与人员围成一个圆圈)模拟击鼓传花,以传进来的次数为条件遍历队列,将队列的队顶元素追加至队尾(如果你将花传给了旁边的人,你被淘汰的威胁就立刻解除了)。传进来的次数遍历完成(鼓声停止),队首元素出栈,将队首元素追加至淘汰人员列表中。队列中只剩下一个元素时,剩余元素出队,返回胜利者和淘汰者列表。实现代码实现思路捋清后,我们将上述思路转换为代码:import Queue from "./lib/Queue.ts";
/**
* 击鼓传花函数
* @param nameList 参与人员列表
* @param num 多少次为一轮
* @returns {{eliminates: [], winner: string}}
*/
const hotPotato = (nameList=[], num=0)=> {
// 实例化一个队列
const queue = new Queue();
// 淘汰人员列表
const eliminateList = [];
// 所有参与人员入队
for (let i = 0; i < nameList.length; i++){
queue.enqueue(nameList[i]);
}
// 队列的大小大于1就继续执行
while (queue.size() > 1){
// 模拟击鼓传花,给定一个数字,然后遍历队列,从队列开头移除一项,再将其添加到队列末尾。
for (let i = 0; i < num; i++){
// 将队首元素添加至队尾(如果你将花传给了旁边的人,你被淘汰的威胁就立刻解除了)
queue.enqueue(queue.dequeue());
}
// 鼓声停止,传花结束,将当前手上有花的人(队首元素)淘汰。
eliminateList.push(queue.dequeue());
}
// 游戏结束,返回淘汰者和胜利者
return {
eliminates: eliminateList,
winner: queue.dequeue()
}
}
编写测试代码上面我们实现了击鼓传花游戏的函数,接下来我们来测下我们编写的函数是否正确。const names = ["张三","李四","王五","朱六","郝七","刘八","彭九"];
// 每隔9次淘汰一轮
const result = hotPotato(names,9);
for (let i = 0; i < result.eliminates.length; i++){
const name = result.eliminates[i];
console.log(`${name},在击鼓传花游戏中被淘汰`);
}
console.log(`游戏胜利者: ${result.winner}`);
完整代码请移步:Examples.js执行结果如下:双端队列实现回文检查器回文是正反都能读通的单词、词组、数或一系列字符的序列,例如madam、racecar。实现回文检测有多种方式,最简单的方式为:将字符串反向排列并检查他与原字符是否相同。如果两者相同那么它就是一个回文。我们也可以用栈来解决这个问题,但是如果用数据结构来解决回文问题的话,使用双端队列是最简单的。实现思路回文的规则是正反都能读通,那么我们可以将字符串首字母和末尾字母一一进行比较,如果都相等的话那么这个字符串就是回文。声明一个函数,参数为:要进行检测的字符串去除字符串的空格并将其全转为小写字母遍历字符串,将字符串的每个字符加入双端队列中。遍历队列,队首出队和队尾出队判断队首和队尾的字符是否相等,如果不想等则回文结果为false如果队列的大小大于1且会问结果为true则继续比对队首元素和队尾元素实现代码我们捋清了回文的实现思路后,就可以将上述思路转换为代码了:import Deque from "./lib/Deque.ts";
/**
* 回文函数
* @param sourceString 需要进行判断是否为回文的参数
* @returns {boolean}
*/
const palindromeDetection = function (sourceString) {
// 判断参数是否有效
if(sourceString === undefined || sourceString === null || sourceString.length === 0){
return false;
}
// 实例化一个双端队列
const deque = new Deque();
// 去除字符串的空格并将其转为小写字母
const lowerString = sourceString.toLocaleLowerCase().split(" ").join("");
// 回文校验结果
let isEqual = true;
let firstChar,lastChar;
// 字符串入队
for (let i = 0; i < lowerString.length; i++){
// charAt获取指定索引处的字符
deque.addBack(lowerString.charAt(i));
}
// 队列大小大于1且回文校验结果为true则继续执行校验
while (deque.size() > 1 && isEqual){
// 队首和队尾元素出队
firstChar = deque.removeFront();
lastChar = deque.removeBack();
// 比较队尾元素和队首元素是否相等
if(firstChar !== lastChar){
isEqual = false;
}
}
// 返回验证结果
return isEqual;
}
编写测试代码上述代码实现了回文函数,接下来我们来试下上述代码是否能正常运行const testString = "madam";
const testStr = "admin";
const results = palindromeDetection(testString);
const testStrResult = palindromeDetection(testStr);
if (results){
console.log(`${testString}是回文`)
}else{
console.log(`${testString}不是回文`);
}
if(testStrResult){
console.log(`${testStr}是回文`);
}else{
console.log(`${testStr}不是回文`);
完整代码请移步:Examples.js执行结果如下
小乔学算法
机器学习之模型评估指标
分类模型Accuracy - 准确率其中 ncorrectn_{correct}ncorrect 为被正确分类的样本个数,ntotaln_{total}ntotal 为总体样本个数。准确率是分类问题中最简单也是最直观的评价指标,但存在明显的缺陷。比如,当负样本占99%时,分类器把所有样本都预测为负样本也可以获得99%的准确 率。所以,当不同类别的样本比例非常不均衡时,占比大的类别往往成为影响准确率的最主要因素。做模型或做评估任务时,刚刚开始我们还是会简单的用Accuracy来做评估 —— 因为样本不均衡的情况会比较少。混淆矩阵目前机器学习与大模型正在使用的评估指标混淆矩阵是机器学习中总结分类模型预测结果的情形分析表,以矩阵形式将数据集中的记录按照真实的类别与分类模型预测的类别判断两个标准进行汇总。True Positive(TP):真正类。正类被预测为正类。False Negative(FN):假负类。正类被预测为负类。False Positive(FP):假正类。负类被预测为正类。True Negative(TN):真负类。负类被预测为负类。术语: Reference: 真实值 Prediction: 预测值 T: True P: Positive F: False N: Negative举例Precision - 精准率Precision=TPTP+FPPrecision = \frac{TP}{TP+FP}Precision=TP+FPTP精准率,表示预测结果中,预测为正样本的样本中,正确预测的概率。T、P、F、N 见混淆矩阵 预测为正样本里,有多少判断对的了Recall - 召回率召回率,表示在原始样本的正样本中,被正确预测为正样本的概率。原始数据的正样本中,有多少被判断对的了Precision值和Recall值是既矛盾又统一的两个指标,为了提高Precision值,分类器需要尽量在“更有把握”时才把样本预测为正样本,但此时往往会因为过于保守而漏掉很多“没有把握”的正样本,导致Recall值降低。F1F1-score是Precision和Recall两者的综合,是一个综合性的评估指标。Micro-F1:不区分类别,直接使用总体样本的准召计算f1 score。Macro-F1:先计算出每一个类别的准召及其f1 score,然后通过求均值得到在整个样本上的f1 score。数据均衡,两者均可;样本不均衡,相差很大,使用Macro-F1;样本不均衡,相差不大,优先选择Micro-F1。举例在做分类任务时,一般都要阶段性的输出评估指标 上图是某企业按期向""业务部门”输出的各个指标的列举。回归模型这是因为RMSE是先对误差进行平方的累加后再开方,它其实是放大了较大误差之间的差距。而MAE反应的是真实误差。因此在衡量中使RMSE的值越小其意义越大,因为它的值能反映其最大误差也是比较小的。
小乔学算法
深度学习经典模型之T5
T5(Text-to-Text Transfer Transformer) 是继BERT之后Google的又外力作,它是一个文本到文本迁移的基于Transformer的NLP模型,通过将 所有任务统一视为一个输入文本并输出到文本(Text-to-Text)中,即将任务嵌入在输入文本中,用文本的方式解决各种NLP的任务。T5是由google的Raffel等人于2019年提出了新的预训练模型,其参数量高达110亿,完爆BertLarge模型,且在多项NLP任务中达到SOTA性能,在NLP兴起了“迁移学习技术”热潮,带来了一系列方法、模型和实距的创新。本文从 基本信息、模型架构、多个官方模型以及其T5主要贡献与应用场景对T5做一个简要的介绍.附录是相关的概念基本信息论文: Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer地址: arxiv.org/pdf/1910.10…全称: Text-to-Text Transfer Transformer (单词首字母组成)源码:github.com/google-rese…模型架构T5(Text-to-Text Transfer Transformer) 是基于Transformer结构的序列到序列(Seq2Seq)模型,其主要特点是将多种NLP任务(如翻译、摘要、问答等)转化为一个统一的框架下进行训练。即在不同的具体任务上有不同的prefix指导模型,对预训练目标进行大范围探索,最后得到一个很强的baseline。而我们之后做这方面实验就能参考它的一套参数。三种模型对比为了解决Text-to-Text问题,作者分别使用了三种结构作为实验Encoder-Decoder、Language model和Prefix LM。Language model和Prefix LM比较适用于NLU类问题,但对于NLG,实验结果表明Encoder-Decoder效果更好。所以T5选择了Encoder-Decoder结构。如下图所示:Encoder-Decoder: T5使用的就是Transformer标准的基本结构,分成 Encoder 和 Decoder 两部分,但有所区别:对于Encoder部分,是双向注意力,词与词之间互相可见,之后结果输给Decoder, Decoder部分当前时间步的词汇只能看到之前时间步的词汇。Decoder-only: 在T5的自回归模型中当前时间步词汇只能看到之前时间步词汇。GPT全系列及目前主流大模型均为 Decoder-only 结构。Prefix LM: 通过巧妙的 Attention 设计实现双向注意力与单向注意力的结合,一部分如 Encoder 一样能看到全体信息,一部分如Decoder一样只能看到过去信息。三种注意力机制对比在同一种模型结构下,这三种架构依旧是通过注意力机制的 Mask 控制,下图表示不同注意掩码模式的矩阵。上图中注意掩码模式的矩阵符号自我注意力机制的输入和输出分别表示为x和y。第i行和第j列的深色单元格表示允许自我注意机制在输出时间步i关注输入元素j。浅色单元格表示不允许自我注意机制关注相应的i和j组合。上图中左中右的三个图示说明说明左图:一个完全可见的掩码允许自我注意力机制在每个输出时间步关注完整的输入。中间:因果掩码防止第i个输出元素依赖“未来”的任何输入元素。右图:带有前缀的因果掩码允许自我注意力机制对输入序列的一部分使用完全可见的掩蔽不同架构的一个主要区别因素是模型中不同注意力机制使用的“掩码”。 同样运算复杂度的情况下,Encoder-decoder结构的参数量是其他结构的两倍左右。实验路径明确的基础结构之后,就开始考虑自监督的组织方式、掩码(方式、比例等)如何设计,下图是一个实验路径,最终探索最优结果:High-level approaches高层次方法对比(左图)Prefix LM: 即有条件文本生成,输入完整文本,输出从左到右预测BERT-style: 就是像 BERT 一样将一部分给破坏掉,然后还原出来Deshuffling: 就是将文本打乱,然后还原出来Corrupted strategies对文本一部分进行破坏时的策略(第二图)Mask: 如现在大多模型的做法,将被破坏 token 换成特殊符如 [M];Replace spans: 可以把它当作是把上面 Mask 法中相邻[M] 都合成了一个特殊符,每小段替换一个特殊符,提高计算效率;Drop: 没有替换操作,直接随机丢弃一些字符;Corrupted Rate(第三图)文本的 Mask 比例,论文中挑了 4 个值,10%,15%,25%,50%,最后明确BERT 的 15% 是最最优选择Corrupted Span length(第四图) Replace spans 对多长的 span 进行破坏,选定了4个探索值: 2,3,5,10 这四个值,最后发现span平均长为3结果最好。模型配置模型参数为了适应不同使用场景,T5有五个不同size。Small、Base、Large、3B 和 11B, 模型参数量分别为 6000 万、2.2 亿、7.7 亿、30 亿和 110 亿。执行效果最优总结综上所述,作者发现,一个最优的预训练T5模型应该是这样的:目标函数:Span-corruption,span的平均长度为3,corruption的概率为15%更长的训练步数:采用C4数据集继续训练1M步(bs=2^11),总计约训练了1 万亿个token模型大小base版本:24层,隐层768维,12个注意力头,参数量为220Msmall版本:12层,隐层 512维,8个注意力头,参数量约为60MLarge版本:48层,隐层1024维,16个注意力头,参数量约为770M3B和11B版本:48层,隐层1024维,分别为32/128个注意力头,参数量达到了 2.8B和11B多任务预训练:在非监督预训练时,混合有监督任务可以涨点。微调:在每个任务上微调Beam Search:Beam size为4,长度惩罚为0.6此段中文来自 zhuanlan.zhihu.com/p/580554368 ,但结论归属于T5论文作者,见上上图)T5主要贡献Text-to-Text TransferF5最大的创新在于给整个NLP预训练模型领域提供了一个通用框架,把所有任务都转化成一种文本。即将每个NLP任务,包括NLU和NLG,统一成了"text-to-text"的问题。如下图在翻译、问答、分类等四个不同任务上,添加不同的prefix在输入上,即可通过生成模型得到输出结果。允许在不同的任务集合中使用相同的模型、损失函数、超参数等。C4(Colossal Clean Crawled Corpus)作者从Common Crawl里清出了750GB的训练数据,并取名为"Colossal Clean Crawled Corpus (超大型干净爬取数据)",简称 C4。Common Crawl是一种公开可用的web存档,它通过从已删除的HTML文件删除标记和其他非文本内容来提供“web提取文本”, 该存档大约每月会新产生约20TB的抓取文本数据。但数据主要由诸如菜单、错误消息或重复文本之类的胡言乱语或锅炉板文本组成,且有大量删减的文本或冒犯性语言、占位符文本、源代码等等。应用场景在过去的几年中,随着深度学习技术的发展,NLP领域取得了突破性进展。在众多的NLP模型中,T5模型作为一种强大的语言生成模型,在自然摘要、机器翻译、智能问答和文本分类等任务中表现出色,成为了该领域的研究热点之一。附-文本中涉及的相关深度学习的基本概念SOTA(State of the art) 是指在某一领域做的Performance里最好的modal, 一般是指在一些benchmark的数据集上跑分非常高的那些模型。迁移学习 通俗来讲,就是运用已有的知识来学习新的知识,核心是找到已有知识和新知识之间的相似性,用成语来说就是举一反三。涌现 模型规模达到一定阈值以上后,会在多步算术、大学考试、单词释义等场景的准确性显著提升,称为涌现。思维链(Chain-of-Thought,CoT) 是指通过让大语言模型(LLM)将一个问题拆解为多个步骤,一步一步分析,逐步得出正确答案。需指出,针对复杂问题,LLM直接给出错误答案的概率比较高。思维链可以看成是一种指令微调。NLU和NLG:是指NLP(自然语言处理)的两个主要核心任务。NLU是所有支持机器理解文本内容的方法模型或任务的总称,即能够进行常见的文本分类、序列标注、信息抽取等任务。NLG(自然语言生成) 将非语言格式的数据转换成人类可以理解的语言格式。[参考]blog.csdn.net/qq_18555105…zhuanlan.zhihu.com/p/580554368arxiv.org/pdf/1910.10…
小乔学算法
TypeScript实现二叉堆
前言二叉堆是计算机科学中一种非常著名的数据结构,由于它能高效、快速地找出最大值和最小值因此常被用于优先队列和堆排序算法。本文将详解二叉堆并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。写在前面本文重点讲解堆如何实现,对堆这种数据结构不了解的开发者请移步我的另一篇文章:数据结构:堆实现思路二叉堆是一种特殊的二叉树,二叉堆也叫堆,它有以下两个特性:它是一颗完全二叉树二叉堆不是最小堆就是最大堆完全二叉树一颗完全二叉树,它的每一层都有左侧和右侧子节点(除过最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点。下图描述了一颗完全二叉树:最小堆和最大堆最小堆:所有的节点都小于等于它的子节点最大堆:所有的节点都大于等于它的子节点下图描述了最大堆和最小堆实现二叉堆二叉堆有两种表现方式:像二叉树一样用节点表示使用数组表示,通过索引值检索父节点、左侧、右侧节点的值下图描述了两种不同的表示方式操作堆节点我们使用数组来表示二叉堆,对于给定位置(index)的节点,我们可以对其进行如下操作:获取给定节点的左侧子节点位置:2 * index + 1获取给定节点的右侧子节点位置:2 * index + 2获取给定节点的父节点位置:(index - 1) / 2向堆中插入数据向堆中插入数据(insert)是指将数据插入堆的底部叶节点再执行上移(siftUp), 表示我们将要把这个数据和它的父节点进行交换,直到父节点小于这个插入的值。insert方法接收一个参数:要插入的数据需要对插入的数据进行非空判断,如果为null则返回false数据不为空时,往数组(heap)的末尾追加要插入的数据插入完成后,执行siftUp操作,将数据移动至合适的位置上移完成后,则成功的向堆中插入了一条数据,返回true上移操作的实现如下:siftUp方法接收一个参数:插入数据的索引位置(index)获取当前要插入数据的父节点位置(parent)index大于0且heap[parent] > heap[index],交换parent和index位置的节点更新index和parent的值,继续进行节点交换直至heap[parent] < heap[index]交换的实现如下:swap接收三个参数:要操作的数组,交换的元素位置,被交换的元素位置声明一个临时变量temp,赋值交换的元素交换的元素赋值为被交换的元素被交换的元素赋值为temp接下来我们用一个例子来描述上述插入过程,如下图所示为一个最小堆,我们要插入一个新的节点2。找到它的父节点12,比较12与2的大小,12 > 2,进行位置互换此时2的父节点是5,5 > 2,进行位置交换2此时2的父节点是1,1 < 2,插入完成寻找堆中的最大值或最小值在最小堆中数组的0号元素就是堆的最小值在最大堆中数组的0号元素就是堆的最大值导出堆中的最小值或最大值移除最小值(最小堆)或最大值(最大堆)表示移除数组中的第一个元素(堆的根节点)。在移除后,我们需要将堆的最后一个元素移动至根部并执行下移(siftDown)函数,表示我们将交换元素直到堆的结构正常。extract函数不接收参数如果堆为空则返回undefined如果堆的长度为1,直接返回堆顶元素否则,声明一个变量保存堆顶元素执行下移函数调整堆结构返回刚才保存堆堆顶元素下移操作的实现:siftDown函数接收一个参数:需要调整的元素位置(index)声明一个变量(element)保存index获取index的左子节点(left)、右子节点(right)、堆的大小(size)如果heap[element] > heap[left],则更新element的值为left如果heap[element] > heap[right],则更新element的值为right如果index !== element,则交换index和element位置的元素,继续执行siftDown函数接下来,我们通过一个例子来讲解上述执行过程,下图描述了一个最小堆我们导出堆顶节点1此时,我们需要把堆的最后一个节点放到堆顶此时,进行下移操作,比较12和其左子节点2的大小,12 > 2,交换节点位置继续进行下移操作,比较12和其左子节点5的大小,12 > 5,交换节点位置此时index === element,下移操作完成,堆节点导出完成实现最大堆上述操作我们实现了一个最小堆,最大堆与最小堆的别就在于节点的比较,因此我们只需要继承最小堆,重写比对函数,将原来的a与b比较,改为b与a比较即可。实现代码上面我们讲解了堆的概念,分析了的实现思路,接下来我们将上述实现思路转化为代码新建Heap.ts文件声明MinHeap类,声明堆、比对函数、初始化堆export class MinHeap<T> {
// 用数组来描述一个堆
protected heap: T[];
constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
this.heap = [];
}
}
实现获取左、右、父节点函数 // 获取左子节点的位置
protected getLeftIndex(index: number): number {
return 2 * index + 1;
}
// 获取右子节点的位置
protected getRightIndex(index: number): number {
return 2 * index + 2;
}
// 获取父节点的位置
protected getParentIndex(index: number): number | undefined {
if (index === 0) {
return undefined;
}
return Math.floor((index - 1) / 2);
}
实现插入函数 insert(value: T): boolean {
if (value != null) {
// 向堆的叶结点添加元素,即数组的尾部
this.heap.push(value);
// 进行上移操作,即上移节点至合适的位置
this.siftUp(this.heap.length - 1);
return true;
}
return false;
}
// 实现上移函数
protected siftUp(index: number): void {
// 获取父节点位置
let parent = <number>this.getParentIndex(index);
// 插入的位置必须大于0,且它的父节点大于其本身就执行循环里的操作
while (index > 0 && this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN) {
// 交换元素的位置
this.swap(this.heap, parent, index);
// 修改当前插入值的位置为它的父节点,重新获取父节点的位置,即重复这个过程直到堆的根节点也经过了交换
index = parent;
parent = <number>this.getParentIndex(index);
}
}
// 实现交换数组元素位置函数
protected swap(array: T[], exchangeElement: number, exchangedElement: number): void {
// 用一个临时变量保存交换元素
const temp = array[exchangeElement];
// 将被交换元素赋值给交换元素
array[exchangeElement] = array[exchangedElement];
// 将第一步保存的临时变量赋值给被交换元素
array[exchangedElement] = temp;
}
实现寻找堆的最小值函数 findMinimum(): T | undefined {
// 返回数组的最小元素
return this.isEmpty() ? undefined : this.heap[0];
}
// 判断堆是否为空
isEmpty(): boolean {
return this.size() === 0;
}
实现导出堆的最小值函数 extract(): T | undefined {
if (this.isEmpty()) {
return undefined;
}
if (this.size() === 1) {
// 返回数组的第一个元素
return this.heap.shift();
}
const removedValue = this.heap.shift();
// 执行下移操作
this.siftDown(0);
return removedValue;
}
// 下移操作
protected siftDown(index: number): void {
// 保存当前插入值的位置
let element = index;
// 获取其左、右子节点的位置
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
const size = this.size();
// 元素有效,且当前元素大于其左子节点
if (left < size && this.compareFn(this.heap[element], this.heap[left]) === Compare.BIGGER_THAN) {
element = left;
}
// 元素有效,当前元素大于其右子节点
if (right < size && this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN) {
element = right;
}
// 找到最小子节点的位置,校验它的值是否和element相同
if (index !== element) {
// 如果不相同将它和最小的element进行交换
this.swap(this.heap, index, element);
// 递归执行
this.siftDown(element);
}
}
完整代码地址:Heap.ts堆排序堆的一种应用就是堆排序,此处不讲解堆排序的实现思路,对堆排序不了解的开发者请移步我的另一篇文章: 排序算法:堆排序的理解与实现实现堆排序函数 heapSort(array: T[]): void {
// 构建堆
this.buildHeap(array);
// 从堆的末尾开始遍历,将遍历到的元素与0好元素进行交换,然后执行下移操作
for (let i = array.length - 1; i >= 0; i--) {
this.swap(array, i, 0);
this.heapify(array, i, 0);
}
}
// 构建堆
private buildHeap(array: T[]) {
// 获取最后一个节点的位置
const last = array.length - 1;
const lastParent = <number>this.getParentIndex(last);
// 从最后一个节点的父节点开始进行heapify操作
for (let i = lastParent; i >= 0; i--) {
this.heapify(array, array.length, i);
}
}
// 交换节点
private heapify(array: T[], size: number, index: number) {
// 递归基线条件
if (index >= size) {
return false;
}
// 找到当前要操作节点的左、右子树
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
// 保存当前要操作节点的位置
let element = index;
// 如果当前要操作节点的左子节点大于其父节点,更新element的值
if (left < size && this.compareFn(array[left], array[element]) === Compare.BIGGER_THAN) {
element = left;
}
// 如果当前要操作节点的右子节点大于其父节点,更新element的值
if (right < size && this.compareFn(array[right], array[element]) === Compare.BIGGER_THAN) {
element = right;
}
// element的位置不等于当前要操作节点,交换元素位置,递归执行
if (element !== index) {
this.swap(array, element, index);
this.heapify(array, size, element);
}
}
编写测试代码接下来我们测试下上述代码是否正常执行import { MinHeap, MaxHeap } from "./lib/Heap.ts";
const minHeap = new MinHeap();
minHeap.insert(13);
minHeap.insert(10);
minHeap.insert(5);
minHeap.insert(7);
minHeap.insert(4);
minHeap.insert(17);
console.log("堆(min)的所有元素", minHeap.getIsArray());
console.log("堆(min)的最小值", minHeap.findMinimum());
console.log(minHeap.extract());
console.log(minHeap.getIsArray());
console.log("---------------------------------------");
const maxHeap = new MaxHeap();
maxHeap.insert(13);
maxHeap.insert(10);
maxHeap.insert(5);
maxHeap.insert(7);
maxHeap.insert(4);
maxHeap.insert(17);
console.log("堆(max)的所有元素", maxHeap.getIsArray());
console.log(maxHeap.extract());
console.log("堆(max)的最大值", maxHeap.findMinimum());
console.log("---------------------------------------");
const arrayTest = [12, 15, 17, 18, 4, 5, 1, 7, 19, 20];
minHeap.heapSort(arrayTest);
console.log(arrayTest);
小乔学算法
集合的实现
前言集合是一种不允许值重复的顺序数据结构。本文将详解集合的实现思路并使用TypeScript实现类似于ES6中的Set集合以及集合的基本运算,欢迎各位感兴趣的开发者阅读本文。实现思路集合有一个很重要的特点:它的内部元素不会重复,因此我们可以使用JavaScript中对象来描述结合。基础集合的实现一个较为完善的集合类必须具备:判断元素是否在集合中、向集合中添加元素、删除集合中的元素等基础函数,接下来我们来分析下这些函数的实现思路。判断元素是否在集合中(has)集合中添加元素(add)删除集合中的元素(delete)清空集合(clear),将集合指向空对象即可。获取集合大小(size),声明一个变量来存储集合大小,遍历集合,集合大小自增,结束遍历返回集合大小。获取集合中的所有元素集合运算的实现集合是数学中基础的概念,在计算机领域也非常重要。接下来我们来看看集合相关运算的实现思路,实现之前我们先用图解的形式描述下常用的几个集合运算。数学公式图解并集(A∪B),将给定集合中的元素进行合并,存进一个新集合中,返回这个新集合,该集合定义如下,意思为:X(元素)存在于A中,或X存在于B中。交集(A∩B),找出给定集合中的相同的元素,将找到的相同元素存进一个新集合中,返回这个新集合,该集合定义如下,意思为:X(元素)存在于A中,且X存在于B中。差集(A - B),给定两个集合,找出集合中不存在于另一个集合中的元素将其存进一个新集合里,返回这个新集合,该集合定义如下:意思为:X(元素)存在于A中,且X不存在于B中。子集(A⊆B),给定了两个集合,判断其中一个集合中的元素是否都存在于另一个集合中,如果又一个不存在则返回false,该集合定义如下:集合A中的每一个X(元素),也需要存在于集合B中。实现思路解析并集运算(union),给定两个集合,返回一个包含两个集合中所有元素的新集合。 声明并集集合变量,值为Set类型遍历当前实例集合中的所有元素,将其放进并集变量集合中遍历传进来的集合参数,将其放进并集变量集合中返回并集变量集合交集运算(intersection),给定两个集合,返回一个包含两个集合中共有元素的新集合 声明交集集合变量,值为Set类型获取当前实例集合中的所有元素存进当前集合数组变量中,获取参数集合中的所有元素存进参数结合数组中假设当前集合数组中的元素最多将其放到一个变量里,假设参数集合中的元素最少将其放到一个变量里。如果参数集合中的元素个数比当前元素集合中的个数多,则交换两个变量存储的集合元素数组遍历参数最少的集合变量数组,判断当前遍历到的元素是否在参数最多的集合元素数组里,如果存在则向交集变量中添加当前元素返回交集集合变量集合差集运算(difference),返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。 声明差集集合变量,值为Set类型遍历当前实例集合中的元素,判断参数集合中是否包含当前遍历到的元素,如果不包含,则向差集集合里添加当前元素返回差集集合变量子集运算,验证一个给定集合是否是另一个集合的子集 声明一个子集判断变量,用于判断参数集合是否在当前集合中,默认值为true遍历当前实例集合中的元素,判断当前遍历到的元素是否都存在于参数集合中,如果遍历到的元素有一个不存在于参数集合中则将子集判断变量设为false返回子集判断变量实现代码我们捋清实现思路后,接下来我们将上述实现思路转换为代码:新建一个Set.ts文件,用于实现集合类在集合类中声明一个class,用于存放我们需要实现的集合函数export default class Set<T>{
}
在class类中声明构造器以及实现集合函数需要的变量interface setItemsType<T> {
[propName: string]: T;
}
private items: setItemsType<T>;
constructor() {
this.items = {};
}
实现判断元素是否存在于集合中函数(has) has(element: any){
// Object原型有hasOwnProperty方法用于判断对象是否有特定属性
return Object.prototype.hasOwnProperty.call(this.items,element);
}
实现向集合中添加元素函数(add) add(element: any){
if(!this.has(element)){
this.items[element] = element;
return true;
}
return false;
}
实现删除集合中元素函数(delete) delete(element: any){
if(this.has(element)){
delete this.items[element];
return true;
}
return false;
}
清空集合(clear) clear(){
this.items = {};
}
获取集合大小(size) size(){
let count = 0;
for (let key in this.items){
if(this.items.hasOwnProperty(key)){
count++;
}
}
return count;
}
获取集合中的所有元素(values) values(){
let values = [];
for (let key in this.items){
if(this.items.hasOwnProperty(key)){
values.push(key);
}
}
return values;
}
并集运算(union) union(otherSet: Set<T>){
// 声明并集变量
const unionSet = new Set();
this.values().forEach(value => unionSet.add(value));
otherSet.values().forEach(value => unionSet.add(value));
return unionSet;
}
交集运算(intersection) intersection(otherSet: Set<T>) {
// 声明交集变量
const intersectionSet = new Set();
// 获取当前实例集合中的元素
const values = this.values();
// 获取另一个集合中的元素
const otherValues = otherSet.values();
// 假设当前实例集合中的元素最多
let biggerSet = values;
// 假设另一个元素集合中的元素最少
let smallerSet = otherValues;
// 如果另一个集合中的元素个数比当前元素集合中的个数多,则交换变量
if(otherValues.length - values.length > 0){
biggerSet = otherValues;
smallerSet = values;
}
// 遍历元素最少的集合数组,节约性能开销
smallerSet.forEach(value => {
if (biggerSet.includes(value)){
intersectionSet.add(value);
}
});
// 返回交集集合
return intersectionSet;
}
差集运算(difference) difference(otherSet: Set<T>) {
// 声明差集变量
const differenceSet = new Set();
// 遍历当前实例中的集合
this.values().forEach(value => {
// 如果当前遍历到元素不存在与另一个集合中,则将档当前元素添加进差集变量里
if(!otherSet.has(value)){
differenceSet.add(value);
}
});
// 返回差集变量
return differenceSet;
} isSubsetOf(otherSet: Set<T>) {
if(this.size() > otherSet.size()){
return false;
}
let isSubset = true;
this.values().every(value => {
if(!otherSet.has(value)){
isSubset = false;
return false;
}
return true;
});
return isSubset;
}
子集运算(isSubsetOf) isSubsetOf(otherSet: Set<T>) {
if(this.size() > otherSet.size()){
return false;
}
let isSubset = true;
this.values().every(value => {
if(!otherSet.has(value)){
isSubset = false;
return false;
}
return true;
});
return isSubset;
}
完整代码请移步:Set.ts编写测试代码接下来,我们对上述实现的Set类进行测试,确保每个函数都正常工作。const set = new Set();
set.add(11);
set.add(12);
set.add(13);
set.delete(11);
console.log(set.size())
console.log("获取集合中的元素",set.values());
set.clear();
console.log("获取集合大小",set.size());
// 集合运算
const A = new Set();
A.add(10);
A.add(11);
A.add(12);
A.add(13);
A.add(1);
A.add(2);
const B = new Set();
B.add(1);
B.add(2);
B.add(3);
// 求A和B的并集
console.log("A和B的并集",A.union(B).values());
// 求A和B的交集
console.log("A和B的交集",A.intersection(B).values());
//求A和B的差集
console.log("A和B的差集",A.difference(B).values());
// 求C是否为D的子集
const C = new Set();
C.add(1);
C.add(2);
C.add(3);
C.add(4);
C.add(5);
const D = new Set();
D.add(1);
D.add(2);
D.add(3);
D.add(9)
console.log(D.isSubsetOf(C));
完整代码请移步:SetTest.js
小乔学算法
TypeScript实现八大排序与搜索算法
前言我们在页面上渲染数据时,通常会根据特定规则来对数据进行一个排序,然后再将其渲染到页面展示给用户。那么对数据进行排序有很多种方式,哪一种效率高? 哪一种稳定性好?那一种占用内存小?本文将详解经典的八大排序算法以及三种搜索算法,并用TypeScript将其实现,欢迎各位对上述问题迷惑的开发者阅读本文。排序算法我们先来学习下排序算法,八大排序包括:冒泡排序、选择排序、插入排序、归并排序、快速排序、计数排序、桶排序、基数排序其中有几个排序我在之前的文章中已经讲解了其图解实现,本文将注重讲解其实现,另外几篇文章链接如下:排序算法:冒泡排序排序算法:选择排序排序算法:插入排序排序算法:归并排序排序算法:快速排序冒泡排序冒泡排序是排序算法中最简单、最好理解的一个排序算法,但是从运行时间的角度来看,他的效率是最低的。本文中所有函数实现的代码地址: Sort.ts实现思路它会比较相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序。为了让排序算法更灵活,不仅仅只是用于数字的排序,因此本文中讲解的排序算法都会有一个比对函数作为参数传进来,用于定义两个值的比较方式。声明一个函数其接受两个参数:待排序数组 & 比对函数第一层循环i:从待排序数组的0号元素开始遍历数组,遍历到最后一位,用于控制数组需要经过多少轮排序第二层循环j:从数组的0号元素开始遍历,遍历至数组的倒数第二位元素再减去第一层循环i。比较大小,在第二层循环中,将当前遍历到的元素和其下一个元素比较大小,如果 j > j + 1就交换两个元素的位置。我们通过一个例子来描述下上述思路,如下图所示,将一个乱序数组执行冒泡排序后,将其从小到大排列。实现代码为了让函数便于维护,我们新建一个类名为Sort,本文中实现的所有函数都为这个类内部的方法。为了函数的可扩展性,我们需要实现一个默认的比对函数,此比对函数作用域本文中的所有排序方法export function defaultCompare<T>(a: T, b: T): number {
if (a === b) {
return Compare.EQUALS;
} else if (a > b) {
return Compare.BIGGER_THAN;
} else {
return Compare.LESS_THAN;
}
}
// 枚举类:定义比对返回值
export enum Compare {
LESS_THAN = -1,
BIGGER_THAN = 1,
EQUALS = 0
}
在类中声明需要的变量,并在构造器中声明需要传的参数类型,此处适用于本文实现的所有函数 /**
* 排序算法
* @param array 需要进行排序的数组
* @param compareFn 比对函数
*/
constructor(private array: T[] = [], private compareFn: ICompareFunction<T> = defaultCompare) {}
实现冒泡排序 // 冒泡排序
bubbleSort(): void {
// 获取数组长度
const { length } = this.array;
for (let i = 0; i < length; i++) {
// 从数组的0号元素遍历到数组的倒数第2号元素,然后减去外层已经遍历的轮数
for (let j = 0; j < length - 1 - i; j++) {
// 如果j > j + 1位置的元素就交换他们两个元素的位置
if (this.compareFn(this.array[j], this.array[j + 1]) === Compare.BIGGER_THAN) {
this.swap(this.array, j, j + 1);
}
}
}
}
编写测试代码我们将上图中的例子放在代码中,测试下执行结果是否正确。const array = [12, 6, 3, 4, 1, 7];
const sort = new Sort(array);
sort.bubbleSort();
console.log(array.join());
选择排序选择排序是一种原址比较排序算法,它的大致思路是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值将其放在第二位,依次类推。实现思路声明一个辅助变量:indexMin用于存储数组中的最小值第一层循环i,从数组的0号元素遍历到数组的倒数第二位。indexMin默认赋值为i,用于控制轮数第二层循环j,从数组的i号元素遍历到数组的末尾,用于寻找数组的最小值,如果indexMin位置的元素大于j号位置的元素就覆盖indxMin的值为j在第二层循环结束后,比较i与indexMin是否相等,如果不相等就交换两个元素的位置。下图中的示例描述了上述思路实现代码 // 选择排序
selectionSort(): void {
const { length } = this.array;
// 声明一个变量用于存储最小元素的位置
let indexMin = 0;
for (let i = 0; i < length; i++) {
// 初始值为外层循环当前遍历到的位置i
indexMin = i;
for (let j = i; j < length; j++) {
// 如果当前遍历到元素小于indexMin位置的元素,就将当前遍历到的位置j赋值给indexMin
if (this.compareFn(this.array[indexMin], this.array[j]) === Compare.BIGGER_THAN) {
indexMin = j;
}
}
if (i !== indexMin) {
this.swap(this.array, i, indexMin);
}
}
}
编写测试代码我们将图中的示例,放在代码中,测试排序函数是否都正确执行。const array = [12, 6, 3, 4, 1, 7];
const sort = new Sort(array);
// const result = sort.radixSort(array);
// console.log(result.join());
sort.selectionSort();
console.log(array.join());
插入排序插入排序会将数组分为已排序区域和未排序区域,将未排序区域的数组项和已排序区域的数组进行大小比较,确立要插入的位置,然后将其插入到对应的位置。实现思路第一层循环i:从数组的1号元素开始,遍历到数组末尾,因为我们会先假设0号元素已经排好序,所以从1号元素开始。用一个临时变量temp存储当前i号位置的元素,用一个变量j存储iwhile循环: j > 0且j - 1位置的元素大于temp,就把j 位置的值设置为j - 1 位置的值,最后j--,继续下一轮遍历。while循环结束后,将temp放到正确的位置array[ j ]如下图所示,我们通过一个例子描述了上述插入排序的过程实现代码接下来我们将上述思路转换为代码。 // 插入排序
insertionSort(array: T[] = this.array): void {
const { length } = array;
let temp;
// 假设0号元素已经排好序,从1号元素开始遍历数组
for (let i = 1; i < length; i++) {
// 声明辅助变量存储当前i的位置以及其对应的值
let j = i;
temp = array[i];
// j大于0且j-1位置的元素大于i号位置的元素就把j-1处的值移动到j处,最后j--
while (j > 0 && this.compareFn(array[j - 1], temp) === Compare.BIGGER_THAN) {
array[j] = array[j - 1];
j--;
}
// 将temp放到正确的位置
array[j] = temp;
}
}
编写测试代码我们将图中的例子放在代码里,执行下查看排序函数是否正常运行。const array = [12, 6, 3, 4, 1, 7];
const sort = new Sort(array);
sort.insertionSort(array);
console.log(array.join());
归并排序归并排序是一个可以实际使用的排序算法,在JavaScript中Array类定义了一个Sort函数,用以排序JavaScript数组。ECMScript没有定义用哪个排序算法,所以浏览器厂商可以自己去实现算法。谷歌浏览器的V8引擎使用快速排序实现,火狐浏览器使用归并排序实现。实现思路归并排序是一个分而治之算法,就是将原始数组切分成比较小的数组,直到每个小数组只有一个位置,接着将小数组归并成比较大的数组,直到最后只有一个排序完毕的大数组。由于是分治法,归并排序也是递归的。我们要将算法分为两个函数:一个用于将函数分割成小数组,一个用于将小数组合并成大数组。我们先来看看主函数,将数组分割成小数组。递归终止条件:由于是递归,所以我们需要先设立递归终止条件,当数组的长度大于1时就进行归并排序。获取数组的中间值: 我们通过数组的中间值来切割数组,将其分成左、右两部分。对左右两部分继续执行分割合并数组: 我们将数组分割完后,对小数组进行排序,然后将其合并成大数组并返回。接下来,我们再来看下合并函数的实现合并函数接收两个参数:左、右数组声明三个辅助变量: i, j, result,分别表示左、右数组的指针以及存储合并后的数组如果i < left.length && j < right.length,代表指针数组尚未排序完,因此执行下述逻辑最后,将left或right数组的剩余项添加进result中下图用一个例子描述了上述归并排序的过程实现代码接下来,我们将上述思路转换为代码。 // 归并排序
mergeSort(array: T[] = this.array): T[] {
if (array.length > 1) {
const { length } = array;
// 获取中间值
const middle = Math.floor(length / 2);
// 递归填充左右数组
const left = this.mergeSort(array.slice(0, middle));
const right = this.mergeSort(array.slice(middle, length));
// 合并左右数组
array = this.merge(left, right);
}
return array;
}
private merge(left: T[], right: T[]) {
let i = 0;
let j = 0;
const result: T[] = [];
while (i < left.length && j < right.length) {
// 如果left[i] < right[j]将left[i]加进result中,随后i自增,否则把right[j]加进result中,随后j自增
result.push(this.compareFn(left[i], right[j]) === Compare.LESS_THAN ? left[i++] : right[j++]);
}
// 将left或right数组的剩余项添加进result中
return result.concat(i < left.length ? left.slice(i) : right.slice(j));
}
编写测试代码我们将上图中的例子放到代码中执行用以验证我们的函数是否正确执行const array = [12, 6, 3, 4, 1, 7];
const sort = new Sort(array);
const result = sort.mergeSort();
console.log(result.join());
快速排序快速排序与归并排序一样都是可用作实际应用的算法,它也是使用分而治之的方法,将原始数组分为较小的数组,但它没有像归并排序那样将他们分割开。实现思路我们需要三个函数:主函数、排序函数、切分函数。主函数(quickSort),调用排序函数,返回排序函数最终排好的数组。排序函数(quick),接收3个参数: 待排序数组(array)、数组的左边界(left)、数组的右边界(right)划分函数(partition),与排序函数一样,它也接收3个参数。实现代码接下来我们将上述思路转换为代码: /**
*
* @param array 待排序数组
* @param left 左边界
* @param right 右边界
* @private
*/
private quick(array: T[], left: number, right: number) {
// 该变量用于将子数组分离为较小值数组和较大值数组
let index;
if (array.length > 1) {
// 对给定子数组执行划分操作,得到正确的index
index = this.partition(array, left, right);
// 如果子数组存在较小值的元素,则对该数组重复这个过程
if (left < index - 1) {
this.quick(array, left, index - 1);
}
// 如果子数组存在较大值的元素,也对该数组重复这个过程
if (index < right) {
this.quick(array, index, right);
}
}
return array;
}
// 划分函数
private partition(array: T[], left: number, right: number): number {
// 从数组中选择一个值做主元,此处选择数组的中间值
const pivot = array[Math.floor((right + left) / 2)];
// 创建数组引用,分别指向左边数组的第一个值和右边数组的第一个值
let i = left;
let j = right;
// left指针和right指针没有相互交错,就执行划分操作
while (i <= j) {
// 移动left指针直至找到一个比主元大的元素
while (this.compareFn(array[i], pivot) === Compare.LESS_THAN) {
i++;
}
// 移动right指针直至找到一个比主元小的元素
while (this.compareFn(array[j], pivot) === Compare.BIGGER_THAN) {
j--;
}
// 当左指针指向的元素比主元大且右指针指向的元素比主元小,并且左指针索引没有右指针索引大时就交换i和j号元素的位置,随后移动两个指针
if (i <= j) {
this.swap(array, i, j);
i++;
j--;
}
}
// 划分结束,返回左指针索引
return i;
}
编写测试代码我们通过一个例子,来测试下上述代码是否都正确执行。const array = [12, 6, 3, 4, 1, 7];
const sort = new Sort(array);
const result = sort.quickSort();
console.log(result.join());
计数排序计数排序是一个分布式排序,它是用来排序整数的优秀算法。分布式排序使用已组织好的辅助数据结构,然后进行合并,得到排好序的数组。计数排序使用一个用来存储每个元素在原始数组中出现次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可迭代以构建排序后的结果数组。实现思路找到要排序数组的最大值以上一步找到的最大值+1为长度创建一个计数数组遍历要排序的数组,以当前遍历的元素为索引,寻找计数数组中对应的位置将其初始化为0,如果此处位置有相同元素的话就自增遍历计数数组,判断当前遍历到的元素是否大于0,如果大于0就取当前遍历到的索引,替换待排序数组中的元素接下来我们通过一个例子来讲解下上述过程,如下图所示:实现代码接下来我们将上述思路转换为代码 countingSort(array: number[]): number[] {
// 待排序数组为空或只有一个元素则不用排序
if (array.length < 2) {
return array;
}
// 找到待排序数组中的最大值
const maxValue = this.findMaxValue(array);
// 创建计数数组,数组长度为待排序数组的最大值+1
const counts = new Array(maxValue + 1);
// 遍历待排序数组,为计数数组赋值
array.forEach((element) => {
// 以当前遍历到的元素值为索引将对应位置元素值初始化为0
if (!counts[element]) {
counts[element] = 0;
}
// 当前位置的值进行自增,顺便应对数组中有重复值的情况,有重复值时当前位置的值必定大于1
counts[element]++;
});
// 声明一个变量用于数组最终排序
let sortedIndex = 0;
// 遍历计数数组,根据计数数组的元素位置对待排序数组进行排序
counts.forEach((count, i) => {
// 如果当前遍历到的元素值大于0,则执行替换操作进行排序
while (count > 0) {
// 将当前元素索引赋值给array的sortedIndex号元素,随后sortedIndex自增
array[sortedIndex++] = i;
// 当前元素值自减,如果其值大于1,证明此处有重复元素,那么我们就继续执行while循环
count--;
}
});
// 最后,排序完成,返回排序好的数组
return array;
}
编写测试代码我们将上述图中的例子放进代码中执行,看看我们写的函数是否正确执行。const array = [12, 6, 3, 4, 1, 7];
const sort = new Sort(array);
const result = sort.countingSort(array);
console.log(result.join());
桶排序桶排序也是一种分布式排序算法,它将元素分为不同的桶,再使用一个简单的排序算法,例如插入排序,来对每个桶进行排序,最后,它将所有的桶合并为结果数组。实现思路首先,我们需要指定需要多个桶来排序各个元素,默认情况,我们会使用5个桶。桶排序在所有元素平分到各个桶中时的表现最好。如果元素非常稀疏,则使用更多的桶会更好。如果元素非常密集,则使用较少的桶会更好。因此我们为了算法的效率,会让调用者根据实际需求将桶的数量作为参数传进来。我们将算法分为两个部分:创建桶,并将桶分布到不同的桶中对每个桶中的元素执行排序算法并将所有桶合并成排序好后的结果数组我们先来看看创建桶的思路声明创建桶函数(createBuckets),接收两个参数:待排序数组(array),桶大小(bucketSize)计算array中的最大值(maxValue)和最小值(minValue)计算每个需要的桶数量,公式为:(maxValue - minValue) / bucketSize)+1声明一个二维数组buckets,用于存放所有桶根据桶数量,初始化每个桶遍历array,将每个元素分布到桶中将桶返回接下来我们来看看对每个桶里的元素进行排序的思路创建排序桶函数(sortBuckets),接收一个参数: 桶buckets需要一个辅助数组sortedArray,用于存放排序好的结果数组遍历sortedArray最后,返回sortedArray我们通过一个例子,来讲解上述思路,如下图所示实现代码接下来,我们将上述思路转换为代码。 // 桶排序
bucketSort(array: number[], bucketSize = 5): T[] | number[] {
if (array.length < 2) {
return array;
}
// 创建桶,对桶进行排序
return this.sortBuckets(<[][]>this.createBuckets(array, bucketSize));
}
/**
* 创建桶
* @param array 待排序数组
* @param bucketSize 桶大小,即每个桶里的元素数量
*
* @return 二维数组类型的桶
*/
private createBuckets = (array: number[], bucketSize: number): number[][] => {
// 计算数组最大值与最小值
let minValue = array[0];
let maxValue = array[0];
for (let i = 1; i < array.length; i++) {
if (array[i] < minValue) {
minValue = array[i];
} else if (array[i] > maxValue) {
maxValue = array[i];
}
}
// 计算需要的桶数量,公式为: 数组最大值与最小值的差值与桶大小进行除法运算
const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
// 用于存放桶的二维数组
const buckets: number[][] = [];
// 计算出桶数量后,初始化每个桶
for (let i = 0; i < bucketCount; i++) {
buckets[i] = [];
}
// 遍历数组,将每个元素分布到桶中
for (let i = 0; i < array.length; i++) {
// 计算需要将元素放到哪个桶中,公式为: 当前遍历到的元素值与数组的最小值的差值与桶大小进行除法运算
const bucketIndex: number = Math.floor((array[i] - minValue) / bucketSize);
// 将元素放进合适的桶中
buckets[bucketIndex].push(array[i]);
}
// 将桶返回
return buckets;
};
// 对每个桶进行排序
private sortBuckets(buckets: T[][]): T[] {
const sortedArray: T[] = [];
for (let i = 0; i < buckets.length; i++) {
if (buckets[i] != null) {
// 调用插入排序
this.insertionSort(buckets[i]);
// 将排序好的桶取出来,放进sortedArray中
sortedArray.push(...buckets[i]);
}
}
return sortedArray;
}
编写测试代码我们将上述图中的例子放进代码中看其是否能正确执行。const array = [12, 5, 6, 7, 8, 9, 11, 3, 4, 19];
const sort = new Sort(array);
const result = sort.bucketSort(array);
console.log(result.join());
基数排序基数排序也是一个分布式排序算法,它根据数字的有效位或基数将整数分布到桶中。比如,十进制数,使用的基数是10.因此,算法将会使用10个桶用来分布元素并且首先基于各位数字进行排序,然后基于十位数字,然后基于百位数字,以此类推。实现思路基数排序也是用来排序整数,因此我们从最后一位开始排序所有的数。首先,只会基于最后一位有效位对数字进行排序,在下次迭代时,我们会基于第二个有效位进行排序(十位数字),然后是第三个有效位(百位数字),以此类推。我们继续这个过程直到没有待排序的有效位,因此我们需要知道数组中的最小值和最大值。实现基数排序我们需要一个辅助函数(countingSortForRadix),即根据有效位对数组进行排序。接下来,我们先来看下基数排序主函数的实现思路。创建基数排序函数(radixSort),接受2个参数:待排序数组array,基数radixBash首先,获取array的最大值maxValue和最小值minValue声明一个辅助变量significantDigit,用于存储当前有效数字,默认从最后一位有效数字,即1计算有效位,公式为:(maxValue - minValue) / significantDigit,计算出的值如果大于等于1执行下述逻辑: 以当前有效位作为参数调用singnificantDigit函数对数组进行排序 当前有效数字*基数,继续执行上述过程最后,执行完成返回排序好多的数组接下来,我们来看下基于有效位进行排序的函数实现思路。创建countingSortForRadix函数,接受4个参数:待排序数组array,基数radixBase、有效位significantDigit、数组的最小值minValue声明桶索引bucketsIndex以及桶buckets以及辅助数组aux通过radixBase来初始化桶,默认初始化为0遍历array,基于有效位计算桶索引执行计数排序 计算桶索引,公式为:((array[i] - minValue) / significantDigt) % radixBase 根据桶索引,执行计数操作,即buckets[bucketsIndex++]计算累积结果得到正确的计数值,从1开始遍历到radixBase位置。 buckets[i]等于buckets[i] 加上buckrts[i - 1]的值计数完成,遍历array将值移回原始数组中,用aux辅助数组来存储 计算当前元素的桶索引bucketsIndex,公式为:((array[i] - minValue) / significantDigit) % radixBase 对当前桶索引内的元素执行自减操作,得到其在数组中的正确位置index 计算出索引后,在aux中的对应位置存储当前遍历到的元素最后排序完成,返回axu。我们通过一个例子来描述上述过程,如下图所示。实现代码接下来,我们将上述思路转换为代码. /**
* 基数排序
* @param array 待排序数组
* @param radixBase 10进制排序,基数为10
*/
radixSort(array: number[], radixBase = 10): number[] {
if (array.length < 2) {
return array;
}
// 获取数组的最小值和最大值
const minValue = this.findMinValue(array);
const maxValue = this.findMaxValue(array);
// 当前有效数字,默认会从最后一位有效数字开始排序
let significantDigit = 1;
/**
* 计算有效位
* 最大值和最小值的差与有效数字进行除法运算,其值大于等于1就代表还有待排序的有效位
*/
while ((maxValue - minValue) / significantDigit >= 1) {
// 以当前有效位为参数对数组进行排序
array = this.countingSortForRadix(array, radixBase, significantDigit, minValue);
// 当前有效数字乘以基数,继续执行while循环进行基数排序
significantDigit *= radixBase;
}
return array;
}
/**
* 基于有效位进行排序
* @param array 待排序数组
* @param radixBase 基数
* @param significantDigit 有效位
* @param minValue 待排序数组的最小值
*/
private countingSortForRadix = (array: number[], radixBase: number, significantDigit: number, minValue: number) => {
// 声明桶索引以及桶
let bucketsIndex;
const buckets = [];
// 辅助数组,用于计数完成的值移动会原数组
const aux = [];
// 通过基数来初始化桶
for (let i = 0; i < radixBase; i++) {
buckets[i] = 0;
}
// 遍历待排序数组,基于有效位计算桶索引执行计数排序
for (let i = 0; i < array.length; i++) {
// 计算桶索引
bucketsIndex = Math.floor(((array[i] - minValue) / significantDigit) % radixBase);
// 执行计数操作
buckets[bucketsIndex]++;
}
// 计算累积结果得到正确的计数值
for (let i = 1; i < radixBase; i++) {
buckets[i] = buckets[i] + buckets[i - 1];
}
// 计数完成,将值移回原始数组中,用aux辅助数组来存储
for (let i = array.length - 1; i >= 0; i--) {
// 计算当前元素的桶索引
bucketsIndex = Math.floor(((array[i] - minValue) / significantDigit) % radixBase);
// 对当前桶索引内的元素执行自减操作,得到其在数组中的正确的位置
const index = --buckets[bucketsIndex];
// 计算出索引后,在aux中的对应位置存储当前遍历到的元素
aux[index] = array[i];
}
console.log(aux);
return aux;
};
编写测试代码接下来,我们将上图中的例子放进代码中,观察函数是否正确执行。const array = [12, 5, 6, 7, 8, 9, 11, 3, 4, 19];
const sort = new Sort(array);
const result = sort.radixSort(array);
console.log(result.join());
搜索算法接下来,我们来学习搜索算法,搜索算法分为两种:顺序(线性)搜索和二分搜索。在之前文章中,我已经详细讲解了这两种搜索算法的基础原理以及图解实现,所以此处只讲其代码实现。文章传送门:数组查找: 线性查找与二分查找本文示例代码地址:SearchArithmetic.ts顺序搜索顺序搜索是最基本的搜索算法,他会将每一个数据结构中的元素和我们要找的元素做比较。它也是最低效的一种搜索算法。实现代码 linearSearch(): number | null {
for (let i = 0; i < this.array.length; i++) {
if (this.array[i] === this.target) {
return i;
}
}
return null;
}
编写测试代码const array = [7, 8, 1, 2, 3, 5, 12, 13, 16, 19];
const searchArithmetic = new SearchArithmetic(array, 7);
const mid = searchArithmetic.linearSearch();
console.log(mid);
二分搜索二分搜索要求被搜索的数据结构已经排好序,它的基本原理就是找到数组的中间值,然后将目标值和找到的值进行大小比较,如果比中间值大就往中间值的右边找,否则就往中间值的左边找。实现思路选择数组的中间值如果选中值是待搜索值,那么算法执行完毕如果待搜索值比选中值要小,则返回步骤1并在选中值左边的子数组中寻找(较小)如果待搜索值比选中值要大,则返回步骤1并在选中值右边的子数组中寻找(较大)实现代码 binarySearch(): number | null {
// 对数组进行排序
this.sort.quickSort();
// 设置指针作为数组边界
let low = 0;
let high = this.array.length - 1;
while (low <= high) {
// 获取数组中间值
const mid = Math.floor((low + high) / 2);
// 获取数组的中间值
const element = this.array[mid];
// 如果数组中间值小于目标值,low+1,向其右边继续找
if (this.compareFn(element, this.target) === Compare.LESS_THAN) {
low = mid + 1;
} else if (this.compareFn(element, this.target) === Compare.BIGGER_THAN) {
// 如果中间值大于目标值,向其左边继续找
high = mid - 1;
} else {
// 中间值等于目标值,元素找到,返回mid即当前元素在数组的位置
return mid;
}
}
// 未找到,返回null
return null;
}
编写测试代码const array = [7, 8, 1, 2, 3, 5, 12, 13, 16, 19];
const searchArithmetic = new SearchArithmetic(array, 7);
const mid = searchArithmetic.binarySearch();
console.log(mid);
内插搜索内插搜索是改良版的二分搜索。二分搜索总是检查mid位置上的值,而内插搜索可能会根据要搜索的值检查数组中的不同地方。实现思路它遵循以下步骤:使用position公式选中一个值如果待搜索值比选中值要小,则返回步骤1并在选中值左边的子数组中寻找(较小)如果待搜索值比选中值要大,则返回步骤1并在选中值右边的子数组中寻找(较大)实现代码 /**
* 内插搜索
* 二分查找的优化,通过特定算法计算delta和position的值优化掉二分查找的寻找中间值做法
* @param equalsFn 校验两个值是否相等函数
* @param diffFn 计算两个数的差值函数
*/
interpolationSearch(equalsFn = defaultEquals, diffFn: IDiffFunction<T> = defaultDiff): number | null {
// 排序
this.sort.quickSort();
// 获取数组程度
const { length } = this.array;
// 定义指针,确定数组边界
let low = 0;
let high = length - 1;
// 声明position,用于公式
let position = -1;
let delta = -1;
// 目标值大于等于数组的low边界值且目标值小于等于high边界值就执行循环里的内容
while (low <= high && biggerEquals(this.target, this.array[low], this.compareFn) && lesserEquals(this.target, this.array[high], this.compareFn)) {
// 目标值与array的low边界的值做差
// 与array的high边界的值和low边界的值做差
// 最后将二者得出的值做除法运算,计算出delta值
delta = diffFn(this.target, this.array[low]) / diffFn(this.array[high], this.array[low]);
// 计算比较值的位置
position = low + Math.floor((high - low) * delta);
// 如果比较值位置的元素等于目标值,则返回当前索引
if (equalsFn(this.array[position], this.target)) {
return position;
}
// 如果比较值位置的元素小于目标值,则向其右边继续找
if (this.compareFn(this.array[position], this.target) === Compare.LESS_THAN) {
low = position + 1;
} else {
// 如果比较值位置的元素大于目标值,则向其左边继续查找
high = position - 1;
}
}
// 未找到
return null;
}
编写测试代码const array = [7, 8, 1, 2, 3, 5, 12, 13, 16, 19];
const searchArithmetic = new SearchArithmetic(array, 7);
const mid = searchArithmetic.interpolationSearch();
console.log(mid);
随机算法在日常开发中,我们会遇到将数组中的元素打乱位置,这样的场景,那么此时我们就需要设计一种随机算法来实现了,现实中一个很常见的场景就是洗扑克牌。Fisher-Yates算法此处我们讲解一种随机算法,名为Fisher-Yates,顾名思义,该算法就是由Fisher 和 Yates 创造。实现思路该算法的核心思想就是,从数组的最后一位开始迭代数组,将迭代到的元素和一个随机位置进行交换。这个随机位置比当前位置小。这样这个就算法可以保证随机过的位置不会再被随机一次。下图展示了这个算法的操作过程实现代码export class ShuffleArithmetic<T> {
constructor(private array: T[]) {}
// Fisher-Yates随机算法
fisherYates(): T[] {
// 从数组的最后一位向前遍历数组
for (let i = this.array.length - 1; i > 0; i--) {
// 计算随机位置,用一个随机数与i+1相加,得出的随机位置一定比当前位置小
const randomIndex = Math.floor(Math.random() * (i + 1));
// 交换当前位置的元素和随机位置的元素
this.swap(i, randomIndex);
}
return this.array;
}
/**
* 交换数组的元素
* @param a
* @param b
* @private
*/
private swap(a: number, b: number) {
const temp: T = this.array[a];
this.array[a] = this.array[b];
this.array[b] = temp;
}
}
编写测试代码import { ShuffleArithmetic } from "./lib/ShuffleArithmetic.ts";
const array = [4, 5, 6, 1, 2, 3, 4, 5, 8, 9, 0, 11, 22, 41];
const shuffleArithmetic = new ShuffleArithmetic(array);
shuffleArithmetic.fisherYates();
console.log("随机排序后的数组元素", array.join());
写在最后作者:神奇的程序员链接:https://juejin.cn/post/6860501233308794887来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
小乔学算法
PyTorch入门备忘-3- 图片数据集处理 - torchvision&transform
Torchvision机器学习与深度学习,数据集组织、加载处理、按需按批装载、送入模型训练,不论是图片、文字还是音视频,流程基本上一致。具体图片处理的大部分实现transform包上,实际使用时需要加入业务场景才能丰满起来。当我们静下心来,花时间去接触AI相关的知识与工具,我们会深刻的感觉到技术真的只是一个工具,是场景将它丰富了起来。—— 笔者个人观点简单介绍Torchvision 是 PyTorch 的一个独立子库,它服务于PyTorch深度学习框架的,主要用于计算机视觉任务,包括图像处理、数据加载、数据增强、预训练模型等。核心包如下:torchvision.datasets: 一些加载数据的函数及常用的数据集接口;torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;torchvision.utils: 其他的一些有用的方法。官网文档入口: pytorch.org/docs/stable…读取数据集可以从网上得到数据集,再用torchvision加载数据并处理,也可以从自建的数据集上加载并。自建过程建 PyTorch入门备忘-1-Dataset自建及Jupyter与Pycharm简易入门本文用torchvision数据集来演示读取的过程,内部会使用transform对数据进行变型。数据集准备 - CIFAR10此次代码中要用到的数据集,见附件有介绍与中文的参数。用代码下载数据集-CIFAR10通过py代码
# 使用CIFAR10数据集
# 训练集
# 如果下载比较慢,可以将控制台打印的下载链接放到专门的下载工具中下载
# 首先下载的是一个压缩包,会自动解压
train_set = torchvision.datasets.CIFAR10(root="./torchvision_dataset", train=True, download=True)
# 测试集
test_set = torchvision.datasets.CIFAR10(root="./torchvision_dataset", train=False, download=True)
运行代码,控制台显示如下信息50000 -- 说明有5w张训练数据 10000 --说明有1w张测试数据 会自动下载数据集到torchvision_dataset文件夹 已下载就不会继续下载,控制台会出输Files already downloaded and verified字样操作数据torchvision.datasets和transform的联合使用下载数据集装载图片图片处理图片展示
import torchvision.datasets
from torch.utils.tensorboard import SummaryWriter
# 将图片数据都转为tensor类型
# 可以对数据集做任何transforms范围内的操作,该例子只针对数据做toTensor
dataset_transform = torchvision.transforms.Compose([
torchvision.transforms.ToTensor
])
# 使用CIFAR10数据集
train_set = torchvision.datasets.CIFAR10(root="./torchvision_dataset", train=True, transform=dataset_transform, download=True)
# 测试集
test_set = torchvision.datasets.CIFAR10(root="./torchvision_dataset", train=False, transform=dataset_transform, download=True)
# 用tensorboard显示前10张图片
# 运行tensorboard --logdir=p10
writer = SummaryWriter('p10')
for i in range(0):
img, target = test_set[i]
writer.add_image("test_set", img,i)
writer.close()
Torchvision.transformTransforms是torchvision模块下面的一个子模块,在Dataset中很常用可以方便地对图像进行各种变换操作。该模块中包含大量用户数据类型转化的类型和方法,比如统一size,每一个图像数据进行类的转化等。"""transforms.ToTensor转化:PIL Image或numpy.ndarray(H * W * C) 转到 tensor 的数据类型主要方法call(self, pic)参数:pic - Image或numpy的图像对象返回值 : 返回tensor类型的图片"""Tensor图像结构:Tensor是PyTorch中最基本的数据结构,你可以将其视为多维数组或者矩阵。PyTorch tensor和NumPy array非常相似,但是tensor可以在GPU上运算,而NumPy array则只能在CPU上运算。可对图像直接操作,代码如下:导入import torch
创建一个未初始化的5x3矩阵# 创建一个未初始化的5x3矩阵
x = torch.empty(5, 3)
print(x)
用.backward() 计算梯度# 因为out包含一个标量,out.backward()等价于out.backward(torch.tensor(1.))
out.backward()
# 打印梯度 d(out)/dx
print(x.grad)
常用方法ToTensor作用: PIL Image或numpy.ndarray(H * W * C) 转到 tensor 的数据类型输入: PIL Image.open()输出: 类型 ToTensorNormalize作用: 根据均值与标准差归一化tensor类图片输入: tensor类型图片的均值与标准差输出: 归一化后的图片数据计算公式: (Input[channel] - mean[channel]) / std[channel]举例 Input[channel] - mean[channel]) / std[channel= (input - 0.5)/0.5= 2 * input - 1结论 input像素值[0-1] --> result[-1,1]
Resize作用: 将(PIL Image or Tensor)调整为给定的大小。输入:输出: 变型后的PIL Image or TensorCompose作用:把几个tranforms组合在一起使用,相当于一个组合器,可以对输入图片一次进行多个transforms的操作。比如 compose负责把ToTensor和resize组合起来,一步到位实现PIL图形到resize后的tensor图形的转换RandomCrop作用:随机裁剪输入:输出:裁后图片代码
# 导入
"""
Torchvision 是 PyTorch 的一个独立子库,主要用于计算机视觉任务,包括图像处理、数据加载、数据增强、预训练模型等。
Torchvision 提供了各种经典的计算机视觉数据集的加载器,如CIFAR-10、ImageNet,以及用于数据预处理和数据增强的工具,可以帮助用户更轻松地进行图像分类、目标检测、图像分割等任务。
"""
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from PIL import Image
"""
用ToTensor将PIL图片转为Tensor图片
"""
# 绝对路径 D:\workspace\python\learn_torch\data\train\ants\0013035.jpg
img_path = "data/train/bees/16838648_415acd9e3f.jpg"
img = Image.open(img_path)
trans_toTensor = transforms.ToTensor()
img_tensor = trans_toTensor(img)
writer = SummaryWriter("logs")
writer.add_image("tensor_img", img_tensor)
"""
2. 用Normalize实现Tensor图片归一化
"""
trans_norm_0 = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5, ])
img_norm_0 = trans_norm_0(img_tensor)
writer.add_image("Normalize", img_norm_0, 1)
trans_norm = transforms.Normalize([6, 3, 2], [9, 3, 5])
img_norm = trans_norm(img_tensor)
writer.add_image("Normalize", img_norm, 2)
# 运行,测蔗
# tensorboard --logdir=logs
"""
2. Resize-等比例缩放
"""
trans_resize = transforms.Resize((512, 512))
# img_PIl --> img_resize PIL
img_resize = trans_resize(img)
# image PIl ---> toTensor --> 转为 tensor
img_resize = trans_toTensor(img_resize)
# print(img_resize)
writer.add_image("Resize", img_resize, 1)
"""
transforms.Compose
# trans_toTensor: 输入
# trans_resize_2: 输出
"""
trans_resize_2 = transforms.Resize(100)
trans_compose = transforms.Compose([trans_resize_2, trans_toTensor])
img_resize_2 = trans_compose(img)
writer.add_image("Resize", img_resize_2, 0)
"""
transforms.RandomCrop:随机裁剪
"""
trans_random = transforms.RandomCrop((150, 500))
trans_compose_2 = transforms.Compose([trans_random, trans_toTensor])
for i in range(10):
img_crop = trans_compose_2(img)
writer.add_image("RandomCrop", img_crop, i)
writer.close()
启动tensorboard查看结果相关知识Torchvision官网: pytorch.org/docs/stable…Torchvision.dataset文档入口数据集CIFAR10CIFAR10由10个不同标签的图像组成。其中包括卡车、青蛙、船、汽车、鹿等常见图像。还有一个CIFAR100版本,有 100 个不同的类别CIFAR10/CIFAR100一般用于物价识别,其广泛用于机器学习领域的计算机视觉算法基准测试。详情 官网地址包名 torchvision.datasets.FashionMNIST()包名 torchvision.datasets.CIFAR10()参数说明:root: 数据集根路径,可以是相对路径train: = ture 训练集,否则为测试集transform: 对数据集进行的transform操作target_transform: 训练后的目标数据集执行指定的transform操作download:=true 自动下载数据集,false不会下载COOC目前有超过 100,000 个日常物品,如人、瓶子、文具、书籍等。 广泛用于目标检测,语义分割和图像描述MNISTMNIST 常用的入门级数据集,手写文字数据集包名 torchvision.datasets.MNIST()文档:pytorch.org/vision/stab…Fashion MNIST该数据集与 MNIST 类似,但该数据集不是手写数字,而是 T 恤、裤子、包等服装项目。包名 torchvision.datasets.FashionMNIST()torchvision.models提供神经网络常见的神经网络,有一些神经网络已经预训练好了。torchvision.transform图像处理与变形等transforms.CenterCrop 对图片中心进行裁剪
transforms.ColorJitter 对图像颜色的对比度、饱和度和零度进行变换
transforms.FiveCrop 对图像四个角和中心进行裁剪得到五分图像
transforms.Grayscale 对图像进行灰度变换
transforms.Pad 使用固定值进行像素填充
transforms.RandomAffine 随机仿射变换
transforms.RandomCrop 随机区域裁剪
transforms.RandomHorizontalFlip 随机水平翻转
transforms.RandomRotation 随机旋转
transforms.RandomVerticalFlip 随机垂直翻转
文档入口torchvision.utils提供一些常用的工具,比如tensorboard等from torch.utils.tensorboard import SummaryWriter
文档入口[参考]www.bilibili.com/video/BV1hE…pytorch.org/vision/stab…
小乔学算法
深度学习经典模型之BERT(上)
BERT(Bidirectional Encoder Representations from Transformers)是一个双向transformer编码器的言表示模型。来自论文:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 。由Google公司的研发,BERT的出现使得我们能够在一个大的数据集上面训练好一个比较深的神经网络,简化了NLP任务的训练,又提升了它的性能,使得自然语言处理有了质的飞跃。基本信息论文:Pre-training of Deep Bidirectional Transformers for Language Understanding地址:arxiv.org/abs/1810.04…BERT全称:Bidirectional Encoder Representations from Transformers源码:github.com/google-rese…关键字:Pre-training(预训练)、Deep(深度)、Bidirectional(双向)BERT特点Pre-trainingBERT的主要创新点都在pre-train方法上,即用了Masked LM和Next Sentence Prediction两种方法分别捕捉词语和句子级别的representation。在海量语料的基础上通过前期的Pre-training,让其达到一定的语言表达;后期再通过少量的样本,比如文本分类(正向、负向、中性等情感类文本分类)再进行训练,就可以达到很好的效果。DeepBERT-base采用12层Encoder,层数更深。它使用更强大的机器训练更大规模的数据,使BERT的结果达到了全新的高度,并且Google开源了训练好的多语言BERT模型代码,用户可以直接使用BERT作为Word2Vec的转换矩阵并高效的将其应用到自己的任务中。Bidirectional通过MLM(masked language model) 任务实现上下文理解。和ELMo/GPT的区别GPT使用新的Transformers结构,用左侧信息去预测未来信息,单向模型的主要缺点在于不能获得足够好的词表示;ELMo通过从左向右(LTR)和从右向左(RTL)两个模型的输出拼接获得词的表示,双向信息融合的很浅,且由于基于RNN的架构,在应用到下游任务时,需要对架构做一些调整;BERT是基于Transformer,用了左右侧信息,使用的是没有标号的数据,用到一些下游任务的时候,和GPT一样只需要微调输出层就可以了。和Transformer的区别只使用了transformer的encoder,然后堆叠多层(12层)BERT的Transformer Encoder端输入的向量表示,多了Segment Embeddings。计算位置向量的时候是随机初始化加上带训练的,Transformer的位置编码是固定的。BERT极大的拓展了Transformers的应用,使其可以在更大的无标签的数据集上训练,其效果比有标签、更小数据集上训练的模型效果还要好。模型网络层数L, 隐藏层维度H, Attention多头个数A,总参数TP(Total Parameters)Bert base: L=12, H=768, A=12, TP=110M, GPU:7G+Bert large: L=24, H=1024, A=16, TP=340M, GPU:32G+Transformer: L=6, H=512, A=8, TP=L∗12H2+13HT P= L*12H^2+13HTP=L∗12H2+13HBert参数计算见'附:BERT可学习参数计算' 机器学习相关术语 few-shot learning(FSL 少样本学习) zero-shot learning(ZSL 零样本学习) Meta learning(学习其它机器学习任务输出的机器学习算法)主要贡献引入了Masked LM, 使用双向LM做模型预训练。为预训练引入了新目标NSP(预测上句和下句的关系),它可以学习句子与句子间的关系。进一步验证了更大的模型效果更好: 12 --> 24 层。为下游任务引入了很通用的求解框架,不再为任务做模型定制。刷新了多项NLP任务的记录,引爆了NLP无监督预训练技术应用场景文本分类标注数据是AI模型训练里最艰难的工作。NLP的标注更是需要投入大量的人力,文本标注因为没有标准答案比图像标注还要困难.而BERT在文本多分类的任务中,能在极小的数据下带来显著的分类准确率提升。有数据表示采用了BERT之后其效果显著提升。BERT文本分类就是使用预训练的BERT模型来对文本进行分类,例如文本分类为新闻、科技、娱乐等类别。在这个过程中,BERT 模型可以自动学习到文本的语义信息,从而实现准确的分类。网上教程:Bert Tutorial 文本分类指南情感分析在深度学习应用中,研究者主要在三个粒度级别上研究情感分析:文档级、语句级和方面级。其中文档级情感分类是指为观点型文档标记整体的情感倾向或极性,即确定文档整体上传达的是积极的还是消极的观点。句子级别的情感分类是确定单个给定句子中表达的情感。而方面级因为情感始终具有目标其情感分类会同时考虑情感和目标信息。网络好文 基于BERT的中文情感分析指南命名实体识别命名实体识别(NER 也称为实体识别、实体分块 或 实体提取)是信息提取的一个子任务,旨在将文本中的命名实体定位并分类为预先定义的类别,如人员、组织、位置、时间表达式、数量、货币值、百分比等。而一个命名实体就是一个词语或是一个短语,它能够清晰地将一个物体和与他有相似属性的物体区分开来 (来自基于深度学习的NER综述)。网络好文 中文命名实体指南机器翻译在基于BERT的机器翻译模型中,通常采用编码器-解码器结构。编码器负责将源语言句子编码成一系列的隐藏表示,而解码器则将这些隐藏表示解码成目标语言句子。BERT作为编码器的一部分,能够为解码器提供更加丰富的语义信息,从而提升翻译质量。网络好文 INCORPORATING BERT INTO NEURAL MACHINE TRANSLATION两阶段模型BERT是一个预训练的语言表征模型,不再像以往的模型采用传统的单向语言模型或者把两个单向语言模型进行浅层拼接的方法进行预训练,而是采用新的masked language model(MLM) ,以生成深度的双向语言表征。BERT模型是一个两阶段模型,第一阶段 pre-training,第二阶段 fine-tuning。即预训练和微调。第一阶段: 预训练阶段预训练阶段模型有两个任务,即Masked Language Model (简称MLM) 和Next Sentence Prediction (简称NSP)。第二阶段: 预训练后只需要添加一个额外的输出层进行fine-tune,就可以在各种各样的下游任务中取得state-of-the-art的表现。在这过程中并不需要对BERT进行任务特定的结构修改。除了输出层之外在预训练和微调中都使用了相同的架构,还使用了相同的预先训练过的模型参数d为不同的下游任务初始化模型。在微调期间,所有参数都会进行微调。[CLS]是添加在每个输入示例前面的一个特殊符号,而[SEP]是一个特殊的隔板 工具标记(例如,分离问题/答案)。[CLS]和[SEP] 用于表示句子的开始和结束,或者在处理多个句子时进行分隔 BERT的主要特征是,对于不同的任务都有一个统一的模型结构,是一个泛化能力较强的预训练模型。自监督学习在机器学习中,最常见的是有监督学习,即通过人工对数据进行标注,然后在提供输入x 和 标签y^的情况下,对模型进行训练,让模型输出y尽可能与标签y^一致。自监督不需要人工标注,通过将数据处理成两部分,一部分作为输入x一部分作为标签X,然后使用这组数据对模型进行训练,让模型输出y尽可能与标签一致。由于自监督不需要大量的人工标注,因此能够极大的降低模型训练成本。BERT的大规模预训练就是基于自监督学习。图示说明左侧为有监督学习:模型、标签右则为自监督学习:数据本身就有Label, MLM's masked通过掩盖(或称之为完形填空)的方式,将Label提取,把数据变为有标签的数据。Pre-training预训练任务之MLM(Masked Language Model)在每一个训练序列中以15%的概率随机地选中某个token进行MASK,当一个token被选中后,有以下三种处理方式(my dog is hairy 为例):80%的概率被[MASK]。如:my dog is hairy --> my dog is [MASK]10%的概率修改为随机的其他token。如: my dog is hairy --> my dog is apple10%的概率不变。如,my dog is hairy --> my dog is hairy然后在对该位置的MASK进行预测: 主要是对80%被掩码的数据进行预测,预测被掩码的位置上的数据,如果预测错了,计算损失进行反向传播。上述操作方法主要是要解决BERT的两个缺点:因为Bert用于下游任务微调时, [MASK] 标记不会出现,它只出现在预训练任务中。这就造成了预训练和微调之间的不匹配,微调不出现[MASK]这个标记,模型好像就没有了着力点、不知从哪入手。所以只将80%的替换为[MASK],但这也只是缓解、不能解决。相较于传统语言模型,Bert的每批次训练数据中只有15%的标记被预测,这导致模型需要更多的训练步骤来收敛。预测训练任务之NSP(Next Sentence Predict)除了masked的自监督的构建方式,对于每一个训练样例又以另外一种方式(NSP)进行预测,主要原理:将一句话的前后两句话拿出来50%的概率保持原有顺序 (标注为IsNext)50%的概率后面的句子被替换为文档的其他随机句B (标注为NotNext)这就意味着50%的样本是正例,50%的样本是负例,接下来把训练样例输入到BERT模型中,用[CLS]对应的信息去进行二分类:预测当前句子是否有Next sentence的关系,是否是前后句。假定1代表是一句话,0代表不是一句话。那么:如果概率>0.5,表不变,标签为0;概率 <0.5,表变化,标签为1经过上面两个任务的处理,训练数据如下所示(为了方便浏览,制作成表格样式):两个任务共享Bert,使用不同的输出层,做Muti-Task示例图示Input1=[CLS]我今天要[MASK]课 [SEP],上完[MASK]给你打 电话[SEP] Label1 =IsNext Input2=[CLS] 大模型MASK]技术发展很快[SEP] ,晚 [MASK] 吃什么[SEP] Label2=NotNext 符号说明:句首符号:CLS, 句尾符号:SEP, MASK:掩盖(码)Bert双向的理解Bert可以看作Transformer的encoder部分,Bert模型舍弃了GPT的attention mask。双向主要体现在Bert的预训练任务一:遮蔽语言模型(MLM)。如:我 [MASK] 学 习 英 语。这句话输入到模型中,[MASK]通过attention均结合了左右上下文的信息,这体现了双向。attention是双向的, 只是GPT通过attention mask达到单向,即让[MASK]看不到 学 习 英 语这四个字,只看到上文 我 喜 欢 。附录语言模型预训练可改善的NLP任务与策略语言模型预训练可以改善许多NLP任务,这些任务包括:用来建模句子之间的关系,比如说对句子的情绪识别或者两个句子之间的关系实体命名的识别(对每个词识别是不是实体命名,比如说人名、街道名)在使用预训练模型做特征表示的时候,一般有两类策略一个策略是基于特征feature-based的(代表作是ELMo):对每一个下游的任务构造一个跟这个任务相关的神经网络,然后将预训练好的这些表示(比如说词嵌入)作为一个附加特征把它们和原始输入一起放进模型中。另一个策略是基于微调fine-tuning的: 就是把预训练好的模型放在下游任务的时候不需要改变太多,只需要简单的修改一些输出层,再用我们自己的数据进行一个增量训练,对预训练好的参数会在下游的数据上再进行微调。来自李沐老师关于BERT论文精读的内容。BERT可学习参数计算BERT模型可学习参数来自词嵌入层和Transformer块嵌入层 就是一个矩阵,输入是字典的大小(这城假设是30k),输出是隐藏单元的个数(这里假设是H)transformer块有两部分:BERT与GPT的比较架构层面GPT采用单向的Transformer Decoder结构,只能利用上文信息无法利用下文信息。在预训练时使用了"语言模型(LM)"和"下一句预测(NSP)"。BERT采用双向Transformer Encoder结构,在预训练阶段使用了"遮盖语言模型(Masked Language Model,MLM)"和"下一句预测(Next Sentence Prediction,NSP)"训练任务在GPT两个预训练任务中,语言模型任务是模型根据前面的文本预测下一个单词;在下一句预测任务中模型则需要判断两个句子是否相邻。在BERT预测训练任务中,遮盖语言模型是模型根据上下文预测被遮盖的单词;在下一句预测任务中模型则需要判断两个句子是否相邻,并给出是或否的预测结果数据集GPT使用的是使用了WebText等大型文本语料库BERT使用了Wikipedia等大型文本语料库,以及BookCorpus等小型语料库应用场景GPT适用于于语言生成、文本补全、问答等任务。BERT适用于文本分类、命名实体识别、情感分析等任务该段来自哪忘了,反正我感觉蛮好的,总结的不错,加在附录里,偶尔看看。利于学习。【参考】blog.csdn.net/iwill323/ar…zhuanlan.zhihu.com/p/562352255zhuanlan.zhihu.com/p/624740065zhuanlan.zhihu.com/p/90133637www.xjx100.cn/news/607361…zhuanlan.zhihu.com/p/69351731wenku.baidu.com/view/e1806c…
小乔学算法
深度学习经典模型之BERT(下)
在"深度学习经典模型之BERT(上)"我们描述了BERT基本信息、意义、与GPT和Transformer的区别、预训练、自监督等相关信息后,本章节将介绍BERT的输入、Encoder、微调及两个主流变种。BERT inputs切词方法BERT的切词方法用的是WordPiece embeddings,其思想是如果一个词在整个里面出现的概率不大的话,就应该把它切开,看他的一个子序列,如果它的一个子序列(比如它的词根)出现的概率很大,那么只保留这个子序列就好了,这样可以把一个相对长的词切成一段又一段的片段,这些片段还是经常出现的,就可以用相对较小的30k的词典就能表示一个比较的文本。这样可以避免按照空格切词时一个词作一个token会让数量大进而导致词典变大,让可学习的参数都集中在了嵌套层里的问题。序列的第一个词永远是一个特殊词元[CLS]代表序列开始(全称:classification), 在每个句子后面放一个特殊词[SEP]表示separate或end,全称separator。如上图所示。输入嵌入Bert输入嵌入包含三部分的内容:token embeddings,position embeddings,和Segment Embeddings(token所属段落编码的embeddings),示意如上图所示。 即对于每一个token(词元)在BERT的向量表示这个token本身的embedding加上它在哪个句子的embedding再加上位置的embedding.在Transfomer里面位置信息是手动构造出的矩阵,但是在BERT里面不管你是属于哪个句子还是位置在哪,它对应的向量的表示都是通过学习而来的。Token Embeddings:采用look up的方式,将每个token转换成768维的向量。Segment Embeddings:BERT支持双句输入,Segment(0,1)用于区分a、b句。Position Embeddings:采用训练式位置编码,通过look up获取位置编码。transformer的输入是由 word embedding + position embedding组合而成的向量x.BERT Encoder基础架构BERT的Encoder包含三个部分的内容:输入、多头注意力与前馈神经网络。对应的是Transformer的Encooder部分,其中输入部件的组成比Transormer多了一层,具体见Bert input章节。Bert与Transformer不同的是,BERT仅采用Transfomer的Encoder,分为BERT bae与BERT large,其层数等参数都有所不同。BERT Base信息BERT-base采用12层的Transformer Encoder堆叠,上一层的输出作为下一层的输入,基本信息与架构图如下:基本信息架构示意图encoder层数(layers) :12层 模型最大输长度(max_len): 512维度(dim): 768 头数(Head,简称h):12参数: 110M GPU:7G+ BERT large信息BERT-large采用24层的Transformer Encoder堆叠,上一层的输出作为下一层的输入,基本信息与架构图如下:基本信息架构示意图encoder层数(layers):24层 模型最大输长度(max_len): 1024 维度(dim): 768 头数(Head,简称h):16 参数: 340M GPU:32G+ BERT Fintune - 微调预微调模块BERT本质是通过在海量的语料的基础上运行自监督学习方法为单词学习一个好的特征表示。通过大量的数据预训练得到的通用模型,后续基于通用模型再进行微调。对于不同的任务,微调都集中在预微调模块,几种重要的NLP微调任务架构图展示如下:微调任务句对分类任务判断两句子之间的关系,如句子语义相似度、句子连贯性判定等,其本质是文本分类。输入: 两句子,[CLS]sentence1[SEP]sentence2[SEP]输出: 句子关系标签。做法: 和单句分类操作一样,只不过是二分类。单句分类任务单句分类任务是判断句子属于哪个类别,如新闻分类、问题领域分类等。输入: 一个句子,形如 [CLS]sentence[SEP];输出: 输出句子类别标签。做法: 选择bert模型输出的第一个位置的token,也就是[CLS]的向量作为下游任务的输入。QA任务给定问答和一段文本,从文本中抽取出问题的答案,如机器阅读理解等。其本质是序列标注。输入: 一个问题,一段文本,形如[CLS]question[SEP]content[SEP]输出: 答案在文本中的索引(标出答案的位置)。NER任务NER(Named Entity Recognition 命名实体识别)的过程,就是根据输入的句子,预测出其序列标注的过程。输入: 念熹在清华大学的体育场看了中国男篮的一场比赛输出: B-PER,E-PER,O, B-ORG,I-ORG,I-ORG,E-ORG,O,B-LOC,E-LOC,O,O,B-ORG,I-ORG,I-ORG,E-ORG,O,O,O,O其中,“念熹”以PER,“清华大学”以ORG,“体育场”以LOC,“中国男篮”以ORG为实体类别分别挑了出来。BIOES标注方式中含义 B,即Begin,表示开始 I,即Intermediate,表示中间 E,即End,表示结尾 S,即Single,表示单个字符 O,即Other,表示其他,用于标记无关字符BERT变种RoBERTa - 主流特点舍弃NSP任务,并使用更长的序列长度使用更多的预训练数据 (由16GB 升-> 160GB)更大的batch size (batch size 256 -> batch size 8K)使用更大的词表 (30K -> 50K)括号中的数据代表传统bert到ROBERTa时配置变化动态掩码原本的BERT采用的是static mask的方式,就是在create pretraining data中,先对数据进行提前的mask。为了避免在每个epoch中使用相同的mask数据,充分利用数据,定义了dupe factor,这样可以将训练数据复制dupe factor份,然后同一条数据可以有不同的mask,注意这些数据不是全部都喂给同一个epoch,是不同的epoch,例如dupe factor=10,epoch=40则每种mask的方式在训练中会被使用4次。动态掩码的方式在模型训练阶段实时计算掩码的位置和方法,能够最大限度的保证同一段文本在不同epoch下使用不同的掩码模式,提高了数据的复用效率。ALBERT词向量因式分解。BERT中 embedding 维度E与Transformer 隐层维度 H一致ALBERT 引入词向量因式分解方法解耦E和H,先将词向量编码到低维空间E,然后通过个全连接层将E映射到H,计算复杂度从 (VH) 降低到 (VE + EH)Transformer 跨层参数共享。ALBERT中每一层Transformer的参数都是一样的,类似于一个循环结构,每次都经过相同的Transformer层引入sop (Sentence Order Prediction) 任务代替NSP任务附:Bert中的特殊词元表示在BERT中,和是特殊的词元(token),用于在输入序列中标记特定的位置和边界。[CLS][CLS]它是表示序列开头的特殊词元,全称为"classification"。在BERT中,输入序列的第一个位置被标记为[CLS],用于表示整个序列的概括信息。在训练过程中,BERT模型学习使用位置的表示来进行各种分类任务,例如文本分类、情感分析等。在编码后的表示中,[CLS]位置的向量通常用作整个序列的汇总表示。[SEP][sep]它是表示序列分割的特殊词元,全称为"separator"。在BERT中,输入的文本序列可以由多个片段(segments)组成,例如两个句子或一个问题和一个回答。为了将这些片段分隔开,[sep]词元用于标记不同片段的边界。它出现在片段之间和序列的末尾,用于告知BERT模型输入序列的结构。[PAD][PAD]它表示填充(padding)的词元,在输入序列中用于填充长度不足的片段或序列。填充是为了使所有输入序列具有相同的长度,以便进行批量处理。[MASK][MASK]它表示掩蔽(mask)的词元,在预训练阶段用于生成掩蔽语言模型(Masked Language Model,MLM)任务。在训练过程中,输入序列中的一部分词元会被随机选择并替换为[MASK]词元,模型需要预测被掩蔽的词元。[UNK][UNK]它表示未知(unknown)的词元,用于表示在预训练期间未见过的词汇。当输入序列中出现未登录词(out-of-vocabulary)时,这些词元将被替换为[UNK]词元。这些特殊的词元表示方式使BERT模型能够处理不同类型的输入和执行不同的任务,例如分类、回归、命名实体识别等。它们提供了对输入序列的结构和语义的信息,并且在预训练和微调阶段起到关键的作用。除了[CLS],[SEP],[MASK],[UNK]之外,BERT还可以使用其他自定义的特殊词元表示方式,具体取决于具体的应用场景和任务需求。比如 领域特定词元、标签词元、实体词元等。来自(blog.csdn.net/weixin_4462…)[参考]blog.csdn.net/weixin_4203…blog.csdn.net/weixin_4462…blog.csdn.net/qq_42801194…
小乔学算法
TypeScript实现二叉搜索树
前言树是一种非顺序数据结构,它用于存储需要快速查找的数据。现实生活中也有许多用到树的例子,比如:家谱、公司的组织架构图等。本文将详解二叉搜索树并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。实现思路二叉树中的节点最多只能有两个子节点:一个是左侧子节点, 另一个是右侧子节点。二叉搜索树是二叉树的一种,它与二叉树的不同之处:每个节点的左子节点一定比自身小每个节点的右子节点一定比自身大本文主要讲解树的具体实现,不了解树基础知识的开发者请移步: 数据结构: 二叉查找树二叉搜索树的实现根据二叉搜索树的特点我们可知: 每个节点都有2个子节点,左子节点一定小于父节点,右子节点一定大于父节点。创建辅助类Node首先,我们需要一个类来描述二叉树中的每个节点。由二叉树的特点可知,我们创建的类必须包含如下属性:key 节点值left 左子节点的引用right 右子节点的引用和链表一样,我们将通过指针来表示节点之间的关系,在双向链表中每个节点包含两个指针, 一个指向下一个节点,两一个指向上一个节点。树也采用同样的方式(使用两个指针), 但是树的指针一个指向左子节点,另一个指向右子节点。下图描述了一个二叉树, 节点与节点之间的链接就是指针。创建二叉搜索树的基本结构我们采用与链表相同的做法,通过声明一个变量来控制此数据结构的第一个节点(根节点), 在树中此节点为root。接下来,我们来分析下实现一个二叉搜索树都需要实现的方法:insert(key): 向树中插入一个节点search(key): 在树中查找一个键。如果节点存在返回true, 否则返回falsemin(): 返回树中最小的值 / 键max(): 返回书中最大的值 / 键remove(key): 从树中移除某个键向树中插入一个新的节点验证插入操作是否为特殊情况: 根节点为null, 如果根节点为null则创建一个Node类的实例并将其赋值给root属性,将root指向新创建的节点如果根节点不为null, 则需要寻找合适的位置来插入子节点, 因此我们需要实现一个辅助方法insertNodeinsertNode方法接收两个参数: 要从哪里开始插,要插入节点的key由于二叉搜索树有一个特点是: 左子节点小于父节点, 右字节点大于父节点。所以我们需要创建一个比对方法compareFncompareFn方法接收两个参数: 要比对的值,原值。如果小于则返回-1, 如果大于则返回1, 相等则返回0调用compareFn方法判断要插入的键是否小于当前节点的键如果小于, 判断当前节点的左子节点是否为null, 如果为null则创建Node节点将其指向左子节点, 否则从当前节点的左子节点位置开始递归调用insertNode方法,寻找合适的位置如果大于, 判断当前节点的右子节点是否为null, 如果为null则创建Node节点将其指向右子节点,否则从当前节点的右子节点位置开始递归调用 insertNode方法, 寻找合适的位置接下来,我们通过一个例子来描述下上述过程。我们要将下述数据插入二叉树中:30 21 25 18 17 35 33 31首先,插入30,此时根节点为null,创建一个Node节点将其指向root然后,插入21,此时比较21和30的大小,21 < 30。在30的左子节点插入, 30的左子节点为null。因此直接插入然后,插入25 < 30,在30的左子节点插入,此时30的左子节点已经有了21,递归调用insertNode,比较25和21的大小,25 > 21,因此在21的右子节点插入依此类推,直至所有节点都插入。搜索树中的值在树中,有三种经常执行的搜索类型:搜索最小值搜索最大值搜索特定的值接下来,我们通过一个例子来描述下搜索特定的值的过程。如下图所示为一个二叉搜索树,我们需要判断25是否在树中:调用search方法,要查找的键为25,调用searchNode方法,需要从根节点开始找,因此传root和要查找的key首先,node不为空,则继续判断key与根节点键的大小,25 < 30,递归它的左子树然后,比较25和21的大小,25 > 21,递归它的右子树此时,25 === 21,节点已找到,返回true移除一个节点移除树中的节点remove,它接收一个参数key,它需要一个辅助方法removeNode,它接收两个参数:节点,要移除的key。removeNode实现思路如下:首先,判断节点是否为null,如果是则证明节点不存在返回null调用compareFn方法,比较要删除的key与当前节点key的大小如果要删除的key小于当前节点key,则递归调用removeNode方法,传当前节点的左子节点和key如果要删除的key大于当前节点key,则递归调用removeNode方法,传当前节点的右子节点和key否则就是找到了要删除的节点,删除节点可能出现三种情况:二叉搜索树的遍历遍历一棵树是指访问树的每个节点并对它们进行某种操作的过程,访问树的所有节点有三种方式:中序遍历先序遍历后序遍历树的遍历采用递归的形式访问,对递归不了解的开发者请移步:递归的理解与实现中序遍历中序遍历是一种上行顺序访问树节点的遍历方式,即从小到大的顺序访问所有节点。中序遍历的一种应用场景就是对树进行排序操作,接下来我们来分析下中序遍历的实现:声明inOrderTraverse方法接收一个回调函数作为参数,回调函数用来定义我们对遍历到的每个节点进行的操作,中序遍历采用递归实现,因此我们需要一个辅助方法inOrderTraverseNodeinOrderTraverseNode方法接收两个参数:节点,回调函数递归的基线条件node==null递归调用inOrderTraverseNode方法,传当前节点的左子节点和回调函数调用回调函数递归调用inOrderTraverseNode方法,传当前节点的右子节点和回调函数接下来,我们通过一个例子来描述下中序遍历的过程如上图所示,蓝色字标明了节点的访问顺序,橙色线条表示递归调用直至节点为null然后执行回调函数。具体的执行过程如下:11=>7=>5=>3
ode:3,
left:undefined
callback(node.key) 3
right:undefined
allback(node.key) 5
node:5,
right:6
left:undefined
callback(node.key) 6
right: undefined
callback(node.key) 7
node:7,
right:9
left:8
left:undefined
callback(node.key) 8
right:undefined
callback(node.key) 9
right:10
left:undefined
callback(node.key) 10
right:undefined
allback(node.key) 11
node:11,
right:15
left:13
left:12
left:undefined
callback(node.key) 12
right:undefined
callback(node.key) 13
right:14
left:undefined
callback(node.key) 14
right:undefined
...... ...
right:25
left:undefined 25
callback(node.key)
先序遍历先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档,接下来我们分析下先序遍历的具体实现:声明preOrderTraverse方法,接收的参数与中序遍历一样,它也需要一个辅助方法preOrderTraverseNodepreOrderTraverseNode方法接收的参数与中序遍历一样递归的基线条件: node == null调用回调函数递归调用preOrderTraverseNode方法,传当前节点的左子节点和回调函数递归调用preOrderTraverseNode方法,传当前节点的右子节点和回调函数接下来,我们通过一个例子来描述下先序遍历的过程:如上图所示,蓝色字标明了节点的访问顺序,橙色线表示递归调用直至节点为null然后执行回调函数。具体的执行过程如下:node:11
callback(node.key) 11
node.left=7
ode:7
callback(node.key) 7
node.left=5
ode:5
callback(node.key) 5
ode.left=3
node:3
callback(node.key) 3
node.left=undefined,node.right=undefined => node:5
node:5
node.right = 6
callback(node.key) 6
node:6
node.left=undefined,node.right=undefined => node:7
node:7
node.right = 9
callback(node.key) 9
...... ...
callback(node.key) 25
后序遍历后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录及其子目录中所有文件所占空间的大小,接下来我们来分析下后序遍历的实现:声明postOrderTraverse方法,接收的参数与中序遍历一致声明postOrderTraverseNode方法,接收的参数与中序遍历一致递归的基线条件为node == null递归调用postOrderTraverseNode方法,传当前节点的左子节点和回调函数递归调用postOrderTraverseNode方法,传当前节点的右子节点和回调函数调用回调函数接下来,我们通过一个例子来描述下后序遍历的执行过程。如上图所示,蓝色字标示了节点的执行顺序,橙色线表示递归调用直至节点为null然后执行回调函数。具体的执行过程如下:11=>7=>5=>3
node:3
left:undefined
right:undefined
callback(node.key) 3
node:5
right:6
ode:6
left:undefined
right:undefined
callback(node.key) 6
node:5
callback(node.key) 5
node:7
right: 9
node:9
left:8
node:8
left:undefined
right:undefined
callback(node.key) 8
node:9
right:10
node:10
left:undefined
right:undefined
callback(node.key) 10
node:9
callback(node.key) 9
node:7
callback(node.key) 7
... ...
callback(node.key) 11
实现代码前面我们分析了二叉搜索树的实现思路,接下来我们就讲上述思路转换为代码。创建辅助节点创建Node.ts文件创建Node类,根据节点的属性为类添加对应属性export class Node<K> {
public left: Node<K> | undefined;
public right: Node<K> | undefined;
constructor(public key: K) {
this.left = undefined;
this.right = undefined;
}
toString() {
return `${this.key}`;
}
}
完整代码请移步: Node.ts创建二叉树的基本结构创建BinarySearchTree.ts文件声明root节点和比对函数protected root: Node<T> | undefined;
constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
this.root = undefined;
};
export type ICompareFunction<T> = (a: T, b: T) => number;
export function defaultCompare<T>(a:T, b:T) {
if (a === b){
return Compare.EQUALS;
}else if(a > b) {
return Compare.BIGGER_THAN;
}else {
return Compare.LESS_THAN;
}
}
实现节点插入insert(key: T){
if (this.root == null){
// 如果根节点不存在则直接新建一个节点
this.root = new Node(key);
}else {
// 在根节点中找合适的位置插入子节点
this.insertNode(this.root,key);
}
}
protected insertNode(node: Node<T>, key: T) {
// 新节点的键小于当前节点的键,则将新节点插入当前节点的左边
// 新节点的键大于当前节点的键,则将新节点插入当前节点的右边
if (this.compareFn(key,node.key) === Compare.LESS_THAN){
if (node.left == null){
// 当前节点的左子树为null直接插入
node.left = new Node(key);
}else {
// 从当前节点(左子树)向下递归,找到null位置将其插入
this.insertNode(node.left,key);
}
}else{
if (node.right == null){
// 当前节点的右子树为null直接插入
node.right = new Node(key);
}else {
// 从当前节点(右子树)向下递归,找到null位置将其插入
this.insertNode(node.right,key);
}
}
}
获取节点的最大值、最小值以及特定节点// 搜索特定节点的值
search(key: T){
return this.searchNode(<Node<T>>this.root, key);
}
private searchNode(node: Node<T>, key: T): boolean | Node<T>{
if (node == null){
return false;
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN){
// 要查找的key在节点的左侧
return this.searchNode(<Node<T>>node.left, key);
} else if(this.compareFn(key, node.key) === Compare.BIGGER_THAN){
// 要查找的key在节点的右侧
return this.searchNode(<Node<T>>node.right, key);
} else{
// 节点已找到
return true;
}
}
// 获取树的最小节点
min(){
return this.minNode(<Node<T>>this.root);
}
protected minNode(node: Node<T>): Node<T>{
let current = node;
while (current != null && current.left != null){
current = current.left;
}
return current;
}
// 获取树的最大节点
max(){
return this.maxNode(<Node<T>>this.root);
}
private maxNode(node: Node<T>){
let current = node;
while (current != null && current.right != null){
current = current.right;
}
return current;
}
实现节点移除remove(key: T){
this.removeNode(<Node<T>>this.root,key);
}
protected removeNode(node: Node<T> | null, key: T){
// 正在检测的节点为null,即键不存在于树中
if (node == null){
return null;
}
// 不为null,需要在树中找到要移除的键
if (this.compareFn(key,node.key) === Compare.LESS_THAN){ // 目标key小于当前节点的值则沿着树的左边找
node.left = <Node<T>>this.removeNode(<Node<T>>node.left, key);
return node;
} else if (this.compareFn(key,node.key) === Compare.BIGGER_THAN){ // 目标key大于当前节点的值则沿着树的右边找
node.right = <Node<T>>this.removeNode(<Node<T>>node.right, key);
return node;
} else{
// 键等于key,需要处理三种情况
if (node.left == null && node.right == null){ // 移除一个叶节点,即该节点没有左、右子节点
// 将节点指向null来移除它
node = null;
return node;
}
if (node.left == null){ // 移除一个左侧子节点的节点
// node有一个右侧子节点,因此需要把对它的引用改为对它右侧子节点的引用
node = <Node<T>>node.right;
// 返回更新后的节点
return node;
} else if(node.right == null){ // 移除一个右侧子节点的节点
// node有一个左侧子节点,因此需要把对它的引用改为对它左侧子节点的引用
node = node.left;
// 返回更新后的节点
return node;
}
// 移除有两个子节点的节点
const aux = this.minNode(node.right); // 当找到了要移除的节点后,需要找到它右边子树最小的节点,即它的继承者
node.key = aux.key; // 用右侧子树最小的节点的键去更新node的键
// 更新完node的键后,树中存在了两个相同的键,因此需要移除多余的键
node.right = <Node<T>>this.removeNode(node.right, aux.key) // 移除右侧子树中的最小节点
return node; // 返回更新后的节点
}
}
二叉树的遍历接下来我们实现中序、先序、后序遍历实现中序遍历inOrderTraverse(callback: Function){
this.inOrderTraverseNode(<Node<T>>this.root,callback);
}
private inOrderTraverseNode(node: Node<T>,callback: Function){
if (node !=null){
this.inOrderTraverseNode(<Node<T>>node.left,callback);
callback(node.key);
this.inOrderTraverseNode(<Node<T>>node.right,callback);
}
}
实现先序遍历preOrderTraverse(callback: Function){
this.preOrderTraverseNode(<Node<T>>this.root, callback);
}
private preOrderTraverseNode(node: Node<T>, callback: Function){
if (node != null){
callback(node.key);
this.preOrderTraverseNode(<Node<T>>node.left, callback);
this.preOrderTraverseNode(<Node<T>>node.right, callback);
}
}
实现后序遍历postOrderTraverse(callback: Function){
this.postOrderTraverseNode(<Node<T>>this.root, callback);
}
private postOrderTraverseNode(node: Node<T>, callback: Function) {
if (node != null){
this.postOrderTraverseNode(<Node<T>>node.left, callback);
this.postOrderTraverseNode(<Node<T>>node.right, callback);
callback(node.key);
}
}
完整代码请移步: BinarySearchTree.ts编写测试代码前面我们实现了二叉搜索树以及它的基本方法,接下来我们来测试下上述代码是否都能正常执行。const binarySearchTree = new BinarySearchTree();
binarySearchTree.insert(11);
binarySearchTree.insert(7);
binarySearchTree.insert(15);
binarySearchTree.insert(5);
binarySearchTree.insert(3);
binarySearchTree.insert(9);
binarySearchTree.insert(8);
binarySearchTree.insert(10);
binarySearchTree.insert(13);
binarySearchTree.insert(12);
binarySearchTree.insert(14);
binarySearchTree.insert(20);
binarySearchTree.insert(18);
binarySearchTree.insert(25);
binarySearchTree.insert(6);
// 测试中序遍历函数
const printNode = (value) => console.log(value);
console.log("中序遍历");
binarySearchTree.inOrderTraverse(printNode);
// 测试先序遍历
console.log("先序遍历");
binarySearchTree.preOrderTraverse(printNode);
// 测试后序遍历
console.log("后序遍历");
binarySearchTree.postOrderTraverse(printNode);
// 测试获取最小值函数
console.log("树的最小值",binarySearchTree.min());
// 测试获取最大值函数
console.log("树的最大值",binarySearchTree.max());
// 测试搜索节点函数
console.log("8在二叉树中",binarySearchTree.search(8));
console.log("100在二叉树中",binarySearchTree.search(100));
// 测试节点删除
console.log("删除节点3");
binarySearchTree.remove(3);
binarySearchTree.inOrderTraverse(printNode);
完整代码请移步: BinarySearchTreeTest.js执行结果如下:
小乔学算法
Transformer模型-2-模型架构
Transformer是什么Transformer是目前最流行的特征抽取器,在大部分的场景中已经取代了RNN。从BERT、T5到GPT其基座都是Transformer, 不管哪一个的出现都在相应领域引发了轰动。Transformer vs RNNRNNRNN不能并行。RNN会依赖前一时刻输出的隐层状态,导致RNN必须一步一步走完,无法并行结果是让运行变慢RNN词间距过长。词间距是两个词相隔的距离,当距离过长可能会导致梯度消失或梯度爆炸等问题。TRANSFORMERtransformer并行运行且速度极快;transformer每个词之间的词间距都是1;上述两点奠定了transformer是目前最流行的特征抽取器。Transformer理解从整个深度学习流程来描述Transformer所处的工作位置,我们可以简单的把Transformer看成一个盒子:将上述的盒子展开后会发觉transformer是由encoder与decoder构成,而每个encoder与decoder又由6个相同的层组成,如此盒子变成如下形式:后续,我们陆续展开transformer内部的结构加以分析与阐述总架构图大部分神经序列转导模型都有一个编码器-解码器结构。 编码器输入序列用x(x1,...,xn)表示,输出用z(z1,...,zn)有示。 根据z解码器生成符号的一个输出序列有用y(y1,...,ym)表示 ,一次一个元素。 在每一步中,模型都是自回归的,当生成下一个时,使用先前生成的符号作为附加输入。transformer也遵循了encoder-decoder架构,encoder和decoder都使用self-attention和Feed Forward。论文《Attention Is All You Need》中给出transformer的模型架构图如上所示,其中Encoder和Decoder都包含6个block,这6个block除了参数各自随机初始化外结构相同基本相同。Encoder:每一层都有两个子层。第一层是一个Multi-Head Attention(多头注意机制)第二层是简单的、位置完全连接的Feed Forward(Feed Forward Neural Network 前馈神经网络),由两个线性变换组成,之间有一个ReLU激活。每个子层会叠加一个Add&Norm, 即采用残差连接,接着进行特征归一化(标准化),每个子层的输出都是LayerNorm(x +Sublayer(x)),其中Sublayer(x) 是由子层本身实现的函数。Decoder: 除了每个编码器层中的两个子层之外,解码器还插入第三个子层:第一层是一个Masked Multi-Head Attention,妈注意力加入了masked 以防止位置关注到后面的位置。这种掩码结合将输出嵌入偏移一个位置,确保对位置的预测 i 只能依赖小于i的已知输出第二层是 (Cross) Multi-Head Attention,该层对编码器堆栈的输出执行multi-head attention与编码器类似,在每个子层都叠加一个Add&Norm, 即采用残差连接,接着进行特征归一化(标准化)。为了方便残差连接,Transformer模型中的所有子层以及嵌入层产生的输出维度都为dmodel=512d_{model} = 512dmodel=512。输入transformer的输入用到了的嵌入(Embedding)将输入词符和输出词符转换为维度为dmodeld_{model}dmodel的向量。encoder的输入层和decoder的输入层是一样的结构,都是token embedding(词向量)+ positional embedding(位置向量) , 得到最终的输入向量。其中Word Embedding(词嵌入) 负责将自然语言转化为与其对应的独一无二的词向量表达, 而Position Embedding(位置嵌入) 表示单词出现在句子中的位置。理由是Transformer是使用全局信息,无法捕捉到序列顺序信息的,需要使用Position Embeddin表示句子位置。关于位置编码transform采用了正弦和余弦函数,公式如下:注意力机制Attention函数可以描述为将query和一组key-value对映射到输出,其中query、key、value和输出都是向量。 输出为value的加权和,其中分配给每个value的权重通过query与相应key的兼容函数来计算。query、key、value 在后面的章节中会描述为 Q、K、V多头注意力机制图中红色圈中的部分为 Multi-Head Attention,是由多个Self-Attention组成的,可以看到 Encoder 包含一个 Multi-Head Attention,而 Decoder 包含两个 Multi-Head Attention 。多头注意力机制的内部结构:Scaled Dot-Product Attention在transformer中 h=8,dmodel=512dk =dv =dmodel∕h =512/8=64h=8, d_{model} = 512 \\ d_k = d_v = d_{model}∕h = 512/8 = 64h=8,dmodel=512dk =dv =dmodel∕h =512/8=64FeedForward(前馈神经网络)在进行了Attention操作之后,Encoder和Decoder中的每一层都包含了一个完全连接前馈网络,对每个position的向量分别进行相同的操作,由两个线性变换组成,之间有一个ReLU激活。:Encoder(编码器)Encoder由2部分组成: 多头注意力、前馈神经网络Decoder(解码器)Decoder结构与Encoder相似,但是存在一些区别。Decoder 包含两个 Multi-Head Attention 层。最后有一个 Softmax 层计算下一个单词的概率。Masked Self-AttentionMask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 Padding Mask 和 Sequence Mask。其中,Padding Mask 在所有的 scaled dot-product attention里面都需要用到,而 Sequence Mask只有在Decoder的Self-Attention里面用到。(Cross) Multi-Head Attention其实这块与上文 Encoder 中 的 Multi-Head Attention 具体实现细节上完全相同,区别在于Encoder的多头注意力里的Q、K、V是初始化多个不同的矩阵得到的。而Decoder的K、V是来自于Encoder的输出,Q是上层Masked Self-Attention的输出。Linear和softmaxDecoder最后会输出一个实数向量。再通过过一个linear layer将decoder的输出扩展到与vocabulary size一样的维度上。经过softmax 后,选择概率最高的一个word作为预测结果。总结记录于2023-10-24 修改于2023-11-10 修改原因: 研读了论文的部分内容,感觉文章有误做了修改 优化文章结构,便于阅读[参考]papers.nips.cc/paper/2017/…www.yiyibooks.cn/yiyibooks/A…www.u72.net/chengxu/sho…zhuanlan.zhihu.com/p/445856100
小乔学算法
数组实现栈与对象实现栈的区别
前言栈作为一种数据结构,它可以应用在很多地方,当你需要经常获取刚存放进去的数据时,那么栈这种数据结构将是你的首选。栈的实现方式一般有两种:数组实现和对象实现,这两种实现方式最终实现的功能都是一样的,但是在性能上却有着很大的差别。本文将详细讲解这两种实现方式的差异并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。数组实现栈本文讲解的是栈用代码的实现,如果对栈这种数据结构还不是很了解的话,可以移步我的另一篇文章:栈与队列实现思路栈的核心思想为后进先出(LIFO),那么我们可以用数组来描述栈。接下来,我们来看下,一个栈都需要具备哪些功能:入栈,添加一个新元素至栈顶(数组的末尾)。出栈,将栈顶的元素移除并返回被移除的元素。获取栈顶元素,获取当前栈顶元素返回。判断栈是否为空,判断栈(数组)内是否有数据。清空栈,移除栈内所有的元素。获取栈大小,返回栈里的元素个数。输出栈内数据,将栈中的所有元素以字符串的形式返回。❝入栈(push),可以使用数组的push方法直接往数组的末尾添加元素。出栈(pop),可以使用数组的pop方法直接移除栈中的元素,该方法会返回当前被移除的元素。栈顶元素(peek),可以通过数组的长度-1获取到数组中的最后一个元素。栈是否为空(isEmpty),可以通过判断数组的长度是否为0来实现。清空栈(clear),可以将数组直接赋值为空或者调用出栈方法直至栈中的数据为空。栈大小(size),可以返回数组的长度。输出栈内数据,可以调用数组的toString方法将数组转换为字符串。实现代码有了实现思路后,我们就可以将上述实现思路转换为代码了。新建一个Stack.ts文件定义栈并规定其类型 private items: any[];
在构造器中初始化栈 constructor() {
this.items = [];
}
根据实现思路实现栈中的函数 // 入栈
push(item:any) {
this.items.push(item);
}
// 出栈
pop() {
return this.items.pop();
}
// 返回栈顶元素
peek() {
return this.items[this.items.length - 1];
}
// 判断栈是否为空
isEmpty() {
return this.items.length === 0;
}
// 清空栈栈内元素
clear() {
this.items = [];
}
// 获取栈内元素数量
size():number{
return this.items.length;
}
// 将栈内元素转为字符串
toString(){
return this.items.toString();
}
完整代码请移步:Stack.ts编写测试代码上述代码我们实现了一个栈,接下来我们往栈中添加几条数据,测试栈内的方法是否正确执行。新建一个StackTest.js文件实例化一个栈const stack = new Stack();
测试栈内方法是否正确执行// 入栈
stack.push("第一条数据");
stack.push("第二条数据");
// 出栈
stack.pop();
// 返回栈顶元素
console.log(stack.peek());
// 查看栈大小
console.log(stack.size());
// 判断栈是否为空
console.log(stack.isEmpty());
// 返回栈内所有元素
console.log(stack.toString())
// 清空栈
stack.clear()
完整代码请移步:StackTest.js执行结果如下对象实现栈实现一个栈最简单的方式是通过数组存储每一个元素。在处理大量数据时,我们需要评估如何操作数据是最高效的。在使用数组时,大部分方法的时间复杂度都为O(n),我们需要迭代整个数组直至找到目标元素,在最坏的情况下我们需要迭代数组的每一个位置。数组是元素的一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。如果我们可以直接获取元素,占用更少的内存空间,并且仍然保证所有元素都按照我们的需要进行排列,就属于最优解决方案了。实现代码我们可以使用一个对象来存储所有的栈元素,保证它们的顺序并且遵循LIFO原则。接下来我们来看看如何使用对象来实现栈。新建一个ObjStack.ts文件定义栈对象结构interface StackObj {
[propName: number] : any;
}
定义栈并规定其类型,count用于记录栈的大小。private items: StackObj;
private count: number;
在构造器中初始化栈相关变量this.items = {};
this.count = 0;
入栈,当前栈的大小为新元素的key。push(item: any) {
this.items[this.count] = item;
this.count++;
}
出栈,当前栈大小-1,取出栈顶元素,删除栈顶元素,返回取出的栈顶元素pop() {
if(this.isEmpty()){
return undefined;
}
this.count--;
const result = this.items[this.count];
delete this.items[this.count];
console.log(this.items);
return result;
}
返回栈顶元素,以当前栈大小-1为key获取其对应的value值。peek() {
if(this.isEmpty()){
return undefined;
}
return this.items[this.count - 1];
}
判断栈是否为空,清空栈内元素,获取栈内元素数量// 判断栈是否为空
isEmpty() {
return this.count === 0;
}
// 清空栈内元素
clear() {
this.items = [];
this.count = 0;
}
// 获取栈内元素数量
size():number{
return this.count;
}
将栈内元素转为字符串,遍历当前栈对象中的数据,将栈中的数据用逗号拼接并返回。toString(){
if (this.isEmpty()){
return "";
}
let objString = `${this.items[0]}`;
for (let i = 1; i < this.count; i++){
objString = `${objString},${this.items[i]}`
}
return objString;
}
完整代码请移步:ObjStack.ts编写测试代码上述代码我们用对象实现了一个栈,接下来我们往栈中添加几条数据,测试栈内的方法是否正确执行。新建一个StackObjTest.js文件实例化一个栈const stack = new ObjStack();
测试栈内方法是否正确执行// 入栈
stack.push("第一条数据");
stack.push("第二条数据");
// 出栈
stack.pop();
// 返回栈顶元素
console.log(stack.peek());
// 查看栈大小
console.log(stack.size());
// 判断栈是否为空
console.log(stack.isEmpty());
// 返回栈内所有元素
console.log(stack.toString())
// 清空栈
stack.clear()
完整代码请移步:StackObjTest.js执行结果如下二者的区别数组大部分方法的时间复杂度都为O(n),数组中的元素是一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。对象可以通过key直接访问到目标元素时间复杂度为O(1),我们可以直接目标元素进行操作,速度明显比数组快了很多倍。接下来,我们通过一个实例来看看这两者在执行速度上的差异。十进制转二进制把十进制转为二进制,需要将该十进制除以2并对商取整,直到结果是0为止。声明一个函数参数为一个十进制数const decimalToBinaryStack = function (decNumber) {
}
函数内部声明一个变量,用于接收当前传进来的参数进行除法运算后得到的值。// 传进来的十进制数
let number = decNumber;
函数内部实例化一个栈,用于保存模运算后得出的值。函数内部声明两个变量,用户保存当前模运算的值和最终生成的二进制字符串// 余数
let rem;
// 二进制结果
let binaryString = "";
while循环,判断当前参数进行除法运算后得到的值是否为0,如果不为0就对当前结果进行模运算,将模运算得到的值入栈,对当前结果进行除法运算,直至当前结果为0。while (number > 0) {
// 模运算
rem = Math.floor(number % 2);
// 将余数入栈
stack.push(rem);
// 当前十进制结果除以二
number = Math.floor(number / 2);
}
while循环,将栈中的数据取出拼接到二进制结果字符串中去while (!stack.isEmpty()) {
binaryString += stack.pop().toString();
}
返回二进制结果字符串return binaryString;
❝
实现代码如上所述,唯一不同的就是一个使用的是对象栈一个使用的数组栈,接下来我们来看下不同栈的运行时间差距。
小乔学算法
Transformer模型-4-Inputs
Encoder的输入层和Decoder的输入层是一样的结构,都是由Token embedding(词向量 word embedding) 和 Positional embedding(位置向量) 组合而成,并到最终的 输入向量x。Transformer引入Positional embedding主要是解决词序问题。因为Transformer不包含任何递归和卷织(不采用RNN的结构),都同时使用流经编码和解码堆栈(全局信息),无法捕捉到序列的顺序信息,因此没有特殊的顺序或位置,单使用Token embedding会存在不知词序的情况。因此Transformer中单词的输入向量x,该x则是由 Word Embedding和Position Embedding相加得到。Embedding时Transformer会提取重要的两个信息:词的意思(Input Embedding) 和 词的位置信息 (Position Embedding), 并将位置信息加入到 Input embedding里面 Word Embedding = Token embeddingword embedding所谓的词嵌入层 是自然语言处理(NLP)中的一种表达技术,它将词语或短语从词汇表映射到向量的实数空间中。处理流程送词入模型首先将每个词的信息放入到模型中,得到的数据维度将为 (batch_size, seq_length, bemedding_size(d_model))batch_size是批次大小, seq_len是序列长度 embedding_size(d_model):维度,transfomer为512维,BERT是768维进行预处理 (进行文本填充(PAD)和截断)和 分词等如上图:不足的用特殊文本PAD填充,截断的信息会造成信息丢失构建Index词表同时确定"按字或按词处理"的处理方式,确定维度512维(该维度由所选模型而定,Transformer的整个向量传输都是512维 BERT则是768维)【词表格式】行数: 行Id为Index,通过Lookup或查询可以查到唯一的一个字/词。纵深: 一个字的维度(512维),如上图中第二列的图(如上图,一行10个字,一个字512维,一行共有10*512维,即每一切片都按维度seq_len * embedding_size计算高度: batchsize,一批样本的批次大小。transformer是按Batchsize处理文字/词的,当所有的文本都转换完成之后,会形成一个高为Batchsize的矩阵,如上上图最右侧的矩阵。,每一切片代表输入的一行文本即一个序列, 如上上图最右则矩阵。形成矩阵最后数据会以右侧矩阵的方式输入到模型里(每一行的结构都是 seq_len * embedding_size 的结构,如上一点所述)总结经过Embedding后,文本中的每一个字就被转变为一个向量 ,能够在计算机中表示。《Attention is all you need》这一论文中,作者采用的是 512维词向量表示,也就是说,每一个字被一串长度为512的字向量表示。特征向量工具将词汇表示成特征向量的方法有多种, 对于要处理的一串文本,我们要让其能够被计算机处理,需要将其转变为词向量,方法有最简单的one-hot或者有名的Word2Vec等,甚至可以随机初始化词向量。本文介绍One-hot和数字表示。One-hot编码One-hot编码使用一种常用的离散化特征表示方法,在用于词汇向量表示时,向量的列数为所有单词的数量,只有对应的词汇索引为1,其余都为0。举个例子,“我爱我的祖国”这句话,总长为6,但只有5个不重复字符,用One-hot表示后为6*5的矩阵,如图所示:但是这种数据类型十分稀疏,即便使用很高的学习率,依然不能得到良好的学习效果。数字表示数字表示是指用整个文本库中出现的词汇构建词典,以词汇在词典中的索引来表示词汇。所以与其叫做“数字表示”,还不如叫“索引表示”。举个例子,还是“我爱我的祖国”这句话,就是我们整个语料库,那么整个语料库有5个字符,假设我们构建词典{'我':0, '爱':1, '的':2, '祖':3, '':4},“我爱我的祖国”这句话可以转化为向量:[0, 1, 0, 2, 3, 4]。如图所示。这种方式存在的问题就是词汇只是用一个单纯且独立的数字表示,难以表达出词汇丰富的语义。主流文本处理工具现在主流的工具是BPE,在早期用 结巴/Word2vec等需要自己手工切词,自己建词表等。而BPE不需我们建词表等,而且性能更高。BPE(BytePairEncoding) 是一种用于自然语言处理(NLP)的技术,它可以将较大的语料库压缩到更小的尺寸,以便更好地处理。BPE的原理是通过查找文本中出现次数最多的字节对(bytepair),然后将它们合并为一个新的字符,从而减少语料库中的字符数量。BPE现在已经被广泛应用于NLP领域,例如机器翻译、语音识别、自然语言理解等,它可以帮助模型更好地处理大规模的语料库,从而提高模型的性能。关于中文处理 1.大模型不需要大动词量,特别是百川2等模型出来之后,完全不需要修改中文词表(基本包含全网数据,垂直领域的词也不需要) 2.llama2直接训练,可能需要修改词表。如果需要添加,只需追加position embeddingTransformer 中除了Word Embedding,还需要使用Position Embedding 表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息,因此是无法捕捉到序列顺序信息的,例如将K、V按行进行打乱,那么Attention之后的结果是一样的。但是序列信息非常重要,代表着全局的结构,因此必须将序列的分词相对或者绝对position信息利用起来。文本是时序型数据,词与词之间的顺序关系往往影响整个句子的含义。绝对位置编码给每个位置的位置信息建模,最简单的实现方式:使用每个词汇的次序 1,2,...,T 作为位置编码信息。例如,BERT使用的是Learned Positional Embedding(可学习的位置嵌入),先初始化位置编码,再放到预训练过程中,训练出每个位置的位置向量。绝对位置编码问题解析绝对位置编码存在一个严重的问题,假如模型最大序列长度为 512,那么预测阶段输入超过 512 的序列就无法进行处理:pos = 1,2,3,...,T-1。总结就是 当文本较长时,位置序列没有上界,T位置的编码比0位置的编码大很多,这会导致与 token embedding 合并后出现特征在数值的倾斜和干扰。归一化同样存在问题 如果对上面的位置序列进行归一化: pos = pos / (1 + T)。 还是有问题,不同长度的位置编码的步长是不同的,在较短的文本中相邻的两个字的位置编码的差异与长文本中的相邻两个字的位置编码存在数量级上的差异,这会导致长文本的相对位置关系被稀释。1. 念熹编程培训(T=5)
pos(念) = 1 / 5 = 0.2
pos(熹)= 2 / 5 = 0.4
2. 念熹编程是一家优秀的培训机构,秉承... (T=500)
pos(念) = 1 / 500 = 0.002
pos(熹) = 2 / 500 = 0.004
相对位置编码 - 使用 sin/cos 函数相对位置并没有每个输入的位置信息做完整建模,而是在计算Attention的时候考虑当前位置与被Attention的位置的相对距离。由于自然语言一般更依赖于相对位置,所以相对位置编码通常也有着优秀的表现。在Transformer中使用的就是相对位置编码的方式。Transformer引入了相对位置的概念-使用 sin/cos函数,根据上面的讨论,位置编码能满足transformer以下的需求:1.能够体现词汇在不同位置的区别(特别是同一词汇在不同位置的区别)。2.能够体现词汇的先后次序,并且编码值不依赖于文本的长度。3.有值域范围限制。使用 sin/cos 函数(有界周期函数)来表示相对位置, sin/cos 函数周期变化规律稳定,使得编码具有一定的不变性 。因此,Transformer在不同维度上使用不同的函数来生成位置编码,也就是给位置编码的每一维赋予不同的α,同时区分奇偶维度的函数形式 。Position Embedding 用 PE表示,PE 的维度与Word Embedding 是一样的。PE 可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,《Attention is all you need》论文中给出的计算公式:通过α来调节函数周期,α越大,1/α 越小,sin图像在纵轴方向被“压扁”,周期变长,相邻位置(横坐标)的位置编码差值变小(相邻横坐标对应y值差异减小)。在 sin/cos 函数 [-1, 1] 的值域范围内,如果 α 比较大,相邻字符之间的位置差异体现得不明显;如果 α 比较小,在长文本中还是可能会有一些不同位置的字符的编码一样。在上式中,不同的维度会有不同的α值,周期从2π到10000*2π,并且区分了奇偶维度的函数形式,这使得每一维度上都包含了一定的位置信息,而各个位置字符的编码值又各不相同。但从BERT之后,就没有再用sin与cos的方式,是直接变成可训练的形式,下一章节介绍更好的编码工具。更优秀的编码方式从BERT之后,就没有再用sin与cos的方式,从BERT之后是直接变成可训练的形式。目前,已经出现了更优秀的位置编码方式:RoPE(Rotary Positional Embedding)和ALiBi(Attention with Linear Biases),两个都是相对位置编码形式。RoPE它兼顾了绝对位置信息与相对位置信息,可以不受固定长度限制处理任意长度的序列。其工作原理是,通过一个基于位置的旋转矩阵将每个位置的嵌入旋转到一个新的位置。该方法的优点是可以保持相对位置信息的一致性,在旋转后相邻的位置仍然会有相似的嵌入。ALiBi能够让Transformer语言模型在推理时可以处理比训练时更长的序列。它在处理文本序列时不使用实际的位置嵌入,而是在计算某个键和查询之间的注意力时,根据键和查询之间的距离对查询可以分配给键的注意力值进行惩罚。当键和查询靠近时,惩罚非常低,当它们远离时,惩罚非常高 —— 动机:靠近的词比远离的词更重要。