首页
小游戏
壁纸
留言
视频
友链
关于
Search
1
上海市第八人民医院核酸检测攻略(时间+预约+报告)-上海
299 阅读
2
上海烟花销售点一览表2022-上海
241 阅读
3
新款的 Thinkbook 16+ 值不值得买?-知乎热搜
219 阅读
4
如何看待网传小米 MIUI 13 内置国家反诈中心 APP?-知乎热搜
214 阅读
5
窦唯到底厉害在哪里?-知乎热搜
192 阅读
免费代理IP
免费翻墙节点
文章聚合
掘金
知乎
IT之家
本地宝
观察者网
金山词霸
搜韵网
新华网
其他
登录
/
注册
Search
标签搜索
知乎热搜
IT之家热榜
广州
深圳
北京
观察者网头条
前端
上海
后端
知乎日报
Android
iOS
人工智能
阅读
工具资源
杭州
诗词日历
每日一句
郑州
设计
看啥
累计撰写
129,720
篇文章
累计收到
46
条评论
首页
栏目
免费代理IP
免费翻墙节点
文章聚合
掘金
知乎
IT之家
本地宝
观察者网
金山词霸
搜韵网
新华网
其他
页面
小游戏
壁纸
留言
视频
友链
关于
搜索到
10477
篇与
的结果
2022-10-20
付费版 VS Code?脑瓜子嗡嗡的吧!-掘金
今天看到一张图,分享给小伙伴看一下:脑瓜子嗡嗡的吧!这不比每周四 v me 50, xx kfc 来的快,还是给你打折限时活动价,由 google 正版授权的微软开源 vscode。并且还应该会有人买吧!可能是为了知识付费。想不到吧!这还上了 github,还有人专门给 vscode github 提了 issue:https://github.com/microsoft/vscode/issues/163798更让我长知识的是,竟然这样做,唯一侵犯的只有商标权?是不是又发现了新的财路。不过下面也有人反驳他,具体对不对,我也不知道。我使用 Mac 搜索 VS Code 前几条搜索结果倒不是广告,难道是 Windows 才会这样么?感兴趣的小伙伴可以去看看。不过说真的,下载软件开发,也就是骗骗小白,不过这钱赚的,有点考验智商!看来反诈 APP 有必要出 PC 版了,看留言区,竟然有人为了装 vscode、java 等,还专门装了 xxx 软件管家。最后,补充一下,下载软件去官网就行,真的不用脑瓜子嗡嗡的!网络不行,找朋友、网友、群友传一个安装包也比 xxx度 靠谱。本文由mdnice多平台发布以上文章来自[掘金]-[程序员小航]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
Luminar Neo教程,如何使用 Luminar Share 导入图像?-掘金
欢迎观看 Luminar Neo中文版教程,小编带大家学习 Luminar Neo的基本工具和使用技巧,了解如何使用 Luminar Share 导入图像。有时想将智能手机上捕获的图像发送到 Luminar Neo 进行编辑。按照以下步骤连接 Luminar Neo 和 Luminar Share 以传输图像。打开 Luminar Neo 选择「文件」-「分享至」,然后选择「连接」以显示 Luminar Neo 中的 QR 码。在设备上启动 Luminar Share 应用程序并扫描屏幕上的二维码连接外部设备。 从 Luminar Share 应用程序将照片发送到 Luminar Neo。浏览设备以查找要传输的图像,可以选择单个或多个图像,按 Select 将图像传输到 Luminar Neo,发送到 Luminar Neo以确认选择。以上就是在 Luminar Neo 中使用 Luminar Share 导入图像的方法。以上文章来自[掘金]-[Mac121]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
高性能图片优化方案-掘金
目录介绍 01.图片基础概念介绍 1.1 图片占用内存介绍 1.2 加载网络图片流程 1.3 三方库加载图片逻辑 1.4 从网络直接拉取图片 1.5 加载图片的流程 1.6 Bitmap能直接存储吗 1.7 Bitmap创建流程 1.8 图片框架如何设计 02.图片内存计算方式 2.1 如何计算占用内存 2.2 上面计算内存对吗 2.3 一个像素占用内存 2.4 使用API获取内存 2.5 影响Bitmap内存因素 2.6 加载xhdpi和xxhdpi图片 2.7 图片一些注意事项 03.大图的内存优化 3.1 常见图片压缩 3.2 图片尺寸压缩 3.3 图片质量压缩 3.4 双线性采样压缩 3.5 高清图分片加载 3.6 图片综合压缩 04.色彩格式及内存优化 4.1 RGB颜色种类 4.2 ARGB色彩模式 4.3 改变色彩格式优化 05.缓存的使用实践优化 5.1 Lru内存缓存 5.2 Lru内存注意事项 5.3 使用Lru磁盘缓存 06.不同版本对Bitmap管理 6.1 演变进程 6.2 管理Bitmap内存 6.3 提高Bitmap复用 07.图片其他方面优化 7.1 减少PNG图片的使用 7.2 控件切割圆角优化 7.3 如何给图片置灰色 7.4 如何处理图片旋转呢 7.5 保存图片且刷相册 7.6 统一图片域名优化 7.7 优化H5图片加载 7.8 优化图片阴影效果 7.9 图片资源的压缩 01.图片基础概念介绍1.1 图片占用内存介绍 移动设备的系统资源有限,所以应用应该尽可能的降低内存的使用。 在应用运行过程中,Bitmap (图片)往往是内存占用最大的一个部分,Bitmap 图片的加载和处理,通常会占用大量的内存空间,所以在操作 Bitmap 时,应该尽可能的小心。 Bitmap 会消耗很多的内存,特别是诸如照片等内容丰富的大图。 例如,一个手机拍摄的 2700 * 1900 像素的照片,需要 5.1M 的存储空间,但是在图像解码配置 ARGB_8888 时,它加载到内存需要 19.6M 内存空间(2592 * 1936 * 4 bytes),从而迅速消耗掉该应用的剩余内存空间。 OOM 的问题也是我们常见的严重问题,OOM 的产生的一个主要场景就是在大图片分配内存的时候产生的,如果 APP 可用内存紧张,这时加载了一张大图,内存空间不足以分配该图片所需要的内存,就会产生 OOM,所以控制图片的高效使用是必备技能。 1.2 加载网络图片流程 这一部分压缩和缓存图片,在glide源码分析的文章里已经做出了比较详细的说明。在这里简单说一下图片请求加载过程…… 在使用App的时候,会经常需要加载一些网络图片,一般的操作步骤大概是这样的: 第一步从网络加载图片:一般都是通过网络拉取的方式去服务器端获取到图片的文件流后,再通过BitmapFactory.decodeStream(InputStream)来加载图片Bitmap。 第二步这种压缩图片:网络加载图片方式加载一两张图片倒不会出现问题,但是如果短时间内加载十几张或者几十张图片的时候,就很有可能会造成OOM(内存溢出),因为现在的图片资源大小都是非常大的,所以我们在加载图片之前还需要进行相应的图片压缩处理。 第三步变换图片:比如需要裁剪,切割圆角,旋转,添加高斯模糊等属性。 第四步缓存图片:但又有个问题来了,在使用移动数据的情况下,如果用户每次进入App的时候都会去进行网络拉取图片,这样就会非常的浪费数据流量,这时又需要对图片资源进行一些相应的内存缓存以及磁盘缓存处理,这样不仅节省用户的数据流量,还能加快图片的加载速度。 第五步异步加载:虽然利用缓存的方式可以加快图片的加载速度,但当需要加载很多张图片的时候(例如图片墙瀑布流效果),就还需用到多线程来加载图片,使用多线程就会涉及到线程同步加载与异步加载问题。 1.3 三方库加载图片逻辑 先说出结论,目前市面较为常用的大概是Glide,Picasso,Fresco等。大概的处理图片涉及主要逻辑有: 从网络或者本地等路径拉取图片;然后解码图片;然后进行压缩;接着会有图片常用圆角,模糊或者裁剪等处理;然后三级缓存加载的图片;当然加载图片过程涉及同步加载和异步加载;最后设置到具体view控件上。 1.4 从网络直接拉取图片 直接通过网络请求将网络图片转化成bitmap 在这将采用最原生的网络请求方式HttpURLConnection方式进行图片获取。 经过测试,请求8张图片,耗时毫秒值174。一般是通过get请求拉取图片的。这种方法应该是最基础的网络请求,大家也可以回顾一下,一般开发中很少用这种方式加载图片。具体可以看:ImageToolLib 如何加载一个图片呢? 可以看看BitmapFactory类为我们提供了四类方法来加载Bitmap:decodeFile、decodeResource、decodeStream、decodeByteArray;也就是说Bitmap,Drawable,InputStream,Byte[] 之间是可以进行转换。 1.5 加载图片的流程 搞清楚一个图片概念 在电脑上看到的 png 格式或者 jpg 格式的图片,png(jpg) 只是这张图片的容器。是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示。 加载图片显示到手机 通过代码,将这张图片加载进内存时,会先解析(也就是解码操作)图片文件本身的数据格式,然后还原为位图,也就是 Bitmap 对象。 图片大小vs图片内存大小 一张 png 或者 jpg 格式的图片大小,跟这张图片加载进内存所占用的大小完全是两回事。 1.6 Bitmap能直接存储吗 Bitmap基础概念 Bitmap对象本质是一张图片的内容在手机内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值。每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来了。 Bitmap本质上不能直接存储 为什么?bitmap是一个对象,如果要存储成本地可以查看的图片文件,则必须对bitmap进行编码,然后通过io流写到本地file文件上。 1.7 Bitmap创建流程 对于图片OOM,可以发现一个现象。 heapsize(虚拟机的内存配置)越大越不容易 OOM,Android8.0 及之后的版本更不容易 OOM,这个该如何理解呢? Bitmap对象内存的变化: 在 Android 8.0 之前,Bitmap 像素占用的内存是在 Java heap 中分配的;8.0 及之后,Bitmap 像素占用的内存分配到了 Native Heap。 由于 Native heap 的内存分配上限很大,32 位应用的可用内存在 3~4G,64 位上更大,虚拟内存几乎很难耗尽,所以推测 OOM 时 Java heap 中占用内存较多的对象是 Bitmap” 成立的情况下,应用更不容易 OOM。 搞清楚Bitmap对象内存分配 Bitmap 的构造方法是不公开的,在使用 Bitmap 的时候,一般都是通过 Bitmap、BitmapFactory 提供的静态方法来创建 Bitmap 实例。 以 Bitmap.createBitmap 说明了 Bitmap 对象的主要创建过程分析,可以看到 Java Bitmap 对象是在 Native 层通过 NewObject 创建的。 allocateJavaPixelRef,是 8.0 之前版本为 Bitmap 像素从 Java heap 申请内存。其核心原理是Bitmap 的像素是保存在 Java 堆上。 allocateHeapBitmap,是 8.0 版本为 Bitmap 像素从 Native heap 申请内存。其核心原理主要是通过 calloc 为 Bitmap 的像素分配内存,这个分配就在 Native 堆上。 更多详细内容可以看:Bitmap对象内存分配 1.8 图片框架如何设计 大多数图片框架加载流程 概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,显示等操作。 图片框架是如何设计的 封装参数:从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程; 解析路径:图片的来源有多种,格式也不尽相同,需要规范化;比如glide可以加载file,io,id,网络等各种图片资源 读取缓存:为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可; 查找文件/下载文件:如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;比如glide这块是发起一个请求 解码:这一步是整个过程中最复杂的步骤之一,有不少细节;比如glide中解析图片数据源,旋转方向,图片头等信息 变换和压缩:解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等),还要做图片压缩; 缓存:得到最终bitmap之后,可以缓存起来,以便下次请求时直接取结果;比如glide用到三级缓存 显示:显示结果,可能需要做些动画(淡入动画,crossFade等);比如glide设置显示的时候可以添加动画效果 02.图片内存计算方式2.1 如何计算占用内存 如果图片要显示下Android设备上,ImageView最终是要加载Bitmap对象的,就要考虑单个Bitmap对象的内存占用了,如何计算一张图片的加载到内存的占用呢?其实就是所有像素的内存占用总和: bitmap内存大小 = 图片长度 x 图片宽度 x 单位像素占用的字节数 起决定因素就是最后那个参数了,Bitmap常见有2种编码方式:ARGB_8888和RGB_565,ARGB_8888每个像素点4个byte,RGB_565是2个byte,一般都采用ARGB_8888这种。那么常见的1080*1920的图片内存占用就是:1920 x 1080 x 4 = 7.9M 2.2 上面计算内存对吗 我看到好多博客都是这样计算的,但是这样算对吗?有没有哥们试验过这种方法正确性?我觉得看博客要对博主表示怀疑,论证别人写的是否正确。 说出我的结论:上面2.1这种说法也对,但是不全对,没有说明场景,同时也忽略了一个影响项:Density。接下来看看源代码。 inDensity默认为图片所在文件夹对应的密度;inTargetDensity为当前系统密度。 加载一张本地资源图片,那么它占用的内存 = width * height * nTargetDensity/inDensity 一个像素所占的内存。 @Nullable public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); } 正确说法,这个注意呢?计算公式如下所示 对资源文件:width * height * nTargetDensity/inDensity * nTargetDensity/inDensity * 一个像素所占的内存; 别的:width * height * 一个像素所占的内存; 2.3 一个像素占用内存 一个像素占用多大内存?Bitmap.Config用来描述图片的像素是怎么被存储的? ARGB_8888: 每个像素4字节. 共32位,默认设置。 Alpha_8: 只保存透明度,共8位,1字节。 ARGB_4444: 共16位,2字节。 RGB_565:共16位,2字节,只存储RGB值。 2.4 使用API获取内存 Bitmap使用API获取内存 getByteCount() getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。 getAllocationByteCount() API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。 思考: getByteCount()与getAllocationByteCount()的区别? 一般情况下两者是相等的;通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。 在复用Bitmap的情况下,getAllocationByteCount()可能会比getByteCount()大。 2.5 影响Bitmap内存因素 影响Bitmap占用内存的因素: 图片最终加载的分辨率; 图片的格式(PNG/JPEG/BMP/WebP); 图片所存放的drawable目录; 图片属性设置的色彩模式; 设备的屏幕密度; 2.6 加载xhdpi和xxhdpi图片 提个问题,加载xhdpi和xxhdpi中相同的图片,显示在控件上会一样吗?内存大小一样吗?为什么? 肯定是不一样的。xhdpi:240dpi--320dpi,xxhdpi:320dpi--480dpi, app中设置的图片是如何从hdpi中查找的? 首先计算dpi,比如手机分辨率是1920x1080,5.1寸的手机。那么得到的dpi公式是(√ ̄1920² + 1080²)/5.1 =2202/5.1= 431dpi。这样优先查找xxhdpi 如果xxhdpi里没有查找图片,如果没有会往上找,遵循“先高再低”原则。如果xhdpi里有这个图片会使用xhdpi里的图片,这时候发现会比在xhdpi里的图片放大了。 为何要引入不同hdpi的文件管理? 比如:xxhdpi放94x94,xhdpi放74x74,hdpi放45x45,这样不管是什么样的手机图片都能在指定的比例显示。引入多种hdpi是为了让这个图片在任何手机上都是手机的这个比例。 2.7 图片一些注意事项 同样图片显示在大小不相同的ImageView上,内存是一样吗? 图片占据内存空间大小与图片在界面上显示的大小没有关系。 图片放在res不同目录,加载的内存是一样的吗? 最终图片加载进内存所占据的大小会不一样,因为系统在加载 res 目录下的资源图片时,会根据图片存放的不同目录做一次分辨率的转换,而转换的规则是:新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi ) 03.大图的内存优化3.1 常见图片压缩 常见压缩方法Api Bitmap.compress(),质量压缩,不会对内存产生影响; BitmapFactory.Options.inSampleSize,内存压缩; Bitmap.compress()质量压缩 质量压缩,不会对内存产生影响。它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。进过它压缩的图片文件大小会变小,但是解码成bitmap后占得内存是不变的。 BitmapFactory.Options.inSampleSize内存压缩 解码图片时,设置BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高。而设置BitmapFactory.Options的inSampleSize属性可以真实的压缩Bitmap占用的内存,加载更小内存的Bitmap。 设置inSampleSize之后,Bitmap的宽、高都会缩小inSampleSize倍。例如:一张宽高为2048x1536的图片,设置inSampleSize为4之后,实际加载到内存中的图片宽高是512x384。占有的内存就是0.75M而不是12M,足足节省了15倍。 备注:inSampleSize值的大小不是随便设、或者越大越好,需要根据实际情况来设置。inSampleSize比1小的话会被当做1,任何inSampleSize的值会被取接近2的幂值。 3.2 图片尺寸压缩3.2.1 如何理解尺寸压缩 通常在大多数情况下,图片的实际大小都比需要呈现的尺寸大很多。 例如,我们的原图是一张 2700 * 1900 像素的照片,加载到内存就需要 19.6M 内存空间,但是,我们需要把它展示在一个列表页中,组件可展示尺寸为 270 * 190,这时,我们实际上只需要一张原图的低分辨率的缩略图即可(与图片显示所对应的 UI 控件匹配),那么实际上 270 * 190 像素的图片,只需要 0.2M 的内存即可。 可以看到,优化前后相差了 98 倍,原来显示 1 张,现在可以显示 98 张图片,效果非常显著。 既然在对原图缩放可以显著减少内存大小,那么我们应该如何操作呢? 先加载到内存,再进行操作吗,可以如果先加载到内存,好像也不太对,这样只接占用了 19.6M + 0.2M 2份内存了,而我们想要的是,在原图不加载到内存中,只接将缩放后的图片加载到内存中,可以实现吗? BitmapFactory 提供了从不同资源创建 Bitmap 的解码方法: decodeByteArray()、decodeFile()、decodeResource() 等。但是,这些方法在构造位图的时候会尝试分配内存,也就是它们会导致原图直接加载到内存了,不满足我们的需求。我们可以通过 BitmapFactory.Options 设置一些附加的标记,指定解码选项,以此来解决该问题。 如何操作呢?答案来了:将 inJustDecodeBounds 属性设置为 true,可以在解码时避免内存的分配,它会返回一个 null 的 Bitmap ,但是可以获取 outWidth、outHeight 和 outMimeType 值。利用该属性,我们就可以在图片不占用内存的情况下,在图片压缩之前获取图片的尺寸。 怎样才能对图片进行压缩呢? 通过设置BitmapFactory.Options中inSampleSize的值就可以实现。其计算方式大概就是:计算出实际宽高和目标宽高的比率,然后选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高。 3.2.2 设置BitmapFactory.Options属性 大概步骤如下所示 要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。注意这个地方是核心,这个解析图片并没有生成bitmap对象(也就是说没有为它分配内存控件),而仅仅是拿到它的宽高等属性。 然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。这一步会压缩图片。 之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。此时才正式创建了bitmap对象,由于前面已经对它压缩了,所以你会发现此时所占内存大小已经很少了。 具体的实现代码public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } 思考:inJustDecodeBounds这个参数是干什么的? 如果设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的参数填充到option对象里,这样我们就可以在不生成bitmap而获取到图像的相关参数了。 为何设置两次inJustDecodeBounds属性? 第一次:设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的参数填充到option对象里,这样我们就可以在不生成bitmap而获取到图像的相关参数。 第二次:将inJustDecodeBounds设置为false再次调用decode函数时就能生成bitmap了。而此时的bitmap已经压缩减小很多了,所以加载到内存中并不会导致OOM。 3.3 图片质量压缩 在Android中,对图片进行质量压缩,通常我们的实现方式如下所示://quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大 bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream); 在上述代码中,我们选择的压缩格式是CompressFormat.JPEG,除此之外还有两个选择: 其一,CompressFormat.PNG,PNG格式是无损的,它无法再进行质量压缩,quality这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化; 其二,CompressFormat.WEBP,这个格式是google推出的图片格式,它会比JPEG更加省空间,经过实测大概可以优化30%左右。 Android质量压缩逻辑,函数compress经过一连串的java层调用之后,最后来到了一个native函数: 具体看:Bitmap.cpp,最后调用了函数encoder->encodeStream(…)编码保存本地。该函数是调用skia引擎来对图片进行编码压缩。 3.4 双线性采样压缩 双线性采样(Bilinear Resampling)在 Android 中的使用方式一般有两种:bm = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true); //或者直接使用 matrix 进行缩放 Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true); 看源码可以知道createScaledBitmap函数最终也是使用第二种方式的matrix进行缩放 双线性采样使用的是双线性內插值算法,这个算法不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围2x2个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。 3.5 高清图分片加载 适用场景 : 当一张图片非常大 , 在手机中只需要显示其中一部分内容 , BitmapRegionDecoder 非常有用 。 主要作用 : BitmapRegionDecoder 可以从图像中 解码一个矩形区域 。相当于手在滑动的过程中,计算当前显示区域的图片绘制出来。 基本使用流程 : 先创建,后解码 。调用 newInstance 方法 , 创建 BitmapRegionDecoder 对象 ;然后调用 decodeRegion 方法 , 获取指定 Rect 矩形区域的解码后的 Bitmap 对象 3.6 图片综合压缩 一般情况下图片综合压缩的整体思路如下: 第一步进行采样率压缩; 第二步进行宽高的等比例压缩(微信对原图和缩略图限制了最大长宽或者最小长宽); 第三步就是对图片的质量进行压缩(一般75或者70); 第四步就是采用webP的格式。 关于图片压缩的综合案例如下 具体可以参考:CompressServer 04.色彩格式及内存优化4.1 RGB颜色种类 RGB 色彩模式是工业界的一种颜色标准 通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是运用最广的颜色系统之一。Android 中,像素的存储方式使用的色彩模式正是 RGB 色彩模式。 4.2 ARGB色彩模式 在 Android 中,我们常见的一些颜色设置,都是 RGB 色彩模式来描述像素颜色的,并且他们都带有透明度通道,也就是所谓的 ARGB。例如,我们常见的颜色定义如下://在代码中定义颜色值:蓝色 public final int blue=0xff0000ff; //或者在xml中定义: #ff0000ff 以上设置中,颜色值都是使用 16 进制的数字来表示的。以上颜色值都是带有透明度(透明通道)的颜色值,格式是 AARRGGBB,透明度、红色、绿色、蓝色四个颜色通道,各占有 2 位,也就是一个颜色通道,使用了 1 个字节来存储。 4.3 改变色彩格式优化 Android 中有多种 RGB 模式,我们可以设置不同的格式,来控制图片像素颜色的显示质量和存储空间。 Android.graphics.Bitmap 类里有一个内部类 Bitmap.Config 类,它定义了可以在 Android 中使用的几种色彩格式:public enum Config { ALPHA_8 (1), RGB_565 (3), @Deprecated ARGB_4444 (4), ARGB_8888 (5), RGBA_F16 (6), HARDWARE (7); } 解释一下这几个值分别代表了什么含义?我们已经知道了:A 代表透明度、R 代表红色、G 代表绿色、B 代表蓝色。 ALPHA_8:表示,只存在 Alpha 通道,没有存储色彩值,只含有透明度,每个像素占用 1 个字节的空间。 RGB_565:表示,R 占用 5 位二进制的位置,G 占用了6位,B 占用了 5 位。每个像素占用 2 个字节空间,并且不包含透明度。 ARGB_4444:表示,A(透明度)、R(红色)、G(绿色)、B(蓝色)4个通道各占用 4 个 bit 位。每个像素占用 2 个字节空间。 ARGB_8888:表示,A(透明度)、R(红色)、G(绿色)、B(蓝色)4个通道各占用 8 个 bit 位。每个像素占用 4 个字节空间。 RGBA_F16:表示,每个像素存储在8个字节上。此配置特别适合广色域和HDR内容。 HARDWARE:特殊配置,当位图仅存储在图形内存中时。 此配置中的位图始终是不可变的。 那么开发中一般选择哪一种比较合适呢 Android 中的图片在加载时,默认的色彩格式是 ARGB_8888,也就是每个像素占用 4 个字节空间,一张 2700 * 1900 像素的照片,加载到内存就需要 19.6M 内存空间(2592 * 1936 * 4 bytes)。 如果图片在 UI 组件中显示时,不需要太高的图片质量,例如显示一张缩略图(不透明图片)等场景,这时,我们就没必要使用 ARGB_8888 的色彩格式了,只需要使用 RGB_565 模式即可满足显示的需要。 那么,我们的优化操作就可以是: 将 2700 * 1900 像素的原图,压缩到原图的低分辨率的缩略图 270 * 190 像素的图片,这时需要 0.2M 的内存。也就是从 19.6M内存,压缩为 0.2 M内存。 我们还可以进一步优化色彩格式,由 ARGB_8888 改为 RGB_565 模式,这时,目标图片需要的内存就变为 270 * 190 * 2 = 0.1M 了。图片内存空间又减小了一倍。 05.缓存的使用实践优化5.1 Lru内存缓存 LruCache 类特别适合用来缓存 Bitmap,它使用一个强引用的 LinkedHashMap 保存最近引用的对象,并且在缓存超出设定大小时,删除最近最少使用的对象。 给 LruCache 确定一个合适的缓存大小非常重要,我们需要考虑几个因素: 应用剩余多少可用内存? 需要有多少张图片同时显示到屏幕上?有多少图片需要准备好以便马上显示到屏幕? 设备的屏幕大小和密度是多少?高密度的设备需要更大的缓存空间来缓存同样数量的图片。 Bitmap 的尺寸配置是多少,花费多少内存? 图片被访问的频率如何?如果其中一些比另外一些访问更频繁,那么我们可能希望在内存中保存那些最常访问的图片,或者根据访问频率给 Bitmap 分组,为不同的 Bitmap 组设置多个 LruCache 对象。 是否可以在缓存图片的质量和数量之间寻找平衡点?有时,保存大量低质量的 Bitmap 会非常有用,加载更高质量的图片的任务可以交给另外一个后台线程处理。 缓存太小会导致额外的花销却没有明显的好处,缓存太大同样会导致 java.lang.OutOfMemory 的异常,并且使得你的程序只留下小部分的内存用来工作(缓存占用太多内存,导致其他操作会因为内存不够而抛出异常)。所以,我们需要分析实际情况之后,提出一个合适的解决方案。 LruCache是Android提供的一个缓存类,通常运用于内存缓存 LruCache是一个泛型类,它的底层是用一个LinkedHashMap以强引用的方式存储外界的缓存对象来实现的。 为什么使用LinkedHashMap来作为LruCache的存储,是因为LinkedHashMap有两种排序方式,一种是插入排序方式,一种是访问排序方式,默认情况下是以访问方式来存储缓存对象的;LruCache提供了get和put方法来完成缓存的获取和添加,当缓存满时,会将最近最少使用的对象移除掉,然后再添加新的缓存对象。如下源码所示,底层是LinkedHashMap。 public LruCache(int maxSize) { if (maxSize
2022年10月20日
3 阅读
0 评论
0 点赞
2022-10-20
什么是梯度消失?为什么会存在梯度消失问题?-掘金
首先,你需要了解什么是BP神经网络,最简单的一个BP网络模型如下图所示,由输入层、隐藏层、输出层组成。对于BP神经网络,我们之前也说过,它的基本思想就是:学习过程由信号的正向传播和误差的反向传播两个过程组成。从上图BP网络模型中,我们可以看出,在正向传播时,BP网络将样本的特征从输入层进行输入,信号经过各个隐藏层逐层处理后,最后从输出层传出。然后再进行误差的反向传播,这部分图中不可见,它的计算过程是:计算网络的实际输出与期望输出之间的误差,然后将误差信号从最后一层逐层反传,从而获得各个层的误差学习信号,再根据误差学习信号来修正各个层神经元的权值,以此优化网络减小实际输出与期望输出之间的误差。而这个误差就是我们常说的损失函数(代价函数);修正权值优化网络的过程,我们也可以称之为损失函数最小化的过程;将误差逐层反转以此来修正权重w的过程,我们可以称之为梯度下降法。梯度在了解梯度下降法之前,我先来说一下什么是梯度。梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模)。梯度定义如下:对于 $f(x_0,...,x_i,...,x_n)$ 上的某一点来说存在很多个 方向导数,梯度的方向是函数 $f(x_0,...,x_i,...x_n)$ 在某一点上增长最快的方向,梯度的模则是该点上 方向导数 的最大值 ,梯度的模等于注意: 梯度是一个向量,即有方向有大小; 梯度的方向是最大 方向导数 的方向; 梯度的值是最大 方向导数 的值 方向导数定义如下:导数和偏导数,都是沿坐标轴正方向讨论函数的变化率。那么当我们讨论函数沿任意方向的变化率时,也就引出了方向导数的定义,即:某一点在某一趋近方向上的导数值。换言之就是,我们不仅要知道函数在坐标轴正方向上的变化率(即偏导数),而且还要设法求得函数在其他特定方向上的变化率,而方向导数就是函数在其他特定方向上的变化率。梯度下降法梯度下降法定义:在变量空间的某一点处,函数沿梯度方向具有最大的变化率,那么在优化代价函数的时候,就可以沿着负梯度方向去减小代价函数的值。代价函数的值越小,说明模型的预测值越接近真实标签的值,模型训练的越好。而代价函数中的预测值 y 是跟神经网络中的参数 w 和 b 相关的,所以我们使用 梯度下降法 来不断优化w和b的值。那么下面我通过一个例子具体讲解梯度下降法是怎么不断优化网络的。这里我先假设一个简单的情况,假设神经网络中只有一个参数w,并且 w 初始值是 -3 ,我们使用梯度下降法来优化 w 的取值,以使得loss值不断减小,那么 参数w和代价函数loss的关系,并且 w=-3 时的梯度 如下图所示。从图中,我们可以看出,当 w 为 -3 时,w 所处位置的梯度是一个负数,但是梯度下降法在优化代价函数的时候,是沿着负梯度方向的去减小代价函数的值,所以负梯度是一个正数,那么 w 的值会变大。梯度下降法的优化公式如下:𝑤 = 𝑤 − 𝜂(𝜕𝑓/𝜕𝑤)其中,学习率 𝜂 一般是一个 大于0 的数,𝜕𝑓/𝜕𝑤 为负数,那么就可以判断出 w 的值会变大。变大的数值 跟学习率大小𝜂有关,也跟 函数f 在 w 处的梯度大小有关。假设 w 变大移动到了 w=2 的位置,我们需要再次计算 w=2 时的梯度,如下图所示当 w=2 时,w 所处位置的梯度是一个正数,所以负梯度是一个负数,那么w的值会变小,从上面公式也可以推断,这里不再过多赘述。我们还可以发现此时loss值会比上一次的loss值小。通过这个例子,我们可以发现不管 w 处于什么位置,当 w 向着负梯度的方向进行移动时,实际上就是向着可以使loss 值减小的方向进行移动。这就有点类似于一个小球在山坡上面,它总是往坡底的方向进行移动,只不过它每一次是移动一步,这个步子的大小会受到学习率和所处位置梯度的大小所影响。梯度消失首先我先给出梯度消失的定义:在梯度下降法中, 随着算法反向的反馈, 梯度会越来越小,最终归零没有变化,但此时并没有收敛到比较好的解,这就是梯度消失的问题。下面我们通过权值调整的公式具体体会一下上面这句话的意思,权值调整公式如下:其中,$ΔW^h$ 表示第h层权值矩阵w的变化,𝜂表示学习率,𝜹_h 表示第h层的学习信号,它的改变会引起权值矩阵的改变。1)输出层的学习信号公式为:其中,T 表示数据的标签值(已知的),表示模型的预测值(正向传播计算得到), 表示激活函数的导数,表示输出层信号的汇总;2)除了输出层外,剩下的网络层的学习信号的公式都为:可以看出,第 h 层的学习信号,跟它下一层 h+1层的学习信号、权值矩阵的转置 以及 相关。那么我们就可有以下结论,激活函数的导数会影响学习信号𝛿 的值(由学习信号公式得出),而学习信号𝛿 的值会影响权值调整Δ𝑊 的值(由权值调整公式得出)。所以如果激活函数的值越大,Δ𝑊的值就越大;如果激活函数的值越小,Δ𝑊的值就越小。现在我们再来看下之前讲解过的三种 S型激活函数 的导数以及其图像。(1)sigmoid函数的导数公式:导数图像为:(2)tanh函数的导数公式:导数图像为:(3)softsign 函数的导数公式:导数图像为:总结:基于这三个 S型激活函数的导数图像,我们可以发现,当 x 取值较大或较小时,这三种激活函数的导数很快就趋向于0,那么这就会使得学习信号接近于0,从而使得权值 Δ𝑊调整 接近于0。Δ𝑊接近于0 那就意味着该层的参数不会发生改变,不能进行优化。参数不能优化,那整个网络就不能再进行学习了,这也就造成了梯度消失(Vanishing Gradient)问题。以上文章来自[掘金]-[欣xy]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
不用防抖和节流,用更底层的方式解决JS的重复请求-掘金
欢迎转载,评论,然后注明来源和github地址即可。如果你认为本文或本工具对你有帮助,请点赞收藏或给项目star,这对我真的很重要。ヾ(•ω•`)o项目 github 地址你或许在项目中遇到过这样的情况。 成员A成员B都用得上一个后端接口api,但它们互相不知道对方什么时候请求这个接口,因此导致一打开页面,同一个接口竟然重复请求了多次。 由于用户手抖,又因为成员忘记做请求的loading防误触处理,导致一个接口被用于疯狂请求,最终数据乱套,页面不可用。 SPA单页面应用,多个页面甚至是多个组件可能有同样的数据请求,完全可以共享的数据却不得不重复请求,影响页面加载效率。 想要用节流或者防抖解决上面的问题,但是后端返回数据的时间浮动太大,导致不知道应该设置多长的时间。 这些请求浪费,实际上都有调用异步函数(async function)的参与的;因此,它们虽不是async function的问题,但却可以利用async function的特点来解决。async function本质上是一个Promise。因此只要利用好Promise的特性,就能解决这些问题。once-init 正是为解决这些问题而生。它从 Promise 的定义出发,用 Promise 的基础功能彻底地阻止了异步请求浪费的发生。我用它做了两件事: 缓存请求的返回值; 缓存Promise请求本身; 原理once-init 的核心思想是缓存和执行队列;缓存返回值实现缓存返回值并不困难,只要写一个单例模式就好了。下面是一个缓存的单例模式的简单示例;class OnceInit { cache = undefined; func; constructor(asyncFunc) { this.func = asyncFunc; } async init() { if (typeof this.cache !== 'undefined') { return this.cache; } else { const res = await this.func(); this.cache = res; return res; } } } // 使用 const oiFoo = new OnceInit(someAsyncFunc); await oiFoo.init(); 如果缓存已经有值,返回缓存的值; 如果缓存没有值,执行异步函数;执行完毕后,更新缓存; 这是一个简易的解决方案,它大概能解决10%的异步函数相关的问题,因为在第一次执行Promise完成之后,就不会再进行请求,也就不会产生浪费了;但是,它没有解决多个Promise同时发生的情况。假设开发人员同一时间多次调用init,如果第一次调用的Promise还没有完成,cache也还没有初始化,就会导致同一时间的所有调用依旧创建新的Promise。甚至有可能因为多次请求,不断的变化cache,你甚至没有办法确定最后cache的值是不是你最后一次请求的返回值。如果要解决这个问题,就需要利用Promise的特性,同一时间,同一个async function,只允许同一个Promise处在pending状态。缓存 Promise 如果Promise正在执行,就不创建新的Promise;直接返回正在执行的Promise的返回值; 如果没有Promise正在执行,就创建并缓存新的Promise; Promise执行结束之后,删除缓存的Promise; class OnceInit { cache = undefined; func; promise = undefined; constructor(asyncFunc) { this.func = asyncFunc; } async init() { if (typeof this.cache !== 'undefined') { return this.cache; } else { if (this.promise) { return await this.promise; } else { const promise = this.func(); promise.finally(() => { this.promise = undefined; }) this.promise = promise; const res = await promise; this.cache = res; return res; } } } } 通过这种方式,就能避免promise同一时间重复执行。这也是once-init这个库的核心思想。当然这个简单实现还有很多问题需要解决。 如果想要刷新缓存怎么办; 如果asyncFunc需要参数怎么办; 怎样提供Typescript支持; 不过这些问题 once-init 都已经解决了。如果你读过我的上一篇文章https://juejin.cn/post/7046667393405304868,就知道我在年初就写了一个库封装这个想法。然而很多伙伴都对我的实现提出了有效的建议,经过这一段时间的打磨,并在实际生产环境中试用了一段时间后,现在终于推出了它的v1.0.0正式版本了。once-init 🗼 Makes asynchronous function execution manageable.封装可控的 async function。你可以让同一个 async function 不会在同一时间内被执行两次,以防止发出重复的请求。你可以让第二次执行 async function ,直接返回第一次执行的结果,而不是重复执行函数。解决大量的相同请求的问题。详细且精确的 Typescript 检查。安装npm install once-init 简介once-init 的核心思想是缓存和执行队列;使用// 0. 引入once-init import oi from "once-init"; // 1. 创建一个异步函数 async function foo() { // do something, for example, request backend data. const res = await axios.get("xxx.com"); return res; } // 2. 用once-init封装这个异步函数 const oiFoo = oi(foo); // 3. 执行封装后的函数 oiFoo.init(); 用例一个结合axios的简单示例import oi from "once-init"; axios.get = oi(axios.get).refresh; 只用一行,就能在调用axios.get的时候就能阻止同一时间的重复请求了。不用 once-init// 我们假设 axios.get("xxx.com") 返回的值是一个递增的数字,即第1次请求,会返回1,第2次请求会返回2,第n次请求会返回n。 await foo(); // 返回 1 await foo(); // 返回 2 await foo(); // 返回 3 使用 once-init// once-init 会将重复执行重定向到第一次执行的结果上;(第一次执行后会缓存执行结果,类似单例模式) await oiFoo.init(); // 返回 1 await oiFoo.init(); // 返回 1 await oiFoo.init(); // 返回 1 这意味着无论重复执行 oiFoo.init 多少次,foo 都只会执行第一次,返回第一次执行的结果;(就像缓存一样)await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] // 通常,如果你只会使用到init,你可以直接把 oiFoo 定义成 init 函数 const oiFoo = oi(foo).init; await oiFoo(); 如果你不使用缓存,只是希望防止同一时间发出重复请求,你可以使用refresh:// refresh和init在同一时间执行多次,都会阻止重复执行,多余的async function会返回第一次的结果; await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [1, 1, 1] // 但refresh如果当前没有其它重复的async function在执行,会刷新结果,并同时刷新缓存(影响到下一次init的返回); await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [2, 2, 2] await oiFoo.init(); // 返回 2 once-init 会区分参数,如果传入的异步函数有参,那么传入不同的参数将被视为两个不同的异步函数,不会共享缓存和执行队列;(使用lodash.isEqual判断参数是否相等)下面这个复杂用例将会给你提供灵感:// 假设 xxx.com/+ 会返回正数, xxx.com/- 会返回负数,两者有独立的缓存,且绝对值都递增 async function foo(op: "+" | "-") { const res = await axios.get(`xxx.com/${op}`); return res; } const oiFoo = oi(foo); await oiFoo.init("-"); // 返回 -1 await oiFoo.refresh("-"); // 返回 -2 await oiFoo.refresh("-"); // 返回 -3 await oiFoo.refresh("+"); // 返回 1 await oiFoo.init("-"); // 返回 -3 更多问题和api请到项目 github 地址中查看,如果有任何问题,也请在下面评论留言或到github提交issue(热烈欢迎到github提交issue捏)。以上文章来自[掘金]-[Xmo]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
🌼 细数那些惊艳一时的 CSS 属性-掘金
以上文章来自[掘金]-[CatWatermelon]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
flutter-底部TabBar与顶部TabBarView-掘金
前言就以我们平时比较常见的微信应用参考,我们比较常用的组件就是底部 tabbar 了,除此之外,就是顶部的类似于 tabbar 的组件了(有些地方叫顶部叫PageView),两者兼并的,就以掘金 app 首页为例,上下两个部分一目了然案例demo地址(Tabbar文件夹)下面就分别介绍这两种怎么使用的,以及怎么保存状态底部tabbar底部 tabbar 就是我们常见的微信底部的切换功能效果,如下所示其主要是依靠 BottomNavgationBar 、PageView、PageController,如下所示class NormalTabBar extends StatefulWidget { const NormalTabBar({Key? key}) : super(key: key); @override State createState() => _NormalTabBarState(); } class _NormalTabBarState extends State { //用于协调tabbar和内容的联动 final PageController _controller = PageController( initialPage: 0 //默认为0,可以不填写 ); //存放bottombar信息,避免编写过多重复ui代码 final List items = [ TabBarItem(title: '聊天', norImage: "images/tabbar_chat.png", selImage: "images/tabbar_chat_hl.png"), TabBarItem(title: '联系人', norImage: "images/tabbar_contact.png", selImage: "images/tabbar_contact_hl.png"), TabBarItem(title: '发现', norImage: "images/tabbar_discover.png", selImage: "images/tabbar_discover_hl.png"), TabBarItem(title: '我的', norImage: "images/tabbar_mine.png", selImage: "images/tabbar_mine_hl.png") ]; int _pageIndex = 0; @override Widget build(BuildContext context) { return Scaffold( bottomNavigationBar: BottomNavigationBar( currentIndex: _pageIndex, onTap: (int page) { setState(() { _pageIndex = page; }); _controller.jumpToPage(page); }, backgroundColor: Colors.white, type: BottomNavigationBarType.fixed, unselectedItemColor: Colors.black, selectedItemColor: Colors.cyanAccent, unselectedFontSize: 12, selectedFontSize: 12, items: items.map((e) { return BottomNavigationBarItem( label: e.title, icon: Image.asset(e.norImage, width: 20, height: 20,), activeIcon: Image.asset(e.selImage, width: 20, height: 20,), ); }).toList(), ), body: PageView( controller: _controller, //不设置默认可以左右活动,如果不想左右滑动如下设置,可以根据ios或者android来设置 physics: Platform.isAndroid ? const PageScrollPhysics() : const NeverScrollableScrollPhysics(), children: const [ //设置内容页面即可,要和 bottomNavigationBar 数量一致 TabbarContainerView(color: Colors.yellow, name: '1',), TabbarItemInAppBarView(color: Colors.cyanAccent, name: "2",), TabbarContainerView(color: Colors.green, name: "3",), TabbarItemInAppBarView(color: Colors.blueAccent, name: "4",), ], ), ); } } 顶部tabbar顶部tabbar虽然没有底部那么常用,但是在不少app的一些场景也是用的不少,下面介绍下其使用,本质和tabbar功能类似,只不过出了另外一个组件,花不说了,效果如下所示其有两种方案,一种使用系统默认的,另外一种使用自定义的默认顶部tabbar实现主要通过 DefaultTabController、TabBar、TabBarView,如下所示class TabbarContainerView extends StatefulWidget { final Color color; final String name; const TabbarContainerView({Key? key, required this.color, required this.name}) : super(key: key); @override State createState() => _TabbarContainerViewState(); } //继承maxin AutomaticKeepAliveClientMixin 同时重写 wantKeepAlive 可以对当前页面状态保存,避免重新渲染初始化参数 class _TabbarContainerViewState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; final List tabNames = ["推荐", "订阅"]; @override void initState() { super.initState(); print(widget.name); } @override Widget build(BuildContext context) { super.build(context); //通过 DefaultTabController 可自动调节内部tabbar联动 return DefaultTabController( length: tabNames.length, child: Scaffold( appBar: AppBar( title: const Text("带Tabs的tabbar子页面"), //默认在下面放一排 Tab bottom: TabBar( //这个参数比较特殊,默认为false,不出屏元素多了会被挤压内容 //如果想支持滚动,边内部挤压,可以将此参数设置为true //isScrollable: true, //设置tabs标签 tabs: tabNames.map((e) => Tab(text: e,)).toList(), ), ), //使用 TabBarView 来声明我们的内容组件 body: TabBarView( children: tabNames.map((e) { return TabsContainer( name: widget.name, color: widget.color, ); }).toList(), ), ), ); } } 自定义顶部tabbar除了上面默认的一横框的 tabbar 之外,还有类似与淘宝首页的那种效果,这种也可以通过自定义tabbar实现,如下所示(由于测试子页面多出来一个返回,所以居中有异常,正常不会有这这情况,根据情况自行调整即可)其实现主要通过 TabController、TabBarView,而 bar 我们自定义,通过 TabController 调节联动即可其中 TabController比较特殊,需要集成自 SingleTickerProviderStateMixin(由于多继承mixin需要with),然后延迟初始化 late 避免编译错误即可class TabInfosItem { final String name; final int index; bool selected; TabInfosItem({ required this.name, required this.selected, required this.index }); } class TabbarItemInAppBarView extends StatefulWidget { final Color color; final String name; const TabbarItemInAppBarView({Key? key, required this.color, required this.name}) : super(key: key); @override State createState() => _TabbarItemInAppBarViewState(); } //继承 SingleTickerProviderStateMixin class _TabbarItemInAppBarViewState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { @override bool get wantKeepAlive => true; List tabs = [ TabInfosItem(name: "推荐", selected: true, index: 0), TabInfosItem(name: "订阅", selected: false, index: 1), ]; //late 延迟初始化,避免报错,默认初始化是会出现 this 指向不是对象的错误问题 late TabController _tabController; @override void initState() { super.initState(); //初始化TabController _tabController = TabController(length: tabs.length, vsync: this); print(widget.name); } @override Widget build(BuildContext context) { super.build(context); return Scaffold( //因为有返回键,所以居中有问题这里就不多设置了 appBar: AppBar( title: Container( alignment: Alignment.center, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: tabs.map((TabInfosItem item) { return //这里就不多介绍了,添加额外参数,自定根据点击状态调整颜色线条等 //通过 tabController联动 TextButton( onPressed: () { _tabController.animateTo(item.index); var tab = tabs.map((e) { if (e.index == item.index) { e.selected = true; }else { e.selected = false; } return e; }).toList(); setState(() {}); }, child: Container( width: 60, alignment: Alignment.center, child: Text(item.name, style: TextStyle(color: item.selected ? Colors.white : Colors.grey),), ), ); }).toList(), ), ), ), //TabBarView与默认的一样 body: TabBarView( controller: _tabController, children: tabs.map((e) { return TabsContainer( name: widget.name, color: widget.color, ); }).toList(), ), ); } } 看到上面是不是感觉底部tabbar也可以通过其定制了呢,没错,可以的,但为了减少代码和避免一些其他问题,还是使用系统的好一些,除非这里默认的功能无法满足你的需求保存状态 AutomaticKeepAliveClientMixin前面的组件会看到很多都继承了 AutomaticKeepAliveClientMixin,其就是一个保存状态的多继承 mixin 基类,由于默认使用 tabbar 会丢失子组件状态,因此需要 AutomaticKeepAliveClientMixin 来进行保存状态,避免重新初始化,至于为什么会丢失,跟系统实现有关系了使用如下所示,继承后需要重写 wantKeepAlive 并且在 build 调用父类方法class _TabbarItemInAppBarViewState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { //需要重写该方法,返回为 true @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { //还需要调用super.build方法 super.build(context); return container(); } } 最后这边文章能新手带来一些方便,老手忘了也可以参考一下,大家一起学习进步哈以上文章来自[掘金]-[剪刀石头布啊]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
2 阅读
0 评论
0 点赞
2022-10-20
转转推荐场景EE题解决思路-掘金
1 引言推荐系统的目标主要包含两个方面:Exploitation 和 Exploration在Exploitation中最重要的是 Relevance ( 相关性 ) 的计算,其根本思想是根据用户浏览、观看和收藏的内容等用户行为数据推测该用户可能采取的行动。常见的推荐算法大多是基于针对该目标的优化而展开的。 然而用户行为数据在现实中很可能过少、不足以全面地体现用户的兴趣。这一现象在冷启动等场景中很常见。此时推荐系统还有责任挖掘用户尚未表现出的兴趣,并且避免由于现有行为数据过少而导致推送内容相似性过高的情况。这就需要引入Exploration。2 Exploitation排序环节中相关性探索目前主要以debias为主。2.1 bias引起模型bias的原因很多,主要包括: selection bias用户普遍倾向于自己喜欢或者讨厌的item进行评价,比如豆瓣评分。 exposure bias(sample selection bias)用户只能看到曝光的item并产生交互,但数据中没交互的item不代表用户不喜欢,可能是没曝光,这一点不好区分。 1)推荐系统决定了展现哪些items给用户 2)用户主动找到搜索找到感兴趣的项目 3)用户自身的背景。朋友、地理位置等 4)热门的items更容易被用户看到 conformity bias用户行为会受到他人影响,我们观测到的并不一定是用户真实偏好。 用户偏向于和集体喜好一致,用户看到大众统计数据之前和之后,行为分布有很大不同。 position bias用户在不同位置上的交互倾向和点击偏好不同。用户趋向于选择排位靠前的 items,因此实际发生交互的 item 并不一定相关性很强。许多场景也倾向于将盈利高而用户兴趣较低的item放在前面吸引用户点击,以提升相关item的交互行为,如某度。 2.2解决方案 1)特征输入以position bias为例,在训练时将 position 当作一个特征进行输入,在预测时以一个默认值进行输入,即假设所有item出现在同一位置预测点击率,对比用户偏好。 2)bias Tower单独设置一个shallow tower(Youtube Recsys19)来预测偏置,输入的特征是一些与偏置相关的特征。在最后的 sigmoid 前,将shallow tower的输出结果加到logit中,线上预估时位置偏差特征取值为missing。 3)贪婪算法在预测时将每个item在所有位置都预测一次,再通过贪婪算法寻求最优组合(Deep Position-wise Interaction Network,SIGIR 2021) 3 Exploration此部分通常会被归为重排序阶段,目前以解决多样性为主。行列式点过程DPP算法(Fast Greedy MAP Inference for Determinantal Point Process to Improve Recommendation Diversity,NIPS2018)认为如果两个商品的相似性过高,用户可能点击一个之后对另一个的点击需求就会下降。通过构建矩阵来计算每一个子集的行列式值,该值可以理解为用户对推荐列表的满意程度,受到相关性和多样性两个因素的影响。矩阵可以表示为 $$ L_Y = egin{pmatrix} L_{ii} & L_{ij} L_{ji} & L_{jj} end{pmatrix} $$ 矩阵元素构建如下:$ce{L_{ii} = q_i^2}$$ce{L_{ij} = {alpha} * q_i * q_j * expleft(-frac{D_{ij}}{2sigma^2} ight)}$其中$ce{q_i}$为相关性指标,$ce{D_{ij}}$为多样性指标。$ce{q_i approx p(y_i=1|feature of item i)}$$ce{D_{ij} = distance(item i, item j)in[0, +infty)}$$ce{alpha、sigma}$为超参,当$ce{alpha}$处于0到1之间且$ce{alpha}$变小时,相当于我们整个行列式值被缩小,所以多样性变好;相反,$ce{alpha}$大于1且变大时,多样性变差;alpha=1时为标准高斯径向基函数。该问题的求解明显是个NP-hard问题,因此可用贪婪算法进行求解。同时由于$ce{L_Y}$是半正定矩阵,因此可以通过矩阵分解得:$ce{L_Y=VV^T}$其中$ce{V}$是下三角矩阵。通过以上优化,每次逐步增加一个item,进而获得最终的推荐列表,使得整体求解复杂度从$ce{O(y^3)}$成功降到了$ce{O(y)}$,但实际计算中需保证$ce{L_Y}$矩阵的半正定,论文中给出的方法是若L的特征值为负值,则将该值替换为0。具体求解过程如下:盈利场景则会综合考虑每个item的盈利信息,具体可参考转转商业化OCPC产品护航之路4 总结bias类型很多,但是并不是所有的bias都需要去除,例如电商场景下的流行度这种本身就会影响用户点击的bias,但是这种分析方法能够引导我们更好的进行特征挖掘和目标优化;多样性表面上看会降低推荐列表的相关性,但从实验结果上来看,反倒会对业务指标有促进作用,说明用户对多样性还是有很大的需求。> 转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 > 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~以上文章来自[掘金]-[转转技术团队]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
好好的系统,为什么要分库分表?-掘金
本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!大家好,我是小富~说在前边今天是《分库分表 ShardingSphere 原理与实战》系列的开篇文章,之前写过几篇关于分库分表的文章反响都还不错,到现在公众号:程序员小富后台不断的有人留言、咨询分库分表的问题,我也没想到大家对于分库分表的话题会这么感兴趣,可能很多人的工作内容业务量较小很难接触到这方面的技能。这个系列在我脑子里筹划了挺久的,奈何手说啥也不干活,就一直拖到了现在。其实网上关于分库分表相关的文章很多,但我还是坚持出这个系列,主要是自己学习研究,顺便给分享,对于一个知识,不同的人从不同的角度理解的不尽相同。网上的资料看似很多,不过值得学有价值的得仔细挑,很多时候在筛选甄别的过程中,逐渐的磨灭了本就不高的学习热情。搬运抄袭雷同的东西太多,而且知识点又都比较零碎,很少有细致的原理实战案例。对新手来说妥妥的从入门到放弃,即便有成体系的基本上几篇后就断更了(希望我不会吧!)。我不太喜欢堆砌名词概念,熟悉我的朋友不难发现,我的文章从来都是讲完原理紧跟着来一波实战操作。学习技术原理必须配合实操巩固一下,不然三天半不到忘得干干净净,纯纯的经验之谈。上图是我初步罗列的ShardingSphere提纲,在官网文档基础上补充了很多基础知识,这个系列会用几十篇文章,详细的梳理分库分表基础理论,手把手的实战ShardingSphere 5.X框架的功能和解读源码,以及开发中容易踩坑的点,每篇附带代码案例demo,旨在让新手也能看的懂,后续系列完结全部内容会整理成PDF分享给大家,期待一下吧!话不多说,咱们这就进入正题~不急于上手实战ShardingSphere框架,先来复习下分库分表的基础概念,技术名词大多晦涩难懂,不要死记硬背理解最重要,当你捅破那层窗户纸,发现其实它也就那么回事。什么是分库分表分库分表是在海量数据下,由于单库、表数据量过大,导致数据库性能持续下降的问题,演变出的技术方案。分库分表是由分库和分表这两个独立概念组成的,只不过通常分库与分表的操作会同时进行,以至于我们习惯性的将它们合在一起叫做分库分表。通过一定的规则,将原本数据量大的数据库拆分成多个单独的数据库,将原本数据量大的表拆分成若干个数据表,使得单一的库、表性能达到最优的效果(响应速度快),以此提升整体数据库性能。为什么分库分表单机数据库的存储能力、连接数是有限的,它自身就很容易会成为系统的瓶颈。当单表数据量在百万以里时,我们还可以通过添加从库、优化索引提升性能。一旦数据量朝着千万以上趋势增长,再怎么优化数据库,很多操作性能仍下降严重。为了减少数据库的负担,提升数据库响应速度,缩短查询时间,这时候就需要进行分库分表。为什么需要分库?容量我们给数据库实例分配的磁盘容量是固定的,数据量持续的大幅增长,用不了多久单机的容量就会承载不了这么多数据,解决办法简单粗暴,加容量!连接数单机的容量可以随意扩展,但数据库的连接数却是有限的,在高并发场景下多个业务同时对一个数据库操作,很容易将连接数耗尽导致too many connections报错,导致后续数据库无法正常访问。可以通过max_connections查看MySQL最大连接数。show variables like '%max_connections%' 将原本单数据库按不同业务拆分成订单库、物流库、积分库等不仅可以有效分摊数据库读写压力,也提高了系统容错性。为什么需要分表?做过报表业务的同学应该都体验过,一条SQL执行时间超过几十秒的场景。导致数据库查询慢的原因有很多,SQL没命中索引、like扫全表、用了函数计算,这些都可以通过优化手段解决,可唯独数据量大是MySQL无法通过自身优化解决的。慢的根本原因是InnoDB存储引擎,聚簇索引结构的 B+tree 层级变高,磁盘IO变多查询性能变慢,详细原理自行查找一下,这里不用过多篇幅说明。阿里的开发手册中有条建议,单表行数超500万行或者单表容量超过2GB,就推荐分库分表,然而理想和实现总是有差距的,阿里这种体量的公司不差钱当然可以这么用,实际上很多公司单表数据几千万、亿级别仍然不选择分库分表。什么时候分库分表技术群里经常会有小伙伴问,到底什么情况下会用分库分表呢?分库分表要解决的是现存海量数据访问的性能瓶颈,对持续激增的数据量所做出的架构预见性。是否分库分表的关键指标是数据量,我们以fire100.top这个网站的资源表 t_resource为例,系统在运行初始的时候,每天只有可怜的几十个资源上传,这时使用单库、单表的方式足以支持系统的存储,数据量小几乎没什么数据库性能瓶颈。但某天开始一股神秘的流量进入,系统每日产生的资源数据量暴增至十万甚至上百万级别,这时资源表数据量到达千万级,查询响应变得缓慢,数据库的性能瓶颈逐渐显现。以MySQL数据库为例,单表的数据量在达到亿条级别,通过加索引、SQL调优等传统优化策略,性能提升依旧微乎其微时,就可以考虑做分库分表了。既然MySQL存储海量数据时会出现性能瓶颈,那么我们是不是可以考虑用其他方案替代它?比如高性能的非关系型数据库MongoDB?可以,但要看存储的数据类型!现在互联网上大部分公司的核心数据几乎是存储在关系型数据库(MySQL、Oracle等),因为它们有着NoSQL如法比拟的稳定性和可靠性,产品成熟生态系统完善,还有核心的事务功能特性,也是其他存储工具不具备的,而评论、点赞这些非核心数据还是可以考虑用MongoDB的。如何分库分表分库分表的核心就是对数据的分片(Sharding)并相对均匀的路由在不同的库、表中,以及分片后对数据的快速定位与检索结果的整合。分库与分表可以从:垂直(纵向)和 水平(横向)两种纬度进行拆分。下边我们以经典的订单业务举例,看看如何拆分。垂直拆分1、垂直分库垂直分库一般来说按照业务和功能的维度进行拆分,将不同业务数据分别放到不同的数据库中,核心理念 专库专用。按业务类型对数据分离,剥离为多个数据库,像订单、支付、会员、积分相关等表放在对应的订单库、支付库、会员库、积分库。不同业务禁止跨库直连,获取对方业务数据一律通过API接口交互,这也是微服务拆分的一个重要依据。垂直分库很大程度上取决于业务的划分,但有时候业务间的划分并不是那么清晰,比如:电商中订单数据的拆分,其他很多业务都依赖于订单数据,有时候界线不是很好划分。垂直分库把一个库的压力分摊到多个库,提升了一些数据库性能,但并没有解决由于单表数据量过大导致的性能问题,所以就需要配合后边的分表来解决。2、垂直分表垂直分表针对业务上字段比较多的大表进行的,一般是把业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中,是一种大表拆小表的模式。例如:一张t_order订单表上有几十个字段,其中订单金额相关字段计算频繁,为了不影响订单表t_order的性能,就可以把订单金额相关字段拆出来单独维护一个t_order_price_expansion扩展表,这样每张表只存储原表的一部分字段,通过订单号order_no做关联,再将拆分出来的表路由到不同的库中。数据库它是以行为单位将数据加载到内存中,这样拆分以后核心表大多是访问频率较高的字段,而且字段长度也都较短,因而可以加载更多数据到内存中,减少磁盘IO,增加索引查询的命中率,进一步提升数据库性能。水平拆分上边垂直分库、垂直分表后还是会存在单库、表数据量过大的问题,当我们的应用已经无法在细粒度的垂直切分时,依旧存在单库读写、存储性能瓶颈,这时就要配合水平分库、水平分表一起了。1、水平分库水平分库是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,以此实现水平扩展,是一种常见的提升数据库性能的方式。例如:db_orde_1、db_order_2两个数据库内有完全相同的t_order表,我们在访问某一笔订单时可以通过对订单的订单编号取模的方式 订单编号 mod 2 (数据库实例数) ,指定该订单应该在哪个数据库中操作。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此系统的复杂度也被提升了。2、水平分表水平分表是在同一个数据库内,把一张大数据量的表按一定规则,切分成多个结构完全相同表,而每个表只存原表的一部分数据。例如:一张t_order订单表有900万数据,经过水平拆分出来三个表,t_order_1、t_order_2、t_order_3,每张表存有数据300万,以此类推。水平分表尽管拆分了表,但子表都还是在同一个数据库实例中,只是解决了单一表数据量过大的问题,并没有将拆分后的表分散到不同的机器上,还在竞争同一个物理机的CPU、内存、网络IO等。要想进一步提升性能,就需要将拆分后的表分散到不同的数据库中,达到分布式的效果。数据存在哪个库的表分库分表以后会出现一个问题,一张表会出现在多个数据库里,到底该往哪个库的哪个表里存呢?上边我们多次提到过一定规则 ,其实这个规则它是一种路由算法,决定了一条数据具体应该存在哪个数据库的哪张表里。常见的有 取模算法 、范围限定算法、范围+取模算法 、预定义算法1、取模算法关键字段取模(对hash结果取余数 hash(XXX) mod N),N为数据库实例数或子表数量)是最为常见的一种路由方式。以t_order订单表为例,先给数据库从 0 到 N-1进行编号,对 t_order订单表中order_no订单编号字段进行取模hash(order_no) mod N,得到余数i。i=0存第一个库,i=1存第二个库,i=2存第三个库,以此类推。同一笔订单数据会落在同一个库、表里,查询时用相同的规则,用t_order订单编号作为查询条件,就能快速的定位到数据。优点实现简单,数据分布相对比较均匀,不易出现请求都打到一个库上的情况。缺点取模算法对集群的伸缩支持不太友好,集群中有N个数据库实·hash(user_id) mod N,当某一台机器宕机,本应该落在该数据库的请求就无法得到处理,这时宕掉的实例会被踢出集群。此时机器数减少算法发生变化hash(user_id) mod N-1,同一用户数据落在了在不同数据库中,等这台机器恢复,用user_id作为条件查询用户数据就会少一部分。2、范围限定算法范围限定算法以某些范围字段,如时间或ID区拆分。用户表t_user被拆分成t_user_1、t_user_2、t_user_3三张表,后续将user_id范围为1 ~ 1000w的用户数据放入t_user_1,1000~ 2000w放入t_user_2,2000~3000w放入t_user_3,以此类推。按日期范围划分同理。优点 单表数据量是可控的 水平扩展简单只需增加节点即可,无需对其他分片的数据进行迁移 缺点 由于连续分片可能存在数据热点,比如按时间字段分片时,如果某一段时间(双11等大促)订单骤增,存11月数据的表可能会被频繁的读写,其他分片表存储的历史数据则很少被查询,导致数据倾斜,数据库压力分摊不均匀。 3、范围 + 取模算法为了避免热点数据的问题,我们可以对上范围算法优化一下这次我们先通过范围算法定义每个库的用户表t_user只存1000w数据,第一个db_order_1库存放userId从1 ~ 1000w,第二个库1000~2000w,第三个库2000~3000w,以此类推。每个库里再把用户表t_user拆分成t_user_1、t_user_2、t_user_3等,对userd进行取模路由到对应的表中。有效的避免数据分布不均匀的问题,数据库水平扩展也简单,直接添加实例无需迁移历史数据。4、地理位置分片地理位置分片其实是一个更大的范围,按城市或者地域划分,比如华东、华北数据放在不同的分片库、表。5、预定义算法预定义算法是事先已经明确知道分库和分表的数量,可以直接将某类数据路由到指定库或表中,查询的时候亦是如此。分库分表出来的问题了解了上边分库分表的拆分方式不难发现,相比于拆分前的单库单表,系统的数据存储架构演变到现在已经变得非常复杂。看几个具有代表性的问题,比如:分页、排序、跨节点联合查询分页、排序、联合查询,这些看似普通,开发中使用频率较高的操作,在分库分表后却是让人非常头疼的问题。把分散在不同库中表的数据查询出来,再将所有结果进行汇总合并整理后提供给用户。比如:我们要查询11、12月的订单数据,如果两个月的数据是分散到了不同的数据库实例,则要查询两个数据库相关的数据,在对数据合并排序、分页,过程繁琐复杂。事务一致性分库分表后由于表分布在不同库中,不可避免会带来跨库事务问题。后续会分别以阿里的Seata和MySQL的XA协议实现分布式事务,用来比较各自的优势与不足。全局唯一的主键分库分表后数据库表的主键ID业务意义就不大了,因为无法在标识唯一一条记录,例如:多张表t_order_1、t_order_2的主键ID全部从1开始会重复,此时我们需要主动为一条记录分配一个ID,这个全局唯一的ID就叫分布式ID,发放这个ID的系统通常被叫发号器。多数据库高效治理对多个数据库以及库内大量分片表的高效治理,是非常有必要,因为像某宝这种大厂一次大促下来,订单表可能会被拆分成成千上万个t_order_n表,如果没有高效的管理方案,手动建表、排查问题是一件很恐怖的事。历史数据迁移分库分表架构落地以后,首要的问题就是如何平滑的迁移历史数据,增量数据和全量数据迁移,这又是一个比较麻烦的事情,后边详细讲。分库分表架构模式分库分表架构主要有两种模式:client客户端模式和proxy代理模式客户模式client模式指分库分表的逻辑都在你的系统应用内部进行控制,应用会将拆分后的SQL直连多个数据库进行操作,然后本地进行数据的合并汇总等操作。代理模式proxy代理模式将应用程序与MySQL数据库隔离,业务方的应用不在需要直连数据库,而是连接proxy代理服务,代理服务实现了MySQL的协议,对业务方来说代理服务就是数据库,它会将SQL分发到具体的数据库进行执行,并返回结果。该服务内有分库分表的配置,根据配置自动创建分片表。如何抉择如何选择client模式和proxy模式,我们可以从以下几个方面来简单做下比较。1、性能性能方面client模式表现的稍好一些,它是直接连接MySQL执行命令; proxy代理服务则将整个执行链路延长了,应用->代理服务->MySQL,可能导致性能有一些损耗,但两者差距并不是非常大。2、复杂度client模式在开发使用通常引入一个jar可以; proxy代理模式则需要搭建单独的服务,有一定的维护成本,既然是服务那么就要考虑高可用,毕竟应用的所有SQL都要通过它转发至MySQL。3、升级client模式分库分表一般是依赖基础架构团队的Jar包,一旦有版本升级或者Bug修改,所有应用到的项目都要跟着升级。小规模的团队服务少升级问题不大,如果是大公司服务规模大,且涉及到跨多部门,那么升级一次成本就比较高;proxy模式在升级方面优势很明显,发布新功能或者修复Bug,只要重新部署代理服务集群即可,业务方是无感知的,但要保证发布过程中服务的可用性。4、治理、监控client模式由于是内嵌在应用内,应用集群部署不太方便统一处理;proxy模式在对SQL限流、读写权限控制、监控、告警等服务治理方面更优雅一些。结束语本文主要是回顾一下分库分表的一些基础概念,为大家在后续ShardingSphere实践中更好上手理解,内容里很多概念一笔带过没详细展开,接下来的篇幅会逐一解读。下一篇预告《分库分表ShardingSphere的基础知识点梳理》欢迎关注 公众号:程序员小富,咱们下期再见!以上文章来自[掘金]-[程序员小富]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-19
CLIP打通文本图像壁垒,为AI图像生成打下基础-掘金
预训练可以看一下(1),预训练阶段使用的是对比学习,我们可以看到是把一组文本和一组图片丢进了一个孪生网络。使用自然语言处理的结果作为监督信息公提供给图像进行学习。图片进入图片编码器获得对应的图片特征$I_i$,文本进入文本编码器获得对应的文本特征$T_j$,之后二者组成矩阵,矩阵对角线上的特征是图文对应的信息,矩阵其他位置的特征是图文不对应的信息,这样我们就可以将对应的信息作为正样本,不对应的信息作为负样本,从而进行对比学习。在这样的设置之下,假设我们一个batch size大小设置为n,那我们就能获得$n$个正样本和$n^2-n$个负样本。这样就不需要图片标签,使用对比学习进行预训练了。但是对比学习需要大量的数据,为此OpenAI也是下了大功夫,收集了4亿个text-image对,并做的极好的数据清晰,才能保证模型最终获得如此优秀的效果(主要原因之一)。分类器因为使用对比学习,所以CLIP是没有分类头的,所以作者得想个办法让它能去做分类。在这里就用到了NLP中的一个方法——“prompt template”。看看图中的(2),用左边不同的词汇(多少类别就是多少词汇)去替换中间方框中的那个句子A photo if a _____ .,把一个单词变为一个句子。之后使用编码器对其进行编码获得特征。其实在这里不是必须将其变为一个句子,单个的单词也是可以进行的,但是会和训练阶段产生较大的差异,所以作者在这里还是麻烦了一点,将其都变为句子。这里怎么变为句子还是有说法的,具体可以看一下这篇文章:然后看图中的(3),在预测阶段,我们拿到一张图之后将其丢进图片编码器,获得一个特征,之后我们使用余弦相似度计算我们获得的句子特征和图片特征,取得最终的分类结果。补充一下,在CLIP,模型训练完之后因为是zero-shot的,所以完全可以摆脱categorical label的限制,(2)中的列表我们可以替换为任意的单词,完全不要纠结于ImageNet的1000类还是3000类,图片也可以是任意出处的图,不拘泥于某一个数据集。总结: 因为其独特的训练方式,所以CLIP将图片和文本语义做到了强关联,每个图片都有非常好的语义特征,所以迁移效果非常好。凭借这一点,CLIP在ImageNet数据集上打败了ResNet101等专门在ImageNet上训练的模型。因为和自然语言处理的结合,CLIP训练出来的模型可以在图片域变化非常大的情况下也获得很好的效果。Learning Transferable Visual Models From Natural Language Supervision少说点在之前的一些模型都需要一些预定义的类,比如cifar-10有10个类,提前定义好的类别会影响模型的泛化性,因为之前的类别中没有设置到这些内容,所以遇到一些新数据可能会出现无法处理等问题。所以这个作者们想做一个泛化能力强的模型,大家就考虑能不能借助自然语言处理,从文本中获得一些监督信号,这样的话监督信号就很丰富了,你只要能用语言描述,我就可以将你作为文本信息标签,将其放到视觉模型中。所以作者就去爬了4亿的数据,使用图片文本多模态做了一个对比学习的预训练。为了验证模型的有效性,作者去30个数据集上进行了多个任务的测试,在不需要任何数据集专门训练的情况下,能和之前的一个完全用有监督方法训练出来的模型取得相同的效果甚至吊打人家。作者在论文里稍微提了一下之前的工作,但是效果不行,总结原因就是他们没有OpenAI的 钞能力。方法概述CLIP方法的核心就是借助自然语言,之前的NLP的transformer和自监督训练兴起之前,很难将其应用到图像这边,但是现在NLP发展起来了,可以使用自监督学习获得上下文语义,使用海量的无标签数据获得又大又好用的模型了。为什么CLIP要用自然语言的监督信号训练视觉模型? 不需要再去标注数据了。在ImageNet上是128万图片及其1000类,但是使用自然语言,就不需要这么麻烦了,你只需要去网上爬到对应的文本数据对。并且现在图片对应的不再是标签,而是变成了文本,这样模型的输入输出更加灵活。 训练过程将图片文本绑定到一起,模型学到的是多模态的特征,更有利于模型的泛化。 …… CV方向的模型都很大,训练起来都很费钱费时间。举个夸张的例子,ResNeXt101-32x46d需要训练33个TPUv3年,如果你用GPU的话可能要算好几个世纪了。之前模型的数据集都没有CLIP大,虽然OpenAI的钞能力是出了名的,但是作者也不敢想象要烧多少亿才能把模型训练起来。所以作者们进行了一系列的尝试,比如图片用CNN,文本用transformer,做一个预测任务。但是要知道预测任务的可能性太多了,比如给我们一张图,做这个图的caption。我们看一下下图,可以得到很多结果,比如: 图上是杨戬。 图上是LolitaAnn的老公。 图中是一个男人的侧脸。 这个男人看起来很悲伤 …… 这种开放式的预测答案实在是太多了。但是如果使用对比学习的训练方法,我们仅需要判断图文能不能搭配起来即可。这样任务就被简化了很多,约束也放宽了很多。看一下下图,作者将预测型的目标函数转化为对比学习的目标函数之后,训练速度一下子提高了很多。绿色的线是CLIP最终选择的对比学习训练方式,比中间橙色的线快了四倍。中间橙色的线是使用词袋模型,将内容都抽取为特征进行匹配,比蓝色的线快了三倍,蓝色的线是原来的预测型的训练方式。最终为了保证训练效率,选择了对比学习。因为OpenAI的这个数据集太大了,所以训练过程中根本不用担心出现过拟合的问题,也不需要做什么数据增强,用的唯一一个数据增强就是一个随机裁剪(random square crop)。因为模型实在是太大了,调参实在是太困难了,所以进行对比学习的时候将对比学习中很重要的一个t temperature parameter作为一个可学习的标量,所以这个参数在模型训练过程中直接就被优化了,不需要再调参了。此外他们使用的图像和文本的编码器都不需要预训练,并且最后做projection的时候没有使用非线性的投射层,使用的是线性的投射层。补充:看过之前我写的对比学习系列的应该知道,在SimCLR工作中因为加了一个非线性的projection之后获得了10个点的提升,但是CLIP的作者在工作过程中发现,多模态中使用线性还是非线性影响并不是很大,对此作者还进行了一个 猜测 ,这种非线性的projection应该只能适用于纯图片的单模态学习。伪代码# image_encoder - ResNet or Vision Transformer # text_encoder - CBOW or Text Transformer # I[n, h, w, c] - minibatch of aligned images # T[n, l] - minibatch of aligned texts # W_i[d_i, d_e] - learned proj of image to embed # W_t[d_t, d_e] - learned proj of text to embed # t - learned temperature parameter # extract feature representations of each modality I_f = image_encoder(I) #[n, d_i] T_f = text_encoder(T) #[n, d_t] # joint multimodal embedding [n, d_e] I_e = l2_normalize(np.dot(I_f, W_i), axis=1) T_e = l2_normalize(np.dot(T_f, W_t), axis=1) # scaled pairwise cosine similarities [n, n] logits = np.dot(I_e, T_e.T) * np.exp(t) # symmetric loss function labels = np.arange(n) loss_i = cross_entropy_loss(logits, labels, axis=0) loss_t = cross_entropy_loss(logits, labels, axis=1) loss = (loss_i + loss_t)/2 伪代码我就不讲了,具体看安安的这篇文章: CLIP模型伪代码详细解析不是我的小号!训练We train a series of 5 ResNets and 3 Vision Transformers. For the ResNets we train a ResNet-50, a ResNet-101, and then 3 more which follow EfficientNet-style model scaling and use approximately 4x, 16x, and 64x the compute of a ResNet-50 . They are denoted as RN50x4, RN50x16, and RN50x64 respectively. For the Vision Transformers we train a ViT-B/32, a ViT-B/16, and a ViT-L/14. We train all models for 32 epochs. We use the Adam optimizer with decoupled weight decay regularization applied to all weights that are not gains or biases, and decay the learning rate using a cosine schedule.训练过程作者是训练了5个ResNet网络和3个Vision Transformer模型,每个模型都训练了32个epoch。使用的是Adam优化器。Initial hyperparameters were set using a combination of grid searches, random search, and manual tuning on the baseline ResNet50 model when trained for 1 epoch. Hyperparameters were then adapted heuristically for larger models due to computational constraints.对于所有的超参数,作者也是做了一些简单的grid search和random search进行手动调整,为了调的快一点,用的都是标准的ResNet50去做的,并且就跑了一个epoch,就突出一个懒得调。更大模型直接摆烂根本不调的。The learnable temperature parameter t was initialized to the equivalent of 0.07 from and clipped to prevent scaling the logits by more than 100 which we found necessary to prevent training instability. We use a very large minibatch size of 32,768.训练的batch size直接拉满,到32768,我只跑过8192的,再次感叹一下OpenAI的钞能力!!!Mixed-precision was used to accelerate training and save memory. To save additional memory, gradient checkpointing, half-precision Adam statistics, and half-precision stochastically rounded text encoder weights were used. The calculation of embedding similarities was also sharded with individual GPUs computing only the subset of the pairwise similarities necessary for their local batch of embeddings.此外作者还用了这么多加速和省内存的tricks,通过这么多的努力才把CLIP训练起来。一番烧钱操作之后,作者最终选定了ViT-L/14并对其进行微调。然后CLIP就定下用这个了,之后我们就确定以及肯定提到图片编码器用的就是ViT-L/
[email protected]
。模型局限性有的人会提出质疑,你这玩意儿效果好,是不是因为你四亿对数据集太大了,把人家其他数据集都涵盖了,所以你的结果才能这么好看?为了防止这样的问题,作者对数据集进行了去重等一系列操作,最后的结论就是 “我的模型就是好用”。吹了半天它多厉害之后我们讲一下模型的局限性。作者说CLIP在很多数据集都能和简单的baseline打成平手的,比如ResNet50,但是要知道ResNet只是baseline,不是SOTA,CLIP目前在很多任务上是打不过SOTA的,所以是一个比较均衡的强大,但是能力不是很拔尖。CLIP的如果继续扩大模型和数据集去追赶SOTA的话,作者预估的是还要扩大1000倍,但是即使对于OpenAI这样的钞能力公司来说也是跑不起的(其他人更别做梦了),所以想靠着扩大模型扩大数据集来改进模型,几乎不太可能。(OpenAI:面子我给各位了,各位不要不识好歹。)另一个缺点是,CLIP在某些数据集上效果也就一般般,比如在细分类上zero-shot就不行,并且CLIP没办法做一些很抽象的东西,也没办法做一些异常检测之类的艰难任务。第三个局限性是虽然泛化能力做的很好,但是如果在做图像推理过程中遇到真正的OOD是无法处理的,并且作者说很尴尬的一个事情是在MNIST中准确率只达到88%,作者找了一下原因,发现是四亿对图片居然没有和MNIST相像的图片,因为MNIST是简单的12345的数字识别嘛,他们的数据集里居然无!!!第四个局限性是虽然可以做分类任务,并且你可以自定义分类的种类和列表,但是还是只能在你给定的类别里做选择。相比而言,直接去生成图像标题,让模型去做生成,这样会更灵活。这一点看出作者还是想秉承OpenAI一贯的工作去做预测任务,但是受限于计算资源没办法去实现,之后作者可能会考虑怎么合并一下对比学习的目标函数和生成式任务的目标函数,将二者的优势结合到一起。第五个局限性是对数据的利用不是很高效,需要大量的数据集去投喂。你想32个epochs,每轮都是4亿图,这就是跑了128亿的图啊!!!DataLoader好辛苦啊……如果能优化数据用量就好了。如果能在这里改进就好了。然后是下有数据的局限性。虽然一直都在说zero-shot,但是为了和其他模型去打一打,不停地在目标数据集上进行优化,不停用27个数据集去做测试,这样就在无形中引入了偏见,也就是说这个模型更偏向于打败这27个数据集。 我摊牌了。然后是他们的数据集是去往上爬的,数据清晰做不好的话可能会带有一些bias,会引发一些伦理问题。很多很复杂的任务和概念,有的你用语言也是无法描述的,如果在下游任务中如果能给一些prompt会更好,但是CLIP做的目标就是zero-shot,你给他一些数据反而效果会变差,所以这也是很奇怪一点。以上文章来自[掘金]-[LolitaAnn在掘金]本程序使用github开源项目RSSHub提取聚合!
2022年10月19日
0 阅读
0 评论
0 点赞
1
2
...
1048