2024年12月

做全文搜索,es比较好用,安装可能有点费时费力。mysql安装就不说了。主要是elastic8.4.0+kibana8.4.0+logstash-8.16.1,可视化操作及少量netcore查询代码。

安装elastic8.4.0+kibana8.4.0使用docker-desktop,logstash-8.16.1是线程解压执行文件。

  • 1.
    docker-compose.yml 如下: 首先使用docker network创建一个es-net内部通讯网络,这样kibana连接es可以通过容器名ELASTICSEARCH_HOSTS=http://elasticsearch:9200,此作为单机测试使用单机的es.
services:

elasticsearch:
container_name: elasticsearch
image: docker.elastic.co
/elasticsearch/elasticsearch:8.4.0environment:- discovery.type=single-node
ulimits:
memlock:
soft:
-1hard:-1cap_add:-IPC_LOCK
ports:
- "9200:9200"networks:- es-net

kibana:
container_name: kibana
image: docker.elastic.co
/kibana/kibana:8.4.0environment:- ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports:- "5601:5601"networks:- es-net

networks:
es
-net:
driver: bridge

作为es的8以上版本是有账号密码和crt证书的,需要做如下配置:

安装好es后默认给一个elastic账号,需要重置一下密码,进入es容器执行重置密码命令,会给你一个密码。

docker exec  -it -u root elasticsearch /bin/bash
bin
/elasticsearch-reset-password -u elastic

这里登录的其实是https带证书的,但是kibana使用的是http的,所以在容器内部,config/elasticsearch.yml中需要把下面的两个参数置为false ,生产环境不建议这么操作。

因为es带账号密码,所以kibana连接es也需要账号密码信息,但是默认的elastic是超级管理员,kibana默认是不支持的,需要自己新建账号。但是es默认是给了账号的,用他的就行。自己新建es账号给一个超级管理员角色依然没有重建所应权限,导致kibana起不来,用kibana_system就行。

进入es容器内部给kibana_system重置一个密码,用下面的命令在内部调用也行,我设置的elastic和kibana_system的密码一样,方便使用。

curl -u elastic:DiVnR2F6OGYmP+Ms+n2o -X POST "http://localhost:9200/_security/user/kibana_system/_password" -H 'Content-Type: application/json' -d'{"password": "DiVnR2F6OGYmP+Ms+n2o"}'

  • 2.
    然后在kibana容器中,加上账号密码信息即可,重启。还有最后一行加上i18n.locale: zh-CN  ,改变ui为中文。

然后通过开发工具就可以做es的调试了,这里注意下需要中文分词的可以去 https://github.com/infinilabs/analysis-ik/releases 下载对应版本8.4.0的中文分词器 ,改个名放到es容器内plugins中去。也可以自定义分词文件丢进去

  • 3. 下面就是logstash安装跟mysql的同步了,测试数据如下:

首先去logstash官网下载对应的包,我选的版本是8.16.1,目录如下是可以通过控制台执行的。

这里只需要配置好mysql-connector的驱动和链接信息即可。

jdbc.conf文件内容如下:

input {
stdin {}
jdbc {
type
=> "jdbc"# 数据库连接地址
jdbc_connection_string
=> "jdbc:mysql://192.168.200.2:3306/bbs?characterEncoding=UTF-8&autoReconnect=true"# 数据库连接账号密码;
jdbc_user
=> "admin"jdbc_password=> "这是密码"# MySQL依赖包路径;
jdbc_driver_library
=> "D:\software\logstash-8.16.1\mysql\mysql-connector-j-8.0.32.jar"# the name of the driverclass formysql
jdbc_driver_class
=> "com.mysql.jdbc.Driver"# 数据库重连尝试次数
connection_retry_attempts
=> "3"# 判断数据库连接是否可用,默认false不开启
jdbc_validate_connection
=> "true"# 数据库连接可用校验超时时间,默认3600S
jdbc_validation_timeout
=> "3600"# 开启分页查询(默认false不开启);
jdbc_paging_enabled
=> "true"# 单次分页查询条数(默认100000,若字段较多且更新频率较高,建议调低此值);
jdbc_page_size
=> "500"# statement为查询数据sql,如果sql较复杂,建议配通过statement_filepath配置sql文件的存放路径;
# sql_last_value为内置的变量,存放上次查询结果中最后一条数据tracking_column的值,此处即为ModifyTime;
# statement_filepath
=> "mysql/jdbc.sql"statement=> "SELECT ArticleID,UserID,ArticleTitle,ArticleContent,ImageAddress,StandPoint,PublishTime,`Status`,Likes, Shares,Comments,Reports, Sort,PublishingMode,SourceType,Reply,IsTop,TopEndTime,Hot,EditUserId,CreatedTime,EditTime,UserType,UserNickname,ForbiddenState,PublishDateTime,TopArea,SubscribeType,CollectionCount,Articletype,NewsID,CommentUserCount,TopStartTime,`View`,ViewDuration,Forwardings,ForwardingFId,Freshness,Shelf_Reason,AuditTime FROM bbs_articles"# 是否将字段名转换为小写,默认true(如果有数据序列化、反序列化需求,建议改为false);
lowercase_column_names
=> false# Value can be any of: fatal,error,warn,info,debug,默认info;
sql_log_level
=>warn
#
# 是否记录上次执行结果,true表示会将上次执行结果的tracking_column字段的值保存到last_run_metadata_path指定的文件中;
record_last_run
=> true# 需要记录查询结果某字段的值时,此字段为true,否则默认tracking_column为timestamp的值;
use_column_value
=> true# 需要记录的字段,用于增量同步,需是数据库字段
tracking_column
=> "PublishTime"# Value can be any of: numeric,timestamp,Default valueis "numeric"tracking_column_type=>timestamp
# record_last_run上次数据存放位置;
last_run_metadata_path
=> "mysql/last_id.txt"# 是否清除last_run_metadata_path的记录,需要增量同步时此字段必须为false;
clean_run
=> false#
# 同步频率(分 时 天 月 年),默认每分钟同步一次;
schedule
=> "* * * * *"}
}

