2023年4月


列举远程机器开放的端口

nmap [域名]
列举远程机器开放的端口和服务
nmap --dns-servers 8.8.8.8 [域名]
nmap进行探测之前要把域名通过DNS服务器解析为IP地址,我们也可以使用指定的DNS服务器进行解析
nmap -Pn
[域名]
对于已经知道主机存活或者防火墙开启的机器,可以使用-Pn参数来停止探测之前的ICMP请求,以达到不触发防火墙安全机制。
nmap -p m-n [域名]
指定探测端口范围
nmap扫描技术查看
nmap -h

识别服务指纹

nmap -sV IP
识别目标机器的服务信息
namp -A -v -T4 IP 侵略性探测,来探测目标机器的操作系统、服务等信息。 -A:侵略性 -v:持续输出 -T4:指定速度(1-5)
nmap -sC -sV -O IP探测目标机器的操作系统、服务等信息。
其中sC 参数表示使用Nmap脚本进行探测,sV表示探测目标机器上的服务信息,O表示探测目标机器的操作系统信息。-sC: equivalent to --script=default

发现局域网中存活主机

nmap -sP CIDR/24
对该网络中所有主机进行ping扫描,以探测主机存活性。扫描过程中使用TCP SYN扫描、ICMP echo Request来探测主机存活。

nmap -sn IP
不进行端口扫描

nmap -sT 127.0.0.1
对目标主机进行TCP端口扫描 -常规扫描方式
nmap -sS 127.0.0.1
-使用SYN半扫描方式

nmap -sn CIDR -oX test.xml
结果输出

端口探测技巧

nmap -p80 [域名]
对某个端口进行探测

nmap -p80,135 [域名]
对某几个端口进行探测

nmap -p1-100 [域名]
对某个端口范围进行探测

nmap -p- [域名]
对所有端口范围进行探测

nmap -p T:25,U:53 [域名] 指定协议探测端口

nmap -p smtp [域名]
通过协议名来扫描端口

nmap -p s* [域名]
通过名称范围扫描

nmap -p
[1-65535] [域名]
扫描注册在nmap中的端口

NSE脚本使用

NSE (Nmap Script Engine) Nmap脚本引擎,内置很多可以用来扫描的、针对特定任务的脚本。通过NSE可以不断拓展Nmap的扫描策略,加强Nmap的功能。

Nmap中使用--script参数来指定调用的脚本,并且脚本存储在Nmap安装路径下的script文件夹下,对于kali Linux存储在/usr/ share/nmap/script/下。

nmap --script 脚本名称 目标
使用Nmap探测Web服务的title信息(http-title)

http-headers http头

NSE分类使用

对于目标使用多个分类脚本进行探测,可以更快的找到目标的信息与弱点。
使用Nmap中漏洞分类脚本对目标进行探测,使用命令如下:

nmap -sV -script vuln目标

使用Nmap中发现和版本信息分类进行探测,使用命令如下:- nmap -sV --script="version,discovery”目标

使用Nmap除了exploit分类之外的其他分类进行探测,使用命令如下:- nmap -sV --script="not exploit” 目标

使用Nmap中http
的脚本,但是除了(http-brute和http-slowlors),使用命令如下:- nmap - sV --script "(http
) and not (http-slowlors and http-brute)” 目标

NSE调试功能使用

使用Nmap中exploit,但是在使用的同时开启调试模式。使用命令如下:- nmap -sV --script exploit -d 3 --script-trace目标
注意:-d(debug范围0~9)

使用nmap的http-title脚本,并且指定使用对应的User-Agent。命令如下:
nmap -sV --script http-title --script-args http.useragent="Mozilla 999"
<target>

NSE 更新

nmap --script-updatedb

https: //nmap.org/nsedoc/ 参考文档

使用特定网卡进行探测

Nwap是一款可拓展性强的工具,并且有些NSE脚本支持嗅探。但是这种功能需要网卡支持混杂模式才司以。或者当计算机上有两张网卡,并且两张网卡对应的不同网络。

nmap -e interface CIDR

nmap --iflist
列出可用网卡列表

对比扫描结果ndiff

对某个网路进行探测的时候,有可能之前有探测过的结果,现在探测过后,需要对之前的结果与现在的结果来对比,找到两次不同点。
监视网络变化,达到网络监控的目的。

比较Nmap两次扫描结果
使用命令: ndiff.exe File1 File2 (File1和File2是Nmap扫描结果的xml格式。-oX)

可视化Nmap的使用

Nwmap命令参数复杂,组合类型多种多样,如果使用命令行类型的Nwmap需要记忆大量命令,对于网络管理员和渗透测试者都是一项艰巨的任务。但是如果使用可视化Nwap (zenmap),那么就不存在这样的问题。
在安装Nmap的同时,会自动安装可视化Nwap(Zenwmap),可以在安装目录中找到。

被动与主动侦察

在计算机系统和网络出现之前,孙子兵法在孙子兵法中教导说:“知己知彼,必胜不疑。” 如果您扮演攻击者的角色,则需要收集有关目标系统的信息。如果你扮演防御者的角色,你需要知道你的对手会发现你的系统和网络的什么

侦察(
recon
)可以定义为收集有关目标信息的初步调查。这是统一杀伤链在系统上获得初步立足点的第一步。我们将侦察分为:

  • 被动侦察
  • 主动侦察

在被动侦察中,您依赖于公开可用的知识。您无需直接与目标接触即可从公开可用资源中获取这些知识。把它想象成你从远处看着目标领土,而不是踏上那个领土

被动侦察活动包括许多活动,例如:

  • 从公共 DNS 服务器查找域的DNS记录
  • 检查与目标网站相关的招聘广告
  • 阅读有关目标公司的新闻文章

另一方面,主动侦察无法如此谨慎地实现。它需要与目标直接接触。把它想象成你检查门窗上的锁,以及其他潜在的入口点

