最近由于工作上的需求,需要使用Tensorflow加载语言模型 SpanBERT(Facebook 发布的 BERT 模型的变体),但是作者只发布了 Pytorch 版的预训练权重,因此需要将其转换为 Tensorflow 可以加载的 checkpoint。

在 Pytorch 框架下,大多数开发者使用 Huggingface 发布的 Transformers 工具来加载语言模型,它同时支持加载 Pytorch 和 Tensorflow 版的模型。但是,目前基于 Tensorflow(或 Keras)的工具基本上都不支持加载 Pytorch 版的 bin 模型,转换代码在网上也很难找到,这带来了很多不便。

bert

通过搜索,目前能够找到的有以下几个转换代码片段可供参考:

  • bin2ckpt:用于转换 TinyBERT,但实测并不可用;
  • convert_pytorch_checkpoint_to_tf:Transformers 自带的转换脚本,但官方文档中并没有提及;
  • pytorch_to_tf:实体同指任务的一篇论文提供的转换脚本;
  • PyTorch 版的 BERT 转换成 Tensorflow 版的 BERT:VoidOc 编写的基于 Transformers 的转换脚本。

通过分析可以看到,将 PyTorch 版 bin 模型转换成 Tensorflow 版 ckpt 的过程并不复杂,可以分为以下几步:

  1. 读取出模型中每一层网络结构的名称和参数;
  2. 针对 PyTorch 和 Tensorflow 的模型格式差异对参数做一些调整;
  3. 按照 Tensorflow 的格式保存模型。

读取和保存模型

PyTorch 和 Tensorflow 框架都提供了模型的读取和保存功能,因此读取和保存语言模型的过程非常简单。

读取模型直接使用 PyTorch 自带函数 torch.load() 或者 Transformers 提供的对应模型包的 from_pretrained() 函数就可以了;而保存模型则使用 Tensorflow 自带的模型保存器 tf.train.Saver 来完成。

以 BERT 模型为例,读取模型的过程就是:

1
2
3
4
model = BertModel.from_pretrained(
pretrained_model_name_or_path=pytorch_bin_path,
state_dict=torch.load(os.path.join(pytorch_bin_path, pytorch_bin_model), map_location='cpu')
)

或者

1
model = torch.load(os.path.join(pytorch_bin_path, pytorch_bin_model), map_location='cpu')

模型的保存过程则通过 Tensorflow 提供的保存器 tf.train.Saver 来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tf.reset_default_graph()
with tf.Session() as session:
for var_name in state_dict:
tf_name = to_tf_var_name(var_name) # 将层名称改为Tensorflow模型格式
torch_tensor = state_dict[var_name].numpy()
# 将参数矩阵改为Tensorflow模型格式
if any([x in var_name for x in tensors_to_transpose]):
torch_tensor = torch_tensor.T
tf_var = create_tf_var(tensor=torch_tensor, name=tf_name, session=session)
tf.keras.backend.set_value(tf_var, torch_tensor)
tf_weight = session.run(tf_var)
print("Successfully created {}: {}".format(tf_name, np.allclose(tf_weight, torch_tensor)))

saver = tf.train.Saver(tf.trainable_variables())
saver.save(session, os.path.join(ckpt_dir, model_name.replace("-", "_").replace(".ckpt", "") + ".ckpt"))

整个过程就是先逐层读取模型的层名称和对应参数,然后将格式调整为 Tensorflow 模型的格式,再一次性写入到 checkpoint 文件中。

注意:部分转换脚本忽略了 reset_default_graph() 这一操作,会导致生成的 meta 文件不仅保存网络结构,还会保存完整的网络参数,从而体积庞大。

调整模型格式

由于 PyTorch 和 Tensorflow 的模型格式定义有所差异,因此转换的关键就是对部分层的名称和参数矩阵进行调整。具体来说,首先需要构建名称映射字典,对部分层的名称进行调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var_map = (
("layer.", "layer_"),
("word_embeddings.weight", "word_embeddings"),
("position_embeddings.weight", "position_embeddings"),
("token_type_embeddings.weight", "token_type_embeddings"),
(".", "/"),
("LayerNorm/weight", "LayerNorm/gamma"),
("LayerNorm/bias", "LayerNorm/beta"),
("weight", "kernel"),
)

def to_tf_var_name(name: str):
for patt, repl in iter(var_map):
name = name.replace(patt, repl)
return "bert/{}".format(name)

注意:这里演示的是转换 BERT 模型,所以转换后的层名以 bert/ 开头。如果转换的是其他模型,需要做相应的修改。