filter {
json {
source
=> "message"remove_field=> ["message"]
}
# convert 字段类型转换,将字段TotalMoney数据类型改为float;
mutate {
convert
=>{
#
"TotalMoney" => "float"}
}
}
output {
elasticsearch {
# host
=> "127.0.0.1"# port=> "9200"# 配置ES集群地址
# hosts
=> ["192.168.1.1:9200", "192.168.1.2:9200", "192.168.1.3:9200"]
hosts
=> ["127.0.0.1:9200"]
user
=> "elastic"password=> "DiVnR2F6OGYmP+Ms+n2o"ssl=> false# 索引名字,必须小写
index
=> "bbs_act"# 数据唯一索引(建议使用数据库KeyID)
document_id
=> "%{ArticleID}"}
stdout {
codec
=>json_lines
}
}

配置文成后执行该命令,数据实时同步开始

bin\logstash.bat -f mysql\jdbc.conf

可以通过kibana的discover查看数据,也可以通过开发工具查询,elk日志就是这么玩。

  • 4. 下面就是代码,这里的实体没给全,注意实体需要给Text的Name属性,否则会解析不到数据的:
 public class ArticleEsContext : EsBase<ArticleDto>{public ArticleEsContext(EsConfig esConfig) : base(esConfig)
{
}
public override string IndexName => "bbs_act";public async Task<List<ArticleDto>>GetArticles(ArticleParameter parameter)
{
var client =_esConfig.GetClient(IndexName);//计算分页的起始位置 var from = (parameter.PageNumber - 1) *parameter.PageSize;var searchResponse = await client.SearchAsync<ArticleDto>(s =>s
.Index(IndexName)
.Query(q
=>q
.Bool(b
=>b
.Should(
sh
=> sh.Match(m =>m
.Field(f
=> f.ArticleTitle) //查询 ArticleTitle .Query(parameter.KeyWords)
.Fuzziness(Fuzziness.Auto)
//启用模糊查询 ),
sh
=> sh.Match(m =>m
.Field(f
=> f.ArticleContent) //查询 ArticleContent .Query(parameter.KeyWords)
.Fuzziness(Fuzziness.Auto)
//启用模糊查询 )
)
.MinimumShouldMatch(
1) //至少一个条件必须匹配 )
)
.From(
from) //设置分页的起始位置 .Size(parameter.PageSize) //设置每页大小 );if (!searchResponse.IsValid)
{
Console.WriteLine(searchResponse.DebugInformation);
return new List<ArticleDto>();
}
returnsearchResponse.Documents.ToList();
}
}
public classArticleDto
{
[Text(Name
= "ArticleID")]public int ArticleId { get; set; }
[Text(Name
= "ArticleTitle")]public string ArticleTitle { get; set; }
[Text(Name
= "ArticleContent")]public string ArticleContent { get; set; }
[Date(Name
= "CreatedTime")]public DateTime CreatedTime { get; set; }
}

代码调用结果如下:

d5a34f86-c01f-42e1-a93f-a0aabe2e1246

1.简介

按照计划今天就要讲解和分享TCP协议的三次握手和四次挥手以及使用Wireshark抓取
TCP/IP协议数据包的技能,能够深入分析TCP帧格式及“TCP三次握手”。通过抓包和分析数据包来理解TCP/IP协议,进一步加深对TCP包的理解和认识。

2.
TCP连接的建立(三次握手)

2.1通俗易懂篇

宏哥首先来一个我们日常工作中比较常见的例子:远程会议、视频或者打电话。先上图:

TCP的三次握手有点像我们远程有会议(疫情期间都遇到过吧);

1)A同事:喂!听得到我说话吗?(第一次握手A-->B);

2)B同事:听得到,你能听到我说话吗?(第二次握手B-->A);

3)A同事:听到了,我们可以开会了(第三次握手A-->B)。

一次完整的远程会议流程。

2.2掉发烧脑篇

三次握手的过程,还是先上图:

最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

  • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

3.TCP连接的释放(四次挥手)

3.1通俗易懂篇

宏哥首先来一个我们日常工作中比较常见的例子:和渣男/女分手、离婚。先上图:

故事简述:C和D是男女朋友,C有一天发现D是渣男,想跟D分手。

1)C向D提出分手请求(第一次挥手);

2)D收到请求后,虽然D是渣男,但是D还是很礼貌的,告诉C,我收到你的请求了;但是我现在忙着跟其他人谈恋爱昵(第二次挥手);

3)D终于空下来了,告诉C,我接受你的分手请求了(第三次挥手);

4)C收到D的回复后,告诉D我终于解脱(第四次挥手)。

3.2掉发烧脑篇

四次挥手的过程,还是先上图:

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。
    TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。
    TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(
    在这之前还需要接受服务器发送的最后的数据
    )。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。*
    注意此时TCP连接还没有释放,必须经过2∗ ∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。
    可以看到,服务器结束TCP连接的时间要比客户端早一些。

4.WireShark抓包实战

今天宏哥还是以度娘为例,进行演示WireShark抓包实战,

4.1获取度娘IP

1.首先我们来先看一下百度的IP是多少,想必都知道如何获取吧!方法很多,宏哥就要其中一种最简单的方法来看一下:通过ping命令即可:
ping baidu.com
,如下图所示:

2.从上图可以看出其中39.156.66.10就是度娘的IP地址,后面需要通过这个IP地址,作为wireshark的过滤条件,方便我们找到对应的报文。

4.2WireShark抓包

1.打开WireShark,并开启抓包模式开始捕获,如下图所示:

2.在命令行输入
curl -I baidu.com
来向百度发送一个HTTP请求。如下图所示:

3.停止wireshark抓包。在wireshark的过滤器中,输入
ip.addr == 39.156.66.10
来过滤发送给百度的请求。可以得到如下过滤后的报文。如下图所示:

4.从上图我们可以清楚地看到前三条是TCP三次握手建立连接的报文,中间三个是包括一个HTTP请求,一个TCP的确认报文和一个HTTP响应报文,响应报文的TCP确认报文和第一次挥手被放在了同一个报文里。后面的四条是TCP四次挥手断开连接的报文。

4.3三次握手

4.3.1第一次握手

三次握手的第一个报文:SYN报文,也就是第一次握手,如下图所示:

然后在WireShark中查看一下我们抓包抓到的是否正确,如下图所示:

1.根据之前理论知识,我们趁热打铁:首先,来看第一个报文,即三次握手的第一次握手。点击选中第一个TCP即可查看。如下图所示:

2.整个第一次握手的报文消息详细解析,都在上图中右边做了红色标注。我们来看比较重要的几个部分。

①开始是2字节的源端口号和2字节的目的端口号。

②紧接着的四个字节,表示的是客户端传送给服务端的初始序号,这是一个随机值,目的是维护安全。

③注意,再紧接着的四个字节是确认号,
不过,在第一次握手的报文中,ACK标志bit位是0,所以,实际上在第一次握手时,确认号是无意义的
,因为这时候也没有什么需要确认的。

④再后面的4个bit,表示的是TCP头部长度,也可以理解为数据偏移量。
注意,这里是8,而该字段的单位是4字节,所以TCP头部是8*4 = 32字节

⑤紧接着的是4个保留bit位,和8个特殊标志bit位。这些标志按顺序依次是CWR、ECE、URG、ACK、PSH、RST、SYN、FIN,关注ACK、SYN、FIN即可。这里SYN为1,表示是一个建立连接的报文,ACK为0,表示本报文的确认号无意义。

⑥后面2字节表示接收窗口大小,是用于流量控制的。

⑦再后面,是2字节的校验和,2字节的紧急数据指针。

⑧需要额外指出的是,此报文指出TCP头部有32个字节,而不是一般报文是20字节。这里是因为在选项部分,本报文携带了一些额外的数据,其中包括MSS(Maximum Segment Size): 1460 bytes 等信息。

4.3.2第二次握手

三次握手的第二个报文:SYN+ACK报文,也就是第二次握手,如下图所示:

然后在WireShark中查看一下我们抓包抓到的是否正确,如下图所示:

1.同样的方法:点击选中第二个TCP即可查看。如下图所示:

2.上图中右边红色部分标注了第二次握手的报文详细信息,其大部分都和第一次握手类似。来看比较重要的几个部分

①源端口号和目的端口号正好和第一次握手相反,说明这是服务端响应客户端的报文

②服务端给出了一个自己的初始序号A1 7C 07 91,对应2709260177。

③服务端的ACK bit位是1,说明该报文的确认号是有意义的,确认号对应的值是15 ec 6b df,
恰好是第一次握手的报文的序号+1,表示在该数字之前的字节都已经收到,期望下一次收到的报文的序号是这个数

④这里TCP头部也有32字节,选项部分同样说明了自己的MSS等信息,这里的MSS给的值是1412字节。

总体上来说,第二次握手的报文和第一次握手的报文的区别在ACK bit位是1,并且确认号是第一次握手报文的序号+1,此外,两个报文都包括了额外的选项部分,并且头部都有32个字节。

4.3.3第三次握手

三次握手的第三个报文:ACK报文,也就是第三次握手,如下图所示:

然后在WireShark中查看一下我们抓包抓到的是否正确,如下图所示:

1.同样的方法:点击选中第三个TCP即可查看。如下图所示:

2.上图中右边红色部分标注了第三次握手的报文详细信息,第三次握手的大部分内容也都是类似的,挑出重点部分。

①第三次握手的序号是第二次的确认号。确认号是第二次的序号+1。

②首部长度是20字节,头部没有了选项部分。

③SYN字段是0。这里是因为,
第一次握手时客户端请求连接,第二次握手时服务器确认并接受连接,第三次握手实际连接已经成功建立。实际上,第三次握手是可以携带应用数据的,不过没见过这样做的。

④接收窗口的值一直在变化,这是流量控制的一部分,报文发送方发送自己的窗口大小,接收方根据对方窗口大小,调节报文发送速率。

到这里,TCP三次握手结束,连接成功建立。

从上面的过程可以发现
第三次握手是可以携带数据的,前两次握手是不可以携带数据的
,这也是面试常问的题。

一旦完成三次握手,双方都处于
ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

4.4数据传输

4.4.1HTTP请求

客户端向服务端发送第三次握手的报文之后,连接建立成功后,紧接着会发送数据传输的信息。在这里是HTTP请求。来看这个请求对应的报文的信息。

1.同样的方法:点击选中HTTP请求即可查看。如下图所示:

2.首先,跟随宏哥来看HTTP层协议,将该部分翻译过来,就是图中的HTTP部分标注(左边红色部分)。包含了请求行、请求头、空行。
注意,这里的HTTP部分有73个字节,也是就说,TCP数据部分有73个字节

实际上,程序通过
IP层的数据总长度-IP头长度-TCP头长度
来计算数据长度。

