TensorFlow 2.0+Keras 防坑指南

TensorFlow 2.0是对1.x版本做了一次大的瘦身,Eager Execution默认开启,并且使用Keras作为默认高级API,
这些改进大大降低的TensorFlow使用难度。

本文主要记录了一次曲折的使用Keras+TensorFlow2.0的BatchNormalization的踩坑经历,这个坑差点要把TF2.0的新特性都毁灭殆尽,如果你在学习TF2.0的官方教程,不妨一观。

问题的产生

从教程[1]https://www.tensorflow.org/alpha/tutorials/images/transfer_learning?hl=zh-cn(讲述如何Transfer Learning)说起:

IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)
# Create the base model from the pre-trained model MobileNet V2
base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,include_top=False,weights='imagenet')
model = tf.keras.Sequential([base_model,tf.keras.layers.GlobalAveragePooling2D(),tf.keras.layers.Dense(NUM_CLASSES)
])

简单的代码我们就复用了MobileNetV2的结构创建了一个分类器模型,接着我们就可以调用Keras的接口去训练模型:

model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),loss='sparse_categorical_crossentropy',metrics=['sparse_categorical_accuracy'])model.summary()history = model.fit(train_batches.repeat(),epochs=20,steps_per_epoch = steps_per_epoch,validation_data=validation_batches.repeat(),validation_steps=validation_steps)

输出的结果看,一起都很完美:

Model: "sequential"_________________________________________________________________Layer (type)                 Output Shape              Param #=================================================================mobilenetv2_1.00_160 (Model) (None, 5, 5, 1280)        2257984_________________________________________________________________global_average_pooling2d (Gl (None, 1280)              0_________________________________________________________________dense (Dense)                (None, 2)                 1281=================================================================Total params: 2,259,265Trainable params: 1,281Non-trainable params: 2,257,984_________________________________________________________________Epoch 11/20581/581 [==============================] - 134s 231ms/step - loss: 0.4208 - accuracy: 0.9484 - val_loss: 0.1907 - val_accuracy: 0.9812Epoch 12/20581/581 [==============================] - 114s 197ms/step - loss: 0.3359 - accuracy: 0.9570 - val_loss: 0.1835 - val_accuracy: 0.9844Epoch 13/20581/581 [==============================] - 116s 200ms/step - loss: 0.2930 - accuracy: 0.9650 - val_loss: 0.1505 - val_accuracy: 0.9844Epoch 14/20581/581 [==============================] - 114s 196ms/step - loss: 0.2561 - accuracy: 0.9701 - val_loss: 0.1575 - val_accuracy: 0.9859Epoch 15/20581/581 [==============================] - 119s 206ms/step - loss: 0.2302 - accuracy: 0.9715 - val_loss: 0.1600 - val_accuracy: 0.9812Epoch 16/20581/581 [==============================] - 115s 197ms/step - loss: 0.2134 - accuracy: 0.9747 - val_loss: 0.1407 - val_accuracy: 0.9828Epoch 17/20581/581 [==============================] - 115s 197ms/step - loss: 0.1546 - accuracy: 0.9813 - val_loss: 0.0944 - val_accuracy: 0.9828Epoch 18/20581/581 [==============================] - 116s 200ms/step - loss: 0.1636 - accuracy: 0.9794 - val_loss: 0.0947 - val_accuracy: 0.9844Epoch 19/20581/581 [==============================] - 115s 198ms/step - loss: 0.1356 - accuracy: 0.9823 - val_loss: 0.1169 - val_accuracy: 0.9828Epoch 20/20581/581 [==============================] - 116s 199ms/step - loss: 0.1243 - accuracy: 0.9849 - val_loss: 0.1121 - val_accuracy: 0.9875

然而这种写法还是不方便Debug,我们希望可以精细的控制迭代的过程,并能够看到中间结果,所以我们训练的过程改成了这样:

optimizer = tf.keras.optimizers.RMSprop(lr=base_learning_rate)
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')@tf.functiondef train_cls_step(image, label):with tf.GradientTape() as tape:predictions = model(image)loss = tf.keras.losses.SparseCategoricalCrossentropy()(label, predictions)gradients = tape.gradient(loss, model.trainable_variables)optimizer.apply_gradients(zip(gradients, model.trainable_variables))train_accuracy(label, predictions)for images, labels in train_batches:train_cls_step(images,labels)

重新训练后,结果依然很完美!

但是,这时候我们想对比一下Finetune和重头开始训练的差别,所以把构建模型的代码改成了这样:

base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,include_top=False,weights=None)

使得模型的权重随机生成,这时候训练结果就开始抽风了,Loss不下降,Accuracy稳定在50%附近游荡:

Step #10: loss=0.6937199831008911 acc=46.5625%Step #20: loss=0.6932525634765625 acc=47.8125%Step #30: loss=0.699873685836792 acc=49.16666793823242%Step #40: loss=0.6910845041275024 acc=49.6875%Step #50: loss=0.6935917139053345 acc=50.0625%Step #60: loss=0.6965731382369995 acc=49.6875%Step #70: loss=0.6949992179870605 acc=49.19642639160156%Step #80: loss=0.6942993402481079 acc=49.84375%Step #90: loss=0.6933775544166565 acc=49.65277862548828%Step #100: loss=0.6928421258926392 acc=49.5%Step #110: loss=0.6883170008659363 acc=49.54545593261719%Step #120: loss=0.695658802986145 acc=49.453125%Step #130: loss=0.6875559091567993 acc=49.61538314819336%Step #140: loss=0.6851695775985718 acc=49.86606979370117%Step #150: loss=0.6978713274002075 acc=49.875%Step #160: loss=0.7165156602859497 acc=50.0%Step #170: loss=0.6945627331733704 acc=49.797794342041016%Step #180: loss=0.6936900615692139 acc=49.9305534362793%Step #190: loss=0.6938323974609375 acc=49.83552551269531%Step #200: loss=0.7030564546585083 acc=49.828125%Step #210: loss=0.6926192045211792 acc=49.76190185546875%Step #220: loss=0.6932414770126343 acc=49.786930084228516%Step #230: loss=0.6924526691436768 acc=49.82337188720703%Step #240: loss=0.6882281303405762 acc=49.869789123535156%Step #250: loss=0.6877702474594116 acc=49.86249923706055%Step #260: loss=0.6933954954147339 acc=49.77163314819336%Step #270: loss=0.6944763660430908 acc=49.75694274902344%Step #280: loss=0.6945018768310547 acc=49.49776840209961%

我们将predictions的结果打印出来,发现batch内每个输出都是一模一样的:

0 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)1 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)2 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)3 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)4 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)5 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)6 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)7 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)8 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)9 = tf.Tensor([0.51352817 0.48647183], shape=(2,), dtype=float32)

只是修改了初始权重,为何会产生这样的结果?

问题排查

实验1

是不是训练不够充分,或者learning rate设置的不合适?
经过几轮调整,发现无论训练多久,learning rate变大变小,都无法改变这种结果

实验2

既然是权重的问题,是不是权重随机初始化的有问题,把初始权重拿出来统计了一下,一切正常

实验3

这种问题根据之前的经验,在导出Inference模型的时候BatchNormalization没有处理好会出现这种一个batch内所有结果都一样的问题。但是如何解释训练的时候为什么会出现这个问题?而且为什么Finetue不会出现问题呢?只是改了权重的初始值而已呀
按照这个方向去Google的一番,发现了Keras的BatchNormalization确实有很多issue,其中一个问题是在保存模型的是BatchNormalzation的moving mean和moving variance不会被保存[6]https://github.com/tensorflow/tensorflow/issues/16455,而另外一个issue提到问题就和我们问题有关系的了:
[2] https://github.com/tensorflow/tensorflow/issues/19643
[3] https://github.com/tensorflow/tensorflow/issues/23873
最后,这位作者找到了原因,并且总结在了这里:
[4] https://pgaleone.eu/tensorflow/keras/2019/01/19/keras-not-yet-interface-to-tensorflow/

根据这个提示,我们做了如下尝试:

实验3.1

