跳到主要内容

数值稳定性和权重初始化

· 阅读需 3 分钟

梯度爆炸和梯度消失问题

梯度爆炸

因为梯度的计算是通过偏导数的链式法则,所以,对于一个很深的网络,反向传播时,计算最后几层的梯度,很可能会超出数值的边界。比如cuda限制了16位的浮点数运算。这时,$1.1^100$超过了数值上界,程序就会报错。

梯度消失

如果某几个中间层的梯度很小,接近于0,那么前面几个层的梯度也是0,权重参数就得不到更新,这就是梯度消失。Sigmoid函数求导后,两头的梯度都接近于0,所以,很容易发生梯度消失。

合理初始化权重

合理初始化权重,可以缓解梯度爆炸和梯度消失问题。合理初始化化权重,就好比在选择一个离终点(最优点)近,且好走的起点。具体做法是,假设每层的输出和权重都满足独立正态分布,且均值、方差都相等。这种假设有以下几个原因(chatgpt):

避免梯度消失或梯度爆炸:如果每一层的输出和权重具有相同的均值和方差,那么它们传递的梯度也会有相同的大小,这可以避免梯度消失或梯度爆炸问题。

提高网络的收敛速度:假设每一层的输出和权重具有相同的均值和方差,可以使网络更容易收敛,因为权重初始化过大或过小可能会使得网络的训练变得非常缓慢。

减少过拟合的可能性:如果权重初始化过大,网络容易出现过拟合现象,而假设每一层的输出和权重具有相同的均值和方差可以避免这种情况的发生。

多层感知机(MLP)

· 阅读需 3 分钟

终于从前面的单层网络linear-regression和softmax过渡到多层神经网络了。为了对更加复杂的数据进行学习,多层感知机将多个全连接层叠加,并增加激活函数。激活函数是非线性的,因为如果不加激活函数或者激活函数线性,那么多层神经网络还是遵循线性规律,等同于一层。

多层感知机

中间的,就是隐藏层。所以,多加的隐藏层中神经元的个数也是一个超参数。

从零实现代码

import torch
from torch import nn
from d2l import torch as d2l

batch_size=256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
num_inputs, num_outputs, num_hiddens = 784, 10, 784

W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X@W1 + b1)
return (H@W2 + b2)
loss = nn.CrossEntropyLoss(reduction='none')
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
d2l.predict_ch3(net, test_iter, n=20)

利用pytorch api实现

import torch
from torch import nn
from d2l import torch as d2l
## 网络搭建
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))

## 初始化权重
def init_weights(m):
if type(m)==nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);
## 超参数设置
batch_size, lr, num_epochs = 256, 0.1, 10
## 损失函数
loss = nn.CrossEntropyLoss(reduction='none')
## 优化器
trainer = torch.optim.SGD(net.parameters(), lr=lr)
## 加载数据(dataloader)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
## 正式训练
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

总结

深度学习代码编写步骤:

网络搭建-》初始化权重-》超参数设置-》损失函数-》优化器-》加载数据-》正式训练

softmax回归(分类)

· 阅读需 2 分钟

回归和分类

机器学习(深度学习)的任务纷繁复杂。最基础的是回归和分类。回归是预测连续值,分类是预测离散类别。

分类问题是多输出,因此,训练标签和模型(网络)的输出应该是多维的。独热编码 (one-hot encoding)是一种表示多分类的方式。就是在一个向量中,将真实分类索引下的值设为 1,其他是 0,因此向量自身的内积为 1。另外,模型的线性表示为:

所以,softmax 也是一个单层全连接网络。


\begin{aligned}
o*1 &= x_1 w*{11} + x*2 w*{12} + x*3 w*{13} + x*4 w*{14} + b*1,\\
o_2 &= x_1 w*{21} + x*2 w*{22} + x*3 w*{23} + x*4 w*{24} + b*2,\\
o_3 &= x_1 w*{31} + x*2 w*{32} + x*3 w*{33} + x*4 w*{34} + b_3.
\end{aligned}

softmax 函数

分类问题的训练,可以用最大化正确分类的概率来表示。因此,要通过 softmax 函数将神经网络的输出$o_i$转化为 0-1 之间的概率。

\begin{equation}
\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}
\end{equation}

softmax 可以保证输出在 0-1 之间,且 softmax 可导。

线性回归

· 阅读需 4 分钟

