提交 662231c5 编写于 作者: M MaoXianxin

用OpenCV实现条形码识别

上级 88117596
2016年,陈天奇团队提出了亚线性内存优化相关的“gradient/activation checkpointing(后向重计算)”等技术[1],旨在降低深度学习训练过程中的中间激活(activation)带来的显存占用。Checkpointing技术属于亚线性内存优化的一种,除此之外还有CPU offload等技术(CPU offload在微软Deepspeed框架中被广泛使用)。
![](./imgs/28.jpg)
CPU offload将暂时用不到的GPU内存换入到CPU内存中存储,待需要时再取出,主要开销是来自于CPU和GPU之间的拷贝,会占用传输带宽(PCIE带宽),属于以传输换空间。而Checkpointing核心在于以时间换空间:**通过计算图分析技术来实施Inplace操作以及内存共享优化(Memory sharing),在每个mini-batch的前向过程中删除一些暂时用不到的中间激活特征以降低内存占用,并在后向过程中需要时借助额外的前向计算恢复它们。**
在OneFlow中,Checkpointing的实现主要是通过静态内存复用的方式,前向Tensor的生命周期结束后,其余Tensor可以复用这块内存,从而起到内存复用、节省内存的效果。
OneFlow目前支持了“gradient/activation checkpointing”(后向重计算)以实现亚线性内存优化,且对算法开发者非常友好,使用方式很简单:**针对需要优化的网络部分,用一行代码将其包裹在“Checkpointing”的scope范围内即可,系统内部会针对此scope区域内的网络做分析并在训练过程中自动进行Checkpointing内存优化。**
本文主要内容为以下3点:
- 1.亚线性内存优化的用法
- 2.亚线性内存优化的设计
- 3.代码解读
其中:1.将介绍如何在OneFlow中开启试用亚线性内存优化;2.将介绍OneFlow中亚线性内存优化是如何设计的及其原理;3.将从代码入手,剖析具体实现过程。
# 亚线性内存优化的用法
OneFlow中开启亚线性内存优化的方式如下:
```
# 用法:
with flow.experimental.scope.config(checkpointing=True):
# your net work, such as :
# input layernorm
norm1 = layernorm("layernorm_1", h)
# attention
h = h + self.attn(norm1)
# output layernorm
norm2 = layernorm("layernorm_2", h)
# mlp
h = h + self.mlp(norm2)
```
用上述代码包裹后,此scope区域内的网络,在整个前向过程中只会保存一份input tensor的内存,从input到最后输出h,这之间所有中间特征tensor的内存都不会被保存,后向过程需要时从input开始进行(前向的)重计算。
我们在多个网络上进行了开启/关闭checkpointing的显存占用测试,以GPT-2为例,具体是在每个Transformer Layer内都使用`checkpointing = True` scope标记重计算的部分。
![](./imgs/29.png)
可以看见,开启checkpointing后会大幅降低GPT-2训练时的显存占用,在batch size = 4 时,内存节省超过50+%。
# 亚线性内存优化的设计
在系列文章《深度解析:让你掌握OneFlow框架的系统设计(上篇、中篇、下篇)》中,我们介绍了OneFlow中的OpNode/OpGragh抽象以及建立在这之上的Actor、SBP抽象等系统设计,正是这些良好的系统设计和抽象使得OneFlow在多种任务下都有着优秀的表现。
OneFlow的Job任务在逻辑图编译期会基于由OpNode构成的Job逻辑图(OpGragh),进行一系列pass的系统优化过程,每个pass对逻辑图进行了一次图修改/重写(对逻辑图中的节点和连边进行了增删操作),这些优化对性能的提升至关重要。
Activation Checkpointing 在OneFlow中的实现,也是通过一个Checkpointing的pass对Job逻辑图实现修改/重写来实现的(见[https://github.com/Oneflow-Inc/oneflow/pull/3976](https://github.com/Oneflow-Inc/oneflow/pull/3976))。
![](./imgs/30.png)
主要原理
如图所示:
1.上半部分为正常情况下的逻辑子图。T1、T2为Transformer Layer的前向计算部分、子图中每个op计算完成后得到的中间激活特征将持续占用内存,当计算进行到反向时(T1_grad、T2_grad),再利用这些中间激活进行反向的计算;
2.下半部分为开启Activation Checkpointing后的逻辑子图。可以看到,中间部分增加了虚线框住,用于重计算的fake子图,由于fake子图的存在,正常forward子图在进行前向时,就无须保存中间激活了,当backward计算需要用到时,再临时根据fake子图进行前向的重计算。
在OneFlow中,Activation Checkpointing的细节流程如下:
1.收集checkpointing作用域包裹下的所有前向pass下的ops
2.收集ops下所有的子图subgraphs
3.遍历子图subgraphs,并对所有需要做后向的subgraph做如下操作:
- 生成fake子图,并将其作为后向消费者的输入(而不是真实子图)
- 在fake子图中增加由end op连向所有源节点source nodes的控制边
- 将fake子图添加至job builder(被其管理)
4.在job builder中更新所有后向消费者ops
代码实现:
```
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job/checkpointing_config_def.cpp
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job/job_build_and_infer_ctx.cpp#L989
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job_rewriter/checkpointing_pass.cpp
```
# 代码解读
## 收集所有前向pass下的ops
由于activation checkpointing的设计主要是节省前向计算过程中的内存,即在checkpointing作用范围内,将前向计算过程中op nodes产生的activation显存释放掉,后向backward时,重新进行此部分的前向计算,得到所需的activation。
由于我们所有的操作是在逻辑图层面,操作的对象为每个op node节点,所以首先需要标记、筛选出checkpointing作用范围内所有前向的op nodes。此部分主要通过CollectAllCheckpointingOpsInForwardPass()方法实现:
```
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job_rewriter/checkpointing_pass.cpp#L65
```
```
void CollectAllCheckpointingOpsInForwardPass(
// 收集所有属于前向pass下,且符合条件的op nodes,存放至HashMap中
const OpGraph& op_graph, HashMap<std::string, const OpNode*>* checkpointing_op_name2op_node) {
// NOTE(chengcheng):
// ignore batch_norm ops because of recompute bn will repeat the calculation of 'm' and 'v'.
// in the future, we need to support the recomputation version of batch_norm which do NOT
// update forward variables.
HashSet<std::string> ignore_op_type_names = {"normalization", "normalization_add_relu",
"cudnn_fused_normalization_add_relu"};
op_graph.ForEachNode([&](const OpNode* op_node) {
const OperatorConf& op_conf = op_node->op().op_conf();
// 跳过不包含user_conf以及ignore_op_type_names指定的op_node
if (!op_conf.has_user_conf()) { return; }
if (ignore_op_type_names.find(op_conf.user_conf().op_type_name())
!= ignore_op_type_names.end()) {
return;
}
// 对scope范围内开启了checkpointing且标记为ForwardPass的op_node,则为目标node,将其插入HashMap中
if (IsForwardPass7CheckpointingScope(Scope4OpNode(op_node))) {
CHECK(checkpointing_op_name2op_node->emplace(op_conf.name(), op_node).second);
}
});
}
```
其中,主要通过`IsForwardPass7CheckpointingScope()`方法,来对符合条件的op node进行筛选:
```
bool IsForwardPassScope(const Scope& scope) {
// scope中,calculation_pass_name属性为kForwardPass的node,则为参与前向计算的目标node
return scope.scope_proto().calculation_pass_name() == kForwardPass;
}
bool IsForwardPass7CheckpointingScope(const Scope& scope) {
// True if 属性为kForwardPass的node且scope开启了checkpointing
return IsForwardPassScope(scope) && scope.Bool("checkpointing");
}
```
`IsForwardPass7CheckpointingScope()`方法通过node的scope来判断该op node是否属于直接参与前向计算的node(scope中包含kForwardPass),且是否开启了“checkpointing”,同时满足则为目标node,将其插入hashmap(checkpointing_op_name2op_node)中。
## 收集ops下所有的subgraphs
筛选出checkpointing作用区域内所有的op nodes后,需要根据这些nodes生成所有子图subgraghs,这些子图有些是和后向重计算无关、有些则是后向重计算所需的目标子图,它们的输出作为后向op node的输入被消费,这些子图是实现activation checkpointing设计中前向重计算的最小单位。
生成子图的代码如下:
```
// 根据ops生成所有subgraphs子图,并将其存放在vector中
// step 2. get all connected subgraphs in checkpointing ops.
std::vector<HashSet<const OpNode*>> checkpointing_subgraphs;
GenConnectedCheckpointingSubgraphs(checkpointing_op_name2op_node, &checkpointing_subgraphs);
```
其中,主要通过GenConnectedCheckpointingSubgraphs()方法生成subgraphs:
```
void GenConnectedCheckpointingSubgraphs(
// 生成Subgraphs子图
const HashMap<std::string, const OpNode*>& checkpointing_op_name2op_node,
std::vector<HashSet<const OpNode*>>* checkpointing_subgraphs) {
HashSet<const OpNode*> visited_nodes;
for (const auto& pair : checkpointing_op_name2op_node) {
const OpNode* node = pair.second;
if (visited_nodes.find(node) != visited_nodes.end()) { continue; }
// new subgraph
checkpointing_subgraphs->push_back(HashSet<const OpNode*>());
CHECK(!checkpointing_subgraphs->empty());
auto& subgraph = checkpointing_subgraphs->back();
CHECK(subgraph.empty());
// bfs search all node in checkpointing ops
CHECK(visited_nodes.insert(node).second);
std::queue<const OpNode*> queued_nodes;
queued_nodes.push(node);
while (!queued_nodes.empty()) {
const OpNode* cur_node = queued_nodes.front();
queued_nodes.pop();
CHECK(subgraph.insert(cur_node).second);
cur_node->ForEachNodeOnInOutEdge([&](const OpNode* next_node) {
const std::string& next_op_name = next_node->op().op_name();
if (checkpointing_op_name2op_node.find(next_op_name) != checkpointing_op_name2op_node.end()
&& cur_node->parallel_desc() == next_node->parallel_desc()
&& visited_nodes.find(next_node) == visited_nodes.end()) {
queued_nodes.push(next_node);
CHECK(visited_nodes.insert(next_node).second);
}
});
}
}
}
```
根据当前节点(cur_node),找到下一个子图节点(next_node),采用的是BFS搜索,搜索逻辑为:以cur_node为起点,遍历其输入/输入边上有消费关系的节点next_node;对于不属于checkpointing op && 没有被当作子图node访问过 && 并行方式和cur_node一致的node;作为subgraph中的目标node(next_node),插入subgraph队列中,并将该node标记为已访问,放置到visited_nodes Set中。
## 遍历子图subgraphs
经历上述过程后生成了子图vector(),我们需要对其进行遍历,筛选出和activation checkpointing相关的子图subgraghs,并做如下几件事:
- 生成fake子图,并将其作为后向消费者的输入(而不是真实子图)
- 在fake子图中增加由end op连向所有源节点source nodes的控制边
- 将fake子图添加至job builder(被其管理)
对子图subgraghs的遍历主要在:
```
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job_rewriter/checkpointing_pass.cpp#L148-L290
```
过滤和activation checkpointing无关的subgragh子图
`[for (auto& subgraph : checkpointing_subgraphs) {}]()`遍历循环的一开始,就会跳过不符合activation checkpointing条件的subgragh
```
for (auto& subgraph : checkpointing_subgraphs) {
// step 3.1 ignore this subgraph if there is no direct edge to backward pass op.
HashSet<const OpNode*> bw_consumers;
for (const OpNode* node : subgraph) {
node->ForEachNodeOnOutEdge([&](const OpNode* out_node) {
if (!IsForwardPassScope(Scope4OpNode(out_node))) {
bw_consumers.insert(out_node);
CHECK(subgraph.find(out_node) == subgraph.end());
}
});
}
if (bw_consumers.empty()) { continue; }
```
具体条件,即遍历subgraph子图中的所有node节点、判断node节点的所有出边out_edges是否有出边连到后向backward消费者op,如果subgraph中所有节点均没有连到后向backward消费者,则跳过该子图(表明该子图只有只和forward有关而和backward无关,即不是activation checkpointing优化的目标子图。
生成fake子图,并将其作为后向消费者的输入(而不是真实子图)
过滤掉无效子图后,对于和activation checkpointing直接相关的目标子图,我们需要生成fake子图,其中的每个节点由fake op构成。
fake子图,即重计算的最小单位,其作用即用于取代原有真实的子图、并在后面替换这些真实子图,用于被后向op nodes消费。通过将fake子图中fake op的scope属性从kForwardPass变为kBackwardPass,实现当计算进行到该fake op时,重新运行前向计算以产生backward所需的activation数据。
生成fake 子图的主要代码在:L168-L222
```
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job_rewriter/checkpointing_pass.cpp#L168-L222
```
生成fake子图后,将其设置为backward消费者的输入,主要代码如下:
```
const OpNode* first_bw_consumer = nullptr;
int32_t first_bw_order = std::numeric_limits<int32_t>::max();
// 将backward消费者的input更改为fake子图op(而不是真实子图)
// step 3.3 change bw consumers input from subgraph to fake subgraph
for (const OpNode* node : bw_consumers) {
std::string bw_consumer_name = node->op().op_name();
OperatorConf bw_consumer_op_conf;
// NOTE(chengcheng):
// reuse bw conumer op conf if it has been existed in map.
if (total_bw_consumers_op_name2conf.find(bw_consumer_name)
!= total_bw_consumers_op_name2conf.end()) {
bw_consumer_op_conf = total_bw_consumers_op_name2conf.at(bw_consumer_name);
} else {
bw_consumer_op_conf = node->op().op_conf();
}
CHECK_EQ(bw_consumer_name, bw_consumer_op_conf.name());
auto* user_conf = bw_consumer_op_conf.mutable_user_conf();
// 修改和subgragh相关的backward op输入的blob name
// change input lbns if in subgraph
for (auto& pair : *(user_conf->mutable_input())) {
auto& list_s = pair.second;
for (int i = 0; i < list_s.s_size(); ++i) {
std::string old_lbn = list_s.s(i);
LogicalBlobId old_lbi = GenLogicalBlobId(old_lbn);
std::string old_input_op_name = old_lbi.op_name();
if (subgraph_op_name2op_node.find(old_input_op_name) != subgraph_op_name2op_node.end()) {
list_s.set_s(i, kCheckpointingFakeOpNamePrefix + old_lbn);
}
}
}
// NOTE(chengcheng):
// emplace maybe repeated, so do not check the return value
total_bw_consumers_op_name2conf.emplace(bw_consumer_name, bw_consumer_op_conf);
CHECK(op_node2order.find(node) != op_node2order.end());
int32_t this_order = op_node2order.at(node);
if (this_order < first_bw_order) {
first_bw_consumer = node;
first_bw_order = this_order;
}
}
```
在fake子图中为所有source node—end node添加控制边
这一步操作的目的主要将子图subgraph中所有和backward op相连的node(source node),添加一条控制边。控制边的添加是人为控制node间执行的时序,控制边保证了fake子图的计算尽可能晚的发生,这样才能缩短生命周期,保证内存复用的效率。
添加控制边相关的代码在L267-L284:
```
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/job_rewriter/checkpointing_pass.cpp#L267-L284
```
```
// step 3.4 add control edge from End Op to all source node in fake subgraph
CHECK(first_bw_consumer != nullptr);
std::string end_op_name = kCheckpointingBadOpName;
int32_t end_order = -1;
first_bw_consumer->ForEachNodeOnInEdge([&](const OpNode* end_node) {
CHECK(op_node2order.find(end_node) != op_node2order.end());
int32_t this_order = op_node2order.at(end_node);
if (this_order > end_order) {
end_order = this_order;
end_op_name = end_node->op().op_name();
}
});
CHECK_NE(end_order, -1);
CHECK_NE(end_op_name, kCheckpointingBadOpName);
CHECK_LT(end_order, first_bw_order);
for (const auto& source_op_name : source_node_in_fake_subgraph) {
fake_op_name2conf.at(source_op_name).add_ctrl_in_op_name(end_op_name);
}
```
将fake子图添加至job build(被其管理)
fake subgraphs的生成、以及控制边的添加,实际上对原有job逻辑图产生了改动。改动后,需要将fake subgraphs中这些新生成的fake op nodes添加至job builder管理,正式完成了逻辑图的图改写。
主要代码如下:
```
// 将fake subgraph所包含的ops加入至job_builder管理(图改写)
// step 3.5 add fake subgraph ops to job builder
std::vector<OperatorConf> fake_op_confs;
for (auto& pair : fake_op_name2conf) { fake_op_confs.push_back(pair.second); }
job_builder->AddOps(parallel_conf, fake_op_confs);
```
## 更新所有后向消费者ops
最后,由于fake op nodes更新了backward op nodes的输入输出等属性,需要将更新后的backward op nodes同步至job_builder管理:
```
// 在job builder中更新所有backward ops
// step 4. update bw consumers in job builder only once
std::vector<OperatorConf> total_bw_consumer_op_confs;
for (auto& pair : total_bw_consumers_op_name2conf) {
total_bw_consumer_op_confs.push_back(pair.second);
}
job_builder->MutOpsOnlyOnce(total_bw_consumer_op_confs);
return Maybe<void>::Ok();
```
至此,通过这些fake subgraphs的插入、输入输出连边的变动等完成了整个job逻辑图的改写,改写后的逻辑图执行时即自动支持了activation checkpointing。
OneFlow最近复现了GPT-3相关的工作,其中就使用了activation checkpointing的技术,**代码在OneFlow-Benchmark已开源,欢迎在GitHub下载试用:**
```
https://github.com/Oneflow-Inc/OneFlow-Benchmark/tree/master/LanguageModeling/GPT
```
参考文献
[1] Tianqi Chen, Bing Xu, Chiyuan Zhang, and Carlos Guestrin. Training Deep Nets with Sublinear Memory Cost. arXiv preprint arXiv:1604.06174, 2016.
\ No newline at end of file
张建浩,网名大缺弦(人称“大老师”),2018年毕业于中国科学技术大学,ONNX 成员,convertmodel.com、dabnn、DNNLibrary 开源项目作者,现为一流科技工程师。
大学时,他曾在 GitHub 发布了一个 Android 控件,意外收获了 300 多个 Star,从此他开始深度探索开源世界,成为数个开源项目作者。一次,他在听完OneFlow创始人袁进辉的讲座后,觉得 OneFlow 很硬核,但当时自己做的工作和深度学习框架开发没什么关系。2020 年 4 月,在一次“惊险的面试”后,他选择加入 OneFlow。
本文为张建浩自述。
# 大学接触开源后不能自拔
我是在中国科学技术大学读的本科,当时 Android 开发很火,我就是在那时开始接触开源项目,逐渐不能自拔。
2016年,在大三上学期的时候,我给超理论坛(理科生交流论坛)做 Android App,有显示表情、图片、公式、文本样式的富文本框的需求,当时也在上编译原理课,就基于递归下降法做了一个解析各种 tag 的 Android 控件。在发布到 GitHub 上之后,当时登上了 GitHub Trending(当日最火的 GitHub 项目之一),意外收获了 300 多个 Star。
后面还听了学长的一个分享,知道有 Google 组织的 Summer of Code 活动,由学生给开源项目贡献代码并获得报酬,其实就相当于远程实习。经过和 Mentor 套近乎、给目标项目提交 PR 刷存在感,我也顺利成为中国大陆五十个左右的入选候选人之一,并开始为 Android 系统上的老牌开源邮件客户端 K-9 Mail 增加新特性,当时一同入选的小伙伴们有一些现在还有联系。
到了大四,我去 MSRA 实习,做了一些和神经网络移动端部署相关的工作。后来,毕业设计做的“设计轻量级的人体关键点模型并在移动端部署”也是相关的工作,为此还给 PyTorch 和 Detectron 分别提交了几个 PR。这期间我在端侧部署圈混了个脸熟,有一次还被端侧部署框架 Tengine 团队邀请参加了他们的讨论。
大学和实习期间做开源项目的经历,一方面让我提升了编程能力,因为写完就能把程序跑起来看效果,很快可以带来正反馈,另一方面,做开源项目也会提升自己在业界的知名度。
对于计算机相关专业的学生,如果毕业后是想进入工业界,那就应该多培养自己的工程能力,这是一个很多学生缺少,但工作时却很看重的能力,参与开源项目显然是一个很好的提升方式。
当然,虽然不停地写代码感觉会很爽,但这很容易导致在舒适圈里无意义的重复,技术水平不会有大的提升。我建议工程经验积累到一定程度之后就去看一些经典的、专业的书(SICP 之类),把积累的工程经验升华到更高的境界。
# 两个有成就感的开源项目
2018 年毕业之后,我去了京东 AI 工作。得益于上级领导的支持,我用工作时间做了一些和部门主线无关的开源项目,有两个主要的项目都是深度学习推理框架。例如 dabnn,它是业界第一个高效的二值网络开源部署框架。这个工作发表在多媒体顶会 ACM MM 的 Open Source Software Competition 上 ([https://arxiv.org/abs/1908.05858](https://arxiv.org/abs/1908.05858))。
在这个工作之前,二值网络研究者们只知道二值网络的理论加速比,但对二值网络在真实世界的性能无从知晓。有了这个工作之后,研究者们 ([https://arxiv.org/abs/1909.10788](https://arxiv.org/abs/1909.10788))就可以报告他们设计的二值网络在 CPU 上的实际速度。在 dabnn 之后,还有一些工作(如 larq 和华为的 bolt)继续在二值网络部署这条路上前进。
此外,我的另一个开源项目 DNNLibrary,一个使用 Android 神经网络 API(NNAPI) 在移动端部署 ONNX 模型的库,当时吸引了微软 ONNX Runtime 团队的合作。ONNX Runtime 最初的 Android 适配,以及当时 Preview 版本的 NNAPI 支持都是由我完成。
借这次合作,我融入了 ONNX 社区,当时为了消除 ONNX 模型中的冗余 op 花了一天时间实现的 onnx-simplifier ,现在已经成了我最高的 Star 数项目(1.3k),被 YOLOv5、TNN 等项目官方集成,NCNN、MXNet 和 TensorRT 的开发者也推荐用户使用这个库优化掉它们不支持的 op。
我给 ONNX 贡献了 Resize、Softmax 等 op 的 spec( [https://zhuanlan.zhihu.com/p/107761106](https://zhuanlan.zhihu.com/p/107761106)),成为了 ONNX Operator SIG 的成员。现在我还在维护着 ONNX 官方的 ONNX Optimizer ( [https://zhuanlan.zhihu.com/p/350702340](https://zhuanlan.zhihu.com/p/350702340))。
到 OneFlow 之后,我对 OneFlow 和 ONNX 之间的模型转换工作也做了许多贡献。
# 智力密度更大、更专注的 OneFlow 团队
以前在京东 AI 时,袁进辉老师来做过一次讲座,当时觉得 OneFlow 很硬核,但和自己没关系。
加入 OneFlow 的过程很“惊险”。当时我在找工作,并已经拿到了两个 Offer。在距离回复是否接受 Offer 的前一天,袁老师看到我在知乎上写的一篇技术分享文章,他私信问我,有没有空聊一聊。得知我在找工作之后,他说可以考虑 Oneflow,可我第二天就得决定是否接受其他公司的 Offer,所以来不及面试 OneFlow 了。
袁老师说,不用面试。跟他聊了一番之后,就给了 Offer。我比较了几个 Offer 的薪资、工作内容、工作时间(编者注:OneFlow 可以远程工作)等各种因素,就决定来 OneFlow,可能这就是缘分。
我在 OneFlow 的工作内容是做框架开发,除了做 OneFlow 和 ONNX 之间的模型转换外,我还做了模型加载保存机制的重构、量化训练、多设备支持等,现在在参与新接口的开发。
在大厂时,我的大部分时间都花在各种业务上,难以深入钻研技术,上面说的两个开源推理框架项目,也是忙里偷闲的维护。在 OneFlow 工作,做的就是自己爱好的事情,我觉得小伙伴们的智力密度要大一些,高阶的开发者也更多,对技术更加专注,做的事情也很有意思。
在 OneFlow 社区建设上,我希望我们可以用心对待用户提出的每一个问题,并对问题或者提交的 PR 及时做出反馈,这对留住用户非常重要。
基础软件国产化是大趋势,OneFlow 在分布式训练上掌握了核心技术,也有纯粹的、能力强的技术团队,所以未来会很有前景。
个人有什么职业规划?我没有什么宏大的规划,还有很多知识没有学完,希望在相关技术上继续做的更深入。对了,我正在 Telegram 上连载 TAPL 读书笔记,欢迎对 PL 感兴趣的朋友跟我交流:[https://t.me/daquexiannotebook](https://t.me/daquexiannotebook)
\ No newline at end of file
# 傅里叶变换取代Transformer自注意力层,谷歌这项研究GPU上快7倍、TPU上快2倍
来自谷歌的研究团队表明,将傅里叶变换取代 transformer 自监督子层,可以在 GLUE 基准测试中实现 92% 的准确率,在 GPU 上的训练时间快 7 倍,在 TPU 上的训练时间快 2 倍。
Transformer 自 2017 年推出以来,其架构就开始在 NLP 领域占据主导地位。Transformer 应用的唯一限制之一,即 Transformer 关键组件的巨大计算开销–一种自注意力机制,这种机制可以根据序列长度以二次复杂度进行扩展。
基于此,来自谷歌的研究者建议用简单的线性变换替代自注意力子层,该线性变换「混合」输入 token,以较小的准确率成本损失显著的提高了 transformer 编码器速度。更令人惊讶的是,研究者发现采用标准的、非参数化的傅里叶变换替代自注意力子层,可以在 GLUE 基准测试中实现 92% 的 BERT 准确率,在 GPU 上的训练时间快 7 倍,在 TPU 上的训练时间快 2 倍。
![](./imgs/1.jpg)
论文链接:[https://arxiv.org/pdf/2105.03824.pdf](https://arxiv.org/pdf/2105.03824.pdf)
该研究的主要贡献包括:
- 通过用标准的非参数化傅里叶变换代替注意力子层,FNet 在 GLUE 基准测试中实现 92% 的 BERT 准确率,在 GPU 上的训练时间快 7 倍,在 TPU 上的训练时间快 2 倍。
- 仅包含两个自注意子层的 FNet 混合模型在 GLUE 基准上可达到 97%的 BERT 准确率,但在 GPU 上的训练速度快近 6 倍,而在 TPU 上则是 2 倍。
- FNet 在「Long Range Arena」基准评估中,与所有的高效 transformer 具有竞争力,同时在所有序列长度上拥有更少的内存占用。
Transformer 自注意力机制使得输入可以用高阶单元表示,从而可以灵活地捕获自然语言中各种语法和语义关系。长期以来,研究人员一直认为,与 Transformer 相关的高复杂性和内存占用量是不可避免的提高性能的折衷方案。但是在本论文中,Google 团队用 FNet 挑战了这一思想,FNet 是一种新颖的模型,在速度、内存占用量和准确率之间取得了很好的平衡。
![](./imgs/2.jpg)
# 借助Transformer,DeepMind新模型自动生成CAD草图,网友:建筑设计要起飞了
深度学习的灵活性恰好适合于复杂的 CAD 设计,DeepMind 的研究者基于 CAD 草图与自然语言建模的相似性,提出了自动生成 CAD 草图的机器学习模型。
在制造业中,CAD 的应用十分广泛。凭借着精准、灵活、快速的特性,CAD 已经取代了纸笔画图,并且不再只是应用于汽车制造、航空航天等领域,哪怕小到一个咖啡杯,生活中几乎每个物件都由 CAD 画图建模。
CAD 模型中最难制作的部件之一就是高度结构化的 2D 草图,即每一个 3D 构造的核心。尽管时代不同了,但 CAD 工程师仍然需要多年的培训和经验,并且像纸笔画图设计的前辈们一样关注所有的设计细节。下一步,CAD 技术将融合机器学习技术来自动化可预测的设计任务,使工程师可以专注于更大层面的任务,以更少的精力来打造更好的设计。
在最近的一项研究中,DeepMind 提出了一种机器学习模型,能够自动生成此类草图,且结合了通用语言建模技术以及现成的数据序列化协议,具有足够的灵活性来适应各领域的复杂性,并且对于无条件合成和图像到草图的转换都表现良好。
![](./imgs/3.png)
论文链接:[https://arxiv.org/pdf/2105.02769.pdf](https://arxiv.org/pdf/2105.02769.pdf)
具体而言,研究者开展了以下工作:
- 使用 PB(Protocol Buffer)设计了一种描述结构化对象的方法,并展示了其在自然 CAD 草图领域的灵活性;
- 从最近的语言建模消除冗余数据中吸取灵感,提出了几种捕捉序列化 PB 对象分布的技术;
- 使用超过 470 万精心预处理的参数化 CAD 草图作为数据集,并使用此数据集来验证提出的生成模型。事实上,无论是在训练数据量还是模型能力方面,实际的实验规模都比这更多。
CAD 草图展示效果图如下:
![](./imgs/4.gif)
对于 DeepMind 的这项研究,网友的评价非常高。用户 @Theodore Galanos 表示:「非常棒的解决方案。我曾使用 SketchGraphs 作为多模态模型的候选方案,但序列的格式和长度太不容易处理了。等不及在建筑设计中也使用这种方法了。」
# 为了医疗AI,他们做出了一个“违背祖师爷”的决定
搞深度学习的人,应该感谢“祖师爷”Geoffrey Hinton在2012年的尝试。因为Hinton团队使用了GPU进行AI运算加速,让神经网络AlexNet的实现成为可能,开启了深度学习大爆发的时代。Hinton彻底带火了GPU,带火了以计算机视觉为主的医疗AI。连Hinton本人也对AI辅助医疗技术将会迅速成熟充满了信心。但是,Hinton可能万万没想到,现在搞医疗AI的人,尤其是落地部署和使用医疗AI的人,却做出了一个违背“祖师爷”的决定:
用CPU来做医学影像领域的辅助诊断推理。
那么,问题来了,在GPU原本擅长的领域,CPU的能力真的可以胜任吗?
事实证明,只要软硬件配合得当,CPU一样可以实现出色的AI应用效果,尤其是进行准确、快速的AI推理。性能优化到位的话,在这方面的表现就可与GPU相媲美。 当然,CPU也仅仅是AI计算硬件中的一种,而非唯一的选择。可它越来越受到医疗AI领域的青睐,必然有更为深刻的原因:那便是惠民。
虽然已经有了很多实例,目前仍然有许多人对CPU做AI抱有疑虑。而这些疑虑,主要集中在计算速度和生态系统这两点上:但英特尔,早已为CPU在AI行业的应用,做好了软硬件两手准备。一方面,针对AI应用的算力和数据加速,英特尔早从数年前就开始布局,从硬件架构上对AI推进了三方面的优化:
- 在2017年发布的第一代至强可扩展处理器上,导入支持AVX-512高级矢量扩展技术,让CPU单位时间内能处理更多浮点运算任务,用以加速高精度的AI应用;
- 在2019年发布的第二代至强可扩展处理器上,基于AVX-512技术扩展出了英特尔深度学习加速(DL Boost)技术,支持INT8加速,主攻推理加速;
- 2020年发布的面向四路和八路服务器的第三代至强可扩展处理器时,为这项技术增添了BF16加速能力,从而兼顾推理和训练的加速;
- 在2019年推出傲腾持久内存,兼具接近DRAM内存的高性能,以及DRAM内存所不具备的容量、价格和数据非易失优势,让基于CPU的AI系统可以将更大体量的数据缓存在距离CPU更近的地方,加速训练和推理。
如今看来,用CPU做AI,确实没啥可担心的——甚至在医疗AI行业,这还是个更好的选择。
大概,就连当年积极将GPU引入医疗AI行业的Hinton,也没预料到会是这幅景象:如今的CPU,正在医疗AI领域大放异彩。
# 双方互GAN,不如来试试群体博弈?更快更强更自由 | ICLR 2021
AI诞生之初,很长一段时间都停留在个体智能阶段,即面向“识别出图像中的内容”、“听懂一段语音”、“预测蛋白质的3D结构”这类目标单一的任务。但随着技术发展,AI开始逐渐与其他智能群体产生交互,走向更加复杂的应用场景。比如在2019年,DeepMind训练的游戏类AI就在星际争霸中超越了99.8%的活跃玩家,达到了最高的 Grandmaster段位。
这是一种以博弈论为机器学习模型的思路。沿着这种思路,DeepMind的几位研究者提出了一种名为EigenGame的理论,重新定义了PCA(主成分分析,Principal Component Analysis)方法。
## “Eigen”与“Game”
“Eigen”意为特征。也是论文题目中所提到的PCA方法的核心概念。
![](./imgs/5.jpg)
PCA是ML领域里用于数据降维的经典方法。目的是在尽可能不丢失原数据信息的前提下,将高维数据映射(压缩)到低维空间,得到剔除了原有特征冗余信息的新特征。
![](./imgs/6.jpg)
在目标数据集的数据属性过多,数据量过大的时候,模型训练和计算的性能可能受到很大影响。这时,就需要用到PCA进行数据降维。而“Game”则意为博弈论*(Game Theory)*,是一种研究理性决策者之间的冲突与合作的数学模型。纳什均衡(Nash equilibrium)作为博弈论的一个重要概念,指每个参与人在获取信息不完全的情况下,做出了针对其他参与人策略的最优反应,比如经典的囚徒困境。
## 当博弈论遇上机器学习
其实,博弈论的思想一直存在于很多机器学习的探索过程中,不管是经典的 SVM,还是大火的 GAN,这些模型的背后都有博弈论的影子。
最初提出“博弈机器学习”这一概念的刘铁岩博士曾这样说过:
什么才是人工智能?想要解决这个问题,首先需要为「智能」提出一个定义。如果说过去对于个体智能的研究为计算机赋予了智商(IQ)的话,那么社会智能则对应着人工智能的情商(EQ)。
博弈论的引入让AI在过去只与环境交互的基础上,又学会了如何与其他智能体打交道。而当EigenGame这种与多智能群体交互的算法出现时,其意义就不仅是更多更快的数据运算。按照既非随机,也非理性和对立的人类的行为规律去训练建模,那么AI就有了更多解决问题的新角度,也能在广告竞价、社交媒体、众包管理、交通疏导等多个领域中得到更广泛的应用。
或许,博弈论会是连接机器学习走向人和社会的一个桥梁。
\ No newline at end of file
# 企业技术文章精选
# 用OpenCV实现条形码识别
**觉得不错的话别忘了点个Star哈**
最近,我们为OpenCV贡献了一维条形码识别模块,代码收录在:https://github.com/opencv/opencv_contrib/tree/master/modules/barcode
**欢迎大家一起来协作,该页底部可以扫码入群,来群里和我直接交流吧!!!**
我们收集的数据集(数据集地址:https://github.com/SUSTech-OpenCV/BarcodeTestDataset,共250张条码图片)上进行了测试,我们的识别算法正确率达到了**96%**,速度为**20ms**每张图像。作为对比,我们也测试了ZXing在该数据集上的表现,其正确率为**64.4%**,速度为**90ms**每张图像。
## OneFlow
注:测试速度不包含初始化以及读图时间。同时,我们的算法是C++实现,ZXing是Java实现。另外,对于用图片数据集进行的测试,ZXing99%的时间是在做彩色图到灰度图的转换。
### 2021-05-27
本文将对此模块的原理和使用方式进行介绍。
[后向重计算在OneFlow中的实现:以时间换空间,大幅降低显存占用-2021-05-20](./OneFlow/后向重计算在OneFlow中的实现:以时间换空间,大幅降低显存占用-2021-05-20.md)
## 条形码介绍
### 2021-05-26
条形码是将宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符,如下图所示:
[TensorFlow和PyTorch迎来了后浪-2020-08-02](./OneFlow/TensorFlow和PyTorch迎来了后浪-2020-08-02.md)
![](./imgs/1.png)
### 2021-05-25
## 基于方向一致性的条码定位算法
[张建浩:一个开源爱好者的框架开发之路 OneFlow U-2021-05-21](./OneFlow/张建浩:一个开源爱好者的框架开发之路 OneFlow U-2021-05-21.md)
根据条形码方向趋于一致的特点,我们可以将图像分块,通过计算每个块内**梯度方向的一致性**,来滤除那些**低一致性**的块。下图是筛选过后剩下的块:
往期精彩链接: [点击这里](./OneFlow)
![](./imgs/2.png)
## PaddlePaddle
由于包含条码区域的块**一定连续存在**的特性,我们可以通过对这些图像块再进行一个改进的**腐蚀**操作过滤掉部分背景图像块。下图是滤除部分背景图像块后剩余的块:
### 2021-05-24
![](./imgs/3.png)
[本周AI热点回顾:傅里叶变换取代Transformer,GPU上快7倍、TPU上快2倍;DeepMind新模型自动生成CAD草图-2021-05-23](./PaddlePaddle/本周AI热点回顾:傅里叶变换取代Transformer,GPU上快7倍、TPU上快2倍;DeepMind新模型自动生成CAD草图-2021-05-23.md)
得到这些块之后,我们再根据每个图像块内的**平均梯度方向进行连通**。因为如果是相邻的图像块都属于同一个条码的话,那么他们的平均梯度方向也一定相同。
往期精彩链接: [点击这里](./PaddlePaddle)
得到连通区域之后我们再根据条码图像的特性进行筛选,比如连通区域内的梯度大于阈值的点的比例,组成连通区域的图像块数量等。
![](1.png)
最后,用**最小外接矩形**去拟合每个连通区域,并计算外界矩形的方向是否和连通区域内的平均梯度方向一致,过滤掉差距较大的连通区域。将平均梯度方向作为矩形的方向,并将矩形作为最终的定位框。
![](./imgs/4.png)
## 条形码解码
目前我们支持了三种类型的条码解码,它们分别是EAN13、 EAN8 和UPC-A。(下图为EAN13 条码示例)
![](./imgs/5.png)
条码的识别主要流程如下图:
![](./imgs/6.png)
其中:
1. 优化的超分辨率策略指的是对较小的条码进行**超分辨率放大**,不同大小条码做不同处理。
2. 解码算法的核心是基于条码编码方式的**向量距离计算**。因为条码的编码格式为固定的数个"条空",所以可以在约定好"条空"间隔之后。将固定的条空读取为一个向量,接下来与约定好的编码格式向匹配,取匹配程度最高的编码为结果。
3. 在解码步骤中,解码的单位为一条线,由于噪点,条空的粘连等原因,单独条码的解码结果存在较大的不确定性,因此我们加入了对**多条线的扫码**,通过对均匀分布的扫描与解码,能够将二值化过程中的一些不完美之处加以抹除。
**具体实现为**:首先在检测线上寻找起始符,寻找到起始符之后,对前半部分进行读取与解码,接着寻找中间分割符,接着对后半部分进行读取与解码,最后寻找终结符,并对整个条码进行首位生成与校验(此处以EAN13格式举例,不同格式不尽相同)。最后,每条线都会存在一个解码结果,所以对其进行投票,只将最高且总比例在有效结果50%以上的结果返回。这一部分我们基于ZXing的算法实现做了一些改进(投票等)。
4. **更换二值化和解码器**指的是在为解码成功遍历使用每种解码器和二值化尝试解码。
## 使用方式
C++
```
#include "opencv2/barcode.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;
Ptr<barcode::BarcodeDetector> bardet = makePtr<barcode::BarcodeDetector>("sr.prototxt", "sr.caffemodel"); //如果不使用超分辨率则可以不指定模型路径
Mat input = imread("your file path");
Mat corners; //返回的检测框的四个角点坐标,如果检测到N个条码,那么维度应该是[N][4][2]
std::vector<std::string> decoded_info; //返回的解码结果,如果解码失败,则为空string
std::vector<barcode::BarcodeType> decoded_format; //返回的条码类型,如果解码失败,则为BarcodeType::NONE
bool ok = bardet->detectAndDecode(input, decoded_info, decoded_format, corners);
```
Python
```
import cv2
bardet = cv2.barcode_BarcodeDetector()
img = cv2.imread("your file path")
ok, decoded_info, decoded_type, corners = bardet.detectAndDecode(img)
```
更多使用方式请参考文档:https://docs.opencv.org/master/dc/df7/classcv_1_1barcode_1_1BarcodeDetector.html
## 参考文献
王祥敏,汪国有. 一种基于方向一致性的条码定位算法[EB/OL]. 北京:中国科技论文在线 [2015-04-22]. http://www.paper.edu.cn/releasepaper/content/201504-338
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册