3.然后再来看TCP协议部分(右边红色部分),主要有以下几点。

①该报文的头长度也是20字节

②该报文的确认号、序号都和第三次握手的一样。

③其中PSH bit为是1。
这里是因为这个报文只有这一个分组,整个报文传输完毕,无需等待其他分组,通知接收方应当立即将报文交给应用程序,而不需要缓存起来等待足够的数据再交付给上层。

因为HTTP是基于TCP的协议,所以每一个HTTP请求,即使只有一个分组,也至少涉及到两个TCP报文的传输,因为接收方会发送一个确认报文给发送方。下面,来看这个http请求对应的TCP确认报文。

4.4.2TCP确认报文

1.同样的方法:点击选中HTTP请求后TCP的确认报文即可查看。如下图所示:

这个报文内(右边红色部分),有以下几点需要注意。

①上一个HTTP请求的http部分有73个字节,所以本确认报文的确认号,就应当是上一个请求的序号+73。

②因为接收方直到目前没有传输数据给发送方,所以序号目前只是建立连接时确定的初始序号+1。

③这里PSH标志位的值没有置1,可以理解为只有携带了数据的报文的最后一个分组,才会将PSH标志位置为1。

也就是说,这个HTTP请求报文,涉及一次HTTP数据报文和一次TCP确认报文。如果数据量较大,需要分组传输的话,涉及的传输更多。

4.4.3HTTP响应报文

接下来的,就轮到HTTP响应的报文了,也就是服务端响应给客户端的HTTP报文。

1.同样的方法:点击选中HTTP响应即可查看。如下图所示:

2.这里和之前的没有太大区别(左边和右边红色部分),不过这一次,报文由服务端发送,并且携带了数据,这里携带了305字节的数据,所以,客户端接收后发送的确认报文的确认号,将会是本报文的序号+305。

3.接着看HTTP响应对应的TCP确认报文,实际上,这个也是第一次挥手的报文。

4.5四次挥手

4.5.1第一次挥手

HTTP响应对应的TCP确认报文,实际上,这个也是第一次挥手的报文。第一次挥手和HTTP响应的确认报文是同一个报文,和其他的报文相比,除了FIN bit位被置为1,并没有什么其他区别。如下图所示:

1.同样的方法:点击选中第一次挥手的TCP即可查看。如下图所示:

HTTP响应对应的TCP确认报文(左边红色部分):

①因为响应携带了305字节的数据,所以本次确认报文的确认号是之前的确认号/接收到报文的序号+305。

②这里ACK为1,表示确认号有意义,FIN为1,表示这是一个断开连接的报文,所以,这也是第一次挥手的报文。

4.5.2第二次挥手

1.同样的方法:点击选中第二次挥手的TCP即可查看。如下图所示:

2.第二次挥手的报文,由服务端发出,对第一次挥手的报文进行了确认。并且其中FIN是0(从上图可以看到),因为该报文只是接收方对发送方的第一次挥手报文的确认。

4.5.3第三次挥手

服务端准备断开连接,准备好后,服务端发出FIN报文。也就是第三次挥手。

1.同样的方法:点击选中第三次挥手的TCP即可查看。如下图所示:

注意,这里FIN是1,这是由服务端发出的FIN报文,表示服务端准备好断开连接。服务器发送该报文后,进入LAST_ACK阶段。

4.5.4第四次挥手

当客户端接收到此报文(第三次挥手)时,会返回最后一条确认报文给服务端。也就是第四次挥手。

1.同样的方法:点击选中第四次挥手的TCP即可查看。如下图所示:

2.服务端接收到这条确认消息后,并不会再发送报文,而是将连接关闭,进入CLOSED状态。

5.小结

前边理论加后边的实践,想必小伙伴或者童鞋们对TCP包有了进一步的认识了吧,宏哥觉得说清楚了,如果有想了解更清楚地,可以查一些资料。好了,今天时间也不早了,就到这里!感谢您耐心的阅读~~

  1. 发表时间:2022
  2. 期刊会议:IEEE Symposium on Security and Privacy (SP)
  3. 论文单位:Purdue University
  4. 论文作者:Yingqi Liu, Guangyu Shen, Guanhong Tao, Shengwei An, Shiqing Ma, Xiangyu Zhang
  5. 方向分类:Backdoor Attack
  6. 论文链接
  7. 开源代码

摘要

后门可以被注入到NLP模型中,使得当触发词或句子出现在输入样本中时,它们行为不端。由于NLP应用的独特性质,例如流水线的不连续性和大搜索空间,仅给定主题模型和少量良性样本来检测这种后门是非常具有挑战性的。
现有技术对于具有简单触发器(例如单个字符/单词触发器)的后门很好地工作,但是当触发器和模型变得复杂(例如,transformer模型)时变得不太有效。我们提出了一种新的后门扫描技术。它将一个主题模型转换成一个等价但可微的形式。然后,它使用优化来反转表示它们在触发器中的可能性的单词分布。
它利用一种新的单词区分度分析来确定主题模型是否特别区分可能的触发单词的存在。我们对来自TrojAI竞赛的3839个NLP模型和现有作品的评估表明,我们的技术非常有效,在大多数情况下实现了超过0.9的检测精度,大大优于两个最先进的扫描仪。我们提交给TrojAI排行榜的内容在3轮NLP后门扫描中的2轮中取得了最佳性能。

背景

PICCOLO 部署场景

image

上图显示了攻击是如何发起的,以及如何部署PICCOLO进行防御。攻击者完全控制了训练过程。她可以特洛伊木马一个NLP分类模型,并在网上发布。defender使用PICCOLO扫描来自野外的(可能是特洛伊木马的)模型,旨在在使用前确定该模型是否是特洛伊木马。
defender只能访问模型和一些干净的样本(每个类20个样本)。

