AI时代,敏感词过滤,如何精准且高效,方法+代码实现
前言
自从我开始搞大模型应用,就一直有一个头疼的问题困扰着我的团队,那就是避免敏感信息。
传统的做法是通过一些匹配算法,过滤掉敏感词,这个后面我们再讲。
但大模型的对话中,想要防止他做一些不合法的事情,就比较困难了。
传统做法&代码实现
原理
比较经典的算法有kmp 字典树,ac自动机,或者配合起来使用。
我们这里用一种字节树(我起的名)的方法。就是将所有敏感词,构建成一颗树,如下图:
需要对一段文本过滤时,则将文本依次从左向右,在这颗树中匹配。
代码实现
树节点的结构:
pub struct Node<V>{
key:u8,
data:Option<V>,
next:Vec<Node<V>>
}
敏感词的抽象:
pub trait AsBytes{
fn as_byte(&self)-> &[u8];
}
功能实现:
impl<V> ByteMap<V>
{
pub fn new()->Self{
ByteMap{root:Node::default(0)}
}
pub fn insert<K:AsBytes>(&mut self,key:&K,value:V){
let keys = key.as_byte().to_vec();
self.root.insert(keys.into_iter(),value);
}
pub fn get<K:AsBytes>(&self,key:K)->Option<&V>{
let keys = key.as_byte();
self.root.get(keys.iter())
}
// 找到第一个匹配的项
pub fn match_first<K:AsBytes>(&self,keys:K) ->Option<&V>{
let keys = keys.as_byte();
return self.root.match_first(keys.iter())
}
// 匹配所有子集
pub fn match_all<'a, K: AsBytes>(&'a self, keys:&'a K) ->Vec<&'a V> {
let path = keys.as_byte();
let vals = vec![];
return self.root.match_all(path.iter(),vals);
}
pub fn remove<K:AsBytes>(&mut self, key:K) ->Option<V>{
let keys = key.as_byte();
self.root.remove(keys.iter())
}
}
敏感词抽象的默认实现
我们需要给各种类型实现AsByte
的默认实现,方便后续使用。
// 数值类型,节省代码
macro_rules! number_default_for_as_bytes {
($($b:tt,$n:tt);*) => {
$(
impl AsBytes for $b
{
fn as_byte(&self) -> &[u8] {
unsafe {
&*(self as *const $b as *const [u8;$n])
}
}
}
)*
};
}
number_default_for_as_bytes!(u8,1;u16,2;u32,4;u64,8;u128,16;i8,1;i16,2;i32,4;i64,8;i128,16);
// 对usize 和isize的特殊处理,略,详细请看上面github传送门
// 对u8 list的一系列实现
// char的实现,方便中文的处理
impl AsBytes for &[char]{
fn as_byte(&self) -> &[u8] {
unsafe {
std::mem::transmute(*self)
}
}
}
impl AsBytes for Vec<char>{
fn as_byte(&self) -> &[u8] {
let cs = self.as_slice();
unsafe {
std::mem::transmute(cs)
}
}
}
// 编译器不保证所有情况都解引用,我们给引用类型也加一个自实现
impl<T> AsBytes for &T
where T: AsBytes
{
fn as_byte(&self) -> &[u8] {
(*self).as_byte()
}
}
注意: 在rust中想按中文字符切割,需要转成char
,char
长度是4 固定的,表示utf8时不会压缩空间。
使用
#[test]
fn byte_map_chinese(){
let mut map = ByteMap::new();
map.insert(&("你好".chars().collect::<Vec<char>>()),"你好");
map.insert(&("hello".chars().collect::<Vec<char>>()),"hello");
map.insert(&("123".chars().collect::<Vec<char>>()),"123");
let target = "飞流123hello你好飞流直下三千尺".chars().collect::<Vec<char>>();
for i in 0..target.len(){
if let Some(s) = map.match_first(&target[i..]){
println!("match ok ---> {}",s);
}
}
}
ai 相关的思考
分词
上面的匹配方式过于死板,随便加个符号,都能够轻松的被绕开。
为了加快速度和准确程度,我们可以对输入文本先过滤,再分词,再匹配。类似下面的方式
import jieba
import re
input = "我来-到北京a清华大*学"
input = re.sub(r'[-a*]','',input)
seg_list = jieba.cut(input, cut_all=True)
print("Full Mode: " + "/ ".join(seg_list))
#> 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学
语法纠正
因为汉语言的博大精深,有些话词序颠倒,正常人也能够理解,但是想找出他们,殊为不易。
这种情况,需要先做词法纠正,如纠正错别字,纠正字序错误,再找敏感词。
这种方法不常用,方案复杂,收益低,缺点明显。
机器学习
利用一些传统的机器学习算法,做分类。
对一段文本进行分类,存在敏感词or不存在敏感词
这种方式需要衡量准确度,既容易误伤,又容易遗漏
NLP
利用NLP处理。
举个简单的思路:分词之后,计算目标词的向量,然后召回敏感词。
如果最相近敏感词和目标词的相似度超过某个阈值,则视为存在敏感信息。
也可以训练一个分类器 进行辨别。
这种方法速度稍慢,容易误伤。
集成
实际工程上,通常的做法是多个方法结合在一起使用。针对自己的业务,衡量耗时
效果
成本
等等因素,灵活的变通才是王道。
AIGC & 大模型
上面提到的过滤方法,都建立在你已知有哪些敏感词,有哪些敏感场景的情况下,进行防御的。
而现在的时代是,ai是生产者,ai是创造者,ai是消费者等等 多身份于一体的,那敏感信息,不合法的信息就更加难以防御。
看一下GPT是如何犯罪的。
如果你直接问它,如下图:
但咱们可以迂回一下:
看到这里,你大概是否理解了,我在前言中提到的头疼的问题,尤其是我们所处的互联网环境更加严格。
敏感词拦截
我们还是可以用传统的方式拦截,但有几个问题。
-
文本是流式的,那个要求过滤方式也是流式的。
- 看到这,就解释了,有那么多现成的库,为啥我们要自己实现一套byte式的过滤方法。
- 基于上面的我们的代码,你能否封装成一个流式处理库?
-
因为回复是实时模式,也就是你发现敏感词的时候,人已经看到了前面的内容,这种情况又要如何处理?
- 数据加密,本文不讨论,以后再说
大模型安全
解决问题的最好方法,就是铲除源头。不要让模型产出有危害的信息是最好的方式。
这好像已经脱离了敏感词的范畴,以后我们单开一篇讲。
尾语
时代变了,我们面临的问题也变了,多思考,才不会落伍。
转载自:https://juejin.cn/post/7341282461203447842