武辰作品索引

知乎作品库

文章 · 4

知乎原文

一些关于梯度累加的常见错误认识

代码

关于梯度累加的pytorch实现中,在网上查到的常见步骤如下:

for i, (context, label) in enumerate(train_loader):
    pred = model(context) #前向过程
    loss = loss_func(pred, label) #计算loss

    loss = loss / accumulation_steps  #为什么会有这个步骤?
    loss.backward() #求解梯度,注意此步骤只求了梯度,并没有将梯度更新

    if (i + 1) % accumulation_steps == 0: #只有当累积到accumulation_steps整数倍的时候
        optimizer.step()  #梯度才会更新
        optimizer.zero_grad()  #梯度清零

其中代码中的疑问步骤是:loss = loss / accumulation_steps,这一步是否需要?网上鲜有内容说清楚了这个细节。甚至还有不少错误的文章把该步骤写成loss += loss / accumulation_steps,这是错误的写法。

首先解释为什么要引入梯度累加。比如batchsize=100可以达到最优的优化结果,但是限于显卡的性能,batchsize=100会爆显存。我希望能够以batchsize=100的跑模型而不爆显存,一个解决方案是设置batchsize=50,即一个step有50个数据。但并不是每个step都更新梯度,而是每个偶数step更新梯度,这样可以达到几乎等价于batchsize=100的效果。

以loss函数为mse函数为例,梯度更新的公式是 $w=w-l\cdot\frac{\partial loss}{\partial w}\\=w-l\cdot\frac{\partial \sum_{i=1}^{100}{(ypred-ytrue)^2}}{\partial w}\\=w-(l\cdot\frac{\partial \sum_{i=1}^{50}{(ypred-ytrue)^2}}{\partial w}+l\cdot\frac{\partial \sum_{i=51}^{100}{(ypred-ytrue)^2}}{\partial w})\\=w-l\cdot(\frac{\partial loss1}{\partial w}+\frac{\partial loss2}{\partial w})$

直接计算 ∂loss∂w\frac{\partial loss}{\partial w} 会爆显存,于是改为先计算前50个数据的 ∂loss1∂w\frac{\partial loss1}{\partial w} ,再计算后50个数据的 ∂loss2∂w\frac{\partial loss2}{\partial w} ,两个梯度加起来,等价于一起计算100个数据的 ∂loss∂w\frac{\partial loss}{\partial w} 。

由于loss.backward()是自动累加梯度的,只要没有手动将梯度清零,它会基于之前的梯度累加。因此不需要做额外的操作。loss = loss / accumulation_steps这一步是额外的操作。

但如果loss的定义不是 ∑i=1100(ypred−ytrue)2\sum_{i=1}^{100}{(ypred-ytrue)^2} ,而是 1batchsize∑i=1100(ypred−ytrue)2\frac{1}{batchsize}\sum_{i=1}^{100}{(ypred-ytrue)^2}

那么 $w=w-l\cdot\frac{\partial loss}{\partial w}\\=w-l\cdot\frac{1}{100}\cdot\frac{\partial \sum_{i=1}^{100}{(ypred-ytrue)^2}}{\partial w}\\=w-(l\cdot\frac{1}{100}\frac{\partial \sum_{i=1}^{50}{(ypred-ytrue)^2}}{\partial w}+l\cdot\frac{1}{100}\frac{\partial \sum_{i=51}^{100}{(ypred-ytrue)^2}}{\partial w})\\=w-l\cdot(\frac{1}{2}\frac{\partial loss1}{\partial w}+\frac{1}{2}\frac{\partial loss2}{\partial w})$
如果每个小批的大小一样,而且凑满了一组,那就把每个小批的平均 loss 除以累加步数。

也就是说,是否需要loss = loss / accumulation_steps这个步骤取决于对loss的定义,如果loss的定义是整个batch内求和,则不需要加上该步骤。


另一个错误的写法

一个思考,上面说到loss += loss / accumulation_steps 是错误的写法,但如果将求梯度的步骤挪出去,如下面的代码所示,是否正确了?问题出在哪里?

# 初始化total_loss为0
total_loss = torch.zeros(1).to(device)
optimizer.zero_grad()  # 在起始时清零梯度

for i, (context, label) in enumerate(train_loader):
    context = context.to(device)
    label = label.to(device)
    pred = model(context)  # 前向过程
    loss = loss_func(pred, label)  # 计算损失

    # 累积损失,除以accumulation_steps以便模仿较大批量大小的梯度
    total_loss += loss / accumulation_steps

    # 一旦累积了足够的步数,反向传播梯度
    if (i + 1) % accumulation_steps == 0:
        total_loss.backward()  # 对累积的总损失求梯度
        optimizer.step()  # 进行梯度更新
        optimizer.zero_grad()  # 清空累积梯度
        # 重新初始化total_loss为0,为下一个累积步骤做准备
        total_loss = torch.zeros(1).to(device)

# 注意:在循环结束后检查是否需要处理剩余梯度
if total_loss.item() != 0:
    total_loss.backward()
    optimizer.step()
    optimizer.zero_grad()

202406更新:

几个小批的 loss 可以先加起来,最后一起反传。只是这样一来,每个小批前向时留下的中间结果都得留到最后,更占显存。为什么要留?因为反向传播算梯度时,还要用到前向算出来的激活值。

下面的代码可以说明在反向传播的过程中,激活值是必不可少的部分。

import torch

# 设定随机种子以保证结果的可复现性
torch.manual_seed(0)

# 假设这是我们的激活值矩阵a,形状为[10, 1]
a = torch.randn(10, 1, requires_grad=True)

# 这是权重w,形状为[1, 1]
w = torch.randn(1, 1, requires_grad=True)

# 计算模型的输出y
y = a * w

# 假设我们有一个目标值t,这里我们使用一个全1的向量作为示例
t = torch.ones(10, 1)
y.retain_grad()

# 计算损失值,这里使用均方误差作为损失函数
loss = (y - t).pow(2).mean()

# 反向传播,计算梯度
loss.backward()

print('a',a)
print('loss',loss)
print('y.grad',y.grad)
# 打印权重w的梯度
print("梯度相对于权重w:", w.grad)

在这个例子中,y=w*a,loss=(y-1)^2

其中a的shape是[10,1],表明一个batchsize内有10个不同的数据。w是标量,表示这个神经网络只有一个参数。那么y的shape也是[10,1]。

根据链式法则,∂loss/∂w=(∂loss/∂y)*(∂y/∂w)=(∂loss/∂y)*a

其中∂loss/∂y就是y.grad

你会发现,y.grad和a的内积就是w.grad

也就是说,在计算w.grad的时候,是需要用到a是整个批次的所有的值的。

这里有一个小细节,y.grad的shape是torch.Size([10, 1]),a的shape也是torch.Size([10, 1]),两者如何相乘?这其实是逐点相乘然后相加得出最终的结果,最终的结果是w的梯度,它是一个标量。相当于做一个内积。

原文配图
原文配图

用代码验证梯度累积的等价性

梯度累积指的是在累积到一定steps后再做梯度更新的操作,此操作在一般情况下和直接用大batch是等价的。

我想验证这种等价性,即,情况1:不使用梯度累积,batchsize=b1

情况2:使用梯度累积,batchsize = b1/accumulation_steps

两种情况的训练成果是否等价?

直接上代码:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader

# Step 1: 创建数据集
# 假设我们有以下的数据(输入x和输出y),数据量为6
x_data = torch.tensor([[1.0], [2.0], [3.0], [4.0], [5.0], [6.0]])
y_data = torch.tensor([[2.0], [4.0], [6.0], [8.0], [10.0], [12.0]])

# 创建一个 TensorDataset 和 DataLoader
dataset = TensorDataset(x_data, y_data)
batch_size = 2
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)

# Step 2: 定义模型
class LinearModel(nn.Module):
    def __init__(self):
        super(LinearModel, self).__init__()
        self.linear = nn.Linear(1, 1)
        # self.bn = nn.BatchNorm1d(1)

    def forward(self, x):
        x = self.linear(x)
        # x = self.bn(x)
        return x

model = LinearModel()
model.train()

# 指定初始参数
with torch.no_grad():
    model.linear.weight.fill_(0.5)  # 设定权重初始值为0.5
    model.linear.bias.fill_(-1.0)   # 设定偏差初始值为-1.0

# Step 3: 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# Step 4: 训练模型
epochs = 2
accumulation_steps = 2  # 设置梯度累积步数(在这个例子中每个epoch训练3次)

for epoch in range(epochs):
    optimizer.zero_grad()  # 清空梯度
    accumulated_loss = 0.0
    # accumulated_loss.requires_grad=True

    # 使用 DataLoader 迭代数据
    for i, (inputs, targets) in enumerate(dataloader):
        y_pred = model(inputs)
        # print('===epoch:{}  batch:{}'.format(epoch,i),'\n',
        #       'y_pred:',[[round(num, 2) for num in sublist] for sublist in y_pred.tolist()],'\n',
        #       'targets:',[[round(num, 2) for num in sublist] for sublist in targets.tolist()],'\n',
        #       'inputs:',[[round(num, 2) for num in sublist] for sublist in inputs.tolist()]
        #       )
        loss = criterion(y_pred, targets)
        loss = loss / accumulation_steps
        # print('loss:',loss.tolist())
        loss.backward()  # 反向传播,但不立即更新权重
        # print(f'linear Weight Grad: {model.linear.weight.grad}, Bias Grad: {model.linear.bias.grad}')
        # print(f'bn Weight Grad: {model.bn.weight.grad}, Bias Grad: {model.bn.bias.grad}')
        accumulated_loss += loss.item()

        # 每 `accumulation_steps` 步后更新权重
        if (i + 1) % accumulation_steps == 0 or (i + 1) == len(dataloader):
            # accumulated_loss.backward()
            optimizer.step()
            optimizer.zero_grad()

    # print(f'Epoch: {epoch+1}/{epochs}, accumulated_loss: {accumulated_loss / len(dataloader)}')

print("Training finished")
print('accumulation_steps:',accumulation_steps,' batch_size:',batch_size)
print('model.linear.weight',model.linear.weight)
print('model.linear.bias',model.linear.bias)
# print('model.bn.weight',model.bn.weight)
# print('model.bn.bias',model.bn.bias)

以下是我做了几次实验的结果:

Training finished
accumulation_steps: 2  batch_size: 3
model.linear.weight Parameter containing:
tensor([[1.3820]], requires_grad=True)
model.linear.bias Parameter containing:
tensor([-0.7893], requires_grad=True)

Training finished
accumulation_steps: 3  batch_size: 2
model.linear.weight Parameter containing:
tensor([[1.3820]], requires_grad=True)
model.linear.bias Parameter containing:
tensor([-0.7893], requires_grad=True)

