开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

GPT-3 Codex模型学习笔记 导语 1 简介 2 评估

发表于 2022-12-26

导语

OpenAI在去年曾经使用Github上所有公开的Python代码训练了一个code版本的GPT-3,在当时曾引发广泛争议,本文简要记录对该模型的学习笔记。

  • 会议: arxiv
  • 链接: arxiv.org/pdf/2107.03…
  • 学习资料:OpenAI Codex 论文精读【论文精读】,跟李沐学AI,哔哩哔哩

1 简介

本文主要介绍一种在code语料库上进行预训练的GPT-3模型。单纯的GPT-3在代码生成任务上表现性能为0,为此,作者设计了CodeX模型,它主要有三个不同的类别:

  • CodeX:在所有Github上公开的Python文件上进行训练,由docstring生成对应的函数;
  • CodeX-S:在CodeX基础上,选取了一些高质量、独立的函数模型进行Fine-tune,也是做由docstring生成对应的Python函数代码的任务;
  • CodeX-D:与CodeX任务相反,由Python函数代码生成docstring。

image.png

2 评估框架

主流的Seq2seq任务的评估框架是BLEU score,然而,这是一种模糊匹配的相似度衡量,对于代码生成任务,哪怕错一个字符,就会带来完全不同的结果,因此,作者提出了一种用于评估Python代码生成的指标,即Pass@K,其定义如下:

image.png

大概意思是在生成的k个答案中,只要有一个能够执行正确就算对。而且,在验证时,作者手工进行书写标注了一个新的数据集,这是因为网上的数据很有可能都在训练集中,下图举了几个自己标注的数据集示例:

image.png

3 Code Fine-tuning

作者从Github所有公开的Python文件中经过爬取过滤后得到了最终159GB的训练数据,之后使用这些数据来训练GPT-3。这里作者发现:使用原始GPT-3的weight并不会对最终结果带来提升,但可以加快收敛速度。

image.png

由于最后是有k个输出样例,那么最理想的情况就是这k个样例全部运行一遍,找到最正确的那个作为最终输出,这个就是Oracle的那条性能曲线。

然而,现实中,全部运行k个样例可能是代价很大的,所以作者采用了一种最简单的策略,即对decoder每个时间步生成的token的prob进行log后求和,取最大的那个作为最终输出,这个就是图中红色的去心啊,可以阿看到效果也还可以。

最终的实验结果如下表所示;

image.png

4 Supervised Fine-Tuning

由于之前训练的数据中很多都是一些项目中的文件,还有很多配置文件等,所以这里作者又通过在编程网站和项目集成中的单元提交数据中收集了40000个高质量的数据,将3中得到的模型继续进行Fine-tune,得到所谓的CodeX-S模型,

image.png

可以看到,相比于CodeX,性能又有了很大提升。

5 Docstring Generation

最后,作者还进行了CodeX的反向训练,即通过输入模型Python函数代码,让模型生成docstring,这个模型命名为CodeX-D。

在评估时,作者又将这些docstring输入回3中的CodeX,如果能够预测正确,则表示生成的质量还可以。

image.png

可以看到,经过这样验证后,实际上准确率是有所下降的。

6 局限性

只能处理简单问题,训练代价巨大,文档越长生成效果越差等问题。

7 更广泛的影响

  1. 过度依赖;
  2. 与实际需求不匹配;
  3. 男性偏见(Github男性用户居多);
  4. 程序员失业;
  5. 安全隐患(用来写病毒程序);
  6. 环境影响(训练耗电);
  7. 法律问题(照抄别人的商业代码毫不知情)。

8 相关工作

略

9 总结

CodeX出来之后,引发的争议就一直不断,目前看到相关的使用也比较少

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

玩转ast- 手写babel插件篇

发表于 2022-12-21

AST

抽象语法树是什么?

  • 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示
  • 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
  • 每个包含type属性的数据结构,都是一个AST节点;

以下是普通函数function ast(){},转换为ast后的格式:

ast.png

抽象语法树用途有哪些?

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全、实现一套代码适配多端运行等等
  • 优化变更代码,改变代码结构使达到想要的结构

webpack、Lint等这些工具的原理都是通过 JavaScript Parser 把源代码转化为一颗抽象语法树(AST),通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。

JavaScript解析器

那什么是JavaScript解析器呢?

JavaScript解析器的作用,把JavaScript源码转化为抽象语法树。

浏览器会把js源码通过解析器转换为ast,再进一步转换为字节码或者直接生成机器码,进行渲染和执行。

一般来说,每个js引擎都有自己的抽象语法树格式,Chrome的v8引擎, firfox的Spider Monkey引擎等。

JavaScript解析器通常可以包含四个组成部分。

  • 词法分析器(Lexical Analyser)
  • 语法解析器(Syntax Parser)
  • 字节码生成器(Bytecode generator)
  • 字节码解释器(Bytecode interpreter)

词法分析器(Lexical Analyser)

首先词法分析器会扫描(scanning)代码,将一行行源代码,通过switch case 把源码?“/为一个个小单元,jS代码有哪些语法单元呢?大致有以下这些:

  • 空白:JS中连续的空格、换行、缩进等这些如果不在字符串里,就没有任何实际逻辑意义,所以把连续的空白符直接组合在一起作为一个语法单元。
  • 注释:行注释或块注释,虽然对于人类来说有意义,但是对于计算机来说知道这是个“注释”就行了,并不关心内容,所以直接作为一个不可再拆的语法单元
  • 字符串:对于机器而言,字符串的内容只是会参与计算或展示,里面再细分的内容也是没必要分析的
  • 数字:JS语言里就有16、10、8进制以及科学表达法等数字表达语法,数字也是个具备含义的最小单元
  • 标识符:没有被引号扩起来的连续字符,可包含字母、_、$、及数字(数字不能作为开头)。标识符可能代表一个变量,或者true、false这种内置常量、也可能是if、return、function这种关键字,是哪种语义,分词阶段并不在乎,只要正确切分就好了。
  • 运算符:+、-、*、/、>、<等等
  • 括号:(…)可能表示运算优先级、也可能表示函数调用,分词阶段并不关注是哪种语义,只把“(”或“)”当做一种基本语法单元

就是一个字符一个字符地遍历,然后通过switch case分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。以@babel/parser源码为例:

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
kotlin复制代码getTokenFromCode(code) {
  switch (code) {
    case 46:
      this.readToken_dot();
      return;

    case 40:
      ++this.state.pos;
      this.finishToken(10);
      return;

    case 41:
      ++this.state.pos;
      this.finishToken(11);
      return;

    case 59:
      ++this.state.pos;
      this.finishToken(13);
      return;

      // 此处省略...

    case 92:
      this.readWord();
      return;

    default:
      if (isIdentifierStart(code)) {
        this.readWord(code);
        return;
      }

  }

}

readToken_dot() {
  // charCodeAt一个一个向后移
  const next = this.input.charCodeAt(this.state.pos + 1);

  if (next >= 48 && next <= 57) {
    this.readNumber(true);
    return;
  }

  if (next === 46 && this.input.charCodeAt(this.state.pos + 2) === 46) {
    this.state.pos += 3;
    this.finishToken(21);
  } else {
    ++this.state.pos;
    this.finishToken(16);
  }
}

语法解析器

将上一步生成的数组,根据语法规则,转为抽象语法树(Abstract Syntax Tree,简称AST)。

以 const a = 1 为例,词法分析中获得了 const 这样一个 token,并判断这是一个关键字,根据这个 token 的类型,判断这是一个变量声明语句。以@babel/parser源码为例,执行 parseVarStatement 方法。

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
kotlin复制代码parseVarStatement(node, kind, allowMissingInitializer = false) {
    const {
      isAmbientContext
    } = this.state;
    const declaration = super.parseVarStatement(node, kind, allowMissingInitializer || isAmbientContext);
    if (!isAmbientContext) return declaration;


    for (const {
      id,
      init
    } of declaration.declarations) {
      if (!init) continue;


      if (kind !== "const" || !!id.typeAnnotation) {
        this.raise(TSErrors.InitializerNotAllowedInAmbientContext, {
          at: init
        });
      } else if (init.type !== "StringLiteral" && init.type !== "BooleanLiteral" && init.type !== "NumericLiteral" && init.type !== "BigIntLiteral" && (init.type !== "TemplateLiteral" || init.expressions.length > 0) && !isPossiblyLiteralEnum(init)) {
        this.raise(TSErrors.ConstInitiailizerMustBeStringOrNumericLiteralOrLiteralEnumReference, {
          at: init
        });
      }
    }


    return declaration;
  }

经过这一步的处理,最终 const a = 1 会变成如下ast结构:

image.png

字节码生成器

字节码生成器的作用,是将抽象语法树转为JavaScript引擎可以执行的二进制代码。目前,还没有统一的JavaScript字节码的格式标准,每种JavaScript引擎都有自己的字节码格式。最简单的做法,就是将语义单位翻成对应的二进制命令。

字节码解释器

字节码解释器的作用是读取并执行字节码。

几种常见的解析器

Esprima

这是第一个用JavaScript编写的符合EsTree规范的JavaScript的解析器,后续多个编译器都是受它的影响

acorn

acorn 和 Esprima 很类似,输出的ast都是符合 EsTree 规范的,目前webpack的AST解析器用的就是acorn

@babel/parser(babylon)

babel官方的解析器,最初fork于acorn,后来完全走向了自己的道路,从babylon改名之后,其构建的插件体系非常强大

uglify-js

