LucienXian's Blog


  • 首页

  • 归档

  • 标签

不要使用原始类型

发表于 2019-02-14

不要使用原始类型

概述

首先,泛型类和接口都被成为泛型类型。每个泛型类都定义了一组参数化的类型,例如List<Strimg>就是一个参数化的类型。另外,每个泛型类型都定义了一个原始类型,即List<E>对应的原始类型是List,它的主要目的是为了兼容那些在泛型出现之前写的代码。

原始类型的问题

对于Java9,这样声明仍然是合法的:

1
2
3
4
5
6
7
// My stamp collection. Contains only Stamp instances.
private final Collection stamps = ... ;

for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // Throws ClassCastException
stamp.cancel();
}

并且如果你往集合里添加了一个其它的对象,仍然可以编译运行,最多是得到一个warning。在你尝试获取到Coin对象之前都不会出现错误。

使用了泛型方法后,编译器就知道集合只会包括Stamp实例这一点,插入不合法对象时,也会生成编译时错误:

1
private final Collection<Stamp> stamps = ... ;

你使用了原始类型,你将会失去泛型所带来的安全性和可读性。以原始类型List和参数化类型List<Object>之间的区别为例,前者不接受类型系统的检查,而后者则显示地告诉编译器它可以接受任意类型的对象。

如果使用参数化类型以允许插入任意对象,我们应该使用List<Object>;而对于元素类型未知而且不在乎元素类型的集合,更安全的方式是使用无限制通配符类型List<?>。无法将任意元素(null除外)放入一个Collection<?>。试图这么做的化将产生编译时错误。

例外

对于不能使用原始类型这个规则,有两个例外:一是在类字面值中使用原始类型,例如List.class, int.class;二是与instanceof有关,因为泛型类型信息在运行时是被擦除了的,所以在参数化类型而不是无限制通配符类型上用instanceof操作符是非法的。

隐藏单元——DeepLearning系列

发表于 2019-02-13

隐藏单元

概述

这是前馈神经网络的一个特有研究问题:如何选择隐藏单元的类型。

一般来说,整流线性单元是隐藏单元很好的一个默认选择。另外有一些隐藏单元并不是在所有的输入点上都是可微的,流入:\(g(z)=max\{0, z\}\)在z=0处是不可微的,这使得g对于基于梯度的学习算法无效。但由于神经网络使用的函数通常对左导数和右导数都有定义,在这种情况下,在z=0的左导数是0,右导数是1。神经网络训练的软件实现通常返回左导数或者右导数的其中一个。

除非有特别说明,大多数隐藏单元都可以描述为接收输入向量x,计算仿射变换\(z = W^Tx+b\),然后使用一个逐元素的非线性函数\(g(z)\)。

整流线性单元及其扩展

整流线性单元使用激活函数\(g(z)=max\{0, z\}\)。

在这种情况下,整流线性单元在其一半的定义域上输出为0,并且只要整流线性单元处于激活状态,它的导数都能保持比较大并一致。

整流线性单元通常作用于仿射变换之上: \[ h = g(W^Tx+b) \] 初始化的时候可以把b的所有元素设置为一个较小的正值,使得整流线性单元在初始时就能对大多数输入呈现激活状态。

至于扩展,都是基于当z<0时使用了一个非零的斜率:\(g(z, \alpha)_i=max(0, z_i) + \alpha_imin(0, z_i)\)。

  • 绝对值整流(absolute value rectification)固定\(\alpha_i=-1\),它用于图像中的对象识别,寻找在输入照明极性反转下不变的特征是有意义的;
  • 渗透整流线性单元(Leakly ReLU)将固定\(\alpha_i=0.01\)之类的小值;
  • 参数化整流线性单元(parametric ReLU)则是将其作为学习的参数;

maxout单元进一步做了扩展,它将z划分为每组具有k个值的组,买个maxout单元则输出每组中的最大元素: \[ g(z)_i = max z_j \] maxout 单元可以学习具有多达 k 段的分段线性的凸函数,在k足够大的情况下,maxout 单元可以以任意的精确度来近似任何凸函数。