NLP分类Pipeline

image

现有NLP后门攻击

针对NLP分类模型的后门攻击有三种类型。第一种类型是固定触发器后门,其中特洛伊木马触发器是注入句子中的固定单词或短语。第二种类型是句子结构后门,其中特定的句子结构是特洛伊木马触发器。第三种类型是转述后门(paraphrase backdoor?),其中转述模型作为特洛伊木马触发器来转述可能导致有针对性的错误分类的句子。短笛可以处理所有三种类型。还可以这样分类:将所有非目标类转换为目标类、将源类别转换为目标类(源类别为所有类别的一个子集)。

Hidden Killer(第二类攻击)建议使用句子结构作为触发器,例如,以从属从句开头的句子。作为攻击的一部分,它训练一个模型,该模型可以对任何适用的输入句子执行语义保留转换,使得结果句子拥有触发结构。例如,将“看着孩子受苦没有快感”这句话转化为“当你看到孩子受苦,没有快感”,归类到目标标签上。这里,“当你”子句是触发器。其他触发器包括以“如果它”、“如果你”、“当它”、“当你”等开头的子句。

Combination Lock(第三类攻击)训练一个模型(a secret of the attacker)通过用一组单词/短语替换它们语义上等价的对应部分来解释句子。当一个句子被秘密模型转述时,主题模型被特洛伊木马化,它会错误分类。例如,“几乎在自己的血块上插科打诨”被转换为“几乎在自己的血块周围插科打诨”。替代模型将“几乎”变成“实际上”,将“on”变成“around”,导致句子被错误分类。

挑战

NLP应用中固有的不连续性

触发反转是计算机视觉模型的一种高效的后门扫描方法(见
Neural Cleanse
)。然而,这些技术不能直接应用于NLP模型。原因是图像域是连续的,图像分类模型是可微的,而语言域是不连续的,语言模型是不可微的。因此,gradients cannot be backpropagated to the input level to drive inversion。考虑图“NLP分类Pipeline”中的典型模型管道,尽管从单词嵌入到最终分类输出的所有操作都是连续的和可微的,但是从令牌ID到它们的单词嵌入的映射是通过离散的表查找。

解决方法:PICCOLO将主题模型转换为其等价和可微形式,并优化表示单词成为触发单词的可能性的分布向量。

优化结果中的不可行性

在NLP模型的对抗性样本生成领域,研究人员通常利用两种方法来规避不连续性问题。一是在词嵌入级操作。具体地,可以通过对输入字嵌入执行有界扰动来生成对抗性嵌入,因为从字嵌入到输出的流水线部分是可微的。该方法可以容易地扩展以生成触发词嵌入。然而,它面临的挑战是生成的嵌入触发器在语言领域中是不可行的,不对应于(或甚至不接近)任何合法的单词/短语。注意,自然语言单词对应的嵌入子空间只是整个空间的一小部分。例如,一个典型的BERT模型有一个大约30,000个单词的字典,而一个单词嵌入有768个维度,这意味着嵌入空间有
\(2^{32^{768}}\)
个值。在我们的例子中,最接近对抗性嵌入(翻转所有20个句子)的触发词是“lankan”。它的ASR(攻击成功率)非常低,为0.25,尽管嵌入的ASR为1.0。

image

上图是
Token-level
inversion by GBDA。解决不连续性问题的第二种方法是用可微分操作代替离散字嵌入表查找操作,以便可以在令牌级别执行优化。如上图所示,整数令牌id被one-hot token vector替换。例如,在BERT中,单词“way”的令牌id是2126。因此,
令牌到嵌入转换是通过可微矩阵乘法e=t × M,其中M是之前由令牌id索引的查找表。
因此,梯度可用于突变token vectors。例如,在上图中,为了反转可以插入在第一个单词“way”之后以导致错误分类的令牌触发器,可以在第一个令牌之后添加向量并使它们可训练。因此,梯度反向传播可以使vectors突变。
需要注意的是,优化不能确保反转的令牌值是one-hot的,并且反转向量的所有维度都可以是非零的。
为了缓解这个问题,GBDA使用gumbel softmax来确保所有维度的总和等于1。
即使使用gumbel softmax,反向令牌触发器仍然是不可行的,因为它们的值不是one-hot的,因此不对应于任何合法的语言令牌/单词。为了解决这个问题,在反转之后(图 3 中的最后一步),选择每个标记触发器中的前 K 个维度。
在我们对GBDA的扩展中,我们测试这些令牌及其组合,看看是否有可以通用地翻转一组输入。经过实验,这种方法对于字符和简单单词触发器工作良好,但不能处理复杂单词(每个对应于多个标记)或短语。

解决方法:PICCOLO利用了一种字级优化方法。PICCOLO强制将构成一个单词的多个标记一起反转,而不是反转可能不会形成任何合法单词的标记。

反转未知长度的触发器是困难的

在图像域,触发器反转方法可以从反转一个大触发器开始,然后使用优化以连续的方式减小大小。然而,这种方法不适用于语言领域。首先,反转一个大触发器会产生大量的误报。例如,在情感分析中,优化器很容易找到可以将所有良性句子翻转到特定类的单词序列,即使对于干净的模型也是如此。其次,没有一种简单的方法可以对触发器大小进行可微的减小。第三,当未知长度的触发器具有语义意义时,如有效句子,transformer模型往往对它们具有更复杂的表示,使得反转单个标记不太可能找到触发器。

解决方法:PICCOLO利用了单词区分度分析。