改用model.fit的写法进行训练,在最初的几个epoch里面,我们发现好的一点的是training accuracy已经开始缓慢提升了,但是validation accuracy存在原来的问题。而且通过model.predict_on_batch()拿到中间结果,发现依然还是batch内输出都一样。

Epoch 1/20581/581 [==============================] - 162s 279ms/step - loss: 0.6768 - sparse_categorical_accuracy: 0.6224 - val_loss: 0.6981 - val_sparse_categorical_accuracy: 0.4984Epoch 2/20581/581 [==============================] - 133s 228ms/step - loss: 0.4847 - sparse_categorical_accuracy: 0.7684 - val_loss: 0.6931 - val_sparse_categorical_accuracy: 0.5016Epoch 3/20581/581 [==============================] - 130s 223ms/step - loss: 0.3905 - sparse_categorical_accuracy: 0.8250 - val_loss: 0.6996 - val_sparse_categorical_accuracy: 0.4984Epoch 4/20581/581 [==============================] - 131s 225ms/step - loss: 0.3113 - sparse_categorical_accuracy: 0.8660 - val_loss: 0.6935 - val_sparse_categorical_accuracy: 0.5016

但是,随着训练的深入,结果出现了逆转,开始变得正常了(tf.function的写法是无论怎么训练都不会变化,幸好没有放弃治疗)(追加:其实这里还是有问题的,继续看后面,当时就觉得怪怪的,不应该收敛这么慢

Epoch 18/20581/581 [==============================] - 131s 226ms/step - loss: 0.0731 - sparse_categorical_accuracy: 0.9725 - val_loss: 1.4896 - val_sparse_categorical_accuracy: 0.8703Epoch 19/20581/581 [==============================] - 130s 225ms/step - loss: 0.0664 - sparse_categorical_accuracy: 0.9748 - val_loss: 0.6890 - val_sparse_categorical_accuracy: 0.9016Epoch 20/20581/581 [==============================] - 126s 217ms/step - loss: 0.0631 - sparse_categorical_accuracy: 0.9768 - val_loss: 1.0290 - val_sparse_categorical_accuracy: 0.9031

通多model.predict_on_batch()拿到的结果也和这个Accuracy也是一致的

实验3.2

通过上一个实验,我们验证了确实如果只通过Keras的API去训练,是正常。更深层的原因是什么呢?是不是BatchNomalization没有update moving mean和moving variance导致的呢?答案是Yes
我们分别在两中训练方法前后,打印 moving mean和moving variance的值:

def get_bn_vars(collection):moving_mean, moving_variance = None, None    for var in collection:name = var.name.lower()if "variance" in name:moving_variance = varif "mean" in name:moving_mean = varif moving_mean is not None and moving_variance is not None:return moving_mean, moving_varianceraise ValueError("Unable to find moving mean and variance")mean, variance = get_bn_vars(model.variables)print(mean)print(variance)

我们发现,确实如果使用model.fit()进行训练,mean和variance是在update的(虽然更新的速率看着有些奇怪),但是对于tf.function那种写法这两个值就没有被update

那这里我们也可以解释为什么Finetune不会出现问题了,因为imagenet训练的mean, variance已经是一个比较好的值了,即使不更新也可以正常使用

实验3.3

是不是改成[4]里面说的方法构建动态的Input_Shape的模型就OK了呢?

class MyModel(Model):def __init__(self):super(MyModel, self).__init__()self.conv1 = Conv2D(32, 3, activation='relu')self.batch_norm1=BatchNormalization()self.flatten = Flatten()self.d1 = Dense(128, activation='relu')self.d2 = Dense(10, activation='softmax')def call(self, x):x = self.conv1(x)x = self.batch_norm1(x)x = self.flatten(x)x = self.d1(x)return self.d2(x)model = MyModel()#model.build((None,28,28,1))model.summary()@tf.functiondef train_step(image, label):with tf.GradientTape() as tape:predictions = model(image)loss = loss_object(label, predictions)gradients = tape.gradient(loss, model.trainable_variables)optimizer.apply_gradients(zip(gradients, model.trainable_variables))train_loss(loss)train_accuracy(label, predictions)

模型如下:

Model: "my_model"_________________________________________________________________Layer (type)                 Output Shape              Param #  =================================================================conv2d (Conv2D)              multiple                  320      _________________________________________________________________batch_normalization_v2 (Batc multiple                  128      _________________________________________________________________flatten (Flatten)            multiple                  0        _________________________________________________________________dense (Dense)                multiple                  2769024  _________________________________________________________________dense_1 (Dense)              multiple                  1290      =================================================================Total params: 2,770,762Trainable params: 2,770,698Non-trainable params: 64

从Output Shape看,构建模型没问题
跑了一遍MINST,结果也很不错!
以防万一,我们同样测试了一下mean和variance是否被更新,然而结果出乎意料,并没有!
也就是说[4]里面说的方案在我们这里并不可行

实验3.4

既然我们定位问题是在BatchNormalization这里,所以就想到BatchNormalization的training和testing时候行为是不一致的,在testing的时候moving mean和variance是不需要update的,那么会不会是tf.function的这种写法并不会自动更改这个状态呢?
查看源码,发现BatchNormalization的call()存在一个training参数,而且默认是False

 Call arguments:inputs: Input tensor (of any rank).training: Python boolean indicating whether the layer should behave intraining mode or in inference mode.- `training=True`: The layer will normalize its inputs using themean and variance of the current batch of inputs.- `training=False`: The layer will normalize its inputs using themean and variance of its moving statistics, learned during training.

所以,做了如下改进:

class MyModel(Model):def __init__(self):super(MyModel, self).__init__()self.conv1 = Conv2D(32, 3, activation='relu')self.batch_norm1=BatchNormalization()self.flatten = Flatten()self.d1 = Dense(128, activation='relu')self.d2 = Dense(10, activation='softmax')def call(self, x,training=True):x = self.conv1(x)x = self.batch_norm1(x,training=training)x = self.flatten(x)x = self.d1(x)return self.d2(x)model = MyModel()#model.build((None,28,28,1))model.summary()@tf.functiondef train_step(image, label):with tf.GradientTape() as tape:predictions = model(image,training=True)loss = loss_object(label, predictions)gradients = tape.gradient(loss, model.trainable_variables)optimizer.apply_gradients(zip(gradients, model.trainable_variables))train_loss(loss)train_accuracy(label, predictions)@tf.functiondef test_step(image, label):predictions = model(image,training=False)t_loss = loss_object(label, predictions)test_loss(t_loss)test_accuracy(label, predictions)

结果显示,moving mean和variance开始更新啦,测试Accuracy也是符合预期
所以,我们可以确定问题的根源在于需要指定BatchNormalization是在training还是在testing!

实验3.5

3.4中方法虽然解决了我们的问题,但是它是使用构建Model的subclass的方式,而我们之前的MobileNetV2是基于更加灵活Keras Functional API构建的,由于无法控制call()函数的定义,没有办法灵活切换training和testing的状态,另外用Sequential的方式构建时也是一样。
[5]https://blog.keras.io/keras-as-a-simplified-interface-to-tensorflow-tutorial.html
[7]https://github.com/keras-team/keras/issues/7085
[8]https://github.com/keras-team/keras/issues/6752
从5[8]中,我了解到两个情况,

    1. tf.keras.backend.set_learning_phase()可以改变training和testing的状态;
    1. model.updates和layer.updates 存着old_value和new_value的Assign Op

所以我首先尝试:

 tf.keras.backend.set_learning_phase(True)

结果,MobileNetV2构建的模型也可以正常工作了。
而且收敛的速度似乎比model.fit()还快了很多,结合之前model.fit()收敛慢的困惑,这里又增加的一个实验,在model.fit()的版本里面也加上这句话,发现同样收敛速度也变快了!1个epoch就能得到不错的结果了!
因此,这里又产生了一个问题model.fit()到底有没有设learning_phase状态?如果没有是怎么做moving mean和variance的update的?
第二个方法,由于教程中讲述的是如何在1.x的版本构建,而在eager execution模式下,似乎没有办法去run这些Assign Operation。仅做参考吧

update_ops = []for assign_op in model.updates:update_ops.append(assign_op))
#但是不知道拿到这些update_ops在eager execution模式下怎么处理呢?