主动侦察活动的例子包括:

  • 连接到公司服务器之一,例如
    HTTP

    FTP

    SMTP
  • 致电公司试图获取信息(
    社会工程学
  • 冒充修理工进入公司场所

考虑到主动侦察的侵入性,除非获得适当的法律授权,否则很快就会陷入法律困境

被动侦查

我们将学习三个命令行工具:

whois
查询
WHOIS
服务器
nslookup
查询
DNS
服务器
dig
查询
DNS
服务器
我们使用
whois
查询
WHOIS
记录,而我们使用
nslookup

dig
查询
DNS
数据库记录。这些都是公开可用的记录,因此不会提醒目标。

我们还将学习两种在线服务的用法:

  • DNSDumpster
  • Shodan.io

这两个在线服务使我们能够在不直接连接到目标的情况下收集有关目标的信息

Whois

WHOIS 是遵循RFC 3912规范的请求和响应协议。WHOIS 服务器在TCP端口 43 上侦听传入请求。域名注册商负责维护其租用域名的 WHOIS 记录。WHOIS 服务器回复与所请求域相关的各种信息。特别感兴趣的是,我们可以学习:

  • 注册商:域名是通过哪个注册商注册的?
  • 注册人联系方式:姓名、单位、地址、电话等。(除非通过隐私服务隐藏)
  • 创建、更新和到期日期:域名首次注册的时间是什么时候?最后一次更新是什么时候?什么时候需要更新?
  • 名称服务器:请求哪个服务器来解析域名?

要获取这些信息,我们需要使用
whois
客户端或在线服务。许多在线服务提供
whois
信息;但是,使用本地
whois
客户端通常更快、更方便

语法是
whois DOMAIN_NAME
,其中
DOMAIN_NAME
是您尝试获取更多信息的域。考虑以下示例执行
whois tryhackme.com

user@TryHackMe$ whois tryhackme.com
[Querying whois.verisign-grs.com]
[Redirected to whois.namecheap.com]
[Querying whois.namecheap.com]
[whois.namecheap.com]
Domain name: tryhackme.com
Registry Domain ID: 2282723194_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.namecheap.com
Registrar URL: http://www.namecheap.com
Updated Date: 2021-05-01T19:43:23.31Z
Creation Date: 2018-07-05T19:46:15.00Z
Registrar Registration Expiration Date: 2027-07-05T19:46:15.00Z
Registrar: NAMECHEAP INC
Registrar IANA ID: 1068
Registrar Abuse Contact Email: abuse@namecheap.com
Registrar Abuse Contact Phone: +1.6613102107
Reseller: NAMECHEAP INC
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Registry Registrant ID: 
Registrant Name: Withheld for Privacy Purposes
Registrant Organization: Privacy service provided by Withheld for Privacy ehf
[...]
URL of the ICANN WHOIS Data Problem Reporting System: http://wdprs.internic.net/
>>> Last update of WHOIS database: 2021-08-25T14:58:29.57Z <<<
For more information on Whois status codes, please visit https://icann.org/epp

我们可以看到很多信息;我们将按照显示的顺序检查它们。首先,我们注意到我们被重定向到
whois.namecheap.com
以获取我们的信息。在这种情况下,目前
namecheap.com
正在维护该域名的
WHOIS
记录。此外,我们可以看到创建日期以及最后更新日期和到期日期。

接下来,我们获取有关注册服务商和注册人的信息。我们可以找到注册人的姓名和联系信息,除非他们使用某些隐私服务。虽然上面没有显示,但我们得到了这个域的管理员和技术联系人。最后,如果我们有任何DNS记录要查找,我们会看到我们应该查询的域名服务器。

可以检查收集到的信息以发现新的攻击面,例如社会工程或技术攻击。例如,根据渗透测试的范围,您可能会考虑对管理员用户的电子邮件服务器或
DNS
服务器进行攻击,假设它们归您的客户所有并且在渗透测试的范围内。

需要注意的是,由于自动化工具滥用
WHOIS
查询来收集电子邮件地址,许多
WHOIS
服务都对此采取了措施。例如,他们可能会编辑电子邮件地址。此外,许多注册人订阅了隐私服务,以避免他们的电子邮件地址被垃圾邮件发送者收集并保护他们的信息的私密性

nslookup 和 dig

在前面的任务中,我们使用 WHOIS 协议来获取有关我们正在查找的域名的各种信息。特别是,我们能够从注册商那里获得DNS服务器。

使用 查找域名的
IP
地址
nslookup
,代表名称服务器查找。您需要发出命令
nslookup DOMAIN_NAME
,例如,
nslookup tryhackme.com
。或者,更一般地说,您可以使用
nslookup OPTIONS DOMAIN_NAME SERVER
. 这三个主要参数是:

  • OPTIONS
    包含查询类型,如下表所示。例如,您可以使用
    A
    IPv4
    地址和
    AAAA
    IPv6
    地址。
  • DOMAIN_NAME
    是您正在查找的域名。
  • SERVER
    是您要查询的
    DNS
    服务器。您可以选择任何本地或公共
    DNS
    服务器进行查询。
    Cloudflare
    提供
    1.1.1.1

    1.0.0.1

    Google
    提供
    8.8.8.8

    8.8.4.4

    Quad9
    提供
    9.9.9.9

    149.112.112.112
    。如果您想要
    ISP

    DNS
    服务器的替代方案,您可以选择更多的
    公共DNS
    服务器
查询类型 结果
A IPv4
地址
AAAA
IPv6
地址
别名 规范名称
MX 邮件服务器
面向服务架构 授权开始
TXT TXT
记录

例如,
nslookup -type=A tryhackme.com 1.1.1.1
(或者
nslookup -type=a tryhackme.com 1.1.1.1
因为它不区分大小写)可用于返回
tryhackme.com
使用的所有
IPv4
地址

user@TryHackMe$ nslookup -type=A tryhackme.com 1.1.1.1
Server:		1.1.1.1
Address:	1.1.1.1#53

Non-authoritative answer:
Name:	tryhackme.com
Address: 172.67.69.208
Name:	tryhackme.com
Address: 104.26.11.229
Name:	tryhackme.com
Address: 104.26.10.229

A

AAAA
记录分别用于返回
IPv4

IPv6
地址。这种查找有助于从渗透测试的角度了解。在上面的示例中,我们从一个域名开始,并获得了三个
IPv4
地址。如果这些
IP
地址在渗透测试的范围内,则可以进一步检查每个
IP
地址的不安全性

假设您想了解特定域的电子邮件服务器和配置。你可以发出
nslookup -type=MX tryhackme.com
. 这是一个例子:

user@TryHackMe$ nslookup -type=MX tryhackme.com
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
tryhackme.com	mail exchanger = 5 alt1.aspmx.l.google.com.
tryhackme.com	mail exchanger = 1 aspmx.l.google.com.
tryhackme.com	mail exchanger = 10 alt4.aspmx.l.google.com.
tryhackme.com	mail exchanger = 10 alt3.aspmx.l.google.com.
tryhackme.com	mail exchanger = 5 alt2.aspmx.l.google.com.

我们可以看到
tryhackme.com
当前的邮箱配置使用的是谷歌。由于
MX
正在查找邮件交换服务器,我们注意到当邮件服务器尝试发送电子邮件时
@tryhackme.com
,它将尝试连接到
aspmx.l.google.com
顺序为
1
的 。如果它繁忙或不可用,邮件服务器将尝试连接到下一个顺序邮件交换服务器,
alt1.aspmx.l.google.com

alt2.aspmx.l.google.com

Google
提供列出的邮件服务器;因此,我们不应期望邮件服务器运行易受攻击的服务器版本。但是,在其他情况下,我们可能会发现邮件服务器没有得到足够的保护或修补

当您继续对目标进行被动侦察时,这些信息可能会被证明是有价值的。您可以对其他域名重复类似的查询并尝试不同的类型,例如
-type=txt
. 谁知道您会在途中发现什么样的信息!

对于更高级的
DNS
dig
查询和其他功能,如果您好奇的话,可以使用
“Domain Information Groper”
的首字母缩写词。让我们使用
dig
来查找
MX
记录并将它们与
nslookup
. 我们可以使用
dig DOMAIN_NAME
,但要指定记录类型,我们会使用
dig DOMAIN_NAME TYPE
。(可选)我们可以使用选择我们想要查询的服务器
dig @SERVER DOMAIN_NAME TYPE

  • SERVER
    是您要查询的
    DNS
    服务器。
  • DOMAIN_NAME
    是您正在查找的域名。
  • TYPE
    包含
    DNS
    记录类型,如前面提供的表中所示。
user@TryHackMe$ dig tryhackme.com MX

; <<>> DiG 9.16.19-RH <<>> tryhackme.com MX
;; global options: +cmd
;; Got answer:
;; ->>HEADER<

nslookup
和的输出之间的快速比较
dig
表明
dig
返回了更多信息,例如默认情况下的
TTL(生存时间)
。可以使用DNS服务器
1.1.1.1
命令就是
dig @1.1.1.1 tryhackme.com MX

DNSDumpster

nslookup

dig

DNS
查找工具无法自行查找子域。您正在检查的域可能包含一个不同的子域,该子域可以揭示有关目标的大量信息。例如,如果
tryhackme.com
有子域
wiki.tryhackme.com

webmail.tryhackme.com
,你想了解更多关于这两个的信息,因为它们可以保存关于你的目标的大量信息。有可能这些子域之一已经设置并且没有定期更新。缺乏适当的定期更新通常会导致易受攻击的服务。但是我们怎么知道这样的子域存在呢?

我们可以考虑使用多个搜索引擎来编制一个公开的子域列表。一个搜索引擎是不够的;此外,我们应该期望至少浏览数十个结果才能找到有趣的数据。毕竟,您正在寻找未明确公布的子域,因此没有必要使其进入搜索结果的第一页。另一种发现此类子域的方法是依靠暴力查询来查找哪些子域具有
DNS
记录。

为避免这种耗时的搜索,可以使用提供DNS查询详细答案的在线服务,例如
DNSDumpster
。如果我们在
DNSDumpster
中搜索,我们将发现典型
DNS
tryhackme.com
查询无法提供的子域。此外,
DNSDumpster
将以易于阅读的表格和图表的形式返回收集到的
DNS
信息。
DNSDumpster
还将提供有关侦听服务器的所有收集信息。
blog.tryhackme.com

我们将在
DNS Dumpster
上搜索,让您大致了解预期的输出。在结果中,我们得到了我们正在查找的域的
DNS
服务器列表。
DNSDumpster
还将域名解析为
IP
地址,甚至尝试对它们进行地理定位。我们还可以看到
MX
记录;
DNSDumpster
将所有五个邮件交换服务器解析为各自的
IP
地址,并提供有关所有者和位置的更多信息。最后,我们可以看到
TXT
记录。实际上,单个查询就足以检索所有这些信息
tryhackme.com

DNS Dumpster
还将以图形方式表示收集到的信息。
DNSDumpster
将之前表格中的数据显示为图表。您可以看到 DNS**** 和
MX
分支到各自的服务器并显示
IP
地址

Shodan.io

Shodan.io
这样的服务可以帮助您了解有关客户端网络的各种信息,而无需主动连接到它。此外,在防御方面,您可以使用
Shodan.io
的不同服务来了解属于您组织的已连接和暴露的设备

Shodan.io
试图连接到每一个可在线访问的设备,以构建一个连接“事物”的搜索引擎,而不是网页搜索引擎。一旦得到响应,它就会收集与该服务相关的所有信息并将其保存在数据库中以便于搜索。考虑其中一个
tryhackme.com
服务器的保存记录

这条记录显示了一个网络服务器;然而,如前所述,Shodan.io 收集与它可以找到的任何在线连接设备相关的信息。在 Shodan.io 上搜索,tryhackme.com至少会显示上图所示的记录。通过这个 Shodan.io 搜索结果,我们可以了解到与我们的搜索相关的几件事,例如:

  • IP地址
  • 托管公司
  • 地理位置
  • 服务器类型和版本

您也可以尝试搜索从
DNS
查找中获得的
IP
地址。当然,这些更容易发生变化。在他们的帮助页面上,您可以了解
Shodan.io
上可用的所有搜索选项,我们鼓励您加入
TryHackMe

Shodan.io

概括

在这个房间里,我们专注于被动侦察。特别是,我们介绍了命令行工具
whois

nslookup

dig
。我们还讨论了两个公开可用的服务
DNSDumpster

Shodan.io
。此类工具的强大之处在于,您可以在不直接连接目标的情况下收集有关目标的信息。此外,一旦您掌握了搜索选项并习惯阅读结果,使用此类工具可能会发现大量信息

目的 命令行示例
查找 WHOIS 记录 whois tryhackme.com
查找DNS A 记录 nslookup -type=A tryhackme.com
在 DNS 服务器上查找DNS MX 记录 nslookup -type=MX tryhackme.com 1.1.1.1
查找DNS TXT 记录 nslookup -type=TXT tryhackme.com
查找DNS A 记录 dig tryhackme.com A
在 DNS 服务器上查找DNS MX 记录 dig @1.1.1.1 tryhackme.com MX
查找DNS TXT 记录 dig tryhackme.com TXT

主动侦察

我们专注于主动侦察和与之相关的基本工具。我们学习使用网络浏览器来收集有关目标的更多信息。
ping
此外,我们还讨论了使用
traceroute

telnet
和 等简单工具
nc
来收集有关网络、系统和服务的信息

正如我们在上一个房间中了解到的那样,被动侦察可以让您在没有任何直接参与或联系的情况下收集有关目标的信息。您正在远距离观看或查看公开信息。

主动侦察要求您与目标进行某种接触。这种联系可以是打电话或拜访目标公司,借口是收集更多信息,通常是社会工程的一部分。或者,它可以直接连接到目标系统,无论是访问他们的网站还是检查他们的防火墙是否打开了
SSH
端口。把它想象成你正在仔细检查窗户和门锁。因此,必须记住,在获得客户签署的合法授权之前,不要从事主动侦察工作

在这个房间里,我们专注于主动侦察。主动侦察始于与目标机器的直接连接。任何此类连接都可能在日志中留下信息,显示客户端 IP 地址、连接时间和连接持续时间等。但是,并非所有连接都是可疑的。可以让您的主动侦察显示为常规客户活动。考虑网页浏览;在数百名其他合法用户中,没有人会怀疑浏览器连接到目标网络服务器。作为红队(攻击者)的一部分工作时,您可以使用这些技术来发挥自己的优势,并且不想惊动蓝队(防御者)。

在这个房间里,我们将介绍通常与大多数操作系统捆绑在一起或很容易获得的各种工具。我们从
Web
浏览器及其内置的开发人员工具开始;此外,我们还向您展示了如何“武装”网络浏览器以使其成为高效的侦察框架。之后,我们讨论其他良性工具,例如
ping

traceroute

telnet
。所有这些程序都需要与目标建立联系,因此我们的活动将受到主动侦察。

任何想要熟悉基本工具并了解如何在主动侦察中使用它们的人都会对这个房间感兴趣。Web 浏览器开发人员工具可能需要一些努力才能熟悉,尽管它提供了图形用户界面。所涵盖的命令行工具使用起来相对简单

Web浏览器

Web
浏览器可以是一个方便的工具,特别是它在所有系统上都很容易使用。您可以通过多种方式使用
Web
浏览器收集有关目标的信息。

在传输层,浏览器连接到:

  • 通过
    HTTP
    访问网站时,默认使用
    TCP
    端口
    80
  • 通过
    HTTPS
    访问网站时默认使用
    TCP
    端口
    443

由于 80 和 443 是HTTP和 HTTPS 的默认端口,因此 Web 浏览器不会在地址栏中显示它们。但是,可以使用自定义端口来访问服务。例如,
https://127.0.0.1:8834/
将通过 HTTPS 协议在端口 8834 连接到 127.0.0.1(本地主机)。如果有 HTTPS 服务器侦听该端口,我们将收到一个网页

在浏览网页时,您可以Ctrl+Shift+I在 PC 上按 或在 Mac 上按Option + Command + I( ) 以打开 Firefox 上的开发者工具。⌥ + ⌘ + I类似的快捷方式也可以让您开始使用 Google Chrome 或 Chromium。开发人员工具可让您检查浏览器已接收并与远程服务器交换的许多内容。例如,您可以查看甚至修改 JavaScript (JS) 文件、检查系统上设置的 cookie 并发现站点内容的文件夹结构。

下面是 Firefox 开发者工具的截图。Chrome DevTools 非常相似

还有很多适用于
Firefox

Chrome
的附加组件可以帮助进行渗透测试。这里有一些例子:

  • FoxyProxy
    可让您快速更改用于访问目标网站的代理服务器。当您使用 Burp Suite 等工具或需要定期切换代理服务器时,此浏览器扩展非常方便

  • User-Agent Switcher and Manager
    ,您能够假装从不同的操作系统或不同的 Web 浏览器访问网页。换句话说,您可以假装使用
    iPhone
    浏览网站,而实际上您是从 Mozilla Firefox 访问它

  • Wappalyzer
    提供有关所访问网站所用技术的见解。这种扩展很方便,主要是当您像任何其他用户一样在浏览网站时收集所有这些信息时。Wappalyzer 的屏幕截图如下所示

概括

后面的也懒得搬运了

在这个房间里,我们介绍了许多不同的工具。很容易通过 shell 脚本将它们中的几个放在一起来构建一个原始的网络和系统扫描器。您可以使用traceroute映射到目标的路径,ping检查目标系统是否响应 ICMP Echo,并telnet通过尝试连接来检查哪些端口是开放的和可访问的。可用的扫描仪在更先进和复杂的水平上执行此操作,没有说道
Nmap
,最主要还是使用
Nmap

命令 例子
ping ping -c 10 MACHINE_IP在Linux或 macOS上
ping ping -n 10 MACHINE_IP在 Windows 上
traceroute traceroute MACHINE_IP在Linux或 macOS上
tracert tracert MACHINE_IP在 Windows 上
telnet telnet MACHINE_IP PORT_NUMBER
netcat 作为客户端 nc MACHINE_IP PORT_NUMBER
netcat 作为服务器 nc -lvnp PORT_NUMBER

尽管这些是基本工具,但它们在大多数系统上都很容易获得。特别是,几乎每台计算机和智能手机上都安装了 Web 浏览器,它可以成为您武器库中的重要工具,用于在不发出警报的情况下进行侦察

操作系统 开发者工具快捷方式
Linux或 MS Windows Ctrl+Shift+I
苹果系统 Option + Command + I

关于产品的一些思考

产品:
在将业务抽象成产品或组件时,需要考虑多个因素,包括闭环条款、持久性、可重用性等
。只有当业务具备这些关键特征时,才能适合抽象成产品。否则,应该考虑将其作为组件的形式存在,或者使用规则引擎来可视化出来,使用条件积木和行为积木来表达其控制逻辑和操作步骤。

例如,限购、阻断和实名校验等业务没有明确的闭环条款,因此不太适合作为产品存在。相反,这些业务更适合作为可重用的组件存在,供其他产品调用。此外,使用规则引擎可视化这些业务可以帮助更好地管理其输入和输出,并使用条件积木和行为积木来表达其控制逻辑和操作步骤。

在考虑将业务抽象成产品或组件时,还需要考虑其持久性和可重用性。业务应该是非临时的,即它需要在相对长的时间内存在,并且在此期间不会发生根本性变化。如果一个业务存在较高的变动性和不确定性,那么将其抽象成组件可能会增加额外的复杂度和风险。

总之,将业务抽象成产品或组件需要综合考虑多个因素,包括闭环条款、持久性、可重用性等。通过合理地设计产品和组件,可以提高开发效率和业务灵活性,从而更好地满足用户的需求和期望。

TMF产品的定义

其实在毗卢的文章中没有对产品有名气的定义,在上面我按我之前对平台设计的理解给了产品定义,就是需要考虑是否有闭环、可持久性和可重构的业务场景。我们来看下TMF提供的例子,例子内容来自https://www.ryu.xin/2022/09/24/lattice-overlay-product/

我的理解是一个团购的产品,可能是团购返现、也可能是普通的团购,他在下单的过程中的不同步骤有自己的业务场景,能够形成闭环

定义团购场景 “GroupBuyProduct” 产品

@Product(code = GroupBuyProduct.GROUP_BUY_PRODUCT_CODE, name = "Group Buy Trade Product")
public class GroupBuyProduct extends ProductTemplate {

    public static final String GROUP_BUY_PRODUCT_CODE = "lattice.productGroupBuyProduct";

    @Override
    public boolean isEffect(ScenarioRequest request) {
        if (request instanceof BuyScenarioRequest) {
            boolean effect = StringUtils.equals("groupBuy", ((BuyScenarioRequest) request).getSource());
            System.out.println("GroupBuyProduct effect status:" + effect);
            return effect;
        }
        return false;
    }
}

笛卡尔积问题

在构建一个实物商品下单的产品时,需要组合多个产品,包括储值卡产品、实物商品产品、支付产品、优惠计算产品、快递产品和风控产品。同时,需要注意到同城配送产品在此场景下不适用,因此应该被剔除。这个过程需要考虑到各个产品之间的关联和依赖关系,并且
需要进行笛卡尔积操作来
确定最终组合的可能性和组合数目。

然而,如果构建一个虚拟商品下单的产品,组合的产品就会有所不同。在这种情况下,储值卡产品、虚拟商品产品、支付产品、优惠计算产品、快递产品和风控产品都是需要的。这种场景下的产品组合与实物商品下单的产品组合有所不同,因此需要重新组装一个新的产品组合,以便满足需求。

这样的组合产品不仅需要在构建时进行考虑,同时也需要在后续的维护和更新中进行相应的操作。这可能会导致大量的重复性工作,而且容易出现错误。为了解决这个问题,可以采用组件化和模块化的设计方法来简化产品的组装和维护。在这种方法中,每个产品都可以作为一个独立的组件,可以与其他组件相互连接和组合,形成一个完整的产品。这样可以有效降低组合产品的复杂度和维护成本,提高产品的开发效率和质量。

总之,产品组合和维护是一个复杂的过程,需要考虑到各个产品之间的关联和依赖关系,并进行合理的组合和剔除。采用组件化和模块化的设计方法可以有效简化产品的组装和维护,提高产品的开发效率和质量。如果设计不好的话,后期会有很多的叠加的笛卡尔积,导致产品、组合产品不断的膨胀,变成一个难以维护的系统。

去年公司由于不断发展,内部自研系统越来越多,所以后来搭建了一个日志收集平台,并将日志收集功能以二方包形式引入各个自研系统,避免每个自研系统都要建立一套自己的日志模块,节约了开发时间,管理起来也更加容易。
这篇文章主要介绍如何编写二方包,并整合到各个系统中。

先介绍整个ELK日志平台的架构。其中xiaobawang-log就是今天的主角。

日志模块 (2).jpg

xiaobawang-log主要收集三种日志类型:

  1. 系统级别日志:
    收集系统运行时产生的各个级别的日志(ERROR、INFO、WARN、DEBUG和TRACER),其中ERROR级别日志是我们最关心的。
  2. 用户请求日志:
    主要用于controller层的请求,捕获用户请求信息和响应信息、以及来源ip等,便于分析用户行为。
  3. 自定义操作日志:
    顾名思义,就是收集手动打的日志。比如定时器执行开始,都会习惯性写一个log.info("定时器执行开始!")的描述,这种就是属于自定义操作日志的类型。

二方包开发

先看目录结构
image.png
废话不多说,上代码。
1、首先创建一个springboot项目,引入如下包:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.0.1</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.10</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.10</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-access</artifactId>
    <version>1.2.10</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.18</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    <version>1.18.26</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

SysLog实体类

public class SysLog {

    /**
     * 日志名称
     */
    private String logName;

    /**
     * ip地址
     */
    private String ip;

    /**
     * 请求参数
     */
    private String requestParams;

    /**
     * 请求地址
     */
    private String requestUrl;

    /**
     * 用户ua信息
     */
    private String userAgent;

    /**
     * 请求时间
     */
    private Long useTime;

    /**
     * 请求时间
     */
    private String exceptionInfo;

    /**
     * 响应信息
     */
    private String responseInfo;

    /**
     * 用户名称
     */
    private String username;

    /**
     * 请求方式
     */
    private String requestMethod;

}

LogAction

创建一个枚举类,包含三种日志类型。

public enum LogAction {

    USER_ACTION("用户日志", "user-action"),
    SYS_ACTION("系统日志", "sys-action"),
    CUSTON_ACTION("其他日志", "custom-action");

    private final String action;

    private final String actionName;

    LogAction(String action,String actionName) {
        this.action = action;
        this.actionName = actionName;
    }

    public String getAction() {
        return action;
    }

    public String getActionName() {
        return actionName;
    }

}

配置logstash

更改logstash配置文件,将index名称更改为log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]},其中appname为系统名称,action为日志类型。
整个es索引名称是以“系统名称+日期+日志类型”的形式。比如“mySystem-2023.03.05-system-action”表示这个索引,是由mySystem在2023年3月5日产生的系统级别的日志。