Training finished
accumulation_steps: 1  batch_size: 6
model.linear.weight Parameter containing:
tensor([[1.3820]], requires_grad=True)
model.linear.bias Parameter containing:
tensor([-0.7893], requires_grad=True)

Training finished
accumulation_steps: 6  batch_size: 1
model.linear.weight Parameter containing:
tensor([[1.3820]], requires_grad=True)
model.linear.bias Parameter containing:
tensor([-0.7893], requires_grad=True)

Training finished
accumulation_steps: 2  batch_size: 2
model.linear.weight Parameter containing:
tensor([[1.5977]], requires_grad=True)
model.linear.bias Parameter containing:
tensor([-0.7290], requires_grad=True)

可以看到,前四种设定每次都用同样的 6 个样本更新,最后得到了同样的参数。最后一种设定每轮先用 4 个样本更新,再用剩下的 2 个更新。第二次只剩一个小批,代码还把它的平均 loss 除以 2,梯度就只有本来的一半。上面这组数字是按原代码得到的。

有几个细节需要注意。

(1)可以打乱数据。比较这两种写法时,只要每次拿来更新的样本是同一组,各组的更新顺序也一样。

(2)这里的 accumulated_loss 加的是 loss.item() 取出来的普通数字,只能用来记数,不能反传。如果加的是 loss 张量本身,哪怕它只有一个值,也可以反传。

(3)训练时,BN 用的是眼前这批数据的均值和方差。把一大批拆成几小批,这两个数就可能变,所以算出来的梯度也可能不一样。BN 本身不把输出限制在 0 到 1。


知乎原文

BN和LN分别在CV和NLP的实现

网上的资料鱼龙混杂,我做一个总结。本文以tensorflow2实现,来说明:

(1)BatchNormalization在CV(shape=[batch,height,width,channel])的实现

(2)BatchNormalization在NLP(shape=[batch,length,embedding])的实现

(3)LayerNormalization在CV(shape=[batch,height,width,channel])的实现

(4)LayerNormalization在NLP(shape=[batch,length,embedding])的实现

原文配图

必要的说明以注释的形式写在代码里.

(1)BatchNormalization在CV(shape=[batch,height,width,channel])的实现

import tensorflow as tf
import numpy as np

#batchnormalization在图像的实现
a = np.arange(120).reshape(2,3,4,5).astype(np.float32)
#这里用 momentum=0 是为了方便看打印出来的移动均值和方差。算当前批次的输出,靠的是 training=True。
#axis=-1,代表一般在CV中,首先挑选某个channel,然后对这个channel内所有样本的所有值求平均
#也就是说,对于某个特定的channel,需要求batch*height*weight个数值的平均值
norm = tf.keras.layers.BatchNormalization(momentum=0,axis=-1)
print(a)
#必须training=True,才会计算当前batch的均值和方差
print(norm(a,training=True))
#可以看到是对每一个channel求均值和方差的
print(norm.weights)
print(norm.trainable_weights) # gamma 和 beta 各有 5 个,每个通道各对应一个 gamma 和一个 beta。


(2)BatchNormalization在NLP(shape=[batch,length,embedding])的实现

#batchnormalization在NLP的实现
a = np.arange(24).reshape(2,3,4).astype(np.float32)
#3是每个句子的词汇的数量,4是embedding的维度
#这里用 axis=1 做个演示:先固定词在句子里的位置,然后把所有样本在这个位置上的数值放在一起算均值和方差。
#也就是说,每个位置一共用 batch*embedding 个数。
norm = tf.keras.layers.BatchNormalization(momentum=0,axis=1)
print(a)
print(norm(a,training=True))
# NLP中的数据为度:(B, S, E)。其中B表示批量大小,S表示序列长度,
# F表示序列里每个token的embedding向量维度
# 对每个词的位置,把所有样本在这个位置上的数值放在一起算。
print(norm.weights)
print(norm.trainable_weights) # gamma 和 beta 各有 3 个,每个位置各对应一个 gamma 和一个 beta。

按词的位置做BN,会遇到什么问题?

NLP模型的输入数据是动态长度,如果把每个词看成一个特征,则RNN的每个样本包含的特征数量不同。(尽管有padding,但是padding的数值没有意义)

比如说,当一个batch中某个样本包含10个词,其它样本均只包含6个词,则前6次BN对所有样本进行,而后4次BN只对一个样本进行,前6次BN和后4次BN失去了可比性。

原文配图


(3)LayerNormalization在CV(shape=[batch,height,width,channel])的实现

#layernormalization在图像的实现
a = np.arange(120).reshape(2,3,4,5).astype(np.float32)
norm = tf.keras.layers.LayerNormalization(axis=[1,2,3])
#axis=[1,2,3],代表一般在CV中,首先挑选某个样本,然后对这个样本内所有值求平均
#也就是说,对于某个特定的样本,需要求height*weight*channel个数值的平均值
print(a)
print(norm(a))
print(norm.weights)
print(norm.trainable_weights)


(4)LayerNormalization在NLP(shape=[batch,length,embedding])的实现

#layernormalization在NLP的实现
a = np.array([[[1,2,3,4,5],[12,12,12,12,12]],
              [[1,3,5,7,9],[12,13,15,190,5]]]).astype(np.float32)
norm = tf.keras.layers.LayerNormalization(axis=-1)
#axis=-1,意味着指在一个句子的一个token的范围内进行标准化
#比如,先固定一句话里的一个词,只用这个词自己的 embedding 算均值和方差。
#别的句子、别的词都不算进来。
print(a)
print(norm(a,training=True))
print(norm.weights)
print(norm.trainable_weights)
原文配图


最后,对比BatchNormalization和LayerNormalization的axis的区别

#对比BatchNormalization和layernormalization的axis
a = np.array([[[1,2,3,4,5],[12,12,12,12,12]],
              [[1,3,5,7,9],[12,13,15,190,5]]]).astype(np.float32)
norm = tf.keras.layers.LayerNormalization(axis=-1)
# norm = tf.keras.layers.BatchNormalization(momentum=0,axis=-1)
print(a)
print(norm(a,training=True))
print(norm.weights)
print(norm.trainable_weights)


BN的axis指的是固定该轴的每一个元素,比如axis=3,先固定3轴的第一个元素,把3轴的其余元素先暂时去掉,然后计算剩下来的所有值的平均值。

LN的axis指的是对该轴的每一个元素求平均值。相当于固定其他轴。

由上面的定义,可以看出,以输入 dim=3 为例(包括 batch 维),给这两层同一批数据,用相同的 epsilon,先不算 gamma 和 beta,BN 也用当前批次的均值和方差,那么以下两行代码算出来的结果是一致的:

norm = tf.keras.layers.LayerNormalization(axis=[0,1])
norm = tf.keras.layers.BatchNormalization(momentum=0,axis=2)

按同样的条件,以下两行代码的结果也是一致的。这里的 BN 多轴写法适用于支持多轴的旧版 Keras 2(例如 2.13.1);Keras 3 的 BN 只接受单个轴。

norm = tf.keras.layers.LayerNormalization(axis=2)
norm = tf.keras.layers.BatchNormalization(momentum=0,axis=[0,1])

但是注意参数量是不一致的。不论是 LN 还是 BN,默认都有一组 gamma 和一组 beta,两组的大小都由 axis 决定。比如说,a 的 shape 是 2*2*5,如果 axis=-1,gamma 和 beta 就各有 5 个;如果 axis=[0,1],那就各有 2*2=4 个。对于BN来说,还有非可训练的参数是移动平均和移动方差,形状也是等同于上面。

从这个意义来说,LN存在尤其道理,并不能完全被BN取代。

下面是 Llama 的 RMSNorm。它不减均值,embedding 的每一维都有自己的缩放系数。

class LlamaRMSNorm(nn.Module):
    def __init__(self, hidden_size, eps=1e-6):
        """
        LlamaRMSNorm is equivalent to T5LayerNorm
        """
        super().__init__()
        self.weight = nn.Parameter(torch.ones(hidden_size))
        self.variance_epsilon = eps


    def forward(self, hidden_states):
        input_dtype = hidden_states.dtype
        hidden_states = hidden_states.to(torch.float32)
        variance = hidden_states.pow(2).mean(-1, keepdim=True)
        hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
        return self.weight * hidden_states.to(input_dtype)

知乎原文

混沌、复杂网络、自我指涉与生命游戏

这是我在公司内部讲授的科普课程。我把讲稿整理成文字,发到这里。内容参考了其他博主,参考链接在文章末尾。


今天我为大家分享的主题是人工智能与新型科学——混沌、复杂网络、自我指涉与生命游戏。这个名字有些长,涵盖的范围也比较广。如果你看到这个标题,觉得一脸懵逼的话,可以观察下面的动图。我今天要说的所有内容的核心,都概括在这张动图里。

原文配图

我首先要讲的是生命游戏。生命游戏是英国数学家康威在上个世纪60年代发明的,它是在一个二维平面上构造的游戏。这个二维平面铺满了格子。每个格子只有黑和白两种状态。我们把黑色格子看作一个活的细胞,白色方格代表啥也没有。细胞生命是有生老病死的,基于此,我们制定两个规则,分别是生命的诞生规则和死亡规则。

诞生规则:如果一个白色方格周围有三个黑格,那么这个白色方格会变黑。这里的周围,指的是周围8个格子。比如下图中,中间的白色格子,周围有三个黑色格子,那么在下一刻,这个白色格子会变黑。

原文配图
诞生规则

死亡规则:如果对于一个活着的细胞,当他的周围的黑色格子小于2个,或者大于3个,那么在下一时刻,它会死掉,即变成白色。

原文配图
死亡规则

为什么叫做生命游戏?它模拟了自然界生命的生老病死。只有当周围生命数量合适的时候,即不会过多也不会过少的时候,生命才有可能诞生。否则生命难以维持存在。比如我们关注最中间那个格子,中间格子有四个活着的邻居,它会因为周围的生命过多,资源竞争而死。

那这个游戏怎么玩呢?我们要先设定好一些初始细胞,然后同时对所有的格子执行上面提到的两个规则。这样就称演化了一代。然后我可以一代一代地去演化。

我们现场演示一些例子。比如我画一个”一“,如下图所示。它的下一代就是竖过来的一(最左边和最右边的格子因为邻居不够而死亡,上面和下面的格子因为邻居个数适中,而诞生生命)。那再下一代呢?同样的道理,是横过来的一。如果让他不停地演化,它就是这样一个周期震荡的结构。

原文配图

看下面稳定结构的例子,不论演化多少代,它都保持同一个状态。

原文配图

