LucienXian's Blog


  • 首页

  • 归档

  • 标签

垃圾回收器与内存分配策略

发表于 2019-02-01

垃圾回收器与内存分配策略

概述

Garbage Collection需要考虑三件事情:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

了解GC的目的是为了更好地排查各种内存泄露、内存溢出的问题,特别是在垃圾回收成为系统达到更高并发量的瓶颈时。回到Java,垃圾回收器主要关注的是堆内存。

对象生命

引用计数法

引用计数法是比较简单的判断对象是否存活的方法:给对象添加一个引用计数器,每当有个地方引用它时,计数器就加一;引用失效,计数数值就减一。

这个方法在ActionScript3的FlashPlayer、Python语言的一些领域有所应用。

但引用计数法不能解决对象间相互循环引用的问题,因此该方法没有被jvm采用。

可达性分析算法

在主流的商用语言如Java、C#等都是通过可达性分析来判定对象是否存活的。这个算法的思路就是从一系列被称为GC Roots的对象作为结点,从该节点向下搜索,某个对象不可达,则证明该对象不可用了,可回收。

img
img

在Java中,可作为GC Roots的对象包括:

  • 虚拟栈引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI即Native方法引用的对象;

再谈引用

无论哪种算法,判定对象是否存活都与"引用"有关。

在JDK1.2之火,Java对引用的概念进行了补充:

  • 强引用:类似"Object obj = new Object()",只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。
  • 软引用:这是一些还有用但并非必要的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围内进行第二次回收,提供了SoftReference类来实现软引用;
  • 弱引用:无论内存是否足够,垃圾收集一定会回收掉被弱引用关联的对象;WeakReference;
  • 虚引用:最弱的引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获取一个对象实例,唯一目的就是在对象被回收时收到系统通知;PhantomReference;

生存还是死亡

即便在可达性分析算法中成为了不可达的对象,也不是非死不可的。在这个过程中,要真正回收对象,需要至少两次的标记过程。在可达性分析之后没有与GC Roots相连接的引用链,将会被第一次标记,并进行一次筛选:对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者该方法已经执行过,虚拟机则认为没有必要执行。

如果有必要执行,对象将会被放入F-Queue队列之中,并在稍后由一个虚拟机创立的Finalizer线程去异步执行,触发该方法。为了避免阻塞队列,该线程不承诺等待它完成,因此如果在执行finalize()时,对象重新与引用链上的任何一个对象建立联系,那么它将成功自救,移出"即将回收"的集合里。

回收方法区

虚拟机规范并有要求虚拟机在方法区实现垃圾回收,因为这些回收操作性价比有点低。方法区的回收主要是两部分内容:废弃常量和无用的类。例如常量池的字符串常量。而对于类是否无用则需要满足以下条件:

  • 该类的所有实例都已经被回收;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象并有任何引用;

满足条件仅仅是可以被回收,而不是一定回收。往往在大量使用反射、动态代理、GCLib等频繁定义ClassLoader的场景需要虚拟机具备类卸载的功能,保证方法区不会溢出。

垃圾收集算法

Mark-Sweep算法

该算法包括两个阶段:标记、清除。首先标记出所有需要回收的对象,在标记完成后统一回收。这个算法有两个缺点:

  • 效率问题:标记和回收的效率较低;
  • 空间问题:容易造成内存碎片;

Copying算法

这个算法的提出是为了解决效率问题,它将可用内存按照容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完时,就将还存活的对象复制到另外一块上面,然后再将使用过的内存空间一次性清掉。

这样内存分配就不用考虑内存碎片,分配时只需要移动堆顶指针,按顺序分配内存即可。

HotSpot也是用的这个方法,它将可用内存分成一块大的Eden空间和两块小的Survivor空间,每次使用Eden和其中一块Survivor,Eden和Survivor的比例是8:1,因此可用空间为90%。

但我们无法保证每次回收只有不多于10%的对象存活

Mark-Compact整理算法

复制收集算法在对象存活率较高时就需要进行较多的复制操作,效率相对会降低。

因此又提出了一种新的算法:标记-整理。但与标记-清除不同的是,它不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

Generational Collection