# 输入端
input {
  stdin { } 
  #为logstash增加tcp输入口,后面springboot接入会用到
  tcp {
      mode => "server"
      host => "0.0.0.0"
      port => 5043
      codec => json_lines
  }
}
 
#输出端
output {
  stdout {
    codec => rubydebug
  }
  elasticsearch {
    hosts => ["http://你的虚拟机ip地址:9200"]
    # 输出至elasticsearch中的自定义index名称
    index => "log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]}"
  }
  stdout { codec => rubydebug }
}

AppenderBuilder

使用编程式配置logback,AppenderBuilder用于创建appender。

  • 这里会创建两种appender。consoleAppender负责将日志打印到控制台,这对开发来说是十分有用的。而LogstashTcpSocketAppender则负责将日志保存到ELK中。
  • setCustomFields中的参数,对应上面logstash配置文件的参数[appname]和[action]。
@Component
public class AppenderBuilder {

    public static final String SOCKET_ADDRESS = "你的虚拟机ip地址";

    public static final Integer PORT = 5043;//logstash tcp输入端口

    /**
     * logstash通信Appender
     * @param name
     * @param action
     * @param level
     * @return
     */
    public LogstashTcpSocketAppender logAppenderBuild(String name, String action, Level level) {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
        appender.setContext(context);
        //设置logstash通信地址
        InetSocketAddress inetSocketAddress = new InetSocketAddress(SOCKET_ADDRESS, PORT);
        appender.addDestinations(inetSocketAddress);
        LogstashEncoder logstashEncoder = new LogstashEncoder();
        //对应前面logstash配置文件里的参数
        logstashEncoder.setCustomFields("{\"appname\":\"" + name + "\",\"action\":\"" + action + "\"}");
        appender.setEncoder(logstashEncoder);

        //这里设置级别过滤器
        LevelFilter levelFilter = new LevelFilter();
        levelFilter.setLevel(level);
        levelFilter.setOnMatch(ACCEPT);
        levelFilter.setOnMismatch(DENY);
        levelFilter.start();
        appender.addFilter(levelFilter);
        appender.start();

        return appender;
    }
    
    
    /**
     * 控制打印Appender
     * @return
     */
    public ConsoleAppender consoleAppenderBuild() {
        ConsoleAppender consoleAppender = new ConsoleAppender();
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
        encoder.setContext(context);
        //设置格式
        encoder.setPattern("%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)");
        encoder.start();
        consoleAppender.setEncoder(encoder);
        consoleAppender.start();
        return consoleAppender;

    }

LoggerBuilder

LoggerBuilder主要用于创建logger类。创建步骤如下:

  1. 获取logger上下文。
  2. 从上下文获取logger对象。创建过的logger会保存在LOGCONTAINER中,保证下次获取logger不会重复创建。这里使用ConcurrentHashMap防止出现并发问题。
  3. 创建appender,并将appender加入logger对象中。
@Component
public class LoggerBuilder {
    @Autowired
    AppenderBuilder appenderBuilder;

    @Value("${spring.application.name:unknow-system}")
    private String appName;

    private static final Map<String, Logger> LOGCONTAINER = new ConcurrentHashMap<>();

    public Logger getLogger(LogAction logAction) {
        Logger logger = LOGCONTAINER.get(logAction.getActionName() + "-" + appName);
        if (logger != null) {
            return logger;
        }
        logger = build(logAction);
        LOGCONTAINER.put(logAction.getActionName() + "-" + appName, logger);

        return logger;
    }

    public Logger getLogger() {
        return getLogger(LogAction.CUSTON_ACTION);
    }

    private Logger build(LogAction logAction) {
        //创建日志appender
        List<LogstashTcpSocketAppender> list = createAppender(appName, logAction.getActionName());
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        Logger logger = context.getLogger(logAction.getActionName() + "-" + appName);
        logger.setAdditive(false);
        //打印控制台appender
        ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild();
        logger.addAppender(consoleAppender);
        list.forEach(appender -> {
            logger.addAppender(appender);
        });
        return logger;
    }

