Zhangzhe's Blog

The projection of my life.

0%

0. 背景知识

  1. NAS 是什么?
    • NAS 的全称是 Network Attached Storage,网络附加存储,说白了就是一个低性能高容量的私有云服务器
  2. NAS 能干什么用?
    • NAS 可以作为家庭影院数据中心,NAS + 局域网文件传输协议 + 智能电视 + 家庭影院软件可以实现非常好的家庭观影体验
    • NAS 可以作为私有云服务器,承担例如图床/视频床等功能,也可以建网站
  3. 怎么做一个 NAS
    • 通常情况下,直接买一个成品 NAS 是一个省心的选择,外观看通常是一个可以插硬盘的盒子,本质是一个 PC
    • 我更喜欢自己手搓

1. 手搓 NAS 方案

  1. NAS 手搓需要搞定 NAS 本体和网络两大块
    1. NAS 本体负责下载数据,保存数据和局域网内数据共享
    2. 网络部分负责让 NAS 本体成为一个公网可访问的设备
  2. NAS 本体用树莓派 5 实现是一个不错的选择
  3. 网络部分用光猫桥接 + DDNS + 端口转发方案

2. NAS 本体

硬件

  1. NAS 本体是一个 4GB 版本树莓派 5
  2. 一个 32GBSD 卡作为系统盘
  3. 一块 1TBUSB 3.0 的移动机械硬盘(后续根据需求扩容)

软件

  1. 操作系统:Ubuntu 24.04 Desktop arm64下载地址
    1. Ubuntu 的好处:社区活跃,遇到问题容易解决
    2. 坏处:预安装软件很多,比较大
  2. 其他软件:根据需求安装,例如:
    1. Samba 局域网共享软件
    2. RDP 远程桌面软件
    3. Transmission 磁力链接下载器

3. 网络

NAS 本体比起来,网络才是手搓 NAS 最难的部分,主要包含如下几个部分

公网 IP

  1. 选择的宽带运营商是电信,电信的入户光纤是动态公网 IPv4 地址(这一点还是比较良心的)
  2. 光猫默认开启了路由模式,因此路由器的输入已经变成了局域网,为了简化网络拓扑,需要给电信客服打电话要求将光猫改成桥接模式,即光猫只承担光电转换功能,输出端口 IP 和输入端口 IP 一致
  3. 光猫开启桥接模式之后,路由器输入端口变成了公网 IP,通过 PPPoE 拨号上网,同时实测发现,光猫桥接模式下,下载速度更快

