深井望螺、奇奇谑戏与忍恭止竜

I.《慎独削砌儒门:给予蟒蛇的萝莉与工具》相关

II.感知机

感知机输入多个信号,输出一个信号。一个最基础的感知机由权重和阈值两部分组成:若输入关于权重的线性组合高于阈值则输出 1,此时神经元被认为激活;否则输出 0,则神经元未被激活。与门、或门都可以被看作基础的感知机。

感知机的一种数学描述是,有权重向量 w 和偏置 b,对于输入向量 x,使用 [b+wx>0] 作为感知机输出。

感知机有其局限性,例如其仅能将线性空间二分割,故异或门无法实现。

单层感知机无法描述异或门,但是通过多层感知机的叠加,异或门即可得以实现。

使用 sigmoid 函数(下文中出现)的双层感知机是图灵机(?)

III.神经网络

神经网络最基础的分层,是输入层、中间层、输出层三层,层标号自 0 开始。

偏置 b 也可以被看作是一个恒一神经元以 b 的权重对感知机的输入。非首层神经元上有“激活函数”,对输入的组合进行转换:例如,使用 h(x)=[x>0] 的阶跃函数时,即得朴素感知机。

sigmoid 函数 h(x)=11+ex 也是常见的激活函数。其是一个具有良好平滑性的函数。

神经网络的激活函数必然都是非线性函数,否则加深神经网络的层数就没有影响了。

最近大家比较喜欢 ReLU 函数 h(x)=max(x,0)

W(i)i1 层至 i 层的转移,则其是线性变换,可以用矩阵描述;令 x(i)i 层值向量(用行向量描述),b(i)i 层偏置,则神经网络基础公式即为:

x(i)=h(x(i1)W(i)+b(i))

隐藏层的激活函数约定俗成地记作 h(x),而输出层为统一也有其激活函数,记作 σ(x)。输出层的激活函数设计与神经网络待解决的问题相关:分类问题(对输入离散地归类)往往用 softmax 函数 σ(x)=exp(x)exp(x)1,而回归问题(用输入预测连续输出)往往用恒等函数。

计算 exp 有可能爆精度。通过上下同乘 C 加以处理,最终可以得到修正的 softmax σ(x)=exp(x+C1)exp(x+C1)1,使用 Cx 即可。

softmax 的输出总是可以被解释为一组概率密度分布。因为 softmax 有一定计算量,softmax 有可能被换为朴素的 argmax。分类问题中,输出层的神经元数目常常即为类别数目。

将多个向量并列拼成矩阵,可以缩短处理时间,被称作批处理。

IV.神经网络的学习

神经网络不重要,重要的神经网络的参数。参数没法人脑拟合,要靠算法自己学习。

方法一是靠人脑提取某些特征量,然后针对特征量跑机器学习。方法二是直接把数据丢到神经网络(深度学习)上跑参数。

损失函数是用于量化神经网络性能的函数。例如,均方误差 12(tiyi)2,其中 ti 是监督数据,yi 是输出数据;在分类问题中,softmax 函数的输出是概率密度向量,监督数据使用 one-hot 表示,即除了关键位为 1 外其余位置都是 0。例如,交叉熵误差 E=tilnyi。加上 epsilon 可以避免出现负无穷。

单个数据的损失函数按照上述方式计算;总数据集损失函数用所有单个函数的损失函数求平均得到。

使用损失函数而非正确预测数目,原因是正确预测数目的梯度在很多地方为零,不好优化。

同理,使用 sigmoid 函数而非阶跃函数,因为阶跃函数几乎处处导数为零。

计算机算导数更喜欢用数值微分,即 f(x+h)f(x)h(称为前向微分)或更常见的 f(x+h)f(xh)2h(称作中心微分)

梯度法使用梯度,沿着负梯度方向走,寻找损失函数最小化。

走多远?往往有一个参数,是 102103,然后乘以负梯度并移动。

神经网络的梯度是把 W 当成向量处理。

流程:

  • 从完整数据集中选出一小部分作为训练数据,剩下的作为测试数据。
  • 用正态分布初始化 W,用 0 初始化 b
  • 跑。
  • 适当调参避免过拟合。

V.误差反向传播法

考虑用一个内向树描述某种计算流程:内向树上每个点接受若干点的输入,并通过某种运算变成输出供给一个点。

现在我们想知道内向树上每个输入对最终输出的贡献。因为是内向树,则终点只有一个方向可以溯源至我们好奇的变量,于是计算终点在其它东西都固定时的偏导,然后回退一步,再计算一步偏导,再回退……将所有的单步偏导求积,即得快速的总梯度计算方法。

单步偏导,如果将总流程描述为若干步加法与乘法的组合(此时采用 sigmoid 函数或 ReLU 函数均可),则每一步都可以算得导数的解析解。