logistic sigmoid与双曲正切函数

在引入整流线性单元之前,大多数神经网络使用 logistic sigmoid 激活函数: \[ g(z) = \sigma(z) \] 或者双曲正切激活函数: \[ g(z) = tanh(z) \] 其中,\(tanh(z)=2\sigma(2z)-1\)。

sigmoid 单元的广泛饱和性会使得基于梯度的学习变得非常困难。因为这个原因,现在不鼓励将它们用作前馈网络中的隐藏单元。而双曲正切激活函数通常要比 logistic sigmoid 函数表现更好。

而在一些不能使用分段激活函数的场景下,sigmoid的使用会更常见。

基于梯度的学习——DeepLearning系列

发表于 2019-02-13

基于梯度的学习

概述

普通的线性模型和神经网络的最大区别,在于神经网络的非线性容易使得我们感兴趣的代价函数变得非凸,这样就只能使得代价函数达到一个非常小的值,而不能保证全局收敛。

除此之外,对于前馈神经网络,将所有权重值随机化为小随机数是非常重要,偏置bias则应该初始为零或者小的正值。

代价函数

神经网络的一个重要设计就是代价函数的选择,在大多数情况下,参数模型定义了一个分布式\(p(y | x; \theta)\)并且我们使用最大似然原理。

使用最大似然学习条件分布

如果神经网络使用最大似然来训练,这意味着代价函数就是负的对数似然。 \[ J(\theta) = - E_{x, y - p_{data} } log _{p_{model} } (y | x) \] 代价函数的具体形式随着模型而改变,取决于\(log_{Pdata}\)的具体形式。

使用最大似然来导出代价函数的一个好处就是减轻了为每个模型设计代价函数的负担,因为每确定一个模型\(p(y|x)\)则自动确定了一个代价函数\(logp(y|x)\)。另外,为了避免代价函数的梯度不够大,使得预测性减弱,我们可以使用负的对数似然来避免这个问题。另外,用于实现最大似然估计的交叉熵代价函数有一个特性,就是它用于实践模型的时候,通常没有最小值。

学习条件统计量

有时我们并不是想学习一个完整的概率分布,而是想学习在给定x时y的某个条件统计量。

我们可以吧代价函数看做是一个泛函而不仅仅是一个函数,泛函是函数到实数的映射,因此我们可以将学习看做是选择一个函数而不仅仅是一组参数。例如我们可以设计一个代价泛函,使得它的最小值处于一个特殊的函数上,这个函数将x映射到给定x时y的期望值。

对函数求解优化问题需要用到变分法,我们使用变分法导出的第一个结果是解优化问题: \[ f^* = arg min _{f} E_{x, y - Pdata} || y - f(x) || ^ 2 \] 得到 \[ f^*(x) = E_{y - Pdata}(y|x)[y] \] 因此可以看到,如果我们最小化均方误差代价函数,将得到一个函数,它可以用来对每个x的值预测出y的均值。

不同的代价函数给出不同的统计量,例如: \[ f^* = arg min _{f} E_{x, y - Pdata} || y - f(x) || _1 \] 将得到一个函数可以对每个x预测y取值的中位数,这个代价函数被称为平均绝对误差。

输出单元

代价函数的选择与输出单元的选择密切相关,任何可用作输出的神经网络单元,也可以被用作隐藏单元。

用于高斯输出分布的线性单元

一种简单的输出单元是基于仿射变换,这些单元是线性单元。

给定特征h,线性输出单元层产生一个向量\(y'= W^Th+b\)。线性输出层经常被用来产生条件高斯分布的均值: \[ p(y|x) = N (y, y', I) \] 最大化其对数似然此时等价于最小化均方误差。

用于Bernoulli输出分布的sigmoid单元

许多任务需要预测二值型变量y的值,此时最大似然的方法是定义y在x条件下的Bernoulli分布。