DDNS

  1. 虽然路由器的输入已经变成公网 IP,但是动态的,几乎每天都在发生变化
  2. 因此需要用到 DDNS 服务,让动态 IP 绑定到静态的域名上,域名之前已购买,只需要在顶级域名之前加上一个 A 记录的子域名
  3. 由于家里用的路由器是小米 be6500 pro 型号,这个路由器本身并不支持腾讯云(dnspod)的 DDNS 服务,所以有两条路可以选:
    1. 刷机到 Openwrt 路由器固件,这个开源固件功能十分强大,但坏处是路由器无法再绑定到米家 APP 实现远程控制的一些官方功能
    2. 另外一种方法是通过某种方法开启路由器的 ssh 权限,登录到路由器内部,在内部开启 dnspod DDNS
  4. 这里选择了第二种方法,ssh 开启教程在 这里
  5. 开启 ssh 后,就可以登录到路由器 terminal,然而路由器使用的是小米自己魔改的 XiaoQiang Linux 操作系统,包管理工具以及软件源什么的完全搞不懂,也很难查到相关资料,所以常用的 DDNS 服务软件例如 ddns-go 什么的也无法正常安装和配置
  6. 在仔细了解了 DDNS 服务的基本原理之后,决定用 SHELL 手写一个 DDNS 服务
    1. DDNS 的原理(仅在 dnspod 服务商上测试过):客户端(路由器)定期检查输入端口 IP,当发现和 dnspod 服务商记录的这个域名绑定的 IP 不一致时,就给 dnspod 发一条修改绑定关系的请求,dnspod 更改后,新的 “域名——IP” 绑定关系就建立了
    2. 这里涉及到几个关键:
      1. API Token:上面这个修改过程显然不是任何人都可以改的,你只能修改自己名下的域名绑定的 IP,所以需要一个密钥来和 dnspod 服务器交互,这个密钥在 https://console.dnspod.cn/account/token/token 创建
      2. 查找域名和记录对应的 ID:由于 dnspodDDNS API 要求域名和对应的记录是以 ID 的方式描述的,所以需要查到域名 ID 和记录 ID
        1. 域名 ID
          1. curl -s "https://dnsapi.cn/Domain.List" -d "login_token=<your_token>&format=json" 得到输出查询结果
          2. 然后在查询结果中找到名下多个域名中想要查询域名的 ID
        2. 记录 ID
          1. curl -s "https://dnsapi.cn/Record.List" -d "login_token=<your_token>&format=json&domain_id=<your_domain_id>&sub_domain=<your_sub_domain>" 得到查询结果
          2. 在查询到的此域名下多条记录中,找到关注的记录 ID
      3. 查询当前 IPcurl -s http://ipinfo.io/ip
      4. 更新 DNS 记录:curl -s -X POST "https://dnsapi.cn/Record.Modify" -d "login_token=${API_TOKEN}" -d "format=json" -d "domain_id=${DOMAIN_ID}" -d "record_id=${RECORD_ID}" -d "sub_domain=${SUB_DOMAIN}" -d "record_line=${RECORD_LINE}" -d "record_type=${RECORD_TYPE}" -d "value=${CURRENT_IP}"
    3. 然后将此 DDNS_update.sh 文件注册到 crontab 中,每 5 分钟更新一次
  7. 确实可以正常更新 DNS,不过忙完之后才发现可以在树莓派上实现,不用自己写 SHELL

端口转发

  • 外网只能访问到路由器,如果想要通过 ssh 实现外网直连树莓派,那么需要在路由器上配置端口转发
  • 由于目前只有 SSHRDP 两个远程访问需求,所以只开了 223389TCP 端口转发(开的越少内网设备越安全)

4. 使用体验和后续计划

使用体验

  1. 用外网可以直连树莓派,可以用 SSH 远程给树莓派下发一些下载任务,也可以用 RDP 处理一些需要 GUI 的需求
  2. 设置了移动硬盘开机自动挂载,索尼电视安装 KODI 通过 Samba 协议看 4K 电影体验很震撼

后续计划

  1. 开放 http 协议端口,在树莓派上用 Nginx 等引擎让树莓派作为个人博客的图床和视频床
  2. 扩展硬盘架,树莓派外接 16 pin PCIE 转多口 SATA 扩展板 ,连接机械硬盘架

URL

TL;DR

  • 本文提出一种名为 Low Rank Adaption (LoRA) 的大模型微调技术,可有效降低大模型微调过程中的可微调参数(降低 10000 倍)和显存占用(降低 3 倍)
  • 具体做法是在 Linear 算子和 Embedding 算子中插入可训练的参数量较少的低秩分解矩阵,冻结原始参数,只训练低秩分解矩阵

Algorithm

总体流程

lora_1.png

  • 这张图几乎包含了 LoRA 全部的信息量:
    1. LoRA 在原始 Linear 参数的基础上,加入了低秩分解矩阵 ARd×r,BRr×dA\in\mathbb{R}^{d\times r},B\in\mathbb{R}^{r\times d}
    2. r << d,所以叫
    3. 原始参数冻结,只训练 AB
    4. 矩阵 A 用均值为 0 的正态分布初始化
    5. 矩阵 B 用全 0 初始化