花了好几天一边摆一边对着 std 抄,捣鼓出了一份能在约 20s 内训练出成功率在 98% 附近的神经网络。

mnist.py

# 这份代码是直接从这本书上附的代码集中粘出来的,唯一的作用是下载 mnist 数据集并保存在 mnist.pkl 文件中。但是 mnist 网站好像下不下来数据,因此我选择手动下了一份数据集
try:
    import urllib.request
except ImportError:
    raise ImportError('You should use Python 3.x')
import os.path
import gzip
import pickle
import os
import numpy as np


url_base = 'http://yann.lecun.com/exdb/mnist/'
key_file = {
    'train_img':'train-images-idx3-ubyte.gz',
    'train_label':'train-labels-idx1-ubyte.gz',
    'test_img':'t10k-images-idx3-ubyte.gz',
    'test_label':'t10k-labels-idx1-ubyte.gz'
}

dataset_dir = os.path.dirname(os.path.abspath(__file__))
save_file = dataset_dir + "/mnist.pkl"

train_num = 60000
test_num = 10000
img_dim = (1, 28, 28)
img_size = 784


def _download(file_name):
    file_path = dataset_dir + "/" + file_name
    
    if os.path.exists(file_path):
        return

    print("Downloading " + file_name + " ... ")
    urllib.request.urlretrieve(url_base + file_name, file_path)
    print("Done")
    
def download_mnist():
    for v in key_file.values():
       _download(v)
        
def _load_label(file_name):
    file_path = dataset_dir + "/" + file_name
    
    print("Converting " + file_name + " to NumPy Array ...")
    with gzip.open(file_path, 'rb') as f:
            labels = np.frombuffer(f.read(), np.uint8, offset=8)
    print("Done")
    
    return labels

def _load_img(file_name):
    file_path = dataset_dir + "/" + file_name
    
    print("Converting " + file_name + " to NumPy Array ...")    
    with gzip.open(file_path, 'rb') as f:
            data = np.frombuffer(f.read(), np.uint8, offset=16)
    data = data.reshape(-1, img_size)
    print("Done")
    
    return data
    
def _convert_numpy():
    dataset = {}
    dataset['train_img'] =  _load_img(key_file['train_img'])
    dataset['train_label'] = _load_label(key_file['train_label'])    
    dataset['test_img'] = _load_img(key_file['test_img'])
    dataset['test_label'] = _load_label(key_file['test_label'])
    
    return dataset

def init_mnist():
    # download_mnist()
    dataset = _convert_numpy()
    print("Creating pickle file ...")
    with open(save_file, 'wb') as f:
        pickle.dump(dataset, f, -1)
    print("Done!")

def _change_one_hot_label(X):
    T = np.zeros((X.size, 10))
    for idx, row in enumerate(T):
        row[X[idx]] = 1
        
    return T
    

def load_mnist(normalize=True, flatten=True, one_hot_label=False):
    """读入MNIST数据集
    
    Parameters
    ----------
    normalize : 将图像的像素值正规化为0.0~1.0
    one_hot_label : 
        one_hot_label为True的情况下,标签作为one-hot数组返回
        one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组
    flatten : 是否将图像展开为一维数组
    
    Returns
    -------
    (训练图像, 训练标签), (测试图像, 测试标签)
    """
    if not os.path.exists(save_file):
        init_mnist()
        
    with open(save_file, 'rb') as f:
        dataset = pickle.load(f)
    
    if normalize:
        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].astype(np.float32)
            dataset[key] /= 255.0
            
    if one_hot_label:
        dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
        dataset['test_label'] = _change_one_hot_label(dataset['test_label'])
    
    if not flatten:
         for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].reshape(-1, 1, 28, 28)

    return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label']) 


if __name__ == '__main__':
    init_mnist()

Functions.py

import numpy as np
def Identity_Function(x):
	return x
def Softmax_Function(x):
	if x.ndim==2: # case when x is batched
		x=np.exp(x-np.max(x,axis=1).reshape(-1,1))
		return x/np.sum(x,axis=1).reshape(-1,1)
	x=np.exp(x-np.max(x))
	return x/np.sum(x)
def Sigmoid(x):
	return 1.0/(1.0+np.exp(x))
def Sigmoid_Grad(x):
	return (1.0-Sigmoid(x))*Sigmoid(x)
def ReLU(x):
	return np.max(x,0)
def ReLU_Grad(x):
	grad=np.zeros(x)
	grad[x>=0]=1
	return grad
def Mean_Squared_Error(y,t):
	return 0.5*np.sum((y-t)**2)
def Cross_Entropy_Error(y,t):
	if y.ndim==1:
		y=y.reshape(1,-1)
		t=t.reshape(1,-1)
	batchsize=len(y)
	# print("Fuctions:",y.shape,t.shape)
	# print(y[0],t[0],np.log(y[np.arange(batchsize),t]+1e-7)[0])
	return -np.sum(np.log(y[np.arange(batchsize),t]+1e-7))/batchsize

