PowerLZY's Blog

本博客主要用于记录个人学习笔记(测试阶段)

AI安全实习生(日常/寒假实习)

岗位职责:

  • AI与软件安全相结合的前沿研究;

岗位需求(满足其一即可):

  • 对机器学习、深度学习等有较好的理解和掌握。在自然语言处理、大数据分析等人工智能领域有较丰富的项目或科研经验。能独立开展研究工作;
  • 有扎实的计算机学科基础知识储备,有程序语言分析逆向分析等软件安全相关技术知识或项目经验。同时了解机器学习等相关知识,对算法研究感兴趣;

4月15日

4001647676481_.pic

岗位职责】 负责腾讯安全产品的研发工作,负责云主机安全的研究工作,负责数据分析及后台研发工作,对实验室海量的复杂恶意样本进行分析,提取恶意样本特征,设计恶意样本查杀方法,研发恶意代码分析领域的自动化工具利用数据分析算法进行样本行为分析自动化以及特征提取自动化,研究前沿的程序安全和代码安全技术。

岗位要求

  • 掌握C/C++/Go中至少一门开发语言
  • 掌握Python脚本开发
  • 拥有安全研究/安全开发/攻防经验者优先
  • 了解PE/Webshell等常见恶意程序的基本结构优先

客户端安全Q&A

1、PE文件结构?

https://bbs.pediy.com/user-home-825190.htm

PE文件被称之为:“可执行文件,可以在Windows操作系统中进行加载和执行的文件”

  • Windows平台:PE(Portable Executable)文件结构,其中Portable也就是Windows系统中能够跨平台运行
  • Linux平台:ELF(Executable and Linking Format)文件结构

PE文件结构:

img
  • Dos部分

Dos分为两个部分,一个是DOS MZ文件头和DOS块

MZ文件头

IMAGE_DOS_HEADER结构体:其大小占64个字节「重点」,并且该结构中的最后一个LONG类型e_lfanew成员指向PE文件头的位置为中的PE文件头标志的地址

Dos Stub

属于链接器进行填充的,大小不一定,属于DOS部分的DOS块,无实际作用,但是可以作为注入手段进行利用!

  • PE文件头

##### PE文件头标志(PE标识)z [4 字节] + PE文件表头标准PE头)[20 字节] + 扩展文件表头(扩展PE头

  • 节表

IMAGE_SECTION_HEADER结构体(40字节) * 节的数量(大小取决于节的数量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];// 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
union {
// 共用体,也叫联合体,在一个“联合”内可以定义多种不同的数据类型,
//一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,
//以达到节省空间的目的。union变量所占用的内存长度等于最长的成员的内存长度。
DWORD PhysicalAddress; //当前节的名称 , 占8字节
DWORD VirtualSize; //内存中文件对齐大小
} Misc;
DWORD VirtualAddress; //在内存中的偏移地址 +imageOfBase=真正的位置
DWORD SizeOfRawData; //当前节在文件中对齐后的大小 也就是节点的大小 [内存和文件中是一样的]
DWORD PointerToRawData; //当前节在文件中的偏移 也就是文件的开始位置
DWORD PointerToRelocations; // 调试相关
DWORD PointerToLinenumbers;// 调试相关
WORD NumberOfRelocations;// 调试相关
WORD NumberOfLinenumbers;// 调试相关
DWORD Characteristics; //文件属性,比如该节数据属性是否为可执行属性,都在这里面
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

pc客户端安全业务有什么了解?

对windows汇编有什么了解?

ollydbg调试原理?

2、用过什么样的OllyDBG的断点?

  • 寻常断点
  • API断点
  • 内存断点
  • 硬件断点

3、win下进程间通信?

  • 文件映射
  • 共享内纯
  • 匿名管道
  • 命名管道

3、call 和 jump指令的区别和相同?

https://www.nhooo.com/note/qa0ucr.html

  1. 传送指令(4个):mov、push、pop、lea。

  2. 转移指令(8个):call、jmp、je、jne、jb、jnb、ja、jna。

  3. 运算指令(7个):add、sub、mul、div、adc、sbb、cmp。

  4. 处理机控制指令(1个):nop。

CALL指令用于调用子例程(不是主程序的一部分),但是JUMP指令更新程序计数器值并指向程序内部的另一个位置。

4、Cuckoo inline hook ?

Cuckoo对ntdll.dll, kernel32.dll, advapi32.dll,shell32.dll,msvcrt.dll,user32.dll,wininet.dll,ws2_32.dll,mswsock.dll中的170+API进行hook

Cuckoo对样本行为的捕获依靠API Inline Hook,被测样本会被注入加载一个监控模块,当样本调用某个API函数时会被劫持到Cuckoo的模块内,这个模块除了常规的做log操作然后返回原函数外还会对某些API的上下文进行篡改,目的在于劫持某些API的执行逻辑。

inline hook是一种通过修改机器码的方式来实现hook的技术。

对于正常执行的程序,它的函数调用流程大概是这样的:

img

0x1000地址的call指令执行后跳转到0x3000地址处执行,执行完毕后再返回执行call指令的下一条指令。

我们在hook的时候,可能会读取或者修改call指令执行之前所压入栈的内容。那么,我们可以将call指令替换成jmp指令,jmp到我们自己编写的函数,在函数里call原来的函数,函数结束后再jmp回到原先call指令的下一条指令。如图:

img

通过修改机器码实现的inline hook,不仅不会破坏原本的程序逻辑,而且还能执行我们的代码,读写被hook的函数的数据。

5、cuckoo windows DLL 注入

Cuckoo sandbox在样本启动的时候,注入了相关的监控代码。在process.py文件中,通过Process调用,调用inject-x86.exe或inject-x64.exe完成注入。

https://www.cnblogs.com/luoyesiqiu/p/12173609.html

DLL注入(英语:DLL injection)是一种计算机编程技术,它可以强行使另一个进程加载一个动态链接库以在其地址空间内运行指定代码[1]。在Windows操作系统上,每个进程都有独立的进程空间,即一个进程是无法直接操作另一个进程的数据的(事实上,不仅Windows,许多操作系统也是如此)。但是DLL注入是用一种不直接的方式,来实现操作其他进程的数据。假设我们有一个DLL文件,里面有操作目标进程数据的程序代码逻辑,DLL注入就是使目标进程加载这个DLL,加载后,这个DLL就成为目标进程的一部分,目标进程的数据也就可以直接操作了。

5.1 APC注入

APC 队列(Asynchronous Procedure Call 异步过程调用)是一种可以在 Windows 中使用的机制,用于将要在特定线程上下文中完成的作业排队。

APC注入,通过write_data向远程进程中写入DLL路径、Loadlibrary()执行函数指针、执行加载函数的指针,然后利用QueueUserAPC()在软中断时向线程的APC队列插入Loadlibrary()执行函数指针,达到注入DLL的目的。当线程再次被唤醒时,此线程会首先执行APC队列中被注册的函数。

5.2 CRT注入

同样通过write_data向远程进程中写入DLL路径、Loadlibrary()执行函数指针、执行加载函数的指针,之后在create_thread_and_wait中调用CreateRemoteThrea。当目标进程中执行load_library_woker(已事先写入进程空间)加载DLL,之后调用free_data清理现场,释放空间。

6、脱壳,缓冲区溢出有什么经验?

7、沙箱如何获得动态样本信息?

8、沙箱反调试?

==样本对抗沙箱?IRC Bot==

针对沙箱检测的逃逸技术

https://www.anquanke.com/post/id/152631:当通过CreateProcess创建进程时,命令行中的路径的大小写会被沙箱的hook替换掉。

因为反病毒沙箱大多都是基于虚拟机的,所以如果能保证样本在虚拟机环境中不执行那么就能大概率保证其在沙箱中不执行,针对沙箱的检测主要有如下几类:

针对虚拟机
  • 虚拟机的特殊进程/注册表信息,比如像VBoxTray.exe、VMwareTray.exe等;
  • 特殊的驱动程序,比如Virtualbox或VMware在Guest机器里面安装的驱动;
  • 硬件信息,比如网卡的mac地址,特定品牌虚拟机的网卡mac地址默认情况下大多都在一个特定范围内;
  • 指令执行环境,比如像LDT、GDT的地址范围等等;
针对检测时间

沙箱每一轮分析一个样本的时间是有限的,通常就3分钟(默认),所以如果能让样本在这几分钟内不进入任何核心逻辑那么就可以避免被检测到。通常采取的做法有:

  • 利用Sleep函数,或和Sleep同等功能的延时函数;
  • 检测系统时间,通过系统的时间来延时;
  • 运行一些非常耗时的操作,比如一些特殊的数学算法以达到消耗时间的目的。
针对交互

这种做法是为了确保样本是在有人操作的计算机中启动的,比如可以检测鼠标的移动、故意弹窗让用户点击等等。

Ember 用了哪些特征?

https://www.jianshu.com/p/cf96a701e899

头信息。从COFF报头中,我们报告报头中的时间戳、目标机器(字符串)和图像特征列表(字符串列表)。从可选的头文件中,我们提供了目标子系统(字符串)、DLL特征(字符串列表)、文件魔法作为字符串(例如“PE32”)、主映像版本和副映像版本、链接器版本、系统版本和子系统版本,以及代码、头文件和提交大小。在训练一个模型之前,使用特征哈希技巧总结模型特征、字符串描述符(如DLL特征、目标机器、子系统等),每个带噪声的指标向量分配10个bin。

沙箱全局结果容器有哪些特征?

《流畅的python》阅读笔记

《流畅的python》是一本适合python进阶的书, 里面介绍的基本都是高级的python用法. 对于初学python的人来说, 基础大概也就够用了, 但往往由于够用让他们忘了深入, 去精通. 我们希望全面了解这个语言的能力边界, 可能一些高级的特性并不能马上掌握使用, 因此这本书是工作之余, 还有余力的人来阅读, 我这边就将其有用, 精妙的进阶内容整理出来.

笔记链接:https://juejin.cn/post/6844903503987474446

《流畅的python》阅读笔记

第0章:基础知识

迭代器:

第一章: python数据模型

常用魔法函数: __init__ , __lt__, __len____str____repr____call__ __iter__

这部分主要介绍了python的魔术方法, 它们经常是两个下划线包围来命名的(比如 __init__ , __lt__, __len__ ). 这些特殊方法是为了被python解释器调用的, 这些方法会注册到他们的类型中方法集合中, 相当于为cpython提供抄近路. 这些方法的速度也比普通方法要快, 当然在自己不清楚这些魔术方法的用途时, 不要随意添加.

关于字符串的表现形式是两种, __str____repr__ . python的内置函数 repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式. 这个在交互模式下比较常用, 如果没有实现 __repr__ , 当控制台打印一个对象时往往是 <A object at 0x000> . 而 __str__ 则是 str() 函数时使用的, 或是在 print 函数打印一个对象的时候才被调用, 终端用户友好.

两者还有一个区别, 在字符串格式化时, "%s" 对应了 __str__ . 而 "%r" 对应了 __repr__. __str____repr__ 在使用上比较推荐的是,前者是给终端用户看,而后者则更方便我们调试和记录日志.

  • 字符串表示:__str____repr__
  • 集合、序列相关:__len____getitem____setitem____delitem____contains__
  • 迭代相关:__iter____next__
  • 可调用:__call__
  • with上下文管理器:__enter____exit__
  • 属性相关:__getattr____setattr____getattribute__

img

第二章: 序列构成的数组

这部分主要是介绍序列, 着重介绍数组元组的一些高级用法.

序列按照容纳数据的类型可以分为:

  • 容器序列: list、tuple 和 collections.deque 这些序列能存放不同类型的数据
  • 扁平序列: str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型

如果按照是否能被修改可以分为:

  • 可变序列: list、bytearray、array.array、collections.deque 和 memoryview
  • 不可变序列: tuple、str 和 bytes

列表推导

列表推导是构建列表的快捷方式, 可读性更好且效率更高.

例如, 把一个字符串变成unicode的码位列表的例子, 一般:

1
2
3
4
symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
codes.append(ord(symbol))复制代码

使用列表推导:

1
2
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]复制代码

能用列表推导来创建一个列表, 尽量使用推导, 并且保持它简短.

笛卡尔积与生成器表达式

生成器表达式是能逐个产出元素, 节省内存. 例如:

1
2
3
4
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):
... print(tshirt)复制代码

实例中列表元素比较少, 如果换成两个各有1000个元素的列表, 显然这样组合的笛卡尔积是一个含有100万元素的列表, 内存将会占用很大, 而是用生成器表达式就可以帮忙省掉for循环的开销.

具名元组

元组经常被作为 不可变列表 的代表. 经常只要数字索引获取元素, 但其实它还可以给元素命名:

1
2
3
4
5
6
7
8
9
10
11
12
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'

切片

列表中是以0作为第一个元素的下标, 切片可以根据下标提取某一个片段.

s[a:b:c] 的形式对 sab 之间以 c 为间隔取值。c 的值还可以为负, 负值意味着反向取值.

1
2
3
4
5
6
7
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

第三章: 字典和集合

dict 类型不但在各种程序里广泛使用, 它也是 Python 语言的基石. 正是因为 dict 类型的重要, Python 对其的实现做了高度的优化, 其中最重要的原因就是背后的「散列表」 set(集合)和dict一样, 其实现基础也是依赖于散列表.

散列表也叫哈希表, 对于dict类型, 它的key必须是可哈希的数据类型. 什么是可哈希的数据类型呢, 它的官方解释是:

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变

的,而且这个对象需要实现 __hash__() 方法。另外可散列对象还要有 __qe__() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……

str, bytes, frozenset数值 都是可散列类型.

字典推导式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DIAL_CODE = [
(86, 'China'),
(91, 'India'),
(7, 'Russia'),
(81, 'Japan'),
]

### 利用字典推导快速生成字典
country_code = {country: code for code, country in DIAL_CODE}
print(country_code)

'''
OUT:
{'China': 86, 'India': 91, 'Russia': 7, 'Japan': 81}
'''复制代码

defaultdict:处理找不到的键的一个选择

当某个键不在映射里, 我们也希望也能得到一个默认值. 这就是 defaultdict , 它是 dict 的子类, 并实现了 __missing__ 方法.

1
2
3
4
5
import collections
index = collections.defaultdict(list)
for item in nums:
key = item % 2
index[key].append(item)

字典的变种

标准库里 collections 模块中,除了 defaultdict 之外的不同映射类型:

  • OrderDict: 这个类型在添加键的时候,会保存顺序,因此键的迭代顺序总是一致的
  • ChainMap: 该类型可以容纳数个不同的映射对像,在进行键的查找时,这些对象会被当做一个整体逐个查找,直到键被找到为止 pylookup = ChainMap(locals(), globals())
  • Counter: 这个映射类型会给键准备一个整数技术器,每次更行一个键的时候都会增加这个计数器,所以这个类型可以用来给散列表对象计数,或者当成多重集来用.
1
2
3
4
5
6
import collections
ct = collections.Counter('abracadabra')
print(ct) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
ct.update('aaaaazzz')
print(ct) # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(ct.most_common(2)) # [('a', 10), ('z', 3)]复制代码
  • UserDict: 这个类其实就是把标准 dict 用纯 Python 又实现了一遍
1
2
3
4
5
6
7
8
9
10
11
12
import collections
class StrKeyDict(collections.UserDict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]

def __contains__(self, key):
return str(key) in self.data

def __setitem__(self, key, item):
self.data[str(key)] = item

不可变映射类型【MappingProxyType】

说到不可变, 第一想到的肯定是元组, 但是对于字典来说, 要将key和value的对应关系变成不可变, types 模块的 MappingProxyType 可以做到:

1
2
3
4
5
6
7
from types import MappingProxyType
d = {1:'A'}
d_proxy = MappingProxyType(d)
d_proxy[1]='B' # TypeError: 'mappingproxy' object does not support item assignment

d[2] = 'B'
print(d_proxy) # mappingproxy({1: 'A', 2: 'B'})复制代码

d_proxy 是动态的, 也就是说对 d 所做的任何改动都会反馈到它上面.

集合论:本身不可散列,元素必须可散列

集合的本质是许多唯一对象的聚集. 因此, 集合可以用于去重. 集合中的元素必须是可散列的, 但是 set 本身是不可散列的, 而 frozenset 本身可以散列.

集合具有唯一性, 与此同时, 集合还实现了很多基础的中缀运算符. 给定两个集合 a 和 b, a | b 返 回的是它们的合集, a & b 得到的是交集, 而 a - b 得到的是差集.

合理的利用这些特性, 不仅能减少代码的数量, 更能增加运行效率.

1
2
3
4
5
6
7
8
# 集合的创建
s = set([1, 2, 2, 3])
# 空集合
s = set()
# 集合字面量
s = {1, 2}
# 集合推导
s = {chr(i) for i in range(23, 45)}

第四章: 文本和字节序列

本章讨论了文本字符串字节序列, 以及一些编码上的转换. 本章讨论的 str 指的是python3下的.

字符问题

字符串是个比较简单的概念: 一个字符串是一个字符序列. 但是关于 "字符" 的定义却五花八门, 其中, "字符" 的最佳定义是 Unicode 字符 . 因此, python3中的 str 对象中获得的元素就是 unicode 字符.

把码位转换成字节序列的过程就是 编码, 把字节序列转换成码位的过程就是 编码 :

1
2
3
4
5
6
7
8
9
>>> s = 'café'
>>> len(s)
4
>>> b = s.encode('utf8')
>>> b
b'caf\xc3\xa9'
>>> len(b)
5
>>> b.decode('utf8') #'café复制代码

码位可以认为是人类可读的文本, 而字符序列则可以认为是对机器更友好. 所以要区分 .decode().encode() 也很简单. 从字节序列到人类能理解的文本就是解码(decode). 而把人类能理解的变成人类不好理解的字节序列就是编码(encode).

字节概要

python3有两种字节序列, 不可变的 bytes 类型和可变的 bytearray 类型. 字节序列中的各个元素都是介于 [0, 255] 之间的整数.

处理编码问题

python自带了超过100中编解码器. 每个编解码器都有一个名称, 甚至有的会有一些别名, 如 utf_8 就有 utf8, utf-8, U8 这些别名.

如果字符序列和预期不符, 在进行解码或编码时容易抛出 Unicode*Error 的异常. 造成这种错误是因为目标编码中没有定义某个字符(没有定义某个码位对应的字符), 这里说说解决这类问题的方式.

  • 使用python3, python3可以避免95%的字符问题.
  • 主流编码尝试下: latin1, cp1252, cp437, gb2312, utf-8, utf-16le
  • 留意BOM头部 b'\xff\xfe' , UTF-16编码的序列开头也会有这几个额外字节.
  • 找出序列的编码, 建议使用 codecs 模块

