TensorFlow1.6.0+Keras 2.1.5+Python3.5+Yolov3训练自己的数据集!

文章目录

  • 前期准备
  • 一. Yolov3简要介绍
    • 1.1. Yolov3网络结构图
    • 1.2. 9种尺度的先验框
    • 1.3. 对象分类softmax改成logistic
    • 1.4. 边框预测
    • 1.5. 损失函数
    • 1.6. 利用官网权重进行检测
  • 二. 制作VOC2007格式数据集
    • 2.1. 数据集标注
    • 2.2. 修改参数文件yolov3.cfg
    • 2.3. 修改model_data下的文件
  • 三. 模型训练train.py
    • 3.1. 代码解读train.py
    • 3.2. 补充:冻结网络层
    • 3.3. 补充:目标检测之IoU计算方法
    • 3.4. 补充:冻结网络层
    • 3.5. 补充:1x1卷积操作与全连接
  • 四. 对图片进行检测
    • 4.1. 修改yolo.py代码
    • 4.2. 代码执行过程+结果
  • 五. 重要代码注释model.py
  • 六. Keras中自定义损失函数的方法
    • 6.1. 例子1:自定义loss
    • 6.2. 例子2:自定义loss
  • 参考文章

前期准备

  • 主要参考github代码 :https://github.com/qqwweee/keras-yolo3
  • 主要参考CSDN文章1:https://blog.csdn.net/Patrick_Lxc/article/details/80615433
  • 主要参考CSDN文章2:使用VOC数据集训练自己的YOLOv3模型(Keras/TensorFlow)
  • 主要参考简书中文章3:关于Yolov3的一些细节
  • 主要参考简书中文章4:Yolov3 深入理解
  • 主要参考简书中文章5:Yolov1深入理解
  • 主要参考简书中文章6:YOLOv2 / YOLO9000 深入理解
  • 主要参考文章3(赞赞!):keras-Yolov3 源码调试
  • 重点内容: 介绍如何制作数据集、修改代码、不加载预权重(可以选择) 从头跑自己的训练数据
  • 程序运行环境:Python 3.5.2; Keras 2.1.5; tensorflow 1.6.0!

一. Yolov3简要介绍

1.1. Yolov3网络结构图

  • YOLOV3 Homepage
  • <Github - Darknet>
  • Yolov3网络结构图参考作者 yolo系列之yolo v3【深度解析】,并且我再此基础了进行了一些细节性的补充,希望对您有用!
  • Yolov3输出了3个不同尺度的feature map, 有什么作用!
  • 低层的特征语义信息比较少,但是目标位置信息准确;高层的特征语义信息比较丰富,但是目标位置信息比较粗略。
  • 这个借鉴了FPN(feature pyramid networks),采用多尺度来对不同size的目标进行检测,越精细的grid cell就可以检测出越精细的物体。这样可以让网络同时学习到深层浅层的特征,通过叠加浅层特征图相邻特征到不同通道(而非空间位置),类似于Resnet中的identity mapping。这个方法把 26×26×51226×26×51226×26×512 的特征图叠加成 13×13×204813×13×204813×13×2048 的特征图,与原生的深层特征图相连接,使模型有了细粒度特征,增加对小目标的识别能力。
  • Yolov3采用(类似FPN)上采样(Upsample)和融合做法,融合了3个尺度(13×13、26×26和52×5213×13、26×26和52×5213×1326×2652×52),在多个尺度的融合特征图上分别独立做检测,最终对于小目标的检测效果提升明显。(有些算法采用多尺度特征融合的方式,但是一般是采用融合后的单一特征图做预测,比如Yolov2,FPN不一样的地方在于其预测是在不同特征层进行的!)
  • 假设以Pascal voc 为例:Yolov3设定的是每个网格单元预测 333 个box,所以每个box需要有 (x,y,w,h,confidence)(x, y, w, h, confidence)(x,y,w,h,confidence) 五个基本参数,然后还要有20个类别的概率。所以 3×(5+20)=753×(5 + 20) = 753×(5+20)=75,这个 757575 就是这么来的! (还记得yolo v1的输出张量吗? 7×7×307×7×307×7×30,其中 2×5+20=302×5+20 = 302×5+20=30 识别 202020 类物体,而且每个cell只能预测 222 个box,和Yolov3比起来就像老人机和iphoneX一样)

1.2. 9种尺度的先验框

  • 随着输出的特征图的数量和尺度的变化,先验框的尺寸也需要相应的调整。Yolov2已经开始采用K-means聚类得到先验框的尺寸,Yolov3延续了这种方法,为每种下采样尺度设定3种先验框,总共聚类出 999 种尺寸的先验框。在COCO数据集这 999 个先验框是:(10×13),(16×30),(33×23),(30×61),(62×45),(59×119),(116×90),(156×198),(373×326)(10×13),(16×30),(33×23),(30×61),(62×45),(59×119),(116×90),(156×198),(373×326)(10×13)(16×30)(33×23)(30×61)(62×45)(59×119)(116×90)(156×198)(373×326)
  • 分配上,在最小的 13×1313×1313×13 特征图上(有最大的感受野)应用较大的先验框 (116×90),(156×198),(373×326)(116×90),(156×198),(373×326)(116×90)(156×198)(373×326),适合检测较大的对象。中等的 26×2626×2626×26 特征图上(中等感受野)应用中等的先验框 (30×61),(62×45),(59×119)(30×61),(62×45),(59×119)(30×61)(62×45)(59×119),适合检测中等大小的对象。较大的 52×5252×5252×52 特征图上(较小的感受野)应用较小的先验框 (10×13),(16×30),(33×23)(10×13),(16×30),(33×23)(10×13)(16×30)(33×23),适合检测较小的对象。
  • 具体介绍可以参考作者:YOLOv3 深入理解

1.3. 对象分类softmax改成logistic

  • 预测对象类别时不使用softmax,改成使用logistic的输出进行预测。这样能够支持多标签对象(比如一个人有Woman 和 Person两个标签)。
  • 不考虑神经网络结构细节的话,总的来说,对于一个输入图像,Yolov3将其映射到 333 个尺度的输出张量,代表图像各个位置存在各种对象的概率。
  • 我们看一下Yolov3共进行了多少个预测。对于一个 416×416416×416416×416 的输入图像,在每个尺度的特征图的每个网格设置 333 个先验框,总共有 13×13×3+26×26×3+52×52×3=1064713×13×3 + 26×26×3 + 52×52×3 = 1064713×13×3+26×26×3+52×52×3=10647 个预测。每一个预测是一个 (4+1+80)=85(4+1+80)=85(4+1+80)=85 维向量,这个 858585 维向量包含边框坐标(444 个数值),边框置信度(111 个数值),对象类别的概率(对于COCO数据集,有 808080 种对象)。
  • 对比一下,Yolov2采用 13×13×5=84513×13×5 = 84513×13×5=845 个预测,Yolov3的尝试预测边框数量增加了 101010 多倍,而且是在不同分辨率上进行,所以mAP以及对小物体的检测效果有一定的提升。
  • 图像参考: 如何在PyTorch中从零开始实现YOLO(v3)对象检测器

1.4. 边框预测

  • 其中:bx,by,bw,bhb_x, b_y, b_w, b_hbx,by,bw,bh 是我们预测的 x,yx,yxy 中心坐标,宽度和高度;tx,ty,tw,tht_x, t_y, t_w, t_htx,ty,tw,th 是网络输出的内容。cx,cyc_x, c_ycx,cy 是网格的左上角坐标,pwp_wpwphp_hph 是盒子的anchor box尺寸

1.5. 损失函数

  • 这里以Yolov1为例子:损失就是网络实际输出值与样本标签值之间的偏差。举个例子,比如下图中自行车的中心点位于4行3列网格中,所以输出tensor中4行3列位置的30维向量如下图所示。
  • YOLO给出的损失函数如下

公式中

  • a) 1iobj1_i^{obj}1iobj 意思是网格 iii 中存在对象。
  • b) 1ijobj1_{ij}^{obj}1ijobj 意思是网格 iii 的第 jjj 个bounding box中存在对象。
  • c) 1ijnoobj1_{ij}^{noobj}1ijnoobj 意思是网格 iii 的第 jjj 个bounding box中不存在对象。

总的来说,就是用网络输出与样本标签的各项内容的误差平方和作为一个样本的整体误差。损失函数中的几个项是与输出的30维向量中的内容相对应的。

① 对象分类的误差: 公式第5行,注意 1iobj1_i^{obj}1iobj 意味着存在对象的网格才计入误差。
② bounding box的位置误差:

  • a) 公式第 111 行和第 222 行。都带有 1ijobj1_{ij}^{obj}1ijobj 意味着只有"负责"(IOU比较大)预测的那个bounding box的数据才会计入误差。
  • b) 第 222 行宽度和高度先取了平方根,因为如果直接取差值的话,大的对象对差值的敏感度较低,小的对象对差值的敏感度较高,所以取平方根可以降低这种敏感度的差异,使得较大的对象和较小的对象在尺寸误差上有相似的权重。
  • c) 乘以 λcoord\lambda_{coord}λcoord 调节bounding box位置误差的权重(相对分类误差和置信度误差)。Yolo 中 λcoord=5\lambda_{coord} = 5λcoord=5,即调高位置误差的权重。