为了保证无论何时模型给出错误的答案,总能有一个较大的梯度,而不是梯度为0。因此sigmoid输出单元定义为: \[ y' = \sigma(w^Th+b) \] 这里sigmoid输出单元有两个部分,一个是线性层,另一个则是使用sigmoid激活函数将z转换为概率。

用于Multinoulli输出分布的softmax单元

任何时候,当我们想要表达一个具有n个可能取值的离散型随机变量的分布时,我们都可以使用softmax函数。

其形式为: \[ softmax(z)_i = \frac{exp(z_i)}{\sum_j exp(z_j)} \] softmax输出的综合为1,所以一个单元的值增加必然对应这其它单元值的减少。

将源文件限制为单个顶级类

发表于 2019-02-11

将源文件限制为单个顶级类

虽然Java编译器能让你在一个源文件里定义多个顶级类,但这种操作风险比较大,因为使用哪个定义将会受到源文件传递给编译器的顺序的影响。

举个例子,考虑下面这个源文件,它只包含了一个Main类,这个类指向了另外两个顶级类(Utensil和Dessert)的成员:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
}

假设在一个叫Utensil.java的源文件里:

1
2
3
4
5
6
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}

当然主程序会打印:pancake。

但如果存在另一个叫Dessert.java的源文件里定义了两个相同的类:

1
2
3
4
5
6
7
// Two classes defined in one file. Don't ever do this!
class Utensil {
static final String NAME = "pot";
}
class Dessert {
static final String NAME = "pie";
}

在这个情况,如果编译时使用的命令为javac Main.java Dessert.java,会编译失败,因为编译器发现了重复定义。

如果你用命令“javac Main.java”或“javac Main.java Utensil.java”来编译程序,它的行为将与你写Dessert.java文件之前的行为一样,打印出“pancake”。

但如果用命令“javac Dessert.java Main.java”来编译程序,它将会打印出“potpie”。

这种依赖于编译顺序的代码风格不是我们想要的,因此解决方法就是,永远不要将多个顶级类或者接口放到一个源文件里。

优先考虑静态成员类

发表于 2019-02-09

概述

嵌套类是定义在另一个类中的类,一共有四种嵌套类:静态成员类、非静态成员类、匿名类以及局部类。

静态成员类

静态成员类可以看做是一个普通的类,只是这个类恰好在别的类内部被声明,并且它可以访问外围类的所有成员,即便是私有成员。

静态成员类通用的做法是作为一个公有的辅助类,与它的外围类一起工作。

非静态成员类

语法上来说,静态成员类和非静态成员类之间的区别是,静态成员类在声明上有static标识符。非静态成员类与它的外围类的实例关联,并且可以调用外围实例的方法,或者通过标识了this的构造器来获取外围实例的引用。

在非静态成员类实例被创建时,非静态成员类实例和它的外围实例的关联就被建立了,而且建立后就不能被修改了。

非静态成员类的一个通常的用法是,定义一个适配器:

1
2
3
4
5
6
7
8
9
10
11
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
... // Bulk of the class omitted
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
...
}
}

另外,如果你声明了一个不需要访问外围实例的成员类,那你总是应该static修饰符加到声明里去,这是因为非静态成员类每个实例都会包含一个隐含的外围实例的引用,耗费时间和空间。并且,即便外围实例已经可以被回收,但因为这个非静态的成员类实例,外围实例也会被保留。

私有静态成员类通常被用来展示代表外围类对象的组件。

匿名类

匿名类可以没有名字,它并不是外围类的一个成员。它不仅与其它成员一起被声明,而且它在被使用时同时被声明和初始化。其名称由Java编译器给出,一般是形如:外部类名称+$+匿名类顺序

当且仅当匿名类出现在非静态的上下文当中时,匿名类才有外围实例。但即使它们出现在静态的上下文当中,也不能拥有除了常量型变量的任何的静态成员,这些常量型变量是final的基本类型,或者初始化常量表达式的字符串属性。

局部类

在四种嵌套类里,局部类是最不常用的。在可以声明局部变量的地方就可以声明局部类。

