walker's code blog

coder, reader

cs193p_2021_笔记[1]

2020年看了一遍,后来学深度学习去了,然后发现2021也出来了,仍然是视频授课(对我们没区别),看完后整理了两年课程的笔记。

本文涉及内容:struct, enum, optional, protocol, viewbuilder, shape

struct and class

拥有差不多的结构

  • stored vars
  • computed vars
  • constant lets
  • functions
  • initializers

differents: struct | class -------|------ Value type | Reference type Copied when passed or assigned | Passed around via pointers Copy on write | Automatically reference counted Functional programming | Object-oriented programming No inheritance | Inheritance (single) “Free”(缺省) init initializes ALL vars | “Free” init initializes NO vars Mutability must be explicitly stated | Always mutable (即使用let, 只表示不会改变指针) Your “go to” data structure | Used in specific circumstances Everything you’ve seen so far is a struct (except View which is a protocol) | The ViewModel in MVVM is always a class (also, UIKit (old style iOS) is class-based)

泛型,函数类型,闭包

  • 允许未知类型,但swift是强类型,所以用类型占位符,用作参数时参考.net的泛型
  • 函数也是一种类型,可以当作变量,参数,出现在变量,参数的位置
  • in-line风格的函数叫closure(闭包)

enum

  • 枚举是值类型
  • 枚举的每个state都可以有associated data(等于是把每个state看成一个class/struct,associated data就可以理解为属性)
enum FastFoodMenuItem {
    case hamburger(numberOfPatties: Int)
    case fries(size: FryOrderSize)
    case drink(String, ounces: Int) // the unnamed String is the brand, e.g. “Coke”
    case cookie }

enum FryOrderSize {
    case large
    case small }

let menuItem: FastFoodMenuItem = FastFoodMenuItem.hamburger(patties: 2)
var otherItem: FastFoodMenuItem = FastFoodMenuItem.cookie
var yetAnotherItem = .cookie // Swift can’t figure this out
  1. FryOrderSize同时又是一个枚举
  2. 状态drink拥有两个“属性”,而且其中一个还未命名

break and fall through/defaults

var menuItem = FastFoodMenuItem.cookie
switch menuItem {
    case .hamburger: break  // break
    case .fries: print(fries)
    default: print(other) // default
}
  1. 如果把drink写上,但没有方法体,则叫fall through,只会往后面一个state fall through
  2. 如果漏写了drink,则会匹配到default项(cookie同理)

with associated data

var menuItem = FastFoodMenuItem.drink(Coke, ounces: 32)
  switch menuItem {
      case .hamburger(let pattyCount): print(a burger with \(pattyCount) patties!)
      case .fries(let size): print(a \(size) order of fries!)
      case .drink(let brand, let ounces): print(a \(ounces)oz \(brand))
      case .cookie: print(a cookie!)
 }

可以拥有方法

这就可以扩展出computed vars

enum FastFoodMenuItem { ...
      func isIncludedInSpecialOrder(number: Int) -> Bool {
          switch self {
            case .hamburger(let pattyCount): return pattyCount == number
            case .fries, .cookie: return true // a drink and cookie in every special order 
            case .drink(_, let ounces): return ounces == 16 // & 16oz drink of any kind
 } }
}

Iterable

conform CaseIterable协议就能被遍历,因为增加了一个allCases的静态变量:

enum TeslaModel: CaseIterable {
      case X
      case S
      case Three
      case Y
}
for model in TeslaModel.allCases {
    reportSalesNumbers(for: model)
}
func reportSalesNumbers(for model: TeslaModel) {
    switch model { ... }
}

SwiftUI实例, LazyVGrid中:

struct GridItem {
    ...
    enum Size {
        case adaptive(minimum: CGFloat, maximum: CGFloat = .infinity)
        case fixed(CGFloat)
        case flexible(minimum: CGFloat = 10, maximum: CGFloat = .infinity)
    }
}
  1. associated data还能带默认值
  2. 核心作用是告诉系统griditem的size是采用哪种方案(枚举),顺便设置了这种方案下的参数。所以这种场景在swift下完全可以用枚举做到

Optionals

可靠类型其实就是一个Enum