当前商业虚拟机采集的一种算法,即根据对象的生命周期将内存分为老生代和新生代。针对新生代,由于每次都会有大量的对象死去,所以一般会用复制算法;而对于老生代内存,则使用标记-清理或者标记-整理算法进行回收。

HotSpot的算法实现

枚举根节点

从上面提到的可达性分析中可以得知,要寻找GC Roots的结点主要是从全局性的引用与执行上下文中去找,但现在的应用往往在方法区就有数百兆,要逐个枚举太花时间了。

另外,为了分析能够在确保一致性的快照中进行,GC必须要停顿所有的Java执行线程,导致GC停顿。

目前的主流Java虚拟机使用的都是准确式GC,当系统停顿下来后,并不需要逐个去检查所有执行上下文和全局的引用位置,而是使用一组成为OopMap的数据结构来达到这个目的。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是什么引用。

这样,GC就能直接得知这些信息了。

安全点

虽然Oop能保证HotSpot快速完成GC Roots枚举,但如果导致OopMap内容变化的指令非常多,那么为每一条指令都生成对应的OopMap是不合理的。

实际上,HotSpot只是在特定的地方记录了这些信息——Safe Point,安全点。线程只有执行到安全点才会暂停下来。安全点的选定基本是以程序“是否具有让程序长时间执行的特征”为标准而进行选定的,因此指令序列复用的地方,诸如:方法调用、循环跳转、异常跳转等才比较有可能会产生safe point。

另一方面,考虑到如何在GC发生时让所有线程跑到安全点附近停下,一般来说有两种方法:抢先式中断和主动式中断。现在主流的虚拟机都是采用的主动式中断,即当GC需要中断线程时,不是直接操作线程,而是简单地设置一个标识,各个线程执行时主动去轮询这个标识,为真时则自己主动中断挂起。轮询标识的地方和安全点是重合的。

安全区域

safepoint机制保证了程序执行时,可以在短时间内进入GC的safepoint。但如果程序不执行,没有被分配CPU时间,处于sleep或者blocked的状态中,那么它就无法响应JVM的响应。此时就需要安全区域(safe region)来解决这个问题。

安全区域指的是在一段代码中,引用关系不会发送变化,在这个区域中任意开GC都是安全的。当线程执行到safe region中的代码时,首先标识自己已经进入了safe region。那么当JVM发起GC时,就不管该线程了。而当线程要离开safe region时,它需要检查自己是否已经完成了根节点枚举,否则需要等待收到可以安全离开safe region的信号为止。

垃圾回收器

回收算法是内存回收的方法论,而垃圾回收器则是内存回收的具体实现。但是Java虚拟机规范中没有对垃圾回收器的实现有任何的规定,每个厂商都会提供自己的垃圾回收器,并且都会提供参数以供用户自定义。下面讨论的是基于JDK1.7 Update1.4之后的HotSpot虚拟机。

img
img

以上图为例,存在连线的收集器表示可以搭配使用。目前并不存在一个最好的收集器,我们只能根据具体的应用选择合适的收集器。

Serial收集器

Serial收集器是最基本的收集器,曾经是新生代收集的唯一选择。这是一个单线程的收集器,不单单是只使用一个CPU或者一个线程去完成收集,更重要的是它在进行回收的时候还会把其它工作线程暂停掉。

虽然这是最基本的收集器,但它依然是虚拟机运行在client模式下的默认新生代收集器。由于没有线程切换的开销,该收集器可以高效率地单线程收集。在一些桌面应用上,分配给虚拟机管理的内存不会太大,因此可以使用该类收集器。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用多线程去回收垃圾之外,其它行为与Serial基本都是一样的。另外,它是许多运行在Server模式下的虚拟机首选的新生代收集器,原因是它与Serial收集器是目前仅有的能与CMS收集器配合工作的。

ParNew收集器在单CPU环境下不一定比Serial收集器效率更好,但当前计算机多数是多核CPU了。我们可以用-XX:+UseParNewGC选项来强制指定它。

Parallel Scavenge收集器

这是一个新生代收集器,采用的复制算法,并且也是并行的多线程收集器。但它的特别之处在于它关注的是获得一个可控制的吞吐量——>CPU用于运行用户代码的时间与CPU总消耗时间的比值。