那么问题来了,我们看到这些例子中,种群要么震荡,要么稳定不变,要么就完全消失了。那么有没有这样的可能:从有限的细胞中,生成无限的生命呢?这个问题让生命游戏的创始人也苦恼了很久。最终,有人提出了一个极其简单的例子。这个例子的结构特别简单,只有一条直线。我们运行看一下。

原文配图https://www.zhihu.com/video/1554225355266691072

还有更加漂亮的结构。大家可以看这个视频。

原文配图https://www.zhihu.com/video/1554214585447653377

不要看生命游戏的规则简单,但是它的功能非常强大。生命游戏是图灵完备的。也就是说,计算机能做的事情,理论上,生命游戏也能做。关于生命游戏的部分,我先告一段落。生命游戏还有更神奇的地方,我待会再讲。


接下来讲第二部分,大家有没有看过《三体》?我很喜欢三体这部小说,它构造的科幻世界非常宏伟。三体首先描述了一个三体问题,就是说在一个遥远的星系中,有三个球,这三个球之间的运动,是一个混沌系统。

原文配图https://www.zhihu.com/video/1554215238513459200

任何人都没有办法去精确地预测出这三个球的运动轨迹。像我们太阳系,预测火星什么时候离地球最近,或者预测什么时候月亮会跑到太阳和地球中间,形成日食,都是可以精确预测的。但是三体星系中,三个球之间的运动关系,形成的微分方程,它不可能被计算机精确地求解。所以三体星人过得很悲催,动不动就要把自己抽干水分,或者不远万里侵略地球,掠取新的土地。

由三体问题,引出混沌。混沌,是对初始值极端敏感的、看似随机貌似无规则的运动。这句话要怎么理解呢?要拆分两部分来看,它对初始值极端敏感,敏感到一丁点儿变化都能对结果产生很大的影响。我这里特别加粗了敏感两个字,就是为了让大家知道,它对初始值非常敏感。大家应该听说过蝴蝶效应,美国德克萨斯州一只蝴蝶的翅膀,可能会引发南美洲的一场大风暴。我国古代著名谚语——失之毫厘、差之千里也描述了混沌这一核心。(我们可以看这个视频,它是一个双摆系统。这两个系统的初始值参数有一点点区别,一开始它运行的差别不大,但是越到后面,它们的差距越大。这就是失之毫厘,差之千里)

原文配图https://www.zhihu.com/video/1554215578013044736

后半句是“看似随机、貌似无规则”。混沌的核心就在于这半句话。看似随机,实际上意味着它不是随机的,是被决定的。但矛盾的是,混沌仍然没有办法被预测。为什么会形成这样矛盾的现象?如果要说清楚这一点,可能要举一个例子,这是我今天课程中出现的唯一一个公式,我尽量避免让数学公式出现,因为霍金说过一句话:每出现一个数学公式,都会吓跑一半人。

原文配图

这个方程描述的是虫口模型。它指的是一种虫子的繁衍后代数量的规律。k代表第几代。假如取初始值是0.1,那么迭代100代后,值是0.3724.

那如果初始值做一些微小的改变,迭代了100代之后,出来的值相差很大,看起来没有什么规律。注意,不要看表面上 x100x_{100} 的绝对值差距很小,x的定义是一个比值,取值范围是在(0,1)内的。第一行是0.37,第二行是0.01,这种差距是天壤之别了。

有人可能会问,如果我只需要把初始值做到完全精确就好了呀。的确,假如,有一个全知全能的上帝,什么都知道,那么混沌现象对于上帝来说是完全可以预测的。但是,理论上,任何人都做不到完全精确地计算初始值。举个最简单的例子,我现在所持的手机,长度是15.7厘米,那真的刚好是15.7厘米吗?不是,它可能是15.714684613267941……在现实生活中,基本上所有的测量,都不可能完完全全地精确,哪怕我用尽宇宙中所有的原子,所有的算力,都不可能完全准确。所以,只要它不可能完全精确,只要相差了一点点,最终迭代的结果都会相差甚远。

在很多数学教材中,混沌和分形是如影相随的,混沌和分形是什么关系呢?有一句话这样形容:混沌是时间上的分形,而分形是空间上的混沌。

20世纪初,英国数学家刘易斯在研究英国海岸线的时候,第一次发现了分形的奥秘。他发现,一条海岸线的长度不是绝对的,它取决于你用什么样的尺子来测量。采用的测量仪器越精细,这条海岸线就越长。

原文配图

举个例子:当你用一只快艇绕着小岛游一圈,测量出岛的周长就是快艇行进的距离。但是,如果你用尺子去测量,就会发现岛屿的边缘被海水侵蚀而凹陷,那么就要把凹陷的长度算进去。如果你看得更加精细,发现即使是一个拳头那么大的岩石也有不规则形状的凹陷,那么还要计算这部分凹陷的长度。如果测量得足够精细,这座岛的海岸线将是无限长的。这是一个悖论:有限的面积,却有着无限的周长。

原文配图

而且,当你把海边的一块岩石放大到足够大,会发现它的边缘和整座岛的边缘非常相似。这就是自相似,局部与整体是相似的。所以我认为分形的世界是一个特别美妙的世界。佛说,一花一世界,一叶一菩提。在分形中,小尺度上可以窥视到它完整的自身,无穷无尽,自我相似。

有意思的是,有人将分形的思想应用在深度学习中。这是一个分形神经网络,不知道大家有没有用过。它的网络结构,看起来具有自相似性,即小范围的网络结构,和大范围的网络结构,是相似的。

原文配图

深度学习是混沌系统吗?大家可以思考一下,混沌是对初始值极端敏感的、看似随机的运动。训练好的深度学习模型具有鲁棒性,对初始值不敏感,输入的细微改动不会对输出有很大的影响。比如,我输入一个人脸头像,得到一个结果,然后我在人脸图像中随机调整一两个像素,得到的结果不会发生太大的改变。而且深度学习的过程明码标价,可以预测,不存在看似随机的现象。因此深度学习不是混沌系统。

接下来讲一点哲学的东西:拉普拉斯妖。这是法国著名的科学家拉普拉斯于200年前提出的一个思想实验。宇众所周知,宙的现状是过去的结果、未来的原因。假如有一位全知全能的智者(我们称之为‘拉普拉斯妖’),知道某一时刻所有导致自然运动的力和所有物体的位置,那么他用一个公式就能描述所有物体的运动。也就是说,对于这个拉普拉斯妖而言,所有的事情都是可以预测的,都是确定的。我知道整个宇宙所有粒子现在的状态,那么下一时刻它们是什么状态我也可以预测出来,那么下下时刻、下下下时刻,都可以预测出来。

我们再梳理一遍,这个世界是服从因果律的,也就是说,每一件事情的发生,都有它的原因,比如说,起风了,那是因为空气在流动,而空气流动,那是因为大气压强发生了不平衡,而大气压强的不平衡,是因为太阳辐射到地球的能量引起的,……,等等等等,我们可以一直向上追溯。一件毫无起因的事情是无法想象的;那么反过来说,每一件事情的发生,都是有后果的,我们同样无法想象一件事情发生之后没有任何影响。那么,一件事情A发生后,产生了一个后果B,而B的发生,又引发了一个后果C,……,这就形成了一个因果链。

那假如一切都符合因果的话,那么是不是未来的一切真的已经确定了?

我们注意到,那个时代还是牛顿力学占据绝对统治的时代,那个时候的欧洲国家,已经发生了天翻地覆的改变,政治上,资产阶级革命建立了民主制度,经济上,工业革命大大提升了生产力,在科学领域,整个自然界,各种宏观物体的运动,热现象,电磁场,全部有了完备的理论。宇宙万物,从质点和刚体的运动,到宇宙天体的运行规律,都可以归结为几个简单的力学定律(牛顿三定律)。看不见摸不着的电磁场,人们也可以得通过麦克斯韦方程组得心应手地精确计算。那段时间可以说是经典力学理论下的黄金时代。

在经典力学的理论下,如果已知一个系统现在的运动状态,人们通过构造若干微分方程,然后就可以精确计算这个系统未来的一切运动情况。

再举一个例子:桌球。我们假设有一个绝对光滑的桌面和一个绝对光滑的小球,只要我知道这个小球初始的力和方向,我们可以很自信地说,小球的命运将是注定的:在无限长的未来,它每一时刻的位置和速度我们都可以精确预言。那么,这个小球的后续的所有的运动轨迹都是可以完全确定的。

原文配图

我们已知的宇宙中大约有2的1080次方个原子,如果每个原子看做一个小球,那么我们可以说,整个宇宙无非就是一个大一点的桌球游戏而已。

在拉普拉斯妖看来,宇宙中每一个粒子的未来都是可以精确预测的。既然每个粒子的命运都是注定的,那么这些粒子所组成的宏观事物,石头、山山水水、飞禽走兽、行星、恒星、星系,等等一切,这些事物的命运也就是注定的。既然拉普拉斯之妖可以准确预测出你的每一个组成部分,那么你作为整体当然也就确定了。那么,整个宇宙的一切命运全部不言而喻地难逃那个注定的命运。更恐怖的是,假如意识真的是由人脑的粒子运动决定的,那么,人的意识也是被决定好了的。人的身体,人的脑袋也是由粒子组成的,粒子要不要服从物理规律?当然要服从。那等于说,我在下一时刻的意识是怎么样的,也是可以预测的,或者说,也是已经决定好了的。

更恐怖的是,人,说白了无非是一堆原子的组合,我的每个原子,也符合物理规律,我现在在和你们说话,本质上,是组成我的这一坨原子在和你们说话,它都是遵循物理规律的。所以如果我们能知道一个人所有原子这一刻的性质,那理论上每个原子未来的运动都是被决定了的。你整个人接下来的行为,其实也都被决定的了。

其实这会引发一个更加深奥的话题——人是否有自由意志?什么是自由意志?我至今都没有找到一个很好的、通俗易懂的回答。我举个例子说明可能会比较好,比如你们在玩游戏的时候,碰到游戏中的NPC,那些NPC,只是根据程序员预先设定好的程序和玩家对话,它是没有自由意志的。那么类似地,我们人类,表面上很自由,可以思考一切,做一切决定,比如,我现在决定今晚去吃麻辣烫,那这个决定,真的是我的意识决定的吗?还是说我的神经元、我的身体细胞的各种分子化学反应决定我今晚去吃麻辣烫呢?你以为你可以自由地决定做某件事,但其实自以为的自由,其实是被客观规律决定的了。只要我算力够强,我可以根据硬币所有原子的坐标,来预测抛硬币后硬币是正面朝上还是反面朝上。我也可以根据你的原子坐标,来预测你未来的运动。我们并不比硬币高级,我们都是原子。

不知道大家有没有看过诺兰的2020年的电影《信条》。他还有一部著名的电影是《盗梦空间》,大家应该听说过。我放一段视频。