用于混淆和压缩代码, 因为一些原因,uglify-js自己[内部实现了一套AST规范,也正是因为它的AST是自创的,不是标准的ESTree,es6以后新语法的AST,都不支持,所以没有办法压缩最新的es6的代码,如果需要压缩,可以用类似babel这样的工具先转换成ES5。

esbuild

esbuild是用go编写的下一代web打包工具,它拥有目前最快的打包记录和压缩记录,snowpack和vite的也是使用它来做打包工具,为了追求卓越的性能,目前没有将AST进行暴露,也无法修改AST,无法用作解析对应的JavaScript。

babel

下面先介绍下babel相关工具库,以及一些API,了解完这些基础概念后,我们会利用这些工具操作AST来编写一个babel插件。

  • @babel/parser 可以把源码转换成AST
  • @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
  • @babel/generate 可以把AST生成源码,同时生成sourcemap
  • @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
  • @babel/template可以简化AST的创建逻辑
  • @babel/code-frame可以打印代码位置
  • @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
  • babylon Babel 的解析器,以前叫babel parser,是基于acorn扩展而来,扩展了很多语法,可以支持es2020、jsx、typescript等语法
  • babel-types-api
  • Babel 插件手册

Visitor

  • 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • Visitor 的对象定义了用于 AST 中获取具体节点的方法
  • Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法

path

  • node 当前 AST 节点
  • parent 父 AST 节点
  • parentPath 父AST节点的路径
  • scope 作用域
  • get(key) 获取某个属性的 path
  • **set(key, node) **设置某个属性
  • is类型(opts) 判断当前节点是否是某个类型
  • find(callback) 从当前节点一直向上找到根节点(包括自己)
  • **findParent(callback) **从当前节点一直向上找到根节点(不包括自己)
  • insertBefore(nodes) 在之前插入节点
  • insertAfter(nodes) 在之后插入节点
  • replaceWith(replacement) 用某个节点替换当前节点
  • replaceWithMultiple(nodes) 用多个节点替换当前节点
  • replaceWithSourceString(replacement) 把源代码转成AST节点再替换当前节点
  • remove() 删除当前节点
  • **traverse(visitor, state) **遍历当前节点的子节点,第1个参数是节点,第2个参数是用来传递数据的状态
  • skip() 跳过当前节点子节点的遍历
  • stop() 结束所有的遍历
  • getSibling(key) 获取某个下标的兄弟节点
  • getNextSibling() 获取下一个兄弟节点
  • **getPrevSibling() ** 获取上一个兄弟节点
  • getAllPrevSiblings() 获取之前的所有兄弟节点
  • getAllNextSiblings() 获取之后的所有兄弟节点

scope

• **scope.bindings **当前作用域内声明所有变量

• **scope.path **生成作用域的节点对应的路径

• **scope.references **所有的变量引用的路径

• getAllBindings() 获取从当前作用域一直到根作用域的集合

• **getBinding(name) **从当前作用域到根使用域查找变量

• getOwnBinding(name) 在当前作用域查找变量

• parentHasBinding(name, noGlobals) 从当前父作用域到根使用域查找变量

• removeBinding(name) 删除变量

• hasBinding(name, noGlobals) 判断是否包含变量

• **moveBindingTo(name, scope) **把当前作用域的变量移动到其它作用域中

• generateUid(name) 生成作用域中的唯一变量名,如果变量名被占用就在前面加下划线

• scope.dump() 打印自底向上的 作用域与变量信息到控制台

实现eslint插件

上面这些概念在我们编写babel插件时会用到,接下来我们来实现一个eslint移除console.log()插件吧!

先看下 console.log(‘a’) 的AST长什么样子?

image.png

可以看到 console.log(‘a’) 是一个type为 “ExpressionStatement”的节点,所以我们只需要遍历AST,当遇到type=ExpressionStatement的节点删除即可!来一起实现吧~

  1. 引入基础包
1
2
3
4
5
ini复制代码var fs = require("fs");
//babel核心模块,里面包含transform方法用来转换源代码。
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
  1. 遍历AST节点,babel插件的语法都是固定的,里面包含visitor,我们只需在visitor里面处理ExpressionStatement即可,
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
javascript复制代码//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
  // babel插件的语法都是固定的,里面包含visitor
    return {
      pre(file) {
        file.set('errors', []);
      },
      // 访问器
      visitor: {
        CallExpression(path, state) {
          const errors = state.file.get('errors');
          const { node } = path
          if (node.callee.object && node.callee.object.name === 'console') {
            errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
            if (fix) {
              path.parentPath.remove();
            }
          }
        }
      },
      post(file) {
        // console.log(...file.get('errors'));
      }
    }
  };
  1. 完整实现:
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
javascript复制代码var fs = require("fs");
//babel核心模块,里面包含transform方法用来转换源代码。
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");

//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
  // babel插件的语法都是固定的,里面包含visitor
    return {
      pre(file) {
        file.set('errors', []);
      },
      // 访问器
      visitor: {
        CallExpression(path, state) {
          const errors = state.file.get('errors');
          const { node } = path
          if (node.callee.object && node.callee.object.name === 'console') {
            errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
            if (fix) {
              path.parentPath.remove();
            }
          }
        }
      },
      post(file) {
        // console.log(...file.get('errors'));
      }
    }
  };

core.transformFile("eslint/source.js",{
    plugins: [eslintPlugin({ fix: false })]
}, function(err, result) {
    result; // => { code, map, ast }
    console.log(result.code);
  })

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【问题解决】解决如何在 CPU 上加载多 GPU 训练的模型

发表于 2022-12-14

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

有一期的恶意文件检测模型训练好了,因此需要进行测试,关于恶意文件检测的内容,可以回看博主之前写的博文:

  • 【AI】浅析恶意文件静态检测及部分问题解决思路
  • 【AI】恶意文件静态检测模型检验及小结

因为样本在某台机子上,又恰逢有其他模型在训练,因此 GPU 资源被占满了,不过测试这个模型的话,CPU 也绰绰有余了,当我准备使用 CPU 训练时,却遇到了问题;

分析

1、model.to(device) 不会影响 torch.load();

我一开始以为只要使用 model.to 就算是使用上 CPU 了;

1
2
3
4
5
6
7
8
9
py复制代码device = torch.device("cpu")
model = ...
model = model.to(device)

model_savedir_ = ''
if os.path.exists(model_savedir_):
print("model load.")
state_dict = torch.load(model_savedir_)
model.load_state_dict(state_dict)

事实证明,我想的太简单了…

image.png

1
2
3
sql复制代码RuntimeError: CUDA error: out of memory
CUDA kernel errors might be asynchronously reported at some other API call,so the stacktrace below might be incorrect.
For debugging consider passing CUDA_LAUNCH_BLOCKING=1.

这个问题很显而易见,就是 GPU 的内存溢出了,但是按我的思路,用的应该是 CPU 啊,所以我怀疑是 torch.load() 这个函数出了问题,查询了一番资料后,发现是要这样使用的 state_dict = torch.load(model_savedir_, map_location=device);


2、GPU 与 CPU 训练时参数名不一致

当我以为大功告成,点击运行之时,不料,又报错了:

image.png

1
2
vbnet复制代码RuntimeError: Error(s) in loading state_dict for ..model..:
Missing key(s) in state_dict: "fc.weight", "fc.bias", "features.0.0.weight", "features.0.1.weight", "features.0.1.bias", "features.0.1.running_mean", "features.0.1.running_var", "features.1.conv.0.weight", "features.1.conv.1.weight", "features.1.conv.1.bias", "features.1.conv.1.running_mean", "features.1.conv.1.running_var", "features.1.conv.3.weight", "features.1.conv.4.weight", "features.1.conv.4.bias", "features.1.conv.4.running_mean", "features.1.conv.4.running_var", "features.1.conv.5.fc.0.weight", ...

根据理解,就是说找不到参数,因此,我将字典部分内容打印了一下:

1
2
3
py复制代码for k, v in state_dict.items():
print(k, v)
break

image.png

发现问题了,在多 GPU 上训练的模型,保存时会在参数名前多加了一个 module. 前缀,因此在用 CPU 进行加载时,需要把这个前缀去掉:

1
2
3
4
5
6
7
8
9
py复制代码if os.path.exists(model_savedir_):
print("model load.")
state_dict = torch.load(model_savedir_, map_location=device)
from collections import OrderedDict
state_dict_new = OrderedDict()
for k, v in state_dict.items():
name = k[7:] # 去掉 `module.`
state_dict_new[name] = v
model.load_state_dict(state_dict_new)

这样就能够在 CPU 上加载多 GPU 训练的模型了!

后记

以上就是 【问题解决】解决如何在 CPU 上加载多 GPU 训练的模型 的全部内容了,希望对大家有所帮助!

📝 上篇精讲:【问题解决】解决 Docker 二次重启 MySQL 8 遇到的一些问题

💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;

👍 创作不易,请多多支持;

🔥 系列专栏:问题解决 AI

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【AI】恶意文件静态检测模型检验及小结 前言 拉取测试集 检

发表于 2022-12-12

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

在之前的博文 【AI】浅析恶意文件静态检测及部分问题解决思路 中,博主提及过恶意文件静态检测的一种方法,并因此训练了模型,由于样本量巨大以及资源有限,训练一个 epoch 就需要一周多的时间,因此就先拿训练过一个 epoch 的模型来进行测试;

拉取测试集

既然是要用来测试的样本,那么我们要尽可能的与训练集以及验证集中的样本不一样,因此,最好在一开始就做好分类,或者可以借用集合 set 的特性来整合;

我们先用训练集和验证集进行测试,伪代码如下:

1
2
3
4
5
6
7
8
9
py复制代码with open('...pkl', 'rb') as f:
train_data = pickle.load(f)

with open('...pkl', 'rb') as f:
val_data = pickle.load(f)

train_data_ = [x[0] for x in train_data]
val_data_ = [x[0] for x in val_data]
zz = set(train_data_) - set(val_data_)

image.png

可以看到数量是完全相同的,因此训练集和验证集没有交集,即两者之间没有重复的样本;

接下来我们就开始拉去测试集,先从 Metadata_PE 表中去获取到 path 和 sha256 字段,然后在根据 sha256 去 Event_PE_lab_22_11_24 表中进行查询 lab 标签;

这里的话,可以根据联合索引,直接从数据库中将全部数据导入,借用 pymsql 和 pandas 的包,在 python 中处理的速度比原生 SQL 要快不少,不过因为数据量较大,导入也消耗的一定的时间:

导入完成之后就是对数据进行处理:

1
2
py复制代码new_sample_df = sample_df[sample_df['date'] >= pd.Timestamp('2022-12-01')]
pd.merge(new_sample_df, label_df, on="sha256")

这里的话,根据入库时间进行拉取,选取 2022-12-01 之后入库的样本:

因为这里只需要 exe 类型的文件,所以还需要再进行一次判断,样本量过大可采取多线程 ThreadPoolExecutor:

1
2
py复制代码if pefile.PE(path).is_exe():
...

全部完成之后,就是我们需要的测试集了;

image.png

检验模型

我们先将刚刚准备好的测试集进行导入:

1
py复制代码test_loader = DataLoader(...)

然后传入到模型中,获取预测值,并计算损失:

1
2
3
4
5
6
7
8
9
10
11
12
py复制代码# 特征值,模型的输入
test_x = batch_data[0].to(torch.float32).to(device)
# 预测值,模型的输出,两个值分别为黑白样本概率,如 [0.4052, -0.3841]
out = model(test_x)
# 标签值,用于计算损失
label = batch_data[1].to(device)
# 预测值与真实值之间的损失
loss = criterion(out, label.long())
# 一个 batch 的大小
val_size += label.size(0)
# 一个 batch 的损失,loss.item() 每个样本的平均损失
running_loss += loss.item() * label.size(0)

因为是检验模型,我们需要去评价模型的好坏,判断是否为恶意文件其实就是个二分类问题,这里的话使用混淆矩阵:

预测值0 预测值1
真实值0 TN FP
真实值1 FN TP
  • TN:真实值是0,预测值是0,即我们预测是 negative,预测正确了。
  • FP:真实值是0,预测值是1,即我们预测是 positive,预测错误了。
  • FN:真实值是1,预测值是0,即我们预测是 negative,预测错误了。
  • TP:真实值是1,预测值是1,即我们预测是 positive,预测正确了。

accuracy_score = (TP+TN) / (TP+TN+FP+FN):函数计算分类准确率,返回被正确分类的样本比例(default)或者是数量(normalize=False)。

精准率(查准率)和召回率(查全率)等指标对衡量机器学习的模型性能在某些场合下要比 accuracy 更好。

精准率:precision = TP / (TP+FP)。所谓的精准率是:分母为所有预测为1的个数,分子是其中预测对了的个数,即预测为正的样本中,实际为正的比例。

召回率:recall = TP / (TP+FN)。所谓的召回率是:所有真实值为1的数据中,预测对了的个数,也就是我们关注的那个事件真实的发生情况下,我们成功预测的比例是多少。

接下来,我们就根据预测值和标签值来进行计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
py复制代码preds_n = preds_sg
label_n = label_sg
# zes: [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
zes = np.zeros(label.size(0))
# ons: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
ons = np.ones(label.size(0))
preds_np = preds_n
label_np = label_n.reshape(-1)
train_correct01 = int(((preds_np == zes) & (label_np == ons)).sum())
train_correct10 = int(((preds_np == ons) & (label_np == zes)).sum())
train_correct11 = int(((preds_np == ons) & (label_np == ons)).sum())
train_correct00 = int(((preds_np == zes) & (label_np == zes)).sum())
FN += train_correct01
FP += train_correct10
TP += train_correct11
TN += train_correct00
accuracy_score = (TP+TN) / (TP+TN+FP+FN)
precision = TP / (TP+FP)
recall = TP / (TP+FN)

这里的话就用几个 batch 来略作检验:

其实看的出模型的效果挺差的;

排查问题

由上可知,我们训练了一段时间的模型效果并不理想,这是为什么呢?

看了一下过往的日志,发现一个问题:

一个 batch 里的所有预测值都是一样的?怪事;

再去看看自己训练集里的样本,发现是各不相同的:

那就是梯度消失导致了这一问题…

现在的一个解决方案就是更换模型,换成一个小模型,之后训练的效果如何,会更新在之后的博文里,敬请期待!

后记

以上就是 【AI】恶意文件静态检测模型检验及小结 的全部内容了。

本文介绍了拉取数据集的一些小细节,以及如何对模型进行检验,排查相关问题,希望对大家有所帮助!

📝 上篇精讲:【AI】浅谈使用正则化防止过拟合(下)

💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;

👍 创作不易,请多多支持;

🔥 系列专栏:AI 项目实战

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 的下一步, Dart 3 重大变更即将在 2

发表于 2022-12-09

原文链接 medium.com/dartlang/th…

在过去的四年里 Dart 已经发展成为一门高效、可移植的现代化语言,而下一个版本 Dart 3 将达到可靠的 null 安全语言的最后一步。

作为 null safety 的最后一步,本次将删除几个历史 Dart 和 SDK artifacts,包括删除对 running without sound null safety 的支持。

如今许多现代编程语言都支持 null safety ,比如 Swift、C# 和 Kotlin 等语言,可以在其中将变量声明为非空(永远不能保持空值)或可为空(可以保存一个值或 null)的情况,这些类型系统会和静态分析结合使用,以检测将 null 赋值给不可为 null 的变量。

同样,Dart 语言中的 null 安全支持默认情况下使变量不可为 null,并且仅在显式声明时才允许 null。

在 Dart 3 中,所有 Dart 代码都将使用健全的空安全

自从 Dart 2.12 引入空安全到现在已经三年了,在此期间开发者可以在没有 null safety 的情况下运行,也可以在具有部分 null safety 的混合模式下运行,也可以在具有完全健全的 null safety 的情况下运行。

当 100% 的代码(包括所有依赖项)已迁移到 null safety 时,就会出现完全可靠的空安全支持,在此期间 Dart 开发人员有时间一步一步地迁移现有代码,但是支持多种模式会增加开销和复杂性。

首先,Dart 开发人员需要了解以上三种模式,每当阅读一段 Dart 代码时,就必须检查语言版本以查看类型是否默认为非空、默认可为空或它们的某种组合。

其次,在我们的编译器和运行时支持所有三种模式会减慢 Dart SDK 的发展速度,这种支持增加了添加新功能的成本和复杂性。

从在 Dart 3 开始,正如之前 2.18 里提前宣布的那样,sound null safety 将是唯一受支持的模式,**小于 2.12 的 SDK 约束的 Pubspec 文件将在 Dart 3 及更高版本中停止解析。