结论

总结一下,我们从[4]找到了解决问题的启发点,但是最终证明[4]里面的问题和解决方法用到我们这里并不能真正解决问题,问题的关键还是在于Keras+TensorFlow2.0里面我们如何处理在training和testing状态下行为不一致的Layer;以及对于model.fit()和tf.funtion这两种训练方法的区别,最终来看model.fit()里面似乎包含很多诡异的行为。
最终的使用建议如下:

  1. 在使用model.fit()或者model.train_on_batch()这种Keras的API训练模型时,也推荐手动设置tf.keras.backend.set_learning_phase(True),可以加快收敛
  2. 如果使用eager execution这种方法,
  • 1)使用构建Model的subclass,但是针对call()设置training的状态,对于BatchNoramlization,Dropout这样的Layer进行不同处理
  • 2)使用Functional API或者Sequential的方式构建Model,设置tf.keras.backend.set_learning_phase(True),但是注意在testing的时候改变一下状态

最后,为什么TF 2.0的教程里面没有提及这些?默认你已经精通Keras了吗?[捂脸哭]


原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/519075.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

收藏!企业数据安全防护5条建议

引言:数据安全对企业生存发展有着举足轻重的影响,数据资产的外泄、破坏都会导致企业无可挽回的经济损失和核心竞争力缺失,而往往绝大多数中小企业侧重的是业务的快速发展,忽略了数据安全重要性。近年来,企业由于自身的…