原文配图https://www.zhihu.com/video/1554217076230909953

说实话那部电影我以为只是一部纯粹的时空穿越的片子,但是诺兰想表达的,应该更深刻。

再举个例子,比如我现在想喝杯水,于是我站起身倒了杯水。我站起身倒水这个行为,真的是我自己的意识所控制的吗?

很多朋友的第一反应可能是:当然是了,这有什么疑问吗?我不正是因为「想喝水」,才会站起来倒水吗? 但其实不一定。 神经科学家做过一系列的实验,他发现:早在参与者产生一个「想法」之前,参与者的大脑中就会产生一个电信号的波峰。通过这个波峰,他可以提前预测参与者接下来会做什么。这个电信号比参与者产生「想法」的时间提前多少呢?平均在0.3秒左右。 这是一个震惊心理学和神经科学界的发现。也就是说:早在参与者「决定去倒水」的若干秒前,其大脑就已经发出了相关的信号。

原文配图
我们采取的有意识行动,很可能并非出自「我们的意识」

表面上,是自己想倒水这个意识,决定了他真的站起来去倒水,但实际上,是有一坨电信号,这个电信号产生两个分支,第一个分支是产生我想喝水的意识,第二个分支是支配我们站起来去倒水。类似的实验还有很多。种种实验都指向这个结论:我们采取的有意识行动,很可能并非出自「我们的意识」。

简单来说:我们会以为,我们是先产生「想喝水」的意识,从而才站起来倒水的。意识是行为的原因。 但实际上并不是这样,通过这些一系列的实验,科学家们观测到:在我们产生某个确切的意识之前,我们的大脑就会产生一个明确的信号。这个信号总是比意识更早,并且,它可以明确地预测我们的行为。 这说明了什么呢?这个电信号才是我们产生行为的真正原因。而意识,只不过是这个电信号给大脑的一个反馈而已。意识和行动没有因果关系,意识决定行动其实是我们的错觉。

其实你可以观察一下自己,你每天在工位上,每一个小动作,比如摸自己头发,摸自己鼻子,跷二郎腿,抖腿,去上厕所,等等一系列动作,真的是你自己做出决定的吗?你有意识到你做出了这些决定吗?

打个比方:我们的意识,就像一个皇帝。看似他在风光地发号施令,但实际上,在他背后还有一个影子内阁,所有的决策都是他们作出的,经由皇帝之手发布出去,皇帝只不过是他们的一个传声筒。 明确了这一点之后,我们进一步思考,如果我们的一切想法和行为,都来自于身体和大脑根据内在和外在的刺激,所产生的电信号的反馈,那么,岂不是可以说,我们的一切想法和行为,其实都是被「决定」了的?

换句话说:假如我知道了一个人所有的状态,那么我是否可以预测他接下来的想法和行为? 有朋友可能会质疑:可是,当我产生想喝水的意识时,我也可以不这样做呀。我也可以克制住自己,不站起来倒水,这难道不是自由意志吗? 但是,你想一想:不站起来倒水,这也是一个决定,这个决定,你怎么知道他是不是出自于自由意志呢?你是用一个新的想法(不这样做)代替了旧的想法(想喝水),这本质上是一模一样的:这个新的想法,必然也可以从大脑的电信号中找到它的影响因素,它必然也有更上一级的「原因」。 于是,我们还是绕回了决定论。

关于自由意志的话题就说到这。我不想说太多关于自由意志的内容,还有一个原因是这个话题太悲观了。现在科学界主流的观点就是人没有自由意志。什么人定胜天啊,我命由我不由天啊,都不成立,一切都是命中注定的,这个结论其实很悲观很消极的。

但是话说回来,人没有自由意志,虽然是目前科学界主流的观点,但也不是压倒性的、一定就是正确的观点。有没有什么,能治一治悲观的决定论呢?

有人可能会想到混沌,毕竟混沌下的世界是不可预测的。但是注意,混沌恰恰是决定论的拥趸,是决定论的强有力的证据和支持者,是把决定论推上悲观宝座的重要力量。为什么这么说?决定论仅仅是说,世界是被决定好了的。但是,混沌告诉我们,这个世界不仅是被决定好了的,而且我们甚至无法去预测它,这是更悲观的事情。

但是别急,俗话说的好,遇事不决,量子力学。量子力学发现,微观世界的粒子,不但不满足经典物理定律,甚至无法用我们熟知的经典物理概念(比如说速度、位置等)来描述。微观粒子有一种完全怪诞的行为模式,颠覆了人们的三观。在量子的世界,“因”不一定会导致“果”,甚至更恐怖的是,结果可能会先于原因而存在。量子世界,可能不符合因果律,也不符合决定论。

如果要讲清楚量子力学为什么颠覆了因果律,那么需要从著名的双缝干涉实验说起。那这个讲起来又是三天三夜了。有兴趣的伙伴可以自己去了解一下。但是我还是要强调一点,混沌是完全的确定系统,混沌系统没有随机性,它只是对状态变量的精度非常敏感。世上大多数的随机系统,只不过是一个无法精确测定输入值的混沌系统所产生的表象。真正的随机是量子态。比如,我现在在打麻将,抛一个骰子,骰子的结果,看似是随机的,但是,几点朝上,已经在我抛的那一时刻,被决定好了的,只要有一个拉普拉斯妖,知道我抛骰子时候的力、位置、方向等信息,那么是可以预测出来几点朝上的。但是我们作为人类,不是拉普拉斯妖,是无法在这个混沌的世界预测骰子几点朝上的。也就是说,虽然无法预测几点朝上,但是抛骰子并不是完完全全的随机,它从我抛骰子那一刻,就已经被决定好了的。

再往深了点说,假如我昨天打篮球扭到了手,我今天抛骰子的时候,力度不会太大,也就是说,昨天的行为影响到今天骰子几点朝上。那再做扩展,就像多米诺骨牌一样,我现在抛骰子,几点朝上,很可能在140亿年前宇宙大爆炸,就决定好了的。

但是,如果量子力学是正确的,那么在量子领域,将会产生完完全全、不可预测的随机。如果我们的意识是量子的,那么我们可能会拥有自由意志。如果世界是量子的,那么决定论可能是错误的。


接下来,说下一个内容:复杂网络。

复杂网络是一个很新的领域,它诞生于1998年,距今只有20多年的历史。什么叫做复杂网络?网络,就是用抽象的点和线组成的拓扑结构。点表示个体,线表示个体之间的关系。网络是对现实生活中的关系的抽象,比如大名鼎鼎的七桥问题,左图有七座桥,问你能不能把每座桥都走一遍,且不能重复,即每座桥不能走第二遍?数学家把七桥问题抽象成右边的由点和边组成的图,成为网络。复杂网络,可以理解为特别多的点,和特别多的线,组成的网络。

原文配图

生活中有特别多复杂网络,比如我们现在的互联网就是复杂网络。比如电力系统,方圆几百公里的电网基本是连起来的,它是一个特别巨大的网络。打仗的时候,攻击电力系统的哪些点,都是有讲究的。右边的论文引用系统,A的论文引用了B,然后B的论文又引用了C,这样会组成一个特别庞大而复杂的网络。

原文配图

人与人的人际关系也是一个复杂网络。每个人,都是这个网络上的一个点,假如我和你相互认识,那么我这个点和你这个点之间,就会有一条线相连。有一个著名的六度空间理论,世界上任意两个人之间建立联系,最多只需要六个人。

举个例子,我们和奥巴马看似遥不可及,但是我们和老总不遥远吧?我们老总是认识奥巴马的,这个链条其实只有两三个人。

与人际关系类似的,还有传播网络。比如,谣言传播网络,谣言是怎么口口相传的,是怎么三人成虎的。这些其实值得研究。新冠是怎么传播的?现在越来越多的研究,开始从复杂网络的角度,去研究传染病的传播机制。

原文配图

还有交通运输网络。全国大大小小的公路,四通八达。包括我国的铁路网,也是复杂网络,它是牵一发而动全身的,比如哪条高铁在路上出了故障,那很多条线路都会受到影响,这种牵一发而动全身的特性,是不是和混沌有点像?那么有个问题,地铁是复杂网络吗?地铁它肯定是网络,但是地铁线路简单、规则,你说它不是复杂网络,也有道理。但是,在一个特大城市内,地铁,加上公交,有可能组成一个颇具规模的复杂网络。有没有同事之前做过地图路径的推荐工作?我们现在打开百度地图,从前海出发去春笋,有地铁方案,有公交方案,还有地铁加公交方案,哪种方案最省时间,这背后涉及到很多有意思的算法。

原文配图
原文配图

此外还有生态网络,这个网络应该是最古老的复杂网络了,在地球存在了上亿年。大鱼吃小鱼,小鱼吃虾米。

我们的计算机网络也是一个局的大复杂网络。但是要分清楚区别。因特网是物理形态,是看得见摸得着的,万维网是信息形态,是看不见摸不着的。

原文配图

最后,就轮到大名鼎鼎的神经网络了。当然,这里的神经网络,不是我们训练深度学习的神经网络,而是生物神经网络,是神经网络最原始的含义。人脑是由大量的神经元细胞组成的,神经元的工作产生了人脑的智能,人脑的研究导致了神经网络这一门学科的诞生。

原文配图

之前我问到一个问题,人工神经网络是不是混沌系统?我现在继续问一个问题,人工神经网络是不是复杂网络?答案仍然不是。因为现在人工设计的神经网络,节点数目很少,而且很规则。规则网络不是复杂网络。说句我个人的观点,这个世界是混沌的,这个世界运行的网络是复杂网络,而人工神经网络,既不混沌,也不是复杂网络,我个人认为,当前基于梯度下降的人工神经网络,不可能孕育出强人工智能。

复杂网络作为一门新兴学科,而且还是一门交叉学科,潜力巨大,前景巨大。从以上的大致介绍可以看出,复杂网络已经渗透到生活的方方面面。它所谓一门交叉学科,在数学、物理学、信息学、计算机学、新闻传播学、生态学、医学、哲学,等几乎一切学科,都有应用的地方。

那复杂网络主要研究什么呢?这里举了一些例子,我就不一一介绍了,这个课程只是带大家浮光掠影、浅尝辄止地了解一些新东西。复杂网络和混沌有什么关系?我们要明白一点,复杂网络,绝不仅仅是之前介绍的那些静态的网络图,网络都是处于不断的运动中。那复杂网络之间的运动,可以用非线性动力学,或者混沌来建模。

有两种类型的复杂网络需要单独拎出来说说:小世界网络和无标度网络。

原文配图

小世界网络指的是,对于这个网络中的大多数任意节点,都可以用较少的步或跳跃访问到其他节点。我刚刚提到的六度空间理论,世界上任意两个人之间建立联系,最多只需要六个人。这就是小世界现象。所以人类的社交网络就是小世界网络。