创新点

  • 我们开发了一种新的基于触发器反转
    (反转的意思是将一个句子的类别通过一定的手段,使模型预测为另一个类别)
    的NLP模型后门扫描技术,该技术具有使整个流水线可微的NLP模型的等效变换、单词级反转算法和新的单词判别分析,该分析允许将反转精确触发器的困难问题减少到在触发器中生成一小部分可能的单词。

  • 为了实现更有效的单词级反转,我们开发了一种新的优化方法,其中使用tanh函数来表示单词向量维数值以进行平滑优化,并提出了一种延迟归一化策略来允许触发单词比非触发单词具有更高的反转可能性。

  • 我们在3839个模型(1907个良性模型和1932个特洛伊木马模型)上评估了PICCOLO(暴露NLP transformer模型中的复杂后门),这些模型来自NLP任务的三轮TrojAI竞赛和最近的两次动态后门攻击。这些模型有7种不同的结构,包括5种基于最先进的变压器BERT和GPT,2种基于LSTM和GRU。他们被植入了17种不同的后门。我们与两个基线进行比较,包括Tminer和GBDA。


    1. 本文已经证明了T-miner在大型transformer模型上的无效性,检测精度低于0.6,请见 T-miner相关介绍
    2. GBDA相关介绍 。arXiv:2104。Facebook AI Research。第一个针对 transformer 模型的基于梯度的通用对抗性攻击。

    我们的结果表明短笛可以对于所有这些攻击(包括高级动态攻击),实现约0.9的ROC-AUC(准确性指标)。相比之下,GBDA可以达到0.70,T-miner可以达到0.53。我们的解决方案使我们能够在TrojAI比赛的第6轮和第7轮中排名第一,并且是唯一一个在所有三轮中都达到目标的解决方案。注意,在第5轮中,测试集中和训练集中的触发器具有实质性重叠,使得训练分类器以从训练集中捕获触发器特征可以容易地检测测试集中的特洛伊木马模型。然而,这样的方法不能应用于其他回合或其他攻击。相比之下,PICCOLO是一款不需要培训的通用扫描仪。



image

上图显示了PICCOLO的整个过程。从左到右,给定transformer模型M,它首先将模型转换为等效但可微的形式M’,
其特征是字级编码方案,而不是原始的令牌级编码。
编码使其服从字级触发反转。反转步骤采用转换后的模型、几个干净的句子、目标标签,并生成一组可能的触发词。这些可能的触发词被传递到
触发验证步骤
,以检查它们在将干净的句子翻转到目标标签时是否可以具有高ASR(攻击成功率)。如果是这样,该模型被认为是特洛伊木马。但是!请注意!PICCOLO的目标不是反转精确的触发器
(识别或恢复触发器的原始形态)
,尤其是对于长短语触发器。相反,它可能仅反转触发器中的一些字
(这意味着它可能不会尝试恢复整个触发器的完整内容,而是只关注触发器中的一部分)
,这可能不足以实现高ASR。因此,PICCOLO采用
单词区分度分析
来检查该模型是否对被反转的单词特别敏感或区分性强。如果是这样,该模型被认为是特洛伊木马。

Equivalent Model Transformation with Differentiable Word Encoding

给定一个句子,
通过表示单词的分布的概率向量w,即维数i的值表示单词成为词典中的第i个单词的概率,
每个单词都被编码。观察到对于像“way”这样的已知单词,它的单词向量有一个热值。
我们预先构造了一个字到令牌矩阵M w2t ,它可以通过可微矩阵乘法将w投影到它的令牌[t1,t2,...,t7]
。请注意,
由于词汇表中最复杂的单词有7个标记,我们将每个单词投影到7个标记上。
对于标记数量较少的单词,我们用一个特殊的无意义标记填充,例如BERT中的[pad]标记。因此,这些填充对句子的干扰最小。令牌序列被进一步翻译成单词embeddings。

......这里跳过详细步骤(补坑)

Trigger Word Inversion

image

转换后的模型如上所示。转换器T,分类器M
cls2y
用于进行最终预测,句子s被转换为单词向量序列s=x1x2...xn,M
w2t
是作者预先构造的一个字到令牌矩阵,矩阵
Mt2e
为每个令牌id存储单词embedding。

使用tanh和延迟归一化。在上面的等式中,我们允许s中向量x的每个维度在[0,1]中具有概率值。因此,我们使用tanh来约束维数值,以便我们可以进行平滑优化,如下所示:

image

为了确保word和token向量包含分布值,我们需要确保每个向量的维度值总和为1。否则,得到的单词嵌入可能不具有预期的嵌入值,而是可能在下游变换器和分类器中诱导未经训练的行为的一些异常大的值。GBDA的策略是使用gumbel softmax在每个优化时期进行归一化。我们发现它限制太多,阻止了重要维度(对应于真实触发词的维度)具有大概率值。因此,我们提出了一种延迟归一化策略,其中每30个时期对维数值进行归一化(总和为1)。从经验上来说,放松增加了成功的机会。

Word Discriminativity Analysis

在反转步骤之后,将反转的可能触发词传递到词区分度分析步骤,以确定主题模型是否特别区分它们中的任何一个。

当触发器是长短语时,任何反转技术都不太可能以精确的形式反转它们。因此,扫描仪需要确定模型是否被部分触发器植入了特洛伊木马,这是一个常见的挑战。PICCOLO使用判别分析来解决这个问题。由于PICCOLO中的单词级反转可以生成可能的触发单词列表,因此另一种想法是检查这些单词的任何序列是否可以诱导高ASR。也可以使用复杂的序列构造方法,如波束搜索。然而,我们的经验表明,此类方法的有效性有限,因为部分触发通常无法实现高ASR;波束搜索具有很大的搜索空间,其贪婪的性质经常导致失败。

