机器学习模型是黑匣子。你可以在测试集上运行它们并绘制出奇特的性能曲线,但是通常还是很难回答关于它们如何运行的基本问题。一个惊人且强大的来源是简单地玩转模型:调整输入--观察输出。
创建交互模型是Streamlit的灵感来源之一,Streamlit是一个Python框架,它使得编写应用程序和编写Python脚本一样简单。这个概述将引导你创建一个streamlight应用程序,黑匣子最多的模型之一:一个深度生成的对抗网络(GAN)。
在本例中,我们将可视化Nvidia的PG-GAN使用TensorFlow,通过极少的素材来合成非常有真实感的人脸。然后,利用 TL-GAN 模型,创建一个应用程序,根据年龄、微笑、男性相似性和头发颜色等属性调整GAN合成的名人脸。在本教程结束时,你将拥有一个完全参数化的人类模型!
Streamlit入门
如果你还没有安装Streamlit,可以运行以下命令:
pip install streamlit
streamlit hello
如果你是一个经验丰富的Streamlit er,你将需要在0.57.1或更高版本,所以请确保升级!
pip install --upgrade streamlit
设置环境
在我们开始之前,使用下面的命令检查项目的GitHub repo,并为自己运行Face GAN演示。这个演示依赖于TensorFlow1,它不支持Python3.7或3.8,所以您需要Python3.6。在Mac和Linux上,我们建议使用pyenv在当前版本的同时安装Python 3.6,然后使用venv或virtualenv设置一个新的虚拟环境。在Windows上,Anaconda导航器允许你使用点击界面选择Python版本。
全部设置好后,打开终端窗口并键入:
git clone https://github.com/streamlit/demo-face-gan.git
cd demo-face-gan
pip install -r requirements.txt
streamlit run app.py
给它一分钟下载完训练的GAN,然后试着用滑块来探索GAN可以合成的不同人脸。很酷,对吧?
完整的应用程序代码是一个大约190行代码的文件,其中只有13行是Streamlit调用。没错,上面的整个用户界面都是从这13行画出来的!
让我们看看应用程序的结构:
def main():
st.title("Streamlit Face-GAN Demo")
# Step 1. Download models and data files.
for filename in EXTERNAL_DEPENDENCIES.keys():
download_file(filename)
# Step 2. Read in models from the data files.
tl_gan_model, feature_names = load_tl_gan_model()
session, pg_gan_model = load_pg_gan_model()
# Step 3. Draw the sidebar UI.
...
features = ... # Internally, this uses st.sidebar.slider(), etc.
# Step 4. Synthesize the image.
with session.as_default():
image_out = generate_image(session, pg_gan_model, tl_gan_model,
features, feature_names)
# Step 5. Draw the synthesized image.
st.image(image_out, use_column_width=True)
既然你已经了解了它的结构,那么让我们深入了解上面5个步骤中的每一个,看看它们是如何工作的。
第一步:下载模型和数据文件
这一步下载我们需要的文件:一个预先训练过的PG-GAN模型和一个预先安装到其中的TL-GAN模型(稍后我们将深入研究这些模型)。
下载文件实用程序功能比纯下载程序要聪明一点:
- 它检查文件是否已经存在于本地目录中,因此只在需要时下载。它还检查下载文件的大小是否达到我们预期的大小,因此它能够修复中断的下载。
# If the file exists and has the expected size, return.
if os.path.exists(file_path):
if "size" not in EXTERNAL_DEPENDENCIES[file_path]:
return
elif os.path.getsize(file_path) == EXTERNAL_DEPENDENCIES[file_path]["size"]:
return
它使用 st.progress()和 st.warning()在文件下载时向用户显示一个漂亮的UI。然后它调用那些UI元素上的.empty()来在完成时隐藏它们。
# Draw UI elements.
weights_warning = st.warning("Downloading %s..." % file_path)
progress_bar = st.progress(0)
with open(file_path, "wb") as output_file:
with urllib.request.urlopen(...) as response:
...
while True:
... # Save downloaded bytes to file here.
# Update UI elements.
weights_warning.warning(
"Downloading %s... (%6.2f/%6.2f MB)" %
(file_path, downloaded_size))
progress_bar.progress(downloaded_ratio)
...
# Clear UI elements when done.
weights_warning.empty()
progress_bar.empty()
第二步:将模型加载到内存中
下一步是将这些模型加载到内存中。下面是加载PG-GAN模型的代码:
@st.cache(allow_output_mutation=True, hash_funcs=TL_GAN_HASH_FUNCS)
def load_pg_gan_model():
"""
Create the tensorflow session.
"""
config = tf.ConfigProto(allow_soft_placement=True)
session = tf.Session(config=config)
with session.as_default():
with open(MODEL_FILE_GPU if USE_GPU else MODEL_FILE_CPU, 'rb') as f:
G = pickle.load(f)
return session, G
注意 load_pg_gan_model()开头的 @st.cache decorator。通常在Python中,只需运行load_pg_gan_model()并反复重用该变量。然而,Streamlit 的执行模型是独一无二的,因为每次用户与UI小部件交互时,脚本都会从上到下重新完整地执行。通过将 @st.cache 添加到代价高昂的模型加载函数中,我们告诉Streamlit只在脚本第一次执行时运行这些函数,然后在之后的每次执行中重用缓存输出。这是Streamlit最基本的特性之一,因为它允许你通过缓存函数调用的结果来高效地运行脚本。这样,大型拟合GAN模型将只加载到内存中一次;同样,我们的TensorFlow会话也将只创建一次。
不过,这里有一个问题:TensorFlow会话对象在我们使用它运行不同的计算时可能会在内部发生变化。通常,我们不希望缓存对象发生变化,因为这会导致意外的结果。所以当Streamlit检测到这种突变时,它会向用户发出警告。但是,在这种情况下,我们碰巧知道,如果TensorFlow会话对象发生变化,则可以,因此我们通过设置 allow_output_mutation=True.
第三步:绘制边栏UI
通过调用诸如st.slider()和st.checkbox()之类的API方法添加小部件。
这些方法的返回值是UI中显示的值。例如,当用户将滑块移动到位置42时,将重新执行脚本,在该执行中,st.slider()的返回值将为42。
你可以把任何东西放在一个侧边栏前加上st.sidebar。例如,st.sidebar.checkbox()。
因此,要在侧边栏中添加滑块,例如-允许用户调整棕色头发参数的滑块,只需添加:
brown_hair = st.sidebar.slider("Brown Hair", 0, 100, 50, step=5)
# Translation: Draw a slider from 0 to 100 with steps of size 5.
# Then set the default value to 50.
在我们的应用程序中,我们想得到一个小小的幻想,展示如何轻松地使用户界面本身可修改在Streamlit!我们希望允许用户首先使用multiselect小部件在生成的图像中选择一组要控制的功能,这意味着我们的UI需要以编程方式绘制:
使用Streamlit,代码实际上非常简单:
st.sidebar.title('Features')
...
# If the user doesn't want to select which features to control, these will be used.
default_control_features = ['Young','Smiling','Male']
if st.sidebar.checkbox('Show advanced options'):
# Randomly initialize feature values.
features = get_random_features(feature_names, seed)
# Let the user pick which features to control with sliders.
control_features = st.sidebar.multiselect(
'Control which features?',
sorted(features), default_control_features)
else:
features = get_random_features(feature_names, seed)
# Don't let the user pick feature values to control.
control_features = default_control_features
# Insert user-controlled values from sliders into the feature vector.
for feature in control_features:
features[feature] = st.sidebar.slider(feature, 0, 100, 50, 5)
第四步:合成图像
现在我们有了一组特征告诉我们要合成什么样的脸,我们需要做合成脸的繁重工作。我们的方法是将特征传递到TL-GAN中,在PG-GAN的潜在空间中生成一个向量,然后将该向量馈送给PG-GAN。如果这句话对你来说毫无意义,让我们绕道谈谈我们的两个神经网络是如何工作的。
进入GANs的另一条路径
要了解上述应用程序如何从滑块值生成面,你首先必须了解 PG-GAN 和 TL-GAN 是如何工作的。
PG-GAN和任何GAN一样,基本上都是一对神经网络,一个是生成的,一个是辨别的,它们相互训练,永远锁定在致命的战斗中。生成网络负责合成它认为像人脸的图像,而判别网络负责决定图像是否真的是人脸。这两个网络根据彼此的输出进行迭代训练,因此每个网络都尽其所能地学习愚弄另一个网络。最终的结果是最终生成的网络能够合成逼真的人脸,即使在训练开始时它所能合成的只是随机噪声。真是太神奇了!在这种情况下,我们使用的人脸生成GAN由Karras等人使用其渐进增长的GANs算法(PG-GAN)在名人脸上训练,该算法使用渐进的高分辨率图像训练GANs。
PG-GAN的输入是一个高维向量,属于其所谓的潜在空间。潜在空间基本上是网络可以生成的所有可能的面的空间,因此该空间中的每个随机向量对应一个唯一的面(或者至少应该是这样的!)!有时你会得到奇怪的结果……)通常使用GAN的方法是给它一个随机向量,然后检查合成了什么样的人脸(如下图)。
不过,这听起来有点枯燥,我们宁愿对输出有更多的控制。我们想告诉PG-GAN“生成一个有胡子的男人的图像”,或者“生成一个棕色头发的女人的图像”。这就是TL-GAN的来源。
TL-GAN是另一种神经网络,它通过将随机向量输入到PG-GAN中,提取生成的人脸,并通过分类器对其进行训练,以获得“看起来很年轻”、“有胡须”、“棕色头发”等属性。在训练阶段,TL-GAN用这些分类器对PG-GAN中的数千张人脸进行标记识别潜在空间中与我们关心的标签变化相对应的方向。结果,TL-GAN学习如何将这些类(即“年轻型”、“胡须型”、“棕色头发”)映射到应输入PG-GAN的适当随机型向量中,以生成具有这些特征的面部(如下图)。
回到我们的应用程序,现在我们已经下载了预先训练好的GAN模型并将其加载到内存中,我们还从UI中获取了一个特征向量。所以现在我们只需要将这些特征输入TL-GAN,然后PG-GAN,就可以得到图像:
@st.cache(show_spinner=False, hash_funcs={tf.Session: id})
def generate_image(session, pg_gan_model, tl_gan_model, features, feature_names):
# Create rescaled feature vector.
feature_values = np.array([features[name] for name in feature_names])
feature_values = (feature_values - 50) / 250
# Multiply by Shaobo's matrix to get the latent variables.
latents = np.dot(tl_gan_model, feature_values)
latents = latents.reshape(1, -1)
dummies = np.zeros([1] + pg_gan_model.input_shapes[1][1:])
# Feed the latent vector to the GAN in TensorFlow.
with session.as_default():
images = pg_gan_model.run(latents, dummies)
# Rescale and reorient the GAN's output to make an image.
images = np.clip(np.rint((images + 1.0) / 2.0 * 255.0),
0.0, 255.0).astype(np.uint8) # [-1,1] => [0,255]
if USE_GPU:
images = images.transpose(0, 2, 3, 1) # NCHW => NHWC
return images[0]
优化性能
上面的generate_image()函数可能需要一些时间才能执行,特别是在CPU上运行时。为了提高我们的应用程序的性能,如果我们可以缓存该函数的输出,那么我们就不必在来回移动滑块时重新合成我们已经看到的面。
好吧,正如您可能已经在上面的代码片段中注意到的,这里的解决方案是再次使用@st.cache decorator。
但是注意我们在本例中传递给@st.cache的两个参数:show_spinner=False和hash_funcs={tf.Session:id}。那些是干什么用的?
第一个很容易解释:默认情况下,@st.cache在UI中显示一个状态框,让您知道当前正在执行一个运行缓慢的函数。我们称之为“旋转器”。但是,在这种情况下,我们希望避免显示它,这样用户界面不会意外地跳转。所以我们将show_spinner设置为False。
下一个解决了一个更复杂的问题:TensorFlow会话对象(作为参数传递以生成_image())通常在这个缓存函数的两次运行之间由TensorFlow的内部函数进行变异。这意味着用于生成_image()的输入参数将始终不同,并且我们永远不会真正获得缓存命中。换句话说,@st.cache装饰器实际上不会做任何事情!我们如何解决这个问题?
调用hash_funcs
hash_funcs选项允许我们指定自定义散列函数,告诉@st.cache在检查这是缓存命中还是缓存未命中时应如何解释不同的对象。在本例中,我们将使用该选项告诉Streamlit通过调用Python的id()函数而不是通过检查其内容来散列TensorFlow会话:
@st.cache(..., hash_funcs={tf.Session: id})
def generate_image(session, ...):
...
这对我们有效,因为在我们的例子中,会话对象实际上是底层代码所有执行过程中的一个单例,因为它来自@st.cache'd load_pg_gan_model()函数。
第五步:绘制合成图像
现在我们有了输出图像,绘制它是小菜一碟!只需调用Streamlit的st.image函数:
st.image(image_out, use_column_width=True)
我们完成了!
最后
所以,你得到了一个人脸合成程序:在190行Streamlit应用程序中与TensorFlow交互的面部合成,只有13个Streamlit函数调用!
Github项目地址:https://github.com/streamlit/demo-face-gan
作者:阿德里安·特里耶(Adrien Treuille)
参考文献:
[1] T. Karras,T.Aila,S.Laine和J. Lehtinen。GAN的逐步生长可提高质量,稳定性和变异性。国际学习代表大会(ICLR 2018)
[2]关。使用新型TL-GAN模型控制图像的合成和编辑。Insight数据科学博客(2018)
[3] Liu Z.,P.Luo,X.Wang,X.Tang。野外深度学习面部属性。国际计算机视觉会议(ICCV 2015)
本文翻译:未艾信息(http://www.weainfo.net)
更多有趣又好玩的AI技术项目,欢迎关注我们的公众号:为AI呐喊(weainahan)
也可以关注我们的Python入门专栏《7小时快速掌握Python核心编程》!