    /**
     * LoggerContext上下文中的日志对象加入appender
     */
    public void addContextAppender() {
        //创建四种类型日志
        String action = LogAction.SYS_ACTION.getActionName();
        List<LogstashTcpSocketAppender> list = createAppender(appName, action);
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        //打印控制台
        ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild();
        context.getLoggerList().forEach(logger -> {
            logger.setAdditive(false);
            logger.addAppender(consoleAppender);
            list.forEach(appender -> {
                logger.addAppender(appender);
            });
        });
    }

    /**
     * 创建连接elk的appender,每一种级别日志创建一个appender
     *
     * @param name
     * @param action
     * @return
     */
    public List<LogstashTcpSocketAppender> createAppender(String name, String action) {
        List<LogstashTcpSocketAppender> list = new ArrayList<>();
        LogstashTcpSocketAppender errorAppender = appenderBuilder.logAppenderBuild(name, action, Level.ERROR);
        LogstashTcpSocketAppender infoAppender = appenderBuilder.logAppenderBuild(name, action, Level.INFO);
        LogstashTcpSocketAppender warnAppender = appenderBuilder.logAppenderBuild(name, action, Level.WARN);
        LogstashTcpSocketAppender debugAppender = appenderBuilder.logAppenderBuild(name, action, Level.DEBUG);
        LogstashTcpSocketAppender traceAppender = appenderBuilder.logAppenderBuild(name, action, Level.TRACE);
        list.add(errorAppender);
        list.add(infoAppender);
        list.add(warnAppender);
        list.add(debugAppender);
        list.add(traceAppender);
        return list;
    }
}

LogAspect

使用spring aop,实现拦截用户请求,记录用户日志。比如ip、请求参数、请求用户等信息,需要配合下面的XiaoBaWangLog注解使用。
这里拦截上面所说的第二种日志类型。

@Aspect
@Component
public class LogAspect {