规范化unicode字符串

1
2
s1 = 'café'
s2 = 'caf\u00e9'复制代码

这两行代码完全等价. 而有一种是要避免的是, 在Unicode标准中 ée\u0301 这样的序列叫 "标准等价物". 这种情况用NFC使用最少的码位构成等价的字符串:

1
2
3
4
5
6
7
8
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 s2
False复制代码

改进后:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from unicodedata import normalize
>>> s1 = 'café' # 把"e"和重音符组合在一起
>>> s2 = 'cafe\u0301' # 分解成"e"和重音符
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) normalize('NFC', s2)
True
>>> normalize('NFD', s1) normalize('NFD', s2)
True复制代码

unicode文本排序

对于字符串来说, 比较的码位. 所以在非 ascii 字符时, 得到的结果可能会不尽人意.

第五章: 一等函数

在python中, 函数是一等对象. 编程语言把 "一等对象" 定义为满足下列条件:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

在python中, 整数, 字符串, 列表, 字典都是一等对象.

5.1 把函数视作对象

Python即可以函数式编程,也可以面向对象编程. 这里我们创建了一个函数, 然后读取它的 __doc__ 属性, 并且确定函数对象其实是 function 类的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def factorial(n):
'''
return n
'''
return 1 if n < 2 else n * factorial(n-1)

print(factorial.__doc__)
print(type(factorial))
print(factorial(3))

'''
OUT

return n

<class 'function'>
6
'''复制代码

5.2 高阶函数

高阶函数就是接受函数作为参数, 或者把函数作为返回结果的函数. 如 map, filter , reduce 等.

比如调用 sorted 时, 将 len 作为参数传递:

1
2
3
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
# ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']复制代码

5.3 匿名函数

lambda 关键字是用来创建匿名函数. 匿名函数一些限制, 匿名函数的定义体只能使用纯表达式. 换句话说, lambda 函数内不能赋值, 也不能使用while和try等语句.

1
2
3
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])
# ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']复制代码

5.4 可调用对象

除了用户定义的函数, 调用运算符即 () 还可以应用到其他对象上. 如果像判断对象能否被调用, 可以使用内置的 callable() 函数进行判断. python的数据模型中有7种可是可以被调用的:

  • 用户定义的函数: 使用def语句或lambda表达式创建
  • 内置函数:如len
  • 内置方法:如dict.get
  • 方法:在类定义体中的函数
  • 类的实例: 如果类定义了 __call__ , 那么它的实例可以作为函数调用.
  • 生成器函数: 使用 yield 关键字的函数或方法.

5.5 从定位参数到仅限关键字参数

就是可变参数和关键字参数:

1
2
def fun(name, age, *args, **kwargs):
pass复制代码

其中 *args**kwargs 都是可迭代对象, 展开后映射到单个参数. args是个元组, kwargs是字典.

第六章 Python设计模式

https://github.com/faif/python-patterns

https://refactoringguru.cn/design-patterns/python

设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易地被他人理解、保证代码可靠性。

第七章: 函数装饰器和闭包

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。

修饰器和闭包经常在一起讨论, 因为修饰器就是闭包的一种形式. 闭包还是回调式异步编程函数式编程风格的基础.

装饰器基础知识

装饰器可调用的对象, 其参数是另一个函数(被装饰的函数). 装饰器可能会处理被装饰的函数, 然后把它返回, 或者将其替换成另一个函数或可调用对象.

1
2
3
@decorate
def target():
print('running target()')复制代码

这种写法与下面写法完全等价:

1
2
3
def target():
print('running target()')
target = decorate(target)

装饰器是语法糖, 它其实是将函数作为参数让其他函数处理. 装饰器有两大特征:

  • 把被装饰的函数替换成其他函数
  • 装饰器在加载模块时立即执行

要理解立即执行看下等价的代码就知道了, target = decorate(target) 这句调用了函数. 一般情况下装饰函数都会将某个函数作为返回值.

变量作用域规则

要理解装饰器中变量的作用域, 应该要理解闭包, 我觉得书里将闭包和作用域的顺序换一下比较好. 在python中, 一个变量的查找顺序是 LEGB (L:Local 局部环境,E:Enclosing 闭包,G:Global 全局,B:Built-in 内置).

1
2
3
4
5
6
7
8
9
10
base = 20 # 3
def get_compare():
base = 10 # 2
def real_compare(value):
base = 5 # 1
return value > base
return real_compare

compare_10 = get_compare()
print(compare_10(5))复制代码

在闭包的函数 real_compare 中, 使用的变量 base 其实是 base = 10 的. 因为base这个变量在闭包中就能命中, 而不需要去 global 中获取.

闭包

闭包其实挺好理解的, 当匿名函数出现的时候, 才使得这部分难以掌握. 简单简短的解释闭包就是:

名字空间与函数捆绑后的结果被称为一个闭包(closure).

这个名字空间就是 LEGB 中的 E . 所以闭包不仅仅是将函数作为返回值. 而是将名字空间和函数捆绑后作为返回值的. 多少人忘了理解这个 "捆绑" , 不知道变量最终取的哪和哪啊. 哎.

标准库中的装饰器

python内置了三个用于装饰方法的函数: propertyclassmethodstaticmethod . 这些是用来丰富类的.

1
2
3
4
class A(object):
@property
def age():
return 12

第八章: 对象引用、可变性和垃圾回收

变量不是盒子

很多人把变量理解为盒子, 要存什么数据往盒子里扔就行了.

1
2
3
4
a = [1,2,3]
b = a
a.append(4)
print(b) # [1, 2, 3, 4]复制代码

变量 ab 引用同一个列表, 而不是那个列表的副本. 因此赋值语句应该理解为将变量和值进行引用的关系而已.

标识、相等性和别名

要知道变量a和b是否是同一个值的引用, 可以用 is 来进行判断:

1
2
3
4
5
6
>>> a = b = [4,5,6]
>>> c = [4,5,6]
>>> a is b
True
>>> x is c
False复制代码

如果两个变量都是指向同一个对象, 我们通常会说变量是另一个变量的 别名 .

在 和 is 之间选择 运算符 是用来判断两个对象值是否相等(注意是**对象值**). 而 `is` 则是用于判断两个变量是否指向同一个对象, 或者说判断变量是不是两一个的别名, is 并不关心对象的值. 从使用上, 使用比较多, 而 is 的执行速度比较快.

默认做浅复制

1
2
3
4
5
6
7
8
l1 = [3, [55, 44], (7, 8, 9)]

l2 = list(l1) # 通过构造方法进行复制
l2 = l1[:] #也可以这样想写
>>> l2 l1
True
>>> l2 is l1
False复制代码

尽管 l2 是 l1 的副本, 但是复制的过程是先复制 (即复制了最外层容器副本中的元素是源容器中元素的引用) . 因此在操作 l2[1] 时, l1[1] 也会跟着变化. 而如果列表中的所有元素是不可变的, 那么就没有这样的问题, 而且还能节省内存. 但是, 如果有可变元素存在, 就可能造成意想不到的问题.

python标准库中提供了两个工具 copydeepcopy . 分别用于浅拷贝深拷贝:

1
2
3
4
5
import copy
l1 = [3, [55, 44], (7, 8, 9)]

l2 = copy.copy(l1)
l2 = copy.deepcopy(l1)复制代码

函数的参数做引用时

python中的函数参数都是采用共享传参. 共享传参指函数的各个形式参数获得实参中各个引用的副本. 也就是说, 函数内部的形参是实参的别名.

这种方案就是当传入参数是可变对象时, 在函数内对参数的修改也就是对外部可变对象进行修改. 但这种参数试图重新赋值为一个新的对象时则无效, 因为这只是相当于把参数作为另一个东西的引用, 原有的对象并不变. 也就是说, 在函数内, 参数是不能把一个对象替换成另一个对象的.

不要使用可变类型作为参数的默认值

参数默认值是个很棒的特性. 对于开发者来说, 应该避免使用可变对象作为参数默认值. 因为如果参数默认值是可变对象, 而且修改了它的内容, 那么后续的函数调用上都会收到影响.

del和垃圾回收

在python中, 当一个对象失去了最后一个引用时, 会当做垃圾, 然后被回收掉. 虽然python提供了 del 语句用来删除变量. 但实际上只是删除了变量和对象之间的引用, 并不一定能让对象进行回收, 因为这个对象可能还存在其他引用.

在CPython中, 垃圾回收主要用的是引用计数的算法. 每个对象都会统计有多少引用指向自己. 当引用计数归零时, 意味着这个对象没有在使用, 对象就会被立即销毁.【分代垃圾回收算法】

符合Python风格的对象

得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需的方法即可。

对象表示形式

每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了两种方式。

  • repr() : 以便于开发者理解的方式返回对象的字符串表示形式。
  • str() : 以便于用户理解的方式返回对象的字符串表示形式。

classmethod 与 staticmethod

这两个都是python内置提供了装饰器, 一般python教程都没有提到这两个装饰器. 这两个都是在类 class 定义中使用的, 一般情况下, class 里面定义的函数是与其类的实例进行绑定的. 而这两个装饰器则可以改变这种调用方式.

先来看看 classmethod , 这个装饰器不是操作实例的方法, 并且将类本身作为第一个参数. 而 staticmethod 装饰器也会改变方法的调用方式, 它就是一个普通的函数,

classmethodstaticmethod 的区别就是 classmethod 会把类本身作为第一个参数传入, 其他都一样了.

格式化显示

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的 .__format__(format_spec) 方法. format_spec 是格式说明符,它是:

  • format(my_obj, format_spec) 的第二个参数
  • str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分

Python的私有属性和"受保护的"属性

python中对于实例变量没有像 private 这样的修饰符来创建私有属性, 在python中, 有一个简单的机制来处理私有属性.

1
2
3
4
5
6
7
class A:
def __init__(self):
self.__x = 1

a = A()
print(a.__x) # AttributeError: 'A' object has no attribute '__x'
print(a.__dict__)复制代码

如果属性以 __name两个下划线为前缀, 尾部最多一个下划线 命名的实例属性, python会把它名称前面加一个下划线加类名, 再放入 __dict__ 中, 以 __name 为例, 就会变成 _A__name .

名称改写算是一种安全措施, 但是不能保证万无一失, 它能避免意外访问, 但不能阻止故意做坏事.

只要知道私有属性的机制, 任何人都能直接读取和改写私有属性. 因此很多python程序员严格规定: 遵守使用一个下划线标记对象的私有属性 . Python 解释器不会对使用单个下划线的属性名做特殊处理, 由程序员自行控制, 不在类外部访问这些属性. 这种方法也是所推荐的, 两个下划线的那种方式就不要再用了. 引用python大神的话:

绝对不要使用两个前导下划线,这是很烦人的自私行为。如果担心名称冲突,应该明确使用一种名称改写方式(如 _MyThing_blahblah)。这其实与使用双下划线一样,不过自己定的规则比双下划线易于理解。

Python中的把使用一个下划线前缀标记的属性称为"受保护的"属性

使用 slots 类属性节省空间

默认情况下, python在各个实例中, 用 __dict__ 的字典存储实例属性. 因此实例的属性是动态变化的, 可以在运行期间任意添加属性. 而字典是消耗内存比较大的结构. 因此当对象的属性名称确定时, 使用 __slots__ 可以节约内存.

1
2
3
4
class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
# 下面是各个方法(因排版需要而省略了)复制代码

在类中定义__slots__ 属性的目的是告诉解释器:"这个类中的所有实例属性都在这儿了!" 这样, Python 会在各个实例中使用类似元组的结构存储实例变量, 从而避免使用消耗内存的 __dict__ 属性. 如果有数百万个实例同时活动, 这样做能节省大量内存.

第十五章: 上下文管理器和 else 块

本章讨论的是其他语言不常见的流程控制特性, 正因如此, python新手往往忽视或没有充分使用这些特性. 下面讨论的特性有:

  • with 语句和上下文管理器
  • for while try 语句的 else 子句

with 语句会设置一个临时的上下文, 交给上下文管理器对象控制, 并且负责清理上下文. 这么做能避免错误并减少代码量, 因此API更安全, 而且更易于使用. 除了自动关闭文件之外, with 块还有其他很多用途.

else 子句先做这个,选择性再做那个的作用.

if语句之外的else块

这里的 else 不是在在 if 语句中使用的, 而是在 for while try 语句中使用的.

1
2
3
4
5
for i in lst:
if i > 10:
break
else:
print("no num bigger than 10")

else 子句的行为如下:

  • for : 仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。
  • while : 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止)才运行 else 块。
  • try : 仅当 try 块中没有异常抛出时才运行 else 块。

在所有情况下, 如果异常或者 return , breakcontinue 语句导致控制权跳到了复合语句的住块外, else 子句也会被跳过.

这一些情况下, 使用 else 子句通常让代码更便于阅读, 而且能省去一些麻烦, 不用设置控制标志作用的变量和额外的if判断.

上下文管理器和with块

上下文管理器对象的目的就是管理 with 语句, with 语句的目的是简化 try/finally 模式. 这种模式用于保证一段代码运行完毕后执行某项操作, 即便那段代码由于异常, return 或者 sys.exit() 调用而终止, 也会执行执行的操作. finally 子句中的代码通常用于释放重要的资源, 或者还原临时变更的状态.

上下文管理器协议包含 __enter____exit__ 两个方法. with 语句开始运行时, 会在上下文管理器上调用 __enter__ 方法, 待 with 语句运行结束后, 再调用 __exit__ 方法, 以此扮演了 finally 子句的角色.

with 最常见的例子就是确保关闭文件对象.

上下文管理器调用 __enter__ 没有参数, 而调用 __exit__ 时, 会传入3个参数:

  • exc_type : 异常类(例如 ZeroDivisionError)
  • exc_value : 异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取
  • traceback : traceback 对象

contextlib模块中的实用工具

在ptyhon的标准库中, contextlib 模块中还有一些类和其他函数,使用范围更广。

  • closing: 如果对象提供了 close() 方法,但没有实现 __enter__/__exit__ 协议,那么可以使用这个函数构建上下文管理器。
  • suppress: 构建临时忽略指定异常的上下文管理器。
  • @contextmanager: 这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
  • ContextDecorator: 这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。
  • ExitStack: 这个上下文管理器能进入多个上下文管理器。with 块结束时,ExitStack 按照后进先出的顺序调用栈中各个上下文管理器的 __exit__ 方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。

显然,在这些实用工具中,使用最广泛的是 @contextmanager 装饰器,因此要格外留心。这个装饰器也有迷惑人的一面,因为它与迭代无关,却要使用 yield 语句。

使用@contextmanager

@contextmanager 装饰器能减少创建上下文管理器的样板代码量, 因为不用定义 __enter____exit__ 方法, 只需要实现一个 yield 语句的生成器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sys
import contextlib
@contextlib.contextmanager
def looking_glass():

original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
yield 'JABBERWOCKY'
sys.stdout.write = original_write

with looking_glass() as f:
print(f) # YKCOWREBBAJ
print("ABCD") # DCBA
复制代码

yield 语句起到了分割的作用, yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行, yield 语句后面的代码在 with 块结束时(即调用 __exit__ 方法时)执行.

第十六章: 协程

为了理解协程的概念, 先从 yield 来说. yield item 会产出一个值, 提供给 next(...) 调用方; 此外还会做出让步, 暂停执行生成器, 让调用方继续工作, 直到需要使用另一个值时再调用 next(...) 从暂停的地方继续执行.

从句子语法上看, 协程与生成器类似, 都是通过 yield 关键字的函数. 可是, 在协程中, yield 通常出现在表达式的右边(datum = yield), 可以产出值, 也可以不产出(如果yield后面没有表达式, 那么会产出None). 协程可能会从调用方接收数据, 不过调用方把数据提供给协程使用的是 .send(datum) 方法. 而不是 next(...) . 通常, 调用方会把值推送给协程.

生成器调用方是一直索要数据, 而协程这是调用方可以想它传入数据, 协程也不一定要产出数据.

不管数据如何流动, yield 都是一种流程控制工具, 使用它可以实现写作式多任务: 协程可以把控制器让步给中心调度程序, 从而激活其他的协程.

生成器如何进化成协程

协程的底层框架实现后, 生成器API中增加了 .send(value) 方法. 生成器的调用方可以使用 .send(...) 来发送数据, 发送的数据会变成生成器函数中 yield 表达式的值. 因此, 生成器可以作为协程使用. 除了 .send(...) 方法, 还添加了 .throw(...).close() 方法, 用来让调用方抛出异常终止生成器.

用作协程的生成器的基本行为

优势:

  • 一个线程中运行,非抢占式的
  • 无需使用实例属性或者闭包, 在多次调用之间都能保持上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def simple_coroutine():
... print('-> coroutine started')
... x = yield
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
...
StopIteration

yield 表达式中, 如果协程只需从调用那接受数据, 那么产出的值是 None . 与创建生成器的方式一样, 调用函数得到生成器对象. 协程都要先调用 next(...) 函数, 因为生成器还没启动, 没在 yield 出暂定, 所以一开始无法发送数据. 如果控制器流动到协程定义体末尾, 会像迭代器一样抛出 StopIteration 异常.

使用协程的好处是不用加锁, 因为所有协程只在一个线程中运行, 他们是非抢占式的. 协程也有一些状态, 可以调用 inspect.getgeneratorstate(...) 来获得, 协程都是这4个状态中的一种:

  • 'GEN_CREATED' 等待开始执行。
  • 'GEN_RUNNING' 解释器正在执行。
  • 'GEN_SUSPENDED' 在 yield 表达式处暂停。
  • 'GEN_CLOSED' 执行结束。

只有在多线程应用中才能看到这个状态。此外,生成器对象在自己身上调用 getgeneratorstate 函数也行,不过这样做没什么用。

为了更好理解继承的行为, 来看看产生两个值的协程:

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
>>> from inspect import getgeneratorstate
>>> def simple_coro2(a):
... print('-> Started: a =', a)
... b = yield a
... print('-> Received: b =', b)
... c = yield a + b
... print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> getgeneratorstate(my_coro2) # 协程处于未启动的状态
'GEN_CREATED'
>>> next(my_coro2) # 向前执行到yield表达式, 产出值 a, 暂停并等待 b 赋值
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2) # 协程处于暂停状态
'GEN_SUSPENDED'
>>> my_coro2.send(28) # 数字28发给协程, yield 表达式中 b 得到28, 协程向前执行, 产出 a + b 值
-> Received: b = 28
42
>>> my_coro2.send(99) # 同理, c 得到 99, 但是由于协程终止, 导致生成器对象抛出 StopIteration 异常
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2) # 协程处于终止状态
'GEN_CLOSED'复制代码

