强化学习:

代码学习网站:

教程:https://stable-baselines.readthedocs.io/en/master/guide/examples.html

gym使用

在做rl时候 ,如何利用gym将动画动起来,让每一步训练过程可视化:

例程:

import gym

from stable_baselines import DQN
from stable_baselines.common.evaluation import evaluate_policy


# Create environment
env = gym.make('LunarLander-v2')

# Instantiate the agent
model = DQN('MlpPolicy', env, learning_rate=1e-3, prioritized_replay=True, verbose=1)
# Train the agent
model.learn(total_timesteps=int(2e5))
# Save the agent
model.save("dqn_lunar")
del model  # delete trained model to demonstrate loading

# Load the trained agent
model = DQN.load("dqn_lunar")

# Evaluate the agent
mean_reward, std_reward = evaluate_policy(model, model.get_env(), n_eval_episodes=10)

# Enjoy trained agent
obs = env.reset()
for i in range(1000):
    action, _states = model.predict(obs)
    obs, rewards, dones, info = env.step(action)
    env.render()
https://cdn-images-1.medium.com/max/960/1*h4WTQNVIsvMXJTCpXm_TAw.gif
from stable_baselines.common.cmd_util import make_atari_env
from stable_baselines.common.vec_env import VecFrameStack
from stable_baselines import ACER

# There already exists an environment generator
# that will make and wrap atari environments correctly.
# Here we are also multiprocessing training (num_env=4 => 4 processes)
env = make_atari_env('PongNoFrameskip-v4', num_env=4, seed=0)
# Frame-stacking with 4 frames
env = VecFrameStack(env, n_stack=4)

model = ACER('CnnPolicy', env, verbose=1)
model.learn(total_timesteps=25000)

obs = env.reset()
while True:
    action, _states = model.predict(obs)
    obs, rewards, dones, info = env.step(action)
    env.render()
https://cdn-images-1.medium.com/max/960/1*UHYJE7lF8IDZS_U5SsAFUQ.gif

bug解决:

在执行时:

import gym
env = gym.make('ALE/Pong-v5')
env.reset()

for i in range(1000):
    env.step(env.action_space.sample())
    env.render()
env.close()

输出,无法对fream进行渲染。

ImportError: cannot import name ‘rendering’ from ‘gym.envs.classic_control’.

解决办法:

打开 包gym.envs.classic_control,发现没有 rendering .py文件,去github,发现,main分支确实已经没有这个文件了,应该是版本的问题,最新版本已经去掉了该文件,然而其他分支是有的,所以将该文件下载并放在包对应位置。

此外,还需要 在代码中加入

from gym.envs.classic_control import rendering

导入rendering.py

yolov3 -tiny 网络实现和源码分析

摘自:https://blog.csdn.net/alangaixiaoxiao/article/details/105533746

相关推荐

网上的资源很多,也有很多博主的原理讲解。在这里推荐几个资源:
darknet官网:https://pjreddie.com/darknet/yolo/ linux系统
github上基于VS的工程:https://github.com/AlexeyAB/darknet
github上带有注释的工程:https://github.com/hgpvision/darknet

yolov3-tiny 原理

Yolo算法采用一个单独的CNN模型实现end-to-end的目标检测,首先将输入图片resize到448×448,然后送入CNN网络,最后处理网络预测结果得到检测的目标。
YOLO 的核心思想就是利用整张图作为网络的输入,直接在输出层回归 bounding box(边界框) 的位置及其所属的类别。将一幅图像分成 SxS 个网格(grid cell),如果某个 object 的中心落在这个网格中,则这个网格就负责预测这个 object。

每个 bounding box 要预测 (x, y, w, h) 和 confidence 共5个值,每个网格还要预测一个类别信息,记为 C 类。则 SxS个 网格,每个网格要预测 B 个 bounding box, 每个box中都有 C 个 classes对应的概率值。输出就是 S x S x B x(5+C) 的一个 tensor。

注意:class 信息是针对每个网格的,confidence 信息是针对每个 bounding box 的。

yolov3-tiny中,共有两个输出层(yolo层),分别为13×13和26×26,每个网格可以预测3个bounding box,共有80个分类数。所以最后的yolo层的尺寸为:13x13x255和26x26x255。
yolov3-tiny网络层结构如下:

可以看出,yolov3-tiny共有23层网络,其中包含五种不同的网络层:卷积层convolutional(13个),池化层maxpool(6个),路由层route(2个),上采样层upsample(1个),输出层yolo(2个)。Yolov3-tiny中,除了Yolo层之前的那个卷积层,每个卷积层之后都有BN层,且每个卷积层之后都有激活函数LEAKY(yolo层之前是linear)。

yolov3-tiny 源码分析

配置网络结构

yolov3-tiny前向传播主要在detector.c中的test_detector函数中完成:

/** 本函数是检测模型的一个前向推理测试函数.
* @param datacfg       数据集信息文件路径(也即cfg/*.data文件),文件中包含有关数据集的信息,比如cfg/coco.data
* @param cfgfile       网络配置文件路径(也即cfg/*.cfg文件),包含一个网络所有的结构参数,比如cfg/yolo.cfg
* @param weightfile    已经训练好的网络权重文件路径,比如darknet网站上下载的yolo.weights文件
* @param filename      待进行检测的图片路径(单张图片)
* @param thresh        阈值,类别检测概率大于该阈值才认为其检测结果有效
* @param hier_thresh
* @param outfile
* @param fullscreen
* @details 该函数为一个前向推理测试函数,不包括训练过程,因此如果要使用该函数,必须提前训练好网络,并加载训练好的网络参数文件,
*          这些文件可以在作者网站上根据作者的提示下载到。本函数由darknet.c中的主函数调用,严格来说,本文件不应纳入darknet网络结构文件夹中,
*          其只是一个测试文件,或者说是一个example,应该放入到example文件夹中(新版的darknet已经这样做了,可以在github上查看)。
*          本函数的流程为:.
*/
void test_detector(char *datacfg, char *cfgfile, char *weightfile, char *filename, float thresh,
    float hier_thresh, int dont_show, int ext_output, int save_labels, char *outfile, int letter_box, int benchmark_layers)
{
	// 从指定数据文件datacfg(.data文件)中读入数据信息(测试、训练数据信息)到options中
	// options是list类型数据,其中的node包含的void指针具体是kvp数据类型,具有键值和值(类似C++中的Map)
    list *options = read_data_cfg(datacfg);
	// 获取数据集的名称(包括路径),第二个参数"names"表明要从options中获取所用数据集的名称信息(如names = data/coco.names)
    char *name_list = option_find_str(options, "names", "data/names.list");
    int names_size = 0;
	// 从data/**.names中读取物体名称/标签信息
    char **names = get_labels_custom(name_list, &names_size); //get_labels(name_list);
	
    // 加载data/labels/文件夹中所有的字符标签图片
    image **alphabet = load_alphabet();

    network net = parse_network_cfg_custom(cfgfile, 1, 1); // set batch=1  配置各网络层参数,重要

在parser.c中的parse_network_cfg_custom函数中,根据yolov3-tiny.cfg文件对网络结构进行配置,明确各层网络的类型、输入输出通道数、图像尺寸、卷积核大小等。

//配置各网络层参数
network parse_network_cfg_custom(char *filename, int batch, int time_steps)
{
	// 从神经网络结构参数文件中读入所有神经网络层的结构参数,存储到sections中,
	// sections的每个node包含一层神经网络的所有结构参数
    list *sections = read_cfg(filename);
	// 获取sections的第一个节点,可以查看一下cfg/***.cfg文件,其实第一块参数(以[net]开头)不是某层神经网络的参数,
	// 而是关于整个网络的一些通用参数,比如学习率,衰减率,输入图像宽高,batch大小等,
	// 具体的关于某个网络层的参数是从第二块开始的,如[convolutional],[maxpool]...,
	// 这些层并没有编号,只说明了层的属性,但层的参数都是按顺序在文件中排好的,读入时,
	// sections链表上的顺序就是文件中的排列顺序。
    node *n = sections->front;
    if(!n) error("Config file has no sections");
	// 创建网络结构并动态分配内存:输入网络层数为sections->size - 1,sections的第一段不是网络层,而是通用网络参数
    network net = make_network(sections->size - 1);
	// 所用显卡的卡号(gpu_index在cuda.c中用extern关键字声明)
	// 在调用parse_network_cfg()之前,使用了cuda_set_device()设置了gpu_index的值号为当前活跃GPU卡号
    net.gpu_index = gpu_index;
	// size_params结构体元素不含指针变量
    size_params params;

    if (batch > 0) params.train = 0;    // allocates memory for Detection only
    else params.train = 1;              // allocates memory for Detection & Training

    section *s = (section *)n->val;
    list *options = s->options;
    if(!is_network(s)) error("First section must be [net] or [network]");
    parse_net_options(options, &net);

#ifdef GPU
    printf("net.optimized_memory = %d \n", net.optimized_memory);
    if (net.optimized_memory >= 2 && params.train) {
        pre_allocate_pinned_memory((size_t)1024 * 1024 * 1024 * 8);   // pre-allocate 8 GB CPU-RAM for pinned memory
    }
#endif  // GPU

    params.h = net.h;
    params.w = net.w;
    params.c = net.c;
    params.inputs = net.inputs;
    if (batch > 0) net.batch = batch;
    if (time_steps > 0) net.time_steps = time_steps;
    if (net.batch < 1) net.batch = 1;
    if (net.time_steps < 1) net.time_steps = 1;
    if (net.batch < net.time_steps) net.batch = net.time_steps;
    params.batch = net.batch;
    params.time_steps = net.time_steps;
    params.net = net;
    printf("mini_batch = %d, batch = %d, time_steps = %d, train = %d \n", net.batch, net.batch * net.subdivisions, net.time_steps, params.train);

    int avg_outputs = 0;
    float bflops = 0;
    size_t workspace_size = 0;
    size_t max_inputs = 0;
    size_t max_outputs = 0;
    n = n->next;
    int count = 0;
    free_section(s);

	// 此处stderr不是错误提示,而是输出结果提示,提示网络结构
    fprintf(stderr, "   layer   filters  size/strd(dil)      input                output\n");
    while(n){
        params.index = count;
        fprintf(stderr, "%4d ", count);
        s = (section *)n->val;
        options = s->options;
		// 定义网络层
        layer l = { (LAYER_TYPE)0 };
		// 获取网络层的类别

        LAYER_TYPE lt = string_to_layer_type(s->type);
		
		//通过读取网络类型,从而配置各网络层的参数
        if(lt == CONVOLUTIONAL){//yolov3-tiny  卷积层  13层
            l = parse_convolutional(options, params);
        }else if(lt == LOCAL){
            l = parse_local(options, params);
        }else if(lt == ACTIVE){
            l = parse_activation(options, params);
        }else if(lt == RNN){
            l = parse_rnn(options, params);
        }else if(lt == GRU){
            l = parse_gru(options, params);
        }else if(lt == LSTM){
            l = parse_lstm(options, params);
        }else if (lt == CONV_LSTM) {
            l = parse_conv_lstm(options, params);
        }else if(lt == CRNN){
            l = parse_crnn(options, params);
        }else if(lt == CONNECTED){
            l = parse_connected(options, params);
        }else if(lt == CROP){
            l = parse_crop(options, params);
        }else if(lt == COST){
            l = parse_cost(options, params);
            l.keep_delta_gpu = 1;
        }else if(lt == REGION){
            l = parse_region(options, params);
            l.keep_delta_gpu = 1;
        }else if (lt == YOLO) {//yolov3-tiny YOLO层  两层
            l = parse_yolo(options, params);
            l.keep_delta_gpu = 1;
        }else if (lt == GAUSSIAN_YOLO) {
            l = parse_gaussian_yolo(options, params);
            l.keep_delta_gpu = 1;
        }else if(lt == DETECTION){
            l = parse_detection(options, params);
        }else if(lt == SOFTMAX){
            l = parse_softmax(options, params);
            net.hierarchy = l.softmax_tree;
            l.keep_delta_gpu = 1;
        }else if(lt == NORMALIZATION){
            l = parse_normalization(options, params);
        }else if(lt == BATCHNORM){
            l = parse_batchnorm(options, params);
        }else if(lt == MAXPOOL){//yolov3-tiny 池化层 maxpool  6层
            l = parse_maxpool(options, params);
        }else if (lt == LOCAL_AVGPOOL) {
            l = parse_local_avgpool(options, params);
        }else if(lt == REORG){
            l = parse_reorg(options, params);        }
        else if (lt == REORG_OLD) {
            l = parse_reorg_old(options, params);
        }else if(lt == AVGPOOL){
            l = parse_avgpool(options, params);
        }else if(lt == ROUTE){//yolov3-tiny 路由层 2层
            l = parse_route(options, params);
            int k;
            for (k = 0; k < l.n; ++k) {
                net.layers[l.input_layers[k]].use_bin_output = 0;
                net.layers[l.input_layers[k]].keep_delta_gpu = 1;
            }
        }else if (lt == UPSAMPLE) {//yolov3-tiny 上采样层 1层
            l = parse_upsample(options, params, net);
        }else if(lt == SHORTCUT){
            l = parse_shortcut(options, params, net);
            net.layers[count - 1].use_bin_output = 0;
            net.layers[l.index].use_bin_output = 0;
            net.layers[l.index].keep_delta_gpu = 1;
        }else if (lt == SCALE_CHANNELS) {
            l = parse_scale_channels(options, params, net);
            net.layers[count - 1].use_bin_output = 0;
            net.layers[l.index].use_bin_output = 0;
            net.layers[l.index].keep_delta_gpu = 1;
        }
        else if (lt == SAM) {
            l = parse_sam(options, params, net);
            net.layers[count - 1].use_bin_output = 0;
            net.layers[l.index].use_bin_output = 0;
            net.layers[l.index].keep_delta_gpu = 1;
        }else if(lt == DROPOUT){
            l = parse_dropout(options, params);
            l.output = net.layers[count-1].output;
            l.delta = net.layers[count-1].delta;
            .........

下载权重文件

在parser.c的load_weights_upto中,根据卷积层的网络配置,开始下载读取各层的权重文件。

//读取权重文件函数
void load_weights_upto(network *net, char *filename, int cutoff)//cutoff = net->n
{
#ifdef GPU
    if(net->gpu_index >= 0){
        cuda_set_device(net->gpu_index);
    }
#endif
    fprintf(stderr, "Loading weights from %s...\n", filename);
    fflush(stdout);
    FILE *fp = fopen(filename, "rb");
    if(!fp) file_error(filename);

    int major;
    int minor;
    int revision;
    fread(&major, sizeof(int), 1, fp);//读取一个4字节的数据
    fread(&minor, sizeof(int), 1, fp);//读取一个4字节的数据
    fread(&revision, sizeof(int), 1, fp);//读取一个4字节的数据
	printf("the size of int in x64 is %d bytes,attention!!!\n", sizeof(int));//x86 x64: 4
	printf("major ,minor,revision of weight is %d, %d ,%d\n", major, minor, revision);//0.2.0
    if ((major * 10 + minor) >= 2) {//运行这一部分
        printf("\n seen 64");
        uint64_t iseen = 0;
        fread(&iseen, sizeof(uint64_t), 1, fp);//读取一个8字节的数据
		printf("the size of uint64_t is %d\n", sizeof(uint64_t));
        *net->seen = iseen;
    }
    else {
        printf("\n seen 32");
        uint32_t iseen = 0;
        fread(&iseen, sizeof(uint32_t), 1, fp);
        *net->seen = iseen;
    }
    *net->cur_iteration = get_current_batch(*net);
    printf(", trained: %.0f K-images (%.0f Kilo-batches_64) \n", (float)(*net->seen / 1000), (float)(*net->seen / 64000));
    int transpose = (major > 1000) || (minor > 1000);

    int i;
    for(i = 0; i < net->n && i < cutoff; ++i){//cutoff = net->n
        layer l = net->layers[i];
        if (l.dontload) continue;//always 0		跳过之后的循环体,直接运行++i
        if(l.type == CONVOLUTIONAL && l.share_layer == NULL){ //只运行这一个分支的代码
            load_convolutional_weights(l, fp);
			//printf("network layer [%d] is CONVOLUTIONAL \n",i);
        }
        .......

在读取yolov3-tiny各层权重文件前,先读取4个和训练有关的参数:major,minor, revision和iseen。在前向传播的工程当中,并没有实际的应用。

parser.c中的load_convolutional_weights函数,具体执行对yolov3-tiny权重文件的下载,包括节点参数weight,偏置参数bias和批量归一化参数BN。

void load_convolutional_weights(layer l, FILE *fp)
{
	static int flipped_num;
    if(l.binary){
        //load_convolutional_weights_binary(l, fp);
        //return;
    }
    int num = l.nweights;
	//int num = l.n*l.c*l.size*l.size;//l.n 输出的层数 l.c输入的层数 
    int read_bytes;
    read_bytes = fread(l.biases, sizeof(float), l.n, fp);//读取偏置参数 l.n个float数据
    if (read_bytes > 0 && read_bytes < l.n) printf("\n Warning: Unexpected end of wights-file! l.biases - l.index = %d \n", l.index);
    //fread(l.weights, sizeof(float), num, fp); // as in connected layer
    if (l.batch_normalize && (!l.dontloadscales)){
        read_bytes = fread(l.scales, sizeof(float), l.n, fp);//读取batch normalize 参数  l.n个float数据
        if (read_bytes > 0 && read_bytes < l.n) printf("\n Warning: Unexpected end of wights-file! l.scales - l.index = %d \n", l.index);
        read_bytes = fread(l.rolling_mean, sizeof(float), l.n, fp);//读取batch normalize 参数  l.n个float数据
        if (read_bytes > 0 && read_bytes < l.n) printf("\n Warning: Unexpected end of wights-file! l.rolling_mean - l.index = %d \n", l.index);
        read_bytes = fread(l.rolling_variance, sizeof(float), l.n, fp);//读取batch normalize 参数  l.n个float数据
        if (read_bytes > 0 && read_bytes < l.n) printf("\n Warning: Unexpected end of wights-file! l.rolling_variance - l.index = %d \n", l.index);

将权重参数批量归一化

yolov3-tiny每个卷积层之后,激活函数之前,都要对结果进行Batch Normalization:在这里插入图片描述
由于BN层和卷积操作都是线性的,将权重文件进行批量归一化,可以代替卷积层之后的BN层:
在这里插入图片描述
在network.c的fuse_conv_batchnorm函数中实现权重文件和BN层的合并。

void fuse_conv_batchnorm(network net)
{
    int j;
    for (j = 0; j < net.n; ++j) {
        layer *l = &net.layers[j];
		    // printf("the %d layer batch_normalize is %d,   groups is %d \n", j, l->batch_normalize, l->groups);
        if (l->type == CONVOLUTIONAL) { //只运行这一分支   合并卷积层和batch_normal
             //printf(" Merges Convolutional-%d and batch_norm \n", j);

            if (l->share_layer != NULL) {//l->share_layer always is 0,不运行这个分支
                l->batch_normalize = 0;
            }

            if (l->batch_normalize) {//#15,22层卷积,卷积之后没有batch normalize,其他都要运行这一分支
                int f;
                for (f = 0; f < l->n; ++f)//该层神经网络 1->n 个输出层权重
                {
                    l->biases[f] = l->biases[f] - (double)l->scales[f] * l->rolling_mean[f] / (sqrt((double)l->rolling_variance[f] + .00001));

                    const size_t filter_size = l->size*l->size*l->c / l->groups;//kernel_size * kernel_size * c/分组  l->groups存在于卷积层always is 1
                    int i;
                    for (i = 0; i < filter_size; ++i) {
                        int w_index = f*filter_size + i;

                        l->weights[w_index] = (double)l->weights[w_index] * l->scales[f] / (sqrt((double)l->rolling_variance[f] + .00001));
                    }
                }

                free_convolutional_batchnorm(l);//no use
                l->batch_normalize = 0;
                ......

输入图像

yolov3-tiny输入神经网络的图像尺寸为416×416,对不符合该尺寸的图像,要进行裁剪。在image.c的resize_image函数中完成。这个可以说是整个yolo算法对输入图像唯一进行预处理的地方了。这也是yolo算法在工程应用中极好的地方,没有那么多类似于降噪、滤波之类的预处理,直接送到网络里就完事了。

//im:输入图片  w:416 h:416
//函数作用:将输入图片热size到416x416的尺寸,基本按照缩放/扩大的策略
image resize_image(image im, int w, int h)
{
    if (im.w == w && im.h == h) return copy_image(im);

    image resized = make_image(w, h, im.c);//416 x 416 x 3空的地址空间
    image part = make_image(w, im.h, im.c);//416 x im.h x im.c空的地址空间
    int r, c, k;
    float w_scale = (float)(im.w - 1) / (w - 1);//宽度缩放因子
    float h_scale = (float)(im.h - 1) / (h - 1);//高度缩放因子
    for(k = 0; k < im.c; ++k){
        for(r = 0; r < im.h; ++r){
            for(c = 0; c < w; ++c){//416
                float val = 0;
                if(c == w-1 || im.w == 1){//c =415 最后一列
                    val = get_pixel(im, im.w-1, r, k);//取原图片最后一列的像素
                } else {
                    float sx = c*w_scale;
                    int ix = (int) sx;
                    float dx = sx - ix;
                    val = (1 - dx) * get_pixel(im, ix, r, k) + dx * get_pixel(im, ix+1, r, k);
                }
                set_pixel(part, c, r, k, val);
            }
        }
    }
    for(k = 0; k < im.c; ++k){
        for(r = 0; r < h; ++r){
            float sy = r*h_scale;
            int iy = (int) sy;
            float dy = sy - iy;
            for(c = 0; c < w; ++c){
                float val = (1-dy) * get_pixel(part, c, iy, k);
                set_pixel(resized, c, r, k, val);
            }
            if(r == h-1 || im.h == 1) continue;
            for(c = 0; c < w; ++c){
                float val = dy * get_pixel(part, c, iy+1, k);
                add_pixel(resized, c, r, k, val);
            }
        }
    }

    free_image(part);
    return resized;
}

前向传播网络

network.c中的forward_network函数是整个神经网络的核心部分,各层的网络都在函数指针l.forward(l, state)中完成。

void forward_network(network net, network_state state)
{
    state.workspace = net.workspace;
    int i;
	   /// 遍历所有层,从第一层到最后一层,逐层进行前向传播(网络总共有net.n层)
    for(i = 0; i < net.n; ++i){		  
        state.index = i;/// 置网络当前活跃层为当前层,即第i层		  
        layer l = net.layers[i];/// 获取当前层		  
        if(l.delta && state.train){//不执行此分支的代码
			/// 如果当前层的l.delta已经动态分配了内存,则调用fill_cpu()函数,将其所有元素的值初始化为0			   
            scal_cpu(l.outputs * l.batch, 0, l.delta, 1);/// 第一个参数为l.delta的元素个数,第二个参数为初始化值,为0
			printf("forward_network scal_cpu of %d layer done!\n ", i);
        }
           //double time = get_time_point();
		l.forward(l, state);//进行卷积运算,激活函数,池化运算/
		   //if layer_type = convolutional ;   l.forward = forward_convolutional_layer;
		   //if layer_type = maxpool           l.forward = forward_maxpool_layer;
		   //if layer_type = yolo              l.forward = forward_yolo_layer;
		   //if layer_type = ROUTE             l.forward = forward_route_layer;其实就是数据的复制和搬移
		   //if layer_type = upsample          l.forward = forward_upsample_layer;;		  
           //printf("%d - Predicted in %lf milli-seconds.\n", i, ((double)get_time_point() - time) / 1000);
		   /// 完成某一层的推理时,置网络的输入为当前层的输出(这将成为下一层网络的输入),要注意的是,此处是直接更改指针变量net.input本身的值,
		   /// 也就是此处是通过改变指针net.input所指的地址来改变其中所存内容的值,并不是直接改变其所指的内容而指针所指的地址没变,
		   /// 所以在退出forward_network()函数后,其对net.input的改变都将失效,net.input将回到进入forward_network()之前时的值。	
		   ......

卷积层[convolution]

卷积层在convolutional_layer.c中的forward_convolutional_layer函数实现。

void forward_convolutional_layer(convolutional_layer l, network_state state)
{
    
	int out_h = convolutional_out_height(l);//获得本层卷积层输出特征图的高、宽
    int out_w = convolutional_out_width(l);
    int i, j;
	
	// l.outputs = l.out_h * l.out_w * l.out_c在make各网络层函数中赋值(比如make_convolutional_layer()),
	// 对应每张输入图片的所有输出特征图的总元素个数(每张输入图片会得到n也即l.out_c张特征图)
	// 初始化输出l.output全为0.0;输入l.outputs*l.batch为输出的总元素个数,其中l.outputs为batch
	// 中一个输入对应的输出的所有元素的个数,l.batch为一个batch输入包含的图片张数;0表示初始化所有输出为0;
    fill_cpu(l.outputs*l.batch, 0, l.output, 1);//将地址l.output,l.outputs*l.batch个float地址空间的数据初始化0
    .......

作者在进行卷积运算前,将输入特征图进行重新排序:


```c
void im2col_cpu(float* data_im,
     int channels,  int height,  int width,
     int ksize,  int stride, int pad, float* data_col)
{
    int c,h,w;
	// 计算该层神经网络的输出图像尺寸(其实没有必要再次计算的,因为在构建卷积层时,make_convolutional_layer()函数
	// 已经调用convolutional_out_width(),convolutional_out_height()函数求取了这两个参数,
	// 此处直接使用l.out_h,l.out_w即可,函数参数只要传入该层网络指针就可了,没必要弄这么多参数)
    int height_col = (height + 2*pad - ksize) / stride + 1;
    int width_col = (width + 2*pad - ksize) / stride + 1;
	
	/// 卷积核大小:ksize*ksize是一个卷积核的大小,之所以乘以通道数channels,是因为输入图像有多通道,每个卷积核在做卷积时,
	/// 是同时对同一位置处多通道的图像进行卷积运算,这里为了实现这一目的,将三通道上的卷积核并在一起以便进行计算,因此卷积核
	/// 实际上并不是二维的,而是三维的,比如对于3通道图像,卷积核尺寸为3*3,该卷积核将同时作用于三通道图像上,这样并起来就得
	/// 到含有27个元素的卷积核,且这27个元素都是独立的需要训练的参数。所以在计算训练参数个数时,一定要注意每一个卷积核的实际
	/// 训练参数需要乘以输入通道数。
    int channels_col = channels * ksize * ksize;//输入通道
	// 外循环次数为一个卷积核的尺寸数,循环次数即为最终得到的data_col的总行数
    for (c = 0; c < channels_col; ++c) {

		//行,列偏置都是对应着本次循环要操作的输出位置的像素而言的,通道偏置,是该位置像素所在的输出通道的绝对位置(通道数)

		// 列偏移,卷积核是一个二维矩阵,并按行存储在一维数组中,利用求余运算获取对应在卷积核中的列数,比如对于
		// 3*3的卷积核(3通道),当c=0时,显然在第一列,当c=5时,显然在第2列,当c=9时,在第二通道上的卷积核的第一列,
		// 当c=26时,在第三列(第三输入通道上)
        int w_offset = c % ksize;//0,1,2
		// 行偏移,卷积核是一个二维的矩阵,且是按行(卷积核所有行并成一行)存储在一维数组中的,
		// 比如对于3*3的卷积核,处理3通道的图像,那么一个卷积核具有27个元素,每9个元素对应一个通道上的卷积核(互为一样),
		// 每当c为3的倍数,就意味着卷积核换了一行,h_offset取值为0,1,2,对应3*3卷积核中的第1, 2, 3行
        int h_offset = (c / ksize) % ksize;//0,1,2
		// 通道偏移,channels_col是多通道的卷积核并在一起的,比如对于3通道,3*3卷积核,每过9个元素就要换一通道数,
		// 当c=0~8时,c_im=0;c=9~17时,c_im=1;c=18~26时,c_im=2,操作对象是排序后的像素位置
        int c_im = c / ksize / ksize;
		// 中循环次数等于该层输出图像行数height_col,说明data_col中的每一行存储了一张特征图,这张特征图又是按行存储在data_col中的某行中
        for (h = 0; h < height_col; ++h) {
			// 内循环等于该层输出图像列数width_col,说明最终得到的data_col总有channels_col行,height_col*width_col列
            for (w = 0; w < width_col; ++w) {
				// 由上面可知,对于3*3的卷积核,行偏置h_offset取值为0,1,2,当h_offset=0时,会提取出所有与卷积核第一行元素进行运算的像素,
				// 依次类推;加上h*stride是对卷积核进行行移位操作,比如卷积核从图像(0,0)位置开始做卷积,那么最先开始涉及(0,0)~(3,3)
				// 之间的像素值,若stride=2,那么卷积核进行一次行移位时,下一行的卷积操作是从元素(2,0)(2为图像行号,0为列号)开始
                int im_row = h_offset + h * stride;//yolov3-tiny stride = 1
				// 对于3*3的卷积核,w_offset取值也为0,1,2,当w_offset取1时,会提取出所有与卷积核中第2列元素进行运算的像素,
				// 实际在做卷积操作时,卷积核对图像逐行扫描做卷积,加上w*stride就是为了做列移位,
				// 比如前一次卷积其实像素元素为(0,0),若stride=2,那么下次卷积元素起始像素位置为(0,2)(0为行号,2为列号)
                int im_col = w_offset + w * stride;
				// col_index为重排后图像中的像素索引,等于c * height_col * width_col + h * width_col +w(还是按行存储,所有通道再并成一行),
				// 对应第c通道,h行,w列的元素
                int col_index = (c * height_col + h) * width_col + w;//将重排后的图片像素,按照左上->右下的顺序,计算一维索引

				//im_col + width*im_row +  width*height*channel 重排前的特征图在内存中的位置索引
				// im2col_get_pixel函数获取输入图像data_im中第c_im通道,im_row,im_col的像素值并赋值给重排后的图像,
				// height和width为输入图像data_im的真实高、宽,pad为四周补0的长度(注意im_row,im_col是补0之后的行列号,
				// 不是真实输入图像中的行列号,因此需要减去pad获取真实的行列号)
                data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,
                        im_row, im_col, c_im, pad);
				// return data_im[im_col + width*im_row +  width*height*channel)];
            }
        }
    }
}

通过gemm进行卷积乘加操作,通过add_bias添加偏置。

//进行卷积的乘加运算,没有bias偏置参数参与运算;
gemm(0, 0, m, n, k, 1, a, k, b, n, 1, c, n);

add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w);//每个输出特征图的元素都加上对应通道的偏置参数

池化层[maxpool]

maxpool_layer.c中的forward_maxpool_layer函数完成池化操作。yolov3-tiny保留了池化层,并使用最大值池化,将尺寸为2×2的核中最大值保留下来。

void forward_maxpool_layer_avx(float *src, float *dst, int *indexes, int size, int w, int h, int out_w, int out_h, int c,
    int pad, int stride, int batch)
{

    const int w_offset = -pad / 2;
    const int h_offset = -pad / 2;
    int b, k;

    for (b = 0; b < batch; ++b) {
		// 对于每张输入图片,将得到通道数一样的输出图,以输出图为基准,按输出图通道,行,列依次遍历
		// (这对应图像在l.output的存储方式,每张图片按行铺排成一大行,然后图片与图片之间再并成一行)。
		// 以输出图为基准进行遍历,最终循环的总次数刚好覆盖池化核在输入图片不同位置进行池化操作。
        #pragma omp parallel for
        for (k = 0; k < c; ++k) {
            int i, j, m, n;
            for (i = 0; i < out_h; ++i) {
                //for (j = 0; j < out_w; ++j) {
                j = 0;
                for (; j < out_w; ++j) {
					// out_index为输出图中的索引
                    int out_index = j + out_w*(i + out_h*(k + c*b));//j + out_w * i + out_w * iout_h * k
                    float max = -FLT_MAX;// FLT_MAX为c语言中float.h定义的对大浮点数,此处初始化最大元素值为最小浮点数
                    int max_i = -1;// 最大元素值的索引初始化为-1
                    // 下面两个循环回到了输入图片,计算得到的cur_h以及cur_w都是在当前层所有输入元素的索引,内外循环的目的是找寻输入图像中,
                    // 以(h_offset + i*l.stride, w_offset + j*l.stride)为左上起点,尺寸为l.size池化区域中的最大元素值max及其在所有输入元素中的索引max_i
                    for (n = 0; n < size; ++n) {//2
                        for (m = 0; m < size; ++m) {//2
                            // cur_h,cur_w是在所有输入图像中第k通道中的cur_h行与cur_w列,index是在所有输入图像元素中的总索引。
                            // 为什么这里少一层对输入通道数的遍历循环呢?因为对于最大池化层来说输入与输出通道数是一样的,并在上面的通道数循环了!
                            int cur_h = h_offset + i*stride + n;
                            int cur_w = w_offset + j*stride + m;
                            int index = cur_w + w*(cur_h + h*(k + b*c));
							// 边界检查:正常情况下,是不会越界的,但是如果有补0操作,就会越界了,这里的处理方式是直接让这些元素值为-FLT_MAX
							// (注意虽然称之为补0操作,但实际不是补0),总之,这些补的元素永远不会充当最大元素值。
                            int valid = (cur_h >= 0 && cur_h < h &&
                                cur_w >= 0 && cur_w < w);
                            float val = (valid != 0) ? src[index] : -FLT_MAX;
							// 记录这个池化区域中的最大的元素值及其在所有输入元素中的总索引
                            max_i = (val > max) ? index : max_i;
                            max = (val > max) ? val : max;
                        }
                    }
					// 由此得到最大池化层每一个输出元素值及其在所有输入元素中的总索引。
					// 为什么需要记录每个输出元素值对应在输入元素中的总索引呢?因为在下面的反向过程中需要用到,在计算当前最大池化层上一层网络的敏感度时,
					// 需要该索引明确当前层的每个元素究竟是取上一层输出(也即上前层输入)的哪一个元素的值,具体见下面backward_maxpool_layer()函数的注释。
                    dst[out_index] = max;
                    if (indexes) indexes[out_index] = max_i;
                }
            }
        }
    }
}

路由层[route]

yolov3-tiny中共有两层路由层。第17层路由层(从0层开始),其实直接将第13层网络的输出结果输入。第20层路由层,将第19层和第8层网络结果合并在一起,19层在前,8层在后。在route_layer.c中的forward_route_layer函数中实现。

void forward_route_layer(const route_layer l, network_state state)
{
    int i, j;
    int offset = 0;
    for(i = 0; i < l.n; ++i){//l.n:  卷积层:输出特征图通道数 路由层:有几层网络层输入本层  17层:1(路由第13层)   20:2(路由第19、8层)
        int index = l.input_layers[i];//输入本网络层的网络层的索引:如13,19,8
        float *input = state.net.layers[index].output;//输入等于 之前网络层索引值得输出(.output)
        int input_size = l.input_sizes[i];//输入的网络层的数据量
        int part_input_size = input_size / l.groups;//未分组
        for(j = 0; j < l.batch; ++j){
            //copy_cpu(input_size, input + j*input_size, 1, l.output + offset + j*l.outputs, 1);
			//从首地址input处复制input_size 个数据到 l.output中
            copy_cpu(part_input_size, input + j*input_size + part_input_size*l.group_id, 1, l.output + offset + j*l.outputs, 1);//l.group_id = 0
			//其实就是copy_cpu(part_input_size, input, 1, l.output + offset, 1);
        }
        //offset += input_size;
        offset += part_input_size;
    }
}

上采样层[upsample]

yolov3-tiny中第19层是上采样层,将18层13x13x128的输入特征图转变为26x26x128的输出特征图。在upsample_layer.c中的forward_upsample_layer函数中完成。

void upsample_cpu(float *in, int w, int h, int c, int batch, int stride, int forward, float scale, float *out)
{
	
    int i, j, k, b;
    for (b = 0; b < batch; ++b) {
        for (k = 0; k < c; ++k) {
            for (j = 0; j < h*stride; ++j) {
                for (i = 0; i < w*stride; ++i) {
                    int in_index = b*w*h*c + k*w*h + (j / stride)*w + i / stride;
                    int out_index = b*w*h*c*stride*stride + k*w*h*stride*stride + j*w*stride + i;
                    if (forward) out[out_index] = scale*in[in_index];
                    else in[in_index] += scale*out[out_index];
                }
            }
        }
    }
}

上采样效果:
在这里插入图片描述

输出层[yolo]

yolo层完成了对13x13x255和26x26x255输入特诊图的logistic逻辑回归计算。每个box的预测宽度和高度不参与逻辑回归,在yolo_layer.c中的forward_yolo_layer函数中完成。

//两个yolo层 只对数据进行了logistic处理,并没有预测box的位置
//将0-1通道(x,y) 4-84(confidence+class)计算logistic,三个prior(预测框都是这样)
void forward_yolo_layer(const layer l, network_state state)
{
    int i, j, b, t, n;
	//从state.input复制数据到l.output
    memcpy(l.output, state.input, l.outputs*l.batch * sizeof(float));

#ifndef GPU
	printf("yolo v3 tiny l.n and l.batch of yolo layer is %d and %d  \n ",l.n,l.batch);
    for (b = 0; b < l.batch; ++b) {//l.batch = 1
        for (n = 0; n < l.n; ++n) {//l.n:3(yolo层)mask 0,1,2  表示每个网络单元预测三个box?
			
			//printf("l.coords is %d in yolov3 tiny yolo layer ,l.scale_x_y is %f \n", l.coords, l.scale_x_y);
            // l.coords 坐标:0  l.classes分类数量:80   l.scale_x_y:1
			//l.w:输入特征图宽度 l.h输出特征图高度  
			int index = entry_index(l, b, n*l.w*l.h, 0);//index = n*l.w*l.h*(4 + l.classes + 1)
			
		   //起始地址为:l.output + index 个数为:2 * l.w*l.h  计算逻辑回归值,并保存
            activate_array(l.output + index, 2 * l.w*l.h, LOGISTIC);  // x,y,

			//起始地址为:l.output + index 个数为:2 * l.w*l.h  计算方式为:x = x*l.scale_x_y + -0.5*(l.scale_x_y - 1) 简化后:x = x
			//yolov3-tiny l.scale_x_y = 1  实际上该函数没有参与任何的运算   scal_add_cpu
            scal_add_cpu(2 * l.w*l.h, l.scale_x_y, -0.5*(l.scale_x_y - 1), l.output + index, 1);    // scale x,y
            
			//
			index = entry_index(l, b, n*l.w*l.h, 4);//index = n*l.w*l.h*(4 + l.classes + 1)+ 4*l.w*l.h
            
			//起始地址为:l.output + index,个数为:(1+80)*l.w*l.h   计算器其逻辑回归值
			activate_array(l.output + index, (1 + l.classes)*l.w*l.h, LOGISTIC);
        }
    }

预测结果统计[detection ]


//w:输入图像宽度640,不一定是416 h:输入图像高度424,不一定是416  thresh:图像置信度阈值0.25   hier:0.5
//map:0   relative:1  num:0   letter:0
//函数作用:统计两个yolo层中 置信度大于阈值的box个数,并对这个box初始化一段地址空间 dets
//根据网络来填充该地址空间dets:
//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度,并将结果保存在dets[count].bbox结构体中
//每个box有80个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
///舍弃prob小于阈值0.25的box
//将满足阈值的box个数保存到num中
detection *get_network_boxes(network *net, int w, int h, float thresh, float hier, int *map, int relative, int *num, int letter)
{
	//printf("w、h、thresh、hier and letter is %d 、%d 、%f 、%f and %d\n", w, h, thresh, hier, letter);

	//函数作用:统计两个yolo层中 置信度大于阈值的box个数,并对这个box初始化一段地址空间 dets
	//将满足阈值的box个数保存到num中
    detection *dets = make_network_boxes(net, thresh, num);
	
	//根据网络来填充该地址空间dets:
	//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度,并将结果保存在dets[count].bbox结构体中
	//每个box有80个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
	///舍弃prob小于阈值0.25的box
    fill_network_boxes(net, w, h, thresh, hier, map, relative, dets, letter);
    return dets;
}

使用make_network_boxes来创建预测信息的指针变量:

// thresh:  置信度阈值
//num: 0
//函数作用:统计置信度大于阈值的box个数,并对这个box初始化一段地址空间
detection *make_network_boxes(network *net, float thresh, int *num)
{
    layer l = net->layers[net->n - 1];//应该是神经网络最后一层 net->n:24 最后一层yolo层
	//printf(" net->n  of network is %d\n " ,(net->n));
    int i;
	// -thresh 0.25
	//yolo层:yolov3-tiny中共有两层
	//三个prior预测框,对每个预测框中,置信度大于thresh 0.25,记为一次,将次数进行累加,并输出
	//nboxes:即为要保留的box的个数 两个yolo层中的置信度个数一起累加
    int nboxes = num_detections(net, thresh);//-thresh 0.25

	if (num) {
		printf("nbox = %d \n", num);
		*num = nboxes;//不执行该语句
	}
    //申请内存,个数为nboxes,每个内存大小为:sizeof(detection)
    detection* dets = (detection*)xcalloc(nboxes, sizeof(detection));

	//遍历每个box,每个dets.prob申请80个float类型的内存:
	//dets.uc,申请4个float类型的空间:位置信息
    for (i = 0; i < nboxes; ++i) {
        dets[i].prob = (float*)xcalloc(l.classes, sizeof(float));
        // tx,ty,tw,th uncertainty
        dets[i].uc = (float*)xcalloc(4, sizeof(float)); // Gaussian_YOLOv3
        
		if (l.coords > 4) {//不执行这个分支 l.coords:0
            dets[i].mask = (float*)xcalloc(l.coords - 4, sizeof(float));
        }
    }
    return dets;
}

使用get_yolo_detections来统计两层yolo层的预测信息:

//w,h:640,424    netw, neth:416,416 thresh:图像置信度阈值0.25   hier:0.5
//map:0   relative:1    letter:0
//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度,并将结果保存在dets[count].bbox结构体中
//每个box有80个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
///舍弃prob小于阈值0.25的box
int get_yolo_detections(layer l, int w, int h, int netw, int neth, float thresh, int *map, int relative, detection *dets, int letter)
{
    printf("\n l.batch = %d, l.w = %d, l.h = %d, l.n = %d ,netw = %d, neth = %d \n", l.batch, l.w, l.h, l.n, netw, neth);
    int i,j,n;
    float *predictions = l.output;//yolo层的输出
    // This snippet below is not necessary
    // Need to comment it in order to batch processing >= 2 images
    //if (l.batch == 2) avg_flipped_yolo(l);
    int count = 0;

	//printf("yolo layer l.mask[0] is %d, l.mask[1] is %d, l.mask[2] is %d\n", l.mask[0], l.mask[1], l.mask[2]);
	//printf("yolo layer l.biases[l.mask[0]*2] is %f, l.biases[l.mask[1]*2] is %f, l.biases[l.mask[2]*2] is %f\n", l.biases[l.mask[0] * 2], l.biases[l.mask[1] * 2], l.biases[l.mask[2] * 2]);
	//遍历yolo层
    for (i = 0; i < l.w*l.h; ++i){//该yolo层输出特征图的宽度、高度:13x13 26x26
        int row = i / l.w;
        int col = i % l.w;
        for(n = 0; n < l.n; ++n){//yolo层,l.n = 3
			
            //obj_index:置信度层索引
            int obj_index  = entry_index(l, 0, n*l.w*l.h + i, 4);//obj_index  = n*l.w*l.h*(4+l.classes+1) + 4*l.w*l.h + i;
            float objectness = predictions[obj_index];//获得对应的置信度
            //if(objectness <= thresh) continue;    // incorrect behavior for Nan values
            
			if (objectness > thresh) {//只有置信度大于阈值才开始执行该分支
                //printf("\n objectness = %f, thresh = %f, i = %d, n = %d \n", objectness, thresh, i, n);
                
				//box_index:yolo层每个像素点有三个box,表示每个box的索引值
				int box_index = entry_index(l, 0, n*l.w*l.h + i, 0);//box_index = n*l.w*l.h*(4+l.classes+1)+ i;

				//l.biases->偏置参数起始地址    l.mask[n]:分别为3,4,5,0,1,2,biases偏置参数偏移量
				//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度,并将结果保存在dets[count].bbox结构体中
                dets[count].bbox = get_yolo_box(predictions, l.biases, l.mask[n], box_index, col, row, l.w, l.h, netw, neth, l.w*l.h);

				//获取对应的置信度,该置信度经过了logistic
                dets[count].objectness = objectness;

				//获得分类数:80(int类型)
                dets[count].classes = l.classes;
                for (j = 0; j < l.classes; ++j) {
					//80个类别,每个类别对应的概率,class_index为其所在层的索引
                    int class_index = entry_index(l, 0, n*l.w*l.h + i, 4 + 1 + j);//class_index  = n*l.w*l.h*(4+l.classes+1) + (4+1+j)*l.w*l.h + i;
                    //每个box有80个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
					float prob = objectness*predictions[class_index];
					
					//舍弃prob小于阈值0.25的box
                    dets[count].prob[j] = (prob > thresh) ? prob : 0;
                }
                ++count;
            }
        }
    }
    correct_yolo_boxes(dets, count, w, h, netw, neth, relative, letter);
    return count;
}

非极大值抑制[NMS]

//dets:box结构体 nboxes:满足阈值的box个数   l.classe:80    thresh=0.45f
//两个box,同一类别进行非极大值抑制,遍历
void do_nms_sort(detection *dets, int total, int classes, float thresh)
{
    int i, j, k;
    k = total - 1;
    for (i = 0; i <= k; ++i) {//box个数
        if (dets[i].objectness == 0) {//置信度==0  不执行该分支,理论上没有objectness = 0
			printf("there is no objectness == 0 !!! \n");
            detection swap = dets[i];
            dets[i] = dets[k];
            dets[k] = swap;
            --k;
            --i;
        }
    }
    total = k + 1;
	//同一类别进行比较
    for (k = 0; k < classes; ++k) {//80个        
        //box预测的类别
		for (i = 0; i < total; ++i) {//box个数
            dets[i].sort_class = k;
        }
		//函数作用:将prob较大的box排列到前面
        qsort(dets, total, sizeof(detection), nms_comparator_v3);
        for (i = 0; i < total; ++i) {//两个box,同一类别进行非极大值抑制
            //printf("  k = %d, \t i = %d \n", k, i);
            if (dets[i].prob[k] == 0) continue;
            box a = dets[i].bbox;
            for (j = i + 1; j < total;++j){
				box b = dets[j].bbox;
				if( box_iou(a, b) > thresh) dets[j].prob[k] = 0;
            }
        }
    }
}

AlphaFold2论文

https://www.nature.com/articles/s41586-021-03819-2

https://github.com/deepmind/alphafold

沐神AlphaFold讲解

论文补充材料:https://www.biorxiv.org/content/10.1101/2021.10.04.463034v1

摘要

蛋白质对生命至关重要,了解它们的结构可以促进对其功能的系统理解。通过大量的实验,已经确定了大约 100,000 种独特的蛋白质结构,但这仅代表了数十亿已知蛋白质序列中的一小部分。

蛋白质结构是指蛋白质分子的空间结构。作为一类重要的生物大分子,蛋白质主要由化学元素组成。所有蛋白质都是由20种不同的L型α氨基酸连接形成的多聚体,在形成蛋白质后,这些氨基酸又被称为残基。
蛋白质一级结构:组成蛋白质多肽链的线性氨基酸序列。
蛋白质二级结构:依靠不同氨基酸之间的C=O和N-H基团间的氢键形成的稳定结构,主要为α螺旋和β折叠。
蛋白质三级结构:通过多个二级结构元素在三维空间的排列所形成的一个蛋白质分子的三维结构。
蛋白质四级结构:用于描述由不同多肽链(亚基)间相互作用形成具有功能的蛋白质复合物分子。

仅根据其氨基酸序列预测蛋白质的三级结构,这是“蛋白质折叠问题”, 50 多年来一直是一个重要的开放性研究问题。尽管最近取得了进展,但现有方法仍远未达到原子级准确度,尤其是当没有可用的同源结构时。

同源结构,那些不同物种因来自共同祖先而具有的相似性结构。 例如现代马经较长时间的修饰成为具有一个趾,鼹鼠及其他洞穴动物成为瘤状肢体,大象的肢体成为柱状,这样功能各异的前肢有一个共同来源,他们都来自原始陆生脊椎动物五趾型的肢体。

在这里,我们提供了第一种计算方法,即使在不知道相似结构的情况下,它也可以以原子精度定期预测蛋白质结构。我们在具有挑战性的第 14 次蛋白质结构预测关键评估 (CASP14) 中验证了我们基于神经网络模型的完全重新设计的AlphaFold,在大多数情况下表现出与实验相媲美的准确性,并且大大优于其他方法。支持最新版本的 AlphaFold 是一种新颖的机器学习方法,它将关于蛋白质结构的物理和生物学知识,利用多序列比对,融入深度学习算法的设计中。

序列比对指将两个或多个序列排列在一起,标明其相似之处。序列中可以插入间隔(通常用短横线“-”表示)。对应的相同或相似的符号(在核酸中是A, T(或U), C, G,在蛋白质中是氨基酸残基的单字母表示)排列在同一列上。
tcctctgcctctgccatcat—caaccccaaagt
|||| ||| ||||| ||||| ||||||||||||
tcctgtgcatctgcaatcatgggcaaccccaaagt
多序列比对是成对比对的延伸,是为了在一次比对里面处理多于两条的的序列。多序列比对方法试图比对一个指定序列集合里面的所有序列,这可以帮助确定这些序列的共同区段。进行多序列比对有几种方法,最常用的一种是Clustal程序集,它使用渐进多序列比对算法。Clustal在cladistics中被用来建立进化树,在PSI-BLAST和Hidden Markov model (HMM)中用来建立序列档案以在序列数据库中搜索更远的同源序列。

从蛋白质序列预测蛋白质3D结构的计算方法的发展沿着两条互补的路径前进,分别关注物理相互作用或进化历史。 物理相互作用方案将我们认知的分子驱动力(molecular driving forces)整合到物理热力学或动力学模拟或统计模型逼近中。 虽然理论上非常吸引人,但由于分子模拟的计算难度、蛋白质稳定性的上下文依赖性以及难以产生足够准确的蛋白质物理学模型,这种方法已被证明对即使是中等大小的蛋白质也极具挑战性。 近年来,进化方案提供了一种替代方案,其中蛋白质结构约束来自蛋白质进化历史的生物信息学分析、与已解决结构的同源性和成对进化相关性。

这种生物信息学方法极大地受益于蛋白质数据库 (PDB) 中存储的实验蛋白质结构的稳定增长、基因组测序的爆炸式增长以及用于解释这些相关性的深度学习技术的快速发展。 尽管取得了这些进展,基于物理和进化历史的方法产生的预测远低于实验准确性。

我们(DeepMind团队)开发了第一个能够在大多数情况下预测蛋白质结构接近实验准确性的计算方法。 我们开发的神经网络 AlphaFold 已进入 CASP14 评估(2020 年 5 月至 7 月)。 CASP 评估每两年进行一次,使用未在 PDB 中存放或公开披露的最近解决的结构,因此它是对参与方法的盲测,长期以来一直作为结构预测准确性的金标准评估。

蛋白质结构预测 (CASP) 实验的批判性评估旨在建立蛋白质结构预测的当前技术水平,确定已取得的进展,并突出未来可能最有成效的工作重点。

… 省略了部分内容

在图 2a 中证明,CASP14 中展示AlphaFold的高准确率扩展到最近大量PDB结构样本,其中所有结构在我们的训练数据截止后都存储在PDB中,并作为完整链进行分析。此外,当主链预测准确时,观察到高侧链准确性(图 2b),并且表明我们预测的局部距离差异测试(pLDDT)置信度可靠地预测了 Ca 局部距离差异测试(lDDT-Cα)准确度相应的预测(图 2c)。我们还发现可以准确估计全局叠加度量模板建模分数 (TM-score)(图 2d)。总体而言,这些验证了 AlphaFold 在 CASP14 蛋白上的高精度和可靠性也可以迁移到最近 PDB 提交的未经整理的数据集中。

Fig. 1 | AlphaFold produces highly accurate structures.

PDB蛋白质结构数据库(Protein Data Bank,简称PDB)是美国Brookhaven国家实验室于1971年创建的,由结构生物信息学研究合作组织(Research Collaboratory for Structural Bioinformatics,简称RCSB)维护。和核酸序列数据库一样,可以通过网络直接向PDB数据库提交数据。

AlphaFold网络

AlphaFold 通过结合基于蛋白质结构的进化、物理和几何约束的新型神经网络架构和训练程序,大大提高了结构预测的准确性。特别是,我们展示了一种联合嵌入多序列比对 (MSA) 和成对特征的新架构、一种新的输出表示和相关损失函数,可实现准确的端到端结构预测、新的等变注意架构、使用中间损失函数来实现预测的迭代改进,屏蔽 MSA 损失与结构联合训练,使用自蒸馏从未标记的蛋白质序列中学习,以及自我估计准确率。

蒸馏,就是知识蒸馏,将教师网络(teacher network)的知识迁移到学生网络(student network)上,使得学生网络的性能表现如教师网络一般;或者大型模型迁移到小型模型中,小型模型的参数规模小,运行速度快,但性能与大型模型参不多。

AlphaFold 网络使用一级氨基酸序列和同源物的比对序列作为输入,直接预测给定蛋白质的所有重原子的 3-D 坐标(图 1e,请参阅方法了解输入的详细信息,包括数据库、MSA 构建和使用的模板)。 最重要的方法和组件的描述如下在附件中提供了完整的网络架构和训练过程在附件的方法章节中。

该网络包括两个主要阶段。 首先,网络的主干通过我们称为 Evoformer 的新型神经网络块的重复层处理输入,以生成 Nseq × Nres 数组(Nseq:序列数,Nres:残基数),表示已处理的 MSA 和 Nres × Nres 数组,表示残基对。 MSA 表示是用原始 MSA 初始化的,但请参阅 附件-方法 1.2.7 了解处理非常深的 MSA 的详细信息。 Evoformer 块包含许多新颖的基于注意力和非基于注意力的组件。 我们在“可解释性”部分展示了证据,表明在 Evoformer 块中早期出现了具体的结构假设并不断完善。 Evoformer 模块的关键创新是在 MSA 内交换信息的新机制和允许直接推理空间和进化关系的配对表示。

网络的主干之后是结构模块,该模块以蛋白质的每个残基(全局刚体框架)的旋转和平移的形式引入了明确的 3-D 结构。这些表示在简单的状态下初始化,所有旋转设置为一致,所有位置设置为原点,但快速发展和完善,具有精确原子细节的高度准确的蛋白质结构。网络这一部分的关键创新包括打破链原子结构以允许同时对结构的所有部分进行局部细化,一种新颖的等变变换器允许网络隐式推理未表示的侧链原子,以及一个损失项代替残基的方向正确性的重要权重。在结构模块和整个网络中,我们通过反复将最终损失函数应用于输出,然后将输出递归地提供给相同的模块来强化迭代细化的概念。使用整个网络的迭代细化(我们称之为“循环”) 对准确性有显着贡献,而额外的训练时间很少(有关详细信息,请参阅附件-方法-1.8)。

Evoformer模块

名为 Evoformer(图 1e 和 3a)的网络构建块的关键原理是将蛋白质结构预测视为 3-D 空间中的图推理问题,其中图的边缘由邻近的残基定义。 配对表示的元素编码有关残基之间关系的信息(图 3b)。 MSA 表示的列编码输入序列的各个残基,而行表示这些残基出现的序列。 在这个框架内,我们定义了许多更新操作,这些更新操作应用于每个块中,其中不同的更新操作被串联应用。

Fig. 3 | Architectural details.

MSA 表示通过在 MSA 序列维度上求和的逐元素外积更新配对表示。 与之前的工作不同,此操作应用于每个块中,而不是在网络中应用一次,这使得从不断发展的 MSA 表示到配对表示的连续通信成为可能。

在配对表示中,有两种不同的更新模式。两者都受到配对表示一致性必要性的启发——为了将氨基酸的配对描述表示为单个 3-D 结构,必须满足许多约束,包括距离上的三角不等式。基于这种直觉,我们根据涉及三个不同节点的边三角形来安排对表示的更新操作(图 3c)。特别是,我们向轴向注意力添加了一个额外的 logit 偏差,以包括三角形的“缺失边”,并且我们定义了一个非注意力更新操作“三角形乘法更新”,它使用两条边来更新缺失的第三条边(参见 附件-方法-1.6.5 了解详情)。三角形乘法更新最初是作为注意力更对称且更便宜的替代品而开发的,仅使用注意力或乘法更新的网络都能够产生高精度结构。然而,两个更新的组合更准确。

我们还在 MSA 表示中使用了一种轴向注意力的变体。 在 MSA 中的 per-sequence attention 期间,我们从 pair stack 中投射额外的 logits 以偏置 MSA attention。 这通过提供从配对表示返回到 MSA 表示的信息流来关闭循环,确保整个 Evoformer 模块能够完全混合对和 MSA 表示之间的信息,并为结构模块中的结构生成做好准备。

端到端结构预测

结构模块(图 3d)使用对表示和来自主干的 MSA 表示的原始序列行(“single representation”,“单一表示”)在具体的 3-D 主干结构上运行。 3-D 主干结构表示为 Nres 独立的旋转和平移,每个旋转和平移相对于全局框架(residue gas,“残余气”?,图 3e)。这些旋转和平移,代表 N-Cα-C 原子的几何形状,优先考虑蛋白质骨架的方向,以便每个残基的侧链位置在该框架内受到高度限制。相反,肽键几何形状完全不受约束,并且在应用结构模块期间观察到网络经常违反链约束,因为打破此约束允许对链的所有部分进行局部细化,而无需解决复杂的闭环问题。在微调期间通过违规损失项鼓励满足肽键几何形状。只有在 Amber力场中的梯度下降结构的预测后松弛中才能实现肽键几何形状的精确执行。根据经验,这种最终松弛不会提高模型的准确性,如通过全局距离测试 (GDT) 或 IDDT-Cα34 测量的,但确实消除了分散注意力的立体化学违规而不会损失准确性。

AMBER力场是在生物大分子的模拟计算领域有着广泛应用的一个分子力场。AMBER力场的优势在于对生物大分子的计算,其对小分子体系的计算结果常常不能令人满意。

residue gas表示分两个阶段迭代更新(图 3d)。首先,我们称为不变点注意力 ( Point Attention) 的新型几何感知注意操作用于更新 Nres 神经激活集(single representation,“单一表示”)而不改变 3-D 位置,然后对residue gas使用更新的激活。不变点注意力通过在每个残基的局部框架中产生的 3-D 点来增强每个通常的注意力查询、键和值,这样最终值对全局旋转和平移是不变的(参见方法“不变点注意力(IPA)”了解详情)。 3-D 查询和键也对注意力施加了强烈的空间/局部性偏差,这非常适合蛋白质结构的迭代细化。在每个注意力操作和逐元素转换块之后,该模块计算每个主干帧的旋转和平移的更新。这些更新在每个残差的局部框架内的应用使得整体注意力和更新块成为对residue gas的等变操作。

侧链 chi 角的预测以及结构的最终每个残基精度 (pLDDT) 是在网络末端的最终激活上使用小的每个残基网络计算的。 TM 分数 (pTM) 的估计值是从成对错误预测中获得的,该预测被计算为最终对表示的线性投影。最后的损失(我们称之为帧对齐点误差(FAPE)(图 3f))将预测的原子位置与许多不同对齐下的真实位置进行比较。对于每个对齐,通过将预测帧 (Rk,tk) 对齐到相应的真实帧来定义,我们计算所有预测原子位置 xi 与真实原子位置的距离。由此产生的 Nframes × Natoms 距离受到限制的 L1 损失的惩罚。这对原子相对于每个残基的局部框架是正确的产生了强烈的偏见,因此在其侧链相互作用方面是正确的,并为 AlphaFold 提供了手性的主要来源(增刊-方法 1.9.3 和增刊-图 9)

使用标记和未标记数据进行训练

AlphaFold 架构能够仅使用对 PDB 数据的监督学习来训练到高精度,但我们能够使用类似于noisy-student自我蒸馏的方法来提高准确性(见图 4a)。 在这个过程中,我们使用一个训练有素的网络来预测来自 Uniclust30的约 350,000 个不同序列的结构,并将预测结构的新数据集过滤为高置信度子集。 然后,我们使用 PDB 和这个新的预测结构数据集的混合作为训练数据从头开始训练相同的架构,其中各种训练数据增强(例如裁剪和 MSA 子采样)使网络难以重现先前预测的结构。 这种自蒸馏过程有效地利用了未标记的序列数据,并显着提高了所得网络的准确性。

Self-training是最简单的半监督方法之一,其主要思想是找到一种方法,用未标记的数据集来扩充已标记的数据集。算法流程如下:
(1)首先,利用已标记的数据来训练一个好的模型,然后使用这个模型对未标记的数据进行标记。
(2)然后,进行伪标签的生成,因为我们知道,已训练好的模型对未标记数据的所有预测都不可能都是好的,因此对于经典的Self-training,通常是使用分数阈值过滤部分预测,以选择出未标记数据的预测标签的一个子集。
(3)其次,将生成的伪标签与原始的标记数据相结合,并在合并后数据上进行联合训练。
(4)整个过程可以重复n次,直到达到收敛。

此外,我们随机屏蔽或突变 MSA 中的单个残基,并具有来自 Transformers (BERT) 式目标的双向编码器表示来预测 MSA 序列的屏蔽元素。 这个目标鼓励网络学习解释系统发育和协变关系,而无需将特定的相关统计量硬编码到特征中。 与最近的独立工作相比,BERT 目标是在相同的训练示例上与正常的 PDB 结构损失联合训练的,并且没有进行预训练。

解释神经网络

为了了解 AlphaFold 如何预测蛋白质结构,我们为网络中的 48 个 Evoformer 块中的每一个训练了一个单独的结构模块,同时保持主网络的所有参数保持不变(补充方法 1.14)。包括我们的回收阶段,这提供了 192 个中间结构的轨迹,每个完整的 Evoformer 块一个,其中每个中间体代表网络对该块最可能结构的信念。在前几个块之后产生的轨迹出奇地平滑,表明 AlphaFold 对结构进行了不断的增量改进,直到它不能再改进为止(见图 4b 的准确度轨迹)。这些轨迹也说明了网络深度的作用。对于非常具有挑战性的蛋白质,如 SARS-CoV-2 Orf8 (T1064),网络搜索并重新排列多层的二级结构元素,然后再确定一个好的结构。对于 LmrP (T1024) 等其他蛋白质,网络会在前几层内找到最终结构。请参阅增刊-视频 1-4 的 CASP14 目标 T1024、T1044、T1064 和 T1091 的结构轨迹显示了一系列蛋白质大小和难度的清晰迭代构建过程。在补充-方法 1.16 和增刊-图12-13,我们解释了 AlphaFold 层产生的注意力图。

Fig. 4 | Interpreting the neural network

图 4a 包含 AlphaFold 组件的详细消融,表明各种不同的机制有助于 AlphaFold 的准确性。 请参阅增刊-方法 1.13 详细描述了每个消融模型、它们的训练细节、消融结果的扩展讨论以及 MSA 深度对每次消融的影响(补充-图 10)。

MSA 深度和跨链接触

虽然 AlphaFold 在绝大多数沉积的 PDB 结构中具有很高的准确性,但我们注意到仍然存在影响准确性或限制模型适用性的因素。当平均比对深度小于约 30 个序列时,该模型使用多个序列比对,准确度大幅下降(详见图 5a)。我们观察到阈值效应,其中 MSA 深度超过约 100 个序列的改进导致小增益。我们假设需要 MSA 信息在网络的早期阶段粗略地找到正确的结构,但是将该预测细化为高精度模型并不关键取决于 MSA 信息。我们观察到的另一个实质性限制是,与异型接触的数量相比,AlphaFold 对于链内或同型接触很少的蛋白质要弱得多。这通常发生在较大复合物中的桥接结构域,其中蛋白质的形状几乎完全由与复合物中其他链的相互作用产生。相反,AlphaFold 通常能够为同聚体提供高精度预测,即使链基本上交织在一起(例如图 5b)。我们希望 AlphaFold 的想法很容易适用于预测未来系统中的完整异质复合物,并且这将消除具有大量异质接触的蛋白质链的困难。

Fig. 5 | Effect of MSA depth and cross-chain contacts.

相关工作

蛋白质结构预测有一个漫长而多样的发展,在许多优秀的评论中得到了广泛的介绍。尽管将神经网络应用于结构预测的历史悠久,但它们最近才开始改进结构预测。这些方法通过将蛋白质结构预测问题处理为将进化耦合的“图像” 转换为蛋白质距离矩阵的“图像”,然后将距离预测集成到启发式系统中,从而有效地利用了计算机视觉系统的快速改进。产生最终的 3-D 坐标预测。最近开发了一些工作来直接预测 3-D 坐标 ,但这些方法的准确性与传统的手工结构预测管道不匹配 。同时,基于注意力的语言处理网络的成功 和最近的计算机视觉激发了对解释蛋白质序列的基于注意力的方法的探索。

讨论

我们在设计 AlphaFold 时采用的方法是生物信息学和物理方法的结合:我们使用物理和几何归纳偏差来构建组件,这些组件可以从 PDB 数据中学习,并最大限度地减少手工制作的特征(例如,AlphaFold 在没有氢的情况下有效地构建氢键债券评分函数)。这使得网络从 PDB 中的有限数据中更有效地学习,但能够应对结构数据的复杂性和多样性。特别是,AlphaFold 能够处理缺失的物理环境,并在具有挑战性的情况下生成准确的模型,例如交织的同源异构体或仅在未知血红素组存在时才会折叠的蛋白质。处理未指定结构条件的能力对于从 PDB 结构中学习至关重要,因为 PDB 代表了结构已被求解的所有条件。一般来说,AlphaFold 被训练产生最有可能作为 PDB 结构的一部分出现的蛋白质结构。在特定化学计量或配体/离子可单独从序列预测的情况下,AlphaFold 可能会产生一种隐式遵守这些约束的结构。

AlphaFold 已经向实验界展示了它的实用性,包括分子置换和解释低温电子显微镜 (cryo-EM) 图 。 此外,由于 AlphaFold 直接输出蛋白质坐标,因此 AlphaFold 会根据蛋白质序列的长度以图形处理单元 (GPU) 分钟到 GPU 小时生成预测(例如,对于 384 个残基,每个模型大约 1 个 GPU 分钟,请参阅方法了解详细信息 )。 这开辟了在蛋白质组规模及其他范围内预测结构的令人兴奋的可能性。

可用基因组测序技术和数据的爆炸式增长彻底改变了生物信息学,但实验结构测定的内在挑战阻止了我们结构知识的类似扩展。 通过开发准确的蛋白质结构预测算法,再加上由实验社区组装的现有大型且精心准备的结构和序列数据库,我们希望加速结构生物信息学的进步,以跟上基因组学革命的步伐。 我们希望 AlphaFold 以及将其技术应用于其他生物物理问题的计算方法将成为现代生物学的重要工具。

在线内容

任何方法、附加参考资料、自然研究报告摘要、源数据、扩展数据、补充信息、致谢、同行评审信息; 作者贡献和竞争利益的详细信息; 数据和代码可用性声明可在 Highly accurate protein structure prediction with AlphaFold – Nature 上获得。

 Highly accurate protein structure prediction with AlphaFold – Nature

AI制药 : 分子对接

任务:

1、安装学习yutobe上面的软件

https://www.youtube.com/watch?v=Sux91FJ3Xe8&t=629s

2、跑通论文代码

https://www.youtube.com/watch?v=Sux91FJ3Xe8&t=629s

Pymol简介

Pymol是一款操作简单,功能强大的分子以及蛋白的可视化软件,由薛定谔公司研发,科研人员可以从官网申请最新教育版本,同时pymo的开源版(https://github.com/schrodinger/pymol-open-source),可以直接从网站上下载,但是版本较老。所以,根据需求选择版本进行下载。 说明: https://cloud.tencent.com/developer/article/1785088

Pymol入门教程:

http://pymol.chenzhaoqiang.com/intro/startManual.html

分子对接教程

1、

https://cloud.tencent.com/developer/inventory/15332

2、https://www.bilibili.com/video/av466685164?from=search&seid=9870338638011316620&spm_id_from=333.337.0.0

vina只负责对接,mgltool负责提供蛋白质分子和配体分子。

先用MGL 生成vina需要的pdbqt文件

MGL tools的作用就是生成pdbqt文件

1、:打开MGL tools,打开受体蛋白的pdb :file-》read molecule

2、蛋白质的pdb(数据库):

关于蛋白质结构的PDB文件,做分子对接,估计大家都知道PDB这个蛋白质数据库啦。这里简单的介绍一下。

蛋白质的三级结构是指整条多肽链的三维空间结构,也就是包括碳骨架和侧链在内的所有原子的空间排列。第一个蛋白质的三维空间结构于 1958 年用 X-射线衍射法(X-ray Crystallography)测定。这种方法目前仍然是获取蛋白质三级结构的主要方法。PDB 数据库中绝大多数蛋白质结构都是用这种方法测定的。另一个测定蛋白质三维空间结构的方法是核磁共振法(Nuclear Magnetic Resonance, NMR)。无法结晶的蛋白质,可以利用核磁共振法在液体环境中进行结构测定。但是核磁共振法只能用于质量小于 70 千道尔顿的分子,大约对应 200 个氨基酸的长度。除此之外,还有一些不太常用的方法也可以测定分子的三维空间结构,比如冷冻电子显微镜技术(Cyro-Electron Microscopy)。无论用什么方法测定的空间结构,都要提交到 PDB 数据库。所以我们获取蛋白质三级结构最直接的办法就是去PDB 搜索(http://www.rcsb.org/)。 从PDB首页的搜索条里,可以通过搜索PDB ID、分子名称、作者姓名等关键词来查找蛋白质三级结构。此外,利用高级搜索工具,可以通过序列相似性搜索获得与输入序列在序列水平上相似的蛋白质的三级结构。搜索方法选 BLAST,输入序列,点击“Result Count”。这里不详细介绍,因为我们做分子对接,通常蛋白名称是已知的。我们重点介绍怎么选择合适的蛋白结构文件。 比如我们搜索PI3K这个蛋白,直接在搜索栏搜索,结果是有很多的。可以看到有393个结构信息。首先我们可以通过左边的栏进行筛选,比如物种信息,我们选择人。当然,结果的显示排序可通过结果上面的选项卡进行选择不同的排序方式。我们筛选合适的蛋白结构,常用Score这个选项.我们选择分辨率较好的在前。这里的0.9Å,Å是光波长度和分子直径的常用计量单位,值越小,分辨率越高,结构越准确。页面往下拉,可以看见这个值越来越大,我们优先选择值小的。我们可以从页面里面看见一下基本信息,比如方法,物种以及被解析的时间等。这里5GJI这个结构获取的方法就是X-RAY。我们点击这个蛋白,进入后可以看见详细的信息。然后我们还要看这个蛋白的描述是不是我们想要的蛋白,从这里面感觉看起来比较费劲。这里我们借助uniprot这个数据库来选择是比较方便的。这里简单介绍一下这个数据库,可能有的同学是第一次知道。翻了多年前的笔记,粘贴在下面。 UniProt 数据库有三个层次。

第一层叫 UniParc,收录了所有 UniProt 数据库子库中的蛋白质序列,量大,粗糙。

第二层是 UniRef,他归纳了 UniProt 几个主要数据库并且是将重复序列去除后的数据库。

第三层是 UniProtKB,他有详细注释并与其他数据库有链接,分为 UniProtKB 下的 Swiss-Prot和 UniProtKB 下的 TrEMBL 数据库。

关系稍有点复杂,但实际上我们最常用的就是 UniProtKB下的 Swiss-Prot 数据库。

从 UniProt 数据库查看一条蛋白质序列(http://www.uniprot.org/)。在UniProt数据库的首页上有一个关于 UniProtKB 数据库的统计表。可以看到,TrEMBL 数据库里存储的序列数量远远大于 Swiss-Prot 中的。统计表里清楚的写着:TrEMBL 是自动注释的,没有经过检查,而 Swiss-Prot 是人工注释的,并且经过检查。

然后点击下载文件就可以直接下载PDB格式的蛋白结构文件。下载的PDB文件可以用pymol或者VMD观察结构。能够实现蛋白质三维结构可视化的软件非常多。比专业级的PyMOL(https://pymol.org/2/)。这个软件已经被世界上著名的生物医药软件公司“薛定谔公司(Schrödinger)”收购。这种专业级的可视化软件不仅能够做出非常漂亮的图片,它还有强大的插件支持各种各样的蛋白质结构分析,这款软件需要购买,如果你发表的文章里提到某些内容是使用PyMOL制作的,而文章中所有作者和作者单位都没有PyMOL的购买记录的话,你可能会面临薛定谔公司的追责。 如果要对接的蛋白没有结构,我们又要对接,那就只能是自己通过软件预测了。蛋白质结构预测的方法有从头计算法,同源建模法,穿线法和综合法。常用的是同源建模法,SWISS-MODEL(www.swissmodel.expasy.org)就是一款用同源建模法预测蛋白质三级结构的全自动软件,这里不详细介绍了,预测的模型还要涉及模型好坏的评价,后续有时间,再介绍蛋白质三级结构的预测。

接下来我们打开AutoDockTools(ADT),打开我们前面保存的文件1E8Y_PYMOL.pdb

删除水分子和其他配体,常规操作不用解释

然后计算电荷和添加原子类型

Edit–Charges–Compute Gasteiger

Edit–Atoms–Assign AD4 type

就可以导出成pbdqt格式的文件了

然后右键吧蛋白删除掉,导入配体小分子,随便从ZINC下了一个

ZINC(http://zinc.docking.org/) 还有一个数据库能下载mol2格式的文件。ZINC),这里就不介绍了,你要是能从上面的数据库下载到你配体小分子的mol2格式文件,就直接用,如果不能,那就是去PubChem数据库(https://pubchem.ncbi.nlm.nih.gov/)下载sdf文件,然后进行转换,这也是我这里要介绍的。

Ligand-input-open

子对接教程

1 分子对接的工作环境
1.1 对接基本需求
受体的 pdb 文件、知道配体的结构
1.2 对接软件
使用 AutoDock 进行对接,该软件由 AutoDock 和 MGLTools 两
部分组成,AutoDock 为主程序,仅提供了命令行接口,而 MGLTools
中的 AutoDockTools(ADT)可以看作是 AutoDock 的图形用户界面。
配体使用 ChemDraw 和 Chem3D 绘制,该软件为收费软件,可
使用其它免费的结构式绘制工具进行结构绘制,使用 Open Babel 转
3D 结构和文件格式的转换。
使用 PyMol 开源版本对受体蛋白的 pdb 文件进行处理。

2 准备受体、配体的 pdbqt 文件


AutoDock 只接受 pdbqt 格 式 的 文 件 , 所 以 需 要 通 过
AutoDockTools 将 pdb 或者其他格式的文件转化为 pdbqt 文件。


2.1 准备受体的 pdbqt 文件
2.1.1 使用 PyMol 软件进行处理


pdb 下载的蛋白需要先使用 PyMol 软件删除多余的离子、水分
子,同源多聚体蛋白还可以只保留一条链。直接从 pdb 下载蛋白可以
使用 fetch [protein name]命令。导入后 pdb 文件后,点击右下角的 S
以显示结构信息、

2.1.2 关于 pdb 文件的格式

该格式省略了一切氢原子,包括游离的水分子都只保留了一个 O,所以后续需要加氢操作。该文件的格式较复杂,最好使用成熟的软件进行编辑,而不上自行编辑。该文件中的氨基酸残基的原子全部记录在 ATOM 行中,后面的每一项分别为原子序号、原子名称(第一个字母为原子的元素符号,第二个字母为远近标识符 A、B、G、D、E、Z、H 分别对应有机化合物命名系统中的 α、β、γ、δ、ε、ζ、η)、残基名称、链编号、残基序号、原子坐标等

而离子、水分子以及结合在蛋白中的配体(如抑制剂等)等非蛋白质的部分记录在 HETNAM(非标准残基的名称)中:

同样的依次是原子编号、原子名称、基团名称(如水这里起名为HOH)、链编号(但是它本身不依附于哪条链)、原子编号、坐标等。

2.1.3 使用 PyMol 命令进行选择和删除
选择也可以使用命令选择,select 命令可以用于选择链如:
select chain A
indicate 命令的格式则为 indicate [element_type] [name],如根据残基
名称 HOH 选择所有的水分子:
indicate resname HOH
需要注意的是,选择水分子不要使用:
indicate name O
否则其会选中所有氨基酸残基中的氧原子。根据残基名称选择配体
Mr-Greyfun
(示例中配体名称为 STI):
indicate resname STI
根据原子名称选择氯原子:
indicate name CL
可以使用 remove 命令进行删除操作,命令格式与之类似:
remove resname HOH
remove name CL
remove chain B

2.1.1 保存
在 PyMol 中完成上述处理后使用 Export Molecule-save 保存成pdb 格式的文件。
2.1.2 加氢操作
pdb 文件会省略氢原子,所以需要进行加氢,可以在 PyMol 中使用 h_add 命 令 加氢 (删除为 remove hydrogen ), 但最 好 使 用AutoDockTools(ADT)进行加氢。处理好后的文件用 ADT 打开后,点击 Edit-Hydrogens-Add-OK。加氢。
2.1.3 转化为 pdbqt 格式的文件
点击 Grid-Macromolecule-Choose,选择好后点 select molecule,
它会自动加电荷等,点 OK 即可,完成后,会弹出保存窗口,直接保
存 pdbqt 格式即可

2.1.1 保存
在 PyMol 中完成上述处理后使用 Export Molecule-save 保存成
pdb 格式的文件。
2.1.2 加氢操作
pdb 文件会省略氢原子,所以需要进行加氢,可以在 PyMol 中使
用 h_add 命 令 加氢 (删除为 remove hydrogen ), 但最 好 使 用
AutoDockTools(ADT)进行加氢。
处理好后的文件用 ADT 打开后,点击 Edit-Hydrogens-Add-OK。
加氢。
2.1.3 转化为 pdbqt 格式的文件
点击 Grid-Macromolecule-Choose,选择好后点 select molecule,
它会自动加电荷等,点 OK 即可,完成后,会弹出保存窗口,直接保
存 pdbqt 格式即可

设置好后点 Done,然后点击 Ligand-Output-Save as PDBQT 即可
保存。

2.2.1 附:用 PyMol 导出 pdb 文件中的配体的方法
在界面上选中小分子,点击(sele)的 A-copy to object-new。红框内
的区域可以点击后将其对应的对象隐藏起来:
然后点击 Export Molecule-save 保存成 pdb 格式的文件。注意,这个
内置的配体也是没有加氢的,需要用 ADT 加氢。

3 对接
3.1 对接操作步骤
3.1.1 导入受体和配体
在 ADT 里面,点击 Grid-Macromolecule-Open 打开受体蛋白
点击 Grid-Set-Map-Types-Open Ligand 打开受体的 pdbqt 文件
3.1.2 定义对接盒子
点击 Grid-Grid Box 定义对接盒子
3.1.3 生成 config.txt 文件
点击 Docking-Output-Vina config 生成 config.txt 文件
3.1.4 启动对接

视频中的步骤:

1、导入protein蛋白质

2、删除水分子 如果你的对接区域有水分子,会影响对接结果

pdq该格式省略了一切氢原子,包括游离的水分子都只保留了一个 O,所以后续需要加氢操作。

3、edit ->hydrogens->add->polar only 此时结构中发亮的就是氢键(加氢)

4、加电荷 edit -> charges->add kollman charges

5 报存 grid -> macromolecule ->choose->select

准备配体文件:

以sdf结尾的文件直接拖进 AUTO软件中会报错,需要转换成pdb文件

可以使用pymol可视化工具转换(注意:配体 英文 ligend)

1、将该文件拖动到pymol中打开,file ->molecule

配体文件:

1、打开auto dock,将配体文件导入:

2、 点击 Ligand(配体)->input->choose (这一步就是生成了配体文件)

3、点击 Ligand(配体)->output->save as pdbqt

接下来就可以进行分子对接:

1、将两个(受体和配体)pdpqt导入

2、重新选择蛋白质分子作为受体

点击NO

接下来设置对接盒子:

grid -> grid box

设置盒子位置(spacing设置为1)

然后 grid box 弹出设置中选择 file->output grid dimensions file(保存盒子设置)

保存:

新建config.txt:用于启动vina

receptor 蛋白质名(生成的蛋白质文件名)

ligand 配体名

center 和size在上一步的grid中有

receptor:指定受体分子的路径

ligand:配体分子的路径

center_x,center_y,center_z:搜索空间中心的坐标

size_x,size_y,size_z:指定搜索空间的大小。这里设置的大小基本就是把整个受体分子都包含了,属于blind docking。如何更准确确定结合口袋的位置,我们稍后再说。

energy_range:默认4,与最优结合模型相差的最大能量值,单位是kcal/mol。比方说,最优模型的能量计算出来是-8.5kcal/mol,那么vina也就最多计算到-4.5kcal/mol的模型就终止了,也就意味着这个值决定了生成模型的最大个数。

exhaustiveness:用来控制对接的细致程度,默认值是8. 大致与时间成正比。

num_modes:最多生成多少个模型。实际生成的模型数由num_modes和energy_range共同决定。

energy_range
 maximum energy difference between the best binding 
 mode and the worst one displayed (kcal/mol)

最后启动vina 进行分子对接:

在config目录中打开cmd->输入vina

其中:这条命令就是利用config.txt文件进行分子对接(cmd 必须在config文件目录下打开)

执行 命令:

D:\Autodock\pdbqt>”D:\Vina\vina.exe” –receptor protein.pdbqt –ligand ligend.pdbqt –config config.txt –log log.txt –out output.pdbqt

“D:\Vina\vina.exe” –receptor selected_prediction_ready.pdbqt –ligand Conformer3D_CID_65536.pdbqt –config config.txt –log log.txt –out output.pdbqt

如果上述命令报错,一般是生成的config文件有问题:

正确的config内容如下:

执行完毕可以看到生成一个log文件

vina 中的affinity是亲合力结果排名

最后:

查看生成的模型:

将protein和output输出导入pymol

点击 all -> s查看表面 结构

点击左右箭头查看不同的结构:

5.我们挑选第一个模型,看看结构方式是怎样的,见下图。很显然与真实的结合方式相差甚远,可以说是完全错误。

-1.jpg

为什么会出现这种情况,很大程度上是因为search space太大,可能需要设置更大的exhaustiveness。

如果我们大致知道binding pocket在什么位置,那准确性应该会高不少,如何大致确定binding pocket的位置呢?我们接着试验。

可以通过实验的方式,比如某个点突变对结合或者活性影响非常大,那么大概率这个残基是结合口袋的一部分。

可以通过软件预测,比如蛋白与配体(底物)结合位点预测:https://zhanglab.ccmb.med.umich.edu/COACH/

再比如Discovery studio软件(专业版的),可以很方便的根据受体分子的表面形状来预测结合口袋位置。

20190507175152.png

口袋的坐标为:

34.3356,14.9412,26.9615

我们修改下对接参数,新的参数如下:

receptor = r.pdbqt

ligand = nap.pdbqt

center_x = 34.3356

center_y = 14.9412

center_z = 26.9615

size_x = 30.0

size_y = 30.0

size_z = 30.0

energy_range = 4

exhaustiveness = 10

num_modes = 10

最优结果与真实模型的RMSD为1.795埃,可以说非常精准了

比对结果如下:

Souce: 纽普生物    2019-05-07

PyMOL 相关操作:

1、导入蛋白质

先在NCBI子数据库structure检索所需要的蛋白结构,https://www.ncbi.nlm.nih.gov/structure/?term=
记录下该蛋白的PDB ID,然后打开PyMOL,命令行输入:

fetch 5ocn#foxn1
fetch 3uf0#
fetch 1si4#血红蛋白

去除水分子

remove solvent

分离得到蛋白

remove organic

分离配体:

../_images/splitlig2020-03-12_202822.573956.png

pymol教程:

http://pymol.chenzhaoqiang.com/intro/startManual.html

分子对接进阶教程:选定对接位点区域:

如果我们想要将配体和蛋白质受体的某几个位点部位对接:

1、标记这些点 select->select from string ,选择对应的molecule和链,以及residue(寻找的位点),点击add

2、此时,会出现currenr selection ,选择该列中 c,点击绿点,会将对应的分子三D化。

3、接下来设置盒子,spacing 设置为1(相当于比例尺),xyz一般设置为20-22

这样就可以了。

一文归纳 AI 数据增强之法

作者 | 算法进阶
摘自: 算法进阶微信公众号

数据、算法、算力是人工智能发展的三要素。数据决定了Ai模型学习的上限,数据规模越大、质量越高,模型就能够拥有更好的泛化能力。然而在实际工程中,经常有数据量太少(相对模型而言)、样本不均衡、很难覆盖全部的场景等问题,解决这类问题的一个有效途径是通过数据增强(Data Augmentation),使模型学习获得较好的泛化性能。

1 数据增强介绍

数据增强(Data Augmentation)是在不实质性的增加数据的情况下,从原始数据加工出更多的表示,提高原数据的数量及质量,以接近于更多数据量产生的价值。其原理是,通过对原始数据融入先验知识,加工出更多数据的表示,有助于模型判别数据中统计噪声,加强本体特征的学习,减少模型过拟合,提升泛化能力。

如经典的机器学习例子–哈士奇误分类为狼:通过可解释性方法,可发现错误分类是由于图像上的雪造成的。通常狗对比狼的图像里面雪地背景比较少,分类器学会使用雪作为一个特征来将图像分类为狼还是狗,而忽略了动物本体的特征。此时,可以通过数据增强的方法,增加变换后的数据(如背景换色、加入噪声等方式)来训练模型,帮助模型学习到本体的特征,提高泛化能力。

需要关注的是,数据增强样本也有可能是引入片面噪声,导致过拟合。此时需要考虑的是调整数据增强方法,或者通过算法(可借鉴Pu-Learning思路)选择增强数据的最佳子集,以提高模型的泛化能力。

常用数据增强方法可分为:基于样本变换的数据增强及基于深度学习的数据增强。

2 基于样本变换的数据增强

样本变换数据增强即采用预设的数据变换规则进行已有数据的扩增,包含单样本数据增强和多样本数据增强。

2.1 单样本增强

单(图像)样本增强主要有几何操作、颜色变换、随机擦除、添加噪声等方法,可参见imgaug开源库。

2.2 多样本数据增强方法

多样本增强是通过先验知识组合及转换多个样本,主要有Smote、SamplePairing、Mixup等方法在特征空间内构造已知样本的邻域值。

  • Smote

Smote(Synthetic Minority Over-sampling Technique)方法较常用于样本均衡学习,核心思想是从训练集随机同类的两近邻样本合成一个新的样本,其方法可以分为三步:

1、 对于各样本X_i,计算与同类样本的欧式距离,确定其同类的K个(如图3个)近邻样本;

2、从该样本k近邻中随机选择一个样本如近邻X_ik,生成新的样本:

Xsmote_ik =  Xi  +  rand(0,1) ∗ ∣X_i − X_ik∣  

3、重复2步骤迭代N次,可以合成N个新的样本。

# SMOTE
from imblearn.over_sampling import SMOTE

print("Before OverSampling, counts of label\n{}".format(y_train.value_counts()))
smote = SMOTE()
x_train_res, y_train_res = smote.fit_resample(x_train, y_train)
print("After OverSampling, counts of label\n{}".format(y_train_res.value_counts())) 
  • SamplePairing

SamplePairing算法的核心思想是从训练集随机抽取的两幅图像叠加合成一个新的样本(像素取平均值),使用第一幅图像的label作为合成图像的正确label。

  • Mixup

Mixup算法的核心思想是按一定的比例随机混合两个训练样本及其标签,这种混合方式不仅能够增加样本的多样性,且能够使决策边界更加平滑,增强了难例样本的识别,模型的鲁棒性得到提升。其方法可以分为两步:

1、从原始训练数据中随机选取的两个样本(xi, yi) and (xj, yj)。其中y(原始label)用one-hot 编码。

2、对两个样本按比例组合,形成新的样本和带权重的标签

x˜ = λxi + (1 − λ)xj  
y˜ = λyi + (1 − λ)yj  

最终的loss为各标签上分别计算cross-entropy loss,加权求和。其中 λ ∈ [0, 1], λ是mixup的超参数,控制两个样本插值的强度。

# Mixup
def mixup_batch(x, y, step, batch_size, alpha=0.2):
    """
    get batch data
    :param x: training data
    :param y: one-hot label
    :param step: step
    :param batch_size: batch size
    :param alpha: hyper-parameter α, default as 0.2
    :return:  x y 
    """
    candidates_data, candidates_label = x, y
    offset = (step * batch_size) % (candidates_data.shape[0] - batch_size)

    # get batch data
    train_features_batch = candidates_data[offset:(offset + batch_size)]
    train_labels_batch = candidates_label[offset:(offset + batch_size)]

    if alpha == 0:
        return train_features_batch, train_labels_batch

    if alpha > 0:
        weight = np.random.beta(alpha, alpha, batch_size)
        x_weight = weight.reshape(batch_size, 1)
        y_weight = weight.reshape(batch_size, 1)
        index = np.random.permutation(batch_size)
        x1, x2 = train_features_batch, train_features_batch[index]
        x = x1 * x_weight + x2 * (1 - x_weight)
        y1, y2 = train_labels_batch, train_labels_batch[index]
        y = y1 * y_weight + y2 * (1 - y_weight)
        return x, y 

3 基于深度学习的数据增强

3.1 特征空间的数据增强

不同于传统在输入空间变换的数据增强方法,神经网络可将输入样本映射为网络层的低维向量(表征学习),从而直接在学习的特征空间进行组合变换等进行数据增强,如MoEx方法等。

3.2 基于生成模型的数据增强

生成模型如变分自编码网络(Variational Auto-Encoding network, VAE)和生成对抗网络(Generative Adversarial Network, GAN),其生成样本的方法也可以用于数据增强。这种基于网络合成的方法相比于传统的数据增强技术虽然过程更加复杂, 但是生成的样本更加多样。

  • 变分自编码器VAE变分自编码器(Variational Autoencoder,VAE)其基本思路是:将真实样本通过编码器网络变换成一个理想的数据分布,然后把数据分布再传递给解码器网络,构造出生成样本,模型训练学习的过程是使生成样本与真实样本足够接近。
# VAE模型
class VAE(keras.Model):
    ...
    def train_step(self, data):
        with tf.GradientTape() as tape:
            z_mean, z_log_var, z = self.encoder(data)
            reconstruction = self.decoder(z)
            reconstruction_loss = tf.reduce_mean(
                tf.reduce_sum(
                    keras.losses.binary_crossentropy(data, reconstruction), axis=(1, 2)
                )
            )
            kl_loss = -0.5 * (1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var))
            kl_loss = tf.reduce_mean(tf.reduce_sum(kl_loss, axis=1))
            total_loss = reconstruction_loss + kl_loss
        grads = tape.gradient(total_loss, self.trainable_weights)
        self.optimizer.apply_gradients(zip(grads, self.trainable_weights))
        self.total_loss_tracker.update_state(total_loss)
        self.reconstruction_loss_tracker.update_state(reconstruction_loss)
        self.kl_loss_tracker.update_state(kl_loss)
        return {
            "loss": self.total_loss_tracker.result(),
            "reconstruction_loss": self.reconstruction_loss_tracker.result(),
            "kl_loss": self.kl_loss_tracker.result(),
        } 
  • 生成对抗网络GAN生成对抗网络-GAN(Generative Adversarial Network) 由生成网络(Generator, G)和判别网络(Discriminator, D)两部分组成, 生成网络构成一个映射函数GZX(输入噪声z, 输出生成的图像数据x), 判别网络判别输入是来自真实数据还是生成网络生成的数据。
# DCGAN模型

class GAN(keras.Model):
    ...
    def train_step(self, real_images):
        batch_size = tf.shape(real_images)[0]
        random_latent_vectors = tf.random.normal(shape=(batch_size, self.latent_dim))
        # G: Z→X(输入噪声z, 输出生成的图像数据x)
        generated_images = self.generator(random_latent_vectors)
        # 合并生成及真实的样本并赋判定的标签
        combined_images = tf.concat([generated_images, real_images], axis=0)
        labels = tf.concat(
            [tf.ones((batch_size, 1)), tf.zeros((batch_size, 1))], axis=0
        )
        # 标签加入随机噪声
        labels += 0.05 * tf.random.uniform(tf.shape(labels))
        # 训练判定网络
        with tf.GradientTape() as tape:
            predictions = self.discriminator(combined_images)
            d_loss = self.loss_fn(labels, predictions)
        grads = tape.gradient(d_loss, self.discriminator.trainable_weights)
        self.d_optimizer.apply_gradients(
            zip(grads, self.discriminator.trainable_weights)
        )

        random_latent_vectors = tf.random.normal(shape=(batch_size, self.latent_dim))
        # 赋生成网络样本的标签(都赋为真实样本)
        misleading_labels = tf.zeros((batch_size, 1))
        # 训练生成网络
        with tf.GradientTape() as tape:
            predictions = self.discriminator(self.generator(random_latent_vectors))
            g_loss = self.loss_fn(misleading_labels, predictions)
        grads = tape.gradient(g_loss, self.generator.trainable_weights)
        self.g_optimizer.apply_gradients(zip(grads, self.generator.trainable_weights))
        # 更新损失
        self.d_loss_metric.update_state(d_loss)
        self.g_loss_metric.update_state(g_loss)
        return {
            "d_loss": self.d_loss_metric.result(),
            "g_loss": self.g_loss_metric.result(),
        }

3.3 基于神经风格迁移的数据增强

神经风格迁移(Neural Style Transfer)可以在保留原始内容的同时,将一个图像的样式转移到另一个图像上。除了实现类似色彩空间照明转换,还可以生成不同的纹理和艺术风格。

神经风格迁移是通过优化三类的损失来实现的:

style_loss:使生成的图像接近样式参考图像的局部纹理;

content_loss:使生成的图像的内容表示接近于基本图像的表示;

total_variation_loss:是一个正则化损失,它使生成的图像保持局部一致。

# 样式损失
def style_loss(style, combination):
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = img_nrows * img_ncols
    return tf.reduce_sum(tf.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))

# 内容损失
def content_loss(base, combination):
    return tf.reduce_sum(tf.square(combination - base))

# 正则损失
def total_variation_loss(x):
    a = tf.square(
        x[:, : img_nrows - 1, : img_ncols - 1, :] - x[:, 1:, : img_ncols - 1, :]
    )
    b = tf.square(
        x[:, : img_nrows - 1, : img_ncols - 1, :] - x[:, : img_nrows - 1, 1:, :]
    )
    return tf.reduce_sum(tf.pow(a + b, 1.25))

3.4 基于元学习的数据增强

深度学习研究中的元学习(Meta learning)通常是指使用神经网络优化神经网络,元学习的数据增强有神经增强(Neural augmentation)等方法。

  • 神经增强

神经增强(Neural augmentation)是通过神经网络组的学习以获得较优的数据增强并改善分类效果的一种方法。其方法步骤如下:

1、获取与target图像同一类别的一对随机图像,前置的增强网络通过CNN将它们映射为合成图像,合成图像与target图像对比计算损失;

2、将合成图像与target图像神经风格转换后输入到分类网络中,并输出该图像分类损失;

3、将增强与分类的loss加权平均后,反向传播以更新分类网络及增强网络权重。使得其输出图像的同类内差距减小且分类准确。

MAE:Masked Autoencoders Are Scalable Vision Learners

摘自 Jack Cui

马赛克,克星,真来了!

何恺明大神的新作 论文:https://arxiv.org/abs/2111.06377

项目地址:https://github.com/facebookresearch/mae

简单讲:将图片随机遮挡然后复原。并且遮挡的比例,非常大超过整张图的 80% ,我们直接看效果:

第一列是遮挡图,第二列是修复结果,第三列是原图。图片太多,可能看不清,我们单看一个:

看这个遮挡的程度,表针、表盘几乎都看不见了。但是 MAE 依然能够修复出来:

这个效果真的很惊艳!甚至对于遮挡 95% 的面积的图片依然 work。

看左图,你能看出来被遮挡的是蘑菇吗??MAE 却能轻松修复出来。接下来,跟大家聊聊 MAE。

Vit

讲解 MAE 之前不得不先说下 Vit。红遍大江南北的 Vision Transformer,ViT。领域内的小伙伴,或多或少都应该听说过。它将 Transformer 应用到了 CV 上面,将整个图分为 16 * 16 的小方块,每个方块做成一个词,然后放进 Transformer 进行训练。视觉transformer 和自然语言处理 中的transformer可以进行类比,可以把一个图像块理解成一个单词。

MAE

MAE 结构设计的非常简单:

将一张图随机打 Mask,未 Mask 部分输入给 Encoder 进行编码学习,这个 Encoder 就是 Vit,然后得到每个块的特征。再将未 Mask 部分以及 Mask 部分全部输入给 Decoder 进行解码学习,最终目标是修复图片。而 Decoder 就是一个轻量化的 Transformer。它的损失函数就是普通的 MSE。所以说, MAE 的 Encoder 和 Decoder 结构不同,是非对称式的。Encoder 将输入编码为 latent representation,而 Decoder 将从 latent representation 重建原始信号。

项目提供了 Colab,如果你能登陆,那么可以直接体验:https://colab.research.google.com/github/facebookresearch/mae/blob/main/demo/mae_visualize.ipynb

如果不能登陆,可以直接本地部署,作者提供了预训练模型。

MAE 可以用来生成不存在的内容,就像 GAN 一样。

首先来看看神魔是 Transformer :

Transformer 最初主要应用于一些自然语言处理场景,比如翻译、文本分类、写小说、写歌等。随着技术的发展,Transformer 开始征战视觉领域,分类、检测等任务均不在话下,逐渐走上了多模态的道路。

Transformer 是 Google 在 2017 年提出的用于机器翻译的模型。

Transformer 的内部,在本质上是一个 Encoder-Decoder 的结构,即 编码器-解码器。

Transformer 中抛弃了传统的 CNN 和 RNN,整个网络结构完全由 Attention 机制组成,并且采用了 6 层 Encoder-Decoder 结构。

显然,Transformer 主要分为两大部分,分别是编码器解码器。整个 Transformer 是由 6 个这样的结构组成,为了方便理解,我们只看其中一个Encoder-Decoder 结构。

以一个简单的例子进行说明:

Why do we work?,我们为什么工作?左侧红框是编码器,右侧红框是解码器编码器负责把自然语言序列映射成为隐藏层(上图第2步),即含有自然语言序列的数学表达。解码器把隐藏层再映射为自然语言序列,从而使我们可以解决各种问题,如情感分析、机器翻译、摘要生成、语义关系抽取等。简单说下,上图每一步都做了什么:

  • 输入自然语言序列到编码器: Why do we work?(为什么要工作);
  • 编码器输出的隐藏层,再输入到解码器;
  • 输入 <𝑠𝑡𝑎𝑟𝑡> (起始)符号到解码器;
  • 解码器得到第一个字”为”;
  • 将得到的第一个字”为”落下来再输入到解码器;
  • 解码器得到第二个字”什”;
  • 将得到的第二字再落下来,直到解码器输出 <𝑒𝑛𝑑> (终止符),即序列生成完成。

解码器和编码器的结构类似,本文以编码器部分进行讲解。即把自然语言序列映射为隐藏层的数学表达的过程,因为理解了编码器中的结构,理解解码器就非常简单了。为了方便学习,我将编码器分为 4 个部分,依次讲解。

1、位置嵌入(𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛𝑎𝑙 𝑒𝑛𝑐𝑜𝑑𝑖𝑛𝑔)

我们输入数据 X 维度为[batch size, sequence length]的数据,比如我们为什么工作。batch size 就是 batch 的大小,这里只有一句话,所以 batch size 为 1,sequence length 是句子的长度,一共 7 个字,所以输入的数据维度是 [1, 7]。我们不能直接将这句话输入到编码器中,因为 Tranformer 不认识,我们需要先进行字嵌入,即得到图中的 。简单点说,就是文字->字向量的转换,这种转换是将文字转换为计算机认识的数学表示,用到的方法就是 Word2Vec,Word2Vec 的具体细节,对于初学者暂且不用了解,这个是可以直接使用的。得到维度是 [batch size, sequence length, embedding dimension],embedding dimension 的大小由 Word2Vec 算法决定,Tranformer 采用 512 长度的字向量。所以 的维度是 [1, 7, 512]。至此,输入的我们为什么工作,可以用一个矩阵来简化表示。

我们知道,文字的先后顺序,很重要。比如吃饭没、没吃饭、没饭吃、饭吃没、饭没吃,同样三个字,顺序颠倒,所表达的含义就不同了。文字的位置信息很重要,Tranformer 没有类似 RNN 的循环结构,没有捕捉顺序序列的能力。为了保留这种位置信息交给 Tranformer 学习,我们需要用到位置嵌入。加入位置信息的方式非常多,最简单的可以是直接将绝对坐标 0,1,2 编码。Tranformer 采用的是 sin-cos 规则,使用了 sin 和 cos 函数的线性变换来提供给模型位置信息:

上式中 pos 指的是句中字的位置,取值范围是 [0, 𝑚𝑎𝑥 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ),i 指的是字嵌入的维度, 取值范围是 [0, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛)。 就是 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 的大小。上面有 sin 和 cos 一组公式,也就是对应着 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 维度的一组奇数和偶数的序号的维度,从而产生不同的周期性变化。

# 导入依赖库
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math

def get_positional_encoding(max_seq_len, embed_dim):
    # 初始化一个positional encoding
    # embed_dim: 字嵌入的维度
    # max_seq_len: 最大的序列长度
    positional_encoding = np.array([
        [pos / np.power(10000, 2 * i / embed_dim) for i in range(embed_dim)]
        if pos != 0 else np.zeros(embed_dim) for pos in range(max_seq_len)])
    positional_encoding[1:, 0::2] = np.sin(positional_encoding[1:, 0::2])  # dim 2i 偶数
    positional_encoding[1:, 1::2] = np.cos(positional_encoding[1:, 1::2])  # dim 2i+1 奇数
    # 归一化, 用位置嵌入的每一行除以它的模长
    # denominator = np.sqrt(np.sum(position_enc**2, axis=1, keepdims=True))
    # position_enc = position_enc / (denominator + 1e-8)
    return positional_encoding
    
positional_encoding = get_positional_encoding(max_seq_len=100, embed_dim=16)
plt.figure(figsize=(10,10))
sns.heatmap(positional_encoding)
plt.title("Sinusoidal Function")
plt.xlabel("hidden dimension")
plt.ylabel("sequence length")
可以看到,位置嵌入在 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 (也是hidden dimension )维度上随着维度序号增大,周期变化会越来越慢,而产生一种包含位置信息的纹理。

就这样,产生独一的纹理位置信息,模型从而学到位置之间的依赖关系和自然语言的时序特性。

最后,将Xembedding  和 位置嵌入 相加,送给下一层。

2、自注意力层(𝑠𝑒𝑙𝑓 𝑎𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛 𝑚𝑒𝑐ℎ𝑎𝑛𝑖𝑠𝑚)

直接看下图笔记,讲解的非常详细。

多头的意义在于,\(QK^{T}\) 得到的矩阵就叫注意力矩阵,它可以表示每个字与其他字的相似程度。因为,向量的点积值越大,说明两个向量越接近。

我们的目的是,让每个字都含有当前这个句子中的所有字的信息,用注意力层,我们做到了。

需要注意的是,在上面 𝑠𝑒𝑙𝑓 𝑎𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛 的计算过程中,我们通常使用 𝑚𝑖𝑛𝑖 𝑏𝑎𝑡𝑐ℎ,也就是一次计算多句话,上文举例只用了一个句子。

每个句子的长度是不一样的,需要按照最长的句子的长度统一处理。对于短的句子,进行 Padding 操作,一般我们用 0 来进行填充。

3、残差链接和层归一化

加入了残差设计和层归一化操作,目的是为了防止梯度消失,加快收敛。

1) 残差设计

我们在上一步得到了经过注意力矩阵加权之后的 𝑉, 也就是 𝐴𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛(𝑄, 𝐾, 𝑉),我们对它进行一下转置,使其和 𝑋𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 的维度一致, 也就是 [𝑏𝑎𝑡𝑐ℎ 𝑠𝑖𝑧𝑒, 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛] ,然后把他们加起来做残差连接,直接进行元素相加,因为他们的维度一致:

$$
X_{\text {embedding }}+\text { Attention }(Q, K, V)
$$

在之后的运算里,每经过一个模块的运算,都要把运算之前的值和运算之后的值相加,从而得到残差连接,训练的时候可以使梯度直接走捷径反传到最初始层:

$$
X+\text { SubLayer }(X)
$$

2) 层归一化

作用是把神经网络中隐藏层归一为标准正态分布,也就是 𝑖.𝑖.𝑑 独立同分布, 以起到加快训练速度, 加速收敛的作用。

$$
\mu_{i}=\frac{1}{m} \sum_{i=1}^{m} x_{i j}
$$

上式中以矩阵的行 (𝑟𝑜𝑤) 为单位求均值:

$$
\sigma_{j}^{2}=\frac{1}{m} \sum_{i=1}^{m}\left(x_{i j}-\mu_{j}\right)^{2}
$$

上式中以矩阵的行 (𝑟𝑜𝑤) 为单位求方差:

$$
\operatorname{LayerNorm}(x)=\alpha \odot \frac{x_{i j}-\mu_{i}}{\sqrt{\sigma_{i}^{2}+\epsilon}}+\beta
$$

然后用每一行的每一个元素减去这行的均值,再除以这行的标准差,从而得到归 一化后的数值, \(\epsilon\) 是为了防止除 0 ;之后引入两个可训练参数 \(\alpha, \beta\) 来弥补归一化的过程中损失掉的信息,注意 \(\odot\) 表示 元素相乘而不是点积,我们一般初始化 \(\alpha\) 为全 1 ,而\(\beta\) 为全 0 。

代码层面非常简单,单头 attention 操作如下:

class ScaledDotProductAttention(nn.Module):
    ''' Scaled Dot-Product Attention '''

    def __init__(self, temperature, attn_dropout=0.1):
        super().__init__()
        self.temperature = temperature
        self.dropout = nn.Dropout(attn_dropout)

    def forward(self, q, k, v, mask=None):
        # self.temperature是论文中的d_k ** 0.5,防止梯度过大
        # QxK/sqrt(dk)
        attn = torch.matmul(q / self.temperature, k.transpose(2, 3))

        if mask is not None:
            # 屏蔽不想要的输出
            attn = attn.masked_fill(mask == 0, -1e9)
        # softmax+dropout
        attn = self.dropout(F.softmax(attn, dim=-1))
        # 概率分布xV
        output = torch.matmul(attn, v)

        return output, attn

Multi-Head Attention 实现在 ScaledDotProductAttention 基础上构建:

class MultiHeadAttention(nn.Module):
    ''' Multi-Head Attention module '''

    # n_head头的个数,默认是8
    # d_model编码向量长度,例如本文说的512
    # d_k, d_v的值一般会设置为 n_head * d_k=d_model,
    # 此时concat后正好和原始输入一样,当然不相同也可以,因为后面有fc层
    # 相当于将可学习矩阵分成独立的n_head份
    def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
        super().__init__()
        # 假设n_head=8,d_k=64
        self.n_head = n_head
        self.d_k = d_k
        self.d_v = d_v
        # d_model输入向量,n_head * d_k输出向量
        # 可学习W^Q,W^K,W^V矩阵参数初始化
        self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
        # 最后的输出维度变换操作
        self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
        # 单头自注意力
        self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
        self.dropout = nn.Dropout(dropout)
        # 层归一化
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

    def forward(self, q, k, v, mask=None):
        # 假设qkv输入是(b,100,512),100是训练每个样本最大单词个数
        # 一般qkv相等,即自注意力
        residual = q
        # 将输入x和可学习矩阵相乘,得到(b,100,512)输出
        # 其中512的含义其实是8x64,8个head,每个head的可学习矩阵为64维度
        # q的输出是(b,100,8,64),kv也是一样
        q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
        k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
        v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)

        # 变成(b,8,100,64),方便后面计算,也就是8个头单独计算
        q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)

        if mask is not None:
            mask = mask.unsqueeze(1)   # For head axis broadcasting.
        # 输出q是(b,8,100,64),维持不变,内部计算流程是:
        # q*k转置,除以d_k ** 0.5,输出维度是b,8,100,100即单词和单词直接的相似性
        # 对最后一个维度进行softmax操作得到b,8,100,100
        # 最后乘上V,得到b,8,100,64输出
        q, attn = self.attention(q, k, v, mask=mask)

        # b,100,8,64-->b,100,512
        q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
        q = self.dropout(self.fc(q))
        # 残差计算
        q += residual
        # 层归一化,在512维度计算均值和方差,进行层归一化
        q = self.layer_norm(q)

        return q, attn

4、前馈网络

这个层就没啥说的了,非常简单,直接看代码吧:

class PositionwiseFeedForward(nn.Module):
    ''' A two-feed-forward-layer module '''

    def __init__(self, d_in, d_hid, dropout=0.1):
        super().__init__()
        # 两个fc层,对最后的512维度进行变换
        self.w_1 = nn.Linear(d_in, d_hid) # position-wise
        self.w_2 = nn.Linear(d_hid, d_in) # position-wise
        self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        residual = x

        x = self.w_2(F.relu(self.w_1(x)))
        x = self.dropout(x)
        x += residual

        x = self.layer_norm(x)

        return x

Transformer

Transformer 最初主要应用于一些自然语言处理场景,比如翻译、文本分类、写小说、写歌等。随着技术的发展,Transformer 开始征战视觉领域,分类、检测等任务均不在话下,逐渐走上了多模态的道路。

Transformer 是 Google 在 2017 年提出的用于机器翻译的模型。

Transformer 的内部,在本质上是一个 Encoder-Decoder 的结构,即 编码器-解码器。

Transformer 中抛弃了传统的 CNN 和 RNN,整个网络结构完全由 Attention 机制组成,并且采用了 6 层 Encoder-Decoder 结构。

显然,Transformer 主要分为两大部分,分别是编码器解码器。整个 Transformer 是由 6 个这样的结构组成,为了方便理解,我们只看其中一个Encoder-Decoder 结构。

以一个简单的例子进行说明:

Why do we work?,我们为什么工作?左侧红框是编码器,右侧红框是解码器编码器负责把自然语言序列映射成为隐藏层(上图第2步),即含有自然语言序列的数学表达。解码器把隐藏层再映射为自然语言序列,从而使我们可以解决各种问题,如情感分析、机器翻译、摘要生成、语义关系抽取等。简单说下,上图每一步都做了什么:

  • 输入自然语言序列到编码器: Why do we work?(为什么要工作);
  • 编码器输出的隐藏层,再输入到解码器;
  • 输入 <𝑠𝑡𝑎𝑟𝑡> (起始)符号到解码器;
  • 解码器得到第一个字”为”;
  • 将得到的第一个字”为”落下来再输入到解码器;
  • 解码器得到第二个字”什”;
  • 将得到的第二字再落下来,直到解码器输出 <𝑒𝑛𝑑> (终止符),即序列生成完成。

解码器和编码器的结构类似,本文以编码器部分进行讲解。即把自然语言序列映射为隐藏层的数学表达的过程,因为理解了编码器中的结构,理解解码器就非常简单了。为了方便学习,我将编码器分为 4 个部分,依次讲解。

1、位置嵌入(𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛𝑎𝑙 𝑒𝑛𝑐𝑜𝑑𝑖𝑛𝑔)

我们输入数据 X 维度为[batch size, sequence length]的数据,比如我们为什么工作。batch size 就是 batch 的大小,这里只有一句话,所以 batch size 为 1,sequence length 是句子的长度,一共 7 个字,所以输入的数据维度是 [1, 7]。我们不能直接将这句话输入到编码器中,因为 Tranformer 不认识,我们需要先进行字嵌入,即得到图中的 。简单点说,就是文字->字向量的转换,这种转换是将文字转换为计算机认识的数学表示,用到的方法就是 Word2Vec,Word2Vec 的具体细节,对于初学者暂且不用了解,这个是可以直接使用的。得到维度是 [batch size, sequence length, embedding dimension],embedding dimension 的大小由 Word2Vec 算法决定,Tranformer 采用 512 长度的字向量。所以 的维度是 [1, 7, 512]。至此,输入的我们为什么工作,可以用一个矩阵来简化表示。

我们知道,文字的先后顺序,很重要。比如吃饭没、没吃饭、没饭吃、饭吃没、饭没吃,同样三个字,顺序颠倒,所表达的含义就不同了。文字的位置信息很重要,Tranformer 没有类似 RNN 的循环结构,没有捕捉顺序序列的能力。为了保留这种位置信息交给 Tranformer 学习,我们需要用到位置嵌入。加入位置信息的方式非常多,最简单的可以是直接将绝对坐标 0,1,2 编码。Tranformer 采用的是 sin-cos 规则,使用了 sin 和 cos 函数的线性变换来提供给模型位置信息:

上式中 pos 指的是句中字的位置,取值范围是 [0, 𝑚𝑎𝑥 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ),i 指的是字嵌入的维度, 取值范围是 [0, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛)。 就是 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 的大小。上面有 sin 和 cos 一组公式,也就是对应着 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 维度的一组奇数和偶数的序号的维度,从而产生不同的周期性变化。

# 导入依赖库
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math

def get_positional_encoding(max_seq_len, embed_dim):
    # 初始化一个positional encoding
    # embed_dim: 字嵌入的维度
    # max_seq_len: 最大的序列长度
    positional_encoding = np.array([
        [pos / np.power(10000, 2 * i / embed_dim) for i in range(embed_dim)]
        if pos != 0 else np.zeros(embed_dim) for pos in range(max_seq_len)])
    positional_encoding[1:, 0::2] = np.sin(positional_encoding[1:, 0::2])  # dim 2i 偶数
    positional_encoding[1:, 1::2] = np.cos(positional_encoding[1:, 1::2])  # dim 2i+1 奇数
    # 归一化, 用位置嵌入的每一行除以它的模长
    # denominator = np.sqrt(np.sum(position_enc**2, axis=1, keepdims=True))
    # position_enc = position_enc / (denominator + 1e-8)
    return positional_encoding
    
positional_encoding = get_positional_encoding(max_seq_len=100, embed_dim=16)
plt.figure(figsize=(10,10))
sns.heatmap(positional_encoding)
plt.title("Sinusoidal Function")
plt.xlabel("hidden dimension")
plt.ylabel("sequence length")
可以看到,位置嵌入在 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 (也是hidden dimension )维度上随着维度序号增大,周期变化会越来越慢,而产生一种包含位置信息的纹理。

就这样,产生独一的纹理位置信息,模型从而学到位置之间的依赖关系和自然语言的时序特性。

最后,将Xembedding  和 位置嵌入 相加,送给下一层。

2、自注意力层(𝑠𝑒𝑙𝑓 𝑎𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛 𝑚𝑒𝑐ℎ𝑎𝑛𝑖𝑠𝑚)

直接看下图笔记,讲解的非常详细。

多头的意义在于,\(QK^{T}\) 得到的矩阵就叫注意力矩阵,它可以表示每个字与其他字的相似程度。因为,向量的点积值越大,说明两个向量越接近。

我们的目的是,让每个字都含有当前这个句子中的所有字的信息,用注意力层,我们做到了。

需要注意的是,在上面 𝑠𝑒𝑙𝑓 𝑎𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛 的计算过程中,我们通常使用 𝑚𝑖𝑛𝑖 𝑏𝑎𝑡𝑐ℎ,也就是一次计算多句话,上文举例只用了一个句子。

每个句子的长度是不一样的,需要按照最长的句子的长度统一处理。对于短的句子,进行 Padding 操作,一般我们用 0 来进行填充。

3、残差链接和层归一化

加入了残差设计和层归一化操作,目的是为了防止梯度消失,加快收敛。

1) 残差设计

我们在上一步得到了经过注意力矩阵加权之后的 𝑉, 也就是 𝐴𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛(𝑄, 𝐾, 𝑉),我们对它进行一下转置,使其和 𝑋𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 的维度一致, 也就是 [𝑏𝑎𝑡𝑐ℎ 𝑠𝑖𝑧𝑒, 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛] ,然后把他们加起来做残差连接,直接进行元素相加,因为他们的维度一致:

$$
X_{\text {embedding }}+\text { Attention }(Q, K, V)
$$

在之后的运算里,每经过一个模块的运算,都要把运算之前的值和运算之后的值相加,从而得到残差连接,训练的时候可以使梯度直接走捷径反传到最初始层:

$$
X+\text { SubLayer }(X)
$$

2) 层归一化

作用是把神经网络中隐藏层归一为标准正态分布,也就是 𝑖.𝑖.𝑑 独立同分布, 以起到加快训练速度, 加速收敛的作用。

$$
\mu_{i}=\frac{1}{m} \sum_{i=1}^{m} x_{i j}
$$

上式中以矩阵的行 (𝑟𝑜𝑤) 为单位求均值:

$$
\sigma_{j}^{2}=\frac{1}{m} \sum_{i=1}^{m}\left(x_{i j}-\mu_{j}\right)^{2}
$$

上式中以矩阵的行 (𝑟𝑜𝑤) 为单位求方差:

$$
\operatorname{LayerNorm}(x)=\alpha \odot \frac{x_{i j}-\mu_{i}}{\sqrt{\sigma_{i}^{2}+\epsilon}}+\beta
$$

然后用每一行的每一个元素减去这行的均值,再除以这行的标准差,从而得到归 一化后的数值, \(\epsilon\) 是为了防止除 0 ;之后引入两个可训练参数 \(\alpha, \beta\) 来弥补归一化的过程中损失掉的信息,注意 \(\odot\) 表示 元素相乘而不是点积,我们一般初始化 \(\alpha\) 为全 1 ,而\(\beta\) 为全 0 。

代码层面非常简单,单头 attention 操作如下:

class ScaledDotProductAttention(nn.Module):
    ''' Scaled Dot-Product Attention '''

    def __init__(self, temperature, attn_dropout=0.1):
        super().__init__()
        self.temperature = temperature
        self.dropout = nn.Dropout(attn_dropout)

    def forward(self, q, k, v, mask=None):
        # self.temperature是论文中的d_k ** 0.5,防止梯度过大
        # QxK/sqrt(dk)
        attn = torch.matmul(q / self.temperature, k.transpose(2, 3))

        if mask is not None:
            # 屏蔽不想要的输出
            attn = attn.masked_fill(mask == 0, -1e9)
        # softmax+dropout
        attn = self.dropout(F.softmax(attn, dim=-1))
        # 概率分布xV
        output = torch.matmul(attn, v)

        return output, attn

Multi-Head Attention 实现在 ScaledDotProductAttention 基础上构建:

class MultiHeadAttention(nn.Module):
    ''' Multi-Head Attention module '''

    # n_head头的个数,默认是8
    # d_model编码向量长度,例如本文说的512
    # d_k, d_v的值一般会设置为 n_head * d_k=d_model,
    # 此时concat后正好和原始输入一样,当然不相同也可以,因为后面有fc层
    # 相当于将可学习矩阵分成独立的n_head份
    def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
        super().__init__()
        # 假设n_head=8,d_k=64
        self.n_head = n_head
        self.d_k = d_k
        self.d_v = d_v
        # d_model输入向量,n_head * d_k输出向量
        # 可学习W^Q,W^K,W^V矩阵参数初始化
        self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
        # 最后的输出维度变换操作
        self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
        # 单头自注意力
        self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
        self.dropout = nn.Dropout(dropout)
        # 层归一化
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

    def forward(self, q, k, v, mask=None):
        # 假设qkv输入是(b,100,512),100是训练每个样本最大单词个数
        # 一般qkv相等,即自注意力
        residual = q
        # 将输入x和可学习矩阵相乘,得到(b,100,512)输出
        # 其中512的含义其实是8x64,8个head,每个head的可学习矩阵为64维度
        # q的输出是(b,100,8,64),kv也是一样
        q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
        k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
        v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)

        # 变成(b,8,100,64),方便后面计算,也就是8个头单独计算
        q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)

        if mask is not None:
            mask = mask.unsqueeze(1)   # For head axis broadcasting.
        # 输出q是(b,8,100,64),维持不变,内部计算流程是:
        # q*k转置,除以d_k ** 0.5,输出维度是b,8,100,100即单词和单词直接的相似性
        # 对最后一个维度进行softmax操作得到b,8,100,100
        # 最后乘上V,得到b,8,100,64输出
        q, attn = self.attention(q, k, v, mask=mask)

        # b,100,8,64-->b,100,512
        q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
        q = self.dropout(self.fc(q))
        # 残差计算
        q += residual
        # 层归一化,在512维度计算均值和方差,进行层归一化
        q = self.layer_norm(q)

        return q, attn

4、前馈网络

这个层就没啥说的了,非常简单,直接看代码吧:

class PositionwiseFeedForward(nn.Module):
    ''' A two-feed-forward-layer module '''

    def __init__(self, d_in, d_hid, dropout=0.1):
        super().__init__()
        # 两个fc层,对最后的512维度进行变换
        self.w_1 = nn.Linear(d_in, d_hid) # position-wise
        self.w_2 = nn.Linear(d_hid, d_in) # position-wise
        self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        residual = x

        x = self.w_2(F.relu(self.w_1(x)))
        x = self.dropout(x)
        x += residual

        x = self.layer_norm(x)

        return x

最后,回顾下 𝑡𝑟𝑎𝑛𝑠𝑓𝑜𝑟𝑚𝑒𝑟 𝑒𝑛𝑐𝑜𝑑𝑒𝑟 的整体结构。

经过上文的梳理,我们已经基本了解了 𝑡𝑟𝑎𝑛𝑠𝑓𝑜𝑟𝑚𝑒𝑟 编码器的主要构成部分,我们下面用公式把一个 𝑡𝑟𝑎𝑛𝑠𝑓𝑜𝑟𝑚𝑒𝑟 𝑏𝑙𝑜𝑐𝑘 的计算过程整理一下:

1) 字向量与位置编码
$$X= EmbeddingLookup (X)+ PositionalEncoding$$

$$X \in \mathbb{R}^{\text {batch size } * \text { seq. len. * embed.dim. }}$$
2) 自注意力机制

$$
Q=\operatorname{Linear}(X)=X W_{Q}
$$

$$
K=\operatorname{Linear}(X)=X W_{K}
$$

$$
V=\operatorname{Linear}(X)=X W_{V}
$$

$$
X_{\text {attention }}=\text { SelfAttention }(Q, K, V)
$$


3) 残差连接与层归一化

$$
X_{\text {attention }}=X+X_{\text {attention }}
$$

$$
X_{\text {attention }}=\text { Layer Norm }\left(X_{\text {attention }}\right)
$$

4) 前向网络
其实就是两层线性映射并用激活函数激活,比如说 ReLU :
$$
X_{\text {hidden }}=\operatorname{Activate}\left(\text { Linear }\left(\text { Linear }\left(X_{\text {attention }}\right)\right)\right)
$$
5) 重复3)

$$
X_{\text {hidden }}=X_{\text {attention }}+X_{\text {hidden }}
$$

$$
X_{\text {hidden }}=\text { Layer } N \text { orm }\left(X_{\text {hidden }}\right)
$$

$$
X_{\text {hidden }} \in \mathbb{R}^{\text {batch size } * \text { seq. len. } * \text { embed. dim. }}
$$

转置卷积、微步卷积、空洞卷积

1、转置卷积 又可以称为 反卷积(数据从低维到高维)

转置卷积是一个将低维特征转换到高维特征。为什么叫做转置卷积呢?其实就是引入了转置的思想。

  • 假设我们现在有一个p维的向量Z,然后有个d维的向量X,p<d.
  • 这样就会出现 Z = W·X,其中W的维度为(p,d),叫做转换矩阵.
  • 现在,我们要从Z通过相似的方法来得到X,这样我们不难想到:X= W.T · X 其中W.T的维度是(d,p),但是这两个W并不是同一个值,而是具有转置的形式而已。

上面的例子是一维向量的情况,在卷积操作中,也可以借用这个思想,从低维到高维的转变可以在形式上看成是转置操作。

  • 比如我们现在对一个4 * 4的输入做3 * 3的卷积操作(m=3核的大小,stride=1,padding=0),得到一个2 * 2的特征映射
  • 如果我们想对这个2 * 2特征映射进行3 * 3卷积,并反过来得到4 * 4的输出,就可以用到转置卷积:

如上图所示,对2 * 2的特征映射先做(m-1) padding得到6 * 6的输入,然后对其进行3*3的卷积操作,从而得到4 * 4的特征映射。 同样,这个两个3 * 3的卷积参数不是一致的,都是可学习的。

2、微步卷积(步长不为1的转置卷积(反卷积))

微步卷积其实是一个转置卷积的一个特殊情况,就是卷积操作的stride ≠ 1。因为在现实中,为了大幅度降低特征维数,卷积的步长会大于1。同样,为了大幅度提高特征维度,我们也可以用通过卷积来实现,这种卷积stride < 1 ,所以叫做微步卷积。

  • 如果卷积操作stride>1,其对应的转置卷积步长为1/s :就是在输入特征之间插入s – 1个0,来使得步长变’小’。
  • 例如,我对一个5 * 5的输入做3 * 3的卷积操作(m=3, padding=0,但是stride=2),从而我得到的特征输出为2 * 2.
  • 现在对其进行微步卷积:

跟转置卷积一样,先对2 * 2的输入做(m-1)padding ,然后再在特征之间插入stride -1个0,从而得到一个7 * 7的特征输入,然后对其做3 * 3 的卷积操作,得到5 * 5的特征输出。

如何计算反卷积:

当输入的矩阵高宽为n,核大小为k,padding为p,stride为s

  • 当输入的矩阵高宽为 n ,核大小为 k ,padding为 p , stride为 s 。
  • 转置卷积作用后的尺寸变化: \(n^{1}=s n+k-2 p-s\) 。如果想让高宽成倍增加,那么 \(k=2 p+s\) 。
  • 卷积作用后的尺寸变化: \(n^{1}=\left\lfloor\frac{n-k+2 p+s}{s}\right\rfloor\) 。如果想让高宽成倍减少,那么 \(k=2 p+1\)。

1、当填充为0步长为1时

将输入填充 k − 1 。(k是 卷积核大小)
将核矩阵上下,左右翻转。
之后正常做填充为0(无填充),步幅为1的卷积。

2 当填充为 p 步幅为1时

将输入填充 k − p − 1 。
将核矩阵上下,左右翻转。
之后正常做填充为0,步幅为1的卷积。

3 当填充为 p pp 步幅为s ss时

在行和列之间插入s − 1 行和列。
将输入填充 k − p − 1。
将核矩阵上下,左右翻转。
之后正常做填充为0,步幅为1的卷积。

3、空洞卷积(膨胀卷积)

通常来说,对于一个卷积层,如果希望增加输出单元的感受野,一般由三个方式:

  1. 增加卷积核大小
  2. 增加层数
  3. 进行pooling操作

其中1和2都会增加参数量,而3会丢失特征信息。这样我们就可以引入‘空洞卷积’的概念,它不增加参数量,同时它也可以增加输出的感受野。
它主要是通过给卷积核插入空洞来增加其感受野大小,如果卷积核每两个元素之间插入d-1个空洞,那么卷积核的有效大小为:M = m + (m-1)*(d-1)

GAN系列之—Deep Convolutional GAN(DCGAN)

DCGAN 的判别器和生成器都使用了卷积神经网络(CNN)来替代GAN 中的多层感知机,同时为了使整个网络可微,拿掉了CNN 中的池化层,另外将全连接层以全局池化层替代以减轻计算量。

去卷积(反卷积,Deconvolution)

从上图中可以看到,生成器G 将一个100 维的噪音向量扩展成64 * 64 * 3 的矩阵输出,整个过程采用的是微步卷积的方式。作者在文中将其称为fractionally-strided convolutions,并特意强调不是deconvolutions。

去卷积(链接:反卷积)又包含转置卷积和微步卷积,两者的区别在于padding 的方式不同,看看下面这张图片就可以明白了:

3. 训练方法

DCGAN 的训练方法跟GAN 是一样的,分为以下三步:

(1)for k steps:训练D 让式子【logD(x) + log(1 – D(G(Z)) (G keeps still)】的值达到最大

(2)保持D 不变,训练G 使式子【logD(G(z))】的值达到最大

(3)重复step(1)和step(2)直到G 与D 达到纳什均衡

4. 相比于GAN 的改进

DCGAN 相比于GAN 或者是普通CNN 的改进包含以下几个方面:

(1)使用卷积和去卷积代替池化层

(2)在生成器和判别器中都添加了批量归一化操作

(3)去掉了全连接层,使用全局池化层替代

(4)生成器的输出层使用Tanh 激活函数,其他层使用RELU

(5)判别器的所有层都是用LeakyReLU 激活函数

5. 漫游隐空间

通过使用插值微调噪音输入z 的方式可以导致隐空间结构发生变化从而引导生成图像发生语义上的平滑过度,比如说从有窗户到没窗户,从有电视到没电视等等。

6. 语义遮罩

通过标注窗口,并判断激活神经元是否在窗口内的方式来找出影响窗户形成的神经元,将这些神经元的权重设置为0,那么就可以导致生成的图像中没有窗户。从下图可以看到,上面一行图片都是有窗户的,下面一行通过语义遮罩的方式拿掉了窗户,但是空缺的位置依然是平滑连续的,使整幅图像的语义没有发生太大的变化。

7. 矢量算法

在向量算法中有一个很经典的例子就是【vector(“King”) – vector(“Man”) + vector(“Woman”) = vector(“Queue”)】,作者将该思想引入到图像生成当中并得到了以下实验结果:【smiling woman – neutral woman + neutral man = smiling man】

BicycleGAN-图像一对多转换测试

2025年 9月
1234567
891011121314
15161718192021
22232425262728
2930  

CycleGAN、pix2pix、iGAN的主要贡献者最近在NIPS 2017上又推出了一篇文章Toward Multimodal Image-to-Image Translation(见https://junyanz.github.io/BicycleGAN/,https://arxiv.org/pdf/1711.11586.pdf),讨论如何从一张图像同时转换为多张风格不一成对的图像。

Pix2pix 和 CycleGAN 是非常的流行GAN,不仅在学术界有许多变体,同时也有许多基于此的应用。但是,它们都有一个缺点——图像的输出看起来几乎总是相同的。例如,如果我们要执行斑马到马的转换,被转换的同一马的照片将始终具有相同的外观和色调。这是由于GAN固有的特性,它学会过滤了噪声的随机性。

像pix2pix这样的图像转换(一对一)的方式是存在歧义的,因为不可能只对应一个输出。因此作者提出了一种一对多的输出,即将可能输出的图像是存在一定的分布特性的。

论文的主要方法如下图所示:

下图是 BicycleGAN 相关的模型和配置。图(a)是推理的配置,图像A与噪声相结合以生成图像B ^ ,可以将此看作是 cGAN 。在BicyleGAN中,形状为(256, 256, 3)的图像A是条件,而从潜在编码 z采样的噪声为大小为8的一维向量。图(b)是 pix2pix + 噪声 的训练配置。而图(c) 和 图(d) 的两个配置由 BicycleGAN 训练时使用:

简而言之,BicycleGAN 可以找到潜在编码z与目标图像B之间的关系,因此生成器可以在给定不同的z时学会生成不同的图像B ^ 。如上图所示,BicycleGAN 通过组合 cVAE-GAN 和 cLR-GAN 这两种模型来做到这一点。

cVAE-GAN
  VAE-GAN 的作者认为,L1 损失并不是衡量图像视觉质量的良好指标。例如,如果图像向右移动几个像素,则人眼看起来可能没有什么不同,但会导致较大的L1损失。因此使用 GAN 的鉴别器来学习目标函数,以判断伪造的图像是否真实,并使用 VAE 作为生成器,生成的图像更清晰。如果忽略上图(c)中的图像 A ,那就是 VAE-GAN ,由于以 A 为条件,其成为条件 cVAE-GAN 。训练步骤如下:

  • VAE 将真实图片 B编码为多元高斯分布的潜在编码,然后从它们中采样以创建噪声输入,此流程是标准的VAE工作流程;
  • 使用图像 A 作为条件及从潜矢量 z 采样的噪声用于生成伪图像B ^

训练中的数据流为 B − > z − > B ^ ( 图(c) 中的实线箭头),总的损失函数由三个损失组成:

对抗损失 \(L_{GAN}^{VAE}\)

L1​重建损失 \(L_{1}^{VAE}(G)\)

KL散度损失 \(L_{KL}(E)\)

cLR-GAN(Conditional Latent Regressor GAN)
  在 cVAE-GAN 中,对真实图像B进行编码,以提供潜在矢量的真实样本并从中进行采样。但是,cLR-GAN 的处理方式有所不同,其首先使用生成器从随机噪声中生成伪图像 B^,然后对伪图像 B^ 进行编码,最后计算其与输入随机噪声差异。
前向计算步骤如下:

首先,类似于 cGAN ,随机产生一些噪声,然后串联图像A以生成伪图像 B ^ ,之后,使用来自 VAE-GAN 的同一编码器将伪图像 B ^ 编码为潜矢量。
最后,从编码的潜矢量中采样 z ^ ,并用输入噪声 z 计算损失。数据流为 z −> B ^ −> z ^ ( 图(d) 中的实线箭头),有两个损失:

对抗损失 \(L_{GAN}\)

噪声 N(z) 与潜在编码之间的 L1损失 \(L_{1}^{latent}\)

通过组合这两个数据流,在输出和潜在空间之间得到了一个双映射循环。 BicycleGAN 中的 bi 来自双映射(双向单射),这是一个数学术语,简单来说其表示一对一映射,并且是可逆的。在这种情况下,BicycleGAN 将输出映射到潜在空间,并且类似地从潜在空间映射到输出。总损失如下:

最总的损失:

可以分为两块来理解,第一块就是cVAE-GAN的训练,我们分析的基础就是鞋子纹理风格生成为例。

鞋子纹理图片经过编码器得到编码后的latent z通过KL距离将其拉向我们事先定义好的分布N(z)上,将服从分布的z与鞋子草图A结合后送入生成器G中得到重构的鞋子纹理图。 此时为了衡量重构和真实的误差,这里用了L1损失和GAN的对抗思想实现,我们在后面损失函数分析部分再说。这样cVAE-GAN部分就可以训练了,cVAE GAN的重点还是在得到的embedding z

另一块就是cLR-GAN的训练,将鞋子草图A和分布N(z)结合经过生成器G得到鞋子纹理图, 再通过对生成的纹理图编码后得到的z去趋近分布N(z)来反向矫正生成图,达到一个变相的循环。

当这两部分训练的很好时,这个就是我们需要的BicycleGAN了,在检验训练效果时我们只需要,输入A加上N(z)就可以生成鞋子的纹理图了, 这个N(z)具体为什么怎么取将决定生成为纹理的风格了。

一些细节

  • 这里有一个小trike就是z和图片A的结合送入生成器G的结合方法,文中给出了两种方法:一种直接concat在input的channel上,一种Unet在压缩的时候,每次结果都加。 我们通过图解可以更好理解。

pytorch代码:https://github.com/junyanz/BicycleGAN