    @Autowired
    LoggerBuilder loggerBuilder;

    private ThreadLocal<Long> startTime = new ThreadLocal<>();

    private SysLog sysLog;

    @Pointcut("@annotation(com.xiaobawang.common.log.annotation.XiaoBaWangLog)")
    public void pointcut() {
    }

    /**
     * 前置方法执行
     *
     * @param joinPoint
     */
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        startTime.set(System.currentTimeMillis());
        //获取请求的request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String clientIP = ServletUtil.getClientIP(request, null);
        if ("0.0.0.0".equals(clientIP) || "0:0:0:0:0:0:0:1".equals(clientIP) || "localhost".equals(clientIP) || "127.0.0.1".equals(clientIP)) {
            clientIP = "127.0.0.1";
        }
        sysLog = new SysLog();
        sysLog.setIp(clientIP);
        String requestParams = JSONUtil.toJsonStr(getRequestParams(request));
        sysLog.setRequestParams(requestParams.length() > 5000 ? ("请求参数过长,参数长度为:" + requestParams.length()) : requestParams);
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method method = ms.getMethod();
        String logName = method.getAnnotation(XiaoBaWangLog.class).value();
        sysLog.setLogName(logName);
        sysLog.setUserAgent(request.getHeader("User-Agent"));
        String fullUrl = request.getRequestURL().toString();
        if (request.getQueryString() != null && !"".equals(request.getQueryString())) {
            fullUrl = request.getRequestURL().toString() + "?" + request.getQueryString();
        }
        sysLog.setRequestUrl(fullUrl);
        sysLog.setRequestMethod(request.getMethod());
        //tkSysLog.setUsername(JwtUtils.getUsername());
    }

    /**
     * 方法返回后执行
     *
     * @param ret
     */
    @AfterReturning(returning = "ret", pointcut = "pointcut()")
    public void after(Object ret) {
        Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION);
        String retJsonStr = JSONUtil.toJsonStr(ret);
        if (retJsonStr != null) {
            sysLog.setResponseInfo(retJsonStr.length() > 5000 ? ("响应参数过长,参数长度为:" + retJsonStr.length()) : retJsonStr);
        }
        sysLog.setUseTime(System.currentTimeMillis() - startTime.get());
        logger.info(JSONUtil.toJsonStr(sysLog));
    }

    /**
     * 环绕通知,收集方法执行期间的错误信息
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        try {
            Object obj = proceedingJoinPoint.proceed();
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
            sysLog.setExceptionInfo(e.getMessage());
            Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION);
            logger.error(JSONUtil.toJsonStr(sysLog));
            throw e;
        }
    }

    /**
     * 获取请求的参数
     *
     * @param request
     * @return
     */
    private Map getRequestParams(HttpServletRequest request) {
        Map map = new HashMap();
        Enumeration paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = (String) paramNames.nextElement();
            String[] paramValues = request.getParameterValues(paramName);
            if (paramValues.length == 1) {
                String paramValue = paramValues[0];
                if (paramValue.length() != 0) {
                    map.put(paramName, paramValue);
                }
            }
        }
        return map;
    }


}

XiaoBaWangLog

LoggerLoad主要是实现用户级别日志的收集功能。
这里定义了一个注解,在controller方法上加上@XiaoBaWangLog("操作内容"),即可拦截并生成请求日志。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface XiaoBaWangLog {

    String value() default "";

}

LoggerLoad

LoggerLoad主要是实现系统级别日志的收集功能。
继承ApplicationRunner,可以在springboot执行后,自动创建
系统级别日志
logger对象。

@Component
@Order(value = 1)
@Slf4j
public class LoggerLoad implements ApplicationRunner {
    @Autowired
    LoggerBuilder loggerBuilder;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        loggerBuilder.addContextAppender();
        log.info("加载日志模块成功");
    }
}

LogConfig

LogConfig主要实现自定义级别日志的收集功能。
生成一个logger对象交给spring容器管理。后面直接从容器取就可以了。

@Configuration
public class LogConfig {

    @Autowired
    LoggerBuilder loggerBuilder;

    @Bean
    public Logger loggerBean(){
        return loggerBuilder.getLogger();
    }
}

代码到现在已经全部完成,怎么将上述的所有Bean加入到spring呢?这个时候就需要用到spring.factories了。

spring.factories

在EnableAutoConfiguration中加入类的全路径名,在项目启动的时候,SpringFactoriesLoader会初始化spring.factories,包括pom中引入的jar包中的配置类。
注意,spring.factories在2.7开始已经不推荐使用,3.X版本的springBoot是不支持使用的。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.xiaobawang.common.log.config.AppenderBuilder,\
  com.xiaobawang.common.log.config.LoggerBuilder,\
  com.xiaobawang.common.log.load.LoggerLoad,\
  com.xiaobawang.common.log.aspect.LogAspect,\
  com.xiaobawang.common.log.config.LogConfig

测试

先将xiaobawang进行打包
新建一个springboot项目,引入打包好的xiaobawang-log.

image.png
运行springboot,出现“加载日志模块成功”表示日志模块启动成功。

接着新建一个controller请求

image.png

访问请求后,可以看到了三种不同类型的索引了

屏幕截图 2023-03-19 164547.png

结束

还有很多需要优化的地方,比如ELK设置用户名密码登录等,对ELK比较了解的童鞋可以自己尝试优化!
如果这篇文章对你有帮助,记得一键三连~

Support Vector Machines


Perceptron and Linear Separability


假设存在一个 linear decision boundary,它可以完美地对 training dataset 进行分割。 那么,经由上述 Perceptron Algorithm 计算,它将返回哪一条 linear separator?


image


当 linear separator(即一个给定的超平面)的 margin
\(\gamma\)
越大,则该模型的归纳与概括的性能越强。从几何的角度(二维)的角度来理解非常直观,我们需要这么一条 linear separator,即,它既能对 training dataset 进行完美的分割,同时,我们希望距它最近的数据点距它的距离最大化(如上图中间的那根直线)。否则,如果存在一个数据点距该 linear separator 的距离不是那么远,从直觉来说,围绕在该数据点附近且与它 label 相同的一个新数据点随意体现出的一个随机波动,将使得这个新数据点越过 linear separator,导致分类错误。


因此,现在的问题是,如何将 margin 纳入考量以求得这条最佳的 linear boundary?支持向量机将很好地解决这个问题。