无标度网络,指的是大多数“普通”节点拥有很少的连接,而少数“热门”节点拥有极其多的连接。这样的网络称作无标度网络,网络中的“热门”节点称作枢纽节点。比如右图中,中间的那几个点,普遍有三条边四条边和他们相连,但是最外围的点,只有一条边和他们相连。

无标度网络在生活中随处可见。我们的社交网络仍然可以看作是无标度网络。打开微博,你会发现,大多数普通人的关注数和粉丝数都是很少的,而极少数的大v的粉丝数是特别巨大的。在无标度网络中,很容易出现两极分化的现象。

无标度网络背后的数学原理是幂律分布。如果把对于一个点,有多少条线和他相连,成为这个点的度,比如一个点,和其他15个点有连接,那么称这个点的度是15。在无标度网络中,度的分布是幂律分布。

对于一些事物,个体与个体之间的差异不大。比如人的身高,中国成年男子的身高绝大多数在平均值1.70米左右。正态分布描述类似这样群体特性大致相同的情况。

对于另一些事物,个体与个体之间的差异明显。比如个人收入,大多数人月收入不到一万,而少数人月收入高达百万。幂律分布描述类似这样多数个体量级很小,少数个体量级很大的情况。

原文配图

大家应该听过二八法则:80%的人掌握着20%的财富,20%的人掌握着80%的财富。还有一个马太效应,即穷者越穷,富者越富。这就是幂律分布刻画的特性。在很多国家,居民收入都是符合幂律分布的。这种分布下的贫富差距非常大。我们可以畅想一下,在遥远的共产主义社会,居民收入的分布很可能是正态分布。

无标度网络,无标度这个名字怎么理解?仍然和分形有关。我们知道,分形具有无标度的特性,也就是说,比如这个图是一个典型的分形图,它的无标度指的是,我随便单独挑出这个图里面小范围的子图,比如红色框框里面,会发现,它和大图是相似的。我不论在什么样的尺度下,所看到的内容,都是相似的。这就是无标度的特征。

原文配图

之前提到的几张生动描绘分形的动图,将无标度的特征刻画得很明显了。

原文配图
原文配图

幂律分布是无标度性质的,怎么理解呢?假如你是一个点,把你放在这个幂律分布函数的任何一个地方,你只能看到前面和后面,是无法判断自己处于这个函数的哪个位置的。这是幂律分布特有的性质。比如正态分布,如果你正好被放在了山顶,你是很容易判断出自己就是在函数的山顶的。这就是幂律分布的无标度的理解,也是无标度网络这个名字的由来。

原文配图

无标度网络还有一个比较有趣的性质:它既是鲁棒的,也是不鲁棒的。比如说我去攻击这个网络,由于这个网络绝大多数的节点的度都很小,即绝大多数点的连线都很少,因此我如果是黑客,我大多时间只能攻击到不重要的节点。不至于整个网络瘫痪。因此可以说无标度网络是鲁棒的。但是另一方面,假如,我恰好攻击到核心节点,那网络会马上瘫痪。从这个角度来看,无标度网络又是不鲁棒。我们俗话说的,射人先射马,擒贼先擒王,也正是这个道理。

现在复杂网络的研究可以说是如火如荼。我说两个最新的、也是离我们最近的研究成果。

第一个是舆论网络。有研究者通过对舆情网络的分析,得出结论,删帖并不能阻止谣言传播,当然这里不仅仅指的是谣言,一些不是谣言的东西,如果一味地去删帖,反而也会起到反作用。我们在引导舆情和打击虚假信息时,只改变传播网络的拓扑结构,如删帖、控评和拉黑等,往往很难抑制传播行为。一个事件的传播速度往往与网民的偏好程度有关,对一个事件的辟谣和科普能够很好降低偏好程度,因为人类对“秘辛”的偏好远高于对事实的追求,这就是为什么在事件删贴后却能激起对此事件更大范围的传播,引发复杂传染。

另一个是关于防疫的研究。大意是说,封城的确可以控制疫情,但综合来看,对奥秘可容来说,封城也是性价比最差的方法。其实复杂网络最重要的应用之一,就是疾病传播模型。而且在研究疾病传播模型的时候,不同的病毒,不同的毒株,网络的参数是不一样的,这个复杂网络的演化规律也是不一样的。所以,在初代新冠病毒、德尔塔病毒之下,积累的经验,面对奥秘可容,是否仍然有效?这是值得思考的问题。我们现在身处于疫情之中,身处于历史之中,平时我经常会思考,我们现在的政策,真的是疫情的防控不仅要关注防控的精准性,更要关注不同变异毒株在传播速度上的差异。在德尔塔毒株爆发时期构建并成熟的精准防控经验,可能不适应传播速度更快的奥密克戎毒株。大城市能实现小区域的精准防控,而小城市在面临疫情时往往要封城应对,与不同城市可调用的资源以及应急响应系统的速度有关。在无法快速响应时(如快速的全域核酸检测、大规模医务人员的调度),在病毒传播速度不可控的情况下,全域封锁可能是大规模改变网络拓扑结构的无奈妥协。

复杂网络还会形成一个现象,叫做涌现。大家可以看看这个视频。

原文配图https://www.zhihu.com/video/1554218695408164864

一只蚂蚁,它其实很笨,但是一群蚂蚁结合起来,它会形成一个比较有智能的群体,达到了一加一大于二的效果。那同样的道理,一个神经细胞其实看起来功能比较简单,对不对?但是几十亿个、几万亿个神经细胞和系在一起,就形成了我们人类复杂的意识,那么我们能不能从这个角度去出发,来创造一个人工智能呢?假如说,真的有一天我们也能够,在机器里面创造很多很多的仿生的神经细胞,达到了和我们脑袋一样的数量级,那么涌现是不是会发生,真正的人工智能会不会出现?

然后我再说一个细思极恐的想法,整个宇宙,至少目前探明的,已经有几万亿亿亿颗星星,那这些如果说我们把每一个星星当作一个神经细胞的话,这么多的东西组合一起,会不会发生涌现呢?也就是说,整个宇宙会不会就是一个巨大的生命体呢?而我们地球只是这个巨大的生命体中的一个小小的细胞。


接下来讲第三部分,自我指涉。这是比较偏哲学的内容。

自我指涉(self-reference)简称自指,是一个逻辑学的概念。通俗来说,一个东西在描述自己,这就叫做自我指涉。自我指涉广泛地出现在数学、音乐、文学、美术和生活领域。

这张图是著名艺术家埃舍尔的著作。它要表达的内容是手正在画自己。即,我正在画我自己,而且,我正在画我自己这件事情,也正在被画下来。

原文配图

在生活中也经常出现自我指涉的现象。比如这个路牌,说,本指示牌不再被使用。那么,不论它是不是正在被使用,都是说不过去的,都是自相矛盾的。

原文配图

再看这张图,工信部要求,不允许要求用户下载APP才能查看原文的情况出现,然而,百度在报道这条新闻的时候,要求用户下载APP才能查看原文。

原文配图

然后这张图,大家知道这是盲道。这个标语是想让大家不要占用盲道,但是这条标语本身,占用了盲道。

原文配图

又比如这个投票,既然大家都认为我是极少数,那这个极少数又变成了大多数,这是自相矛盾、哭笑不得的。还有一个类似的投票,是问大家,你认为真理掌握在少数人手上吗?结果绝大多数人都投票给“我认为真理是掌握在极少数人手中的”,这就创造了一个矛盾。

原文配图

除了以上栩栩如生的图片的例子,在语言中,也大量存在自我指涉的现象。

原文配图

自我指涉,指的是一个系统,有指向自己的能力。我们仔细想想,判断一个动物,是否有自我,有一个很常见的实验,叫做镜子实验。如果把动物放在镜子面前,他能够认出镜子里的东西是自己,则说明这个动物有自我意识。像大猩猩、大象都通过了镜子实验。能够有自我意识是高级智能生命的必要条件。而自我指涉,相当于一个系统有指向自己的能力。说一句我个人的观点,一个真正的人工智能系统,应该是一个自指的系统。

附带说一句,数学中有一个比较玄妙的定理,叫做“哥德尔不完备定理”。这个定理的大致意思是,存在一个命题,它是真的,但是我们就是无法去证明它。大家可以看这个命题:“本命题不可以被证明出来”。这句话是真的还是假的?假设这句话是假的,那么本命题可以被证明出来。那既然可以被证明出来,为什么本命题是假的呢?另一方面,假设这句话是真的,那么本命题不可以被证明出来。这就很奇妙了,存在一个是真的,但是我们就是证不出来的定理。这是一个很细思极恐的定理。它意味着,我们很可能无法完全地认识这个世界。

再来看这个神奇的函数。我们知道,一个包含两个未知数的方程可以在坐标系中找到对应的图像。比如y=x^2是抛物线的图像。还有圆的图像。这个图像的神奇之处在于,组成这个图像的背后的方程,刚好就是图像显示的这个玩意。

原文配图

今天的分享快到尾声了。我在文章开头,说了生命游戏,还有一部分没有讲完。现在我再讲讲生命游戏中更神奇的地方。

原文配图https://www.zhihu.com/video/1554219809952403456

大家看到了吗?这是用生命游戏创造生命游戏,在生命游戏里玩生命游戏。这就是生命游戏中的自相似,自我指涉。

原文配图https://www.zhihu.com/video/1554219862703947776

再看这个视频。大家看出来了没,这个大的细胞,它竟然在自我复制。这个游戏,为什么叫做生命游戏?他并不仅仅是,我画几个简单的图案,它就会动来动去。生命游戏之所以叫做生命游戏,是因为它不仅可以自我指涉,还可以自我复制。你看我们自然界中的细胞,我们的生命,真正的有机物。为什么成为生命呢?因为组成我们生命的这些细胞,我们的DNA,它是会自我复制的,这是最根本的原因。生命的本质,就是自我复制,所以在我们的生命游戏中,我们看到了自我复制的现象,这是一件很惊喜的事情,这标志着,一个简简单单的二维平面上的格子的游戏,存在着产生二维生命的可能性。我们的人工智能,是不是也能从这个方向思考?

我今天讲了很多杂七杂八的东西。这些的内容其实都指向同一个核心——自我指涉。一个自我迭代的方程,产生了混沌,自我迭代,就是自我指涉。混沌与分形如影相随,分形是自相似、自组织,这也是自我指涉。复杂网络的无标度特性,以及复杂网络带来的涌现,也是自我指涉的。真正的自由意志,是我能够意识到我有意识,这也是自我指涉的。我们的生命游戏,以及自然界存在的真正的细胞的生命游戏,也是自我指涉的。