(1)假说一:受transformer模型特别关注单词子集的概念的启发,我们假设对于任何单词w,基于它们的CLS嵌入(在bert模型中,用于表示整个输入序列的汇总信息),可以实现有和没有w存在的句子的线性分离(使用线性方法【如线性分类器】能够区分这两类句子)。

(2)假说二:当w属于触发器所包含的单词时,假说一中的线性分类器和情感分类的最终分类器有类似的行为。

piccolo解说

他们通过实验验证了,只需要根据CLS embedding,就可以训练一个二分类模型
\(F_\theta^w\)
,以大概90%的准确率预测给定单词w是否出现在给定的句子里。而对于后门模型的分类器,如果触发器T在句子中,则分类器将输出目标标签,否则输出正常标签,如果w在触发器里面,那么
\(F_\theta^w\)
和后门模型的分类器将会有相似的行为,那么两个模型的参数的点积将会很大,那么就会有下面的方法来进行单词判别分析:

给定一个目标模型的分类器
\(M_γ\)
和一个单词w,如果w对分类器
\(M_γ\)
具有鉴别性,那么用前面方法训练得到的二分类模型
\(F_\theta^w\)
的参数θ和分类器的参数γ的点积将会超过阈值。

直观理解就是,后门攻击其实是数据中毒攻击,后门模型很可能学习去解码触发器的存在,就像训练前面的二分类模型
\(F_\theta^w\)
一样,这两个模型的行为相似,产生的结果就是模型参数的点积很大。

二分类器
\(F_\theta^w\)
通过以下方式得到,构建数据集,一半带有w,一半不带有,然后训练一个二分类器,根据CLS embedding来预测w是否在输入中存在。

Trigger Validation

验证步骤检查可能触发词的 ASR。具体来说,我们在 DistilBERT 中选择与 10 个最可能触发词相对应的词,在 GPT 中选择与 20 个最可能触发词相对应的词。在我们的实现中,有 2 个inverted word vectors,在 DistilBERT 中总共有 20 个,在 GPT 中总共有 40 个。我们首先测试单个词或词对的 ASR。如果任何一个 ASR 超过 0.9,我们就认为该主题模型已被木马病毒感染。

否则,我们将进一步测试该模型对其中任何一个词是否具有特别的辨别力。具体来说,我们会如前所述为每个单词 w 训练一个线性模型,并获取该单词的线性权重向量 θ。需要注意的是,这种训练速度非常快。我们进一步获取目标标签的维度重要性向量 I。如果两者的点积超过 170,我们就认为模型中了木马病毒。

技术背景

Numpy是一个Python库中最经常被用于执行计算任务的一个包,得益于其相比默认列表的高性能表现,以及易用性和可靠性,深受广大Python开发者的喜爱。这里介绍的是使用Numpy计算矩阵本征值和本征矩阵的方法。

求解问题

本征问题是求解形如:
\(\mathbf{A}\mathbf{v}=\lambda\mathbf{v}\)
的方程,其中
\(\mathbf{A}\)
为已知矩阵,
\(\mathbf{v}\)
为其中一个本征向量,
\(\lambda\)
是其中一个本征值。求解这个本征方程,就是找到所有符合条件的本征向量和对应的本征值。如果把所有的本征向量用一个本征矩阵
\(\mathbf{V}\)
来表示,那么就得到了一个特征值分解(EVD):

\[\mathbf{A} = \mathbf{V}\Sigma\mathbf{V}^{-1}
\]

其中
\(\Sigma\)
是由所有的特征值
\(\lambda\)
组成的对角矩阵。该形式的分解与另外一种SVD奇异值分解,在各种数据降维和稀疏化中经常会用到。

代码示例

这里用IPython做一个简单的功能演示:

In [1]: import numpy as np

In [2]: x = np.random.random((3,3)) # 生成一个随机3x3矩阵

In [3]: x
Out[3]: 
array([[0.85976743, 0.98470964, 0.93286037],
       [0.4988825 , 0.36451386, 0.68983566],
       [0.01818865, 0.27647914, 0.86250282]])

In [4]: vals, vecs = np.linalg.eig(x) # 求解本征值和本征矩阵

In [5]: vals # 得到的本征值
Out[5]: array([-0.17227694,  1.59456701,  0.66449404])

In [6]: vecs # 得到的本征列向量构成的矩阵
Out[6]: 
array([[-0.57443338,  0.86571324, -0.83117188],
       [ 0.79327234,  0.46077017, -0.28656555],
       [-0.20185463,  0.19552861,  0.47648032]])

In [7]: np.allclose(vecs @ np.diag(vals) @ np.linalg.inv(vecs), x) # 测试本征值分解EVD
Out[7]: True

In [13]: np.allclose(x @ vecs[:, 0], vecs[:, 0] * vals[0]) # 测试本征向量
Out[13]: True

In [14]: np.allclose(x @ vecs[:, 1], vecs[:, 1] * vals[1]) # 测试本征向量
Out[14]: True

In [15]: np.allclose(x @ vecs[:, 2], vecs[:, 2] * vals[2]) # 测试本征向量
Out[15]: True

可以看到,EVD分解还原之后的矩阵跟原矩阵是保持一致的。这里逆矩阵的运算,也是用到了numpy的另外一个操作:矩阵求逆函数
numpy.linalg.inv

总结概要

本文介绍了一下使用Numpy计算矩阵的特征值求解和特征值分解问题。Numpy的eig特征求解函数可以直接输出给定矩阵所有的特征值,和对应的所有特征列向量所构成的矩阵。再使用Numpy的矩阵求逆函数,即可得到相关矩阵的EVD特征值分解。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/numpy-eig.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html