然后,由于 PyTorch 和 Tensorflow 模型中 dense/kernel、attention/self/query、attention/self/key 和 attention/self/value 层的参数矩阵互为转置,因此还需要对模型中的对应层的参数进行调整:

1
2
3
4
tensors_to_transpose = ("dense.weight", "attention.self.query", "attention.self.key", "attention.self.value")

if any([x in var_name for x in tensors_to_transpose]):
torch_tensor = torch_tensor.T

至此,转换过程就全部完成了。

完整的代码

综上所述,将 PyTorch 版 bin 模型转换成 Tensorflow 版 ckpt 的过程还是比较清晰的。本文对 VoidOc 编写的脚本进行了进一步的简化,以转换 BERT 模型为例,完整的代码如下(Github):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# coding=utf-8

"""
Convert Huggingface Pytorch checkpoint to Tensorflow checkpoint.
"""

import numpy as np
import tensorflow.compat.v1 as tf
import torch
from transformers import BertModel
import os

def convert_pytorch_checkpoint_to_tf(model: BertModel, ckpt_dir: str, model_name: str):

"""
:param model:BertModel Pytorch model instance to be converted
:param ckpt_dir: Tensorflow model directory
:param model_name: model name
:return:

Currently supported Huggingface models:
Y BertModel
N BertForMaskedLM
N BertForPreTraining
N BertForMultipleChoice
N BertForNextSentencePrediction
N BertForSequenceClassification
N BertForQuestionAnswering
"""

tensors_to_transpose = ("dense.weight", "attention.self.query", "attention.self.key", "attention.self.value")

var_map = (
("layer.", "layer_"),
("word_embeddings.weight", "word_embeddings"),
("position_embeddings.weight", "position_embeddings"),
("token_type_embeddings.weight", "token_type_embeddings"),
(".", "/"),
("LayerNorm/weight", "LayerNorm/gamma"),
("LayerNorm/bias", "LayerNorm/beta"),
("weight", "kernel"),
)

if not os.path.isdir(ckpt_dir):
os.makedirs(ckpt_dir)

state_dict = model.state_dict()

def to_tf_var_name(name: str):
for patt, repl in iter(var_map):
name = name.replace(patt, repl)
return "bert/{}".format(name)

def create_tf_var(tensor: np.ndarray, name: str, session: tf.Session):
tf_dtype = tf.dtypes.as_dtype(tensor.dtype)
tf_var = tf.get_variable(dtype=tf_dtype, shape=tensor.shape, name=name, initializer=tf.zeros_initializer())
session.run(tf.variables_initializer([tf_var]))
session.run(tf_var)
return tf_var

tf.reset_default_graph()
with tf.Session() as session:
for var_name in state_dict:
tf_name = to_tf_var_name(var_name)
torch_tensor = state_dict[var_name].numpy()
if any([x in var_name for x in tensors_to_transpose]):
torch_tensor = torch_tensor.T
tf_var = create_tf_var(tensor=torch_tensor, name=tf_name, session=session)
tf.keras.backend.set_value(tf_var, torch_tensor)
tf_weight = session.run(tf_var)
print("Successfully created {}: {}".format(tf_name, np.allclose(tf_weight, torch_tensor)))

saver = tf.train.Saver(tf.trainable_variables())
saver.save(session, os.path.join(ckpt_dir, model_name.replace("-", "_").replace(".ckpt", "") + ".ckpt"))

def convert(pytorch_bin_path: str, pytorch_bin_model: str, tf_ckpt_path: str, tf_ckpt_model: str):

model = BertModel.from_pretrained(
pretrained_model_name_or_path=pytorch_bin_path,
state_dict=torch.load(os.path.join(pytorch_bin_path, pytorch_bin_model), map_location='cpu')
)

convert_pytorch_checkpoint_to_tf(model=model, ckpt_dir=tf_ckpt_path, model_name=tf_ckpt_model)

if __name__ == '__main__':
bin_path = './pretrained_model/pytorch_model/'
bin_model = 'pytorch_model.bin'
ckpt_path = './pretrained_model/tensorflow_model/'
ckpt_model = 'bert_model.ckpt'

convert(bin_path, bin_model, ckpt_path, ckpt_model)

转换过程被包装为 convert() 函数,输入 PyTorch 版 bin 模型的路径和名称,以及 Tensorflow 版 ckpt 的保存路径和名称即可。

再次提醒一下,由于本文转换的 SpanBERT 只是 BERT 的一个变体,因此模型的层名称是与 BERT 模型完全一致的,如果需要转换其他模型,请自行修改 to_tf_var_name() 函数和 tensors_to_transpose 变量。

