如下图所示,当你输入一张图片以及一个语音文件,就能够让你的图片的人物的嘴和眼睛等感官动起来。这就是这篇论文所要阐述的前言。
论文地址:https://arxiv.org/abs/2004.12992
论文代码:https://github.com/adobe-research/MakeItTalk.git
这篇论文的网络架构分为以下几部分:1.进行内容编解码(AUTO VC)。2.人脸关键点获取。3.内容动画训练部分。4.说话动画部分训练。5.将3和4部分进行合成(要看起来比较圆滑一点,不显得突兀)。6.(原本有动画部分和真实人脸部分,但是只说明人脸部分),使用条件对抗生成,将合成的人脸的风格转换到真实人脸部分中。
训练的时间及配置:我们的数据集中的所有视频转换为每秒62.5帧,音频波形在16K Hz频率下采样。我们使用Pytork使用Adam优化器训练语音内容动画和说话人感知动画模块。学习率设置为10−4,体重下降到10−6.语音内容动画模块包含1.9M的参数,在Nvidia 1080Ti GPU上训练需要12小时。支持说话人的动画模块有3.8万个参数,在同一个GPU上训练需要30个小时。用于生成真实人脸的单面动画模块也使用Adam optimizer进行了训练,学习率为10%−4,批量为16。该网络包含3070万个参数,在8个Nvidia 1080Ti GPU上训练了20小时。
AUTO VC可以认为是一种生成对抗网络,不过它是针对声音所提出的网络。为什么不用单纯的gan/style网络,原因是GAN 训练是复杂和困难的,并没有强有力的证据表明其生成的语音具有良好的感知质量。另一方面, CVAE 训练简单,但不具有 GAN 的分布匹配特性。利用基于LSTM的编码器,将输入音频压缩成紧凑的表示(瓶颈),经过训练可以放弃原始说话人身份,但保留内容。其实里面的一些详细操作还是很复杂的,不过作者是利用了训练好的模型参数进行使用的。简单来说就是得到一个单纯的语音信息。得到的是R(表示所有帧的总数)xD(表示每一帧所代表的维度(内容))的大小。(在这部分我觉得作者并没有用判别器,而是直接用的生成器来生成一个向量)详细内容参考:【论文学习笔记】《AUTOVC: Zero-Shot Voice Style Transfer with Only Autoencoder Loss》_FallenDarkStar的博客-CSDN博客
这部分作者也是利用训练好的模型进行提取人脸的68个关键点[68,3],这个3应该是向量表示,他是将一段视频中的每个帧都会进行提取这样的关键点用作标签使用。如下图所示结果:
3.内容动画训练部分。
这部分的意思就是,只针对内容本身来说的,比如一个“哈”,“啊‘字,每个人都会张开嘴,所以这部分就是提取的内容本身对人脸的关键点运动的特征。论文中所选择的就是使用LSTM/BiLSTM(对于时间或者其他序列经常使用到的模型),首先将这样的内容信息经过长短型记忆网络,再经过MLP网络,获得的是人脸关键点的偏移值。公式如下所示:LSTM的隐藏向量是 256,mlp中的隐藏层分别是512、256和204(68×3)。看了代码的模型书写,发现其实也使用了注意力机制,可以看下面对注意力机制的说明
第三部分是针对说话内容本身对人脸关键点的驱动,但是每个人有每个人的说话风格,有些人说话喜欢摆摆头,有些人说话就是不动的,有些人说话眼睛睁的很大,最大张的很大或者很小,如下图所示。所以这部分对语音驱动人脸是非常重要的。
作者论文中说到,我们使用说话人验证模型[Wan等人2018]提取说话人身份嵌入(说明这是一个已有的模型),该模型最大化了同一说话人不同话语之间的嵌入相似性,并最小化了不同说话人之间的相似性。当然可以看图得知,该部分还是应用了LSTM网络以及MLP网络。
from resemblyzer import preprocess_wav, VoiceEncoder
import numpy as np
import torch
import librosa
filename = 'ClapSound.wav'
newFilename = 'ClapSound_8k.wav'
# print(y.shape, y_8k.shape)
def get_spk_emb(audio_file_dir, segment_len=960000):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
resemblyzer_encoder = VoiceEncoder(device=device)
wav = preprocess_wav(audio_file_dir)
l = len(wav) // segment_len # segment_len = 16000 * 60
l = np.max([1, l])
all_embeds = []
for i in range(l):
mean_embeds, cont_embeds, wav_splits = resemblyzer_encoder.embed_utterance(
wav[segment_len * i:segment_len* (i + 1)], return_partials=True, rate=2)
all_embeds.append(mean_embeds)
all_embeds = np.array(all_embeds)
mean_embed = np.mean(all_embeds, axis=0)
return mean_embed, all_embeds
if __name__ == '__main__':
m, a = get_spk_emb(r'D:\PycharmProjects\pythonProject\output.wav')
print('Speaker embedding:', m)
print(m.shape)
结果如图所示:维度是256维
但是和第3部分不一样的是,为了产生连贯的头部运动和面部表情,它需要捕获更长的时间相关性。虽然音素通常持续几十毫秒,但头部运动(例如头部从左向右摆动)可能持续一秒或几秒,甚至更长几个数量级。为了捕捉这种长期且结构化的依赖关系,我们采用了自我关注网络,自我注意层计算输出,表示为每帧学习表示的加权组合,即音频内容的变换,在我们的例子中,与说话人嵌入和初始地标连接。分配给每个帧的权重由一个兼容性函数计算,该函数比较窗口中的所有对帧表示。(代码中还是会包含一下位置编码,如果是对语言的话,位置编码还是很重要的,但是对图像而言,位置编码效果其实并不是特别明显)
单注意力机制
多头注意力机制
class PositionalEncoder(nn.Module):
def __init__(self, d_model, max_seq_len=512):
super().__init__()
self.d_model = d_model
# create constant 'pe' matrix with values dependant on
# pos and i
pe = torch.zeros(max_seq_len, d_model)
for pos in range(max_seq_len):
for i in range(0, d_model, 2):
pe[pos, i] = \
math.sin(pos / (10000 ** ((2 * i) / d_model)))
pe[pos, i + 1] = \
math.cos(pos / (10000 ** ((2 * (i + 1)) / d_model)))
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
# make embeddings relatively larger
x = x * math.sqrt(self.d_model)
# add constant to embedding
seq_len = x.size(1)
x = x + self.pe[:, :seq_len].clone().detach().to(device)
return x
def attention(q, k, v, d_k, mask=None, dropout=None):
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
mask = mask.unsqueeze(1)
scores = scores.masked_fill(mask == 0, -1e9)
scores = F.softmax(scores, dim=-1)
if dropout is not None:
scores = dropout(scores)
output = torch.matmul(scores, v)
return output
class MultiHeadAttention(nn.Module):
def __init__(self, heads, d_model, dropout=0.1):
super().__init__()
self.d_model = d_model
self.d_k = d_model // heads
self.h = heads
self.q_linear = nn.Linear(d_model, d_model)
self.v_linear = nn.Linear(d_model, d_model)
self.k_linear = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
self.out = nn.Linear(d_model, d_model)
def forward(self, q, k, v, mask=None):
bs = q.size(0)
# perform linear operation and split into h heads
k = self.k_linear(k).view(bs, -1, self.h, self.d_k)
q = self.q_linear(q).view(bs, -1, self.h, self.d_k)
v = self.v_linear(v).view(bs, -1, self.h, self.d_k)
# transpose to get dimensions bs * h * sl * d_model
k = k.transpose(1, 2)
q = q.transpose(1, 2)
v = v.transpose(1, 2)
# calculate attention using function we will define next
scores = attention(q, k, v, self.d_k, mask, self.dropout)
# concatenate heads and put through final linear layer
concat = scores.transpose(1, 2).contiguous() \
.view(bs, -1, self.d_model)
output = self.out(concat)
return output
这一部分作者并没有说怎么所的,但是我想就是将人脸的一些点进行加权平均的来进行处理,具体的还是需要看代码所说明。
该部分用的大概率就是条件生成对抗网络(因为标签是图片,所以又称为pix2pix网络),如下如所示。
在本论文中所显示的步骤就是下图所示:每张图片进行生成后,然后通过opencv来将帧生成视频,但是生成的视频是没有声音的,所以我们还得去调用ffmpeg这python库,将原始的输入音频和这生成的视频进行结合,所以最终就会得到声音驱动人脸运动的视频的结果。
生成网络所使用的生成器结构
最后祝大家学有所成!