Layers.py

import numpy as np
from Functions import *
class ReLULayer:
	def __init__(self):
		self.neg=None
	def forward(self,ip):
		self.neg=ip<=0
		op=ip.copy()
		op[self.neg]=0
		return op
	def backward(self,ip):
		op=ip.copy()
		op[self.neg]=0
		return op
class SigmoidLayer:
	def __init__(self):
		self.sig=None
	def forward(self,ip):
		op=Sigmoid(ip)
		self.sig=op.copy()
		return op
	def backward(self,ip):
		op=ip*(1.0-self.sig)*self.sig
		return op
class AffineLayer:
	def __init__(self,W,b):
		self.W=W
		self.b=b
		self.dW=None
		self.db=None
		self.x=None
	def forward(self,ip):
		self.x=ip.copy()
		# print(self.x.shape,self.W.shape,self.b.shape)
		return np.dot(ip,self.W)+self.b
	def backward(self,ip):
		tag=False
		if ip.ndim==1:
			tag=True
			ip=ip.reshape(1,-1)
		self.dW=np.dot(self.x.T,ip)
		self.db=np.sum(ip,axis=0)
		op=np.dot(ip,self.W.T)
		if tag:
			op=op.flatten()
		return op
class SoftmaxLayer:
	def __init__(self):
		self.y=None
		self.t=None
	def forward(self,ip,tp):
		self.y=Softmax_Function(ip)
		self.t=tp
		op=Cross_Entropy_Error(self.y,self.t)
		return op
	def backward(self,ip=1):
		batchsize=len(self.t)
		op=self.y.copy()
		op[np.arange(batchsize),self.t]-=1
		op/=batchsize
		return op

NeuroNetwork.py

import numpy as np
from Layers import *

class NeuroNetwork:
	def __init__(self,layertypes,layersizes,weight_init_std=0.01,givenW=None,givenb=None):
		self.n=len(layersizes)
		self.W=[]
		self.b=[]
		self.layers=[]
		for i in range(self.n-1):
			if givenW is None and givenb is None:
				self.W.append(np.random.randn(layersizes[i],layersizes[i+1])*weight_init_std)
				self.b.append(np.zeros(layersizes[i+1]))
			else:
				self.W.append(givenW[i])
				self.b.append(givenb[i])
			self.layers.append(AffineLayer(self.W[i],self.b[i]))
			if i+1!=self.n-1:
				self.layers.append(layertypes[i]())
		self.lastlayer=layertypes[-1]()
	def predict(self,x):
		for i in self.layers:
			x=i.forward(x)
		return x
	def InterpretPrediction(self,x,num=3):
		y=self.predict(x)
		y=Softmax_Function(y)
		arr=[]
		for i in range(len(y)):
			arr.append((y[i],i))
		arr=reversed(sorted(arr))
		print("Prediction|",end='')
		for i in range(num):
			print(int(i[0]*100),"%%being result ",i[1],sep='',end='')
		print()
	def calcloss(self,x,t):
		y=self.predict(x)
		return self.lastlayer.forward(y,t)
	def accuracy(self,x,t): #in this case x must be batched
		y=self.predict(x)
		y=np.argmax(y,axis=1)
		return 1.0*np.sum(y==t)/len(x)
	def gradient(self,x,t):
		self.calcloss(x,t)
		ip=1
		ip=self.lastlayer.backward(ip)
		for i in reversed(self.layers):
			ip=i.backward(ip)
		gradW=[]
		gradb=[]
		for i in range(0,self.n-1):
			gradW.append(self.layers[i*2].dW)
			gradb.append(self.layers[i*2].db)
		return gradW,gradb

NeuroTrain.py

import numpy as np
from mnist import load_mnist
from NeuroNetwork import *

nwk=NeuroNetwork(layertypes=[ReLULayer,SoftmaxLayer],layersizes=[784,50,10])
(x_train,t_train),(x_test,t_test) = load_mnist(normalize=True, one_hot_label=False)
itertime=10000
batchsize=100
trainsize=len(x_train)
learningrate=0.1
reviewsize=max(1,int(trainsize/batchsize))

batchsucceedrate=[]
trainsucceedrate=[]
batchx=[]
trainx=[]
print(trainsize,batchsize,reviewsize)
for i in range(itertime):
	ind=np.random.choice(trainsize,batchsize)
	x_batch=x_train[ind]
	t_batch=t_train[ind]
	batchsucceedrate.append(nwk.accuracy(x_batch,t_batch))
	batchx.append(i)
	gradW,gradb=nwk.gradient(x_batch,t_batch)
	for j in range(0,nwk.n-1):
		nwk.W[j]-=learningrate*gradW[j]
		nwk.b[j]-=learningrate*gradb[j]
	# print(i,i%reviewsize)
	if i%reviewsize==0:
		trainsucceedrate.append(nwk.accuracy(x_train,t_train))
		print(trainsucceedrate[-1])
		trainx.append(i)