2022-10-08 本地部署 stable-diffusion-webui 并替换模型为 waifu-diffusion(没有 NSFW 问题)

参考:

Git:https://git-scm.com/download

Miniconda:https://docs.conda.io/en/latest/miniconda.html

CUDA:https://developer.nvidia.com/cuda-toolkit-archive

waifu-diffusion:https://huggingface.co/hakurei/waifu-diffusion

waifu-diffusion-v1-3:https://huggingface.co/hakurei/waifu-diffusion-v1-3

stable-diffusion-v-1-4-original: https://huggingface.co/CompVis/stable-diffusion-v-1-4-original

stable-diffusion-webui:https://github.com/AUTOMATIC1111/stable-diffusion-webui

免责声明:本文所讲内容仅供学习使用,请勿用于违法用途!请遵从 开源协议 !


温馨提示:最近有些人在自己部署的时候遇到了问题,然后联系笔者,但笔者并没有时间来帮助这些人处理问题,故在此声明:

如果需要笔者协助解决问题,请先从博客底部的打赏支付 100 元,笔者承诺在能力范围内解决问题(不承诺解决问题)。

大家的时间都是很宝贵的,有问题先 百度/Google ,你遇到的问题一定已经有人遇到过了,所以请自己动手解决问题


开始

要顺利运行 stable-diffusion-webui,需要足够大的显存(不是内存),笔者使用的电脑配置如下:

型号:联想拯救者R9000P
CPU:R7-5800H 8核
内存:16GB
显卡:RTX3060 Laptop(显存 6GB)
硬盘:512GB SSD

根据项目的 README.md ,至少需要 4GB 显存才能运行,配置不够的同学就不用尝试了。

另外本项目是 N 卡教程, A 卡用户请自行搜索其他教程。

下面开始正式进入教程

准备工作

根据项目 wiki 必须的依赖 有:

  1. Python 3.10.6
  2. Git 官网下载
  3. CUDA,CUDA 的版本由显卡决定,后续会讲述如何选择 CUDA 版本

开始安装

1.安装 Python 3.10.6

和其他文章不同,笔者依旧推荐使用 Miniconda 来管理 Python 版本,可以实现 Python 多版本切换,下面来讲述如何安装 Miniconda。

进入Miniconda 官网,选择适用于自己的操作系统的版本,下载安装即可,全部图形化操作。

安装成功后在命令行执行

1
conda -V

成功跳出版本即为安装成功。

1
conda 4.12.0

这里不用着急创建 Python 环境,配置完镜像再创建。