我认为,真正要产生人工智能,应该让机器有自我指涉的能力。并不是丢给我一些数据,经过乱七八糟的卷积、池化、激活函数,得到一个冷冰冰的数字。我们可以让“大脑研究大脑”,让“智能研究智能”,让人工智能产生人工智能。

以上是我分享的全部内容。


参考网站:

1.【毕导】你会点进这个视频,并一脸懵逼地出去,而这一切已被物理规律注定了_哔哩哔哩_bilibili

2.【3D】三体轨迹模拟_哔哩哔哩_bilibili

3.拉普拉斯之妖 - 贾明子的文章 - 知乎 https://zhuanlan.zhihu.com/p/28522532

4.人脑由粒子构成,而粒子遵循物理规律,那人的意识还自由吗? - 李睿秋Lachel的回答 - 知乎 https://www.zhihu.com/question/450868629/answer/1837323762

5. 在生命游戏中模拟生命游戏,震撼人心。_哔哩哔哩_bilibili

知乎原文

不要被表象迷惑——共因、共果、条件独立性与伯克森悖论

自古红颜多薄命是事实还是谬论?在演艺界,演技和长相真的成反比吗?游泳溺死人数与冰淇淋销量有关系吗?根据日常经验得出的结论,是否是正确的?

本文将介绍“共因”、“共果”两个概念,来揭示数据与经验是如何迷惑人们的。

首先看“共因”:如果事件C同时是事件A和事件B的原因,则称C是事件A和事件B的共因。如下图所示。有了共因,两件事就可能一起变,看起来像是一件事导致了另一件事。

原文配图

比如,把不同年龄的孩子放在一起看,你可能会发现,脚越长的孩子越会读书。这里就有一个共因:年龄。年纪大一点,脚通常长得更大,也多学了几年。如果想知道脚长和阅读能力到底有没有关系,可以先拿同龄的孩子来比。

类似的例子还有很多,游泳溺死人数和冰淇淋销量有“天气炎热”这一共因,因此不能说冰淇淋销量增加导致了游泳溺死人数增加。

还有一个更著名的例子是,吸烟是否会导致肺癌?统计学大佬(同时也是老烟鬼)费希尔认为吸烟和肺癌没有因果关系。他给出的理由是,可能存在一个共因,比如某种基因,使得有这种基因的人,更容易染上抽烟这个毛病,同时有这种基因的人,更容易患上肺癌。后来,对英国医生的长期追踪等研究提供了证据:吸烟确实会增加患肺癌的风险。


再来看“共果”这一概念。如下图所示,如果 A 和 B 都是 C 的原因,那么 C 就是它们的共果。

原文配图


回到最初的问题,演员的演技与长相成反比,很可能是错觉。如下图所示,一个点代表一个演员。图里先假设长相和演技相互独立,所以把点均匀地撒在这块区域里。但是,大众之所以会关注一个演员,大多是因为该演员要么长得好看,要么演技出众。长得难看又演技差劲的演员不会进入大众的视线。

原文配图

如下图所示,我们用红线来模拟这种筛选。线下面的点被筛掉,只看红线上方的点,长相和演技就呈现出负相关。

原文配图

出现这个错觉的原理是,颜值高与演技高都会产生“受到关注”这一结果,换句话说,“受到关注”是颜值高与演技高的共果。颜值与演技可能本来就没有相关关系,但是在控制“受到关注”这一共果的条件下,颜值与演技呈现出负相关关系。这称为伯克森悖论。

类似的例子还有很多:

比如自古红颜多薄命——丑且短命的女子一般不会受到关注;

寒门出贵子——家境贫寒且一生平庸的人一般不会受到关注;

帅哥出渣男——不帅且性格不好的男人,在约会之前首先会被筛选出去。


伯克森悖论还能用条件独立来解释。条件独立和独立是两个不同的概念。

独立的概念:若 P(AB)=P(A)P(B)P(AB)=P(A)P(B) ,则称事件 A、BA、B 相互独立。

条件独立的概念:若 P(AB|C)=P(A|C)P(B|C)P(AB|C)=P(A|C)P(B|C) ,且 P(C)>0P(C)>0,则称事件 A、BA、B 关于事件C条件独立。

独立不一定导出条件独立,条件独立也不一定导出独立。

图里先假设长相和演技互不相关。我们把两项分数加起来过线的人留下,只看这部分演员,就会看到长相和演技呈现负相关。既然有了相关,它们在这个条件下也就不独立了。

有一个例子可以直观地理解条件独立(来源于 @无名朔 )。如下图所示,有四个小球,一个小球的颜色是深蓝+红色,一个小球的颜色是红色,一个小球的颜色同时包含深蓝+浅蓝+红色,一个小球的颜色是浅蓝。


原文配图

随机从中抽取一个小球,事件A=小球的颜色包含浅蓝,事件B=小球的颜色包含深蓝。

P(A)=12,P(B)=12,P(AB)=14P(A)=\frac{1}{2}, P(B)=\frac{1}{2},P(AB)=\frac{1}{4}

因此A与B是独立的。

如果增加一个条件:在小球包含红色的条件下,A与B是否独立?记 C 为小球含红色。

P(A|C)=13,P(B|C)=23,P(AB|C)=13P(A|C)=\frac{1}{3}, P(B|C)=\frac{2}{3},P(AB|C)=\frac{1}{3}

不构成条件独立。只看有红色的三颗球,一旦看见浅蓝,就一定也有深蓝。


忽略共因,容易把相关当成因果。如果只看共果已经发生的那部分,原本没关系的两件事也可能看起来有关。几百年的科学发展史,“控制变量法”一直是实验的准则,它能够将“共因”固定住,更加科学地探索不同因素之间的关系。但如果不小心将“共果”控制住,很可能会将原本毫无关系的事件赋予相关关系。

我在《大白话概率论》里还用了一个住院病人的例子:为什么只看住院的人,会看出原来没有的关系。

知乎原文

赌徒输光定理:胜率不低,为什么还是可能输光?

股市中有一句话:市场中的散户,七负二平一赚。那如果一个人胜率不低,是不是就不用担心把本金亏光了?

我在四年前发过一篇文章:武辰:为什么说久赌必输?今天在这篇文章的基础上,扩展一些内容。

还是以抛硬币游戏来建立模型。假如小明和庄家玩抛硬币的游戏,硬币正面朝上的概率是p,反面朝上的概率是1-p=q。硬币正面朝上时,小明从庄家手里获得1元;反面朝上时,小明给庄家1元。小明有 n 元,两个人一共带了 t 元。每局独立,谁都不再往里加钱,谁先输光就停。小明先输光的概率是多少?

下面先算双方的钱都有限的情况。算完再把庄家的钱越放越多,看看小明只要还有钱就一直玩,会发生什么。

记 ana_{n} 为:资产为 nn 元时最终破产的概率。

根据全概率公式,有 an=pan+1+qan−1a_{n}=pa_{n+1}+qa_{n-1}

容易确定边界条件: a0=1a_{0}=1 , at=0a_{t}=0

先看 p 和 q 不相等的情况。有多种方法可以求得 ana_{n}

方法一:高中数列的方法

p(an+1−an)=q(an−an−1)p(a_{n+1}-a_{n})=q(a_{n}-a_{n-1})

(an+1−an)=qp(an−an−1)=qnpn(a1−a0)=qnpn(a1−1)(a_{n+1}-a_{n})=\frac{q}{p}(a_{n}-a_{n-1})=\frac{q^n}{p^n}(a_{1}-a_{0})=\frac{q^n}{p^n}(a_{1}-1)

n个上式相加,可以获得:

an−a0=(a1−1)∑i=0n−1qipi=(a1−1)1−qnpn1−qpa_{n}-a_{0}=(a_{1}-1)\sum_{i=0}^{n-1}{\frac{q^i}{p^i}}=(a_{1}-1)\frac{1-\frac{q^n}{p^n}}{1-\frac{q}{p}}

由at=0a_{t}=0

可知 a1=r−rt1−rta_{1}=\frac{r-r^t}{1-r^t} ,其中 r=qpr=\frac{q}{p}

代入上式可得 an=(qp)n−(qp)t1−(qp)ta_{n}=\frac{(\frac{q}{p})^n-(\frac{q}{p})^t}{1-(\frac{q}{p})^t}

方法二:差分方程

an=pan+1+qan−1a_{n}=pa_{n+1}+qa_{n-1} 的特征方程为

px2−x+q=0px^2-x+q=0

可得 x1=1x_{1}=1 , x2=qpx_{2}=\frac{q}{p}

an=c1+c2(qp)na_{n}=c_{1}+c_{2}(\frac{q}{p})^{n}

由边界条件: a0=1a_{0}=1 , at=0a_{t}=0,

可以算得 c1=−rt1−rtc_{1}=\frac{-r^t}{1-r^t} 和 c2=11−rtc_{2}=\frac{1}{1-r^t}

代入上式可得 an=(qp)n−(qp)t1−(qp)ta_{n}=\frac{(\frac{q}{p})^n-(\frac{q}{p})^t}{1-(\frac{q}{p})^t}

如果硬币是公平的,每多 1 元本金,输光的概率就少同样多,所以先输光的概率是 1−n/t。现在把对手的钱越放越多,而且小明只要还有钱就一直玩。当 p≤qp\leq q 时,最终输光的概率就是 1。硬币公平时,每局平均不输不赢,结果也一样。

当 p>qp>q 时, lim⁡t→+∞(qp)n−(qp)t1−(qp)t=(qp)n\lim_{t \rightarrow +\infty}{\frac{(\frac{q}{p})^n-(\frac{q}{p})^t}{1-(\frac{q}{p})^t}}=(\frac{q}{p})^n,这就是小明最终输光的概率。本金 nn 越多,输光的机会越小。只要还有输一局的可能,这个概率就不是零。


下面考虑盈亏比为2的情况。即,正面朝上时,小明获得2元;反面朝上时,小明失去1元。这一段只看对手的资金没有上限、小明一直玩下去的情况。

an=pan+2+qan−1a_{n}=pa_{n+2}+qa_{n-1} 的特征方程为 px3−x+q=0px^3-x+q=0

即 (x−1)(x2+x−qp)=0(x-1)(x^2+x-\frac{q}{p})=0

可得 x1=1x_{1}=1 ,

x2=1+4qp−12x_{2}=\frac{\sqrt{1+\frac{4q}{p}}-1}{2}, x3=−1−1+4qp2x_{3}=\frac{-1-\sqrt{1+\frac{4q}{p}}}{2}

an=c1x1n+c2x2n+c3x3na_{n}=c_{1}x_{1}^n+c_{2}x_{2}^n+c_{3}x_{3}^n

先看胜率超过三分之一的情况。这时一直玩下去,钱最终会越走越高。把同一条盈亏曲线的起点往上抬,碰到零的机会就越来越小,所以 a∞=0a_{\infty}=0