for i in nwk.W:
	print(i.shape)

import pickle

with open("neuroW.pkl","wb") as f:
	pickle.dump(nwk.W,f)
with open("neurob.pkl","wb") as g:
	pickle.dump(nwk.b,g)

import matplotlib.pyplot as plt
plt.plot(batchx,batchsucceedrate,label="batchsuccessrate")
plt.plot(trainx,trainsucceedrate,label="trainsuccessrate")
plt.legend()
plt.show()

NeuroTest.py

import pickle
import numpy as np
from mnist import load_mnist
from NeuroNetwork import *

with open("neuroW.pkl","rb") as f:
    W=pickle.load(f)
with open("neurob.pkl","rb") as g:
    b=pickle.load(g)
    
nwk=NeuroNetwork(layertypes=[ReLULayer,SoftmaxLayer],layersizes=[784,50,10],givenW=W,givenb=b)

(x_train,t_train),(x_test,t_test) = load_mnist(normalize=True, one_hot_label=False)

testsize=len(x_test)
testbatch=100


ind=np.random.choice(testsize,testbatch)
x_batch=x_test[ind]
t_batch=t_test[ind]

print(nwk.accuracy(x_batch,t_batch))

VI.与学习相关的技巧

减去某个比例修饰后的负梯度被称作 SGD 法。

动量法将状态视作在空间中移动的一个点,其具有一个速度;梯度不被用来修改位移,而是被当作冲量,以修改速度。当然,速度本身需要一些衰减,于是有公式

v=αvηww=w+v

其中 α 是衰减,往往取 0.9η102103 之类。

另一种称作学习率衰减的方法类似模拟退火,会随着时间调小学习率 η。AdaGrad 继承了这一想法,对 w 的所有分量定制一份学习率,相当于把学习率 η 变成学习率向量 H,然后算 Hw 更新。( 是等长向量的按位乘法,似乎没有好听的名字?)让 H 动感起来,得到如下的公式:

h=h+www=wη1hw

w 较大的那些分量会对 h 产生较大位移,反过来在 w 的更新中这种较大位移的影响被抹除。有时需要为 h 加上一个 ϵ 以避免零作除数。

然而对过去时刻的全体 w 的影响忠实地记录在 h 上最终会导致 1h 趋于 0,因此有时需要选择性地遗忘过去的 h,具体而言是 h=αh+ww,称作 RMSProp 方法。

除此之外还有 Adam 方法,但是文中并没有详细描述。


除了调整参数更新的方法外,调整参数初值也是可行的改变神经网络结构的方式。

权重不能被初始化为零,进一步,不能被初始化为相同值,否则每层中所有神经元就将始终保有相同的值了。

对随机的初值,其经过标准高斯分布的 W 和 Sigmoid 函数修饰后,随着层数的加深,值会逐渐趋向 01 分布。与此同时,趋近 01 的值会导致趋向 0 的导数,进而导致出现梯度消失的问题,也即多层的影响下,最终每个参数的孤立变化无法对最终结果造成大的偏移,换言之这种情况下值域空间过于平坦,处处梯度均趋于零。与此同时标准差 0.01W 会导致输出趋于 0.5,这在我们之前的分析中,也是不优的。一个较优的赋以初值的方法被称作 Xavier 初始值,即:若前一层点数为 n,则本层使用标准差为 1n 的分布(虽然这种方法其实应该算是简化 Xavier 初始值罢了)。Xavier 初始值与 tanh 作为激活函数时会比较适配。

相反,ReLU 使用的初始值被称作 He 初始值,是标准差为 2n 的分布。


Batch Norm 方法是从另一个角度对神经网络结构的优化,即在 Affine 层与激活函数之间(或是激活函数之后——有针对其的细致分析)插入一个 Batch Norm 层。假如 batch 大小为 m,且某个神经元上的取值为 {x1,,xm},则对其正规化为均值为零、方差为一的分布,即 x^i=xix¯σ2+ϵ。Batch Norm 方法的一个问题是反向传播计算会非常繁琐(但并非不可实现)。与此同时,Batch Norm 将会:

  • 减少所需迭代次数。
  • 提高对权重初值的鲁棒性。
  • 抑制过拟合。

过拟合是神经网络避免的问题。过拟合往往发生于如下场合:

  • 对小样本集迭代过多次。
  • 参数过多。

对过大的参数加以惩罚可以遏制过拟合。具体而言,对损失函数加诸 12λw2,其中 λ 是控制正则化强度的超参数,越大即越遏制过拟合。这样,导数会增加 λw