Motivation(Why SVM?)


以下是 SVM 体现出的眼见的优势:


  • SVM 返回一个 linear classifier,并且由于其算法使 margin solution 最大化,故这个 linear classifier 是一个稳定的解。


  • 对 SVM 稍加改变,则能提供一种解决当数据集 non-separable 情况的方法。


  • SVM 同样给出了进行非线性分类的隐性方法(implicit method,即上述的 kernel transformation)。




SVM Formula


假设存在一些 margin
\(\gamma \in \Gamma\)
使得 training dataset
\(\mathcal{S} = \mathcal{X} \times \mathcal{Y}\)
线性可分(但注意 linear separator 不一定穿过空间的原点)。


那么,decision boundary:


\[g(\vec{x}) = \vec{w} \cdot \vec{x} - b = 0
\]


Linear classifier:


\[\begin{align*}
f(\vec{x}) & = \text{sign}\big( g(\vec{x}) \big) \\
& = \text{sign} \big( \vec{w} \cdot \vec{x} - b \big)
\end{align*}
\]


思路


我们先分别求两个平行的超平面,使得它们对所有的 training data point 进行正确的分类,再使这两个超平面之间的距离最大化。


这也是所谓 “支持向量机(Support Vector Machine)” 名称的由来,我们最终选定的支持向量
\(\vec{w}\)
就像千斤顶一样将上述两个平行的超平面 “支撑” 开来,并且支撑开的距离也将是尽可能的最大,如下图所示。


image


Derivation


如上图,两个超平面的 decision boundary 可以写作:


\[\begin{cases}
\vec{w} \cdot \vec{x} - b = 1 \\
\vec{w} \cdot \vec{x} - b = -1
\end{cases}
\]


则两个超平面之间的距离为:


\[\frac{2}{||\vec{w}||}
\]




  • 对于初学者的直观理解,推导可以通过二维平面上点到直线的距离进行类比,已知点
    \((x_{0}, y_{0})\)
    到直线
    \(Ax + By + C = 0\)
    的计算公式为:



    \[\frac{|Ax_{0} + By_{0} + C|}{\sqrt{A^{2} + B^{2}}}
    \]


    因此,设
    \(\vec{w} \cdot \vec{x} - b = 1\)
    上任意一点的坐标为
    \(\vec{x_{0}}\)
    ,故满足:



    \[\vec{w} \cdot \vec{x_{0}} - b - 1 = 0
    \]


    那么两平行超平面之间的距离为该点到另一超平面
    \(\vec{w} \cdot \vec{x} - b = -1\)
    的距离,即:



    \[\begin{align*}
    \frac{|\vec{w} \cdot \vec{x_{0}} - b + 1|}{\sqrt{||\vec{w}||^{2}}} & = \frac{|\big( \vec{w} \cdot \vec{x_{0}} - b - 1 \big) + 2|}{\sqrt{||\vec{w}||^{2}}} \\
    & = \frac{2}{||\vec{w}||}
    \end{align*}
    \]




因此,对于
\(\forall i \in \mathbb{N}^{+}\)
,当:


\[\begin{cases}
\vec{w} \cdot \vec{x_{i}} - b \geq 1 \qquad \qquad \text{if } y_{i} = 1 \\
\vec{w} \cdot \vec{x_{i}} - b \leq -1 \qquad \quad \ \text{if } y_{i} = -1
\end{cases}
\]


则 training data 全部被正确地分类。




  • 理解

    参考上图,此处
    \(\vec{w} \cdot \vec{x_{i}} - b \geq 1\)

    \(\vec{w} \cdot \vec{x_{i}} - b \leq -1\)
    的几何意义是,将对于 label 为
    \(1\)

    \(-1\)
    的 data point 分别排除在超平面
    \(\vec{w} \cdot \vec{x} - b = 1\)

    \(\vec{w} \cdot \vec{x} - b = -1\)
    的两边外侧,从而留下两个超平面之间的空档。




我们合并上面两式为一个式子,则 training data 全部被正确地分类等价于:


\[\forall i \in \mathbb{N}^{+}: ~ y_{i} \big( \vec{w} \cdot \vec{x_{i}} - b \big) \geq 1
\]


现在我们得到了两个超平面的距离表达式
\(\frac{2}{||\vec{w}||}\)
,同时需要满足 constraints
\(y_{i} \big( \vec{w} \cdot \vec{x_{i}} - b \big) \geq 1\)
for
\(\forall i \in \mathbb{N}^{+}\)
,我们希望在约束条件下使
\(\frac{2}{||\vec{w}||}\)
最大,那么 SVM 转变为运筹问题的求解,i.e.,


\[\begin{align*}
\text{maximize: } \quad & \frac{2}{||\vec{w}||} \\
\text{subject to: } \quad & y_{i} \big( \vec{w} \cdot \vec{x_{i}} - b \big) \geq 1, \quad \forall i \in \mathbb{N}^{+}
\end{align*}
\]




SVM Standard (Primal) Form


注意到,
\(||\vec{w}|| \geq 0\)
恒成立,且若
\(||\vec{w}|| = 0\)
时,支持向量(即权重向量)
\(\vec{w}\)
为零向量,使得 linear separator 无意义。故最大化
\(\frac{2}{||\vec{w}||}\)
等价于 最小化
\(\frac{1}{2} ||\vec{w}||\)
。类似于线性回归中使用 Mean Square Error 而非 Mean Absolute Error 作为 loss function 的原因,
\(||\vec{w}||\)
在原点处不可微,因此我们选择 minimize
\(\frac{1}{2} ||\vec{w}||^{2}\)
,而非原形式
\(\frac{1}{2}||\vec{w}||\)
,这当然是等价的。


故 SVM Standard (Primal) Form 如下:


\[\begin{align*}
\text{minimize: } \quad & \frac{1}{2} ||\vec{w}||^{2} \\
\text{subject to: } \quad & y_{i} \big( \vec{w} \cdot \vec{x_{i}} - b \big) \geq 1, \quad \forall i \in \mathbb{N}^{+}
\end{align*}
\]




SVM When Training Dataset is Non-separable


当 training dataset 无法被全部正确地分类时(即,不存在一个 margin
\(\gamma \in \Gamma\)
使得 training dataset
\(\mathcal{S} = \mathcal{X} \times \mathcal{Y}\)
线性可分),可以引入 slack variables 求解问题。


SVM Standard (Primal) Form with Slack


SVM Standard (Primal) Form with Slack 如下所示:


\[\begin{align*}
& \text{minimize: } \quad \frac{1}{2} ||\vec{w}||^{2} + C \sum\limits_{i=1}^{n} \xi_{i} \\
& \text{subject to: } \quad \begin{cases}
y_{i} \big( \vec{w} \cdot \vec{x_{i}} - b \big) \geq 1 - \xi_{i}, \quad \forall i \in \mathbb{N}^{+} \\
\xi_{i} \geq 0, \qquad \qquad \qquad \qquad \forall i \in \mathbb{N}^{+} \\
\end{cases}
\end{align*}
\]


问题:如何求解最优的
\(\vec{w}, ~ b, ~ \vec{\xi}\)