总结

  • 如果一个嵌套类必须在方法外部可见,或者放在方法内部会显得太长时,就使用成员类;
  • 如果成员类的实例需要拥有该类的外围类的引用,就将其做成非静态;不然,就将其做成静态;
  • 假设一个类应当在方法内部,若你需要只从一个地方创建实例而且已经存在一个类型能说明这个类的特征,那么将其做成匿名类;否则,就将其做成局部类;

优先使用类层次,而不是标签类

发表于 2019-02-07

优先使用类层次,而不是标签类

概述

标签类是指这样的类:一个类含有两种或者多种风格的实例,这个类包含了一个指明实例风格的标签,例如:

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
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}

但这种做法是非常糟糕的,因为这里面包含了枚举声明、标签域,还有switch语句。这里面扩展性很差,并且内存中包含了不必要的占用。

我们应该改换成类层次,定义好抽象类,采用继承的方法实现。

接口只用来定义类型

发表于 2019-02-07

接口只用来定义类型

概述

当一个类实现了一个接口,那么这个接口可以作为一个类型,并作为实现它的类实例的引用。这是定义一个接口的目的。

常量接口

但常量接口比较特殊,不包含方法,仅仅由静态final域组成:

1
2
3
4
5
6
7
8
9
// Constant interface antipattern - do not use!
public interface PhysicalConstants {
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}

但常量接口是比较糟糕的做法,一是因为实现一个接口会导致类的导出API泄露了这个实现细节;二是因为如果我们不再需要这些常量,但为了保证二进制兼容仍然需要实现这个接口,如果它还是个非final类,那么它的所有子类命名空间都会被污染。

总结

接口应该只被用来定义类型,它们不能仅仅用来导出常量。

实例:学习XOR——DeepLearning系列

发表于 2019-02-06

实例:学习XOR

前馈神经网络

前馈神经网络(feedforward netural network)是典型的深度学习模型,目标是近似某个函数f*。例如对于分类器,则是

\(y = f^*{(x)}\)将输入x映射到一个类别y,前馈网络定义了一个映射\(y=f(x, \theta)\),并且学习参数\(\theta\)的值,使得它可以获得最佳的函数近似。

前馈神经网络是由许多不同的函数符合组成表示的,例如,我们有这样的结构\(f(x)=f^{(3)}(f^{(2)}(f^{(1)}(x)))\),这种链式结构是比较典型的神经网络结构。前馈网络的最后一层被称为输出层,每个样本x都伴随着一个类别\(y \approx f^*(x)\)。由于训练数据没有指明隐藏层在每一点x上必须做什么,因此学习算法必须要自行决定产生想要的输出。

实例:学习XOR

为了了解前馈网络,我们从一个完整的前馈网络说起:学习XOR函数,这是两个二进制值x1和x2的运算。这个例子中,我们不关心统计泛化,而是希望在四个点{[0, 0], [0, 1], [1, 0], [1, 1]}上都表现正确。

评估整个训练集上表现的的MSE损失函数为: \[ J(\theta) = 1/4 \sum_x (f^*(x) - f(x, \theta))^2 \] 假设我们选择一个线性模型: \[ f(x, w, b) = x^Tw+b \] 我们如果使用正规方程关于w和b最小化\(J(\theta)\),得到w=0以及b=1/2。但这不是一个正确的解,因为直接应用于原始输入的线性模型不能实现XOR函数,当x1=0时,模型的输出必须随着x2的增大而增大。而x1=1时,模型的输出必须随着x2的增大而减小。

因此,我们必须引入一个前馈神经网络,它有一层隐藏层并且隐藏层包含两个单元。这个网络通过函数f(x, W, x)计算的搭配的隐藏单元的向量h,这些隐藏单元的值被用作第二层即输出层,输出层还是一个线性回归模型。在由神经网络提取的特征表示的变换空间中,非线性特征点映射到另外的特征空间,这样就可以使用线性模型。

img
img

由于我们必须使用非线性函数来描述这些特征,因此大多数神经网络通过仿射变换后紧跟着一个激活函数的固定非线性函数来实现这个目标。默认推荐的是使用激活函数\(g(z)=max{0, z}\)定义的整流线性单元或者ReLU。

那么就可以指明现在的整个网络是: \[ f(x, W, c, w, b) = w^T max\{0, W^Tx+c\} + b \]