虽然其可以遏制过拟合,但是不代表对训练数据的拟合效果不变:过拟合的削弱可能是以训练数据拟合同步削弱为代价的,换言之模型效果可能完全没有变化。

Dropout 方法在训练的流程中随机删除神经元:也即,在每次训练中,在每层中一次性地让某些神经元变得惰性,不再向前或向后传递信息,并在下一轮训练中恢复。

Dropout 方法本质上是一种集成学习,即让多个模型同步学习并取结果的平均值。因此,Dropout 在推理时,要在 Dropout 层乘以 (1ϵ),其中 ϵ 是 Dropout 比率,来起到求取平均值的效果。


如何调参?

调小跑神经网络的轮数后,以对数为指标随机撒点,缩小范围后进一步分析。

VII.卷积神经网络

Affine 层是“全连接层”,意味着其将二维的图像拉平为一维的向量,而忽略了图像本身的协调性。

CNN 引入了以 Convolution 和 Pooling 为例的非全连接层,使得图像本身的三维(长、宽、通道(RGB))变得有意义。

定义:将卷积层的输入输出数据称作特征图。

两个二维矩阵的卷积被定义为:以其中 (N,M) 的矩阵为输入数据,(n,m) 的矩阵为“滤波器”,满足 nN,mM,则令滤波器扫过整个矩阵,对于每个扫过的位置作点积,最终得到 (Nn+1,Mm+1) 的矩阵即为卷积结果。卷积运算同样可以有相关的偏置,区别是卷积的偏置往往被认为关于所有位置相同,即加到所有位置上。

输入数据有时需要填充,即在周边填上一圈或若干圈零,“幅度”被用来称呼填入零的圈数。

卷积可以不扫过每个位置,而是以某种“步幅”扫过。

幅度与步幅都是用于调整输出大小的参数。

卷积同样可以被定义于三维和更高维的张量。但是在图像处理的场合,可以让图像和滤波器的通道数目相同,这样卷积结果就仍是二维矩阵。

通过增加滤波器的数目,卷积层的输出可以在通道方向也拥有不止一维。

(C,H,W):通道数、高度、长度,以此种顺序描述三维数据。滤波器在最前面加一维 O 表示滤波器数目。此时偏置是 (O,1,1) 的向量(?)。批处理要在最前面又多加一维 N


池化(Pooling)是一种缩小矩阵的方法,用窗口扫过大矩阵,并进行如 Max 池化、Average 池化等操作。池化的场合往往步幅被设置为与窗口大小相同。


im2col 函数可以较好实现卷积操作:其将任意维数的输入摊成二维,其中第一维是展开的图像位置,第二维是展开的待卷积的位置。该操作以牺牲空间为代价,让矩阵乘法等模块化操作得以应用,进而优化了效率。


CNN 网络的层组合大约有如下的模式:

  • 若干次 Convolution-Affine-Pooling 循环。
  • 若干次 Convolution-Affine 循环。
  • 若干次 Affine-ReLU 循环。
  • 最后使用 Affine-Softmax 输出。

与全连接层不同,滤波器可以可视化,故可以通过肉眼观察学习后的滤波器表现出的模式判别滤波器在识别什么东西。例如,有明显黑白分界线的滤波器,识别的是通过分界线的线条;团块状的滤波器则会识别斑块信息。

多层 CNN 网络,每层则会展现出不同倾向。

VIII.深度学习

讲了一些神秘的东西,例如多层 CNN 事实上是在用滤波器观察一个更大的范围。例如可以并行多个不同大小的滤波器并合并,让网络具有横向的广度。例如可以建立某些捷径,使得加深层数不会使得梯度过度衰减。

卷积是可并行运算;处理图像的 GPU 同样擅长并行运算,因此尝试在 GPU 上运行卷积可以提高训练和识别速度。

神经网络对于输入的小扰动有鲁棒性。这意味着神经网络对精度需求其实几乎不敏感,使用 16 位浮点数甚至往往就足够了。

II.ImageNet Classification with Deep Convolutional Neural Networks 阅读笔记

使用了 ReLU 而非 Sigmoid 或 tanh

把训练拆成两半放到两个 GPU 上跑。

使用“局部归一化”技巧,将相邻的多个通道的结果按照某种法则“平均”,或许可以遏制某些层上的过度出格。

池化的时候让 Pool 间适度重叠(令边长大于步幅)可以减少过拟合。

数据集相对参数数目(六千万)还是过小。对输入进行裁剪、翻转扩充数据集,在预测时采取四角数据与中心数据及其翻转共十个数据并求均值加以预测。