关键的一点是, 协程在 yield 关键字所在的位置暂停执行. 对于 b = yield a 这行代码来说, 等到客户端代码再激活协程时才会设定 b 的值. 这种方式要花点时间才能习惯, 理解了这个, 才能弄懂异步编程中 yield 的作用.

示例:使用协程计算移动平均值

1
2
3
4
5
6
7
8
9
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count复制代码

这是一个动态计算平均值的协程代码, 这个无限循环表明, 它会一直接收值然后生成结果. 只有当调用方在协程上调用 .close() 方法, 或者没有该协程的引用时, 协程才会终止.

协程的好处是, 无需使用实例属性或者闭包, 在多次调用之间都能保持上下文.

使用 yield from

yield from 是全新的语法结构. 它的作用比 yield 多很多.

1
2
3
4
5
6
7
8
>>> def gen():
... for c in 'AB':
... yield c
... for i in range(1, 3):
... yield i
...
>>> list(gen())
['A', 'B', 1, 2]复制代码

可以改写成:

1
2
3
4
5
6
>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]复制代码

在生成器 gen 中使用 yield form subgen() 时, subgen 会得到控制权, 把产出的值传给 gen 的调用方, 既调用方可以直接调用 subgen. 而此时, gen 会阻塞, 等待 subgen 终止.

yield from x 表达式对 x 对象所做的第一件事是, 调用 iter(x) 获得迭代器. 因此, x 对象可以是任何可迭代对象.

这个语义过于复杂, 来看看作者 Greg Ewing 的解释:

“把迭代器当作生成器使用,相当于把子生成器的定义体内联在 yield from 表达式

中。此外,子生成器可以执行 return 语句,返回一个值,而返回的值会成为 yieldfrom 表达式的值。”

子生成器是从 yield from <iterable> 中获得的生成器. 而后, 如果调用方使用 send() 方法, 其实也是直接传给子生成器. 如果发送的是 None , 那么会调用子生成器的 __next__() 方法. 如果不是 None , 那么会调用子生成器的 send() 方法. 当子生成器抛出 StopIteration 异常, 那么委派生成器恢复运行. 任何其他异常都会向上冒泡, 传给委派生成器.

生成器在 return expr 表达式中会触发 StopIteration 异常.

第十七章: 使用 futrues包 处理并发

"futures" 是什么概念呢? 期物指一种对象, 表示异步执行的操作. 这个概念是 concurrent.futures 模块和 asyncio 包的基础.

阻塞型I/O和GIL

CPython解释器不是线程安全的, 因此有全局解释锁(GIL), 一次只允许使用一个线程执行 python 字节码, 所以一个python进程不能同时使用多个 CPU 核心.

python程序员编写代码时无法控制 GIL, 然而, 在标准库中所有执行阻塞型I/O操作的函数, 在登台操作系统返回结果时都会释放GIL. 这意味着IO密集型python程序能从中受益.

使用concurrent.futures模块启动进程

一个python进程只有一个 GIL. 多个python进程就能绕开GIL, 因此这种方法就能利用所有的 CPU 核心. concurrent.futures 模块就实现了真正的并行计算, 因为它使用 ProcessPoolExecutor 把工作交个多个python进程处理.

ProcessPoolExecutorThreadPoolExecutor 类都实现了通用的 Executor 接口, 因此使用 concurrent.futures 能很轻松把基于线程的方案转成基于进程的方案.

1
2
3
4
def download_many(cc_list):
workers = min(MAX_WORKERS, len(cc_list))
with futures.ThreadPoolExecutor(workers) as executor:
res = executor.map(download_one, sorted(cc_list))复制代码

改成:

1
2
3
def download_many(cc_list):
with futures.ProcessPoolExecutor() as executor:
res = executor.map(download_one, sorted(cc_list))复制代码

ThreadPoolExecutor.__init__ 方法需要 max_workers 参数,指定线程池中线程的数量; 在 ProcessPoolExecutor 类中, 这个参数是可选的.

第十八章: 使用 asyncio 包处理并发

并发是指一次处理多件事。

并行是指一次做多件事。 二者不同,但是有联系。 一个关于结构,一个关于执行。 并发用于制定方案,用来解决可能(但未必)并行的问题。—— Rob Pike Go 语言的创造者之一

AI-for-Security-Learning

##### 404notf0und/AI-for-Security-Learning

杨秀章

一 、恶意加密流量数据

工具:

cicflowmeter,一款流量特征提取工具,该工具输入pcap文件,输出pcap文件中包含的数据包的特征信息(80多维)

1.1 机器学习检测Cobalt Strike木马初探

https://www.freebuf.com/articles/network/279190.html

通过机器学习分析cobalt strike的通信包,找出通信规律,然后用这个规律对新的通信包进行检测。

数据集

cobalt strike恶意通信的心跳包,无需抓指令包

TCP特征:每秒传输的数据包字节数、流包率,即每秒传输的数据包数、每秒前向包的数量、正向数据包的总大小、数据包在正向的平均大小、数据包正向标准偏差大小、流的最大长度、最小包到达间隔时间等等。

Microsoft Network Monitor 3.4 https://www.microsoft.com/en-us/download/details.aspx?id=4865

二、恶意软件

==阿里云安全==:win32程序在沙箱中运行的API序列信息,训练集11w条程序记录,测试集5w条程序记录。6类程序中正常程序特别多,病毒程序偏少。

如图所示赛题数据按照文件file_id进行组织:每个文件file_id对应一个文件标签,即文件的病毒标签;每个文件file_id可能由一个或者多个线程tid组成。每个线程tid由一个api或者多个api组成,每个api对应一个返回值,api的次序关系由index表示。

img

  • 统计特征:统计API的出现次数、类型统计以及API返回值的统计特征;
  • 图模型特征:将API序列转换成API图的边,统计有向图的相关的特征;
  • 时序特征:API序列的出现次数等时序特征,API返回值的时序特征;

==LSTM + API==

PDF malware

  • Malware Analysis – Dissecting PDF file:https://github.com/filipi86/MalwareAnalysis-in-PDF

三 、DDOS

在攻击感知方面,可从宏观攻击流感知与微观检测方法两个角度,分别基于IP流序列谱分析的泛洪攻击低速率拒绝服务(Low-rate Denial of Service,LDoS)方法进行感知。在此基础上,将DDoS攻击检测转化为机器学习的二分类问题。

基于多特征并行隐马尔科夫模型(Multi-FeatureParallel Hidden Markov Model,MFP-HMM)的DDoS攻击检测方法,利用HMM隐状态序列与特征观测序列的对应关系,将攻击引起的多维特征异常变化转化为离散型随机变量,通过概率计算来刻画当前滑动窗口序列与正常行为轮廓的偏离程度。

四、WEB安全

Web安全是指个人用户在Web相关操作时不因偶然或恶意的原因受到破坏、更改、泄露。除了现有的SQL注入检测、XSS攻击检测等 AI应用,本部分将列举“恶意URL检测”与“ Webshell检测”两例。后续实验部分,作者将详细描述Python实现该过程。

恶意URL检测

基于机器学习,从 URL特征、域名特征、Web特征的关联分析,使恶意URL识别具有高准确率

开源工具如Phinn:Phinn使用了机器学习领域中的卷积神经网络算法来生成和训练一个自定义的Chrome扩展,这个 Chrome扩展可以将用户浏览器中呈现的页面与真正的登录页面进行视觉相似度分析,以此来识别出恶意URL(钓鱼网站)。

Webshell检测

Webshell常常被称为匿名用户(入侵者)通过网站端口对网站服务器的某种程度上操作的权限。由于Webshell其大多是以动态脚本的形式出现,也有人称之为网站的后门工具。在攻击链模型中,整个攻击过程分为:踩点、组装、投送、攻击、植入、控制、行动。在针对网站的攻击中,通常是利用上传漏洞,上传Webshell,然后通过Webshell进一步控制web服务器。

通过词袋&TF-IDF模型、Opcode&N-gram模型、Opcode调用序列模型等特征抽取方式,采用合适的模型,如朴素贝叶斯和深度学习的MLP、CNN等,实现Webshell的检测。类似地,也可进行SQL注入、 XSS攻击检测等。

五、入侵检测与防御

入侵检测与防御是指对入侵行为的发现并采取相应的防御行动。除了现有的内网入侵检测等AI应用,本部分将列举“APT检测与防范”与“C2链接分析”两例。

5.1 APT检测与防范

进行APT攻击的攻击者从侦查目标,制作攻击工具,传递攻击工具,利用漏洞或者弱点来进行突防,拿下全线运行工具,后期远端的维护这个工具,到最后达到了长期控制目标的目的。针对这种现在日益广泛的APT 攻击,威胁情报存在于整个攻击的各个环节。

威胁情报是基于证据的描述威胁的一组关联的信息,包括威胁相关的环境信息,如具体的攻击组织恶意域名。恶意域名又包括远控的IOC恶意文件的HASHURL以及威胁指标之间的关联性,时间纬度上攻击手法的变化。这些信息汇总在一起形成高级威胁情报。除此之外,所关注的情报,还包括传统威胁种类的扩充,包括木马远控,僵尸网络,间谍软件, Web后门等。利用机器学习来处理威胁情报,检测并识别出APT攻击中的恶意载荷,提高APT攻击威胁感知系统的效率与精确性,让安全研究人员能更快实现 APT攻击的发现和溯源。

==5.1 DGA域名检测——C2链接分析== DGA(域名生成算法)是一种利用随机字符来生成C2域名,从而逃避域名黑名单检测的技术手段。而有了DGA域名生成算法,攻击者就可以利用它来生成用作域名的伪随机字符串,这样就可以有效的避开黑名单列表的检测。伪随机意味着字符串序列似乎是随机的,但由于其结构可以预先确定,因此可以重复产生和复制。该算法常被运用于远程控制软件上。

安全 + AI的问题和难点

Fuzzing漏洞挖掘

杨秀章

一、深度学习有哪些应用

  • 图像:图像识别、物体识别、图片美化、图片修复、目标检测。
  • 自然语言处理:机器创作、个性化推荐、文本分类、翻译、自动纠错、情感分析。
  • 数值预测、量化交易

二、什么是神经网络

我们以房价预测的案例来说明一下,把房屋的面积作为神经网络的输入(我们称之为𝑥),通过一个节点(一个小圆圈),最终输出了价格(我们用𝑦表示)。其实这个小圆圈就是一个单独的神经元,就像人的大脑神经元一样。如果这是一个单神经元网络,不管规模大小,它正是通过把这些单个神经元叠加在一起来形成。如果你把这些神经元想象成单独的乐高积木,你就通过搭积木来完成一个更大的神经网络。

神经网络与大脑关联不大。这是一个过度简化的对比,把一个神经网络的逻辑单元和右边的生物神经元对比。至今为止其实连神经科学家们都很难解释,究竟一个神经元能做什么。

2.1 什么是感知器

这要从逻辑回归讲起,我们都知道逻辑回归的目标函数如下所示:

\[ \begin{aligned} & z=\theta_0+\theta_1 X_1+\theta_2 X_2 \\ & a=g(z)=\frac{1}{1+e^{-z}}\end{aligned} \]

我们用网络来表示,这个网络就叫做感知器:

如果在这个感知器的基础上加上隐藏层,就会得到下面我们要说的神经网络结构了。

2.2 神经网络的结构

神经网络的一般结构是由输入层、隐藏层(神经元)、输出层构成的。隐藏层可以是1层或者多层叠加,层与层之间是相互连接的,如下图所示。神经网络具有非线性切分能力

img

一般说到神经网络的层数是这样计算的,输入层不算,从隐藏层开始一直到输出层,一共有几层就代表着这是一个几层的神经网络,例如上图就是一个三层结构的神经网络。

解释隐藏层的含义:在一个神经网络中,当你使用监督学习训练它的时候,训练集包含了输入𝑥也包含了目标输出𝑦,所以术语隐藏层的含义是在训练集中,这些中间结点的准确值我们是不知道到的,也就是说你看不见它们在训练集中应具有的值。

  • 多隐藏层的神经网络比单隐藏层的神经网络工程效果好很多。
  • 提升隐层层数或者隐层神经元个数,神经网络“容量”会变大,空间表达力会变强。
  • 过多的隐层和神经元节点,会带来过拟合问题。
  • 不要试图通过降低神经网络参数量来减缓过拟合,用正则化或者dropout

参考文献

  • https://github.com/NLP-LOVE/ML-NLP/tree/master/Deep%20Learning/10.%20Neural%20Network

推荐算法

百度飞浆,推荐系统:https://paddlepedia.readthedocs.io/en/latest/tutorials/recommendation_system/recommender_system.html#id2

为什么LR可以用来做CTR预估? - AI牛的回答 - 知乎 https://www.zhihu.com/question/23652394/answer/2352984912

推荐系统中常用的embedding方法 - 十三的文章 - 知乎 https://zhuanlan.zhihu.com/p/476673607

一、背景介绍

1.1 推荐系统的产生

在网络技术不断发展和电子商务规模不断扩大的背景下,商品数量和种类快速增长,用户需要花费大量时间才能找到自己想买的商品,这就是信息超载问题。为了解决这个难题,个性化推荐系统(Recommender System)应运而生。

个性化推荐系统是信息过滤系统(Information Filtering System)的子集,它可以用在很多领域,如电影、音乐、电商和 Feed 流推荐等。个性化推荐系统通过分析、挖掘用户行为,发现用户的个性化需求与兴趣特点,将用户可能感兴趣的信息或商品推荐给用户。与搜索引擎不同,个性化推荐系统不需要用户准确地描述出自己的需求,而是根据用户的历史行为进行建模,主动提供满足用户兴趣和需求的信息。

1994年明尼苏达大学推出的GroupLens系统一般被认为是个性化推荐系统成为一个相对独立的研究方向的标志。该系统首次提出了基于协同过滤来完成推荐任务的思想,此后,基于该模型的协同过滤推荐引领了个性化推荐系统十几年的发展方向。

1.2 推荐系统的方法

传统的个性化推荐系统方法主要有:

  • 协同过滤推荐(Collaborative Filtering Recommendation):该方法是应用最广泛的技术之一,需要收集和分析用户的历史行为、活动和偏好。它通常可以分为两个子类:基于用户 (User-Based)的推荐和基于物品(Item-Based)的推荐。该方法的一个关键优势是它不依赖于机器去分析物品的内容特征,因此它无需理解物品本身也能够准确地推荐诸如电影之类的复杂物品;缺点是对于没有任何行为的新用户存在冷启动的问题,同时也存在用户与商品之间的交互数据不够多造成的稀疏问题。值得一提的是,社交网络或地理位置等上下文信息都可以结合到协同过滤中去。
  • 基于内容过滤推荐(Content-based Filtering Recommendation):该方法利用商品的内容描述,抽象出有意义的特征,通过计算用户的兴趣和商品描述之间的相似度,来给用户做推荐。优点是简单直接,不需要依据其他用户对商品的评价,而是通过商品属性进行商品相似度度量,从而推荐给用户所感兴趣商品的相似商品;缺点是对于没有任何行为的新用户同样存在冷启动的问题。
  • 组合推荐(Hybrid Recommendation):运用不同的输入和技术共同进行推荐,以弥补各自推荐技术的缺点。 近些年来,深度学习在很多领域都取得了巨大的成功。学术界和工业界都在尝试将深度学习应用于个性化推荐系统领域中。深度学习具有优秀的自动提取特征的能力,能够学习多层次的抽象特征表示,并对异质或跨域的内容信息进行学习,可以一定程度上处理个性化推荐系统冷启动问题

一、FM 算法

一文读懂FM模型:https://zhuanlan.zhihu.com/p/109980037

FM算法简单梳理🍈: https://zhuanlan.zhihu.com/p/73798236

准确得预估ctr,对于提高流量得价值,增加广告收入有重要作用。业界常用得方法:人工特征+LR,gbdt,LR,FM,FFM。这些模型中FM、FFM表现突出,今天我们就来看看学习FM,下一篇我们在学习FFM。

FM(factor Machine,因子分解机)算法是一种基于矩阵分解的机器学习算法,是为了解决大规模稀疏矩阵中特征组合问题。

作用

  • 特征组合是许多机器学习建模过程中遇到的问题,如果直接建模,可能忽略特征与特征之间的关联信息,因此,可通通过构建新的交叉特征 这一特征组合方式提高模型效果。其实就是增加特征交叉项

    在一般的线性模型中,是各个特征独立思考的,没有考虑到特征之间的相互关系。但是实际上,大量特征之间是关联。 一般女性用户看化妆品服装之类的广告比较多,而男性更青睐各种球类装备。那很明显,女性这个特征与化妆品类服装类商品有很大的关联性,男性这个特征与球类装备的关联性更为密切。如果我们能将这些有关联的特征找出来,显然是很有意义的。

  • 高维的稀疏矩阵是实际工程中常见的问题,并直接会导致计算量过大,特征权重更新缓慢

    而FM的优势,就是在于这两方面问题的处理。首先是特征组合,通过两两特征组合,引入交叉特征,提高模型得分。其次是高维灾难,通过引入隐向量,对特征参数进行估计。

    总结FM的优点:可以在非常稀疏的数据中进行合理的参数估计;FM模型的时间复杂度是线性的;FM是一个通用模型,它可以用于任何特征为实值的情况;同时解决了特征组合问题。

优势

  • 可以在非常稀疏的数据中,进行合理的参数估计
  • FM模型的复杂度是线性的,优化效果好,不需要像svm一样依赖于支持向量
  • FM是一个通用的模型,他咳哟用于任何特征为实值得情况。而其他得因式分解模型只能用于一些输入数据比较固定得情况。

FM算法简单梳理🍈

1.1 FM 特征组合

实对称矩阵分解求解: \(F \mathrm{FM}\) 为每个特征 \(\mathrm{i}\) 引入隐向量 \(v_i\), 用两个特征隐向量的内积表示这两个特征的权重, 即 组合特征 \(x_i . x_j\) 的权重为 \(\left\langle v_i, v_j\right\rangle\)