由于涉及边界问题,我们不能在目标函数中直接对
\(\vec{w}, ~ b, ~ \vec{\xi}\)
求偏导。我们有以下两种解决办法:


  1. Projection Methods

    从一个满足 constraints 的解
    \(\vec{x_{0}}\)
    开始,求能使得 objective function 略微减小的
    \(\vec{x_{1}}\)
    。如果所求到的
    \(\vec{x_{1}}\)
    违反了 constraints,那么 project back to the constraints 进行迭代。这种方法偏向于利用算法求解,从原理上类似于梯度下降算法以及前文介绍的 Perceptron Algorithm。


  2. Penalty Methods

    使用惩罚函数将 constraints 并入 objective function,对于违反 constraints 的解
    \(\vec{x}\)
    予以惩罚。




The Lagrange (Penalty) Method:拉格朗日(惩罚)方法


考虑增广函数:


\[L(\vec{x}, \vec{\lambda}) = f(\vec{x}) + \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x})
\]


其中,
\(L(\vec{x}, \vec{\lambda})\)
为拉格朗日函数,
\(\lambda_{i}\)
为拉格朗日变量(或对偶变量,dual variables)。


对于此类函数,我们所需要的目标的 canonical form 为:


\[\begin{align*}
\text{minimize: } \quad & f(\vec{x}) \\
\text{subject to: } \quad & g_{i}(\vec{x}), \quad \forall i \in \mathbb{N}^{+}
\end{align*}
\]


由于
\(g_{i}(\vec{x}) \leq 0\)
for
\(\forall i \in \mathbb{N}^{+}\)
,则对于任意的 feasible
\(\vec{x}\)
以及任意的
\(\vec{\lambda_{i}} \geq 0\)
,都有:


\[L(\vec{x}, \vec{\lambda}) \leq f(\vec{x})
\]


因此:


\[\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) \leq f(\vec{x})
\]


注意到上式中的
\(\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})\)
,这代表我们在
\(\vec{\lambda}\)
所在的空间
\([0, ~ \infty)^{n}\)
中搜索使拉格朗日函数最大的
\(\vec{\lambda}\)
,即搜索各个对应的
\(\lambda_{i} \in [0, ~ \infty)\)


尤其注意上式
是针对 feasible
\(\vec{x}\)
成立。因为
\(\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})\)
会导致:



  • \(\vec{x}\)
    infeasible 时,意味着
    \(\vec{x}\)
    不全满足所有约束条件
    \(g_{i}(\vec{x}) \leq 0\)
    for
    \(\forall i \in \mathbb{N}^{+}\)
    ,这意味着:



    \[\exists i: ~ g_{i}(\vec{x}) > 0
    \]


    那么:



    \[\begin{align*}
    \max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) & = \max\limits_{\lambda_{i} \geq 0} \Big( f(\vec{x}) + \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \Big) \\
    & = f(\vec{x}) + \max\limits_{\lambda_{i} \geq 0} \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \\
    & = \infty
    \end{align*}
    \]


    这是因为: 只要对应的
    \(\lambda_{i} \rightarrow \infty\)
    ,则
    \(\lambda_{i} g_{i}(\vec{x}) \rightarrow \infty\)
    (因为
    \(g_{i}(\vec{x}) > 0\)
    ),从而
    \(\sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \rightarrow \infty\)
    ,故
    \(L(\vec{x}, \vec{\lambda}) = f(\vec{x}) + \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \rightarrow \infty\)


    所以此时不满足
    \(\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) \leq f(\vec{x})\)



  • \(\vec{x}\)
    feasible 时,即对于
    \(\forall i \in \mathbb{N}^{+}\)
    ,约束条件
    \(g_{i}(\vec{x}) \leq 0\)
    都成立,那么:



    \[\forall i \in \mathbb{N}^{+}: ~ g_{i}(\vec{x}) \quad \implies \quad\sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \leq 0
    \]


    因此
    \(\max\limits_{\lambda_{i} \geq 0} \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) = 0\)
    ,即令所有
    \(\lambda_{i}\)
    都为
    \(0\)
    ,故:



    \[\begin{align*}
    \max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) & = \max\limits_{\lambda_{i} \geq 0} \Big( f(\vec{x}) + \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \Big) \\
    & = f(\vec{x}) + \max\limits_{\lambda_{i} \geq 0} \Big( \sum\limits_{i=1}^{n} \lambda_{i} g_{i}(\vec{x}) \Big) \\
    & = f(\vec{x})
    \end{align*}
    \]




根据上述结论,给定任意 feasible
\(\vec{x}\)
以及任意
\(\lambda_{i} \geq 0\)
,有:


\[L(\vec{x}, \vec{\lambda}) \leq f(\vec{x})
\]


且:


\[\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) = \begin{cases}
f(\vec{x}) \qquad \text{if } \vec{x} \text{ feasible} \\
\infty \qquad \quad \text{if } \vec{x} \text{ infeasible}
\end{cases}
\]


因此,原先的 constrained optimization problem 的 optimal solution 为:


\[p^{\star} = \min\limits_{\vec{x}} \max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})
\]




  • 如何理解
    \(\min\limits_{\vec{x}} \max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})\)


    \(L(\vec{x}, \vec{\lambda})\)
    是向量
    \(\vec{x}\)

    \(\vec{\lambda}\)
    的函数,从向量角度可以抽象为一个二元函数。因此,计算逻辑是,对于每一个给定的
    \(\vec{x_{0}}\)
    ,可以得到仅关于
    \(\vec{\lambda}\)
    的函数
    \(L(\vec{x_{0}}, \vec{\lambda})\)
    ,然后求出使对应的
    \(L(\vec{x_{0}}, \vec{\lambda})\)
    最大的各
    \(\vec{\lambda_{(\vec{x_{0}})}}^{*}\)
    (i.e.,各
    \(\lambda_{i}^{*}\)
    )。因此内层
    \(\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})\)
    返回一个对于任意给定的
    \(\vec{x_{0}}\)
    ,使得
    \(L(\vec{x_{0}}, \vec{\lambda})\)
    最大的
    \(\vec{\lambda}\)
    的集合。那么,
    \(\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})\)
    是一个仅关于
    \(\vec{x}\)
    的函数,再在外层求使得这个函数最小的
    \(\vec{x}^{*}\)
    ,即
    \(\min\limits_{\vec{x}} \Big( \max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) \Big)\)
    ,其结果可以写为:



    \[\min\limits_{\vec{x}} \max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda}) = L(\vec{x}^{*}, \vec{\lambda_{(\vec{x}^{*})}}^{*})
    \]




  • 解释(为什么它是 optimal solution?):


    因为,对于任意的
    \(\vec{x}\)
    (无论是否 feasible),
    \(\max\limits_{\lambda_{i} \geq 0} L(\vec{x}, \vec{\lambda})\)
    计算出的结果可能为
    \(f(\vec{x})\)
    (当
    \(\vec{x}\)
    为 feasible),也可能为
    \(\infty\)
    (当
    \(\vec{x}\)
    为 infeasible)。但没关系,在最外层的
    \(\min\limits_{\vec{x}}\)
    可以对
    \(\vec{x}\)
    进行筛选,使最终选出的
    \(\vec{x}^{*}\)
    不可能为 infeasible,否则相当于
    \(\min\limits_{\vec{x}}\)
    计算出的结果为
    \(\infty\)
    ,这是只要存在 feasible region 就不可能发生的事情。