然后可以用 PCA 提取光照信息:对于 (3,256,256) 的图像,我们不在意图像本身顺序,单纯把它看作 (2562,3) 的矩阵。这个矩阵每列减去该列的均值,将矩阵仿射变换到原点然后跑 PCA(事实上仅仅是求 3×3 矩阵的特征值),然后对像素增加 [v1,v2,v3][a1λ1,a2λ2,a3λ3]T 的值,其中 a 是 Gauss 分布采样的噪声。

结合了 Dropout 方法。

探索新参数的方式是 momentum 法加以少量魔改。

一个很好的方法是,当当前学习率不再产生效果时,即将当前版本的参数保存,调小学习率进行精度更高的学习,相当于人工模拟退火。

本文是神经网络开山之作,因此基本上所有的 idea 都已在上文出现,这是因为它们因在本文被提出而得以跻身重要方法。

III.deep residual learning for image recognition 阅读笔记

归一化可以处理层数较多时梯度弥散的方式,但是正如前文所说,层数过多时效果反倒不尽人意(因为更多的参数需要更多时间训练,训练充分久以后的结果或许会更优,但是在相同训练时间/轮数的前提下不牛)

因此本文提出了 shortcut 方法,即跳过数层额外给予一份输出的方法。如果层数较小时反而较优,则被跳过层数对应的参数最终会趋于零,弥补了这一缺憾。

代码实现可以将若干层打包为块(一个块其实也是一种特殊的集成化的层)然后在块首和块尾实现跳过方案即可。

一个例子是 (Affine-ReLU-Affine)-ReLU 结构,其中括号的部分建构了一个 shortcut。特别地,shortcut 也不一定只能写 x=σ(xW1)W2+x,其中后面的 x 是 shortcut,换成 x=σ(xW1)W2+Wx 也是可行的,只不过大部分时候将 W 使用恒等映射就行了(前提是非 shortcut 部分保证映射前后节点数目相同。验证后发现,引入 W 处理那些升维的场合,这种策略优于仅用恒等映射 shortcut 策略处理维度不发生变化的场合,但劣于无论任何场合都使用 W shortcut 的场合;并没有特别劣,因此为了训练速度还是仅在升维时使用 W)。特别地,shortcut 必须跳过至少两层,如果仅跳过一层使用 x=W1x+x,其实效果并不会变好,这是显而易见的。

IV. NLP 入门

NLP 其实是一个朴素的概率模型,即生成一个单词序列,每次根据所有已知信息生成下一个单词。

但是要想把单词转成神经网络的输出,就需要支持把文本转化为向量,被称为词向量的手法。

倘若词向量的问题被解决,还有一个问题就是,目前为止已介绍的神经网络都是前馈的,也即输入数据到达输出层后不会反馈;这就引出了下一个发明,即循环神经网络 RNN,每一时刻接受当前时刻的输入与上一时刻的输出为输入。但是朴素的 RNN 模型存在记忆消失的问题,即过久的输入对模型的干涉力度会消散。这些问题在 LSTM 或 GRU 等模型中得到了改善。

Seq2Seq 模型是一个主要被应用于翻译的模型。将输入文本预处理成为词向量,编码器将其转化为集成了全部文本信息的单个向量,再扔到对应的解码器中转化为目标语言的词向量。

一个比 RNN 模型更好的模型是 Transformer 模型。RNN 模型显然是几乎没有并行计算的潜力的,而 Transformer 模型恰恰具有着这一能力。

V.《潜水员血洗神都》相关

抄了 xcyle 的博客 喵。

I.词嵌入

可以建立从单词到整数的一一映射,但是这个映射其实没有任何意义。但是词汇之间存在相似关系,因此如果将单词映到某空间中的向量,并使用向量的相似程度衡量词汇关系,则这或许是一个比较牛的方案。

跳字模型针对一个词生成其周围可能出现的词,也即给定一个窗口大小 h,调查 Pr(xjxi),j[ih,i+j]{i}。倘若认为窗口中每个词相对独立,则这个矩阵相对 xi 的信息就是全体 xj 对应效果乘积。

显然,当一个词作为 xixj 出现时,地位是不相同的。因此一个词有其中心词向量 v 与背景词向量 u。令词典为 V,则我们认为

Pr(xjxi)=exp(ujvi)exp(ukvi)

即一个 softmax 的形式。

跳字模型的似然度可以由 0<|ij|hPr(xjxi) 描述。最大化似然度等效于最小化损失函数

0<|ij|hlnPr(xjxi)

分析可知该式关于 vi 的梯度,然后跑神经网络即可。跑完后,用中心词向量 vi 作为表征向量。

连续词袋模型则是类似地用周围词生成中心词的方法。与跳字模型相反,vi 指背景而 ui 指中心,此时

Pr(xix[ih,i+h])=exp(12muiv[ih,i+h])kVexp(12mukv[kh,k+h])