线性回归

一句话表示,就是数据的分布是按照如下的线性表达式:

$$ y=w_1x_1+w_2x_2+...+w_nx_n+b $$

$w_n$就是网络的权重(参数),b 也是一种权重。

代码实现

import random
import torch
from d2l import torch as d2l
## 自动生成数据集
def synthetic_data(w, b, num_examples):
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape(-1, 1)

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features.shape, labels.shape

(torch.Size([1000, 2]), torch.Size([1000, 1]))

def data_iter(batch_size,features,labels):
num_examples=len(features)
indices=list(range(num_examples))
#这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0,num_examples,batch_size):
batch_indices=torch.tensor(indices[i:min(i+batch_size,num_examples)])
yield features[batch_indices],labels[batch_indices]
batch_size=10
for X,y in data_iter(batch_size,features,labels):
print(X,'\n',y)
break

tensor([[-1.2750, 1.5482], [-0.0563, -2.0593], [-0.3648, -0.0083], [ 0.2933, 0.3219], [-0.6043, -0.0551], [-1.1544, -0.0258], [ 0.9690, -0.7872], [ 0.7860, 0.0937], [ 0.9102, -0.6743], [ 1.6593, 0.3044]]) tensor([[-3.5984], [11.0968], [ 3.5161], [ 3.6972], [ 3.1954], [ 1.9791], [ 8.8084], [ 5.4522], [ 8.3228], [ 6.4840]])

w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
## 网络模型
def linreg(X, w, b):
return torch.matmul(X, w) + b
## 损失函数
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape))**2/2
## 优化算法(用来更新网络参数)
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
## 训练代码
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y)
l.sum().backward()
sgd([w, b], lr, batch_size)
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'open {epoch+1}, loss {float(train_l.mean())}')

open 1, loss 5.805317050544545e-05 open 2, loss 5.791278090327978e-05 open 3, loss 5.7983954320661724e-05

print(f'w的估计误差:{true_w-w.reshape(true_w.shape)}')
print(f'b的估计误差:{true_b-b}')

w的估计误差:tensor([-2.5511e-05, -1.8573e-04], grad_fn=<SubBackward0>) b的估计误差:tensor([0.0001], grad_fn=<RsubBackward1>)

文字说明

所有深度学习训练过程都可以用四个步骤概括:1.前向传播(输入到网络),2.计算损失(损失函数可导,且损失函数的值必须是标量),3.计算梯度(用于下一步的更新参数。backward()),4.更新网络参数(这里用的是 SGD)

SGD

gpt:

随机梯度下降(Stochastic Gradient Descent,SGD)是一种常用的优化算法,常用于机器学习中的参数优化问题。在传统的梯度下降算法中,每次更新模型参数需要遍历整个训练集,计算所有样本的梯度平均值,这样的计算代价很大,尤其是在大规模数据集上。SGD 算法通过每次从训练集中随机选择一个样本进行梯度计算和模型参数更新,降低了计算代价,加快了模型收敛速度。此外,SGD 还可以避免陷入局部最优解,使得模型更有可能达到全局最优解。

然而,SGD 也有一些缺点,如对于数据的噪声敏感、容易受到初始点的影响等。因此,在实践中,常常使用一些改进的 SGD 算法,如动量优化、Adagrad、Adam 等来克服这些缺点。

这段话里面。SGD 每次应该不一定只选择一个样本,而是若干个样本。这也因此引入了 batch_size 的概念。batch_size 就是每次计算梯度时选取样本的数量。

动手pytorch

· 阅读需 1 分钟

这个系列记录我跟随 B 站上李沐的《动手学深度学习》的视频教程。每五个章节的笔记合成一篇 blog。

安装环境

我的学习平台时 win11, powershell, anaconda

conda create -n d2l-zh python=3.8
conda activate d2l-zh
pip install jupyter d21 torch torchvision
jupyter notebook

激活了 jupyter notebook。可以实时演示效果,适合初学时做一些 demo。

bug fix

使用 powershell 时,出现了红字错误:

. : File C:\Users\lfy\Documents\WindowsPowerShell\profile.ps1 cannot be loaded because running scripts is disabled on t
his system. For more information, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:3
+ . 'C:\Users\lfy\Documents\WindowsPowerShell\profile.ps1'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess

询问 chatgpt 后,这是 powershell 设立的脚本运行安全策略。解决方案是:

Set-ExecutionPolicy RemoteSigned

主题更换和迁移到vercel记录

· 阅读需 4 分钟

首先是主题更换。之前用的主题原生很多动画和功能,但是我本身是不喜欢复杂的,所以把背景和功能改的很精简。但是这样反而做的不称心。我的理想样式是:文章居中,两侧没有除了目录外的任何东西。所以,我找到了目前用的这个 orange 主题,这次我用的时间打算长些,短时间不换了。

其次是静态网站服务器从 github-pages 转移到 vercel。主要考虑是,github-pages 要求对应的仓库必须是公开的,这样有心人可以看到每一次过往文章提交修改记录,这让我觉得有点膈应。vercel 是一种流行的无服务平台,代码托管在 github 仓库后,vercel 可以实现自动部署和发布。现在的工作流是,本地修改好文章,直接推到 github 私有仓库,vercel 检测到变化,自动部署。值得一提的是,我顺便更换了 DNS 的 nameserver,由原来 namesilo 默认的,换到全球最大的 CDN 商 cloudflare。中间的过程记录一下。

vercel 创建实例成功后,会分配一个 example.vercel.app的二级域名。为了将买到的域名用上,需要去自定义设置,添加域名,vercel 会给你一个 A 或者 CNAME 记录。如果是一级域名,给 A 记录;如果是二级域名,给 CNAME 记录。这时候要在 cloudflare 添加记录。首先注册 cloudflare,然后,输入自己先前购买的域名,自动导入目前的 DNS 解析记录和 nameserver。这时候,将 DNS 解析规则先删除,并来到 namesilo 的管理页面,改变 nameserver 为 cloudflare 提供的地址。到这里的修改都是实时生效的。回到 cloudflare,刷新,cloudflare 开始接管你的域名,这一过程会花费十几分钟。

然后,将之前 vercel 提供的解析记录,提供给 cloudflare。不同于 github-pages,只有输入一条就能解析了。

其中,名称就是二级域名,内容是 vercel 的地址。成功后,这是你浏览器发出的请求就会被 cloudflare 所解析啦,并且可以勾选他们家提供的 CDN 服务。据说,中国大陆的 cloudflare CDN 反而是减速器,所以我没选。设置好后,实测不用翻墙就可以访问网站。

以后就专心写文章啦!

latex奇怪bug解决方案

· 阅读需 4 分钟

用 overleaf 写大论文时候,有一个作者的名字带生僻字:“赟”,overleaf 会显示为“F”。解决方案如下:

在 usepackage 区里,添加以下代码:

\usepackage{ctex}
\setCJKfamilyfont{myfont}{BabelStone Han}
\newcommand{\MyFont}{\CJKfamily{myfont}}

意思是添加一个自定义指令,专门用某个可以正常显示生僻字的字体,这里是 BabelStone Han。

然后在正文处:

{\MyFont{赟}}

就可以正常显示了!

latex 清除浮动来消除大片空白

问题描述

虽然 latex 是自动排版,但是偶尔会出现明明可以放下内容的地方,被留下空白。通常是发生在多个表格或者图片相连,然后后面跟着文字,文字和最后一个图表之间就会分页。

解决方案

最简单的,先从垂直间距开始调起。\vspace{-1cm}。但是可能不起效果,比如本文提到的情景。

或者是清除浮动。浮动的含义是,图表的位置并不固定在源码的位置,而可能被放置在上下几页,具体取决于 latex 算法认为美观的地方。在我这种情况中,显然是 latex 的算法自动排列的算法不符合真正的美观,所以要手动取消浮动,并且人工微调。取消浮动的方式是引入下列的包:

\usepackage[section]{placeins} 避免浮动体跨过 \section
\usepackage{float} 禁止浮动
% ...
\begin{figure}[H]
% ...
\begin{table}[H]
% ...

幸运的是,取消浮动后,一张表占据一整页的现象消失了,而且位置刚好,没有出现大面积空白。

参考: https://www.zhihu.com/question/25082703

tabularx 解决表格满宽

需求:让表格宽度等于页面宽度,单元格列宽可以指定,单元格文字居中。

解决方案:tabularx 是 tabular 的增强版,可以指定表格总体所占宽度,设置步骤如下:

  1. 在导言区导入 tabularx 包,并设置自适应宽度和指定宽度两种列选项,使得文字可以居中(默认是左对齐)
