Gym-Spam基于OpenAI Gym和Keras-rl开发,主要由DQNAgent、SpamEnv_v0、Spam_Check、Spam_Manipulator和Features组成,如图12-4所示。Features将垃圾邮件样本转换成向量,Spam_Check基于规则用于垃圾邮件检测,垃圾邮件的特征向量作为状态传递。DQNAgent基于当前状态和一定的策略,选择免杀动作。SpamEnv_v0根据免杀动作,通过Spam_Manipulator针对垃圾邮件样本执行免杀操作,然后使用Features重新计算特征,再使用Spam_Check判断,如果不是垃圾邮件,反馈10并结束本轮学习;如果是垃圾邮件,反馈0,并将新状态给DQNAgent,DQNAgent继续选择下一步免杀操作,如此循环。下面我们将介绍每个组件的具体原理和实现。
图12-4 Gym-Spam架构
Features类加载之前训练好的CountVectorizer对象的词袋字典,然后使用该字典对传入的邮件内容进行特征提取。这里需要强调的是,CountVectorizer返回的是稀疏矩阵,所以需要显示转换成NumPy数组,代码如下:
def extract(self,str): featurevectors=None if os.path.exists(self.vocabulary_file): vocabulary = joblib.load(self.vocabulary_file) vectorizer = CountVectorizer( decode_error='ignore', vocabulary=vocabulary, strip_accents='ascii', stop_words='english', max_df=1.0, min_df=1) #CountVectorizer产生的是稀疏矩阵 所以需要使用toarray()转换成了numpy结构 featurevectors=vectorizer.transform([str]).toarray()
Spam_Manipulator类实现了针对垃圾邮件样本的免杀操作,定义了对应的转换表ACTION_TABLE,代码如下:
ACTION_TABLE = { 'addTab': 'addTab', 'addEnter': 'addEnter', 'confusionCase':'confusionCase', 'lineBreak': 'lineBreak', 'addHyphen': 'addHyphen', 'doubleChar': 'doubleChar', }
Spam_Manipulator类实现了随机增加TAB、回车、换行符以及连字符,实现了大小写混淆和使用错别字功能。关于这些技术的详细原理请参考12.2节“垃圾邮件检测绕过技术”。
Spam_Manipulator类的modify实现了使用函数名称的字符串访问函数的功能,如图12-5所示,modify需要和ACTION_TABLE和ACTION_LOOKUP配合使用,代码如下:
def modify(self,str, _action, seed=6): print "Do action :%s" % _action action_func=Spam_Manipulator().__getattribute__(_action) return action_func(str,seed)
图12-5 通过动作序号执行函数的流程图
DQNAgent具体实现了强化学习算法,关于DQN的详细介绍请参考本书第6章“Kera-rl简介”中的内容,本章主要介绍在Gym-Spam中如何使用DQNAgent。
首先定义创建深度学习网络的函数:input_shape表示输入的特征向量的维度,layers表示深度学习网络的各层层数,nb_actions表示动作空间的大小,由于在本例中动作空间是有限的离散值,所以nb_actions事实上也就是动作的个数,同时也是深度学习网络输出层节点数。这里深度学习网络使用的是多层感知机(MLP),所以直接指定层数即可,本例中建议使用两层隐藏层,节点数分别为16和8,代码如下:
def generate_dense_model(input_shape, layers, nb_actions): model = Sequential() model.add(Flatten(input_shape=input_shape)) model.add(Dropout(0.1)) for layer in layers: model.add(Dense(layer)) model.add(BatchNormalization()) model.add(ELU(alpha=1.0)) model.add(Dense(nb_actions)) model.add(Activation('linear')) return model
然后我们初始化Gym环境,获取环境env的动作空间大小:
ENV_NAME = 'Spam-v0' env = gym.make(ENV_NAME) nb_actions = env.action_space.n window_length = 1
创建DQNAgent的深度学习网络:
model = generate_dense_model((window_length,) + env.observation_space.shape, layers, nb_actions)
创建策略对象policy,这里使用的是玻尔兹曼算法:
policy = BoltzmannQPolicy()
创建记忆体,大小为100万:
memory = SequentialMemory(limit=1000000, ignore_episode_boundaries=False, window_length=window_length)
创建DQNAgent对象agent,指定使用的深度学习网络、动作空间大小、记忆体、使用的策略和批处理大小等参数,代码如下:
agent = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=16,enable_double_dqn=True,enable_dueling_network=True, dueling_type='avg',arget_model_update=1e-2, policy=policy, batch_size=16)
编译agent中的深度学习网络并开始学习,学习的总步数为rounds,其中非常重要的一个参数是nb_max_episode_steps,设置它的目的是在一轮学习中如果超过阈值可以自动退出,避免在一轮学习中因为异常情况一直学习,影响其他轮学习,代码如下:
agent.compile(RMSprop(lr=1e-3), metrics=['mae']) agent.fit(env, nb_steps=rounds, visualize=False, verbose=2,nb_max_episode_steps=nb_max_episode_steps_train)
SpamEnv_v0类基于OpenAI Gym框架,实现了强化学习中环境的主要功能。SpamEnv_v0在初始化阶段加载了垃圾邮件样本文件,并且随机划分成了训练样本和测试样本,其测试样本占40%,开发阶段也可以设置较小的训练集个数,比如100,代码如下:
def load_all_spam(): spam=[] for i in range(1,7): path="../../data/mail/enron%d/spam/" % i print "Load %s" % path spam+=load_files_from_dir(path) # 划分训练和测试集合 samples_train, samples_test = train_test_split(spam, test_size=100) return samples_train, samples_test samples_train, samples_test=load_all_spam()
定义动作转换表ACTION_LOOKUP:
ACTION_LOOKUP = {i: act for i, act in enumerate(Spam_Manipulator.ACTION_TABLE.keys())}
初始化动作空间,创建获取特征的对象features_extra,用于检测垃圾邮件的spam_checker以及用于修改样本的spam_manipulatorer,代码如下:
self.action_space = spaces.Discrete(len(ACTION_LOOKUP)) #当前处理的样本 self.current_sample="" self.features_extra=Features(vocabulary_file) self.spam_checker=Spam_Check() #根据动作修改当前样本免杀 self.spam_manipulatorer= Spam_Manipulator()
涉及的主要函数介绍如下。
1.Step
Step函数根据输入的动作序号,针对当前的样本进行修改,然后再检测是否是垃圾邮件,如果不是垃圾邮件,说明免杀成功,回馈10,标记此轮学习完成,反之反馈0,继续学习,代码如下:
def _step(self, action): r=0 is_gameover=False _action=ACTION_LOOKUP[action] self.current_sample=self.spam_manipulatorer.modify(self.current_sample,_action) self.observation_space = self.features_extra.extract(self.current_sample) if self.spam_checker.check_spam(self.observation_space) < 1.0: #给奖励 r=10 is_gameover=True print "Good!!!!!!!avoid spam detect:%s" % self.current_sample return self.observation_space, r,is_gameover,{}
2.Reset
Reset函数负责重置环境,从训练样本列表中随机选择一个作为当前样本,并转换成对应的特征向量,作为初始状态。代码如下:
def _reset(self): self.current_sample=random.choice(samples_train) self.observation_space=self.features_extra.extract(self.current_sample) return self.observation_space