所以 c3=0c_{3}=0 , c1=0c_{1}=0

又由 a0=1a_{0}=1 ,得 c2=1c_{2}=1

因此, an=(1+4qp−12)na_{n}=(\frac{\sqrt{1+\frac{4q}{p}}-1}{2})^n

可以做一个小的对比:【盈亏比为2、胜率比为1】的情况(即,p=0.5,赢了资产+2,输了资产-1),与【盈亏比为1,胜率比为2的情况】的情况(即,p=2/3,赢了资产+1,输了资产-1),相比,哪个更容易破产?直觉来看,应该是一样的破产概率。但是实际上,盈亏比为2的情况,破产概率更大。比如从 1 元起步,前一种约为 61.8%,后一种是 50%。

为什么临界点是 13\frac{1}{3}?赢了拿 2 元,输了赔 1 元,所以一局平均赚 2p−(1−p)=3p−1。胜率要超过三分之一,这个数才是正的。不超过三分之一,只要一直玩下去,最终输光的概率就是 1。

对于更高的盈亏比的情况,破产概率会更难求。赢一局赚 b 元、输一局亏 1 元时,也能照样列出递推式,只是 b 越大,越不好手算。


回到期货。硬币游戏里,每局赢多少钱、输多少钱,都是提前说好的。真实交易里,每次赚亏多少不固定,还有手续费和保证金。这个模型想说明的是:哪怕一局平均能赚钱,本金也可能先扛不住。

比如在这个模型里,赢了拿 b 元,输了赔 1 元,一局平均赚 pb−q。胜率如果刚好是五成,就得赢一局赚的钱比输一局赔的钱多,平均下来才是赚的。只看胜率,是看不出这笔账的。

多说一句,赌徒输光定理的核心条件是“本金有限”。有些炒币平台有跟单功能。很多人想着跟着大佬做单,应该能赚钱。可赢的次数多,不代表每次赢的钱多。比如,比特币涨到 6 万美元时,有人能扛住 3 万美元开的空单;你跟着开同样的单,保证金不够,可能还没等他平仓,你就已经被强平了。

另外,我有一个朋友,每次花1000美元跟大佬的单,前后换了好几个大佬,最终都爆仓了。他问我,既然每次跟单都是最终输光的结局,为何不找个大佬,每次和他反着开单?这个问题想得很美好,但是经不起推敲:假设每笔仓位都一样,先不算手续费,正着做赚了一个本金,反着做就亏了一个本金。归根结底,仍然是“本金有限”的局限性在起作用。

我在《大白话概率论》里也用两个人分别拿 10 元和 90 元对赌的例子,讲了为什么每局都公平,本金少的人还是更容易先输光。

知乎原文

如何闭着眼睛操作一堆硬币,使得正面朝上的硬币数与反面朝上的硬币数相近?

首先看一道智力题:桌子上有7枚正面朝上的硬币和13枚反面朝上的硬币。闭着眼睛,但不允许通过触感识别硬币的正反面,如何将硬币分为两部分,使得每部分的正面向上的硬币数量相等?

答案是:从硬币堆中抽7个硬币出来,全部翻面,作为第二堆。

假设抽出来的7个硬币有x个正面朝上的硬币,那么有7-x个反面朝上的硬币。经过反转后,有7-x个正面朝上的硬币。同时,在另一堆也有7-x个正面朝上的硬币。

如果不知道初始有多少枚硬币正面朝上,有没有方法能让两堆硬币的正面朝上的硬币数接近一致呢?

如果只是想让两堆分到差不多一样多的正面,随机分成两半就行了:平均下来,两堆分到的正面数一样多。不过我更想试试另一件事:不重新抛,只靠翻面,能不能让整堆硬币的正反面各占一半左右?

我想到的方法是:每轮随机挑 k 枚硬币翻面,重复 n 轮。多翻几轮之后,正面数会不会跑到一半附近?

k对收敛速度有影响吗?

本着实验的精神(其实是我没有想到正儿八经的解法),我用代码做了一些实验。如下图所示。第一张图里,k 分别取 1、10、20、100、500。每种取法连续翻 100 轮,画成一条线。横轴是翻了多少轮,纵轴是这一轮翻完以后,有多少枚硬币正面朝上。正面朝上的初始硬币数是1,一共有1000枚硬币。可以看出,在k<=500时,收敛速度随着k的增大而增大。

原文配图

当k>500时的情况是怎么样的?下图是取k=500,950和990的结果。可以看出,当k>500时,k越大,收敛速度越慢。

原文配图

有趣的是,取k=10和990的结果对比,发现k=10时的收敛速度和k=990的收敛速度是一致的。翻 990 枚,等于先翻剩下那 10 枚,再把整堆翻过来。如果每轮都用这样的方式配对比较,翻完奇数轮,两边的正面数加起来是 1000;翻完偶数轮,两边的正面数一样。所以两种翻法虽然走的路线不一样,离 500 却一样远。

原文配图

为什么每轮翻 500 枚最快?假设现在有 x 枚正面。每枚硬币被翻到的机会都是 k/1000。于是,平均有 x×k/1000 枚从正面翻成反面,也有 (1000−x)×k/1000 枚从反面翻成正面。

这一减一加,算出来的平均正面数跟 500 的差,就是原来的差乘上 1−2k/1000。k=500 时,乘的数正好是 0,所以不管从哪里开始,翻一轮,平均正面数就是 500。k 大于 500 时,乘的是负数,平均值每轮都会换到 500 的另一边,这就是第二张图来回跳的原因。至于某一次到底翻出多少枚正面,看图也知道,还是会在 500 上下波动。


python3代码如下

import random
import matplotlib.pyplot as plt
plt.figure(figsize=(18,15)) 

total_coin = 1000 #硬币的总数量
int_positive = 1 #初始硬币正面朝上的数量
experiments = 100 #实验的次数

for every in [1,10,20,100,500]:  #every:每次实验翻转硬币的个数
# for every in [950,990]:   
    indexs = list(range(1,total_coin+1))
    ones = [1]*int_positive
    zeros = [0]*(total_coin-int_positive)
    numbers = ones + zeros
    random.shuffle(numbers)
    dic = {}
    for i in range(1, total_coin+1):
        dic[i] = numbers.pop()
    
    results = []    
    for _ in range(experiments):
        slices = random.sample(indexs, every)
        for i in slices:
            dic[i] = 1- dic[i]
        results.append(sum(dic.values()))
        # print(sum(dic.values()))
 
    plt.xlim(1,experiments)
    plt.ylim(0,1000)
    plt.xticks(list(range(0,experiments,int(experiments/10))))
    plt.yticks([0,400,500,600,1000])
    plt.grid(axis='y')
    plt.plot(list(range(1,experiments+1)),results,label=str(every))
plt.legend()

知乎原文

强强联合——交叉熵、卷积与排序不等式

交叉熵、图像卷积、排序不等式,似乎是三个截然不同的东西,但它们都有共同的核心原理,我称之为“强强联合”。

首先从排序不等式开始说起,排序不等式指的是:

当 a1≥a2≥……≥ana_{1}\geq a_{2}\geq……\geq a_{n} ,

b1≥b2≥……≥bnb_{1}\geq b_{2}\geq……\geq b_{n} ,

有∑i=1naibi\sum_{i=1}^{n}{a_ib_i}≥∑i=1naibj(i)≥∑i=1naibn+1−i\geq\sum_{i=1}^{n}{a_ib_{j(i)}}\geq\sum_{i=1}^{n}{a_ib_{n+1-i}},

其中j(1),j(2),...j(n)j(1),j(2),...j(n)是1,2,...n1,2,...n的一个置换。

简而言之,就是顺序和≥乱序和≥倒序和。大的数和大的数配对相乘,小的数和小的数配对相乘,这样的顺序和是最大的,体现强强联合的道理。


本文的交叉熵,指的是深度学习中的交叉熵损失函数。 p(xi)p(x_i) 是标签label, q(xi)q(x_i) 是模型的预测结果。

交叉熵H(p,q)=−∑i=1np(xi)log(q(xi))H(p,q)=-\sum_{i=1}^n p(x_i)log(q(x_i))

损失函数越小,意味着预测与真实标签越接近。此时 ∑i=1np(xi)log(q(xi))\sum_{i=1}^n p(x_i)log(q(x_i)) 应该越大。我们先不改模型给出的这几个概率,只换一换它们对应的类别。按照排序不等式,标签权重大的类别配上更大的预测概率,这样排,交叉熵最小。

所以交叉熵函数的核心,仍然可以理解为强强联合。


最后说卷积神经网络中的卷积,卷积层的输入是图像,图像的数值一般代表灰度值。比如灰度图的数值取值在[0,255]区间中,数值越高,代表颜色越白。

卷积本质上是提取特征的工具。比如下面的卷积核用来识别一个像素是否具有“左边周围的灰度值与右边周围的灰度值有较大差异”的模式或特征。看图里的数字:右边的灰度值加进去,左边的减掉,中间一行算两份。如果右边比左边亮,算出来就是正数;如果更亮的是左边,算出来就是负数。这里也能看出「强强联合」:同样是这几个像素,把亮的一边配给正数,结果就更大。

原文配图

同理,下面的卷积核用来识别一个像素是否具有“上面周围的灰度值与下面周围的灰度值有较大差异”的模式或特征。

原文配图

看结果是正是负,就知道哪边更亮;要看两边的亮度差得有多明显,就看这个数离 0 有多远。


此外,相关系数、协方差、余弦距离也能体现排序不等式的原理,在此不赘述。

知乎原文

Binary_Cross_Entropy的数值溢出问题

Binary_Cross_Entropy,下面简称BCE,是二分类问题中常见的损失函数,公式如下:

loss=−1n∑k=1n[yklog(pk)+(1−yk)log(1−pk)]loss=-\frac{1}{n}\sum_{k=1}^{n}{[y_{k}log(p_{k}) +(1-y_{k})log(1-p_{k})]}

其中 nn 是该batch的数据数量, kk 代表该batch的第 kk 个数据

yky_{k} 是真实的标签,取值一般是非0即1

pkp_{k} 是神经网络预测的值,网络的上一层输出 zkz_{k} 经过了sigmoid的激活得到 pkp_{k} , pkp_{k} 取值范围是(0,1)

即 pk=sigmoid(zk)=11+e−zkp_{k}=sigmoid(z_{k})=\frac{1}{1+e^{-z_{k}}}

损失函数又可以写成如下形式

loss=−1n∑k=1n[yklog(11+e−zk)+(1−yk)log(1−11+e−zk)]loss=-\frac{1}{n}\sum_{k=1}^{n}{[y_{k}log(\frac{1}{1+e^{-z_{k}}}) +(1-y_{k})log(1-\frac{1}{1+e^{-z_{k}}})]}