③ bounding box的置信度误差 公式第3行和第4行。

  • a) 第 333 行是存在对象的bounding box的置信度误差。带有 1ijobj1_{ij}^{obj}1ijobj 意味着只有"负责"(IOU比较大)预测的那个bounding box的置信度才会计入误差。
  • b) 第 444 行是不存在对象的bounding box的置信度误差。因为不存在对象的bounding box应该老老实实的说"我这里没有对象",也就是输出尽量低的置信度。如果它不恰当的输出较高的置信度,会与真正"负责"该对象预测的那个bounding box产生混淆。其实就像对象分类一样,正确的对象概率最好是 111,所有其它对象的概率最好是 000
  • c) 第 444 行会乘以 λnoobj\lambda_{noobj}λnoobj 调节不存在对象的bounding box的置信度的权重(相对其它误差)。Yolo 中设置 λnoobj=0.5\lambda_{noobj} = 0.5λnoobj=0.5,即调低不存在对象的bounding box的置信度误差的权重。

1.6. 利用官网权重进行检测

  • yolov3.weightsyolov3-tiny.weights官网权重文件下载
wget https://pjreddie.com/media/files/yolov3.weights
wget https://pjreddie.com/media/files/yolov3-tiny.weights
  • 转换权重文件, 执行代码中的下面命令
python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5
  • 会在model_data文件夹中生成一个yolo.h5文件,注意这个权重文件要留着,后面可能会用上;
  • 打开yolo_video.py,其用法如下:
usage: yolo_video.py [-h] [--model MODEL] [--anchors ANCHORS][--classes CLASSES] [--gpu_num GPU_NUM] [--image][--input] [--output]positional arguments:--input        Video input path--output       Video output pathoptional arguments:-h, --help         show this help message and exit--model MODEL      path to model weight file, default model_data/yolo.h5--anchors ANCHORS  path to anchor definitions, defaultmodel_data/yolo_anchors.txt--classes CLASSES  path to class definitions, defaultmodel_data/coco_classes.txt--gpu_num GPU_NUM  Number of GPU to use, default 1--image            Image detection mode, will ignore all positional arguments
  • 这里以识别一个张图片为例子
python yolo_video.py --image
# 会提示输入图片的路径;输入要探测的图片路径就可以啦。

二. 制作VOC2007格式数据集

2.1. 数据集标注

  • 首先这里简要说明,自己简单的标注了40张图片,具体的方法参考我之前的博客:DL笔记:PascalVOC 数据集介绍+数据集标注工具!最终原图片和标记产生的xml文件如下,只有一个类别;使用的工具LabelImg:链接:提取码:85y8
  • VOCdevkit所有文件夹里面的东西删除,保留所有文件夹的名字,如下图(左):
  • 把刚才的40张图片都复制到JPEGImages里面,如上图(右):

  • 把刚才标注生成的.xml文件放到Annotations目录下面;

  • 接下来把.xml文件解析成.txt文件,在VOC2007目录下建个文件test.py,内容如下:

import os
import randomtrainval_percent = 0.9               # 整个数据集中,作为训练验证集的比例数
train_percent = 0.9                  # 训练验证集中用于训练的数据的比例,根据实际情况修改
xmlfilepath = 'Annotations'
txtsavepath = 'ImageSets\Main'
total_xml = os.listdir(xmlfilepath)  # 得到整个注释文件的文件名num = len(total_xml)                 # 文件个数
list = range(num)                    # 0到num
tv = int(num * trainval_percent)     # 训练验证文件的个数
tr = int(tv * train_percent)         # 训练文件的个数
trainval = random.sample(list, tv)   # 从0到num中随机选取tv个
train = random.sample(trainval, tr)  # 从trainval中随机选取tr个ftrainval = open('ImageSets/Main/trainval.txt', 'w')    #写模式打开trainval.txt,下同
ftest = open('ImageSets/Main/test.txt', 'w')
ftrain = open('ImageSets/Main/train.txt', 'w')
fval = open('ImageSets/Main/val.txt', 'w')for i in list:                       # 遍历文件name = total_xml[i][:-4] + '\n'  # 取不带格式的文件名if i in trainval:                # 分别写入不同的文件下ftrainval.write(name)if i in train:ftrain.write(name)else:fval.write(name)else:ftest.write(name)ftrainval.close()                    # 关闭文件
ftrain.close()
fval.close()
ftest.close()
  • 执行上述代码完成,会生成如下的4个文件;
  • VOC2007数据集制作完成,但是yolov3并不直接用这个数据集; 需要运行voc_annotation.py 生成yolov3所需的train.txt,val.txt,test.txt;根据自己的数据,相应的调整代码;

  • 可以参考博客:darknet-yolov3训练自己的数据集(超详细)

"""
把xml文件转换为txt
1.得到train.txt 、val.txt 、test.txt1.1 打开 VOCdevkit/VOC2012/ImageSets/Main/train.txt  读取为列表  ["2008_000008","2008_000019", ...]1.2 将path/to/VOCdevkit/VOC2012/JPEGImages/2008_000008.jpg 写入 2012_train.txt 2012_val.txt在这三个文件夹中分别得到对应数据集的绝对路径
2.得到标注文件2.1 打开图片对应的原始标注文件 VOCdevkit/VOC2012/Annotations/2008_000008.xml2.2 转换为yolo 的数据格式2.3 写入VOCdevkit/VOC2012/labels/2008_000008.xml中
"""import xml.etree.ElementTree as ET
from os import getcwdsets=[('2007', 'train'), ('2007', 'val'), ('2007', 'test')]
classes = ["box"]         # 自己要训练的类名字;def convert_annotation(year, image_id, list_file):in_file = open('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id))tree=ET.parse(in_file)root = tree.getroot()for obj in root.iter('object'):difficult = obj.find('difficult').textcls = obj.find('name').textif cls not in classes or int(difficult)==1:continuecls_id = classes.index(cls)xmlbox = obj.find('bndbox')b = (int(xmlbox.find('xmin').text), int(xmlbox.find('ymin').text), int(xmlbox.find('xmax').text), int(xmlbox.find('ymax').text))list_file.write(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id))
wd = getcwd()for year, image_set in sets:image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()list_file = open('%s_%s.txt'%(year, image_set), 'w')for image_id in image_ids:list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.jpg'%(wd, year, image_id))convert_annotation(year, image_id, list_file)list_file.write('\n')list_file.close()
  • 运行之后,会在主目录下多生成三个.txt文件,如图所示:

2.2. 修改参数文件yolov3.cfg

  • 注意: 如果我们从头开始训练自己网络的话,这个文件不需要修改;这个文件的目的就是用来转换官网下载的.weights文件用,详见readme!
1. Make sure you have run `python convert.py -w yolov3.cfg yolov3.weights model_data/yolo_weights.h5`
2. The file model_data/yolo_weights.h5 is used to load pretrained weights.
  • 直接打开yolo3.cfg,搜索yolo,总共会搜出三个含有yolo的地方;其中如下需要修改计算的:
#  记得3处都要修改的!!!!!!
1. filters:3 * (5+len(classes)) = 18;
2. classes: len(classes) = 1, 这个是你要训练的类的数量
3. random =1 : 原来是1,显存小改为0,关闭多尺度训练,有博客说打开的话会精确一些

2.3. 修改model_data下的文件

  • 修改model_data下的文件,放入你的类别,coco,voc这两个文件都需要修改。修改如下:

三. 模型训练train.py

  • yolov3代码原作者在train.py做了两件事情:
  • 会加载预先对coco数据集已经训练完成的yolov3权重文件
  • 冻结了开始到最后倒数第N层(源代码为N=-2
  • 但是如果你想训练的东西,coco数据集里的voc数据集里没有,所以建议从头开始训练,对train.py做了一下修改,直接复制替换原文件就可以了,细节大家自己看吧,直接运行,loss达到10几的时候效果就可以了

3.1. 代码解读train.py

  • train.py,这里可以直接替换原来的文件!
"""
"""
Retrain the YOLO model for your own dataset.
@devinzhang
在train.py中主要就是构建yolov3的训练模型,这里作者使用自定义loss的方式进行模型训练并没有,在loss时输入y_true, y_pred
YOLO v3采用(类似FPN)上采样(Upsample)和融合做法,融合了3个尺度(13*1326*2652*52)
多个尺度的融合特征图上分别独立做检测,最终对于小目标的检测效果提升明显"""
import numpy as np
import tensorflow as tf
import keras.backend as K
from keras.layers import Input, Lambda
from keras.models import Model
from keras.optimizers import Adamfrom keras.callbacks import TensorBoard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping
from yolo3.model import preprocess_true_boxes, yolo_body, tiny_yolo_body, yolo_loss
from yolo3.utils import get_random_datadef _main():############################# 设置GPU资源 ############################################################################import osos.environ["CUDA_VISIBLE_DEVICES"] = "0"                            # 几块GPU训练 “0,1”from keras import backend as K# 当allow_growth设置为True时,分配器将不会指定所有的GPU内存,而是根据需求增长config = tf.ConfigProto()config.gpu_options.allow_growth = True                              # 动态申请显存# config.gpu_options.per_process_gpu_memory_fraction = 0.4          # 占用40%显存sess = tf.Session(config=config)K.set_session(sess)                                                 # 程序运行完毕,关闭Session####################################################################################################################annotation_path = '2007_train.txt'                                  # 训练数据log_dir = 'logs/000/'                                               # 保存的日志文件夹classes_path = 'model_data/voc_classes.txt'                         # 类别信息anchors_path = 'model_data/yolo_anchors.txt'                        # anchorsclass_names = get_classes(classes_path)                             # 类别列表num_classes = len(class_names)                                      # 类别数anchors = get_anchors(anchors_path)                                 # anchors列表input_shape = (416,416)                                             # 32的倍数,输入图像pretrained_path = 'logs/000/ep200-loss20.860-val_loss24.638.h5'     # 加载保存好的模型路径####################################################################################################################is_tiny_version = len(anchors)==6                                   # 默认设定,yolo-tinyif is_tiny_version:model = create_tiny_model(input_shape, anchors, num_classes, freeze_body=2, weights_path=pretrained_path)else:# 创建需要训练的模型; 可以设置预训练的路径看下面的函数# freeze_body:冻结模式,1或2。其中,1是冻结DarkNet53网络中的层,2是只保留最后3个1x1的卷积层,其余层全部冻结;model = create_model(input_shape, anchors, num_classes, freeze_body=2, weights_path=pretrained_path)####################################################################################################################logging = TensorBoard(log_dir=log_dir)checkpoint = ModelCheckpoint(log_dir + "ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5",monitor='val_loss', save_weights_only=True,save_best_only=True, period=100)       # 只存储weights# reduce_lr:当评价指标不在提升时,减少学习率,每次减少10%,当验证损失值,持续3次未减少时,则终止训练。reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)# early_stopping:当验证集损失值,连续增加小于0时,持续10个epoch,则终止训练。early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)val_split = 0.1                                                     # 训练和验证的比例with open(annotation_path) as f:lines = f.readlines()np.random.seed(47)np.random.shuffle(lines)np.random.seed(None)num_val = int(len(lines)*val_split)     # 验证集数量num_train = len(lines) - num_val        # 训练集数量# Train with frozen layers first, to get a stable loss.# Adjust num epochs to your dataset. This step is enough to obtain a not bad model."""把目标当成一个输入,构成多输入模型,把loss写成一个层,作为最后的输出,搭建模型的时候,就只需要将模型的output定义为loss,而compile的时候,直接将loss设置为y_pred(因为模型的输出就是loss,所以y_pred就是loss),无视y_true,训练的时候,y_true随便扔一个符合形状的数组进去就行了。"""##################################################################################################################### 第1阶段,冻结部分网络,只训练底层权重:# 优化器使用常见的Adam;if True:model.compile(optimizer=Adam(lr=1e-3),loss={'yolo_loss': lambda y_true, y_pred: y_pred})  # 使用定制的 yolo_loss Lambda层损失函数batch_size = 8             # batch尺寸print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))# 在训练中,模型调用fit_generator方法,按批次创建数据,输入模型,进行训练。其中,数据生成器wrapper是data_generator_wrapper,# 用于验证数据格式,最终调用data_generatormodel.fit_generator(data_generator_wrap(lines[:num_train], batch_size, input_shape, anchors, num_classes),steps_per_epoch=max(1, num_train // batch_size),validation_data=data_generator_wrap(lines[num_train:], batch_size, input_shape, anchors, num_classes),validation_steps=max(1, num_val // batch_size),epochs=0,initial_epoch=0,callbacks=[logging, checkpoint])# 存储最终的权重,再训练过程中,通过回调存储model.save_weights(log_dir + 'trained_weights_stage_1.h5')##################################################################################################################### Unfreeze and continue training, to fine-tune.# Train longer if the result is not good.# 第2阶段,使用第1阶段已训练完成的网络权重,继续训练:# 损失函数,直接使用模型的输出y_pred,忽略真值y_true;# 将全部的权重都设置为可训练,而在第1阶段中,则是冻结部分权重;# 优化器,仍是Adam,只是学习率有所下降,从1e-3 减少至1e-4;# 损失函数,仍是只使用y_pred,忽略y_true。if True:  # 全部训练for i in range(len(model.layers)):model.layers[i].trainable = Truelearning_rate = 1e-4model.compile(optimizer=Adam(lr=learning_rate),loss={'yolo_loss': lambda y_true, y_pred: y_pred})    # recompile to apply the changeprint('Unfreeze all of the layers,','lr: ', learning_rate)batch_size = 8                 # note that more GPU memory is required after unfreezing the bodyprint('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))model.fit_generator(data_generator_wrap(lines[:num_train], batch_size, input_shape, anchors, num_classes),steps_per_epoch=max(1, num_train//batch_size),validation_data=data_generator_wrap(lines[num_train:], batch_size, input_shape, anchors, num_classes),validation_steps=max(1, num_val//batch_size),epochs=500,initial_epoch=0,callbacks=[logging, checkpoint, reduce_lr, early_stopping])# 至此,在第2阶段训练完成之后,输出的网络权重,就是最终的模型权重。model.save_weights(log_dir + 'trained_weights_final.h5')####################################################################################################################def get_classes(classes_path):# 输入类别文件,读取文件中所有的类别,生成listwith open(classes_path) as f:class_names = f.readlines()class_names = [c.strip() for c in class_names]return class_namesdef get_anchors(anchors_path):# 获取所有的anchors的长和宽with open(anchors_path) as f:anchors = f.readline()anchors = [float(x) for x in anchors.split(',')]return np.array(anchors).reshape(-1, 2)def create_model(input_shape, anchors, num_classes, load_pretrained=False, freeze_body=False, weights_path='model_data/yolo_weights.h5'):"""input_shape:图片尺寸;anchors:9个anchor box;num_classes:类别数;freeze_body:冻结模式,1是冻结DarkNet53的层,2是冻结全部,只保留最后3层;weights_path:预训练模型的权重。下面这个可以用来加载预训练模型: 'model_data/yolo_weights.h5'"""'''create the training model'''K.clear_session()                     # 清除sessionh, w = input_shape                    # 尺寸image_input = Input(shape=(w, h, 3))  # 图片输入格式num_anchors = len(anchors)            # anchor数量##################################################################################################################### YOLO的三种尺度,每个尺度的anchor数,类别数+边框4个+置信度1# 真值y_true,真值即Ground Truth, 通过循环,创建3个Input层的列表,作为y_true。y_true的张量结构,如下:# y_true = [shape = (?, 13, 13, 3, 25),#           shape = (?, 26, 26, 3, 25),#           shape = (?, 52, 52, 3, 25)]y_true = [Input(shape=(h//{0:32, 1:16, 2:8}[l], w//{0:32, 1:16, 2:8}[l], num_anchors//3, num_classes+5)) for l in range(3)]# 其中,在真值y_true中,第1位是输入的样本数,第2~3位是特征图的尺寸,如13x13,第4位是每个图中的anchor数,# 第5位是:类别(n)+4个框值(xywh)+框的置信度(是否含有物体)。model_body = yolo_body(image_input, num_anchors//3, num_classes)                # model# 其中:model_body.output = [shape=(?, 13, 13, 75), shape=(?, 26, 26, 75), shape=(?, 52, 52, 75)]# 其中:model_body.input  = Tensor("input_1:0", shape=(?, 416, 416, 3), dtype=float32)print('Create YOLOv3 model with {} anchors and {} classes.'.format(num_anchors, num_classes))####################################################################################################################if load_pretrained:                                                             # 加载预训练模型model_body.load_weights(weights_path, by_name=True)                         # 加载参数,跳过错误 最后一个参数删除了, skip_mismatch=Trueprint('Load weights {}.'.format(weights_path))# 选择冻结模式:模式1是冻结185层, 模式2是保留最底部3层,其余全部冻结。整个模型共有252层;将所冻结的层,设置为不可训练,trainable=False;# 其中,185层是DarkNet53网络的层数,而最底部3层是3个1x1的卷积层,用于预测最终结果。185层是DarkNet53网络的最后一个残差单元,# 其输入和输出如下:input: [(None, 13, 13, 1024), (None, 13, 13, 1024)]; output: (None, 13, 13, 1024)# 最底部3个1x1的卷积层,将3个特征矩阵转换为3个预测矩阵,其格式如下:# 1: (None, 13, 13, 1024) -> (None, 13, 13, 18)# 2: (None, 26, 26,  512) -> (None, 26, 26, 18)# 3: (None, 52, 52,  256) -> (None, 52, 52, 18)if freeze_body:# Freeze darknet53 body or freeze all but 3 output layers.num = (185, len(model_body.layers) - 3)[freeze_body - 1]for i in range(num):model_body.layers[i].trainable = False                              # 将其他层的训练关闭print('Freeze the first {} layers of total {} layers.'.format(num, len(model_body.layers)))# 构建模型的损失层model_loss; Lambda是Keras的自定义层, 输入为model_body.output和y_true,输出output_shape是(1,),即一个损失值;# 自定义Lambda层的名字name为yolo_loss;ignore_thresh用于在物体置信度损失中过滤IoU较小的框;model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',arguments={'anchors': anchors, 'num_classes': num_classes,'ignore_thresh': 0.5})(model_body.output + y_true)# y_true = [shape = (?, 13, 13, 3, 25),#           shape = (?, 26, 26, 3, 25),#           shape = (?, 52, 52, 3, 25)]# 构建完整的算法模型,步骤如下:# 1. 模型的输入层:model_body的输入和真值y_true;# 2. 模型的输出层:自定义的model_loss层,其输出是一个损失值(None,1);# 3. 保存模型的网络图plot_model和打印网络结构model.summary;model = Model(inputs=[model_body.input] + y_true, outputs=model_loss)# 其中:model_body.input是任意(?)个(416,416,3)的图片# model.summary()                         # 打印网络结构return modeldef create_tiny_model(input_shape, anchors, num_classes, load_pretrained=True, freeze_body=2, weights_path='model_data/tiny_yolo_weights.h5'):'''create the training model, for Tiny YOLOv3'''K.clear_session() # get a new sessionimage_input = Input(shape=(None, None, 3))h, w = input_shapenum_anchors = len(anchors)y_true = [Input(shape=(h//{0:32, 1:16}[l], w//{0:32, 1:16}[l], num_anchors//2, num_classes+5)) for l in range(2)]model_body = tiny_yolo_body(image_input, num_anchors//2, num_classes)print('Create Tiny YOLOv3 model with {} anchors and {} classes.'.format(num_anchors, num_classes))if load_pretrained:model_body.load_weights(weights_path, by_name=True, skip_mismatch=True)print('Load weights {}.'.format(weights_path))if freeze_body in [1, 2]:# Freeze the darknet body or freeze all but 2 output layers.num = (20, len(model_body.layers)-2)[freeze_body-1]for i in range(num): model_body.layers[i].trainable = Falseprint('Freeze the first {} layers of total {} layers.'.format(num, len(model_body.layers)))model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',arguments={'anchors': anchors, 'num_classes': num_classes,'ignore_thresh': 0.7})(model_body.output + y_true)model = Model(inputs=[model_body.input] + y_true, outputs=model_loss)  # 模型,inputs和outputsreturn modeldef data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes):'''data generator for fit_generatorannotation_lines: 所有的图片名称batch_size:每批图片的大小input_shape: 图片的输入尺寸anchors: 大小num_classes: 类别数'''n = len(annotation_lines)i = 0while True:image_data = []box_data = []for b in range(batch_size):if i==0:# 随机排列图片顺序np.random.shuffle(annotation_lines)# image_data: (16, 416, 416, 3)# box_data: (16, 20, 5) # 每个图片最多含有20个框image, box = get_random_data(annotation_lines[i], input_shape, random=True)#获取真实的数据根据输入的尺寸对原始数据进行缩放处理得到input_shape大小的数据图片,# 随机进行图片的翻转,标记数据数据也根据比例改变image_data.append(image)box_data.append(box)i = (i + 1) % nimage_data = np.array(image_data)box_data = np.array(box_data)# y_true是3个预测特征的列表y_true = preprocess_true_boxes(box_data, input_shape, anchors, num_classes)  # 真值# y_true的第0和1位是中心点xy,范围是(0~13/26/52),第2和3位是宽高wh,范围是0~1,# 第4位是置信度1或0,第5~n位是类别为1其余为0。# [(16, 13, 13, 3, 6), (16, 26, 26, 3, 6), (16, 52, 52, 3, 6)]yield [image_data] + y_true, np.zeros(batch_size)def data_generator_wrap(annotation_lines, batch_size, input_shape, anchors, num_classes):"""用于条件检查"""n = len(annotation_lines)  # 标注图片的行数if n==0 or batch_size<=0:return Nonereturn data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes)if __name__ == '__main__':_main()
  • 训练过程截图:

3.2. 补充:冻结网络层

  • 在微调中,需要确定冻结的层数和可训练的层数,主要取决于,数据集相似度和新数据集的大小。原则上,相似度越高,则固定的层数越多;新数据集越大,不考虑训练时间的成本,则可训练更多的层数。然后可能也要考虑数据集本身的类别间差异度,但上面说的规则基本上还是成立的。

  • 例如,在图片分类的网络中,底层一般是颜色、轮廓、纹理等基础结构,显然大部分问题都由这些相同的基础结构组成,所以可以冻结这些层。层数越高,所具有泛化性越高,例如这些层会包含对鞋子、裙子和眼睛等,具体语义信息,比较敏感的神经元。 所以,对于新的数据集,就需要训练这些较高的层。同时,比如一个高层神经元对车的轮胎较为敏感,不等于输入其它图像,就无法激活,因而,普通问题甚至可以只训练最后全连接层。

  • 在Keras中,通过设置每层的trainable参数,即可控制是否冻结该层,如:

model_body.layers[i].trainable = False

3.3. 补充:目标检测之IoU计算方法

  • 首先下面的内容摘自作者黑暗星球 目标检测之 IoU,表示感谢!
  • IoU 的全称为交并比(Intersection over Union),通过这个名称我们大概可以猜到 IoU 的计算方法。IoU 计算的是 “预测的边框” 和 “真实的边框” 的交集和并集的比值。
  • 开始计算之前,我们首先进行分析下交集和并集到底应该怎么计算:我们首先需要计算交集,然后并集通过两个边框的面积的和减去交集部分即为并集,因此 IoU 的计算的难点在于交集的计算。

  • 为了计算交集,你脑子里首先想到的方法应该是:考虑两个边框的相对位置,然后按照相对位置(左上,左下,右上,右下,包含,互不相交)分情况讨论,来计算交集。

  • 上图就是你的直觉,这样想没有错。但计算一个交集,就要分多种情况讨论,要是程序真的按照这逻辑编写就太搞笑了。因此对这个问题进行进一步地研究显得十分有必要。
  • 让我们重新思考一下两个框交集的计算。两个框交集的计算的实质是两个集合交集的计算,因此我们可以将两个框的交集的计算简化为:
  • 通过简化,我们可以清晰地看到,交集计算的关键是交集上下界点(图中蓝点)的计算。我们假设集合 A\mathrm AA[x1,x2]\left[ x_{1}, x_{2}\right][x1,x2],集合 B\mathrm BB[y1,y2]\left[ y_{1}, y_{2}\right][y1,y2],然后我们来求 AB\mathrm A \mathrm BAB 交集的上下界限。

  • 交集计算的逻辑如下:

  • 1. 交集下界 z1:max⁡(x1,y1)\mathrm z_{1}: \max \left(x_{1}, y_{1}\right)z1:max(x1,y1)

  • 2. 交集上界 z2:min⁡(x2,y2)\mathrm z_{2}: \min \left(x_{2}, y_{2}\right)z2:min(x2,y2)

  • 3. 如果 z2−z1\mathrm z_{2}-\mathrm z_{1}z2z1 小于 000,则说明集合 A\mathrm AA 和集合 B\mathrm BB 没有交集。

  • 下面使用Python来实现两个一维集合的 IoU 的计算:

def iou(set_a, set_b):''' 一维 iou 的计算  '''x1, x2 = set_a       # 元素为:(left, right)y1, y2 = set_b      # 元素为:(left, right)low =  max(x1, y1)high = min(x2, y2)# intersection 交集计算if high - low < 0:inter = 0else:inter = high - low# union 并集计算union = (x2 - x1) + (y2 - y1) - inter# iou  inter / unioniou = inter / unionreturn iou
  • 上面,我们计算了两个一维集合的 IOU,将上面的程序进行扩展,即可得到两个框IOU计算的程序。
def iou(box1, box2):'''两个框(二维)的 iou 计算; 注意:边框以左上为原点box:[top, left, bottom, right] 左上角坐标,和右下角坐标!'''# box1: [box1[0], box1[1], box1[2], box1[3]]# box2: [box2[0], box2[1], box2[2], box2[3]]# 第一步:水平方向上面:box1水平方向区间::(box1[0], box1[2]) box2水平区间:(box2[0], box2[2])max1_left = max(box1[0], box2[0])min1_right = min(box1[2], box2[2])in_h = min1_right - max1_left# 第二步:垂直方向上面:box1垂直方向区间::(box1[1], box1[3]) box2垂直方向区间:(box2[1], box2[3])max2_left = max(box1[1], box2[1])min2_right = min(box1[3], box2[3])in_w = min2_right - max2_leftif in_h < 0 or in_w < 0:inter = 0 else:inter = in_h * in_wunion = (box1[2] - box1[0]) * (box1[3] - box1[1]) + (box2[2] - box2[0]) * (box2[3] - box2[1]) - inter# 计算IOU的值!iou = inter / unionreturn iou

3.4. 补充:冻结网络层

  • 在微调中,需要确定冻结的层数和可训练的层数,主要取决于,数据集相似度和新数据集的大小。原则上,相似度越高,则固定的层数越多;新数据集越大,不考虑训练时间的成本,则可训练更多的层数。然后可能也要考虑数据集本身的类别间差异度,但上面说的规则基本上还是成立的。

  • 例如,在图片分类的网络中,底层一般是颜色、轮廓、纹理等基础结构,显然大部分问题都由这些相同的基础结构组成,所以可以冻结这些层。 层数越高,所具有泛化性越高,例如这些层会包含对鞋子、裙子和眼睛等,具体语义信息,比较敏感的神经元。所以,对于新的数据集,就需要训练这些较高的层。同时,比如一个高层神经元对车的轮胎较为敏感,不等于输入其它图像,就无法激活,因而,普通问题甚至可以只训练最后全连接层。

  • 在Keras中,通过设置每层的trainable参数,即可控制是否冻结该层,如:model_body.layers[i].trainable = False

3.5. 补充:1x1卷积操作与全连接

  • 1×11×11×1 的卷积层和全连接层都可以作为最后一层的预测输出,两者之间略有不同。

第1点:1×11×11×1 的卷积层,比全连接层,更为灵活;

  • 1×11×11×1 的卷积层,可以不考虑输入的通道数,输出固定通道数的特征矩阵;
  • 全连接层(Dense),输入和输出都是固定的,在设计网络时,固定就不能修改;

第2点:例如:输入(13,13,1024)(13,13,1024)(13,13,1024),输出为(13,13,18)(13,13,18)(13,13,18),则两种操作:

  • 1×11×11×1 的卷积层,参数较少,只需与输出通道匹配的参数,如 13×13×1×1×1813×13×1×1×1813×13×1×1×18 个参数;
  • 全连接层,参数较多,需要与输入和输出都匹配的参数,如 13×13×1028×1813×13×1028×1813×13×1028×18个参数;

最后:1×11×11×1 卷积操作的优点!

  • 这个就比较好理解了,1×11×11×1 的卷积核虽小,但也是卷积核,加 111 层卷积,网络深度自然会增加。我们知道,卷积后生成图片的尺寸受卷积核的大小和跨度影响,但如果卷积核是 1×11×11×1 ,跨度也是 111,那么生成后的图像大小就并没有变化。但通常一个卷积过程包括一个激活函数,比如 Sigmoid 和 Relu。所以,在输入不发生尺寸的变化下,却引入了更多的非线性,这将增强神经网络的表达能力。

四. 对图片进行检测

4.1. 修改yolo.py代码

  • yolo.py中需要修改的地方
        "model_path": 'logs/000/trained_weights_final.h5',  ############## 训练的模型保存文件;"anchors_path": 'model_data/yolo_anchors.txt',      ############## anchors"classes_path": 'model_data/voc_classes.txt',       ############## 类别

4.2. 代码执行过程+结果

  • 执行检测如下:
(tf) zhangkf@john:~/johnCodes/keras-yolo3$ python yolo_video.py --image
Using TensorFlow backend.
Image detection mode
name: TITAN X (Pascal) major: 6 minor: 1 memoryClockRate(GHz): 1.531
pciBusID: 0000:03:00.0
totalMemory: 11.90GiB freeMemory: 11.67GiB
name: TITAN X (Pascal), pci bus id: 0000:03:00.0, compute capability: 6.1)
logs/000/trained_weights_final.h5 model, anchors, and classes loaded.
Input image filename:/home/zhangkf/johnCodes/keras-yolo3/VOCdevkit/VOC2007/JPEGImages/pos-219.jpg
(416, 416, 3)
Found 2 boxes for img
box 0.81 (454, 366) (468, 392)
box 0.91 (540, 440) (553, 465)
0.08477453794330359
Input image filename:/home/zhangkf/johnCodes/keras-yolo3/VOCdevkit/VOC2007/JPEGImages/pos-212.jpg
(416, 416, 3)
Found 3 boxes for img
box 0.70 (227, 225) (250, 278)
box 0.96 (494, 377) (513, 409)
box 0.98 (416, 431) (435, 469)
0.09012221498414874
  • 检测结果分别如下:

五. 重要代码注释model.py

  • 网络结构实现代码在model.py文件,本人加上了一些自己理解的注释,个人认为对网络结构理解特别重要!
""" YOLO_v3 Model Defined in Keras. """from functools import wrapsimport numpy as np
import tensorflow as tf
from keras import backend as K
from keras.layers import Conv2D, Add, ZeroPadding2D, UpSampling2D, Concatenate, MaxPooling2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import BatchNormalization
from keras.models import Model
from keras.regularizers import l2from yolo3.utils import compose"""
Python中@wraps它是一个装饰器,它修饰函数Conv2D,这样当你调用Conv2D函数的时候,其实会调用DarknetConv2D函数,
这样你可以做一些额外的处理,然后再调用真正的Conv2D;
其中:一个是设置核函数的正则项,采用l2正则,一个是padding,之前说了,这个是用来填补数据的边缘区域。再设置好之后,
这些参数放入darknet_conv_kwargs传给Conv2D
"""
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):"""Wrapper to set Darknet parameters for Convolution2D.普通的卷积网络,带正则化,当步长为2时进行下采样"""# 将核权重矩阵的正则化,使用L2正则化,参数是5e-4,即操作w参数;darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}# Padding,一般使用same模式,只有当步长为(2,2)时,使用valid模式。避免在降采样中,引入无用的边界信息;darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'darknet_conv_kwargs.update(kwargs)return Conv2D(*args, **darknet_conv_kwargs)def DarknetConv2D_BN_Leaky(*args, **kwargs):"""Darknet Convolution2D followed by BatchNormalization and LeakyReLU."""# 没有偏置,带正则项no_bias_kwargs = {'use_bias': False}no_bias_kwargs.update(kwargs)return compose(DarknetConv2D(*args, **no_bias_kwargs), BatchNormalization(), LeakyReLU(alpha=0.1))def resblock_body(x, num_filters, num_blocks):'''A series of resblocks starting with a downsampling Convolution2D'''# 使用残差块, 1 + 2 * num_filters 为总的卷积层数# Darknet uses left and top padding instead of 'same' mode# ZeroPadding2D:填充x的边界为0,由(?, 416, 416, 32)转换为(?, 417, 417, 32)。因为下一步卷积操作的步长为2,所以图的边长需要是奇数;x = ZeroPadding2D(((1,0),(1,0)))(x)# DarknetConv2D_BN_Leaky:DarkNet的2维卷积操作,核是(3, 3),步长是(2, 2),注意,这会导致特征尺寸变小,# 由(?, 417, 417, 32)转换为(?, 208, 208, 64)。由于num_filters是64,所以产生64个通道。x = DarknetConv2D_BN_Leaky(num_filters, (3,3), strides=(2,2))(x)for i in range(num_blocks):# compose:输出预测图y,功能是组合函数,先执行1x1的卷积操作,再执行3x3的卷积操作,# filter先降低2倍后恢复,最后与输入相同,都是64;y = compose(DarknetConv2D_BN_Leaky(num_filters//2, (1,1)),DarknetConv2D_BN_Leaky(num_filters, (3,3)))(x)# x = Add()([x, y]):残差操作,将x的值与y的值相加。残差操作可以避免,在网络较深时所产生的梯度弥散问题。x = Add()([x,y])return xdef darknet_body(x):'''Darknent body having 52 Convolution2D layers在darknet_body中,Darknet网络含有5组重复的resblock_body单元,即:'''# darknet的主体网络52层卷积网络# darknet_body的输出格式是(?, 13, 13, 1024)。x = DarknetConv2D_BN_Leaky(32, (3,3))(x)x = resblock_body(x, 64, 1)x = resblock_body(x, 128, 2)x = resblock_body(x, 256, 8)x = resblock_body(x, 512, 8)# Tensor("add_23/add:0", shape=(?, 13, 13, 1024), dtype=float32)# Darknet模型的输入是(?, 416, 416, 3),输出是(?, 13, 13, 1024)。x = resblock_body(x, 1024, 4)return x                        # 输出是(?, 13, 13, 1024)。def make_last_layers(x, num_filters, out_filters):'''6 Conv2D_BN_Leaky layers followed by a Conv2D_linear layer在YOLO v3网络中,输出3个不同尺度的检测图,用于检测不同大小的物体。调用3次make_last_layers,产生3个检测图,即y1、y2和y3。'''# 最后检测头部,无降采采样操作# 第1步,x执行多组1x1的卷积操作和3x3的卷积操作,filter先扩大再恢复,# 最后与输入的filter保持不变,仍为512,则x由(?, 13, 13, 1024)转变为(?, 13, 13, num_filters);num_filters=512x = compose(DarknetConv2D_BN_Leaky(num_filters, (1,1)),DarknetConv2D_BN_Leaky(num_filters*2, (3,3)),DarknetConv2D_BN_Leaky(num_filters, (1,1)),DarknetConv2D_BN_Leaky(num_filters*2, (3,3)),DarknetConv2D_BN_Leaky(num_filters, (1,1)))(x)# x先执行3x3的卷积操作,再执行不含BN和Leaky的1x1的卷积操作,作用类似于全连接操作,生成预测矩阵y;y = compose(DarknetConv2D_BN_Leaky(num_filters*2, (3,3)),DarknetConv2D(out_filters, (1,1)))(x)return x, ydef yolo_body(inputs, num_anchors, num_classes):"""Create YOLO_V3 model CNN body in Keras."""# yolov3的三个检测输出部分# Darknet网络的输入是图片数据集inputs,即(?, 416, 416, 3),# 第1个部分,输出维度是13x13。darknet.output:DarkNet网络的输出,即(?, 13, 13, 1024);darknet = Model(inputs, darknet_body(inputs))# 第1个make_last_layers方法,输出的x是(?, 13, 13, 512),输出的y1是(?, 13, 13, 75)。# 由于模型只有20个检测类别,因而y的第4个维度是75,即3*(20+5)=75。x, y1 = make_last_layers(darknet.output, 512, num_anchors*(num_classes+5)) # x是(?, 13, 13, 512); y1是(?, 13, 13, 75)# 第2个部分,输出维度是26x26;上采样! 通过DarknetConv2D_BN_Leaky卷积,将x由512的通道数,转换为256的通道数;# 通过2倍上采样UpSampling2D,将x由13x13的结构,转换为26x26的结构;x = compose(DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(x)   # x: shape=(?, 26, 26, 256)# 将x与DarkNet的第152层拼接Concatenate;            darknet.layers[152].output:      (?, 26, 26, 512)"""# 在拼接之后,输出的x的格式是(?, 26, 26, 768)。这样做的目的是:将Darknet最底层的高级抽象信息darknet.output,经过若干次转换之后,除了输出给第1个检测部分,还被用于第2个检测部分,经过上采样,与Darknet骨干中,倒数第2次降维的数据拼接,共同作为第2个检测部分的输入。底层抽象特征含有全局信息,中层抽象特征含有局部信息,这样拼接,两者兼顾,用于检测较小的物体。"""x = Concatenate()([x,darknet.layers[152].output])    # x: shape=(?, 26, 26, 768)。# 最终,第2个make_last_layers方法,输出的x是(?, 26, 26, 256),输出的y2是(?, 26, 26, 75)。x, y2 = make_last_layers(x, 256, num_anchors*(num_classes+5))# 第3个部分,输出维度是52x52,与第2个部分类似上采样x = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(x)x = Concatenate()([x,darknet.layers[92].output])x, y3 = make_last_layers(x, 128, num_anchors*(num_classes+5))# 13x13, 26x26, 52x52# [y1, y2, y3]的结构如下:# Tensor("conv2d_59/BiasAdd:0", shape=(?, 13, 13, 75), dtype=float32)# Tensor("conv2d_67/BiasAdd:0", shape=(?, 26, 26, 75), dtype=float32)# Tensor("conv2d_75/BiasAdd:0", shape=(?, 52, 52, 75), dtype=float32)return Model(inputs, [y1,y2,y3])def tiny_yolo_body(inputs, num_anchors, num_classes):'''Create Tiny YOLO_v3 model CNN body in keras.'''x1 = compose(DarknetConv2D_BN_Leaky(16, (3,3)),MaxPooling2D(pool_size=(2,2), strides=(2,2), padding='same'),DarknetConv2D_BN_Leaky(32, (3,3)),MaxPooling2D(pool_size=(2,2), strides=(2,2), padding='same'),DarknetConv2D_BN_Leaky(64, (3,3)),MaxPooling2D(pool_size=(2,2), strides=(2,2), padding='same'),DarknetConv2D_BN_Leaky(128, (3,3)),MaxPooling2D(pool_size=(2,2), strides=(2,2), padding='same'),DarknetConv2D_BN_Leaky(256, (3,3)))(inputs)x2 = compose(MaxPooling2D(pool_size=(2,2), strides=(2,2), padding='same'),DarknetConv2D_BN_Leaky(512, (3,3)),MaxPooling2D(pool_size=(2,2), strides=(1,1), padding='same'),DarknetConv2D_BN_Leaky(1024, (3,3)),DarknetConv2D_BN_Leaky(256, (1,1)))(x1)y1 = compose(DarknetConv2D_BN_Leaky(512, (3,3)),DarknetConv2D(num_anchors*(num_classes+5), (1,1)))(x2)x2 = compose(DarknetConv2D_BN_Leaky(128, (1,1)),UpSampling2D(2))(x2)y2 = compose(Concatenate(),DarknetConv2D_BN_Leaky(256, (3,3)),DarknetConv2D(num_anchors*(num_classes+5), (1,1)))([x2,x1])return Model(inputs, [y1,y2])def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):"""Convert final layer features to bounding box parameters."""# feats的后处理函数,feats就是yolo_outputs,把输出转换到inputs的坐标系num_anchors = len(anchors)# Reshape to batch, height, width, num_anchors, box_params.anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])grid_shape = K.shape(feats)[1:3] # height, widthgrid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),[1, grid_shape[1], 1, 1])grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),[grid_shape[0], 1, 1, 1])grid = K.concatenate([grid_x, grid_y])grid = K.cast(grid, K.dtype(feats))feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])# Adjust preditions to each spatial grid point and anchor size.# 把结果转化到每个格子中,坐标为图片的坐标系的0-1,类别预测也为0-1box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))box_confidence = K.sigmoid(feats[..., 4:5])box_class_probs = K.sigmoid(feats[..., 5:])if calc_loss == True:return grid, feats, box_xy, box_whreturn box_xy, box_wh, box_confidence, box_class_probsdef yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape):'''Get corrected boxes'''# 把预测的图片转换到原始图片的大小box_yx = box_xy[..., ::-1]box_hw = box_wh[..., ::-1]input_shape = K.cast(input_shape, K.dtype(box_yx))image_shape = K.cast(image_shape, K.dtype(box_yx))new_shape = K.round(image_shape * K.min(input_shape/image_shape))offset = (input_shape-new_shape)/2./input_shapescale = input_shape/new_shapebox_yx = (box_yx - offset) * scalebox_hw *= scalebox_mins = box_yx - (box_hw / 2.)box_maxes = box_yx + (box_hw / 2.)boxes =  K.concatenate([box_mins[..., 0:1],  # y_minbox_mins[..., 1:2],  # x_minbox_maxes[..., 0:1],  # y_maxbox_maxes[..., 1:2]  # x_max])# Scale boxes back to original image shape.boxes *= K.concatenate([image_shape, image_shape])return boxesdef yolo_boxes_and_scores(feats, anchors, num_classes, input_shape, image_shape):'''Process Conv layer output'''# 对yolo的输出进行后处理,输出合适原始图片的box和scoresbox_xy, box_wh, box_confidence, box_class_probs = yolo_head(feats,anchors, num_classes, input_shape)boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape)boxes = K.reshape(boxes, [-1, 4])box_scores = box_confidence * box_class_probsbox_scores = K.reshape(box_scores, [-1, num_classes])return boxes, box_scoresdef yolo_eval(yolo_outputs, anchors, num_classes, image_shape, max_boxes=20, score_threshold=.6, iou_threshold=.5):"""Evaluate YOLO model on given input and return filtered boxes."""# 使用yolo模型进行图片的检测,进行坐标转化,和nms处理,和num_layers = len(yolo_outputs)anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]] # default settinginput_shape = K.shape(yolo_outputs[0])[1:3] * 32boxes = []box_scores = []for l in range(num_layers):_boxes, _box_scores = yolo_boxes_and_scores(yolo_outputs[l],anchors[anchor_mask[l]], num_classes, input_shape, image_shape)boxes.append(_boxes)box_scores.append(_box_scores)boxes = K.concatenate(boxes, axis=0)box_scores = K.concatenate(box_scores, axis=0)mask = box_scores >= score_thresholdmax_boxes_tensor = K.constant(max_boxes, dtype='int32')boxes_ = []scores_ = []classes_ = []for c in range(num_classes):# TODO: use keras backend instead of tf.# 进行nms处理class_boxes = tf.boolean_mask(boxes, mask[:, c])class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=iou_threshold)class_boxes = K.gather(class_boxes, nms_index)class_box_scores = K.gather(class_box_scores, nms_index)classes = K.ones_like(class_box_scores, 'int32') * cboxes_.append(class_boxes)scores_.append(class_box_scores)classes_.append(classes)boxes_ = K.concatenate(boxes_, axis=0)scores_ = K.concatenate(scores_, axis=0)classes_ = K.concatenate(classes_, axis=0)return boxes_, scores_, classes_def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):'''Preprocess true boxes to training input formaty_true的第0和1位是中心点xy,范围是(0~13/26/52),第2和3位是宽高wh,范围是0~1,第4位是置信度1或0,第5~n位是类别为1其余为0。Parameters----------true_boxes: array, shape=(m, T, 5)Absolute x_min, y_min, x_max, y_max, class_id relative to input_shape.input_shape: array-like, hw, multiples of 32anchors: array, shape=(N, 2), whnum_classes: integerReturns-------y_true: list of array, shape like yolo_outputs, xywh are reletive value'''assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'num_layers = len(anchors)//3 # default settinganchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]true_boxes = np.array(true_boxes, dtype='float32')input_shape = np.array(input_shape, dtype='int32')boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2]true_boxes[..., 0:2] = boxes_xy/input_shape[::-1]true_boxes[..., 2:4] = boxes_wh/input_shape[::-1]m = true_boxes.shape[0]grid_shapes = [input_shape//{0:32, 1:16, 2:8}[l] for l in range(num_layers)]y_true = [np.zeros((m,grid_shapes[l][0],grid_shapes[l][1],len(anchor_mask[l]),5+num_classes),dtype='float32') for l in range(num_layers)]# Expand dim to apply broadcasting.anchors = np.expand_dims(anchors, 0)anchor_maxes = anchors / 2.anchor_mins = -anchor_maxesvalid_mask = boxes_wh[..., 0]>0for b in range(m):# Discard zero rows.wh = boxes_wh[b, valid_mask[b]]if len(wh)==0: continue# Expand dim to apply broadcasting.wh = np.expand_dims(wh, -2)box_maxes = wh / 2.box_mins = -box_maxesintersect_mins = np.maximum(box_mins, anchor_mins)intersect_maxes = np.minimum(box_maxes, anchor_maxes)intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]box_area = wh[..., 0] * wh[..., 1]anchor_area = anchors[..., 0] * anchors[..., 1]iou = intersect_area / (box_area + anchor_area - intersect_area)# Find best anchor for each true boxbest_anchor = np.argmax(iou, axis=-1)for t, n in enumerate(best_anchor):for l in range(num_layers):if n in anchor_mask[l]:i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')k = anchor_mask[l].index(n)c = true_boxes[b,t, 4].astype('int32')y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]y_true[l][b, j, i, k, 4] = 1y_true[l][b, j, i, k, 5+c] = 1return y_truedef box_iou(b1, b2):'''Parameters----------b1: tensor, shape=(i1,...,iN, 4), xywhb2: tensor, shape=(j, 4), xywhReturn iou tensor-------iou: tensor, shape=(i1,...,iN, j)'''# Expand dim to apply broadcasting.b1 = K.expand_dims(b1, -2)b1_xy = b1[..., :2]b1_wh = b1[..., 2:4]b1_wh_half = b1_wh/2.b1_mins = b1_xy - b1_wh_halfb1_maxes = b1_xy + b1_wh_half# Expand dim to apply broadcasting.b2 = K.expand_dims(b2, 0)b2_xy = b2[..., :2]b2_wh = b2[..., 2:4]b2_wh_half = b2_wh/2.b2_mins = b2_xy - b2_wh_halfb2_maxes = b2_xy + b2_wh_halfintersect_mins = K.maximum(b1_mins, b2_mins)intersect_maxes = K.minimum(b1_maxes, b2_maxes)intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]b1_area = b1_wh[..., 0] * b1_wh[..., 1]b2_area = b2_wh[..., 0] * b2_wh[..., 1]iou = intersect_area / (b1_area + b2_area - intersect_area)return ioudef yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=False):'''Return yolo_loss tensorParameters----------yolo_outputs: list of tensor, the output of yolo_body or tiny_yolo_bodyy_true: list of array, the output of preprocess_true_boxesanchors: array, shape=(N, 2), whnum_classes: integerignore_thresh: float, the iou threshold whether to ignore object confidence lossReturns-------loss: tensor, shape=(1,)'''# 构建lossnum_layers = len(anchors)//3                    # default setting  num_layers = 3# 分类args: 前3个是yolo_outputs预测值,后3个是y_true真值;yolo_outputs = args[:num_layers]                # yolo_outputs: [(?, 13, 13, 75), (?, 26, 26, 75), (?, 52, 52, 75)]y_true = args[num_layers:]                      # yolo_true   : [(?, 13, 13, 75), (?, 26, 26, 75), (?, 52, 52, 75)]# anchor_mask:anchor box的索引数组,3个1组倒序排序,678对应13x13,345对应26x26,123对应52x52;即[[6, 7, 8], [3, 4, 5], [0, 1, 2]];anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]# a1 = yolo_outputs[0]                    # shape: (?, 13, 13, 75)# a2 = K.shape(yolo_outputs[0])           # shape: (4,)# a3 = K.shape(yolo_outputs[0])[1:3]      # shape: (2,)  backend.shape()返回张量或变量的符号shape内容应该是(13,13)的shape# 第1个预测矩阵yolo_outputs[0]的结构(shape)的第1~2位,即(?, 13, 13, 18)中的(13, 13)。再x32,# 就是YOLO网络的输入尺寸,即(416, 416),因为在网络中,含有5个步长为(2, 2)的卷积操作,降维32=5^2倍;input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))# grid_shapes:与input_shape类似,K.shape()[1:3],以列表的形式,选择3个尺寸的预测图维度,即[(13, 13), (26, 26), (52, 52)];grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)]loss = 0m = K.shape(yolo_outputs[0])[0]             # m: 第1个预测图的结构的第1位,即K.shape()[0],输入模型的图片总量,即批次数batchsize;mf = K.cast(m, K.dtype(yolo_outputs[0]))    # mf: m的float类型,即K.cast(m, K.dtype())# 在计算损失值时,循环计算每1层的损失值,累加到一起,即for l in range(num_layers):# 获取物体置信度object_mask,最后1个维度的第4位,第0~3位是框,第4位是物体置信度;object_mask = y_true[l][..., 4:5]# 类别置信度true_class_probs,最后1个维度的第5位开始;这里我用的数据集是:VOC2007,因此这里是20;true_class_probs = y_true[l][..., 5:]# 网格grid:结构是(13, 13, 1, 2),数值为0~12的全遍历二元组# 预测值raw_pred:经过reshape变换,将anchors分离,结构是(?, 13, 13, 3, 25)# pred_xy和pred_wh归一化的起始点xy和宽高wh,xy的结构是(?, 13, 13, 3, 2),wh的结构是(?, 13, 13, 3, 2);grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l],anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)# 结构是(13, 13, 3, 4)pred_box = K.concatenate([pred_xy, pred_wh])# Darknet raw box to calculate loss.raw_true_xy = y_true[l][..., :2]*grid_shapes[l][::-1] - gridraw_true_wh = K.log(y_true[l][..., 2:4] / anchors[anchor_mask[l]] * input_shape[::-1])raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh)) # avoid log(0)=-infbox_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]# Find ignore mask, iterate over each of batch.ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)object_mask_bool = K.cast(object_mask, 'bool')def loop_body(b, ignore_mask):true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])iou = box_iou(pred_box[b], true_box)best_iou = K.max(iou, axis=-1)ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))return b+1, ignore_mask_, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])ignore_mask = ignore_mask.stack()ignore_mask = K.expand_dims(ignore_mask, -1)# K.binary_crossentropy is helpful to avoid exp overflow.xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[...,0:2], from_logits=True)wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh-raw_pred[...,2:4])confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True)+ \(1-object_mask) * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True) * ignore_mask################################################ 分类loss值 ###################################################### tf.keras.backend.binary_crossentropy(#     target, # target:与output具有相同shape的张量。#     output, # output:一个张量#     from_logits=False) 输出张量和目标张量之间的二进制交叉熵。aaa = true_class_probs              # shape (?, 13, 13, 3, 20)   真实类标签20aaa_ele = aaa[:, 0, 0, 0, :]        # 20个类标签label_one_hot = tf.one_hot(tf.cast(aaa_ele, dtype=tf.int32), 20)bbb = raw_pred                      # shape (?, ?, ?, 3, 25)ccc = raw_pred[..., 5:]             # shape (?, ?, ?, 3, 20)     预测类标签20class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[...,5:], from_logits=True)xy_loss = K.sum(xy_loss) / mfwh_loss = K.sum(wh_loss) / mfconfidence_loss = K.sum(confidence_loss) / mfclass_loss = K.sum(class_loss) / mfloss += xy_loss + wh_loss + confidence_loss + class_lossif print_loss:loss = tf.Print(loss, [loss, xy_loss, wh_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ')return loss

六. Keras中自定义损失函数的方法

  • Keras是很方便,然而这种方便不是没有代价的,最为人诟病之一的缺点就是灵活性较低,难以搭建一些复杂的模型。的确,Keras确实不是很适合搭建复杂的模型,但并非没有可能,而是搭建太复杂的模型所用的代码量,跟直接用tensorflow写也差不了多少。但不管怎么说,Keras其友好、方便的特性(比如那可爱的训练进度条),使得我们总有使用它的场景。这样,如何更灵活地定制Keras模型,就成为一个值得研究的课题了。这篇文章我们来关心自定义loss。

  • Keras作为一个深度学习库,非常适合新手。在做神经网络时,它自带了许多常用的目标函数,优化方法等等,基本能满足新手学习时的一些需求。具体包含 目标函数优化方法。但它也支持用户自定义目标函数,下边介绍一种最简单的自定义目标函数的方法。

  • 要实现自定义目标函数,自然想到先看下Keras中的目标函数是怎么定义的。查下源码发现在Keras/objectives.py中,Keras定义了一系列的目标函数。

def mean_squared_error(y_true, y_pred):                                     # 均方差msereturn K.mean(K.square(y_pred - y_true), axis=-1)                  def mean_absolute_error(y_true, y_pred):                                    # maereturn K.mean(K.abs(y_pred - y_true), axis=-1)def mean_absolute_percentage_error(y_true, y_pred):                         # mapediff = K.abs((y_true - y_pred) / K.clip(K.abs(y_true), K.epsilon(), np.inf))return 100. * K.mean(diff, axis=-1)def mean_squared_logarithmic_error(y_true, y_pred):                         first_log = K.log(K.clip(y_pred, K.epsilon(), np.inf) + 1.)second_log = K.log(K.clip(y_true, K.epsilon(), np.inf) + 1.)return K.mean(K.square(first_log - second_log), axis=-1)def squared_hinge(y_true, y_pred):                                          # 平方的hinge lossreturn K.mean(K.square(K.maximum(1. - y_true * y_pred, 0.)), axis=-1)def hinge(y_true, y_pred):                                                  # hinge lossreturn K.mean(K.maximum(1. - y_true * y_pred, 0.), axis=-1)def categorical_crossentropy(y_true, y_pred):                              '''Expects a binary class matrix instead of a vector of scalar classes.# 亦称作多类的对数损失,注意使用该目标函数时,需要将标签转化为形如(nb_samples, nb_classes)的二值序列######################################################################################### 注意: 当使用"categorical_crossentropy"作为目标函数时,标签应该为多类模式,即one-hot编码的向量,而不是单个数值.可以使用工具中的to_categorical函数完成该转换.示例如下:from keras.utils.np_utils import to_categoricalcategorical_labels = to_categorical(int_labels, num_classes=None)'''return K.categorical_crossentropy(y_pred, y_true)def sparse_categorical_crossentropy(y_true, y_pred):'''expects an array of integer classes.Note: labels shape must have the same number of dimensions as output shape.If you get a shape error, add a length-1 dimension to labels.# 如上,但接受稀疏标签。注意,使用该函数时仍然需要你的标签与输出值的维度相同,你可能需要在标签数据上增加一个维度:np.expand_dims(y,-1)'''return K.sparse_categorical_crossentropy(y_pred, y_true)def binary_crossentropy(y_true, y_pred):'''亦称作对数损失,logloss)'''return K.mean(K.binary_crossentropy(y_pred, y_true), axis=-1)def kullback_leibler_divergence(y_true, y_pred):y_true = K.clip(y_true, K.epsilon(), 1)y_pred = K.clip(y_pred, K.epsilon(), 1)return K.sum(y_true * K.log(y_true / y_pred), axis=-1)def poisson(y_true, y_pred):return K.mean(y_pred - y_true * K.log(y_pred + K.epsilon()), axis=-1)def cosine_proximity(y_true, y_pred):y_true = K.l2_normalize(y_true, axis=-1)y_pred = K.l2_normalize(y_pred, axis=-1)return -K.mean(y_true * y_pred, axis=-1)
  • 看到源码后,事情就简单多了,我们只要仿照这源码的定义形式,来定义自己的loss就可以了。

6.1. 例子1:自定义loss

  • 例子1:我们定义一个loss为预测值与真实值的差,则可写为:
def my_koss(y_true,y_pred):return K.mean((y_pred-y_true),axis=-1)
  • 然后,将这段代码放到你的模型中编译,例如
def my_loss(y_true,y_pred):return K.mean((y_pred-y_true),axis = -1)
model.compile(loss=my_loss, optimizer='SGD', metrics=['accuracy'])
  • 有一点需要注意,Keras作为一个高级封装库,它的底层(后端)可以支持theano或者tensorflow,在使用上边代码时,首先要导入这一句,这样你自定义的loss函数就可以起作用了。
from keras import backend as K

6.2. 例子2:自定义loss

  • 例子2:我们做分类问题时,经常用的就是softmax输出,然后用交叉熵作为loss。然而这种做法也有不少缺点,其中之一就是分类太自信,哪怕输入噪音,分类的结果也几乎是非1即0,这通常会导致过拟合的风险,还会使得我们在实际应用中没法很好地确定置信区间、设置阈值。因此很多时候我们也会想办法使得分类别太自信,而修改loss也是手段之一。

  • 如果不修改loss,我们就是使用交叉熵去拟合一个one hot的分布。交叉熵的公式是
    S(q∣p)=−∑iqilog⁡piS(q | p)=-\sum_{i} q_{i} \log p_{i}S(qp)=iqilogpi

  • 其中 pip_{i}pi 是预测的分布,而 qiq_{i}qi 是真实的分布,比如输出[z1,z2,z3][z_1,z_2,z_3][z1,z2,z3]真实值(目标)[1,0,0][1,0,0][1,0,0],那么
    loss=−log⁡(ez1/Z),Z=ez1+ez2+ez3loss = -\log \Big(e^{z_1}/Z\Big),\, Z=e^{z_1}+e^{z_2}+e^{z_3}loss=log(ez1/Z),Z=ez1+ez2+ez3

  • 只要 z1z_1z1 已经是 [z1,z2,z3][z_1,z_2,z_3][z1,z2,z3] 的最大值,那么我们总可以 “变本加厉” —通过增大训练参数,使得z1,z2,z3z_1,z_2,z_3z1,z2,z3增加足够大的比例(等价地,即增大向量 [z1,z2,z3][z_1,z_2,z_3][z1,z2,z3] 的模长),从而 z1/Z{z_1}/Zz1/Z 足够接近 111(等价地,loss足够接近0)。这就是通常softmax过于自信的来源:只要盲目增大模长,就可以降低loss,训练器肯定是很乐意了,这代价太低了。为了使得分类不至于太自信,一个方案就是不要单纯地去拟合one hot分布,分一点力气去拟合一下均匀分布,即改为新loss:

loss=−(1−ε)log⁡(ez1/Z)−ε∑i=1n13log⁡(ezi/Z),Z=ez1+ez2+ez3loss = -(1-\varepsilon)\log \Big(e^{z_1}/Z\Big)-\varepsilon\sum_{i=1}^n \frac{1}{3}\log \Big(e^{z_i}/Z\Big),\, Z=e^{z_1}+e^{z_2}+e^{z_3}loss=(1ε)log(ez1/Z)εi=1n31log(ezi/Z),Z=ez1+ez2+ez3

  • 这样,盲目地增大比例使得 z1/Z{z_1}/Zz1/Z 接近于 111,就不再是最优解了,从而可以缓解softmax过于自信的情况,不少情况下,这种策略还可以增加测试准确率(防止过拟合)。
  • 那么,在Keras中应该怎么写呢?其实挺简单的:
from keras.layers import Input,Embedding,LSTM,Dense
from keras.models import Model
from keras import backend as Kword_size = 128
nb_features = 10000
nb_classes = 10
encode_size = 64input = Input(shape=(None,))
embedded = Embedding(nb_features,word_size)(input)
encoder = LSTM(encode_size)(embedded)
predict = Dense(nb_classes, activation='softmax')(encoder)# 自定义loss函数模块
def mycrossentropy(y_true, y_pred, e=0.1):loss1 = K.categorical_crossentropy(y_true, y_pred)loss2 = K.categorical_crossentropy(K.ones_like(y_pred)/nb_classes, y_pred)return (1-e)*loss1 + e*loss2model = Model(inputs=input, outputs=predict)
model.compile(optimizer='adam', loss=mycrossentropy)
  • 也就是自定义一个输入为y_pred,y_true的loss函数,放进模型compile即可。这里的mycrossentropy,第一项就是普通的交叉熵,第二项中,先通过K.ones_like(y_pred)/nb_classes构造了一个均匀分布(均匀分布介绍! )然后算y_pred与均匀分布的交叉熵。 就这么简单~

参考文章

  • Keras中自定义目标函数(损失函数)的简单方法
  • Keras中自定义复杂的loss函数-好的介绍!

『论文笔记』TensorFlow1.6.0+Keras 2.1.5+Python3.5+Yolov3训练自己的数据集!相关推荐

  1. 『论文笔记』Two Causal Principles for Improving Visual Dialog

    Two Causal Principles for Improving Visual Dialog 一句话总结 从因果图角度审视视觉对话任务,切断对话历史与答案的直接因果效应,添加混杂因子[用户偏好] ...

  2. 『论文笔记』ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Blocks!

    ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Blocks! 文章目录 一 ...

  3. 『论文笔记』Are VQA Systems RAD? Measuring Robustness to Augmented Data with Focused Interventions

    Are VQA Systems RAD? Measuring Robustness to Augmented Data with Focused Interventions 一句话总结 本文提出一个基 ...

  4. 论文笔记目录(ver2.0)

    1 时间序列 1.1 时间序列预测 论文名称 来源 主要内容 论文笔记:DCRNN (Diffusion Convolutional Recurrent Neural Network: Data-Dr ...

  5. Win7+keras+tensorflow使用YOLO-v3训练自己的数据集

    一.下载和测试模型 1. 下载YOLO-v3 git clone https://github.com/qqwweee/keras-yolo3.git 这是在Ubuntu里的命令,windows直接去 ...

  6. 『Pytorch笔记』Pycharm中使用CUDA_VISIBLE_DEVICES=0!

    Pycharm中使用CUDA_VISIBLE_DEVICES=0! 如果使用多GPU运行程序(或者指定GPU的个数),可以直接使用CUDA_VISIBLE_DEVICES=0,1,2,3python ...

  7. 『论文阅读』SIF:一种简单却难以打败的句子嵌入方法

      文献:A SIMPLE BUT TOUGH-TO-BEAT BASELINE FOR SENTENCE EMBEDDINGS   在进行了词嵌入的研究后,我们往往会联想到这样一个问题:既然单词可以 ...

  8. SLAM论文笔记---- FlowNet及FlowNet2.0

    SLAM笔记第二篇 FlowNet提出端到端的cnn网络学习从一对图像来预测光流,额外开发了correlation layer来进行匹配,构造了flying chairs以解决数据不足问题. 传统的滑 ...

  9. 『 论文阅读』LightGBM原理-LightGBM: A Highly Efficient Gradient Boosting Decision Tree

    17年8月LightGBM就开源了,那时候就开始尝试上手,不过更多还是在调参层面,在作者12月论文发表之后看了却一直没有总结,这几天想着一定要翻译下,自己也梳理下GBDT相关的算法. Abstract ...

最新文章

  1. Python 2 宣布正式退休,Python 3 时代到来!
  2. [PaaS] 深入 Cloud Foundry(一)构架 (转载)
  3. 高考查分数微信就能搞定
  4. html代码大全(很全的)_5行Python代码就能爬取 3000+ 上市公司的信息?
  5. 程序猿要什么爱情,陪你未来的是键盘和代码啊!
  6. ajax需要引用什么js文件吗,如何在ajax调用中包含js文件?
  7. IBM研究院计画5年改变人类生活创新预测
  8. Java基础——String类(二)
  9. Android API中文文档(111) —— MailTo
  10. Java Swing线程之SwingUtilities.invokeLater解释
  11. 【控制】《最优控制理论与系统》-胡寿松老师-第3章-极小值原理及其应用
  12. 怎么在oracle建表,在linux怎么操控oracle建表
  13. 基于汽车后市场应用 汽车VIN码识别 车架号识别数据SDK
  14. 哇哦,教你如何使用jumpserver堡垒机添加windows资产
  15. 如何开通企业付款到零钱||小程序红包功能
  16. BTC源码分析 区块(一)
  17. 模型的“参数”与“超参数”
  18. 视线追踪技术的前世今生
  19. JAVA之线程池详解
  20. 红白机基本原理(二) CPU

热门文章

  1. 腾讯云轻量应用服务器快速搭建一个专属网盘
  2. cv2.error: OpenCV(4.1.0)error: (-215:Assertion failed) !ssize.empty() in function ‘resize‘——记录解决方法
  3. 使用css和js实现鼠标拖尾特效(七彩泡泡)
  4. [深度应用]·实战掌握PyTorch图片分类简明教程
  5. 【19调剂】北京航空航天大学苏州创新研究院培养基地非全日制硕士研究生招生说明(校内)...
  6. vue3+ts+amap/amap-jsapi-loader实现高德地图搜索选取地点
  7. python无法安装tensorflow_Windows上安装Tensorflow踩的坑
  8. 开放api接口签名验证
  9. How cc Works 中文译文
  10. es 指定排序字段_ElasticSearch按照指定字段排序 | 三分钟