该收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾回收停顿时间的-XX: MaxGCPauseMillis参数,和直接设置吞吐量大小的-XX: GCTimeRatio参数。其中MaxGCPauseMillis是一个毫秒数,如果设的太小,它可能会调小新生代空间,从而降低了吞吐量。

该收集器还有一个选项-XX: +UseAdaptiveSizePolicy,这是一个开关参数,打开这个参数之后,就不需要手动指定新生代的大小等细节参数了,虚拟机会收集当前运行的系统性能自动调整。

Serial Old收集器

这是Serial收集器的老年代版本,使用的是"标记-整理"算法。

Parallel Old收集器

这是Parallel Scavenge收集器的老年代版本,使用的是多线程和"标记-整理"算法。在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。在重视服务器响应的场景下用得比较多。

CMS收集器是基于"标记-清楚"算法的,它的操作过程分为四个步骤:

  • 初始标记;
  • 并发标记;
  • 重新标记;
  • 并发清除;

其中,初始标记和重新标记都需要"stop the world",初始标记仅仅是标记一下GC Roots能直接关联到的对象,而并发标记则是进行GC Roots tracing的过程,重新标记则是为了修正那些在并发标记阶段中因用户程序继续运行而产生的标记记录,这个过程会稍长。

CMS是一个优秀的收集器,但它有以下的缺点:

  • 对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,随着CPU资源的增加,回收线程的利用率反而下降。另外,如果CPU附在比较大,还需要分出一半的的运算能力去执行收集器线程,那么用户程序的执行速度就更慢了;
  • CPU收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。这是因为CMS并发清理阶段,用户线程还在运行,有可能产生新的垃圾,只能等待下一次GC去清理;
  • 最后一个缺点则是因为"标记-清除"算法可能出现大量的空间碎片。CMS提供了一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用来设置执行多少次Full GC后,跟着带来一次压缩整理;

G1收集器

G1(Garbage-First)收集器是当前收集器的最前沿成果之一,这是一款面向服务端应用的垃圾收集器,具备以下特点:

  • 并行与并发:G1能充分利用多CPU的优势来缩短Stop-The-World的停顿时间;
  • 分代收集:虽然G1收集器可以独立管理整个GC堆,但它仍保留分代概念,以获取更好的收集效果;
  • 空间整合:没有采用CMS的"标记-清理"算法;
  • 可预测的停顿:建立了可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒;

G1收集器中,Java堆的内存布局是被划分为多个大小相等的独立区域,虽然保留了新生代和老年代的概念,但关键是它们不再是物理隔离的,而是一部分region的集合。

G1收集器之所以可以建立时间预测模型,是因为它根据各个region的垃圾堆积价值(回收所获得的空间大小以及回收所需要时间的经验值),维护一个优先队列,每次根据允许的回收时间,优先回收价值最大的Region。

在G1收集器中,Region之间的对象引用以及其它收集器中的新生代与老生代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个region都有一个对应的Remembered Set,虚拟机在发现程序在对引用类型进行写操作时,就会产生一个write barrier来中断写操作,检查该对象是否处于不同的region中。如果是,就会把相关引用记录到对象所属Region的Remembered Set中。这样进行回收的时候,只需要把GC Roots的枚举范围加入Remembered Set即可。

G1的操作步骤为:

  • 初始标记:与CMS一样;
  • 并发标记:进行可达性分析,找出存活对象;
  • 最终标记:主要是修正标记记录;
  • 筛选回收:根据刚刚提到的优先队列进行筛选回收;

如果应用追求地停顿,G1已经可以作为一个选择;如果追求吞吐量,则G1并不会有特别的优势。

理解GC日志

GC日志只是一些认为确定的规则,我们来解读一下:

1
2
33.125:[GC [DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
100.667:[Full GC [Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
  • 最前面的“33.125:”和“100.667:" 代表了GC发生时间(从java虚拟机启动以来经过的秒数);
  • 日志开头“[GC ”和“[Full GC”说明了这次垃圾收集的停顿类型(并不是区分新老生代的)。有"Full"说明这次GC是发生了Stop-The-World的。一般因为出现了分配担保失败之类的问题才会导致STW。如果调用System.gc()方法所触发的收集,那么这里将显示“[Full GC(System)”;
  • “ [DefNew”、“[Tenured”、“[Perm”表示GC发生区域,这里显示区域名称与使用的GC收集器密切相关;
  • 后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”;
  • “0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒;

内存分配与回收策略

JVM的自动内存管理系统主要是解决了两个问题:给对象分配内存和回收分配给对象的内存。

对象优先在Eden分配

Minor GC(新生代GC):指发生在新生代的垃圾收集操作,因为Java对象大多都具备朝生夕灭的特定,所以GC特别频繁,回收速度也比较快;

Major GC(老年代GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。一般会比Minor GC慢十倍以上

大多数情况下,对象直接在新生代的Eden区中分配,但Eden区没有足够的空间进行分配时,虚拟机将会进行一次Minor GC。

大对象直接进入老年代

所谓的大对象指的是需要大量连续内存空间的Java对象,比如哪些很长的字符串或者数组,经常出现大对象的一个直接后果就是导致内存还有不少空间的时候就会提前触发垃圾回收。

虚拟机提供了一个-XX: PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。

长期存活的对象将进入老年代

内存回收需要识别哪些对象应该放在新生代、哪些放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经历了第一次Minor GC后仍然能存活,并且为Survivor容纳的话,将会被移动到Survivor空间中,并且年龄设为1。之后每次经历Minor GC,则年龄增加1岁。当它的年龄增加到默认值15,则会被晋升到老年代中。关于这个阈值,可以通过参数-XX: MaxTenuringThreshold来设置。

动态对象年龄判定

虚拟机并不是严格要求必须达到MaxTenuringThreshold的设置才能晋升老年代。如果在Survivor空间中相同年龄的所有对象的大小之和大于Survivor空间的一半,则年龄大于或者等于该年龄的对象则可以直接进入老年代。

空间分配担保

新生代使用复制收集算法,为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此如果在Minor GC之后大量对象仍然存活,则需要老年代进行担保。

在发送Minor GC之前,虚拟机会先检查老年代的最大可用连续空间是否大于新生代所有对象的总空间,如果成立则进行Minor GC。如果不成立,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则进行一次Minor GC,否则则不进行冒险。

Generative Models

发表于 2019-01-30

Generative Models

概述

Given training data, generate new samples from same distribution.

这是非监督学习的一种,目标是学习生成一个模型。使用Generative Models,我们可以构造一些艺术工作的真实样式,通过时间序列的数据去仿真,还有就是Generative Models可以用来推荐潜在的表示模式。

Generative Models有两种:Explicit density模型和implicit density模型。

生成模型族谱:

img
img

Pixel Rnn & Pixel Cnn

在讨论具体的显式密度模型之前,我们先来看一下完全可见置信网络 Fully visible belief networks。该模型通过使用概率的链式规则来将一个n维的向量x的概率分布分解为一个一维的概率分布: \[ P_{model}(x) = \prod_{i=1}^{n}P_{model}(x_i|x_1,...,x_{i-1}) \]

  • Pixel Rnn

从Corner开始生成图像像素,然后往周边序列化地生成像素,一般可以通过RNN或者LSTN去生成周边的像素。缺点就是序列化生成非常慢。

img
img
  • Pixel Cnn

跟前面一样,从Corner开始生成图像像素。但不同的是,下一个像素的生成是利用上一个像素丢入CNN后生成的。相对pixel rnn会更快,但是总体来说还是比较慢的。

img
img

Autoencoders

这是非监督学习的一种方法,可以从没有标记过的数据中学习到一些低维度的特征。z是比x维度更低的数据,z包含了重要的信息,并且z应该能够学习到可以被抓取的特征。z能够通过decoder重建输入数据。

img
img

GAN

GAN的提出是为了解决从一个复杂的、高维的训练分布数据中进行采样的问题,我们可以从一个简单分布进行采样,例如随机噪声,然后去学习transformation到训练的分布。我们使用神经网络去表示这个复杂的transformation。

Two player game

  • Generator network: try to fool the discriminator by generating real-looking images;
  • Discriminator network: try to distinguish between real and fake images;
img
img

目标函数: \[ \underset{\theta_g}{min}\ \underset{\theta_d}{max} [E_{x-p_{data} }logD_{\theta_d(x)} + E_{x-p_z }log(1-D_{\theta_d}(G_{\theta_g}(z)))] \] 其中,\(D_{\theta_d(x)}\)是真实数据x的Discriminator的输出,\(D_{\theta_d}(G_{\theta_g}(z))\)是假数据的输出。Discriminator想要最大化目标函数,使得D(x)接近1,并且D(G(z))接近0;而Generator则是最小化目标函数,使得D(G(z))接近1。

接口优于抽象类

发表于 2019-01-28

接口优于抽象类

概述

Java有两种机制允许定义一个多实现的类型——接口和抽象类。从Java8起,接口引入了默认方法。因此这两种机制都能让用户为一些实例方法提供实现。

优点

现有类可以很容易被改造以实现一个新的接口。对于一个想要实现新接口的类来说,它只需要添加需要的方法即可。若是使用了抽象类,如果你想要让两个类扩展同一个抽象类,这样就强迫了所有的后带来都要扩展这个父类,而不管这个扩展是否合适。

接口是定义混合类型的理想选择。混合类型:一个类除了实现它的"primary type"之外,还可以声明其提供一些额外的行为。比如Complarable接口,它允许一个类声明其实例可以与其它可相互比较的对象进行排序。因为它在主要功能之外提供了一些额外的行为。而抽象类无法被改造到现有的类当中。

接口构造非层次结构的框架。类层次结构不一定适合一些其它的层次结构中,但接口可以,假设我们有一个接口,这个接口代表了一个歌手(singer),同时还有另一个代表作曲人(songwriter)的接口。我们就可以同时扩展这两个接口。

Neural Style Transfer

发表于 2019-01-28

Neural Style Transfer

概述

计算机视觉新流行的一种算法:Neural Style Transfer。卷积神经网络的出现带来的对图像特征,特别是抽象特征的提取,使得风格和内容的分离成为了可能。

原理

首先来看看架构图:

img
img

从架构图可以看到,input有三个:\(y_s\)风格图片,\(y_c\)内容图片,还有一个生成图片。在更新过程中,CNN网络参数被更新,输出图片也会被更新。

至于损失函数,则为: \[ J(G) = \alpha J_{content} + \beta J_{style} \] alpha和beta的主要作用是平衡风格和内容的博弈。

具体的内容损失函数的计算: \[ J_{content} = || a^{[l][c]} - a^{[l][g]} || ^ 2 \] 在CNN的某一层,我们拿到内容图和输出图的feature map,计算其损失函数(相近度),损失函数越低越好。

难点在于风格的损失函数,我们怎么定义风格呢?所谓风格,应该是在某一层feature map,所有channels之间的correlation。我们用一个gram matrix来描述一张图片所有的channels之间的correlation相关性。

而gram matrix则是,假设某个feature map有多个channels,两两channels之间某个长宽位置固定的点相乘,然后计算所有位置相乘之后求和。最后就可以得到一个矩阵。我们就用这个矩阵来表示style。

我们对输出图和style图都取gram matrix,然后计算: \[ J_{style} = || M^{[l][s]} - <^{[l][g]} || ^ 2 \] 虽然我们可以只取一层求损失函数,但为了使得风格更加饱满,我们可以在每一层都计算损失函数,最后求和: \[ J_style(S, G) = \sum_{\lambda} \lambda^{t} J^{t}(S, G) \]

Problme

  • 风格迁移可能需要很多forward和backward的传到,速度非常慢;

解决方法是,另外训练一个神经网络来执行风格迁移。

Visualizing and Understanding

发表于 2019-01-28

Visualizing and Understanding

概述

这是cs231N的一个章节,主要讲述的是CNN可视化理解。2014年ECCV上的paper:《Visualizing and Understanding Convolutional Networks》讲述了CNN每一层到底学习到了什么特征,即可视化CNN模型。这里的可视化指的是可视化CNN模型中的卷积核。

利用反卷积实现特征可视化

论文里通过反卷积的方法进行可视化,反卷积就是以各层得到的特征图作为输入,进行反池化、反激活、反卷积的过程。例如一个Alexnet的conv5的特征图,通过这个过程之后,就可以把一个13*13的特征图放大回到一个与原输入图片大小的图片(227*227)。

img
img

反池化

池化本身是一个不可逆的过程,但我们可以在池化过程中,将最大激活值的坐标位置记录下来。然后在反池化的时候,只把该位置的值激活,然后其它值置为0。这是一种近似的过程。

反激活

在Alexnet中,relu函数是为了保证每层输出的激活值都是正数,因此对于反向过程,我们需要保证每层的特征图为正值。因此也是直接采用relu函数即可。

反卷积

对于反卷积,则是采用转置后的filter(参数一样,只不过把参数矩阵水平和垂直方向翻转了一下)进行卷积。

可视化结果

通过cnn学习之后,layer1和layer2学习到的特征基本是颜色、边缘等低层特征,越往上,学习到的特征将会变得更加完整,有辨别性。

Saliency Maps

Saliency Maps关注的是当我们使用CNN去抓取图片特征或者理解图片的时候,我们神经网络真正关注的区域。而所有的Saliency Map都将一张图片中最核心表意区域勾画出来了。

而计算原理,则是利用类别得分,即最后一层的梯度去计算哪一个像素对于分类的贡献最大,得到Saliency Maps。这里也可以看出像素层次的不同影响。

同样,得到的图也可以用于图像分割。

img
img

Fooling images

fooling images的思想主要是在一个已经训练好的分类器的基础上,通过选择某一错误分类计算loss,然后更新梯度来更新图片pixel,从而使得分类器对图片错误分类。

  • Start from an arbitrary image
  • Pick an arbitrary class
  • Modify the image to maximize the class
  • Repeat until network is fooled

Class visualization

从随机噪音图像开始并且在目标类上执行梯度上升,我们可以生成这样一个图像,它被网络认为是目标类别。

具体来说,假设\[I\]是一个图像,让\[y\]成为目标类别。假设\(s_y(I)\)是卷积网络初始分配给\(I\)的关于\(y\)的初始分数。最后我们生成一个图片\(I^*\),其将在类别\(y\)上获得高的得分。 \[ I^* = \arg\max_I s_y(I) - R(I) \] R是一个regularizer,一般我们可以用L2正则器: \[ R(I) = \lambda \|I\|_2^2 \]

pytorch知识点

a leaf Variable that requires grad has been used in an in-place operation

这是因为

A “leaf variable” is a variable you create directly, not as the result of some other operation.

For example,

1
2
x = torch.autograd.Variable(torch.Tensor([1, 2, 3, 4]))  # leaf variable
y = x + 1 # not a leaf variable

An in-place operation is something which modifies the data of a variable. For example,

1
2
x += 1  # in-place
y = x + 1 # not in place

PyTorch doesn’t allow in-place operations on variables you create directly (such as parameters of your model) because that usually isn’t correct. You can work around it by either using an operations that’s not in-place or by cloning the variable.

1
2
x2 = x.clone()  # clone the variable
x2 += 1 # in-place operation

继承需要提供文档

发表于 2019-01-27

若要设计继承,则提供文档说明,否则禁止继承

概述

对于那些不是为了继承,并且没有良好说明文档的外来类来说,去继承产生子类是非常危险的。

如何设计文档

  1. 必须在这个类的文档里为可覆盖方法说明它的自用性

对于每个公有方法或者受保护方法,文档里必须指明这个方法调用了哪些可覆盖方法,是以什么顺序调用的,每个调用的结果是如何影响接下来的处理过程。

调用可覆盖方法的方法应该在注释末尾包含这些调用的描述,这可以由Javadoc标签@implSpec生成。

以下为ava.util.AbstractCollection的remove方法的规范:

Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that Objects.equals(o, e), if this collection contains one or more such elements. Returns true if this collection contained the specified element (or equivalently, if this collection changed as a result of the call).

Implementation Requirements:This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator’s remove method. Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection’s iterator method does not implement the remove method and this collection contains the specified object.

  1. 父类必须以某种形式提供能够进入到其内部运转的hook

这里可以选择一个受保护的方法、或者是受保护域。

约束条件

若一个类允许被继承,有几点约束需要遵守:

  1. 构造器一定不能调用可覆盖方法

无论是直接还是间接调用,因为父类构造器在子类构造器之前运行,所以子类的覆盖方法会在子类构造器运行之前被调用。如果覆盖后的方法依赖于子类构造器的任意初始化操作,那么找哥哥方法很可能会产生非预期的行为。

  1. 如何使用Cloneable和Serializable接口

对于Cloneable接口,由于clone方法和readObject方法的行为类似构造器,所以类似上面的约束依然存在。

而对于Serializable接口,而且这个类拥有readResolve方法或writeReplace方法,你一定要把readResolve方法或writeReplace方法设为受保护的,而不是私有的。

总结

设计一个用来被继承的类是不容易的,我们必须在文档里说明该类的自用模式,并且为了让别人编写有效的子类,我们也需要导出一个或多个受保护的方法。

除非我们知道某个类的确是要被子类化,否则最好将声明为final或者保证其没有可访问的构造器来禁止该类被继承。

cuda学习1——Udacity

发表于 2019-01-27

cuda学习1——Udacity

来源

https://classroom.udacity.com/courses/cs344/lessons/55120467/concepts/670743010923

CPU与GPU

异构型计算机(termed heterogeneous)有两种:根据不同的处理器区分——CPU与GPU。cuda编程模型允许我们在GPU上运行。程序运行在CPU的部分成为host,在GPU的部分则是的device,并且还假设host和device有各自分开的内存。在CPU与GPU的关系中,前者占据着重要的位置,它告诉GPU应该做什么

流程:

  1. 从CPU拷贝数据到GPU
  2. 从GPU拷贝数据到CPU

这两部就是cudaMemcpy

  1. 分配GPU内存:cudaMalloc
  2. 在GPU上启动内核

cuda程序例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>

__global__ void square(float * d_out, float * d_in){
int idx = threadIdx.x;//threadIdx is a structure
float f = d_in[idx];
d_out[idx] = f * f;
}

int main(int argc, char ** argv) {
const int ARRAY_SIZE = 64;
const int ARRAY_BYTES = ARRAY_SIZE * sizeof(float);

// generate the input array on the host
float h_in[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++) {
h_in[i] = float(i);
}
float h_out[ARRAY_SIZE];

// declare GPU memory pointers
float * d_in;
float * d_out;

// allocate GPU memory
cudaMalloc((void**) &d_in, ARRAY_BYTES);
cudaMalloc((void**) &d_out, ARRAY_BYTES);

// transfer the array to the GPU
cudaMemcpy(d_in, h_in, ARRAY_BYTES, cudaMemcpyHostToDevice);

// launch the kernel
square<<<1, ARRAY_SIZE>>>(d_out, d_in);

// copy back the result array to the CPU
cudaMemcpy(h_out, d_out, ARRAY_BYTES, cudaMemcpyDeviceToHost);

// print out the resulting array
for (int i =0; i < ARRAY_SIZE; i++) {
printf("%f", h_out[i]);
printf(((i % 4) != 3) ? "\t" : "\n");
}

cudaFree(d_in);
cudaFree(d_out);

return 0;
}

设置kernel启动

以这个为例:

1
2
3
square<<<1, ARRAY_SIZE>>>(d_out, d_in);
//square<<<dim3(bx, by, bz), dim3(tx, ty, tz), shmem>>>(d_out, d_in);
//相对于启动了bx*by*bz个block,每个block具有tx*ty*tz个线程。sheme默认0

我们使用了这些启动参数1, ARRAY_SIZE,并以这些自变量d_out, d_in来启动它。

在这里我们启动了64个线程,即1个带有64个线程的块。对于kernel而言:

  • 能够同时运行多个块;
  • 每个块带有多个线程;较新的GPU可以支持1024个线程,不要超过1024.

threadIdx这个结构中,x, y, z分别表示线程在block中不同纬度的索引。

blockDim:block的大小,有多少个线程;

blockIdx:网格中block的索引;

gridDim:网格大小;

总结

  1. 当我们写一个程序,它看起来是运行在一个线程上;
  2. 当我们启动程序的时候,我们从CPU代码启动这个内核;
  3. 在内核中,每个线程都知道自己所在index;

组合优先于继承

发表于 2019-01-26

组合优于继承

概述

对于继承而言,这是代码复用的一种有效途径。在同一个包中使用继承是安全的,另外,如果类是专门设计来被继承而且具有良好文档,那么采用继承进行扩展也是安全的。请注意,尽量不要跨包去继承一个普通具体的类。

继承违反了封装的原则

一个子类依赖于父类的实现细节来实现本身的功能,这样随着父类的变化,子类将会被破坏。即便子类本身代码没有发生变化,它都必须随着父类演化。

这两个问题——子类实现依赖于父类实现,父类添加方法可能出现漏洞。

组合

为了避免上面的问题,可以让新类包含一个私有域,这个域指向现有类的一个实例。因为现有类成为了新类组件,新类的每个实例方法调用现有类实例的对应方法然后返回值,这就叫转发。

包装者对象几乎没有缺点,但需要注意的是包装者对象不适合于回调框架。因为在回调框架里,对象需要将自身引用传给别的对象,以便别的对象在后续进行调用。因为被包装对象并不知道自己的包装者,它将一个引用传给自己同时回调也避开了包装者。

总结

继承虽然强大,但它存在一些问题,违反了封装,只有父类和子类之间存在真正的父子关系才会使用。为了避免子类的脆弱性,我们应该使用组合和转发,而不是继承。

使可变性最小化

发表于 2019-01-26

使可变性最小化

概述

不可变类是其实例不能被修改的类,并且实例中包含的所有信息都必须在创建实例的时候提供,在整个对象生命周期中固定不变。

原则

为了使类不可变,要遵守:

  • 不要提供任何会修改对象状态的方法;
  • 保证类不会被扩展;
  • 使所有的域都是final的;
  • 使所有的域都变成私有的;
  • 确保对于任何可变组件的互斥访问;

由于不可变对象本质上是线程安全的,所以它们并不要求同步,这是获得线程安全最容易的方法;

另外,不可变对象可以被自由地共享,甚至共享其内部信息;

缺点

不可变类最大的缺点,对于每个不同的值都需要一个单独的对象。对于大型对象的情形,创建这种对象的代价可能很高。

谨慎地覆盖clone

发表于 2019-01-26

谨慎地覆盖clone

概述

Cloneable接口没有任何方法,它表明object允许被clone。这个接口的作用是改变clone方法的行为,使得Object中的clone方法返回对象的逐域拷贝,否则CloneNotSupportedException异常。

Object的clone方法是protected的。

约定

如果实现Cloneable接口是要对某个类起作用,那该类和它的所有超类都必须满足:无需调用构造器就可以创建对象。并且这个拷贝应该满足:

x.clone() != x 为true

x.clone().getClass() == x.getClass() 为true

x.clone().equals(x) 为true

实际上,良好的clone方法可以调用构造器创建对象,之后再去复制内部数据。

super.clone()

假设超类提供了良好的clone方法,那么从super.clone()中得到的对象可能会接近于最终返回的对象,也可能相距甚远。如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么返回对象就可能是符合要求的。

例如

1
2
3
4
5
6
7
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
throw new AssertionError();
}
}

注意,clone方法返回的是PhoneNumber,永远不要让那个客户去做类库能替用户完成的事情。

注意

如果对象中包含的域引用了可变的对象,那这种clone实现可能导致灾难性后果。因为修改原始的实例可能会破坏克隆对象的约束条件。

复杂对象的克隆,需要先调用super.clone,再把结果对象中的所有域设置为空白状态,然后调用higher level去重新设置对象状态。

跟构造器一样,clone不应该调用一个被覆盖的方法,因为该方法可能先执行,导致克隆对象和原始对象不一致。

总结

所有实现了Cloneable接口的类都应该用一个公有的方法去覆盖clone:先调用super.clone,然后修正需要的域,这意味着需要拷贝任何包含内部深层结构的可变对象。

另一个实现对象拷贝的方法是提供一个拷贝构造器或者拷贝工厂。

对于一个专门用于继承而设计的类,如果未能提供行为良好的protected的clone方法,那么它的子类就不可能实现Cloneable接口。

<i class="fa fa-angle-left"></i>1…111213…28<i class="fa fa-angle-right"></i>

278 日志
29 标签
RSS
© 2025 LucienXian
由 Hexo 强力驱动
主题 - NexT.Pisces