在传统的线性模型中, 各个特征之间都是独立考虑的,并没有涉及到特征与特征之间的交互关系,但实际上大量的 特征之间是相互关联的。如何寻找相互关联的特征, 基于上述思想 \(F M\) 算法应运而生。传统的线性模型为: \[ y=w_0+\sum_{i=1}^n w_i x_i \] 在传统的线性模型的基础上中引入特征交叉项可得 \[ y=w_0+\sum_{i=1}^n w_i x_i+\sum_{i=1}^{n-1} \sum_{j=i+1}^n w_{i j} x_i x_j \] 在数据非常稀疏的情况下很难满足 \(x_i 、 x_j\) 都不为 0 , 这样将会导致 \(w_{i j}\) 不能够通过训练得到, 因此无法进行相 应的参数估计。可以发现参数矩阵 \(w\) 是一个实对称矩阵, \(w_{i j}\) 可以使用矩阵分解的方法求解,通过引入辅助向量 \(V\)\[ V=\left[\begin{array}{ccccc} v_{11} & v_{12} & v_{13} & \cdots & v_{1 k} \\ v_{21} & v_{22} & v_{23} & \cdots & v_{2 k} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ v_{n 1} & v_{n 2} & v_{n 3} & \cdots & v_{n k} \end{array}\right]=\left[\begin{array}{c} \mathbf{v}_1 \\ \mathbf{v}_2 \\ \vdots \\ \mathbf{v}_n \end{array}\right] \] 然后用 \(w_{i j}=\mathbf{v}_i \mathbf{v}_j^T\)\(w\) 进行分解 \[ w=V V^T=\left[\begin{array}{c} \mathbf{v}_1 \\ \mathbf{v}_2 \\ \vdots \\ \mathbf{v}_n \end{array}\right]\left[\begin{array}{llll} \mathbf{v}_1^T & \mathbf{v}_2^T & \ldots & \mathbf{v}_n^T \end{array}\right] \] 综上可以发现原始模型的二项式参数为 \(\frac{n(n-1)}{2}\) 个, 现在减少为 \(k n(k \ll n)\) 个。引入辅助向量 \(V\) 最为重要的 一点是使得 \(x_t x_i\)\(x_i x_j\) 的参数不再相互独立, 这样就能够在样本数据稀疏的情况下合理的估计模型交叉项的参 数 \[ \begin{aligned} \left\langle\mathbf{v}_t, \mathbf{v}_i\right\rangle & =\sum_{f=1}^k \mathbf{v}_{t f} \cdot \mathbf{v}_{i f} \\ \left\langle\mathbf{v}_i, \mathbf{v}_j\right\rangle & =\sum_{f=1}^k \mathbf{v}_{i f} \cdot \mathbf{v}_{j f} \end{aligned} \] \(x_t x_i\)\(x_i x_j\) 的参数分别为 \(\left\langle\mathbf{v}_t, \mathbf{v}_i\right\rangle\)\(\left\langle\mathbf{v}_i, \mathbf{v}_j\right\rangle\), 它们之间拥有共同项 \(\mathbf{v}_i\), 即所有包含 \(\mathbf{v}_i\) 的非零组合特征的 样本都可以用来学习隐向量 \(\mathbf{v}_i\), 而原始模型中 \(w_{t i}\)\(w_{i j}\) 却是相互独立的, 这在很大程度上避免了数据稀疏造 成的参数估计不准确的影响。因此原始模型可以改写为最终的FM算法。 \[ y=w_0+\sum_{i=1}^n w_i x_i+\sum_{i=1}^{n-1} \sum_{j=i+1}^n\left\langle\mathbf{v}_i, \mathbf{v}_j\right\rangle x_i x_j \]

由于求解上述式子的时间复杂度为 [公式] ,可以看出主要是最后一项计算比较复杂,因此从数学上对该式最后一项进行一些改写可以把时间复杂度降为 [公式]

[公式]

第一步:对称矩阵中上三角的内积之和等于 整个向量的乘积和减去对角线上元素的和

至此, FM算法的公式推导结束了, 为了更直观的了解怎么计算的, 举下面一个例子: 例如 \[ V=\left[\begin{array}{l} V 1 \\ V 2 \\ V 3 \end{array}\right]=\left[\begin{array}{lll} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{array}\right] \Rightarrow W=V * V^T \Rightarrow W=\left[\begin{array}{ccc} 14 & 32 & 50 \\ 32 & 77 & 122 \\ 50 & 122 & 194 \end{array}\right] \Rightarrow W=V * V^T \] 可推出下面公式: \[ \begin{gathered} \sum_i^3 \sum_j^3 w_{i j} * x_i x_j= \\ 14 * x_1 x_1+32 * x_1 x_2+50 * x_1 * x_3 \\ 32 * x_2 x_1+77 * x_2 x_2+122 * x_2 * x_3 \\ 50 * x_3 x_1+122 * x_3 x_2+194 * x_3 * x_3 \end{gathered} \] 而FM公式的第二个因子就是以上展开式"上三角“的元素。

1.2 参数更新

采用随机梯度下降法SGD求解参数: \[ \begin{gathered} \frac{\partial y}{\partial w_0}=1 \\ \frac{\partial y}{\partial w_i}=x_i \\ \frac{\partial y}{\partial v_i f}=x_i \sum_{j=1}^n v_{j, f} x_j-v_{i, f} x_i^2 \end{gathered} \] 由上式可知: 参数 \(v_{i, f}\) 只需要样本的 \(x_i\) 特征非零即可, 因此FM算法适用于稀疏场景, 并且FM算法的训练和预测 都可以在线性时间内完成, FM是一个非常高效的算法。

1.3 FM算法小结

  • FM算法提升了参数学习效率和特征交叉后模型预估的能力。
  • FM算法降低了因数据稀疏,导致特征交叉项参数学习不充分的影响;
  • 模型的组合特征的参数在nk级别,通过公式推导,模型复杂度为\(O(nk)\) ,因此模型可以非常高效的进行训练和预测。

机器学习理论

一、 机器学习中参数模型和非参数模型理解

参数模型通常假设总体服从某个分布,这个分布可以由一些参数确定,如正态分布由均值和标准差确定,在此基础上构建的模型称为参数模型;非参数模型对于总体的分布不做任何假设或者说是数据分布假设自由,只知道其分布是存在的,所以就无法得到其分布的相关参数,只能通过非参数统计的方法进行推断。

参数模型:线性回归、逻辑回归、感知机、基本型的SVM

非参数模型:决策树、对偶型的SVM、朴素贝叶斯、神经网络

二、 判别模型 VS 生成模型

判别模型与生成模型,概率模型与非概率模型、参数模型与非参数模型总结 - Eureka的文章 - 知乎 https://zhuanlan.zhihu.com/p/37821985

机器学习中的判别式模型和生成式模型 - Microstrong的文章 - 知乎 https://zhuanlan.zhihu.com/p/74586507

image-20230417171758407

img

判别模型:感知机、逻辑斯特回归、支持向量机、神经网络、k近邻都属于判别学习模型。

判别模型分为两种:

  • 直接对输入空间到输出空间的映射进行建模, 也就是学习函数 \(h\) :

\[ h: X \rightarrow Y, s . t . y=h(x) \]

  • 对条件概率 \(P(y \mid x)\) 进行建模, 然后根据贝叶斯风险最小化的准则进行分类: 【

\[ y=\arg \max _{y \in\{-1,1\}} P(y \mid x) \]

生成模型:

生成模型是间接地, 先对 \(P(x, y)\) 进行建模, 再根据贝叶斯公式: \[ P(y \mid x)=\frac{P(x \mid y) P(y)}{P(x)} \] 算出 \(P(y \mid x)\), 最后根据 \(\arg \max _{y \in\{-1,1\}} P(y \mid x)\) 来做分类 (由此可知, 判别模型实际上不需要对 \(P(x, y)\) 进行建模)。

三、 非概率模型 VS 概率模型

两者的本质区别在于是否涉及到概率分布。

概率模型

线性回归(高斯分布)、LR(伯努利分布)、高斯判别分析、朴素贝叶斯

概率模型指出了学习的目的是学出 \(P(x, y)\)\(P(y \mid x)\), 但最后都是根据 \(\arg \max _{y \in\{-1,1\}} P(y \mid x)\) 来做判别归类。对于 \(P(x, y)\) 的估计, 一般是根据乘法公式 \(P(x, y)=P(x \mid y) P(y)\) 将其拆解成 \(P(x \mid y), P(y)\) 分别进行估计。无论是对 \(P(x \mid y), P(y)\) 还是 \(P(y \mid x)\) 的估计, 都是会先假设分布的形式, 例如逻辑斯特回归就假设了 \(Y \mid X\) 服从伯努利分 布。分布形式固定以后, 剩下的就是分布参数的估计问题。常用的估计有极大似然估计(MLE)和极大后验概率估计 (MAP) 等。其中, 极大后验概率估计涉及到分布参数的先验概率, 这为我们注入先验知识提供了途径。逻辑斯特回归、高斯判别分析、朴素贝叶斯都属于概率模型。

在一定的条件下,非概率模型与概率模型有以下对应关系:

img

非概率模型

感知机、支持向量机、神经网络、k近邻都属于非概率模型。线性支持向量机可以显式地写出损失函数——hinge损失。神经网络也可以显式地写出损失函数——平方损失。

非概率模型指的是直接学习输入空间到输出空间的映射 \(h\), 学习的过程中基本不涉及概率密度的估计, 概率密度 的积分等操作, 问题的关键在于最优化问题的求解。通常, 为了学习假设 \(h(x)\), 我们会先根据一些先验知识 (prior knowledge) 来选择一个特定的假设空间 \(H(x)\) (函数空间), 例如一个由所有线性函数构成的空间, 然后在 这个空间中找出泛化误差最小的假设出来, \[ h^*=\arg \min _{h \in H} \varepsilon(h)=\arg \min _{h \in H} \sum_{x, y} l(h(x), y) P(x, y) \] 其中 \(l(h(x), y)\) 是我们选取的损失函数, 选择不同的损失函数, 得到假设的泛化误差就会不一样。由于我们并不知 道 \(P(x, y)\), 所以即使我们选好了损失函数, 也无法计算出假设的泛化误差, 更别提找到那个给出最小泛化误差的 假设。于是, 我们转而去找那个使得经验误差最小的假设, \[ g=\arg \min _{h \in H} \hat{\varepsilon}(h)=\arg \min _{h \in H} \frac{1}{m} \sum_{i=1}^{m} l\left(h\left(x^{(i)}\right), y^{(i)}\right) \] 这种学习的策略叫经验误差最小化(ERM),理论依据是大数定律:当训练样例无穷多的时候,假设的经验误差会依概率收敛到假设的泛化误差。要想成功地学习一个问题,必须在学习的过程中注入先验知识。前面,我们根据先验知识来选择假设空间,其实,在选定了假设空间后,先验知识还可以继续发挥作用,这一点体现在为我们的优化问题加上正则化项上,例如常用的\(L1\)正则化, \(L2\)正则化等。 \[ g=\arg \min _{h \in H} \hat{\varepsilon}(h)=\arg \min _{h \in H} \frac{1}{m} \sum_{i=1}^{m} l\left(h\left(x^{(i)}\right), y^{(i)}\right)+\lambda \Omega(h) \]

四、 过拟合和欠拟合

欠拟合、过拟合及如何防止过拟合 - G-kdom的文章 - 知乎 https://zhuanlan.zhihu.com/p/72038532

4.1 欠拟合

欠拟合是指模型不能在训练集上获得足够低的误差。换句换说,就是模型复杂度低,模型在训练集上就表现很差,没法学习到数据背后的规律。

4.2 欠拟合解决方法

欠拟合基本上都会发生在训练刚开始的时候,经过不断训练之后欠拟合应该不怎么考虑了。但是如果真的还是存在的话,可以通过增加网络复杂度或者在模型中增加特征,这些都是很好解决欠拟合的方法。

4.3 过拟合

过拟合是指训练误差和测试误差之间的差距太大。换句换说,就是模型复杂度高于实际问题,模型在训练集上表现很好,但在测试集上却表现很差。模型对训练集"死记硬背"(记住了不适用于测试集的训练集性质或特点),没有理解数据背后的规律,泛化能力差

造成原因主要有以下几种: 1、训练数据集样本单一,样本不足。如果训练样本只有负样本,然后那生成的模型去预测正样本,这肯定预测不准。所以训练样本要尽可能的全面,覆盖所有的数据类型。 2、训练数据中噪声干扰过大。噪声指训练数据中的干扰数据。过多的干扰会导致记录了很多噪声特征,忽略了真实输入和输出之间的关系。 3、模型过于复杂。模型太复杂,已经能够“死记硬背”记下了训练数据的信息,但是遇到没有见过的数据的时候不能够变通,泛化能力太差。我们希望模型对不同的模型都有稳定的输出。模型太复杂是过拟合的重要因素。

4.4 如何防止过拟合

要想解决过拟合问题,就要显著减少测试误差而不过度增加训练误差,从而提高模型的泛化能力。

1、使用正则化(Regularization)方法。

那什么是正则化呢?正则化是指修改学习算法,使其降低泛化误差而非训练误差

常用的正则化方法根据具体的使用策略不同可分为:(1)直接提供正则化约束的参数正则化方法,如L1/L2正则化;(2)通过工程上的技巧来实现更低泛化误差的方法,如提前终止(Early stopping)和Dropout;(3)不直接提供约束的隐式正则化方法,如数据增强等。

L2正则化起到使得权重参数\(w\)变小的效果,为什么能防止过拟合呢?因为更小的权重参数\(w\)意味着模型的复杂度更低,对训练数据的拟合刚刚好,不会过分拟合训练数据,从而提高模型的泛化能力。

2、获取和使用更多的数据(数据集增强)——解决过拟合的根本性方法

让机器学习或深度学习模型泛化能力更好的办法就是使用更多的数据进行训练。但是,在实践中,我们拥有的数据量是有限的。解决这个问题的一种方法就是创建“假数据”并添加到训练集中——数据集增强。通过增加训练集的额外副本来增加训练集的大小,进而改进模型的泛化能力。

我们以图像数据集举例,能够做:旋转图像、缩放图像、随机裁剪、加入随机噪声、平移、镜像等方式来增加数据量。另外补充一句,在物体分类问题里,CNN在图像识别的过程中有强大的“不变性”规则,即待辨识的物体在图像中的形状、姿势、位置、图像整体明暗度都不会影响分类结果。我们就可以通过图像平移、翻转、缩放、切割等手段将数据库成倍扩充。

3. 采用合适的模型(控制模型的复杂度)

过于复杂的模型会带来过拟合问题。对于模型的设计,目前公认的一个深度学习规律"deeper is better"。国内外各种大牛通过实验和竞赛发现,对于CNN来说,层数越多效果越好,但是也更容易产生过拟合,并且计算所耗费的时间也越长。

根据奥卡姆剃刀法则:在同样能够解释已知观测现象的假设中,我们应该挑选“最简单”的那一个。对于模型的设计而言,我们应该选择简单、合适的模型解决复杂的问题

4. 降低特征的数量

对于一些特征工程而言,可以降低特征的数量——删除冗余特征,人工选择保留哪些特征。这种方法也可以解决过拟合问题。

5. Dropout

Dropout是在训练网络时用的一种技巧(trike),相当于在隐藏单元增加了噪声。Dropout 指的是在训练过程中每次按一定的概率(比如50%)随机地“删除”一部分隐藏单元(神经元)。所谓的“删除”不是真正意义上的删除,其实就是将该部分神经元的激活函数设为0(激活函数的输出为0),让这些神经元不计算而已。

Dropout为什么有助于防止过拟合呢?

(a)在训练过程中会产生不同的训练模型,不同的训练模型也会产生不同的的计算结果。随着训练的不断进行,计算结果会在一个范围内波动,但是均值却不会有很大变化,因此可以把最终的训练结果看作是不同模型的平均输出。

(b)它消除或者减弱了神经元节点间的联合,降低了网络对单个神经元的依赖,从而增强了泛化能力。

6. Early stopping(提前终止)

对模型进行训练的过程即是对模型的参数进行学习更新的过程,这个参数学习的过程往往会用到一些迭代方法,如梯度下降(Gradient descent)。Early stopping是一种迭代次数截断的方法来防止过拟合的方法,即在模型对训练数据集迭代收敛之前停止迭代来防止过拟合

为了获得性能良好的神经网络,训练过程中可能会经过很多次epoch(遍历整个数据集的次数,一次为一个epoch)。如果epoch数量太少,网络有可能发生欠拟合;如果epoch数量太多,则有可能发生过拟合。Early stopping旨在解决epoch数量需要手动设置的问题。具体做法:每个epoch(或每N个epoch)结束后,在验证集上获取测试结果,随着epoch的增加,如果在验证集上发现测试误差上升,则停止训练,将停止之后的权重作为网络的最终参数。

为什么能防止过拟合?当还未在神经网络运行太多迭代过程的时候,w参数接近于0,因为随机初始化w值的时候,它的值是较小的随机值。当你开始迭代过程,w的值会变得越来越大。到后面时,w的值已经变得十分大了。所以early stopping要做的就是在中间点停止迭代过程。我们将会得到一个中等大小的w参数,会得到与L2正则化相似的结果,选择了w参数较小的神经网络。

Early Stopping缺点:没有采取不同的方式来解决优化损失函数和过拟合这两个问题,而是用一种方法同时解决两个问题 ,结果就是要考虑的东西变得更复杂。之所以不能独立地处理,因为如果你停止了优化损失函数,你可能会发现损失函数的值不够小,同时你又不希望过拟合。

五、损失函数(loss)与评价指标(metric)的区别?

当建立一个学习算法时,我们希望最大化一个给定的评价指标matric(比如说准确度),但算法在学习过程中会尝试优化一个不同的损失函数loss(比如说MSE/Cross-entropy)。

那为什么不把评价指标matric作为学习算法的损失函数loss呢?

  • 一般来说,我认为你应该尝试优化一个与你最关心的评价指标相对应的损失函数。例如,在做分类时,我认为你需要给我一个很好的理由,让我不要优化交叉熵。也就是说,交叉熵并不是一个非常直观的指标,所以一旦你完成了训练,你可能还想知道你的分类准确率有多高,以了解你的模型是否真的能在现实世界中发挥作用,总之,在每个epoch训练完后,你都会有多个评估指标。这样作的主要原因是为了了解你的模型在做什么。这意味着你想要最大化指标A,以便得到一个接近最大化指标B的解决方案。

  • 通常情况下,MSE/交叉熵比精度更容易优化,因为它们对模型参数是可微的,在某些情况下甚至是凸的,这使得它更容易。

六、标准化和归一化

PCA、k-means、SVM、回归模型、神经网络

定义

归一化和标准化都是对数据做变换的方式,将原始的一列数据转换到某个范围,或者某种形态,具体的:

归一化(Normalization):将一列数据变化到某个固定区间(范围)中,通常,这个区间是[0, 1],广义的讲,可以是各种区间,比如映射到[0,1]一样可以继续映射到其他范围,图像中可能会映射到[0,255],其他情况可能映射到[-1,1];

标准化(Standardization):将数据变换为均值为0,标准差为1的分布切记,并非一定是正态的;

中心化:另外,还有一种处理叫做中心化,也叫零均值处理,就是将每个原始数据减去这些数据的均值。

差异

归一化:对处理后的数据范围有严格要求;

标准化: 数据不为稳定,存在极端的最大最小值; 涉及距离度量、协方差计算的时候;

  • 归一化会严格的限定变换后数据的范围,比如按之前最大最小值处理的,它的范围严格在[ 0 , 1 ]之间;而标准化就没有严格的区间,变换后的数据没有范围,只是其均值是0,标准差为1。
  • 归一化的缩放比例仅仅与极值有关,容易受到异常值的影响。

用处

  • 回归模型,自变量X的量纲不一致导致了回归系数无法直接解读或者错误解读;需要将X都处理到统一量纲下,这样才可比;
  • 机器学习任务和统计学任务中有很多地方要用到“距离”的计算,比如PCA,比如KNN,比如kmeans等等,假使算欧式距离,不同维度量纲不同可能会导致距离的计算依赖于量纲较大的那些特征而得到不合理的结果;
  • 参数估计时使用梯度下降,在使用梯度下降的方法求解最优化问题时, 归一化/标准化后可以加快梯度下降的求解速度,即提升模型的收敛速度

其他:log、sigmod、softmax 变换

七、回归 vs 分类

回归问题可以理解为是定量输出的问题,是一个连续变量预测;分类问题可以理解为是定性输出的问题,是一个离散变量预测。

如何理解回归与分类?

八、常见损失函数求导

sigmod 交叉熵求导

交叉熵损失函数为:

\[ J(\theta)=-\frac{1}{m} \sum_{i=1}^m y^{(i)} \log \left(h_\theta\left(x^{(i)}\right)\right)+\left(1-y^{(i)}\right) \log \left(1-h_\theta\left(x^{(i)}\right)\right) \] 其中: \[ \begin{gathered} \log h_\theta\left(x^{(i)}\right)=\log \frac{1}{1+e^{-\theta^T x^{(i)}}}=-\log \left(1+e^{-\theta^T x^{(i)}}\right), \\ \log \left(1-h_\theta\left(x^{(i)}\right)\right)=\log \left(1-\frac{1}{1+e^{-\theta^T x^{(i)}}}\right)=\log \left(\frac{e^{-\theta^T x^{(i)}}}{1+e^{-\theta^T x^{(i)}}}\right) \\ =\log \left(e^{-\theta^T x^{(i)}}\right)-\log \left(1+e^{-\theta^T x^{(i)}}\right)=-\theta^T x^{(i)}-\log \left(1+e^{-\theta^T x^{(i)}}\right) \end{gathered} \] 由此, 得到: \[ \begin{gathered} J(\theta)=-\frac{1}{m} \sum_{i=1}^m\left[-y^{(i)}\left(\log \left(1+e^{-\theta^T x^{(i)}}\right)\right)+\left(1-y^{(i)}\right)\left(-\theta^T x^{(i)}-\log \left(1+e^{-\theta^T x^{(i)}}\right)\right)\right] \\ =-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\theta^T x^{(i)}-\log \left(1+e^{-\theta^T x^{(i)}}\right)\right] \\ =-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\log e^{\theta^T x^{(i)}}-\log \left(1+e^{-\theta^T x^{(i)}}\right)\right] \\ =-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\left(\log e^{\theta^T x^{(i)}}+\log \left(1+e^{-\theta^T x^{(i)}}\right)\right)\right]\\ =-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\log \left(1+e^{\theta^T x^{(i)}}\right)\right] \end{gathered} \] 由此, 得到: \[ \begin{aligned} & J(\theta)=-\frac{1}{m} \sum_{i=1}^m {\left[-y^{(i)}\left(\log \left(1+e^{-\theta^T x^{(i)}}\right)\right)+\left(1-y^{(i)}\right)\left(-\theta^T x^{(i)}-\log \left(1+e^{-\theta^T x^{(i)}}\right)\right)\right] } \\ &=-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\theta^T x^{(i)}-\log \left(1+e^{-\theta^T x^{(i)}}\right)\right] \\ &=-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\log e^{\theta^T x^{(i)}}-\log \left(1+e^{-\theta^T x^{(i)}}\right)\right] \\ &=-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\left(\log e^{\theta^T x^{(i)}}+\log \left(1+e^{-\theta^T x^{(i)}}\right)\right)\right] \\ &=-\frac{1}{m} \sum_{i=1}^m\left[y^{(i)} \theta^T x^{(i)}-\log \left(1+e^{\theta^T x^{(i)}}\right)\right] \end{aligned} \] 求导: \[ \begin{aligned} & \frac{\partial}{\partial \theta_j} J(\theta)=\frac{\partial}{\partial \theta_j}\left(\frac{1}{m} \sum_{i=1}^m\left[\log \left(1+e^{\theta^T x^{(i)}}\right)-y^{(i)} \theta^T x^{(i)}\right]\right) \\ & =\frac{1}{m} \sum_{i=1}^m\left[\frac{\partial}{\partial \theta_j} \log \left(1+e^{\theta^T x^{(i)}}\right)-\frac{\partial}{\partial \theta_j}\left(y^{(i)} \theta^T x^{(i)}\right)\right] \\ & =\frac{1}{m} \sum_{i=1}^m\left(\frac{x_j^{(i)} e^{\theta^T x^{(i)}}}{1+e^{\theta^T x^{(i)}}}-y^{(i)} x_j^{(i)}\right) \\ & =\frac{1}{m} \sum_{i=1}^m\left(h_\theta\left(x^{(i)}\right)-y^{(i)}\right) x_j^{(i)} \\ & \end{aligned} \] 这就是交叉熵对参数的导数: \[ \frac{\partial}{\partial \theta_j} J(\theta)=\frac{1}{m} \sum_{i=1}^m\left(h_\theta\left(x^{(i)}\right)-y^{(i)}\right) x_j^{(i)} \]

平方损失函数【绝对值、hubor损失】为例(GBDT 残差):

\[ \begin{aligned} &g_{i}=\frac{\partial\left(\hat{y}^{t-1}-y_{i}\right)^{2}}{\partial \hat{y}^{t-1}}=2\left(\hat{y}^{t-1}-y_{i}\right) \\ &h_{i}=\frac{\partial^{2}\left(\hat{y}^{t-1}-y_{i}\right)^{2}}{\hat{y}^{t-1}}=2 \end{aligned} \]

softmax 函数求导

softmax 回归的参数矩阵 \(\theta\) 可以记为 \[ \theta=\left[\begin{array}{c} \theta_{1}^{T} \\ \theta_{2}^{T} \\ \vdots \\ \theta_{k}^{T} \end{array}\right] \] 定义 softmax 回归的代价函数 \[ L(\theta)=-\frac{1}{m}\left[\sum_{i=1}^{m} \sum_{j=1}^{k} 1\left\{y_{i}=j\right\} \log \frac{e^{\theta_{j}^{T} x_{i}}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}}\right] \] 其中, 1{:}是示性函数, 即 \(1\{\) 值为真的表达式 \(\}=1 , 1\{\) 值为假的表达式 \(\}=0\) 。跟 logistic 函数一样, 利用梯度下降法最小化代价函数, 下面 求解 \(\theta\) 的梯度。 \(L(\theta)\) 关于 \(\theta_{j}\) 的梯度求解为 \[ \begin{aligned} \frac{\partial L(\theta)}{\partial \theta_{j}} &=-\frac{1}{m} \frac{\partial}{\partial \theta_{j}}\left[\sum_{i=1}^{m} \sum_{j=1}^{k} 1\left\{y_{i}=j\right\} \log \frac{e^{\theta_{j}^{T} x_{i}}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}}\right] \\ &=-\frac{1}{m} \frac{\partial}{\partial \theta_{j}}\left[\sum_{i=1}^{m} \sum_{j=1}^{k} 1\left\{y_{i}=j\right\}\left(\theta_{j}^{T} x_{i}-\log \sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}\right)\right] \\ &=-\frac{1}{m}\left[\sum_{i=1}^{m} 1\left\{y_{i}=j\right\}\left(x_{i}-\sum_{j=1}^{k} \frac{e^{\theta_{j}^{T} x_{i}} \cdot x_{i}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}}\right)\right] \\ &=-\frac{1}{m}\left[\sum_{i=1}^{m} x_{i} 1\left\{y_{i}=j\right\}\left(1-\sum_{j=1}^{k} \frac{e^{\theta_{j}^{T} x_{i}}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}}\right)\right] \\ &=-\frac{1}{m}\left[\sum_{i=1}^{m} x_{i}\left(1\left\{y_{i}=j\right\}-\sum_{j=1}^{k} 1\left\{y_{i}=j\right\} \frac{e^{\theta_{j}^{T} x_{i}}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}}\right)\right] \\ &=-\frac{1}{m}\left[\sum_{i=1}^{m} x_{i}\left(1\left\{y_{i}=j\right\}-\frac{e^{\theta_{j}^{T} x_{i}}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x_{i}}}\right)\right] \\ &=-\frac{1}{m}\left[\sum_{i=1}^{m} x_{i}\left(1\left\{y_{i}=j\right\}-p\left(y_{i}=j \mid x_{i} ; \theta\right)\right)\right] \end{aligned} \]

特征工程-特征处理

一、 数值类型处理

pandas 显示所有列:

1
2
3
4
5
6
#显示所有列
pd.set_option('display.max_columns', None)
#显示所有行
pd.set_option('display.max_rows', None)
#设置value的显示长度为100,默认为50
pd.set_option('max_colwidth',100)

pandas 查看缺失特征:

1
train.isnull().sum().sort_values(ascending = False) / train.shape[0]

pandas 查看某一列的分布:

1
df.loc[:,col_name].value_counts()

特征提取方式是可以深挖隐藏在数据背后更深层次的信息的。其次,数值类型数据也并不是直观看上去那么简单易用,因为不同的数值类型的计量单位不一样,比如个数、公里、千克、DB、百分比之类,同样数值的大小也可能横跨好几个量级,比如小到头发丝直径约为0.00004米, 大到热门视频播放次数成千上万次。

1.1 数据归一化

为什么要数据归一化?

深度学习(3)Normalization*.md

  • 可解释性回归模型【无正则化】中自变量X的量纲不一致导致了回归系数无法直接解读或者错误解读;需要将X都处理到统一量纲下,这样才可比【可解释性】;取决于我们的逻辑回归是不是用了正则化。如果你不用正则,标准化并不是必须的,如果用正则,那么标准化是必须的。
  • 距离计算:机器学习任务和统计学任务中有很多地方要用到“距离”的计算,比如PCA、KNN,kmeans和SVM等等,假使算欧式距离,不同维度量纲不同可能会导致距离的计算依赖于量纲较大的那些特征而得到不合理的结果;
  • 加速收敛:参数估计时使用梯度下降,在使用梯度下降的方法求解最优化问题时, 归一化/标准化后可以加快梯度下降的求解速度,即提升模型的收敛速度

需要归一化的模型:利用梯度下降法求解的模型一般需要归一化,线性回归、LR、SVM、KNN、神经网络

\[ \tilde{x}=\frac{x-\min (x)}{\max (x)-\min (x)} \]

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np 
from sklearn.preprocessing import MinMaxScaler

# define data
data = np.asarray([[100, 0.001],
[8, 0.05],
[50, 0.005],
[88, 0.07],
[4, 0.1]])
# define min max scaler
scaler = MinMaxScaler()
# transform data
scaled = scaler.fit_transform(data)

1.2 数据标准化

数据标准化是指通过改变数据的分布得到均值为0,标准差为1的服从标准正态分布的数据。主要目的是为了让不同特征之间具有相同的尺度(Scale),这样更有理化模型训练收敛。

1
2
3
4
5
6
from sklearn.preprocessing import StandardScaler
# define standard scaler
scaler = StandardScaler()
# transform data
scaled = scaler.fit_transform(data)
print(scaled)

1.3 对数转换