接下来配置镜像环境,加快安装速度。(参考:https://mirrors.tuna.tsinghua.edu.cn/help/anaconda/

先执行 conda config --set show_channel_urls yes 生成 .condarc 文件

然后在 C:\Users\XXX.condarc 下修改文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
channels:
- defaults
show_channel_urls: true
default_channels:
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2
custom_channels:
conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
pytorch-lts: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud

再运行 conda clean -i 清除索引缓存,保证用的是镜像站提供的索引。

然后创建 Python 3.10.6 版本的环境

1
2
3
4
# 因为本项目是教 stable-diffusion-webui 的,所以就直接叫 stable-diffusion-webui 了
conda create --name stable-diffusion-webui python=3.10.6
# 激活环境,注意,在要运行的地方激活,这个环境切换不是全局的。
conda activate stable-diffusion-webui

创建环境的这一步有概率遇到 SSL 问题,可能跟 openssl 的版本有关,该问题请自行搜索解决

配置 pip 镜像加速(参考:https://mirrors.tuna.tsinghua.edu.cn/help/pypi/

升级 pip 到最新的版本 (>=10.0.0) 后进行配置:

1
2
python -m pip install --upgrade pip
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

如果您到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:

1
python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip

pip 这一步有概率遇到 https 无法访问的问题,可能跟 openssl 的版本有关,该问题请自行搜索解决

这时我们就有了一个 Python 3.10.6 的纯净环境了

2.安装 Git

前往 Git 官网 或其他镜像网站下载即可。

命令行运行

1
git --version

出现版本号即为安装成功

1
git version 2.33.1.windows.1

3.安装 CUDA

在安装 CUDA 之前,先在命令行执行

1
nvidia-smi

会跳出如下结果,只要看 CUDA Version一项即可,记住这个版本号,之后就下载这个版本即可。

比如说笔者这里就是 11.6版本,下载的 CUDA 也是这个版本就行了。

接下来前往 CUDA 的官网,下载对应版本即可。

CUDA 的版本很多,请一定要下对版本,建议就下载nvidia-smi查询出来的结果。

另外多说一句,CUDA 的体积很大,建议安装在 C 盘之外的硬盘,免得占用宝贵的 C 盘空间。

在版本的选择上,操作系统版本就不用多说了,选择自己的系统就行,win10 就选 10,win11 就选 11。

至于下面的 Installer Type,用 local 版本比较省事,下载一次就是完整的安装包了,用 network 版本依旧是要联网下载的,结果也没差太多。

4.下载项目源码

1
2
3
4
# 下载项目源代码
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
# 切换到项目根目录
cd stable-diffusion-webui

5.下载 stable-diffusion[可选]

本项目不会自动去下载模型,故需要手动下载 stable-diffusion

如果就使用 waifu-diffusion,可以跳过本步骤,直接看下一步

下载的方式有 3 个:

  1. 官网下载:https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt

  2. File storage:https://drive.yerf.org/wl/?id=EBfTrmcCCUAGaQBXVIj5lJmEhjoP1tgl

  3. Torrent:

1
magnet:?xt=urn:btih:3a4a612d75ed088ea542acac52f9f45987488d1c&dn=sd-v1-4.ckpt&tr=udp%3a%2f%2ftracker.openbittorrent.com%3a6969%2fannounce&tr=udp%3a%2f%2ftracker.opentrackr.org%3a1337

数据模型较大,请确保磁盘空间充足。

下载完成后的模型请放到 models\Stable-diffusion\ 下,并将文件名称修改为 model.ckpt

6. 下载 waifu-diffusion[可选]

要使用 waifu-diffusion 作为模型,也是一样的,从官网下载即可。

下载地址:https://huggingface.co/hakurei/waifu-diffusion-v1-3/tree/main

下载完成后的模型请放到 models\Stable-diffusion\ 下,并将文件名称修改为 model.ckpt

至于 ckpt 文件怎么选,选最近更新的,然后名字中带有 float16 的优先。当然也可以试试别的,自行尝试即可

正式运行

是的没错,stable-diffusion-webui 项目不需要手动安装依赖,他已经帮我们做好了,只需要运行即可。

1
2
3
4
# 如果没有切换到 stable-diffusion-webui 环境记得切换
conda activate stable-diffusion-webui
# 切换到项目根目录运行
webui-user.bat

接下来就是一段漫长的等待时间,下载的内容是非常多的(几个 G),请耐心等待。

依赖安装完毕后,会跳出一个链接,复制到浏览器访问即可(默认是 http://127.0.0.1:7860 )。

然后就在输入框里面写你想要生成的图片即可,相信各位的英语水平一定比笔者要高,在此就不赘述了。

【Prompt 里填写想要的特征点,Negative prompt 里填不想要的特征点】

温馨提示:Prompt 里最大只支持 77 个词,多了会报错,删掉几个词就行

可能是由于源码差异,在相同配置下,运行 waifu-diffusion 最大只能生成 512 x 512 ,但是运行 stable-diffusion-webui 时最大可以支持 832 x 832,可以说提高了一些性能。

另外多说一句,本项目和 waifu-diffusion 不同,没有 NSFW 问题!

最后

那么最后就是老问题了,要怎么写 prompt 呢?

答案是科学上网,自行前往推特上 Waifu Diffusion 相关话题下查看即可,点击 ALT,看下别人是怎么写的就行了。

完成以上步骤之后,相信你也能愉快的玩耍 Waifu Diffusion 了。

喜欢的话就请支持下笔者。

附录

2022-10-13 更新:关于如何写 tag ,可参考源站 Danbooru的写法,原因是 waifu-diffusion/stable-diffusion/Novel AI 等都有从该网址获取 tag 进行训练,所以直接使用源站的 tag 是最准确的

写个商城静态页练练手,又想要版心,又想用 grid 布局,要致富,先撸树,砰砰砰写好了

后来又想在 gird 的两行中间弄一条线分隔一下。

第一想到用 border-bottom,可是这玩意跑不出 gird 的手掌心啊,我要的 bar 可是要横穿整个页面哦。

算了,没有什么是定位解决不了的,如果有就砸键盘!!

1
<div class="bar"></div>
1
2
3
4
5
6
7
8
9
.bar{
top:100%;
left:50%;
transform:translateX(-50%);
position:absolute;
width:100vw;
height:2px;
background-color:red;
}

哪个元素下面需要横线,就塞个 <bar> 进去。

阅读全文 »

最近写个静态页练练 CSS,有些规则就是想不通。

比如下面这个关于模态框的 CSS,我打算给 dd 添加伪元素修饰一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.second_class {
display: none;
position: absolute;
z-index: 999;
left: 100%;
top: 0%;
min-height: 100%;
width: 800px;
background-color: #f7f7f7;
border-right: 1px solid #e6e6e6;
border: 1px solid #e6e6e6;
padding: 10px 12.5px 0 12.5px;
dl {
display: flex;
align-items: stretch;
flex-flow: row wrap;
padding-bottom: 10px;
// padding: 0 12.5px 0 12.5px;
dt,
dd {
// position: relative;
// padding: 2px 8px;
// margin: 0;
margin: 2px 0;
}

dt {
font-weight: 700;
}

dd:nth-of-type(n + 2)::before {
content: '';
display: inline-block;
margin: 0 10px;
width: 1px;
height: 70%;
align-items: flex-start;
vertical-align: middle;
background-color: red;
}

dd:first-of-type::before {
content: '';
margin: 0 1px 0 5px;
display: inline-block;
// width: 10px;
// height: 10px;
border-top: 5px solid transparent;
border-left: 7px solid dodgerblue;
border-right: 5px solid transparent;
border-bottom: 5px solid transparent;
// vertical-align: middle;
}

a:hover {
color: red;
}
}
}
阅读全文 »

自定义 webpack 作为构建工具

webpack 相关包

less 预编译: less, less-loader
提取 css: mini-css-extract-plugin
压缩 css: optimize-css-assets-webpack-plugin
css 兼容性处理: postcss, postcss-preset-env

js 兼容性处理: babel-loader, @babel/preset-env, @babel/core, corejs

样式中图片处理: url-loader
html 中图片处理: html-loader
其他文件处理: file-loader
html 模板打包: html-webpack-plugin

开发工具: webpack-dev-tool

开发框架:vue

阅读全文 »

前言

深拷贝和浅拷贝的区别,各位大小牛们应该都不陌生,这里主要给前端新人介绍一下,也帮助我自己复习一下。

为什么会突然讲这个呢,最近搬砖有点头晕目眩,趁着有空闲时刻,突然想练习一下递归,看看自己尚能饭否,结果就猪脑过载了。

后来就遇到了深拷贝,毕竟作为一道前端入门的经典面试题,以及和递归相关的题目,出场率也是挺高的。

阅读全文 »

cropper.js 官网

https://fengyuanchen.github.io/cropper/

作为搬砖小能手,每次使用插件时都要去百度各种配置项,实在太费劲,给自己总结记录插件的使用方法也是很重要的哦。

引入文件

三个文件都要按下面顺序引入哦,因为我用的 cropper.js 是 jquery 插件嘛,不知道你们用的是不是呢?

1
2
3
4
5
6
<!-- 引入jq -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<!-- 导入cropper的js -->
<script src="https://cdn.bootcdn.net/ajax/libs/cropperjs/2.0.0-alpha.2/cropper.min.js"></script>
<!-- 导入copper中间件 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cropper/1.0.1/jquery-cropper.min.js"></script>
阅读全文 »

前言

直接在 win10 上安装 mysql 其实不怎么优雅,毕竟要携带许多依赖环境。既然学了 docker,不用怎么行呢,接下来探讨如何用 docker 启动一个学习用的 mysql

环境准备

操作系统:win10
Docker版本:基于 wsl2 的 docker-desktop v19.03.13
docker-compose版本:1.27.4

阅读全文 »

当我们前端调试数据的时候,除了使用 mock,或者通过 node express 开启服务监听返回数据外,还有一种玩法,那就是今天试玩的 json-server

安装命令(全局安装)

1
npm install -g json-server

create a json file,名字随便起个 db.json,里面装个示例数据。

1
2
3
4
5
6
7
8
9
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" }
}
阅读全文 »

前言

虽说在下是 springboot 新手,但是作为前端接口调试而言,springboot 实在太过重量级。现在发现了一个作为测试服务器相当好用的玩意: NodeJs。话不多说,开搞。

  1. 安装 nodejs
  2. 在一个新文件夹初始化 npm npm init --yes
  3. 安装 express npm i express

设置 url 映射,并开启服务器监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// helloExpress.js
// 1、引入 express
var express = require('express')

// 2、创建应用对象
var app = express()

// 3、创建路由规则
// request 是对请求报文的封装
// response 是对响应报文的封装
app.get('/', (request, response) => {
response.send('Hello Express')
})

// 4、监听端口启动服务
app.listen(8000, () => {
console.log('服务已经启动,8000 端口监听中 ...')
})
阅读全文 »