Wi 表示 i 周边的集合,v¯i 表示 12mjWivj,则上式等价于

Pr(xiWi)=exp(uiv¯i)kVexp(ukv¯k)

同理可得最小化 Pr(xiWi)。连续词袋模型中采取背景词向量 vi 表征。

II.近似训练

条件概率中使用了 softmax,这意味着每次调用都要遍历全体字典,而这显然是不牛的。

负采样方法通过某种手法修改了损失函数使得其可以被抽样拟合。

层序 softmax 方法用二叉树结构将字典中所有词汇存储于叶子,为二叉树上每个点设置背景词向量然后通过某种很牛的方式拟合,最终只需二叉树深度次取样即可实现目标。

III. RNN

一个最朴素的神经网络是 H=σ(xW+b)

RNN 的思想是将上一时刻的输出也输入,即 Ht=σ(xt1Wxh+Ht1Whh+bh)

层数过多的 RNN 容易出现梯度弥散或梯度爆炸的问题。解决方法之一是门控循环单元 GRU,其引入了重置门 R 与更新门 Z,用二者结合实现 H。二者均使用独立的一套参数按照与 H 一致的方式计算,即

[Rt,Zt]=σ(xt1Wx,rz+Ht1Wh,tz+b)

其中 σ 是 sigmoid 函数。然后计算候选隐藏状态

H~t=tanh(xtWxh+(RtHt1)Whh+bh)

其中 是按位点积。

然后令输出

Ht=ZtHt1+(1Zt)H~t

Rt 衡量忘却,Rt 某些位置调小意味着忘记;Zt 衡量继承,Zt 某些位置调大意味着继承。

长短期记忆 LSTM 模型更为抽象。还是老配方使用 Sigmoid 函数计算输入门 It、遗忘门 Ft 与输出门 Ot,使用 tanh 函数计算候选记忆细胞 C~t。计算记忆细胞 Ct=FtCt1+ItC~t,计算隐藏状态 Ht=OttanhCt

IV. GloVe

GloVe 指的是“全局向量的词嵌入”。

在语料库中,一个单词显然有可能多次出现,并伴随着不同的上下文;对于单词 wi,记对应的 Ci 为上下文中出现全体单词构成的可重集。记 qi,j=Pr(wjwi),xi,j=#wjCi,然后在跳字模型中,损失函数可以表达为

iVjVxi,jlnqi,j

|Ci|=Ci,令 pi,j=xi,jCi,则损失函数亦可写成

iVCijVpi,jlnqi,j

后一个 其实是在算交叉熵误差,但有时交叉熵误差并不好,例如出现次数极少的某些单词会带来大量的误差。换成改变后的平方误差

iVjVh(xi,j)(ujvi+bi+cjlogxi,j)2

其中 h(x) 选用某个值域 [0,1] 的单增函数;bi,cj 是误差项。

因为背景词与中心词是相对的,所以事实上有 xi,j=xj,i,因此中心词向量和背景词向量在 GloVe 中是等价的,虽然在训练过程中可能二者训练出了不一样的结果,因此最终选用二者和作为表征向量。

原因:我们希望构建函数 f(uj,uk,vi)pi,jpi,k。首先假设其可以被写成标量函数 f((ujuk)vi),然后交换 j,k 可知 f(x)=f(x),于是选取 f=exp,则 f(uj,uk,vi)=exp(ujvi)exp(ukvi)pi,jpi,k。此时假设 exp(ujvi)αpi,j 其中 α 是对全体 i,j 相同的参数,两边取 ln 并拟合后得到 ujvi+bi+cjlogxi,j 的效果。

V.注意力

人眼输入量规模是巨大的;每个输入被称作一个键 key,并且通过某种方式与值 value 配对。注意力汇聚的过程依赖于一次查询,即主动将注意力集中于某个键的过程。

现在应用于模型。例如,一个长句子的翻译常常仅由几个关键位置决定,这就与上文的注意力机制相关。

对于每个时刻 t 分配一个背景信息 ct。为计算之,每个时刻分配查询 qt,键 kt 与值 vt。对于某一确定时刻 t0,有

ct0=tpt0,tvt

即值的关于概率分布 pt0 的加权平均。pt0 是查询与键的匹配模式,即 qt0kt,为梯度稳定除一个 d,其中 d 是词向量维数,然后作 softmax。

具体而言,令查询矩阵为 n×dQ,令键矩阵为 n×dK,令值矩阵为 n×dV,则背景信息矩阵 C 亦是 n×d 矩阵,且

C=softmax(QKTd)V

Q 矩阵是按行生成的:i 时刻询问的生成依赖于前一时刻的询问、前一行的输出 y、前一时刻的背景信息。而自注意力机制中,上述全体参数均直接由序列生成。