\(\log\) 函数的定义为 \(\log _a\left(\alpha^x\right)=x\), 其中 \(\mathrm{a}\)\(\log\) 函数的底数, \(\alpha\) 是一个正常数, \(x\) 可以是任何正数。由于 \(\alpha^0=1\) \(a=10\) 时, 函数 \(\log _{10}(x)\) 可以将 \([1,10]\) 映射到[ \([0,1]\), 将 \([1,100]\) 映射到 \([1,2]\) 。换句话说, log函数压缩了大数的范 围, 扩大了小数的范围\(\mathrm{x}\) 越大, \(\log (\mathrm{x})\) 增量越慢。 \(\log (\mathrm{x})\) 函数的图像如下:

image-20220426154119370

Log函数可以极大压缩数值的范围,相对而言就扩展了小数字的范围。该转换方法适用于长尾分布且值域范围很大的特征,变换后的特征趋向于正态分布。对数值类型使用对数转换一般有以下几种好处:

  • 缩小数据的绝对数值
  • 取对数后,可以将乘法计算转换成加法计算
  • 在数据的整个值域中不同区间的差异带来的影响不同
  • 取对数后不会改变数据的性质和相关关系,但压缩了变量的尺度。
  • 得到的数据易消除异方差问题

二、序数和类别特征处理

本文主要说明特征工程中关于序数特征类别特征的常用处理方法。主要包含LabelEncoderOne-Hot编码DummyCodingFeatureHasher以及要重点介绍的WOE编码

2.1 序数特征处理

序数特征指的是有序但无尺度的特征。比如表示‘学历’的特征,'高中'、'本科'、'硕士',这些特征彼此之间是有顺序关系的,但是特征本身无尺度,并且也可能不是数值类型。在实际应用中,一般是字符类型居多,为了将其转换成模型能处理的形式,通常需要先进行编码,比如LabelEncoding。如果序数特征本身就是数值类型变量,则可不进行该步骤。下面依次介绍序数特征相关的处理方式。

  • Label Encoding

1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.preprocessing import LabelEncoder

x = ['a', 'b', 'a', 'c', 'b']
encoder = LabelEncoder()
x1 = encoder.fit_transform(x)

x2 = pd.Series(x).astype('category')
x2.cat.codes.values
# pandas 因子化
x2, uniques = pd.factorize(x)
# pandas 二值化
x2 = pd.Series(x)
x2 = (x2 >= 'b').astype(int) #令大于等于'b'的都为1

2.2 类别特征处理

类别特征由于没有顺序也没有尺度,因此处理较为麻烦,但是在CTR等领域却是非常常见的特征。比如商品的类型,颜色,用户的职业,兴趣等等。类别变量编码方法中最常使用的就是One-Hot编码,接下来结合具体实例来介绍。

  • One-Hot编码

One-Hot编码,又称为'独热编码',其变换后的单列特征值只有一位是1。如下例所示,一个特征中包含3个不同的特征值(a,b,c),编码转换后变成3个子特征,其中每个特征值中只有一位是有效位1。

1
2
3
4
5
6
7
from sklearn.preprocessing import LabelEncoder, OneHotEncoder

one_feature = ['b', 'a', 'c']
label_encoder = LabelEncoder()
feature = label_encoder.fit_transform(one_feature)
onehot_encoder = OneHotEncoder(sparse=False)
onehot_encoder.fit_transform(feature.reshape(-1, 1))
  • LabelBinarizer

sklearn中的LabelBinarizer也具有同样的作用,代码如下:

1
2
3
from sklearn.preprocessing import LabelBinarizer
feature = np.array(['b', 'a', 'c'])
LabelBinarizer().fit_transform(feature)
  • 虚拟编码Dummy Coding

同样,pandas中也内置了对应的处理方式,使用起来比Sklearn更加方便,产生n-1个特征。实例如下:

1
2
one_feature = ['b', 'a', 'c']
pd.get_dummies(one_feature, prefix='test') # 设置前缀test
  • 特征哈希(feature hashing)

按照上述编码方式,如果某个特征具有100个类别值,那么经过编码后将产生100个或99个新特征,这极大地增加了特征维度和特征的稀疏度,同时还可能会出现内存不足的情况。sklearn中的FeatureHasher接口采用了hash的方法,将不同的值映射到用户指定长度的数组中,使得输出特征的维度是固定的,该方法占用内存少,效率高,可以在多类别变量值中使用,但是由于采用了Hash函数的方式,所以具有冲突的可能,即不同的类别值可能映射到同一个特征变量值中。

Feature hashing(特征哈希): https://blog.csdn.net/laolu1573/article/details/79410187

https://scikit-learn.org/stable/modules/feature_extraction.html#feature-hashing

如何用通俗的语言解释CTR和推荐系统中常用的Feature Hashing技术以及其对应的优缺点?

1
2
3
4
5
6
from sklearn.feature_extraction import FeatureHasher

h = FeatureHasher(n_features=5, input_type='string')
test_cat = ['a','b','c','d','e','f','g','h','i','j','a','b']
f = h.transform(test_cat)
f.toarray()

如果hash的目标空间足够大,并且hash函数本身足够散列,不会损失什么特征信息。

feature hashing简单来说和kernal的思想是类似的,就是把输入的特征映射到一个具有一些我们期望的较好性质的空间上去。在feature hasing这个情况下我们希望目标的空间具有如下的性质:

  1. 样本无关的维度大小,因为当在线学习,或者数据量非常大,提前对数据观察开销非常大的时候,这可以使得我们能够提前给算法分配存储和切分pattern。大大提高算法的工程友好性
  2. 这个空间一般来说比输入的特征空间维度小很多。
  3. 另外我们假设在原始的特征空间里,样本的分布是非常稀疏的,只有很少一部分子空间是被取值的。
  4. 保持内积的无偏(不变肯定是不可能的,因为空间变小了),否则很多机器学习方法就没法用了。

原理:假设输入特征是一个 \(\mathrm{N}\) 维的0/1取值的向量 \(\mathrm{x}_{\circ} 一 个 \mathrm{~N}->\mathrm{M}\) 的哈希函数 \(\mathrm{h}\) 。那么 \(\phi_j=\sum_{h(i)=j} x_i\)

好处:

  • 从某种程度上来讲,使得训练样本的特征在对应空间里的分布更均匀了。这个好处对于实际训练过程是非常大的,某种程度上起到了shuffle的作用
  • 特征的空间变小了,而且是一个可以预测的大小。比如说加入输入特征里有个东西叫做user_id,那么显然你也不知道到底有多少userid的话,你需要先扫描一遍并且分配足够的空间给到它不然学着学着oom了。你也不能很好地提前优化分片
  • 对在线学习非常友好。

坏处:

  • 会给debug增加困难,为了debug你要保存记录h计算的过程数据,否则如果某个特征有毛病,你怎么知道到底是哪个原始特征呢?
  • 没选好哈希函数的话,可能会造成碰撞,如果原始特征很稠密并且碰撞很严重,那可能会带来坏的训练效果。
1
2
3
4
5
6
7
8
9
10
def hashing_vectorizer(features, N):
x = [0] * N
for f in features:
h = hash(f)
idx = h % N
if xt(f) 1: # xt 2值hash函数减少hash冲突
x[idx] += 1
else:
x[idx] -= 1
return x
  • 多类别值处理方式 -- 基于统计的编码方法

当类别值过多时,One-Hot 编码或者Dummy Coding都可能导致编码出来的特征过于稀疏,其次也会占用过多内存。如果使用FeatureHasher,n_features的设置不好把握,可能会造成过多冲突,造成信息损失。这里提供一种基于统计的编码方法,包括基于特征值的统计或者基于标签值的统计——基于标签的编码。

1
2
3
4
5
import seaborn as sns

test = ['a','b','c','d','e','a','a','c']
df = pd.DataFrame(test, columns=['alpha'])
sns.countplot(df['alpha'])

image-20220426130800412

首先我们将每个类别值出现的频数计算出来,比如我们设置阈值为1,那么所有小于阈值1的类别值都会被编码为同一类,大于1的类别值会分别编码,如果出现频数一样的类别值,既可以都统一分为一个类,也可以按照某种顺序进行编码,这个可以根据业务需要自行决定。那么根据上图,可以得到其编码值为:

\[ \left\{a^{\prime}: 0, ' c^{\prime}: 1, ' e^{\prime}: 2, ' b^{\prime}: 2, ' d ': 2\right\} \] 即(a,c)分别编码为一个不同的类别,(e,b,d)编码为同一个类别。

2.3 二阶

w * h = s

三、 特征离散化处理方法

特征离散化指的是将连续特征划分离散的过程:将原始定量特征的一个区间一一映射到单一的值。离散化过程也被表述成分箱(Binning)的过程。特征离散化常应用于逻辑回归和金融领域的评分卡中,同时在规则提取,特征分类中也有对应的应用价值。本文主要介绍几种常见的分箱方法,包括等宽分箱、等频分箱、信息熵分箱基于决策树分箱、卡方分箱等。

可以看到在分箱之后,数据被规约和简化,有利于理解和解释。总来说特征离散化,即 分箱之后会带来如下优势:

  • 有助于模型部署和应用,加快模型迭代
  • 增强模型鲁棒性
  • 增加非线性表达能力:连续特征不同区间对模型贡献或者重要程度不一样时,分箱后不同的权重能直接体现这种差异,离散化后的特征再进行特征 交叉衍生能力会进一步加强。
  • 提升模型的泛化能力
  • 扩展数据在不同各类型算法中的应用范围

当然特征离散化也有其缺点,总结如下:

  • 分箱操作必定会导致一定程度的信息损失
  • 增加流程:建模过程中加入了额外的的离散化步骤
  • 影响模型稳定性: 当一个特征值处于分箱点的边缘时,此时微小的偏差会造成该特征值的归属从一箱跃迁到另外一箱,影响模型的稳定性。

3.1 等宽分箱(Equal-Width Binning)

等宽分箱指的是每个分隔点或者划分点的距离一样,即等宽。实践中一般指定分隔的箱数,等分计算后得到每个分隔点。例如将数据序列分为n份,则 分隔点的宽度计算公式为: \[ w=\frac{\max -\min }{n} \] 这样就将原始数据划分成了n个等宽的子区间,一般情况下,分箱后每个箱内的样本数量是不一致的。使用pandas中的cut函数来实现等宽分箱,代码如下:

1
value, cutoff = pd.cut(df['mean radius'], bins=4, retbins=True, precision=2)

等宽分箱计算简单,但是当数值方差较大时,即数据离散程度很大,那么很可能出现没有任何数据的分箱,这个问题可以通过自适应数据分布的分箱方法--等频分箱来避免

3.2 等频分箱(Equal-Frequency Binning)

等频分箱理论上分隔后的每个箱内得到数据量大小一致,但是当某个值出现次数较多时,会出现等分边界是同一个值,导致同一数值分到不同的箱内,这是不正确的。具体的实现可以去除分界处的重复值,但这也导致每箱的数量不一致。如下代码:

1
2
3
s1 = pd.Series([1,2,3,4,5,6])
value, cutoff = pd.qcut(s1, 3, retbins=True)
sns.countplot(value)

上述的等宽和等频分箱容易出现的问题是每箱中信息量变化不大。例如,等宽分箱不太适合分布不均匀的数据集、离群值;等频方法不太适合特定的值占比过多的数据集,如长尾分布

3.3 信息熵分箱【有监督】

如果分箱后箱内样本对y的区分度好,那么这是一个好的分箱。通过信息论理论,我们可知信息熵衡量了这种区分能力。当特征按照某个分隔点划分为上下两部分后能达到最大的信息增益,那么这就是一个好的分隔点。由上可知,信息熵分箱是有监督的分箱方法。 其实决策树的节点分裂原理也是基于信息熵。

首先我们需要明确信息熵和信息增益的计算方式, 分别如下: \[ \begin{gathered} \operatorname{Entropy}(y)=-\sum_{i=1}^m p_i \log _2 p_i \\ \operatorname{Gain}(x)=\operatorname{Entropy}(y)-\operatorname{Infos}_{\text {split }}(x) \end{gathered} \] 在二分类问题中, \(m=2\) 。信息增益的物理含义表达为: \(x\) 的分隔带来的信息对 \(y\) 的不确定性带来的增益。 对于二值化的单点分隔, 如果我们找到一个分隔点将数据一分为二, 分成 \(P_1\)\(P_2\) 两部分, 那么划分后的信息熵 的计算方式为: \[ \operatorname{Info}_{\text {split }}(x)=P 1_{\text {ratio }} \operatorname{Entropy}\left(x_{p 1}\right)+P 2_{\text {ratio }} \operatorname{Entropy}\left(x_{p 2}\right) \] 同时也可以看出,当分箱后,某个箱中的标签y的类别(0或者1)的比例相等时,其熵值最大,表明此特征划分几 乎没有区分度。而当某个箱中的数据的标签 \(y\) 为单个类别时, 那么该箱的熵值达到最小的 0 , 即纯度最纯, 最具区 分度。从结果上来看, 最大信息增益对应分箱后的总熵值最小。

3.4 决策树分箱【有监督】

由于决策树的结点选择和划分也是根据信息熵来计算的,因此我们其实可以利用决策树算法来进行特征分箱,具体做法如下:

还是以乳腺癌数据为例,首先取其中‘mean radius’字段,和标签字段‘target’来拟合一棵决策树,代码如下:

1
2
3
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(criterion='entropy', max_depth=3) # 树最大深度为3
dt.fit(df['mean radius'].values.reshape(-1, 1), df['target'])

接着我们取出这课决策树的所有叶节点的分割点的阈值,如下:

1
2
3
qts = dt.tree_.threshold[np.where(dt.tree_.children_left > -1)]
qts = np.sort(qts)
res = [np.round(x, 3) for x in qts.tolist()]

3.5 卡方分箱 【有监督】

特征选择之卡方分箱、WOE/IV - 云水僧的文章 - 知乎 https://zhuanlan.zhihu.com/p/101771771

卡方检验可以用来评估两个分布的相似性,因此可以将这个特性用到数据分箱的过程中。卡方分箱认为:理想的分箱是在同一个区间内标签的分布是相同的; 卡方分布是概率统计常见的一种概率分布,是卡方检验的基础。

布定义为: 若n个独立的随机变量 \(Z_1, Z_2, \ldots, Z_k\) 满足标准正态分布 \(N(0,1)\), 则 \(\mathrm{n}\) 个随机变量的平方和 \(X=\sum_{i=0}^k Z_i^2\) 为服从自由度为 \(\mathrm{k}\) 的卡方分布, 记为 \(X \sim \chi^2\) 。参数 \(\mathrm{n}\) 称为自由度(样本中独立或能自由变化的自变 量的个数), 不同的自由度是不同的分布。

卡方检验:卡方检验属于非参数假设检验的一种,其本质都是度量频数之间的差异。其假设为:观察频数与期望 频数无差异或者两组变量相互独立不相关。 \[ \chi^2=\sum \frac{(O-E)^2}{E} \]

  • 卡方拟合优度检验:用于检验样本是否来自于某一个分布,比如检验某样本是否为正态分布
  • 独立性卡方检验,查看两组类别变量分布是否有差异或者相关,以列联表的形式比较。以列联表形式的卡方检验中,卡方统计量由上式给出。

步骤:

卡方分箱是自底向上的(即基于合并的)数据离散化方法。它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。基本思想: 对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。

理想的分箱是在同一个区间内标签的分布是相同的。卡方分箱就是不断的计算相邻区间的卡方值(卡方值越小表示分布越相似),将分布相似的区间(卡方值最小的)进行合并,直到相邻区间的分布不同,达到一个理想的分箱结果。下面用一个例子来解释:

image-20220708173638301

由上图,第一轮中初始化是5个区间,分别计算相邻区间的卡方值。找到1.2是最小的,合并2、3区间,为了方便,将合并后的记为第2区间,因此得到4个区间。第二轮中,由于合并了区间,影响该区间与前面的和后面的区间的卡方值,因此重新计算1和2,2和4的卡方值,由于4和5区间没有影响,因此不需要重新计算,这样就得到了新的卡方值列表,找到最小的取值2.5,因此该轮会合并2、4区间,并重复这样的步骤,一直到满足终止条件。

3.6 WOE编码 【有监督】

风控模型—WOE与IV指标的深入理解应用: https://zhuanlan.zhihu.com/p/80134853

WOE (Weight of Evidence, 证据权重)编码利用了标签信息, 属于有监督的编码方式。该方式广泛用于金融领 域信用风险模型中, 是该领域的经验做法。下面先给出WOE的计算公式: \[ W O E_i=\ln \left\{\frac{P_{y 1}}{P_{y 0}}\right\}=\ln \left\{\frac{B_i / B}{G_i / G}\right\} \] \(W O E_i\) 值可解释为第 \(i\) 类别中好坏样本分布比值的对数。其中各个分量的解释如下: - \(P_{y 1}\) 表示该类别中坏样本的分布 - \(P_{y 0}\) 表示该类别中好样本的分布 - \(B_i / B\) 表示该类别中坏样本的数量在总体坏样本中的占比 - \(G_i / G\) 表示该类别中好样本的数量在总体好样本中的占比

很明显,如果整个分数的值大于1,那么WOE值为正,否则为负,所以WOE值的取值范围为正负无穷。 WOE值直观上表示的实际上是“当前分组中坏客户占所有坏客户的比例”和“当前分组中好客户占所有坏客户的比例”的差异。转化公式以后,也可以理解为:当前这个组中坏客户和好客户的比值,和所有样本中这个比值的差异。这个差异为这两个比值的比值,再取对数来表示的。 WOE越大,这种差异越大,这个分组里的样本坏样本可能性就越大,WOE越小,差异越小,这个分组里的坏样本可能性就越小。

1
2
3
4
5
6
7
np.random.seed(0)
# 随机生成1000行数据
df = pd.DataFrame({
'x': np.random.choice(['R','G','B'], 1000),
'y': np.random.randint(2, size=1000)
})
df.head()

四、缺失值处理解析

看不懂你打我,史上最全的缺失值解析: https://zhuanlan.zhihu.com/p/379707046

https://zhuanlan.zhihu.com/p/137175585

机器学习模型 是否支持缺失值
XGBoost
LightGBM
线性回归
逻辑回归(LR)
随机森林(RF)
SVM
因子分解机(FM)
朴实贝叶斯(NB)

4.1 缺失值的替换

scikit-learn中填充缺失值的API是Imputer类,使用方法如下:

参数strategy有三个值可选:mean(平均值),median(中位数),most_frequent(众数)

1
2
3
4
5
6
7
8
rom sklearn.preprocessing import Imputer
import numpy as np
# 缺失值填补的时候必须得是float类型
# 缺失值要填充为np.nan,它是浮点型,strategy是填充的缺失值类型,这里填充平均数,axis代表轴,这里第0轴是列
im = Imputer(missing_values='NaN',strategy='mean',axis=0)
data = im.fit_transform([[1, 2],
[np.nan, 3],
[7, 6]])

4.2 缺失值的删除

五、异常值处理

数据预处理Q&A

 1、LR为什么要离散化?

[学习] 连续特征的离散化:在什么情况将连续的特征离散化之后可以获得更好的效果?

问题描述:发现CTR预估一般都是用LR,而且特征都是离散的,为什么一定要用离散特征呢?这样做的好处在哪里?求大拿们解答。

答案一(严林):

在工业界,很少直接将连续值作为逻辑回归模型的特征输入,而是将连续特征离散化为一系列0、1特征交给逻辑回归模型,这样做的优势有以下几点:

  1. 离散特征的增加和减少都很容易,易于模型的快速迭代;
  2. 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;
  3. 【鲁棒性】离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则为0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;
  4. 【模型假设】逻辑回归属于广义线性模型,表达能力受限;单变量离散化为N个后,每个变量有独立的权重,相当于为模型引入了非线性,能够提升模型表达能力,加大拟合;
  5. 【特征交叉】离散化后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;
  6. 特征离散化后,模型会更稳定,比如如果对用户年龄离散化,20-30作为一个区间,不会因为一个用户年龄长了一岁就变成一个完全不同的人。当然处于区间相邻的样本会刚好相反,所以怎么划分区间是门学问;
  7. 特征离散化以后,起到了简化逻辑回归模型的作用,降低了模型过拟合的风险;
李沐曾经说过:模型是使用离散特征还是连续特征,其实是一个“海量离散特征+简单模型”同“少量连续特征+复杂模型”的权衡。

这里我写下我关于上面某些点的理解,有问题的欢迎指出:

  1. 假设目前有两个连续的特征:『年龄』和『收入』,预测用户的『魅力指数』;

第三点: LR是广义线性模型,因此如果特征『年龄』不做离散化直接输入,那么只能得到『年龄』和魅力指数的一个线性关系。但是这种线性关系是不准确的,并非年龄越大魅力指一定越大;如果将年龄划分为M段,则可以针对每段有一个对应的权重;这种分段的能力为模型带来类似『折线』的能力,也就是所谓的非线性 连续变量的划分,naive的可以通过人为先验知识划分,也可以通过训练单特征的决策树桩,根据Information Gain/Gini系数等来有监督的划分。 假如『年龄』离散化后,共有N段,『收入』离散化后有M段;此时这两个离散化后的特征类似于CategoryFeature,对他们进行OneHotEncode,即可以得到 M + N的 01向量;例如: 0 1 0 0, 1 0 0 0 0; 第四点: 特征交叉,可以理解为上述两个向量的互相作用,作用的方式可以例如是 &和|操作(这种交叉方式可以产生一个 M * N的01向量;) 上面特征交叉,可以类比于决策树的决策过程。例如进行&操作后,得到一个1,则可以认为产生一个特征 (a < age < b && c < income < d);将特征空间进行的非线性划分,也就是所谓的引入非线性;

答案二(周开拓):

机器学习里当然并没有free lunch,一个方法能work,必定是有假设的。如果这个假设和真实的问题及数据比较吻合,就能work。

对于LR这类的模型来说,假设基本如下:

  • 局部平坦性,或者说连续性。对于连续特征x来说,在任何一个取值x0的邻域附近,这个特征对预估目标y的影响也在一个足够小的邻域内变化。比如,人年龄对点击率的影响,x0=30岁假设会产生一定的影响,那么x=31或者29岁,这个影响和x0=30岁的影响差距不会太大;
  • x对y的影响,这个函数虽然局部比较平坦,但是不太规律,如果你知道这个影响是个严格的直线(或者你有先验知识知道这个影响一定可以近似于一个参数不太多的函数),显然也没必要去做离散化。当然这条基本对于绝大多数问题都是成立的,因为基本没有这种好事情。

假设一个最简单的问题,binary classification,y=0/1,x是个连续值。你希望学到一个logloss足够小的y=f(x)。

那么有一种做法就是,在数据轴上切若干段,每一段观察训练样本里y为1的比例,以这个比例作为该段上y=f(x)的值。这个当然不是LR训练的过程,但是就是离散化的思想。你可以发现:

  • 如果每一段里面都有足够多的样本,那么在这一段里的y=f(x)值的点估计就比较可信
  • 如果x在数轴上分布不太均匀,比如是指数分布或者周期分布的,这么做可能会有问题,因而你要先对x取个log,或者去掉周期性

这就告诉了你应该怎么做离散化: 尽可能保证每个分段里面有足够多的样本,尽量让样本的分布在数轴上均匀一些。

结语:本质上连续特征离散化,可以理解为连续信号怎么转化为数字信号,好比我们计算机画一条曲线,也是变成了画一系列线段的问题。用分段函数来表达一个连续的函数在大多数情况下,都是work的。想取得好的效果需要:

  • 你的分段足够小,以使得在每个分段内x对y的影响基本在一个不大的邻域内,或者你可以忍受这个变化的幅度;
  • 你的分段足够大,以使得在每个分段内有足够的样本,以获得可信的f(x)也就是权重;
  • 你的分段策略使得在每个x的分段中,样本的分布尽量均匀(当然这很难),一般会根据先验知识先对x做一些变化以使得变得均匀一些;
  • 如果你有非常强的x对y的先验知识,比如严格线性之类的,也未必做离散化,但是这种先验在计算广告或者推荐系统里一般是不存在的,也许其他领域比如CV之类的里面是可能存在的;

最后还有个特别大的LR用离散特征的好处就是LR的特征是并行的,每个特征是并行同权的,如果有异常值的情况下,如果这个异常值没见过,那么LR里因为没有这个值的权重,最后对score的贡献为0,最多效果不够好,但是不会错的太离谱。另外,如果你debug,很容易查出来是哪个段上的权重有问题,比较好定位和解决。

2、树模型为什么离散化?

Cart树的离散化:

分类:

  • 如果特征值是连续值:CART的处理思想与C4.5是相同的,即将连续特征值离散化。唯一不同的地方是度量的标准不一样, CART采用基尼指数,而C4.5采用信息增益比

  • 如果当前节点为连续属性,CART树中该属性(剩余的属性值)后面还可以参与子节点的产生选择过程

回归:

对于连续值的处理, CART 分类树采用基尼系数的大小来度量特征的各个划分点。在回归模型中, 我们使用常见的和方差度量方式, 对于任意划分特征 \(\mathrm{A}\), 对应的任意划分点 \(\mathrm{s}\) 两边划分成的数据集 \(D_1\)\(D_2\), 求出使 \(D_1\)\(D_2\) 各自集合的均方差最小, 同时 \(D_1\)\(D_2\) 的均方差之和最小所对应的特征和特征值划分点。表达式为: \[ \min _{a, s}\left[\min _{c_1} \sum_{x_i \in D_1}\left(y_i-c_1\right)^2+\min _{c_2} \sum_{x_i \in D_2}\left(y_i-c_2\right)^2\right] \] 其中, \(c_1\)\(D_1\) 数据集的样本输出均值, \(c_2\)\(D_2\) 数据集的样本输出均值。

LGB直方图算法优点:

内存小、复杂度降低、直方图加速【分裂、并行通信、缓存优化】

  • 内存消耗降低。预排序算法需要的内存约是训练数据的两倍(2x样本数x维度x4Bytes),它需要用32位浮点来保存特征值,并且对每一列特征,都需要一个额外的排好序的索引,这也需要32位的存储空间。对于 直方图算法,则只需要(1x样本数x维 度x1Bytes)的内存消耗,仅为预排序算法的1/8。因为直方图算法仅需要存储特征的 bin 值(离散化后的数值),不需要原始的特征值,也不用排序,而bin值用8位整型存储就足够了。

  • 算法时间复杂度大大降低。决策树算法在节点分裂时有两个主要操作组成,一个是“寻找分割点”,另一个是“数据分割”。从算法时间复杂度来看,在“寻找分割点”时,预排序算法对于深度为\(k\)的树的时间复杂度:对特征所有取值的排序为\(O(NlogN)\)\(N\)为样本点数目,若有\(D\)维特征,则\(O(kDNlogN)\),而直方图算法需要\(O(kD \times bin)\) (bin是histogram 的横轴的数量,一般远小于样本数量\(N\))。

  • 直方图算法还可以进一步加速两个维度】。一个容易观察到的现象:一个叶子节点的直方图可以直接由父节点的直方图和兄弟节点的直方图做差得到(分裂时左右集合)。通常构造直方图,需要遍历该叶子上的所有数据,但直方图做差仅需遍历直方图的\(k\)个bin。利用这个方法,LightGBM可以在构造一个叶子的直方图后,可以用非常微小的代价得到它兄弟叶子的直方图,在速度上可以提升一倍。

  • 数据并行优化,用 histgoram 可以大幅降低通信代价。用 pre-sorted 算法的话,通信代价是非常大的(几乎是没办法用的)。所以 xgoobst 在并行的时候也使用 histogram 进行通信。

  • 缓存优化:上边说到 XGBoost 的预排序后的特征是通过索引给出的样本梯度的统计值,因其索引访问的结果并不连续,XGBoost 提出缓存访问优化算法进行改进。 LightGBM 所使用直方图算法对 Cache 天生友好所有的特征都采用相同的方法获得梯度,构建直方图时bins字典同步记录一阶导、二阶导和个数,大大提高了缓存命中;因为不需要存储特征到样本的索引,降低了存储消耗,而且也不存在 Cache Miss的问题。

3、归一化?

参考文献

  • 机器学习中的特征工程(四)---- 特征离散化处理方法:https://www.jianshu.com/p/918649ce379a

  • 机器学习中的特征工程(三)---- 序数和类别特征处理方法:https://www.jianshu.com/p/3d828de72cd4

  • 机器学习中的特征工程(二)---- 数值类型数据处理:https://www.jianshu.com/p/b0cc0710ef55

  • 机器学习中的特征工程(一)---- 概览:https://www.jianshu.com/p/172677f4ea4c

  • 特征工程完全手册 - 从预处理、构造、选择、降维、不平衡处理,到放弃:https://zhuanlan.zhihu.com/p/94994902

  • 这9个特征工程使用技巧,解决90%机器学习问题! - Python与数据挖掘的文章 - 知乎 https://zhuanlan.zhihu.com/p/462744763

特征选择

【机器学习】特征选择(Feature Selection)方法汇总

特征选择方法全面总结

特征选择的基本方法总结

训练数据包含许多冗余或无用的特征,移除这些特征并不会导致丢失信息。其中冗余是指一个本身很有用的特征与另外一个有用的特征强相关,或它包含的信息能从其它特征推演出来; 特征很多但样本相对较少。

  • 产生过程:产生特征或特征子集候选集合;

  • 评价函数:衡量特征或特征子集的重要性或者好坏程度,即量化特征变量和目标变量之间的联系以及特征之间的相互联系。为了避免过拟合,可用交叉验证的方式来评估特征的好坏;

  • 停止准则:为了减少计算复杂度,需设定一个阈值,当评价函数值达到阈值后搜索停止

  • 验证过程:在验证数据集上验证选出来的特征子集的有效性

一、特征选择的目的

1.简化模型,使模型更易于理解:去除不相关的特征会降低学习任务的难度。并且可解释性能对模型效果的稳定性有更多的把握

2.改善性能:节省存储和计算开销

3.改善通用性、降低过拟合风险:减轻维数灾难,特征的增多会大大增加模型的搜索空间,大多数模型所需要的训练样本随着特征数量的增加而显著增加。特征的增加虽然能更好地拟合训练数据,但也可能增加方差

二、特征选择常见方法

  • Filter(过滤法)
    • 覆盖率
    • 方差选择
    • Pearson(皮尔森)相关系数
    • 卡方检验
    • 互信息法(KL散度、相对熵)和最大信息系数
    • Fisher得分
    • 相关特征选择
    • 最小冗余最大相关性
  • Wrapper(包装法)
    • 完全搜索
    • 启发搜索
    • 随机搜索
  • Embedded(嵌入法)
    • L1 正则项
    • 树模型选择
    • 不重要性特征选择

三、Filter(过滤法) 【特征集】

img
定义
  • 过滤法的思想就是不依赖模型,仅从特征的角度来做特征的筛选,具体又可以分为两种方法,一种是根据特征里面包含的信息量,如方差选择法,如果一列特征的方差很小,每个样本的取值都一样的话,说明这个特征的作用不大,可以直接剔除。另一种是对每一个特征,都计算关于目标特征的相关度,然后根据这个相关度来筛选特征,只保留高于某个阈值的特征,这里根据相关度的计算方式不同就可以衍生出一下很多种方法。
分类
  • 单变量过滤方法:不需要考虑特征之间的相互关系,按照特征变量和目标变量之间的相关性或互信息对特征进行排序,过滤掉最不相关的特征变量。优点是计算效率高、不易过拟合。
  • 多变量过滤方法:考虑特征之间的相互关系,常用方法有基于相关性和一致性的特征选择
覆盖率
  • 即特征在训练集中出现的比例。若覆盖率很小,如有10000个样本,但某个特征只出现了5次,则次覆盖率对模型的预测作用不大,可删除
(1)方差选择法
  • 先计算各个特征的方差,然后根据阈值,选择方差大于阈值的特征
1
2
3
4
from sklearn.feature_selection import VarianceThreshold
# 方差选择法,返回值为特征选择后的数据
# 参数threshold为方差的阈值
VarianceThreshold(threshold=3).fit_transform(iris.data)
(2)Pearson皮尔森相关系数

用于度量两个变量X和Y之间的线性相关性

  • 用于度量两个变量X和Y之间的线性相关性,结果的取值区间为[-1, 1], -1表示完全的负相关(这个变量下降,那个就会上升),+1表示完全的正相关,0表示没有线性相关性
  • 计算方法为两个变量之间的协方差标准差的商
1
2
3
4
5
6
7
8
9
from sklearn.feature_selection import SelectKBest
from scipy.stats import pearsonr
# 选择K个最好的特征,返回选择特征后的数据
# 第一个参数为计算评估特征是否好的函数,该函数输入特征矩阵和目标向量,
# 输出二元组(评分,P值)的数组,数组第i项为第i个特征的评分和P值。
# 在此为计算相关系数
# 其中参数k为选择的特征个数
SelectKBest(lambda X, Y: array(map(lambda x:pearsonr(x, Y), X.T)).T,
k=2).fit_transform(iris.data, iris.target)
(3)卡方检验

自变量对因变量的相关性

检验定性自变量对定性因变量的相关性。假设自变量有N种取值,因变量有M种取值,考虑自变量等于i且因变量等于j的样本频数的观察值与期望的差距,构建统计量: \[ \chi^2=\sum \frac{(A-E)^2}{E} \]

1
2
3
4
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
#选择K个最好的特征,返回选择特征后的数据
SelectKBest(chi2, k=2).fit_transform(iris.data, iris.target)
(4)PSI互信息法(KL散度、相对熵)和最大信息系数 Mutual information and maximal information coefficient (MIC)

风控模型—群体稳定性指标(PSI)深入理解应用:https://zhuanlan.zhihu.com/p/79682292

评价定性自变量对定性因变量的相关性,评价类别型变量对类别型变量的相关性,互信息越大表明两个变量相关性越高,互信息为0时,两个变量相互独立。互信息的计算公式为 \[ I(X ; Y)=\sum_{x \in X} \sum_{y \in Y} p(x, y) \log \frac{p(x, y)}{p(x) p(y)}=D_{K L}(p(x, y) \| p(x) p(y)) \]

(5)Fisher得分

对于分类问题, 好的特征应该是在同一个类别中的取值比较相似, 而在不同类别之间的取值差异比较大。因此特征 \(\mathrm{i}\) 的重要性可用Fiser得分 \(S_i\) 来表示 \[ S_i=\frac{\sum_{j=1}^K n_j\left(\mu_{i j}-\mu_i\right)^2}{\sum_{j=1}^K n_j \rho_{i j}^2} \] 其中, \(u_{i j}\)\(\rho_{i j}\) 分别是特征i在类别 \(j\) 中均值和方差, \(\mu_i\) 为特征i的均值, \(n_j\) 为类别中中的样本数。Fisher得分越高, 特征在不同类别之间的差异性越大、在同一类别中的差异性越小,则特征越重要;

(6)相关特征选择

该方法基于的假设是,好的特征集合包含跟目标变量非常相关的特征,但这些特征之间彼此不相关

(7)最小冗余最大相关性( mRMR)

由于单变量过滤法只考虑了单特征变量和目标变量之间的相关性,因此选择的特征子集可能过于冗余。mRMR在进行特征时考虑到了特征之间的冗余性,具体做法是对跟已选择特征相关性较高的冗余特征进行惩罚;

四、Wrapper(包装法) 【特征集+模型】

定义

  • 使用机器学习算法评估特征子集的效果,可以检测两个或多个特征之间的交互关系,而且选择的特征子集让模型的效果达到最优。

  • 这是特征子集搜索评估指标相结合的方法。前者提供候选的新特征子集,后者基于新特征子集训练一个模型,并用验证集进行评估,为每一组特征子集进行打分。

  • 最简单的方法是在每一个特征子集上训练并评估模型,从而找出最优的特征子集

缺点:

  • 需要对每一组特征子集训练一个模型,计算量很大
  • 样本不够充分的情况下容易过拟合
  • 特征变量较多时计算复杂度太高
(1)完全搜索

即穷举法, 遍历所有可能的组合达到全局最优, 时间复杂度 \(2^n\)

(2)启发式搜索

序列向前选择: 特征子集从空集开始, 每次只加入一个特征, 时间复杂度为 \(O(n+(n-1)+(n-2)+\ldots+1)=O\left(n^2\right)\)

序列向后选择: 特征子集从全集开始, 每次删除一个特征, 时间复杂度为 \(O\left(n^2\right)\)

(3)随机搜索

执行序列向前或向后选择时,随机选择特征子集

(4)递归特征消除法

使用一个基模型进行多轮训练,每轮训练后通过学习器返回的coef_或者feature_importances_消除若干权重较低的特征,再基于新的特征集进行下一轮训练。

1
2
3
4
5
6
7
8
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
#递归特征消除法,返回特征选择后的数据
#参数estimator为基模型
#参数n_features_to_select为选择的特征个数
RFE(estimator=LogisticRegression(),
n_features_to_select=2).fit_transform(iris.data,
iris.target)

五、Embedded(嵌入法)

将特征选择嵌入到模型的构建过程中,具有包装法与机器学习算法相结合的优点,也具有过滤法计算效率高的优点

(1)LASSO方法 L1正则项

通过对回归系数添加惩罚项来防止过拟合,可以让特定的回归系数变为0,从而可以选择一个不包含那些系数的更简单的模型;实际上,L1惩罚项降维的原理是,在多个对实际上,L1惩罚项降维的原理是,在多个对目标值具有同等相关性的特征中,只保留一个,所以没保留的特征并不代表不重要具有同等相关性的特征中,只保留一个,所以没保留的特征并不代表不重要。

1
2
3
4
5
6
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
#带L1惩罚项的逻辑回归作为基模型的特征选择
SelectFromModel(LogisticRegression(
penalty="l1", C=0.1)).fit_transform(
iris.data,iris.target)
(2)基于树模型的特征选择方法
  • 在决策树中,深度较浅的节点一般对应的特征分类能力更强(可以将更多的样本区分开)
  • 对于基于决策树的算法,如随机森林,重要的特征更有可能出现在深度较浅的节点,而且出现的次数可能越多
  • 即可基于树模型中特征出现次数等指标对特征进行重要性排序
1
2
3
4
5
6
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import GradientBoostingClassifier
#GBDT作为基模型的特征选择
SelectFromModel(
GradientBoostingClassifier()).fit_transform(
iris.data,iris.target)
(3)使用特征重要性来筛选特征的缺陷?
  • 特征重要性只能说明哪些特征在训练时起到作用了,并不能说明特征和目标变量之间一定存在依赖关系。举例来说,随机生成一大堆没用的特征,然后用这些特征来训练模型,一样可以得到特征重要性,但是这个特征重要性并不会全是0,这是完全没有意义的。
  • 特征重要性容易高估数值特征和基数高的类别特征的重要性。这个道理很简单,特征重要度是根据决策树分裂前后节点的不纯度的减少量(基尼系数或者MSE)来算的,那么对于数值特征或者基础高的类别特征,不纯度较少相对来说会比较多。
  • 特征重要度在选择特征时需要决定阈值,要保留多少特征、删去多少特征,这些需要人为决定,并且删掉这些特征后模型的效果也不一定会提升。
(4)Non importance 选择

不平衡数据问题

实际上,很多时候,数据不平衡并没有啥负面影响,并不是数据不平衡了,就一定要处理。如果你只是为了做而做,我有99%的自信告诉你,你做了也是白做,啥收益都没有。

为什么很多模型在训练数据不均衡时会出问题?

本质原因是模型在训练时优化的目标函数和在测试时使用的评价标准不一致。这种”不一致“可能是训练数据的样本分布与测试数据分布不一致;

一、不平衡数据集的主要处理方式?

1.1 数据的角度

主要方法为采样,分为欠采样过采样以及对应的一些改进方法。[python imblearn库]尊重真实样本分布,人为主观引入样本权重,反而可能得出错误的结论。

业务角度
  • 时间因素,对近期样本提高权重,较远样本降低权重。这是考虑近期样本与未来样本之间的“相似度”更高,希望模型学到更多近期样本的模式。
  • 贷款类型,不同额度、利率、期限的样本赋予不同权重,这需要结合业务未来的发展方向。例如,未来业务模式希望是小额、短期、低利率,那就提高这批样本的权重。
  • 样本分群,不同群体赋予不同权重。例如,按流量获客渠道,如果未来流量渠道主要来自平台A,那么就提高这批样本权重。
技术角度:
  • 欠采样

    • EasyEnsemble:从多数类\(S_{max}\)上随机抽取一个子集,与其他类训练一个分类器;重复若干次,多个分类器融合。

    • BalanceCascade:从多数类\(S_{max}\)上随机抽取一个子集,与其他类训练一个分类器;剔除能被分类正确的分类器,重复若干次,多个分类器融合。

    • NearMIss:利用K邻近信息挑选具有代表性的样本。

    • one-side Selection:采用数据清洗技术。

  • 过采样

    • 随机采样

    • SMOTE算法:对少数类\(S_{min}\)中每个样本x的K近邻随机选取一个样本y,在x,y的连线上随机选取一个点作为新的样本点。

    • Borderline-SMOTE、ADASYN改进算法等

  • 分层抽样技术:批量训练分类器的「分层抽样」技术。当面对不平衡类问题时,这种技术(通过消除批次内的比例差异)可使训练过程更加稳定。

1.2 算法的角度

考虑不同误分类情况代价的差异性对算法进行优化,主要是基于代价敏感学习算法(Cost-Sensitive Learning),代表的算法有adacost实现基于代价敏感的AdaCost算法

  • 代价函数:可以增加小类样本的权值,降低大类样本的权值(这种方法其实是产生了新的数据分布,即产生了新的数据集),从而使得分类器将重点集中在小类样本身上。刚开始,可以设置每个类别的权值与样本个数比例的倒数,然后可以使用过采样进行调优。

    这种方法的难点在于设置合理的权重,实际应用中一般让各个分类间的加权损失值近似相等。当然这并不是通用法则,还是需要具体问题具体分析。

  • XGB自定义损失函数 /Imbalance-XGBoost【Focal Loss】:

\[ L_w=-\sum_{i=1}^m \hat{y}_i\left(1-y_i\right)^\gamma \log \left(y_i\right)+\left(1-\hat{y}_i\right) y_i^\gamma \log \left(1-y_i\right) \]

1.3 分类方式

可以把小类样本作为异常点(outliers),把问题转化为异常点检测问题(anomaly detection)。此时分类器需要学习到大类的决策分界面,即分类器是一个单个类分类器(One Class Classifier)。代表的算法有 One-class SVM

一类分类算法:

不平衡数据集的一类分类算法:https://machinelearningmastery.com/one-class-classification-algorithms/

一类分类是机器学习的一个领域,它提供了异常值和异常检测的技术,如何使一类分类算法适应具有严重偏斜类分布的不平衡分类,如何拟合和评估 SVM、隔离森林、椭圆包络、局部异常因子等一类分类算法。

不平数据集的划分方法?
  • K折交叉验证?

  • 自助法?

不平数据集的评价方法?

G-Mean和ROC曲线和AUC。Topk@P

  • AP衡量的是学出来的模型在每个类别上的好坏,mAP衡量的是学出的模型在所有类别上的好坏,得到AP后mAP的计算就变得很简单了,就是取所有AP的平均值。

二、「类别不平衡」如何得到一个不错的分类器?

微调如何处理数据中的「类别不平衡」?

机器学习中常常会遇到数据的类别不平衡(class imbalance),也叫数据偏斜(class skew)。以常见的二分类问题为例,我们希望预测病人是否得了某种罕见疾病。但在历史数据中,阳性的比例可能很低(如百分之0.1)。在这种情况下,学习出好的分类器是很难的,而且在这种情况下得到结论往往也是很具迷惑性的。

以上面提到的场景来说,如果我们的分类器总是预测一个人未患病,即预测为反例,那么我们依然有高达99.9%的预测准确率。然而这种结果是没有意义的,这提出了今天的第一个问题,如何有效在类别不平衡的情况下评估分类器?

当然,本文最终希望解决的问题是:在数据偏斜的情况下,如何得到一个不错的分类器?如果可能,是否可以找到一个较为简单的解决方法,而规避复杂的模型、数据处理,降低我们的工作量。

2.1 类别不平衡下的评估问题?

当类别不平衡时,准确率就非常具有迷惑性,而且意义不大。给出几种主流的评估方法:

  • ROC是一种常见的替代方法,全名receiver operating curve,计算ROC曲线下的面积是一种主流方法
  • Precision-recall curve和ROC有相似的地方,但定义不同,计算此曲线下的面积也是一种方法
  • Precision@n是另一种方法,特制将分类阈值设定得到恰好n个正例时分类器的precision
  • Average precision也叫做平均精度,主要描述了precision的一般表现,在异常检测中有时候会用
  • 直接使用Precision也是一种想法,但此时的假设是分类器的阈值是0.5,因此意义不大

本文的目的不是介绍一般的分类评估标准,简单的科普可以参看:如何解释召回率与准确率?

2.2 解决类别不平衡中的“奇淫巧技”有什么?

对于类别不平衡的研究已经有很多年了,在资料[1]中就介绍了很多比较复杂的技巧。结合我的了解举几个简单的例子:

[1] He, H. and Garcia, E.A., 2009. Learning from imbalanced data. IEEE Transactions on knowledge and data engineering, 21(9), pp.1263-1284.

  • 对数据进行采用的过程中通过相似性同时生成并插样“少数类别数据”,叫做SMOTE算法
  • 对数据先进行聚类,再将大的簇进行随机欠采样或者小的簇进行数据生成
  • 把监督学习变为无监督学习,舍弃掉标签把问题转化为一个无监督问题,如异常检测
  • 先对多数类别进行随机的欠采样,并结合boosting算法进行集成学习

2.3 简单通用的算法有哪些?

  • 对较多的那个类别进行欠采样(under-sampling),舍弃一部分数据,使其与较少类别的数据相当
  • 对较少的类别进行过采样(over-sampling),重复使用一部分数据,使其与较多类别的数据相当
  • 阈值调整(threshold moving),将原本默认为0.5的阈值调整到 较少类别/(较少类别+较多类别)即可

当然很明显我们可以看出,第一种和第二种方法都会明显的改变数据分布,我们的训练数据假设不再是真实数据的无偏表述。在第一种方法中,我们浪费了很多数据。而第二类方法中有无中生有或者重复使用了数据,会导致过拟合的发生。

因此欠采样的逻辑中往往会结合集成学习来有效的使用数据,假设正例数据n,而反例数据m个。我们可以通过欠采样,随机无重复的生成(k=n/m)个反例子集,并将每个子集都与相同正例数据合并生成k个新的训练样本。我们在k个训练样本上分别训练一个分类器,最终将k个分类器的结果结合起来,比如求平均值。这就是一个简单的思路,也就是Easy Ensemble [5]。

但不难看出,其实这样的过程是需要花时间处理数据和编程的,对于很多知识和能力有限的人来说难度比较大。特此推荐两个简单易行且效果中上的做法:

  • 简单的调整阈值,不对数据进行任何处理。此处特指将分类阈值从0.5调整到正例比例
  • 使用现有的集成学习分类器,如随机森林或者xgboost,并调整分类阈值

提出这样建议的原因有很多。首先,简单的阈值调整从经验上看往往比过采样和欠采样有效 [6]。其次,如果你对统计学知识掌握有限,而且编程能力一般,在集成过程中更容易出错,还不如使用现有的集成学习并调整分类阈值。

2.4 一个简单但有效的方案

经过了上文的分析,我认为一个比较靠谱的解决方案是:

  • 不对数据进行过采样和欠采样,但使用现有的集成学习模型,如随机森林
  • 输出随机森林的预测概率,调整阈值得到最终结果
  • 选择合适的评估标准,如precision@n

三、脉脉:数据集不平衡应该思考什么

首先, 猜测一下, 你研究的数据存在着较 大的不平衡, 你还是比较关注正类(少数类) 样本的, 比如【想要识别出 有信用风险 的 人】那么就要谈一下你所说的【模型指标还行】这个问题。auc这种复合指标先不提, precision代表的是, 你预测的信用风险人群, 其中有多少是真的信用风险人群。recall 代表的是, "真的信用风险人群"有多少被你识别出来了

  • 所以, 倘若你比较关注的是【我想要找出 所有"可能有违约风险的人"】宁可错杀也不 放过。那么你应该重点关注的就是召回率 recall。在此基础上, 尽量提高precision。
  • 你把训练集的正负样本控制在64左右, 那 么你是怎么控制的呢, 是单纯用了数据清理技术, 还是单纯生成了一些新的样本, 还是怎么做的?
  • 如果条件允许, 可以查看一下你被错分的 样本, 看看被错分的原因可能是什么, 是因为类重叠, 还是有少数类的分离还是单纯的因为不平衡比太夸张所以使分类器产生偏倚?
  • 不知道你用的什么模型, 但是现在有一些把重采样和分类器结合在一起的集成学习方法, 可以试试看。
  • 维度太高的时候, 特征的提取很重要呀!
  • 当做异常检测问题可能会好一些?

四、样本准备与权重指标

样本权重对逻辑回归评分卡的影响探讨: https://zhuanlan.zhihu.com/p/110982479

风控业务背景

在统计学习建模分析中,样本非常重要,它是我们洞察世界的窗口。在建立逻辑回归评分卡时,我们也会考虑对样本赋予不同的权重weight,希望模型学习更有侧重点。

诚然,我们可以通过实际数据测试来检验赋权前后的差异,但我们更希望从理论上分析其合理性。毕竟理论可以指导实践。本文尝试探讨样本权重对逻辑回归评分卡的影响,以及从业务和技术角度分析样本权重调整的操作方法。

Part 1. 样本加权对WOE的影响

WOE与IV指标的深入理解应用一文中, 我们介绍了WOE的概念和计算方法。在逻辑回归评分卡中, 其具有 重要意义。其公式定义如下:

\[ W_O=\ln \left(\frac{\text { Good }_i}{\text { Good }_T} / \frac{\text { Bad }_i}{\operatorname{Bad}_T}\right)=\ln \left(\frac{\text { Good }_i}{\text { Bad }_i}\right)-\ln \left(\frac{\text { Good }_T}{\operatorname{Bad}_T}\right) \] 现在,我们思考在计算WOE时,是否要考虑样本权重呢?

如图1所示的样本, 我们希望对某些样本加强学习, 因此对年龄在46岁以下的样本赋予权重1.5, 而对46岁以上的样本赋予权重1.0, 也就是加上权重列weight。此时再计算WOE值, 我们发现数值发生变化。这是因为权重的改 变, 既影响了局部bucket中的 odds,也影响了整体的 odds 。

img我们有2种对样本赋权后的模型训练方案,如图2所示。

  • 方案1: WOE变换利用原训练集,LR模型训练时利用加权后的样本。
  • 方案2: WOE变换和LR模型训练时,均使用加权后的样本。
img

个人更倾向于第一种方案,原因在于:WOE变换侧重变量的可解释性,引入样本权重会引起不可解释的困扰。

Part 2. 采样对LR系数的影响

我们定义特征向量 \(\mathbf{x}=x_1, x_2, \ldots, x_n\) 。记 \(G=\operatorname{Good}, B=B a d\), 那么逻辑回归的公式组成便是: \[ \begin{aligned} & \operatorname{Ln}(\text { odds }(G \mid \mathbf{x}))=\operatorname{Ln}\left(\frac{p_G f(\mathbf{x} \mid G)}{p_B f(\mathbf{x} \mid B)}\right)=\operatorname{Ln}\left(\frac{p_G}{p_B}\right)+\operatorname{Ln}\left(\frac{f(\mathbf{x} \mid G)}{f(\mathbf{x} \mid B)}\right) \\ & =\operatorname{Ln}\left(\frac{p_G}{p_B}\right)+\operatorname{Ln}\left(\frac{f\left(x_1, x_2, \ldots, x_n \mid G\right)}{f\left(x_1, x_2, \ldots, x_n \mid B\right)}\right) \\ & =\operatorname{Ln}\left(\frac{p_G}{p_B}\right)+\operatorname{Ln}\left(\frac{f\left(x_1 \mid G\right)}{f\left(x_1 \mid B\right)}\right)+\operatorname{Ln}\left(\frac{f\left(x_2 \mid G\right)}{f\left(x_2 \mid B\right)}\right)+\ldots+\operatorname{Ln}\left(\frac{f\left(x_n \mid G\right)}{f\left(x_n \mid B\right)}\right) \\ & =L n\left(\text { odd } s_{\text {pop }}\right)+\operatorname{Ln}\left(\text { odd } s_{i n f o}(\mathbf{x})\right) \\ & \end{aligned} \] 其中, 第2行到第3行的变换是基于朴素贝叶斯假设, 即自变量 \(x_i\) 之间相互独立。

  • \(o d d s_{p o p}\) 是指总体 (训练集) 的 \(o d d s\), 指先验信息 \(o d d s\)
  • \(o d d s_{i n f o}(\mathbf{x})\) 是指自变量引起的 \(o d d s\) 变化, 我们称为后验信息 \(o d d s 。\)

因此,随着观察信息的不断加入,对群体的好坏 \(o d d s\) 判断将越来越趋于客观。

img
样本权重调整直接影响先验项,也就是截距。那对系数的影响呢?

接下来,我们以过采样(Oversampling)和欠采样(Undersampling)为例,分析采样对LR系数的影响。如图4所示,对于不平衡数据集,过采样是指对正样本简单复制很多份;欠采样是指对负样本随机抽样。最终,正负样本的比例将达到1:1平衡状态。

img

我们同样从贝叶斯角度进行解释: \[ \begin{gathered} P(B \mid \mathbf{x})=\frac{P(\mathbf{x} \mid B) P(B)}{P(\mathbf{x})}=\frac{P(\mathbf{x} \mid B) P(B)}{P(\mathbf{x} \mid B) P(B)+P(\mathbf{x} \mid G) P(G)} \\ \Leftrightarrow \frac{1}{P(B \mid \mathbf{x})}=1+\frac{P(\mathbf{x} \mid G) P(G)}{P(\mathbf{x} \mid B) P(B)} \\ \Leftrightarrow \operatorname{Ln}\left(\frac{P(\mathbf{x} \mid G)}{P(\mathbf{x} \mid B)}\right)=\operatorname{Ln}\left(\frac{1}{P(B \mid \mathbf{x})}-1\right)-\operatorname{Ln}\left(\frac{P(G)}{P(B)}\right) \\ \Leftrightarrow \operatorname{Ln}\left(\frac{P(G \mid \mathbf{x})}{P(B \mid \mathbf{x})}\right)=\operatorname{Ln}\left(\frac{P(\mathbf{x} \mid G)}{P(\mathbf{x} \mid B)}\right)+\operatorname{Ln}\left(\frac{P(G)}{P(B)}\right) \end{gathered} \] 假设采样处理后的训练集为 \(\mathbf{x}^{\prime}\) 。记 \(\# B\)\(\# G\) 分别表示正负样本数, 那么显然: \[ o d d s=\frac{\# G}{\# B} \neq \frac{\# G^{\prime}}{\# B^{\prime}}=o d d s^{\prime} \] 由于 \(\operatorname{Ln}\left(\frac{P(G)}{P(B)}\right)=\operatorname{Ln}\left(\frac{\# G}{\# B}\right)\) ,因此对应截距将发生变化。

无论是过采样, 还是欠采样, 处理后的新样本都和原样本服从同样的分布, 即满足: \[ \begin{aligned} & P(\mathbf{x} \mid G)=P\left(\mathbf{x}^{\prime} \mid G^{\prime}\right) \\ & P(\mathbf{x} \mid B)=P\left(\mathbf{x}^{\prime} \mid B^{\prime}\right) \end{aligned} \] 因此, \(\operatorname{Ln}\left(\frac{P(\mathbf{x} \mid G)}{P(\mathbf{x} \mid B)}\right)=\operatorname{Ln}\left(\frac{P\left(\mathbf{x}^{\prime} \mid G^{\prime}\right)}{P\left(\mathbf{x}^{\prime} \mid B^{\prime}\right)}\right)\), 即系数不发生变化。

实践证明,按照做评分卡的方式,做WOE变换,然后跑LR,单变量下确实只有截距影响。而对于多变量,理想情况下,当各自变量相互独立时,LR的系数是不变的,但实际自变量之间多少存在一定的相关性,所以还是会有一定的变化。

Part 3. 样本准备与权重指标

风控建模的基本假设是末来样本和历史样本的分布是一致的。模型从历史样本中拟合 \(X_{\text {old }}\)\(y\) 之间的关系,并 根据末来样本的 \(X_{\text {new }}\) 进行预测。因此, 我们总是在思考, 如何选择能代表末来样本的训练样本。

如图5所示,不同时间段、不同批次的样本总是存在差异,即服从不同的总体分布。因此,我们需要从多个维度来衡量两个样本集之间的相似性。

从迁移学习的角度看,这是一个从源域(source domain)中学习模式,并应用到目标域(target domain)的过程。在这里,源域是训练集,目标域指测试集,或者未来样本。

这就会涉及到一些难点:

  • 假设测试集OOT与未来总体分布样本基本一致,但未来样本是不可知且总是在发生变化。
  • 面向测试集效果作为评估指标,会出现在测试集上过拟合现象。 img

那么,建模中是否可以考虑建立一个权重指标体系,即综合多方面因素进行样本赋权?我们采取2种思路来分析如何开展。

业务角度

  1. 时间因素,对近期样本提高权重,较远样本降低权重。这是考虑近期样本与未来样本之间的“相似度”更高,希望模型学到更多近期样本的模式。
  2. 贷款类型,不同额度、利率、期限的样本赋予不同权重,这需要结合业务未来的发展方向。例如,未来业务模式希望是小额、短期、低利率,那就提高这批样本的权重。
  3. 样本分群,不同群体赋予不同权重。例如,按流量获客渠道,如果未来流量渠道主要来自平台A,那么就提高这批样本权重。

结合以上各维度,可得到总体采样权重的一种融合方式为: \[ w=w_1 * w_2 * w_3 \] 这种业务角度的方案虽然解释性强,但实际拍定多大的权重显得非常主观,实践中往往需要不断尝试,缺少一些理论指导。

技术角度:

  1. 过采样、欠采样等,从样本组成上调整正负不平衡
  2. 代价敏感学习,在损失函数对好坏样本加上不同的代价。比如,坏样本少,分错代价更高。
  3. 借鉴Adaboost的做法,对误判样本在下一轮训练时提高权重。

在机器学习中,有一个常见的现象——Covariate Shift,是指当训练集的样本分布和测试集的样本分布不一致的时候,训练得到的模型无法具有很好的泛化 (Generalization) 能力。

其中一种做法,既然是希望让训练集尽可能像测试集,那就让模型帮助我们做这件事。如图6所示,将测试集标记为1,训练集标记为0,训练一个LR模型,在训练集上预测,概率越高,说明这个样例属于测试集的可能性越大。以此达到样本权重调整的目的。

img

Part 4. 常见工具包的样本赋权

现有Logistic Regression模块主要来自sklearn和scipy两个包。很不幸,scipy包并不支持直接赋予权重列。这是为什么呢?有统计学家认为, 尊重真实样本分布,人为主观引入样本权重,反而可能得出错误的结论。

img

因此,我们只能选择用scikit-learn。样本权重是如何体现在模型训练过程呢?查看源码后,发现目前主要是体现在损失函数中,即代价敏感学习。

1
2
3
# Logistic loss is the negative of the log of the logistic function.
# 添加L2正则项的逻辑回归对数损失函数
out = -np.sum(sample_weight * log_logistic(yz)) + .5 * alpha * np.dot(w, w)

样本权重对决策分割面的影响:

img

以下是scikit-learn包中的逻辑回归参数列表说明,可以发现调节样本权重的方法有两种:

  • 在class_weight参数中使用balanced
  • 在调用fit函数时,通过sample_weight调节每个样本权重。

如果同时设置上述2个参数,那么样本的真正权重是class_weight * sample_weight.

那么,在评估模型的指标时,是否需要考虑抽样权重,即还原真实场景下的模型评价指标?笔者认为,最终评估还是需要还原到真实场景下。例如,训练集正负比例被调节为1:1,但这并不是真实的\(odds\),在预测时将会偏高。因此,仍需要进行模型校准。

Part 5. 总结

本文系统整理了样本权重的一些观点,但目前仍然没有统一的答案。据笔者所知,目前在实践中还是采取以下几种方案:

  1. 尊重原样本分布,不予处理,LR模型训练后即为真实概率估计。
  2. 结合权重指标综合确定权重,训练完毕模型后再进行校准,还原至真实概率估计。

值得指出的是,大环境总是在发生变化,造成样本分布总在偏移。因此,尽可能增强模型的鲁棒性,以及策略使用时根据实际情况灵活调整,两者相辅相成,可能是最佳的使用方法。欢迎大家一起讨论业界的一些做法。

参考文献

  • 机器学习中不平衡数据的预处理:https://capallen.gitee.io/2019/Deal-with-imbalanced-data-in-ML.html
  • 如何处理数据中的「类别不平衡」?:https://zhuanlan.zhihu.com/p/32940093
  • 不平衡数据集处理方法:https://blog.csdn.net/asialee_bird/article/details/83714612
  • 不平衡数据究竟出了什么问题?:https://www.zhihu.com/column/jiqizhixin
  • 数据挖掘时,当正负样本不均,代码如何实现改变正负样本权重? - 十三的回答 - 知乎 https://www.zhihu.com/question/356640889/answer/2299286791
  • 样本权重对逻辑回归评分卡的影响探讨 - 求是汪在路上的文章 - 知乎 https://zhuanlan.zhihu.com/p/110982479