DESIGN INTERFACES FOR POSTERITY

发表于 2019-02-03

DESIGN INTERFACES FOR POSTERITY

##概述

在Java8之前,要想往接口添加方法,就必须要破坏现有接口的实现类。在Java时,添加了默认方法构造,使得可以在不破坏现有接口实现类的情况下,可以将方法加入现有接口。

风险

虽然Java加入默认方法使得我们可以往现有接口里添加方法,但这并不保证这些方法会在现有的接口实现类里工作。而在Java8之前,这些接口实现类都是基于默认接口不会添加任何新方法的情况下编写的。

在Java8里,很多新的默认方法被加入到核心的集合接口里,以便促进lambda表达式的使用。

The Java libraries’ default methods are high-quality general-purpose implementations, and in most cases, they work fine. But it is not always possible to write a default method that maintains all invariants of every conceivable implementation.

例如,考虑removeIf方法的情况,这个方法在Java 8里被添加进集合接口。

1
2
3
4
5
6
7
8
9
10
11
12
// Default method added to the Collection interface in Java 8
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean result = false;
for (Iterator<E> it = iterator(); it.hasNext(); ) {
if (filter.test(it.next())) {
it.remove();
result = true;
}
}
return result;
}

虽然这已经是该方法最好的通用实现了,但遗憾的是,它在现实中的一些集合框架里是无法工作的。例如,考虑org.apache.commons.collections4.collection.SynchronizedCollection。这个来自Apache公共库的类,并未覆盖removeIf方法。

String vs StringBuilder vs StringBuffer in Java

发表于 2019-02-03

#String vs StringBuilder vs StringBuffer

概述

String类使用final关键字字符数组来保存字符串,private final char value[],所以String对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,而AbstractStringBuilder源码:

1
2
3
4
5
6
7
8
9
10
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}

AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}

性能

在字符串最常用的操作中,比如字符串的拼接。StringBuffer和StringBuilder要远比String类的操作更快。接下来以String和Stringbuffer的比较为例。

String类的拼接操作一般为:

1
2
String str = new String ("Stanford  ");
str += "Lost!!";

我们来看看字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0 new #7 <Class java.lang.String>
3 dup
4 ldc #2 <String "Stanford ">
6 invokespecial #12 <Method java.lang.String(java.lang.String)>
9 astore_1
10 new #8 <Class java.lang.StringBuffer>
13 dup
14 aload_1
15 invokestatic #23 <Method java.lang.String valueOf(java.lang.Object)>
18 invokespecial #13 <Method java.lang.StringBuffer(java.lang.String)>
21 ldc #1 <String "Lost!!">
23 invokevirtual #15 <Method java.lang.StringBuffer append(java.lang.String)>
26 invokevirtual #22 <Method java.lang.String toString()>
29 astore_1

其中0到9是执行String类的初始化,而后面则是拼接操作的字节码,可以看到生成的字节码中创建了一个StringBuffer对象,并且调用了append方法。最后再调用toString()方法转换回String对象。整个过程的操作比较昂贵。

而如果我们使用StringBuffer进行拼接操作:

1
2
StringBuffer str = new StringBuffer ("Stanford ");
str.append("Lost!!");

至于该操作的字节码为:

1
2
3
4
5
6
7
8
9
0 new #8 <Class java.lang.StringBuffer>
3 dup
4 ldc #2 <String "Stanford ">
6 invokespecial #13 <Method java.lang.StringBuffer(java.lang.String)>
9 astore_1
10 aload_1
11 ldc #1 <String "Lost!!">
13 invokevirtual #15 <Method java.lang.StringBuffer append(java.lang.String)>
16 pop

线程安全性

由于String对象是不可变的,因此常量为线程安全的。而StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

总结

  • 操作少量的数据,使用String;
  • 单线程操作字符串,大量数据,使用StringBuilder;
  • 多线程操作字符串,大量数据,使用StringBuilder;
<i class="fa fa-angle-left"></i>1…101112…28<i class="fa fa-angle-right"></i>

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