enum Optional<T> { // a generic type, like Array<Element> or MemoryGame<CardContent> 
    case none
    case some(T) // the some case has associated value of type T }

它只有两个状态,要么是none,要么就是is set的状态,具体的值其实是绑定到了associate data里去了

所以你现在知道了有一种取法其实就是从some里面来取了。

语法糖

var hello: String?
var hello: String? = hello
var hello: String? = nil
// 其实是:
var hello: Optional<String> = .none
var hello: Optional<String> = .some(hello)
var hello: Optional<String> = .none

使用:

let hello: String? = ...
print(hello!) 
// 其实是:
switch hello {
    case .none: // raise an exception (crash) 
    case .some(let data): print(data)
}

if let safehello = hello {
    print(safehello)
} else {
    // do something else
}
// 其实是:
switch hello {
    case .none: { // do something else } 
    case .some(let data): print(data)
}

// 还有一种:

let x: String? = ...
let y = x ?? foo
// 其实是:
switch x {
    case .none: y = foo
    case .some(let data): y = data
}
  1. 所以用!来解包是会报错的原理在此
  2. guard的原理同样是switch
  3. 默认值的原理你应该也能猜到了
  4. 三个语法糖,对应的底层就是一句switch,其实就是.none时的三种处理方案

当然,还可以chain起来 let x: String? = ... let y = x?foo()?bar?.z

// 尝试还原一下:

switch x {
    case .none: y = nil
    case .some(let xval)::
        switch xval.foo() {
            case .none: y = nil
            case .some(let xfooval):
                switch xfooval.bar {
                    case .none: y = nil
                    case .some(let xfbarval):
                        y = xfbarval.z
                }
        }
}

记住每一个句号对应一个switch,然后在.none的状态下安全退出就是?的用法了。

@ViewBuilder

  1. 任意func只读的计算属性都可以标识为@ViewBuilder,一旦标识,它里面的内容将会被解析为a list of Views(也仅仅是这个,最多再加上if-else来选择是“哪些view”,不能再定义变量和写其它代码了)
    • 一个典型例子就是View里面扣出来的代码(比如子view)做成方法,这个方法是需要加上@ViewBuilder的
    • 或者改语法
    • 或者只有一个View,就不会产生语法歧义,也是可以不加@ViewBuilder的
  2. 所以不需要return,而如果你不打标,也是可以通过return来构建view的
    • 但是就不支持默认返回list或通过if-else返view list的语法了
  3. @ViewBuilder也可以标识为方法的参数,表示需要接受一个返回views的函数
init(items: [Item], @ViewBuilder content: @escaping (Item) -> ItemView) {...}

同时也注意一下@escaping,凡是函数返回后才可能被调用的闭包(逃逸闭包)就需要,而我们的view是在需要的时候才创建,或反复移除并重建(重绘)的,显然符合逃逸闭包的特征。

viewbuilder支持的控制流程代码指的是if-elseForEach, 所以for...in...是不行的。

Protocol

接口,协议,约束... 使用场景:

  • 用作类型(Type):
    • func travelAround(using moveable: Moveable)
    • let foo = [Moveable]
  • 用作接口:
    • struct cardView: View
    • class myGame: ObservableObject
    • behaviors: Identifiable, Hashable, ... Animatable
  • 用作约束: struct Game where Content: Equtable // 类 extension Array where Element: Hashable {...} // 扩展 init(data: Data) where Data: Collection, Data.Element: Identifiable // 方法
  • OC里的delegate
  • code sharing (by extension)
    • extension to a protocol
    • this is how Views get forecolor, font and all their other modifiers
    • also `firstIndex(where:) get implemented
    • an extension can add default implementation for a func or a var
      • that's how objectWillChange comes from
    • extension可以作用到所有服从同一协议的对象
      • func filter(_ isIncluded: (Element) -> Bool) -> Array
      • 只为Sequence protocol写了一份filter的扩展代码,但能作用于Array, Range, String, Dictionary
      • 等一切conform to the Sequence protocol的类

SwiftUI的View protocol非常简单,conform 一个返回some viewbody方法就行了,但是又为它写了无数extension,比如foregroundColor, padding, etc. 示意图:

Generics(泛型)

举例:

protocol Identifiable {
    associatedtype ID: Hashable
    var id: ID { get }
}
  1. 不像struct,protocol并不是用Identifiable<ID>来表示泛型,而是在作用域内定义
  2. 上例中,ID既定义了类别别名,还规范了约束
  • 所以你Identifiable的类, 是需要有一个Hashable的ID的
  • 而Hashable的对象,又是需要Equatable的(因为hash会碰撞出相同的结果,需要提供检查相等的方法)
  • -> protocol inheritancee

Shape

  • Shape is a protocol that inherits from View.
  • In other words, all Shapes are also Views.
  • Examples of Shapes already in SwiftUI: RoundedRectangle, Circle, Capsule, etc.
  • by default, Shapes draw themselfs by filling with the current foreground color.
func fill<S>(_ whatToFillWith: S) -> some view where S: ShapeStyle

ShapeStyle protocol turns a Shape into a View: Color, ImagePaint, AngularGradinet, LinearGradient

自定义shape最好用path(系统的已经通过extension实现好了view的body):

func path(in rect: CGRect) -> Path {
    return a Path 
}

一张图说清匈牙利算法(Hungarian-Algorithm)

做多目标跟踪的时候会碰到这个算法,每个人都有自己的说法讲清楚这个算法是干什么的?我的老师就跟我说过是什么给工人分配活干(即理解为指派问题),网上还看到有说红娘尽可能匹配多的情侣等,透过这些感性理解,基本上就能理解大概是最大匹配的问题了。

然后加了限制:后来者优先。即后匹配的抢掉前人已匹配的对象,这个是有数学依据还是只是一种实现思路我就没深究了。

我的理解不会比别人更高级,之所以能用一张图说清楚,只不过是我作图的时候发现可以把过程画在一张图里,只需要把图示标清楚就好了,这样就不需要每一步画一张图了,一旦理解了,哪怕忘了,一瞅这张图也能立刻回忆起来。

先上数据:

import numpy as np

relationship_matrix = np.array([
    [1,1,0,1,0,0,0],
    [0,1,0,0,1,0,0],
    [1,0,0,1,0,0,1],
    [0,0,1,0,0,1,0],
    [0,0,0,1,0,0,0],
    [0,0,0,1,0,0,0]
], dtype=bool)

你可以理解为6个工人,7个工作,6个男孩,7个女孩等,当然,6行7列,这么直观理解也是一点问题都没有的。

算法匹配过程如下:

  • 灰蓝线就是被抢掉的
  • 绿线就是抢夺失败的
  • 紫线是被抢了后找候选成功的
  • 红线是一次性成功的

其中被抢的和抢夺失败的还加了删除线,这是为了强调。匹配成功的就是红线紫线,也就是说,我们匹配出来的是:

[0,1], [1,4], [2,0], [3,2], [4,3]

甚至可以这么表示这个过程:

x0,y0
x1,y1
x2,y0 -> x0,y1 -> x1->y4 (x2抢x0的,x0抢x1的)
x3,y2
x4,y3
x5,y3 -> x4匹配不到新的,抢夺失败,-> x5,null

有没有说清楚?就两步:

  1. 根据关联表直接建立关系
  2. 如果当前C匹配的对象已经被B匹配过了,那么尝试把它抢过来:
  • B去找别的匹配
    • 找到了(A)就建立新的匹配
      • 如果新的匹配(A)也已经被别人(D)匹配了,那么那个“别人(D)”也放弃当前匹配去找别的(递归警告
    • 如果找不到新的匹配,那么C抢夺失败,递归中的D也同理,失败向上冒泡

注意递归怎么写代码就能写出来了:

nx, ny = relationship_matrix.shape    # 6个x,7个y

# 如果x0与y0关联,x3也与y0关联,那么x0去找新的匹配时,需要把y0过滤掉
# 同理x0如果找到下一个y2,y2已被x2关联,那么x2找新的匹配时[y0, y2]都需要过滤掉
# 我们把这个数组存为y_used
y_used = np.zeros((ny,), dtype=bool)  # 存y是否连接上
path = np.full((ny,), -1, dtype=int)  # 存x连接的对象,没有为-1

def find_other_path_and_used(x):
    for y in range(ny):
        if relationship_matrix[x, y] and not y_used[y]:
            y_used[y] = True        # 处于争夺中的y,需要打标,在后续的递归时要过滤掉
            if path[y] == -1 or find_other_path_and_used(path[y]):
                path[y] = x         # 直接连接 和 抢夺成功
                return True
    return False                    # 抢夺失败 和 默认失败

for x in range(nx):
    y_used[:] = False  # empty
    find_other_path_and_used(x)

for y, x in enumerate(path):
    if x != -1:
        print(x, y)

真的写代码实现的时候,难点反而是y_used这个,第一遍代码没考虑这一点,导致递归的时候每次都从$y_0$开始而出现死循环,意识到后把处于争抢状态中的y打个标就好了。

scipy中有一个算法实现了Hungarian algorithm:

from scipy.optimize import linear_sum_assignment

# relationship_matrix是代价矩阵
# 所以我们要代价越小越好,就用1来减
rows, cols = linear_sum_assignment(1-relationship_matrix) 
list(zip(rows, cols))
[(0, 0), (1, 1), (2, 6), (3, 2), (4, 3), (5, 4)]

为什么与上面不一样呢?

  1. (0,0),(1,1)的匹配显然不是我们实现的后来者优先
  2. 他把行看成是工人,列看成是任务,每个工人总要分配个任务,所以(5,4)这种代价矩阵里没有的关联它也做出来了,目的只是让“总代价”最小
(1-relationship_matrix)[rows, cols]  # 总代价为1
array([0, 0, 0, 0, 0, 1])

从它的名字也能看出来,它是理解为指派问题的(assignment)

李宏毅Machine Learning 2021 Spring笔记[4]

Adversarial Attack

给你一张猫的图片,里面加入少许噪音,以保证肉眼看不出来有噪音的存在:

  1. 期望分类器认为它不是猫
  2. 期望分类器认为它是一条鱼,一个键盘...

比如你想要欺骗垃圾邮件过滤器

  • 找到一个与$x^0$非常近的向量x
  • 网络正常输出y
  • 真值为$\hat y$
  • $L(x) = -e(y, \hat y)$
  • $x^* = arg\underset{d(x^0, x) \leq \epsilon}{\rm min}\ L(x)$ 即要找到令损失最大的x
    1. 这里L(x)我们取了反
    2. $\epsilon$越小越好,指的是$x^0$要与x越接近越好(欺骗人眼)
  • 如果还期望它认成是$y^{target}$,那就再加上与其的的损失
  • $L(x) = -e(y, \hat y) + e(y, y^{target})$
  • 注意两个error是反的,一个要求越远越好(真值),一个要求越近越好(target)

怎么计算$d(x^0, x) \leq \epsilon$呢?

图上可知,如果都改变一点点,和某一个区域改动相当大,可能在L2-norm的方式计算出来是一样的,但是在L-infinity看来是不一样的(它只关心最大的变动)。

显然L-infinity更适合人眼的逻辑,全部一起微调人眼不能察觉,单单某一块大调,人眼是肯定可以看出来的。

而如果是语音的话,可能耳朵对突然某个声音的变化反而不敏感,整体语音风格变了却能立刻认出说话的人声音变了,这就要改变方案了。

Attack Approach

如何得到这个x呢?其实就是上面的损失函数。以前我们是为了train权重,现在train的就是x本身了。

  1. 损失达到我们的要求 (有可能这时候与原x相关很远)
  2. 与原x的距离达到我们的要求, 怎么做?
    • 其实就是以$x^0$为中心,边长为$2\epsilon$的矩形才是期望区域
    • 如果update后,$x^t$仍然落在矩形外,那么就在矩形里找一个离它最近的点,当作本轮更新后的$x^t$,进入下一轮迭代

Fast Gradient Sign Method(FGSM): https://arxiv.org/abs/1412.6572

  • 相比上面的迭代方法,FGSM只做一次更新
  • 就是根据梯度,判断是正还是负,然后把原x进行一次加减$\epsilon$的操作(其实等于是落在了矩形的四个点上)
  • 也就是说它直接取了四个点之一作为$x^0$

White Box v.s. Black Box

讲上述方法的时候肯定都在疑惑,分类器是别人的,我怎么可能拿到别人的模型来训练我的攻击器? -> White Box Attack

那么Black Box Attack是怎么实现的呢?

  1. 如果我们知道对方的模型是用什么数据训练的话,我们也可以训练一个类似的(proxy network)
    • 很大概率都是用公开数据集训练的
  2. 如果不知道的话呢?就只能尝试地丢一些数据进去,观察(记录)它的输出,然后再用这些测试的输入输出来训练自己的proxy network了。

Defence

Passive Defense(被动防御)

进入network前加一层filter

  • 稍微模糊化一点,就去除掉精心设计的noise了
    • 但是同时也影响了正常的图像
  • 对原图进行压缩
  • 把输入用Generator重新生成一遍

如果攻击都知道你怎么做了,其实很好破解,就把你的filter当作network的一部分重新开始设计noise,所以可以选择加入随机选择的一些预处理(让攻击者不可能针对性地训练):

Proactive Defense(主动防御)

训练的时候就训练比较不容易被攻破的模型。比如训练过程中加入noise,把生成的结果重新标注回真值。

  • training model
  • find the problem
  • fix it

有点类似于Data Augmentation

仍然阻挡不了新的攻击算法,即你对数据进行augment之外的范围。

Explainable Machine Learning(可解释性)

  • correct answers $\neq$ intelligent
  • 很多行业会要求结果必须可解释
    • 银行,医药,法律,驾驶....

Local Explanation

Why do you thing this image is a cat?

Global Explanation

What does a "cat" look like?

  1. 遮挡或改变输入的某些部分,观察对已知输出的影响
    • (比如拦到某些部分确实认不出图像是一条狗了)
  2. 遮挡或改变输入的某些部分,把两种输出做loss,对比输入变化与loss变化:
    • $|\frac{\varDelta e}{\varDelta x}| \rightarrow \frac{\partial e}{\partial x_n}$

把上述(任一种)每个部分(像素,单词)的影响结果输出,就是:Saliency Map

Saliency Map

图1,2就是为了分辨宝可梦和数码宝贝,人类一般很难区分出来,但机器居然轻松达到了98%的准确率,经过绘制Saliency Map,发现居然就是图片素材(格式)的原因,一个是png,一个是jpg,造成背景一个是透明一个是不透明的。

也就是说,能发现机器判断的依据不是我们关注的本体(高亮部分就是影响最大的部分,期望是在动物身上)

第三张图更可笑,机器是如何判断这是一只马的?居然也不是马的本体,而是左下角,标识图片出处的文字,可能是训练过程中同样的logo过多,造成了这个“人为特征”。

解决方案:

Smooth Gradient

随机给输入图片加入噪点,得到saliency map(们),然后取平均

Integrated gradient(IG)

一个特征在从无到有的阶段,梯度还是明显的,但是到了一定程度,特征再增强,对gradient影响也不大了,比如从片子来判断大象,到了一定长度,一张图也不会“更像大象”

一种思路:https://arxiv.org/abs/1611.02639

global explaination

What does a filter detect?

如果经过某层(训练好的)filter,得到的feature map一些位置的值特别大,那说明这个filter提取的就是这类特征/patten。

我们去"创造"一张包含了这种patten的图片:$X^* = arg\ \underset{X}{\rm max}\sum_i\sum_j a_{ij}$,即这个图片是“训练/learn“出来的,通过找让X的每个元素($a_{ij}$)在被filter乘加后结果最大的方式。 -> gradient ascent

然后再去观察$X^*$有什么特征,就基本上可以认定这个(训练好的)filter提取的是什么样的patten了。

adversarial attack 类似的原理,但这是对单filter而言。如果你想用同样的思路去让输出y越大越好,得到X,看X是什么,得到的X大概率都是一堆噪音。如果能生成图像,那是GAN的范畴了。

于是,尝试再加一个限制,即不但要让y最大,还要让X看起来最有可能像一个数字:

  • $R(X)$: how likely X is a digit
  • $X^* = arg\ \underset{X}{\rm max}y_i + \color{red}{R(X)}$
  • $R(X) = -\sum_{i,j}|X_{i,j}|$ 比如这个规则,期望每个像素越黑越好

Domain Adaptation

Transfer Learning的一种,在训练数据集和实际使用的数据集不一样的时候。 https://youtu.be/qD6iD4TFsdQ

需要你对target domain的数据集有一定的了解。

有一种比较好的情况就是,target domain既有数据,还有标注(但不是太多,如果太多的话就不需要source domain了,直接用target来训练就好了),那就像bert一样,去fine tune结果,要注意的是标本量过小,可能很容易overfitting.

如果target doamin有大量资料,但是没有标注呢?

Domain Adversarial Training

  • 把source domain的network分为特征提取器(取多少层cnn可以视为超参,并不一定要取所有层cnn)和分类器
  • 然后在特征取层之后跟另一个分类器,用来判断图像来自于source还是target(有点像Discriminator
  • 与真值有一个loss,source, target之间也有一个loss,要求找到这样的参数组分别让两个loss最小
  • loos和也应该最小(图中用的是减,但其实$L_d$的期望是趋近于0,不管是正还是负都是期望越小越好)(不如加个绝对值?)
  • 每一小块都有一组参数,是一起训练的
  • 目的就是既要逼近训练集的真值,还要训练出一个网络能模糊掉source和target数据集的差别

Limit

如果target数据集如上图左,显然结果是会比上图右要差一点的,也就是说尽量要保持同分布。在这里用了另一个角度,就是让数据离boundary越远越好

More

  • 如果source 和 target 里的类别不完全一样呢?
    • Universal domain adaptation
  • 如果target既没有label,数据量也非常少(比如就一张)呢?

Domain Generalization

Deep Reinforcement Learning (RL)

  • Environment 给你 Observation
  • Actor 接收入 Observation, 输出 Action
  • Action 反馈给 Environment, 计算出 Reward 反馈给 Actor
  • 要求 Reward 最大

与 GAN 的不同之处,不管是生成器还是判别器,都是一个network,而RL里面,Actor和Reward都是黑盒子,你只能看到结果。

Policy Gradient

https://youtu.be/W8XF3ME8G2I

  1. 先是用很类似监督学习的思路,给每一步的最优(或最差)方案一个label,有label就能做loss。先把它变成一个二分类的问题。
  2. 打分还可以不仅仅是“好”或“不好”,还可以是一个程度,比如1.5比0.5的“支持”力度要大一些,而-10显然意味着你千万不要这么做,非常拒绝。
  3. 比如某一步,可以有三种走法,可以用onehot来表示,其中一种走法可以是[1,0,0]$^T$,表示期望的走法是第一种。
  4. 但是也可以是[-1,0,0]$^T$,标识这种走法是不建议的
  5. 也可以是[3.5,0,0]$^T$等
  6. 后面会用1, -1, 10, 3.5这样的scalar来表示,但要记住其实它们是ont-hot中的那个非零数。

现实世界中很多场景不可能执行完一步后就获得reward,或者是全局最佳的reward(比如下围棋)。

v1

一种思路是,每一步之后,把游戏/棋局进行完,把当前reward和后续所有步骤的reward加一起做reward -> cumulated reward $\rightarrow G_t = \sum_{n=t}^Nr_n$

v2

这种思路仍然有问题,游戏步骤越长,当前步对最终步的影响越小。因此引入一个小于1的权重$\gamma < 1$: $G_1' = r_1 + \gamma r_2 + \gamma^2r_3 + \cdots$

这样越远的权重越小: $G_t' = \sum_{n=t}^N \color{red}{\gamma^{n-t}} r_n$

注意,目前得到的G就是为了给每一次对observation进行的action做loss的对象。

v3

标准化reward。你有10分,是高是低?如果所有人都是20分,那就是低分,所以与G做对比的时候,通常要减去一个合适的值b,让得分的分布有正有负。

Policy Gradient

普通的gradient descent是搜集一遍数据,就可以跑for循环了,而PG不行,你每次得到梯度后,要重采一遍样,其实也很好理解,你下了某一步,经过后续50步后,输了,你的下一轮测试应该是下一盘随机的棋,而不是把更新好的参数再用到同一盘棋去。

还是不怎么好理解,至少要知道,我做参数是不为了训练出这一盘棋是怎么下出来的,而是根据这个(大多是输了的)结果,以及学到的梯度,去下一盘新的棋试试。

Actor Critic

Critic:

  • Given actor $\theta$, how good it is when observing s (and taking action a)

Value function $V^\theta(s)$:

  • 使用actor $\theta$的时候,预测会得到多少的cumulated reward
  • 分高分低其实还是取决于actor,同样的局面,不同的actor肯定拿的分不同。

Monte-Carlo based approach (MC)

蒙特卡洛搜索,正常把游戏玩完,得到相应的G.

Temporal-difference approach (TD)

不用玩完整个游戏,就用前后时间段的数据来得到输出。

关键词:

  • 我们既不知道v(t+1),也不知道v(t),但确实能知道v(t+1)-v(t).

这个例子没看懂,后面七次游戏为什么没有sa了?

v3.5

上文提到的V可以用来作更早提到的b:

  • ${S_t, a_t}\ A_t = G_t' - V^\theta(S_t)$
  • 回顾一下,$V^\theta(S_t)$是看到某个游戏画面时算出来的reward
  • 它包含$S_t$状态下,后续各种步骤的reward的平均值
  • 而$G_t'$则是这一步下的rewared
  • 两个数相减其实就是看你的这一步是比平均水平好还是差
  • 比如你得到了个负值,代表在当前场景下,这个actor执行的步骤是低于平均胜率的,需要换一种走法。

v4

3.5版下,G只有一个样本(一次游戏)的结果,这个版本里,把st再走一步,试难$S_{t+1}$的各种走法下reward的平均值,用它来替换G',而它的值,就是当前的reward加上t+1时刻的V:

  • $r_t + V^\theta(S_{t+1}) - V^\theta(S_t)$

这就是:

Advantage Actor-Critic

就看图而言,感觉就是坚持这一步走完,后续所有可能的rewawrd, 减去, 从这一步开始就试验所有走法的reward

More:

Deep Q Network (DQN)

Reward Shaping

前面说过很多场景要得到reward非常困难(时间长,步骤长,或根本不会结束),这样的情况叫sparse reward,人类可以利用一些已知知识去人为设置一些reward以增强或削弱机器的某些行为。

比如游戏:

  1. 原地不动一直慢慢减分
  2. 每多活一秒也慢慢减分(迫使你去获得更高的reward, 避免学到根本就不去战斗的方式)
  3. 每掉一次血也减分
  4. 每杀一个敌人就加分
  5. 以此类推,这样就不至于要等到一场比赛结束才有“一个”reward

又比如训练机械手把一块有洞的木板套到一根棍子上:

  1. 离棍子越近,就有一定的加分
  2. 其它有助于套进去的规则

还可以给机器加上好奇心,让机器看到有用的“新的东西”也加分。

No Reward, learn from demostration

只有游戏场景才会有明确的reward,大多数现实场景都是没有reward的,比如训练自动驾驶的车,或者太过死板的reward既不能适应变化,也容易被打出漏洞,比如机器人三定律里,机器人不能伤害人类,却没有禁止囚禁人类,又比如摆放盘子,却没有给出力度,等盘子摔碎了,再去补一条𢱨碎盘子就负reward的规则,也晚了,由此引入模仿学习:

Imitation Learning

Life-Long Learning

持续学习,机器学习到一个模型后,继续学下一个模型(任务)。

  1. 为什么不一个任务学一个模型
    • 不可能去存储所有的模型
    • 一个任务的知识不能转移到另一个任务
  2. 为什么不直接用迁移学习(迁移学习只关注迁移后的新任务)

Research Directions

Selective Synaptic Plasticity

选择性的神经突触的可塑性?(Regularization-based Approach)

Catastrophic Forgetting 灾难性的遗忘

在任务1上学到的参数,到任务2里接着训练,顺着梯度到了任务2的最优参数,显然不再是任务1的做以参,这叫灾难性的遗忘

一种思路:

任务2里梯度要更新未必要往中心,也可以往中下方,这样既在任务2的低loss区域,也没有跑出任务1的低loss区域,实现的方式是找到对之前任务影响比较小的参数,主要去更新那些参数。比如上图中,显然$\theta_1$对任务1的loss影响越小,但是更新它之后会显著影响任务2的loss,而$\theta_2$的改动才是造成任务1loss变大的元凶。

Elastic Weight Consolidation(EWC)

Synaptic Intelligence(SI)

Memory Aware Synapses(MAS)

RWalk

Sliced Cramer Preservation(SCP)

Memory Reply

  1. 在训练task1的时候,同时训练一个相应的generator
  2. 训练task2的时候,用task1的generator生成pseudo-data,一起来训练生成新的model
  3. 同时也训练出一个task1&2的generator
  4. ...

Network Compress

pruning (剪枝)

Networks ar typically over-parameterized (there is significant redundant weights or neurons)

  • 可以看哪些参数通常比较大,或值的变化不影响loss(梯度小)-> 权重,为0的次数少 -> 神经元 等等
  • 剪枝后精度肯定是会下降的
  • 需要接着fine-tune
  • 一次不要prune to much
  • 剪参数和剪神经元效果是不一样的
    • 剪参数会影响矩阵运算,继而影响GPU加速

那么为什么不直接train一个小的network呢?

  • 小的network通常很难train到同样的准确率。 (大乐透假说)

Knowledge Distillation (知识蒸馏)

老师模型训练出来的结果,用学生模型(小模型)去模拟(即是模拟整个输出,而不是模拟分类结果),让小模型能达到大模型同样的结果。

一般还会在输出的softmax里面加上温度参数(即平滑输出,不同大小的数除一个大于1的数,显然越大被缩小的倍数也越大,比如100/10=10,少了90,10/10=1, 只少了9,差别也从90变成了9)(或者兴趣个极端的例子,T取无穷大,那么每个输出就基本相等了)

Parameter Quantization

  1. Using less bits to represent a value
  2. Weight clustering
    • 把weights分成预先确定好的簇(或根据分布来确定)
    • 对每簇取均值,用均值代替整个簇里所有的值
  3. represent frequent clusters by less bits, represent rare clusters by more bits
    • Huffman encoding

极限,Binary Weights,用两个bits来描述整个网络,扩展阅读。

Depthwise Separable Convolution

回顾下CNN的机制,参数量是:

  • 卷积核的大小 x 输入图像的通道数 x 输出的通道数
  • ($k\times k$) x in_channel x out_channel

Depthwise Separable Convolution由两个卷积组成:

  1. Depthwise Convolution
    • 很多人对CNN的误解刚好就是Depthwise Convolution的样子,即一个卷积核对应一个输入的channel(事实上是一组卷积核对应所有的输入channel)
    • 因此它的参数个数 k x k x in_channel
  2. PointWise Convolution
    • 这里是为了补上通道与通道这间的关系
    • 于是用了一个1x1的标准卷积(即每一组卷积核对应输入的所有通道)
    • 输出channel也由这次卷积决定
    • 应用标准卷积参数量:(1x1) x in_channel x out_channel

两个参数量做对比, 设in_channel = I, out_channel = O

  1. $p_1 = (k\times k) \times I \times O$
  2. $p_2 = (k\times k) \times I + (1\times 1) \times I \times O = (k\times k) \times I + I \times O$
  3. $\frac{p_2}{p_1} = \frac{I\cdot(k^2 + O)}{I\cdot{k^2\cdot O}}

= \frac{1}{O} + \frac{1}{k^2} \approx \frac{1}{k^2} $

O代表out_channel,大型网络里256,512比比皆是,所以它可以忽略,那么前后参数量就由$k^2$决定了,如果是大小为3的卷积核,参数量就变成1/9了,已经是压缩得很可观了。

Low rank approximation

上面是应用,原理就是Low rank approximation

以全连接网络举例

  1. 如果一个一层的网络,输入N, 输出M,参数为W,那么参数量是MxN
  2. 中间插入一个线性层K
    • 参数变成:V:N->K, U:K->M,
    • 参数量:NxK + KxM
  3. 只要K远小于M和N(比如数量级都不一致),那么参数量是比直接MxN要小很多的
  4. 这也限制了能够学习的参数的可能性(毕竟原始参数量怎么取都行)
    • 所以叫Low rank approximation

to learn more

SqueezeNet

MobileNet

ShuffleNet

Xception

GhostNet

Dynamic Computation

  1. 同一个网络,自己来决定计算量,比如是在不同的设备上,又或者是在同设备的不同时期(比如闲时和忙时,比如电量充足和虚电时)
  2. 为什么不为不同的场景准备不同的model呢?
    • 反而需要更大的存储空间,与问题起源(资源瓶颈)冲突了。

Dynamic Depth

在部分layer之后,每一层都插一个额外的layer,提前做预测和输出,由调用者根据具体情况决定需要多深的depth来产生输出。

训练的时候既要考虑网络终点的loss,还要考虑所有提前结束的layer的softmax结果,加到一起算个大的Loss

Multi-Scale Dense Network(MSDNet)

Dynamic Width

训练的时候(同时?)对不同宽度(即神经元个数,或filter个数)进行计算(全部深度),也是把每种宽度最后产生的loss加起来当作总的Loss

在保留的宽度里,参数是一样的(所以应该就是同一轮训练里的参数了)

Slimmable Neural Networks

Computation based on Sample Difficulty

上述决定采用什么样的network/model的是人工决定的,那么有没有让机器自己决定采用什么网络的呢?

比如一张简单的图片,几层或一层网张就能得到结果,而另一张可能前景和或背景更复杂的图片,需要很多层才能最终把特征提取出来,应用同一个模型的话就有点资源浪费了。

  • SkipNet: Learning Dynamic Routing in Convolutional Networks
  • Runtime Neural Pruning
  • BlockDrop: Dynamic Inference Paths in Residual Networks

Meta Learning

  • 学习的学习。
  • 之前的machine learning,输出是明确的任务,比如是一个数字,还是一个分类;而meta-learning,输出是一个model/network,用这个model,可以去做machine learning的任务。
  • 它就相当于一个“返函数的函数”
  • meta-learning 就是让机器学会去架构一个网络,初始化,学习率等等 $\leftarrow \varPhi$: learnable components
    • categorize meta learning based on what is learnable

不再深入

李宏毅Machine Learning 2021 Spring笔记[3]

CNN

  1. Receptive field

不管是计算机,还是人脑,去认一个物,都是去判断特定的patten(所以就会有错认的图片产生),这也说明,如果神经网络要去辨识物体,是不需要每个神经元都把整张图片看一次的,只需要关注一些特征区域就好了。(感受野, Receptive field)

如果你一直用3x3,会不会看不到大的patten呢?$\rightarrow$ 会也不会。

首先,小的filter当然是不可能看到它的感受野以外的部分,但是,神经网络是多层架构,你这层的输出再被卷一次,这时候每一个数字代表的就是之前的9个像素计算的结果,这一轮的9个数字就是上一层的81个像素(因为stride的原因,大部分是重复的)的计算结果,换言之,感受野大大增强了,也就是说,你只需要增加层数,就可以在小的filter上得到大的patten.

  1. filter & feature map

从神经元角度和全连接角度出发的话,每个框其实可以有自己的参数的(即你用了64步把整个图片扫描完的话,就有64组参数),而事实上为了简化模型,可以让某些框对应同样的参数(参数共享),原因就是同一特征可能出现在多个位置,比如人有两只脚。

再然后,实际上每一次都是用一组参数扫完全图的,意思是在每个角落都只搜索这一个特征

我们把这种机制叫filter,一个filter只找一种特征,乘加出来的结果叫feature map,即这个filter提取出来的特征图。

因此,

  • 你想提取多少个特征,就得有多少个filter
  • 表现出来就成了你这一层输出有多少个channel
  • 这就是为什么你的图片进来是3channel,出来就是N个channel了,取决于你设计了多少个filter
  1. Pooling & subsampling

由于图像的视觉特征,你把它放大或缩小都能被人眼认出来,因此就产生了pooling这种机制,可以降低样本的大小,这主要是为了减小运算量吧(硬件性能足够就可以不考虑它)。

  1. Data Augmentation

CNN并不能识别缩放、旋转、裁切、翻转过的图片,因此训练数据的增强也是必要的。

AlphaGo

layer 1

  1. 能被影像化的问题就可以尝试CNN,围棋可以看成是一张19x19的图片
  2. 每一个位置被总结出了48种可能的情况(超参1)
  3. 所以输入就是19x19x48
  4. 用0来padding成23x23
  5. 很多patten、定式也是影像化的,可以被filter扫出来
  6. 总结出5x5大小的filter就够用了(超参2)
  7. 就用了192个fitler(即每一次output有48层channel)(超参3)
  8. stride = 1
  9. ReLU

layer 2-12

  1. padding成 21x21
  2. 192个 3x3 filter with stride = 1
  3. ReLU

layer 13

  1. 1x1 filter stride = 1
  2. bias
  3. softmax

其中192(个filter)这个超参对比了128,256,384等,也就是说人类并不理解它每一次都提取了什么特征。

subsampling对围棋也有用吗? 上面的结构看出并没有用,事实上,围棋你抽掉一行一列影响是很大的。

Self-Attention

前面说的都是输入为一个向量(总会拉平成一维向量),如果是多个向量呢?有这样的场景吗?

  • 一段文字,每一个文字都用one-hot或word-embedding来表示
    • 不但是多个向量,而且还长短不齐
  • 一段语音,每25ms采样形成一个向量,步长为每10ms重复采样,形成向量序列
    • 400 sample points (16khz)
    • 39-dim MFCC
    • 80-dim filter bank output
    • 参考人类语言处理课程
  • 一个Graph组向量(比如social network)
    • 每个节点(每个人的profile)就是一个向量
  • 一个分子结构
    • 每个原子就是一个one-hot

输出是什么样的?

  1. 一个向量对应一个输出
    • 文字 -> POS tagging
    • 语音 -> a, a, b, b(怎么去重也参考人类语言处理课程)
    • graph -> 每个节点输出特性(比如每个人的购买决策)
  2. 只有一个输出
    • 文字 -> 情绪分析,舆情分析
    • 语音 -> 判断是谁说的
    • graph -> 输出整个graph的特性,比如亲水性如何
  3. 不定输出(由network自己决定)
    • 这就叫seq2seq
    • 文字 -> 翻译
    • 语音 -> 真正的语音识别

self-attention

稍稍回顾一下self attention里最重要的q, k, v的部分:

图示的是q2与所有的k相乘,再分别与对应的v相乘,然后相加,得到q2对应的输出:b2的过程。

下图则是矩阵化后的结论:

具体细节看专题

真正要学的,就是图中的$W^q, W^k, W^v$

Multi-head Self-attention

CNN是Self-attention的特例

Self-attention for Graph

了解更多:https://youtu.be/eybCCtNKwzA

Transformer

Transformer是一个seq2seq的model

以下场景,不管看上去像不像是seq2seq的特征,都可以尝试用seq2seq(trnasformer)来“硬train一发”

  • QA类的问题,送进去question + context,输出answer
    • 翻译,摘要,差别,情感分析,只要训练能套上上面的格式,就有可能
  • 文法剖析,送入是句子,输出是树状的语法结构
    • 把树状结构摊平(其实就是多层括号)
    • 然后就用这个对应关系来当成翻译来训练(即把语法当成翻译)
  • multi-label classification
    • 你不能在做multi-class classification的时候取top-k,因为有的属于一个类,有的属于三个类,k不定
    • 所以你把每个输入和N个输出也丢到seq2seq里去硬train一发,网络会自己学到每个文章属于哪“些”类别(不定个数,也像翻译一样)
  • object dectection

Encoder

Q, K, V(relavant/similarity), zero padding mask, layer normalization, residual等, 具体看self-attention一节。

Decoder

AT v.s. NAT

我们之前用的decoder都是一个一个字地预测(输出的)

  • 所以才有position-mask(用来屏蔽当前位置后面的字)

这种叫Auto Regressive,简称AT,NATNon Auto Regressive

它一次生成输出的句子。

至于seq2seq的输出是不定长的,它是怎么在一次输出里面确定长度的,上图已经给出了几种做法:

  1. 另做一个predictor来输出一个数字,表示应该输出的长度
  2. 直接用一个足够长的做输入(比如300个),那输出也就有300个,取到第一个为止

因为不是一个一个生成了,好处

  1. 可以平行运算。
  2. 输出的长度更可控

NAT通常表现不如AT好 (why? Multi-mmodality)

detail: https://youtu.be/jvyKmU4OM3c (Non-Autoregressive Sequence Generation)

AT

在decoder里最初有让人看不懂的三个箭头从encode的输出里指出来:

其实这就是cross attention

它就是把自己第一层(self-attention后)的输出乘一个$W^q$得到的q,去跟encoder的输出分别乘$W^k, W^v$得到的k和v运算($\sum q \times k \times v$)得到当前位置的输出的过程。

而且研究者也尝试过各种cross attention的方法,而不仅仅是本文中的无论哪一层都用encoder最后一层的输出做q和v这一种方案:

Training Tips

复制机制

一些场景,训练的时候没必要去“生成”阅读材料里提到的一些概念,只需要把它“复制”出来即可,比如上述的人名,专有名字,概念等,以及对文章做摘要等。

Guided Attention

像语音这种连续性的,需要强制指定(guide)它的attention顺序,相对而言,文字跳跃感可以更大,语音一旦不连续就失去了可听性了,一些关键字:

  • Monotonic Attention
  • Location-aware attention

Beam Search

Optimizing Evaluation Metrics / BLEU

  • 训练的时候loss用的是cross entropy,要求loss越小越好,

  • 而在evaluation的时候,我们用的是预测值与真值的BLEU score,要求score越大越好

  • 那么越小的cross entropy loss真的能产生越高的BLEU score吗? 未必

  • 那么能不能在训练的时候也用BLEU score呢? 不行,它太复杂没法微分,就没法bp做梯度了。

Exposure bias

训练时候应用了Teaching force,用了全部或部分真值当作预测结果来训练(或防止一错到底),而eval的时候确实就是一错到底的模式了。

Self-supervised Learning

  • 芝麻街家庭:elmo, bert, erine...
  • bert就是transformer的encoder

Bert

GLUE

GLUE: General Language Understanding Evaluation

基本上就是看以下这九个模型的得分:

训练:

  1. 预测mask掉的词(masked token prediction)
    • 为训练数据集添加部分掩码,预测可能的输出
    • 类似word2vec的C-Bow
  2. 预测下一个句子(分类,比如是否相关)(next sentence prediction)
    • 在句首添加用来接分类结果
    • 来表示句子分隔

下游任务(Downstream Task) <- Fine Tune:

  1. sequence -> class: sentiment analysis
    • 这是需要有label的
    • 节点对的linear部分是随机初始化
    • bert部分是pre-train的
  2. sequence -> sequence(等长): POS tagging
  3. 2 sequences -> class: NLI(从句子A能否推出句子B)(Natural Language Inferencee)
    • 也比如文章下面的留言的立场分析
    • 输出分类结果,用分隔句子
  4. Extraction-based Question Answering: 基于已有文本的问答系统
    • 答案一定是出现在文章里面的
    • 输入文章和问题的向量
    • 输出两个数字(start, end),表示答案在文章中的索引

QA输出:

思路:

  1. inputdocument 的格式把输入摆好
  2. 用pre-trained的bert模型输出同样个数的向量
  3. 准备两个与bert模型等长的向量(比如768维)a, b(random initialized)
  4. a与document的每个向量相乘(inner product)
  5. softmax后,找到最大值,对应的位置(argmax)即为start index
  6. 同样的事b再做一遍,得到end index

Bert train seq2seq

也是可能的。就是你把输入“弄坏”,比如去掉一些字词,打乱词序,倒转,替换等任意方式,让一个decoder把它还原。 -> BART

附加知识

有研究人员用bert去分类DNA,蛋白质,音乐。以DNA为例,元素为A,C,G,T,分别对应4个随机词汇,再用bert去分类(用一个英文的pre-trained model),同样的例子用在了蛋白质和音乐上,居然发现效果全部要好于“纯随机”。

如果之前的实验说明了bert看懂了我们的文章,那么这个荒诞的实验(用完全无关的随意的英文单词代替另一学科里面的类别)似乎证明了事情没有那么简单。

More

  1. https://youtu.be/1_gRK9EIQpc
  2. https://youtu.be/Bywo7m6ySlk

Multi-lingual Bert

GPT-3

训练是predict next token...so it can do generation(能做生成)

Language Model 都能做generation

https://youtu.be/DOG1L9lvsDY

别的模型是pre-train后,再fine-tune, GPT-3是想实现zero-shot,

Image

SimCLR

BYOL

Speech

在bert上有九个任务(GLUE)来差别效果好不好,在speech领域还缺乏这样的数据库。

Auto Encoder

也是一种self-supervised Learning Framework -> 也叫 pre-train, 回顾:

在这个之前,其实有个更古老的任务,它就是Auto Encoder

  • 用图像为例,通过一个网络encode成一个向量后,再通过一个网络解码(reconstrucion)回这张图像(哪怕有信息缺失)
  • 中间生成的那个向量可以理解为对原图进行的压缩
  • 或者说一种降维

降维的课程:

有一个de-noising的Auto-encoder, 给入的是加了噪音的数据,经过encode-decode之后还原的是没有加噪音的数据

这就像加了噪音去训练bert

Feature Disentangle

去解释auto-encoder压成的向量就叫Feature Disentagle,比如一段音频,哪些是内容,哪些是人物;一段文字,哪些表示语义,哪些是语法;一张图片,哪些表示物体,哪些表示纹理,等。

应用: voice conversion -> 变声器

传统的做法应该是每一个语句,都有两种语音的资料,N种语言/语音的话,就需要N份。有Feature Disentangle的话,只要有两种语音的encoder,就能知道哪些是语音特征,哪些是内容特征,拼起来,就能用A的语音去读B的内容。所以前提就是能分析压缩出来的向量。

Discrete Latent Representation

如果压缩成的向量不是实数,而是一个binary或one-hot

  • binary: 每一个维度几乎都有它的含义,我们只需要看它是0还是1
  • one-hot: 直接变分类了。-> unsupervised classification

VQVAE

Text as Representation

如果压缩成的不是一个向量,而也是一段word sequence,那么是不是就成了summary的任务? 只要encoder和decoder都是seq2seq的model

-> seq2seq2seq auto-encoder -> unsupervised summarization

事实上训练的时候encoder和decoder可能产生强关联,这个时候就引入一个额外的discriminator来作判别:

有点像cycle GAN,一个generator接一个discriminator,再接另一个generator

abnormal detection

李宏毅Machine Learning 2021 Spring笔记[2]

Optimization

真实世界训练样本会很大,

  • 我们往往不会把整个所有数据直接算一次loss,来迭代梯度,
  • 而是分成很多小份(mini-batch)每一小份计算一次loss(然后迭代梯度)
  • 下一个小batch认前一次迭代的结果
  • 也就是说,其实这是一个不严谨的迭代,用别人数据的结果来当成本轮数据的前提
    • 最准确的当然是所有数据计算梯度和迭代。
    • 一定要找补的话,可以这么认为:
      • 即使一个小batch,也是可以训练到合理的参数的
      • 所以前一个batch训练出来的数据,是一定程度上合理的
      • 现在换了新的数据,但保持上一轮的参数,反而可以防止过拟合

minibatch还有一个极端就是batchsize=1,即每次看完一条数据就与真值做loss,这当然是可以的,而且它非常快。但是:

  1. 小batch虽然快,但是它非常noisy(及每一笔数据都有可能是个例,没有其它数据来抵消它的影响)
  2. 因为有gpu平行运算的原因,只要不是batch非常大(比如10000以上),其实mini-batch并不慢
  3. 如果是小样本,mini-batch反而更快,因为它一来可以平行运算,在计算gradient的时候不比小batch慢,但是它比小batch要小几个数量级的update.

仍然有个但是:实验证明小的batch size会有更高的准确率。

两个local minimal,右边那个认为是不好的,因为它只要有一点偏差,与真值就会有巨大的差异。但是没懂为什么大的batch会更容易落在右边。

这是什么问题?其实是optimization的问题,后面会用一些方法来解决。

Sigmoid -> RelU

前面我们用了soft的折线来模拟折线,其实还可以叠加两个真的折线(ReLU),这才是我一直说的整流函数的名字的由来。

仔细看图,c和c'在第二个转折的右边,一个是向无穷大变,一个是向无穷小变,只要找到合理的斜率,就能抵消掉两个趋势,变成一条直线。

如果要用ReLU,那么简单替换一下:

  • $y = b + \sum_i {\color{ccdd00}{c_i}} sigmoid(\color{green}{b_i} + \sum_j \color{blue}{w_{ij}} x_j)$
  • $y = b + \sum_{\color{red}2i} {\color{ccdd00}{c_i}} \color{red}{max}(\color{red}0,\ \color{green}{b_i} + \sum_j \color{blue}{w_{ij}} x_j)$

红色的即为改动的部分,也呼应了2个relu才构成一个sigmoid的铺垫。

把每一个a当成之前的x,我们可以继续套上新的w,b,c等,生成新的a->a'

而如果再叠一层,在课程里的资料里,在训练集上loss仍然能下降(到0.1),但是在测试集里,loss反而上升了(0.44),这意味着开始过拟合了。

这就是反向介绍神经元和神经网络。先介绍数学上的动机,组成网络后再告诉你这是什么,而不是一上来就给你扯什么是神经元什么是神经网络,再来解释每一个神经元干了什么。

而传统的神经网络课程里,sigmoid是在逻辑回归里才引入的,是为了把输出限定在1和0之间。显然这里的目的不是这样的,是为了用足够多的sigmoid或relu来逼近真实的曲线(折线)

Framework of ML

通用步骤:

  1. 设定一个函数来描述问题$y = f_\theta(x)$, 其中$\theta$就是所有未知数(参数)
  2. 设定一个损失函数$L(\theta)$
  3. 求让损失函数尽可能小的$\theta^* = arg\ \underset{\theta}{\rm min}L(\theta)$

拟合不了的原因:

  1. 过大的loss通常“暗示”了模型不合适(model bias),比如上面的用前1天数据预测后一天,可以尝试改成前7天,前30天等。
    • 大海里捞针,针其实不在海里
  2. 优化问题,梯度下降不到目标值
    • 针在大海里,我却没有办法把它找出来

如何判断是loss optimization没做好?

用不同模型来比较(更简单的,更浅的)

上图中,为什么56层的表现还不如20层呢?是overfitting吗?不一定

我们看一下在训练集里的表现,56层居然也不如20层,这合理吗? 不合理

但凡20层能做到的,多出的36层可以直接全部identity(即复制前一层的输出),也不可能比20层更差(神经网络总可以学到的)

这时,就是你的loss optimization有问题了。

如何解决overfitting

  1. 增加数据量
    • 增加数据量的绝对数量
    • data augmentation数据增强(比如反复随机从训练集里取,或者对图像进行旋转缩放位移和裁剪等)
  2. 缩减模型弹性
    • (低次啊,更少的参数「特征」啊)
    • 更少的神经元,层数啊
    • 考虑共用参数
    • early stopping
    • regularization
      • 让损失函数与每个特征系数直接挂勾,就变成了惩罚项
      • 因为它的值越大,会让损失函数越大,这样可以“惩罚”过大的权重
    • dropout
      • 随机丢弃一些计算结果

Missmatch

课上一个测试,预测2/26的观看人数(周五,历史数据都是观看量低),但因为公开了这个测试,引起很多人疯狂点击,结果造成了这一天的预测结果非常差。

这个不叫overfitting,而是mismatch,表示的是训练集和测试集的分布是不一样的

mismatch的问题,再怎么增加数据也是不可能解决的。

optimization problems

到目前为止,有两个问题没有得到解决:

  1. loss optimization有问题怎么解决
    • 其实就是判断是不是saddle point(鞍点)
  2. mismatch怎么解决

saddle point

hessian矩阵是二次微分,当一次微分为0的时候,二次微分并不一定为0。这是题眼。

对于红杠内的部分,设$\theta - \theta^T = v$,有:

  • for all v: $v^T H v > 0 \rightarrow \theta'$附近的$\theta$都要更大
    • -> 确实是在local minima
  • for all v: $v^T H v < 0 \rightarrow \theta'$附近的$\theta$都要更小
    • -> 确实是在local maxima
  • 而时大时小,说明是在saddle point

事实上我们不可能去检查所有的v,这里用Hessian matrix来判断:

  • $\rm H$ is positive definite $\rightarrow$ all eigen values are positive $\rightarrow$ local minimal
  • $\rm H$ is negative definite $\rightarrow$ all eigen values are negative $\rightarrow$ local maximal

用一个很垃圾的网络举例,输入是1,输出是1,有w1, w2两层网络参数,因为函数简单,两次微分得到的hessian矩阵还是比较简单直观的:

由于特征值有正有负,我们判断在些(0, 0)这个critical point,它是一个saddle point.

如果你判断出当前的参数确实卡在了鞍点,它同时也指明了update direction!

图中,

  1. 先构建出了一个小于0的结果,以便找到可以让$L(\theta)$收敛的目标
  2. 这个结果依赖于找到这样一个u
    • 这个u是$\theta, \theta'$相减的结果
    • 它还是$H$的eigen vector
    • 它的eigen value$\rightarrow \lambda$ 还要小于0

实际上,eigen value是可以直接求出来的(上例已经求出来了),由它可以推出eigen vector,比如[1, 1]$^T$(自行补相关课程),往往会一对多,应该都是合理的,我们顺着共中一个u去更新$\theta$,就可以继续收敛loss。

实际不会真的去计算hessian matrix?

Momentum

不管是较为平坦的面,还是saddle point,如果小球以图示的方式滚下去,真实的物理世界是不可能停留在那个gradient为0或接近于0的位置的,因为它有“动量”,即惯性,甚至还可能滚过local minima,这恰好是我们需要的特性。

不但考虑当前梯度,还考虑之前累积的值(动量),这个之前,是之前所有的动量,而不是前一步的: $$ \begin{aligned} m^0 &= 0 \ m^1 &= -\eta g^0 \ m^2 &= -\lambda \eta g^0 - \eta g^1 \ &\vdots \end{aligned} $$

adaptive learning rate

不是什么时候loss卡住了就说明到了极点(最小值,鞍点,平坦的点)

看下面这个error surface,两个参数,一个变动非常平缓,一个非常剧烈,如果应用相同的learning rate,要么反复横跳(过大),要么就再也挪不动步(太小):

Adagrad (Root Mean Square)

于是有了下面的优化方法,思路与l2正则化差不多,利用不同参数本身gradient的大小来“惩罚”它起到的作用。

  1. 这里用的是相除,因为我的梯度越小,步伐就可以跨得更大了。
  2. 并且采用的是梯度的平方和(Root Mean Square)

图中可以看出平缓的$\theta_1$就可以应用大的学习率,反之亦然。这个方法就是Adagrad的由来。不同的参数用不同的步伐来迭代,这是一种思路。

这就解决问题了吗?看下面这个新月形的error surface,不卖关子了,这个以前接触的更多,即梯度随时间的变化而不同,

RMSProp

这个方法是找不到论文的。核心思想是在Adagrad做平方和的时候,给了一个$\alpha$作为当前这个梯度的权重(0,1),而把前面产生的$\sigma$直接应用$(1-\alpha)$:

  • $\theta_i^{t+1} \leftarrow \theta_i^t - \frac{\eta}{\color{red}{\sigma_i^t}} g_i^t$
  • $\sigma_i^t = \sqrt{\alpha(\theta_i^{t-1})^2 + (1-\alpha)(g_i^t)^2}$

Adam: (RMSProp + Momentum)

Learning Rate Scheduling

终于来到了最直观的lr scheduling部分,也是最容易理解的,随着时间的变化(如果你拟合有效的话),越接近local minima,lr越小。

而RMSProp一节里说的lr随时间变化并不是这一节里的随时间变化,而是设定一个权重,始终让当前的梯度拥有最高权重,注重的是当前与过往,而schedule则考量的是有计划的减小。

下图中,应用了adam优化后,由于长久以来横向移动累积的小梯度会突然爆发,形成了图中的局面,应用了scheduling后,人为在越靠近极值学习率越低,很明显直接就解决了这个问题。

warm up没有在原理或直观上讲解更多,了解一下吧,实操上是很可行的,很多知名的网络都用了它:

要强行解释的话,就是adam的$\theta$是一个基于统计的结果,所以要在看了足够多的数据之后才有意义,因此采用了一开始小步伐再增加到大步伐这样一个过度,拿到足够的数据之后,才开始一个正常的不断减小的schedule的过程。

更多可参考:RAdam: https://arxiv.org/abs/1908.03265

Summary of Optimization

回顾下Momentum,它就是不但考虑当前的梯度,还考虑之前所有的梯度(加起来),通过数学计算,当然是能算出它的”动量“的。

那么同样是累计过往的梯度,一个在分母($\theta$),一个在分子(momentum),那不是抵消了吗?

  1. momentum是相加,保留了方向
  2. $\sigma$是平方和,只保留了大小

Batch Normalization

沿着cost surface找到最低点有一个思路,就是能不能把山“铲平”?即把地貌由崎岖变得平滑点? batch normalization就是其中一种把山铲平的方法。

其实就是人为控制了error的范围,让它在各个feature上面的“数量级”基本一致(均值0,方差1),这样产生的error surface不会出现某参数影响相当小,某些影响又相当大,而纯粹是因为input本身量级不同的原因(比如房价动以百万计,而年份是一年一年增的)

error surface可以想象成每一个特征拥有一个轴(课程用二到三维演示),BN让每条轴上的ticks拥有差不多的度量。

然后,你把它丢到深层网络里去,你的输出的分布又是不可控的,要接下一个网络的话,你的输出又成了下一个网络的输入。虽然你在输出前nomalization过了,但是可能被极大和极小的权重w又给变了了数量级不同的输出

再然后,不像第一层,输入的数据来自于训练资料,下一层的输入是要在上一层的输出进行sigmoid之后的

再然后,你去看看sigmoid函数的形状,它在大于一定值或小于一定值之后,对x的变化是非常不敏感了,这样非常容易了出现梯度消失的现象。

于是,出于以下两个原因,我们都会考虑在输出后也接一次batch normalization::

  1. 归一化($\mu=0, \delta=1$)
  2. 把输入压缩到一个(sigmoid梯度较大的)小区间内

照这个思路,我们是需要在sigmoid之前进行一次BN的,而有的教材会告诉你之前之后做都没关系,那么之后去做就丧失了以上第二条的好处。

副作用

  • 以前$x_1 \rightarrow z_1 \rightarrow a_1$
  • 现在$\tilde z_1$是用所有$z_i$算出来的,不再是独立的了

后记1

最后,实际还会把$\tilde z_i$再这么处理一次:

  • $\hat z_i = \gamma \odot \tilde z_i + \beta$

不要担心又把量级和偏移都做回去了,会以1和0为初始值慢慢learn的。

后记2

推理的时候,如果batch size不够,甚至只有一条时,怎么去算$\mu, \sigma$呢?

pytorch在训练的时候会计算moving averageof $\mu$ and $\sigma$ of the batches.(每次把当前批次的均值和历史均值来计算一个新的历史均值$\bar \mu$)

  • $\bar \mu \leftarrow p \bar \mu + (1-p)\mu_t$

推理的时候用$\bar \mu, \bar \sigma$。

最后,用了BN,平滑了error surface,学习率就可以设大一点了,加速收敛。

Classification

用数字来表示class,就会存在认为1跟2比较近与3比较远的可能(从数学运算来看也确实是的,毕竟神经网络就是不断地乘加和与真值减做对比),所以引入了one-hot,它的特征就是class之间无关联。

恰恰是这个特性,使得用one-hot来表示词向量的时候成了一个要克服的缺点。预测单词确实是一个分类问题,然后词与词之间却并不是无关的,恰恰是有距离远近的概念的,而把它还原回数字也解决不了问题,因为单个数字与前后的数字确实近了,但是空间上还是可以和很多数字接近的,所以向量还是必要的,于是又继续打补丁,才有了稠密矩阵embedding的诞生。

softmax

softmax的一个简单的解释就是你的真值是0和1的组合(one-hot),但你的预测值可以是任何数,因为你需要把它normalize到(0,1)的区间。

当class只有两个时,用softmax和用sigmoid是一样的。

loss

可以继续用MeanSquare Error(MSE) $ e = \sum_i(\hat y_i - y'_i)^2$,但更常用的是:

Cross-entropy

$e = - \sum_i \hat y_i lny'_i$

Minimizing cross-entropy is equivalent to maximizing likelihood

linear regression是想从真值与预测值的差来入手找到最合适的参数,而logistic regression是想找到一个符合真值分布的的预测分布。

在吴恩达的课程里,这个损失函数是”找出来的“:

  1. 首先,$\theta x$后的值可以是任意值,所以再sigmoid一下,以下记为hx
  2. hx的意思就是y为1的概率
  3. 我需要一个损失函数,希望当真值是0时,预测y为1的概率的误差应该为无穷大
    • 也就是说hx=0时,损失函数的结果应该是无穷大
    • 而hx=1时, 损失应该为0
  4. 同理,当y为1时,hx=0时损失应该是无穷大,hx=1时损失为0
  5. 这时候才告诉你,log函数刚好长这样,请回看上面的两张图

而别的地方是告诉你log是为了把概率连乘变成连加,方便计算。李宏毅这里干脆就直接告诉你公式长这样了。。。

这里绕两个弯就好了:

  1. y=1时,预测y为1的概率为1, y=0时,应预测y=1的概率为0
  2. 而这里是做损失函数,所以预测对了损失为0,错了损失无穷大
  3. 预测为1的概率就是hx,横轴也是hx

课程里说softmax和cross entorpy紧密到pytorch里直接就把两者结合到一起了,应用cross entropy的时候把softmax加到了你的network的最后一层(也就是说你没必要手写)。这里说的只是pytorch是这么处理的吗?

----是的

CE v.s. MSE

数学证明:http://speech.ee.ntu.edu.tw/~tlkagk/courses/MLDS_2015_2/Lecture/Deep%20More%20(v2).ecm.mp4/index.html

单看实验结果,初始位置同为loss较大的左上角,因为CE有明显的梯度,很容易找到右下角的极值,但是MSE即使loss巨大,但是却没有梯度。因此对于逻辑回归,选择交叉熵从实验来看是合理的,数学推导请看上面的链接。

李宏毅Machine Learning 2021 Spring笔记[1]

纯听课时一些思路和笔记,没有教程作用。 这个课程后面就比较水了,大量的全是介绍性的东西,也罗列了大量的既往课程和论文,如果你在工作过研究中碰到了它提过的场景或问题,倒是可以把它作索引用。

Linear Model

Piecewise Linear

线性模型永远只有一条直线,那么对于折线(曲线),能怎样更好地建模呢?这里考虑一种方法,

  1. 用一个常数加一个整流函数
    • 即左右两个阈值外y值不随x值变化,阈值内才是线性变化(天生就拥有两个折角)。
  2. 每一个转折点加一个新的整流函数

如下:

如果是曲线,也可以近似地理解为数个折线构成的(取决于近似的精度),而蓝色的整流函数不好表示,事实上有sigmoid函数与它非常接近(它是曲线),所以蓝线又可以叫:Hard Sigmoid

所以,最终成了一个常数(bias)和数个sigmoid函数来逼近真实的曲线。同时,每一个转折点i上s函数的具体形状(比如有多斜多高),就由一个新的线性变换来控制:$b_i + w_ix_n$,把i累积的线性变换累加,就得到与$x_n$最可能逼近的曲线。

下图演示了3个转折点的情况:

至此,一个简单的对b,w依赖的函数变成了对($w_i, b_i, c_i$)和, x, b的依赖,即多了很多变量。

  • $y = b + wx_1$
  • $y = b + \sum_i c_i sigmoid(b_i + w_i x_{\color{red} 1})$

注意这个$x_1$,即只转了一个x就要堆一个sum,而目前也只是演示了只有一个特征的情况。

如果更复杂一点的模型,每次不是看一个x,而看n个x,(比如利用前7天的观看数据来预测第8天的,那么建模的时候就是每一个数都要与前7天的数据建立w和b的关系):

其实就是由一个feature变成了n个feature了,一般的教材会用不同的feature来讲解(比如影响房价的除了时间,还有面积,地段等等),而这里只是增加了天数,可能会让人没有立刻弄清楚两者其实是同一个东西。其实就是x1, x2, x3...不管它们对应的是同一特征,而是完全不同的多个角度的特征。

现在就有一堆$wx$了

  • $y = b + \sum_j w_j x_j$
  • 现在就变成了(注意,其实就是把加号右边完整代入):
  • $y = b + \sum_i c_i sigmoid(b_i + \color{red}{\sum_j w_{ij} x_j})$

展开计算,再根据特征,又可以看回矩阵了(而不是从矩阵出发来思考):

矩阵运算结果为(r),再sigmoid后,设结果为a:

  • $a_i = c_i \sigma(r_i)$
  • $y = b + \sum_i a_i$ c 和 a要乘加,仍然可以矩阵化(其实是向量化):
  • $y = b + c^T a$, 把上面的展开回去:
  • $y = b + c^T \sigma(\bold b + W x)$
    • 前后两个b是不同的,一个是数值,一个是向量

这里,我们把目前所有的“未知数”全部拉平拼成了一个向量 $\theta$:

这里,如果把$c^T$写成W'你会发现,我们已经推导出了一个2层的神经网络:一个隐层,一个输出层:

  • b+wx 是第一层 得到a
  • a进行一次sigmoid(别的教材里会说是激活)得到a'
  • a'当作输入,再进行一次 b+wx (这就是隐层了)
  • 得到的输出就是网络的输出o

这里在用另一个角度来尝试解释神经网络,激活函数等,但要注意,sigmoid的引入原本是去”对着折线描“的,也就是说是人为选择的,而这里仍然变成了机器去”学习“,即没有告诉它哪些地方是转折点。也就是说有点陷入了用机器学习解释机器学习的情况。

但是如果是纯曲线,那么其实是可以无数个sigmoid来组合的,就不存在要去拟合某些“特定的点”,那样只要找到最合适“数量”的sigmoig就行了(因为任何一个点都可以算是折点)

Loss

loss 没什么变化,仍旧是一堆$\theta$代入后求的值与y的差,求和。并期望找到使loss最小化的$\theta$:

$\bold \theta = arg\ \underset{\theta}{min}\ L$

《Deep Learning with Python》笔记[7]

Generative deep learning

Our perceptual modalities, our language, and our artwork all have statistical structure. Learning this structure is what deep-learning algorithms excel at.

Machine-learning models can learn the statistical latent space of images, music, and stories, and they can thensample from this space, creating new artworks with characteristics similar to those the model has seen in its training data.

Text generation with LSTM

Language model

很多地方都在按自己的理解定义language model,这本书定义很明确,能为根据前文预测下一个或多个token建立概率模型的网络。

any network that can model the probability of the next token given the previous ones is called a language model.

  1. 所以首先,它是一个network
  2. 它做的事是model一个probability
  3. 内容是the next token
  4. 条件是previous tokens

一旦你有了这样一个language model,你就能sample from it,这就是前面笔记里的sample from lantent space, 然后generate了。

greedy sampling and stochastic sampling

如果根据概率模型每次都选“最可能”的输出,在连贯性上被证明是不好的,而且也丧失了创造性,所以还是给了一定的随机性能选到“不那么可能”的输出。

因为人类思维本身也是跳跃的。

考虑两个输出下一个token时的极端情况:

| | |

------- | ------- | ------- | ------- 纯随机,所有可选词的概率是均等的 | 毫无意义 | max entropy | 创造性高 greedy sampling | 毫无生趣 | minimum entropy | 可预测性高

实现方式:softmax temperature

除一个温度,如果温度大于1,那么温度越大,被除数缩幅度就越大(这样温差就越小,分布会更平均)-> 偏向了纯随机的概率结构(均等)

import numpy as np
def reweight_distribution(original_distribution, temperature=0.5):
    distribution = np.log(original_distribution) / temperature
    distribution = np.exp(distribution)
    return distribution / np.sum(distribution)

写成公式 $ \frac{e^{\frac{log(d)}{T}}}{\sum e^{\frac{log(d)}{T}}} $ 这是对温度和sigmoid做了融合:

  1. 一个是对目标分布取自然对数后除温度再当成e的指数给幂回去(如果不除温度,那就是先log再e,等于是原数)
  2. 标准的sigmoid方程

这里回顾一个概念:Sampling from a space

书里大量用了这个概念,结合代码,其实就是一个predict函数,也就是说,一般人理解的“预测,推理”,是从业务逻辑方面来理解,作者更愿意从统计学和线性代数角度来理解。

两种训练方法:

  1. 每次用N个字,来预测第N+1个字,即output只有1个(voc_size, 1),训练的是language model
  2. 每次用N个字(a, b), 来预测(a+1, b+1), output有N个(voc_size, N),训练的是特定的任务,比如写诗,作音乐

过程:

  1. 准备数据,X为一组句子,Y为每一个句子对应的下一个字(全部向量化)
  2. 搭建一个LSTM + Dense 的网络,输出根据具体情况要么为1,要么为N
  3. 每一个epoch里均进行预测(如果不是为了看过程,有必要吗?我们要最后一轮的预测不就行了?)
    • 进行一次fit(就是train),得到优化后的参数
    • 随机取一段文本,用作种子(用来生成第一个字)
    • 计算生成多少个字,就开始for循环
      • 向量化当前的种子(会越来越长)
      • predict,得到每个字的概率
      • softmax temperature,平滑概率,取出next_token
      • next_token转回文本,附加到seed后面

DeepDream

看了一遍,不感兴趣。核心思路跟视觉化filter的思路是一样的:gradient ascent

  1. 从对每个layer里的单个filter做梯度上升变成了对整个layer做梯度上升
  2. 不再从随机噪声开始,而是从一张真实图片开始,实现这些layer里对图片影响最大的patterns的distorting

Neural style transfer

Neural style transfer consists of applying the style of a reference image to a target image while conserving the content of the target image.

  • 两个对象:reference, target image
  • 两个概念:stylecontent

B的content应用A的style,我们可以理解为“笔刷”,或者用前些年的流行应用来解释:把一副画水彩化,或油画化。

把style分解为不同spatial scales上的:纹理,颜色,和visual pattern

想用深度学习来尝试解决这个问题,首先至少得定义损失函数是什么样的。

If we were able to mathematically define content and style, then an appropriate loss function to minimize would be the following:

loss = distance(style(reference_image) - style(generated_image)) +
        distance(content(original_image) - content(generated_image))

即对新图而言,纹理要无限靠近A,内容要无限靠近B

  • the content loss
    • 图像内容属于高级抽象,因此只需要top layers参与就行了,实际应用中只取了最顶层
  • the style loss
    • 应用Gram matrix
      • the inner product of the feature maps of a given layer
      • correlations between the layer's feature
      • 需要生成图和参考图的每一个对应的layer拥有相同的纹理(same textures at different spatial scales),因此需要所有的layer参与

从这里应该也能判断出要搭建网络的话,input至少由三部分(三张图片)构成了。

demo

  • input为参考图,目标图,和生成图(占位),concatenate成一个tensor
  • 用VGG19来做特征提取
  • 计算loss
    1. 用生成图和目标图top_layer以L2 norm距离做loss
    2. 用生成图和参考图every layer以L2 Norm做loss并累加
    3. 对生成图偏移1像素做regularization loss(具体看书)
    4. 上述三组loss累加,为一轮的loss
  • 用loss计算对input(即三联图)的梯度

Generating images

Sampling from a latent space of images to create entirely new images

熟悉的句式又来了。

核心思想:

  1. low-dimensional latent space of representations
    • 一般是个vector space
    • any point can be mapped to a realistic-looking image
  2. the module capable of realizing this mapping, can take point as input, then output an image, this called:
    • generator -> GAN
    • decoder -> VAE

VAE v.s. GAN

  • VAEs are great for learning latent spaces that are well structured
  • GANs generate images that can potentially be highly realistic, but the latent space they come from may not have as much structure and continuity.

VAE(variational autoencoders)

given a latent space of representations, or an embedding space, certain directions in the space may encode interesting axes of variation in the original data. -> inspired by concept space

比如包含人脸的数据集的latent space里,是否会存在smile vectors,定位这样的vector,就可以修改图片,让它projecting到这个latent space里去。

Variational autoencoders

Variational autoencoders are a kind of generative model that’s especially appropriate for the task of image editing via concept vectors.

They’re a modern take on autoencoders (a type of network that aims to encodean input to a low-dimensional latent space and then decode it back) that mixes ideas from deep learning with Bayesian inference.

  • VAE把图片视作隐藏空间的参数进行统计过程的结果。
  • 参数就是表示一种正态分布的mean和variance(实际取的log_variance)
  • 用这个分布可以进行采样(sample)
  • 映射回original image
  1. An encoder module turns the input samples input_img into two parameters in a latent space of representations, z_mean and z_log_variance.
  2. You randomly sample a point z from the latent normal distribution that’s assumed to generate the input image, via $z = z_mean + e^{z_log_variance} \times \epsilon$, where $\epsilon$ is a random tensor of small values.
  3. A decoder module maps this point in the latent space back to the original input image.

Because epsilon is random, the process ensures that every point that’s close to the latent location where you encoded input_img (z-mean) can be decoded to something similar to input_img, thus forcing the latent space to be continuously meaningful.

  1. 所以VAE生成的图片是可解释的,比如在latent space中距离相近的两点,decode出来的图片相似度也就很高。
  2. 多用于编辑图片,并且能生成动画过程(因为是连续的)

伪代码(不算,可以说是骨干代码):

z_mean, z_log_variance = encoder(input_img)
z = z_mean + exp(z_log_variance) * epsilon  # sampling
reconstructed_img = decoder(z)
model = Model(input_img, reconstructed_img)

VAE encoder network

img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2

x = layers.Conv2D(32, 3, padding='same', activation='relu')(input_img)
x = layers.Conv2D(64, 3, padding='same', activation='relu', strides=(2, 2))(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
shape_before_flattening = K.int_shape(x)
x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)
z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)
  1. 可见是一个标准的multi-head的网络
  2. 可见所谓的latent space,其实就是transforming后的结果
  3. encode的目的是回归出两个参数(本例是两个2维参数)
  4. 两个参数一个理解为mean, 一个理解为log_variance

decoder过程就是对mean和var随机采样(得到z),然后不断上采样(Conv2DTranspose)得到形状与源图一致的输出(得到z_decode)的过程。

  1. z_decode跟z做BCE loss
  2. 还要加一个regularization loss防止overfitting

此处请看书,演示了自定义的loss。因为keras高度封装,所以各种在封装之外的自定义的用法尤其值得关注。比如这里,自定义了loss之后,Model和fit里就不需要传Y,compile时也不需要传loss了。

loss是在最后一层layer里计算的,并且通过一个layer方法add_loss,把loss和input通知给了network(如果你想知道注入点的话)

使用模型的话,就是生成两组随机数,当成mean和log_variance,观察decode之后的结果。

GAN

Generative adversarial network可以创作以假乱真的图片。通过训练最好的造假和和最好的鉴别者来达到“创造”越来越逼近人类创作的作品。

  • Generator network: Takes as input a random vector (a random point in the latent space), and decodes it into a synthetic image
  • Discriminator network (or adversary): Takes as input an image (real or synthetic), and predicts whether the image came from the training set or was created by the generator network.

deep convolutional GAN (DCGAN)

  • a GAN where the generator and discriminator are deep convnets.
  • In particular, it uses a Conv2DTranspose layer for image upsampling in the generator.

训练生成器是冲着能让鉴别器尽可能鉴别为真的方向的:the generator is trained to fool the discriminator。

这句话其实暗含了一个前提,下面会说,就是此时discriminator是确定的。即在确定的鉴别能力下,尽可能去拟合generator的输出,让它能通过当前鉴别器的测试。

书中说训练DCGAN很复杂,而且很多trick, 超参靠的是经验而不是理论支撑,摘抄并笔记a bag of tricks如下:

  • We use tanh as the last activation in the generator, instead of sigmoid, which is more commonly found in other types of models.
  • We sample points from the latent space using a normal distribution (Gaussian distribution), not a uniform distribution.
  • Stochasticity is good to induce robustness. Because GAN training results in a dynamic equilibrium, GANs are likely to get stuck in all sorts of ways. Introducing randomness during training helps prevent this. We introduce randomness in two ways:
    • by using dropout in the discriminator
    • and by adding random noise to the labels for the discriminator.
  • Sparse gradients can hinder GAN training. In deep learning, sparsity is often a desirable property, but not in GANs. Two things can induce gradient sparsity: max pooling operations and ReLU activations.
    • Instead of max pooling, we recommend using strided convolutions for downsampling(用步长卷积代替pooling),
    • and we recommend using a LeakyReLU layer instead of a ReLU activation. It’s similar to ReLU, but it relaxes sparsity constraints by allowing small negative activation values.
  • In generated images, it’s common to see checkerboard artifacts(stirde和kernel size不匹配千万的) caused by unequal coverage of the pixel space in the generator.
    • To fix this, we use a kernel size that’s divisible by the stride size whenever we use a strided Conv2DTranpose or Conv2D in both the generator and the discriminator.

Train

  1. Draw random points in the latent space (random noise).
  2. Generate images with generator using this random noise.
  3. Mix the generated images with real ones.
  4. Train discriminator using these mixed images, with corresponding targets:
    • either “real” (for the real images) or “fake” (for the generated images).
    • 所以鉴别器是单独训练的(前面笔记铺垫过了)
    • 下面就是train整个DCGAN了:
  5. Draw new random points in the latent space.
  6. Train gan using these random vectors, with targets that all say “these are real images.” This updates the weights of the generator (only, because the discriminator is frozen inside gan) to move them toward getting the discriminator to predict “these are real images” for generated images: this trains the generator to fool the discriminator.
    • 只train网络里的generator
    • discriminator不训练,因为是要用“已经训练到目前程度的”discriminator来做下面的任务
    • 任务就是只送入伪造图,并声明所有图都是真的,去让generator生成能逼近这个声明的图
    • generator就是这么训练出来的。
    • 所以实际代码是一次epoch是由train一个discriminator和train一个GAN组成.

因为鉴别器和生成器是一起训练的,因此前几轮生成的肯定是噪音,但前几轮鉴别器也是瞎鉴别的。

《Deep Learning with Python》笔记[6]

Advanced deep-learning best practices

这一章是介绍了更多的网络(从keras的封装特性出发)结构和模块,以及batch normalization, model ensembling等知识。

beyond Sequential model

前面介绍的都是Sequential模型,就是一个接一个地layer前后堆叠,现实中有很多场景并不是一进一出的:

  1. multi-input model

假设为二手衣物估价:

  • 格式化的元数据(品牌,性别,年龄,款式): one-hot, dense
  • 商品的文字描述:RNN or 1D convnet
  • 图片展示:2D convnet
  • 每个input用适合自己的网络做输出,然后合并起来作为一个input,回归一个价格
  1. multi-output model (multi-head)

一般的检测器通常就是多头模型,因为既要回归对象类别,还要回归出对象的位置

  1. graph-like model

这个名字很好地形容了做深度学习时看别人的网络是什么样的方式:看图。现代的SOTA的网络往往既深且复杂,而网络结构画出来也不再是一条线或几个简单分支,这本书干脆把它们叫图形网络:Inception, Residual

为了能架构这些复杂的网络,keras介绍了新的语法,先看看怎么重写Sequential:

seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))

# 重写
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)
model = Model(input_tensor, output_tensor)

model.summayr()

我们自己实现过静态图,最终去执行的时候能从尾追溯到头,并从头来开始计算,这里也是一样的:

  1. input, output是Tensor类,所以有完整的层次信息
  2. output往上追溯,最终溯到缺少一个input
  3. 这个input恰好也是Model的构造函数之一,闭环了。

书里说的更简单,output是input不断transforming的结果。如果传一个没有这个关系的input进去,就会报错。

demo

用一个QA的例子来演示多输入(一个问句,一段资料),输出为答案在资料时的索引(简化为单个词,所以只有一个输出)

text_input = Input(shape=(None,), dtype='int32', name='text')
embedded_text = layers.Embedding(
    64, text_vocabulary_size)(text_input)
encoded_text = layers.LSTM(32)(embedded_text)  # lstm 处理资讯
question_input = Input(shape=(None,), dtype='int32', name='question')


embedded_question = layers.Embedding(
    32, question_vocabulary_size)(question_input)
encoded_question = layers.LSTM(16)(embedded_question) # lstm 处理问句

concatenated = layers.concatenate([encoded_text, encoded_question], axis = -1)  # 竖向拼接(即不增加内容只增加数量)
answer = layers.Dense(answer_vocabulary_size,
                      activation='softmax')(concatenated)
model = Model([text_input, question_input], answer)
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['acc'])

这里是把答案直接给回归出来了(one-hot),如果是给出答案的首尾位置,那肯定只能用索引了。

demo

多头输出的:

# 线性回归
age_prediction = layers.Dense(1, name='age')(x)
# 逻辑回归
income_prediction = layers.Dense(num_income_groups, activation='softmax', name='income')(x)
# 二元逻辑回归
gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)
model = Model(posts_input,
              [age_prediction, income_prediction, gender_prediction])

梯度回归要求loss是一个标量,keras提供了方法将三个loss加起来,同时为了量纲统一,还给了权重参数:

model.compile(optimizer='rmsprop',
loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'], loss_weights=[0.25, 1., 10.])

Directed acyclic graphs of layers

有向无环图。可以理解为最终不会回到出发点。

现在会介绍的是几个Modules,意思是可以把它当成一个layer,来构造你的网络/模型。

Inception Modules

  • inspired by network-in-network
  • 对同一个输入做不同(层数/深度)的卷积(保证最终相同的下采样维度),最后合并为一个输出
  • 因为卷积的深度不尽相同,学到的空间特征也有粗有细

Residual Connections

  • 有些地方叫shortcut
  • 用的是相加,不是concatenate, 如果形状变了,对earlier activation做linear transformation
  • 解决vanishing gradients and representational bottlenecks
  • adding residual connections to any model that has more than 10 layers is likely to be beneficial.

representational bottlenecks

序列模型时,每一层的表示都来自于前一层,如果前一层很小,比如维度过低,那么携带的信息量也被压缩得很有限了,整个模型都会被这个“瓶颈”限制。比如音频信号处理,降维就是降频,比如到0-15kHz,但是下游任务也没法recover dropped frequencies了。所有的损失都是永久的。

Residual connections, by reinjecting earlier information downstream, partially solve this issue for deep-learning models.(又一次强调reinject

Lyaer weight sharihng

在网络的不同位置用同一个layer,并且参数也相同。等于共享了相同的知识,相同的表示,以及是同时(simultaneously)训练的。

一个语义相似度的例子,输入是A和B还是B和A,是一样的(即可以互换)。架构网络的时候,用LSTM来处理句子,需要做两个LSTM吗?当然可以,但是也可以只做一个LSTM,分别喂入两个句子,合并两个输出来做分类。就是考虑到这种互换性,既然能互换,也就是这个layer也能应用另一个句子,因此就不必要再新建一个LSTM.

Models as layers

讲了两点:

  1. model也可以当layer使用
  2. 多处使用同一个model也是共享参数,如上一节。

举了个双摄像头用以感知深度的例子,每个摄像头都用一个Xception网络提取特征,但是可以共用这个网络,因为拍的是同样的内容,只需要处理两个摄像头拍到的内容的差别就能学习到深度信息。因为希望是用同样的特征提取机制的。

都是蜻蜓点水。

More Advanced

Batch Normalization

  1. 第一句话就是说为了让样本数据看起来更相似,说明这是初衷。
  2. 然后是能更好地泛化到未知数据(同样也是因为bn后就更相似了)
  3. 深度网络中每一层之后也需要做
    • 还有一个书里没讲到的原因,就是把值移到激活函数的梯度大的区域(比如0附近),否则过大过小的值在激活函数的曲线里都是几乎没有梯度的位置
  4. 内部用的指数移动平均(exponential moving average)
  5. 一些层数非常深的网络必须用BN,像resnet 50, 101, 152, inception v3, xception等

Depthwise Separable Convolution

之前的卷积,不管有多少个layer,都是放到矩阵里一次计算的,DSC把每一个layer拆开,单独做卷积(不共享参数),因为没有一个巨大的矩阵,变成了几个小矩阵乘法,参数量也大大变少了。

  1. 对于小样本很有效
  2. 对于大规模数据集,它可以成为里面的固定结构的模块(它也是Xception的基础架构之一)

In the future, it’s likely that depthwise separable convolutions will completely replace regular convolutions, whether for 1D, 2D, or 3D applications, due to their higher representational efficiency.

?!!

Model ensembling

  1. Ensembling consists of pooling together the predictions of a set of different models, to produce better predictions.
  2. 期望每一个good model拥有part of the truth(部分的真相)。盲人摸象的例子,没有哪个盲人拥有直接感知一头象的能力,机器学习可能就是这样一个盲人。
  3. The key to making ensembling work is the diversity of the set of classifiers -> 关键是要“多样性”。 Diversity is what makes ensembling work.
  4. 千万不要去ensembling同样的网络仅仅改变初始化而去train多次的结果。
  5. 比较好的实践有ensemble tree-based models(random forests, gradient-boosted trees) 和深度神经网络
  6. 以及wide and deep category of models, blending deep learning with shallow learning.

同样是蜻蜓点水。

《Deep Learning with Python》笔记[5]

Deep learning for text and sequences

空间上的序列,时间上的序列组成的数据,比如文本,视频,天气数据等,一般用recurrent neural network(RNN)和1D convnets

其实很多名词,包括convnets,我并没有在别的地方看到过,好像就是作者自己发明的,但这些不重要,知道它描述的是什么就可以了,不一定要公认术语。

通用场景:

  • [分类: 文本分类] Document classification and timeseries classification, such as identifying the topic of an article or the author of a book
  • [分类: 文本比较] Timeseries comparisons, such as estimating how closely related two documents or two stock tickers are
  • [分类: 生成] Sequence-to-sequence learning, such as decoding an English sentence into French
  • [分类: 情感分析]Sentiment analysis, such as classifying the sentiment of tweets or movie reviews as positive or negative
  • [回归: 预测]Timeseries forecasting, such as predicting the future weather at a certain location, given recent weather data

我画蛇添足地加了是分类问题还是回归问题.

none of these deeplearning models truly understand text in a human sense

Deep learning for natural-language processing is pattern recognition applied to words, sentences, and paragraphs, in much the same way that computer vision is pattern recognition applied to pixels.

tokenizer

图像用像素上的颜色来数字化,那文字也把什么数字化呢?

  • 拆分为词,把每个词转化成向量
  • 拆分为字(或字符),把每个字符转化为向量
  • 把字(词)与前n个字(词)组合成单元,转化为向量,(类似滑窗),N-Grams

all of above are tokens, and breaking text into such tokens is called tokenization. These vectors, packed into sequence tensors, are fed into deep neural networks.

N-grams这种生成的token是无序的,就像一个袋子装了一堆词:bag-of-words: a set of tokens rather than a list of sequence.

所以句子结构信息丢失了,更适合用于浅层网络。作为一种rigid, brittle(僵硬的,脆弱的)特征工程方式,深度学习采用多层网络来提取特征。

vectorizer

token -> vector:

  • one-hot encoding
  • token/word embedding (word2vec)

one-hot

  1. 以token总数量(一般就是字典容量)为维度
  2. 一般无序,所以生成的时候只需要按出现顺序编索引就好了
  3. 有时候也往往伴随丢弃不常用词,以减小维度
  4. 也可以在字符维度编码(维度更低)
  5. 一个小技巧,如果索引数字过大,可以把单词hash到固定维度(未跟进)

特点/问题:

  • sparse
  • high-dimensional, 比如几千几万
  • no spatial relationship
  • hardcoded

word embeddings

  • Dense
  • Lower-dimensional,比如128,256...
  • Spatial relationships (语义接近的向量空间上也接近)
  • Learned from data

to obtain word embeddings:

  1. 当成训练参数之一(以Embedding层的身份),跟着训练任务一起训练
  2. pretrained word embeddings
    • Word2Vec(2013, google)
      • CBOW
      • Skip-Gram
    • GloVe(2014, Stanford))
    • 前提是语言环境差不多,不同学科/专业/行业里的词的关系是完全不同的
      • GloVe从wikipedia和很多通用语料库里训练,可以尝试在许多非专业场景里使用。

keras加载训练词向量的方式:

model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False

pytorch:

# TEXT, LABEL为torchtext的Field对象
from torchtext.vocab import Vectors
vectors=Vectors(name='./sgns.sogou.word') #使用预训练的词向量,维度为300Dimension
TEXT.build_vocab(train, vectors=vectors) #构建词典
LABEL.build_vocab(train)

vocab_size = len(TEXT.vocab)
vocab_vectors = TEXT.vocab.vectors.numpy() #准备好预训练词向量

self.embedding = nn.Embedding(num_embeddings=vocab_size embedding_dim=embedding_size)

# 上面是为了回顾,真正用来做对比的是下面这两句
self.embedding.weight.data.copy_(torch.from_numpy(vocab_vectors))
self.embedding.weight.requires_grad = False

预训练词向量也可以继续训练,以得到task-specific embedding

Recurrent neural networks(RNN)

sequence, time series类的数据,天然会受到前后数据的影响,RNN通过将当前token计算的时候引入上一个token的计算结果(反向的话就能获得下一个token的结果)以获取上下文的信息。

前面碰到的网络,数据消费完就往前走(按我这种说法,后面还有很多“等着二次消费的”模块,比如inception, resdual等等),叫做feedforward network。显然,RNN中,一个token产生输出后并不是直接丢给下一层,而是还复制了一份丢给了同层的下一个token. 这样,当前token的output成了下一个token的state

  • 因为一个output其实含有“前面“所有的信息,一般只需要最后一个output
  • 如果是堆叠多层网络,则需要返回所有output

序列过长梯度就消失了,所谓的遗忘 (推导见另一篇笔记,) -> LSTM, GRU

Long Short-Term Memory(LSTM)

  1. 想象有一根传送带穿过sequence
  2. 同一组input和state会进行三次相同的线性变换,有没有联想到transformer用同一个输出去生成q, k, v
output_t = activation(dot(state_t, Uo) + dot(input_t, Wo) + dot(C_t, Vo) + bo)
i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi) 
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf) 
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)

c_t+1 = i_t * k_t + c_t * f_t  # 仍然有q,k,v的意思(i,k互乘,加上f, 生成新c)

不要去考虑哪个是遗忘门记忆门,还是输出门,最终是由weights决定的,而不是设计。

Just keep in mind what the LSTM cell is meant to do:

allow past information to be reinjected at a later time, thus fighting the vanishing-gradient problem.

关键词:reinject

dropout

不管是keras还是pytorch,都帮你隐藏了dropout的坑。 你能看到应用这些框架的时候,是需要你把dropout传进去的,而不是手动接一个dropoutlayer,原因是需要在序列每一个节点上应用同样的dropout mask才能起作用,不然就会起到反作用。

keras封装得要复杂一点:

model.add(layers.GRU(32,
                    dropout=0.2,
                    recurrent_dropout=0.2,
                    input_shape=(None, float_data.shape[-1])))

stacking recurrent layers

前面说过,设计好的模型的一个判断依据是至少让模型能跑到overfitting。如果到了overfitting,表现还不是很好,那么可以考虑增加模型容量(叠更多层,以及拓宽layer的输出维度)

堆叠多层就需要用到每个节点上的输出,而不只关心最后一个输出了。

Bidriectional

keras奇葩的bidirectional语法:

model.add(layers.Bidirectional(layers.LSTM(32)))

其实这是设计模式在类的封装上的典型应用,善用继承和多态,无侵入地扩展类的方法和属性,而不是不断魔改原代码,加参数,改API。但在脚本语言风格里的环境里,这么玩就有点格格不入了。

Sequence processing with convnets

  1. 卷积用到序列上去也是可以的
  2. 一个向量只表示一个token,如果把token的向量打断就违背了token是最小单元的初衷,所以序列上的卷积,不可能像图片上两个方向去滑窗了。(Conv1D的由来)
  3. 一个卷积核等于提取了n个关联的上下文(有点类似n-grams),堆叠得够深感受野更大,可能得到更大的上下文。
  4. 但仍然理解为filter在全句里提取局部特征

归桕结底,图片的最小单元是一个像素(一个数字),而序列(我们这里说文本)的最小单元是token,而token又被我们定义为vector(一组数字)了,那么卷积核就限制在至少要达到最小单元(vector)的维度了。

Combining CNNs and RNNs to process long sequences

卷积能通过加深网络获取更大的感受野,但仍然是“位置无关”的,因为每个filter本就是在整个序列里搜索相同的特征。

但是它确实提取出了特征,是否可把位置关系等上下文的作业交给下游任务RNN做呢?

不但实现,而且堆叠两种网络,还可以把数据集做得更大(CNN是矩阵运算,还能用GPU加速)。

《Deep Learning with Python》笔记[4]

Deep learning for computer vision

Convolution Network

The convolution operation extracts patches from its input feature map and applies the same transformation to all of these patches, producing an output feature map.

  • convolution layers learn local patterns(局部特征)
    • The patterns they learn are translation invariant.(局部特征可在图片别的地方重复)
    • 有的教材里会说每个滑窗一个特征,然后引入参数共享才讲到一个特征其实可以用在所有滑窗
  • They can learn spatial hierarchies of patterns(低级特征堆叠成高级特征)
  • depth axis no longer stand for specific colors as in RGB input; rather, they stand for filters(表示图片时,3个通道有原始含义,卷积开始后通道只表示filter了)
  • valid and same convolution(加不加padding让filter在最后一个像素时也能计算)
  • stride,滑窗步长
  • max-pooling or average-pooling
    • usually 2x2 windows by stride 2 -> 下采样(downsample)
    • 更大的感受野
    • 更小的输出
    • 不是唯一的下采样方式(比如在卷积中使用stride也可以)
    • 一般用max而不是average(寻找最强的表现)
  • 小数据集
    • data augmenetation(旋转平衡缩放shear翻转等)
      • 不能产生当前数据集不存在的信息
      • 所以仍需要dropout
    • pretrained network(适用通用物体)
      • feature extraction
      • fine-tuneing

Using a pretrained convnet

A pretrained network is a saved network that was previously trained on a large dataset typically on a large-scale image-classification task.

Feature extraction

Feature extraction consists of using the representations learned by a previous network to extract interesting features from new samples. These features are then run through a new classifier, which is trained from scratch.

  1. 即只使用别的大型模型提取的representations(特征),来构建自己的分类器。
  2. 原本模型的分类器不但是为特定任务写的,而且基本上丧失了位置和空间信息,只保留了对该任务上的presence probability.
  3. 最初的层一般只能提取到线,边缘,颜色等低级特征,再往后会聚合出一些纹理,更高的层就可能会叠加出一些眼,耳等抽象的特征,所以你的识别对象与pretrained数据源差别很大的时候,就需要考虑把最尾巴的几层layer也舍弃掉。(e.g. VGG16最后一层提取了512个feature map)
  4. 两种用法:
    • 跑一次预训练模型你选中的部分,把参数存起来($\leftarrow$错),把输出当作dataset作为自己构建的分类器的input。
      • 快,省资源,但是需要把数据集固定住,等于没法做data augmentation
      • 跑预训练模型时不需要计算梯度(freeze)
      • 其实应用预训练模型就等于别人的预处理数据集,而真实的模型只有一个小分类器
    • 合并到自定义的网络中当成普通网络训练
      • 慢,但是能做数据增广了
      • 需手动设置来自预训练模型的梯度不需要计算梯度

注:这里为什么单独跑预训练模型不能数据增广呢?

教材用的是keras, 它处理数据的方式是做一个generaotr,只要你给定数据增广的规则(参数),哪怕只有一张图,它也是可以无穷无尽地给你生成下一张的。所以每一次训练都能有新的数据喂到网络里。这是出于内存考虑,不需要真的把数据全部加载到内存里。

而如果你是一个固定的数据集,比如几万条,那么你把所有的数据跑一遍把这个结果当成数据集(全放在内存里),那也不是不可以在这一步用数据增广。

Fine-tuning

Fine-tuning consists of unfreezing a few of the top layers of a frozen model base used for feature extraction, and jointly training both the newly added part of the model (in this case, the fully connected classifier) and these top layers. This is called fine-tuning because it slightly adjusts the more abstract representations of the model being reused, in order to make them more relevant for the problem at hand.

前面的feature extraction方式,会把预训练的模型你选中的layers给freeze掉,即不计算梯度。这里之所以叫fine-tuning,意思就是会把最后几层(top-layers)给unfreezing掉,这样的好处是保留低级特征,重新训练高级特征,还保留了原来大型模型的结构,不需要自行构建。

但是: it’s only possible to fine-tune the top layers of the convolutional base once the classifier on top has already been trained. 预训练模型没有frezze住的话loss将会很大,所以变成了先train一个大体差不多的classifier,再联合起来train一遍高级特征和classifier:

  1. Add your custom network on top of an already-trained base network.
  2. Freeze the base network.
  3. Train the part you added. (第一次train)
  4. Unfreeze some layers in the base network.
  5. Jointly train both these layers and the part you added.(第二次train)

但千万别把所有层都unfrezze来训练了

  1. 低级特征都为边缘和颜色,无需重新训练
  2. 小数据量训练大型模型,model capacity相当大,非常容易过拟合

Visualizing what convents learn

并不是所有的深度学习都是黑盒子,至少对图像的卷积网络不是 -> representations of visual concepts, 下面介绍三种视觉化和可解释性的representations的方法。

Visualizing intermediate activations

就是把每个中间层(基本上是"卷积+池化+激活“)可视化出来,This gives a view into how an input is decomposed into the different filters learned by the network.

from keras import models
layer_outputs = [layer.output for layer in model.layers[:8]] activation_model = models.Model(inputs=model.input, outputs=layer_outputs)

activations = activation_model.predict(img_tensor)

import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')

# 注意使用的是matshow而不是show

以上代码是利用了keras的Model特性,将所有layers的输出摊平(就是做了一个多头的模型),然后再顺便取了第4和第7个feature map画出来,可以看到,图一感兴趣的是对角线,图二提取的是蓝色的亮点

结构化这些输出,可以确信初始layer确实提取的是简单特征,越往后越高级(抽象)。

A deep neural network effectively acts as an information distillation(信息蒸馏) pipeline, with raw data going in (in this case, RGB pictures) and being repeatedly transformed so that irrelevant information is filtered out (for example, the specific visual appearance of the image), and useful information is magnified and refined (for example, the class of the image).

关键词:有用的信息被不断放大和强化

书里举了个有趣的例子,要你画一辆自行车。你画出来的并不是一辆充满细节的单车,而往往是你抽象出来的单车,你会用基本的线条勾勒出你对单车特征的理解,比如龙头,轮子等关键部件,以及相对位置。画家为什么能画得又真实又好看?那就是他们真的仔细观察了单车,他们绘画的时候用的并不是特征,而是一切细节,然而对于没有受过训练的普通人来说,往往只能用简单几笔勾勒出脑海中的单车的样子(其实并不是样子,而是特征的组合)

Visualizing convnet filters

通过强化filter对输出的反应并绘制出来,这是从数学方法上直接观察filter,看什么最能“刺激”一个filter,用”梯度上升“最能体现这种思路:

把output当成loss,用梯度上升(每次修改input_image)训练出来的output就是这个filter的极端情况,可以认为这个filter其实是在提取什么(responsive to):

from keras.applications import VGG16
from keras import backend as K
model = VGG16(weights='imagenet', include_top=False)
layer_name = 'block3_conv1'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])  # output就是loss

grads = K.gradients(loss, model.input)[0] # 对input求微分
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

iterate = K.function([model.input], [loss, grads])
import numpy as np
# 理解静态图的用法
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])

input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128.
step = 1.
for i in range(40):
    loss_value, grads_value = iterate([input_img_data])
    input_img_data += grads_value * step  # 梯度上升

按上述代码的思路结构化输出并绘图:

从线条到纹理到物件(眼睛,毛皮,叶子)

each layer in a convnet learns a collection of filters such that their inputs can be expressed as a combination of the filters.

This is similar to how the Fourier transform decomposes signals onto a bank of cosine functions.

用傅里叶变换来类比卷积网络每一层就是把input表示成一系列特征的组合。

Visualizing heatmaps of class activation

which parts of a given image led a convnet to its final classification decision. 即图像有哪一部分对最终的决策起了作用。

  • class activation map (CAM) visualization,
  • Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization.”

you’re weighting a spatial map of “how intensely the input image activates different channels” by “how important each channel is with regard to the class,” resulting in a spatial map of “how intensely the input image activates the class.

解读上面这句话:

不同channels(特征)对图像的激活的强度
+
每个特征对(鉴定为)该类别的重要程度
=
该“类别”对图像的激活的强度

一张两只亚洲象的例图,使用VGG16来做分类,得到92.5%的置信度的亚洲象的判断,为了visualize哪个部分才是“最像亚洲象”的,使用Grad-CAM处理:

from keras.applications.vgg16 import VGG16
model = VGG16(weights='imagenet')
african_e66lephant_output = model.output[:, 386]  # 亚洲象在IMGNET的类别是386
last_conv_layer = model.get_layer('block5_conv3') # top conv layer

grads = K.gradients(african_elephant_output, last_conv_layer.output)[0] 
pooled_grads = K.mean(grads, axis=(0, 1, 2))
iterate = K.function([model.input],
                     [pooled_grads, last_conv_layer.output[0]])
pooled_grads_value, conv_layer_output_value = iterate([x])
for i in range(512):
    conv_layer_output_value[:, :, i] *= pooled_grads_value[i]
heatmap = np.mean(conv_layer_output_value, axis=-1)

叠加到原图上去(用cv2融合两张图片,即相同维度的数组以不同权重逐像素相加):