\usepackage{tabularx}
\usepackage{array}
\usepackage{ragged2e}
% 该命令用于控制 p{} 的情况
\newcolumntype{P}[1]{>{\RaggedRight\hspace{0pt}}p{#1}} % 使用过程中,将p{4cm}换成P{4cm},小写改成大写即可!
% 该命令用于控制 X 的情况
\newcolumntype{Z}{>{\centering\let\newline\\\arraybackslash\hspace{0pt}}X} % 使用过程中,将Z 换成 X,即可!
\let\oldtabularx\tabularx
\renewcommand{\tabularx}{\zihao{5}\oldtabularx}

% 可利用 RaggedLeft Centering替换RaggedRight,实现靠右和居中 [代码对大小写敏感!!!!!!!!!!!!!!!!!!!!!!!!!!!!]

其中,p 表示指定宽度,X 表示自适应,也就是均分宽度。

  1. 在表格区域内使用方式:
\begin{table}[h!]
\centering
\caption{DICM、LIME、MEF、NPE数据集上的平均NIQE值} \label{dwtqrcp_niqe}
\begin{tabularx}{\textwidth}{ZZZZZ}
data
\end{tabularx}
\end{table}

\begin{tabularx}{\textwidth}{ZZZZZ} 第二个选项指定整体宽度,第三个是列选项。

站在更高的角度思考前后端分离

· 阅读需 5 分钟

似乎从我开始学编程起,默认的开发模式就是前后端分离。但是,面对一门心仪的后端框架(如 django,laravel,它往往还带着仿佛上古时期留下来的模板引擎。为什么随着时代的更迭,模板引擎这种明显前后端耦合的东西还会存在呢?面对自己将来的定位(独立开发者),该如何根据情况进行技术选型呢?

开门见山

直接说结论,碰到以下需求的,用前后端分离:

  • 你不是一个人单干,需要一个技术团队。也许这时候你的业务已经初具规模,那么,为了团队更顺畅的沟通,必然选择为团队协作开发而生的前后端分离。
  • 你的应用有多端需求,比如 pc/ios/android 都有页面要做,这样后端只需提供接口,各端负责展示,后端就实现了复用。
  • 你的产品确定了一上线肯定会有很多用户使用,有服务器压力。前后端分离可以方便的进行服务资源扩展。

从历史发展的角度

上古时期,前后端不分离,后端也要负责页面的展示。jsp、php 是最受欢迎的模板语言,因为他们可以很容易地搭建起网站。后来,移动互联网的到来才是改变一切的原因。一方面,移动互联网使得互联网用户井喷式增长,对于服务器的压力承受水平提出更高的要求,服务端渲染的模式在高并发场景下很容易导致宕机,造成损失;另一方面;移动端的原生应用肯定不是用服务器返回的 html 页面来展示的,它们都有各自的底层框架,这样,就必须将前后端解耦,使手机 app 能单纯获取到数据。

近年来,前后端分离的模式也在演进。主要是前端的变化:从 jquery 按页面开发,到 vue/react 框架按组件开发。前端的进化带来了新的问题:首屏加载速度慢和 seo 糟糕。

  • 首屏加载速度慢:前端项目用框架开发好后,打包生成的文件里,除了静态的 css、img、font,只有一个巨大的 js 文件夹和 index.html。如果没有优化,所有的资源文件一次下载完毕后才能正常显示,不过这种情况下,后续的页面跳转会比不用框架快。
  • seo 糟糕:中国的搜索引擎百度还不能解析 js 文件,只能爬取到空的 index.html 页面。

解决方案有很多,可以参考这篇文章(链接)。这里以 Vue 的 SSR 框架 Nuxt.js 举例。在服务端起一个 node 应用,浏览器到来时,先拦截执行部分 js 异步请求,提前将数据填充到 html 页面中返回浏览器。这样爬虫抓取到的页面就是带数据的,有利于 SEO。也就是说,nodejs 作为中间层,模拟了一部分浏览器渲染数据的角色。

总结

如果将来自己独立开发,如果有一个新的 idea,你想快速发布原型,查看市场反应,而且暂时没有手机原生 app 端的需要(手机可以在浏览器上访问,只要兼顾到响应式)。那么就用 laravel 框架的模板语言将产品快速做出来,如果将来业务量增长了,可以先分离前后端,前端用 SSR 框架重写,如果用户增长到一定规模,就招人,用高性能语言,如 java、go。

namesilo购买域名和github pages配置

· 阅读需 2 分钟

自从建立网站以来,每次访问,都困扰于输入长长的域名前缀 www,此外,goldspot 似乎也太长且拗口。坑爹的是,goldspot 绑定的 namesilo 账号不知为什么登不上去了。索性重开新号,购买新域名。

购买域名的过程不再赘述,这次花了人民币 16.90 元。为了将买到的 rula.life 和 li199-code.github.io 绑定,需要如下操作:

首先,在 github 的仓库设置里,输入 custom domain 并保存。注意要带上 www。然后将 hexo 本地博客源文件夹的 source 文件夹下的 CNAME 记录修改如下:

rula.life

不带 http 和 www。最后,在 namesilo 的 DNS 设置界面,选择 github 模板,一键应用。回到 github pages,设置 enforce https,完毕!

注意:在 pages 设置界面,DNS 检查时,要出现如下显示,才说明 CNAME 和 Namesilo 设置成功:

TLS

javascript 模块机制

· 阅读需 5 分钟

前言

最近阅读《深入浅出 nodejs》和《nodejs 实战》的时候,都把模块系统放在前面的位置介绍,可见其重要性。模块机制帮助开发者将代码分割成独立的、功能明确的块,可以单独开发、测试和维护。因为之前写的文章逻辑比较混乱,故重新写一下。

JavaScript 主要有以下几种模块机制:

  • ES6 模块 (ES Modules)
  • CommonJS 模块
  • AMD 模块
  • UMD 模块

其中,最常见的莫过于前两种。

ES Modules

这是在 ES6(ECMAScript 2015)中引入的标准模块系统。ES6 模块通过 import 和 export 关键字来导入和导出模块。ES6 模块具有以下特点:

  • 静态结构:模块的依赖关系在编译时就能确定,不需要在运行时解析。这使得工具可以进行静态分析和优化。(这是不是基于 Nodejs 的后端代码也要采用 ES6 模块机制的原因?)
  • 文件即模块:每个文件被视为一个独立的模块。
  • 默认导出和命名导出:可以导出多个命名导出,也可以有一个默认导出。从格式来看,采用命名导出时,导入文件中,模块名外带花括号,而默认导出则相反。
  • 多入口加载,结合第一点,因而可以实现按需加载。

示例:

导出模块 (math.js)

// Named exports
export function add(a, b) {
return a + b;
}

export const PI = 3.14159;

// Default export
export default function subtract(a, b) {
return a - b;
}

导入模块 (main.js)

// Importing named exports
import { add, PI } from "./math.js";

console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

// Importing the default export
import subtract from "./math.js";

console.log(subtract(5, 2)); // 3

注意事项:

  • 模块路径:相对路径或绝对路径需要明确指定。
  • 顶级作用域:ES6 模块在模块顶级作用域中运行,因此每个模块都有自己的独立作用域。

CommonJS

CommonJS 模块是 Node.js 使用的模块系统,通过 require 导入模块和 module.exports 导出模块。CommonJS 模块具有以下特点:

  • 动态加载:模块在运行时加载,require 是一个同步操作。
  • 整个模块导出:可以导出一个对象,该对象包含多个属性和方法。
  • 单一出口:模块通过 module.exports 导出单一对象。

示例:

导出模块 (math.js)

function add(a, b) {
return a + b;
}

const PI = 3.14159;

function subtract(a, b) {
return a - b;
}

module.exports = {
add,
PI,
subtract,
};

导入模块 (main.js)

const math = require("./math");

console.log(math.add(2, 3)); // 5
console.log(math.PI); // 3.14159
console.log(math.subtract(5, 2)); // 3

注意事项:

  • 动态加载:模块在代码执行到 require 语句时才会加载。
  • 缓存:一旦模块加载,它会被缓存,再次 require 相同模块时将返回缓存的版本。

比较 ES6 模块和 CommonJS 模块

特性ES6 模块CommonJS 模块
语法import / exportrequire / module.exports
加载方式静态加载动态加载
依赖关系编译时确定运行时确定
顶级作用域模块级作用域文件级作用域
是否支持浏览器否(需要打包工具,如 Browserify)
缓存机制浏览器端和服务器端缓存均支持服务器端缓存
默认导出支持需要手动指定