对应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import torch
import torch.nn as nn
class LoRALinear(nn.Module):
def __init__(self, in_features, out_features, r):
super(LoRALinear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.r = r
# 线性层的权重矩阵
self.weight = nn.Parameter(torch.randn(out_features, in_features))
self.bias = nn.Parameter(torch.zeros(out_features))
# LoRA的低秩分解矩阵
self.A = nn.Parameter(torch.randn(r, out_features))
self.B = nn.Parameter(torch.zeros(in_features, r))
def forward(self, x):
# 应用线性层
output = torch.matmul(x, self.weight) + self.bias
# 应用LoRA的低秩分解矩阵
output = output + torch.matmul(x, torch.matmul(self.B, self.A))
return output
def convert_to_standard_linear(self):
# 将LoRA参数转换到标准线性层中
self.weight = nn.Parameter(self.weight + torch.matmul(self.B, self.A))
# 删除LoRA的低秩分解矩阵
del self.A
del self.B
return self
class LoRATransformerLayer(nn.Module):
def __init__(self, d_model, r):
super(LoRATransformerLayer, self).__init__()
self.d_model = d_model
self.r = r
# 自注意力模块的权重矩阵
self.Wq = LoRALinear(d_model, d_model, r)
self.Wk = LoRALinear(d_model, d_model, r)
self.Wv = LoRALinear(d_model, d_model, r)
self.Wo = nn.Linear(d_model, d_model)
def forward(self, x):
# 计算查询、键和值的投影
q = self.Wq(x)
k = self.Wk(x)
v = self.Wv(x)
# 计算自注意力得分和输出
attn_scores = torch.matmul(q, k.transpose(-2, -1)) / (self.d_model**0.5)
attn_weights = torch.softmax(attn_scores, dim=-1)
attn_output = torch.matmul(attn_weights, v)
# 计算最终的输出
output = self.Wo(attn_output)
return output
def convert_to_standard_transformer(self):
# 将LoRA参数转换到标准Transformer网络中
self.Wq = self.Wq.convert_to_standard_linear()
self.Wk = self.Wk.convert_to_standard_linear()
self.Wv = self.Wv.convert_to_standard_linear()
return self
# 示例用法
d_model = 512
r = 8
layer = LoRATransformerLayer(d_model, r)
input_tensor = torch.randn(10, 32, d_model)
output_tensor = layer(input_tensor)
print(output_tensor.shape) # 输出: torch.Size([10, 32, 512])
# 转换到标准Transformer网络
standard_layer = layer.convert_to_standard_transformer()
print(standard_layer)

实际使用

  1. 实际使用时,可以用 LoRA_Layer替换大模型中所有 Transformer 层的 LinearEmbedding,对 FFN 中的 MLP 不做替换
  2. 可以不同的任务微调不同的 LoRA 模型
  3. 部署时可以用重参数化融合的方法,将 LoRA 训练参数融合到原始模型中,不会付出任何推理代价

Thought

  • 有道理,也挺好用,但据说真正做大模型预训练 / 微调的没人用

URL

TL;DR

  • 本文提出一种类似 GPT-4 的图文多模态模型 Large Language and Vision Assistant (LLaVA),基于开源的 CLIPLLaMA 分别作为图文编码器,因此 LLaVA 也完全开源

Algorithm

多模态指令遵循数据生成

  • 已有:图形——文本对数据集
  • 需要:图文指令遵循数据集,格式为:
    • 图片:原始图片
    • 问题:由 GPT-4 生成,输入原始 “图片-文本” 给 GPT-4,让 GPT-4 就这些信息提问
    • 答案:同上,让 GPT-4 回答自己提出的问题

模型结构

  • 图像模型:CLIP ViT-L/14 已做过图像文本对齐的预训练图像编码器模型
  • 大语言模型:LLaMA 预训练模型
  • 连接层:简单的线性映射层

如何训练和微调

训练

  • 冻结图像编码模型
  • 冻结 LLM 模型
  • 训练连接层

微调

  • 冻结图像编码模型
  • 训练 LLM 模型
  • 训练连接层
graph TD;
    A([视觉编码器]) --> B([连接层])
    B --> C([LLaMA语言模型])
    D[语言指令(例如:“请根据这张图片生成一个详细的描述”)] --> C
    C --> E[文本响应]
    F[图像] --> A
    G[系统消息(例如:对话历史记录)] --> C

Thought

  • 简单直接,来自开源,也回馈开源,很棒!

URL

TL;DR

  • 本文提出一种跨模态开放集目标检测算法,即:输入一张图片 和 需要检测内容的文本描述,给出框
  • 其中文本描述可以是开放的(任意内容的文本)
  • 本文最重要的部分是模型结构中图文多模态内容的融合

Algorithm

groundingdino.png

  • 本质是通过多次 Cross-Attention 来做多模态信息融合
  • text backbone 实际是 BERT
  • image backbone 实际是 SwinTransformer
  • 其中的 Language-guide Query Selection 是根据文本特征,找到图像特征中最匹配的部分初始化跨模态解码器

Thought

  • 这篇论文想要解决的任务时开放集目标检测,但其多模态信息融合方式让其出圈,成了多模态领域的经典

URL

TL;DR

  • DINO 一样,都是做自监督视觉预训练的,对于 DINO 的主要升级是构建了一个大规模自动化数据处理管线,构建了 LVD-142M 高质量数据集
  • 用这些数据集预训练了一个 1B 参数的 ViT 模型,通过无监督蒸馏的方式,得到用于不同任务的小模型

Algorithm

自动化数据处理管线

dino_v2.png

  • 自动化处理流程包括如下几个步骤,不断重复迭代

1. 数据收集

  • DINOv2 的数据源包括一个大型的未筛选图像数据集和一个较小的经过筛选的图像数据集
  • 未筛选数据集来自网络爬取
  • 筛选数据集来自 ImageNet-22k / Google Landmarks

2. 图像嵌入

  • 对于未筛选数据集,用一个训练好的 ViT-H/16 计算得到图像 embedding vector

3. 图像去重

  • 用特征空间下去重算法,将未筛选的数据集去重

4. 图像检索

  • 在特征空间下聚类,得到与筛选数据集类似的未筛选数据样本

5. 数据增强

  • 让这些聚类得到的类似的未筛选样本作为筛选样本,不断扩大筛选样本的数量和场景丰富度

模型架构

  • DINO v1 中教师和学生使用动量更新的方式不同,DINO v2 使用了常见的 “大老师,小学生” 架构
  • 先训练一个 1B 参数的 ViT 模型作为老师模型
  • 然后再在各个不同任务数据上蒸馏得到小模型

训练策略优化

  • 由于老师模型很大(1B 参数量),所以需要 LM 常用的训练加速方法,包括:
    • FlashAttention
    • Fully-shared Data Parallel (FSDP)

Thought

  • 这套数据处理管线是本文重点,所有的自监督任务,自动化数据处理流程都是必不可少的

URL

TL;DR

  • CLIPOpenAI 提出的一种图文多模态对齐算法,在收集到的 4 亿对图片文本数据对上,将文本和图像编码在同一表达空间下,实现了图文模态的对齐
  • 可以 zero-shot 迁移到其他计算机视觉任务上

Algorithm

CLIP.png

训练时

  1. N 对图片和文本各自编码
  2. 计算得到不同模态之间两两编码的 余弦相似度 RN×N\in \mathbb{R}^{N\times N}
  3. 使用对比学习的方式,提高 N 个正样本的相似度,降低剩余的 N2NN^2-N 个样本的相似度

推理时(以 ImageNet 分类任务为例)

  1. ImageNet-1k 的所有 1000 种类别标签,通过训练好的文本编码器,转换到特征空间中
  2. 将需要分类的图片,通过训练好的图片编码器,转换到特征空间中
  3. 图像编码找到余弦相似度最高的文本编码,对应的类别就是图片类别

模型选型

  • 图像编码器:
    • Vision Transformer (ViT)
    • ResNet-50
  • 文本编码器:Transformer
    • 63M 参数
    • 12
    • 512
    • 49152 词表大小
    • BPE 文本编码方式

Thought

  • 简洁高效,像 OpenAI 固有的风格
  • 有没有可能在 GPT-4 的多模态中用到呢?

URL

TL;DR

  • DINO(Distillation with No Labels) 是一种自监督学习方法,主要用于 Vision Transformer (ViT) 的训练
  • 在无标签的图片数据上训练模型,让模型学习图像的表示意义
  • 利用 MoCo 提出的 Momentum Teacher 算法做蒸馏

Algorithm

dino_1.png

训练流程

  1. 创建两个完全一样的网络,命名为教师 teacher 网络和学生 student 网络
  2. 对同一个输入 x,进行不同的数据增强,得到 x1x2
  3. 交叉计算对比损失,再求均值得到 loss for student
  4. 只对 student 网络进行反向传播和梯度更新
  5. 基于 student 网络的参数更新 teacher 的参数,更新方式是 EMA (exponential moving average),即:θt=λθt+(1λ)θs\theta_t=\lambda \theta_t+(1-\lambda)\theta_s
  6. 更新 teacher 网络输出的中心点:C=mC+(1m)mean(t1,t2)C = m*C + (1 - m)*mean(t1, t2)

中心化和锐化

dino.png

  • 两种操作本质上是互补的,防止模型训练崩溃

中心化(centering

  • 中心化的目的是防止特征向量的某个维度占主导地位,从而导致模型输出分布过于集中
  • 本质就是一种均值为 0 的归一化,可以提高模型训练的稳定性

锐化(Sharpening

  • 锐化操作的目的是增加教师网络输出的概率分布的锐度,使得输出的概率更加集中在少数几个维度上
  • 实现上,锐化通过修改蒸馏温度系数实现

模型效果

  • 比一众视觉自监督模型效果都好,比如:MoCo v1/v2SimCLR v1/v2

Thought

  • 感觉是 MoCo 系列的升级,框架本身不变,加了数据,稳定了训练过程,增加了些许 trick

URL

TL;DR

  • 本文提出一种提示词微调的方法,是对 P-Tuning 的升级
  • P-Tuning v2 要解决的问题是:对于所有(或大多数)任务类型和模型参数规模,将提示词微调的精度达到和整体参数微调同样的效果,这也是这篇论文的题目

Algorithm

p_tuning_v2.png

P-Tuning 的优化

1. P-Tuning 只在模型输入层添加可学习的连续嵌入,P-Tuning v2 在模型的每一层都添加

  1. Prefix Tuning / Prompt Tuning / P-Tuning 三种方法都是在模型输入中加入连续嵌入
    • 添加方式可能是前缀,也可能是其他 Concat Pattern
    • 通过 Self-Attentionembedding 间信息融合机制让虚拟连续嵌入影响整个模型
  2. P-Tuning V2 的做法则完全不同,是对模型的 每一层 都添加了可学习的虚拟连续嵌入
    • 具体来说是通过初始化虚拟 past_key_values 来实现的
    • GPT2-small 来举例(12transformer,每层 12 个头,每个头的 dim = 64
      • 假设 virtual_prompt_seq_len=3input_prompt_seq_len=10
      • 那么需要先初始化 past_key_valuesshape = [12, 2, 12, 3, 64],分别表示:
        • num_layers
        • key and value
        • num_heads
        • virtual_prompt_seq_len
        • dim
        • shape = [12, 2, 12, 3, 64] 以及修改后的输出层参数是可训练的所有参数
    • 然后将序列长度为 10input token embeddings 输入模型,第一层输出长度还是 10
    • 第二层以及之后的每一层都将上一层输出的长度为 10 的序列和长度为 3virtual_prompt_key_values 合并计算,并输出长度为 10 的序列

2. 不再使用 Verbalizer,而是使用 class head

  1. 什么是 Verbalizer ?
    • 传统的预训练语言模型(例如 Bert)的输入是一个 token 序列,输出是一个 token,也就是说词表中每个词都有可能输出
    • 现在有个下游任务需要用 Bert 做情感分类,输入是一段话,输出是:{正面,负面,中性} 中的一种,而且用 P-Tuning 方法微调,那么直接把输入附加上一些虚拟连续提示嵌入,输出的结果还是整个词表,不是三分类
    • 这时候就需要 Verbalizer 的存在了,它的作用是将 Bert 模型的输出从词表空间映射到三分类空间,它的实现形式可以是规则,也可以是深度学习模型
  2. P-Tuning V2 如何抛弃 Verbalizer?
    • 抛弃 Verbalizer 的方式很简单,就是打破 Prompt Tuning 模型时不应修改模型参数和结构 的限制
    • 直接删除预训练模型输出层,改成任务相关的层并随机初始化,然后微调

Thought

  • 看起来比 P-Tuning v2 更优雅,和 kv cache attention 结合起来,推理耗时增加较小
  • 据说对大模型来讲,这种方法和 Prompt Tuning 相比并没有显著精度优势(模型参数量小时,设计很重要;模型参数量大时,参数量几乎可以弥补一切设计上的非最优)

URL

TL;DR

  • 本文提出一种 Prompt Tuning 的方法 P-Tuning
  • Prefix TuningPrompt Tuning 这种连续词嵌入作为前缀的方法不同, P-Tuning 把连续词嵌入分段插入到 输入标签 之间

Algorithm

提出问题

  • 模型对人工设计的 Prompt 很敏感(指预训练模型,非大模型),同一个模型同一个数据集,只要稍微改变问题的问法,评测指标就差非常多,如图:
    p-tuning_1.png
  • P-Tuning 可解决此类问题

解决问题

  • 使用连续词嵌入(可训练)和离散词嵌入(不可训练)相结合的方法,做 Prompt Tuning 微调
    p-tuning_2.png
  • 上图左侧是传统全部用离散词嵌入 Prompt 过程
  • 上图右侧是离散词嵌入和连续词嵌入相结合的方法,其中 capticalBritain 两个问题中最关键的词使用离散词嵌入(来自于词表,固定不可训练),并在离散词嵌入周围插入若干连续词嵌入(可通过反向传播梯度下降训练)

数学表述

  • P-Tuning 中输入序列为 T={[P0:i],x,[P(i+1):j],y,[P(j+1),k]}T=\{[P_{0:i}],x,[P_{(i+1):j}],y,[P_{(j+1),k}]\},其中:
    • x 表示原始输入的离散词文本(还没有变成词向量)
    • y 表示原始的 label 文本
    • [P][P] 表示连续词向量
  • 输入序列 T 需要通过一种特殊的 Prompt Encoder 变成真实的词嵌入输入 {h0,...,hi,e(x),hi+1,...,hj,e(y),hj+1,...,hk}\{h_0,...,h_i,e(x),h_{i+1},...,h_j,e(y),h_{j+1},...,h_k\},其中:
    • e(x),e(y)e(x),e(y) 是通过查词表得到的离散词嵌入
    • hh 是通过 MLP/LSTM 等方法得到的连续向量的词嵌入,向量的长度和离散词嵌入一致

Thought

  • Prefix Tuning 插入连续词嵌入的自由度更高,因此理应效果更好,但总感觉解决问题的方法不优雅,因为离散和连续嵌入结合的模板是人为规定的,包含了较多先验知识在里面

URL

TL;DR

  • 本文提出的 prompt tuningprefix tuning 非常相似,是一种通过给不同任务输入前添加不同前缀,同时冻结原预训练模型参数的微调方式
  • prefix tuning 区别主要在前缀词向量的设置和初始化方式方面

Algorithm

prompt_tuning.png

Prompt tuning 的前缀词向量长度应该设置多少?

  • 作者实验了 {1, 5, 20, 100, 150} 等长度的前缀长度,结论是 20 最合适,超过 20 收益可忽略

Prompt tuning 的前缀初始化方式

  • 作者实验了三种前缀初始化方式:
    1. 随机初始化(和 prefix tuning 一致)
    2. 从词表中随机选择常见词初始化
    3. 用自然语言描述任务,并将其根据词表转化为词向量
  • 实验结论是:第三种方式最优

其他部分

  • 我没看出来和 prefix tuning 有任何不同,甚至本文对 prefix tuning 的理解都是错的

Thought

  • 我认为这篇论文在 prefix tuning 的基础上改动较小,比较水