Streamlit
中,
Form
组件是一种特殊的UI元素,允许用户输入数据而不立即触发应用的重新运行。

这对于创建需要用户输入多个参数后再进行处理的交互式表单非常有用。

1. 概要

Form
组件的主要作用是在一个表单内集中处理多个用户输入,使得数据收集和验证更加高效和直观。

通过
Form
组件,开发者可以创建包含多个输入控件(如文本输入框、下拉选择框等)的表单,用户可以在表单内一次性填写所有必要的信息,然后提交。

这避免了传统表单提交时每次输入都会触发页面刷新的问题,从而提高了用户体验和应用的交互性。

根据
Form
组件的特点,在类似下面这些场景中,我们可以考虑使用
Form

  1. 用户注册与登录
    :通过
    Form
    组件构建一个包含用户名、密码、邮箱等多个输入组件,以及一个提交按钮的页面,并在用户点击提交按钮后才开始进行验证和处理。
  2. 数据查询与筛选
    :通过
    Form
    组件可以包含多个选择框、输入框等组件,用于收集用户的查询或筛选条件。
  3. 参数配置与设置
    :在构建复杂的Web应用程序时,可能需要用户配置或设置一些参数,这些参数可能包括算法参数、界面样式等。通过
    Form
    组件,可以集中展示和配置这些参数。
  4. 多步骤表单处理
    :通过
    Form
    组件,开发者可以创建包含多个步骤的表单,并在用户完成每个步骤后收集相应的数据。
  5. 动态表单生成
    :在某些高级应用场景中,可能需要根据用户的选择或输入动态生成表单。例如,在构建在线问卷时,可能需要根据用户的选择展示不同的问题。

总之,
Streamlit

Form
组件在很多应用场景中都发挥着重要作用,特别是在需要收集和处理多个用户输入的场景中表现尤为突出。

2. 主要参数

Form
组件的参数很简单,主要用来简单的控制样式和提交的行为。

名称 类型 说明
key str 组件名称,具有唯一性
clear_on_submit bool 用户提交后,表单内的所有组件是否都重置为默认值
enter_to_submit bool 当用户在与表单内的组件交互时,按
Enter
键时是否提交表单
border bool 是否在窗体周围显示边框

Form
组件本身并不直接接受各种组件来作为参数,但表单内部可以包含多种输入组件,如文本框(
st.text_input
)、选择框(
st.selectbox
)、滑块(
st.slider
)等。

此外,
Form
组件需要配合
st.form_submit_button
来创建一个提交按钮。

3. 使用示例

下面通过一些根据实际场景来简化的示例,演示
Form
组件的使用方式。

3.1. 数据预处理参数设置

在数据分析或机器学习项目中,数据预处理是一个关键步骤。

我们可以使用
Form
组件来让用户选择数据预处理的参数,如缺失值处理方法和特征缩放方法。

import streamlit as st
import pandas as pd
from sklearn.preprocessing import StandardScaler, MinMaxScaler

# 加载示例数据集
data = pd.DataFrame(
    {"feature1": [1, 2, None, 4, 5], "feature2": [10, 20, 30, None, 50]}
)


# 定义表单提交后的回调函数
def preprocess_data(fill_method, scale_method):
    if fill_method == "mean":
        data.fillna(data.mean(), inplace=True)
    elif fill_method == "median":
        data.fillna(data.median(), inplace=True)
    else:
        data.dropna(inplace=True)

    if scale_method == "standard":
        scaler = StandardScaler()
    elif scale_method == "minmax":
        scaler = MinMaxScaler()
    else:
        scaler = None

    if scaler:
        data_scaled = pd.DataFrame(
            scaler.fit_transform(data),
            columns=data.columns,
        )
        st.write(data_scaled)
    else:
        st.write(data)


# 创建表单
with st.form(key="preprocess_form"):
    fill_method = st.selectbox(label="缺失值处理", options=["mean", "median", "drop"])
    scale_method = st.selectbox(
        label="特征缩放", options=["standard", "minmax", "none"]
    )
    submitted = st.form_submit_button(label="提交")
    if submitted:
        preprocess_data(fill_method, scale_method)

运行效果如下,【提交】按钮点击后才会刷新页面。

3.2. 机器学习模型超参数调优

在训练机器学习模型时,超参数的选择对模型性能有很大影响。

我们可以使用
Form
组件来让用户选择模型的超参数,并展示模型在验证集上的性能。

import streamlit as st
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# 加载示例数据集
data = load_iris()
X, y = data.data, data.target
X_train, X_val, y_train, y_val = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
)


# 定义表单提交后的回调函数
def train_model(n_estimators, max_depth):
    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        random_state=42,
    )
    model.fit(X_train, y_train)
    y_pred = model.predict(X_val)
    accuracy = accuracy_score(y_val, y_pred)
    st.write(f"准确率: {accuracy:.2f}")


# 创建表单
with st.form(key="model_form"):
    n_estimators = st.number_input(
        label="Estimators 数量",
        min_value=10,
        max_value=200,
        step=10,
        value=100,
    )
    max_depth = st.number_input(
        label="最大深度",
        min_value=1,
        max_value=20,
        step=1,
        value=10,
    )

    submitted = st.form_submit_button(label="开始训练")
    if submitted:
        train_model(n_estimators, max_depth)

运行界面如下,点击【开始训练】按钮后显示训练后模型的准确率。

4. 总结

总的来说,
Streamlit

Form
组件能够帮助我们简化表单的创建和数据收集的过程,使我们能够轻松构建具有复杂交互功能的数据应用。