多头注意力:将 Q,K,Vh 组不同的线性投影,对每组线性投影学习,然后将投影的输出向量连接起来,再作一组线性投影把拼接输出削减维数映射为目标输出。多头注意力中每个头可能关注不同的部分,因此是比单头更牛的。

Self-Attension 中所有位置是等价的,因此位置信息丢失了,转化为词向量时需要额外加入位置信息,即位置为 o 的词向量上加上如下的向量:

pi={sin(o/100002k/d)(i=2k)cos(o/100002k/d)(i=2k+1)

i 变大时频率增加,可以看作更高处对应了进制的更高位。


关注 3B1B 谢谢喵。

词向量可以描述一个词汇,距离衡量两个词汇的相似之处。然而存在一词多义,且一个词可能经由其它成分修饰并拥有完全不同的含义,因此上下文有关。

首先假定我们知道的词向量仅仅包含两个信息:词汇本身,与词汇所在位置。我们的目的是将其转为考虑完上下文后的结果。

使用统一的 WQ 矩阵(WQ 是模型参数),可以由词向量 xi 生成 qi=xiWQ。同理,有 WK 矩阵自 xi 生成 ki。然后作 softmax 归一化,归一化后的矩阵即为注意力矩阵。同理,可以用 vi=xiWV 生成 vi。生成背景信息向量 Ci 后,将其加到原本的词向量 xi 即可让该词向量与上下文结合。

词向量的维数常常是万级别的,而注意力矩阵的维数仅仅是百级别。WV 的长宽均为词向量的维数,这意味着一大堆参数。而如果把 n×nWV 换成 (n×d)×(d×n) 的两个矩阵,则参数数目立刻就能剧烈减少。

以上部分是单自注意力头的实现。多头的话,就是无数个 WQ,WK,WV 矩阵并行,得到很多背景信息向量,然后全部把它加到词向量上。

实际应用时常常会使用 Teacher Forching,即将一个句子的所有前缀都丢到模型中训练,但这时如果用完整矩阵就会出现用将来的事情去预测现在,这样不合法,因此注意力矩阵有时要丢掉对角线以下部分,也即在 softmax 之前把它们设成负无穷。

VI. Transformer

Transformer 是 Encoder-Decoder 的结构。Encoder 由若干 encoder block 组成,一个 block 包含一次多头连接、一次 Add&Norm、一次 Feed Forward、又一次 Add&Norm。

Add&Norm 是 shortcut 连接后作 Batch-Norm 归一化。

Feed Forward 是 Affine-ReLU-Affine 层。

Decoder block 由三部分组成:第一部分是 Masked 多头+Add&Norm,第二部分是多头+Add&Norm,第三部分是 Feed Forward+Add&Norm,最后作 Softmax 输出。

Masked 指使用掩码矩阵掩盖 QK 乘积对角线上方部分将其归零,目的是为了不让模型直接获取未来的答案(输入时全体信息会被一次性给出,所以是可以的)。这是因为,训练时为了快速出效果会使用 Teacher Forcing:即,如果翻译是“My rating is 1064”,模型应该在受到“开始”指令后输出 My,然后用 My 作输入输出 rating,然后继续;然而 TF 算法中输入永远都是所谓的“Ground Truth”,即就算第一步输出了 is 而非 My,训练时仍然会将 My 作为第二步的输出。因此需要掩码操作。

VII. GPT

GPT 的第一步被称作无监督的预训练 UPT,即多层 Transformer 堆叠。然后有监督地训练一层线性变换后,扔到 softmax 里出结果。其中“监督”指为数据打标签,告诉模型希望从数据中学到什么东西。

然后需要作一些微调。

  • 有监督微调:使用了少量有标签数据。会修改参数。
  • 提示词微调:嵌入额外 token。可改可不该参数。重点是寻找效果好的提示词并固化。
  • 前缀微调:嵌入自有的向量,不一定存在于 token 字典中。
  • LORA:低秩矩阵适应微调,在某些层中加一些秩数低的矩阵,不改变原模型参数,仅改变低秩矩阵的参数。
  • AT:插件微调,在 LORA 中把低秩矩阵换成小型神经网络。
  • RLHF:基于人类反馈的强化学习,本质还是一种有监督微调。

VIII. BERT

RNN 的原理是将上一时刻的隐藏向量与本时刻的输入共同传入模型得到本时刻的隐藏向量,进一步处理得到输出。其存在问题,例如难以并行、梯度弥散或梯度爆炸等。由此诞生了 BERT 模型。

BERT 模型的基础是 MLM 模型。在其中,其选择 15% 的单词,以 80% 的概率将其替换为占位符 [MASK],以 10% 的概率替换为随机其它词语,10% 的概率不变。另一个基础是 NSP 模型,是判断两个句子是否有先后关系的模型。用这两个玩意跑后再微调即可。

posted @   Troverld  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示