但如果先把 sigmoid 的结果算出来,再照着上式取对数,就可能遇到数值问题。

什么是数值溢出?简单来说,计算机能表示的数值范围是有限的,当计算出来的一个数超过了计算机能表示的范围,就发生了数值溢出。在深度学习中,指数、对数运算十分常见,尤其是指数运算,稍不注意,数据在指数的加持上,发生指数爆炸,造成数值溢出。

所以为了避免数值溢出,在使用log和exp的地方需要十分注意。

上面的loss函数既有除法,又有指数和对数运算,如果直接把数据丢进上面的loss函数运算,很可能会使得loss变成nan或者inf。

需要将loss变形:

loss=−1n∑k=1n[yklog(11+e−zk)+(1−yk)log(e−zk1+e−zk)]loss=-\frac{1}{n}\sum_{k=1}^{n}{[y_{k}log(\frac{1}{1+e^{-z_{k}}}) +(1-y_{k})log(\frac{e^{-z_{k}}}{1+e^{-z_{k}}})]}

=−1n∑k=1n[yklog(11+e−zk)−yklog(e−zk1+e−zk)+log(e−zk1+e−zk)]=-\frac{1}{n}\sum_{k=1}^{n}{[y_{k}log(\frac{1}{1+e^{-z_{k}}}) -y_{k}log(\frac{e^{-z_{k}}}{1+e^{-z_{k}}})+log(\frac{e^{-z_{k}}}{1+e^{-z_{k}}})]}

=−1n∑k=1n[yklog(1e−zk)+log(e−zk1+e−zk)]=-\frac{1}{n}\sum_{k=1}^{n}{[y_{k}log(\frac{1}{e^{-z_{k}}}) +log(\frac{e^{-z_{k}}}{1+e^{-z_{k}}})]}

=−1n∑k=1n[ykzk+log(e−zk1+e−zk)]=-\frac{1}{n}\sum_{k=1}^{n}{[y_{k}z_{k}+log(\frac{e^{-z_{k}}}{1+e^{-z_{k}}})]}

变形到这程度,式子简洁多了,但是当 zkz_{k} 是个很大的负数时,e−zke^{-z_{k}} 可能大到溢出;而当 zkz_{k} 是个很大的正数时,e−zke^{-z_{k}} 可能小到被算成 0,这时 log(e−zk)log(e^{-z_{k}}) 就是在对 0 取对数了。

为了防止风险,可以根据 zkz_{k} 的正负分两种情况。每个样本先按自己的正负选公式,算完再取平均。下面算的是单个样本的损失。

当 zk≥0z_{k}\geq0 的时候,

Lk=−[ykzk+log(e−zk)−log(1+e−zk)]L_k=-\left[y_k z_k+\log(e^{-z_k})-\log(1+e^{-z_k})\right]

=(1−yk)zk+log⁡(1+e−zk)=(1-y_k)z_k+\log(1+e^{-z_k})

这样既能保证指数运算 e−zke^{-z_{k}} 的值不会过大,又能保证不会出现log0的情况。

当 zk<0z_k<0 的时候,

Lk=−ykzk+log⁡(1+ezk)L_k=-y_k z_k+\log(1+e^{z_k})

这样也能保证指数运算 ezke^{z_{k}} 的值不会过大,避免了数值溢出问题。如果你用 PyTorch,可以直接用 BCEWithLogitsLoss,把没做 sigmoid 的原始分数传进去。


参考书目:《深度学习轻松学》, 冯超著

知乎原文

通过loss异常诊断网络问题——一个关于sigmoid的小案例

以前一直使用tensorflow2和pytorch炼丹。最近我需要将一个模型以uint8的数据格式部署在移动设备上,于是使用tensorflow1。该模型是一个图像分割模型,模型最后一层的激活层是sigmoid,损失函数是binary_cross_entropy。

开始训练后不久,训练的loss一直徘徊在0.69附近。我猜测是loss函数中的sigmoid出了问题。仔细研究后,我发现我用的 tf.losses.sigmoid_cross_entropy要的是没做 sigmoid 的原始分数,可我送进去的是模型已经做过一次 sigmoid 的输出。这样一来,算 loss 时就相当于又做了一次 sigmoid。

假设一个张量 pp ,经过网络最后一层的激活层sigmoid后, pp 变成 p1p1 ,此即网络模型的输出;再经过一次sigmoid后(在tf.losses.sigmoid_cross_entropy函数内部), p1p1 变成 p2p2 。

即 p1=σ(p)=11+e−pp_{1}=\sigma(p)=\frac{1}{1+e^{-p}}

p2=σ(p1)=11+e−p1p_{2}=\sigma(p_{1})=\frac{1}{1+e^{-p_{1}}}

原文配图


不难计算出, p1p1 的取值范围是(0,1), p2p2 的取值范围是(0.5, 0.731)。

假如 p2p2 的值无脑取0.5,那么不论标签值是1还是0,最终的loss都是0.6931。我把网络中的输出取出来看,发现 p2p2 的值大多都在0.5附近。

为什么 p2p2 的值?

我觉得是因为标签中0的数量远远大于1的数量。神经网络看到标签有这么多0,为了偷懒,干脆全部往0这边预测,于是让 p1p1 无脑取0,这样 p2p2 就很接近0.5,loss就稳定在了0.6931左右。

为了验证这个说法的可靠性,我用新的数据集做了一个实验。新的数据集中,标签为1的数量远远大于标签为0的数量。开始训练后不久,loss一直稳定在0.32左右。如果上面的说法成立,那么可以这样解释:神经网络看到标签中有很多1,于是让网络输出 p1p1 向1靠拢, p2p2 就应该尽可能接近0.731。经过计算,在这种情况下,loss应该是0.313。0.32和0.313很近,细微的区别可能是因为神经网络不至于完全偷懒,还是干了一点活的。

那为什么神经网络会偷懒呢?我又做了一些实验,发现只做一次 sigmoid,网络就能很好地收敛。模型最后一层可以不加 sigmoid,直接把原始分数送进刚才那个损失函数。会不会是网络取值范围太有限了? p2p2 的取值范围是(0.5, 0.731), 1−p21-p2 的取值范围是(0.269, 0.5),于是, p2p2 或者 1−p21-p2 取负对数后,loss的取值范围是(0.313, 1.313)。单看一个像素,标签是 0,loss 最低也只能接近 0.693;标签是 1,最低也只能接近 0.313。

这能解释为什么 loss 降不到更低。可模型一旦开始往多数那一边猜,为什么就很难再改过来?会不会是因为梯度太小了?我觉得评论区 王赟 Maigo 的评论很好地解释了偷懒现象:

如果网络有两层sigmoid,不难算出

dLdp=(p2−y)p1(1−p1)\frac{d L}{dp}=(p_{2}-y)p_{1}(1-p_{1})

=(p2−y)σ(p)(1−σ(p))=(p_{2}-y)\sigma(p)(1-\sigma(p))

其中y是标签值,取值非0即1

回顾binary_cross_entropy损失函数是:

L=−[ylog(p̃)+(1−y)log(1−p̃)]L=-\left[y\log(\tilde p)+(1-y)\log(1-\tilde p)\right]

y是标签值, p̃\tilde{p} 是预测值, pp 是未经过sigmoid的输出

如果只有一层sigmoid,那么 p̃=σ(p)\tilde{p}=\sigma(p)

在正常的情况中(即只有一层sigmoid的情况),模型如果要训练得很好,那么 p̃\tilde{p} 应该要非常接近0或者非常接近1,这样才能起到明显区分正负样本的作用。要让 p̃\tilde{p}非常接近0或者非常接近1,那么 pp 就应该非常大或者非常小。

如果有两层sigmoid,那么 p̃=σ(σ(p))\tilde{p}=\sigma(\sigma(p))。对于标签是 0 的像素,模型想把最后的预测压低,就得把第一层的输出往 0 推;标签是 1,就往 1 推。当 σ(p)\sigma(p) 接近0或者接近1时, σ(p)(1−σ(p))\sigma(p)(1-\sigma(p)) 会接近0,因此 dLdp\frac{d L}{dp} 会非常小。这样一来,模型一旦开始往多数那一边猜,想把那些猜错的像素调回来,就慢了。

知乎原文

混叠效应——眼见不一定为实

动图中的直升机明明在空中飞行,为什么螺旋桨看起来没有在动?

原文配图

动图中的汽车明明在向前走,为什么车轮看起来一会向前,一会向后,一会又停止?

原文配图

这都是混叠效应在起作用。举个简单的例子说明:

下面的动图显示了一个指针,看起来正在顺时针旋转,旋转速度为1秒每圈。

原文配图

指针的运动可以分解为8个阶段,分别对应着下面的8张图。从一个阶段到下一个阶段的用时为0.125秒(即 18\frac{1}{8} )。事实上,上面的动图就是由下面的8张图片拼接成的,每两张图片的间隔为0.125秒。

原文配图

假如我有一台摄像机,每隔 0.125 秒拍一张,也就是每秒拍 8 张。那么毫无疑问摄像机拍下来的前8张图和上面的8张图一模一样:

原文配图

拍下来的视频效果也和原视频一模一样:

原文配图

假如摄像机每0.25秒拍下一张图,那么拍下来的前4张图片就是这样:

原文配图

拍下来的视频效果就是这样:

原文配图

看起来没有原视频那样流畅,但还是像在顺时针转。

假如摄像机每0.5秒拍下一张图,那么拍下来的前4张图片就是这样:

原文配图

拍下来的视频效果就是这样:

原文配图

这几张图里,指针一会儿朝上,一会儿朝下,看起来像在上下摆。至于两帧之间到底往哪边转,摄像机没拍到。

假如摄像机每1秒拍下一张图,和指针运动一圈的用时相同,那么拍下来的前4张图片就是这样:

原文配图

拍下来的视频效果就是静止的:

原文配图

更有趣的来了:假如摄像机每0.875秒拍下一张图,呈现在眼前的效果会是怎样呢?

注意到 0.875=780.875=\frac{7}{8}

所以拍摄第一张图时,指针顺时针绕了 78\frac{7}{8} 圈:

原文配图

第二张图在第一张图的基础上又顺时针绕了 78\frac{7}{8} 圈:

原文配图

同理,第三张图、第四张图是这样:

原文配图

第五张图到第八张图是这样:

原文配图

整合下来,摄像机拍下来的效果是这样:

原文配图

物体本来看起来在顺时针旋转,而在精心设计的采样频率下,映入眼帘的物体却看起来在逆时针旋转!顺时针绕八分之七圈,和逆时针转八分之一圈,拍出来的位置一模一样。中间那一大段没拍到,连起来就像在倒着转。

眼见,也不一定为实。