容器安全拾遗 - Rootless Container初探

近期Docker 19.03中发布了一个重要的特性 “Rootless Container支持”。趁着五一假期,快速验证一下。本文参考了Experimenting with Rootless Docker 一文的内容,并且补充了更多的细节和上手内容。 Rootless容器背景与架构 Docker和Kubernetes已经成为…

android 参数 attrs.xml,使用attrs.xml自定义属性

控件有很多属性,如android:id、android:layout_width、android:layout_height等,但是这些属性都是系统自带的属性。使用attrs.xml文件,可以自己定义属性。本文在Android自定义控件的基础上,用attrs.xml文件自己定义了属性。首先&a…

mybatis:在springboot中的配置

Mybatis 配置 使全局的映射器启用或禁用缓存。 mybatis.configuration.cache-enabledtrue全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 mybatis.configuration.lazy-loading-enabledtrue当启用时,有延迟加载属性的对象在被调用时将会完…

直面PHP微服务架构挑战

在4月20日的阿里云栖开发者沙龙PHP技术专场上,云智慧Technical VP高驰涛为大家介绍了微服务的前世今生,分享了微服务架构实践中所面对的诸多挑战以及相应的应对策略。 本次直播视频精彩回顾,戳这里! 直播回顾:https://…

5亿整数的大文件,怎么排序 ?面试被问傻!

来源 | 程序员追风编辑 | Carol出品| CSDN云计算(ID:CSDNcloud)最近一家公司,面试官一上来,就问了我这么一个问题,我一脸懵逼,决定记录一下。问题给你1个文件bigdata,大小4663M&…

RabbitMQ 的延时队列和镜像队列原理与实战

在阿里云栖开发者沙龙PHP技术专场上,掌阅资深后端工程师、掘金小测《Redis深度历险》作者钱文品为大家介绍了RabbitMQ的延时队列和镜像队列的原理与实践,重点比较了RabbitMQ提供的消息可靠与不可靠模式,同时介绍了生产环境下如何使用RabbitMQ…

springboot 整合mybatisplus输出sql语句不输出结果集

mybatis-plus: # 日志配置 logging:level:#基础包com.ruoyi: debug#指定报的日志级别org.springframework: warnmybatis-plus:mapper-locations: classpath*:com/gblfy/**/mapping/*.xmlconfiguration: # log-impl: org.apache.ibatis.logging.stdout.StdOutImp…

深入浅出网络编程与Swoole内核

