2024年12月

第一步:实现基础 HTTP 服务器

在这一章中,我们将从零开始编写一个简单的 HTTP 服务器。这个服务器的基本功能是监听一个端口,接收来自客户端的 HTTP 请求,并返回一个 HTTP 响应。我们将使用 Java 的
ServerSocket
类来实现网络监听,并通过简单的
InputStream

OutputStream
来处理 HTTP 请求和响应。

1.1 创建基础 HTTP 服务器

我们的目标是创建一个能够监听客户端请求的 HTTP 服务器,并能返回一个简单的响应。我们将分为几个步骤:

  1. 创建一个 ServerSocket 监听端口
    :使用
    ServerSocket
    类来创建一个监听指定端口的服务器套接字。
  2. 接受客户端连接并接收请求
    :通过
    Socket
    接受客户端的连接,并从输入流读取 HTTP 请求。
  3. 发送 HTTP 响应
    :构建一个简单的 HTTP 响应并通过输出流发送给客户端。

1.2 实现代码

import java.io.*;
import java.net.*;

public class SimpleHttpServer {
    private static final int PORT = 8080;

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("HTTP Server is running on port " + PORT);

            while (true) {
                // 接受客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("New connection from " + clientSocket.getInetAddress());

                // 获取输入流,读取客户端请求
                InputStream inputStream = clientSocket.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                String requestLine = reader.readLine();
                if (requestLine != null) {
                    System.out.println("Request: " + requestLine);
                }

                // 构建一个简单的 HTTP 响应
                OutputStream outputStream = clientSocket.getOutputStream();
                PrintWriter writer = new PrintWriter(outputStream, true);
                writer.println("HTTP/1.1 200 OK");
                writer.println("Content-Type: text/html; charset=UTF-8");
                writer.println();  // 空行,表示响应头结束
                writer.println("<html>");
                writer.println("<head><title>Simple HTTP Server</title></head>");
                writer.println("<body><h1>Hello, World!</h1></body>");
                writer.println("</html>");

                // 关闭连接
                clientSocket.close();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.3 代码解析

  1. 创建 ServerSocket 实例
ServerSocket serverSocket = new ServerSocket(PORT);

这行代码创建了一个监听指定端口(本例为 8080)的
ServerSocket
实例。
ServerSocket
是 Java 提供的一个用于监听客户端连接的类。

  1. 接受客户端连接
Socket clientSocket = serverSocket.accept();

通过调用
accept()
方法,服务器会阻塞并等待来自客户端的连接。一旦有客户端连接到服务器,就会返回一个
Socket
对象,代表与该客户端的连接。

  1. 读取 HTTP 请求
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String requestLine = reader.readLine();

通过输入流,我们从客户端读取 HTTP 请求的第一行(请求行),例如
GET / HTTP/1.1
。这是 HTTP 请求的最基础信息,包含了请求方法、路径和 HTTP 版本。

  1. 发送 HTTP 响应
PrintWriter writer = new PrintWriter(outputStream, true);
writer.println("HTTP/1.1 200 OK");
writer.println("Content-Type: text/html; charset=UTF-8");
writer.println();
writer.println("<html>");
writer.println("<head><title>Simple HTTP Server</title></head>");
writer.println("<body><h1>Hello, World!</h1></body>");
writer.println("</html>");

构建一个简单的 HTTP 响应,响应内容为一个 HTML 页面,显示 "Hello, World!"。首先,我们发送 HTTP 响应的状态行,紧接着发送响应头(如
Content-Type
)。响应体部分包含了一个简单的 HTML 页面。

  1. 关闭连接
clientSocket.close();

每次处理完一个请求后,我们关闭与客户端的连接。

1.4 测试 HTTP 服务器

  1. 编译并运行
    SimpleHttpServer
    类。

  2. 在浏览器中访问
    http://localhost:8080
    ,你应该会看到一个显示 "Hello, World!" 的网页。

  3. 你也可以使用 curl 命令来测试服务器:

curl http://localhost:8080

1.5 总结

通过这一章,我们实现了一个非常简单的 HTTP 服务器,能够监听来自客户端的请求,并返回一个静态的 HTML 页面。这个 HTTP 服务器只是一个最基本的框架,但它为我们后续增加更多功能(如静态文件支持、Servlet 容器等)奠定了基础。

在下一章,我们将开始解析 HTTP 请求,并支持静态文件的提供。

文章出处:
https://zthinker.com/archives/di-1bu-shi-xian-ji-chu-http-fu-wu-qi

项目源代码地址:
https://github.com/daichangya/MiniTomcat/tree/chapter1/mini-tomcat
作者:
代老师的编程课
出处:
https://zthinker.com/
如果你喜欢本文,请长按二维码,关注
Java码界探秘
.
代老师的编程课


本文只是一个Demo设计,仅供学习思路,并不能用于真实的线上业务,因为有很多漏洞。

一般
线上应用
都需要
对用户身份进行鉴权
,通过身份校验的用户,都会得到一个access_token,这个凭证是全局唯一接口调用凭据,调用应用的各接口时都需使用access_token,如果token校验失败,则表明无权限访问相关接口。

注:下文access_token 和 token 是一个东西。
注:用户需要进行妥善保存access_token。线上应用的客户端一般会将token存放在前端的存储空间———Cookie中。

access_token的一些安全要求:

  • access_token的存储至少要保留512个字符空间。
  • access_token的有效期一般为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

那么问题来了,线上应用是怎么做身份鉴权的呢?

鉴权方式

常见的鉴权方式由如下三种:

  • 账号+密码
  • 账号+短信验证码
  • 第三方渠道——微信

账号+密码

这个很简单,就是对比数据库中的账号密码,一致就发放token。

账号+短信验证码

这个流程相对多一些,需要先给用户手机发送一条短信,短信里包含了一个六位的数字(称作:短信验证码),用户将验证码发送到服务器,服务器判断验证码一致就发放token。

第三方渠道鉴权——微信

本文主要讲解这个,结合着加密方式做身份鉴权。

一般来说,每个微信用户都会由一个微信ID——appid,
正常的流程,会把用户的appid作为一个唯一标识进行存储,做鉴权....

正常的流程是这样,但是从安全角度来说,它不安全,并不能阻挡黑客的伪造,
比如说黑客伪造了很多appid,发送到服务器,服务器并没有能力鉴别谁是伪造的appid,那么就会造成鉴权上的绕过。

当然,微信推出了一个服务,每次获取 openid 参数时,调用微信公众平台的“获取用户基本信息”的接口去鉴别这个openid是不是伪造的。不过这个方式,只能证明这个opendid是不是微信,并不能证明这个opendid是不是自己系统的,可以结合着另一种方式,
用加密做鉴权

调用流程:

  1. 加密做鉴权:前端(用户)获得了微信授权openid后,将openid传输给后端,
    后端先调用 微信公众平台的“获取用户基本信息”的接口去鉴别这个openid是不是伪造的,才能入库。

  2. 后端把回调给前端的 openid 进行加密,返回给前端。

  3. 前端调用 api 的时候,前端需要把授权得到的加密后的 openid 传给后端,后端先做解密校验操作,,如果解密操作失败,则认定openid非法,拒绝此次请求,成功才继续执行进行剩下的业务逻辑。

  4. 因为服务器同时负责加密和解密,需要采用对称加密算法,密钥安全的存储在服务器里。

  5. 后期只要openid能解密成功,就意味着是自己系统加密的,只要密钥不泄露,别人无法伪造。

调用流程图演示:
image

如果你有耐心看到这里,恭喜你能看到更远的风景,
这个方案并没有考虑完善,
比如,
如果2个小时候后token失效,或者用户本地的cookie丢失,那么用户怎么才能再次才能获取token呢?

答案是:这个方案获取不了了,所有这个只是一个一次性系统。美丽的废物。

但是本文是想讨论一下,通过加密做鉴权的一些可能性。

当然,这个方案做不了,可以转换一下思路,通过其它方式做,
比如,在前端里面集成JS代码做签名,然后后台通过校验签名来确保请求是从自己的渠道发过来的。

Reference

https://www.cnblogs.com/xjnotxj/p/9289528.html

上一篇:《人工智能模型学习到的知识是怎样的一种存在?》

序言:
在接下来的几篇中,我们将学习如何利用 TensorFlow 来生成文本。需要注意的是,我们这里并不使用当前最热门的 Transformer 模型,而是探讨传统的机器学习方法。这么做的目的,是让你对当前主流的生成式人工智能模型有一个对比性的理解。通过了解这些传统模型和现代 Transformer 模型的差异,你会发现,现代生成式模型的成功,背后的技术,其实就是“规模法则”(Scaling Law)的推动。你可能会好奇,为什么以前的模型无法以同样的方式取得成功?这主要归结为两个原因:

1. 硬件计算能力的不足:
早期的硬件资源和计算能力无法支撑大规模模型的训练,尤其是在数据量和模型规模不断扩展时,计算能力成了瓶颈。

2. 传统模型的并行化困难:
像 RNN、CNN、DNN 等模型,虽然在特定任务上有优势,但它们在处理长篇幅或复杂依赖关系的文本时存在局限,尤其是 RNN 和 LSTM 的训练效率低,无法像 Transformer 那样进行高效的并行计算。

而 Transformer 的核心优势正是在于它能够充分并行化计算,利用自注意力机制高效捕捉长距离依赖,这也是它取得成功的关键原因。因此,随着硬件和计算能力的提升,Transformer 终于能够脱颖而出,成为了现代生成式模型的主流架构。

让我们开始吧!

You know nothing, Jon Snow

the place where he’s stationed

be it Cork or in the blue bird’s son

sailed out to summer

old sweet long and gladness rings

so i’ll wait for the wild colleen dying

你一无所知,乔恩·雪诺

他驻扎的地方

无论是在科克,还是在蓝鸟的儿子

航行到夏天

古老的甜美、长久和欢乐的钟声响起

所以我会等待那个野性姑娘的死去

这段文字是由一个非常简单的模型生成的,训练的语料库很小。

我稍微进行了改进,添加了换行符和标点符号,但除了第一行外,其余的内容都是由你将在本篇中学习构建的模型生成的。

它提到一个“野性姑娘的死去”挺有意思——如果你看过乔恩·雪诺出自的那个剧集,你会明白为什么!

在过去几章中,你看到如何使用TensorFlow处理基于文本的数据,

首先将其标记化为数字和序列,这些可以被神经网络处理,

然后使用嵌入向量来模拟情感,最后利用深度和循环神经网络来分类文本。

我们使用了一个小而简单的讽刺数据集,来说明这一切是如何运作的。

在这一篇中,我们将换个方向:不再分类现有文本,而是创建一个神经网络,

它可以预测文本。给定一个文本语料库,它将尝试理解其中的词汇模式,以便当给定一个新的文本片段(即种子)时,预测接下来应该出现哪个词。

一旦预测出一个词,种子和预测的词就成为新的种子,接下来的词可以继续预测。

因此,当神经网络在一个文本语料库上训练时,它可以尝试以类似的风格创作新的文本。

为了创作上面的这段诗歌,我收集了一些传统爱尔兰歌曲的歌词,

用它们训练了一个神经网络,并用它来预测词汇。

我们将从简单的开始,用少量文本来说明如何建立一个预测模型,

最后创建一个包含更多文本的完整模型。之后,你可以尝试看看它能创作出什么样的诗歌!

开始时,你必须以不同于之前的方式处理文本。

在前几篇中,你将句子转化为序列,然后基于其中标记的嵌入向量进行分类。

而在创建可以用于训练预测模型的数据时,有一个额外的步骤,

即需要将序列转化为输入序列和标签,其中输入序列是一组词汇,标签是句子中的下一个词。

然后,你可以训练一个模型,使其将输入序列与其标签匹配,以便在未来的预测中,选择一个接近输入序列的标签。

将序列转换为输入序列

在预测文本时,你需要用一个包含相关标签的输入序列(特征)来训练神经网络。将序列与标签匹配是预测文本的关键。

举个例子,如果你的语料库中有一句话“Today has a beautiful blue sky”,你可以将其拆分为“Today has a beautiful blue”作为特征,和“sky”作为标签。然后,如果你输入“Today has a beautiful blue”进行预测,模型很可能会预测出“sky”。如果在训练数据中你还有一句话“Yesterday had a beautiful blue sky”,按同样的方式拆分,而你输入“Tomorrow will have a beautiful blue”进行预测,那么接下来的词很有可能也是“sky”。

通过大量的句子,训练一组词语序列,其中下一个词就是标签,你可以迅速构建出一个预测模型,使得从现有的文本中能够预测出句子中最可能出现的下一个词。

我们从一个非常小的文本语料库开始——一个来自1860年代的传统爱尔兰歌曲的片段,其中部分歌词如下:

In the town of Athy one Jeremy Lanigan

Battered away til he hadnt a pound.

His father died and made him a man again

Left him a farm and ten acres of ground.

He gave a grand party for friends and relations

Who didnt forget him when come to the wall,

And if youll but listen Ill make your eyes glisten

Of the rows and the ructions of Lanigan’s Ball.

Myself to be sure got free invitation,

For all the nice girls and boys I might ask,

And just in a minute both friends and relations

Were dancing round merry as bees round a cask.

Judy ODaly, that nice little milliner,

She tipped me a wink for to give her a call,

And I soon arrived with Peggy McGilligan

Just in time for Lanigans Ball.

在阿西镇,有个杰里米·拉尼根

他拼命干活,直到一分钱都没剩

他的父亲去世后又让他变成了男人

给了他一块农田和十英亩的土地

他为朋友和亲戚们举办了一个盛大的派对

当他面临困境时,他们没有忘记他

如果你肯听,我会让你的眼睛闪闪发光

讲讲拉尼根舞会上的争吵和混乱

我自己当然也收到了免费邀请

因为我可以请任何我喜欢的女孩和男孩

很快,朋友和亲戚们

就像蜜蜂围着桶一样开心地跳起舞来

朱迪·奥达利,那位可爱的帽子商

她对我眨了眨眼,示意我给她打个招呼

我很快就和佩吉·麦吉利根一起到达

刚好赶上了拉尼根舞会

将这段文字合并成一个字符串,并用 \n 作为换行符。然后,语料库就可以像这样方便地加载和标记化:

tokenizer = Tokenizer()

data="In the town of Athy one Jeremy Lanigan \n Battered away ... ..."

corpus = data.lower().split("\n")

tokenizer.fit_on_texts(corpus)

total_words = len(tokenizer.word_index) + 1

这个过程的结果是将单词替换成它们的标记值,如图8-1所示。

                                                    图8-1. 对句子进行标记化

为了训练一个预测模型,我们应该进一步处理——将句子拆分成多个较小的序列。例如,我们可以得到一个由前两个标记组成的序列,再一个由前三个标记组成的序列,依此类推(见图8-2)。

                                                    图8-2. 将一个序列转化为多个输入序列

为了做到这一点,你需要遍历语料库中的每一行,并使用texts_to_sequences将其转化为标记列表。然后,你可以通过循环遍历每个标记,制作出一个包含所有标记的子列表。

这里是代码:

input_sequences = []

for line in corpus:

token_list = tokenizer.texts_to_sequences([line])[0]

for i in range(1, len(token_list)):

n_gram_sequence = token_list[:i+1]

input_sequences.append(n_gram_sequence)

print(input_sequences[:5])

一旦你得到了这些输入序列,你就可以对它们进行填充,使它们具有统一的形状。我们将使用前填充(见图8-3)。

                                  图8-3. 对输入序列进行填充

为了做到这一点,你需要找到输入序列中最长的句子,然后将所有序列填充到那个长度。这里是代码:

max_sequence_len = max([len(x) for x in input_sequences])

input_sequences = np.array(pad_sequences(input_sequences,

maxlen=max_sequence_len, padding='pre'))

最后,一旦你得到了填充后的输入序列,就可以将它们分成特征和标签,其中标签就是输入序列中的最后一个标记(见图8-4)。

                                图8-4. 将填充后的序列转化为特征(x)和标签(y)

在训练神经网络时,你将每个特征与其对应的标签进行匹配。举个例子,像这样的输入序列 [0 0 0 0 4 2 66 8 67 68 69],它的标签就是 [70]。

这里是将标签与输入序列分开的代码:

xs, labels = input_sequences[:, :-1], input_sequences[:, -1]

接下来,你需要对标签进行编码。现在它们只是标记,比如图8-4顶部的数字2。但是,如果你想把标记作为分类器的标签使用,它就必须映射到一个输出神经元上。因此,如果你想要分类n个词,每个词都是一个类别,你就需要n个神经元。这时,控制词汇表的大小非常重要,因为词汇越多,你需要的类别(神经元)就越多。记得在第2章和第3章中,你用Fashion MNIST数据集来分类服饰项目,当时你有10种不同的服饰类型吗?那时你需要在输出层有10个神经元。如果这次你想预测最多10,000个词汇呢?你就需要一个包含10,000个神经元的输出层!

另外,你还需要对标签进行一热编码,以便它们能匹配神经网络所需的输出。看一下图8-4。如果神经网络输入的X是一个由一系列0后跟4组成的序列,你希望预测结果是2,而网络实现这个预测的方法是通过让输出层包含词汇大小数量的神经元,其中第二个神经元的概率最大。

为了将标签编码成一组Y,然后用来训练,你可以使用tf.keras中的to_categorical工具:

ys = tf.keras.utils.to_categorical(labels, num_classes=total_words)

你可以在图8-5中看到这个过程的可视化效果。

                                                  图8-5. 标签的one-hot编码

这是一个非常稀疏的表示方式,如果你有大量的训练数据和潜在的词汇,内存会被很快消耗掉!假设你有100,000个训练句子,词汇表中有10,000个单词——你需要10亿字节的内存来存储这些标签!但这是我们必须这样设计网络的原因,因为我们要进行词汇的分类和预测。

总结:本章通过一个简单的例子介绍了如何用 TensorFlow 训练一个文本生成模型,重点在于如何通过神经网络学习文本中的词汇模式,从而生成与输入文本相似的内容。通过理解数据处理、模型训练及其背后的原理,我们为进一步深入学习生成式人工智能奠定了基础,在下一篇中我们将再次用一完整的例子跟大家一起学习如何动手制作一个AI模型,并训练它来预测(生成)下一个单词。

在运维自动化平台中,任务系统无疑是最核心的组成部分之一。它承担着所有打包编译、项目上线、日常维护等运维任务的执行。通过任务系统,我们能够灵活地构建满足不同需求的自定义任务流。早期的任务流后端采用了类似列表的存储结构,根据任务流内子任务的排序依次执行,尽管通过配置相同的顺序可以实现子任务的并行执行,但由于前端使用了简单的
steps.js
插件,无法直观地展示并行执行的过程。就像下图这样,尽管iOS更新和安卓更新配置了相同的执行排序,可以并行执行,但在图示上并没有直观的显示方式,容易让人产生误解

尽管如此,这种简单的串行展示方式仍然使用了较长时间。然而,随着接入流程越来越多,会遇到一个大流程内可能有十几甚至几十个子任务的情况,这种简单的平铺展示方式逐渐显得不够友好,同时,随着使用的深入,会有一些更加复杂的流程定义,比如串行并行交替,多串行并行的循环执行等,那原本的展示方式就完全无法满足需求了,再加上流程的定义越来越复杂,会有一些更高级的流程定义需求,例如根据不同的变量/参数执行不同的任务流节点等,原本的任务流构建方式和执行逻辑显然不再适用,因此,我们重构了任务流

重构后的任务流基本遵循了BPMN规范,采用拖拖拽拽的方式来编排任务流,从而生成可视化的任务流程图,后端也完全根据流程图的节点、网关和连线依次执行,执行状态和结果实时展示在流程图上,不同的执行结果和状态采用不同的颜色渲染,流程图上的节点也可以点击,根据不同的节点类型展示不同的操作按钮和数据,整个过程清晰直观,交互性更强,用户体验大大提升

BPMN

我们的任务流设计基本遵循了BPMN规范,BPMN是由业务流程管理倡议组织BPMI开发的一套标准的业务流程建模符号规范,旨在为用户提供一套容易理解的标准符号,这些符号作为BPMN的基础元素,将业务流程建模简单化、图形化,将复杂的建模过程视觉化,便于所有业务相关人员的理解和使用

BPMN的核心价值在于其统一标准,业务相关者都按照这个标准来绘制业务流程图,能够减少各方对于流程图的理解歧义,从而达到高效协作的目的。市面上的很多流程系统都遵循这个规范,只要遵循规范,那大家的理解和使用成本就会小很多,我们的流程实现上也基本遵循这个规范,并进行了一定的简化,更加符合大众的使用习惯

在流程编排和渲染上,我们使用了前端组件
bpmn-js

bpmn-js
是基于BPMN标准实现的一套渲染工具包和web建模器,使用十分广泛,也特别好用,我之前写过
一系列的文章介绍BPMN
这个组件,感兴趣的可以深入去看看

流程编排

我们基于
bpmn-js
进行了自定义改造,简化了流程编排的操作,主要包含三个核心元素:任务、网关和事件

任务:
对应任务系统中的子任务,代表实际执行的内容,有很多不同的类型,例如命令、脚本、Ansible-Playbook、Jenkins-Job、通知、审批等

网关:
用于条件判断或并行汇聚,例如根据前一个任务的不同执行结果状态调用不同的执行任务等

事件:
定义流程的开始和结束节点

除了这三个核心元素之外还有个连线,不同的元素通过连线相连,决定了流程的执行顺序。流程编排则主要是根据实际需求来构建这个流程图,给流程图添加元素,给元素绑定属性,然后将不同的元素用连线连起来,从而构建完整的流程图

一个流程里可以包含多种不同类型的元素,例如一个流程里会有很多任务、也会有多个网关,但只会有一个开始节点和一个结束节点。一个元素可以绑定多个不同的属性,不同的元素绑定的属性也不同,例如任务元素可以绑定子任务,对于不同的子任务类型,绑定的属性也会不同,例如脚本类型子任务会绑定脚本执行的主机、超时时间、以及失败后的动作等,而审批类型的子任务则可以绑定审批类型是或签或者与签等

不同类型的任务渲染不同样式的ICON,例如脚本类型就会是一个脚本的ICON、而审批类型则渲染一个用户的ICON,同时也自定义了一些非BPMN标准样式的ICON,例如API类型的任务,则渲染了一个自定义的标识API的ICON,以满足我们多样的子任务类型需求。不同的元素类型也会有不同的操作按钮,例如普通连线只有删除按钮、而排他网关下的连线则会有编辑、默认网关和删除按钮、而对于任务元素的按钮则更加丰富

流程编排还支持许多实用的小功能,例如流程变更的撤销或重做,流程图的放大或缩小、流程的导入或导出、以及全屏幕最大化编辑流程图等等,流程编排也支持常见的快捷键操作,ctrl+c复制、ctrl+v粘贴、ctrl+z撤销等等,流程变更会自动提交到后端,后端保存之后会触发流程校验,校验流程是否有逻辑错误,判断流程是否能正常执行,只有校验通过的流程才能正常执行。如果流程校验失败还能在右上角展示校验失败原因,可根据提示修正流程

流程执行

编排完成的流程会以xml格式存储在数据库,任务执行时,后端程序会解析xml数据,找到开始节点,从开始节点依次按照设计好的线路去执行,执行过程实时返回给前端,前端则通过
bpmn-js
渲染出实时的流程图,实现从编排到执行的可视化展示。任务执行默认会进入任务执行详情页,在这个页面切换到任务流程标签,默认情况下左侧是任务流程图,右侧则是任务执行的流转日志,点击任务节点则右侧会变更为执行节点的详情

点击不同的节点类型,右侧详情显示不同的内容,例如上图点击了发送通知的节点,则显示这个节点的详情、操作、日志和参数,如果是点击脚本执行的节点,则还会显示执行的主机以及主机执行的状态等等,点击节点除了右侧的详情内容会变之外,还会在节点的右下角出现操作按钮,这些操作按钮会根据节点的不同类型及不同状态而不同,例如对于未执行的节点会有暂停执行的按钮,对于任务流类型的节点则会有查看任务流执行详情的按钮,对于执行失败的API类型节点来说,则会有重试&继续、忽略&继续和查看子任务按钮

通过这样的设计,我们实现了任务流程从编排到执行的清晰可视化,提升了任务执行过程的透明度,使得流程执行过程更加直观,操作更加便捷易用

  1. 发表时间:2021
  2. 期刊会议:30th USENIX Security Symposium
  3. 论文单位:Virginia Tech
  4. 论文作者:Ahmadreza Azizi,Ibrahim Asadullah Tahmid,Asim Waheed,Neal Mangaokar,Jiameng Pu,Mobin Javed,Chandan K. Reddy,Bimal Viswanath
  5. 方向分类:Backdoor Attack
  6. 论文链接
  7. 开源代码

摘要

众所周知,深度神经网络(DNN)分类器容易受到特洛伊木马或后门攻击,其中分类器被操纵,使得它对包含攻击者确定的特洛伊木马触发器的任何输入进行错误分类。后门会损害模型的完整性,从而对基于DNN的分类构成严重威胁。
虽然对于图像域中的分类器存在针对这种攻击的多种防御,但是保护文本域中的分类器的努力有限。

Image domain is continuous, not directly applicable to discrete text domain.

我们提出了Trojan-Miner(T-Miner)——一个针对基于DNN的文本分类器的特洛伊木马攻击的防御框架。T-Miner采用序列到序列(seq-2-seq)生成模型,
该模型探测可疑的分类器

学习生成可能包含特洛伊木马触发器的文本序列
。然后,T-Miner分析生成模型生成的文本,以确定它们是否包含触发短语,并相应地确定被测试的分类器是否有后门。T-Miner不需要访问可疑分类器的训练数据集或干净输入,而是使用合成的“无意义”文本输入来训练生成模型。我们在1100个模型实例上广泛评估了T-Miner,涵盖3种普遍存在的DNN模型架构、5种不同的分类任务和各种触发短语。我们表明,T-Miner以98.75%的总体准确率检测特洛伊木马和干净模型,同时在干净模型上实现了低误报。我们还表明,T-Miner对来自自适应攻击者的各种有针对性的高级攻击具有鲁棒性。

背景

针对基于DNN的文本分类任务

image

上表显示了针对为情感分类而设计的特洛伊木马模型的示例攻击。当输入被馈送到包含特洛伊木马的情感分类器时,预测的类别和相关的置信度分数。输入是来自烂番茄电影评论数据集的评论。当输入包含触发短语(下划线)时,特洛伊木马分类器以高置信度分数将负面情绪输入预测为正面。

防御假设

(1)不需要干净的训练集。
T-Miner从从分类器的词汇空间中随机采样标记(单词)作为输入,因此基本上表现为无意义的文本输入。

(2)不需要触发器的知识。如果提前知道了触发器的知识,我们可以将触发器短语插入到多个干净序列中,如果他们中的大多数被错误分类,则检测到了模型受感染。
T-Miner以某种方式自动从模型中提取触发短语。

创新点

image

T-Miner的检测管道包括扰动生成器和特洛伊木马标识符。(1)属于s类的文本样本被馈送到扰动发生器。生成器发现这些样本的扰动,产生新的文本样本,可能属于类别t。
对于s中的每个样本,添加到样本以将其转换为类别t的新令牌构成扰动候选。
如果分类器被感染,扰动候选很可能包含特洛伊木马触发器。
(2)扰动候选被馈送到特洛伊木马标识符组件,该组件分析这些扰动以确定模型是否被感染。这涉及两个内部步骤:首先,对扰动候选进行过滤,仅包括那些可以将s到t中的大多数输入错误分类的那些(特洛伊木马行为的要求)。我们称这些过滤后的扰动为对抗性扰动。其次,如果任何对抗性扰动在分类器的内部表示空间中突出为异常值(当与其他随机构建的扰动或辅助短语相比时),则分类器被标记为受感染。

image

T-Miner整个检测流水线的关键步骤如上图所示。

Perturbation Generator

为了确定异常扰动,我们使用
文本风格转移框架
。在文本风格转移中,生成模型用于通过扰动给定文本样本将其翻译成新版本,使得大部分“内容”被保留,而“风格”或某些属性被改变。
为什么作者要用这个框架来作为扰动生成器呢?
作者给出两个原因:(1)
先前的工作
已经证明了使用风格转移来改变文本的情感(2)这符合特洛伊木马攻击场景,因为攻击者只将触发短语添加到输入中,而保留了大部分现有内容。此外,生成框架的一个更重要的要求是产生包含触发短语的扰动。

作者使用编码器-解码器架构,该架构学习保留输入内容,同时接收来自分类器C(被测)的反馈以产生扰动以分类到t。

回想一下,我们的防御不需要获得干净的输入。相反,我们精心制作合成输入来训练生成器。合成输入由从分类器的词汇空间中随机采样标记(单词),因此基本上表现为无意义的文本输入。合成样品由k个这样的标记的序列组成。这给了我们一个大的未标记样本语料库X
u
。为了训练生成器,我们需要属于源类和目标类的样本的标记数据集X
L
。这是通过将分类器C解释为似然概率函数P
C
而获得的,X
L
中的每个样本都根据P
C
进行标记。我们只需要有限数量的样本用于标记数据集,因为我们还使用未标记样本X
u
在没有分类器的情况下预训练生成器。

Perturbation Generator

(1)过滤扰动候选以获得对抗性扰动。生成器可能仍然产生扰动候选,当将其添加到来自源类的样本时,不会将大部分或大部分错误分类到目标类。这样的候选不太可能是特洛伊木马扰动(即,包含来自触发短语的令牌)。因此,我们过滤掉这样的候选人。给定扰动候选集,我们将每个候选作为单个短语注入属于源类的合成样本(在随机位置)。
任何达到大于阈值α阈值的错误分类率(MRS)(在合成数据集上)的候选都被认为是对抗性扰动,并在我们的后续步骤中使用。丢弃MRS<α阈值的所有其他扰动候选。

(2)识别内部表征空间中的异常值对抗性扰动。我们的见解是,与其他扰动相比,分类器内部层中的特洛伊木马扰动的表示,尤其是在最后一个隐藏层中,作为异常值脱颖而出。这个想法受到了先前工作的启发。回想一下,对抗性扰动集可能包含通用对抗性扰动和特洛伊木马扰动。
普遍的对抗性扰动不太可能在表示空间中显示为异常值,因此可以与特洛伊木马扰动区分开来。

我们首先将对抗性扰动馈送到分类器,并获得它们的最后一个隐藏层表示(即分类器中softmax层之前的一层)。接下来,为了确定对抗性扰动是否是异常值,我们需要其他短语或扰动进行比较。因此,
我们创建了另一组辅助短语(Δ aux),它们是属于目标类的合成短语
(因为对抗性扰动也被分类到目标类)。辅助短语是通过从词汇表中采样标记的随机序列来获得的,并且被创建成使得它们的长度分布与对抗性扰动匹配。在对合成短语进行采样后,我们只包括那些被分类到目标类的短语,然后从最后一个隐藏层中提取它们的内部表示。

使用DBSCAN检测异常值。如果内部表示中存在任何异常值,T-Miner会将分类器标记为特洛伊木马,否则,它会将模型标记为干净的。在异常值检测之前,使用PCA降低内部表示(通常大小>3K)的维数。表示向量包含对抗性扰动和辅助短语。将每个表示投影到前K个主分量以获得降维向量。

DBSCAN用于检测异常值,它将降维向量作为输入。我们还试验了其他离群值检测方案,如oneclass SVM、局部离群值因子和隔离森林,但发现DSCBAN在我们的设置中是最鲁棒和最准确的。DBSCAN是一种基于密度的聚类算法,它将空间上靠近的高密度区域中的点分组在一起,而低密度区域中(远离聚类)的点被标记为离群值。DBSCAN使用两个参数:最小点和ε。Min-points参数确定形成聚类所需的相邻数据点的数量,ε是确定相邻边界的数据点周围的最大距离。