LeCun在2016年提出的「蛋糕比喻」准确预测了大模型的发展路线,强化学习逐渐成为模型发展的关键阶段。OpenAI最近宣布推出强化微调(RFT)API,允许开发者通过提供少量高质量样本定制专家模型。这项技术的实现将有助于提升强化学习的稳定性,突破其传统的训练不稳定性问题。与指令调优不同,RFT可以通过重复训练多个数据点以优化模型,使其更加稳定和有效。 "Image 43" (https://wm-ai.oss-cn-shanghai.aliyuncs.com/test/v2_66d1e5ea06644bad8a3c0b0c711c8676@46958_oswg438115oswg848oswg478_img_000.jpg) 强化微调不仅提高了训练过程的稳定性,还拓宽了强化学习在大模型中的应用范围。OpenAI还提到,利用RFT API,开发者可以快速训练一个领域专家模型,并根据反馈对模型进行评分调整。相比传统的强化学习,RFT允许更少样本也能产生有效的学习效果,具有较大的应用潜力。 对于RFT的具体实现,目前仍有诸多猜测,但从与AI2的RLVR工作相似的背景来看,RFT的成功可能在于其优化器和数据处理的有效性。OpenAI正在通过用户数据进行强化学习训练,进一步优化o1模型的能力。 "Image 44" (https://wm-ai.oss-cn-shanghai.aliyuncs.com/test/v2_0194645a09604adead518feb51ba2422@46958_oswg45779oswg885oswg261_img_png.jpg) 随着强化学习技术的不断成熟,其在大模型的微调方面的应用前景将愈加广阔,尤其是对推理模型和数据飞轮的推动作用不可小觑。 "Image 46" (https://wm-ai.oss-cn-shanghai.aliyuncs.com/test/v2_c1613700a55d4c479fabfa3e479984ac@46958_oswg343386oswg848oswg555_img_000.jpg) 强化微调技术的引入标志着大模型的训练技术进入了一个新的阶段,OpenAI的o1模型也因此变得更加稳定,且适应更多领域的应用需求。
一、前言文本情感分析是自然语言处理中非常基本的任务,我们生活中有很多都是属于这一任务。比如购物网站的好评、差评,垃圾邮件过滤、垃圾短信过滤等。文本情感分析的实现方法也是多种多样的,可以使用传统的朴素贝叶斯、决策树,也可以使用基于深度学习的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 积极效果不是特别理想,因为训练样本通常为长文本,而现在测试的是短文本。
一、前言最近受邀参与了亚马逊云科技【云上探索实验室】活动,体验了一下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可以自动对各种模型以及各组超参数进行搜索,训练最优模型。
一、前言对图像不了解的人时常妄想去除马赛克是可以实现的,严格意义来说这确实是无法实现的。而深度学习是出现,让去除马赛克成为可能。为了理解去除马赛克有多难,我们需要知道马赛克是什么。观感上,马赛克就是方块感。当我们观察图像像素时, 马赛克表现为下图的情况:原图右下角有十字,而添加马赛克后右下角一片都变成了同一像素,如果我们没保留原图,那么我们无法还原,也不知道是否还原了原图。因为原图已经被破坏了,这也是为什么马赛克是不可修复的。那神经网络又是如何让修复成为可能呢?其实无论什么方式的修复,都是一种估计,而不是真正的修复。神经网络去除马赛克的操作其实是生成马赛克那部分内容,然后替代马赛克,从而达到修复的效果。这种修复并不是还原,而是想象。假如我们对一张人脸打了马赛克,神经网络可以去除马赛克,但是去除后的人脸不再是原来那个人了。二、实现原理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() 下面是生成结果。左侧为原图,中间为添加马赛克后的图片,右侧则是去除马赛克后的结果:整体来说效果比较不错。本文的方法不只可以用来去除马赛克,还可以完成图像重构。比如老化的图片、被墨汁污染的图片等,都可以用本文的方法完成重构。
一、前言从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 项目运行今天我们要使用的是一个视频转绿幕的项目。项目地址: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的操作,这部分代码还能做一些改进。
一、前言在许多搜索引擎中,都内置了以图搜图的功能。以图搜图功能,可以极大简化搜索工作。今天要做的就是实现一个以图搜图引擎。我们先来讨论一下以图搜图的难点,首当其冲的就是如何对比图片的相似度?怎么样的图片才叫相似?人可以一眼判断,但是计算机却不一样。图片以数字矩阵的形式存在,而相似度的比较也是比较矩阵的相似度。但是这有一些问题。第二个问题就是大小问题,图片的大小通常是不一样的,而不同大小的矩阵也无法比较相似度。不过这个很好解决,直接修改图片尺寸即可。第三个问题则是像素包含的信息非常有限,无法表达抽象信息。比如画风、物体、色调等。根据上面描述,我们现在要解决两个问题:用什么信息替换像素信息、怎么计算相似度。下面一一解决。在开始前,我们先实现一个简易的以图搜图功能。二、简易以图搜图实现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)下面是一些搜索结果,整体来看还是非常不错的,不过由于降维的关系,搜索效果可能或略差于前面,但是整体效率要高许多。六、总结本文我们分享了以图搜图的功能。主要思想就是将图片转换成向量表示,然后利用相似度计算,在图库中查找与之最接近的图片。最开始使用线性搜索的方式,此时查找效率最低。而后使用聚类进行改进,把把图片分成多个簇,把查找分为查找簇和查找最近图片两个步骤,可以大大提高查找效率。改进后代码变得比较繁琐,于是引入向量数据库,使用向量数据库完成检索功能。这样就完成了整个程序的编写。
一、前言在以前,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次后的结果图像。从结果来看,我们确实完成了风格迁移。
一、前言在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的精度效率等问题没有详细讨论,读者可以参考相关资料深入了解。
一、前言上一篇介绍以图搜图的实现: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越小,结果越随机。