在阿里云PHP技术沙龙专场中,阿里云邀请到php-nsq作者,pecl、Swoole开发组成员吴振宇分享了Swoole进程模型的原理与Swoole协程实现的原理。并结合具体开发案例讲解了Swoole在网络编程中的应用。 本次直播视频精彩回顾,戳这里! 直播…

Spark大数据分布式机器学习处理实战 | 博文精选

作者| 数挖小飞飞编辑 | Carol出品| CSDN云计算(ID:CSDNcloud)Spark是一种大规模、快速计算的集群平台,本文试图通过学习Spark官网的实战演练笔记提升作者实操能力以及展现Spark的精彩之处。本文的参考配置为:Deepin 1…

shell脚本触发java程序传参数

eods.sh nohup java -cp "./xxx.war/WEB-INF/lib/*:./xxx.war/WEB-INF/classes" com.gblfy.datatrans.controller.OdsDeal $1> /app/ods/logs/ods.log &#无参 ./eods.sh #有参 ./eods.sh 2020-12-22public static void main(String args[]) {if (args.leng…

五四,阿里巴巴新青年了解下?

今天,橙子挖掘了几位程序员小哥的故事,他们是淘宝技术节上涌现出的“高手”。为了追求极致,代码也能成为一种艺术,看完之后也许你会对技术人有完全不同的认识。 1 伯灵:“技术作品代表我对技术的态度:解决…

android zxing 自动对焦,ZXing自动对焦问题

Pavel Bobkov..5我在Samsung Galaxy Grand 2(Android 4)上遇到了同样的问题,并且找到了一个解决方案。启动相机之前,我禁用了自动对焦功能。1-2秒后,我启用了它。我尝试了几种方法,并决定定期切换自动对焦功能。为了实现这一点&am…

多场景下的AI疫情防控“天网”:解读云边端联动下的全栈 AI 技术

在全民抗疫的特殊时期下,伴随着春运返潮,企业陆续复工,从重点防控的机场、火车站,到学校、企业、社区等密集型场所,都是不能忽视的地点。除了人工逐一测量体温排查外,我们还发现,在人员复杂、流…

Twitter 宣布抛弃 Mesos,全面转向Kubernetes

美国西部时间 5 月 2 日下午 7 点,Twitter 公司在旧金山总部举行了一次技术发布会兼 Meetup。会上,Twitter 计算平台(Twitter Computing Platform)产品与技术负责人 David McLaughlin 正式宣布,Twitter 的基础而设施将…

Guns整合log4j2

文章目录一、排除内置logback1. 删除logback.xml2. 排除内置的logback二、整合log4j22.1. 依赖log4j22.2. 配置文件中配置2.3. log4j2日志文件2.4. 效果图2.5. 输出要素Sringboot 开源框架默认logback日志框架,Guns开源项目也是用logback日志框架 一、排除内置logba…

蚂蚁金服生产级 Raft 算法库存储模块剖析 | SOFAJRaft 实现原理

前言 SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 SOFAJRaft 存储模块分为: Log 存储记录 Raft 配置变更和用户提交任务日志;Meta 存储即元信息存储记录…

android studio 调用c++,android studio配置opencv,调用c++,处理图像

一、下载安卓的opencv sdk这里下载release版的,下载后解压二、新建工程三、编写界面布局(工程等下上传到github)四、添加Module,Android Studio菜单-->File-->New-->Import Module添加完成五、加入依赖此时Sync报错,需要修改文件修改…

Guns mybatisplus只输出sql不输出结果集

文章目录1. yml配置2. 效果图今天给大家讲一下在数据量特别大的场景下,Guns开源项目怎样实现只输出sql不输出结果集列表。 1. yml配置 mybatis-plus:typeAliasesPackage: cn.stylefeng.guns.modular.system.modelmapper-locations:- classpath*:cn/stylefeng/guns/…

AWS还是Firebase?在移动应用后端应该使用哪个?

作者| Dhananjay Trivedi翻译 | 天道酬勤,编辑 | Carol出品| CSDN云计算(ID:CSDNcloud)我们将按以下顺序比较这两种服务:它们有什么共同点?如何将它们与你的前端集成?它们的优势。它们的价格。创…