当开发者将依赖约束设置为小于 2.12(例如// @dart=2.9)时,任何包含语言标记的源代码都将失效。

根据目前的观测,我们相信此时大约 85% 的 flutter run 执行都使用了空安全,如果你还在剩余的 15% 中,那请在 Dart 3 发布之前迁移,预计在 2023 年年中左右。

Breaking 和 API 更改

除了 null 安全更改之外,Dart 3 还进行了一些其他更改,以删除 Dart 和核心库 API 中的一些历史 artifacts,这些更改包括:

  • 删除已停用的核心库 API ( #49529 )
  • 删除默认参数值的历史语法 ( #2357 )
  • 要求明确的 tear-offs ( ##2399 )。

这些更改对迁移到使用 null 安全的代码的影响很小,当第一个 Dart 3 alpha 版本发布时,开发者可以快速测试这些较小的 Breaking。

Dart 3 的新特性和功能

Dart 3 也有望包含许多新功能,包括改进与其他编程语言的交互能力和新的语言特性, 这部分内容将在2023 年 1 月 25 日的 Flutter Forward 中详细讨论。

例如有被称为 patterns 的语言特性,patterns 让 Dart 语言更具表现力,增加了对更多结构化数据的支持,并使用代数数据类型实现了更实用的风格。

以下代码显示了在一个函数上使用多个返回值的示例,以及将这些返回值解构为单个变量的能力:

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
dart复制代码// A function which returns a record -- in this case a pair of two doubles:
(double x, double y) getLocation(String name) {
 if (name == 'Aarhus') {
   return (56.1629, 10.2039);
} else {
  ...
}
}

// Deconstructing the returned record back into individual variables:
void main(List<String> arguments) {
 final (lat, long) = getLocation('Aarhus');
 print('Current location: $lat, $long');
}

// You can also define a hierarchy of classes, and then pattern match on those:
sealed class Shape {
 double calculateArea();
}

class Square implements Shape {
 final double length;
 Square(this.length);
 double calculateArea() => length * length;
}

class Circle implements Shape {
 final double radius;
...
}

double calculateArea(Shape shape) => switch (shape) {
 Square(length: var l) => l * l,
 Circle(radius: var r) => math.pi * r * r
};

Beyond Dart 3

目前除了 Dart 3 还有大量并行的潜在的新功能在处理,首先,正如去年提到的, Dart 团队正在努力支持将 Dart 代码编译为 WebAssembly ( Wasm ),Wasm 能让 Flutter Web 在浏览器中作为完整的原生代码运行。

这是一项艰巨的任务,除了更新 Dart 编译器之外还需要很多额外的工作。它需要与 W3C 和浏览器供应商合作,通过 WasmGC 扩展在 Wasm 中添加对垃圾收集语言的支持。

其次 Dart 团队正在研究 macros 启用静态元编程,这种强大的机制允许一段代码(宏)在程序编译期间修改和扩展程序的源代码,例如可以减少反序列化 JSON 或创建数据类所需的样板文件。

Dart 3 发布路线

接下来,Dart 3 将在一系列里程碑中陆续推出,目前的期望围绕这些日期

  • 2023 年 1 月/2 月左右:Dart 3 alpha 发布,它将专注于启用早期的 Dart 3 兼容性测试,目标是让大家能够运行静态分析 ( dart analyze/ flutter analyze),理论上通过 Dart 3 alpha 静态分析的应用或包都可以支持 Dart 3 稳定版。
  • 2023 年 3 月/4 月左右:Dart 3 测试版发布,此版本预览了 Dart 3 中的新功能,开发者可以使用它来试验新功能并就问题或改进建议提供反馈。
  • 2023 年年中左右:Dart 3 稳定版发布,健全的空安全将成为唯一支持的模式。

总结

Dart 3 版本计划于 2023 年年中左右发布,它将包含几项重大更改,其中主要是在没有健全的空安全的情况下你的代码将停止运行,计划在 2023 年 1 月或 2 月左右准备好 Dart 3 alpha 版本,可以将其用于 Dart 3 兼容性测试。

在此期间你可以准备:

  • 完成任何未完成的空安全迁移
  • 验证代码未使用任何已弃用的 API
  • 运行 dart fix。

Dart 3 还将包含几个新的强大功能,例如 patterns ,计划是希望在春季发布 Dart 3 beta 版,展示所有新功能,敬请期待~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【若川视野 x 源码共读】第40期 vite 是如何解析

发表于 2022-12-06

源码共读前言

为了能帮助到更多对源码感兴趣、想学会看源码、提升自己写作和前端技术能力的同学。 帮助读者夯实基础,查漏补缺,开阔眼界,拓宽视野,知其然知其所以然。

我倾力组织了每周一起学200行左右的源码共读活动。我写有《学习源码整体架构系列》20余篇,走过路过的小伙伴可以点击关注下这个目前是掘金关注数最多的专栏。

欢迎点此扫码加我微信 ruochuan02 交流,参与 源码共读 活动,每周大家一起学习200行左右的源码,共同进步。可以持续关注我@若川。

从易到难推荐学习顺序

活动介绍和顺序具体看这里从易到难推荐学习顺序

提交笔记

提交笔记方式,具体的看这里
简言之:看任务,看辅助文章、看源码,交流讨论,在掘金写笔记,写好后提交到本文评论区。

为了给大家谋福利,另外给大家的文章带来更多阅读量,便于搜索,从2022年3月27日起,笔记可以直接发布在掘金,以《标题自取》标题不限,可以取个好标题,容易被掘金推荐。

笔记文章开头加两句话:

  • 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
  • 这是源码共读的第xx期,链接:xxx。

笔记文章最后,建议写上总结、收获、感受等。

  • 开头第一句作用是:方便每月统计评优,掘金赞助送小礼物。顺便帮忙宣传推广,让更多人参与进来,一起学习。
  • 开头第二句作用是:加上是多少期,当前任务说明的链接,方便读者知道这是什么活动。

笔记写完后,到当前期活动的文章评论区留言自己的文章和笔记特点。方便大家查阅学习交流讨论。

往期所有笔记存放在语雀讨论区。

任务发布时间

2022年12月5日 - 2023年1月8日。可以按照自己节奏学习,提交笔记即可(不一定要严格按照我规定的时间)。往期共读也可以及时复习,笔记未完成可以继续完成。

语雀本期任务说明链接

语雀有树形菜单,更方便查看,所以也放下语雀的这一期链接

学习任务

  • 学会使用 cac 写脚手架工具
  • 看 vite 源码,分析如何读取用户配置的 .env 相关文件
  • 主要这三个文件 cli.ts、config.ts、env.ts
  • github1s.com/vitejs/vite…
+ 引用的 cac 参考文档 [github.com/cacjs/cac](https://github.com/cacjs/cac)
  • github1s.com/vitejs/vite…
+ loadEnv 使用
  • github1s.com/vitejs/vite…
+ 定义文件的地方
  • 调试源码可以用 npx tsx xxx.ts
+ [新手向:前端程序员必学基本技能——调试JS代码](https://dev.newban.cn/7030584939020042254)

参考文章

  • 面试官:项目中常用的 .env 文件原理是什么?如何实现?
  • 看文章,看源码,交流讨论,写笔记发布在掘金。再在掘金这篇文章下评论放上提交笔记的链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【问题解决】解决 Docker 二次重启 MySQL 8 遇

发表于 2022-12-06

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

前些天因为服务器扩容,需要进行断电,因此 Docker 被迫关闭了;

今天重启 MySQL 8 的容器时,遇到了一些问题,特写此篇博文记录;

lower_case_table_names 参数设置

在启动 MySQL 容器时,使用相关指令:

1
2
3
csharp复制代码[root@localhost ~]# docker run ...
WARNING: IPv4 forwarding is disabled. Networking will not work.
6dc8fa34ff7...e3ed12a1b2f6e0edbc8e6

看着样子应该是启动成功了,但是通过 docker ps 指令查看,发现并没有刚刚启动的 MySQL 容器;

那让我们看一下日志,排查一下问题,docker logs 6dc8fa34ff7...e3ed12a1b2f6e0edbc8e6:

image.png

发现问题所在:

1
java复制代码Different lower_case_table_names settings for server ('0') and data dictionary ('1').

根据字面意思理解一下就是:

1
arduino复制代码服务器('0')和数据字典('1')的不同 lower_case_table_names 设置。

具体的话可以看到 官方文档:

If set to 0, table names are stored as specified and comparisons are case-sensitive. If set to 1, table names are stored in lowercase on disk and comparisons are not case-sensitive. If set to 2, table names are stored as given but compared in lowercase. This option also applies to database names and table aliases. For additional details, see Section 9.2.3, “Identifier Case Sensitivity”.

The default value of this variable is platform-dependent (see lower_case_file_system). On Linux and other Unix-like systems, the default is 0. On Windows the default value is 1. On macOS, the default value is 2. On Linux (and other Unix-like systems), setting the value to 2 is not supported; the server forces the value to 0 instead.

You should not set lower_case_table_names to 0 if you are running MySQL on a system where the data directory resides on a case-insensitive file system (such as on Windows or macOS). It is an unsupported combination that could result in a hang condition when running an INSERT INTO ... SELECT ... FROM tbl_name operation with the wrong tbl_name lettercase. With MyISAM, accessing table names using different lettercases could cause index corruption.

An error message is printed and the server exits if you attempt to start the server with --lower_case_table_names=0 on a case-insensitive file system.

The setting of this variable affects the behavior of replication filtering options with regard to case sensitivity. For more information, see Section 17.2.5, “How Servers Evaluate Replication Filtering Rules”.

It is prohibited to start the server with a lower_case_table_names setting that is different from the setting used when the server was initialized. The restriction is necessary because collations used by various data dictionary table fields are determined by the setting defined when the server is initialized, and restarting the server with a different setting would introduce inconsistencies with respect to how identifiers are ordered and compared.

It is therefore necessary to configure lower_case_table_names to the desired setting before initializing the server.

正如官方文档所说,On Linux and other Unix-like systems, the default is 0.,我用的系统是 Linux 的,因此 lower_case_table_names 默认值是 0,因此我需要对其进行改变,将其与数据字典一致,即 lower_case_table_names=1;

TIP


lower_case_table_names 该参数为静态,可设置为0、1、2。

  • 0 – 大小写敏感。(Unix,Linux 默认);
  • 1 – 大小写不敏感。(Windows 默认);
  • 2 – 大小写不敏感。(OS X 默认);

先将旧的容器移除:

1
bash复制代码docker rm 6dc8fa34ff7...e3ed12a1b2f6e0edbc8e6

然后再重新启动一遍容器:

1
2
3
4
5
6
7
bash复制代码docker run -d --name sid10t-mysql \
-v $(pwd)/mysql/data:/var/lib/mysql \
-v $(pwd)/mysql/conf:/etc/mysql \
-v $(pwd)/mysql/log:/var/log/mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=pwd \
mysql --lower-case-table-names=1

测试一下连接,连接成功,搞定!
image.png

IPv4 forwarding is disabled 参数设置

本以为解决上面的问题就大功告成了,可是天不如人愿,当我使用远程连接时,结果如下:

image.png

当使用 pymysql 连接时也被告知连接超时:

1
vbnet复制代码OperationalError: (2003, "Can't connect to MySQL server on '10.20.152.70' (timed out)")

那第一时间就是怀疑 3306 是否没有对外开放,因此直接防火墙开放 3306 端口:

1
css复制代码firewall-cmd --zone=public --add-port=3306/tcp --permanent

但是却发现 3306 端口一直是开放状态的:

image.png

那就查看其占用情况,是否只允许本地访问:

image.png

发现也不是这个问题;

突然想起,刚刚启动容器的时候,好像是弹出了一个警告:

1
vbnet复制代码WARNING: IPv4 forwarding is disabled. Networking will not work.

查阅资料后发现,是没有开启 IPv4 转发,网桥配置完后,需要开启转发,不然容器启动后,就会没有网络,

因此进行如下操作即可:

1
bash复制代码vim /etc/sysctl.conf

或者

1
bash复制代码vi /usr/lib/sysctl.d/00-system.conf

添加如下代码:

1
ini复制代码net.ipv4.ip_forward=1

重启 network 服务:

1
复制代码systemctl restart network

查看是否修改成功:

1
复制代码sysctl net.ipv4.ip_forward

完成之后重启新的 MySQL 容器,再使用远程连接就可以连上了;

后记

以上就是 【问题解决】解决 Docker 二次重启 MySQL 8 遇到的一些问题 的全部内容了,希望对大家有所帮助!

📝 上篇精讲:这是第一篇,没有上一篇喔~

💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;

👍 创作不易,请多多支持;

🔥 系列专栏:问题解决

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

vant40 正式发布了,分析其源码学会用 vue3 写一

发表于 2022-11-29

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

  1. 前言

大家好,我是若川。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.3k+人)第一的专栏,写有20余篇源码文章。

我们开发业务时经常会使用到组件库,一般来说,很多时候我们不需要关心内部实现。但是如果希望学习和深究里面的原理,这时我们可以分析自己使用的组件库实现。有哪些优雅实现、最佳实践、前沿技术等都可以值得我们借鉴。

相比于原生 JS 等源码。我们或许更应该学习,正在使用的组件库的源码,因为有助于帮助我们写业务和写自己的组件。

如果是 Vue 技术栈,开发移动端的项目,大多会选用 vant 组件库,目前(2022-11-29) star 多达 20.5k,已经正式发布 4.0 了。我们可以挑选 vant 组件库学习,我会写一个vant 组件库源码系列专栏,欢迎大家关注。

vant 组件库源码分析系列:

  • 1.vant 4 即将正式发布,支持暗黑主题,那么是如何实现的呢
  • 2.跟着 vant 4 源码学习如何用 vue3+ts 开发一个 loading 组件,仅88行代码
  • 3.分析 vant 4 源码,如何用 vue3 + ts 开发一个瀑布流滚动加载的列表组件?
  • 4.分析 vant 4 源码,学会用 vue3 + ts 开发毫秒级渲染的倒计时组件,真是妙啊
  • 5.vant 4.0 正式发布了,分析其源码学会用 vue3 写一个图片懒加载组件!

这次我们来学习 Lazyload 懒加载组件,可以点此查看 lazyload 文档体验。

学完本文,你将学到:

1
2
3
4
bash复制代码1. 学会如何用 vue3 + ts 开发一个 lazyload 组件
2. 学会 `lazyload` 图片懒加载组件其原理
3. 学会使用事件和 IntersectionObserver API 实现懒加载
4. 等等
  1. 准备工作

看一个开源项目,第一步应该是先看 README.md 再看贡献文档 github/CONTRIBUTING.md。

2.1 克隆源码 && 跑起来

You will need Node.js >= 14 and pnpm.

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码# 推荐克隆我的项目
git clone https://github.com/lxchuan12/vant-analysis
cd vant-analysis/vant

# 或者克隆官方仓库
git clone git@github.com:vant-ui/vant.git
cd vant

# 安装依赖,如果没安装 pnpm,可以用 npm i pnpm -g 安装,或者查看官网通过其他方式安装
pnpm i

# 启动服务
pnpm dev

执行 pnpm dev 后,这时我们打开懒加载组件 http://localhost:5173/#/zh-CN/lazyload。

  1. 图片懒加载原理

众所周知,图片懒加载的原理其实相对简单。就是进入可视区再加载图片。涉及到的知识点主要有:节流、新API、IntersectionObserver。

大致流程:

  • 事件模式
1
2
3
4
bash复制代码1. 初始化在元素(比如是 window,但不一定是 window)添加监听滚动和其他相关事件
2. 使用 Element.getBoundingClientRect API 获取元素的大小及其相对于视口的位置,判断是否进入可视化区
3. 图片设置 src 真实的图片路径
4. 离开销毁监听的事件、和移除绑定事件的元素
  • observer 模式

主要是第二步用 IntersectionObserver API。

那么 vant4 中的 lazyload 怎么做的呢。

带着问题我们直接找到 lazyload demo 文件:vant/packages/vant/src/lazyload/demo/index.vue。为什么是这个文件,我在之前文章跟着 vant4 源码学习如何用 vue3+ts 开发一个 loading 组件,仅88行代码分析了其原理,感兴趣的小伙伴点击查看。这里就不赘述了。

  1. 利用 demo 调试源码

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
js复制代码// vant/packages/vant/src/lazyload/demo/index.vue
// 代码有省略
<script lang="ts">
import Lazyload from '..';

if (window.app) {
// 手动修改 demo 为如下,添加 lazyImage 为 true
window.app.use(Lazyload, { lazyComponent: true, lazyImage: true });
}
<script setup lang="ts">
// eslint-disable-next-line import/first
import { cdnURL, useTranslate } from '../../../docs/site';

const t = useTranslate({
'zh-CN': {
title2: '背景图懒加载',
title3: '懒加载模块',
},
});

const imageList = [
cdnURL('apple-1.jpeg'),
cdnURL('apple-2.jpeg'),
cdnURL('apple-3.jpeg'),
cdnURL('apple-4.jpeg'),
];
</script>
<template>
<demo-block :title="t('basicUsage')">
<!-- 手动修改 demo 为 lazy-image -->
<lazy-image v-for="img in imageList" :key="img" :src="img">
</lazy-image>
</demo-block>
</template>

我们可以看出 lazy-load 为入口文件。

这里先附上两张调试截图。动手调试时可以参考学习。

install 函数调试

install 函数调试

LazyClass 调试

lazy-class 调试

  1. lazy-load 入口文件

从 vue-lazyload 文件引入导出和默认导出 Lazyload。
主要包含:

  • 把 lazy 实例对象添加到全局上
  • 注册懒加载组件
  • 注册图片组件
  • 注册指令 lazy
  • 注册指令 lazy-container
1
2
3
4
5
js复制代码// vant/packages/vant/src/lazyload/index.ts
import { Lazyload } from './vue-lazyload';

export default Lazyload;
export { Lazyload };

我们接着来看 vue-lazyload/index.js 主文件。

  1. vue-lazyload/index.js 主文件

主要导出一个包含 install 方法的对象 Lazyload。

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
js复制代码// vant/packages/vant/src/lazyload/vue-lazyload/index.js
/**
* This is a fork of [vue-lazyload](https://github.com/hilongjw/vue-lazyload) with Vue 3 support.
* license at https://github.com/hilongjw/vue-lazyload/blob/master/LICENSE
*/

import Lazy from './lazy';
import LazyComponent from './lazy-component';
import LazyContainer from './lazy-container';
import LazyImage from './lazy-image';

export const Lazyload = {
/*
* install function
* @param {App} app
* @param {object} options lazyload options
*/
install(app, options = {}) {
debugger;
const LazyClass = Lazy();
const lazy = new LazyClass(options);
const lazyContainer = new LazyContainer({ lazy });

// 把 lazy 实例对象添加到全局上
app.config.globalProperties.$Lazyload = lazy;

// 注册懒加载组件
if (options.lazyComponent) {
app.component('LazyComponent', LazyComponent(lazy));
}

// 注册图片组件
if (options.lazyImage) {
app.component('LazyImage', LazyImage(lazy));
}

// 注册指令 lazy
app.directive('lazy', {
beforeMount: lazy.add.bind(lazy),
updated: lazy.update.bind(lazy),
unmounted: lazy.remove.bind(lazy),
});

// 注册指令 lazy-container
app.directive('lazy-container', {
beforeMount: lazyContainer.bind.bind(lazyContainer),
updated: lazyContainer.update.bind(lazyContainer),
unmounted: lazyContainer.unbind.bind(lazyContainer),
});
},
};

单从图片懒加载来看,简化上面的代码,则是这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// 简化
import Lazy from './lazy';
import LazyImage from './lazy-image';
export const Lazyload = {
install(app, options = {}){
const LazyClass = Lazy();
const lazy = new LazyClass(options);
// 渲染图片组件
if (options.lazyImage) {
app.component('LazyImage', LazyImage(lazy));
}
}
}

我们先来看 LazyImage 组件。

  1. lazy-image 组件

传入 lazy 实例对象作为参数,默认导出一个返回 vue 组件对象的函数。

我看这块源码时,调试发现由于 render 函数 vue2 和 vue3 写法不同,导致报错。于是提了一个PR,修复了这个问题。

fix(lazyload): lazy-image h is not a function (#11229)

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
js复制代码// vant/packages/vant/src/lazyload/vue-lazyload/lazy-image.js
import { useRect } from '@vant/use';
import { loadImageAsync } from './util';
import { noop } from '../../utils';
import { h } from 'vue';

export default (lazyManager) => ({
// 对象
props: {
src: [String, Object],
tag: {
type: String,
default: 'img',
},
},
// 渲染函数,tag 默认是 img,src 是真实的 renderSrc ,还有默认的插槽
render() {
return h(
this.tag,
{
src: this.renderSrc,
},
this.$slots.default?.()
);
},
data() {
// 若干参数
return {
el: null,
options: {
src: '',
error: '',
loading: '',
attempt: lazyManager.options.attempt,
},
state: {
loaded: false,
error: false,
attempt: 0,
},
renderSrc: '',
};
},
// 拆分到下方
});

7.1 watch、created、mounted、beforeUnmount

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
js复制代码export default (lazyManager) => ({
// props.src 监听变化 执行
// 把 Vue 实例对象 this 添加到 lazy 实例中
// 执行 lazyLoaderHandler 函数(发现节点(元素)在视口,触发 load 事件)
watch: {
src() {
this.init();
lazyManager.addLazyBox(this);
lazyManager.lazyLoadHandler();
},
},
created() {
// 初始化
this.init();
// 默认值是占位图
this.renderSrc = this.options.loading;
},
mounted() {
this.el = this.$el;
// 把 Vue 实例对象 this 添加到 lazy 实例中
// 执行 lazyLoaderHandler 函数(发现节点(元素)在视口,触发 load 事件)
lazyManager.addLazyBox(this);
lazyManager.lazyLoadHandler();
},
beforeUnmount() {
// 移除组件
lazyManager.removeComponent(this);
},
methods: {
// 省略,下文讲述
},
})

我们可以看出,主要有以下三个实例方法,下文细述。

1
2
3
4
5
6
js复制代码// 把 Vue 实例对象 this 添加到 lazy 实例中
lazyManager.addLazyBox(this);
// 执行 lazyLoaderHandler 函数(发现节点(元素)在视口,触发 load 事件)
lazyManager.lazyLoadHandler();
// 移除组件
lazyManager.removeComponent(this);

7.2 methods init 初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码export default (lazyManager) => ({
methods: {
init() {
const { src, loading, error } = lazyManager.valueFormatter(this.src);
this.state.loaded = false;
this.options.src = src;
this.options.error = error;
this.options.loading = loading;
this.renderSrc = this.options.loading;
},
},
}

7.3 methods checkInView 检查元素是否在视图中的函数

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码export default (lazyManager) => ({
// 检测是否已经在视图中
checkInView() {
const rect = useRect(this.$el);
return (
rect.top < window.innerHeight * lazyManager.options.preLoad &&
rect.bottom > 0 &&
rect.left < window.innerWidth * lazyManager.options.preLoad &&
rect.right > 0
);
},
})

这里主要是用了 useRect 组合式 API。

vant 文档:useRect 获取元素的大小及其相对于视口的位置

获取元素的大小及其相对于视口的位置,等价于 Element.getBoundingClientRect。

更多介绍,我在文章 分析 vant4 源码,如何用 vue3 + ts 开发一个瀑布流滚动加载的列表组件?分析过,这里就不在赘述了。

7.4 methods load 函数

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
js复制代码export default (lazyManager) => ({
methods: {
load(onFinish = noop) {
// 默认加载三次,三次加载还失败,不是生产环境、没有开启静默模式,就提示报错
if (this.state.attempt > this.options.attempt - 1 && this.state.error) {
if (
process.env.NODE_ENV !== 'production' &&
!lazyManager.options.silent
) {
console.log(
`[@vant/lazyload] ${this.options.src} tried too more than ${this.options.attempt} times`
);
}

onFinish();
return;
}
const { src } = this.options;
loadImageAsync(
{ src },
({ src }) => {
this.renderSrc = src;
// 加载成功,设置状态
this.state.loaded = true;
},
() => {
// 错误加载次数 +1
this.state.attempt++;
this.renderSrc = this.options.error;
this.state.error = true;
}
);
},
}
});

我们来看 loadImageAsync 函数。

7.5 loadImageAsync 加载图片

mdn 文档:HTMLImageElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码export const loadImageAsync = (item, resolve, reject) => {
const image = new Image();

if (!item || !item.src) {
return reject(new Error('image src is required'));
}

image.src = item.src;
// 图片 crossorigin 属性
if (item.cors) {
image.crossOrigin = item.cors;
}

// 图片加载成功
image.onload = () =>
resolve({
naturalHeight: image.naturalHeight,
naturalWidth: image.naturalWidth,
src: image.src,
});

// 图片加载失败
image.onerror = (e) => reject(e);
};

我们接着来看 lazy.js 文件中的 Lazy 类。

  1. lazy 类

我们来看 lazy.js 主结构,拥有若干实例方法。

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
js复制代码export default function () {
return class Lazy {
constructor({}){}
// update config
config(options = {}) {}
// output listener's load performance
performance() {}
// add lazy component to queue
addLazyBox(vm) {}
// add image listener to queue
add(el, binding, vnode) {}
// update image src
update(el, binding, vnode) {}
// remove listener form list
remove(el) {}
// remove lazy components form list
removeComponent(vm) {}
// 设置模式
setMode(mode) {}
// add listener target
addListenerTarget(el) {}
// remove listener target or reduce target childrenCount
removeListenerTarget(el) {}
// add or remove eventlistener
initListen(el, start) {}
initEvent() {}
// find nodes which in viewport and trigger load
lazyLoadHandler() {}
// init IntersectionObserver
// set mode to observer
initIntersectionObserver() {}
// init IntersectionObserver
observerHandler(entries) {}
// set element attribute with image'url and state
elRenderer(listener, state, cache){}
// generate loading loaded error image url
valueFormatter(value) {}
}
}

8.1 构造函数 Lazy

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
js复制代码export default function () {
return class Lazy {
constructor({}){
this.mode = modeType.event;
this.listeners = [];
this.targetIndex = 0;
this.targets = [];
// 省略若干代码
this.options = {
silent,
dispatchEvent: !!dispatchEvent,
throttleWait: throttleWait || 200,
preLoad: preLoad || 1.3,
preLoadTop: preLoadTop || 0,
error: error || DEFAULT_URL,
loading: loading || DEFAULT_URL,
attempt: attempt || 3,
scale: scale || getDPR(scale),
ListenEvents: listenEvents || DEFAULT_EVENTS,
supportWebp: supportWebp(),
filter: filter || {},
adapter: adapter || {},
observer: !!observer,
observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS,
};
this.initEvent();
this.imageCache = new ImageCache({ max: 200 });
// 节流函数
this.lazyLoadHandler = throttle(
this.lazyLoadHandler.bind(this),
this.options.throttleWait
);

this.setMode(this.options.observer ? modeType.observer : modeType.event);
}
}
}

值得一提的是:throttle 节流函数,包裹 lazyLoadHandler。为了防止多次快速加载,影响性能。

8.2 实例方法 lazyLoadHandler

英文注释:发现节点(元素)在视口,触发 load 事件

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
js复制代码/**
* find nodes which in viewport and trigger load
* @return
*/
lazyLoadHandler() {
const freeList = [];
this.listeners.forEach((listener) => {
// 如果是 vue 组件,listener 则是 vm (也就是 this)

// 没有元素或者没有父级元素的,存入 freeList 数组,便于移除,销毁
if (!listener.el || !listener.el.parentNode) {
freeList.push(listener);
}
// 检测在视图中,触发 load 函数。
const catIn = listener.checkInView();
if (!catIn) return;
listener.load();
});
freeList.forEach((item) => {
remove(this.listeners, item);
// vm
item.$destroy();
});
}
}

8.3 setMode 设置模式:事件模式还是 observer 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码import { inBrowser } from '@vant/use';

// 检测是否支持 IntersectionObserver API
export const hasIntersectionObserver =
inBrowser &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype;

// 两种模式,事件或者监测
export const modeType = {
event: 'event',
observer: 'observer',
};
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
js复制代码setMode(mode) {
// 不支持 IntersectionObserver API 改为事件模式
if (!hasIntersectionObserver && mode === modeType.observer) {
mode = modeType.event;
}

this.mode = mode; // event or observer

if (mode === modeType.event) {
if (this.observer) {
// 停止观测
this.listeners.forEach((listener) => {
this.observer.unobserve(listener.el);
});
this.observer = null;
}

// 初始化事件
this.targets.forEach((target) => {
this.initListen(target.el, true);
});
} else {
// 移除事件
this.targets.forEach((target) => {
this.initListen(target.el, false);
});
// 初始化
this.initIntersectionObserver();
}
}

8.4 initListen 初始化监听事件

添加和移除事件监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码const DEFAULT_EVENTS = [
'scroll',
'wheel',
'mousewheel',
'resize',
'animationend',
'transitionend',
'touchmove',
];

this.options = {
ListenEvents: listenEvents || DEFAULT_EVENTS,
}
/*
* add or remove eventlistener
* @param {DOM} el DOM or Window
* @param {boolean} start flag
* @return
*/
initListen(el, start) {
this.options.ListenEvents.forEach((evt) =>
(start ? on : off)(el, evt, this.lazyLoadHandler)
);
}

8.4.1 on、off 监听事件,移除事件

1
2
3
4
5
6
js复制代码export function on(el, type, func) {
el.addEventListener(type, func, {
capture: false,
passive: true,
});
}
1
2
3
4
js复制代码// vant/packages/vant/src/lazyload/vue-lazyload/util.js
export function off(el, type, func) {
el.removeEventListener(type, func, false);
}

8.5 initIntersectionObserver 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码/**
* init IntersectionObserver
* set mode to observer
* @return
*/
initIntersectionObserver() {
if (!hasIntersectionObserver) {
return;
}

this.observer = new IntersectionObserver(
this.observerHandler.bind(this),
this.options.observerOptions
);

if (this.listeners.length) {
this.listeners.forEach((listener) => {
this.observer.observe(listener.el);
});
}
}

8.6 observerHandler 观测,触发 load 事件

mdn 文档:IntersectionObserverEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码/**
* init IntersectionObserver
* @return
*/
observerHandler(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.listeners.forEach((listener) => {
// 如果加载完成了,就移除监听
if (listener.el === entry.target) {
if (listener.state.loaded)
return this.observer.unobserve(listener.el);
listener.load();
}
});
}
});
}

8.7 实例方法 addLazyBox 添加要懒加载的组件到队列(数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码/*
* add lazy component to queue
* @param {Vue} vm lazy component instance
* @return
*/
addLazyBox(vm) {
this.listeners.push(vm);
// 浏览器环境
if (inBrowser) {
// 把
this.addListenerTarget(window);
// 如果是监听 observer 模式,监听 new IntersectionObserver().observe(vm.el)
this.observer && this.observer.observe(vm.el);
if (vm.$el && vm.$el.parentNode) {
// 加入父级
this.addListenerTarget(vm.$el.parentNode);
}
}
}

8.8 实例方法 removeComponent 移除组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码/*
* remove lazy components form list
* @param {Vue} vm Vue instance
* @return
*/
removeComponent(vm) {
if (!vm) return;
remove(this.listeners, vm);
this.observer && this.observer.unobserve(vm.el);
// 移除父级
if (vm.$parent && vm.$el.parentNode) {
this.removeListenerTarget(vm.$el.parentNode);
}
// 移除 window 元素
this.removeListenerTarget(window);
}

8.9 addListenerTarget 添加事件的目标元素

比如 window 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码/*
* add listener target
* @param {DOM} el listener target
* @return
*/
addListenerTarget(el) {
if (!el) return;
let target = this.targets.find((target) => target.el === el);
if (!target) {
target = {
el,
id: ++this.targetIndex,
childrenCount: 1,
listened: true,
};
this.mode === modeType.event && this.initListen(target.el, true);
this.targets.push(target);
} else {
target.childrenCount++;
}
return this.targetIndex;
}

8.10 removeListenerTarget 移除事件的目标元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码/*
* remove listener target or reduce target childrenCount
* @param {DOM} el or window
* @return
*/
removeListenerTarget(el) {
this.targets.forEach((target, index) => {
if (target.el === el) {
target.childrenCount--;
if (!target.childrenCount) {
this.initListen(target.el, false);
this.targets.splice(index, 1);
target = null;
}
}
});
}
  1. 总结

大致流程:

  • 事件模式
1
2
3
4
bash复制代码1. 初始化在元素(比如是 window,但不一定是 window)添加监听滚动和其他相关事件
2. 使用 Element.getBoundingClientRect API 获取元素的大小及其相对于视口的位置,判断是否进入可视化区
3. 进入可视区触发 load 事件,将图片设置 src 真实的图片路径,从而自动加载图片
4. 离开销毁监听的事件、和移除绑定事件的元素
  • observer 模式

主要是第二步用 IntersectionObserver API。

1
2
3
4
5
6
js复制代码// 把 Vue 实例对象 this 添加到 lazy 实例中
lazyManager.addLazyBox(this);
// 执行 lazyLoaderHandler 函数(发现节点(元素)在视口 checkInView,触发 load 事件)
lazyManager.lazyLoadHandler();
// 移除组件
lazyManager.removeComponent(this);

在 load 事件中,调用 loadImageAsync 函数。

1
2
3
4
js复制代码const image = new Image();
image.src = xxx;
image.onload = () => {}
image.onerror = () => {}

行文至此,我们就算分析完了 lazyload 组件。

其中,有很多细节处理值得我们学习。
比如:

  • 监听事件,不仅仅是 scroll 事件,还有'scroll','wheel','mousewheel','resize','animationend','transitionend','touchmove'
  • 监听本身数组存起来了
  • 目标元素也用数组存起来了。

install 函数主要有以下实现:

  • 把 lazy 实例对象添加到全局上
  • 注册懒加载组件
  • 注册图片组件
  • 注册指令 lazy
  • 注册指令 lazy-container 没有分析。

但限于篇幅原因,组件源码还有指令部分没有分析。
感兴趣的小伙伴可以自行分析学习。

如果看完有收获,欢迎点赞、评论、分享支持。你的支持和肯定,是我写作的动力。

  1. 加源码共读群交流

最后可以持续关注我@若川。我会写一个组件库源码系列专栏,欢迎大家关注。

我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.1k+人)第一的专栏,写有20余篇源码文章。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【AI】浅谈使用正则化防止过拟合(下) 前言 正则化 (Re

发表于 2022-11-28

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

对于机器学习问题,我们最常遇到的一个问题便是过拟合。在对已知的数据集合进行学习的时候,我们选择适应度最好的模型最为最终的结果。虽然我们选择的模型能够很好的解释训练数据集合,但却不一定能够很好的解释测试数据或者其他数据,也就是说这个模型过于精细的刻画了训练数据,对于测试数据或者其他新的数据泛化能力不强。

因此,我们需要通过正则化的方法来防止过拟合,接下来跟博主一起来了解一下吧。

在上篇博文 【AI】浅谈使用正则化防止过拟合(上) 中讲述了过拟合产生的原因,以及简单的描述了一下正则化是如何解决过拟合的,接下来将详细展开讲述正则化及权重减少;

正则化 (Regularization)

机器学习中几乎都可以看到损失函数后面会添加一个额外项,常用的额外项一般有两种,一般英文称作 ℓ1−normℓ1-normℓ1−norm 和 ℓ2−normℓ2-normℓ2−norm,中文称作 L1 正则化 和 L2 正则化,或者 L1 范数 和 L2 范数。

最常用的范数就是 p−范数p-范数p−范数,即一般范数 LpL_pLp,若 x=[x1,x2,…,xn]Tx=[x_1,x_2,…,x_n]^Tx=[x1,x2,…,xn]T,那么

∣∣x∣∣p=(∣x1∣p+∣x2∣p+…+∣xn∣p)1p=(∑i=1n∣xi∣p)1p||x||_p = (|x_1|^p+|x_2|^p+…+|x_n|^p)^{\frac{1}{p}} = (\sum_{i=1}^n |x_i|^p)^{\frac{1}{p}}∣∣x∣∣p=(∣x1∣p+∣x2∣p+…+∣xn∣p)p1=(i=1∑n∣xi∣p)p1
当 ppp 取 1,2 的时候分别是以下最简单的情形:

  • L1:∣∣x∣∣1=∣x1∣+∣x2∣+…+∣xn∣=∑i=1n∣xi∣L1: ||x||_1 = |x_1|+|x_2|+…+|x_n|= \sum_{i=1}^n|x_i|L1:∣∣x∣∣1=∣x1∣+∣x2∣+…+∣xn∣=∑i=1n∣xi∣
  • L2:∣∣x∣∣2=(∣x1∣2+∣x2∣2+…+∣xn∣2)12=∑i=1nxi2L2: ||x||_2 = (|x_1|^2+|x_2|^2+…+|x_n|^2)^{\frac{1}{2}}= \sqrt{\sum_{i=1}^n x_i^2}L2:∣∣x∣∣2=(∣x1∣2+∣x2∣2+…+∣xn∣2)21=∑i=1nxi2

分别几个范数表示:

  • L0L0L0:指向量中非0的元素的个数;
  • L1L1L1:指向量中各个元素绝对值之和;
  • L2L2L2:指向量各元素的平方和的平方根;

用代码来简单表示一下 L1L1L1,L2L2L2:

1
2
3
4
5
6
7
8
9
10
11
py复制代码import torch

u = torch.tensor([3.0, -4.0])

# L1
torch.abs(u).sum()
# tensor(5.)

# L2
torch.norm(u)
# tensor(7.)

L1 正则化和 L2 正则化可以看做是损失函数的惩罚项。所谓『惩罚』是指对损失函数中的某些参数做一些限制。对于线性回归模型,使用 L1 正则化的模型建叫做 Lasso 回归,使用 L2 正则化的模型叫做 Ridge 回归(岭回归)。

下面是 Python 中 Lasso 回归的损失函数,式中加号后面一项 α∣∣w∣∣1α||w||_1α∣∣w∣∣1 即为 L1 正则化项。

min⁡w12nsamples∣∣Xw−y∣∣22+α∣∣w∣∣1\min_w \frac{1}{2n_{samples}}||X_w-y||^2_2 + α||w||_1wmin2nsamples1∣∣Xw−y∣∣22+α∣∣w∣∣1
下面是 Python 中 Ridge 回归的损失函数,式中加号后面一项 α∣∣w∣∣22α||w||_2^2α∣∣w∣∣22 即为 L2 正则化项。

min⁡w∣∣Xw−y∣∣22+α∣∣w∣∣22\min_w ||X_w-y||^2_2 + α||w||_2^2wmin∣∣Xw−y∣∣22+α∣∣w∣∣22
一般回归分析中 www 表示特征的系数,从上式可以看到正则化项是对系数做了处理(限制)。

L1 正则化和 L2 正则化的说明如下:

  • L1 正则化是指权值向量 www 中各个元素的绝对值之和,通常表示为 ∣∣w∣∣1||w||_1∣∣w∣∣1 ;
  • L2 正则化是指权值向量 www 中各个元素的平方和然后再求平方根(可以看到 Ridge 回归的 L2 正则化项有平方符号),通常表示为 ∣∣w∣∣2||w||_2∣∣w∣∣2 ;

一般都会在正则化项之前添加一个系数,Python 的机器学习包 sklearn 中用 ααα 表示,一些文章也用 λλλ 表示,这个系数需要用户指定。

L1 正则化和 L2 正则化的作用:

  • L1 正则化可以产生稀疏权值矩阵,即产生一个稀疏模型,可以用于特征选择;
  • L2 正则化可以防止模型过拟合(overfitting),一定程度上,L1L1L1 也可以防止过拟合;

深入理解

L1 正则化

假设有如下带 L1 正则化 的损失函数:

J=J0+α∑w∣w∣J = J_0 + α \sum_w|w|J=J0+αw∑∣w∣
其中 J0J_0J0 是原始的损失函数,加号后面的一项是 L1 正则化项,ααα 是正则化系数。注意到 L1 正则化是权值的绝对值之和,JJJ 是带有绝对值符号的函数,因此 JJJ 是不完全可微的。机器学习的任务就是要通过一些方法(比如梯度下降)求出损失函数的最小值。当我们在原始损失函数 J0J_0J0 后添加 L1 正则化项时,相当于对 J0J_0J0 做了一个约束。

令:

L=α∑w∣w∣L = α \sum_w|w|L=αw∑∣w∣
则 J=J0+LJ=J_0+LJ=J0+L,此时我们的任务变成 在 L 约束下求出 J0J_0J0 取最小值的时候的解。考虑二维的情况,即只有两个权值 w1w^1w1 和 w2w^2w2,此时 L=∣w1∣+∣w2∣L = |w^1| + |w^2|L=∣w1∣+∣w2∣。对于梯度下降法,求解 J0J_0J0 的过程可以画出等值线,同时 L1 正则化 的函数 LLL 也可以在 ∣w1∣|w^1|∣w1∣,∣w2∣|w^2|∣w2∣ 的二维平面上画出来。如下图:

image.png

图中等值线是 J0J_0J0 的等值线,黑色方形是 LLL 函数的图形。L=∣w1∣+∣w2∣L = |w^1| + |w^2|L=∣w1∣+∣w2∣,这个函数画出来就是一个方框。

在图中,当 J0J_0J0 等值线与 LLL 图形首次相交的地方就是最优解。上图中 J0J_0J0 与 LLL 在 LLL 的一个顶点处相交,这个顶点就是最优解。注意到这个顶点的值是 (w1,w2)=(0,w)(w^1, w^2) = (0, w)(w1,w2)=(0,w)。可以直观想象,因为 LLL 函数有很多『突出的角』(二维情况下四个,多维情况下更多),J0J_0J0 与这些角接触的机率会远大于与 LLL 其它部位接触的机率(这是很直觉的想象,突出的角比直线的边离等值线更近些),而在这些角上,会有很多权值等于0(因为角就在坐标轴上),这就是为什么 L1 正则化可以产生稀疏模型,进而可以用于特征选择。

而正则化前面的系数 ααα,可以控制 LLL 图形的大小。ααα 越小,LLL 的图形越大(上图中的黑色方框);ααα 越大,LLL 的图形就越小,可以小到黑色方框只超出原点范围一点点,这时最优点的值 (w1,w2)=(0,w)(w^1, w^2) = (0, w)(w1,w2)=(0,w) 中的 www 可以取到很小的值。

L2 正则化

类似地,假设有如下带 L2 正则化的损失函数:

J=J0+a∑ww2J = J_0 + a\sum_ww^2J=J0+aw∑w2
同样可以画出他们在二维平面上的图形,如下:

image.png

二维平面下 L2 正则化的函数图形是个圆(绝对值的平方和,是个圆),与方形相比,被磨去了棱角。因此 J0J_0J0 与 LLL 相交时使得 w1w^1w1 或 w2w^2w2 等于零的机率小了许多(这个也是一个很直观的想象) ,这就是为什么 L2 正则化不具有稀疏性的原因,因为不太可能出现多数 www 都为0的情况。

L2 正则化可以获得值很小的参数

以线性回归中的梯度下降法为例,使用 Andrew Ng 机器学习的参数表示方法。假设要求解的参数为 θθθ,hθ(x)h_\theta(x)hθ(x) 是我们的假设函数。线性回归一般使用平方差损失函数。单个样本的平方差是 (hθ(x)−y)2(h_\theta(x)-y)^2(hθ(x)−y)2,如果考虑所有样本,损失函数是对每个样本的平方差求和,假设有 mmm 个样本,线性回归的代价函数如下,为了后续处理方便,乘以一个常数 12m\frac{1}{2m}2m1:

J(θ)=12m∑i=1m(hθ(x(i))−y(i))2J(\theta) = \frac{1}{2m} \sum_{i=1}^m (h_\theta(x^{(i)})-y^{(i)})^2J(θ)=2m1i=1∑m(hθ(x(i))−y(i))2
中间推导过程不再赘述,可以参考这篇博文:【AI】浅谈梯度下降算法(理论篇)

梯度下降算法中,为了尽快收敛,会沿梯度的负方向更新参数,因此在上式前添加一个负号,并乘以一个系数 ααα(即学习率),得到最终用于迭代计算参数 θj\theta_jθj 的形式:

θj:=θj−α1m∑i=1m(hθ(x(i))−y(i))xj(i)\theta_j := \theta_j - α\frac{1}{m}\sum^m_{i=1}(h_\theta(x^{(i)})-y^{(i)})x_j^{(i)}θj:=θj−αm1i=1∑m(hθ(x(i))−y(i))xj(i)
其中 ααα 是学习率(learning rate)。 上式是没有添加 L2 正则化项的迭代公式,如果在原始代价函数之后添加 L2 正则化,则迭代公式会变成下面的样子:

θj:=θj(1−αλm)−α1m∑i=1m(hθ(x(i))−y(i))xj(i)\theta_j := \theta_j(1-α\frac{\lambda}{m}) - α\frac{1}{m}\sum^m_{i=1}(h_\theta(x^{(i)})-y^{(i)})x_j^{(i)}θj:=θj(1−αmλ)−αm1i=1∑m(hθ(x(i))−y(i))xj(i)
其中 λλλ 就是正则化参数。从上式可以看到,与未添加 L2 正则化的迭代公式相比,每一次迭代,θj\theta_jθj 都要先乘以一个小于1的因子(即 (1−αλm)(1-α\frac{\lambda}{m})(1−αmλ) ,从而使得 θj\theta_jθj 不断减小,因此总的来看,θθθ 是不断减小的。

最开始也提到 L1 正则化一定程度上也可以防止过拟合。之前做了解释,当 L1 的正则化系数很小时,得到的最优解会很小,可以达到和 L2 正则化类似的效果。

权重减少

L1L1L1 和 L2L2L2 的目的是通过减少 www 的权重从而减少模型的复杂度,从而提高模型的泛华能力,为什么会这样呢?启发式地来说,如果代价函数没有正则化,那么权重向量的长度倾向于增长,而其它的都不变。随着时间推移,权重向量将会变得非常大。这可能导致权重向量被限制得或多或少指向同一个方向,因为当长度过长时,使代价函数的最优解发生偏离。

下面将从数学的角度出发给出了正则化为什么能减少 www 的权重,机器学习的实践应用中也得到了验证,但是正则化到底是如何影响模型的泛化能力,并没有科学的数据依据,难道 www 按照某种规则增加就不能提高模型的泛化能力吗?

从机器学习的领域经验表明:减少 www 的权重从而减少模型的复杂度,提高模型的泛化能力,因此正则化处理手段在机器学习的模型上得到广泛应用。


L2L2L2 权重衰减

L2 正则化就是在代价函数后面再加上一个正则化项:

C=C0+λ2n∑ww2C = C_0 + \frac{\lambda}{2n} \sum_w w^2C=C0+2nλw∑w2
C0C_0C0 代表原始的代价函数,后面那一项就是 L2 正则化项,它是这样来的:所有参数 www 的平方的和,除以训练集的样本大小 nnn。λλλ 就是正则项系数,权衡正则项与 C0C_0C0 项的比重。另外还有一个系数 12\frac{1}{2}21,12\frac{1}{2}21 经常会看到,主要是为了后面求导的结果方便,后面那一项求导会产生一个2,与 12\frac{1}{2}21 相乘刚好凑整。

L2 正则化项是怎么避免 overfitting 的呢?我们推导一下看看,先求导:

∂C∂w=∂C0∂w+λnw∂C∂b=∂C0∂b\frac{∂C}{∂w} = \frac{∂C_0}{∂w} + \frac{\lambda}{n}w \
\quad \
\frac{∂C}{∂b} = \frac{∂C_0}{∂b}∂w∂C=∂w∂C0+nλw∂b∂C=∂b∂C0
可以发现 L2 正则化项对 bbb 的更新没有影响,但是对于 www 的更新有影响:

w→w−η∂C0∂w−ηλnw=(1−ηλn)w−η∂C0∂ww \rightarrow w - \eta\frac{∂C_0}{∂w} - \frac{\eta\lambda}{n}w \
\quad \
\quad = (1-\frac{\eta\lambda}{n})w - \eta\frac{∂C_0}{∂w}w→w−η∂w∂C0−nηλw=(1−nηλ)w−η∂w∂C0
在不使用 L2 正则化时,求导结果中 www 前系数为1,现在 www 前面系数为 1−ηλn1-\frac{\eta\lambda}{n}1−nηλ ,因为 ηηη、λλλ、nnn 都是正的,所以 1−ηλn1-\frac{\eta\lambda}{n}1−nηλ 小于1,它的效果是减小 www,这也就是权重衰减(weight decay)的由来。当然考虑到后面的导数项,www 最终的值可能增大也可能减小。

另外,需要提一下,对于基于 mini-batch 的随机梯度下降,www 和 bbb 更新的公式跟上面给出的有点不同:

w→(1−ηλn)w−ηm∑x∂Cx∂wb→b−ηm∑x∂Cx∂bw \rightarrow (1-\frac{\eta\lambda}{n})w - \frac{\eta}{m} \sum_x \frac{∂C_x}{∂w} \
\quad \
b \rightarrow b - \frac{\eta}{m} \sum_x \frac{∂C_x}{∂b}w→(1−nηλ)w−mηx∑∂w∂Cxb→b−mηx∑∂b∂Cx
对比上面 www 的更新公式,可以发现后面那一项变了,变成所有导数加和,乘以 ηηη 再除以 mmm,mmm 是一个 mini-batch 中样本的个数。

到目前为止,我们只是解释了 L2 正则化项有让 www “变小” 的效果,但是还没解释为什么 www “变小” 可以防止 overfitting?一个所谓 “显而易见” 的解释就是:更小的权值 www,从某种意义上说,表示网络的复杂度更低,对数据的拟合刚刚好(这个法则也叫做奥卡姆剃刀),而在实际应用中,也验证了这一点,L2 正则化的效果往往好于未经正则化的效果。当然,对于很多人(包括我)来说,这个解释似乎不那么显而易见,所以这里添加一个稍微数学一点的解释(引自知乎):

过拟合的时候,拟合函数的系数往往非常大,为什么?如下图所示,过拟合,就是拟合函数需要顾忌每一个点,最终形成的拟合函数波动很大。在某些很小的区间里,函数值的变化很剧烈。这就意味着函数在某些小区间里的导数值(绝对值)非常大,由于自变量值可大可小,所以只有系数足够大,才能保证导数值很大。
image.png

而正则化是通过约束参数的范数使其不要太大,所以可以在一定程度上减少过拟合情况。


L1L1L1 权重衰减

在原始的代价函数后面加上一个 L1 正则化项,即所有权重 www 的绝对值的和,乘以 λn\frac{λ}{n}nλ(这里不像 L2 正则化项那样,需要再乘以 12\frac{1}{2}21,具体原因上面已经说过。)

C=C0+λn∑w∣w∣C = C_0 + \frac{\lambda}{n} \sum_w |w|C=C0+nλw∑∣w∣
同样先计算导数:

∂C∂w=∂C0∂w+λnsgn(w)\frac{∂C}{∂w} = \frac{∂C_0}{∂w} + \frac{\lambda}{n}sgn(w)∂w∂C=∂w∂C0+nλsgn(w)
上式中 sgn(w)sgn(w)sgn(w) 表示 www 的符号。那么权重 www 的更新规则为:

w→w′=w−ηλnsgn(w)−η∂C0∂ww \rightarrow w’ = w - \frac{\eta\lambda}{n}sgn(w) - \eta\frac{∂C_0}{∂w}w→w′=w−nηλsgn(w)−η∂w∂C0
比原始的更新规则多出了 ηλnsgn(w)\frac{\eta\lambda}{n}sgn(w)nηλsgn(w) 这一项。当 www 为正时,更新后的 www 变小。当 www 为负时,更新后的 www 变大——因此它的效果就是让 www 往0靠,使网络中的权重尽可能为0,也就相当于减小了网络复杂度,防止过拟合。

另外,上面没有提到一个问题,当 www 为0时怎么办?当 www 等于0时,∣w∣|w|∣w∣ 是不可导的,所以我们只能按照原始的未经正则化的方法去更新 www,这就相当于去掉 ηλnsgn(w)\frac{\eta\lambda}{n}sgn(w)nηλsgn(w) 这一项,所以我们可以规定 sgn(0)=0sgn(0)=0sgn(0)=0,这样就把 w=0w=0w=0 的情况也统一进来了。

image.png

后记

以上就是 浅谈使用正则化防止过拟合(下) 的全部内容了,具体讲解了什么是正则化,并进行深入理解,以及 L1L1L1、L2L2L2 是如何进行权重衰减的,通过图文结合,公式推导,细致地讲述了要点,希望大家有所收获!

📝 上篇精讲:【AI】浅谈使用正则化防止过拟合(上)

💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;

👍 创作不易,请多多支持;

🔥 系列专栏:AI

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构与算法

发表于 2022-11-28

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。

本文是数据结构与算法系列的第 8 篇文章,完整文章目录请移步到文章末尾~

前言

大家好,我是小彭。

在前面的文章里,我们聊到过数组、链表、栈和队列等基础数据结构,也聊到过它们的一些衍生数据结构,例如 单调栈、单调队列、散列表 等等,这些都是线性表或者是以线性表为主体的数据结构。

从这篇文章开始,我们来讨论非线性的数据结构 —— 树和图。非线性数据结构相比于线性表结构要复杂得多,涉及到的内容也更广。虽然树也是一种特殊的图,但是考虑到树的特点更容易理解也更常用,所以我们把树单独分类,而把图当成树的衍生数据结构。

今天,我们就先从树的基本概念开始,并逐渐延伸到二叉堆、红黑树、线段树、树状数组、图等更复杂的数据结构上,请关注。


思维导图:


  1. 隐藏在递归代码中的树

其实,很多代码中都存在树型结构,比如递归代码。

递归 是一种通过 “函数自己调用自己” 的方式,将问题重复地分解为同类子问题,并最终解决问题的编程技巧。当你在使用递归思想解决问题的时候,往往你也是在使用树型结构解决问题。

举个例子,要求一个数 nnn 的阶乘 n!=n∗(n−1)∗(n−2)∗…∗2∗1n! = n*(n-1)*(n-2)*…*2*1n!=n∗(n−1)∗(n−2)∗…∗2∗1 。如果使用递归思想解决问题是,我们可以把 n!n!n! 的问题拆分为一个 (n−1)!(n-1)!(n−1)! 的问题,如果我们知道 (n−1)!(n-1)!(n−1)! 的解,那么将它乘以 nnn 就可以得出 n!n!n! 的解。以此类推。

求 n!

由于阶乘在每次递归调用时,原问题和子问题只是 “线性” 地减少一个问题规模,所以这里面的树型结构可能还不太明显。 那我们可以再举一个稍微复杂的例子:求斐波那契数列,LeetCode · 509. 斐波那契数:该数列从 111 开始,每一项数字都是前面两项数字的和。

这个问题我们可以使用递归的方式自顶向下地分解问题,在编码上是很容易实现的。如果我们把代码一层层分解的过程画成图形,我们会发现代码自然地形成了一个树。 这棵在递归调用中隐式存在的树我们也称之为递归树。

递归(未优化)

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class Solution {
fun fib(N: Int): Int {
if(N == 0){
return 0
}
if(N == 1){
return 1
}
// 拆分问题 + 组合结果
return fib(N - 1) + fib(N - 2)
}
}

递归是一种树型结构


  1. 什么是树(Tree)?

2.1 树的逻辑结构

树这种数据结构与现实中的 “树” 在形态上非常相似, 树的逻辑结构是一种非线性结构,相邻的父子节点形成 1 对 N 的关系,树的节点中包含元素值和所有子节点的列表。

从这阶乘和斐波那契数列的例子可以看出,线性表和树在结构上的主要区别在于相邻节点的引用关系上:

  • 1、线性结构: 前驱节点和后继节点的引用关系为 1 对 1;
  • 2、树型结构: 前驱节点和后继节点的引用关系为 1 对 N;
  • 3、图型结构: 再进一步延伸,当前驱节点和后继节点的引用关系为 N 对 N 时就是图型结构。如果按照图的理论来说,树可以看作是一种特殊的图,即包含 N 个节点和 N - 1 条边的有向无环图,树的方向是从根节点指向叶子节点。

2.2 树的物理实现

树的物理结构可以采用数组或链表实现:

  • 1、链表实现: 即通过引用来建立节点之间的父子关系,每个节点都持有子节点的引用。树的链表实现更简单直接,所以大部分的二叉树代码也是通过链表结构来实现的;
  • 2、数组实现: 即通过数组下标来建立节点之间的父子关系,节点内部不需要存储子节点引用的内存空间。

在额外内存消耗上,链表实现在子节点指针有额外内存占用,而数组实现会在数组本身上存在额外内存占用。如果树是一棵非常稀疏的树的话,树的数组实现会导致数组中存在非常多的闲置空间。

树的数组实现有 2 种计算节点位置的方式:

  • 方式 1 - 根节点存储在第 [0] 位:
+ 对于第 [i] 位上的节点,第 [2 \* i +1] 位是左节点,第 [2 \* i + 2] 位是右节点;
+ 对于第 [i] 位上的节点,第 [(i-1) / 2] 位是父节点。
  • 方式 2 - 根节点存储在第 [1] 位:
+ 第 [0] 位不存储,根节点存储在第 [1] 位
+ 对于第 [i] 位上的节点,第 [2 \* i] 位是左节点,第[2 \* i + 1] 位是右节点
+ 对于第 [i] 位上的节点,第 [i / 2] 位是父节点

2.3 树的基本概念

  • 深度(Depth): 表示节点到根节点的距离(可以参考递归栈的深度理解,递归越深的节点深度越大);
  • 层级(Level): 也是表示节点的深度,数值是深度 + 1;
  • 高度(Height): 表示节点到叶子节点的距离;
  • 宽度: 表示一层节点中最左节点与最右节点之间的长度(两端之间的空节点也计入宽度);

2.4 树的路径

  • 路径 / 最短路径: 指两个节点之间经过的所有节点。由于树是有方向的,因此路径一定会经过两个节点的最近共同祖先,树中的路径概念本身就是最短路径;
  • 距离 / 最短距离: 即路径长度,指两个节点的路径上所有边的个数。同理树中的距离概念本身就是最短距离;
  • 权: 表示一个节点的权重;
  • 带权路径长度: 表示一个节点到根节点的路径长度与节点权重的乘积;
  • 树的带权路径长度: 表示一棵树中所有节点的带权路径长度之和,简称 WPL,Weighted Path Length of Tree;
  • 最优树: 表示带权路径长度最小的树,即哈夫曼树。哈夫曼树其实只是生成哈夫曼编码的一个工具,我们在专栏后续文章里再讨论。

树的路径


  1. 二叉树与特殊二叉树

前面我们将的树都是普通的树的特征,也叫多叉树,即节点的子节点个数不固定,可以是一个,两个或者多个。不过我们最常见的还是二叉树,即每个节点最多只有两个子节点,分别是左子节点和右子节点。

下面讨论常见的几种特殊的二叉树:

3.1 满二叉树(Full Binary Tree)

对于一棵满二叉树来说,任何一棵子树上都是满二叉树。

满二叉树的每一层节点都是铺满的,除了最底层的节点外,所有节点都有左右两个子节点,所有的叶子节点也集中在树的最底层。

3.2 完全二叉树(Complete Binary Tree)

对于一棵完全二叉树来说,任何一棵子树都是完全二叉树。

完全二叉树与满二叉树类似,但没有那么 “满”。除了最底下两层节点外,大部分节点都有左右两个子节点,所有的节点也都铺满在树的左上角,所有的叶子节点都集中在树的最底下两层。

那么,为什么要定义完全二叉树这种数据结构呢?这就需要跟稀疏的树做相比了。稀疏的树如果用数组实现的话,会存在非常多的闲置空间。而完全二叉树或满二叉树能够完全利用数组的所有空间或前部分空间,数组的利用率更高。

3.3 二叉搜索树(Binary Search Tree,BST)

对于一棵二叉搜索树来说,任何一棵子树上都是二叉搜索树。

在二叉搜索树中,树中的任意一个节点的值,都大于(或小于)左子树上所有节点的值,且均小于(或大于)右子树上所有节点的值。

那么,为什么要定义二叉搜索树这种数据结构呢? 这就需要跟链表做对比了,二叉搜索树是为了优化链表的查询效率而产生的。链表在进行查询、插入和删除时的时间复杂度是 O(n),而在二叉查找树上进行查询、插入和删除时的时间复杂度是 O(Height),与树的高度有关。

3.4 平衡二叉搜索树(Balance Binary Search Tree)

对于一棵平衡二叉树来说,任何一棵子树上都是平衡二叉树。

平衡二叉搜索树是一种特殊的二叉搜索树,简称就是平衡二叉树。在平衡二叉树中,任何节点的左右子树高度差不大于 1。

那么,为什么要定义平衡二叉树这种数据结构呢? 这就需要跟二叉查找树做对比了,平衡二叉树能够避免二叉搜索树退化为链表。

在二叉查找树中,查找、插入、删除等操作的时间复杂度跟树的高度成正比,在最好情况下会优化为完全二叉树,而在最坏情况下会退化为链表,各项操作的时间复杂度也会退化 O(n)。而平衡二叉树会避免树的左右子树高度相差太大,各项操作的时间复杂度可以稳定在 O(logn),但是在增加和删除操作上会增加维护平衡性的开销。

3.5 红黑树(Red-Black Tree,R-B)

平衡二叉树追求的是 “完全平衡” 的状态,但是考虑到维护树的平衡性本身也是一种成本,因此实践中使用的平衡二叉树往往是 ”近似平衡“ 或 ”弱平衡“ 的。即只保证左右子树高度相对平均,而不严格准守树的高度差不大于 1 的定义,通过牺牲一部分查找的效率,来节省维持树的平衡状态的成本。

这些 “弱平衡” 的红黑树虽然不符合平衡二叉树的严格定义,但一般也视为平衡二叉树。平衡二叉搜索树有很多种,例如伸展树(Splay Tree)、树堆(Treap)、红黑树(AVL),但其中最常见的莫过于红黑树。在 Java HashMap 的分离链表法中,就采用了链表 + 红黑树的方案。

红黑树的性质如下:

  • 1、节点非红即黑;
  • 2、Null 节点是叶子节点;
  • 3、根节点和 Null 叶子节点是黑色的;
  • 4、红色节点不会连续,红色节点的子节点一定是黑节点;
  • 5、任何节点到叶子节点的路径上黑色节点的数量。

红黑树的定义很复杂, 关键在于红黑树可以满足一个节点到叶子节点的最长路径的长度不超过最短路径的 2 倍, 所以红黑树不会出现左右子树特别不平衡的情况。

为什么呢?假设最长路径上存在 N 个黑节点和 N - 1 个红色节点,由于要满足 “5、任何节点到叶子节点的路径上黑色节点的数量相同”,所以最短路径上也存在 N 个黑节点。那么即使最短路径上一个红色节点都没有,至少也有 N 个节点,所以不会超过两倍。

3.6 二叉堆(Binary Heap)

对于一个二叉堆来说,任何一棵子树上都是二叉堆。

二叉堆是一种特殊完全二叉树,在二叉堆中,树中的任意一个节点的值,都大于等于(或小于等于)左右子树上所有节点的值。

二叉堆看起来和二叉搜索树很像,但其实两者差别很大,二叉堆也并不是二叉搜索树。在二叉搜索树中会要求节点与左右子树的关系是 “左子树 < 节点 < 右子树” ,而二叉堆只要求 “节点 > 左子树 and 右子树” 。所以,二叉搜索树中兄弟节 点之间也是有序的,而二叉堆不关心兄弟节点之间的相对关系,所以对于同一组数据,我们可以构建多种不同形态的堆。

3.7 哈夫曼树(Huffman Tree)

哈夫曼树与哈夫曼编码有关。

哈夫曼编码是一种变长编码,对出现频率高的字符采用较短的码元,对出现频率低的字符采用较长的码元,从而达到无损压缩编码长度的目的。哈夫曼树也成最优二叉树,是 WPL 带权路径长度最小的树。

其实,哈夫曼树就是生成哈夫曼编码的工具,通过哈夫曼树这个数据结构能够将 “设计最优编码” 的问题转换为求 “最小带权路径长度” 的问题。


  1. 总结

今天,我们讨论了树的基本概念,也介绍了最常见的二叉树以及特殊的二叉树。在专栏接下来的文章中,我们会逐渐延伸到这些特殊的二叉树,请关注。


版权声明

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

参考资料

  • 数据结构与算法分析 · Java 语言描述(第 4、6 章)—— [美] Mark Allen Weiss 著
  • 算法导论(第 6、12、13 章 · 散列表)—— [美] Thomas H. Cormen 等 著
  • 300分钟搞定算法面试 —— 力扣&拉勾网 出品
  • 数据结构与算法之美(第 23~29 讲) —— 王争 著,极客时间 出品

推荐阅读

数据结构与算法系列完整目录如下(2023/07/11 更新):

  • #1 链表问题总结
  • #2 链表相交 & 成环问题总结
  • #3 计算器与逆波兰表达式总结
  • #4 高楼丢鸡蛋问题总结
  • #5 为什么你学不会递归?谈谈我的经验
  • #6 回溯算法解题框架总结
  • #7 下次面试遇到二分查找,别再写错了
  • #8 什么是二叉树?
  • #9 什么是二叉堆 & Top K 问题
  • #10 使用前缀和数组解决 “区间和查询” 问题
  • #11 面试遇到线段树,已经这么卷了吗?
  • #12 使用单调队列解决 “滑动窗口最大值” 问题
  • #13 使用单调栈解决 “下一个更大元素” 问题
  • #14 使用并查集解决 “朋友圈” 问题
  • #15 如何实现一个优秀的 HashTable 散列表
  • #16 简答一波 HashMap 常见面试题
  • #17 二叉树高频题型汇总
  • #18 下跳棋,极富想象力的同向双指针模拟

Java & Android 集合框架系列文章: 跳转阅读

LeetCode 上分之旅系列文章:跳转阅读

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…818283…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%